@4pm/cli 1.17.0 → 1.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +848 -241
- package/dist/project-sample/.claude/AUTONOMOUS-CRON.md +16 -23
- package/dist/project-sample/.claude/AUTONOMOUS.md +31 -36
- package/dist/project-sample/.claude/hooks/autonomous-tick.sh +8 -349
- package/dist/project-sample/.claude/settings.json +1 -3
- package/dist/project-sample/.claude/templates/AI_TODO.empty.md +2 -2
- package/dist/project-sample/.claude/templates/README.md +1 -1
- package/dist/project-sample/.claude/templates/USER_QA.empty.md +13 -6
- package/dist/project-sample/.claude/templates/USER_QA.sample.md +6 -3
- package/dist/project-sample/.claude/templates/USER_TODO.empty.md +13 -6
- package/dist/project-sample/.claude/templates/USER_TODO.sample.md +7 -6
- package/dist/project-sample/.claude/templates/project.secrets.sample.json +0 -1
- package/dist/project-sample/AI_PLACEHOLDER.md +5 -5
- package/dist/project-sample/AI_TODO.md +2 -2
- package/dist/project-sample/USER_QA.md +13 -6
- package/dist/project-sample/USER_TODO.md +13 -6
- package/dist/project-sample/project.secrets.json.sample +0 -1
- package/package.json +1 -1
- package/dist/project-sample/.claude/.autonomous.settings.json +0 -24
- package/dist/project-sample/.claude/commands/auto-cycle.md +0 -177
- package/dist/project-sample/.claude/hooks/__pycache__/autonomous-history.cpython-312.pyc +0 -0
- package/dist/project-sample/.claude/hooks/autonomous-history.py +0 -193
- package/dist/project-sample/.claude/skills/check-usage/SKILL.md +0 -40
- package/dist/project-sample/.claude/skills/check-usage/check_usage.py +0 -161
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:
|
|
2286
|
+
var { randomBytes: randomBytes4, 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();
|
|
@@ -2821,7 +2821,7 @@ var require_websocket = __commonJS({
|
|
|
2821
2821
|
}
|
|
2822
2822
|
}
|
|
2823
2823
|
const defaultPort = isSecure ? 443 : 80;
|
|
2824
|
-
const key =
|
|
2824
|
+
const key = randomBytes4(16).toString("base64");
|
|
2825
2825
|
const request = isSecure ? https.request : http.request;
|
|
2826
2826
|
const protocolSet = /* @__PURE__ */ new Set();
|
|
2827
2827
|
let perMessageDeflate;
|
|
@@ -28199,7 +28199,7 @@ import { createInterface } from "readline/promises";
|
|
|
28199
28199
|
|
|
28200
28200
|
// src/core/credential.ts
|
|
28201
28201
|
import {
|
|
28202
|
-
chmodSync,
|
|
28202
|
+
chmodSync as chmodSync2,
|
|
28203
28203
|
existsSync as existsSync2,
|
|
28204
28204
|
readFileSync as readFileSync2,
|
|
28205
28205
|
rmSync as rmSync2,
|
|
@@ -28220,11 +28220,17 @@ import { homedir } from "os";
|
|
|
28220
28220
|
import { join } from "path";
|
|
28221
28221
|
|
|
28222
28222
|
// src/common/io/ensure-dir.ts
|
|
28223
|
-
import { mkdirSync } from "fs";
|
|
28223
|
+
import { chmodSync, mkdirSync } from "fs";
|
|
28224
28224
|
var CLI_IMAGE = "ghcr.io/4azuo/4pm-cli:full";
|
|
28225
|
-
function ensureDir(dir2) {
|
|
28225
|
+
function ensureDir(dir2, mode) {
|
|
28226
28226
|
try {
|
|
28227
|
-
mkdirSync(dir2, { recursive: true });
|
|
28227
|
+
mkdirSync(dir2, { recursive: true, ...mode !== void 0 ? { mode } : {} });
|
|
28228
|
+
if (mode !== void 0) {
|
|
28229
|
+
try {
|
|
28230
|
+
chmodSync(dir2, mode);
|
|
28231
|
+
} catch {
|
|
28232
|
+
}
|
|
28233
|
+
}
|
|
28228
28234
|
} catch (err) {
|
|
28229
28235
|
const code = err.code;
|
|
28230
28236
|
if (code === "EACCES" || code === "EPERM") {
|
|
@@ -28255,7 +28261,7 @@ function defaultProfileName() {
|
|
|
28255
28261
|
return DEFAULT_PROFILE;
|
|
28256
28262
|
}
|
|
28257
28263
|
function writeDefaultProfile(userId) {
|
|
28258
|
-
ensureDir(join(homedir(), ".4pm"));
|
|
28264
|
+
ensureDir(join(homedir(), ".4pm"), 448);
|
|
28259
28265
|
writeFileSync(defaultPointerPath(), userId, "utf8");
|
|
28260
28266
|
}
|
|
28261
28267
|
function clearDefaultProfileIf(name) {
|
|
@@ -28292,7 +28298,7 @@ function listProfiles() {
|
|
|
28292
28298
|
}
|
|
28293
28299
|
function profileDir(name) {
|
|
28294
28300
|
const dir2 = join(homedir(), ".4pm", "profiles", name);
|
|
28295
|
-
ensureDir(dir2);
|
|
28301
|
+
ensureDir(dir2, 448);
|
|
28296
28302
|
return dir2;
|
|
28297
28303
|
}
|
|
28298
28304
|
function defaultProfileConfig() {
|
|
@@ -28417,7 +28423,7 @@ function readCredential(profileDir2, profileName) {
|
|
|
28417
28423
|
function writeCredential(profileDir2, credential) {
|
|
28418
28424
|
const path = credentialPath(profileDir2);
|
|
28419
28425
|
writeFileSync2(path, JSON.stringify(credential, null, 2), "utf8");
|
|
28420
|
-
|
|
28426
|
+
chmodSync2(path, 384);
|
|
28421
28427
|
}
|
|
28422
28428
|
function deleteCredential(profileDir2) {
|
|
28423
28429
|
rmSync2(credentialPath(profileDir2), { force: true });
|
|
@@ -28873,7 +28879,12 @@ var en_default = {
|
|
|
28873
28879
|
"slash.claudeCmd.usage": "usage: /claude-cmd /context (forwards to the AI CLI)",
|
|
28874
28880
|
"start.connecting": "Connecting to {server} (profile: {profile})\u2026",
|
|
28875
28881
|
"start.recovered": "Recovered from an internal error \u2014 reconnecting: {message}",
|
|
28876
|
-
"attach.attached": 'Attached to profile "{profile}" \u2014 the worker runs headless; input is forwarded.'
|
|
28882
|
+
"attach.attached": 'Attached to profile "{profile}" \u2014 the worker runs headless; input is forwarded.',
|
|
28883
|
+
"autoRun.done": "Autonomous cycle complete.",
|
|
28884
|
+
"autoRun.failed": "Autonomous cycle failed to dispatch: {note}",
|
|
28885
|
+
"autoRun.noDaemon": 'No running daemon for profile "{profile}" \u2014 start it first: 4pm start --profile {profile}',
|
|
28886
|
+
"autoRun.timeout": "Autonomous cycle exceeded the time limit \u2014 aborting.",
|
|
28887
|
+
"autoRun.authFailed": "Control-channel authentication failed \u2014 the daemon rejected the token."
|
|
28877
28888
|
};
|
|
28878
28889
|
|
|
28879
28890
|
// src/i18n/messages/vi.json
|
|
@@ -29051,7 +29062,12 @@ var vi_default = {
|
|
|
29051
29062
|
"slash.claudeCmd.usage": "c\xE1ch d\xF9ng: /claude-cmd /context (chuy\u1EC3n ti\u1EBFp t\u1EDBi AI CLI)",
|
|
29052
29063
|
"start.connecting": "\u0110ang k\u1EBFt n\u1ED1i t\u1EDBi {server} (h\u1ED3 s\u01A1: {profile})\u2026",
|
|
29053
29064
|
"start.recovered": "\u0110\xE3 kh\xF4i ph\u1EE5c sau l\u1ED7i n\u1ED9i b\u1ED9 \u2014 \u0111ang k\u1EBFt n\u1ED1i l\u1EA1i: {message}",
|
|
29054
|
-
"attach.attached": '\u0110\xE3 g\u1EAFn v\xE0o h\u1ED3 s\u01A1 "{profile}" \u2014 worker ch\u1EA1y kh\xF4ng giao di\u1EC7n; \u0111\u1EA7u v\xE0o \u0111\u01B0\u1EE3c chuy\u1EC3n ti\u1EBFp.'
|
|
29065
|
+
"attach.attached": '\u0110\xE3 g\u1EAFn v\xE0o h\u1ED3 s\u01A1 "{profile}" \u2014 worker ch\u1EA1y kh\xF4ng giao di\u1EC7n; \u0111\u1EA7u v\xE0o \u0111\u01B0\u1EE3c chuy\u1EC3n ti\u1EBFp.',
|
|
29066
|
+
"autoRun.done": "Chu k\u1EF3 t\u1EF1 \u0111\u1ED9ng \u0111\xE3 ho\xE0n t\u1EA5t.",
|
|
29067
|
+
"autoRun.failed": "Kh\xF4ng \u0111i\u1EC1u ph\u1ED1i \u0111\u01B0\u1EE3c chu k\u1EF3 t\u1EF1 \u0111\u1ED9ng: {note}",
|
|
29068
|
+
"autoRun.noDaemon": 'Kh\xF4ng c\xF3 ti\u1EBFn tr\xECnh n\u1EC1n n\xE0o cho h\u1ED3 s\u01A1 "{profile}" \u2014 h\xE3y kh\u1EDFi \u0111\u1ED9ng tr\u01B0\u1EDBc: 4pm start --profile {profile}',
|
|
29069
|
+
"autoRun.timeout": "Chu k\u1EF3 t\u1EF1 \u0111\u1ED9ng v\u01B0\u1EE3t qu\xE1 gi\u1EDBi h\u1EA1n th\u1EDDi gian \u2014 \u0111ang h\u1EE7y.",
|
|
29070
|
+
"autoRun.authFailed": "X\xE1c th\u1EF1c k\xEAnh \u0111i\u1EC1u khi\u1EC3n th\u1EA5t b\u1EA1i \u2014 daemon t\u1EEB ch\u1ED1i token."
|
|
29055
29071
|
};
|
|
29056
29072
|
|
|
29057
29073
|
// src/i18n/messages/ja.json
|
|
@@ -29229,7 +29245,12 @@ var ja_default = {
|
|
|
29229
29245
|
"slash.claudeCmd.usage": "\u4F7F\u3044\u65B9: /claude-cmd /context (AI CLI \u306B\u8EE2\u9001)",
|
|
29230
29246
|
"start.connecting": "{server} \u306B\u63A5\u7D9A\u4E2D\uFF08\u30D7\u30ED\u30D5\u30A1\u30A4\u30EB: {profile}\uFF09\u2026",
|
|
29231
29247
|
"start.recovered": "\u5185\u90E8\u30A8\u30E9\u30FC\u304B\u3089\u56DE\u5FA9\u3057\u307E\u3057\u305F \u2014 \u518D\u63A5\u7D9A\u4E2D: {message}",
|
|
29232
|
-
"attach.attached": "\u30D7\u30ED\u30D5\u30A1\u30A4\u30EB\u300C{profile}\u300D\u306B\u30A2\u30BF\u30C3\u30C1\u3057\u307E\u3057\u305F \u2014 worker \u306F\u30D8\u30C3\u30C9\u30EC\u30B9\u3067\u52D5\u4F5C\u3057\u3001\u5165\u529B\u306F\u8EE2\u9001\u3055\u308C\u307E\u3059\u3002"
|
|
29248
|
+
"attach.attached": "\u30D7\u30ED\u30D5\u30A1\u30A4\u30EB\u300C{profile}\u300D\u306B\u30A2\u30BF\u30C3\u30C1\u3057\u307E\u3057\u305F \u2014 worker \u306F\u30D8\u30C3\u30C9\u30EC\u30B9\u3067\u52D5\u4F5C\u3057\u3001\u5165\u529B\u306F\u8EE2\u9001\u3055\u308C\u307E\u3059\u3002",
|
|
29249
|
+
"autoRun.done": "\u81EA\u5F8B\u30B5\u30A4\u30AF\u30EB\u304C\u5B8C\u4E86\u3057\u307E\u3057\u305F\u3002",
|
|
29250
|
+
"autoRun.failed": "\u81EA\u5F8B\u30B5\u30A4\u30AF\u30EB\u306E\u30C7\u30A3\u30B9\u30D1\u30C3\u30C1\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {note}",
|
|
29251
|
+
"autoRun.noDaemon": '\u30D7\u30ED\u30D5\u30A1\u30A4\u30EB "{profile}" \u306E\u30C7\u30FC\u30E2\u30F3\u304C\u5B9F\u884C\u3055\u308C\u3066\u3044\u307E\u305B\u3093 \u2014 \u5148\u306B\u8D77\u52D5\u3057\u3066\u304F\u3060\u3055\u3044: 4pm start --profile {profile}',
|
|
29252
|
+
"autoRun.timeout": "\u81EA\u5F8B\u30B5\u30A4\u30AF\u30EB\u304C\u5236\u9650\u6642\u9593\u3092\u8D85\u3048\u307E\u3057\u305F \u2014 \u4E2D\u6B62\u3057\u307E\u3059\u3002",
|
|
29253
|
+
"autoRun.authFailed": "\u30B3\u30F3\u30C8\u30ED\u30FC\u30EB\u30C1\u30E3\u30CD\u30EB\u306E\u8A8D\u8A3C\u306B\u5931\u6557\u3057\u307E\u3057\u305F \u2014 \u30C7\u30FC\u30E2\u30F3\u304C\u30C8\u30FC\u30AF\u30F3\u3092\u62D2\u5426\u3057\u307E\u3057\u305F\u3002"
|
|
29233
29254
|
};
|
|
29234
29255
|
|
|
29235
29256
|
// src/i18n/messages/zh.json
|
|
@@ -29407,7 +29428,12 @@ var zh_default = {
|
|
|
29407
29428
|
"slash.claudeCmd.usage": "\u7528\u6CD5\uFF1A/claude-cmd /context (\u8F6C\u53D1\u7ED9 AI CLI)",
|
|
29408
29429
|
"start.connecting": "\u6B63\u5728\u8FDE\u63A5\u5230 {server}\uFF08\u914D\u7F6E\u6587\u4EF6\uFF1A{profile}\uFF09\u2026",
|
|
29409
29430
|
"start.recovered": "\u5DF2\u4ECE\u5185\u90E8\u9519\u8BEF\u4E2D\u6062\u590D \u2014 \u6B63\u5728\u91CD\u65B0\u8FDE\u63A5\uFF1A{message}",
|
|
29410
|
-
"attach.attached": "\u5DF2\u9644\u52A0\u5230\u914D\u7F6E\u6587\u4EF6 \u201C{profile}\u201D \u2014 worker \u4EE5\u65E0\u754C\u9762\u65B9\u5F0F\u8FD0\u884C\uFF1B\u8F93\u5165\u5C06\u88AB\u8F6C\u53D1\u3002"
|
|
29431
|
+
"attach.attached": "\u5DF2\u9644\u52A0\u5230\u914D\u7F6E\u6587\u4EF6 \u201C{profile}\u201D \u2014 worker \u4EE5\u65E0\u754C\u9762\u65B9\u5F0F\u8FD0\u884C\uFF1B\u8F93\u5165\u5C06\u88AB\u8F6C\u53D1\u3002",
|
|
29432
|
+
"autoRun.done": "\u81EA\u4E3B\u5FAA\u73AF\u5DF2\u5B8C\u6210\u3002",
|
|
29433
|
+
"autoRun.failed": "\u81EA\u4E3B\u5FAA\u73AF\u8C03\u5EA6\u5931\u8D25\uFF1A{note}",
|
|
29434
|
+
"autoRun.noDaemon": "\u914D\u7F6E\u6587\u4EF6 \u201C{profile}\u201D \u6CA1\u6709\u6B63\u5728\u8FD0\u884C\u7684\u5B88\u62A4\u8FDB\u7A0B \u2014 \u8BF7\u5148\u542F\u52A8\uFF1A4pm start --profile {profile}",
|
|
29435
|
+
"autoRun.timeout": "\u81EA\u4E3B\u5FAA\u73AF\u8D85\u8FC7\u65F6\u95F4\u9650\u5236 \u2014 \u6B63\u5728\u4E2D\u6B62\u3002",
|
|
29436
|
+
"autoRun.authFailed": "\u63A7\u5236\u901A\u9053\u8BA4\u8BC1\u5931\u8D25 \u2014 \u5B88\u62A4\u8FDB\u7A0B\u62D2\u7EDD\u4E86\u4EE4\u724C\u3002"
|
|
29411
29437
|
};
|
|
29412
29438
|
|
|
29413
29439
|
// src/i18n/messages/index.ts
|
|
@@ -29494,7 +29520,7 @@ async function runLink(serverUrl, explicitProfile, token = null) {
|
|
|
29494
29520
|
|
|
29495
29521
|
// src/commands/start.ts
|
|
29496
29522
|
import { spawn as spawn10 } from "child_process";
|
|
29497
|
-
import { join as
|
|
29523
|
+
import { join as join36 } from "path";
|
|
29498
29524
|
|
|
29499
29525
|
// src/core/update.ts
|
|
29500
29526
|
import { execSync as execSync2 } from "child_process";
|
|
@@ -32706,8 +32732,8 @@ async function checkAndUpdate(serverUrl, autoUpdate) {
|
|
|
32706
32732
|
|
|
32707
32733
|
// src/core/ws-client.ts
|
|
32708
32734
|
init_wrapper();
|
|
32709
|
-
import { randomUUID as
|
|
32710
|
-
import { basename as basename7, join as
|
|
32735
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
32736
|
+
import { basename as basename7, join as join31 } from "path";
|
|
32711
32737
|
import { existsSync as existsSync16, mkdirSync as mkdirSync7 } from "fs";
|
|
32712
32738
|
|
|
32713
32739
|
// vendor/@4pm/utils/helpers.ts
|
|
@@ -37117,6 +37143,13 @@ var autonomousWriteRequestSchema = external_exports.discriminatedUnion("kind", [
|
|
|
37117
37143
|
unapprove: external_exports.array(external_exports.string().min(1).max(64)).max(500)
|
|
37118
37144
|
}),
|
|
37119
37145
|
external_exports.object({ kind: external_exports.literal("userTodo"), content: external_exports.string().min(1).max(16 * 1024) }),
|
|
37146
|
+
// Traced book save (ADR-0320): the cli diffs rows by id + stamps `.autonomous.authors.json` so
|
|
37147
|
+
// authorship is server-filled, not a spoofable `.md` cell. `by`/author are added server-side.
|
|
37148
|
+
external_exports.object({
|
|
37149
|
+
kind: external_exports.literal("bookSave"),
|
|
37150
|
+
book: external_exports.enum(["userTodo", "userQa", "aiTodo"]),
|
|
37151
|
+
content: external_exports.string().max(256 * 1024)
|
|
37152
|
+
}),
|
|
37120
37153
|
external_exports.object({ kind: external_exports.literal("cron"), action: external_exports.enum(["install", "uninstall"]) })
|
|
37121
37154
|
]);
|
|
37122
37155
|
var autonomousLogsQuerySchema = external_exports.object({
|
|
@@ -39864,15 +39897,124 @@ function reportToolResult(tool, ok, message) {
|
|
|
39864
39897
|
}
|
|
39865
39898
|
|
|
39866
39899
|
// src/core/autonomous.ts
|
|
39867
|
-
import { readFile as
|
|
39868
|
-
import { readFileSync as
|
|
39900
|
+
import { readFile as readFile4, writeFile as writeFile4, chmod } from "fs/promises";
|
|
39901
|
+
import { readFileSync as readFileSync14 } from "fs";
|
|
39869
39902
|
import { execFile as execFile4, spawn as spawn5 } from "child_process";
|
|
39870
|
-
import { join as
|
|
39903
|
+
import { join as join17 } from "path";
|
|
39871
39904
|
import { promisify as promisify4 } from "util";
|
|
39872
|
-
|
|
39873
|
-
|
|
39905
|
+
|
|
39906
|
+
// src/core/autonomous-config.ts
|
|
39907
|
+
import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
|
|
39908
|
+
import { readFileSync as readFileSync13 } from "fs";
|
|
39909
|
+
import { join as join15 } from "path";
|
|
39910
|
+
var DEFAULT_AUTONOMOUS_CONFIG = {
|
|
39911
|
+
paused: true,
|
|
39912
|
+
cronSchedule: "*/10 * * * *",
|
|
39913
|
+
quietHours: "",
|
|
39914
|
+
maxTicksPerDay: -1,
|
|
39915
|
+
stopOnConsecutiveFailures: 3,
|
|
39916
|
+
logRetentionDays: 14,
|
|
39917
|
+
model: "",
|
|
39918
|
+
maxSessionPct: 80,
|
|
39919
|
+
maxWeeklyPct: 90
|
|
39920
|
+
};
|
|
39921
|
+
function autonomousConfigPath(profileDir2) {
|
|
39922
|
+
return join15(profileDir2, "autonomous.config.json");
|
|
39923
|
+
}
|
|
39924
|
+
function coerce2(raw) {
|
|
39925
|
+
const o = raw && typeof raw === "object" ? raw : {};
|
|
39926
|
+
const d = DEFAULT_AUTONOMOUS_CONFIG;
|
|
39927
|
+
const num = (v2, def) => typeof v2 === "number" && Number.isFinite(v2) ? v2 : def;
|
|
39928
|
+
const str = (v2, def) => typeof v2 === "string" ? v2 : def;
|
|
39929
|
+
return {
|
|
39930
|
+
paused: typeof o.paused === "boolean" ? o.paused : d.paused,
|
|
39931
|
+
cronSchedule: str(o.cronSchedule, d.cronSchedule),
|
|
39932
|
+
quietHours: str(o.quietHours, d.quietHours),
|
|
39933
|
+
maxTicksPerDay: num(o.maxTicksPerDay, d.maxTicksPerDay),
|
|
39934
|
+
stopOnConsecutiveFailures: num(o.stopOnConsecutiveFailures, d.stopOnConsecutiveFailures),
|
|
39935
|
+
logRetentionDays: num(o.logRetentionDays, d.logRetentionDays),
|
|
39936
|
+
model: str(o.model, d.model),
|
|
39937
|
+
maxSessionPct: num(o.maxSessionPct, d.maxSessionPct),
|
|
39938
|
+
maxWeeklyPct: num(o.maxWeeklyPct, d.maxWeeklyPct)
|
|
39939
|
+
};
|
|
39940
|
+
}
|
|
39941
|
+
async function readAutonomousConfig(profileDir2) {
|
|
39942
|
+
try {
|
|
39943
|
+
return coerce2(JSON.parse(await readFile2(autonomousConfigPath(profileDir2), "utf8")));
|
|
39944
|
+
} catch {
|
|
39945
|
+
return { ...DEFAULT_AUTONOMOUS_CONFIG };
|
|
39946
|
+
}
|
|
39947
|
+
}
|
|
39948
|
+
function readAutonomousConfigSync(profileDir2) {
|
|
39949
|
+
try {
|
|
39950
|
+
return coerce2(JSON.parse(readFileSync13(autonomousConfigPath(profileDir2), "utf8")));
|
|
39951
|
+
} catch {
|
|
39952
|
+
return { ...DEFAULT_AUTONOMOUS_CONFIG };
|
|
39953
|
+
}
|
|
39954
|
+
}
|
|
39955
|
+
async function readAutonomousConfigText(profileDir2) {
|
|
39956
|
+
try {
|
|
39957
|
+
return await readFile2(autonomousConfigPath(profileDir2), "utf8");
|
|
39958
|
+
} catch {
|
|
39959
|
+
return JSON.stringify(DEFAULT_AUTONOMOUS_CONFIG, null, 2) + "\n";
|
|
39960
|
+
}
|
|
39961
|
+
}
|
|
39962
|
+
async function writeAutonomousConfig(profileDir2, raw) {
|
|
39963
|
+
const cfg = coerce2(raw);
|
|
39964
|
+
await writeFile2(autonomousConfigPath(profileDir2), JSON.stringify(cfg, null, 2) + "\n", "utf8");
|
|
39965
|
+
}
|
|
39966
|
+
function isInQuietHours(quietHours, now = /* @__PURE__ */ new Date()) {
|
|
39967
|
+
const m2 = /^(\d{1,2}):(\d{2})-(\d{1,2}):(\d{2})$/.exec(quietHours.trim());
|
|
39968
|
+
if (!m2) return false;
|
|
39969
|
+
const toMin = (h, mi2) => Number(h) * 60 + Number(mi2);
|
|
39970
|
+
const start = toMin(m2[1], m2[2]);
|
|
39971
|
+
const end = toMin(m2[3], m2[4]);
|
|
39972
|
+
const cur = now.getHours() * 60 + now.getMinutes();
|
|
39973
|
+
return start <= end ? cur >= start && cur < end : cur >= start || cur < end;
|
|
39974
|
+
}
|
|
39975
|
+
|
|
39976
|
+
// src/core/autonomous-history.ts
|
|
39977
|
+
import { readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
|
|
39978
|
+
import { join as join16 } from "path";
|
|
39874
39979
|
var HISTORIES_REL = ".claude/.autonomous.histories.json";
|
|
39980
|
+
var MAX_RECORDS = 50;
|
|
39981
|
+
var SKELETON = { cronApplied: "", consecutiveFails: 0, ticks: { day: "", count: 0 }, records: [] };
|
|
39982
|
+
function historiesPath(root) {
|
|
39983
|
+
return join16(root, HISTORIES_REL);
|
|
39984
|
+
}
|
|
39985
|
+
async function readHistories(root) {
|
|
39986
|
+
try {
|
|
39987
|
+
const d = JSON.parse(await readFile3(historiesPath(root), "utf8"));
|
|
39988
|
+
const ticks = d.ticks ?? {};
|
|
39989
|
+
return {
|
|
39990
|
+
cronApplied: typeof d.cronApplied === "string" ? d.cronApplied : typeof d.cron_applied === "string" ? d.cron_applied : "",
|
|
39991
|
+
consecutiveFails: typeof d.consecutiveFails === "number" ? d.consecutiveFails : typeof d.consecutive_fails === "number" ? d.consecutive_fails : 0,
|
|
39992
|
+
ticks: { day: typeof ticks.day === "string" ? ticks.day : "", count: typeof ticks.count === "number" ? ticks.count : 0 },
|
|
39993
|
+
records: Array.isArray(d.records) ? d.records : []
|
|
39994
|
+
};
|
|
39995
|
+
} catch {
|
|
39996
|
+
return { ...SKELETON, ticks: { ...SKELETON.ticks }, records: [] };
|
|
39997
|
+
}
|
|
39998
|
+
}
|
|
39999
|
+
async function writeHistories(root, h) {
|
|
40000
|
+
try {
|
|
40001
|
+
await writeFile3(historiesPath(root), JSON.stringify(h, null, 2) + "\n", "utf8");
|
|
40002
|
+
} catch {
|
|
40003
|
+
}
|
|
40004
|
+
}
|
|
40005
|
+
function todayTickCount(h, today) {
|
|
40006
|
+
return h.ticks.day === today ? h.ticks.count : 0;
|
|
40007
|
+
}
|
|
40008
|
+
function pushRecord(h, rec) {
|
|
40009
|
+
h.records.push(rec);
|
|
40010
|
+
if (h.records.length > MAX_RECORDS) h.records = h.records.slice(-MAX_RECORDS);
|
|
40011
|
+
return h;
|
|
40012
|
+
}
|
|
40013
|
+
|
|
40014
|
+
// src/core/autonomous.ts
|
|
40015
|
+
var run4 = promisify4(execFile4);
|
|
39875
40016
|
var APPROVALS_REL = ".claude/.autonomous.approvals.json";
|
|
40017
|
+
var AUTHORS_REL = ".claude/.autonomous.authors.json";
|
|
39876
40018
|
var TICK_REL = ".claude/hooks/autonomous-tick.sh";
|
|
39877
40019
|
var LOG_DIR_REL = ".claude/logs";
|
|
39878
40020
|
var BOOK_FILES = {
|
|
@@ -39882,10 +40024,9 @@ var BOOK_FILES = {
|
|
|
39882
40024
|
aiDone: "AI_DONE.md",
|
|
39883
40025
|
userQa: "USER_QA.md"
|
|
39884
40026
|
};
|
|
39885
|
-
var DEFAULT_CRON = "*/10 * * * *";
|
|
39886
40027
|
async function readText(path, fallback = "") {
|
|
39887
40028
|
try {
|
|
39888
|
-
return await
|
|
40029
|
+
return await readFile4(path, "utf8");
|
|
39889
40030
|
} catch {
|
|
39890
40031
|
return fallback;
|
|
39891
40032
|
}
|
|
@@ -39901,8 +40042,37 @@ function parseJson(text) {
|
|
|
39901
40042
|
function tableCell(value) {
|
|
39902
40043
|
return value.replace(/\r?\n/g, " ").replace(/\|/g, "\\|").trim();
|
|
39903
40044
|
}
|
|
40045
|
+
async function readJsonMap(path) {
|
|
40046
|
+
return parseJson(await readText(path, "{}")) ?? {};
|
|
40047
|
+
}
|
|
40048
|
+
function entryBy(map, id) {
|
|
40049
|
+
const e = map[id];
|
|
40050
|
+
return e && typeof e === "object" && typeof e.by === "string" ? e.by : null;
|
|
40051
|
+
}
|
|
40052
|
+
function tableRowsById(md) {
|
|
40053
|
+
const rows = /* @__PURE__ */ new Map();
|
|
40054
|
+
let headerSeen = false;
|
|
40055
|
+
let inTable = false;
|
|
40056
|
+
for (const line of md.split(/\r?\n/)) {
|
|
40057
|
+
const s3 = line.trim();
|
|
40058
|
+
if (!s3.startsWith("|")) {
|
|
40059
|
+
if (inTable) break;
|
|
40060
|
+
continue;
|
|
40061
|
+
}
|
|
40062
|
+
inTable = true;
|
|
40063
|
+
if (!headerSeen) {
|
|
40064
|
+
headerSeen = true;
|
|
40065
|
+
continue;
|
|
40066
|
+
}
|
|
40067
|
+
if (/^\|[\s:|-]+\|?$/.test(s3)) continue;
|
|
40068
|
+
const cells = s3.replace(/^\|/, "").replace(/\|$/, "").split("|").map((c) => c.trim());
|
|
40069
|
+
const id = cells[0] ?? "";
|
|
40070
|
+
if (id) rows.set(id, cells.join(""));
|
|
40071
|
+
}
|
|
40072
|
+
return rows;
|
|
40073
|
+
}
|
|
39904
40074
|
function tickScript(root) {
|
|
39905
|
-
return
|
|
40075
|
+
return join17(root, TICK_REL);
|
|
39906
40076
|
}
|
|
39907
40077
|
async function crontabList() {
|
|
39908
40078
|
try {
|
|
@@ -39924,28 +40094,28 @@ async function cronInstalled(root) {
|
|
|
39924
40094
|
const script = tickScript(root);
|
|
39925
40095
|
return (await crontabList()).split("\n").some((l) => l.includes(script) && !l.trim().startsWith("#"));
|
|
39926
40096
|
}
|
|
39927
|
-
async function getAutonomousStatus(root) {
|
|
39928
|
-
const
|
|
39929
|
-
|
|
39930
|
-
|
|
39931
|
-
|
|
39932
|
-
|
|
40097
|
+
async function getAutonomousStatus(root, profileDir2) {
|
|
40098
|
+
const [cfg, hist, installed] = await Promise.all([
|
|
40099
|
+
readAutonomousConfig(profileDir2),
|
|
40100
|
+
readHistories(root),
|
|
40101
|
+
cronInstalled(root)
|
|
40102
|
+
]);
|
|
40103
|
+
const last = hist.records[hist.records.length - 1];
|
|
39933
40104
|
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
39934
40105
|
return {
|
|
39935
|
-
installed
|
|
39936
|
-
paused:
|
|
39937
|
-
cronSchedule:
|
|
39938
|
-
lastTickAt: last
|
|
39939
|
-
lastResult: last
|
|
39940
|
-
consecutiveFails:
|
|
39941
|
-
todayTicks: ticks.day === today
|
|
40106
|
+
installed,
|
|
40107
|
+
paused: cfg.paused,
|
|
40108
|
+
cronSchedule: cfg.cronSchedule,
|
|
40109
|
+
lastTickAt: last?.ts ?? null,
|
|
40110
|
+
lastResult: last?.status ?? null,
|
|
40111
|
+
consecutiveFails: hist.consecutiveFails,
|
|
40112
|
+
todayTicks: hist.ticks.day === today ? hist.ticks.count : 0
|
|
39942
40113
|
};
|
|
39943
40114
|
}
|
|
39944
|
-
function isAutonomousRunning(root) {
|
|
40115
|
+
function isAutonomousRunning(root, profileDir2) {
|
|
39945
40116
|
try {
|
|
39946
|
-
|
|
39947
|
-
|
|
39948
|
-
const h = JSON.parse(readFileSync13(join15(root, HISTORIES_REL), "utf8"));
|
|
40117
|
+
if (readAutonomousConfigSync(profileDir2).paused) return false;
|
|
40118
|
+
const h = JSON.parse(readFileSync14(join17(root, ".claude/.autonomous.histories.json"), "utf8"));
|
|
39949
40119
|
const ts2 = h.records?.[h.records.length - 1]?.ts;
|
|
39950
40120
|
if (!ts2) return false;
|
|
39951
40121
|
const t2 = Date.parse(ts2.replace(" ", "T"));
|
|
@@ -39954,32 +40124,36 @@ function isAutonomousRunning(root) {
|
|
|
39954
40124
|
return false;
|
|
39955
40125
|
}
|
|
39956
40126
|
}
|
|
39957
|
-
async function readAutonomous(root) {
|
|
39958
|
-
const [settings, approvals, status, ...books] = await Promise.all([
|
|
39959
|
-
|
|
39960
|
-
readText(
|
|
39961
|
-
|
|
39962
|
-
|
|
40127
|
+
async function readAutonomous(root, profileDir2) {
|
|
40128
|
+
const [settings, approvals, authors, status, ...books] = await Promise.all([
|
|
40129
|
+
readAutonomousConfigText(profileDir2),
|
|
40130
|
+
readText(join17(root, APPROVALS_REL), "{}"),
|
|
40131
|
+
readText(join17(root, AUTHORS_REL), "{}"),
|
|
40132
|
+
getAutonomousStatus(root, profileDir2),
|
|
40133
|
+
...Object.values(BOOK_FILES).map((f2) => readText(join17(root, f2)))
|
|
39963
40134
|
]);
|
|
39964
40135
|
const keys = Object.keys(BOOK_FILES);
|
|
39965
40136
|
const bookMap = {};
|
|
39966
40137
|
keys.forEach((k2, i) => bookMap[k2] = books[i] ?? "");
|
|
39967
|
-
return { settings, status, books: bookMap, approvals };
|
|
40138
|
+
return { settings, status, books: bookMap, approvals, authors };
|
|
39968
40139
|
}
|
|
39969
40140
|
async function readAutonomousLogs(root, date) {
|
|
39970
40141
|
const day = date && /^\d{4}-\d{2}-\d{2}$/.test(date) ? date : (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
39971
|
-
const text = await readText(
|
|
40142
|
+
const text = await readText(join17(root, LOG_DIR_REL, `autonomous-tick-${day}.log`));
|
|
39972
40143
|
const lines = text ? text.split("\n").filter((l) => l.length > 0).slice(-500) : [];
|
|
39973
40144
|
return { date: day, lines };
|
|
39974
40145
|
}
|
|
39975
|
-
async function installCron(root) {
|
|
40146
|
+
async function installCron(root, profileDir2) {
|
|
39976
40147
|
const script = tickScript(root);
|
|
39977
|
-
const
|
|
40148
|
+
const cfg = await readAutonomousConfig(profileDir2);
|
|
39978
40149
|
await chmod(script, 493).catch(() => void 0);
|
|
39979
40150
|
const kept = (await crontabList()).split("\n").filter((l) => l.trim() && !l.includes(script));
|
|
39980
|
-
kept.push(`${
|
|
40151
|
+
kept.push(`${cfg.cronSchedule} ${script}`);
|
|
39981
40152
|
await crontabSet(kept.join("\n"));
|
|
39982
40153
|
}
|
|
40154
|
+
async function syncCron(root, profileDir2) {
|
|
40155
|
+
if (await cronInstalled(root)) await installCron(root, profileDir2);
|
|
40156
|
+
}
|
|
39983
40157
|
async function uninstallCron(root) {
|
|
39984
40158
|
const script = tickScript(root);
|
|
39985
40159
|
const cur = await crontabList();
|
|
@@ -39987,51 +40161,84 @@ async function uninstallCron(root) {
|
|
|
39987
40161
|
const kept = cur.split("\n").filter((l) => l.trim() && !l.includes(script));
|
|
39988
40162
|
await crontabSet(kept.join("\n"));
|
|
39989
40163
|
}
|
|
39990
|
-
async function writeAutonomous(root, req, by) {
|
|
40164
|
+
async function writeAutonomous(root, profileDir2, req, by) {
|
|
39991
40165
|
try {
|
|
39992
40166
|
switch (req.kind) {
|
|
39993
40167
|
case "settings": {
|
|
39994
|
-
|
|
39995
|
-
|
|
40168
|
+
const parsed = parseJson(req.settings);
|
|
40169
|
+
if (!parsed) return { ok: false, error: "settings is not valid JSON" };
|
|
40170
|
+
await writeAutonomousConfig(profileDir2, parsed);
|
|
40171
|
+
await syncCron(root, profileDir2);
|
|
39996
40172
|
break;
|
|
39997
40173
|
}
|
|
39998
40174
|
case "approvals": {
|
|
39999
|
-
|
|
40000
|
-
|
|
40175
|
+
if (req.approved && !req.byIsAdmin) {
|
|
40176
|
+
const authors = await readJsonMap(join17(root, AUTHORS_REL));
|
|
40177
|
+
if (entryBy(authors, req.taskId) === by) {
|
|
40178
|
+
return { ok: false, code: "APPROVAL_SELF", failedId: req.taskId, error: "self-approval blocked" };
|
|
40179
|
+
}
|
|
40180
|
+
}
|
|
40181
|
+
const map = parseJson(await readText(join17(root, APPROVALS_REL), "{}")) ?? {};
|
|
40182
|
+
if (req.approved) map[req.taskId] = { approved: true, by, byLabel: req.byLabel ?? by, at: (/* @__PURE__ */ new Date()).toISOString() };
|
|
40001
40183
|
else delete map[req.taskId];
|
|
40002
|
-
await
|
|
40184
|
+
await writeFile4(join17(root, APPROVALS_REL), JSON.stringify(map, null, 2) + "\n", "utf8");
|
|
40003
40185
|
break;
|
|
40004
40186
|
}
|
|
40005
40187
|
case "approvalsBatch": {
|
|
40006
|
-
|
|
40188
|
+
if (!req.byIsAdmin) {
|
|
40189
|
+
const authors = await readJsonMap(join17(root, AUTHORS_REL));
|
|
40190
|
+
const selfId = req.approve.find((id) => entryBy(authors, id) === by);
|
|
40191
|
+
if (selfId) return { ok: false, code: "APPROVAL_SELF", failedId: selfId, error: "self-approval blocked" };
|
|
40192
|
+
}
|
|
40193
|
+
const map = parseJson(await readText(join17(root, APPROVALS_REL), "{}")) ?? {};
|
|
40007
40194
|
const at2 = (/* @__PURE__ */ new Date()).toISOString();
|
|
40008
|
-
|
|
40195
|
+
const byLabel = req.byLabel ?? by;
|
|
40196
|
+
for (const taskId of req.approve) map[taskId] = { approved: true, by, byLabel, at: at2 };
|
|
40009
40197
|
for (const taskId of req.unapprove) delete map[taskId];
|
|
40010
|
-
await
|
|
40198
|
+
await writeFile4(join17(root, APPROVALS_REL), JSON.stringify(map, null, 2) + "\n", "utf8");
|
|
40011
40199
|
break;
|
|
40012
40200
|
}
|
|
40013
40201
|
case "userTodo": {
|
|
40014
|
-
const cur = await readText(
|
|
40015
|
-
const
|
|
40016
|
-
const
|
|
40202
|
+
const cur = await readText(join17(root, BOOK_FILES.userTodo));
|
|
40203
|
+
const ids = [...cur.matchAll(/REQ-(\d{4})-(\d{4})/g)];
|
|
40204
|
+
const maxGroup = ids.reduce((m2, g2) => Math.max(m2, Number(g2[1])), 0);
|
|
40205
|
+
const id = `REQ-${String(maxGroup + 1).padStart(4, "0")}-0001`;
|
|
40206
|
+
const row = `| ${id} | | | ${tableCell(req.content)} |
|
|
40017
40207
|
`;
|
|
40018
|
-
await
|
|
40208
|
+
await writeFile4(join17(root, BOOK_FILES.userTodo), cur.replace(/\s*$/, "\n") + row, "utf8");
|
|
40209
|
+
const authors = await readJsonMap(join17(root, AUTHORS_REL));
|
|
40210
|
+
authors[id] = { by, byLabel: req.byLabel ?? by, at: (/* @__PURE__ */ new Date()).toISOString() };
|
|
40211
|
+
await writeFile4(join17(root, AUTHORS_REL), JSON.stringify(authors, null, 2) + "\n", "utf8");
|
|
40212
|
+
break;
|
|
40213
|
+
}
|
|
40214
|
+
case "bookSave": {
|
|
40215
|
+
const file = BOOK_FILES[req.book];
|
|
40216
|
+
const prev = tableRowsById(await readText(join17(root, file)));
|
|
40217
|
+
const next = tableRowsById(req.content);
|
|
40218
|
+
const authors = await readJsonMap(join17(root, AUTHORS_REL));
|
|
40219
|
+
const at2 = (/* @__PURE__ */ new Date()).toISOString();
|
|
40220
|
+
const byLabel = req.byLabel ?? by;
|
|
40221
|
+
for (const [id, cells] of next) {
|
|
40222
|
+
if (prev.get(id) !== cells) authors[id] = { by, byLabel, at: at2 };
|
|
40223
|
+
}
|
|
40224
|
+
await writeFile4(join17(root, file), req.content, "utf8");
|
|
40225
|
+
await writeFile4(join17(root, AUTHORS_REL), JSON.stringify(authors, null, 2) + "\n", "utf8");
|
|
40019
40226
|
break;
|
|
40020
40227
|
}
|
|
40021
40228
|
case "cron": {
|
|
40022
|
-
if (req.action === "install") await installCron(root);
|
|
40229
|
+
if (req.action === "install") await installCron(root, profileDir2);
|
|
40023
40230
|
else await uninstallCron(root);
|
|
40024
40231
|
break;
|
|
40025
40232
|
}
|
|
40026
40233
|
}
|
|
40027
|
-
return { ok: true, status: await getAutonomousStatus(root) };
|
|
40234
|
+
return { ok: true, status: await getAutonomousStatus(root, profileDir2) };
|
|
40028
40235
|
} catch (err) {
|
|
40029
40236
|
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
40030
40237
|
}
|
|
40031
40238
|
}
|
|
40032
40239
|
|
|
40033
40240
|
// src/core/network-probe.ts
|
|
40034
|
-
import { existsSync as existsSync11, readFileSync as
|
|
40241
|
+
import { existsSync as existsSync11, readFileSync as readFileSync15 } from "fs";
|
|
40035
40242
|
import { connect } from "net";
|
|
40036
40243
|
var CANARY_TARGETS = [
|
|
40037
40244
|
{ host: "1.1.1.1", port: 443 },
|
|
@@ -40041,7 +40248,7 @@ var PROBE_TIMEOUT_MS = 2500;
|
|
|
40041
40248
|
function detectContainerized() {
|
|
40042
40249
|
try {
|
|
40043
40250
|
if (existsSync11("/.dockerenv")) return true;
|
|
40044
|
-
const cgroup =
|
|
40251
|
+
const cgroup = readFileSync15("/proc/1/cgroup", "utf8");
|
|
40045
40252
|
return /docker|containerd|kubepods|libpod/.test(cgroup);
|
|
40046
40253
|
} catch {
|
|
40047
40254
|
return false;
|
|
@@ -40075,7 +40282,7 @@ function probeInbound() {
|
|
|
40075
40282
|
const isExposed = (path, loopbackHexPrefixes) => {
|
|
40076
40283
|
let body;
|
|
40077
40284
|
try {
|
|
40078
|
-
body =
|
|
40285
|
+
body = readFileSync15(path, "utf8");
|
|
40079
40286
|
} catch {
|
|
40080
40287
|
return false;
|
|
40081
40288
|
}
|
|
@@ -40110,7 +40317,7 @@ async function probeNetwork() {
|
|
|
40110
40317
|
// src/core/git-auth.ts
|
|
40111
40318
|
import { writeFileSync as writeFileSync8 } from "fs";
|
|
40112
40319
|
import { homedir as homedir4 } from "os";
|
|
40113
|
-
import { join as
|
|
40320
|
+
import { join as join18 } from "path";
|
|
40114
40321
|
function tokenTarget(method) {
|
|
40115
40322
|
if (method === "gitlab-group-token") {
|
|
40116
40323
|
return { host: process.env.FOURPM_GIT_HOST || "gitlab.com", user: "oauth2" };
|
|
@@ -40128,7 +40335,7 @@ function applyGitAuth(method) {
|
|
|
40128
40335
|
try {
|
|
40129
40336
|
const line = `https://${target.user}:${token}@${target.host}
|
|
40130
40337
|
`;
|
|
40131
|
-
writeFileSync8(
|
|
40338
|
+
writeFileSync8(join18(homedir4(), ".git-credentials"), line, { encoding: "utf8", mode: 384 });
|
|
40132
40339
|
return true;
|
|
40133
40340
|
} catch {
|
|
40134
40341
|
return false;
|
|
@@ -40366,7 +40573,7 @@ function formatTimestampInZone(date, timeZone) {
|
|
|
40366
40573
|
|
|
40367
40574
|
// src/ui/slash-commands.ts
|
|
40368
40575
|
import { existsSync as existsSync12 } from "fs";
|
|
40369
|
-
import { join as
|
|
40576
|
+
import { join as join19 } from "path";
|
|
40370
40577
|
var CONFIG_KEYS = [
|
|
40371
40578
|
"aiCli",
|
|
40372
40579
|
"claudeHome",
|
|
@@ -40378,7 +40585,7 @@ var CONFIG_KEYS = [
|
|
|
40378
40585
|
"aiEnv"
|
|
40379
40586
|
];
|
|
40380
40587
|
function configPath(profileDir2) {
|
|
40381
|
-
return
|
|
40588
|
+
return join19(profileDir2, "config.json");
|
|
40382
40589
|
}
|
|
40383
40590
|
function parseConfigAssignment(key, valueTokens) {
|
|
40384
40591
|
const value = valueTokens.join(" ").trim();
|
|
@@ -40513,7 +40720,7 @@ function runOutput(ctx) {
|
|
|
40513
40720
|
}
|
|
40514
40721
|
function runLogs(ctx) {
|
|
40515
40722
|
const n = Math.max(1, Math.min(200, Number(ctx.args[0]) || 20));
|
|
40516
|
-
const dir2 =
|
|
40723
|
+
const dir2 = join19(ctx.info.profileDir, "logs");
|
|
40517
40724
|
const lines = readRecentLogLines(dir2, n);
|
|
40518
40725
|
if (lines.length === 0) {
|
|
40519
40726
|
ctx.print(t("slash.logs.none"));
|
|
@@ -41351,8 +41558,251 @@ function rejectByReview(ctx, commandId, origin, prompt, reasons) {
|
|
|
41351
41558
|
});
|
|
41352
41559
|
}
|
|
41353
41560
|
|
|
41561
|
+
// src/core/autonomous-cycle.ts
|
|
41562
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
41563
|
+
import { appendFile as appendFile2, mkdir as mkdir3, readFile as readFile5, readdir as readdir2, rm as rm2, stat } from "fs/promises";
|
|
41564
|
+
import { join as join20 } from "path";
|
|
41565
|
+
function buildAutonomousCyclePrompt() {
|
|
41566
|
+
return `You are running UNATTENDED as the 4PM autonomous cycle (headless, one tick). Do EXACTLY ONE small
|
|
41567
|
+
work cycle with the steps below, then STOP. Keep each cycle to ONE small task so you never run out of
|
|
41568
|
+
tokens mid-way. If a step fails, append a row to the **Incidents** table of \`AI_DONE.md\` and STOP \u2014
|
|
41569
|
+
do not push on. Write all books/docs/code in the language the project's \`CLAUDE.md\` specifies (default
|
|
41570
|
+
English).
|
|
41571
|
+
|
|
41572
|
+
## Books & approval (MUST read the approvals file)
|
|
41573
|
+
The five "book" files at the project root \u2014 \`USER_TODO.md\`, \`USER_QA.md\`, \`AI_TODO.md\`,
|
|
41574
|
+
\`AI_PROGRESS.md\`, \`AI_DONE.md\` \u2014 have canonical templates in \`.claude/templates/\` (\`<NAME>.empty.md\`
|
|
41575
|
+
= EMPTY, \`<NAME>.sample.md\` = example WITH DATA). To tell empty/has-work, COMPARE a book to its
|
|
41576
|
+
\`<NAME>.empty.md\` (equal after trimming \u21D2 empty). When clearing a book, overwrite it with EXACTLY the
|
|
41577
|
+
empty template (\`cp .claude/templates/<NAME>.empty.md <NAME>\`).
|
|
41578
|
+
|
|
41579
|
+
The single source of truth for approval is \`.claude/.autonomous.approvals.json\` \u2014 a map
|
|
41580
|
+
\`{ "<ID>": { "approved": true, "by": "...", "at": "..." } }\` (the user ticks rows in the web grids). A
|
|
41581
|
+
row is APPROVED only when its ID has \`"approved": true\`. Row IDs: \`USER_TODO\` = \`REQ-\u2026\`, \`USER_QA\`
|
|
41582
|
+
= \`QA-\u2026\`, \`AI_TODO\` = \`TSK-\u2026\`. NEVER act on an unapproved row.
|
|
41583
|
+
|
|
41584
|
+
## Step 1 \u2014 Base branch (from the spec, ADR-0292)
|
|
41585
|
+
The project root IS the primary repo (ADR-0314). Its currently checked-out branch is the base branch
|
|
41586
|
+
(4PM cloned it with \`-b <branch>\`): \`BASE=$(git rev-parse --abbrev-ref HEAD)\`. Sync it:
|
|
41587
|
+
\`git fetch origin\` then \`git pull --ff-only origin "$BASE"\` (skip the pull if the remote has no such
|
|
41588
|
+
branch yet). Do NOT invent a separate integration/dev branch.
|
|
41589
|
+
|
|
41590
|
+
## Step 2 \u2014 Intake APPROVED user requests \u2192 tasks
|
|
41591
|
+
Read \`USER_TODO.md\` (content-only columns \`ID | Group | Depends | Request\`; who/when wrote or approved a
|
|
41592
|
+
row lives in \`.claude/.autonomous.authors.json\` / \`.autonomous.approvals.json\`, ADR-0320 \u2014 never add
|
|
41593
|
+
provenance columns to the table). For each row that is APPROVED (its \`REQ-\u2026\` in the approvals file) AND
|
|
41594
|
+
whose \`Depends\` \`REQ-\u2026\` are already handled:
|
|
41595
|
+
- If the request is CLEAR: split it into small tasks and append them to \`AI_TODO.md\` (7 columns
|
|
41596
|
+
\`ID | Priority | Tag | Depends | Group | Task description | Notes\`), each with an ID
|
|
41597
|
+
\`TSK-{groupid:0000}-{taskid:0000}\`. **groupid** must be UNIQUE + INCREASING across all history \u2014 find
|
|
41598
|
+
the largest ever used in git history of \`AI_TODO.md\`/\`AI_DONE.md\`/\`AI_PROGRESS.md\`, then use max+1;
|
|
41599
|
+
**taskid** starts at 0001 within the group. Leave \`Tag\` blank unless a catalog tag applies; leave
|
|
41600
|
+
\`Priority\` = High/Medium/Low (default Medium). Then REMOVE the analysed request row from
|
|
41601
|
+
\`USER_TODO.md\` (if it becomes empty, reset it to the empty template).
|
|
41602
|
+
- If the request is UNCLEAR (ambiguous/missing info/contradictory): do NOT guess. Append a row to
|
|
41603
|
+
\`USER_QA.md\` (content-only columns \`ID | Group | Depends | Original request | Question / options |
|
|
41604
|
+
Answer\`) with a new \`QA-{groupid:0000}-{qaid:0000}\` id, the original request and the question +
|
|
41605
|
+
options; leave \`Answer\` blank for the user. Remove the request row from \`USER_TODO.md\`. (Do NOT
|
|
41606
|
+
generate tasks for it.)
|
|
41607
|
+
An unapproved request row is LEFT in place (wait for approval).
|
|
41608
|
+
|
|
41609
|
+
## Step 3 \u2014 Fold APPROVED answers back (USER_QA)
|
|
41610
|
+
For each \`USER_QA.md\` row that has an \`Answer\` AND is APPROVED (its \`QA-\u2026\` in the approvals file):
|
|
41611
|
+
re-analyse using the answer \u2014 append the resulting tasks to \`AI_TODO.md\` (same ID rules as Step 2) \u2014
|
|
41612
|
+
then clear that QA row. An unanswered or unapproved QA row is left in place.
|
|
41613
|
+
|
|
41614
|
+
## Step 4 \u2014 Pick ONE approved task and start it
|
|
41615
|
+
Read \`AI_TODO.md\` + the approvals file. Filter tasks that are BOTH approved (\`TSK-\u2026\` in approvals) AND
|
|
41616
|
+
have every \`Depends\` \`TSK-\u2026\` already in \`AI_DONE.md\`. Run group by group (smallest eligible group
|
|
41617
|
+
first), within a group High \u2192 Medium \u2192 Low, then line order. If NO eligible task: append a row to the
|
|
41618
|
+
\`AI_DONE.md\` Incidents table (e.g. "only generated tasks / waiting for approval / waiting for deps")
|
|
41619
|
+
and STOP. Otherwise move the chosen task into \`AI_PROGRESS.md\` (with a start timestamp), remove it from
|
|
41620
|
+
\`AI_TODO.md\`, and commit on the base branch: \`git commit -am "chore(auto): start TSK-\u2026"\`.
|
|
41621
|
+
|
|
41622
|
+
## Step 5 \u2014 Implement on a task branch + test
|
|
41623
|
+
\`git checkout -b task/TSK-\u2026\`. Implement following the project's architecture + \`CLAUDE.md\`; add/update
|
|
41624
|
+
tests. Run the project's test command (per \`CLAUDE.md\` / the project's scripts); WAIT for it to finish
|
|
41625
|
+
and check the result. Keep any produced report/evidence. Commit: \`git commit -am "feat(TSK-\u2026): <desc>"\`.
|
|
41626
|
+
|
|
41627
|
+
## Step 6 \u2014 Pull request into the base branch (NO direct merge \u2014 ADR-0319/0271)
|
|
41628
|
+
1. Rebase onto the freshest base to minimise conflicts: \`git fetch origin\` then
|
|
41629
|
+
\`git rebase origin/"$BASE"\` (on conflict you can't resolve confidently, \`git rebase --abort\`, write a
|
|
41630
|
+
\`USER_QA.md\` row, and STOP).
|
|
41631
|
+
2. Push the task branch: \`git push -u origin task/TSK-\u2026\`.
|
|
41632
|
+
3. Open a PR into the base branch with \`gh\` (GitHub) or \`glab\` (GitLab) \u2014 e.g.
|
|
41633
|
+
\`gh pr create --base "$BASE" --head task/TSK-\u2026 --title "TSK-\u2026: <desc>" --body "<summary>"\`. Do NOT
|
|
41634
|
+
merge it and do NOT fast-forward the base yourself \u2014 a human reviews the PR. If no remote / no
|
|
41635
|
+
\`gh\`/\`glab\` auth is available, keep the local task-branch commit and append an Incidents row saying
|
|
41636
|
+
the PR could not be opened.
|
|
41637
|
+
|
|
41638
|
+
## Step 7 \u2014 Update the books
|
|
41639
|
+
On the base branch, append a row to the \`AI_DONE.md\` Done table (Timestamp, ID, Task description, Files,
|
|
41640
|
+
Notes \u2014 include the PR link if opened) and REMOVE the task from \`AI_PROGRESS.md\` (reset it to the empty
|
|
41641
|
+
template if nothing is in progress). Commit + push the base branch:
|
|
41642
|
+
\`git commit -am "chore(auto): finish TSK-\u2026 (PR opened)" && git push origin "$BASE"\` (a push rejected as
|
|
41643
|
+
non-fast-forward \u21D2 \`git pull --rebase origin "$BASE"\` then push again).
|
|
41644
|
+
|
|
41645
|
+
## Step 8 \u2014 Stop
|
|
41646
|
+
Print a one-line summary (task id + PR link or incident). STOP \u2014 the next cron tick runs the next cycle.
|
|
41647
|
+
The 4PM cli serializes cycles (one at a time); you do NOT manage any run lock.`;
|
|
41648
|
+
}
|
|
41649
|
+
var running = /* @__PURE__ */ new Set();
|
|
41650
|
+
async function runAutonomousCycle(ctx) {
|
|
41651
|
+
const root = ctx.physicRoot;
|
|
41652
|
+
const profileDir2 = ctx.profileDir;
|
|
41653
|
+
if (!root) {
|
|
41654
|
+
ctx.bus.autonomousDone(false, "no served project");
|
|
41655
|
+
return;
|
|
41656
|
+
}
|
|
41657
|
+
if (running.has(root)) {
|
|
41658
|
+
ctx.bus.autonomousDone(false, "a cycle is already running");
|
|
41659
|
+
return;
|
|
41660
|
+
}
|
|
41661
|
+
running.add(root);
|
|
41662
|
+
const log = (line) => dayLog(root, line);
|
|
41663
|
+
try {
|
|
41664
|
+
const cfg = await readAutonomousConfig(profileDir2);
|
|
41665
|
+
await pruneLogs(root, cfg.logRetentionDays);
|
|
41666
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
41667
|
+
if (!ctx.isReady) {
|
|
41668
|
+
await log("[skip] cli offline (no WS connection)");
|
|
41669
|
+
ctx.bus.autonomousDone(false, "cli offline");
|
|
41670
|
+
return;
|
|
41671
|
+
}
|
|
41672
|
+
if (cfg.paused) {
|
|
41673
|
+
await log("[skip] paused");
|
|
41674
|
+
ctx.bus.autonomousDone(false, "paused");
|
|
41675
|
+
return;
|
|
41676
|
+
}
|
|
41677
|
+
if (isInQuietHours(cfg.quietHours)) {
|
|
41678
|
+
await log("[skip] within quiet hours");
|
|
41679
|
+
ctx.bus.autonomousDone(false, "quiet hours");
|
|
41680
|
+
return;
|
|
41681
|
+
}
|
|
41682
|
+
let hist = await readHistories(root);
|
|
41683
|
+
if (cfg.maxTicksPerDay > 0 && todayTickCount(hist, today) >= cfg.maxTicksPerDay) {
|
|
41684
|
+
await log(`[skip] reached max ticks/day (${cfg.maxTicksPerDay})`);
|
|
41685
|
+
ctx.bus.autonomousDone(false, "max ticks reached");
|
|
41686
|
+
return;
|
|
41687
|
+
}
|
|
41688
|
+
if (!await hasWork(root)) {
|
|
41689
|
+
await log("[skip] no approved USER_TODO/USER_QA/AI_TODO work, AI_PROGRESS empty");
|
|
41690
|
+
ctx.bus.autonomousDone(false, "no work");
|
|
41691
|
+
return;
|
|
41692
|
+
}
|
|
41693
|
+
if (await allAiProfilesExhausted(ctx, cfg)) {
|
|
41694
|
+
await log(`[skip] all AI profiles over usage limit (\u2265 ${cfg.maxSessionPct}%/${cfg.maxWeeklyPct}%)`);
|
|
41695
|
+
await recordRun(root, hist, "skip", "all AI profiles over usage limit", today);
|
|
41696
|
+
ctx.bus.autonomousDone(false, "usage limit");
|
|
41697
|
+
return;
|
|
41698
|
+
}
|
|
41699
|
+
hist.ticks = { day: today, count: todayTickCount(hist, today) + 1 };
|
|
41700
|
+
await writeHistories(root, hist);
|
|
41701
|
+
await log(`[run] cycle (tick ${hist.ticks.count}/${today})`);
|
|
41702
|
+
const commandId = randomUUID2();
|
|
41703
|
+
const override = cfg.model ? { model: cfg.model } : void 0;
|
|
41704
|
+
try {
|
|
41705
|
+
await runAiPrompt(ctx, buildAutonomousCyclePrompt(), commandId, "local", false, void 0, override, false, true);
|
|
41706
|
+
hist = await readHistories(root);
|
|
41707
|
+
hist.consecutiveFails = 0;
|
|
41708
|
+
await recordRun(root, hist, "success", "cycle complete", today);
|
|
41709
|
+
await log("[done] cycle complete");
|
|
41710
|
+
ctx.bus.autonomousDone(true);
|
|
41711
|
+
} catch (err) {
|
|
41712
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
41713
|
+
hist = await readHistories(root);
|
|
41714
|
+
hist.consecutiveFails += 1;
|
|
41715
|
+
await recordRun(root, hist, "failure", msg, today);
|
|
41716
|
+
await log(`[warn] cycle failed: ${msg}`);
|
|
41717
|
+
if (cfg.stopOnConsecutiveFailures > 0 && hist.consecutiveFails >= cfg.stopOnConsecutiveFailures) {
|
|
41718
|
+
await writeAutonomousConfig(profileDir2, { ...cfg, paused: true });
|
|
41719
|
+
await log(`[stop] ${hist.consecutiveFails} consecutive failures \u2192 paused`);
|
|
41720
|
+
}
|
|
41721
|
+
ctx.bus.autonomousDone(false, msg);
|
|
41722
|
+
}
|
|
41723
|
+
} catch (err) {
|
|
41724
|
+
ctx.bus.autonomousDone(false, err instanceof Error ? err.message : String(err));
|
|
41725
|
+
} finally {
|
|
41726
|
+
running.delete(root);
|
|
41727
|
+
}
|
|
41728
|
+
}
|
|
41729
|
+
async function dayLog(root, line) {
|
|
41730
|
+
try {
|
|
41731
|
+
const dir2 = join20(root, ".claude", "logs");
|
|
41732
|
+
await mkdir3(dir2, { recursive: true });
|
|
41733
|
+
const day = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
41734
|
+
const ts2 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 19).replace("T", " ");
|
|
41735
|
+
await appendFile2(join20(dir2, `autonomous-tick-${day}.log`), `${ts2} ${line}
|
|
41736
|
+
`, "utf8");
|
|
41737
|
+
} catch {
|
|
41738
|
+
}
|
|
41739
|
+
}
|
|
41740
|
+
async function pruneLogs(root, retentionDays) {
|
|
41741
|
+
if (!(retentionDays > 0)) return;
|
|
41742
|
+
try {
|
|
41743
|
+
const dir2 = join20(root, ".claude", "logs");
|
|
41744
|
+
const cutoff = Date.now() - retentionDays * 864e5;
|
|
41745
|
+
for (const f2 of await readdir2(dir2)) {
|
|
41746
|
+
if (!/^autonomous-tick-.*\.log$/.test(f2)) continue;
|
|
41747
|
+
const st2 = await stat(join20(dir2, f2)).catch(() => null);
|
|
41748
|
+
if (st2 && st2.mtimeMs < cutoff) await rm2(join20(dir2, f2), { force: true }).catch(() => void 0);
|
|
41749
|
+
}
|
|
41750
|
+
} catch {
|
|
41751
|
+
}
|
|
41752
|
+
}
|
|
41753
|
+
async function recordRun(root, hist, status, note, today) {
|
|
41754
|
+
const ts2 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 19).replace("T", " ");
|
|
41755
|
+
pushRecord(hist, { ts: ts2, status, note, tick: `${today} #${hist.ticks.count}` });
|
|
41756
|
+
await writeHistories(root, hist);
|
|
41757
|
+
}
|
|
41758
|
+
async function readJsonObject(path) {
|
|
41759
|
+
try {
|
|
41760
|
+
return JSON.parse(await readFile5(path, "utf8"));
|
|
41761
|
+
} catch {
|
|
41762
|
+
return {};
|
|
41763
|
+
}
|
|
41764
|
+
}
|
|
41765
|
+
async function hasWork(root) {
|
|
41766
|
+
const approvals = await readJsonObject(join20(root, ".claude/.autonomous.approvals.json"));
|
|
41767
|
+
const approved = Object.entries(approvals).filter(([, v2]) => v2 && typeof v2 === "object" && v2.approved === true).map(([k2]) => k2);
|
|
41768
|
+
const read = (f2) => readFile5(join20(root, f2), "utf8").catch(() => "");
|
|
41769
|
+
const [userTodo, userQa, aiTodo, aiProgress] = await Promise.all([
|
|
41770
|
+
read("USER_TODO.md"),
|
|
41771
|
+
read("USER_QA.md"),
|
|
41772
|
+
read("AI_TODO.md"),
|
|
41773
|
+
read("AI_PROGRESS.md")
|
|
41774
|
+
]);
|
|
41775
|
+
const anyApproved = (prefix, text) => approved.some((id) => id.startsWith(prefix) && text.includes(id));
|
|
41776
|
+
return anyApproved("REQ-", userTodo) || anyApproved("QA-", userQa) || anyApproved("TSK-", aiTodo) || /TSK-\d{4}-\d{4}/.test(aiProgress);
|
|
41777
|
+
}
|
|
41778
|
+
function hasUsableCodex(config) {
|
|
41779
|
+
const mixed = config.aiProfiles?.some(
|
|
41780
|
+
(c) => c.provider === "codex" && c.enabled !== false && Boolean(c.profile?.trim())
|
|
41781
|
+
);
|
|
41782
|
+
const legacy = (config.codexHome ?? []).some((p2) => p2.enabled !== false && Boolean(p2.profile?.trim()));
|
|
41783
|
+
return Boolean(mixed || legacy);
|
|
41784
|
+
}
|
|
41785
|
+
async function allAiProfilesExhausted(ctx, cfg) {
|
|
41786
|
+
const config = readProfileConfig(ctx.profileDir);
|
|
41787
|
+
const active = activeProvider(config);
|
|
41788
|
+
if (active === "codex" || active === "antigravity") return false;
|
|
41789
|
+
if (active === null && hasUsableCodex(config)) return false;
|
|
41790
|
+
const dirs = claudeHomeDirs(config);
|
|
41791
|
+
if (dirs.length === 0) return false;
|
|
41792
|
+
const usages = await Promise.all(dirs.map((d) => checkClaudeUsage([d]).catch(() => null)));
|
|
41793
|
+
let readAny = false;
|
|
41794
|
+
for (const u2 of usages) {
|
|
41795
|
+
if (!u2) continue;
|
|
41796
|
+
readAny = true;
|
|
41797
|
+
if (u2.session.utilizationPct < cfg.maxSessionPct && u2.weekly.utilizationPct < cfg.maxWeeklyPct) {
|
|
41798
|
+
return false;
|
|
41799
|
+
}
|
|
41800
|
+
}
|
|
41801
|
+
return readAny;
|
|
41802
|
+
}
|
|
41803
|
+
|
|
41354
41804
|
// src/core/fs-browse.ts
|
|
41355
|
-
import { readdir as
|
|
41805
|
+
import { readdir as readdir3 } from "fs/promises";
|
|
41356
41806
|
import { resolve as resolve2, sep as sep2 } from "path";
|
|
41357
41807
|
async function listDir(path, root) {
|
|
41358
41808
|
if (!root) return { path: "", entries: [] };
|
|
@@ -41360,7 +41810,7 @@ async function listDir(path, root) {
|
|
|
41360
41810
|
const requested = resolve2(base, path && path.trim() ? path : ".");
|
|
41361
41811
|
const target = requested === base || requested.startsWith(base + sep2) ? requested : base;
|
|
41362
41812
|
try {
|
|
41363
|
-
const dirents = await
|
|
41813
|
+
const dirents = await readdir3(target, { withFileTypes: true });
|
|
41364
41814
|
const entries2 = dirents.map((d) => ({
|
|
41365
41815
|
name: d.name,
|
|
41366
41816
|
type: d.isDirectory() ? "dir" : "file"
|
|
@@ -41375,7 +41825,7 @@ async function listDir(path, root) {
|
|
|
41375
41825
|
}
|
|
41376
41826
|
|
|
41377
41827
|
// src/core/fs-read.ts
|
|
41378
|
-
import { readFile as
|
|
41828
|
+
import { readFile as readFile6 } from "fs/promises";
|
|
41379
41829
|
import { resolve as resolve3, sep as sep3 } from "path";
|
|
41380
41830
|
var MAX_BYTES = 256 * 1024;
|
|
41381
41831
|
function resolveInRoot(root, relPath) {
|
|
@@ -41390,7 +41840,7 @@ async function readWorkerFile(root, path) {
|
|
|
41390
41840
|
return { path, content: "", truncated: false };
|
|
41391
41841
|
}
|
|
41392
41842
|
try {
|
|
41393
|
-
const buf = await
|
|
41843
|
+
const buf = await readFile6(target);
|
|
41394
41844
|
const truncated = buf.byteLength > MAX_BYTES;
|
|
41395
41845
|
const content = buf.subarray(0, MAX_BYTES).toString("utf8");
|
|
41396
41846
|
return { path: target, content, truncated };
|
|
@@ -41400,7 +41850,7 @@ async function readWorkerFile(root, path) {
|
|
|
41400
41850
|
}
|
|
41401
41851
|
|
|
41402
41852
|
// src/core/fs-write.ts
|
|
41403
|
-
import { mkdir as
|
|
41853
|
+
import { mkdir as mkdir4, writeFile as writeFile5 } from "fs/promises";
|
|
41404
41854
|
import { dirname as dirname4, resolve as resolve4, sep as sep4 } from "path";
|
|
41405
41855
|
var MAX_BYTES2 = 2 * 1024 * 1024;
|
|
41406
41856
|
function resolveInRoot2(root, relPath) {
|
|
@@ -41419,8 +41869,8 @@ async function writeWorkerFile(root, relPath, content) {
|
|
|
41419
41869
|
return { ok: false, path: target, bytes: 0, error: `content too large (max ${MAX_BYTES2} bytes)` };
|
|
41420
41870
|
}
|
|
41421
41871
|
try {
|
|
41422
|
-
await
|
|
41423
|
-
await
|
|
41872
|
+
await mkdir4(dirname4(target), { recursive: true });
|
|
41873
|
+
await writeFile5(target, content, "utf8");
|
|
41424
41874
|
return { ok: true, path: target, bytes };
|
|
41425
41875
|
} catch (err) {
|
|
41426
41876
|
return { ok: false, path: target, bytes: 0, error: err instanceof Error ? err.message : String(err) };
|
|
@@ -41428,7 +41878,7 @@ async function writeWorkerFile(root, relPath, content) {
|
|
|
41428
41878
|
}
|
|
41429
41879
|
|
|
41430
41880
|
// src/core/fs-mutate.ts
|
|
41431
|
-
import { mkdir as
|
|
41881
|
+
import { mkdir as mkdir5, writeFile as writeFile6, rename, rm as rm3, stat as stat2 } from "fs/promises";
|
|
41432
41882
|
import { basename as basename3, dirname as dirname5, resolve as resolve5, sep as sep5 } from "path";
|
|
41433
41883
|
var MAX_BYTES3 = 2 * 1024 * 1024;
|
|
41434
41884
|
function resolveInRoot3(root, rel) {
|
|
@@ -41438,14 +41888,14 @@ function resolveInRoot3(root, rel) {
|
|
|
41438
41888
|
}
|
|
41439
41889
|
async function isDir(p2) {
|
|
41440
41890
|
try {
|
|
41441
|
-
return (await
|
|
41891
|
+
return (await stat2(p2)).isDirectory();
|
|
41442
41892
|
} catch {
|
|
41443
41893
|
return false;
|
|
41444
41894
|
}
|
|
41445
41895
|
}
|
|
41446
41896
|
async function exists(p2) {
|
|
41447
41897
|
try {
|
|
41448
|
-
await
|
|
41898
|
+
await stat2(p2);
|
|
41449
41899
|
return true;
|
|
41450
41900
|
} catch {
|
|
41451
41901
|
return false;
|
|
@@ -41459,7 +41909,7 @@ async function mutateFs(root, req) {
|
|
|
41459
41909
|
case "mkdir": {
|
|
41460
41910
|
const target = resolveInRoot3(root, req.path ?? "");
|
|
41461
41911
|
if (!target || target === resolve5(root)) return escaped(req.path ?? "");
|
|
41462
|
-
await
|
|
41912
|
+
await mkdir5(target, { recursive: true });
|
|
41463
41913
|
return { ok: true, path: target };
|
|
41464
41914
|
}
|
|
41465
41915
|
case "create": {
|
|
@@ -41470,8 +41920,8 @@ async function mutateFs(root, req) {
|
|
|
41470
41920
|
if (Buffer.byteLength(content, "utf8") > MAX_BYTES3) {
|
|
41471
41921
|
return { ok: false, path: target, error: `content too large (max ${MAX_BYTES3} bytes)` };
|
|
41472
41922
|
}
|
|
41473
|
-
await
|
|
41474
|
-
await
|
|
41923
|
+
await mkdir5(dirname5(target), { recursive: true });
|
|
41924
|
+
await writeFile6(target, content, "utf8");
|
|
41475
41925
|
return { ok: true, path: target };
|
|
41476
41926
|
}
|
|
41477
41927
|
case "move": {
|
|
@@ -41482,7 +41932,7 @@ async function mutateFs(root, req) {
|
|
|
41482
41932
|
if (await isDir(to2)) to2 = resolve5(to2, basename3(from));
|
|
41483
41933
|
if (to2 !== resolve5(root) && !to2.startsWith(resolve5(root) + sep5)) return escaped(req.to ?? "");
|
|
41484
41934
|
if (await exists(to2)) return { ok: false, path: to2, error: "destination already exists" };
|
|
41485
|
-
await
|
|
41935
|
+
await mkdir5(dirname5(to2), { recursive: true });
|
|
41486
41936
|
await rename(from, to2);
|
|
41487
41937
|
return { ok: true, path: to2 };
|
|
41488
41938
|
}
|
|
@@ -41490,7 +41940,7 @@ async function mutateFs(root, req) {
|
|
|
41490
41940
|
const target = resolveInRoot3(root, req.path ?? "");
|
|
41491
41941
|
if (!target) return escaped(req.path ?? "");
|
|
41492
41942
|
if (target === resolve5(root)) return { ok: false, path: target, error: "cannot delete the project root" };
|
|
41493
|
-
await
|
|
41943
|
+
await rm3(target, { recursive: true, force: true });
|
|
41494
41944
|
return { ok: true, path: target };
|
|
41495
41945
|
}
|
|
41496
41946
|
default:
|
|
@@ -41502,7 +41952,7 @@ async function mutateFs(root, req) {
|
|
|
41502
41952
|
}
|
|
41503
41953
|
|
|
41504
41954
|
// src/core/fs-transfer.ts
|
|
41505
|
-
import { mkdir as
|
|
41955
|
+
import { mkdir as mkdir6, readFile as readFile7, stat as stat3, writeFile as writeFile7 } from "fs/promises";
|
|
41506
41956
|
import { basename as basename4, dirname as dirname6, extname, resolve as resolve6, sep as sep6 } from "path";
|
|
41507
41957
|
function resolveInRoot4(root, relPath) {
|
|
41508
41958
|
if (!root) return null;
|
|
@@ -41546,8 +41996,8 @@ async function uploadBinaryFile(root, relPath, contentBase64) {
|
|
|
41546
41996
|
return { ok: false, path: target, bytes: 0, error: `file too large (max ${FS_TRANSFER_MAX_BYTES} bytes)` };
|
|
41547
41997
|
}
|
|
41548
41998
|
try {
|
|
41549
|
-
await
|
|
41550
|
-
await
|
|
41999
|
+
await mkdir6(dirname6(target), { recursive: true });
|
|
42000
|
+
await writeFile7(target, buf);
|
|
41551
42001
|
return { ok: true, path: target, bytes: buf.byteLength };
|
|
41552
42002
|
} catch (err) {
|
|
41553
42003
|
return { ok: false, path: target, bytes: 0, error: err instanceof Error ? err.message : String(err) };
|
|
@@ -41557,12 +42007,12 @@ async function downloadBinaryFile(root, relPath) {
|
|
|
41557
42007
|
const target = resolveInRoot4(root, relPath);
|
|
41558
42008
|
if (!target) return { error: "path escapes the project root" };
|
|
41559
42009
|
try {
|
|
41560
|
-
const info = await
|
|
42010
|
+
const info = await stat3(target);
|
|
41561
42011
|
if (!info.isFile()) return { error: "not a file" };
|
|
41562
42012
|
if (info.size > FS_TRANSFER_MAX_BYTES) {
|
|
41563
42013
|
return { error: `file too large (max ${FS_TRANSFER_MAX_BYTES} bytes)` };
|
|
41564
42014
|
}
|
|
41565
|
-
const buf = await
|
|
42015
|
+
const buf = await readFile7(target);
|
|
41566
42016
|
return {
|
|
41567
42017
|
contentBase64: buf.toString("base64"),
|
|
41568
42018
|
contentType: guessMime(target),
|
|
@@ -41621,7 +42071,7 @@ function handleFsChannels(ctx, message, payload) {
|
|
|
41621
42071
|
}
|
|
41622
42072
|
|
|
41623
42073
|
// src/core/git-diff.ts
|
|
41624
|
-
import { readFile as
|
|
42074
|
+
import { readFile as readFile8 } from "fs/promises";
|
|
41625
42075
|
import { execFile as execFile6 } from "child_process";
|
|
41626
42076
|
import { basename as basename5, dirname as dirname7, resolve as resolve7, sep as sep7 } from "path";
|
|
41627
42077
|
import { promisify as promisify6 } from "util";
|
|
@@ -41648,7 +42098,7 @@ async function gitDiff(root, path) {
|
|
|
41648
42098
|
const base = basename5(target);
|
|
41649
42099
|
let newBuf = null;
|
|
41650
42100
|
try {
|
|
41651
|
-
newBuf = await
|
|
42101
|
+
newBuf = await readFile8(target);
|
|
41652
42102
|
} catch {
|
|
41653
42103
|
}
|
|
41654
42104
|
let oldBuf = null;
|
|
@@ -41715,18 +42165,18 @@ ${stderr}`;
|
|
|
41715
42165
|
|
|
41716
42166
|
// src/core/git-ssh-key.ts
|
|
41717
42167
|
import { execFile as execFile8 } from "child_process";
|
|
41718
|
-
import { readFile as
|
|
41719
|
-
import { join as
|
|
42168
|
+
import { readFile as readFile9, rm as rm4, mkdir as mkdir7 } from "fs/promises";
|
|
42169
|
+
import { join as join21 } from "path";
|
|
41720
42170
|
import { promisify as promisify8 } from "util";
|
|
41721
42171
|
var run8 = promisify8(execFile8);
|
|
41722
42172
|
function keyPaths(profileDir2) {
|
|
41723
|
-
const dir2 =
|
|
41724
|
-
const key =
|
|
42173
|
+
const dir2 = join21(profileDir2, ".ssh");
|
|
42174
|
+
const key = join21(dir2, "id_4pm");
|
|
41725
42175
|
return { dir: dir2, key, pub: `${key}.pub` };
|
|
41726
42176
|
}
|
|
41727
42177
|
async function readPublicKey(pub) {
|
|
41728
42178
|
try {
|
|
41729
|
-
return (await
|
|
42179
|
+
return (await readFile9(pub, "utf8")).trim();
|
|
41730
42180
|
} catch {
|
|
41731
42181
|
return null;
|
|
41732
42182
|
}
|
|
@@ -41748,9 +42198,9 @@ async function configureGit(key) {
|
|
|
41748
42198
|
}
|
|
41749
42199
|
async function generate(profileDir2) {
|
|
41750
42200
|
const { dir: dir2, key, pub } = keyPaths(profileDir2);
|
|
41751
|
-
await
|
|
41752
|
-
await
|
|
41753
|
-
await
|
|
42201
|
+
await mkdir7(dir2, { recursive: true });
|
|
42202
|
+
await rm4(key, { force: true });
|
|
42203
|
+
await rm4(pub, { force: true });
|
|
41754
42204
|
await run8("ssh-keygen", ["-t", "ed25519", "-f", key, "-N", "", "-C", "4pm-deploy-key"], {
|
|
41755
42205
|
timeout: 15e3
|
|
41756
42206
|
});
|
|
@@ -41759,8 +42209,8 @@ async function generate(profileDir2) {
|
|
|
41759
42209
|
}
|
|
41760
42210
|
async function remove(profileDir2) {
|
|
41761
42211
|
const { key, pub } = keyPaths(profileDir2);
|
|
41762
|
-
await
|
|
41763
|
-
await
|
|
42212
|
+
await rm4(key, { force: true });
|
|
42213
|
+
await rm4(pub, { force: true });
|
|
41764
42214
|
await run8("git", ["config", "--global", "--unset", "core.sshCommand"], { timeout: 5e3 }).catch(
|
|
41765
42215
|
() => void 0
|
|
41766
42216
|
);
|
|
@@ -41934,7 +42384,7 @@ function handleGitChannels(ctx, message, payload) {
|
|
|
41934
42384
|
}
|
|
41935
42385
|
|
|
41936
42386
|
// src/core/ws-client/handlers/misc.ts
|
|
41937
|
-
import { join as
|
|
42387
|
+
import { join as join22 } from "path";
|
|
41938
42388
|
|
|
41939
42389
|
// src/core/outbound-review.ts
|
|
41940
42390
|
function compilePatterns(sources, fallback) {
|
|
@@ -42016,7 +42466,7 @@ function handleMiscChannels(ctx, message, payload) {
|
|
|
42016
42466
|
}
|
|
42017
42467
|
case WsChannels.LOG_READ: {
|
|
42018
42468
|
const limit = Math.min(Math.max(payload.limit ?? 200, 1), 2e3);
|
|
42019
|
-
const lines = readRecentLogLines(
|
|
42469
|
+
const lines = readRecentLogLines(join22(ctx.profileDir, "logs"), limit);
|
|
42020
42470
|
ctx.send(WsChannels.LOG_READ, { lines }, message.id);
|
|
42021
42471
|
return true;
|
|
42022
42472
|
}
|
|
@@ -42048,14 +42498,14 @@ function handleMiscChannels(ctx, message, payload) {
|
|
|
42048
42498
|
|
|
42049
42499
|
// src/core/ws-client/handlers/project.ts
|
|
42050
42500
|
import { existsSync as existsSync13, mkdirSync as mkdirSync6, renameSync, rmSync as rmSync6 } from "fs";
|
|
42051
|
-
import { join as
|
|
42501
|
+
import { join as join23 } from "path";
|
|
42052
42502
|
function handleProjectChannels(ctx, message, payload) {
|
|
42053
42503
|
switch (message.channel) {
|
|
42054
42504
|
case WsChannels.PHYSIC_SYNC: {
|
|
42055
42505
|
const sync = payload;
|
|
42056
42506
|
const oldName = sync.oldName;
|
|
42057
|
-
const oldPath = oldName ?
|
|
42058
|
-
const newPath =
|
|
42507
|
+
const oldPath = oldName ? join23(ctx.profileDir, oldName) : null;
|
|
42508
|
+
const newPath = join23(ctx.profileDir, sync.newName);
|
|
42059
42509
|
if (oldName) {
|
|
42060
42510
|
const oldRoot = ctx.physicFolderPath(oldName);
|
|
42061
42511
|
if (oldRoot) void uninstallCron(oldRoot).catch(() => void 0);
|
|
@@ -42077,7 +42527,7 @@ function handleProjectChannels(ctx, message, payload) {
|
|
|
42077
42527
|
if (del.name) {
|
|
42078
42528
|
const delRoot = ctx.physicFolderPath(del.name);
|
|
42079
42529
|
if (delRoot) void uninstallCron(delRoot).catch(() => void 0);
|
|
42080
|
-
rmSync6(
|
|
42530
|
+
rmSync6(join23(ctx.profileDir, del.name), { recursive: true, force: true });
|
|
42081
42531
|
ctx.bus.log(t("project.folderDeleted", { name: del.name }));
|
|
42082
42532
|
}
|
|
42083
42533
|
void manageSshKey("delete", ctx.profileDir).catch(() => void 0);
|
|
@@ -42346,8 +42796,8 @@ function handleToolsChannels(ctx, message, payload) {
|
|
|
42346
42796
|
}
|
|
42347
42797
|
|
|
42348
42798
|
// src/core/agents.ts
|
|
42349
|
-
import { readFile as
|
|
42350
|
-
import { join as
|
|
42799
|
+
import { readFile as readFile10, writeFile as writeFile8, mkdir as mkdir8, readdir as readdir4, rm as rm5 } from "fs/promises";
|
|
42800
|
+
import { join as join24 } from "path";
|
|
42351
42801
|
var AGENTS_REL = ".claude/agents";
|
|
42352
42802
|
var SKILLS_REL = ".claude/skills";
|
|
42353
42803
|
var MEMORY_DIRS = /* @__PURE__ */ new Set(["short-memory", "long-memory"]);
|
|
@@ -42356,7 +42806,7 @@ function safeName(name) {
|
|
|
42356
42806
|
}
|
|
42357
42807
|
async function readText2(path, fallback = "") {
|
|
42358
42808
|
try {
|
|
42359
|
-
return await
|
|
42809
|
+
return await readFile10(path, "utf8");
|
|
42360
42810
|
} catch {
|
|
42361
42811
|
return fallback;
|
|
42362
42812
|
}
|
|
@@ -42371,17 +42821,17 @@ async function listAgents(root) {
|
|
|
42371
42821
|
const subagents = [];
|
|
42372
42822
|
const skills = [];
|
|
42373
42823
|
try {
|
|
42374
|
-
for (const e of await
|
|
42824
|
+
for (const e of await readdir4(join24(root, AGENTS_REL), { withFileTypes: true })) {
|
|
42375
42825
|
if (!e.isFile() || !e.name.endsWith(".md") || MEMORY_DIRS.has(e.name)) continue;
|
|
42376
|
-
const fm = frontmatter(await readText2(
|
|
42826
|
+
const fm = frontmatter(await readText2(join24(root, AGENTS_REL, e.name)));
|
|
42377
42827
|
subagents.push({ name: e.name.replace(/\.md$/, ""), description: fm.description });
|
|
42378
42828
|
}
|
|
42379
42829
|
} catch {
|
|
42380
42830
|
}
|
|
42381
42831
|
try {
|
|
42382
|
-
for (const e of await
|
|
42832
|
+
for (const e of await readdir4(join24(root, SKILLS_REL), { withFileTypes: true })) {
|
|
42383
42833
|
if (!e.isDirectory()) continue;
|
|
42384
|
-
const fm = frontmatter(await readText2(
|
|
42834
|
+
const fm = frontmatter(await readText2(join24(root, SKILLS_REL, e.name, "SKILL.md")));
|
|
42385
42835
|
skills.push({ name: e.name, description: fm.description });
|
|
42386
42836
|
}
|
|
42387
42837
|
} catch {
|
|
@@ -42392,15 +42842,15 @@ async function listAgents(root) {
|
|
|
42392
42842
|
}
|
|
42393
42843
|
function itemPath(root, kind, name) {
|
|
42394
42844
|
const n = safeName(name);
|
|
42395
|
-
return kind === "subagent" ?
|
|
42845
|
+
return kind === "subagent" ? join24(root, AGENTS_REL, `${n}.md`) : join24(root, SKILLS_REL, n, "SKILL.md");
|
|
42396
42846
|
}
|
|
42397
42847
|
async function readAgent(root, kind, name) {
|
|
42398
42848
|
const content = await readText2(itemPath(root, kind, name));
|
|
42399
42849
|
if (kind !== "subagent") return { content };
|
|
42400
42850
|
const n = safeName(name);
|
|
42401
42851
|
const [short, long] = await Promise.all([
|
|
42402
|
-
readText2(
|
|
42403
|
-
readText2(
|
|
42852
|
+
readText2(join24(root, AGENTS_REL, "short-memory", `${n}.md`)),
|
|
42853
|
+
readText2(join24(root, AGENTS_REL, "long-memory", `${n}.md`))
|
|
42404
42854
|
]);
|
|
42405
42855
|
return { content, memory: { short, long } };
|
|
42406
42856
|
}
|
|
@@ -42409,14 +42859,14 @@ async function writeAgent(root, req) {
|
|
|
42409
42859
|
if (!n) return { ok: false, error: "invalid name" };
|
|
42410
42860
|
try {
|
|
42411
42861
|
if (req.action === "delete") {
|
|
42412
|
-
if (req.kind === "subagent") await
|
|
42413
|
-
else await
|
|
42862
|
+
if (req.kind === "subagent") await rm5(itemPath(root, "subagent", n), { force: true });
|
|
42863
|
+
else await rm5(join24(root, SKILLS_REL, n), { recursive: true, force: true });
|
|
42414
42864
|
return { ok: true };
|
|
42415
42865
|
}
|
|
42416
42866
|
if (req.content === void 0) return { ok: false, error: "content is required" };
|
|
42417
42867
|
const path = itemPath(root, req.kind, n);
|
|
42418
|
-
await
|
|
42419
|
-
await
|
|
42868
|
+
await mkdir8(join24(path, ".."), { recursive: true });
|
|
42869
|
+
await writeFile8(path, req.content, "utf8");
|
|
42420
42870
|
return { ok: true };
|
|
42421
42871
|
} catch (err) {
|
|
42422
42872
|
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
@@ -42424,21 +42874,21 @@ async function writeAgent(root, req) {
|
|
|
42424
42874
|
}
|
|
42425
42875
|
|
|
42426
42876
|
// src/core/secrets.ts
|
|
42427
|
-
import { readFile as
|
|
42428
|
-
import { join as
|
|
42877
|
+
import { readFile as readFile11, writeFile as writeFile9 } from "fs/promises";
|
|
42878
|
+
import { join as join25 } from "path";
|
|
42429
42879
|
var SECURITY_REL = "AI_SECURITY.md";
|
|
42430
42880
|
var PLACEHOLDER_REL = "AI_PLACEHOLDER.md";
|
|
42431
42881
|
var SECRETS_REL = "project.secrets.json";
|
|
42432
42882
|
async function readText3(path, fallback = "") {
|
|
42433
42883
|
try {
|
|
42434
|
-
return await
|
|
42884
|
+
return await readFile11(path, "utf8");
|
|
42435
42885
|
} catch {
|
|
42436
42886
|
return fallback;
|
|
42437
42887
|
}
|
|
42438
42888
|
}
|
|
42439
42889
|
async function readSecretsObject(root) {
|
|
42440
42890
|
try {
|
|
42441
|
-
const v2 = JSON.parse(await
|
|
42891
|
+
const v2 = JSON.parse(await readFile11(join25(root, SECRETS_REL), "utf8"));
|
|
42442
42892
|
return v2 && typeof v2 === "object" ? v2 : {};
|
|
42443
42893
|
} catch {
|
|
42444
42894
|
return {};
|
|
@@ -42457,8 +42907,8 @@ function placeholderKeys(text) {
|
|
|
42457
42907
|
}
|
|
42458
42908
|
async function readSecrets(root) {
|
|
42459
42909
|
const [security, placeholder, secrets] = await Promise.all([
|
|
42460
|
-
readText3(
|
|
42461
|
-
readText3(
|
|
42910
|
+
readText3(join25(root, SECURITY_REL)),
|
|
42911
|
+
readText3(join25(root, PLACEHOLDER_REL)),
|
|
42462
42912
|
readSecretsObject(root)
|
|
42463
42913
|
]);
|
|
42464
42914
|
const names = /* @__PURE__ */ new Set([...placeholderKeys(placeholder), ...Object.keys(secrets)]);
|
|
@@ -42471,14 +42921,14 @@ async function readSecrets(root) {
|
|
|
42471
42921
|
async function writeSecrets(root, req) {
|
|
42472
42922
|
try {
|
|
42473
42923
|
if (req.kind === "security") {
|
|
42474
|
-
await
|
|
42924
|
+
await writeFile9(join25(root, SECURITY_REL), req.content, "utf8");
|
|
42475
42925
|
} else if (req.kind === "placeholder") {
|
|
42476
|
-
await
|
|
42926
|
+
await writeFile9(join25(root, PLACEHOLDER_REL), req.content, "utf8");
|
|
42477
42927
|
} else {
|
|
42478
42928
|
const secrets = await readSecretsObject(root);
|
|
42479
42929
|
if (req.kind === "secret") secrets[req.key] = req.value;
|
|
42480
42930
|
else delete secrets[req.key];
|
|
42481
|
-
await
|
|
42931
|
+
await writeFile9(join25(root, SECRETS_REL), JSON.stringify(secrets, null, 2) + "\n", "utf8");
|
|
42482
42932
|
}
|
|
42483
42933
|
return { ok: true };
|
|
42484
42934
|
} catch (err) {
|
|
@@ -42487,18 +42937,18 @@ async function writeSecrets(root, req) {
|
|
|
42487
42937
|
}
|
|
42488
42938
|
|
|
42489
42939
|
// src/core/agent-tools.ts
|
|
42490
|
-
import { readFile as
|
|
42491
|
-
import { dirname as dirname8, join as
|
|
42940
|
+
import { readFile as readFile12, writeFile as writeFile10, mkdir as mkdir9 } from "fs/promises";
|
|
42941
|
+
import { dirname as dirname8, join as join26 } from "path";
|
|
42492
42942
|
var SHARED_REL = ".claude/settings.json";
|
|
42493
42943
|
var LOCAL_REL = ".claude/settings.local.json";
|
|
42494
42944
|
var SECRETS_DENY = "Read(./project.secrets.json)";
|
|
42495
42945
|
var MODES = ["default", "acceptEdits", "plan", "bypassPermissions"];
|
|
42496
42946
|
function fileFor(root, scope) {
|
|
42497
|
-
return
|
|
42947
|
+
return join26(root, scope === "shared" ? SHARED_REL : LOCAL_REL);
|
|
42498
42948
|
}
|
|
42499
42949
|
async function readSettingsObject(path) {
|
|
42500
42950
|
try {
|
|
42501
|
-
const v2 = JSON.parse(await
|
|
42951
|
+
const v2 = JSON.parse(await readFile12(path, "utf8"));
|
|
42502
42952
|
return v2 && typeof v2 === "object" ? v2 : {};
|
|
42503
42953
|
} catch {
|
|
42504
42954
|
return {};
|
|
@@ -42521,8 +42971,8 @@ async function writeAgentTools(root, scope, permissions) {
|
|
|
42521
42971
|
const next = normalizePermissions(permissions);
|
|
42522
42972
|
if (scope === "shared" && !next.deny.includes(SECRETS_DENY)) next.deny = [...next.deny, SECRETS_DENY];
|
|
42523
42973
|
obj.permissions = next;
|
|
42524
|
-
await
|
|
42525
|
-
await
|
|
42974
|
+
await mkdir9(dirname8(path), { recursive: true });
|
|
42975
|
+
await writeFile10(path, JSON.stringify(obj, null, 2) + "\n", "utf8");
|
|
42526
42976
|
return { ok: true };
|
|
42527
42977
|
} catch (err) {
|
|
42528
42978
|
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
@@ -42531,8 +42981,8 @@ async function writeAgentTools(root, scope, permissions) {
|
|
|
42531
42981
|
|
|
42532
42982
|
// src/core/packages.ts
|
|
42533
42983
|
import { createHash as createHash4 } from "crypto";
|
|
42534
|
-
import { readFile as
|
|
42535
|
-
import { dirname as dirname9, join as
|
|
42984
|
+
import { readFile as readFile13, writeFile as writeFile11, mkdir as mkdir10, readdir as readdir5, rm as rm6, stat as stat4 } from "fs/promises";
|
|
42985
|
+
import { dirname as dirname9, join as join27, relative } from "path";
|
|
42536
42986
|
var AGENTS_REL2 = ".claude/agents";
|
|
42537
42987
|
var SKILLS_REL2 = ".claude/skills";
|
|
42538
42988
|
var MANIFEST_REL = ".claude/.4pm-packages.json";
|
|
@@ -42541,7 +42991,7 @@ function safeName2(name) {
|
|
|
42541
42991
|
}
|
|
42542
42992
|
async function readMaybe(path) {
|
|
42543
42993
|
try {
|
|
42544
|
-
return await
|
|
42994
|
+
return await readFile13(path);
|
|
42545
42995
|
} catch {
|
|
42546
42996
|
return null;
|
|
42547
42997
|
}
|
|
@@ -42551,12 +43001,12 @@ async function listFilesRel(dir2) {
|
|
|
42551
43001
|
async function walk2(d) {
|
|
42552
43002
|
let entries2;
|
|
42553
43003
|
try {
|
|
42554
|
-
entries2 = await
|
|
43004
|
+
entries2 = await readdir5(d, { withFileTypes: true });
|
|
42555
43005
|
} catch {
|
|
42556
43006
|
return;
|
|
42557
43007
|
}
|
|
42558
43008
|
for (const e of entries2) {
|
|
42559
|
-
const full =
|
|
43009
|
+
const full = join27(d, e.name);
|
|
42560
43010
|
if (e.isDirectory()) await walk2(full);
|
|
42561
43011
|
else if (e.isFile()) out.push(relative(dir2, full));
|
|
42562
43012
|
}
|
|
@@ -42567,16 +43017,16 @@ async function listFilesRel(dir2) {
|
|
|
42567
43017
|
async function readArtifactFiles(root, kind, slug) {
|
|
42568
43018
|
const files = [];
|
|
42569
43019
|
if (kind === "subagent") {
|
|
42570
|
-
const agent = await readMaybe(
|
|
43020
|
+
const agent = await readMaybe(join27(root, AGENTS_REL2, `${slug}.md`));
|
|
42571
43021
|
if (agent) files.push({ path: "agent.md", contentBase64: agent.toString("base64") });
|
|
42572
|
-
const short = await readMaybe(
|
|
43022
|
+
const short = await readMaybe(join27(root, AGENTS_REL2, "short-memory", `${slug}.md`));
|
|
42573
43023
|
if (short) files.push({ path: "short-memory.md", contentBase64: short.toString("base64") });
|
|
42574
|
-
const long = await readMaybe(
|
|
43024
|
+
const long = await readMaybe(join27(root, AGENTS_REL2, "long-memory", `${slug}.md`));
|
|
42575
43025
|
if (long) files.push({ path: "long-memory.md", contentBase64: long.toString("base64") });
|
|
42576
43026
|
} else {
|
|
42577
|
-
const base =
|
|
43027
|
+
const base = join27(root, SKILLS_REL2, slug);
|
|
42578
43028
|
for (const rel of await listFilesRel(base)) {
|
|
42579
|
-
const buf = await readMaybe(
|
|
43029
|
+
const buf = await readMaybe(join27(base, rel));
|
|
42580
43030
|
if (buf) files.push({ path: rel.split("\\").join("/"), contentBase64: buf.toString("base64") });
|
|
42581
43031
|
}
|
|
42582
43032
|
}
|
|
@@ -42594,16 +43044,16 @@ async function artifactSha256(root, kind, slug) {
|
|
|
42594
43044
|
return h.digest("hex");
|
|
42595
43045
|
}
|
|
42596
43046
|
async function artifactExists(root, kind, slug) {
|
|
42597
|
-
const target = kind === "subagent" ?
|
|
43047
|
+
const target = kind === "subagent" ? join27(root, AGENTS_REL2, `${slug}.md`) : join27(root, SKILLS_REL2, slug);
|
|
42598
43048
|
try {
|
|
42599
|
-
await
|
|
43049
|
+
await stat4(target);
|
|
42600
43050
|
return true;
|
|
42601
43051
|
} catch {
|
|
42602
43052
|
return false;
|
|
42603
43053
|
}
|
|
42604
43054
|
}
|
|
42605
43055
|
async function readManifest(root) {
|
|
42606
|
-
const buf = await readMaybe(
|
|
43056
|
+
const buf = await readMaybe(join27(root, MANIFEST_REL));
|
|
42607
43057
|
if (!buf) return {};
|
|
42608
43058
|
try {
|
|
42609
43059
|
return JSON.parse(buf.toString("utf8"));
|
|
@@ -42612,35 +43062,35 @@ async function readManifest(root) {
|
|
|
42612
43062
|
}
|
|
42613
43063
|
}
|
|
42614
43064
|
async function writeManifest(root, manifest) {
|
|
42615
|
-
const path =
|
|
42616
|
-
await
|
|
42617
|
-
await
|
|
43065
|
+
const path = join27(root, MANIFEST_REL);
|
|
43066
|
+
await mkdir10(dirname9(path), { recursive: true });
|
|
43067
|
+
await writeFile11(path, JSON.stringify(manifest, null, 2), "utf8");
|
|
42618
43068
|
}
|
|
42619
43069
|
async function writeArtifactFiles(root, kind, slug, files) {
|
|
42620
43070
|
const byPath = new Map(files.map((f2) => [f2.path, Buffer.from(f2.contentBase64, "base64")]));
|
|
42621
43071
|
if (kind === "subagent") {
|
|
42622
43072
|
const agent = byPath.get("agent.md");
|
|
42623
43073
|
if (agent) {
|
|
42624
|
-
await
|
|
42625
|
-
await
|
|
43074
|
+
await mkdir10(join27(root, AGENTS_REL2), { recursive: true });
|
|
43075
|
+
await writeFile11(join27(root, AGENTS_REL2, `${slug}.md`), agent);
|
|
42626
43076
|
}
|
|
42627
43077
|
const short = byPath.get("short-memory.md");
|
|
42628
43078
|
if (short) {
|
|
42629
|
-
await
|
|
42630
|
-
await
|
|
43079
|
+
await mkdir10(join27(root, AGENTS_REL2, "short-memory"), { recursive: true });
|
|
43080
|
+
await writeFile11(join27(root, AGENTS_REL2, "short-memory", `${slug}.md`), short);
|
|
42631
43081
|
}
|
|
42632
43082
|
const long = byPath.get("long-memory.md");
|
|
42633
43083
|
if (long) {
|
|
42634
|
-
await
|
|
42635
|
-
await
|
|
43084
|
+
await mkdir10(join27(root, AGENTS_REL2, "long-memory"), { recursive: true });
|
|
43085
|
+
await writeFile11(join27(root, AGENTS_REL2, "long-memory", `${slug}.md`), long);
|
|
42636
43086
|
}
|
|
42637
43087
|
} else {
|
|
42638
|
-
const base =
|
|
43088
|
+
const base = join27(root, SKILLS_REL2, slug);
|
|
42639
43089
|
for (const [rel, buf] of byPath) {
|
|
42640
43090
|
if (rel.includes("..") || rel.startsWith("/")) continue;
|
|
42641
|
-
const dest =
|
|
42642
|
-
await
|
|
42643
|
-
await
|
|
43091
|
+
const dest = join27(base, rel);
|
|
43092
|
+
await mkdir10(dirname9(dest), { recursive: true });
|
|
43093
|
+
await writeFile11(dest, buf);
|
|
42644
43094
|
}
|
|
42645
43095
|
}
|
|
42646
43096
|
}
|
|
@@ -42704,11 +43154,11 @@ async function removePackage(root, slug) {
|
|
|
42704
43154
|
const entry = manifest[n];
|
|
42705
43155
|
if (!entry) return { ok: false, error: "not installed" };
|
|
42706
43156
|
if (entry.kind === "subagent") {
|
|
42707
|
-
await
|
|
42708
|
-
await
|
|
42709
|
-
await
|
|
43157
|
+
await rm6(join27(root, AGENTS_REL2, `${n}.md`), { force: true });
|
|
43158
|
+
await rm6(join27(root, AGENTS_REL2, "short-memory", `${n}.md`), { force: true });
|
|
43159
|
+
await rm6(join27(root, AGENTS_REL2, "long-memory", `${n}.md`), { force: true });
|
|
42710
43160
|
} else {
|
|
42711
|
-
await
|
|
43161
|
+
await rm6(join27(root, SKILLS_REL2, n), { recursive: true, force: true });
|
|
42712
43162
|
}
|
|
42713
43163
|
delete manifest[n];
|
|
42714
43164
|
await writeManifest(root, manifest);
|
|
@@ -42719,8 +43169,8 @@ async function removePackage(root, slug) {
|
|
|
42719
43169
|
}
|
|
42720
43170
|
|
|
42721
43171
|
// src/core/graph.ts
|
|
42722
|
-
import { readFile as
|
|
42723
|
-
import { join as
|
|
43172
|
+
import { readFile as readFile14, readdir as readdir6 } from "fs/promises";
|
|
43173
|
+
import { join as join28, relative as relative2, dirname as dirname10, resolve as resolve9, extname as extname2, basename as basename6 } from "path";
|
|
42724
43174
|
var MAX_FILES = 2e3;
|
|
42725
43175
|
var MAX_NODES = 3e3;
|
|
42726
43176
|
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".next", "out", "coverage", ".turbo"]);
|
|
@@ -42732,7 +43182,7 @@ async function walk(root, accept) {
|
|
|
42732
43182
|
const dir2 = stack.pop();
|
|
42733
43183
|
let entries2;
|
|
42734
43184
|
try {
|
|
42735
|
-
entries2 = await
|
|
43185
|
+
entries2 = await readdir6(dir2, { withFileTypes: true });
|
|
42736
43186
|
} catch {
|
|
42737
43187
|
continue;
|
|
42738
43188
|
}
|
|
@@ -42740,7 +43190,7 @@ async function walk(root, accept) {
|
|
|
42740
43190
|
if (e.name.startsWith(".") && e.name !== ".claude") {
|
|
42741
43191
|
if (e.isDirectory()) continue;
|
|
42742
43192
|
}
|
|
42743
|
-
const full =
|
|
43193
|
+
const full = join28(dir2, e.name);
|
|
42744
43194
|
if (e.isDirectory()) {
|
|
42745
43195
|
if (!SKIP_DIRS.has(e.name)) stack.push(full);
|
|
42746
43196
|
} else if (accept(full)) {
|
|
@@ -42768,7 +43218,7 @@ async function buildDocs(root) {
|
|
|
42768
43218
|
const rel = relative2(root, f2).split("\\").join("/");
|
|
42769
43219
|
let text = "";
|
|
42770
43220
|
try {
|
|
42771
|
-
text = await
|
|
43221
|
+
text = await readFile14(f2, "utf8");
|
|
42772
43222
|
} catch {
|
|
42773
43223
|
continue;
|
|
42774
43224
|
}
|
|
@@ -42807,7 +43257,7 @@ async function buildCode(root) {
|
|
|
42807
43257
|
const rel = relative2(root, f2).split("\\").join("/");
|
|
42808
43258
|
let text = "";
|
|
42809
43259
|
try {
|
|
42810
|
-
text = await
|
|
43260
|
+
text = await readFile14(f2, "utf8");
|
|
42811
43261
|
} catch {
|
|
42812
43262
|
continue;
|
|
42813
43263
|
}
|
|
@@ -42859,11 +43309,11 @@ async function buildGraph(root, mode) {
|
|
|
42859
43309
|
}
|
|
42860
43310
|
|
|
42861
43311
|
// src/core/rag.ts
|
|
42862
|
-
import { readFile as
|
|
43312
|
+
import { readFile as readFile15, writeFile as writeFile12, statfs as statfs2, mkdir as mkdir11 } from "fs/promises";
|
|
42863
43313
|
import { existsSync as existsSync14 } from "fs";
|
|
42864
43314
|
import { execFile as execFile10, spawn as spawn9 } from "child_process";
|
|
42865
43315
|
import { totalmem as totalmem2, cpus as cpus2 } from "os";
|
|
42866
|
-
import { join as
|
|
43316
|
+
import { join as join29 } from "path";
|
|
42867
43317
|
import { promisify as promisify10 } from "util";
|
|
42868
43318
|
var run10 = promisify10(execFile10);
|
|
42869
43319
|
var RAG_REL = ".claude/rag";
|
|
@@ -42884,7 +43334,7 @@ async function detectPython() {
|
|
|
42884
43334
|
}
|
|
42885
43335
|
async function installLog(root) {
|
|
42886
43336
|
try {
|
|
42887
|
-
const text = await
|
|
43337
|
+
const text = await readFile15(join29(root, RAG_REL, "install.log"), "utf8");
|
|
42888
43338
|
return text.split("\n").filter((l) => l.length > 0).slice(-200);
|
|
42889
43339
|
} catch {
|
|
42890
43340
|
return [];
|
|
@@ -42946,14 +43396,14 @@ async function ragStatus(root) {
|
|
|
42946
43396
|
};
|
|
42947
43397
|
let modelName = "";
|
|
42948
43398
|
try {
|
|
42949
|
-
modelName = (await
|
|
43399
|
+
modelName = (await readFile15(join29(root, RAG_REL, ".installed"), "utf8")).trim();
|
|
42950
43400
|
} catch {
|
|
42951
43401
|
modelName = "";
|
|
42952
43402
|
}
|
|
42953
43403
|
const modelPresent = modelName.length > 0;
|
|
42954
43404
|
return {
|
|
42955
43405
|
installed: fastembed && sqliteVec && modelPresent,
|
|
42956
|
-
installing: existsSync14(
|
|
43406
|
+
installing: existsSync14(join29(root, RAG_REL, ".installing")),
|
|
42957
43407
|
python: { found: py.found, version: py.version },
|
|
42958
43408
|
deps: { pip, fastembed, sqliteVec },
|
|
42959
43409
|
model: { present: modelPresent, name: modelName },
|
|
@@ -42964,9 +43414,9 @@ async function ragStatus(root) {
|
|
|
42964
43414
|
};
|
|
42965
43415
|
}
|
|
42966
43416
|
async function readIndexMeta(root) {
|
|
42967
|
-
const indexing = existsSync14(
|
|
43417
|
+
const indexing = existsSync14(join29(root, RAG_REL, ".indexing"));
|
|
42968
43418
|
try {
|
|
42969
|
-
const m2 = JSON.parse(await
|
|
43419
|
+
const m2 = JSON.parse(await readFile15(join29(root, RAG_REL, ".index.json"), "utf8"));
|
|
42970
43420
|
return {
|
|
42971
43421
|
present: true,
|
|
42972
43422
|
indexing,
|
|
@@ -43033,14 +43483,14 @@ if __name__ == "__main__":
|
|
|
43033
43483
|
elif cmd == "query": query(sys.argv[2], int(sys.argv[3]) if len(sys.argv) > 3 else 8)
|
|
43034
43484
|
`;
|
|
43035
43485
|
async function ensureEngine(root) {
|
|
43036
|
-
await
|
|
43037
|
-
await
|
|
43486
|
+
await mkdir11(join29(root, RAG_REL), { recursive: true }).catch(() => void 0);
|
|
43487
|
+
await writeFile12(join29(root, RAG_REL, "rag_engine.py"), RAG_ENGINE_PY, "utf8");
|
|
43038
43488
|
}
|
|
43039
43489
|
async function ragReindex(root) {
|
|
43040
43490
|
const py = await detectPython();
|
|
43041
43491
|
if (!py.found) return { ok: false, started: false, error: "python3 not found" };
|
|
43042
|
-
if (!existsSync14(
|
|
43043
|
-
if (existsSync14(
|
|
43492
|
+
if (!existsSync14(join29(root, RAG_REL, ".installed"))) return { ok: false, started: false, error: "RAG is not installed" };
|
|
43493
|
+
if (existsSync14(join29(root, RAG_REL, ".indexing"))) return { ok: true, started: true };
|
|
43044
43494
|
await ensureEngine(root);
|
|
43045
43495
|
const script = `
|
|
43046
43496
|
set +e
|
|
@@ -43060,12 +43510,12 @@ rm -f "${RAG_REL}/.indexing"
|
|
|
43060
43510
|
async function ragQuery(root, queryText, k2 = 8) {
|
|
43061
43511
|
const py = await detectPython();
|
|
43062
43512
|
if (!py.found) return { results: [], error: "python3 not found" };
|
|
43063
|
-
if (!existsSync14(
|
|
43513
|
+
if (!existsSync14(join29(root, RAG_REL, "index.db"))) return { results: [], error: "no index \u2014 reindex first" };
|
|
43064
43514
|
await ensureEngine(root);
|
|
43065
43515
|
try {
|
|
43066
43516
|
const { stdout } = await run10(
|
|
43067
43517
|
py.bin,
|
|
43068
|
-
[
|
|
43518
|
+
[join29(RAG_REL, "rag_engine.py"), "query", queryText, String(Math.min(Math.max(k2, 1), 20))],
|
|
43069
43519
|
{ cwd: root, timeout: 3e4, maxBuffer: 4 * 1024 * 1024 }
|
|
43070
43520
|
);
|
|
43071
43521
|
const parsed = JSON.parse(stdout.trim() || '{"results":[]}');
|
|
@@ -43080,9 +43530,9 @@ async function ragInstall(root, model) {
|
|
|
43080
43530
|
}
|
|
43081
43531
|
const py = await detectPython();
|
|
43082
43532
|
if (!py.found) return { ok: false, started: false, error: "python3 not found on the worker" };
|
|
43083
|
-
if (existsSync14(
|
|
43533
|
+
if (existsSync14(join29(root, RAG_REL, ".installing"))) return { ok: true, started: true };
|
|
43084
43534
|
try {
|
|
43085
|
-
await
|
|
43535
|
+
await mkdir11(join29(root, RAG_REL), { recursive: true });
|
|
43086
43536
|
} catch {
|
|
43087
43537
|
}
|
|
43088
43538
|
const script = `
|
|
@@ -43109,8 +43559,8 @@ rm -f "${RAG_REL}/.installing"
|
|
|
43109
43559
|
}
|
|
43110
43560
|
|
|
43111
43561
|
// src/core/config-sync.ts
|
|
43112
|
-
import { existsSync as existsSync15, readFileSync as
|
|
43113
|
-
import { join as
|
|
43562
|
+
import { existsSync as existsSync15, readFileSync as readFileSync16 } from "fs";
|
|
43563
|
+
import { join as join30 } from "path";
|
|
43114
43564
|
var SERVER_MANAGED_KEYS = [
|
|
43115
43565
|
"physicPath",
|
|
43116
43566
|
"sessionSwitchPct",
|
|
@@ -43127,10 +43577,10 @@ var SERVER_MANAGED_KEYS = [
|
|
|
43127
43577
|
"projectAiMemoryBudgetChars"
|
|
43128
43578
|
];
|
|
43129
43579
|
function readConfigText(profileDir2) {
|
|
43130
|
-
const path =
|
|
43580
|
+
const path = join30(profileDir2, "config.json");
|
|
43131
43581
|
if (existsSync15(path)) {
|
|
43132
43582
|
try {
|
|
43133
|
-
return
|
|
43583
|
+
return readFileSync16(path, "utf8");
|
|
43134
43584
|
} catch {
|
|
43135
43585
|
}
|
|
43136
43586
|
}
|
|
@@ -43161,7 +43611,7 @@ function handleWorkerChannels(ctx, message, payload) {
|
|
|
43161
43611
|
switch (message.channel) {
|
|
43162
43612
|
case WsChannels.AUTONOMOUS_READ:
|
|
43163
43613
|
if (ctx.physicRoot) {
|
|
43164
|
-
void readAutonomous(ctx.physicRoot).then(
|
|
43614
|
+
void readAutonomous(ctx.physicRoot, ctx.profileDir).then(
|
|
43165
43615
|
(reply) => ctx.send(WsChannels.AUTONOMOUS_READ, reply, message.id)
|
|
43166
43616
|
);
|
|
43167
43617
|
}
|
|
@@ -43178,7 +43628,7 @@ function handleWorkerChannels(ctx, message, payload) {
|
|
|
43178
43628
|
case WsChannels.AUTONOMOUS_WRITE: {
|
|
43179
43629
|
const req = payload;
|
|
43180
43630
|
if (ctx.physicRoot) {
|
|
43181
|
-
void writeAutonomous(ctx.physicRoot, req, req.by ?? "unknown").then(
|
|
43631
|
+
void writeAutonomous(ctx.physicRoot, ctx.profileDir, req, req.by ?? "unknown").then(
|
|
43182
43632
|
(reply) => ctx.send(WsChannels.AUTONOMOUS_WRITE, reply, message.id)
|
|
43183
43633
|
);
|
|
43184
43634
|
}
|
|
@@ -43367,6 +43817,7 @@ var WsClient = class {
|
|
|
43367
43817
|
);
|
|
43368
43818
|
this.hctx = this.buildHandlerCtx();
|
|
43369
43819
|
context.bus.onLocalSubmit((input) => void runLocalCommand(this.hctx, input));
|
|
43820
|
+
context.bus.onAutonomousRun(() => void runAutonomousCycle(this.hctx));
|
|
43370
43821
|
context.bus.onReconnect(() => this.reconnectNow());
|
|
43371
43822
|
context.bus.onTranscript((entry) => this.emitConsole({ kind: "add", entry: toDtoEntry(entry) }));
|
|
43372
43823
|
context.bus.onTranscriptUpdate(
|
|
@@ -43916,7 +44367,7 @@ var WsClient = class {
|
|
|
43916
44367
|
}
|
|
43917
44368
|
const { payload, nonce } = encryptPayload(this.sessionKey, data);
|
|
43918
44369
|
const envelope = {
|
|
43919
|
-
id:
|
|
44370
|
+
id: randomUUID3(),
|
|
43920
44371
|
replyTo,
|
|
43921
44372
|
channel,
|
|
43922
44373
|
wsToken: this.wsToken,
|
|
@@ -43937,7 +44388,7 @@ var WsClient = class {
|
|
|
43937
44388
|
}
|
|
43938
44389
|
const { payload, nonce } = encryptPayload(this.sessionKey, data);
|
|
43939
44390
|
const envelope = {
|
|
43940
|
-
id:
|
|
44391
|
+
id: randomUUID3(),
|
|
43941
44392
|
replyTo: null,
|
|
43942
44393
|
channel,
|
|
43943
44394
|
wsToken: this.wsToken,
|
|
@@ -44019,7 +44470,7 @@ var WsClient = class {
|
|
|
44019
44470
|
*/
|
|
44020
44471
|
physicFolderPath(projectName) {
|
|
44021
44472
|
const safe = projectName.replace(/[/\\]/g, "_").replace(/\.\./g, "_").trim();
|
|
44022
|
-
return safe ?
|
|
44473
|
+
return safe ? join31(this.context.profileDir, safe) : null;
|
|
44023
44474
|
}
|
|
44024
44475
|
/**
|
|
44025
44476
|
* Report machine.status: version + the state of the physic project being served
|
|
@@ -44036,7 +44487,7 @@ var WsClient = class {
|
|
|
44036
44487
|
physicPath,
|
|
44037
44488
|
physicPathExists: physicPath ? existsSync16(physicPath) : void 0,
|
|
44038
44489
|
// Real coarse state (ADR-0152): !paused and a recent tick (was hard-coded false).
|
|
44039
|
-
autonomousRunning: physicPath ? isAutonomousRunning(physicPath) : false,
|
|
44490
|
+
autonomousRunning: physicPath ? isAutonomousRunning(physicPath, this.context.profileDir) : false,
|
|
44040
44491
|
// The machine-user AI-run limit (0 = unlimited) + profile count so a dispatch can resolve the
|
|
44041
44492
|
// effective timeout and derive the SSE reply windows server-side (ADR-0256).
|
|
44042
44493
|
aiRunTimeoutSec: config.aiRunTimeoutSec ?? 0,
|
|
@@ -44128,7 +44579,7 @@ var WsClient = class {
|
|
|
44128
44579
|
*/
|
|
44129
44580
|
uploadLog() {
|
|
44130
44581
|
try {
|
|
44131
|
-
const upload = readLogUpload(
|
|
44582
|
+
const upload = readLogUpload(join31(this.context.profileDir, "logs"));
|
|
44132
44583
|
if (!upload) return;
|
|
44133
44584
|
const sig = `${upload.fileName}:${upload.totalBytes}`;
|
|
44134
44585
|
if (sig === this.lastLogSig) return;
|
|
@@ -44290,10 +44741,10 @@ var WsClient = class {
|
|
|
44290
44741
|
};
|
|
44291
44742
|
|
|
44292
44743
|
// src/core/instance-lock.ts
|
|
44293
|
-
import { readFileSync as
|
|
44294
|
-
import { join as
|
|
44744
|
+
import { readFileSync as readFileSync17, rmSync as rmSync7, writeFileSync as writeFileSync9 } from "fs";
|
|
44745
|
+
import { join as join32 } from "path";
|
|
44295
44746
|
function lockPath(profileDir2) {
|
|
44296
|
-
return
|
|
44747
|
+
return join32(profileDir2, "start.lock");
|
|
44297
44748
|
}
|
|
44298
44749
|
function isAlive(pid) {
|
|
44299
44750
|
try {
|
|
@@ -44305,7 +44756,7 @@ function isAlive(pid) {
|
|
|
44305
44756
|
}
|
|
44306
44757
|
function lockHolder(profileDir2) {
|
|
44307
44758
|
try {
|
|
44308
|
-
const pid = Number.parseInt(
|
|
44759
|
+
const pid = Number.parseInt(readFileSync17(lockPath(profileDir2), "utf8").trim(), 10);
|
|
44309
44760
|
if (Number.isInteger(pid) && pid > 0 && pid !== process.pid && isAlive(pid)) return pid;
|
|
44310
44761
|
} catch {
|
|
44311
44762
|
}
|
|
@@ -44318,7 +44769,7 @@ function acquireInstanceLock(profileDir2) {
|
|
|
44318
44769
|
}
|
|
44319
44770
|
function releaseInstanceLock(profileDir2) {
|
|
44320
44771
|
try {
|
|
44321
|
-
const pid = Number.parseInt(
|
|
44772
|
+
const pid = Number.parseInt(readFileSync17(lockPath(profileDir2), "utf8").trim(), 10);
|
|
44322
44773
|
if (pid === process.pid) rmSync7(lockPath(profileDir2), { force: true });
|
|
44323
44774
|
} catch {
|
|
44324
44775
|
}
|
|
@@ -44326,7 +44777,7 @@ function releaseInstanceLock(profileDir2) {
|
|
|
44326
44777
|
|
|
44327
44778
|
// src/core/session-bus.ts
|
|
44328
44779
|
import { EventEmitter } from "events";
|
|
44329
|
-
import { randomUUID as
|
|
44780
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
44330
44781
|
var REPLAY_CAP = 500;
|
|
44331
44782
|
var SessionBus = class {
|
|
44332
44783
|
emitter = new EventEmitter();
|
|
@@ -44357,7 +44808,7 @@ var SessionBus = class {
|
|
|
44357
44808
|
/** Producer → consumers: push one transcript entry. Returns its id (for `updateEntry`). */
|
|
44358
44809
|
push(input) {
|
|
44359
44810
|
const entry = {
|
|
44360
|
-
id:
|
|
44811
|
+
id: randomUUID4(),
|
|
44361
44812
|
level: "info",
|
|
44362
44813
|
...input,
|
|
44363
44814
|
ts: Date.now()
|
|
@@ -44588,15 +45039,38 @@ var SessionBus = class {
|
|
|
44588
45039
|
this.emitter.on("reconnect", fn);
|
|
44589
45040
|
return () => this.emitter.off("reconnect", fn);
|
|
44590
45041
|
}
|
|
45042
|
+
/**
|
|
45043
|
+
* Trigger → session: run ONE autonomous cycle (ADR-0319). Sent by `4pm auto-run` over the control
|
|
45044
|
+
* socket so the cron tick runs the cycle through THIS daemon's live WS session — reusing its
|
|
45045
|
+
* profile-failover, token metering (ADR-0072) and folder-scope (ADR-0181) — instead of a raw `claude -p`.
|
|
45046
|
+
*/
|
|
45047
|
+
submitAutonomous() {
|
|
45048
|
+
this.emitter.emit("autonomous-run");
|
|
45049
|
+
}
|
|
45050
|
+
/** Subscribe to autonomous-cycle triggers. */
|
|
45051
|
+
onAutonomousRun(fn) {
|
|
45052
|
+
this.emitter.on("autonomous-run", fn);
|
|
45053
|
+
return () => this.emitter.off("autonomous-run", fn);
|
|
45054
|
+
}
|
|
45055
|
+
/** Session → trigger: the autonomous cycle settled (ok + an optional note). */
|
|
45056
|
+
autonomousDone(ok, note) {
|
|
45057
|
+
this.emitter.emit("autonomous-done", ok, note);
|
|
45058
|
+
}
|
|
45059
|
+
/** Subscribe to autonomous-cycle completion. */
|
|
45060
|
+
onAutonomousDone(fn) {
|
|
45061
|
+
this.emitter.on("autonomous-done", fn);
|
|
45062
|
+
return () => this.emitter.off("autonomous-done", fn);
|
|
45063
|
+
}
|
|
44591
45064
|
};
|
|
44592
45065
|
|
|
44593
45066
|
// src/core/control-server.ts
|
|
44594
45067
|
import { createServer } from "net";
|
|
44595
|
-
import { rmSync as rmSync8 } from "fs";
|
|
44596
|
-
import { join as
|
|
45068
|
+
import { chmodSync as chmodSync4, rmSync as rmSync8 } from "fs";
|
|
45069
|
+
import { join as join34 } from "path";
|
|
44597
45070
|
|
|
44598
45071
|
// src/core/control-protocol.ts
|
|
44599
45072
|
var CONTROL_SOCKET_FILE = "control.sock";
|
|
45073
|
+
var CONTROL_TOKEN_FILE = "control.token";
|
|
44600
45074
|
function encodeFrame(frame) {
|
|
44601
45075
|
return `${JSON.stringify(frame)}
|
|
44602
45076
|
`;
|
|
@@ -44620,9 +45094,41 @@ function createFrameParser() {
|
|
|
44620
45094
|
};
|
|
44621
45095
|
}
|
|
44622
45096
|
|
|
45097
|
+
// src/core/control-token.ts
|
|
45098
|
+
import { chmodSync as chmodSync3, readFileSync as readFileSync18, writeFileSync as writeFileSync10 } from "fs";
|
|
45099
|
+
import { randomBytes as randomBytes3, timingSafeEqual } from "crypto";
|
|
45100
|
+
import { join as join33 } from "path";
|
|
45101
|
+
function controlTokenPath(profileDir2) {
|
|
45102
|
+
return join33(profileDir2, CONTROL_TOKEN_FILE);
|
|
45103
|
+
}
|
|
45104
|
+
function writeControlToken(profileDir2) {
|
|
45105
|
+
const token = randomBytes3(32).toString("hex");
|
|
45106
|
+
const path = controlTokenPath(profileDir2);
|
|
45107
|
+
writeFileSync10(path, token, { mode: 384 });
|
|
45108
|
+
try {
|
|
45109
|
+
chmodSync3(path, 384);
|
|
45110
|
+
} catch {
|
|
45111
|
+
}
|
|
45112
|
+
return token;
|
|
45113
|
+
}
|
|
45114
|
+
function readControlToken(profileDir2) {
|
|
45115
|
+
try {
|
|
45116
|
+
const v2 = readFileSync18(controlTokenPath(profileDir2), "utf8").trim();
|
|
45117
|
+
return v2.length > 0 ? v2 : null;
|
|
45118
|
+
} catch {
|
|
45119
|
+
return null;
|
|
45120
|
+
}
|
|
45121
|
+
}
|
|
45122
|
+
function tokenMatches(expected, given) {
|
|
45123
|
+
if (!given) return false;
|
|
45124
|
+
const a = Buffer.from(expected, "utf8");
|
|
45125
|
+
const b2 = Buffer.from(given, "utf8");
|
|
45126
|
+
return a.length === b2.length && timingSafeEqual(a, b2);
|
|
45127
|
+
}
|
|
45128
|
+
|
|
44623
45129
|
// src/core/control-server.ts
|
|
44624
45130
|
function startControlServer(bus, profileDir2, info) {
|
|
44625
|
-
const socketPath =
|
|
45131
|
+
const socketPath = join34(profileDir2, CONTROL_SOCKET_FILE);
|
|
44626
45132
|
try {
|
|
44627
45133
|
rmSync8(socketPath, { force: true });
|
|
44628
45134
|
} catch {
|
|
@@ -44650,29 +45156,44 @@ function startControlServer(bus, profileDir2, info) {
|
|
|
44650
45156
|
bus.onWorker((worker) => broadcast({ t: "worker", worker })),
|
|
44651
45157
|
bus.onProject((project) => broadcast({ t: "project", project })),
|
|
44652
45158
|
bus.onUsage((usage) => broadcast({ t: "usage", usage })),
|
|
44653
|
-
bus.onTokens((total) => broadcast({ t: "tokens", total }))
|
|
45159
|
+
bus.onTokens((total) => broadcast({ t: "tokens", total })),
|
|
45160
|
+
// Report an autonomous cycle's completion back to the `4pm auto-run` client (ADR-0319).
|
|
45161
|
+
bus.onAutonomousDone((ok, note) => broadcast({ t: "autonomousDone", ok, note }))
|
|
44654
45162
|
];
|
|
45163
|
+
const token = writeControlToken(profileDir2);
|
|
45164
|
+
const sendSnapshot = (sock) => send(sock, {
|
|
45165
|
+
t: "snapshot",
|
|
45166
|
+
info,
|
|
45167
|
+
status: bus.status,
|
|
45168
|
+
busy: bus.busy,
|
|
45169
|
+
activeProfile: bus.activeProfile,
|
|
45170
|
+
scope: bus.scope,
|
|
45171
|
+
worker: bus.worker,
|
|
45172
|
+
project: bus.project,
|
|
45173
|
+
usage: bus.usage,
|
|
45174
|
+
tokens: bus.sessionTokens,
|
|
45175
|
+
transcript: bus.snapshot()
|
|
45176
|
+
});
|
|
44655
45177
|
const server = createServer((sock) => {
|
|
44656
|
-
clients.add(sock);
|
|
44657
|
-
send(sock, {
|
|
44658
|
-
t: "snapshot",
|
|
44659
|
-
info,
|
|
44660
|
-
status: bus.status,
|
|
44661
|
-
busy: bus.busy,
|
|
44662
|
-
activeProfile: bus.activeProfile,
|
|
44663
|
-
scope: bus.scope,
|
|
44664
|
-
worker: bus.worker,
|
|
44665
|
-
project: bus.project,
|
|
44666
|
-
usage: bus.usage,
|
|
44667
|
-
tokens: bus.sessionTokens,
|
|
44668
|
-
transcript: bus.snapshot()
|
|
44669
|
-
});
|
|
44670
|
-
const parse = createFrameParser();
|
|
44671
45178
|
sock.setEncoding("utf8");
|
|
45179
|
+
let authed = false;
|
|
45180
|
+
const parse = createFrameParser();
|
|
44672
45181
|
sock.on("data", (chunk) => {
|
|
44673
45182
|
for (const frame of parse(chunk)) {
|
|
45183
|
+
if (!authed) {
|
|
45184
|
+
if (frame.t === "auth" && tokenMatches(token, frame.token)) {
|
|
45185
|
+
authed = true;
|
|
45186
|
+
clients.add(sock);
|
|
45187
|
+
sendSnapshot(sock);
|
|
45188
|
+
} else {
|
|
45189
|
+
send(sock, { t: "authError" });
|
|
45190
|
+
sock.destroy();
|
|
45191
|
+
}
|
|
45192
|
+
continue;
|
|
45193
|
+
}
|
|
44674
45194
|
if (frame.t === "submit") bus.submitLocal(frame.input);
|
|
44675
45195
|
else if (frame.t === "reconnect") bus.requestReconnect();
|
|
45196
|
+
else if (frame.t === "autonomousRun") bus.submitAutonomous();
|
|
44676
45197
|
}
|
|
44677
45198
|
});
|
|
44678
45199
|
const drop = () => {
|
|
@@ -44683,7 +45204,12 @@ function startControlServer(bus, profileDir2, info) {
|
|
|
44683
45204
|
});
|
|
44684
45205
|
server.on("error", () => {
|
|
44685
45206
|
});
|
|
44686
|
-
server.listen(socketPath)
|
|
45207
|
+
server.listen(socketPath, () => {
|
|
45208
|
+
try {
|
|
45209
|
+
chmodSync4(socketPath, 384);
|
|
45210
|
+
} catch {
|
|
45211
|
+
}
|
|
45212
|
+
});
|
|
44687
45213
|
return () => {
|
|
44688
45214
|
for (const un2 of unsubs) un2();
|
|
44689
45215
|
for (const sock of clients) sock.destroy();
|
|
@@ -44691,6 +45217,7 @@ function startControlServer(bus, profileDir2, info) {
|
|
|
44691
45217
|
server.close();
|
|
44692
45218
|
try {
|
|
44693
45219
|
rmSync8(socketPath, { force: true });
|
|
45220
|
+
rmSync8(controlTokenPath(profileDir2), { force: true });
|
|
44694
45221
|
} catch {
|
|
44695
45222
|
}
|
|
44696
45223
|
};
|
|
@@ -50353,17 +50880,17 @@ var import_react21 = __toESM(require_react(), 1);
|
|
|
50353
50880
|
var import_react26 = __toESM(require_react(), 1);
|
|
50354
50881
|
|
|
50355
50882
|
// src/core/input-history.ts
|
|
50356
|
-
import { existsSync as existsSync18, readFileSync as
|
|
50357
|
-
import { join as
|
|
50883
|
+
import { existsSync as existsSync18, readFileSync as readFileSync20, writeFileSync as writeFileSync11 } from "fs";
|
|
50884
|
+
import { join as join35 } from "path";
|
|
50358
50885
|
var MAX_HISTORY = 200;
|
|
50359
50886
|
function historyPath(profileDir2) {
|
|
50360
|
-
return
|
|
50887
|
+
return join35(profileDir2, "input-history.json");
|
|
50361
50888
|
}
|
|
50362
50889
|
function loadInputHistory(profileDir2) {
|
|
50363
50890
|
const path = historyPath(profileDir2);
|
|
50364
50891
|
if (!existsSync18(path)) return [];
|
|
50365
50892
|
try {
|
|
50366
|
-
const data = JSON.parse(
|
|
50893
|
+
const data = JSON.parse(readFileSync20(path, "utf8"));
|
|
50367
50894
|
return Array.isArray(data) ? data.filter((x) => typeof x === "string") : [];
|
|
50368
50895
|
} catch {
|
|
50369
50896
|
return [];
|
|
@@ -50374,7 +50901,7 @@ function appendInputHistory(profileDir2, line, current2) {
|
|
|
50374
50901
|
if (!trimmed || current2[current2.length - 1] === trimmed) return current2;
|
|
50375
50902
|
const next = [...current2, trimmed].slice(-MAX_HISTORY);
|
|
50376
50903
|
try {
|
|
50377
|
-
|
|
50904
|
+
writeFileSync11(historyPath(profileDir2), JSON.stringify(next), "utf8");
|
|
50378
50905
|
} catch {
|
|
50379
50906
|
}
|
|
50380
50907
|
return next;
|
|
@@ -50971,7 +51498,7 @@ async function runStart(profileDir2, profileName) {
|
|
|
50971
51498
|
return;
|
|
50972
51499
|
}
|
|
50973
51500
|
logger.configure({
|
|
50974
|
-
dir:
|
|
51501
|
+
dir: join36(profileDir2, "logs"),
|
|
50975
51502
|
level: resolveLogLevel(),
|
|
50976
51503
|
base: { version: CLI_VERSION, profile: profileName, scope: credential.scope }
|
|
50977
51504
|
});
|
|
@@ -51080,15 +51607,21 @@ async function runStart(profileDir2, profileName) {
|
|
|
51080
51607
|
}
|
|
51081
51608
|
|
|
51082
51609
|
// src/commands/attach.ts
|
|
51083
|
-
import { join as
|
|
51610
|
+
import { join as join37 } from "path";
|
|
51084
51611
|
|
|
51085
51612
|
// src/core/control-client.ts
|
|
51086
51613
|
import { connect as connect2 } from "net";
|
|
51087
|
-
function connectControl(socketPath, bus) {
|
|
51614
|
+
function connectControl(socketPath, bus, token) {
|
|
51088
51615
|
return new Promise((resolve10, reject) => {
|
|
51089
51616
|
const socket = connect2(socketPath);
|
|
51090
51617
|
socket.setEncoding("utf8");
|
|
51091
51618
|
let ready = false;
|
|
51619
|
+
socket.on("connect", () => {
|
|
51620
|
+
try {
|
|
51621
|
+
socket.write(encodeFrame({ t: "auth", token: token ?? "" }));
|
|
51622
|
+
} catch {
|
|
51623
|
+
}
|
|
51624
|
+
});
|
|
51092
51625
|
const forwardSubmit = bus.onLocalSubmit((input) => {
|
|
51093
51626
|
try {
|
|
51094
51627
|
socket.write(encodeFrame({ t: "submit", input }));
|
|
@@ -51167,6 +51700,9 @@ function connectControl(socketPath, bus) {
|
|
|
51167
51700
|
case "tokens":
|
|
51168
51701
|
bus.setTokens(frame.total);
|
|
51169
51702
|
break;
|
|
51703
|
+
case "authError":
|
|
51704
|
+
if (!ready) reject(new Error("control-channel auth failed"));
|
|
51705
|
+
break;
|
|
51170
51706
|
}
|
|
51171
51707
|
}
|
|
51172
51708
|
});
|
|
@@ -51180,11 +51716,11 @@ async function runAttach(profileDir2, profileName) {
|
|
|
51180
51716
|
process.exitCode = 1;
|
|
51181
51717
|
return;
|
|
51182
51718
|
}
|
|
51183
|
-
const socketPath =
|
|
51719
|
+
const socketPath = join37(profileDir2, CONTROL_SOCKET_FILE);
|
|
51184
51720
|
const bus = new SessionBus();
|
|
51185
51721
|
let conn;
|
|
51186
51722
|
try {
|
|
51187
|
-
conn = await connectControl(socketPath, bus);
|
|
51723
|
+
conn = await connectControl(socketPath, bus, readControlToken(profileDir2));
|
|
51188
51724
|
} catch {
|
|
51189
51725
|
console.error(t("attach.noDaemon", { profile: profileName }));
|
|
51190
51726
|
process.exitCode = 1;
|
|
@@ -51209,6 +51745,67 @@ async function runAttach(profileDir2, profileName) {
|
|
|
51209
51745
|
}
|
|
51210
51746
|
}
|
|
51211
51747
|
|
|
51748
|
+
// src/commands/auto-run.ts
|
|
51749
|
+
import { connect as connect3 } from "net";
|
|
51750
|
+
import { join as join38 } from "path";
|
|
51751
|
+
var AUTO_RUN_TIMEOUT_MS = 3 * 60 * 60 * 1e3;
|
|
51752
|
+
async function runAutoRun(profileDir2, profileName) {
|
|
51753
|
+
initI18n(readProfileConfig(profileDir2).locale);
|
|
51754
|
+
const socketPath = join38(profileDir2, CONTROL_SOCKET_FILE);
|
|
51755
|
+
const token = readControlToken(profileDir2);
|
|
51756
|
+
const code = await new Promise((resolve10) => {
|
|
51757
|
+
let settled = false;
|
|
51758
|
+
const done = (c) => {
|
|
51759
|
+
if (settled) return;
|
|
51760
|
+
settled = true;
|
|
51761
|
+
clearTimeout(timer);
|
|
51762
|
+
try {
|
|
51763
|
+
socket.destroy();
|
|
51764
|
+
} catch {
|
|
51765
|
+
}
|
|
51766
|
+
resolve10(c);
|
|
51767
|
+
};
|
|
51768
|
+
const socket = connect3(socketPath);
|
|
51769
|
+
socket.setEncoding("utf8");
|
|
51770
|
+
const timer = setTimeout(() => {
|
|
51771
|
+
console.error(t("autoRun.timeout"));
|
|
51772
|
+
done(1);
|
|
51773
|
+
}, AUTO_RUN_TIMEOUT_MS);
|
|
51774
|
+
timer.unref?.();
|
|
51775
|
+
socket.on("connect", () => {
|
|
51776
|
+
try {
|
|
51777
|
+
socket.write(encodeFrame({ t: "auth", token: token ?? "" }));
|
|
51778
|
+
socket.write(encodeFrame({ t: "autonomousRun" }));
|
|
51779
|
+
} catch {
|
|
51780
|
+
done(1);
|
|
51781
|
+
}
|
|
51782
|
+
});
|
|
51783
|
+
const parse = createFrameParser();
|
|
51784
|
+
socket.on("data", (chunk) => {
|
|
51785
|
+
for (const frame of parse(chunk)) {
|
|
51786
|
+
if (frame.t === "transcript") {
|
|
51787
|
+
if (frame.entry.text) console.log(frame.entry.text);
|
|
51788
|
+
} else if (frame.t === "update") {
|
|
51789
|
+
if (frame.text) console.log(frame.text);
|
|
51790
|
+
} else if (frame.t === "autonomousDone") {
|
|
51791
|
+
if (frame.ok) console.log(t("autoRun.done"));
|
|
51792
|
+
else console.error(t("autoRun.failed", { note: frame.note ?? "" }));
|
|
51793
|
+
done(frame.ok ? 0 : 1);
|
|
51794
|
+
} else if (frame.t === "authError") {
|
|
51795
|
+
console.error(t("autoRun.authFailed"));
|
|
51796
|
+
done(1);
|
|
51797
|
+
}
|
|
51798
|
+
}
|
|
51799
|
+
});
|
|
51800
|
+
socket.on("error", () => {
|
|
51801
|
+
console.error(t("autoRun.noDaemon", { profile: profileName }));
|
|
51802
|
+
done(1);
|
|
51803
|
+
});
|
|
51804
|
+
socket.on("close", () => done(1));
|
|
51805
|
+
});
|
|
51806
|
+
process.exitCode = code;
|
|
51807
|
+
}
|
|
51808
|
+
|
|
51212
51809
|
// src/commands/ai-login.ts
|
|
51213
51810
|
import { spawn as spawn11 } from "child_process";
|
|
51214
51811
|
import { mkdirSync as mkdirSync8 } from "fs";
|
|
@@ -51433,6 +52030,15 @@ async function main() {
|
|
|
51433
52030
|
await runAttach(profileDir(name), name);
|
|
51434
52031
|
break;
|
|
51435
52032
|
}
|
|
52033
|
+
case "auto-run": {
|
|
52034
|
+
const name = await pickLinkedProfile(explicitProfile, "auto-run");
|
|
52035
|
+
if (!name) {
|
|
52036
|
+
process.exitCode = 1;
|
|
52037
|
+
break;
|
|
52038
|
+
}
|
|
52039
|
+
await runAutoRun(profileDir(name), name);
|
|
52040
|
+
break;
|
|
52041
|
+
}
|
|
51436
52042
|
case "ai-login": {
|
|
51437
52043
|
const name = await pickLinkedProfile(explicitProfile, "ai-login");
|
|
51438
52044
|
if (!name) {
|
|
@@ -51473,6 +52079,7 @@ async function main() {
|
|
|
51473
52079
|
" 4pm link Pair with the server (pairing \u2192 confirmation code)",
|
|
51474
52080
|
" 4pm start Connect WS, receive commands from the server",
|
|
51475
52081
|
" 4pm attach Open the TUI against a running headless daemon (ADR-0192)",
|
|
52082
|
+
" 4pm auto-run Run one autonomous cycle via the running daemon (cron \u2014 ADR-0319)",
|
|
51476
52083
|
" 4pm ai-login Log into a profile's AI CLI (claude/codex) in place (ADR-0199)",
|
|
51477
52084
|
" 4pm unlink Delete a link (pick a profile if several)",
|
|
51478
52085
|
" 4pm version Show the installed version (+ latest from the server)",
|