openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
332 lines (331 loc) • 14.9 kB
JavaScript
import { c as isRecord } from "./record-coerce-DItp3I4t.js";
import { i as normalizeBoundedOptionalString } from "./string-coerce-CIXf7egm.js";
import { k as withTimeout } from "./fs-safe-B6pvPGnf.js";
import "./string-coerce-runtime-GQa0ehRA.js";
import "./security-runtime-Ckf0kc0h.js";
import { t as publishSessionCatalogHost } from "./session-catalog-C5rciJKU.js";
import { t as resolveClaudeCatalogHomeDir } from "./session-catalog-home-DC4c4SgQ.js";
import { n as CLAUDE_SESSIONS_LIST_COMMAND, o as ClaudeCatalogParamsError, r as CLAUDE_SESSION_READ_COMMAND } from "./session-catalog-shared-CiyzxK6Y.js";
import { a as configuredClaudeConfigDir, s as gatewayClaudeScanOptions } from "./session-catalog-scan-4wxzSKEc.js";
import { t as CLAUDE_LOCAL_SESSION_HOST_ID } from "./session-catalog-adoption-C3d_naEs.js";
import { n as parseTranscriptLine } from "./session-catalog-transcript-C6d689--.js";
import { t as listClaudeSessions } from "./session-catalog-discovery-DrNiZpI6.js";
import { n as resolveNodeLabel, t as createNodeListFailedError } from "./session-catalog-node-helpers-Bb_Ro2Ey.js";
import { a as encodeOffset, c as readListParams, d as readTranscriptParams, f as unwrapNodePayload, i as decodeOffset, l as readNodePageCursor, o as parseCatalogPage, s as parseGatewayQuery, u as readOptionalCursor } from "./session-catalog-parsing-l2H-NE-8.js";
import { t as claudeNodeTerminalCapability } from "./session-catalog-terminal-DYvGl7EN.js";
import fs from "node:fs/promises";
//#region extensions/anthropic/session-catalog-listing.ts
const TRANSCRIPT_READ_CHUNK_BYTES = 131072;
const MAX_TRANSCRIPT_SCAN_BYTES = 67108864;
const MAX_TRANSCRIPT_PAGE_BYTES = 20971520;
const NODE_INVOKE_TIMEOUT_MS = 3e4;
const NODE_CATALOG_LIST_RESPONSE_TIMEOUT_MS = 2e4;
const CLAUDE_HISTORY_IMPORT_MAX_ITEMS = 200;
const CLAUDE_HISTORY_IMPORT_MAX_BYTES = 524288;
async function listLocalClaudeSessionPage(value, homeDir, scanOptions) {
const resolvedHome = homeDir ?? resolveClaudeCatalogHomeDir();
const resolvedScanOptions = scanOptions ?? (homeDir === void 0 ? gatewayClaudeScanOptions(true) : {});
const params = readListParams(value);
const offset = decodeOffset(params.cursor, "catalog");
const search = params.searchTerm?.toLocaleLowerCase();
const records = (await listClaudeSessions(resolvedHome, resolvedScanOptions)).filter((record) => {
if (!search) return true;
return [
record.name,
record.cwd,
record.gitBranch,
record.threadId
].some((candidate) => candidate?.toLocaleLowerCase().includes(search));
});
const page = records.slice(offset, offset + params.limit).map(({ filePath: _filePath, ...record }) => record);
const nextOffset = offset + page.length;
return {
sessions: page,
...nextOffset < records.length ? { nextCursor: encodeOffset(nextOffset) } : {}
};
}
async function readLocalClaudeTranscriptPage(value, homeDir, scanOptions) {
const resolvedHome = homeDir ?? resolveClaudeCatalogHomeDir();
const resolvedScanOptions = scanOptions ?? (homeDir === void 0 ? gatewayClaudeScanOptions(true) : {});
const params = readTranscriptParams(value);
let filePath = (await listClaudeSessions(resolvedHome, resolvedScanOptions)).find((record) => record.threadId === params.threadId)?.filePath;
if (!filePath) filePath = (await listClaudeSessions(resolvedHome, {
...resolvedScanOptions,
forceRefresh: true
})).find((record) => record.threadId === params.threadId)?.filePath;
if (!filePath) throw new ClaudeCatalogParamsError("Claude session is unavailable");
const handle = await fs.open(filePath, "r");
try {
const stat = await handle.stat();
const requestedEnd = params.cursor ? decodeOffset(params.cursor, "transcript") : stat.size;
if (requestedEnd > stat.size) throw new ClaudeCatalogParamsError("transcript cursor is invalid");
let position = requestedEnd;
let scanned = 0;
let fragments = [];
const found = [];
while (position > 0 && scanned < MAX_TRANSCRIPT_SCAN_BYTES && found.length <= params.limit) {
const size = Math.min(TRANSCRIPT_READ_CHUNK_BYTES, position, MAX_TRANSCRIPT_SCAN_BYTES - scanned);
position -= size;
const chunk = Buffer.allocUnsafe(size);
let filled = 0;
while (filled < size) {
const { bytesRead } = await handle.read(chunk, filled, size - filled, position + filled);
if (bytesRead === 0) throw new Error("Claude transcript changed while it was being read");
filled += bytesRead;
}
scanned += filled;
let right = filled;
for (let index = filled - 1; index >= 0; index -= 1) {
if (chunk[index] !== 10) continue;
const segment = chunk.subarray(index + 1, right);
if (segment.length > 0 || fragments.length > 0) {
const line = Buffer.concat([segment, ...fragments.toReversed()]);
const item = parseTranscriptLine(line, normalizeBoundedOptionalString);
fragments = [];
if (item) {
item.resumeCursor = encodeOffset(position + index + 1 + line.length);
found.push({
item,
start: position + index + 1
});
if (found.length > params.limit) break;
}
}
right = index;
}
if (found.length > params.limit) break;
const prefix = chunk.subarray(0, right);
if (position === 0) {
if (prefix.length > 0 || fragments.length > 0) {
const line = Buffer.concat([prefix, ...fragments.toReversed()]);
const item = parseTranscriptLine(line, normalizeBoundedOptionalString);
if (item) {
item.resumeCursor = encodeOffset(line.length);
found.push({
item,
start: 0
});
}
}
fragments = [];
} else if (prefix.length > 0) fragments.push(prefix);
}
if (position > 0 && found.length < params.limit) throw new Error("Claude transcript page exceeded the safe scan limit");
const requested = found.slice(0, params.limit);
const selected = [];
let selectedBytes = 0;
for (const entry of requested) {
const itemBytes = Buffer.byteLength(JSON.stringify(entry.item), "utf8");
if (selected.length > 0 && selectedBytes + itemBytes > 20905984) break;
selected.push(entry);
selectedBytes += itemBytes;
}
const earliestStart = selected.at(-1)?.start;
const hasEarlierItems = selected.length < found.length || position > 0;
return {
threadId: params.threadId,
items: selected.map((entry) => entry.item),
...hasEarlierItems && earliestStart !== void 0 && earliestStart > 0 ? { nextCursor: encodeOffset(earliestStart) } : {}
};
} finally {
await handle.close();
}
}
async function listClaudeSessionCatalog(params) {
const query = parseGatewayQuery(params.query);
const requested = query.hostIds ? new Set(query.hostIds) : void 0;
const scanOptions = gatewayClaudeScanOptions(params.allowProcessHomeFallback);
const localHosts = (params.allowProcessHomeFallback !== false || scanOptions.configDir !== void 0) && (!requested || requested.has("gateway:local")) ? [(async () => {
try {
return {
hostId: CLAUDE_LOCAL_SESSION_HOST_ID,
label: "Local Claude",
kind: "gateway",
connected: true,
...await listLocalClaudeSessionPage({
limit: query.limitPerHost,
...query.search ? { searchTerm: query.search } : {},
...query.cursors?.["gateway:local"] !== void 0 ? { cursor: query.cursors[CLAUDE_LOCAL_SESSION_HOST_ID] } : {}
}, resolveClaudeCatalogHomeDir(), scanOptions)
};
} catch {
return {
hostId: CLAUDE_LOCAL_SESSION_HOST_ID,
label: "Local Claude",
kind: "gateway",
connected: true,
sessions: [],
error: {
code: "LOCAL_READ_FAILED",
message: "Local Claude sessions are unavailable"
}
};
}
})()] : [];
for (const host of localHosts) publishSessionCatalogHost(params, host);
if (!(!requested || query.hostIds?.some((hostId) => hostId.startsWith("node:")))) return { hosts: await Promise.all(localHosts) };
let nodes;
try {
nodes = (await (params.listNodes?.() ?? params.runtime.nodes.list())).nodes;
} catch (error) {
const registryHost = {
hostId: "node:registry",
label: "Paired nodes",
kind: "node",
connected: false,
canStartTerminal: false,
sessions: [],
error: createNodeListFailedError(error)
};
params.onHost?.(registryHost);
return { hosts: [...await Promise.all(localHosts), registryHost] };
}
params.signal?.throwIfAborted();
const eligible = nodes.filter((node) => node.gatewayLocal !== true && (node.commands?.includes("anthropic.claude.sessions.list.v1") || claudeNodeTerminalCapability(node).canStartTerminal) && (!requested || requested.has(`node:${node.nodeId}`))).slice(0, 100 - localHosts.length).toSorted((left, right) => resolveNodeLabel(left).localeCompare(resolveNodeLabel(right)));
const nodeHosts = await Promise.all(eligible.map(async (node) => {
const hostId = `node:${node.nodeId}`;
const { canOpenTerminalClaude, canStartTerminal } = claudeNodeTerminalCapability(node);
const common = {
hostId,
label: resolveNodeLabel(node),
kind: "node",
connected: node.connected === true,
nodeId: node.nodeId,
canContinueClaude: node.commands?.includes("anthropic.claude.sessions.read.v1") === true && node.commands.includes("agent.cli.claude.run.v1") && node.invocableCommands?.includes("anthropic.claude.sessions.list.v1") === true && node.invocableCommands.includes("anthropic.claude.sessions.read.v1") && node.invocableCommands.includes("agent.cli.claude.run.v1"),
canOpenTerminalClaude,
canStartTerminal,
sessions: []
};
if (node.connected !== true) {
const host = Object.assign({}, common, { error: {
code: "NODE_OFFLINE",
message: "Paired node is offline"
} });
params.onHost?.(host);
return host;
}
if (!node.commands?.includes("anthropic.claude.sessions.list.v1")) {
params.onHost?.(common);
return common;
}
const eventualHost = Promise.resolve().then(async () => {
const raw = await params.runtime.nodes.invoke({
nodeId: node.nodeId,
command: CLAUDE_SESSIONS_LIST_COMMAND,
params: {
limit: query.limitPerHost,
...query.search ? { searchTerm: query.search } : {},
...query.cursors?.[hostId] !== void 0 ? { cursor: query.cursors[hostId] } : {}
},
timeoutMs: NODE_INVOKE_TIMEOUT_MS,
scopes: ["operator.write"],
signal: params.signal
});
return Object.assign({}, common, parseCatalogPage(unwrapNodePayload(raw)));
}).catch(() => Object.assign({}, common, { error: {
code: "NODE_INVOKE_FAILED",
message: "Paired node Claude sessions are unavailable"
} }));
publishSessionCatalogHost(params, eventualHost);
try {
return await withTimeout(eventualHost, NODE_CATALOG_LIST_RESPONSE_TIMEOUT_MS, { message: "paired node Claude session catalog timed out" });
} catch {
return Object.assign({}, common, { error: {
code: "NODE_INVOKE_FAILED",
message: "Paired node Claude sessions are unavailable"
} });
}
}));
return { hosts: [...await Promise.all(localHosts), ...nodeHosts] };
}
async function readClaudeSessionTranscript(params) {
const cursor = readOptionalCursor(params.cursor, "transcript");
if (params.hostId === "gateway:local") {
assertClaudeLocalAccess(params.hostId, params.allowProcessHomeFallback);
return {
hostId: params.hostId,
label: "Local Claude",
...await readLocalClaudeTranscriptPage({
threadId: params.threadId,
limit: params.limit,
...cursor !== void 0 ? { cursor } : {}
}, resolveClaudeCatalogHomeDir(), gatewayClaudeScanOptions(params.allowProcessHomeFallback))
};
}
if (!params.hostId.startsWith("node:")) throw new ClaudeCatalogParamsError("hostId is invalid");
const nodeId = params.hostId.slice(5);
const node = (await params.runtime.nodes.list()).nodes.find((candidate) => candidate.nodeId === nodeId && candidate.connected === true && candidate.commands?.includes("anthropic.claude.sessions.read.v1"));
if (!node) throw new ClaudeCatalogParamsError("paired-node Claude session host is unavailable");
const raw = await params.runtime.nodes.invoke({
nodeId,
command: CLAUDE_SESSION_READ_COMMAND,
params: {
threadId: params.threadId,
limit: params.limit,
...cursor !== void 0 ? { cursor } : {}
},
timeoutMs: NODE_INVOKE_TIMEOUT_MS,
scopes: ["operator.write"]
});
const page = unwrapNodePayload(raw);
if (!isRecord(page) || !Array.isArray(page.items) || page.items.length > 50 || page.items.some((item) => !isRecord(item) || typeof item.type !== "string") || page.threadId !== params.threadId || Buffer.byteLength(JSON.stringify(page), "utf8") > MAX_TRANSCRIPT_PAGE_BYTES) throw new Error("Claude node returned an invalid transcript page");
const nextCursor = readNodePageCursor(page, "Claude node returned an invalid transcript page");
return {
hostId: params.hostId,
label: resolveNodeLabel(node),
threadId: params.threadId,
items: page.items,
...nextCursor !== void 0 ? { nextCursor } : {}
};
}
function assertClaudeLocalAccess(hostId, allowProcessHomeFallback) {
if (hostId === "gateway:local" && allowProcessHomeFallback === false && configuredClaudeConfigDir() === void 0) throw new ClaudeCatalogParamsError("local Claude sessions are unavailable in isolated state");
}
async function readBoundedClaudeHistory(params) {
const items = [];
let cursor;
let bytes = 0;
while (items.length < CLAUDE_HISTORY_IMPORT_MAX_ITEMS) {
const page = await readClaudeSessionTranscript({
runtime: params.runtime,
hostId: params.hostId,
threadId: params.threadId,
limit: Math.min(50, CLAUDE_HISTORY_IMPORT_MAX_ITEMS - items.length),
allowProcessHomeFallback: params.allowProcessHomeFallback,
...cursor ? { cursor } : {}
});
for (const item of page.items) {
const itemBytes = Buffer.byteLength(JSON.stringify(item), "utf8");
if (items.length > 0 && bytes + itemBytes > CLAUDE_HISTORY_IMPORT_MAX_BYTES) return items;
items.push(item);
bytes += itemBytes;
}
if (!page.nextCursor || page.nextCursor === cursor) break;
cursor = page.nextCursor;
}
return items;
}
async function resolveNodeClaudeRecord(params) {
let cursor;
for (let pageIndex = 0; pageIndex < 100; pageIndex += 1) {
const raw = await params.runtime.nodes.invoke({
nodeId: params.nodeId,
command: CLAUDE_SESSIONS_LIST_COMMAND,
params: {
limit: 100,
searchTerm: params.threadId,
...cursor ? { cursor } : {}
},
timeoutMs: NODE_INVOKE_TIMEOUT_MS,
scopes: ["operator.write"]
});
const page = parseCatalogPage(unwrapNodePayload(raw));
const record = page.sessions.find((candidate) => candidate.threadId === params.threadId);
if (record) return record;
if (!page.nextCursor || page.nextCursor === cursor) break;
cursor = page.nextCursor;
}
throw new ClaudeCatalogParamsError("Claude session is unavailable on the paired node");
}
//#endregion
export { readClaudeSessionTranscript as a, readBoundedClaudeHistory as i, listClaudeSessionCatalog as n, readLocalClaudeTranscriptPage as o, listLocalClaudeSessionPage as r, resolveNodeClaudeRecord as s, assertClaudeLocalAccess as t };