@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.
Files changed (129) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/docs/extensions.md +1055 -0
  3. package/docs/rpc.md +69 -13
  4. package/docs/session-tree-plan.md +1 -1
  5. package/examples/extensions/README.md +141 -0
  6. package/examples/extensions/api-demo.ts +87 -0
  7. package/examples/extensions/chalk-logger.ts +26 -0
  8. package/examples/extensions/hello.ts +33 -0
  9. package/examples/extensions/pirate.ts +44 -0
  10. package/examples/extensions/plan-mode.ts +551 -0
  11. package/examples/extensions/subagent/agents/reviewer.md +35 -0
  12. package/examples/extensions/todo.ts +299 -0
  13. package/examples/extensions/tools.ts +145 -0
  14. package/examples/extensions/with-deps/index.ts +36 -0
  15. package/examples/extensions/with-deps/package-lock.json +31 -0
  16. package/examples/extensions/with-deps/package.json +16 -0
  17. package/examples/sdk/02-custom-model.ts +3 -3
  18. package/examples/sdk/05-tools.ts +7 -3
  19. package/examples/sdk/06-extensions.ts +81 -0
  20. package/examples/sdk/06-hooks.ts +14 -13
  21. package/examples/sdk/08-prompt-templates.ts +42 -0
  22. package/examples/sdk/08-slash-commands.ts +17 -12
  23. package/examples/sdk/09-api-keys-and-oauth.ts +2 -2
  24. package/examples/sdk/12-full-control.ts +6 -6
  25. package/package.json +11 -7
  26. package/src/capability/extension-module.ts +34 -0
  27. package/src/cli/args.ts +22 -7
  28. package/src/cli/file-processor.ts +38 -67
  29. package/src/cli/list-models.ts +1 -1
  30. package/src/config.ts +25 -14
  31. package/src/core/agent-session.ts +505 -242
  32. package/src/core/auth-storage.ts +33 -21
  33. package/src/core/compaction/branch-summarization.ts +4 -4
  34. package/src/core/compaction/compaction.ts +3 -3
  35. package/src/core/custom-commands/bundled/wt/index.ts +430 -0
  36. package/src/core/custom-commands/loader.ts +9 -0
  37. package/src/core/custom-tools/wrapper.ts +5 -0
  38. package/src/core/event-bus.ts +59 -0
  39. package/src/core/export-html/vendor/highlight.min.js +1213 -0
  40. package/src/core/export-html/vendor/marked.min.js +6 -0
  41. package/src/core/extensions/index.ts +100 -0
  42. package/src/core/extensions/loader.ts +501 -0
  43. package/src/core/extensions/runner.ts +477 -0
  44. package/src/core/extensions/types.ts +712 -0
  45. package/src/core/extensions/wrapper.ts +147 -0
  46. package/src/core/hooks/types.ts +2 -2
  47. package/src/core/index.ts +10 -21
  48. package/src/core/keybindings.ts +199 -0
  49. package/src/core/messages.ts +26 -7
  50. package/src/core/model-registry.ts +123 -46
  51. package/src/core/model-resolver.ts +7 -5
  52. package/src/core/prompt-templates.ts +242 -0
  53. package/src/core/sdk.ts +378 -295
  54. package/src/core/session-manager.ts +72 -58
  55. package/src/core/settings-manager.ts +118 -22
  56. package/src/core/system-prompt.ts +24 -1
  57. package/src/core/terminal-notify.ts +37 -0
  58. package/src/core/tools/context.ts +4 -4
  59. package/src/core/tools/exa/mcp-client.ts +5 -4
  60. package/src/core/tools/exa/render.ts +176 -131
  61. package/src/core/tools/find.ts +7 -1
  62. package/src/core/tools/gemini-image.ts +361 -0
  63. package/src/core/tools/git.ts +216 -0
  64. package/src/core/tools/index.ts +28 -15
  65. package/src/core/tools/ls.ts +9 -2
  66. package/src/core/tools/lsp/config.ts +5 -4
  67. package/src/core/tools/lsp/index.ts +17 -12
  68. package/src/core/tools/lsp/render.ts +39 -47
  69. package/src/core/tools/read.ts +66 -29
  70. package/src/core/tools/render-utils.ts +268 -0
  71. package/src/core/tools/renderers.ts +243 -225
  72. package/src/core/tools/task/discovery.ts +2 -2
  73. package/src/core/tools/task/executor.ts +66 -58
  74. package/src/core/tools/task/index.ts +29 -10
  75. package/src/core/tools/task/model-resolver.ts +8 -13
  76. package/src/core/tools/task/omp-command.ts +24 -0
  77. package/src/core/tools/task/render.ts +37 -62
  78. package/src/core/tools/task/types.ts +3 -0
  79. package/src/core/tools/web-fetch.ts +29 -28
  80. package/src/core/tools/web-search/index.ts +6 -5
  81. package/src/core/tools/web-search/providers/exa.ts +6 -5
  82. package/src/core/tools/web-search/render.ts +66 -111
  83. package/src/core/voice-controller.ts +135 -0
  84. package/src/core/voice-supervisor.ts +1003 -0
  85. package/src/core/voice.ts +308 -0
  86. package/src/discovery/builtin.ts +75 -1
  87. package/src/discovery/claude.ts +47 -1
  88. package/src/discovery/codex.ts +54 -2
  89. package/src/discovery/gemini.ts +55 -2
  90. package/src/discovery/helpers.ts +100 -1
  91. package/src/discovery/index.ts +2 -0
  92. package/src/index.ts +14 -9
  93. package/src/lib/worktree/collapse.ts +179 -0
  94. package/src/lib/worktree/constants.ts +14 -0
  95. package/src/lib/worktree/errors.ts +23 -0
  96. package/src/lib/worktree/git.ts +110 -0
  97. package/src/lib/worktree/index.ts +23 -0
  98. package/src/lib/worktree/operations.ts +216 -0
  99. package/src/lib/worktree/session.ts +114 -0
  100. package/src/lib/worktree/stats.ts +67 -0
  101. package/src/main.ts +61 -37
  102. package/src/migrations.ts +37 -7
  103. package/src/modes/interactive/components/bash-execution.ts +6 -4
  104. package/src/modes/interactive/components/custom-editor.ts +55 -0
  105. package/src/modes/interactive/components/custom-message.ts +95 -0
  106. package/src/modes/interactive/components/extensions/extension-list.ts +5 -0
  107. package/src/modes/interactive/components/extensions/inspector-panel.ts +18 -12
  108. package/src/modes/interactive/components/extensions/state-manager.ts +12 -0
  109. package/src/modes/interactive/components/extensions/types.ts +1 -0
  110. package/src/modes/interactive/components/footer.ts +324 -0
  111. package/src/modes/interactive/components/hook-selector.ts +3 -3
  112. package/src/modes/interactive/components/model-selector.ts +7 -6
  113. package/src/modes/interactive/components/oauth-selector.ts +3 -3
  114. package/src/modes/interactive/components/settings-defs.ts +55 -6
  115. package/src/modes/interactive/components/status-line.ts +45 -37
  116. package/src/modes/interactive/components/tool-execution.ts +95 -23
  117. package/src/modes/interactive/interactive-mode.ts +643 -113
  118. package/src/modes/interactive/theme/defaults/index.ts +16 -16
  119. package/src/modes/print-mode.ts +14 -72
  120. package/src/modes/rpc/rpc-client.ts +23 -9
  121. package/src/modes/rpc/rpc-mode.ts +137 -125
  122. package/src/modes/rpc/rpc-types.ts +46 -24
  123. package/src/prompts/task.md +1 -0
  124. package/src/prompts/tools/gemini-image.md +4 -0
  125. package/src/prompts/tools/git.md +9 -0
  126. package/src/prompts/voice-summary.md +12 -0
  127. package/src/utils/image-convert.ts +26 -0
  128. package/src/utils/image-resize.ts +215 -0
  129. package/src/utils/shell-snapshot.ts +22 -20
