@4pm/cli 0.8.3 → 1.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +16 -0
  2. package/dist/index.js +1666 -273
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -2283,7 +2283,7 @@ var require_websocket = __commonJS({
2283
2283
  var http = __require("http");
2284
2284
  var net = __require("net");
2285
2285
  var tls = __require("tls");
2286
- var { randomBytes: randomBytes3, createHash: createHash3 } = __require("crypto");
2286
+ var { randomBytes: randomBytes3, createHash: createHash5 } = __require("crypto");
2287
2287
  var { Duplex, Readable } = __require("stream");
2288
2288
  var { URL: URL2 } = __require("url");
2289
2289
  var PerMessageDeflate2 = require_permessage_deflate();
@@ -2951,7 +2951,7 @@ var require_websocket = __commonJS({
2951
2951
  abortHandshake(websocket, socket, "Invalid Upgrade header");
2952
2952
  return;
2953
2953
  }
2954
- const digest = createHash3("sha1").update(key + GUID).digest("base64");
2954
+ const digest = createHash5("sha1").update(key + GUID).digest("base64");
2955
2955
  if (res.headers["sec-websocket-accept"] !== digest) {
2956
2956
  abortHandshake(websocket, socket, "Invalid Sec-WebSocket-Accept header");
2957
2957
  return;
@@ -3320,7 +3320,7 @@ var require_websocket_server = __commonJS({
3320
3320
  var EventEmitter4 = __require("events");
3321
3321
  var http = __require("http");
3322
3322
  var { Duplex } = __require("stream");
3323
- var { createHash: createHash3 } = __require("crypto");
3323
+ var { createHash: createHash5 } = __require("crypto");
3324
3324
  var extension2 = require_extension();
3325
3325
  var PerMessageDeflate2 = require_permessage_deflate();
3326
3326
  var subprotocol2 = require_subprotocol();
@@ -3627,7 +3627,7 @@ var require_websocket_server = __commonJS({
3627
3627
  );
3628
3628
  }
3629
3629
  if (this._state > RUNNING) return abortHandshake(socket, 503);
3630
- const digest = createHash3("sha1").update(key + GUID).digest("base64");
3630
+ const digest = createHash5("sha1").update(key + GUID).digest("base64");
3631
3631
  const headers = [
3632
3632
  "HTTP/1.1 101 Switching Protocols",
3633
3633
  "Upgrade: websocket",
@@ -5973,14 +5973,14 @@ var require_signal_exit = __commonJS({
5973
5973
  if (opts && opts.alwaysLast) {
5974
5974
  ev = "afterexit";
5975
5975
  }
5976
- var remove = function() {
5976
+ var remove2 = function() {
5977
5977
  emitter.removeListener(ev, cb);
5978
5978
  if (emitter.listeners("exit").length === 0 && emitter.listeners("afterexit").length === 0) {
5979
5979
  unload();
5980
5980
  }
5981
5981
  };
5982
5982
  emitter.on(ev, cb);
5983
- return remove;
5983
+ return remove2;
5984
5984
  };
5985
5985
  unload = function unload2() {
5986
5986
  if (!loaded || !processOk(global.process)) {
@@ -11606,7 +11606,7 @@ var require_react_reconciler_development = __commonJS({
11606
11606
  }
11607
11607
  }
11608
11608
  var assign = Object.assign;
11609
- function get(key) {
11609
+ function get2(key) {
11610
11610
  return key._reactInternals;
11611
11611
  }
11612
11612
  function set(key, value) {
@@ -11961,7 +11961,7 @@ var require_react_reconciler_development = __commonJS({
11961
11961
  instance._warnedAboutRefsInRender = true;
11962
11962
  }
11963
11963
  }
11964
- var fiber = get(component);
11964
+ var fiber = get2(component);
11965
11965
  if (!fiber) {
11966
11966
  return false;
11967
11967
  }
@@ -18301,7 +18301,7 @@ var require_react_reconciler_development = __commonJS({
18301
18301
  var classComponentUpdater = {
18302
18302
  isMounted,
18303
18303
  enqueueSetState: function(inst, payload, callback) {
18304
- var fiber = get(inst);
18304
+ var fiber = get2(inst);
18305
18305
  var eventTime = requestEventTime();
18306
18306
  var lane = requestUpdateLane(fiber);
18307
18307
  var update = createUpdate(eventTime, lane);
@@ -18322,7 +18322,7 @@ var require_react_reconciler_development = __commonJS({
18322
18322
  }
18323
18323
  },
18324
18324
  enqueueReplaceState: function(inst, payload, callback) {
18325
- var fiber = get(inst);
18325
+ var fiber = get2(inst);
18326
18326
  var eventTime = requestEventTime();
18327
18327
  var lane = requestUpdateLane(fiber);
18328
18328
  var update = createUpdate(eventTime, lane);
@@ -18344,7 +18344,7 @@ var require_react_reconciler_development = __commonJS({
18344
18344
  }
18345
18345
  },
18346
18346
  enqueueForceUpdate: function(inst, callback) {
18347
- var fiber = get(inst);
18347
+ var fiber = get2(inst);
18348
18348
  var eventTime = requestEventTime();
18349
18349
  var lane = requestUpdateLane(fiber);
18350
18350
  var update = createUpdate(eventTime, lane);
@@ -26201,7 +26201,7 @@ var require_react_reconciler_development = __commonJS({
26201
26201
  if (!parentComponent) {
26202
26202
  return emptyContextObject;
26203
26203
  }
26204
- var fiber = get(parentComponent);
26204
+ var fiber = get2(parentComponent);
26205
26205
  var parentContext = findCurrentUnmaskedContext(fiber);
26206
26206
  if (fiber.tag === ClassComponent) {
26207
26207
  var Component = fiber.type;
@@ -26212,7 +26212,7 @@ var require_react_reconciler_development = __commonJS({
26212
26212
  return parentContext;
26213
26213
  }
26214
26214
  function findHostInstance(component) {
26215
- var fiber = get(component);
26215
+ var fiber = get2(component);
26216
26216
  if (fiber === void 0) {
26217
26217
  if (typeof component.render === "function") {
26218
26218
  throw new Error("Unable to find node on an unmounted component.");
@@ -26229,7 +26229,7 @@ var require_react_reconciler_development = __commonJS({
26229
26229
  }
26230
26230
  function findHostInstanceWithWarning(component, methodName) {
26231
26231
  {
26232
- var fiber = get(component);
26232
+ var fiber = get2(component);
26233
26233
  if (fiber === void 0) {
26234
26234
  if (typeof component.render === "function") {
26235
26235
  throw new Error("Unable to find node on an unmounted component.");
@@ -28274,16 +28274,19 @@ function profileDir(name) {
28274
28274
  }
28275
28275
  function defaultProfileConfig() {
28276
28276
  return {
28277
+ // `aiCli` still names the provider the OAuth-only paths use (usage snapshot / support answer).
28277
28278
  aiCli: "claude",
28278
- // Lists so failover across profiles is discoverable (ADR-0057) — add more objects.
28279
- // `args` are the full per-CLI defaults (claude stream-json for metering — ADR-0072).
28280
- // `model` = the CLI's `--model` flag; default "sonnet" — the balanced mid-price tier
28281
- // (cheaper than opus, more capable than haiku). "" ⇒ the CLI's own default model.
28282
- claudeHome: [
28283
- { profile: ".claude", args: ["-p", "--output-format", "stream-json", "--verbose"], model: "sonnet" }
28279
+ // Unified mixed credential list (ADR-0182): one ordered, cross-provider failover chain — the
28280
+ // list order IS the priority, and `aiFailoverMode` picks working-first vs top-first. `args`
28281
+ // here are EXTRA args only (empty by default) — the token-metering flags (claude stream-json /
28282
+ // codex `exec --json`) are hardcoded in source and always injected (ADR-0158, ADR-0072), so
28283
+ // they can't be dropped by editing this file. `model` = the CLI's `--model` flag; default
28284
+ // "sonnet" (balanced mid-price tier). "" ⇒ the CLI's own default model.
28285
+ aiProfiles: [
28286
+ { provider: "claude", profile: ".claude", args: [], model: "sonnet" },
28287
+ { provider: "codex", profile: ".codex", args: [], model: "" }
28284
28288
  ],
28285
- codexHome: [{ profile: ".codex", args: ["exec"], model: "" }],
28286
- antigravityHome: [],
28289
+ aiFailoverMode: "remember",
28287
28290
  autoUpdate: true,
28288
28291
  commandHistoryUploadMinutes: 10,
28289
28292
  autoClearIdleMinutes: 10
@@ -28431,6 +28434,13 @@ function confirmPairing(serverUrl, hashcode2, machine) {
28431
28434
  hostname: machine.hostname
28432
28435
  });
28433
28436
  }
28437
+ function pairWithToken(serverUrl, token, machine) {
28438
+ return post(serverUrl, "/machine-links/pair-token", {
28439
+ token,
28440
+ fingerprint: machine.fingerprint,
28441
+ hostname: machine.hostname
28442
+ });
28443
+ }
28434
28444
  function requestWsToken(serverUrl, hashcode3) {
28435
28445
  return post(serverUrl, "/machine-links/token", { hashcode3 });
28436
28446
  }
@@ -28449,22 +28459,25 @@ async function fetchCliVersion(serverUrl) {
28449
28459
  function sanitizeProfileName(name) {
28450
28460
  return name.replace(/[^a-zA-Z0-9._-]/g, "_") || "profile";
28451
28461
  }
28452
- async function runLink(serverUrl, explicitProfile) {
28453
- const hashcode1 = randomBytes(32).toString("hex");
28454
- console.log(`\u2500\u2500 4PM cli pairing (profile: ${explicitProfile ?? "default"}) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`);
28455
- console.log("1. Open the 4PM web \u2192 /machines, enter hashcode (1):");
28456
- console.log(`
28462
+ async function runLink(serverUrl, explicitProfile, token = null) {
28463
+ let result;
28464
+ if (token) {
28465
+ console.log(`\u2500\u2500 4PM cli headless pairing (profile: ${explicitProfile ?? "default"}) \u2500\u2500`);
28466
+ result = await pairWithToken(serverUrl, token, collectFingerprint());
28467
+ } else {
28468
+ const hashcode1 = randomBytes(32).toString("hex");
28469
+ console.log(`\u2500\u2500 4PM cli pairing (profile: ${explicitProfile ?? "default"}) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`);
28470
+ console.log("1. Open the 4PM web \u2192 /machines, enter hashcode (1):");
28471
+ console.log(`
28457
28472
  ${hashcode1}
28458
28473
  `);
28459
- console.log("2. The web will show hashcode (2) \u2014 paste it here.");
28460
- const rl = createInterface({ input: process.stdin, output: process.stdout });
28461
- const hashcode2 = (await rl.question("hashcode (2): ")).trim();
28462
- rl.close();
28463
- const { hashcode3, username, scope, projectName } = await confirmPairing(
28464
- serverUrl,
28465
- hashcode2,
28466
- collectFingerprint()
28467
- );
28474
+ console.log("2. The web will show hashcode (2) \u2014 paste it here.");
28475
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
28476
+ const hashcode2 = (await rl.question("hashcode (2): ")).trim();
28477
+ rl.close();
28478
+ result = await confirmPairing(serverUrl, hashcode2, collectFingerprint());
28479
+ }
28480
+ const { hashcode3, username, scope, projectName } = result;
28468
28481
  const profileName = explicitProfile ?? sanitizeProfileName(username);
28469
28482
  const existed = readCredential(profileDir(profileName), profileName) != null;
28470
28483
  const dir2 = profileDir(profileName);
@@ -28494,8 +28507,8 @@ async function runLink(serverUrl, explicitProfile) {
28494
28507
  }
28495
28508
 
28496
28509
  // src/commands/start.ts
28497
- import { spawn as spawn6 } from "child_process";
28498
- import { join as join23 } from "path";
28510
+ import { spawn as spawn8 } from "child_process";
28511
+ import { join as join30 } from "path";
28499
28512
 
28500
28513
  // src/core/update.ts
28501
28514
  import { execSync as execSync2 } from "child_process";
@@ -28585,6 +28598,7 @@ async function updateViaDownload(tarballUrl, checksum, signature) {
28585
28598
  });
28586
28599
  }
28587
28600
  async function updateToLatest(serverUrl) {
28601
+ if (CLI_VERSION.startsWith("0.0.0")) return { action: "dev-build", version: CLI_VERSION };
28588
28602
  const meta = await fetchCliVersion(serverUrl);
28589
28603
  if (compareSemver(CLI_VERSION, meta.latest) >= 0) {
28590
28604
  return { action: "already-latest", version: CLI_VERSION };
@@ -28667,7 +28681,7 @@ async function checkAndUpdate(serverUrl, autoUpdate) {
28667
28681
  // src/core/ws-client.ts
28668
28682
  init_wrapper();
28669
28683
  import { randomUUID } from "crypto";
28670
- import { basename as basename4, join as join19 } from "path";
28684
+ import { basename as basename4, join as join25 } from "path";
28671
28685
 
28672
28686
  // ../../22-packages/11-shared/51-utils/src/helpers.ts
28673
28687
  function backoffJitterMs(attempt, baseMs = 500, maxMs = 1e4) {
@@ -28780,6 +28794,9 @@ var PermissionCode = {
28780
28794
  PROJECT_AGENTS: "project.agents",
28781
28795
  // Security & placeholder management (ADR-0154): AI_SECURITY/AI_PLACEHOLDER docs + write-only secrets
28782
28796
  PROJECT_SECRETS: "project.secrets",
28797
+ // Agent tool-permission editor (ADR-0183): edit the Claude `permissions` block of
28798
+ // .claude/settings.json (shared) + per-cli .claude/settings.local.json
28799
+ PROJECT_AGENT_TOOLS: "project.agent_tools",
28783
28800
  // RAG capability install (ADR-0156): install/manage the worker's RAG engine
28784
28801
  PROJECT_RAG: "project.rag",
28785
28802
  // permission
@@ -28810,6 +28827,13 @@ var PermissionCode = {
28810
28827
  // org template files (ADR-0113) — ADMIN + PM manage the org's template library
28811
28828
  TEMPLATE_READ: "template.read",
28812
28829
  TEMPLATE_WRITE: "template.write",
28830
+ // skill/subagent marketplace (ADR-0185) — browse (all roles) + publish (ADMIN/PM/TL/SA_SE)
28831
+ MARKET_READ: "market.read",
28832
+ MARKET_PUBLISH: "market.publish",
28833
+ // moderate the org's global (public) package submissions (ADR-0186) — ADMIN/PM (TL inherits)
28834
+ MARKET_MODERATE: "market.moderate",
28835
+ // knowledge-social posting (ADR-0190) — create/edit/publish + review posts (ADMIN/PM/TL/SA_SE)
28836
+ KNOWLEDGE_PUBLISH: "knowledge.publish",
28813
28837
  // integration (community-server — ADR-0058; served by @4pm/community)
28814
28838
  INTEGRATION_READ: "integration.read",
28815
28839
  INTEGRATION_CONNECT: "integration.connect",
@@ -28819,7 +28843,10 @@ var PermissionCode = {
28819
28843
  INTEGRATION_PAGE_WRITE: "integration.page_write",
28820
28844
  // community "4rum" news/forum (MEMO) — every role reads/posts (org-scoped)
28821
28845
  COMMUNITY_READ: "community.read",
28822
- COMMUNITY_POST: "community.post"
28846
+ COMMUNITY_POST: "community.post",
28847
+ // support & AI help (ADR-0167/0168) — self-scoped baseline for every role
28848
+ SUPPORT_USE: "support.use",
28849
+ HELP_USE: "help.use"
28823
28850
  };
28824
28851
  var ALL_PERMISSION_CODES = Object.values(PermissionCode);
28825
28852
  var GrantSubjectType = {
@@ -28838,7 +28865,10 @@ var COMMON = [
28838
28865
  P.SEARCH_QUERY,
28839
28866
  // community 4rum — read + post (org-scoped — MEMO)
28840
28867
  P.COMMUNITY_READ,
28841
- P.COMMUNITY_POST
28868
+ P.COMMUNITY_POST,
28869
+ // file/read own support tickets + ask the AI Help agent (ADR-0167/0168)
28870
+ P.SUPPORT_USE,
28871
+ P.HELP_USE
28842
28872
  ];
28843
28873
  var ROLE_DEFAULT_PERMISSIONS = {
28844
28874
  // ADMIN: full permissions
@@ -28855,6 +28885,10 @@ var ROLE_DEFAULT_PERMISSIONS = {
28855
28885
  P.QUOTA_MANAGE,
28856
28886
  P.TEMPLATE_READ,
28857
28887
  P.TEMPLATE_WRITE,
28888
+ P.MARKET_READ,
28889
+ P.MARKET_PUBLISH,
28890
+ P.MARKET_MODERATE,
28891
+ P.KNOWLEDGE_PUBLISH,
28858
28892
  P.TEAM_LIST,
28859
28893
  P.TEAM_CREATE,
28860
28894
  P.TEAM_READ,
@@ -28886,6 +28920,7 @@ var ROLE_DEFAULT_PERMISSIONS = {
28886
28920
  P.PROJECT_AUTONOMOUS,
28887
28921
  P.PROJECT_AGENTS,
28888
28922
  P.PROJECT_SECRETS,
28923
+ P.PROJECT_AGENT_TOOLS,
28889
28924
  P.PROJECT_RAG,
28890
28925
  P.PERMISSION_LIST,
28891
28926
  P.PERMISSION_READ,
@@ -28916,8 +28951,15 @@ var ROLE_DEFAULT_PERMISSIONS = {
28916
28951
  P.PROJECT_AGENTS,
28917
28952
  // manage security/placeholder + set (write-only) secrets — ADR-0154
28918
28953
  P.PROJECT_SECRETS,
28954
+ // edit the Claude tool-permission policy (settings.json + settings.local.json) — ADR-0183
28955
+ P.PROJECT_AGENT_TOOLS,
28919
28956
  // install/manage the worker RAG engine — ADR-0156
28920
28957
  P.PROJECT_RAG,
28958
+ // browse + publish skill/subagent packages — ADR-0185
28959
+ P.MARKET_READ,
28960
+ P.MARKET_PUBLISH,
28961
+ // author knowledge posts — ADR-0190
28962
+ P.KNOWLEDGE_PUBLISH,
28921
28963
  P.INTEGRATION_READ
28922
28964
  ],
28923
28965
  [Role.QA]: [
@@ -28929,6 +28971,8 @@ var ROLE_DEFAULT_PERMISSIONS = {
28929
28971
  P.PROJECT_MEMO_READ,
28930
28972
  P.PROJECT_MEMO_WRITE,
28931
28973
  P.PROJECT_GIT,
28974
+ // browse the marketplace (read-only) — ADR-0185
28975
+ P.MARKET_READ,
28932
28976
  P.INTEGRATION_READ
28933
28977
  ],
28934
28978
  [Role.BA]: [
@@ -28940,6 +28984,8 @@ var ROLE_DEFAULT_PERMISSIONS = {
28940
28984
  P.PROJECT_MEMO_READ,
28941
28985
  P.PROJECT_MEMO_WRITE,
28942
28986
  P.PROJECT_GIT,
28987
+ // browse the marketplace (read-only) — ADR-0185
28988
+ P.MARKET_READ,
28943
28989
  P.INTEGRATION_READ
28944
28990
  ],
28945
28991
  [Role.MACHINE]: [
@@ -33196,7 +33242,10 @@ var registerRequestSchema = external_exports.object({
33196
33242
  phone: phoneSchema,
33197
33243
  ipAllowlist: ipAllowlistSchema.optional(),
33198
33244
  planCode: external_exports.string().min(1).max(50).optional(),
33199
- provider: paymentProviderSchema.optional()
33245
+ provider: paymentProviderSchema.optional(),
33246
+ // One-time token from an SSO "prefill" round-trip (ADR-0161): a verified provider profile
33247
+ // whose email must match `email`; on success the identity is linked to the new account.
33248
+ ssoToken: hexTokenSchema.optional()
33200
33249
  });
33201
33250
  var mailTokenRequestSchema = external_exports.object({
33202
33251
  token: hexTokenSchema
@@ -33405,6 +33454,7 @@ var updateMemoItemSchema = external_exports.object({
33405
33454
  }).refine((v) => v.content !== void 0 || v.done !== void 0, {
33406
33455
  message: "content or done required"
33407
33456
  });
33457
+ var HTTP_JSON_BODY_LIMIT_BYTES = 2 * 1048576;
33408
33458
  var putSpecDraftRequestSchema = external_exports.object({
33409
33459
  spec: external_exports.record(external_exports.unknown()),
33410
33460
  specVersion: external_exports.number().int().positive().optional()
@@ -33420,6 +33470,7 @@ var createArtifactRequestSchema = external_exports.object({
33420
33470
  kind: external_exports.enum(TEMPLATE_KINDS),
33421
33471
  content: external_exports.string().min(1).max(ARTIFACT_MAX_BYTES)
33422
33472
  });
33473
+ var GIT_URL_RE = /^(https?:\/\/|git@[^\s:]+:|ssh:\/\/).+/i;
33423
33474
  var repoSpecSchema = external_exports.object({
33424
33475
  /** Role label (e.g. "docs" / "web" / "server") — descriptive, not constrained. */
33425
33476
  role: external_exports.string().max(60).optional().default(""),
@@ -33427,23 +33478,15 @@ var repoSpecSchema = external_exports.object({
33427
33478
  desc: external_exports.string().max(500).optional().default(""),
33428
33479
  /** Exactly one repo of the project is the primary. */
33429
33480
  primary: external_exports.boolean().optional().default(false),
33430
- /** `create` a new repo (provider) or use an `existing` one (url). */
33431
- op: external_exports.enum(["create", "existing"]).default("create"),
33432
- provider: external_exports.enum(["gh", "glab"]).default("gh"),
33433
- /** New repo name (op=create; empty ⇒ project id for the primary). */
33434
- name: external_exports.string().max(100).optional().default(""),
33435
- visibility: external_exports.enum(["private", "public"]).default("private"),
33436
- /** Existing repo url to clone/link (op=existing). */
33437
- url: external_exports.string().max(500).optional().default(""),
33481
+ /** Clone url — `https` or `ssh` (required; ADR-0172). */
33482
+ url: external_exports.string().max(500).regex(GIT_URL_RE, "must be an https or ssh git url"),
33438
33483
  /** Sub-repo subfolder under the target root (empty ⇒ derived from role/name). */
33439
- subdir: external_exports.string().max(120).optional().default(""),
33440
- /** Default branch for this repo (empty ⇒ provider default, e.g. `main`). */
33441
- defaultBranch: external_exports.string().max(100).optional().default(""),
33442
- /** `.gitignore` template name for this repo (empty ⇒ none). */
33443
- gitignore: external_exports.string().max(40).optional().default(""),
33444
- /** Commit-message convention for this repo (empty ⇒ unspecified). */
33445
- commitConvention: external_exports.string().max(40).optional().default("")
33484
+ subdir: external_exports.string().max(120).optional().default("")
33446
33485
  });
33486
+ function repoName(url) {
33487
+ const last = (url || "").trim().replace(/\/+$/, "").split(/[/:]/).pop() ?? "";
33488
+ return last.replace(/\.git$/i, "");
33489
+ }
33447
33490
  var specFieldEnvelopeSchema = external_exports.object({
33448
33491
  id: external_exports.string().min(1),
33449
33492
  name: external_exports.string().optional().default(""),
@@ -33501,16 +33544,6 @@ var addRepoRequestSchema = external_exports.object({
33501
33544
  message: "Exactly one repo must be primary"
33502
33545
  });
33503
33546
  }
33504
- for (let i = 0; i < v.repos.length; i++) {
33505
- const r = v.repos[i];
33506
- if (r && r.op === "existing" && !r.url) {
33507
- ctx.addIssue({
33508
- code: external_exports.ZodIssueCode.custom,
33509
- path: ["repos", i, "url"],
33510
- message: "An existing repo requires a url"
33511
- });
33512
- }
33513
- }
33514
33547
  });
33515
33548
 
33516
33549
  // ../../22-packages/11-shared/31-dto/src/activity.ts
@@ -33552,6 +33585,16 @@ var confirmRequestSchema = external_exports.object({
33552
33585
  var wsTokenRequestSchema = external_exports.object({
33553
33586
  hashcode3: hexTokenSchema
33554
33587
  });
33588
+ var pairTokenRequestSchema = external_exports.object({
33589
+ token: external_exports.string().min(16).max(200),
33590
+ fingerprint: external_exports.string().max(200).optional(),
33591
+ hostname: external_exports.string().max(200).optional()
33592
+ });
33593
+ var issueProvisioningTokenRequestSchema = external_exports.object({
33594
+ machineLinkId: external_exports.string().uuid(),
33595
+ /** TTL seconds (0/absent ⇒ a default short TTL applied server-side). */
33596
+ ttlSec: external_exports.number().int().min(0).max(2592e3).optional()
33597
+ });
33555
33598
  var listWorkersQuerySchema = baseRequestSchema.extend({
33556
33599
  status: external_exports.enum(["online", "offline"]).optional()
33557
33600
  });
@@ -33627,13 +33670,36 @@ var agentWriteRequestSchema = external_exports.object({
33627
33670
  ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["content"], message: "content is required for write" });
33628
33671
  }
33629
33672
  });
33673
+ var toolRuleSchema = external_exports.string().min(1).max(500);
33674
+ var agentToolsPermissionsSchema = external_exports.object({
33675
+ defaultMode: external_exports.enum(["default", "acceptEdits", "plan", "bypassPermissions"]),
33676
+ allow: external_exports.array(toolRuleSchema).max(500),
33677
+ ask: external_exports.array(toolRuleSchema).max(500),
33678
+ deny: external_exports.array(toolRuleSchema).max(500)
33679
+ });
33680
+ var agentToolsReadQuerySchema = external_exports.object({
33681
+ scope: external_exports.enum(["shared", "local"])
33682
+ });
33683
+ var agentToolsWriteRequestSchema = external_exports.object({
33684
+ scope: external_exports.enum(["shared", "local"]),
33685
+ permissions: agentToolsPermissionsSchema
33686
+ });
33630
33687
 
33631
33688
  // ../../22-packages/11-shared/31-dto/src/command.ts
33632
33689
  var COMMAND_MAX_LEN = 8e3;
33633
33690
  var AI_PROMPT_MAX_LEN = 2e5;
33634
33691
  var dispatchCommandRequestSchema = external_exports.object({
33635
33692
  projectId: external_exports.string().uuid(),
33636
- machineLinkId: external_exports.string().uuid(),
33693
+ // Target cli. Required unless `pick` is set — with `pick:"idle"` the server chooses the
33694
+ // target from the project's pool, so the client omits it (ADR-0171).
33695
+ machineLinkId: external_exports.string().uuid().optional(),
33696
+ /**
33697
+ * Server-side idle-cli pick (ADR-0171): `"idle"` ⇒ omit `machineLinkId` and let the server
33698
+ * select + atomically claim an idle cli from the project's pool (machine-users + org
33699
+ * orchestrator). Used by the wizard's AI ops so parallel suggests spread across the pool
33700
+ * without double-booking a busy cli; no idle cli ⇒ `ALL_CLIS_BUSY`.
33701
+ */
33702
+ pick: external_exports.enum(["idle"]).optional(),
33637
33703
  // In AI mode `command` carries the raw prompt (can be long — the spec-assist prompts
33638
33704
  // embed the whole self-describing spec envelope, ADR-0106), else an executable name.
33639
33705
  command: external_exports.string().min(1).max(AI_PROMPT_MAX_LEN),
@@ -33656,6 +33722,13 @@ var dispatchCommandRequestSchema = external_exports.object({
33656
33722
  */
33657
33723
  gitOp: external_exports.enum(["read", "write"]).optional()
33658
33724
  }).superRefine((v, ctx) => {
33725
+ if (v.machineLinkId == null === (v.pick == null)) {
33726
+ ctx.addIssue({
33727
+ code: external_exports.ZodIssueCode.custom,
33728
+ path: ["machineLinkId"],
33729
+ message: "Provide exactly one of machineLinkId or pick."
33730
+ });
33731
+ }
33659
33732
  if (!v.ai && v.command.length > COMMAND_MAX_LEN) {
33660
33733
  ctx.addIssue({
33661
33734
  code: external_exports.ZodIssueCode.too_big,
@@ -33817,6 +33890,72 @@ var addonChangeRequestSchema = external_exports.object({
33817
33890
  provider: external_exports.enum([PaymentProvider.STRIPE, PaymentProvider.PAYPAL]).optional()
33818
33891
  });
33819
33892
 
33893
+ // ../../22-packages/11-shared/31-dto/src/market.ts
33894
+ var listMarketQuerySchema = baseRequestSchema.extend({
33895
+ scope: external_exports.enum(["personal", "org", "global"]).optional(),
33896
+ kind: external_exports.enum(["subagent", "skill"]).optional(),
33897
+ category: external_exports.string().max(50).optional(),
33898
+ tag: external_exports.string().max(50).optional(),
33899
+ minRating: external_exports.coerce.number().min(1).max(5).optional()
33900
+ });
33901
+ var marketReviewUpsertSchema = external_exports.object({
33902
+ rating: external_exports.coerce.number().int().min(1).max(5),
33903
+ title: external_exports.string().max(120).optional(),
33904
+ body: external_exports.string().max(2e3).optional()
33905
+ });
33906
+ var marketPurchaseSchema = external_exports.object({
33907
+ method: external_exports.enum(["card", "credit"]).optional(),
33908
+ provider: external_exports.string().max(30).optional(),
33909
+ email: external_exports.string().email().max(200).optional(),
33910
+ successUrl: external_exports.string().url().max(500).optional(),
33911
+ cancelUrl: external_exports.string().url().max(500).optional()
33912
+ });
33913
+ var marketPublishScopeSchema = external_exports.enum(["personal", "org", "global"]);
33914
+ var packageInstallRequestSchema = external_exports.object({
33915
+ packageId: external_exports.string().uuid(),
33916
+ version: external_exports.string().min(1).max(30)
33917
+ });
33918
+ var packagePublishFromProjectRequestSchema = external_exports.object({
33919
+ kind: external_exports.enum(["subagent", "skill"]),
33920
+ name: external_exports.string().min(1).max(100),
33921
+ scope: external_exports.enum(["personal", "org", "global"]),
33922
+ version: external_exports.string().min(1).max(30),
33923
+ changelog: external_exports.string().max(2e3).optional()
33924
+ });
33925
+ var marketRejectRequestSchema = external_exports.object({
33926
+ reason: external_exports.string().max(500).optional()
33927
+ });
33928
+
33929
+ // ../../22-packages/11-shared/31-dto/src/knowledge.ts
33930
+ var listKnowledgeQuerySchema = baseRequestSchema.extend({
33931
+ scope: external_exports.enum(["personal", "org", "global"]).optional(),
33932
+ category: external_exports.string().max(50).optional(),
33933
+ tag: external_exports.string().max(50).optional(),
33934
+ minRating: external_exports.coerce.number().min(1).max(5).optional()
33935
+ });
33936
+ var knowledgePostUpsertSchema = external_exports.object({
33937
+ title: external_exports.string().min(1).max(200),
33938
+ summary: external_exports.string().max(500).optional(),
33939
+ bodyMarkdown: external_exports.string().min(1).max(1e5),
33940
+ scope: external_exports.enum(["personal", "org", "global"]),
33941
+ category: external_exports.string().max(50).optional(),
33942
+ tags: external_exports.array(external_exports.string().max(50)).max(10).optional(),
33943
+ status: external_exports.enum(["draft", "published"]).default("published"),
33944
+ sourceProjectId: external_exports.string().uuid().optional(),
33945
+ /** Denormalized author display name (the web supplies it from the session; authorId is the
33946
+ * trustworthy key). */
33947
+ authorName: external_exports.string().max(120).optional()
33948
+ });
33949
+ var knowledgeReviewUpsertSchema = external_exports.object({
33950
+ rating: external_exports.coerce.number().int().min(1).max(5),
33951
+ title: external_exports.string().max(120).optional(),
33952
+ body: external_exports.string().max(2e3).optional()
33953
+ });
33954
+ var knowledgeDistillSchema = external_exports.object({
33955
+ projectId: external_exports.string().uuid(),
33956
+ prompt: external_exports.string().max(2e3).optional()
33957
+ });
33958
+
33820
33959
  // ../../22-packages/11-shared/31-dto/src/search.ts
33821
33960
  var SearchHitType = {
33822
33961
  PROJECT: "project",
@@ -33866,6 +34005,62 @@ var createCommunityPostSchema = external_exports.object({
33866
34005
  authorName: external_exports.string().max(100).optional()
33867
34006
  });
33868
34007
 
34008
+ // ../../22-packages/11-shared/31-dto/src/support.ts
34009
+ var SUPPORT_CATEGORIES = [
34010
+ "bug",
34011
+ "billing",
34012
+ "how_to",
34013
+ "account",
34014
+ "other"
34015
+ ];
34016
+ var SUPPORT_STATUSES = [
34017
+ "open",
34018
+ "in_progress",
34019
+ "resolved",
34020
+ "closed"
34021
+ ];
34022
+ var supportContextSchema = external_exports.object({
34023
+ route: external_exports.string().max(512).optional(),
34024
+ orgId: external_exports.string().max(64).optional(),
34025
+ projectId: external_exports.string().max(64).optional(),
34026
+ appVersion: external_exports.string().max(64).optional(),
34027
+ locale: external_exports.string().max(16).optional()
34028
+ }).optional();
34029
+ var createSupportTicketRequestSchema = external_exports.object({
34030
+ category: external_exports.enum(SUPPORT_CATEGORIES),
34031
+ subject: external_exports.string().min(1).max(160),
34032
+ body: external_exports.string().min(1).max(1e4),
34033
+ attachments: external_exports.array(external_exports.string().max(512)).max(10).optional(),
34034
+ context: supportContextSchema
34035
+ });
34036
+ var listSupportTicketsQuerySchema = baseRequestSchema.extend({
34037
+ status: external_exports.enum(SUPPORT_STATUSES).optional(),
34038
+ category: external_exports.enum(SUPPORT_CATEGORIES).optional()
34039
+ });
34040
+ var replySupportTicketRequestSchema = external_exports.object({
34041
+ body: external_exports.string().min(1).max(1e4),
34042
+ attachments: external_exports.array(external_exports.string().max(512)).max(10).optional()
34043
+ });
34044
+ var adminListSupportTicketsQuerySchema = baseRequestSchema.extend({
34045
+ status: external_exports.enum(SUPPORT_STATUSES).optional(),
34046
+ category: external_exports.enum(SUPPORT_CATEGORIES).optional(),
34047
+ orgId: external_exports.string().uuid().optional()
34048
+ });
34049
+ var adminReplySupportTicketRequestSchema = external_exports.object({
34050
+ body: external_exports.string().min(1).max(1e4),
34051
+ status: external_exports.enum(SUPPORT_STATUSES).optional(),
34052
+ assign: external_exports.boolean().optional(),
34053
+ // the acting platform admin's GitHub id (author trace), filled by admin-server.
34054
+ by: external_exports.string().max(64).optional()
34055
+ });
34056
+
34057
+ // ../../22-packages/11-shared/31-dto/src/help.ts
34058
+ var askHelpRequestSchema = external_exports.object({
34059
+ question: external_exports.string().min(1).max(2e3),
34060
+ // continue an existing conversation; omit ⇒ a new thread.
34061
+ threadId: external_exports.string().uuid().optional()
34062
+ });
34063
+
33869
34064
  // ../../22-packages/21-protocol/21-websocket/src/channels/index.ts
33870
34065
  var WsChannels = {
33871
34066
  ...WsChannel,
@@ -33890,9 +34085,20 @@ var WsChannels = {
33890
34085
  AGENTS_LIST: "agents.list",
33891
34086
  AGENTS_READ: "agents.read",
33892
34087
  AGENTS_WRITE: "agents.write",
34088
+ // skill/subagent marketplace (ADR-0185): pack a project artifact into a zip (publish),
34089
+ // install a package version into .claude/agents|skills + .4pm-packages.json, list installed
34090
+ // (with on-disk sha256 for drift), remove an installed package.
34091
+ PACKAGES_PACK: "packages.pack",
34092
+ PACKAGES_INSTALL: "packages.install",
34093
+ PACKAGES_LIST: "packages.list",
34094
+ PACKAGES_REMOVE: "packages.remove",
33893
34095
  // security & placeholder management (ADR-0154): AI_SECURITY/AI_PLACEHOLDER + write-only secrets
33894
34096
  SECRETS_READ: "secrets.read",
33895
34097
  SECRETS_WRITE: "secrets.write",
34098
+ // agent tool-permission editor (ADR-0183): read/write the `permissions` block of
34099
+ // .claude/settings.json (shared) or .claude/settings.local.json (per-cli local)
34100
+ AGENT_TOOLS_READ: "agentTools.read",
34101
+ AGENT_TOOLS_WRITE: "agentTools.write",
33896
34102
  // docs & code dependency graph (ADR-0155): build a {nodes,edges,orphans} graph per mode
33897
34103
  GRAPH_BUILD: "graph.build",
33898
34104
  // RAG capability + install (ADR-0156): probe the worker + install a worker-tuned model
@@ -33907,6 +34113,9 @@ var WsChannels = {
33907
34113
  GIT_LOG: "git.log",
33908
34114
  GIT_COMMIT: "git.commit",
33909
34115
  GIT_COMMIT_DIFF: "git.commit-diff",
34116
+ // server → cli (reply): manage the rented worker's ssh deploy key (ADR-0173) — generate
34117
+ // (ed25519, on the worker) / get / delete. The private key never leaves the worker.
34118
+ GIT_SSH_KEY: "git.ssh-key",
33910
34119
  PROJECT_CREATE: "project.create",
33911
34120
  PROJECT_ADD: "project.add",
33912
34121
  PROJECT_PROGRESS: "project.progress",
@@ -33943,7 +34152,14 @@ var WsChannels = {
33943
34152
  // outbound review (ADR-0082): machine→server request, server→outbound evaluate, result back
33944
34153
  REVIEW_REQUEST: "review.request",
33945
34154
  REVIEW_EVALUATE: "review.evaluate",
33946
- REVIEW_RESULT: "review.result"
34155
+ REVIEW_RESULT: "review.result",
34156
+ // AI support answer (ADR-0170): server → support-agent cli request–reply. The dispatcher
34157
+ // sends the question + asker role + the shared docs/FAQ repo config; the worker clones/pulls
34158
+ // the repo, runs `claude` grounded in its docs+FAQ, and replies with the composed answer.
34159
+ SUPPORT_ANSWER: "support.answer",
34160
+ // AI knowledge distill (ADR-0190): server → project cli request–reply. The worker runs the AI CLI
34161
+ // in the project's working dir with a distillation prompt and replies with the composed markdown.
34162
+ KNOWLEDGE_COMPOSE: "knowledge.compose"
33947
34163
  };
33948
34164
 
33949
34165
  // ../../22-packages/21-protocol/21-websocket/src/crypto.ts
@@ -34060,7 +34276,7 @@ var OutputBatcher = class {
34060
34276
  };
34061
34277
 
34062
34278
  // src/core/ws-client.ts
34063
- import { existsSync as existsSync9, mkdirSync as mkdirSync4, rmSync as rmSync4 } from "fs";
34279
+ import { existsSync as existsSync10, mkdirSync as mkdirSync5, rmSync as rmSync5 } from "fs";
34064
34280
  import { randomUUID as randomCommandId } from "crypto";
34065
34281
 
34066
34282
  // src/core/update-scheduler.ts
@@ -34219,9 +34435,9 @@ var UpdateScheduler = class {
34219
34435
  day: "2-digit",
34220
34436
  hour: "2-digit"
34221
34437
  }).formatToParts(/* @__PURE__ */ new Date());
34222
- const get = (type) => parts.find((p) => p.type === type)?.value ?? "";
34223
- const hour = Number.parseInt(get("hour"), 10);
34224
- const dateKey = `${get("year")}-${get("month")}-${get("day")}`;
34438
+ const get2 = (type) => parts.find((p) => p.type === type)?.value ?? "";
34439
+ const hour = Number.parseInt(get2("hour"), 10);
34440
+ const dateKey = `${get2("year")}-${get2("month")}-${get2("day")}`;
34225
34441
  if (Number.isNaN(hour)) return null;
34226
34442
  return { hour: hour % 24, dateKey };
34227
34443
  } catch {
@@ -34336,18 +34552,11 @@ function assistantText(ev) {
34336
34552
  }
34337
34553
  function createAiStreamParser(cli) {
34338
34554
  const isClaude = cli.includes("claude");
34555
+ const isCodex = cli.includes("codex");
34556
+ const isJson = isClaude || isCodex;
34339
34557
  let buffer = "";
34340
34558
  const acc = { tokens: 0, input: 0, output: 0, cacheRead: 0, cacheCreation: 0 };
34341
- function handleLine(line) {
34342
- const trimmed = line.trim();
34343
- if (!isClaude || !trimmed.startsWith("{")) return null;
34344
- let ev;
34345
- try {
34346
- ev = JSON.parse(trimmed);
34347
- } catch {
34348
- return null;
34349
- }
34350
- if (typeof ev.type !== "string") return null;
34559
+ function handleClaude(ev) {
34351
34560
  if (ev.type === "result" && ev.usage) {
34352
34561
  acc.input = ev.usage.input_tokens ?? 0;
34353
34562
  acc.output = ev.usage.output_tokens ?? 0;
@@ -34364,6 +34573,34 @@ function createAiStreamParser(cli) {
34364
34573
  }
34365
34574
  return "";
34366
34575
  }
34576
+ function handleCodex(ev) {
34577
+ if (ev.type === "turn.completed" && ev.usage) {
34578
+ acc.input = ev.usage.input_tokens ?? 0;
34579
+ acc.output = (ev.usage.output_tokens ?? 0) + (ev.usage.reasoning_output_tokens ?? 0);
34580
+ acc.cacheRead = ev.usage.cached_input_tokens ?? 0;
34581
+ acc.cacheCreation = 0;
34582
+ acc.tokens = acc.input + acc.output;
34583
+ return "";
34584
+ }
34585
+ if (ev.type === "item.completed" && ev.item?.type === "agent_message") {
34586
+ const text = ev.item.text;
34587
+ return text ? `${text}
34588
+ ` : "";
34589
+ }
34590
+ return "";
34591
+ }
34592
+ function handleLine(line) {
34593
+ const trimmed = line.trim();
34594
+ if (!isJson || !trimmed.startsWith("{")) return null;
34595
+ let ev;
34596
+ try {
34597
+ ev = JSON.parse(trimmed);
34598
+ } catch {
34599
+ return null;
34600
+ }
34601
+ if (typeof ev.type !== "string") return null;
34602
+ return isCodex ? handleCodex(ev) : handleClaude(ev);
34603
+ }
34367
34604
  return {
34368
34605
  push(raw) {
34369
34606
  buffer += raw;
@@ -34417,7 +34654,7 @@ async function runAiFailover(plan, commandId, cwd2, handlers) {
34417
34654
  for (let i = 0; i < total; i++) {
34418
34655
  const attempt = plan.attempts[i];
34419
34656
  if (total > 1) handlers.onAttemptStart(attempt.label, i, total);
34420
- const parser = createAiStreamParser(plan.cmd);
34657
+ const parser = createAiStreamParser(attempt.cmd);
34421
34658
  let captured = "";
34422
34659
  const emit = (raw) => {
34423
34660
  const text = parser.push(raw);
@@ -34426,7 +34663,7 @@ async function runAiFailover(plan, commandId, cwd2, handlers) {
34426
34663
  handlers.onChunk(text);
34427
34664
  }
34428
34665
  };
34429
- finalExit = await runAttempt(commandId, plan.cmd, attempt.args, cwd2, attempt.env, emit);
34666
+ finalExit = await runAttempt(commandId, attempt.cmd, attempt.args, cwd2, attempt.env, emit);
34430
34667
  const tail = parser.flush();
34431
34668
  if (tail) {
34432
34669
  captured += tail;
@@ -34435,13 +34672,13 @@ async function runAiFailover(plan, commandId, cwd2, handlers) {
34435
34672
  if (finalExit === 0) {
34436
34673
  const usage = parser.usage();
34437
34674
  if (usage.tokens === 0) usage.tokens = estimateTokens(captured);
34438
- return { exitCode: 0, workedDir: attempt.dir, usage };
34675
+ return { exitCode: 0, workedDir: attempt.dir, workedKey: attempt.key, workedCmd: attempt.cmd, usage };
34439
34676
  }
34440
34677
  const reason = isAuthFailure(captured) ? "auth" : isSessionLimit(captured) ? "limit" : null;
34441
34678
  if (!reason || i === total - 1) break;
34442
34679
  handlers.onAttemptFail(attempt.label, reason);
34443
34680
  }
34444
- return { exitCode: finalExit, workedDir: null, usage: { tokens: 0, input: 0, output: 0, cacheRead: 0, cacheCreation: 0 } };
34681
+ return { exitCode: finalExit, workedDir: null, workedKey: null, workedCmd: null, usage: { tokens: 0, input: 0, output: 0, cacheRead: 0, cacheCreation: 0 } };
34445
34682
  }
34446
34683
 
34447
34684
  // src/core/ai-profile-state.ts
@@ -34471,16 +34708,66 @@ function setWorkingProfile(profileDir2, cmd, dir2) {
34471
34708
  } catch {
34472
34709
  }
34473
34710
  }
34711
+ var WORKING_CREDENTIAL_KEY = "__credential__";
34712
+ function getWorkingCredential(profileDir2) {
34713
+ return readState(profileDir2)[WORKING_CREDENTIAL_KEY] ?? null;
34714
+ }
34715
+ function setWorkingCredential(profileDir2, key) {
34716
+ const state = readState(profileDir2);
34717
+ if (state[WORKING_CREDENTIAL_KEY] === key) return;
34718
+ state[WORKING_CREDENTIAL_KEY] = key;
34719
+ try {
34720
+ writeFileSync4(statePath(profileDir2), JSON.stringify(state, null, 2), "utf8");
34721
+ } catch {
34722
+ }
34723
+ }
34474
34724
 
34475
34725
  // src/utils/ai-cli.ts
34476
34726
  import { readFileSync as readFileSync8 } from "fs";
34477
34727
  import { homedir as homedir3 } from "os";
34478
34728
  import { basename, isAbsolute, join as join8 } from "path";
34479
- var DEFAULT_AI_ARGS = {
34729
+ var REQUIRED_AI_ARGS = {
34480
34730
  claude: ["-p", "--output-format", "stream-json", "--verbose"],
34481
- codex: ["exec"]
34731
+ codex: ["exec", "--json"],
34732
+ antigravity: []
34482
34733
  };
34734
+ function requiredArgs(cmd) {
34735
+ if (cmd.includes("claude")) return REQUIRED_AI_ARGS.claude;
34736
+ if (cmd.includes("codex")) return REQUIRED_AI_ARGS.codex;
34737
+ return REQUIRED_AI_ARGS.antigravity;
34738
+ }
34483
34739
  var DEFAULT_AI_CLI = "claude";
34740
+ var AI_PROVIDERS = ["claude", "codex", "antigravity"];
34741
+ function isUsableProfile(p) {
34742
+ return p.enabled !== false && Boolean(p.profile && p.profile.trim());
34743
+ }
34744
+ function isUsableCredential(c) {
34745
+ return isUsableProfile(c) && AI_PROVIDERS.includes(c.provider);
34746
+ }
34747
+ function credentialKey(provider, resolvedDir) {
34748
+ return `${provider}::${resolvedDir}`;
34749
+ }
34750
+ function dirFromCredentialKey(key) {
34751
+ const at = key.indexOf("::");
34752
+ return at >= 0 ? key.slice(at + 2) : key;
34753
+ }
34754
+ function labelFromCredentialKey(key) {
34755
+ return profileDisplayLabel(dirFromCredentialKey(key));
34756
+ }
34757
+ function resolveClaudeAuthMode(config) {
34758
+ const key = config.aiEnv?.["ANTHROPIC_API_KEY"] ?? process.env.ANTHROPIC_API_KEY;
34759
+ return key && key.trim() ? "api-key" : "subscription";
34760
+ }
34761
+ function isUnifiedConfig(config) {
34762
+ return Array.isArray(config.aiProfiles) && config.aiProfiles.some(isUsableCredential);
34763
+ }
34764
+ function claudeProfilesOf(config) {
34765
+ if (isUnifiedConfig(config)) return config.aiProfiles.filter((c) => c.provider === "claude");
34766
+ return config.claudeHome ?? [];
34767
+ }
34768
+ function claudeCredentialKeys(config) {
34769
+ return claudeProfilesOf(config).filter(isUsableProfile).map((p) => credentialKey("claude", resolveHomePath(p.profile)));
34770
+ }
34484
34771
  function resolveHomePath(value) {
34485
34772
  if (value.startsWith("~")) return join8(homedir3(), value.slice(1).replace(/^[/\\]/, ""));
34486
34773
  if (isAbsolute(value)) return value;
@@ -34488,16 +34775,25 @@ function resolveHomePath(value) {
34488
34775
  }
34489
34776
  function profileDirs(profiles) {
34490
34777
  if (!profiles) return [];
34491
- return profiles.map((p) => p.profile).filter((v) => v && v.trim()).map(resolveHomePath);
34778
+ return profiles.filter(isUsableProfile).map((p) => resolveHomePath(p.profile));
34779
+ }
34780
+ function folderScopeGuard(folder, prompt) {
34781
+ return [
34782
+ `IMPORTANT: You are working inside the project folder \`${folder}\`. Only read, use, and`,
34783
+ "modify content inside this folder to accomplish the task; do not access, reference, or",
34784
+ "change anything outside it.",
34785
+ "",
34786
+ prompt
34787
+ ].join("\n");
34492
34788
  }
34493
34789
  function buildRunArgs(profile, cmd, prompt) {
34494
- const pre = profile.args ?? DEFAULT_AI_ARGS[cmd] ?? [];
34790
+ const extras = profile.args ?? [];
34495
34791
  const model = profile.model?.trim();
34496
34792
  const modelArgs = model ? ["--model", model] : [];
34497
- return [...pre, ...modelArgs, prompt];
34793
+ return [...requiredArgs(cmd), ...extras, ...modelArgs, prompt];
34498
34794
  }
34499
34795
  function claudeHomeDirs(config) {
34500
- const dirs = profileDirs(config.claudeHome);
34796
+ const dirs = profileDirs(claudeProfilesOf(config));
34501
34797
  return dirs.length > 0 ? dirs : [join8(homedir3(), ".claude")];
34502
34798
  }
34503
34799
  function profileDisplayLabel(dir2) {
@@ -34521,29 +34817,64 @@ function profilesFor(config, cmd) {
34521
34817
  return [];
34522
34818
  }
34523
34819
  function profileLabels(config) {
34820
+ if (isUnifiedConfig(config)) {
34821
+ return config.aiProfiles.filter(isUsableCredential).map((c) => c.label?.trim() || profileDisplayLabel(resolveHomePath(c.profile)));
34822
+ }
34524
34823
  const cmd = config.aiCli ?? DEFAULT_AI_CLI;
34525
34824
  return profileDirs(profilesFor(config, cmd)).map((dir2) => profileDisplayLabel(dir2));
34526
34825
  }
34527
- function planAiRun(prompt, config, workingDir) {
34528
- const cmd = config.aiCli ?? DEFAULT_AI_CLI;
34826
+ function resolveClaudeProfiles(config, workingDir) {
34827
+ const profiles = claudeProfilesOf(config).filter(isUsableProfile).map((p) => ({ dir: resolveHomePath(p.profile), model: p.model?.trim() || void 0 }));
34828
+ const idx = workingDir ? profiles.findIndex((p) => p.dir === workingDir) : -1;
34829
+ if (idx > 0) profiles.unshift(profiles.splice(idx, 1)[0]);
34830
+ return profiles;
34831
+ }
34832
+ function planAiRun(prompt, config, hint = {}) {
34833
+ return isUnifiedConfig(config) ? planUnifiedRun(prompt, config, hint.credential ?? null) : planLegacyRun(prompt, config, hint.dir ?? null);
34834
+ }
34835
+ function planUnifiedRun(prompt, config, workingCredential) {
34836
+ const baseEnv = config.aiEnv ?? {};
34837
+ const resolved = config.aiProfiles.filter(isUsableCredential).map((cred) => {
34838
+ const cmd = cred.provider;
34839
+ const dir2 = resolveHomePath(cred.profile);
34840
+ return { cred, cmd, dir: dir2, key: credentialKey(cmd, dir2), envVar: profileEnvVar(cmd) };
34841
+ });
34842
+ const ordered = workingCredential && resolved.some((r) => r.key === workingCredential) ? [
34843
+ resolved.find((r) => r.key === workingCredential),
34844
+ ...resolved.filter((r) => r.key !== workingCredential)
34845
+ ] : resolved;
34846
+ const attempts = ordered.map(({ cred, cmd, dir: dir2, key, envVar }) => ({
34847
+ cmd,
34848
+ label: cred.label?.trim() || profileDisplayLabel(dir2),
34849
+ dir: dir2,
34850
+ key,
34851
+ args: buildRunArgs(cred, cmd, prompt),
34852
+ env: envVar ? { ...baseEnv, [envVar]: dir2 } : { ...baseEnv }
34853
+ }));
34854
+ return { cmd: attempts[0]?.cmd ?? DEFAULT_AI_CLI, attempts };
34855
+ }
34856
+ function planLegacyRun(prompt, config, workingDir) {
34857
+ const cmd = config.aiCli || DEFAULT_AI_CLI;
34529
34858
  const baseEnv = config.aiEnv ?? {};
34530
34859
  const envVar = profileEnvVar(cmd);
34531
34860
  const defaultPlan = {
34532
34861
  cmd,
34533
34862
  attempts: [
34534
- { label: "default", dir: null, args: buildRunArgs({ profile: "" }, cmd, prompt), env: { ...baseEnv } }
34863
+ { cmd, label: "default", dir: null, key: null, args: buildRunArgs({ profile: "" }, cmd, prompt), env: { ...baseEnv } }
34535
34864
  ]
34536
34865
  };
34537
34866
  if (!envVar) return defaultPlan;
34538
- const resolved = profilesFor(config, cmd).filter((p) => p.profile && p.profile.trim()).map((p) => ({ profile: p, dir: resolveHomePath(p.profile) }));
34867
+ const resolved = profilesFor(config, cmd).filter(isUsableProfile).map((p) => ({ profile: p, dir: resolveHomePath(p.profile) }));
34539
34868
  if (resolved.length === 0) return defaultPlan;
34540
34869
  const ordered = workingDir && resolved.some((r) => r.dir === workingDir) ? [
34541
34870
  resolved.find((r) => r.dir === workingDir),
34542
34871
  ...resolved.filter((r) => r.dir !== workingDir)
34543
34872
  ] : resolved;
34544
34873
  const attempts = ordered.map(({ profile, dir: dir2 }) => ({
34874
+ cmd,
34545
34875
  label: profileDisplayLabel(dir2),
34546
34876
  dir: dir2,
34877
+ key: credentialKey(cmd, dir2),
34547
34878
  args: buildRunArgs(profile, cmd, prompt),
34548
34879
  env: { ...baseEnv, [envVar]: dir2 }
34549
34880
  }));
@@ -35053,13 +35384,228 @@ async function writeWorkerFile(root, relPath, content) {
35053
35384
  }
35054
35385
  }
35055
35386
 
35056
- // src/core/autonomous.ts
35057
- import { readFile as readFile2, writeFile as writeFile2, chmod } from "fs/promises";
35058
- import { readFileSync as readFileSync12 } from "fs";
35059
- import { execFile as execFile2, spawn as spawn4 } from "child_process";
35387
+ // src/core/support-answer.ts
35388
+ import { spawn as spawn4 } from "child_process";
35389
+ import { execFile as execFile2 } from "child_process";
35390
+ import { createHash as createHash3 } from "crypto";
35391
+ import { existsSync as existsSync7, mkdirSync as mkdirSync4, readdirSync as readdirSync4, readFileSync as readFileSync12, statSync as statSync3 } from "fs";
35060
35392
  import { join as join12 } from "path";
35061
35393
  import { promisify as promisify2 } from "util";
35062
- var run2 = promisify2(execFile2);
35394
+ var execFileAsync = promisify2(execFile2);
35395
+ var MAX_CONTEXT_CHARS = 12e4;
35396
+ var ANSWER_TIMEOUT_MS = 12e4;
35397
+ var DOC_EXTENSIONS = [".md", ".mdx", ".txt"];
35398
+ function kbRoot(profileDir2) {
35399
+ return join12(profileDir2, "support-kb");
35400
+ }
35401
+ function repoDir(profileDir2, url, branch) {
35402
+ const key = createHash3("sha1").update(`${url}#${branch}`).digest("hex").slice(0, 16);
35403
+ return join12(kbRoot(profileDir2), key);
35404
+ }
35405
+ function authedUrl(url, token) {
35406
+ if (!token) return url;
35407
+ if (url.startsWith("https://")) return url.replace("https://", `https://${token}@`);
35408
+ return url;
35409
+ }
35410
+ async function ensureRepo(profileDir2, repo) {
35411
+ const dir2 = repoDir(profileDir2, repo.url, repo.branch);
35412
+ if (existsSync7(join12(dir2, ".git"))) return dir2;
35413
+ const url = authedUrl(repo.url, repo.token);
35414
+ const gitEnv = { ...process.env, GIT_TERMINAL_PROMPT: "0" };
35415
+ mkdirSync4(kbRoot(profileDir2), { recursive: true });
35416
+ await execFileAsync("git", ["clone", "--depth", "1", "--branch", repo.branch, url, dir2], {
35417
+ env: gitEnv
35418
+ });
35419
+ return dir2;
35420
+ }
35421
+ async function refreshSupportKb(profileDir2) {
35422
+ const root = kbRoot(profileDir2);
35423
+ if (!existsSync7(root)) return;
35424
+ const gitEnv = { ...process.env, GIT_TERMINAL_PROMPT: "0" };
35425
+ for (const entry of readdirSync4(root)) {
35426
+ const dir2 = join12(root, entry);
35427
+ if (!existsSync7(join12(dir2, ".git"))) continue;
35428
+ try {
35429
+ await execFileAsync("git", ["-C", dir2, "pull", "--ff-only"], { env: gitEnv });
35430
+ } catch (err) {
35431
+ logger.warn("support.kb.refresh.failed", { dir: dir2, error: String(err) });
35432
+ }
35433
+ }
35434
+ }
35435
+ function collectDocs(dir2) {
35436
+ const parts = [];
35437
+ let total = 0;
35438
+ const walk2 = (current) => {
35439
+ if (total >= MAX_CONTEXT_CHARS) return;
35440
+ for (const entry of readdirSync4(current)) {
35441
+ if (entry === ".git" || entry === "node_modules") continue;
35442
+ const full = join12(current, entry);
35443
+ const st = statSync3(full);
35444
+ if (st.isDirectory()) {
35445
+ walk2(full);
35446
+ } else if (DOC_EXTENSIONS.some((ext) => entry.toLowerCase().endsWith(ext))) {
35447
+ const rel = full.slice(dir2.length + 1);
35448
+ const body = readFileSync12(full, "utf8");
35449
+ const block = `
35450
+
35451
+ ===== ${rel} =====
35452
+ ${body}`;
35453
+ parts.push(block.slice(0, MAX_CONTEXT_CHARS - total));
35454
+ total += block.length;
35455
+ if (total >= MAX_CONTEXT_CHARS) return;
35456
+ }
35457
+ }
35458
+ };
35459
+ walk2(dir2);
35460
+ return parts.join("");
35461
+ }
35462
+ function buildPrompt(docs, question, askerRole) {
35463
+ const roleNote = askerRole === "admin" ? "The asker is a platform admin." : "The asker is a regular user \u2014 do NOT reveal admin-only features.";
35464
+ return [
35465
+ "You are the 4PM product support assistant. Answer the user's question about how to use 4PM",
35466
+ "using ONLY the documentation and FAQ provided below. If the answer is not in the docs, say",
35467
+ "you don't have that information and suggest contacting human support \u2014 do not invent details.",
35468
+ roleNote,
35469
+ "Answer concisely in the user's language; reference the relevant doc heading/path when useful.",
35470
+ "",
35471
+ "===== DOCUMENTATION =====",
35472
+ docs,
35473
+ "",
35474
+ "===== QUESTION =====",
35475
+ question
35476
+ ].join("\n");
35477
+ }
35478
+ function runClaudeOnce(cmd, profile, prompt, extraEnv) {
35479
+ return new Promise((resolve8, reject) => {
35480
+ const args = ["-p", ...profile?.model ? ["--model", profile.model] : []];
35481
+ const env3 = {
35482
+ ...process.env,
35483
+ ...extraEnv,
35484
+ ...profile ? { CLAUDE_CONFIG_DIR: profile.dir } : {}
35485
+ };
35486
+ const child = spawn4(cmd, args, { stdio: ["pipe", "pipe", "pipe"], env: env3 });
35487
+ let out = "";
35488
+ let err = "";
35489
+ const timer = setTimeout(() => {
35490
+ child.kill("SIGKILL");
35491
+ reject(new Error("claude answer timed out"));
35492
+ }, ANSWER_TIMEOUT_MS);
35493
+ timer.unref();
35494
+ child.stdout.on("data", (d) => out += d.toString());
35495
+ child.stderr.on("data", (d) => err += d.toString());
35496
+ child.on("error", (e) => {
35497
+ clearTimeout(timer);
35498
+ reject(e);
35499
+ });
35500
+ child.on("close", (code) => {
35501
+ clearTimeout(timer);
35502
+ resolve8({ code: code ?? -1, out, err });
35503
+ });
35504
+ child.stdin.write(prompt);
35505
+ child.stdin.end();
35506
+ });
35507
+ }
35508
+ async function runClaudeWithFailover(prompt, ai) {
35509
+ const attempts = ai.profiles.length > 0 ? ai.profiles : [null];
35510
+ let lastReason = "no attempt";
35511
+ for (let i = 0; i < attempts.length; i++) {
35512
+ const { code, out, err } = await runClaudeOnce(ai.cmd, attempts[i], prompt, ai.env);
35513
+ if (code === 0 && out.trim()) return out.trim();
35514
+ const combined = err || out;
35515
+ lastReason = `exited ${code}: ${combined.slice(0, 500)}`;
35516
+ if (!isAuthFailure(combined) && !isSessionLimit(combined) || i === attempts.length - 1) break;
35517
+ }
35518
+ throw new Error(`claude ${lastReason}`);
35519
+ }
35520
+ async function runSupportAnswer(req, ai, profileDir2) {
35521
+ try {
35522
+ const dir2 = await ensureRepo(profileDir2, req.repo);
35523
+ const docs = collectDocs(dir2);
35524
+ if (!docs.trim()) return { body: "", error: "KB repo has no documentation" };
35525
+ const body = await runClaudeWithFailover(buildPrompt(docs, req.question, req.askerRole), ai);
35526
+ if (!body) return { body: "", error: "empty answer" };
35527
+ return { body };
35528
+ } catch (err) {
35529
+ logger.warn("support.answer.failed", { error: String(err) });
35530
+ return { body: "", error: String(err) };
35531
+ }
35532
+ }
35533
+
35534
+ // src/core/knowledge-compose.ts
35535
+ import { spawn as spawn5 } from "child_process";
35536
+ var COMPOSE_TIMEOUT_MS = 18e4;
35537
+ function buildPrompt2(req) {
35538
+ const focus = req.prompt ? `
35539
+
35540
+ Extra focus from the author: ${req.prompt}` : "";
35541
+ return [
35542
+ `You are in the working directory of a software project named "${req.projectName}".`,
35543
+ "Read the project (code, docs, config) and write a concise, shareable KNOWLEDGE ARTICLE that",
35544
+ "distils it for other engineers. Use Markdown and this structure:",
35545
+ "- a single top-level title line starting with '# '",
35546
+ "- a one-paragraph summary",
35547
+ "- '## The problem' \u2014 what problem the project solves",
35548
+ "- '## The approach' \u2014 the key decisions and how it works",
35549
+ "- '## What we learned' \u2014 reusable takeaways",
35550
+ "Output ONLY the Markdown article, no preamble or code fences around the whole thing." + focus
35551
+ ].join("\n");
35552
+ }
35553
+ function runOnce(cmd, profile, prompt, cwd2, extraEnv) {
35554
+ return new Promise((resolve8, reject) => {
35555
+ const args = ["-p", ...profile?.model ? ["--model", profile.model] : []];
35556
+ const env3 = {
35557
+ ...process.env,
35558
+ ...extraEnv,
35559
+ ...profile ? { CLAUDE_CONFIG_DIR: profile.dir } : {}
35560
+ };
35561
+ const child = spawn5(cmd, args, { stdio: ["pipe", "pipe", "pipe"], cwd: cwd2, env: env3 });
35562
+ let out = "";
35563
+ let err = "";
35564
+ const timer = setTimeout(() => {
35565
+ child.kill("SIGKILL");
35566
+ reject(new Error("knowledge compose timed out"));
35567
+ }, COMPOSE_TIMEOUT_MS);
35568
+ timer.unref();
35569
+ child.stdout.on("data", (d) => out += d.toString());
35570
+ child.stderr.on("data", (d) => err += d.toString());
35571
+ child.on("error", (e) => {
35572
+ clearTimeout(timer);
35573
+ reject(e);
35574
+ });
35575
+ child.on("close", (code) => {
35576
+ clearTimeout(timer);
35577
+ resolve8({ code: code ?? -1, out, err });
35578
+ });
35579
+ child.stdin.write(prompt);
35580
+ child.stdin.end();
35581
+ });
35582
+ }
35583
+ async function runKnowledgeCompose(req, ai, physicRoot) {
35584
+ const prompt = buildPrompt2(req);
35585
+ const attempts = ai.profiles.length > 0 ? ai.profiles : [null];
35586
+ let lastReason = "no attempt";
35587
+ for (let i = 0; i < attempts.length; i++) {
35588
+ try {
35589
+ const { code, out, err } = await runOnce(ai.cmd, attempts[i], prompt, physicRoot, ai.env);
35590
+ if (code === 0 && out.trim()) return { bodyMarkdown: out.trim() };
35591
+ const combined = err || out;
35592
+ lastReason = `exited ${code}: ${combined.slice(0, 500)}`;
35593
+ if (!isAuthFailure(combined) && !isSessionLimit(combined) || i === attempts.length - 1) break;
35594
+ } catch (e) {
35595
+ lastReason = String(e);
35596
+ break;
35597
+ }
35598
+ }
35599
+ return { bodyMarkdown: "", error: lastReason };
35600
+ }
35601
+
35602
+ // src/core/autonomous.ts
35603
+ import { readFile as readFile2, writeFile as writeFile2, chmod } from "fs/promises";
35604
+ import { readFileSync as readFileSync13 } from "fs";
35605
+ import { execFile as execFile3, spawn as spawn6 } from "child_process";
35606
+ import { join as join13 } from "path";
35607
+ import { promisify as promisify3 } from "util";
35608
+ var run2 = promisify3(execFile3);
35063
35609
  var SETTINGS_REL = ".claude/.autonomous.settings.json";
35064
35610
  var HISTORIES_REL = ".claude/.autonomous.histories.json";
35065
35611
  var APPROVALS_REL = ".claude/.autonomous.approvals.json";
@@ -35089,7 +35635,7 @@ function parseJson(text) {
35089
35635
  }
35090
35636
  }
35091
35637
  function tickScript(root) {
35092
- return join12(root, TICK_REL);
35638
+ return join13(root, TICK_REL);
35093
35639
  }
35094
35640
  async function crontabList() {
35095
35641
  try {
@@ -35101,7 +35647,7 @@ async function crontabList() {
35101
35647
  }
35102
35648
  async function crontabSet(content) {
35103
35649
  return new Promise((resolve8) => {
35104
- const child = spawn4("crontab", ["-"], { stdio: ["pipe", "ignore", "ignore"] });
35650
+ const child = spawn6("crontab", ["-"], { stdio: ["pipe", "ignore", "ignore"] });
35105
35651
  child.on("error", () => resolve8(false));
35106
35652
  child.on("close", (code) => resolve8(code === 0));
35107
35653
  child.stdin.end(content.endsWith("\n") ? content : content + "\n");
@@ -35112,8 +35658,8 @@ async function cronInstalled(root) {
35112
35658
  return (await crontabList()).split("\n").some((l) => l.includes(script) && !l.trim().startsWith("#"));
35113
35659
  }
35114
35660
  async function getAutonomousStatus(root) {
35115
- const settings = parseJson(await readText(join12(root, SETTINGS_REL))) ?? {};
35116
- const hist = parseJson(await readText(join12(root, HISTORIES_REL))) ?? {};
35661
+ const settings = parseJson(await readText(join13(root, SETTINGS_REL))) ?? {};
35662
+ const hist = parseJson(await readText(join13(root, HISTORIES_REL))) ?? {};
35117
35663
  const records = Array.isArray(hist.records) ? hist.records : [];
35118
35664
  const last = records[records.length - 1];
35119
35665
  const ticks = hist.ticks ?? {};
@@ -35130,9 +35676,9 @@ async function getAutonomousStatus(root) {
35130
35676
  }
35131
35677
  function isAutonomousRunning(root) {
35132
35678
  try {
35133
- const s = JSON.parse(readFileSync12(join12(root, SETTINGS_REL), "utf8"));
35679
+ const s = JSON.parse(readFileSync13(join13(root, SETTINGS_REL), "utf8"));
35134
35680
  if (s.paused === true) return false;
35135
- const h = JSON.parse(readFileSync12(join12(root, HISTORIES_REL), "utf8"));
35681
+ const h = JSON.parse(readFileSync13(join13(root, HISTORIES_REL), "utf8"));
35136
35682
  const ts = h.records?.[h.records.length - 1]?.ts;
35137
35683
  if (!ts) return false;
35138
35684
  const t = Date.parse(ts.replace(" ", "T"));
@@ -35143,10 +35689,10 @@ function isAutonomousRunning(root) {
35143
35689
  }
35144
35690
  async function readAutonomous(root) {
35145
35691
  const [settings, approvals, status, ...books] = await Promise.all([
35146
- readText(join12(root, SETTINGS_REL)),
35147
- readText(join12(root, APPROVALS_REL), "{}"),
35692
+ readText(join13(root, SETTINGS_REL)),
35693
+ readText(join13(root, APPROVALS_REL), "{}"),
35148
35694
  getAutonomousStatus(root),
35149
- ...Object.values(BOOK_FILES).map((f) => readText(join12(root, f)))
35695
+ ...Object.values(BOOK_FILES).map((f) => readText(join13(root, f)))
35150
35696
  ]);
35151
35697
  const keys = Object.keys(BOOK_FILES);
35152
35698
  const bookMap = {};
@@ -35155,7 +35701,7 @@ async function readAutonomous(root) {
35155
35701
  }
35156
35702
  async function readAutonomousLogs(root, date) {
35157
35703
  const day = date && /^\d{4}-\d{2}-\d{2}$/.test(date) ? date : (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
35158
- const text = await readText(join12(root, LOG_DIR_REL, `autonomous-tick-${day}.log`));
35704
+ const text = await readText(join13(root, LOG_DIR_REL, `autonomous-tick-${day}.log`));
35159
35705
  const lines = text ? text.split("\n").filter((l) => l.length > 0).slice(-500) : [];
35160
35706
  return { date: day, lines };
35161
35707
  }
@@ -35179,24 +35725,24 @@ async function writeAutonomous(root, req, by) {
35179
35725
  switch (req.kind) {
35180
35726
  case "settings": {
35181
35727
  if (!parseJson(req.settings)) return { ok: false, error: "settings is not valid JSON" };
35182
- await writeFile2(join12(root, SETTINGS_REL), req.settings, "utf8");
35728
+ await writeFile2(join13(root, SETTINGS_REL), req.settings, "utf8");
35183
35729
  break;
35184
35730
  }
35185
35731
  case "approvals": {
35186
- const map = parseJson(await readText(join12(root, APPROVALS_REL), "{}")) ?? {};
35732
+ const map = parseJson(await readText(join13(root, APPROVALS_REL), "{}")) ?? {};
35187
35733
  if (req.approved) map[req.taskId] = { approved: true, by, at: (/* @__PURE__ */ new Date()).toISOString() };
35188
35734
  else delete map[req.taskId];
35189
- await writeFile2(join12(root, APPROVALS_REL), JSON.stringify(map, null, 2) + "\n", "utf8");
35735
+ await writeFile2(join13(root, APPROVALS_REL), JSON.stringify(map, null, 2) + "\n", "utf8");
35190
35736
  break;
35191
35737
  }
35192
35738
  case "userTodo": {
35193
- const cur = await readText(join12(root, BOOK_FILES.userTodo));
35739
+ const cur = await readText(join13(root, BOOK_FILES.userTodo));
35194
35740
  const block = `
35195
35741
 
35196
35742
  > posted by ${by} at ${(/* @__PURE__ */ new Date()).toISOString()}
35197
35743
  ${req.content.trim()}
35198
35744
  `;
35199
- await writeFile2(join12(root, BOOK_FILES.userTodo), cur.trimEnd() + block, "utf8");
35745
+ await writeFile2(join13(root, BOOK_FILES.userTodo), cur.trimEnd() + block, "utf8");
35200
35746
  break;
35201
35747
  }
35202
35748
  case "cron": {
@@ -35213,7 +35759,7 @@ ${req.content.trim()}
35213
35759
 
35214
35760
  // src/core/agents.ts
35215
35761
  import { readFile as readFile3, writeFile as writeFile3, mkdir as mkdir3, readdir as readdir3, rm } from "fs/promises";
35216
- import { join as join13 } from "path";
35762
+ import { join as join14 } from "path";
35217
35763
  var AGENTS_REL = ".claude/agents";
35218
35764
  var SKILLS_REL = ".claude/skills";
35219
35765
  var MEMORY_DIRS = /* @__PURE__ */ new Set(["short-memory", "long-memory"]);
@@ -35230,24 +35776,24 @@ async function readText2(path, fallback = "") {
35230
35776
  function frontmatter(text) {
35231
35777
  const m = text.match(/^---\r?\n([\s\S]*?)\r?\n---/);
35232
35778
  const body = m?.[1] ?? "";
35233
- const get = (key) => body.match(new RegExp(`^${key}:\\s*(.+)$`, "m"))?.[1]?.trim().replace(/^["']|["']$/g, "") ?? "";
35234
- return { name: get("name"), description: get("description") };
35779
+ const get2 = (key) => body.match(new RegExp(`^${key}:\\s*(.+)$`, "m"))?.[1]?.trim().replace(/^["']|["']$/g, "") ?? "";
35780
+ return { name: get2("name"), description: get2("description") };
35235
35781
  }
35236
35782
  async function listAgents(root) {
35237
35783
  const subagents = [];
35238
35784
  const skills = [];
35239
35785
  try {
35240
- for (const e of await readdir3(join13(root, AGENTS_REL), { withFileTypes: true })) {
35786
+ for (const e of await readdir3(join14(root, AGENTS_REL), { withFileTypes: true })) {
35241
35787
  if (!e.isFile() || !e.name.endsWith(".md") || MEMORY_DIRS.has(e.name)) continue;
35242
- const fm = frontmatter(await readText2(join13(root, AGENTS_REL, e.name)));
35788
+ const fm = frontmatter(await readText2(join14(root, AGENTS_REL, e.name)));
35243
35789
  subagents.push({ name: e.name.replace(/\.md$/, ""), description: fm.description });
35244
35790
  }
35245
35791
  } catch {
35246
35792
  }
35247
35793
  try {
35248
- for (const e of await readdir3(join13(root, SKILLS_REL), { withFileTypes: true })) {
35794
+ for (const e of await readdir3(join14(root, SKILLS_REL), { withFileTypes: true })) {
35249
35795
  if (!e.isDirectory()) continue;
35250
- const fm = frontmatter(await readText2(join13(root, SKILLS_REL, e.name, "SKILL.md")));
35796
+ const fm = frontmatter(await readText2(join14(root, SKILLS_REL, e.name, "SKILL.md")));
35251
35797
  skills.push({ name: e.name, description: fm.description });
35252
35798
  }
35253
35799
  } catch {
@@ -35258,15 +35804,15 @@ async function listAgents(root) {
35258
35804
  }
35259
35805
  function itemPath(root, kind, name) {
35260
35806
  const n = safeName(name);
35261
- return kind === "subagent" ? join13(root, AGENTS_REL, `${n}.md`) : join13(root, SKILLS_REL, n, "SKILL.md");
35807
+ return kind === "subagent" ? join14(root, AGENTS_REL, `${n}.md`) : join14(root, SKILLS_REL, n, "SKILL.md");
35262
35808
  }
35263
35809
  async function readAgent(root, kind, name) {
35264
35810
  const content = await readText2(itemPath(root, kind, name));
35265
35811
  if (kind !== "subagent") return { content };
35266
35812
  const n = safeName(name);
35267
35813
  const [short, long] = await Promise.all([
35268
- readText2(join13(root, AGENTS_REL, "short-memory", `${n}.md`)),
35269
- readText2(join13(root, AGENTS_REL, "long-memory", `${n}.md`))
35814
+ readText2(join14(root, AGENTS_REL, "short-memory", `${n}.md`)),
35815
+ readText2(join14(root, AGENTS_REL, "long-memory", `${n}.md`))
35270
35816
  ]);
35271
35817
  return { content, memory: { short, long } };
35272
35818
  }
@@ -35276,12 +35822,12 @@ async function writeAgent(root, req) {
35276
35822
  try {
35277
35823
  if (req.action === "delete") {
35278
35824
  if (req.kind === "subagent") await rm(itemPath(root, "subagent", n), { force: true });
35279
- else await rm(join13(root, SKILLS_REL, n), { recursive: true, force: true });
35825
+ else await rm(join14(root, SKILLS_REL, n), { recursive: true, force: true });
35280
35826
  return { ok: true };
35281
35827
  }
35282
35828
  if (req.content === void 0) return { ok: false, error: "content is required" };
35283
35829
  const path = itemPath(root, req.kind, n);
35284
- await mkdir3(join13(path, ".."), { recursive: true });
35830
+ await mkdir3(join14(path, ".."), { recursive: true });
35285
35831
  await writeFile3(path, req.content, "utf8");
35286
35832
  return { ok: true };
35287
35833
  } catch (err) {
@@ -35289,22 +35835,257 @@ async function writeAgent(root, req) {
35289
35835
  }
35290
35836
  }
35291
35837
 
35838
+ // src/core/packages.ts
35839
+ import { createHash as createHash4 } from "crypto";
35840
+ import { readFile as readFile4, writeFile as writeFile4, mkdir as mkdir4, readdir as readdir4, rm as rm2, stat } from "fs/promises";
35841
+ import { dirname as dirname4, join as join15, relative } from "path";
35842
+ var AGENTS_REL2 = ".claude/agents";
35843
+ var SKILLS_REL2 = ".claude/skills";
35844
+ var MANIFEST_REL = ".claude/.4pm-packages.json";
35845
+ function safeName2(name) {
35846
+ return name.replace(/[^\w.-]+/g, "-").replace(/^[-.]+|[-.]+$/g, "");
35847
+ }
35848
+ async function readMaybe(path) {
35849
+ try {
35850
+ return await readFile4(path);
35851
+ } catch {
35852
+ return null;
35853
+ }
35854
+ }
35855
+ async function listFilesRel(dir2) {
35856
+ const out = [];
35857
+ async function walk2(d) {
35858
+ let entries2;
35859
+ try {
35860
+ entries2 = await readdir4(d, { withFileTypes: true });
35861
+ } catch {
35862
+ return;
35863
+ }
35864
+ for (const e of entries2) {
35865
+ const full = join15(d, e.name);
35866
+ if (e.isDirectory()) await walk2(full);
35867
+ else if (e.isFile()) out.push(relative(dir2, full));
35868
+ }
35869
+ }
35870
+ await walk2(dir2);
35871
+ return out.sort();
35872
+ }
35873
+ async function readArtifactFiles(root, kind, slug) {
35874
+ const files = [];
35875
+ if (kind === "subagent") {
35876
+ const agent = await readMaybe(join15(root, AGENTS_REL2, `${slug}.md`));
35877
+ if (agent) files.push({ path: "agent.md", contentBase64: agent.toString("base64") });
35878
+ const short = await readMaybe(join15(root, AGENTS_REL2, "short-memory", `${slug}.md`));
35879
+ if (short) files.push({ path: "short-memory.md", contentBase64: short.toString("base64") });
35880
+ const long = await readMaybe(join15(root, AGENTS_REL2, "long-memory", `${slug}.md`));
35881
+ if (long) files.push({ path: "long-memory.md", contentBase64: long.toString("base64") });
35882
+ } else {
35883
+ const base = join15(root, SKILLS_REL2, slug);
35884
+ for (const rel of await listFilesRel(base)) {
35885
+ const buf = await readMaybe(join15(base, rel));
35886
+ if (buf) files.push({ path: rel.split("\\").join("/"), contentBase64: buf.toString("base64") });
35887
+ }
35888
+ }
35889
+ return files;
35890
+ }
35891
+ async function artifactSha256(root, kind, slug) {
35892
+ const files = await readArtifactFiles(root, kind, slug);
35893
+ const h = createHash4("sha256");
35894
+ for (const f of [...files].sort((a, b) => a.path.localeCompare(b.path))) {
35895
+ h.update(f.path);
35896
+ h.update("\0");
35897
+ h.update(f.contentBase64);
35898
+ h.update("\0");
35899
+ }
35900
+ return h.digest("hex");
35901
+ }
35902
+ async function artifactExists(root, kind, slug) {
35903
+ const target = kind === "subagent" ? join15(root, AGENTS_REL2, `${slug}.md`) : join15(root, SKILLS_REL2, slug);
35904
+ try {
35905
+ await stat(target);
35906
+ return true;
35907
+ } catch {
35908
+ return false;
35909
+ }
35910
+ }
35911
+ async function readManifest(root) {
35912
+ const buf = await readMaybe(join15(root, MANIFEST_REL));
35913
+ if (!buf) return {};
35914
+ try {
35915
+ return JSON.parse(buf.toString("utf8"));
35916
+ } catch {
35917
+ return {};
35918
+ }
35919
+ }
35920
+ async function writeManifest(root, manifest) {
35921
+ const path = join15(root, MANIFEST_REL);
35922
+ await mkdir4(dirname4(path), { recursive: true });
35923
+ await writeFile4(path, JSON.stringify(manifest, null, 2), "utf8");
35924
+ }
35925
+ async function writeArtifactFiles(root, kind, slug, files) {
35926
+ const byPath = new Map(files.map((f) => [f.path, Buffer.from(f.contentBase64, "base64")]));
35927
+ if (kind === "subagent") {
35928
+ const agent = byPath.get("agent.md");
35929
+ if (agent) {
35930
+ await mkdir4(join15(root, AGENTS_REL2), { recursive: true });
35931
+ await writeFile4(join15(root, AGENTS_REL2, `${slug}.md`), agent);
35932
+ }
35933
+ const short = byPath.get("short-memory.md");
35934
+ if (short) {
35935
+ await mkdir4(join15(root, AGENTS_REL2, "short-memory"), { recursive: true });
35936
+ await writeFile4(join15(root, AGENTS_REL2, "short-memory", `${slug}.md`), short);
35937
+ }
35938
+ const long = byPath.get("long-memory.md");
35939
+ if (long) {
35940
+ await mkdir4(join15(root, AGENTS_REL2, "long-memory"), { recursive: true });
35941
+ await writeFile4(join15(root, AGENTS_REL2, "long-memory", `${slug}.md`), long);
35942
+ }
35943
+ } else {
35944
+ const base = join15(root, SKILLS_REL2, slug);
35945
+ for (const [rel, buf] of byPath) {
35946
+ if (rel.includes("..") || rel.startsWith("/")) continue;
35947
+ const dest = join15(base, rel);
35948
+ await mkdir4(dirname4(dest), { recursive: true });
35949
+ await writeFile4(dest, buf);
35950
+ }
35951
+ }
35952
+ }
35953
+ async function packPackage(root, kind, name) {
35954
+ const slug = safeName2(name);
35955
+ if (!slug) return { ok: false, files: [], error: "invalid name" };
35956
+ const files = await readArtifactFiles(root, kind, slug);
35957
+ if (files.length === 0) return { ok: false, files: [], error: "artifact not found" };
35958
+ return { ok: true, files };
35959
+ }
35960
+ async function installPackage(root, req) {
35961
+ const slug = safeName2(req.slug);
35962
+ if (!slug) return { ok: false, error: "invalid slug" };
35963
+ try {
35964
+ const manifest = await readManifest(root);
35965
+ const existing = manifest[slug];
35966
+ if (!existing && await artifactExists(root, req.kind, slug)) {
35967
+ return { ok: false, error: "ALREADY_INSTALLED" };
35968
+ }
35969
+ if (existing) {
35970
+ const currentSha = await artifactSha256(root, req.kind, slug);
35971
+ if (currentSha !== existing.sha256) return { ok: false, error: "DRIFT" };
35972
+ }
35973
+ await writeArtifactFiles(root, req.kind, slug, req.files);
35974
+ const sha256 = await artifactSha256(root, req.kind, slug);
35975
+ manifest[slug] = {
35976
+ kind: req.kind,
35977
+ version: req.version,
35978
+ packageId: req.packageId,
35979
+ installedAt: (/* @__PURE__ */ new Date()).toISOString(),
35980
+ sha256
35981
+ };
35982
+ await writeManifest(root, manifest);
35983
+ return { ok: true, sha256 };
35984
+ } catch (err) {
35985
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
35986
+ }
35987
+ }
35988
+ async function listPackages(root) {
35989
+ const manifest = await readManifest(root);
35990
+ const installed = [];
35991
+ for (const [slug, entry] of Object.entries(manifest)) {
35992
+ installed.push({
35993
+ slug,
35994
+ kind: entry.kind,
35995
+ version: entry.version,
35996
+ packageId: entry.packageId,
35997
+ installedAt: entry.installedAt,
35998
+ sha256: entry.sha256,
35999
+ currentSha256: await artifactSha256(root, entry.kind, slug)
36000
+ });
36001
+ }
36002
+ installed.sort((a, b) => a.slug.localeCompare(b.slug));
36003
+ return { installed };
36004
+ }
36005
+ async function removePackage(root, slug) {
36006
+ const n = safeName2(slug);
36007
+ if (!n) return { ok: false, error: "invalid slug" };
36008
+ try {
36009
+ const manifest = await readManifest(root);
36010
+ const entry = manifest[n];
36011
+ if (!entry) return { ok: false, error: "not installed" };
36012
+ if (entry.kind === "subagent") {
36013
+ await rm2(join15(root, AGENTS_REL2, `${n}.md`), { force: true });
36014
+ await rm2(join15(root, AGENTS_REL2, "short-memory", `${n}.md`), { force: true });
36015
+ await rm2(join15(root, AGENTS_REL2, "long-memory", `${n}.md`), { force: true });
36016
+ } else {
36017
+ await rm2(join15(root, SKILLS_REL2, n), { recursive: true, force: true });
36018
+ }
36019
+ delete manifest[n];
36020
+ await writeManifest(root, manifest);
36021
+ return { ok: true };
36022
+ } catch (err) {
36023
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
36024
+ }
36025
+ }
36026
+
36027
+ // src/core/sandbox.ts
36028
+ import { rmSync as rmSync4, writeFileSync as writeFileSync6 } from "fs";
36029
+ import { join as join16 } from "path";
36030
+ var EGRESS_ALLOWLIST_FILE = "sandbox-egress.txt";
36031
+ function writeEgressAllowlist(profileDir2, policy) {
36032
+ const path = join16(profileDir2, EGRESS_ALLOWLIST_FILE);
36033
+ try {
36034
+ if (policy && policy.network === "allowlist") {
36035
+ const body = ["# 4PM egress allowlist (ADR-0192) \u2014 generated from the project sandbox policy"].concat(policy.allowedDomains).join("\n");
36036
+ writeFileSync6(path, `${body}
36037
+ `, "utf8");
36038
+ } else {
36039
+ rmSync4(path, { force: true });
36040
+ }
36041
+ } catch {
36042
+ }
36043
+ }
36044
+
36045
+ // src/core/git-auth.ts
36046
+ import { writeFileSync as writeFileSync7 } from "fs";
36047
+ import { homedir as homedir4 } from "os";
36048
+ import { join as join17 } from "path";
36049
+ function tokenTarget(method) {
36050
+ if (method === "gitlab-group-token") {
36051
+ return { host: process.env.FOURPM_GIT_HOST || "gitlab.com", user: "oauth2" };
36052
+ }
36053
+ if (method === "github-app") {
36054
+ return { host: process.env.FOURPM_GIT_HOST || "github.com", user: "x-access-token" };
36055
+ }
36056
+ return null;
36057
+ }
36058
+ function applyGitAuth(method) {
36059
+ if (!method) return false;
36060
+ const target = tokenTarget(method);
36061
+ const token = process.env.FOURPM_GIT_TOKEN;
36062
+ if (!target || !token) return false;
36063
+ try {
36064
+ const line = `https://${target.user}:${token}@${target.host}
36065
+ `;
36066
+ writeFileSync7(join17(homedir4(), ".git-credentials"), line, { encoding: "utf8", mode: 384 });
36067
+ return true;
36068
+ } catch {
36069
+ return false;
36070
+ }
36071
+ }
36072
+
35292
36073
  // src/core/secrets.ts
35293
- import { readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
35294
- import { join as join14 } from "path";
36074
+ import { readFile as readFile5, writeFile as writeFile5 } from "fs/promises";
36075
+ import { join as join18 } from "path";
35295
36076
  var SECURITY_REL = "AI_SECURITY.md";
35296
36077
  var PLACEHOLDER_REL = "AI_PLACEHOLDER.md";
35297
36078
  var SECRETS_REL = "project.secrets.json";
35298
36079
  async function readText3(path, fallback = "") {
35299
36080
  try {
35300
- return await readFile4(path, "utf8");
36081
+ return await readFile5(path, "utf8");
35301
36082
  } catch {
35302
36083
  return fallback;
35303
36084
  }
35304
36085
  }
35305
36086
  async function readSecretsObject(root) {
35306
36087
  try {
35307
- const v = JSON.parse(await readFile4(join14(root, SECRETS_REL), "utf8"));
36088
+ const v = JSON.parse(await readFile5(join18(root, SECRETS_REL), "utf8"));
35308
36089
  return v && typeof v === "object" ? v : {};
35309
36090
  } catch {
35310
36091
  return {};
@@ -35323,8 +36104,8 @@ function placeholderKeys(text) {
35323
36104
  }
35324
36105
  async function readSecrets(root) {
35325
36106
  const [security, placeholder, secrets] = await Promise.all([
35326
- readText3(join14(root, SECURITY_REL)),
35327
- readText3(join14(root, PLACEHOLDER_REL)),
36107
+ readText3(join18(root, SECURITY_REL)),
36108
+ readText3(join18(root, PLACEHOLDER_REL)),
35328
36109
  readSecretsObject(root)
35329
36110
  ]);
35330
36111
  const names = /* @__PURE__ */ new Set([...placeholderKeys(placeholder), ...Object.keys(secrets)]);
@@ -35337,14 +36118,14 @@ async function readSecrets(root) {
35337
36118
  async function writeSecrets(root, req) {
35338
36119
  try {
35339
36120
  if (req.kind === "security") {
35340
- await writeFile4(join14(root, SECURITY_REL), req.content, "utf8");
36121
+ await writeFile5(join18(root, SECURITY_REL), req.content, "utf8");
35341
36122
  } else if (req.kind === "placeholder") {
35342
- await writeFile4(join14(root, PLACEHOLDER_REL), req.content, "utf8");
36123
+ await writeFile5(join18(root, PLACEHOLDER_REL), req.content, "utf8");
35343
36124
  } else {
35344
36125
  const secrets = await readSecretsObject(root);
35345
36126
  if (req.kind === "secret") secrets[req.key] = req.value;
35346
36127
  else delete secrets[req.key];
35347
- await writeFile4(join14(root, SECRETS_REL), JSON.stringify(secrets, null, 2) + "\n", "utf8");
36128
+ await writeFile5(join18(root, SECRETS_REL), JSON.stringify(secrets, null, 2) + "\n", "utf8");
35348
36129
  }
35349
36130
  return { ok: true };
35350
36131
  } catch (err) {
@@ -35352,9 +36133,52 @@ async function writeSecrets(root, req) {
35352
36133
  }
35353
36134
  }
35354
36135
 
36136
+ // src/core/agent-tools.ts
36137
+ import { readFile as readFile6, writeFile as writeFile6, mkdir as mkdir5 } from "fs/promises";
36138
+ import { dirname as dirname5, join as join19 } from "path";
36139
+ var SHARED_REL = ".claude/settings.json";
36140
+ var LOCAL_REL = ".claude/settings.local.json";
36141
+ var SECRETS_DENY = "Read(./project.secrets.json)";
36142
+ var MODES = ["default", "acceptEdits", "plan", "bypassPermissions"];
36143
+ function fileFor(root, scope) {
36144
+ return join19(root, scope === "shared" ? SHARED_REL : LOCAL_REL);
36145
+ }
36146
+ async function readSettingsObject(path) {
36147
+ try {
36148
+ const v = JSON.parse(await readFile6(path, "utf8"));
36149
+ return v && typeof v === "object" ? v : {};
36150
+ } catch {
36151
+ return {};
36152
+ }
36153
+ }
36154
+ function normalizePermissions(raw) {
36155
+ const p = raw && typeof raw === "object" ? raw : {};
36156
+ const mode = MODES.includes(p.defaultMode) ? p.defaultMode : "default";
36157
+ const list = (v) => Array.isArray(v) ? v.filter((x) => typeof x === "string") : [];
36158
+ return { defaultMode: mode, allow: list(p.allow), ask: list(p.ask), deny: list(p.deny) };
36159
+ }
36160
+ async function readAgentTools(root, scope) {
36161
+ const obj = await readSettingsObject(fileFor(root, scope));
36162
+ return { permissions: normalizePermissions(obj.permissions) };
36163
+ }
36164
+ async function writeAgentTools(root, scope, permissions) {
36165
+ try {
36166
+ const path = fileFor(root, scope);
36167
+ const obj = await readSettingsObject(path);
36168
+ const next = normalizePermissions(permissions);
36169
+ if (scope === "shared" && !next.deny.includes(SECRETS_DENY)) next.deny = [...next.deny, SECRETS_DENY];
36170
+ obj.permissions = next;
36171
+ await mkdir5(dirname5(path), { recursive: true });
36172
+ await writeFile6(path, JSON.stringify(obj, null, 2) + "\n", "utf8");
36173
+ return { ok: true };
36174
+ } catch (err) {
36175
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
36176
+ }
36177
+ }
36178
+
35355
36179
  // src/core/graph.ts
35356
- import { readFile as readFile5, readdir as readdir4 } from "fs/promises";
35357
- import { join as join15, relative, dirname as dirname4, resolve as resolve4, extname, basename as basename2 } from "path";
36180
+ import { readFile as readFile7, readdir as readdir5 } from "fs/promises";
36181
+ import { join as join20, relative as relative2, dirname as dirname6, resolve as resolve4, extname, basename as basename2 } from "path";
35358
36182
  var MAX_FILES = 2e3;
35359
36183
  var MAX_NODES = 3e3;
35360
36184
  var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".next", "out", "coverage", ".turbo"]);
@@ -35366,7 +36190,7 @@ async function walk(root, accept) {
35366
36190
  const dir2 = stack.pop();
35367
36191
  let entries2;
35368
36192
  try {
35369
- entries2 = await readdir4(dir2, { withFileTypes: true });
36193
+ entries2 = await readdir5(dir2, { withFileTypes: true });
35370
36194
  } catch {
35371
36195
  continue;
35372
36196
  }
@@ -35374,7 +36198,7 @@ async function walk(root, accept) {
35374
36198
  if (e.name.startsWith(".") && e.name !== ".claude") {
35375
36199
  if (e.isDirectory()) continue;
35376
36200
  }
35377
- const full = join15(dir2, e.name);
36201
+ const full = join20(dir2, e.name);
35378
36202
  if (e.isDirectory()) {
35379
36203
  if (!SKIP_DIRS.has(e.name)) stack.push(full);
35380
36204
  } else if (accept(full)) {
@@ -35391,25 +36215,25 @@ function orphansOf(nodes, edges) {
35391
36215
  }
35392
36216
  async function buildDocs(root) {
35393
36217
  const files = await walk(root, (p) => extname(p) === ".md");
35394
- const ids = new Set(files.map((f) => relative(root, f).split("\\").join("/")));
36218
+ const ids = new Set(files.map((f) => relative2(root, f).split("\\").join("/")));
35395
36219
  const nodes = files.map((f) => {
35396
- const rel = relative(root, f).split("\\").join("/");
36220
+ const rel = relative2(root, f).split("\\").join("/");
35397
36221
  return { id: rel, label: basename2(rel), kind: "doc", file: rel };
35398
36222
  });
35399
36223
  const edges = [];
35400
36224
  const seen = /* @__PURE__ */ new Set();
35401
36225
  for (const f of files) {
35402
- const rel = relative(root, f).split("\\").join("/");
36226
+ const rel = relative2(root, f).split("\\").join("/");
35403
36227
  let text = "";
35404
36228
  try {
35405
- text = await readFile5(f, "utf8");
36229
+ text = await readFile7(f, "utf8");
35406
36230
  } catch {
35407
36231
  continue;
35408
36232
  }
35409
36233
  for (const m of text.matchAll(/\[[^\]]*\]\(([^)]+)\)/g)) {
35410
36234
  const href = (m[1] ?? "").split("#")[0].trim();
35411
36235
  if (!href || !href.endsWith(".md") || href.startsWith("http")) continue;
35412
- const target = relative(root, resolve4(dirname4(f), href)).split("\\").join("/");
36236
+ const target = relative2(root, resolve4(dirname6(f), href)).split("\\").join("/");
35413
36237
  if (ids.has(target) && target !== rel) {
35414
36238
  const key = `${rel}->${target}`;
35415
36239
  if (!seen.has(key)) {
@@ -35438,10 +36262,10 @@ async function buildCode(root) {
35438
36262
  const fileDefs = /* @__PURE__ */ new Map();
35439
36263
  for (const f of files) {
35440
36264
  if (nodes.length >= MAX_NODES) break;
35441
- const rel = relative(root, f).split("\\").join("/");
36265
+ const rel = relative2(root, f).split("\\").join("/");
35442
36266
  let text = "";
35443
36267
  try {
35444
- text = await readFile5(f, "utf8");
36268
+ text = await readFile7(f, "utf8");
35445
36269
  } catch {
35446
36270
  continue;
35447
36271
  }
@@ -35493,13 +36317,13 @@ async function buildGraph(root, mode) {
35493
36317
  }
35494
36318
 
35495
36319
  // src/core/rag.ts
35496
- import { readFile as readFile6, writeFile as writeFile5, statfs, mkdir as mkdir4 } from "fs/promises";
35497
- import { existsSync as existsSync7 } from "fs";
35498
- import { execFile as execFile3, spawn as spawn5 } from "child_process";
36320
+ import { readFile as readFile8, writeFile as writeFile7, statfs, mkdir as mkdir6 } from "fs/promises";
36321
+ import { existsSync as existsSync8 } from "fs";
36322
+ import { execFile as execFile4, spawn as spawn7 } from "child_process";
35499
36323
  import { totalmem, cpus } from "os";
35500
- import { join as join16 } from "path";
35501
- import { promisify as promisify3 } from "util";
35502
- var run3 = promisify3(execFile3);
36324
+ import { join as join21 } from "path";
36325
+ import { promisify as promisify4 } from "util";
36326
+ var run3 = promisify4(execFile4);
35503
36327
  var RAG_REL = ".claude/rag";
35504
36328
  async function tryRun(cmd, args) {
35505
36329
  try {
@@ -35518,7 +36342,7 @@ async function detectPython() {
35518
36342
  }
35519
36343
  async function installLog(root) {
35520
36344
  try {
35521
- const text = await readFile6(join16(root, RAG_REL, "install.log"), "utf8");
36345
+ const text = await readFile8(join21(root, RAG_REL, "install.log"), "utf8");
35522
36346
  return text.split("\n").filter((l) => l.length > 0).slice(-200);
35523
36347
  } catch {
35524
36348
  return [];
@@ -35580,14 +36404,14 @@ async function ragStatus(root) {
35580
36404
  };
35581
36405
  let modelName = "";
35582
36406
  try {
35583
- modelName = (await readFile6(join16(root, RAG_REL, ".installed"), "utf8")).trim();
36407
+ modelName = (await readFile8(join21(root, RAG_REL, ".installed"), "utf8")).trim();
35584
36408
  } catch {
35585
36409
  modelName = "";
35586
36410
  }
35587
36411
  const modelPresent = modelName.length > 0;
35588
36412
  return {
35589
36413
  installed: fastembed && sqliteVec && modelPresent,
35590
- installing: existsSync7(join16(root, RAG_REL, ".installing")),
36414
+ installing: existsSync8(join21(root, RAG_REL, ".installing")),
35591
36415
  python: { found: py.found, version: py.version },
35592
36416
  deps: { pip, fastembed, sqliteVec },
35593
36417
  model: { present: modelPresent, name: modelName },
@@ -35598,9 +36422,9 @@ async function ragStatus(root) {
35598
36422
  };
35599
36423
  }
35600
36424
  async function readIndexMeta(root) {
35601
- const indexing = existsSync7(join16(root, RAG_REL, ".indexing"));
36425
+ const indexing = existsSync8(join21(root, RAG_REL, ".indexing"));
35602
36426
  try {
35603
- const m = JSON.parse(await readFile6(join16(root, RAG_REL, ".index.json"), "utf8"));
36427
+ const m = JSON.parse(await readFile8(join21(root, RAG_REL, ".index.json"), "utf8"));
35604
36428
  return {
35605
36429
  present: true,
35606
36430
  indexing,
@@ -35667,14 +36491,14 @@ if __name__ == "__main__":
35667
36491
  elif cmd == "query": query(sys.argv[2], int(sys.argv[3]) if len(sys.argv) > 3 else 8)
35668
36492
  `;
35669
36493
  async function ensureEngine(root) {
35670
- await mkdir4(join16(root, RAG_REL), { recursive: true }).catch(() => void 0);
35671
- await writeFile5(join16(root, RAG_REL, "rag_engine.py"), RAG_ENGINE_PY, "utf8");
36494
+ await mkdir6(join21(root, RAG_REL), { recursive: true }).catch(() => void 0);
36495
+ await writeFile7(join21(root, RAG_REL, "rag_engine.py"), RAG_ENGINE_PY, "utf8");
35672
36496
  }
35673
36497
  async function ragReindex(root) {
35674
36498
  const py = await detectPython();
35675
36499
  if (!py.found) return { ok: false, started: false, error: "python3 not found" };
35676
- if (!existsSync7(join16(root, RAG_REL, ".installed"))) return { ok: false, started: false, error: "RAG is not installed" };
35677
- if (existsSync7(join16(root, RAG_REL, ".indexing"))) return { ok: true, started: true };
36500
+ if (!existsSync8(join21(root, RAG_REL, ".installed"))) return { ok: false, started: false, error: "RAG is not installed" };
36501
+ if (existsSync8(join21(root, RAG_REL, ".indexing"))) return { ok: true, started: true };
35678
36502
  await ensureEngine(root);
35679
36503
  const script = `
35680
36504
  set +e
@@ -35682,7 +36506,7 @@ touch "${RAG_REL}/.indexing"
35682
36506
  { echo "[reindex] $(date)"; "$RAG_PY" "${RAG_REL}/rag_engine.py" reindex 2>&1; } >> "${RAG_REL}/index.log" 2>&1
35683
36507
  rm -f "${RAG_REL}/.indexing"
35684
36508
  `;
35685
- const child = spawn5("bash", ["-c", script], {
36509
+ const child = spawn7("bash", ["-c", script], {
35686
36510
  cwd: root,
35687
36511
  env: { ...process.env, RAG_PY: py.bin },
35688
36512
  detached: true,
@@ -35694,12 +36518,12 @@ rm -f "${RAG_REL}/.indexing"
35694
36518
  async function ragQuery(root, queryText, k = 8) {
35695
36519
  const py = await detectPython();
35696
36520
  if (!py.found) return { results: [], error: "python3 not found" };
35697
- if (!existsSync7(join16(root, RAG_REL, "index.db"))) return { results: [], error: "no index \u2014 reindex first" };
36521
+ if (!existsSync8(join21(root, RAG_REL, "index.db"))) return { results: [], error: "no index \u2014 reindex first" };
35698
36522
  await ensureEngine(root);
35699
36523
  try {
35700
36524
  const { stdout } = await run3(
35701
36525
  py.bin,
35702
- [join16(RAG_REL, "rag_engine.py"), "query", queryText, String(Math.min(Math.max(k, 1), 20))],
36526
+ [join21(RAG_REL, "rag_engine.py"), "query", queryText, String(Math.min(Math.max(k, 1), 20))],
35703
36527
  { cwd: root, timeout: 3e4, maxBuffer: 4 * 1024 * 1024 }
35704
36528
  );
35705
36529
  const parsed = JSON.parse(stdout.trim() || '{"results":[]}');
@@ -35711,9 +36535,9 @@ async function ragQuery(root, queryText, k = 8) {
35711
36535
  async function ragInstall(root, model) {
35712
36536
  const py = await detectPython();
35713
36537
  if (!py.found) return { ok: false, started: false, error: "python3 not found on the worker" };
35714
- if (existsSync7(join16(root, RAG_REL, ".installing"))) return { ok: true, started: true };
36538
+ if (existsSync8(join21(root, RAG_REL, ".installing"))) return { ok: true, started: true };
35715
36539
  try {
35716
- await mkdir4(join16(root, RAG_REL), { recursive: true });
36540
+ await mkdir6(join21(root, RAG_REL), { recursive: true });
35717
36541
  } catch {
35718
36542
  }
35719
36543
  const script = `
@@ -35729,7 +36553,7 @@ rm -f "${RAG_REL}/.installed"
35729
36553
  } >> "${RAG_REL}/install.log" 2>&1
35730
36554
  rm -f "${RAG_REL}/.installing"
35731
36555
  `;
35732
- const child = spawn5("bash", ["-c", script], {
36556
+ const child = spawn7("bash", ["-c", script], {
35733
36557
  cwd: root,
35734
36558
  env: { ...process.env, RAG_PY: py.bin, RAG_MODEL: model },
35735
36559
  detached: true,
@@ -35740,18 +36564,18 @@ rm -f "${RAG_REL}/.installing"
35740
36564
  }
35741
36565
 
35742
36566
  // src/core/git-diff.ts
35743
- import { readFile as readFile7 } from "fs/promises";
35744
- import { execFile as execFile4 } from "child_process";
35745
- import { basename as basename3, dirname as dirname5, resolve as resolve5 } from "path";
35746
- import { promisify as promisify4 } from "util";
35747
- var run4 = promisify4(execFile4);
36567
+ import { readFile as readFile9 } from "fs/promises";
36568
+ import { execFile as execFile5 } from "child_process";
36569
+ import { basename as basename3, dirname as dirname7, resolve as resolve5 } from "path";
36570
+ import { promisify as promisify5 } from "util";
36571
+ var run4 = promisify5(execFile5);
35748
36572
  async function gitDiff(path) {
35749
36573
  const target = resolve5(path);
35750
- const dir2 = dirname5(target);
36574
+ const dir2 = dirname7(target);
35751
36575
  const base = basename3(target);
35752
36576
  let newContent = "";
35753
36577
  try {
35754
- newContent = await readFile7(target, "utf8");
36578
+ newContent = await readFile9(target, "utf8");
35755
36579
  } catch {
35756
36580
  }
35757
36581
  let oldContent = "";
@@ -35768,9 +36592,9 @@ async function gitDiff(path) {
35768
36592
  }
35769
36593
 
35770
36594
  // src/core/git-env.ts
35771
- import { execFile as execFile5 } from "child_process";
35772
- import { promisify as promisify5 } from "util";
35773
- var run5 = promisify5(execFile5);
36595
+ import { execFile as execFile6 } from "child_process";
36596
+ import { promisify as promisify6 } from "util";
36597
+ var run5 = promisify6(execFile6);
35774
36598
  async function hasBinary(bin, args = ["--version"]) {
35775
36599
  try {
35776
36600
  await run5(bin, args, { timeout: 5e3 });
@@ -35798,12 +36622,80 @@ ${stderr}`;
35798
36622
  return { installed, authenticated, account, claudeCli };
35799
36623
  }
35800
36624
 
36625
+ // src/core/git-ssh-key.ts
36626
+ import { execFile as execFile7 } from "child_process";
36627
+ import { readFile as readFile10, rm as rm3, mkdir as mkdir7 } from "fs/promises";
36628
+ import { join as join22 } from "path";
36629
+ import { promisify as promisify7 } from "util";
36630
+ var run6 = promisify7(execFile7);
36631
+ function keyPaths(profileDir2) {
36632
+ const dir2 = join22(profileDir2, ".ssh");
36633
+ const key = join22(dir2, "id_4pm");
36634
+ return { dir: dir2, key, pub: `${key}.pub` };
36635
+ }
36636
+ async function readPublicKey(pub) {
36637
+ try {
36638
+ return (await readFile10(pub, "utf8")).trim();
36639
+ } catch {
36640
+ return null;
36641
+ }
36642
+ }
36643
+ async function fingerprintOf(pub) {
36644
+ try {
36645
+ const { stdout } = await run6("ssh-keygen", ["-lf", pub], { timeout: 5e3 });
36646
+ return stdout.match(/SHA256:[A-Za-z0-9+/=]+/)?.[0] ?? null;
36647
+ } catch {
36648
+ return null;
36649
+ }
36650
+ }
36651
+ async function configureGit(key) {
36652
+ await run6(
36653
+ "git",
36654
+ ["config", "--global", "core.sshCommand", `ssh -i ${key} -o IdentitiesOnly=yes`],
36655
+ { timeout: 5e3 }
36656
+ ).catch(() => void 0);
36657
+ }
36658
+ async function generate(profileDir2) {
36659
+ const { dir: dir2, key, pub } = keyPaths(profileDir2);
36660
+ await mkdir7(dir2, { recursive: true });
36661
+ await rm3(key, { force: true });
36662
+ await rm3(pub, { force: true });
36663
+ await run6("ssh-keygen", ["-t", "ed25519", "-f", key, "-N", "", "-C", "4pm-deploy-key"], {
36664
+ timeout: 15e3
36665
+ });
36666
+ await configureGit(key);
36667
+ return { publicKey: await readPublicKey(pub), fingerprint: await fingerprintOf(pub) };
36668
+ }
36669
+ async function remove(profileDir2) {
36670
+ const { key, pub } = keyPaths(profileDir2);
36671
+ await rm3(key, { force: true });
36672
+ await rm3(pub, { force: true });
36673
+ await run6("git", ["config", "--global", "--unset", "core.sshCommand"], { timeout: 5e3 }).catch(
36674
+ () => void 0
36675
+ );
36676
+ return { publicKey: null, fingerprint: null };
36677
+ }
36678
+ async function get(profileDir2) {
36679
+ const { pub } = keyPaths(profileDir2);
36680
+ const publicKey = await readPublicKey(pub);
36681
+ return { publicKey, fingerprint: publicKey ? await fingerprintOf(pub) : null };
36682
+ }
36683
+ async function manageSshKey(op, profileDir2) {
36684
+ try {
36685
+ if (op === "generate") return await generate(profileDir2);
36686
+ if (op === "delete") return await remove(profileDir2);
36687
+ return await get(profileDir2);
36688
+ } catch {
36689
+ return { publicKey: null, fingerprint: null };
36690
+ }
36691
+ }
36692
+
35801
36693
  // src/core/git-history.ts
35802
- import { readdir as readdir5 } from "fs/promises";
35803
- import { execFile as execFile6 } from "child_process";
36694
+ import { readdir as readdir6 } from "fs/promises";
36695
+ import { execFile as execFile8 } from "child_process";
35804
36696
  import { resolve as resolve6, sep as sep4 } from "path";
35805
- import { promisify as promisify6 } from "util";
35806
- var run6 = promisify6(execFile6);
36697
+ import { promisify as promisify8 } from "util";
36698
+ var run7 = promisify8(execFile8);
35807
36699
  var FS = "";
35808
36700
  var RS = "";
35809
36701
  function resolveRepo(root, repo) {
@@ -35814,7 +36706,7 @@ function resolveRepo(root, repo) {
35814
36706
  }
35815
36707
  async function git(cwd2, args) {
35816
36708
  try {
35817
- const { stdout } = await run6("git", args, {
36709
+ const { stdout } = await run7("git", args, {
35818
36710
  cwd: cwd2,
35819
36711
  timeout: 15e3,
35820
36712
  maxBuffer: 8 * 1024 * 1024
@@ -35840,13 +36732,13 @@ async function gitRepos(root) {
35840
36732
  };
35841
36733
  await add(base);
35842
36734
  try {
35843
- const lvl1 = await readdir5(base, { withFileTypes: true });
36735
+ const lvl1 = await readdir6(base, { withFileTypes: true });
35844
36736
  for (const d of lvl1) {
35845
36737
  if (!d.isDirectory() || d.name === ".git" || d.name === "node_modules") continue;
35846
36738
  const p1 = resolve6(base, d.name);
35847
36739
  await add(p1);
35848
36740
  try {
35849
- const lvl2 = await readdir5(p1, { withFileTypes: true });
36741
+ const lvl2 = await readdir6(p1, { withFileTypes: true });
35850
36742
  for (const d2 of lvl2) {
35851
36743
  if (!d2.isDirectory() || d2.name === ".git" || d2.name === "node_modules") continue;
35852
36744
  await add(resolve6(p1, d2.name));
@@ -35916,77 +36808,77 @@ async function gitCommitDiff(root, repo, hash, path) {
35916
36808
  }
35917
36809
 
35918
36810
  // src/core/git-commit-identity.ts
35919
- import { execFile as execFile7 } from "child_process";
35920
- import { promisify as promisify7 } from "util";
35921
- var run7 = promisify7(execFile7);
36811
+ import { execFile as execFile9 } from "child_process";
36812
+ import { promisify as promisify9 } from "util";
36813
+ var run8 = promisify9(execFile9);
35922
36814
  async function setCommitAuthor(cwd2, machineUsername) {
35923
36815
  if (!machineUsername) return;
35924
36816
  try {
35925
- await run7("git", ["config", "--local", "user.name", machineUsername], { cwd: cwd2, timeout: 8e3 });
36817
+ await run8("git", ["config", "--local", "user.name", machineUsername], { cwd: cwd2, timeout: 8e3 });
35926
36818
  } catch {
35927
36819
  }
35928
36820
  }
35929
36821
 
35930
36822
  // src/core/scaffold.ts
35931
- import { cp, mkdir as mkdir5, writeFile as writeFile6 } from "fs/promises";
35932
- import { execFile as execFile8 } from "child_process";
35933
- import { dirname as dirname6, join as join17, resolve as resolve7 } from "path";
36823
+ import { cp, mkdir as mkdir8, writeFile as writeFile8 } from "fs/promises";
36824
+ import { execFile as execFile10 } from "child_process";
36825
+ import { dirname as dirname8, join as join23, resolve as resolve7 } from "path";
35934
36826
  import { fileURLToPath as fileURLToPath3 } from "url";
35935
- import { promisify as promisify8 } from "util";
35936
- var run8 = promisify8(execFile8);
36827
+ import { promisify as promisify10 } from "util";
36828
+ var run9 = promisify10(execFile10);
35937
36829
  function reposOf(spec) {
35938
36830
  const repos = spec?.repos;
35939
36831
  return Array.isArray(repos) ? repos : [];
35940
36832
  }
35941
36833
  function subDirName(repo) {
35942
- const raw = (repo.subdir || repo.role || repo.name || "repo").trim();
36834
+ const raw = (repo.subdir || repo.role || repoName(repo.url ?? "") || "repo").trim();
35943
36835
  return raw.replace(/[^\w.-]+/g, "-").replace(/^-+|-+$/g, "") || "repo";
35944
36836
  }
35945
36837
  async function provisionRepos(target, repos, emit, opts = {}) {
35946
36838
  for (const repo of repos) {
35947
36839
  if (repo.primary) {
35948
- if (opts.clonePrimary && repo.op === "existing" && repo.url) {
36840
+ if (opts.clonePrimary && repo.url) {
35949
36841
  emit("git", `Cloning ${repo.url}\u2026`);
35950
- await run8("git", ["clone", repo.url, target], { timeout: 12e4 });
36842
+ await run9("git", ["clone", repo.url, target], { timeout: 12e4 });
35951
36843
  continue;
35952
36844
  }
35953
36845
  emit("git", "Initializing the primary repo\u2026");
35954
- await run8("git", ["init"], { cwd: target, timeout: 2e4 });
35955
- if (repo.op === "existing" && repo.url) {
35956
- await run8("git", ["remote", "add", "origin", repo.url], { cwd: target, timeout: 2e4 }).catch(
36846
+ await run9("git", ["init"], { cwd: target, timeout: 2e4 });
36847
+ if (repo.url) {
36848
+ await run9("git", ["remote", "add", "origin", repo.url], { cwd: target, timeout: 2e4 }).catch(
35957
36849
  () => void 0
35958
36850
  );
35959
36851
  }
35960
36852
  continue;
35961
36853
  }
35962
- const dir2 = join17(target, subDirName(repo));
35963
- if (repo.op === "existing" && repo.url) {
36854
+ const dir2 = join23(target, subDirName(repo));
36855
+ if (repo.url) {
35964
36856
  emit("git", `Cloning sub-repo ${repo.url}\u2026`);
35965
- await run8("git", ["clone", repo.url, dir2], { timeout: 12e4 });
36857
+ await run9("git", ["clone", repo.url, dir2], { timeout: 12e4 });
35966
36858
  } else {
35967
36859
  emit("git", `Initializing sub-repo ${subDirName(repo)}\u2026`);
35968
- await mkdir5(dir2, { recursive: true });
35969
- await run8("git", ["init"], { cwd: dir2, timeout: 2e4 });
36860
+ await mkdir8(dir2, { recursive: true });
36861
+ await run9("git", ["init"], { cwd: dir2, timeout: 2e4 });
35970
36862
  }
35971
36863
  }
35972
36864
  }
35973
36865
  function sampleDir() {
35974
36866
  if (process.env.SCAFFOLD_SAMPLE_DIR) return process.env.SCAFFOLD_SAMPLE_DIR;
35975
- const here = dirname6(fileURLToPath3(import.meta.url));
36867
+ const here = dirname8(fileURLToPath3(import.meta.url));
35976
36868
  return resolve7(here, "../../project-sample");
35977
36869
  }
35978
36870
  async function scaffoldProject(payload, profileDir2, onProgress) {
35979
36871
  const emit = (step, message) => onProgress?.({ projectId: payload.projectId, step, message });
35980
36872
  try {
35981
36873
  const root = process.env.SCAFFOLD_ROOT ? resolve7(process.env.SCAFFOLD_ROOT) : resolve7(profileDir2);
35982
- const target = join17(root, payload.projectName);
35983
- await mkdir5(target, { recursive: true });
36874
+ const target = join23(root, payload.projectName);
36875
+ await mkdir8(target, { recursive: true });
35984
36876
  emit("copy", "Copying the sample template\u2026");
35985
36877
  await cp(sampleDir(), target, { recursive: true, force: false, errorOnExist: false });
35986
36878
  if (payload.spec) {
35987
36879
  emit("spec", "Writing project.spec.json\u2026");
35988
- await writeFile6(
35989
- join17(target, "project.spec.json"),
36880
+ await writeFile8(
36881
+ join23(target, "project.spec.json"),
35990
36882
  JSON.stringify(payload.spec, null, 2),
35991
36883
  "utf8"
35992
36884
  );
@@ -36004,7 +36896,7 @@ async function aiInit(target, spec, emit) {
36004
36896
  emit("ai-init", "AI init: subagents, README, CLAUDE.md\u2026");
36005
36897
  const subagents = Array.isArray(spec.subagents) ? spec.subagents : [];
36006
36898
  if (subagents.length > 0) {
36007
- await mkdir5(join17(target, ".claude", "agents"), { recursive: true });
36899
+ await mkdir8(join23(target, ".claude", "agents"), { recursive: true });
36008
36900
  for (const sa of subagents) {
36009
36901
  const name = (sa.name || "").trim().replace(/[^\w.-]+/g, "-").replace(/^-+|-+$/g, "");
36010
36902
  if (!name) continue;
@@ -36014,17 +36906,17 @@ name: ${name}
36014
36906
 
36015
36907
  ${(sa.description || "").trim()}
36016
36908
  `;
36017
- await writeFile6(join17(target, ".claude", "agents", `${name}.md`), body, "utf8");
36909
+ await writeFile8(join23(target, ".claude", "agents", `${name}.md`), body, "utf8");
36018
36910
  }
36019
36911
  }
36020
36912
  const specJson = JSON.stringify(spec);
36021
36913
  await generateFile(
36022
- join17(target, "README.md"),
36914
+ join23(target, "README.md"),
36023
36915
  `Write a concise README.md (Markdown only, no preamble) for this project from its spec JSON:
36024
36916
  ${specJson}`
36025
36917
  );
36026
36918
  await generateFile(
36027
- join17(target, "CLAUDE.md"),
36919
+ join23(target, "CLAUDE.md"),
36028
36920
  `Write a CLAUDE.md (Markdown only, no preamble) with guidance/conventions for AI agents working in this project, derived from its spec JSON:
36029
36921
  ${specJson}`
36030
36922
  );
@@ -36032,7 +36924,7 @@ ${specJson}`
36032
36924
  async function generateFile(path, prompt) {
36033
36925
  try {
36034
36926
  const text = await aiGenerate(prompt);
36035
- if (text.trim()) await writeFile6(path, text.trim() + "\n", "utf8");
36927
+ if (text.trim()) await writeFile8(path, text.trim() + "\n", "utf8");
36036
36928
  } catch {
36037
36929
  }
36038
36930
  }
@@ -36040,8 +36932,8 @@ async function addProject(payload, profileDir2, onProgress) {
36040
36932
  const emit = (step, message) => onProgress?.({ projectId: payload.projectId, step, message });
36041
36933
  try {
36042
36934
  const root = process.env.SCAFFOLD_ROOT ? resolve7(process.env.SCAFFOLD_ROOT) : resolve7(profileDir2);
36043
- const target = join17(root, payload.projectName);
36044
- await mkdir5(target, { recursive: true });
36935
+ const target = join23(root, payload.projectName);
36936
+ await mkdir8(target, { recursive: true });
36045
36937
  const repos = payload.repos ?? [];
36046
36938
  if (repos.length > 0) await provisionRepos(target, repos, emit, { clonePrimary: true });
36047
36939
  onProgress?.({ projectId: payload.projectId, step: "done", message: "Project added.", done: true });
@@ -36052,14 +36944,14 @@ async function addProject(payload, profileDir2, onProgress) {
36052
36944
  }
36053
36945
 
36054
36946
  // src/core/config-sync.ts
36055
- import { existsSync as existsSync8, readFileSync as readFileSync13 } from "fs";
36056
- import { join as join18 } from "path";
36947
+ import { existsSync as existsSync9, readFileSync as readFileSync14 } from "fs";
36948
+ import { join as join24 } from "path";
36057
36949
  var SERVER_MANAGED_KEYS = ["physicPath", "sessionSwitchPct", "perPromptTokenLimit"];
36058
36950
  function readConfigText(profileDir2) {
36059
- const path = join18(profileDir2, "config.json");
36060
- if (existsSync8(path)) {
36951
+ const path = join24(profileDir2, "config.json");
36952
+ if (existsSync9(path)) {
36061
36953
  try {
36062
- return readFileSync13(path, "utf8");
36954
+ return readFileSync14(path, "utf8");
36063
36955
  } catch {
36064
36956
  }
36065
36957
  }
@@ -36089,6 +36981,7 @@ function applyConfigText(profileDir2, text) {
36089
36981
  var HEARTBEAT_MS = 3e4;
36090
36982
  var USAGE_POLL_MS = 5 * 6e4;
36091
36983
  var LOG_UPLOAD_MS = 10 * 6e4;
36984
+ var KB_REFRESH_MS = 24 * 60 * 6e4;
36092
36985
  var RECONNECT_BACKOFF_BASE_MS = 1e3;
36093
36986
  var RECONNECT_MAX_BACKOFF_DEFAULT_SEC = 60;
36094
36987
  var RECONNECT_BACKOFF_MAX_ATTEMPT = 6;
@@ -36135,11 +37028,17 @@ var WsClient = class {
36135
37028
  heartbeat = null;
36136
37029
  usageTimer = null;
36137
37030
  logUploadTimer = null;
37031
+ kbRefreshTimer = null;
36138
37032
  /** Signature (fileName + totalBytes) of the last log upload — skip re-sending if unchanged. */
36139
37033
  lastLogSig = null;
36140
37034
  usageSnapshot = null;
36141
37035
  /** Whether the serving project requires outbound review before each AI spawn (ADR-0082). */
36142
37036
  outboundReviewEnabled = false;
37037
+ /**
37038
+ * Whether the serving project restricts the AI to its worker folder (project aiScope) —
37039
+ * when true the cli prepends a folder-scope guard to every AI prompt. Fed by each ws_token.
37040
+ */
37041
+ restrictToFolder = false;
36143
37042
  stopped = false;
36144
37043
  /** Resolver to wake the backoff sleep early (set while waiting) — for /reconnect. */
36145
37044
  wakeReconnect = null;
@@ -36343,6 +37242,9 @@ var WsClient = class {
36343
37242
  perPromptTokenLimit: token.projectTokens?.perPromptTokenLimit ?? 0
36344
37243
  });
36345
37244
  this.outboundReviewEnabled = token.outboundReview?.enabled === true;
37245
+ this.restrictToFolder = token.aiRestrictToFolder === true;
37246
+ writeEgressAllowlist(this.context.profileDir, token.aiSandbox ?? null);
37247
+ applyGitAuth(token.gitAuth ?? null);
36346
37248
  this.machineUsername = token.machineUsername ?? "";
36347
37249
  if (token.scope && token.scope !== this.credential.scope) {
36348
37250
  this.credential.scope = token.scope;
@@ -36421,6 +37323,7 @@ var WsClient = class {
36421
37323
  this.startHeartbeat();
36422
37324
  this.startUsagePolling();
36423
37325
  this.startLogUpload();
37326
+ this.startKbRefresh();
36424
37327
  this.connectWaiters.splice(0).forEach((fn) => fn());
36425
37328
  return;
36426
37329
  }
@@ -36482,8 +37385,8 @@ var WsClient = class {
36482
37385
  const oldRoot = this.physicFolderPath(sync.oldName);
36483
37386
  if (oldRoot) void uninstallCron(oldRoot).catch(() => void 0);
36484
37387
  }
36485
- if (sync.oldName) rmSync4(join19(this.context.profileDir, sync.oldName), { recursive: true, force: true });
36486
- mkdirSync4(join19(this.context.profileDir, sync.newName), { recursive: true });
37388
+ if (sync.oldName) rmSync5(join25(this.context.profileDir, sync.oldName), { recursive: true, force: true });
37389
+ mkdirSync5(join25(this.context.profileDir, sync.newName), { recursive: true });
36487
37390
  this.physicRoot = this.physicFolderPath(sync.newName);
36488
37391
  this.bus.setProject(sync.newName);
36489
37392
  this.bus.log(`Physic project folder synced \u2192 ${sync.newName}`);
@@ -36494,9 +37397,10 @@ var WsClient = class {
36494
37397
  if (del.name) {
36495
37398
  const delRoot = this.physicFolderPath(del.name);
36496
37399
  if (delRoot) void uninstallCron(delRoot).catch(() => void 0);
36497
- rmSync4(join19(this.context.profileDir, del.name), { recursive: true, force: true });
37400
+ rmSync5(join25(this.context.profileDir, del.name), { recursive: true, force: true });
36498
37401
  this.bus.log(`Physic project folder deleted (project removed) \u2192 ${del.name}`);
36499
37402
  }
37403
+ void manageSshKey("delete", this.context.profileDir).catch(() => void 0);
36500
37404
  this.bus.setProject(null);
36501
37405
  this.physicRoot = null;
36502
37406
  break;
@@ -36566,6 +37470,61 @@ var WsClient = class {
36566
37470
  );
36567
37471
  break;
36568
37472
  }
37473
+ case WsChannels.SUPPORT_ANSWER: {
37474
+ const req = payload;
37475
+ const supportConfig = readProfileConfig(this.context.profileDir);
37476
+ const supportCmd = supportConfig.aiCli || "claude";
37477
+ const supportAi = {
37478
+ cmd: supportCmd,
37479
+ profiles: resolveClaudeProfiles(
37480
+ supportConfig,
37481
+ getWorkingProfile(this.context.profileDir, supportCmd)
37482
+ ),
37483
+ env: supportConfig.aiEnv
37484
+ };
37485
+ this.bus.push({ source: "server", kind: "aireq", text: `${supportCmd} \u2039 ${req.question}` });
37486
+ this.bus.startBusy("support-answer");
37487
+ void runSupportAnswer(req, supportAi, this.context.profileDir).then((reply) => {
37488
+ if (reply.error) {
37489
+ this.bus.push({
37490
+ source: "server",
37491
+ kind: "log",
37492
+ text: `support-answer failed: ${reply.error}`,
37493
+ level: "warn"
37494
+ });
37495
+ } else {
37496
+ this.bus.push({ source: "server", kind: "aires", text: `${supportCmd} \u203A` });
37497
+ this.bus.push({ source: "server", kind: "out", text: reply.body });
37498
+ }
37499
+ this.send(WsChannels.SUPPORT_ANSWER, reply, message.id);
37500
+ }).finally(() => this.bus.endBusy("support-answer"));
37501
+ break;
37502
+ }
37503
+ case WsChannels.KNOWLEDGE_COMPOSE: {
37504
+ const kreq = payload;
37505
+ if (!this.physicRoot) {
37506
+ this.send(WsChannels.KNOWLEDGE_COMPOSE, { bodyMarkdown: "", error: "no project" }, message.id);
37507
+ break;
37508
+ }
37509
+ const kcfg = readProfileConfig(this.context.profileDir);
37510
+ const kcmd = kcfg.aiCli || "claude";
37511
+ const kai = {
37512
+ cmd: kcmd,
37513
+ profiles: resolveClaudeProfiles(kcfg, getWorkingProfile(this.context.profileDir, kcmd)),
37514
+ env: kcfg.aiEnv
37515
+ };
37516
+ this.bus.push({ source: "server", kind: "aireq", text: `${kcmd} \u2039 distil knowledge` });
37517
+ this.bus.startBusy("knowledge-compose");
37518
+ void runKnowledgeCompose(kreq, kai, this.physicRoot).then((reply) => {
37519
+ if (reply.error) {
37520
+ this.bus.push({ source: "server", kind: "log", text: `knowledge-compose failed: ${reply.error}`, level: "warn" });
37521
+ } else {
37522
+ this.bus.push({ source: "server", kind: "aires", text: `${kcmd} \u203A` });
37523
+ }
37524
+ this.send(WsChannels.KNOWLEDGE_COMPOSE, reply, message.id);
37525
+ }).finally(() => this.bus.endBusy("knowledge-compose"));
37526
+ break;
37527
+ }
36569
37528
  case WsChannels.AUTONOMOUS_READ:
36570
37529
  if (this.physicRoot) {
36571
37530
  void readAutonomous(this.physicRoot).then(
@@ -36632,6 +37591,58 @@ var WsClient = class {
36632
37591
  }
36633
37592
  break;
36634
37593
  }
37594
+ case WsChannels.AGENT_TOOLS_READ: {
37595
+ const req = payload;
37596
+ if (this.physicRoot) {
37597
+ void readAgentTools(this.physicRoot, req.scope).then(
37598
+ (reply) => this.send(WsChannels.AGENT_TOOLS_READ, reply, message.id)
37599
+ );
37600
+ }
37601
+ break;
37602
+ }
37603
+ case WsChannels.AGENT_TOOLS_WRITE: {
37604
+ const req = payload;
37605
+ if (this.physicRoot) {
37606
+ void writeAgentTools(this.physicRoot, req.scope, req.permissions).then(
37607
+ (reply) => this.send(WsChannels.AGENT_TOOLS_WRITE, reply, message.id)
37608
+ );
37609
+ }
37610
+ break;
37611
+ }
37612
+ case WsChannels.PACKAGES_PACK: {
37613
+ const req = payload;
37614
+ if (this.physicRoot) {
37615
+ void packPackage(this.physicRoot, req.kind, req.name).then(
37616
+ (reply) => this.send(WsChannels.PACKAGES_PACK, reply, message.id)
37617
+ );
37618
+ }
37619
+ break;
37620
+ }
37621
+ case WsChannels.PACKAGES_INSTALL: {
37622
+ const req = payload;
37623
+ if (this.physicRoot) {
37624
+ void installPackage(this.physicRoot, req).then(
37625
+ (reply) => this.send(WsChannels.PACKAGES_INSTALL, reply, message.id)
37626
+ );
37627
+ }
37628
+ break;
37629
+ }
37630
+ case WsChannels.PACKAGES_LIST:
37631
+ if (this.physicRoot) {
37632
+ void listPackages(this.physicRoot).then(
37633
+ (reply) => this.send(WsChannels.PACKAGES_LIST, reply, message.id)
37634
+ );
37635
+ }
37636
+ break;
37637
+ case WsChannels.PACKAGES_REMOVE: {
37638
+ const req = payload;
37639
+ if (this.physicRoot) {
37640
+ void removePackage(this.physicRoot, req.slug).then(
37641
+ (reply) => this.send(WsChannels.PACKAGES_REMOVE, reply, message.id)
37642
+ );
37643
+ }
37644
+ break;
37645
+ }
36635
37646
  case WsChannels.GRAPH_BUILD: {
36636
37647
  const req = payload;
36637
37648
  if (this.physicRoot) {
@@ -36703,6 +37714,13 @@ var WsClient = class {
36703
37714
  (reply) => this.send(WsChannels.GIT_REPOS, reply, message.id)
36704
37715
  );
36705
37716
  break;
37717
+ case WsChannels.GIT_SSH_KEY: {
37718
+ const req = payload;
37719
+ void manageSshKey(req.op, this.context.profileDir).then(
37720
+ (reply) => this.send(WsChannels.GIT_SSH_KEY, reply, message.id)
37721
+ );
37722
+ break;
37723
+ }
36706
37724
  case WsChannels.GIT_LOG: {
36707
37725
  const req = payload;
36708
37726
  void gitLog(this.physicRoot, req.repo ?? "", req.skip ?? 0, req.limit ?? 50).then(
@@ -36726,7 +37744,7 @@ var WsClient = class {
36726
37744
  }
36727
37745
  case WsChannels.LOG_READ: {
36728
37746
  const limit = Math.min(Math.max(payload.limit ?? 200, 1), 2e3);
36729
- const lines = readRecentLogLines(join19(this.context.profileDir, "logs"), limit);
37747
+ const lines = readRecentLogLines(join25(this.context.profileDir, "logs"), limit);
36730
37748
  this.send(WsChannels.LOG_READ, { lines }, message.id);
36731
37749
  break;
36732
37750
  }
@@ -36829,9 +37847,13 @@ var WsClient = class {
36829
37847
  } else {
36830
37848
  this.bus.log("No project attached \u2014 running in the current directory.", "warn");
36831
37849
  }
36832
- const cmd = config.aiCli ?? "claude";
36833
- const workingDir = getWorkingProfile(this.context.profileDir, cmd);
36834
- const startDir = this.profileUnderSessionPressure(config, cmd, workingDir);
37850
+ const cmd = config.aiCli || "claude";
37851
+ const unified = isUnifiedConfig(config);
37852
+ const mode = config.aiFailoverMode ?? "remember";
37853
+ const workingDir = unified ? null : getWorkingProfile(this.context.profileDir, cmd);
37854
+ const workingCred = unified && mode === "remember" ? getWorkingCredential(this.context.profileDir) : null;
37855
+ const startDir = unified ? null : this.profileUnderSessionPressure(config, cmd, workingDir);
37856
+ const startCred = unified ? this.credentialUnderSessionPressure(config, workingCred) : null;
36835
37857
  if (startDir && startDir !== workingDir) {
36836
37858
  this.bus.push({
36837
37859
  source: origin,
@@ -36839,7 +37861,19 @@ var WsClient = class {
36839
37861
  text: `session ${this.usageSnapshot?.session.utilizationPct ?? 0}% \u2265 ${config.sessionSwitchPct}% \u2014 switching to Claude profile "${profileDisplayLabel(startDir)}"`
36840
37862
  });
36841
37863
  }
36842
- const plan = planAiRun(prompt, config, startDir ?? workingDir);
37864
+ if (startCred && startCred !== workingCred) {
37865
+ this.bus.push({
37866
+ source: origin,
37867
+ kind: "log",
37868
+ text: `session ${this.usageSnapshot?.session.utilizationPct ?? 0}% \u2265 ${config.sessionSwitchPct}% \u2014 switching to Claude profile "${labelFromCredentialKey(startCred)}"`
37869
+ });
37870
+ }
37871
+ const effectivePrompt = this.restrictToFolder && this.physicRoot ? folderScopeGuard(this.physicRoot, prompt) : prompt;
37872
+ const plan = planAiRun(
37873
+ effectivePrompt,
37874
+ config,
37875
+ unified ? { credential: startCred ?? workingCred } : { dir: startDir ?? workingDir }
37876
+ );
36843
37877
  const markerArgs = plan.attempts[0]?.args ?? [];
36844
37878
  this.bus.push({ source: origin, kind: "aireq", text: `${plan.cmd} \u2039 ${prompt}` });
36845
37879
  logger.info("command.ai", { commandId, origin, cmd: plan.cmd, profiles: plan.attempts.length });
@@ -36905,7 +37939,11 @@ var WsClient = class {
36905
37939
  this.bus.endBusy(plan.cmd);
36906
37940
  const profileLabel = result.workedDir ? profileDisplayLabel(result.workedDir) : null;
36907
37941
  if (result.workedDir) {
36908
- setWorkingProfile(this.context.profileDir, plan.cmd, result.workedDir);
37942
+ if (unified && result.workedKey) {
37943
+ setWorkingCredential(this.context.profileDir, result.workedKey);
37944
+ } else {
37945
+ setWorkingProfile(this.context.profileDir, result.workedCmd ?? plan.cmd, result.workedDir);
37946
+ }
36909
37947
  this.bus.setActiveProfile(profileLabel ?? plan.cmd);
36910
37948
  }
36911
37949
  this.send(WsChannels.COMMAND_OUTPUT, {
@@ -36948,7 +37986,9 @@ var WsClient = class {
36948
37986
  inputTokens: result.usage.input,
36949
37987
  outputTokens: result.usage.output,
36950
37988
  cacheReadTokens: result.usage.cacheRead,
36951
- cacheCreationTokens: result.usage.cacheCreation
37989
+ cacheCreationTokens: result.usage.cacheCreation,
37990
+ // Auth mode for billing split (ADR-0192 §5): subscription (OAuth) vs api-key (API-billed).
37991
+ authMode: resolveClaudeAuthMode(config)
36952
37992
  });
36953
37993
  this.bus.addTokens?.(result.usage.tokens);
36954
37994
  }
@@ -36979,6 +38019,22 @@ var WsClient = class {
36979
38019
  const next = dirs[(currentIndex + 1) % dirs.length];
36980
38020
  return next ?? null;
36981
38021
  }
38022
+ /**
38023
+ * Unified-list equivalent of {@link profileUnderSessionPressure} (ADR-0182): under session
38024
+ * pressure, return the next **claude** credential key (cyclically after the working one) so the
38025
+ * failover starts on a fresher Claude account. Claude-only — %session is a Claude subscription
38026
+ * metric; codex/antigravity entries are unaffected. Null ⇒ keep the working-first order.
38027
+ */
38028
+ credentialUnderSessionPressure(config, workingCred) {
38029
+ const threshold = config.sessionSwitchPct ?? 0;
38030
+ if (threshold <= 0) return null;
38031
+ const util2 = this.usageSnapshot?.session.utilizationPct ?? 0;
38032
+ if (util2 < threshold) return null;
38033
+ const keys = claudeCredentialKeys(config);
38034
+ if (keys.length < 2) return null;
38035
+ const currentIndex = workingCred ? keys.indexOf(workingCred) : -1;
38036
+ return keys[(currentIndex + 1) % keys.length] ?? null;
38037
+ }
36982
38038
  /**
36983
38039
  * Reject a prompt whose estimated tokens exceed the project's per-prompt limit (ADR-0081)
36984
38040
  * without spawning: surface it in the transcript + close the command on the server with a
@@ -37222,8 +38278,8 @@ var WsClient = class {
37222
38278
  /** Create the physic folder at an absolute path if missing (best-effort). */
37223
38279
  ensurePhysicFolderPath(folder) {
37224
38280
  try {
37225
- if (!existsSync9(folder)) {
37226
- mkdirSync4(folder, { recursive: true });
38281
+ if (!existsSync10(folder)) {
38282
+ mkdirSync5(folder, { recursive: true });
37227
38283
  this.bus.log(`Created physic project folder: ${folder}`);
37228
38284
  }
37229
38285
  } catch (err) {
@@ -37237,7 +38293,7 @@ var WsClient = class {
37237
38293
  */
37238
38294
  physicFolderPath(projectName) {
37239
38295
  const safe = projectName.replace(/[/\\]/g, "_").replace(/\.\./g, "_").trim();
37240
- return safe ? join19(this.context.profileDir, safe) : null;
38296
+ return safe ? join25(this.context.profileDir, safe) : null;
37241
38297
  }
37242
38298
  /**
37243
38299
  * Report machine.status: version + the state of the physic project being served
@@ -37251,7 +38307,7 @@ var WsClient = class {
37251
38307
  projects: [],
37252
38308
  version: CLI_VERSION,
37253
38309
  physicPath,
37254
- physicPathExists: physicPath ? existsSync9(physicPath) : void 0,
38310
+ physicPathExists: physicPath ? existsSync10(physicPath) : void 0,
37255
38311
  // Real coarse state (ADR-0152): !paused and a recent tick (was hard-coded false).
37256
38312
  autonomousRunning: physicPath ? isAutonomousRunning(physicPath) : false
37257
38313
  });
@@ -37272,6 +38328,8 @@ var WsClient = class {
37272
38328
  this.usageTimer = null;
37273
38329
  if (this.logUploadTimer) clearInterval(this.logUploadTimer);
37274
38330
  this.logUploadTimer = null;
38331
+ if (this.kbRefreshTimer) clearInterval(this.kbRefreshTimer);
38332
+ this.kbRefreshTimer = null;
37275
38333
  }
37276
38334
  /**
37277
38335
  * Poll the Claude subscription usage API and push a `machine.usage` snapshot (ADR-0072).
@@ -37282,7 +38340,7 @@ var WsClient = class {
37282
38340
  if (!this.sessionKey) return;
37283
38341
  try {
37284
38342
  const config = readProfileConfig(this.context.profileDir);
37285
- const aiCli2 = config.aiCli ?? "claude";
38343
+ const aiCli2 = config.aiCli || "claude";
37286
38344
  const working = getWorkingProfile(this.context.profileDir, aiCli2);
37287
38345
  const configured = claudeHomeDirs(config);
37288
38346
  const dirs = working ? [working, ...configured.filter((d) => d !== working)] : configured;
@@ -37316,6 +38374,20 @@ var WsClient = class {
37316
38374
  this.logUploadTimer.unref?.();
37317
38375
  this.uploadLog();
37318
38376
  }
38377
+ /**
38378
+ * Start the daily FAQ/KB refresh for a support agent (ADR-0170): pull each KB clone already
38379
+ * present in this profile so answers stay current without fetching on each dispatch. A no-op
38380
+ * for a normal project cli (no `support-kb` dir), so it is safe to start on every session.
38381
+ */
38382
+ startKbRefresh() {
38383
+ if (this.kbRefreshTimer) clearInterval(this.kbRefreshTimer);
38384
+ this.kbRefreshTimer = setInterval(
38385
+ () => void refreshSupportKb(this.context.profileDir),
38386
+ KB_REFRESH_MS
38387
+ );
38388
+ this.kbRefreshTimer.unref?.();
38389
+ void refreshSupportKb(this.context.profileDir);
38390
+ }
37319
38391
  /**
37320
38392
  * Upload the newest cli log file + the total log footprint to the server (ADR-0122), so it is
37321
38393
  * stored + retained server-side and counted in the machine user's storage. Skips when nothing
@@ -37323,7 +38395,7 @@ var WsClient = class {
37323
38395
  */
37324
38396
  uploadLog() {
37325
38397
  try {
37326
- const upload = readLogUpload(join19(this.context.profileDir, "logs"));
38398
+ const upload = readLogUpload(join25(this.context.profileDir, "logs"));
37327
38399
  if (!upload) return;
37328
38400
  const sig = `${upload.fileName}:${upload.totalBytes}`;
37329
38401
  if (sig === this.lastLogSig) return;
@@ -37351,10 +38423,10 @@ var WsClient = class {
37351
38423
  };
37352
38424
 
37353
38425
  // src/core/instance-lock.ts
37354
- import { readFileSync as readFileSync14, rmSync as rmSync5, writeFileSync as writeFileSync6 } from "fs";
37355
- import { join as join20 } from "path";
38426
+ import { readFileSync as readFileSync15, rmSync as rmSync6, writeFileSync as writeFileSync8 } from "fs";
38427
+ import { join as join26 } from "path";
37356
38428
  function lockPath(profileDir2) {
37357
- return join20(profileDir2, "start.lock");
38429
+ return join26(profileDir2, "start.lock");
37358
38430
  }
37359
38431
  function isAlive(pid) {
37360
38432
  try {
@@ -37366,7 +38438,7 @@ function isAlive(pid) {
37366
38438
  }
37367
38439
  function lockHolder(profileDir2) {
37368
38440
  try {
37369
- const pid = Number.parseInt(readFileSync14(lockPath(profileDir2), "utf8").trim(), 10);
38441
+ const pid = Number.parseInt(readFileSync15(lockPath(profileDir2), "utf8").trim(), 10);
37370
38442
  if (Number.isInteger(pid) && pid > 0 && isAlive(pid)) return pid;
37371
38443
  } catch {
37372
38444
  }
@@ -37374,13 +38446,13 @@ function lockHolder(profileDir2) {
37374
38446
  }
37375
38447
  function acquireInstanceLock(profileDir2) {
37376
38448
  if (lockHolder(profileDir2) !== null) return false;
37377
- writeFileSync6(lockPath(profileDir2), String(process.pid), "utf8");
38449
+ writeFileSync8(lockPath(profileDir2), String(process.pid), "utf8");
37378
38450
  return true;
37379
38451
  }
37380
38452
  function releaseInstanceLock(profileDir2) {
37381
38453
  try {
37382
- const pid = Number.parseInt(readFileSync14(lockPath(profileDir2), "utf8").trim(), 10);
37383
- if (pid === process.pid) rmSync5(lockPath(profileDir2), { force: true });
38454
+ const pid = Number.parseInt(readFileSync15(lockPath(profileDir2), "utf8").trim(), 10);
38455
+ if (pid === process.pid) rmSync6(lockPath(profileDir2), { force: true });
37384
38456
  } catch {
37385
38457
  }
37386
38458
  }
@@ -37467,6 +38539,16 @@ var SessionBus = class {
37467
38539
  log(text, level = "info") {
37468
38540
  this.push({ source: "system", kind: "log", text, level });
37469
38541
  }
38542
+ /**
38543
+ * Insert a fully-formed transcript entry (preserving its id/ts) — used by the attach client
38544
+ * (ADR-0192 §2) to mirror a daemon's transcript over the control channel so `updateEntry` can
38545
+ * still target the same id. `push` is for producers minting a new entry; this is for replay.
38546
+ */
38547
+ pushEntry(entry) {
38548
+ this.history.push(entry);
38549
+ if (this.history.length > REPLAY_CAP) this.history.shift();
38550
+ this.emitter.emit("transcript", entry);
38551
+ }
37470
38552
  /** Producer → consumers: update the connection status. */
37471
38553
  setStatus(status) {
37472
38554
  this.currentStatus = status;
@@ -37492,6 +38574,15 @@ var SessionBus = class {
37492
38574
  else this.busyStack.pop();
37493
38575
  this.emitter.emit("busy", this.busy);
37494
38576
  }
38577
+ /**
38578
+ * Mirror the current busy label directly (attach client — ADR-0192 §2): replaces the stack with
38579
+ * `[label]` (or empties it) and notifies. For a real session use `startBusy`/`endBusy` instead.
38580
+ */
38581
+ setBusy(label) {
38582
+ this.busyStack.length = 0;
38583
+ if (label) this.busyStack.push(label);
38584
+ this.emitter.emit("busy", this.busy);
38585
+ }
37495
38586
  /** Subscribe to processing-state changes. */
37496
38587
  onBusy(fn) {
37497
38588
  this.emitter.on("busy", fn);
@@ -37574,6 +38665,11 @@ var SessionBus = class {
37574
38665
  this.currentSessionTokens += n;
37575
38666
  this.emitter.emit("tokens", this.currentSessionTokens);
37576
38667
  }
38668
+ /** Set the session token total directly (attach client mirror — ADR-0192 §2). */
38669
+ setTokens(total) {
38670
+ this.currentSessionTokens = total;
38671
+ this.emitter.emit("tokens", this.currentSessionTokens);
38672
+ }
37577
38673
  /** Subscribe to session-token changes. */
37578
38674
  onTokens(fn) {
37579
38675
  this.emitter.on("tokens", fn);
@@ -37615,6 +38711,112 @@ var SessionBus = class {
37615
38711
  }
37616
38712
  };
37617
38713
 
38714
+ // src/core/control-server.ts
38715
+ import { createServer } from "net";
38716
+ import { rmSync as rmSync7 } from "fs";
38717
+ import { join as join27 } from "path";
38718
+
38719
+ // src/core/control-protocol.ts
38720
+ var CONTROL_SOCKET_FILE = "control.sock";
38721
+ function encodeFrame(frame) {
38722
+ return `${JSON.stringify(frame)}
38723
+ `;
38724
+ }
38725
+ function createFrameParser() {
38726
+ let buffer = "";
38727
+ return (chunk) => {
38728
+ buffer += chunk;
38729
+ const lines = buffer.split("\n");
38730
+ buffer = lines.pop() ?? "";
38731
+ const out = [];
38732
+ for (const line of lines) {
38733
+ const s = line.trim();
38734
+ if (!s) continue;
38735
+ try {
38736
+ out.push(JSON.parse(s));
38737
+ } catch {
38738
+ }
38739
+ }
38740
+ return out;
38741
+ };
38742
+ }
38743
+
38744
+ // src/core/control-server.ts
38745
+ function startControlServer(bus, profileDir2, info) {
38746
+ const socketPath = join27(profileDir2, CONTROL_SOCKET_FILE);
38747
+ try {
38748
+ rmSync7(socketPath, { force: true });
38749
+ } catch {
38750
+ }
38751
+ const clients = /* @__PURE__ */ new Set();
38752
+ const send = (sock, frame) => {
38753
+ try {
38754
+ sock.write(encodeFrame(frame));
38755
+ } catch {
38756
+ }
38757
+ };
38758
+ const broadcast = (frame) => {
38759
+ for (const sock of clients) send(sock, frame);
38760
+ };
38761
+ const unsubs = [
38762
+ bus.onTranscript((entry) => broadcast({ t: "transcript", entry })),
38763
+ bus.onTranscriptUpdate(
38764
+ (entry) => broadcast({ t: "update", id: entry.id, text: entry.text, level: entry.level })
38765
+ ),
38766
+ bus.onClear(() => broadcast({ t: "clear" })),
38767
+ bus.onStatus((status) => broadcast({ t: "status", status })),
38768
+ bus.onBusy((label) => broadcast({ t: "busy", label })),
38769
+ bus.onActiveProfile((label) => broadcast({ t: "activeProfile", label })),
38770
+ bus.onScope((scope) => broadcast({ t: "scope", scope })),
38771
+ bus.onWorker((worker) => broadcast({ t: "worker", worker })),
38772
+ bus.onProject((project) => broadcast({ t: "project", project })),
38773
+ bus.onUsage((usage) => broadcast({ t: "usage", usage })),
38774
+ bus.onTokens((total) => broadcast({ t: "tokens", total }))
38775
+ ];
38776
+ const server = createServer((sock) => {
38777
+ clients.add(sock);
38778
+ send(sock, {
38779
+ t: "snapshot",
38780
+ info,
38781
+ status: bus.status,
38782
+ busy: bus.busy,
38783
+ activeProfile: bus.activeProfile,
38784
+ scope: bus.scope,
38785
+ worker: bus.worker,
38786
+ project: bus.project,
38787
+ usage: bus.usage,
38788
+ tokens: bus.sessionTokens,
38789
+ transcript: bus.snapshot()
38790
+ });
38791
+ const parse = createFrameParser();
38792
+ sock.setEncoding("utf8");
38793
+ sock.on("data", (chunk) => {
38794
+ for (const frame of parse(chunk)) {
38795
+ if (frame.t === "submit") bus.submitLocal(frame.input);
38796
+ else if (frame.t === "reconnect") bus.requestReconnect();
38797
+ }
38798
+ });
38799
+ const drop = () => {
38800
+ clients.delete(sock);
38801
+ };
38802
+ sock.on("close", drop);
38803
+ sock.on("error", drop);
38804
+ });
38805
+ server.on("error", () => {
38806
+ });
38807
+ server.listen(socketPath);
38808
+ return () => {
38809
+ for (const un of unsubs) un();
38810
+ for (const sock of clients) sock.destroy();
38811
+ clients.clear();
38812
+ server.close();
38813
+ try {
38814
+ rmSync7(socketPath, { force: true });
38815
+ } catch {
38816
+ }
38817
+ };
38818
+ }
38819
+
37618
38820
  // src/ui/console-sink.ts
37619
38821
  function attachConsoleSink(bus) {
37620
38822
  bus.onTranscript((entry) => {
@@ -43250,17 +44452,17 @@ var import_react21 = __toESM(require_react(), 1);
43250
44452
  var import_react26 = __toESM(require_react(), 1);
43251
44453
 
43252
44454
  // src/core/input-history.ts
43253
- import { existsSync as existsSync11, readFileSync as readFileSync16, writeFileSync as writeFileSync7 } from "fs";
43254
- import { join as join21 } from "path";
44455
+ import { existsSync as existsSync12, readFileSync as readFileSync17, writeFileSync as writeFileSync9 } from "fs";
44456
+ import { join as join28 } from "path";
43255
44457
  var MAX_HISTORY = 200;
43256
44458
  function historyPath(profileDir2) {
43257
- return join21(profileDir2, "input-history.json");
44459
+ return join28(profileDir2, "input-history.json");
43258
44460
  }
43259
44461
  function loadInputHistory(profileDir2) {
43260
44462
  const path = historyPath(profileDir2);
43261
- if (!existsSync11(path)) return [];
44463
+ if (!existsSync12(path)) return [];
43262
44464
  try {
43263
- const data = JSON.parse(readFileSync16(path, "utf8"));
44465
+ const data = JSON.parse(readFileSync17(path, "utf8"));
43264
44466
  return Array.isArray(data) ? data.filter((x) => typeof x === "string") : [];
43265
44467
  } catch {
43266
44468
  return [];
@@ -43271,7 +44473,7 @@ function appendInputHistory(profileDir2, line, current) {
43271
44473
  if (!trimmed || current[current.length - 1] === trimmed) return current;
43272
44474
  const next = [...current, trimmed].slice(-MAX_HISTORY);
43273
44475
  try {
43274
- writeFileSync7(historyPath(profileDir2), JSON.stringify(next), "utf8");
44476
+ writeFileSync9(historyPath(profileDir2), JSON.stringify(next), "utf8");
43275
44477
  } catch {
43276
44478
  }
43277
44479
  return next;
@@ -43520,8 +44722,8 @@ function TranscriptLine({ line }) {
43520
44722
  var import_react24 = __toESM(require_react(), 1);
43521
44723
 
43522
44724
  // src/ui/slash-commands.ts
43523
- import { existsSync as existsSync12 } from "fs";
43524
- import { join as join22 } from "path";
44725
+ import { existsSync as existsSync13 } from "fs";
44726
+ import { join as join29 } from "path";
43525
44727
  var CONFIG_KEYS = [
43526
44728
  "aiCli",
43527
44729
  "claudeHome",
@@ -43533,7 +44735,7 @@ var CONFIG_KEYS = [
43533
44735
  "aiEnv"
43534
44736
  ];
43535
44737
  function configPath(profileDir2) {
43536
- return join22(profileDir2, "config.json");
44738
+ return join29(profileDir2, "config.json");
43537
44739
  }
43538
44740
  function parseConfigAssignment(key, valueTokens) {
43539
44741
  const value = valueTokens.join(" ").trim();
@@ -43583,7 +44785,7 @@ function runConfig(ctx) {
43583
44785
  ctx.print(`\u2714 ${verb} default config: ${path}`);
43584
44786
  ctx.print("add claude profiles: /config set claudeHome .claude .claude-1 (tried in order)");
43585
44787
  };
43586
- if (existsSync12(path)) {
44788
+ if (existsSync13(path)) {
43587
44789
  ctx.confirm(
43588
44790
  `config exists: ${path} \u2014 replace it with defaults? (y/N)`,
43589
44791
  () => writeDefaults("replaced with")
@@ -43668,7 +44870,7 @@ function runOutput(ctx) {
43668
44870
  }
43669
44871
  function runLogs(ctx) {
43670
44872
  const n = Math.max(1, Math.min(200, Number(ctx.args[0]) || 20));
43671
- const dir2 = join22(ctx.info.profileDir, "logs");
44873
+ const dir2 = join29(ctx.info.profileDir, "logs");
43672
44874
  const lines = readRecentLogLines(dir2, n);
43673
44875
  if (lines.length === 0) {
43674
44876
  ctx.print("No log lines yet.");
@@ -43946,7 +45148,7 @@ var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u282
43946
45148
  function Spinner() {
43947
45149
  const [index, setIndex] = (0, import_react25.useState)(0);
43948
45150
  (0, import_react25.useEffect)(() => {
43949
- const timer = setInterval(() => setIndex((i) => (i + 1) % FRAMES.length), 90);
45151
+ const timer = setInterval(() => setIndex((i) => (i + 1) % FRAMES.length), 120);
43950
45152
  return () => clearInterval(timer);
43951
45153
  }, []);
43952
45154
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Text, { color: "yellow", children: FRAMES[index] });
@@ -43956,6 +45158,7 @@ function Spinner() {
43956
45158
  var import_jsx_runtime5 = __toESM(require_jsx_runtime(), 1);
43957
45159
  var CHROME_ROWS = 12;
43958
45160
  var ENTRY_CAP = 3e3;
45161
+ var UPDATE_FLUSH_MS = 66;
43959
45162
  function Rule({ width }) {
43960
45163
  return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Text, { dimColor: true, children: "\u2500".repeat(Math.max(10, width)) });
43961
45164
  }
@@ -44003,6 +45206,16 @@ function App2({
44003
45206
  entriesLenRef.current = entries2.length;
44004
45207
  const pendingConfirmRef = (0, import_react26.useRef)(pendingConfirm);
44005
45208
  pendingConfirmRef.current = pendingConfirm;
45209
+ const pendingUpdatesRef = (0, import_react26.useRef)(/* @__PURE__ */ new Map());
45210
+ const flushTimerRef = (0, import_react26.useRef)(null);
45211
+ const flushUpdates = (0, import_react26.useCallback)(() => {
45212
+ flushTimerRef.current = null;
45213
+ const pending = pendingUpdatesRef.current;
45214
+ if (pending.size === 0) return;
45215
+ const snapshot = new Map(pending);
45216
+ pending.clear();
45217
+ setEntries((prev) => prev.map((e) => snapshot.has(e.id) ? { ...e, text: snapshot.get(e.id) } : e));
45218
+ }, []);
44006
45219
  const clearTranscript = (0, import_react26.useCallback)(() => {
44007
45220
  setEntries([]);
44008
45221
  setScrollBack(0);
@@ -44017,9 +45230,12 @@ function App2({
44017
45230
  if (entry.source !== "system") lastActivityRef.current = Date.now();
44018
45231
  if (entry.kind === "aireq" || entry.kind === "aires") setExpanded(/* @__PURE__ */ new Set());
44019
45232
  });
44020
- const offUpdate = bus.onTranscriptUpdate(
44021
- (updated) => setEntries((prev) => prev.map((e) => e.id === updated.id ? { ...e, text: updated.text } : e))
44022
- );
45233
+ const offUpdate = bus.onTranscriptUpdate((updated) => {
45234
+ pendingUpdatesRef.current.set(updated.id, updated.text);
45235
+ if (flushTimerRef.current === null) {
45236
+ flushTimerRef.current = setTimeout(flushUpdates, UPDATE_FLUSH_MS);
45237
+ }
45238
+ });
44023
45239
  const offStatus = bus.onStatus(setStatus);
44024
45240
  const offBusy = bus.onBusy(setBusy);
44025
45241
  const offProfile = bus.onActiveProfile(setActiveProfile);
@@ -44039,8 +45255,13 @@ function App2({
44039
45255
  offProject();
44040
45256
  offUsage();
44041
45257
  offTokens();
45258
+ if (flushTimerRef.current !== null) {
45259
+ clearTimeout(flushTimerRef.current);
45260
+ flushTimerRef.current = null;
45261
+ }
45262
+ flushUpdates();
44042
45263
  };
44043
- }, [bus]);
45264
+ }, [bus, flushUpdates]);
44044
45265
  (0, import_react26.useEffect)(() => {
44045
45266
  if (autoClearIdleMs <= 0) return;
44046
45267
  const timer = setInterval(() => {
@@ -44056,7 +45277,8 @@ function App2({
44056
45277
  }, 6e4);
44057
45278
  return () => clearInterval(timer);
44058
45279
  }, [autoClearIdleMs, bus, clearTranscript]);
44059
- const bodyHeight = Math.max(3, rows - CHROME_ROWS);
45280
+ const frameRows = Math.max(4, rows - 1);
45281
+ const bodyHeight = Math.max(3, frameRows - CHROME_ROWS);
44060
45282
  const textWidth = Math.max(10, cols - 1 - 8);
44061
45283
  const lines = (0, import_react26.useMemo)(() => flattenEntries(entries2, textWidth, expanded), [entries2, textWidth, expanded]);
44062
45284
  const maxBlock = (0, import_react26.useMemo)(() => lines.reduce((m, l) => Math.max(m, l.blockIndex ?? 0), 0), [lines]);
@@ -44124,7 +45346,7 @@ function App2({
44124
45346
  if (key.pageUp) setScrollBack((b) => Math.min(maxScroll, b + bodyHeight));
44125
45347
  else if (key.pageDown) setScrollBack((b) => Math.max(0, b - bodyHeight));
44126
45348
  });
44127
- return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(Box_default, { flexDirection: "column", height: rows, children: [
45349
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(Box_default, { flexDirection: "column", height: frameRows, children: [
44128
45350
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Banner, { info, scope, worker, project, status, activeProfile, usage, sessionTokens }),
44129
45351
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Rule, { width }),
44130
45352
  /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(Box_default, { flexDirection: "column", height: bodyHeight, overflow: "hidden", children: [
@@ -44169,7 +45391,7 @@ async function runStart(profileDir2, profileName) {
44169
45391
  return;
44170
45392
  }
44171
45393
  logger.configure({
44172
- dir: join23(profileDir2, "logs"),
45394
+ dir: join30(profileDir2, "logs"),
44173
45395
  level: resolveLogLevel(),
44174
45396
  base: { version: CLI_VERSION, profile: profileName, scope: credential.scope }
44175
45397
  });
@@ -44189,7 +45411,7 @@ async function runStart(profileDir2, profileName) {
44189
45411
  if (result.action === "updated") {
44190
45412
  logger.info("update.updated", { version: result.version });
44191
45413
  console.log("Restarting with the new version\u2026");
44192
- const child = spawn6(process.execPath, process.argv.slice(1), {
45414
+ const child = spawn8(process.execPath, process.argv.slice(1), {
44193
45415
  stdio: "inherit",
44194
45416
  env: { ...process.env, FOURPM_NO_UPDATE: "1" }
44195
45417
  });
@@ -44204,6 +45426,7 @@ async function runStart(profileDir2, profileName) {
44204
45426
  return;
44205
45427
  }
44206
45428
  const release = () => releaseInstanceLock(profileDir2);
45429
+ let stopControl = null;
44207
45430
  process.once("exit", release);
44208
45431
  process.once("SIGINT", () => process.exit(0));
44209
45432
  process.once("SIGTERM", () => process.exit(0));
@@ -44217,6 +45440,14 @@ async function runStart(profileDir2, profileName) {
44217
45440
  profileDir: profileDir2,
44218
45441
  bus
44219
45442
  });
45443
+ stopControl = startControlServer(bus, profileDir2, {
45444
+ version: CLI_VERSION,
45445
+ scope: credential.scope,
45446
+ profile: profileName,
45447
+ serverUrl: credential.serverUrl,
45448
+ physicPath: config.physicPath ?? null,
45449
+ aiCli: config.aiCli || "claude"
45450
+ });
44220
45451
  const runResilient = async () => {
44221
45452
  for (; ; ) {
44222
45453
  try {
@@ -44240,10 +45471,10 @@ async function runStart(profileDir2, profileName) {
44240
45471
  profileDir: profileDir2,
44241
45472
  serverUrl: credential.serverUrl,
44242
45473
  physicPath: config.physicPath ?? null,
44243
- aiCli: config.aiCli ?? "claude",
45474
+ aiCli: config.aiCli || "claude",
44244
45475
  whoami: () => fetchWhoami(credential.serverUrl, credential.hashcode3).catch(() => null)
44245
45476
  };
44246
- const aiCli2 = config.aiCli ?? "claude";
45477
+ const aiCli2 = config.aiCli || "claude";
44247
45478
  const workingDir = getWorkingProfile(profileDir2, aiCli2);
44248
45479
  const labels = profileLabels(config);
44249
45480
  bus.setActiveProfile(workingDir ? profileDisplayLabel(workingDir) : labels[0] ?? null);
@@ -44257,12 +45488,144 @@ async function runStart(profileDir2, profileName) {
44257
45488
  await runResilient();
44258
45489
  }
44259
45490
  } finally {
45491
+ stopControl?.();
44260
45492
  release();
44261
45493
  }
44262
45494
  }
44263
45495
 
45496
+ // src/commands/attach.ts
45497
+ import { join as join31 } from "path";
45498
+
45499
+ // src/core/control-client.ts
45500
+ import { connect } from "net";
45501
+ function connectControl(socketPath, bus) {
45502
+ return new Promise((resolve8, reject) => {
45503
+ const socket = connect(socketPath);
45504
+ socket.setEncoding("utf8");
45505
+ let ready = false;
45506
+ const forwardSubmit = bus.onLocalSubmit((input) => {
45507
+ try {
45508
+ socket.write(encodeFrame({ t: "submit", input }));
45509
+ } catch {
45510
+ }
45511
+ });
45512
+ const forwardReconnect = bus.onReconnect(() => {
45513
+ try {
45514
+ socket.write(encodeFrame({ t: "reconnect" }));
45515
+ } catch {
45516
+ }
45517
+ });
45518
+ const close = () => {
45519
+ forwardSubmit();
45520
+ forwardReconnect();
45521
+ socket.destroy();
45522
+ };
45523
+ const parse = createFrameParser();
45524
+ socket.on("data", (chunk) => {
45525
+ for (const frame of parse(chunk)) apply(frame);
45526
+ });
45527
+ socket.on("error", (err) => {
45528
+ if (!ready) reject(err);
45529
+ });
45530
+ socket.on("close", () => {
45531
+ if (ready) {
45532
+ bus.setStatus("stopped");
45533
+ bus.log("Daemon disconnected \u2014 the worker process closed the control channel.", "warn");
45534
+ }
45535
+ });
45536
+ function apply(frame) {
45537
+ switch (frame.t) {
45538
+ case "snapshot":
45539
+ bus.setStatus(frame.status);
45540
+ bus.setBusy(frame.busy);
45541
+ bus.setActiveProfile(frame.activeProfile);
45542
+ if (frame.scope) bus.setScope(frame.scope);
45543
+ if (frame.worker) bus.setWorker(frame.worker);
45544
+ bus.setProject(frame.project);
45545
+ if (frame.usage) bus.setUsage(frame.usage);
45546
+ bus.setTokens(frame.tokens);
45547
+ for (const entry of frame.transcript) bus.pushEntry(entry);
45548
+ ready = true;
45549
+ resolve8({ info: frame.info, close });
45550
+ break;
45551
+ case "transcript":
45552
+ bus.pushEntry(frame.entry);
45553
+ break;
45554
+ case "update":
45555
+ bus.updateEntry(frame.id, frame.text, frame.level);
45556
+ break;
45557
+ case "clear":
45558
+ bus.clear();
45559
+ break;
45560
+ case "status":
45561
+ bus.setStatus(frame.status);
45562
+ break;
45563
+ case "busy":
45564
+ bus.setBusy(frame.label);
45565
+ break;
45566
+ case "activeProfile":
45567
+ bus.setActiveProfile(frame.label);
45568
+ break;
45569
+ case "scope":
45570
+ bus.setScope(frame.scope);
45571
+ break;
45572
+ case "worker":
45573
+ bus.setWorker(frame.worker);
45574
+ break;
45575
+ case "project":
45576
+ bus.setProject(frame.project);
45577
+ break;
45578
+ case "usage":
45579
+ bus.setUsage(frame.usage);
45580
+ break;
45581
+ case "tokens":
45582
+ bus.setTokens(frame.total);
45583
+ break;
45584
+ }
45585
+ }
45586
+ });
45587
+ }
45588
+
45589
+ // src/commands/attach.ts
45590
+ async function runAttach(profileDir2, profileName) {
45591
+ if (!process.stdout.isTTY) {
45592
+ console.error("`4pm attach` needs an interactive terminal (TTY).");
45593
+ process.exitCode = 1;
45594
+ return;
45595
+ }
45596
+ const socketPath = join31(profileDir2, CONTROL_SOCKET_FILE);
45597
+ const bus = new SessionBus();
45598
+ let conn;
45599
+ try {
45600
+ conn = await connectControl(socketPath, bus);
45601
+ } catch {
45602
+ console.error(
45603
+ `No running daemon for profile "${profileName}". Start it first: 4pm start --profile ${profileName}`
45604
+ );
45605
+ process.exitCode = 1;
45606
+ return;
45607
+ }
45608
+ const credential = readCredential(profileDir2, profileName);
45609
+ const info = {
45610
+ version: conn.info.version,
45611
+ scope: conn.info.scope,
45612
+ profile: conn.info.profile,
45613
+ profileDir: profileDir2,
45614
+ serverUrl: conn.info.serverUrl,
45615
+ physicPath: conn.info.physicPath,
45616
+ aiCli: conn.info.aiCli,
45617
+ whoami: () => credential ? fetchWhoami(credential.serverUrl, credential.hashcode3).catch(() => null) : Promise.resolve(null)
45618
+ };
45619
+ bus.log(`Attached to profile "${profileName}" \u2014 the worker runs headless; input is forwarded.`);
45620
+ try {
45621
+ await runTui(bus, info);
45622
+ } finally {
45623
+ conn.close();
45624
+ }
45625
+ }
45626
+
44264
45627
  // src/commands/unlink.ts
44265
- import { rmSync as rmSync6 } from "fs";
45628
+ import { rmSync as rmSync8 } from "fs";
44266
45629
  async function runUnlink(profileDir2, profileName) {
44267
45630
  const cred = readCredential(profileDir2, profileName);
44268
45631
  if (!cred) {
@@ -44277,23 +45640,37 @@ async function runUnlink(profileDir2, profileName) {
44277
45640
  `\u26A0 Could not reach the server to revoke \u2014 revoke it at web /machines. (${err.message})`
44278
45641
  );
44279
45642
  }
44280
- rmSync6(profileDir2, { recursive: true, force: true });
45643
+ rmSync8(profileDir2, { recursive: true, force: true });
44281
45644
  clearDefaultProfileIf(profileName);
44282
45645
  console.log("\u2714 Deleted the profile and its physic project folder.");
44283
45646
  }
44284
45647
 
44285
45648
  // src/commands/update.ts
45649
+ function isConnectionError(err) {
45650
+ return err instanceof TypeError && err.message === "fetch failed";
45651
+ }
44286
45652
  async function runUpdate(profileDir2, profileName, serverUrlFlag) {
44287
45653
  const serverUrl = (serverUrlFlag || readCredential(profileDir2, profileName)?.serverUrl || process.env.FOURPM_SERVER || "http://localhost:42001").replace(/\/$/, "");
44288
45654
  let result;
44289
45655
  try {
44290
45656
  result = await updateToLatest(serverUrl);
44291
45657
  } catch (err) {
44292
- console.error(`Could not check for updates: ${err}`);
45658
+ if (isConnectionError(err)) {
45659
+ console.error(
45660
+ `Could not reach the server at ${serverUrl} \u2014 is it running? Set the server with \`--server <url>\` or the FOURPM_SERVER env var.`
45661
+ );
45662
+ } else {
45663
+ console.error(`Could not check for updates: ${err}`);
45664
+ }
44293
45665
  process.exitCode = 1;
44294
45666
  return;
44295
45667
  }
44296
45668
  switch (result.action) {
45669
+ case "dev-build":
45670
+ console.log(
45671
+ `Running a local dev build (${result.version}) \u2014 update it by rebuilding the repo (\`pnpm --filter @4pm/cli build\`), not \`4pm update\`.`
45672
+ );
45673
+ break;
44297
45674
  case "already-latest":
44298
45675
  console.log(`Already up to date (${result.version}).`);
44299
45676
  break;
@@ -44398,10 +45775,16 @@ async function main() {
44398
45775
  switch (command) {
44399
45776
  case "link": {
44400
45777
  const serverUrl = flagValue(rest, "server") || process.env.FOURPM_SERVER || "http://localhost:42001";
44401
- await runLink(serverUrl.replace(/\/$/, ""), explicitProfile);
45778
+ const pairToken = flagValue(rest, "token") || process.env.FOURPM_PAIR_TOKEN || null;
45779
+ await runLink(serverUrl.replace(/\/$/, ""), explicitProfile, pairToken);
44402
45780
  break;
44403
45781
  }
44404
45782
  case "start": {
45783
+ const bootToken = process.env.FOURPM_PAIR_TOKEN || null;
45784
+ if (bootToken && listProfiles().filter((p) => p.linked).length === 0) {
45785
+ const serverUrl = flagValue(rest, "server") || process.env.FOURPM_SERVER || "http://localhost:42001";
45786
+ await runLink(serverUrl.replace(/\/$/, ""), explicitProfile, bootToken);
45787
+ }
44405
45788
  const name = await pickLinkedProfile(explicitProfile, "start");
44406
45789
  if (!name) {
44407
45790
  process.exitCode = 1;
@@ -44410,6 +45793,15 @@ async function main() {
44410
45793
  await runStart(profileDir(name), name);
44411
45794
  break;
44412
45795
  }
45796
+ case "attach": {
45797
+ const name = await pickLinkedProfile(explicitProfile, "attach");
45798
+ if (!name) {
45799
+ process.exitCode = 1;
45800
+ break;
45801
+ }
45802
+ await runAttach(profileDir(name), name);
45803
+ break;
45804
+ }
44413
45805
  case "unlink": {
44414
45806
  const name = await pickLinkedProfile(explicitProfile, "unlink");
44415
45807
  if (!name) {
@@ -44440,6 +45832,7 @@ async function main() {
44440
45832
  "Usage:",
44441
45833
  " 4pm link Pair with the server (hashcode 1\u21922\u21923)",
44442
45834
  " 4pm start Connect WS, receive commands from the server",
45835
+ " 4pm attach Open the TUI against a running headless daemon (ADR-0192)",
44443
45836
  " 4pm unlink Delete a link (pick a profile if several)",
44444
45837
  " 4pm version Show the installed version (+ latest from the server)",
44445
45838
  " 4pm update Update to the latest version now",