@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.
package/dist/agent.js CHANGED
@@ -4030,7 +4030,7 @@ import { v7 as uuidv7 } from "uuid";
4030
4030
  // package.json
4031
4031
  var package_default = {
4032
4032
  name: "@posthog/agent",
4033
- version: "2.3.507",
4033
+ version: "2.3.510",
4034
4034
  repository: "https://github.com/PostHog/code",
4035
4035
  description: "TypeScript agent framework wrapping Claude Agent SDK with Git-based task execution for PostHog",
4036
4036
  exports: {
@@ -8681,6 +8681,72 @@ function toolContent() {
8681
8681
  return new ToolContentBuilder();
8682
8682
  }
8683
8683
 
8684
+ // src/utils/partial-json.ts
8685
+ function tryParsePartialJson(s) {
8686
+ const trimmed2 = s.trim();
8687
+ if (!trimmed2) return null;
8688
+ try {
8689
+ return JSON.parse(trimmed2);
8690
+ } catch {
8691
+ }
8692
+ const closers = [];
8693
+ let inString = false;
8694
+ let escaped = false;
8695
+ for (let i2 = 0; i2 < trimmed2.length; i2++) {
8696
+ const ch = trimmed2[i2];
8697
+ if (inString) {
8698
+ if (escaped) {
8699
+ escaped = false;
8700
+ } else if (ch === "\\") {
8701
+ escaped = true;
8702
+ } else if (ch === '"') {
8703
+ inString = false;
8704
+ }
8705
+ continue;
8706
+ }
8707
+ if (ch === '"') inString = true;
8708
+ else if (ch === "{") closers.push("}");
8709
+ else if (ch === "[") closers.push("]");
8710
+ else if (ch === "}" || ch === "]") closers.pop();
8711
+ }
8712
+ const closeBrackets = (str) => {
8713
+ let out2 = str;
8714
+ for (let i2 = closers.length - 1; i2 >= 0; i2--) out2 += closers[i2];
8715
+ return out2;
8716
+ };
8717
+ const candidates = [];
8718
+ const closedString = inString ? `${trimmed2}"` : trimmed2;
8719
+ candidates.push(closeBrackets(closedString));
8720
+ let stripped = closedString.replace(/[,:]\s*$/, "");
8721
+ stripped = stripped.replace(/,?\s*"[^"]*"\s*:?\s*$/, "");
8722
+ candidates.push(closeBrackets(stripped));
8723
+ for (const candidate of candidates) {
8724
+ try {
8725
+ return JSON.parse(candidate);
8726
+ } catch {
8727
+ }
8728
+ }
8729
+ return null;
8730
+ }
8731
+
8732
+ // src/adapters/claude/permissions/posthog-exec-gate.ts
8733
+ var POSTHOG_EXEC_TOOL_RE = /^mcp__posthog(?:_[^_]+)*__exec$/;
8734
+ var POSTHOG_CALL_COMMAND_RE = /^\s*call\s+(?:--json\s+)?([a-zA-Z0-9_-]+)/;
8735
+ var POSTHOG_DESTRUCTIVE_SUBTOOL_RE = /(^|-)(partial-update|update|delete|destroy)(-|$)/i;
8736
+ function isPostHogExecTool(toolName) {
8737
+ return POSTHOG_EXEC_TOOL_RE.test(toolName);
8738
+ }
8739
+ function extractPostHogSubTool(toolInput) {
8740
+ if (!toolInput || typeof toolInput !== "object") return null;
8741
+ const command = toolInput.command;
8742
+ if (typeof command !== "string") return null;
8743
+ const match = command.match(POSTHOG_CALL_COMMAND_RE);
8744
+ return match ? match[1] ?? null : null;
8745
+ }
8746
+ function isPostHogDestructiveSubTool(subTool) {
8747
+ return POSTHOG_DESTRUCTIVE_SUBTOOL_RE.test(subTool);
8748
+ }
8749
+
8684
8750
  // src/adapters/claude/hooks.ts
8685
8751
  function extractTextFromToolResponse(response) {
8686
8752
  if (typeof response === "string") return response;
@@ -8821,6 +8887,19 @@ var createPreToolUseHook = (settingsManager, logger) => async (input, _toolUseID
8821
8887
  `[PreToolUseHook] Tool: ${toolName}, Decision: ${permissionCheck.decision}, Rule: ${permissionCheck.rule}`
8822
8888
  );
8823
8889
  }
8890
+ if (permissionCheck.decision === "allow" && isPostHogExecTool(toolName)) {
8891
+ const subTool = extractPostHogSubTool(toolInput);
8892
+ if (subTool && isPostHogDestructiveSubTool(subTool)) {
8893
+ return {
8894
+ continue: true,
8895
+ hookSpecificOutput: {
8896
+ hookEventName: "PreToolUse",
8897
+ permissionDecision: "ask",
8898
+ permissionDecisionReason: `Destructive PostHog sub-tool '${subTool}' requires explicit approval`
8899
+ }
8900
+ };
8901
+ }
8902
+ }
8824
8903
  switch (permissionCheck.decision) {
8825
8904
  case "allow":
8826
8905
  return {
@@ -9816,12 +9895,19 @@ function toAcpNotifications(content, role, sessionId, toolUseCache, fileContentC
9816
9895
  }
9817
9896
  return output;
9818
9897
  }
9819
- function streamEventToAcpNotifications(message, sessionId, toolUseCache, fileContentCache, client, logger, parentToolCallId, registerHooks, supportsTerminalOutput, cwd, enrichedReadCache) {
9898
+ function streamEventToAcpNotifications(message, sessionId, toolUseCache, toolUseStreamCache, fileContentCache, client, logger, parentToolCallId, registerHooks, supportsTerminalOutput, cwd, enrichedReadCache) {
9820
9899
  const event = message.event;
9821
9900
  switch (event.type) {
9822
- case "content_block_start":
9901
+ case "content_block_start": {
9902
+ const block = event.content_block;
9903
+ if (block.type === "tool_use" || block.type === "mcp_tool_use") {
9904
+ toolUseStreamCache.set(event.index, {
9905
+ toolUseId: block.id,
9906
+ partialJson: ""
9907
+ });
9908
+ }
9823
9909
  return toAcpNotifications(
9824
- [event.content_block],
9910
+ [block],
9825
9911
  "assistant",
9826
9912
  sessionId,
9827
9913
  toolUseCache,
@@ -9835,7 +9921,16 @@ function streamEventToAcpNotifications(message, sessionId, toolUseCache, fileCon
9835
9921
  void 0,
9836
9922
  enrichedReadCache
9837
9923
  );
9838
- case "content_block_delta":
9924
+ }
9925
+ case "content_block_delta": {
9926
+ if (event.delta.type === "input_json_delta") {
9927
+ return inputJsonDeltaToAcpNotifications(
9928
+ event.index,
9929
+ event.delta.partial_json,
9930
+ sessionId,
9931
+ toolUseStreamCache
9932
+ );
9933
+ }
9839
9934
  return toAcpNotifications(
9840
9935
  [event.delta],
9841
9936
  "assistant",
@@ -9851,16 +9946,36 @@ function streamEventToAcpNotifications(message, sessionId, toolUseCache, fileCon
9851
9946
  void 0,
9852
9947
  enrichedReadCache
9853
9948
  );
9949
+ }
9950
+ case "content_block_stop":
9951
+ toolUseStreamCache.delete(event.index);
9952
+ return [];
9854
9953
  case "message_start":
9855
9954
  case "message_delta":
9856
9955
  case "message_stop":
9857
- case "content_block_stop":
9858
9956
  return [];
9859
9957
  default:
9860
9958
  unreachable(event, logger);
9861
9959
  return [];
9862
9960
  }
9863
9961
  }
9962
+ function inputJsonDeltaToAcpNotifications(index, partialJson, sessionId, toolUseStreamCache) {
9963
+ const entry = toolUseStreamCache.get(index);
9964
+ if (!entry) return [];
9965
+ entry.partialJson += partialJson;
9966
+ const parsed = tryParsePartialJson(entry.partialJson);
9967
+ if (!parsed || typeof parsed !== "object") return [];
9968
+ return [
9969
+ {
9970
+ sessionId,
9971
+ update: {
9972
+ sessionUpdate: "tool_call_update",
9973
+ toolCallId: entry.toolUseId,
9974
+ rawInput: parsed
9975
+ }
9976
+ }
9977
+ ];
9978
+ }
9864
9979
  async function handleSystemMessage(message, context) {
9865
9980
  const { session, sessionId, client, logger } = context;
9866
9981
  switch (message.subtype) {
@@ -10004,12 +10119,20 @@ function extractUsageFromResult(message) {
10004
10119
  };
10005
10120
  }
10006
10121
  async function handleStreamEvent(message, context) {
10007
- const { sessionId, client, toolUseCache, fileContentCache, logger } = context;
10122
+ const {
10123
+ sessionId,
10124
+ client,
10125
+ toolUseCache,
10126
+ toolUseStreamCache,
10127
+ fileContentCache,
10128
+ logger
10129
+ } = context;
10008
10130
  const parentToolCallId = message.parent_tool_use_id ?? void 0;
10009
10131
  for (const notification of streamEventToAcpNotifications(
10010
10132
  message,
10011
10133
  sessionId,
10012
10134
  toolUseCache,
10135
+ toolUseStreamCache,
10013
10136
  fileContentCache,
10014
10137
  client,
10015
10138
  logger,
@@ -10415,6 +10538,12 @@ async function emitToolDenial(context, message) {
10415
10538
  }
10416
10539
  });
10417
10540
  }
10541
+ async function buildDenialResult(context, response) {
10542
+ const feedback = response._meta?.customInput?.trim();
10543
+ const message = feedback ? `User refused permission to run tool with feedback: ${feedback}` : "User refused permission to run tool";
10544
+ await emitToolDenial(context, message);
10545
+ return { behavior: "deny", message, interrupt: !feedback };
10546
+ }
10418
10547
  function getPlanFromFile(session, fileContentCache) {
10419
10548
  return session.lastPlanContent || (session.lastPlanFilePath ? fileContentCache[session.lastPlanFilePath] : void 0);
10420
10549
  }
@@ -10639,12 +10768,8 @@ async function handleDefaultPermissionFlow(context) {
10639
10768
  behavior: "allow",
10640
10769
  updatedInput: toolInput
10641
10770
  };
10642
- } else {
10643
- const feedback = response._meta?.customInput?.trim();
10644
- const message = feedback ? `User refused permission to run tool with feedback: ${feedback}` : "User refused permission to run tool";
10645
- await emitToolDenial(context, message);
10646
- return { behavior: "deny", message, interrupt: !feedback };
10647
10771
  }
10772
+ return buildDenialResult(context, response);
10648
10773
  }
10649
10774
  function parseMcpToolName(toolName) {
10650
10775
  const parts2 = toolName.split("__");
@@ -10707,10 +10832,61 @@ ${metadata2.description}` : "";
10707
10832
  updatedInput: toolInput
10708
10833
  };
10709
10834
  }
10710
- const feedback = response._meta?.customInput?.trim();
10711
- const message = feedback ? `User refused permission to run tool with feedback: ${feedback}` : "User refused permission to run tool";
10712
- await emitToolDenial(context, message);
10713
- return { behavior: "deny", message, interrupt: !feedback };
10835
+ return buildDenialResult(context, response);
10836
+ }
10837
+ async function handlePostHogExecApprovalFlow(context, subTool) {
10838
+ const { toolName, toolInput, toolUseID, client, sessionId, session } = context;
10839
+ const response = await client.requestPermission({
10840
+ options: [
10841
+ { kind: "allow_once", name: "Yes", optionId: "allow" },
10842
+ {
10843
+ kind: "allow_always",
10844
+ name: "Yes, always allow",
10845
+ optionId: "allow_always"
10846
+ },
10847
+ {
10848
+ kind: "reject_once",
10849
+ name: "Type here to tell the agent what to do differently",
10850
+ optionId: "reject",
10851
+ _meta: { customInput: true }
10852
+ }
10853
+ ],
10854
+ sessionId,
10855
+ toolCall: {
10856
+ toolCallId: toolUseID,
10857
+ title: `The agent wants to run \`${subTool}\` on PostHog`,
10858
+ kind: "other",
10859
+ content: [
10860
+ {
10861
+ type: "content",
10862
+ content: text(
10863
+ "This will modify live PostHog data. Approve to run this sub-tool."
10864
+ )
10865
+ }
10866
+ ],
10867
+ rawInput: { ...toolInput, toolName }
10868
+ }
10869
+ });
10870
+ if (context.signal?.aborted || response.outcome?.outcome === "cancelled") {
10871
+ throw new Error("Tool use aborted");
10872
+ }
10873
+ if (response.outcome?.outcome === "selected" && (response.outcome.optionId === "allow" || response.outcome.optionId === "allow_always")) {
10874
+ if (response.outcome.optionId === "allow_always") {
10875
+ try {
10876
+ await session.settingsManager.addPostHogExecApproval(subTool);
10877
+ } catch (error) {
10878
+ context.logger.warn(
10879
+ "[canUseTool] Failed to persist PostHog exec approval",
10880
+ { error: error instanceof Error ? error.message : String(error) }
10881
+ );
10882
+ }
10883
+ }
10884
+ return {
10885
+ behavior: "allow",
10886
+ updatedInput: toolInput
10887
+ };
10888
+ }
10889
+ return buildDenialResult(context, response);
10714
10890
  }
10715
10891
  function handlePlanFileException(context) {
10716
10892
  const { session, toolName, toolInput } = context;
@@ -10792,6 +10968,24 @@ async function canUseTool(context) {
10792
10968
  if (approvalState === "needs_approval") {
10793
10969
  return handleMcpApprovalFlow(context);
10794
10970
  }
10971
+ if (isPostHogExecTool(toolName)) {
10972
+ const subTool = extractPostHogSubTool(toolInput);
10973
+ if (subTool && isPostHogDestructiveSubTool(subTool)) {
10974
+ if (session.permissionMode === "auto" || session.permissionMode === "bypassPermissions") {
10975
+ return {
10976
+ behavior: "allow",
10977
+ updatedInput: toolInput
10978
+ };
10979
+ }
10980
+ if (session.settingsManager.hasPostHogExecApproval(subTool)) {
10981
+ return {
10982
+ behavior: "allow",
10983
+ updatedInput: toolInput
10984
+ };
10985
+ }
10986
+ return handlePostHogExecApprovalFlow(context, subTool);
10987
+ }
10988
+ }
10795
10989
  }
10796
10990
  if (isToolAllowedForMode(toolName, session.permissionMode)) {
10797
10991
  return {
@@ -16318,6 +16512,7 @@ var SettingsManager = class {
16318
16512
  ask: []
16319
16513
  };
16320
16514
  const merged = { permissions };
16515
+ const posthogApprovedExecTools = /* @__PURE__ */ new Set();
16321
16516
  for (const settings of allSettings) {
16322
16517
  if (settings.permissions) {
16323
16518
  if (settings.permissions.allow) {
@@ -16345,6 +16540,14 @@ var SettingsManager = class {
16345
16540
  if (settings.model) {
16346
16541
  merged.model = settings.model;
16347
16542
  }
16543
+ if (settings.posthogApprovedExecTools) {
16544
+ for (const tool of settings.posthogApprovedExecTools) {
16545
+ posthogApprovedExecTools.add(tool);
16546
+ }
16547
+ }
16548
+ }
16549
+ if (posthogApprovedExecTools.size > 0) {
16550
+ merged.posthogApprovedExecTools = Array.from(posthogApprovedExecTools);
16348
16551
  }
16349
16552
  this.mergedSettings = merged;
16350
16553
  }
@@ -16409,6 +16612,39 @@ var SettingsManager = class {
16409
16612
  const next = { ...existing, permissions };
16410
16613
  await fs7.promises.mkdir(path11.dirname(filePath), { recursive: true });
16411
16614
  await writeFileAtomic(filePath, `${JSON.stringify(next, null, 2)}
16615
+ `);
16616
+ this.localSettings = next;
16617
+ this.mergeAllSettings();
16618
+ } finally {
16619
+ this.writeMutex.release();
16620
+ }
16621
+ }
16622
+ hasPostHogExecApproval(subTool) {
16623
+ return this.mergedSettings.posthogApprovedExecTools?.includes(subTool) ?? false;
16624
+ }
16625
+ /**
16626
+ * Persists an approved PostHog MCP `exec` sub-tool (e.g. `experiment-update`)
16627
+ * to the local settings file so future calls skip the prompt. Mirrors
16628
+ * `addAllowRules` — serialised via `writeMutex`, atomic temp-file + rename.
16629
+ */
16630
+ async addPostHogExecApproval(subTool) {
16631
+ if (!subTool) return;
16632
+ if (!this.initialized) await this.initialize();
16633
+ await this.writeMutex.acquire();
16634
+ try {
16635
+ const filePath = this.getLocalSettingsPath();
16636
+ const existing = await readSettingsFileForUpdate(filePath);
16637
+ const current2 = new Set(existing.posthogApprovedExecTools ?? []);
16638
+ if (current2.has(subTool)) {
16639
+ return;
16640
+ }
16641
+ current2.add(subTool);
16642
+ const next = {
16643
+ ...existing,
16644
+ posthogApprovedExecTools: Array.from(current2)
16645
+ };
16646
+ await fs7.promises.mkdir(path11.dirname(filePath), { recursive: true });
16647
+ await writeFileAtomic(filePath, `${JSON.stringify(next, null, 2)}
16412
16648
  `);
16413
16649
  this.localSettings = next;
16414
16650
  this.mergeAllSettings();
@@ -16450,6 +16686,7 @@ function shouldEmitRawMessage(config, message) {
16450
16686
  var ClaudeAcpAgent = class extends BaseAcpAgent {
16451
16687
  adapterName = "claude";
16452
16688
  toolUseCache;
16689
+ toolUseStreamCache;
16453
16690
  backgroundTerminals = {};
16454
16691
  clientCapabilities;
16455
16692
  options;
@@ -16459,6 +16696,7 @@ var ClaudeAcpAgent = class extends BaseAcpAgent {
16459
16696
  super(client);
16460
16697
  this.options = options;
16461
16698
  this.toolUseCache = {};
16699
+ this.toolUseStreamCache = /* @__PURE__ */ new Map();
16462
16700
  this.logger = new Logger({ debug: true, prefix: "[ClaudeAcpAgent]" });
16463
16701
  this.enrichment = createEnrichment(options?.posthogApiConfig, this.logger);
16464
16702
  }
@@ -16653,6 +16891,7 @@ var ClaudeAcpAgent = class extends BaseAcpAgent {
16653
16891
  sessionId: params.sessionId,
16654
16892
  client: this.client,
16655
16893
  toolUseCache: this.toolUseCache,
16894
+ toolUseStreamCache: this.toolUseStreamCache,
16656
16895
  fileContentCache: this.fileContentCache,
16657
16896
  enrichedReadCache: this.enrichedReadCache,
16658
16897
  logger: this.logger,
@@ -16905,6 +17144,7 @@ var ClaudeAcpAgent = class extends BaseAcpAgent {
16905
17144
  }
16906
17145
  throw error;
16907
17146
  } finally {
17147
+ this.toolUseStreamCache.clear();
16908
17148
  if (!handedOff) {
16909
17149
  this.session.promptRunning = false;
16910
17150
  for (const [key, pending] of this.session.pendingMessages) {
@@ -17479,6 +17719,7 @@ var ClaudeAcpAgent = class extends BaseAcpAgent {
17479
17719
  sessionId,
17480
17720
  client: this.client,
17481
17721
  toolUseCache: this.toolUseCache,
17722
+ toolUseStreamCache: this.toolUseStreamCache,
17482
17723
  fileContentCache: this.fileContentCache,
17483
17724
  enrichedReadCache: this.enrichedReadCache,
17484
17725
  logger: this.logger,