@4pm/cli 0.9.0 → 1.2.3

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 +1524 -255
  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` here are EXTRA args only (empty by default) — the token-metering flags
28280
- // (claude stream-json / codex `exec --json`) are hardcoded in source and always
28281
- // injected (ADR-0158, ADR-0072), so they cannot be dropped by editing this file.
28282
- // `model` = the CLI's `--model` flag; default "sonnet" — the balanced mid-price tier
28283
- // (cheaper than opus, more capable than haiku). "" ⇒ the CLI's own default model.
28284
- claudeHome: [{ profile: ".claude", args: [], model: "sonnet" }],
28285
- codexHome: [{ profile: ".codex", args: [], model: "" }],
28286
- antigravityHome: [],
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: "" }
28288
+ ],
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";
@@ -28668,7 +28681,7 @@ async function checkAndUpdate(serverUrl, autoUpdate) {
28668
28681
  // src/core/ws-client.ts
28669
28682
  init_wrapper();
28670
28683
  import { randomUUID } from "crypto";
28671
- import { basename as basename4, join as join19 } from "path";
28684
+ import { basename as basename4, join as join25 } from "path";
28672
28685
 
28673
28686
  // ../../22-packages/11-shared/51-utils/src/helpers.ts
28674
28687
  function backoffJitterMs(attempt, baseMs = 500, maxMs = 1e4) {
@@ -28781,6 +28794,9 @@ var PermissionCode = {
28781
28794
  PROJECT_AGENTS: "project.agents",
28782
28795
  // Security & placeholder management (ADR-0154): AI_SECURITY/AI_PLACEHOLDER docs + write-only secrets
28783
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",
28784
28800
  // RAG capability install (ADR-0156): install/manage the worker's RAG engine
28785
28801
  PROJECT_RAG: "project.rag",
28786
28802
  // permission
@@ -28811,6 +28827,13 @@ var PermissionCode = {
28811
28827
  // org template files (ADR-0113) — ADMIN + PM manage the org's template library
28812
28828
  TEMPLATE_READ: "template.read",
28813
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",
28814
28837
  // integration (community-server — ADR-0058; served by @4pm/community)
28815
28838
  INTEGRATION_READ: "integration.read",
28816
28839
  INTEGRATION_CONNECT: "integration.connect",
@@ -28862,6 +28885,10 @@ var ROLE_DEFAULT_PERMISSIONS = {
28862
28885
  P.QUOTA_MANAGE,
28863
28886
  P.TEMPLATE_READ,
28864
28887
  P.TEMPLATE_WRITE,
28888
+ P.MARKET_READ,
28889
+ P.MARKET_PUBLISH,
28890
+ P.MARKET_MODERATE,
28891
+ P.KNOWLEDGE_PUBLISH,
28865
28892
  P.TEAM_LIST,
28866
28893
  P.TEAM_CREATE,
28867
28894
  P.TEAM_READ,
@@ -28893,6 +28920,7 @@ var ROLE_DEFAULT_PERMISSIONS = {
28893
28920
  P.PROJECT_AUTONOMOUS,
28894
28921
  P.PROJECT_AGENTS,
28895
28922
  P.PROJECT_SECRETS,
28923
+ P.PROJECT_AGENT_TOOLS,
28896
28924
  P.PROJECT_RAG,
28897
28925
  P.PERMISSION_LIST,
28898
28926
  P.PERMISSION_READ,
@@ -28923,8 +28951,15 @@ var ROLE_DEFAULT_PERMISSIONS = {
28923
28951
  P.PROJECT_AGENTS,
28924
28952
  // manage security/placeholder + set (write-only) secrets — ADR-0154
28925
28953
  P.PROJECT_SECRETS,
28954
+ // edit the Claude tool-permission policy (settings.json + settings.local.json) — ADR-0183
28955
+ P.PROJECT_AGENT_TOOLS,
28926
28956
  // install/manage the worker RAG engine — ADR-0156
28927
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,
28928
28963
  P.INTEGRATION_READ
28929
28964
  ],
28930
28965
  [Role.QA]: [
@@ -28936,6 +28971,8 @@ var ROLE_DEFAULT_PERMISSIONS = {
28936
28971
  P.PROJECT_MEMO_READ,
28937
28972
  P.PROJECT_MEMO_WRITE,
28938
28973
  P.PROJECT_GIT,
28974
+ // browse the marketplace (read-only) — ADR-0185
28975
+ P.MARKET_READ,
28939
28976
  P.INTEGRATION_READ
28940
28977
  ],
28941
28978
  [Role.BA]: [
@@ -28947,6 +28984,8 @@ var ROLE_DEFAULT_PERMISSIONS = {
28947
28984
  P.PROJECT_MEMO_READ,
28948
28985
  P.PROJECT_MEMO_WRITE,
28949
28986
  P.PROJECT_GIT,
28987
+ // browse the marketplace (read-only) — ADR-0185
28988
+ P.MARKET_READ,
28950
28989
  P.INTEGRATION_READ
28951
28990
  ],
28952
28991
  [Role.MACHINE]: [
@@ -33431,6 +33470,7 @@ var createArtifactRequestSchema = external_exports.object({
33431
33470
  kind: external_exports.enum(TEMPLATE_KINDS),
33432
33471
  content: external_exports.string().min(1).max(ARTIFACT_MAX_BYTES)
33433
33472
  });
33473
+ var GIT_URL_RE = /^(https?:\/\/|git@[^\s:]+:|ssh:\/\/).+/i;
33434
33474
  var repoSpecSchema = external_exports.object({
33435
33475
  /** Role label (e.g. "docs" / "web" / "server") — descriptive, not constrained. */
33436
33476
  role: external_exports.string().max(60).optional().default(""),
@@ -33438,23 +33478,15 @@ var repoSpecSchema = external_exports.object({
33438
33478
  desc: external_exports.string().max(500).optional().default(""),
33439
33479
  /** Exactly one repo of the project is the primary. */
33440
33480
  primary: external_exports.boolean().optional().default(false),
33441
- /** `create` a new repo (provider) or use an `existing` one (url). */
33442
- op: external_exports.enum(["create", "existing"]).default("create"),
33443
- provider: external_exports.enum(["gh", "glab"]).default("gh"),
33444
- /** New repo name (op=create; empty ⇒ project id for the primary). */
33445
- name: external_exports.string().max(100).optional().default(""),
33446
- visibility: external_exports.enum(["private", "public"]).default("private"),
33447
- /** Existing repo url to clone/link (op=existing). */
33448
- 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"),
33449
33483
  /** Sub-repo subfolder under the target root (empty ⇒ derived from role/name). */
33450
- subdir: external_exports.string().max(120).optional().default(""),
33451
- /** Default branch for this repo (empty ⇒ provider default, e.g. `main`). */
33452
- defaultBranch: external_exports.string().max(100).optional().default(""),
33453
- /** `.gitignore` template name for this repo (empty ⇒ none). */
33454
- gitignore: external_exports.string().max(40).optional().default(""),
33455
- /** Commit-message convention for this repo (empty ⇒ unspecified). */
33456
- commitConvention: external_exports.string().max(40).optional().default("")
33484
+ subdir: external_exports.string().max(120).optional().default("")
33457
33485
  });
33486
+ function repoName(url) {
33487
+ const last = (url || "").trim().replace(/\/+$/, "").split(/[/:]/).pop() ?? "";
33488
+ return last.replace(/\.git$/i, "");
33489
+ }
33458
33490
  var specFieldEnvelopeSchema = external_exports.object({
33459
33491
  id: external_exports.string().min(1),
33460
33492
  name: external_exports.string().optional().default(""),
@@ -33512,16 +33544,6 @@ var addRepoRequestSchema = external_exports.object({
33512
33544
  message: "Exactly one repo must be primary"
33513
33545
  });
33514
33546
  }
33515
- for (let i = 0; i < v.repos.length; i++) {
33516
- const r = v.repos[i];
33517
- if (r && r.op === "existing" && !r.url) {
33518
- ctx.addIssue({
33519
- code: external_exports.ZodIssueCode.custom,
33520
- path: ["repos", i, "url"],
33521
- message: "An existing repo requires a url"
33522
- });
33523
- }
33524
- }
33525
33547
  });
33526
33548
 
33527
33549
  // ../../22-packages/11-shared/31-dto/src/activity.ts
@@ -33563,6 +33585,16 @@ var confirmRequestSchema = external_exports.object({
33563
33585
  var wsTokenRequestSchema = external_exports.object({
33564
33586
  hashcode3: hexTokenSchema
33565
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
+ });
33566
33598
  var listWorkersQuerySchema = baseRequestSchema.extend({
33567
33599
  status: external_exports.enum(["online", "offline"]).optional()
33568
33600
  });
@@ -33638,6 +33670,20 @@ var agentWriteRequestSchema = external_exports.object({
33638
33670
  ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["content"], message: "content is required for write" });
33639
33671
  }
33640
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
+ });
33641
33687
 
33642
33688
  // ../../22-packages/11-shared/31-dto/src/command.ts
33643
33689
  var COMMAND_MAX_LEN = 8e3;
@@ -33844,6 +33890,72 @@ var addonChangeRequestSchema = external_exports.object({
33844
33890
  provider: external_exports.enum([PaymentProvider.STRIPE, PaymentProvider.PAYPAL]).optional()
33845
33891
  });
33846
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
+
33847
33959
  // ../../22-packages/11-shared/31-dto/src/search.ts
33848
33960
  var SearchHitType = {
33849
33961
  PROJECT: "project",
@@ -33973,9 +34085,20 @@ var WsChannels = {
33973
34085
  AGENTS_LIST: "agents.list",
33974
34086
  AGENTS_READ: "agents.read",
33975
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",
33976
34095
  // security & placeholder management (ADR-0154): AI_SECURITY/AI_PLACEHOLDER + write-only secrets
33977
34096
  SECRETS_READ: "secrets.read",
33978
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",
33979
34102
  // docs & code dependency graph (ADR-0155): build a {nodes,edges,orphans} graph per mode
33980
34103
  GRAPH_BUILD: "graph.build",
33981
34104
  // RAG capability + install (ADR-0156): probe the worker + install a worker-tuned model
@@ -33990,6 +34113,9 @@ var WsChannels = {
33990
34113
  GIT_LOG: "git.log",
33991
34114
  GIT_COMMIT: "git.commit",
33992
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",
33993
34119
  PROJECT_CREATE: "project.create",
33994
34120
  PROJECT_ADD: "project.add",
33995
34121
  PROJECT_PROGRESS: "project.progress",
@@ -34026,7 +34152,14 @@ var WsChannels = {
34026
34152
  // outbound review (ADR-0082): machine→server request, server→outbound evaluate, result back
34027
34153
  REVIEW_REQUEST: "review.request",
34028
34154
  REVIEW_EVALUATE: "review.evaluate",
34029
- 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"
34030
34163
  };
34031
34164
 
34032
34165
  // ../../22-packages/21-protocol/21-websocket/src/crypto.ts
@@ -34143,7 +34276,7 @@ var OutputBatcher = class {
34143
34276
  };
34144
34277
 
34145
34278
  // src/core/ws-client.ts
34146
- 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";
34147
34280
  import { randomUUID as randomCommandId } from "crypto";
34148
34281
 
34149
34282
  // src/core/update-scheduler.ts
@@ -34302,9 +34435,9 @@ var UpdateScheduler = class {
34302
34435
  day: "2-digit",
34303
34436
  hour: "2-digit"
34304
34437
  }).formatToParts(/* @__PURE__ */ new Date());
34305
- const get = (type) => parts.find((p) => p.type === type)?.value ?? "";
34306
- const hour = Number.parseInt(get("hour"), 10);
34307
- 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")}`;
34308
34441
  if (Number.isNaN(hour)) return null;
