@oh-my-pi/pi-coding-agent 11.7.3 → 11.8.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 +47 -0
- package/docs/extensions.md +66 -2
- package/docs/rpc.md +36 -0
- package/examples/extensions/reload-runtime.ts +37 -0
- package/package.json +7 -7
- package/src/cli/args.ts +11 -11
- package/src/cli/file-processor.ts +27 -2
- package/src/extensibility/extensions/runner.ts +4 -0
- package/src/extensibility/extensions/types.ts +12 -0
- package/src/extensibility/skills.ts +12 -1
- package/src/modes/controllers/extension-ui-controller.ts +21 -0
- package/src/modes/print-mode.ts +3 -0
- package/src/modes/rpc/rpc-mode.ts +8 -0
- package/src/modes/utils/ui-helpers.ts +12 -5
- package/src/prompts/system/system-prompt.md +1 -0
- package/src/prompts/tools/task.md +25 -13
- package/src/session/agent-session.ts +16 -0
- package/src/session/messages.ts +4 -0
- package/src/system-prompt.ts +7 -0
- package/src/tools/truncate.ts +3 -1
- package/src/utils/file-mentions.ts +24 -0
- package/src/utils/tools-manager.ts +14 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,53 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [11.8.1] - 2026-02-10
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added current date to system prompt context in YYYY-MM-DD format for date-aware agent reasoning
|
|
10
|
+
- Added file size display in UI when files are skipped due to size limits
|
|
11
|
+
- Added support for gigabyte (GB) file size formatting in truncate utility
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- Changed skipped file messages to include file size information for better visibility into why files were excluded
|
|
16
|
+
- Changed file processing to skip reading files exceeding 5MB (text) or 25MB (images) and include them as path-only references instead
|
|
17
|
+
- Changed @mention auto-reading to skip files exceeding 5MB (text) or 25MB (images) to prevent out-of-memory issues with large files
|
|
18
|
+
- Clarified that subagents automatically inherit full system prompt including AGENTS.md, context files, and skills — do not repeat project rules or conventions in task context
|
|
19
|
+
- Updated task context guidance to focus on session-specific information subagents lack, eliminating redundant documentation of project constraints already available to them
|
|
20
|
+
- Refined constraints template to emphasize task-specific rules and session decisions rather than global project conventions
|
|
21
|
+
- Expanded anti-patterns section to explicitly flag redundant context that wastes tokens by repeating AGENTS.md rules, project constraints, and tool preferences
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- Fixed bash tool hanging when commands spawn background jobs by properly detecting foreground process completion
|
|
26
|
+
- Fixed bash tool occasionally hanging after command completion when background jobs keep stdout/stderr open
|
|
27
|
+
- Fixed crash when auto-reading @mentions for very large files by skipping content injection with an explicit "skipped" note
|
|
28
|
+
- Improved bash tool output draining after foreground completion to reduce tail output truncation
|
|
29
|
+
|
|
30
|
+
## [11.8.0] - 2026-02-10
|
|
31
|
+
### Added
|
|
32
|
+
|
|
33
|
+
- Added `ctx.reload()` method to extension command context to reload extensions, skills, prompts, and themes from disk
|
|
34
|
+
- Added `ctx.ui.pasteToEditor()` method to paste text into the editor with proper handling (e.g., large paste markers in interactive mode)
|
|
35
|
+
- Added extension UI sub-protocol for RPC mode enabling dialog methods (`select`, `confirm`, `input`, `editor`) and fire-and-forget UI methods via client communication
|
|
36
|
+
- Added support for tilde (`~`) expansion in custom skill directory paths
|
|
37
|
+
- Added example extension demonstrating `ctx.reload()` usage with both command and LLM-callable tool patterns
|
|
38
|
+
|
|
39
|
+
### Changed
|
|
40
|
+
|
|
41
|
+
- Changed `ctx.hasUI` behavior: now `true` in RPC mode (previously `false`), with dialog methods working via extension UI sub-protocol
|
|
42
|
+
- Changed warning output for invalid CLI arguments to use structured logging instead of console.error
|
|
43
|
+
- Changed help text to indicate command-specific help is available via `<command> --help`
|
|
44
|
+
- Changed tool result event handlers to chain like middleware, allowing each handler to see and modify results from previous handlers with partial patch support
|
|
45
|
+
|
|
46
|
+
### Fixed
|
|
47
|
+
|
|
48
|
+
- Fixed archive extraction security vulnerability by validating that extracted paths do not escape the extraction directory
|
|
49
|
+
- Fixed archive format validation to reject unsupported formats before extraction attempt
|
|
50
|
+
- Fixed archive extraction error handling to provide clear error messages on failure
|
|
51
|
+
|
|
5
52
|
## [11.7.0] - 2026-02-07
|
|
6
53
|
### Changed
|
|
7
54
|
|
package/docs/extensions.md
CHANGED
|
@@ -583,6 +583,11 @@ pi.on("tool_call", async (event, ctx) => {
|
|
|
583
583
|
|
|
584
584
|
Fired after tool executes. **Can modify result.**
|
|
585
585
|
|
|
586
|
+
`tool_result` handlers chain like middleware:
|
|
587
|
+
- Handlers run in extension load order
|
|
588
|
+
- Each handler sees the latest result after previous handler changes
|
|
589
|
+
- Handlers can return partial patches (`content`, `details`, or `isError`); omitted fields keep their current values
|
|
590
|
+
|
|
586
591
|
```typescript
|
|
587
592
|
pi.on("tool_result", async (event, ctx) => {
|
|
588
593
|
// event.toolName, event.toolCallId, event.input
|
|
@@ -609,7 +614,7 @@ UI methods for user interaction. See [Custom UI](#custom-ui) for full details.
|
|
|
609
614
|
|
|
610
615
|
### ctx.hasUI
|
|
611
616
|
|
|
612
|
-
`false` in print mode (`-p`)
|
|
617
|
+
`false` in print mode (`-p`) and JSON mode. `true` in interactive and RPC mode. In RPC mode, dialog methods (`select`, `confirm`, `input`, `editor`) work via the extension UI sub-protocol, and fire-and-forget methods (`notify`, `setStatus`, `setWidget`, `setTitle`, `setEditorText`) emit requests to the client. Some TUI-specific methods are no-ops or return defaults (see [rpc.md](rpc.md#extension-ui-protocol)).
|
|
613
618
|
|
|
614
619
|
### ctx.cwd
|
|
615
620
|
|
|
@@ -704,6 +709,62 @@ const result = await ctx.navigateTree("entry-id-456", {
|
|
|
704
709
|
});
|
|
705
710
|
```
|
|
706
711
|
|
|
712
|
+
### ctx.reload()
|
|
713
|
+
|
|
714
|
+
Run the same reload flow as `/reload`.
|
|
715
|
+
|
|
716
|
+
```typescript
|
|
717
|
+
pi.registerCommand("reload-runtime", {
|
|
718
|
+
description: "Reload extensions, skills, prompts, and themes",
|
|
719
|
+
handler: async (_args, ctx) => {
|
|
720
|
+
await ctx.reload();
|
|
721
|
+
return;
|
|
722
|
+
},
|
|
723
|
+
});
|
|
724
|
+
```
|
|
725
|
+
|
|
726
|
+
Important behavior:
|
|
727
|
+
- `await ctx.reload()` emits `session_shutdown` for the current extension runtime
|
|
728
|
+
- It then reloads resources and emits `session_start` (and `resources_discover` with reason `"reload"`) for the new runtime
|
|
729
|
+
- The currently running command handler still continues in the old call frame
|
|
730
|
+
- Code after `await ctx.reload()` still runs from the pre-reload version
|
|
731
|
+
- Code after `await ctx.reload()` must not assume old in-memory extension state is still valid
|
|
732
|
+
- After the handler returns, future commands/events/tool calls use the new extension version
|
|
733
|
+
|
|
734
|
+
For predictable behavior, treat reload as terminal for that handler (`await ctx.reload(); return;`).
|
|
735
|
+
|
|
736
|
+
Tools run with `ExtensionContext`, so they cannot call `ctx.reload()` directly. Use a command as the reload entrypoint, then expose a tool that queues that command as a follow-up user message.
|
|
737
|
+
|
|
738
|
+
Example tool the LLM can call to trigger reload:
|
|
739
|
+
|
|
740
|
+
```typescript
|
|
741
|
+
import type { ExtensionAPI } from "@oh-my-pi/pi-coding-agent";
|
|
742
|
+
import { Type } from "@sinclair/typebox";
|
|
743
|
+
|
|
744
|
+
export default function (pi: ExtensionAPI) {
|
|
745
|
+
pi.registerCommand("reload-runtime", {
|
|
746
|
+
description: "Reload extensions, skills, prompts, and themes",
|
|
747
|
+
handler: async (_args, ctx) => {
|
|
748
|
+
await ctx.reload();
|
|
749
|
+
return;
|
|
750
|
+
},
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
pi.registerTool({
|
|
754
|
+
name: "reload_runtime",
|
|
755
|
+
label: "Reload Runtime",
|
|
756
|
+
description: "Reload extensions, skills, prompts, and themes",
|
|
757
|
+
parameters: Type.Object({}),
|
|
758
|
+
async execute() {
|
|
759
|
+
pi.sendUserMessage("/reload-runtime", { deliverAs: "followUp" });
|
|
760
|
+
return {
|
|
761
|
+
content: [{ type: "text", text: "Queued /reload-runtime as a follow-up command." }],
|
|
762
|
+
};
|
|
763
|
+
},
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
```
|
|
767
|
+
|
|
707
768
|
## ExtensionAPI Methods
|
|
708
769
|
|
|
709
770
|
### pi.on(event, handler)
|
|
@@ -1111,6 +1172,9 @@ ctx.ui.setTitle("omp - my-project");
|
|
|
1111
1172
|
ctx.ui.setEditorText("Prefill text");
|
|
1112
1173
|
const current = ctx.ui.getEditorText();
|
|
1113
1174
|
|
|
1175
|
+
// Paste into editor (triggers paste handling, including collapse for large content)
|
|
1176
|
+
ctx.ui.pasteToEditor("pasted content");
|
|
1177
|
+
|
|
1114
1178
|
// Custom editor component
|
|
1115
1179
|
ctx.ui.setEditorComponent((tui, theme, keybindings) => new MyEditor(tui, theme, keybindings)); // EditorComponent
|
|
1116
1180
|
ctx.ui.setEditorComponent(undefined); // Restore default
|
|
@@ -1147,7 +1211,7 @@ The callback receives:
|
|
|
1147
1211
|
- `keybindings` - Keybindings manager for resolving bindings
|
|
1148
1212
|
- `done(value)` - Call to close component and return value
|
|
1149
1213
|
|
|
1150
|
-
See [tui.md](tui.md) for the full component API and [examples/extensions/](../examples/extensions/) for working examples (todo.ts, tools.ts).
|
|
1214
|
+
See [tui.md](tui.md) for the full component API and [examples/extensions/](../examples/extensions/) for working examples (todo.ts, tools.ts, reload-runtime.ts).
|
|
1151
1215
|
|
|
1152
1216
|
### Message Rendering
|
|
1153
1217
|
|
package/docs/rpc.md
CHANGED
|
@@ -686,6 +686,42 @@ Response:
|
|
|
686
686
|
|
|
687
687
|
Returns an error if the name is empty.
|
|
688
688
|
|
|
689
|
+
## Extension UI (stdout)
|
|
690
|
+
|
|
691
|
+
In RPC mode, extensions receive an [`ExtensionUIContext`](./extensions.md#custom-ui) backed by an extension UI sub-protocol.
|
|
692
|
+
When an extension calls a dialog or UI method, the agent emits an `extension_ui_request` JSON line on stdout. The host must
|
|
693
|
+
respond by writing an `extension_ui_response` JSON line on stdin.
|
|
694
|
+
|
|
695
|
+
If a dialog request includes a `timeout` field, the agent auto-resolves it with a default value when the timeout expires.
|
|
696
|
+
The host does not need to track or enforce timeouts.
|
|
697
|
+
|
|
698
|
+
Example request (stdout):
|
|
699
|
+
|
|
700
|
+
```json
|
|
701
|
+
{ "type": "extension_ui_request", "id": "req-123", "method": "confirm", "title": "Confirm", "message": "Continue?", "timeout": 30000 }
|
|
702
|
+
```
|
|
703
|
+
|
|
704
|
+
Example response (stdin):
|
|
705
|
+
|
|
706
|
+
```json
|
|
707
|
+
{ "type": "extension_ui_response", "id": "req-123", "confirmed": true }
|
|
708
|
+
```
|
|
709
|
+
|
|
710
|
+
### Unsupported / degraded UI methods
|
|
711
|
+
|
|
712
|
+
Some `ExtensionUIContext` methods are not supported or degraded in RPC mode because they require direct TUI access:
|
|
713
|
+
|
|
714
|
+
- `custom()` returns `undefined`
|
|
715
|
+
- `setWorkingMessage()`, `setFooter()`, `setHeader()`, `setEditorComponent()`, `setToolsExpanded()` are no-ops
|
|
716
|
+
- `getEditorText()` returns `""`
|
|
717
|
+
- `getToolsExpanded()` returns `false`
|
|
718
|
+
- `setWidget()` only supports `string[]` (factory functions/components are ignored)
|
|
719
|
+
- `getAllThemes()` returns `[]`
|
|
720
|
+
- `getTheme()` returns `undefined`
|
|
721
|
+
- `setTheme()` returns `{ success: false, error: "Theme switching not supported in RPC mode" }`
|
|
722
|
+
|
|
723
|
+
Note: `ctx.hasUI` is `true` in RPC mode because dialog and fire-and-forget UI methods are functional via this sub-protocol.
|
|
724
|
+
|
|
689
725
|
## Events
|
|
690
726
|
|
|
691
727
|
Events are streamed to stdout as JSON lines during agent operation. Events do NOT include an `id` field (only responses do).
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reload Runtime Extension
|
|
3
|
+
*
|
|
4
|
+
* Demonstrates ctx.reload() from ExtensionCommandContext and an LLM-callable
|
|
5
|
+
* tool that queues a follow-up command to trigger reload.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
9
|
+
import { Type } from "@sinclair/typebox";
|
|
10
|
+
|
|
11
|
+
export default function (pi: ExtensionAPI) {
|
|
12
|
+
// Command entrypoint for reload.
|
|
13
|
+
// Treat reload as terminal for this handler.
|
|
14
|
+
pi.registerCommand("reload-runtime", {
|
|
15
|
+
description: "Reload extensions, skills, prompts, and themes",
|
|
16
|
+
handler: async (_args, ctx) => {
|
|
17
|
+
await ctx.reload();
|
|
18
|
+
return;
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// LLM-callable tool. Tools get ExtensionContext, so they cannot call ctx.reload() directly.
|
|
23
|
+
// Instead, queue a follow-up user command that executes the command above.
|
|
24
|
+
pi.registerTool({
|
|
25
|
+
name: "reload_runtime",
|
|
26
|
+
label: "Reload Runtime",
|
|
27
|
+
description: "Reload extensions, skills, prompts, and themes",
|
|
28
|
+
parameters: Type.Object({}),
|
|
29
|
+
async execute() {
|
|
30
|
+
pi.sendUserMessage("/reload-runtime", { deliverAs: "followUp" });
|
|
31
|
+
return {
|
|
32
|
+
content: [{ type: "text", text: "Queued /reload-runtime as a follow-up command." }],
|
|
33
|
+
details: {},
|
|
34
|
+
};
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
3
|
-
"version": "11.
|
|
3
|
+
"version": "11.8.1",
|
|
4
4
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"ompConfig": {
|
|
@@ -90,12 +90,12 @@
|
|
|
90
90
|
"@mozilla/readability": "0.6.0",
|
|
91
91
|
"@oclif/core": "^4.8.0",
|
|
92
92
|
"@oclif/plugin-autocomplete": "^3.2.40",
|
|
93
|
-
"@oh-my-pi/omp-stats": "11.
|
|
94
|
-
"@oh-my-pi/pi-agent-core": "11.
|
|
95
|
-
"@oh-my-pi/pi-ai": "11.
|
|
96
|
-
"@oh-my-pi/pi-natives": "11.
|
|
97
|
-
"@oh-my-pi/pi-tui": "11.
|
|
98
|
-
"@oh-my-pi/pi-utils": "11.
|
|
93
|
+
"@oh-my-pi/omp-stats": "11.8.1",
|
|
94
|
+
"@oh-my-pi/pi-agent-core": "11.8.1",
|
|
95
|
+
"@oh-my-pi/pi-ai": "11.8.1",
|
|
96
|
+
"@oh-my-pi/pi-natives": "11.8.1",
|
|
97
|
+
"@oh-my-pi/pi-tui": "11.8.1",
|
|
98
|
+
"@oh-my-pi/pi-utils": "11.8.1",
|
|
99
99
|
"@sinclair/typebox": "^0.34.48",
|
|
100
100
|
"ajv": "^8.17.1",
|
|
101
101
|
"chalk": "^5.6.2",
|
package/src/cli/args.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* CLI argument parsing and help display
|
|
3
3
|
*/
|
|
4
4
|
import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
5
|
+
import { logger } from "@oh-my-pi/pi-utils";
|
|
5
6
|
import chalk from "chalk";
|
|
6
7
|
import { APP_NAME, CONFIG_DIR_NAME } from "../config";
|
|
7
8
|
import { BUILTIN_TOOLS } from "../tools";
|
|
@@ -115,11 +116,10 @@ export function parseArgs(args: string[], extensionFlags?: Map<string, { type: "
|
|
|
115
116
|
if (name in BUILTIN_TOOLS) {
|
|
116
117
|
validTools.push(name);
|
|
117
118
|
} else {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
);
|
|
119
|
+
logger.warn("Unknown tool passed to --tools", {
|
|
120
|
+
tool: name,
|
|
121
|
+
validTools: Object.keys(BUILTIN_TOOLS),
|
|
122
|
+
});
|
|
123
123
|
}
|
|
124
124
|
}
|
|
125
125
|
result.tools = validTools;
|
|
@@ -128,11 +128,10 @@ export function parseArgs(args: string[], extensionFlags?: Map<string, { type: "
|
|
|
128
128
|
if (isValidThinkingLevel(level)) {
|
|
129
129
|
result.thinking = level;
|
|
130
130
|
} else {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
);
|
|
131
|
+
logger.warn("Invalid thinking level passed to --thinking", {
|
|
132
|
+
level,
|
|
133
|
+
validThinkingLevels: [...VALID_THINKING_LEVELS],
|
|
134
|
+
});
|
|
136
135
|
}
|
|
137
136
|
} else if (arg === "--print" || arg === "-p") {
|
|
138
137
|
result.print = true;
|
|
@@ -245,7 +244,8 @@ ${chalk.bold("Available Tools (all enabled by default):")}
|
|
|
245
244
|
export function printHelp(): void {
|
|
246
245
|
process.stdout.write(
|
|
247
246
|
`${chalk.bold(APP_NAME)} - AI coding assistant\n\n` +
|
|
248
|
-
`Run ${APP_NAME} --help for full command and option details.\n
|
|
247
|
+
`Run ${APP_NAME} --help for full command and option details.\n` +
|
|
248
|
+
`Run ${APP_NAME} <command> --help for command-specific help.\n\n` +
|
|
249
249
|
`${getExtraHelpText()}\n`,
|
|
250
250
|
);
|
|
251
251
|
}
|
|
@@ -7,9 +7,15 @@ import type { ImageContent } from "@oh-my-pi/pi-ai";
|
|
|
7
7
|
import { isEnoent } from "@oh-my-pi/pi-utils";
|
|
8
8
|
import chalk from "chalk";
|
|
9
9
|
import { resolveReadPath } from "../tools/path-utils";
|
|
10
|
+
import { formatSize } from "../tools/truncate";
|
|
10
11
|
import { formatDimensionNote, resizeImage } from "../utils/image-resize";
|
|
11
12
|
import { detectSupportedImageMimeTypeFromFile } from "../utils/mime";
|
|
12
13
|
|
|
14
|
+
// Keep CLI startup responsive and avoid OOM when users pass huge files.
|
|
15
|
+
// If a file exceeds these limits, we include it as a path-only <file/> block.
|
|
16
|
+
const MAX_CLI_TEXT_BYTES = 5 * 1024 * 1024; // 5MB
|
|
17
|
+
const MAX_CLI_IMAGE_BYTES = 25 * 1024 * 1024; // 25MB
|
|
18
|
+
|
|
13
19
|
export interface ProcessedFiles {
|
|
14
20
|
text: string;
|
|
15
21
|
images: ImageContent[];
|
|
@@ -30,6 +36,27 @@ export async function processFileArguments(fileArgs: string[], options?: Process
|
|
|
30
36
|
// Expand and resolve path (handles ~ expansion and macOS screenshot Unicode spaces)
|
|
31
37
|
const absolutePath = path.resolve(resolveReadPath(fileArg, process.cwd()));
|
|
32
38
|
|
|
39
|
+
let stat: Awaited<ReturnType<typeof fs.stat>>;
|
|
40
|
+
try {
|
|
41
|
+
stat = await fs.stat(absolutePath);
|
|
42
|
+
} catch (err) {
|
|
43
|
+
if (isEnoent(err)) {
|
|
44
|
+
console.error(chalk.red(`Error: File not found: ${absolutePath}`));
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
throw err;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const mimeType = await detectSupportedImageMimeTypeFromFile(absolutePath);
|
|
51
|
+
const maxBytes = mimeType ? MAX_CLI_IMAGE_BYTES : MAX_CLI_TEXT_BYTES;
|
|
52
|
+
if (stat.size > maxBytes) {
|
|
53
|
+
console.error(
|
|
54
|
+
chalk.yellow(`Warning: Skipping file contents (too large: ${formatSize(stat.size)}): ${absolutePath}`),
|
|
55
|
+
);
|
|
56
|
+
text += `<file name="${absolutePath}">(skipped: too large, ${formatSize(stat.size)})</file>\n`;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
|
|
33
60
|
// Read file, handling not-found gracefully
|
|
34
61
|
let buffer: Buffer;
|
|
35
62
|
try {
|
|
@@ -45,8 +72,6 @@ export async function processFileArguments(fileArgs: string[], options?: Process
|
|
|
45
72
|
continue;
|
|
46
73
|
}
|
|
47
74
|
|
|
48
|
-
const mimeType = await detectSupportedImageMimeTypeFromFile(absolutePath);
|
|
49
|
-
|
|
50
75
|
if (mimeType) {
|
|
51
76
|
// Handle image file
|
|
52
77
|
const base64Content = buffer.toBase64();
|
|
@@ -138,6 +138,7 @@ const noOpUIContext: ExtensionUIContext = {
|
|
|
138
138
|
setTitle: () => {},
|
|
139
139
|
custom: async () => undefined as never,
|
|
140
140
|
setEditorText: () => {},
|
|
141
|
+
pasteToEditor: () => {},
|
|
141
142
|
getEditorText: () => "",
|
|
142
143
|
editor: async () => undefined,
|
|
143
144
|
setEditorComponent: () => {},
|
|
@@ -166,6 +167,7 @@ export class ExtensionRunner {
|
|
|
166
167
|
private branchHandler: BranchHandler = async () => ({ cancelled: false });
|
|
167
168
|
private navigateTreeHandler: NavigateTreeHandler = async () => ({ cancelled: false });
|
|
168
169
|
private switchSessionHandler: SwitchSessionHandler = async () => ({ cancelled: false });
|
|
170
|
+
private reloadHandler: () => Promise<void> = async () => {};
|
|
169
171
|
private shutdownHandler: ShutdownHandler = () => {};
|
|
170
172
|
private commandDiagnostics: Array<{ type: string; message: string; path: string }> = [];
|
|
171
173
|
|
|
@@ -212,6 +214,7 @@ export class ExtensionRunner {
|
|
|
212
214
|
this.branchHandler = commandContextActions.branch;
|
|
213
215
|
this.navigateTreeHandler = commandContextActions.navigateTree;
|
|
214
216
|
this.switchSessionHandler = commandContextActions.switchSession;
|
|
217
|
+
this.reloadHandler = commandContextActions.reload;
|
|
215
218
|
this.getContextUsageFn = commandContextActions.getContextUsage;
|
|
216
219
|
this.compactFn = commandContextActions.compact;
|
|
217
220
|
}
|
|
@@ -405,6 +408,7 @@ export class ExtensionRunner {
|
|
|
405
408
|
branch: entryId => this.branchHandler(entryId),
|
|
406
409
|
navigateTree: (targetId, options) => this.navigateTreeHandler(targetId, options),
|
|
407
410
|
switchSession: sessionPath => this.switchSessionHandler(sessionPath),
|
|
411
|
+
reload: () => this.reloadHandler(),
|
|
408
412
|
compact: instructionsOrOptions => this.compactFn(instructionsOrOptions),
|
|
409
413
|
};
|
|
410
414
|
}
|
|
@@ -112,6 +112,14 @@ export interface ExtensionUIContext {
|
|
|
112
112
|
/** Set the text in the core input editor. */
|
|
113
113
|
setEditorText(text: string): void;
|
|
114
114
|
|
|
115
|
+
/**
|
|
116
|
+
* Paste text into the core input editor.
|
|
117
|
+
*
|
|
118
|
+
* Interactive mode should route through the editor's paste handling (e.g. large paste markers).
|
|
119
|
+
* Non-interactive modes may fall back to replacing the editor text.
|
|
120
|
+
*/
|
|
121
|
+
pasteToEditor(text: string): void;
|
|
122
|
+
|
|
115
123
|
/** Get the current text from the core input editor. */
|
|
116
124
|
getEditorText(): string;
|
|
117
125
|
|
|
@@ -220,6 +228,9 @@ export interface ExtensionCommandContext extends ExtensionContext {
|
|
|
220
228
|
/** Switch to a different session file. */
|
|
221
229
|
switchSession(sessionPath: string): Promise<{ cancelled: boolean }>;
|
|
222
230
|
|
|
231
|
+
/** Reload the current session/runtime state. */
|
|
232
|
+
reload(): Promise<void>;
|
|
233
|
+
|
|
223
234
|
/** Compact the session context (interactive mode shows UI). */
|
|
224
235
|
compact(instructionsOrOptions?: string | CompactOptions): Promise<void>;
|
|
225
236
|
}
|
|
@@ -1008,6 +1019,7 @@ export interface ExtensionCommandContextActions {
|
|
|
1008
1019
|
navigateTree: (targetId: string, options?: { summarize?: boolean }) => Promise<{ cancelled: boolean }>;
|
|
1009
1020
|
compact: (instructionsOrOptions?: string | CompactOptions) => Promise<void>;
|
|
1010
1021
|
switchSession: (sessionPath: string) => Promise<{ cancelled: boolean }>;
|
|
1022
|
+
reload: () => Promise<void>;
|
|
1011
1023
|
}
|
|
1012
1024
|
|
|
1013
1025
|
/** Full runtime = state + actions. */
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as fs from "node:fs/promises";
|
|
2
|
+
import * as os from "node:os";
|
|
2
3
|
import * as path from "node:path";
|
|
3
4
|
import { logger } from "@oh-my-pi/pi-utils";
|
|
4
5
|
import { skillCapability } from "../capability/skill";
|
|
@@ -316,7 +317,17 @@ export async function loadSkills(options: LoadSkillsOptions = {}): Promise<LoadS
|
|
|
316
317
|
|
|
317
318
|
// Process custom directories - scan directly without using full provider system
|
|
318
319
|
const allCustomSkills: Array<{ skill: Skill; path: string }> = [];
|
|
319
|
-
const customScanResults = await Promise.all(
|
|
320
|
+
const customScanResults = await Promise.all(
|
|
321
|
+
customDirectories.map(dir => {
|
|
322
|
+
let resolved = dir;
|
|
323
|
+
if (resolved.startsWith("~/")) {
|
|
324
|
+
resolved = path.join(os.homedir(), resolved.slice(2));
|
|
325
|
+
} else if (resolved === "~") {
|
|
326
|
+
resolved = os.homedir();
|
|
327
|
+
}
|
|
328
|
+
return scanDirectoryForSkills(resolved);
|
|
329
|
+
}),
|
|
330
|
+
);
|
|
320
331
|
for (const customSkills of customScanResults) {
|
|
321
332
|
for (const s of customSkills.skills) {
|
|
322
333
|
if (matchesIgnorePatterns(s.name)) continue;
|
|
@@ -47,6 +47,9 @@ export class ExtensionUiController {
|
|
|
47
47
|
setTitle: title => setTerminalTitle(title),
|
|
48
48
|
custom: (factory, _options) => this.showHookCustom(factory),
|
|
49
49
|
setEditorText: text => this.ctx.editor.setText(text),
|
|
50
|
+
pasteToEditor: text => {
|
|
51
|
+
this.ctx.editor.handleInput(`\x1b[200~${text}\x1b[201~`);
|
|
52
|
+
},
|
|
50
53
|
getEditorText: () => this.ctx.editor.getText(),
|
|
51
54
|
editor: (title, prefill) => this.showHookEditor(title, prefill),
|
|
52
55
|
get theme() {
|
|
@@ -138,6 +141,13 @@ export class ExtensionUiController {
|
|
|
138
141
|
const commandActions: ExtensionCommandContextActions = {
|
|
139
142
|
getContextUsage: () => this.ctx.session.getContextUsage(),
|
|
140
143
|
waitForIdle: () => this.ctx.session.agent.waitForIdle(),
|
|
144
|
+
reload: async () => {
|
|
145
|
+
await this.ctx.session.reload();
|
|
146
|
+
this.ctx.chatContainer.clear();
|
|
147
|
+
this.ctx.renderInitialMessages();
|
|
148
|
+
await this.ctx.reloadTodos();
|
|
149
|
+
this.ctx.showStatus("Reloaded session");
|
|
150
|
+
},
|
|
141
151
|
newSession: async options => {
|
|
142
152
|
// Stop any loading animation
|
|
143
153
|
if (this.ctx.loadingAnimation) {
|
|
@@ -319,6 +329,16 @@ export class ExtensionUiController {
|
|
|
319
329
|
const commandActions: ExtensionCommandContextActions = {
|
|
320
330
|
getContextUsage: () => this.ctx.session.getContextUsage(),
|
|
321
331
|
waitForIdle: () => this.ctx.session.agent.waitForIdle(),
|
|
332
|
+
reload: async () => {
|
|
333
|
+
if (this.ctx.isBackgrounded) {
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
await this.ctx.session.reload();
|
|
337
|
+
this.ctx.chatContainer.clear();
|
|
338
|
+
this.ctx.renderInitialMessages();
|
|
339
|
+
await this.ctx.reloadTodos();
|
|
340
|
+
this.ctx.showStatus("Reloaded session");
|
|
341
|
+
},
|
|
322
342
|
newSession: async options => {
|
|
323
343
|
if (this.ctx.isBackgrounded) {
|
|
324
344
|
return { cancelled: true };
|
|
@@ -436,6 +456,7 @@ export class ExtensionUiController {
|
|
|
436
456
|
setTitle: () => {},
|
|
437
457
|
custom: async () => undefined as never,
|
|
438
458
|
setEditorText: () => {},
|
|
459
|
+
pasteToEditor: () => {},
|
|
439
460
|
getEditorText: () => "",
|
|
440
461
|
editor: async () => undefined,
|
|
441
462
|
get theme() {
|
package/src/modes/print-mode.ts
CHANGED
|
@@ -114,6 +114,9 @@ export async function runPrintMode(session: AgentSession, options: PrintModeOpti
|
|
|
114
114
|
const success = await session.switchSession(sessionPath);
|
|
115
115
|
return { cancelled: !success };
|
|
116
116
|
},
|
|
117
|
+
reload: async () => {
|
|
118
|
+
await session.reload();
|
|
119
|
+
},
|
|
117
120
|
compact: async instructionsOrOptions => {
|
|
118
121
|
const instructions = typeof instructionsOrOptions === "string" ? instructionsOrOptions : undefined;
|
|
119
122
|
const options =
|
|
@@ -227,6 +227,11 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
|
|
|
227
227
|
return undefined as never;
|
|
228
228
|
}
|
|
229
229
|
|
|
230
|
+
pasteToEditor(text: string): void {
|
|
231
|
+
// Paste handling not supported in RPC mode - falls back to setEditorText
|
|
232
|
+
this.setEditorText(text);
|
|
233
|
+
}
|
|
234
|
+
|
|
230
235
|
setEditorText(text: string): void {
|
|
231
236
|
// Fire and forget - host can implement editor control
|
|
232
237
|
this.output({
|
|
@@ -379,6 +384,9 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
|
|
|
379
384
|
const success = await session.switchSession(sessionPath);
|
|
380
385
|
return { cancelled: !success };
|
|
381
386
|
},
|
|
387
|
+
reload: async () => {
|
|
388
|
+
await session.reload();
|
|
389
|
+
},
|
|
382
390
|
compact: async instructionsOrOptions => {
|
|
383
391
|
const instructions = typeof instructionsOrOptions === "string" ? instructionsOrOptions : undefined;
|
|
384
392
|
const options =
|
|
@@ -17,6 +17,7 @@ import { theme } from "../../modes/theme/theme";
|
|
|
17
17
|
import type { CompactionQueuedMessage, InteractiveModeContext } from "../../modes/types";
|
|
18
18
|
import { type CustomMessage, SKILL_PROMPT_MESSAGE_TYPE, type SkillPromptDetails } from "../../session/messages";
|
|
19
19
|
import type { SessionContext } from "../../session/session-manager";
|
|
20
|
+
import { formatSize } from "../../tools/truncate";
|
|
20
21
|
|
|
21
22
|
type TextBlock = { type: "text"; text: string };
|
|
22
23
|
|
|
@@ -127,11 +128,17 @@ export class UiHelpers {
|
|
|
127
128
|
case "fileMention": {
|
|
128
129
|
// Render compact file mention display
|
|
129
130
|
for (const file of message.files) {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
131
|
+
let suffix: string;
|
|
132
|
+
if (file.skippedReason === "tooLarge") {
|
|
133
|
+
const size = typeof file.byteSize === "number" ? formatSize(file.byteSize) : "unknown size";
|
|
134
|
+
suffix = `(skipped: ${size})`;
|
|
135
|
+
} else {
|
|
136
|
+
suffix = file.image
|
|
137
|
+
? "(image)"
|
|
138
|
+
: file.lineCount === undefined
|
|
139
|
+
? "(unknown lines)"
|
|
140
|
+
: `(${file.lineCount} lines)`;
|
|
141
|
+
}
|
|
135
142
|
const text = `${theme.fg("dim", `${theme.tree.last} `)}${theme.fg("muted", "Read")} ${theme.fg(
|
|
136
143
|
"accent",
|
|
137
144
|
file.path,
|
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
# Task
|
|
2
2
|
|
|
3
|
-
Launch subagents to execute parallel, well-scoped tasks
|
|
3
|
+
Launch subagents to execute parallel, well-scoped tasks.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## What subagents inherit automatically
|
|
6
|
+
Subagents receive the **full system prompt**, including AGENTS.md, context files, and skills. Do NOT repeat project rules, coding conventions, or style guidelines in `context` — they already have them.
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
## What subagents do NOT have
|
|
9
|
+
Subagents have no access to your conversation history. They don't know:
|
|
8
10
|
- Decisions you made but didn't write down
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
+
- Which approach you chose among alternatives
|
|
12
|
+
- What you learned from reading files during this session
|
|
13
|
+
- Requirements the user stated only in conversation
|
|
14
|
+
|
|
15
|
+
Subagents CAN grep the parent conversation file for supplementary details.
|
|
11
16
|
---
|
|
12
17
|
|
|
13
18
|
## Parameters
|
|
@@ -18,9 +23,13 @@ Agent type for all tasks in this batch.
|
|
|
18
23
|
|
|
19
24
|
### `context` (optional — strongly recommended)
|
|
20
25
|
|
|
21
|
-
Shared background prepended verbatim to every task `assignment`.
|
|
26
|
+
Shared background prepended verbatim to every task `assignment`. Use only for session-specific information subagents lack.
|
|
27
|
+
|
|
28
|
+
<critical>
|
|
29
|
+
Do NOT include project rules, coding conventions, or style guidelines — subagents already have AGENTS.md and context files in their system prompt. Repeating them wastes tokens.
|
|
30
|
+
</critical>
|
|
22
31
|
|
|
23
|
-
Use template; omit non-applicable sections
|
|
32
|
+
Use template; omit non-applicable sections:
|
|
24
33
|
|
|
25
34
|
````
|
|
26
35
|
## Goal
|
|
@@ -30,9 +39,8 @@ One sentence: batch accomplishes together.
|
|
|
30
39
|
Explicitly exclude tempting scope — what tasks must not touch/attempt.
|
|
31
40
|
|
|
32
41
|
## Constraints
|
|
33
|
-
- MUST / MUST NOT rules
|
|
34
|
-
-
|
|
35
|
-
- What exists vs what to create
|
|
42
|
+
- Task-specific MUST / MUST NOT rules not already in AGENTS.md
|
|
43
|
+
- Decisions made during this session that affect implementation
|
|
36
44
|
|
|
37
45
|
## Reference Files
|
|
38
46
|
- `path/to/file.ext` — pattern demo
|
|
@@ -47,9 +55,9 @@ Explicitly exclude tempting scope — what tasks must not touch/attempt.
|
|
|
47
55
|
- Definition of "done" for batch
|
|
48
56
|
- Note: build/test/lint verification happens AFTER all tasks complete — not inside tasks (see below)
|
|
49
57
|
````
|
|
50
|
-
**Belongs in `context`**:
|
|
58
|
+
**Belongs in `context`**: task-specific goal, non-goals, session decisions, reference paths, shared type definitions, API contracts, global acceptance commands — anything 2+ tasks need that isn't already in AGENTS.md.
|
|
51
59
|
**Rule of thumb:** if repeat in 2+ tasks, belongs in `context`.
|
|
52
|
-
**Does NOT belong in `context`**: per-task file lists, one-off requirements (go in `assignment`), structured output format (goes in `schema`).
|
|
60
|
+
**Does NOT belong in `context`**: project rules already in AGENTS.md/context files, per-task file lists, one-off requirements (go in `assignment`), structured output format (goes in `schema`).
|
|
53
61
|
|
|
54
62
|
### `tasks` (required)
|
|
55
63
|
|
|
@@ -117,6 +125,10 @@ Use structure every assignment:
|
|
|
117
125
|
- "Use existing patterns."
|
|
118
126
|
- "Follow conventions."
|
|
119
127
|
- "No WASM."
|
|
128
|
+
**Redundant context** — wastes tokens repeating what subagents already have:
|
|
129
|
+
- Restating AGENTS.md rules (coding style, import conventions, logger usage)
|
|
130
|
+
- Repeating project constraints from context files
|
|
131
|
+
- Listing tool/framework preferences already documented in the repo
|
|
120
132
|
|
|
121
133
|
If tempted to write above, expand using templates.
|
|
122
134
|
**Output format in prose instead of `schema`** — agent returns null:
|
|
@@ -252,7 +264,7 @@ Layered work with dependencies:
|
|
|
252
264
|
## Pre-flight checklist
|
|
253
265
|
|
|
254
266
|
Before calling tool, verify:
|
|
255
|
-
- [ ] `context` includes
|
|
267
|
+
- [ ] `context` includes only session-specific info not already in AGENTS.md/context files
|
|
256
268
|
- [ ] Each `assignment` follows assignment template — not one-liner
|
|
257
269
|
- [ ] Each `assignment` includes edge cases / "don’t break" items
|
|
258
270
|
- [ ] Tasks truly parallel (no hidden dependencies)
|
|
@@ -225,6 +225,7 @@ const noOpUIContext: ExtensionUIContext = {
|
|
|
225
225
|
setTitle: () => {},
|
|
226
226
|
custom: async () => undefined as never,
|
|
227
227
|
setEditorText: () => {},
|
|
228
|
+
pasteToEditor: () => {},
|
|
228
229
|
getEditorText: () => "",
|
|
229
230
|
editor: async () => undefined,
|
|
230
231
|
get theme() {
|
|
@@ -1436,6 +1437,9 @@ export class AgentSession {
|
|
|
1436
1437
|
const success = await this.switchSession(sessionPath);
|
|
1437
1438
|
return { cancelled: !success };
|
|
1438
1439
|
},
|
|
1440
|
+
reload: async () => {
|
|
1441
|
+
await this.reload();
|
|
1442
|
+
},
|
|
1439
1443
|
getSystemPrompt: () => this.systemPrompt,
|
|
1440
1444
|
};
|
|
1441
1445
|
}
|
|
@@ -3334,6 +3338,18 @@ Be thorough - include exact file paths, function names, error messages, and tech
|
|
|
3334
3338
|
// Session Management
|
|
3335
3339
|
// =========================================================================
|
|
3336
3340
|
|
|
3341
|
+
/**
|
|
3342
|
+
* Reload the current session from disk.
|
|
3343
|
+
*
|
|
3344
|
+
* Intended for extension commands and headless modes to re-read the current session
|
|
3345
|
+
* file and re-emit session_switch hooks.
|
|
3346
|
+
*/
|
|
3347
|
+
async reload(): Promise<void> {
|
|
3348
|
+
const sessionFile = this.sessionFile;
|
|
3349
|
+
if (!sessionFile) return;
|
|
3350
|
+
await this.switchSession(sessionFile);
|
|
3351
|
+
}
|
|
3352
|
+
|
|
3337
3353
|
/**
|
|
3338
3354
|
* Switch to a different session file.
|
|
3339
3355
|
* Aborts current operation, loads messages, restores model/thinking.
|
package/src/session/messages.ts
CHANGED
|
@@ -114,6 +114,10 @@ export interface FileMentionMessage {
|
|
|
114
114
|
path: string;
|
|
115
115
|
content: string;
|
|
116
116
|
lineCount?: number;
|
|
117
|
+
/** File size in bytes, if known. */
|
|
118
|
+
byteSize?: number;
|
|
119
|
+
/** Why the file contents were omitted from auto-read. */
|
|
120
|
+
skippedReason?: "tooLarge";
|
|
117
121
|
image?: ImageContent;
|
|
118
122
|
}>;
|
|
119
123
|
timestamp: number;
|
package/src/system-prompt.ts
CHANGED
|
@@ -473,6 +473,11 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
473
473
|
const systemPromptCustomization = await loadSystemPromptFiles({ cwd: resolvedCwd });
|
|
474
474
|
|
|
475
475
|
const now = new Date();
|
|
476
|
+
const date = now.toLocaleDateString("en-CA", {
|
|
477
|
+
year: "numeric",
|
|
478
|
+
month: "2-digit",
|
|
479
|
+
day: "2-digit",
|
|
480
|
+
});
|
|
476
481
|
const dateTime = now.toLocaleString("en-US", {
|
|
477
482
|
weekday: "long",
|
|
478
483
|
year: "numeric",
|
|
@@ -529,6 +534,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
529
534
|
skills: filteredSkills,
|
|
530
535
|
preloadedSkills: preloadedSkillContents,
|
|
531
536
|
rules: rules ?? [],
|
|
537
|
+
date,
|
|
532
538
|
dateTime,
|
|
533
539
|
cwd: resolvedCwd,
|
|
534
540
|
});
|
|
@@ -544,6 +550,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
544
550
|
skills: filteredSkills,
|
|
545
551
|
preloadedSkills: preloadedSkillContents,
|
|
546
552
|
rules: rules ?? [],
|
|
553
|
+
date,
|
|
547
554
|
dateTime,
|
|
548
555
|
cwd: resolvedCwd,
|
|
549
556
|
appendSystemPrompt: resolvedAppendPrompt ?? "",
|
package/src/tools/truncate.ts
CHANGED
|
@@ -53,8 +53,10 @@ export function formatSize(bytes: number): string {
|
|
|
53
53
|
return `${bytes}B`;
|
|
54
54
|
} else if (bytes < 1024 * 1024) {
|
|
55
55
|
return `${(bytes / 1024).toFixed(1)}KB`;
|
|
56
|
-
} else {
|
|
56
|
+
} else if (bytes < 1024 * 1024 * 1024) {
|
|
57
57
|
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
58
|
+
} else {
|
|
59
|
+
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)}GB`;
|
|
58
60
|
}
|
|
59
61
|
}
|
|
60
62
|
|
|
@@ -22,6 +22,11 @@ const TRAILING_PUNCTUATION_REGEX = /[)\]}>.,;:!?"'`]+$/;
|
|
|
22
22
|
const MENTION_BOUNDARY_REGEX = /[\s([{<"'`]/;
|
|
23
23
|
const DEFAULT_DIR_LIMIT = 500;
|
|
24
24
|
|
|
25
|
+
// Avoid OOM when users @mention very large files. Above these limits we skip
|
|
26
|
+
// auto-reading and only include the path in the message.
|
|
27
|
+
const MAX_AUTO_READ_TEXT_BYTES = 5 * 1024 * 1024; // 5MB
|
|
28
|
+
const MAX_AUTO_READ_IMAGE_BYTES = 25 * 1024 * 1024; // 25MB
|
|
29
|
+
|
|
25
30
|
function isMentionBoundary(text: string, index: number): boolean {
|
|
26
31
|
if (index === 0) return true;
|
|
27
32
|
return MENTION_BOUNDARY_REGEX.test(text[index - 1]);
|
|
@@ -183,6 +188,15 @@ export async function generateFileMentionMessages(
|
|
|
183
188
|
|
|
184
189
|
const mimeType = await detectSupportedImageMimeTypeFromFile(absolutePath);
|
|
185
190
|
if (mimeType) {
|
|
191
|
+
if (stat.size > MAX_AUTO_READ_IMAGE_BYTES) {
|
|
192
|
+
files.push({
|
|
193
|
+
path: filePath,
|
|
194
|
+
content: `(skipped auto-read: too large, ${formatSize(stat.size)})`,
|
|
195
|
+
byteSize: stat.size,
|
|
196
|
+
skippedReason: "tooLarge",
|
|
197
|
+
});
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
186
200
|
const buffer = await fs.readFile(absolutePath);
|
|
187
201
|
if (buffer.length === 0) {
|
|
188
202
|
continue;
|
|
@@ -210,6 +224,16 @@ export async function generateFileMentionMessages(
|
|
|
210
224
|
continue;
|
|
211
225
|
}
|
|
212
226
|
|
|
227
|
+
if (stat.size > MAX_AUTO_READ_TEXT_BYTES) {
|
|
228
|
+
files.push({
|
|
229
|
+
path: filePath,
|
|
230
|
+
content: `(skipped auto-read: too large, ${formatSize(stat.size)})`,
|
|
231
|
+
byteSize: stat.size,
|
|
232
|
+
skippedReason: "tooLarge",
|
|
233
|
+
});
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
|
|
213
237
|
const content = await Bun.file(absolutePath).text();
|
|
214
238
|
const { output, lineCount } = buildTextOutput(content);
|
|
215
239
|
files.push({ path: filePath, content: output, lineCount });
|
|
@@ -203,12 +203,24 @@ async function downloadTool(tool: ToolName, signal?: AbortSignal): Promise<strin
|
|
|
203
203
|
const tmp = await TempDir.create("@omp-tools-extract-");
|
|
204
204
|
|
|
205
205
|
try {
|
|
206
|
-
if (assetName.endsWith(".tar.gz")
|
|
206
|
+
if (!assetName.endsWith(".tar.gz") && !assetName.endsWith(".zip")) {
|
|
207
|
+
throw new Error(`Unsupported archive format: ${assetName}`);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
try {
|
|
207
211
|
const archive = new Bun.Archive(await Bun.file(archivePath).arrayBuffer());
|
|
208
212
|
const files = await archive.files();
|
|
213
|
+
const extractRoot = path.resolve(tmp.path());
|
|
214
|
+
|
|
209
215
|
for (const [filePath, file] of files) {
|
|
210
|
-
|
|
216
|
+
const outputPath = path.resolve(extractRoot, filePath);
|
|
217
|
+
if (!outputPath.startsWith(extractRoot + path.sep)) {
|
|
218
|
+
throw new Error(`Archive entry escapes extraction dir: ${filePath}`);
|
|
219
|
+
}
|
|
220
|
+
await Bun.write(outputPath, file);
|
|
211
221
|
}
|
|
222
|
+
} catch (err) {
|
|
223
|
+
throw new Error(`Failed to extract ${assetName}: ${err instanceof Error ? err.message : String(err)}`);
|
|
212
224
|
}
|
|
213
225
|
|
|
214
226
|
// Find the binary in extracted files
|