@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.
- package/README.md +16 -0
- package/dist/index.js +1524 -255
- 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:
|
|
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 =
|
|
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:
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
//
|
|
28279
|
-
//
|
|
28280
|
-
//
|
|
28281
|
-
// injected (ADR-0158, ADR-0072), so
|
|
28282
|
-
// `model` = the CLI's `--model` flag; default
|
|
28283
|
-
// (
|
|
28284
|
-
|
|
28285
|
-
|
|
28286
|
-
|
|
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
|
-
|
|
28454
|
-
|
|
28455
|
-
|
|
28456
|
-
|
|
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
|
-
|
|
28460
|
-
|
|
28461
|
-
|
|
28462
|
-
|
|
28463
|
-
|
|
28464
|
-
|
|
28465
|
-
|
|
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
|
|
28498
|
-
import { join as
|
|
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
|
|
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
|
-
/**
|
|
33442
|
-
|
|
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
|
|
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
|
|
34306
|
-
const hour = Number.parseInt(
|
|
34307
|
-
const dateKey = `${
|
|
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(
|
|
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,
|
|
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)
|
|
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
|
|
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
|
|
34638
|
-
const
|
|
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(
|
|
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/
|
|
35167
|
-
import {
|
|
35168
|
-
import {
|
|
35169
|
-
import {
|
|
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
|
|
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
|
|
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 =
|
|
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(
|
|
35226
|
-
const hist = parseJson(await readText(
|
|
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(
|
|
35679
|
+
const s = JSON.parse(readFileSync13(join13(root, SETTINGS_REL), "utf8"));
|
|
35244
35680
|
if (s.paused === true) return false;
|
|
35245
|
-
const h = JSON.parse(
|
|
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(
|
|
35257
|
-
readText(
|
|
35692
|
+
readText(join13(root, SETTINGS_REL)),
|
|
35693
|
+
readText(join13(root, APPROVALS_REL), "{}"),
|
|
35258
35694
|
getAutonomousStatus(root),
|
|
35259
|
-
...Object.values(BOOK_FILES).map((f) => readText(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
35344
|
-
return { name:
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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" ?
|
|
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(
|
|
35379
|
-
readText2(
|
|
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(
|
|
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(
|
|
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
|
|
35404
|
-
import { join as
|
|
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
|
|
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
|
|
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(
|
|
35437
|
-
readText3(
|
|
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
|
|
36121
|
+
await writeFile5(join18(root, SECURITY_REL), req.content, "utf8");
|
|
35451
36122
|
} else if (req.kind === "placeholder") {
|
|
35452
|
-
await
|
|
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
|
|
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
|
|
35467
|
-
import { join as
|
|
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
|
|
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 =
|
|
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) =>
|
|
36218
|
+
const ids = new Set(files.map((f) => relative2(root, f).split("\\").join("/")));
|
|
35505
36219
|
const nodes = files.map((f) => {
|
|
35506
|
-
const rel =
|
|
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 =
|
|
36226
|
+
const rel = relative2(root, f).split("\\").join("/");
|
|
35513
36227
|
let text = "";
|
|
35514
36228
|
try {
|
|
35515
|
-
text = await
|
|
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 =
|
|
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 =
|
|
36265
|
+
const rel = relative2(root, f).split("\\").join("/");
|
|
35552
36266
|
let text = "";
|
|
35553
36267
|
try {
|
|
35554
|
-
text = await
|
|
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
|
|
35607
|
-
import { existsSync as
|
|
35608
|
-
import { execFile as
|
|
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
|
|
35611
|
-
import { promisify as
|
|
35612
|
-
var run3 =
|
|
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
|
|
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
|
|
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:
|
|
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 =
|
|
36425
|
+
const indexing = existsSync8(join21(root, RAG_REL, ".indexing"));
|
|
35712
36426
|
try {
|
|
35713
|
-
const m = JSON.parse(await
|
|
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
|
|
35781
|
-
await
|
|
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 (!
|
|
35787
|
-
if (
|
|
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 =
|
|
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 (!
|
|
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
|
-
[
|
|
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 (
|
|
36538
|
+
if (existsSync8(join21(root, RAG_REL, ".installing"))) return { ok: true, started: true };
|
|
35825
36539
|
try {
|
|
35826
|
-
await
|
|
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 =
|
|
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
|
|
35854
|
-
import { execFile as
|
|
35855
|
-
import { basename as basename3, dirname as
|
|
35856
|
-
import { promisify as
|
|
35857
|
-
var run4 =
|
|
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 =
|
|
36574
|
+
const dir2 = dirname7(target);
|
|
35861
36575
|
const base = basename3(target);
|
|
35862
36576
|
let newContent = "";
|
|
35863
36577
|
try {
|
|
35864
|
-
newContent = await
|
|
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
|
|
35882
|
-
import { promisify as
|
|
35883
|
-
var run5 =
|
|
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
|
|
35913
|
-
import { execFile as
|
|
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
|
|
35916
|
-
var
|
|
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
|
|
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
|
|
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
|
|
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
|
|
36030
|
-
import { promisify as
|
|
36031
|
-
var
|
|
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
|
|
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
|
|
36042
|
-
import { execFile as
|
|
36043
|
-
import { dirname as
|
|
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
|
|
36046
|
-
var
|
|
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.
|
|
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.
|
|
36840
|
+
if (opts.clonePrimary && repo.url) {
|
|
36059
36841
|
emit("git", `Cloning ${repo.url}\u2026`);
|
|
36060
|
-
await
|
|
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
|
|
36065
|
-
if (repo.
|
|
36066
|
-
await
|
|
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 =
|
|
36073
|
-
if (repo.
|
|
36854
|
+
const dir2 = join23(target, subDirName(repo));
|
|
36855
|
+
if (repo.url) {
|
|
36074
36856
|
emit("git", `Cloning sub-repo ${repo.url}\u2026`);
|
|
36075
|
-
await
|
|
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
|
|
36079
|
-
await
|
|
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 =
|
|
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 =
|
|
36093
|
-
await
|
|
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
|
|
36099
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
36154
|
-
await
|
|
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
|
|
36166
|
-
import { join as
|
|
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 =
|
|
36170
|
-
if (
|
|
36951
|
+
const path = join24(profileDir2, "config.json");
|
|
36952
|
+
if (existsSync9(path)) {
|
|
36171
36953
|
try {
|
|
36172
|
-
return
|
|
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)
|
|
36596
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
36943
|
-
const
|
|
36944
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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 (!
|
|
37336
|
-
|
|
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 ?
|
|
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 ?
|
|
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
|
|
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(
|
|
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
|
|
37465
|
-
import { join as
|
|
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
|
|
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(
|
|
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
|
-
|
|
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(
|
|
37493
|
-
if (pid === process.pid)
|
|
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
|
|
43364
|
-
import { join as
|
|
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
|
|
44459
|
+
return join28(profileDir2, "input-history.json");
|
|
43368
44460
|
}
|
|
43369
44461
|
function loadInputHistory(profileDir2) {
|
|
43370
44462
|
const path = historyPath(profileDir2);
|
|
43371
|
-
if (!
|
|
44463
|
+
if (!existsSync12(path)) return [];
|
|
43372
44464
|
try {
|
|
43373
|
-
const data = JSON.parse(
|
|
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
|
-
|
|
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
|
|
43634
|
-
import { join as
|
|
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
|
|
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 (
|
|
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 =
|
|
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),
|
|
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
|
-
|
|
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
|
|
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:
|
|
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:
|
|
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 =
|
|
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
|
|
45474
|
+
aiCli: config.aiCli || "claude",
|
|
44354
45475
|
whoami: () => fetchWhoami(credential.serverUrl, credential.hashcode3).catch(() => null)
|
|
44355
45476
|
};
|
|
44356
|
-
const aiCli2 = config.aiCli
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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",
|