34309
34442
  return { hour: hour % 24, dateKey };
34310
34443
  } catch {
@@ -34521,7 +34654,7 @@ async function runAiFailover(plan, commandId, cwd2, handlers) {
34521
34654
  for (let i = 0; i < total; i++) {
34522
34655
  const attempt = plan.attempts[i];
34523
34656
  if (total > 1) handlers.onAttemptStart(attempt.label, i, total);
34524
- const parser = createAiStreamParser(plan.cmd);
34657
+ const parser = createAiStreamParser(attempt.cmd);
34525
34658
  let captured = "";
34526
34659
  const emit = (raw) => {
34527
34660
  const text = parser.push(raw);
@@ -34530,7 +34663,7 @@ async function runAiFailover(plan, commandId, cwd2, handlers) {
34530
34663
  handlers.onChunk(text);
34531
34664
  }
34532
34665
  };
34533
- 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);
34534
34667
  const tail = parser.flush();
34535
34668
  if (tail) {
34536
34669
  captured += tail;
@@ -34539,13 +34672,13 @@ async function runAiFailover(plan, commandId, cwd2, handlers) {
34539
34672
  if (finalExit === 0) {
34540
34673
  const usage = parser.usage();
34541
34674
  if (usage.tokens === 0) usage.tokens = estimateTokens(captured);
34542
- return { exitCode: 0, workedDir: attempt.dir, usage };
34675
+ return { exitCode: 0, workedDir: attempt.dir, workedKey: attempt.key, workedCmd: attempt.cmd, usage };
34543
34676
  }
34544
34677
  const reason = isAuthFailure(captured) ? "auth" : isSessionLimit(captured) ? "limit" : null;
34545
34678
  if (!reason || i === total - 1) break;
34546
34679
  handlers.onAttemptFail(attempt.label, reason);
34547
34680
  }
34548
- 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 } };
34549
34682
  }
34550
34683
 
34551
34684
  // src/core/ai-profile-state.ts
@@ -34575,6 +34708,19 @@ function setWorkingProfile(profileDir2, cmd, dir2) {
34575
34708
  } catch {
34576
34709
  }
34577
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
+ }
34578
34724
 
34579
34725
  // src/utils/ai-cli.ts
34580
34726
  import { readFileSync as readFileSync8 } from "fs";
@@ -34591,6 +34737,37 @@ function requiredArgs(cmd) {
34591
34737
  return REQUIRED_AI_ARGS.antigravity;
34592
34738
  }
34593
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
+ }
34594
34771
  function resolveHomePath(value) {
34595
34772
  if (value.startsWith("~")) return join8(homedir3(), value.slice(1).replace(/^[/\\]/, ""));
34596
34773
  if (isAbsolute(value)) return value;
@@ -34598,7 +34775,16 @@ function resolveHomePath(value) {
34598
34775
  }
34599
34776
  function profileDirs(profiles) {
34600
34777
  if (!profiles) return [];
34601
- 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");
34602
34788
  }
34603
34789
  function buildRunArgs(profile, cmd, prompt) {
34604
34790
  const extras = profile.args ?? [];
@@ -34607,7 +34793,7 @@ function buildRunArgs(profile, cmd, prompt) {
34607
34793
  return [...requiredArgs(cmd), ...extras, ...modelArgs, prompt];
34608
34794
  }
34609
34795
  function claudeHomeDirs(config) {
34610
- const dirs = profileDirs(config.claudeHome);
34796
+ const dirs = profileDirs(claudeProfilesOf(config));
34611
34797
  return dirs.length > 0 ? dirs : [join8(homedir3(), ".claude")];
34612
34798
  }
34613
34799
  function profileDisplayLabel(dir2) {
@@ -34631,29 +34817,64 @@ function profilesFor(config, cmd) {
34631
34817
  return [];
34632
34818
  }
34633
34819
  function profileLabels(config) {
34820
+ if (isUnifiedConfig(config)) {
34821
+ return config.aiProfiles.filter(isUsableCredential).map((c) => c.label?.trim() || profileDisplayLabel(resolveHomePath(c.profile)));
34822
+ }
34634
34823
  const cmd = config.aiCli ?? DEFAULT_AI_CLI;
34635
34824
  return profileDirs(profilesFor(config, cmd)).map((dir2) => profileDisplayLabel(dir2));
34636
34825
  }
34637
- function planAiRun(prompt, config, workingDir) {
34638
- 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;
34639
34858
  const baseEnv = config.aiEnv ?? {};
34640
34859
  const envVar = profileEnvVar(cmd);
34641
34860
  const defaultPlan = {
34642
34861
  cmd,
34643
34862
  attempts: [
34644
- { 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 } }
34645
34864
  ]
34646
34865
  };
34647
34866
  if (!envVar) return defaultPlan;
34648
- 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) }));
34649
34868
  if (resolved.length === 0) return defaultPlan;
34650
34869
  const ordered = workingDir && resolved.some((r) => r.dir === workingDir) ? [
34651
34870
  resolved.find((r) => r.dir === workingDir),
34652
34871
  ...resolved.filter((r) => r.dir !== workingDir)
34653
34872
  ] : resolved;
34654
34873
  const attempts = ordered.map(({ profile, dir: dir2 }) => ({
34874
+ cmd,
34655
34875
  label: profileDisplayLabel(dir2),
34656
34876
  dir: dir2,
34877
+ key: credentialKey(cmd, dir2),
34657
34878
  args: buildRunArgs(profile, cmd, prompt),
34658
34879
  env: { ...baseEnv, [envVar]: dir2 }
34659
34880
  }));
@@ -35163,13 +35384,228 @@ async function writeWorkerFile(root, relPath, content) {
35163
35384
  }
35164
35385
  }
35165
35386
 
35166
- // src/core/autonomous.ts
35167
- import { readFile as readFile2, writeFile as writeFile2, chmod } from "fs/promises";
35168
- import { readFileSync as readFileSync12 } from "fs";
35169
- 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";
35170
35392
  import { join as join12 } from "path";
35171
35393
  import { promisify as promisify2 } from "util";
35172
- 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);
35173
35609
  var SETTINGS_REL = ".claude/.autonomous.settings.json";
35174
35610
  var HISTORIES_REL = ".claude/.autonomous.histories.json";
35175
35611
  var APPROVALS_REL = ".claude/.autonomous.approvals.json";
@@ -35199,7 +35635,7 @@ function parseJson(text) {
35199
35635
  }
35200
35636
  }
35201
35637
  function tickScript(root) {
35202
- return join12(root, TICK_REL);
35638
+ return join13(root, TICK_REL);
35203
35639
  }
