@kodizm/acp 0.8.2 → 0.9.0

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.
Files changed (2) hide show
  1. package/dist/index.js +988 -270
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -46,6 +46,75 @@ var __esm = (fn, res, err) => () => {
46
46
  };
47
47
  var __require = import.meta.require;
48
48
 
49
+ // package.json
50
+ var package_default;
51
+ var init_package = __esm(() => {
52
+ package_default = {
53
+ name: "@kodizm/acp",
54
+ version: "0.9.0",
55
+ description: "Custom ACP server bridging Claude Code and opencode CLIs through a single Kodizm-flavored protocol surface.",
56
+ type: "module",
57
+ bin: {
58
+ "kodizm-acp": "./dist/index.js"
59
+ },
60
+ main: "./dist/index.js",
61
+ files: [
62
+ "dist/",
63
+ "README.md",
64
+ "LICENSE"
65
+ ],
66
+ scripts: {
67
+ dev: "bun run src/index.ts",
68
+ build: "bun build src/index.ts --target=bun --outdir=dist",
69
+ test: "bun test test/unit test/e2e",
70
+ "test:unit": "bun test test/unit",
71
+ "test:e2e": "bun test test/e2e",
72
+ "test:integration": "./scripts/reap-opencode.sh bun test test/integration",
73
+ "test:watch": "bun test --watch",
74
+ lint: "biome check src test",
75
+ "lint:fix": "biome check --write src test",
76
+ format: "biome format --write src test",
77
+ typecheck: "tsc --noEmit",
78
+ "typecheck:src": "tsc --noEmit -p tsconfig.publish.json",
79
+ prepublishOnly: "bun run build && bun run typecheck:src"
80
+ },
81
+ engines: {
82
+ bun: ">=1.1.0"
83
+ },
84
+ publishConfig: {
85
+ access: "public"
86
+ },
87
+ config: {
88
+ claudeAgentSdk: "0.3.280"
89
+ },
90
+ devDependencies: {
91
+ "@anthropic-ai/claude-agent-sdk": "0.3.280",
92
+ "@anthropic-ai/sdk": ">=0.93.0",
93
+ "@biomejs/biome": "^1.9.0",
94
+ "@modelcontextprotocol/sdk": "^1.29.0",
95
+ "@opencode-ai/sdk": "^1.18.32",
96
+ "@types/bun": "latest",
97
+ typescript: "^5.6.0",
98
+ zod: "^4.0.0"
99
+ },
100
+ license: "Apache-2.0",
101
+ homepage: "https://github.com/kodizm/acp",
102
+ repository: {
103
+ type: "git",
104
+ url: "git+https://github.com/kodizm/acp.git"
105
+ },
106
+ keywords: [
107
+ "acp",
108
+ "agent-client-protocol",
109
+ "claude",
110
+ "opencode",
111
+ "ai",
112
+ "bridge",
113
+ "jsonrpc"
114
+ ]
115
+ };
116
+ });
117
+
49
118
  // src/server/errors.ts
50
119
  var JsonRpcErrorCode, JsonRpcError, InvalidParamsError, SessionNotFoundError, MethodNotSupportedError, AcpTimeoutError, BackendNotConfiguredError, UnknownBackendError;
51
120
  var init_errors = __esm(() => {
@@ -5206,58 +5275,7 @@ var init_events = __esm(() => {
5206
5275
  ]);
5207
5276
  });
5208
5277
 
5209
- // src/backends/claude/permission-bridge.ts
5210
- function buildCanUseTool(deps) {
5211
- return async function canUseTool(toolName, input, options) {
5212
- const payload = {
5213
- sessionId: deps.sessionId,
5214
- toolCall: {
5215
- toolCallId: options.toolUseID,
5216
- rawInput: input,
5217
- title: options.title ?? toolName,
5218
- kind: "tool_use"
5219
- },
5220
- options: DEFAULT_OPTIONS
5221
- };
5222
- if (options.agentID !== undefined) {
5223
- payload.agentId = options.agentID;
5224
- }
5225
- const event = {
5226
- sessionId: deps.sessionId,
5227
- type: "permission_request",
5228
- toolUseId: options.toolUseID,
5229
- name: toolName,
5230
- options: DEFAULT_OPTIONS.map((o) => ({ optionId: o.optionId, label: o.name })),
5231
- ...options.agentID === undefined ? {} : { agentId: options.agentID, parentSessionId: deps.sessionId }
5232
- };
5233
- deps.emit.send(event);
5234
- const raceOptions = {
5235
- signal: composeSignals(deps.signal, options.signal)
5236
- };
5237
- if (deps.permissionTimeoutMs !== undefined) {
5238
- raceOptions.timeoutMs = deps.permissionTimeoutMs;
5239
- }
5240
- if (deps.deferTimeoutMs !== undefined && deps.onDefer !== undefined) {
5241
- raceOptions.deferTimeoutMs = deps.deferTimeoutMs;
5242
- }
5243
- let raced;
5244
- try {
5245
- raced = await awaitPermissionResponse(deps.server, "session/request_permission", payload, raceOptions);
5246
- } catch (error) {
5247
- if (error instanceof AcpTimeoutError) {
5248
- return { behavior: "deny", message: "Permission RPC timed out" };
5249
- }
5250
- throw error;
5251
- }
5252
- if (raced === DEFERRED_SENTINEL) {
5253
- if (deps.onDefer === undefined) {
5254
- return { behavior: "deny", message: "Permission deferred (no handler)" };
5255
- }
5256
- return await deps.onDefer({ toolName, input, options });
5257
- }
5258
- return mapPermissionOutcome(raced, input, options.suggestions);
5259
- };
5260
- }
5278
+ // src/backends/permission-rpc.ts
5261
5279
  async function awaitPermissionResponse(server, method, params, options = {}) {
5262
5280
  const racers = [server.request(method, params)];
5263
5281
  let timeoutHandle;
@@ -5304,49 +5322,9 @@ async function awaitPermissionResponse(server, method, params, options = {}) {
5304
5322
  }
5305
5323
  }
5306
5324
  }
