@oh-my-pi/pi-coding-agent 14.9.9 → 15.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +82 -0
- package/package.json +7 -7
- package/scripts/format-prompts.ts +1 -1
- package/src/cli/args.ts +2 -2
- package/src/cli.ts +1 -0
- package/src/commands/acp.ts +24 -0
- package/src/commands/launch.ts +6 -4
- package/src/commit/agentic/prompts/system.md +1 -1
- package/src/config/model-resolver.ts +30 -0
- package/src/config/settings-schema.ts +31 -0
- package/src/edit/index.ts +22 -1
- package/src/edit/modes/patch.ts +10 -0
- package/src/edit/modes/replace.ts +3 -0
- package/src/edit/renderer.ts +10 -0
- package/src/eval/js/context-manager.ts +1 -1
- package/src/eval/js/shared/rewrite-imports.ts +120 -48
- package/src/eval/js/shared/runtime.ts +31 -4
- package/src/eval/js/tool-bridge.ts +43 -21
- package/src/extensibility/extensions/runner.ts +54 -1
- package/src/extensibility/extensions/types.ts +11 -0
- package/src/extensibility/skills.ts +33 -1
- package/src/internal-urls/docs-index.generated.ts +6 -6
- package/src/internal-urls/index.ts +1 -0
- package/src/internal-urls/issue-pr-protocol.ts +577 -0
- package/src/internal-urls/router.ts +6 -3
- package/src/internal-urls/types.ts +22 -1
- package/src/main.ts +13 -9
- package/src/modes/acp/acp-agent.ts +361 -54
- package/src/modes/acp/acp-client-bridge.ts +152 -0
- package/src/modes/acp/acp-event-mapper.ts +180 -15
- package/src/modes/acp/terminal-auth.ts +37 -0
- package/src/modes/components/read-tool-group.ts +29 -1
- package/src/modes/controllers/command-controller.ts +14 -6
- package/src/modes/controllers/event-controller.ts +24 -11
- package/src/modes/controllers/extension-ui-controller.ts +8 -2
- package/src/modes/controllers/input-controller.ts +72 -39
- package/src/modes/interactive-mode.ts +71 -7
- package/src/modes/rpc/rpc-mode.ts +17 -2
- package/src/modes/types.ts +6 -2
- package/src/modes/utils/ui-helpers.ts +15 -3
- package/src/prompts/agents/designer.md +5 -5
- package/src/prompts/agents/explore.md +7 -7
- package/src/prompts/agents/init.md +9 -9
- package/src/prompts/agents/librarian.md +14 -14
- package/src/prompts/agents/plan.md +4 -4
- package/src/prompts/agents/reviewer.md +5 -5
- package/src/prompts/agents/task.md +10 -10
- package/src/prompts/commands/orchestrate.md +2 -2
- package/src/prompts/compaction/branch-summary.md +3 -3
- package/src/prompts/compaction/compaction-short-summary.md +7 -7
- package/src/prompts/compaction/compaction-summary-context.md +1 -1
- package/src/prompts/compaction/compaction-summary.md +5 -5
- package/src/prompts/compaction/compaction-turn-prefix.md +3 -3
- package/src/prompts/compaction/compaction-update-summary.md +11 -11
- package/src/prompts/memories/consolidation.md +2 -2
- package/src/prompts/memories/read-path.md +1 -1
- package/src/prompts/memories/stage_one_input.md +1 -1
- package/src/prompts/memories/stage_one_system.md +5 -5
- package/src/prompts/review-request.md +4 -4
- package/src/prompts/system/agent-creation-architect.md +17 -17
- package/src/prompts/system/agent-creation-user.md +2 -2
- package/src/prompts/system/commit-message-system.md +2 -2
- package/src/prompts/system/custom-system-prompt.md +2 -2
- package/src/prompts/system/eager-todo.md +6 -6
- package/src/prompts/system/handoff-document.md +1 -1
- package/src/prompts/system/plan-mode-active.md +22 -21
- package/src/prompts/system/plan-mode-approved.md +4 -4
- package/src/prompts/system/plan-mode-compact-instructions.md +16 -0
- package/src/prompts/system/plan-mode-reference.md +2 -2
- package/src/prompts/system/plan-mode-subagent.md +8 -8
- package/src/prompts/system/plan-mode-tool-decision-reminder.md +2 -2
- package/src/prompts/system/project-prompt.md +4 -4
- package/src/prompts/system/subagent-system-prompt.md +7 -7
- package/src/prompts/system/subagent-yield-reminder.md +4 -4
- package/src/prompts/system/system-prompt.md +72 -71
- package/src/prompts/system/ttsr-interrupt.md +1 -1
- package/src/prompts/tools/apply-patch.md +1 -1
- package/src/prompts/tools/ast-edit.md +3 -3
- package/src/prompts/tools/ast-grep.md +3 -3
- package/src/prompts/tools/browser.md +3 -3
- package/src/prompts/tools/checkpoint.md +3 -3
- package/src/prompts/tools/exit-plan-mode.md +2 -2
- package/src/prompts/tools/find.md +3 -3
- package/src/prompts/tools/github.md +2 -5
- package/src/prompts/tools/hashline.md +6 -6
- package/src/prompts/tools/image-gen.md +3 -3
- package/src/prompts/tools/irc.md +1 -1
- package/src/prompts/tools/lsp.md +2 -2
- package/src/prompts/tools/patch.md +6 -6
- package/src/prompts/tools/read.md +7 -7
- package/src/prompts/tools/replace.md +5 -5
- package/src/prompts/tools/retain.md +1 -1
- package/src/prompts/tools/rewind.md +2 -2
- package/src/prompts/tools/search.md +2 -2
- package/src/prompts/tools/ssh.md +2 -2
- package/src/prompts/tools/task.md +12 -6
- package/src/prompts/tools/web-search.md +2 -2
- package/src/prompts/tools/write.md +3 -3
- package/src/sdk.ts +69 -12
- package/src/session/agent-session.ts +231 -22
- package/src/session/client-bridge.ts +81 -0
- package/src/session/compaction/errors.ts +31 -0
- package/src/session/compaction/index.ts +1 -0
- package/src/slash-commands/acp-builtins.ts +46 -0
- package/src/slash-commands/builtin-registry.ts +699 -116
- package/src/slash-commands/helpers/context-report.ts +39 -0
- package/src/slash-commands/helpers/format.ts +23 -0
- package/src/slash-commands/helpers/marketplace-manager.ts +25 -0
- package/src/slash-commands/helpers/mcp.ts +532 -0
- package/src/slash-commands/helpers/parse.ts +85 -0
- package/src/slash-commands/helpers/ssh.ts +193 -0
- package/src/slash-commands/helpers/todo.ts +279 -0
- package/src/slash-commands/helpers/usage-report.ts +91 -0
- package/src/slash-commands/types.ts +126 -0
- package/src/task/executor.ts +10 -3
- package/src/task/index.ts +17 -1
- package/src/task/render.ts +6 -3
- package/src/tools/bash.ts +176 -2
- package/src/tools/conflict-detect.ts +6 -6
- package/src/tools/fetch.ts +15 -4
- package/src/tools/find.ts +19 -1
- package/src/tools/gh-renderer.ts +0 -12
- package/src/tools/gh.ts +682 -176
- package/src/tools/github-cache.ts +548 -0
- package/src/tools/index.ts +3 -0
- package/src/tools/read.ts +110 -27
- package/src/tools/write.ts +23 -1
- package/src/tui/code-cell.ts +70 -2
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { parse as babelParse } from "@babel/parser";
|
|
2
2
|
|
|
3
|
-
// Static ESM `import` declarations are not valid inside vm.runInContext (script-mode parsing)
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
3
|
+
// Static ESM `import` declarations are not valid inside vm.runInContext (script-mode parsing),
|
|
4
|
+
// and dynamic `import(...)` would otherwise resolve specifiers against the worker module's URL
|
|
5
|
+
// instead of the session cwd. We rewrite both forms so they route through the worker-injected
|
|
6
|
+
// `__omp_import__` helper, which resolves the specifier against the active session cwd. A real
|
|
7
|
+
// parser keeps imports embedded in string literals, template literals, or comments intact.
|
|
7
8
|
|
|
8
9
|
type BabelImportDeclaration = {
|
|
9
10
|
type: "ImportDeclaration";
|
|
@@ -25,26 +26,78 @@ type BabelLexicalDecl =
|
|
|
25
26
|
| { type: "VariableDeclaration"; kind: "const" | "let" | "var"; start: number; end: number }
|
|
26
27
|
| { type: "ClassDeclaration"; start: number; end: number; id: { start: number; end: number; name: string } | null };
|
|
27
28
|
|
|
28
|
-
|
|
29
|
+
type BabelExpressionStatement = {
|
|
30
|
+
type: "ExpressionStatement";
|
|
31
|
+
start: number;
|
|
32
|
+
end: number;
|
|
33
|
+
expression?: { type?: string };
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
type BabelProgramNode = BabelImportDeclaration | BabelLexicalDecl | BabelExpressionStatement | { type: string };
|
|
37
|
+
|
|
38
|
+
type BabelNode = { type: string; start: number; end: number; [key: string]: unknown };
|
|
39
|
+
|
|
40
|
+
function parseProgram(code: string): { program: { body: ReadonlyArray<BabelProgramNode> } } | null {
|
|
41
|
+
try {
|
|
42
|
+
return babelParse(code, {
|
|
43
|
+
sourceType: "module",
|
|
44
|
+
allowAwaitOutsideFunction: true,
|
|
45
|
+
allowReturnOutsideFunction: true,
|
|
46
|
+
allowImportExportEverywhere: true,
|
|
47
|
+
allowNewTargetOutsideFunction: true,
|
|
48
|
+
allowSuperOutsideMethod: true,
|
|
49
|
+
allowUndeclaredExports: true,
|
|
50
|
+
errorRecovery: true,
|
|
51
|
+
}) as unknown as { program: { body: ReadonlyArray<BabelProgramNode> } };
|
|
52
|
+
} catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function buildOmpImportCall(sourceLiteral: string, optionsLiteral: string | undefined): string {
|
|
29
58
|
// Route every static import through the worker-injected `__omp_import__` helper so the
|
|
30
59
|
// specifier resolves against the session cwd (and `with`-attribute imports keep working).
|
|
31
|
-
return
|
|
60
|
+
return optionsLiteral ? `__omp_import__(${sourceLiteral}, ${optionsLiteral})` : `__omp_import__(${sourceLiteral})`;
|
|
32
61
|
}
|
|
33
62
|
|
|
34
|
-
|
|
63
|
+
// Walks every node in `root`, depth-first, invoking `visit` on each one. Skips Babel's
|
|
64
|
+
// non-AST bookkeeping fields so we don't recurse into source locations or comment arrays.
|
|
65
|
+
function walkNodes(root: unknown, visit: (node: BabelNode) => void): void {
|
|
66
|
+
const stack: unknown[] = [root];
|
|
67
|
+
while (stack.length > 0) {
|
|
68
|
+
const current = stack.pop();
|
|
69
|
+
if (!current || typeof current !== "object") continue;
|
|
70
|
+
if (Array.isArray(current)) {
|
|
71
|
+
for (let i = current.length - 1; i >= 0; i--) stack.push(current[i]);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
const node = current as Record<string, unknown>;
|
|
75
|
+
if (typeof node.type === "string") visit(node as unknown as BabelNode);
|
|
76
|
+
for (const key in node) {
|
|
77
|
+
if (key === "loc" || key === "extra" || key === "range") continue;
|
|
78
|
+
if (key === "leadingComments" || key === "trailingComments" || key === "innerComments") continue;
|
|
79
|
+
const value = node[key];
|
|
80
|
+
if (value && typeof value === "object") stack.push(value);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function buildOptionsLiteral(node: BabelImportDeclaration): string | undefined {
|
|
35
86
|
const attrs = node.attributes;
|
|
36
87
|
if (!attrs || attrs.length === 0) return undefined;
|
|
37
88
|
const pairs = attrs.map(attr => {
|
|
38
89
|
const key = attr.key.type === "Identifier" ? attr.key.name : JSON.stringify(attr.key.value);
|
|
39
90
|
return `${key}: ${JSON.stringify(attr.value.value)}`;
|
|
40
91
|
});
|
|
41
|
-
|
|
92
|
+
// Native dynamic import takes options as `{ with: { ... } }`. `__omp_import__` forwards the
|
|
93
|
+
// options bag verbatim, so we wrap the attribute pairs accordingly.
|
|
94
|
+
return `{ with: { ${pairs.join(", ")} } }`;
|
|
42
95
|
}
|
|
43
96
|
|
|
44
97
|
function rewriteImportNode(node: BabelImportDeclaration): string {
|
|
45
98
|
const sourceLiteral = JSON.stringify(node.source.value);
|
|
46
|
-
const
|
|
47
|
-
const importCall =
|
|
99
|
+
const optionsLiteral = buildOptionsLiteral(node);
|
|
100
|
+
const importCall = buildOmpImportCall(sourceLiteral, optionsLiteral);
|
|
48
101
|
|
|
49
102
|
let defaultName: string | undefined;
|
|
50
103
|
let namespaceName: string | undefined;
|
|
@@ -73,37 +126,43 @@ function rewriteImportNode(node: BabelImportDeclaration): string {
|
|
|
73
126
|
return `await ${importCall};`;
|
|
74
127
|
}
|
|
75
128
|
|
|
76
|
-
export function
|
|
129
|
+
export function rewriteImports(code: string): string {
|
|
77
130
|
if (!code.includes("import")) return code;
|
|
78
131
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
ast = babelParse(code, {
|
|
82
|
-
sourceType: "module",
|
|
83
|
-
allowAwaitOutsideFunction: true,
|
|
84
|
-
allowReturnOutsideFunction: true,
|
|
85
|
-
allowImportExportEverywhere: true,
|
|
86
|
-
allowNewTargetOutsideFunction: true,
|
|
87
|
-
allowSuperOutsideMethod: true,
|
|
88
|
-
allowUndeclaredExports: true,
|
|
89
|
-
errorRecovery: true,
|
|
90
|
-
}) as unknown as typeof ast;
|
|
91
|
-
} catch {
|
|
132
|
+
const ast = parseProgram(code);
|
|
133
|
+
if (!ast) {
|
|
92
134
|
// Parser bailed entirely — let the VM surface the real syntax error.
|
|
93
135
|
return code;
|
|
94
136
|
}
|
|
95
137
|
|
|
96
|
-
|
|
138
|
+
type Edit = { start: number; end: number; text: string };
|
|
139
|
+
const edits: Edit[] = [];
|
|
140
|
+
|
|
141
|
+
// Top-level static `import` declarations become `await __omp_import__(...)` calls.
|
|
97
142
|
for (const node of ast.program.body) {
|
|
98
|
-
if (node.type
|
|
143
|
+
if (node.type !== "ImportDeclaration") continue;
|
|
144
|
+
const decl = node as unknown as BabelImportDeclaration;
|
|
145
|
+
edits.push({ start: decl.start, end: decl.end, text: rewriteImportNode(decl) });
|
|
99
146
|
}
|
|
100
|
-
|
|
147
|
+
|
|
148
|
+
// Dynamic `import(...)` expressions (anywhere) get their callee swapped for `__omp_import__`
|
|
149
|
+
// so the specifier resolves against the session cwd instead of the worker module's URL.
|
|
150
|
+
walkNodes(ast, node => {
|
|
151
|
+
if (node.type !== "CallExpression") return;
|
|
152
|
+
const call = node as unknown as { callee?: { type?: string; start?: number; end?: number } };
|
|
153
|
+
const callee = call.callee;
|
|
154
|
+
if (!callee || callee.type !== "Import" || typeof callee.start !== "number" || typeof callee.end !== "number")
|
|
155
|
+
return;
|
|
156
|
+
edits.push({ start: callee.start, end: callee.end, text: "__omp_import__" });
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
if (edits.length === 0) return code;
|
|
101
160
|
|
|
102
161
|
// Splice from the back so earlier offsets stay valid.
|
|
103
|
-
|
|
162
|
+
edits.sort((a, b) => b.start - a.start);
|
|
104
163
|
let result = code;
|
|
105
|
-
for (const
|
|
106
|
-
result = result.slice(0,
|
|
164
|
+
for (const edit of edits) {
|
|
165
|
+
result = result.slice(0, edit.start) + edit.text + result.slice(edit.end);
|
|
107
166
|
}
|
|
108
167
|
return result;
|
|
109
168
|
}
|
|
@@ -124,19 +183,8 @@ export function rewriteStaticImports(code: string): string {
|
|
|
124
183
|
export function demoteTopLevelLexicals(code: string): string {
|
|
125
184
|
if (!/\b(?:const|let|class)\b/.test(code)) return code;
|
|
126
185
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
ast = babelParse(code, {
|
|
130
|
-
sourceType: "module",
|
|
131
|
-
allowAwaitOutsideFunction: true,
|
|
132
|
-
allowReturnOutsideFunction: true,
|
|
133
|
-
allowImportExportEverywhere: true,
|
|
134
|
-
allowNewTargetOutsideFunction: true,
|
|
135
|
-
allowSuperOutsideMethod: true,
|
|
136
|
-
allowUndeclaredExports: true,
|
|
137
|
-
errorRecovery: true,
|
|
138
|
-
}) as unknown as typeof ast;
|
|
139
|
-
} catch {
|
|
186
|
+
const ast = parseProgram(code);
|
|
187
|
+
if (!ast) {
|
|
140
188
|
return code;
|
|
141
189
|
}
|
|
142
190
|
|
|
@@ -172,6 +220,24 @@ export function demoteTopLevelLexicals(code: string): string {
|
|
|
172
220
|
return result;
|
|
173
221
|
}
|
|
174
222
|
|
|
223
|
+
function returnFinalExpression(code: string): { source: string; returned: boolean } {
|
|
224
|
+
const ast = parseProgram(code);
|
|
225
|
+
const body = ast?.program.body;
|
|
226
|
+
if (!body) return { source: code, returned: false };
|
|
227
|
+
let lastIndex = body.length - 1;
|
|
228
|
+
while (lastIndex >= 0 && body[lastIndex]?.type === "EmptyStatement") lastIndex--;
|
|
229
|
+
const last = lastIndex >= 0 ? body[lastIndex] : undefined;
|
|
230
|
+
if (last?.type !== "ExpressionStatement") return { source: code, returned: false };
|
|
231
|
+
|
|
232
|
+
const expression = last as BabelExpressionStatement;
|
|
233
|
+
const prefix = code.slice(0, expression.start);
|
|
234
|
+
const statement = code.slice(expression.start, expression.end);
|
|
235
|
+
const suffix = code.slice(expression.end);
|
|
236
|
+
const semicolonMatch = statement.match(/;\s*$/);
|
|
237
|
+
const trimmedStatement = semicolonMatch ? statement.slice(0, semicolonMatch.index) : statement;
|
|
238
|
+
return { source: `${prefix}__omp_set_final_expr__((${trimmedStatement}));${suffix}`, returned: true };
|
|
239
|
+
}
|
|
240
|
+
|
|
175
241
|
/**
|
|
176
242
|
* Strip TypeScript syntax (type annotations, `interface`, `as`, `satisfies`, generics in
|
|
177
243
|
* call expressions, etc.) before the import/lexical rewriters parse the code. We use Bun's
|
|
@@ -198,14 +264,20 @@ export function stripTypeScript(code: string): string {
|
|
|
198
264
|
const LOOKS_LIKE_TS =
|
|
199
265
|
/(?:\binterface\s+\w|\btype\s+\w+\s*=|\b(?:as|satisfies)\s+(?:[A-Z]|\bconst\b)|:\s*(?:string|number|boolean|any|unknown|void|never|object|[A-Z]\w*)\b|<\s*[A-Z]\w*\s*[,>])/;
|
|
200
266
|
|
|
201
|
-
export function wrapCode(code: string): { source: string; asyncWrapped: boolean } {
|
|
202
|
-
const
|
|
203
|
-
const
|
|
267
|
+
export function wrapCode(code: string): { source: string; asyncWrapped: boolean; finalExpressionReturned: boolean } {
|
|
268
|
+
const stripped = stripTypeScript(code);
|
|
269
|
+
const finalExpression = returnFinalExpression(stripped);
|
|
270
|
+
const rewritten = {
|
|
271
|
+
source: demoteTopLevelLexicals(rewriteImports(finalExpression.source)),
|
|
272
|
+
returned: finalExpression.returned,
|
|
273
|
+
};
|
|
274
|
+
const needsAsyncWrapper = /\bawait\b|\breturn\b/.test(rewritten.source);
|
|
204
275
|
if (!needsAsyncWrapper) {
|
|
205
|
-
return { source: rewritten, asyncWrapped: false };
|
|
276
|
+
return { source: rewritten.source, asyncWrapped: false, finalExpressionReturned: rewritten.returned };
|
|
206
277
|
}
|
|
207
278
|
return {
|
|
208
|
-
source: `(async () => {\n${rewritten}\n})()`,
|
|
279
|
+
source: `(async () => {\n${rewritten.source}\n})()`,
|
|
209
280
|
asyncWrapped: true,
|
|
281
|
+
finalExpressionReturned: rewritten.returned,
|
|
210
282
|
};
|
|
211
283
|
}
|
|
@@ -4,6 +4,8 @@ import * as path from "node:path";
|
|
|
4
4
|
import { pathToFileURL } from "node:url";
|
|
5
5
|
import * as util from "node:util";
|
|
6
6
|
|
|
7
|
+
import { logger } from "@oh-my-pi/pi-utils";
|
|
8
|
+
|
|
7
9
|
import { ToolError } from "../../../tools/tool-errors";
|
|
8
10
|
import { createHelpers, type HelperBundle } from "./helpers";
|
|
9
11
|
import { awaitMaybePromise, indirectEval } from "./indirect-eval";
|
|
@@ -48,6 +50,8 @@ export class JsRuntime {
|
|
|
48
50
|
readonly sessionId: string;
|
|
49
51
|
#env: Map<string, string>;
|
|
50
52
|
#getHooks: () => RuntimeHooks | null;
|
|
53
|
+
#finalExpressionSet = false;
|
|
54
|
+
#finalExpressionValue: unknown;
|
|
51
55
|
|
|
52
56
|
constructor(opts: RuntimeOptions) {
|
|
53
57
|
this.#cwd = opts.initialCwd;
|
|
@@ -82,8 +86,20 @@ export class JsRuntime {
|
|
|
82
86
|
}
|
|
83
87
|
|
|
84
88
|
async run(code: string, filename?: string): Promise<unknown> {
|
|
89
|
+
this.#finalExpressionSet = false;
|
|
90
|
+
this.#finalExpressionValue = undefined;
|
|
85
91
|
const wrapped = wrapCode(code);
|
|
86
92
|
const value = indirectEval(wrapped.source, filename);
|
|
93
|
+
if (wrapped.finalExpressionReturned) {
|
|
94
|
+
const awaited = await awaitMaybePromise(value);
|
|
95
|
+
if (this.#finalExpressionSet) {
|
|
96
|
+
const finalValue = this.#finalExpressionValue;
|
|
97
|
+
this.#finalExpressionSet = false;
|
|
98
|
+
this.#finalExpressionValue = undefined;
|
|
99
|
+
return await awaitMaybePromise(finalValue);
|
|
100
|
+
}
|
|
101
|
+
return awaited;
|
|
102
|
+
}
|
|
87
103
|
return await awaitMaybePromise(value);
|
|
88
104
|
}
|
|
89
105
|
|
|
@@ -97,7 +113,14 @@ export class JsRuntime {
|
|
|
97
113
|
hooks.onDisplay({ type: "image", data: record.data, mimeType: record.mimeType });
|
|
98
114
|
return;
|
|
99
115
|
}
|
|
100
|
-
|
|
116
|
+
try {
|
|
117
|
+
hooks.onDisplay({ type: "json", data: structuredClone(value) });
|
|
118
|
+
} catch (err) {
|
|
119
|
+
logger.debug("js displayValue: value is not structured-cloneable, falling back to text", {
|
|
120
|
+
error: err instanceof Error ? err.message : String(err),
|
|
121
|
+
});
|
|
122
|
+
hooks.onText(`${Object.prototype.toString.call(value)}\n`);
|
|
123
|
+
}
|
|
101
124
|
return;
|
|
102
125
|
}
|
|
103
126
|
hooks.onText(`${String(value)}\n`);
|
|
@@ -112,9 +135,9 @@ export class JsRuntime {
|
|
|
112
135
|
if (!hooks) throw new ToolError("Tool calls are only valid inside an active run");
|
|
113
136
|
return await hooks.callTool(name, args);
|
|
114
137
|
},
|
|
115
|
-
__omp_import__: async (source: string,
|
|
138
|
+
__omp_import__: async (source: string, options?: ImportCallOptions) => {
|
|
116
139
|
const target = resolveImportSpecifier(this.#cwd, source);
|
|
117
|
-
return
|
|
140
|
+
return options !== undefined ? await import(target, options) : await import(target);
|
|
118
141
|
},
|
|
119
142
|
__omp_emit_status__: (op: string, data: Record<string, unknown> = {}) => {
|
|
120
143
|
const event: JsStatusEvent = { op, ...data };
|
|
@@ -126,6 +149,10 @@ export class JsRuntime {
|
|
|
126
149
|
this.#getHooks()?.onText(text.endsWith("\n") ? text : `${text}\n`);
|
|
127
150
|
},
|
|
128
151
|
__omp_display__: (value: unknown) => this.displayValue(value),
|
|
152
|
+
__omp_set_final_expr__: (value: unknown) => {
|
|
153
|
+
this.#finalExpressionSet = true;
|
|
154
|
+
this.#finalExpressionValue = value;
|
|
155
|
+
},
|
|
129
156
|
webcrypto: crypto,
|
|
130
157
|
// `process` is intentionally not overridden — user code gets the host worker's real
|
|
131
158
|
// `process` object. Subsetting it caused segfaults in workers that share state with
|
|
@@ -152,7 +179,7 @@ function buildRequire(cwd: string): NodeJS.Require {
|
|
|
152
179
|
}
|
|
153
180
|
|
|
154
181
|
/**
|
|
155
|
-
* Resolve an import specifier emitted by `
|
|
182
|
+
* Resolve an import specifier emitted by `rewriteImports` against the active session
|
|
156
183
|
* cwd. Relative paths (`./`, `../`, `/`) and bare specifiers (`pkg`, `@scope/pkg`) both go
|
|
157
184
|
* through `Bun.resolveSync` rooted at the cwd so user-pasted ESM behaves as if it lived in
|
|
158
185
|
* the project — not next to the worker module. URL-like specifiers (`file://`, `data:`,
|
|
@@ -17,7 +17,17 @@ type ToolValue =
|
|
|
17
17
|
text: string;
|
|
18
18
|
details?: unknown;
|
|
19
19
|
images?: Array<{ mimeType: string; data: string }>;
|
|
20
|
+
hasError?: boolean;
|
|
20
21
|
};
|
|
22
|
+
function toolResultHasError(result: AgentToolResult): boolean {
|
|
23
|
+
if ((result as { isError?: unknown }).isError === true) {
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
if (!(result.details && typeof result.details === "object")) {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
return (result.details as { isError?: unknown }).isError === true;
|
|
30
|
+
}
|
|
21
31
|
|
|
22
32
|
function getTool(session: ToolSession, name: string): AgentTool {
|
|
23
33
|
const tool = session.getToolByName?.(name);
|
|
@@ -38,7 +48,13 @@ function normalizeArgs(args: unknown): unknown {
|
|
|
38
48
|
return record;
|
|
39
49
|
}
|
|
40
50
|
|
|
41
|
-
function summarizeToolResult(
|
|
51
|
+
function summarizeToolResult(
|
|
52
|
+
name: string,
|
|
53
|
+
args: unknown,
|
|
54
|
+
result: AgentToolResult,
|
|
55
|
+
text: string,
|
|
56
|
+
hasError: boolean,
|
|
57
|
+
): JsStatusEvent {
|
|
42
58
|
const record = (args && typeof args === "object" ? (args as Record<string, unknown>) : {}) as Record<
|
|
43
59
|
string,
|
|
44
60
|
unknown
|
|
@@ -46,39 +62,41 @@ function summarizeToolResult(name: string, args: unknown, result: AgentToolResul
|
|
|
46
62
|
const details = (
|
|
47
63
|
result.details && typeof result.details === "object" ? (result.details as Record<string, unknown>) : {}
|
|
48
64
|
) as Record<string, unknown>;
|
|
65
|
+
const withError = (event: JsStatusEvent): JsStatusEvent =>
|
|
66
|
+
hasError ? { ...event, hasError: true, error: text.slice(0, 500) } : event;
|
|
49
67
|
|
|
50
68
|
switch (name) {
|
|
51
69
|
case "read":
|
|
52
|
-
return { op: "read", path: record.path, chars: text.length, preview: text.slice(0, 500) };
|
|
70
|
+
return withError({ op: "read", path: record.path, chars: text.length, preview: text.slice(0, 500) });
|
|
53
71
|
case "write":
|
|
54
|
-
return {
|
|
72
|
+
return withError({
|
|
55
73
|
op: "write",
|
|
56
74
|
path: record.path,
|
|
57
75
|
chars: typeof record.content === "string" ? record.content.length : 0,
|
|
58
|
-
};
|
|
76
|
+
});
|
|
59
77
|
case "grep":
|
|
60
|
-
return {
|
|
78
|
+
return withError({
|
|
61
79
|
op: "grep",
|
|
62
80
|
pattern: record.pattern,
|
|
63
81
|
path: record.path,
|
|
64
82
|
count: details.matchCount ?? undefined,
|
|
65
|
-
};
|
|
83
|
+
});
|
|
66
84
|
case "find":
|
|
67
|
-
return {
|
|
85
|
+
return withError({
|
|
68
86
|
op: "find",
|
|
69
87
|
pattern: record.pattern,
|
|
70
88
|
count: details.fileCount ?? undefined,
|
|
71
89
|
matches: Array.isArray(details.files) ? details.files.slice(0, 20) : undefined,
|
|
72
|
-
};
|
|
90
|
+
});
|
|
73
91
|
case "bash":
|
|
74
|
-
return {
|
|
92
|
+
return withError({
|
|
75
93
|
op: "run",
|
|
76
94
|
cmd: record.command,
|
|
77
95
|
code: typeof details.exitCode === "number" ? details.exitCode : undefined,
|
|
78
96
|
output: text.slice(0, 500),
|
|
79
|
-
};
|
|
97
|
+
});
|
|
80
98
|
default:
|
|
81
|
-
return { op: name, chars: text.length };
|
|
99
|
+
return withError({ op: name, chars: text.length });
|
|
82
100
|
}
|
|
83
101
|
}
|
|
84
102
|
|
|
@@ -97,21 +115,25 @@ export async function callSessionTool(name: string, args: unknown, options: Tool
|
|
|
97
115
|
content.type === "image" && typeof content.mimeType === "string" && typeof content.data === "string",
|
|
98
116
|
);
|
|
99
117
|
const text = textBlocks.map(block => block.text).join("");
|
|
100
|
-
|
|
101
|
-
|
|
118
|
+
const hasError = toolResultHasError(result);
|
|
119
|
+
options.emitStatus?.(summarizeToolResult(name, normalizedArgs, result, text, hasError));
|
|
120
|
+
if (result.details === undefined && imageBlocks.length === 0 && !hasError) {
|
|
102
121
|
return text;
|
|
103
122
|
}
|
|
104
|
-
|
|
123
|
+
const value: Exclude<ToolValue, string> = {
|
|
105
124
|
text,
|
|
106
125
|
details: result.details,
|
|
107
|
-
images:
|
|
108
|
-
imageBlocks.length > 0
|
|
109
|
-
? imageBlocks.map(block => ({
|
|
110
|
-
mimeType: block.mimeType,
|
|
111
|
-
data: block.data,
|
|
112
|
-
}))
|
|
113
|
-
: undefined,
|
|
114
126
|
};
|
|
127
|
+
if (imageBlocks.length > 0) {
|
|
128
|
+
value.images = imageBlocks.map(block => ({
|
|
129
|
+
mimeType: block.mimeType,
|
|
130
|
+
data: block.data,
|
|
131
|
+
}));
|
|
132
|
+
}
|
|
133
|
+
if (hasError) {
|
|
134
|
+
value.hasError = true;
|
|
135
|
+
}
|
|
136
|
+
return value;
|
|
115
137
|
} catch (error) {
|
|
116
138
|
options.emitStatus?.({
|
|
117
139
|
op: name,
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Extension runner - executes extensions and manages their lifecycle.
|
|
3
3
|
*/
|
|
4
4
|
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
|
5
|
-
import type { ImageContent, Model, ProviderResponseMetadata } from "@oh-my-pi/pi-ai";
|
|
5
|
+
import type { CredentialDisabledEvent, ImageContent, Model, ProviderResponseMetadata } from "@oh-my-pi/pi-ai";
|
|
6
6
|
import type { KeyId } from "@oh-my-pi/pi-tui";
|
|
7
7
|
import { logger } from "@oh-my-pi/pi-utils";
|
|
8
8
|
import type { ModelRegistry } from "../../config/model-registry";
|
|
@@ -69,6 +69,8 @@ export function __test_setExtensionHandlerTimeoutMs(timeoutMs: number): void {
|
|
|
69
69
|
|
|
70
70
|
const EXTENSION_HANDLER_TIMEOUT = Symbol("extensionHandlerTimeout");
|
|
71
71
|
|
|
72
|
+
const MAX_PENDING_CREDENTIAL_DISABLED = 32;
|
|
73
|
+
|
|
72
74
|
/**
|
|
73
75
|
* Events handled by the generic emit() method.
|
|
74
76
|
* Events with dedicated emitXxx() methods are excluded for stronger type safety.
|
|
@@ -185,6 +187,15 @@ export class ExtensionRunner {
|
|
|
185
187
|
#reloadHandler: () => Promise<void> = async () => {};
|
|
186
188
|
#shutdownHandler: ShutdownHandler = () => {};
|
|
187
189
|
#commandDiagnostics: Array<{ type: string; message: string; path: string }> = [];
|
|
190
|
+
#initialized = false;
|
|
191
|
+
/**
|
|
192
|
+
* Buffer for `credential_disabled` events received via {@link emitCredentialDisabled}
|
|
193
|
+
* before {@link initialize} has run. Drained through {@link emit} once initialize sets
|
|
194
|
+
* up the runtime context, so extension handlers see a populated UI/runtime context
|
|
195
|
+
* rather than the constructor's no-op default. Bounded at
|
|
196
|
+
* {@link MAX_PENDING_CREDENTIAL_DISABLED}; oldest entries are dropped under pressure.
|
|
197
|
+
*/
|
|
198
|
+
#pendingCredentialDisabled: CredentialDisabledEvent[] = [];
|
|
188
199
|
|
|
189
200
|
constructor(
|
|
190
201
|
private readonly extensions: Extension[],
|
|
@@ -237,6 +248,48 @@ export class ExtensionRunner {
|
|
|
237
248
|
}
|
|
238
249
|
|
|
239
250
|
this.#uiContext = uiContext ?? noOpUIContext;
|
|
251
|
+
this.#initialized = true;
|
|
252
|
+
|
|
253
|
+
// Drain events buffered by emitCredentialDisabled() before initialize ran. The
|
|
254
|
+
// spread adds the `type` discriminator — `event` is the pi-ai shape (no `type`).
|
|
255
|
+
// Deferred by one microtask so callers that register an onError listener
|
|
256
|
+
// synchronously after initialize() see handler errors routed through it.
|
|
257
|
+
const pending = this.#pendingCredentialDisabled.splice(0);
|
|
258
|
+
queueMicrotask(() => {
|
|
259
|
+
for (const event of pending) {
|
|
260
|
+
this.emit({ type: "credential_disabled", ...event }).catch((error: unknown) => {
|
|
261
|
+
logger.warn("credential_disabled handler threw during initialize flush", {
|
|
262
|
+
provider: event.provider,
|
|
263
|
+
error: error instanceof Error ? error.message : String(error),
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Forward a `credential_disabled` event from `AuthStorage` to extension handlers.
|
|
272
|
+
*
|
|
273
|
+
* If {@link initialize} has not yet run, the event is buffered and replayed once
|
|
274
|
+
* initialize wires the runtime/UI context. This matters because mode controllers
|
|
275
|
+
* (interactive, RPC, ACP, print, subagent) call `initialize()` AFTER `createAgentSession`
|
|
276
|
+
* returns, but `AuthStorage` can fire `credential_disabled` during startup model probes
|
|
277
|
+
* inside `createAgentSession()`. Without deferral, extension handlers would observe
|
|
278
|
+
* `hasUI=false`, an unset model, and no-op runtime actions on exactly the headline
|
|
279
|
+
* "OAuth invalid_grant during startup" path the event was designed to surface.
|
|
280
|
+
*
|
|
281
|
+
* Always returns; never throws. Errors from handlers are routed through
|
|
282
|
+
* {@link onError} via {@link emit}'s normal isolation.
|
|
283
|
+
*/
|
|
284
|
+
async emitCredentialDisabled(event: CredentialDisabledEvent): Promise<void> {
|
|
285
|
+
if (!this.#initialized) {
|
|
286
|
+
if (this.#pendingCredentialDisabled.length >= MAX_PENDING_CREDENTIAL_DISABLED) {
|
|
287
|
+
this.#pendingCredentialDisabled.shift();
|
|
288
|
+
}
|
|
289
|
+
this.#pendingCredentialDisabled.push(event);
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
await this.emit({ type: "credential_disabled", ...event });
|
|
240
293
|
}
|
|
241
294
|
|
|
242
295
|
getUIContext(): ExtensionUIContext {
|
|
@@ -624,6 +624,15 @@ export interface TodoReminderEvent {
|
|
|
624
624
|
maxAttempts: number;
|
|
625
625
|
}
|
|
626
626
|
|
|
627
|
+
/** Fired when AuthStorage automatically soft-disables a credential (e.g. OAuth `invalid_grant`). Not fired for user-initiated `remove()` or duplicate-credential dedup. */
|
|
628
|
+
export interface CredentialDisabledEvent {
|
|
629
|
+
type: "credential_disabled";
|
|
630
|
+
/** Provider id whose credential was disabled (e.g. "anthropic"). */
|
|
631
|
+
provider: string;
|
|
632
|
+
/** Verbatim error captured for forensics (truncated upstream). */
|
|
633
|
+
disabledCause: string;
|
|
634
|
+
}
|
|
635
|
+
|
|
627
636
|
// ============================================================================
|
|
628
637
|
// User Bash Events
|
|
629
638
|
// ============================================================================
|
|
@@ -831,6 +840,7 @@ export type ExtensionEvent =
|
|
|
831
840
|
| AutoRetryEndEvent
|
|
832
841
|
| TtsrTriggeredEvent
|
|
833
842
|
| TodoReminderEvent
|
|
843
|
+
| CredentialDisabledEvent
|
|
834
844
|
| UserBashEvent
|
|
835
845
|
| UserPythonEvent
|
|
836
846
|
| InputEvent
|
|
@@ -1012,6 +1022,7 @@ export interface ExtensionAPI {
|
|
|
1012
1022
|
on(event: "auto_retry_end", handler: ExtensionHandler<AutoRetryEndEvent>): void;
|
|
1013
1023
|
on(event: "ttsr_triggered", handler: ExtensionHandler<TtsrTriggeredEvent>): void;
|
|
1014
1024
|
on(event: "todo_reminder", handler: ExtensionHandler<TodoReminderEvent>): void;
|
|
1025
|
+
on(event: "credential_disabled", handler: ExtensionHandler<CredentialDisabledEvent>): void;
|
|
1015
1026
|
on(event: "input", handler: ExtensionHandler<InputEvent, InputEventResult>): void;
|
|
1016
1027
|
on(event: "tool_call", handler: ExtensionHandler<ToolCallEvent, ToolCallEventResult>): void;
|
|
1017
1028
|
on(event: "tool_result", handler: ExtensionHandler<ToolResultEvent, ToolResultEventResult>): void;
|
|
@@ -6,8 +6,8 @@ import type { SourceMeta } from "../capability/types";
|
|
|
6
6
|
import type { SkillsSettings } from "../config/settings";
|
|
7
7
|
import { type Skill as CapabilitySkill, loadCapability } from "../discovery";
|
|
8
8
|
import { compareSkillOrder, scanSkillsFromDir } from "../discovery/helpers";
|
|
9
|
+
import type { SkillPromptDetails } from "../session/messages";
|
|
9
10
|
import { expandTilde } from "../tools/path-utils";
|
|
10
|
-
|
|
11
11
|
export interface Skill {
|
|
12
12
|
name: string;
|
|
13
13
|
description: string;
|
|
@@ -270,3 +270,35 @@ export async function loadSkills(options: LoadSkillsOptions = {}): Promise<LoadS
|
|
|
270
270
|
warnings: [...(result.warnings ?? []).map(w => ({ skillPath: "", message: w })), ...collisionWarnings],
|
|
271
271
|
};
|
|
272
272
|
}
|
|
273
|
+
|
|
274
|
+
export interface BuiltSkillPromptMessage {
|
|
275
|
+
message: string;
|
|
276
|
+
details: SkillPromptDetails;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export function getSkillSlashCommandName(skill: Pick<Skill, "name">): string {
|
|
280
|
+
return `skill:${skill.name}`;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export async function buildSkillPromptMessage(
|
|
284
|
+
skill: Pick<Skill, "name" | "filePath">,
|
|
285
|
+
args: string,
|
|
286
|
+
): Promise<BuiltSkillPromptMessage> {
|
|
287
|
+
const content = await Bun.file(skill.filePath).text();
|
|
288
|
+
const body = content.replace(/^---\n[\s\S]*?\n---\n/, "").trim();
|
|
289
|
+
const metaLines = [`Skill: ${skill.filePath}`];
|
|
290
|
+
const trimmedArgs = args.trim();
|
|
291
|
+
if (trimmedArgs) {
|
|
292
|
+
metaLines.push(`User: ${trimmedArgs}`);
|
|
293
|
+
}
|
|
294
|
+
const message = `${body}\n\n---\n\n${metaLines.join("\n")}`;
|
|
295
|
+
return {
|
|
296
|
+
message,
|
|
297
|
+
details: {
|
|
298
|
+
name: skill.name,
|
|
299
|
+
path: skill.filePath,
|
|
300
|
+
args: trimmedArgs || undefined,
|
|
301
|
+
lineCount: body ? body.split("\n").length : 0,
|
|
302
|
+
},
|
|
303
|
+
};
|
|
304
|
+
}
|