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