35204
35640
  async function crontabList() {
35205
35641
  try {
@@ -35211,7 +35647,7 @@ async function crontabList() {
35211
35647
  }
35212
35648
  async function crontabSet(content) {
35213
35649
  return new Promise((resolve8) => {
35214
- const child = spawn4("crontab", ["-"], { stdio: ["pipe", "ignore", "ignore"] });
35650
+ const child = spawn6("crontab", ["-"], { stdio: ["pipe", "ignore", "ignore"] });
35215
35651
  child.on("error", () => resolve8(false));
35216
35652
  child.on("close", (code) => resolve8(code === 0));
35217
35653
  child.stdin.end(content.endsWith("\n") ? content : content + "\n");
@@ -35222,8 +35658,8 @@ async function cronInstalled(root) {
35222
35658
  return (await crontabList()).split("\n").some((l) => l.includes(script) && !l.trim().startsWith("#"));
35223
35659
  }
35224
35660
  async function getAutonomousStatus(root) {
35225
- const settings = parseJson(await readText(join12(root, SETTINGS_REL))) ?? {};
35226
- 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))) ?? {};
35227
35663
  const records = Array.isArray(hist.records) ? hist.records : [];
35228
35664
  const last = records[records.length - 1];
35229
35665
  const ticks = hist.ticks ?? {};
@@ -35240,9 +35676,9 @@ async function getAutonomousStatus(root) {
35240
35676
  }
35241
35677
  function isAutonomousRunning(root) {
35242
35678
  try {
35243
- const s = JSON.parse(readFileSync12(join12(root, SETTINGS_REL), "utf8"));
35679
+ const s = JSON.parse(readFileSync13(join13(root, SETTINGS_REL), "utf8"));
35244
35680
  if (s.paused === true) return false;
35245
- const h = JSON.parse(readFileSync12(join12(root, HISTORIES_REL), "utf8"));
35681
+ const h = JSON.parse(readFileSync13(join13(root, HISTORIES_REL), "utf8"));
35246
35682
  const ts = h.records?.[h.records.length - 1]?.ts;
35247
35683
  if (!ts) return false;
35248
35684
  const t = Date.parse(ts.replace(" ", "T"));
@@ -35253,10 +35689,10 @@ function isAutonomousRunning(root) {
35253
35689
  }
35254
35690
  async function readAutonomous(root) {
35255
35691
  const [settings, approvals, status, ...books] = await Promise.all([
35256
- readText(join12(root, SETTINGS_REL)),
35257
- readText(join12(root, APPROVALS_REL), "{}"),
35692
+ readText(join13(root, SETTINGS_REL)),
35693
+ readText(join13(root, APPROVALS_REL), "{}"),
35258
35694
  getAutonomousStatus(root),
35259
- ...Object.values(BOOK_FILES).map((f) => readText(join12(root, f)))
35695
+ ...Object.values(BOOK_FILES).map((f) => readText(join13(root, f)))
35260
35696
  ]);
35261
35697
  const keys = Object.keys(BOOK_FILES);
35262
35698
  const bookMap = {};
@@ -35265,7 +35701,7 @@ async function readAutonomous(root) {
35265
35701
  }
35266
35702
  async function readAutonomousLogs(root, date) {
35267
35703
  const day = date && /^\d{4}-\d{2}-\d{2}$/.test(date) ? date : (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
35268
- 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`));
35269
35705
  const lines = text ? text.split("\n").filter((l) => l.length > 0).slice(-500) : [];
35270
35706
  return { date: day, lines };
35271
35707
  }
@@ -35289,24 +35725,24 @@ async function writeAutonomous(root, req, by) {
35289
35725
  switch (req.kind) {
35290
35726
  case "settings": {
35291
35727
  if (!parseJson(req.settings)) return { ok: false, error: "settings is not valid JSON" };
35292
- await writeFile2(join12(root, SETTINGS_REL), req.settings, "utf8");
35728
+ await writeFile2(join13(root, SETTINGS_REL), req.settings, "utf8");
35293
35729
  break;
35294
35730
  }
35295
35731
  case "approvals": {
35296
- const map = parseJson(await readText(join12(root, APPROVALS_REL), "{}")) ?? {};
35732
+ const map = parseJson(await readText(join13(root, APPROVALS_REL), "{}")) ?? {};
35297
35733
  if (req.approved) map[req.taskId] = { approved: true, by, at: (/* @__PURE__ */ new Date()).toISOString() };
35298
35734
  else delete map[req.taskId];
35299
- 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");
35300
35736
  break;
35301
35737
  }
35302
35738
  case "userTodo": {
35303
- const cur = await readText(join12(root, BOOK_FILES.userTodo));
35739
+ const cur = await readText(join13(root, BOOK_FILES.userTodo));
35304
35740
  const block = `
35305
35741
 
35306
35742
  > posted by ${by} at ${(/* @__PURE__ */ new Date()).toISOString()}
35307
35743
  ${req.content.trim()}
35308
35744
  `;
35309
- await writeFile2(join12(root, BOOK_FILES.userTodo), cur.trimEnd() + block, "utf8");
35745
+ await writeFile2(join13(root, BOOK_FILES.userTodo), cur.trimEnd() + block, "utf8");
35310
35746
  break;
35311
35747
  }
35312
35748
  case "cron": {
@@ -35323,7 +35759,7 @@ ${req.content.trim()}
35323
35759
 
35324
35760
  // src/core/agents.ts
35325
35761
  import { readFile as readFile3, writeFile as writeFile3, mkdir as mkdir3, readdir as readdir3, rm } from "fs/promises";
35326
- import { join as join13 } from "path";
35762
+ import { join as join14 } from "path";
35327
35763
  var AGENTS_REL = ".claude/agents";
35328
35764
  var SKILLS_REL = ".claude/skills";
35329
35765
  var MEMORY_DIRS = /* @__PURE__ */ new Set(["short-memory", "long-memory"]);
@@ -35340,24 +35776,24 @@ async function readText2(path, fallback = "") {
35340
35776
  function frontmatter(text) {
35341
35777
  const m = text.match(/^---\r?\n([\s\S]*?)\r?\n---/);
35342
35778
  const body = m?.[1] ?? "";
35343
- const get = (key) => body.match(new RegExp(`^${key}:\\s*(.+)$`, "m"))?.[1]?.trim().replace(/^["']|["']$/g, "") ?? "";
35344
- 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") };
35345
35781
  }
35346
35782
  async function listAgents(root) {
35347
35783
  const subagents = [];
35348
35784
  const skills = [];
35349
35785
  try {
35350
- 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 })) {
35351
35787
  if (!e.isFile() || !e.name.endsWith(".md") || MEMORY_DIRS.has(e.name)) continue;
35352
- const fm = frontmatter(await readText2(join13(root, AGENTS_REL, e.name)));
35788
+ const fm = frontmatter(await readText2(join14(root, AGENTS_REL, e.name)));
35353
35789
  subagents.push({ name: e.name.replace(/\.md$/, ""), description: fm.description });
35354
35790
  }
35355
35791
  } catch {
35356
35792
  }
35357
35793
  try {
35358
- 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 })) {
35359
35795
  if (!e.isDirectory()) continue;
35360
- 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")));
35361
35797
  skills.push({ name: e.name, description: fm.description });
35362
35798
  }
35363
35799
  } catch {
@@ -35368,15 +35804,15 @@ async function listAgents(root) {
35368
35804
  }
35369
35805
  function itemPath(root, kind, name) {
35370
35806
  const n = safeName(name);
35371
- 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");
35372
35808
  }
35373
35809
  async function readAgent(root, kind, name) {
35374
35810
  const content = await readText2(itemPath(root, kind, name));
35375
35811
  if (kind !== "subagent") return { content };
35376
35812
  const n = safeName(name);
35377
35813
  const [short, long] = await Promise.all([
35378
- readText2(join13(root, AGENTS_REL, "short-memory", `${n}.md`)),
35379
- 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`))
35380
35816
  ]);
35381
35817
  return { content, memory: { short, long } };
35382
35818
  }
@@ -35386,12 +35822,12 @@ async function writeAgent(root, req) {
35386
35822
  try {
35387
35823
  if (req.action === "delete") {
35388
35824
  if (req.kind === "subagent") await rm(itemPath(root, "subagent", n), { force: true });
35389
- 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 });
35390
35826
  return { ok: true };
35391
35827
  }
35392
35828
  if (req.content === void 0) return { ok: false, error: "content is required" };
35393
35829
  const path = itemPath(root, req.kind, n);
35394
- await mkdir3(join13(path, ".."), { recursive: true });
35830
+ await mkdir3(join14(path, ".."), { recursive: true });
35395
35831
  await writeFile3(path, req.content, "utf8");
35396
35832
  return { ok: true };
35397
35833
  } catch (err) {
@@ -35399,22 +35835,257 @@ async function writeAgent(root, req) {
35399
35835
  }
35400
35836
  }
35401
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
+
35402
36073
  // src/core/secrets.ts
35403
- import { readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
35404
- import { join as join14 } from "path";
36074
+ import { readFile as readFile5, writeFile as writeFile5 } from "fs/promises";
36075
+ import { join as join18 } from "path";
35405
36076
  var SECURITY_REL = "AI_SECURITY.md";
35406
36077
  var PLACEHOLDER_REL = "AI_PLACEHOLDER.md";
35407
36078
  var SECRETS_REL = "project.secrets.json";
35408
36079
  async function readText3(path, fallback = "") {
35409
36080
  try {
35410
- return await readFile4(path, "utf8");
36081
+ return await readFile5(path, "utf8");
35411
36082
  } catch {
35412
36083
  return fallback;
35413
36084
  }
35414
36085
  }
35415
36086
  async function readSecretsObject(root) {
35416
36087
  try {
35417
- const v = JSON.parse(await readFile4(join14(root, SECRETS_REL), "utf8"));
36088
+ const v = JSON.parse(await readFile5(join18(root, SECRETS_REL), "utf8"));
35418
36089
  return v && typeof v === "object" ? v : {};
35419
36090
  } catch {
35420
36091
  return {};
@@ -35433,8 +36104,8 @@ function placeholderKeys(text) {
35433
36104
  }
35434
36105
  async function readSecrets(root) {
35435
36106
  const [security, placeholder, secrets] = await Promise.all([
35436
- readText3(join14(root, SECURITY_REL)),
35437
- readText3(join14(root, PLACEHOLDER_REL)),
36107
+ readText3(join18(root, SECURITY_REL)),
36108
+ readText3(join18(root, PLACEHOLDER_REL)),
35438
36109
  readSecretsObject(root)
35439
36110
  ]);
35440
36111
  const names = /* @__PURE__ */ new Set([...placeholderKeys(placeholder), ...Object.keys(secrets)]);
@@ -35447,14 +36118,14 @@ async function readSecrets(root) {
35447
36118
  async function writeSecrets(root, req) {
35448
36119
  try {
35449
36120
  if (req.kind === "security") {
35450
- await writeFile4(join14(root, SECURITY_REL), req.content, "utf8");
36121
+ await writeFile5(join18(root, SECURITY_REL), req.content, "utf8");
35451
36122
  } else if (req.kind === "placeholder") {
35452
- await writeFile4(join14(root, PLACEHOLDER_REL), req.content, "utf8");
36123
+ await writeFile5(join18(root, PLACEHOLDER_REL), req.content, "utf8");
35453
36124
  } else {
35454
36125
  const secrets = await readSecretsObject(root);
35455
36126
  if (req.kind === "secret") secrets[req.key] = req.value;
35456
36127
  else delete secrets[req.key];
35457
- 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");
35458
36129
  }
35459
36130
  return { ok: true };
35460
36131
  } catch (err) {
@@ -35462,9 +36133,52 @@ async function writeSecrets(root, req) {
35462
36133
  }
35463
36134
  }
35464
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
+
35465
36179
  // src/core/graph.ts
35466
- import { readFile as readFile5, readdir as readdir4 } from "fs/promises";
35467
- 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";
35468
36182
  var MAX_FILES = 2e3;
35469
36183
  var MAX_NODES = 3e3;
35470
36184
  var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".next", "out", "coverage", ".turbo"]);
@@ -35476,7 +36190,7 @@ async function walk(root, accept) {
35476
36190
  const dir2 = stack.pop();
35477
36191
  let entries2;
35478
36192
  try {
35479
- entries2 = await readdir4(dir2, { withFileTypes: true });
36193
+ entries2 = await readdir5(dir2, { withFileTypes: true });
35480
36194
  } catch {
35481
36195
  continue;
35482
36196
  }
@@ -35484,7 +36198,7 @@ async function walk(root, accept) {
35484
36198
  if (e.name.startsWith(".") && e.name !== ".claude") {
35485
36199
  if (e.isDirectory()) continue;
35486
36200
  }
35487
- const full = join15(dir2, e.name);
36201
+ const full = join20(dir2, e.name);
35488
36202
  if (e.isDirectory()) {
35489
36203
  if (!SKIP_DIRS.has(e.name)) stack.push(full);
35490
36204
  } else if (accept(full)) {
@@ -35501,25 +36215,25 @@ function orphansOf(nodes, edges) {
35501
36215
  }
35502
36216
  async function buildDocs(root) {
35503
36217
  const files = await walk(root, (p) => extname(p) === ".md");
35504
- 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("/")));
35505
36219
  const nodes = files.map((f) => {
35506
- const rel = relative(root, f).split("\\").join("/");
36220
+ const rel = relative2(root, f).split("\\").join("/");
35507
36221
  return { id: rel, label: basename2(rel), kind: "doc", file: rel };
35508
36222
  });
35509
36223
  const edges = [];
35510
36224
  const seen = /* @__PURE__ */ new Set();
35511
36225
  for (const f of files) {
35512
- const rel = relative(root, f).split("\\").join("/");
36226
+ const rel = relative2(root, f).split("\\").join("/");
35513
36227
  let text = "";
35514
36228
  try {
35515
- text = await readFile5(f, "utf8");
36229
+ text = await readFile7(f, "utf8");
35516
36230
  } catch {
35517
36231
  continue;
35518
36232
  }
35519
36233
  for (const m of text.matchAll(/\[[^\]]*\]\(([^)]+)\)/g)) {
35520
36234
  const href = (m[1] ?? "").split("#")[0].trim();
35521
36235
  if (!href || !href.endsWith(".md") || href.startsWith("http")) continue;
35522
- const target = relative(root, resolve4(dirname4(f), href)).split("\\").join("/");
36236
+ const target = relative2(root, resolve4(dirname6(f), href)).split("\\").join("/");
35523
36237
  if (ids.has(target) && target !== rel) {
35524
36238
  const key = `${rel}->${target}`;
35525
36239
  if (!seen.has(key)) {
@@ -35548,10 +36262,10 @@ async function buildCode(root) {
35548
36262
  const fileDefs = /* @__PURE__ */ new Map();
35549
36263
  for (const f of files) {
35550
36264
  if (nodes.length >= MAX_NODES) break;
35551
- const rel = relative(root, f).split("\\").join("/");
36265
+ const rel = relative2(root, f).split("\\").join("/");
35552
36266
  let text = "";
35553
36267
  try {
35554
- text = await readFile5(f, "utf8");
36268
+ text = await readFile7(f, "utf8");
35555
36269
  } catch {
35556
36270
  continue;
35557
36271
  }
@@ -35603,13 +36317,13 @@ async function buildGraph(root, mode) {
35603
36317
  }
35604
36318
 
35605
36319
  // src/core/rag.ts
35606
- import { readFile as readFile6, writeFile as writeFile5, statfs, mkdir as mkdir4 } from "fs/promises";
35607
- import { existsSync as existsSync7 } from "fs";
35608
- 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";
35609
36323
  import { totalmem, cpus } from "os";
35610
- import { join as join16 } from "path";
35611
- import { promisify as promisify3 } from "util";
35612
- var run3 = promisify3(execFile3);
36324
+ import { join as join21 } from "path";
36325
+ import { promisify as promisify4 } from "util";
36326
+ var run3 = promisify4(execFile4);
35613
36327
  var RAG_REL = ".claude/rag";
35614
36328
  async function tryRun(cmd, args) {
35615
36329
  try {
@@ -35628,7 +36342,7 @@ async function detectPython() {
35628
36342
  }
35629
36343
  async function installLog(root) {
35630
36344
  try {
35631
- const text = await readFile6(join16(root, RAG_REL, "install.log"), "utf8");
36345
+ const text = await readFile8(join21(root, RAG_REL, "install.log"), "utf8");
35632
36346
  return text.split("\n").filter((l) => l.length > 0).slice(-200);
35633
36347
  } catch {
35634
36348
  return [];
@@ -35690,14 +36404,14 @@ async function ragStatus(root) {
35690
36404
  };
35691
36405
  let modelName = "";
35692
36406
  try {
35693
- modelName = (await readFile6(join16(root, RAG_REL, ".installed"), "utf8")).trim();
36407
+ modelName = (await readFile8(join21(root, RAG_REL, ".installed"), "utf8")).trim();
35694
36408
  } catch {
35695
36409
  modelName = "";
35696
36410
  }
35697
36411
  const modelPresent = modelName.length > 0;
35698
36412
  return {
35699
36413
  installed: fastembed && sqliteVec && modelPresent,
35700
- installing: existsSync7(join16(root, RAG_REL, ".installing")),
36414
+ installing: existsSync8(join21(root, RAG_REL, ".installing")),
35701
36415
  python: { found: py.found, version: py.version },
35702
36416
  deps: { pip, fastembed, sqliteVec },
35703
36417
  model: { present: modelPresent, name: modelName },
@@ -35708,9 +36422,9 @@ async function ragStatus(root) {
35708
36422
  };
35709
36423
  }
35710
36424
  async function readIndexMeta(root) {
35711
- const indexing = existsSync7(join16(root, RAG_REL, ".indexing"));
36425
+ const indexing = existsSync8(join21(root, RAG_REL, ".indexing"));
35712
36426
  try {
35713
- 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"));
35714
36428
  return {
35715
36429
  present: true,
35716
36430
  indexing,
@@ -35777,14 +36491,14 @@ if __name__ == "__main__":
35777
36491
  elif cmd == "query": query(sys.argv[2], int(sys.argv[3]) if len(sys.argv) > 3 else 8)
35778
36492
  `;
35779
36493
  async function ensureEngine(root) {
35780
- await mkdir4(join16(root, RAG_REL), { recursive: true }).catch(() => void 0);
35781
- 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");
35782
36496
  }
35783
36497
  async function ragReindex(root) {
35784
36498
  const py = await detectPython();
35785
36499
  if (!py.found) return { ok: false, started: false, error: "python3 not found" };
35786
- if (!existsSync7(join16(root, RAG_REL, ".installed"))) return { ok: false, started: false, error: "RAG is not installed" };
35787
- 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 };
35788
36502
  await ensureEngine(root);
35789
36503
  const script = `
35790
36504
  set +e
@@ -35792,7 +36506,7 @@ touch "${RAG_REL}/.indexing"
35792
36506
  { echo "[reindex] $(date)"; "$RAG_PY" "${RAG_REL}/rag_engine.py" reindex 2>&1; } >> "${RAG_REL}/index.log" 2>&1
35793
36507
  rm -f "${RAG_REL}/.indexing"
35794
36508
  `;
35795
- const child = spawn5("bash", ["-c", script], {
36509
+ const child = spawn7("bash", ["-c", script], {
35796
36510
  cwd: root,
35797
36511
  env: { ...process.env, RAG_PY: py.bin },
35798
36512
  detached: true,
@@ -35804,12 +36518,12 @@ rm -f "${RAG_REL}/.indexing"
35804
36518
  async function ragQuery(root, queryText, k = 8) {
35805
36519
  const py = await detectPython();
35806
36520
  if (!py.found) return { results: [], error: "python3 not found" };
35807
- 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" };
35808
36522
  await ensureEngine(root);
35809
36523
  try {
35810
36524
  const { stdout } = await run3(
35811
36525
  py.bin,
35812
- [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))],
35813
36527
  { cwd: root, timeout: 3e4, maxBuffer: 4 * 1024 * 1024 }
35814
36528
  );
35815
36529
  const parsed = JSON.parse(stdout.trim() || '{"results":[]}');
@@ -35821,9 +36535,9 @@ async function ragQuery(root, queryText, k = 8) {
35821
36535
  async function ragInstall(root, model) {
35822
36536
  const py = await detectPython();
35823
36537
  if (!py.found) return { ok: false, started: false, error: "python3 not found on the worker" };
35824
- 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 };
35825
36539
  try {
35826
- await mkdir4(join16(root, RAG_REL), { recursive: true });
36540
+ await mkdir6(join21(root, RAG_REL), { recursive: true });
35827
36541
  } catch {
35828
36542
  }
35829
36543
  const script = `
@@ -35839,7 +36553,7 @@ rm -f "${RAG_REL}/.installed"
35839
36553
  } >> "${RAG_REL}/install.log" 2>&1
35840
36554
  rm -f "${RAG_REL}/.installing"
35841
36555
  `;
35842
- const child = spawn5("bash", ["-c", script], {
36556
+ const child = spawn7("bash", ["-c", script], {
35843
36557
  cwd: root,
35844
36558
  env: { ...process.env, RAG_PY: py.bin, RAG_MODEL: model },
35845
36559
  detached: true,
@@ -35850,18 +36564,18 @@ rm -f "${RAG_REL}/.installing"
35850
36564
  }
35851
36565
 
35852
36566
  // src/core/git-diff.ts
35853
- import { readFile as readFile7 } from "fs/promises";
35854
- import { execFile as execFile4 } from "child_process";
35855
- import { basename as basename3, dirname as dirname5, resolve as resolve5 } from "path";
35856
- import { promisify as promisify4 } from "util";
35857
- 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);
35858
36572
  async function gitDiff(path) {
35859
36573
  const target = resolve5(path);
35860
- const dir2 = dirname5(target);
36574
+ const dir2 = dirname7(target);
35861
36575
  const base = basename3(target);
35862
36576
  let newContent = "";
35863
36577
  try {
35864
- newContent = await readFile7(target, "utf8");
36578
+ newContent = await readFile9(target, "utf8");
35865
36579
  } catch {
35866
36580
  }
35867
36581
  let oldContent = "";
@@ -35878,9 +36592,9 @@ async function gitDiff(path) {
35878
36592
  }
35879
36593
 
35880
36594
  // src/core/git-env.ts
35881
- import { execFile as execFile5 } from "child_process";
35882
- import { promisify as promisify5 } from "util";
35883
- var run5 = promisify5(execFile5);
36595
+ import { execFile as execFile6 } from "child_process";
36596
+ import { promisify as promisify6 } from "util";
36597
+ var run5 = promisify6(execFile6);
35884
36598
  async function hasBinary(bin, args = ["--version"]) {
35885
36599
  try {
35886
36600
  await run5(bin, args, { timeout: 5e3 });
@@ -35908,12 +36622,80 @@ ${stderr}`;
35908
36622
  return { installed, authenticated, account, claudeCli };
35909
36623
  }
35910
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
+
35911
36693
  // src/core/git-history.ts
35912
- import { readdir as readdir5 } from "fs/promises";
35913
- import { execFile as execFile6 } from "child_process";
36694
+ import { readdir as readdir6 } from "fs/promises";
36695
+ import { execFile as execFile8 } from "child_process";
35914
36696
  import { resolve as resolve6, sep as sep4 } from "path";
35915
- import { promisify as promisify6 } from "util";
35916
- var run6 = promisify6(execFile6);
36697
+ import { promisify as promisify8 } from "util";
36698
+ var run7 = promisify8(execFile8);
35917
36699
  var FS = "";
35918
36700
  var RS = "";
35919
36701
  function resolveRepo(root, repo) {
@@ -35924,7 +36706,7 @@ function resolveRepo(root, repo) {
35924
36706
  }
35925
36707
  async function git(cwd2, args) {
35926
36708
  try {
35927
- const { stdout } = await run6("git", args, {
36709
+ const { stdout } = await run7("git", args, {
35928
36710
  cwd: cwd2,
35929
36711
  timeout: 15e3,
35930
36712
  maxBuffer: 8 * 1024 * 1024
@@ -35950,13 +36732,13 @@ async function gitRepos(root) {
35950
36732
  };
35951
36733
  await add(base);
35952
36734
  try {
35953
- const lvl1 = await readdir5(base, { withFileTypes: true });
36735
+ const lvl1 = await readdir6(base, { withFileTypes: true });
35954
36736
  for (const d of lvl1) {
35955
36737
  if (!d.isDirectory() || d.name === ".git" || d.name === "node_modules") continue;
35956
36738
  const p1 = resolve6(base, d.name);
35957
36739
  await add(p1);
35958
36740
  try {
35959
- const lvl2 = await readdir5(p1, { withFileTypes: true });
36741
+ const lvl2 = await readdir6(p1, { withFileTypes: true });
35960
36742
  for (const d2 of lvl2) {
35961
36743
  if (!d2.isDirectory() || d2.name === ".git" || d2.name === "node_modules") continue;
35962
36744
  await add(resolve6(p1, d2.name));
@@ -36026,77 +36808,77 @@ async function gitCommitDiff(root, repo, hash, path) {
36026
36808
  }
36027
36809
 
36028
36810
  // src/core/git-commit-identity.ts
36029
- import { execFile as execFile7 } from "child_process";
36030
- import { promisify as promisify7 } from "util";
36031
- var run7 = promisify7(execFile7);
36811
+ import { execFile as execFile9 } from "child_process";
36812
+ import { promisify as promisify9 } from "util";
36813
+ var run8 = promisify9(execFile9);
36032
36814
  async function setCommitAuthor(cwd2, machineUsername) {
36033
36815
  if (!machineUsername) return;
36034
36816
  try {
36035
- 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 });
36036
36818
  } catch {
36037
36819
  }
36038
36820
  }
36039
36821
 
36040
36822
  // src/core/scaffold.ts
36041
- import { cp, mkdir as mkdir5, writeFile as writeFile6 } from "fs/promises";
36042
- import { execFile as execFile8 } from "child_process";
36043
- 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";
36044
36826
  import { fileURLToPath as fileURLToPath3 } from "url";
36045
- import { promisify as promisify8 } from "util";
36046
- var run8 = promisify8(execFile8);
36827
+ import { promisify as promisify10 } from "util";
36828
+ var run9 = promisify10(execFile10);
36047
36829
  function reposOf(spec) {
36048
36830
  const repos = spec?.repos;
36049
36831
  return Array.isArray(repos) ? repos : [];
36050
36832
  }
36051
36833
  function subDirName(repo) {
36052
- const raw = (repo.subdir || repo.role || repo.name || "repo").trim();
36834
+ const raw = (repo.subdir || repo.role || repoName(repo.url ?? "") || "repo").trim();
36053
36835
  return raw.replace(/[^\w.-]+/g, "-").replace(/^-+|-+$/g, "") || "repo";
36054
36836
  }
36055
36837
  async function provisionRepos(target, repos, emit, opts = {}) {
36056
36838
  for (const repo of repos) {
36057
36839
  if (repo.primary) {
36058
- if (opts.clonePrimary && repo.op === "existing" && repo.url) {
36840
+ if (opts.clonePrimary && repo.url) {
36059
36841
  emit("git", `Cloning ${repo.url}\u2026`);
36060
- await run8("git", ["clone", repo.url, target], { timeout: 12e4 });
36842
+ await run9("git", ["clone", repo.url, target], { timeout: 12e4 });
36061
36843
  continue;
36062
36844
  }
36063
36845
  emit("git", "Initializing the primary repo\u2026");
36064
- await run8("git", ["init"], { cwd: target, timeout: 2e4 });
36065
- if (repo.op === "existing" && repo.url) {
36066
- 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(
36067
36849
  () => void 0
36068
36850
  );
36069
36851
  }
36070
36852
  continue;
36071
36853
  }
36072
- const dir2 = join17(target, subDirName(repo));
36073
- if (repo.op === "existing" && repo.url) {
36854
+ const dir2 = join23(target, subDirName(repo));
36855
+ if (repo.url) {
36074
36856
  emit("git", `Cloning sub-repo ${repo.url}\u2026`);
36075
- await run8("git", ["clone", repo.url, dir2], { timeout: 12e4 });
36857
+ await run9("git", ["clone", repo.url, dir2], { timeout: 12e4 });
36076
36858
  } else {
36077
36859
  emit("git", `Initializing sub-repo ${subDirName(repo)}\u2026`);
36078
- await mkdir5(dir2, { recursive: true });
36079
- await run8("git", ["init"], { cwd: dir2, timeout: 2e4 });
36860
+ await mkdir8(dir2, { recursive: true });
36861
+ await run9("git", ["init"], { cwd: dir2, timeout: 2e4 });
36080
36862
  }
36081
36863
  }
36082
36864
  }
36083
36865
  function sampleDir() {
36084
36866
  if (process.env.SCAFFOLD_SAMPLE_DIR) return process.env.SCAFFOLD_SAMPLE_DIR;
36085
- const here = dirname6(fileURLToPath3(import.meta.url));
36867
+ const here = dirname8(fileURLToPath3(import.meta.url));
36086
36868
  return resolve7(here, "../../project-sample");
36087
36869
  }
36088
36870
  async function scaffoldProject(payload, profileDir2, onProgress) {
36089
36871
  const emit = (step, message) => onProgress?.({ projectId: payload.projectId, step, message });
36090
36872
  try {
36091
36873
  const root = process.env.SCAFFOLD_ROOT ? resolve7(process.env.SCAFFOLD_ROOT) : resolve7(profileDir2);
36092
- const target = join17(root, payload.projectName);
36093
- await mkdir5(target, { recursive: true });
36874
+ const target = join23(root, payload.projectName);
36875
+ await mkdir8(target, { recursive: true });
36094
36876
  emit("copy", "Copying the sample template\u2026");
36095
36877
  await cp(sampleDir(), target, { recursive: true, force: false, errorOnExist: false });
36096
36878
  if (payload.spec) {
36097
36879
  emit("spec", "Writing project.spec.json\u2026");
36098
- await writeFile6(
36099
- join17(target, "project.spec.json"),
36880
+ await writeFile8(
36881
+ join23(target, "project.spec.json"),
36100
36882
  JSON.stringify(payload.spec, null, 2),
36101
36883
  "utf8"
36102
36884
  );
@@ -36114,7 +36896,7 @@ async function aiInit(target, spec, emit) {
36114
36896
  emit("ai-init", "AI init: subagents, README, CLAUDE.md\u2026");
36115
36897
  const subagents = Array.isArray(spec.subagents) ? spec.subagents : [];
36116
36898
  if (subagents.length > 0) {
36117
- await mkdir5(join17(target, ".claude", "agents"), { recursive: true });
36899
+ await mkdir8(join23(target, ".claude", "agents"), { recursive: true });
36118
36900
  for (const sa of subagents) {
36119
36901
  const name = (sa.name || "").trim().replace(/[^\w.-]+/g, "-").replace(/^-+|-+$/g, "");
36120
36902
  if (!name) continue;
@@ -36124,17 +36906,17 @@ name: ${name}
36124
36906
 
36125
36907
  ${(sa.description || "").trim()}
36126
36908
  `;
36127
- await writeFile6(join17(target, ".claude", "agents", `${name}.md`), body, "utf8");
36909
+ await writeFile8(join23(target, ".claude", "agents", `${name}.md`), body, "utf8");
36128
36910
  }
36129
36911
  }
36130
36912
  const specJson = JSON.stringify(spec);
36131
36913
  await generateFile(
36132
- join17(target, "README.md"),
36914
+ join23(target, "README.md"),
36133
36915
  `Write a concise README.md (Markdown only, no preamble) for this project from its spec JSON:
36134
36916
  ${specJson}`
36135
36917
  );
36136
36918
  await generateFile(
36137
- join17(target, "CLAUDE.md"),
36919
+ join23(target, "CLAUDE.md"),
36138
36920
  `Write a CLAUDE.md (Markdown only, no preamble) with guidance/conventions for AI agents working in this project, derived from its spec JSON:
36139
36921
  ${specJson}`
36140
36922
  );
@@ -36142,7 +36924,7 @@ ${specJson}`
36142
36924
  async function generateFile(path, prompt) {
36143
36925
  try {
36144
36926
  const text = await aiGenerate(prompt);
36145
- if (text.trim()) await writeFile6(path, text.trim() + "\n", "utf8");
36927
+ if (text.trim()) await writeFile8(path, text.trim() + "\n", "utf8");
36146
36928
  } catch {
36147
36929
  }
36148
36930
  }
@@ -36150,8 +36932,8 @@ async function addProject(payload, profileDir2, onProgress) {
36150
36932
  const emit = (step, message) => onProgress?.({ projectId: payload.projectId, step, message });
36151
36933
  try {
36152
36934
  const root = process.env.SCAFFOLD_ROOT ? resolve7(process.env.SCAFFOLD_ROOT) : resolve7(profileDir2);
36153
- const target = join17(root, payload.projectName);
36154
- await mkdir5(target, { recursive: true });
36935
+ const target = join23(root, payload.projectName);
36936
+ await mkdir8(target, { recursive: true });
36155
36937
  const repos = payload.repos ?? [];
36156
36938
  if (repos.length > 0) await provisionRepos(target, repos, emit, { clonePrimary: true });
36157
36939
  onProgress?.({ projectId: payload.projectId, step: "done", message: "Project added.", done: true });
@@ -36162,14 +36944,14 @@ async function addProject(payload, profileDir2, onProgress) {
36162
36944
  }
36163
36945
 
36164
36946
  // src/core/config-sync.ts
36165
- import { existsSync as existsSync8, readFileSync as readFileSync13 } from "fs";
36166
- import { join as join18 } from "path";
36947
+ import { existsSync as existsSync9, readFileSync as readFileSync14 } from "fs";
36948
+ import { join as join24 } from "path";
36167
36949
  var SERVER_MANAGED_KEYS = ["physicPath", "sessionSwitchPct", "perPromptTokenLimit"];
36168
36950
  function readConfigText(profileDir2) {
36169
- const path = join18(profileDir2, "config.json");
36170
- if (existsSync8(path)) {
36951
+ const path = join24(profileDir2, "config.json");
36952
+ if (existsSync9(path)) {
36171
36953
  try {
36172
- return readFileSync13(path, "utf8");
36954
+ return readFileSync14(path, "utf8");
36173
36955
  } catch {
36174
36956
  }
36175
36957
  }
@@ -36199,6 +36981,7 @@ function applyConfigText(profileDir2, text) {
36199
36981
  var HEARTBEAT_MS = 3e4;
36200
36982
  var USAGE_POLL_MS = 5 * 6e4;
36201
36983
  var LOG_UPLOAD_MS = 10 * 6e4;
36984
+ var KB_REFRESH_MS = 24 * 60 * 6e4;
36202
36985
  var RECONNECT_BACKOFF_BASE_MS = 1e3;
36203
36986
  var RECONNECT_MAX_BACKOFF_DEFAULT_SEC = 60;
36204
36987
  var RECONNECT_BACKOFF_MAX_ATTEMPT = 6;
@@ -36245,11 +37028,17 @@ var WsClient = class {
36245
37028
  heartbeat = null;
36246
37029
  usageTimer = null;
36247
37030
  logUploadTimer = null;
37031
+ kbRefreshTimer = null;
36248
37032
  /** Signature (fileName + totalBytes) of the last log upload — skip re-sending if unchanged. */
36249
37033
  lastLogSig = null;
36250
37034
  usageSnapshot = null;
36251
37035
  /** Whether the serving project requires outbound review before each AI spawn (ADR-0082). */
36252
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;
36253
37042
  stopped = false;
36254
37043
  /** Resolver to wake the backoff sleep early (set while waiting) — for /reconnect. */
36255
37044
  wakeReconnect = null;
@@ -36453,6 +37242,9 @@ var WsClient = class {
36453
37242
  perPromptTokenLimit: token.projectTokens?.perPromptTokenLimit ?? 0
36454
37243
  });
36455
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);
36456
37248
  this.machineUsername = token.machineUsername ?? "";
36457
37249
  if (token.scope && token.scope !== this.credential.scope) {
36458
37250
  this.credential.scope = token.scope;
@@ -36531,6 +37323,7 @@ var WsClient = class {
36531
37323
  this.startHeartbeat();
36532
37324
  this.startUsagePolling();
36533
37325
  this.startLogUpload();
37326
+ this.startKbRefresh();
36534
37327
  this.connectWaiters.splice(0).forEach((fn) => fn());
36535
37328
  return;
36536
37329
  }
@@ -36592,8 +37385,8 @@ var WsClient = class {
36592
37385
  const oldRoot = this.physicFolderPath(sync.oldName);
36593
37386
  if (oldRoot) void uninstallCron(oldRoot).catch(() => void 0);
36594
37387
  }
36595
- if (sync.oldName) rmSync4(join19(this.context.profileDir, sync.oldName), { recursive: true, force: true });
36596
- 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 });
36597
37390
  this.physicRoot = this.physicFolderPath(sync.newName);
36598
37391
  this.bus.setProject(sync.newName);
36599
37392
  this.bus.log(`Physic project folder synced \u2192 ${sync.newName}`);
@@ -36604,9 +37397,10 @@ var WsClient = class {
36604
37397
  if (del.name) {
36605
37398
  const delRoot = this.physicFolderPath(del.name);
36606
37399
  if (delRoot) void uninstallCron(delRoot).catch(() => void 0);
36607
- rmSync4(join19(this.context.profileDir, del.name), { recursive: true, force: true });
37400
+ rmSync5(join25(this.context.profileDir, del.name), { recursive: true, force: true });
36608
37401
  this.bus.log(`Physic project folder deleted (project removed) \u2192 ${del.name}`);
36609
37402
  }
37403
+ void manageSshKey("delete", this.context.profileDir).catch(() => void 0);
36610
37404
  this.bus.setProject(null);
36611
37405
  this.physicRoot = null;
36612
37406
  break;
@@ -36676,6 +37470,61 @@ var WsClient = class {
36676
37470
  );
36677
37471
  break;
36678
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
+ }
36679
37528
  case WsChannels.AUTONOMOUS_READ:
36680
37529
  if (this.physicRoot) {
36681
37530
  void readAutonomous(this.physicRoot).then(
@@ -36742,6 +37591,58 @@ var WsClient = class {
36742
37591
  }
36743
37592
  break;
36744
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
+ }
36745
37646
  case WsChannels.GRAPH_BUILD: {
36746
37647
  const req = payload;
36747
37648
  if (this.physicRoot) {
@@ -36813,6 +37714,13 @@ var WsClient = class {
36813
37714
  (reply) => this.send(WsChannels.GIT_REPOS, reply, message.id)
36814
37715
  );
36815
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
+ }
36816
37724
  case WsChannels.GIT_LOG: {
36817
37725
  const req = payload;
36818
37726
  void gitLog(this.physicRoot, req.repo ?? "", req.skip ?? 0, req.limit ?? 50).then(
@@ -36836,7 +37744,7 @@ var WsClient = class {
36836
37744
  }
36837
37745
  case WsChannels.LOG_READ: {
36838
37746
  const limit = Math.min(Math.max(payload.limit ?? 200, 1), 2e3);
36839
- const lines = readRecentLogLines(join19(this.context.profileDir, "logs"), limit);
37747
+ const lines = readRecentLogLines(join25(this.context.profileDir, "logs"), limit);
36840
37748
  this.send(WsChannels.LOG_READ, { lines }, message.id);
36841
37749
  break;
36842
37750
  }
@@ -36939,9 +37847,13 @@ var WsClient = class {
36939
37847
  } else {
36940
37848
  this.bus.log("No project attached \u2014 running in the current directory.", "warn");
36941
37849
  }
36942
- const cmd = config.aiCli ?? "claude";
36943
- const workingDir = getWorkingProfile(this.context.profileDir, cmd);
36944
- 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;
36945
37857
  if (startDir && startDir !== workingDir) {
36946
37858
  this.bus.push({
36947
37859
  source: origin,
@@ -36949,7 +37861,19 @@ var WsClient = class {
36949
37861
  text: `session ${this.usageSnapshot?.session.utilizationPct ?? 0}% \u2265 ${config.sessionSwitchPct}% \u2014 switching to Claude profile "${profileDisplayLabel(startDir)}"`
36950
37862
  });
36951
37863
  }
36952
- 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
+ );
36953
37877
  const markerArgs = plan.attempts[0]?.args ?? [];
36954
37878
  this.bus.push({ source: origin, kind: "aireq", text: `${plan.cmd} \u2039 ${prompt}` });
36955
37879
  logger.info("command.ai", { commandId, origin, cmd: plan.cmd, profiles: plan.attempts.length });
@@ -37015,7 +37939,11 @@ var WsClient = class {
37015
37939
  this.bus.endBusy(plan.cmd);
37016
37940
  const profileLabel = result.workedDir ? profileDisplayLabel(result.workedDir) : null;
37017
37941
  if (result.workedDir) {
37018
- 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
+ }
37019
37947
  this.bus.setActiveProfile(profileLabel ?? plan.cmd);
37020
37948
  }
37021
37949
  this.send(WsChannels.COMMAND_OUTPUT, {
@@ -37058,7 +37986,9 @@ var WsClient = class {
37058
37986
  inputTokens: result.usage.input,
37059
37987
  outputTokens: result.usage.output,
37060
37988
  cacheReadTokens: result.usage.cacheRead,
37061
- 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)
37062
37992
  });
37063
37993
  this.bus.addTokens?.(result.usage.tokens);
37064
37994
  }
@@ -37089,6 +38019,22 @@ var WsClient = class {
37089
38019
  const next = dirs[(currentIndex + 1) % dirs.length];
37090
38020
  return next ?? null;
37091
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
+ }
37092
38038
  /**
37093
38039
  * Reject a prompt whose estimated tokens exceed the project's per-prompt limit (ADR-0081)
37094
38040
  * without spawning: surface it in the transcript + close the command on the server with a
@@ -37332,8 +38278,8 @@ var WsClient = class {
37332
38278
  /** Create the physic folder at an absolute path if missing (best-effort). */
37333
38279
  ensurePhysicFolderPath(folder) {
37334
38280
  try {
37335
- if (!existsSync9(folder)) {
37336
- mkdirSync4(folder, { recursive: true });
38281
+ if (!existsSync10(folder)) {
38282
+ mkdirSync5(folder, { recursive: true });
37337
38283
  this.bus.log(`Created physic project folder: ${folder}`);
37338
38284
  }
37339
38285
  } catch (err) {
@@ -37347,7 +38293,7 @@ var WsClient = class {
37347
38293
  */
37348
38294
  physicFolderPath(projectName) {
37349
38295
  const safe = projectName.replace(/[/\\]/g, "_").replace(/\.\./g, "_").trim();
37350
- return safe ? join19(this.context.profileDir, safe) : null;
38296
+ return safe ? join25(this.context.profileDir, safe) : null;
37351
38297
  }
37352
38298
  /**
37353
38299
  * Report machine.status: version + the state of the physic project being served
@@ -37361,7 +38307,7 @@ var WsClient = class {
37361
38307
  projects: [],
37362
38308
  version: CLI_VERSION,
37363
38309
  physicPath,
37364
- physicPathExists: physicPath ? existsSync9(physicPath) : void 0,
38310
+ physicPathExists: physicPath ? existsSync10(physicPath) : void 0,
37365
38311
  // Real coarse state (ADR-0152): !paused and a recent tick (was hard-coded false).
37366
38312
  autonomousRunning: physicPath ? isAutonomousRunning(physicPath) : false
37367
38313
  });
@@ -37382,6 +38328,8 @@ var WsClient = class {
37382
38328
  this.usageTimer = null;
37383
38329
  if (this.logUploadTimer) clearInterval(this.logUploadTimer);
37384
38330
  this.logUploadTimer = null;
38331
+ if (this.kbRefreshTimer) clearInterval(this.kbRefreshTimer);
38332
+ this.kbRefreshTimer = null;
37385
38333
  }
37386
38334
  /**
37387
38335
  * Poll the Claude subscription usage API and push a `machine.usage` snapshot (ADR-0072).
@@ -37392,7 +38340,7 @@ var WsClient = class {
37392
38340
  if (!this.sessionKey) return;
37393
38341
  try {
37394
38342
  const config = readProfileConfig(this.context.profileDir);
37395
- const aiCli2 = config.aiCli ?? "claude";
38343
+ const aiCli2 = config.aiCli || "claude";
37396
38344
  const working = getWorkingProfile(this.context.profileDir, aiCli2);
37397
38345
  const configured = claudeHomeDirs(config);
37398
38346
  const dirs = working ? [working, ...configured.filter((d) => d !== working)] : configured;
@@ -37426,6 +38374,20 @@ var WsClient = class {
37426
38374
  this.logUploadTimer.unref?.();
37427
38375
  this.uploadLog();
37428
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
+ }
37429
38391
  /**
37430
38392
  * Upload the newest cli log file + the total log footprint to the server (ADR-0122), so it is
37431
38393
  * stored + retained server-side and counted in the machine user's storage. Skips when nothing
@@ -37433,7 +38395,7 @@ var WsClient = class {
37433
38395
  */
37434
38396
  uploadLog() {
37435
38397
  try {
37436
- const upload = readLogUpload(join19(this.context.profileDir, "logs"));
38398
+ const upload = readLogUpload(join25(this.context.profileDir, "logs"));
37437
38399
  if (!upload) return;
37438
38400
  const sig = `${upload.fileName}:${upload.totalBytes}`;
37439
38401
  if (sig === this.lastLogSig) return;
@@ -37461,10 +38423,10 @@ var WsClient = class {
37461
38423
  };
37462
38424
 
37463
38425
  // src/core/instance-lock.ts
37464
- import { readFileSync as readFileSync14, rmSync as rmSync5, writeFileSync as writeFileSync6 } from "fs";
37465
- 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";
37466
38428
  function lockPath(profileDir2) {
37467
- return join20(profileDir2, "start.lock");
38429
+ return join26(profileDir2, "start.lock");
37468
38430
  }
37469
38431
  function isAlive(pid) {
37470
38432
  try {
@@ -37476,7 +38438,7 @@ function isAlive(pid) {
37476
38438
  }
37477
38439
  function lockHolder(profileDir2) {
37478
38440
  try {
37479
- const pid = Number.parseInt(readFileSync14(lockPath(profileDir2), "utf8").trim(), 10);
38441
+ const pid = Number.parseInt(readFileSync15(lockPath(profileDir2), "utf8").trim(), 10);
37480
38442
  if (Number.isInteger(pid) && pid > 0 && isAlive(pid)) return pid;
37481
38443
  } catch {
37482
38444
  }
@@ -37484,13 +38446,13 @@ function lockHolder(profileDir2) {
37484
38446
  }
37485
38447
  function acquireInstanceLock(profileDir2) {
37486
38448
  if (lockHolder(profileDir2) !== null) return false;
37487
- writeFileSync6(lockPath(profileDir2), String(process.pid), "utf8");
38449
+ writeFileSync8(lockPath(profileDir2), String(process.pid), "utf8");
37488
38450
  return true;
37489
38451
  }
37490
38452
  function releaseInstanceLock(profileDir2) {
37491
38453
  try {
37492
- const pid = Number.parseInt(readFileSync14(lockPath(profileDir2), "utf8").trim(), 10);
37493
- 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 });
37494
38456
  } catch {
37495
38457
  }
37496
38458
  }
@@ -37577,6 +38539,16 @@ var SessionBus = class {
37577
38539
  log(text, level = "info") {
37578
38540
  this.push({ source: "system", kind: "log", text, level });
37579
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
+ }
37580
38552
  /** Producer → consumers: update the connection status. */
37581
38553
  setStatus(status) {
37582
38554
  this.currentStatus = status;
@@ -37602,6 +38574,15 @@ var SessionBus = class {
37602
38574
  else this.busyStack.pop();
37603
38575
  this.emitter.emit("busy", this.busy);
37604
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
+ }
37605
38586
  /** Subscribe to processing-state changes. */
37606
38587
  onBusy(fn) {
37607
38588
  this.emitter.on("busy", fn);
@@ -37684,6 +38665,11 @@ var SessionBus = class {
37684
38665
  this.currentSessionTokens += n;
37685
38666
  this.emitter.emit("tokens", this.currentSessionTokens);
37686
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
+ }
37687
38673
  /** Subscribe to session-token changes. */
37688
38674
  onTokens(fn) {
37689
38675
  this.emitter.on("tokens", fn);
@@ -37725,6 +38711,112 @@ var SessionBus = class {
37725
38711
  }
37726
38712
  };
37727
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
+
37728
38820
  // src/ui/console-sink.ts
37729
38821
  function attachConsoleSink(bus) {
37730
38822
  bus.onTranscript((entry) => {
@@ -43360,17 +44452,17 @@ var import_react21 = __toESM(require_react(), 1);
43360
44452
  var import_react26 = __toESM(require_react(), 1);
43361
44453
 
43362
44454
  // src/core/input-history.ts
43363
- import { existsSync as existsSync11, readFileSync as readFileSync16, writeFileSync as writeFileSync7 } from "fs";
43364
- 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";
43365
44457
  var MAX_HISTORY = 200;
43366
44458
  function historyPath(profileDir2) {
43367
- return join21(profileDir2, "input-history.json");
44459
+ return join28(profileDir2, "input-history.json");
43368
44460
  }
43369
44461
  function loadInputHistory(profileDir2) {
43370
44462
  const path = historyPath(profileDir2);
43371
- if (!existsSync11(path)) return [];
44463
+ if (!existsSync12(path)) return [];
43372
44464
  try {
43373
- const data = JSON.parse(readFileSync16(path, "utf8"));
44465
+ const data = JSON.parse(readFileSync17(path, "utf8"));
43374
44466
  return Array.isArray(data) ? data.filter((x) => typeof x === "string") : [];
43375
44467
  } catch {
43376
44468
  return [];
@@ -43381,7 +44473,7 @@ function appendInputHistory(profileDir2, line, current) {
43381
44473
  if (!trimmed || current[current.length - 1] === trimmed) return current;
43382
44474
  const next = [...current, trimmed].slice(-MAX_HISTORY);
43383
44475
  try {
43384
- writeFileSync7(historyPath(profileDir2), JSON.stringify(next), "utf8");
44476
+ writeFileSync9(historyPath(profileDir2), JSON.stringify(next), "utf8");
43385
44477
  } catch {
43386
44478
  }
43387
44479
  return next;
@@ -43630,8 +44722,8 @@ function TranscriptLine({ line }) {
43630
44722
  var import_react24 = __toESM(require_react(), 1);
43631
44723
 
43632
44724
  // src/ui/slash-commands.ts
43633
- import { existsSync as existsSync12 } from "fs";
43634
- import { join as join22 } from "path";
44725
+ import { existsSync as existsSync13 } from "fs";
44726
+ import { join as join29 } from "path";
43635
44727
  var CONFIG_KEYS = [
43636
44728
  "aiCli",
43637
44729
  "claudeHome",
@@ -43643,7 +44735,7 @@ var CONFIG_KEYS = [
43643
44735
  "aiEnv"
43644
44736
  ];
43645
44737
  function configPath(profileDir2) {
43646
- return join22(profileDir2, "config.json");
44738
+ return join29(profileDir2, "config.json");
43647
44739
  }
43648
44740
  function parseConfigAssignment(key, valueTokens) {
43649
44741
  const value = valueTokens.join(" ").trim();
@@ -43693,7 +44785,7 @@ function runConfig(ctx) {
43693
44785
  ctx.print(`\u2714 ${verb} default config: ${path}`);
43694
44786
  ctx.print("add claude profiles: /config set claudeHome .claude .claude-1 (tried in order)");
43695
44787
  };
43696
- if (existsSync12(path)) {
44788
+ if (existsSync13(path)) {
43697
44789
  ctx.confirm(
43698
44790
  `config exists: ${path} \u2014 replace it with defaults? (y/N)`,
43699
44791
  () => writeDefaults("replaced with")
@@ -43778,7 +44870,7 @@ function runOutput(ctx) {
43778
44870
  }
43779
44871
  function runLogs(ctx) {
43780
44872
  const n = Math.max(1, Math.min(200, Number(ctx.args[0]) || 20));
43781
- const dir2 = join22(ctx.info.profileDir, "logs");
44873
+ const dir2 = join29(ctx.info.profileDir, "logs");
43782
44874
  const lines = readRecentLogLines(dir2, n);
43783
44875
  if (lines.length === 0) {
43784
44876
  ctx.print("No log lines yet.");
@@ -44056,7 +45148,7 @@ var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u282
44056
45148
  function Spinner() {
44057
45149
  const [index, setIndex] = (0, import_react25.useState)(0);
44058
45150
  (0, import_react25.useEffect)(() => {
44059
- const timer = setInterval(() => setIndex((i) => (i + 1) % FRAMES.length), 90);
45151
+ const timer = setInterval(() => setIndex((i) => (i + 1) % FRAMES.length), 120);
44060
45152
  return () => clearInterval(timer);
44061
45153
  }, []);
44062
45154
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Text, { color: "yellow", children: FRAMES[index] });
@@ -44066,6 +45158,7 @@ function Spinner() {
44066
45158
  var import_jsx_runtime5 = __toESM(require_jsx_runtime(), 1);
44067
45159
  var CHROME_ROWS = 12;
44068
45160
  var ENTRY_CAP = 3e3;
45161
+ var UPDATE_FLUSH_MS = 66;
44069
45162
  function Rule({ width }) {
44070
45163
  return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Text, { dimColor: true, children: "\u2500".repeat(Math.max(10, width)) });
44071
45164
  }
@@ -44113,6 +45206,16 @@ function App2({
44113
45206
  entriesLenRef.current = entries2.length;
44114
45207
  const pendingConfirmRef = (0, import_react26.useRef)(pendingConfirm);
44115
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
+ }, []);
44116
45219
  const clearTranscript = (0, import_react26.useCallback)(() => {
44117
45220
  setEntries([]);
44118
45221
  setScrollBack(0);
@@ -44127,9 +45230,12 @@ function App2({
44127
45230
  if (entry.source !== "system") lastActivityRef.current = Date.now();
44128
45231
  if (entry.kind === "aireq" || entry.kind === "aires") setExpanded(/* @__PURE__ */ new Set());
44129
45232
  });
44130
- const offUpdate = bus.onTranscriptUpdate(
44131
- (updated) => setEntries((prev) => prev.map((e) => e.id === updated.id ? { ...e, text: updated.text } : e))
44132
- );
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
+ });
44133
45239
  const offStatus = bus.onStatus(setStatus);
44134
45240
  const offBusy = bus.onBusy(setBusy);
44135
45241
  const offProfile = bus.onActiveProfile(setActiveProfile);
@@ -44149,8 +45255,13 @@ function App2({
44149
45255
  offProject();
44150
45256
  offUsage();
44151
45257
  offTokens();
45258
+ if (flushTimerRef.current !== null) {
45259
+ clearTimeout(flushTimerRef.current);
45260
+ flushTimerRef.current = null;
45261
+ }
45262
+ flushUpdates();
44152
45263
  };
44153
- }, [bus]);
45264
+ }, [bus, flushUpdates]);
44154
45265
  (0, import_react26.useEffect)(() => {
44155
45266
  if (autoClearIdleMs <= 0) return;
44156
45267
  const timer = setInterval(() => {
@@ -44166,7 +45277,8 @@ function App2({
44166
45277
  }, 6e4);
44167
45278
  return () => clearInterval(timer);
44168
45279
  }, [autoClearIdleMs, bus, clearTranscript]);
44169
- 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);
44170
45282
  const textWidth = Math.max(10, cols - 1 - 8);
44171
45283
  const lines = (0, import_react26.useMemo)(() => flattenEntries(entries2, textWidth, expanded), [entries2, textWidth, expanded]);
44172
45284
  const maxBlock = (0, import_react26.useMemo)(() => lines.reduce((m, l) => Math.max(m, l.blockIndex ?? 0), 0), [lines]);
@@ -44234,7 +45346,7 @@ function App2({
44234
45346
  if (key.pageUp) setScrollBack((b) => Math.min(maxScroll, b + bodyHeight));
44235
45347
  else if (key.pageDown) setScrollBack((b) => Math.max(0, b - bodyHeight));
44236
45348
  });
44237
- 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: [
44238
45350
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Banner, { info, scope, worker, project, status, activeProfile, usage, sessionTokens }),
44239
45351
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Rule, { width }),
44240
45352
  /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(Box_default, { flexDirection: "column", height: bodyHeight, overflow: "hidden", children: [
@@ -44279,7 +45391,7 @@ async function runStart(profileDir2, profileName) {
44279
45391
  return;
44280
45392
  }
44281
45393
  logger.configure({
44282
- dir: join23(profileDir2, "logs"),
45394
+ dir: join30(profileDir2, "logs"),
44283
45395
  level: resolveLogLevel(),
44284
45396
  base: { version: CLI_VERSION, profile: profileName, scope: credential.scope }
44285
45397
  });
@@ -44299,7 +45411,7 @@ async function runStart(profileDir2, profileName) {
44299
45411
  if (result.action === "updated") {
44300
45412
  logger.info("update.updated", { version: result.version });
44301
45413
  console.log("Restarting with the new version\u2026");
44302
- const child = spawn6(process.execPath, process.argv.slice(1), {
45414
+ const child = spawn8(process.execPath, process.argv.slice(1), {
44303
45415
  stdio: "inherit",
44304
45416
  env: { ...process.env, FOURPM_NO_UPDATE: "1" }
44305
45417
  });
@@ -44314,6 +45426,7 @@ async function runStart(profileDir2, profileName) {
44314
45426
  return;
44315
45427
  }
44316
45428
  const release = () => releaseInstanceLock(profileDir2);
45429
+ let stopControl = null;
44317
45430
  process.once("exit", release);
44318
45431
  process.once("SIGINT", () => process.exit(0));
44319
45432
  process.once("SIGTERM", () => process.exit(0));
@@ -44327,6 +45440,14 @@ async function runStart(profileDir2, profileName) {
44327
45440
  profileDir: profileDir2,
44328
45441
  bus
44329
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
+ });
44330
45451
  const runResilient = async () => {
44331
45452
  for (; ; ) {
44332
45453
  try {
@@ -44350,10 +45471,10 @@ async function runStart(profileDir2, profileName) {
44350
45471
  profileDir: profileDir2,
44351
45472
  serverUrl: credential.serverUrl,
44352
45473
  physicPath: config.physicPath ?? null,
44353
- aiCli: config.aiCli ?? "claude",
45474
+ aiCli: config.aiCli || "claude",
44354
45475
  whoami: () => fetchWhoami(credential.serverUrl, credential.hashcode3).catch(() => null)
44355
45476
  };
44356
- const aiCli2 = config.aiCli ?? "claude";
45477
+ const aiCli2 = config.aiCli || "claude";
44357
45478
  const workingDir = getWorkingProfile(profileDir2, aiCli2);
44358
45479
  const labels = profileLabels(config);
44359
45480
  bus.setActiveProfile(workingDir ? profileDisplayLabel(workingDir) : labels[0] ?? null);
@@ -44367,12 +45488,144 @@ async function runStart(profileDir2, profileName) {
44367
45488
  await runResilient();
44368
45489
  }
44369
45490
  } finally {
45491
+ stopControl?.();
44370
45492
  release();
44371
45493
  }
44372
45494
  }
44373
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
+
44374
45627
  // src/commands/unlink.ts
44375
- import { rmSync as rmSync6 } from "fs";
45628
+ import { rmSync as rmSync8 } from "fs";
44376
45629
  async function runUnlink(profileDir2, profileName) {
44377
45630
  const cred = readCredential(profileDir2, profileName);
44378
45631
  if (!cred) {
@@ -44387,7 +45640,7 @@ async function runUnlink(profileDir2, profileName) {
44387
45640
  `\u26A0 Could not reach the server to revoke \u2014 revoke it at web /machines. (${err.message})`
44388
45641
  );
44389
45642
  }
44390
- rmSync6(profileDir2, { recursive: true, force: true });
45643
+ rmSync8(profileDir2, { recursive: true, force: true });
44391
45644
  clearDefaultProfileIf(profileName);
44392
45645
  console.log("\u2714 Deleted the profile and its physic project folder.");
44393
45646
  }
@@ -44522,10 +45775,16 @@ async function main() {
44522
45775
  switch (command) {
44523
45776
  case "link": {
44524
45777
  const serverUrl = flagValue(rest, "server") || process.env.FOURPM_SERVER || "http://localhost:42001";
44525
- await runLink(serverUrl.replace(/\/$/, ""), explicitProfile);
45778
+ const pairToken = flagValue(rest, "token") || process.env.FOURPM_PAIR_TOKEN || null;
45779
+ await runLink(serverUrl.replace(/\/$/, ""), explicitProfile, pairToken);
44526
45780
  break;
44527
45781
  }
44528
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
+ }
44529
45788
  const name = await pickLinkedProfile(explicitProfile, "start");
44530
45789
  if (!name) {
44531
45790
  process.exitCode = 1;
@@ -44534,6 +45793,15 @@ async function main() {
44534
45793
  await runStart(profileDir(name), name);
44535
45794
  break;
44536
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
+ }
44537
45805
  case "unlink": {
44538
45806
  const name = await pickLinkedProfile(explicitProfile, "unlink");
44539
45807
  if (!name) {
@@ -44564,6 +45832,7 @@ async function main() {
44564
45832
  "Usage:",
44565
45833
  " 4pm link Pair with the server (hashcode 1\u21922\u21923)",
44566
45834
  " 4pm start Connect WS, receive commands from the server",
45835
+ " 4pm attach Open the TUI against a running headless daemon (ADR-0192)",
44567
45836
  " 4pm unlink Delete a link (pick a profile if several)",
44568
45837
  " 4pm version Show the installed version (+ latest from the server)",
44569
45838
  " 4pm update Update to the latest version now",