@4pm/cli 1.5.0 → 1.5.2-b
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 +1181 -740
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -28316,6 +28316,11 @@ function defaultProfileConfig() {
|
|
|
28316
28316
|
// Terminate a single AI run after 5 min by default (ADR-0243) so a hung/looping AI CLI
|
|
28317
28317
|
// (e.g. a heavy spec review/compose) can't spin the dispatcher forever; 0 = no limit.
|
|
28318
28318
|
aiRunTimeoutSec: 300,
|
|
28319
|
+
// Web-Console blocked slash commands (ADR-0249): block the two that tamper with / kill the
|
|
28320
|
+
// worker by default — `/quit` (stops the cli — DoS) and `/config` (writes config.json). Every
|
|
28321
|
+
// other command (incl. /reconnect, /logs — non-destructive) is allowed; operators adjust this
|
|
28322
|
+
// list in the Worker config. Empty ⇒ allow all.
|
|
28323
|
+
webBlockedCommands: ["quit", "config"],
|
|
28319
28324
|
// Shared AI memory (ADR-0245) — off by default (opt-in; costs a compaction call per turn).
|
|
28320
28325
|
aiMemoryEnabled: false,
|
|
28321
28326
|
aiMemoryBudgetChars: 6e3
|
|
@@ -28332,6 +28337,11 @@ function resolveIdleAutoClearMinutes(config) {
|
|
|
28332
28337
|
const project = config.projectAutoClearIdleMinutes ?? 0;
|
|
28333
28338
|
return project > 0 ? project : config.autoClearIdleMinutes ?? 10;
|
|
28334
28339
|
}
|
|
28340
|
+
var DEFAULT_WEB_BLOCKED_COMMANDS = ["quit", "config"];
|
|
28341
|
+
function resolveWebBlockedCommands(config) {
|
|
28342
|
+
const list = Array.isArray(config.webBlockedCommands) ? config.webBlockedCommands : DEFAULT_WEB_BLOCKED_COMMANDS;
|
|
28343
|
+
return new Set(list.map((c) => String(c).trim().toLowerCase()).filter(Boolean));
|
|
28344
|
+
}
|
|
28335
28345
|
function ensureProfileConfig(dir2) {
|
|
28336
28346
|
if (existsSync(join(dir2, "config.json"))) return false;
|
|
28337
28347
|
writeProfileConfig(dir2, defaultProfileConfig());
|
|
@@ -28765,7 +28775,7 @@ async function checkAndUpdate(serverUrl, autoUpdate) {
|
|
|
28765
28775
|
// src/core/ws-client.ts
|
|
28766
28776
|
init_wrapper();
|
|
28767
28777
|
import { randomUUID } from "crypto";
|
|
28768
|
-
import { basename as basename4, join as
|
|
28778
|
+
import { basename as basename4, join as join25 } from "path";
|
|
28769
28779
|
|
|
28770
28780
|
// vendor/@4pm/utils/helpers.ts
|
|
28771
28781
|
function backoffJitterMs(attempt, baseMs = 500, maxMs = 1e4) {
|
|
@@ -28824,18 +28834,21 @@ function isValidRoleSet(roles) {
|
|
|
28824
28834
|
|
|
28825
28835
|
// vendor/@4pm/constants/worker-tools.ts
|
|
28826
28836
|
var WORKER_TOOL_CATALOG = [
|
|
28827
|
-
{ id: "git", label: "Git", category: "vcs", versionArg: "--version", installPackage: null, installable: false },
|
|
28828
|
-
{ id: "node", label: "Node.js", category: "runtime", versionArg: "--version", installPackage: null, installable: false },
|
|
28829
|
-
{ id: "npm", label: "npm", category: "runtime", versionArg: "--version", installPackage: null, installable: false },
|
|
28830
|
-
{ id: "pnpm", label: "pnpm", category: "runtime", versionArg: "--version", installPackage: "pnpm", installable: false },
|
|
28831
|
-
{ id: "claude", label: "Claude Code", category: "ai-cli", versionArg: "--version", installPackage: "@anthropic-ai/claude-code", installable: false },
|
|
28832
|
-
{ id: "codex", label: "Codex", category: "ai-cli", versionArg: "--version", installPackage: "@openai/codex", installable: false },
|
|
28833
|
-
{ id: "gh", label: "GitHub CLI", category: "vcs", versionArg: "--version", installPackage: null, installable: false },
|
|
28834
|
-
{ id: "glab", label: "GitLab CLI", category: "vcs", versionArg: "--version", installPackage: null, installable: false }
|
|
28837
|
+
{ id: "git", label: "Git", category: "vcs", versionArg: "--version", installPackage: null, installable: false, updatable: false },
|
|
28838
|
+
{ id: "node", label: "Node.js", category: "runtime", versionArg: "--version", installPackage: null, installable: false, updatable: false },
|
|
28839
|
+
{ id: "npm", label: "npm", category: "runtime", versionArg: "--version", installPackage: null, installable: false, updatable: false },
|
|
28840
|
+
{ id: "pnpm", label: "pnpm", category: "runtime", versionArg: "--version", installPackage: "pnpm", installable: false, updatable: true },
|
|
28841
|
+
{ id: "claude", label: "Claude Code", category: "ai-cli", versionArg: "--version", installPackage: "@anthropic-ai/claude-code", installable: false, updatable: true },
|
|
28842
|
+
{ id: "codex", label: "Codex", category: "ai-cli", versionArg: "--version", installPackage: "@openai/codex", installable: false, updatable: true },
|
|
28843
|
+
{ id: "gh", label: "GitHub CLI", category: "vcs", versionArg: "--version", installPackage: null, installable: false, updatable: false },
|
|
28844
|
+
{ id: "glab", label: "GitLab CLI", category: "vcs", versionArg: "--version", installPackage: null, installable: false, updatable: false }
|
|
28835
28845
|
];
|
|
28836
28846
|
var WORKER_TOOL_PREREQUISITE_IDS = WORKER_TOOL_CATALOG.filter(
|
|
28837
28847
|
(t) => !t.installable
|
|
28838
28848
|
).map((t) => t.id);
|
|
28849
|
+
var WORKER_TOOL_UPDATABLE_IDS = WORKER_TOOL_CATALOG.filter(
|
|
28850
|
+
(t) => t.updatable
|
|
28851
|
+
).map((t) => t.id);
|
|
28839
28852
|
var WORKER_TOOL_BUNDLED_GLOBALS = ["npm", "corepack"];
|
|
28840
28853
|
|
|
28841
28854
|
// vendor/@4pm/constants/ws.ts
|
|
@@ -32932,7 +32945,15 @@ var dispatchCommandRequestSchema = external_exports.object({
|
|
|
32932
32945
|
* `project.git_write` (`write`) instead of `command.execute`, and validates `command`
|
|
32933
32946
|
* against the git allowlist (`isGitCommandAllowed`). Absent ⇒ normal console dispatch.
|
|
32934
32947
|
*/
|
|
32935
|
-
gitOp: external_exports.enum(["read", "write"]).optional()
|
|
32948
|
+
gitOp: external_exports.enum(["read", "write"]).optional(),
|
|
32949
|
+
/**
|
|
32950
|
+
* One-shot AI mode (ADR-0249): the prompt is a text-in → text-out task (spec review /
|
|
32951
|
+
* compose / suggest / generators) that must NOT trigger the AI CLI's agentic tool loop.
|
|
32952
|
+
* The cli caps such a run (`--max-turns 1` + disallowed agentic tools) so it can't wander
|
|
32953
|
+
* the repo / edit files / loop forever. Only meaningful with `ai:true`; absent ⇒ a full
|
|
32954
|
+
* agent run (the Console tab + Git merge, which legitimately use tools).
|
|
32955
|
+
*/
|
|
32956
|
+
aiOneShot: external_exports.boolean().optional()
|
|
32936
32957
|
}).superRefine((v, ctx) => {
|
|
32937
32958
|
if (v.machineLinkId == null === (v.pick == null)) {
|
|
32938
32959
|
ctx.addIssue({
|
|
@@ -32955,10 +32976,30 @@ var dispatchCommandRequestSchema = external_exports.object({
|
|
|
32955
32976
|
var listCommandsQuerySchema = baseRequestSchema.extend({
|
|
32956
32977
|
projectId: external_exports.string().uuid(),
|
|
32957
32978
|
// Optional search filters (command-0005): `search` (from BaseRequest) matches the command
|
|
32958
|
-
// text/status; `from`/`to` are an inclusive `YYYY-MM-DD` date range over `startedAt
|
|
32979
|
+
// text/status; `from`/`to` are an inclusive `YYYY-MM-DD` date range over `startedAt`;
|
|
32980
|
+
// `machineLinkId` (ADR-0249) filters to one machine-user's commands (absent ⇒ all).
|
|
32959
32981
|
from: external_exports.string().optional(),
|
|
32960
|
-
to: external_exports.string().optional()
|
|
32982
|
+
to: external_exports.string().optional(),
|
|
32983
|
+
machineLinkId: external_exports.string().uuid().optional()
|
|
32961
32984
|
});
|
|
32985
|
+
var CLI_SLASH_COMMANDS = [
|
|
32986
|
+
{ name: "help", usage: "/help", description: "List slash commands" },
|
|
32987
|
+
{ name: "version", usage: "/version", description: "Show the cli version" },
|
|
32988
|
+
{ name: "status", usage: "/status", description: "Show connection + profile + session status" },
|
|
32989
|
+
{ name: "history", usage: "/history [N]", description: "List the last N executed commands (+ time)" },
|
|
32990
|
+
{ name: "output", usage: "/output <n>", description: "Replay a command's output (n from /history)" },
|
|
32991
|
+
{ name: "logs", usage: "/logs [N]", description: "Tail the profile's structured log" },
|
|
32992
|
+
{ name: "expand", usage: "/expand [N]", description: "Expand a collapsed \u25B8[N] block" },
|
|
32993
|
+
{ name: "collapse", usage: "/collapse [N]", description: "Collapse an expanded \u25B8[N] block" },
|
|
32994
|
+
{ name: "reconnect", usage: "/reconnect", description: "Reconnect to the server now (skip the backoff)" },
|
|
32995
|
+
{ name: "whoami", usage: "/whoami", description: "Show this machine account + its teams & projects" },
|
|
32996
|
+
{ name: "ai-profile", usage: "/ai-profile [list|use <n>|reset]", description: "List AI profiles / switch which one runs" },
|
|
32997
|
+
{ name: "config", usage: "/config [show|init|set <k> <v>|delete <k>]", description: "View / init / update / delete profile config" },
|
|
32998
|
+
{ name: "claude-cmd", usage: "/claude-cmd /context", description: "Run an AI-CLI slash command (e.g. /context, /usage)" },
|
|
32999
|
+
{ name: "clear", usage: "/clear", description: "Clear the transcript" },
|
|
33000
|
+
{ name: "quit", usage: "/quit", description: "Quit the cli (also /exit)" }
|
|
33001
|
+
];
|
|
33002
|
+
var CLI_SLASH_COMMAND_NAMES = CLI_SLASH_COMMANDS.map((c) => c.name);
|
|
32962
33003
|
|
|
32963
33004
|
// vendor/@4pm/validation/schemas.ts
|
|
32964
33005
|
var passwordSchema = external_exports.string().min(8, "password: at least 8 characters").max(32, "password: at most 32 characters").regex(/[a-zA-Z]/, "password: needs at least 1 letter").regex(/[0-9]/, "password: needs at least 1 digit");
|
|
@@ -33001,7 +33042,10 @@ var listWorkersQuerySchema = baseRequestSchema.extend({
|
|
|
33001
33042
|
/** Only workers whose live CPU/memory/disk usage % ≥ the given threshold (0–100). */
|
|
33002
33043
|
minCpuPct: external_exports.coerce.number().min(0).max(100).optional(),
|
|
33003
33044
|
minMemPct: external_exports.coerce.number().min(0).max(100).optional(),
|
|
33004
|
-
minDiskPct: external_exports.coerce.number().min(0).max(100).optional()
|
|
33045
|
+
minDiskPct: external_exports.coerce.number().min(0).max(100).optional(),
|
|
33046
|
+
/** Hide workers with no cli attached (clis = 0). The web defaults this on; unchecking the
|
|
33047
|
+
* "show empty CLIs" box drops it so empty workers are listed too. */
|
|
33048
|
+
hideEmptyClis: external_exports.enum(["true", "false"]).transform((v) => v === "true").optional()
|
|
33005
33049
|
});
|
|
33006
33050
|
var updateWorkerRequestSchema = external_exports.object({
|
|
33007
33051
|
name: external_exports.string().min(1).max(100).optional()
|
|
@@ -33030,6 +33074,12 @@ var workerToolInstallSchema = external_exports.object({
|
|
|
33030
33074
|
/** Package manager to run the global install with. */
|
|
33031
33075
|
manager: external_exports.enum(["npm", "pnpm"])
|
|
33032
33076
|
});
|
|
33077
|
+
var workerToolAutoUpdateSchema = external_exports.object({
|
|
33078
|
+
/** Catalog id or installed extra's npm package name. */
|
|
33079
|
+
name: external_exports.string().trim().min(1).max(214),
|
|
33080
|
+
/** true = flag this tool for auto-update to @latest; false = clear it. */
|
|
33081
|
+
enabled: external_exports.boolean()
|
|
33082
|
+
});
|
|
33033
33083
|
var configWriteRequestSchema = external_exports.object({
|
|
33034
33084
|
config: external_exports.string().max(256 * 1024)
|
|
33035
33085
|
});
|
|
@@ -33634,8 +33684,14 @@ var WsChannels = {
|
|
|
33634
33684
|
TOOLS_LIST: "tools.list",
|
|
33635
33685
|
TOOLS_INSTALL: "tools.install",
|
|
33636
33686
|
TOOLS_UNINSTALL: "tools.uninstall",
|
|
33687
|
+
// update-to-latest of an npm-distributed catalog tool or an extra (ADR-0252): same streamed
|
|
33688
|
+
// shape as install/uninstall — the cli reinstalls the package at `@latest`.
|
|
33689
|
+
TOOLS_UPDATE: "tools.update",
|
|
33637
33690
|
TOOLS_PROGRESS: "tools.progress",
|
|
33638
33691
|
TOOLS_DONE: "tools.done",
|
|
33692
|
+
// toggle per-tool auto-update (ADR-0253): request/reply — the cli persists the flag in
|
|
33693
|
+
// config.json (`autoUpdateTools`); the ADR-0074 idle daily tick later runs the flagged tools.
|
|
33694
|
+
TOOLS_AUTOUPDATE: "tools.autoUpdate",
|
|
33639
33695
|
// autonomous mode control (ADR-0152): read settings+status+books+approvals, tail logs, and
|
|
33640
33696
|
// write {settings|approvals|userTodo|cron} — the dashboard drives the worker's autonomous engine.
|
|
33641
33697
|
AUTONOMOUS_READ: "autonomous.read",
|
|
@@ -33792,11 +33848,11 @@ function decryptPayload(key, payloadBase64, nonceBase64) {
|
|
|
33792
33848
|
}
|
|
33793
33849
|
|
|
33794
33850
|
// src/core/ws-client.ts
|
|
33795
|
-
import { existsSync as
|
|
33851
|
+
import { existsSync as existsSync13, mkdirSync as mkdirSync5, rmSync as rmSync4 } from "fs";
|
|
33796
33852
|
import { randomUUID as randomCommandId } from "crypto";
|
|
33797
33853
|
|
|
33798
33854
|
// src/core/update-scheduler.ts
|
|
33799
|
-
import { spawn } from "child_process";
|
|
33855
|
+
import { spawn as spawn2 } from "child_process";
|
|
33800
33856
|
|
|
33801
33857
|
// src/common/logger/logger.ts
|
|
33802
33858
|
import { appendFile, mkdir, readdir, unlink } from "fs/promises";
|
|
@@ -33901,6 +33957,153 @@ ${content.slice(-LOG_UPLOAD_CONTENT_MAX)}`;
|
|
|
33901
33957
|
}
|
|
33902
33958
|
}
|
|
33903
33959
|
|
|
33960
|
+
// src/core/worker-tools.ts
|
|
33961
|
+
import { spawn } from "child_process";
|
|
33962
|
+
var NPM_NAME = /^(?:@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/;
|
|
33963
|
+
function run(cmd, args, onLine, timeoutMs = 18e4) {
|
|
33964
|
+
return new Promise((resolve8) => {
|
|
33965
|
+
let out = "";
|
|
33966
|
+
let buf = "";
|
|
33967
|
+
let child = null;
|
|
33968
|
+
const done = (code) => {
|
|
33969
|
+
clearTimeout(timer);
|
|
33970
|
+
if (onLine && buf.trim()) onLine(buf.trim());
|
|
33971
|
+
resolve8({ code, out });
|
|
33972
|
+
};
|
|
33973
|
+
const timer = setTimeout(() => {
|
|
33974
|
+
child?.kill();
|
|
33975
|
+
if (onLine) onLine(`timed out after ${Math.round(timeoutMs / 1e3)}s`);
|
|
33976
|
+
done(124);
|
|
33977
|
+
}, timeoutMs);
|
|
33978
|
+
timer.unref?.();
|
|
33979
|
+
const feed = (d) => {
|
|
33980
|
+
const text = d.toString("utf8");
|
|
33981
|
+
out += text;
|
|
33982
|
+
if (!onLine) return;
|
|
33983
|
+
buf += text;
|
|
33984
|
+
let nl;
|
|
33985
|
+
while ((nl = buf.indexOf("\n")) >= 0) {
|
|
33986
|
+
const line = buf.slice(0, nl).replace(/\r$/, "");
|
|
33987
|
+
buf = buf.slice(nl + 1);
|
|
33988
|
+
onLine(line);
|
|
33989
|
+
}
|
|
33990
|
+
};
|
|
33991
|
+
try {
|
|
33992
|
+
child = spawn(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
33993
|
+
child.stdout?.on("data", feed);
|
|
33994
|
+
child.stderr?.on("data", feed);
|
|
33995
|
+
child.on("exit", (code) => done(code ?? 0));
|
|
33996
|
+
child.on("error", () => done(127));
|
|
33997
|
+
} catch {
|
|
33998
|
+
done(127);
|
|
33999
|
+
}
|
|
34000
|
+
});
|
|
34001
|
+
}
|
|
34002
|
+
async function detectOne(cmd, versionArg) {
|
|
34003
|
+
const { code, out } = await run(cmd, [versionArg], void 0, 15e3);
|
|
34004
|
+
if (code !== 0) return null;
|
|
34005
|
+
const first = out.split("\n").map((l) => l.trim()).find(Boolean);
|
|
34006
|
+
return first ?? "";
|
|
34007
|
+
}
|
|
34008
|
+
async function listExtras(autoUpdateSet = /* @__PURE__ */ new Set()) {
|
|
34009
|
+
const { out } = await run("npm", ["ls", "-g", "--depth=0", "--json"], void 0, 2e4);
|
|
34010
|
+
if (!out.trim()) return [];
|
|
34011
|
+
let deps = {};
|
|
34012
|
+
try {
|
|
34013
|
+
deps = JSON.parse(out).dependencies ?? {};
|
|
34014
|
+
} catch {
|
|
34015
|
+
return [];
|
|
34016
|
+
}
|
|
34017
|
+
const catalogPkgs = new Set(WORKER_TOOL_BUNDLED_GLOBALS);
|
|
34018
|
+
for (const t of WORKER_TOOL_CATALOG) {
|
|
34019
|
+
catalogPkgs.add(t.id);
|
|
34020
|
+
if (t.installPackage) catalogPkgs.add(t.installPackage);
|
|
34021
|
+
}
|
|
34022
|
+
return Object.entries(deps).filter(([name]) => !catalogPkgs.has(name)).map(([name, info]) => ({
|
|
34023
|
+
id: name,
|
|
34024
|
+
label: name,
|
|
34025
|
+
category: "runtime",
|
|
34026
|
+
installed: true,
|
|
34027
|
+
version: info.version ?? null,
|
|
34028
|
+
installable: true,
|
|
34029
|
+
updatable: true,
|
|
34030
|
+
autoUpdate: autoUpdateSet.has(name)
|
|
34031
|
+
}));
|
|
34032
|
+
}
|
|
34033
|
+
async function detectWorkerTools(autoUpdateTools = []) {
|
|
34034
|
+
const autoUpdateSet = new Set(autoUpdateTools);
|
|
34035
|
+
const catalog = await Promise.all(
|
|
34036
|
+
WORKER_TOOL_CATALOG.map(async (t) => {
|
|
34037
|
+
const version = await detectOne(t.id, t.versionArg);
|
|
34038
|
+
return {
|
|
34039
|
+
id: t.id,
|
|
34040
|
+
label: t.label,
|
|
34041
|
+
category: t.category,
|
|
34042
|
+
installed: version !== null,
|
|
34043
|
+
version,
|
|
34044
|
+
installable: t.installable,
|
|
34045
|
+
updatable: t.updatable,
|
|
34046
|
+
autoUpdate: autoUpdateSet.has(t.id)
|
|
34047
|
+
};
|
|
34048
|
+
})
|
|
34049
|
+
);
|
|
34050
|
+
const extras = await listExtras(autoUpdateSet).catch(() => []);
|
|
34051
|
+
return { catalog, extras };
|
|
34052
|
+
}
|
|
34053
|
+
function opPast(op) {
|
|
34054
|
+
return op === "install" ? "installed" : op === "uninstall" ? "uninstalled" : "updated";
|
|
34055
|
+
}
|
|
34056
|
+
function resolvePackage(name, op) {
|
|
34057
|
+
const entry = WORKER_TOOL_CATALOG.find((t) => t.id === name);
|
|
34058
|
+
if (entry) {
|
|
34059
|
+
const allowed = op === "update" ? entry.updatable : entry.installable;
|
|
34060
|
+
if (!allowed) {
|
|
34061
|
+
return { error: `"${name}" is a prerequisite and cannot be ${opPast(op)} from here.` };
|
|
34062
|
+
}
|
|
34063
|
+
return { pkg: entry.installPackage ?? name };
|
|
34064
|
+
}
|
|
34065
|
+
if (WORKER_TOOL_PREREQUISITE_IDS.includes(name)) {
|
|
34066
|
+
return { error: `"${name}" is a prerequisite and cannot be ${opPast(op)} from here.` };
|
|
34067
|
+
}
|
|
34068
|
+
if (WORKER_TOOL_BUNDLED_GLOBALS.includes(name)) {
|
|
34069
|
+
return { error: `"${name}" ships with Node and cannot be ${opPast(op)} from here.` };
|
|
34070
|
+
}
|
|
34071
|
+
if (!NPM_NAME.test(name)) return { error: `Invalid package name: "${name}".` };
|
|
34072
|
+
return { pkg: name };
|
|
34073
|
+
}
|
|
34074
|
+
function opArgs(op, manager, pkg) {
|
|
34075
|
+
if (op === "uninstall") return manager === "pnpm" ? ["remove", "-g", pkg] : ["uninstall", "-g", pkg];
|
|
34076
|
+
const target = op === "update" ? `${pkg}@latest` : pkg;
|
|
34077
|
+
return manager === "pnpm" ? ["add", "-g", target] : ["install", "-g", target];
|
|
34078
|
+
}
|
|
34079
|
+
async function runWorkerToolOp(op, name, manager, onLine) {
|
|
34080
|
+
const resolved = resolvePackage(name, op);
|
|
34081
|
+
if (resolved.error || !resolved.pkg) {
|
|
34082
|
+
return { ok: false, exitCode: 1, error: resolved.error ?? "Invalid package." };
|
|
34083
|
+
}
|
|
34084
|
+
onLine(`$ ${manager} ${opArgs(op, manager, resolved.pkg).join(" ")}`);
|
|
34085
|
+
const { code } = await run(manager, opArgs(op, manager, resolved.pkg), onLine);
|
|
34086
|
+
return { ok: code === 0, exitCode: code, error: code === 0 ? void 0 : `${manager} exited ${code}` };
|
|
34087
|
+
}
|
|
34088
|
+
function setToolAutoUpdate(profileDir2, name, enabled) {
|
|
34089
|
+
const resolved = resolvePackage(name, "update");
|
|
34090
|
+
if (resolved.error) return { ok: false, error: resolved.error };
|
|
34091
|
+
const current = readProfileConfig(profileDir2).autoUpdateTools ?? [];
|
|
34092
|
+
const set = new Set(current);
|
|
34093
|
+
if (enabled) set.add(name);
|
|
34094
|
+
else set.delete(name);
|
|
34095
|
+
writeProfileConfig(profileDir2, { autoUpdateTools: [...set] });
|
|
34096
|
+
return { ok: true };
|
|
34097
|
+
}
|
|
34098
|
+
async function autoUpdateFlaggedTools(tools, onLine) {
|
|
34099
|
+
for (const name of tools) {
|
|
34100
|
+
const res = await runWorkerToolOp("update", name, "npm", onLine).catch(
|
|
34101
|
+
(err) => ({ ok: false, exitCode: 1, error: String(err) })
|
|
34102
|
+
);
|
|
34103
|
+
onLine(res.ok ? `\u2713 ${name} updated` : `\u2717 ${name}: ${res.error ?? "failed"}`);
|
|
34104
|
+
}
|
|
34105
|
+
}
|
|
34106
|
+
|
|
33904
34107
|
// src/core/update-scheduler.ts
|
|
33905
34108
|
var TICK_MS = 6e4;
|
|
33906
34109
|
var UpdateScheduler = class {
|
|
@@ -33983,6 +34186,7 @@ var UpdateScheduler = class {
|
|
|
33983
34186
|
async runUpdate() {
|
|
33984
34187
|
this.updating = true;
|
|
33985
34188
|
try {
|
|
34189
|
+
await this.updateFlaggedTools();
|
|
33986
34190
|
this.bus.log("Scheduled auto-update: checking for a new version\u2026");
|
|
33987
34191
|
const result = await updateToLatest(this.serverUrl);
|
|
33988
34192
|
if (result.action === "already-latest") {
|
|
@@ -33996,7 +34200,7 @@ var UpdateScheduler = class {
|
|
|
33996
34200
|
}
|
|
33997
34201
|
logger.info("update.scheduled.updated", { version: result.version });
|
|
33998
34202
|
this.bus.log(`Updated to ${result.version} \u2014 restarting to apply\u2026`);
|
|
33999
|
-
const child =
|
|
34203
|
+
const child = spawn2(process.execPath, process.argv.slice(1), {
|
|
34000
34204
|
stdio: "inherit",
|
|
34001
34205
|
env: { ...process.env, FOURPM_NO_UPDATE: "1" }
|
|
34002
34206
|
});
|
|
@@ -34007,18 +34211,45 @@ var UpdateScheduler = class {
|
|
|
34007
34211
|
this.updating = false;
|
|
34008
34212
|
}
|
|
34009
34213
|
}
|
|
34214
|
+
/** Update each tool flagged for auto-update in config.json to @latest (ADR-0253) — best-effort. */
|
|
34215
|
+
async updateFlaggedTools() {
|
|
34216
|
+
const tools = readProfileConfig(this.profileDir).autoUpdateTools ?? [];
|
|
34217
|
+
if (tools.length === 0) return;
|
|
34218
|
+
this.bus.log(`Scheduled tool auto-update: ${tools.join(", ")}\u2026`);
|
|
34219
|
+
await autoUpdateFlaggedTools(tools, (line) => logger.info("update.tool.line", { line })).catch(
|
|
34220
|
+
(err) => logger.warn("update.tool.error", { error: String(err) })
|
|
34221
|
+
);
|
|
34222
|
+
}
|
|
34010
34223
|
};
|
|
34011
34224
|
|
|
34012
34225
|
// src/core/executor.ts
|
|
34013
|
-
import { spawn as
|
|
34226
|
+
import { spawn as spawn3 } from "child_process";
|
|
34014
34227
|
var KILL_GRACE_MS = 5e3;
|
|
34015
34228
|
async function runCommand(dispatch, emit, opts) {
|
|
34016
34229
|
let seq = 0;
|
|
34017
|
-
|
|
34018
|
-
|
|
34019
|
-
|
|
34020
|
-
|
|
34230
|
+
let child;
|
|
34231
|
+
try {
|
|
34232
|
+
child = spawn3(dispatch.cmd, dispatch.args ?? [], {
|
|
34233
|
+
cwd: dispatch.path,
|
|
34234
|
+
env: { ...process.env, ...dispatch.env },
|
|
34235
|
+
shell: false
|
|
34236
|
+
});
|
|
34237
|
+
} catch (err) {
|
|
34238
|
+
emit({
|
|
34239
|
+
commandId: dispatch.commandId,
|
|
34240
|
+
seq: seq++,
|
|
34241
|
+
chunk: `spawn error: ${err.message}
|
|
34242
|
+
`,
|
|
34243
|
+
done: true,
|
|
34244
|
+
exitCode: -1
|
|
34245
|
+
});
|
|
34246
|
+
return;
|
|
34247
|
+
}
|
|
34248
|
+
child.stdin?.on("error", () => {
|
|
34021
34249
|
});
|
|
34250
|
+
if (opts?.stdin != null) {
|
|
34251
|
+
child.stdin?.write(opts.stdin);
|
|
34252
|
+
}
|
|
34022
34253
|
child.stdin?.end();
|
|
34023
34254
|
const timeoutMs = opts?.timeoutMs && opts.timeoutMs > 0 ? opts.timeoutMs : 0;
|
|
34024
34255
|
let timedOut = false;
|
|
@@ -34117,15 +34348,24 @@ function createAiStreamParser(cli) {
|
|
|
34117
34348
|
let buffer = "";
|
|
34118
34349
|
const acc = { tokens: 0, input: 0, output: 0, cacheRead: 0, cacheCreation: 0 };
|
|
34119
34350
|
let sessionId = "";
|
|
34351
|
+
let apiError = { isError: false, status: null };
|
|
34120
34352
|
function handleClaude(ev) {
|
|
34121
34353
|
if (typeof ev.session_id === "string" && ev.session_id) sessionId = ev.session_id;
|
|
34122
|
-
if (ev.type === "result"
|
|
34123
|
-
|
|
34124
|
-
|
|
34125
|
-
|
|
34126
|
-
|
|
34127
|
-
|
|
34128
|
-
|
|
34354
|
+
if (ev.type === "result") {
|
|
34355
|
+
if (ev.is_error === true) {
|
|
34356
|
+
apiError = {
|
|
34357
|
+
isError: true,
|
|
34358
|
+
status: typeof ev.api_error_status === "number" ? ev.api_error_status : null
|
|
34359
|
+
};
|
|
34360
|
+
}
|
|
34361
|
+
if (ev.usage) {
|
|
34362
|
+
acc.input = ev.usage.input_tokens ?? 0;
|
|
34363
|
+
acc.output = ev.usage.output_tokens ?? 0;
|
|
34364
|
+
acc.cacheRead = ev.usage.cache_read_input_tokens ?? 0;
|
|
34365
|
+
acc.cacheCreation = ev.usage.cache_creation_input_tokens ?? 0;
|
|
34366
|
+
acc.tokens = acc.input + acc.output;
|
|
34367
|
+
if (typeof ev.total_cost_usd === "number") acc.costUsd = ev.total_cost_usd;
|
|
34368
|
+
}
|
|
34129
34369
|
return "";
|
|
34130
34370
|
}
|
|
34131
34371
|
if (ev.type === "assistant") {
|
|
@@ -34185,7 +34425,8 @@ function createAiStreamParser(cli) {
|
|
|
34185
34425
|
return handled === null ? line : handled;
|
|
34186
34426
|
},
|
|
34187
34427
|
usage: () => ({ ...acc }),
|
|
34188
|
-
sessionId: () => sessionId
|
|
34428
|
+
sessionId: () => sessionId,
|
|
34429
|
+
apiError: () => ({ ...apiError })
|
|
34189
34430
|
};
|
|
34190
34431
|
}
|
|
34191
34432
|
function estimateTokens(text) {
|
|
@@ -34203,12 +34444,21 @@ function isSessionLimit(text) {
|
|
|
34203
34444
|
text
|
|
34204
34445
|
);
|
|
34205
34446
|
}
|
|
34206
|
-
function
|
|
34447
|
+
function isOutOfCredits(text) {
|
|
34448
|
+
return /out of (?:usage )?credits|credits[_ ]required|out[_ ]of[_ ]credits|insufficient credits/i.test(
|
|
34449
|
+
text
|
|
34450
|
+
);
|
|
34451
|
+
}
|
|
34452
|
+
function runAttempt(commandId, cmd, args, stdin, cwd2, env3, onChunk, timeoutMs) {
|
|
34207
34453
|
return new Promise((resolve8) => {
|
|
34208
|
-
|
|
34209
|
-
|
|
34210
|
-
|
|
34211
|
-
|
|
34454
|
+
runCommand(
|
|
34455
|
+
{ commandId, cmd, args, path: cwd2, env: env3 },
|
|
34456
|
+
(out) => {
|
|
34457
|
+
if (out.chunk) onChunk(out.chunk);
|
|
34458
|
+
if (out.done) resolve8(out.exitCode ?? -1);
|
|
34459
|
+
},
|
|
34460
|
+
{ stdin, ...timeoutMs > 0 ? { timeoutMs } : {} }
|
|
34461
|
+
).catch(() => resolve8(-1));
|
|
34212
34462
|
});
|
|
34213
34463
|
}
|
|
34214
34464
|
async function runAiFailover(plan, commandId, cwd2, handlers, timeoutMs = 0) {
|
|
@@ -34229,14 +34479,15 @@ async function runAiFailover(plan, commandId, cwd2, handlers, timeoutMs = 0) {
|
|
|
34229
34479
|
handlers.onChunk(text);
|
|
34230
34480
|
}
|
|
34231
34481
|
};
|
|
34232
|
-
finalExit = await runAttempt(commandId, attempt.cmd, attempt.args, cwd2, attempt.env, emit, timeoutMs);
|
|
34482
|
+
finalExit = await runAttempt(commandId, attempt.cmd, attempt.args, attempt.stdin, cwd2, attempt.env, emit, timeoutMs);
|
|
34233
34483
|
const tail = parser.flush();
|
|
34234
34484
|
if (tail) {
|
|
34235
34485
|
captured += tail;
|
|
34236
34486
|
handlers.onChunk(tail);
|
|
34237
34487
|
}
|
|
34238
34488
|
lastCaptured = captured;
|
|
34239
|
-
|
|
34489
|
+
const apiErr = parser.apiError();
|
|
34490
|
+
if (finalExit === 0 && !apiErr.isError) {
|
|
34240
34491
|
const usage = parser.usage();
|
|
34241
34492
|
if (usage.tokens === 0) usage.tokens = estimateTokens(captured);
|
|
34242
34493
|
reportToolResult(attempt.cmd, true);
|
|
@@ -34249,7 +34500,8 @@ async function runAiFailover(plan, commandId, cwd2, handlers, timeoutMs = 0) {
|
|
|
34249
34500
|
sessionId: parser.sessionId()
|
|
34250
34501
|
};
|
|
34251
34502
|
}
|
|
34252
|
-
|
|
34503
|
+
if (finalExit === 0 && apiErr.isError) finalExit = apiErr.status && apiErr.status > 0 ? apiErr.status : 1;
|
|
34504
|
+
const reason = isAuthFailure(captured) ? "auth" : apiErr.status === 429 || isOutOfCredits(captured) ? "credits" : isSessionLimit(captured) ? "limit" : "other";
|
|
34253
34505
|
if (i === total - 1) break;
|
|
34254
34506
|
handlers.onAttemptFail(attempt.label, reason);
|
|
34255
34507
|
}
|
|
@@ -34258,6 +34510,7 @@ async function runAiFailover(plan, commandId, cwd2, handlers, timeoutMs = 0) {
|
|
|
34258
34510
|
}
|
|
34259
34511
|
function summarizeAiFailure(captured, exitCode) {
|
|
34260
34512
|
if (isAuthFailure(captured)) return "Not logged in";
|
|
34513
|
+
if (isOutOfCredits(captured)) return "Out of usage credits";
|
|
34261
34514
|
if (isSessionLimit(captured)) return "Usage limit reached";
|
|
34262
34515
|
const firstLine = captured.split("\n").map((l) => l.trim()).find((l) => l.length > 0);
|
|
34263
34516
|
return firstLine || `exited ${exitCode}`;
|
|
@@ -34303,6 +34556,52 @@ function setWorkingCredential(profileDir2, key) {
|
|
|
34303
34556
|
} catch {
|
|
34304
34557
|
}
|
|
34305
34558
|
}
|
|
34559
|
+
var PINNED_CREDENTIAL_KEY = "__pinned__";
|
|
34560
|
+
function getPinnedCredential(profileDir2) {
|
|
34561
|
+
return readState(profileDir2)[PINNED_CREDENTIAL_KEY] ?? null;
|
|
34562
|
+
}
|
|
34563
|
+
function setPinnedCredential(profileDir2, key) {
|
|
34564
|
+
const state = readState(profileDir2);
|
|
34565
|
+
if (state[PINNED_CREDENTIAL_KEY] === key) return;
|
|
34566
|
+
state[PINNED_CREDENTIAL_KEY] = key;
|
|
34567
|
+
try {
|
|
34568
|
+
writeFileSync4(statePath(profileDir2), JSON.stringify(state, null, 2), "utf8");
|
|
34569
|
+
} catch {
|
|
34570
|
+
}
|
|
34571
|
+
}
|
|
34572
|
+
function clearPinnedCredential(profileDir2) {
|
|
34573
|
+
const state = readState(profileDir2);
|
|
34574
|
+
if (!(PINNED_CREDENTIAL_KEY in state)) return;
|
|
34575
|
+
delete state[PINNED_CREDENTIAL_KEY];
|
|
34576
|
+
try {
|
|
34577
|
+
writeFileSync4(statePath(profileDir2), JSON.stringify(state, null, 2), "utf8");
|
|
34578
|
+
} catch {
|
|
34579
|
+
}
|
|
34580
|
+
}
|
|
34581
|
+
|
|
34582
|
+
// src/utils/time.ts
|
|
34583
|
+
function formatTimestampInZone(date, timeZone) {
|
|
34584
|
+
const build = (tz) => {
|
|
34585
|
+
const parts = new Intl.DateTimeFormat("en-CA", {
|
|
34586
|
+
...tz ? { timeZone: tz } : {},
|
|
34587
|
+
hour12: false,
|
|
34588
|
+
year: "numeric",
|
|
34589
|
+
month: "2-digit",
|
|
34590
|
+
day: "2-digit",
|
|
34591
|
+
hour: "2-digit",
|
|
34592
|
+
minute: "2-digit",
|
|
34593
|
+
second: "2-digit"
|
|
34594
|
+
}).formatToParts(date);
|
|
34595
|
+
const get2 = (type) => parts.find((p) => p.type === type)?.value ?? "";
|
|
34596
|
+
const hour = get2("hour") === "24" ? "00" : get2("hour");
|
|
34597
|
+
return `${get2("year")}/${get2("month")}/${get2("day")} ${hour}:${get2("minute")}:${get2("second")}`;
|
|
34598
|
+
};
|
|
34599
|
+
try {
|
|
34600
|
+
return build(timeZone);
|
|
34601
|
+
} catch {
|
|
34602
|
+
return build();
|
|
34603
|
+
}
|
|
34604
|
+
}
|
|
34306
34605
|
|
|
34307
34606
|
// src/utils/ai-cli.ts
|
|
34308
34607
|
import { readFileSync as readFileSync8 } from "fs";
|
|
@@ -34313,6 +34612,42 @@ var REQUIRED_AI_ARGS = {
|
|
|
34313
34612
|
codex: ["exec", "--json"],
|
|
34314
34613
|
antigravity: []
|
|
34315
34614
|
};
|
|
34615
|
+
var ONE_SHOT_DISALLOWED_CLAUDE_TOOLS = [
|
|
34616
|
+
"Bash",
|
|
34617
|
+
"Edit",
|
|
34618
|
+
"Write",
|
|
34619
|
+
"Read",
|
|
34620
|
+
"Glob",
|
|
34621
|
+
"Grep",
|
|
34622
|
+
"NotebookEdit",
|
|
34623
|
+
"Task",
|
|
34624
|
+
"WebSearch",
|
|
34625
|
+
"WebFetch",
|
|
34626
|
+
"Skill",
|
|
34627
|
+
"SlashCommand",
|
|
34628
|
+
"ToolSearch",
|
|
34629
|
+
"TaskCreate",
|
|
34630
|
+
"TaskGet",
|
|
34631
|
+
"TaskList",
|
|
34632
|
+
"TaskOutput",
|
|
34633
|
+
"TaskStop",
|
|
34634
|
+
"TaskUpdate",
|
|
34635
|
+
"Monitor",
|
|
34636
|
+
"DesignSync",
|
|
34637
|
+
"CronCreate",
|
|
34638
|
+
"CronDelete",
|
|
34639
|
+
"CronList",
|
|
34640
|
+
"EnterWorktree",
|
|
34641
|
+
"ExitWorktree",
|
|
34642
|
+
"RemoteTrigger",
|
|
34643
|
+
"ScheduleWakeup",
|
|
34644
|
+
"SendMessage",
|
|
34645
|
+
"PushNotification"
|
|
34646
|
+
].join(",");
|
|
34647
|
+
function oneShotArgs(cmd) {
|
|
34648
|
+
if (!cmd.includes("claude")) return [];
|
|
34649
|
+
return ["--max-turns=1", "--disallowedTools", ONE_SHOT_DISALLOWED_CLAUDE_TOOLS, "--permission-mode", "default"];
|
|
34650
|
+
}
|
|
34316
34651
|
function requiredArgs(cmd) {
|
|
34317
34652
|
if (cmd.includes("claude")) return REQUIRED_AI_ARGS.claude;
|
|
34318
34653
|
if (cmd.includes("codex")) return REQUIRED_AI_ARGS.codex;
|
|
@@ -34372,12 +34707,15 @@ function folderScopeGuard(folder, prompt) {
|
|
|
34372
34707
|
prompt
|
|
34373
34708
|
].join("\n");
|
|
34374
34709
|
}
|
|
34375
|
-
function buildRunArgs(profile, cmd,
|
|
34710
|
+
function buildRunArgs(profile, cmd, resumeId, oneShot = false) {
|
|
34376
34711
|
const extras = profile.args ?? [];
|
|
34377
34712
|
const model = profile.model?.trim();
|
|
34378
34713
|
const modelArgs = model ? ["--model", model] : [];
|
|
34379
34714
|
const resumeArgs = resumeId && cmd.includes("claude") ? ["--resume", resumeId] : [];
|
|
34380
|
-
|
|
34715
|
+
const turns = profile.maxTurns;
|
|
34716
|
+
const perProfileTurns = !oneShot && cmd.includes("claude") && typeof turns === "number" && turns > 0 ? [`--max-turns=${Math.floor(turns)}`] : [];
|
|
34717
|
+
const oneShotExtra = oneShot ? oneShotArgs(cmd) : perProfileTurns;
|
|
34718
|
+
return [...requiredArgs(cmd), ...resumeArgs, ...extras, ...modelArgs, ...oneShotExtra];
|
|
34381
34719
|
}
|
|
34382
34720
|
function claudeHomeDirs(config) {
|
|
34383
34721
|
const dirs = profileDirs(claudeProfilesOf(config));
|
|
@@ -34416,10 +34754,10 @@ function resolveClaudeProfiles(config, workingDir) {
|
|
|
34416
34754
|
if (idx > 0) profiles.unshift(profiles.splice(idx, 1)[0]);
|
|
34417
34755
|
return profiles;
|
|
34418
34756
|
}
|
|
34419
|
-
function planAiRun(prompt, config, hint = {}, resume = /* @__PURE__ */ new Map()) {
|
|
34420
|
-
return isUnifiedConfig(config) ? planUnifiedRun(prompt, config, hint.credential ?? null, resume) : planLegacyRun(prompt, config, hint.dir ?? null, resume);
|
|
34757
|
+
function planAiRun(prompt, config, hint = {}, resume = /* @__PURE__ */ new Map(), oneShot = false) {
|
|
34758
|
+
return isUnifiedConfig(config) ? planUnifiedRun(prompt, config, hint.credential ?? null, resume, oneShot) : planLegacyRun(prompt, config, hint.dir ?? null, resume, oneShot);
|
|
34421
34759
|
}
|
|
34422
|
-
function planUnifiedRun(prompt, config, workingCredential, resume) {
|
|
34760
|
+
function planUnifiedRun(prompt, config, workingCredential, resume, oneShot) {
|
|
34423
34761
|
const baseEnv = config.aiEnv ?? {};
|
|
34424
34762
|
const active = activeProvider(config);
|
|
34425
34763
|
const usable = config.aiProfiles.filter(isUsableCredential);
|
|
@@ -34428,7 +34766,7 @@ function planUnifiedRun(prompt, config, workingCredential, resume) {
|
|
|
34428
34766
|
return {
|
|
34429
34767
|
cmd: active,
|
|
34430
34768
|
attempts: [
|
|
34431
|
-
{ cmd: active, label: "default", dir: null, key: null, args: buildRunArgs({ profile: "" }, active,
|
|
34769
|
+
{ cmd: active, label: "default", dir: null, key: null, args: buildRunArgs({ profile: "" }, active, void 0, oneShot), stdin: prompt, env: { ...baseEnv } }
|
|
34432
34770
|
]
|
|
34433
34771
|
};
|
|
34434
34772
|
}
|
|
@@ -34446,19 +34784,20 @@ function planUnifiedRun(prompt, config, workingCredential, resume) {
|
|
|
34446
34784
|
label: cred.label?.trim() || profileDisplayLabel(dir2),
|
|
34447
34785
|
dir: dir2,
|
|
34448
34786
|
key,
|
|
34449
|
-
args: buildRunArgs(cred, cmd,
|
|
34787
|
+
args: buildRunArgs(cred, cmd, resume.get(key), oneShot),
|
|
34788
|
+
stdin: prompt,
|
|
34450
34789
|
env: envVar ? { ...baseEnv, [envVar]: dir2 } : { ...baseEnv }
|
|
34451
34790
|
}));
|
|
34452
34791
|
return { cmd: attempts[0]?.cmd ?? DEFAULT_AI_CLI, attempts };
|
|
34453
34792
|
}
|
|
34454
|
-
function planLegacyRun(prompt, config, workingDir, resume) {
|
|
34793
|
+
function planLegacyRun(prompt, config, workingDir, resume, oneShot) {
|
|
34455
34794
|
const cmd = config.aiCli || DEFAULT_AI_CLI;
|
|
34456
34795
|
const baseEnv = config.aiEnv ?? {};
|
|
34457
34796
|
const envVar = profileEnvVar(cmd);
|
|
34458
34797
|
const defaultPlan = {
|
|
34459
34798
|
cmd,
|
|
34460
34799
|
attempts: [
|
|
34461
|
-
{ cmd, label: "default", dir: null, key: null, args: buildRunArgs({ profile: "" }, cmd,
|
|
34800
|
+
{ cmd, label: "default", dir: null, key: null, args: buildRunArgs({ profile: "" }, cmd, void 0, oneShot), stdin: prompt, env: { ...baseEnv } }
|
|
34462
34801
|
]
|
|
34463
34802
|
};
|
|
34464
34803
|
if (!envVar) return defaultPlan;
|
|
@@ -34475,7 +34814,8 @@ function planLegacyRun(prompt, config, workingDir, resume) {
|
|
|
34475
34814
|
label: profileDisplayLabel(dir2),
|
|
34476
34815
|
dir: dir2,
|
|
34477
34816
|
key,
|
|
34478
|
-
args: buildRunArgs(profile, cmd,
|
|
34817
|
+
args: buildRunArgs(profile, cmd, resume.get(key), oneShot),
|
|
34818
|
+
stdin: prompt,
|
|
34479
34819
|
env: { ...baseEnv, [envVar]: dir2 }
|
|
34480
34820
|
};
|
|
34481
34821
|
});
|
|
@@ -34483,7 +34823,7 @@ function planLegacyRun(prompt, config, workingDir, resume) {
|
|
|
34483
34823
|
}
|
|
34484
34824
|
|
|
34485
34825
|
// src/core/claude-usage.ts
|
|
34486
|
-
import { spawn as
|
|
34826
|
+
import { spawn as spawn4 } from "child_process";
|
|
34487
34827
|
import { readFileSync as readFileSync9 } from "fs";
|
|
34488
34828
|
import { join as join9 } from "path";
|
|
34489
34829
|
var USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
|
|
@@ -34530,7 +34870,7 @@ function runUsageCli(dir2, aiCli2) {
|
|
|
34530
34870
|
}, 3e4);
|
|
34531
34871
|
timer.unref?.();
|
|
34532
34872
|
try {
|
|
34533
|
-
child =
|
|
34873
|
+
child = spawn4(aiCli2, ["/usage"], {
|
|
34534
34874
|
env: { ...process.env, CLAUDE_CONFIG_DIR: dir2 },
|
|
34535
34875
|
stdio: ["ignore", "pipe", "ignore"]
|
|
34536
34876
|
});
|
|
@@ -34600,7 +34940,7 @@ async function checkClaudeUsage(dirs, aiCli2 = "claude") {
|
|
|
34600
34940
|
// src/core/ai-assist.ts
|
|
34601
34941
|
import { execFile } from "child_process";
|
|
34602
34942
|
import { promisify } from "util";
|
|
34603
|
-
var
|
|
34943
|
+
var run2 = promisify(execFile);
|
|
34604
34944
|
function aiCli() {
|
|
34605
34945
|
return process.env.AI_CLI || "claude";
|
|
34606
34946
|
}
|
|
@@ -34643,7 +34983,7 @@ function parseAi(cli, stdout, prompt) {
|
|
|
34643
34983
|
}
|
|
34644
34984
|
async function runAi(prompt) {
|
|
34645
34985
|
const cli = aiCli();
|
|
34646
|
-
const { stdout } = await
|
|
34986
|
+
const { stdout } = await run2(cli, aiArgs(cli, prompt), {
|
|
34647
34987
|
timeout: 12e4,
|
|
34648
34988
|
maxBuffer: 8 * 1024 * 1024
|
|
34649
34989
|
});
|
|
@@ -34986,7 +35326,7 @@ async function writeWorkerFile(root, relPath, content) {
|
|
|
34986
35326
|
}
|
|
34987
35327
|
|
|
34988
35328
|
// src/core/support-answer.ts
|
|
34989
|
-
import { spawn as
|
|
35329
|
+
import { spawn as spawn5 } from "child_process";
|
|
34990
35330
|
import { execFile as execFile2 } from "child_process";
|
|
34991
35331
|
import { createHash as createHash3 } from "crypto";
|
|
34992
35332
|
import { existsSync as existsSync8, mkdirSync as mkdirSync4, readdirSync as readdirSync4, readFileSync as readFileSync12, statSync as statSync3 } from "fs";
|
|
@@ -35121,7 +35461,7 @@ function runClaudeOnce(cmd, profile, prompt, extraEnv) {
|
|
|
35121
35461
|
...extraEnv,
|
|
35122
35462
|
...profile ? { CLAUDE_CONFIG_DIR: profile.dir } : {}
|
|
35123
35463
|
};
|
|
35124
|
-
const child =
|
|
35464
|
+
const child = spawn5(cmd, args, { stdio: ["pipe", "pipe", "pipe"], env: env3 });
|
|
35125
35465
|
const parser = createAiStreamParser(cmd);
|
|
35126
35466
|
let out = "";
|
|
35127
35467
|
let err = "";
|
|
@@ -35191,7 +35531,7 @@ async function runSupportAnswer(req, ai, profileDir2) {
|
|
|
35191
35531
|
}
|
|
35192
35532
|
|
|
35193
35533
|
// src/core/knowledge-compose.ts
|
|
35194
|
-
import { spawn as
|
|
35534
|
+
import { spawn as spawn6 } from "child_process";
|
|
35195
35535
|
var COMPOSE_TIMEOUT_MS = 18e4;
|
|
35196
35536
|
function buildPrompt2(req) {
|
|
35197
35537
|
const focus = req.prompt ? `
|
|
@@ -35217,7 +35557,7 @@ function runOnce(cmd, profile, prompt, cwd2, extraEnv) {
|
|
|
35217
35557
|
...extraEnv,
|
|
35218
35558
|
...profile ? { CLAUDE_CONFIG_DIR: profile.dir } : {}
|
|
35219
35559
|
};
|
|
35220
|
-
const child =
|
|
35560
|
+
const child = spawn6(cmd, args, { stdio: ["pipe", "pipe", "pipe"], cwd: cwd2, env: env3 });
|
|
35221
35561
|
let out = "";
|
|
35222
35562
|
let err = "";
|
|
35223
35563
|
const timer = setTimeout(() => {
|
|
@@ -35265,10 +35605,10 @@ async function runKnowledgeCompose(req, ai, physicRoot) {
|
|
|
35265
35605
|
// src/core/autonomous.ts
|
|
35266
35606
|
import { readFile as readFile3, writeFile as writeFile2, chmod } from "fs/promises";
|
|
35267
35607
|
import { readFileSync as readFileSync13 } from "fs";
|
|
35268
|
-
import { execFile as execFile3, spawn as
|
|
35608
|
+
import { execFile as execFile3, spawn as spawn7 } from "child_process";
|
|
35269
35609
|
import { join as join13 } from "path";
|
|
35270
35610
|
import { promisify as promisify3 } from "util";
|
|
35271
|
-
var
|
|
35611
|
+
var run3 = promisify3(execFile3);
|
|
35272
35612
|
var SETTINGS_REL = ".claude/.autonomous.settings.json";
|
|
35273
35613
|
var HISTORIES_REL = ".claude/.autonomous.histories.json";
|
|
35274
35614
|
var APPROVALS_REL = ".claude/.autonomous.approvals.json";
|
|
@@ -35302,7 +35642,7 @@ function tickScript(root) {
|
|
|
35302
35642
|
}
|
|
35303
35643
|
async function crontabList() {
|
|
35304
35644
|
try {
|
|
35305
|
-
const { stdout } = await
|
|
35645
|
+
const { stdout } = await run3("crontab", ["-l"], { timeout: 1e4 });
|
|
35306
35646
|
return stdout;
|
|
35307
35647
|
} catch {
|
|
35308
35648
|
return "";
|
|
@@ -35310,7 +35650,7 @@ async function crontabList() {
|
|
|
35310
35650
|
}
|
|
35311
35651
|
async function crontabSet(content) {
|
|
35312
35652
|
return new Promise((resolve8) => {
|
|
35313
|
-
const child =
|
|
35653
|
+
const child = spawn7("crontab", ["-"], { stdio: ["pipe", "ignore", "ignore"] });
|
|
35314
35654
|
child.on("error", () => resolve8(false));
|
|
35315
35655
|
child.on("close", (code) => resolve8(code === 0));
|
|
35316
35656
|
child.stdin.end(content.endsWith("\n") ? content : content + "\n");
|
|
@@ -36041,15 +36381,15 @@ async function buildGraph(root, mode) {
|
|
|
36041
36381
|
// src/core/rag.ts
|
|
36042
36382
|
import { readFile as readFile9, writeFile as writeFile7, statfs as statfs2, mkdir as mkdir6 } from "fs/promises";
|
|
36043
36383
|
import { existsSync as existsSync10 } from "fs";
|
|
36044
|
-
import { execFile as execFile4, spawn as
|
|
36384
|
+
import { execFile as execFile4, spawn as spawn8 } from "child_process";
|
|
36045
36385
|
import { totalmem as totalmem2, cpus as cpus2 } from "os";
|
|
36046
36386
|
import { join as join20 } from "path";
|
|
36047
36387
|
import { promisify as promisify4 } from "util";
|
|
36048
|
-
var
|
|
36388
|
+
var run4 = promisify4(execFile4);
|
|
36049
36389
|
var RAG_REL = ".claude/rag";
|
|
36050
36390
|
async function tryRun(cmd, args) {
|
|
36051
36391
|
try {
|
|
36052
|
-
const { stdout } = await
|
|
36392
|
+
const { stdout } = await run4(cmd, args, { timeout: 8e3 });
|
|
36053
36393
|
return { ok: true, out: stdout.trim() };
|
|
36054
36394
|
} catch {
|
|
36055
36395
|
return { ok: false, out: "" };
|
|
@@ -36228,7 +36568,7 @@ touch "${RAG_REL}/.indexing"
|
|
|
36228
36568
|
{ echo "[reindex] $(date)"; "$RAG_PY" "${RAG_REL}/rag_engine.py" reindex 2>&1; } >> "${RAG_REL}/index.log" 2>&1
|
|
36229
36569
|
rm -f "${RAG_REL}/.indexing"
|
|
36230
36570
|
`;
|
|
36231
|
-
const child =
|
|
36571
|
+
const child = spawn8("bash", ["-c", script], {
|
|
36232
36572
|
cwd: root,
|
|
36233
36573
|
env: { ...process.env, RAG_PY: py.bin },
|
|
36234
36574
|
detached: true,
|
|
@@ -36243,7 +36583,7 @@ async function ragQuery(root, queryText, k = 8) {
|
|
|
36243
36583
|
if (!existsSync10(join20(root, RAG_REL, "index.db"))) return { results: [], error: "no index \u2014 reindex first" };
|
|
36244
36584
|
await ensureEngine(root);
|
|
36245
36585
|
try {
|
|
36246
|
-
const { stdout } = await
|
|
36586
|
+
const { stdout } = await run4(
|
|
36247
36587
|
py.bin,
|
|
36248
36588
|
[join20(RAG_REL, "rag_engine.py"), "query", queryText, String(Math.min(Math.max(k, 1), 20))],
|
|
36249
36589
|
{ cwd: root, timeout: 3e4, maxBuffer: 4 * 1024 * 1024 }
|
|
@@ -36278,7 +36618,7 @@ rm -f "${RAG_REL}/.installed"
|
|
|
36278
36618
|
} >> "${RAG_REL}/install.log" 2>&1
|
|
36279
36619
|
rm -f "${RAG_REL}/.installing"
|
|
36280
36620
|
`;
|
|
36281
|
-
const child =
|
|
36621
|
+
const child = spawn8("bash", ["-c", script], {
|
|
36282
36622
|
cwd: root,
|
|
36283
36623
|
env: { ...process.env, RAG_PY: py.bin, RAG_MODEL: model },
|
|
36284
36624
|
detached: true,
|
|
@@ -36293,7 +36633,7 @@ import { readFile as readFile10 } from "fs/promises";
|
|
|
36293
36633
|
import { execFile as execFile5 } from "child_process";
|
|
36294
36634
|
import { basename as basename3, dirname as dirname7, resolve as resolve5 } from "path";
|
|
36295
36635
|
import { promisify as promisify5 } from "util";
|
|
36296
|
-
var
|
|
36636
|
+
var run5 = promisify5(execFile5);
|
|
36297
36637
|
async function gitDiff(path) {
|
|
36298
36638
|
const target = resolve5(path);
|
|
36299
36639
|
const dir2 = dirname7(target);
|
|
@@ -36305,7 +36645,7 @@ async function gitDiff(path) {
|
|
|
36305
36645
|
}
|
|
36306
36646
|
let oldContent = "";
|
|
36307
36647
|
try {
|
|
36308
|
-
const { stdout } = await
|
|
36648
|
+
const { stdout } = await run5("git", ["show", `HEAD:./${base}`], {
|
|
36309
36649
|
cwd: dir2,
|
|
36310
36650
|
timeout: 15e3,
|
|
36311
36651
|
maxBuffer: 4 * 1024 * 1024
|
|
@@ -36319,10 +36659,10 @@ async function gitDiff(path) {
|
|
|
36319
36659
|
// src/core/git-env.ts
|
|
36320
36660
|
import { execFile as execFile6 } from "child_process";
|
|
36321
36661
|
import { promisify as promisify6 } from "util";
|
|
36322
|
-
var
|
|
36662
|
+
var run6 = promisify6(execFile6);
|
|
36323
36663
|
async function hasBinary(bin, args = ["--version"]) {
|
|
36324
36664
|
try {
|
|
36325
|
-
await
|
|
36665
|
+
await run6(bin, args, { timeout: 5e3 });
|
|
36326
36666
|
return true;
|
|
36327
36667
|
} catch {
|
|
36328
36668
|
return false;
|
|
@@ -36335,7 +36675,7 @@ async function checkGitEnv(provider) {
|
|
|
36335
36675
|
let account = null;
|
|
36336
36676
|
if (installed) {
|
|
36337
36677
|
try {
|
|
36338
|
-
const { stdout, stderr } = await
|
|
36678
|
+
const { stdout, stderr } = await run6(bin, ["auth", "status"], { timeout: 8e3 });
|
|
36339
36679
|
const out = `${stdout}
|
|
36340
36680
|
${stderr}`;
|
|
36341
36681
|
authenticated = /logged in|logged into|✓/i.test(out);
|
|
@@ -36352,7 +36692,7 @@ import { execFile as execFile7 } from "child_process";
|
|
|
36352
36692
|
import { readFile as readFile11, rm as rm3, mkdir as mkdir7 } from "fs/promises";
|
|
36353
36693
|
import { join as join21 } from "path";
|
|
36354
36694
|
import { promisify as promisify7 } from "util";
|
|
36355
|
-
var
|
|
36695
|
+
var run7 = promisify7(execFile7);
|
|
36356
36696
|
function keyPaths(profileDir2) {
|
|
36357
36697
|
const dir2 = join21(profileDir2, ".ssh");
|
|
36358
36698
|
const key = join21(dir2, "id_4pm");
|
|
@@ -36367,14 +36707,14 @@ async function readPublicKey(pub) {
|
|
|
36367
36707
|
}
|
|
36368
36708
|
async function fingerprintOf(pub) {
|
|
36369
36709
|
try {
|
|
36370
|
-
const { stdout } = await
|
|
36710
|
+
const { stdout } = await run7("ssh-keygen", ["-lf", pub], { timeout: 5e3 });
|
|
36371
36711
|
return stdout.match(/SHA256:[A-Za-z0-9+/=]+/)?.[0] ?? null;
|
|
36372
36712
|
} catch {
|
|
36373
36713
|
return null;
|
|
36374
36714
|
}
|
|
36375
36715
|
}
|
|
36376
36716
|
async function configureGit(key) {
|
|
36377
|
-
await
|
|
36717
|
+
await run7(
|
|
36378
36718
|
"git",
|
|
36379
36719
|
["config", "--global", "core.sshCommand", `ssh -i ${key} -o IdentitiesOnly=yes`],
|
|
36380
36720
|
{ timeout: 5e3 }
|
|
@@ -36385,7 +36725,7 @@ async function generate(profileDir2) {
|
|
|
36385
36725
|
await mkdir7(dir2, { recursive: true });
|
|
36386
36726
|
await rm3(key, { force: true });
|
|
36387
36727
|
await rm3(pub, { force: true });
|
|
36388
|
-
await
|
|
36728
|
+
await run7("ssh-keygen", ["-t", "ed25519", "-f", key, "-N", "", "-C", "4pm-deploy-key"], {
|
|
36389
36729
|
timeout: 15e3
|
|
36390
36730
|
});
|
|
36391
36731
|
await configureGit(key);
|
|
@@ -36395,7 +36735,7 @@ async function remove(profileDir2) {
|
|
|
36395
36735
|
const { key, pub } = keyPaths(profileDir2);
|
|
36396
36736
|
await rm3(key, { force: true });
|
|
36397
36737
|
await rm3(pub, { force: true });
|
|
36398
|
-
await
|
|
36738
|
+
await run7("git", ["config", "--global", "--unset", "core.sshCommand"], { timeout: 5e3 }).catch(
|
|
36399
36739
|
() => void 0
|
|
36400
36740
|
);
|
|
36401
36741
|
return { publicKey: null, fingerprint: null };
|
|
@@ -36420,7 +36760,7 @@ import { readdir as readdir6 } from "fs/promises";
|
|
|
36420
36760
|
import { execFile as execFile8 } from "child_process";
|
|
36421
36761
|
import { resolve as resolve6, sep as sep4 } from "path";
|
|
36422
36762
|
import { promisify as promisify8 } from "util";
|
|
36423
|
-
var
|
|
36763
|
+
var run8 = promisify8(execFile8);
|
|
36424
36764
|
var FS = "";
|
|
36425
36765
|
var RS = "";
|
|
36426
36766
|
function resolveRepo(root, repo) {
|
|
@@ -36431,7 +36771,7 @@ function resolveRepo(root, repo) {
|
|
|
36431
36771
|
}
|
|
36432
36772
|
async function git(cwd2, args) {
|
|
36433
36773
|
try {
|
|
36434
|
-
const { stdout } = await
|
|
36774
|
+
const { stdout } = await run8("git", args, {
|
|
36435
36775
|
cwd: cwd2,
|
|
36436
36776
|
timeout: 15e3,
|
|
36437
36777
|
maxBuffer: 8 * 1024 * 1024
|
|
@@ -36535,11 +36875,11 @@ async function gitCommitDiff(root, repo, hash, path) {
|
|
|
36535
36875
|
// src/core/git-commit-identity.ts
|
|
36536
36876
|
import { execFile as execFile9 } from "child_process";
|
|
36537
36877
|
import { promisify as promisify9 } from "util";
|
|
36538
|
-
var
|
|
36878
|
+
var run9 = promisify9(execFile9);
|
|
36539
36879
|
async function setCommitAuthor(cwd2, machineUsername) {
|
|
36540
36880
|
if (!machineUsername) return;
|
|
36541
36881
|
try {
|
|
36542
|
-
await
|
|
36882
|
+
await run9("git", ["config", "--local", "user.name", machineUsername], { cwd: cwd2, timeout: 8e3 });
|
|
36543
36883
|
} catch {
|
|
36544
36884
|
}
|
|
36545
36885
|
}
|
|
@@ -36550,7 +36890,7 @@ import { execFile as execFile10 } from "child_process";
|
|
|
36550
36890
|
import { dirname as dirname8, join as join22, resolve as resolve7 } from "path";
|
|
36551
36891
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
36552
36892
|
import { promisify as promisify10 } from "util";
|
|
36553
|
-
var
|
|
36893
|
+
var run10 = promisify10(execFile10);
|
|
36554
36894
|
function reposOf(spec) {
|
|
36555
36895
|
const repos = spec?.repos;
|
|
36556
36896
|
return Array.isArray(repos) ? repos : [];
|
|
@@ -36564,13 +36904,13 @@ async function provisionRepos(target, repos, emit, opts = {}) {
|
|
|
36564
36904
|
if (repo.primary) {
|
|
36565
36905
|
if (opts.clonePrimary && repo.url) {
|
|
36566
36906
|
emit("git", `Cloning ${repo.url}\u2026`);
|
|
36567
|
-
await
|
|
36907
|
+
await run10("git", ["clone", repo.url, target], { timeout: 12e4 });
|
|
36568
36908
|
continue;
|
|
36569
36909
|
}
|
|
36570
36910
|
emit("git", "Initializing the primary repo\u2026");
|
|
36571
|
-
await
|
|
36911
|
+
await run10("git", ["init"], { cwd: target, timeout: 2e4 });
|
|
36572
36912
|
if (repo.url) {
|
|
36573
|
-
await
|
|
36913
|
+
await run10("git", ["remote", "add", "origin", repo.url], { cwd: target, timeout: 2e4 }).catch(
|
|
36574
36914
|
() => void 0
|
|
36575
36915
|
);
|
|
36576
36916
|
}
|
|
@@ -36579,11 +36919,11 @@ async function provisionRepos(target, repos, emit, opts = {}) {
|
|
|
36579
36919
|
const dir2 = join22(target, subDirName(repo));
|
|
36580
36920
|
if (repo.url) {
|
|
36581
36921
|
emit("git", `Cloning sub-repo ${repo.url}\u2026`);
|
|
36582
|
-
await
|
|
36922
|
+
await run10("git", ["clone", repo.url, dir2], { timeout: 12e4 });
|
|
36583
36923
|
} else {
|
|
36584
36924
|
emit("git", `Initializing sub-repo ${subDirName(repo)}\u2026`);
|
|
36585
36925
|
await mkdir8(dir2, { recursive: true });
|
|
36586
|
-
await
|
|
36926
|
+
await run10("git", ["init"], { cwd: dir2, timeout: 2e4 });
|
|
36587
36927
|
}
|
|
36588
36928
|
}
|
|
36589
36929
|
}
|
|
@@ -36668,240 +37008,513 @@ async function addProject(payload, profileDir2, onProgress) {
|
|
|
36668
37008
|
}
|
|
36669
37009
|
}
|
|
36670
37010
|
|
|
36671
|
-
// src/
|
|
36672
|
-
import {
|
|
36673
|
-
var COMPACT_TIMEOUT_MS = 6e4;
|
|
36674
|
-
function buildPrompt3(input) {
|
|
36675
|
-
return [
|
|
36676
|
-
"You maintain a COMPACT running memory of an ongoing assistant conversation so it can continue",
|
|
36677
|
-
"across sessions and accounts. Merge the latest exchange into the existing memory and return the",
|
|
36678
|
-
`UPDATED memory. Keep it UNDER ${input.budgetChars} characters. Use terse Markdown bullets grouped`,
|
|
36679
|
-
"as: Decisions, Constraints, Open tasks, Glossary. Keep durable facts; drop chit-chat and anything",
|
|
36680
|
-
"now obsolete. Output ONLY the updated memory text \u2014 no preamble, no code fences.",
|
|
36681
|
-
"",
|
|
36682
|
-
"=== EXISTING MEMORY ===",
|
|
36683
|
-
input.oldMemory || "(empty)",
|
|
36684
|
-
"",
|
|
36685
|
-
"=== LATEST USER PROMPT ===",
|
|
36686
|
-
input.prompt,
|
|
36687
|
-
"",
|
|
36688
|
-
"=== LATEST ASSISTANT ANSWER ===",
|
|
36689
|
-
input.answer
|
|
36690
|
-
].join("\n");
|
|
36691
|
-
}
|
|
36692
|
-
function runOnce2(cmd, profile, prompt, cwd2, extraEnv) {
|
|
36693
|
-
return new Promise((resolve8, reject) => {
|
|
36694
|
-
const args = ["-p", ...profile?.model ? ["--model", profile.model] : []];
|
|
36695
|
-
const env3 = {
|
|
36696
|
-
...process.env,
|
|
36697
|
-
...extraEnv,
|
|
36698
|
-
...profile ? { CLAUDE_CONFIG_DIR: profile.dir } : {}
|
|
36699
|
-
};
|
|
36700
|
-
const child = spawn8(cmd, args, { stdio: ["pipe", "pipe", "pipe"], cwd: cwd2, env: env3 });
|
|
36701
|
-
let out = "";
|
|
36702
|
-
let err = "";
|
|
36703
|
-
const timer = setTimeout(() => {
|
|
36704
|
-
child.kill("SIGKILL");
|
|
36705
|
-
reject(new Error("memory compaction timed out"));
|
|
36706
|
-
}, COMPACT_TIMEOUT_MS);
|
|
36707
|
-
timer.unref();
|
|
36708
|
-
child.stdout.on("data", (d) => out += d.toString());
|
|
36709
|
-
child.stderr.on("data", (d) => err += d.toString());
|
|
36710
|
-
child.on("error", (e) => {
|
|
36711
|
-
clearTimeout(timer);
|
|
36712
|
-
reject(e);
|
|
36713
|
-
});
|
|
36714
|
-
child.on("close", (code) => {
|
|
36715
|
-
clearTimeout(timer);
|
|
36716
|
-
resolve8({ code: code ?? -1, out, err });
|
|
36717
|
-
});
|
|
36718
|
-
child.stdin.write(prompt);
|
|
36719
|
-
child.stdin.end();
|
|
36720
|
-
});
|
|
36721
|
-
}
|
|
36722
|
-
async function runMemoryCompaction(ai, cwd2, input) {
|
|
36723
|
-
const prompt = buildPrompt3(input);
|
|
36724
|
-
const attempts = ai.profiles.length > 0 ? ai.profiles : [null];
|
|
36725
|
-
for (let i = 0; i < attempts.length; i++) {
|
|
36726
|
-
try {
|
|
36727
|
-
const { code, out } = await runOnce2(ai.cmd, attempts[i], prompt, cwd2, ai.env);
|
|
36728
|
-
if (code === 0 && out.trim()) {
|
|
36729
|
-
reportToolResult(ai.cmd, true);
|
|
36730
|
-
return out.trim().slice(0, input.budgetChars);
|
|
36731
|
-
}
|
|
36732
|
-
if (i === attempts.length - 1) break;
|
|
36733
|
-
} catch {
|
|
36734
|
-
break;
|
|
36735
|
-
}
|
|
36736
|
-
}
|
|
36737
|
-
return "";
|
|
36738
|
-
}
|
|
36739
|
-
|
|
36740
|
-
// src/core/config-sync.ts
|
|
36741
|
-
import { existsSync as existsSync11, readFileSync as readFileSync15 } from "fs";
|
|
37011
|
+
// src/ui/slash-commands.ts
|
|
37012
|
+
import { existsSync as existsSync11 } from "fs";
|
|
36742
37013
|
import { join as join23 } from "path";
|
|
36743
|
-
var
|
|
37014
|
+
var CONFIG_KEYS = [
|
|
37015
|
+
"aiCli",
|
|
37016
|
+
"claudeHome",
|
|
37017
|
+
"codexHome",
|
|
37018
|
+
"antigravityHome",
|
|
36744
37019
|
"physicPath",
|
|
36745
|
-
"
|
|
36746
|
-
"
|
|
36747
|
-
|
|
36748
|
-
|
|
36749
|
-
|
|
36750
|
-
|
|
36751
|
-
|
|
36752
|
-
|
|
36753
|
-
|
|
36754
|
-
|
|
36755
|
-
|
|
36756
|
-
|
|
36757
|
-
|
|
36758
|
-
|
|
36759
|
-
|
|
36760
|
-
|
|
36761
|
-
|
|
36762
|
-
|
|
36763
|
-
|
|
37020
|
+
"commandHistoryUploadMinutes",
|
|
37021
|
+
"autoUpdate",
|
|
37022
|
+
"aiEnv"
|
|
37023
|
+
];
|
|
37024
|
+
function configPath(profileDir2) {
|
|
37025
|
+
return join23(profileDir2, "config.json");
|
|
37026
|
+
}
|
|
37027
|
+
function parseConfigAssignment(key, valueTokens) {
|
|
37028
|
+
const value = valueTokens.join(" ").trim();
|
|
37029
|
+
switch (key) {
|
|
37030
|
+
case "aiCli":
|
|
37031
|
+
case "physicPath":
|
|
37032
|
+
if (!value) return { error: `Missing value for "${key}".` };
|
|
37033
|
+
return { patch: { [key]: value } };
|
|
37034
|
+
case "claudeHome":
|
|
37035
|
+
case "codexHome":
|
|
37036
|
+
case "antigravityHome": {
|
|
37037
|
+
const dirs = valueTokens.filter((t) => t.trim());
|
|
37038
|
+
if (dirs.length === 0) return { error: `Missing value for "${key}".` };
|
|
37039
|
+
return { patch: { [key]: dirs.map((profile) => ({ profile, model: "" })) } };
|
|
37040
|
+
}
|
|
37041
|
+
case "commandHistoryUploadMinutes": {
|
|
37042
|
+
const n = Number(value);
|
|
37043
|
+
if (!Number.isFinite(n) || n <= 0) return { error: `"${value}" is not a positive number.` };
|
|
37044
|
+
return { patch: { commandHistoryUploadMinutes: n } };
|
|
36764
37045
|
}
|
|
37046
|
+
case "autoUpdate":
|
|
37047
|
+
if (value !== "true" && value !== "false") return { error: `Use true|false, not "${value}".` };
|
|
37048
|
+
return { patch: { autoUpdate: value === "true" } };
|
|
37049
|
+
case "aiEnv":
|
|
37050
|
+
try {
|
|
37051
|
+
return { patch: { aiEnv: JSON.parse(value) } };
|
|
37052
|
+
} catch {
|
|
37053
|
+
return { error: `aiEnv must be JSON, e.g. {"KEY":"val"}.` };
|
|
37054
|
+
}
|
|
37055
|
+
default:
|
|
37056
|
+
return { error: `Unknown key "${key}". Allowed: ${CONFIG_KEYS.join(", ")}.` };
|
|
36765
37057
|
}
|
|
36766
|
-
return JSON.stringify(defaultProfileConfig(), null, 2);
|
|
36767
37058
|
}
|
|
36768
|
-
function
|
|
36769
|
-
|
|
36770
|
-
|
|
36771
|
-
|
|
36772
|
-
|
|
36773
|
-
|
|
37059
|
+
function runConfig(ctx) {
|
|
37060
|
+
const [sub, ...rest] = ctx.args;
|
|
37061
|
+
const dir2 = ctx.info.profileDir;
|
|
37062
|
+
const path = configPath(dir2);
|
|
37063
|
+
if (!sub || sub === "show") {
|
|
37064
|
+
ctx.print(`config: ${path}`);
|
|
37065
|
+
const json = JSON.stringify(readProfileConfig(dir2), null, 2);
|
|
37066
|
+
for (const line of json.split("\n")) ctx.print(line);
|
|
37067
|
+
return;
|
|
36774
37068
|
}
|
|
36775
|
-
if (
|
|
36776
|
-
|
|
37069
|
+
if (sub === "init") {
|
|
37070
|
+
const writeDefaults = (verb) => {
|
|
37071
|
+
overwriteProfileConfig(dir2, defaultProfileConfig());
|
|
37072
|
+
ctx.print(`\u2714 ${verb} default config: ${path}`);
|
|
37073
|
+
ctx.print("add claude profiles: /config set claudeHome .claude .claude-1 (tried in order)");
|
|
37074
|
+
};
|
|
37075
|
+
if (existsSync11(path)) {
|
|
37076
|
+
ctx.confirm(
|
|
37077
|
+
`config exists: ${path} \u2014 replace it with defaults? (y/N)`,
|
|
37078
|
+
() => writeDefaults("replaced with")
|
|
37079
|
+
);
|
|
37080
|
+
return;
|
|
37081
|
+
}
|
|
37082
|
+
writeDefaults("wrote");
|
|
37083
|
+
return;
|
|
36777
37084
|
}
|
|
36778
|
-
|
|
36779
|
-
|
|
36780
|
-
|
|
36781
|
-
|
|
36782
|
-
|
|
37085
|
+
if (sub === "set") {
|
|
37086
|
+
const [key, ...valueTokens] = rest;
|
|
37087
|
+
if (!key) {
|
|
37088
|
+
ctx.print("usage: /config set <key> <value>", "error");
|
|
37089
|
+
return;
|
|
37090
|
+
}
|
|
37091
|
+
const result = parseConfigAssignment(key, valueTokens);
|
|
37092
|
+
if ("error" in result) {
|
|
37093
|
+
ctx.print(result.error, "error");
|
|
37094
|
+
return;
|
|
37095
|
+
}
|
|
37096
|
+
const merged = writeProfileConfig(dir2, result.patch);
|
|
37097
|
+
ctx.print(`\u2714 ${key} = ${JSON.stringify(merged[key])} (applies to the next prompt)`);
|
|
37098
|
+
return;
|
|
36783
37099
|
}
|
|
36784
|
-
|
|
36785
|
-
|
|
37100
|
+
if (sub === "delete" || sub === "unset") {
|
|
37101
|
+
const key = rest[0];
|
|
37102
|
+
if (!key) {
|
|
37103
|
+
ctx.print("usage: /config delete <key>", "error");
|
|
37104
|
+
return;
|
|
37105
|
+
}
|
|
37106
|
+
if (!CONFIG_KEYS.includes(key)) {
|
|
37107
|
+
ctx.print(`Unknown key "${key}". Allowed: ${CONFIG_KEYS.join(", ")}.`, "error");
|
|
37108
|
+
return;
|
|
37109
|
+
}
|
|
37110
|
+
deleteProfileConfigKey(dir2, key);
|
|
37111
|
+
ctx.print(`\u2714 deleted ${key}`);
|
|
37112
|
+
return;
|
|
37113
|
+
}
|
|
37114
|
+
ctx.print(`Unknown /config subcommand "${sub}". Use: show | init | set | delete.`, "error");
|
|
36786
37115
|
}
|
|
36787
|
-
|
|
36788
|
-
|
|
36789
|
-
|
|
36790
|
-
|
|
36791
|
-
|
|
36792
|
-
return
|
|
36793
|
-
|
|
36794
|
-
|
|
36795
|
-
|
|
36796
|
-
|
|
36797
|
-
|
|
36798
|
-
|
|
36799
|
-
|
|
36800
|
-
|
|
36801
|
-
|
|
36802
|
-
|
|
36803
|
-
|
|
36804
|
-
|
|
36805
|
-
|
|
36806
|
-
|
|
36807
|
-
const
|
|
36808
|
-
|
|
36809
|
-
|
|
36810
|
-
|
|
36811
|
-
|
|
36812
|
-
|
|
36813
|
-
|
|
36814
|
-
|
|
36815
|
-
|
|
36816
|
-
|
|
36817
|
-
|
|
36818
|
-
|
|
37116
|
+
function fmtDuration(totalSec) {
|
|
37117
|
+
const h = Math.floor(totalSec / 3600);
|
|
37118
|
+
const m = Math.floor(totalSec % 3600 / 60);
|
|
37119
|
+
const s = totalSec % 60;
|
|
37120
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
37121
|
+
return h > 0 ? `${h}h ${pad(m)}m ${pad(s)}s` : `${m}m ${pad(s)}s`;
|
|
37122
|
+
}
|
|
37123
|
+
function commandLine(cmd, args) {
|
|
37124
|
+
return `${cmd} ${args.join(" ")}`.trim();
|
|
37125
|
+
}
|
|
37126
|
+
function runHistory(ctx) {
|
|
37127
|
+
const n = Math.max(1, Math.min(50, Number(ctx.args[0]) || 15));
|
|
37128
|
+
const all = getCommandHistory();
|
|
37129
|
+
if (all.length === 0) {
|
|
37130
|
+
ctx.print("No commands run yet.");
|
|
37131
|
+
return;
|
|
37132
|
+
}
|
|
37133
|
+
const recent = all.slice(-n).reverse();
|
|
37134
|
+
ctx.print(`Last ${recent.length} of ${all.length} commands (newest first) \xB7 /output <n> to view:`);
|
|
37135
|
+
recent.forEach((e, i) => {
|
|
37136
|
+
const time = new Date(e.startedAt).toLocaleTimeString();
|
|
37137
|
+
const state = e.status === "done" ? "\u2713" : e.status === "failed" ? `\u2717 exit ${e.exitCode}` : "\xB7 running";
|
|
37138
|
+
ctx.print(` [${i + 1}] ${time} ${commandLine(e.cmd, e.args)} \xB7 ${state}`);
|
|
37139
|
+
});
|
|
37140
|
+
}
|
|
37141
|
+
function runOutput(ctx) {
|
|
37142
|
+
const all = getCommandHistory();
|
|
37143
|
+
const n = Number(ctx.args[0]);
|
|
37144
|
+
if (!Number.isInteger(n) || n < 1 || n > all.length) {
|
|
37145
|
+
ctx.print("usage: /output <n> (n = index from /history)", "error");
|
|
37146
|
+
return;
|
|
37147
|
+
}
|
|
37148
|
+
const entry = all.slice().reverse()[n - 1];
|
|
37149
|
+
const state = entry.exitCode != null ? ` (exit ${entry.exitCode})` : "";
|
|
37150
|
+
ctx.print(`\u2500\u2500 output [${n}] ${commandLine(entry.cmd, entry.args)} \xB7 ${entry.status}${state} \u2500\u2500`);
|
|
37151
|
+
const out = readCommandOutput(entry.commandId);
|
|
37152
|
+
if (out == null) {
|
|
37153
|
+
ctx.print("(no output stored \u2014 it ran in a previous session or produced none)", "warn");
|
|
37154
|
+
return;
|
|
37155
|
+
}
|
|
37156
|
+
for (const line of out.replace(/\n$/, "").split("\n")) ctx.print(line);
|
|
37157
|
+
}
|
|
37158
|
+
function runLogs(ctx) {
|
|
37159
|
+
const n = Math.max(1, Math.min(200, Number(ctx.args[0]) || 20));
|
|
37160
|
+
const dir2 = join23(ctx.info.profileDir, "logs");
|
|
37161
|
+
const lines = readRecentLogLines(dir2, n);
|
|
37162
|
+
if (lines.length === 0) {
|
|
37163
|
+
ctx.print("No log lines yet.");
|
|
37164
|
+
return;
|
|
37165
|
+
}
|
|
37166
|
+
ctx.print(`Last ${lines.length} log lines (${dir2}):`);
|
|
37167
|
+
for (const raw of lines) {
|
|
36819
37168
|
try {
|
|
36820
|
-
|
|
36821
|
-
|
|
36822
|
-
|
|
36823
|
-
|
|
36824
|
-
|
|
37169
|
+
const j = JSON.parse(raw);
|
|
37170
|
+
const time = String(j.timestamp ?? "").slice(11, 19);
|
|
37171
|
+
const level = String(j.level ?? "").padEnd(5);
|
|
37172
|
+
const { timestamp, level: _l, version, profile, scope, event, ...fields } = j;
|
|
37173
|
+
void timestamp;
|
|
37174
|
+
void _l;
|
|
37175
|
+
void version;
|
|
37176
|
+
void profile;
|
|
37177
|
+
void scope;
|
|
37178
|
+
const extra = Object.keys(fields).length ? " " + JSON.stringify(fields) : "";
|
|
37179
|
+
ctx.print(` ${time} ${level} ${String(event ?? "")}${extra}`);
|
|
36825
37180
|
} catch {
|
|
36826
|
-
|
|
37181
|
+
ctx.print(` ${raw}`);
|
|
37182
|
+
}
|
|
37183
|
+
}
|
|
37184
|
+
}
|
|
37185
|
+
function runWhoami(ctx) {
|
|
37186
|
+
ctx.print("whoami: fetching from the server\u2026");
|
|
37187
|
+
void ctx.info.whoami().then((w) => {
|
|
37188
|
+
if (!w) {
|
|
37189
|
+
ctx.print("whoami: unavailable (server unreachable or link invalid)", "warn");
|
|
37190
|
+
return;
|
|
36827
37191
|
}
|
|
37192
|
+
ctx.print(`account: ${w.username} \xB7 scope: ${w.scope} \xB7 roles: ${w.roles.join(", ") || "\u2014"}`);
|
|
37193
|
+
ctx.print(`teams: ${w.teams.length ? w.teams.map((t) => t.name).join(", ") : "(none)"}`);
|
|
37194
|
+
ctx.print(`projects: ${w.projects.length ? w.projects.map((p) => p.name).join(", ") : "(none)"}`);
|
|
36828
37195
|
});
|
|
36829
37196
|
}
|
|
36830
|
-
|
|
36831
|
-
|
|
36832
|
-
if (code !== 0) return null;
|
|
36833
|
-
const first = out.split("\n").map((l) => l.trim()).find(Boolean);
|
|
36834
|
-
return first ?? "";
|
|
37197
|
+
function aiProfileLabel(c) {
|
|
37198
|
+
return c.label?.trim() || profileDisplayLabel(resolveHomePath(c.profile));
|
|
36835
37199
|
}
|
|
36836
|
-
|
|
36837
|
-
const
|
|
36838
|
-
|
|
36839
|
-
|
|
36840
|
-
|
|
36841
|
-
|
|
36842
|
-
|
|
36843
|
-
|
|
37200
|
+
function runAiProfile(ctx) {
|
|
37201
|
+
const dir2 = ctx.info.profileDir;
|
|
37202
|
+
const config = readProfileConfig(dir2);
|
|
37203
|
+
const list = Array.isArray(config.aiProfiles) ? config.aiProfiles : [];
|
|
37204
|
+
const [sub, ...rest] = ctx.args;
|
|
37205
|
+
const action = (sub ?? "list").toLowerCase();
|
|
37206
|
+
if (action === "list") {
|
|
37207
|
+
if (list.length === 0) {
|
|
37208
|
+
ctx.print("No AI profiles configured (config.aiProfiles is empty).", "warn");
|
|
37209
|
+
ctx.print("This worker may use a legacy per-provider config \u2014 /config show to inspect.");
|
|
37210
|
+
return;
|
|
37211
|
+
}
|
|
37212
|
+
const pinned = getPinnedCredential(dir2);
|
|
37213
|
+
const working = getWorkingCredential(dir2);
|
|
37214
|
+
ctx.print("AI profiles (config order = failover priority) \xB7 /ai-profile use <n> to switch:");
|
|
37215
|
+
list.forEach((c, i) => {
|
|
37216
|
+
const usable = isUsableCredential(c);
|
|
37217
|
+
const key = usable ? credentialKey(c.provider, resolveHomePath(c.profile)) : null;
|
|
37218
|
+
const flags = [];
|
|
37219
|
+
if (key && key === pinned) flags.push("pinned");
|
|
37220
|
+
else if (key && !pinned && key === working) flags.push("active");
|
|
37221
|
+
if (c.enabled === false) flags.push("disabled");
|
|
37222
|
+
const model = c.model?.trim() ? ` \xB7 model=${c.model.trim()}` : "";
|
|
37223
|
+
const flagStr = flags.length ? ` (${flags.join(", ")})` : "";
|
|
37224
|
+
const marker = key && key === pinned ? "*" : " ";
|
|
37225
|
+
ctx.print(` ${marker}[${i + 1}] ${c.provider.padEnd(11)} ${aiProfileLabel(c)}${model}${flagStr}`);
|
|
37226
|
+
});
|
|
37227
|
+
if (pinned) {
|
|
37228
|
+
ctx.print("A profile is pinned \u2014 tried first every prompt; /ai-profile reset returns to auto.");
|
|
37229
|
+
}
|
|
37230
|
+
return;
|
|
36844
37231
|
}
|
|
36845
|
-
|
|
36846
|
-
|
|
36847
|
-
|
|
36848
|
-
|
|
37232
|
+
if (action === "reset" || action === "auto" || action === "clear") {
|
|
37233
|
+
clearPinnedCredential(dir2);
|
|
37234
|
+
ctx.print("\u2714 AI profile pin cleared \u2014 failover is back to automatic (config order / remembered).");
|
|
37235
|
+
const working = getWorkingCredential(dir2);
|
|
37236
|
+
ctx.setActiveProfile(working ? labelFromCredentialKey(working) : null);
|
|
37237
|
+
return;
|
|
36849
37238
|
}
|
|
36850
|
-
|
|
36851
|
-
|
|
36852
|
-
|
|
36853
|
-
|
|
36854
|
-
|
|
36855
|
-
|
|
36856
|
-
|
|
36857
|
-
|
|
37239
|
+
if (action === "use" || action === "switch") {
|
|
37240
|
+
const n = Number(rest[0]);
|
|
37241
|
+
if (!Number.isInteger(n) || n < 1 || n > list.length) {
|
|
37242
|
+
ctx.print(`usage: /ai-profile use <n> (n = 1-${list.length || "?"} from /ai-profile list)`, "error");
|
|
37243
|
+
return;
|
|
37244
|
+
}
|
|
37245
|
+
const cred = list[n - 1];
|
|
37246
|
+
if (!isUsableCredential(cred)) {
|
|
37247
|
+
const why = cred.enabled === false ? "it is disabled" : "it has no usable profile dir / provider";
|
|
37248
|
+
ctx.print(`Profile [${n}] "${aiProfileLabel(cred)}" can't be used \u2014 ${why}.`, "error");
|
|
37249
|
+
return;
|
|
37250
|
+
}
|
|
37251
|
+
const credDir = resolveHomePath(cred.profile);
|
|
37252
|
+
setPinnedCredential(dir2, credentialKey(cred.provider, credDir));
|
|
37253
|
+
ctx.setActiveProfile(profileDisplayLabel(credDir));
|
|
37254
|
+
ctx.print(
|
|
37255
|
+
`\u2714 switched to ${cred.provider} profile "${aiProfileLabel(cred)}" \u2014 tried first from the next prompt (failover keeps the rest as backup).`
|
|
37256
|
+
);
|
|
37257
|
+
return;
|
|
37258
|
+
}
|
|
37259
|
+
ctx.print(`Unknown /ai-profile subcommand "${sub}". Use: list | use <n> | reset.`, "error");
|
|
36858
37260
|
}
|
|
36859
|
-
|
|
36860
|
-
|
|
36861
|
-
|
|
36862
|
-
|
|
36863
|
-
|
|
36864
|
-
|
|
36865
|
-
|
|
36866
|
-
|
|
36867
|
-
|
|
36868
|
-
|
|
36869
|
-
|
|
36870
|
-
|
|
36871
|
-
})
|
|
36872
|
-
);
|
|
36873
|
-
const extras = await listExtras().catch(() => []);
|
|
36874
|
-
return { catalog, extras };
|
|
37261
|
+
function runFold(ctx, action) {
|
|
37262
|
+
if (ctx.maxBlock === 0) {
|
|
37263
|
+
ctx.print("No collapsible blocks on screen.", "warn");
|
|
37264
|
+
return;
|
|
37265
|
+
}
|
|
37266
|
+
const n = ctx.args[0] != null ? Number(ctx.args[0]) : ctx.maxBlock;
|
|
37267
|
+
if (!Number.isInteger(n) || n < 1 || n > ctx.maxBlock) {
|
|
37268
|
+
ctx.print(`usage: /${action} [1-${ctx.maxBlock}] (no number \u21D2 the newest block)`, "error");
|
|
37269
|
+
return;
|
|
37270
|
+
}
|
|
37271
|
+
if (action === "expand") ctx.expand(n);
|
|
37272
|
+
else ctx.collapse(n);
|
|
36875
37273
|
}
|
|
36876
|
-
|
|
36877
|
-
|
|
36878
|
-
|
|
36879
|
-
|
|
36880
|
-
|
|
37274
|
+
var COMMANDS = [
|
|
37275
|
+
{
|
|
37276
|
+
name: "help",
|
|
37277
|
+
usage: "/help",
|
|
37278
|
+
description: "List slash commands",
|
|
37279
|
+
run: (ctx) => {
|
|
37280
|
+
ctx.print("Slash commands:");
|
|
37281
|
+
for (const cmd of COMMANDS) ctx.print(` ${cmd.usage.padEnd(28)} ${cmd.description}`);
|
|
36881
37282
|
}
|
|
36882
|
-
|
|
37283
|
+
},
|
|
37284
|
+
{
|
|
37285
|
+
name: "version",
|
|
37286
|
+
usage: "/version",
|
|
37287
|
+
description: "Show the cli version",
|
|
37288
|
+
run: (ctx) => ctx.print(`4PM CLI v${ctx.info.version} \xB7 profile ${ctx.info.profile}`)
|
|
37289
|
+
},
|
|
37290
|
+
{
|
|
37291
|
+
name: "status",
|
|
37292
|
+
usage: "/status",
|
|
37293
|
+
description: "Show connection + profile + session status",
|
|
37294
|
+
run: (ctx) => {
|
|
37295
|
+
const s = ctx.live;
|
|
37296
|
+
const uptime = fmtDuration(Math.max(0, Math.floor((Date.now() - s.startedAt) / 1e3)));
|
|
37297
|
+
ctx.print(`connection: ${s.status}`);
|
|
37298
|
+
ctx.print(`scope: ${s.scope}`);
|
|
37299
|
+
ctx.print(`worker: ${s.worker ?? "(unknown)"}`);
|
|
37300
|
+
if (s.project) ctx.print(`project: ${s.project}`);
|
|
37301
|
+
ctx.print(`profile: ${ctx.info.profile}`);
|
|
37302
|
+
ctx.print(`server: ${ctx.info.serverUrl}`);
|
|
37303
|
+
ctx.print(`aiCli: ${ctx.info.aiCli}${s.activeProfile ? ` \xB7 profile ${s.activeProfile}` : ""}`);
|
|
37304
|
+
ctx.print(`serving: ${ctx.info.physicPath ?? "(none)"}`);
|
|
37305
|
+
ctx.print(`uptime: ${uptime} \xB7 commands: ${getCommandHistory().length}`);
|
|
37306
|
+
}
|
|
37307
|
+
},
|
|
37308
|
+
{
|
|
37309
|
+
name: "history",
|
|
37310
|
+
usage: "/history [N]",
|
|
37311
|
+
description: "List the last N executed commands (+ time)",
|
|
37312
|
+
run: runHistory
|
|
37313
|
+
},
|
|
37314
|
+
{
|
|
37315
|
+
name: "output",
|
|
37316
|
+
usage: "/output <n>",
|
|
37317
|
+
description: "Replay a command's output (n from /history)",
|
|
37318
|
+
run: runOutput
|
|
37319
|
+
},
|
|
37320
|
+
{
|
|
37321
|
+
name: "logs",
|
|
37322
|
+
usage: "/logs [N]",
|
|
37323
|
+
description: "Tail the profile's structured log",
|
|
37324
|
+
run: runLogs
|
|
37325
|
+
},
|
|
37326
|
+
{
|
|
37327
|
+
name: "expand",
|
|
37328
|
+
usage: "/expand [N]",
|
|
37329
|
+
description: "Expand a collapsed \u25B8[N] block (default: newest); pretty-prints json/code",
|
|
37330
|
+
run: (ctx) => runFold(ctx, "expand")
|
|
37331
|
+
},
|
|
37332
|
+
{
|
|
37333
|
+
name: "collapse",
|
|
37334
|
+
usage: "/collapse [N]",
|
|
37335
|
+
description: "Collapse an expanded \u25B8[N] block (default: newest)",
|
|
37336
|
+
run: (ctx) => runFold(ctx, "collapse")
|
|
37337
|
+
},
|
|
37338
|
+
{
|
|
37339
|
+
name: "reconnect",
|
|
37340
|
+
usage: "/reconnect",
|
|
37341
|
+
description: "Reconnect to the server now (skip the backoff)",
|
|
37342
|
+
run: (ctx) => ctx.reconnect()
|
|
37343
|
+
},
|
|
37344
|
+
{
|
|
37345
|
+
name: "whoami",
|
|
37346
|
+
usage: "/whoami",
|
|
37347
|
+
description: "Show this machine account + its teams & projects",
|
|
37348
|
+
run: runWhoami
|
|
37349
|
+
},
|
|
37350
|
+
{
|
|
37351
|
+
name: "ai-profile",
|
|
37352
|
+
usage: "/ai-profile [list|use <n>|reset]",
|
|
37353
|
+
description: "List AI profiles / switch which one runs (ADR-0250)",
|
|
37354
|
+
run: runAiProfile
|
|
37355
|
+
},
|
|
37356
|
+
{
|
|
37357
|
+
name: "config",
|
|
37358
|
+
usage: "/config [show|init|set <k> <v>|delete <k>]",
|
|
37359
|
+
description: "View / init / update / delete profile config",
|
|
37360
|
+
run: runConfig
|
|
37361
|
+
},
|
|
37362
|
+
{
|
|
37363
|
+
name: "claude-cmd",
|
|
37364
|
+
usage: "/claude-cmd /context",
|
|
37365
|
+
description: "Run an AI-CLI slash command (e.g. /context, /usage, /status)",
|
|
37366
|
+
run: (ctx) => {
|
|
37367
|
+
const rest = ctx.args.join(" ").trim();
|
|
37368
|
+
if (!rest) {
|
|
37369
|
+
ctx.print("usage: /claude-cmd /context (forwards to the AI CLI)", "error");
|
|
37370
|
+
return;
|
|
37371
|
+
}
|
|
37372
|
+
ctx.submitAi(rest);
|
|
37373
|
+
}
|
|
37374
|
+
},
|
|
37375
|
+
{
|
|
37376
|
+
name: "clear",
|
|
37377
|
+
usage: "/clear",
|
|
37378
|
+
description: "Clear the transcript",
|
|
37379
|
+
run: (ctx) => ctx.clear()
|
|
37380
|
+
},
|
|
37381
|
+
{
|
|
37382
|
+
name: "quit",
|
|
37383
|
+
usage: "/quit",
|
|
37384
|
+
description: "Quit the cli (also /exit, Ctrl+C)",
|
|
37385
|
+
run: (ctx) => ctx.quit()
|
|
36883
37386
|
}
|
|
36884
|
-
|
|
36885
|
-
|
|
37387
|
+
];
|
|
37388
|
+
var ALIASES = { exit: "quit" };
|
|
37389
|
+
var SLASH_COMMANDS = COMMANDS.map((c) => ({ name: c.name, usage: c.usage, description: c.description }));
|
|
37390
|
+
function runSlashCommand(line, base) {
|
|
37391
|
+
const tokens = line.replace(/^\//, "").trim().split(/\s+/);
|
|
37392
|
+
const rawName = tokens[0] ?? "";
|
|
37393
|
+
const name = ALIASES[rawName] ?? rawName;
|
|
37394
|
+
const command = COMMANDS.find((c) => c.name === name);
|
|
37395
|
+
const ctx = { ...base, args: tokens.slice(1) };
|
|
37396
|
+
if (!command) {
|
|
37397
|
+
ctx.print(`Unknown command: /${rawName} \u2014 try /help`, "error");
|
|
37398
|
+
return;
|
|
36886
37399
|
}
|
|
36887
|
-
|
|
36888
|
-
|
|
37400
|
+
command.run(ctx);
|
|
37401
|
+
}
|
|
37402
|
+
|
|
37403
|
+
// src/core/memory-compact.ts
|
|
37404
|
+
import { spawn as spawn9 } from "child_process";
|
|
37405
|
+
var COMPACT_TIMEOUT_MS = 6e4;
|
|
37406
|
+
function buildPrompt3(input) {
|
|
37407
|
+
return [
|
|
37408
|
+
"You maintain a COMPACT running memory of an ongoing assistant conversation so it can continue",
|
|
37409
|
+
"across sessions and accounts. Merge the latest exchange into the existing memory and return the",
|
|
37410
|
+
`UPDATED memory. Keep it UNDER ${input.budgetChars} characters. Use terse Markdown bullets grouped`,
|
|
37411
|
+
"as: Decisions, Constraints, Open tasks, Glossary. Keep durable facts; drop chit-chat and anything",
|
|
37412
|
+
"now obsolete. Output ONLY the updated memory text \u2014 no preamble, no code fences.",
|
|
37413
|
+
"",
|
|
37414
|
+
"=== EXISTING MEMORY ===",
|
|
37415
|
+
input.oldMemory || "(empty)",
|
|
37416
|
+
"",
|
|
37417
|
+
"=== LATEST USER PROMPT ===",
|
|
37418
|
+
input.prompt,
|
|
37419
|
+
"",
|
|
37420
|
+
"=== LATEST ASSISTANT ANSWER ===",
|
|
37421
|
+
input.answer
|
|
37422
|
+
].join("\n");
|
|
37423
|
+
}
|
|
37424
|
+
function runOnce2(cmd, profile, prompt, cwd2, extraEnv) {
|
|
37425
|
+
return new Promise((resolve8, reject) => {
|
|
37426
|
+
const args = ["-p", ...profile?.model ? ["--model", profile.model] : []];
|
|
37427
|
+
const env3 = {
|
|
37428
|
+
...process.env,
|
|
37429
|
+
...extraEnv,
|
|
37430
|
+
...profile ? { CLAUDE_CONFIG_DIR: profile.dir } : {}
|
|
37431
|
+
};
|
|
37432
|
+
const child = spawn9(cmd, args, { stdio: ["pipe", "pipe", "pipe"], cwd: cwd2, env: env3 });
|
|
37433
|
+
let out = "";
|
|
37434
|
+
let err = "";
|
|
37435
|
+
const timer = setTimeout(() => {
|
|
37436
|
+
child.kill("SIGKILL");
|
|
37437
|
+
reject(new Error("memory compaction timed out"));
|
|
37438
|
+
}, COMPACT_TIMEOUT_MS);
|
|
37439
|
+
timer.unref();
|
|
37440
|
+
child.stdout.on("data", (d) => out += d.toString());
|
|
37441
|
+
child.stderr.on("data", (d) => err += d.toString());
|
|
37442
|
+
child.on("error", (e) => {
|
|
37443
|
+
clearTimeout(timer);
|
|
37444
|
+
reject(e);
|
|
37445
|
+
});
|
|
37446
|
+
child.on("close", (code) => {
|
|
37447
|
+
clearTimeout(timer);
|
|
37448
|
+
resolve8({ code: code ?? -1, out, err });
|
|
37449
|
+
});
|
|
37450
|
+
child.stdin.write(prompt);
|
|
37451
|
+
child.stdin.end();
|
|
37452
|
+
});
|
|
37453
|
+
}
|
|
37454
|
+
async function runMemoryCompaction(ai, cwd2, input) {
|
|
37455
|
+
const prompt = buildPrompt3(input);
|
|
37456
|
+
const attempts = ai.profiles.length > 0 ? ai.profiles : [null];
|
|
37457
|
+
for (let i = 0; i < attempts.length; i++) {
|
|
37458
|
+
try {
|
|
37459
|
+
const { code, out } = await runOnce2(ai.cmd, attempts[i], prompt, cwd2, ai.env);
|
|
37460
|
+
if (code === 0 && out.trim()) {
|
|
37461
|
+
reportToolResult(ai.cmd, true);
|
|
37462
|
+
return out.trim().slice(0, input.budgetChars);
|
|
37463
|
+
}
|
|
37464
|
+
if (i === attempts.length - 1) break;
|
|
37465
|
+
} catch {
|
|
37466
|
+
break;
|
|
37467
|
+
}
|
|
36889
37468
|
}
|
|
36890
|
-
|
|
36891
|
-
return { pkg: name };
|
|
37469
|
+
return "";
|
|
36892
37470
|
}
|
|
36893
|
-
|
|
36894
|
-
|
|
36895
|
-
|
|
37471
|
+
|
|
37472
|
+
// src/core/config-sync.ts
|
|
37473
|
+
import { existsSync as existsSync12, readFileSync as readFileSync15 } from "fs";
|
|
37474
|
+
import { join as join24 } from "path";
|
|
37475
|
+
var SERVER_MANAGED_KEYS = [
|
|
37476
|
+
"physicPath",
|
|
37477
|
+
"sessionSwitchPct",
|
|
37478
|
+
"perPromptTokenLimit",
|
|
37479
|
+
// Project AI-run timeout override mirror (ADR-0243) — the ws_token refreshes it; a Worker-config
|
|
37480
|
+
// edit must not fight it. NOTE: the machine-user's own `aiRunTimeoutSec` is NOT here (operator-editable).
|
|
37481
|
+
"projectAiRunTimeoutSec",
|
|
37482
|
+
// Project idle auto-clear override mirror (ADR-0244) — same rule; the machine-user's own
|
|
37483
|
+
// `autoClearIdleMinutes` is operator-editable and NOT listed here.
|
|
37484
|
+
"projectAutoClearIdleMinutes",
|
|
37485
|
+
// Project shared-AI-memory override mirrors (ADR-0245) — the machine-user's own `aiMemoryEnabled`
|
|
37486
|
+
// / `aiMemoryBudgetChars` are operator-editable and NOT listed here.
|
|
37487
|
+
"projectAiMemoryMode",
|
|
37488
|
+
"projectAiMemoryBudgetChars"
|
|
37489
|
+
];
|
|
37490
|
+
function readConfigText(profileDir2) {
|
|
37491
|
+
const path = join24(profileDir2, "config.json");
|
|
37492
|
+
if (existsSync12(path)) {
|
|
37493
|
+
try {
|
|
37494
|
+
return readFileSync15(path, "utf8");
|
|
37495
|
+
} catch {
|
|
37496
|
+
}
|
|
37497
|
+
}
|
|
37498
|
+
return JSON.stringify(defaultProfileConfig(), null, 2);
|
|
36896
37499
|
}
|
|
36897
|
-
|
|
36898
|
-
|
|
36899
|
-
|
|
36900
|
-
|
|
37500
|
+
function applyConfigText(profileDir2, text) {
|
|
37501
|
+
let parsed;
|
|
37502
|
+
try {
|
|
37503
|
+
parsed = JSON.parse(text);
|
|
37504
|
+
} catch (err) {
|
|
37505
|
+
return { ok: false, error: `Invalid JSON: ${err.message}` };
|
|
36901
37506
|
}
|
|
36902
|
-
|
|
36903
|
-
|
|
36904
|
-
|
|
37507
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
37508
|
+
return { ok: false, error: "config.json must be a JSON object." };
|
|
37509
|
+
}
|
|
37510
|
+
const next = { ...parsed };
|
|
37511
|
+
const current = readProfileConfig(profileDir2);
|
|
37512
|
+
for (const key of SERVER_MANAGED_KEYS) {
|
|
37513
|
+
if (current[key] !== void 0) next[key] = current[key];
|
|
37514
|
+
else delete next[key];
|
|
37515
|
+
}
|
|
37516
|
+
overwriteProfileConfig(profileDir2, next);
|
|
37517
|
+
return { ok: true };
|
|
36905
37518
|
}
|
|
36906
37519
|
|
|
36907
37520
|
// src/core/ws-client.ts
|
|
@@ -36927,7 +37540,16 @@ var CONSOLE_WATCH_LEASE_MS = 45e3;
|
|
|
36927
37540
|
var METRICS_SAMPLE_INTERVAL_MS = 5e3;
|
|
36928
37541
|
var METRICS_WATCH_LEASE_MS = 45e3;
|
|
36929
37542
|
function toDtoEntry(e) {
|
|
36930
|
-
return {
|
|
37543
|
+
return {
|
|
37544
|
+
id: e.id,
|
|
37545
|
+
source: e.source,
|
|
37546
|
+
kind: e.kind,
|
|
37547
|
+
text: e.text,
|
|
37548
|
+
level: e.level,
|
|
37549
|
+
resultKind: e.resultKind,
|
|
37550
|
+
// Processing time on a terminal `exit` entry (ADR-0249) — the web renders it inline.
|
|
37551
|
+
...e.durationMs != null ? { durationMs: e.durationMs } : {}
|
|
37552
|
+
};
|
|
36931
37553
|
}
|
|
36932
37554
|
var WsClient = class {
|
|
36933
37555
|
constructor(context) {
|
|
@@ -36992,8 +37614,13 @@ var WsClient = class {
|
|
|
36992
37614
|
reconnectEntryId = null;
|
|
36993
37615
|
/** Org-configured reconnect cap (seconds) from the last ws_token — overrides env. */
|
|
36994
37616
|
orgReconnectMaxSec = null;
|
|
37617
|
+
/** Org timezone (IANA) from the last ws_token (ADR-0132) — a rented worker follows its renter's
|
|
37618
|
+
* org; used to stamp the AI-run start time on the `aireq` transcript marker (ADR-0249). */
|
|
37619
|
+
orgTimezone = "UTC";
|
|
36995
37620
|
/** Env override for the reconnect cap (seconds); resolved once at construction. */
|
|
36996
37621
|
envReconnectMaxSec = envReconnectMaxSec();
|
|
37622
|
+
/** Process start (ms) — uptime for a web-dispatched `/status` (ADR-0249). */
|
|
37623
|
+
startedAtMs = Date.now();
|
|
36997
37624
|
credential;
|
|
36998
37625
|
/** The physic project folder root this cli serves (null = no project) — fs.list is
|
|
36999
37626
|
* scoped to it so the web FsPicker can only browse inward, never out. */
|
|
@@ -37174,6 +37801,9 @@ var WsClient = class {
|
|
|
37174
37801
|
if (typeof token.reconnectMaxBackoffSec === "number") {
|
|
37175
37802
|
this.orgReconnectMaxSec = token.reconnectMaxBackoffSec;
|
|
37176
37803
|
}
|
|
37804
|
+
if (typeof token.timezone === "string" && token.timezone) {
|
|
37805
|
+
this.orgTimezone = token.timezone;
|
|
37806
|
+
}
|
|
37177
37807
|
if (typeof token.autoUpdateDaily === "boolean") {
|
|
37178
37808
|
this.updateScheduler.configure({
|
|
37179
37809
|
autoUpdateDaily: token.autoUpdateDaily,
|
|
@@ -37351,8 +37981,8 @@ var WsClient = class {
|
|
|
37351
37981
|
const oldRoot = this.physicFolderPath(sync.oldName);
|
|
37352
37982
|
if (oldRoot) void uninstallCron(oldRoot).catch(() => void 0);
|
|
37353
37983
|
}
|
|
37354
|
-
if (sync.oldName) rmSync4(
|
|
37355
|
-
mkdirSync5(
|
|
37984
|
+
if (sync.oldName) rmSync4(join25(this.context.profileDir, sync.oldName), { recursive: true, force: true });
|
|
37985
|
+
mkdirSync5(join25(this.context.profileDir, sync.newName), { recursive: true });
|
|
37356
37986
|
this.physicRoot = this.physicFolderPath(sync.newName);
|
|
37357
37987
|
this.bus.setProject(sync.newName);
|
|
37358
37988
|
this.bus.log(`Physic project folder synced \u2192 ${sync.newName}`);
|
|
@@ -37363,7 +37993,7 @@ var WsClient = class {
|
|
|
37363
37993
|
if (del.name) {
|
|
37364
37994
|
const delRoot = this.physicFolderPath(del.name);
|
|
37365
37995
|
if (delRoot) void uninstallCron(delRoot).catch(() => void 0);
|
|
37366
|
-
rmSync4(
|
|
37996
|
+
rmSync4(join25(this.context.profileDir, del.name), { recursive: true, force: true });
|
|
37367
37997
|
this.bus.log(`Physic project folder deleted (project removed) \u2192 ${del.name}`);
|
|
37368
37998
|
}
|
|
37369
37999
|
void manageSshKey("delete", this.context.profileDir).catch(() => void 0);
|
|
@@ -37384,7 +38014,11 @@ var WsClient = class {
|
|
|
37384
38014
|
ai: dispatch.ai ?? false
|
|
37385
38015
|
});
|
|
37386
38016
|
if (dispatch.ai) {
|
|
37387
|
-
|
|
38017
|
+
if (dispatch.cmd.trimStart().startsWith("/")) {
|
|
38018
|
+
void this.runWebSlashCommand(dispatch.cmd, dispatch.commandId);
|
|
38019
|
+
} else {
|
|
38020
|
+
void this.runAiPrompt(dispatch.cmd, dispatch.commandId, "server", dispatch.aiOneShot ?? false);
|
|
38021
|
+
}
|
|
37388
38022
|
break;
|
|
37389
38023
|
}
|
|
37390
38024
|
recordCommand({
|
|
@@ -37399,6 +38033,7 @@ var WsClient = class {
|
|
|
37399
38033
|
text: `$ ${dispatch.cmd} ${dispatch.args.join(" ")}`.trimEnd()
|
|
37400
38034
|
});
|
|
37401
38035
|
this.bus.startBusy(dispatch.cmd);
|
|
38036
|
+
const cmdStartMs = Date.now();
|
|
37402
38037
|
void runCommand(dispatch, (out) => {
|
|
37403
38038
|
if (out.chunk) {
|
|
37404
38039
|
this.bus.push({ source: "server", kind: "out", text: out.chunk });
|
|
@@ -37413,7 +38048,8 @@ var WsClient = class {
|
|
|
37413
38048
|
source: "server",
|
|
37414
38049
|
kind: "exit",
|
|
37415
38050
|
text: code === 0 ? "\u2713 done" : `\u2717 failed (exit ${code})`,
|
|
37416
|
-
level: code === 0 ? "info" : "error"
|
|
38051
|
+
level: code === 0 ? "info" : "error",
|
|
38052
|
+
durationMs: Date.now() - cmdStartMs
|
|
37417
38053
|
});
|
|
37418
38054
|
}
|
|
37419
38055
|
});
|
|
@@ -37667,14 +38303,21 @@ var WsClient = class {
|
|
|
37667
38303
|
break;
|
|
37668
38304
|
}
|
|
37669
38305
|
case WsChannels.TOOLS_LIST:
|
|
37670
|
-
void detectWorkerTools().then(
|
|
38306
|
+
void detectWorkerTools(readProfileConfig(this.context.profileDir).autoUpdateTools ?? []).then(
|
|
37671
38307
|
(reply) => this.send(WsChannels.TOOLS_LIST, reply, message.id)
|
|
37672
38308
|
);
|
|
37673
38309
|
break;
|
|
38310
|
+
case WsChannels.TOOLS_AUTOUPDATE: {
|
|
38311
|
+
const req = payload;
|
|
38312
|
+
const res = setToolAutoUpdate(this.context.profileDir, req.name, req.enabled);
|
|
38313
|
+
this.send(WsChannels.TOOLS_AUTOUPDATE, res, message.id);
|
|
38314
|
+
break;
|
|
38315
|
+
}
|
|
37674
38316
|
case WsChannels.TOOLS_INSTALL:
|
|
37675
|
-
case WsChannels.TOOLS_UNINSTALL:
|
|
38317
|
+
case WsChannels.TOOLS_UNINSTALL:
|
|
38318
|
+
case WsChannels.TOOLS_UPDATE: {
|
|
37676
38319
|
const req = payload;
|
|
37677
|
-
const op = message.channel === WsChannels.TOOLS_INSTALL ? "install" : "uninstall";
|
|
38320
|
+
const op = message.channel === WsChannels.TOOLS_INSTALL ? "install" : message.channel === WsChannels.TOOLS_UPDATE ? "update" : "uninstall";
|
|
37678
38321
|
this.send(message.channel, { started: true }, message.id);
|
|
37679
38322
|
void runWorkerToolOp(
|
|
37680
38323
|
op,
|
|
@@ -37736,7 +38379,7 @@ var WsClient = class {
|
|
|
37736
38379
|
}
|
|
37737
38380
|
case WsChannels.LOG_READ: {
|
|
37738
38381
|
const limit = Math.min(Math.max(payload.limit ?? 200, 1), 2e3);
|
|
37739
|
-
const lines = readRecentLogLines(
|
|
38382
|
+
const lines = readRecentLogLines(join25(this.context.profileDir, "logs"), limit);
|
|
37740
38383
|
this.send(WsChannels.LOG_READ, { lines }, message.id);
|
|
37741
38384
|
break;
|
|
37742
38385
|
}
|
|
@@ -37815,7 +38458,7 @@ var WsClient = class {
|
|
|
37815
38458
|
* exactly like typing the prompt in the cli (tries profiles, meters real tokens) instead
|
|
37816
38459
|
* of spawning the text as a raw command. Streams output to the transcript AND the server.
|
|
37817
38460
|
*/
|
|
37818
|
-
async runAiPrompt(prompt, commandId, origin) {
|
|
38461
|
+
async runAiPrompt(prompt, commandId, origin, oneShot = false) {
|
|
37819
38462
|
const config = readProfileConfig(this.context.profileDir);
|
|
37820
38463
|
const projectTimeoutSec = config.projectAiRunTimeoutSec ?? 0;
|
|
37821
38464
|
const machineTimeoutSec = config.aiRunTimeoutSec ?? 0;
|
|
@@ -37855,9 +38498,10 @@ var WsClient = class {
|
|
|
37855
38498
|
const unified = isUnifiedConfig(config);
|
|
37856
38499
|
const mode = config.aiFailoverMode ?? "remember";
|
|
37857
38500
|
const workingDir = unified ? null : getWorkingProfile(this.context.profileDir, cmd);
|
|
37858
|
-
const
|
|
38501
|
+
const pinnedCred = unified ? getPinnedCredential(this.context.profileDir) : null;
|
|
38502
|
+
const workingCred = unified ? pinnedCred ?? (mode === "remember" ? getWorkingCredential(this.context.profileDir) : null) : null;
|
|
37859
38503
|
const startDir = unified ? null : this.profileUnderSessionPressure(config, cmd, workingDir);
|
|
37860
|
-
const startCred = unified ? this.credentialUnderSessionPressure(config, workingCred) : null;
|
|
38504
|
+
const startCred = unified && !pinnedCred ? this.credentialUnderSessionPressure(config, workingCred) : null;
|
|
37861
38505
|
if (startDir && startDir !== workingDir) {
|
|
37862
38506
|
this.bus.push({
|
|
37863
38507
|
source: origin,
|
|
@@ -37887,9 +38531,10 @@ ${this.aiMemory}
|
|
|
37887
38531
|
|
|
37888
38532
|
${guardedPrompt}`;
|
|
37889
38533
|
}
|
|
37890
|
-
const plan = planAiRun(effectivePrompt, config, hint, resume);
|
|
38534
|
+
const plan = planAiRun(effectivePrompt, config, hint, resume, oneShot);
|
|
37891
38535
|
const markerArgs = plan.attempts[0]?.args ?? [];
|
|
37892
|
-
|
|
38536
|
+
const startStamp = formatTimestampInZone(/* @__PURE__ */ new Date(), this.orgTimezone);
|
|
38537
|
+
this.bus.push({ source: origin, kind: "aireq", text: `${startStamp} ${plan.cmd} \u2039 ${prompt}` });
|
|
37893
38538
|
logger.info("command.ai", { commandId, origin, cmd: plan.cmd, profiles: plan.attempts.length });
|
|
37894
38539
|
if (origin === "local") {
|
|
37895
38540
|
this.send(WsChannels.COMMAND_ANNOUNCE, {
|
|
@@ -37903,6 +38548,7 @@ ${guardedPrompt}`;
|
|
|
37903
38548
|
}
|
|
37904
38549
|
recordCommand({ commandId, cmd: plan.cmd, args: markerArgs });
|
|
37905
38550
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
38551
|
+
const aiStartedMs = Date.now();
|
|
37906
38552
|
this.bus.startBusy(plan.cmd);
|
|
37907
38553
|
let seq = 0;
|
|
37908
38554
|
let responseHeaderShown = false;
|
|
@@ -37946,7 +38592,7 @@ ${guardedPrompt}`;
|
|
|
37946
38592
|
`, log: true });
|
|
37947
38593
|
},
|
|
37948
38594
|
onAttemptFail: (label, reason) => {
|
|
37949
|
-
const text = reason === "limit" ? `profile "${label}" hit its session limit \u2014 trying next` : `profile "${label}" failed to authenticate \u2014 trying next`;
|
|
38595
|
+
const text = reason === "limit" ? `profile "${label}" hit its session limit \u2014 trying next` : reason === "credits" ? `profile "${label}" is out of usage credits \u2014 trying next` : `profile "${label}" failed to authenticate \u2014 trying next`;
|
|
37950
38596
|
this.bus.push({ source: origin, kind: "log", text, level: "warn" });
|
|
37951
38597
|
this.send(WsChannels.COMMAND_OUTPUT, { commandId, seq: seq++, chunk: `${text}
|
|
37952
38598
|
`, log: true });
|
|
@@ -38019,7 +38665,8 @@ ${guardedPrompt}`;
|
|
|
38019
38665
|
source: origin,
|
|
38020
38666
|
kind: "exit",
|
|
38021
38667
|
text: result.exitCode === 0 ? "\u2713 ready \u2014 enter your next prompt" : `\u2717 ${plan.cmd} failed (exit ${result.exitCode}) \u2014 see the error above`,
|
|
38022
|
-
level: result.exitCode === 0 ? "info" : "error"
|
|
38668
|
+
level: result.exitCode === 0 ? "info" : "error",
|
|
38669
|
+
durationMs: Date.now() - aiStartedMs
|
|
38023
38670
|
});
|
|
38024
38671
|
if (memCfg.enabled && result.exitCode === 0) {
|
|
38025
38672
|
if (result.workedKey && result.sessionId) {
|
|
@@ -38029,6 +38676,116 @@ ${guardedPrompt}`;
|
|
|
38029
38676
|
});
|
|
38030
38677
|
}
|
|
38031
38678
|
}
|
|
38679
|
+
/**
|
|
38680
|
+
* Run a `/…` 4pm-cli slash command dispatched from the web Console (ADR-0249). Mirrors the TUI's
|
|
38681
|
+
* `runSlashCommand` with a **server-origin** context that streams `print` output back over
|
|
38682
|
+
* command.output + console.sync, so a web user gets the same commands as an operator at the
|
|
38683
|
+
* machine — EXCEPT any the operator disabled via `webBlockedCommands`. `/claude-cmd <x>` forwards
|
|
38684
|
+
* `x` to the AI CLI (a real AI run). Fold ops (`/expand`/`/collapse`) are TUI-only — the web has
|
|
38685
|
+
* its own fold viewer — so they just print a hint. Interactive `confirm` (e.g. `/config init`) is
|
|
38686
|
+
* not supported from the web.
|
|
38687
|
+
*/
|
|
38688
|
+
async runWebSlashCommand(line, commandId) {
|
|
38689
|
+
const origin = "server";
|
|
38690
|
+
const config = readProfileConfig(this.context.profileDir);
|
|
38691
|
+
const trimmed = line.trim();
|
|
38692
|
+
const startedMs = Date.now();
|
|
38693
|
+
this.bus.push({ source: origin, kind: "cmd", text: trimmed });
|
|
38694
|
+
recordCommand({ commandId, cmd: trimmed, args: [] });
|
|
38695
|
+
let seq = 0;
|
|
38696
|
+
const emit = (text) => {
|
|
38697
|
+
appendCommandOutput(commandId, `${text}
|
|
38698
|
+
`);
|
|
38699
|
+
this.send(WsChannels.COMMAND_OUTPUT, { commandId, seq: seq++, chunk: `${text}
|
|
38700
|
+
` });
|
|
38701
|
+
};
|
|
38702
|
+
const settle = (exitCode) => {
|
|
38703
|
+
this.send(WsChannels.COMMAND_OUTPUT, { commandId, seq: seq++, chunk: "", done: true, exitCode });
|
|
38704
|
+
finishCommand(commandId, exitCode);
|
|
38705
|
+
this.send(WsChannels.COMMAND_HISTORY, {
|
|
38706
|
+
commandId,
|
|
38707
|
+
cmd: trimmed,
|
|
38708
|
+
args: [],
|
|
38709
|
+
status: exitCode === 0 ? "done" : "failed",
|
|
38710
|
+
exitCode
|
|
38711
|
+
});
|
|
38712
|
+
this.bus.push({
|
|
38713
|
+
source: origin,
|
|
38714
|
+
kind: "exit",
|
|
38715
|
+
text: exitCode === 0 ? "\u2713 done" : `\u2717 failed (exit ${exitCode})`,
|
|
38716
|
+
level: exitCode === 0 ? "info" : "error",
|
|
38717
|
+
durationMs: Date.now() - startedMs
|
|
38718
|
+
});
|
|
38719
|
+
};
|
|
38720
|
+
const rawName = trimmed.replace(/^\//, "").trim().split(/\s+/)[0]?.toLowerCase() ?? "";
|
|
38721
|
+
const name = rawName === "exit" ? "quit" : rawName;
|
|
38722
|
+
const blocked = resolveWebBlockedCommands(config);
|
|
38723
|
+
if (blocked.has(name) || blocked.has(rawName)) {
|
|
38724
|
+
const msg = `Command /${rawName} is disabled on this worker (blocked by the machine config).`;
|
|
38725
|
+
this.bus.push({ source: origin, kind: "log", text: msg, level: "error" });
|
|
38726
|
+
emit(msg);
|
|
38727
|
+
settle(1);
|
|
38728
|
+
return;
|
|
38729
|
+
}
|
|
38730
|
+
if (name === "claude-cmd") {
|
|
38731
|
+
const rest = trimmed.replace(/^\/claude-cmd\s*/i, "").trim();
|
|
38732
|
+
if (!rest) {
|
|
38733
|
+
emit("usage: /claude-cmd /context (forwards to the AI CLI)");
|
|
38734
|
+
settle(1);
|
|
38735
|
+
return;
|
|
38736
|
+
}
|
|
38737
|
+
await this.runAiPrompt(rest, commandId, origin, false);
|
|
38738
|
+
return;
|
|
38739
|
+
}
|
|
38740
|
+
const info = {
|
|
38741
|
+
version: CLI_VERSION,
|
|
38742
|
+
scope: this.bus.scope ?? "project",
|
|
38743
|
+
profile: basename4(this.context.profileDir),
|
|
38744
|
+
profileDir: this.context.profileDir,
|
|
38745
|
+
serverUrl: this.context.credential.serverUrl,
|
|
38746
|
+
physicPath: this.physicRoot ?? config.physicPath ?? null,
|
|
38747
|
+
aiCli: config.aiCli || "claude",
|
|
38748
|
+
whoami: async () => null
|
|
38749
|
+
};
|
|
38750
|
+
runSlashCommand(trimmed, {
|
|
38751
|
+
info,
|
|
38752
|
+
print: (text, level) => {
|
|
38753
|
+
this.bus.push({ source: origin, kind: "log", text, level });
|
|
38754
|
+
emit(text);
|
|
38755
|
+
},
|
|
38756
|
+
clear: () => {
|
|
38757
|
+
this.bus.clear();
|
|
38758
|
+
this.bus.clearSession();
|
|
38759
|
+
},
|
|
38760
|
+
quit: () => {
|
|
38761
|
+
emit("Quit requested from the web console \u2014 the worker cli is shutting down.");
|
|
38762
|
+
setTimeout(() => process.exit(0), 200);
|
|
38763
|
+
},
|
|
38764
|
+
confirm: (prompt) => {
|
|
38765
|
+
const note = `${prompt} \u2014 interactive prompts aren't supported from the web console; run it on the machine.`;
|
|
38766
|
+
this.bus.push({ source: origin, kind: "log", text: note, level: "warn" });
|
|
38767
|
+
emit(note);
|
|
38768
|
+
},
|
|
38769
|
+
submitAi: () => {
|
|
38770
|
+
},
|
|
38771
|
+
// only /claude-cmd uses this, and it is special-cased above
|
|
38772
|
+
// /ai-profile use/reset from the web Console updates the shared header too (ADR-0250).
|
|
38773
|
+
setActiveProfile: (label) => this.bus.setActiveProfile(label),
|
|
38774
|
+
reconnect: () => this.reconnectNow(),
|
|
38775
|
+
expand: () => emit("Use the web console's fold viewer to expand blocks."),
|
|
38776
|
+
collapse: () => emit("Use the web console's fold viewer to collapse blocks."),
|
|
38777
|
+
maxBlock: 0,
|
|
38778
|
+
live: {
|
|
38779
|
+
status: this.bus.status,
|
|
38780
|
+
scope: this.bus.scope ?? "",
|
|
38781
|
+
worker: this.bus.worker,
|
|
38782
|
+
project: this.bus.project,
|
|
38783
|
+
activeProfile: this.bus.activeProfile,
|
|
38784
|
+
startedAt: this.startedAtMs
|
|
38785
|
+
}
|
|
38786
|
+
});
|
|
38787
|
+
settle(0);
|
|
38788
|
+
}
|
|
38032
38789
|
/**
|
|
38033
38790
|
* Fold the latest exchange into the shared AI memory (ADR-0245): a background claude compaction to a
|
|
38034
38791
|
* budget-bounded summary, cached locally + written back to the server (`memory.update`). Best-effort —
|
|
@@ -38380,7 +39137,7 @@ ${guardedPrompt}`;
|
|
|
38380
39137
|
/** Create the physic folder at an absolute path if missing (best-effort). */
|
|
38381
39138
|
ensurePhysicFolderPath(folder) {
|
|
38382
39139
|
try {
|
|
38383
|
-
if (!
|
|
39140
|
+
if (!existsSync13(folder)) {
|
|
38384
39141
|
mkdirSync5(folder, { recursive: true });
|
|
38385
39142
|
this.bus.log(`Created physic project folder: ${folder}`);
|
|
38386
39143
|
}
|
|
@@ -38395,7 +39152,7 @@ ${guardedPrompt}`;
|
|
|
38395
39152
|
*/
|
|
38396
39153
|
physicFolderPath(projectName) {
|
|
38397
39154
|
const safe = projectName.replace(/[/\\]/g, "_").replace(/\.\./g, "_").trim();
|
|
38398
|
-
return safe ?
|
|
39155
|
+
return safe ? join25(this.context.profileDir, safe) : null;
|
|
38399
39156
|
}
|
|
38400
39157
|
/**
|
|
38401
39158
|
* Report machine.status: version + the state of the physic project being served
|
|
@@ -38409,7 +39166,7 @@ ${guardedPrompt}`;
|
|
|
38409
39166
|
projects: [],
|
|
38410
39167
|
version: CLI_VERSION,
|
|
38411
39168
|
physicPath,
|
|
38412
|
-
physicPathExists: physicPath ?
|
|
39169
|
+
physicPathExists: physicPath ? existsSync13(physicPath) : void 0,
|
|
38413
39170
|
// Real coarse state (ADR-0152): !paused and a recent tick (was hard-coded false).
|
|
38414
39171
|
autonomousRunning: physicPath ? isAutonomousRunning(physicPath) : false
|
|
38415
39172
|
});
|
|
@@ -38499,7 +39256,7 @@ ${guardedPrompt}`;
|
|
|
38499
39256
|
*/
|
|
38500
39257
|
uploadLog() {
|
|
38501
39258
|
try {
|
|
38502
|
-
const upload = readLogUpload(
|
|
39259
|
+
const upload = readLogUpload(join25(this.context.profileDir, "logs"));
|
|
38503
39260
|
if (!upload) return;
|
|
38504
39261
|
const sig = `${upload.fileName}:${upload.totalBytes}`;
|
|
38505
39262
|
if (sig === this.lastLogSig) return;
|
|
@@ -38528,9 +39285,9 @@ ${guardedPrompt}`;
|
|
|
38528
39285
|
|
|
38529
39286
|
// src/core/instance-lock.ts
|
|
38530
39287
|
import { readFileSync as readFileSync16, rmSync as rmSync5, writeFileSync as writeFileSync7 } from "fs";
|
|
38531
|
-
import { join as
|
|
39288
|
+
import { join as join26 } from "path";
|
|
38532
39289
|
function lockPath(profileDir2) {
|
|
38533
|
-
return
|
|
39290
|
+
return join26(profileDir2, "start.lock");
|
|
38534
39291
|
}
|
|
38535
39292
|
function isAlive(pid) {
|
|
38536
39293
|
try {
|
|
@@ -38830,7 +39587,7 @@ var SessionBus = class {
|
|
|
38830
39587
|
// src/core/control-server.ts
|
|
38831
39588
|
import { createServer } from "net";
|
|
38832
39589
|
import { rmSync as rmSync6 } from "fs";
|
|
38833
|
-
import { join as
|
|
39590
|
+
import { join as join27 } from "path";
|
|
38834
39591
|
|
|
38835
39592
|
// src/core/control-protocol.ts
|
|
38836
39593
|
var CONTROL_SOCKET_FILE = "control.sock";
|
|
@@ -38859,7 +39616,7 @@ function createFrameParser() {
|
|
|
38859
39616
|
|
|
38860
39617
|
// src/core/control-server.ts
|
|
38861
39618
|
function startControlServer(bus, profileDir2, info) {
|
|
38862
|
-
const socketPath =
|
|
39619
|
+
const socketPath = join27(profileDir2, CONTROL_SOCKET_FILE);
|
|
38863
39620
|
try {
|
|
38864
39621
|
rmSync6(socketPath, { force: true });
|
|
38865
39622
|
} catch {
|
|
@@ -44590,15 +45347,15 @@ var import_react21 = __toESM(require_react(), 1);
|
|
|
44590
45347
|
var import_react26 = __toESM(require_react(), 1);
|
|
44591
45348
|
|
|
44592
45349
|
// src/core/input-history.ts
|
|
44593
|
-
import { existsSync as
|
|
44594
|
-
import { join as
|
|
45350
|
+
import { existsSync as existsSync15, readFileSync as readFileSync18, writeFileSync as writeFileSync8 } from "fs";
|
|
45351
|
+
import { join as join28 } from "path";
|
|
44595
45352
|
var MAX_HISTORY = 200;
|
|
44596
45353
|
function historyPath(profileDir2) {
|
|
44597
|
-
return
|
|
45354
|
+
return join28(profileDir2, "input-history.json");
|
|
44598
45355
|
}
|
|
44599
45356
|
function loadInputHistory(profileDir2) {
|
|
44600
45357
|
const path = historyPath(profileDir2);
|
|
44601
|
-
if (!
|
|
45358
|
+
if (!existsSync15(path)) return [];
|
|
44602
45359
|
try {
|
|
44603
45360
|
const data = JSON.parse(readFileSync18(path, "utf8"));
|
|
44604
45361
|
return Array.isArray(data) ? data.filter((x) => typeof x === "string") : [];
|
|
@@ -44720,12 +45477,17 @@ function textColor(line) {
|
|
|
44720
45477
|
if (line.kind === "exit") return "green";
|
|
44721
45478
|
return void 0;
|
|
44722
45479
|
}
|
|
45480
|
+
var START_STAMP_RE = /^(\d{4}\/\d{2}\/\d{2} \d{2}:\d{2}:\d{2} )/;
|
|
44723
45481
|
function AiMarkerLine({ line }) {
|
|
44724
45482
|
const arrow = line.kind === "aireq" ? "\u2039" : "\u203A";
|
|
44725
45483
|
const idx = line.text.indexOf(arrow);
|
|
44726
45484
|
if (idx < 0) return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text, { children: line.text || " " });
|
|
45485
|
+
const before = line.text.slice(0, idx);
|
|
45486
|
+
const stamp = line.kind === "aireq" ? START_STAMP_RE.exec(before)?.[1] ?? "" : "";
|
|
45487
|
+
const name = stamp ? before.slice(stamp.length) : before;
|
|
44727
45488
|
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Text, { children: [
|
|
44728
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text, {
|
|
45489
|
+
stamp ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text, { dimColor: true, children: stamp }) : null,
|
|
45490
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text, { color: "cyan", bold: true, children: name }),
|
|
44729
45491
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text, { dimColor: true, children: arrow }),
|
|
44730
45492
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text, { children: line.text.slice(idx + 1) })
|
|
44731
45493
|
] });
|
|
@@ -44750,438 +45512,114 @@ function wrapToWidth(text, width) {
|
|
|
44750
45512
|
line = word.trimStart();
|
|
44751
45513
|
} else {
|
|
44752
45514
|
line += word;
|
|
44753
|
-
}
|
|
44754
|
-
}
|
|
44755
|
-
if (line) rows.push(line.trimEnd());
|
|
44756
|
-
return rows.length > 0 ? rows : [""];
|
|
44757
|
-
}
|
|
44758
|
-
var DETAIL_RE = /<detail\s+title="([^"]*)">([\s\S]*?)<\/detail>/g;
|
|
44759
|
-
function toSegments(raw) {
|
|
44760
|
-
if (!raw.includes("<detail")) {
|
|
44761
|
-
return raw.split("\n").map((text) => ({ text, collapsed: false }));
|
|
44762
|
-
}
|
|
44763
|
-
const out = [];
|
|
44764
|
-
const re = new RegExp(DETAIL_RE);
|
|
44765
|
-
let last = 0;
|
|
44766
|
-
let m;
|
|
44767
|
-
while ((m = re.exec(raw)) !== null) {
|
|
44768
|
-
const before = raw.slice(last, m.index).replace(/\n$/, "");
|
|
44769
|
-
if (before.length > 0) for (const p of before.split("\n")) out.push({ text: p, collapsed: false });
|
|
44770
|
-
out.push({ text: m[1] || "details", collapsed: true, body: m[2] ?? "" });
|
|
44771
|
-
last = m.index + m[0].length;
|
|
44772
|
-
if (raw[last] === "\n") last += 1;
|
|
44773
|
-
}
|
|
44774
|
-
const rest = raw.slice(last);
|
|
44775
|
-
if (rest.length > 0) for (const p of rest.split("\n")) out.push({ text: p, collapsed: false });
|
|
44776
|
-
if (out.length === 0) out.push({ text: "", collapsed: false });
|
|
44777
|
-
return out;
|
|
44778
|
-
}
|
|
44779
|
-
function pushCollapsible(lines, entry, opts) {
|
|
44780
|
-
const { blockNo, title, body, expanded, firstRow, textWidth, keyBase } = opts;
|
|
44781
|
-
const base = { source: entry.source, level: entry.level, kind: entry.kind };
|
|
44782
|
-
lines.push({
|
|
44783
|
-
...base,
|
|
44784
|
-
key: `${keyBase}:m`,
|
|
44785
|
-
text: `${expanded ? "\u25BE" : "\u25B8"}[${blockNo}] ${title}`,
|
|
44786
|
-
showTag: firstRow,
|
|
44787
|
-
collapsed: true,
|
|
44788
|
-
spacer: false,
|
|
44789
|
-
blockIndex: blockNo
|
|
44790
|
-
});
|
|
44791
|
-
if (!expanded) return;
|
|
44792
|
-
prettyResult(body).split("\n").forEach(
|
|
44793
|
-
(seg, i) => wrapToWidth(seg, textWidth).forEach(
|
|
44794
|
-
(row, j) => lines.push({ ...base, key: `${keyBase}:b:${i}:${j}`, text: row, showTag: false, collapsed: false, spacer: false })
|
|
44795
|
-
)
|
|
44796
|
-
);
|
|
44797
|
-
}
|
|
44798
|
-
function flattenEntries(entries2, textWidth, expanded = /* @__PURE__ */ new Set()) {
|
|
44799
|
-
const lines = [];
|
|
44800
|
-
let prevKey = null;
|
|
44801
|
-
let blockNo = 0;
|
|
44802
|
-
for (const entry of entries2) {
|
|
44803
|
-
const blockKey = `${entry.source}|${entry.kind}`;
|
|
44804
|
-
const newBlock = blockKey !== prevKey;
|
|
44805
|
-
if (newBlock && lines.length > 0) {
|
|
44806
|
-
lines.push({
|
|
44807
|
-
key: `sp:${entry.id}`,
|
|
44808
|
-
source: entry.source,
|
|
44809
|
-
level: entry.level,
|
|
44810
|
-
kind: entry.kind,
|
|
44811
|
-
text: "",
|
|
44812
|
-
showTag: false,
|
|
44813
|
-
collapsed: false,
|
|
44814
|
-
spacer: true
|
|
44815
|
-
});
|
|
44816
|
-
}
|
|
44817
|
-
prevKey = blockKey;
|
|
44818
|
-
let firstRow = newBlock;
|
|
44819
|
-
if (entry.kind === "result") {
|
|
44820
|
-
blockNo += 1;
|
|
44821
|
-
const count = prettyResult(entry.text).split("\n").length;
|
|
44822
|
-
const title = `result \xB7 ${entry.resultKind ?? "json"} \xB7 ${count} ${count === 1 ? "line" : "lines"}`;
|
|
44823
|
-
pushCollapsible(lines, entry, { blockNo, title, body: entry.text, expanded: expanded.has(blockNo), firstRow, textWidth, keyBase: entry.id });
|
|
44824
|
-
continue;
|
|
44825
|
-
}
|
|
44826
|
-
const raw = entry.kind === "out" ? entry.text.replace(/\n$/, "") : entry.text;
|
|
44827
|
-
toSegments(raw).forEach((seg, i) => {
|
|
44828
|
-
if (seg.collapsed) {
|
|
44829
|
-
blockNo += 1;
|
|
44830
|
-
pushCollapsible(lines, entry, { blockNo, title: seg.text, body: seg.body ?? "", expanded: expanded.has(blockNo), firstRow, textWidth, keyBase: `${entry.id}:${i}` });
|
|
44831
|
-
firstRow = false;
|
|
44832
|
-
return;
|
|
44833
|
-
}
|
|
44834
|
-
wrapToWidth(seg.text, textWidth).forEach((row, j) => {
|
|
44835
|
-
lines.push({
|
|
44836
|
-
key: `${entry.id}:${i}:${j}`,
|
|
44837
|
-
source: entry.source,
|
|
44838
|
-
level: entry.level,
|
|
44839
|
-
kind: entry.kind,
|
|
44840
|
-
text: row,
|
|
44841
|
-
showTag: firstRow,
|
|
44842
|
-
collapsed: false,
|
|
44843
|
-
spacer: false
|
|
44844
|
-
});
|
|
44845
|
-
firstRow = false;
|
|
44846
|
-
});
|
|
44847
|
-
});
|
|
44848
|
-
}
|
|
44849
|
-
return lines;
|
|
44850
|
-
}
|
|
44851
|
-
function TranscriptLine({ line }) {
|
|
44852
|
-
const tag = sourceTag(line.source);
|
|
44853
|
-
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Box_default, { flexDirection: "row", children: [
|
|
44854
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Box_default, { width: 7, marginRight: 1, children: line.showTag ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text, { color: tag.color, dimColor: line.source === "system", children: tag.text }) : null }),
|
|
44855
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Box_default, { flexGrow: 1, children: line.spacer ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text, { children: " " }) : line.collapsed ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text, { dimColor: true, wrap: "truncate-end", children: line.text || " " }) : line.kind === "aireq" || line.kind === "aires" ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(AiMarkerLine, { line }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text, { color: textColor(line), wrap: "truncate-end", children: line.text || " " }) })
|
|
44856
|
-
] });
|
|
44857
|
-
}
|
|
44858
|
-
|
|
44859
|
-
// src/ui/command-input.tsx
|
|
44860
|
-
var import_react24 = __toESM(require_react(), 1);
|
|
44861
|
-
|
|
44862
|
-
// src/ui/slash-commands.ts
|
|
44863
|
-
import { existsSync as existsSync15 } from "fs";
|
|
44864
|
-
import { join as join28 } from "path";
|
|
44865
|
-
var CONFIG_KEYS = [
|
|
44866
|
-
"aiCli",
|
|
44867
|
-
"claudeHome",
|
|
44868
|
-
"codexHome",
|
|
44869
|
-
"antigravityHome",
|
|
44870
|
-
"physicPath",
|
|
44871
|
-
"commandHistoryUploadMinutes",
|
|
44872
|
-
"autoUpdate",
|
|
44873
|
-
"aiEnv"
|
|
44874
|
-
];
|
|
44875
|
-
function configPath(profileDir2) {
|
|
44876
|
-
return join28(profileDir2, "config.json");
|
|
44877
|
-
}
|
|
44878
|
-
function parseConfigAssignment(key, valueTokens) {
|
|
44879
|
-
const value = valueTokens.join(" ").trim();
|
|
44880
|
-
switch (key) {
|
|
44881
|
-
case "aiCli":
|
|
44882
|
-
case "physicPath":
|
|
44883
|
-
if (!value) return { error: `Missing value for "${key}".` };
|
|
44884
|
-
return { patch: { [key]: value } };
|
|
44885
|
-
case "claudeHome":
|
|
44886
|
-
case "codexHome":
|
|
44887
|
-
case "antigravityHome": {
|
|
44888
|
-
const dirs = valueTokens.filter((t) => t.trim());
|
|
44889
|
-
if (dirs.length === 0) return { error: `Missing value for "${key}".` };
|
|
44890
|
-
return { patch: { [key]: dirs.map((profile) => ({ profile, model: "" })) } };
|
|
44891
|
-
}
|
|
44892
|
-
case "commandHistoryUploadMinutes": {
|
|
44893
|
-
const n = Number(value);
|
|
44894
|
-
if (!Number.isFinite(n) || n <= 0) return { error: `"${value}" is not a positive number.` };
|
|
44895
|
-
return { patch: { commandHistoryUploadMinutes: n } };
|
|
44896
|
-
}
|
|
44897
|
-
case "autoUpdate":
|
|
44898
|
-
if (value !== "true" && value !== "false") return { error: `Use true|false, not "${value}".` };
|
|
44899
|
-
return { patch: { autoUpdate: value === "true" } };
|
|
44900
|
-
case "aiEnv":
|
|
44901
|
-
try {
|
|
44902
|
-
return { patch: { aiEnv: JSON.parse(value) } };
|
|
44903
|
-
} catch {
|
|
44904
|
-
return { error: `aiEnv must be JSON, e.g. {"KEY":"val"}.` };
|
|
44905
|
-
}
|
|
44906
|
-
default:
|
|
44907
|
-
return { error: `Unknown key "${key}". Allowed: ${CONFIG_KEYS.join(", ")}.` };
|
|
44908
|
-
}
|
|
44909
|
-
}
|
|
44910
|
-
function runConfig(ctx) {
|
|
44911
|
-
const [sub, ...rest] = ctx.args;
|
|
44912
|
-
const dir2 = ctx.info.profileDir;
|
|
44913
|
-
const path = configPath(dir2);
|
|
44914
|
-
if (!sub || sub === "show") {
|
|
44915
|
-
ctx.print(`config: ${path}`);
|
|
44916
|
-
const json = JSON.stringify(readProfileConfig(dir2), null, 2);
|
|
44917
|
-
for (const line of json.split("\n")) ctx.print(line);
|
|
44918
|
-
return;
|
|
44919
|
-
}
|
|
44920
|
-
if (sub === "init") {
|
|
44921
|
-
const writeDefaults = (verb) => {
|
|
44922
|
-
overwriteProfileConfig(dir2, defaultProfileConfig());
|
|
44923
|
-
ctx.print(`\u2714 ${verb} default config: ${path}`);
|
|
44924
|
-
ctx.print("add claude profiles: /config set claudeHome .claude .claude-1 (tried in order)");
|
|
44925
|
-
};
|
|
44926
|
-
if (existsSync15(path)) {
|
|
44927
|
-
ctx.confirm(
|
|
44928
|
-
`config exists: ${path} \u2014 replace it with defaults? (y/N)`,
|
|
44929
|
-
() => writeDefaults("replaced with")
|
|
44930
|
-
);
|
|
44931
|
-
return;
|
|
44932
|
-
}
|
|
44933
|
-
writeDefaults("wrote");
|
|
44934
|
-
return;
|
|
44935
|
-
}
|
|
44936
|
-
if (sub === "set") {
|
|
44937
|
-
const [key, ...valueTokens] = rest;
|
|
44938
|
-
if (!key) {
|
|
44939
|
-
ctx.print("usage: /config set <key> <value>", "error");
|
|
44940
|
-
return;
|
|
44941
|
-
}
|
|
44942
|
-
const result = parseConfigAssignment(key, valueTokens);
|
|
44943
|
-
if ("error" in result) {
|
|
44944
|
-
ctx.print(result.error, "error");
|
|
44945
|
-
return;
|
|
44946
|
-
}
|
|
44947
|
-
const merged = writeProfileConfig(dir2, result.patch);
|
|
44948
|
-
ctx.print(`\u2714 ${key} = ${JSON.stringify(merged[key])} (applies to the next prompt)`);
|
|
44949
|
-
return;
|
|
44950
|
-
}
|
|
44951
|
-
if (sub === "delete" || sub === "unset") {
|
|
44952
|
-
const key = rest[0];
|
|
44953
|
-
if (!key) {
|
|
44954
|
-
ctx.print("usage: /config delete <key>", "error");
|
|
44955
|
-
return;
|
|
44956
|
-
}
|
|
44957
|
-
if (!CONFIG_KEYS.includes(key)) {
|
|
44958
|
-
ctx.print(`Unknown key "${key}". Allowed: ${CONFIG_KEYS.join(", ")}.`, "error");
|
|
44959
|
-
return;
|
|
44960
|
-
}
|
|
44961
|
-
deleteProfileConfigKey(dir2, key);
|
|
44962
|
-
ctx.print(`\u2714 deleted ${key}`);
|
|
44963
|
-
return;
|
|
44964
|
-
}
|
|
44965
|
-
ctx.print(`Unknown /config subcommand "${sub}". Use: show | init | set | delete.`, "error");
|
|
44966
|
-
}
|
|
44967
|
-
function fmtDuration(totalSec) {
|
|
44968
|
-
const h = Math.floor(totalSec / 3600);
|
|
44969
|
-
const m = Math.floor(totalSec % 3600 / 60);
|
|
44970
|
-
const s = totalSec % 60;
|
|
44971
|
-
const pad = (n) => String(n).padStart(2, "0");
|
|
44972
|
-
return h > 0 ? `${h}h ${pad(m)}m ${pad(s)}s` : `${m}m ${pad(s)}s`;
|
|
44973
|
-
}
|
|
44974
|
-
function commandLine(cmd, args) {
|
|
44975
|
-
return `${cmd} ${args.join(" ")}`.trim();
|
|
44976
|
-
}
|
|
44977
|
-
function runHistory(ctx) {
|
|
44978
|
-
const n = Math.max(1, Math.min(50, Number(ctx.args[0]) || 15));
|
|
44979
|
-
const all = getCommandHistory();
|
|
44980
|
-
if (all.length === 0) {
|
|
44981
|
-
ctx.print("No commands run yet.");
|
|
44982
|
-
return;
|
|
44983
|
-
}
|
|
44984
|
-
const recent = all.slice(-n).reverse();
|
|
44985
|
-
ctx.print(`Last ${recent.length} of ${all.length} commands (newest first) \xB7 /output <n> to view:`);
|
|
44986
|
-
recent.forEach((e, i) => {
|
|
44987
|
-
const time = new Date(e.startedAt).toLocaleTimeString();
|
|
44988
|
-
const state = e.status === "done" ? "\u2713" : e.status === "failed" ? `\u2717 exit ${e.exitCode}` : "\xB7 running";
|
|
44989
|
-
ctx.print(` [${i + 1}] ${time} ${commandLine(e.cmd, e.args)} \xB7 ${state}`);
|
|
44990
|
-
});
|
|
44991
|
-
}
|
|
44992
|
-
function runOutput(ctx) {
|
|
44993
|
-
const all = getCommandHistory();
|
|
44994
|
-
const n = Number(ctx.args[0]);
|
|
44995
|
-
if (!Number.isInteger(n) || n < 1 || n > all.length) {
|
|
44996
|
-
ctx.print("usage: /output <n> (n = index from /history)", "error");
|
|
44997
|
-
return;
|
|
44998
|
-
}
|
|
44999
|
-
const entry = all.slice().reverse()[n - 1];
|
|
45000
|
-
const state = entry.exitCode != null ? ` (exit ${entry.exitCode})` : "";
|
|
45001
|
-
ctx.print(`\u2500\u2500 output [${n}] ${commandLine(entry.cmd, entry.args)} \xB7 ${entry.status}${state} \u2500\u2500`);
|
|
45002
|
-
const out = readCommandOutput(entry.commandId);
|
|
45003
|
-
if (out == null) {
|
|
45004
|
-
ctx.print("(no output stored \u2014 it ran in a previous session or produced none)", "warn");
|
|
45005
|
-
return;
|
|
45515
|
+
}
|
|
45006
45516
|
}
|
|
45007
|
-
|
|
45517
|
+
if (line) rows.push(line.trimEnd());
|
|
45518
|
+
return rows.length > 0 ? rows : [""];
|
|
45008
45519
|
}
|
|
45009
|
-
|
|
45010
|
-
|
|
45011
|
-
|
|
45012
|
-
|
|
45013
|
-
if (lines.length === 0) {
|
|
45014
|
-
ctx.print("No log lines yet.");
|
|
45015
|
-
return;
|
|
45520
|
+
var DETAIL_RE = /<detail\s+title="([^"]*)">([\s\S]*?)<\/detail>/g;
|
|
45521
|
+
function toSegments(raw) {
|
|
45522
|
+
if (!raw.includes("<detail")) {
|
|
45523
|
+
return raw.split("\n").map((text) => ({ text, collapsed: false }));
|
|
45016
45524
|
}
|
|
45017
|
-
|
|
45018
|
-
|
|
45019
|
-
|
|
45020
|
-
|
|
45021
|
-
|
|
45022
|
-
|
|
45023
|
-
|
|
45024
|
-
|
|
45025
|
-
|
|
45026
|
-
|
|
45027
|
-
void profile;
|
|
45028
|
-
void scope;
|
|
45029
|
-
const extra = Object.keys(fields).length ? " " + JSON.stringify(fields) : "";
|
|
45030
|
-
ctx.print(` ${time} ${level} ${String(event ?? "")}${extra}`);
|
|
45031
|
-
} catch {
|
|
45032
|
-
ctx.print(` ${raw}`);
|
|
45033
|
-
}
|
|
45525
|
+
const out = [];
|
|
45526
|
+
const re = new RegExp(DETAIL_RE);
|
|
45527
|
+
let last = 0;
|
|
45528
|
+
let m;
|
|
45529
|
+
while ((m = re.exec(raw)) !== null) {
|
|
45530
|
+
const before = raw.slice(last, m.index).replace(/\n$/, "");
|
|
45531
|
+
if (before.length > 0) for (const p of before.split("\n")) out.push({ text: p, collapsed: false });
|
|
45532
|
+
out.push({ text: m[1] || "details", collapsed: true, body: m[2] ?? "" });
|
|
45533
|
+
last = m.index + m[0].length;
|
|
45534
|
+
if (raw[last] === "\n") last += 1;
|
|
45034
45535
|
}
|
|
45536
|
+
const rest = raw.slice(last);
|
|
45537
|
+
if (rest.length > 0) for (const p of rest.split("\n")) out.push({ text: p, collapsed: false });
|
|
45538
|
+
if (out.length === 0) out.push({ text: "", collapsed: false });
|
|
45539
|
+
return out;
|
|
45035
45540
|
}
|
|
45036
|
-
function
|
|
45037
|
-
|
|
45038
|
-
|
|
45039
|
-
|
|
45040
|
-
|
|
45041
|
-
|
|
45042
|
-
}
|
|
45043
|
-
|
|
45044
|
-
|
|
45045
|
-
|
|
45541
|
+
function pushCollapsible(lines, entry, opts) {
|
|
45542
|
+
const { blockNo, title, body, expanded, firstRow, textWidth, keyBase } = opts;
|
|
45543
|
+
const base = { source: entry.source, level: entry.level, kind: entry.kind };
|
|
45544
|
+
lines.push({
|
|
45545
|
+
...base,
|
|
45546
|
+
key: `${keyBase}:m`,
|
|
45547
|
+
text: `${expanded ? "\u25BE" : "\u25B8"}[${blockNo}] ${title}`,
|
|
45548
|
+
showTag: firstRow,
|
|
45549
|
+
collapsed: true,
|
|
45550
|
+
spacer: false,
|
|
45551
|
+
blockIndex: blockNo
|
|
45046
45552
|
});
|
|
45553
|
+
if (!expanded) return;
|
|
45554
|
+
prettyResult(body).split("\n").forEach(
|
|
45555
|
+
(seg, i) => wrapToWidth(seg, textWidth).forEach(
|
|
45556
|
+
(row, j) => lines.push({ ...base, key: `${keyBase}:b:${i}:${j}`, text: row, showTag: false, collapsed: false, spacer: false })
|
|
45557
|
+
)
|
|
45558
|
+
);
|
|
45047
45559
|
}
|
|
45048
|
-
function
|
|
45049
|
-
|
|
45050
|
-
|
|
45051
|
-
|
|
45052
|
-
|
|
45053
|
-
|
|
45054
|
-
|
|
45055
|
-
|
|
45056
|
-
|
|
45057
|
-
|
|
45058
|
-
|
|
45059
|
-
|
|
45060
|
-
|
|
45061
|
-
|
|
45062
|
-
|
|
45063
|
-
|
|
45064
|
-
|
|
45065
|
-
|
|
45066
|
-
run: (ctx) => {
|
|
45067
|
-
ctx.print("Slash commands:");
|
|
45068
|
-
for (const cmd of COMMANDS) ctx.print(` ${cmd.usage.padEnd(28)} ${cmd.description}`);
|
|
45560
|
+
function flattenEntries(entries2, textWidth, expanded = /* @__PURE__ */ new Set()) {
|
|
45561
|
+
const lines = [];
|
|
45562
|
+
let prevKey = null;
|
|
45563
|
+
let blockNo = 0;
|
|
45564
|
+
for (const entry of entries2) {
|
|
45565
|
+
const blockKey = `${entry.source}|${entry.kind}`;
|
|
45566
|
+
const newBlock = blockKey !== prevKey;
|
|
45567
|
+
if (newBlock && lines.length > 0) {
|
|
45568
|
+
lines.push({
|
|
45569
|
+
key: `sp:${entry.id}`,
|
|
45570
|
+
source: entry.source,
|
|
45571
|
+
level: entry.level,
|
|
45572
|
+
kind: entry.kind,
|
|
45573
|
+
text: "",
|
|
45574
|
+
showTag: false,
|
|
45575
|
+
collapsed: false,
|
|
45576
|
+
spacer: true
|
|
45577
|
+
});
|
|
45069
45578
|
}
|
|
45070
|
-
|
|
45071
|
-
|
|
45072
|
-
|
|
45073
|
-
|
|
45074
|
-
|
|
45075
|
-
|
|
45076
|
-
|
|
45077
|
-
|
|
45078
|
-
name: "status",
|
|
45079
|
-
usage: "/status",
|
|
45080
|
-
description: "Show connection + profile + session status",
|
|
45081
|
-
run: (ctx) => {
|
|
45082
|
-
const s = ctx.live;
|
|
45083
|
-
const uptime = fmtDuration(Math.max(0, Math.floor((Date.now() - s.startedAt) / 1e3)));
|
|
45084
|
-
ctx.print(`connection: ${s.status}`);
|
|
45085
|
-
ctx.print(`scope: ${s.scope}`);
|
|
45086
|
-
ctx.print(`worker: ${s.worker ?? "(unknown)"}`);
|
|
45087
|
-
if (s.project) ctx.print(`project: ${s.project}`);
|
|
45088
|
-
ctx.print(`profile: ${ctx.info.profile}`);
|
|
45089
|
-
ctx.print(`server: ${ctx.info.serverUrl}`);
|
|
45090
|
-
ctx.print(`aiCli: ${ctx.info.aiCli}${s.activeProfile ? ` \xB7 profile ${s.activeProfile}` : ""}`);
|
|
45091
|
-
ctx.print(`serving: ${ctx.info.physicPath ?? "(none)"}`);
|
|
45092
|
-
ctx.print(`uptime: ${uptime} \xB7 commands: ${getCommandHistory().length}`);
|
|
45579
|
+
prevKey = blockKey;
|
|
45580
|
+
let firstRow = newBlock;
|
|
45581
|
+
if (entry.kind === "result") {
|
|
45582
|
+
blockNo += 1;
|
|
45583
|
+
const count = prettyResult(entry.text).split("\n").length;
|
|
45584
|
+
const title = `result \xB7 ${entry.resultKind ?? "json"} \xB7 ${count} ${count === 1 ? "line" : "lines"}`;
|
|
45585
|
+
pushCollapsible(lines, entry, { blockNo, title, body: entry.text, expanded: expanded.has(blockNo), firstRow, textWidth, keyBase: entry.id });
|
|
45586
|
+
continue;
|
|
45093
45587
|
}
|
|
45094
|
-
|
|
45095
|
-
|
|
45096
|
-
|
|
45097
|
-
|
|
45098
|
-
|
|
45099
|
-
|
|
45100
|
-
},
|
|
45101
|
-
{
|
|
45102
|
-
name: "output",
|
|
45103
|
-
usage: "/output <n>",
|
|
45104
|
-
description: "Replay a command's output (n from /history)",
|
|
45105
|
-
run: runOutput
|
|
45106
|
-
},
|
|
45107
|
-
{
|
|
45108
|
-
name: "logs",
|
|
45109
|
-
usage: "/logs [N]",
|
|
45110
|
-
description: "Tail the profile's structured log",
|
|
45111
|
-
run: runLogs
|
|
45112
|
-
},
|
|
45113
|
-
{
|
|
45114
|
-
name: "expand",
|
|
45115
|
-
usage: "/expand [N]",
|
|
45116
|
-
description: "Expand a collapsed \u25B8[N] block (default: newest); pretty-prints json/code",
|
|
45117
|
-
run: (ctx) => runFold(ctx, "expand")
|
|
45118
|
-
},
|
|
45119
|
-
{
|
|
45120
|
-
name: "collapse",
|
|
45121
|
-
usage: "/collapse [N]",
|
|
45122
|
-
description: "Collapse an expanded \u25B8[N] block (default: newest)",
|
|
45123
|
-
run: (ctx) => runFold(ctx, "collapse")
|
|
45124
|
-
},
|
|
45125
|
-
{
|
|
45126
|
-
name: "reconnect",
|
|
45127
|
-
usage: "/reconnect",
|
|
45128
|
-
description: "Reconnect to the server now (skip the backoff)",
|
|
45129
|
-
run: (ctx) => ctx.reconnect()
|
|
45130
|
-
},
|
|
45131
|
-
{
|
|
45132
|
-
name: "whoami",
|
|
45133
|
-
usage: "/whoami",
|
|
45134
|
-
description: "Show this machine account + its teams & projects",
|
|
45135
|
-
run: runWhoami
|
|
45136
|
-
},
|
|
45137
|
-
{
|
|
45138
|
-
name: "config",
|
|
45139
|
-
usage: "/config [show|init|set <k> <v>|delete <k>]",
|
|
45140
|
-
description: "View / init / update / delete profile config",
|
|
45141
|
-
run: runConfig
|
|
45142
|
-
},
|
|
45143
|
-
{
|
|
45144
|
-
name: "claude-cmd",
|
|
45145
|
-
usage: "/claude-cmd /context",
|
|
45146
|
-
description: "Run an AI-CLI slash command (e.g. /context, /usage, /status)",
|
|
45147
|
-
run: (ctx) => {
|
|
45148
|
-
const rest = ctx.args.join(" ").trim();
|
|
45149
|
-
if (!rest) {
|
|
45150
|
-
ctx.print("usage: /claude-cmd /context (forwards to the AI CLI)", "error");
|
|
45588
|
+
const raw = entry.kind === "out" ? entry.text.replace(/\n$/, "") : entry.text;
|
|
45589
|
+
toSegments(raw).forEach((seg, i) => {
|
|
45590
|
+
if (seg.collapsed) {
|
|
45591
|
+
blockNo += 1;
|
|
45592
|
+
pushCollapsible(lines, entry, { blockNo, title: seg.text, body: seg.body ?? "", expanded: expanded.has(blockNo), firstRow, textWidth, keyBase: `${entry.id}:${i}` });
|
|
45593
|
+
firstRow = false;
|
|
45151
45594
|
return;
|
|
45152
45595
|
}
|
|
45153
|
-
|
|
45154
|
-
|
|
45155
|
-
|
|
45156
|
-
|
|
45157
|
-
|
|
45158
|
-
|
|
45159
|
-
|
|
45160
|
-
|
|
45161
|
-
|
|
45162
|
-
|
|
45163
|
-
|
|
45164
|
-
|
|
45165
|
-
|
|
45166
|
-
|
|
45167
|
-
}
|
|
45168
|
-
];
|
|
45169
|
-
var ALIASES = { exit: "quit" };
|
|
45170
|
-
var SLASH_COMMANDS = COMMANDS.map((c) => ({ name: c.name, usage: c.usage, description: c.description }));
|
|
45171
|
-
function runSlashCommand(line, base) {
|
|
45172
|
-
const tokens = line.replace(/^\//, "").trim().split(/\s+/);
|
|
45173
|
-
const rawName = tokens[0] ?? "";
|
|
45174
|
-
const name = ALIASES[rawName] ?? rawName;
|
|
45175
|
-
const command = COMMANDS.find((c) => c.name === name);
|
|
45176
|
-
const ctx = { ...base, args: tokens.slice(1) };
|
|
45177
|
-
if (!command) {
|
|
45178
|
-
ctx.print(`Unknown command: /${rawName} \u2014 try /help`, "error");
|
|
45179
|
-
return;
|
|
45596
|
+
wrapToWidth(seg.text, textWidth).forEach((row, j) => {
|
|
45597
|
+
lines.push({
|
|
45598
|
+
key: `${entry.id}:${i}:${j}`,
|
|
45599
|
+
source: entry.source,
|
|
45600
|
+
level: entry.level,
|
|
45601
|
+
kind: entry.kind,
|
|
45602
|
+
text: row,
|
|
45603
|
+
showTag: firstRow,
|
|
45604
|
+
collapsed: false,
|
|
45605
|
+
spacer: false
|
|
45606
|
+
});
|
|
45607
|
+
firstRow = false;
|
|
45608
|
+
});
|
|
45609
|
+
});
|
|
45180
45610
|
}
|
|
45181
|
-
|
|
45611
|
+
return lines;
|
|
45612
|
+
}
|
|
45613
|
+
function TranscriptLine({ line }) {
|
|
45614
|
+
const tag = sourceTag(line.source);
|
|
45615
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Box_default, { flexDirection: "row", children: [
|
|
45616
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Box_default, { width: 7, marginRight: 1, children: line.showTag ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text, { color: tag.color, dimColor: line.source === "system", children: tag.text }) : null }),
|
|
45617
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Box_default, { flexGrow: 1, children: line.spacer ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text, { children: " " }) : line.collapsed ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text, { dimColor: true, wrap: "truncate-end", children: line.text || " " }) : line.kind === "aireq" || line.kind === "aires" ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(AiMarkerLine, { line }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text, { color: textColor(line), wrap: "truncate-end", children: line.text || " " }) })
|
|
45618
|
+
] });
|
|
45182
45619
|
}
|
|
45183
45620
|
|
|
45184
45621
|
// src/ui/command-input.tsx
|
|
45622
|
+
var import_react24 = __toESM(require_react(), 1);
|
|
45185
45623
|
var import_jsx_runtime3 = __toESM(require_jsx_runtime(), 1);
|
|
45186
45624
|
function Hint() {
|
|
45187
45625
|
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Text, { dimColor: true, children: " /help \xB7 Tab complete \xB7 \u2191\u2193 history \xB7 PgUp/PgDn scroll \xB7 Ctrl+C quit" });
|
|
@@ -45466,6 +45904,9 @@ function App2({
|
|
|
45466
45904
|
setPendingConfirm({ prompt, onYes });
|
|
45467
45905
|
},
|
|
45468
45906
|
submitAi: (aiInput) => bus.submitLocal(aiInput),
|
|
45907
|
+
// /ai-profile use/reset updates the header's active-profile label at once (ADR-0250);
|
|
45908
|
+
// the bus event feeds the React state (see the onActiveProfile subscription above).
|
|
45909
|
+
setActiveProfile: (label) => bus.setActiveProfile(label),
|
|
45469
45910
|
reconnect: () => bus.requestReconnect(),
|
|
45470
45911
|
expand: expandFold,
|
|
45471
45912
|
collapse: collapseFold,
|