@docyrus/docyrus 0.0.34 → 0.0.36

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 (66) hide show
  1. package/README.md +25 -0
  2. package/agent-loader.js +3 -2
  3. package/agent-loader.js.map +2 -2
  4. package/main.js +82162 -46093
  5. package/main.js.map +4 -4
  6. package/package.json +12 -3
  7. package/resources/chrome-tools/browser-content.js +46 -46
  8. package/resources/chrome-tools/browser-cookies.js +16 -16
  9. package/resources/chrome-tools/browser-eval.js +27 -27
  10. package/resources/chrome-tools/browser-hn-scraper.js +1 -1
  11. package/resources/chrome-tools/browser-nav.js +23 -23
  12. package/resources/chrome-tools/browser-pick.js +127 -127
  13. package/resources/chrome-tools/browser-screenshot.js +10 -10
  14. package/resources/chrome-tools/browser-start.js +38 -38
  15. package/resources/pi-agent/extensions/answer.ts +392 -384
  16. package/resources/pi-agent/extensions/context.ts +415 -415
  17. package/resources/pi-agent/extensions/control.ts +1287 -1287
  18. package/resources/pi-agent/extensions/diff.ts +171 -171
  19. package/resources/pi-agent/extensions/files.ts +155 -155
  20. package/resources/pi-agent/extensions/knowledge.ts +664 -0
  21. package/resources/pi-agent/extensions/loop.ts +375 -375
  22. package/resources/pi-agent/extensions/pi-bash-live-view/index.ts +1 -1
  23. package/resources/pi-agent/extensions/pi-bash-live-view/package.json +22 -22
  24. package/resources/pi-agent/extensions/pi-bash-live-view/pty-execute.ts +2 -2
  25. package/resources/pi-agent/extensions/pi-bash-live-view/pty-session.ts +2 -2
  26. package/resources/pi-agent/extensions/pi-bash-live-view/spawn-helper.ts +2 -2
  27. package/resources/pi-agent/extensions/pi-bash-live-view/terminal-emulator.ts +18 -18
  28. package/resources/pi-agent/extensions/pi-bash-live-view/truncate.ts +1 -1
  29. package/resources/pi-agent/extensions/pi-bash-live-view/widget.ts +4 -4
  30. package/resources/pi-agent/extensions/pi-custom-compaction/package.json +4 -4
  31. package/resources/pi-agent/extensions/pi-mcp-adapter/app-bridge.bundle.js +14 -14
  32. package/resources/pi-agent/extensions/pi-mcp-adapter/commands.ts +6 -6
  33. package/resources/pi-agent/extensions/pi-mcp-adapter/config.ts +9 -9
  34. package/resources/pi-agent/extensions/pi-mcp-adapter/consent-manager.ts +4 -4
  35. package/resources/pi-agent/extensions/pi-mcp-adapter/direct-tools.ts +13 -13
  36. package/resources/pi-agent/extensions/pi-mcp-adapter/glimpse-ui.ts +5 -5
  37. package/resources/pi-agent/extensions/pi-mcp-adapter/host-html-template.ts +13 -13
  38. package/resources/pi-agent/extensions/pi-mcp-adapter/index.ts +14 -14
  39. package/resources/pi-agent/extensions/pi-mcp-adapter/init.ts +17 -17
  40. package/resources/pi-agent/extensions/pi-mcp-adapter/lifecycle.ts +2 -2
  41. package/resources/pi-agent/extensions/pi-mcp-adapter/logger.ts +2 -2
  42. package/resources/pi-agent/extensions/pi-mcp-adapter/mcp-panel.ts +17 -17
  43. package/resources/pi-agent/extensions/pi-mcp-adapter/metadata-cache.ts +9 -9
  44. package/resources/pi-agent/extensions/pi-mcp-adapter/npx-resolver.ts +35 -35
  45. package/resources/pi-agent/extensions/pi-mcp-adapter/oauth-handler.ts +1 -1
  46. package/resources/pi-agent/extensions/pi-mcp-adapter/proxy-modes.ts +12 -12
  47. package/resources/pi-agent/extensions/pi-mcp-adapter/server-manager.ts +6 -6
  48. package/resources/pi-agent/extensions/pi-mcp-adapter/tool-metadata.ts +4 -4
  49. package/resources/pi-agent/extensions/pi-mcp-adapter/types.ts +2 -2
  50. package/resources/pi-agent/extensions/pi-mcp-adapter/ui-resource-handler.ts +6 -6
  51. package/resources/pi-agent/extensions/pi-mcp-adapter/ui-server.ts +17 -17
  52. package/resources/pi-agent/extensions/pi-mcp-adapter/ui-session.ts +22 -22
  53. package/resources/pi-agent/extensions/pi-mcp-adapter/utils.ts +2 -2
  54. package/resources/pi-agent/extensions/prompt-editor.ts +900 -900
  55. package/resources/pi-agent/extensions/prompt-url-widget.ts +122 -122
  56. package/resources/pi-agent/extensions/redraws.ts +14 -14
  57. package/resources/pi-agent/extensions/review.ts +1533 -1533
  58. package/resources/pi-agent/extensions/todos.ts +1735 -1735
  59. package/resources/pi-agent/extensions/tps.ts +40 -40
  60. package/resources/pi-agent/extensions/whimsical.ts +3 -3
  61. package/resources/pi-agent/prompts/agent-system.md +2 -0
  62. package/resources/pi-agent/prompts/coder-system.md +2 -0
  63. package/server-loader.js +82 -1
  64. package/server-loader.js.map +3 -3
  65. package/tui.mjs +2 -0
  66. package/tui.mjs.map +1 -1
@@ -64,33 +64,33 @@ const SESSION_MESSAGE_TYPE = "session-message";
64
64
  const SENDER_INFO_PATTERN = /<sender_info>[\s\S]*?<\/sender_info>/g;
65
65
 
66
66
  function expandUserPath(inputPath: string): string {
67
- if (inputPath === "~") return os.homedir();
68
- if (inputPath.startsWith("~/")) return path.join(os.homedir(), inputPath.slice(2));
69
- return inputPath;
67
+ if (inputPath === "~") {return os.homedir();}
68
+ if (inputPath.startsWith("~/")) {return path.join(os.homedir(), inputPath.slice(2));}
69
+ return inputPath;
70
70
  }
71
71
 
72
72
  function getScopedAgentDir(): string {
73
- const envCandidates = ["PI_CODING_AGENT_DIR", "TAU_CODING_AGENT_DIR"];
74
- for (const key of envCandidates) {
75
- const value = process.env[key]?.trim();
76
- if (value) {
77
- return expandUserPath(value);
78
- }
79
- }
80
-
81
- for (const [key, value] of Object.entries(process.env)) {
82
- if (!key.endsWith("_CODING_AGENT_DIR") || !value?.trim()) {
83
- continue;
84
- }
85
-
86
- return expandUserPath(value.trim());
87
- }
88
-
89
- return path.join(os.homedir(), ".pi", "agent");
73
+ const envCandidates = ["PI_CODING_AGENT_DIR", "TAU_CODING_AGENT_DIR"];
74
+ for (const key of envCandidates) {
75
+ const value = process.env[key]?.trim();
76
+ if (value) {
77
+ return expandUserPath(value);
78
+ }
79
+ }
80
+
81
+ for (const [key, value] of Object.entries(process.env)) {
82
+ if (!key.endsWith("_CODING_AGENT_DIR") || !value?.trim()) {
83
+ continue;
84
+ }
85
+
86
+ return expandUserPath(value.trim());
87
+ }
88
+
89
+ return path.join(os.homedir(), ".pi", "agent");
90
90
  }
91
91
 
92
92
  export function getControlDir(): string {
93
- return path.join(getScopedAgentDir(), "session-control");
93
+ return path.join(getScopedAgentDir(), "session-control");
94
94
  }
95
95
 
96
96
  const CONTROL_DIR = getControlDir();
@@ -195,25 +195,25 @@ const TURN_SUMMARY_PROMPT = `Summarize what happened in this conversation since
195
195
  Be concise but comprehensive. Preserve exact file paths, function names, and error messages.`;
196
196
 