@@ -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 HookFactory, SessionManager } from "@oh-my-pi/pi-coding-agent";
8
+ import { createAgentSession, type ExtensionFactory, SessionManager } from "@oh-my-pi/pi-coding-agent";
8
9
 
9
- // Logging hook
10
- const loggingHook: HookFactory = (api) => {
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 hook (returns { block: true, reason: "..." })
26
- const safetyHook: HookFactory = (api) => {
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
- hooks: [{ factory: loggingHook }, { factory: safetyHook }],
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 hooks:
54
- // hooks: []
54
+ // Disable all extensions:
55
+ // extensions: []
55
56
 
56
- // Merge with discovered hooks:
57
- // const discovered = await discoverHooks();
58
- // hooks: [...discovered, { factory: myHook }]
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
- // additionalHookPaths: ["/extra/hooks"]
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
- discoverSlashCommands,
10
- type FileSlashCommand,
10
+ discoverPromptTemplates,
11
+ type PromptTemplate,
11
12
  SessionManager,
12
13
  } from "@oh-my-pi/pi-coding-agent";
13
14
 
14
- // Discover commands from cwd/.omp/commands/ and ~/.omp/agent/commands/
15
- const discovered = discoverSlashCommands();
16
- console.log("Discovered slash commands:");
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 commands
22
- const deployCommand: FileSlashCommand = {
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
- // Use discovered + custom commands
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
- slashCommands: [...discovered, deployCommand],
40
+ promptTemplates: [...discovered, deployCommand],
36
41
  sessionManager: SessionManager.inMemory(),
37
42
  });
