@posthog/agent 1.29.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -38,24 +38,30 @@ interface ToolUpdate {
38
38
  locations?: ToolCallLocation[];
39
39
  }
40
40
 
41
+ interface ToolUse {
42
+ name: string;
43
+ input?: unknown;
44
+ }
45
+
41
46
  export function toolInfoFromToolUse(
42
- toolUse: any,
47
+ toolUse: ToolUse,
43
48
  cachedFileContent: { [key: string]: string },
44
49
  logger: Logger = new Logger({ debug: false, prefix: "[ClaudeTools]" }),
45
50
  ): ToolInfo {
46
51
  const name = toolUse.name;
47
- const input = toolUse.input;
52
+ // Cast input to allow property access - each case handles its expected properties
53
+ const input = toolUse.input as Record<string, unknown> | undefined;
48
54
 
49
55
  switch (name) {
50
56
  case "Task":
51
57
  return {
52
- title: input?.description ? input.description : "Task",
58
+ title: input?.description ? String(input.description) : "Task",
53
59
  kind: "think",
54
60
  content: input?.prompt
55
61
  ? [
56
62
  {
57
63
  type: "content",
58
- content: { type: "text", text: input.prompt },
64
+ content: { type: "text", text: String(input.prompt) },
59
65
  },
60
66
  ]
61
67
  : [],
@@ -64,42 +70,46 @@ export function toolInfoFromToolUse(
64
70
  case "NotebookRead":
65
71
  return {
66
72
  title: input?.notebook_path
67
- ? `Read Notebook ${input.notebook_path}`
73
+ ? `Read Notebook ${String(input.notebook_path)}`
68
74
  : "Read Notebook",
69
75
  kind: "read",
70
76
  content: [],
71
- locations: input?.notebook_path ? [{ path: input.notebook_path }] : [],
77
+ locations: input?.notebook_path
78
+ ? [{ path: String(input.notebook_path) }]
79
+ : [],
72
80
  };
73
81
 
74
82
  case "NotebookEdit":
75
83
  return {
76
84
  title: input?.notebook_path
77
- ? `Edit Notebook ${input.notebook_path}`
85
+ ? `Edit Notebook ${String(input.notebook_path)}`
78
86
  : "Edit Notebook",
79
87
  kind: "edit",
80
88
  content: input?.new_source
81
89
  ? [
82
90
  {
83
91
  type: "content",
84
- content: { type: "text", text: input.new_source },
92
+ content: { type: "text", text: String(input.new_source) },
85
93
  },
86
94
  ]
87
95
  : [],
88
- locations: input?.notebook_path ? [{ path: input.notebook_path }] : [],
96
+ locations: input?.notebook_path
97
+ ? [{ path: String(input.notebook_path) }]
98
+ : [],
89
99
  };
90
100
 
91
101
  case "Bash":
92
102
  case toolNames.bash:
93
103
  return {
94
104
  title: input?.command
95
- ? `\`${input.command.replaceAll("`", "\\`")}\``
105
+ ? `\`${String(input.command).replaceAll("`", "\\`")}\``
96
106
  : "Terminal",
97
107
  kind: "execute",
98
108
  content: input?.description
99
109
  ? [
100
110
  {
101
111
  type: "content",
102
- content: { type: "text", text: input.description },
112
+ content: { type: "text", text: String(input.description) },
103
113
  },
104
114
  ]
105
115
  : [],
@@ -123,24 +133,21 @@ export function toolInfoFromToolUse(
123
133
 
124
134
  case toolNames.read: {
125
135
  let limit = "";
126
- if (input.limit) {
127
- limit =
128
- " (" +
129
- ((input.offset ?? 0) + 1) +
130
- " - " +
131
- ((input.offset ?? 0) + input.limit) +
132
- ")";
133
- } else if (input.offset) {
134
- limit = ` (from line ${input.offset + 1})`;
136
+ const inputLimit = input?.limit as number | undefined;
137
+ const inputOffset = (input?.offset as number | undefined) ?? 0;
138
+ if (inputLimit) {
139
+ limit = ` (${inputOffset + 1} - ${inputOffset + inputLimit})`;
140
+ } else if (inputOffset) {
141
+ limit = ` (from line ${inputOffset + 1})`;
135
142
  }
136
143
  return {
137
- title: `Read ${input.file_path ?? "File"}${limit}`,
144
+ title: `Read ${input?.file_path ? String(input.file_path) : "File"}${limit}`,
138
145
  kind: "read",
139
- locations: input.file_path
146
+ locations: input?.file_path
140
147
  ? [
141
148
  {
142
- path: input.file_path,
143
- line: input.offset ?? 0,
149
+ path: String(input.file_path),
150
+ line: inputOffset,
144
151
  },
145
152
  ]
146
153
  : [],
@@ -153,11 +160,11 @@ export function toolInfoFromToolUse(
153
160
  title: "Read File",
154
161
  kind: "read",
155
162
  content: [],
156
- locations: input.file_path
163
+ locations: input?.file_path
157
164
  ? [
158
165
  {
159
- path: input.file_path,
160
- line: input.offset ?? 0,
166
+ path: String(input.file_path),
167
+ line: (input?.offset as number | undefined) ?? 0,
161
168
  },
162
169
  ]
163
170
  : [],
@@ -165,7 +172,7 @@ export function toolInfoFromToolUse(
165
172
 
166
173
  case "LS":
167
174
  return {
168
- title: `List the ${input?.path ? `\`${input.path}\`` : "current"} directory's contents`,
175
+ title: `List the ${input?.path ? `\`${String(input.path)}\`` : "current"} directory's contents`,
169
176
  kind: "search",
170
177
  content: [],
171
178
  locations: [],
@@ -173,9 +180,9 @@ export function toolInfoFromToolUse(
173
180
 
174
181
  case toolNames.edit:
175
182
  case "Edit": {
176
- const path = input?.file_path ?? input?.file_path;
177
- let oldText = input.old_string ?? null;
178
- let newText = input.new_string ?? "";
183
+ const path = input?.file_path ? String(input.file_path) : undefined;
184
+ let oldText = input?.old_string ? String(input.old_string) : null;
185
+ let newText = input?.new_string ? String(input.new_string) : "";
179
186
  let affectedLines: number[] = [];
180
187
 
181
188
  if (path && oldText) {
@@ -218,86 +225,92 @@ export function toolInfoFromToolUse(
218
225
  }
219
226
 
220
227
  case toolNames.write: {
221
- let content: ToolCallContent[] = [];
222
- if (input?.file_path) {
223
- content = [
228
+ let contentResult: ToolCallContent[] = [];
229
+ const filePath = input?.file_path ? String(input.file_path) : undefined;
230
+ const contentStr = input?.content ? String(input.content) : undefined;
231
+ if (filePath) {
232
+ contentResult = [
224
233
  {
225
234
  type: "diff",
226
- path: input.file_path,
235
+ path: filePath,
227
236
  oldText: null,
228
- newText: input.content,
237
+ newText: contentStr ?? "",
229
238
  },
230
239
  ];
231
- } else if (input?.content) {
232
- content = [
240
+ } else if (contentStr) {
241
+ contentResult = [
233
242
  {
234
243
  type: "content",
235
- content: { type: "text", text: input.content },
244
+ content: { type: "text", text: contentStr },
236
245
  },
237
246
  ];
238
247
  }
239
248
  return {
240
- title: input?.file_path ? `Write ${input.file_path}` : "Write",
249
+ title: filePath ? `Write ${filePath}` : "Write",
241
250
  kind: "edit",
242
- content,
243
- locations: input?.file_path ? [{ path: input.file_path }] : [],
251
+ content: contentResult,
252
+ locations: filePath ? [{ path: filePath }] : [],
244
253
  };
245
254
  }
246
255
 
247
- case "Write":
256
+ case "Write": {
257
+ const filePath = input?.file_path ? String(input.file_path) : undefined;
258
+ const contentStr = input?.content ? String(input.content) : "";
248
259
  return {
249
- title: input?.file_path ? `Write ${input.file_path}` : "Write",
260
+ title: filePath ? `Write ${filePath}` : "Write",
250
261
  kind: "edit",
251
- content: input?.file_path
262
+ content: filePath
252
263
  ? [
253
264
  {
254
265
  type: "diff",
255
- path: input.file_path,
266
+ path: filePath,
256
267
  oldText: null,
257
- newText: input.content,
268
+ newText: contentStr,
258
269
  },
259
270
  ]
260
271
  : [],
261
- locations: input?.file_path ? [{ path: input.file_path }] : [],
272
+ locations: filePath ? [{ path: filePath }] : [],
262
273
  };
274
+ }
263
275
 
264
276
  case "Glob": {
265
277
  let label = "Find";
266
- if (input.path) {
267
- label += ` \`${input.path}\``;
278
+ const pathStr = input?.path ? String(input.path) : undefined;
279
+ if (pathStr) {
280
+ label += ` \`${pathStr}\``;
268
281
  }
269
- if (input.pattern) {
270
- label += ` \`${input.pattern}\``;
282
+ if (input?.pattern) {
283
+ label += ` \`${String(input.pattern)}\``;
271
284
  }
272
285
  return {
273
286
  title: label,
274
287
  kind: "search",
275
288
  content: [],
276
- locations: input.path ? [{ path: input.path }] : [],
289
+ locations: pathStr ? [{ path: pathStr }] : [],
277
290
  };
278
291
  }
279
292
 
280
293
  case "Grep": {
281
294
  let label = "grep";
282
295
 
283
- if (input["-i"]) {
296
+ if (input?.["-i"]) {
284
297
  label += " -i";
285
298
  }
286
- if (input["-n"]) {
299
+ if (input?.["-n"]) {
287
300
  label += " -n";
288
301
  }
289
302
 
290
- if (input["-A"] !== undefined) {
303
+ if (input?.["-A"] !== undefined) {
291
304
  label += ` -A ${input["-A"]}`;
292
305
  }
293
- if (input["-B"] !== undefined) {
306
+ if (input?.["-B"] !== undefined) {
294
307
  label += ` -B ${input["-B"]}`;
295
308
  }
296
- if (input["-C"] !== undefined) {
309
+ if (input?.["-C"] !== undefined) {
297
310
  label += ` -C ${input["-C"]}`;
298
311
  }
299
312
 
300
- if (input.output_mode) {
313
+ if (input?.output_mode) {
301
314
  switch (input.output_mode) {
302
315
  case "FilesWithMatches":
303
316
  label += " -l";
@@ -310,26 +323,26 @@ export function toolInfoFromToolUse(
310
323
  }
311
324
  }
312
325
 
313
- if (input.head_limit !== undefined) {
326
+ if (input?.head_limit !== undefined) {
314
327
  label += ` | head -${input.head_limit}`;
315
328
  }
316
329
 
317
- if (input.glob) {
318
- label += ` --include="${input.glob}"`;
330
+ if (input?.glob) {
331
+ label += ` --include="${String(input.glob)}"`;
319
332
  }
320
333
 
321
- if (input.type) {
322
- label += ` --type=${input.type}`;
334
+ if (input?.type) {
335
+ label += ` --type=${String(input.type)}`;
323
336
  }
324
337
 
325
- if (input.multiline) {
338
+ if (input?.multiline) {
326
339
  label += " -P";
327
340
  }
328
341
 
329
- label += ` "${input.pattern}"`;
342
+ label += ` "${input?.pattern ? String(input.pattern) : ""}"`;
330
343
 
331
- if (input.path) {
332
- label += ` ${input.path}`;
344
+ if (input?.path) {
345
+ label += ` ${String(input.path)}`;
333
346
  }
334
347
 
335
348
  return {
@@ -341,27 +354,29 @@ export function toolInfoFromToolUse(
341
354
 
342
355
  case "WebFetch":
343
356
  return {
344
- title: input?.url ? `Fetch ${input.url}` : "Fetch",
357
+ title: input?.url ? `Fetch ${String(input.url)}` : "Fetch",
345
358
  kind: "fetch",
346
359
  content: input?.prompt
347
360
  ? [
348
361
  {
349
362
  type: "content",
350
- content: { type: "text", text: input.prompt },
363
+ content: { type: "text", text: String(input.prompt) },
351
364
  },
352
365
  ]
353
366
  : [],
354
367
  };
355
368
 
356
369
  case "WebSearch": {
357
- let label = `"${input.query}"`;
370
+ let label = `"${input?.query ? String(input.query) : ""}"`;
371
+ const allowedDomains = input?.allowed_domains as string[] | undefined;
372
+ const blockedDomains = input?.blocked_domains as string[] | undefined;
358
373
 
359
- if (input.allowed_domains && input.allowed_domains.length > 0) {
360
- label += ` (allowed: ${input.allowed_domains.join(", ")})`;
374
+ if (allowedDomains && allowedDomains.length > 0) {
375
+ label += ` (allowed: ${allowedDomains.join(", ")})`;
361
376
  }
362
377
 
363
- if (input.blocked_domains && input.blocked_domains.length > 0) {
364
- label += ` (blocked: ${input.blocked_domains.join(", ")})`;
378
+ if (blockedDomains && blockedDomains.length > 0) {
379
+ label += ` (blocked: ${blockedDomains.join(", ")})`;
365
380
  }
366
381
 
367
382
  return {
@@ -374,7 +389,7 @@ export function toolInfoFromToolUse(
374
389
  case "TodoWrite":
375
390
  return {
376
391
  title: Array.isArray(input?.todos)
377
- ? `Update TODOs: ${input.todos.map((todo: any) => todo.content).join(", ")}`
392
+ ? `Update TODOs: ${input.todos.map((todo: { content?: string }) => todo.content).join(", ")}`
378
393
  : "Update TODOs",
379
394
  kind: "think",
380
395
  content: [],
@@ -385,9 +400,35 @@ export function toolInfoFromToolUse(
385
400
  title: "Ready to code?",
386
401
  kind: "switch_mode",
387
402
  content: input?.plan
388
- ? [{ type: "content", content: { type: "text", text: input.plan } }]
403
+ ? [
404
+ {
405
+ type: "content",
406
+ content: { type: "text", text: String(input.plan) },
407
+ },
408
+ ]
409
+ : [],
410
+ };
411
+
412
+ case "AskUserQuestion": {
413
+ const questions = input?.questions as
414
+ | Array<{ question?: string }>
415
+ | undefined;
416
+ return {
417
+ title: questions?.[0]?.question || "Question",
418
+ kind: "ask" as ToolKind,
419
+ content: questions
420
+ ? [
421
+ {
422
+ type: "content",
423
+ content: {
424
+ type: "text",
425
+ text: JSON.stringify(questions, null, 2),
426
+ },
427
+ },
428
+ ]
389
429
  : [],
390
430
  };
431
+ }
391
432
 
392
433
  case "Other": {
393
434
  let output: string;
@@ -431,25 +472,32 @@ export function toolUpdateFromToolResult(
431
472
  | BetaTextEditorCodeExecutionToolResultBlockParam
432
473
  | BetaRequestMCPToolResultBlockParam
433
474
  | BetaToolSearchToolResultBlockParam,
434
- toolUse: any | undefined,
475
+ toolUse: ToolUse | undefined,
435
476
  ): ToolUpdate {
436
477
  switch (toolUse?.name) {
437
478
  case "Read":
438
479
  case toolNames.read:
439
480
  if (Array.isArray(toolResult.content) && toolResult.content.length > 0) {
440
481
  return {
441
- content: toolResult.content.map((content: any) => ({
442
- type: "content",
443
- content:
444
- content.type === "text"
445
- ? {
446
- type: "text",
447
- text: markdownEscape(
448
- content.text.replace(SYSTEM_REMINDER, ""),
449
- ),
450
- }
451
- : content,
452
- })),
482
+ content: toolResult.content.map((item) => {
483
+ const itemObj = item as { type?: string; text?: string };
484
+ if (itemObj.type === "text") {
485
+ return {
486
+ type: "content" as const,
487
+ content: {
488
+ type: "text" as const,
489
+ text: markdownEscape(
490
+ (itemObj.text ?? "").replace(SYSTEM_REMINDER, ""),
491
+ ),
492
+ },
493
+ };
494
+ }
495
+ // For non-text content, return as-is with proper typing
496
+ return {
497
+ type: "content" as const,
498
+ content: item as { type: "text"; text: string },
499
+ };
500
+ }),
453
501
  };
454
502
  } else if (
455
503
  typeof toolResult.content === "string" &&
@@ -492,6 +540,29 @@ export function toolUpdateFromToolResult(
492
540
  case "ExitPlanMode": {
493
541
  return { title: "Exited Plan Mode" };
494
542
  }
543
+ case "AskUserQuestion": {
544
+ // The answer is returned in the tool result
545
+ const content = toolResult.content;
546
+ if (Array.isArray(content) && content.length > 0) {
547
+ const firstItem = content[0];
548
+ if (
549
+ typeof firstItem === "object" &&
550
+ firstItem !== null &&
551
+ "text" in firstItem
552
+ ) {
553
+ return {
554
+ title: "Answer received",
555
+ content: [
556
+ {
557
+ type: "content",
558
+ content: { type: "text", text: String(firstItem.text) },
559
+ },
560
+ ],
561
+ };
562
+ }
563
+ }
564
+ return { title: "Question answered" };
565
+ }
495
566
  default: {
496
567
  return toAcpContentUpdate(
497
568
  toolResult.content,
@@ -502,21 +573,27 @@ export function toolUpdateFromToolResult(
502
573
  }
503
574
 
504
575
  function toAcpContentUpdate(
505
- content: any,
576
+ content: unknown,
506
577
  isError: boolean = false,
507
578
  ): { content?: ToolCallContent[] } {
508
579
  if (Array.isArray(content) && content.length > 0) {
509
580
  return {
510
- content: content.map((content: any) => ({
511
- type: "content",
512
- content:
513
- isError && content.type === "text"
514
- ? {
515
- ...content,
516
- text: `\`\`\`\n${content.text}\n\`\`\``,
517
- }
518
- : content,
519
- })),
581
+ content: content.map((item) => {
582
+ const itemObj = item as { type?: string; text?: string };
583
+ if (isError && itemObj.type === "text") {
584
+ return {
585
+ type: "content" as const,
586
+ content: {
587
+ type: "text" as const,
588
+ text: `\`\`\`\n${itemObj.text ?? ""}\n\`\`\``,
589
+ },
590
+ };
591
+ }
592
+ return {
593
+ type: "content" as const,
594
+ content: item as { type: "text"; text: string },
595
+ };
596
+ }),
520
597
  };
521
598
  } else if (typeof content === "string" && content.length > 0) {
522
599
  return {
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Shared ACP connection factory.
3
+ *
4
+ * Creates ACP connections for the Claude Code agent.
5
+ */
6
+
7
+ import { AgentSideConnection, ndJsonStream } from "@agentclientprotocol/sdk";
8
+ import type { SessionStore } from "@/session-store.js";
9
+ import { Logger } from "@/utils/logger.js";
10
+ import { createTappedWritableStream } from "@/utils/tapped-stream.js";
11
+ import { ClaudeAcpAgent } from "./claude/claude.js";
12
+ import { createBidirectionalStreams, type StreamPair } from "./claude/utils.js";
13
+
14
+ export type AgentFramework = "claude";
15
+
16
+ export type AcpConnectionConfig = {
17
+ framework?: AgentFramework;
18
+ sessionStore?: SessionStore;
19
+ sessionId?: string;
20
+ taskId?: string;
21
+ };
22
+
23
+ export type InProcessAcpConnection = {
24
+ agentConnection: AgentSideConnection;
25
+ clientStreams: StreamPair;
26
+ };
27
+
28
+ /**
29
+ * Creates an ACP connection with the specified agent framework.
30
+ *
31
+ * @param config - Configuration including framework selection
32
+ * @returns Connection with agent and client streams
33
+ */
34
+ export function createAcpConnection(
35
+ config: AcpConnectionConfig = {},
36
+ ): InProcessAcpConnection {
37
+ const logger = new Logger({ debug: true, prefix: "[AcpConnection]" });
38
+ const streams = createBidirectionalStreams();
39
+
40
+ const { sessionStore, framework = "claude" } = config;
41
+
42
+ // Tap both streams for automatic persistence
43
+ // All messages (bidirectional) will be persisted as they flow through
44
+ let agentWritable = streams.agent.writable;
45
+ let clientWritable = streams.client.writable;
46
+
47
+ if (config.sessionId && sessionStore) {
48
+ // Register session for persistence BEFORE tapping streams
49
+ // This ensures all messages from the start get persisted
50
+ if (!sessionStore.isRegistered(config.sessionId)) {
51
+ sessionStore.register(config.sessionId, {
52
+ taskId: config.taskId ?? config.sessionId,
53
+ runId: config.sessionId,
54
+ logUrl: "", // Will be updated when we get the real logUrl
55
+ });
56
+ }
57
+
58
+ // Tap agent→client stream
59
+ agentWritable = createTappedWritableStream(streams.agent.writable, {
60
+ onMessage: (line) => {
61
+ sessionStore.appendRawLine(config.sessionId!, line);
62
+ },
63
+ logger,
64
+ });
65
+
66
+ // Tap client→agent stream
67
+ clientWritable = createTappedWritableStream(streams.client.writable, {
68
+ onMessage: (line) => {
69
+ sessionStore.appendRawLine(config.sessionId!, line);
70
+ },
71
+ logger,
72
+ });
73
+ } else {
74
+ logger.info("Tapped streams NOT enabled", {
75
+ hasSessionId: !!config.sessionId,
76
+ hasSessionStore: !!sessionStore,
77
+ });
78
+ }
79
+
80
+ const agentStream = ndJsonStream(agentWritable, streams.agent.readable);
81
+
82
+ // Create the Claude agent
83
+ const agentConnection = new AgentSideConnection((client) => {
84
+ logger.info("Creating Claude agent");
85
+ return new ClaudeAcpAgent(client, sessionStore);
86
+ }, agentStream);
87
+
88
+ return {
89
+ agentConnection,
90
+ clientStreams: {
91
+ readable: streams.client.readable,
92
+ writable: clientWritable,
93
+ },
94
+ };
95
+ }