@4pm/cli 0.7.0 → 0.8.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 +656 -156
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1353,7 +1353,7 @@ var require_react_development = __commonJS({
1353
1353
  var dispatcher = resolveDispatcher();
1354
1354
  return dispatcher.useReducer(reducer, initialArg, init);
1355
1355
  }
1356
- function useRef(initialValue) {
1356
+ function useRef2(initialValue) {
1357
1357
  var dispatcher = resolveDispatcher();
1358
1358
  return dispatcher.useRef(initialValue);
1359
1359
  }
@@ -1369,7 +1369,7 @@ var require_react_development = __commonJS({
1369
1369
  var dispatcher = resolveDispatcher();
1370
1370
  return dispatcher.useLayoutEffect(create2, deps);
1371
1371
  }
1372
- function useCallback(callback, deps) {
1372
+ function useCallback2(callback, deps) {
1373
1373
  var dispatcher = resolveDispatcher();
1374
1374
  return dispatcher.useCallback(callback, deps);
1375
1375
  }
@@ -1995,14 +1995,14 @@ var require_react_development = __commonJS({
1995
1995
  var thenableResult = result;
1996
1996
  var wasAwaited = false;
1997
1997
  var thenable = {
1998
- then: function(resolve6, reject) {
1998
+ then: function(resolve7, reject) {
1999
1999
  wasAwaited = true;
2000
2000
  thenableResult.then(function(returnValue2) {
2001
2001
  popActScope(prevActScopeDepth);
2002
2002
  if (actScopeDepth === 0) {
2003
- recursivelyFlushAsyncActWork(returnValue2, resolve6, reject);
2003
+ recursivelyFlushAsyncActWork(returnValue2, resolve7, reject);
2004
2004
  } else {
2005
- resolve6(returnValue2);
2005
+ resolve7(returnValue2);
2006
2006
  }
2007
2007
  }, function(error2) {
2008
2008
  popActScope(prevActScopeDepth);
@@ -2032,20 +2032,20 @@ var require_react_development = __commonJS({
2032
2032
  ReactCurrentActQueue.current = null;
2033
2033
  }
2034
2034
  var _thenable = {
2035
- then: function(resolve6, reject) {
2035
+ then: function(resolve7, reject) {
2036
2036
  if (ReactCurrentActQueue.current === null) {
2037
2037
  ReactCurrentActQueue.current = [];
2038
- recursivelyFlushAsyncActWork(returnValue, resolve6, reject);
2038
+ recursivelyFlushAsyncActWork(returnValue, resolve7, reject);
2039
2039
  } else {
2040
- resolve6(returnValue);
2040
+ resolve7(returnValue);
2041
2041
  }
2042
2042
  }
2043
2043
  };
2044
2044
  return _thenable;
2045
2045
  } else {
2046
2046
  var _thenable2 = {
2047
- then: function(resolve6, reject) {
2048
- resolve6(returnValue);
2047
+ then: function(resolve7, reject) {
2048
+ resolve7(returnValue);
2049
2049
  }
2050
2050
  };
2051
2051
  return _thenable2;
@@ -2061,7 +2061,7 @@ var require_react_development = __commonJS({
2061
2061
  actScopeDepth = prevActScopeDepth;
2062
2062
  }
2063
2063
  }
2064
- function recursivelyFlushAsyncActWork(returnValue, resolve6, reject) {
2064
+ function recursivelyFlushAsyncActWork(returnValue, resolve7, reject) {
2065
2065
  {
2066
2066
  var queue = ReactCurrentActQueue.current;
2067
2067
  if (queue !== null) {
@@ -2070,16 +2070,16 @@ var require_react_development = __commonJS({
2070
2070
  enqueueTask(function() {
2071
2071
  if (queue.length === 0) {
2072
2072
  ReactCurrentActQueue.current = null;
2073
- resolve6(returnValue);
2073
+ resolve7(returnValue);
2074
2074
  } else {
2075
- recursivelyFlushAsyncActWork(returnValue, resolve6, reject);
2075
+ recursivelyFlushAsyncActWork(returnValue, resolve7, reject);
2076
2076
  }
2077
2077
  });
2078
2078
  } catch (error2) {
2079
2079
  reject(error2);
2080
2080
  }
2081
2081
  } else {
2082
- resolve6(returnValue);
2082
+ resolve7(returnValue);
2083
2083
  }
2084
2084
  }
2085
2085
  }
@@ -2136,7 +2136,7 @@ var require_react_development = __commonJS({
2136
2136
  exports.memo = memo;
2137
2137
  exports.startTransition = startTransition;
2138
2138
  exports.unstable_act = act;
2139
- exports.useCallback = useCallback;
2139
+ exports.useCallback = useCallback2;
2140
2140
  exports.useContext = useContext7;
2141
2141
  exports.useDebugValue = useDebugValue;
2142
2142
  exports.useDeferredValue = useDeferredValue;
@@ -2147,7 +2147,7 @@ var require_react_development = __commonJS({
2147
2147
  exports.useLayoutEffect = useLayoutEffect2;
2148
2148
  exports.useMemo = useMemo4;
2149
2149
  exports.useReducer = useReducer;
2150
- exports.useRef = useRef;
2150
+ exports.useRef = useRef2;
2151
2151
  exports.useState = useState6;
2152
2152
  exports.useSyncExternalStore = useSyncExternalStore;
2153
2153
  exports.useTransition = useTransition;
@@ -24482,7 +24482,8 @@ function defaultProfileConfig() {
24482
24482
  codexHome: [{ profile: ".codex", args: ["exec"], model: "" }],
24483
24483
  antigravityHome: [],
24484
24484
  autoUpdate: true,
24485
- commandHistoryUploadMinutes: 10
24485
+ commandHistoryUploadMinutes: 10,
24486
+ autoClearIdleMinutes: 10
24486
24487
  };
24487
24488
  }
24488
24489
  function ensureProfileConfig(dir2) {
@@ -24690,8 +24691,8 @@ async function runLink(serverUrl, explicitProfile) {
24690
24691
  }
24691
24692
 
24692
24693
  // src/commands/start.ts
24693
- import { spawn as spawn4 } from "child_process";
24694
- import { join as join17 } from "path";
24694
+ import { spawn as spawn5 } from "child_process";
24695
+ import { join as join19 } from "path";
24695
24696
 
24696
24697
  // src/core/update.ts
24697
24698
  import { execSync as execSync2 } from "child_process";
@@ -24862,7 +24863,7 @@ async function checkAndUpdate(serverUrl, autoUpdate) {
24862
24863
 
24863
24864
  // src/core/ws-client.ts
24864
24865
  import { randomUUID } from "crypto";
24865
- import { basename as basename3, join as join13 } from "path";
24866
+ import { basename as basename3, join as join15 } from "path";
24866
24867
 
24867
24868
  // ../../22-packages/11-shared/51-utils/src/helpers.ts
24868
24869
  function backoffJitterMs(attempt, baseMs = 500, maxMs = 1e4) {
@@ -24965,6 +24966,12 @@ var PermissionCode = {
24965
24966
  PROJECT_AI_ASSIST: "project.ai_assist",
24966
24967
  PROJECT_MEMO_READ: "project.memo_read",
24967
24968
  PROJECT_MEMO_WRITE: "project.memo_write",
24969
+ // Git tab (ADR-0151): read git (branches/graph/log/status/diff) via command dispatch;
24970
+ // write = commit/push/pull/checkout/merge/PR + fs.write (machine-0027)
24971
+ PROJECT_GIT: "project.git",
24972
+ PROJECT_GIT_WRITE: "project.git_write",
24973
+ // Autonomous mode (ADR-0152): settings/start-pause/VERIFY-approve/post-USER_TODO
24974
+ PROJECT_AUTONOMOUS: "project.autonomous",
24968
24975
  // permission
24969
24976
  PERMISSION_LIST: "permission.list",
24970
24977
  PERMISSION_READ: "permission.read",
@@ -25064,6 +25071,9 @@ var ROLE_DEFAULT_PERMISSIONS = {
25064
25071
  P.PROJECT_AI_ASSIST,
25065
25072
  P.PROJECT_MEMO_READ,
25066
25073
  P.PROJECT_MEMO_WRITE,
25074
+ P.PROJECT_GIT,
25075
+ P.PROJECT_GIT_WRITE,
25076
+ P.PROJECT_AUTONOMOUS,
25067
25077
  P.PERMISSION_LIST,
25068
25078
  P.PERMISSION_READ,
25069
25079
  P.MACHINE_PAIR,
@@ -25088,6 +25098,7 @@ var ROLE_DEFAULT_PERMISSIONS = {
25088
25098
  P.PROJECT_READ,
25089
25099
  P.PROJECT_MEMO_READ,
25090
25100
  P.PROJECT_MEMO_WRITE,
25101
+ P.PROJECT_GIT,
25091
25102
  P.INTEGRATION_READ
25092
25103
  ],
25093
25104
  [Role.QA]: [
@@ -25098,6 +25109,7 @@ var ROLE_DEFAULT_PERMISSIONS = {
25098
25109
  P.PROJECT_READ,
25099
25110
  P.PROJECT_MEMO_READ,
25100
25111
  P.PROJECT_MEMO_WRITE,
25112
+ P.PROJECT_GIT,
25101
25113
  P.INTEGRATION_READ
25102
25114
  ],
25103
25115
  [Role.BA]: [
@@ -25108,6 +25120,7 @@ var ROLE_DEFAULT_PERMISSIONS = {
25108
25120
  P.PROJECT_READ,
25109
25121
  P.PROJECT_MEMO_READ,
25110
25122
  P.PROJECT_MEMO_WRITE,
25123
+ P.PROJECT_GIT,
25111
25124
  P.INTEGRATION_READ
25112
25125
  ],
25113
25126
  [Role.MACHINE]: [
@@ -25115,6 +25128,11 @@ var ROLE_DEFAULT_PERMISSIONS = {
25115
25128
  P.PROJECT_READ,
25116
25129
  P.PROJECT_MEMO_READ,
25117
25130
  P.PROJECT_MEMO_WRITE,
25131
+ // git read + write so the autonomous merge cycle (auto-cycle.md) can run (ADR-0151)
25132
+ P.PROJECT_GIT,
25133
+ P.PROJECT_GIT_WRITE,
25134
+ // autonomous mode control on its own physic project (ADR-0152)
25135
+ P.PROJECT_AUTONOMOUS,
25118
25136
  // view one's own worker/cli + configure the physic project
25119
25137
  P.MACHINE_LIST,
25120
25138
  P.MACHINE_PHYSIC_MANAGE,
@@ -25142,7 +25160,10 @@ var PROJECT_SCOPE_CHECKED_CODES = [
25142
25160
  P.PROJECT_PAUSE,
25143
25161
  P.PROJECT_CLOSE,
25144
25162
  P.PROJECT_PAUSE_MEMBER,
25145
- P.PROJECT_AI_ASSIST
25163
+ P.PROJECT_AI_ASSIST,
25164
+ P.PROJECT_GIT,
25165
+ P.PROJECT_GIT_WRITE,
25166
+ P.PROJECT_AUTONOMOUS
25146
25167
  ];
25147
25168
  var SELF_SERVICE_CODES = [
25148
25169
  P.AUTH_LOGOUT,
@@ -25182,6 +25203,9 @@ var TEAM_GRANTABLE_CODES = [
25182
25203
  P.PROJECT_AI_ASSIST,
25183
25204
  P.PROJECT_MEMO_READ,
25184
25205
  P.PROJECT_MEMO_WRITE,
25206
+ P.PROJECT_GIT,
25207
+ P.PROJECT_GIT_WRITE,
25208
+ P.PROJECT_AUTONOMOUS,
25185
25209
  P.COMMAND_EXECUTE,
25186
25210
  P.COMMAND_READ,
25187
25211
  P.MACHINE_LIST,
@@ -25203,6 +25227,9 @@ var PROJECT_GRANTABLE_CODES = [
25203
25227
  P.PROJECT_AI_ASSIST,
25204
25228
  P.PROJECT_MEMO_READ,
25205
25229
  P.PROJECT_MEMO_WRITE,
25230
+ P.PROJECT_GIT,
25231
+ P.PROJECT_GIT_WRITE,
25232
+ P.PROJECT_AUTONOMOUS,
25206
25233
  P.COMMAND_EXECUTE,
25207
25234
  P.COMMAND_READ,
25208
25235
  P.MACHINE_PHYSIC_MANAGE
@@ -29488,6 +29515,9 @@ var createProjectRequestSchema = external_exports.object({
29488
29515
  description: external_exports.string().max(2e3).optional(),
29489
29516
  machineUserId: external_exports.string().uuid().optional()
29490
29517
  });
29518
+ var copyProjectRequestSchema = external_exports.object({
29519
+ name: external_exports.string().min(1).max(100)
29520
+ });
29491
29521
  var DEFAULT_OUTBOUND_RULES = {
29492
29522
  secret: [
29493
29523
  String.raw`/\b(api[_-]?key|secret|password|passwd|access[_-]?token|bearer)\b\s*[:=]\s*\S+/i`,
@@ -29518,7 +29548,15 @@ var updateProjectRequestSchema = external_exports.object({
29518
29548
  // Messenger retention in days (0 = keep forever) — manager only (ADR-0078).
29519
29549
  messengerRetentionDays: external_exports.number().int().min(0).max(3650).optional(),
29520
29550
  // Project settings (ADR-0081) — key-level shallow-merge; `tokens` clamped on read.
29521
- settings: external_exports.record(external_exports.unknown()).optional()
29551
+ settings: external_exports.record(external_exports.unknown()).optional(),
29552
+ // Identity ↔ spec sync (ADR-0148): the Settings tab edits the spec basic group
29553
+ // (id = spec slug, name, desc). The server merges these into `spec` (READY) or
29554
+ // `specDraft` (DRAFT) so the Content → Spec tab / wizard stays consistent.
29555
+ specBasic: external_exports.object({
29556
+ id: external_exports.string().max(200),
29557
+ name: external_exports.string().max(100),
29558
+ desc: external_exports.string().max(2e3)
29559
+ }).partial().optional()
29522
29560
  });
29523
29561
  var MEMO_MAX_LENGTH = 65536;
29524
29562
  var putMemoRequestSchema = external_exports.object({
@@ -29705,6 +29743,25 @@ var listMachineLinksQuerySchema = baseRequestSchema.extend({
29705
29743
  var updateMachineLinkRequestSchema = external_exports.object({
29706
29744
  hashcode3ExpiresAt: external_exports.string().datetime().nullable()
29707
29745
  });
29746
+ var configWriteRequestSchema = external_exports.object({
29747
+ config: external_exports.string().max(256 * 1024)
29748
+ });
29749
+ var FILE_WRITE_MAX_BYTES = 2 * 1024 * 1024;
29750
+ var fsWriteRequestSchema = external_exports.object({
29751
+ /** Path relative to the physic-project root; a `..` escaping the root is rejected on the cli. */
29752
+ path: external_exports.string().min(1).max(1024),
29753
+ /** Full new UTF-8 content (replaces the file wholesale). */
29754
+ content: external_exports.string().max(FILE_WRITE_MAX_BYTES)
29755
+ });
29756
+ var autonomousWriteRequestSchema = external_exports.discriminatedUnion("kind", [
29757
+ external_exports.object({ kind: external_exports.literal("settings"), settings: external_exports.string().max(64 * 1024) }),
29758
+ external_exports.object({ kind: external_exports.literal("approvals"), taskId: external_exports.string().min(1).max(64), approved: external_exports.boolean() }),
29759
+ external_exports.object({ kind: external_exports.literal("userTodo"), content: external_exports.string().min(1).max(16 * 1024) }),
29760
+ external_exports.object({ kind: external_exports.literal("cron"), action: external_exports.enum(["install", "uninstall"]) })
29761
+ ]);
29762
+ var autonomousLogsQuerySchema = external_exports.object({
29763
+ date: external_exports.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional()
29764
+ });
29708
29765
 
29709
29766
  // ../../22-packages/11-shared/31-dto/src/command.ts
29710
29767
  var COMMAND_MAX_LEN = 8e3;
@@ -29725,7 +29782,14 @@ var dispatchCommandRequestSchema = external_exports.object({
29725
29782
  * Short human label shown to OTHER tabs watching this cli's activity feed (ADR-0101):
29726
29783
  * e.g. "AI review" so the Console shows a friendly line instead of the raw prompt.
29727
29784
  */
29728
- label: external_exports.string().max(120).optional()
29785
+ label: external_exports.string().max(120).optional(),
29786
+ /**
29787
+ * Git tab (ADR-0151): when set, `command` is a `git`/`gh`/`glab` command run on the
29788
+ * worker for the Git tab. The server routes the permission to `project.git` (`read`) /
29789
+ * `project.git_write` (`write`) instead of `command.execute`, and validates `command`
29790
+ * against the git allowlist (`isGitCommandAllowed`). Absent ⇒ normal console dispatch.
29791
+ */
29792
+ gitOp: external_exports.enum(["read", "write"]).optional()
29729
29793
  }).superRefine((v, ctx) => {
29730
29794
  if (!v.ai && v.command.length > COMMAND_MAX_LEN) {
29731
29795
  ctx.addIssue({
@@ -29872,12 +29936,6 @@ var checkoutRequestSchema = external_exports.object({
29872
29936
  planCode: external_exports.string().min(1).max(50),
29873
29937
  provider: external_exports.enum([PaymentProvider.STRIPE, PaymentProvider.PAYPAL]).optional()
29874
29938
  });
29875
- var upsertPlanRequestSchema = external_exports.object({
29876
- name: external_exports.string().min(1).max(100),
29877
- price: external_exports.number().int().min(0).default(0),
29878
- limits: external_exports.array(planLimitSchema).default([]),
29879
- entitlements: planEntitlementsSchema.optional()
29880
- });
29881
29939
  var assignPlanRequestSchema = external_exports.object({
29882
29940
  planCode: external_exports.string().min(1).max(50),
29883
29941
  /** Cycle length in days (defaults to 30). */
@@ -29948,8 +30006,21 @@ var WsChannels = {
29948
30006
  ...WsChannel,
29949
30007
  FS_LIST: "fs.list",
29950
30008
  FS_READ: "fs.read",
30009
+ // server → cli (reply): write a file, path-clamped to the physic root (machine-0027,
30010
+ // ADR-0151) — the Git tab's manual conflict resolution (`project.git_write`); the only
30011
+ // git-tab write not carried by a git command over command dispatch.
30012
+ FS_WRITE: "fs.write",
29951
30013
  GIT_DIFF: "git.diff",
29952
30014
  GIT_ENV: "git.env",
30015
+ // server → cli (reply): read/write the paired profile's config.json (machine-0025/0026,
30016
+ // ADR-0141) — the machine-user Settings tab syncs the worker cli's config.
30017
+ CONFIG_READ: "config.read",
30018
+ CONFIG_WRITE: "config.write",
30019
+ // autonomous mode control (ADR-0152): read settings+status+books+approvals, tail logs, and
30020
+ // write {settings|approvals|userTodo|cron} — the dashboard drives the worker's autonomous engine.
30021
+ AUTONOMOUS_READ: "autonomous.read",
30022
+ AUTONOMOUS_WRITE: "autonomous.write",
30023
+ AUTONOMOUS_LOGS: "autonomous.logs",
29953
30024
  // git history browse (read-through cli — ADR-0089): repos on the physic project,
29954
30025
  // commit log, changed files of a commit, parent↔commit diff of one file.
29955
30026
  GIT_REPOS: "git.repos",
@@ -29967,6 +30038,11 @@ var WsChannels = {
29967
30038
  PHYSIC_DELETE: "physic.delete",
29968
30039
  // command lifecycle
29969
30040
  COMMAND_ANNOUNCE: "command.announce",
30041
+ // console transcript sync (ADR-0150): the cli streams its authoritative SessionBus
30042
+ // transcript so the web Console renders it verbatim; gated by console.watch (server → cli:
30043
+ // whether a web viewer is attached) so an unwatched cli syncs nothing.
30044
+ CONSOLE_SYNC: "console.sync",
30045
+ CONSOLE_WATCH: "console.watch",
29970
30046
  // AI spec-assist (suggest/review/compose) moved off dedicated ai.* channels to
29971
30047
  // command.dispatch({ ai:true }) → the profile-failover AI path (ADR-0100).
29972
30048
  // quota & usage (ADR-0020)
@@ -30104,7 +30180,7 @@ var OutputBatcher = class {
30104
30180
  };
30105
30181
 
30106
30182
  // src/core/ws-client.ts
30107
- import { existsSync as existsSync7, mkdirSync as mkdirSync4, rmSync as rmSync4 } from "fs";
30183
+ import { existsSync as existsSync8, mkdirSync as mkdirSync4, rmSync as rmSync4 } from "fs";
30108
30184
  import { randomUUID as randomCommandId } from "crypto";
30109
30185
 
30110
30186
  // src/core/update-scheduler.ts
@@ -30347,7 +30423,7 @@ async function runCommand(dispatch, emit) {
30347
30423
  });
30348
30424
  child.stdout?.on("data", (data) => batcher.push(data.toString("utf8")));
30349
30425
  child.stderr?.on("data", (data) => batcher.push(data.toString("utf8")));
30350
- await new Promise((resolve6) => {
30426
+ await new Promise((resolve7) => {
30351
30427
  child.on("close", (exitCode) => {
30352
30428
  batcher.flush();
30353
30429
  emit({
@@ -30357,7 +30433,7 @@ async function runCommand(dispatch, emit) {
30357
30433
  done: true,
30358
30434
  exitCode: exitCode ?? -1
30359
30435
  });
30360
- resolve6();
30436
+ resolve7();
30361
30437
  });
30362
30438
  child.on("error", (err) => {
30363
30439
  emit({
@@ -30368,7 +30444,7 @@ async function runCommand(dispatch, emit) {
30368
30444
  done: true,
30369
30445
  exitCode: -1
30370
30446
  });
30371
- resolve6();
30447
+ resolve7();
30372
30448
  });
30373
30449
  });
30374
30450
  }
@@ -30448,10 +30524,10 @@ function isSessionLimit(text) {
30448
30524
  );
30449
30525
  }
30450
30526
  function runAttempt(commandId, cmd, args, cwd2, env3, onChunk) {
30451
- return new Promise((resolve6) => {
30527
+ return new Promise((resolve7) => {
30452
30528
  void runCommand({ commandId, cmd, args, path: cwd2, env: env3 }, (out) => {
30453
30529
  if (out.chunk) onChunk(out.chunk);
30454
- if (out.done) resolve6(out.exitCode ?? -1);
30530
+ if (out.done) resolve7(out.exitCode ?? -1);
30455
30531
  });
30456
30532
  });
30457
30533
  }
@@ -30629,16 +30705,16 @@ async function fetchUsage(token) {
30629
30705
  }
30630
30706
  }
30631
30707
  function runUsageCli(dir2, aiCli2) {
30632
- return new Promise((resolve6) => {
30708
+ return new Promise((resolve7) => {
30633
30709
  let out = "";
30634
30710
  let child = null;
30635
30711
  const finish = () => {
30636
30712
  clearTimeout(timer);
30637
- resolve6(out);
30713
+ resolve7(out);
30638
30714
  };
30639
30715
  const timer = setTimeout(() => {
30640
30716
  child?.kill();
30641
- resolve6(out);
30717
+ resolve7(out);
30642
30718
  }, 3e4);
30643
30719
  timer.unref?.();
30644
30720
  try {
@@ -31069,24 +31145,210 @@ async function readWorkerFile(path) {
31069
31145
  }
31070
31146
  }
31071
31147
 
31072
- // src/core/git-diff.ts
31073
- import { readFile as readFile2 } from "fs/promises";
31074
- import { execFile as execFile2 } from "child_process";
31075
- import { basename as basename2, dirname as dirname3, resolve as resolve3 } from "path";
31148
+ // src/core/fs-write.ts
31149
+ import { mkdir as mkdir2, writeFile } from "fs/promises";
31150
+ import { dirname as dirname3, resolve as resolve3, sep as sep3 } from "path";
31151
+ var MAX_BYTES2 = 2 * 1024 * 1024;
31152
+ function resolveInRoot(root, relPath) {
31153
+ if (!root) return null;
31154
+ const base = resolve3(root);
31155
+ const target = resolve3(base, relPath || ".");
31156
+ return target === base || target.startsWith(base + sep3) ? target : null;
31157
+ }
31158
+ async function writeWorkerFile(root, relPath, content) {
31159
+ const target = resolveInRoot(root, relPath);
31160
+ if (!target) {
31161
+ return { ok: false, path: relPath, bytes: 0, error: "path escapes the project root" };
31162
+ }
31163
+ const bytes = Buffer.byteLength(content, "utf8");
31164
+ if (bytes > MAX_BYTES2) {
31165
+ return { ok: false, path: target, bytes: 0, error: `content too large (max ${MAX_BYTES2} bytes)` };
31166
+ }
31167
+ try {
31168
+ await mkdir2(dirname3(target), { recursive: true });
31169
+ await writeFile(target, content, "utf8");
31170
+ return { ok: true, path: target, bytes };
31171
+ } catch (err) {
31172
+ return { ok: false, path: target, bytes: 0, error: err instanceof Error ? err.message : String(err) };
31173
+ }
31174
+ }
31175
+
31176
+ // src/core/autonomous.ts
31177
+ import { readFile as readFile2, writeFile as writeFile2, chmod } from "fs/promises";
31178
+ import { readFileSync as readFileSync12 } from "fs";
31179
+ import { execFile as execFile2, spawn as spawn4 } from "child_process";
31180
+ import { join as join12 } from "path";
31076
31181
  import { promisify as promisify2 } from "util";
31077
31182
  var run2 = promisify2(execFile2);
31183
+ var SETTINGS_REL = ".claude/.autonomous.settings.json";
31184
+ var HISTORIES_REL = ".claude/.autonomous.histories.json";
31185
+ var APPROVALS_REL = ".claude/.autonomous.approvals.json";
31186
+ var TICK_REL = ".claude/hooks/autonomous-tick.sh";
31187
+ var LOG_DIR_REL = ".claude/logs";
31188
+ var BOOK_FILES = {
31189
+ userTodo: "USER_TODO.md",
31190
+ aiTodo: "AI_TODO.md",
31191
+ aiProgress: "AI_PROGRESS.md",
31192
+ aiDone: "AI_DONE.md",
31193
+ userQa: "USER_QA.md"
31194
+ };
31195
+ var DEFAULT_CRON = "*/10 * * * *";
31196
+ async function readText(path, fallback = "") {
31197
+ try {
31198
+ return await readFile2(path, "utf8");
31199
+ } catch {
31200
+ return fallback;
31201
+ }
31202
+ }
31203
+ function parseJson(text) {
31204
+ try {
31205
+ const v = JSON.parse(text);
31206
+ return v && typeof v === "object" ? v : null;
31207
+ } catch {
31208
+ return null;
31209
+ }
31210
+ }
31211
+ function tickScript(root) {
31212
+ return join12(root, TICK_REL);
31213
+ }
31214
+ async function crontabList() {
31215
+ try {
31216
+ const { stdout } = await run2("crontab", ["-l"], { timeout: 1e4 });
31217
+ return stdout;
31218
+ } catch {
31219
+ return "";
31220
+ }
31221
+ }
31222
+ async function crontabSet(content) {
31223
+ return new Promise((resolve7) => {
31224
+ const child = spawn4("crontab", ["-"], { stdio: ["pipe", "ignore", "ignore"] });
31225
+ child.on("error", () => resolve7(false));
31226
+ child.on("close", (code) => resolve7(code === 0));
31227
+ child.stdin.end(content.endsWith("\n") ? content : content + "\n");
31228
+ });
31229
+ }
31230
+ async function cronInstalled(root) {
31231
+ const script = tickScript(root);
31232
+ return (await crontabList()).split("\n").some((l) => l.includes(script) && !l.trim().startsWith("#"));
31233
+ }
31234
+ async function getAutonomousStatus(root) {
31235
+ const settings = parseJson(await readText(join12(root, SETTINGS_REL))) ?? {};
31236
+ const hist = parseJson(await readText(join12(root, HISTORIES_REL))) ?? {};
31237
+ const records = Array.isArray(hist.records) ? hist.records : [];
31238
+ const last = records[records.length - 1];
31239
+ const ticks = hist.ticks ?? {};
31240
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
31241
+ return {
31242
+ installed: await cronInstalled(root),
31243
+ paused: settings.paused === true,
31244
+ cronSchedule: typeof settings.cron_schedule === "string" ? settings.cron_schedule : DEFAULT_CRON,
31245
+ lastTickAt: last && typeof last.ts === "string" ? last.ts : null,
31246
+ lastResult: last && typeof last.status === "string" ? last.status : null,
31247
+ consecutiveFails: typeof hist.consecutive_fails === "number" ? hist.consecutive_fails : 0,
31248
+ todayTicks: ticks.day === today && typeof ticks.count === "number" ? ticks.count : 0
31249
+ };
31250
+ }
31251
+ function isAutonomousRunning(root) {
31252
+ try {
31253
+ const s = JSON.parse(readFileSync12(join12(root, SETTINGS_REL), "utf8"));
31254
+ if (s.paused === true) return false;
31255
+ const h = JSON.parse(readFileSync12(join12(root, HISTORIES_REL), "utf8"));
31256
+ const ts = h.records?.[h.records.length - 1]?.ts;
31257
+ if (!ts) return false;
31258
+ const t = Date.parse(ts.replace(" ", "T"));
31259
+ return Number.isFinite(t) && Date.now() - t < 6 * 3600 * 1e3;
31260
+ } catch {
31261
+ return false;
31262
+ }
31263
+ }
31264
+ async function readAutonomous(root) {
31265
+ const [settings, approvals, status, ...books] = await Promise.all([
31266
+ readText(join12(root, SETTINGS_REL)),
31267
+ readText(join12(root, APPROVALS_REL), "{}"),
31268
+ getAutonomousStatus(root),
31269
+ ...Object.values(BOOK_FILES).map((f) => readText(join12(root, f)))
31270
+ ]);
31271
+ const keys = Object.keys(BOOK_FILES);
31272
+ const bookMap = {};
31273
+ keys.forEach((k, i) => bookMap[k] = books[i] ?? "");
31274
+ return { settings, status, books: bookMap, approvals };
31275
+ }
31276
+ async function readAutonomousLogs(root, date) {
31277
+ const day = date && /^\d{4}-\d{2}-\d{2}$/.test(date) ? date : (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
31278
+ const text = await readText(join12(root, LOG_DIR_REL, `autonomous-tick-${day}.log`));
31279
+ const lines = text ? text.split("\n").filter((l) => l.length > 0).slice(-500) : [];
31280
+ return { date: day, lines };
31281
+ }
31282
+ async function installCron(root) {
31283
+ const script = tickScript(root);
31284
+ const status = await getAutonomousStatus(root);
31285
+ await chmod(script, 493).catch(() => void 0);
31286
+ const kept = (await crontabList()).split("\n").filter((l) => l.trim() && !l.includes(script));
31287
+ kept.push(`${status.cronSchedule} ${script}`);
31288
+ await crontabSet(kept.join("\n"));
31289
+ }
31290
+ async function uninstallCron(root) {
31291
+ const script = tickScript(root);
31292
+ const cur = await crontabList();
31293
+ if (!cur.includes(script)) return;
31294
+ const kept = cur.split("\n").filter((l) => l.trim() && !l.includes(script));
31295
+ await crontabSet(kept.join("\n"));
31296
+ }
31297
+ async function writeAutonomous(root, req, by) {
31298
+ try {
31299
+ switch (req.kind) {
31300
+ case "settings": {
31301
+ if (!parseJson(req.settings)) return { ok: false, error: "settings is not valid JSON" };
31302
+ await writeFile2(join12(root, SETTINGS_REL), req.settings, "utf8");
31303
+ break;
31304
+ }
31305
+ case "approvals": {
31306
+ const map = parseJson(await readText(join12(root, APPROVALS_REL), "{}")) ?? {};
31307
+ if (req.approved) map[req.taskId] = { approved: true, by, at: (/* @__PURE__ */ new Date()).toISOString() };
31308
+ else delete map[req.taskId];
31309
+ await writeFile2(join12(root, APPROVALS_REL), JSON.stringify(map, null, 2) + "\n", "utf8");
31310
+ break;
31311
+ }
31312
+ case "userTodo": {
31313
+ const cur = await readText(join12(root, BOOK_FILES.userTodo));
31314
+ const block = `
31315
+
31316
+ > posted by ${by} at ${(/* @__PURE__ */ new Date()).toISOString()}
31317
+ ${req.content.trim()}
31318
+ `;
31319
+ await writeFile2(join12(root, BOOK_FILES.userTodo), cur.trimEnd() + block, "utf8");
31320
+ break;
31321
+ }
31322
+ case "cron": {
31323
+ if (req.action === "install") await installCron(root);
31324
+ else await uninstallCron(root);
31325
+ break;
31326
+ }
31327
+ }
31328
+ return { ok: true, status: await getAutonomousStatus(root) };
31329
+ } catch (err) {
31330
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
31331
+ }
31332
+ }
31333
+
31334
+ // src/core/git-diff.ts
31335
+ import { readFile as readFile3 } from "fs/promises";
31336
+ import { execFile as execFile3 } from "child_process";
31337
+ import { basename as basename2, dirname as dirname4, resolve as resolve4 } from "path";
31338
+ import { promisify as promisify3 } from "util";
31339
+ var run3 = promisify3(execFile3);
31078
31340
  async function gitDiff(path) {
31079
- const target = resolve3(path);
31080
- const dir2 = dirname3(target);
31341
+ const target = resolve4(path);
31342
+ const dir2 = dirname4(target);
31081
31343
  const base = basename2(target);
31082
31344
  let newContent = "";
31083
31345
  try {
31084
- newContent = await readFile2(target, "utf8");
31346
+ newContent = await readFile3(target, "utf8");
31085
31347
  } catch {
31086
31348
  }
31087
31349
  let oldContent = "";
31088
31350
  try {
31089
- const { stdout } = await run2("git", ["show", `HEAD:./${base}`], {
31351
+ const { stdout } = await run3("git", ["show", `HEAD:./${base}`], {
31090
31352
  cwd: dir2,
31091
31353
  timeout: 15e3,
31092
31354
  maxBuffer: 4 * 1024 * 1024
@@ -31098,12 +31360,12 @@ async function gitDiff(path) {
31098
31360
  }
31099
31361
 
31100
31362
  // src/core/git-env.ts
31101
- import { execFile as execFile3 } from "child_process";
31102
- import { promisify as promisify3 } from "util";
31103
- var run3 = promisify3(execFile3);
31363
+ import { execFile as execFile4 } from "child_process";
31364
+ import { promisify as promisify4 } from "util";
31365
+ var run4 = promisify4(execFile4);
31104
31366
  async function hasBinary(bin, args = ["--version"]) {
31105
31367
  try {
31106
- await run3(bin, args, { timeout: 5e3 });
31368
+ await run4(bin, args, { timeout: 5e3 });
31107
31369
  return true;
31108
31370
  } catch {
31109
31371
  return false;
@@ -31116,7 +31378,7 @@ async function checkGitEnv(provider) {
31116
31378
  let account = null;
31117
31379
  if (installed) {
31118
31380
  try {
31119
- const { stdout, stderr } = await run3(bin, ["auth", "status"], { timeout: 8e3 });
31381
+ const { stdout, stderr } = await run4(bin, ["auth", "status"], { timeout: 8e3 });
31120
31382
  const out = `${stdout}
31121
31383
  ${stderr}`;
31122
31384
  authenticated = /logged in|logged into|✓/i.test(out);
@@ -31130,21 +31392,21 @@ ${stderr}`;
31130
31392
 
31131
31393
  // src/core/git-history.ts
31132
31394
  import { readdir as readdir3 } from "fs/promises";
31133
- import { execFile as execFile4 } from "child_process";
31134
- import { resolve as resolve4, sep as sep3 } from "path";
31135
- import { promisify as promisify4 } from "util";
31136
- var run4 = promisify4(execFile4);
31395
+ import { execFile as execFile5 } from "child_process";
31396
+ import { resolve as resolve5, sep as sep4 } from "path";
31397
+ import { promisify as promisify5 } from "util";
31398
+ var run5 = promisify5(execFile5);
31137
31399
  var FS = "";
31138
31400
  var RS = "";
31139
31401
  function resolveRepo(root, repo) {
31140
31402
  if (!root) return null;
31141
- const base = resolve4(root);
31142
- const requested = resolve4(base, repo && repo.trim() ? repo : ".");
31143
- return requested === base || requested.startsWith(base + sep3) ? requested : base;
31403
+ const base = resolve5(root);
31404
+ const requested = resolve5(base, repo && repo.trim() ? repo : ".");
31405
+ return requested === base || requested.startsWith(base + sep4) ? requested : base;
31144
31406
  }
31145
31407
  async function git(cwd2, args) {
31146
31408
  try {
31147
- const { stdout } = await run4("git", args, {
31409
+ const { stdout } = await run5("git", args, {
31148
31410
  cwd: cwd2,
31149
31411
  timeout: 15e3,
31150
31412
  maxBuffer: 8 * 1024 * 1024
@@ -31156,13 +31418,13 @@ async function git(cwd2, args) {
31156
31418
  }
31157
31419
  async function gitRepos(root) {
31158
31420
  if (!root) return { repos: [] };
31159
- const base = resolve4(root);
31421
+ const base = resolve5(root);
31160
31422
  const found = /* @__PURE__ */ new Set();
31161
31423
  const repos = [];
31162
31424
  const add = async (dir2) => {
31163
31425
  const inside = await git(dir2, ["rev-parse", "--show-toplevel"]);
31164
31426
  const top = inside.trim();
31165
- if (!top || found.has(top) || !(top === base || top.startsWith(base + sep3))) return;
31427
+ if (!top || found.has(top) || !(top === base || top.startsWith(base + sep4))) return;
31166
31428
  found.add(top);
31167
31429
  const subdir = top === base ? "" : top.slice(base.length + 1);
31168
31430
  const remote = (await git(top, ["remote", "get-url", "origin"])).trim();
@@ -31173,13 +31435,13 @@ async function gitRepos(root) {
31173
31435
  const lvl1 = await readdir3(base, { withFileTypes: true });
31174
31436
  for (const d of lvl1) {
31175
31437
  if (!d.isDirectory() || d.name === ".git" || d.name === "node_modules") continue;
31176
- const p1 = resolve4(base, d.name);
31438
+ const p1 = resolve5(base, d.name);
31177
31439
  await add(p1);
31178
31440
  try {
31179
31441
  const lvl2 = await readdir3(p1, { withFileTypes: true });
31180
31442
  for (const d2 of lvl2) {
31181
31443
  if (!d2.isDirectory() || d2.name === ".git" || d2.name === "node_modules") continue;
31182
- await add(resolve4(p1, d2.name));
31444
+ await add(resolve5(p1, d2.name));
31183
31445
  }
31184
31446
  } catch {
31185
31447
  }
@@ -31246,24 +31508,24 @@ async function gitCommitDiff(root, repo, hash, path) {
31246
31508
  }
31247
31509
 
31248
31510
  // src/core/git-commit-identity.ts
31249
- import { execFile as execFile5 } from "child_process";
31250
- import { promisify as promisify5 } from "util";
31251
- var run5 = promisify5(execFile5);
31511
+ import { execFile as execFile6 } from "child_process";
31512
+ import { promisify as promisify6 } from "util";
31513
+ var run6 = promisify6(execFile6);
31252
31514
  async function setCommitAuthor(cwd2, machineUsername) {
31253
31515
  if (!machineUsername) return;
31254
31516
  try {
31255
- await run5("git", ["config", "--local", "user.name", machineUsername], { cwd: cwd2, timeout: 8e3 });
31517
+ await run6("git", ["config", "--local", "user.name", machineUsername], { cwd: cwd2, timeout: 8e3 });
31256
31518
  } catch {
31257
31519
  }
31258
31520
  }
31259
31521
 
31260
31522
  // src/core/scaffold.ts
31261
- import { cp, mkdir as mkdir2, writeFile } from "fs/promises";
31262
- import { execFile as execFile6 } from "child_process";
31263
- import { dirname as dirname4, join as join12, resolve as resolve5 } from "path";
31523
+ import { cp, mkdir as mkdir3, writeFile as writeFile3 } from "fs/promises";
31524
+ import { execFile as execFile7 } from "child_process";
31525
+ import { dirname as dirname5, join as join13, resolve as resolve6 } from "path";
31264
31526
  import { fileURLToPath as fileURLToPath3 } from "url";
31265
- import { promisify as promisify6 } from "util";
31266
- var run6 = promisify6(execFile6);
31527
+ import { promisify as promisify7 } from "util";
31528
+ var run7 = promisify7(execFile7);
31267
31529
  function reposOf(spec) {
31268
31530
  const repos = spec?.repos;
31269
31531
  return Array.isArray(repos) ? repos : [];
@@ -31277,46 +31539,46 @@ async function provisionRepos(target, repos, emit, opts = {}) {
31277
31539
  if (repo.primary) {
31278
31540
  if (opts.clonePrimary && repo.op === "existing" && repo.url) {
31279
31541
  emit("git", `Cloning ${repo.url}\u2026`);
31280
- await run6("git", ["clone", repo.url, target], { timeout: 12e4 });
31542
+ await run7("git", ["clone", repo.url, target], { timeout: 12e4 });
31281
31543
  continue;
31282
31544
  }
31283
31545
  emit("git", "Initializing the primary repo\u2026");
31284
- await run6("git", ["init"], { cwd: target, timeout: 2e4 });
31546
+ await run7("git", ["init"], { cwd: target, timeout: 2e4 });
31285
31547
  if (repo.op === "existing" && repo.url) {
31286
- await run6("git", ["remote", "add", "origin", repo.url], { cwd: target, timeout: 2e4 }).catch(
31548
+ await run7("git", ["remote", "add", "origin", repo.url], { cwd: target, timeout: 2e4 }).catch(
31287
31549
  () => void 0
31288
31550
  );
31289
31551
  }
31290
31552
  continue;
31291
31553
  }
31292
- const dir2 = join12(target, subDirName(repo));
31554
+ const dir2 = join13(target, subDirName(repo));
31293
31555
  if (repo.op === "existing" && repo.url) {
31294
31556
  emit("git", `Cloning sub-repo ${repo.url}\u2026`);
31295
- await run6("git", ["clone", repo.url, dir2], { timeout: 12e4 });
31557
+ await run7("git", ["clone", repo.url, dir2], { timeout: 12e4 });
31296
31558
  } else {
31297
31559
  emit("git", `Initializing sub-repo ${subDirName(repo)}\u2026`);
31298
- await mkdir2(dir2, { recursive: true });
31299
- await run6("git", ["init"], { cwd: dir2, timeout: 2e4 });
31560
+ await mkdir3(dir2, { recursive: true });
31561
+ await run7("git", ["init"], { cwd: dir2, timeout: 2e4 });
31300
31562
  }
31301
31563
  }
31302
31564
  }
31303
31565
  function sampleDir() {
31304
31566
  if (process.env.SCAFFOLD_SAMPLE_DIR) return process.env.SCAFFOLD_SAMPLE_DIR;
31305
- const here = dirname4(fileURLToPath3(import.meta.url));
31306
- return resolve5(here, "../../project-sample");
31567
+ const here = dirname5(fileURLToPath3(import.meta.url));
31568
+ return resolve6(here, "../../project-sample");
31307
31569
  }
31308
31570
  async function scaffoldProject(payload, profileDir2, onProgress) {
31309
31571
  const emit = (step, message) => onProgress?.({ projectId: payload.projectId, step, message });
31310
31572
  try {
31311
- const root = process.env.SCAFFOLD_ROOT ? resolve5(process.env.SCAFFOLD_ROOT) : resolve5(profileDir2);
31312
- const target = join12(root, payload.projectName);
31313
- await mkdir2(target, { recursive: true });
31573
+ const root = process.env.SCAFFOLD_ROOT ? resolve6(process.env.SCAFFOLD_ROOT) : resolve6(profileDir2);
31574
+ const target = join13(root, payload.projectName);
31575
+ await mkdir3(target, { recursive: true });
31314
31576
  emit("copy", "Copying the sample template\u2026");
31315
31577
  await cp(sampleDir(), target, { recursive: true, force: false, errorOnExist: false });
31316
31578
  if (payload.spec) {
31317
31579
  emit("spec", "Writing project.spec.json\u2026");
31318
- await writeFile(
31319
- join12(target, "project.spec.json"),
31580
+ await writeFile3(
31581
+ join13(target, "project.spec.json"),
31320
31582
  JSON.stringify(payload.spec, null, 2),
31321
31583
  "utf8"
31322
31584
  );
@@ -31334,7 +31596,7 @@ async function aiInit(target, spec, emit) {
31334
31596
  emit("ai-init", "AI init: subagents, README, CLAUDE.md\u2026");
31335
31597
  const subagents = Array.isArray(spec.subagents) ? spec.subagents : [];
31336
31598
  if (subagents.length > 0) {
31337
- await mkdir2(join12(target, ".claude", "agents"), { recursive: true });
31599
+ await mkdir3(join13(target, ".claude", "agents"), { recursive: true });
31338
31600
  for (const sa of subagents) {
31339
31601
  const name = (sa.name || "").trim().replace(/[^\w.-]+/g, "-").replace(/^-+|-+$/g, "");
31340
31602
  if (!name) continue;
@@ -31344,17 +31606,17 @@ name: ${name}
31344
31606
 
31345
31607
  ${(sa.description || "").trim()}
31346
31608
  `;
31347
- await writeFile(join12(target, ".claude", "agents", `${name}.md`), body, "utf8");
31609
+ await writeFile3(join13(target, ".claude", "agents", `${name}.md`), body, "utf8");
31348
31610
  }
31349
31611
  }
31350
31612
  const specJson = JSON.stringify(spec);
31351
31613
  await generateFile(
31352
- join12(target, "README.md"),
31614
+ join13(target, "README.md"),
31353
31615
  `Write a concise README.md (Markdown only, no preamble) for this project from its spec JSON:
31354
31616
  ${specJson}`
31355
31617
  );
31356
31618
  await generateFile(
31357
- join12(target, "CLAUDE.md"),
31619
+ join13(target, "CLAUDE.md"),
31358
31620
  `Write a CLAUDE.md (Markdown only, no preamble) with guidance/conventions for AI agents working in this project, derived from its spec JSON:
31359
31621
  ${specJson}`
31360
31622
  );
@@ -31362,16 +31624,16 @@ ${specJson}`
31362
31624
  async function generateFile(path, prompt) {
31363
31625
  try {
31364
31626
  const text = await aiGenerate(prompt);
31365
- if (text.trim()) await writeFile(path, text.trim() + "\n", "utf8");
31627
+ if (text.trim()) await writeFile3(path, text.trim() + "\n", "utf8");
31366
31628
  } catch {
31367
31629
  }
31368
31630
  }
31369
31631
  async function addProject(payload, profileDir2, onProgress) {
31370
31632
  const emit = (step, message) => onProgress?.({ projectId: payload.projectId, step, message });
31371
31633
  try {
31372
- const root = process.env.SCAFFOLD_ROOT ? resolve5(process.env.SCAFFOLD_ROOT) : resolve5(profileDir2);
31373
- const target = join12(root, payload.projectName);
31374
- await mkdir2(target, { recursive: true });
31634
+ const root = process.env.SCAFFOLD_ROOT ? resolve6(process.env.SCAFFOLD_ROOT) : resolve6(profileDir2);
31635
+ const target = join13(root, payload.projectName);
31636
+ await mkdir3(target, { recursive: true });
31375
31637
  const repos = payload.repos ?? [];
31376
31638
  if (repos.length > 0) await provisionRepos(target, repos, emit, { clonePrimary: true });
31377
31639
  onProgress?.({ projectId: payload.projectId, step: "done", message: "Project added.", done: true });
@@ -31381,6 +31643,40 @@ async function addProject(payload, profileDir2, onProgress) {
31381
31643
  }
31382
31644
  }
31383
31645
 
31646
+ // src/core/config-sync.ts
31647
+ import { existsSync as existsSync7, readFileSync as readFileSync13 } from "fs";
31648
+ import { join as join14 } from "path";
31649
+ var SERVER_MANAGED_KEYS = ["physicPath", "sessionSwitchPct", "perPromptTokenLimit"];
31650
+ function readConfigText(profileDir2) {
31651
+ const path = join14(profileDir2, "config.json");
31652
+ if (existsSync7(path)) {
31653
+ try {
31654
+ return readFileSync13(path, "utf8");
31655
+ } catch {
31656
+ }
31657
+ }
31658
+ return JSON.stringify(defaultProfileConfig(), null, 2);
31659
+ }
31660
+ function applyConfigText(profileDir2, text) {
31661
+ let parsed;
31662
+ try {
31663
+ parsed = JSON.parse(text);
31664
+ } catch (err) {
31665
+ return { ok: false, error: `Invalid JSON: ${err.message}` };
31666
+ }
31667
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
31668
+ return { ok: false, error: "config.json must be a JSON object." };
31669
+ }
31670
+ const next = { ...parsed };
31671
+ const current = readProfileConfig(profileDir2);
31672
+ for (const key of SERVER_MANAGED_KEYS) {
31673
+ if (current[key] !== void 0) next[key] = current[key];
31674
+ else delete next[key];
31675
+ }
31676
+ overwriteProfileConfig(profileDir2, next);
31677
+ return { ok: true };
31678
+ }
31679
+
31384
31680
  // src/core/ws-client.ts
31385
31681
  var HEARTBEAT_MS = 3e4;
31386
31682
  var USAGE_POLL_MS = 5 * 6e4;
@@ -31397,6 +31693,11 @@ function envReconnectMaxSec() {
31397
31693
  var MAX_HASHCODE_INVALID_RETRIES = 5;
31398
31694
  var CLOSE_DRAINING = 4001;
31399
31695
  var OFFLINE_BUFFER_MAX_BYTES = 512 * 1024;
31696
+ var CONSOLE_SNAPSHOT_INTERVAL_MS = 3e4;
31697
+ var CONSOLE_WATCH_LEASE_MS = 45e3;
31698
+ function toDtoEntry(e) {
31699
+ return { id: e.id, source: e.source, kind: e.kind, text: e.text, level: e.level, resultKind: e.resultKind };
31700
+ }
31400
31701
  var WsClient = class {
31401
31702
  constructor(context) {
31402
31703
  this.context = context;
@@ -31408,6 +31709,11 @@ var WsClient = class {
31408
31709
  );
31409
31710
  context.bus.onLocalSubmit((input) => void this.runLocalCommand(input));
31410
31711
  context.bus.onReconnect(() => this.reconnectNow());
31712
+ context.bus.onTranscript((entry) => this.emitConsole({ kind: "add", entry: toDtoEntry(entry) }));
31713
+ context.bus.onTranscriptUpdate(
31714
+ (entry) => this.emitConsole({ kind: "update", entry: toDtoEntry(entry) })
31715
+ );
31716
+ context.bus.onClear(() => this.emitConsole({ kind: "clear" }));
31411
31717
  }
31412
31718
  context;
31413
31719
  socket = null;
@@ -31454,6 +31760,18 @@ var WsClient = class {
31454
31760
  */
31455
31761
  handledCommands = /* @__PURE__ */ new Set();
31456
31762
  offlineBufferBytes = 0;
31763
+ /**
31764
+ * Console-sync state (ADR-0150). `consoleWatching` is set by `console.watch` from the server
31765
+ * (a web Console viewer is attached); only then does the cli emit `console.sync`. `consoleRev`
31766
+ * is the monotonic revision the web uses to detect a gap; `consoleSnapshotTimer` periodically
31767
+ * re-sends a full snapshot to self-heal. Watching resets to false on each reconnect (the server
31768
+ * re-asserts it), so a fresh session re-snapshots.
31769
+ */
31770
+ consoleWatching = false;
31771
+ consoleRev = 0;
31772
+ consoleSnapshotTimer = null;
31773
+ /** Lease expiry: stop syncing if the server does not renew `console.watch` in time. */
31774
+ consoleWatchExpiry = null;
31457
31775
  /** cli → server requests awaiting a reply (quota.check — ADR-0020). */
31458
31776
  pending = /* @__PURE__ */ new Map();
31459
31777
  /** Daily scheduled cli auto-update (ADR-0074); policy fed from each ws_token. */
@@ -31487,14 +31805,14 @@ var WsClient = class {
31487
31805
  */
31488
31806
  awaitConnected(timeoutMs) {
31489
31807
  if (this.sessionKey && this.bus.status === "connected") return Promise.resolve(true);
31490
- return new Promise((resolve6) => {
31808
+ return new Promise((resolve7) => {
31491
31809
  const waiter = () => {
31492
31810
  clearTimeout(timer);
31493
- resolve6(true);
31811
+ resolve7(true);
31494
31812
  };
31495
31813
  const timer = setTimeout(() => {
31496
31814
  this.connectWaiters = this.connectWaiters.filter((w) => w !== waiter);
31497
- resolve6(false);
31815
+ resolve7(false);
31498
31816
  }, timeoutMs);
31499
31817
  this.connectWaiters.push(waiter);
31500
31818
  });
@@ -31521,16 +31839,16 @@ var WsClient = class {
31521
31839
  this.forceReconnect = false;
31522
31840
  return Promise.resolve();
31523
31841
  }
31524
- return new Promise((resolve6) => {
31842
+ return new Promise((resolve7) => {
31525
31843
  const timer = setTimeout(() => {
31526
31844
  this.wakeReconnect = null;
31527
- resolve6();
31845
+ resolve7();
31528
31846
  }, ms);
31529
31847
  this.wakeReconnect = () => {
31530
31848
  clearTimeout(timer);
31531
31849
  this.wakeReconnect = null;
31532
31850
  this.forceReconnect = false;
31533
- resolve6();
31851
+ resolve7();
31534
31852
  };
31535
31853
  });
31536
31854
  }
@@ -31586,7 +31904,7 @@ var WsClient = class {
31586
31904
  * One WS session: token → connect → handshake → handle messages until close.
31587
31905
  */
31588
31906
  connectOnce() {
31589
- return new Promise((resolve6, reject) => {
31907
+ return new Promise((resolve7, reject) => {
31590
31908
  void (async () => {
31591
31909
  this.sessionEstablished = false;
31592
31910
  this.bus.setStatus("connecting");
@@ -31655,7 +31973,7 @@ var WsClient = class {
31655
31973
  if (code === CLOSE_DRAINING) {
31656
31974
  this.bus.log("Server draining \u2014 will reconnect (backoff + jitter).");
31657
31975
  }
31658
- resolve6();
31976
+ resolve7();
31659
31977
  });
31660
31978
  socket.on("error", (err) => {
31661
31979
  this.clearHeartbeat();
@@ -31682,6 +32000,7 @@ var WsClient = class {
31682
32000
  this.physicRoot = null;
31683
32001
  }
31684
32002
  logger.info("ws.connected", { version: CLI_VERSION });
32003
+ this.stopConsoleSync();
31685
32004
  this.bus.setStatus("connected");
31686
32005
  this.bus.log("\u2714 Connected to the server (session encryption ready).");
31687
32006
  this.sendMachineStatus();
@@ -31746,8 +32065,12 @@ var WsClient = class {
31746
32065
  switch (message.channel) {
31747
32066
  case WsChannels.PHYSIC_SYNC: {
31748
32067
  const sync = payload;
31749
- if (sync.oldName) rmSync4(join13(this.context.profileDir, sync.oldName), { recursive: true, force: true });
31750
- mkdirSync4(join13(this.context.profileDir, sync.newName), { recursive: true });
32068
+ if (sync.oldName) {
32069
+ const oldRoot = this.physicFolderPath(sync.oldName);
32070
+ if (oldRoot) void uninstallCron(oldRoot).catch(() => void 0);
32071
+ }
32072
+ if (sync.oldName) rmSync4(join15(this.context.profileDir, sync.oldName), { recursive: true, force: true });
32073
+ mkdirSync4(join15(this.context.profileDir, sync.newName), { recursive: true });
31751
32074
  this.physicRoot = this.physicFolderPath(sync.newName);
31752
32075
  this.bus.setProject(sync.newName);
31753
32076
  this.bus.log(`Physic project folder synced \u2192 ${sync.newName}`);
@@ -31756,7 +32079,9 @@ var WsClient = class {
31756
32079
  case WsChannels.PHYSIC_DELETE: {
31757
32080
  const del = payload;
31758
32081
  if (del.name) {
31759
- rmSync4(join13(this.context.profileDir, del.name), { recursive: true, force: true });
32082
+ const delRoot = this.physicFolderPath(del.name);
32083
+ if (delRoot) void uninstallCron(delRoot).catch(() => void 0);
32084
+ rmSync4(join15(this.context.profileDir, del.name), { recursive: true, force: true });
31760
32085
  this.bus.log(`Physic project folder deleted (project removed) \u2192 ${del.name}`);
31761
32086
  }
31762
32087
  this.bus.setProject(null);
@@ -31821,6 +32146,53 @@ var WsClient = class {
31821
32146
  (reply) => this.send(WsChannels.FS_READ, reply, message.id)
31822
32147
  );
31823
32148
  break;
32149
+ case WsChannels.FS_WRITE: {
32150
+ const req = payload;
32151
+ void writeWorkerFile(this.physicRoot, req.path, req.content).then(
32152
+ (reply) => this.send(WsChannels.FS_WRITE, reply, message.id)
32153
+ );
32154
+ break;
32155
+ }
32156
+ case WsChannels.AUTONOMOUS_READ:
32157
+ if (this.physicRoot) {
32158
+ void readAutonomous(this.physicRoot).then(
32159
+ (reply) => this.send(WsChannels.AUTONOMOUS_READ, reply, message.id)
32160
+ );
32161
+ }
32162
+ break;
32163
+ case WsChannels.AUTONOMOUS_LOGS: {
32164
+ const req = payload;
32165
+ if (this.physicRoot) {
32166
+ void readAutonomousLogs(this.physicRoot, req.date).then(
32167
+ (reply) => this.send(WsChannels.AUTONOMOUS_LOGS, reply, message.id)
32168
+ );
32169
+ }
32170
+ break;
32171
+ }
32172
+ case WsChannels.AUTONOMOUS_WRITE: {
32173
+ const req = payload;
32174
+ if (this.physicRoot) {
32175
+ void writeAutonomous(this.physicRoot, req, req.by ?? "unknown").then(
32176
+ (reply) => this.send(WsChannels.AUTONOMOUS_WRITE, reply, message.id)
32177
+ );
32178
+ }
32179
+ break;
32180
+ }
32181
+ case WsChannels.CONFIG_READ:
32182
+ this.send(
32183
+ WsChannels.CONFIG_READ,
32184
+ { config: readConfigText(this.context.profileDir) },
32185
+ message.id
32186
+ );
32187
+ break;
32188
+ case WsChannels.CONFIG_WRITE: {
32189
+ const res = applyConfigText(
32190
+ this.context.profileDir,
32191
+ payload.config
32192
+ );
32193
+ this.send(WsChannels.CONFIG_WRITE, res, message.id);
32194
+ break;
32195
+ }
31824
32196
  case WsChannels.GIT_DIFF:
31825
32197
  void gitDiff(payload.path).then(
31826
32198
  (reply) => this.send(WsChannels.GIT_DIFF, reply, message.id)
@@ -31859,7 +32231,7 @@ var WsClient = class {
31859
32231
  }
31860
32232
  case WsChannels.LOG_READ: {
31861
32233
  const limit = Math.min(Math.max(payload.limit ?? 200, 1), 2e3);
31862
- const lines = readRecentLogLines(join13(this.context.profileDir, "logs"), limit);
32234
+ const lines = readRecentLogLines(join15(this.context.profileDir, "logs"), limit);
31863
32235
  this.send(WsChannels.LOG_READ, { lines }, message.id);
31864
32236
  break;
31865
32237
  }
@@ -31889,6 +32261,9 @@ var WsClient = class {
31889
32261
  (reply) => this.send(WsChannels.REVIEW_EVALUATE, reply, message.id)
31890
32262
  );
31891
32263
  break;
32264
+ case WsChannels.CONSOLE_WATCH:
32265
+ this.setConsoleWatching(payload.on);
32266
+ break;
31892
32267
  default:
31893
32268
  this.bus.log(`Unsupported channel: ${message.channel}`, "warn");
31894
32269
  }
@@ -31933,7 +32308,7 @@ var WsClient = class {
31933
32308
  if (perPromptLimit > 0) {
31934
32309
  const estimated = estimateTokens(prompt);
31935
32310
  if (estimated > perPromptLimit) {
31936
- this.rejectPromptOverLimit(commandId, origin, estimated, perPromptLimit);
32311
+ this.rejectPromptOverLimit(commandId, origin, prompt, estimated, perPromptLimit);
31937
32312
  return;
31938
32313
  }
31939
32314
  }
@@ -31948,7 +32323,7 @@ var WsClient = class {
31948
32323
  verdict = { commandId, ok: false, reasons: ["unavailable"] };
31949
32324
  }
31950
32325
  if (!verdict.ok) {
31951
- this.rejectByReview(commandId, origin, verdict.reasons);
32326
+ this.rejectByReview(commandId, origin, prompt, verdict.reasons);
31952
32327
  return;
31953
32328
  }
31954
32329
  }
@@ -31978,6 +32353,8 @@ var WsClient = class {
31978
32353
  commandId,
31979
32354
  cmd: plan.cmd,
31980
32355
  args: markerArgs,
32356
+ // The raw prompt so the web console echoes it like this TUI (ADR-0108), not "claude".
32357
+ prompt,
31981
32358
  origin: "local"
31982
32359
  });
31983
32360
  }
@@ -32017,17 +32394,18 @@ var WsClient = class {
32017
32394
  this.bus.push({ source: origin, kind: "out", text: resultBuf });
32018
32395
  }
32019
32396
  },
32020
- onAttemptStart: (label, index, total) => this.bus.push({
32021
- source: origin,
32022
- kind: "log",
32023
- text: `\u2192 trying ${plan.cmd} profile "${label}" (${index + 1}/${total})\u2026`
32024
- }),
32025
- onAttemptFail: (label, reason) => this.bus.push({
32026
- source: origin,
32027
- kind: "log",
32028
- text: reason === "limit" ? `profile "${label}" hit its session limit \u2014 trying next` : `profile "${label}" failed to authenticate \u2014 trying next`,
32029
- level: "warn"
32030
- })
32397
+ onAttemptStart: (label, index, total) => {
32398
+ const text = `\u2192 trying ${plan.cmd} profile "${label}" (${index + 1}/${total})\u2026`;
32399
+ this.bus.push({ source: origin, kind: "log", text });
32400
+ this.send(WsChannels.COMMAND_OUTPUT, { commandId, seq: seq++, chunk: `${text}
32401
+ `, log: true });
32402
+ },
32403
+ onAttemptFail: (label, reason) => {
32404
+ const text = reason === "limit" ? `profile "${label}" hit its session limit \u2014 trying next` : `profile "${label}" failed to authenticate \u2014 trying next`;
32405
+ this.bus.push({ source: origin, kind: "log", text, level: "warn" });
32406
+ this.send(WsChannels.COMMAND_OUTPUT, { commandId, seq: seq++, chunk: `${text}
32407
+ `, log: true });
32408
+ }
32031
32409
  });
32032
32410
  this.bus.endBusy(plan.cmd);
32033
32411
  const profileLabel = result.workedDir ? profileDisplayLabel(result.workedDir) : null;
@@ -32050,6 +32428,15 @@ var WsClient = class {
32050
32428
  status: result.exitCode === 0 ? "done" : "failed",
32051
32429
  exitCode: result.exitCode,
32052
32430
  tokens: result.usage.tokens,
32431
+ // The ai_tokens split for this run (ADR-0145); attached only when we metered real tokens.
32432
+ ...result.usage.tokens > 0 && {
32433
+ tokensBreakdown: {
32434
+ input: result.usage.input,
32435
+ output: result.usage.output,
32436
+ cacheRead: result.usage.cacheRead,
32437
+ cacheCreation: result.usage.cacheCreation
32438
+ }
32439
+ },
32053
32440
  startedAt,
32054
32441
  finishedAt: (/* @__PURE__ */ new Date()).toISOString()
32055
32442
  });
@@ -32058,7 +32445,16 @@ var WsClient = class {
32058
32445
  { metric: UsageMetric.COMMANDS, amount: 1, occurredAt }
32059
32446
  ];
32060
32447
  if (result.exitCode === 0 && result.usage.tokens > 0) {
32061
- events.push({ metric: UsageMetric.AI_TOKENS, amount: result.usage.tokens, occurredAt });
32448
+ events.push({
32449
+ metric: UsageMetric.AI_TOKENS,
32450
+ amount: result.usage.tokens,
32451
+ occurredAt,
32452
+ // The ai_tokens split so the server can persist + display it (ADR-0145).
32453
+ inputTokens: result.usage.input,
32454
+ outputTokens: result.usage.output,
32455
+ cacheReadTokens: result.usage.cacheRead,
32456
+ cacheCreationTokens: result.usage.cacheCreation
32457
+ });
32062
32458
  this.bus.addTokens?.(result.usage.tokens);
32063
32459
  }
32064
32460
  this.reportUsage(events, profileLabel);
@@ -32093,7 +32489,7 @@ var WsClient = class {
32093
32489
  * without spawning: surface it in the transcript + close the command on the server with a
32094
32490
  * PROMPT_TOKEN_LIMIT_EXCEEDED note (exit 1). No usage is metered (nothing ran).
32095
32491
  */
32096
- rejectPromptOverLimit(commandId, origin, estimated, limit) {
32492
+ rejectPromptOverLimit(commandId, origin, prompt, estimated, limit) {
32097
32493
  const message = `[PROMPT_TOKEN_LIMIT_EXCEEDED] Prompt (~${estimated} tokens) exceeds the project per-prompt limit of ${limit} tokens.`;
32098
32494
  logger.warn("command.ai.prompt-limit", { commandId, origin, estimated, limit });
32099
32495
  this.bus.push({ source: origin, kind: "log", text: message, level: "error" });
@@ -32102,6 +32498,7 @@ var WsClient = class {
32102
32498
  commandId,
32103
32499
  cmd: "claude",
32104
32500
  args: [],
32501
+ prompt,
32105
32502
  origin: "local"
32106
32503
  });
32107
32504
  }
@@ -32119,7 +32516,7 @@ var WsClient = class {
32119
32516
  * verdict + close the command on the server. `reasons` contains only violation categories
32120
32517
  * (never secret values); a "unavailable" reason maps to no-outbound-available.
32121
32518
  */
32122
- rejectByReview(commandId, origin, reasons) {
32519
+ rejectByReview(commandId, origin, prompt, reasons) {
32123
32520
  const unavailable = reasons.includes("unavailable");
32124
32521
  const code = unavailable ? "OUTBOUND_REVIEW_UNAVAILABLE" : "OUTBOUND_REVIEW_REJECTED";
32125
32522
  const detail = reasons.filter((r) => r !== "unavailable").join(", ");
@@ -32131,6 +32528,7 @@ var WsClient = class {
32131
32528
  commandId,
32132
32529
  cmd: "claude",
32133
32530
  args: [],
32531
+ prompt,
32134
32532
  origin: "local"
32135
32533
  });
32136
32534
  }
@@ -32164,12 +32562,65 @@ var WsClient = class {
32164
32562
  };
32165
32563
  this.socket.send(JSON.stringify(envelope));
32166
32564
  }
32565
+ /**
32566
+ * Console sync (ADR-0150): emit one incremental `console.sync` event (add/update/clear) — only
32567
+ * while a web Console viewer is attached (`consoleWatching`); each carries the next `rev` so the
32568
+ * web detects a gap. Dropped when disconnected — the next snapshot on (re)attach re-syncs.
32569
+ */
32570
+ emitConsole(ev) {
32571
+ if (!this.consoleWatching) return;
32572
+ this.send(WsChannels.CONSOLE_SYNC, { ...ev, rev: ++this.consoleRev });
32573
+ }
32574
+ /** Send a full transcript snapshot (on attach + the periodic self-heal — ADR-0150). */
32575
+ sendConsoleSnapshot() {
32576
+ const entries2 = this.bus.snapshot().map(toDtoEntry);
32577
+ this.send(WsChannels.CONSOLE_SYNC, {
32578
+ kind: "snapshot",
32579
+ rev: ++this.consoleRev,
32580
+ entries: entries2
32581
+ });
32582
+ }
32583
+ /**
32584
+ * Handle `console.watch` (ADR-0150) — a renewable lease. `on` renews it: start mirroring on the
32585
+ * first assertion (fresh snapshot + arm the periodic snapshot); later renewals just extend the
32586
+ * lease (no re-snapshot). No renewal within `CONSOLE_WATCH_LEASE_MS` ⇒ the lease expires and
32587
+ * syncing stops. An explicit `on:false` stops immediately.
32588
+ */
32589
+ setConsoleWatching(on) {
32590
+ if (!on) {
32591
+ this.stopConsoleSync();
32592
+ return;
32593
+ }
32594
+ if (this.consoleWatchExpiry) clearTimeout(this.consoleWatchExpiry);
32595
+ this.consoleWatchExpiry = setTimeout(() => this.stopConsoleSync(), CONSOLE_WATCH_LEASE_MS);
32596
+ this.consoleWatchExpiry.unref?.();
32597
+ if (this.consoleWatching) return;
32598
+ this.consoleWatching = true;
32599
+ this.sendConsoleSnapshot();
32600
+ this.consoleSnapshotTimer = setInterval(
32601
+ () => this.sendConsoleSnapshot(),
32602
+ CONSOLE_SNAPSHOT_INTERVAL_MS
32603
+ );
32604
+ this.consoleSnapshotTimer.unref?.();
32605
+ }
32606
+ /** Stop mirroring the transcript (lease expired / viewer left / reconnect) — clears both timers. */
32607
+ stopConsoleSync() {
32608
+ this.consoleWatching = false;
32609
+ if (this.consoleSnapshotTimer) {
32610
+ clearInterval(this.consoleSnapshotTimer);
32611
+ this.consoleSnapshotTimer = null;
32612
+ }
32613
+ if (this.consoleWatchExpiry) {
32614
+ clearTimeout(this.consoleWatchExpiry);
32615
+ this.consoleWatchExpiry = null;
32616
+ }
32617
+ }
32167
32618
  /**
32168
32619
  * Send a cli → server request and await the reply by envelope id (15s timeout).
32169
32620
  * Used for quota.check before spawning an AI cli (ADR-0020).
32170
32621
  */
32171
32622
  request(channel, data) {
32172
- return new Promise((resolve6, reject) => {
32623
+ return new Promise((resolve7, reject) => {
32173
32624
  if (!this.socket || !this.sessionKey) {
32174
32625
  reject(new Error("Not connected to the server."));
32175
32626
  return;
@@ -32189,7 +32640,7 @@ var WsClient = class {
32189
32640
  }, 15e3);
32190
32641
  timer.unref?.();
32191
32642
  this.pending.set(envelope.id, {
32192
- resolve: (value) => resolve6(value),
32643
+ resolve: (value) => resolve7(value),
32193
32644
  timer
32194
32645
  });
32195
32646
  this.socket.send(JSON.stringify(envelope));
@@ -32236,7 +32687,7 @@ var WsClient = class {
32236
32687
  /** Create the physic folder at an absolute path if missing (best-effort). */
32237
32688
  ensurePhysicFolderPath(folder) {
32238
32689
  try {
32239
- if (!existsSync7(folder)) {
32690
+ if (!existsSync8(folder)) {
32240
32691
  mkdirSync4(folder, { recursive: true });
32241
32692
  this.bus.log(`Created physic project folder: ${folder}`);
32242
32693
  }
@@ -32251,7 +32702,7 @@ var WsClient = class {
32251
32702
  */
32252
32703
  physicFolderPath(projectName) {
32253
32704
  const safe = projectName.replace(/[/\\]/g, "_").replace(/\.\./g, "_").trim();
32254
- return safe ? join13(this.context.profileDir, safe) : null;
32705
+ return safe ? join15(this.context.profileDir, safe) : null;
32255
32706
  }
32256
32707
  /**
32257
32708
  * Report machine.status: version + the state of the physic project being served
@@ -32265,8 +32716,9 @@ var WsClient = class {
32265
32716
  projects: [],
32266
32717
  version: CLI_VERSION,
32267
32718
  physicPath,
32268
- physicPathExists: physicPath ? existsSync7(physicPath) : void 0,
32269
- autonomousRunning: false
32719
+ physicPathExists: physicPath ? existsSync8(physicPath) : void 0,
32720
+ // Real coarse state (ADR-0152): !paused and a recent tick (was hard-coded false).
32721
+ autonomousRunning: physicPath ? isAutonomousRunning(physicPath) : false
32270
32722
  });
32271
32723
  }
32272
32724
  /** Heartbeat every 30s: ping + periodically re-report machine.status. */
@@ -32336,7 +32788,7 @@ var WsClient = class {
32336
32788
  */
32337
32789
  uploadLog() {
32338
32790
  try {
32339
- const upload = readLogUpload(join13(this.context.profileDir, "logs"));
32791
+ const upload = readLogUpload(join15(this.context.profileDir, "logs"));
32340
32792
  if (!upload) return;
32341
32793
  const sig = `${upload.fileName}:${upload.totalBytes}`;
32342
32794
  if (sig === this.lastLogSig) return;
@@ -32364,10 +32816,10 @@ var WsClient = class {
32364
32816
  };
32365
32817
 
32366
32818
  // src/core/instance-lock.ts
32367
- import { readFileSync as readFileSync12, rmSync as rmSync5, writeFileSync as writeFileSync6 } from "fs";
32368
- import { join as join14 } from "path";
32819
+ import { readFileSync as readFileSync14, rmSync as rmSync5, writeFileSync as writeFileSync6 } from "fs";
32820
+ import { join as join16 } from "path";
32369
32821
  function lockPath(profileDir2) {
32370
- return join14(profileDir2, "start.lock");
32822
+ return join16(profileDir2, "start.lock");
32371
32823
  }
32372
32824
  function isAlive(pid) {
32373
32825
  try {
@@ -32379,7 +32831,7 @@ function isAlive(pid) {
32379
32831
  }
32380
32832
  function lockHolder(profileDir2) {
32381
32833
  try {
32382
- const pid = Number.parseInt(readFileSync12(lockPath(profileDir2), "utf8").trim(), 10);
32834
+ const pid = Number.parseInt(readFileSync14(lockPath(profileDir2), "utf8").trim(), 10);
32383
32835
  if (Number.isInteger(pid) && pid > 0 && isAlive(pid)) return pid;
32384
32836
  } catch {
32385
32837
  }
@@ -32392,7 +32844,7 @@ function acquireInstanceLock(profileDir2) {
32392
32844
  }
32393
32845
  function releaseInstanceLock(profileDir2) {
32394
32846
  try {
32395
- const pid = Number.parseInt(readFileSync12(lockPath(profileDir2), "utf8").trim(), 10);
32847
+ const pid = Number.parseInt(readFileSync14(lockPath(profileDir2), "utf8").trim(), 10);
32396
32848
  if (pid === process.pid) rmSync5(lockPath(profileDir2), { force: true });
32397
32849
  } catch {
32398
32850
  }
@@ -32441,6 +32893,24 @@ var SessionBus = class {
32441
32893
  this.emitter.emit("transcript", entry);
32442
32894
  return entry.id;
32443
32895
  }
32896
+ /**
32897
+ * Wipe the transcript (`/clear` + idle auto-clear — ADR-0057): empty the replay history and
32898
+ * notify consumers so the TUI clears the screen and the console-sync emitter (ADR-0150) sends
32899
+ * a `clear` event. The current snapshot afterwards is empty (until new lines arrive).
32900
+ */
32901
+ clear() {
32902
+ this.history.length = 0;
32903
+ this.emitter.emit("clear");
32904
+ }
32905
+ /** Subscribe to transcript clears (TUI screen wipe + console.sync `clear` — ADR-0150). */
32906
+ onClear(fn) {
32907
+ this.emitter.on("clear", fn);
32908
+ return () => this.emitter.off("clear", fn);
32909
+ }
32910
+ /** The current transcript entries (a copy) — the console-sync snapshot source (ADR-0150). */
32911
+ snapshot() {
32912
+ return [...this.history];
32913
+ }
32444
32914
  /**
32445
32915
  * Producer → consumers: replace the text of an already-pushed entry (ADR-0108) — used to
32446
32916
  * grow a streaming `result` block in place without pushing a new line per chunk. No-op if the
@@ -37876,8 +38346,8 @@ var Ink = class {
37876
38346
  }
37877
38347
  }
37878
38348
  async waitUntilExit() {
37879
- this.exitPromise ||= new Promise((resolve6, reject) => {
37880
- this.resolveExitPromise = resolve6;
38349
+ this.exitPromise ||= new Promise((resolve7, reject) => {
38350
+ this.resolveExitPromise = resolve7;
37881
38351
  this.rejectExitPromise = reject;
37882
38352
  });
37883
38353
  return this.exitPromise;
@@ -38244,17 +38714,17 @@ var import_react21 = __toESM(require_react(), 1);
38244
38714
  var import_react26 = __toESM(require_react(), 1);
38245
38715
 
38246
38716
  // src/core/input-history.ts
38247
- import { existsSync as existsSync9, readFileSync as readFileSync14, writeFileSync as writeFileSync7 } from "fs";
38248
- import { join as join15 } from "path";
38717
+ import { existsSync as existsSync10, readFileSync as readFileSync16, writeFileSync as writeFileSync7 } from "fs";
38718
+ import { join as join17 } from "path";
38249
38719
  var MAX_HISTORY = 200;
38250
38720
  function historyPath(profileDir2) {
38251
- return join15(profileDir2, "input-history.json");
38721
+ return join17(profileDir2, "input-history.json");
38252
38722
  }
38253
38723
  function loadInputHistory(profileDir2) {
38254
38724
  const path = historyPath(profileDir2);
38255
- if (!existsSync9(path)) return [];
38725
+ if (!existsSync10(path)) return [];
38256
38726
  try {
38257
- const data = JSON.parse(readFileSync14(path, "utf8"));
38727
+ const data = JSON.parse(readFileSync16(path, "utf8"));
38258
38728
  return Array.isArray(data) ? data.filter((x) => typeof x === "string") : [];
38259
38729
  } catch {
38260
38730
  return [];
@@ -38514,8 +38984,8 @@ function TranscriptLine({ line }) {
38514
38984
  var import_react24 = __toESM(require_react(), 1);
38515
38985
 
38516
38986
  // src/ui/slash-commands.ts
38517
- import { existsSync as existsSync10 } from "fs";
38518
- import { join as join16 } from "path";
38987
+ import { existsSync as existsSync11 } from "fs";
38988
+ import { join as join18 } from "path";
38519
38989
  var CONFIG_KEYS = [
38520
38990
  "aiCli",
38521
38991
  "claudeHome",
@@ -38527,7 +38997,7 @@ var CONFIG_KEYS = [
38527
38997
  "aiEnv"
38528
38998
  ];
38529
38999
  function configPath(profileDir2) {
38530
- return join16(profileDir2, "config.json");
39000
+ return join18(profileDir2, "config.json");
38531
39001
  }
38532
39002
  function parseConfigAssignment(key, valueTokens) {
38533
39003
  const value = valueTokens.join(" ").trim();
@@ -38577,7 +39047,7 @@ function runConfig(ctx) {
38577
39047
  ctx.print(`\u2714 ${verb} default config: ${path}`);
38578
39048
  ctx.print("add claude profiles: /config set claudeHome .claude .claude-1 (tried in order)");
38579
39049
  };
38580
- if (existsSync10(path)) {
39050
+ if (existsSync11(path)) {
38581
39051
  ctx.confirm(
38582
39052
  `config exists: ${path} \u2014 replace it with defaults? (y/N)`,
38583
39053
  () => writeDefaults("replaced with")
@@ -38662,7 +39132,7 @@ function runOutput(ctx) {
38662
39132
  }
38663
39133
  function runLogs(ctx) {
38664
39134
  const n = Math.max(1, Math.min(200, Number(ctx.args[0]) || 20));
38665
- const dir2 = join16(ctx.info.profileDir, "logs");
39135
+ const dir2 = join18(ctx.info.profileDir, "logs");
38666
39136
  const lines = readRecentLogLines(dir2, n);
38667
39137
  if (lines.length === 0) {
38668
39138
  ctx.print("No log lines yet.");
@@ -38986,11 +39456,29 @@ function App2({
38986
39456
  const [history, setHistory] = (0, import_react26.useState)(() => loadInputHistory(info.profileDir));
38987
39457
  const [startedAt] = (0, import_react26.useState)(() => Date.now());
38988
39458
  const [pendingConfirm, setPendingConfirm] = (0, import_react26.useState)(null);
39459
+ const [autoClearIdleMs] = (0, import_react26.useState)(() => {
39460
+ const min = readProfileConfig(info.profileDir).autoClearIdleMinutes ?? 10;
39461
+ return min > 0 ? min * 6e4 : 0;
39462
+ });
39463
+ const lastActivityRef = (0, import_react26.useRef)(startedAt);
39464
+ const busyRef = (0, import_react26.useRef)(busy);
39465
+ busyRef.current = busy;
39466
+ const entriesLenRef = (0, import_react26.useRef)(entries2.length);
39467
+ entriesLenRef.current = entries2.length;
39468
+ const pendingConfirmRef = (0, import_react26.useRef)(pendingConfirm);
39469
+ pendingConfirmRef.current = pendingConfirm;
39470
+ const clearTranscript = (0, import_react26.useCallback)(() => {
39471
+ setEntries([]);
39472
+ setScrollBack(0);
39473
+ stdout.write("\x1B[2J\x1B[3J\x1B[H");
39474
+ bus.clear();
39475
+ }, [stdout, bus]);
38989
39476
  (0, import_react26.useEffect)(() => {
38990
39477
  const offTranscript = bus.onTranscript((entry) => {
38991
39478
  setEntries(
38992
39479
  (prev) => prev.length >= ENTRY_CAP ? [...prev.slice(prev.length - ENTRY_CAP + 1), entry] : [...prev, entry]
38993
39480
  );
39481
+ if (entry.source !== "system") lastActivityRef.current = Date.now();
38994
39482
  if (entry.kind === "aireq" || entry.kind === "aires") setExpanded(/* @__PURE__ */ new Set());
38995
39483
  });
38996
39484
  const offUpdate = bus.onTranscriptUpdate(
@@ -39017,6 +39505,21 @@ function App2({
39017
39505
  offTokens();
39018
39506
  };
39019
39507
  }, [bus]);
39508
+ (0, import_react26.useEffect)(() => {
39509
+ if (autoClearIdleMs <= 0) return;
39510
+ const timer = setInterval(() => {
39511
+ if (busyRef.current !== null || pendingConfirmRef.current || entriesLenRef.current === 0) return;
39512
+ if (Date.now() - lastActivityRef.current < autoClearIdleMs) return;
39513
+ clearTranscript();
39514
+ bus.push({
39515
+ source: "system",
39516
+ kind: "log",
39517
+ text: `Transcript auto-cleared after ${Math.round(autoClearIdleMs / 6e4)} min idle.`
39518
+ });
39519
+ lastActivityRef.current = Date.now();
39520
+ }, 6e4);
39521
+ return () => clearInterval(timer);
39522
+ }, [autoClearIdleMs, bus, clearTranscript]);
39020
39523
  const bodyHeight = Math.max(3, rows - CHROME_ROWS);
39021
39524
  const textWidth = Math.max(10, cols - 1 - 8);
39022
39525
  const lines = (0, import_react26.useMemo)(() => flattenEntries(entries2, textWidth, expanded), [entries2, textWidth, expanded]);
@@ -39042,6 +39545,7 @@ function App2({
39042
39545
  };
39043
39546
  const handleSubmit = (input) => {
39044
39547
  setScrollBack(0);
39548
+ lastActivityRef.current = Date.now();
39045
39549
  const trimmed = input.trim();
39046
39550
  if (pendingConfirm) {
39047
39551
  const { onYes } = pendingConfirm;
@@ -39057,11 +39561,7 @@ function App2({
39057
39561
  runSlashCommand(trimmed, {
39058
39562
  info,
39059
39563
  print: (text, level) => bus.push({ source: "local", kind: "log", text, level }),
39060
- clear: () => {
39061
- setEntries([]);
39062
- setScrollBack(0);
39063
- stdout.write("\x1B[2J\x1B[3J\x1B[H");
39064
- },
39564
+ clear: clearTranscript,
39065
39565
  quit: () => exit(),
39066
39566
  confirm: (prompt, onYes) => {
39067
39567
  bus.push({ source: "local", kind: "log", text: prompt });
@@ -39133,7 +39633,7 @@ async function runStart(profileDir2, profileName) {
39133
39633
  return;
39134
39634
  }
39135
39635
  logger.configure({
39136
- dir: join17(profileDir2, "logs"),
39636
+ dir: join19(profileDir2, "logs"),
39137
39637
  level: resolveLogLevel(),
39138
39638
  base: { version: CLI_VERSION, profile: profileName, scope: credential.scope }
39139
39639
  });
@@ -39153,7 +39653,7 @@ async function runStart(profileDir2, profileName) {
39153
39653
  if (result.action === "updated") {
39154
39654
  logger.info("update.updated", { version: result.version });
39155
39655
  console.log("Restarting with the new version\u2026");
39156
- const child = spawn4(process.execPath, process.argv.slice(1), {
39656
+ const child = spawn5(process.execPath, process.argv.slice(1), {
39157
39657
  stdio: "inherit",
39158
39658
  env: { ...process.env, FOURPM_NO_UPDATE: "1" }
39159
39659
  });
@@ -39191,7 +39691,7 @@ async function runStart(profileDir2, profileName) {
39191
39691
  logger.error("ws.run.crash", { message: e.message, stack: e.stack });
39192
39692
  if (client.isStopped) return;
39193
39693
  bus.log(`Recovered from an internal error \u2014 reconnecting: ${e.message}`, "error");
39194
- await new Promise((resolve6) => setTimeout(resolve6, 1e3));
39694
+ await new Promise((resolve7) => setTimeout(resolve7, 1e3));
39195
39695
  }
39196
39696
  }
39197
39697
  };
@@ -39318,10 +39818,10 @@ function Picker({
39318
39818
  ] });
39319
39819
  }
39320
39820
  function selectFromList(items, title) {
39321
- return new Promise((resolve6) => {
39821
+ return new Promise((resolve7) => {
39322
39822
  let chosen = null;
39323
39823
  const app = render_default(/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Picker, { items, title, onDone: (v) => chosen = v }));
39324
- void app.waitUntilExit().then(() => resolve6(chosen));
39824
+ void app.waitUntilExit().then(() => resolve7(chosen));
39325
39825
  });
39326
39826
  }
39327
39827