my-pi 0.0.13 → 0.1.1
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/README.md +2 -0
- package/dist/{api-CWEizv2k.js → api-Dxi4curf.js} +639 -102
- package/dist/api-Dxi4curf.js.map +1 -0
- package/dist/api.js +1 -1
- package/dist/index.js +19 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/extensions/config.test.ts +9 -0
- package/src/extensions/config.ts +30 -2
- package/src/extensions/confirm-destructive.test.ts +157 -0
- package/src/extensions/confirm-destructive.ts +61 -0
- package/src/extensions/extensions.test.ts +114 -0
- package/src/extensions/extensions.ts +114 -108
- package/src/extensions/handoff.ts +152 -66
- package/src/extensions/hooks-resolution.test.ts +246 -0
- package/src/extensions/hooks-resolution.ts +584 -0
- package/src/extensions/session-name.ts +234 -0
- package/dist/api-CWEizv2k.js.map +0 -1
package/dist/api.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { n as create_my_pi, r as runPrintMode, t as InteractiveMode } from "./api-
|
|
1
|
+
import { n as create_my_pi, r as runPrintMode, t as InteractiveMode } from "./api-Dxi4curf.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-
|
|
2
|
+
import { n as create_my_pi } from "./api-Dxi4curf.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,21 @@ 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
|
+
},
|
|
114
|
+
"no-confirm-destructive": {
|
|
115
|
+
type: "boolean",
|
|
116
|
+
description: "Disable destructive action confirmations",
|
|
117
|
+
default: false
|
|
118
|
+
},
|
|
119
|
+
"no-hooks": {
|
|
120
|
+
type: "boolean",
|
|
121
|
+
description: "Disable Claude-style hook execution",
|
|
122
|
+
default: false
|
|
123
|
+
},
|
|
109
124
|
telemetry: {
|
|
110
125
|
type: "boolean",
|
|
111
126
|
description: "Enable local SQLite telemetry for this process",
|
|
@@ -179,6 +194,9 @@ runMain(defineCommand({
|
|
|
179
194
|
recall: !args["no-builtin"] && !args["no-recall"],
|
|
180
195
|
prompt_presets: !args["no-builtin"] && !args["no-prompt-presets"],
|
|
181
196
|
lsp: !args["no-builtin"] && !args["no-lsp"],
|
|
197
|
+
session_name: !args["no-builtin"] && !args["no-session-name"],
|
|
198
|
+
confirm_destructive: !args["no-builtin"] && !args["no-confirm-destructive"],
|
|
199
|
+
hooks_resolution: !args["no-builtin"] && !args["no-hooks"],
|
|
182
200
|
telemetry: telemetry_override,
|
|
183
201
|
telemetry_db_path: args["telemetry-db"],
|
|
184
202
|
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\t'no-confirm-destructive': {\n\t\t\ttype: 'boolean',\n\t\t\tdescription: 'Disable destructive action confirmations',\n\t\t\tdefault: false,\n\t\t},\n\t\t'no-hooks': {\n\t\t\ttype: 'boolean',\n\t\t\tdescription: 'Disable Claude-style hook execution',\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\tconfirm_destructive:\n\t\t\t\t!args['no-builtin'] && !args['no-confirm-destructive'],\n\t\t\thooks_resolution: !args['no-builtin'] && !args['no-hooks'],\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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"}
|
package/package.json
CHANGED
|
@@ -26,6 +26,15 @@ 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
|
+
);
|
|
32
|
+
expect(find_builtin_extension('confirm')?.key).toBe(
|
|
33
|
+
'confirm-destructive',
|
|
34
|
+
);
|
|
35
|
+
expect(find_builtin_extension('hooks')?.key).toBe(
|
|
36
|
+
'hooks-resolution',
|
|
37
|
+
);
|
|
29
38
|
});
|
|
30
39
|
});
|
|
31
40
|
|
package/src/extensions/config.ts
CHANGED
|
@@ -16,7 +16,10 @@ export type BuiltinExtensionKey =
|
|
|
16
16
|
| 'handoff'
|
|
17
17
|
| 'recall'
|
|
18
18
|
| 'prompt-presets'
|
|
19
|
-
| 'lsp'
|
|
19
|
+
| 'lsp'
|
|
20
|
+
| 'session-name'
|
|
21
|
+
| 'confirm-destructive'
|
|
22
|
+
| 'hooks-resolution';
|
|
20
23
|
|
|
21
24
|
export interface BuiltinExtensionInfo {
|
|
22
25
|
key: BuiltinExtensionKey;
|
|
@@ -79,7 +82,8 @@ export const BUILTIN_EXTENSIONS: BuiltinExtensionInfo[] = [
|
|
|
79
82
|
{
|
|
80
83
|
key: 'handoff',
|
|
81
84
|
label: 'Handoff',
|
|
82
|
-
description:
|
|
85
|
+
description:
|
|
86
|
+
'AI-generated session handoff with editor review and new-session prefill',
|
|
83
87
|
cli_flag: '--no-handoff',
|
|
84
88
|
aliases: ['handoff'],
|
|
85
89
|
},
|
|
@@ -106,6 +110,30 @@ export const BUILTIN_EXTENSIONS: BuiltinExtensionInfo[] = [
|
|
|
106
110
|
cli_flag: '--no-lsp',
|
|
107
111
|
aliases: ['lsp', 'language-server'],
|
|
108
112
|
},
|
|
113
|
+
{
|
|
114
|
+
key: 'session-name',
|
|
115
|
+
label: 'Session name',
|
|
116
|
+
description:
|
|
117
|
+
'AI-powered session auto-naming and /session-name command',
|
|
118
|
+
cli_flag: '--no-session-name',
|
|
119
|
+
aliases: ['session-name', 'session', 'auto-name'],
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
key: 'confirm-destructive',
|
|
123
|
+
label: 'Confirm destructive',
|
|
124
|
+
description:
|
|
125
|
+
'Prompt before destructive session actions like clear, switch, and fork',
|
|
126
|
+
cli_flag: '--no-confirm-destructive',
|
|
127
|
+
aliases: ['confirm-destructive', 'confirm'],
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
key: 'hooks-resolution',
|
|
131
|
+
label: 'Hooks resolution',
|
|
132
|
+
description:
|
|
133
|
+
'Claude Code style PostToolUse hook compatibility from .claude, .rulesync, and .pi configs',
|
|
134
|
+
cli_flag: '--no-hooks',
|
|
135
|
+
aliases: ['hooks-resolution', 'hooks'],
|
|
136
|
+
},
|
|
109
137
|
];
|
|
110
138
|
|
|
111
139
|
export function get_builtin_extensions_config_path(): string {
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import type { ExtensionAPI } from '@mariozechner/pi-coding-agent';
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import confirm_destructive from './confirm-destructive.js';
|
|
4
|
+
|
|
5
|
+
function create_test_pi() {
|
|
6
|
+
const events = new Map<string, any>();
|
|
7
|
+
const pi = {
|
|
8
|
+
on(name: string, handler: any) {
|
|
9
|
+
events.set(name, handler);
|
|
10
|
+
},
|
|
11
|
+
} as unknown as ExtensionAPI;
|
|
12
|
+
return { pi, events };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function create_context(overrides: Partial<any> = {}) {
|
|
16
|
+
const notify = vi.fn();
|
|
17
|
+
const confirm = vi.fn();
|
|
18
|
+
const select = vi.fn();
|
|
19
|
+
|
|
20
|
+
const ctx = {
|
|
21
|
+
hasUI: true,
|
|
22
|
+
ui: {
|
|
23
|
+
notify,
|
|
24
|
+
confirm,
|
|
25
|
+
select,
|
|
26
|
+
},
|
|
27
|
+
sessionManager: {
|
|
28
|
+
getEntries: vi.fn().mockReturnValue([]),
|
|
29
|
+
},
|
|
30
|
+
...overrides,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
return { ctx, notify, confirm, select };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
describe('confirm-destructive extension', () => {
|
|
37
|
+
it('confirms before clearing a session and cancels on rejection', async () => {
|
|
38
|
+
const { pi, events } = create_test_pi();
|
|
39
|
+
await confirm_destructive(pi);
|
|
40
|
+
|
|
41
|
+
const handler = events.get('session_before_switch');
|
|
42
|
+
const { ctx, confirm, notify } = create_context();
|
|
43
|
+
confirm.mockResolvedValue(false);
|
|
44
|
+
|
|
45
|
+
const result = await handler({ reason: 'new' }, ctx);
|
|
46
|
+
|
|
47
|
+
expect(confirm).toHaveBeenCalledWith(
|
|
48
|
+
'Clear session?',
|
|
49
|
+
'This will delete all messages in the current session.',
|
|
50
|
+
);
|
|
51
|
+
expect(notify).toHaveBeenCalledWith('Clear cancelled', 'info');
|
|
52
|
+
expect(result).toEqual({ cancel: true });
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('allows clearing a session when confirmed', async () => {
|
|
56
|
+
const { pi, events } = create_test_pi();
|
|
57
|
+
await confirm_destructive(pi);
|
|
58
|
+
|
|
59
|
+
const handler = events.get('session_before_switch');
|
|
60
|
+
const { ctx, confirm, notify } = create_context();
|
|
61
|
+
confirm.mockResolvedValue(true);
|
|
62
|
+
|
|
63
|
+
const result = await handler({ reason: 'new' }, ctx);
|
|
64
|
+
|
|
65
|
+
expect(confirm).toHaveBeenCalledOnce();
|
|
66
|
+
expect(notify).not.toHaveBeenCalled();
|
|
67
|
+
expect(result).toBeUndefined();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('does not prompt when switching sessions without user messages', async () => {
|
|
71
|
+
const { pi, events } = create_test_pi();
|
|
72
|
+
await confirm_destructive(pi);
|
|
73
|
+
|
|
74
|
+
const handler = events.get('session_before_switch');
|
|
75
|
+
const { ctx, confirm } = create_context({
|
|
76
|
+
sessionManager: {
|
|
77
|
+
getEntries: vi.fn().mockReturnValue([
|
|
78
|
+
{
|
|
79
|
+
type: 'message',
|
|
80
|
+
message: { role: 'assistant' },
|
|
81
|
+
},
|
|
82
|
+
]),
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const result = await handler({ reason: 'resume' }, ctx);
|
|
87
|
+
|
|
88
|
+
expect(confirm).not.toHaveBeenCalled();
|
|
89
|
+
expect(result).toBeUndefined();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('confirms before switching sessions when user messages exist', async () => {
|
|
93
|
+
const { pi, events } = create_test_pi();
|
|
94
|
+
await confirm_destructive(pi);
|
|
95
|
+
|
|
96
|
+
const handler = events.get('session_before_switch');
|
|
97
|
+
const { ctx, confirm, notify } = create_context({
|
|
98
|
+
sessionManager: {
|
|
99
|
+
getEntries: vi.fn().mockReturnValue([
|
|
100
|
+
{
|
|
101
|
+
type: 'message',
|
|
102
|
+
message: { role: 'user' },
|
|
103
|
+
},
|
|
104
|
+
]),
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
confirm.mockResolvedValue(false);
|
|
108
|
+
|
|
109
|
+
const result = await handler({ reason: 'resume' }, ctx);
|
|
110
|
+
|
|
111
|
+
expect(confirm).toHaveBeenCalledWith(
|
|
112
|
+
'Switch session?',
|
|
113
|
+
'You have messages in the current session. Switch anyway?',
|
|
114
|
+
);
|
|
115
|
+
expect(notify).toHaveBeenCalledWith('Switch cancelled', 'info');
|
|
116
|
+
expect(result).toEqual({ cancel: true });
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('confirms before forking and cancels when declined', async () => {
|
|
120
|
+
const { pi, events } = create_test_pi();
|
|
121
|
+
await confirm_destructive(pi);
|
|
122
|
+
|
|
123
|
+
const handler = events.get('session_before_fork');
|
|
124
|
+
const { ctx, select, notify } = create_context();
|
|
125
|
+
select.mockResolvedValue('No, stay in current session');
|
|
126
|
+
|
|
127
|
+
const result = await handler(
|
|
128
|
+
{ entryId: '1234567890abcdef' },
|
|
129
|
+
ctx,
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
expect(select).toHaveBeenCalledWith('Fork from entry 12345678?', [
|
|
133
|
+
'Yes, create fork',
|
|
134
|
+
'No, stay in current session',
|
|
135
|
+
]);
|
|
136
|
+
expect(notify).toHaveBeenCalledWith('Fork cancelled', 'info');
|
|
137
|
+
expect(result).toEqual({ cancel: true });
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('allows forking when confirmed', async () => {
|
|
141
|
+
const { pi, events } = create_test_pi();
|
|
142
|
+
await confirm_destructive(pi);
|
|
143
|
+
|
|
144
|
+
const handler = events.get('session_before_fork');
|
|
145
|
+
const { ctx, select, notify } = create_context();
|
|
146
|
+
select.mockResolvedValue('Yes, create fork');
|
|
147
|
+
|
|
148
|
+
const result = await handler(
|
|
149
|
+
{ entryId: '1234567890abcdef' },
|
|
150
|
+
ctx,
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
expect(select).toHaveBeenCalledOnce();
|
|
154
|
+
expect(notify).not.toHaveBeenCalled();
|
|
155
|
+
expect(result).toBeUndefined();
|
|
156
|
+
});
|
|
157
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// Confirm destructive actions — prompt before clear, switch, or fork
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
ExtensionAPI,
|
|
5
|
+
SessionBeforeSwitchEvent,
|
|
6
|
+
SessionMessageEntry,
|
|
7
|
+
} from '@mariozechner/pi-coding-agent';
|
|
8
|
+
|
|
9
|
+
export default async function confirm_destructive(pi: ExtensionAPI) {
|
|
10
|
+
pi.on(
|
|
11
|
+
'session_before_switch',
|
|
12
|
+
async (event: SessionBeforeSwitchEvent, ctx) => {
|
|
13
|
+
if (!ctx.hasUI) return;
|
|
14
|
+
|
|
15
|
+
if (event.reason === 'new') {
|
|
16
|
+
const confirmed = await ctx.ui.confirm(
|
|
17
|
+
'Clear session?',
|
|
18
|
+
'This will delete all messages in the current session.',
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
if (!confirmed) {
|
|
22
|
+
ctx.ui.notify('Clear cancelled', 'info');
|
|
23
|
+
return { cancel: true };
|
|
24
|
+
}
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const entries = ctx.sessionManager.getEntries();
|
|
29
|
+
const has_unsaved_work = entries.some(
|
|
30
|
+
(e): e is SessionMessageEntry =>
|
|
31
|
+
e.type === 'message' && e.message.role === 'user',
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
if (has_unsaved_work) {
|
|
35
|
+
const confirmed = await ctx.ui.confirm(
|
|
36
|
+
'Switch session?',
|
|
37
|
+
'You have messages in the current session. Switch anyway?',
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
if (!confirmed) {
|
|
41
|
+
ctx.ui.notify('Switch cancelled', 'info');
|
|
42
|
+
return { cancel: true };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
pi.on('session_before_fork', async (event, ctx) => {
|
|
49
|
+
if (!ctx.hasUI) return;
|
|
50
|
+
|
|
51
|
+
const choice = await ctx.ui.select(
|
|
52
|
+
`Fork from entry ${event.entryId.slice(0, 8)}?`,
|
|
53
|
+
['Yes, create fork', 'No, stay in current session'],
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
if (choice !== 'Yes, create fork') {
|
|
57
|
+
ctx.ui.notify('Fork cancelled', 'info');
|
|
58
|
+
return { cancel: true };
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import type { ExtensionAPI } from '@mariozechner/pi-coding-agent';
|
|
2
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
6
|
+
import { create_extensions_extension } from './extensions.js';
|
|
7
|
+
|
|
8
|
+
function create_test_pi() {
|
|
9
|
+
const commands = new Map<string, any>();
|
|
10
|
+
const pi = {
|
|
11
|
+
registerCommand(name: string, definition: any) {
|
|
12
|
+
commands.set(name, definition);
|
|
13
|
+
},
|
|
14
|
+
} as unknown as ExtensionAPI;
|
|
15
|
+
return { pi, commands };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function create_command_context(options?: { hasUI?: boolean }) {
|
|
19
|
+
const notifications: Array<{ message: string; level?: string }> =
|
|
20
|
+
[];
|
|
21
|
+
let custom_calls = 0;
|
|
22
|
+
let reload_calls = 0;
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
ctx: {
|
|
26
|
+
hasUI: options?.hasUI ?? true,
|
|
27
|
+
ui: {
|
|
28
|
+
notify(message: string, level?: string) {
|
|
29
|
+
notifications.push({ message, level });
|
|
30
|
+
},
|
|
31
|
+
async custom() {
|
|
32
|
+
custom_calls += 1;
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
async reload() {
|
|
36
|
+
reload_calls += 1;
|
|
37
|
+
},
|
|
38
|
+
} as any,
|
|
39
|
+
notifications,
|
|
40
|
+
get custom_calls() {
|
|
41
|
+
return custom_calls;
|
|
42
|
+
},
|
|
43
|
+
get reload_calls() {
|
|
44
|
+
return reload_calls;
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const dirs: string[] = [];
|
|
50
|
+
const original_xdg = process.env.XDG_CONFIG_HOME;
|
|
51
|
+
|
|
52
|
+
afterEach(() => {
|
|
53
|
+
for (const dir of dirs.splice(0)) {
|
|
54
|
+
rmSync(dir, { recursive: true, force: true });
|
|
55
|
+
}
|
|
56
|
+
if (original_xdg === undefined) {
|
|
57
|
+
delete process.env.XDG_CONFIG_HOME;
|
|
58
|
+
} else {
|
|
59
|
+
process.env.XDG_CONFIG_HOME = original_xdg;
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe('extensions command', () => {
|
|
64
|
+
it.each(['enable', 'disable', 'toggle'])(
|
|
65
|
+
'opens the interactive manager for /extensions %s with no key in UI mode',
|
|
66
|
+
async (subcommand) => {
|
|
67
|
+
const config_home = mkdtempSync(
|
|
68
|
+
join(tmpdir(), 'my-pi-ext-test-'),
|
|
69
|
+
);
|
|
70
|
+
dirs.push(config_home);
|
|
71
|
+
process.env.XDG_CONFIG_HOME = config_home;
|
|
72
|
+
|
|
73
|
+
const { pi, commands } = create_test_pi();
|
|
74
|
+
await create_extensions_extension()(pi);
|
|
75
|
+
|
|
76
|
+
const command = commands.get('extensions');
|
|
77
|
+
expect(command).toBeTruthy();
|
|
78
|
+
|
|
79
|
+
const command_context = create_command_context({ hasUI: true });
|
|
80
|
+
await command.handler(subcommand, command_context.ctx);
|
|
81
|
+
|
|
82
|
+
expect(command_context.custom_calls).toBe(1);
|
|
83
|
+
expect(command_context.reload_calls).toBe(0);
|
|
84
|
+
expect(
|
|
85
|
+
command_context.notifications.some((entry) =>
|
|
86
|
+
entry.message.includes('Usage: /extensions'),
|
|
87
|
+
),
|
|
88
|
+
).toBe(false);
|
|
89
|
+
},
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
it('keeps the usage warning for /extensions toggle with no key outside UI mode', async () => {
|
|
93
|
+
const config_home = mkdtempSync(
|
|
94
|
+
join(tmpdir(), 'my-pi-ext-test-'),
|
|
95
|
+
);
|
|
96
|
+
dirs.push(config_home);
|
|
97
|
+
process.env.XDG_CONFIG_HOME = config_home;
|
|
98
|
+
|
|
99
|
+
const { pi, commands } = create_test_pi();
|
|
100
|
+
await create_extensions_extension()(pi);
|
|
101
|
+
|
|
102
|
+
const command = commands.get('extensions');
|
|
103
|
+
expect(command).toBeTruthy();
|
|
104
|
+
|
|
105
|
+
const command_context = create_command_context({ hasUI: false });
|
|
106
|
+
await command.handler('toggle', command_context.ctx);
|
|
107
|
+
|
|
108
|
+
expect(command_context.custom_calls).toBe(0);
|
|
109
|
+
expect(command_context.notifications).toContainEqual({
|
|
110
|
+
message: 'Usage: /extensions toggle <key>',
|
|
111
|
+
level: 'warning',
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
});
|