my-pi 0.0.13 → 0.1.0

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/api.js CHANGED
@@ -1,2 +1,2 @@
1
- import { n as create_my_pi, r as runPrintMode, t as InteractiveMode } from "./api-CWEizv2k.js";
1
+ import { n as create_my_pi, r as runPrintMode, t as InteractiveMode } from "./api-1ZXLxSgP.js";
2
2
  export { InteractiveMode, create_my_pi, runPrintMode };
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { n as create_my_pi } from "./api-CWEizv2k.js";
2
+ import { n as create_my_pi } from "./api-1ZXLxSgP.js";
3
3
  import { InteractiveMode, runPrintMode } from "@mariozechner/pi-coding-agent";
4
4
  import { defineCommand, runMain } from "citty";
5
5
  import { readFileSync } from "node:fs";
@@ -106,6 +106,11 @@ runMain(defineCommand({
106
106
  description: "Disable LSP extension",
107
107
  default: false
108
108
  },
109
+ "no-session-name": {
110
+ type: "boolean",
111
+ description: "Disable session name extension",
112
+ default: false
113
+ },
109
114
  telemetry: {
110
115
  type: "boolean",
111
116
  description: "Enable local SQLite telemetry for this process",
@@ -179,6 +184,7 @@ runMain(defineCommand({
179
184
  recall: !args["no-builtin"] && !args["no-recall"],
180
185
  prompt_presets: !args["no-builtin"] && !args["no-prompt-presets"],
181
186
  lsp: !args["no-builtin"] && !args["no-lsp"],
187
+ session_name: !args["no-builtin"] && !args["no-session-name"],
182
188
  telemetry: telemetry_override,
183
189
  telemetry_db_path: args["telemetry-db"],
184
190
  model: args.model,
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["#!/usr/bin/env node\n\n// CLI for my-pi — composable pi coding agent\n// Extension stacking patterns inspired by https://github.com/disler/pi-vs-claude-code\n\nimport {\n\tInteractiveMode,\n\trunPrintMode,\n} from '@mariozechner/pi-coding-agent';\nimport { defineCommand, runMain } from 'citty';\nimport { readFileSync } from 'node:fs';\nimport { dirname, join, resolve } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { create_my_pi } from './api.js';\n\n// Suppress node:sqlite ExperimentalWarning\nprocess.removeAllListeners('warning');\nprocess.on('warning', (warning) => {\n\tif (warning.name !== 'ExperimentalWarning') {\n\t\tconsole.warn(warning);\n\t}\n});\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\nconst pkg = JSON.parse(\n\treadFileSync(join(__dirname, '..', 'package.json'), 'utf-8'),\n);\n\n// citty can't handle repeatable args, so parse -e from argv directly\n// (citty uses strict: false, so unknown flags are silently ignored)\nfunction parse_extension_paths(argv: string[]): string[] {\n\tconst paths: string[] = [];\n\tfor (let i = 0; i < argv.length; i++) {\n\t\tif (\n\t\t\t(argv[i] === '-e' || argv[i] === '--extension') &&\n\t\t\ti + 1 < argv.length\n\t\t) {\n\t\t\tpaths.push(resolve(argv[++i]));\n\t\t}\n\t}\n\treturn paths;\n}\n\nasync function read_stdin(): Promise<string> {\n\tconst chunks: Buffer[] = [];\n\tfor await (const chunk of process.stdin) {\n\t\tchunks.push(chunk as Buffer);\n\t}\n\treturn Buffer.concat(chunks).toString('utf-8').trim();\n}\n\nfunction print_usage(): void {\n\tconsole.log(`my-pi v${pkg.version} — composable pi coding agent\\n`);\n\tconsole.log('Usage:');\n\tconsole.log(\n\t\t' my-pi \"prompt\" One-shot print mode',\n\t);\n\tconsole.log(\n\t\t' my-pi Interactive TUI mode',\n\t);\n\tconsole.log(\n\t\t' my-pi -P \"prompt\" Explicit print mode',\n\t);\n\tconsole.log(\n\t\t' my-pi --json \"prompt\" NDJSON output for agents',\n\t);\n\tconsole.log(\n\t\t' my-pi -e ext.ts Stack an extension',\n\t);\n\tconsole.log(\n\t\t' my-pi -e a.ts -e b.ts Stack multiple extensions',\n\t);\n\tconsole.log(\n\t\t' my-pi --telemetry --json \"task\" Enable local SQLite telemetry',\n\t);\n\tconsole.log(\n\t\t' my-pi --agent-dir /tmp/pi-agent Override auth/config/session dir',\n\t);\n\tconsole.log(\n\t\t' echo \"prompt\" | my-pi --json Pipe stdin as prompt',\n\t);\n\tconsole.log(\n\t\t' my-pi -m claude-haiku-4-5-20241022 Set initial model',\n\t);\n\tconsole.log(\n\t\t' my-pi --no-builtin -e ext.ts Skip all built-in extensions',\n\t);\n}\n\nconst main = defineCommand({\n\tmeta: {\n\t\tname: 'my-pi',\n\t\tversion: pkg.version,\n\t\tdescription:\n\t\t\t'Composable pi coding agent with MCP, LSP, chains, presets, and local eval telemetry',\n\t},\n\targs: {\n\t\tprint: {\n\t\t\ttype: 'boolean',\n\t\t\talias: 'P',\n\t\t\tdescription: 'Print mode (non-interactive, one-shot)',\n\t\t\tdefault: false,\n\t\t},\n\t\t'agent-dir': {\n\t\t\ttype: 'string',\n\t\t\tdescription:\n\t\t\t\t'Override Pi auth/config/session directory for this process',\n\t\t\trequired: false,\n\t\t},\n\t\tjson: {\n\t\t\ttype: 'boolean',\n\t\t\talias: 'j',\n\t\t\tdescription: 'Output NDJSON events (for agent consumption)',\n\t\t\tdefault: false,\n\t\t},\n\t\t'no-builtin': {\n\t\t\ttype: 'boolean',\n\t\t\tdescription: 'Disable all built-in extensions',\n\t\t\tdefault: false,\n\t\t},\n\t\t'no-mcp': {\n\t\t\ttype: 'boolean',\n\t\t\tdescription: 'Disable built-in MCP extension',\n\t\t\tdefault: false,\n\t\t},\n\t\t'no-skills': {\n\t\t\ttype: 'boolean',\n\t\t\tdescription: 'Disable built-in skills extension',\n\t\t\tdefault: false,\n\t\t},\n\t\t'no-chain': {\n\t\t\ttype: 'boolean',\n\t\t\tdescription: 'Disable built-in chain extension',\n\t\t\tdefault: false,\n\t\t},\n\t\t'no-filter': {\n\t\t\ttype: 'boolean',\n\t\t\tdescription: 'Disable secret redaction in tool output',\n\t\t\tdefault: false,\n\t\t},\n\t\t'no-handoff': {\n\t\t\ttype: 'boolean',\n\t\t\tdescription: 'Disable handoff extension',\n\t\t\tdefault: false,\n\t\t},\n\t\t'no-recall': {\n\t\t\ttype: 'boolean',\n\t\t\tdescription: 'Disable recall extension',\n\t\t\tdefault: false,\n\t\t},\n\t\t'no-prompt-presets': {\n\t\t\ttype: 'boolean',\n\t\t\tdescription: 'Disable prompt presets extension',\n\t\t\tdefault: false,\n\t\t},\n\t\t'no-lsp': {\n\t\t\ttype: 'boolean',\n\t\t\tdescription: 'Disable LSP extension',\n\t\t\tdefault: false,\n\t\t},\n\t\ttelemetry: {\n\t\t\ttype: 'boolean',\n\t\t\tdescription: 'Enable local SQLite telemetry for this process',\n\t\t\tdefault: false,\n\t\t},\n\t\t'no-telemetry': {\n\t\t\ttype: 'boolean',\n\t\t\tdescription: 'Disable local SQLite telemetry for this process',\n\t\t\tdefault: false,\n\t\t},\n\t\t'telemetry-db': {\n\t\t\ttype: 'string',\n\t\t\tdescription:\n\t\t\t\t'Override telemetry database path for this process',\n\t\t\trequired: false,\n\t\t},\n\t\tmodel: {\n\t\t\ttype: 'string',\n\t\t\talias: 'm',\n\t\t\tdescription:\n\t\t\t\t'Model to use (e.g. claude-sonnet-4-5-20241022, gpt-5.4)',\n\t\t},\n\t\t'system-prompt': {\n\t\t\ttype: 'string',\n\t\t\tdescription: 'Replace the base system prompt',\n\t\t\trequired: false,\n\t\t},\n\t\t'append-system-prompt': {\n\t\t\ttype: 'string',\n\t\t\tdescription: 'Append one-off instructions to the system prompt',\n\t\t\trequired: false,\n\t\t},\n\t\tprompt: {\n\t\t\ttype: 'string',\n\t\t\talias: 'p',\n\t\t\tdescription: 'Prompt text (alternative to positional argument)',\n\t\t\trequired: false,\n\t\t},\n\t},\n\tasync run({ args }) {\n\t\tconst cwd = process.cwd();\n\t\tconst extension_paths = parse_extension_paths(process.argv);\n\n\t\t// Resolve prompt: named --prompt flag > positional > stdin\n\t\tlet prompt = args.prompt;\n\t\tif (!prompt) {\n\t\t\t// Check for positional arguments (after citty strips flags)\n\t\t\tconst positionals = (args as any)._ as string[] | undefined;\n\t\t\tif (positionals && positionals.length > 0) {\n\t\t\t\tprompt = positionals[0];\n\t\t\t}\n\t\t}\n\t\tif (!prompt && !process.stdin.isTTY) {\n\t\t\tprompt = await read_stdin();\n\t\t}\n\n\t\t// Model validation (issue #5)\n\t\tif (args.model && /[/\\\\]/.test(args.model)) {\n\t\t\tconsole.error(\n\t\t\t\t`Error: Invalid model \"${args.model}\". Use bare model names without provider prefixes.`,\n\t\t\t);\n\t\t\tconsole.error(\n\t\t\t\t` Examples: claude-sonnet-4-5-20241022, gpt-5.4, mistral-large`,\n\t\t\t);\n\t\t\tprocess.exit(1);\n\t\t}\n\n\t\tif (\n\t\t\t!args.print &&\n\t\t\t!args.json &&\n\t\t\t!prompt &&\n\t\t\t!process.stdout.isTTY\n\t\t) {\n\t\t\tprint_usage();\n\t\t\treturn;\n\t\t}\n\n\t\t// Startup feedback so silence = broken (issue #3)\n\t\tif (args.print || args.json || prompt) {\n\t\t\tprocess.stderr.write(\n\t\t\t\t`my-pi: connecting to ${args.model || 'default model'}...\\n`,\n\t\t\t);\n\t\t}\n\n\t\tif (args.telemetry && args['no-telemetry']) {\n\t\t\tconsole.error(\n\t\t\t\t'Error: --telemetry and --no-telemetry cannot be used together.',\n\t\t\t);\n\t\t\tprocess.exit(1);\n\t\t}\n\n\t\tconst telemetry_override = args.telemetry\n\t\t\t? true\n\t\t\t: args['no-telemetry']\n\t\t\t\t? false\n\t\t\t\t: undefined;\n\n\t\tconst runtime = await create_my_pi({\n\t\t\tcwd,\n\t\t\tagent_dir: args['agent-dir'],\n\t\t\textensions: extension_paths,\n\t\t\tmcp: !args['no-builtin'] && !args['no-mcp'],\n\t\t\tskills: !args['no-builtin'] && !args['no-skills'],\n\t\t\tchain: !args['no-builtin'] && !args['no-chain'],\n\t\t\tfilter_output: !args['no-builtin'] && !args['no-filter'],\n\t\t\thandoff: !args['no-builtin'] && !args['no-handoff'],\n\t\t\trecall: !args['no-builtin'] && !args['no-recall'],\n\t\t\tprompt_presets:\n\t\t\t\t!args['no-builtin'] && !args['no-prompt-presets'],\n\t\t\tlsp: !args['no-builtin'] && !args['no-lsp'],\n\t\t\ttelemetry: telemetry_override,\n\t\t\ttelemetry_db_path: args['telemetry-db'],\n\t\t\tmodel: args.model,\n\t\t\tsystem_prompt: args['system-prompt'],\n\t\t\tappend_system_prompt: args['append-system-prompt'],\n\t\t});\n\n\t\tif (args.print || args.json || prompt) {\n\t\t\tconst code = await runPrintMode(runtime, {\n\t\t\t\tmode: args.json ? 'json' : 'text',\n\t\t\t\tinitialMessage: prompt || '',\n\t\t\t\tinitialImages: [],\n\t\t\t\tmessages: [],\n\t\t\t});\n\t\t\tprocess.exit(code);\n\t\t} else if (!process.stdout.isTTY) {\n\t\t\tprint_usage();\n\t\t} else {\n\t\t\tconst mode = new InteractiveMode(runtime, {\n\t\t\t\tmigratedProviders: [],\n\t\t\t\tmodelFallbackMessage: undefined,\n\t\t\t\tinitialMessage: undefined,\n\t\t\t\tinitialImages: [],\n\t\t\t\tinitialMessages: [],\n\t\t\t});\n\t\t\tawait mode.run();\n\t\t}\n\t},\n});\n\nvoid runMain(main);\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["#!/usr/bin/env node\n\n// CLI for my-pi — composable pi coding agent\n// Extension stacking patterns inspired by https://github.com/disler/pi-vs-claude-code\n\nimport {\n\tInteractiveMode,\n\trunPrintMode,\n} from '@mariozechner/pi-coding-agent';\nimport { defineCommand, runMain } from 'citty';\nimport { readFileSync } from 'node:fs';\nimport { dirname, join, resolve } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { create_my_pi } from './api.js';\n\n// Suppress node:sqlite ExperimentalWarning\nprocess.removeAllListeners('warning');\nprocess.on('warning', (warning) => {\n\tif (warning.name !== 'ExperimentalWarning') {\n\t\tconsole.warn(warning);\n\t}\n});\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\nconst pkg = JSON.parse(\n\treadFileSync(join(__dirname, '..', 'package.json'), 'utf-8'),\n);\n\n// citty can't handle repeatable args, so parse -e from argv directly\n// (citty uses strict: false, so unknown flags are silently ignored)\nfunction parse_extension_paths(argv: string[]): string[] {\n\tconst paths: string[] = [];\n\tfor (let i = 0; i < argv.length; i++) {\n\t\tif (\n\t\t\t(argv[i] === '-e' || argv[i] === '--extension') &&\n\t\t\ti + 1 < argv.length\n\t\t) {\n\t\t\tpaths.push(resolve(argv[++i]));\n\t\t}\n\t}\n\treturn paths;\n}\n\nasync function read_stdin(): Promise<string> {\n\tconst chunks: Buffer[] = [];\n\tfor await (const chunk of process.stdin) {\n\t\tchunks.push(chunk as Buffer);\n\t}\n\treturn Buffer.concat(chunks).toString('utf-8').trim();\n}\n\nfunction print_usage(): void {\n\tconsole.log(`my-pi v${pkg.version} — composable pi coding agent\\n`);\n\tconsole.log('Usage:');\n\tconsole.log(\n\t\t' my-pi \"prompt\" One-shot print mode',\n\t);\n\tconsole.log(\n\t\t' my-pi Interactive TUI mode',\n\t);\n\tconsole.log(\n\t\t' my-pi -P \"prompt\" Explicit print mode',\n\t);\n\tconsole.log(\n\t\t' my-pi --json \"prompt\" NDJSON output for agents',\n\t);\n\tconsole.log(\n\t\t' my-pi -e ext.ts Stack an extension',\n\t);\n\tconsole.log(\n\t\t' my-pi -e a.ts -e b.ts Stack multiple extensions',\n\t);\n\tconsole.log(\n\t\t' my-pi --telemetry --json \"task\" Enable local SQLite telemetry',\n\t);\n\tconsole.log(\n\t\t' my-pi --agent-dir /tmp/pi-agent Override auth/config/session dir',\n\t);\n\tconsole.log(\n\t\t' echo \"prompt\" | my-pi --json Pipe stdin as prompt',\n\t);\n\tconsole.log(\n\t\t' my-pi -m claude-haiku-4-5-20241022 Set initial model',\n\t);\n\tconsole.log(\n\t\t' my-pi --no-builtin -e ext.ts Skip all built-in extensions',\n\t);\n}\n\nconst main = defineCommand({\n\tmeta: {\n\t\tname: 'my-pi',\n\t\tversion: pkg.version,\n\t\tdescription:\n\t\t\t'Composable pi coding agent with MCP, LSP, chains, presets, and local eval telemetry',\n\t},\n\targs: {\n\t\tprint: {\n\t\t\ttype: 'boolean',\n\t\t\talias: 'P',\n\t\t\tdescription: 'Print mode (non-interactive, one-shot)',\n\t\t\tdefault: false,\n\t\t},\n\t\t'agent-dir': {\n\t\t\ttype: 'string',\n\t\t\tdescription:\n\t\t\t\t'Override Pi auth/config/session directory for this process',\n\t\t\trequired: false,\n\t\t},\n\t\tjson: {\n\t\t\ttype: 'boolean',\n\t\t\talias: 'j',\n\t\t\tdescription: 'Output NDJSON events (for agent consumption)',\n\t\t\tdefault: false,\n\t\t},\n\t\t'no-builtin': {\n\t\t\ttype: 'boolean',\n\t\t\tdescription: 'Disable all built-in extensions',\n\t\t\tdefault: false,\n\t\t},\n\t\t'no-mcp': {\n\t\t\ttype: 'boolean',\n\t\t\tdescription: 'Disable built-in MCP extension',\n\t\t\tdefault: false,\n\t\t},\n\t\t'no-skills': {\n\t\t\ttype: 'boolean',\n\t\t\tdescription: 'Disable built-in skills extension',\n\t\t\tdefault: false,\n\t\t},\n\t\t'no-chain': {\n\t\t\ttype: 'boolean',\n\t\t\tdescription: 'Disable built-in chain extension',\n\t\t\tdefault: false,\n\t\t},\n\t\t'no-filter': {\n\t\t\ttype: 'boolean',\n\t\t\tdescription: 'Disable secret redaction in tool output',\n\t\t\tdefault: false,\n\t\t},\n\t\t'no-handoff': {\n\t\t\ttype: 'boolean',\n\t\t\tdescription: 'Disable handoff extension',\n\t\t\tdefault: false,\n\t\t},\n\t\t'no-recall': {\n\t\t\ttype: 'boolean',\n\t\t\tdescription: 'Disable recall extension',\n\t\t\tdefault: false,\n\t\t},\n\t\t'no-prompt-presets': {\n\t\t\ttype: 'boolean',\n\t\t\tdescription: 'Disable prompt presets extension',\n\t\t\tdefault: false,\n\t\t},\n\t\t'no-lsp': {\n\t\t\ttype: 'boolean',\n\t\t\tdescription: 'Disable LSP extension',\n\t\t\tdefault: false,\n\t\t},\n\t\t'no-session-name': {\n\t\t\ttype: 'boolean',\n\t\t\tdescription: 'Disable session name extension',\n\t\t\tdefault: false,\n\t\t},\n\t\ttelemetry: {\n\t\t\ttype: 'boolean',\n\t\t\tdescription: 'Enable local SQLite telemetry for this process',\n\t\t\tdefault: false,\n\t\t},\n\t\t'no-telemetry': {\n\t\t\ttype: 'boolean',\n\t\t\tdescription: 'Disable local SQLite telemetry for this process',\n\t\t\tdefault: false,\n\t\t},\n\t\t'telemetry-db': {\n\t\t\ttype: 'string',\n\t\t\tdescription:\n\t\t\t\t'Override telemetry database path for this process',\n\t\t\trequired: false,\n\t\t},\n\t\tmodel: {\n\t\t\ttype: 'string',\n\t\t\talias: 'm',\n\t\t\tdescription:\n\t\t\t\t'Model to use (e.g. claude-sonnet-4-5-20241022, gpt-5.4)',\n\t\t},\n\t\t'system-prompt': {\n\t\t\ttype: 'string',\n\t\t\tdescription: 'Replace the base system prompt',\n\t\t\trequired: false,\n\t\t},\n\t\t'append-system-prompt': {\n\t\t\ttype: 'string',\n\t\t\tdescription: 'Append one-off instructions to the system prompt',\n\t\t\trequired: false,\n\t\t},\n\t\tprompt: {\n\t\t\ttype: 'string',\n\t\t\talias: 'p',\n\t\t\tdescription: 'Prompt text (alternative to positional argument)',\n\t\t\trequired: false,\n\t\t},\n\t},\n\tasync run({ args }) {\n\t\tconst cwd = process.cwd();\n\t\tconst extension_paths = parse_extension_paths(process.argv);\n\n\t\t// Resolve prompt: named --prompt flag > positional > stdin\n\t\tlet prompt = args.prompt;\n\t\tif (!prompt) {\n\t\t\t// Check for positional arguments (after citty strips flags)\n\t\t\tconst positionals = (args as any)._ as string[] | undefined;\n\t\t\tif (positionals && positionals.length > 0) {\n\t\t\t\tprompt = positionals[0];\n\t\t\t}\n\t\t}\n\t\tif (!prompt && !process.stdin.isTTY) {\n\t\t\tprompt = await read_stdin();\n\t\t}\n\n\t\t// Model validation (issue #5)\n\t\tif (args.model && /[/\\\\]/.test(args.model)) {\n\t\t\tconsole.error(\n\t\t\t\t`Error: Invalid model \"${args.model}\". Use bare model names without provider prefixes.`,\n\t\t\t);\n\t\t\tconsole.error(\n\t\t\t\t` Examples: claude-sonnet-4-5-20241022, gpt-5.4, mistral-large`,\n\t\t\t);\n\t\t\tprocess.exit(1);\n\t\t}\n\n\t\tif (\n\t\t\t!args.print &&\n\t\t\t!args.json &&\n\t\t\t!prompt &&\n\t\t\t!process.stdout.isTTY\n\t\t) {\n\t\t\tprint_usage();\n\t\t\treturn;\n\t\t}\n\n\t\t// Startup feedback so silence = broken (issue #3)\n\t\tif (args.print || args.json || prompt) {\n\t\t\tprocess.stderr.write(\n\t\t\t\t`my-pi: connecting to ${args.model || 'default model'}...\\n`,\n\t\t\t);\n\t\t}\n\n\t\tif (args.telemetry && args['no-telemetry']) {\n\t\t\tconsole.error(\n\t\t\t\t'Error: --telemetry and --no-telemetry cannot be used together.',\n\t\t\t);\n\t\t\tprocess.exit(1);\n\t\t}\n\n\t\tconst telemetry_override = args.telemetry\n\t\t\t? true\n\t\t\t: args['no-telemetry']\n\t\t\t\t? false\n\t\t\t\t: undefined;\n\n\t\tconst runtime = await create_my_pi({\n\t\t\tcwd,\n\t\t\tagent_dir: args['agent-dir'],\n\t\t\textensions: extension_paths,\n\t\t\tmcp: !args['no-builtin'] && !args['no-mcp'],\n\t\t\tskills: !args['no-builtin'] && !args['no-skills'],\n\t\t\tchain: !args['no-builtin'] && !args['no-chain'],\n\t\t\tfilter_output: !args['no-builtin'] && !args['no-filter'],\n\t\t\thandoff: !args['no-builtin'] && !args['no-handoff'],\n\t\t\trecall: !args['no-builtin'] && !args['no-recall'],\n\t\t\tprompt_presets:\n\t\t\t\t!args['no-builtin'] && !args['no-prompt-presets'],\n\t\t\tlsp: !args['no-builtin'] && !args['no-lsp'],\n\t\t\tsession_name: !args['no-builtin'] && !args['no-session-name'],\n\t\t\ttelemetry: telemetry_override,\n\t\t\ttelemetry_db_path: args['telemetry-db'],\n\t\t\tmodel: args.model,\n\t\t\tsystem_prompt: args['system-prompt'],\n\t\t\tappend_system_prompt: args['append-system-prompt'],\n\t\t});\n\n\t\tif (args.print || args.json || prompt) {\n\t\t\tconst code = await runPrintMode(runtime, {\n\t\t\t\tmode: args.json ? 'json' : 'text',\n\t\t\t\tinitialMessage: prompt || '',\n\t\t\t\tinitialImages: [],\n\t\t\t\tmessages: [],\n\t\t\t});\n\t\t\tprocess.exit(code);\n\t\t} else if (!process.stdout.isTTY) {\n\t\t\tprint_usage();\n\t\t} else {\n\t\t\tconst mode = new InteractiveMode(runtime, {\n\t\t\t\tmigratedProviders: [],\n\t\t\t\tmodelFallbackMessage: undefined,\n\t\t\t\tinitialMessage: undefined,\n\t\t\t\tinitialImages: [],\n\t\t\t\tinitialMessages: [],\n\t\t\t});\n\t\t\tawait mode.run();\n\t\t}\n\t},\n});\n\nvoid runMain(main);\n"],"mappings":";;;;;;;;AAgBA,QAAQ,mBAAmB,UAAU;AACrC,QAAQ,GAAG,YAAY,YAAY;AAClC,KAAI,QAAQ,SAAS,sBACpB,SAAQ,KAAK,QAAQ;EAErB;AAEF,MAAM,YAAY,QAAQ,cAAc,OAAO,KAAK,IAAI,CAAC;AACzD,MAAM,MAAM,KAAK,MAChB,aAAa,KAAK,WAAW,MAAM,eAAe,EAAE,QAAQ,CAC5D;AAID,SAAS,sBAAsB,MAA0B;CACxD,MAAM,QAAkB,EAAE;AAC1B,MAAK,IAAI,IAAI,GAAG,IAAI,KAAK,QAAQ,IAChC,MACE,KAAK,OAAO,QAAQ,KAAK,OAAO,kBACjC,IAAI,IAAI,KAAK,OAEb,OAAM,KAAK,QAAQ,KAAK,EAAE,GAAG,CAAC;AAGhC,QAAO;;AAGR,eAAe,aAA8B;CAC5C,MAAM,SAAmB,EAAE;AAC3B,YAAW,MAAM,SAAS,QAAQ,MACjC,QAAO,KAAK,MAAgB;AAE7B,QAAO,OAAO,OAAO,OAAO,CAAC,SAAS,QAAQ,CAAC,MAAM;;AAGtD,SAAS,cAAoB;AAC5B,SAAQ,IAAI,UAAU,IAAI,QAAQ,iCAAiC;AACnE,SAAQ,IAAI,SAAS;AACrB,SAAQ,IACP,2DACA;AACD,SAAQ,IACP,0DACA;AACD,SAAQ,IACP,2DACA;AACD,SAAQ,IACP,gEACA;AACD,SAAQ,IACP,wDACA;AACD,SAAQ,IACP,+DACA;AACD,SAAQ,IACP,qEACA;AACD,SAAQ,IACP,uEACA;AACD,SAAQ,IACP,4DACA;AACD,SAAQ,IACP,0DACA;AACD,SAAQ,IACP,kEACA;;AA4NG,QAzNQ,cAAc;CAC1B,MAAM;EACL,MAAM;EACN,SAAS,IAAI;EACb,aACC;EACD;CACD,MAAM;EACL,OAAO;GACN,MAAM;GACN,OAAO;GACP,aAAa;GACb,SAAS;GACT;EACD,aAAa;GACZ,MAAM;GACN,aACC;GACD,UAAU;GACV;EACD,MAAM;GACL,MAAM;GACN,OAAO;GACP,aAAa;GACb,SAAS;GACT;EACD,cAAc;GACb,MAAM;GACN,aAAa;GACb,SAAS;GACT;EACD,UAAU;GACT,MAAM;GACN,aAAa;GACb,SAAS;GACT;EACD,aAAa;GACZ,MAAM;GACN,aAAa;GACb,SAAS;GACT;EACD,YAAY;GACX,MAAM;GACN,aAAa;GACb,SAAS;GACT;EACD,aAAa;GACZ,MAAM;GACN,aAAa;GACb,SAAS;GACT;EACD,cAAc;GACb,MAAM;GACN,aAAa;GACb,SAAS;GACT;EACD,aAAa;GACZ,MAAM;GACN,aAAa;GACb,SAAS;GACT;EACD,qBAAqB;GACpB,MAAM;GACN,aAAa;GACb,SAAS;GACT;EACD,UAAU;GACT,MAAM;GACN,aAAa;GACb,SAAS;GACT;EACD,mBAAmB;GAClB,MAAM;GACN,aAAa;GACb,SAAS;GACT;EACD,WAAW;GACV,MAAM;GACN,aAAa;GACb,SAAS;GACT;EACD,gBAAgB;GACf,MAAM;GACN,aAAa;GACb,SAAS;GACT;EACD,gBAAgB;GACf,MAAM;GACN,aACC;GACD,UAAU;GACV;EACD,OAAO;GACN,MAAM;GACN,OAAO;GACP,aACC;GACD;EACD,iBAAiB;GAChB,MAAM;GACN,aAAa;GACb,UAAU;GACV;EACD,wBAAwB;GACvB,MAAM;GACN,aAAa;GACb,UAAU;GACV;EACD,QAAQ;GACP,MAAM;GACN,OAAO;GACP,aAAa;GACb,UAAU;GACV;EACD;CACD,MAAM,IAAI,EAAE,QAAQ;EACnB,MAAM,MAAM,QAAQ,KAAK;EACzB,MAAM,kBAAkB,sBAAsB,QAAQ,KAAK;EAG3D,IAAI,SAAS,KAAK;AAClB,MAAI,CAAC,QAAQ;GAEZ,MAAM,cAAe,KAAa;AAClC,OAAI,eAAe,YAAY,SAAS,EACvC,UAAS,YAAY;;AAGvB,MAAI,CAAC,UAAU,CAAC,QAAQ,MAAM,MAC7B,UAAS,MAAM,YAAY;AAI5B,MAAI,KAAK,SAAS,QAAQ,KAAK,KAAK,MAAM,EAAE;AAC3C,WAAQ,MACP,yBAAyB,KAAK,MAAM,oDACpC;AACD,WAAQ,MACP,iEACA;AACD,WAAQ,KAAK,EAAE;;AAGhB,MACC,CAAC,KAAK,SACN,CAAC,KAAK,QACN,CAAC,UACD,CAAC,QAAQ,OAAO,OACf;AACD,gBAAa;AACb;;AAID,MAAI,KAAK,SAAS,KAAK,QAAQ,OAC9B,SAAQ,OAAO,MACd,wBAAwB,KAAK,SAAS,gBAAgB,OACtD;AAGF,MAAI,KAAK,aAAa,KAAK,iBAAiB;AAC3C,WAAQ,MACP,iEACA;AACD,WAAQ,KAAK,EAAE;;EAGhB,MAAM,qBAAqB,KAAK,YAC7B,OACA,KAAK,kBACJ,QACA,KAAA;EAEJ,MAAM,UAAU,MAAM,aAAa;GAClC;GACA,WAAW,KAAK;GAChB,YAAY;GACZ,KAAK,CAAC,KAAK,iBAAiB,CAAC,KAAK;GAClC,QAAQ,CAAC,KAAK,iBAAiB,CAAC,KAAK;GACrC,OAAO,CAAC,KAAK,iBAAiB,CAAC,KAAK;GACpC,eAAe,CAAC,KAAK,iBAAiB,CAAC,KAAK;GAC5C,SAAS,CAAC,KAAK,iBAAiB,CAAC,KAAK;GACtC,QAAQ,CAAC,KAAK,iBAAiB,CAAC,KAAK;GACrC,gBACC,CAAC,KAAK,iBAAiB,CAAC,KAAK;GAC9B,KAAK,CAAC,KAAK,iBAAiB,CAAC,KAAK;GAClC,cAAc,CAAC,KAAK,iBAAiB,CAAC,KAAK;GAC3C,WAAW;GACX,mBAAmB,KAAK;GACxB,OAAO,KAAK;GACZ,eAAe,KAAK;GACpB,sBAAsB,KAAK;GAC3B,CAAC;AAEF,MAAI,KAAK,SAAS,KAAK,QAAQ,QAAQ;GACtC,MAAM,OAAO,MAAM,aAAa,SAAS;IACxC,MAAM,KAAK,OAAO,SAAS;IAC3B,gBAAgB,UAAU;IAC1B,eAAe,EAAE;IACjB,UAAU,EAAE;IACZ,CAAC;AACF,WAAQ,KAAK,KAAK;aACR,CAAC,QAAQ,OAAO,MAC1B,cAAa;MASb,OAPa,IAAI,gBAAgB,SAAS;GACzC,mBAAmB,EAAE;GACrB,sBAAsB,KAAA;GACtB,gBAAgB,KAAA;GAChB,eAAe,EAAE;GACjB,iBAAiB,EAAE;GACnB,CAAC,CACS,KAAK;;CAGlB,CAAC,CAEgB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "my-pi",
3
- "version": "0.0.13",
3
+ "version": "0.1.0",
4
4
  "description": "Composable pi coding agent with MCP, LSP, agent chains, prompt presets, and local eval telemetry",
5
5
  "keywords": [
6
6
  "cli",
@@ -26,6 +26,9 @@ describe('find_builtin_extension', () => {
26
26
  expect(find_builtin_extension('language-server')?.key).toBe(
27
27
  'lsp',
28
28
  );
29
+ expect(find_builtin_extension('auto-name')?.key).toBe(
30
+ 'session-name',
31
+ );
29
32
  });
30
33
  });
31
34
 
@@ -16,7 +16,8 @@ export type BuiltinExtensionKey =
16
16
  | 'handoff'
17
17
  | 'recall'
18
18
  | 'prompt-presets'
19
- | 'lsp';
19
+ | 'lsp'
20
+ | 'session-name';
20
21
 
21
22
  export interface BuiltinExtensionInfo {
22
23
  key: BuiltinExtensionKey;
@@ -79,7 +80,8 @@ export const BUILTIN_EXTENSIONS: BuiltinExtensionInfo[] = [
79
80
  {
80
81
  key: 'handoff',
81
82
  label: 'Handoff',
82
- description: 'Session handoff export and /handoff command',
83
+ description:
84
+ 'AI-generated session handoff with editor review and new-session prefill',
83
85
  cli_flag: '--no-handoff',
84
86
  aliases: ['handoff'],
85
87
  },
@@ -106,6 +108,14 @@ export const BUILTIN_EXTENSIONS: BuiltinExtensionInfo[] = [
106
108
  cli_flag: '--no-lsp',
107
109
  aliases: ['lsp', 'language-server'],
108
110
  },
111
+ {
112
+ key: 'session-name',
113
+ label: 'Session name',
114
+ description:
115
+ 'AI-powered session auto-naming and /session-name command',
116
+ cli_flag: '--no-session-name',
117
+ aliases: ['session-name', 'session', 'auto-name'],
118
+ },
109
119
  ];
110
120
 
111
121
  export function get_builtin_extensions_config_path(): string {
@@ -1,87 +1,173 @@
1
- // Handoff extension — extract session context for a new session
2
- // Inspired by jayshah5696/pi-agent-extensions
1
+ // Handoff extension — generate a focused prompt for a new session
3
2
 
4
- import type { ExtensionAPI } from '@mariozechner/pi-coding-agent';
5
- import { writeFileSync } from 'node:fs';
6
- import { join } from 'node:path';
3
+ import { complete, type Message } from '@mariozechner/pi-ai';
4
+ import type {
5
+ ExtensionAPI,
6
+ SessionEntry,
7
+ } from '@mariozechner/pi-coding-agent';
8
+ import {
9
+ BorderedLoader,
10
+ convertToLlm,
11
+ serializeConversation,
12
+ } from '@mariozechner/pi-coding-agent';
7
13
 
8
- // Default export for Pi Package / additionalExtensionPaths loading
9
- export default async function handoff(pi: ExtensionAPI) {
10
- const history: Array<{
11
- role: string;
12
- summary: string;
13
- timestamp: number;
14
- }> = [];
15
-
16
- // Track conversation turns
17
- pi.on('message_end', async (event) => {
18
- const msg = event.message as unknown as Record<string, unknown>;
19
- if (!msg) return;
20
-
21
- const content = msg.content as
22
- | Array<{ type: string; text?: string }>
23
- | undefined;
24
- if (!Array.isArray(content)) return;
25
-
26
- const text = content
27
- .filter((c) => c.type === 'text')
28
- .map((c) => c.text || '')
29
- .join('\n');
30
-
31
- if (!text) return;
32
-
33
- const summary =
34
- text.length > 200 ? text.slice(0, 200) + '...' : text;
35
-
36
- history.push({
37
- role: (msg.role as string) || 'unknown',
38
- summary,
39
- timestamp: Date.now(),
40
- });
41
- });
14
+ const SYSTEM_PROMPT = `You are a context transfer assistant. Given a conversation history and the user's goal for a new thread, generate a focused prompt that:
15
+
16
+ 1. Summarizes relevant context from the conversation (decisions made, approaches taken, key findings)
17
+ 2. Lists any relevant files that were discussed or modified
18
+ 3. Clearly states the next task based on the user's goal
19
+ 4. Is self-contained - the new thread should be able to proceed without the old conversation
20
+
21
+ Format your response as a prompt the user can send to start the new thread. Be concise but include all necessary context. Do not include any preamble like "Here's the prompt" - just output the prompt itself.
22
+
23
+ Example output format:
24
+ ## Context
25
+ We've been working on X. Key decisions:
26
+ - Decision 1
27
+ - Decision 2
28
+
29
+ Files involved:
30
+ - path/to/file1.ts
31
+ - path/to/file2.ts
32
+
33
+ ## Task
34
+ [Clear description of what to do next based on user's goal]`;
42
35
 
36
+ export default async function handoff(pi: ExtensionAPI) {
43
37
  pi.registerCommand('handoff', {
44
38
  description:
45
- 'Export session context as a handoff prompt for a new session',
39
+ 'Transfer context to a new focused session with an AI-generated prompt',
46
40
  handler: async (args, ctx) => {
47
- const task = args.trim();
41
+ if (!ctx.hasUI) {
42
+ ctx.ui.notify('handoff requires interactive mode', 'error');
43
+ return;
44
+ }
45
+
46
+ if (!ctx.model) {
47
+ ctx.ui.notify('No model selected', 'error');
48
+ return;
49
+ }
48
50
 
49
- if (history.length === 0) {
51
+ const goal = args.trim();
52
+ if (!goal) {
50
53
  ctx.ui.notify(
51
- 'No conversation history to hand off',
52
- 'warning',
54
+ 'Usage: /handoff <goal for new thread>',
55
+ 'error',
53
56
  );
54
57
  return;
55
58
  }
56
59
 
57
- const context = history
58
- .map((h) => `[${h.role}] ${h.summary}`)
59
- .join('\n\n');
60
+ const branch = ctx.sessionManager.getBranch();
61
+ const messages = branch
62
+ .filter(
63
+ (entry): entry is SessionEntry & { type: 'message' } =>
64
+ entry.type === 'message',
65
+ )
66
+ .map((entry) => entry.message);
60
67
 
61
- const handoff = `## Handoff from Previous Session
62
-
63
- ### Context
64
- The previous session covered the following:
68
+ if (messages.length === 0) {
69
+ ctx.ui.notify('No conversation to hand off', 'error');
70
+ return;
71
+ }
65
72
 
66
- ${context}
73
+ const llm_messages = convertToLlm(messages);
74
+ const conversation_text = serializeConversation(llm_messages);
75
+ const current_session_file =
76
+ ctx.sessionManager.getSessionFile();
77
+ const model = ctx.model;
78
+
79
+ const result = await ctx.ui.custom<string | null>(
80
+ (tui, theme, _kb, done) => {
81
+ const loader = new BorderedLoader(
82
+ tui,
83
+ theme,
84
+ 'Generating handoff prompt...',
85
+ );
86
+ loader.onAbort = () => done(null);
87
+
88
+ const generate = async () => {
89
+ const auth =
90
+ await ctx.modelRegistry.getApiKeyAndHeaders(model);
91
+ if (!auth.ok || !auth.apiKey) {
92
+ throw new Error(
93
+ auth.ok
94
+ ? `No API key for ${model.provider}`
95
+ : auth.error,
96
+ );
97
+ }
98
+
99
+ const user_message: Message = {
100
+ role: 'user',
101
+ content: [
102
+ {
103
+ type: 'text',
104
+ text: `## Conversation History\n\n${conversation_text}\n\n## User's Goal for New Thread\n\n${goal}`,
105
+ },
106
+ ],
107
+ timestamp: Date.now(),
108
+ };
109
+
110
+ const response = await complete(
111
+ model,
112
+ {
113
+ systemPrompt: SYSTEM_PROMPT,
114
+ messages: [user_message],
115
+ },
116
+ {
117
+ apiKey: auth.apiKey,
118
+ headers: auth.headers,
119
+ signal: loader.signal,
120
+ },
121
+ );
122
+
123
+ if (response.stopReason === 'aborted') {
124
+ return null;
125
+ }
126
+
127
+ return response.content
128
+ .filter(
129
+ (c): c is { type: 'text'; text: string } =>
130
+ c.type === 'text',
131
+ )
132
+ .map((c) => c.text)
133
+ .join('\n');
134
+ };
135
+
136
+ generate()
137
+ .then(done)
138
+ .catch((err) => {
139
+ console.error('Handoff generation failed:', err);
140
+ done(null);
141
+ });
142
+
143
+ return loader;
144
+ },
145
+ );
67
146
 
68
- ### Task
69
- ${task || 'Continue from where the previous session left off.'}
147
+ if (result === null) {
148
+ ctx.ui.notify('Cancelled', 'info');
149
+ return;
150
+ }
70
151
 
71
- ### Instructions
72
- - Review the context above to understand what was done
73
- - Do NOT repeat work that was already completed
74
- - Focus on the task described above
75
- `;
152
+ const edited_prompt = await ctx.ui.editor(
153
+ 'Edit handoff prompt',
154
+ result,
155
+ );
156
+ if (edited_prompt === undefined) {
157
+ ctx.ui.notify('Cancelled', 'info');
158
+ return;
159
+ }
76
160
 
77
- // Write to file
78
- const filename = `handoff-${Date.now()}.md`;
79
- const filepath = join(ctx.cwd, filename);
80
- writeFileSync(filepath, handoff, 'utf-8');
161
+ const new_session_result = await ctx.newSession({
162
+ parentSession: current_session_file,
163
+ });
164
+ if (new_session_result.cancelled) {
165
+ ctx.ui.notify('New session cancelled', 'info');
166
+ return;
167
+ }
81
168
 
82
- ctx.ui.notify(
83
- `Handoff written to ${filename}\n\nUse: my-pi < ${filename}`,
84
- );
169
+ ctx.ui.setEditorText(edited_prompt);
170
+ ctx.ui.notify('Handoff ready. Submit when ready.', 'info');
85
171
  },
86
172
  });
87
173
  }
@@ -0,0 +1,234 @@
1
+ // Session name — AI-powered session naming
2
+ // Adapted from Thomas Lopes' pi dotfiles
3
+
4
+ import { complete, type Message } from '@mariozechner/pi-ai';
5
+ import type {
6
+ ExtensionAPI,
7
+ SessionEntry,
8
+ } from '@mariozechner/pi-coding-agent';
9
+ import {
10
+ BorderedLoader,
11
+ convertToLlm,
12
+ serializeConversation,
13
+ } from '@mariozechner/pi-coding-agent';
14
+
15
+ const SYSTEM_PROMPT = `You are a session naming assistant. Given a conversation history, generate a short, descriptive session name (2-5 words) that captures the main topic or task.
16
+
17
+ Guidelines:
18
+ - Be concise but specific
19
+ - Use kebab-case or natural language
20
+ - Focus on the core task/question
21
+ - Avoid generic names like "discussion" or "conversation"
22
+ - No quotes, no punctuation at the end
23
+
24
+ Examples:
25
+ - "fix auth bug" -> "fix-auth-bug" or "authentication fix"
26
+ - "how do I deploy to vercel" -> "vercel deployment"
27
+ - "explain react hooks" -> "react hooks explanation"
28
+ - "optimize database queries" -> "db query optimization"
29
+
30
+ Output ONLY the session name, nothing else.`;
31
+
32
+ const AUTO_NAME_THRESHOLD = 1;
33
+ const MAX_CHARS = 4000;
34
+ const MAX_NAME_LEN = 50;
35
+
36
+ function clean_name(value: string): string {
37
+ return value
38
+ .replace(/^["']|["']$/g, '')
39
+ .replace(/\n/g, ' ')
40
+ .replace(/\s+/g, ' ')
41
+ .trim()
42
+ .slice(0, MAX_NAME_LEN);
43
+ }
44
+
45
+ function truncate_conversation(value: string): string {
46
+ return value.length > MAX_CHARS
47
+ ? value.slice(0, MAX_CHARS) + '\n...'
48
+ : value;
49
+ }
50
+
51
+ async function generate_session_name(
52
+ ctx: {
53
+ modelRegistry: {
54
+ getApiKeyAndHeaders: (
55
+ model: NonNullable<
56
+ Parameters<
57
+ Parameters<ExtensionAPI['registerCommand']>[1]['handler']
58
+ >[1]['model']
59
+ >,
60
+ ) => Promise<any>;
61
+ };
62
+ },
63
+ model: NonNullable<
64
+ Parameters<
65
+ Parameters<ExtensionAPI['registerCommand']>[1]['handler']
66
+ >[1]['model']
67
+ >,
68
+ conversation_text: string,
69
+ signal?: AbortSignal,
70
+ ): Promise<string | null> {
71
+ const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
72
+ if (!auth.ok || !auth.apiKey) {
73
+ throw new Error(
74
+ auth.ok ? `No API key for ${model.provider}` : auth.error,
75
+ );
76
+ }
77
+
78
+ const user_message: Message = {
79
+ role: 'user',
80
+ content: [
81
+ {
82
+ type: 'text',
83
+ text: `## Conversation History\n\n${truncate_conversation(conversation_text)}\n\nGenerate a concise session name for this conversation.`,
84
+ },
85
+ ],
86
+ timestamp: Date.now(),
87
+ };
88
+
89
+ const response = await complete(
90
+ model,
91
+ { systemPrompt: SYSTEM_PROMPT, messages: [user_message] },
92
+ { apiKey: auth.apiKey, headers: auth.headers, signal },
93
+ );
94
+
95
+ if (response.stopReason === 'aborted') {
96
+ return null;
97
+ }
98
+
99
+ return clean_name(
100
+ response.content
101
+ .filter(
102
+ (c): c is { type: 'text'; text: string } => c.type === 'text',
103
+ )
104
+ .map((c) => c.text.trim())
105
+ .join(' '),
106
+ );
107
+ }
108
+
109
+ export default async function session_name(pi: ExtensionAPI) {
110
+ let auto_named_attempted = false;
111
+
112
+ pi.on('agent_end', async (_event, ctx) => {
113
+ if (!ctx.hasUI || !ctx.model) return;
114
+ if (pi.getSessionName() || auto_named_attempted) return;
115
+
116
+ const branch = ctx.sessionManager.getBranch();
117
+ const user_messages = branch.filter(
118
+ (entry): entry is SessionEntry & { type: 'message' } =>
119
+ entry.type === 'message' && entry.message.role === 'user',
120
+ );
121
+ if (user_messages.length < AUTO_NAME_THRESHOLD) return;
122
+
123
+ auto_named_attempted = true;
124
+ const messages = branch
125
+ .filter(
126
+ (entry): entry is SessionEntry & { type: 'message' } =>
127
+ entry.type === 'message',
128
+ )
129
+ .map((entry) => entry.message);
130
+ if (messages.length === 0) return;
131
+
132
+ const conversation_text = serializeConversation(
133
+ convertToLlm(messages),
134
+ );
135
+
136
+ generate_session_name(ctx, ctx.model, conversation_text)
137
+ .then((name) => {
138
+ if (!name) return;
139
+ pi.setSessionName(name);
140
+ ctx.ui.notify(`Auto-named: ${name}`, 'info');
141
+ })
142
+ .catch((err) => {
143
+ console.error('Auto-naming failed:', err);
144
+ });
145
+ });
146
+
147
+ pi.on('session_start', async () => {
148
+ auto_named_attempted = false;
149
+ });
150
+
151
+ pi.registerCommand('session-name', {
152
+ description:
153
+ 'Set, show, or auto-generate the current session name',
154
+ handler: async (args, ctx) => {
155
+ const trimmed = args.trim();
156
+
157
+ if (!trimmed) {
158
+ const current = pi.getSessionName();
159
+ ctx.ui.notify(
160
+ current ? `Session: ${current}` : 'No session name set',
161
+ 'info',
162
+ );
163
+ return;
164
+ }
165
+
166
+ if (trimmed === '--auto' || trimmed === '-a') {
167
+ if (!ctx.hasUI || !ctx.model) {
168
+ ctx.ui.notify(
169
+ 'Auto-naming requires interactive mode and a selected model',
170
+ 'error',
171
+ );
172
+ return;
173
+ }
174
+
175
+ const branch = ctx.sessionManager.getBranch();
176
+ const messages = branch
177
+ .filter(
178
+ (entry): entry is SessionEntry & { type: 'message' } =>
179
+ entry.type === 'message',
180
+ )
181
+ .map((entry) => entry.message);
182
+ if (messages.length === 0) {
183
+ ctx.ui.notify('No conversation to analyze', 'error');
184
+ return;
185
+ }
186
+
187
+ const conversation_text = serializeConversation(
188
+ convertToLlm(messages),
189
+ );
190
+
191
+ const result = await ctx.ui.custom<string | null>(
192
+ (tui, theme, _kb, done) => {
193
+ const loader = new BorderedLoader(
194
+ tui,
195
+ theme,
196
+ 'Generating session name...',
197
+ );
198
+ loader.onAbort = () => done(null);
199
+
200
+ generate_session_name(
201
+ ctx,
202
+ ctx.model!,
203
+ conversation_text,
204
+ loader.signal,
205
+ )
206
+ .then(done)
207
+ .catch((err) => {
208
+ console.error('Auto-naming failed:', err);
209
+ done(null);
210
+ });
211
+
212
+ return loader;
213
+ },
214
+ );
215
+
216
+ if (result === null) {
217
+ ctx.ui.notify('Auto-naming cancelled', 'info');
218
+ return;
219
+ }
220
+ if (!result) {
221
+ ctx.ui.notify('Failed to generate name', 'error');
222
+ return;
223
+ }
224
+
225
+ pi.setSessionName(result);
226
+ ctx.ui.notify(`Session named: ${result}`, 'info');
227
+ return;
228
+ }
229
+
230
+ pi.setSessionName(clean_name(trimmed));
231
+ ctx.ui.notify(`Session named: ${clean_name(trimmed)}`, 'info');
232
+ },
233
+ });
234
+ }