@posthog/agent 2.3.259 → 2.3.263

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.
@@ -5683,7 +5683,7 @@ var import_hono = require("hono");
5683
5683
  // package.json
5684
5684
  var package_default = {
5685
5685
  name: "@posthog/agent",
5686
- version: "2.3.259",
5686
+ version: "2.3.263",
5687
5687
  repository: "https://github.com/PostHog/code",
5688
5688
  description: "TypeScript agent framework wrapping Claude Agent SDK with Git-based task execution for PostHog",
5689
5689
  exports: {
@@ -5844,7 +5844,9 @@ var POSTHOG_NOTIFICATIONS = {
5844
5844
  /** Marks a boundary for log compaction */
5845
5845
  COMPACT_BOUNDARY: "_posthog/compact_boundary",
5846
5846
  /** Token usage update for a session turn */
5847
- USAGE_UPDATE: "_posthog/usage_update"
5847
+ USAGE_UPDATE: "_posthog/usage_update",
5848
+ /** Response to a relayed permission request (plan approval, question) */
5849
+ PERMISSION_RESPONSE: "_posthog/permission_response"
5848
5850
  };
5849
5851
  function isNotification(method, notification) {
5850
5852
  if (!method) return false;
@@ -8281,6 +8283,11 @@ async function canUseTool(context) {
8281
8283
  if (planFileResult) {
8282
8284
  return planFileResult;
8283
8285
  }
8286
+ if (session.permissionMode === "plan") {
8287
+ const message = `This tool is not available in plan mode. Write your plan to a file in ${getClaudePlansDir()} and call ExitPlanMode when ready.`;
8288
+ await emitToolDenial(context, message);
8289
+ return { behavior: "deny", message, interrupt: false };
8290
+ }
8284
8291
  return handleDefaultPermissionFlow(context);
8285
8292
  }
8286
8293
 
@@ -12219,13 +12226,27 @@ var userMessageParamsSchema = import_v4.z.object({
12219
12226
  import_v4.z.array(import_v4.z.record(import_v4.z.string(), import_v4.z.unknown())).min(1, "Content is required")
12220
12227
  ])
12221
12228
  });
12229
+ var permissionResponseParamsSchema = import_v4.z.object({
12230
+ requestId: import_v4.z.string().min(1, "requestId is required"),
12231
+ optionId: import_v4.z.string().min(1, "optionId is required"),
12232
+ customInput: import_v4.z.string().optional(),
12233
+ answers: import_v4.z.record(import_v4.z.string(), import_v4.z.string()).optional()
12234
+ });
12235
+ var setConfigOptionParamsSchema = import_v4.z.object({
12236
+ configId: import_v4.z.string().min(1, "configId is required"),
12237
+ value: import_v4.z.string().min(1, "value is required")
12238
+ });
12222
12239
  var commandParamsSchemas = {
12223
12240
  user_message: userMessageParamsSchema,
12224
12241
  "posthog/user_message": userMessageParamsSchema,
12225
12242
  cancel: import_v4.z.object({}).optional(),
12226
12243
  "posthog/cancel": import_v4.z.object({}).optional(),
12227
12244
  close: import_v4.z.object({}).optional(),
12228
- "posthog/close": import_v4.z.object({}).optional()
12245
+ "posthog/close": import_v4.z.object({}).optional(),
12246
+ permission_response: permissionResponseParamsSchema,
12247
+ "posthog/permission_response": permissionResponseParamsSchema,
12248
+ set_config_option: setConfigOptionParamsSchema,
12249
+ "posthog/set_config_option": setConfigOptionParamsSchema
12229
12250
  };
12230
12251
  function validateCommandParams(method, params) {
12231
12252
  const schema = commandParamsSchemas[method] ?? commandParamsSchemas[method.replace("posthog/", "")];
@@ -12342,6 +12363,7 @@ var AgentServer = class _AgentServer {
12342
12363
  // causing a second session to be created and duplicate Slack messages to be sent.
12343
12364
  initializationPromise = null;
12344
12365
  pendingEvents = [];
12366
+ pendingPermissions = /* @__PURE__ */ new Map();
12345
12367
  detachSseController(controller) {
12346
12368
  if (this.session?.sseController === controller) {
12347
12369
  this.session.sseController = null;
@@ -12379,6 +12401,9 @@ var AgentServer = class _AgentServer {
12379
12401
  getEffectiveMode(payload) {
12380
12402
  return payload.mode ?? this.config.mode;
12381
12403
  }
12404
+ getSessionPermissionMode() {
12405
+ return this.session?.permissionMode ?? "default";
12406
+ }
12382
12407
  createApp() {
12383
12408
  const app = new import_hono.Hono();
12384
12409
  app.get("/health", (c) => {
@@ -12423,6 +12448,7 @@ var AgentServer = class _AgentServer {
12423
12448
  await this.initializeSession(payload, sseController);
12424
12449
  } else {
12425
12450
  this.session.sseController = sseController;
12451
+ this.session.hasDesktopConnected = true;
12426
12452
  this.replayPendingEvents();
12427
12453
  }
12428
12454
  this.sendSseEvent(sseController, {
@@ -12612,12 +12638,9 @@ var AgentServer = class _AgentServer {
12612
12638
  prompt,
12613
12639
  ...this.detectedPrUrl && {
12614
12640
  _meta: {
12615
- prContext: `IMPORTANT \u2014 OVERRIDE PREVIOUS INSTRUCTIONS ABOUT CREATING BRANCHES/PRs.
12616
- You already have an open pull request: ${this.detectedPrUrl}
12617
- You MUST:
12618
- 1. Check out the existing PR branch with \`gh pr checkout ${this.detectedPrUrl}\`
12619
- 2. Make changes, commit, and push to that branch
12620
- You MUST NOT create a new branch, close the existing PR, or create a new PR.`
12641
+ // Keep the live-session PR override aligned with the startup
12642
+ // prompt policy so non-Slack runs remain review-first.
12643
+ prContext: this.buildDetectedPrContext(this.detectedPrUrl)
12621
12644
  }
12622
12645
  }
12623
12646
  });
@@ -12665,6 +12688,43 @@ You MUST NOT create a new branch, close the existing PR, or create a new PR.`
12665
12688
  await this.cleanupSession();
12666
12689
  return { closed: true };
12667
12690
  }
12691
+ case "posthog/set_config_option":
12692
+ case "set_config_option": {
12693
+ const configId = params.configId;
12694
+ const value = params.value;
12695
+ this.logger.info("Set config option requested", { configId, value });
12696
+ const result = await this.session.clientConnection.setSessionConfigOption({
12697
+ sessionId: this.session.acpSessionId,
12698
+ configId,
12699
+ value
12700
+ });
12701
+ return {
12702
+ configOptions: result.configOptions
12703
+ };
12704
+ }
12705
+ case POSTHOG_NOTIFICATIONS.PERMISSION_RESPONSE:
12706
+ case "permission_response": {
12707
+ const requestId = params.requestId;
12708
+ const optionId = params.optionId;
12709
+ const customInput = params.customInput;
12710
+ const answers = params.answers;
12711
+ this.logger.info("Permission response received", {
12712
+ requestId,
12713
+ optionId
12714
+ });
12715
+ const resolved = this.resolvePermission(
12716
+ requestId,
12717
+ optionId,
12718
+ customInput,
12719
+ answers
12720
+ );
12721
+ if (!resolved) {
12722
+ throw new Error(
12723
+ `No pending permission request found for id: ${requestId}`
12724
+ );
12725
+ }
12726
+ return { resolved: true };
12727
+ }
12668
12728
  default:
12669
12729
  throw new Error(`Unknown method: ${method}`);
12670
12730
  }
@@ -12783,6 +12843,8 @@ You MUST NOT create a new branch, close the existing PR, or create a new PR.`
12783
12843
  if (prUrl) {
12784
12844
  this.detectedPrUrl = prUrl;
12785
12845
  }
12846
+ const runState = preTaskRun?.state;
12847
+ const initialPermissionMode = typeof runState?.initial_permission_mode === "string" ? runState.initial_permission_mode : "bypassPermissions";
12786
12848
  const sessionResponse = await clientConnection.newSession({
12787
12849
  cwd: this.config.repositoryPath ?? "/tmp/workspace",
12788
12850
  mcpServers: this.config.mcpServers ?? [],
@@ -12792,6 +12854,7 @@ You MUST NOT create a new branch, close the existing PR, or create a new PR.`
12792
12854
  systemPrompt: this.buildSessionSystemPrompt(prUrl),
12793
12855
  allowedDomains: this.config.allowedDomains,
12794
12856
  jsonSchema: preTask?.json_schema ?? null,
12857
+ permissionMode: initialPermissionMode,
12795
12858
  ...this.config.claudeCode?.plugins?.length && {
12796
12859
  claudeCode: {
12797
12860
  options: {
@@ -12814,7 +12877,9 @@ You MUST NOT create a new branch, close the existing PR, or create a new PR.`
12814
12877
  treeTracker,
12815
12878
  sseController,
12816
12879
  deviceInfo,
12817
- logWriter
12880
+ logWriter,
12881
+ permissionMode: initialPermissionMode,
12882
+ hasDesktopConnected: sseController !== null
12818
12883
  };
12819
12884
  this.logger = new Logger({
12820
12885
  debug: true,
@@ -12828,6 +12893,7 @@ You MUST NOT create a new branch, close the existing PR, or create a new PR.`
12828
12893
  this.logger.info(
12829
12894
  `Agent version: ${this.config.version ?? package_default.version}`
12830
12895
  );
12896
+ this.logger.info(`Initial permission mode: ${initialPermissionMode}`);
12831
12897
  this.posthogAPI.updateTaskRun(payload.task_id, payload.run_id, {
12832
12898
  status: "in_progress"
12833
12899
  }).catch(
@@ -13077,13 +13143,38 @@ ${toolSummary}`);
13077
13143
  }
13078
13144
  return { append: cloudAppend };
13079
13145
  }
13146
+ getCloudInteractionOrigin() {
13147
+ return process.env.POSTHOG_CODE_INTERACTION_ORIGIN ?? process.env.CODE_INTERACTION_ORIGIN ?? process.env.TWIG_INTERACTION_ORIGIN;
13148
+ }
13149
+ /**
13150
+ * Slack-origin cloud runs auto-publish by default. Every other origin is
13151
+ * review-first unless the user explicitly asks, and createPr=false always
13152
+ * disables publishing.
13153
+ */
13154
+ shouldAutoPublishCloudChanges() {
13155
+ return this.getCloudInteractionOrigin() === "slack" && this.config.createPr !== false;
13156
+ }
13157
+ buildDetectedPrContext(prUrl) {
13158
+ if (!this.shouldAutoPublishCloudChanges()) {
13159
+ return `An open pull request already exists: ${prUrl}
13160
+ Use that PR as context if it is helpful, but stop with local changes ready for review.
13161
+ Do NOT create commits, push to the PR branch, update the pull request, create a new branch, or create a new pull request unless the user explicitly asks.`;
13162
+ }
13163
+ return `IMPORTANT \u2014 OVERRIDE PREVIOUS INSTRUCTIONS ABOUT CREATING BRANCHES/PRs.
13164
+ You already have an open pull request: ${prUrl}
13165
+ You MUST:
13166
+ 1. Check out the existing PR branch with \`gh pr checkout ${prUrl}\`
13167
+ 2. Make changes, commit, and push to that branch
13168
+ You MUST NOT create a new branch, close the existing PR, or create a new PR.`;
13169
+ }
13080
13170
  buildCloudSystemPrompt(prUrl) {
13081
13171
  const taskId = this.config.taskId;
13172
+ const shouldAutoCreatePr = this.shouldAutoPublishCloudChanges();
13082
13173
  const attributionInstructions = `
13083
13174
  ## Attribution
13084
13175
  Do NOT use Claude Code's default attribution (no "Co-Authored-By" trailers, no "Generated with [Claude Code]" lines).
13085
13176
 
13086
- Instead, add the following trailers to EVERY commit message (after a blank line at the end):
13177
+ If you create a commit, add the following trailers to the commit message (after a blank line at the end):
13087
13178
  Generated-By: PostHog Code
13088
13179
  Task-Id: ${taskId}
13089
13180
 
@@ -13098,6 +13189,20 @@ EOF
13098
13189
  )"
13099
13190
  \`\`\``;
13100
13191
  if (prUrl) {
13192
+ if (!shouldAutoCreatePr) {
13193
+ return `
13194
+ # Cloud Task Execution
13195
+
13196
+ This task already has an open pull request: ${prUrl}
13197
+
13198
+ Do the requested work, but stop with local changes ready for review.
13199
+
13200
+ Important:
13201
+ - Do NOT create new commits, push to the branch, or update the pull request unless the user explicitly asks.
13202
+ - Do NOT create a new branch or a new pull request.
13203
+ ${attributionInstructions}
13204
+ `;
13205
+ }
13101
13206
  return `
13102
13207
  # Cloud Task Execution
13103
13208
 
@@ -13131,6 +13236,17 @@ When the user asks for code changes or software engineering tasks:
13131
13236
  Important:
13132
13237
  - Do NOT create branches, commits, or pull requests in this mode.
13133
13238
  - Prefer using MCP tools to answer questions with real data over giving generic advice.
13239
+ `;
13240
+ }
13241
+ if (!shouldAutoCreatePr) {
13242
+ return `
13243
+ # Cloud Task Execution
13244
+
13245
+ Do the requested work, but stop with local changes ready for review.
13246
+
13247
+ Important:
13248
+ - Do NOT create a branch, commit, push, or open a pull request unless the user explicitly asks.
13249
+ ${attributionInstructions}
13134
13250
  `;
13135
13251
  }
13136
13252
  return `
@@ -13239,32 +13355,73 @@ ${attributionInstructions}
13239
13355
  LLM_GATEWAY_URL: gatewayUrl
13240
13356
  });
13241
13357
  }
13358
+ buildSlackQuestionRelayResponse(payload, toolMeta2) {
13359
+ this.relaySlackQuestion(payload, toolMeta2);
13360
+ return {
13361
+ outcome: { outcome: "cancelled" },
13362
+ _meta: {
13363
+ message: "This question has been relayed to the Slack thread where this task originated. The user will reply there. Do NOT re-ask the question or pick an answer yourself. Simply let the user know you are waiting for their reply."
13364
+ }
13365
+ };
13366
+ }
13367
+ shouldBlockPublishPermission(params) {
13368
+ if (this.config.createPr !== false) {
13369
+ return false;
13370
+ }
13371
+ const meta = params.toolCall?._meta && typeof params.toolCall._meta === "object" && !Array.isArray(params.toolCall._meta) ? params.toolCall._meta : null;
13372
+ const rawInput = params.toolCall?.rawInput && typeof params.toolCall.rawInput === "object" && !Array.isArray(params.toolCall.rawInput) ? params.toolCall.rawInput : null;
13373
+ const toolName = typeof meta?.toolName === "string" ? meta.toolName : null;
13374
+ const command = typeof rawInput?.command === "string" ? rawInput.command : null;
13375
+ return Boolean(
13376
+ toolName && (toolName === "Bash" || toolName.includes("bash")) && command && /\bgit\s+push\b|\bgh\s+pr\s+(create|edit|ready|merge)\b/.test(command)
13377
+ );
13378
+ }
13242
13379
  createCloudClient(payload) {
13243
13380
  const mode = this.getEffectiveMode(payload);
13244
- const interactionOrigin = process.env.CODE_INTERACTION_ORIGIN ?? process.env.TWIG_INTERACTION_ORIGIN;
13381
+ const interactionOrigin = process.env.POSTHOG_CODE_INTERACTION_ORIGIN ?? process.env.CODE_INTERACTION_ORIGIN ?? process.env.TWIG_INTERACTION_ORIGIN;
13245
13382
  return {
13246
13383
  requestPermission: async (params) => {
13247
13384
  this.logger.debug("Permission request", {
13248
13385
  mode,
13249
13386
  interactionOrigin,
13387
+ kind: params.toolCall?.kind,
13250
13388
  options: params.options
13251
13389
  });
13252
13390
  const allowOption = params.options.find(
13253
13391
  (o) => o.kind === "allow_once" || o.kind === "allow_always"
13254
13392
  );
13255
13393
  const selectedOptionId = allowOption?.optionId ?? params.options[0].optionId;
13394
+ const codeToolKind = params.toolCall?._meta?.codeToolKind;
13395
+ const isPlanApproval = params.toolCall?.kind === "switch_mode";
13256
13396
  if (interactionOrigin === "slack") {
13257
- const codeToolKind = params.toolCall?._meta?.codeToolKind;
13258
13397
  if (codeToolKind === "question") {
13259
- this.relaySlackQuestion(payload, params.toolCall?._meta);
13260
- return {
13261
- outcome: { outcome: "cancelled" },
13262
- _meta: {
13263
- message: "This question has been relayed to the Slack thread where this task originated. The user will reply there. Do NOT re-ask the question or pick an answer yourself. Simply let the user know you are waiting for their reply."
13264
- }
13265
- };
13398
+ return this.buildSlackQuestionRelayResponse(
13399
+ payload,
13400
+ params.toolCall?._meta
13401
+ );
13402
+ }
13403
+ }
13404
+ {
13405
+ const isQuestion = codeToolKind === "question";
13406
+ const sessionPermissionMode = this.getSessionPermissionMode();
13407
+ const needsRelay = isQuestion || isPlanApproval || sessionPermissionMode === "default";
13408
+ if (needsRelay && this.session?.hasDesktopConnected) {
13409
+ this.logger.info("Relaying permission to connected client", {
13410
+ kind: params.toolCall?.kind,
13411
+ isQuestion,
13412
+ sessionPermissionMode
13413
+ });
13414
+ return this.relayPermissionToClient(params);
13266
13415
  }
13267
13416
  }
13417
+ if (this.shouldBlockPublishPermission(params)) {
13418
+ return {
13419
+ outcome: { outcome: "cancelled" },
13420
+ _meta: {
13421
+ message: "This run is configured to stop before publishing. Do not push commits or create/update pull requests unless the user explicitly asks."
13422
+ }
13423
+ };
13424
+ }
13268
13425
  return {
13269
13426
  outcome: {
13270
13427
  outcome: "selected",
@@ -13276,6 +13433,12 @@ ${attributionInstructions}
13276
13433
  this.logger.debug("Extension notification", { method, params });
13277
13434
  },
13278
13435
  sessionUpdate: async (params) => {
13436
+ if (params.update?.sessionUpdate === "current_mode_update" && typeof params.update?.currentModeId === "string" && this.session) {
13437
+ this.session.permissionMode = params.update.currentModeId;
13438
+ this.logger.info("Permission mode updated", {
13439
+ mode: params.update.currentModeId
13440
+ });
13441
+ }
13279
13442
  if (params.update?.sessionUpdate === "tool_call_update") {
13280
13443
  const meta = params.update?._meta?.claudeCode;
13281
13444
  const toolName = meta?.toolName;
@@ -13454,6 +13617,13 @@ ${attributionInstructions}
13454
13617
  } catch (error) {
13455
13618
  this.logger.error("Failed to flush session logs", error);
13456
13619
  }
13620
+ for (const [, pending] of this.pendingPermissions) {
13621
+ pending.resolve({
13622
+ outcome: { outcome: "selected", optionId: "reject" },
13623
+ _meta: { customInput: "Session is shutting down." }
13624
+ });
13625
+ }
13626
+ this.pendingPermissions.clear();
13457
13627
  try {
13458
13628
  await this.session.acpConnection.cleanup();
13459
13629
  } catch (error) {
@@ -13531,6 +13701,39 @@ ${attributionInstructions}
13531
13701
  this.detachSseController(controller);
13532
13702
  }
13533
13703
  }
13704
+ /**
13705
+ * Relay a permission request (e.g., plan approval) to the connected desktop
13706
+ * app via SSE and wait for a response via the `/command` endpoint.
13707
+ *
13708
+ * The promise waits indefinitely — if SSE is disconnected, the event is
13709
+ * buffered by broadcastEvent and replayed when the client reconnects. Session
13710
+ * cleanup force-resolves all pending permissions, so there is no leak.
13711
+ */
13712
+ relayPermissionToClient(params) {
13713
+ const requestId = crypto.randomUUID();
13714
+ this.broadcastEvent({
13715
+ type: "permission_request",
13716
+ requestId,
13717
+ options: params.options,
13718
+ toolCall: params.toolCall
13719
+ });
13720
+ return new Promise((resolve4) => {
13721
+ this.pendingPermissions.set(requestId, { resolve: resolve4 });
13722
+ });
13723
+ }
13724
+ resolvePermission(requestId, optionId, customInput, answers) {
13725
+ const pending = this.pendingPermissions.get(requestId);
13726
+ if (!pending) return false;
13727
+ this.pendingPermissions.delete(requestId);
13728
+ const meta = {};
13729
+ if (customInput) meta.customInput = customInput;
13730
+ if (answers) meta.answers = answers;
13731
+ pending.resolve({
13732
+ outcome: { outcome: "selected", optionId },
13733
+ ...Object.keys(meta).length > 0 ? { _meta: meta } : {}
13734
+ });
13735
+ return true;
13736
+ }
13534
13737
  };
13535
13738
 
13536
13739
  // src/server/bin.ts
@@ -13549,6 +13752,12 @@ var envSchema = import_v42.z.object({
13549
13752
  }).regex(/^\d+$/, "POSTHOG_PROJECT_ID must be a numeric string").transform((val) => parseInt(val, 10))
13550
13753
  });
13551
13754
  var program = new import_commander.Command();
13755
+ function parseBooleanOption(raw, flag) {
13756
+ if (raw === void 0) return void 0;
13757
+ if (raw === "true") return true;
13758
+ if (raw === "false") return false;
13759
+ program.error(`${flag} must be either "true" or "false"`);
13760
+ }
13552
13761
  function parseJsonOption(raw, schema, flag) {
13553
13762
  if (!raw) return void 0;
13554
13763
  let parsed;
@@ -13572,7 +13781,7 @@ program.name("agent-server").description("PostHog cloud agent server - runs in s
13572
13781
  ).option("--repositoryPath <path>", "Path to the repository").requiredOption("--taskId <id>", "Task ID").requiredOption("--runId <id>", "Task run ID").option(
13573
13782
  "--mcpServers <json>",
13574
13783
  "MCP servers config as JSON array (ACP McpServer[] format)"
13575
- ).option("--baseBranch <branch>", "Base branch for PR creation").option(
13784
+ ).option("--createPr <boolean>", "Whether this run may publish changes").option("--baseBranch <branch>", "Base branch for PR creation").option(
13576
13785
  "--claudeCodeConfig <json>",
13577
13786
  "Claude Code config as JSON (systemPrompt, systemPromptAppend, plugins)"
13578
13787
  ).option(
@@ -13588,6 +13797,7 @@ ${errors}`);
13588
13797
  }
13589
13798
  const env = envResult.data;
13590
13799
  const mode = options.mode === "background" ? "background" : "interactive";
13800
+ const createPr = parseBooleanOption(options.createPr, "--createPr");
13591
13801
  const mcpServers = parseJsonOption(
13592
13802
  options.mcpServers,
13593
13803
  mcpServersSchema,
@@ -13609,6 +13819,7 @@ ${errors}`);
13609
13819
  mode,
13610
13820
  taskId: options.taskId,
13611
13821
  runId: options.runId,
13822
+ createPr,
13612
13823
  mcpServers,
13613
13824
  baseBranch: options.baseBranch,
13614
13825
  claudeCode,