@4pm/cli 0.7.0 → 0.8.2

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.
package/dist/index.js CHANGED
@@ -11,7 +11,7 @@ import {
11
11
  __require,
12
12
  __toESM,
13
13
  wrapper_default
14
- } from "./chunk-YRQBADPD.js";
14
+ } from "./chunk-I7WQWC33.js";
15
15
 
16
16
  // ../../node_modules/.pnpm/react@18.3.1/node_modules/react/cjs/react.production.min.js
17
17
  var require_react_production_min = __commonJS({
@@ -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(resolve8, 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, resolve8, reject);
2004
2004
  } else {
2005
- resolve6(returnValue2);
2005
+ resolve8(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(resolve8, reject) {
2036
2036
  if (ReactCurrentActQueue.current === null) {
2037
2037
  ReactCurrentActQueue.current = [];
2038
- recursivelyFlushAsyncActWork(returnValue, resolve6, reject);
2038
+ recursivelyFlushAsyncActWork(returnValue, resolve8, reject);
2039
2039
  } else {
2040
- resolve6(returnValue);
2040
+ resolve8(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(resolve8, reject) {
2048
+ resolve8(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, resolve8, 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
+ resolve8(returnValue);
2074
2074
  } else {
2075
- recursivelyFlushAsyncActWork(returnValue, resolve6, reject);
2075
+ recursivelyFlushAsyncActWork(returnValue, resolve8, reject);
2076
2076
  }
2077
2077
  });
2078
2078
  } catch (error2) {
2079
2079
  reject(error2);
2080
2080
  }
2081
2081
  } else {
2082
- resolve6(returnValue);
2082
+ resolve8(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 spawn6 } from "child_process";
24695
+ import { join as join23 } 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 basename4, join as join19 } 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,18 @@ 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",
24975
+ // Subagents & skills management (ADR-0153): create/edit/delete .claude/agents + skills
24976
+ PROJECT_AGENTS: "project.agents",
24977
+ // Security & placeholder management (ADR-0154): AI_SECURITY/AI_PLACEHOLDER docs + write-only secrets
24978
+ PROJECT_SECRETS: "project.secrets",
24979
+ // RAG capability install (ADR-0156): install/manage the worker's RAG engine
24980
+ PROJECT_RAG: "project.rag",
24968
24981
  // permission
24969
24982
  PERMISSION_LIST: "permission.list",
24970
24983
  PERMISSION_READ: "permission.read",
@@ -25064,6 +25077,12 @@ var ROLE_DEFAULT_PERMISSIONS = {
25064
25077
  P.PROJECT_AI_ASSIST,
25065
25078
  P.PROJECT_MEMO_READ,
25066
25079
  P.PROJECT_MEMO_WRITE,
25080
+ P.PROJECT_GIT,
25081
+ P.PROJECT_GIT_WRITE,
25082
+ P.PROJECT_AUTONOMOUS,
25083
+ P.PROJECT_AGENTS,
25084
+ P.PROJECT_SECRETS,
25085
+ P.PROJECT_RAG,
25067
25086
  P.PERMISSION_LIST,
25068
25087
  P.PERMISSION_READ,
25069
25088
  P.MACHINE_PAIR,
@@ -25088,6 +25107,13 @@ var ROLE_DEFAULT_PERMISSIONS = {
25088
25107
  P.PROJECT_READ,
25089
25108
  P.PROJECT_MEMO_READ,
25090
25109
  P.PROJECT_MEMO_WRITE,
25110
+ P.PROJECT_GIT,
25111
+ // manage subagents/skills (dev artifacts) — ADR-0153
25112
+ P.PROJECT_AGENTS,
25113
+ // manage security/placeholder + set (write-only) secrets — ADR-0154
25114
+ P.PROJECT_SECRETS,
25115
+ // install/manage the worker RAG engine — ADR-0156
25116
+ P.PROJECT_RAG,
25091
25117
  P.INTEGRATION_READ
25092
25118
  ],
25093
25119
  [Role.QA]: [
@@ -25098,6 +25124,7 @@ var ROLE_DEFAULT_PERMISSIONS = {
25098
25124
  P.PROJECT_READ,
25099
25125
  P.PROJECT_MEMO_READ,
25100
25126
  P.PROJECT_MEMO_WRITE,
25127
+ P.PROJECT_GIT,
25101
25128
  P.INTEGRATION_READ
25102
25129
  ],
25103
25130
  [Role.BA]: [
@@ -25108,6 +25135,7 @@ var ROLE_DEFAULT_PERMISSIONS = {
25108
25135
  P.PROJECT_READ,
25109
25136
  P.PROJECT_MEMO_READ,
25110
25137
  P.PROJECT_MEMO_WRITE,
25138
+ P.PROJECT_GIT,
25111
25139
  P.INTEGRATION_READ
25112
25140
  ],
25113
25141
  [Role.MACHINE]: [
@@ -25115,6 +25143,13 @@ var ROLE_DEFAULT_PERMISSIONS = {
25115
25143
  P.PROJECT_READ,
25116
25144
  P.PROJECT_MEMO_READ,
25117
25145
  P.PROJECT_MEMO_WRITE,
25146
+ // git read + write so the autonomous merge cycle (auto-cycle.md) can run (ADR-0151)
25147
+ P.PROJECT_GIT,
25148
+ P.PROJECT_GIT_WRITE,
25149
+ // autonomous mode control on its own physic project (ADR-0152)
25150
+ P.PROJECT_AUTONOMOUS,
25151
+ // manage its own subagents/skills (ADR-0153)
25152
+ P.PROJECT_AGENTS,
25118
25153
  // view one's own worker/cli + configure the physic project
25119
25154
  P.MACHINE_LIST,
25120
25155
  P.MACHINE_PHYSIC_MANAGE,
@@ -25142,7 +25177,13 @@ var PROJECT_SCOPE_CHECKED_CODES = [
25142
25177
  P.PROJECT_PAUSE,
25143
25178
  P.PROJECT_CLOSE,
25144
25179
  P.PROJECT_PAUSE_MEMBER,
25145
- P.PROJECT_AI_ASSIST
25180
+ P.PROJECT_AI_ASSIST,
25181
+ P.PROJECT_GIT,
25182
+ P.PROJECT_GIT_WRITE,
25183
+ P.PROJECT_AUTONOMOUS,
25184
+ P.PROJECT_AGENTS,
25185
+ P.PROJECT_SECRETS,
25186
+ P.PROJECT_RAG
25146
25187
  ];
25147
25188
  var SELF_SERVICE_CODES = [
25148
25189
  P.AUTH_LOGOUT,
@@ -25182,6 +25223,12 @@ var TEAM_GRANTABLE_CODES = [
25182
25223
  P.PROJECT_AI_ASSIST,
25183
25224
  P.PROJECT_MEMO_READ,
25184
25225
  P.PROJECT_MEMO_WRITE,
25226
+ P.PROJECT_GIT,
25227
+ P.PROJECT_GIT_WRITE,
25228
+ P.PROJECT_AUTONOMOUS,
25229
+ P.PROJECT_AGENTS,
25230
+ P.PROJECT_SECRETS,
25231
+ P.PROJECT_RAG,
25185
25232
  P.COMMAND_EXECUTE,
25186
25233
  P.COMMAND_READ,
25187
25234
  P.MACHINE_LIST,
@@ -25203,6 +25250,12 @@ var PROJECT_GRANTABLE_CODES = [
25203
25250
  P.PROJECT_AI_ASSIST,
25204
25251
  P.PROJECT_MEMO_READ,
25205
25252
  P.PROJECT_MEMO_WRITE,
25253
+ P.PROJECT_GIT,
25254
+ P.PROJECT_GIT_WRITE,
25255
+ P.PROJECT_AUTONOMOUS,
25256
+ P.PROJECT_AGENTS,
25257
+ P.PROJECT_SECRETS,
25258
+ P.PROJECT_RAG,
25206
25259
  P.COMMAND_EXECUTE,
25207
25260
  P.COMMAND_READ,
25208
25261
  P.MACHINE_PHYSIC_MANAGE
@@ -29315,7 +29368,7 @@ var pinnedFilterSchema = external_exports.enum(["true", "false"]).transform((v)
29315
29368
  var deletedFilterSchema = external_exports.enum(["true", "false"]).transform((v) => v === "true").optional();
29316
29369
 
29317
29370
  // ../../22-packages/11-shared/41-validation/src/schemas.ts
29318
- var passwordSchema = external_exports.string().min(8, "password: at least 8 characters").max(72, "password: at most 72 characters").regex(/[a-zA-Z]/, "password: needs at least 1 letter").regex(/[0-9]/, "password: needs at least 1 digit");
29371
+ var passwordSchema = external_exports.string().min(8, "password: at least 8 characters").max(32, "password: at most 32 characters").regex(/[a-zA-Z]/, "password: needs at least 1 letter").regex(/[0-9]/, "password: needs at least 1 digit");
29319
29372
  var usernameSchema = external_exports.string().min(3).max(32).regex(/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/, "username: invalid characters");
29320
29373
  var emailSchema = external_exports.string().email().max(254).toLowerCase();
29321
29374
  var phoneSchema = external_exports.string().regex(/^\+?[0-9]{8,15}$/, "phone: invalid format");
@@ -29344,6 +29397,9 @@ var registerRequestSchema = external_exports.object({
29344
29397
  var mailTokenRequestSchema = external_exports.object({
29345
29398
  token: hexTokenSchema
29346
29399
  });
29400
+ var resendVerifyRequestSchema = external_exports.object({
29401
+ email: emailSchema
29402
+ });
29347
29403
  var loginRequestSchema = external_exports.object({
29348
29404
  identifier: external_exports.string().min(1).max(254),
29349
29405
  password: external_exports.string().min(1).max(72),
@@ -29488,6 +29544,9 @@ var createProjectRequestSchema = external_exports.object({
29488
29544
  description: external_exports.string().max(2e3).optional(),
29489
29545
  machineUserId: external_exports.string().uuid().optional()
29490
29546
  });
29547
+ var copyProjectRequestSchema = external_exports.object({
29548
+ name: external_exports.string().min(1).max(100)
29549
+ });
29491
29550
  var DEFAULT_OUTBOUND_RULES = {
29492
29551
  secret: [
29493
29552
  String.raw`/\b(api[_-]?key|secret|password|passwd|access[_-]?token|bearer)\b\s*[:=]\s*\S+/i`,
@@ -29518,7 +29577,15 @@ var updateProjectRequestSchema = external_exports.object({
29518
29577
  // Messenger retention in days (0 = keep forever) — manager only (ADR-0078).
29519
29578
  messengerRetentionDays: external_exports.number().int().min(0).max(3650).optional(),
29520
29579
  // Project settings (ADR-0081) — key-level shallow-merge; `tokens` clamped on read.
29521
- settings: external_exports.record(external_exports.unknown()).optional()
29580
+ settings: external_exports.record(external_exports.unknown()).optional(),
29581
+ // Identity ↔ spec sync (ADR-0148): the Settings tab edits the spec basic group
29582
+ // (id = spec slug, name, desc). The server merges these into `spec` (READY) or
29583
+ // `specDraft` (DRAFT) so the Content → Spec tab / wizard stays consistent.
29584
+ specBasic: external_exports.object({
29585
+ id: external_exports.string().max(200),
29586
+ name: external_exports.string().max(100),
29587
+ desc: external_exports.string().max(2e3)
29588
+ }).partial().optional()
29522
29589
  });
29523
29590
  var MEMO_MAX_LENGTH = 65536;
29524
29591
  var putMemoRequestSchema = external_exports.object({
@@ -29705,6 +29772,57 @@ var listMachineLinksQuerySchema = baseRequestSchema.extend({
29705
29772
  var updateMachineLinkRequestSchema = external_exports.object({
29706
29773
  hashcode3ExpiresAt: external_exports.string().datetime().nullable()
29707
29774
  });
29775
+ var configWriteRequestSchema = external_exports.object({
29776
+ config: external_exports.string().max(256 * 1024)
29777
+ });
29778
+ var FILE_WRITE_MAX_BYTES = 2 * 1024 * 1024;
29779
+ var fsWriteRequestSchema = external_exports.object({
29780
+ /** Path relative to the physic-project root; a `..` escaping the root is rejected on the cli. */
29781
+ path: external_exports.string().min(1).max(1024),
29782
+ /** Full new UTF-8 content (replaces the file wholesale). */
29783
+ content: external_exports.string().max(FILE_WRITE_MAX_BYTES)
29784
+ });
29785
+ var autonomousWriteRequestSchema = external_exports.discriminatedUnion("kind", [
29786
+ external_exports.object({ kind: external_exports.literal("settings"), settings: external_exports.string().max(64 * 1024) }),
29787
+ external_exports.object({ kind: external_exports.literal("approvals"), taskId: external_exports.string().min(1).max(64), approved: external_exports.boolean() }),
29788
+ external_exports.object({ kind: external_exports.literal("userTodo"), content: external_exports.string().min(1).max(16 * 1024) }),
29789
+ external_exports.object({ kind: external_exports.literal("cron"), action: external_exports.enum(["install", "uninstall"]) })
29790
+ ]);
29791
+ var autonomousLogsQuerySchema = external_exports.object({
29792
+ date: external_exports.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional()
29793
+ });
29794
+ var agentNameSchema = external_exports.string().min(1).max(64).regex(/^[\w.-]+$/);
29795
+ var agentReadQuerySchema = external_exports.object({
29796
+ kind: external_exports.enum(["subagent", "skill"]),
29797
+ name: agentNameSchema
29798
+ });
29799
+ var ragInstallRequestSchema = external_exports.object({
29800
+ model: external_exports.string().min(1).max(200)
29801
+ });
29802
+ var ragQueryQuerySchema = external_exports.object({
29803
+ q: external_exports.string().min(1).max(2e3),
29804
+ k: external_exports.coerce.number().int().min(1).max(20).optional()
29805
+ });
29806
+ var graphBuildQuerySchema = external_exports.object({
29807
+ mode: external_exports.enum(["docs", "code"])
29808
+ });
29809
+ var secretKeySchema = external_exports.string().min(1).max(128).regex(/^[\w.-]+$/);
29810
+ var secretsWriteRequestSchema = external_exports.discriminatedUnion("kind", [
29811
+ external_exports.object({ kind: external_exports.literal("security"), content: external_exports.string().max(128 * 1024) }),
29812
+ external_exports.object({ kind: external_exports.literal("placeholder"), content: external_exports.string().max(128 * 1024) }),
29813
+ external_exports.object({ kind: external_exports.literal("secret"), key: secretKeySchema, value: external_exports.string().max(8192) }),
29814
+ external_exports.object({ kind: external_exports.literal("secretDelete"), key: secretKeySchema })
29815
+ ]);
29816
+ var agentWriteRequestSchema = external_exports.object({
29817
+ kind: external_exports.enum(["subagent", "skill"]),
29818
+ name: agentNameSchema,
29819
+ action: external_exports.enum(["write", "delete"]),
29820
+ content: external_exports.string().max(128 * 1024).optional()
29821
+ }).superRefine((v, ctx) => {
29822
+ if (v.action === "write" && (v.content === void 0 || v.content.length === 0)) {
29823
+ ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["content"], message: "content is required for write" });
29824
+ }
29825
+ });
29708
29826
 
29709
29827
  // ../../22-packages/11-shared/31-dto/src/command.ts
29710
29828
  var COMMAND_MAX_LEN = 8e3;
@@ -29725,7 +29843,14 @@ var dispatchCommandRequestSchema = external_exports.object({
29725
29843
  * Short human label shown to OTHER tabs watching this cli's activity feed (ADR-0101):
29726
29844
  * e.g. "AI review" so the Console shows a friendly line instead of the raw prompt.
29727
29845
  */
29728
- label: external_exports.string().max(120).optional()
29846
+ label: external_exports.string().max(120).optional(),
29847
+ /**
29848
+ * Git tab (ADR-0151): when set, `command` is a `git`/`gh`/`glab` command run on the
29849
+ * worker for the Git tab. The server routes the permission to `project.git` (`read`) /
29850
+ * `project.git_write` (`write`) instead of `command.execute`, and validates `command`
29851
+ * against the git allowlist (`isGitCommandAllowed`). Absent ⇒ normal console dispatch.
29852
+ */
29853
+ gitOp: external_exports.enum(["read", "write"]).optional()
29729
29854
  }).superRefine((v, ctx) => {
29730
29855
  if (!v.ai && v.command.length > COMMAND_MAX_LEN) {
29731
29856
  ctx.addIssue({
@@ -29872,12 +29997,6 @@ var checkoutRequestSchema = external_exports.object({
29872
29997
  planCode: external_exports.string().min(1).max(50),
29873
29998
  provider: external_exports.enum([PaymentProvider.STRIPE, PaymentProvider.PAYPAL]).optional()
29874
29999
  });
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
30000
  var assignPlanRequestSchema = external_exports.object({
29882
30001
  planCode: external_exports.string().min(1).max(50),
29883
30002
  /** Cycle length in days (defaults to 30). */
@@ -29948,8 +30067,36 @@ var WsChannels = {
29948
30067
  ...WsChannel,
29949
30068
  FS_LIST: "fs.list",
29950
30069
  FS_READ: "fs.read",
30070
+ // server → cli (reply): write a file, path-clamped to the physic root (machine-0027,
30071
+ // ADR-0151) — the Git tab's manual conflict resolution (`project.git_write`); the only
30072
+ // git-tab write not carried by a git command over command dispatch.
30073
+ FS_WRITE: "fs.write",
29951
30074
  GIT_DIFF: "git.diff",
29952
30075
  GIT_ENV: "git.env",
30076
+ // server → cli (reply): read/write the paired profile's config.json (machine-0025/0026,
30077
+ // ADR-0141) — the machine-user Settings tab syncs the worker cli's config.
30078
+ CONFIG_READ: "config.read",
30079
+ CONFIG_WRITE: "config.write",
30080
+ // autonomous mode control (ADR-0152): read settings+status+books+approvals, tail logs, and
30081
+ // write {settings|approvals|userTodo|cron} — the dashboard drives the worker's autonomous engine.
30082
+ AUTONOMOUS_READ: "autonomous.read",
30083
+ AUTONOMOUS_WRITE: "autonomous.write",
30084
+ AUTONOMOUS_LOGS: "autonomous.logs",
30085
+ // subagents & skills management (ADR-0153): list/read/write .claude/agents + .claude/skills
30086
+ AGENTS_LIST: "agents.list",
30087
+ AGENTS_READ: "agents.read",
30088
+ AGENTS_WRITE: "agents.write",
30089
+ // security & placeholder management (ADR-0154): AI_SECURITY/AI_PLACEHOLDER + write-only secrets
30090
+ SECRETS_READ: "secrets.read",
30091
+ SECRETS_WRITE: "secrets.write",
30092
+ // docs & code dependency graph (ADR-0155): build a {nodes,edges,orphans} graph per mode
30093
+ GRAPH_BUILD: "graph.build",
30094
+ // RAG capability + install (ADR-0156): probe the worker + install a worker-tuned model
30095
+ RAG_STATUS: "rag.status",
30096
+ RAG_INSTALL: "rag.install",
30097
+ // RAG index + query (ADR-0157): reindex docs into sqlite-vec, semantic query
30098
+ RAG_REINDEX: "rag.reindex",
30099
+ RAG_QUERY: "rag.query",
29953
30100
  // git history browse (read-through cli — ADR-0089): repos on the physic project,
29954
30101
  // commit log, changed files of a commit, parent↔commit diff of one file.
29955
30102
  GIT_REPOS: "git.repos",
@@ -29967,6 +30114,11 @@ var WsChannels = {
29967
30114
  PHYSIC_DELETE: "physic.delete",
29968
30115
  // command lifecycle
29969
30116
  COMMAND_ANNOUNCE: "command.announce",
30117
+ // console transcript sync (ADR-0150): the cli streams its authoritative SessionBus
30118
+ // transcript so the web Console renders it verbatim; gated by console.watch (server → cli:
30119
+ // whether a web viewer is attached) so an unwatched cli syncs nothing.
30120
+ CONSOLE_SYNC: "console.sync",
30121
+ CONSOLE_WATCH: "console.watch",
29970
30122
  // AI spec-assist (suggest/review/compose) moved off dedicated ai.* channels to
29971
30123
  // command.dispatch({ ai:true }) → the profile-failover AI path (ADR-0100).
29972
30124
  // quota & usage (ADR-0020)
@@ -30104,7 +30256,7 @@ var OutputBatcher = class {
30104
30256
  };
30105
30257
 
30106
30258
  // src/core/ws-client.ts
30107
- import { existsSync as existsSync7, mkdirSync as mkdirSync4, rmSync as rmSync4 } from "fs";
30259
+ import { existsSync as existsSync9, mkdirSync as mkdirSync4, rmSync as rmSync4 } from "fs";
30108
30260
  import { randomUUID as randomCommandId } from "crypto";
30109
30261
 
30110
30262
  // src/core/update-scheduler.ts
@@ -30347,7 +30499,7 @@ async function runCommand(dispatch, emit) {
30347
30499
  });
30348
30500
  child.stdout?.on("data", (data) => batcher.push(data.toString("utf8")));
30349
30501
  child.stderr?.on("data", (data) => batcher.push(data.toString("utf8")));
30350
- await new Promise((resolve6) => {
30502
+ await new Promise((resolve8) => {
30351
30503
  child.on("close", (exitCode) => {
30352
30504
  batcher.flush();
30353
30505
  emit({
@@ -30357,7 +30509,7 @@ async function runCommand(dispatch, emit) {
30357
30509
  done: true,
30358
30510
  exitCode: exitCode ?? -1
30359
30511
  });
30360
- resolve6();
30512
+ resolve8();
30361
30513
  });
30362
30514
  child.on("error", (err) => {
30363
30515
  emit({
@@ -30368,7 +30520,7 @@ async function runCommand(dispatch, emit) {
30368
30520
  done: true,
30369
30521
  exitCode: -1
30370
30522
  });
30371
- resolve6();
30523
+ resolve8();
30372
30524
  });
30373
30525
  });
30374
30526
  }
@@ -30448,10 +30600,10 @@ function isSessionLimit(text) {
30448
30600
  );
30449
30601
  }
30450
30602
  function runAttempt(commandId, cmd, args, cwd2, env3, onChunk) {
30451
- return new Promise((resolve6) => {
30603
+ return new Promise((resolve8) => {
30452
30604
  void runCommand({ commandId, cmd, args, path: cwd2, env: env3 }, (out) => {
30453
30605
  if (out.chunk) onChunk(out.chunk);
30454
- if (out.done) resolve6(out.exitCode ?? -1);
30606
+ if (out.done) resolve8(out.exitCode ?? -1);
30455
30607
  });
30456
30608
  });
30457
30609
  }
@@ -30629,16 +30781,16 @@ async function fetchUsage(token) {
30629
30781
  }
30630
30782
  }
30631
30783
  function runUsageCli(dir2, aiCli2) {
30632
- return new Promise((resolve6) => {
30784
+ return new Promise((resolve8) => {
30633
30785
  let out = "";
30634
30786
  let child = null;
30635
30787
  const finish = () => {
30636
30788
  clearTimeout(timer);
30637
- resolve6(out);
30789
+ resolve8(out);
30638
30790
  };
30639
30791
  const timer = setTimeout(() => {
30640
30792
  child?.kill();
30641
- resolve6(out);
30793
+ resolve8(out);
30642
30794
  }, 3e4);
30643
30795
  timer.unref?.();
30644
30796
  try {
@@ -31069,24 +31221,738 @@ async function readWorkerFile(path) {
31069
31221
  }
31070
31222
  }
31071
31223
 
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";
31224
+ // src/core/fs-write.ts
31225
+ import { mkdir as mkdir2, writeFile } from "fs/promises";
31226
+ import { dirname as dirname3, resolve as resolve3, sep as sep3 } from "path";
31227
+ var MAX_BYTES2 = 2 * 1024 * 1024;
31228
+ function resolveInRoot(root, relPath) {
31229
+ if (!root) return null;
31230
+ const base = resolve3(root);
31231
+ const target = resolve3(base, relPath || ".");
31232
+ return target === base || target.startsWith(base + sep3) ? target : null;
31233
+ }
31234
+ async function writeWorkerFile(root, relPath, content) {
31235
+ const target = resolveInRoot(root, relPath);
31236
+ if (!target) {
31237
+ return { ok: false, path: relPath, bytes: 0, error: "path escapes the project root" };
31238
+ }
31239
+ const bytes = Buffer.byteLength(content, "utf8");
31240
+ if (bytes > MAX_BYTES2) {
31241
+ return { ok: false, path: target, bytes: 0, error: `content too large (max ${MAX_BYTES2} bytes)` };
31242
+ }
31243
+ try {
31244
+ await mkdir2(dirname3(target), { recursive: true });
31245
+ await writeFile(target, content, "utf8");
31246
+ return { ok: true, path: target, bytes };
31247
+ } catch (err) {
31248
+ return { ok: false, path: target, bytes: 0, error: err instanceof Error ? err.message : String(err) };
31249
+ }
31250
+ }
31251
+
31252
+ // src/core/autonomous.ts
31253
+ import { readFile as readFile2, writeFile as writeFile2, chmod } from "fs/promises";
31254
+ import { readFileSync as readFileSync12 } from "fs";
31255
+ import { execFile as execFile2, spawn as spawn4 } from "child_process";
31256
+ import { join as join12 } from "path";
31076
31257
  import { promisify as promisify2 } from "util";
31077
31258
  var run2 = promisify2(execFile2);
31259
+ var SETTINGS_REL = ".claude/.autonomous.settings.json";
31260
+ var HISTORIES_REL = ".claude/.autonomous.histories.json";
31261
+ var APPROVALS_REL = ".claude/.autonomous.approvals.json";
31262
+ var TICK_REL = ".claude/hooks/autonomous-tick.sh";
31263
+ var LOG_DIR_REL = ".claude/logs";
31264
+ var BOOK_FILES = {
31265
+ userTodo: "USER_TODO.md",
31266
+ aiTodo: "AI_TODO.md",
31267
+ aiProgress: "AI_PROGRESS.md",
31268
+ aiDone: "AI_DONE.md",
31269
+ userQa: "USER_QA.md"
31270
+ };
31271
+ var DEFAULT_CRON = "*/10 * * * *";
31272
+ async function readText(path, fallback = "") {
31273
+ try {
31274
+ return await readFile2(path, "utf8");
31275
+ } catch {
31276
+ return fallback;
31277
+ }
31278
+ }
31279
+ function parseJson(text) {
31280
+ try {
31281
+ const v = JSON.parse(text);
31282
+ return v && typeof v === "object" ? v : null;
31283
+ } catch {
31284
+ return null;
31285
+ }
31286
+ }
31287
+ function tickScript(root) {
31288
+ return join12(root, TICK_REL);
31289
+ }
31290
+ async function crontabList() {
31291
+ try {
31292
+ const { stdout } = await run2("crontab", ["-l"], { timeout: 1e4 });
31293
+ return stdout;
31294
+ } catch {
31295
+ return "";
31296
+ }
31297
+ }
31298
+ async function crontabSet(content) {
31299
+ return new Promise((resolve8) => {
31300
+ const child = spawn4("crontab", ["-"], { stdio: ["pipe", "ignore", "ignore"] });
31301
+ child.on("error", () => resolve8(false));
31302
+ child.on("close", (code) => resolve8(code === 0));
31303
+ child.stdin.end(content.endsWith("\n") ? content : content + "\n");
31304
+ });
31305
+ }
31306
+ async function cronInstalled(root) {
31307
+ const script = tickScript(root);
31308
+ return (await crontabList()).split("\n").some((l) => l.includes(script) && !l.trim().startsWith("#"));
31309
+ }
31310
+ async function getAutonomousStatus(root) {
31311
+ const settings = parseJson(await readText(join12(root, SETTINGS_REL))) ?? {};
31312
+ const hist = parseJson(await readText(join12(root, HISTORIES_REL))) ?? {};
31313
+ const records = Array.isArray(hist.records) ? hist.records : [];
31314
+ const last = records[records.length - 1];
31315
+ const ticks = hist.ticks ?? {};
31316
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
31317
+ return {
31318
+ installed: await cronInstalled(root),
31319
+ paused: settings.paused === true,
31320
+ cronSchedule: typeof settings.cron_schedule === "string" ? settings.cron_schedule : DEFAULT_CRON,
31321
+ lastTickAt: last && typeof last.ts === "string" ? last.ts : null,
31322
+ lastResult: last && typeof last.status === "string" ? last.status : null,
31323
+ consecutiveFails: typeof hist.consecutive_fails === "number" ? hist.consecutive_fails : 0,
31324
+ todayTicks: ticks.day === today && typeof ticks.count === "number" ? ticks.count : 0
31325
+ };
31326
+ }
31327
+ function isAutonomousRunning(root) {
31328
+ try {
31329
+ const s = JSON.parse(readFileSync12(join12(root, SETTINGS_REL), "utf8"));
31330
+ if (s.paused === true) return false;
31331
+ const h = JSON.parse(readFileSync12(join12(root, HISTORIES_REL), "utf8"));
31332
+ const ts = h.records?.[h.records.length - 1]?.ts;
31333
+ if (!ts) return false;
31334
+ const t = Date.parse(ts.replace(" ", "T"));
31335
+ return Number.isFinite(t) && Date.now() - t < 6 * 3600 * 1e3;
31336
+ } catch {
31337
+ return false;
31338
+ }
31339
+ }
31340
+ async function readAutonomous(root) {
31341
+ const [settings, approvals, status, ...books] = await Promise.all([
31342
+ readText(join12(root, SETTINGS_REL)),
31343
+ readText(join12(root, APPROVALS_REL), "{}"),
31344
+ getAutonomousStatus(root),
31345
+ ...Object.values(BOOK_FILES).map((f) => readText(join12(root, f)))
31346
+ ]);
31347
+ const keys = Object.keys(BOOK_FILES);
31348
+ const bookMap = {};
31349
+ keys.forEach((k, i) => bookMap[k] = books[i] ?? "");
31350
+ return { settings, status, books: bookMap, approvals };
31351
+ }
31352
+ async function readAutonomousLogs(root, date) {
31353
+ const day = date && /^\d{4}-\d{2}-\d{2}$/.test(date) ? date : (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
31354
+ const text = await readText(join12(root, LOG_DIR_REL, `autonomous-tick-${day}.log`));
31355
+ const lines = text ? text.split("\n").filter((l) => l.length > 0).slice(-500) : [];
31356
+ return { date: day, lines };
31357
+ }
31358
+ async function installCron(root) {
31359
+ const script = tickScript(root);
31360
+ const status = await getAutonomousStatus(root);
31361
+ await chmod(script, 493).catch(() => void 0);
31362
+ const kept = (await crontabList()).split("\n").filter((l) => l.trim() && !l.includes(script));
31363
+ kept.push(`${status.cronSchedule} ${script}`);
31364
+ await crontabSet(kept.join("\n"));
31365
+ }
31366
+ async function uninstallCron(root) {
31367
+ const script = tickScript(root);
31368
+ const cur = await crontabList();
31369
+ if (!cur.includes(script)) return;
31370
+ const kept = cur.split("\n").filter((l) => l.trim() && !l.includes(script));
31371
+ await crontabSet(kept.join("\n"));
31372
+ }
31373
+ async function writeAutonomous(root, req, by) {
31374
+ try {
31375
+ switch (req.kind) {
31376
+ case "settings": {
31377
+ if (!parseJson(req.settings)) return { ok: false, error: "settings is not valid JSON" };
31378
+ await writeFile2(join12(root, SETTINGS_REL), req.settings, "utf8");
31379
+ break;
31380
+ }
31381
+ case "approvals": {
31382
+ const map = parseJson(await readText(join12(root, APPROVALS_REL), "{}")) ?? {};
31383
+ if (req.approved) map[req.taskId] = { approved: true, by, at: (/* @__PURE__ */ new Date()).toISOString() };
31384
+ else delete map[req.taskId];
31385
+ await writeFile2(join12(root, APPROVALS_REL), JSON.stringify(map, null, 2) + "\n", "utf8");
31386
+ break;
31387
+ }
31388
+ case "userTodo": {
31389
+ const cur = await readText(join12(root, BOOK_FILES.userTodo));
31390
+ const block = `
31391
+
31392
+ > posted by ${by} at ${(/* @__PURE__ */ new Date()).toISOString()}
31393
+ ${req.content.trim()}
31394
+ `;
31395
+ await writeFile2(join12(root, BOOK_FILES.userTodo), cur.trimEnd() + block, "utf8");
31396
+ break;
31397
+ }
31398
+ case "cron": {
31399
+ if (req.action === "install") await installCron(root);
31400
+ else await uninstallCron(root);
31401
+ break;
31402
+ }
31403
+ }
31404
+ return { ok: true, status: await getAutonomousStatus(root) };
31405
+ } catch (err) {
31406
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
31407
+ }
31408
+ }
31409
+
31410
+ // src/core/agents.ts
31411
+ import { readFile as readFile3, writeFile as writeFile3, mkdir as mkdir3, readdir as readdir3, rm } from "fs/promises";
31412
+ import { join as join13 } from "path";
31413
+ var AGENTS_REL = ".claude/agents";
31414
+ var SKILLS_REL = ".claude/skills";
31415
+ var MEMORY_DIRS = /* @__PURE__ */ new Set(["short-memory", "long-memory"]);
31416
+ function safeName(name) {
31417
+ return name.replace(/[^\w.-]+/g, "-").replace(/^[-.]+|[-.]+$/g, "");
31418
+ }
31419
+ async function readText2(path, fallback = "") {
31420
+ try {
31421
+ return await readFile3(path, "utf8");
31422
+ } catch {
31423
+ return fallback;
31424
+ }
31425
+ }
31426
+ function frontmatter(text) {
31427
+ const m = text.match(/^---\r?\n([\s\S]*?)\r?\n---/);
31428
+ const body = m?.[1] ?? "";
31429
+ const get = (key) => body.match(new RegExp(`^${key}:\\s*(.+)$`, "m"))?.[1]?.trim().replace(/^["']|["']$/g, "") ?? "";
31430
+ return { name: get("name"), description: get("description") };
31431
+ }
31432
+ async function listAgents(root) {
31433
+ const subagents = [];
31434
+ const skills = [];
31435
+ try {
31436
+ for (const e of await readdir3(join13(root, AGENTS_REL), { withFileTypes: true })) {
31437
+ if (!e.isFile() || !e.name.endsWith(".md") || MEMORY_DIRS.has(e.name)) continue;
31438
+ const fm = frontmatter(await readText2(join13(root, AGENTS_REL, e.name)));
31439
+ subagents.push({ name: e.name.replace(/\.md$/, ""), description: fm.description });
31440
+ }
31441
+ } catch {
31442
+ }
31443
+ try {
31444
+ for (const e of await readdir3(join13(root, SKILLS_REL), { withFileTypes: true })) {
31445
+ if (!e.isDirectory()) continue;
31446
+ const fm = frontmatter(await readText2(join13(root, SKILLS_REL, e.name, "SKILL.md")));
31447
+ skills.push({ name: e.name, description: fm.description });
31448
+ }
31449
+ } catch {
31450
+ }
31451
+ subagents.sort((a, b) => a.name.localeCompare(b.name));
31452
+ skills.sort((a, b) => a.name.localeCompare(b.name));
31453
+ return { subagents, skills };
31454
+ }
31455
+ function itemPath(root, kind, name) {
31456
+ const n = safeName(name);
31457
+ return kind === "subagent" ? join13(root, AGENTS_REL, `${n}.md`) : join13(root, SKILLS_REL, n, "SKILL.md");
31458
+ }
31459
+ async function readAgent(root, kind, name) {
31460
+ const content = await readText2(itemPath(root, kind, name));
31461
+ if (kind !== "subagent") return { content };
31462
+ const n = safeName(name);
31463
+ const [short, long] = await Promise.all([
31464
+ readText2(join13(root, AGENTS_REL, "short-memory", `${n}.md`)),
31465
+ readText2(join13(root, AGENTS_REL, "long-memory", `${n}.md`))
31466
+ ]);
31467
+ return { content, memory: { short, long } };
31468
+ }
31469
+ async function writeAgent(root, req) {
31470
+ const n = safeName(req.name);
31471
+ if (!n) return { ok: false, error: "invalid name" };
31472
+ try {
31473
+ if (req.action === "delete") {
31474
+ if (req.kind === "subagent") await rm(itemPath(root, "subagent", n), { force: true });
31475
+ else await rm(join13(root, SKILLS_REL, n), { recursive: true, force: true });
31476
+ return { ok: true };
31477
+ }
31478
+ if (req.content === void 0) return { ok: false, error: "content is required" };
31479
+ const path = itemPath(root, req.kind, n);
31480
+ await mkdir3(join13(path, ".."), { recursive: true });
31481
+ await writeFile3(path, req.content, "utf8");
31482
+ return { ok: true };
31483
+ } catch (err) {
31484
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
31485
+ }
31486
+ }
31487
+
31488
+ // src/core/secrets.ts
31489
+ import { readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
31490
+ import { join as join14 } from "path";
31491
+ var SECURITY_REL = "AI_SECURITY.md";
31492
+ var PLACEHOLDER_REL = "AI_PLACEHOLDER.md";
31493
+ var SECRETS_REL = "project.secrets.json";
31494
+ async function readText3(path, fallback = "") {
31495
+ try {
31496
+ return await readFile4(path, "utf8");
31497
+ } catch {
31498
+ return fallback;
31499
+ }
31500
+ }
31501
+ async function readSecretsObject(root) {
31502
+ try {
31503
+ const v = JSON.parse(await readFile4(join14(root, SECRETS_REL), "utf8"));
31504
+ return v && typeof v === "object" ? v : {};
31505
+ } catch {
31506
+ return {};
31507
+ }
31508
+ }
31509
+ function placeholderKeys(text) {
31510
+ const keys = [];
31511
+ for (const line of text.split("\n")) {
31512
+ const s = line.trim();
31513
+ if (!s.startsWith("|")) continue;
31514
+ if (/^\|[\s:|-]+\|?$/.test(s)) continue;
31515
+ const first = s.replace(/^\|/, "").split("|")[0]?.trim() ?? "";
31516
+ if (/^[\w.-]+$/.test(first) && first.toLowerCase() !== "key") keys.push(first);
31517
+ }
31518
+ return keys;
31519
+ }
31520
+ async function readSecrets(root) {
31521
+ const [security, placeholder, secrets] = await Promise.all([
31522
+ readText3(join14(root, SECURITY_REL)),
31523
+ readText3(join14(root, PLACEHOLDER_REL)),
31524
+ readSecretsObject(root)
31525
+ ]);
31526
+ const names = /* @__PURE__ */ new Set([...placeholderKeys(placeholder), ...Object.keys(secrets)]);
31527
+ const keys = [...names].sort().map((name) => ({
31528
+ name,
31529
+ set: secrets[name] !== void 0 && String(secrets[name]).length > 0
31530
+ }));
31531
+ return { security, placeholder, keys };
31532
+ }
31533
+ async function writeSecrets(root, req) {
31534
+ try {
31535
+ if (req.kind === "security") {
31536
+ await writeFile4(join14(root, SECURITY_REL), req.content, "utf8");
31537
+ } else if (req.kind === "placeholder") {
31538
+ await writeFile4(join14(root, PLACEHOLDER_REL), req.content, "utf8");
31539
+ } else {
31540
+ const secrets = await readSecretsObject(root);
31541
+ if (req.kind === "secret") secrets[req.key] = req.value;
31542
+ else delete secrets[req.key];
31543
+ await writeFile4(join14(root, SECRETS_REL), JSON.stringify(secrets, null, 2) + "\n", "utf8");
31544
+ }
31545
+ return { ok: true };
31546
+ } catch (err) {
31547
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
31548
+ }
31549
+ }
31550
+
31551
+ // src/core/graph.ts
31552
+ import { readFile as readFile5, readdir as readdir4 } from "fs/promises";
31553
+ import { join as join15, relative, dirname as dirname4, resolve as resolve4, extname, basename as basename2 } from "path";
31554
+ var MAX_FILES = 2e3;
31555
+ var MAX_NODES = 3e3;
31556
+ var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".next", "out", "coverage", ".turbo"]);
31557
+ var CODE_EXT = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".py", ".go", ".rs", ".java", ".rb", ".php"]);
31558
+ async function walk(root, accept) {
31559
+ const out = [];
31560
+ const stack = [root];
31561
+ while (stack.length && out.length < MAX_FILES) {
31562
+ const dir2 = stack.pop();
31563
+ let entries2;
31564
+ try {
31565
+ entries2 = await readdir4(dir2, { withFileTypes: true });
31566
+ } catch {
31567
+ continue;
31568
+ }
31569
+ for (const e of entries2) {
31570
+ if (e.name.startsWith(".") && e.name !== ".claude") {
31571
+ if (e.isDirectory()) continue;
31572
+ }
31573
+ const full = join15(dir2, e.name);
31574
+ if (e.isDirectory()) {
31575
+ if (!SKIP_DIRS.has(e.name)) stack.push(full);
31576
+ } else if (accept(full)) {
31577
+ out.push(full);
31578
+ if (out.length >= MAX_FILES) break;
31579
+ }
31580
+ }
31581
+ }
31582
+ return out;
31583
+ }
31584
+ function orphansOf(nodes, edges) {
31585
+ const hasInbound = new Set(edges.map((e) => e.to));
31586
+ return nodes.filter((n) => !hasInbound.has(n.id)).map((n) => n.id);
31587
+ }
31588
+ async function buildDocs(root) {
31589
+ const files = await walk(root, (p) => extname(p) === ".md");
31590
+ const ids = new Set(files.map((f) => relative(root, f).split("\\").join("/")));
31591
+ const nodes = files.map((f) => {
31592
+ const rel = relative(root, f).split("\\").join("/");
31593
+ return { id: rel, label: basename2(rel), kind: "doc", file: rel };
31594
+ });
31595
+ const edges = [];
31596
+ const seen = /* @__PURE__ */ new Set();
31597
+ for (const f of files) {
31598
+ const rel = relative(root, f).split("\\").join("/");
31599
+ let text = "";
31600
+ try {
31601
+ text = await readFile5(f, "utf8");
31602
+ } catch {
31603
+ continue;
31604
+ }
31605
+ for (const m of text.matchAll(/\[[^\]]*\]\(([^)]+)\)/g)) {
31606
+ const href = (m[1] ?? "").split("#")[0].trim();
31607
+ if (!href || !href.endsWith(".md") || href.startsWith("http")) continue;
31608
+ const target = relative(root, resolve4(dirname4(f), href)).split("\\").join("/");
31609
+ if (ids.has(target) && target !== rel) {
31610
+ const key = `${rel}->${target}`;
31611
+ if (!seen.has(key)) {
31612
+ seen.add(key);
31613
+ edges.push({ from: rel, to: target });
31614
+ }
31615
+ }
31616
+ }
31617
+ }
31618
+ return { nodes, edges, orphans: orphansOf(nodes, edges) };
31619
+ }
31620
+ var DEF_PATTERNS = [
31621
+ /\bfunction\s+([A-Za-z_$][\w$]*)/g,
31622
+ // JS/TS function decl
31623
+ /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*(?:async\s*)?(?:function\b|\([^)]*\)\s*=>|[A-Za-z_$][\w$]*\s*=>)/g,
31624
+ // arrow/func expr
31625
+ /^\s*def\s+([A-Za-z_]\w*)/gm,
31626
+ // Python
31627
+ /\bfunc\s+(?:\([^)]*\)\s*)?([A-Za-z_]\w*)/g
31628
+ // Go
31629
+ ];
31630
+ async function buildCode(root) {
31631
+ const files = await walk(root, (p) => CODE_EXT.has(extname(p)));
31632
+ const nodes = [];
31633
+ const nameToIds = /* @__PURE__ */ new Map();
31634
+ const fileDefs = /* @__PURE__ */ new Map();
31635
+ for (const f of files) {
31636
+ if (nodes.length >= MAX_NODES) break;
31637
+ const rel = relative(root, f).split("\\").join("/");
31638
+ let text = "";
31639
+ try {
31640
+ text = await readFile5(f, "utf8");
31641
+ } catch {
31642
+ continue;
31643
+ }
31644
+ const defs = [];
31645
+ for (const re of DEF_PATTERNS) {
31646
+ for (const m of text.matchAll(re)) {
31647
+ const name = m[1] ?? "";
31648
+ if (!name || name.length < 2) continue;
31649
+ const id = `${rel}#${name}`;
31650
+ if (nameToIds.has(name) && (nameToIds.get(name) ?? []).includes(id)) continue;
31651
+ if (nodes.length >= MAX_NODES) break;
31652
+ defs.push({ name, id, start: m.index ?? 0 });
31653
+ nodes.push({ id, label: name, kind: "fn", file: rel });
31654
+ nameToIds.set(name, [...nameToIds.get(name) ?? [], id]);
31655
+ }
31656
+ }
31657
+ defs.sort((a, b) => a.start - b.start);
31658
+ fileDefs.set(rel, { rel, text, defs });
31659
+ }
31660
+ const edges = [];
31661
+ const seen = /* @__PURE__ */ new Set();
31662
+ for (const { text, defs } of fileDefs.values()) {
31663
+ for (let i = 0; i < defs.length; i++) {
31664
+ const def = defs[i];
31665
+ const end = i + 1 < defs.length ? defs[i + 1].start : text.length;
31666
+ const body = text.slice(def.start, end);
31667
+ for (const m of body.matchAll(/([A-Za-z_$][\w$]*)\s*\(/g)) {
31668
+ const callee = m[1] ?? "";
31669
+ if (callee === def.name) continue;
31670
+ for (const targetId of nameToIds.get(callee) ?? []) {
31671
+ if (targetId === def.id) continue;
31672
+ const key = `${def.id}->${targetId}`;
31673
+ if (!seen.has(key)) {
31674
+ seen.add(key);
31675
+ edges.push({ from: def.id, to: targetId });
31676
+ }
31677
+ }
31678
+ }
31679
+ }
31680
+ }
31681
+ return { nodes, edges, orphans: orphansOf(nodes, edges) };
31682
+ }
31683
+ async function buildGraph(root, mode) {
31684
+ try {
31685
+ return mode === "docs" ? await buildDocs(root) : await buildCode(root);
31686
+ } catch {
31687
+ return { nodes: [], edges: [], orphans: [] };
31688
+ }
31689
+ }
31690
+
31691
+ // src/core/rag.ts
31692
+ import { readFile as readFile6, writeFile as writeFile5, statfs, mkdir as mkdir4 } from "fs/promises";
31693
+ import { existsSync as existsSync7 } from "fs";
31694
+ import { execFile as execFile3, spawn as spawn5 } from "child_process";
31695
+ import { totalmem, cpus } from "os";
31696
+ import { join as join16 } from "path";
31697
+ import { promisify as promisify3 } from "util";
31698
+ var run3 = promisify3(execFile3);
31699
+ var RAG_REL = ".claude/rag";
31700
+ async function tryRun(cmd, args) {
31701
+ try {
31702
+ const { stdout } = await run3(cmd, args, { timeout: 8e3 });
31703
+ return { ok: true, out: stdout.trim() };
31704
+ } catch {
31705
+ return { ok: false, out: "" };
31706
+ }
31707
+ }
31708
+ async function detectPython() {
31709
+ for (const bin of ["python3", "python"]) {
31710
+ const r = await tryRun(bin, ["--version"]);
31711
+ if (r.ok) return { bin, found: true, version: r.out.replace(/^Python\s*/i, "") };
31712
+ }
31713
+ return { bin: "python3", found: false, version: "" };
31714
+ }
31715
+ async function installLog(root) {
31716
+ try {
31717
+ const text = await readFile6(join16(root, RAG_REL, "install.log"), "utf8");
31718
+ return text.split("\n").filter((l) => l.length > 0).slice(-200);
31719
+ } catch {
31720
+ return [];
31721
+ }
31722
+ }
31723
+ function optionsFor(m) {
31724
+ const opts = [
31725
+ {
31726
+ id: "small",
31727
+ label: "Small (bge-small, EN) \xB7 ~130 MB",
31728
+ model: "BAAI/bge-small-en-v1.5",
31729
+ sizeMB: 130,
31730
+ recommended: m.ramGB < 8,
31731
+ note: "Lightweight, low RAM."
31732
+ }
31733
+ ];
31734
+ if (m.diskFreeGB > 1.5) {
31735
+ opts.push({
31736
+ id: "multilingual",
31737
+ label: "Multilingual (MiniLM, incl. Vietnamese) \xB7 ~470 MB",
31738
+ model: "sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2",
31739
+ sizeMB: 470,
31740
+ recommended: m.ramGB >= 8 && m.diskFreeGB > 2,
31741
+ note: "Best for mixed VI/EN content."
31742
+ });
31743
+ }
31744
+ if (m.ramGB >= 8 && m.diskFreeGB > 2) {
31745
+ opts.push({
31746
+ id: "base",
31747
+ label: "Base (bge-base, EN) \xB7 ~440 MB",
31748
+ model: "BAAI/bge-base-en-v1.5",
31749
+ sizeMB: 440,
31750
+ recommended: false,
31751
+ note: "Higher quality EN, needs \u2265 8 GB RAM."
31752
+ });
31753
+ }
31754
+ return opts;
31755
+ }
31756
+ async function ragStatus(root) {
31757
+ const py = await detectPython();
31758
+ const [pip, fastembed, sqliteVec] = py.found ? await Promise.all([
31759
+ tryRun(py.bin, ["-m", "pip", "--version"]).then((r) => r.ok),
31760
+ tryRun(py.bin, ["-c", "import fastembed"]).then((r) => r.ok),
31761
+ tryRun(py.bin, ["-c", "import sqlite_vec"]).then((r) => r.ok)
31762
+ ]) : [false, false, false];
31763
+ let diskFreeGB = 0;
31764
+ try {
31765
+ const s = await statfs(root);
31766
+ diskFreeGB = Math.round(s.bavail * s.bsize / 1e9 * 10) / 10;
31767
+ } catch {
31768
+ diskFreeGB = 0;
31769
+ }
31770
+ const gpu = (await tryRun("nvidia-smi", ["-L"])).ok;
31771
+ const machine = {
31772
+ ramGB: Math.round(totalmem() / 1e9 * 10) / 10,
31773
+ cpus: cpus().length,
31774
+ diskFreeGB,
31775
+ gpu
31776
+ };
31777
+ let modelName = "";
31778
+ try {
31779
+ modelName = (await readFile6(join16(root, RAG_REL, ".installed"), "utf8")).trim();
31780
+ } catch {
31781
+ modelName = "";
31782
+ }
31783
+ const modelPresent = modelName.length > 0;
31784
+ return {
31785
+ installed: fastembed && sqliteVec && modelPresent,
31786
+ installing: existsSync7(join16(root, RAG_REL, ".installing")),
31787
+ python: { found: py.found, version: py.version },
31788
+ deps: { pip, fastembed, sqliteVec },
31789
+ model: { present: modelPresent, name: modelName },
31790
+ machine,
31791
+ options: optionsFor(machine),
31792
+ installLog: await installLog(root),
31793
+ index: await readIndexMeta(root)
31794
+ };
31795
+ }
31796
+ async function readIndexMeta(root) {
31797
+ const indexing = existsSync7(join16(root, RAG_REL, ".indexing"));
31798
+ try {
31799
+ const m = JSON.parse(await readFile6(join16(root, RAG_REL, ".index.json"), "utf8"));
31800
+ return {
31801
+ present: true,
31802
+ indexing,
31803
+ chunks: typeof m.chunks === "number" ? m.chunks : 0,
31804
+ indexedAt: typeof m.indexedAt === "string" ? m.indexedAt : ""
31805
+ };
31806
+ } catch {
31807
+ return { present: false, indexing, chunks: 0, indexedAt: "" };
31808
+ }
31809
+ }
31810
+ var RAG_ENGINE_PY = `import sys, os, json, glob, sqlite3, datetime
31811
+ BASE = os.path.join(".claude", "rag")
31812
+ DB = os.path.join(BASE, "index.db")
31813
+ def model_name():
31814
+ return open(os.path.join(BASE, ".installed"), encoding="utf-8").read().strip()
31815
+ def connect():
31816
+ import sqlite_vec
31817
+ db = sqlite3.connect(DB)
31818
+ db.enable_load_extension(True)
31819
+ sqlite_vec.load(db)
31820
+ db.enable_load_extension(False)
31821
+ return db
31822
+ def embedder():
31823
+ from fastembed import TextEmbedding
31824
+ return TextEmbedding(model_name())
31825
+ def reindex():
31826
+ import sqlite_vec
31827
+ emb = embedder()
31828
+ chunks = []
31829
+ for p in glob.glob("**/*.md", recursive=True):
31830
+ parts = p.split(os.sep)
31831
+ if "node_modules" in parts or ".git" in parts: continue
31832
+ try: txt = open(p, encoding="utf-8").read()
31833
+ except Exception: continue
31834
+ for i in range(0, len(txt), 800):
31835
+ c = txt[i:i+1000].strip()
31836
+ if len(c) > 20: chunks.append((p, c))
31837
+ if len(chunks) >= 5000: break
31838
+ vectors = list(emb.embed([c for _, c in chunks])) if chunks else []
31839
+ dim = len(vectors[0]) if vectors else 384
31840
+ os.makedirs(BASE, exist_ok=True)
31841
+ db = connect()
31842
+ db.execute("DROP TABLE IF EXISTS chunks")
31843
+ db.execute("CREATE VIRTUAL TABLE chunks USING vec0(embedding float[%d], +path text, +snippet text)" % dim)
31844
+ for (p, c), v in zip(chunks, vectors):
31845
+ db.execute("INSERT INTO chunks(embedding, path, snippet) VALUES (?, ?, ?)",
31846
+ (sqlite_vec.serialize_float32(list(v)), p, c[:400]))
31847
+ db.commit(); db.close()
31848
+ json.dump({"chunks": len(chunks), "indexedAt": datetime.datetime.now().isoformat(timespec="seconds"), "model": model_name()},
31849
+ open(os.path.join(BASE, ".index.json"), "w", encoding="utf-8"))
31850
+ print("[reindex] DONE %d chunks" % len(chunks))
31851
+ def query(q, k):
31852
+ import sqlite_vec
31853
+ v = list(embedder().embed([q]))[0]
31854
+ db = connect()
31855
+ rows = db.execute("SELECT path, snippet, distance FROM chunks WHERE embedding MATCH ? ORDER BY distance LIMIT ?",
31856
+ (sqlite_vec.serialize_float32(list(v)), k)).fetchall()
31857
+ db.close()
31858
+ out = [{"path": r[0], "snippet": r[1], "score": round(1.0/(1.0+float(r[2])), 4)} for r in rows]
31859
+ print(json.dumps({"results": out}, ensure_ascii=False))
31860
+ if __name__ == "__main__":
31861
+ cmd = sys.argv[1] if len(sys.argv) > 1 else ""
31862
+ if cmd == "reindex": reindex()
31863
+ elif cmd == "query": query(sys.argv[2], int(sys.argv[3]) if len(sys.argv) > 3 else 8)
31864
+ `;
31865
+ async function ensureEngine(root) {
31866
+ await mkdir4(join16(root, RAG_REL), { recursive: true }).catch(() => void 0);
31867
+ await writeFile5(join16(root, RAG_REL, "rag_engine.py"), RAG_ENGINE_PY, "utf8");
31868
+ }
31869
+ async function ragReindex(root) {
31870
+ const py = await detectPython();
31871
+ if (!py.found) return { ok: false, started: false, error: "python3 not found" };
31872
+ if (!existsSync7(join16(root, RAG_REL, ".installed"))) return { ok: false, started: false, error: "RAG is not installed" };
31873
+ if (existsSync7(join16(root, RAG_REL, ".indexing"))) return { ok: true, started: true };
31874
+ await ensureEngine(root);
31875
+ const script = `
31876
+ set +e
31877
+ touch "${RAG_REL}/.indexing"
31878
+ { echo "[reindex] $(date)"; "$RAG_PY" "${RAG_REL}/rag_engine.py" reindex 2>&1; } >> "${RAG_REL}/index.log" 2>&1
31879
+ rm -f "${RAG_REL}/.indexing"
31880
+ `;
31881
+ const child = spawn5("bash", ["-c", script], {
31882
+ cwd: root,
31883
+ env: { ...process.env, RAG_PY: py.bin },
31884
+ detached: true,
31885
+ stdio: "ignore"
31886
+ });
31887
+ child.unref();
31888
+ return { ok: true, started: true };
31889
+ }
31890
+ async function ragQuery(root, queryText, k = 8) {
31891
+ const py = await detectPython();
31892
+ if (!py.found) return { results: [], error: "python3 not found" };
31893
+ if (!existsSync7(join16(root, RAG_REL, "index.db"))) return { results: [], error: "no index \u2014 reindex first" };
31894
+ await ensureEngine(root);
31895
+ try {
31896
+ const { stdout } = await run3(
31897
+ py.bin,
31898
+ [join16(RAG_REL, "rag_engine.py"), "query", queryText, String(Math.min(Math.max(k, 1), 20))],
31899
+ { cwd: root, timeout: 3e4, maxBuffer: 4 * 1024 * 1024 }
31900
+ );
31901
+ const parsed = JSON.parse(stdout.trim() || '{"results":[]}');
31902
+ return { results: Array.isArray(parsed.results) ? parsed.results : [] };
31903
+ } catch (err) {
31904
+ return { results: [], error: err instanceof Error ? err.message : String(err) };
31905
+ }
31906
+ }
31907
+ async function ragInstall(root, model) {
31908
+ const py = await detectPython();
31909
+ if (!py.found) return { ok: false, started: false, error: "python3 not found on the worker" };
31910
+ if (existsSync7(join16(root, RAG_REL, ".installing"))) return { ok: true, started: true };
31911
+ try {
31912
+ await mkdir4(join16(root, RAG_REL), { recursive: true });
31913
+ } catch {
31914
+ }
31915
+ const script = `
31916
+ set +e
31917
+ touch "${RAG_REL}/.installing"
31918
+ rm -f "${RAG_REL}/.installed"
31919
+ {
31920
+ echo "[install] $(date) pip install fastembed sqlite-vec"
31921
+ "$RAG_PY" -m pip install --quiet --user fastembed sqlite-vec 2>&1
31922
+ echo "[install] warming model: $RAG_MODEL (first run downloads it)"
31923
+ "$RAG_PY" -c "from fastembed import TextEmbedding; TextEmbedding('$RAG_MODEL'); print('[install] model ready')" 2>&1
31924
+ if [ $? -eq 0 ]; then printf '%s' "$RAG_MODEL" > "${RAG_REL}/.installed"; echo "[install] DONE"; else echo "[install] FAILED"; fi
31925
+ } >> "${RAG_REL}/install.log" 2>&1
31926
+ rm -f "${RAG_REL}/.installing"
31927
+ `;
31928
+ const child = spawn5("bash", ["-c", script], {
31929
+ cwd: root,
31930
+ env: { ...process.env, RAG_PY: py.bin, RAG_MODEL: model },
31931
+ detached: true,
31932
+ stdio: "ignore"
31933
+ });
31934
+ child.unref();
31935
+ return { ok: true, started: true };
31936
+ }
31937
+
31938
+ // src/core/git-diff.ts
31939
+ import { readFile as readFile7 } from "fs/promises";
31940
+ import { execFile as execFile4 } from "child_process";
31941
+ import { basename as basename3, dirname as dirname5, resolve as resolve5 } from "path";
31942
+ import { promisify as promisify4 } from "util";
31943
+ var run4 = promisify4(execFile4);
31078
31944
  async function gitDiff(path) {
31079
- const target = resolve3(path);
31080
- const dir2 = dirname3(target);
31081
- const base = basename2(target);
31945
+ const target = resolve5(path);
31946
+ const dir2 = dirname5(target);
31947
+ const base = basename3(target);
31082
31948
  let newContent = "";
31083
31949
  try {
31084
- newContent = await readFile2(target, "utf8");
31950
+ newContent = await readFile7(target, "utf8");
31085
31951
  } catch {
31086
31952
  }
31087
31953
  let oldContent = "";
31088
31954
  try {
31089
- const { stdout } = await run2("git", ["show", `HEAD:./${base}`], {
31955
+ const { stdout } = await run4("git", ["show", `HEAD:./${base}`], {
31090
31956
  cwd: dir2,
31091
31957
  timeout: 15e3,
31092
31958
  maxBuffer: 4 * 1024 * 1024
@@ -31098,12 +31964,12 @@ async function gitDiff(path) {
31098
31964
  }
31099
31965
 
31100
31966
  // 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);
31967
+ import { execFile as execFile5 } from "child_process";
31968
+ import { promisify as promisify5 } from "util";
31969
+ var run5 = promisify5(execFile5);
31104
31970
  async function hasBinary(bin, args = ["--version"]) {
31105
31971
  try {
31106
- await run3(bin, args, { timeout: 5e3 });
31972
+ await run5(bin, args, { timeout: 5e3 });
31107
31973
  return true;
31108
31974
  } catch {
31109
31975
  return false;
@@ -31116,7 +31982,7 @@ async function checkGitEnv(provider) {
31116
31982
  let account = null;
31117
31983
  if (installed) {
31118
31984
  try {
31119
- const { stdout, stderr } = await run3(bin, ["auth", "status"], { timeout: 8e3 });
31985
+ const { stdout, stderr } = await run5(bin, ["auth", "status"], { timeout: 8e3 });
31120
31986
  const out = `${stdout}
31121
31987
  ${stderr}`;
31122
31988
  authenticated = /logged in|logged into|✓/i.test(out);
@@ -31129,22 +31995,22 @@ ${stderr}`;
31129
31995
  }
31130
31996
 
31131
31997
  // src/core/git-history.ts
31132
- 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);
31998
+ import { readdir as readdir5 } from "fs/promises";
31999
+ import { execFile as execFile6 } from "child_process";
32000
+ import { resolve as resolve6, sep as sep4 } from "path";
32001
+ import { promisify as promisify6 } from "util";
32002
+ var run6 = promisify6(execFile6);
31137
32003
  var FS = "";
31138
32004
  var RS = "";
31139
32005
  function resolveRepo(root, repo) {
31140
32006
  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;
32007
+ const base = resolve6(root);
32008
+ const requested = resolve6(base, repo && repo.trim() ? repo : ".");
32009
+ return requested === base || requested.startsWith(base + sep4) ? requested : base;
31144
32010
  }
31145
32011
  async function git(cwd2, args) {
31146
32012
  try {
31147
- const { stdout } = await run4("git", args, {
32013
+ const { stdout } = await run6("git", args, {
31148
32014
  cwd: cwd2,
31149
32015
  timeout: 15e3,
31150
32016
  maxBuffer: 8 * 1024 * 1024
@@ -31156,13 +32022,13 @@ async function git(cwd2, args) {
31156
32022
  }
31157
32023
  async function gitRepos(root) {
31158
32024
  if (!root) return { repos: [] };
31159
- const base = resolve4(root);
32025
+ const base = resolve6(root);
31160
32026
  const found = /* @__PURE__ */ new Set();
31161
32027
  const repos = [];
31162
32028
  const add = async (dir2) => {
31163
32029
  const inside = await git(dir2, ["rev-parse", "--show-toplevel"]);
31164
32030
  const top = inside.trim();
31165
- if (!top || found.has(top) || !(top === base || top.startsWith(base + sep3))) return;
32031
+ if (!top || found.has(top) || !(top === base || top.startsWith(base + sep4))) return;
31166
32032
  found.add(top);
31167
32033
  const subdir = top === base ? "" : top.slice(base.length + 1);
31168
32034
  const remote = (await git(top, ["remote", "get-url", "origin"])).trim();
@@ -31170,16 +32036,16 @@ async function gitRepos(root) {
31170
32036
  };
31171
32037
  await add(base);
31172
32038
  try {
31173
- const lvl1 = await readdir3(base, { withFileTypes: true });
32039
+ const lvl1 = await readdir5(base, { withFileTypes: true });
31174
32040
  for (const d of lvl1) {
31175
32041
  if (!d.isDirectory() || d.name === ".git" || d.name === "node_modules") continue;
31176
- const p1 = resolve4(base, d.name);
32042
+ const p1 = resolve6(base, d.name);
31177
32043
  await add(p1);
31178
32044
  try {
31179
- const lvl2 = await readdir3(p1, { withFileTypes: true });
32045
+ const lvl2 = await readdir5(p1, { withFileTypes: true });
31180
32046
  for (const d2 of lvl2) {
31181
32047
  if (!d2.isDirectory() || d2.name === ".git" || d2.name === "node_modules") continue;
31182
- await add(resolve4(p1, d2.name));
32048
+ await add(resolve6(p1, d2.name));
31183
32049
  }
31184
32050
  } catch {
31185
32051
  }
@@ -31246,24 +32112,24 @@ async function gitCommitDiff(root, repo, hash, path) {
31246
32112
  }
31247
32113
 
31248
32114
  // 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);
32115
+ import { execFile as execFile7 } from "child_process";
32116
+ import { promisify as promisify7 } from "util";
32117
+ var run7 = promisify7(execFile7);
31252
32118
  async function setCommitAuthor(cwd2, machineUsername) {
31253
32119
  if (!machineUsername) return;
31254
32120
  try {
31255
- await run5("git", ["config", "--local", "user.name", machineUsername], { cwd: cwd2, timeout: 8e3 });
32121
+ await run7("git", ["config", "--local", "user.name", machineUsername], { cwd: cwd2, timeout: 8e3 });
31256
32122
  } catch {
31257
32123
  }
31258
32124
  }
31259
32125
 
31260
32126
  // 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";
32127
+ import { cp, mkdir as mkdir5, writeFile as writeFile6 } from "fs/promises";
32128
+ import { execFile as execFile8 } from "child_process";
32129
+ import { dirname as dirname6, join as join17, resolve as resolve7 } from "path";
31264
32130
  import { fileURLToPath as fileURLToPath3 } from "url";
31265
- import { promisify as promisify6 } from "util";
31266
- var run6 = promisify6(execFile6);
32131
+ import { promisify as promisify8 } from "util";
32132
+ var run8 = promisify8(execFile8);
31267
32133
  function reposOf(spec) {
31268
32134
  const repos = spec?.repos;
31269
32135
  return Array.isArray(repos) ? repos : [];
@@ -31277,46 +32143,46 @@ async function provisionRepos(target, repos, emit, opts = {}) {
31277
32143
  if (repo.primary) {
31278
32144
  if (opts.clonePrimary && repo.op === "existing" && repo.url) {
31279
32145
  emit("git", `Cloning ${repo.url}\u2026`);
31280
- await run6("git", ["clone", repo.url, target], { timeout: 12e4 });
32146
+ await run8("git", ["clone", repo.url, target], { timeout: 12e4 });
31281
32147
  continue;
31282
32148
  }
31283
32149
  emit("git", "Initializing the primary repo\u2026");
31284
- await run6("git", ["init"], { cwd: target, timeout: 2e4 });
32150
+ await run8("git", ["init"], { cwd: target, timeout: 2e4 });
31285
32151
  if (repo.op === "existing" && repo.url) {
31286
- await run6("git", ["remote", "add", "origin", repo.url], { cwd: target, timeout: 2e4 }).catch(
32152
+ await run8("git", ["remote", "add", "origin", repo.url], { cwd: target, timeout: 2e4 }).catch(
31287
32153
  () => void 0
31288
32154
  );
31289
32155
  }
31290
32156
  continue;
31291
32157
  }
31292
- const dir2 = join12(target, subDirName(repo));
32158
+ const dir2 = join17(target, subDirName(repo));
31293
32159
  if (repo.op === "existing" && repo.url) {
31294
32160
  emit("git", `Cloning sub-repo ${repo.url}\u2026`);
31295
- await run6("git", ["clone", repo.url, dir2], { timeout: 12e4 });
32161
+ await run8("git", ["clone", repo.url, dir2], { timeout: 12e4 });
31296
32162
  } else {
31297
32163
  emit("git", `Initializing sub-repo ${subDirName(repo)}\u2026`);
31298
- await mkdir2(dir2, { recursive: true });
31299
- await run6("git", ["init"], { cwd: dir2, timeout: 2e4 });
32164
+ await mkdir5(dir2, { recursive: true });
32165
+ await run8("git", ["init"], { cwd: dir2, timeout: 2e4 });
31300
32166
  }
31301
32167
  }
31302
32168
  }
31303
32169
  function sampleDir() {
31304
32170
  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");
32171
+ const here = dirname6(fileURLToPath3(import.meta.url));
32172
+ return resolve7(here, "../../project-sample");
31307
32173
  }
31308
32174
  async function scaffoldProject(payload, profileDir2, onProgress) {
31309
32175
  const emit = (step, message) => onProgress?.({ projectId: payload.projectId, step, message });
31310
32176
  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 });
32177
+ const root = process.env.SCAFFOLD_ROOT ? resolve7(process.env.SCAFFOLD_ROOT) : resolve7(profileDir2);
32178
+ const target = join17(root, payload.projectName);
32179
+ await mkdir5(target, { recursive: true });
31314
32180
  emit("copy", "Copying the sample template\u2026");
31315
32181
  await cp(sampleDir(), target, { recursive: true, force: false, errorOnExist: false });
31316
32182
  if (payload.spec) {
31317
32183
  emit("spec", "Writing project.spec.json\u2026");
31318
- await writeFile(
31319
- join12(target, "project.spec.json"),
32184
+ await writeFile6(
32185
+ join17(target, "project.spec.json"),
31320
32186
  JSON.stringify(payload.spec, null, 2),
31321
32187
  "utf8"
31322
32188
  );
@@ -31334,7 +32200,7 @@ async function aiInit(target, spec, emit) {
31334
32200
  emit("ai-init", "AI init: subagents, README, CLAUDE.md\u2026");
31335
32201
  const subagents = Array.isArray(spec.subagents) ? spec.subagents : [];
31336
32202
  if (subagents.length > 0) {
31337
- await mkdir2(join12(target, ".claude", "agents"), { recursive: true });
32203
+ await mkdir5(join17(target, ".claude", "agents"), { recursive: true });
31338
32204
  for (const sa of subagents) {
31339
32205
  const name = (sa.name || "").trim().replace(/[^\w.-]+/g, "-").replace(/^-+|-+$/g, "");
31340
32206
  if (!name) continue;
@@ -31344,17 +32210,17 @@ name: ${name}
31344
32210
 
31345
32211
  ${(sa.description || "").trim()}
31346
32212
  `;
31347
- await writeFile(join12(target, ".claude", "agents", `${name}.md`), body, "utf8");
32213
+ await writeFile6(join17(target, ".claude", "agents", `${name}.md`), body, "utf8");
31348
32214
  }
31349
32215
  }
31350
32216
  const specJson = JSON.stringify(spec);
31351
32217
  await generateFile(
31352
- join12(target, "README.md"),
32218
+ join17(target, "README.md"),
31353
32219
  `Write a concise README.md (Markdown only, no preamble) for this project from its spec JSON:
31354
32220
  ${specJson}`
31355
32221
  );
31356
32222
  await generateFile(
31357
- join12(target, "CLAUDE.md"),
32223
+ join17(target, "CLAUDE.md"),
31358
32224
  `Write a CLAUDE.md (Markdown only, no preamble) with guidance/conventions for AI agents working in this project, derived from its spec JSON:
31359
32225
  ${specJson}`
31360
32226
  );
@@ -31362,16 +32228,16 @@ ${specJson}`
31362
32228
  async function generateFile(path, prompt) {
31363
32229
  try {
31364
32230
  const text = await aiGenerate(prompt);
31365
- if (text.trim()) await writeFile(path, text.trim() + "\n", "utf8");
32231
+ if (text.trim()) await writeFile6(path, text.trim() + "\n", "utf8");
31366
32232
  } catch {
31367
32233
  }
31368
32234
  }
31369
32235
  async function addProject(payload, profileDir2, onProgress) {
31370
32236
  const emit = (step, message) => onProgress?.({ projectId: payload.projectId, step, message });
31371
32237
  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 });
32238
+ const root = process.env.SCAFFOLD_ROOT ? resolve7(process.env.SCAFFOLD_ROOT) : resolve7(profileDir2);
32239
+ const target = join17(root, payload.projectName);
32240
+ await mkdir5(target, { recursive: true });
31375
32241
  const repos = payload.repos ?? [];
31376
32242
  if (repos.length > 0) await provisionRepos(target, repos, emit, { clonePrimary: true });
31377
32243
  onProgress?.({ projectId: payload.projectId, step: "done", message: "Project added.", done: true });
@@ -31381,6 +32247,40 @@ async function addProject(payload, profileDir2, onProgress) {
31381
32247
  }
31382
32248
  }
31383
32249
 
32250
+ // src/core/config-sync.ts
32251
+ import { existsSync as existsSync8, readFileSync as readFileSync13 } from "fs";
32252
+ import { join as join18 } from "path";
32253
+ var SERVER_MANAGED_KEYS = ["physicPath", "sessionSwitchPct", "perPromptTokenLimit"];
32254
+ function readConfigText(profileDir2) {
32255
+ const path = join18(profileDir2, "config.json");
32256
+ if (existsSync8(path)) {
32257
+ try {
32258
+ return readFileSync13(path, "utf8");
32259
+ } catch {
32260
+ }
32261
+ }
32262
+ return JSON.stringify(defaultProfileConfig(), null, 2);
32263
+ }
32264
+ function applyConfigText(profileDir2, text) {
32265
+ let parsed;
32266
+ try {
32267
+ parsed = JSON.parse(text);
32268
+ } catch (err) {
32269
+ return { ok: false, error: `Invalid JSON: ${err.message}` };
32270
+ }
32271
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
32272
+ return { ok: false, error: "config.json must be a JSON object." };
32273
+ }
32274
+ const next = { ...parsed };
32275
+ const current = readProfileConfig(profileDir2);
32276
+ for (const key of SERVER_MANAGED_KEYS) {
32277
+ if (current[key] !== void 0) next[key] = current[key];
32278
+ else delete next[key];
32279
+ }
32280
+ overwriteProfileConfig(profileDir2, next);
32281
+ return { ok: true };
32282
+ }
32283
+
31384
32284
  // src/core/ws-client.ts
31385
32285
  var HEARTBEAT_MS = 3e4;
31386
32286
  var USAGE_POLL_MS = 5 * 6e4;
@@ -31397,6 +32297,11 @@ function envReconnectMaxSec() {
31397
32297
  var MAX_HASHCODE_INVALID_RETRIES = 5;
31398
32298
  var CLOSE_DRAINING = 4001;
31399
32299
  var OFFLINE_BUFFER_MAX_BYTES = 512 * 1024;
32300
+ var CONSOLE_SNAPSHOT_INTERVAL_MS = 3e4;
32301
+ var CONSOLE_WATCH_LEASE_MS = 45e3;
32302
+ function toDtoEntry(e) {
32303
+ return { id: e.id, source: e.source, kind: e.kind, text: e.text, level: e.level, resultKind: e.resultKind };
32304
+ }
31400
32305
  var WsClient = class {
31401
32306
  constructor(context) {
31402
32307
  this.context = context;
@@ -31408,6 +32313,11 @@ var WsClient = class {
31408
32313
  );
31409
32314
  context.bus.onLocalSubmit((input) => void this.runLocalCommand(input));
31410
32315
  context.bus.onReconnect(() => this.reconnectNow());
32316
+ context.bus.onTranscript((entry) => this.emitConsole({ kind: "add", entry: toDtoEntry(entry) }));
32317
+ context.bus.onTranscriptUpdate(
32318
+ (entry) => this.emitConsole({ kind: "update", entry: toDtoEntry(entry) })
32319
+ );
32320
+ context.bus.onClear(() => this.emitConsoleClear());
31411
32321
  }
31412
32322
  context;
31413
32323
  socket = null;
@@ -31431,6 +32341,13 @@ var WsClient = class {
31431
32341
  wakeReconnect = null;
31432
32342
  /** When set, the next backoff sleep returns immediately (forced reconnect). */
31433
32343
  forceReconnect = false;
32344
+ /**
32345
+ * The single "reconnecting…" transcript line for the current outage (approach B): created on
32346
+ * the first failed attempt and updated in place each retry, then finalized to the "connected"
32347
+ * line on the next hello_ack — so one outage adds one line, not an error + a wait line per
32348
+ * attempt. Null while connected / between outages.
32349
+ */
32350
+ reconnectEntryId = null;
31434
32351
  /** Org-configured reconnect cap (seconds) from the last ws_token — overrides env. */
31435
32352
  orgReconnectMaxSec = null;
31436
32353
  /** Env override for the reconnect cap (seconds); resolved once at construction. */
@@ -31454,6 +32371,18 @@ var WsClient = class {
31454
32371
  */
31455
32372
  handledCommands = /* @__PURE__ */ new Set();
31456
32373
  offlineBufferBytes = 0;
32374
+ /**
32375
+ * Console-sync state (ADR-0150). `consoleWatching` is set by `console.watch` from the server
32376
+ * (a web Console viewer is attached); only then does the cli emit `console.sync`. `consoleRev`
32377
+ * is the monotonic revision the web uses to detect a gap; `consoleSnapshotTimer` periodically
32378
+ * re-sends a full snapshot to self-heal. Watching resets to false on each reconnect (the server
32379
+ * re-asserts it), so a fresh session re-snapshots.
32380
+ */
32381
+ consoleWatching = false;
32382
+ consoleRev = 0;
32383
+ consoleSnapshotTimer = null;
32384
+ /** Lease expiry: stop syncing if the server does not renew `console.watch` in time. */
32385
+ consoleWatchExpiry = null;
31457
32386
  /** cli → server requests awaiting a reply (quota.check — ADR-0020). */
31458
32387
  pending = /* @__PURE__ */ new Map();
31459
32388
  /** Daily scheduled cli auto-update (ADR-0074); policy fed from each ws_token. */
@@ -31487,14 +32416,14 @@ var WsClient = class {
31487
32416
  */
31488
32417
  awaitConnected(timeoutMs) {
31489
32418
  if (this.sessionKey && this.bus.status === "connected") return Promise.resolve(true);
31490
- return new Promise((resolve6) => {
32419
+ return new Promise((resolve8) => {
31491
32420
  const waiter = () => {
31492
32421
  clearTimeout(timer);
31493
- resolve6(true);
32422
+ resolve8(true);
31494
32423
  };
31495
32424
  const timer = setTimeout(() => {
31496
32425
  this.connectWaiters = this.connectWaiters.filter((w) => w !== waiter);
31497
- resolve6(false);
32426
+ resolve8(false);
31498
32427
  }, timeoutMs);
31499
32428
  this.connectWaiters.push(waiter);
31500
32429
  });
@@ -31521,16 +32450,16 @@ var WsClient = class {
31521
32450
  this.forceReconnect = false;
31522
32451
  return Promise.resolve();
31523
32452
  }
31524
- return new Promise((resolve6) => {
32453
+ return new Promise((resolve8) => {
31525
32454
  const timer = setTimeout(() => {
31526
32455
  this.wakeReconnect = null;
31527
- resolve6();
32456
+ resolve8();
31528
32457
  }, ms);
31529
32458
  this.wakeReconnect = () => {
31530
32459
  clearTimeout(timer);
31531
32460
  this.wakeReconnect = null;
31532
32461
  this.forceReconnect = false;
31533
- resolve6();
32462
+ resolve8();
31534
32463
  };
31535
32464
  });
31536
32465
  }
@@ -31540,6 +32469,7 @@ var WsClient = class {
31540
32469
  async run() {
31541
32470
  let hashcodeInvalidStreak = 0;
31542
32471
  for (let attempt = 0; !this.stopped; attempt++) {
32472
+ let reconnectDetail = "Disconnected";
31543
32473
  try {
31544
32474
  await this.connectOnce();
31545
32475
  if (this.sessionEstablished) {
@@ -31557,17 +32487,14 @@ var WsClient = class {
31557
32487
  this.logout(err.errorCode);
31558
32488
  return;
31559
32489
  }
31560
- this.bus.log(
31561
- `Token request rejected (HASHCODE_INVALID) \u2014 retrying (${hashcodeInvalidStreak}/${MAX_HASHCODE_INVALID_RETRIES}).`,
31562
- "warn"
31563
- );
32490
+ reconnectDetail = `Token rejected (HASHCODE_INVALID ${hashcodeInvalidStreak}/${MAX_HASHCODE_INVALID_RETRIES})`;
31564
32491
  } else if (err.errorCode === "USER_PAUSED") {
31565
- this.bus.log("This account is paused \u2014 retrying until it is resumed.", "warn");
32492
+ reconnectDetail = "This account is paused \u2014 retrying until it is resumed";
31566
32493
  } else {
31567
- this.bus.log(`Connection error: ${err}`, "error");
32494
+ reconnectDetail = `Connection error: ${err}`;
31568
32495
  }
31569
32496
  } else {
31570
- this.bus.log(`Connection error: ${err}`, "error");
32497
+ reconnectDetail = `Connection error: ${err}`;
31571
32498
  }
31572
32499
  }
31573
32500
  if (this.stopped) return;
@@ -31578,7 +32505,7 @@ var WsClient = class {
31578
32505
  );
31579
32506
  logger.info("ws.reconnect", { attempt, waitMs: wait });
31580
32507
  this.bus.setStatus("reconnecting");
31581
- this.bus.log(`Reconnecting in ${Math.round(wait / 1e3)}s\u2026 (/reconnect to retry now)`);
32508
+ this.showReconnectStatus(reconnectDetail, Math.round(wait / 1e3), attempt + 1);
31582
32509
  await this.interruptibleSleep(wait);
31583
32510
  }
31584
32511
  }
@@ -31586,7 +32513,7 @@ var WsClient = class {
31586
32513
  * One WS session: token → connect → handshake → handle messages until close.
31587
32514
  */
31588
32515
  connectOnce() {
31589
- return new Promise((resolve6, reject) => {
32516
+ return new Promise((resolve8, reject) => {
31590
32517
  void (async () => {
31591
32518
  this.sessionEstablished = false;
31592
32519
  this.bus.setStatus("connecting");
@@ -31655,7 +32582,7 @@ var WsClient = class {
31655
32582
  if (code === CLOSE_DRAINING) {
31656
32583
  this.bus.log("Server draining \u2014 will reconnect (backoff + jitter).");
31657
32584
  }
31658
- resolve6();
32585
+ resolve8();
31659
32586
  });
31660
32587
  socket.on("error", (err) => {
31661
32588
  this.clearHeartbeat();
@@ -31682,8 +32609,9 @@ var WsClient = class {
31682
32609
  this.physicRoot = null;
31683
32610
  }
31684
32611
  logger.info("ws.connected", { version: CLI_VERSION });
32612
+ this.stopConsoleSync();
31685
32613
  this.bus.setStatus("connected");
31686
- this.bus.log("\u2714 Connected to the server (session encryption ready).");
32614
+ this.finishReconnect();
31687
32615
  this.sendMachineStatus();
31688
32616
  this.flushOfflineBuffer();
31689
32617
  this.startHeartbeat();
@@ -31746,8 +32674,12 @@ var WsClient = class {
31746
32674
  switch (message.channel) {
31747
32675
  case WsChannels.PHYSIC_SYNC: {
31748
32676
  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 });
32677
+ if (sync.oldName) {
32678
+ const oldRoot = this.physicFolderPath(sync.oldName);
32679
+ if (oldRoot) void uninstallCron(oldRoot).catch(() => void 0);
32680
+ }
32681
+ if (sync.oldName) rmSync4(join19(this.context.profileDir, sync.oldName), { recursive: true, force: true });
32682
+ mkdirSync4(join19(this.context.profileDir, sync.newName), { recursive: true });
31751
32683
  this.physicRoot = this.physicFolderPath(sync.newName);
31752
32684
  this.bus.setProject(sync.newName);
31753
32685
  this.bus.log(`Physic project folder synced \u2192 ${sync.newName}`);
@@ -31756,7 +32688,9 @@ var WsClient = class {
31756
32688
  case WsChannels.PHYSIC_DELETE: {
31757
32689
  const del = payload;
31758
32690
  if (del.name) {
31759
- rmSync4(join13(this.context.profileDir, del.name), { recursive: true, force: true });
32691
+ const delRoot = this.physicFolderPath(del.name);
32692
+ if (delRoot) void uninstallCron(delRoot).catch(() => void 0);
32693
+ rmSync4(join19(this.context.profileDir, del.name), { recursive: true, force: true });
31760
32694
  this.bus.log(`Physic project folder deleted (project removed) \u2192 ${del.name}`);
31761
32695
  }
31762
32696
  this.bus.setProject(null);
@@ -31821,6 +32755,135 @@ var WsClient = class {
31821
32755
  (reply) => this.send(WsChannels.FS_READ, reply, message.id)
31822
32756
  );
31823
32757
  break;
32758
+ case WsChannels.FS_WRITE: {
32759
+ const req = payload;
32760
+ void writeWorkerFile(this.physicRoot, req.path, req.content).then(
32761
+ (reply) => this.send(WsChannels.FS_WRITE, reply, message.id)
32762
+ );
32763
+ break;
32764
+ }
32765
+ case WsChannels.AUTONOMOUS_READ:
32766
+ if (this.physicRoot) {
32767
+ void readAutonomous(this.physicRoot).then(
32768
+ (reply) => this.send(WsChannels.AUTONOMOUS_READ, reply, message.id)
32769
+ );
32770
+ }
32771
+ break;
32772
+ case WsChannels.AUTONOMOUS_LOGS: {
32773
+ const req = payload;
32774
+ if (this.physicRoot) {
32775
+ void readAutonomousLogs(this.physicRoot, req.date).then(
32776
+ (reply) => this.send(WsChannels.AUTONOMOUS_LOGS, reply, message.id)
32777
+ );
32778
+ }
32779
+ break;
32780
+ }
32781
+ case WsChannels.AUTONOMOUS_WRITE: {
32782
+ const req = payload;
32783
+ if (this.physicRoot) {
32784
+ void writeAutonomous(this.physicRoot, req, req.by ?? "unknown").then(
32785
+ (reply) => this.send(WsChannels.AUTONOMOUS_WRITE, reply, message.id)
32786
+ );
32787
+ }
32788
+ break;
32789
+ }
32790
+ case WsChannels.AGENTS_LIST:
32791
+ if (this.physicRoot) {
32792
+ void listAgents(this.physicRoot).then(
32793
+ (reply) => this.send(WsChannels.AGENTS_LIST, reply, message.id)
32794
+ );
32795
+ }
32796
+ break;
32797
+ case WsChannels.AGENTS_READ: {
32798
+ const req = payload;
32799
+ if (this.physicRoot) {
32800
+ void readAgent(this.physicRoot, req.kind, req.name).then(
32801
+ (reply) => this.send(WsChannels.AGENTS_READ, reply, message.id)
32802
+ );
32803
+ }
32804
+ break;
32805
+ }
32806
+ case WsChannels.AGENTS_WRITE: {
32807
+ const req = payload;
32808
+ if (this.physicRoot) {
32809
+ void writeAgent(this.physicRoot, req).then(
32810
+ (reply) => this.send(WsChannels.AGENTS_WRITE, reply, message.id)
32811
+ );
32812
+ }
32813
+ break;
32814
+ }
32815
+ case WsChannels.SECRETS_READ:
32816
+ if (this.physicRoot) {
32817
+ void readSecrets(this.physicRoot).then(
32818
+ (reply) => this.send(WsChannels.SECRETS_READ, reply, message.id)
32819
+ );
32820
+ }
32821
+ break;
32822
+ case WsChannels.SECRETS_WRITE: {
32823
+ const req = payload;
32824
+ if (this.physicRoot) {
32825
+ void writeSecrets(this.physicRoot, req).then(
32826
+ (reply) => this.send(WsChannels.SECRETS_WRITE, reply, message.id)
32827
+ );
32828
+ }
32829
+ break;
32830
+ }
32831
+ case WsChannels.GRAPH_BUILD: {
32832
+ const req = payload;
32833
+ if (this.physicRoot) {
32834
+ void buildGraph(this.physicRoot, req.mode).then(
32835
+ (reply) => this.send(WsChannels.GRAPH_BUILD, reply, message.id)
32836
+ );
32837
+ }
32838
+ break;
32839
+ }
32840
+ case WsChannels.RAG_STATUS:
32841
+ if (this.physicRoot) {
32842
+ void ragStatus(this.physicRoot).then(
32843
+ (reply) => this.send(WsChannels.RAG_STATUS, reply, message.id)
32844
+ );
32845
+ }
32846
+ break;
32847
+ case WsChannels.RAG_INSTALL: {
32848
+ const req = payload;
32849
+ if (this.physicRoot) {
32850
+ void ragInstall(this.physicRoot, req.model).then(
32851
+ (reply) => this.send(WsChannels.RAG_INSTALL, reply, message.id)
32852
+ );
32853
+ }
32854
+ break;
32855
+ }
32856
+ case WsChannels.RAG_REINDEX:
32857
+ if (this.physicRoot) {
32858
+ void ragReindex(this.physicRoot).then(
32859
+ (reply) => this.send(WsChannels.RAG_REINDEX, reply, message.id)
32860
+ );
32861
+ }
32862
+ break;
32863
+ case WsChannels.RAG_QUERY: {
32864
+ const req = payload;
32865
+ if (this.physicRoot) {
32866
+ void ragQuery(this.physicRoot, req.query, req.k).then(
32867
+ (reply) => this.send(WsChannels.RAG_QUERY, reply, message.id)
32868
+ );
32869
+ }
32870
+ break;
32871
+ }
32872
+ case WsChannels.CONFIG_READ:
32873
+ this.send(
32874
+ WsChannels.CONFIG_READ,
32875
+ { config: readConfigText(this.context.profileDir) },
32876
+ message.id
32877
+ );
32878
+ break;
32879
+ case WsChannels.CONFIG_WRITE: {
32880
+ const res = applyConfigText(
32881
+ this.context.profileDir,
32882
+ payload.config
32883
+ );
32884
+ this.send(WsChannels.CONFIG_WRITE, res, message.id);
32885
+ break;
32886
+ }
31824
32887
  case WsChannels.GIT_DIFF:
31825
32888
  void gitDiff(payload.path).then(
31826
32889
  (reply) => this.send(WsChannels.GIT_DIFF, reply, message.id)
@@ -31859,7 +32922,7 @@ var WsClient = class {
31859
32922
  }
31860
32923
  case WsChannels.LOG_READ: {
31861
32924
  const limit = Math.min(Math.max(payload.limit ?? 200, 1), 2e3);
31862
- const lines = readRecentLogLines(join13(this.context.profileDir, "logs"), limit);
32925
+ const lines = readRecentLogLines(join19(this.context.profileDir, "logs"), limit);
31863
32926
  this.send(WsChannels.LOG_READ, { lines }, message.id);
31864
32927
  break;
31865
32928
  }
@@ -31889,6 +32952,9 @@ var WsClient = class {
31889
32952
  (reply) => this.send(WsChannels.REVIEW_EVALUATE, reply, message.id)
31890
32953
  );
31891
32954
  break;
32955
+ case WsChannels.CONSOLE_WATCH:
32956
+ this.setConsoleWatching(payload.on);
32957
+ break;
31892
32958
  default:
31893
32959
  this.bus.log(`Unsupported channel: ${message.channel}`, "warn");
31894
32960
  }
@@ -31933,7 +32999,7 @@ var WsClient = class {
31933
32999
  if (perPromptLimit > 0) {
31934
33000
  const estimated = estimateTokens(prompt);
31935
33001
  if (estimated > perPromptLimit) {
31936
- this.rejectPromptOverLimit(commandId, origin, estimated, perPromptLimit);
33002
+ this.rejectPromptOverLimit(commandId, origin, prompt, estimated, perPromptLimit);
31937
33003
  return;
31938
33004
  }
31939
33005
  }
@@ -31948,7 +33014,7 @@ var WsClient = class {
31948
33014
  verdict = { commandId, ok: false, reasons: ["unavailable"] };
31949
33015
  }
31950
33016
  if (!verdict.ok) {
31951
- this.rejectByReview(commandId, origin, verdict.reasons);
33017
+ this.rejectByReview(commandId, origin, prompt, verdict.reasons);
31952
33018
  return;
31953
33019
  }
31954
33020
  }
@@ -31978,6 +33044,8 @@ var WsClient = class {
31978
33044
  commandId,
31979
33045
  cmd: plan.cmd,
31980
33046
  args: markerArgs,
33047
+ // The raw prompt so the web console echoes it like this TUI (ADR-0108), not "claude".
33048
+ prompt,
31981
33049
  origin: "local"
31982
33050
  });
31983
33051
  }
@@ -32017,17 +33085,18 @@ var WsClient = class {
32017
33085
  this.bus.push({ source: origin, kind: "out", text: resultBuf });
32018
33086
  }
32019
33087
  },
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
- })
33088
+ onAttemptStart: (label, index, total) => {
33089
+ const text = `\u2192 trying ${plan.cmd} profile "${label}" (${index + 1}/${total})\u2026`;
33090
+ this.bus.push({ source: origin, kind: "log", text });
33091
+ this.send(WsChannels.COMMAND_OUTPUT, { commandId, seq: seq++, chunk: `${text}
33092
+ `, log: true });
33093
+ },
33094
+ onAttemptFail: (label, reason) => {
33095
+ const text = reason === "limit" ? `profile "${label}" hit its session limit \u2014 trying next` : `profile "${label}" failed to authenticate \u2014 trying next`;
33096
+ this.bus.push({ source: origin, kind: "log", text, level: "warn" });
33097
+ this.send(WsChannels.COMMAND_OUTPUT, { commandId, seq: seq++, chunk: `${text}
33098
+ `, log: true });
33099
+ }
32031
33100
  });
32032
33101
  this.bus.endBusy(plan.cmd);
32033
33102
  const profileLabel = result.workedDir ? profileDisplayLabel(result.workedDir) : null;
@@ -32050,6 +33119,15 @@ var WsClient = class {
32050
33119
  status: result.exitCode === 0 ? "done" : "failed",
32051
33120
  exitCode: result.exitCode,
32052
33121
  tokens: result.usage.tokens,
33122
+ // The ai_tokens split for this run (ADR-0145); attached only when we metered real tokens.
33123
+ ...result.usage.tokens > 0 && {
33124
+ tokensBreakdown: {
33125
+ input: result.usage.input,
33126
+ output: result.usage.output,
33127
+ cacheRead: result.usage.cacheRead,
33128
+ cacheCreation: result.usage.cacheCreation
33129
+ }
33130
+ },
32053
33131
  startedAt,
32054
33132
  finishedAt: (/* @__PURE__ */ new Date()).toISOString()
32055
33133
  });
@@ -32058,7 +33136,16 @@ var WsClient = class {
32058
33136
  { metric: UsageMetric.COMMANDS, amount: 1, occurredAt }
32059
33137
  ];
32060
33138
  if (result.exitCode === 0 && result.usage.tokens > 0) {
32061
- events.push({ metric: UsageMetric.AI_TOKENS, amount: result.usage.tokens, occurredAt });
33139
+ events.push({
33140
+ metric: UsageMetric.AI_TOKENS,
33141
+ amount: result.usage.tokens,
33142
+ occurredAt,
33143
+ // The ai_tokens split so the server can persist + display it (ADR-0145).
33144
+ inputTokens: result.usage.input,
33145
+ outputTokens: result.usage.output,
33146
+ cacheReadTokens: result.usage.cacheRead,
33147
+ cacheCreationTokens: result.usage.cacheCreation
33148
+ });
32062
33149
  this.bus.addTokens?.(result.usage.tokens);
32063
33150
  }
32064
33151
  this.reportUsage(events, profileLabel);
@@ -32093,7 +33180,7 @@ var WsClient = class {
32093
33180
  * without spawning: surface it in the transcript + close the command on the server with a
32094
33181
  * PROMPT_TOKEN_LIMIT_EXCEEDED note (exit 1). No usage is metered (nothing ran).
32095
33182
  */
32096
- rejectPromptOverLimit(commandId, origin, estimated, limit) {
33183
+ rejectPromptOverLimit(commandId, origin, prompt, estimated, limit) {
32097
33184
  const message = `[PROMPT_TOKEN_LIMIT_EXCEEDED] Prompt (~${estimated} tokens) exceeds the project per-prompt limit of ${limit} tokens.`;
32098
33185
  logger.warn("command.ai.prompt-limit", { commandId, origin, estimated, limit });
32099
33186
  this.bus.push({ source: origin, kind: "log", text: message, level: "error" });
@@ -32102,6 +33189,7 @@ var WsClient = class {
32102
33189
  commandId,
32103
33190
  cmd: "claude",
32104
33191
  args: [],
33192
+ prompt,
32105
33193
  origin: "local"
32106
33194
  });
32107
33195
  }
@@ -32119,7 +33207,7 @@ var WsClient = class {
32119
33207
  * verdict + close the command on the server. `reasons` contains only violation categories
32120
33208
  * (never secret values); a "unavailable" reason maps to no-outbound-available.
32121
33209
  */
32122
- rejectByReview(commandId, origin, reasons) {
33210
+ rejectByReview(commandId, origin, prompt, reasons) {
32123
33211
  const unavailable = reasons.includes("unavailable");
32124
33212
  const code = unavailable ? "OUTBOUND_REVIEW_UNAVAILABLE" : "OUTBOUND_REVIEW_REJECTED";
32125
33213
  const detail = reasons.filter((r) => r !== "unavailable").join(", ");
@@ -32131,6 +33219,7 @@ var WsClient = class {
32131
33219
  commandId,
32132
33220
  cmd: "claude",
32133
33221
  args: [],
33222
+ prompt,
32134
33223
  origin: "local"
32135
33224
  });
32136
33225
  }
@@ -32164,12 +33253,105 @@ var WsClient = class {
32164
33253
  };
32165
33254
  this.socket.send(JSON.stringify(envelope));
32166
33255
  }
33256
+ /**
33257
+ * Console sync (ADR-0150): emit one incremental `console.sync` event (add/update/clear) — only
33258
+ * while a web Console viewer is attached (`consoleWatching`); each carries the next `rev` so the
33259
+ * web detects a gap. Dropped when disconnected — the next snapshot on (re)attach re-syncs.
33260
+ */
33261
+ emitConsole(ev) {
33262
+ if (!this.consoleWatching) return;
33263
+ this.send(WsChannels.CONSOLE_SYNC, { ...ev, rev: ++this.consoleRev });
33264
+ }
33265
+ /**
33266
+ * A `/clear` (or idle auto-clear) wiped the transcript (ADR-0150). Emit it as an absolute empty
33267
+ * **snapshot** rather than an incremental `clear` event: a snapshot is applied unconditionally on
33268
+ * the web (no `rev`-gap check), so the clear can't be silently dropped when the web's revision
33269
+ * drifted during a reconnect flap. No-op while unwatched — the next `console.watch` re-arm
33270
+ * snapshots the (now empty) buffer anyway. The history is already emptied before this fires, so
33271
+ * the snapshot carries no entries.
33272
+ */
33273
+ emitConsoleClear() {
33274
+ if (!this.consoleWatching) return;
33275
+ this.sendConsoleSnapshot();
33276
+ }
33277
+ /**
33278
+ * Show the reconnect-backoff status as a SINGLE transcript line for the current outage
33279
+ * (approach B): create it on the first failed attempt, then update it in place on every retry —
33280
+ * so an outage adds one line instead of an error + a "reconnecting in Ns" line per attempt
33281
+ * (which flooded the TUI and, on the next snapshot, the web Console).
33282
+ */
33283
+ showReconnectStatus(detail, waitSec, attempt) {
33284
+ const text = `\u27F3 ${detail} \u2014 reconnecting in ${waitSec}s (attempt ${attempt}, /reconnect to retry now)`;
33285
+ if (this.reconnectEntryId) {
33286
+ this.bus.updateEntry(this.reconnectEntryId, text, "warn");
33287
+ } else {
33288
+ this.reconnectEntryId = this.bus.push({ source: "system", kind: "log", text, level: "warn" });
33289
+ }
33290
+ }
33291
+ /**
33292
+ * A session became ready (hello_ack): finalize any in-flight reconnect line in place to the
33293
+ * "connected" line (so the outage's single line resolves cleanly), or — on a first connect with
33294
+ * no prior outage — push a fresh connected line.
33295
+ */
33296
+ finishReconnect() {
33297
+ const msg = "\u2714 Connected to the server (session encryption ready).";
33298
+ if (this.reconnectEntryId) {
33299
+ this.bus.updateEntry(this.reconnectEntryId, msg, "info");
33300
+ this.reconnectEntryId = null;
33301
+ } else {
33302
+ this.bus.log(msg);
33303
+ }
33304
+ }
33305
+ /** Send a full transcript snapshot (on attach + the periodic self-heal — ADR-0150). */
33306
+ sendConsoleSnapshot() {
33307
+ const entries2 = this.bus.snapshot().map(toDtoEntry);
33308
+ this.send(WsChannels.CONSOLE_SYNC, {
33309
+ kind: "snapshot",
33310
+ rev: ++this.consoleRev,
33311
+ entries: entries2
33312
+ });
33313
+ }
33314
+ /**
33315
+ * Handle `console.watch` (ADR-0150) — a renewable lease. `on` renews it: start mirroring on the
33316
+ * first assertion (fresh snapshot + arm the periodic snapshot); later renewals just extend the
33317
+ * lease (no re-snapshot). No renewal within `CONSOLE_WATCH_LEASE_MS` ⇒ the lease expires and
33318
+ * syncing stops. An explicit `on:false` stops immediately.
33319
+ */
33320
+ setConsoleWatching(on) {
33321
+ if (!on) {
33322
+ this.stopConsoleSync();
33323
+ return;
33324
+ }
33325
+ if (this.consoleWatchExpiry) clearTimeout(this.consoleWatchExpiry);
33326
+ this.consoleWatchExpiry = setTimeout(() => this.stopConsoleSync(), CONSOLE_WATCH_LEASE_MS);
33327
+ this.consoleWatchExpiry.unref?.();
33328
+ if (this.consoleWatching) return;
33329
+ this.consoleWatching = true;
33330
+ this.sendConsoleSnapshot();
33331
+ this.consoleSnapshotTimer = setInterval(
33332
+ () => this.sendConsoleSnapshot(),
33333
+ CONSOLE_SNAPSHOT_INTERVAL_MS
33334
+ );
33335
+ this.consoleSnapshotTimer.unref?.();
33336
+ }
33337
+ /** Stop mirroring the transcript (lease expired / viewer left / reconnect) — clears both timers. */
33338
+ stopConsoleSync() {
33339
+ this.consoleWatching = false;
33340
+ if (this.consoleSnapshotTimer) {
33341
+ clearInterval(this.consoleSnapshotTimer);
33342
+ this.consoleSnapshotTimer = null;
33343
+ }
33344
+ if (this.consoleWatchExpiry) {
33345
+ clearTimeout(this.consoleWatchExpiry);
33346
+ this.consoleWatchExpiry = null;
33347
+ }
33348
+ }
32167
33349
  /**
32168
33350
  * Send a cli → server request and await the reply by envelope id (15s timeout).
32169
33351
  * Used for quota.check before spawning an AI cli (ADR-0020).
32170
33352
  */
32171
33353
  request(channel, data) {
32172
- return new Promise((resolve6, reject) => {
33354
+ return new Promise((resolve8, reject) => {
32173
33355
  if (!this.socket || !this.sessionKey) {
32174
33356
  reject(new Error("Not connected to the server."));
32175
33357
  return;
@@ -32189,7 +33371,7 @@ var WsClient = class {
32189
33371
  }, 15e3);
32190
33372
  timer.unref?.();
32191
33373
  this.pending.set(envelope.id, {
32192
- resolve: (value) => resolve6(value),
33374
+ resolve: (value) => resolve8(value),
32193
33375
  timer
32194
33376
  });
32195
33377
  this.socket.send(JSON.stringify(envelope));
@@ -32236,7 +33418,7 @@ var WsClient = class {
32236
33418
  /** Create the physic folder at an absolute path if missing (best-effort). */
32237
33419
  ensurePhysicFolderPath(folder) {
32238
33420
  try {
32239
- if (!existsSync7(folder)) {
33421
+ if (!existsSync9(folder)) {
32240
33422
  mkdirSync4(folder, { recursive: true });
32241
33423
  this.bus.log(`Created physic project folder: ${folder}`);
32242
33424
  }
@@ -32251,7 +33433,7 @@ var WsClient = class {
32251
33433
  */
32252
33434
  physicFolderPath(projectName) {
32253
33435
  const safe = projectName.replace(/[/\\]/g, "_").replace(/\.\./g, "_").trim();
32254
- return safe ? join13(this.context.profileDir, safe) : null;
33436
+ return safe ? join19(this.context.profileDir, safe) : null;
32255
33437
  }
32256
33438
  /**
32257
33439
  * Report machine.status: version + the state of the physic project being served
@@ -32265,8 +33447,9 @@ var WsClient = class {
32265
33447
  projects: [],
32266
33448
  version: CLI_VERSION,
32267
33449
  physicPath,
32268
- physicPathExists: physicPath ? existsSync7(physicPath) : void 0,
32269
- autonomousRunning: false
33450
+ physicPathExists: physicPath ? existsSync9(physicPath) : void 0,
33451
+ // Real coarse state (ADR-0152): !paused and a recent tick (was hard-coded false).
33452
+ autonomousRunning: physicPath ? isAutonomousRunning(physicPath) : false
32270
33453
  });
32271
33454
  }
32272
33455
  /** Heartbeat every 30s: ping + periodically re-report machine.status. */
@@ -32299,7 +33482,7 @@ var WsClient = class {
32299
33482
  const working = getWorkingProfile(this.context.profileDir, aiCli2);
32300
33483
  const configured = claudeHomeDirs(config);
32301
33484
  const dirs = working ? [working, ...configured.filter((d) => d !== working)] : configured;
32302
- const profile = working ? basename3(working) : dirs[0] ? basename3(dirs[0]) : "default";
33485
+ const profile = working ? basename4(working) : dirs[0] ? basename4(dirs[0]) : "default";
32303
33486
  const api = await checkClaudeUsage(dirs, aiCli2);
32304
33487
  const snapshot = api ?? {
32305
33488
  plan: "?",
@@ -32336,7 +33519,7 @@ var WsClient = class {
32336
33519
  */
32337
33520
  uploadLog() {
32338
33521
  try {
32339
- const upload = readLogUpload(join13(this.context.profileDir, "logs"));
33522
+ const upload = readLogUpload(join19(this.context.profileDir, "logs"));
32340
33523
  if (!upload) return;
32341
33524
  const sig = `${upload.fileName}:${upload.totalBytes}`;
32342
33525
  if (sig === this.lastLogSig) return;
@@ -32364,10 +33547,10 @@ var WsClient = class {
32364
33547
  };
32365
33548
 
32366
33549
  // 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";
33550
+ import { readFileSync as readFileSync14, rmSync as rmSync5, writeFileSync as writeFileSync6 } from "fs";
33551
+ import { join as join20 } from "path";
32369
33552
  function lockPath(profileDir2) {
32370
- return join14(profileDir2, "start.lock");
33553
+ return join20(profileDir2, "start.lock");
32371
33554
  }
32372
33555
  function isAlive(pid) {
32373
33556
  try {
@@ -32379,7 +33562,7 @@ function isAlive(pid) {
32379
33562
  }
32380
33563
  function lockHolder(profileDir2) {
32381
33564
  try {
32382
- const pid = Number.parseInt(readFileSync12(lockPath(profileDir2), "utf8").trim(), 10);
33565
+ const pid = Number.parseInt(readFileSync14(lockPath(profileDir2), "utf8").trim(), 10);
32383
33566
  if (Number.isInteger(pid) && pid > 0 && isAlive(pid)) return pid;
32384
33567
  } catch {
32385
33568
  }
@@ -32392,7 +33575,7 @@ function acquireInstanceLock(profileDir2) {
32392
33575
  }
32393
33576
  function releaseInstanceLock(profileDir2) {
32394
33577
  try {
32395
- const pid = Number.parseInt(readFileSync12(lockPath(profileDir2), "utf8").trim(), 10);
33578
+ const pid = Number.parseInt(readFileSync14(lockPath(profileDir2), "utf8").trim(), 10);
32396
33579
  if (pid === process.pid) rmSync5(lockPath(profileDir2), { force: true });
32397
33580
  } catch {
32398
33581
  }
@@ -32442,14 +33625,33 @@ var SessionBus = class {
32442
33625
  return entry.id;
32443
33626
  }
32444
33627
  /**
32445
- * Producer → consumers: replace the text of an already-pushed entry (ADR-0108) — used to
32446
- * grow a streaming `result` block in place without pushing a new line per chunk. No-op if the
32447
- * entry has aged out of the replay window.
33628
+ * Wipe the transcript (`/clear` + idle auto-clear — ADR-0057): empty the replay history and
33629
+ * notify consumers so the TUI clears the screen and the console-sync emitter (ADR-0150) sends
33630
+ * a `clear` event. The current snapshot afterwards is empty (until new lines arrive).
32448
33631
  */
32449
- updateEntry(id, text) {
33632
+ clear() {
33633
+ this.history.length = 0;
33634
+ this.emitter.emit("clear");
33635
+ }
33636
+ /** Subscribe to transcript clears (TUI screen wipe + console.sync `clear` — ADR-0150). */
33637
+ onClear(fn) {
33638
+ this.emitter.on("clear", fn);
33639
+ return () => this.emitter.off("clear", fn);
33640
+ }
33641
+ /** The current transcript entries (a copy) — the console-sync snapshot source (ADR-0150). */
33642
+ snapshot() {
33643
+ return [...this.history];
33644
+ }
33645
+ /**
33646
+ * Producer → consumers: replace the text (and optionally the level) of an already-pushed entry
33647
+ * (ADR-0108) — used to grow a streaming `result` block in place, or to update a single reconnect
33648
+ * status line without pushing a new line per attempt. No-op if the entry has aged out.
33649
+ */
33650
+ updateEntry(id, text, level) {
32450
33651
  const entry = this.history.find((e) => e.id === id);
32451
33652
  if (!entry) return;
32452
33653
  entry.text = text;
33654
+ if (level) entry.level = level;
32453
33655
  this.emitter.emit("transcript-update", entry);
32454
33656
  }
32455
33657
  /** Subscribe to in-place entry updates (a `result` block growing — ADR-0108). */
@@ -35658,7 +36860,7 @@ var styles_default = styles2;
35658
36860
  // ../../node_modules/.pnpm/ink@5.2.1_@types+react@18.3.31_react@18.3.1/node_modules/ink/build/reconciler.js
35659
36861
  if (process3.env["DEV"] === "true") {
35660
36862
  try {
35661
- await import("./devtools-JZIQKAXX.js");
36863
+ await import("./devtools-ZL4ACXHE.js");
35662
36864
  } catch (error) {
35663
36865
  if (error.code === "ERR_MODULE_NOT_FOUND") {
35664
36866
  console.warn(`
@@ -37876,8 +39078,8 @@ var Ink = class {
37876
39078
  }
37877
39079
  }
37878
39080
  async waitUntilExit() {
37879
- this.exitPromise ||= new Promise((resolve6, reject) => {
37880
- this.resolveExitPromise = resolve6;
39081
+ this.exitPromise ||= new Promise((resolve8, reject) => {
39082
+ this.resolveExitPromise = resolve8;
37881
39083
  this.rejectExitPromise = reject;
37882
39084
  });
37883
39085
  return this.exitPromise;
@@ -38244,17 +39446,17 @@ var import_react21 = __toESM(require_react(), 1);
38244
39446
  var import_react26 = __toESM(require_react(), 1);
38245
39447
 
38246
39448
  // 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";
39449
+ import { existsSync as existsSync11, readFileSync as readFileSync16, writeFileSync as writeFileSync7 } from "fs";
39450
+ import { join as join21 } from "path";
38249
39451
  var MAX_HISTORY = 200;
38250
39452
  function historyPath(profileDir2) {
38251
- return join15(profileDir2, "input-history.json");
39453
+ return join21(profileDir2, "input-history.json");
38252
39454
  }
38253
39455
  function loadInputHistory(profileDir2) {
38254
39456
  const path = historyPath(profileDir2);
38255
- if (!existsSync9(path)) return [];
39457
+ if (!existsSync11(path)) return [];
38256
39458
  try {
38257
- const data = JSON.parse(readFileSync14(path, "utf8"));
39459
+ const data = JSON.parse(readFileSync16(path, "utf8"));
38258
39460
  return Array.isArray(data) ? data.filter((x) => typeof x === "string") : [];
38259
39461
  } catch {
38260
39462
  return [];
@@ -38514,8 +39716,8 @@ function TranscriptLine({ line }) {
38514
39716
  var import_react24 = __toESM(require_react(), 1);
38515
39717
 
38516
39718
  // src/ui/slash-commands.ts
38517
- import { existsSync as existsSync10 } from "fs";
38518
- import { join as join16 } from "path";
39719
+ import { existsSync as existsSync12 } from "fs";
39720
+ import { join as join22 } from "path";
38519
39721
  var CONFIG_KEYS = [
38520
39722
  "aiCli",
38521
39723
  "claudeHome",
@@ -38527,7 +39729,7 @@ var CONFIG_KEYS = [
38527
39729
  "aiEnv"
38528
39730
  ];
38529
39731
  function configPath(profileDir2) {
38530
- return join16(profileDir2, "config.json");
39732
+ return join22(profileDir2, "config.json");
38531
39733
  }
38532
39734
  function parseConfigAssignment(key, valueTokens) {
38533
39735
  const value = valueTokens.join(" ").trim();
@@ -38577,7 +39779,7 @@ function runConfig(ctx) {
38577
39779
  ctx.print(`\u2714 ${verb} default config: ${path}`);
38578
39780
  ctx.print("add claude profiles: /config set claudeHome .claude .claude-1 (tried in order)");
38579
39781
  };
38580
- if (existsSync10(path)) {
39782
+ if (existsSync12(path)) {
38581
39783
  ctx.confirm(
38582
39784
  `config exists: ${path} \u2014 replace it with defaults? (y/N)`,
38583
39785
  () => writeDefaults("replaced with")
@@ -38662,7 +39864,7 @@ function runOutput(ctx) {
38662
39864
  }
38663
39865
  function runLogs(ctx) {
38664
39866
  const n = Math.max(1, Math.min(200, Number(ctx.args[0]) || 20));
38665
- const dir2 = join16(ctx.info.profileDir, "logs");
39867
+ const dir2 = join22(ctx.info.profileDir, "logs");
38666
39868
  const lines = readRecentLogLines(dir2, n);
38667
39869
  if (lines.length === 0) {
38668
39870
  ctx.print("No log lines yet.");
@@ -38986,11 +40188,29 @@ function App2({
38986
40188
  const [history, setHistory] = (0, import_react26.useState)(() => loadInputHistory(info.profileDir));
38987
40189
  const [startedAt] = (0, import_react26.useState)(() => Date.now());
38988
40190
  const [pendingConfirm, setPendingConfirm] = (0, import_react26.useState)(null);
40191
+ const [autoClearIdleMs] = (0, import_react26.useState)(() => {
40192
+ const min = readProfileConfig(info.profileDir).autoClearIdleMinutes ?? 10;
40193
+ return min > 0 ? min * 6e4 : 0;
40194
+ });
40195
+ const lastActivityRef = (0, import_react26.useRef)(startedAt);
40196
+ const busyRef = (0, import_react26.useRef)(busy);
40197
+ busyRef.current = busy;
40198
+ const entriesLenRef = (0, import_react26.useRef)(entries2.length);
40199
+ entriesLenRef.current = entries2.length;
40200
+ const pendingConfirmRef = (0, import_react26.useRef)(pendingConfirm);
40201
+ pendingConfirmRef.current = pendingConfirm;
40202
+ const clearTranscript = (0, import_react26.useCallback)(() => {
40203
+ setEntries([]);
40204
+ setScrollBack(0);
40205
+ stdout.write("\x1B[2J\x1B[3J\x1B[H");
40206
+ bus.clear();
40207
+ }, [stdout, bus]);
38989
40208
  (0, import_react26.useEffect)(() => {
38990
40209
  const offTranscript = bus.onTranscript((entry) => {
38991
40210
  setEntries(
38992
40211
  (prev) => prev.length >= ENTRY_CAP ? [...prev.slice(prev.length - ENTRY_CAP + 1), entry] : [...prev, entry]
38993
40212
  );
40213
+ if (entry.source !== "system") lastActivityRef.current = Date.now();
38994
40214
  if (entry.kind === "aireq" || entry.kind === "aires") setExpanded(/* @__PURE__ */ new Set());
38995
40215
  });
38996
40216
  const offUpdate = bus.onTranscriptUpdate(
@@ -39017,6 +40237,21 @@ function App2({
39017
40237
  offTokens();
39018
40238
  };
39019
40239
  }, [bus]);
40240
+ (0, import_react26.useEffect)(() => {
40241
+ if (autoClearIdleMs <= 0) return;
40242
+ const timer = setInterval(() => {
40243
+ if (busyRef.current !== null || pendingConfirmRef.current || entriesLenRef.current === 0) return;
40244
+ if (Date.now() - lastActivityRef.current < autoClearIdleMs) return;
40245
+ clearTranscript();
40246
+ bus.push({
40247
+ source: "system",
40248
+ kind: "log",
40249
+ text: `Transcript auto-cleared after ${Math.round(autoClearIdleMs / 6e4)} min idle.`
40250
+ });
40251
+ lastActivityRef.current = Date.now();
40252
+ }, 6e4);
40253
+ return () => clearInterval(timer);
40254
+ }, [autoClearIdleMs, bus, clearTranscript]);
39020
40255
  const bodyHeight = Math.max(3, rows - CHROME_ROWS);
39021
40256
  const textWidth = Math.max(10, cols - 1 - 8);
39022
40257
  const lines = (0, import_react26.useMemo)(() => flattenEntries(entries2, textWidth, expanded), [entries2, textWidth, expanded]);
@@ -39042,6 +40277,7 @@ function App2({
39042
40277
  };
39043
40278
  const handleSubmit = (input) => {
39044
40279
  setScrollBack(0);
40280
+ lastActivityRef.current = Date.now();
39045
40281
  const trimmed = input.trim();
39046
40282
  if (pendingConfirm) {
39047
40283
  const { onYes } = pendingConfirm;
@@ -39057,11 +40293,7 @@ function App2({
39057
40293
  runSlashCommand(trimmed, {
39058
40294
  info,
39059
40295
  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
- },
40296
+ clear: clearTranscript,
39065
40297
  quit: () => exit(),
39066
40298
  confirm: (prompt, onYes) => {
39067
40299
  bus.push({ source: "local", kind: "log", text: prompt });
@@ -39133,7 +40365,7 @@ async function runStart(profileDir2, profileName) {
39133
40365
  return;
39134
40366
  }
39135
40367
  logger.configure({
39136
- dir: join17(profileDir2, "logs"),
40368
+ dir: join23(profileDir2, "logs"),
39137
40369
  level: resolveLogLevel(),
39138
40370
  base: { version: CLI_VERSION, profile: profileName, scope: credential.scope }
39139
40371
  });
@@ -39153,7 +40385,7 @@ async function runStart(profileDir2, profileName) {
39153
40385
  if (result.action === "updated") {
39154
40386
  logger.info("update.updated", { version: result.version });
39155
40387
  console.log("Restarting with the new version\u2026");
39156
- const child = spawn4(process.execPath, process.argv.slice(1), {
40388
+ const child = spawn6(process.execPath, process.argv.slice(1), {
39157
40389
  stdio: "inherit",
39158
40390
  env: { ...process.env, FOURPM_NO_UPDATE: "1" }
39159
40391
  });
@@ -39191,7 +40423,7 @@ async function runStart(profileDir2, profileName) {
39191
40423
  logger.error("ws.run.crash", { message: e.message, stack: e.stack });
39192
40424
  if (client.isStopped) return;
39193
40425
  bus.log(`Recovered from an internal error \u2014 reconnecting: ${e.message}`, "error");
39194
- await new Promise((resolve6) => setTimeout(resolve6, 1e3));
40426
+ await new Promise((resolve8) => setTimeout(resolve8, 1e3));
39195
40427
  }
39196
40428
  }
39197
40429
  };
@@ -39318,10 +40550,10 @@ function Picker({
39318
40550
  ] });
39319
40551
  }
39320
40552
  function selectFromList(items, title) {
39321
- return new Promise((resolve6) => {
40553
+ return new Promise((resolve8) => {
39322
40554
  let chosen = null;
39323
40555
  const app = render_default(/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Picker, { items, title, onDone: (v) => chosen = v }));
39324
- void app.waitUntilExit().then(() => resolve6(chosen));
40556
+ void app.waitUntilExit().then(() => resolve8(chosen));
39325
40557
  });
39326
40558
  }
39327
40559