@posthog/agent 2.3.388 → 2.3.401

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 (34) hide show
  1. package/dist/adapters/claude/conversion/tool-use-to-acp.js.map +1 -1
  2. package/dist/adapters/claude/mcp/tool-metadata.d.ts +24 -0
  3. package/dist/adapters/claude/mcp/tool-metadata.js +165 -0
  4. package/dist/adapters/claude/mcp/tool-metadata.js.map +1 -0
  5. package/dist/adapters/claude/tools.js.map +1 -1
  6. package/dist/agent.js +113 -3
  7. package/dist/agent.js.map +1 -1
  8. package/dist/handoff-checkpoint.d.ts +1 -0
  9. package/dist/handoff-checkpoint.js +17 -1
  10. package/dist/handoff-checkpoint.js.map +1 -1
  11. package/dist/index.d.ts +7 -9
  12. package/dist/index.js.map +1 -1
  13. package/dist/posthog-api.js +5 -1
  14. package/dist/posthog-api.js.map +1 -1
  15. package/dist/server/agent-server.js +258 -101
  16. package/dist/server/agent-server.js.map +1 -1
  17. package/dist/server/bin.cjs +248 -98
  18. package/dist/server/bin.cjs.map +1 -1
  19. package/dist/tree-tracker.js +128 -97
  20. package/dist/tree-tracker.js.map +1 -1
  21. package/package.json +7 -3
  22. package/src/adapters/claude/claude-agent.ts +5 -0
  23. package/src/adapters/claude/mcp/tool-metadata.test.ts +93 -0
  24. package/src/adapters/claude/mcp/tool-metadata.ts +33 -0
  25. package/src/adapters/claude/permissions/permission-handlers.test.ts +165 -0
  26. package/src/adapters/claude/permissions/permission-handlers.ts +105 -0
  27. package/src/adapters/claude/session/instructions.ts +9 -1
  28. package/src/adapters/claude/types.ts +2 -0
  29. package/src/handoff-checkpoint.test.ts +1 -0
  30. package/src/handoff-checkpoint.ts +17 -1
  31. package/src/sagas/apply-snapshot-saga.test.ts +1 -0
  32. package/src/sagas/apply-snapshot-saga.ts +68 -54
  33. package/src/sagas/capture-tree-saga.test.ts +18 -0
  34. package/src/sagas/capture-tree-saga.ts +64 -49
@@ -8605,7 +8605,7 @@ import { z as z4 } from "zod";
8605
8605
  // package.json
