@posthog/agent 2.3.351 → 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.351",
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: {
@@ -14231,16 +14257,16 @@ function permissionOptions(allowAlwaysLabel) {
14231
14257
  }
14232
14258
  ];
14233
14259
  }
14234
- function buildPermissionOptions(toolName, toolInput, cwd, suggestions) {
14260
+ function buildPermissionOptions(toolName, toolInput, repoRoot, suggestions) {
14235
14261
  if (BASH_TOOLS.has(toolName)) {
14236
14262
  const rawRuleContent = suggestions?.flatMap((s) => "rules" in s ? s.rules : []).find((r) => r.toolName === "Bash" && r.ruleContent)?.ruleContent;
14237
14263
  const ruleContent = rawRuleContent?.replace(/:?\*$/, "");
14238
14264
  const command = toolInput?.command;
14239
14265
  const cmdName = command?.split(/\s+/)[0] ?? "this command";
14240
- const cwdLabel = cwd ? ` in ${cwd}` : "";
14266
+ const scopeLabel = repoRoot ? ` in ${repoRoot}` : "";
14241
14267
  const label = ruleContent ?? `\`${cmdName}\` commands`;
14242
14268
  return permissionOptions(
14243
- `Yes, and don't ask again for ${label}${cwdLabel}`
14269
+ `Yes, and don't ask again for ${label}${scopeLabel}`
14244
14270
  );
14245
14271
  }
14246
14272
  if (toolName === "BashOutput") {
@@ -14526,7 +14552,7 @@ async function handleDefaultPermissionFlow(context) {
14526
14552
  const options = buildPermissionOptions(
14527
14553
  toolName,
14528
14554
  toolInput,
14529
- session?.cwd,
14555
+ session.settingsManager.getRepoRoot(),
14530
14556
  suggestions
14531
14557
  );
14532
14558
  const response = await client.requestPermission({
@@ -14546,17 +14572,19 @@ async function handleDefaultPermissionFlow(context) {
14546
14572
  }
14547
14573
  if (response.outcome?.outcome === "selected" && (response.outcome.optionId === "allow" || response.outcome.optionId === "allow_always")) {
14548
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
+ }
14549
14584
  return {
14550
14585
  behavior: "allow",
14551
14586
  updatedInput: toolInput,
14552
- updatedPermissions: suggestions ?? [
14553
- {
14554
- type: "addRules",
14555
- rules: [{ toolName }],
14556
- behavior: "allow",
14557
- destination: "localSettings"
14558
- }
14559
- ]
14587
+ updatedPermissions: buildSessionPermissions(suggestions, rules)
14560
14588
  };
14561
14589
  }
14562
14590
  return {
@@ -14589,6 +14617,26 @@ function handlePlanFileException(context) {
14589
14617
  updatedInput: toolInput
14590
14618
  };
14591
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
+ }
14592
14640
  function extractDomainFromUrl(url) {
14593
14641
  try {
14594
14642
  return new URL(url).hostname;
@@ -14991,6 +15039,47 @@ var fs7 = __toESM(require("fs"), 1);
14991
15039
  var os3 = __toESM(require("os"), 1);
14992
15040
  var path9 = __toESM(require("path"), 1);
14993
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
14994
15083
  var ACP_TOOL_NAME_PREFIX = "mcp__acp__";
14995
15084
  var acpToolNames = {
14996
15085
  read: `${ACP_TOOL_NAME_PREFIX}Read`,
@@ -15047,7 +15136,7 @@ function matchesGlob(pattern, filePath, cwd) {
15047
15136
  });
15048
15137
  }
15049
15138
  function matchesRule(rule, toolName, toolInput, cwd) {
15050
- 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;
15051
15140
  if (!ruleAppliesToTool) {
15052
15141
  return false;
15053
15142
  }
@@ -15077,6 +15166,19 @@ function matchesRule(rule, toolName, toolInput, cwd) {
15077
15166
  }
15078
15167
  return matchesGlob(rule.argument, actualArg, cwd);
15079
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
+ }
15080
15182
  async function loadSettingsFile(filePath) {
15081
15183
  if (!filePath) {
15082
15184
  return {};
@@ -15095,6 +15197,17 @@ async function loadSettingsFile(filePath) {
15095
15197
  return {};
15096
15198
  }
15097
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
+ }
15098
15211
  function getManagedSettingsPath() {
15099
15212
  switch (process.platform) {
15100
15213
  case "darwin":
@@ -15109,6 +15222,7 @@ function getManagedSettingsPath() {
15109
15222
  }
15110
15223
  var SettingsManager = class {
15111
15224
  cwd;
15225
+ repoRoot;
15112
15226
  userSettings = {};
15113
15227
  projectSettings = {};
15114
15228
  localSettings = {};
@@ -15116,8 +15230,10 @@ var SettingsManager = class {
15116
15230
  mergedSettings = {};
15117
15231
  initialized = false;
15118
15232
  initPromise = null;
15233
+ writeMutex = new AsyncMutex();
15119
15234
  constructor(cwd) {
15120
15235
  this.cwd = cwd;
15236
+ this.repoRoot = cwd;
15121
15237
  }
15122
15238
  async initialize() {
15123
15239
  if (this.initialized) return;
@@ -15135,10 +15251,16 @@ var SettingsManager = class {
15135
15251
  getProjectSettingsPath() {
15136
15252
  return path9.join(this.cwd, ".claude", "settings.json");
15137
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
+ */
15138
15259
  getLocalSettingsPath() {
15139
- return path9.join(this.cwd, ".claude", "settings.local.json");
15260
+ return path9.join(this.repoRoot, ".claude", "settings.local.json");
15140
15261
  }
15141
15262
  async loadAllSettings() {
15263
+ this.repoRoot = await resolveMainRepoPath(this.cwd);
15142
15264
  const [userSettings, projectSettings, localSettings, enterpriseSettings] = await Promise.all([
15143
15265
  loadSettingsFile(this.getUserSettingsPath()),
15144
15266
  loadSettingsFile(this.getProjectSettingsPath()),
@@ -15195,9 +15317,6 @@ var SettingsManager = class {
15195
15317
  this.mergedSettings = merged;
15196
15318
  }
15197
15319
  checkPermission(toolName, toolInput) {
15198
- if (!toolName.startsWith(ACP_TOOL_NAME_PREFIX)) {
15199
- return { decision: "ask" };
15200
- }
15201
15320
  const permissions = this.mergedSettings.permissions;
15202
15321
  if (!permissions) {
15203
15322
  return { decision: "ask" };
@@ -15228,6 +15347,43 @@ var SettingsManager = class {
15228
15347
  getCwd() {
15229
15348
  return this.cwd;
15230
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
+ }
15231
15387
  async setCwd(cwd) {
15232
15388
  if (this.cwd === cwd) return;
15233
15389
  if (this.initPromise) await this.initPromise;
@@ -18813,35 +18969,6 @@ var SessionLogWriter = class _SessionLogWriter {
18813
18969
  }
18814
18970
  };
18815
18971
 
18816
- // src/utils/async-mutex.ts
18817
- var AsyncMutex = class {
18818
- locked = false;
18819
- queue = [];
18820
- async acquire() {
18821
- if (!this.locked) {
18822
- this.locked = true;
18823
- return;
18824
- }
18825
- return new Promise((resolve4) => {
18826
- this.queue.push(resolve4);
18827
- });
18828
- }
18829
- release() {
18830
- const next = this.queue.shift();
18831
- if (next) {
18832
- next();
18833
- } else {
18834
- this.locked = false;
18835
- }
18836
- }
18837
- isLocked() {
18838
- return this.locked;
18839
- }
18840
- get queueLength() {
18841
- return this.queue.length;
18842
- }
18843
- };
18844
-
18845
18972
  // src/server/cloud-prompt.ts
18846
18973
  function normalizeCloudPromptContent(content) {
18847
18974
  if (typeof content === "string") {