@oh-my-pi/pi-coding-agent 3.15.1 → 3.20.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/CHANGELOG.md +60 -0
- package/docs/extensions.md +1055 -0
- package/docs/rpc.md +69 -13
- package/docs/session-tree-plan.md +1 -1
- package/examples/extensions/README.md +141 -0
- package/examples/extensions/api-demo.ts +87 -0
- package/examples/extensions/chalk-logger.ts +26 -0
- package/examples/extensions/hello.ts +33 -0
- package/examples/extensions/pirate.ts +44 -0
- package/examples/extensions/plan-mode.ts +551 -0
- package/examples/extensions/subagent/agents/reviewer.md +35 -0
- package/examples/extensions/todo.ts +299 -0
- package/examples/extensions/tools.ts +145 -0
- package/examples/extensions/with-deps/index.ts +36 -0
- package/examples/extensions/with-deps/package-lock.json +31 -0
- package/examples/extensions/with-deps/package.json +16 -0
- package/examples/sdk/02-custom-model.ts +3 -3
- package/examples/sdk/05-tools.ts +7 -3
- package/examples/sdk/06-extensions.ts +81 -0
- package/examples/sdk/06-hooks.ts +14 -13
- package/examples/sdk/08-prompt-templates.ts +42 -0
- package/examples/sdk/08-slash-commands.ts +17 -12
- package/examples/sdk/09-api-keys-and-oauth.ts +2 -2
- package/examples/sdk/12-full-control.ts +6 -6
- package/package.json +11 -7
- package/src/capability/extension-module.ts +34 -0
- package/src/cli/args.ts +22 -7
- package/src/cli/file-processor.ts +38 -67
- package/src/cli/list-models.ts +1 -1
- package/src/config.ts +25 -14
- package/src/core/agent-session.ts +505 -242
- package/src/core/auth-storage.ts +33 -21
- package/src/core/compaction/branch-summarization.ts +4 -4
- package/src/core/compaction/compaction.ts +3 -3
- package/src/core/custom-commands/bundled/wt/index.ts +430 -0
- package/src/core/custom-commands/loader.ts +9 -0
- package/src/core/custom-tools/wrapper.ts +5 -0
- package/src/core/event-bus.ts +59 -0
- package/src/core/export-html/vendor/highlight.min.js +1213 -0
- package/src/core/export-html/vendor/marked.min.js +6 -0
- package/src/core/extensions/index.ts +100 -0
- package/src/core/extensions/loader.ts +501 -0
- package/src/core/extensions/runner.ts +477 -0
- package/src/core/extensions/types.ts +712 -0
- package/src/core/extensions/wrapper.ts +147 -0
- package/src/core/hooks/types.ts +2 -2
- package/src/core/index.ts +10 -21
- package/src/core/keybindings.ts +199 -0
- package/src/core/messages.ts +26 -7
- package/src/core/model-registry.ts +123 -46
- package/src/core/model-resolver.ts +7 -5
- package/src/core/prompt-templates.ts +242 -0
- package/src/core/sdk.ts +378 -295
- package/src/core/session-manager.ts +72 -58
- package/src/core/settings-manager.ts +118 -22
- package/src/core/system-prompt.ts +24 -1
- package/src/core/terminal-notify.ts +37 -0
- package/src/core/tools/context.ts +4 -4
- package/src/core/tools/exa/mcp-client.ts +5 -4
- package/src/core/tools/exa/render.ts +176 -131
- package/src/core/tools/find.ts +7 -1
- package/src/core/tools/gemini-image.ts +361 -0
- package/src/core/tools/git.ts +216 -0
- package/src/core/tools/index.ts +28 -15
- package/src/core/tools/ls.ts +9 -2
- package/src/core/tools/lsp/config.ts +5 -4
- package/src/core/tools/lsp/index.ts +17 -12
- package/src/core/tools/lsp/render.ts +39 -47
- package/src/core/tools/read.ts +66 -29
- package/src/core/tools/render-utils.ts +268 -0
- package/src/core/tools/renderers.ts +243 -225
- package/src/core/tools/task/discovery.ts +2 -2
- package/src/core/tools/task/executor.ts +66 -58
- package/src/core/tools/task/index.ts +29 -10
- package/src/core/tools/task/model-resolver.ts +8 -13
- package/src/core/tools/task/omp-command.ts +24 -0
- package/src/core/tools/task/render.ts +37 -62
- package/src/core/tools/task/types.ts +3 -0
- package/src/core/tools/web-fetch.ts +29 -28
- package/src/core/tools/web-search/index.ts +6 -5
- package/src/core/tools/web-search/providers/exa.ts +6 -5
- package/src/core/tools/web-search/render.ts +66 -111
- package/src/core/voice-controller.ts +135 -0
- package/src/core/voice-supervisor.ts +1003 -0
- package/src/core/voice.ts +308 -0
- package/src/discovery/builtin.ts +75 -1
- package/src/discovery/claude.ts +47 -1
- package/src/discovery/codex.ts +54 -2
- package/src/discovery/gemini.ts +55 -2
- package/src/discovery/helpers.ts +100 -1
- package/src/discovery/index.ts +2 -0
- package/src/index.ts +14 -9
- package/src/lib/worktree/collapse.ts +179 -0
- package/src/lib/worktree/constants.ts +14 -0
- package/src/lib/worktree/errors.ts +23 -0
- package/src/lib/worktree/git.ts +110 -0
- package/src/lib/worktree/index.ts +23 -0
- package/src/lib/worktree/operations.ts +216 -0
- package/src/lib/worktree/session.ts +114 -0
- package/src/lib/worktree/stats.ts +67 -0
- package/src/main.ts +61 -37
- package/src/migrations.ts +37 -7
- package/src/modes/interactive/components/bash-execution.ts +6 -4
- package/src/modes/interactive/components/custom-editor.ts +55 -0
- package/src/modes/interactive/components/custom-message.ts +95 -0
- package/src/modes/interactive/components/extensions/extension-list.ts +5 -0
- package/src/modes/interactive/components/extensions/inspector-panel.ts +18 -12
- package/src/modes/interactive/components/extensions/state-manager.ts +12 -0
- package/src/modes/interactive/components/extensions/types.ts +1 -0
- package/src/modes/interactive/components/footer.ts +324 -0
- package/src/modes/interactive/components/hook-selector.ts +3 -3
- package/src/modes/interactive/components/model-selector.ts +7 -6
- package/src/modes/interactive/components/oauth-selector.ts +3 -3
- package/src/modes/interactive/components/settings-defs.ts +55 -6
- package/src/modes/interactive/components/status-line.ts +45 -37
- package/src/modes/interactive/components/tool-execution.ts +95 -23
- package/src/modes/interactive/interactive-mode.ts +643 -113
- package/src/modes/interactive/theme/defaults/index.ts +16 -16
- package/src/modes/print-mode.ts +14 -72
- package/src/modes/rpc/rpc-client.ts +23 -9
- package/src/modes/rpc/rpc-mode.ts +137 -125
- package/src/modes/rpc/rpc-types.ts +46 -24
- package/src/prompts/task.md +1 -0
- package/src/prompts/tools/gemini-image.md +4 -0
- package/src/prompts/tools/git.md +9 -0
- package/src/prompts/voice-summary.md +12 -0
- package/src/utils/image-convert.ts +26 -0
- package/src/utils/image-resize.ts +215 -0
- package/src/utils/shell-snapshot.ts +22 -20
package/examples/sdk/06-hooks.ts
CHANGED
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
* Hooks Configuration
|
|
3
3
|
*
|
|
4
4
|
* Hooks intercept agent events for logging, blocking, or modification.
|
|
5
|
+
* Note: "hooks" is now called "extensions" in the API.
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
|
-
import { createAgentSession, type
|
|
8
|
+
import { createAgentSession, type ExtensionFactory, SessionManager } from "@oh-my-pi/pi-coding-agent";
|
|
8
9
|
|
|
9
|
-
// Logging hook
|
|
10
|
-
const loggingHook:
|
|
10
|
+
// Logging hook (now called extension)
|
|
11
|
+
const loggingHook: ExtensionFactory = (api) => {
|
|
11
12
|
api.on("agent_start", async () => {
|
|
12
13
|
console.log("[Hook] Agent starting");
|
|
13
14
|
});
|
|
@@ -22,8 +23,8 @@ const loggingHook: HookFactory = (api) => {
|
|
|
22
23
|
});
|
|
23
24
|
};
|
|
24
25
|
|
|
25
|
-
// Blocking
|
|
26
|
-
const safetyHook:
|
|
26
|
+
// Blocking extension (returns { block: true, reason: "..." })
|
|
27
|
+
const safetyHook: ExtensionFactory = (api) => {
|
|
27
28
|
api.on("tool_call", async (event) => {
|
|
28
29
|
if (event.toolName === "bash") {
|
|
29
30
|
const cmd = (event.input as { command?: string }).command ?? "";
|
|
@@ -35,9 +36,9 @@ const safetyHook: HookFactory = (api) => {
|
|
|
35
36
|
});
|
|
36
37
|
};
|
|
37
38
|
|
|
38
|
-
// Use inline hooks
|
|
39
|
+
// Use inline extensions (hooks is now extensions)
|
|
39
40
|
const { session } = await createAgentSession({
|
|
40
|
-
|
|
41
|
+
extensions: [loggingHook, safetyHook],
|
|
41
42
|
sessionManager: SessionManager.inMemory(),
|
|
42
43
|
});
|
|
43
44
|
|
|
@@ -50,12 +51,12 @@ session.subscribe((event) => {
|
|
|
50
51
|
await session.prompt("List files in the current directory.");
|
|
51
52
|
console.log();
|
|
52
53
|
|
|
53
|
-
// Disable all
|
|
54
|
-
//
|
|
54
|
+
// Disable all extensions:
|
|
55
|
+
// extensions: []
|
|
55
56
|
|
|
56
|
-
// Merge with discovered
|
|
57
|
-
// const discovered = await
|
|
58
|
-
//
|
|
57
|
+
// Merge with discovered extensions:
|
|
58
|
+
// const discovered = await discoverExtensions();
|
|
59
|
+
// extensions: [...discovered.extensions.map(e => e.factory), myHook]
|
|
59
60
|
|
|
60
61
|
// Add paths without replacing discovery:
|
|
61
|
-
//
|
|
62
|
+
// additionalExtensionPaths: ["/extra/extensions"]
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prompt Templates
|
|
3
|
+
*
|
|
4
|
+
* File-based templates that inject content when invoked with /templatename.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
createAgentSession,
|
|
9
|
+
discoverPromptTemplates,
|
|
10
|
+
type PromptTemplate,
|
|
11
|
+
SessionManager,
|
|
12
|
+
} from "@oh-my-pi/pi-coding-agent";
|
|
13
|
+
|
|
14
|
+
// Discover templates from cwd/.pi/prompts/ and ~/.pi/agent/prompts/
|
|
15
|
+
const discovered = await discoverPromptTemplates();
|
|
16
|
+
console.log("Discovered prompt templates:");
|
|
17
|
+
for (const template of discovered) {
|
|
18
|
+
console.log(` /${template.name}: ${template.description}`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Define custom templates
|
|
22
|
+
const deployTemplate: PromptTemplate = {
|
|
23
|
+
name: "deploy",
|
|
24
|
+
description: "Deploy the application",
|
|
25
|
+
source: "(custom)",
|
|
26
|
+
content: `# Deploy Instructions
|
|
27
|
+
|
|
28
|
+
1. Build: npm run build
|
|
29
|
+
2. Test: npm test
|
|
30
|
+
3. Deploy: npm run deploy`,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// Use discovered + custom templates
|
|
34
|
+
await createAgentSession({
|
|
35
|
+
promptTemplates: [...discovered, deployTemplate],
|
|
36
|
+
sessionManager: SessionManager.inMemory(),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
console.log(`Session created with ${discovered.length + 1} prompt templates`);
|
|
40
|
+
|
|
41
|
+
// Disable prompt templates:
|
|
42
|
+
// promptTemplates: []
|
|
@@ -2,24 +2,25 @@
|
|
|
2
2
|
* Slash Commands
|
|
3
3
|
*
|
|
4
4
|
* File-based commands that inject content when invoked with /commandname.
|
|
5
|
+
* Note: File-based slash commands are now called "prompt templates".
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
8
|
import {
|
|
8
9
|
createAgentSession,
|
|
9
|
-
|
|
10
|
-
type
|
|
10
|
+
discoverPromptTemplates,
|
|
11
|
+
type PromptTemplate,
|
|
11
12
|
SessionManager,
|
|
12
13
|
} from "@oh-my-pi/pi-coding-agent";
|
|
13
14
|
|
|
14
|
-
// Discover
|
|
15
|
-
const discovered =
|
|
16
|
-
console.log("Discovered
|
|
15
|
+
// Discover prompt templates from cwd/.pi/prompts/ and ~/.pi/agent/prompts/
|
|
16
|
+
const discovered = await discoverPromptTemplates();
|
|
17
|
+
console.log("Discovered prompt templates:");
|
|
17
18
|
for (const cmd of discovered) {
|
|
18
19
|
console.log(` /${cmd.name}: ${cmd.description}`);
|
|
19
20
|
}
|
|
20
21
|
|
|
21
|
-
// Define custom
|
|
22
|
-
const deployCommand:
|
|
22
|
+
// Define custom prompt templates
|
|
23
|
+
const deployCommand: PromptTemplate = {
|
|
23
24
|
name: "deploy",
|
|
24
25
|
description: "Deploy the application",
|
|
25
26
|
source: "(custom)",
|
|
@@ -30,13 +31,17 @@ const deployCommand: FileSlashCommand = {
|
|
|
30
31
|
3. Deploy: npm run deploy`,
|
|
31
32
|
};
|
|
32
33
|
|
|
33
|
-
//
|
|
34
|
+
// Note: slashCommands is now managed by the agent session automatically.
|
|
35
|
+
// Custom commands can be loaded via discoverCustomTSCommands() for TypeScript commands.
|
|
36
|
+
// For file-based markdown commands, use promptTemplates instead.
|
|
37
|
+
|
|
38
|
+
// Convert file-based slash commands to prompt templates
|
|
34
39
|
await createAgentSession({
|
|
35
|
-
|
|
40
|
+
promptTemplates: [...discovered, deployCommand],
|
|
36
41
|
sessionManager: SessionManager.inMemory(),
|
|
37
42
|
});
|
|
38
43
|
|
|
39
|
-
console.log(`Session created with ${discovered.length + 1}
|
|
44
|
+
console.log(`Session created with ${discovered.length + 1} prompt templates`);
|
|
40
45
|
|
|
41
|
-
// Disable
|
|
42
|
-
//
|
|
46
|
+
// Disable prompt templates:
|
|
47
|
+
// promptTemplates: []
|
|
@@ -15,8 +15,8 @@ import {
|
|
|
15
15
|
|
|
16
16
|
// Default: discoverAuthStorage() uses ~/.omp/agent/auth.json
|
|
17
17
|
// discoverModels() loads built-in + custom models from ~/.omp/agent/models.json
|
|
18
|
-
const authStorage = discoverAuthStorage();
|
|
19
|
-
const modelRegistry = discoverModels(authStorage);
|
|
18
|
+
const authStorage = await discoverAuthStorage();
|
|
19
|
+
const modelRegistry = await discoverModels(authStorage);
|
|
20
20
|
|
|
21
21
|
await createAgentSession({
|
|
22
22
|
sessionManager: SessionManager.inMemory(),
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
createAgentSession,
|
|
16
16
|
createBashTool,
|
|
17
17
|
createReadTool,
|
|
18
|
-
type
|
|
18
|
+
type ExtensionFactory,
|
|
19
19
|
ModelRegistry,
|
|
20
20
|
SessionManager,
|
|
21
21
|
SettingsManager,
|
|
@@ -33,8 +33,8 @@ if (process.env.MY_ANTHROPIC_KEY) {
|
|
|
33
33
|
// Model registry with no custom models.json
|
|
34
34
|
const modelRegistry = new ModelRegistry(authStorage);
|
|
35
35
|
|
|
36
|
-
// Inline
|
|
37
|
-
const auditHook:
|
|
36
|
+
// Inline extension
|
|
37
|
+
const auditHook: ExtensionFactory = (api) => {
|
|
38
38
|
api.on("tool_call", async (event) => {
|
|
39
39
|
console.log(`[Audit] ${event.toolName}`);
|
|
40
40
|
return undefined;
|
|
@@ -76,11 +76,11 @@ const { session } = await createAgentSession({
|
|
|
76
76
|
Available: read, bash, status. Be concise.`,
|
|
77
77
|
// Use factory functions with the same cwd to ensure path resolution works correctly
|
|
78
78
|
tools: [createReadTool(cwd), createBashTool(cwd)],
|
|
79
|
-
customTools: [
|
|
80
|
-
|
|
79
|
+
customTools: [statusTool],
|
|
80
|
+
extensions: [auditHook],
|
|
81
81
|
skills: [],
|
|
82
82
|
contextFiles: [],
|
|
83
|
-
|
|
83
|
+
promptTemplates: [],
|
|
84
84
|
sessionManager: SessionManager.inMemory(),
|
|
85
85
|
settingsManager,
|
|
86
86
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.20.1",
|
|
4
4
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"ompConfig": {
|
|
@@ -39,9 +39,11 @@
|
|
|
39
39
|
"prepublishOnly": "bun run generate-template && bun run clean && bun run build"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@oh-my-pi/pi-agent-core": "3.
|
|
43
|
-
"@oh-my-pi/pi-ai": "3.
|
|
44
|
-
"@oh-my-pi/pi-
|
|
42
|
+
"@oh-my-pi/pi-agent-core": "3.20.1",
|
|
43
|
+
"@oh-my-pi/pi-ai": "3.20.1",
|
|
44
|
+
"@oh-my-pi/pi-git-tool": "3.20.1",
|
|
45
|
+
"@oh-my-pi/pi-tui": "3.20.1",
|
|
46
|
+
"@openai/agents": "^0.3.7",
|
|
45
47
|
"@sinclair/typebox": "^0.34.46",
|
|
46
48
|
"ajv": "^8.17.1",
|
|
47
49
|
"chalk": "^5.5.0",
|
|
@@ -55,18 +57,20 @@
|
|
|
55
57
|
"nanoid": "^5.1.6",
|
|
56
58
|
"ndjson": "^2.0.0",
|
|
57
59
|
"node-html-parser": "^6.1.13",
|
|
60
|
+
"sharp": "^0.34.2",
|
|
58
61
|
"smol-toml": "^1.6.0",
|
|
59
62
|
"strip-ansi": "^7.1.2",
|
|
60
|
-
"sharp": "^0.34.2",
|
|
61
63
|
"winston": "^3.17.0",
|
|
62
64
|
"winston-daily-rotate-file": "^5.0.0",
|
|
63
|
-
"yaml": "^2.8.2"
|
|
65
|
+
"yaml": "^2.8.2",
|
|
66
|
+
"zod": "^4.3.5"
|
|
64
67
|
},
|
|
65
68
|
"devDependencies": {
|
|
66
69
|
"@types/diff": "^7.0.2",
|
|
70
|
+
"@types/ms": "^2.1.0",
|
|
67
71
|
"@types/ndjson": "^2.0.4",
|
|
68
72
|
"@types/node": "^24.3.0",
|
|
69
|
-
"
|
|
73
|
+
"ms": "^2.1.3"
|
|
70
74
|
},
|
|
71
75
|
"keywords": [
|
|
72
76
|
"coding-agent",
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extension Modules Capability
|
|
3
|
+
*
|
|
4
|
+
* TypeScript/JavaScript extension modules loaded by the extension system.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { defineCapability } from "./index";
|
|
8
|
+
import type { SourceMeta } from "./types";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A loaded extension module.
|
|
12
|
+
*/
|
|
13
|
+
export interface ExtensionModule {
|
|
14
|
+
/** Extension module name (derived from path) */
|
|
15
|
+
name: string;
|
|
16
|
+
/** Absolute path to extension entrypoint */
|
|
17
|
+
path: string;
|
|
18
|
+
/** Source level */
|
|
19
|
+
level: "user" | "project";
|
|
20
|
+
/** Source metadata */
|
|
21
|
+
_source: SourceMeta;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const extensionModuleCapability = defineCapability<ExtensionModule>({
|
|
25
|
+
id: "extension-modules",
|
|
26
|
+
displayName: "Extension Modules",
|
|
27
|
+
description: "TypeScript/JavaScript extension modules loaded by the extension system",
|
|
28
|
+
key: (ext) => ext.name,
|
|
29
|
+
validate: (ext) => {
|
|
30
|
+
if (!ext.name) return "Missing name";
|
|
31
|
+
if (!ext.path) return "Missing path";
|
|
32
|
+
return undefined;
|
|
33
|
+
},
|
|
34
|
+
});
|
package/src/cli/args.ts
CHANGED
|
@@ -29,7 +29,7 @@ export interface Args {
|
|
|
29
29
|
models?: string[];
|
|
30
30
|
tools?: ToolName[];
|
|
31
31
|
hooks?: string[];
|
|
32
|
-
|
|
32
|
+
extensions?: string[];
|
|
33
33
|
print?: boolean;
|
|
34
34
|
export?: string;
|
|
35
35
|
noSkills?: boolean;
|
|
@@ -37,6 +37,8 @@ export interface Args {
|
|
|
37
37
|
listModels?: string | true;
|
|
38
38
|
messages: string[];
|
|
39
39
|
fileArgs: string[];
|
|
40
|
+
/** Unknown flags (potentially extension flags) - map of flag name to value */
|
|
41
|
+
unknownFlags: Map<string, boolean | string>;
|
|
40
42
|
}
|
|
41
43
|
|
|
42
44
|
const VALID_THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh"] as const;
|
|
@@ -45,10 +47,11 @@ export function isValidThinkingLevel(level: string): level is ThinkingLevel {
|
|
|
45
47
|
return VALID_THINKING_LEVELS.includes(level as ThinkingLevel);
|
|
46
48
|
}
|
|
47
49
|
|
|
48
|
-
export function parseArgs(args: string[]): Args {
|
|
50
|
+
export function parseArgs(args: string[], extensionFlags?: Map<string, { type: "boolean" | "string" }>): Args {
|
|
49
51
|
const result: Args = {
|
|
50
52
|
messages: [],
|
|
51
53
|
fileArgs: [],
|
|
54
|
+
unknownFlags: new Map(),
|
|
52
55
|
};
|
|
53
56
|
|
|
54
57
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -120,9 +123,9 @@ export function parseArgs(args: string[]): Args {
|
|
|
120
123
|
} else if (arg === "--hook" && i + 1 < args.length) {
|
|
121
124
|
result.hooks = result.hooks ?? [];
|
|
122
125
|
result.hooks.push(args[++i]);
|
|
123
|
-
} else if (arg === "--
|
|
124
|
-
result.
|
|
125
|
-
result.
|
|
126
|
+
} else if ((arg === "--extension" || arg === "-e") && i + 1 < args.length) {
|
|
127
|
+
result.extensions = result.extensions ?? [];
|
|
128
|
+
result.extensions.push(args[++i]);
|
|
126
129
|
} else if (arg === "--no-skills") {
|
|
127
130
|
result.noSkills = true;
|
|
128
131
|
} else if (arg === "--skills" && i + 1 < args.length) {
|
|
@@ -137,6 +140,18 @@ export function parseArgs(args: string[]): Args {
|
|
|
137
140
|
}
|
|
138
141
|
} else if (arg.startsWith("@")) {
|
|
139
142
|
result.fileArgs.push(arg.slice(1)); // Remove @ prefix
|
|
143
|
+
} else if (arg.startsWith("--") && extensionFlags) {
|
|
144
|
+
// Check if it's an extension-registered flag
|
|
145
|
+
const flagName = arg.slice(2);
|
|
146
|
+
const extFlag = extensionFlags.get(flagName);
|
|
147
|
+
if (extFlag) {
|
|
148
|
+
if (extFlag.type === "boolean") {
|
|
149
|
+
result.unknownFlags.set(flagName, true);
|
|
150
|
+
} else if (extFlag.type === "string" && i + 1 < args.length) {
|
|
151
|
+
result.unknownFlags.set(flagName, args[++i]);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
// Unknown flags without extensionFlags are silently ignored (first pass)
|
|
140
155
|
} else if (!arg.startsWith("-")) {
|
|
141
156
|
result.messages.push(arg);
|
|
142
157
|
}
|
|
@@ -170,8 +185,8 @@ ${chalk.bold("Options:")}
|
|
|
170
185
|
--tools <tools> Comma-separated list of tools to enable (default: read,bash,edit,write)
|
|
171
186
|
Available: read, bash, edit, write, grep, find, ls
|
|
172
187
|
--thinking <level> Set thinking level: off, minimal, low, medium, high, xhigh
|
|
173
|
-
--hook <path> Load a hook file (can be used multiple times)
|
|
174
|
-
--
|
|
188
|
+
--hook <path> Load a hook/extension file (can be used multiple times)
|
|
189
|
+
--extension, -e <path> Load an extension file (can be used multiple times)
|
|
175
190
|
--no-skills Disable skills discovery and loading
|
|
176
191
|
--skills <patterns> Comma-separated glob patterns to filter skills (e.g., git-*,docker)
|
|
177
192
|
--export <file> Export session file to HTML and exit
|
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
* Process @file CLI arguments into text content and image attachments
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
6
6
|
import { resolve } from "node:path";
|
|
7
7
|
import type { ImageContent } from "@oh-my-pi/pi-ai";
|
|
8
8
|
import chalk from "chalk";
|
|
9
|
-
import sharp from "sharp";
|
|
10
9
|
import { resolveReadPath } from "../core/tools/path-utils";
|
|
10
|
+
import { formatDimensionNote, resizeImage } from "../utils/image-resize";
|
|
11
11
|
import { detectSupportedImageMimeTypeFromFile } from "../utils/mime";
|
|
12
12
|
|
|
13
13
|
export interface ProcessedFiles {
|
|
@@ -15,55 +15,14 @@ export interface ProcessedFiles {
|
|
|
15
15
|
images: ImageContent[];
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const JPEG_CONVERT_THRESHOLD_BYTES = 2 * 1024 * 1024;
|
|
22
|
-
const JPEG_QUALITY = 85;
|
|
23
|
-
|
|
24
|
-
async function processImageAttachment(buffer: Buffer, mimeType: string): Promise<{ buffer: Buffer; mimeType: string }> {
|
|
25
|
-
const metadata = await sharp(buffer, { failOnError: false }).metadata();
|
|
26
|
-
const width = metadata.width ?? 0;
|
|
27
|
-
const height = metadata.height ?? 0;
|
|
28
|
-
const maxDim = Math.max(width, height);
|
|
29
|
-
const shouldResize = width > 0 && height > 0 && maxDim > RESIZE_TRIGGER_MAX_DIMENSION;
|
|
30
|
-
const shouldConvertToJpeg = buffer.length > JPEG_CONVERT_THRESHOLD_BYTES;
|
|
31
|
-
|
|
32
|
-
if (!shouldResize && !shouldConvertToJpeg) {
|
|
33
|
-
return { buffer, mimeType };
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
let pipeline = sharp(buffer, { failOnError: false });
|
|
37
|
-
if (shouldResize) {
|
|
38
|
-
pipeline = pipeline.resize({
|
|
39
|
-
width: MAX_RESIZE_WIDTH,
|
|
40
|
-
height: MAX_RESIZE_HEIGHT,
|
|
41
|
-
fit: "inside",
|
|
42
|
-
withoutEnlargement: true,
|
|
43
|
-
});
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
if (shouldConvertToJpeg) {
|
|
47
|
-
pipeline = pipeline.jpeg({ quality: JPEG_QUALITY });
|
|
48
|
-
return { buffer: await pipeline.toBuffer(), mimeType: "image/jpeg" };
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
if (mimeType === "image/png") {
|
|
52
|
-
pipeline = pipeline.png();
|
|
53
|
-
} else if (mimeType === "image/webp") {
|
|
54
|
-
pipeline = pipeline.webp();
|
|
55
|
-
} else if (mimeType === "image/gif") {
|
|
56
|
-
pipeline = pipeline.gif();
|
|
57
|
-
} else {
|
|
58
|
-
pipeline = pipeline.jpeg({ quality: JPEG_QUALITY });
|
|
59
|
-
return { buffer: await pipeline.toBuffer(), mimeType: "image/jpeg" };
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
return { buffer: await pipeline.toBuffer(), mimeType };
|
|
18
|
+
export interface ProcessFileOptions {
|
|
19
|
+
/** Whether to auto-resize images to 2000x2000 max. Default: true */
|
|
20
|
+
autoResizeImages?: boolean;
|
|
63
21
|
}
|
|
64
22
|
|
|
65
23
|
/** Process @file arguments into text content and image attachments */
|
|
66
|
-
export async function processFileArguments(fileArgs: string[]): Promise<ProcessedFiles> {
|
|
24
|
+
export async function processFileArguments(fileArgs: string[], options?: ProcessFileOptions): Promise<ProcessedFiles> {
|
|
25
|
+
const _autoResizeImages = options?.autoResizeImages ?? true;
|
|
67
26
|
let text = "";
|
|
68
27
|
const images: ImageContent[] = [];
|
|
69
28
|
|
|
@@ -71,16 +30,12 @@ export async function processFileArguments(fileArgs: string[]): Promise<Processe
|
|
|
71
30
|
// Expand and resolve path (handles ~ expansion and macOS screenshot Unicode spaces)
|
|
72
31
|
const absolutePath = resolve(resolveReadPath(fileArg, process.cwd()));
|
|
73
32
|
|
|
74
|
-
// Check if file exists
|
|
75
|
-
|
|
76
|
-
await access(absolutePath);
|
|
77
|
-
} catch {
|
|
33
|
+
// Check if file exists and is not empty
|
|
34
|
+
if (!existsSync(absolutePath)) {
|
|
78
35
|
console.error(chalk.red(`Error: File not found: ${absolutePath}`));
|
|
79
36
|
process.exit(1);
|
|
80
37
|
}
|
|
81
|
-
|
|
82
|
-
// Check if file is empty
|
|
83
|
-
const stats = await stat(absolutePath);
|
|
38
|
+
const stats = statSync(absolutePath);
|
|
84
39
|
if (stats.size === 0) {
|
|
85
40
|
// Skip empty files
|
|
86
41
|
continue;
|
|
@@ -90,24 +45,40 @@ export async function processFileArguments(fileArgs: string[]): Promise<Processe
|
|
|
90
45
|
|
|
91
46
|
if (mimeType) {
|
|
92
47
|
// Handle image file
|
|
93
|
-
const
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
data: base64Content,
|
|
101
|
-
|
|
48
|
+
const buffer = readFileSync(absolutePath);
|
|
49
|
+
const base64Content = buffer.toString("base64");
|
|
50
|
+
|
|
51
|
+
let attachment: ImageContent;
|
|
52
|
+
let dimensionNote: string | undefined;
|
|
53
|
+
|
|
54
|
+
if (_autoResizeImages) {
|
|
55
|
+
const resized = await resizeImage({ type: "image", data: base64Content, mimeType });
|
|
56
|
+
dimensionNote = formatDimensionNote(resized);
|
|
57
|
+
attachment = {
|
|
58
|
+
type: "image",
|
|
59
|
+
mimeType: resized.mimeType,
|
|
60
|
+
data: resized.data,
|
|
61
|
+
};
|
|
62
|
+
} else {
|
|
63
|
+
attachment = {
|
|
64
|
+
type: "image",
|
|
65
|
+
mimeType,
|
|
66
|
+
data: base64Content,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
102
69
|
|
|
103
70
|
images.push(attachment);
|
|
104
71
|
|
|
105
|
-
// Add text reference to image
|
|
106
|
-
|
|
72
|
+
// Add text reference to image with optional dimension note
|
|
73
|
+
if (dimensionNote) {
|
|
74
|
+
text += `<file name="${absolutePath}">${dimensionNote}</file>\n`;
|
|
75
|
+
} else {
|
|
76
|
+
text += `<file name="${absolutePath}"></file>\n`;
|
|
77
|
+
}
|
|
107
78
|
} else {
|
|
108
79
|
// Handle text file
|
|
109
80
|
try {
|
|
110
|
-
const content =
|
|
81
|
+
const content = readFileSync(absolutePath, "utf-8");
|
|
111
82
|
text += `<file name="${absolutePath}">\n${content}\n</file>\n`;
|
|
112
83
|
} catch (error: unknown) {
|
|
113
84
|
const message = error instanceof Error ? error.message : String(error);
|
package/src/cli/list-models.ts
CHANGED
|
@@ -25,7 +25,7 @@ function formatTokenCount(count: number): string {
|
|
|
25
25
|
* List available models, optionally filtered by search pattern
|
|
26
26
|
*/
|
|
27
27
|
export async function listModels(modelRegistry: ModelRegistry, searchPattern?: string): Promise<void> {
|
|
28
|
-
const models =
|
|
28
|
+
const models = modelRegistry.getAvailable();
|
|
29
29
|
|
|
30
30
|
if (models.length === 0) {
|
|
31
31
|
console.log("No models available. Set API keys in environment variables.");
|
package/src/config.ts
CHANGED
|
@@ -104,6 +104,11 @@ export function getCommandsDir(): string {
|
|
|
104
104
|
return join(getAgentDir(), "commands");
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
/** Get path to prompts directory */
|
|
108
|
+
export function getPromptsDir(): string {
|
|
109
|
+
return join(getAgentDir(), "prompts");
|
|
110
|
+
}
|
|
111
|
+
|
|
107
112
|
/** Get path to sessions directory */
|
|
108
113
|
export function getSessionsDir(): string {
|
|
109
114
|
return join(getAgentDir(), "sessions");
|
|
@@ -230,8 +235,8 @@ export function readConfigFile<T = unknown>(
|
|
|
230
235
|
|
|
231
236
|
for (const { path: base, source, level } of dirs) {
|
|
232
237
|
const filePath = join(base, subpath);
|
|
233
|
-
|
|
234
|
-
|
|
238
|
+
try {
|
|
239
|
+
if (existsSync(filePath)) {
|
|
235
240
|
const content = readFileSync(filePath, "utf-8");
|
|
236
241
|
return {
|
|
237
242
|
path: filePath,
|
|
@@ -239,9 +244,9 @@ export function readConfigFile<T = unknown>(
|
|
|
239
244
|
level,
|
|
240
245
|
content: JSON.parse(content) as T,
|
|
241
246
|
};
|
|
242
|
-
} catch {
|
|
243
|
-
// Continue to next file on parse error
|
|
244
247
|
}
|
|
248
|
+
} catch {
|
|
249
|
+
// Continue to next file on parse error
|
|
245
250
|
}
|
|
246
251
|
}
|
|
247
252
|
|
|
@@ -261,8 +266,8 @@ export function readAllConfigFiles<T = unknown>(
|
|
|
261
266
|
|
|
262
267
|
for (const { path: base, source, level } of dirs) {
|
|
263
268
|
const filePath = join(base, subpath);
|
|
264
|
-
|
|
265
|
-
|
|
269
|
+
try {
|
|
270
|
+
if (existsSync(filePath)) {
|
|
266
271
|
const content = readFileSync(filePath, "utf-8");
|
|
267
272
|
results.push({
|
|
268
273
|
path: filePath,
|
|
@@ -270,9 +275,9 @@ export function readAllConfigFiles<T = unknown>(
|
|
|
270
275
|
level,
|
|
271
276
|
content: JSON.parse(content) as T,
|
|
272
277
|
});
|
|
273
|
-
} catch {
|
|
274
|
-
// Skip files that fail to parse
|
|
275
278
|
}
|
|
279
|
+
} catch {
|
|
280
|
+
// Skip files that fail to parse
|
|
276
281
|
}
|
|
277
282
|
}
|
|
278
283
|
|
|
@@ -319,9 +324,9 @@ export function findConfigFileWithMeta(
|
|
|
319
324
|
// Walk-Up Config Discovery (for monorepo scenarios)
|
|
320
325
|
// =============================================================================
|
|
321
326
|
|
|
322
|
-
function isDirectory(p: string): boolean {
|
|
327
|
+
async function isDirectory(p: string): Promise<boolean> {
|
|
323
328
|
try {
|
|
324
|
-
return statSync(p).isDirectory();
|
|
329
|
+
return existsSync(p) && statSync(p).isDirectory();
|
|
325
330
|
} catch {
|
|
326
331
|
return false;
|
|
327
332
|
}
|
|
@@ -335,14 +340,17 @@ function isDirectory(p: string): boolean {
|
|
|
335
340
|
* @param cwd - Starting directory
|
|
336
341
|
* @returns First existing directory found, or undefined
|
|
337
342
|
*/
|
|
338
|
-
export function findNearestProjectConfigDir(
|
|
343
|
+
export async function findNearestProjectConfigDir(
|
|
344
|
+
subpath: string,
|
|
345
|
+
cwd: string = process.cwd(),
|
|
346
|
+
): Promise<ConfigDirEntry | undefined> {
|
|
339
347
|
let currentDir = cwd;
|
|
340
348
|
|
|
341
349
|
while (true) {
|
|
342
350
|
// Check all config bases at this level, in priority order
|
|
343
351
|
for (const { base, name } of PROJECT_CONFIG_BASES) {
|
|
344
352
|
const candidate = join(currentDir, base, subpath);
|
|
345
|
-
if (isDirectory(candidate)) {
|
|
353
|
+
if (await isDirectory(candidate)) {
|
|
346
354
|
return { path: candidate, source: name, level: "project" };
|
|
347
355
|
}
|
|
348
356
|
}
|
|
@@ -361,7 +369,10 @@ export function findNearestProjectConfigDir(subpath: string, cwd: string = proce
|
|
|
361
369
|
* Returns one entry per config base (.omp, .pi, .claude) - the nearest one found.
|
|
362
370
|
* Results are in priority order (highest first).
|
|
363
371
|
*/
|
|
364
|
-
export function findAllNearestProjectConfigDirs(
|
|
372
|
+
export async function findAllNearestProjectConfigDirs(
|
|
373
|
+
subpath: string,
|
|
374
|
+
cwd: string = process.cwd(),
|
|
375
|
+
): Promise<ConfigDirEntry[]> {
|
|
365
376
|
const results: ConfigDirEntry[] = [];
|
|
366
377
|
const foundBases = new Set<string>();
|
|
367
378
|
|
|
@@ -372,7 +383,7 @@ export function findAllNearestProjectConfigDirs(subpath: string, cwd: string = p
|
|
|
372
383
|
if (foundBases.has(name)) continue;
|
|
373
384
|
|
|
374
385
|
const candidate = join(currentDir, base, subpath);
|
|
375
|
-
if (isDirectory(candidate)) {
|
|
386
|
+
if (await isDirectory(candidate)) {
|
|
376
387
|
results.push({ path: candidate, source: name, level: "project" });
|
|
377
388
|
foundBases.add(name);
|
|
378
389
|
}
|