8606
8606
  var package_default = {
8607
8607
  name: "@posthog/agent",
8608
- version: "2.3.388",
8608
+ version: "2.3.401",
8609
8609
  repository: "https://github.com/PostHog/code",
8610
8610
  description: "TypeScript agent framework wrapping Claude Agent SDK with Git-based task execution for PostHog",
8611
8611
  exports: {
@@ -8657,6 +8657,10 @@ var package_default = {
8657
8657
  types: "./dist/adapters/reasoning-effort.d.ts",
8658
8658
  import: "./dist/adapters/reasoning-effort.js"
8659
8659
  },
8660
+ "./adapters/claude/mcp/tool-metadata": {
8661
+ types: "./dist/adapters/claude/mcp/tool-metadata.d.ts",
8662
+ import: "./dist/adapters/claude/mcp/tool-metadata.js"
8663
+ },
8660
8664
  "./execution-mode": {
8661
8665
  types: "./dist/execution-mode.d.ts",
8662
8666
  import: "./dist/execution-mode.js"
@@ -13657,10 +13661,12 @@ async function fetchMcpToolMetadata(q, logger = new Logger({ debug: false, prefi
13657
13661
  for (const tool of server.tools) {
13658
13662
  const toolKey = buildToolKey(server.name, tool.name);
13659
13663
  const readOnly = tool.annotations?.readOnly === true;
13664
+ const existing = mcpToolMetadataCache.get(toolKey);
13660
13665
  mcpToolMetadataCache.set(toolKey, {
13661
13666
  readOnly,
13662
13667
  name: tool.name,
13663
- description: tool.description
13668
+ description: tool.description,
13669
+ approvalState: existing?.approvalState
13664
13670
  });
13665
13671
  if (readOnly) readOnlyCount++;
13666
13672
  }
@@ -13702,6 +13708,23 @@ function getConnectedMcpServerNames() {
13702
13708
  }
13703
13709
  return [...names];
13704
13710
  }
13711
+ function getMcpToolApprovalState(toolName) {
13712
+ return mcpToolMetadataCache.get(toolName)?.approvalState;
13713
+ }
13714
+ function setMcpToolApprovalStates(approvals) {
13715
+ for (const [toolKey, approvalState] of Object.entries(approvals)) {
13716
+ const existing = mcpToolMetadataCache.get(toolKey);
13717
+ if (existing) {
13718
+ existing.approvalState = approvalState;
13719
+ } else {
13720
+ mcpToolMetadataCache.set(toolKey, {
13721
+ readOnly: false,
13722
+ name: toolKey,
13723
+ approvalState
13724
+ });
13725
+ }
13726
+ }
13727
+ }
13705
13728
 
13706
13729
  // src/adapters/claude/conversion/tool-use-to-acp.ts
13707
13730
  var SYSTEM_REMINDER_REGEX = /\s*<system-reminder>[\s\S]*?<\/system-reminder>/g;
@@ -15397,6 +15420,72 @@ async function handleDefaultPermissionFlow(context) {
15397
15420
  return { behavior: "deny", message, interrupt: !feedback };
15398
15421
  }
15399
15422
  }
15423
+ function parseMcpToolName(toolName) {
15424
+ const parts2 = toolName.split("__");
15425
+ return {
15426
+ serverName: parts2[1] ?? toolName,
15427
+ tool: parts2.slice(2).join("__") || toolName
15428
+ };
15429
+ }
15430
+ async function handleMcpApprovalFlow(context) {
15431
+ const { toolName, toolInput, toolUseID, client, sessionId } = context;
15432
+ const { serverName, tool: displayTool } = parseMcpToolName(toolName);
15433
+ const metadata2 = getMcpToolMetadata(toolName);
15434
+ const description = metadata2?.description ? `
15435
+
15436
+ ${metadata2.description}` : "";
15437
+ const response = await client.requestPermission({
15438
+ options: [
15439
+ { kind: "allow_once", name: "Yes", optionId: "allow" },
15440
+ {
15441
+ kind: "allow_always",
15442
+ name: "Yes, always allow",
15443
+ optionId: "allow_always"
15444
+ },
15445
+ {
15446
+ kind: "reject_once",
15447
+ name: "Type here to tell the agent what to do differently",
15448
+ optionId: "reject",
15449
+ _meta: { customInput: true }
15450
+ }
15451
+ ],
15452
+ sessionId,
15453
+ toolCall: {
15454
+ toolCallId: toolUseID,
15455
+ title: `The agent wants to call ${displayTool} (${serverName})`,
15456
+ kind: "other",
15457
+ content: description ? [{ type: "content", content: text(description) }] : [],
15458
+ rawInput: { ...toolInput, toolName }
15459
+ }
15460
+ });
15461
+ if (context.signal?.aborted || response.outcome?.outcome === "cancelled") {
15462
+ throw new Error("Tool use aborted");
15463
+ }
15464
+ if (response.outcome?.outcome === "selected" && (response.outcome.optionId === "allow" || response.outcome.optionId === "allow_always")) {
15465
+ if (response.outcome.optionId === "allow_always") {
15466
+ return {
15467
+ behavior: "allow",
15468
+ updatedInput: toolInput,
15469
+ updatedPermissions: [
15470
+ {
15471
+ type: "addRules",
15472
+ rules: [{ toolName }],
15473
+ behavior: "allow",
15474
+ destination: "localSettings"
15475
+ }
15476
+ ]
15477
+ };
15478
+ }
15479
+ return {
15480
+ behavior: "allow",
15481
+ updatedInput: toolInput
15482
+ };
15483
+ }
15484
+ const feedback = response._meta?.customInput?.trim();
15485
+ const message = feedback ? `User refused permission to run tool with feedback: ${feedback}` : "User refused permission to run tool";
15486
+ await emitToolDenial(context, message);
15487
+ return { behavior: "deny", message, interrupt: !feedback };
15488
+ }
15400
15489
  function handlePlanFileException(context) {
15401
15490
  const { session, toolName, toolInput } = context;
15402
15491
  if (session.permissionMode !== "plan" || !WRITE_TOOLS.has(toolName)) {
@@ -15467,6 +15556,17 @@ async function canUseTool(context) {
15467
15556
  }
15468
15557
  }
15469
15558
  }
15559
+ if (toolName.startsWith("mcp__")) {
15560
+ const approvalState = getMcpToolApprovalState(toolName);
15561
+ if (approvalState === "do_not_use") {
15562
+ const message = "This tool has been blocked. To re-enable it, go to Settings > MCP Servers in PostHog Code.";
15563
+ await emitToolDenial(context, message);
15564
+ return { behavior: "deny", message, interrupt: false };
15565
+ }
15566
+ if (approvalState === "needs_approval") {
15567
+ return handleMcpApprovalFlow(context);
15568
+ }
15569
+ }
15470
15570
  if (isToolAllowedForMode(toolName, session.permissionMode)) {
15471
15571
  return {
15472
15572
  behavior: "allow",
@@ -15679,7 +15779,14 @@ Only enter plan mode (EnterPlanMode) when the user is requesting a significant c
15679
15779
 
15680
15780
  When in doubt, continue executing and incorporate the feedback inline.
15681
15781
  `;
15682
- var APPENDED_INSTRUCTIONS = BRANCH_NAMING + PLAN_MODE;
15782
+ var MCP_TOOLS = `
15783
+ # MCP Tool Access
15784
+
15785
+ If an MCP tool call is explicitly denied with a message, relay that denial message to the user exactly as given. Do NOT suggest checking "Claude Code settings."
15786
+
15787
+ If an MCP tool call returns an error, treat it as a normal tool error \u2014 troubleshoot, retry, or inform the user about the specific error. Do NOT assume it is a permissions issue and do NOT direct the user to any settings page.
15788
+ `;
15789
+ var APPENDED_INSTRUCTIONS = BRANCH_NAMING + PLAN_MODE + MCP_TOOLS;
15683
15790
 
15684
15791
  // src/adapters/claude/session/options.ts
15685
15792
  function buildSystemPrompt(customPrompt) {
@@ -17008,6 +17115,9 @@ var ClaudeAcpAgent = class extends BaseAcpAgent {
17008
17115
  const earlyModelId = settingsManager.getSettings().model || meta?.model || "";
17009
17116
  const mcpServers = supportsMcpInjection(earlyModelId) ? parseMcpServers(params) : {};
17010
17117
  const systemPrompt = buildSystemPrompt(meta?.systemPrompt);
17118
+ if (meta?.mcpToolApprovals) {
17119
+ setMcpToolApprovalStates(meta.mcpToolApprovals);
17120
+ }
17011
17121
  const outputFormat = meta?.jsonSchema && this.options?.onStructuredOutput ? { type: "json_schema", schema: meta.jsonSchema } : void 0;
17012
17122
  this.logger.debug(isResume ? "Resuming session" : "Creating new session", {
17013
17123
  sessionId,
@@ -18334,7 +18444,14 @@ function createCodexConnection(config) {
18334
18444
  }
18335
18445
 
18336
18446
  // src/handoff-checkpoint.ts
18337
- import { mkdir as mkdir4, readFile as readFile4, rm as rm4, writeFile as writeFile2 } from "fs/promises";
18447
+ import {
18448
+ mkdir as mkdir4,
18449
+ readdir,
18450
+ readFile as readFile4,
18451
+ rm as rm4,
18452
+ rmdir,
18453
+ writeFile as writeFile2
18454
+ } from "fs/promises";
18338
18455
  import { join as join9 } from "path";
18339
18456
 
18340
18457
  // ../git/dist/handoff.js
@@ -19165,6 +19282,7 @@ var HandoffCheckpointTracker = class {
19165
19282
  } finally {
19166
19283
  await this.removeIfPresent(packPath);
19167
19284
  await this.removeIfPresent(indexPath);
19285
+ await this.removeTmpDirIfEmpty(tmpDir);
19168
19286
  }
19169
19287
  }
19170
19288
  toGitCheckpoint(checkpoint) {
@@ -19323,6 +19441,14 @@ var HandoffCheckpointTracker = class {
19323
19441
  await rm4(filePath, { force: true }).catch(() => {
19324
19442
  });
19325
19443
  }
19444
+ async removeTmpDirIfEmpty(tmpDir) {
19445
+ const entries = await readdir(tmpDir).catch(() => null);
19446
+ if (!entries || entries.length > 0) {
19447
+ return;
19448
+ }
19449
+ await rmdir(tmpDir).catch(() => {
19450
+ });
19451
+ }
19326
19452
  };
19327
19453
 
19328
19454
  // src/utils/gateway.ts
@@ -20213,7 +20339,7 @@ var SessionLogWriter = class _SessionLogWriter {
20213
20339
  };
20214
20340
 
20215
20341
  // src/sagas/apply-snapshot-saga.ts
20216
- import { mkdir as mkdir7, rm as rm6, writeFile as writeFile5 } from "fs/promises";
20342
+ import { mkdir as mkdir7, readdir as readdir2, rm as rm6, rmdir as rmdir2, writeFile as writeFile5 } from "fs/promises";
20217
20343
  import { join as join12 } from "path";
20218
20344
 
20219
20345
  // ../git/dist/sagas/tree.js
@@ -20487,68 +20613,83 @@ var ApplySnapshotSaga = class extends Saga {
20487
20613
  throw new Error("Cannot apply snapshot: no archive URL");
20488
20614
  }
20489
20615
  const archiveUrl = snapshot.archiveUrl;
20490
- await this.step({
20491
- name: "create_tmp_dir",
20492
- execute: () => mkdir7(tmpDir, { recursive: true }),
20493
- rollback: async () => {
20494
- }
20495
- });
20496
- const archivePath = join12(tmpDir, `${snapshot.treeHash}.tar.gz`);
20497
- this.archivePath = archivePath;
20498
- await this.step({
20499
- name: "download_archive",
20500
- execute: async () => {
20501
- const arrayBuffer = await apiClient.downloadArtifact(
20502
- taskId,
20503
- runId,
20504
- archiveUrl
20505
- );
20506
- if (!arrayBuffer) {
20507
- throw new Error("Failed to download archive");
20616
+ try {
20617
+ await this.step({
20618
+ name: "create_tmp_dir",
20619
+ execute: () => mkdir7(tmpDir, { recursive: true }),
20620
+ rollback: async () => {
20508
20621
  }
20509
- const base64Content = Buffer.from(arrayBuffer).toString("utf-8");
20510
- const binaryContent = Buffer.from(base64Content, "base64");
20511
- await writeFile5(archivePath, binaryContent);
20512
- this.log.info("Tree archive downloaded", {
20513
- treeHash: snapshot.treeHash,
20514
- snapshotBytes: binaryContent.byteLength,
20515
- snapshotWireBytes: arrayBuffer.byteLength,
20516
- totalBytes: binaryContent.byteLength,
20517
- totalWireBytes: arrayBuffer.byteLength
20518
- });
20519
- },
20520
- rollback: async () => {
20521
- if (this.archivePath) {
20522
- await rm6(this.archivePath, { force: true }).catch(() => {
20622
+ });
20623
+ const archivePath = join12(tmpDir, `${snapshot.treeHash}.tar.gz`);
20624
+ this.archivePath = archivePath;
20625
+ await this.step({
20626
+ name: "download_archive",
20627
+ execute: async () => {
20628
+ const arrayBuffer = await apiClient.downloadArtifact(
20629
+ taskId,
20630
+ runId,
20631
+ archiveUrl
20632
+ );
20633
+ if (!arrayBuffer) {
20634
+ throw new Error("Failed to download archive");
20635
+ }
20636
+ const base64Content = Buffer.from(arrayBuffer).toString("utf-8");
20637
+ const binaryContent = Buffer.from(base64Content, "base64");
20638
+ await writeFile5(archivePath, binaryContent);
20639
+ this.log.info("Tree archive downloaded", {
20640
+ treeHash: snapshot.treeHash,
20641
+ snapshotBytes: binaryContent.byteLength,
20642
+ snapshotWireBytes: arrayBuffer.byteLength,
20643
+ totalBytes: binaryContent.byteLength,
20644
+ totalWireBytes: arrayBuffer.byteLength
20523
20645
  });
20646
+ },
20647
+ rollback: async () => {
20648
+ if (this.archivePath) {
20649
+ await rm6(this.archivePath, { force: true }).catch(() => {
20650
+ });
20651
+ }
20524
20652
  }
20653
+ });
20654
+ const gitApplySaga = new ApplyTreeSaga(this.log);
20655
+ const applyResult = await gitApplySaga.run({
20656
+ baseDir: repositoryPath,
20657
+ treeHash: snapshot.treeHash,
20658
+ baseCommit: snapshot.baseCommit,
20659
+ changes: snapshot.changes,
20660
+ archivePath: this.archivePath
20661
+ });
20662
+ if (!applyResult.success) {
20663
+ throw new Error(`Failed to apply tree: ${applyResult.error}`);
20525
20664
  }
20526
- });
20527
- const gitApplySaga = new ApplyTreeSaga(this.log);
20528
- const applyResult = await gitApplySaga.run({
20529
- baseDir: repositoryPath,
20530
- treeHash: snapshot.treeHash,
20531
- baseCommit: snapshot.baseCommit,
20532
- changes: snapshot.changes,
20533
- archivePath: this.archivePath
20534
- });
20535
- if (!applyResult.success) {
20536
- throw new Error(`Failed to apply tree: ${applyResult.error}`);
20665
+ this.log.info("Tree snapshot applied", {
20666
+ treeHash: snapshot.treeHash,
20667
+ totalChanges: snapshot.changes.length,
20668
+ deletedFiles: snapshot.changes.filter((c) => c.status === "D").length
20669
+ });
20670
+ return { treeHash: snapshot.treeHash };
20671
+ } finally {
20672
+ if (this.archivePath) {
20673
+ await rm6(this.archivePath, { force: true }).catch(() => {
20674
+ });
20675
+ }
20676
+ await this.removeTmpDirIfEmpty(tmpDir);
20677
+ this.archivePath = null;
20537
20678
  }
20538
- await rm6(this.archivePath, { force: true }).catch(() => {
20539
- });
20540
- this.log.info("Tree snapshot applied", {
20541
- treeHash: snapshot.treeHash,
20542
- totalChanges: snapshot.changes.length,
20543
- deletedFiles: snapshot.changes.filter((c) => c.status === "D").length
20679
+ }
20680
+ async removeTmpDirIfEmpty(tmpDir) {
20681
+ const entries = await readdir2(tmpDir).catch(() => null);
20682
+ if (!entries || entries.length > 0) {
20683
+ return;
20684
+ }
20685
+ await rmdir2(tmpDir).catch(() => {
20544
20686
  });
20545
- return { treeHash: snapshot.treeHash };
20546
20687
  }
20547
20688
  };
20548
20689
 
20549
20690
  // src/sagas/capture-tree-saga.ts
20550
20691
  import { existsSync as existsSync6 } from "fs";
20551
- import { readFile as readFile6, rm as rm7 } from "fs/promises";
20692
+ import { readdir as readdir3, readFile as readFile6, rm as rm7, rmdir as rmdir3 } from "fs/promises";
20552
20693
  import { join as join13 } from "path";
20553
20694
  var CaptureTreeSaga2 = class extends Saga {
20554
20695
  sagaName = "CaptureTreeSaga";
@@ -20569,54 +20710,62 @@ var CaptureTreeSaga2 = class extends Saga {
20569
20710
  }
20570
20711
  const shouldArchive = !!apiClient;
20571
20712
  const archivePath = shouldArchive ? join13(tmpDir, `tree-${Date.now()}.tar.gz`) : void 0;
20572
- const gitCaptureSaga = new CaptureTreeSaga(this.log);
20573
- const captureResult = await gitCaptureSaga.run({
20574
- baseDir: repositoryPath,
20575
- lastTreeHash,
20576
- archivePath
20577
- });
20578
- if (!captureResult.success) {
20579
- throw new Error(`Failed to capture tree: ${captureResult.error}`);
20580
- }
20581
- const {
20582
- snapshot: gitSnapshot,
20583
- archivePath: createdArchivePath,
20584
- changed
20585
- } = captureResult.data;
20586
- if (!changed || !gitSnapshot) {
20587
- this.log.debug("No changes since last capture", { lastTreeHash });
20588
- return { snapshot: null, newTreeHash: lastTreeHash };
20589
- }
20590
- let archiveUrl;
20591
- if (apiClient && createdArchivePath) {
20592
- try {
20593
- archiveUrl = await this.uploadArchive(
20594
- createdArchivePath,
20595
- gitSnapshot.treeHash,
20596
- apiClient,
20597
- taskId,
20598
- runId
20599
- );
20600
- } finally {
20601
- await rm7(createdArchivePath, { force: true }).catch(() => {
20713
+ try {
20714
+ const gitCaptureSaga = new CaptureTreeSaga(this.log);
20715
+ const captureResult = await gitCaptureSaga.run({
20716
+ baseDir: repositoryPath,
20717
+ lastTreeHash,
20718
+ archivePath
20719
+ });
20720
+ if (!captureResult.success) {
20721
+ throw new Error(`Failed to capture tree: ${captureResult.error}`);
20722
+ }
20723
+ const {
20724
+ snapshot: gitSnapshot,
20725
+ archivePath: createdArchivePath,
20726
+ changed
20727
+ } = captureResult.data;
20728
+ if (!changed || !gitSnapshot) {
20729
+ this.log.debug("No changes since last capture", { lastTreeHash });
20730
+ return { snapshot: null, newTreeHash: lastTreeHash };
20731
+ }
20732
+ let archiveUrl;
20733
+ if (apiClient && createdArchivePath) {
20734
+ try {
20735
+ archiveUrl = await this.uploadArchive(
20736
+ createdArchivePath,
20737
+ gitSnapshot.treeHash,
20738
+ apiClient,
20739
+ taskId,
20740
+ runId
20741
+ );
20742
+ } finally {
20743
+ await rm7(createdArchivePath, { force: true }).catch(() => {
20744
+ });
20745
+ }
20746
+ }
20747
+ const snapshot = {
20748
+ treeHash: gitSnapshot.treeHash,
20749
+ baseCommit: gitSnapshot.baseCommit,
20750
+ changes: gitSnapshot.changes,
20751
+ timestamp: gitSnapshot.timestamp,
20752
+ interrupted,
20753
+ archiveUrl
20754
+ };
20755
+ this.log.info("Tree captured", {
20756
+ treeHash: snapshot.treeHash,
20757
+ changes: snapshot.changes.length,
20758
+ interrupted,
20759
+ archiveUrl
20760
+ });
20761
+ return { snapshot, newTreeHash: snapshot.treeHash };
20762
+ } finally {
20763
+ if (archivePath) {
20764
+ await rm7(archivePath, { force: true }).catch(() => {
20602
20765
  });
20603
20766
  }
20767
+ await this.removeTmpDirIfEmpty(tmpDir);
20604
20768
  }
20605
- const snapshot = {
20606
- treeHash: gitSnapshot.treeHash,
20607
- baseCommit: gitSnapshot.baseCommit,
20608
- changes: gitSnapshot.changes,
20609
- timestamp: gitSnapshot.timestamp,
20610
- interrupted,
20611
- archiveUrl
20612
- };
20613
- this.log.info("Tree captured", {
20614
- treeHash: snapshot.treeHash,
20615
- changes: snapshot.changes.length,
20616
- interrupted,
20617
- archiveUrl
20618
- });
20619
- return { snapshot, newTreeHash: snapshot.treeHash };
20620
20769
  }
20621
20770
  async uploadArchive(archivePath, treeHash, apiClient, taskId, runId) {
20622
20771
  const archiveUrl = await this.step({
@@ -20655,6 +20804,14 @@ var CaptureTreeSaga2 = class extends Saga {
20655
20804
  });
20656
20805
  return archiveUrl;
20657
20806
  }
20807
+ async removeTmpDirIfEmpty(tmpDir) {
20808
+ const entries = await readdir3(tmpDir).catch(() => null);
20809
+ if (!entries || entries.length > 0) {
20810
+ return;
20811
+ }
20812
+ await rmdir3(tmpDir).catch(() => {
20813
+ });
20814
+ }
20658
20815
  };
20659
20816
 
20660
20817
  // src/tree-tracker.ts