@posthog/agent 2.3.507 → 2.3.510

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.
@@ -8755,7 +8755,7 @@ var import_zod3 = require("zod");
8755
8755
  // package.json
8756
8756
  var package_default = {
8757
8757
  name: "@posthog/agent",
8758
- version: "2.3.507",
8758
+ version: "2.3.510",
8759
8759
  repository: "https://github.com/PostHog/code",
8760
8760
  description: "TypeScript agent framework wrapping Claude Agent SDK with Git-based task execution for PostHog",
8761
8761
  exports: {
@@ -13612,6 +13612,72 @@ function toolContent() {
13612
13612
  return new ToolContentBuilder();
13613
13613
  }
13614
13614
 
13615
+ // src/utils/partial-json.ts
13616
+ function tryParsePartialJson(s) {
13617
+ const trimmed2 = s.trim();
13618
+ if (!trimmed2) return null;
13619
+ try {
13620
+ return JSON.parse(trimmed2);
13621
+ } catch {
13622
+ }
13623
+ const closers = [];
13624
+ let inString = false;
13625
+ let escaped = false;
13626
+ for (let i2 = 0; i2 < trimmed2.length; i2++) {
13627
+ const ch = trimmed2[i2];
13628
+ if (inString) {
13629
+ if (escaped) {
13630
+ escaped = false;
13631
+ } else if (ch === "\\") {
13632
+ escaped = true;
13633
+ } else if (ch === '"') {
13634
+ inString = false;
13635
+ }
13636
+ continue;
13637
+ }
13638
+ if (ch === '"') inString = true;
13639
+ else if (ch === "{") closers.push("}");
13640
+ else if (ch === "[") closers.push("]");
13641
+ else if (ch === "}" || ch === "]") closers.pop();
13642
+ }
13643
+ const closeBrackets = (str) => {
13644
+ let out2 = str;
13645
+ for (let i2 = closers.length - 1; i2 >= 0; i2--) out2 += closers[i2];
13646
+ return out2;
13647
+ };
13648
+ const candidates = [];
13649
+ const closedString = inString ? `${trimmed2}"` : trimmed2;
13650
+ candidates.push(closeBrackets(closedString));
13651
+ let stripped = closedString.replace(/[,:]\s*$/, "");
13652
+ stripped = stripped.replace(/,?\s*"[^"]*"\s*:?\s*$/, "");
13653
+ candidates.push(closeBrackets(stripped));
13654
+ for (const candidate of candidates) {
13655
+ try {
13656
+ return JSON.parse(candidate);
13657
+ } catch {
13658
+ }
13659
+ }
13660
+ return null;
13661
+ }
13662
+
13663
+ // src/adapters/claude/permissions/posthog-exec-gate.ts
13664
+ var POSTHOG_EXEC_TOOL_RE = /^mcp__posthog(?:_[^_]+)*__exec$/;
13665
+ var POSTHOG_CALL_COMMAND_RE = /^\s*call\s+(?:--json\s+)?([a-zA-Z0-9_-]+)/;
13666
+ var POSTHOG_DESTRUCTIVE_SUBTOOL_RE = /(^|-)(partial-update|update|delete|destroy)(-|$)/i;
13667
+ function isPostHogExecTool(toolName) {
13668
+ return POSTHOG_EXEC_TOOL_RE.test(toolName);
13669
+ }
13670
+ function extractPostHogSubTool(toolInput) {
13671
+ if (!toolInput || typeof toolInput !== "object") return null;
13672
+ const command = toolInput.command;
13673
+ if (typeof command !== "string") return null;
13674
+ const match = command.match(POSTHOG_CALL_COMMAND_RE);
13675
+ return match ? match[1] ?? null : null;
13676
+ }
13677
+ function isPostHogDestructiveSubTool(subTool) {
13678
+ return POSTHOG_DESTRUCTIVE_SUBTOOL_RE.test(subTool);
13679
+ }
13680
+
13615
13681
  // src/adapters/claude/hooks.ts
13616
13682
  function extractTextFromToolResponse(response) {
13617
13683
  if (typeof response === "string") return response;
@@ -13752,6 +13818,19 @@ var createPreToolUseHook = (settingsManager, logger) => async (input, _toolUseID
13752
13818
  `[PreToolUseHook] Tool: ${toolName}, Decision: ${permissionCheck.decision}, Rule: ${permissionCheck.rule}`
13753
13819
  );
13754
13820
  }
13821
+ if (permissionCheck.decision === "allow" && isPostHogExecTool(toolName)) {
13822
+ const subTool = extractPostHogSubTool(toolInput);
13823
+ if (subTool && isPostHogDestructiveSubTool(subTool)) {
13824
+ return {
13825
+ continue: true,
13826
+ hookSpecificOutput: {
13827
+ hookEventName: "PreToolUse",
13828
+ permissionDecision: "ask",
13829
+ permissionDecisionReason: `Destructive PostHog sub-tool '${subTool}' requires explicit approval`
13830
+ }
13831
+ };
13832
+ }
13833
+ }
13755
13834
  switch (permissionCheck.decision) {
13756
13835
  case "allow":
13757
13836
  return {
@@ -14747,12 +14826,19 @@ function toAcpNotifications(content, role, sessionId, toolUseCache, fileContentC
14747
14826
  }
14748
14827
  return output;
14749
14828
  }
14750
- function streamEventToAcpNotifications(message, sessionId, toolUseCache, fileContentCache, client, logger, parentToolCallId, registerHooks, supportsTerminalOutput, cwd, enrichedReadCache) {
14829
+ function streamEventToAcpNotifications(message, sessionId, toolUseCache, toolUseStreamCache, fileContentCache, client, logger, parentToolCallId, registerHooks, supportsTerminalOutput, cwd, enrichedReadCache) {
14751
14830
  const event = message.event;
14752
14831
  switch (event.type) {
14753
- case "content_block_start":
14832
+ case "content_block_start": {
14833
+ const block = event.content_block;
14834
+ if (block.type === "tool_use" || block.type === "mcp_tool_use") {
14835
+ toolUseStreamCache.set(event.index, {
14836
+ toolUseId: block.id,
14837
+ partialJson: ""
14838
+ });
14839
+ }
14754
14840
  return toAcpNotifications(
14755
- [event.content_block],
14841
+ [block],
14756
14842
  "assistant",
14757
14843
  sessionId,
14758
14844
  toolUseCache,
@@ -14766,7 +14852,16 @@ function streamEventToAcpNotifications(message, sessionId, toolUseCache, fileCon
14766
14852
  void 0,
14767
14853
  enrichedReadCache
14768
14854
  );
14769
- case "content_block_delta":
14855
+ }
14856
+ case "content_block_delta": {
14857
+ if (event.delta.type === "input_json_delta") {
14858
+ return inputJsonDeltaToAcpNotifications(
14859
+ event.index,
14860
+ event.delta.partial_json,
14861
+ sessionId,
14862
+ toolUseStreamCache
14863
+ );
14864
+ }
14770
14865
  return toAcpNotifications(
14771
14866
  [event.delta],
14772
14867
  "assistant",
@@ -14782,16 +14877,36 @@ function streamEventToAcpNotifications(message, sessionId, toolUseCache, fileCon
14782
14877
  void 0,
14783
14878
  enrichedReadCache
14784
14879
  );
14880
+ }
14881
+ case "content_block_stop":
14882
+ toolUseStreamCache.delete(event.index);
14883
+ return [];
14785
14884
  case "message_start":
14786
14885
  case "message_delta":
14787
14886
  case "message_stop":
14788
- case "content_block_stop":
14789
14887
  return [];
14790
14888
  default:
14791
14889
  unreachable(event, logger);
14792
14890
  return [];
14793
14891
  }
14794
14892
  }
14893
+ function inputJsonDeltaToAcpNotifications(index, partialJson, sessionId, toolUseStreamCache) {
14894
+ const entry = toolUseStreamCache.get(index);
14895
+ if (!entry) return [];
14896
+ entry.partialJson += partialJson;
14897
+ const parsed = tryParsePartialJson(entry.partialJson);
14898
+ if (!parsed || typeof parsed !== "object") return [];
14899
+ return [
14900
+ {
14901
+ sessionId,
14902
+ update: {
14903
+ sessionUpdate: "tool_call_update",
14904
+ toolCallId: entry.toolUseId,
14905
+ rawInput: parsed
14906
+ }
14907
+ }
14908
+ ];
14909
+ }
14795
14910
  async function handleSystemMessage(message, context) {
14796
14911
  const { session, sessionId, client, logger } = context;
14797
14912
  switch (message.subtype) {
@@ -14935,12 +15050,20 @@ function extractUsageFromResult(message) {
14935
15050
  };
14936
15051
  }
14937
15052
  async function handleStreamEvent(message, context) {
14938
- const { sessionId, client, toolUseCache, fileContentCache, logger } = context;
15053
+ const {
15054
+ sessionId,
15055
+ client,
15056
+ toolUseCache,
15057
+ toolUseStreamCache,
15058
+ fileContentCache,
15059
+ logger
15060
+ } = context;
14939
15061
  const parentToolCallId = message.parent_tool_use_id ?? void 0;
14940
15062
  for (const notification of streamEventToAcpNotifications(
14941
15063
  message,
14942
15064
  sessionId,
14943
15065
  toolUseCache,
15066
+ toolUseStreamCache,
14944
15067
  fileContentCache,
14945
15068
  client,
14946
15069
  logger,
@@ -15346,6 +15469,12 @@ async function emitToolDenial(context, message) {
15346
15469
  }
15347
15470
  });
15348
15471
  }
15472
+ async function buildDenialResult(context, response) {
15473
+ const feedback = response._meta?.customInput?.trim();
15474
+ const message = feedback ? `User refused permission to run tool with feedback: ${feedback}` : "User refused permission to run tool";
15475
+ await emitToolDenial(context, message);
15476
+ return { behavior: "deny", message, interrupt: !feedback };
15477
+ }
15349
15478
  function getPlanFromFile(session, fileContentCache) {
15350
15479
  return session.lastPlanContent || (session.lastPlanFilePath ? fileContentCache[session.lastPlanFilePath] : void 0);
15351
15480
  }
@@ -15570,12 +15699,8 @@ async function handleDefaultPermissionFlow(context) {
15570
15699
  behavior: "allow",
15571
15700
  updatedInput: toolInput
15572
15701
  };
15573
- } else {
15574
- const feedback = response._meta?.customInput?.trim();
15575
- const message = feedback ? `User refused permission to run tool with feedback: ${feedback}` : "User refused permission to run tool";
15576
- await emitToolDenial(context, message);
15577
- return { behavior: "deny", message, interrupt: !feedback };
15578
15702
  }
15703
+ return buildDenialResult(context, response);
15579
15704
  }
15580
15705
  function parseMcpToolName(toolName) {
15581
15706
  const parts2 = toolName.split("__");
@@ -15638,10 +15763,61 @@ ${metadata2.description}` : "";
15638
15763
  updatedInput: toolInput
15639
15764
  };
15640
15765
  }
15641
- const feedback = response._meta?.customInput?.trim();
15642
- const message = feedback ? `User refused permission to run tool with feedback: ${feedback}` : "User refused permission to run tool";
15643
- await emitToolDenial(context, message);
15644
- return { behavior: "deny", message, interrupt: !feedback };
15766
+ return buildDenialResult(context, response);
15767
+ }
15768
+ async function handlePostHogExecApprovalFlow(context, subTool) {
15769
+ const { toolName, toolInput, toolUseID, client, sessionId, session } = context;
15770
+ const response = await client.requestPermission({
15771
+ options: [
15772
+ { kind: "allow_once", name: "Yes", optionId: "allow" },
15773
+ {
15774
+ kind: "allow_always",
15775
+ name: "Yes, always allow",
15776
+ optionId: "allow_always"
15777
+ },
15778
+ {
15779
+ kind: "reject_once",
15780
+ name: "Type here to tell the agent what to do differently",
15781
+ optionId: "reject",
15782
+ _meta: { customInput: true }
15783
+ }
15784
+ ],
15785
+ sessionId,
15786
+ toolCall: {
15787
+ toolCallId: toolUseID,
15788
+ title: `The agent wants to run \`${subTool}\` on PostHog`,
15789
+ kind: "other",
15790
+ content: [
15791
+ {
15792
+ type: "content",
15793
+ content: text(
15794
+ "This will modify live PostHog data. Approve to run this sub-tool."
15795
+ )
15796
+ }
15797
+ ],
15798
+ rawInput: { ...toolInput, toolName }
15799
+ }
15800
+ });
15801
+ if (context.signal?.aborted || response.outcome?.outcome === "cancelled") {
15802
+ throw new Error("Tool use aborted");
15803
+ }
15804
+ if (response.outcome?.outcome === "selected" && (response.outcome.optionId === "allow" || response.outcome.optionId === "allow_always")) {
15805
+ if (response.outcome.optionId === "allow_always") {
15806
+ try {
15807
+ await session.settingsManager.addPostHogExecApproval(subTool);
15808
+ } catch (error) {
15809
+ context.logger.warn(
15810
+ "[canUseTool] Failed to persist PostHog exec approval",
15811
+ { error: error instanceof Error ? error.message : String(error) }
15812
+ );
15813
+ }
15814
+ }
15815
+ return {
15816
+ behavior: "allow",
15817
+ updatedInput: toolInput
15818
+ };
15819
+ }
15820
+ return buildDenialResult(context, response);
15645
15821
  }
15646
15822
  function handlePlanFileException(context) {
15647
15823
  const { session, toolName, toolInput } = context;
@@ -15723,6 +15899,24 @@ async function canUseTool(context) {
15723
15899
  if (approvalState === "needs_approval") {
15724
15900
  return handleMcpApprovalFlow(context);
15725
15901
  }
15902
+ if (isPostHogExecTool(toolName)) {
15903
+ const subTool = extractPostHogSubTool(toolInput);
15904
+ if (subTool && isPostHogDestructiveSubTool(subTool)) {
15905
+ if (session.permissionMode === "auto" || session.permissionMode === "bypassPermissions") {
15906
+ return {
15907
+ behavior: "allow",
15908
+ updatedInput: toolInput
15909
+ };
15910
+ }
15911
+ if (session.settingsManager.hasPostHogExecApproval(subTool)) {
15912
+ return {
15913
+ behavior: "allow",
15914
+ updatedInput: toolInput
15915
+ };
15916
+ }
15917
+ return handlePostHogExecApprovalFlow(context, subTool);
15918
+ }
15919
+ }
15726
15920
  }
15727
15921
  if (isToolAllowedForMode(toolName, session.permissionMode)) {
15728
15922
  return {
@@ -16351,6 +16545,7 @@ var SettingsManager = class {
16351
16545
  ask: []
16352
16546
  };
16353
16547
  const merged = { permissions };
16548
+ const posthogApprovedExecTools = /* @__PURE__ */ new Set();
16354
16549
  for (const settings of allSettings) {
16355
16550
  if (settings.permissions) {
16356
16551
  if (settings.permissions.allow) {
@@ -16378,6 +16573,14 @@ var SettingsManager = class {
16378
16573
  if (settings.model) {
16379
16574
  merged.model = settings.model;
16380
16575
  }
16576
+ if (settings.posthogApprovedExecTools) {
16577
+ for (const tool of settings.posthogApprovedExecTools) {
16578
+ posthogApprovedExecTools.add(tool);
16579
+ }
16580
+ }
16581
+ }
16582
+ if (posthogApprovedExecTools.size > 0) {
16583
+ merged.posthogApprovedExecTools = Array.from(posthogApprovedExecTools);
16381
16584
  }
16382
16585
  this.mergedSettings = merged;
16383
16586
  }
@@ -16442,6 +16645,39 @@ var SettingsManager = class {
16442
16645
  const next = { ...existing, permissions };
16443
16646
  await fs7.promises.mkdir(path9.dirname(filePath), { recursive: true });
16444
16647
  await writeFileAtomic(filePath, `${JSON.stringify(next, null, 2)}
16648
+ `);
16649
+ this.localSettings = next;
16650
+ this.mergeAllSettings();
16651
+ } finally {
16652
+ this.writeMutex.release();
16653
+ }
16654
+ }
16655
+ hasPostHogExecApproval(subTool) {
16656
+ return this.mergedSettings.posthogApprovedExecTools?.includes(subTool) ?? false;
16657
+ }
16658
+ /**
16659
+ * Persists an approved PostHog MCP `exec` sub-tool (e.g. `experiment-update`)
16660
+ * to the local settings file so future calls skip the prompt. Mirrors
16661
+ * `addAllowRules` — serialised via `writeMutex`, atomic temp-file + rename.
16662
+ */
16663
+ async addPostHogExecApproval(subTool) {
16664
+ if (!subTool) return;
16665
+ if (!this.initialized) await this.initialize();
16666
+ await this.writeMutex.acquire();
16667
+ try {
16668
+ const filePath = this.getLocalSettingsPath();
16669
+ const existing = await readSettingsFileForUpdate(filePath);
16670
+ const current2 = new Set(existing.posthogApprovedExecTools ?? []);
16671
+ if (current2.has(subTool)) {
16672
+ return;
16673
+ }
16674
+ current2.add(subTool);
16675
+ const next = {
16676
+ ...existing,
16677
+ posthogApprovedExecTools: Array.from(current2)
16678
+ };
16679
+ await fs7.promises.mkdir(path9.dirname(filePath), { recursive: true });
16680
+ await writeFileAtomic(filePath, `${JSON.stringify(next, null, 2)}
16445
16681
  `);
16446
16682
  this.localSettings = next;
16447
16683
  this.mergeAllSettings();
@@ -16483,6 +16719,7 @@ function shouldEmitRawMessage(config, message) {
16483
16719
  var ClaudeAcpAgent = class extends BaseAcpAgent {
16484
16720
  adapterName = "claude";
16485
16721
  toolUseCache;
16722
+ toolUseStreamCache;
16486
16723
  backgroundTerminals = {};
16487
16724
  clientCapabilities;
16488
16725
  options;
@@ -16492,6 +16729,7 @@ var ClaudeAcpAgent = class extends BaseAcpAgent {
16492
16729
  super(client);
16493
16730
  this.options = options;
16494
16731
  this.toolUseCache = {};
16732
+ this.toolUseStreamCache = /* @__PURE__ */ new Map();
16495
16733
  this.logger = new Logger({ debug: true, prefix: "[ClaudeAcpAgent]" });
16496
16734
  this.enrichment = createEnrichment(options?.posthogApiConfig, this.logger);
16497
16735
  }
@@ -16686,6 +16924,7 @@ var ClaudeAcpAgent = class extends BaseAcpAgent {
16686
16924
  sessionId: params.sessionId,
16687
16925
  client: this.client,
16688
16926
  toolUseCache: this.toolUseCache,
16927
+ toolUseStreamCache: this.toolUseStreamCache,
16689
16928
  fileContentCache: this.fileContentCache,
16690
16929
  enrichedReadCache: this.enrichedReadCache,
16691
16930
  logger: this.logger,
@@ -16938,6 +17177,7 @@ var ClaudeAcpAgent = class extends BaseAcpAgent {
16938
17177
  }
16939
17178
  throw error;
16940
17179
  } finally {
17180
+ this.toolUseStreamCache.clear();
16941
17181
  if (!handedOff) {
16942
17182
  this.session.promptRunning = false;
16943
17183
  for (const [key, pending] of this.session.pendingMessages) {
@@ -17512,6 +17752,7 @@ var ClaudeAcpAgent = class extends BaseAcpAgent {
17512
17752
  sessionId,
17513
17753
  client: this.client,
17514
17754
  toolUseCache: this.toolUseCache,
17755
+ toolUseStreamCache: this.toolUseStreamCache,
17515
17756
  fileContentCache: this.fileContentCache,
17516
17757
  enrichedReadCache: this.enrichedReadCache,
17517
17758
  logger: this.logger,