38
43
 
39
- console.log(`Session created with ${discovered.length + 1} slash commands`);
44
+ console.log(`Session created with ${discovered.length + 1} prompt templates`);
40
45
 
41
- // Disable slash commands:
42
- // slashCommands: []
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 HookFactory,
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 hook
37
- const auditHook: HookFactory = (api) => {
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: [{ tool: statusTool }],
80
- hooks: [{ factory: auditHook }],
79
+ customTools: [statusTool],
80
+ extensions: [auditHook],
81
81
  skills: [],
82
82
  contextFiles: [],
83
- slashCommands: [],
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.15.1",
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.15.1",
43
- "@oh-my-pi/pi-ai": "3.15.1",
44
- "@oh-my-pi/pi-tui": "3.15.1",
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
- "vitest": "^3.2.4"
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
- customTools?: string[];
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 === "--tool" && i + 1 < args.length) {
124
- result.customTools = result.customTools ?? [];
125
- result.customTools.push(args[++i]);
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
- --tool <path> Load a custom tool file (can be used multiple times)
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 { access, readFile, stat } from "node:fs/promises";
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
- const RESIZE_TRIGGER_MAX_DIMENSION = 2048;
19
- const MAX_RESIZE_WIDTH = 1920;
20
- const MAX_RESIZE_HEIGHT = 1080;
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
- try {
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 content = await readFile(absolutePath);
94
- const processed = await processImageAttachment(content, mimeType);
95
- const base64Content = processed.buffer.toString("base64");
96
-
97
- const attachment: ImageContent = {
98
- type: "image",
99
- mimeType: processed.mimeType,
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
- text += `<file name="${absolutePath}"></file>\n`;
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 = await readFile(absolutePath, "utf-8");
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);
@@ -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 = await modelRegistry.getAvailable();
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
- if (existsSync(filePath)) {
234
- try {
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
- if (existsSync(filePath)) {
265
- try {
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(subpath: string, cwd: string = process.cwd()): ConfigDirEntry | undefined {
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(subpath: string, cwd: string = process.cwd()): ConfigDirEntry[] {
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
  }