@ashx-j/lunr 0.2.2 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/builtin-extensions/ashxj-thinking.d.ts +1 -0
- package/dist/builtin-extensions/ashxj-thinking.d.ts.map +1 -1
- package/dist/builtin-extensions/ashxj-thinking.js +19 -7
- package/dist/builtin-extensions/ashxj-thinking.js.map +1 -1
- package/dist/builtin-extensions/pi-web-access/index.d.ts.map +1 -1
- package/dist/builtin-extensions/pi-web-access/index.js +18 -13
- package/dist/builtin-extensions/pi-web-access/index.js.map +1 -1
- package/dist/builtin-extensions/pi-web-access/render-search-chrome.d.ts +2 -0
- package/dist/builtin-extensions/pi-web-access/render-search-chrome.d.ts.map +1 -1
- package/dist/builtin-extensions/pi-web-access/render-search-chrome.js +12 -0
- package/dist/builtin-extensions/pi-web-access/render-search-chrome.js.map +1 -1
- package/dist/core/extensions/types.d.ts +4 -0
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/tools/bash.d.ts.map +1 -1
- package/dist/core/tools/bash.js +14 -5
- package/dist/core/tools/bash.js.map +1 -1
- package/dist/core/tools/edit.d.ts.map +1 -1
- package/dist/core/tools/edit.js +13 -7
- package/dist/core/tools/edit.js.map +1 -1
- package/dist/core/tools/find.d.ts.map +1 -1
- package/dist/core/tools/find.js +10 -5
- package/dist/core/tools/find.js.map +1 -1
- package/dist/core/tools/grep.d.ts.map +1 -1
- package/dist/core/tools/grep.js +10 -5
- package/dist/core/tools/grep.js.map +1 -1
- package/dist/core/tools/ls.d.ts.map +1 -1
- package/dist/core/tools/ls.js +10 -3
- package/dist/core/tools/ls.js.map +1 -1
- package/dist/core/tools/read.d.ts.map +1 -1
- package/dist/core/tools/read.js +24 -15
- package/dist/core/tools/read.js.map +1 -1
- package/dist/core/tools/render-utils.d.ts +21 -0
- package/dist/core/tools/render-utils.d.ts.map +1 -1
- package/dist/core/tools/render-utils.js +32 -0
- package/dist/core/tools/render-utils.js.map +1 -1
- package/dist/core/tools/write.d.ts.map +1 -1
- package/dist/core/tools/write.js +19 -4
- package/dist/core/tools/write.js.map +1 -1
- package/dist/gateway/commands.d.ts.map +1 -1
- package/dist/gateway/commands.js +1 -0
- package/dist/gateway/commands.js.map +1 -1
- package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/settings-selector.js +5 -5
- package/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/dist/modes/interactive/components/thinking-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/thinking-selector.js +5 -5
- package/dist/modes/interactive/components/thinking-selector.js.map +1 -1
- package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/tool-execution.js +16 -4
- package/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/docs/keybindings.md +1 -1
- package/docs/quickstart.md +1 -1
- package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/gondolin/package-lock.json +2 -2
- package/examples/extensions/gondolin/package.json +1 -1
- package/examples/extensions/sandbox/package-lock.json +2 -2
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/package.json +4 -4
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"write.d.ts","sourceRoot":"","sources":["../../../src/core/tools/write.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,+BAA+B,CAAC;AAI/D,OAAO,EAAE,KAAK,MAAM,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAG5C,OAAO,KAAK,EAAE,cAAc,EAA2B,MAAM,wBAAwB,CAAC;AAatF,QAAA,MAAM,WAAW;;;EAGf,CAAC;AAEH,MAAM,MAAM,cAAc,GAAG,MAAM,CAAC,OAAO,WAAW,CAAC,CAAC;AAExD;;;GAGG;AACH,MAAM,WAAW,eAAe;IAC/B,8BAA8B;IAC9B,SAAS,EAAE,CAAC,YAAY,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACpE,mCAAmC;IACnC,KAAK,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACtC;AAOD,MAAM,WAAW,gBAAgB;IAChC,oEAAoE;IACpE,UAAU,CAAC,EAAE,eAAe,CAAC;CAC7B;AAgJD,wBAAgB,yBAAyB,CACxC,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,gBAAgB,GACxB,cAAc,CAAC,OAAO,WAAW,EAAE,SAAS,CAAC,CAiF/C;AAED,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,gBAAgB,GAAG,SAAS,CAAC,OAAO,WAAW,CAAC,CAEtG","sourcesContent":["import type { AgentTool } from \"@ashx-j/lunr-agent\";\nimport { Container, Text } from \"@ashx-j/lunr-tui\";\nimport { mkdir as fsMkdir, writeFile as fsWriteFile } from \"fs/promises\";\nimport { dirname } from \"path\";\nimport { type Static, Type } from \"typebox\";\nimport { keyHint } from \"../../modes/interactive/components/keybinding-hints.ts\";\nimport { getLanguageFromPath, highlightCode, type Theme } from \"../../modes/interactive/theme/theme.ts\";\nimport type { ToolDefinition, ToolRenderResultOptions } from \"../extensions/types.ts\";\nimport { withFileMutationQueue } from \"./file-mutation-queue.ts\";\nimport { resolveToCwd } from \"./path-utils.ts\";\nimport {\n\tnormalizeDisplayText,\n\trenderToolFileName,\n\trenderToolPath,\n\treplaceTabs,\n\tstr,\n\ttoolStatusDotFromContext,\n} from \"./render-utils.ts\";\nimport { wrapToolDefinition } from \"./tool-definition-wrapper.ts\";\n\nconst writeSchema = Type.Object({\n\tpath: Type.String({ description: \"Path to the file to write (relative or absolute)\" }),\n\tcontent: Type.String({ description: \"Content to write to the file\" }),\n});\n\nexport type WriteToolInput = Static<typeof writeSchema>;\n\n/**\n * Pluggable operations for the write tool.\n * Override these to delegate file writing to remote systems (for example SSH).\n */\nexport interface WriteOperations {\n\t/** Write content to a file */\n\twriteFile: (absolutePath: string, content: string) => Promise<void>;\n\t/** Create directory recursively */\n\tmkdir: (dir: string) => Promise<void>;\n}\n\nconst defaultWriteOperations: WriteOperations = {\n\twriteFile: (path, content) => fsWriteFile(path, content, \"utf-8\"),\n\tmkdir: (dir) => fsMkdir(dir, { recursive: true }).then(() => {}),\n};\n\nexport interface WriteToolOptions {\n\t/** Custom operations for file writing. Default: local filesystem */\n\toperations?: WriteOperations;\n}\n\ntype WriteHighlightCache = {\n\trawPath: string | null;\n\tlang: string;\n\trawContent: string;\n\tnormalizedLines: string[];\n\thighlightedLines: string[];\n};\n\nclass WriteCallRenderComponent extends Text {\n\tcache?: WriteHighlightCache;\n\n\tconstructor() {\n\t\tsuper(\"\", 0, 0);\n\t}\n}\n\nconst WRITE_PARTIAL_FULL_HIGHLIGHT_LINES = 50;\n\nfunction highlightSingleLine(line: string, lang: string): string {\n\tconst highlighted = highlightCode(line, lang);\n\treturn highlighted[0] ?? \"\";\n}\n\nfunction refreshWriteHighlightPrefix(cache: WriteHighlightCache): void {\n\tconst prefixCount = Math.min(WRITE_PARTIAL_FULL_HIGHLIGHT_LINES, cache.normalizedLines.length);\n\tif (prefixCount === 0) return;\n\tconst prefixSource = cache.normalizedLines.slice(0, prefixCount).join(\"\\n\");\n\tconst prefixHighlighted = highlightCode(prefixSource, cache.lang);\n\tfor (let i = 0; i < prefixCount; i++) {\n\t\tcache.highlightedLines[i] =\n\t\t\tprefixHighlighted[i] ?? highlightSingleLine(cache.normalizedLines[i] ?? \"\", cache.lang);\n\t}\n}\n\nfunction rebuildWriteHighlightCacheFull(rawPath: string | null, fileContent: string): WriteHighlightCache | undefined {\n\tconst lang = rawPath ? getLanguageFromPath(rawPath) : undefined;\n\tif (!lang) return undefined;\n\tconst displayContent = normalizeDisplayText(fileContent);\n\tconst normalized = replaceTabs(displayContent);\n\treturn {\n\t\trawPath,\n\t\tlang,\n\t\trawContent: fileContent,\n\t\tnormalizedLines: normalized.split(\"\\n\"),\n\t\thighlightedLines: highlightCode(normalized, lang),\n\t};\n}\n\nfunction updateWriteHighlightCacheIncremental(\n\tcache: WriteHighlightCache | undefined,\n\trawPath: string | null,\n\tfileContent: string,\n): WriteHighlightCache | undefined {\n\tconst lang = rawPath ? getLanguageFromPath(rawPath) : undefined;\n\tif (!lang) return undefined;\n\tif (!cache) return rebuildWriteHighlightCacheFull(rawPath, fileContent);\n\tif (cache.lang !== lang || cache.rawPath !== rawPath) return rebuildWriteHighlightCacheFull(rawPath, fileContent);\n\tif (!fileContent.startsWith(cache.rawContent)) return rebuildWriteHighlightCacheFull(rawPath, fileContent);\n\tif (fileContent.length === cache.rawContent.length) return cache;\n\n\tconst deltaRaw = fileContent.slice(cache.rawContent.length);\n\tconst deltaDisplay = normalizeDisplayText(deltaRaw);\n\tconst deltaNormalized = replaceTabs(deltaDisplay);\n\tcache.rawContent = fileContent;\n\tif (cache.normalizedLines.length === 0) {\n\t\tcache.normalizedLines.push(\"\");\n\t\tcache.highlightedLines.push(\"\");\n\t}\n\n\tconst segments = deltaNormalized.split(\"\\n\");\n\tconst lastIndex = cache.normalizedLines.length - 1;\n\tcache.normalizedLines[lastIndex] += segments[0];\n\tcache.highlightedLines[lastIndex] = highlightSingleLine(cache.normalizedLines[lastIndex], cache.lang);\n\tfor (let i = 1; i < segments.length; i++) {\n\t\tcache.normalizedLines.push(segments[i]);\n\t\tcache.highlightedLines.push(highlightSingleLine(segments[i], cache.lang));\n\t}\n\trefreshWriteHighlightPrefix(cache);\n\treturn cache;\n}\n\nfunction trimTrailingEmptyLines(lines: string[]): string[] {\n\tlet end = lines.length;\n\twhile (end > 0 && lines[end - 1] === \"\") {\n\t\tend--;\n\t}\n\treturn lines.slice(0, end);\n}\n\nfunction formatWriteCall(\n\targs: { path?: string; file_path?: string; content?: string } | undefined,\n\toptions: ToolRenderResultOptions,\n\ttheme: Theme,\n\tcache: WriteHighlightCache | undefined,\n\tcwd: string,\n\t// lunr: compact-by-default — finished, successful, non-expanded calls render\n\t// header-only; the content preview stays while streaming and when expanded.\n\tcompact = false,\n): string {\n\tconst rawPath = str(args?.file_path ?? args?.path);\n\tconst fileContent = str(args?.content);\n\tconst pathDisplay = compact ? renderToolFileName(rawPath, theme, cwd) : renderToolPath(rawPath, theme, cwd);\n\tlet text = `${theme.fg(\"toolTitle\", theme.bold(\"write\"))} ${pathDisplay}`;\n\n\tif (fileContent === null) {\n\t\ttext += `\\n\\n${theme.fg(\"error\", \"[invalid content arg - expected string]\")}`;\n\t} else if (fileContent && !compact) {\n\t\tconst lang = rawPath ? getLanguageFromPath(rawPath) : undefined;\n\t\tconst renderedLines = lang\n\t\t\t? (cache?.highlightedLines ?? highlightCode(replaceTabs(normalizeDisplayText(fileContent)), lang))\n\t\t\t: normalizeDisplayText(fileContent).split(\"\\n\");\n\t\tconst lines = trimTrailingEmptyLines(renderedLines);\n\t\tconst totalLines = lines.length;\n\t\tconst maxLines = options.expanded ? lines.length : 10;\n\t\tconst displayLines = lines.slice(0, maxLines);\n\t\tconst remaining = lines.length - maxLines;\n\t\ttext += `\\n\\n${displayLines.map((line) => (lang ? line : theme.fg(\"toolOutput\", replaceTabs(line)))).join(\"\\n\")}`;\n\t\tif (remaining > 0) {\n\t\t\ttext += `${theme.fg(\"muted\", `\\n... (${remaining} more lines, ${totalLines} total,`)} ${keyHint(\"app.tools.expand\", \"to expand\")}${theme.fg(\"muted\", \")\")}`;\n\t\t}\n\t}\n\n\treturn text;\n}\n\nfunction formatWriteResult(\n\tresult: { content: Array<{ type: string; text?: string; data?: string; mimeType?: string }>; isError?: boolean },\n\ttheme: Theme,\n): string | undefined {\n\tif (!result.isError) {\n\t\treturn undefined;\n\t}\n\tconst output = result.content\n\t\t.filter((c) => c.type === \"text\")\n\t\t.map((c) => c.text || \"\")\n\t\t.join(\"\\n\");\n\tif (!output) {\n\t\treturn undefined;\n\t}\n\treturn `\\n${theme.fg(\"error\", output)}`;\n}\n\nexport function createWriteToolDefinition(\n\tcwd: string,\n\toptions?: WriteToolOptions,\n): ToolDefinition<typeof writeSchema, undefined> {\n\tconst ops = options?.operations ?? defaultWriteOperations;\n\treturn {\n\t\tname: \"write\",\n\t\tlabel: \"write\",\n\t\tdescription:\n\t\t\t\"Write content to a file. Creates the file if it doesn't exist, overwrites if it does. Automatically creates parent directories.\",\n\t\tpromptSnippet: \"Create or overwrite files\",\n\t\tpromptGuidelines: [\"Use write only for new files or complete rewrites.\"],\n\t\tparameters: writeSchema,\n\t\tasync execute(\n\t\t\t_toolCallId,\n\t\t\t{ path, content }: { path: string; content: string },\n\t\t\tsignal?: AbortSignal,\n\t\t\t_onUpdate?,\n\t\t\t_ctx?,\n\t\t) {\n\t\t\tconst absolutePath = resolveToCwd(path, cwd);\n\t\t\tconst dir = dirname(absolutePath);\n\t\t\treturn withFileMutationQueue(absolutePath, async () => {\n\t\t\t\t// Do not reject from an abort event listener here: that would release the\n\t\t\t\t// mutation queue while an in-flight filesystem operation may still finish.\n\t\t\t\t// Checking signal.aborted after each await observes the same aborts while\n\t\t\t\t// keeping the queue locked until the current operation has settled.\n\t\t\t\tconst throwIfAborted = (): void => {\n\t\t\t\t\tif (signal?.aborted) throw new Error(\"Operation aborted\");\n\t\t\t\t};\n\n\t\t\t\tthrowIfAborted();\n\t\t\t\t// Create parent directories if needed.\n\t\t\t\tawait ops.mkdir(dir);\n\t\t\t\tthrowIfAborted();\n\n\t\t\t\t// Write the file contents.\n\t\t\t\tawait ops.writeFile(absolutePath, content);\n\t\t\t\tthrowIfAborted();\n\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [{ type: \"text\", text: `Successfully wrote ${content.length} bytes to ${path}` }],\n\t\t\t\t\tdetails: undefined,\n\t\t\t\t};\n\t\t\t});\n\t\t},\n\t\trenderCall(args, theme, context) {\n\t\t\tconst renderArgs = args as { path?: string; file_path?: string; content?: string } | undefined;\n\t\t\tconst rawPath = str(renderArgs?.file_path ?? renderArgs?.path);\n\t\t\tconst fileContent = str(renderArgs?.content);\n\t\t\tconst component =\n\t\t\t\t(context.lastComponent as WriteCallRenderComponent | undefined) ?? new WriteCallRenderComponent();\n\t\t\tif (fileContent !== null) {\n\t\t\t\tcomponent.cache = context.argsComplete\n\t\t\t\t\t? rebuildWriteHighlightCacheFull(rawPath, fileContent)\n\t\t\t\t\t: updateWriteHighlightCacheIncremental(component.cache, rawPath, fileContent);\n\t\t\t} else {\n\t\t\t\tcomponent.cache = undefined;\n\t\t\t}\n\t\t\tcomponent.setText(\n\t\t\t\t`${toolStatusDotFromContext(context, theme)} ${formatWriteCall(\n\t\t\t\t\trenderArgs,\n\t\t\t\t\t{ expanded: context.expanded, isPartial: context.isPartial },\n\t\t\t\t\ttheme,\n\t\t\t\t\tcomponent.cache,\n\t\t\t\t\tcontext.cwd,\n\t\t\t\t\t// lunr: compact-by-default\n\t\t\t\t\t!context.isPartial && !context.expanded && !context.isError,\n\t\t\t\t)}`,\n\t\t\t);\n\t\t\treturn component;\n\t\t},\n\t\trenderResult(result, _options, theme, context) {\n\t\t\tconst output = formatWriteResult({ ...result, isError: context.isError }, theme);\n\t\t\tif (!output) {\n\t\t\t\tconst component = (context.lastComponent as Container | undefined) ?? new Container();\n\t\t\t\tcomponent.clear();\n\t\t\t\treturn component;\n\t\t\t}\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(output);\n\t\t\treturn text;\n\t\t},\n\t};\n}\n\nexport function createWriteTool(cwd: string, options?: WriteToolOptions): AgentTool<typeof writeSchema> {\n\treturn wrapToolDefinition(createWriteToolDefinition(cwd, options));\n}\n"]}
|
|
1
|
+
{"version":3,"file":"write.d.ts","sourceRoot":"","sources":["../../../src/core/tools/write.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,+BAA+B,CAAC;AAI/D,OAAO,EAAE,KAAK,MAAM,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAG5C,OAAO,KAAK,EAAE,cAAc,EAA2B,MAAM,wBAAwB,CAAC;AActF,QAAA,MAAM,WAAW;;;EAGf,CAAC;AAEH,MAAM,MAAM,cAAc,GAAG,MAAM,CAAC,OAAO,WAAW,CAAC,CAAC;AAExD;;;GAGG;AACH,MAAM,WAAW,eAAe;IAC/B,8BAA8B;IAC9B,SAAS,EAAE,CAAC,YAAY,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACpE,mCAAmC;IACnC,KAAK,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACtC;AAOD,MAAM,WAAW,gBAAgB;IAChC,oEAAoE;IACpE,UAAU,CAAC,EAAE,eAAe,CAAC;CAC7B;AAgJD,wBAAgB,yBAAyB,CACxC,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,gBAAgB,GACxB,cAAc,CAAC,OAAO,WAAW,EAAE,SAAS,CAAC,CAkG/C;AAED,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,gBAAgB,GAAG,SAAS,CAAC,OAAO,WAAW,CAAC,CAEtG","sourcesContent":["import type { AgentTool } from \"@ashx-j/lunr-agent\";\nimport { Container, Text } from \"@ashx-j/lunr-tui\";\nimport { mkdir as fsMkdir, writeFile as fsWriteFile } from \"fs/promises\";\nimport { dirname } from \"path\";\nimport { type Static, Type } from \"typebox\";\nimport { keyHint } from \"../../modes/interactive/components/keybinding-hints.ts\";\nimport { getLanguageFromPath, highlightCode, type Theme } from \"../../modes/interactive/theme/theme.ts\";\nimport type { ToolDefinition, ToolRenderResultOptions } from \"../extensions/types.ts\";\nimport { withFileMutationQueue } from \"./file-mutation-queue.ts\";\nimport { resolveToCwd } from \"./path-utils.ts\";\nimport {\n\tnormalizeDisplayText,\n\tformatGroupedCall,\n\trenderToolFileName,\n\trenderToolPath,\n\treplaceTabs,\n\tstr,\n\ttoolStatusDotFromContext,\n} from \"./render-utils.ts\";\nimport { wrapToolDefinition } from \"./tool-definition-wrapper.ts\";\n\nconst writeSchema = Type.Object({\n\tpath: Type.String({ description: \"Path to the file to write (relative or absolute)\" }),\n\tcontent: Type.String({ description: \"Content to write to the file\" }),\n});\n\nexport type WriteToolInput = Static<typeof writeSchema>;\n\n/**\n * Pluggable operations for the write tool.\n * Override these to delegate file writing to remote systems (for example SSH).\n */\nexport interface WriteOperations {\n\t/** Write content to a file */\n\twriteFile: (absolutePath: string, content: string) => Promise<void>;\n\t/** Create directory recursively */\n\tmkdir: (dir: string) => Promise<void>;\n}\n\nconst defaultWriteOperations: WriteOperations = {\n\twriteFile: (path, content) => fsWriteFile(path, content, \"utf-8\"),\n\tmkdir: (dir) => fsMkdir(dir, { recursive: true }).then(() => {}),\n};\n\nexport interface WriteToolOptions {\n\t/** Custom operations for file writing. Default: local filesystem */\n\toperations?: WriteOperations;\n}\n\ntype WriteHighlightCache = {\n\trawPath: string | null;\n\tlang: string;\n\trawContent: string;\n\tnormalizedLines: string[];\n\thighlightedLines: string[];\n};\n\nclass WriteCallRenderComponent extends Text {\n\tcache?: WriteHighlightCache;\n\n\tconstructor() {\n\t\tsuper(\"\", 0, 0);\n\t}\n}\n\nconst WRITE_PARTIAL_FULL_HIGHLIGHT_LINES = 50;\n\nfunction highlightSingleLine(line: string, lang: string): string {\n\tconst highlighted = highlightCode(line, lang);\n\treturn highlighted[0] ?? \"\";\n}\n\nfunction refreshWriteHighlightPrefix(cache: WriteHighlightCache): void {\n\tconst prefixCount = Math.min(WRITE_PARTIAL_FULL_HIGHLIGHT_LINES, cache.normalizedLines.length);\n\tif (prefixCount === 0) return;\n\tconst prefixSource = cache.normalizedLines.slice(0, prefixCount).join(\"\\n\");\n\tconst prefixHighlighted = highlightCode(prefixSource, cache.lang);\n\tfor (let i = 0; i < prefixCount; i++) {\n\t\tcache.highlightedLines[i] =\n\t\t\tprefixHighlighted[i] ?? highlightSingleLine(cache.normalizedLines[i] ?? \"\", cache.lang);\n\t}\n}\n\nfunction rebuildWriteHighlightCacheFull(rawPath: string | null, fileContent: string): WriteHighlightCache | undefined {\n\tconst lang = rawPath ? getLanguageFromPath(rawPath) : undefined;\n\tif (!lang) return undefined;\n\tconst displayContent = normalizeDisplayText(fileContent);\n\tconst normalized = replaceTabs(displayContent);\n\treturn {\n\t\trawPath,\n\t\tlang,\n\t\trawContent: fileContent,\n\t\tnormalizedLines: normalized.split(\"\\n\"),\n\t\thighlightedLines: highlightCode(normalized, lang),\n\t};\n}\n\nfunction updateWriteHighlightCacheIncremental(\n\tcache: WriteHighlightCache | undefined,\n\trawPath: string | null,\n\tfileContent: string,\n): WriteHighlightCache | undefined {\n\tconst lang = rawPath ? getLanguageFromPath(rawPath) : undefined;\n\tif (!lang) return undefined;\n\tif (!cache) return rebuildWriteHighlightCacheFull(rawPath, fileContent);\n\tif (cache.lang !== lang || cache.rawPath !== rawPath) return rebuildWriteHighlightCacheFull(rawPath, fileContent);\n\tif (!fileContent.startsWith(cache.rawContent)) return rebuildWriteHighlightCacheFull(rawPath, fileContent);\n\tif (fileContent.length === cache.rawContent.length) return cache;\n\n\tconst deltaRaw = fileContent.slice(cache.rawContent.length);\n\tconst deltaDisplay = normalizeDisplayText(deltaRaw);\n\tconst deltaNormalized = replaceTabs(deltaDisplay);\n\tcache.rawContent = fileContent;\n\tif (cache.normalizedLines.length === 0) {\n\t\tcache.normalizedLines.push(\"\");\n\t\tcache.highlightedLines.push(\"\");\n\t}\n\n\tconst segments = deltaNormalized.split(\"\\n\");\n\tconst lastIndex = cache.normalizedLines.length - 1;\n\tcache.normalizedLines[lastIndex] += segments[0];\n\tcache.highlightedLines[lastIndex] = highlightSingleLine(cache.normalizedLines[lastIndex], cache.lang);\n\tfor (let i = 1; i < segments.length; i++) {\n\t\tcache.normalizedLines.push(segments[i]);\n\t\tcache.highlightedLines.push(highlightSingleLine(segments[i], cache.lang));\n\t}\n\trefreshWriteHighlightPrefix(cache);\n\treturn cache;\n}\n\nfunction trimTrailingEmptyLines(lines: string[]): string[] {\n\tlet end = lines.length;\n\twhile (end > 0 && lines[end - 1] === \"\") {\n\t\tend--;\n\t}\n\treturn lines.slice(0, end);\n}\n\nfunction formatWriteCall(\n\targs: { path?: string; file_path?: string; content?: string } | undefined,\n\toptions: ToolRenderResultOptions,\n\ttheme: Theme,\n\tcache: WriteHighlightCache | undefined,\n\tcwd: string,\n\t// lunr: compact-by-default — finished, successful, non-expanded calls render\n\t// header-only; the content preview stays while streaming and when expanded.\n\tcompact = false,\n): string {\n\tconst rawPath = str(args?.file_path ?? args?.path);\n\tconst fileContent = str(args?.content);\n\tconst pathDisplay = compact ? renderToolFileName(rawPath, theme, cwd) : renderToolPath(rawPath, theme, cwd);\n\tlet text = `${theme.fg(\"toolTitle\", theme.bold(\"write\"))} ${pathDisplay}`;\n\n\tif (fileContent === null) {\n\t\ttext += `\\n\\n${theme.fg(\"error\", \"[invalid content arg - expected string]\")}`;\n\t} else if (fileContent && !compact) {\n\t\tconst lang = rawPath ? getLanguageFromPath(rawPath) : undefined;\n\t\tconst renderedLines = lang\n\t\t\t? (cache?.highlightedLines ?? highlightCode(replaceTabs(normalizeDisplayText(fileContent)), lang))\n\t\t\t: normalizeDisplayText(fileContent).split(\"\\n\");\n\t\tconst lines = trimTrailingEmptyLines(renderedLines);\n\t\tconst totalLines = lines.length;\n\t\tconst maxLines = options.expanded ? lines.length : 10;\n\t\tconst displayLines = lines.slice(0, maxLines);\n\t\tconst remaining = lines.length - maxLines;\n\t\ttext += `\\n\\n${displayLines.map((line) => (lang ? line : theme.fg(\"toolOutput\", replaceTabs(line)))).join(\"\\n\")}`;\n\t\tif (remaining > 0) {\n\t\t\ttext += `${theme.fg(\"muted\", `\\n... (${remaining} more lines, ${totalLines} total,`)} ${keyHint(\"app.tools.expand\", \"to expand\")}${theme.fg(\"muted\", \")\")}`;\n\t\t}\n\t}\n\n\treturn text;\n}\n\nfunction formatWriteResult(\n\tresult: { content: Array<{ type: string; text?: string; data?: string; mimeType?: string }>; isError?: boolean },\n\ttheme: Theme,\n): string | undefined {\n\tif (!result.isError) {\n\t\treturn undefined;\n\t}\n\tconst output = result.content\n\t\t.filter((c) => c.type === \"text\")\n\t\t.map((c) => c.text || \"\")\n\t\t.join(\"\\n\");\n\tif (!output) {\n\t\treturn undefined;\n\t}\n\treturn `\\n${theme.fg(\"error\", output)}`;\n}\n\nexport function createWriteToolDefinition(\n\tcwd: string,\n\toptions?: WriteToolOptions,\n): ToolDefinition<typeof writeSchema, undefined> {\n\tconst ops = options?.operations ?? defaultWriteOperations;\n\treturn {\n\t\tname: \"write\",\n\t\tlabel: \"write\",\n\t\tdescription:\n\t\t\t\"Write content to a file. Creates the file if it doesn't exist, overwrites if it does. Automatically creates parent directories.\",\n\t\tpromptSnippet: \"Create or overwrite files\",\n\t\tpromptGuidelines: [\"Use write only for new files or complete rewrites.\"],\n\t\tparameters: writeSchema,\n\t\tasync execute(\n\t\t\t_toolCallId,\n\t\t\t{ path, content }: { path: string; content: string },\n\t\t\tsignal?: AbortSignal,\n\t\t\t_onUpdate?,\n\t\t\t_ctx?,\n\t\t) {\n\t\t\tconst absolutePath = resolveToCwd(path, cwd);\n\t\t\tconst dir = dirname(absolutePath);\n\t\t\treturn withFileMutationQueue(absolutePath, async () => {\n\t\t\t\t// Do not reject from an abort event listener here: that would release the\n\t\t\t\t// mutation queue while an in-flight filesystem operation may still finish.\n\t\t\t\t// Checking signal.aborted after each await observes the same aborts while\n\t\t\t\t// keeping the queue locked until the current operation has settled.\n\t\t\t\tconst throwIfAborted = (): void => {\n\t\t\t\t\tif (signal?.aborted) throw new Error(\"Operation aborted\");\n\t\t\t\t};\n\n\t\t\t\tthrowIfAborted();\n\t\t\t\t// Create parent directories if needed.\n\t\t\t\tawait ops.mkdir(dir);\n\t\t\t\tthrowIfAborted();\n\n\t\t\t\t// Write the file contents.\n\t\t\t\tawait ops.writeFile(absolutePath, content);\n\t\t\t\tthrowIfAborted();\n\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [{ type: \"text\", text: `Successfully wrote ${content.length} bytes to ${path}` }],\n\t\t\t\t\tdetails: undefined,\n\t\t\t\t};\n\t\t\t});\n\t\t},\n\t\trenderCall(args, theme, context) {\n\t\t\tconst renderArgs = args as { path?: string; file_path?: string; content?: string } | undefined;\n\t\t\tconst rawPath = str(renderArgs?.file_path ?? renderArgs?.path);\n\t\t\tconst fileContent = str(renderArgs?.content);\n\t\t\tconst component =\n\t\t\t\t(context.lastComponent as WriteCallRenderComponent | undefined) ?? new WriteCallRenderComponent();\n\t\t\tif (fileContent !== null) {\n\t\t\t\tcomponent.cache = context.argsComplete\n\t\t\t\t\t? rebuildWriteHighlightCacheFull(rawPath, fileContent)\n\t\t\t\t\t: updateWriteHighlightCacheIncremental(component.cache, rawPath, fileContent);\n\t\t\t} else {\n\t\t\t\tcomponent.cache = undefined;\n\t\t\t}\n\t\t\tconst compact = !context.isPartial && !context.expanded && !context.isError;\n\t\t\tif (compact) {\n\t\t\t\tconst pathDisplay = renderToolFileName(rawPath, theme, context.cwd);\n\t\t\t\tlet detail = pathDisplay;\n\t\t\t\tif (fileContent === null) {\n\t\t\t\t\tdetail += `\\n\\n${theme.fg(\"error\", \"[invalid content arg - expected string]\")}`;\n\t\t\t\t}\n\t\t\t\tcomponent.setText(\n\t\t\t\t\tformatGroupedCall({\n\t\t\t\t\t\trole: context.groupRole ?? \"singleton\",\n\t\t\t\t\t\tcompact: true,\n\t\t\t\t\t\tdot: toolStatusDotFromContext(context, theme),\n\t\t\t\t\t\ttitle: theme.fg(\"toolTitle\", theme.bold(\"write\")),\n\t\t\t\t\t\tdetail,\n\t\t\t\t\t}),\n\t\t\t\t);\n\t\t\t} else {\n\t\t\t\tcomponent.setText(\n\t\t\t\t\t`${toolStatusDotFromContext(context, theme)} ${formatWriteCall(\n\t\t\t\t\t\trenderArgs,\n\t\t\t\t\t\t{ expanded: context.expanded, isPartial: context.isPartial },\n\t\t\t\t\t\ttheme,\n\t\t\t\t\t\tcomponent.cache,\n\t\t\t\t\t\tcontext.cwd,\n\t\t\t\t\t\tfalse,\n\t\t\t\t\t)}`,\n\t\t\t\t);\n\t\t\t}\n\t\t\treturn component;\n\t\t},\n\t\trenderResult(result, _options, theme, context) {\n\t\t\tconst output = formatWriteResult({ ...result, isError: context.isError }, theme);\n\t\t\tif (!output) {\n\t\t\t\tconst component = (context.lastComponent as Container | undefined) ?? new Container();\n\t\t\t\tcomponent.clear();\n\t\t\t\treturn component;\n\t\t\t}\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(output);\n\t\t\treturn text;\n\t\t},\n\t};\n}\n\nexport function createWriteTool(cwd: string, options?: WriteToolOptions): AgentTool<typeof writeSchema> {\n\treturn wrapToolDefinition(createWriteToolDefinition(cwd, options));\n}\n"]}
|
package/dist/core/tools/write.js
CHANGED
|
@@ -6,7 +6,7 @@ import { keyHint } from "../../modes/interactive/components/keybinding-hints.js"
|
|
|
6
6
|
import { getLanguageFromPath, highlightCode } from "../../modes/interactive/theme/theme.js";
|
|
7
7
|
import { withFileMutationQueue } from "./file-mutation-queue.js";
|
|
8
8
|
import { resolveToCwd } from "./path-utils.js";
|
|
9
|
-
import { normalizeDisplayText, renderToolFileName, renderToolPath, replaceTabs, str, toolStatusDotFromContext, } from "./render-utils.js";
|
|
9
|
+
import { normalizeDisplayText, formatGroupedCall, renderToolFileName, renderToolPath, replaceTabs, str, toolStatusDotFromContext, } from "./render-utils.js";
|
|
10
10
|
import { wrapToolDefinition } from "./tool-definition-wrapper.js";
|
|
11
11
|
const writeSchema = Type.Object({
|
|
12
12
|
path: Type.String({ description: "Path to the file to write (relative or absolute)" }),
|
|
@@ -178,9 +178,24 @@ export function createWriteToolDefinition(cwd, options) {
|
|
|
178
178
|
else {
|
|
179
179
|
component.cache = undefined;
|
|
180
180
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
181
|
+
const compact = !context.isPartial && !context.expanded && !context.isError;
|
|
182
|
+
if (compact) {
|
|
183
|
+
const pathDisplay = renderToolFileName(rawPath, theme, context.cwd);
|
|
184
|
+
let detail = pathDisplay;
|
|
185
|
+
if (fileContent === null) {
|
|
186
|
+
detail += `\n\n${theme.fg("error", "[invalid content arg - expected string]")}`;
|
|
187
|
+
}
|
|
188
|
+
component.setText(formatGroupedCall({
|
|
189
|
+
role: context.groupRole ?? "singleton",
|
|
190
|
+
compact: true,
|
|
191
|
+
dot: toolStatusDotFromContext(context, theme),
|
|
192
|
+
title: theme.fg("toolTitle", theme.bold("write")),
|
|
193
|
+
detail,
|
|
194
|
+
}));
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
component.setText(`${toolStatusDotFromContext(context, theme)} ${formatWriteCall(renderArgs, { expanded: context.expanded, isPartial: context.isPartial }, theme, component.cache, context.cwd, false)}`);
|
|
198
|
+
}
|
|
184
199
|
return component;
|
|
185
200
|
},
|
|
186
201
|
renderResult(result, _options, theme, context) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"write.js","sourceRoot":"","sources":["../../../src/core/tools/write.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AgentTool } from \"@ashx-j/lunr-agent\";\nimport { Container, Text } from \"@ashx-j/lunr-tui\";\nimport { mkdir as fsMkdir, writeFile as fsWriteFile } from \"fs/promises\";\nimport { dirname } from \"path\";\nimport { type Static, Type } from \"typebox\";\nimport { keyHint } from \"../../modes/interactive/components/keybinding-hints.ts\";\nimport { getLanguageFromPath, highlightCode, type Theme } from \"../../modes/interactive/theme/theme.ts\";\nimport type { ToolDefinition, ToolRenderResultOptions } from \"../extensions/types.ts\";\nimport { withFileMutationQueue } from \"./file-mutation-queue.ts\";\nimport { resolveToCwd } from \"./path-utils.ts\";\nimport {\n\tnormalizeDisplayText,\n\trenderToolFileName,\n\trenderToolPath,\n\treplaceTabs,\n\tstr,\n\ttoolStatusDotFromContext,\n} from \"./render-utils.ts\";\nimport { wrapToolDefinition } from \"./tool-definition-wrapper.ts\";\n\nconst writeSchema = Type.Object({\n\tpath: Type.String({ description: \"Path to the file to write (relative or absolute)\" }),\n\tcontent: Type.String({ description: \"Content to write to the file\" }),\n});\n\nexport type WriteToolInput = Static<typeof writeSchema>;\n\n/**\n * Pluggable operations for the write tool.\n * Override these to delegate file writing to remote systems (for example SSH).\n */\nexport interface WriteOperations {\n\t/** Write content to a file */\n\twriteFile: (absolutePath: string, content: string) => Promise<void>;\n\t/** Create directory recursively */\n\tmkdir: (dir: string) => Promise<void>;\n}\n\nconst defaultWriteOperations: WriteOperations = {\n\twriteFile: (path, content) => fsWriteFile(path, content, \"utf-8\"),\n\tmkdir: (dir) => fsMkdir(dir, { recursive: true }).then(() => {}),\n};\n\nexport interface WriteToolOptions {\n\t/** Custom operations for file writing. Default: local filesystem */\n\toperations?: WriteOperations;\n}\n\ntype WriteHighlightCache = {\n\trawPath: string | null;\n\tlang: string;\n\trawContent: string;\n\tnormalizedLines: string[];\n\thighlightedLines: string[];\n};\n\nclass WriteCallRenderComponent extends Text {\n\tcache?: WriteHighlightCache;\n\n\tconstructor() {\n\t\tsuper(\"\", 0, 0);\n\t}\n}\n\nconst WRITE_PARTIAL_FULL_HIGHLIGHT_LINES = 50;\n\nfunction highlightSingleLine(line: string, lang: string): string {\n\tconst highlighted = highlightCode(line, lang);\n\treturn highlighted[0] ?? \"\";\n}\n\nfunction refreshWriteHighlightPrefix(cache: WriteHighlightCache): void {\n\tconst prefixCount = Math.min(WRITE_PARTIAL_FULL_HIGHLIGHT_LINES, cache.normalizedLines.length);\n\tif (prefixCount === 0) return;\n\tconst prefixSource = cache.normalizedLines.slice(0, prefixCount).join(\"\\n\");\n\tconst prefixHighlighted = highlightCode(prefixSource, cache.lang);\n\tfor (let i = 0; i < prefixCount; i++) {\n\t\tcache.highlightedLines[i] =\n\t\t\tprefixHighlighted[i] ?? highlightSingleLine(cache.normalizedLines[i] ?? \"\", cache.lang);\n\t}\n}\n\nfunction rebuildWriteHighlightCacheFull(rawPath: string | null, fileContent: string): WriteHighlightCache | undefined {\n\tconst lang = rawPath ? getLanguageFromPath(rawPath) : undefined;\n\tif (!lang) return undefined;\n\tconst displayContent = normalizeDisplayText(fileContent);\n\tconst normalized = replaceTabs(displayContent);\n\treturn {\n\t\trawPath,\n\t\tlang,\n\t\trawContent: fileContent,\n\t\tnormalizedLines: normalized.split(\"\\n\"),\n\t\thighlightedLines: highlightCode(normalized, lang),\n\t};\n}\n\nfunction updateWriteHighlightCacheIncremental(\n\tcache: WriteHighlightCache | undefined,\n\trawPath: string | null,\n\tfileContent: string,\n): WriteHighlightCache | undefined {\n\tconst lang = rawPath ? getLanguageFromPath(rawPath) : undefined;\n\tif (!lang) return undefined;\n\tif (!cache) return rebuildWriteHighlightCacheFull(rawPath, fileContent);\n\tif (cache.lang !== lang || cache.rawPath !== rawPath) return rebuildWriteHighlightCacheFull(rawPath, fileContent);\n\tif (!fileContent.startsWith(cache.rawContent)) return rebuildWriteHighlightCacheFull(rawPath, fileContent);\n\tif (fileContent.length === cache.rawContent.length) return cache;\n\n\tconst deltaRaw = fileContent.slice(cache.rawContent.length);\n\tconst deltaDisplay = normalizeDisplayText(deltaRaw);\n\tconst deltaNormalized = replaceTabs(deltaDisplay);\n\tcache.rawContent = fileContent;\n\tif (cache.normalizedLines.length === 0) {\n\t\tcache.normalizedLines.push(\"\");\n\t\tcache.highlightedLines.push(\"\");\n\t}\n\n\tconst segments = deltaNormalized.split(\"\\n\");\n\tconst lastIndex = cache.normalizedLines.length - 1;\n\tcache.normalizedLines[lastIndex] += segments[0];\n\tcache.highlightedLines[lastIndex] = highlightSingleLine(cache.normalizedLines[lastIndex], cache.lang);\n\tfor (let i = 1; i < segments.length; i++) {\n\t\tcache.normalizedLines.push(segments[i]);\n\t\tcache.highlightedLines.push(highlightSingleLine(segments[i], cache.lang));\n\t}\n\trefreshWriteHighlightPrefix(cache);\n\treturn cache;\n}\n\nfunction trimTrailingEmptyLines(lines: string[]): string[] {\n\tlet end = lines.length;\n\twhile (end > 0 && lines[end - 1] === \"\") {\n\t\tend--;\n\t}\n\treturn lines.slice(0, end);\n}\n\nfunction formatWriteCall(\n\targs: { path?: string; file_path?: string; content?: string } | undefined,\n\toptions: ToolRenderResultOptions,\n\ttheme: Theme,\n\tcache: WriteHighlightCache | undefined,\n\tcwd: string,\n\t// lunr: compact-by-default — finished, successful, non-expanded calls render\n\t// header-only; the content preview stays while streaming and when expanded.\n\tcompact = false,\n): string {\n\tconst rawPath = str(args?.file_path ?? args?.path);\n\tconst fileContent = str(args?.content);\n\tconst pathDisplay = compact ? renderToolFileName(rawPath, theme, cwd) : renderToolPath(rawPath, theme, cwd);\n\tlet text = `${theme.fg(\"toolTitle\", theme.bold(\"write\"))} ${pathDisplay}`;\n\n\tif (fileContent === null) {\n\t\ttext += `\\n\\n${theme.fg(\"error\", \"[invalid content arg - expected string]\")}`;\n\t} else if (fileContent && !compact) {\n\t\tconst lang = rawPath ? getLanguageFromPath(rawPath) : undefined;\n\t\tconst renderedLines = lang\n\t\t\t? (cache?.highlightedLines ?? highlightCode(replaceTabs(normalizeDisplayText(fileContent)), lang))\n\t\t\t: normalizeDisplayText(fileContent).split(\"\\n\");\n\t\tconst lines = trimTrailingEmptyLines(renderedLines);\n\t\tconst totalLines = lines.length;\n\t\tconst maxLines = options.expanded ? lines.length : 10;\n\t\tconst displayLines = lines.slice(0, maxLines);\n\t\tconst remaining = lines.length - maxLines;\n\t\ttext += `\\n\\n${displayLines.map((line) => (lang ? line : theme.fg(\"toolOutput\", replaceTabs(line)))).join(\"\\n\")}`;\n\t\tif (remaining > 0) {\n\t\t\ttext += `${theme.fg(\"muted\", `\\n... (${remaining} more lines, ${totalLines} total,`)} ${keyHint(\"app.tools.expand\", \"to expand\")}${theme.fg(\"muted\", \")\")}`;\n\t\t}\n\t}\n\n\treturn text;\n}\n\nfunction formatWriteResult(\n\tresult: { content: Array<{ type: string; text?: string; data?: string; mimeType?: string }>; isError?: boolean },\n\ttheme: Theme,\n): string | undefined {\n\tif (!result.isError) {\n\t\treturn undefined;\n\t}\n\tconst output = result.content\n\t\t.filter((c) => c.type === \"text\")\n\t\t.map((c) => c.text || \"\")\n\t\t.join(\"\\n\");\n\tif (!output) {\n\t\treturn undefined;\n\t}\n\treturn `\\n${theme.fg(\"error\", output)}`;\n}\n\nexport function createWriteToolDefinition(\n\tcwd: string,\n\toptions?: WriteToolOptions,\n): ToolDefinition<typeof writeSchema, undefined> {\n\tconst ops = options?.operations ?? defaultWriteOperations;\n\treturn {\n\t\tname: \"write\",\n\t\tlabel: \"write\",\n\t\tdescription:\n\t\t\t\"Write content to a file. Creates the file if it doesn't exist, overwrites if it does. Automatically creates parent directories.\",\n\t\tpromptSnippet: \"Create or overwrite files\",\n\t\tpromptGuidelines: [\"Use write only for new files or complete rewrites.\"],\n\t\tparameters: writeSchema,\n\t\tasync execute(\n\t\t\t_toolCallId,\n\t\t\t{ path, content }: { path: string; content: string },\n\t\t\tsignal?: AbortSignal,\n\t\t\t_onUpdate?,\n\t\t\t_ctx?,\n\t\t) {\n\t\t\tconst absolutePath = resolveToCwd(path, cwd);\n\t\t\tconst dir = dirname(absolutePath);\n\t\t\treturn withFileMutationQueue(absolutePath, async () => {\n\t\t\t\t// Do not reject from an abort event listener here: that would release the\n\t\t\t\t// mutation queue while an in-flight filesystem operation may still finish.\n\t\t\t\t// Checking signal.aborted after each await observes the same aborts while\n\t\t\t\t// keeping the queue locked until the current operation has settled.\n\t\t\t\tconst throwIfAborted = (): void => {\n\t\t\t\t\tif (signal?.aborted) throw new Error(\"Operation aborted\");\n\t\t\t\t};\n\n\t\t\t\tthrowIfAborted();\n\t\t\t\t// Create parent directories if needed.\n\t\t\t\tawait ops.mkdir(dir);\n\t\t\t\tthrowIfAborted();\n\n\t\t\t\t// Write the file contents.\n\t\t\t\tawait ops.writeFile(absolutePath, content);\n\t\t\t\tthrowIfAborted();\n\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [{ type: \"text\", text: `Successfully wrote ${content.length} bytes to ${path}` }],\n\t\t\t\t\tdetails: undefined,\n\t\t\t\t};\n\t\t\t});\n\t\t},\n\t\trenderCall(args, theme, context) {\n\t\t\tconst renderArgs = args as { path?: string; file_path?: string; content?: string } | undefined;\n\t\t\tconst rawPath = str(renderArgs?.file_path ?? renderArgs?.path);\n\t\t\tconst fileContent = str(renderArgs?.content);\n\t\t\tconst component =\n\t\t\t\t(context.lastComponent as WriteCallRenderComponent | undefined) ?? new WriteCallRenderComponent();\n\t\t\tif (fileContent !== null) {\n\t\t\t\tcomponent.cache = context.argsComplete\n\t\t\t\t\t? rebuildWriteHighlightCacheFull(rawPath, fileContent)\n\t\t\t\t\t: updateWriteHighlightCacheIncremental(component.cache, rawPath, fileContent);\n\t\t\t} else {\n\t\t\t\tcomponent.cache = undefined;\n\t\t\t}\n\t\t\tcomponent.setText(\n\t\t\t\t`${toolStatusDotFromContext(context, theme)} ${formatWriteCall(\n\t\t\t\t\trenderArgs,\n\t\t\t\t\t{ expanded: context.expanded, isPartial: context.isPartial },\n\t\t\t\t\ttheme,\n\t\t\t\t\tcomponent.cache,\n\t\t\t\t\tcontext.cwd,\n\t\t\t\t\t// lunr: compact-by-default\n\t\t\t\t\t!context.isPartial && !context.expanded && !context.isError,\n\t\t\t\t)}`,\n\t\t\t);\n\t\t\treturn component;\n\t\t},\n\t\trenderResult(result, _options, theme, context) {\n\t\t\tconst output = formatWriteResult({ ...result, isError: context.isError }, theme);\n\t\t\tif (!output) {\n\t\t\t\tconst component = (context.lastComponent as Container | undefined) ?? new Container();\n\t\t\t\tcomponent.clear();\n\t\t\t\treturn component;\n\t\t\t}\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(output);\n\t\t\treturn text;\n\t\t},\n\t};\n}\n\nexport function createWriteTool(cwd: string, options?: WriteToolOptions): AgentTool<typeof writeSchema> {\n\treturn wrapToolDefinition(createWriteToolDefinition(cwd, options));\n}\n"]}
|
|
1
|
+
{"version":3,"file":"write.js","sourceRoot":"","sources":["../../../src/core/tools/write.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AgentTool } from \"@ashx-j/lunr-agent\";\nimport { Container, Text } from \"@ashx-j/lunr-tui\";\nimport { mkdir as fsMkdir, writeFile as fsWriteFile } from \"fs/promises\";\nimport { dirname } from \"path\";\nimport { type Static, Type } from \"typebox\";\nimport { keyHint } from \"../../modes/interactive/components/keybinding-hints.ts\";\nimport { getLanguageFromPath, highlightCode, type Theme } from \"../../modes/interactive/theme/theme.ts\";\nimport type { ToolDefinition, ToolRenderResultOptions } from \"../extensions/types.ts\";\nimport { withFileMutationQueue } from \"./file-mutation-queue.ts\";\nimport { resolveToCwd } from \"./path-utils.ts\";\nimport {\n\tnormalizeDisplayText,\n\tformatGroupedCall,\n\trenderToolFileName,\n\trenderToolPath,\n\treplaceTabs,\n\tstr,\n\ttoolStatusDotFromContext,\n} from \"./render-utils.ts\";\nimport { wrapToolDefinition } from \"./tool-definition-wrapper.ts\";\n\nconst writeSchema = Type.Object({\n\tpath: Type.String({ description: \"Path to the file to write (relative or absolute)\" }),\n\tcontent: Type.String({ description: \"Content to write to the file\" }),\n});\n\nexport type WriteToolInput = Static<typeof writeSchema>;\n\n/**\n * Pluggable operations for the write tool.\n * Override these to delegate file writing to remote systems (for example SSH).\n */\nexport interface WriteOperations {\n\t/** Write content to a file */\n\twriteFile: (absolutePath: string, content: string) => Promise<void>;\n\t/** Create directory recursively */\n\tmkdir: (dir: string) => Promise<void>;\n}\n\nconst defaultWriteOperations: WriteOperations = {\n\twriteFile: (path, content) => fsWriteFile(path, content, \"utf-8\"),\n\tmkdir: (dir) => fsMkdir(dir, { recursive: true }).then(() => {}),\n};\n\nexport interface WriteToolOptions {\n\t/** Custom operations for file writing. Default: local filesystem */\n\toperations?: WriteOperations;\n}\n\ntype WriteHighlightCache = {\n\trawPath: string | null;\n\tlang: string;\n\trawContent: string;\n\tnormalizedLines: string[];\n\thighlightedLines: string[];\n};\n\nclass WriteCallRenderComponent extends Text {\n\tcache?: WriteHighlightCache;\n\n\tconstructor() {\n\t\tsuper(\"\", 0, 0);\n\t}\n}\n\nconst WRITE_PARTIAL_FULL_HIGHLIGHT_LINES = 50;\n\nfunction highlightSingleLine(line: string, lang: string): string {\n\tconst highlighted = highlightCode(line, lang);\n\treturn highlighted[0] ?? \"\";\n}\n\nfunction refreshWriteHighlightPrefix(cache: WriteHighlightCache): void {\n\tconst prefixCount = Math.min(WRITE_PARTIAL_FULL_HIGHLIGHT_LINES, cache.normalizedLines.length);\n\tif (prefixCount === 0) return;\n\tconst prefixSource = cache.normalizedLines.slice(0, prefixCount).join(\"\\n\");\n\tconst prefixHighlighted = highlightCode(prefixSource, cache.lang);\n\tfor (let i = 0; i < prefixCount; i++) {\n\t\tcache.highlightedLines[i] =\n\t\t\tprefixHighlighted[i] ?? highlightSingleLine(cache.normalizedLines[i] ?? \"\", cache.lang);\n\t}\n}\n\nfunction rebuildWriteHighlightCacheFull(rawPath: string | null, fileContent: string): WriteHighlightCache | undefined {\n\tconst lang = rawPath ? getLanguageFromPath(rawPath) : undefined;\n\tif (!lang) return undefined;\n\tconst displayContent = normalizeDisplayText(fileContent);\n\tconst normalized = replaceTabs(displayContent);\n\treturn {\n\t\trawPath,\n\t\tlang,\n\t\trawContent: fileContent,\n\t\tnormalizedLines: normalized.split(\"\\n\"),\n\t\thighlightedLines: highlightCode(normalized, lang),\n\t};\n}\n\nfunction updateWriteHighlightCacheIncremental(\n\tcache: WriteHighlightCache | undefined,\n\trawPath: string | null,\n\tfileContent: string,\n): WriteHighlightCache | undefined {\n\tconst lang = rawPath ? getLanguageFromPath(rawPath) : undefined;\n\tif (!lang) return undefined;\n\tif (!cache) return rebuildWriteHighlightCacheFull(rawPath, fileContent);\n\tif (cache.lang !== lang || cache.rawPath !== rawPath) return rebuildWriteHighlightCacheFull(rawPath, fileContent);\n\tif (!fileContent.startsWith(cache.rawContent)) return rebuildWriteHighlightCacheFull(rawPath, fileContent);\n\tif (fileContent.length === cache.rawContent.length) return cache;\n\n\tconst deltaRaw = fileContent.slice(cache.rawContent.length);\n\tconst deltaDisplay = normalizeDisplayText(deltaRaw);\n\tconst deltaNormalized = replaceTabs(deltaDisplay);\n\tcache.rawContent = fileContent;\n\tif (cache.normalizedLines.length === 0) {\n\t\tcache.normalizedLines.push(\"\");\n\t\tcache.highlightedLines.push(\"\");\n\t}\n\n\tconst segments = deltaNormalized.split(\"\\n\");\n\tconst lastIndex = cache.normalizedLines.length - 1;\n\tcache.normalizedLines[lastIndex] += segments[0];\n\tcache.highlightedLines[lastIndex] = highlightSingleLine(cache.normalizedLines[lastIndex], cache.lang);\n\tfor (let i = 1; i < segments.length; i++) {\n\t\tcache.normalizedLines.push(segments[i]);\n\t\tcache.highlightedLines.push(highlightSingleLine(segments[i], cache.lang));\n\t}\n\trefreshWriteHighlightPrefix(cache);\n\treturn cache;\n}\n\nfunction trimTrailingEmptyLines(lines: string[]): string[] {\n\tlet end = lines.length;\n\twhile (end > 0 && lines[end - 1] === \"\") {\n\t\tend--;\n\t}\n\treturn lines.slice(0, end);\n}\n\nfunction formatWriteCall(\n\targs: { path?: string; file_path?: string; content?: string } | undefined,\n\toptions: ToolRenderResultOptions,\n\ttheme: Theme,\n\tcache: WriteHighlightCache | undefined,\n\tcwd: string,\n\t// lunr: compact-by-default — finished, successful, non-expanded calls render\n\t// header-only; the content preview stays while streaming and when expanded.\n\tcompact = false,\n): string {\n\tconst rawPath = str(args?.file_path ?? args?.path);\n\tconst fileContent = str(args?.content);\n\tconst pathDisplay = compact ? renderToolFileName(rawPath, theme, cwd) : renderToolPath(rawPath, theme, cwd);\n\tlet text = `${theme.fg(\"toolTitle\", theme.bold(\"write\"))} ${pathDisplay}`;\n\n\tif (fileContent === null) {\n\t\ttext += `\\n\\n${theme.fg(\"error\", \"[invalid content arg - expected string]\")}`;\n\t} else if (fileContent && !compact) {\n\t\tconst lang = rawPath ? getLanguageFromPath(rawPath) : undefined;\n\t\tconst renderedLines = lang\n\t\t\t? (cache?.highlightedLines ?? highlightCode(replaceTabs(normalizeDisplayText(fileContent)), lang))\n\t\t\t: normalizeDisplayText(fileContent).split(\"\\n\");\n\t\tconst lines = trimTrailingEmptyLines(renderedLines);\n\t\tconst totalLines = lines.length;\n\t\tconst maxLines = options.expanded ? lines.length : 10;\n\t\tconst displayLines = lines.slice(0, maxLines);\n\t\tconst remaining = lines.length - maxLines;\n\t\ttext += `\\n\\n${displayLines.map((line) => (lang ? line : theme.fg(\"toolOutput\", replaceTabs(line)))).join(\"\\n\")}`;\n\t\tif (remaining > 0) {\n\t\t\ttext += `${theme.fg(\"muted\", `\\n... (${remaining} more lines, ${totalLines} total,`)} ${keyHint(\"app.tools.expand\", \"to expand\")}${theme.fg(\"muted\", \")\")}`;\n\t\t}\n\t}\n\n\treturn text;\n}\n\nfunction formatWriteResult(\n\tresult: { content: Array<{ type: string; text?: string; data?: string; mimeType?: string }>; isError?: boolean },\n\ttheme: Theme,\n): string | undefined {\n\tif (!result.isError) {\n\t\treturn undefined;\n\t}\n\tconst output = result.content\n\t\t.filter((c) => c.type === \"text\")\n\t\t.map((c) => c.text || \"\")\n\t\t.join(\"\\n\");\n\tif (!output) {\n\t\treturn undefined;\n\t}\n\treturn `\\n${theme.fg(\"error\", output)}`;\n}\n\nexport function createWriteToolDefinition(\n\tcwd: string,\n\toptions?: WriteToolOptions,\n): ToolDefinition<typeof writeSchema, undefined> {\n\tconst ops = options?.operations ?? defaultWriteOperations;\n\treturn {\n\t\tname: \"write\",\n\t\tlabel: \"write\",\n\t\tdescription:\n\t\t\t\"Write content to a file. Creates the file if it doesn't exist, overwrites if it does. Automatically creates parent directories.\",\n\t\tpromptSnippet: \"Create or overwrite files\",\n\t\tpromptGuidelines: [\"Use write only for new files or complete rewrites.\"],\n\t\tparameters: writeSchema,\n\t\tasync execute(\n\t\t\t_toolCallId,\n\t\t\t{ path, content }: { path: string; content: string },\n\t\t\tsignal?: AbortSignal,\n\t\t\t_onUpdate?,\n\t\t\t_ctx?,\n\t\t) {\n\t\t\tconst absolutePath = resolveToCwd(path, cwd);\n\t\t\tconst dir = dirname(absolutePath);\n\t\t\treturn withFileMutationQueue(absolutePath, async () => {\n\t\t\t\t// Do not reject from an abort event listener here: that would release the\n\t\t\t\t// mutation queue while an in-flight filesystem operation may still finish.\n\t\t\t\t// Checking signal.aborted after each await observes the same aborts while\n\t\t\t\t// keeping the queue locked until the current operation has settled.\n\t\t\t\tconst throwIfAborted = (): void => {\n\t\t\t\t\tif (signal?.aborted) throw new Error(\"Operation aborted\");\n\t\t\t\t};\n\n\t\t\t\tthrowIfAborted();\n\t\t\t\t// Create parent directories if needed.\n\t\t\t\tawait ops.mkdir(dir);\n\t\t\t\tthrowIfAborted();\n\n\t\t\t\t// Write the file contents.\n\t\t\t\tawait ops.writeFile(absolutePath, content);\n\t\t\t\tthrowIfAborted();\n\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [{ type: \"text\", text: `Successfully wrote ${content.length} bytes to ${path}` }],\n\t\t\t\t\tdetails: undefined,\n\t\t\t\t};\n\t\t\t});\n\t\t},\n\t\trenderCall(args, theme, context) {\n\t\t\tconst renderArgs = args as { path?: string; file_path?: string; content?: string } | undefined;\n\t\t\tconst rawPath = str(renderArgs?.file_path ?? renderArgs?.path);\n\t\t\tconst fileContent = str(renderArgs?.content);\n\t\t\tconst component =\n\t\t\t\t(context.lastComponent as WriteCallRenderComponent | undefined) ?? new WriteCallRenderComponent();\n\t\t\tif (fileContent !== null) {\n\t\t\t\tcomponent.cache = context.argsComplete\n\t\t\t\t\t? rebuildWriteHighlightCacheFull(rawPath, fileContent)\n\t\t\t\t\t: updateWriteHighlightCacheIncremental(component.cache, rawPath, fileContent);\n\t\t\t} else {\n\t\t\t\tcomponent.cache = undefined;\n\t\t\t}\n\t\t\tconst compact = !context.isPartial && !context.expanded && !context.isError;\n\t\t\tif (compact) {\n\t\t\t\tconst pathDisplay = renderToolFileName(rawPath, theme, context.cwd);\n\t\t\t\tlet detail = pathDisplay;\n\t\t\t\tif (fileContent === null) {\n\t\t\t\t\tdetail += `\\n\\n${theme.fg(\"error\", \"[invalid content arg - expected string]\")}`;\n\t\t\t\t}\n\t\t\t\tcomponent.setText(\n\t\t\t\t\tformatGroupedCall({\n\t\t\t\t\t\trole: context.groupRole ?? \"singleton\",\n\t\t\t\t\t\tcompact: true,\n\t\t\t\t\t\tdot: toolStatusDotFromContext(context, theme),\n\t\t\t\t\t\ttitle: theme.fg(\"toolTitle\", theme.bold(\"write\")),\n\t\t\t\t\t\tdetail,\n\t\t\t\t\t}),\n\t\t\t\t);\n\t\t\t} else {\n\t\t\t\tcomponent.setText(\n\t\t\t\t\t`${toolStatusDotFromContext(context, theme)} ${formatWriteCall(\n\t\t\t\t\t\trenderArgs,\n\t\t\t\t\t\t{ expanded: context.expanded, isPartial: context.isPartial },\n\t\t\t\t\t\ttheme,\n\t\t\t\t\t\tcomponent.cache,\n\t\t\t\t\t\tcontext.cwd,\n\t\t\t\t\t\tfalse,\n\t\t\t\t\t)}`,\n\t\t\t\t);\n\t\t\t}\n\t\t\treturn component;\n\t\t},\n\t\trenderResult(result, _options, theme, context) {\n\t\t\tconst output = formatWriteResult({ ...result, isError: context.isError }, theme);\n\t\t\tif (!output) {\n\t\t\t\tconst component = (context.lastComponent as Container | undefined) ?? new Container();\n\t\t\t\tcomponent.clear();\n\t\t\t\treturn component;\n\t\t\t}\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(output);\n\t\t\treturn text;\n\t\t},\n\t};\n}\n\nexport function createWriteTool(cwd: string, options?: WriteToolOptions): AgentTool<typeof writeSchema> {\n\treturn wrapToolDefinition(createWriteToolDefinition(cwd, options));\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"commands.d.ts","sourceRoot":"","sources":["../../src/gateway/commands.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAWH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAEvD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,KAAK,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAmEhE,MAAM,WAAW,kBAAkB;IAClC,KAAK,EAAE,YAAY,CAAC;IACpB,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,eAAe,CAAC;IACzB,MAAM,EAAE,UAAU,CAAC;IACnB,OAAO,CAAC,EAAE,aAAa,CAAC;IACxB,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACnC,IAAI,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,WAAW;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,OAAO,CAAC;IACpB,YAAY,EAAE,OAAO,CAAC;IAEtB,OAAO,CAAC,GAAG,EAAE,kBAAkB,GAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAAC;CAC1D;AA2jBD,eAAO,MAAM,aAAa,EAAE,WAAW,EAetC,CAAC;AAEF,gGAAgG;AAChG,wBAAgB,eAAe,IAAI;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,EAAE,CAEzE;AAED,wBAAgB,cAAc,IAAI,MAAM,CASvC;AAED;;;;GAIG;AACH,wBAAsB,cAAc,CAAC,GAAG,EAAE,WAAW,EAAE,GAAG,EAAE,kBAAkB,GAAG,OAAO,CAAC,OAAO,CAAC,CAwBhG;AAED,uEAAuE;AACvE,wBAAsB,gBAAgB,CAAC,OAAO,EAAE,eAAe,EAAE,KAAK,EAAE,YAAY,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAKjH","sourcesContent":["/**\n * lunR: gateway chat command registry.\n *\n * Every command is a small headless handler over AgentSession. Router.ts keeps\n * platform gating/authorization and delegates to runChatCommand for busy/session\n * guards, error wrapping, and /help generation.\n */\n\nimport type { ThinkingLevel } from \"@ashx-j/lunr-agent\";\nimport type { Model } from \"@ashx-j/lunr-ai/compat\";\nimport { computeContextBreakdown } from \"../core/context-breakdown.ts\";\nimport type { ToolDefinition } from \"../core/extensions/types.ts\";\nimport { findExactModelReferenceMatch } from \"../core/model-resolver.ts\";\nimport type { ModelRuntime } from \"../core/model-runtime.ts\";\nimport type { SessionInfo } from \"../core/session-manager.ts\";\nimport { SessionManager } from \"../core/session-manager.ts\";\nimport { buildSwarmPrompt } from \"../core/swarm.ts\";\nimport type { BridgeSession } from \"./agent-bridge.ts\";\nimport { createPicker, type PickerItem } from \"./buttons.ts\";\nimport type { BridgeLike } from \"./router.ts\";\nimport type { MessageEvent, PlatformAdapter } from \"./types.ts\";\n\nconst MODEL_CACHE_TTL_MS = 5 * 60 * 1000;\nconst SESSIONS_CACHE_TTL_MS = 5 * 60 * 1000;\nconst MAX_MODEL_LIST_LINES = 15;\nconst PROVIDER_PER_PAGE = 10;\nconst _MODEL_PER_PAGE = 8;\nconst MAX_MODELS_PER_PROVIDER = 50;\nconst MODEL_ID_MAX = 38;\nconst MODEL_CACHE_CAP = 100;\nconst SESSIONS_CACHE_CAP = 100;\n\ninterface ModelCacheEntry {\n\tmodels: Model<any>[];\n\texpires: number;\n}\n\ninterface SessionsCacheEntry {\n\tsessions: SessionInfo[];\n\texpires: number;\n}\n\nconst modelCache = new Map<string, ModelCacheEntry>();\nconst sessionsCache = new Map<string, SessionsCacheEntry>();\n\nfunction evictLRU<K, V>(map: Map<K, V>, cap: number): void {\n\twhile (map.size > cap) {\n\t\tconst first = map.keys().next().value;\n\t\tif (first !== undefined) map.delete(first);\n\t}\n}\n\nfunction getCachedModels(key: string): Model<any>[] | undefined {\n\tconst cached = modelCache.get(key);\n\tif (!cached) return undefined;\n\tif (Date.now() > cached.expires) {\n\t\tmodelCache.delete(key);\n\t\treturn undefined;\n\t}\n\t// LRU refresh.\n\tmodelCache.delete(key);\n\tmodelCache.set(key, cached);\n\treturn cached.models;\n}\n\nfunction setCachedModels(key: string, models: Model<any>[]): void {\n\tmodelCache.set(key, { models, expires: Date.now() + MODEL_CACHE_TTL_MS });\n\tevictLRU(modelCache, MODEL_CACHE_CAP);\n}\n\nfunction getCachedSessions(key: string): SessionInfo[] | undefined {\n\tconst cached = sessionsCache.get(key);\n\tif (!cached) return undefined;\n\tif (Date.now() > cached.expires) {\n\t\tsessionsCache.delete(key);\n\t\treturn undefined;\n\t}\n\tsessionsCache.delete(key);\n\tsessionsCache.set(key, cached);\n\treturn cached.sessions;\n}\n\nfunction setCachedSessions(key: string, sessions: SessionInfo[]): void {\n\tsessionsCache.set(key, { sessions, expires: Date.now() + SESSIONS_CACHE_TTL_MS });\n\tevictLRU(sessionsCache, SESSIONS_CACHE_CAP);\n}\n\nexport interface ChatCommandContext {\n\tevent: MessageEvent;\n\tkey: string;\n\tadapter: PlatformAdapter;\n\tbridge: BridgeLike;\n\tsession?: BridgeSession;\n\treply(text: string): Promise<void>;\n\targs: string;\n}\n\nexport interface ChatCommand {\n\tname: string;\n\taliases?: string[];\n\tdescription: string;\n\tbypassBusy: boolean;\n\tneedsSession: boolean;\n\t// biome-ignore lint/suspicious/noConfusingVoidType: handlers may return nothing; runChatCommand treats undefined as consumed.\n\thandler(ctx: ChatCommandContext): Promise<boolean | void>;\n}\n\nfunction oneLine(text: string): string {\n\treturn text.replace(/\\s+/g, \" \").trim().slice(0, 200);\n}\n\nfunction prefixLines(text: string, prefix: string): string {\n\treturn text\n\t\t.split(\"\\n\")\n\t\t.map((line, index) => (index === 0 ? prefix : \" \") + line)\n\t\t.join(\"\\n\");\n}\n\nfunction truncate(text: string, max: number): string {\n\tif (text.length <= max) return text;\n\treturn `${text.slice(0, max - 1)}…`;\n}\n\nfunction formatRelativeTime(date: Date): string {\n\tconst seconds = Math.max(0, Math.floor((Date.now() - date.getTime()) / 1000));\n\tif (seconds < 60) return `${seconds}s ago`;\n\tconst minutes = Math.floor(seconds / 60);\n\tif (minutes < 60) return `${minutes}m ago`;\n\tconst hours = Math.floor(minutes / 60);\n\tif (hours < 24) return `${hours}h ago`;\n\tconst days = Math.floor(hours / 24);\n\treturn `${days}d ago`;\n}\n\nfunction formatModelList(models: Model<any>[], maxLines = MAX_MODEL_LIST_LINES): string[] {\n\tif (models.length === 0) return [\"No models available.\"];\n\tconst byProvider = new Map<string, Model<any>[]>();\n\tfor (const model of models) {\n\t\tconst list = byProvider.get(model.provider) ?? [];\n\t\tlist.push(model);\n\t\tbyProvider.set(model.provider, list);\n\t}\n\tconst providers = [...byProvider.keys()].sort();\n\tconst lines: string[] = [];\n\tlet index = 1;\n\tfor (const provider of providers) {\n\t\tconst list = (byProvider.get(provider) ?? []).sort((a, b) => a.id.localeCompare(b.id));\n\t\tfor (const model of list) {\n\t\t\tlines.push(`${index}) ${provider}/${model.id}`);\n\t\t\tindex++;\n\t\t}\n\t}\n\tif (lines.length <= maxLines) return lines;\n\treturn [\n\t\t`${models.length} models across ${providers.length} providers.`,\n\t\t\"Use /model <provider/id> or /model <query>.\",\n\t\t...providers.map((p) => `- ${p}`),\n\t];\n}\n\nfunction groupModelsByProvider(models: Model<any>[]): Map<string, Model<any>[]> {\n\tconst byProvider = new Map<string, Model<any>[]>();\n\tfor (const model of models) {\n\t\tconst list = byProvider.get(model.provider) ?? [];\n\t\tlist.push(model);\n\t\tbyProvider.set(model.provider, list);\n\t}\n\tfor (const list of byProvider.values()) {\n\t\tlist.sort((a, b) => a.id.localeCompare(b.id));\n\t}\n\treturn byProvider;\n}\n\nfunction truncateModelId(id: string): string {\n\tconst lastSlash = id.lastIndexOf(\"/\");\n\tconst name = lastSlash >= 0 ? id.slice(lastSlash + 1) : id;\n\treturn truncate(name, MODEL_ID_MAX);\n}\n\nasync function refreshAndCacheModels(session: BridgeSession, key: string): Promise<Model<any>[]> {\n\tconst runtime: ModelRuntime = session.modelRuntime;\n\tawait runtime.refresh();\n\tconst models = [...(await runtime.getAvailable())].sort(\n\t\t(a, b) => a.provider.localeCompare(b.provider) || a.id.localeCompare(b.id),\n\t);\n\tsetCachedModels(key, models);\n\treturn models;\n}\n\nasync function setModelAndReply(ctx: ChatCommandContext, model: Model<any>): Promise<void> {\n\tconst session = ctx.session!;\n\ttry {\n\t\tawait session.setModel(model);\n\t} catch (error) {\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\tif (message.includes(\"No API key\")) {\n\t\t\tawait ctx.reply(`No API key for ${model.provider}.`);\n\t\t\treturn;\n\t\t}\n\t\tthrow error;\n\t}\n\tawait ctx.reply(`Model → ${model.provider}/${model.id} (thinking level re-clamped to ${session.thinkingLevel})`);\n}\n\nconst newCommand: ChatCommand = {\n\tname: \"new\",\n\taliases: [\"reset\"],\n\tdescription: \"start a fresh session for this chat\",\n\tbypassBusy: false,\n\tneedsSession: false,\n\tasync handler(ctx) {\n\t\tawait ctx.bridge.reset(ctx.key);\n\t\tawait ctx.reply(\"Session reset — next message starts a fresh session.\");\n\t},\n};\n\nconst undoCommand: ChatCommand = {\n\tname: \"undo\",\n\tdescription: \"rewind the session tree before the last user turn\",\n\tbypassBusy: false,\n\tneedsSession: true,\n\tasync handler(ctx) {\n\t\tconst { userText } = await ctx.bridge.undo(ctx.key);\n\t\tconst text = userText ? ` \"${truncate(userText, 60)}\"` : \"\";\n\t\tawait ctx.reply(`Undone — rewound before${text}. /redo to restore.`);\n\t},\n};\n\nconst redoCommand: ChatCommand = {\n\tname: \"redo\",\n\tdescription: \"restore the last undone turn\",\n\tbypassBusy: false,\n\tneedsSession: true,\n\tasync handler(ctx) {\n\t\tawait ctx.bridge.redo(ctx.key);\n\t\tawait ctx.reply(\"Redone.\");\n\t},\n};\n\nconst stopCommand: ChatCommand = {\n\tname: \"stop\",\n\tdescription: \"abort the running turn\",\n\tbypassBusy: true,\n\tneedsSession: false,\n\tasync handler(ctx) {\n\t\tawait ctx.bridge.abort(ctx.key);\n\t\tawait ctx.reply(\"Stopped.\");\n\t},\n};\n\nconst statusCommand: ChatCommand = {\n\tname: \"status\",\n\tdescription: \"session status\",\n\tbypassBusy: true,\n\tneedsSession: false,\n\tasync handler(ctx) {\n\t\tconst status = ctx.bridge.getStatus(ctx.key);\n\t\tconst age = status.createdAt\n\t\t\t? `${Math.max(0, Math.round((Date.now() - Date.parse(status.createdAt)) / 1000))}s`\n\t\t\t: \"no session yet\";\n\t\tawait ctx.reply(\n\t\t\t`${ctx.event.source.platform} · session age ${age} · ${status.busy ? \"busy\" : \"idle\"} · queue ${status.queueDepth}`,\n\t\t);\n\t},\n};\n\nconst whoamiCommand: ChatCommand = {\n\tname: \"whoami\",\n\tdescription: \"your ids and session key\",\n\tbypassBusy: true,\n\tneedsSession: false,\n\tasync handler(ctx) {\n\t\tconst { userId, chatId } = ctx.event.source;\n\t\tawait ctx.reply(`userId: ${userId}\\nchatId: ${chatId}\\nkey: ${ctx.key}`);\n\t},\n};\n\nconst helpCommand: ChatCommand = {\n\tname: \"help\",\n\tdescription: \"this message\",\n\tbypassBusy: true,\n\tneedsSession: false,\n\tasync handler(ctx) {\n\t\tawait ctx.reply(formatHelpText());\n\t},\n};\n\nconst modelCommand: ChatCommand = {\n\tname: \"model\",\n\tdescription: \"list or switch models\",\n\tbypassBusy: false,\n\tneedsSession: true,\n\tasync handler(ctx) {\n\t\tconst session = ctx.session!;\n\t\tconst arg = ctx.args.trim();\n\t\tif (!arg) {\n\t\t\tconst cached = getCachedModels(ctx.key);\n\t\t\tconst models = cached ?? (await refreshAndCacheModels(session, ctx.key));\n\t\t\tconst byProvider = groupModelsByProvider(models);\n\t\t\tconst current = session.model;\n\t\t\tconst currentRef = current ? `${current.provider}/${current.id}` : \"\";\n\t\t\tconst providers = [...byProvider.keys()].sort();\n\t\t\tconst providerItems: PickerItem[] = providers.map((provider) => ({\n\t\t\t\tlabel: `${current?.provider === provider ? \"✓ \" : \"\"}${provider} (${byProvider.get(provider)?.length ?? 0})`,\n\t\t\t\tvalue: provider,\n\t\t\t}));\n\n\t\t\tconst pickerOpts = {\n\t\t\t\treplyTo: ctx.event.messageId,\n\t\t\t\tthreadId: ctx.event.source.threadId,\n\t\t\t};\n\t\t\tconst result = await createPicker(\n\t\t\t\tctx.adapter,\n\t\t\t\tctx.event.source,\n\t\t\t\t{\n\t\t\t\t\tkind: \"model\",\n\t\t\t\t\tsessionKey: ctx.key,\n\t\t\t\t\tinvokerId: ctx.event.source.userId,\n\t\t\t\t\titems: providerItems,\n\t\t\t\t\tperPage: PROVIDER_PER_PAGE,\n\t\t\t\t\ttitle: \"Pick a model provider\",\n\t\t\t\t\tasync resolve(item) {\n\t\t\t\t\t\tif (item.value === \"__back__\") {\n\t\t\t\t\t\t\treturn { done: false, items: providerItems, title: \"Pick a model provider\" };\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (!item.value.includes(\"/\")) {\n\t\t\t\t\t\t\tconst provider = item.value;\n\t\t\t\t\t\t\tconst list = byProvider.get(provider) ?? [];\n\t\t\t\t\t\t\tconst capped = list.slice(0, MAX_MODELS_PER_PROVIDER);\n\t\t\t\t\t\t\tconst note =\n\t\t\t\t\t\t\t\tlist.length > MAX_MODELS_PER_PROVIDER\n\t\t\t\t\t\t\t\t\t? ` — ${list.length - MAX_MODELS_PER_PROVIDER} more available`\n\t\t\t\t\t\t\t\t\t: \"\";\n\t\t\t\t\t\t\tconst modelItems: PickerItem[] = [];\n\t\t\t\t\t\t\tfor (const m of capped) {\n\t\t\t\t\t\t\t\tconst ref = `${provider}/${m.id}`;\n\t\t\t\t\t\t\t\tmodelItems.push({\n\t\t\t\t\t\t\t\t\tlabel: `${currentRef === ref ? \"✓ \" : \"\"}${truncateModelId(m.id)}`,\n\t\t\t\t\t\t\t\t\tvalue: ref,\n\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tmodelItems.unshift({ label: \"◀ Back\", value: \"__back__\" });\n\t\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\t\tdone: false,\n\t\t\t\t\t\t\t\titems: modelItems,\n\t\t\t\t\t\t\t\ttitle: `${provider} (${list.length})${note}`,\n\t\t\t\t\t\t\t\tbreadcrumbs: provider,\n\t\t\t\t\t\t\t};\n\t\t\t\t\t\t}\n\t\t\t\t\t\tconst exact = findExactModelReferenceMatch(item.value, models);\n\t\t\t\t\t\tif (!exact) {\n\t\t\t\t\t\t\treturn { done: true, text: \"That model is no longer available.\" };\n\t\t\t\t\t\t}\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tawait session.setModel(exact);\n\t\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\t\t\t\t\t\tif (message.includes(\"No API key\")) {\n\t\t\t\t\t\t\t\treturn { done: true, text: `No API key for ${exact.provider}.` };\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tthrow error;\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\tdone: true,\n\t\t\t\t\t\t\ttext: `☾ Model → ${exact.provider}/${exact.id} (thinking level re-clamped to ${session.thinkingLevel})`,\n\t\t\t\t\t\t};\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tpickerOpts,\n\t\t\t);\n\t\t\tif (!result.success) {\n\t\t\t\tconst currentText = `${current?.provider ?? \"none\"}/${current?.id ?? \"none\"}`;\n\t\t\t\tconst lines = formatModelList(models);\n\t\t\t\tawait ctx.reply([`Current: ${currentText}`, ...lines, \"Use /model <n> or /model <provider/id>\"].join(\"\\n\"));\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\tconst numeric = Number.parseInt(arg, 10);\n\t\tif (!Number.isNaN(numeric) && /^\\d+$/.test(arg)) {\n\t\t\tconst cached = getCachedModels(ctx.key);\n\t\t\tif (!cached || numeric < 1 || numeric > cached.length) {\n\t\t\t\tawait ctx.reply(\"Invalid selection — run /model to see the current list.\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tawait setModelAndReply(ctx, cached[numeric - 1]);\n\t\t\treturn;\n\t\t}\n\n\t\tconst models = getCachedModels(ctx.key) ?? (await refreshAndCacheModels(session, ctx.key));\n\t\tconst exact = findExactModelReferenceMatch(arg, models);\n\t\tif (exact) {\n\t\t\tawait setModelAndReply(ctx, exact);\n\t\t\treturn;\n\t\t}\n\n\t\tconst query = arg.toLowerCase();\n\t\tconst matches = models.filter(\n\t\t\t(m) =>\n\t\t\t\tm.id.toLowerCase().includes(query) ||\n\t\t\t\t`${m.provider}/${m.id}`.toLowerCase().includes(query) ||\n\t\t\t\t(m.name?.toLowerCase().includes(query) ?? false),\n\t\t);\n\t\tif (matches.length === 0) {\n\t\t\tawait ctx.reply(`No model matches \"${arg}\".`);\n\t\t\treturn;\n\t\t}\n\t\tif (matches.length > 1) {\n\t\t\tawait ctx.reply(formatModelList(matches).join(\"\\n\"));\n\t\t\treturn;\n\t\t}\n\t\tawait setModelAndReply(ctx, matches[0]);\n\t},\n};\n\nconst sessionsCommand: ChatCommand = {\n\tname: \"sessions\",\n\tdescription: \"list or switch sessions\",\n\tbypassBusy: false,\n\tneedsSession: true,\n\tasync handler(ctx) {\n\t\tconst session = ctx.session!;\n\t\tconst arg = ctx.args.trim();\n\t\tconst currentFile = session.sessionManager?.getSessionFile();\n\n\t\tif (!arg) {\n\t\t\tconst all = await SessionManager.list(process.cwd());\n\t\t\tall.sort((a, b) => b.modified.getTime() - a.modified.getTime());\n\t\t\tconst sessions = all.slice(0, 10);\n\t\t\tsetCachedSessions(ctx.key, sessions);\n\t\t\tconst items: PickerItem[] = sessions.map((s) => ({\n\t\t\t\tlabel: `${s.path === currentFile ? \"☾ \" : \"\"}${s.name ? truncate(s.name, 30) : truncate(s.firstMessage, 30) || \"(empty)\"}`,\n\t\t\t\tvalue: s.path,\n\t\t\t}));\n\t\t\tconst result = await createPicker(\n\t\t\t\tctx.adapter,\n\t\t\t\tctx.event.source,\n\t\t\t\t{\n\t\t\t\t\tkind: \"sessions\",\n\t\t\t\t\tsessionKey: ctx.key,\n\t\t\t\t\tinvokerId: ctx.event.source.userId,\n\t\t\t\t\titems,\n\t\t\t\t\tperPage: 8,\n\t\t\t\t\ttitle: \"Pick a session\",\n\t\t\t\t\tasync resolve(item) {\n\t\t\t\t\t\tawait ctx.bridge.switchSession(ctx.key, item.value);\n\t\t\t\t\t\tconst switched = await ctx.bridge.getSession(ctx.key);\n\t\t\t\t\t\tconst name = switched?.sessionManager?.getSessionName() ?? \"unnamed\";\n\t\t\t\t\t\tconst count = switched?.sessionManager?.getEntries().length ?? 0;\n\t\t\t\t\t\treturn { done: true, text: `☾ Switched to \"${name}\" (${count} msgs). History continues here.` };\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{ replyTo: ctx.event.messageId, threadId: ctx.event.source.threadId },\n\t\t\t);\n\t\t\tif (!result.success) {\n\t\t\t\tconst lines = sessions.map((s, i) => {\n\t\t\t\t\tconst marker = s.path === currentFile ? \" ☾\" : \"\";\n\t\t\t\t\tconst label = s.name ? truncate(s.name, 40) : truncate(s.firstMessage, 40) || \"(empty)\";\n\t\t\t\t\treturn `${i + 1}) ${label}${marker} · ${formatRelativeTime(s.modified)} · ${s.messageCount} msgs`;\n\t\t\t\t});\n\t\t\t\tawait ctx.reply(\n\t\t\t\t\t[`Sessions for ${process.cwd()}`, ...lines, \"Use /sessions <n> or /sessions <id-prefix>\"].join(\"\\n\"),\n\t\t\t\t);\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\tlet path: string | undefined;\n\t\tconst numeric = Number.parseInt(arg, 10);\n\t\tconst cached = getCachedSessions(ctx.key);\n\t\tif (!Number.isNaN(numeric) && /^\\d+$/.test(arg)) {\n\t\t\tif (!cached || numeric < 1 || numeric > cached.length) {\n\t\t\t\tawait ctx.reply(\"Invalid selection — run /sessions to see the current list.\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tpath = cached[numeric - 1].path;\n\t\t} else {\n\t\t\tconst pool = cached ?? (await SessionManager.list(process.cwd()));\n\t\t\tconst matches = pool.filter((s) => s.id.startsWith(arg) || s.path.startsWith(arg));\n\t\t\tif (matches.length === 0) {\n\t\t\t\tawait ctx.reply(`No session matches \"${arg}\".`);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (matches.length > 1) {\n\t\t\t\tawait ctx.reply(`Multiple sessions match \"${arg}\" — use a longer prefix.`);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tpath = matches[0].path;\n\t\t}\n\n\t\tif (!path) {\n\t\t\tawait ctx.reply(\"No session selected.\");\n\t\t\treturn;\n\t\t}\n\n\t\tawait ctx.bridge.switchSession(ctx.key, path);\n\t\tconst switched = await ctx.bridge.getSession(ctx.key);\n\t\tconst name = switched?.sessionManager?.getSessionName() ?? \"unnamed\";\n\t\tconst count = switched?.sessionManager?.getEntries().length ?? 0;\n\t\tawait ctx.reply(`Switched to \"${name}\" (${count} msgs). History continues here.`);\n\t},\n};\n\nconst titleCommand: ChatCommand = {\n\tname: \"title\",\n\taliases: [\"name\"],\n\tdescription: \"name the current session\",\n\tbypassBusy: true,\n\tneedsSession: true,\n\tasync handler(ctx) {\n\t\tconst session = ctx.session!;\n\t\tconst name = ctx.args.trim();\n\t\tif (!name) {\n\t\t\tconst current = session.sessionManager?.getSessionName();\n\t\t\tawait ctx.reply(current ? `Session title: ${current}` : \"(unnamed)\");\n\t\t\treturn;\n\t\t}\n\t\tsession.setSessionName(name);\n\t\tconst readBack = session.sessionManager?.getSessionName() ?? name;\n\t\tawait ctx.reply(`Session titled \"${readBack}\".`);\n\t},\n};\n\nconst contextCommand: ChatCommand = {\n\tname: \"context\",\n\tdescription: \"estimated context window breakdown\",\n\tbypassBusy: true,\n\tneedsSession: true,\n\tasync handler(ctx) {\n\t\tconst session = ctx.session!;\n\t\tconst model = session.model;\n\t\tif (!model) {\n\t\t\tawait ctx.reply(\"No model selected.\");\n\t\t\treturn;\n\t\t}\n\t\tconst contextWindow = model.contextWindow ?? 0;\n\t\tif (contextWindow <= 0) {\n\t\t\tawait ctx.reply(\"Context window unknown for this model.\");\n\t\t\treturn;\n\t\t}\n\n\t\tconst tools: ToolDefinition[] = session\n\t\t\t.getActiveToolNames()\n\t\t\t.map((name) => session.getToolDefinition(name))\n\t\t\t.filter((d): d is ToolDefinition => d !== undefined);\n\n\t\tconst breakdown = computeContextBreakdown({\n\t\t\tsystemPrompt: session.systemPrompt,\n\t\t\ttools,\n\t\t\tmessages: session.messages,\n\t\t\tcontextWindow,\n\t\t});\n\n\t\tconst usage = session.getContextUsage();\n\t\tconst tokens = usage?.tokens ?? breakdown.total;\n\t\tconst percent = usage?.percent ?? (contextWindow > 0 ? (tokens / contextWindow) * 100 : 0);\n\n\t\tconst messageTokens =\n\t\t\tbreakdown.user +\n\t\t\tbreakdown.assistantText +\n\t\t\tbreakdown.thinking +\n\t\t\tbreakdown.toolCalls +\n\t\t\tbreakdown.toolResults +\n\t\t\tbreakdown.summaries;\n\n\t\tconst stats = session.getSessionStats();\n\t\tawait ctx.reply(\n\t\t\t[\n\t\t\t\t`Context: ${tokens.toLocaleString(\"en-US\")} / ${contextWindow.toLocaleString(\"en-US\")} tokens (${Math.round(percent)}%)`,\n\t\t\t\t` system ${breakdown.systemPrompt.toLocaleString(\"en-US\")} · tools ${breakdown.toolDefinitions.toLocaleString(\"en-US\")} · messages ${messageTokens.toLocaleString(\"en-US\")}`,\n\t\t\t\t`Session: ${stats.totalMessages} messages · ${stats.userMessages} turns · model ${model.provider}/${model.id}`,\n\t\t\t].join(\"\\n\"),\n\t\t);\n\t},\n};\n\nconst swarmCommand: ChatCommand = {\n\tname: \"swarm\",\n\tdescription: \"decompose a task across parallel subagents\",\n\tbypassBusy: false,\n\tneedsSession: true,\n\tasync handler(ctx) {\n\t\tconst task = ctx.args.trim();\n\t\tif (!task) {\n\t\t\tawait ctx.reply(\"Usage: /swarm <task> — decomposes the task across parallel subagents.\");\n\t\t\treturn true;\n\t\t}\n\t\tctx.event.text = buildSwarmPrompt(task);\n\t\treturn false;\n\t},\n};\n\nconst compactCommand: ChatCommand = {\n\tname: \"compact\",\n\tdescription: \"summarize and compress session context\",\n\tbypassBusy: true,\n\tneedsSession: true,\n\tasync handler(ctx) {\n\t\tconst session = ctx.session!;\n\t\tconst instructions = ctx.args.trim() || undefined;\n\t\ttry {\n\t\t\tconst result = await session.compact(instructions);\n\t\t\tconst before = result.tokensBefore.toLocaleString(\"en-US\");\n\t\t\tconst after =\n\t\t\t\tresult.estimatedTokensAfter !== undefined\n\t\t\t\t\t? `~${result.estimatedTokensAfter.toLocaleString(\"en-US\")}`\n\t\t\t\t\t: \"~?\";\n\t\t\tawait ctx.reply(`Compacted: ${before} → ${after} tokens.`);\n\t\t} catch (error) {\n\t\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\t\tif (\n\t\t\t\tmessage.includes(\"Nothing to compact\") ||\n\t\t\t\tmessage.includes(\"Already compacted\") ||\n\t\t\t\tmessage.includes(\"No model selected\")\n\t\t\t) {\n\t\t\t\tawait ctx.reply(message);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tthrow error;\n\t\t}\n\t},\n};\n\nconst thinkingCommand: ChatCommand = {\n\tname: \"thinking\",\n\tdescription: \"get or set the thinking level\",\n\tbypassBusy: false,\n\tneedsSession: true,\n\tasync handler(ctx) {\n\t\tconst session = ctx.session!;\n\t\tconst levels = session.getAvailableThinkingLevels();\n\t\tif (!session.supportsThinking()) {\n\t\t\tawait ctx.reply(\n\t\t\t\t`Model ${session.model?.provider ?? \"none\"}/${session.model?.id ?? \"none\"} doesn't support thinking.`,\n\t\t\t);\n\t\t\treturn;\n\t\t}\n\t\tconst arg = ctx.args.trim().toLowerCase() as ThinkingLevel;\n\t\tif (!arg) {\n\t\t\tconst current = session.thinkingLevel;\n\t\t\tconst items: PickerItem[] = levels.map((level) => ({\n\t\t\t\tlabel: `${level === current ? \"✓ \" : \"\"}${level}`,\n\t\t\t\tvalue: level,\n\t\t\t}));\n\t\t\tconst result = await createPicker(\n\t\t\t\tctx.adapter,\n\t\t\t\tctx.event.source,\n\t\t\t\t{\n\t\t\t\t\tkind: \"thinking\",\n\t\t\t\t\tsessionKey: ctx.key,\n\t\t\t\t\tinvokerId: ctx.event.source.userId,\n\t\t\t\t\titems,\n\t\t\t\t\tperPage: levels.length,\n\t\t\t\t\ttitle: \"Pick a thinking level\",\n\t\t\t\t\tasync resolve(item) {\n\t\t\t\t\t\tconst level = item.value as ThinkingLevel;\n\t\t\t\t\t\tsession.setThinkingLevel(level);\n\t\t\t\t\t\treturn { done: true, text: `☾ Thinking → ${level}` };\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{ replyTo: ctx.event.messageId, threadId: ctx.event.source.threadId },\n\t\t\t);\n\t\t\tif (!result.success) {\n\t\t\t\tawait ctx.reply(`Level: ${current} — available: ${levels.join(\", \")}`);\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t\tif (!levels.includes(arg)) {\n\t\t\tawait ctx.reply(`Invalid level. Available: ${levels.join(\", \")}`);\n\t\t\treturn;\n\t\t}\n\t\tsession.setThinkingLevel(arg);\n\t\tawait ctx.reply(`Thinking → ${arg}`);\n\t},\n};\n\nexport const CHAT_COMMANDS: ChatCommand[] = [\n\tnewCommand,\n\tundoCommand,\n\tredoCommand,\n\tstopCommand,\n\tstatusCommand,\n\twhoamiCommand,\n\thelpCommand,\n\tmodelCommand,\n\tsessionsCommand,\n\ttitleCommand,\n\tcontextCommand,\n\tswarmCommand,\n\tcompactCommand,\n\tthinkingCommand,\n];\n\n/** Platform menu specs (e.g. Telegram setMyCommands): canonical names only, aliases skipped. */\nexport function botCommandSpecs(): { name: string; description: string }[] {\n\treturn CHAT_COMMANDS.map((c) => ({ name: c.name, description: c.description }));\n}\n\nexport function formatHelpText(): string {\n\tconst lines = [\"lunR gateway commands:\"];\n\tfor (const cmd of CHAT_COMMANDS) {\n\t\tconst names = [cmd.name, ...(cmd.aliases ?? [])].map((a) => `/${a}`).join(\" | \");\n\t\tlines.push(`${names} — ${cmd.description}`);\n\t}\n\tlines.push(\"\");\n\tlines.push(\"Tap to pick: /model, /thinking, /sessions (run without args to see buttons).\");\n\treturn lines.join(\"\\n\");\n}\n\n/**\n * Run a chat command with busy/session guards and error → \"⚠ one-line\".\n * Returns true when the event was consumed by the command, false when the\n * router should continue (e.g. /swarm mutates the event and wants a normal turn).\n */\nexport async function runChatCommand(cmd: ChatCommand, ctx: ChatCommandContext): Promise<boolean> {\n\tif (!cmd.bypassBusy && ctx.bridge.getStatus(ctx.key).busy) {\n\t\tawait ctx.reply(\"⏳ busy — /stop first or wait\");\n\t\treturn true;\n\t}\n\tif (cmd.needsSession) {\n\t\tconst session = await ctx.bridge.getSession(ctx.key);\n\t\tif (!session) {\n\t\t\tawait ctx.reply(\"No session yet — send a message first.\");\n\t\t\treturn true;\n\t\t}\n\t\tctx.session = session;\n\t}\n\ttry {\n\t\tconst consumed = await cmd.handler(ctx);\n\t\treturn consumed ?? true;\n\t} catch (err) {\n\t\tconst message = err instanceof Error ? err.message : String(err);\n\t\tawait ctx.adapter.send(ctx.event.source.chatId, `⚠ ${oneLine(message)}`, {\n\t\t\treplyTo: ctx.event.messageId,\n\t\t\tthreadId: ctx.event.source.threadId,\n\t\t});\n\t\treturn true;\n\t}\n}\n\n/** Reply helper used by the router to build the ChatCommandContext. */\nexport async function sendCommandReply(adapter: PlatformAdapter, event: MessageEvent, text: string): Promise<void> {\n\tawait adapter.send(event.source.chatId, prefixLines(text, \"☾ \"), {\n\t\treplyTo: event.messageId,\n\t\tthreadId: event.source.threadId,\n\t});\n}\n"]}
|
|
1
|
+
{"version":3,"file":"commands.d.ts","sourceRoot":"","sources":["../../src/gateway/commands.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAWH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAEvD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,KAAK,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAmEhE,MAAM,WAAW,kBAAkB;IAClC,KAAK,EAAE,YAAY,CAAC;IACpB,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,eAAe,CAAC;IACzB,MAAM,EAAE,UAAU,CAAC;IACnB,OAAO,CAAC,EAAE,aAAa,CAAC;IACxB,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACnC,IAAI,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,WAAW;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,OAAO,CAAC;IACpB,YAAY,EAAE,OAAO,CAAC;IAEtB,OAAO,CAAC,GAAG,EAAE,kBAAkB,GAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAAC;CAC1D;AA4jBD,eAAO,MAAM,aAAa,EAAE,WAAW,EAetC,CAAC;AAEF,gGAAgG;AAChG,wBAAgB,eAAe,IAAI;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,EAAE,CAEzE;AAED,wBAAgB,cAAc,IAAI,MAAM,CASvC;AAED;;;;GAIG;AACH,wBAAsB,cAAc,CAAC,GAAG,EAAE,WAAW,EAAE,GAAG,EAAE,kBAAkB,GAAG,OAAO,CAAC,OAAO,CAAC,CAwBhG;AAED,uEAAuE;AACvE,wBAAsB,gBAAgB,CAAC,OAAO,EAAE,eAAe,EAAE,KAAK,EAAE,YAAY,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAKjH","sourcesContent":["/**\n * lunR: gateway chat command registry.\n *\n * Every command is a small headless handler over AgentSession. Router.ts keeps\n * platform gating/authorization and delegates to runChatCommand for busy/session\n * guards, error wrapping, and /help generation.\n */\n\nimport type { ThinkingLevel } from \"@ashx-j/lunr-agent\";\nimport type { Model } from \"@ashx-j/lunr-ai/compat\";\nimport { computeContextBreakdown } from \"../core/context-breakdown.ts\";\nimport type { ToolDefinition } from \"../core/extensions/types.ts\";\nimport { findExactModelReferenceMatch } from \"../core/model-resolver.ts\";\nimport type { ModelRuntime } from \"../core/model-runtime.ts\";\nimport type { SessionInfo } from \"../core/session-manager.ts\";\nimport { SessionManager } from \"../core/session-manager.ts\";\nimport { buildSwarmPrompt } from \"../core/swarm.ts\";\nimport type { BridgeSession } from \"./agent-bridge.ts\";\nimport { createPicker, type PickerItem } from \"./buttons.ts\";\nimport type { BridgeLike } from \"./router.ts\";\nimport type { MessageEvent, PlatformAdapter } from \"./types.ts\";\n\nconst MODEL_CACHE_TTL_MS = 5 * 60 * 1000;\nconst SESSIONS_CACHE_TTL_MS = 5 * 60 * 1000;\nconst MAX_MODEL_LIST_LINES = 15;\nconst PROVIDER_PER_PAGE = 10;\nconst _MODEL_PER_PAGE = 8;\nconst MAX_MODELS_PER_PROVIDER = 50;\nconst MODEL_ID_MAX = 38;\nconst MODEL_CACHE_CAP = 100;\nconst SESSIONS_CACHE_CAP = 100;\n\ninterface ModelCacheEntry {\n\tmodels: Model<any>[];\n\texpires: number;\n}\n\ninterface SessionsCacheEntry {\n\tsessions: SessionInfo[];\n\texpires: number;\n}\n\nconst modelCache = new Map<string, ModelCacheEntry>();\nconst sessionsCache = new Map<string, SessionsCacheEntry>();\n\nfunction evictLRU<K, V>(map: Map<K, V>, cap: number): void {\n\twhile (map.size > cap) {\n\t\tconst first = map.keys().next().value;\n\t\tif (first !== undefined) map.delete(first);\n\t}\n}\n\nfunction getCachedModels(key: string): Model<any>[] | undefined {\n\tconst cached = modelCache.get(key);\n\tif (!cached) return undefined;\n\tif (Date.now() > cached.expires) {\n\t\tmodelCache.delete(key);\n\t\treturn undefined;\n\t}\n\t// LRU refresh.\n\tmodelCache.delete(key);\n\tmodelCache.set(key, cached);\n\treturn cached.models;\n}\n\nfunction setCachedModels(key: string, models: Model<any>[]): void {\n\tmodelCache.set(key, { models, expires: Date.now() + MODEL_CACHE_TTL_MS });\n\tevictLRU(modelCache, MODEL_CACHE_CAP);\n}\n\nfunction getCachedSessions(key: string): SessionInfo[] | undefined {\n\tconst cached = sessionsCache.get(key);\n\tif (!cached) return undefined;\n\tif (Date.now() > cached.expires) {\n\t\tsessionsCache.delete(key);\n\t\treturn undefined;\n\t}\n\tsessionsCache.delete(key);\n\tsessionsCache.set(key, cached);\n\treturn cached.sessions;\n}\n\nfunction setCachedSessions(key: string, sessions: SessionInfo[]): void {\n\tsessionsCache.set(key, { sessions, expires: Date.now() + SESSIONS_CACHE_TTL_MS });\n\tevictLRU(sessionsCache, SESSIONS_CACHE_CAP);\n}\n\nexport interface ChatCommandContext {\n\tevent: MessageEvent;\n\tkey: string;\n\tadapter: PlatformAdapter;\n\tbridge: BridgeLike;\n\tsession?: BridgeSession;\n\treply(text: string): Promise<void>;\n\targs: string;\n}\n\nexport interface ChatCommand {\n\tname: string;\n\taliases?: string[];\n\tdescription: string;\n\tbypassBusy: boolean;\n\tneedsSession: boolean;\n\t// biome-ignore lint/suspicious/noConfusingVoidType: handlers may return nothing; runChatCommand treats undefined as consumed.\n\thandler(ctx: ChatCommandContext): Promise<boolean | void>;\n}\n\nfunction oneLine(text: string): string {\n\treturn text.replace(/\\s+/g, \" \").trim().slice(0, 200);\n}\n\nfunction prefixLines(text: string, prefix: string): string {\n\treturn text\n\t\t.split(\"\\n\")\n\t\t.map((line, index) => (index === 0 ? prefix : \" \") + line)\n\t\t.join(\"\\n\");\n}\n\nfunction truncate(text: string, max: number): string {\n\tif (text.length <= max) return text;\n\treturn `${text.slice(0, max - 1)}…`;\n}\n\nfunction formatRelativeTime(date: Date): string {\n\tconst seconds = Math.max(0, Math.floor((Date.now() - date.getTime()) / 1000));\n\tif (seconds < 60) return `${seconds}s ago`;\n\tconst minutes = Math.floor(seconds / 60);\n\tif (minutes < 60) return `${minutes}m ago`;\n\tconst hours = Math.floor(minutes / 60);\n\tif (hours < 24) return `${hours}h ago`;\n\tconst days = Math.floor(hours / 24);\n\treturn `${days}d ago`;\n}\n\nfunction formatModelList(models: Model<any>[], maxLines = MAX_MODEL_LIST_LINES): string[] {\n\tif (models.length === 0) return [\"No models available.\"];\n\tconst byProvider = new Map<string, Model<any>[]>();\n\tfor (const model of models) {\n\t\tconst list = byProvider.get(model.provider) ?? [];\n\t\tlist.push(model);\n\t\tbyProvider.set(model.provider, list);\n\t}\n\tconst providers = [...byProvider.keys()].sort();\n\tconst lines: string[] = [];\n\tlet index = 1;\n\tfor (const provider of providers) {\n\t\tconst list = (byProvider.get(provider) ?? []).sort((a, b) => a.id.localeCompare(b.id));\n\t\tfor (const model of list) {\n\t\t\tlines.push(`${index}) ${provider}/${model.id}`);\n\t\t\tindex++;\n\t\t}\n\t}\n\tif (lines.length <= maxLines) return lines;\n\treturn [\n\t\t`${models.length} models across ${providers.length} providers.`,\n\t\t\"Use /model <provider/id> or /model <query>.\",\n\t\t...providers.map((p) => `- ${p}`),\n\t];\n}\n\nfunction groupModelsByProvider(models: Model<any>[]): Map<string, Model<any>[]> {\n\tconst byProvider = new Map<string, Model<any>[]>();\n\tfor (const model of models) {\n\t\tconst list = byProvider.get(model.provider) ?? [];\n\t\tlist.push(model);\n\t\tbyProvider.set(model.provider, list);\n\t}\n\tfor (const list of byProvider.values()) {\n\t\tlist.sort((a, b) => a.id.localeCompare(b.id));\n\t}\n\treturn byProvider;\n}\n\nfunction truncateModelId(id: string): string {\n\tconst lastSlash = id.lastIndexOf(\"/\");\n\tconst name = lastSlash >= 0 ? id.slice(lastSlash + 1) : id;\n\treturn truncate(name, MODEL_ID_MAX);\n}\n\nasync function refreshAndCacheModels(session: BridgeSession, key: string): Promise<Model<any>[]> {\n\tconst runtime: ModelRuntime = session.modelRuntime;\n\tawait runtime.refresh();\n\tconst models = [...(await runtime.getAvailable())].sort(\n\t\t(a, b) => a.provider.localeCompare(b.provider) || a.id.localeCompare(b.id),\n\t);\n\tsetCachedModels(key, models);\n\treturn models;\n}\n\nasync function setModelAndReply(ctx: ChatCommandContext, model: Model<any>): Promise<void> {\n\tconst session = ctx.session!;\n\ttry {\n\t\tawait session.setModel(model);\n\t} catch (error) {\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\tif (message.includes(\"No API key\")) {\n\t\t\tawait ctx.reply(`No API key for ${model.provider}.`);\n\t\t\treturn;\n\t\t}\n\t\tthrow error;\n\t}\n\tawait ctx.reply(`Model → ${model.provider}/${model.id} (thinking level re-clamped to ${session.thinkingLevel})`);\n}\n\nconst newCommand: ChatCommand = {\n\tname: \"new\",\n\taliases: [\"reset\"],\n\tdescription: \"start a fresh session for this chat\",\n\tbypassBusy: false,\n\tneedsSession: false,\n\tasync handler(ctx) {\n\t\tawait ctx.bridge.reset(ctx.key);\n\t\tawait ctx.reply(\"Session reset — next message starts a fresh session.\");\n\t},\n};\n\nconst undoCommand: ChatCommand = {\n\tname: \"undo\",\n\tdescription: \"rewind the session tree before the last user turn\",\n\tbypassBusy: false,\n\tneedsSession: true,\n\tasync handler(ctx) {\n\t\tconst { userText } = await ctx.bridge.undo(ctx.key);\n\t\tconst text = userText ? ` \"${truncate(userText, 60)}\"` : \"\";\n\t\tawait ctx.reply(`Undone — rewound before${text}. /redo to restore.`);\n\t},\n};\n\nconst redoCommand: ChatCommand = {\n\tname: \"redo\",\n\tdescription: \"restore the last undone turn\",\n\tbypassBusy: false,\n\tneedsSession: true,\n\tasync handler(ctx) {\n\t\tawait ctx.bridge.redo(ctx.key);\n\t\tawait ctx.reply(\"Redone.\");\n\t},\n};\n\nconst stopCommand: ChatCommand = {\n\tname: \"stop\",\n\tdescription: \"abort the running turn\",\n\tbypassBusy: true,\n\tneedsSession: false,\n\tasync handler(ctx) {\n\t\tawait ctx.bridge.abort(ctx.key);\n\t\tawait ctx.reply(\"Stopped.\");\n\t},\n};\n\nconst statusCommand: ChatCommand = {\n\tname: \"status\",\n\tdescription: \"session status\",\n\tbypassBusy: true,\n\tneedsSession: false,\n\tasync handler(ctx) {\n\t\tconst status = ctx.bridge.getStatus(ctx.key);\n\t\tconst age = status.createdAt\n\t\t\t? `${Math.max(0, Math.round((Date.now() - Date.parse(status.createdAt)) / 1000))}s`\n\t\t\t: \"no session yet\";\n\t\tawait ctx.reply(\n\t\t\t`${ctx.event.source.platform} · session age ${age} · ${status.busy ? \"busy\" : \"idle\"} · queue ${status.queueDepth}`,\n\t\t);\n\t},\n};\n\nconst whoamiCommand: ChatCommand = {\n\tname: \"whoami\",\n\tdescription: \"your ids and session key\",\n\tbypassBusy: true,\n\tneedsSession: false,\n\tasync handler(ctx) {\n\t\tconst { userId, chatId } = ctx.event.source;\n\t\tawait ctx.reply(`userId: ${userId}\\nchatId: ${chatId}\\nkey: ${ctx.key}`);\n\t},\n};\n\nconst helpCommand: ChatCommand = {\n\tname: \"help\",\n\tdescription: \"this message\",\n\tbypassBusy: true,\n\tneedsSession: false,\n\tasync handler(ctx) {\n\t\tawait ctx.reply(formatHelpText());\n\t},\n};\n\nconst modelCommand: ChatCommand = {\n\tname: \"model\",\n\tdescription: \"list or switch models\",\n\tbypassBusy: false,\n\tneedsSession: true,\n\tasync handler(ctx) {\n\t\tconst session = ctx.session!;\n\t\tconst arg = ctx.args.trim();\n\t\tif (!arg) {\n\t\t\tconst cached = getCachedModels(ctx.key);\n\t\t\tconst models = cached ?? (await refreshAndCacheModels(session, ctx.key));\n\t\t\tconst byProvider = groupModelsByProvider(models);\n\t\t\tconst current = session.model;\n\t\t\tconst currentRef = current ? `${current.provider}/${current.id}` : \"\";\n\t\t\tconst providers = [...byProvider.keys()].sort();\n\t\t\tconst providerItems: PickerItem[] = providers.map((provider) => ({\n\t\t\t\tlabel: `${current?.provider === provider ? \"✓ \" : \"\"}${provider} (${byProvider.get(provider)?.length ?? 0})`,\n\t\t\t\tvalue: provider,\n\t\t\t}));\n\n\t\t\tconst pickerOpts = {\n\t\t\t\treplyTo: ctx.event.messageId,\n\t\t\t\tthreadId: ctx.event.source.threadId,\n\t\t\t};\n\t\t\tconst result = await createPicker(\n\t\t\t\tctx.adapter,\n\t\t\t\tctx.event.source,\n\t\t\t\t{\n\t\t\t\t\tkind: \"model\",\n\t\t\t\t\tsessionKey: ctx.key,\n\t\t\t\t\tinvokerId: ctx.event.source.userId,\n\t\t\t\t\titems: providerItems,\n\t\t\t\t\tperPage: PROVIDER_PER_PAGE,\n\t\t\t\t\ttitle: \"Pick a model provider\",\n\t\t\t\t\tasync resolve(item) {\n\t\t\t\t\t\tif (item.value === \"__back__\") {\n\t\t\t\t\t\t\treturn { done: false, items: providerItems, title: \"Pick a model provider\" };\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (!item.value.includes(\"/\")) {\n\t\t\t\t\t\t\tconst provider = item.value;\n\t\t\t\t\t\t\tconst list = byProvider.get(provider) ?? [];\n\t\t\t\t\t\t\tconst capped = list.slice(0, MAX_MODELS_PER_PROVIDER);\n\t\t\t\t\t\t\tconst note =\n\t\t\t\t\t\t\t\tlist.length > MAX_MODELS_PER_PROVIDER\n\t\t\t\t\t\t\t\t\t? ` — ${list.length - MAX_MODELS_PER_PROVIDER} more available`\n\t\t\t\t\t\t\t\t\t: \"\";\n\t\t\t\t\t\t\tconst modelItems: PickerItem[] = [];\n\t\t\t\t\t\t\tfor (const m of capped) {\n\t\t\t\t\t\t\t\tconst ref = `${provider}/${m.id}`;\n\t\t\t\t\t\t\t\tmodelItems.push({\n\t\t\t\t\t\t\t\t\tlabel: `${currentRef === ref ? \"✓ \" : \"\"}${truncateModelId(m.id)}`,\n\t\t\t\t\t\t\t\t\tvalue: ref,\n\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tmodelItems.unshift({ label: \"◀ Back\", value: \"__back__\" });\n\t\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\t\tdone: false,\n\t\t\t\t\t\t\t\titems: modelItems,\n\t\t\t\t\t\t\t\ttitle: `${provider} (${list.length})${note}`,\n\t\t\t\t\t\t\t\tbreadcrumbs: provider,\n\t\t\t\t\t\t\t};\n\t\t\t\t\t\t}\n\t\t\t\t\t\tconst exact = findExactModelReferenceMatch(item.value, models);\n\t\t\t\t\t\tif (!exact) {\n\t\t\t\t\t\t\treturn { done: true, text: \"That model is no longer available.\" };\n\t\t\t\t\t\t}\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tawait session.setModel(exact);\n\t\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\t\t\t\t\t\tif (message.includes(\"No API key\")) {\n\t\t\t\t\t\t\t\treturn { done: true, text: `No API key for ${exact.provider}.` };\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tthrow error;\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\tdone: true,\n\t\t\t\t\t\t\ttext: `☾ Model → ${exact.provider}/${exact.id} (thinking level re-clamped to ${session.thinkingLevel})`,\n\t\t\t\t\t\t};\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tpickerOpts,\n\t\t\t);\n\t\t\tif (!result.success) {\n\t\t\t\tconst currentText = `${current?.provider ?? \"none\"}/${current?.id ?? \"none\"}`;\n\t\t\t\tconst lines = formatModelList(models);\n\t\t\t\tawait ctx.reply([`Current: ${currentText}`, ...lines, \"Use /model <n> or /model <provider/id>\"].join(\"\\n\"));\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\tconst numeric = Number.parseInt(arg, 10);\n\t\tif (!Number.isNaN(numeric) && /^\\d+$/.test(arg)) {\n\t\t\tconst cached = getCachedModels(ctx.key);\n\t\t\tif (!cached || numeric < 1 || numeric > cached.length) {\n\t\t\t\tawait ctx.reply(\"Invalid selection — run /model to see the current list.\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tawait setModelAndReply(ctx, cached[numeric - 1]);\n\t\t\treturn;\n\t\t}\n\n\t\tconst models = getCachedModels(ctx.key) ?? (await refreshAndCacheModels(session, ctx.key));\n\t\tconst exact = findExactModelReferenceMatch(arg, models);\n\t\tif (exact) {\n\t\t\tawait setModelAndReply(ctx, exact);\n\t\t\treturn;\n\t\t}\n\n\t\tconst query = arg.toLowerCase();\n\t\tconst matches = models.filter(\n\t\t\t(m) =>\n\t\t\t\tm.id.toLowerCase().includes(query) ||\n\t\t\t\t`${m.provider}/${m.id}`.toLowerCase().includes(query) ||\n\t\t\t\t(m.name?.toLowerCase().includes(query) ?? false),\n\t\t);\n\t\tif (matches.length === 0) {\n\t\t\tawait ctx.reply(`No model matches \"${arg}\".`);\n\t\t\treturn;\n\t\t}\n\t\tif (matches.length > 1) {\n\t\t\tawait ctx.reply(formatModelList(matches).join(\"\\n\"));\n\t\t\treturn;\n\t\t}\n\t\tawait setModelAndReply(ctx, matches[0]);\n\t},\n};\n\nconst sessionsCommand: ChatCommand = {\n\tname: \"sessions\",\n\tdescription: \"list or switch sessions\",\n\tbypassBusy: false,\n\tneedsSession: true,\n\tasync handler(ctx) {\n\t\tconst session = ctx.session!;\n\t\tconst arg = ctx.args.trim();\n\t\tconst currentFile = session.sessionManager?.getSessionFile();\n\n\t\tif (!arg) {\n\t\t\tconst all = await SessionManager.list(process.cwd());\n\t\t\tall.sort((a, b) => b.modified.getTime() - a.modified.getTime());\n\t\t\tconst sessions = all.slice(0, 10);\n\t\t\tsetCachedSessions(ctx.key, sessions);\n\t\t\tconst items: PickerItem[] = sessions.map((s) => ({\n\t\t\t\tlabel: `${s.path === currentFile ? \"☾ \" : \"\"}${s.name ? truncate(s.name, 30) : truncate(s.firstMessage, 30) || \"(empty)\"}`,\n\t\t\t\tvalue: s.path,\n\t\t\t}));\n\t\t\tconst result = await createPicker(\n\t\t\t\tctx.adapter,\n\t\t\t\tctx.event.source,\n\t\t\t\t{\n\t\t\t\t\tkind: \"sessions\",\n\t\t\t\t\tsessionKey: ctx.key,\n\t\t\t\t\tinvokerId: ctx.event.source.userId,\n\t\t\t\t\titems,\n\t\t\t\t\tperPage: 8,\n\t\t\t\t\ttitle: \"Pick a session\",\n\t\t\t\t\tasync resolve(item) {\n\t\t\t\t\t\tawait ctx.bridge.switchSession(ctx.key, item.value);\n\t\t\t\t\t\tconst switched = await ctx.bridge.getSession(ctx.key);\n\t\t\t\t\t\tconst name = switched?.sessionManager?.getSessionName() ?? \"unnamed\";\n\t\t\t\t\t\tconst count = switched?.sessionManager?.getEntries().length ?? 0;\n\t\t\t\t\t\treturn { done: true, text: `☾ Switched to \"${name}\" (${count} msgs). History continues here.` };\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{ replyTo: ctx.event.messageId, threadId: ctx.event.source.threadId },\n\t\t\t);\n\t\t\tif (!result.success) {\n\t\t\t\tconst lines = sessions.map((s, i) => {\n\t\t\t\t\tconst marker = s.path === currentFile ? \" ☾\" : \"\";\n\t\t\t\t\tconst label = s.name ? truncate(s.name, 40) : truncate(s.firstMessage, 40) || \"(empty)\";\n\t\t\t\t\treturn `${i + 1}) ${label}${marker} · ${formatRelativeTime(s.modified)} · ${s.messageCount} msgs`;\n\t\t\t\t});\n\t\t\t\tawait ctx.reply(\n\t\t\t\t\t[`Sessions for ${process.cwd()}`, ...lines, \"Use /sessions <n> or /sessions <id-prefix>\"].join(\"\\n\"),\n\t\t\t\t);\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\tlet path: string | undefined;\n\t\tconst numeric = Number.parseInt(arg, 10);\n\t\tconst cached = getCachedSessions(ctx.key);\n\t\tif (!Number.isNaN(numeric) && /^\\d+$/.test(arg)) {\n\t\t\tif (!cached || numeric < 1 || numeric > cached.length) {\n\t\t\t\tawait ctx.reply(\"Invalid selection — run /sessions to see the current list.\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tpath = cached[numeric - 1].path;\n\t\t} else {\n\t\t\tconst pool = cached ?? (await SessionManager.list(process.cwd()));\n\t\t\tconst matches = pool.filter((s) => s.id.startsWith(arg) || s.path.startsWith(arg));\n\t\t\tif (matches.length === 0) {\n\t\t\t\tawait ctx.reply(`No session matches \"${arg}\".`);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (matches.length > 1) {\n\t\t\t\tawait ctx.reply(`Multiple sessions match \"${arg}\" — use a longer prefix.`);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tpath = matches[0].path;\n\t\t}\n\n\t\tif (!path) {\n\t\t\tawait ctx.reply(\"No session selected.\");\n\t\t\treturn;\n\t\t}\n\n\t\tawait ctx.bridge.switchSession(ctx.key, path);\n\t\tconst switched = await ctx.bridge.getSession(ctx.key);\n\t\tconst name = switched?.sessionManager?.getSessionName() ?? \"unnamed\";\n\t\tconst count = switched?.sessionManager?.getEntries().length ?? 0;\n\t\tawait ctx.reply(`Switched to \"${name}\" (${count} msgs). History continues here.`);\n\t},\n};\n\nconst titleCommand: ChatCommand = {\n\tname: \"title\",\n\taliases: [\"name\"],\n\tdescription: \"name the current session\",\n\tbypassBusy: true,\n\tneedsSession: true,\n\tasync handler(ctx) {\n\t\tconst session = ctx.session!;\n\t\tconst name = ctx.args.trim();\n\t\tif (!name) {\n\t\t\tconst current = session.sessionManager?.getSessionName();\n\t\t\tawait ctx.reply(current ? `Session title: ${current}` : \"(unnamed)\");\n\t\t\treturn;\n\t\t}\n\t\tsession.setSessionName(name);\n\t\tconst readBack = session.sessionManager?.getSessionName() ?? name;\n\t\tawait ctx.reply(`Session titled \"${readBack}\".`);\n\t},\n};\n\nconst contextCommand: ChatCommand = {\n\tname: \"context\",\n\tdescription: \"estimated context window breakdown\",\n\tbypassBusy: true,\n\tneedsSession: true,\n\tasync handler(ctx) {\n\t\tconst session = ctx.session!;\n\t\tconst model = session.model;\n\t\tif (!model) {\n\t\t\tawait ctx.reply(\"No model selected.\");\n\t\t\treturn;\n\t\t}\n\t\tconst contextWindow = model.contextWindow ?? 0;\n\t\tif (contextWindow <= 0) {\n\t\t\tawait ctx.reply(\"Context window unknown for this model.\");\n\t\t\treturn;\n\t\t}\n\n\t\tconst tools: ToolDefinition[] = session\n\t\t\t.getActiveToolNames()\n\t\t\t.map((name) => session.getToolDefinition(name))\n\t\t\t.filter((d): d is ToolDefinition => d !== undefined);\n\n\t\tconst breakdown = computeContextBreakdown({\n\t\t\tsystemPrompt: session.systemPrompt,\n\t\t\ttools,\n\t\t\tmessages: session.messages,\n\t\t\tcontextWindow,\n\t\t});\n\n\t\tconst usage = session.getContextUsage();\n\t\tconst tokens = usage?.tokens ?? breakdown.total;\n\t\tconst percent = usage?.percent ?? (contextWindow > 0 ? (tokens / contextWindow) * 100 : 0);\n\n\t\tconst messageTokens =\n\t\t\tbreakdown.user +\n\t\t\tbreakdown.assistantText +\n\t\t\tbreakdown.thinking +\n\t\t\tbreakdown.toolCalls +\n\t\t\tbreakdown.toolResults +\n\t\t\tbreakdown.summaries;\n\n\t\tconst stats = session.getSessionStats();\n\t\tawait ctx.reply(\n\t\t\t[\n\t\t\t\t`Context: ${tokens.toLocaleString(\"en-US\")} / ${contextWindow.toLocaleString(\"en-US\")} tokens (${Math.round(percent)}%)`,\n\t\t\t\t` system ${breakdown.systemPrompt.toLocaleString(\"en-US\")} · tools ${breakdown.toolDefinitions.toLocaleString(\"en-US\")} · messages ${messageTokens.toLocaleString(\"en-US\")}`,\n\t\t\t\t`Session: ${stats.totalMessages} messages · ${stats.userMessages} turns · model ${model.provider}/${model.id}`,\n\t\t\t].join(\"\\n\"),\n\t\t);\n\t},\n};\n\nconst swarmCommand: ChatCommand = {\n\tname: \"swarm\",\n\tdescription: \"decompose a task across parallel subagents\",\n\tbypassBusy: false,\n\tneedsSession: true,\n\tasync handler(ctx) {\n\t\tconst task = ctx.args.trim();\n\t\tif (!task) {\n\t\t\tawait ctx.reply(\"Usage: /swarm <task> — decomposes the task across parallel subagents.\");\n\t\t\treturn true;\n\t\t}\n\t\tctx.event.text = buildSwarmPrompt(task);\n\t\treturn false;\n\t},\n};\n\nconst compactCommand: ChatCommand = {\n\tname: \"compact\",\n\tdescription: \"summarize and compress session context\",\n\tbypassBusy: true,\n\tneedsSession: true,\n\tasync handler(ctx) {\n\t\tconst session = ctx.session!;\n\t\tconst instructions = ctx.args.trim() || undefined;\n\t\ttry {\n\t\t\tconst result = await session.compact(instructions);\n\t\t\tconst before = result.tokensBefore.toLocaleString(\"en-US\");\n\t\t\tconst after =\n\t\t\t\tresult.estimatedTokensAfter !== undefined\n\t\t\t\t\t? `~${result.estimatedTokensAfter.toLocaleString(\"en-US\")}`\n\t\t\t\t\t: \"~?\";\n\t\t\tawait ctx.reply(`Compacted: ${before} → ${after} tokens.`);\n\t\t} catch (error) {\n\t\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\t\tif (\n\t\t\t\tmessage.includes(\"Nothing to compact\") ||\n\t\t\t\tmessage.includes(\"Already compacted\") ||\n\t\t\t\tmessage.includes(\"No model selected\")\n\t\t\t) {\n\t\t\t\tawait ctx.reply(message);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tthrow error;\n\t\t}\n\t},\n};\n\nconst thinkingCommand: ChatCommand = {\n\tname: \"thinking\",\n\taliases: [\"effort\", \"reasoning\"],\n\tdescription: \"get or set the thinking level\",\n\tbypassBusy: false,\n\tneedsSession: true,\n\tasync handler(ctx) {\n\t\tconst session = ctx.session!;\n\t\tconst levels = session.getAvailableThinkingLevels();\n\t\tif (!session.supportsThinking()) {\n\t\t\tawait ctx.reply(\n\t\t\t\t`Model ${session.model?.provider ?? \"none\"}/${session.model?.id ?? \"none\"} doesn't support thinking.`,\n\t\t\t);\n\t\t\treturn;\n\t\t}\n\t\tconst arg = ctx.args.trim().toLowerCase() as ThinkingLevel;\n\t\tif (!arg) {\n\t\t\tconst current = session.thinkingLevel;\n\t\t\tconst items: PickerItem[] = levels.map((level) => ({\n\t\t\t\tlabel: `${level === current ? \"✓ \" : \"\"}${level}`,\n\t\t\t\tvalue: level,\n\t\t\t}));\n\t\t\tconst result = await createPicker(\n\t\t\t\tctx.adapter,\n\t\t\t\tctx.event.source,\n\t\t\t\t{\n\t\t\t\t\tkind: \"thinking\",\n\t\t\t\t\tsessionKey: ctx.key,\n\t\t\t\t\tinvokerId: ctx.event.source.userId,\n\t\t\t\t\titems,\n\t\t\t\t\tperPage: levels.length,\n\t\t\t\t\ttitle: \"Pick a thinking level\",\n\t\t\t\t\tasync resolve(item) {\n\t\t\t\t\t\tconst level = item.value as ThinkingLevel;\n\t\t\t\t\t\tsession.setThinkingLevel(level);\n\t\t\t\t\t\treturn { done: true, text: `☾ Thinking → ${level}` };\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{ replyTo: ctx.event.messageId, threadId: ctx.event.source.threadId },\n\t\t\t);\n\t\t\tif (!result.success) {\n\t\t\t\tawait ctx.reply(`Level: ${current} — available: ${levels.join(\", \")}`);\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t\tif (!levels.includes(arg)) {\n\t\t\tawait ctx.reply(`Invalid level. Available: ${levels.join(\", \")}`);\n\t\t\treturn;\n\t\t}\n\t\tsession.setThinkingLevel(arg);\n\t\tawait ctx.reply(`Thinking → ${arg}`);\n\t},\n};\n\nexport const CHAT_COMMANDS: ChatCommand[] = [\n\tnewCommand,\n\tundoCommand,\n\tredoCommand,\n\tstopCommand,\n\tstatusCommand,\n\twhoamiCommand,\n\thelpCommand,\n\tmodelCommand,\n\tsessionsCommand,\n\ttitleCommand,\n\tcontextCommand,\n\tswarmCommand,\n\tcompactCommand,\n\tthinkingCommand,\n];\n\n/** Platform menu specs (e.g. Telegram setMyCommands): canonical names only, aliases skipped. */\nexport function botCommandSpecs(): { name: string; description: string }[] {\n\treturn CHAT_COMMANDS.map((c) => ({ name: c.name, description: c.description }));\n}\n\nexport function formatHelpText(): string {\n\tconst lines = [\"lunR gateway commands:\"];\n\tfor (const cmd of CHAT_COMMANDS) {\n\t\tconst names = [cmd.name, ...(cmd.aliases ?? [])].map((a) => `/${a}`).join(\" | \");\n\t\tlines.push(`${names} — ${cmd.description}`);\n\t}\n\tlines.push(\"\");\n\tlines.push(\"Tap to pick: /model, /thinking, /sessions (run without args to see buttons).\");\n\treturn lines.join(\"\\n\");\n}\n\n/**\n * Run a chat command with busy/session guards and error → \"⚠ one-line\".\n * Returns true when the event was consumed by the command, false when the\n * router should continue (e.g. /swarm mutates the event and wants a normal turn).\n */\nexport async function runChatCommand(cmd: ChatCommand, ctx: ChatCommandContext): Promise<boolean> {\n\tif (!cmd.bypassBusy && ctx.bridge.getStatus(ctx.key).busy) {\n\t\tawait ctx.reply(\"⏳ busy — /stop first or wait\");\n\t\treturn true;\n\t}\n\tif (cmd.needsSession) {\n\t\tconst session = await ctx.bridge.getSession(ctx.key);\n\t\tif (!session) {\n\t\t\tawait ctx.reply(\"No session yet — send a message first.\");\n\t\t\treturn true;\n\t\t}\n\t\tctx.session = session;\n\t}\n\ttry {\n\t\tconst consumed = await cmd.handler(ctx);\n\t\treturn consumed ?? true;\n\t} catch (err) {\n\t\tconst message = err instanceof Error ? err.message : String(err);\n\t\tawait ctx.adapter.send(ctx.event.source.chatId, `⚠ ${oneLine(message)}`, {\n\t\t\treplyTo: ctx.event.messageId,\n\t\t\tthreadId: ctx.event.source.threadId,\n\t\t});\n\t\treturn true;\n\t}\n}\n\n/** Reply helper used by the router to build the ChatCommandContext. */\nexport async function sendCommandReply(adapter: PlatformAdapter, event: MessageEvent, text: string): Promise<void> {\n\tawait adapter.send(event.source.chatId, prefixLines(text, \"☾ \"), {\n\t\treplyTo: event.messageId,\n\t\tthreadId: event.source.threadId,\n\t});\n}\n"]}
|
package/dist/gateway/commands.js
CHANGED