@oh-my-pi/pi-coding-agent 6.1.0 → 6.7.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 +56 -0
- package/docs/sdk.md +1 -1
- package/package.json +5 -5
- package/scripts/generate-template.ts +6 -6
- package/src/cli/args.ts +3 -0
- package/src/core/agent-session.ts +39 -0
- package/src/core/bash-executor.ts +3 -3
- package/src/core/cursor/exec-bridge.ts +95 -88
- package/src/core/custom-commands/bundled/review/index.ts +142 -145
- package/src/core/custom-commands/bundled/wt/index.ts +68 -66
- package/src/core/custom-commands/loader.ts +4 -6
- package/src/core/custom-tools/index.ts +2 -2
- package/src/core/custom-tools/loader.ts +66 -61
- package/src/core/custom-tools/types.ts +4 -4
- package/src/core/custom-tools/wrapper.ts +61 -25
- package/src/core/event-bus.ts +19 -47
- package/src/core/extensions/index.ts +8 -4
- package/src/core/extensions/loader.ts +160 -120
- package/src/core/extensions/types.ts +4 -4
- package/src/core/extensions/wrapper.ts +149 -100
- package/src/core/hooks/index.ts +1 -1
- package/src/core/hooks/tool-wrapper.ts +96 -70
- package/src/core/hooks/types.ts +1 -2
- package/src/core/index.ts +1 -0
- package/src/core/mcp/index.ts +6 -2
- package/src/core/mcp/json-rpc.ts +88 -0
- package/src/core/mcp/loader.ts +22 -4
- package/src/core/mcp/manager.ts +202 -48
- package/src/core/mcp/tool-bridge.ts +143 -55
- package/src/core/mcp/tool-cache.ts +122 -0
- package/src/core/python-executor.ts +3 -9
- package/src/core/sdk.ts +33 -32
- package/src/core/session-manager.ts +30 -0
- package/src/core/settings-manager.ts +34 -1
- package/src/core/ssh/ssh-executor.ts +6 -84
- package/src/core/streaming-output.ts +107 -53
- package/src/core/tools/ask.ts +92 -93
- package/src/core/tools/bash.ts +103 -94
- package/src/core/tools/calculator.ts +41 -26
- package/src/core/tools/complete.ts +76 -66
- package/src/core/tools/context.ts +25 -25
- package/src/core/tools/exa/index.ts +1 -1
- package/src/core/tools/exa/mcp-client.ts +56 -101
- package/src/core/tools/find.ts +250 -253
- package/src/core/tools/git.ts +39 -33
- package/src/core/tools/grep.ts +440 -427
- package/src/core/tools/index.ts +62 -61
- package/src/core/tools/ls.ts +119 -114
- package/src/core/tools/lsp/clients/biome-client.ts +5 -7
- package/src/core/tools/lsp/clients/index.ts +4 -4
- package/src/core/tools/lsp/clients/lsp-linter-client.ts +5 -7
- package/src/core/tools/lsp/config.ts +2 -2
- package/src/core/tools/lsp/index.ts +824 -639
- package/src/core/tools/notebook.ts +121 -119
- package/src/core/tools/output.ts +163 -147
- package/src/core/tools/patch/applicator.ts +1100 -0
- package/src/core/tools/patch/diff.ts +362 -0
- package/src/core/tools/patch/fuzzy.ts +647 -0
- package/src/core/tools/patch/index.ts +430 -0
- package/src/core/tools/patch/normalize.ts +220 -0
- package/src/core/tools/patch/normative.ts +49 -0
- package/src/core/tools/patch/parser.ts +528 -0
- package/src/core/tools/patch/shared.ts +228 -0
- package/src/core/tools/patch/types.ts +244 -0
- package/src/core/tools/python.ts +139 -136
- package/src/core/tools/read.ts +237 -216
- package/src/core/tools/render-utils.ts +196 -77
- package/src/core/tools/renderers.ts +1 -1
- package/src/core/tools/ssh.ts +99 -80
- package/src/core/tools/task/executor.ts +11 -7
- package/src/core/tools/task/index.ts +352 -343
- package/src/core/tools/task/worker.ts +13 -23
- package/src/core/tools/todo-write.ts +74 -59
- package/src/core/tools/web-fetch.ts +54 -47
- package/src/core/tools/web-search/index.ts +27 -16
- package/src/core/tools/write.ts +89 -41
- package/src/core/ttsr.ts +106 -152
- package/src/core/voice.ts +49 -39
- package/src/index.ts +16 -12
- package/src/lib/worktree/index.ts +1 -9
- package/src/modes/interactive/components/diff.ts +15 -8
- package/src/modes/interactive/components/settings-defs.ts +24 -0
- package/src/modes/interactive/components/tool-execution.ts +34 -6
- package/src/modes/interactive/controllers/event-controller.ts +6 -19
- package/src/modes/interactive/controllers/input-controller.ts +1 -1
- package/src/modes/interactive/utils/ui-helpers.ts +5 -1
- package/src/modes/rpc/rpc-mode.ts +99 -81
- package/src/prompts/tools/patch.md +76 -0
- package/src/prompts/tools/read.md +1 -1
- package/src/prompts/tools/{edit.md → replace.md} +1 -0
- package/src/utils/shell.ts +0 -40
- package/src/core/tools/edit-diff.ts +0 -574
- package/src/core/tools/edit.ts +0 -326
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
1
|
+
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
2
2
|
import { StringEnum } from "@oh-my-pi/pi-ai";
|
|
3
3
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
4
4
|
import { Text } from "@oh-my-pi/pi-tui";
|
|
5
|
-
import { Type } from "@sinclair/typebox";
|
|
5
|
+
import { type Static, Type } from "@sinclair/typebox";
|
|
6
6
|
import type { Theme } from "../../modes/interactive/theme/theme";
|
|
7
7
|
import type { RenderResultOptions } from "../custom-tools/types";
|
|
8
8
|
import type { ToolSession } from "../sdk";
|
|
@@ -63,133 +63,135 @@ function splitIntoLines(content: string): string[] {
|
|
|
63
63
|
return content.split("\n").map((line, i, arr) => (i < arr.length - 1 ? `${line}\n` : line));
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
|
|
67
|
-
return {
|
|
68
|
-
name: "notebook",
|
|
69
|
-
label: "Notebook",
|
|
70
|
-
description:
|
|
71
|
-
"Completely replaces the contents of a specific cell in a Jupyter notebook (.ipynb file) with new source. Jupyter notebooks are interactive documents that combine code, text, and visualizations, commonly used for data analysis and scientific computing. The notebook_path parameter must be an absolute path, not a relative path. The cell_number is 0-indexed. Use edit_mode=insert to add a new cell at the index specified by cell_number. Use edit_mode=delete to delete the cell at the index specified by cell_number.",
|
|
72
|
-
parameters: notebookSchema,
|
|
73
|
-
execute: async (
|
|
74
|
-
_toolCallId: string,
|
|
75
|
-
{
|
|
76
|
-
action,
|
|
77
|
-
notebook_path,
|
|
78
|
-
cell_index,
|
|
79
|
-
content,
|
|
80
|
-
cell_type,
|
|
81
|
-
}: { action: string; notebook_path: string; cell_index: number; content?: string; cell_type?: string },
|
|
82
|
-
signal?: AbortSignal,
|
|
83
|
-
) => {
|
|
84
|
-
const absolutePath = resolveToCwd(notebook_path, session.cwd);
|
|
85
|
-
|
|
86
|
-
return untilAborted(signal, async () => {
|
|
87
|
-
// Check if file exists
|
|
88
|
-
const file = Bun.file(absolutePath);
|
|
89
|
-
if (!(await file.exists())) {
|
|
90
|
-
throw new Error(`Notebook not found: ${notebook_path}`);
|
|
91
|
-
}
|
|
66
|
+
type NotebookParams = Static<typeof notebookSchema>;
|
|
92
67
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
}
|
|
68
|
+
export class NotebookTool implements AgentTool<typeof notebookSchema, NotebookToolDetails> {
|
|
69
|
+
public readonly name = "notebook";
|
|
70
|
+
public readonly label = "Notebook";
|
|
71
|
+
public readonly description =
|
|
72
|
+
"Completely replaces the contents of a specific cell in a Jupyter notebook (.ipynb file) with new source. Jupyter notebooks are interactive documents that combine code, text, and visualizations, commonly used for data analysis and scientific computing. The notebook_path parameter must be an absolute path, not a relative path. The cell_number is 0-indexed. Use edit_mode=insert to add a new cell at the index specified by cell_number. Use edit_mode=delete to delete the cell at the index specified by cell_number.";
|
|
73
|
+
public readonly parameters = notebookSchema;
|
|
100
74
|
|
|
101
|
-
|
|
102
|
-
if (!notebook.cells || !Array.isArray(notebook.cells)) {
|
|
103
|
-
throw new Error(`Invalid notebook structure (missing cells array): ${notebook_path}`);
|
|
104
|
-
}
|
|
75
|
+
private readonly session: ToolSession;
|
|
105
76
|
|
|
106
|
-
|
|
77
|
+
constructor(session: ToolSession) {
|
|
78
|
+
this.session = session;
|
|
79
|
+
}
|
|
107
80
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
81
|
+
public async execute(
|
|
82
|
+
_toolCallId: string,
|
|
83
|
+
params: NotebookParams,
|
|
84
|
+
signal?: AbortSignal,
|
|
85
|
+
_onUpdate?: AgentToolUpdateCallback<NotebookToolDetails>,
|
|
86
|
+
_context?: AgentToolContext,
|
|
87
|
+
): Promise<AgentToolResult<NotebookToolDetails>> {
|
|
88
|
+
const { action, notebook_path, cell_index, content, cell_type } = params;
|
|
89
|
+
const absolutePath = resolveToCwd(notebook_path, this.session.cwd);
|
|
90
|
+
|
|
91
|
+
return untilAborted(signal, async () => {
|
|
92
|
+
// Check if file exists
|
|
93
|
+
const file = Bun.file(absolutePath);
|
|
94
|
+
if (!(await file.exists())) {
|
|
95
|
+
throw new Error(`Notebook not found: ${notebook_path}`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Read and parse notebook
|
|
99
|
+
let notebook: Notebook;
|
|
100
|
+
try {
|
|
101
|
+
notebook = await file.json();
|
|
102
|
+
} catch {
|
|
103
|
+
throw new Error(`Invalid JSON in notebook: ${notebook_path}`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Validate notebook structure
|
|
107
|
+
if (!notebook.cells || !Array.isArray(notebook.cells)) {
|
|
108
|
+
throw new Error(`Invalid notebook structure (missing cells array): ${notebook_path}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const cellCount = notebook.cells.length;
|
|
112
|
+
|
|
113
|
+
// Validate cell_index based on action
|
|
114
|
+
if (action === "insert") {
|
|
115
|
+
if (cell_index < 0 || cell_index > cellCount) {
|
|
116
|
+
throw new Error(`Cell index ${cell_index} out of range for insert (0-${cellCount}) in ${notebook_path}`);
|
|
119
117
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
throw new Error(`Content is required for ${action} action`);
|
|
118
|
+
} else {
|
|
119
|
+
if (cell_index < 0 || cell_index >= cellCount) {
|
|
120
|
+
throw new Error(`Cell index ${cell_index} out of range (0-${cellCount - 1}) in ${notebook_path}`);
|
|
124
121
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
}
|
|
158
|
-
case "delete": {
|
|
159
|
-
const removedCell = notebook.cells[cell_index];
|
|
160
|
-
finalCellType = removedCell.cell_type;
|
|
161
|
-
cellSource = removedCell.source;
|
|
162
|
-
notebook.cells.splice(cell_index, 1);
|
|
163
|
-
resultMessage = `Deleted cell ${cell_index} (${finalCellType})`;
|
|
164
|
-
break;
|
|
165
|
-
}
|
|
166
|
-
default: {
|
|
167
|
-
throw new Error(`Invalid action: ${action}`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Validate content for edit/insert
|
|
125
|
+
if ((action === "edit" || action === "insert") && content === undefined) {
|
|
126
|
+
throw new Error(`Content is required for ${action} action`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Perform the action
|
|
130
|
+
let resultMessage: string;
|
|
131
|
+
let finalCellType: string | undefined;
|
|
132
|
+
let cellSource: string[] | undefined;
|
|
133
|
+
|
|
134
|
+
switch (action) {
|
|
135
|
+
case "edit": {
|
|
136
|
+
const sourceLines = splitIntoLines(content!);
|
|
137
|
+
notebook.cells[cell_index].source = sourceLines;
|
|
138
|
+
finalCellType = notebook.cells[cell_index].cell_type;
|
|
139
|
+
cellSource = sourceLines;
|
|
140
|
+
resultMessage = `Replaced cell ${cell_index} (${finalCellType})`;
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
case "insert": {
|
|
144
|
+
const sourceLines = splitIntoLines(content!);
|
|
145
|
+
const newCellType = (cell_type as "code" | "markdown") || "code";
|
|
146
|
+
const newCell: NotebookCell = {
|
|
147
|
+
cell_type: newCellType,
|
|
148
|
+
source: sourceLines,
|
|
149
|
+
metadata: {},
|
|
150
|
+
};
|
|
151
|
+
if (newCellType === "code") {
|
|
152
|
+
newCell.execution_count = null;
|
|
153
|
+
newCell.outputs = [];
|
|
168
154
|
}
|
|
155
|
+
notebook.cells.splice(cell_index, 0, newCell);
|
|
156
|
+
finalCellType = newCellType;
|
|
157
|
+
cellSource = sourceLines;
|
|
158
|
+
resultMessage = `Inserted ${newCellType} cell at position ${cell_index}`;
|
|
159
|
+
break;
|
|
169
160
|
}
|
|
161
|
+
case "delete": {
|
|
162
|
+
const removedCell = notebook.cells[cell_index];
|
|
163
|
+
finalCellType = removedCell.cell_type;
|
|
164
|
+
cellSource = removedCell.source;
|
|
165
|
+
notebook.cells.splice(cell_index, 1);
|
|
166
|
+
resultMessage = `Deleted cell ${cell_index} (${finalCellType})`;
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
default: {
|
|
170
|
+
throw new Error(`Invalid action: ${action}`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
170
173
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
},
|
|
181
|
-
],
|
|
182
|
-
details: {
|
|
183
|
-
action: action as "edit" | "insert" | "delete",
|
|
184
|
-
cellIndex: cell_index,
|
|
185
|
-
cellType: finalCellType,
|
|
186
|
-
totalCells: newCellCount,
|
|
187
|
-
cellSource,
|
|
174
|
+
// Write back with single-space indentation
|
|
175
|
+
await Bun.write(absolutePath, JSON.stringify(notebook, null, 1));
|
|
176
|
+
|
|
177
|
+
const newCellCount = notebook.cells.length;
|
|
178
|
+
return {
|
|
179
|
+
content: [
|
|
180
|
+
{
|
|
181
|
+
type: "text",
|
|
182
|
+
text: `${resultMessage}. Notebook now has ${newCellCount} cells.`,
|
|
188
183
|
},
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
184
|
+
],
|
|
185
|
+
details: {
|
|
186
|
+
action: action as "edit" | "insert" | "delete",
|
|
187
|
+
cellIndex: cell_index,
|
|
188
|
+
cellType: finalCellType,
|
|
189
|
+
totalCells: newCellCount,
|
|
190
|
+
cellSource,
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
});
|
|
194
|
+
}
|
|
193
195
|
}
|
|
194
196
|
|
|
195
197
|
// =============================================================================
|
package/src/core/tools/output.ts
CHANGED
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
|
|
7
7
|
import * as fs from "node:fs";
|
|
8
8
|
import * as path from "node:path";
|
|
9
|
-
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
10
|
-
import { StringEnum
|
|
9
|
+
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
10
|
+
import { StringEnum } from "@oh-my-pi/pi-ai";
|
|
11
11
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
12
12
|
import { Text } from "@oh-my-pi/pi-tui";
|
|
13
13
|
import { Type } from "@sinclair/typebox";
|
|
@@ -232,166 +232,182 @@ function extractPreviewLines(content: string, maxLines: number): string[] {
|
|
|
232
232
|
return preview;
|
|
233
233
|
}
|
|
234
234
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
_toolCallId: string,
|
|
243
|
-
params: {
|
|
244
|
-
ids: string[];
|
|
245
|
-
format?: "raw" | "json" | "stripped";
|
|
246
|
-
query?: string;
|
|
247
|
-
offset?: number;
|
|
248
|
-
limit?: number;
|
|
249
|
-
},
|
|
250
|
-
): Promise<{ content: TextContent[]; details: OutputToolDetails }> => {
|
|
251
|
-
const sessionFile = session.getSessionFile();
|
|
252
|
-
|
|
253
|
-
if (!sessionFile) {
|
|
254
|
-
return {
|
|
255
|
-
content: [{ type: "text", text: "No session - output artifacts unavailable" }],
|
|
256
|
-
details: { outputs: [], notFound: params.ids },
|
|
257
|
-
};
|
|
258
|
-
}
|
|
235
|
+
type OutputParams = {
|
|
236
|
+
ids: string[];
|
|
237
|
+
format?: "raw" | "json" | "stripped";
|
|
238
|
+
query?: string;
|
|
239
|
+
offset?: number;
|
|
240
|
+
limit?: number;
|
|
241
|
+
};
|
|
259
242
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
243
|
+
/**
|
|
244
|
+
* Output tool for reading agent/task outputs by ID.
|
|
245
|
+
*
|
|
246
|
+
* Resolves IDs like "reviewer_0" to artifact paths in the current session.
|
|
247
|
+
*/
|
|
248
|
+
export class OutputTool implements AgentTool<typeof outputSchema, OutputToolDetails> {
|
|
249
|
+
public readonly name = "output";
|
|
250
|
+
public readonly label = "Output";
|
|
251
|
+
public readonly description: string;
|
|
252
|
+
public readonly parameters = outputSchema;
|
|
267
253
|
|
|
268
|
-
|
|
269
|
-
const notFound: string[] = [];
|
|
270
|
-
const outputContentById = new Map<string, string>();
|
|
271
|
-
const query = params.query?.trim();
|
|
272
|
-
const wantsQuery = query !== undefined && query.length > 0;
|
|
273
|
-
const format = params.format ?? (wantsQuery ? "json" : "raw");
|
|
254
|
+
private readonly session: ToolSession;
|
|
274
255
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
256
|
+
constructor(session: ToolSession) {
|
|
257
|
+
this.session = session;
|
|
258
|
+
this.description = renderPromptTemplate(outputDescription);
|
|
259
|
+
}
|
|
278
260
|
|
|
279
|
-
|
|
261
|
+
public async execute(
|
|
262
|
+
_toolCallId: string,
|
|
263
|
+
params: OutputParams,
|
|
264
|
+
_signal?: AbortSignal,
|
|
265
|
+
_onUpdate?: AgentToolUpdateCallback<OutputToolDetails>,
|
|
266
|
+
_context?: AgentToolContext,
|
|
267
|
+
): Promise<AgentToolResult<OutputToolDetails>> {
|
|
268
|
+
const sessionFile = this.session.getSessionFile();
|
|
280
269
|
|
|
281
|
-
|
|
282
|
-
|
|
270
|
+
if (!sessionFile) {
|
|
271
|
+
return {
|
|
272
|
+
content: [{ type: "text", text: "No session - output artifacts unavailable" }],
|
|
273
|
+
details: { outputs: [], notFound: params.ids },
|
|
274
|
+
};
|
|
275
|
+
}
|
|
283
276
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
}
|
|
277
|
+
const artifactsDir = getArtifactsDir(sessionFile);
|
|
278
|
+
if (!artifactsDir || !fs.existsSync(artifactsDir)) {
|
|
279
|
+
return {
|
|
280
|
+
content: [{ type: "text", text: "No artifacts directory found" }],
|
|
281
|
+
details: { outputs: [], notFound: params.ids },
|
|
282
|
+
};
|
|
283
|
+
}
|
|
288
284
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
let range: OutputRange | undefined;
|
|
296
|
-
|
|
297
|
-
if (wantsQuery && query) {
|
|
298
|
-
let jsonValue: unknown;
|
|
299
|
-
try {
|
|
300
|
-
jsonValue = JSON.parse(rawContent);
|
|
301
|
-
} catch (err) {
|
|
302
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
303
|
-
throw new Error(`Output ${id} is not valid JSON: ${message}`);
|
|
304
|
-
}
|
|
305
|
-
const value = applyQuery(jsonValue, query);
|
|
306
|
-
queryResults.push({ id, value });
|
|
307
|
-
try {
|
|
308
|
-
selectedContent = JSON.stringify(value, null, 2) ?? "null";
|
|
309
|
-
} catch {
|
|
310
|
-
selectedContent = String(value);
|
|
311
|
-
}
|
|
312
|
-
} else if (params.offset !== undefined || params.limit !== undefined) {
|
|
313
|
-
const startLine = Math.max(1, params.offset ?? 1);
|
|
314
|
-
if (startLine > totalLines) {
|
|
315
|
-
throw new Error(
|
|
316
|
-
`Offset ${params.offset ?? startLine} is beyond end of output (${totalLines} lines) for ${id}`,
|
|
317
|
-
);
|
|
318
|
-
}
|
|
319
|
-
const effectiveLimit = params.limit ?? totalLines - startLine + 1;
|
|
320
|
-
const endLine = Math.min(totalLines, startLine + effectiveLimit - 1);
|
|
321
|
-
const selectedLines = rawLines.slice(startLine - 1, endLine);
|
|
322
|
-
selectedContent = selectedLines.join("\n");
|
|
323
|
-
range = { startLine, endLine, totalLines };
|
|
324
|
-
}
|
|
285
|
+
const outputs: OutputEntry[] = [];
|
|
286
|
+
const notFound: string[] = [];
|
|
287
|
+
const outputContentById = new Map<string, string>();
|
|
288
|
+
const query = params.query?.trim();
|
|
289
|
+
const wantsQuery = query !== undefined && query.length > 0;
|
|
290
|
+
const format = params.format ?? (wantsQuery ? "json" : "raw");
|
|
325
291
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
path: outputPath,
|
|
330
|
-
lineCount: wantsQuery ? selectedContent.split("\n").length : totalLines,
|
|
331
|
-
charCount: wantsQuery ? selectedContent.length : totalChars,
|
|
332
|
-
provenance: parseOutputProvenance(id),
|
|
333
|
-
previewLines: extractPreviewLines(selectedContent, 4),
|
|
334
|
-
range,
|
|
335
|
-
query: query,
|
|
336
|
-
});
|
|
337
|
-
}
|
|
292
|
+
if (wantsQuery && (params.offset !== undefined || params.limit !== undefined)) {
|
|
293
|
+
throw new Error("query cannot be combined with offset/limit");
|
|
294
|
+
}
|
|
338
295
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
return {
|
|
348
|
-
content: [{ type: "text", text: errorMsg }],
|
|
349
|
-
details: { outputs, notFound, availableIds: available },
|
|
350
|
-
};
|
|
296
|
+
const queryResults: Array<{ id: string; value: unknown }> = [];
|
|
297
|
+
|
|
298
|
+
for (const id of params.ids) {
|
|
299
|
+
const outputPath = path.join(artifactsDir, `${id}.out.md`);
|
|
300
|
+
|
|
301
|
+
if (!fs.existsSync(outputPath)) {
|
|
302
|
+
notFound.push(id);
|
|
303
|
+
continue;
|
|
351
304
|
}
|
|
352
305
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
306
|
+
const rawContent = fs.readFileSync(outputPath, "utf-8");
|
|
307
|
+
const rawLines = rawContent.split("\n");
|
|
308
|
+
const totalLines = rawLines.length;
|
|
309
|
+
const totalChars = rawContent.length;
|
|
310
|
+
|
|
311
|
+
let selectedContent = rawContent;
|
|
312
|
+
let range: OutputRange | undefined;
|
|
313
|
+
|
|
314
|
+
if (wantsQuery && query) {
|
|
315
|
+
let jsonValue: unknown;
|
|
316
|
+
try {
|
|
317
|
+
jsonValue = JSON.parse(rawContent);
|
|
318
|
+
} catch (err) {
|
|
319
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
320
|
+
throw new Error(`Output ${id} is not valid JSON: ${message}`);
|
|
321
|
+
}
|
|
322
|
+
const value = applyQuery(jsonValue, query);
|
|
323
|
+
queryResults.push({ id, value });
|
|
324
|
+
try {
|
|
325
|
+
selectedContent = JSON.stringify(value, null, 2) ?? "null";
|
|
326
|
+
} catch {
|
|
327
|
+
selectedContent = String(value);
|
|
328
|
+
}
|
|
329
|
+
} else if (params.offset !== undefined || params.limit !== undefined) {
|
|
330
|
+
const startLine = Math.max(1, params.offset ?? 1);
|
|
331
|
+
if (startLine > totalLines) {
|
|
332
|
+
throw new Error(
|
|
333
|
+
`Offset ${params.offset ?? startLine} is beyond end of output (${totalLines} lines) for ${id}`,
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
const effectiveLimit = params.limit ?? totalLines - startLine + 1;
|
|
337
|
+
const endLine = Math.min(totalLines, startLine + effectiveLimit - 1);
|
|
338
|
+
const selectedLines = rawLines.slice(startLine - 1, endLine);
|
|
339
|
+
selectedContent = selectedLines.join("\n");
|
|
340
|
+
range = { startLine, endLine, totalLines };
|
|
387
341
|
}
|
|
388
342
|
|
|
343
|
+
outputContentById.set(id, selectedContent);
|
|
344
|
+
outputs.push({
|
|
345
|
+
id,
|
|
346
|
+
path: outputPath,
|
|
347
|
+
lineCount: wantsQuery ? selectedContent.split("\n").length : totalLines,
|
|
348
|
+
charCount: wantsQuery ? selectedContent.length : totalChars,
|
|
349
|
+
provenance: parseOutputProvenance(id),
|
|
350
|
+
previewLines: extractPreviewLines(selectedContent, 4),
|
|
351
|
+
range,
|
|
352
|
+
query: query,
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Error case: some IDs not found
|
|
357
|
+
if (notFound.length > 0) {
|
|
358
|
+
const available = listAvailableOutputs(artifactsDir);
|
|
359
|
+
const errorMsg =
|
|
360
|
+
available.length > 0
|
|
361
|
+
? `Not found: ${notFound.join(", ")}\nAvailable: ${available.join(", ")}`
|
|
362
|
+
: `Not found: ${notFound.join(", ")}\nNo outputs available in current session`;
|
|
363
|
+
|
|
389
364
|
return {
|
|
390
|
-
content: [{ type: "text", text:
|
|
391
|
-
details: { outputs },
|
|
365
|
+
content: [{ type: "text", text: errorMsg }],
|
|
366
|
+
details: { outputs, notFound, availableIds: available },
|
|
392
367
|
};
|
|
393
|
-
}
|
|
394
|
-
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Success: build response based on format
|
|
371
|
+
let contentText: string;
|
|
372
|
+
|
|
373
|
+
if (format === "json") {
|
|
374
|
+
const jsonData = wantsQuery
|
|
375
|
+
? queryResults
|
|
376
|
+
: outputs.map((o) => ({
|
|
377
|
+
id: o.id,
|
|
378
|
+
lineCount: o.lineCount,
|
|
379
|
+
charCount: o.charCount,
|
|
380
|
+
provenance: o.provenance,
|
|
381
|
+
previewLines: o.previewLines,
|
|
382
|
+
range: o.range,
|
|
383
|
+
content: outputContentById.get(o.id) ?? "",
|
|
384
|
+
}));
|
|
385
|
+
contentText = JSON.stringify(jsonData, null, 2);
|
|
386
|
+
} else {
|
|
387
|
+
// raw or stripped
|
|
388
|
+
const parts = outputs.map((o) => {
|
|
389
|
+
let content = outputContentById.get(o.id) ?? "";
|
|
390
|
+
if (format === "stripped") {
|
|
391
|
+
content = stripAnsi(content);
|
|
392
|
+
}
|
|
393
|
+
if (o.range && o.range.endLine < o.range.totalLines) {
|
|
394
|
+
const nextOffset = o.range.endLine + 1;
|
|
395
|
+
content += `\n\n[Showing lines ${o.range.startLine}-${o.range.endLine} of ${o.range.totalLines}. Use offset=${nextOffset} to continue]`;
|
|
396
|
+
}
|
|
397
|
+
// Add header for multiple outputs
|
|
398
|
+
if (outputs.length > 1) {
|
|
399
|
+
return `=== ${o.id} (${o.lineCount} lines, ${formatBytes(o.charCount)}) ===\n${content}`;
|
|
400
|
+
}
|
|
401
|
+
return content;
|
|
402
|
+
});
|
|
403
|
+
contentText = parts.join("\n\n");
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
return {
|
|
407
|
+
content: [{ type: "text", text: contentText }],
|
|
408
|
+
details: { outputs },
|
|
409
|
+
};
|
|
410
|
+
}
|
|
395
411
|
}
|
|
396
412
|
|
|
397
413
|
// =============================================================================
|