@4pm/cli 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +7 -7
  2. package/dist/index.js +998 -228
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1353,7 +1353,7 @@ var require_react_development = __commonJS({
1353
1353
  var dispatcher = resolveDispatcher();
1354
1354
  return dispatcher.useReducer(reducer, initialArg, init);
1355
1355
  }
1356
- function useRef(initialValue) {
1356
+ function useRef2(initialValue) {
1357
1357
  var dispatcher = resolveDispatcher();
1358
1358
  return dispatcher.useRef(initialValue);
1359
1359
  }
@@ -1369,7 +1369,7 @@ var require_react_development = __commonJS({
1369
1369
  var dispatcher = resolveDispatcher();
1370
1370
  return dispatcher.useLayoutEffect(create2, deps);
1371
1371
  }
1372
- function useCallback(callback, deps) {
1372
+ function useCallback2(callback, deps) {
1373
1373
  var dispatcher = resolveDispatcher();
1374
1374
  return dispatcher.useCallback(callback, deps);
1375
1375
  }
@@ -1995,14 +1995,14 @@ var require_react_development = __commonJS({
1995
1995
  var thenableResult = result;
1996
1996
  var wasAwaited = false;
1997
1997
  var thenable = {
1998
- then: function(resolve6, reject) {
1998
+ then: function(resolve7, reject) {
1999
1999
  wasAwaited = true;
2000
2000
  thenableResult.then(function(returnValue2) {
2001
2001
  popActScope(prevActScopeDepth);
2002
2002
  if (actScopeDepth === 0) {
2003
- recursivelyFlushAsyncActWork(returnValue2, resolve6, reject);
2003
+ recursivelyFlushAsyncActWork(returnValue2, resolve7, reject);
2004
2004
  } else {
2005
- resolve6(returnValue2);
2005
+ resolve7(returnValue2);
2006
2006
  }
2007
2007
  }, function(error2) {
2008
2008
  popActScope(prevActScopeDepth);
@@ -2032,20 +2032,20 @@ var require_react_development = __commonJS({
2032
2032
  ReactCurrentActQueue.current = null;
2033
2033
  }
2034
2034
  var _thenable = {
2035
- then: function(resolve6, reject) {
2035
+ then: function(resolve7, reject) {
2036
2036
  if (ReactCurrentActQueue.current === null) {
2037
2037
  ReactCurrentActQueue.current = [];
2038
- recursivelyFlushAsyncActWork(returnValue, resolve6, reject);
2038
+ recursivelyFlushAsyncActWork(returnValue, resolve7, reject);
2039
2039
  } else {
2040
- resolve6(returnValue);
2040
+ resolve7(returnValue);
2041
2041
  }
2042
2042
  }
2043
2043
  };
2044
2044
  return _thenable;
2045
2045
  } else {
2046
2046
  var _thenable2 = {
2047
- then: function(resolve6, reject) {
2048
- resolve6(returnValue);
2047
+ then: function(resolve7, reject) {
2048
+ resolve7(returnValue);
2049
2049
  }
2050
2050
  };
2051
2051
  return _thenable2;
@@ -2061,7 +2061,7 @@ var require_react_development = __commonJS({
2061
2061
  actScopeDepth = prevActScopeDepth;
2062
2062
  }
2063
2063
  }
2064
- function recursivelyFlushAsyncActWork(returnValue, resolve6, reject) {
2064
+ function recursivelyFlushAsyncActWork(returnValue, resolve7, reject) {
2065
2065
  {
2066
2066
  var queue = ReactCurrentActQueue.current;
2067
2067
  if (queue !== null) {
@@ -2070,16 +2070,16 @@ var require_react_development = __commonJS({
2070
2070
  enqueueTask(function() {
2071
2071
  if (queue.length === 0) {
2072
2072
  ReactCurrentActQueue.current = null;
2073
- resolve6(returnValue);
2073
+ resolve7(returnValue);
2074
2074
  } else {
2075
- recursivelyFlushAsyncActWork(returnValue, resolve6, reject);
2075
+ recursivelyFlushAsyncActWork(returnValue, resolve7, reject);
2076
2076
  }
2077
2077
  });
2078
2078
  } catch (error2) {
2079
2079
  reject(error2);
2080
2080
  }
2081
2081
  } else {
2082
- resolve6(returnValue);
2082
+ resolve7(returnValue);
2083
2083
  }
2084
2084
  }
2085
2085
  }
@@ -2136,7 +2136,7 @@ var require_react_development = __commonJS({
2136
2136
  exports.memo = memo;
2137
2137
  exports.startTransition = startTransition;
2138
2138
  exports.unstable_act = act;
2139
- exports.useCallback = useCallback;
2139
+ exports.useCallback = useCallback2;
2140
2140
  exports.useContext = useContext7;
2141
2141
  exports.useDebugValue = useDebugValue;
2142
2142
  exports.useDeferredValue = useDeferredValue;
@@ -2147,7 +2147,7 @@ var require_react_development = __commonJS({
2147
2147
  exports.useLayoutEffect = useLayoutEffect2;
2148
2148
  exports.useMemo = useMemo4;
2149
2149
  exports.useReducer = useReducer;
2150
- exports.useRef = useRef;
2150
+ exports.useRef = useRef2;
2151
2151
  exports.useState = useState6;
2152
2152
  exports.useSyncExternalStore = useSyncExternalStore;
2153
2153
  exports.useTransition = useTransition;
@@ -24469,6 +24469,28 @@ function profileDir(name) {
24469
24469
  mkdirSync(dir2, { recursive: true });
24470
24470
  return dir2;
24471
24471
  }
24472
+ function defaultProfileConfig() {
24473
+ return {
24474
+ aiCli: "claude",
24475
+ // Lists so failover across profiles is discoverable (ADR-0057) — add more objects.
24476
+ // `args` are the full per-CLI defaults (claude stream-json for metering — ADR-0072).
24477
+ // `model` = the CLI's `--model` flag; default "sonnet" — the balanced mid-price tier
24478
+ // (cheaper than opus, more capable than haiku). "" ⇒ the CLI's own default model.
24479
+ claudeHome: [
24480
+ { profile: ".claude", args: ["-p", "--output-format", "stream-json", "--verbose"], model: "sonnet" }
24481
+ ],
24482
+ codexHome: [{ profile: ".codex", args: ["exec"], model: "" }],
24483
+ antigravityHome: [],
24484
+ autoUpdate: true,
24485
+ commandHistoryUploadMinutes: 10,
24486
+ autoClearIdleMinutes: 10
24487
+ };
24488
+ }
24489
+ function ensureProfileConfig(dir2) {
24490
+ if (existsSync(join(dir2, "config.json"))) return false;
24491
+ writeProfileConfig(dir2, defaultProfileConfig());
24492
+ return true;
24493
+ }
24472
24494
  function readProfileConfig(dir2) {
24473
24495
  const path = join(dir2, "config.json");
24474
24496
  if (!existsSync(path)) return {};
@@ -24483,6 +24505,10 @@ function writeProfileConfig(dir2, patch) {
24483
24505
  writeFileSync(join(dir2, "config.json"), JSON.stringify(merged, null, 2), "utf8");
24484
24506
  return merged;
24485
24507
  }
24508
+ function overwriteProfileConfig(dir2, config) {
24509
+ writeFileSync(join(dir2, "config.json"), JSON.stringify(config, null, 2), "utf8");
24510
+ return config;
24511
+ }
24486
24512
  function deleteProfileConfigKey(dir2, key) {
24487
24513
  const config = readProfileConfig(dir2);
24488
24514
  delete config[key];
@@ -24638,15 +24664,17 @@ async function runLink(serverUrl, explicitProfile) {
24638
24664
  );
24639
24665
  const profileName = explicitProfile ?? sanitizeProfileName(username);
24640
24666
  const existed = readCredential(profileDir(profileName), profileName) != null;
24641
- writeCredential(profileDir(profileName), {
24667
+ const dir2 = profileDir(profileName);
24668
+ writeCredential(dir2, {
24642
24669
  serverUrl,
24643
24670
  hashcode3,
24644
24671
  scope,
24645
24672
  // decided by the server from the pairing user (ADR-0010); shown in the TUI
24646
24673
  pairedAt: (/* @__PURE__ */ new Date()).toISOString()
24647
24674
  });
24675
+ ensureProfileConfig(dir2);
24648
24676
  if (projectName && scope === "project") {
24649
- const folder = join3(profileDir(profileName), projectName);
24677
+ const folder = join3(dir2, projectName);
24650
24678
  mkdirSync2(folder, { recursive: true });
24651
24679
  console.log(`\u2714 Created physic project folder: ${folder}`);
24652
24680
  }
@@ -24663,8 +24691,8 @@ async function runLink(serverUrl, explicitProfile) {
24663
24691
  }
24664
24692
 
24665
24693
  // src/commands/start.ts
24666
- import { spawn as spawn4 } from "child_process";
24667
- import { join as join17 } from "path";
24694
+ import { spawn as spawn5 } from "child_process";
24695
+ import { join as join19 } from "path";
24668
24696
 
24669
24697
  // src/core/update.ts
24670
24698
  import { execSync as execSync2 } from "child_process";
@@ -24835,7 +24863,7 @@ async function checkAndUpdate(serverUrl, autoUpdate) {
24835
24863
 
24836
24864
  // src/core/ws-client.ts
24837
24865
  import { randomUUID } from "crypto";
24838
- import { basename as basename3, join as join13 } from "path";
24866
+ import { basename as basename3, join as join15 } from "path";
24839
24867
 
24840
24868
  // ../../22-packages/11-shared/51-utils/src/helpers.ts
24841
24869
  function backoffJitterMs(attempt, baseMs = 500, maxMs = 1e4) {
@@ -24938,6 +24966,12 @@ var PermissionCode = {
24938
24966
  PROJECT_AI_ASSIST: "project.ai_assist",
24939
24967
  PROJECT_MEMO_READ: "project.memo_read",
24940
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",
24941
24975
  // permission
24942
24976
  PERMISSION_LIST: "permission.list",
24943
24977
  PERMISSION_READ: "permission.read",
@@ -24963,6 +24997,9 @@ var PermissionCode = {
24963
24997
  // billing
24964
24998
  BILLING_READ: "billing.read",
24965
24999
  BILLING_MANAGE: "billing.manage",
25000
+ // org template files (ADR-0113) — ADMIN + PM manage the org's template library
25001
+ TEMPLATE_READ: "template.read",
25002
+ TEMPLATE_WRITE: "template.write",
24966
25003
  // integration (community-server — ADR-0058; served by @4pm/community)
24967
25004
  INTEGRATION_READ: "integration.read",
24968
25005
  INTEGRATION_CONNECT: "integration.connect",
@@ -25006,6 +25043,8 @@ var ROLE_DEFAULT_PERMISSIONS = {
25006
25043
  P.USER_INVITE,
25007
25044
  P.QUOTA_READ,
25008
25045
  P.QUOTA_MANAGE,
25046
+ P.TEMPLATE_READ,
25047
+ P.TEMPLATE_WRITE,
25009
25048
  P.TEAM_LIST,
25010
25049
  P.TEAM_CREATE,
25011
25050
  P.TEAM_READ,
@@ -25032,6 +25071,9 @@ var ROLE_DEFAULT_PERMISSIONS = {
25032
25071
  P.PROJECT_AI_ASSIST,
25033
25072
  P.PROJECT_MEMO_READ,
25034
25073
  P.PROJECT_MEMO_WRITE,
25074
+ P.PROJECT_GIT,
25075
+ P.PROJECT_GIT_WRITE,
25076
+ P.PROJECT_AUTONOMOUS,
25035
25077
  P.PERMISSION_LIST,
25036
25078
  P.PERMISSION_READ,
25037
25079
  P.MACHINE_PAIR,
@@ -25056,6 +25098,7 @@ var ROLE_DEFAULT_PERMISSIONS = {
25056
25098
  P.PROJECT_READ,
25057
25099
  P.PROJECT_MEMO_READ,
25058
25100
  P.PROJECT_MEMO_WRITE,
25101
+ P.PROJECT_GIT,
25059
25102
  P.INTEGRATION_READ
25060
25103
  ],
25061
25104
  [Role.QA]: [
@@ -25066,6 +25109,7 @@ var ROLE_DEFAULT_PERMISSIONS = {
25066
25109
  P.PROJECT_READ,
25067
25110
  P.PROJECT_MEMO_READ,
25068
25111
  P.PROJECT_MEMO_WRITE,
25112
+ P.PROJECT_GIT,
25069
25113
  P.INTEGRATION_READ
25070
25114
  ],
25071
25115
  [Role.BA]: [
@@ -25076,6 +25120,7 @@ var ROLE_DEFAULT_PERMISSIONS = {
25076
25120
  P.PROJECT_READ,
25077
25121
  P.PROJECT_MEMO_READ,
25078
25122
  P.PROJECT_MEMO_WRITE,
25123
+ P.PROJECT_GIT,
25079
25124
  P.INTEGRATION_READ
25080
25125
  ],
25081
25126
  [Role.MACHINE]: [
@@ -25083,6 +25128,11 @@ var ROLE_DEFAULT_PERMISSIONS = {
25083
25128
  P.PROJECT_READ,
25084
25129
  P.PROJECT_MEMO_READ,
25085
25130
  P.PROJECT_MEMO_WRITE,
25131
+ // git read + write so the autonomous merge cycle (auto-cycle.md) can run (ADR-0151)
25132
+ P.PROJECT_GIT,
25133
+ P.PROJECT_GIT_WRITE,
25134
+ // autonomous mode control on its own physic project (ADR-0152)
25135
+ P.PROJECT_AUTONOMOUS,
25086
25136
  // view one's own worker/cli + configure the physic project
25087
25137
  P.MACHINE_LIST,
25088
25138
  P.MACHINE_PHYSIC_MANAGE,
@@ -25110,7 +25160,10 @@ var PROJECT_SCOPE_CHECKED_CODES = [
25110
25160
  P.PROJECT_PAUSE,
25111
25161
  P.PROJECT_CLOSE,
25112
25162
  P.PROJECT_PAUSE_MEMBER,
25113
- P.PROJECT_AI_ASSIST
25163
+ P.PROJECT_AI_ASSIST,
25164
+ P.PROJECT_GIT,
25165
+ P.PROJECT_GIT_WRITE,
25166
+ P.PROJECT_AUTONOMOUS
25114
25167
  ];
25115
25168
  var SELF_SERVICE_CODES = [
25116
25169
  P.AUTH_LOGOUT,
@@ -25150,6 +25203,9 @@ var TEAM_GRANTABLE_CODES = [
25150
25203
  P.PROJECT_AI_ASSIST,
25151
25204
  P.PROJECT_MEMO_READ,
25152
25205
  P.PROJECT_MEMO_WRITE,
25206
+ P.PROJECT_GIT,
25207
+ P.PROJECT_GIT_WRITE,
25208
+ P.PROJECT_AUTONOMOUS,
25153
25209
  P.COMMAND_EXECUTE,
25154
25210
  P.COMMAND_READ,
25155
25211
  P.MACHINE_LIST,
@@ -25171,6 +25227,9 @@ var PROJECT_GRANTABLE_CODES = [
25171
25227
  P.PROJECT_AI_ASSIST,
25172
25228
  P.PROJECT_MEMO_READ,
25173
25229
  P.PROJECT_MEMO_WRITE,
25230
+ P.PROJECT_GIT,
25231
+ P.PROJECT_GIT_WRITE,
25232
+ P.PROJECT_AUTONOMOUS,
25174
25233
  P.COMMAND_EXECUTE,
25175
25234
  P.COMMAND_READ,
25176
25235
  P.MACHINE_PHYSIC_MANAGE
@@ -29351,16 +29410,30 @@ var resetPasswordRequestSchema = external_exports.object({
29351
29410
 
29352
29411
  // ../../22-packages/11-shared/31-dto/src/org.ts
29353
29412
  var CLI_SETTINGS_BOUNDS = {
29354
- reconnectMaxBackoffSec: { min: 5, max: 600 },
29413
+ reconnectMaxBackoffSec: { min: 5, max: 60 },
29355
29414
  autoUpdateHour: { min: 0, max: 23 }
29356
29415
  };
29416
+ var orgAliasSchema = external_exports.string().regex(/^([a-z0-9-]{3,40})?$/, "alias must be 3\u201340 chars of a\u2013z, 0\u20139, -");
29357
29417
  var updateOrgRequestSchema = external_exports.object({
29358
29418
  name: external_exports.string().min(1).max(100).optional(),
29419
+ // ADMIN-only login alias (ADR-0111); empty string clears it.
29420
+ alias: orgAliasSchema.optional(),
29359
29421
  settings: external_exports.record(external_exports.unknown()).optional()
29360
29422
  });
29361
29423
 
29362
29424
  // ../../22-packages/11-shared/31-dto/src/user.ts
29363
29425
  var AVATAR_MAX_BYTES = 2 * 1024 * 1024;
29426
+ var USAGE_ALERT_METRICS = ["session5h", "weekly", "monthlyTokens"];
29427
+ var usageAlertRuleSchema = external_exports.object({
29428
+ id: external_exports.string().min(1).max(64),
29429
+ metric: external_exports.enum(USAGE_ALERT_METRICS),
29430
+ threshold: external_exports.number().int().min(1),
29431
+ email: external_exports.union([emailSchema, external_exports.literal("")]).optional(),
29432
+ enabled: external_exports.boolean().default(true)
29433
+ });
29434
+ var userAlertsSchema = external_exports.object({
29435
+ rules: external_exports.array(usageAlertRuleSchema).max(20).default([])
29436
+ });
29364
29437
  var rolesSchema = external_exports.array(external_exports.enum(ALL_ROLES)).min(1).refine((roles) => isValidRoleSet(roles), {
29365
29438
  message: "ADMIN/MACHINE cannot be combined with other roles"
29366
29439
  });
@@ -29382,7 +29455,11 @@ var updateUserRequestSchema = external_exports.object({
29382
29455
  email: emailSchema.optional(),
29383
29456
  phone: phoneSchema.nullable().optional(),
29384
29457
  roles: rolesSchema.optional(),
29385
- ipAllowlist: ipAllowlistSchema.optional()
29458
+ ipAllowlist: ipAllowlistSchema.optional(),
29459
+ // Usage-alert rules (ADR-0110) — manager-only, like the IP allowlist.
29460
+ alerts: userAlertsSchema.optional(),
29461
+ // cli data retention days (ADR-0115) — manager-only; capped at the plan's retentionDays.
29462
+ cliRetentionDays: external_exports.number().int().min(0).max(3650).optional()
29386
29463
  });
29387
29464
  var setUserPasswordRequestSchema = external_exports.object({
29388
29465
  password: passwordSchema
@@ -29421,6 +29498,9 @@ var attachTeamUserRequestSchema = external_exports.object({
29421
29498
  isLead: external_exports.boolean().optional()
29422
29499
  });
29423
29500
 
29501
+ // ../../22-packages/11-shared/31-dto/src/template.ts
29502
+ var TEMPLATE_KINDS = ["report", "est", "wbs", "proposal", "feasibility"];
29503
+
29424
29504
  // ../../22-packages/11-shared/31-dto/src/project.ts
29425
29505
  var listProjectsQuerySchema = baseRequestSchema.extend({
29426
29506
  pinned: pinnedFilterSchema,
@@ -29435,6 +29515,9 @@ var createProjectRequestSchema = external_exports.object({
29435
29515
  description: external_exports.string().max(2e3).optional(),
29436
29516
  machineUserId: external_exports.string().uuid().optional()
29437
29517
  });
29518
+ var copyProjectRequestSchema = external_exports.object({
29519
+ name: external_exports.string().min(1).max(100)
29520
+ });
29438
29521
  var DEFAULT_OUTBOUND_RULES = {
29439
29522
  secret: [
29440
29523
  String.raw`/\b(api[_-]?key|secret|password|passwd|access[_-]?token|bearer)\b\s*[:=]\s*\S+/i`,
@@ -29465,7 +29548,15 @@ var updateProjectRequestSchema = external_exports.object({
29465
29548
  // Messenger retention in days (0 = keep forever) — manager only (ADR-0078).
29466
29549
  messengerRetentionDays: external_exports.number().int().min(0).max(3650).optional(),
29467
29550
  // Project settings (ADR-0081) — key-level shallow-merge; `tokens` clamped on read.
29468
- settings: external_exports.record(external_exports.unknown()).optional()
29551
+ settings: external_exports.record(external_exports.unknown()).optional(),
29552
+ // Identity ↔ spec sync (ADR-0148): the Settings tab edits the spec basic group
29553
+ // (id = spec slug, name, desc). The server merges these into `spec` (READY) or
29554
+ // `specDraft` (DRAFT) so the Content → Spec tab / wizard stays consistent.
29555
+ specBasic: external_exports.object({
29556
+ id: external_exports.string().max(200),
29557
+ name: external_exports.string().max(100),
29558
+ desc: external_exports.string().max(2e3)
29559
+ }).partial().optional()
29469
29560
  });
29470
29561
  var MEMO_MAX_LENGTH = 65536;
29471
29562
  var putMemoRequestSchema = external_exports.object({
@@ -29485,6 +29576,17 @@ var putSpecDraftRequestSchema = external_exports.object({
29485
29576
  spec: external_exports.record(external_exports.unknown()),
29486
29577
  specVersion: external_exports.number().int().positive().optional()
29487
29578
  });
29579
+ var putProjectSpecRequestSchema = external_exports.object({
29580
+ spec: external_exports.record(external_exports.unknown())
29581
+ });
29582
+ var ARTIFACT_MAX_BYTES = 512 * 1024;
29583
+ var listArtifactsQuerySchema = external_exports.object({
29584
+ kind: external_exports.enum(TEMPLATE_KINDS).optional()
29585
+ });
29586
+ var createArtifactRequestSchema = external_exports.object({
29587
+ kind: external_exports.enum(TEMPLATE_KINDS),
29588
+ content: external_exports.string().min(1).max(ARTIFACT_MAX_BYTES)
29589
+ });
29488
29590
  var repoSpecSchema = external_exports.object({
29489
29591
  /** Role label (e.g. "docs" / "web" / "server") — descriptive, not constrained. */
29490
29592
  role: external_exports.string().max(60).optional().default(""),
@@ -29556,10 +29658,26 @@ var createFromSpecRequestSchema = external_exports.object({
29556
29658
  });
29557
29659
  var addRepoRequestSchema = external_exports.object({
29558
29660
  projectId: external_exports.string().uuid(),
29559
- path: external_exports.string().min(1).max(500),
29560
- /** Repo to clone; empty ⇒ `git init` in place. */
29561
- repoUrl: external_exports.string().url().max(500).optional(),
29562
- provider: external_exports.enum(["gh", "glab"]).optional()
29661
+ repos: external_exports.array(repoSpecSchema).min(1)
29662
+ }).superRefine((v, ctx) => {
29663
+ const primaries = v.repos.filter((r) => r.primary).length;
29664
+ if (primaries !== 1) {
29665
+ ctx.addIssue({
29666
+ code: external_exports.ZodIssueCode.custom,
29667
+ path: ["repos"],
29668
+ message: "Exactly one repo must be primary"
29669
+ });
29670
+ }
29671
+ for (let i = 0; i < v.repos.length; i++) {
29672
+ const r = v.repos[i];
29673
+ if (r && r.op === "existing" && !r.url) {
29674
+ ctx.addIssue({
29675
+ code: external_exports.ZodIssueCode.custom,
29676
+ path: ["repos", i, "url"],
29677
+ message: "An existing repo requires a url"
29678
+ });
29679
+ }
29680
+ }
29563
29681
  });
29564
29682
 
29565
29683
  // ../../22-packages/11-shared/31-dto/src/activity.ts
@@ -29625,6 +29743,25 @@ var listMachineLinksQuerySchema = baseRequestSchema.extend({
29625
29743
  var updateMachineLinkRequestSchema = external_exports.object({
29626
29744
  hashcode3ExpiresAt: external_exports.string().datetime().nullable()
29627
29745
  });
29746
+ var configWriteRequestSchema = external_exports.object({
29747
+ config: external_exports.string().max(256 * 1024)
29748
+ });
29749
+ var FILE_WRITE_MAX_BYTES = 2 * 1024 * 1024;
29750
+ var fsWriteRequestSchema = external_exports.object({
29751
+ /** Path relative to the physic-project root; a `..` escaping the root is rejected on the cli. */
29752
+ path: external_exports.string().min(1).max(1024),
29753
+ /** Full new UTF-8 content (replaces the file wholesale). */
29754
+ content: external_exports.string().max(FILE_WRITE_MAX_BYTES)
29755
+ });
29756
+ var autonomousWriteRequestSchema = external_exports.discriminatedUnion("kind", [
29757
+ external_exports.object({ kind: external_exports.literal("settings"), settings: external_exports.string().max(64 * 1024) }),
29758
+ external_exports.object({ kind: external_exports.literal("approvals"), taskId: external_exports.string().min(1).max(64), approved: external_exports.boolean() }),
29759
+ external_exports.object({ kind: external_exports.literal("userTodo"), content: external_exports.string().min(1).max(16 * 1024) }),
29760
+ external_exports.object({ kind: external_exports.literal("cron"), action: external_exports.enum(["install", "uninstall"]) })
29761
+ ]);
29762
+ var autonomousLogsQuerySchema = external_exports.object({
29763
+ date: external_exports.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional()
29764
+ });
29628
29765
 
29629
29766
  // ../../22-packages/11-shared/31-dto/src/command.ts
29630
29767
  var COMMAND_MAX_LEN = 8e3;
@@ -29645,7 +29782,14 @@ var dispatchCommandRequestSchema = external_exports.object({
29645
29782
  * Short human label shown to OTHER tabs watching this cli's activity feed (ADR-0101):
29646
29783
  * e.g. "AI review" so the Console shows a friendly line instead of the raw prompt.
29647
29784
  */
29648
- label: external_exports.string().max(120).optional()
29785
+ label: external_exports.string().max(120).optional(),
29786
+ /**
29787
+ * Git tab (ADR-0151): when set, `command` is a `git`/`gh`/`glab` command run on the
29788
+ * worker for the Git tab. The server routes the permission to `project.git` (`read`) /
29789
+ * `project.git_write` (`write`) instead of `command.execute`, and validates `command`
29790
+ * against the git allowlist (`isGitCommandAllowed`). Absent ⇒ normal console dispatch.
29791
+ */
29792
+ gitOp: external_exports.enum(["read", "write"]).optional()
29649
29793
  }).superRefine((v, ctx) => {
29650
29794
  if (!v.ai && v.command.length > COMMAND_MAX_LEN) {
29651
29795
  ctx.addIssue({
@@ -29659,7 +29803,11 @@ var dispatchCommandRequestSchema = external_exports.object({
29659
29803
  }
29660
29804
  });
29661
29805
  var listCommandsQuerySchema = baseRequestSchema.extend({
29662
- projectId: external_exports.string().uuid()
29806
+ projectId: external_exports.string().uuid(),
29807
+ // Optional search filters (command-0005): `search` (from BaseRequest) matches the command
29808
+ // text/status; `from`/`to` are an inclusive `YYYY-MM-DD` date range over `startedAt`.
29809
+ from: external_exports.string().optional(),
29810
+ to: external_exports.string().optional()
29663
29811
  });
29664
29812
 
29665
29813
  // ../../22-packages/11-shared/31-dto/src/invitation.ts
@@ -29757,23 +29905,52 @@ var planEntitlementsSchema = external_exports.object({
29757
29905
  /** Allowed to use Community/4rum. */
29758
29906
  community: external_exports.boolean().default(false),
29759
29907
  /** Retention cap (days) for log/command/chat; null = unlimited. */
29760
- retentionDays: external_exports.number().int().min(1).nullable().default(null)
29908
+ retentionDays: external_exports.number().int().min(1).nullable().default(null),
29909
+ /**
29910
+ * Total 4PM-hosted storage per org (bytes). Originally the template-file cap (ADR-0113),
29911
+ * broadened by ADR-0122 to the org's WHOLE hosted storage: messages + command history
29912
+ * (cli input/output/log) + files (templates/attachments/artifacts). `null` = unlimited.
29913
+ */
29914
+ templateStorageBytes: external_exports.number().int().min(0).nullable().default(null),
29915
+ /** Per-file cap for a template upload (bytes — ADR-0113); null = unlimited. */
29916
+ templateFileMaxBytes: external_exports.number().int().min(0).nullable().default(null),
29917
+ /**
29918
+ * How many 4PM-hosted machine-users the org may rent (ADR-0126). Base plans grant `0`;
29919
+ * raised only by the `rented_machine` add-on (ADR-0123/0124). Never `null` (not "unlimited").
29920
+ */
29921
+ rentedMachineSeats: external_exports.number().int().min(0).default(0)
29761
29922
  });
29923
+ var DEFAULT_ENTITLEMENTS = {
29924
+ maxUsers: 5,
29925
+ maxProjects: 1,
29926
+ teams: false,
29927
+ community: false,
29928
+ retentionDays: 30,
29929
+ // Free tier: 100 MB total, 10 MB per file (ADR-0113).
29930
+ templateStorageBytes: 100 * 1024 * 1024,
29931
+ templateFileMaxBytes: 10 * 1024 * 1024,
29932
+ // No rented machine-user seats by default — bought via the `rented_machine` add-on (ADR-0126).
29933
+ rentedMachineSeats: 0
29934
+ };
29762
29935
  var checkoutRequestSchema = external_exports.object({
29763
29936
  planCode: external_exports.string().min(1).max(50),
29764
29937
  provider: external_exports.enum([PaymentProvider.STRIPE, PaymentProvider.PAYPAL]).optional()
29765
29938
  });
29766
- var upsertPlanRequestSchema = external_exports.object({
29767
- name: external_exports.string().min(1).max(100),
29768
- price: external_exports.number().int().min(0).default(0),
29769
- limits: external_exports.array(planLimitSchema).default([]),
29770
- entitlements: planEntitlementsSchema.optional()
29771
- });
29772
29939
  var assignPlanRequestSchema = external_exports.object({
29773
29940
  planCode: external_exports.string().min(1).max(50),
29774
29941
  /** Cycle length in days (defaults to 30). */
29775
29942
  periodDays: external_exports.number().int().min(1).max(366).default(30)
29776
29943
  });
29944
+ var addonBuyRequestSchema = external_exports.object({
29945
+ code: external_exports.string().min(1).max(50),
29946
+ quantity: external_exports.number().int().min(1).max(1e3),
29947
+ /** Card path only; ignored when the org pays by credit (ADR-0104/0123). */
29948
+ provider: external_exports.enum([PaymentProvider.STRIPE, PaymentProvider.PAYPAL]).optional()
29949
+ });
29950
+ var addonChangeRequestSchema = external_exports.object({
29951
+ quantity: external_exports.number().int().min(0).max(1e3),
29952
+ provider: external_exports.enum([PaymentProvider.STRIPE, PaymentProvider.PAYPAL]).optional()
29953
+ });
29777
29954
 
29778
29955
  // ../../22-packages/11-shared/31-dto/src/search.ts
29779
29956
  var SearchHitType = {
@@ -29787,11 +29964,11 @@ var SearchHitType = {
29787
29964
  MILESTONE: "milestone",
29788
29965
  /** Page/wiki (confluence + project docs — community). */
29789
29966
  PAGE: "page",
29790
- /** Message trong channel (Teams channel — community). */
29967
+ /** A message in a channel (Teams channel — community). */
29791
29968
  MESSAGE: "message",
29792
- /** Message direct/group (lịch sử trò chuyện — community). */
29969
+ /** A direct/group message (chat history — community). */
29793
29970
  CHAT: "chat",
29794
- /** Bài 4rum (community news/forum). */
29971
+ /** A forum post (community news/forum). */
29795
29972
  POST: "post"
29796
29973
  };
29797
29974
  var searchQuerySchema = external_exports.object({
@@ -29829,8 +30006,21 @@ var WsChannels = {
29829
30006
  ...WsChannel,
29830
30007
  FS_LIST: "fs.list",
29831
30008
  FS_READ: "fs.read",
30009
+ // server → cli (reply): write a file, path-clamped to the physic root (machine-0027,
30010
+ // ADR-0151) — the Git tab's manual conflict resolution (`project.git_write`); the only
30011
+ // git-tab write not carried by a git command over command dispatch.
30012
+ FS_WRITE: "fs.write",
29832
30013
  GIT_DIFF: "git.diff",
29833
30014
  GIT_ENV: "git.env",
30015
+ // server → cli (reply): read/write the paired profile's config.json (machine-0025/0026,
30016
+ // ADR-0141) — the machine-user Settings tab syncs the worker cli's config.
30017
+ CONFIG_READ: "config.read",
30018
+ CONFIG_WRITE: "config.write",
30019
+ // autonomous mode control (ADR-0152): read settings+status+books+approvals, tail logs, and
30020
+ // write {settings|approvals|userTodo|cron} — the dashboard drives the worker's autonomous engine.
30021
+ AUTONOMOUS_READ: "autonomous.read",
30022
+ AUTONOMOUS_WRITE: "autonomous.write",
30023
+ AUTONOMOUS_LOGS: "autonomous.logs",
29834
30024
  // git history browse (read-through cli — ADR-0089): repos on the physic project,
29835
30025
  // commit log, changed files of a commit, parent↔commit diff of one file.
29836
30026
  GIT_REPOS: "git.repos",
@@ -29848,6 +30038,11 @@ var WsChannels = {
29848
30038
  PHYSIC_DELETE: "physic.delete",
29849
30039
  // command lifecycle
29850
30040
  COMMAND_ANNOUNCE: "command.announce",
30041
+ // console transcript sync (ADR-0150): the cli streams its authoritative SessionBus
30042
+ // transcript so the web Console renders it verbatim; gated by console.watch (server → cli:
30043
+ // whether a web viewer is attached) so an unwatched cli syncs nothing.
30044
+ CONSOLE_SYNC: "console.sync",
30045
+ CONSOLE_WATCH: "console.watch",
29851
30046
  // AI spec-assist (suggest/review/compose) moved off dedicated ai.* channels to
29852
30047
  // command.dispatch({ ai:true }) → the profile-failover AI path (ADR-0100).
29853
30048
  // quota & usage (ADR-0020)
@@ -29857,8 +30052,14 @@ var WsChannels = {
29857
30052
  MACHINE_USAGE: "machine.usage",
29858
30053
  // cli → server: durable per-command history record on finish (ADR-0072)
29859
30054
  COMMAND_HISTORY: "command.history",
30055
+ // cli → server: periodic upload of the cli's own JSONL log file (ADR-0122) — stored
30056
+ // server-side + counted in the machine user's storage footprint.
30057
+ MACHINE_LOG: "machine.log",
29860
30058
  // server → cli (reply): tail the cli's own JSONL logs on demand (ADR-0072)
29861
30059
  LOG_READ: "log.read",
30060
+ // server → cli (reply): read a finished command's captured output on demand (ADR-0115) —
30061
+ // cli stores command-output/{commandId}.log; returns null when pruned/unavailable.
30062
+ COMMAND_OUTPUT_READ: "command.output-read",
29862
30063
  // outbound review (ADR-0082): machine→server request, server→outbound evaluate, result back
29863
30064
  REVIEW_REQUEST: "review.request",
29864
30065
  REVIEW_EVALUATE: "review.evaluate",
@@ -29979,7 +30180,7 @@ var OutputBatcher = class {
29979
30180
  };
29980
30181
 
29981
30182
  // src/core/ws-client.ts
29982
- import { existsSync as existsSync7, mkdirSync as mkdirSync4, rmSync as rmSync4 } from "fs";
30183
+ import { existsSync as existsSync8, mkdirSync as mkdirSync4, rmSync as rmSync4 } from "fs";
29983
30184
  import { randomUUID as randomCommandId } from "crypto";
29984
30185
 
29985
30186
  // src/core/update-scheduler.ts
@@ -29987,7 +30188,7 @@ import { spawn } from "child_process";
29987
30188
 
29988
30189
  // src/common/logger/logger.ts
29989
30190
  import { appendFile, mkdir, readdir, unlink } from "fs/promises";
29990
- import { readdirSync as readdirSync2, readFileSync as readFileSync6 } from "fs";
30191
+ import { readdirSync as readdirSync2, readFileSync as readFileSync6, statSync } from "fs";
29991
30192
  import { join as join6 } from "path";
29992
30193
  var LEVEL_ORDER = { error: 0, warn: 1, info: 2, debug: 3 };
29993
30194
  var DAY_MS = 24 * 60 * 60 * 1e3;
@@ -30006,6 +30207,13 @@ var CliLogger = class {
30006
30207
  this.retentionDays = opts.retentionDays ?? DEFAULT_RETENTION_DAYS;
30007
30208
  void this.cleanupOldLogs();
30008
30209
  }
30210
+ /** Update the retention window at runtime + prune now (ADR-0115). `days<=0` keeps current. */
30211
+ setRetentionDays(days) {
30212
+ if (days > 0) {
30213
+ this.retentionDays = days;
30214
+ void this.cleanupOldLogs();
30215
+ }
30216
+ }
30009
30217
  /** Log an error-level event. */
30010
30218
  error(event, fields) {
30011
30219
  this.write("error", event, fields);
@@ -30062,6 +30270,24 @@ function readRecentLogLines(dir2, limit) {
30062
30270
  return [];
30063
30271
  }
30064
30272
  }
30273
+ var LOG_UPLOAD_CONTENT_MAX = 512 * 1024;
30274
+ function readLogUpload(dir2) {
30275
+ try {
30276
+ const files = readdirSync2(dir2).filter((f) => /^cli-\d{4}-\d{2}-\d{2}\.jsonl$/.test(f)).sort();
30277
+ const latest = files[files.length - 1];
30278
+ if (!latest) return null;
30279
+ let totalBytes = 0;
30280
+ for (const f of files) totalBytes += statSync(join6(dir2, f)).size;
30281
+ let content = readFileSync6(join6(dir2, latest), "utf8");
30282
+ if (content.length > LOG_UPLOAD_CONTENT_MAX) {
30283
+ content = `\u2026[truncated]
30284
+ ${content.slice(-LOG_UPLOAD_CONTENT_MAX)}`;
30285
+ }
30286
+ return { fileName: latest, content, totalBytes };
30287
+ } catch {
30288
+ return null;
30289
+ }
30290
+ }
30065
30291
 
30066
30292
  // src/core/update-scheduler.ts
30067
30293
  var TICK_MS = 6e4;
@@ -30197,7 +30423,7 @@ async function runCommand(dispatch, emit) {
30197
30423
  });
30198
30424
  child.stdout?.on("data", (data) => batcher.push(data.toString("utf8")));
30199
30425
  child.stderr?.on("data", (data) => batcher.push(data.toString("utf8")));
30200
- await new Promise((resolve6) => {
30426
+ await new Promise((resolve7) => {
30201
30427
  child.on("close", (exitCode) => {
30202
30428
  batcher.flush();
30203
30429
  emit({
@@ -30207,7 +30433,7 @@ async function runCommand(dispatch, emit) {
30207
30433
  done: true,
30208
30434
  exitCode: exitCode ?? -1
30209
30435
  });
30210
- resolve6();
30436
+ resolve7();
30211
30437
  });
30212
30438
  child.on("error", (err) => {
30213
30439
  emit({
@@ -30218,7 +30444,7 @@ async function runCommand(dispatch, emit) {
30218
30444
  done: true,
30219
30445
  exitCode: -1
30220
30446
  });
30221
- resolve6();
30447
+ resolve7();
30222
30448
  });
30223
30449
  });
30224
30450
  }
@@ -30298,10 +30524,10 @@ function isSessionLimit(text) {
30298
30524
  );
30299
30525
  }
30300
30526
  function runAttempt(commandId, cmd, args, cwd2, env3, onChunk) {
30301
- return new Promise((resolve6) => {
30527
+ return new Promise((resolve7) => {
30302
30528
  void runCommand({ commandId, cmd, args, path: cwd2, env: env3 }, (out) => {
30303
30529
  if (out.chunk) onChunk(out.chunk);
30304
- if (out.done) resolve6(out.exitCode ?? -1);
30530
+ if (out.done) resolve7(out.exitCode ?? -1);
30305
30531
  });
30306
30532
  });
30307
30533
  }
@@ -30320,7 +30546,7 @@ async function runAiFailover(plan, commandId, cwd2, handlers) {
30320
30546
  handlers.onChunk(text);
30321
30547
  }
30322
30548
  };
30323
- finalExit = await runAttempt(commandId, plan.cmd, plan.args, cwd2, attempt.env, emit);
30549
+ finalExit = await runAttempt(commandId, plan.cmd, attempt.args, cwd2, attempt.env, emit);
30324
30550
  const tail = parser.flush();
30325
30551
  if (tail) {
30326
30552
  captured += tail;
@@ -30380,13 +30606,18 @@ function resolveHomePath(value) {
30380
30606
  if (isAbsolute(value)) return value;
30381
30607
  return join8(homedir3(), value);
30382
30608
  }
30383
- function resolveDirs(value) {
30384
- if (!value) return [];
30385
- const list = Array.isArray(value) ? value : [value];
30386
- return list.filter((v) => v && v.trim()).map(resolveHomePath);
30609
+ function profileDirs(profiles) {
30610
+ if (!profiles) return [];
30611
+ return profiles.map((p) => p.profile).filter((v) => v && v.trim()).map(resolveHomePath);
30612
+ }
30613
+ function buildRunArgs(profile, cmd, prompt) {
30614
+ const pre = profile.args ?? DEFAULT_AI_ARGS[cmd] ?? [];
30615
+ const model = profile.model?.trim();
30616
+ const modelArgs = model ? ["--model", model] : [];
30617
+ return [...pre, ...modelArgs, prompt];
30387
30618
  }
30388
- function claudeConfigDirs(config) {
30389
- const dirs = resolveDirs(config.claudeConfigDir);
30619
+ function claudeHomeDirs(config) {
30620
+ const dirs = profileDirs(config.claudeHome);
30390
30621
  return dirs.length > 0 ? dirs : [join8(homedir3(), ".claude")];
30391
30622
  }
30392
30623
  function profileDisplayLabel(dir2) {
@@ -30403,28 +30634,40 @@ function profileEnvVar(cmd) {
30403
30634
  if (cmd.includes("codex")) return "CODEX_HOME";
30404
30635
  return null;
30405
30636
  }
30637
+ function profilesFor(config, cmd) {
30638
+ const envVar = profileEnvVar(cmd);
30639
+ if (envVar === "CLAUDE_CONFIG_DIR") return config.claudeHome ?? [];
30640
+ if (envVar === "CODEX_HOME") return config.codexHome ?? [];
30641
+ return [];
30642
+ }
30406
30643
  function profileLabels(config) {
30407
30644
  const cmd = config.aiCli ?? DEFAULT_AI_CLI;
30408
- const envVar = profileEnvVar(cmd);
30409
- const dirs = envVar === "CLAUDE_CONFIG_DIR" ? resolveDirs(config.claudeConfigDir) : envVar === "CODEX_HOME" ? resolveDirs(config.codexHome) : [];
30410
- return dirs.map((dir2) => profileDisplayLabel(dir2));
30645
+ return profileDirs(profilesFor(config, cmd)).map((dir2) => profileDisplayLabel(dir2));
30411
30646
  }
30412
30647
  function planAiRun(prompt, config, workingDir) {
30413
30648
  const cmd = config.aiCli ?? DEFAULT_AI_CLI;
30414
- const args = [...config.aiArgs ?? DEFAULT_AI_ARGS[cmd] ?? [], prompt];
30415
30649
  const baseEnv = config.aiEnv ?? {};
30416
30650
  const envVar = profileEnvVar(cmd);
30417
- const dirs = envVar ? envVar === "CLAUDE_CONFIG_DIR" ? resolveDirs(config.claudeConfigDir) : resolveDirs(config.codexHome) : [];
30418
- if (!envVar || dirs.length === 0) {
30419
- return { cmd, args, attempts: [{ label: "default", dir: null, env: { ...baseEnv } }] };
30420
- }
30421
- const ordered = workingDir && dirs.includes(workingDir) ? [workingDir, ...dirs.filter((d) => d !== workingDir)] : dirs;
30422
- const attempts = ordered.map((dir2) => ({
30651
+ const defaultPlan = {
30652
+ cmd,
30653
+ attempts: [
30654
+ { label: "default", dir: null, args: buildRunArgs({ profile: "" }, cmd, prompt), env: { ...baseEnv } }
30655
+ ]
30656
+ };
30657
+ if (!envVar) return defaultPlan;
30658
+ const resolved = profilesFor(config, cmd).filter((p) => p.profile && p.profile.trim()).map((p) => ({ profile: p, dir: resolveHomePath(p.profile) }));
30659
+ if (resolved.length === 0) return defaultPlan;
30660
+ const ordered = workingDir && resolved.some((r) => r.dir === workingDir) ? [
30661
+ resolved.find((r) => r.dir === workingDir),
30662
+ ...resolved.filter((r) => r.dir !== workingDir)
30663
+ ] : resolved;
30664
+ const attempts = ordered.map(({ profile, dir: dir2 }) => ({
30423
30665
  label: profileDisplayLabel(dir2),
30424
30666
  dir: dir2,
30667
+ args: buildRunArgs(profile, cmd, prompt),
30425
30668
  env: { ...baseEnv, [envVar]: dir2 }
30426
30669
  }));
30427
- return { cmd, args, attempts };
30670
+ return { cmd, attempts };
30428
30671
  }
30429
30672
 
30430
30673
  // src/core/claude-usage.ts
@@ -30462,16 +30705,16 @@ async function fetchUsage(token) {
30462
30705
  }
30463
30706
  }
30464
30707
  function runUsageCli(dir2, aiCli2) {
30465
- return new Promise((resolve6) => {
30708
+ return new Promise((resolve7) => {
30466
30709
  let out = "";
30467
30710
  let child = null;
30468
30711
  const finish = () => {
30469
30712
  clearTimeout(timer);
30470
- resolve6(out);
30713
+ resolve7(out);
30471
30714
  };
30472
30715
  const timer = setTimeout(() => {
30473
30716
  child?.kill();
30474
- resolve6(out);
30717
+ resolve7(out);
30475
30718
  }, 3e4);
30476
30719
  timer.unref?.();
30477
30720
  try {
@@ -30779,6 +31022,13 @@ function recordCommand(entry) {
30779
31022
  function getCommandHistory() {
30780
31023
  return entries;
30781
31024
  }
31025
+ function pruneCommandHistoryByAge(days) {
31026
+ if (days <= 0) return;
31027
+ const cutoff = Date.now() - days * 24 * 60 * 60 * 1e3;
31028
+ const before = entries.length;
31029
+ entries = entries.filter((e) => new Date(e.startedAt).getTime() >= cutoff);
31030
+ if (entries.length !== before) persist();
31031
+ }
30782
31032
  function finishCommand(commandId, exitCode, usage) {
30783
31033
  const entry = entries.find((e) => e.commandId === commandId && e.status === "running");
30784
31034
  if (!entry) return;
@@ -30806,7 +31056,7 @@ import {
30806
31056
  readFileSync as readFileSync11,
30807
31057
  readdirSync as readdirSync3,
30808
31058
  rmSync as rmSync3,
30809
- statSync
31059
+ statSync as statSync2
30810
31060
  } from "fs";
30811
31061
  import { join as join11 } from "path";
30812
31062
  var KEEP_FILES = 200;
@@ -30839,11 +31089,22 @@ function readCommandOutput(commandId) {
30839
31089
  function prune() {
30840
31090
  if (!dir) return;
30841
31091
  try {
30842
- const files = readdirSync3(dir).filter((f) => f.endsWith(".log")).map((f) => ({ f, t: statSync(join11(dir, f)).mtimeMs })).sort((a, b) => b.t - a.t);
31092
+ const files = readdirSync3(dir).filter((f) => f.endsWith(".log")).map((f) => ({ f, t: statSync2(join11(dir, f)).mtimeMs })).sort((a, b) => b.t - a.t);
30843
31093
  for (const { f } of files.slice(KEEP_FILES)) rmSync3(join11(dir, f), { force: true });
30844
31094
  } catch {
30845
31095
  }
30846
31096
  }
31097
+ function pruneCommandOutputByAge(days) {
31098
+ if (!dir || days <= 0) return;
31099
+ const cutoff = Date.now() - days * 24 * 60 * 60 * 1e3;
31100
+ try {
31101
+ for (const f of readdirSync3(dir).filter((n) => n.endsWith(".log"))) {
31102
+ const p = join11(dir, f);
31103
+ if (statSync2(p).mtimeMs < cutoff) rmSync3(p, { force: true });
31104
+ }
31105
+ } catch {
31106
+ }
31107
+ }
30847
31108
 
30848
31109
  // src/core/fs-browse.ts
30849
31110
  import { readdir as readdir2 } from "fs/promises";
@@ -30884,24 +31145,210 @@ async function readWorkerFile(path) {
30884
31145
  }
30885
31146
  }
30886
31147
 
30887
- // src/core/git-diff.ts
30888
- import { readFile as readFile2 } from "fs/promises";
30889
- import { execFile as execFile2 } from "child_process";
30890
- import { basename as basename2, dirname as dirname3, resolve as resolve3 } from "path";
31148
+ // src/core/fs-write.ts
31149
+ import { mkdir as mkdir2, writeFile } from "fs/promises";
31150
+ import { dirname as dirname3, resolve as resolve3, sep as sep3 } from "path";
31151
+ var MAX_BYTES2 = 2 * 1024 * 1024;
31152
+ function resolveInRoot(root, relPath) {
31153
+ if (!root) return null;
31154
+ const base = resolve3(root);
31155
+ const target = resolve3(base, relPath || ".");
31156
+ return target === base || target.startsWith(base + sep3) ? target : null;
31157
+ }
31158
+ async function writeWorkerFile(root, relPath, content) {
31159
+ const target = resolveInRoot(root, relPath);
31160
+ if (!target) {
31161
+ return { ok: false, path: relPath, bytes: 0, error: "path escapes the project root" };
31162
+ }
31163
+ const bytes = Buffer.byteLength(content, "utf8");
31164
+ if (bytes > MAX_BYTES2) {
31165
+ return { ok: false, path: target, bytes: 0, error: `content too large (max ${MAX_BYTES2} bytes)` };
31166
+ }
31167
+ try {
31168
+ await mkdir2(dirname3(target), { recursive: true });
31169
+ await writeFile(target, content, "utf8");
31170
+ return { ok: true, path: target, bytes };
31171
+ } catch (err) {
31172
+ return { ok: false, path: target, bytes: 0, error: err instanceof Error ? err.message : String(err) };
31173
+ }
31174
+ }
31175
+
31176
+ // src/core/autonomous.ts
31177
+ import { readFile as readFile2, writeFile as writeFile2, chmod } from "fs/promises";
31178
+ import { readFileSync as readFileSync12 } from "fs";
31179
+ import { execFile as execFile2, spawn as spawn4 } from "child_process";
31180
+ import { join as join12 } from "path";
30891
31181
  import { promisify as promisify2 } from "util";
30892
31182
  var run2 = promisify2(execFile2);
31183
+ var SETTINGS_REL = ".claude/.autonomous.settings.json";
31184
+ var HISTORIES_REL = ".claude/.autonomous.histories.json";
31185
+ var APPROVALS_REL = ".claude/.autonomous.approvals.json";
31186
+ var TICK_REL = ".claude/hooks/autonomous-tick.sh";
31187
+ var LOG_DIR_REL = ".claude/logs";
31188
+ var BOOK_FILES = {
31189
+ userTodo: "USER_TODO.md",
31190
+ aiTodo: "AI_TODO.md",
31191
+ aiProgress: "AI_PROGRESS.md",
31192
+ aiDone: "AI_DONE.md",
31193
+ userQa: "USER_QA.md"
31194
+ };
31195
+ var DEFAULT_CRON = "*/10 * * * *";
31196
+ async function readText(path, fallback = "") {
31197
+ try {
31198
+ return await readFile2(path, "utf8");
31199
+ } catch {
31200
+ return fallback;
31201
+ }
31202
+ }
31203
+ function parseJson(text) {
31204
+ try {
31205
+ const v = JSON.parse(text);
31206
+ return v && typeof v === "object" ? v : null;
31207
+ } catch {
31208
+ return null;
31209
+ }
31210
+ }
31211
+ function tickScript(root) {
31212
+ return join12(root, TICK_REL);
31213
+ }
31214
+ async function crontabList() {
31215
+ try {
31216
+ const { stdout } = await run2("crontab", ["-l"], { timeout: 1e4 });
31217
+ return stdout;
31218
+ } catch {
31219
+ return "";
31220
+ }
31221
+ }
31222
+ async function crontabSet(content) {
31223
+ return new Promise((resolve7) => {
31224
+ const child = spawn4("crontab", ["-"], { stdio: ["pipe", "ignore", "ignore"] });
31225
+ child.on("error", () => resolve7(false));
31226
+ child.on("close", (code) => resolve7(code === 0));
31227
+ child.stdin.end(content.endsWith("\n") ? content : content + "\n");
31228
+ });
31229
+ }
31230
+ async function cronInstalled(root) {
31231
+ const script = tickScript(root);
31232
+ return (await crontabList()).split("\n").some((l) => l.includes(script) && !l.trim().startsWith("#"));
31233
+ }
31234
+ async function getAutonomousStatus(root) {
31235
+ const settings = parseJson(await readText(join12(root, SETTINGS_REL))) ?? {};
31236
+ const hist = parseJson(await readText(join12(root, HISTORIES_REL))) ?? {};
31237
+ const records = Array.isArray(hist.records) ? hist.records : [];
31238
+ const last = records[records.length - 1];
31239
+ const ticks = hist.ticks ?? {};
31240
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
31241
+ return {
31242
+ installed: await cronInstalled(root),
31243
+ paused: settings.paused === true,
31244
+ cronSchedule: typeof settings.cron_schedule === "string" ? settings.cron_schedule : DEFAULT_CRON,
31245
+ lastTickAt: last && typeof last.ts === "string" ? last.ts : null,
31246
+ lastResult: last && typeof last.status === "string" ? last.status : null,
31247
+ consecutiveFails: typeof hist.consecutive_fails === "number" ? hist.consecutive_fails : 0,
31248
+ todayTicks: ticks.day === today && typeof ticks.count === "number" ? ticks.count : 0
31249
+ };
31250
+ }
31251
+ function isAutonomousRunning(root) {
31252
+ try {
31253
+ const s = JSON.parse(readFileSync12(join12(root, SETTINGS_REL), "utf8"));
31254
+ if (s.paused === true) return false;
31255
+ const h = JSON.parse(readFileSync12(join12(root, HISTORIES_REL), "utf8"));
31256
+ const ts = h.records?.[h.records.length - 1]?.ts;
31257
+ if (!ts) return false;
31258
+ const t = Date.parse(ts.replace(" ", "T"));
31259
+ return Number.isFinite(t) && Date.now() - t < 6 * 3600 * 1e3;
31260
+ } catch {
31261
+ return false;
31262
+ }
31263
+ }
31264
+ async function readAutonomous(root) {
31265
+ const [settings, approvals, status, ...books] = await Promise.all([
31266
+ readText(join12(root, SETTINGS_REL)),
31267
+ readText(join12(root, APPROVALS_REL), "{}"),
31268
+ getAutonomousStatus(root),
31269
+ ...Object.values(BOOK_FILES).map((f) => readText(join12(root, f)))
31270
+ ]);
31271
+ const keys = Object.keys(BOOK_FILES);
31272
+ const bookMap = {};
31273
+ keys.forEach((k, i) => bookMap[k] = books[i] ?? "");
31274
+ return { settings, status, books: bookMap, approvals };
31275
+ }
31276
+ async function readAutonomousLogs(root, date) {
31277
+ const day = date && /^\d{4}-\d{2}-\d{2}$/.test(date) ? date : (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
31278
+ const text = await readText(join12(root, LOG_DIR_REL, `autonomous-tick-${day}.log`));
31279
+ const lines = text ? text.split("\n").filter((l) => l.length > 0).slice(-500) : [];
31280
+ return { date: day, lines };
31281
+ }
31282
+ async function installCron(root) {
31283
+ const script = tickScript(root);
31284
+ const status = await getAutonomousStatus(root);
31285
+ await chmod(script, 493).catch(() => void 0);
31286
+ const kept = (await crontabList()).split("\n").filter((l) => l.trim() && !l.includes(script));
31287
+ kept.push(`${status.cronSchedule} ${script}`);
31288
+ await crontabSet(kept.join("\n"));
31289
+ }
31290
+ async function uninstallCron(root) {
31291
+ const script = tickScript(root);
31292
+ const cur = await crontabList();
31293
+ if (!cur.includes(script)) return;
31294
+ const kept = cur.split("\n").filter((l) => l.trim() && !l.includes(script));
31295
+ await crontabSet(kept.join("\n"));
31296
+ }
31297
+ async function writeAutonomous(root, req, by) {
31298
+ try {
31299
+ switch (req.kind) {
31300
+ case "settings": {
31301
+ if (!parseJson(req.settings)) return { ok: false, error: "settings is not valid JSON" };
31302
+ await writeFile2(join12(root, SETTINGS_REL), req.settings, "utf8");
31303
+ break;
31304
+ }
31305
+ case "approvals": {
31306
+ const map = parseJson(await readText(join12(root, APPROVALS_REL), "{}")) ?? {};
31307
+ if (req.approved) map[req.taskId] = { approved: true, by, at: (/* @__PURE__ */ new Date()).toISOString() };
31308
+ else delete map[req.taskId];
31309
+ await writeFile2(join12(root, APPROVALS_REL), JSON.stringify(map, null, 2) + "\n", "utf8");
31310
+ break;
31311
+ }
31312
+ case "userTodo": {
31313
+ const cur = await readText(join12(root, BOOK_FILES.userTodo));
31314
+ const block = `
31315
+
31316
+ > posted by ${by} at ${(/* @__PURE__ */ new Date()).toISOString()}
31317
+ ${req.content.trim()}
31318
+ `;
31319
+ await writeFile2(join12(root, BOOK_FILES.userTodo), cur.trimEnd() + block, "utf8");
31320
+ break;
31321
+ }
31322
+ case "cron": {
31323
+ if (req.action === "install") await installCron(root);
31324
+ else await uninstallCron(root);
31325
+ break;
31326
+ }
31327
+ }
31328
+ return { ok: true, status: await getAutonomousStatus(root) };
31329
+ } catch (err) {
31330
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
31331
+ }
31332
+ }
31333
+
31334
+ // src/core/git-diff.ts
31335
+ import { readFile as readFile3 } from "fs/promises";
31336
+ import { execFile as execFile3 } from "child_process";
31337
+ import { basename as basename2, dirname as dirname4, resolve as resolve4 } from "path";
31338
+ import { promisify as promisify3 } from "util";
31339
+ var run3 = promisify3(execFile3);
30893
31340
  async function gitDiff(path) {
30894
- const target = resolve3(path);
30895
- const dir2 = dirname3(target);
31341
+ const target = resolve4(path);
31342
+ const dir2 = dirname4(target);
30896
31343
  const base = basename2(target);
30897
31344
  let newContent = "";
30898
31345
  try {
30899
- newContent = await readFile2(target, "utf8");
31346
+ newContent = await readFile3(target, "utf8");
30900
31347
  } catch {
30901
31348
  }
30902
31349
  let oldContent = "";
30903
31350
  try {
30904
- const { stdout } = await run2("git", ["show", `HEAD:./${base}`], {
31351
+ const { stdout } = await run3("git", ["show", `HEAD:./${base}`], {
30905
31352
  cwd: dir2,
30906
31353
  timeout: 15e3,
30907
31354
  maxBuffer: 4 * 1024 * 1024
@@ -30913,12 +31360,12 @@ async function gitDiff(path) {
30913
31360
  }
30914
31361
 
30915
31362
  // src/core/git-env.ts
30916
- import { execFile as execFile3 } from "child_process";
30917
- import { promisify as promisify3 } from "util";
30918
- var run3 = promisify3(execFile3);
31363
+ import { execFile as execFile4 } from "child_process";
31364
+ import { promisify as promisify4 } from "util";
31365
+ var run4 = promisify4(execFile4);
30919
31366
  async function hasBinary(bin, args = ["--version"]) {
30920
31367
  try {
30921
- await run3(bin, args, { timeout: 5e3 });
31368
+ await run4(bin, args, { timeout: 5e3 });
30922
31369
  return true;
30923
31370
  } catch {
30924
31371
  return false;
@@ -30931,7 +31378,7 @@ async function checkGitEnv(provider) {
30931
31378
  let account = null;
30932
31379
  if (installed) {
30933
31380
  try {
30934
- const { stdout, stderr } = await run3(bin, ["auth", "status"], { timeout: 8e3 });
31381
+ const { stdout, stderr } = await run4(bin, ["auth", "status"], { timeout: 8e3 });
30935
31382
  const out = `${stdout}
30936
31383
  ${stderr}`;
30937
31384
  authenticated = /logged in|logged into|✓/i.test(out);
@@ -30945,21 +31392,21 @@ ${stderr}`;
30945
31392
 
30946
31393
  // src/core/git-history.ts
30947
31394
  import { readdir as readdir3 } from "fs/promises";
30948
- import { execFile as execFile4 } from "child_process";
30949
- import { resolve as resolve4, sep as sep3 } from "path";
30950
- import { promisify as promisify4 } from "util";
30951
- var run4 = promisify4(execFile4);
31395
+ import { execFile as execFile5 } from "child_process";
31396
+ import { resolve as resolve5, sep as sep4 } from "path";
31397
+ import { promisify as promisify5 } from "util";
31398
+ var run5 = promisify5(execFile5);
30952
31399
  var FS = "";
30953
31400
  var RS = "";
30954
31401
  function resolveRepo(root, repo) {
30955
31402
  if (!root) return null;
30956
- const base = resolve4(root);
30957
- const requested = resolve4(base, repo && repo.trim() ? repo : ".");
30958
- return requested === base || requested.startsWith(base + sep3) ? requested : base;
31403
+ const base = resolve5(root);
31404
+ const requested = resolve5(base, repo && repo.trim() ? repo : ".");
31405
+ return requested === base || requested.startsWith(base + sep4) ? requested : base;
30959
31406
  }
30960
31407
  async function git(cwd2, args) {
30961
31408
  try {
30962
- const { stdout } = await run4("git", args, {
31409
+ const { stdout } = await run5("git", args, {
30963
31410
  cwd: cwd2,
30964
31411
  timeout: 15e3,
30965
31412
  maxBuffer: 8 * 1024 * 1024
@@ -30971,13 +31418,13 @@ async function git(cwd2, args) {
30971
31418
  }
30972
31419
  async function gitRepos(root) {
30973
31420
  if (!root) return { repos: [] };
30974
- const base = resolve4(root);
31421
+ const base = resolve5(root);
30975
31422
  const found = /* @__PURE__ */ new Set();
30976
31423
  const repos = [];
30977
31424
  const add = async (dir2) => {
30978
31425
  const inside = await git(dir2, ["rev-parse", "--show-toplevel"]);
30979
31426
  const top = inside.trim();
30980
- if (!top || found.has(top) || !(top === base || top.startsWith(base + sep3))) return;
31427
+ if (!top || found.has(top) || !(top === base || top.startsWith(base + sep4))) return;
30981
31428
  found.add(top);
30982
31429
  const subdir = top === base ? "" : top.slice(base.length + 1);
30983
31430
  const remote = (await git(top, ["remote", "get-url", "origin"])).trim();
@@ -30988,13 +31435,13 @@ async function gitRepos(root) {
30988
31435
  const lvl1 = await readdir3(base, { withFileTypes: true });
30989
31436
  for (const d of lvl1) {
30990
31437
  if (!d.isDirectory() || d.name === ".git" || d.name === "node_modules") continue;
30991
- const p1 = resolve4(base, d.name);
31438
+ const p1 = resolve5(base, d.name);
30992
31439
  await add(p1);
30993
31440
  try {
30994
31441
  const lvl2 = await readdir3(p1, { withFileTypes: true });
30995
31442
  for (const d2 of lvl2) {
30996
31443
  if (!d2.isDirectory() || d2.name === ".git" || d2.name === "node_modules") continue;
30997
- await add(resolve4(p1, d2.name));
31444
+ await add(resolve5(p1, d2.name));
30998
31445
  }
30999
31446
  } catch {
31000
31447
  }
@@ -31061,24 +31508,24 @@ async function gitCommitDiff(root, repo, hash, path) {
31061
31508
  }
31062
31509
 
31063
31510
  // src/core/git-commit-identity.ts
31064
- import { execFile as execFile5 } from "child_process";
31065
- import { promisify as promisify5 } from "util";
31066
- var run5 = promisify5(execFile5);
31511
+ import { execFile as execFile6 } from "child_process";
31512
+ import { promisify as promisify6 } from "util";
31513
+ var run6 = promisify6(execFile6);
31067
31514
  async function setCommitAuthor(cwd2, machineUsername) {
31068
31515
  if (!machineUsername) return;
31069
31516
  try {
31070
- await run5("git", ["config", "--local", "user.name", machineUsername], { cwd: cwd2, timeout: 8e3 });
31517
+ await run6("git", ["config", "--local", "user.name", machineUsername], { cwd: cwd2, timeout: 8e3 });
31071
31518
  } catch {
31072
31519
  }
31073
31520
  }
31074
31521
 
31075
31522
  // src/core/scaffold.ts
31076
- import { cp, mkdir as mkdir2, writeFile } from "fs/promises";
31077
- import { execFile as execFile6 } from "child_process";
31078
- import { dirname as dirname4, join as join12, resolve as resolve5 } from "path";
31523
+ import { cp, mkdir as mkdir3, writeFile as writeFile3 } from "fs/promises";
31524
+ import { execFile as execFile7 } from "child_process";
31525
+ import { dirname as dirname5, join as join13, resolve as resolve6 } from "path";
31079
31526
  import { fileURLToPath as fileURLToPath3 } from "url";
31080
- import { promisify as promisify6 } from "util";
31081
- var run6 = promisify6(execFile6);
31527
+ import { promisify as promisify7 } from "util";
31528
+ var run7 = promisify7(execFile7);
31082
31529
  function reposOf(spec) {
31083
31530
  const repos = spec?.repos;
31084
31531
  return Array.isArray(repos) ? repos : [];
@@ -31087,46 +31534,51 @@ function subDirName(repo) {
31087
31534
  const raw = (repo.subdir || repo.role || repo.name || "repo").trim();
31088
31535
  return raw.replace(/[^\w.-]+/g, "-").replace(/^-+|-+$/g, "") || "repo";
31089
31536
  }
31090
- async function provisionRepos(target, repos, emit) {
31537
+ async function provisionRepos(target, repos, emit, opts = {}) {
31091
31538
  for (const repo of repos) {
31092
31539
  if (repo.primary) {
31540
+ if (opts.clonePrimary && repo.op === "existing" && repo.url) {
31541
+ emit("git", `Cloning ${repo.url}\u2026`);
31542
+ await run7("git", ["clone", repo.url, target], { timeout: 12e4 });
31543
+ continue;
31544
+ }
31093
31545
  emit("git", "Initializing the primary repo\u2026");
31094
- await run6("git", ["init"], { cwd: target, timeout: 2e4 });
31546
+ await run7("git", ["init"], { cwd: target, timeout: 2e4 });
31095
31547
  if (repo.op === "existing" && repo.url) {
31096
- await run6("git", ["remote", "add", "origin", repo.url], { cwd: target, timeout: 2e4 }).catch(
31548
+ await run7("git", ["remote", "add", "origin", repo.url], { cwd: target, timeout: 2e4 }).catch(
31097
31549
  () => void 0
31098
31550
  );
31099
31551
  }
31100
31552
  continue;
31101
31553
  }
31102
- const dir2 = join12(target, subDirName(repo));
31554
+ const dir2 = join13(target, subDirName(repo));
31103
31555
  if (repo.op === "existing" && repo.url) {
31104
31556
  emit("git", `Cloning sub-repo ${repo.url}\u2026`);
31105
- await run6("git", ["clone", repo.url, dir2], { timeout: 12e4 });
31557
+ await run7("git", ["clone", repo.url, dir2], { timeout: 12e4 });
31106
31558
  } else {
31107
31559
  emit("git", `Initializing sub-repo ${subDirName(repo)}\u2026`);
31108
- await mkdir2(dir2, { recursive: true });
31109
- await run6("git", ["init"], { cwd: dir2, timeout: 2e4 });
31560
+ await mkdir3(dir2, { recursive: true });
31561
+ await run7("git", ["init"], { cwd: dir2, timeout: 2e4 });
31110
31562
  }
31111
31563
  }
31112
31564
  }
31113
31565
  function sampleDir() {
31114
31566
  if (process.env.SCAFFOLD_SAMPLE_DIR) return process.env.SCAFFOLD_SAMPLE_DIR;
31115
- const here = dirname4(fileURLToPath3(import.meta.url));
31116
- return resolve5(here, "../../project-sample");
31567
+ const here = dirname5(fileURLToPath3(import.meta.url));
31568
+ return resolve6(here, "../../project-sample");
31117
31569
  }
31118
31570
  async function scaffoldProject(payload, profileDir2, onProgress) {
31119
31571
  const emit = (step, message) => onProgress?.({ projectId: payload.projectId, step, message });
31120
31572
  try {
31121
- const root = process.env.SCAFFOLD_ROOT ? resolve5(process.env.SCAFFOLD_ROOT) : resolve5(profileDir2);
31122
- const target = join12(root, payload.projectName);
31123
- await mkdir2(target, { recursive: true });
31573
+ const root = process.env.SCAFFOLD_ROOT ? resolve6(process.env.SCAFFOLD_ROOT) : resolve6(profileDir2);
31574
+ const target = join13(root, payload.projectName);
31575
+ await mkdir3(target, { recursive: true });
31124
31576
  emit("copy", "Copying the sample template\u2026");
31125
31577
  await cp(sampleDir(), target, { recursive: true, force: false, errorOnExist: false });
31126
31578
  if (payload.spec) {
31127
31579
  emit("spec", "Writing project.spec.json\u2026");
31128
- await writeFile(
31129
- join12(target, "project.spec.json"),
31580
+ await writeFile3(
31581
+ join13(target, "project.spec.json"),
31130
31582
  JSON.stringify(payload.spec, null, 2),
31131
31583
  "utf8"
31132
31584
  );
@@ -31144,7 +31596,7 @@ async function aiInit(target, spec, emit) {
31144
31596
  emit("ai-init", "AI init: subagents, README, CLAUDE.md\u2026");
31145
31597
  const subagents = Array.isArray(spec.subagents) ? spec.subagents : [];
31146
31598
  if (subagents.length > 0) {
31147
- await mkdir2(join12(target, ".claude", "agents"), { recursive: true });
31599
+ await mkdir3(join13(target, ".claude", "agents"), { recursive: true });
31148
31600
  for (const sa of subagents) {
31149
31601
  const name = (sa.name || "").trim().replace(/[^\w.-]+/g, "-").replace(/^-+|-+$/g, "");
31150
31602
  if (!name) continue;
@@ -31154,17 +31606,17 @@ name: ${name}
31154
31606
 
31155
31607
  ${(sa.description || "").trim()}
31156
31608
  `;
31157
- await writeFile(join12(target, ".claude", "agents", `${name}.md`), body, "utf8");
31609
+ await writeFile3(join13(target, ".claude", "agents", `${name}.md`), body, "utf8");
31158
31610
  }
31159
31611
  }
31160
31612
  const specJson = JSON.stringify(spec);
31161
31613
  await generateFile(
31162
- join12(target, "README.md"),
31614
+ join13(target, "README.md"),
31163
31615
  `Write a concise README.md (Markdown only, no preamble) for this project from its spec JSON:
31164
31616
  ${specJson}`
31165
31617
  );
31166
31618
  await generateFile(
31167
- join12(target, "CLAUDE.md"),
31619
+ join13(target, "CLAUDE.md"),
31168
31620
  `Write a CLAUDE.md (Markdown only, no preamble) with guidance/conventions for AI agents working in this project, derived from its spec JSON:
31169
31621
  ${specJson}`
31170
31622
  );
@@ -31172,35 +31624,66 @@ ${specJson}`
31172
31624
  async function generateFile(path, prompt) {
31173
31625
  try {
31174
31626
  const text = await aiGenerate(prompt);
31175
- if (text.trim()) await writeFile(path, text.trim() + "\n", "utf8");
31627
+ if (text.trim()) await writeFile3(path, text.trim() + "\n", "utf8");
31176
31628
  } catch {
31177
31629
  }
31178
31630
  }
31179
- async function addProject(payload, onProgress) {
31631
+ async function addProject(payload, profileDir2, onProgress) {
31180
31632
  const emit = (step, message) => onProgress?.({ projectId: payload.projectId, step, message });
31181
31633
  try {
31182
- const target = resolve5(payload.path);
31183
- await mkdir2(target, { recursive: true });
31184
- if (payload.repoUrl) {
31185
- emit("clone", `Cloning ${payload.repoUrl}\u2026`);
31186
- await run6("git", ["clone", payload.repoUrl, target], { timeout: 12e4 });
31187
- } else {
31188
- emit("init", "git init\u2026");
31189
- await run6("git", ["init"], { cwd: target, timeout: 2e4 });
31190
- }
31191
- onProgress?.({ projectId: payload.projectId, step: "done", message: "Repo ready.", done: true });
31634
+ const root = process.env.SCAFFOLD_ROOT ? resolve6(process.env.SCAFFOLD_ROOT) : resolve6(profileDir2);
31635
+ const target = join13(root, payload.projectName);
31636
+ await mkdir3(target, { recursive: true });
31637
+ const repos = payload.repos ?? [];
31638
+ if (repos.length > 0) await provisionRepos(target, repos, emit, { clonePrimary: true });
31639
+ onProgress?.({ projectId: payload.projectId, step: "done", message: "Project added.", done: true });
31192
31640
  return { ok: true, path: target };
31193
31641
  } catch (err) {
31194
31642
  return { ok: false, error: err instanceof Error ? err.message : String(err) };
31195
31643
  }
31196
31644
  }
31197
31645
 
31646
+ // src/core/config-sync.ts
31647
+ import { existsSync as existsSync7, readFileSync as readFileSync13 } from "fs";
31648
+ import { join as join14 } from "path";
31649
+ var SERVER_MANAGED_KEYS = ["physicPath", "sessionSwitchPct", "perPromptTokenLimit"];
31650
+ function readConfigText(profileDir2) {
31651
+ const path = join14(profileDir2, "config.json");
31652
+ if (existsSync7(path)) {
31653
+ try {
31654
+ return readFileSync13(path, "utf8");
31655
+ } catch {
31656
+ }
31657
+ }
31658
+ return JSON.stringify(defaultProfileConfig(), null, 2);
31659
+ }
31660
+ function applyConfigText(profileDir2, text) {
31661
+ let parsed;
31662
+ try {
31663
+ parsed = JSON.parse(text);
31664
+ } catch (err) {
31665
+ return { ok: false, error: `Invalid JSON: ${err.message}` };
31666
+ }
31667
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
31668
+ return { ok: false, error: "config.json must be a JSON object." };
31669
+ }
31670
+ const next = { ...parsed };
31671
+ const current = readProfileConfig(profileDir2);
31672
+ for (const key of SERVER_MANAGED_KEYS) {
31673
+ if (current[key] !== void 0) next[key] = current[key];
31674
+ else delete next[key];
31675
+ }
31676
+ overwriteProfileConfig(profileDir2, next);
31677
+ return { ok: true };
31678
+ }
31679
+
31198
31680
  // src/core/ws-client.ts
31199
31681
  var HEARTBEAT_MS = 3e4;
31200
31682
  var USAGE_POLL_MS = 5 * 6e4;
31683
+ var LOG_UPLOAD_MS = 10 * 6e4;
31201
31684
  var RECONNECT_BACKOFF_BASE_MS = 1e3;
31202
- var RECONNECT_MAX_BACKOFF_DEFAULT_SEC = 600;
31203
- var RECONNECT_BACKOFF_MAX_ATTEMPT = 10;
31685
+ var RECONNECT_MAX_BACKOFF_DEFAULT_SEC = 60;
31686
+ var RECONNECT_BACKOFF_MAX_ATTEMPT = 6;
31204
31687
  function envReconnectMaxSec() {
31205
31688
  const raw = Number(process.env.FOURPM_RECONNECT_MAX_BACKOFF_SEC);
31206
31689
  if (!Number.isFinite(raw)) return null;
@@ -31210,6 +31693,11 @@ function envReconnectMaxSec() {
31210
31693
  var MAX_HASHCODE_INVALID_RETRIES = 5;
31211
31694
  var CLOSE_DRAINING = 4001;
31212
31695
  var OFFLINE_BUFFER_MAX_BYTES = 512 * 1024;
31696
+ var CONSOLE_SNAPSHOT_INTERVAL_MS = 3e4;
31697
+ var CONSOLE_WATCH_LEASE_MS = 45e3;
31698
+ function toDtoEntry(e) {
31699
+ return { id: e.id, source: e.source, kind: e.kind, text: e.text, level: e.level, resultKind: e.resultKind };
31700
+ }
31213
31701
  var WsClient = class {
31214
31702
  constructor(context) {
31215
31703
  this.context = context;
@@ -31221,14 +31709,26 @@ var WsClient = class {
31221
31709
  );
31222
31710
  context.bus.onLocalSubmit((input) => void this.runLocalCommand(input));
31223
31711
  context.bus.onReconnect(() => this.reconnectNow());
31712
+ context.bus.onTranscript((entry) => this.emitConsole({ kind: "add", entry: toDtoEntry(entry) }));
31713
+ context.bus.onTranscriptUpdate(
31714
+ (entry) => this.emitConsole({ kind: "update", entry: toDtoEntry(entry) })
31715
+ );
31716
+ context.bus.onClear(() => this.emitConsole({ kind: "clear" }));
31224
31717
  }
31225
31718
  context;
31226
31719
  socket = null;
31227
31720
  sessionKey = null;
31721
+ // True once the current connectOnce() reached hello_ack — a *real* session. Gates the
31722
+ // backoff reset so a socket that opens then closes before the handshake keeps backing off
31723
+ // instead of hammering the server every ~1s.
31724
+ sessionEstablished = false;
31228
31725
  ecdh = null;
31229
31726
  wsToken = "";
31230
31727
  heartbeat = null;
31231
31728
  usageTimer = null;
31729
+ logUploadTimer = null;
31730
+ /** Signature (fileName + totalBytes) of the last log upload — skip re-sending if unchanged. */
31731
+ lastLogSig = null;
31232
31732
  usageSnapshot = null;
31233
31733
  /** Whether the serving project requires outbound review before each AI spawn (ADR-0082). */
31234
31734
  outboundReviewEnabled = false;
@@ -31253,7 +31753,25 @@ var WsClient = class {
31253
31753
  connectWaiters = [];
31254
31754
  /** Output buffered while disconnected (flushed on reconnect). */
31255
31755
  offlineBuffer = [];
31756
+ /**
31757
+ * Command ids already accepted (insertion-ordered, bounded). WS delivery is at-least-once —
31758
+ * a redelivered `COMMAND_DISPATCH` (reconnect/blip during a long AI run) must NOT run again:
31759
+ * re-running an AI prompt would waste tokens and echo the prompt a second time in the console.
31760
+ */
31761
+ handledCommands = /* @__PURE__ */ new Set();
31256
31762
  offlineBufferBytes = 0;
31763
+ /**
31764
+ * Console-sync state (ADR-0150). `consoleWatching` is set by `console.watch` from the server
31765
+ * (a web Console viewer is attached); only then does the cli emit `console.sync`. `consoleRev`
31766
+ * is the monotonic revision the web uses to detect a gap; `consoleSnapshotTimer` periodically
31767
+ * re-sends a full snapshot to self-heal. Watching resets to false on each reconnect (the server
31768
+ * re-asserts it), so a fresh session re-snapshots.
31769
+ */
31770
+ consoleWatching = false;
31771
+ consoleRev = 0;
31772
+ consoleSnapshotTimer = null;
31773
+ /** Lease expiry: stop syncing if the server does not renew `console.watch` in time. */
31774
+ consoleWatchExpiry = null;
31257
31775
  /** cli → server requests awaiting a reply (quota.check — ADR-0020). */
31258
31776
  pending = /* @__PURE__ */ new Map();
31259
31777
  /** Daily scheduled cli auto-update (ADR-0074); policy fed from each ws_token. */
@@ -31287,42 +31805,50 @@ var WsClient = class {
31287
31805
  */
31288
31806
  awaitConnected(timeoutMs) {
31289
31807
  if (this.sessionKey && this.bus.status === "connected") return Promise.resolve(true);
31290
- return new Promise((resolve6) => {
31808
+ return new Promise((resolve7) => {
31291
31809
  const waiter = () => {
31292
31810
  clearTimeout(timer);
31293
- resolve6(true);
31811
+ resolve7(true);
31294
31812
  };
31295
31813
  const timer = setTimeout(() => {
31296
31814
  this.connectWaiters = this.connectWaiters.filter((w) => w !== waiter);
31297
- resolve6(false);
31815
+ resolve7(false);
31298
31816
  }, timeoutMs);
31299
31817
  this.connectWaiters.push(waiter);
31300
31818
  });
31301
31819
  }
31302
31820
  /**
31303
31821
  * Resolve the reconnect-backoff cap (ms): the org value (from ws_token) over the
31304
- * env override over the default. Each is already clamped to the 10-min ceiling.
31822
+ * env override over the default. Each is already clamped to the 1-min ceiling.
31305
31823
  */
31306
31824
  reconnectMaxMs() {
31307
31825
  const sec = this.orgReconnectMaxSec ?? this.envReconnectMaxSec ?? RECONNECT_MAX_BACKOFF_DEFAULT_SEC;
31308
31826
  return sec * 1e3;
31309
31827
  }
31828
+ /** Mark a commandId as accepted for dedup, keeping the set bounded (drop the oldest). */
31829
+ markCommandHandled(commandId) {
31830
+ this.handledCommands.add(commandId);
31831
+ if (this.handledCommands.size > 1e3) {
31832
+ const oldest = this.handledCommands.values().next().value;
31833
+ if (oldest !== void 0) this.handledCommands.delete(oldest);
31834
+ }
31835
+ }
31310
31836
  /** Backoff sleep that can be woken early by reconnectNow(). */
31311
31837
  interruptibleSleep(ms) {
31312
31838
  if (this.forceReconnect) {
31313
31839
  this.forceReconnect = false;
31314
31840
  return Promise.resolve();
31315
31841
  }
31316
- return new Promise((resolve6) => {
31842
+ return new Promise((resolve7) => {
31317
31843
  const timer = setTimeout(() => {
31318
31844
  this.wakeReconnect = null;
31319
- resolve6();
31845
+ resolve7();
31320
31846
  }, ms);
31321
31847
  this.wakeReconnect = () => {
31322
31848
  clearTimeout(timer);
31323
31849
  this.wakeReconnect = null;
31324
31850
  this.forceReconnect = false;
31325
- resolve6();
31851
+ resolve7();
31326
31852
  };
31327
31853
  });
31328
31854
  }
@@ -31334,8 +31860,10 @@ var WsClient = class {
31334
31860
  for (let attempt = 0; !this.stopped; attempt++) {
31335
31861
  try {
31336
31862
  await this.connectOnce();
31337
- attempt = 0;
31338
- hashcodeInvalidStreak = 0;
31863
+ if (this.sessionEstablished) {
31864
+ attempt = 0;
31865
+ hashcodeInvalidStreak = 0;
31866
+ }
31339
31867
  } catch (err) {
31340
31868
  if (err instanceof CliApiError) {
31341
31869
  if (["HASHCODE_EXPIRED", "LINK_REVOKED"].includes(err.errorCode)) {
@@ -31376,8 +31904,9 @@ var WsClient = class {
31376
31904
  * One WS session: token → connect → handshake → handle messages until close.
31377
31905
  */
31378
31906
  connectOnce() {
31379
- return new Promise((resolve6, reject) => {
31907
+ return new Promise((resolve7, reject) => {
31380
31908
  void (async () => {
31909
+ this.sessionEstablished = false;
31381
31910
  this.bus.setStatus("connecting");
31382
31911
  const token = await requestWsToken(this.credential.serverUrl, this.credential.hashcode3);
31383
31912
  this.wsToken = token.wsToken;
@@ -31391,6 +31920,11 @@ var WsClient = class {
31391
31920
  timezone: token.timezone
31392
31921
  });
31393
31922
  }
31923
+ if (typeof token.cliRetentionDays === "number" && token.cliRetentionDays > 0) {
31924
+ logger.setRetentionDays(token.cliRetentionDays);
31925
+ pruneCommandHistoryByAge(token.cliRetentionDays);
31926
+ pruneCommandOutputByAge(token.cliRetentionDays);
31927
+ }
31394
31928
  writeProfileConfig(this.context.profileDir, {
31395
31929
  sessionSwitchPct: token.projectTokens?.sessionSwitchPct ?? 0,
31396
31930
  perPromptTokenLimit: token.projectTokens?.perPromptTokenLimit ?? 0
@@ -31404,7 +31938,12 @@ var WsClient = class {
31404
31938
  } else if (token.scope) {
31405
31939
  this.bus.setScope(token.scope);
31406
31940
  }
31407
- const wsUrl = this.credential.serverUrl.replace(/^http/, "ws") + "/ws";
31941
+ if (token.wsUrl !== void 0 && token.wsUrl !== this.credential.wsUrl) {
31942
+ this.credential.wsUrl = token.wsUrl || void 0;
31943
+ writeCredential(this.context.profileDir, this.credential);
31944
+ }
31945
+ const wsBase = this.credential.wsUrl ?? this.credential.serverUrl;
31946
+ const wsUrl = wsBase.replace(/^http/, "ws") + "/ws";
31408
31947
  const socket = new wrapper_default(wsUrl);
31409
31948
  this.socket = socket;
31410
31949
  socket.on("open", () => {
@@ -31434,7 +31973,7 @@ var WsClient = class {
31434
31973
  if (code === CLOSE_DRAINING) {
31435
31974
  this.bus.log("Server draining \u2014 will reconnect (backoff + jitter).");
31436
31975
  }
31437
- resolve6();
31976
+ resolve7();
31438
31977
  });
31439
31978
  socket.on("error", (err) => {
31440
31979
  this.clearHeartbeat();
@@ -31450,6 +31989,7 @@ var WsClient = class {
31450
31989
  if ("type" in message) {
31451
31990
  if (message.type === "hello_ack" && this.ecdh) {
31452
31991
  this.sessionKey = deriveSessionKey(this.ecdh, message.pubkey);
31992
+ this.sessionEstablished = true;
31453
31993
  if (message.workerName) this.bus.setWorker(message.workerName);
31454
31994
  if (message.projectName) {
31455
31995
  this.bus.setProject(message.projectName);
@@ -31460,12 +32000,14 @@ var WsClient = class {
31460
32000
  this.physicRoot = null;
31461
32001
  }
31462
32002
  logger.info("ws.connected", { version: CLI_VERSION });
32003
+ this.stopConsoleSync();
31463
32004
  this.bus.setStatus("connected");
31464
32005
  this.bus.log("\u2714 Connected to the server (session encryption ready).");
31465
32006
  this.sendMachineStatus();
31466
32007
  this.flushOfflineBuffer();
31467
32008
  this.startHeartbeat();
31468
32009
  this.startUsagePolling();
32010
+ this.startLogUpload();
31469
32011
  this.connectWaiters.splice(0).forEach((fn) => fn());
31470
32012
  return;
31471
32013
  }
@@ -31523,8 +32065,12 @@ var WsClient = class {
31523
32065
  switch (message.channel) {
31524
32066
  case WsChannels.PHYSIC_SYNC: {
31525
32067
  const sync = payload;
31526
- if (sync.oldName) rmSync4(join13(this.context.profileDir, sync.oldName), { recursive: true, force: true });
31527
- mkdirSync4(join13(this.context.profileDir, sync.newName), { recursive: true });
32068
+ if (sync.oldName) {
32069
+ const oldRoot = this.physicFolderPath(sync.oldName);
32070
+ if (oldRoot) void uninstallCron(oldRoot).catch(() => void 0);
32071
+ }
32072
+ if (sync.oldName) rmSync4(join15(this.context.profileDir, sync.oldName), { recursive: true, force: true });
32073
+ mkdirSync4(join15(this.context.profileDir, sync.newName), { recursive: true });
31528
32074
  this.physicRoot = this.physicFolderPath(sync.newName);
31529
32075
  this.bus.setProject(sync.newName);
31530
32076
  this.bus.log(`Physic project folder synced \u2192 ${sync.newName}`);
@@ -31533,7 +32079,9 @@ var WsClient = class {
31533
32079
  case WsChannels.PHYSIC_DELETE: {
31534
32080
  const del = payload;
31535
32081
  if (del.name) {
31536
- rmSync4(join13(this.context.profileDir, del.name), { recursive: true, force: true });
32082
+ const delRoot = this.physicFolderPath(del.name);
32083
+ if (delRoot) void uninstallCron(delRoot).catch(() => void 0);
32084
+ rmSync4(join15(this.context.profileDir, del.name), { recursive: true, force: true });
31537
32085
  this.bus.log(`Physic project folder deleted (project removed) \u2192 ${del.name}`);
31538
32086
  }
31539
32087
  this.bus.setProject(null);
@@ -31542,6 +32090,11 @@ var WsClient = class {
31542
32090
  }
31543
32091
  case WsChannels.COMMAND_DISPATCH: {
31544
32092
  const dispatch = payload;
32093
+ if (this.handledCommands.has(dispatch.commandId)) {
32094
+ logger.info("command.dispatch.duplicate", { commandId: dispatch.commandId });
32095
+ break;
32096
+ }
32097
+ this.markCommandHandled(dispatch.commandId);
31545
32098
  logger.info("command.dispatch", {
31546
32099
  commandId: dispatch.commandId,
31547
32100
  cmd: dispatch.cmd,
@@ -31593,6 +32146,53 @@ var WsClient = class {
31593
32146
  (reply) => this.send(WsChannels.FS_READ, reply, message.id)
31594
32147
  );
31595
32148
  break;
32149
+ case WsChannels.FS_WRITE: {
32150
+ const req = payload;
32151
+ void writeWorkerFile(this.physicRoot, req.path, req.content).then(
32152
+ (reply) => this.send(WsChannels.FS_WRITE, reply, message.id)
32153
+ );
32154
+ break;
32155
+ }
32156
+ case WsChannels.AUTONOMOUS_READ:
32157
+ if (this.physicRoot) {
32158
+ void readAutonomous(this.physicRoot).then(
32159
+ (reply) => this.send(WsChannels.AUTONOMOUS_READ, reply, message.id)
32160
+ );
32161
+ }
32162
+ break;
32163
+ case WsChannels.AUTONOMOUS_LOGS: {
32164
+ const req = payload;
32165
+ if (this.physicRoot) {
32166
+ void readAutonomousLogs(this.physicRoot, req.date).then(
32167
+ (reply) => this.send(WsChannels.AUTONOMOUS_LOGS, reply, message.id)
32168
+ );
32169
+ }
32170
+ break;
32171
+ }
32172
+ case WsChannels.AUTONOMOUS_WRITE: {
32173
+ const req = payload;
32174
+ if (this.physicRoot) {
32175
+ void writeAutonomous(this.physicRoot, req, req.by ?? "unknown").then(
32176
+ (reply) => this.send(WsChannels.AUTONOMOUS_WRITE, reply, message.id)
32177
+ );
32178
+ }
32179
+ break;
32180
+ }
32181
+ case WsChannels.CONFIG_READ:
32182
+ this.send(
32183
+ WsChannels.CONFIG_READ,
32184
+ { config: readConfigText(this.context.profileDir) },
32185
+ message.id
32186
+ );
32187
+ break;
32188
+ case WsChannels.CONFIG_WRITE: {
32189
+ const res = applyConfigText(
32190
+ this.context.profileDir,
32191
+ payload.config
32192
+ );
32193
+ this.send(WsChannels.CONFIG_WRITE, res, message.id);
32194
+ break;
32195
+ }
31596
32196
  case WsChannels.GIT_DIFF:
31597
32197
  void gitDiff(payload.path).then(
31598
32198
  (reply) => this.send(WsChannels.GIT_DIFF, reply, message.id)
@@ -31631,10 +32231,15 @@ var WsClient = class {
31631
32231
  }
31632
32232
  case WsChannels.LOG_READ: {
31633
32233
  const limit = Math.min(Math.max(payload.limit ?? 200, 1), 2e3);
31634
- const lines = readRecentLogLines(join13(this.context.profileDir, "logs"), limit);
32234
+ const lines = readRecentLogLines(join15(this.context.profileDir, "logs"), limit);
31635
32235
  this.send(WsChannels.LOG_READ, { lines }, message.id);
31636
32236
  break;
31637
32237
  }
32238
+ case WsChannels.COMMAND_OUTPUT_READ: {
32239
+ const output = readCommandOutput(payload.commandId);
32240
+ this.send(WsChannels.COMMAND_OUTPUT_READ, { output }, message.id);
32241
+ break;
32242
+ }
31638
32243
  case WsChannels.PROJECT_CREATE:
31639
32244
  void scaffoldProject(
31640
32245
  payload,
@@ -31645,6 +32250,7 @@ var WsClient = class {
31645
32250
  case WsChannels.PROJECT_ADD:
31646
32251
  void addProject(
31647
32252
  payload,
32253
+ this.context.profileDir,
31648
32254
  (p) => this.send(WsChannels.PROJECT_PROGRESS, p)
31649
32255
  ).then((reply) => this.send(WsChannels.PROJECT_ADD, reply, message.id));
31650
32256
  break;
@@ -31655,6 +32261,9 @@ var WsClient = class {
31655
32261
  (reply) => this.send(WsChannels.REVIEW_EVALUATE, reply, message.id)
31656
32262
  );
31657
32263
  break;
32264
+ case WsChannels.CONSOLE_WATCH:
32265
+ this.setConsoleWatching(payload.on);
32266
+ break;
31658
32267
  default:
31659
32268
  this.bus.log(`Unsupported channel: ${message.channel}`, "warn");
31660
32269
  }
@@ -31699,7 +32308,7 @@ var WsClient = class {
31699
32308
  if (perPromptLimit > 0) {
31700
32309
  const estimated = estimateTokens(prompt);
31701
32310
  if (estimated > perPromptLimit) {
31702
- this.rejectPromptOverLimit(commandId, origin, estimated, perPromptLimit);
32311
+ this.rejectPromptOverLimit(commandId, origin, prompt, estimated, perPromptLimit);
31703
32312
  return;
31704
32313
  }
31705
32314
  }
@@ -31714,7 +32323,7 @@ var WsClient = class {
31714
32323
  verdict = { commandId, ok: false, reasons: ["unavailable"] };
31715
32324
  }
31716
32325
  if (!verdict.ok) {
31717
- this.rejectByReview(commandId, origin, verdict.reasons);
32326
+ this.rejectByReview(commandId, origin, prompt, verdict.reasons);
31718
32327
  return;
31719
32328
  }
31720
32329
  }
@@ -31736,17 +32345,20 @@ var WsClient = class {
31736
32345
  });
31737
32346
  }
31738
32347
  const plan = planAiRun(prompt, config, startDir ?? workingDir);
32348
+ const markerArgs = plan.attempts[0]?.args ?? [];
31739
32349
  this.bus.push({ source: origin, kind: "aireq", text: `${plan.cmd} \u2039 ${prompt}` });
31740
32350
  logger.info("command.ai", { commandId, origin, cmd: plan.cmd, profiles: plan.attempts.length });
31741
32351
  if (origin === "local") {
31742
32352
  this.send(WsChannels.COMMAND_ANNOUNCE, {
31743
32353
  commandId,
31744
32354
  cmd: plan.cmd,
31745
- args: plan.args,
32355
+ args: markerArgs,
32356
+ // The raw prompt so the web console echoes it like this TUI (ADR-0108), not "claude".
32357
+ prompt,
31746
32358
  origin: "local"
31747
32359
  });
31748
32360
  }
31749
- recordCommand({ commandId, cmd: plan.cmd, args: plan.args });
32361
+ recordCommand({ commandId, cmd: plan.cmd, args: markerArgs });
31750
32362
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
31751
32363
  this.bus.startBusy(plan.cmd);
31752
32364
  let seq = 0;
@@ -31782,17 +32394,18 @@ var WsClient = class {
31782
32394
  this.bus.push({ source: origin, kind: "out", text: resultBuf });
31783
32395
  }
31784
32396
  },
31785
- onAttemptStart: (label, index, total) => this.bus.push({
31786
- source: origin,
31787
- kind: "log",
31788
- text: `\u2192 trying ${plan.cmd} profile "${label}" (${index + 1}/${total})\u2026`
31789
- }),
31790
- onAttemptFail: (label, reason) => this.bus.push({
31791
- source: origin,
31792
- kind: "log",
31793
- text: reason === "limit" ? `profile "${label}" hit its session limit \u2014 trying next` : `profile "${label}" failed to authenticate \u2014 trying next`,
31794
- level: "warn"
31795
- })
32397
+ onAttemptStart: (label, index, total) => {
32398
+ const text = `\u2192 trying ${plan.cmd} profile "${label}" (${index + 1}/${total})\u2026`;
32399
+ this.bus.push({ source: origin, kind: "log", text });
32400
+ this.send(WsChannels.COMMAND_OUTPUT, { commandId, seq: seq++, chunk: `${text}
32401
+ `, log: true });
32402
+ },
32403
+ onAttemptFail: (label, reason) => {
32404
+ const text = reason === "limit" ? `profile "${label}" hit its session limit \u2014 trying next` : `profile "${label}" failed to authenticate \u2014 trying next`;
32405
+ this.bus.push({ source: origin, kind: "log", text, level: "warn" });
32406
+ this.send(WsChannels.COMMAND_OUTPUT, { commandId, seq: seq++, chunk: `${text}
32407
+ `, log: true });
32408
+ }
31796
32409
  });
31797
32410
  this.bus.endBusy(plan.cmd);
31798
32411
  const profileLabel = result.workedDir ? profileDisplayLabel(result.workedDir) : null;
@@ -31811,10 +32424,19 @@ var WsClient = class {
31811
32424
  this.send(WsChannels.COMMAND_HISTORY, {
31812
32425
  commandId,
31813
32426
  cmd: plan.cmd,
31814
- args: plan.args,
32427
+ args: markerArgs,
31815
32428
  status: result.exitCode === 0 ? "done" : "failed",
31816
32429
  exitCode: result.exitCode,
31817
32430
  tokens: result.usage.tokens,
32431
+ // The ai_tokens split for this run (ADR-0145); attached only when we metered real tokens.
32432
+ ...result.usage.tokens > 0 && {
32433
+ tokensBreakdown: {
32434
+ input: result.usage.input,
32435
+ output: result.usage.output,
32436
+ cacheRead: result.usage.cacheRead,
32437
+ cacheCreation: result.usage.cacheCreation
32438
+ }
32439
+ },
31818
32440
  startedAt,
31819
32441
  finishedAt: (/* @__PURE__ */ new Date()).toISOString()
31820
32442
  });
@@ -31823,7 +32445,16 @@ var WsClient = class {
31823
32445
  { metric: UsageMetric.COMMANDS, amount: 1, occurredAt }
31824
32446
  ];
31825
32447
  if (result.exitCode === 0 && result.usage.tokens > 0) {
31826
- events.push({ metric: UsageMetric.AI_TOKENS, amount: result.usage.tokens, occurredAt });
32448
+ events.push({
32449
+ metric: UsageMetric.AI_TOKENS,
32450
+ amount: result.usage.tokens,
32451
+ occurredAt,
32452
+ // The ai_tokens split so the server can persist + display it (ADR-0145).
32453
+ inputTokens: result.usage.input,
32454
+ outputTokens: result.usage.output,
32455
+ cacheReadTokens: result.usage.cacheRead,
32456
+ cacheCreationTokens: result.usage.cacheCreation
32457
+ });
31827
32458
  this.bus.addTokens?.(result.usage.tokens);
31828
32459
  }
31829
32460
  this.reportUsage(events, profileLabel);
@@ -31847,7 +32478,7 @@ var WsClient = class {
31847
32478
  if (threshold <= 0 || cmd !== "claude") return null;
31848
32479
  const util2 = this.usageSnapshot?.session.utilizationPct ?? 0;
31849
32480
  if (util2 < threshold) return null;
31850
- const dirs = claudeConfigDirs(config);
32481
+ const dirs = claudeHomeDirs(config);
31851
32482
  if (dirs.length < 2) return null;
31852
32483
  const currentIndex = workingDir ? dirs.indexOf(workingDir) : -1;
31853
32484
  const next = dirs[(currentIndex + 1) % dirs.length];
@@ -31858,7 +32489,7 @@ var WsClient = class {
31858
32489
  * without spawning: surface it in the transcript + close the command on the server with a
31859
32490
  * PROMPT_TOKEN_LIMIT_EXCEEDED note (exit 1). No usage is metered (nothing ran).
31860
32491
  */
31861
- rejectPromptOverLimit(commandId, origin, estimated, limit) {
32492
+ rejectPromptOverLimit(commandId, origin, prompt, estimated, limit) {
31862
32493
  const message = `[PROMPT_TOKEN_LIMIT_EXCEEDED] Prompt (~${estimated} tokens) exceeds the project per-prompt limit of ${limit} tokens.`;
31863
32494
  logger.warn("command.ai.prompt-limit", { commandId, origin, estimated, limit });
31864
32495
  this.bus.push({ source: origin, kind: "log", text: message, level: "error" });
@@ -31867,6 +32498,7 @@ var WsClient = class {
31867
32498
  commandId,
31868
32499
  cmd: "claude",
31869
32500
  args: [],
32501
+ prompt,
31870
32502
  origin: "local"
31871
32503
  });
31872
32504
  }
@@ -31884,7 +32516,7 @@ var WsClient = class {
31884
32516
  * verdict + close the command on the server. `reasons` contains only violation categories
31885
32517
  * (never secret values); a "unavailable" reason maps to no-outbound-available.
31886
32518
  */
31887
- rejectByReview(commandId, origin, reasons) {
32519
+ rejectByReview(commandId, origin, prompt, reasons) {
31888
32520
  const unavailable = reasons.includes("unavailable");
31889
32521
  const code = unavailable ? "OUTBOUND_REVIEW_UNAVAILABLE" : "OUTBOUND_REVIEW_REJECTED";
31890
32522
  const detail = reasons.filter((r) => r !== "unavailable").join(", ");
@@ -31896,6 +32528,7 @@ var WsClient = class {
31896
32528
  commandId,
31897
32529
  cmd: "claude",
31898
32530
  args: [],
32531
+ prompt,
31899
32532
  origin: "local"
31900
32533
  });
31901
32534
  }
@@ -31929,12 +32562,65 @@ var WsClient = class {
31929
32562
  };
31930
32563
  this.socket.send(JSON.stringify(envelope));
31931
32564
  }
32565
+ /**
32566
+ * Console sync (ADR-0150): emit one incremental `console.sync` event (add/update/clear) — only
32567
+ * while a web Console viewer is attached (`consoleWatching`); each carries the next `rev` so the
32568
+ * web detects a gap. Dropped when disconnected — the next snapshot on (re)attach re-syncs.
32569
+ */
32570
+ emitConsole(ev) {
32571
+ if (!this.consoleWatching) return;
32572
+ this.send(WsChannels.CONSOLE_SYNC, { ...ev, rev: ++this.consoleRev });
32573
+ }
32574
+ /** Send a full transcript snapshot (on attach + the periodic self-heal — ADR-0150). */
32575
+ sendConsoleSnapshot() {
32576
+ const entries2 = this.bus.snapshot().map(toDtoEntry);
32577
+ this.send(WsChannels.CONSOLE_SYNC, {
32578
+ kind: "snapshot",
32579
+ rev: ++this.consoleRev,
32580
+ entries: entries2
32581
+ });
32582
+ }
32583
+ /**
32584
+ * Handle `console.watch` (ADR-0150) — a renewable lease. `on` renews it: start mirroring on the
32585
+ * first assertion (fresh snapshot + arm the periodic snapshot); later renewals just extend the
32586
+ * lease (no re-snapshot). No renewal within `CONSOLE_WATCH_LEASE_MS` ⇒ the lease expires and
32587
+ * syncing stops. An explicit `on:false` stops immediately.
32588
+ */
32589
+ setConsoleWatching(on) {
32590
+ if (!on) {
32591
+ this.stopConsoleSync();
32592
+ return;
32593
+ }
32594
+ if (this.consoleWatchExpiry) clearTimeout(this.consoleWatchExpiry);
32595
+ this.consoleWatchExpiry = setTimeout(() => this.stopConsoleSync(), CONSOLE_WATCH_LEASE_MS);
32596
+ this.consoleWatchExpiry.unref?.();
32597
+ if (this.consoleWatching) return;
32598
+ this.consoleWatching = true;
32599
+ this.sendConsoleSnapshot();
32600
+ this.consoleSnapshotTimer = setInterval(
32601
+ () => this.sendConsoleSnapshot(),
32602
+ CONSOLE_SNAPSHOT_INTERVAL_MS
32603
+ );
32604
+ this.consoleSnapshotTimer.unref?.();
32605
+ }
32606
+ /** Stop mirroring the transcript (lease expired / viewer left / reconnect) — clears both timers. */
32607
+ stopConsoleSync() {
32608
+ this.consoleWatching = false;
32609
+ if (this.consoleSnapshotTimer) {
32610
+ clearInterval(this.consoleSnapshotTimer);
32611
+ this.consoleSnapshotTimer = null;
32612
+ }
32613
+ if (this.consoleWatchExpiry) {
32614
+ clearTimeout(this.consoleWatchExpiry);
32615
+ this.consoleWatchExpiry = null;
32616
+ }
32617
+ }
31932
32618
  /**
31933
32619
  * Send a cli → server request and await the reply by envelope id (15s timeout).
31934
32620
  * Used for quota.check before spawning an AI cli (ADR-0020).
31935
32621
  */
31936
32622
  request(channel, data) {
31937
- return new Promise((resolve6, reject) => {
32623
+ return new Promise((resolve7, reject) => {
31938
32624
  if (!this.socket || !this.sessionKey) {
31939
32625
  reject(new Error("Not connected to the server."));
31940
32626
  return;
@@ -31954,7 +32640,7 @@ var WsClient = class {
31954
32640
  }, 15e3);
31955
32641
  timer.unref?.();
31956
32642
  this.pending.set(envelope.id, {
31957
- resolve: (value) => resolve6(value),
32643
+ resolve: (value) => resolve7(value),
31958
32644
  timer
31959
32645
  });
31960
32646
  this.socket.send(JSON.stringify(envelope));
@@ -32001,7 +32687,7 @@ var WsClient = class {
32001
32687
  /** Create the physic folder at an absolute path if missing (best-effort). */
32002
32688
  ensurePhysicFolderPath(folder) {
32003
32689
  try {
32004
- if (!existsSync7(folder)) {
32690
+ if (!existsSync8(folder)) {
32005
32691
  mkdirSync4(folder, { recursive: true });
32006
32692
  this.bus.log(`Created physic project folder: ${folder}`);
32007
32693
  }
@@ -32016,7 +32702,7 @@ var WsClient = class {
32016
32702
  */
32017
32703
  physicFolderPath(projectName) {
32018
32704
  const safe = projectName.replace(/[/\\]/g, "_").replace(/\.\./g, "_").trim();
32019
- return safe ? join13(this.context.profileDir, safe) : null;
32705
+ return safe ? join15(this.context.profileDir, safe) : null;
32020
32706
  }
32021
32707
  /**
32022
32708
  * Report machine.status: version + the state of the physic project being served
@@ -32030,8 +32716,9 @@ var WsClient = class {
32030
32716
  projects: [],
32031
32717
  version: CLI_VERSION,
32032
32718
  physicPath,
32033
- physicPathExists: physicPath ? existsSync7(physicPath) : void 0,
32034
- autonomousRunning: false
32719
+ physicPathExists: physicPath ? existsSync8(physicPath) : void 0,
32720
+ // Real coarse state (ADR-0152): !paused and a recent tick (was hard-coded false).
32721
+ autonomousRunning: physicPath ? isAutonomousRunning(physicPath) : false
32035
32722
  });
32036
32723
  }
32037
32724
  /** Heartbeat every 30s: ping + periodically re-report machine.status. */
@@ -32048,6 +32735,8 @@ var WsClient = class {
32048
32735
  this.heartbeat = null;
32049
32736
  if (this.usageTimer) clearInterval(this.usageTimer);
32050
32737
  this.usageTimer = null;
32738
+ if (this.logUploadTimer) clearInterval(this.logUploadTimer);
32739
+ this.logUploadTimer = null;
32051
32740
  }
32052
32741
  /**
32053
32742
  * Poll the Claude subscription usage API and push a `machine.usage` snapshot (ADR-0072).
@@ -32060,7 +32749,7 @@ var WsClient = class {
32060
32749
  const config = readProfileConfig(this.context.profileDir);
32061
32750
  const aiCli2 = config.aiCli ?? "claude";
32062
32751
  const working = getWorkingProfile(this.context.profileDir, aiCli2);
32063
- const configured = claudeConfigDirs(config);
32752
+ const configured = claudeHomeDirs(config);
32064
32753
  const dirs = working ? [working, ...configured.filter((d) => d !== working)] : configured;
32065
32754
  const profile = working ? basename3(working) : dirs[0] ? basename3(dirs[0]) : "default";
32066
32755
  const api = await checkClaudeUsage(dirs, aiCli2);
@@ -32085,6 +32774,29 @@ var WsClient = class {
32085
32774
  this.usageTimer.unref?.();
32086
32775
  void this.pollUsage();
32087
32776
  }
32777
+ /** Start the periodic upload of the cli's own JSONL log to the server (ADR-0122). */
32778
+ startLogUpload() {
32779
+ if (this.logUploadTimer) clearInterval(this.logUploadTimer);
32780
+ this.logUploadTimer = setInterval(() => this.uploadLog(), LOG_UPLOAD_MS);
32781
+ this.logUploadTimer.unref?.();
32782
+ this.uploadLog();
32783
+ }
32784
+ /**
32785
+ * Upload the newest cli log file + the total log footprint to the server (ADR-0122), so it is
32786
+ * stored + retained server-side and counted in the machine user's storage. Skips when nothing
32787
+ * changed since the last upload. Best-effort — never disrupts the session.
32788
+ */
32789
+ uploadLog() {
32790
+ try {
32791
+ const upload = readLogUpload(join15(this.context.profileDir, "logs"));
32792
+ if (!upload) return;
32793
+ const sig = `${upload.fileName}:${upload.totalBytes}`;
32794
+ if (sig === this.lastLogSig) return;
32795
+ this.lastLogSig = sig;
32796
+ this.send(WsChannels.MACHINE_LOG, upload);
32797
+ } catch {
32798
+ }
32799
+ }
32088
32800
  /**
32089
32801
  * Logout: close WS, delete `.cre`, and prompt to pair again (Unlinked state).
32090
32802
  */
@@ -32104,10 +32816,10 @@ var WsClient = class {
32104
32816
  };
32105
32817
 
32106
32818
  // src/core/instance-lock.ts
32107
- import { readFileSync as readFileSync12, rmSync as rmSync5, writeFileSync as writeFileSync6 } from "fs";
32108
- import { join as join14 } from "path";
32819
+ import { readFileSync as readFileSync14, rmSync as rmSync5, writeFileSync as writeFileSync6 } from "fs";
32820
+ import { join as join16 } from "path";
32109
32821
  function lockPath(profileDir2) {
32110
- return join14(profileDir2, "start.lock");
32822
+ return join16(profileDir2, "start.lock");
32111
32823
  }
32112
32824
  function isAlive(pid) {
32113
32825
  try {
@@ -32119,7 +32831,7 @@ function isAlive(pid) {
32119
32831
  }
32120
32832
  function lockHolder(profileDir2) {
32121
32833
  try {
32122
- const pid = Number.parseInt(readFileSync12(lockPath(profileDir2), "utf8").trim(), 10);
32834
+ const pid = Number.parseInt(readFileSync14(lockPath(profileDir2), "utf8").trim(), 10);
32123
32835
  if (Number.isInteger(pid) && pid > 0 && isAlive(pid)) return pid;
32124
32836
  } catch {
32125
32837
  }
@@ -32132,7 +32844,7 @@ function acquireInstanceLock(profileDir2) {
32132
32844
  }
32133
32845
  function releaseInstanceLock(profileDir2) {
32134
32846
  try {
32135
- const pid = Number.parseInt(readFileSync12(lockPath(profileDir2), "utf8").trim(), 10);
32847
+ const pid = Number.parseInt(readFileSync14(lockPath(profileDir2), "utf8").trim(), 10);
32136
32848
  if (pid === process.pid) rmSync5(lockPath(profileDir2), { force: true });
32137
32849
  } catch {
32138
32850
  }
@@ -32181,6 +32893,24 @@ var SessionBus = class {
32181
32893
  this.emitter.emit("transcript", entry);
32182
32894
  return entry.id;
32183
32895
  }
32896
+ /**
32897
+ * Wipe the transcript (`/clear` + idle auto-clear — ADR-0057): empty the replay history and
32898
+ * notify consumers so the TUI clears the screen and the console-sync emitter (ADR-0150) sends
32899
+ * a `clear` event. The current snapshot afterwards is empty (until new lines arrive).
32900
+ */
32901
+ clear() {
32902
+ this.history.length = 0;
32903
+ this.emitter.emit("clear");
32904
+ }
32905
+ /** Subscribe to transcript clears (TUI screen wipe + console.sync `clear` — ADR-0150). */
32906
+ onClear(fn) {
32907
+ this.emitter.on("clear", fn);
32908
+ return () => this.emitter.off("clear", fn);
32909
+ }
32910
+ /** The current transcript entries (a copy) — the console-sync snapshot source (ADR-0150). */
32911
+ snapshot() {
32912
+ return [...this.history];
32913
+ }
32184
32914
  /**
32185
32915
  * Producer → consumers: replace the text of an already-pushed entry (ADR-0108) — used to
32186
32916
  * grow a streaming `result` block in place without pushing a new line per chunk. No-op if the
@@ -37616,8 +38346,8 @@ var Ink = class {
37616
38346
  }
37617
38347
  }
37618
38348
  async waitUntilExit() {
37619
- this.exitPromise ||= new Promise((resolve6, reject) => {
37620
- this.resolveExitPromise = resolve6;
38349
+ this.exitPromise ||= new Promise((resolve7, reject) => {
38350
+ this.resolveExitPromise = resolve7;
37621
38351
  this.rejectExitPromise = reject;
37622
38352
  });
37623
38353
  return this.exitPromise;
@@ -37984,17 +38714,17 @@ var import_react21 = __toESM(require_react(), 1);
37984
38714
  var import_react26 = __toESM(require_react(), 1);
37985
38715
 
37986
38716
  // src/core/input-history.ts
37987
- import { existsSync as existsSync9, readFileSync as readFileSync14, writeFileSync as writeFileSync7 } from "fs";
37988
- import { join as join15 } from "path";
38717
+ import { existsSync as existsSync10, readFileSync as readFileSync16, writeFileSync as writeFileSync7 } from "fs";
38718
+ import { join as join17 } from "path";
37989
38719
  var MAX_HISTORY = 200;
37990
38720
  function historyPath(profileDir2) {
37991
- return join15(profileDir2, "input-history.json");
38721
+ return join17(profileDir2, "input-history.json");
37992
38722
  }
37993
38723
  function loadInputHistory(profileDir2) {
37994
38724
  const path = historyPath(profileDir2);
37995
- if (!existsSync9(path)) return [];
38725
+ if (!existsSync10(path)) return [];
37996
38726
  try {
37997
- const data = JSON.parse(readFileSync14(path, "utf8"));
38727
+ const data = JSON.parse(readFileSync16(path, "utf8"));
37998
38728
  return Array.isArray(data) ? data.filter((x) => typeof x === "string") : [];
37999
38729
  } catch {
38000
38730
  return [];
@@ -38254,20 +38984,20 @@ function TranscriptLine({ line }) {
38254
38984
  var import_react24 = __toESM(require_react(), 1);
38255
38985
 
38256
38986
  // src/ui/slash-commands.ts
38257
- import { existsSync as existsSync10 } from "fs";
38258
- import { join as join16 } from "path";
38987
+ import { existsSync as existsSync11 } from "fs";
38988
+ import { join as join18 } from "path";
38259
38989
  var CONFIG_KEYS = [
38260
38990
  "aiCli",
38261
- "aiArgs",
38262
- "claudeConfigDir",
38991
+ "claudeHome",
38263
38992
  "codexHome",
38993
+ "antigravityHome",
38264
38994
  "physicPath",
38265
38995
  "commandHistoryUploadMinutes",
38266
38996
  "autoUpdate",
38267
38997
  "aiEnv"
38268
38998
  ];
38269
38999
  function configPath(profileDir2) {
38270
- return join16(profileDir2, "config.json");
39000
+ return join18(profileDir2, "config.json");
38271
39001
  }
38272
39002
  function parseConfigAssignment(key, valueTokens) {
38273
39003
  const value = valueTokens.join(" ").trim();
@@ -38276,14 +39006,13 @@ function parseConfigAssignment(key, valueTokens) {
38276
39006
  case "physicPath":
38277
39007
  if (!value) return { error: `Missing value for "${key}".` };
38278
39008
  return { patch: { [key]: value } };
38279
- case "claudeConfigDir":
38280
- case "codexHome": {
39009
+ case "claudeHome":
39010
+ case "codexHome":
39011
+ case "antigravityHome": {
38281
39012
  const dirs = valueTokens.filter((t) => t.trim());
38282
39013
  if (dirs.length === 0) return { error: `Missing value for "${key}".` };
38283
- return { patch: { [key]: dirs.length === 1 ? dirs[0] : dirs } };
39014
+ return { patch: { [key]: dirs.map((profile) => ({ profile, model: "" })) } };
38284
39015
  }
38285
- case "aiArgs":
38286
- return { patch: { aiArgs: valueTokens } };
38287
39016
  case "commandHistoryUploadMinutes": {
38288
39017
  const n = Number(value);
38289
39018
  if (!Number.isFinite(n) || n <= 0) return { error: `"${value}" is not a positive number.` };
@@ -38313,22 +39042,19 @@ function runConfig(ctx) {
38313
39042
  return;
38314
39043
  }
38315
39044
  if (sub === "init") {
38316
- if (existsSync10(path)) {
38317
- ctx.print(`config already exists: ${path} (use /config set <key> <value>)`, "warn");
39045
+ const writeDefaults = (verb) => {
39046
+ overwriteProfileConfig(dir2, defaultProfileConfig());
39047
+ ctx.print(`\u2714 ${verb} default config: ${path}`);
39048
+ ctx.print("add claude profiles: /config set claudeHome .claude .claude-1 (tried in order)");
39049
+ };
39050
+ if (existsSync11(path)) {
39051
+ ctx.confirm(
39052
+ `config exists: ${path} \u2014 replace it with defaults? (y/N)`,
39053
+ () => writeDefaults("replaced with")
39054
+ );
38318
39055
  return;
38319
39056
  }
38320
- const defaults = {
38321
- aiCli: "claude",
38322
- aiArgs: ["-p"],
38323
- // Lists so failover across profiles is discoverable (ADR-0057) — add more dirs.
38324
- claudeConfigDir: [".claude"],
38325
- codexHome: [".codex"],
38326
- autoUpdate: true,
38327
- commandHistoryUploadMinutes: 10
38328
- };
38329
- writeProfileConfig(dir2, defaults);
38330
- ctx.print(`\u2714 wrote default config: ${path}`);
38331
- ctx.print("add claude profiles: /config set claudeConfigDir .claude .claude-1 (tried in order)");
39057
+ writeDefaults("wrote");
38332
39058
  return;
38333
39059
  }
38334
39060
  if (sub === "set") {
@@ -38406,7 +39132,7 @@ function runOutput(ctx) {
38406
39132
  }
38407
39133
  function runLogs(ctx) {
38408
39134
  const n = Math.max(1, Math.min(200, Number(ctx.args[0]) || 20));
38409
- const dir2 = join16(ctx.info.profileDir, "logs");
39135
+ const dir2 = join18(ctx.info.profileDir, "logs");
38410
39136
  const lines = readRecentLogLines(dir2, n);
38411
39137
  if (lines.length === 0) {
38412
39138
  ctx.print("No log lines yet.");
@@ -38729,11 +39455,30 @@ function App2({
38729
39455
  const [expanded, setExpanded] = (0, import_react26.useState)(() => /* @__PURE__ */ new Set());
38730
39456
  const [history, setHistory] = (0, import_react26.useState)(() => loadInputHistory(info.profileDir));
38731
39457
  const [startedAt] = (0, import_react26.useState)(() => Date.now());
39458
+ const [pendingConfirm, setPendingConfirm] = (0, import_react26.useState)(null);
39459
+ const [autoClearIdleMs] = (0, import_react26.useState)(() => {
39460
+ const min = readProfileConfig(info.profileDir).autoClearIdleMinutes ?? 10;
39461
+ return min > 0 ? min * 6e4 : 0;
39462
+ });
39463
+ const lastActivityRef = (0, import_react26.useRef)(startedAt);
39464
+ const busyRef = (0, import_react26.useRef)(busy);
39465
+ busyRef.current = busy;
39466
+ const entriesLenRef = (0, import_react26.useRef)(entries2.length);
39467
+ entriesLenRef.current = entries2.length;
39468
+ const pendingConfirmRef = (0, import_react26.useRef)(pendingConfirm);
39469
+ pendingConfirmRef.current = pendingConfirm;
39470
+ const clearTranscript = (0, import_react26.useCallback)(() => {
39471
+ setEntries([]);
39472
+ setScrollBack(0);
39473
+ stdout.write("\x1B[2J\x1B[3J\x1B[H");
39474
+ bus.clear();
39475
+ }, [stdout, bus]);
38732
39476
  (0, import_react26.useEffect)(() => {
38733
39477
  const offTranscript = bus.onTranscript((entry) => {
38734
39478
  setEntries(
38735
39479
  (prev) => prev.length >= ENTRY_CAP ? [...prev.slice(prev.length - ENTRY_CAP + 1), entry] : [...prev, entry]
38736
39480
  );
39481
+ if (entry.source !== "system") lastActivityRef.current = Date.now();
38737
39482
  if (entry.kind === "aireq" || entry.kind === "aires") setExpanded(/* @__PURE__ */ new Set());
38738
39483
  });
38739
39484
  const offUpdate = bus.onTranscriptUpdate(
@@ -38760,6 +39505,21 @@ function App2({
38760
39505
  offTokens();
38761
39506
  };
38762
39507
  }, [bus]);
39508
+ (0, import_react26.useEffect)(() => {
39509
+ if (autoClearIdleMs <= 0) return;
39510
+ const timer = setInterval(() => {
39511
+ if (busyRef.current !== null || pendingConfirmRef.current || entriesLenRef.current === 0) return;
39512
+ if (Date.now() - lastActivityRef.current < autoClearIdleMs) return;
39513
+ clearTranscript();
39514
+ bus.push({
39515
+ source: "system",
39516
+ kind: "log",
39517
+ text: `Transcript auto-cleared after ${Math.round(autoClearIdleMs / 6e4)} min idle.`
39518
+ });
39519
+ lastActivityRef.current = Date.now();
39520
+ }, 6e4);
39521
+ return () => clearInterval(timer);
39522
+ }, [autoClearIdleMs, bus, clearTranscript]);
38763
39523
  const bodyHeight = Math.max(3, rows - CHROME_ROWS);
38764
39524
  const textWidth = Math.max(10, cols - 1 - 8);
38765
39525
  const lines = (0, import_react26.useMemo)(() => flattenEntries(entries2, textWidth, expanded), [entries2, textWidth, expanded]);
@@ -38785,19 +39545,28 @@ function App2({
38785
39545
  };
38786
39546
  const handleSubmit = (input) => {
38787
39547
  setScrollBack(0);
38788
- setHistory((prev) => appendInputHistory(info.profileDir, input, prev));
39548
+ lastActivityRef.current = Date.now();
38789
39549
  const trimmed = input.trim();
39550
+ if (pendingConfirm) {
39551
+ const { onYes } = pendingConfirm;
39552
+ setPendingConfirm(null);
39553
+ bus.push({ source: "local", kind: "cmd", text: trimmed });
39554
+ if (/^y(es)?$/i.test(trimmed)) onYes();
39555
+ else bus.push({ source: "local", kind: "log", text: "cancelled." });
39556
+ return;
39557
+ }
39558
+ setHistory((prev) => appendInputHistory(info.profileDir, input, prev));
38790
39559
  if (trimmed.startsWith("/")) {
38791
39560
  bus.push({ source: "local", kind: "cmd", text: trimmed });
38792
39561
  runSlashCommand(trimmed, {
38793
39562
  info,
38794
39563
  print: (text, level) => bus.push({ source: "local", kind: "log", text, level }),
38795
- clear: () => {
38796
- setEntries([]);
38797
- setScrollBack(0);
38798
- stdout.write("\x1B[2J\x1B[3J\x1B[H");
38799
- },
39564
+ clear: clearTranscript,
38800
39565
  quit: () => exit(),
39566
+ confirm: (prompt, onYes) => {
39567
+ bus.push({ source: "local", kind: "log", text: prompt });
39568
+ setPendingConfirm({ prompt, onYes });
39569
+ },
38801
39570
  submitAi: (aiInput) => bus.submitLocal(aiInput),
38802
39571
  reconnect: () => bus.requestReconnect(),
38803
39572
  expand: expandFold,
@@ -38864,11 +39633,12 @@ async function runStart(profileDir2, profileName) {
38864
39633
  return;
38865
39634
  }
38866
39635
  logger.configure({
38867
- dir: join17(profileDir2, "logs"),
39636
+ dir: join19(profileDir2, "logs"),
38868
39637
  level: resolveLogLevel(),
38869
39638
  base: { version: CLI_VERSION, profile: profileName, scope: credential.scope }
38870
39639
  });
38871
39640
  logger.info("cli.start", { serverUrl: credential.serverUrl });
39641
+ ensureProfileConfig(profileDir2);
38872
39642
  const config = readProfileConfig(profileDir2);
38873
39643
  const autoUpdate = config.autoUpdate !== false && process.env.FOURPM_NO_UPDATE !== "1";
38874
39644
  const result = await checkAndUpdate(credential.serverUrl, autoUpdate);
@@ -38883,7 +39653,7 @@ async function runStart(profileDir2, profileName) {
38883
39653
  if (result.action === "updated") {
38884
39654
  logger.info("update.updated", { version: result.version });
38885
39655
  console.log("Restarting with the new version\u2026");
38886
- const child = spawn4(process.execPath, process.argv.slice(1), {
39656
+ const child = spawn5(process.execPath, process.argv.slice(1), {
38887
39657
  stdio: "inherit",
38888
39658
  env: { ...process.env, FOURPM_NO_UPDATE: "1" }
38889
39659
  });
@@ -38921,7 +39691,7 @@ async function runStart(profileDir2, profileName) {
38921
39691
  logger.error("ws.run.crash", { message: e.message, stack: e.stack });
38922
39692
  if (client.isStopped) return;
38923
39693
  bus.log(`Recovered from an internal error \u2014 reconnecting: ${e.message}`, "error");
38924
- await new Promise((resolve6) => setTimeout(resolve6, 1e3));
39694
+ await new Promise((resolve7) => setTimeout(resolve7, 1e3));
38925
39695
  }
38926
39696
  }
38927
39697
  };
@@ -39048,10 +39818,10 @@ function Picker({
39048
39818
  ] });
39049
39819
  }
39050
39820
  function selectFromList(items, title) {
39051
- return new Promise((resolve6) => {
39821
+ return new Promise((resolve7) => {
39052
39822
  let chosen = null;
39053
39823
  const app = render_default(/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Picker, { items, title, onDone: (v) => chosen = v }));
39054
- void app.waitUntilExit().then(() => resolve6(chosen));
39824
+ void app.waitUntilExit().then(() => resolve7(chosen));
39055
39825
  });
39056
39826
  }
39057
39827