5307
- function composeSignals(a, b) {
5308
- if (a.aborted)
5309
- return a;
5310
- if (b.aborted)
5311
- return b;
5312
- const controller = new AbortController;
5313
- const onAbort = () => controller.abort();
5314
- a.addEventListener("abort", onAbort, { once: true });
5315
- b.addEventListener("abort", onAbort, { once: true });
5316
- return controller.signal;
5317
- }
5318
- function mapPermissionOutcome(response, input, suggestions) {
5319
- const outcome = response.outcome;
5320
- if (outcome.outcome === "cancelled") {
5321
- throw new Error("Tool use aborted");
5322
- }
5323
- if (outcome.optionId === "allow") {
5324
- return { behavior: "allow", updatedInput: input };
5325
- }
5326
- if (outcome.optionId === "allow_always") {
5327
- return {
5328
- behavior: "allow",
5329
- updatedInput: input,
5330
- updatedPermissions: suggestions !== undefined && suggestions.length > 0 ? suggestions : [
5331
- {
5332
- type: "addRules",
5333
- rules: [{ toolName: "unknown" }],
5334
- behavior: "allow",
5335
- destination: "session"
5336
- }
5337
- ]
5338
- };
5339
- }
5340
- return { behavior: "deny", message: "User refused permission to run tool" };
5341
- }
5342
- var DEFAULT_OPTIONS, DEFERRED_SENTINEL;
5343
- var init_permission_bridge = __esm(() => {
5325
+ var DEFERRED_SENTINEL;
5326
+ var init_permission_rpc = __esm(() => {
5344
5327
  init_errors();
5345
- DEFAULT_OPTIONS = [
5346
- { kind: "allow_always", name: "Always allow this tool", optionId: "allow_always" },
5347
- { kind: "allow_once", name: "Allow", optionId: "allow" },
5348
- { kind: "reject_once", name: "Reject", optionId: "reject" }
5349
- ];
5350
5328
  DEFERRED_SENTINEL = Symbol.for("kodizm.permission.deferred");
5351
5329
  });
5352
5330
 
@@ -5399,7 +5377,7 @@ var QuestionsArraySchema;
5399
5377
  var init_ask_user_question = __esm(() => {
5400
5378
  init_zod();
5401
5379
  init_events();
5402
- init_permission_bridge();
5380
+ init_permission_rpc();
5403
5381
  QuestionsArraySchema = array(KodizmQuestionSchema).min(1).max(4);
5404
5382
  });
5405
5383
 
@@ -5784,6 +5762,104 @@ function buildSdkServer(server) {
5784
5762
  return sdkServer;
5785
5763
  }
5786
5764
 
5765
+ // src/backends/claude/permission-bridge.ts
5766
+ function buildCanUseTool(deps) {
5767
+ return async function canUseTool(toolName, input, options) {
5768
+ const payload = {
5769
+ sessionId: deps.sessionId,
5770
+ toolCall: {
5771
+ toolCallId: options.toolUseID,
5772
+ rawInput: input,
5773
+ title: options.title ?? toolName,
5774
+ kind: "tool_use"
5775
+ },
5776
+ options: DEFAULT_OPTIONS
5777
+ };
5778
+ if (options.agentID !== undefined) {
5779
+ payload.agentId = options.agentID;
5780
+ }
5781
+ const event = {
5782
+ sessionId: deps.sessionId,
5783
+ type: "permission_request",
5784
+ toolUseId: options.toolUseID,
5785
+ name: toolName,
5786
+ options: DEFAULT_OPTIONS.map((o) => ({ optionId: o.optionId, label: o.name })),
5787
+ ...options.agentID === undefined ? {} : { agentId: options.agentID, parentSessionId: deps.sessionId }
5788
+ };
5789
+ deps.emit.send(event);
5790
+ const raceOptions = {
5791
+ signal: composeSignals(deps.signal, options.signal)
5792
+ };
5793
+ if (deps.permissionTimeoutMs !== undefined) {
5794
+ raceOptions.timeoutMs = deps.permissionTimeoutMs;
5795
+ }
5796
+ if (deps.deferTimeoutMs !== undefined && deps.onDefer !== undefined) {
5797
+ raceOptions.deferTimeoutMs = deps.deferTimeoutMs;
5798
+ }
5799
+ let raced;
5800
+ try {
5801
+ raced = await awaitPermissionResponse(deps.server, "session/request_permission", payload, raceOptions);
5802
+ } catch (error) {
5803
+ if (error instanceof AcpTimeoutError) {
5804
+ return { behavior: "deny", message: "Permission RPC timed out" };
5805
+ }
5806
+ throw error;
5807
+ }
5808
+ if (raced === DEFERRED_SENTINEL) {
5809
+ if (deps.onDefer === undefined) {
5810
+ return { behavior: "deny", message: "Permission deferred (no handler)" };
5811
+ }
5812
+ return await deps.onDefer({ toolName, input, options });
5813
+ }
5814
+ return mapPermissionOutcome(raced, input, options.suggestions);
5815
+ };
5816
+ }
5817
+ function composeSignals(a, b) {
5818
+ if (a.aborted)
5819
+ return a;
5820
+ if (b.aborted)
5821
+ return b;
5822
+ const controller = new AbortController;
5823
+ const onAbort = () => controller.abort();
5824
+ a.addEventListener("abort", onAbort, { once: true });
5825
+ b.addEventListener("abort", onAbort, { once: true });
5826
+ return controller.signal;
5827
+ }
5828
+ function mapPermissionOutcome(response, input, suggestions) {
5829
+ const outcome = response.outcome;
5830
+ if (outcome.outcome === "cancelled") {
5831
+ throw new Error("Tool use aborted");
5832
+ }
5833
+ if (outcome.optionId === "allow") {
5834
+ return { behavior: "allow", updatedInput: input };
5835
+ }
5836
+ if (outcome.optionId === "allow_always") {
5837
+ return {
5838
+ behavior: "allow",
5839
+ updatedInput: input,
5840
+ updatedPermissions: suggestions !== undefined && suggestions.length > 0 ? suggestions : [
5841
+ {
5842
+ type: "addRules",
5843
+ rules: [{ toolName: "unknown" }],
5844
+ behavior: "allow",
5845
+ destination: "session"
5846
+ }
5847
+ ]
5848
+ };
5849
+ }
5850
+ return { behavior: "deny", message: "User refused permission to run tool" };
5851
+ }
5852
+ var DEFAULT_OPTIONS;
5853
+ var init_permission_bridge = __esm(() => {
5854
+ init_errors();
5855
+ init_permission_rpc();
5856
+ DEFAULT_OPTIONS = [
5857
+ { kind: "allow_always", name: "Always allow this tool", optionId: "allow_always" },
5858
+ { kind: "allow_once", name: "Allow", optionId: "allow" },
5859
+ { kind: "reject_once", name: "Reject", optionId: "reject" }
5860
+ ];
5861
+ });
5862
+
5787
5863
  // src/wire/policy.ts
5788
5864
  function parseCanonicalPattern(input) {
5789
5865
  if (input.length === 0) {
@@ -51522,7 +51598,7 @@ async function safeReply2(sdk, requestId, body) {
51522
51598
  }
51523
51599
  var CANONICAL_OPTIONS;
51524
51600
  var init_permission_bridge2 = __esm(() => {
51525
- init_permission_bridge();
51601
+ init_permission_rpc();
51526
51602
  CANONICAL_OPTIONS = [
51527
51603
  { optionId: "allow", label: "Allow" },
51528
51604
  { optionId: "allow_always", label: "Always allow" },
@@ -52194,11 +52270,17 @@ function apiStopReason(reason) {
52194
52270
  function turnFailed(hook) {
52195
52271
  const error = text(hook.error) ?? "unknown";
52196
52272
  const details = text(hook.error_details);
52273
+ return turnFailedWith(failureReason(error), details === undefined ? `StopFailure ${error}` : `StopFailure ${error}: ${details}`.slice(0, TITLE_MAX * 2));
52274
+ }
52275
+ function turnFailedWith(reason, detail) {
52197
52276
  return draft("turn_failed", {
52198
- reason: FAILURE_REASONS[error] ?? "api_error",
52199
- detail: details === undefined ? `StopFailure ${error}` : `StopFailure ${error}: ${details}`.slice(0, TITLE_MAX * 2)
52277
+ reason,
52278
+ detail
52200
52279
  });
52201
52280
  }
52281
+ function failureReason(error) {
52282
+ return FAILURE_REASONS[error] ?? "api_error";
52283
+ }
52202
52284
  function sessionEnded(reason) {
52203
52285
  return draft("session_ended", { reason });
52204
52286
  }
@@ -52365,11 +52447,30 @@ async function buildPaneSettings(options) {
52365
52447
  }
52366
52448
  ]
52367
52449
  ]));
52368
- return {
52450
+ const settings = {
52369
52451
  ...base,
52370
52452
  disableAllHooks: false,
52453
+ autoContinueAtUsageLimit: false,
52371
52454
  hooks
52372
52455
  };
52456
+ if (options.rules !== undefined) {
52457
+ settings.permissions = mergePermissions(base.permissions, options.rules);
52458
+ }
52459
+ return settings;
52460
+ }
52461
+ function mergePermissions(base, rules) {
52462
+ const permissions = isRecord2(base) ? base : {};
52463
+ const merge = (list) => {
52464
+ const own = permissions[list];
52465
+ const entries = Array.isArray(own) ? own.filter((entry) => typeof entry === "string") : [];
52466
+ return [...new Set([...entries, ...rules[list]])];
52467
+ };
52468
+ return {
52469
+ ...permissions,
52470
+ allow: merge("allow"),
52471
+ deny: merge("deny"),
52472
+ ask: merge("ask")
52473
+ };
52373
52474
  }
52374
52475
  async function writePaneSettings(path, options) {
52375
52476
  const settings = await buildPaneSettings(options);
@@ -52558,8 +52659,9 @@ var init_input = __esm(() => {
52558
52659
  });
52559
52660
 
52560
52661
  // src/live/pane.ts
52561
- import { mkdir as mkdir3, readFile as readFile3, realpath as realpath3, rename as rename4, writeFile as writeFile2 } from "fs/promises";
52562
- import { join as join6 } from "path";
52662
+ import { randomUUID as randomUUID5 } from "crypto";
52663
+ import { mkdir as mkdir3, readFile as readFile3, realpath as realpath3, rename as rename4, stat as stat2, writeFile as writeFile2 } from "fs/promises";
52664
+ import { dirname as dirname4, join as join6, resolve as resolve5 } from "path";
52563
52665
  function supportsPasteBufferS(versionOutput) {
52564
52666
  const match = /^tmux (?:next-)?(\d+)\.(\d+)[a-z]?$/.exec(versionOutput.trim());
52565
52667
  if (match === null) {
@@ -52585,7 +52687,7 @@ async function createPane(options) {
52585
52687
  }
52586
52688
  const env = paneEnv(options);
52587
52689
  const cwd = await realpath3(options.cwd);
52588
- await seedTrust(env.CLAUDE_CONFIG_DIR === undefined ? [env.HOME] : [env.HOME, env.CLAUDE_CONFIG_DIR], cwd);
52690
+ await seedTrust(env.CLAUDE_CONFIG_DIR === undefined ? [env.HOME] : [env.HOME, env.CLAUDE_CONFIG_DIR], await trustRoots(cwd), approvedKeySuffix(options.env?.ANTHROPIC_API_KEY));
52589
52691
  const { PATH: path, TERM: term, ...sessionEnv } = env;
52590
52692
  const created = await run(pane.socket, [
52591
52693
  "start-server",
@@ -52674,11 +52776,21 @@ function paneEnv(options) {
52674
52776
  ...env,
52675
52777
  HOME: options.home,
52676
52778
  CI: "1",
52677
- TERM: "xterm-256color"
52779
+ TERM: "xterm-256color",
52780
+ CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: "1"
52678
52781
  };
52679
52782
  }
52680
52783
  function launchArgv(options) {
52681
52784
  const argv = [options.claudeBin ?? "claude", "--settings", options.settingsPath, "--setting-sources", ""];
52785
+ if (options.permissionMode !== undefined) {
52786
+ argv.push("--permission-mode", options.permissionMode);
52787
+ }
52788
+ if (options.mcpConfig !== undefined) {
52789
+ argv.push("--mcp-config", options.mcpConfig, "--strict-mcp-config");
52790
+ }
52791
+ if (options.systemPromptFile !== undefined) {
52792
+ argv.push("--append-system-prompt-file", options.systemPromptFile);
52793
+ }
52682
52794
  if (options.model !== undefined) {
52683
52795
  argv.push("--model", options.model);
52684
52796
  }
@@ -52687,26 +52799,129 @@ function launchArgv(options) {
52687
52799
  }
52688
52800
  return argv;
52689
52801
  }
52690
- async function seedTrust(dirs, cwd) {
52802
+ async function trustRoots(cwd) {
52803
+ const roots = new Set([cwd]);
52804
+ const repository = await findGitEntry(cwd);
52805
+ if (repository === null) {
52806
+ return [...roots];
52807
+ }
52808
+ roots.add(repository.root);
52809
+ if (repository.isFile) {
52810
+ const main = await worktreeMainRoot(repository.root);
52811
+ if (main !== null) {
52812
+ roots.add(main);
52813
+ }
52814
+ }
52815
+ return [...roots];
52816
+ }
52817
+ async function findGitEntry(start) {
52818
+ let dir = start;
52819
+ while (true) {
52820
+ const entry = await statIfPresent(join6(dir, ".git"));
52821
+ if (entry !== null) {
52822
+ return {
52823
+ root: dir,
52824
+ isFile: entry.isFile()
52825
+ };
52826
+ }
52827
+ const parent = dirname4(dir);
52828
+ if (parent === dir) {
52829
+ return null;
52830
+ }
52831
+ dir = parent;
52832
+ }
52833
+ }
52834
+ async function worktreeMainRoot(root) {
52835
+ const gitfile = await readTextIfPresent(join6(root, ".git"));
52836
+ const target = gitfile === null ? undefined : /^gitdir:\s*(.+?)\s*$/m.exec(gitfile)?.[1];
52837
+ if (target === undefined) {
52838
+ return null;
52839
+ }
52840
+ const gitdir = resolve5(root, target);
52841
+ const commondir = await readTextIfPresent(join6(gitdir, "commondir"));
52842
+ if (commondir === null) {
52843
+ return null;
52844
+ }
52845
+ return realpathIfPresent(dirname4(resolve5(gitdir, commondir.trim())));
52846
+ }
52847
+ async function statIfPresent(path) {
52848
+ try {
52849
+ return await stat2(path);
52850
+ } catch (error) {
52851
+ if (isAbsent(error)) {
52852
+ return null;
52853
+ }
52854
+ throw error;
52855
+ }
52856
+ }
52857
+ async function readTextIfPresent(path) {
52858
+ try {
52859
+ return await readFile3(path, "utf8");
52860
+ } catch (error) {
52861
+ if (isAbsent(error)) {
52862
+ return null;
52863
+ }
52864
+ throw error;
52865
+ }
52866
+ }
52867
+ async function realpathIfPresent(path) {
52868
+ try {
52869
+ return await realpath3(path);
52870
+ } catch (error) {
52871
+ if (isAbsent(error)) {
52872
+ return null;
52873
+ }
52874
+ throw error;
52875
+ }
52876
+ }
52877
+ function isAbsent(error) {
52878
+ return error instanceof Error && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR");
52879
+ }
52880
+ function approvedKeySuffix(apiKey) {
52881
+ return apiKey === undefined || apiKey === "" ? undefined : apiKey.slice(-20);
52882
+ }
52883
+ function approveKey(current, suffix) {
52884
+ const responses = isRecord3(current) ? current : {};
52885
+ const approved = stringList(responses.approved);
52886
+ return {
52887
+ ...responses,
52888
+ approved: approved.includes(suffix) ? approved : [...approved, suffix],
52889
+ rejected: stringList(responses.rejected)
52890
+ };
52891
+ }
52892
+ function stringList(value) {
52893
+ return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
52894
+ }
52895
+ function seedTrust(dirs, roots, keySuffix) {
52896
+ const seeded = seeding.then(() => writeTrust(dirs, roots, keySuffix));
52897
+ seeding = seeded.catch(() => {
52898
+ return;
52899
+ });
52900
+ return seeded;
52901
+ }
52902
+ async function writeTrust(dirs, roots, keySuffix) {
52691
52903
  for (const dir of dirs) {
52692
52904
  const path = join6(dir, ".claude.json");
52693
52905
  const config = await readConfig(path);
52694
52906
  const projects = isRecord3(config.projects) ? config.projects : {};
52695
- const project = isRecord3(projects[cwd]) ? projects[cwd] : {};
52696
- projects[cwd] = {
52697
- allowedTools: [],
52698
- ...project,
52699
- hasTrustDialogAccepted: true,
52700
- hasCompletedProjectOnboarding: true
52701
- };
52907
+ for (const root of roots) {
52908
+ const project = isRecord3(projects[root]) ? projects[root] : {};
52909
+ projects[root] = {
52910
+ allowedTools: [],
52911
+ ...project,
52912
+ hasTrustDialogAccepted: true,
52913
+ hasCompletedProjectOnboarding: true
52914
+ };
52915
+ }
52702
52916
  const seeded = {
52703
52917
  theme: "dark",
52704
52918
  ...config,
52705
52919
  hasCompletedOnboarding: true,
52706
- projects
52920
+ projects,
52921
+ ...keySuffix === undefined ? {} : { customApiKeyResponses: approveKey(config.customApiKeyResponses, keySuffix) }
52707
52922
  };
52708
52923
  await mkdir3(dir, { recursive: true });
52709
- const staging = `${path}.${process.pid}.tmp`;
52924
+ const staging = `${path}.${randomUUID5()}.tmp`;
52710
52925
  await writeFile2(staging, JSON.stringify(seeded, null, 2), { mode: 384 });
52711
52926
  await rename4(staging, path);
52712
52927
  }
@@ -52791,7 +53006,7 @@ async function failOnPane(pane, stderr) {
52791
53006
  }
52792
53007
  throw new PaneError("tmux_failed", stderr);
52793
53008
  }
52794
- var LIVE_SOCKET = "kodizm-live", PASTE_ENTER_DELAY_MS = 328, SUBMIT_WINDOWS_MS, SESSION_START_TIMEOUT_MS = 1e4, CANCEL_CLEAR_DELAY_MS = 414, PANE_ID, RESUME_ID, DROPPED_ENV, PASTE_S_SINCE, PaneError, bufferSequence = 0, pasteSFlags;
53009
+ var LIVE_SOCKET = "kodizm-live", PASTE_ENTER_DELAY_MS = 328, SUBMIT_WINDOWS_MS, SESSION_START_TIMEOUT_MS = 1e4, CANCEL_CLEAR_DELAY_MS = 414, PANE_ID, RESUME_ID, DROPPED_ENV, PASTE_S_SINCE, PaneError, bufferSequence = 0, seeding, pasteSFlags;
52795
53010
  var init_pane = __esm(() => {
52796
53011
  init_input();
52797
53012
  SUBMIT_WINDOWS_MS = [328, 656, 1312, 2624];
@@ -52807,11 +53022,197 @@ var init_pane = __esm(() => {
52807
53022
  this.reason = reason;
52808
53023
  }
52809
53024
  };
53025
+ seeding = Promise.resolve();
52810
53026
  pasteSFlags = new Map;
52811
53027
  });
52812
53028
 
53029
+ // src/live/policy.ts
53030
+ function parseToolPolicy(text) {
53031
+ const policy = parseJson2(text);
53032
+ if (!isRecord4(policy) || !Object.keys(policy).every((key) => POLICY_KEYS.includes(key))) {
53033
+ return null;
53034
+ }
53035
+ const rules = {
53036
+ allow: [],
53037
+ deny: [],
53038
+ ask: []
53039
+ };
53040
+ for (const list of RULE_LISTS) {
53041
+ const entries = policy[list] === undefined ? [] : policy[list];
53042
+ if (!Array.isArray(entries) || !entries.every((entry) => typeof entry === "string")) {
53043
+ return null;
53044
+ }
53045
+ for (const entry of entries) {
53046
+ const rule = translatePattern(entry, list);
53047
+ if (rule === null) {
53048
+ return null;
53049
+ }
53050
+ if (!rules[list].includes(rule)) {
53051
+ rules[list].push(rule);
53052
+ }
53053
+ }
53054
+ }
53055
+ return rules;
53056
+ }
53057
+ function translatePattern(pattern, list) {
53058
+ let parsed;
53059
+ try {
53060
+ parsed = parseCanonicalPattern(pattern);
53061
+ } catch (error) {
53062
+ if (error instanceof InvalidPatternError) {
53063
+ return null;
53064
+ }
53065
+ throw error;
53066
+ }
53067
+ if (parsed.mcpPath !== undefined) {
53068
+ return mcpRule(parsed.mcpPath);
53069
+ }
53070
+ const { toolName, argPattern } = parsed;
53071
+ if (!TOOL_NAME.test(toolName)) {
53072
+ return null;
53073
+ }
53074
+ if (argPattern === undefined || argPattern === "*") {
53075
+ return toolName;
53076
+ }
53077
+ const fileTool = FILE_TOOLS.get(toolName);
53078
+ if (fileTool !== undefined && fileTool !== toolName && list === "allow") {
53079
+ return null;
53080
+ }
53081
+ const tool = fileTool ?? toolName;
53082
+ const content = ruleContent(tool, argPattern, list);
53083
+ return isEnforceableContent(tool, content) ? `${tool}(${escapeRuleContent(content)})` : null;
53084
+ }
53085
+ function mcpRule(path) {
53086
+ const wildcardMisplaced = path.some((segment, index) => segment.includes("*") && !(index === 1 && segment === "*"));
53087
+ if (path.length > 2 || wildcardMisplaced || path.some((segment) => /[()]/.test(segment))) {
53088
+ return null;
53089
+ }
53090
+ return `mcp__${path.join("__")}`;
53091
+ }
53092
+ function ruleContent(tool, argPattern, list) {
53093
+ if (tool === "Read" || tool === "Edit") {
53094
+ return anchorPath(argPattern);
53095
+ }
53096
+ return tool === "Bash" ? bashArg(argPattern, list) : argPattern;
53097
+ }
53098
+ function anchorPath(path) {
53099
+ return path.startsWith("/") && !path.startsWith("//") ? `/${path}` : path;
53100
+ }
53101
+ function bashArg(argPattern, list) {
53102
+ const prefix = argPattern.slice(0, -1);
53103
+ if (list !== "allow" || !argPattern.endsWith("*") || prefix.includes("*") || /[:\s]$/.test(prefix)) {
53104
+ return argPattern;
53105
+ }
53106
+ return `${prefix}:*`;
53107
+ }
53108
+ function escapeRuleContent(content) {
53109
+ return content.replace(/\\/g, "\\\\").replace(/\(/g, "\\(").replace(/\)/g, "\\)");
53110
+ }
53111
+ function isEnforceableContent(tool, content) {
53112
+ if (tool === "Bash") {
53113
+ return (!content.includes(":*") || content.endsWith(":*")) && content !== ":*";
53114
+ }
53115
+ if (tool === "Read" || tool === "Edit") {
53116
+ return !content.includes(":*") && (!content.includes("*") || /^\*|\*$|\*\*|\/\*|\*\.|\*\)/.test(content));
53117
+ }
53118
+ return true;
53119
+ }
53120
+ function parseJson2(text) {
53121
+ try {
53122
+ return JSON.parse(text);
53123
+ } catch (error) {
53124
+ if (error instanceof SyntaxError) {
53125
+ return;
53126
+ }
53127
+ throw error;
53128
+ }
53129
+ }
53130
+ function isRecord4(value) {
53131
+ return typeof value === "object" && value !== null && !Array.isArray(value);
53132
+ }
53133
+ var RULE_LISTS, POLICY_KEYS, FILE_TOOLS, TOOL_NAME;
53134
+ var init_policy4 = __esm(() => {
53135
+ init_policy();
53136
+ RULE_LISTS = ["allow", "deny", "ask"];
53137
+ POLICY_KEYS = [...RULE_LISTS, "defaultMode"];
53138
+ FILE_TOOLS = new Map([
53139
+ ["Read", "Read"],
53140
+ ["Edit", "Edit"],
53141
+ ["Write", "Edit"],
53142
+ ["MultiEdit", "Edit"],
53143
+ ["NotebookEdit", "Edit"],
53144
+ ["Glob", "Read"],
53145
+ ["Grep", "Read"]
53146
+ ]);
53147
+ TOOL_NAME = /^[A-Z][A-Za-z0-9_]*$/;
53148
+ });
53149
+
53150
+ // src/live/start-flags.ts
53151
+ function parseStartExtras(args) {
53152
+ for (const key of Object.keys(args)) {
53153
+ if (!START_FLAGS.has(key)) {
53154
+ return null;
53155
+ }
53156
+ }
53157
+ const permissionMode = parsePermissionMode(args["permission-mode"]);
53158
+ const oneShot = parseOneShot(args["one-shot"]);
53159
+ const deadlineSeconds = parseSeconds(args["deadline-seconds"], 1);
53160
+ const retainFailedSeconds = parseSeconds(args["retain-failed-seconds"], 0);
53161
+ if (permissionMode === null || oneShot === null || deadlineSeconds === null || retainFailedSeconds === null) {
53162
+ return null;
53163
+ }
53164
+ return {
53165
+ permissionMode,
53166
+ oneShot,
53167
+ deadlineSeconds,
53168
+ retainFailedSeconds: retainFailedSeconds ?? 0
53169
+ };
53170
+ }
53171
+ function parsePermissionMode(value) {
53172
+ if (value === undefined) {
53173
+ return;
53174
+ }
53175
+ return value === "default" || value === "dontAsk" ? value : null;
53176
+ }
53177
+ function parseOneShot(value) {
53178
+ if (value === undefined || value === "0") {
53179
+ return false;
53180
+ }
53181
+ return value === "1" ? true : null;
53182
+ }
53183
+ function parseSeconds(value, min) {
53184
+ if (value === undefined) {
53185
+ return;
53186
+ }
53187
+ if (!DIGITS.test(value)) {
53188
+ return null;
53189
+ }
53190
+ const seconds = Number(value);
53191
+ return seconds >= min && seconds <= MAX_SECONDS ? seconds : null;
53192
+ }
53193
+ var START_FLAGS, START_FILE_FLAGS, MAX_SECONDS = 2147483, DIGITS;
53194
+ var init_start_flags = __esm(() => {
53195
+ START_FLAGS = new Set([
53196
+ "pane",
53197
+ "cwd",
53198
+ "settings-base",
53199
+ "env-file",
53200
+ "model",
53201
+ "resume",
53202
+ "mcp-config",
53203
+ "tool-policy",
53204
+ "system-prompt-file",
53205
+ "permission-mode",
53206
+ "one-shot",
53207
+ "deadline-seconds",
53208
+ "retain-failed-seconds"
53209
+ ]);
53210
+ START_FILE_FLAGS = ["env-file", "tool-policy", "system-prompt-file", "mcp-config"];
53211
+ DIGITS = /^[0-9]+$/;
53212
+ });
53213
+
52813
53214
  // src/live/transcript.ts
52814
- import { stat as stat2 } from "fs/promises";
53215
+ import { stat as stat3 } from "fs/promises";
52815
53216
 
52816
53217
  class TranscriptTail {
52817
53218
  path;
@@ -52875,7 +53276,7 @@ function classifyInterrupt(record) {
52875
53276
  return null;
52876
53277
  }
52877
53278
  const [block] = content;
52878
- if (!isRecord4(block) || block.type !== "text") {
53279
+ if (!isRecord5(block) || block.type !== "text") {
52879
53280
  return null;
52880
53281
  }
52881
53282
  if (block.text === INTERRUPT_MARKER) {
@@ -52886,6 +53287,29 @@ function classifyInterrupt(record) {
52886
53287
  }
52887
53288
  return null;
52888
53289
  }
53290
+ function classifyApiError(record) {
53291
+ if (record.type !== "assistant" || !isMainChain(record) || record.isApiErrorMessage !== true) {
53292
+ return null;
53293
+ }
53294
+ const error = typeof record.error === "string" ? record.error : "unknown";
53295
+ const status = typeof record.apiErrorStatus === "number" ? record.apiErrorStatus : null;
53296
+ const message = assistantBlocks(record).filter((block) => block.kind === "text").map((block) => block.text).join(`
53297
+ `);
53298
+ return {
53299
+ reason: apiErrorReason(error, status, message),
53300
+ detail: `API error ${status ?? error}: ${message.slice(0, API_ERROR_DETAIL_MAX)}`
53301
+ };
53302
+ }
53303
+ function apiErrorReason(error, status, message) {
53304
+ if (error === "rate_limit" && USAGE_LIMIT_TEXT.test(message) && LIMIT_RESETS.test(message) && !NOT_A_USAGE_LIMIT.test(message)) {
53305
+ return "usage_limit";
53306
+ }
53307
+ const reason = failureReason(error);
53308
+ if (reason !== "api_error" || status === null) {
53309
+ return reason;
53310
+ }
53311
+ return STATUS_REASONS[status] ?? reason;
53312
+ }
52889
53313
 
52890
53314
  class UsageLedger {
52891
53315
  counted = new Map;
@@ -52894,7 +53318,7 @@ class UsageLedger {
52894
53318
  if (record.type !== "assistant" || !isMainChain(record) || message === null) {
52895
53319
  return null;
52896
53320
  }
52897
- if (typeof message.id !== "string" || !isRecord4(message.usage)) {
53321
+ if (typeof message.id !== "string" || !isRecord5(message.usage)) {
52898
53322
  return null;
52899
53323
  }
52900
53324
  const seen = this.counted.get(message.id) ?? emptyCounts();
@@ -52921,7 +53345,7 @@ function assistantBlocks(record) {
52921
53345
  }
52922
53346
  const blocks = [];
52923
53347
  for (const block of content) {
52924
- if (!isRecord4(block)) {
53348
+ if (!isRecord5(block)) {
52925
53349
  continue;
52926
53350
  }
52927
53351
  if (block.type === "text" && typeof block.text === "string" && block.text !== "") {
@@ -52941,12 +53365,12 @@ function isMainChain(record) {
52941
53365
  return record.isSidechain !== true;
52942
53366
  }
52943
53367
  function messageOf(record) {
52944
- return isRecord4(record.message) ? record.message : null;
53368
+ return isRecord5(record.message) ? record.message : null;
52945
53369
  }
52946
53370
  function parseLine(line) {
52947
53371
  try {
52948
53372
  const value = JSON.parse(line);
52949
- return isRecord4(value) ? value : null;
53373
+ return isRecord5(value) ? value : null;
52950
53374
  } catch (error) {
52951
53375
  if (error instanceof SyntaxError) {
52952
53376
  return null;
@@ -52956,7 +53380,7 @@ function parseLine(line) {
52956
53380
  }
52957
53381
  async function fileSize(path) {
52958
53382
  try {
52959
- return (await stat2(path)).size;
53383
+ return (await stat3(path)).size;
52960
53384
  } catch (error) {
52961
53385
  if (error.code === "ENOENT") {
52962
53386
  return null;
@@ -52984,17 +53408,27 @@ function emptyCounts() {
52984
53408
  cacheCreationTokens: 0
52985
53409
  };
52986
53410
  }
52987
- function isRecord4(value) {
53411
+ function isRecord5(value) {
52988
53412
  return typeof value === "object" && value !== null && !Array.isArray(value);
52989
53413
  }
52990
- var INTERRUPT_MARKER = "[Request interrupted by user]", INTERRUPT_TOOL_MARKER = "[Request interrupted by user for tool use]", NEWLINE = 10;
52991
- var init_transcript = () => {};
53414
+ var INTERRUPT_MARKER = "[Request interrupted by user]", INTERRUPT_TOOL_MARKER = "[Request interrupted by user for tool use]", NEWLINE = 10, USAGE_LIMIT_TEXT, LIMIT_RESETS, NOT_A_USAGE_LIMIT, STATUS_REASONS, API_ERROR_DETAIL_MAX = 200;
53415
+ var init_transcript = __esm(() => {
53416
+ init_events2();
53417
+ USAGE_LIMIT_TEXT = /\b(usage|session|weekly) limit\b|\bhit your\b[^.\n]*\blimit\b/i;
53418
+ LIMIT_RESETS = /\bresets?\b/i;
53419
+ NOT_A_USAGE_LIMIT = /\bnot your (usage )?limit\b/i;
53420
+ STATUS_REASONS = {
53421
+ 429: "rate_limit",
53422
+ 401: "auth_error"
53423
+ };
53424
+ });
52992
53425
 
52993
53426
  // src/live/daemon.ts
52994
- import { randomUUID as randomUUID5 } from "crypto";
52995
- import { chmod, mkdir as mkdir4, readFile as readFile4, stat as stat3, unlink as unlink2 } from "fs/promises";
53427
+ import { randomUUID as randomUUID6 } from "crypto";
53428
+ import { renameSync as renameSync2, rmSync as rmSync2, writeFileSync } from "fs";
53429
+ import { chmod, lstat as lstat2, mkdir as mkdir4, readFile as readFile4, realpath as realpath4, unlink as unlink2 } from "fs/promises";
52996
53430
  import { createServer } from "net";
52997
- import { dirname as dirname4, isAbsolute as isAbsolute4, join as join7 } from "path";
53431
+ import { dirname as dirname5, isAbsolute as isAbsolute4, join as join7 } from "path";
52998
53432
  function exitCodeFor(code) {
52999
53433
  return EXIT_CODES[code];
53000
53434
  }
@@ -53157,6 +53591,9 @@ class LivePane {
53157
53591
  gen;
53158
53592
  ingestUrl;
53159
53593
  daemon;
53594
+ extras;
53595
+ mcpConfig;
53596
+ paneDir;
53160
53597
  started = new Latch;
53161
53598
  submitted = new Latch;
53162
53599
  turnClosed = new Latch;
@@ -53179,17 +53616,29 @@ class LivePane {
53179
53616
  skippedLines = 0;
53180
53617
  lastAssistantText;
53181
53618
  lastStopReason = null;
53619
+ apiError;
53182
53620
  settled = false;
53183
53621
  lastActivityAt = Date.now();
53184
53622
  sessionEndReason;
53185
53623
  timers = [];
53624
+ heartbeatTimer;
53625
+ deadlineTimer;
53626
+ retentionTimer;
53627
+ interruptTimer;
53628
+ interrupt;
53629
+ isRetaining = false;
53630
+ ending = false;
53631
+ teardown;
53186
53632
  monitoring = false;
53187
53633
  closed = false;
53188
- constructor(pane, gen, ingestUrl, token, authValues, daemon) {
53634
+ constructor(pane, gen, ingestUrl, token, authValues, daemon, extras, mcpConfig, paneDir) {
53189
53635
  this.pane = pane;
53190
53636
  this.gen = gen;
53191
53637
  this.ingestUrl = ingestUrl;
53192
53638
  this.daemon = daemon;
53639
+ this.extras = extras;
53640
+ this.mcpConfig = mcpConfig;
53641
+ this.paneDir = paneDir;
53193
53642
  this.token = token;
53194
53643
  this.secrets = [token, ...authValues.filter((value) => value.length >= MIN_SECRET_LENGTH)];
53195
53644
  this.queue = new EventQueue({
@@ -53198,7 +53647,11 @@ class LivePane {
53198
53647
  bearer: () => this.token,
53199
53648
  secrets: () => this.secrets,
53200
53649
  timings: () => daemon.timings,
53201
- onStaleGen: () => void this.abandon(),
53650
+ onStaleGen: () => {
53651
+ if (!this.isRetaining) {
53652
+ this.abandon();
53653
+ }
53654
+ },
53202
53655
  logger: daemon.logger
53203
53656
  });
53204
53657
  }
@@ -53208,13 +53661,16 @@ class LivePane {
53208
53661
  get busy() {
53209
53662
  return this.turn !== undefined || this.compacting;
53210
53663
  }
53664
+ get refusesInput() {
53665
+ return this.ending || this.interrupt !== undefined;
53666
+ }
53211
53667
  begin() {
53212
53668
  const timings = this.daemon.timings;
53213
53669
  this.timers = [
53214
53670
  setInterval(() => void this.guard("transcript", () => this.readTranscript()), timings.transcriptPollMs),
53215
- setInterval(() => void this.guard("monitor", () => this.monitor()), timings.monitorMs),
53216
- setInterval(() => void this.guard("heartbeat", () => this.beat()), timings.heartbeatMs)
53671
+ setInterval(() => void this.guard("monitor", () => this.monitor()), timings.monitorMs)
53217
53672
  ];
53673
+ this.heartbeatTimer = setInterval(() => void this.guard("heartbeat", () => this.beat()), timings.heartbeatMs);
53218
53674
  }
53219
53675
  rotate(token) {
53220
53676
  this.token = token;
@@ -53280,24 +53736,28 @@ class LivePane {
53280
53736
  }
53281
53737
  this.closeOpenTools();
53282
53738
  this.clearDialog();
53739
+ const message = text(draft.payload.lastAssistantMessage);
53740
+ if (draft.type === "turn_completed" && message !== undefined) {
53741
+ this.persistLastAssistantMessage(message);
53742
+ }
53283
53743
  this.queue.push(draft);
53284
53744
  this.turn = undefined;
53745
+ clearTimeout(this.deadlineTimer);
53746
+ this.deadlineTimer = undefined;
53747
+ this.lastActivityAt = Date.now();
53285
53748
  this.lastCloseCause = cause;
53286
53749
  this.turnClosed.fire();
53750
+ this.afterTurn(draft.type);
53287
53751
  }
53288
53752
  clearDialog() {
53289
53753
  this.dialog?.abort.abort();
53290
53754
  this.dialog = undefined;
53291
53755
  }
53292
53756
  interruptedTurn() {
53293
- return turnCompleted("interrupted", this.lastAssistantText);
53757
+ return turnCompleted("interrupted", this.lastAssistantText ?? "");
53294
53758
  }
53295
- async stop() {
53296
- this.close();
53297
- await killPane(this.pane);
53298
- this.queue.push(sessionEnded("stopped"));
53299
- await this.queue.drain(STOP_DRAIN_MS);
53300
- this.queue.stop();
53759
+ stop(reason = "stopped") {
53760
+ return this.tearDown(() => this.finish(reason, () => killPane(this.pane)));
53301
53761
  }
53302
53762
  async dispose() {
53303
53763
  this.close();
@@ -53309,7 +53769,130 @@ class LivePane {
53309
53769
  for (const timer of this.timers.splice(0)) {
53310
53770
  clearInterval(timer);
53311
53771
  }
53772
+ clearInterval(this.heartbeatTimer);
53773
+ clearTimeout(this.deadlineTimer);
53774
+ clearTimeout(this.retentionTimer);
53775
+ clearTimeout(this.interruptTimer);
53312
53776
  this.clearDialog();
53777
+ this.removeMcpConfig();
53778
+ }
53779
+ tearDown(run) {
53780
+ this.teardown ??= run();
53781
+ return this.teardown;
53782
+ }
53783
+ async finish(reason, kill) {
53784
+ try {
53785
+ this.close();
53786
+ await kill();
53787
+ this.queue.push(sessionEnded(reason));
53788
+ await this.queue.drain(STOP_DRAIN_MS);
53789
+ this.queue.stop();
53790
+ } finally {
53791
+ this.daemon.remove(this);
53792
+ }
53793
+ }
53794
+ afterTurn(type) {
53795
+ if (this.closed || this.ending) {
53796
+ return;
53797
+ }
53798
+ const { oneShot, retainFailedSeconds } = this.extras;
53799
+ if (type === "turn_completed" && oneShot) {
53800
+ this.ending = true;
53801
+ this.guard("end", () => this.endAfterDelivery(0));
53802
+ return;
53803
+ }
53804
+ if (type !== "turn_failed" || !oneShot && retainFailedSeconds === 0) {
53805
+ return;
53806
+ }
53807
+ this.ending = true;
53808
+ if (retainFailedSeconds > 0) {
53809
+ this.isRetaining = true;
53810
+ clearInterval(this.heartbeatTimer);
53811
+ }
53812
+ this.guard("end", () => this.endAfterDelivery(retainFailedSeconds * this.daemon.timings.secondMs));
53813
+ }
53814
+ async endAfterDelivery(delayMs) {
53815
+ await this.queue.drain(this.daemon.timings.deliveryDrainMs);
53816
+ if (this.closed) {
53817
+ return;
53818
+ }
53819
+ if (delayMs > 0) {
53820
+ await new Promise((resolve) => {
53821
+ this.retentionTimer = setTimeout(resolve, delayMs);
53822
+ });
53823
+ if (this.closed) {
53824
+ return;
53825
+ }
53826
+ }
53827
+ await this.end("stopped");
53828
+ }
53829
+ end(reason) {
53830
+ return this.tearDown(() => this.finish(reason, () => this.killLogged()));
53831
+ }
53832
+ async onDeadline(promptId, seconds) {
53833
+ if (this.closed || this.turn?.promptId !== promptId) {
53834
+ return;
53835
+ }
53836
+ this.closeTurn(turnFailedWith("deadline", `The turn ran longer than ${seconds} seconds.`), "deadline");
53837
+ const interrupt = {
53838
+ sent: false,
53839
+ marked: false
53840
+ };
53841
+ this.interrupt = interrupt;
53842
+ this.interruptTimer = setTimeout(() => this.endInterrupt(), this.daemon.timings.cancelTimeoutMs);
53843
+ try {
53844
+ await cancelPane(this.pane);
53845
+ interrupt.sent = true;
53846
+ this.settleInterrupt();
53847
+ } catch (error) {
53848
+ if (!(error instanceof PaneError)) {
53849
+ throw error;
53850
+ }
53851
+ this.daemon.logger.warn("tmuxd could not interrupt a turn past its deadline", {
53852
+ pane: this.id,
53853
+ reason: error.reason
53854
+ });
53855
+ }
53856
+ }
53857
+ settleInterrupt() {
53858
+ if (this.interrupt?.sent === true && this.interrupt.marked) {
53859
+ this.endInterrupt();
53860
+ }
53861
+ }
53862
+ endInterrupt() {
53863
+ clearTimeout(this.interruptTimer);
53864
+ this.interruptTimer = undefined;
53865
+ this.interrupt = undefined;
53866
+ }
53867
+ removeMcpConfig() {
53868
+ if (this.mcpConfig === undefined) {
53869
+ return;
53870
+ }
53871
+ try {
53872
+ rmSync2(this.mcpConfig, { force: true });
53873
+ } catch (error) {
53874
+ if (!isErrno(error)) {
53875
+ throw error;
53876
+ }
53877
+ this.daemon.logger.error("tmuxd could not remove the mcp-config", { pane: this.id, code: error.code });
53878
+ }
53879
+ }
53880
+ persistLastAssistantMessage(message) {
53881
+ const path = join7(this.paneDir, LAST_MESSAGE_FILE);
53882
+ const temp = `${path}.${randomUUID6()}.tmp`;
53883
+ try {
53884
+ writeFileSync(temp, redactDeep(message, this.secrets), { mode: 384, flag: "wx" });
53885
+ renameSync2(temp, path);
53886
+ } catch (error) {
53887
+ if (!isErrno(error)) {
53888
+ throw error;
53889
+ }
53890
+ rmSync2(temp, { force: true });
53891
+ this.daemon.logger.error("tmuxd could not persist the last assistant message", {
53892
+ pane: this.id,
53893
+ code: error.code
53894
+ });
53895
+ }
53313
53896
  }
53314
53897
  async onSessionStart(payload) {
53315
53898
  const source = text(payload.source);
@@ -53333,20 +53916,26 @@ class LivePane {
53333
53916
  return;
53334
53917
  }
53335
53918
  this.turn = {
53336
- promptId: this.pendingPromptId ?? text(payload.prompt_id) ?? randomUUID5()
53919
+ promptId: this.pendingPromptId ?? text(payload.prompt_id) ?? randomUUID6()
53337
53920
  };
53338
53921
  this.pendingPromptId = undefined;
53339
53922
  this.settled = false;
53340
53923
  this.lastAssistantText = undefined;
53341
53924
  this.lastStopReason = null;
53925
+ this.apiError = undefined;
53342
53926
  this.queue.push(turnStarted(this.turn.promptId));
53343
53927
  this.submitted.fire();
53928
+ const seconds = this.extras.deadlineSeconds;
53929
+ if (seconds !== undefined) {
53930
+ const promptId = this.turn.promptId;
53931
+ this.deadlineTimer = setTimeout(() => void this.guard("deadline", () => this.onDeadline(promptId, seconds)), seconds * this.daemon.timings.secondMs);
53932
+ }
53344
53933
  }
53345
53934
  onPreToolUse(payload) {
53346
53935
  if (this.turn === undefined) {
53347
53936
  return;
53348
53937
  }
53349
- const toolCallId = text(payload.tool_use_id) ?? randomUUID5();
53938
+ const toolCallId = text(payload.tool_use_id) ?? randomUUID6();
53350
53939
  this.openTools.set(toolCallId, {
53351
53940
  name: text(payload.tool_name),
53352
53941
  input: payload.tool_input
@@ -53355,9 +53944,22 @@ class LivePane {
53355
53944
  this.queue.push(toolCallBegin(payload, toolCallId));
53356
53945
  }
53357
53946
  async onPermissionRequest(payload, signal) {
53947
+ if (this.extras.permissionMode === "dontAsk") {
53948
+ this.daemon.logger.debug("tmuxd denied a permission request in dontAsk mode", {
53949
+ pane: this.id,
53950
+ tool: text(payload.tool_name)
53951
+ });
53952
+ return {
53953
+ ok: true,
53954
+ decision: {
53955
+ behavior: "deny",
53956
+ message: UNATTENDED_DENIAL
53957
+ }
53958
+ };
53959
+ }
53358
53960
  const correlated = this.findOpenTool(payload);
53359
53961
  const dialog = {
53360
- toolCallId: correlated ?? randomUUID5(),
53962
+ toolCallId: correlated ?? randomUUID6(),
53361
53963
  correlated: correlated !== undefined,
53362
53964
  toolName: text(payload.tool_name),
53363
53965
  input: payload.tool_input,
@@ -53481,7 +54083,10 @@ class LivePane {
53481
54083
  return;
53482
54084
  }
53483
54085
  await this.syncTranscript();
53484
- this.closeTurn(turnCompleted(apiStopReason(this.lastStopReason), text(payload.last_assistant_message) ?? this.lastAssistantText), "stop");
54086
+ this.closeTurn(this.endOfTurn(turnCompleted(apiStopReason(this.lastStopReason), text(payload.last_assistant_message) ?? this.lastAssistantText ?? "")), "stop");
54087
+ }
54088
+ endOfTurn(completed) {
54089
+ return this.apiError === undefined ? completed : turnFailedWith(this.apiError.reason, this.apiError.detail);
53485
54090
  }
53486
54091
  async onStopFailure(payload) {
53487
54092
  if (this.turn === undefined) {
@@ -53535,13 +54140,19 @@ class LivePane {
53535
54140
  }
53536
54141
  this.lastActivityAt = Date.now();
53537
54142
  if (classifyInterrupt(record) !== null) {
53538
- this.closeTurn(this.interruptedTurn(), "marker");
54143
+ if (this.interrupt === undefined) {
54144
+ this.closeTurn(this.interruptedTurn(), "marker");
54145
+ } else {
54146
+ this.interrupt.marked = true;
54147
+ this.settleInterrupt();
54148
+ }
53539
54149
  this.clearDialog();
53540
54150
  continue;
53541
54151
  }
54152
+ const failure = classifyApiError(record);
53542
54153
  for (const block of assistantBlocks(record)) {
53543
54154
  this.queue.push(block.kind === "text" ? outputChunk(block.text) : thinkingChunk(block.text));
53544
- if (block.kind === "text") {
54155
+ if (block.kind === "text" && failure === null) {
53545
54156
  this.lastAssistantText = block.text;
53546
54157
  }
53547
54158
  }
@@ -53555,6 +54166,14 @@ class LivePane {
53555
54166
  } else if (record.type === "user") {
53556
54167
  this.settled = false;
53557
54168
  }
54169
+ if (failure !== null) {
54170
+ this.apiError = failure;
54171
+ if (failure.reason === "usage_limit") {
54172
+ this.closeTurn(turnFailedWith(failure.reason, failure.detail), "api_error");
54173
+ }
54174
+ } else if (record.type === "assistant" || record.type === "user" && record.isMeta !== true) {
54175
+ this.apiError = undefined;
54176
+ }
53558
54177
  }
53559
54178
  }
53560
54179
  async monitor() {
@@ -53563,21 +54182,33 @@ class LivePane {
53563
54182
  }
53564
54183
  this.monitoring = true;
53565
54184
  try {
53566
- if (!await this.alive()) {
54185
+ const alive = await this.alive();
54186
+ if (this.closed) {
54187
+ return;
54188
+ }
54189
+ if (!alive) {
53567
54190
  await this.onDeath();
53568
54191
  return;
53569
54192
  }
53570
54193
  const quiet = Date.now() - this.lastActivityAt >= this.daemon.timings.watchdogQuietMs;
53571
54194
  const idle = this.dialog === undefined && this.openTools.size === 0 && !this.compacting && this.settled;
53572
54195
  if (this.turn !== undefined && quiet && idle) {
53573
- this.closeTurn(turnCompleted("watchdog", this.lastAssistantText), "watchdog");
54196
+ this.closeTurn(this.endOfTurn(turnCompleted("watchdog", this.lastAssistantText ?? "")), "watchdog");
54197
+ }
54198
+ const idleTimeoutMs = this.daemon.timings.idleTimeoutMs;
54199
+ const unoccupied = this.turn === undefined && this.pendingPromptId === undefined && this.dialog === undefined && !this.compacting && !this.isRetaining;
54200
+ if (idleTimeoutMs > 0 && unoccupied && Date.now() - this.lastActivityAt >= idleTimeoutMs) {
54201
+ await this.end("idle_timeout");
53574
54202
  }
53575
54203
  } finally {
53576
54204
  this.monitoring = false;
53577
54205
  }
53578
54206
  }
53579
54207
  async beat() {
53580
- if (!this.closed && await this.alive()) {
54208
+ if (this.closed || this.isRetaining || !await this.alive()) {
54209
+ return;
54210
+ }
54211
+ if (!this.closed && !this.isRetaining) {
53581
54212
  this.queue.push(heartbeat());
53582
54213
  }
53583
54214
  }
@@ -53588,28 +54219,32 @@ class LivePane {
53588
54219
  if (this.closed) {
53589
54220
  return;
53590
54221
  }
53591
- const turnRunning = this.turn !== undefined;
53592
- this.close();
53593
- if (this.sessionEndReason !== undefined) {
53594
- this.queue.push(sessionEnded("stopped"));
53595
- } else {
53596
- this.queue.push(sessionEnded("crashed"));
53597
- this.queue.push(processDied());
53598
- if (turnRunning) {
53599
- this.queue.push(sessionFailed("process_died", "claude exited while a turn was running."));
54222
+ await this.tearDown(async () => {
54223
+ const turnRunning = this.turn !== undefined;
54224
+ this.close();
54225
+ if (this.sessionEndReason !== undefined) {
54226
+ this.queue.push(sessionEnded("stopped"));
54227
+ } else {
54228
+ this.queue.push(sessionEnded("crashed"));
54229
+ this.queue.push(processDied());
54230
+ if (turnRunning) {
54231
+ this.queue.push(sessionFailed("process_died", "claude exited while a turn was running."));
54232
+ }
53600
54233
  }
53601
- }
53602
- await this.queue.drain(STOP_DRAIN_MS);
53603
- this.queue.stop();
53604
- this.daemon.remove(this);
54234
+ await this.queue.drain(STOP_DRAIN_MS);
54235
+ this.queue.stop();
54236
+ this.daemon.remove(this);
54237
+ });
53605
54238
  }
53606
54239
  async abandon() {
53607
54240
  if (this.closed) {
53608
54241
  return;
53609
54242
  }
53610
- this.close();
53611
- await this.killLogged();
53612
- this.daemon.remove(this);
54243
+ await this.tearDown(async () => {
54244
+ this.close();
54245
+ await this.killLogged();
54246
+ this.daemon.remove(this);
54247
+ });
53613
54248
  }
53614
54249
  async killLogged() {
53615
54250
  try {
@@ -53639,6 +54274,7 @@ class LiveDaemon {
53639
54274
  logger;
53640
54275
  timings;
53641
54276
  panes = new Map;
54277
+ starting = new Set;
53642
54278
  server;
53643
54279
  constructor(options) {
53644
54280
  this.options = options;
@@ -53656,7 +54292,7 @@ class LiveDaemon {
53656
54292
  }
53657
54293
  async listen() {
53658
54294
  const { socketPath } = this.options;
53659
- await mkdir4(dirname4(socketPath), { recursive: true, mode: 448 });
54295
+ await mkdir4(dirname5(socketPath), { recursive: true, mode: 448 });
53660
54296
  if (await Bun.file(socketPath).exists()) {
53661
54297
  if (await this.answering(socketPath)) {
53662
54298
  throw new Error(`a tmuxd daemon already listens on ${socketPath}`);
@@ -53724,52 +54360,90 @@ class LiveDaemon {
53724
54360
  }
53725
54361
  }
53726
54362
  async start(args) {
53727
- const envFile = args["env-file"];
53728
- if (envFile === undefined || !isAbsolute4(envFile)) {
53729
- return refuse("usage_error");
53730
- }
54363
+ let result;
53731
54364
  try {
53732
- return await this.launch(args, envFile);
54365
+ result = await this.launch(args);
54366
+ return result;
53733
54367
  } finally {
53734
- await removeFile(envFile);
54368
+ await removeHandoff(args, result?.ok === true, this.logger);
53735
54369
  }
53736
54370
  }
53737
- async launch(args, envFile) {
54371
+ async launch(args) {
53738
54372
  const { pane: id, cwd, model, resume } = args;
54373
+ const envFile = args["env-file"];
53739
54374
  const settingsBase = args["settings-base"];
54375
+ const policyFile = args["tool-policy"];
54376
+ const systemPromptFile = args["system-prompt-file"];
54377
+ const mcpConfig = args["mcp-config"];
53740
54378
  if (id === undefined || !PANE_ID2.test(id) || cwd === undefined || !isAbsolute4(cwd)) {
53741
54379
  return refuse("usage_error");
53742
54380
  }
53743
- if (settingsBase === undefined || !isAbsolute4(settingsBase)) {
54381
+ if (envFile === undefined || !isAbsolute4(envFile) || settingsBase === undefined || !isAbsolute4(settingsBase)) {
53744
54382
  return refuse("usage_error");
53745
54383
  }
53746
54384
  if (model !== undefined && !MODEL_NAME.test(model)) {
53747
54385
  return refuse("usage_error");
53748
54386
  }
53749
- if (this.panes.has(id)) {
54387
+ const extras = parseStartExtras(args);
54388
+ if (extras === null || [policyFile, systemPromptFile, mcpConfig].some((path) => !optionalAbsolute(path))) {
54389
+ return refuse("usage_error");
54390
+ }
54391
+ if (this.panes.has(id) || this.starting.has(id)) {
53750
54392
  return refuse("pane_exists");
53751
54393
  }
53752
- if (this.panes.size >= (this.options.maxPanes ?? DEFAULT_MAX_PANES)) {
54394
+ if (this.panes.size + this.starting.size >= (this.options.maxPanes ?? DEFAULT_MAX_PANES)) {
53753
54395
  return refuse("pane_cap");
53754
54396
  }
54397
+ this.starting.add(id);
54398
+ try {
54399
+ return await this.open({
54400
+ id,
54401
+ cwd,
54402
+ model,
54403
+ resume,
54404
+ envFile,
54405
+ settingsBase,
54406
+ policyFile,
54407
+ systemPromptFile,
54408
+ mcpConfig,
54409
+ extras
54410
+ });
54411
+ } finally {
54412
+ this.starting.delete(id);
54413
+ }
54414
+ }
54415
+ async open(request) {
54416
+ const { id, cwd, model, resume, envFile, settingsBase, policyFile, systemPromptFile, mcpConfig, extras } = request;
53755
54417
  const content = await readSecretFile(envFile);
53756
54418
  const env = content === null ? null : parseStartEnv(content);
53757
54419
  if (env === null) {
53758
54420
  return refuse("usage_error");
53759
54421
  }
54422
+ for (const path of [systemPromptFile, mcpConfig]) {
54423
+ if (path !== undefined && !await isPrivateFile(path)) {
54424
+ return refuse("usage_error");
54425
+ }
54426
+ }
54427
+ const policy = policyFile === undefined ? undefined : await readToolPolicy(policyFile);
54428
+ if (policy === null) {
54429
+ return refuse("usage_error");
54430
+ }
54431
+ const mcpSpellings = mcpConfig === undefined ? [] : [...new Set([mcpConfig, await realpath4(mcpConfig)])];
53760
54432
  const settingsPath = join7(settingsBase, `${id}.settings.json`);
53761
54433
  await mkdir4(settingsBase, { recursive: true, mode: 448 });
53762
54434
  await writePaneSettings(settingsPath, {
53763
54435
  basePath: this.options.baseSettingsPath ?? DEFAULT_BASE_SETTINGS_PATH,
53764
54436
  helperArgv: this.options.helperArgv,
53765
54437
  socketPath: this.options.socketPath,
53766
- paneId: id
54438
+ paneId: id,
54439
+ rules: paneRules(policy, mcpSpellings, extras.permissionMode)
53767
54440
  });
53768
54441
  const pane = {
53769
54442
  id,
53770
54443
  socket: this.options.tmuxSocket ?? LIVE_SOCKET
53771
54444
  };
53772
- const live = new LivePane(pane, env.gen, env.ingestUrl, env.token, Object.values(env.authEnv), this);
54445
+ const live = new LivePane(pane, env.gen, env.ingestUrl, env.token, Object.values(env.authEnv), this, extras, mcpConfig, settingsBase);
54446
+ this.starting.delete(id);
53773
54447
  this.panes.set(id, live);
53774
54448
  try {
53775
54449
  await createPane({
@@ -53780,6 +54454,9 @@ class LiveDaemon {
53780
54454
  env: env.authEnv,
53781
54455
  model,
53782
54456
  resumeSessionId: resume,
54457
+ mcpConfig,
54458
+ systemPromptFile,
54459
+ permissionMode: extras.permissionMode,
53783
54460
  claudeBin: this.options.claudeBin,
53784
54461
  socket: pane.socket,
53785
54462
  confirmStarted: (timeoutMs) => live.started.wait(timeoutMs)
@@ -53789,7 +54466,13 @@ class LiveDaemon {
53789
54466
  live.queue.stop();
53790
54467
  return refuse(launchFailure(error));
53791
54468
  }
53792
- live.pid = await panePid(pane);
54469
+ try {
54470
+ live.pid = await panePid(pane);
54471
+ } catch (error) {
54472
+ this.panes.delete(id);
54473
+ await live.dispose();
54474
+ return refuse(launchFailure(error));
54475
+ }
53793
54476
  live.begin();
53794
54477
  return accept({
53795
54478
  pane: id,
@@ -53825,7 +54508,7 @@ class LiveDaemon {
53825
54508
  }
53826
54509
  throw error;
53827
54510
  }
53828
- const promptId = randomUUID5();
54511
+ const promptId = randomUUID6();
53829
54512
  live.pendingPromptId = promptId;
53830
54513
  live.submitted.arm();
53831
54514
  let outcome;
@@ -53971,7 +54654,7 @@ class LiveDaemon {
53971
54654
  if (live.dialog !== undefined) {
53972
54655
  return refuse("dialog_open");
53973
54656
  }
53974
- if (live.busy || live.pendingPromptId !== undefined) {
54657
+ if (live.busy || live.refusesInput || live.pendingPromptId !== undefined) {
53975
54658
  return refuse("busy");
53976
54659
  }
53977
54660
  return null;
@@ -54083,17 +54766,65 @@ function parseStartEnv(content) {
54083
54766
  authEnv: Object.fromEntries(Object.entries(values).filter(([key]) => !key.startsWith(LIVE_KEY_PREFIX)))
54084
54767
  };
54085
54768
  }
54086
- async function readSecretFile(path) {
54087
- const info = await stat3(path).catch((error) => {
54769
+ function paneRules(policy, mcpPaths, mode) {
54770
+ const deny = [...mcpPaths.map((path) => `Read(/${path})`), ...mode === "dontAsk" ? ["EnterPlanMode"] : []];
54771
+ if (policy === undefined && deny.length === 0) {
54772
+ return;
54773
+ }
54774
+ return {
54775
+ allow: policy?.allow ?? [],
54776
+ deny: [...policy?.deny ?? [], ...deny],
54777
+ ask: policy?.ask ?? []
54778
+ };
54779
+ }
54780
+ function optionalAbsolute(path) {
54781
+ return path === undefined || isAbsolute4(path);
54782
+ }
54783
+ async function removeHandoff(args, keepMcp, logger) {
54784
+ const files = START_FILE_FLAGS.flatMap((flag) => {
54785
+ const path = args[flag];
54786
+ return path === undefined || !isAbsolute4(path) || keepMcp && flag === "mcp-config" ? [] : [[flag, path]];
54787
+ });
54788
+ for (const failure of await removeHandoffFiles(files)) {
54789
+ logger.error("tmuxd could not remove a hand-off file", { ...failure });
54790
+ }
54791
+ }
54792
+ async function removeHandoffFiles(files) {
54793
+ const ordered = [
54794
+ ...files.filter(([flag]) => flag === "mcp-config"),
54795
+ ...files.filter(([flag]) => flag !== "mcp-config")
54796
+ ];
54797
+ const failures = [];
54798
+ for (const [flag, path] of ordered) {
54799
+ try {
54800
+ await removeFile(path);
54801
+ } catch (error) {
54802
+ if (!isErrno(error)) {
54803
+ throw error;
54804
+ }
54805
+ failures.push({
54806
+ flag,
54807
+ code: error.code
54808
+ });
54809
+ }
54810
+ }
54811
+ return failures;
54812
+ }
54813
+ async function isPrivateFile(path) {
54814
+ const info = await lstat2(path).catch((error) => {
54088
54815
  if (error.code === "ENOENT") {
54089
54816
  return null;
54090
54817
  }
54091
54818
  throw error;
54092
54819
  });
54093
- if (info === null || !info.isFile() || (info.mode & 511) !== 384) {
54094
- return null;
54095
- }
54096
- return readFile4(path, "utf8");
54820
+ return info?.isFile() === true && (info.mode & 511) === 384;
54821
+ }
54822
+ async function readSecretFile(path) {
54823
+ return await isPrivateFile(path) ? readFile4(path, "utf8") : null;
54824
+ }
54825
+ async function readToolPolicy(path) {
54826
+ const content = await readSecretFile(path);
54827
+ return content === null ? null : parseToolPolicy(content);
54097
54828
  }
54098
54829
  async function readFileOrNull(path) {
54099
54830
  return readFile4(path, "utf8").catch((error) => {
@@ -54255,11 +54986,18 @@ function accept(data) {
54255
54986
  return {
54256
54987
  ok: true,
54257
54988
  code: "ok",
54258
- data: Object.fromEntries(Object.entries(data).filter(([, value]) => value !== undefined))
54989
+ data: stamped(data)
54990
+ };
54991
+ }
54992
+ function refuse(code, data = {}) {
54993
+ return {
54994
+ ok: false,
54995
+ code,
54996
+ data: stamped(data)
54259
54997
  };
54260
54998
  }
54261
- function refuse(code, data) {
54262
- return data === undefined ? { ok: false, code } : { ok: false, code, data };
54999
+ function stamped(data) {
55000
+ return Object.fromEntries(Object.entries({ daemon_version: DAEMON_VERSION, ...data }).filter(([, value]) => value !== undefined));
54263
55001
  }
54264
55002
  function notRunning(id) {
54265
55003
  return refuse("pane_not_running", {
@@ -54267,14 +55005,18 @@ function notRunning(id) {
54267
55005
  running: false
54268
55006
  });
54269
55007
  }
54270
- var HEARTBEAT_INTERVAL_MS = 20000, WATCHDOG_QUIET_MS = 30000, CANCEL_TIMEOUT_MS = 3000, PERMISSION_POLL_MS = 1500, INGEST_TIMEOUT_MS = 1e4, MAX_BATCH = 200, DEFAULT_MAX_PANES = 4, STOP_DRAIN_MS = 1e4, PERMISSION_WAIT_MS, PERMISSION_OPEN_RETRY_MS = 90000, MIN_SECRET_LENGTH = 8, MAX_REQUEST_BYTES, PANE_ID2, ENV_KEY, LIVE_KEY_PREFIX = "KODIZM_LIVE_", TOKEN_PREFIX = "kdz-int-jwt.", COMMAND_NAMES, REJECTION_RULES, EXIT_CODES, DEFAULT_TIMINGS;
55008
+ var DAEMON_VERSION, HEARTBEAT_INTERVAL_MS = 20000, WATCHDOG_QUIET_MS = 30000, CANCEL_TIMEOUT_MS = 3000, PERMISSION_POLL_MS = 1500, INGEST_TIMEOUT_MS = 1e4, MAX_BATCH = 200, DEFAULT_MAX_PANES = 4, STOP_DRAIN_MS = 1e4, PERMISSION_WAIT_MS, PERMISSION_OPEN_RETRY_MS = 90000, MIN_SECRET_LENGTH = 8, MAX_REQUEST_BYTES, PANE_ID2, ENV_KEY, LIVE_KEY_PREFIX = "KODIZM_LIVE_", TOKEN_PREFIX = "kdz-int-jwt.", LAST_MESSAGE_FILE = "last-assistant-message.txt", COMMAND_NAMES, UNATTENDED_DENIAL = "This session runs unattended and approves nothing, so the tool call is denied.", REJECTION_RULES, EXIT_CODES, DEFAULT_TIMINGS;
54271
55009
  var init_daemon = __esm(() => {
55010
+ init_package();
54272
55011
  init_logger();
54273
55012
  init_events2();
54274
55013
  init_hooks();
54275
55014
  init_input();
54276
55015
  init_pane();
55016
+ init_policy4();
55017
+ init_start_flags();
54277
55018
  init_transcript();
55019
+ DAEMON_VERSION = package_default.version;
54278
55020
  PERMISSION_WAIT_MS = PERMISSION_HOOK_TIMEOUT_S * 1000 - 1e4;
54279
55021
  MAX_REQUEST_BYTES = 8 * 1024 * 1024;
54280
55022
  PANE_ID2 = /^[A-Za-z0-9_-]{1,64}$/;
@@ -54308,14 +55050,17 @@ var init_daemon = __esm(() => {
54308
55050
  monitorMs: 2000,
54309
55051
  transcriptPollMs: 250,
54310
55052
  retryBaseMs: 250,
54311
- retryMaxMs: 1e4
55053
+ retryMaxMs: 1e4,
55054
+ secondMs: 1000,
55055
+ idleTimeoutMs: 0,
55056
+ deliveryDrainMs: 120000
54312
55057
  };
54313
55058
  });
54314
55059
 
54315
55060
  // src/live/ctl.ts
54316
55061
  import { spawn as spawn2 } from "child_process";
54317
55062
  import { homedir as homedir2, tmpdir } from "os";
54318
- import { resolve as resolve5 } from "path";
55063
+ import { resolve as resolve6 } from "path";
54319
55064
  async function runTmuxd(args) {
54320
55065
  const [command, ...rest] = args;
54321
55066
  switch (command) {
@@ -54338,9 +55083,9 @@ async function runTmuxd(args) {
54338
55083
  async function runCtl(args) {
54339
55084
  const [verb, ...rest] = args;
54340
55085
  const flags = parseFlags(rest);
54341
- if (verb === undefined || !VERBS.has(verb) || flags === null) {
55086
+ if (verb === undefined || !VERBS.has(verb) || flags === null || verb === "start" && parseStartExtras(flags) === null) {
54342
55087
  await discardHandoff(verb, rest);
54343
- return print({ ok: false, code: "usage_error" });
55088
+ return print(ctlMade("usage_error"));
54344
55089
  }
54345
55090
  const socketPath = socketPathFromEnv();
54346
55091
  const message = {
@@ -54359,7 +55104,7 @@ async function runCtl(args) {
54359
55104
  if (error.reason === "unreachable") {
54360
55105
  await discardHandoff(verb, rest);
54361
55106
  }
54362
- return print(error.reason === "unreachable" ? { ok: false, code: "pane_not_running", data: { pane: flags.pane, running: false } } : { ok: false, code: "timeout" });
55107
+ return print(error.reason === "unreachable" ? ctlMade("pane_not_running", { pane: flags.pane, running: false }) : ctlMade("timeout"));
54363
55108
  }
54364
55109
  }
54365
55110
  async function ask(socketPath, message, spawnIfMissing) {
@@ -54426,6 +55171,7 @@ async function runDaemon() {
54426
55171
  baseSettingsPath: env.KODIZM_LIVE_BASE_SETTINGS ?? DEFAULT_BASE_SETTINGS_PATH,
54427
55172
  maxPanes: Number.isInteger(maxPanes) && maxPanes > 0 ? maxPanes : DEFAULT_MAX_PANES,
54428
55173
  logger: createLogger({ env }),
55174
+ timings: { idleTimeoutMs: idleTimeoutMs(env) },
54429
55175
  onIdle: () => void daemon.close().then(shutdown)
54430
55176
  });
54431
55177
  await daemon.listen();
@@ -54435,6 +55181,10 @@ async function runDaemon() {
54435
55181
  await stopped;
54436
55182
  return 0;
54437
55183
  }
55184
+ function idleTimeoutMs(env) {
55185
+ const seconds = env.KODIZM_TMUXD_IDLE_TIMEOUT_SECONDS;
55186
+ return seconds !== undefined && /^[1-9]\d*$/.test(seconds) ? Number(seconds) * 1000 : 0;
55187
+ }
54438
55188
  function spawnDaemon() {
54439
55189
  const child = spawn2(process.execPath, [process.argv[1] ?? "", "tmuxd", "daemon"], {
54440
55190
  detached: true,
@@ -54455,21 +55205,37 @@ function parseFlags(args) {
54455
55205
  if (!flag.startsWith("--") || name === "" || value === undefined || name in flags) {
54456
55206
  return null;
54457
55207
  }
54458
- flags[name] = PATH_FLAGS.has(name) ? resolve5(value) : value;
55208
+ flags[name] = PATH_FLAGS.has(name) ? resolve6(value) : value;
54459
55209
  }
54460
55210
  return flags;
54461
55211
  }
54462
55212
  async function discardHandoff(verb, args) {
54463
- const flag = verb === undefined ? undefined : HANDOFF_FLAGS[verb];
54464
- if (flag === undefined) {
55213
+ const flags = verb === undefined ? undefined : HANDOFF_FLAGS.get(verb);
55214
+ if (flags === undefined) {
54465
55215
  return;
54466
55216
  }
55217
+ const files = [];
54467
55218
  for (let index = 0;index < args.length - 1; index += 1) {
54468
55219
  const value = args[index + 1];
54469
- if (args[index] === `--${flag}` && value !== undefined) {
54470
- await removeFile(resolve5(value));
55220
+ const flag = flags.find((name) => args[index] === `--${name}`);
55221
+ if (value !== undefined && flag !== undefined) {
55222
+ files.push([flag, resolve6(value)]);
54471
55223
  }
54472
55224
  }
55225
+ for (const failure of await removeHandoffFiles(files)) {
55226
+ process.stderr.write(`${JSON.stringify({ level: "error", message: "tmuxd ctl could not remove a hand-off file", ...failure })}
55227
+ `);
55228
+ }
55229
+ }
55230
+ function ctlMade(code, data = {}) {
55231
+ return {
55232
+ ok: false,
55233
+ code,
55234
+ data: {
55235
+ daemon_version: DAEMON_VERSION,
55236
+ ...data
55237
+ }
55238
+ };
54473
55239
  }
54474
55240
  function toResult(response) {
54475
55241
  const code = typeof response === "object" && response !== null ? response.code : undefined;
@@ -54498,11 +55264,15 @@ var init_ctl = __esm(() => {
54498
55264
  init_daemon();
54499
55265
  init_hooks();
54500
55266
  init_pane();
55267
+ init_start_flags();
54501
55268
  USAGE = `Usage: kodizm-acp tmuxd <command>
54502
55269
 
54503
55270
  Commands:
54504
55271
  ctl <verb> [flags] Drive a Live pane through the daemon; prints one JSON line.
54505
55272
  Verbs: start, prompt, command, cancel, stop, rotate-token, status.
55273
+ start also takes --permission-mode default|dontAsk, --one-shot 0|1,
55274
+ --deadline-seconds N, --retain-failed-seconds N and
55275
+ --mcp-config/--tool-policy/--system-prompt-file PATH.
54506
55276
  hook <Event> --pane <id> --socket <path> Hook helper the pane settings run; reads the payload on stdin.
54507
55277
  daemon Run the daemon in the foreground; ctl start spawns it detached.
54508
55278
 
@@ -54510,82 +55280,30 @@ Environment:
54510
55280
  KODIZM_TMUXD_SOCKET daemon socket (default: $XDG_RUNTIME_DIR or the temp dir, kodizm-live-<uid>/tmuxd.sock)
54511
55281
  KODIZM_TMUXD_TMUX_SOCKET tmux socket name (default: ${LIVE_SOCKET})
54512
55282
  KODIZM_TMUXD_MAX_PANES panes per daemon (default: ${DEFAULT_MAX_PANES})
55283
+ KODIZM_TMUXD_IDLE_TIMEOUT_SECONDS
55284
+ end a pane with no turn, dialog or activity for N seconds (default: off)
54513
55285
  KODIZM_LIVE_BASE_SETTINGS base settings with the deny list (default: ${DEFAULT_BASE_SETTINGS_PATH})
54514
55286
  CLAUDE_CODE_PATH claude binary (default: claude on PATH)
54515
55287
  `;
54516
55288
  VERBS = new Set(["start", "prompt", "command", "cancel", "stop", "rotate-token", "status"]);
54517
- PATH_FLAGS = new Set(["settings-base", "env-file", "file", "token-file"]);
54518
- HANDOFF_FLAGS = {
54519
- start: "env-file",
54520
- prompt: "file",
54521
- "rotate-token": "token-file"
54522
- };
55289
+ PATH_FLAGS = new Set([
55290
+ "settings-base",
55291
+ "env-file",
55292
+ "file",
55293
+ "token-file",
55294
+ "mcp-config",
55295
+ "tool-policy",
55296
+ "system-prompt-file"
55297
+ ]);
55298
+ HANDOFF_FLAGS = new Map([
55299
+ ["start", START_FILE_FLAGS],
55300
+ ["prompt", ["file"]],
55301
+ ["rotate-token", ["token-file"]]
55302
+ ]);
54523
55303
  });
54524
- // package.json
54525
- var package_default = {
54526
- name: "@kodizm/acp",
54527
- version: "0.8.2",
54528
- description: "Custom ACP server bridging Claude Code and opencode CLIs through a single Kodizm-flavored protocol surface.",
54529
- type: "module",
54530
- bin: {
54531
- "kodizm-acp": "./dist/index.js"
54532
- },
54533
- main: "./dist/index.js",
54534
- files: [
54535
- "dist/",
54536
- "README.md",
54537
- "LICENSE"
54538
- ],
54539
- scripts: {
54540
- dev: "bun run src/index.ts",
54541
- build: "bun build src/index.ts --target=bun --outdir=dist",
54542
- test: "bun test test/unit test/e2e",
54543
- "test:unit": "bun test test/unit",
54544
- "test:e2e": "bun test test/e2e",
54545
- "test:integration": "./scripts/reap-opencode.sh bun test test/integration",
54546
- "test:watch": "bun test --watch",
54547
- lint: "biome check src test",
54548
- "lint:fix": "biome check --write src test",
54549
- format: "biome format --write src test",
54550
- typecheck: "tsc --noEmit",
54551
- "typecheck:src": "tsc --noEmit -p tsconfig.publish.json",
54552
- prepublishOnly: "bun run build && bun run typecheck:src"
54553
- },
54554
- engines: {
54555
- bun: ">=1.1.0"
54556
- },
54557
- publishConfig: {
54558
- access: "public"
54559
- },
54560
- config: {
54561
- claudeAgentSdk: "0.3.280"
54562
- },
54563
- devDependencies: {
54564
- "@anthropic-ai/claude-agent-sdk": "0.3.280",
54565
- "@anthropic-ai/sdk": ">=0.93.0",
54566
- "@biomejs/biome": "^1.9.0",
54567
- "@modelcontextprotocol/sdk": "^1.29.0",
54568
- "@opencode-ai/sdk": "^1.18.32",
54569
- "@types/bun": "latest",
54570
- typescript: "^5.6.0",
54571
- zod: "^4.0.0"
54572
- },
54573
- license: "Apache-2.0",
54574
- homepage: "https://github.com/kodizm/acp",
54575
- repository: {
54576
- type: "git",
54577
- url: "git+https://github.com/kodizm/acp.git"
54578
- },
54579
- keywords: [
54580
- "acp",
54581
- "agent-client-protocol",
54582
- "claude",
54583
- "opencode",
54584
- "ai",
54585
- "bridge",
54586
- "jsonrpc"
54587
- ]
54588
- };
55304
+
55305
+ // src/index.ts
55306
+ init_package();
54589
55307
 
54590
55308
  // src/backends/driver.ts
54591
55309
  init_errors();