197
197
  async function selectSummarizationModel(
198
- currentModel: Model<Api> | undefined,
199
- modelRegistry: {
198
+ currentModel: Model<Api> | undefined,
199
+ modelRegistry: {
200
200
  find: (provider: string, modelId: string) => Model<Api> | undefined;
201
201
  getApiKey: (model: Model<Api>) => Promise<string | undefined>;
202
202
  },
203
203
  ): Promise<Model<Api> | undefined> {
204
- const codexModel = modelRegistry.find("openai-codex", CODEX_MODEL_ID);
205
- if (codexModel) {
206
- const apiKey = await modelRegistry.getApiKey(codexModel);
207
- if (apiKey) return codexModel;
208
- }
209
-
210
- const haikuModel = modelRegistry.find("anthropic", HAIKU_MODEL_ID);
211
- if (haikuModel) {
212
- const apiKey = await modelRegistry.getApiKey(haikuModel);
213
- if (apiKey) return haikuModel;
214
- }
215
-
216
- return currentModel;
204
+ const codexModel = modelRegistry.find("openai-codex", CODEX_MODEL_ID);
205
+ if (codexModel) {
206
+ const apiKey = await modelRegistry.getApiKey(codexModel);
207
+ if (apiKey) {return codexModel;}
208
+ }
209
+
210
+ const haikuModel = modelRegistry.find("anthropic", HAIKU_MODEL_ID);
211
+ if (haikuModel) {
212
+ const apiKey = await modelRegistry.getApiKey(haikuModel);
213
+ if (apiKey) {return haikuModel;}
214
+ }
215
+
216
+ return currentModel;
217
217
  }
218
218
 
219
219
  // ============================================================================
@@ -223,155 +223,155 @@ async function selectSummarizationModel(
223
223
  const STATUS_KEY = "session-control";
224
224
 
225
225
  function isErrnoException(error: unknown): error is NodeJS.ErrnoException {
226
- return typeof error === "object" && error !== null && "code" in error;
226
+ return typeof error === "object" && error !== null && "code" in error;
227
227
  }
228
228
 
229
229
  function getSocketPath(sessionId: string): string {
230
- return path.join(CONTROL_DIR, `${sessionId}${SOCKET_SUFFIX}`);
230
+ return path.join(CONTROL_DIR, `${sessionId}${SOCKET_SUFFIX}`);
231
231
  }
232
232
 
233
233
  function isSafeSessionId(sessionId: string): boolean {
234
- return !sessionId.includes("/") && !sessionId.includes("\\") && !sessionId.includes("..") && sessionId.length > 0;
234
+ return !sessionId.includes("/") && !sessionId.includes("\\") && !sessionId.includes("..") && sessionId.length > 0;
235
235
  }
236
236
 
237
237
  function isSafeAlias(alias: string): boolean {
238
- return !alias.includes("/") && !alias.includes("\\") && !alias.includes("..") && alias.length > 0;
238
+ return !alias.includes("/") && !alias.includes("\\") && !alias.includes("..") && alias.length > 0;
239
239
  }
240
240
 
241
241
  function getAliasPath(alias: string): string {
242
- return path.join(CONTROL_DIR, `${alias}.alias`);
242
+ return path.join(CONTROL_DIR, `${alias}.alias`);
243
243
  }
244
244
 
245
245
  function getSessionAlias(ctx: ExtensionContext): string | null {
246
- const sessionName = ctx.sessionManager.getSessionName();
247
- const alias = sessionName ? sessionName.trim() : "";
248
- if (!alias || !isSafeAlias(alias)) return null;
249
- return alias;
246
+ const sessionName = ctx.sessionManager.getSessionName();
247
+ const alias = sessionName ? sessionName.trim() : "";
248
+ if (!alias || !isSafeAlias(alias)) {return null;}
249
+ return alias;
250
250
  }
251
251
 
252
252
  async function ensureControlDir(): Promise<void> {
253
- await fs.mkdir(CONTROL_DIR, { recursive: true });
253
+ await fs.mkdir(CONTROL_DIR, { recursive: true });
254
254
  }
255
255
 
256
256
  async function removeSocket(socketPath: string | null): Promise<void> {
257
- if (!socketPath) return;
258
- try {
259
- await fs.unlink(socketPath);
260
- } catch (error) {
261
- if (isErrnoException(error) && error.code !== "ENOENT") {
262
- throw error;
263
- }
264
- }
257
+ if (!socketPath) {return;}
258
+ try {
259
+ await fs.unlink(socketPath);
260
+ } catch (error) {
261
+ if (isErrnoException(error) && error.code !== "ENOENT") {
262
+ throw error;
263
+ }
264
+ }
265
265
  }
266
266
 
267
267
  // TODO: add GC for stale sockets/aliases older than 7 days.
268
268
  async function removeAliasesForSocket(socketPath: string | null): Promise<void> {
269
- if (!socketPath) return;
270
- try {
271
- const entries = await fs.readdir(CONTROL_DIR, { withFileTypes: true });
272
- for (const entry of entries) {
273
- if (!entry.isSymbolicLink()) continue;
274
- const aliasPath = path.join(CONTROL_DIR, entry.name);
275
- let target: string;
276
- try {
277
- target = await fs.readlink(aliasPath);
278
- } catch {
279
- continue;
280
- }
281
- const resolvedTarget = path.resolve(CONTROL_DIR, target);
282
- if (resolvedTarget === socketPath) {
283
- await fs.unlink(aliasPath);
284
- }
285
- }
286
- } catch (error) {
287
- if (isErrnoException(error) && error.code === "ENOENT") return;
288
- throw error;
289
- }
269
+ if (!socketPath) {return;}
270
+ try {
271
+ const entries = await fs.readdir(CONTROL_DIR, { withFileTypes: true });
272
+ for (const entry of entries) {
273
+ if (!entry.isSymbolicLink()) {continue;}
274
+ const aliasPath = path.join(CONTROL_DIR, entry.name);
275
+ let target: string;
276
+ try {
277
+ target = await fs.readlink(aliasPath);
278
+ } catch {
279
+ continue;
280
+ }
281
+ const resolvedTarget = path.resolve(CONTROL_DIR, target);
282
+ if (resolvedTarget === socketPath) {
283
+ await fs.unlink(aliasPath);
284
+ }
285
+ }
286
+ } catch (error) {
287
+ if (isErrnoException(error) && error.code === "ENOENT") {return;}
288
+ throw error;
289
+ }
290
290
  }
291
291
 
292
292
  async function createAliasSymlink(sessionId: string, alias: string): Promise<void> {
293
- if (!alias || !isSafeAlias(alias)) return;
294
- const aliasPath = getAliasPath(alias);
295
- const target = `${sessionId}${SOCKET_SUFFIX}`;
296
- try {
297
- await fs.unlink(aliasPath);
298
- } catch (error) {
299
- if (isErrnoException(error) && error.code !== "ENOENT") {
300
- throw error;
301
- }
302
- }
303
- try {
304
- await fs.symlink(target, aliasPath);
305
- } catch (error) {
306
- if (isErrnoException(error) && error.code !== "EEXIST") {
307
- throw error;
308
- }
309
- }
293
+ if (!alias || !isSafeAlias(alias)) {return;}
294
+ const aliasPath = getAliasPath(alias);
295
+ const target = `${sessionId}${SOCKET_SUFFIX}`;
296
+ try {
297
+ await fs.unlink(aliasPath);
298
+ } catch (error) {
299
+ if (isErrnoException(error) && error.code !== "ENOENT") {
300
+ throw error;
301
+ }
302
+ }
303
+ try {
304
+ await fs.symlink(target, aliasPath);
305
+ } catch (error) {
306
+ if (isErrnoException(error) && error.code !== "EEXIST") {
307
+ throw error;
308
+ }
309
+ }
310
310
  }
311
311
 
312
312
  async function resolveSessionIdFromAlias(alias: string): Promise<string | null> {
313
- if (!alias || !isSafeAlias(alias)) return null;
314
- const aliasPath = getAliasPath(alias);
315
- try {
316
- const target = await fs.readlink(aliasPath);
317
- const resolvedTarget = path.resolve(CONTROL_DIR, target);
318
- const base = path.basename(resolvedTarget);
319
- if (!base.endsWith(SOCKET_SUFFIX)) return null;
320
- const sessionId = base.slice(0, -SOCKET_SUFFIX.length);
321
- return isSafeSessionId(sessionId) ? sessionId : null;
322
- } catch (error) {
323
- if (isErrnoException(error) && error.code === "ENOENT") return null;
324
- return null;
325
- }
313
+ if (!alias || !isSafeAlias(alias)) {return null;}
314
+ const aliasPath = getAliasPath(alias);
315
+ try {
316
+ const target = await fs.readlink(aliasPath);
317
+ const resolvedTarget = path.resolve(CONTROL_DIR, target);
318
+ const base = path.basename(resolvedTarget);
319
+ if (!base.endsWith(SOCKET_SUFFIX)) {return null;}
320
+ const sessionId = base.slice(0, -SOCKET_SUFFIX.length);
321
+ return isSafeSessionId(sessionId) ? sessionId : null;
322
+ } catch (error) {
323
+ if (isErrnoException(error) && error.code === "ENOENT") {return null;}
324
+ return null;
325
+ }
326
326
  }
327
327
 
328
328
  async function getAliasMap(): Promise<Map<string, string[]>> {
329
- const aliasMap = new Map<string, string[]>();
330
- const entries = await fs.readdir(CONTROL_DIR, { withFileTypes: true });
331
- for (const entry of entries) {
332
- if (!entry.isSymbolicLink()) continue;
333
- if (!entry.name.endsWith(".alias")) continue;
334
- const aliasPath = path.join(CONTROL_DIR, entry.name);
335
- let target: string;
336
- try {
337
- target = await fs.readlink(aliasPath);
338
- } catch {
339
- continue;
340
- }
341
- const resolvedTarget = path.resolve(CONTROL_DIR, target);
342
- const aliases = aliasMap.get(resolvedTarget);
343
- const aliasName = entry.name.slice(0, -".alias".length);
344
- if (aliases) {
345
- aliases.push(aliasName);
346
- } else {
347
- aliasMap.set(resolvedTarget, [aliasName]);
348
- }
349
- }
350
- return aliasMap;
329
+ const aliasMap = new Map<string, string[]>();
330
+ const entries = await fs.readdir(CONTROL_DIR, { withFileTypes: true });
331
+ for (const entry of entries) {
332
+ if (!entry.isSymbolicLink()) {continue;}
333
+ if (!entry.name.endsWith(".alias")) {continue;}
334
+ const aliasPath = path.join(CONTROL_DIR, entry.name);
335
+ let target: string;
336
+ try {
337
+ target = await fs.readlink(aliasPath);
338
+ } catch {
339
+ continue;
340
+ }
341
+ const resolvedTarget = path.resolve(CONTROL_DIR, target);
342
+ const aliases = aliasMap.get(resolvedTarget);
343
+ const aliasName = entry.name.slice(0, -".alias".length);
344
+ if (aliases) {
345
+ aliases.push(aliasName);
346
+ } else {
347
+ aliasMap.set(resolvedTarget, [aliasName]);
348
+ }
349
+ }
350
+ return aliasMap;
351
351
  }
352
352
 
353
353
  async function isSocketAlive(socketPath: string): Promise<boolean> {
354
- return await new Promise((resolve) => {
355
- const socket = net.createConnection(socketPath);
356
- const timeout = setTimeout(() => {
357
- socket.destroy();
358
- resolve(false);
359
- }, 300);
360
-
361
- const cleanup = (alive: boolean) => {
362
- clearTimeout(timeout);
363
- socket.removeAllListeners();
364
- resolve(alive);
365
- };
366
-
367
- socket.once("connect", () => {
368
- socket.end();
369
- cleanup(true);
370
- });
371
- socket.once("error", () => {
372
- cleanup(false);
373
- });
374
- });
354
+ return await new Promise((resolve) => {
355
+ const socket = net.createConnection(socketPath);
356
+ const timeout = setTimeout(() => {
357
+ socket.destroy();
358
+ resolve(false);
359
+ }, 300);
360
+
361
+ const cleanup = (alive: boolean) => {
362
+ clearTimeout(timeout);
363
+ socket.removeAllListeners();
364
+ resolve(alive);
365
+ };
366
+
367
+ socket.once("connect", () => {
368
+ socket.end();
369
+ cleanup(true);
370
+ });
371
+ socket.once("error", () => {
372
+ cleanup(false);
373
+ });
374
+ });
375
375
  }
376
376
 
377
377
  type LiveSessionInfo = {
@@ -382,71 +382,71 @@ type LiveSessionInfo = {
382
382
  };
383
383
 
384
384
  async function getLiveSessions(): Promise<LiveSessionInfo[]> {
385
- await ensureControlDir();
386
- const entries = await fs.readdir(CONTROL_DIR, { withFileTypes: true });
387
- const aliasMap = await getAliasMap();
388
- const sessions: LiveSessionInfo[] = [];
389
-
390
- for (const entry of entries) {
391
- if (!entry.name.endsWith(SOCKET_SUFFIX)) continue;
392
- const socketPath = path.join(CONTROL_DIR, entry.name);
393
- const alive = await isSocketAlive(socketPath);
394
- if (!alive) continue;
395
- const sessionId = entry.name.slice(0, -SOCKET_SUFFIX.length);
396
- if (!isSafeSessionId(sessionId)) continue;
397
- const aliases = aliasMap.get(socketPath) ?? [];
398
- const name = aliases[0];
399
- sessions.push({ sessionId, name, aliases, socketPath });
400
- }
401
-
402
- sessions.sort((a, b) => (a.name ?? a.sessionId).localeCompare(b.name ?? b.sessionId));
403
- return sessions;
385
+ await ensureControlDir();
386
+ const entries = await fs.readdir(CONTROL_DIR, { withFileTypes: true });
387
+ const aliasMap = await getAliasMap();
388
+ const sessions: LiveSessionInfo[] = [];
389
+
390
+ for (const entry of entries) {
391
+ if (!entry.name.endsWith(SOCKET_SUFFIX)) {continue;}
392
+ const socketPath = path.join(CONTROL_DIR, entry.name);
393
+ const alive = await isSocketAlive(socketPath);
394
+ if (!alive) {continue;}
395
+ const sessionId = entry.name.slice(0, -SOCKET_SUFFIX.length);
396
+ if (!isSafeSessionId(sessionId)) {continue;}
397
+ const aliases = aliasMap.get(socketPath) ?? [];
398
+ const name = aliases[0];
399
+ sessions.push({ sessionId, name, aliases, socketPath });
400
+ }
401
+
402
+ sessions.sort((a, b) => (a.name ?? a.sessionId).localeCompare(b.name ?? b.sessionId));
403
+ return sessions;
404
404
  }
405
405
 
406
406
  async function syncAlias(state: SocketState, ctx: ExtensionContext): Promise<void> {
407
- if (!state.server || !state.socketPath) return;
408
- const alias = getSessionAlias(ctx);
409
- if (alias && alias !== state.alias) {
410
- await removeAliasesForSocket(state.socketPath);
411
- await createAliasSymlink(ctx.sessionManager.getSessionId(), alias);
412
- state.alias = alias;
413
- return;
414
- }
415
- if (!alias && state.alias) {
416
- await removeAliasesForSocket(state.socketPath);
417
- state.alias = null;
418
- }
407
+ if (!state.server || !state.socketPath) {return;}
408
+ const alias = getSessionAlias(ctx);
409
+ if (alias && alias !== state.alias) {
410
+ await removeAliasesForSocket(state.socketPath);
411
+ await createAliasSymlink(ctx.sessionManager.getSessionId(), alias);
412
+ state.alias = alias;
413
+ return;
414
+ }
415
+ if (!alias && state.alias) {
416
+ await removeAliasesForSocket(state.socketPath);
417
+ state.alias = null;
418
+ }
419
419
  }
420
420
 
421
421
  function writeResponse(socket: net.Socket, response: RpcResponse): void {
422
- try {
423
- socket.write(`${JSON.stringify(response)}\n`);
424
- } catch {
422
+ try {
423
+ socket.write(`${JSON.stringify(response)}\n`);
424
+ } catch {
425
425
  // Socket may be closed
426
- }
426
+ }
427
427
  }
428
428
 
429
429
  function writeEvent(socket: net.Socket, event: RpcEvent): void {
430
- try {
431
- socket.write(`${JSON.stringify(event)}\n`);
432
- } catch {
430
+ try {
431
+ socket.write(`${JSON.stringify(event)}\n`);
432
+ } catch {
433
433
  // Socket may be closed
434
- }
434
+ }
435
435
  }
436
436
 
437
437
  function parseCommand(line: string): { command?: RpcCommand; error?: string } {
438
- try {
439
- const parsed = JSON.parse(line) as RpcCommand;
440
- if (!parsed || typeof parsed !== "object") {
441
- return { error: "Invalid command" };
442
- }
443
- if (typeof parsed.type !== "string") {
444
- return { error: "Missing command type" };
445
- }
446
- return { command: parsed };
447
- } catch (error) {
448
- return { error: error instanceof Error ? error.message : "Failed to parse command" };
449
- }
438
+ try {
439
+ const parsed = JSON.parse(line) as RpcCommand;
440
+ if (!parsed || typeof parsed !== "object") {
441
+ return { error: "Invalid command" };
442
+ }
443
+ if (typeof parsed.type !== "string") {
444
+ return { error: "Missing command type" };
445
+ }
446
+ return { command: parsed };
447
+ } catch (error) {
448
+ return { error: error instanceof Error ? error.message : "Failed to parse command" };
449
+ }
450
450
  }
451
451
 
452
452
  // ============================================================================
@@ -460,83 +460,83 @@ interface ExtractedMessage {
460
460
  }
461
461
 
462
462
  function getLastAssistantMessage(ctx: ExtensionContext): ExtractedMessage | undefined {
463
- const branch = ctx.sessionManager.getBranch();
464
-
465
- for (let i = branch.length - 1; i >= 0; i--) {
466
- const entry = branch[i];
467
- if (entry.type === "message") {
468
- const msg = entry.message;
469
- if ("role" in msg && msg.role === "assistant") {
470
- const textParts = msg.content
471
- .filter((c): c is { type: "text"; text: string } => c.type === "text")
472
- .map((c) => c.text);
473
- if (textParts.length > 0) {
474
- return {
475
- role: "assistant",
476
- content: textParts.join("\n"),
477
- timestamp: msg.timestamp,
478
- };
479
- }
480
- }
481
- }
482
- }
483
- return undefined;
463
+ const branch = ctx.sessionManager.getBranch();
464
+
465
+ for (let i = branch.length - 1; i >= 0; i--) {
466
+ const entry = branch[i];
467
+ if (entry.type === "message") {
468
+ const msg = entry.message;
469
+ if ("role" in msg && msg.role === "assistant") {
470
+ const textParts = msg.content
471
+ .filter((c): c is { type: "text"; text: string } => c.type === "text")
472
+ .map((c) => c.text);
473
+ if (textParts.length > 0) {
474
+ return {
475
+ role: "assistant",
476
+ content: textParts.join("\n"),
477
+ timestamp: msg.timestamp,
478
+ };
479
+ }
480
+ }
481
+ }
482
+ }
483
+ return undefined;
484
484
  }
485
485
 
486
486
  function getMessagesSinceLastPrompt(ctx: ExtensionContext): ExtractedMessage[] {
487
- const branch = ctx.sessionManager.getBranch();
488
- const messages: ExtractedMessage[] = [];
489
-
490
- let lastUserIndex = -1;
491
- for (let i = branch.length - 1; i >= 0; i--) {
492
- const entry = branch[i];
493
- if (entry.type === "message" && "role" in entry.message && entry.message.role === "user") {
494
- lastUserIndex = i;
495
- break;
496
- }
497
- }
498
-
499
- if (lastUserIndex === -1) return [];
500
-
501
- for (let i = lastUserIndex; i < branch.length; i++) {
502
- const entry = branch[i];
503
- if (entry.type === "message") {
504
- const msg = entry.message;
505
- if ("role" in msg && (msg.role === "user" || msg.role === "assistant")) {
506
- const textParts = msg.content
507
- .filter((c): c is { type: "text"; text: string } => c.type === "text")
508
- .map((c) => c.text);
509
- if (textParts.length > 0) {
510
- messages.push({
511
- role: msg.role,
512
- content: textParts.join("\n"),
513
- timestamp: msg.timestamp,
514
- });
515
- }
516
- }
517
- }
518
- }
519
-
520
- return messages;
487
+ const branch = ctx.sessionManager.getBranch();
488
+ const messages: ExtractedMessage[] = [];
489
+
490
+ let lastUserIndex = -1;
491
+ for (let i = branch.length - 1; i >= 0; i--) {
492
+ const entry = branch[i];
493
+ if (entry.type === "message" && "role" in entry.message && entry.message.role === "user") {
494
+ lastUserIndex = i;
495
+ break;
496
+ }
497
+ }
498
+
499
+ if (lastUserIndex === -1) {return [];}
500
+
501
+ for (let i = lastUserIndex; i < branch.length; i++) {
502
+ const entry = branch[i];
503
+ if (entry.type === "message") {
504
+ const msg = entry.message;
505
+ if ("role" in msg && (msg.role === "user" || msg.role === "assistant")) {
506
+ const textParts = msg.content
507
+ .filter((c): c is { type: "text"; text: string } => c.type === "text")
508
+ .map((c) => c.text);
509
+ if (textParts.length > 0) {
510
+ messages.push({
511
+ role: msg.role,
512
+ content: textParts.join("\n"),
513
+ timestamp: msg.timestamp,
514
+ });
515
+ }
516
+ }
517
+ }
518
+ }
519
+
520
+ return messages;
521
521
  }
522
522
 
523
523
  function getFirstEntryId(ctx: ExtensionContext): string | undefined {
524
- const entries = ctx.sessionManager.getEntries();
525
- if (entries.length === 0) return undefined;
526
- const root = entries.find((e) => e.parentId === null);
527
- return root?.id ?? entries[0]?.id;
524
+ const entries = ctx.sessionManager.getEntries();
525
+ if (entries.length === 0) {return undefined;}
526
+ const root = entries.find((e) => e.parentId === null);
527
+ return root?.id ?? entries[0]?.id;
528
528
  }
529
529
 
530
530
  function extractTextContent(content: string | Array<TextContent | { type: string }>): string {
531
- if (typeof content === "string") return content;
532
- return content
533
- .filter((c): c is TextContent => c.type === "text")
534
- .map((c) => c.text)
535
- .join("\n");
531
+ if (typeof content === "string") {return content;}
532
+ return content
533
+ .filter((c): c is TextContent => c.type === "text")
534
+ .map((c) => c.text)
535
+ .join("\n");
536
536
  }
537
537
 
538
538
  function stripSenderInfo(text: string): string {
539
- return text.replace(SENDER_INFO_PATTERN, "").trim();
539
+ return text.replace(SENDER_INFO_PATTERN, "").trim();
540
540
  }
541
541
 
542
542
  interface SenderInfo {
@@ -545,69 +545,69 @@ interface SenderInfo {
545
545
  }
546
546
 
547
547
  function parseSenderInfo(text: string): SenderInfo | null {
548
- const match = text.match(/<sender_info>([\s\S]*?)<\/sender_info>/);
549
- if (!match) return null;
550
- const raw = match[1].trim();
551
- if (!raw) return null;
552
-
553
- if (raw.startsWith("{")) {
554
- try {
555
- const parsed = JSON.parse(raw) as { sessionId?: unknown; sessionName?: unknown };
556
- const sessionId = typeof parsed.sessionId === "string" ? parsed.sessionId.trim() : "";
557
- const sessionName = typeof parsed.sessionName === "string" ? parsed.sessionName.trim() : "";
558
- if (sessionId || sessionName) {
559
- return {
560
- sessionId: sessionId || undefined,
561
- sessionName: sessionName || undefined,
562
- };
563
- }
564
- } catch {
548
+ const match = text.match(/<sender_info>([\s\S]*?)<\/sender_info>/);
549
+ if (!match) {return null;}
550
+ const raw = match[1].trim();
551
+ if (!raw) {return null;}
552
+
553
+ if (raw.startsWith("{")) {
554
+ try {
555
+ const parsed = JSON.parse(raw) as { sessionId?: unknown; sessionName?: unknown };
556
+ const sessionId = typeof parsed.sessionId === "string" ? parsed.sessionId.trim() : "";
557
+ const sessionName = typeof parsed.sessionName === "string" ? parsed.sessionName.trim() : "";
558
+ if (sessionId || sessionName) {
559
+ return {
560
+ sessionId: sessionId || undefined,
561
+ sessionName: sessionName || undefined,
562
+ };
563
+ }
564
+ } catch {
565
565
  // Ignore JSON parse errors, fall back to legacy parsing.
566
- }
567
- }
566
+ }
567
+ }
568
568
 
569
- const legacyIdMatch = raw.match(/session\s+([a-f0-9-]{6,})/i);
570
- if (legacyIdMatch) {
571
- return { sessionId: legacyIdMatch[1] };
572
- }
569
+ const legacyIdMatch = raw.match(/session\s+([a-f0-9-]{6,})/i);
570
+ if (legacyIdMatch) {
571
+ return { sessionId: legacyIdMatch[1] };
572
+ }
573
573
 
574
- return null;
574
+ return null;
575
575
  }
576
576
 
577
577
  function formatSenderInfo(info: SenderInfo | null): string | null {
578
- if (!info) return null;
579
- const { sessionName, sessionId } = info;
580
- if (sessionName && sessionId) return `${sessionName} (${sessionId})`;
581
- if (sessionName) return sessionName;
582
- if (sessionId) return sessionId;
583
- return null;
578
+ if (!info) {return null;}
579
+ const { sessionName, sessionId } = info;
580
+ if (sessionName && sessionId) {return `${sessionName} (${sessionId})`;}
581
+ if (sessionName) {return sessionName;}
582
+ if (sessionId) {return sessionId;}
583
+ return null;
584
584
  }
585
585
 
586
586
  const renderSessionMessage: MessageRenderer = (message, { expanded }, theme) => {
587
- const rawContent = extractTextContent(message.content);
588
- const senderInfo = parseSenderInfo(rawContent);
589
- let text = stripSenderInfo(rawContent);
590
- if (!text) text = "(no content)";
591
-
592
- if (!expanded) {
593
- const lines = text.split("\n");
594
- if (lines.length > 5) {
595
- text = `${lines.slice(0, 5).join("\n")}\n...`;
596
- }
597
- }
598
-
599
- const box = new Box(1, 1, (t) => theme.bg("customMessageBg", t));
600
- const labelBase = theme.fg("customMessageLabel", `\x1b[1m[${message.customType}]\x1b[22m`);
601
- const senderText = formatSenderInfo(senderInfo);
602
- const label = senderText ? `${labelBase} ${theme.fg("dim", `from ${senderText}`)}` : labelBase;
603
- box.addChild(new Text(label, 0, 0));
604
- box.addChild(new Spacer(1));
605
- box.addChild(
606
- new Markdown(text, 0, 0, getMarkdownTheme(), {
607
- color: (value: string) => theme.fg("customMessageText", value),
608
- }),
609
- );
610
- return box;
587
+ const rawContent = extractTextContent(message.content);
588
+ const senderInfo = parseSenderInfo(rawContent);
589
+ let text = stripSenderInfo(rawContent);
590
+ if (!text) {text = "(no content)";}
591
+
592
+ if (!expanded) {
593
+ const lines = text.split("\n");
594
+ if (lines.length > 5) {
595
+ text = `${lines.slice(0, 5).join("\n")}\n...`;
596
+ }
597
+ }
598
+
599
+ const box = new Box(1, 1, (t) => theme.bg("customMessageBg", t));
600
+ const labelBase = theme.fg("customMessageLabel", `\x1b[1m[${message.customType}]\x1b[22m`);
601
+ const senderText = formatSenderInfo(senderInfo);
602
+ const label = senderText ? `${labelBase} ${theme.fg("dim", `from ${senderText}`)}` : labelBase;
603
+ box.addChild(new Text(label, 0, 0));
604
+ box.addChild(new Spacer(1));
605
+ box.addChild(
606
+ new Markdown(text, 0, 0, getMarkdownTheme(), {
607
+ color: (value: string) => theme.fg("customMessageText", value),
608
+ }),
609
+ );
610
+ return box;
611
611
  };
612
612
 
613
613
  // ============================================================================
@@ -615,187 +615,187 @@ const renderSessionMessage: MessageRenderer = (message, { expanded }, theme) =>
615
615
  // ============================================================================
616
616
 
617
617
  async function handleCommand(
618
- pi: ExtensionAPI,
619
- state: SocketState,
620
- command: RpcCommand,
621
- socket: net.Socket,
618
+ pi: ExtensionAPI,
619
+ state: SocketState,
620
+ command: RpcCommand,
621
+ socket: net.Socket,
622
622
  ): Promise<void> {
623
- const id = "id" in command && typeof command.id === "string" ? command.id : undefined;
624
- const respond = (success: boolean, commandName: string, data?: unknown, error?: string) => {
625
- if (state.context) {
626
- void syncAlias(state, state.context);
627
- }
628
- writeResponse(socket, { type: "response", command: commandName, success, data, error, id });
629
- };
630
-
631
- const ctx = state.context;
632
- if (!ctx) {
633
- respond(false, command.type, undefined, "Session not ready");
634
- return;
635
- }
636
-
637
- void syncAlias(state, ctx);
623
+ const id = "id" in command && typeof command.id === "string" ? command.id : undefined;
624
+ const respond = (success: boolean, commandName: string, data?: unknown, error?: string) => {
625
+ if (state.context) {
626
+ void syncAlias(state, state.context);
627
+ }
628
+ writeResponse(socket, { type: "response", command: commandName, success, data, error, id });
629
+ };
630
+
631
+ const ctx = state.context;
632
+ if (!ctx) {
633
+ respond(false, command.type, undefined, "Session not ready");
634
+ return;
635
+ }
636
+
637
+ void syncAlias(state, ctx);
638
638
 
639
639
  // Abort
640
- if (command.type === "abort") {
641
- ctx.abort();
642
- respond(true, "abort");
643
- return;
644
- }
640
+ if (command.type === "abort") {
641
+ ctx.abort();
642
+ respond(true, "abort");
643
+ return;
644
+ }
645
645
 
646
646
  // Subscribe to turn_end
647
- if (command.type === "subscribe") {
648
- if (command.event === "turn_end") {
649
- const subscriptionId = id ?? `sub_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
650
- state.turnEndSubscriptions.push({ socket, subscriptionId });
651
-
652
- const cleanup = () => {
653
- const idx = state.turnEndSubscriptions.findIndex((s) => s.subscriptionId === subscriptionId);
654
- if (idx !== -1) state.turnEndSubscriptions.splice(idx, 1);
655
- };
656
- socket.once("close", cleanup);
657
- socket.once("error", cleanup);
658
-
659
- respond(true, "subscribe", { subscriptionId, event: "turn_end" });
660
- return;
661
- }
662
- respond(false, "subscribe", undefined, `Unknown event type: ${command.event}`);
663
- return;
664
- }
647
+ if (command.type === "subscribe") {
648
+ if (command.event === "turn_end") {
649
+ const subscriptionId = id ?? `sub_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
650
+ state.turnEndSubscriptions.push({ socket, subscriptionId });
651
+
652
+ const cleanup = () => {
653
+ const idx = state.turnEndSubscriptions.findIndex((s) => s.subscriptionId === subscriptionId);
654
+ if (idx !== -1) {state.turnEndSubscriptions.splice(idx, 1);}
655
+ };
656
+ socket.once("close", cleanup);
657
+ socket.once("error", cleanup);
658
+
659
+ respond(true, "subscribe", { subscriptionId, event: "turn_end" });
660
+ return;
661
+ }
662
+ respond(false, "subscribe", undefined, `Unknown event type: ${command.event}`);
663
+ return;
664
+ }
665
665
 
666
666
  // Get last message
667
- if (command.type === "get_message") {
668
- const message = getLastAssistantMessage(ctx);
669
- if (!message) {
670
- respond(true, "get_message", { message: null });
671
- return;
672
- }
673
- respond(true, "get_message", { message });
674
- return;
675
- }
667
+ if (command.type === "get_message") {
668
+ const message = getLastAssistantMessage(ctx);
669
+ if (!message) {
670
+ respond(true, "get_message", { message: null });
671
+ return;
672
+ }
673
+ respond(true, "get_message", { message });
674
+ return;
675
+ }
676
676
 
677
677
  // Get summary
678
- if (command.type === "get_summary") {
679
- const messages = getMessagesSinceLastPrompt(ctx);
680
- if (messages.length === 0) {
681
- respond(false, "get_summary", undefined, "No messages to summarize");
682
- return;
683
- }
684
-
685
- const model = await selectSummarizationModel(ctx.model, ctx.modelRegistry);
686
- if (!model) {
687
- respond(false, "get_summary", undefined, "No model available for summarization");
688
- return;
689
- }
690
-
691
- const apiKey = await ctx.modelRegistry.getApiKey(model);
692
- if (!apiKey) {
693
- respond(false, "get_summary", undefined, "No API key available for summarization model");
694
- return;
695
- }
696
-
697
- try {
698
- const conversationText = messages
699
- .map((m) => `${m.role === "user" ? "User" : "Assistant"}: ${m.content}`)
700
- .join("\n\n");
701
-
702
- const userMessage: UserMessage = {
703
- role: "user",
704
- content: [{ type: "text", text: `<conversation>\n${conversationText}\n</conversation>\n\n${TURN_SUMMARY_PROMPT}` }],
705
- timestamp: Date.now(),
706
- };
707
-
708
- const response = await complete(
709
- model,
710
- { systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, messages: [userMessage] },
711
- { apiKey },
712
- );
713
-
714
- if (response.stopReason === "aborted" || response.stopReason === "error") {
715
- respond(false, "get_summary", undefined, "Summarization failed");
716
- return;
717
- }
718
-
719
- const summary = response.content
720
- .filter((c): c is { type: "text"; text: string } => c.type === "text")
721
- .map((c) => c.text)
722
- .join("\n");
723
-
724
- respond(true, "get_summary", { summary, model: model.id });
725
- } catch (error) {
726
- respond(false, "get_summary", undefined, error instanceof Error ? error.message : "Summarization failed");
727
- }
728
- return;
729
- }
678
+ if (command.type === "get_summary") {
679
+ const messages = getMessagesSinceLastPrompt(ctx);
680
+ if (messages.length === 0) {
681
+ respond(false, "get_summary", undefined, "No messages to summarize");
682
+ return;
683
+ }
684
+
685
+ const model = await selectSummarizationModel(ctx.model, ctx.modelRegistry);
686
+ if (!model) {
687
+ respond(false, "get_summary", undefined, "No model available for summarization");
688
+ return;
689
+ }
690
+
691
+ const apiKey = await ctx.modelRegistry.getApiKey(model);
692
+ if (!apiKey) {
693
+ respond(false, "get_summary", undefined, "No API key available for summarization model");
694
+ return;
695
+ }
696
+
697
+ try {
698
+ const conversationText = messages
699
+ .map((m) => `${m.role === "user" ? "User" : "Assistant"}: ${m.content}`)
700
+ .join("\n\n");
701
+
702
+ const userMessage: UserMessage = {
703
+ role: "user",
704
+ content: [{ type: "text", text: `<conversation>\n${conversationText}\n</conversation>\n\n${TURN_SUMMARY_PROMPT}` }],
705
+ timestamp: Date.now(),
706
+ };
707
+
708
+ const response = await complete(
709
+ model,
710
+ { systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, messages: [userMessage] },
711
+ { apiKey },
712
+ );
713
+
714
+ if (response.stopReason === "aborted" || response.stopReason === "error") {
715
+ respond(false, "get_summary", undefined, "Summarization failed");
716
+ return;
717
+ }
718
+
719
+ const summary = response.content
720
+ .filter((c): c is { type: "text"; text: string } => c.type === "text")
721
+ .map((c) => c.text)
722
+ .join("\n");
723
+
724
+ respond(true, "get_summary", { summary, model: model.id });
725
+ } catch (error) {
726
+ respond(false, "get_summary", undefined, error instanceof Error ? error.message : "Summarization failed");
727
+ }
728
+ return;
729
+ }
730
730
 
731
731
  // Clear session
732
- if (command.type === "clear") {
733
- if (!ctx.isIdle()) {
734
- respond(false, "clear", undefined, "Session is busy - wait for turn to complete");
735
- return;
736
- }
737
-
738
- const firstEntryId = getFirstEntryId(ctx);
739
- if (!firstEntryId) {
740
- respond(false, "clear", undefined, "No entries in session");
741
- return;
742
- }
743
-
744
- const currentLeafId = ctx.sessionManager.getLeafId();
745
- if (currentLeafId === firstEntryId) {
746
- respond(true, "clear", { cleared: true, alreadyAtRoot: true });
747
- return;
748
- }
749
-
750
- if (command.summarize) {
732
+ if (command.type === "clear") {
733
+ if (!ctx.isIdle()) {
734
+ respond(false, "clear", undefined, "Session is busy - wait for turn to complete");
735
+ return;
736
+ }
737
+
738
+ const firstEntryId = getFirstEntryId(ctx);
739
+ if (!firstEntryId) {
740
+ respond(false, "clear", undefined, "No entries in session");
741
+ return;
742
+ }
743
+
744
+ const currentLeafId = ctx.sessionManager.getLeafId();
745
+ if (currentLeafId === firstEntryId) {
746
+ respond(true, "clear", { cleared: true, alreadyAtRoot: true });
747
+ return;
748
+ }
749
+
750
+ if (command.summarize) {
751
751
  // Summarization requires navigateTree which we don't have direct access to
752
752
  // Return an error for now - the caller should clear without summarize
753
753
  // or use a different approach
754
- respond(false, "clear", undefined, "Clear with summarization not supported via RPC - use summarize=false");
755
- return;
756
- }
754
+ respond(false, "clear", undefined, "Clear with summarization not supported via RPC - use summarize=false");
755
+ return;
756
+ }
757
757
 
758
758
  // Access internal session manager to rewind (type assertion to access non-readonly methods)
759
- try {
760
- const sessionManager = ctx.sessionManager as unknown as { rewindTo(id: string): void };
761
- sessionManager.rewindTo(firstEntryId);
762
- respond(true, "clear", { cleared: true, targetId: firstEntryId });
763
- } catch (error) {
764
- respond(false, "clear", undefined, error instanceof Error ? error.message : "Clear failed");
765
- }
766
- return;
767
- }
759
+ try {
760
+ const sessionManager = ctx.sessionManager as unknown as { rewindTo(id: string): void };
761
+ sessionManager.rewindTo(firstEntryId);
762
+ respond(true, "clear", { cleared: true, targetId: firstEntryId });
763
+ } catch (error) {
764
+ respond(false, "clear", undefined, error instanceof Error ? error.message : "Clear failed");
765
+ }
766
+ return;
767
+ }
768
768
 
769
769
  // Send message
770
- if (command.type === "send") {
771
- const message = command.message;
772
- if (typeof message !== "string" || message.trim().length === 0) {
773
- respond(false, "send", undefined, "Missing message");
774
- return;
775
- }
776
-
777
- const mode = command.mode ?? "steer";
778
- const isIdle = ctx.isIdle();
779
- const customMessage = {
780
- customType: SESSION_MESSAGE_TYPE,
781
- content: message,
782
- display: true,
783
- };
784
-
785
- if (isIdle) {
786
- pi.sendMessage(customMessage, { triggerTurn: true });
787
- } else {
788
- pi.sendMessage(customMessage, {
789
- triggerTurn: true,
790
- deliverAs: mode === "follow_up" ? "followUp" : "steer",
791
- });
792
- }
793
-
794
- respond(true, "send", { delivered: true, mode: isIdle ? "direct" : mode });
795
- return;
796
- }
797
-
798
- respond(false, command.type, undefined, `Unsupported command: ${command.type}`);
770
+ if (command.type === "send") {
771
+ const message = command.message;
772
+ if (typeof message !== "string" || message.trim().length === 0) {
773
+ respond(false, "send", undefined, "Missing message");
774
+ return;
775
+ }
776
+
777
+ const mode = command.mode ?? "steer";
778
+ const isIdle = ctx.isIdle();
779
+ const customMessage = {
780
+ customType: SESSION_MESSAGE_TYPE,
781
+ content: message,
782
+ display: true,
783
+ };
784
+
785
+ if (isIdle) {
786
+ pi.sendMessage(customMessage, { triggerTurn: true });
787
+ } else {
788
+ pi.sendMessage(customMessage, {
789
+ triggerTurn: true,
790
+ deliverAs: mode === "follow_up" ? "followUp" : "steer",
791
+ });
792
+ }
793
+
794
+ respond(true, "send", { delivered: true, mode: isIdle ? "direct" : mode });
795
+ return;
796
+ }
797
+
798
+ respond(false, command.type, undefined, `Unsupported command: ${command.type}`);
799
799
  }
800
800
 
801
801
  // ============================================================================
@@ -803,47 +803,47 @@ async function handleCommand(
803
803
  // ============================================================================
804
804
 
805
805
  async function createServer(pi: ExtensionAPI, state: SocketState, socketPath: string): Promise<net.Server> {
806
- const server = net.createServer((socket) => {
807
- socket.setEncoding("utf8");
808
- let buffer = "";
809
- socket.on("data", (chunk) => {
810
- buffer += chunk;
811
- let newlineIndex = buffer.indexOf("\n");
812
- while (newlineIndex !== -1) {
813
- const line = buffer.slice(0, newlineIndex).trim();
814
- buffer = buffer.slice(newlineIndex + 1);
815
- newlineIndex = buffer.indexOf("\n");
816
- if (!line) continue;
817
-
818
- const parsed = parseCommand(line);
819
- if (parsed.error) {
820
- if (state.context) {
821
- void syncAlias(state, state.context);
822
- }
823
- writeResponse(socket, {
824
- type: "response",
825
- command: "parse",
826
- success: false,
827
- error: `Failed to parse command: ${parsed.error}`,
828
- });
829
- continue;
830
- }
831
-
832
- handleCommand(pi, state, parsed.command!, socket);
833
- }
834
- });
835
- });
806
+ const server = net.createServer((socket) => {
807
+ socket.setEncoding("utf8");
808
+ let buffer = "";
809
+ socket.on("data", (chunk) => {
810
+ buffer += chunk;
811
+ let newlineIndex = buffer.indexOf("\n");
812
+ while (newlineIndex !== -1) {
813
+ const line = buffer.slice(0, newlineIndex).trim();
814
+ buffer = buffer.slice(newlineIndex + 1);
815
+ newlineIndex = buffer.indexOf("\n");
816
+ if (!line) {continue;}
817
+
818
+ const parsed = parseCommand(line);
819
+ if (parsed.error) {
820
+ if (state.context) {
821
+ void syncAlias(state, state.context);
822
+ }
823
+ writeResponse(socket, {
824
+ type: "response",
825
+ command: "parse",
826
+ success: false,
827
+ error: `Failed to parse command: ${parsed.error}`,
828
+ });
829
+ continue;
830
+ }
831
+
832
+ handleCommand(pi, state, parsed.command!, socket);
833
+ }
834
+ });
835
+ });
836
836
 
837
837
  // Wait for server to start listening, with error handling
838
- await new Promise<void>((resolve, reject) => {
839
- server.once("error", reject);
840
- server.listen(socketPath, () => {
841
- server.removeListener("error", reject);
842
- resolve();
843
- });
844
- });
845
-
846
- return server;
838
+ await new Promise<void>((resolve, reject) => {
839
+ server.once("error", reject);
840
+ server.listen(socketPath, () => {
841
+ server.removeListener("error", reject);
842
+ resolve();
843
+ });
844
+ });
845
+
846
+ return server;
847
847
  }
848
848
 
849
849
  interface RpcClientOptions {
@@ -852,280 +852,280 @@ interface RpcClientOptions {
852
852
  }
853
853
 
854
854
  async function sendRpcCommand(
855
- socketPath: string,
856
- command: RpcCommand,
857
- options: RpcClientOptions = {},
855
+ socketPath: string,
856
+ command: RpcCommand,
857
+ options: RpcClientOptions = {},
858
858
  ): Promise<{ response: RpcResponse; event?: { message?: ExtractedMessage; turnIndex?: number } }> {
859
- const { timeout = 5000, waitForEvent } = options;
859
+ const { timeout = 5000, waitForEvent } = options;
860
860
 
861
- return new Promise((resolve, reject) => {
862
- const socket = net.createConnection(socketPath);
863
- socket.setEncoding("utf8");
861
+ return new Promise((resolve, reject) => {
862
+ const socket = net.createConnection(socketPath);
863
+ socket.setEncoding("utf8");
864
864
 
865
- const timeoutHandle = setTimeout(() => {
866
- socket.destroy(new Error("timeout"));
867
- }, timeout);
865
+ const timeoutHandle = setTimeout(() => {
866
+ socket.destroy(new Error("timeout"));
867
+ }, timeout);
868
868
 
869
- let buffer = "";
870
- let response: RpcResponse | null = null;
869
+ let buffer = "";
870
+ let response: RpcResponse | null = null;
871
871
 
872
- const cleanup = () => {
873
- clearTimeout(timeoutHandle);
874
- socket.removeAllListeners();
875
- };
872
+ const cleanup = () => {
873
+ clearTimeout(timeoutHandle);
874
+ socket.removeAllListeners();
875
+ };
876
876
 
877
- socket.on("connect", () => {
878
- socket.write(`${JSON.stringify(command)}\n`);
877
+ socket.on("connect", () => {
878
+ socket.write(`${JSON.stringify(command)}\n`);
879
879
 
880
880
  // If waiting for turn_end, also subscribe
881
- if (waitForEvent === "turn_end") {
882
- const subscribeCmd: RpcSubscribeCommand = { type: "subscribe", event: "turn_end" };
883
- socket.write(`${JSON.stringify(subscribeCmd)}\n`);
884
- }
885
- });
886
-
887
- socket.on("data", (chunk) => {
888
- buffer += chunk;
889
- let newlineIndex = buffer.indexOf("\n");
890
- while (newlineIndex !== -1) {
891
- const line = buffer.slice(0, newlineIndex).trim();
892
- buffer = buffer.slice(newlineIndex + 1);
893
- newlineIndex = buffer.indexOf("\n");
894
- if (!line) continue;
895
-
896
- try {
897
- const msg = JSON.parse(line);
881
+ if (waitForEvent === "turn_end") {
882
+ const subscribeCmd: RpcSubscribeCommand = { type: "subscribe", event: "turn_end" };
883
+ socket.write(`${JSON.stringify(subscribeCmd)}\n`);
884
+ }
885
+ });
886
+
887
+ socket.on("data", (chunk) => {
888
+ buffer += chunk;
889
+ let newlineIndex = buffer.indexOf("\n");
890
+ while (newlineIndex !== -1) {
891
+ const line = buffer.slice(0, newlineIndex).trim();
892
+ buffer = buffer.slice(newlineIndex + 1);
893
+ newlineIndex = buffer.indexOf("\n");
894
+ if (!line) {continue;}
895
+
896
+ try {
897
+ const msg = JSON.parse(line);
898
898
 
899
899
  // Handle response
900
- if (msg.type === "response") {
901
- if (msg.command === command.type) {
902
- response = msg;
900
+ if (msg.type === "response") {
901
+ if (msg.command === command.type) {
902
+ response = msg;
903
903
  // If not waiting for event, we're done
904
- if (!waitForEvent) {
905
- cleanup();
906
- socket.end();
907
- resolve({ response });
908
- return;
909
- }
910
- }
904
+ if (!waitForEvent) {
905
+ cleanup();
906
+ socket.end();
907
+ resolve({ response });
908
+ return;
909
+ }
910
+ }
911
911
  // Ignore subscribe response
912
- continue;
913
- }
912
+ continue;
913
+ }
914
914
 
915
915
  // Handle turn_end event
916
- if (msg.type === "event" && msg.event === "turn_end" && waitForEvent === "turn_end") {
917
- cleanup();
918
- socket.end();
919
- if (!response) {
920
- reject(new Error("Received event before response"));
921
- return;
922
- }
923
- resolve({ response, event: msg.data || {} });
924
- return;
925
- }
926
- } catch {
916
+ if (msg.type === "event" && msg.event === "turn_end" && waitForEvent === "turn_end") {
917
+ cleanup();
918
+ socket.end();
919
+ if (!response) {
920
+ reject(new Error("Received event before response"));
921
+ return;
922
+ }
923
+ resolve({ response, event: msg.data || {} });
924
+ return;
925
+ }
926
+ } catch {
927
927
  // Ignore parse errors, keep waiting
928
- }
929
- }
930
- });
931
-
932
- socket.on("error", (error) => {
933
- cleanup();
934
- reject(error);
935
- });
936
- });
928
+ }
929
+ }
930
+ });
931
+
932
+ socket.on("error", (error) => {
933
+ cleanup();
934
+ reject(error);
935
+ });
936
+ });
937
937
  }
938
938
 
939
939
  async function startControlServer(pi: ExtensionAPI, state: SocketState, ctx: ExtensionContext): Promise<void> {
940
- await ensureControlDir();
941
- const sessionId = ctx.sessionManager.getSessionId();
942
- const socketPath = getSocketPath(sessionId);
943
-
944
- if (state.socketPath === socketPath && state.server) {
945
- state.context = ctx;
946
- await syncAlias(state, ctx);
947
- return;
948
- }
949
-
950
- await stopControlServer(state);
951
- await removeSocket(socketPath);
952
-
953
- state.context = ctx;
954
- state.socketPath = socketPath;
955
- state.server = await createServer(pi, state, socketPath);
956
- state.alias = null;
957
- await syncAlias(state, ctx);
940
+ await ensureControlDir();
941
+ const sessionId = ctx.sessionManager.getSessionId();
942
+ const socketPath = getSocketPath(sessionId);
943
+
944
+ if (state.socketPath === socketPath && state.server) {
945
+ state.context = ctx;
946
+ await syncAlias(state, ctx);
947
+ return;
948
+ }
949
+
950
+ await stopControlServer(state);
951
+ await removeSocket(socketPath);
952
+
953
+ state.context = ctx;
954
+ state.socketPath = socketPath;
955
+ state.server = await createServer(pi, state, socketPath);
956
+ state.alias = null;
957
+ await syncAlias(state, ctx);
958
958
  }
959
959
 
960
960
  async function stopControlServer(state: SocketState): Promise<void> {
961
- if (!state.server) {
962
- await removeAliasesForSocket(state.socketPath);
963
- await removeSocket(state.socketPath);
964
- state.socketPath = null;
965
- state.alias = null;
966
- return;
967
- }
968
-
969
- const socketPath = state.socketPath;
970
- state.socketPath = null;
971
- state.turnEndSubscriptions = [];
972
- await new Promise<void>((resolve) => state.server?.close(() => resolve()));
973
- state.server = null;
974
- await removeAliasesForSocket(socketPath);
975
- await removeSocket(socketPath);
976
- state.alias = null;
961
+ if (!state.server) {
962
+ await removeAliasesForSocket(state.socketPath);
963
+ await removeSocket(state.socketPath);
964
+ state.socketPath = null;
965
+ state.alias = null;
966
+ return;
967
+ }
968
+
969
+ const socketPath = state.socketPath;
970
+ state.socketPath = null;
971
+ state.turnEndSubscriptions = [];
972
+ await new Promise<void>((resolve) => state.server?.close(() => resolve()));
973
+ state.server = null;
974
+ await removeAliasesForSocket(socketPath);
975
+ await removeSocket(socketPath);
976
+ state.alias = null;
977
977
  }
978
978
 
979
979
  function updateStatus(ctx: ExtensionContext | null, enabled: boolean): void {
980
- if (!ctx?.hasUI) return;
981
- if (!enabled) {
982
- ctx.ui.setStatus(STATUS_KEY, undefined);
983
- return;
984
- }
985
- const sessionId = ctx.sessionManager.getSessionId();
986
- ctx.ui.setStatus(STATUS_KEY, ctx.ui.theme.fg("dim", `session ${sessionId}`));
980
+ if (!ctx?.hasUI) {return;}
981
+ if (!enabled) {
982
+ ctx.ui.setStatus(STATUS_KEY, undefined);
983
+ return;
984
+ }
985
+ const sessionId = ctx.sessionManager.getSessionId();
986
+ ctx.ui.setStatus(STATUS_KEY, ctx.ui.theme.fg("dim", `session ${sessionId}`));
987
987
  }
988
988
 
989
989
  function updateSessionEnv(ctx: ExtensionContext | null, enabled: boolean): void {
990
- if (!enabled) {
991
- delete process.env.PI_SESSION_ID;
992
- return;
993
- }
994
- if (!ctx) return;
995
- process.env.PI_SESSION_ID = ctx.sessionManager.getSessionId();
990
+ if (!enabled) {
991
+ delete process.env.PI_SESSION_ID;
992
+ return;
993
+ }
994
+ if (!ctx) {return;}
995
+ process.env.PI_SESSION_ID = ctx.sessionManager.getSessionId();
996
996
  }
997
997
 
998
998
  // Extension factories run before extension flag values are hydrated into runtime.flagValues,
999
999
  // so we inspect argv directly when deciding whether to register tools at load time.
1000
1000
  function wasBooleanFlagPassed(flagName: string): boolean {
1001
- const flag = `--${flagName}`;
1002
- return process.argv.slice(2).includes(flag);
1001
+ const flag = `--${flagName}`;
1002
+ return process.argv.slice(2).includes(flag);
1003
1003
  }
1004
1004
 
1005
1005
  function shouldRegisterControlTools(pi: ExtensionAPI): boolean {
1006
- return pi.getFlag(CONTROL_FLAG) === true || wasBooleanFlagPassed(CONTROL_FLAG);
1006
+ return pi.getFlag(CONTROL_FLAG) === true || wasBooleanFlagPassed(CONTROL_FLAG);
1007
1007
  }
1008
1008
 
1009
1009
  // ============================================================================
1010
1010
  // Extension Export
1011
1011
  // ============================================================================
1012
1012
 
1013
- export default function (pi: ExtensionAPI) {
1014
- pi.registerFlag(CONTROL_FLAG, {
1015
- description: "Enable per-session control socket under the scoped agent directory",
1016
- type: "boolean",
1017
- });
1018
- pi.registerFlag(CONTROL_TARGET_FLAG, {
1019
- description: "Target session name or session id for startup control send",
1020
- type: "string",
1021
- });
1022
- pi.registerFlag(CONTROL_SEND_MESSAGE_FLAG, {
1023
- description: "Message to send to --control-session at startup",
1024
- type: "string",
1025
- });
1026
- pi.registerFlag(CONTROL_SEND_MODE_FLAG, {
1027
- description: "Startup send mode: steer or follow_up",
1028
- type: "string",
1029
- default: "steer",
1030
- });
1031
- pi.registerFlag(CONTROL_SEND_WAIT_FLAG, {
1032
- description: "Startup send wait mode: turn_end or message_processed",
1033
- type: "string",
1034
- });
1035
- pi.registerFlag(CONTROL_SEND_INCLUDE_SENDER_FLAG, {
1036
- description: "Include <sender_info> in startup messages (advanced; default: false)",
1037
- type: "boolean",
1038
- });
1039
-
1040
- let cliSendHandled = false;
1041
-
1042
- const state: SocketState = {
1043
- server: null,
1044
- socketPath: null,
1045
- context: null,
1046
- alias: null,
1047
- aliasTimer: null,
1048
- turnEndSubscriptions: [],
1049
- };
1050
-
1051
- pi.registerMessageRenderer(SESSION_MESSAGE_TYPE, renderSessionMessage);
1052
-
1053
- if (shouldRegisterControlTools(pi)) {
1054
- registerSessionTool(pi, state);
1055
- registerListSessionsTool(pi);
1056
- }
1057
- registerControlSessionsCommand(pi);
1058
-
1059
- const refreshServer = async (ctx: ExtensionContext) => {
1060
- const enabled = pi.getFlag(CONTROL_FLAG) === true;
1061
- if (!enabled) {
1062
- if (state.aliasTimer) {
1063
- clearInterval(state.aliasTimer);
1064
- state.aliasTimer = null;
1065
- }
1066
- await stopControlServer(state);
1067
- updateStatus(ctx, false);
1068
- updateSessionEnv(ctx, false);
1069
- return;
1070
- }
1071
- await startControlServer(pi, state, ctx);
1072
- if (!state.aliasTimer) {
1073
- state.aliasTimer = setInterval(() => {
1074
- if (!state.context) return;
1075
- void syncAlias(state, state.context);
1076
- }, 1000);
1077
- }
1078
- updateStatus(ctx, true);
1079
- updateSessionEnv(ctx, true);
1080
- };
1081
-
1082
- pi.on("session_start", async (_event, ctx) => {
1083
- await refreshServer(ctx);
1084
- if (!cliSendHandled) {
1085
- cliSendHandled = true;
1086
- await maybeHandleStartupControlSend(pi, ctx);
1087
- }
1088
- });
1089
-
1090
- pi.on("session_switch", async (_event, ctx) => {
1091
- await refreshServer(ctx);
1092
- });
1093
-
1094
- pi.on("session_fork", async (_event, ctx) => {
1095
- await refreshServer(ctx);
1096
- });
1097
-
1098
- pi.on("session_shutdown", async () => {
1099
- if (state.aliasTimer) {
1100
- clearInterval(state.aliasTimer);
1101
- state.aliasTimer = null;
1102
- }
1103
- updateStatus(state.context, false);
1104
- updateSessionEnv(state.context, false);
1105
- await stopControlServer(state);
1106
- });
1013
+ export default function(pi: ExtensionAPI) {
1014
+ pi.registerFlag(CONTROL_FLAG, {
1015
+ description: "Enable per-session control socket under the scoped agent directory",
1016
+ type: "boolean",
1017
+ });
1018
+ pi.registerFlag(CONTROL_TARGET_FLAG, {
1019
+ description: "Target session name or session id for startup control send",
1020
+ type: "string",
1021
+ });
1022
+ pi.registerFlag(CONTROL_SEND_MESSAGE_FLAG, {
1023
+ description: "Message to send to --control-session at startup",
1024
+ type: "string",
1025
+ });
1026
+ pi.registerFlag(CONTROL_SEND_MODE_FLAG, {
1027
+ description: "Startup send mode: steer or follow_up",
1028
+ type: "string",
1029
+ default: "steer",
1030
+ });
1031
+ pi.registerFlag(CONTROL_SEND_WAIT_FLAG, {
1032
+ description: "Startup send wait mode: turn_end or message_processed",
1033
+ type: "string",
1034
+ });
1035
+ pi.registerFlag(CONTROL_SEND_INCLUDE_SENDER_FLAG, {
1036
+ description: "Include <sender_info> in startup messages (advanced; default: false)",
1037
+ type: "boolean",
1038
+ });
1039
+
1040
+ let cliSendHandled = false;
1041
+
1042
+ const state: SocketState = {
1043
+ server: null,
1044
+ socketPath: null,
1045
+ context: null,
1046
+ alias: null,
1047
+ aliasTimer: null,
1048
+ turnEndSubscriptions: [],
1049
+ };
1050
+
1051
+ pi.registerMessageRenderer(SESSION_MESSAGE_TYPE, renderSessionMessage);
1052
+
1053
+ if (shouldRegisterControlTools(pi)) {
1054
+ registerSessionTool(pi, state);
1055
+ registerListSessionsTool(pi);
1056
+ }
1057
+ registerControlSessionsCommand(pi);
1058
+
1059
+ const refreshServer = async(ctx: ExtensionContext) => {
1060
+ const enabled = pi.getFlag(CONTROL_FLAG) === true;
1061
+ if (!enabled) {
1062
+ if (state.aliasTimer) {
1063
+ clearInterval(state.aliasTimer);
1064
+ state.aliasTimer = null;
1065
+ }
1066
+ await stopControlServer(state);
1067
+ updateStatus(ctx, false);
1068
+ updateSessionEnv(ctx, false);
1069
+ return;
1070
+ }
1071
+ await startControlServer(pi, state, ctx);
1072
+ if (!state.aliasTimer) {
1073
+ state.aliasTimer = setInterval(() => {
1074
+ if (!state.context) {return;}
1075
+ void syncAlias(state, state.context);
1076
+ }, 1000);
1077
+ }
1078
+ updateStatus(ctx, true);
1079
+ updateSessionEnv(ctx, true);
1080
+ };
1081
+
1082
+ pi.on("session_start", async(_event, ctx) => {
1083
+ await refreshServer(ctx);
1084
+ if (!cliSendHandled) {
1085
+ cliSendHandled = true;
1086
+ await maybeHandleStartupControlSend(pi, ctx);
1087
+ }
1088
+ });
1089
+
1090
+ pi.on("session_switch", async(_event, ctx) => {
1091
+ await refreshServer(ctx);
1092
+ });
1093
+
1094
+ pi.on("session_fork", async(_event, ctx) => {
1095
+ await refreshServer(ctx);
1096
+ });
1097
+
1098
+ pi.on("session_shutdown", async() => {
1099
+ if (state.aliasTimer) {
1100
+ clearInterval(state.aliasTimer);
1101
+ state.aliasTimer = null;
1102
+ }
1103
+ updateStatus(state.context, false);
1104
+ updateSessionEnv(state.context, false);
1105
+ await stopControlServer(state);
1106
+ });
1107
1107
 
1108
1108
  // Fire turn_end events to subscribers
1109
- pi.on("turn_end", (event: TurnEndEvent, ctx: ExtensionContext) => {
1110
- if (state.turnEndSubscriptions.length === 0) return;
1109
+ pi.on("turn_end", (event: TurnEndEvent, ctx: ExtensionContext) => {
1110
+ if (state.turnEndSubscriptions.length === 0) {return;}
1111
1111
 
1112
- void syncAlias(state, ctx);
1113
- const lastMessage = getLastAssistantMessage(ctx);
1114
- const eventData = { message: lastMessage, turnIndex: event.turnIndex };
1112
+ void syncAlias(state, ctx);
1113
+ const lastMessage = getLastAssistantMessage(ctx);
1114
+ const eventData = { message: lastMessage, turnIndex: event.turnIndex };
1115
1115
 
1116
1116
  // Fire to all subscribers (one-shot)
1117
- const subscriptions = [...state.turnEndSubscriptions];
1118
- state.turnEndSubscriptions = [];
1119
-
1120
- for (const sub of subscriptions) {
1121
- writeEvent(sub.socket, {
1122
- type: "event",
1123
- event: "turn_end",
1124
- data: eventData,
1125
- subscriptionId: sub.subscriptionId,
1126
- });
1127
- }
1128
- });
1117
+ const subscriptions = [...state.turnEndSubscriptions];
1118
+ state.turnEndSubscriptions = [];
1119
+
1120
+ for (const sub of subscriptions) {
1121
+ writeEvent(sub.socket, {
1122
+ type: "event",
1123
+ event: "turn_end",
1124
+ data: eventData,
1125
+ subscriptionId: sub.subscriptionId,
1126
+ });
1127
+ }
1128
+ });
1129
1129
  }
1130
1130
 
1131
1131
  // ============================================================================
@@ -1133,10 +1133,10 @@ export default function (pi: ExtensionAPI) {
1133
1133
  // ============================================================================
1134
1134
 
1135
1135
  function registerSessionTool(pi: ExtensionAPI, state: SocketState): void {
1136
- pi.registerTool({
1137
- name: "send_to_session",
1138
- label: "Send To Session",
1139
- description: `Interact with another running pi session via its control socket.
1136
+ pi.registerTool({
1137
+ name: "send_to_session",
1138
+ label: "Send To Session",
1139
+ description: `Interact with another running pi session via its control socket.
1140
1140
 
1141
1141
  Actions:
1142
1142
  - send: Send a message (default). Requires 'message' parameter.
@@ -1173,366 +1173,366 @@ CLI bridge (for shell scripts/background jobs):
1173
1173
  Note: If you ask the target session to reply back via sender_info, do not use wait_until; waiting is redundant and can duplicate responses.
1174
1174
 
1175
1175
  Messages automatically include sender session info for replies. When you want a response, instruct the target session to reply directly to the sender by calling send_to_session with the sender_info reference (do not poll get_message).`,
1176
- parameters: Type.Object({
1177
- sessionId: Type.Optional(Type.String({ description: "Target session id (UUID)" })),
1178
- sessionName: Type.Optional(Type.String({ description: "Target session name (alias)" })),
1179
- action: Type.Optional(
1180
- StringEnum(["send", "get_message", "get_summary", "clear"] as const, {
1181
- description: "Action to perform (default: send)",
1182
- default: "send",
1183
- }),
1184
- ),
1185
- message: Type.Optional(Type.String({ description: "Message to send (required for action=send)" })),
1186
- mode: Type.Optional(
1187
- StringEnum(["steer", "follow_up"] as const, {
1188
- description: "Delivery mode for send: steer (immediate) or follow_up (after task)",
1189
- default: "steer",
1190
- }),
1191
- ),
1192
- wait_until: Type.Optional(
1193
- StringEnum(["turn_end", "message_processed"] as const, {
1194
- description: "Wait behavior for send action",
1195
- }),
1196
- ),
1197
- }),
1198
- async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
1199
- const action = params.action ?? "send";
1200
- const sessionName = params.sessionName?.trim();
1201
- const sessionId = params.sessionId?.trim();
1202
- let targetSessionId: string | null = null;
1203
- const displayTarget = sessionName || sessionId || "";
1204
-
1205
- if (sessionName) {
1206
- targetSessionId = await resolveSessionIdFromAlias(sessionName);
1207
- if (!targetSessionId) {
1208
- return {
1209
- content: [{ type: "text", text: "Unknown session name" }],
1210
- isError: true,
1211
- details: { error: "Unknown session name" },
1212
- };
1213
- }
1214
- }
1215
-
1216
- if (sessionId) {
1217
- if (!isSafeSessionId(sessionId)) {
1218
- return {
1219
- content: [{ type: "text", text: "Invalid session id" }],
1220
- isError: true,
1221
- details: { error: "Invalid session id" },
1222
- };
1223
- }
1224
- if (targetSessionId && targetSessionId !== sessionId) {
1225
- return {
1226
- content: [{ type: "text", text: "Session name does not match session id" }],
1227
- isError: true,
1228
- details: { error: "Session name does not match session id" },
1229
- };
1230
- }
1231
- targetSessionId = sessionId;
1232
- }
1233
-
1234
- if (!targetSessionId) {
1235
- return {
1236
- content: [{ type: "text", text: "Missing session id or session name" }],
1237
- isError: true,
1238
- details: { error: "Missing session id or session name" },
1239
- };
1240
- }
1241
-
1242
- const socketPath = getSocketPath(targetSessionId);
1243
- const senderSessionId = state.context?.sessionManager.getSessionId();
1244
-
1245
- try {
1176
+ parameters: Type.Object({
1177
+ sessionId: Type.Optional(Type.String({ description: "Target session id (UUID)" })),
1178
+ sessionName: Type.Optional(Type.String({ description: "Target session name (alias)" })),
1179
+ action: Type.Optional(
1180
+ StringEnum(["send", "get_message", "get_summary", "clear"] as const, {
1181
+ description: "Action to perform (default: send)",
1182
+ default: "send",
1183
+ }),
1184
+ ),
1185
+ message: Type.Optional(Type.String({ description: "Message to send (required for action=send)" })),
1186
+ mode: Type.Optional(
1187
+ StringEnum(["steer", "follow_up"] as const, {
1188
+ description: "Delivery mode for send: steer (immediate) or follow_up (after task)",
1189
+ default: "steer",
1190
+ }),
1191
+ ),
1192
+ wait_until: Type.Optional(
1193
+ StringEnum(["turn_end", "message_processed"] as const, {
1194
+ description: "Wait behavior for send action",
1195
+ }),
1196
+ ),
1197
+ }),
1198
+ async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
1199
+ const action = params.action ?? "send";
1200
+ const sessionName = params.sessionName?.trim();
1201
+ const sessionId = params.sessionId?.trim();
1202
+ let targetSessionId: string | null = null;
1203
+ const displayTarget = sessionName || sessionId || "";
1204
+
1205
+ if (sessionName) {
1206
+ targetSessionId = await resolveSessionIdFromAlias(sessionName);
1207
+ if (!targetSessionId) {
1208
+ return {
1209
+ content: [{ type: "text", text: "Unknown session name" }],
1210
+ isError: true,
1211
+ details: { error: "Unknown session name" },
1212
+ };
1213
+ }
1214
+ }
1215
+
1216
+ if (sessionId) {
1217
+ if (!isSafeSessionId(sessionId)) {
1218
+ return {
1219
+ content: [{ type: "text", text: "Invalid session id" }],
1220
+ isError: true,
1221
+ details: { error: "Invalid session id" },
1222
+ };
1223
+ }
1224
+ if (targetSessionId && targetSessionId !== sessionId) {
1225
+ return {
1226
+ content: [{ type: "text", text: "Session name does not match session id" }],
1227
+ isError: true,
1228
+ details: { error: "Session name does not match session id" },
1229
+ };
1230
+ }
1231
+ targetSessionId = sessionId;
1232
+ }
1233
+
1234
+ if (!targetSessionId) {
1235
+ return {
1236
+ content: [{ type: "text", text: "Missing session id or session name" }],
1237
+ isError: true,
1238
+ details: { error: "Missing session id or session name" },
1239
+ };
1240
+ }
1241
+
1242
+ const socketPath = getSocketPath(targetSessionId);
1243
+ const senderSessionId = state.context?.sessionManager.getSessionId();
1244
+
1245
+ try {
1246
1246
  // Handle each action
1247
- if (action === "get_message") {
1248
- const result = await sendRpcCommand(socketPath, { type: "get_message" });
1249
- if (!result.response.success) {
1250
- return {
1251
- content: [{ type: "text", text: `Failed: ${result.response.error ?? "unknown error"}` }],
1252
- isError: true,
1253
- details: result,
1254
- };
1255
- }
1256
- const data = result.response.data as { message?: ExtractedMessage };
1257
- if (!data?.message) {
1258
- return {
1259
- content: [{ type: "text", text: "No assistant message found in session" }],
1260
- details: result,
1261
- };
1262
- }
1263
- return {
1264
- content: [{ type: "text", text: data.message.content }],
1265
- details: { message: data.message },
1266
- };
1267
- }
1268
-
1269
- if (action === "get_summary") {
1270
- const result = await sendRpcCommand(socketPath, { type: "get_summary" }, { timeout: 60000 });
1271
- if (!result.response.success) {
1272
- return {
1273
- content: [{ type: "text", text: `Failed: ${result.response.error ?? "unknown error"}` }],
1274
- isError: true,
1275
- details: result,
1276
- };
1277
- }
1278
- const data = result.response.data as { summary?: string; model?: string };
1279
- if (!data?.summary) {
1280
- return {
1281
- content: [{ type: "text", text: "No summary generated" }],
1282
- details: result,
1283
- };
1284
- }
1285
- return {
1286
- content: [{ type: "text", text: `Summary (via ${data.model}):\n\n${data.summary}` }],
1287
- details: { summary: data.summary, model: data.model },
1288
- };
1289
- }
1290
-
1291
- if (action === "clear") {
1292
- const result = await sendRpcCommand(socketPath, { type: "clear", summarize: false }, { timeout: 10000 });
1293
- if (!result.response.success) {
1294
- return {
1295
- content: [{ type: "text", text: `Failed to clear: ${result.response.error ?? "unknown error"}` }],
1296
- isError: true,
1297
- details: result,
1298
- };
1299
- }
1300
- const data = result.response.data as { cleared?: boolean; alreadyAtRoot?: boolean };
1301
- const msg = data?.alreadyAtRoot ? "Session already at root" : "Session cleared";
1302
- return {
1303
- content: [{ type: "text", text: msg }],
1304
- details: data,
1305
- };
1306
- }
1247
+ if (action === "get_message") {
1248
+ const result = await sendRpcCommand(socketPath, { type: "get_message" });
1249
+ if (!result.response.success) {
1250
+ return {
1251
+ content: [{ type: "text", text: `Failed: ${result.response.error ?? "unknown error"}` }],
1252
+ isError: true,
1253
+ details: result,
1254
+ };
1255
+ }
1256
+ const data = result.response.data as { message?: ExtractedMessage };
1257
+ if (!data?.message) {
1258
+ return {
1259
+ content: [{ type: "text", text: "No assistant message found in session" }],
1260
+ details: result,
1261
+ };
1262
+ }
1263
+ return {
1264
+ content: [{ type: "text", text: data.message.content }],
1265
+ details: { message: data.message },
1266
+ };
1267
+ }
1268
+
1269
+ if (action === "get_summary") {
1270
+ const result = await sendRpcCommand(socketPath, { type: "get_summary" }, { timeout: 60000 });
1271
+ if (!result.response.success) {
1272
+ return {
1273
+ content: [{ type: "text", text: `Failed: ${result.response.error ?? "unknown error"}` }],
1274
+ isError: true,
1275
+ details: result,
1276
+ };
1277
+ }
1278
+ const data = result.response.data as { summary?: string; model?: string };
1279
+ if (!data?.summary) {
1280
+ return {
1281
+ content: [{ type: "text", text: "No summary generated" }],
1282
+ details: result,
1283
+ };
1284
+ }
1285
+ return {
1286
+ content: [{ type: "text", text: `Summary (via ${data.model}):\n\n${data.summary}` }],
1287
+ details: { summary: data.summary, model: data.model },
1288
+ };
1289
+ }
1290
+
1291
+ if (action === "clear") {
1292
+ const result = await sendRpcCommand(socketPath, { type: "clear", summarize: false }, { timeout: 10000 });
1293
+ if (!result.response.success) {
1294
+ return {
1295
+ content: [{ type: "text", text: `Failed to clear: ${result.response.error ?? "unknown error"}` }],
1296
+ isError: true,
1297
+ details: result,
1298
+ };
1299
+ }
1300
+ const data = result.response.data as { cleared?: boolean; alreadyAtRoot?: boolean };
1301
+ const msg = data?.alreadyAtRoot ? "Session already at root" : "Session cleared";
1302
+ return {
1303
+ content: [{ type: "text", text: msg }],
1304
+ details: data,
1305
+ };
1306
+ }
1307
1307
 
1308
1308
  // action === "send"
1309
- if (!params.message || params.message.trim().length === 0) {
1310
- return {
1311
- content: [{ type: "text", text: "Missing message for send action" }],
1312
- isError: true,
1313
- details: { error: "Missing message" },
1314
- };
1315
- }
1316
-
1317
- const senderSessionName = state.context?.sessionManager.getSessionName()?.trim();
1318
- const senderInfo = senderSessionId
1319
- ? `\n\n<sender_info>${JSON.stringify({
1320
- sessionId: senderSessionId,
1321
- sessionName: senderSessionName || undefined,
1322
- })}</sender_info>`
1323
- : "";
1324
-
1325
- const sendCommand: RpcSendCommand = {
1326
- type: "send",
1327
- message: params.message + senderInfo,
1328
- mode: params.mode ?? "steer",
1329
- };
1309
+ if (!params.message || params.message.trim().length === 0) {
1310
+ return {
1311
+ content: [{ type: "text", text: "Missing message for send action" }],
1312
+ isError: true,
1313
+ details: { error: "Missing message" },
1314
+ };
1315
+ }
1316
+
1317
+ const senderSessionName = state.context?.sessionManager.getSessionName()?.trim();
1318
+ const senderInfo = senderSessionId
1319
+ ? `\n\n<sender_info>${JSON.stringify({
1320
+ sessionId: senderSessionId,
1321
+ sessionName: senderSessionName || undefined,
1322
+ })}</sender_info>`
1323
+ : "";
1324
+
1325
+ const sendCommand: RpcSendCommand = {
1326
+ type: "send",
1327
+ message: params.message + senderInfo,
1328
+ mode: params.mode ?? "steer",
1329
+ };
1330
1330
 
1331
1331
  // Determine wait behavior
1332
- if (params.wait_until === "message_processed") {
1332
+ if (params.wait_until === "message_processed") {
1333
1333
  // Just send and confirm delivery
1334
- const result = await sendRpcCommand(socketPath, sendCommand);
1335
- if (!result.response.success) {
1336
- return {
1337
- content: [{ type: "text", text: `Failed: ${result.response.error ?? "unknown error"}` }],
1338
- isError: true,
1339
- details: result,
1340
- };
1341
- }
1342
- return {
1343
- content: [{ type: "text", text: "Message delivered to session" }],
1344
- details: result.response.data,
1345
- };
1346
- }
1347
-
1348
- if (params.wait_until === "turn_end") {
1334
+ const result = await sendRpcCommand(socketPath, sendCommand);
1335
+ if (!result.response.success) {
1336
+ return {
1337
+ content: [{ type: "text", text: `Failed: ${result.response.error ?? "unknown error"}` }],
1338
+ isError: true,
1339
+ details: result,
1340
+ };
1341
+ }
1342
+ return {
1343
+ content: [{ type: "text", text: "Message delivered to session" }],
1344
+ details: result.response.data,
1345
+ };
1346
+ }
1347
+
1348
+ if (params.wait_until === "turn_end") {
1349
1349
  // Send and wait for turn to complete
1350
- const result = await sendRpcCommand(socketPath, sendCommand, {
1351
- timeout: 300000, // 5 minutes
1352
- waitForEvent: "turn_end",
1353
- });
1354
-
1355
- if (!result.response.success) {
1356
- return {
1357
- content: [{ type: "text", text: `Failed: ${result.response.error ?? "unknown error"}` }],
1358
- isError: true,
1359
- details: result,
1360
- };
1361
- }
1362
-
1363
- const lastMessage = result.event?.message;
1364
- if (!lastMessage) {
1365
- return {
1366
- content: [{ type: "text", text: "Turn completed but no assistant message found" }],
1367
- details: { turnIndex: result.event?.turnIndex },
1368
- };
1369
- }
1370
-
1371
- return {
1372
- content: [{ type: "text", text: lastMessage.content }],
1373
- details: { message: lastMessage, turnIndex: result.event?.turnIndex },
1374
- };
1375
- }
1350
+ const result = await sendRpcCommand(socketPath, sendCommand, {
1351
+ timeout: 300000, // 5 minutes
1352
+ waitForEvent: "turn_end",
1353
+ });
1354
+
1355
+ if (!result.response.success) {
1356
+ return {
1357
+ content: [{ type: "text", text: `Failed: ${result.response.error ?? "unknown error"}` }],
1358
+ isError: true,
1359
+ details: result,
1360
+ };
1361
+ }
1362
+
1363
+ const lastMessage = result.event?.message;
1364
+ if (!lastMessage) {
1365
+ return {
1366
+ content: [{ type: "text", text: "Turn completed but no assistant message found" }],
1367
+ details: { turnIndex: result.event?.turnIndex },
1368
+ };
1369
+ }
1370
+
1371
+ return {
1372
+ content: [{ type: "text", text: lastMessage.content }],
1373
+ details: { message: lastMessage, turnIndex: result.event?.turnIndex },
1374
+ };
1375
+ }
1376
1376
 
1377
1377
  // No wait - just send
1378
- const result = await sendRpcCommand(socketPath, sendCommand);
1379
- if (!result.response.success) {
1380
- return {
1381
- content: [{ type: "text", text: `Failed: ${result.response.error ?? "unknown error"}` }],
1382
- isError: true,
1383
- details: result,
1384
- };
1385
- }
1386
-
1387
- return {
1388
- content: [{ type: "text", text: `Message sent to session ${displayTarget || targetSessionId}` }],
1389
- details: result.response.data,
1390
- };
1391
- } catch (error) {
1392
- const message = error instanceof Error ? error.message : "Unknown error";
1393
- return {
1394
- content: [{ type: "text", text: `Failed: ${message}` }],
1395
- isError: true,
1396
- details: { error: message },
1397
- };
1398
- }
1399
- },
1400
-
1401
- renderCall(args, theme) {
1402
- const action = args.action ?? "send";
1403
- const sessionRef = args.sessionName ?? args.sessionId ?? "...";
1404
- const shortSessionRef = sessionRef.length > 12 ? sessionRef.slice(0, 8) + "..." : sessionRef;
1378
+ const result = await sendRpcCommand(socketPath, sendCommand);
1379
+ if (!result.response.success) {
1380
+ return {
1381
+ content: [{ type: "text", text: `Failed: ${result.response.error ?? "unknown error"}` }],
1382
+ isError: true,
1383
+ details: result,
1384
+ };
1385
+ }
1386
+
1387
+ return {
1388
+ content: [{ type: "text", text: `Message sent to session ${displayTarget || targetSessionId}` }],
1389
+ details: result.response.data,
1390
+ };
1391
+ } catch (error) {
1392
+ const message = error instanceof Error ? error.message : "Unknown error";
1393
+ return {
1394
+ content: [{ type: "text", text: `Failed: ${message}` }],
1395
+ isError: true,
1396
+ details: { error: message },
1397
+ };
1398
+ }
1399
+ },
1400
+
1401
+ renderCall(args, theme) {
1402
+ const action = args.action ?? "send";
1403
+ const sessionRef = args.sessionName ?? args.sessionId ?? "...";
1404
+ const shortSessionRef = sessionRef.length > 12 ? sessionRef.slice(0, 8) + "..." : sessionRef;
1405
1405
 
1406
1406
  // Build the header line
1407
- let header = theme.fg("toolTitle", theme.bold("→ session "));
1408
- header += theme.fg("accent", shortSessionRef);
1407
+ let header = theme.fg("toolTitle", theme.bold("→ session "));
1408
+ header += theme.fg("accent", shortSessionRef);
1409
1409
 
1410
1410
  // Add action-specific info
1411
- if (action === "send") {
1412
- const mode = args.mode ?? "steer";
1413
- const wait = args.wait_until;
1414
- let info = theme.fg("muted", ` (${mode}`);
1415
- if (wait) info += theme.fg("dim", `, wait: ${wait}`);
1416
- info += theme.fg("muted", ")");
1417
- header += info;
1418
- } else {
1419
- header += theme.fg("muted", ` (${action})`);
1420
- }
1411
+ if (action === "send") {
1412
+ const mode = args.mode ?? "steer";
1413
+ const wait = args.wait_until;
1414
+ let info = theme.fg("muted", ` (${mode}`);
1415
+ if (wait) {info += theme.fg("dim", `, wait: ${wait}`);}
1416
+ info += theme.fg("muted", ")");
1417
+ header += info;
1418
+ } else {
1419
+ header += theme.fg("muted", ` (${action})`);
1420
+ }
1421
1421
 
1422
1422
  // For send action, show the message
1423
- if (action === "send" && args.message) {
1424
- const msg = args.message;
1425
- const preview = msg.length > 80 ? msg.slice(0, 80) + "..." : msg;
1423
+ if (action === "send" && args.message) {
1424
+ const msg = args.message;
1425
+ const preview = msg.length > 80 ? msg.slice(0, 80) + "..." : msg;
1426
1426
  // Handle multi-line messages
1427
- const firstLine = preview.split("\n")[0];
1428
- const hasMore = preview.includes("\n") || msg.length > 80;
1429
- return new Text(
1430
- header + "\n " + theme.fg("dim", `"${firstLine}${hasMore ? "..." : ""}"`),
1431
- 0,
1432
- 0,
1433
- );
1434
- }
1435
-
1436
- return new Text(header, 0, 0);
1437
- },
1438
-
1439
- renderResult(result, { expanded }, theme) {
1440
- const details = result.details as Record<string, unknown> | undefined;
1441
- const isError = result.isError === true;
1427
+ const firstLine = preview.split("\n")[0];
1428
+ const hasMore = preview.includes("\n") || msg.length > 80;
1429
+ return new Text(
1430
+ header + "\n " + theme.fg("dim", `"${firstLine}${hasMore ? "..." : ""}"`),
1431
+ 0,
1432
+ 0,
1433
+ );
1434
+ }
1435
+
1436
+ return new Text(header, 0, 0);
1437
+ },
1438
+
1439
+ renderResult(result, { expanded }, theme) {
1440
+ const details = result.details as Record<string, unknown> | undefined;
1441
+ const isError = result.isError === true;
1442
1442
 
1443
1443
  // Error case
1444
- if (isError || details?.error) {
1445
- const errorMsg = (details?.error as string) || result.content[0]?.type === "text" ? (result.content[0] as { type: "text"; text: string }).text : "Unknown error";
1446
- return new Text(theme.fg("error", "✗ ") + theme.fg("error", errorMsg), 0, 0);
1447
- }
1444
+ if (isError || details?.error) {
1445
+ const errorMsg = (details?.error as string) || result.content[0]?.type === "text" ? (result.content[0] as { type: "text"; text: string }).text : "Unknown error";
1446
+ return new Text(theme.fg("error", "✗ ") + theme.fg("error", errorMsg), 0, 0);
1447
+ }
1448
1448
 
1449
1449
  // Detect action from details structure
1450
- const hasMessage = details && "message" in details && details.message;
1451
- const hasSummary = details && "summary" in details;
1452
- const hasCleared = details && "cleared" in details;
1453
- const hasTurnIndex = details && "turnIndex" in details;
1450
+ const hasMessage = details && "message" in details && details.message;
1451
+ const hasSummary = details && "summary" in details;
1452
+ const hasCleared = details && "cleared" in details;
1453
+ const hasTurnIndex = details && "turnIndex" in details;
1454
1454
 
1455
1455
  // get_message or turn_end result with message
1456
- if (hasMessage) {
1457
- const message = details.message as ExtractedMessage;
1458
- const icon = theme.fg("success", "✓");
1459
-
1460
- if (expanded) {
1461
- const container = new Container();
1462
- container.addChild(new Text(icon + theme.fg("muted", " Message received"), 0, 0));
1463
- container.addChild(new Spacer(1));
1464
- container.addChild(new Markdown(message.content, 0, 0, getMarkdownTheme()));
1465
- if (hasTurnIndex) {
1466
- container.addChild(new Spacer(1));
1467
- container.addChild(new Text(theme.fg("dim", `Turn #${details.turnIndex}`), 0, 0));
1468
- }
1469
- return container;
1470
- }
1456
+ if (hasMessage) {
1457
+ const message = details.message as ExtractedMessage;
1458
+ const icon = theme.fg("success", "✓");
1459
+
1460
+ if (expanded) {
1461
+ const container = new Container();
1462
+ container.addChild(new Text(icon + theme.fg("muted", " Message received"), 0, 0));
1463
+ container.addChild(new Spacer(1));
1464
+ container.addChild(new Markdown(message.content, 0, 0, getMarkdownTheme()));
1465
+ if (hasTurnIndex) {
1466
+ container.addChild(new Spacer(1));
1467
+ container.addChild(new Text(theme.fg("dim", `Turn #${details.turnIndex}`), 0, 0));
1468
+ }
1469
+ return container;
1470
+ }
1471
1471
 
1472
1472
  // Collapsed view - show preview
1473
- const preview = message.content.length > 200
1474
- ? message.content.slice(0, 200) + "..."
1475
- : message.content;
1476
- const lines = preview.split("\n").slice(0, 5);
1477
- let text = icon + theme.fg("muted", " Message received");
1478
- if (hasTurnIndex) text += theme.fg("dim", ` (turn #${details.turnIndex})`);
1479
- text += "\n" + theme.fg("toolOutput", lines.join("\n"));
1480
- if (message.content.split("\n").length > 5 || message.content.length > 200) {
1481
- text += "\n" + theme.fg("dim", "(Ctrl+O to expand)");
1482
- }
1483
- return new Text(text, 0, 0);
1484
- }
1473
+ const preview = message.content.length > 200
1474
+ ? message.content.slice(0, 200) + "..."
1475
+ : message.content;
1476
+ const lines = preview.split("\n").slice(0, 5);
1477
+ let text = icon + theme.fg("muted", " Message received");
1478
+ if (hasTurnIndex) {text += theme.fg("dim", ` (turn #${details.turnIndex})`);}
1479
+ text += "\n" + theme.fg("toolOutput", lines.join("\n"));
1480
+ if (message.content.split("\n").length > 5 || message.content.length > 200) {
1481
+ text += "\n" + theme.fg("dim", "(Ctrl+O to expand)");
1482
+ }
1483
+ return new Text(text, 0, 0);
1484
+ }
1485
1485
 
1486
1486
  // get_summary result
1487
- if (hasSummary) {
1488
- const summary = details.summary as string;
1489
- const model = details.model as string | undefined;
1490
- const icon = theme.fg("success", "✓");
1491
-
1492
- if (expanded) {
1493
- const container = new Container();
1494
- let header = icon + theme.fg("muted", " Summary");
1495
- if (model) header += theme.fg("dim", ` via ${model}`);
1496
- container.addChild(new Text(header, 0, 0));
1497
- container.addChild(new Spacer(1));
1498
- container.addChild(new Markdown(summary, 0, 0, getMarkdownTheme()));
1499
- return container;
1500
- }
1501
-
1502
- const preview = summary.length > 200 ? summary.slice(0, 200) + "..." : summary;
1503
- const lines = preview.split("\n").slice(0, 5);
1504
- let text = icon + theme.fg("muted", " Summary");
1505
- if (model) text += theme.fg("dim", ` via ${model}`);
1506
- text += "\n" + theme.fg("toolOutput", lines.join("\n"));
1507
- if (summary.split("\n").length > 5 || summary.length > 200) {
1508
- text += "\n" + theme.fg("dim", "(Ctrl+O to expand)");
1509
- }
1510
- return new Text(text, 0, 0);
1511
- }
1487
+ if (hasSummary) {
1488
+ const summary = details.summary as string;
1489
+ const model = details.model as string | undefined;
1490
+ const icon = theme.fg("success", "✓");
1491
+
1492
+ if (expanded) {
1493
+ const container = new Container();
1494
+ let header = icon + theme.fg("muted", " Summary");
1495
+ if (model) {header += theme.fg("dim", ` via ${model}`);}
1496
+ container.addChild(new Text(header, 0, 0));
1497
+ container.addChild(new Spacer(1));
1498
+ container.addChild(new Markdown(summary, 0, 0, getMarkdownTheme()));
1499
+ return container;
1500
+ }
1501
+
1502
+ const preview = summary.length > 200 ? summary.slice(0, 200) + "..." : summary;
1503
+ const lines = preview.split("\n").slice(0, 5);
1504
+ let text = icon + theme.fg("muted", " Summary");
1505
+ if (model) {text += theme.fg("dim", ` via ${model}`);}
1506
+ text += "\n" + theme.fg("toolOutput", lines.join("\n"));
1507
+ if (summary.split("\n").length > 5 || summary.length > 200) {
1508
+ text += "\n" + theme.fg("dim", "(Ctrl+O to expand)");
1509
+ }
1510
+ return new Text(text, 0, 0);
1511
+ }
1512
1512
 
1513
1513
  // clear result
1514
- if (hasCleared) {
1515
- const alreadyAtRoot = details.alreadyAtRoot as boolean | undefined;
1516
- const icon = theme.fg("success", "✓");
1517
- const msg = alreadyAtRoot ? "Session already at root" : "Session cleared";
1518
- return new Text(icon + " " + theme.fg("muted", msg), 0, 0);
1519
- }
1514
+ if (hasCleared) {
1515
+ const alreadyAtRoot = details.alreadyAtRoot as boolean | undefined;
1516
+ const icon = theme.fg("success", "✓");
1517
+ const msg = alreadyAtRoot ? "Session already at root" : "Session cleared";
1518
+ return new Text(icon + " " + theme.fg("muted", msg), 0, 0);
1519
+ }
1520
1520
 
1521
1521
  // send result (no wait or message_processed)
1522
- if (details && "delivered" in details) {
1523
- const mode = details.mode as string | undefined;
1524
- const icon = theme.fg("success", "✓");
1525
- let text = icon + theme.fg("muted", " Message delivered");
1526
- if (mode) text += theme.fg("dim", ` (${mode})`);
1527
- return new Text(text, 0, 0);
1528
- }
1522
+ if (details && "delivered" in details) {
1523
+ const mode = details.mode as string | undefined;
1524
+ const icon = theme.fg("success", "✓");
1525
+ let text = icon + theme.fg("muted", " Message delivered");
1526
+ if (mode) {text += theme.fg("dim", ` (${mode})`);}
1527
+ return new Text(text, 0, 0);
1528
+ }
1529
1529
 
1530
1530
  // Fallback - just show the text content
1531
- const text = result.content[0];
1532
- const content = text?.type === "text" ? text.text : "(no output)";
1533
- return new Text(theme.fg("success", "✓ ") + theme.fg("muted", content), 0, 0);
1534
- },
1535
- });
1531
+ const text = result.content[0];
1532
+ const content = text?.type === "text" ? text.text : "(no output)";
1533
+ return new Text(theme.fg("success", "✓ ") + theme.fg("muted", content), 0, 0);
1534
+ },
1535
+ });
1536
1536
  }
1537
1537
 
1538
1538
  // ============================================================================
@@ -1540,32 +1540,32 @@ Messages automatically include sender session info for replies. When you want a
1540
1540
  // ============================================================================
1541
1541
 
1542
1542
  function registerListSessionsTool(pi: ExtensionAPI): void {
1543
- pi.registerTool({
1544
- name: "list_sessions",
1545
- label: "List Sessions",
1546
- description: "List live sessions that expose a control socket (optionally with session names). Use this for discovery only; for the current session id in shell/bash use $PI_SESSION_ID.",
1547
- parameters: Type.Object({}),
1548
- async execute(_toolCallId, _params, _signal, _onUpdate, _ctx) {
1549
- const sessions = await getLiveSessions();
1550
-
1551
- if (sessions.length === 0) {
1552
- return {
1553
- content: [{ type: "text", text: "No live sessions found." }],
1554
- details: { sessions: [] },
1555
- };
1556
- }
1557
-
1558
- const lines = sessions.map((session) => {
1559
- const name = session.name ? ` (${session.name})` : "";
1560
- return `- ${session.sessionId}${name}`;
1561
- });
1562
-
1563
- return {
1564
- content: [{ type: "text", text: `Live sessions:\n${lines.join("\n")}` }],
1565
- details: { sessions },
1566
- };
1567
- },
1568
- });
1543
+ pi.registerTool({
1544
+ name: "list_sessions",
1545
+ label: "List Sessions",
1546
+ description: "List live sessions that expose a control socket (optionally with session names). Use this for discovery only; for the current session id in shell/bash use $PI_SESSION_ID.",
1547
+ parameters: Type.Object({}),
1548
+ async execute(_toolCallId, _params, _signal, _onUpdate, _ctx) {
1549
+ const sessions = await getLiveSessions();
1550
+
1551
+ if (sessions.length === 0) {
1552
+ return {
1553
+ content: [{ type: "text", text: "No live sessions found." }],
1554
+ details: { sessions: [] },
1555
+ };
1556
+ }
1557
+
1558
+ const lines = sessions.map((session) => {
1559
+ const name = session.name ? ` (${session.name})` : "";
1560
+ return `- ${session.sessionId}${name}`;
1561
+ });
1562
+
1563
+ return {
1564
+ content: [{ type: "text", text: `Live sessions:\n${lines.join("\n")}` }],
1565
+ details: { sessions },
1566
+ };
1567
+ },
1568
+ });
1569
1569
  }
1570
1570
 
1571
1571
  type StartupControlSendOptions = {
@@ -1577,203 +1577,203 @@ type StartupControlSendOptions = {
1577
1577
  };
1578
1578
 
1579
1579
  function normalizeMode(raw: string): "steer" | "follow_up" | null {
1580
- const value = raw.trim().toLowerCase();
1581
- if (value === "steer") return "steer";
1582
- if (value === "follow_up" || value === "follow-up" || value === "followup") return "follow_up";
1583
- return null;
1580
+ const value = raw.trim().toLowerCase();
1581
+ if (value === "steer") {return "steer";}
1582
+ if (value === "follow_up" || value === "follow-up" || value === "followup") {return "follow_up";}
1583
+ return null;
1584
1584
  }
1585
1585
 
1586
1586
  function normalizeWaitUntil(raw: string): "turn_end" | "message_processed" | null {
1587
- const value = raw.trim().toLowerCase();
1588
- if (value === "turn_end" || value === "turn-end") return "turn_end";
1589
- if (value === "message_processed" || value === "message-processed") return "message_processed";
1590
- return null;
1587
+ const value = raw.trim().toLowerCase();
1588
+ if (value === "turn_end" || value === "turn-end") {return "turn_end";}
1589
+ if (value === "message_processed" || value === "message-processed") {return "message_processed";}
1590
+ return null;
1591
1591
  }
1592
1592
 
1593
1593
  function getStringFlag(pi: ExtensionAPI, name: string): string | undefined {
1594
- const value = pi.getFlag(name);
1595
- if (typeof value !== "string") return undefined;
1596
- const trimmed = value.trim();
1597
- return trimmed.length > 0 ? trimmed : undefined;
1594
+ const value = pi.getFlag(name);
1595
+ if (typeof value !== "string") {return undefined;}
1596
+ const trimmed = value.trim();
1597
+ return trimmed.length > 0 ? trimmed : undefined;
1598
1598
  }
1599
1599
 
1600
1600
  function parseStartupControlSendOptions(pi: ExtensionAPI): { options?: StartupControlSendOptions; error?: string } {
1601
- const target = getStringFlag(pi, CONTROL_TARGET_FLAG);
1602
- const message = getStringFlag(pi, CONTROL_SEND_MESSAGE_FLAG);
1603
-
1604
- if (!target && !message) {
1605
- return {};
1606
- }
1607
- if (target && !message) {
1608
- return { error: `Missing --${CONTROL_SEND_MESSAGE_FLAG} (required with --${CONTROL_TARGET_FLAG})` };
1609
- }
1610
- if (!target && message) {
1611
- return { error: `Missing --${CONTROL_TARGET_FLAG} (required with --${CONTROL_SEND_MESSAGE_FLAG})` };
1612
- }
1613
-
1614
- const rawMode = getStringFlag(pi, CONTROL_SEND_MODE_FLAG) ?? "steer";
1615
- const mode = normalizeMode(rawMode);
1616
- if (!mode) {
1617
- return { error: `Invalid --${CONTROL_SEND_MODE_FLAG}: ${rawMode}. Use steer|follow_up.` };
1618
- }
1619
-
1620
- const rawWait = getStringFlag(pi, CONTROL_SEND_WAIT_FLAG);
1621
- let waitUntil: "turn_end" | "message_processed" | undefined;
1622
- if (rawWait) {
1623
- const normalized = normalizeWaitUntil(rawWait);
1624
- if (!normalized) {
1625
- return {
1626
- error: `Invalid --${CONTROL_SEND_WAIT_FLAG}: ${rawWait}. Use turn_end|message_processed.`,
1627
- };
1628
- }
1629
- waitUntil = normalized;
1630
- }
1631
-
1632
- const includeSenderInfo = pi.getFlag(CONTROL_SEND_INCLUDE_SENDER_FLAG) === true;
1633
-
1634
- return {
1635
- options: {
1636
- target: target!,
1637
- message: message!,
1638
- mode,
1639
- waitUntil,
1640
- includeSenderInfo,
1641
- },
1642
- };
1601
+ const target = getStringFlag(pi, CONTROL_TARGET_FLAG);
1602
+ const message = getStringFlag(pi, CONTROL_SEND_MESSAGE_FLAG);
1603
+
1604
+ if (!target && !message) {
1605
+ return {};
1606
+ }
1607
+ if (target && !message) {
1608
+ return { error: `Missing --${CONTROL_SEND_MESSAGE_FLAG} (required with --${CONTROL_TARGET_FLAG})` };
1609
+ }
1610
+ if (!target && message) {
1611
+ return { error: `Missing --${CONTROL_TARGET_FLAG} (required with --${CONTROL_SEND_MESSAGE_FLAG})` };
1612
+ }
1613
+
1614
+ const rawMode = getStringFlag(pi, CONTROL_SEND_MODE_FLAG) ?? "steer";
1615
+ const mode = normalizeMode(rawMode);
1616
+ if (!mode) {
1617
+ return { error: `Invalid --${CONTROL_SEND_MODE_FLAG}: ${rawMode}. Use steer|follow_up.` };
1618
+ }
1619
+
1620
+ const rawWait = getStringFlag(pi, CONTROL_SEND_WAIT_FLAG);
1621
+ let waitUntil: "turn_end" | "message_processed" | undefined;
1622
+ if (rawWait) {
1623
+ const normalized = normalizeWaitUntil(rawWait);
1624
+ if (!normalized) {
1625
+ return {
1626
+ error: `Invalid --${CONTROL_SEND_WAIT_FLAG}: ${rawWait}. Use turn_end|message_processed.`,
1627
+ };
1628
+ }
1629
+ waitUntil = normalized;
1630
+ }
1631
+
1632
+ const includeSenderInfo = pi.getFlag(CONTROL_SEND_INCLUDE_SENDER_FLAG) === true;
1633
+
1634
+ return {
1635
+ options: {
1636
+ target: target!,
1637
+ message: message!,
1638
+ mode,
1639
+ waitUntil,
1640
+ includeSenderInfo,
1641
+ },
1642
+ };
1643
1643
  }
1644
1644
 
1645
1645
  function reportStartupControlSend(ctx: ExtensionContext, message: string, level: "info" | "warning" | "error" = "info"): void {
1646
- if (ctx.hasUI) {
1647
- ctx.ui.notify(message, level);
1648
- return;
1649
- }
1650
- if (level === "error") {
1651
- console.error(message);
1652
- return;
1653
- }
1654
- console.log(message);
1646
+ if (ctx.hasUI) {
1647
+ ctx.ui.notify(message, level);
1648
+ return;
1649
+ }
1650
+ if (level === "error") {
1651
+ console.error(message);
1652
+ return;
1653
+ }
1654
+ console.log(message);
1655
1655
  }
1656
1656
 
1657
1657
  async function maybeHandleStartupControlSend(pi: ExtensionAPI, ctx: ExtensionContext): Promise<void> {
1658
- const parsed = parseStartupControlSendOptions(pi);
1659
- if (!parsed.options) {
1660
- if (parsed.error) {
1661
- reportStartupControlSend(ctx, parsed.error, "error");
1662
- }
1663
- return;
1664
- }
1665
-
1666
- const { target, message, mode, waitUntil, includeSenderInfo } = parsed.options;
1667
- let targetSessionId = await resolveSessionIdFromAlias(target);
1668
- if (!targetSessionId && isSafeSessionId(target)) {
1669
- targetSessionId = target;
1670
- }
1671
-
1672
- if (!targetSessionId) {
1673
- reportStartupControlSend(ctx, `Unknown target session: ${target}`, "error");
1674
- return;
1675
- }
1676
-
1677
- const socketPath = getSocketPath(targetSessionId);
1678
- const alive = await isSocketAlive(socketPath);
1679
- if (!alive) {
1680
- reportStartupControlSend(ctx, `Target session not reachable: ${target}`, "error");
1681
- return;
1682
- }
1683
-
1684
- const senderInfo = includeSenderInfo
1685
- ? (() => {
1686
- const senderSessionId = ctx.sessionManager.getSessionId();
1687
- const senderSessionName = ctx.sessionManager.getSessionName()?.trim();
1688
- return senderSessionId
1689
- ? `\n\n<sender_info>${JSON.stringify({
1690
- sessionId: senderSessionId,
1691
- sessionName: senderSessionName || undefined,
1692
- })}</sender_info>`
1693
- : "";
1694
- })()
1695
- : "";
1696
-
1697
- const sendCommand: RpcSendCommand = {
1698
- type: "send",
1699
- message: message + senderInfo,
1700
- mode,
1701
- };
1702
-
1703
- try {
1704
- if (waitUntil === "turn_end") {
1705
- const result = await sendRpcCommand(socketPath, sendCommand, {
1706
- timeout: 300000,
1707
- waitForEvent: "turn_end",
1708
- });
1709
- if (!result.response.success) {
1710
- reportStartupControlSend(ctx, `Failed to send: ${result.response.error ?? "unknown error"}`, "error");
1711
- return;
1712
- }
1713
- const lastMessage = result.event?.message;
1714
- if (!lastMessage?.content) {
1715
- reportStartupControlSend(ctx, `Message delivered to ${target}; turn completed without assistant output.`);
1716
- return;
1717
- }
1718
- if (ctx.hasUI) {
1719
- pi.sendMessage(
1720
- {
1721
- customType: "control-send",
1722
- content: `Startup response from ${target}:\n\n${lastMessage.content}`,
1723
- display: true,
1724
- },
1725
- { triggerTurn: false },
1726
- );
1727
- } else {
1728
- console.log(lastMessage.content);
1729
- }
1730
- return;
1731
- }
1732
-
1733
- const result = await sendRpcCommand(socketPath, sendCommand, { timeout: 30000 });
1734
- if (!result.response.success) {
1735
- reportStartupControlSend(ctx, `Failed to send: ${result.response.error ?? "unknown error"}`, "error");
1736
- return;
1737
- }
1738
-
1739
- const waitLabel = waitUntil === "message_processed" ? " (message processed)" : "";
1740
- reportStartupControlSend(ctx, `Message sent to ${target}${waitLabel}`);
1741
- } catch (error) {
1742
- const msg = error instanceof Error ? error.message : "unknown error";
1743
- reportStartupControlSend(ctx, `Failed to send to ${target}: ${msg}`, "error");
1744
- }
1658
+ const parsed = parseStartupControlSendOptions(pi);
1659
+ if (!parsed.options) {
1660
+ if (parsed.error) {
1661
+ reportStartupControlSend(ctx, parsed.error, "error");
1662
+ }
1663
+ return;
1664
+ }
1665
+
1666
+ const { target, message, mode, waitUntil, includeSenderInfo } = parsed.options;
1667
+ let targetSessionId = await resolveSessionIdFromAlias(target);
1668
+ if (!targetSessionId && isSafeSessionId(target)) {
1669
+ targetSessionId = target;
1670
+ }
1671
+
1672
+ if (!targetSessionId) {
1673
+ reportStartupControlSend(ctx, `Unknown target session: ${target}`, "error");
1674
+ return;
1675
+ }
1676
+
1677
+ const socketPath = getSocketPath(targetSessionId);
1678
+ const alive = await isSocketAlive(socketPath);
1679
+ if (!alive) {
1680
+ reportStartupControlSend(ctx, `Target session not reachable: ${target}`, "error");
1681
+ return;
1682
+ }
1683
+
1684
+ const senderInfo = includeSenderInfo
1685
+ ? (() => {
1686
+ const senderSessionId = ctx.sessionManager.getSessionId();
1687
+ const senderSessionName = ctx.sessionManager.getSessionName()?.trim();
1688
+ return senderSessionId
1689
+ ? `\n\n<sender_info>${JSON.stringify({
1690
+ sessionId: senderSessionId,
1691
+ sessionName: senderSessionName || undefined,
1692
+ })}</sender_info>`
1693
+ : "";
1694
+ })()
1695
+ : "";
1696
+
1697
+ const sendCommand: RpcSendCommand = {
1698
+ type: "send",
1699
+ message: message + senderInfo,
1700
+ mode,
1701
+ };
1702
+
1703
+ try {
1704
+ if (waitUntil === "turn_end") {
1705
+ const result = await sendRpcCommand(socketPath, sendCommand, {
1706
+ timeout: 300000,
1707
+ waitForEvent: "turn_end",
1708
+ });
1709
+ if (!result.response.success) {
1710
+ reportStartupControlSend(ctx, `Failed to send: ${result.response.error ?? "unknown error"}`, "error");
1711
+ return;
1712
+ }
1713
+ const lastMessage = result.event?.message;
1714
+ if (!lastMessage?.content) {
1715
+ reportStartupControlSend(ctx, `Message delivered to ${target}; turn completed without assistant output.`);
1716
+ return;
1717
+ }
1718
+ if (ctx.hasUI) {
1719
+ pi.sendMessage(
1720
+ {
1721
+ customType: "control-send",
1722
+ content: `Startup response from ${target}:\n\n${lastMessage.content}`,
1723
+ display: true,
1724
+ },
1725
+ { triggerTurn: false },
1726
+ );
1727
+ } else {
1728
+ console.log(lastMessage.content);
1729
+ }
1730
+ return;
1731
+ }
1732
+
1733
+ const result = await sendRpcCommand(socketPath, sendCommand, { timeout: 30000 });
1734
+ if (!result.response.success) {
1735
+ reportStartupControlSend(ctx, `Failed to send: ${result.response.error ?? "unknown error"}`, "error");
1736
+ return;
1737
+ }
1738
+
1739
+ const waitLabel = waitUntil === "message_processed" ? " (message processed)" : "";
1740
+ reportStartupControlSend(ctx, `Message sent to ${target}${waitLabel}`);
1741
+ } catch (error) {
1742
+ const msg = error instanceof Error ? error.message : "unknown error";
1743
+ reportStartupControlSend(ctx, `Failed to send to ${target}: ${msg}`, "error");
1744
+ }
1745
1745
  }
1746
1746
 
1747
1747
  function registerControlSessionsCommand(pi: ExtensionAPI): void {
1748
- pi.registerCommand("control-sessions", {
1749
- description: "List controllable sessions (from session-control sockets)",
1750
- handler: async (_args, ctx) => {
1751
- if (pi.getFlag(CONTROL_FLAG) !== true) {
1752
- if (ctx.hasUI) {
1753
- ctx.ui.notify("Session control not enabled (use --session-control)", "warning");
1754
- }
1755
- return;
1756
- }
1757
-
1758
- const sessions = await getLiveSessions();
1759
- const currentSessionId = ctx.sessionManager.getSessionId();
1760
- const lines = sessions.map((session) => {
1761
- const name = session.name ? ` (${session.name})` : "";
1762
- const current = session.sessionId === currentSessionId ? " (current)" : "";
1763
- return `- ${session.sessionId}${name}${current}`;
1764
- });
1765
- const content = sessions.length === 0
1766
- ? "No live sessions found."
1767
- : `Controllable sessions:\n${lines.join("\n")}`;
1768
-
1769
- pi.sendMessage(
1770
- {
1771
- customType: "control-sessions",
1772
- content,
1773
- display: true,
1774
- },
1775
- { triggerTurn: false },
1776
- );
1777
- },
1778
- });
1748
+ pi.registerCommand("control-sessions", {
1749
+ description: "List controllable sessions (from session-control sockets)",
1750
+ handler: async(_args, ctx) => {
1751
+ if (pi.getFlag(CONTROL_FLAG) !== true) {
1752
+ if (ctx.hasUI) {
1753
+ ctx.ui.notify("Session control not enabled (use --session-control)", "warning");
1754
+ }
1755
+ return;
1756
+ }
1757
+
1758
+ const sessions = await getLiveSessions();
1759
+ const currentSessionId = ctx.sessionManager.getSessionId();
1760
+ const lines = sessions.map((session) => {
1761
+ const name = session.name ? ` (${session.name})` : "";
1762
+ const current = session.sessionId === currentSessionId ? " (current)" : "";
1763
+ return `- ${session.sessionId}${name}${current}`;
1764
+ });
1765
+ const content = sessions.length === 0
1766
+ ? "No live sessions found."
1767
+ : `Controllable sessions:\n${lines.join("\n")}`;
1768
+
1769
+ pi.sendMessage(
1770
+ {
1771
+ customType: "control-sessions",
1772
+ content,
1773
+ display: true,
1774
+ },
1775
+ { triggerTurn: false },
1776
+ );
1777
+ },
1778
+ });
1779
1779
  }