@posthog/agent 2.3.349 → 2.3.353

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.
@@ -8688,6 +8688,32 @@ async function getCurrentBranch(baseDir, options) {
8688
8688
  return branch === "HEAD" ? null : branch;
8689
8689
  }, { signal: options?.abortSignal });
8690
8690
  }
8691
+ async function listWorktrees(baseDir, options) {
8692
+ const manager = getGitOperationManager();
8693
+ return manager.executeRead(baseDir, async (git) => {
8694
+ const output = await git.raw(["worktree", "list", "--porcelain"]);
8695
+ const worktrees = [];
8696
+ let current2 = {};
8697
+ for (const line of output.split("\n")) {
8698
+ if (line.startsWith("worktree ")) {
8699
+ if (current2.path) {
8700
+ worktrees.push(current2);
8701
+ }
8702
+ current2 = { path: line.slice(9), branch: null };
8703
+ } else if (line.startsWith("HEAD ")) {
8704
+ current2.head = line.slice(5);
8705
+ } else if (line.startsWith("branch ")) {
8706
+ current2.branch = line.slice(7).replace("refs/heads/", "");
8707
+ } else if (line === "detached") {
8708
+ current2.branch = null;
8709
+ }
8710
+ }
8711
+ if (current2.path) {
8712
+ worktrees.push(current2);
8713
+ }
8714
+ return worktrees;
8715
+ }, { signal: options?.abortSignal });
8716
+ }
8691
8717
  async function getHeadSha(baseDir, options) {
8692
8718
  const manager = getGitOperationManager();
8693
8719
  return manager.executeRead(baseDir, (git) => git.revparse(["HEAD"]), {
@@ -8701,7 +8727,7 @@ var import_hono = require("hono");
8701
8727
  // package.json
8702
8728
  var package_default = {
8703
8729
  name: "@posthog/agent",
8704
- version: "2.3.349",
8730
+ version: "2.3.353",
8705
8731
  repository: "https://github.com/PostHog/code",
8706
8732
  description: "TypeScript agent framework wrapping Claude Agent SDK with Git-based task execution for PostHog",
8707
8733
  exports: {
@@ -12740,7 +12766,7 @@ var createPostToolUseHook = ({ onModeChange, logger }) => async (input, toolUseI
12740
12766
  var SUBAGENT_REWRITES = {
12741
12767
  Explore: "ph-explore"
12742
12768
  };
12743
- var createSubagentRewriteHook = (logger) => async (input, _toolUseID) => {
12769
+ var createSubagentRewriteHook = (logger, registeredAgents) => async (input, _toolUseID) => {
12744
12770
  if (input.hook_event_name !== "PreToolUse") {
12745
12771
  return { continue: true };
12746
12772
  }
@@ -12753,6 +12779,12 @@ var createSubagentRewriteHook = (logger) => async (input, _toolUseID) => {
12753
12779
  return { continue: true };
12754
12780
  }
12755
12781
  const target = SUBAGENT_REWRITES[subagentType];
12782
+ if (!registeredAgents.has(target)) {
12783
+ logger.warn(
12784
+ `[SubagentRewriteHook] Skipping rewrite ${subagentType} \u2192 ${target}: target agent not registered for this session. Falling back to built-in ${subagentType}.`
12785
+ );
12786
+ return { continue: true };
12787
+ }
12756
12788
  logger.info(
12757
12789
  `[SubagentRewriteHook] Rewriting subagent_type: ${subagentType} \u2192 ${target}`
12758
12790
  );
@@ -14225,16 +14257,16 @@ function permissionOptions(allowAlwaysLabel) {
14225
14257
  }
14226
14258
  ];
14227
14259
  }
14228
- function buildPermissionOptions(toolName, toolInput, cwd, suggestions) {
14260
+ function buildPermissionOptions(toolName, toolInput, repoRoot, suggestions) {
14229
14261
  if (BASH_TOOLS.has(toolName)) {
14230
14262
  const rawRuleContent = suggestions?.flatMap((s) => "rules" in s ? s.rules : []).find((r) => r.toolName === "Bash" && r.ruleContent)?.ruleContent;
14231
14263
  const ruleContent = rawRuleContent?.replace(/:?\*$/, "");
14232
14264
  const command = toolInput?.command;
14233
14265
  const cmdName = command?.split(/\s+/)[0] ?? "this command";
14234
- const cwdLabel = cwd ? ` in ${cwd}` : "";
14266
+ const scopeLabel = repoRoot ? ` in ${repoRoot}` : "";
14235
14267
  const label = ruleContent ?? `\`${cmdName}\` commands`;
14236
14268
  return permissionOptions(
14237
- `Yes, and don't ask again for ${label}${cwdLabel}`
14269
+ `Yes, and don't ask again for ${label}${scopeLabel}`
14238
14270
  );
14239
14271
  }
14240
14272
  if (toolName === "BashOutput") {
@@ -14520,7 +14552,7 @@ async function handleDefaultPermissionFlow(context) {
14520
14552
  const options = buildPermissionOptions(
14521
14553
  toolName,
14522
14554
  toolInput,
14523
- session?.cwd,
14555
+ session.settingsManager.getRepoRoot(),
14524
14556
  suggestions
14525
14557
  );
14526
14558
  const response = await client.requestPermission({
@@ -14540,17 +14572,19 @@ async function handleDefaultPermissionFlow(context) {
14540
14572
  }
14541
14573
  if (response.outcome?.outcome === "selected" && (response.outcome.optionId === "allow" || response.outcome.optionId === "allow_always")) {
14542
14574
  if (response.outcome.optionId === "allow_always") {
14575
+ const rules = extractAllowRules(suggestions, toolName);
14576
+ try {
14577
+ await session.settingsManager.addAllowRules(rules);
14578
+ } catch (error) {
14579
+ context.logger.warn(
14580
+ "[canUseTool] Failed to persist allow rules to repository settings",
14581
+ { error: error instanceof Error ? error.message : String(error) }
14582
+ );
14583
+ }
14543
14584
  return {
14544
14585
  behavior: "allow",
14545
14586
  updatedInput: toolInput,
14546
- updatedPermissions: suggestions ?? [
14547
- {
14548
- type: "addRules",
14549
- rules: [{ toolName }],
14550
- behavior: "allow",
14551
- destination: "localSettings"
14552
- }
14553
- ]
14587
+ updatedPermissions: buildSessionPermissions(suggestions, rules)
14554
14588
  };
14555
14589
  }
14556
14590
  return {
@@ -14583,6 +14617,26 @@ function handlePlanFileException(context) {
14583
14617
  updatedInput: toolInput
14584
14618
  };
14585
14619
  }
14620
+ function extractAllowRules(suggestions, toolName) {
14621
+ if (!suggestions || suggestions.length === 0) {
14622
+ return [{ toolName }];
14623
+ }
14624
+ return suggestions.filter(
14625
+ (update) => update.type === "addRules" && update.behavior === "allow"
14626
+ ).flatMap((update) => "rules" in update ? update.rules : []);
14627
+ }
14628
+ function buildSessionPermissions(suggestions, rules) {
14629
+ const passthrough = (suggestions ?? []).filter(
14630
+ (update) => !(update.type === "addRules" && update.behavior === "allow")
14631
+ ).map((update) => ({ ...update, destination: "session" }));
14632
+ if (rules.length === 0) {
14633
+ return passthrough;
14634
+ }
14635
+ return [
14636
+ { type: "addRules", rules, behavior: "allow", destination: "session" },
14637
+ ...passthrough
14638
+ ];
14639
+ }
14586
14640
  function extractDomainFromUrl(url) {
14587
14641
  try {
14588
14642
  return new URL(url).hostname;
@@ -14759,7 +14813,7 @@ function buildEnvironment() {
14759
14813
  CLAUDE_CODE_EMIT_SESSION_STATE_EVENTS: "1"
14760
14814
  };
14761
14815
  }
14762
- function buildHooks(userHooks, onModeChange, settingsManager, logger, enrichmentDeps, enrichedReadCache) {
14816
+ function buildHooks(userHooks, onModeChange, settingsManager, logger, enrichmentDeps, enrichedReadCache, registeredAgents) {
14763
14817
  const postToolUseHooks = [createPostToolUseHook({ onModeChange, logger })];
14764
14818
  if (enrichmentDeps && enrichedReadCache) {
14765
14819
  postToolUseHooks.push(
@@ -14777,12 +14831,49 @@ function buildHooks(userHooks, onModeChange, settingsManager, logger, enrichment
14777
14831
  {
14778
14832
  hooks: [
14779
14833
  createPreToolUseHook(settingsManager, logger),
14780
- createSubagentRewriteHook(logger)
14834
+ createSubagentRewriteHook(logger, registeredAgents)
14781
14835
  ]
14782
14836
  }
14783
14837
  ]
14784
14838
  };
14785
14839
  }
14840
+ var PH_EXPLORE_AGENT = {
14841
+ description: 'Fast agent for exploring and understanding codebases. Use this when you need to find files by pattern (eg. "src/components/**/*.tsx"), search for code or keywords (eg. "where is the auth middleware?"), or answer questions about how the codebase works (eg. "how does the session service handle reconnects?"). When calling this agent, specify a thoroughness level: "quick" for targeted lookups, "medium" for broader exploration, or "very thorough" for comprehensive analysis across multiple locations.',
14842
+ model: "haiku",
14843
+ prompt: `You are a fast, read-only codebase exploration agent.
14844
+
14845
+ Your job is to find files, search code, read the most relevant sources, and report findings clearly.
14846
+
14847
+ Rules:
14848
+ - Never create, modify, delete, move, or copy files.
14849
+ - Never use shell redirection or any command that changes system state.
14850
+ - Use Glob for broad file pattern matching.
14851
+ - Use Grep for searching file contents.
14852
+ - Use Read when you know the exact file path to inspect.
14853
+ - Use Bash only for safe read-only commands like ls, git status, git log, git diff, find, cat, head, and tail.
14854
+ - Adapt your search approach based on the thoroughness level specified by the caller.
14855
+ - Return file paths as absolute paths in your final response.
14856
+ - Avoid using emojis.
14857
+ - Wherever possible, spawn multiple parallel tool calls for grepping and reading files.
14858
+ - Search efficiently, then read only the most relevant files.
14859
+ - Return findings directly in your final response \u2014 do not create files.`,
14860
+ tools: [
14861
+ "Bash",
14862
+ "Glob",
14863
+ "Grep",
14864
+ "Read",
14865
+ "WebFetch",
14866
+ "WebSearch",
14867
+ "NotebookRead",
14868
+ "TodoWrite"
14869
+ ]
14870
+ };
14871
+ function buildAgents(userAgents) {
14872
+ return {
14873
+ "ph-explore": PH_EXPLORE_AGENT,
14874
+ ...userAgents || {}
14875
+ };
14876
+ }
14786
14877
  function getAbortController(userProvidedController) {
14787
14878
  const controller = userProvidedController ?? new AbortController();
14788
14879
  if (controller.signal.aborted) {
@@ -14868,6 +14959,8 @@ function ensureLocalSettings(cwd) {
14868
14959
  function buildSessionOptions(params) {
14869
14960
  ensureLocalSettings(params.cwd);
14870
14961
  const tools = params.userProvidedOptions?.tools ?? (params.disableBuiltInTools ? [] : { type: "preset", preset: "claude_code" });
14962
+ const agents = buildAgents(params.userProvidedOptions?.agents);
14963
+ const registeredAgentNames = new Set(Object.keys(agents));
14871
14964
  const options = {
14872
14965
  ...params.userProvidedOptions,
14873
14966
  betas: ["context-1m-2025-08-07"],
@@ -14881,6 +14974,7 @@ function buildSessionOptions(params) {
14881
14974
  canUseTool: params.canUseTool,
14882
14975
  executable: "node",
14883
14976
  tools,
14977
+ agents,
14884
14978
  extraArgs: {
14885
14979
  ...params.userProvidedOptions?.extraArgs,
14886
14980
  "replay-user-messages": ""
@@ -14896,7 +14990,8 @@ function buildSessionOptions(params) {
14896
14990
  params.settingsManager,
14897
14991
  params.logger,
14898
14992
  params.enrichmentDeps,
14899
- params.enrichedReadCache
14993
+ params.enrichedReadCache,
14994
+ registeredAgentNames
14900
14995
  ),
14901
14996
  outputFormat: params.outputFormat,
14902
14997
  abortController: getAbortController(
@@ -14944,6 +15039,47 @@ var fs7 = __toESM(require("fs"), 1);
14944
15039
  var os3 = __toESM(require("os"), 1);
14945
15040
  var path9 = __toESM(require("path"), 1);
14946
15041
  var import_minimatch = require("minimatch");
15042
+
15043
+ // src/utils/async-mutex.ts
15044
+ var AsyncMutex = class {
15045
+ locked = false;
15046
+ queue = [];
15047
+ async acquire() {
15048
+ if (!this.locked) {
15049
+ this.locked = true;
15050
+ return;
15051
+ }
15052
+ return new Promise((resolve4) => {
15053
+ this.queue.push(resolve4);
15054
+ });
15055
+ }
15056
+ release() {
15057
+ const next = this.queue.shift();
15058
+ if (next) {
15059
+ next();
15060
+ } else {
15061
+ this.locked = false;
15062
+ }
15063
+ }
15064
+ isLocked() {
15065
+ return this.locked;
15066
+ }
15067
+ get queueLength() {
15068
+ return this.queue.length;
15069
+ }
15070
+ };
15071
+
15072
+ // src/adapters/claude/session/repo-path.ts
15073
+ async function resolveMainRepoPath(cwd) {
15074
+ try {
15075
+ const worktrees = await listWorktrees(cwd);
15076
+ return worktrees[0]?.path ?? cwd;
15077
+ } catch {
15078
+ return cwd;
15079
+ }
15080
+ }
15081
+
15082
+ // src/adapters/claude/session/settings.ts
14947
15083
  var ACP_TOOL_NAME_PREFIX = "mcp__acp__";
14948
15084
  var acpToolNames = {
14949
15085
  read: `${ACP_TOOL_NAME_PREFIX}Read`,
@@ -15000,7 +15136,7 @@ function matchesGlob(pattern, filePath, cwd) {
15000
15136
  });
15001
15137
  }
15002
15138
  function matchesRule(rule, toolName, toolInput, cwd) {
15003
- const ruleAppliesToTool = rule.toolName === "Bash" && toolName === acpToolNames.bash || rule.toolName === "Edit" && FILE_EDITING_TOOLS.includes(toolName) || rule.toolName === "Read" && FILE_READING_TOOLS.includes(toolName);
15139
+ const ruleAppliesToTool = rule.toolName === "Bash" && toolName === acpToolNames.bash || rule.toolName === "Edit" && FILE_EDITING_TOOLS.includes(toolName) || rule.toolName === "Read" && FILE_READING_TOOLS.includes(toolName) || rule.toolName === toolName && !rule.argument;
15004
15140
  if (!ruleAppliesToTool) {
15005
15141
  return false;
15006
15142
  }
@@ -15030,6 +15166,19 @@ function matchesRule(rule, toolName, toolInput, cwd) {
15030
15166
  }
15031
15167
  return matchesGlob(rule.argument, actualArg, cwd);
15032
15168
  }
15169
+ function formatRule(rule) {
15170
+ return rule.ruleContent ? `${rule.toolName}(${rule.ruleContent})` : rule.toolName;
15171
+ }
15172
+ async function writeFileAtomic(filePath, data) {
15173
+ const tmpPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
15174
+ await fs7.promises.writeFile(tmpPath, data);
15175
+ try {
15176
+ await fs7.promises.rename(tmpPath, filePath);
15177
+ } catch (error) {
15178
+ await fs7.promises.rm(tmpPath, { force: true });
15179
+ throw error;
15180
+ }
15181
+ }
15033
15182
  async function loadSettingsFile(filePath) {
15034
15183
  if (!filePath) {
15035
15184
  return {};
@@ -15048,6 +15197,17 @@ async function loadSettingsFile(filePath) {
15048
15197
  return {};
15049
15198
  }
15050
15199
  }
15200
+ async function readSettingsFileForUpdate(filePath) {
15201
+ try {
15202
+ const content = await fs7.promises.readFile(filePath, "utf-8");
15203
+ return JSON.parse(content);
15204
+ } catch (error) {
15205
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
15206
+ return {};
15207
+ }
15208
+ throw error;
15209
+ }
15210
+ }
15051
15211
  function getManagedSettingsPath() {
15052
15212
  switch (process.platform) {
15053
15213
  case "darwin":
@@ -15062,6 +15222,7 @@ function getManagedSettingsPath() {
15062
15222
  }
15063
15223
  var SettingsManager = class {
15064
15224
  cwd;
15225
+ repoRoot;
15065
15226
  userSettings = {};
15066
15227
  projectSettings = {};
15067
15228
  localSettings = {};
@@ -15069,8 +15230,10 @@ var SettingsManager = class {
15069
15230
  mergedSettings = {};
15070
15231
  initialized = false;
15071
15232
  initPromise = null;
15233
+ writeMutex = new AsyncMutex();
15072
15234
  constructor(cwd) {
15073
15235
  this.cwd = cwd;
15236
+ this.repoRoot = cwd;
15074
15237
  }
15075
15238
  async initialize() {
15076
15239
  if (this.initialized) return;
@@ -15088,10 +15251,16 @@ var SettingsManager = class {
15088
15251
  getProjectSettingsPath() {
15089
15252
  return path9.join(this.cwd, ".claude", "settings.json");
15090
15253
  }
15254
+ /**
15255
+ * Local settings are anchored to the primary worktree so every worktree of
15256
+ * the same repository shares a single `.claude/settings.local.json`. This
15257
+ * avoids re-prompting for the same permission in every worktree.
15258
+ */
15091
15259
  getLocalSettingsPath() {
15092
- return path9.join(this.cwd, ".claude", "settings.local.json");
15260
+ return path9.join(this.repoRoot, ".claude", "settings.local.json");
15093
15261
  }
15094
15262
  async loadAllSettings() {
15263
+ this.repoRoot = await resolveMainRepoPath(this.cwd);
15095
15264
  const [userSettings, projectSettings, localSettings, enterpriseSettings] = await Promise.all([
15096
15265
  loadSettingsFile(this.getUserSettingsPath()),
15097
15266
  loadSettingsFile(this.getProjectSettingsPath()),
@@ -15148,9 +15317,6 @@ var SettingsManager = class {
15148
15317
  this.mergedSettings = merged;
15149
15318
  }
15150
15319
  checkPermission(toolName, toolInput) {
15151
- if (!toolName.startsWith(ACP_TOOL_NAME_PREFIX)) {
15152
- return { decision: "ask" };
15153
- }
15154
15320
  const permissions = this.mergedSettings.permissions;
15155
15321
  if (!permissions) {
15156
15322
  return { decision: "ask" };
@@ -15181,6 +15347,43 @@ var SettingsManager = class {
15181
15347
  getCwd() {
15182
15348
  return this.cwd;
15183
15349
  }
15350
+ getRepoRoot() {
15351
+ return this.repoRoot;
15352
+ }
15353
+ /**
15354
+ * Persists allow rules to `<primary-worktree>/.claude/settings.local.json`.
15355
+ * Because local settings are resolved against the primary worktree, every
15356
+ * worktree of the same repository picks up the new rule on next load.
15357
+ *
15358
+ * Writes are serialised via `writeMutex` to prevent concurrent callers from
15359
+ * clobbering each other, and use a temp-file + rename to keep the file
15360
+ * consistent if the process dies mid-write.
15361
+ */
15362
+ async addAllowRules(rules) {
15363
+ if (rules.length === 0) return;
15364
+ if (!this.initialized) await this.initialize();
15365
+ await this.writeMutex.acquire();
15366
+ try {
15367
+ const filePath = this.getLocalSettingsPath();
15368
+ const existing = await readSettingsFileForUpdate(filePath);
15369
+ const permissions = {
15370
+ ...existing.permissions ?? {}
15371
+ };
15372
+ const current2 = new Set(permissions.allow ?? []);
15373
+ for (const rule of rules) {
15374
+ current2.add(formatRule(rule));
15375
+ }
15376
+ permissions.allow = Array.from(current2);
15377
+ const next = { ...existing, permissions };
15378
+ await fs7.promises.mkdir(path9.dirname(filePath), { recursive: true });
15379
+ await writeFileAtomic(filePath, `${JSON.stringify(next, null, 2)}
15380
+ `);
15381
+ this.localSettings = next;
15382
+ this.mergeAllSettings();
15383
+ } finally {
15384
+ this.writeMutex.release();
15385
+ }
15386
+ }
15184
15387
  async setCwd(cwd) {
15185
15388
  if (this.cwd === cwd) return;
15186
15389
  if (this.initPromise) await this.initPromise;
@@ -18766,35 +18969,6 @@ var SessionLogWriter = class _SessionLogWriter {
18766
18969
  }
18767
18970
  };
18768
18971
 
18769
- // src/utils/async-mutex.ts
18770
- var AsyncMutex = class {
18771
- locked = false;
18772
- queue = [];
18773
- async acquire() {
18774
- if (!this.locked) {
18775
- this.locked = true;
18776
- return;
18777
- }
18778
- return new Promise((resolve4) => {
18779
- this.queue.push(resolve4);
18780
- });
18781
- }
18782
- release() {
18783
- const next = this.queue.shift();
18784
- if (next) {
18785
- next();
18786
- } else {
18787
- this.locked = false;
18788
- }
18789
- }
18790
- isLocked() {
18791
- return this.locked;
18792
- }
18793
- get queueLength() {
18794
- return this.queue.length;
18795
- }
18796
- };
18797
-
18798
18972
  // src/server/cloud-prompt.ts
18799
18973
  function normalizeCloudPromptContent(content) {
18800
18974
  if (typeof content === "string") {