@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.
Files changed (158) hide show
  1. package/CHANGELOG.md +86 -0
  2. package/README.md +1 -1
  3. package/examples/custom-tools/subagent/index.ts +1 -1
  4. package/package.json +10 -9
  5. package/src/bun-imports.d.ts +16 -0
  6. package/src/cli/args.ts +5 -6
  7. package/src/cli/file-processor.ts +3 -3
  8. package/src/cli/list-models.ts +2 -2
  9. package/src/cli/plugin-cli.ts +1 -1
  10. package/src/cli/session-picker.ts +2 -2
  11. package/src/cli/update-cli.ts +273 -0
  12. package/src/cli.ts +1 -1
  13. package/src/config.ts +23 -75
  14. package/src/core/agent-session.ts +158 -16
  15. package/src/core/auth-storage.ts +2 -3
  16. package/src/core/bash-executor.ts +50 -10
  17. package/src/core/compaction/branch-summarization.ts +5 -5
  18. package/src/core/compaction/compaction.ts +3 -3
  19. package/src/core/compaction/index.ts +3 -3
  20. package/src/core/custom-commands/bundled/review/index.ts +156 -0
  21. package/src/core/custom-commands/index.ts +15 -0
  22. package/src/core/custom-commands/loader.ts +232 -0
  23. package/src/core/custom-commands/types.ts +112 -0
  24. package/src/core/custom-tools/index.ts +3 -3
  25. package/src/core/custom-tools/loader.ts +10 -8
  26. package/src/core/custom-tools/types.ts +11 -6
  27. package/src/core/custom-tools/wrapper.ts +2 -1
  28. package/src/core/exec.ts +22 -12
  29. package/src/core/export-html/index.ts +38 -123
  30. package/src/core/export-html/template.css +0 -7
  31. package/src/core/export-html/template.html +3 -4
  32. package/src/core/export-html/template.macro.ts +24 -0
  33. package/src/core/file-mentions.ts +54 -0
  34. package/src/core/hooks/index.ts +5 -5
  35. package/src/core/hooks/loader.ts +21 -16
  36. package/src/core/hooks/runner.ts +6 -6
  37. package/src/core/hooks/tool-wrapper.ts +2 -2
  38. package/src/core/hooks/types.ts +12 -15
  39. package/src/core/index.ts +6 -6
  40. package/src/core/logger.ts +112 -0
  41. package/src/core/mcp/client.ts +3 -3
  42. package/src/core/mcp/config.ts +1 -1
  43. package/src/core/mcp/index.ts +12 -12
  44. package/src/core/mcp/loader.ts +2 -2
  45. package/src/core/mcp/manager.ts +6 -6
  46. package/src/core/mcp/tool-bridge.ts +3 -3
  47. package/src/core/mcp/transports/http.ts +1 -1
  48. package/src/core/mcp/transports/index.ts +2 -2
  49. package/src/core/mcp/transports/stdio.ts +1 -1
  50. package/src/core/messages.ts +22 -0
  51. package/src/core/model-registry.ts +2 -2
  52. package/src/core/model-resolver.ts +2 -2
  53. package/src/core/plugins/doctor.ts +1 -1
  54. package/src/core/plugins/index.ts +6 -6
  55. package/src/core/plugins/installer.ts +4 -4
  56. package/src/core/plugins/loader.ts +4 -9
  57. package/src/core/plugins/manager.ts +5 -5
  58. package/src/core/plugins/paths.ts +3 -3
  59. package/src/core/sdk.ts +77 -35
  60. package/src/core/session-manager.ts +6 -6
  61. package/src/core/settings-manager.ts +16 -3
  62. package/src/core/skills.ts +5 -5
  63. package/src/core/slash-commands.ts +60 -45
  64. package/src/core/system-prompt.ts +6 -6
  65. package/src/core/title-generator.ts +2 -2
  66. package/src/core/tools/bash.ts +32 -155
  67. package/src/core/tools/context.ts +2 -2
  68. package/src/core/tools/edit-diff.ts +3 -3
  69. package/src/core/tools/edit.ts +18 -5
  70. package/src/core/tools/exa/company.ts +3 -3
  71. package/src/core/tools/exa/index.ts +16 -17
  72. package/src/core/tools/exa/linkedin.ts +3 -3
  73. package/src/core/tools/exa/mcp-client.ts +9 -9
  74. package/src/core/tools/exa/render.ts +5 -5
  75. package/src/core/tools/exa/researcher.ts +3 -3
  76. package/src/core/tools/exa/search.ts +6 -5
  77. package/src/core/tools/exa/types.ts +5 -6
  78. package/src/core/tools/exa/websets.ts +3 -3
  79. package/src/core/tools/find.ts +3 -3
  80. package/src/core/tools/grep.ts +3 -3
  81. package/src/core/tools/index.ts +48 -34
  82. package/src/core/tools/ls.ts +4 -4
  83. package/src/core/tools/lsp/client.ts +161 -90
  84. package/src/core/tools/lsp/config.ts +1 -1
  85. package/src/core/tools/lsp/edits.ts +2 -2
  86. package/src/core/tools/lsp/index.ts +15 -13
  87. package/src/core/tools/lsp/render.ts +2 -2
  88. package/src/core/tools/lsp/rust-analyzer.ts +3 -3
  89. package/src/core/tools/lsp/utils.ts +1 -1
  90. package/src/core/tools/notebook.ts +1 -1
  91. package/src/core/tools/output.ts +175 -0
  92. package/src/core/tools/read.ts +7 -7
  93. package/src/core/tools/renderers.ts +92 -13
  94. package/src/core/tools/review.ts +268 -0
  95. package/src/core/tools/task/agents.ts +22 -38
  96. package/src/core/tools/task/bundled-agents/reviewer.md +52 -37
  97. package/src/core/tools/task/commands.ts +31 -10
  98. package/src/core/tools/task/discovery.ts +2 -2
  99. package/src/core/tools/task/executor.ts +145 -28
  100. package/src/core/tools/task/index.ts +78 -30
  101. package/src/core/tools/task/model-resolver.ts +30 -20
  102. package/src/core/tools/task/parallel.ts +1 -1
  103. package/src/core/tools/task/render.ts +219 -30
  104. package/src/core/tools/task/subprocess-tool-registry.ts +89 -0
  105. package/src/core/tools/task/types.ts +36 -2
  106. package/src/core/tools/web-fetch.ts +5 -3
  107. package/src/core/tools/web-search/auth.ts +1 -1
  108. package/src/core/tools/web-search/index.ts +17 -15
  109. package/src/core/tools/web-search/providers/anthropic.ts +2 -2
  110. package/src/core/tools/web-search/providers/exa.ts +3 -5
  111. package/src/core/tools/web-search/providers/perplexity.ts +1 -1
  112. package/src/core/tools/web-search/render.ts +3 -3
  113. package/src/core/tools/write.ts +4 -4
  114. package/src/index.ts +29 -18
  115. package/src/main.ts +50 -33
  116. package/src/migrations.ts +3 -3
  117. package/src/modes/index.ts +5 -5
  118. package/src/modes/interactive/components/armin.ts +1 -1
  119. package/src/modes/interactive/components/assistant-message.ts +1 -1
  120. package/src/modes/interactive/components/bash-execution.ts +4 -4
  121. package/src/modes/interactive/components/bordered-loader.ts +2 -2
  122. package/src/modes/interactive/components/branch-summary-message.ts +2 -2
  123. package/src/modes/interactive/components/compaction-summary-message.ts +2 -2
  124. package/src/modes/interactive/components/diff.ts +1 -1
  125. package/src/modes/interactive/components/dynamic-border.ts +1 -1
  126. package/src/modes/interactive/components/footer.ts +5 -5
  127. package/src/modes/interactive/components/hook-editor.ts +2 -2
  128. package/src/modes/interactive/components/hook-input.ts +2 -2
  129. package/src/modes/interactive/components/hook-message.ts +3 -3
  130. package/src/modes/interactive/components/hook-selector.ts +2 -2
  131. package/src/modes/interactive/components/model-selector.ts +281 -59
  132. package/src/modes/interactive/components/oauth-selector.ts +3 -3
  133. package/src/modes/interactive/components/plugin-settings.ts +4 -4
  134. package/src/modes/interactive/components/queue-mode-selector.ts +2 -2
  135. package/src/modes/interactive/components/session-selector.ts +4 -4
  136. package/src/modes/interactive/components/settings-defs.ts +1 -1
  137. package/src/modes/interactive/components/settings-selector.ts +5 -5
  138. package/src/modes/interactive/components/show-images-selector.ts +2 -2
  139. package/src/modes/interactive/components/theme-selector.ts +2 -2
  140. package/src/modes/interactive/components/thinking-selector.ts +2 -2
  141. package/src/modes/interactive/components/tool-execution.ts +26 -8
  142. package/src/modes/interactive/components/tree-selector.ts +3 -3
  143. package/src/modes/interactive/components/user-message-selector.ts +2 -2
  144. package/src/modes/interactive/components/user-message.ts +1 -1
  145. package/src/modes/interactive/components/welcome.ts +2 -2
  146. package/src/modes/interactive/interactive-mode.ts +86 -42
  147. package/src/modes/interactive/theme/theme.ts +15 -17
  148. package/src/modes/print-mode.ts +4 -3
  149. package/src/modes/rpc/rpc-client.ts +4 -4
  150. package/src/modes/rpc/rpc-mode.ts +22 -12
  151. package/src/modes/rpc/rpc-types.ts +3 -3
  152. package/src/utils/changelog.ts +2 -2
  153. package/src/utils/clipboard.ts +1 -1
  154. package/src/utils/shell-snapshot.ts +218 -0
  155. package/src/utils/shell.ts +93 -13
  156. package/src/utils/tools-manager.ts +1 -1
  157. package/examples/custom-tools/subagent/agents/reviewer.md +0 -35
  158. 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.js";
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.js";
12
- import { detectLanguageId, fileToUri } from "./utils.js";
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
- client.messageBuffer = concatBuffers(client.messageBuffer, value);
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
- let parsed = parseMessage(client.messageBuffer);
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
- client.messageBuffer = remaining;
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(client.messageBuffer);
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 (clients.has(key)) {
372
- const client = clients.get(key)!;
373
- client.lastActivity = Date.now();
374
- return client;
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
- const args = config.args ?? [];
378
- const command = config.resolvedCommand ?? config.command;
379
- const proc = Bun.spawn([command, ...args], {
380
- cwd,
381
- stdin: "pipe",
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
- const client: LspClient = {
387
- name: key,
388
- cwd,
389
- process: proc,
390
- config,
391
- requestId: 0,
392
- diagnostics: new Map(),
393
- openFiles: new Map(),
394
- pendingRequests: new Map(),
395
- messageBuffer: new Uint8Array(0),
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
- // Register crash recovery - remove client on process exit
402
- proc.exited.then(() => {
403
- clients.delete(key);
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
- // Start background message reader
407
- startMessageReader(client);
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
- try {
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
- client.serverCapabilities = initResult.capabilities as LspClient["serverCapabilities"];
445
+ // Send initialized notification
446
+ await sendNotification(client, "initialized", {});
425
447
 
426
- // Send initialized notification
427
- await sendNotification(client, "initialized", {});
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
- return client;
430
- } catch (err) {
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
- const content = fs.readFileSync(filePath, "utf-8");
449
- const languageId = detectLanguageId(filePath);
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
- await sendNotification(client, "textDocument/didOpen", {
452
- textDocument: {
453
- uri,
454
- languageId,
455
- version: 1,
456
- text: content,
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
- client.openFiles.set(uri, { version: 1, languageId });
461
- client.lastActivity = Date.now();
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 info = client.openFiles.get(uri);
521
+ const lockKey = `${client.name}:${uri}`;
471
522
 
472
- if (!info) {
473
- await ensureFileOpen(client, filePath);
474
- return;
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
- const content = fs.readFileSync(filePath, "utf-8");
478
- info.version++;
529
+ // Lock and refresh file
530
+ const refreshPromise = (async () => {
531
+ const info = client.openFiles.get(uri);
479
532
 
480
- await sendNotification(client, "textDocument/didChange", {
481
- textDocument: { uri, version: info.version },
482
- contentChanges: [{ text: content }],
483
- });
533
+ if (!info) {
534
+ await ensureFileOpen(client, filePath);
535
+ return;
536
+ }
484
537
 
485
- await sendNotification(client, "textDocument/didSave", {
486
- textDocument: { uri },
487
- text: content,
488
- });
538
+ const content = fs.readFileSync(filePath, "utf-8");
539
+ const version = ++info.version;
489
540
 
490
- client.lastActivity = Date.now();
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.js";
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.js";
4
- import { uriToFile } from "./utils.js";
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.js";
5
- import { resolveToCwd } from "../path-utils.js";
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.js";
15
- import { getServerForFile, getServersForFile, hasCapability, type LspConfig, loadConfig } from "./config.js";
16
- import { applyTextEdits, applyWorkspaceEdit } from "./edits.js";
17
- import { renderCall, renderResult } from "./render.js";
18
- import * as rustAnalyzer from "./rust-analyzer.js";
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.js";
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.js";
52
+ } from "./utils";
52
53
 
53
- export type { LspServerStatus } from "./client.js";
54
- export type { LspToolDetails } from "./types.js";
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 (e) {
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") + `\n... and ${lines.length - 50} more lines`, projectType };
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.js";
15
- import type { LspParams, LspToolDetails } from "./types.js";
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.js";
2
- import type { Diagnostic, ExpandMacroResult, LspClient, RelatedTest, Runnable, WorkspaceEdit } from "./types.js";
3
- import { fileToUri } from "./utils.js";
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.
@@ -8,7 +8,7 @@ import type {
8
8
  SymbolKind,
9
9
  TextEdit,
10
10
  WorkspaceEdit,
11
- } from "./types.js";
11
+ } from "./types";
12
12
 
13
13
  // =============================================================================
14
14
  // Language Detection
@@ -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.js";
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());
@@ -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 { spawnSync } from "child_process";
5
- import { constants } from "fs";
6
- import { access, readFile } from "fs/promises";
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"]);