@oh-my-pi/pi-coding-agent 1.341.0 → 2.1.1337
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 +86 -0
- package/README.md +1 -1
- package/examples/custom-tools/subagent/index.ts +1 -1
- package/package.json +10 -9
- package/src/bun-imports.d.ts +16 -0
- package/src/cli/args.ts +5 -6
- package/src/cli/file-processor.ts +3 -3
- package/src/cli/list-models.ts +2 -2
- package/src/cli/plugin-cli.ts +1 -1
- package/src/cli/session-picker.ts +2 -2
- package/src/cli/update-cli.ts +273 -0
- package/src/cli.ts +1 -1
- package/src/config.ts +23 -75
- package/src/core/agent-session.ts +158 -16
- package/src/core/auth-storage.ts +2 -3
- package/src/core/bash-executor.ts +50 -10
- package/src/core/compaction/branch-summarization.ts +5 -5
- package/src/core/compaction/compaction.ts +3 -3
- package/src/core/compaction/index.ts +3 -3
- package/src/core/custom-commands/bundled/review/index.ts +156 -0
- package/src/core/custom-commands/index.ts +15 -0
- package/src/core/custom-commands/loader.ts +232 -0
- package/src/core/custom-commands/types.ts +112 -0
- package/src/core/custom-tools/index.ts +3 -3
- package/src/core/custom-tools/loader.ts +10 -8
- package/src/core/custom-tools/types.ts +11 -6
- package/src/core/custom-tools/wrapper.ts +2 -1
- package/src/core/exec.ts +22 -12
- package/src/core/export-html/index.ts +38 -123
- package/src/core/export-html/template.css +0 -7
- package/src/core/export-html/template.html +3 -4
- package/src/core/export-html/template.macro.ts +24 -0
- package/src/core/file-mentions.ts +54 -0
- package/src/core/hooks/index.ts +5 -5
- package/src/core/hooks/loader.ts +21 -16
- package/src/core/hooks/runner.ts +6 -6
- package/src/core/hooks/tool-wrapper.ts +2 -2
- package/src/core/hooks/types.ts +12 -15
- package/src/core/index.ts +6 -6
- package/src/core/logger.ts +112 -0
- package/src/core/mcp/client.ts +3 -3
- package/src/core/mcp/config.ts +1 -1
- package/src/core/mcp/index.ts +12 -12
- package/src/core/mcp/loader.ts +2 -2
- package/src/core/mcp/manager.ts +6 -6
- package/src/core/mcp/tool-bridge.ts +3 -3
- package/src/core/mcp/transports/http.ts +1 -1
- package/src/core/mcp/transports/index.ts +2 -2
- package/src/core/mcp/transports/stdio.ts +1 -1
- package/src/core/messages.ts +22 -0
- package/src/core/model-registry.ts +2 -2
- package/src/core/model-resolver.ts +2 -2
- package/src/core/plugins/doctor.ts +1 -1
- package/src/core/plugins/index.ts +6 -6
- package/src/core/plugins/installer.ts +4 -4
- package/src/core/plugins/loader.ts +4 -9
- package/src/core/plugins/manager.ts +5 -5
- package/src/core/plugins/paths.ts +3 -3
- package/src/core/sdk.ts +77 -35
- package/src/core/session-manager.ts +6 -6
- package/src/core/settings-manager.ts +16 -3
- package/src/core/skills.ts +5 -5
- package/src/core/slash-commands.ts +60 -45
- package/src/core/system-prompt.ts +6 -6
- package/src/core/title-generator.ts +2 -2
- package/src/core/tools/bash.ts +32 -155
- package/src/core/tools/context.ts +2 -2
- package/src/core/tools/edit-diff.ts +3 -3
- package/src/core/tools/edit.ts +18 -5
- package/src/core/tools/exa/company.ts +3 -3
- package/src/core/tools/exa/index.ts +16 -17
- package/src/core/tools/exa/linkedin.ts +3 -3
- package/src/core/tools/exa/mcp-client.ts +9 -9
- package/src/core/tools/exa/render.ts +5 -5
- package/src/core/tools/exa/researcher.ts +3 -3
- package/src/core/tools/exa/search.ts +6 -5
- package/src/core/tools/exa/types.ts +5 -6
- package/src/core/tools/exa/websets.ts +3 -3
- package/src/core/tools/find.ts +3 -3
- package/src/core/tools/grep.ts +3 -3
- package/src/core/tools/index.ts +48 -34
- package/src/core/tools/ls.ts +4 -4
- package/src/core/tools/lsp/client.ts +161 -90
- package/src/core/tools/lsp/config.ts +1 -1
- package/src/core/tools/lsp/edits.ts +2 -2
- package/src/core/tools/lsp/index.ts +15 -13
- package/src/core/tools/lsp/render.ts +2 -2
- package/src/core/tools/lsp/rust-analyzer.ts +3 -3
- package/src/core/tools/lsp/utils.ts +1 -1
- package/src/core/tools/notebook.ts +1 -1
- package/src/core/tools/output.ts +175 -0
- package/src/core/tools/read.ts +7 -7
- package/src/core/tools/renderers.ts +92 -13
- package/src/core/tools/review.ts +268 -0
- package/src/core/tools/task/agents.ts +22 -38
- package/src/core/tools/task/bundled-agents/reviewer.md +52 -37
- package/src/core/tools/task/commands.ts +31 -10
- package/src/core/tools/task/discovery.ts +2 -2
- package/src/core/tools/task/executor.ts +145 -28
- package/src/core/tools/task/index.ts +78 -30
- package/src/core/tools/task/model-resolver.ts +30 -20
- package/src/core/tools/task/parallel.ts +1 -1
- package/src/core/tools/task/render.ts +219 -30
- package/src/core/tools/task/subprocess-tool-registry.ts +89 -0
- package/src/core/tools/task/types.ts +36 -2
- package/src/core/tools/web-fetch.ts +5 -3
- package/src/core/tools/web-search/auth.ts +1 -1
- package/src/core/tools/web-search/index.ts +17 -15
- package/src/core/tools/web-search/providers/anthropic.ts +2 -2
- package/src/core/tools/web-search/providers/exa.ts +3 -5
- package/src/core/tools/web-search/providers/perplexity.ts +1 -1
- package/src/core/tools/web-search/render.ts +3 -3
- package/src/core/tools/write.ts +4 -4
- package/src/index.ts +29 -18
- package/src/main.ts +50 -33
- package/src/migrations.ts +3 -3
- package/src/modes/index.ts +5 -5
- package/src/modes/interactive/components/armin.ts +1 -1
- package/src/modes/interactive/components/assistant-message.ts +1 -1
- package/src/modes/interactive/components/bash-execution.ts +4 -4
- package/src/modes/interactive/components/bordered-loader.ts +2 -2
- package/src/modes/interactive/components/branch-summary-message.ts +2 -2
- package/src/modes/interactive/components/compaction-summary-message.ts +2 -2
- package/src/modes/interactive/components/diff.ts +1 -1
- package/src/modes/interactive/components/dynamic-border.ts +1 -1
- package/src/modes/interactive/components/footer.ts +5 -5
- package/src/modes/interactive/components/hook-editor.ts +2 -2
- package/src/modes/interactive/components/hook-input.ts +2 -2
- package/src/modes/interactive/components/hook-message.ts +3 -3
- package/src/modes/interactive/components/hook-selector.ts +2 -2
- package/src/modes/interactive/components/model-selector.ts +281 -59
- package/src/modes/interactive/components/oauth-selector.ts +3 -3
- package/src/modes/interactive/components/plugin-settings.ts +4 -4
- package/src/modes/interactive/components/queue-mode-selector.ts +2 -2
- package/src/modes/interactive/components/session-selector.ts +4 -4
- package/src/modes/interactive/components/settings-defs.ts +1 -1
- package/src/modes/interactive/components/settings-selector.ts +5 -5
- package/src/modes/interactive/components/show-images-selector.ts +2 -2
- package/src/modes/interactive/components/theme-selector.ts +2 -2
- package/src/modes/interactive/components/thinking-selector.ts +2 -2
- package/src/modes/interactive/components/tool-execution.ts +26 -8
- package/src/modes/interactive/components/tree-selector.ts +3 -3
- package/src/modes/interactive/components/user-message-selector.ts +2 -2
- package/src/modes/interactive/components/user-message.ts +1 -1
- package/src/modes/interactive/components/welcome.ts +2 -2
- package/src/modes/interactive/interactive-mode.ts +86 -42
- package/src/modes/interactive/theme/theme.ts +15 -17
- package/src/modes/print-mode.ts +4 -3
- package/src/modes/rpc/rpc-client.ts +4 -4
- package/src/modes/rpc/rpc-mode.ts +22 -12
- package/src/modes/rpc/rpc-types.ts +3 -3
- package/src/utils/changelog.ts +2 -2
- package/src/utils/clipboard.ts +1 -1
- package/src/utils/shell-snapshot.ts +218 -0
- package/src/utils/shell.ts +93 -13
- package/src/utils/tools-manager.ts +1 -1
- package/examples/custom-tools/subagent/agents/reviewer.md +0 -35
- package/src/core/tools/exa/logger.ts +0 -56
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
|
-
import { applyWorkspaceEdit } from "./edits
|
|
2
|
+
import { applyWorkspaceEdit } from "./edits";
|
|
3
3
|
import type {
|
|
4
4
|
Diagnostic,
|
|
5
5
|
LspClient,
|
|
@@ -8,14 +8,16 @@ import type {
|
|
|
8
8
|
LspJsonRpcResponse,
|
|
9
9
|
ServerConfig,
|
|
10
10
|
WorkspaceEdit,
|
|
11
|
-
} from "./types
|
|
12
|
-
import { detectLanguageId, fileToUri } from "./utils
|
|
11
|
+
} from "./types";
|
|
12
|
+
import { detectLanguageId, fileToUri } from "./utils";
|
|
13
13
|
|
|
14
14
|
// =============================================================================
|
|
15
15
|
// Client State
|
|
16
16
|
// =============================================================================
|
|
17
17
|
|
|
18
18
|
const clients = new Map<string, LspClient>();
|
|
19
|
+
const clientLocks = new Map<string, Promise<LspClient>>();
|
|
20
|
+
const fileOperationLocks = new Map<string, Promise<void>>();
|
|
19
21
|
|
|
20
22
|
// Idle timeout configuration (disabled by default)
|
|
21
23
|
let idleTimeoutMs: number | null = null;
|
|
@@ -233,13 +235,17 @@ async function startMessageReader(client: LspClient): Promise<void> {
|
|
|
233
235
|
const { done, value } = await reader.read();
|
|
234
236
|
if (done) break;
|
|
235
237
|
|
|
236
|
-
|
|
238
|
+
// Atomically update buffer before processing
|
|
239
|
+
const currentBuffer = concatBuffers(client.messageBuffer, value);
|
|
240
|
+
client.messageBuffer = currentBuffer;
|
|
237
241
|
|
|
238
242
|
// Process all complete messages in buffer
|
|
239
|
-
|
|
243
|
+
// Use local variable to avoid race with concurrent buffer updates
|
|
244
|
+
let workingBuffer = currentBuffer;
|
|
245
|
+
let parsed = parseMessage(workingBuffer);
|
|
240
246
|
while (parsed) {
|
|
241
247
|
const { message, remaining } = parsed;
|
|
242
|
-
|
|
248
|
+
workingBuffer = remaining;
|
|
243
249
|
|
|
244
250
|
// Route message
|
|
245
251
|
if ("id" in message && message.id !== undefined) {
|
|
@@ -263,8 +269,11 @@ async function startMessageReader(client: LspClient): Promise<void> {
|
|
|
263
269
|
}
|
|
264
270
|
}
|
|
265
271
|
|
|
266
|
-
parsed = parseMessage(
|
|
272
|
+
parsed = parseMessage(workingBuffer);
|
|
267
273
|
}
|
|
274
|
+
|
|
275
|
+
// Atomically commit processed buffer
|
|
276
|
+
client.messageBuffer = workingBuffer;
|
|
268
277
|
}
|
|
269
278
|
} catch (err) {
|
|
270
279
|
// Connection closed or error - reject all pending requests
|
|
@@ -368,71 +377,88 @@ async function sendResponse(
|
|
|
368
377
|
export async function getOrCreateClient(config: ServerConfig, cwd: string): Promise<LspClient> {
|
|
369
378
|
const key = `${config.command}:${cwd}`;
|
|
370
379
|
|
|
371
|
-
if
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
380
|
+
// Check if client already exists
|
|
381
|
+
const existingClient = clients.get(key);
|
|
382
|
+
if (existingClient) {
|
|
383
|
+
existingClient.lastActivity = Date.now();
|
|
384
|
+
return existingClient;
|
|
375
385
|
}
|
|
376
386
|
|
|
377
|
-
|
|
378
|
-
const
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
stdout: "pipe",
|
|
383
|
-
stderr: "pipe",
|
|
384
|
-
});
|
|
387
|
+
// Check if another coroutine is already creating this client
|
|
388
|
+
const existingLock = clientLocks.get(key);
|
|
389
|
+
if (existingLock) {
|
|
390
|
+
return existingLock;
|
|
391
|
+
}
|
|
385
392
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
isReading: false,
|
|
397
|
-
lastActivity: Date.now(),
|
|
398
|
-
};
|
|
399
|
-
clients.set(key, client);
|
|
393
|
+
// Create new client with lock
|
|
394
|
+
const clientPromise = (async () => {
|
|
395
|
+
const args = config.args ?? [];
|
|
396
|
+
const command = config.resolvedCommand ?? config.command;
|
|
397
|
+
const proc = Bun.spawn([command, ...args], {
|
|
398
|
+
cwd,
|
|
399
|
+
stdin: "pipe",
|
|
400
|
+
stdout: "pipe",
|
|
401
|
+
stderr: "pipe",
|
|
402
|
+
});
|
|
400
403
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
404
|
+
const client: LspClient = {
|
|
405
|
+
name: key,
|
|
406
|
+
cwd,
|
|
407
|
+
process: proc,
|
|
408
|
+
config,
|
|
409
|
+
requestId: 0,
|
|
410
|
+
diagnostics: new Map(),
|
|
411
|
+
openFiles: new Map(),
|
|
412
|
+
pendingRequests: new Map(),
|
|
413
|
+
messageBuffer: new Uint8Array(0),
|
|
414
|
+
isReading: false,
|
|
415
|
+
lastActivity: Date.now(),
|
|
416
|
+
};
|
|
417
|
+
clients.set(key, client);
|
|
418
|
+
|
|
419
|
+
// Register crash recovery - remove client on process exit
|
|
420
|
+
proc.exited.then(() => {
|
|
421
|
+
clients.delete(key);
|
|
422
|
+
clientLocks.delete(key);
|
|
423
|
+
});
|
|
405
424
|
|
|
406
|
-
|
|
407
|
-
|
|
425
|
+
// Start background message reader
|
|
426
|
+
startMessageReader(client);
|
|
427
|
+
|
|
428
|
+
try {
|
|
429
|
+
// Send initialize request
|
|
430
|
+
const initResult = (await sendRequest(client, "initialize", {
|
|
431
|
+
processId: process.pid,
|
|
432
|
+
rootUri: fileToUri(cwd),
|
|
433
|
+
rootPath: cwd,
|
|
434
|
+
capabilities: CLIENT_CAPABILITIES,
|
|
435
|
+
initializationOptions: config.initOptions ?? {},
|
|
436
|
+
workspaceFolders: [{ uri: fileToUri(cwd), name: cwd.split("/").pop() ?? "workspace" }],
|
|
437
|
+
})) as { capabilities?: unknown };
|
|
438
|
+
|
|
439
|
+
if (!initResult) {
|
|
440
|
+
throw new Error("Failed to initialize LSP: no response");
|
|
441
|
+
}
|
|
408
442
|
|
|
409
|
-
|
|
410
|
-
// Send initialize request
|
|
411
|
-
const initResult = (await sendRequest(client, "initialize", {
|
|
412
|
-
processId: process.pid,
|
|
413
|
-
rootUri: fileToUri(cwd),
|
|
414
|
-
rootPath: cwd,
|
|
415
|
-
capabilities: CLIENT_CAPABILITIES,
|
|
416
|
-
initializationOptions: config.initOptions ?? {},
|
|
417
|
-
workspaceFolders: [{ uri: fileToUri(cwd), name: cwd.split("/").pop() ?? "workspace" }],
|
|
418
|
-
})) as { capabilities?: unknown };
|
|
419
|
-
|
|
420
|
-
if (!initResult) {
|
|
421
|
-
throw new Error("Failed to initialize LSP: no response");
|
|
422
|
-
}
|
|
443
|
+
client.serverCapabilities = initResult.capabilities as LspClient["serverCapabilities"];
|
|
423
444
|
|
|
424
|
-
|
|
445
|
+
// Send initialized notification
|
|
446
|
+
await sendNotification(client, "initialized", {});
|
|
425
447
|
|
|
426
|
-
|
|
427
|
-
|
|
448
|
+
return client;
|
|
449
|
+
} catch (err) {
|
|
450
|
+
// Clean up on initialization failure
|
|
451
|
+
clients.delete(key);
|
|
452
|
+
clientLocks.delete(key);
|
|
453
|
+
proc.kill();
|
|
454
|
+
throw err;
|
|
455
|
+
} finally {
|
|
456
|
+
clientLocks.delete(key);
|
|
457
|
+
}
|
|
458
|
+
})();
|
|
428
459
|
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
// Clean up on initialization failure
|
|
432
|
-
clients.delete(key);
|
|
433
|
-
proc.kill();
|
|
434
|
-
throw err;
|
|
435
|
-
}
|
|
460
|
+
clientLocks.set(key, clientPromise);
|
|
461
|
+
return clientPromise;
|
|
436
462
|
}
|
|
437
463
|
|
|
438
464
|
/**
|
|
@@ -441,24 +467,49 @@ export async function getOrCreateClient(config: ServerConfig, cwd: string): Prom
|
|
|
441
467
|
*/
|
|
442
468
|
export async function ensureFileOpen(client: LspClient, filePath: string): Promise<void> {
|
|
443
469
|
const uri = fileToUri(filePath);
|
|
470
|
+
const lockKey = `${client.name}:${uri}`;
|
|
471
|
+
|
|
472
|
+
// Check if file is already open
|
|
444
473
|
if (client.openFiles.has(uri)) {
|
|
445
474
|
return;
|
|
446
475
|
}
|
|
447
476
|
|
|
448
|
-
|
|
449
|
-
const
|
|
477
|
+
// Check if another operation is already opening this file
|
|
478
|
+
const existingLock = fileOperationLocks.get(lockKey);
|
|
479
|
+
if (existingLock) {
|
|
480
|
+
await existingLock;
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
450
483
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
},
|
|
458
|
-
});
|
|
484
|
+
// Lock and open file
|
|
485
|
+
const openPromise = (async () => {
|
|
486
|
+
// Double-check after acquiring lock
|
|
487
|
+
if (client.openFiles.has(uri)) {
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
459
490
|
|
|
460
|
-
|
|
461
|
-
|
|
491
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
492
|
+
const languageId = detectLanguageId(filePath);
|
|
493
|
+
|
|
494
|
+
await sendNotification(client, "textDocument/didOpen", {
|
|
495
|
+
textDocument: {
|
|
496
|
+
uri,
|
|
497
|
+
languageId,
|
|
498
|
+
version: 1,
|
|
499
|
+
text: content,
|
|
500
|
+
},
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
client.openFiles.set(uri, { version: 1, languageId });
|
|
504
|
+
client.lastActivity = Date.now();
|
|
505
|
+
})();
|
|
506
|
+
|
|
507
|
+
fileOperationLocks.set(lockKey, openPromise);
|
|
508
|
+
try {
|
|
509
|
+
await openPromise;
|
|
510
|
+
} finally {
|
|
511
|
+
fileOperationLocks.delete(lockKey);
|
|
512
|
+
}
|
|
462
513
|
}
|
|
463
514
|
|
|
464
515
|
/**
|
|
@@ -467,27 +518,45 @@ export async function ensureFileOpen(client: LspClient, filePath: string): Promi
|
|
|
467
518
|
*/
|
|
468
519
|
export async function refreshFile(client: LspClient, filePath: string): Promise<void> {
|
|
469
520
|
const uri = fileToUri(filePath);
|
|
470
|
-
const
|
|
521
|
+
const lockKey = `${client.name}:${uri}`;
|
|
471
522
|
|
|
472
|
-
if
|
|
473
|
-
|
|
474
|
-
|
|
523
|
+
// Check if another operation is in progress
|
|
524
|
+
const existingLock = fileOperationLocks.get(lockKey);
|
|
525
|
+
if (existingLock) {
|
|
526
|
+
await existingLock;
|
|
475
527
|
}
|
|
476
528
|
|
|
477
|
-
|
|
478
|
-
|
|
529
|
+
// Lock and refresh file
|
|
530
|
+
const refreshPromise = (async () => {
|
|
531
|
+
const info = client.openFiles.get(uri);
|
|
479
532
|
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
533
|
+
if (!info) {
|
|
534
|
+
await ensureFileOpen(client, filePath);
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
484
537
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
text: content,
|
|
488
|
-
});
|
|
538
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
539
|
+
const version = ++info.version;
|
|
489
540
|
|
|
490
|
-
|
|
541
|
+
await sendNotification(client, "textDocument/didChange", {
|
|
542
|
+
textDocument: { uri, version },
|
|
543
|
+
contentChanges: [{ text: content }],
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
await sendNotification(client, "textDocument/didSave", {
|
|
547
|
+
textDocument: { uri },
|
|
548
|
+
text: content,
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
client.lastActivity = Date.now();
|
|
552
|
+
})();
|
|
553
|
+
|
|
554
|
+
fileOperationLocks.set(lockKey, refreshPromise);
|
|
555
|
+
try {
|
|
556
|
+
await refreshPromise;
|
|
557
|
+
} finally {
|
|
558
|
+
fileOperationLocks.delete(lockKey);
|
|
559
|
+
}
|
|
491
560
|
}
|
|
492
561
|
|
|
493
562
|
/**
|
|
@@ -519,7 +588,9 @@ export function shutdownClient(key: string): void {
|
|
|
519
588
|
* Send an LSP request and wait for response.
|
|
520
589
|
*/
|
|
521
590
|
export async function sendRequest(client: LspClient, method: string, params: unknown): Promise<unknown> {
|
|
591
|
+
// Atomically increment and capture request ID
|
|
522
592
|
const id = ++client.requestId;
|
|
593
|
+
|
|
523
594
|
const request: LspJsonRpcRequest = {
|
|
524
595
|
jsonrpc: "2.0",
|
|
525
596
|
id,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { extname, join } from "node:path";
|
|
4
|
-
import type { ServerConfig } from "./types
|
|
4
|
+
import type { ServerConfig } from "./types";
|
|
5
5
|
|
|
6
6
|
export interface LspConfig {
|
|
7
7
|
servers: Record<string, ServerConfig>;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { mkdir, rename, rm } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import type { CreateFile, DeleteFile, RenameFile, TextDocumentEdit, TextEdit, WorkspaceEdit } from "./types
|
|
4
|
-
import { uriToFile } from "./utils
|
|
3
|
+
import type { CreateFile, DeleteFile, RenameFile, TextDocumentEdit, TextEdit, WorkspaceEdit } from "./types";
|
|
4
|
+
import { uriToFile } from "./utils";
|
|
5
5
|
|
|
6
6
|
// =============================================================================
|
|
7
7
|
// Text Edit Application
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
4
|
-
import type { Theme } from "../../../modes/interactive/theme/theme
|
|
5
|
-
import {
|
|
4
|
+
import type { Theme } from "../../../modes/interactive/theme/theme";
|
|
5
|
+
import { logger } from "../../logger";
|
|
6
|
+
import { resolveToCwd } from "../path-utils";
|
|
6
7
|
import {
|
|
7
8
|
ensureFileOpen,
|
|
8
9
|
getActiveClients,
|
|
@@ -11,11 +12,11 @@ import {
|
|
|
11
12
|
refreshFile,
|
|
12
13
|
sendRequest,
|
|
13
14
|
setIdleTimeout,
|
|
14
|
-
} from "./client
|
|
15
|
-
import { getServerForFile, getServersForFile, hasCapability, type LspConfig, loadConfig } from "./config
|
|
16
|
-
import { applyTextEdits, applyWorkspaceEdit } from "./edits
|
|
17
|
-
import { renderCall, renderResult } from "./render
|
|
18
|
-
import * as rustAnalyzer from "./rust-analyzer
|
|
15
|
+
} from "./client";
|
|
16
|
+
import { getServerForFile, getServersForFile, hasCapability, type LspConfig, loadConfig } from "./config";
|
|
17
|
+
import { applyTextEdits, applyWorkspaceEdit } from "./edits";
|
|
18
|
+
import { renderCall, renderResult } from "./render";
|
|
19
|
+
import * as rustAnalyzer from "./rust-analyzer";
|
|
19
20
|
import {
|
|
20
21
|
type CallHierarchyIncomingCall,
|
|
21
22
|
type CallHierarchyItem,
|
|
@@ -35,7 +36,7 @@ import {
|
|
|
35
36
|
type SymbolInformation,
|
|
36
37
|
type TextEdit,
|
|
37
38
|
type WorkspaceEdit,
|
|
38
|
-
} from "./types
|
|
39
|
+
} from "./types";
|
|
39
40
|
import {
|
|
40
41
|
extractHoverText,
|
|
41
42
|
fileToUri,
|
|
@@ -48,10 +49,10 @@ import {
|
|
|
48
49
|
sleep,
|
|
49
50
|
symbolKindToIcon,
|
|
50
51
|
uriToFile,
|
|
51
|
-
} from "./utils
|
|
52
|
+
} from "./utils";
|
|
52
53
|
|
|
53
|
-
export type { LspServerStatus } from "./client
|
|
54
|
-
export type { LspToolDetails } from "./types
|
|
54
|
+
export type { LspServerStatus } from "./client";
|
|
55
|
+
export type { LspToolDetails } from "./types";
|
|
55
56
|
|
|
56
57
|
/** Result from warming up LSP servers */
|
|
57
58
|
export interface LspWarmupResult {
|
|
@@ -273,7 +274,8 @@ async function runWorkspaceDiagnostics(
|
|
|
273
274
|
const formatted = collected.slice(0, 50).map((d) => formatDiagnostic(d.diagnostic, d.filePath));
|
|
274
275
|
const more = collected.length > 50 ? `\n ... and ${collected.length - 50} more` : "";
|
|
275
276
|
return { output: `${summary}:\n${formatted.map((f) => ` ${f}`).join("\n")}${more}`, projectType };
|
|
276
|
-
} catch (
|
|
277
|
+
} catch (err) {
|
|
278
|
+
logger.debug("LSP diagnostics failed, falling back to shell", { error: String(err) });
|
|
277
279
|
// Fall through to shell command
|
|
278
280
|
}
|
|
279
281
|
}
|
|
@@ -305,7 +307,7 @@ async function runWorkspaceDiagnostics(
|
|
|
305
307
|
// Limit output length
|
|
306
308
|
const lines = combined.split("\n");
|
|
307
309
|
if (lines.length > 50) {
|
|
308
|
-
return { output: lines.slice(0, 50).join("\n")
|
|
310
|
+
return { output: `${lines.slice(0, 50).join("\n")}\n... and ${lines.length - 50} more lines`, projectType };
|
|
309
311
|
}
|
|
310
312
|
|
|
311
313
|
return { output: combined, projectType };
|
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
import type { AgentToolResult, RenderResultOptions } from "@oh-my-pi/pi-agent-core";
|
|
12
12
|
import { Text } from "@oh-my-pi/pi-tui";
|
|
13
13
|
import { highlight, supportsLanguage } from "cli-highlight";
|
|
14
|
-
import type { Theme } from "../../../modes/interactive/theme/theme
|
|
15
|
-
import type { LspParams, LspToolDetails } from "./types
|
|
14
|
+
import type { Theme } from "../../../modes/interactive/theme/theme";
|
|
15
|
+
import type { LspParams, LspToolDetails } from "./types";
|
|
16
16
|
|
|
17
17
|
// =============================================================================
|
|
18
18
|
// Tree Drawing Characters
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { sendNotification, sendRequest } from "./client
|
|
2
|
-
import type { Diagnostic, ExpandMacroResult, LspClient, RelatedTest, Runnable, WorkspaceEdit } from "./types
|
|
3
|
-
import { fileToUri } from "./utils
|
|
1
|
+
import { sendNotification, sendRequest } from "./client";
|
|
2
|
+
import type { Diagnostic, ExpandMacroResult, LspClient, RelatedTest, Runnable, WorkspaceEdit } from "./types";
|
|
3
|
+
import { fileToUri } from "./utils";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Wait for specified milliseconds.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
2
2
|
import { Type } from "@sinclair/typebox";
|
|
3
|
-
import { resolveToCwd } from "./path-utils
|
|
3
|
+
import { resolveToCwd } from "./path-utils";
|
|
4
4
|
|
|
5
5
|
const notebookSchema = Type.Object({
|
|
6
6
|
action: Type.Union([Type.Literal("edit"), Type.Literal("insert"), Type.Literal("delete")], {
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Output tool for reading agent/task outputs by ID.
|
|
3
|
+
*
|
|
4
|
+
* Resolves IDs like "reviewer_0" to artifact paths in the current session.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as fs from "node:fs";
|
|
8
|
+
import * as path from "node:path";
|
|
9
|
+
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
10
|
+
import type { TextContent } from "@oh-my-pi/pi-ai";
|
|
11
|
+
import { Type } from "@sinclair/typebox";
|
|
12
|
+
import type { SessionContext } from "./index";
|
|
13
|
+
import { getArtifactsDir } from "./task/artifacts";
|
|
14
|
+
|
|
15
|
+
const outputSchema = Type.Object({
|
|
16
|
+
ids: Type.Array(Type.String(), {
|
|
17
|
+
description: "Agent output IDs to read (e.g., ['reviewer_0', 'explore_1'])",
|
|
18
|
+
minItems: 1,
|
|
19
|
+
}),
|
|
20
|
+
format: Type.Optional(
|
|
21
|
+
Type.Union([Type.Literal("raw"), Type.Literal("json"), Type.Literal("stripped")], {
|
|
22
|
+
description: "Output format: raw (default), json (structured), stripped (no ANSI)",
|
|
23
|
+
}),
|
|
24
|
+
),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
/** Metadata for a single output file */
|
|
28
|
+
interface OutputEntry {
|
|
29
|
+
id: string;
|
|
30
|
+
path: string;
|
|
31
|
+
lineCount: number;
|
|
32
|
+
charCount: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface OutputToolDetails {
|
|
36
|
+
outputs: OutputEntry[];
|
|
37
|
+
notFound?: string[];
|
|
38
|
+
availableIds?: string[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Strip ANSI escape codes from text */
|
|
42
|
+
function stripAnsi(text: string): string {
|
|
43
|
+
return text.replace(/\x1b\[[0-9;]*m/g, "");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** List available output IDs in artifacts directory */
|
|
47
|
+
function listAvailableOutputs(artifactsDir: string): string[] {
|
|
48
|
+
try {
|
|
49
|
+
const files = fs.readdirSync(artifactsDir);
|
|
50
|
+
return files.filter((f) => f.endsWith(".out.md")).map((f) => f.replace(".out.md", ""));
|
|
51
|
+
} catch {
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Format byte count for display */
|
|
57
|
+
function formatBytes(bytes: number): string {
|
|
58
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
59
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}K`;
|
|
60
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)}M`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function createOutputTool(
|
|
64
|
+
_cwd: string,
|
|
65
|
+
sessionContext?: SessionContext,
|
|
66
|
+
): AgentTool<typeof outputSchema, OutputToolDetails> {
|
|
67
|
+
return {
|
|
68
|
+
name: "output",
|
|
69
|
+
label: "Output",
|
|
70
|
+
description: `Read full agent/task output by ID.
|
|
71
|
+
|
|
72
|
+
Use when the Task tool's truncated preview isn't sufficient for your needs.
|
|
73
|
+
The Task tool already returns summaries with line/char counts in its result.
|
|
74
|
+
|
|
75
|
+
Parameters:
|
|
76
|
+
- ids: Array of output IDs (e.g., ["reviewer_0", "explore_1"])
|
|
77
|
+
- format: "raw" (default), "json" (structured object), or "stripped" (no ANSI codes)
|
|
78
|
+
|
|
79
|
+
Returns the full output content. For unknown IDs, returns an error with available IDs.
|
|
80
|
+
|
|
81
|
+
Example: { "ids": ["reviewer_0"] }`,
|
|
82
|
+
parameters: outputSchema,
|
|
83
|
+
execute: async (
|
|
84
|
+
_toolCallId: string,
|
|
85
|
+
params: { ids: string[]; format?: "raw" | "json" | "stripped" },
|
|
86
|
+
): Promise<{ content: TextContent[]; details: OutputToolDetails }> => {
|
|
87
|
+
const sessionFile = sessionContext?.getSessionFile();
|
|
88
|
+
|
|
89
|
+
if (!sessionFile) {
|
|
90
|
+
return {
|
|
91
|
+
content: [{ type: "text", text: "No session - output artifacts unavailable" }],
|
|
92
|
+
details: { outputs: [], notFound: params.ids },
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const artifactsDir = getArtifactsDir(sessionFile);
|
|
97
|
+
if (!artifactsDir || !fs.existsSync(artifactsDir)) {
|
|
98
|
+
return {
|
|
99
|
+
content: [{ type: "text", text: "No artifacts directory found" }],
|
|
100
|
+
details: { outputs: [], notFound: params.ids },
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const outputs: OutputEntry[] = [];
|
|
105
|
+
const notFound: string[] = [];
|
|
106
|
+
const format = params.format ?? "raw";
|
|
107
|
+
|
|
108
|
+
for (const id of params.ids) {
|
|
109
|
+
const outputPath = path.join(artifactsDir, `${id}.out.md`);
|
|
110
|
+
|
|
111
|
+
if (!fs.existsSync(outputPath)) {
|
|
112
|
+
notFound.push(id);
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const content = fs.readFileSync(outputPath, "utf-8");
|
|
117
|
+
outputs.push({
|
|
118
|
+
id,
|
|
119
|
+
path: outputPath,
|
|
120
|
+
lineCount: content.split("\n").length,
|
|
121
|
+
charCount: content.length,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Error case: some IDs not found
|
|
126
|
+
if (notFound.length > 0) {
|
|
127
|
+
const available = listAvailableOutputs(artifactsDir);
|
|
128
|
+
const errorMsg =
|
|
129
|
+
available.length > 0
|
|
130
|
+
? `Not found: ${notFound.join(", ")}\nAvailable: ${available.join(", ")}`
|
|
131
|
+
: `Not found: ${notFound.join(", ")}\nNo outputs available in current session`;
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
content: [{ type: "text", text: errorMsg }],
|
|
135
|
+
details: { outputs, notFound, availableIds: available },
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Success: build response based on format
|
|
140
|
+
let contentText: string;
|
|
141
|
+
|
|
142
|
+
if (format === "json") {
|
|
143
|
+
const jsonData = outputs.map((o) => ({
|
|
144
|
+
id: o.id,
|
|
145
|
+
lineCount: o.lineCount,
|
|
146
|
+
charCount: o.charCount,
|
|
147
|
+
content: fs.readFileSync(o.path, "utf-8"),
|
|
148
|
+
}));
|
|
149
|
+
contentText = JSON.stringify(jsonData, null, 2);
|
|
150
|
+
} else {
|
|
151
|
+
// raw or stripped
|
|
152
|
+
const parts = outputs.map((o) => {
|
|
153
|
+
let content = fs.readFileSync(o.path, "utf-8");
|
|
154
|
+
if (format === "stripped") {
|
|
155
|
+
content = stripAnsi(content);
|
|
156
|
+
}
|
|
157
|
+
// Add header for multiple outputs
|
|
158
|
+
if (outputs.length > 1) {
|
|
159
|
+
return `=== ${o.id} (${o.lineCount} lines, ${formatBytes(o.charCount)}) ===\n${content}`;
|
|
160
|
+
}
|
|
161
|
+
return content;
|
|
162
|
+
});
|
|
163
|
+
contentText = parts.join("\n\n");
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
content: [{ type: "text", text: contentText }],
|
|
168
|
+
details: { outputs },
|
|
169
|
+
};
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Default output tool using process.cwd() - for backwards compatibility */
|
|
175
|
+
export const outputTool = createOutputTool(process.cwd());
|
package/src/core/tools/read.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { constants } from "node:fs";
|
|
3
|
+
import { access, readFile } from "node:fs/promises";
|
|
4
|
+
import { extname } from "node:path";
|
|
1
5
|
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
2
6
|
import type { ImageContent, TextContent } from "@oh-my-pi/pi-ai";
|
|
3
7
|
import { Type } from "@sinclair/typebox";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { extname } from "path";
|
|
8
|
-
import { detectSupportedImageMimeTypeFromFile } from "../../utils/mime.js";
|
|
9
|
-
import { resolveReadPath } from "./path-utils.js";
|
|
10
|
-
import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, type TruncationResult, truncateHead } from "./truncate.js";
|
|
8
|
+
import { detectSupportedImageMimeTypeFromFile } from "../../utils/mime";
|
|
9
|
+
import { resolveReadPath } from "./path-utils";
|
|
10
|
+
import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, type TruncationResult, truncateHead } from "./truncate";
|
|
11
11
|
|
|
12
12
|
// Document types convertible via markitdown
|
|
13
13
|
const CONVERTIBLE_EXTENSIONS = new Set([".pdf", ".doc", ".docx", ".ppt", ".pptx", ".xls", ".xlsx", ".rtf", ".epub"]);
|