@4pm/cli 1.5.0 → 1.5.1
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 +855 -636
- 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) {
|
|
@@ -32932,7 +32942,15 @@ var dispatchCommandRequestSchema = external_exports.object({
|
|
|
32932
32942
|
* `project.git_write` (`write`) instead of `command.execute`, and validates `command`
|
|
32933
32943
|
* against the git allowlist (`isGitCommandAllowed`). Absent ⇒ normal console dispatch.
|
|
32934
32944
|
*/
|
|
32935
|
-
gitOp: external_exports.enum(["read", "write"]).optional()
|
|
32945
|
+
gitOp: external_exports.enum(["read", "write"]).optional(),
|
|
32946
|
+
/**
|
|
32947
|
+
* One-shot AI mode (ADR-0249): the prompt is a text-in → text-out task (spec review /
|
|
32948
|
+
* compose / suggest / generators) that must NOT trigger the AI CLI's agentic tool loop.
|
|
32949
|
+
* The cli caps such a run (`--max-turns 1` + disallowed agentic tools) so it can't wander
|
|
32950
|
+
* the repo / edit files / loop forever. Only meaningful with `ai:true`; absent ⇒ a full
|
|
32951
|
+
* agent run (the Console tab + Git merge, which legitimately use tools).
|
|
32952
|
+
*/
|
|
32953
|
+
aiOneShot: external_exports.boolean().optional()
|
|
32936
32954
|
}).superRefine((v, ctx) => {
|
|
32937
32955
|
if (v.machineLinkId == null === (v.pick == null)) {
|
|
32938
32956
|
ctx.addIssue({
|
|
@@ -32955,10 +32973,29 @@ var dispatchCommandRequestSchema = external_exports.object({
|
|
|
32955
32973
|
var listCommandsQuerySchema = baseRequestSchema.extend({
|
|
32956
32974
|
projectId: external_exports.string().uuid(),
|
|
32957
32975
|
// 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
|
|
32976
|
+
// text/status; `from`/`to` are an inclusive `YYYY-MM-DD` date range over `startedAt`;
|
|
32977
|
+
// `machineLinkId` (ADR-0249) filters to one machine-user's commands (absent ⇒ all).
|
|
32959
32978
|
from: external_exports.string().optional(),
|
|
32960
|
-
to: external_exports.string().optional()
|
|
32979
|
+
to: external_exports.string().optional(),
|
|
32980
|
+
machineLinkId: external_exports.string().uuid().optional()
|
|
32961
32981
|
});
|
|
32982
|
+
var CLI_SLASH_COMMANDS = [
|
|
32983
|
+
{ name: "help", usage: "/help", description: "List slash commands" },
|
|
32984
|
+
{ name: "version", usage: "/version", description: "Show the cli version" },
|
|
32985
|
+
{ name: "status", usage: "/status", description: "Show connection + profile + session status" },
|
|
32986
|
+
{ name: "history", usage: "/history [N]", description: "List the last N executed commands (+ time)" },
|
|
32987
|
+
{ name: "output", usage: "/output <n>", description: "Replay a command's output (n from /history)" },
|
|
32988
|
+
{ name: "logs", usage: "/logs [N]", description: "Tail the profile's structured log" },
|
|
32989
|
+
{ name: "expand", usage: "/expand [N]", description: "Expand a collapsed \u25B8[N] block" },
|
|
32990
|
+
{ name: "collapse", usage: "/collapse [N]", description: "Collapse an expanded \u25B8[N] block" },
|
|
32991
|
+
{ name: "reconnect", usage: "/reconnect", description: "Reconnect to the server now (skip the backoff)" },
|
|
32992
|
+
{ name: "whoami", usage: "/whoami", description: "Show this machine account + its teams & projects" },
|
|
32993
|
+
{ name: "config", usage: "/config [show|init|set <k> <v>|delete <k>]", description: "View / init / update / delete profile config" },
|
|
32994
|
+
{ name: "claude-cmd", usage: "/claude-cmd /context", description: "Run an AI-CLI slash command (e.g. /context, /usage)" },
|
|
32995
|
+
{ name: "clear", usage: "/clear", description: "Clear the transcript" },
|
|
32996
|
+
{ name: "quit", usage: "/quit", description: "Quit the cli (also /exit)" }
|
|
32997
|
+
];
|
|
32998
|
+
var CLI_SLASH_COMMAND_NAMES = CLI_SLASH_COMMANDS.map((c) => c.name);
|
|
32962
32999
|
|
|
32963
33000
|
// vendor/@4pm/validation/schemas.ts
|
|
32964
33001
|
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");
|
|
@@ -33792,7 +33829,7 @@ function decryptPayload(key, payloadBase64, nonceBase64) {
|
|
|
33792
33829
|
}
|
|
33793
33830
|
|
|
33794
33831
|
// src/core/ws-client.ts
|
|
33795
|
-
import { existsSync as
|
|
33832
|
+
import { existsSync as existsSync13, mkdirSync as mkdirSync5, rmSync as rmSync4 } from "fs";
|
|
33796
33833
|
import { randomUUID as randomCommandId } from "crypto";
|
|
33797
33834
|
|
|
33798
33835
|
// src/core/update-scheduler.ts
|
|
@@ -34117,15 +34154,24 @@ function createAiStreamParser(cli) {
|
|
|
34117
34154
|
let buffer = "";
|
|
34118
34155
|
const acc = { tokens: 0, input: 0, output: 0, cacheRead: 0, cacheCreation: 0 };
|
|
34119
34156
|
let sessionId = "";
|
|
34157
|
+
let apiError = { isError: false, status: null };
|
|
34120
34158
|
function handleClaude(ev) {
|
|
34121
34159
|
if (typeof ev.session_id === "string" && ev.session_id) sessionId = ev.session_id;
|
|
34122
|
-
if (ev.type === "result"
|
|
34123
|
-
|
|
34124
|
-
|
|
34125
|
-
|
|
34126
|
-
|
|
34127
|
-
|
|
34128
|
-
|
|
34160
|
+
if (ev.type === "result") {
|
|
34161
|
+
if (ev.is_error === true) {
|
|
34162
|
+
apiError = {
|
|
34163
|
+
isError: true,
|
|
34164
|
+
status: typeof ev.api_error_status === "number" ? ev.api_error_status : null
|
|
34165
|
+
};
|
|
34166
|
+
}
|
|
34167
|
+
if (ev.usage) {
|
|
34168
|
+
acc.input = ev.usage.input_tokens ?? 0;
|
|
34169
|
+
acc.output = ev.usage.output_tokens ?? 0;
|
|
34170
|
+
acc.cacheRead = ev.usage.cache_read_input_tokens ?? 0;
|
|
34171
|
+
acc.cacheCreation = ev.usage.cache_creation_input_tokens ?? 0;
|
|
34172
|
+
acc.tokens = acc.input + acc.output;
|
|
34173
|
+
if (typeof ev.total_cost_usd === "number") acc.costUsd = ev.total_cost_usd;
|
|
34174
|
+
}
|
|
34129
34175
|
return "";
|
|
34130
34176
|
}
|
|
34131
34177
|
if (ev.type === "assistant") {
|
|
@@ -34185,7 +34231,8 @@ function createAiStreamParser(cli) {
|
|
|
34185
34231
|
return handled === null ? line : handled;
|
|
34186
34232
|
},
|
|
34187
34233
|
usage: () => ({ ...acc }),
|
|
34188
|
-
sessionId: () => sessionId
|
|
34234
|
+
sessionId: () => sessionId,
|
|
34235
|
+
apiError: () => ({ ...apiError })
|
|
34189
34236
|
};
|
|
34190
34237
|
}
|
|
34191
34238
|
function estimateTokens(text) {
|
|
@@ -34203,6 +34250,11 @@ function isSessionLimit(text) {
|
|
|
34203
34250
|
text
|
|
34204
34251
|
);
|
|
34205
34252
|
}
|
|
34253
|
+
function isOutOfCredits(text) {
|
|
34254
|
+
return /out of (?:usage )?credits|credits[_ ]required|out[_ ]of[_ ]credits|insufficient credits/i.test(
|
|
34255
|
+
text
|
|
34256
|
+
);
|
|
34257
|
+
}
|
|
34206
34258
|
function runAttempt(commandId, cmd, args, cwd2, env3, onChunk, timeoutMs) {
|
|
34207
34259
|
return new Promise((resolve8) => {
|
|
34208
34260
|
void runCommand({ commandId, cmd, args, path: cwd2, env: env3 }, (out) => {
|
|
@@ -34236,7 +34288,8 @@ async function runAiFailover(plan, commandId, cwd2, handlers, timeoutMs = 0) {
|
|
|
34236
34288
|
handlers.onChunk(tail);
|
|
34237
34289
|
}
|
|
34238
34290
|
lastCaptured = captured;
|
|
34239
|
-
|
|
34291
|
+
const apiErr = parser.apiError();
|
|
34292
|
+
if (finalExit === 0 && !apiErr.isError) {
|
|
34240
34293
|
const usage = parser.usage();
|
|
34241
34294
|
if (usage.tokens === 0) usage.tokens = estimateTokens(captured);
|
|
34242
34295
|
reportToolResult(attempt.cmd, true);
|
|
@@ -34249,7 +34302,8 @@ async function runAiFailover(plan, commandId, cwd2, handlers, timeoutMs = 0) {
|
|
|
34249
34302
|
sessionId: parser.sessionId()
|
|
34250
34303
|
};
|
|
34251
34304
|
}
|
|
34252
|
-
|
|
34305
|
+
if (finalExit === 0 && apiErr.isError) finalExit = apiErr.status && apiErr.status > 0 ? apiErr.status : 1;
|
|
34306
|
+
const reason = isAuthFailure(captured) ? "auth" : apiErr.status === 429 || isOutOfCredits(captured) ? "credits" : isSessionLimit(captured) ? "limit" : "other";
|
|
34253
34307
|
if (i === total - 1) break;
|
|
34254
34308
|
handlers.onAttemptFail(attempt.label, reason);
|
|
34255
34309
|
}
|
|
@@ -34258,6 +34312,7 @@ async function runAiFailover(plan, commandId, cwd2, handlers, timeoutMs = 0) {
|
|
|
34258
34312
|
}
|
|
34259
34313
|
function summarizeAiFailure(captured, exitCode) {
|
|
34260
34314
|
if (isAuthFailure(captured)) return "Not logged in";
|
|
34315
|
+
if (isOutOfCredits(captured)) return "Out of usage credits";
|
|
34261
34316
|
if (isSessionLimit(captured)) return "Usage limit reached";
|
|
34262
34317
|
const firstLine = captured.split("\n").map((l) => l.trim()).find((l) => l.length > 0);
|
|
34263
34318
|
return firstLine || `exited ${exitCode}`;
|
|
@@ -34313,6 +34368,42 @@ var REQUIRED_AI_ARGS = {
|
|
|
34313
34368
|
codex: ["exec", "--json"],
|
|
34314
34369
|
antigravity: []
|
|
34315
34370
|
};
|
|
34371
|
+
var ONE_SHOT_DISALLOWED_CLAUDE_TOOLS = [
|
|
34372
|
+
"Bash",
|
|
34373
|
+
"Edit",
|
|
34374
|
+
"Write",
|
|
34375
|
+
"Read",
|
|
34376
|
+
"Glob",
|
|
34377
|
+
"Grep",
|
|
34378
|
+
"NotebookEdit",
|
|
34379
|
+
"Task",
|
|
34380
|
+
"WebSearch",
|
|
34381
|
+
"WebFetch",
|
|
34382
|
+
"Skill",
|
|
34383
|
+
"SlashCommand",
|
|
34384
|
+
"ToolSearch",
|
|
34385
|
+
"TaskCreate",
|
|
34386
|
+
"TaskGet",
|
|
34387
|
+
"TaskList",
|
|
34388
|
+
"TaskOutput",
|
|
34389
|
+
"TaskStop",
|
|
34390
|
+
"TaskUpdate",
|
|
34391
|
+
"Monitor",
|
|
34392
|
+
"DesignSync",
|
|
34393
|
+
"CronCreate",
|
|
34394
|
+
"CronDelete",
|
|
34395
|
+
"CronList",
|
|
34396
|
+
"EnterWorktree",
|
|
34397
|
+
"ExitWorktree",
|
|
34398
|
+
"RemoteTrigger",
|
|
34399
|
+
"ScheduleWakeup",
|
|
34400
|
+
"SendMessage",
|
|
34401
|
+
"PushNotification"
|
|
34402
|
+
].join(",");
|
|
34403
|
+
function oneShotArgs(cmd) {
|
|
34404
|
+
if (!cmd.includes("claude")) return [];
|
|
34405
|
+
return ["--max-turns=1", "--disallowedTools", ONE_SHOT_DISALLOWED_CLAUDE_TOOLS, "--permission-mode", "default"];
|
|
34406
|
+
}
|
|
34316
34407
|
function requiredArgs(cmd) {
|
|
34317
34408
|
if (cmd.includes("claude")) return REQUIRED_AI_ARGS.claude;
|
|
34318
34409
|
if (cmd.includes("codex")) return REQUIRED_AI_ARGS.codex;
|
|
@@ -34372,12 +34463,15 @@ function folderScopeGuard(folder, prompt) {
|
|
|
34372
34463
|
prompt
|
|
34373
34464
|
].join("\n");
|
|
34374
34465
|
}
|
|
34375
|
-
function buildRunArgs(profile, cmd, prompt, resumeId) {
|
|
34466
|
+
function buildRunArgs(profile, cmd, prompt, resumeId, oneShot = false) {
|
|
34376
34467
|
const extras = profile.args ?? [];
|
|
34377
34468
|
const model = profile.model?.trim();
|
|
34378
34469
|
const modelArgs = model ? ["--model", model] : [];
|
|
34379
34470
|
const resumeArgs = resumeId && cmd.includes("claude") ? ["--resume", resumeId] : [];
|
|
34380
|
-
|
|
34471
|
+
const turns = profile.maxTurns;
|
|
34472
|
+
const perProfileTurns = !oneShot && cmd.includes("claude") && typeof turns === "number" && turns > 0 ? [`--max-turns=${Math.floor(turns)}`] : [];
|
|
34473
|
+
const oneShotExtra = oneShot ? oneShotArgs(cmd) : perProfileTurns;
|
|
34474
|
+
return [...requiredArgs(cmd), ...resumeArgs, ...extras, ...modelArgs, ...oneShotExtra, prompt];
|
|
34381
34475
|
}
|
|
34382
34476
|
function claudeHomeDirs(config) {
|
|
34383
34477
|
const dirs = profileDirs(claudeProfilesOf(config));
|
|
@@ -34416,10 +34510,10 @@ function resolveClaudeProfiles(config, workingDir) {
|
|
|
34416
34510
|
if (idx > 0) profiles.unshift(profiles.splice(idx, 1)[0]);
|
|
34417
34511
|
return profiles;
|
|
34418
34512
|
}
|
|
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);
|
|
34513
|
+
function planAiRun(prompt, config, hint = {}, resume = /* @__PURE__ */ new Map(), oneShot = false) {
|
|
34514
|
+
return isUnifiedConfig(config) ? planUnifiedRun(prompt, config, hint.credential ?? null, resume, oneShot) : planLegacyRun(prompt, config, hint.dir ?? null, resume, oneShot);
|
|
34421
34515
|
}
|
|
34422
|
-
function planUnifiedRun(prompt, config, workingCredential, resume) {
|
|
34516
|
+
function planUnifiedRun(prompt, config, workingCredential, resume, oneShot) {
|
|
34423
34517
|
const baseEnv = config.aiEnv ?? {};
|
|
34424
34518
|
const active = activeProvider(config);
|
|
34425
34519
|
const usable = config.aiProfiles.filter(isUsableCredential);
|
|
@@ -34428,7 +34522,7 @@ function planUnifiedRun(prompt, config, workingCredential, resume) {
|
|
|
34428
34522
|
return {
|
|
34429
34523
|
cmd: active,
|
|
34430
34524
|
attempts: [
|
|
34431
|
-
{ cmd: active, label: "default", dir: null, key: null, args: buildRunArgs({ profile: "" }, active, prompt), env: { ...baseEnv } }
|
|
34525
|
+
{ cmd: active, label: "default", dir: null, key: null, args: buildRunArgs({ profile: "" }, active, prompt, void 0, oneShot), env: { ...baseEnv } }
|
|
34432
34526
|
]
|
|
34433
34527
|
};
|
|
34434
34528
|
}
|
|
@@ -34446,19 +34540,19 @@ function planUnifiedRun(prompt, config, workingCredential, resume) {
|
|
|
34446
34540
|
label: cred.label?.trim() || profileDisplayLabel(dir2),
|
|
34447
34541
|
dir: dir2,
|
|
34448
34542
|
key,
|
|
34449
|
-
args: buildRunArgs(cred, cmd, prompt, resume.get(key)),
|
|
34543
|
+
args: buildRunArgs(cred, cmd, prompt, resume.get(key), oneShot),
|
|
34450
34544
|
env: envVar ? { ...baseEnv, [envVar]: dir2 } : { ...baseEnv }
|
|
34451
34545
|
}));
|
|
34452
34546
|
return { cmd: attempts[0]?.cmd ?? DEFAULT_AI_CLI, attempts };
|
|
34453
34547
|
}
|
|
34454
|
-
function planLegacyRun(prompt, config, workingDir, resume) {
|
|
34548
|
+
function planLegacyRun(prompt, config, workingDir, resume, oneShot) {
|
|
34455
34549
|
const cmd = config.aiCli || DEFAULT_AI_CLI;
|
|
34456
34550
|
const baseEnv = config.aiEnv ?? {};
|
|
34457
34551
|
const envVar = profileEnvVar(cmd);
|
|
34458
34552
|
const defaultPlan = {
|
|
34459
34553
|
cmd,
|
|
34460
34554
|
attempts: [
|
|
34461
|
-
{ cmd, label: "default", dir: null, key: null, args: buildRunArgs({ profile: "" }, cmd, prompt), env: { ...baseEnv } }
|
|
34555
|
+
{ cmd, label: "default", dir: null, key: null, args: buildRunArgs({ profile: "" }, cmd, prompt, void 0, oneShot), env: { ...baseEnv } }
|
|
34462
34556
|
]
|
|
34463
34557
|
};
|
|
34464
34558
|
if (!envVar) return defaultPlan;
|
|
@@ -34475,7 +34569,7 @@ function planLegacyRun(prompt, config, workingDir, resume) {
|
|
|
34475
34569
|
label: profileDisplayLabel(dir2),
|
|
34476
34570
|
dir: dir2,
|
|
34477
34571
|
key,
|
|
34478
|
-
args: buildRunArgs(profile, cmd, prompt, resume.get(key)),
|
|
34572
|
+
args: buildRunArgs(profile, cmd, prompt, resume.get(key), oneShot),
|
|
34479
34573
|
env: { ...baseEnv, [envVar]: dir2 }
|
|
34480
34574
|
};
|
|
34481
34575
|
});
|
|
@@ -36668,188 +36762,510 @@ async function addProject(payload, profileDir2, onProgress) {
|
|
|
36668
36762
|
}
|
|
36669
36763
|
}
|
|
36670
36764
|
|
|
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";
|
|
36765
|
+
// src/ui/slash-commands.ts
|
|
36766
|
+
import { existsSync as existsSync11 } from "fs";
|
|
36742
36767
|
import { join as join23 } from "path";
|
|
36743
|
-
var
|
|
36768
|
+
var CONFIG_KEYS = [
|
|
36769
|
+
"aiCli",
|
|
36770
|
+
"claudeHome",
|
|
36771
|
+
"codexHome",
|
|
36772
|
+
"antigravityHome",
|
|
36744
36773
|
"physicPath",
|
|
36745
|
-
"
|
|
36746
|
-
"
|
|
36747
|
-
|
|
36748
|
-
// edit must not fight it. NOTE: the machine-user's own `aiRunTimeoutSec` is NOT here (operator-editable).
|
|
36749
|
-
"projectAiRunTimeoutSec",
|
|
36750
|
-
// Project idle auto-clear override mirror (ADR-0244) — same rule; the machine-user's own
|
|
36751
|
-
// `autoClearIdleMinutes` is operator-editable and NOT listed here.
|
|
36752
|
-
"projectAutoClearIdleMinutes",
|
|
36753
|
-
// Project shared-AI-memory override mirrors (ADR-0245) — the machine-user's own `aiMemoryEnabled`
|
|
36754
|
-
// / `aiMemoryBudgetChars` are operator-editable and NOT listed here.
|
|
36755
|
-
"projectAiMemoryMode",
|
|
36756
|
-
"projectAiMemoryBudgetChars"
|
|
36774
|
+
"commandHistoryUploadMinutes",
|
|
36775
|
+
"autoUpdate",
|
|
36776
|
+
"aiEnv"
|
|
36757
36777
|
];
|
|
36758
|
-
function
|
|
36759
|
-
|
|
36760
|
-
|
|
36761
|
-
|
|
36762
|
-
|
|
36763
|
-
|
|
36778
|
+
function configPath(profileDir2) {
|
|
36779
|
+
return join23(profileDir2, "config.json");
|
|
36780
|
+
}
|
|
36781
|
+
function parseConfigAssignment(key, valueTokens) {
|
|
36782
|
+
const value = valueTokens.join(" ").trim();
|
|
36783
|
+
switch (key) {
|
|
36784
|
+
case "aiCli":
|
|
36785
|
+
case "physicPath":
|
|
36786
|
+
if (!value) return { error: `Missing value for "${key}".` };
|
|
36787
|
+
return { patch: { [key]: value } };
|
|
36788
|
+
case "claudeHome":
|
|
36789
|
+
case "codexHome":
|
|
36790
|
+
case "antigravityHome": {
|
|
36791
|
+
const dirs = valueTokens.filter((t) => t.trim());
|
|
36792
|
+
if (dirs.length === 0) return { error: `Missing value for "${key}".` };
|
|
36793
|
+
return { patch: { [key]: dirs.map((profile) => ({ profile, model: "" })) } };
|
|
36794
|
+
}
|
|
36795
|
+
case "commandHistoryUploadMinutes": {
|
|
36796
|
+
const n = Number(value);
|
|
36797
|
+
if (!Number.isFinite(n) || n <= 0) return { error: `"${value}" is not a positive number.` };
|
|
36798
|
+
return { patch: { commandHistoryUploadMinutes: n } };
|
|
36764
36799
|
}
|
|
36800
|
+
case "autoUpdate":
|
|
36801
|
+
if (value !== "true" && value !== "false") return { error: `Use true|false, not "${value}".` };
|
|
36802
|
+
return { patch: { autoUpdate: value === "true" } };
|
|
36803
|
+
case "aiEnv":
|
|
36804
|
+
try {
|
|
36805
|
+
return { patch: { aiEnv: JSON.parse(value) } };
|
|
36806
|
+
} catch {
|
|
36807
|
+
return { error: `aiEnv must be JSON, e.g. {"KEY":"val"}.` };
|
|
36808
|
+
}
|
|
36809
|
+
default:
|
|
36810
|
+
return { error: `Unknown key "${key}". Allowed: ${CONFIG_KEYS.join(", ")}.` };
|
|
36765
36811
|
}
|
|
36766
|
-
return JSON.stringify(defaultProfileConfig(), null, 2);
|
|
36767
36812
|
}
|
|
36768
|
-
function
|
|
36769
|
-
|
|
36770
|
-
|
|
36771
|
-
|
|
36772
|
-
|
|
36773
|
-
|
|
36774
|
-
|
|
36775
|
-
|
|
36776
|
-
return
|
|
36777
|
-
}
|
|
36778
|
-
const next = { ...parsed };
|
|
36779
|
-
const current = readProfileConfig(profileDir2);
|
|
36780
|
-
for (const key of SERVER_MANAGED_KEYS) {
|
|
36781
|
-
if (current[key] !== void 0) next[key] = current[key];
|
|
36782
|
-
else delete next[key];
|
|
36813
|
+
function runConfig(ctx) {
|
|
36814
|
+
const [sub, ...rest] = ctx.args;
|
|
36815
|
+
const dir2 = ctx.info.profileDir;
|
|
36816
|
+
const path = configPath(dir2);
|
|
36817
|
+
if (!sub || sub === "show") {
|
|
36818
|
+
ctx.print(`config: ${path}`);
|
|
36819
|
+
const json = JSON.stringify(readProfileConfig(dir2), null, 2);
|
|
36820
|
+
for (const line of json.split("\n")) ctx.print(line);
|
|
36821
|
+
return;
|
|
36783
36822
|
}
|
|
36784
|
-
|
|
36785
|
-
|
|
36786
|
-
|
|
36787
|
-
|
|
36788
|
-
|
|
36789
|
-
import { spawn as spawn9 } from "child_process";
|
|
36790
|
-
var NPM_NAME = /^(?:@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/;
|
|
36791
|
-
function run10(cmd, args, onLine, timeoutMs = 18e4) {
|
|
36792
|
-
return new Promise((resolve8) => {
|
|
36793
|
-
let out = "";
|
|
36794
|
-
let buf = "";
|
|
36795
|
-
let child = null;
|
|
36796
|
-
const done = (code) => {
|
|
36797
|
-
clearTimeout(timer);
|
|
36798
|
-
if (onLine && buf.trim()) onLine(buf.trim());
|
|
36799
|
-
resolve8({ code, out });
|
|
36800
|
-
};
|
|
36801
|
-
const timer = setTimeout(() => {
|
|
36802
|
-
child?.kill();
|
|
36803
|
-
if (onLine) onLine(`timed out after ${Math.round(timeoutMs / 1e3)}s`);
|
|
36804
|
-
done(124);
|
|
36805
|
-
}, timeoutMs);
|
|
36806
|
-
timer.unref?.();
|
|
36807
|
-
const feed = (d) => {
|
|
36808
|
-
const text = d.toString("utf8");
|
|
36809
|
-
out += text;
|
|
36810
|
-
if (!onLine) return;
|
|
36811
|
-
buf += text;
|
|
36812
|
-
let nl;
|
|
36813
|
-
while ((nl = buf.indexOf("\n")) >= 0) {
|
|
36814
|
-
const line = buf.slice(0, nl).replace(/\r$/, "");
|
|
36815
|
-
buf = buf.slice(nl + 1);
|
|
36816
|
-
onLine(line);
|
|
36817
|
-
}
|
|
36823
|
+
if (sub === "init") {
|
|
36824
|
+
const writeDefaults = (verb) => {
|
|
36825
|
+
overwriteProfileConfig(dir2, defaultProfileConfig());
|
|
36826
|
+
ctx.print(`\u2714 ${verb} default config: ${path}`);
|
|
36827
|
+
ctx.print("add claude profiles: /config set claudeHome .claude .claude-1 (tried in order)");
|
|
36818
36828
|
};
|
|
36819
|
-
|
|
36820
|
-
|
|
36821
|
-
|
|
36822
|
-
|
|
36823
|
-
|
|
36824
|
-
|
|
36825
|
-
} catch {
|
|
36826
|
-
done(127);
|
|
36829
|
+
if (existsSync11(path)) {
|
|
36830
|
+
ctx.confirm(
|
|
36831
|
+
`config exists: ${path} \u2014 replace it with defaults? (y/N)`,
|
|
36832
|
+
() => writeDefaults("replaced with")
|
|
36833
|
+
);
|
|
36834
|
+
return;
|
|
36827
36835
|
}
|
|
36828
|
-
|
|
36829
|
-
|
|
36830
|
-
async function detectOne(cmd, versionArg) {
|
|
36831
|
-
const { code, out } = await run10(cmd, [versionArg], void 0, 15e3);
|
|
36832
|
-
if (code !== 0) return null;
|
|
36833
|
-
const first = out.split("\n").map((l) => l.trim()).find(Boolean);
|
|
36834
|
-
return first ?? "";
|
|
36835
|
-
}
|
|
36836
|
-
async function listExtras() {
|
|
36837
|
-
const { out } = await run10("npm", ["ls", "-g", "--depth=0", "--json"], void 0, 2e4);
|
|
36838
|
-
if (!out.trim()) return [];
|
|
36839
|
-
let deps = {};
|
|
36840
|
-
try {
|
|
36841
|
-
deps = JSON.parse(out).dependencies ?? {};
|
|
36842
|
-
} catch {
|
|
36843
|
-
return [];
|
|
36836
|
+
writeDefaults("wrote");
|
|
36837
|
+
return;
|
|
36844
36838
|
}
|
|
36845
|
-
|
|
36846
|
-
|
|
36847
|
-
|
|
36848
|
-
|
|
36839
|
+
if (sub === "set") {
|
|
36840
|
+
const [key, ...valueTokens] = rest;
|
|
36841
|
+
if (!key) {
|
|
36842
|
+
ctx.print("usage: /config set <key> <value>", "error");
|
|
36843
|
+
return;
|
|
36844
|
+
}
|
|
36845
|
+
const result = parseConfigAssignment(key, valueTokens);
|
|
36846
|
+
if ("error" in result) {
|
|
36847
|
+
ctx.print(result.error, "error");
|
|
36848
|
+
return;
|
|
36849
|
+
}
|
|
36850
|
+
const merged = writeProfileConfig(dir2, result.patch);
|
|
36851
|
+
ctx.print(`\u2714 ${key} = ${JSON.stringify(merged[key])} (applies to the next prompt)`);
|
|
36852
|
+
return;
|
|
36849
36853
|
}
|
|
36850
|
-
|
|
36851
|
-
|
|
36852
|
-
|
|
36854
|
+
if (sub === "delete" || sub === "unset") {
|
|
36855
|
+
const key = rest[0];
|
|
36856
|
+
if (!key) {
|
|
36857
|
+
ctx.print("usage: /config delete <key>", "error");
|
|
36858
|
+
return;
|
|
36859
|
+
}
|
|
36860
|
+
if (!CONFIG_KEYS.includes(key)) {
|
|
36861
|
+
ctx.print(`Unknown key "${key}". Allowed: ${CONFIG_KEYS.join(", ")}.`, "error");
|
|
36862
|
+
return;
|
|
36863
|
+
}
|
|
36864
|
+
deleteProfileConfigKey(dir2, key);
|
|
36865
|
+
ctx.print(`\u2714 deleted ${key}`);
|
|
36866
|
+
return;
|
|
36867
|
+
}
|
|
36868
|
+
ctx.print(`Unknown /config subcommand "${sub}". Use: show | init | set | delete.`, "error");
|
|
36869
|
+
}
|
|
36870
|
+
function fmtDuration(totalSec) {
|
|
36871
|
+
const h = Math.floor(totalSec / 3600);
|
|
36872
|
+
const m = Math.floor(totalSec % 3600 / 60);
|
|
36873
|
+
const s = totalSec % 60;
|
|
36874
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
36875
|
+
return h > 0 ? `${h}h ${pad(m)}m ${pad(s)}s` : `${m}m ${pad(s)}s`;
|
|
36876
|
+
}
|
|
36877
|
+
function commandLine(cmd, args) {
|
|
36878
|
+
return `${cmd} ${args.join(" ")}`.trim();
|
|
36879
|
+
}
|
|
36880
|
+
function runHistory(ctx) {
|
|
36881
|
+
const n = Math.max(1, Math.min(50, Number(ctx.args[0]) || 15));
|
|
36882
|
+
const all = getCommandHistory();
|
|
36883
|
+
if (all.length === 0) {
|
|
36884
|
+
ctx.print("No commands run yet.");
|
|
36885
|
+
return;
|
|
36886
|
+
}
|
|
36887
|
+
const recent = all.slice(-n).reverse();
|
|
36888
|
+
ctx.print(`Last ${recent.length} of ${all.length} commands (newest first) \xB7 /output <n> to view:`);
|
|
36889
|
+
recent.forEach((e, i) => {
|
|
36890
|
+
const time = new Date(e.startedAt).toLocaleTimeString();
|
|
36891
|
+
const state = e.status === "done" ? "\u2713" : e.status === "failed" ? `\u2717 exit ${e.exitCode}` : "\xB7 running";
|
|
36892
|
+
ctx.print(` [${i + 1}] ${time} ${commandLine(e.cmd, e.args)} \xB7 ${state}`);
|
|
36893
|
+
});
|
|
36894
|
+
}
|
|
36895
|
+
function runOutput(ctx) {
|
|
36896
|
+
const all = getCommandHistory();
|
|
36897
|
+
const n = Number(ctx.args[0]);
|
|
36898
|
+
if (!Number.isInteger(n) || n < 1 || n > all.length) {
|
|
36899
|
+
ctx.print("usage: /output <n> (n = index from /history)", "error");
|
|
36900
|
+
return;
|
|
36901
|
+
}
|
|
36902
|
+
const entry = all.slice().reverse()[n - 1];
|
|
36903
|
+
const state = entry.exitCode != null ? ` (exit ${entry.exitCode})` : "";
|
|
36904
|
+
ctx.print(`\u2500\u2500 output [${n}] ${commandLine(entry.cmd, entry.args)} \xB7 ${entry.status}${state} \u2500\u2500`);
|
|
36905
|
+
const out = readCommandOutput(entry.commandId);
|
|
36906
|
+
if (out == null) {
|
|
36907
|
+
ctx.print("(no output stored \u2014 it ran in a previous session or produced none)", "warn");
|
|
36908
|
+
return;
|
|
36909
|
+
}
|
|
36910
|
+
for (const line of out.replace(/\n$/, "").split("\n")) ctx.print(line);
|
|
36911
|
+
}
|
|
36912
|
+
function runLogs(ctx) {
|
|
36913
|
+
const n = Math.max(1, Math.min(200, Number(ctx.args[0]) || 20));
|
|
36914
|
+
const dir2 = join23(ctx.info.profileDir, "logs");
|
|
36915
|
+
const lines = readRecentLogLines(dir2, n);
|
|
36916
|
+
if (lines.length === 0) {
|
|
36917
|
+
ctx.print("No log lines yet.");
|
|
36918
|
+
return;
|
|
36919
|
+
}
|
|
36920
|
+
ctx.print(`Last ${lines.length} log lines (${dir2}):`);
|
|
36921
|
+
for (const raw of lines) {
|
|
36922
|
+
try {
|
|
36923
|
+
const j = JSON.parse(raw);
|
|
36924
|
+
const time = String(j.timestamp ?? "").slice(11, 19);
|
|
36925
|
+
const level = String(j.level ?? "").padEnd(5);
|
|
36926
|
+
const { timestamp, level: _l, version, profile, scope, event, ...fields } = j;
|
|
36927
|
+
void timestamp;
|
|
36928
|
+
void _l;
|
|
36929
|
+
void version;
|
|
36930
|
+
void profile;
|
|
36931
|
+
void scope;
|
|
36932
|
+
const extra = Object.keys(fields).length ? " " + JSON.stringify(fields) : "";
|
|
36933
|
+
ctx.print(` ${time} ${level} ${String(event ?? "")}${extra}`);
|
|
36934
|
+
} catch {
|
|
36935
|
+
ctx.print(` ${raw}`);
|
|
36936
|
+
}
|
|
36937
|
+
}
|
|
36938
|
+
}
|
|
36939
|
+
function runWhoami(ctx) {
|
|
36940
|
+
ctx.print("whoami: fetching from the server\u2026");
|
|
36941
|
+
void ctx.info.whoami().then((w) => {
|
|
36942
|
+
if (!w) {
|
|
36943
|
+
ctx.print("whoami: unavailable (server unreachable or link invalid)", "warn");
|
|
36944
|
+
return;
|
|
36945
|
+
}
|
|
36946
|
+
ctx.print(`account: ${w.username} \xB7 scope: ${w.scope} \xB7 roles: ${w.roles.join(", ") || "\u2014"}`);
|
|
36947
|
+
ctx.print(`teams: ${w.teams.length ? w.teams.map((t) => t.name).join(", ") : "(none)"}`);
|
|
36948
|
+
ctx.print(`projects: ${w.projects.length ? w.projects.map((p) => p.name).join(", ") : "(none)"}`);
|
|
36949
|
+
});
|
|
36950
|
+
}
|
|
36951
|
+
function runFold(ctx, action) {
|
|
36952
|
+
if (ctx.maxBlock === 0) {
|
|
36953
|
+
ctx.print("No collapsible blocks on screen.", "warn");
|
|
36954
|
+
return;
|
|
36955
|
+
}
|
|
36956
|
+
const n = ctx.args[0] != null ? Number(ctx.args[0]) : ctx.maxBlock;
|
|
36957
|
+
if (!Number.isInteger(n) || n < 1 || n > ctx.maxBlock) {
|
|
36958
|
+
ctx.print(`usage: /${action} [1-${ctx.maxBlock}] (no number \u21D2 the newest block)`, "error");
|
|
36959
|
+
return;
|
|
36960
|
+
}
|
|
36961
|
+
if (action === "expand") ctx.expand(n);
|
|
36962
|
+
else ctx.collapse(n);
|
|
36963
|
+
}
|
|
36964
|
+
var COMMANDS = [
|
|
36965
|
+
{
|
|
36966
|
+
name: "help",
|
|
36967
|
+
usage: "/help",
|
|
36968
|
+
description: "List slash commands",
|
|
36969
|
+
run: (ctx) => {
|
|
36970
|
+
ctx.print("Slash commands:");
|
|
36971
|
+
for (const cmd of COMMANDS) ctx.print(` ${cmd.usage.padEnd(28)} ${cmd.description}`);
|
|
36972
|
+
}
|
|
36973
|
+
},
|
|
36974
|
+
{
|
|
36975
|
+
name: "version",
|
|
36976
|
+
usage: "/version",
|
|
36977
|
+
description: "Show the cli version",
|
|
36978
|
+
run: (ctx) => ctx.print(`4PM CLI v${ctx.info.version} \xB7 profile ${ctx.info.profile}`)
|
|
36979
|
+
},
|
|
36980
|
+
{
|
|
36981
|
+
name: "status",
|
|
36982
|
+
usage: "/status",
|
|
36983
|
+
description: "Show connection + profile + session status",
|
|
36984
|
+
run: (ctx) => {
|
|
36985
|
+
const s = ctx.live;
|
|
36986
|
+
const uptime = fmtDuration(Math.max(0, Math.floor((Date.now() - s.startedAt) / 1e3)));
|
|
36987
|
+
ctx.print(`connection: ${s.status}`);
|
|
36988
|
+
ctx.print(`scope: ${s.scope}`);
|
|
36989
|
+
ctx.print(`worker: ${s.worker ?? "(unknown)"}`);
|
|
36990
|
+
if (s.project) ctx.print(`project: ${s.project}`);
|
|
36991
|
+
ctx.print(`profile: ${ctx.info.profile}`);
|
|
36992
|
+
ctx.print(`server: ${ctx.info.serverUrl}`);
|
|
36993
|
+
ctx.print(`aiCli: ${ctx.info.aiCli}${s.activeProfile ? ` \xB7 profile ${s.activeProfile}` : ""}`);
|
|
36994
|
+
ctx.print(`serving: ${ctx.info.physicPath ?? "(none)"}`);
|
|
36995
|
+
ctx.print(`uptime: ${uptime} \xB7 commands: ${getCommandHistory().length}`);
|
|
36996
|
+
}
|
|
36997
|
+
},
|
|
36998
|
+
{
|
|
36999
|
+
name: "history",
|
|
37000
|
+
usage: "/history [N]",
|
|
37001
|
+
description: "List the last N executed commands (+ time)",
|
|
37002
|
+
run: runHistory
|
|
37003
|
+
},
|
|
37004
|
+
{
|
|
37005
|
+
name: "output",
|
|
37006
|
+
usage: "/output <n>",
|
|
37007
|
+
description: "Replay a command's output (n from /history)",
|
|
37008
|
+
run: runOutput
|
|
37009
|
+
},
|
|
37010
|
+
{
|
|
37011
|
+
name: "logs",
|
|
37012
|
+
usage: "/logs [N]",
|
|
37013
|
+
description: "Tail the profile's structured log",
|
|
37014
|
+
run: runLogs
|
|
37015
|
+
},
|
|
37016
|
+
{
|
|
37017
|
+
name: "expand",
|
|
37018
|
+
usage: "/expand [N]",
|
|
37019
|
+
description: "Expand a collapsed \u25B8[N] block (default: newest); pretty-prints json/code",
|
|
37020
|
+
run: (ctx) => runFold(ctx, "expand")
|
|
37021
|
+
},
|
|
37022
|
+
{
|
|
37023
|
+
name: "collapse",
|
|
37024
|
+
usage: "/collapse [N]",
|
|
37025
|
+
description: "Collapse an expanded \u25B8[N] block (default: newest)",
|
|
37026
|
+
run: (ctx) => runFold(ctx, "collapse")
|
|
37027
|
+
},
|
|
37028
|
+
{
|
|
37029
|
+
name: "reconnect",
|
|
37030
|
+
usage: "/reconnect",
|
|
37031
|
+
description: "Reconnect to the server now (skip the backoff)",
|
|
37032
|
+
run: (ctx) => ctx.reconnect()
|
|
37033
|
+
},
|
|
37034
|
+
{
|
|
37035
|
+
name: "whoami",
|
|
37036
|
+
usage: "/whoami",
|
|
37037
|
+
description: "Show this machine account + its teams & projects",
|
|
37038
|
+
run: runWhoami
|
|
37039
|
+
},
|
|
37040
|
+
{
|
|
37041
|
+
name: "config",
|
|
37042
|
+
usage: "/config [show|init|set <k> <v>|delete <k>]",
|
|
37043
|
+
description: "View / init / update / delete profile config",
|
|
37044
|
+
run: runConfig
|
|
37045
|
+
},
|
|
37046
|
+
{
|
|
37047
|
+
name: "claude-cmd",
|
|
37048
|
+
usage: "/claude-cmd /context",
|
|
37049
|
+
description: "Run an AI-CLI slash command (e.g. /context, /usage, /status)",
|
|
37050
|
+
run: (ctx) => {
|
|
37051
|
+
const rest = ctx.args.join(" ").trim();
|
|
37052
|
+
if (!rest) {
|
|
37053
|
+
ctx.print("usage: /claude-cmd /context (forwards to the AI CLI)", "error");
|
|
37054
|
+
return;
|
|
37055
|
+
}
|
|
37056
|
+
ctx.submitAi(rest);
|
|
37057
|
+
}
|
|
37058
|
+
},
|
|
37059
|
+
{
|
|
37060
|
+
name: "clear",
|
|
37061
|
+
usage: "/clear",
|
|
37062
|
+
description: "Clear the transcript",
|
|
37063
|
+
run: (ctx) => ctx.clear()
|
|
37064
|
+
},
|
|
37065
|
+
{
|
|
37066
|
+
name: "quit",
|
|
37067
|
+
usage: "/quit",
|
|
37068
|
+
description: "Quit the cli (also /exit, Ctrl+C)",
|
|
37069
|
+
run: (ctx) => ctx.quit()
|
|
37070
|
+
}
|
|
37071
|
+
];
|
|
37072
|
+
var ALIASES = { exit: "quit" };
|
|
37073
|
+
var SLASH_COMMANDS = COMMANDS.map((c) => ({ name: c.name, usage: c.usage, description: c.description }));
|
|
37074
|
+
function runSlashCommand(line, base) {
|
|
37075
|
+
const tokens = line.replace(/^\//, "").trim().split(/\s+/);
|
|
37076
|
+
const rawName = tokens[0] ?? "";
|
|
37077
|
+
const name = ALIASES[rawName] ?? rawName;
|
|
37078
|
+
const command = COMMANDS.find((c) => c.name === name);
|
|
37079
|
+
const ctx = { ...base, args: tokens.slice(1) };
|
|
37080
|
+
if (!command) {
|
|
37081
|
+
ctx.print(`Unknown command: /${rawName} \u2014 try /help`, "error");
|
|
37082
|
+
return;
|
|
37083
|
+
}
|
|
37084
|
+
command.run(ctx);
|
|
37085
|
+
}
|
|
37086
|
+
|
|
37087
|
+
// src/core/memory-compact.ts
|
|
37088
|
+
import { spawn as spawn8 } from "child_process";
|
|
37089
|
+
var COMPACT_TIMEOUT_MS = 6e4;
|
|
37090
|
+
function buildPrompt3(input) {
|
|
37091
|
+
return [
|
|
37092
|
+
"You maintain a COMPACT running memory of an ongoing assistant conversation so it can continue",
|
|
37093
|
+
"across sessions and accounts. Merge the latest exchange into the existing memory and return the",
|
|
37094
|
+
`UPDATED memory. Keep it UNDER ${input.budgetChars} characters. Use terse Markdown bullets grouped`,
|
|
37095
|
+
"as: Decisions, Constraints, Open tasks, Glossary. Keep durable facts; drop chit-chat and anything",
|
|
37096
|
+
"now obsolete. Output ONLY the updated memory text \u2014 no preamble, no code fences.",
|
|
37097
|
+
"",
|
|
37098
|
+
"=== EXISTING MEMORY ===",
|
|
37099
|
+
input.oldMemory || "(empty)",
|
|
37100
|
+
"",
|
|
37101
|
+
"=== LATEST USER PROMPT ===",
|
|
37102
|
+
input.prompt,
|
|
37103
|
+
"",
|
|
37104
|
+
"=== LATEST ASSISTANT ANSWER ===",
|
|
37105
|
+
input.answer
|
|
37106
|
+
].join("\n");
|
|
37107
|
+
}
|
|
37108
|
+
function runOnce2(cmd, profile, prompt, cwd2, extraEnv) {
|
|
37109
|
+
return new Promise((resolve8, reject) => {
|
|
37110
|
+
const args = ["-p", ...profile?.model ? ["--model", profile.model] : []];
|
|
37111
|
+
const env3 = {
|
|
37112
|
+
...process.env,
|
|
37113
|
+
...extraEnv,
|
|
37114
|
+
...profile ? { CLAUDE_CONFIG_DIR: profile.dir } : {}
|
|
37115
|
+
};
|
|
37116
|
+
const child = spawn8(cmd, args, { stdio: ["pipe", "pipe", "pipe"], cwd: cwd2, env: env3 });
|
|
37117
|
+
let out = "";
|
|
37118
|
+
let err = "";
|
|
37119
|
+
const timer = setTimeout(() => {
|
|
37120
|
+
child.kill("SIGKILL");
|
|
37121
|
+
reject(new Error("memory compaction timed out"));
|
|
37122
|
+
}, COMPACT_TIMEOUT_MS);
|
|
37123
|
+
timer.unref();
|
|
37124
|
+
child.stdout.on("data", (d) => out += d.toString());
|
|
37125
|
+
child.stderr.on("data", (d) => err += d.toString());
|
|
37126
|
+
child.on("error", (e) => {
|
|
37127
|
+
clearTimeout(timer);
|
|
37128
|
+
reject(e);
|
|
37129
|
+
});
|
|
37130
|
+
child.on("close", (code) => {
|
|
37131
|
+
clearTimeout(timer);
|
|
37132
|
+
resolve8({ code: code ?? -1, out, err });
|
|
37133
|
+
});
|
|
37134
|
+
child.stdin.write(prompt);
|
|
37135
|
+
child.stdin.end();
|
|
37136
|
+
});
|
|
37137
|
+
}
|
|
37138
|
+
async function runMemoryCompaction(ai, cwd2, input) {
|
|
37139
|
+
const prompt = buildPrompt3(input);
|
|
37140
|
+
const attempts = ai.profiles.length > 0 ? ai.profiles : [null];
|
|
37141
|
+
for (let i = 0; i < attempts.length; i++) {
|
|
37142
|
+
try {
|
|
37143
|
+
const { code, out } = await runOnce2(ai.cmd, attempts[i], prompt, cwd2, ai.env);
|
|
37144
|
+
if (code === 0 && out.trim()) {
|
|
37145
|
+
reportToolResult(ai.cmd, true);
|
|
37146
|
+
return out.trim().slice(0, input.budgetChars);
|
|
37147
|
+
}
|
|
37148
|
+
if (i === attempts.length - 1) break;
|
|
37149
|
+
} catch {
|
|
37150
|
+
break;
|
|
37151
|
+
}
|
|
37152
|
+
}
|
|
37153
|
+
return "";
|
|
37154
|
+
}
|
|
37155
|
+
|
|
37156
|
+
// src/core/config-sync.ts
|
|
37157
|
+
import { existsSync as existsSync12, readFileSync as readFileSync15 } from "fs";
|
|
37158
|
+
import { join as join24 } from "path";
|
|
37159
|
+
var SERVER_MANAGED_KEYS = [
|
|
37160
|
+
"physicPath",
|
|
37161
|
+
"sessionSwitchPct",
|
|
37162
|
+
"perPromptTokenLimit",
|
|
37163
|
+
// Project AI-run timeout override mirror (ADR-0243) — the ws_token refreshes it; a Worker-config
|
|
37164
|
+
// edit must not fight it. NOTE: the machine-user's own `aiRunTimeoutSec` is NOT here (operator-editable).
|
|
37165
|
+
"projectAiRunTimeoutSec",
|
|
37166
|
+
// Project idle auto-clear override mirror (ADR-0244) — same rule; the machine-user's own
|
|
37167
|
+
// `autoClearIdleMinutes` is operator-editable and NOT listed here.
|
|
37168
|
+
"projectAutoClearIdleMinutes",
|
|
37169
|
+
// Project shared-AI-memory override mirrors (ADR-0245) — the machine-user's own `aiMemoryEnabled`
|
|
37170
|
+
// / `aiMemoryBudgetChars` are operator-editable and NOT listed here.
|
|
37171
|
+
"projectAiMemoryMode",
|
|
37172
|
+
"projectAiMemoryBudgetChars"
|
|
37173
|
+
];
|
|
37174
|
+
function readConfigText(profileDir2) {
|
|
37175
|
+
const path = join24(profileDir2, "config.json");
|
|
37176
|
+
if (existsSync12(path)) {
|
|
37177
|
+
try {
|
|
37178
|
+
return readFileSync15(path, "utf8");
|
|
37179
|
+
} catch {
|
|
37180
|
+
}
|
|
37181
|
+
}
|
|
37182
|
+
return JSON.stringify(defaultProfileConfig(), null, 2);
|
|
37183
|
+
}
|
|
37184
|
+
function applyConfigText(profileDir2, text) {
|
|
37185
|
+
let parsed;
|
|
37186
|
+
try {
|
|
37187
|
+
parsed = JSON.parse(text);
|
|
37188
|
+
} catch (err) {
|
|
37189
|
+
return { ok: false, error: `Invalid JSON: ${err.message}` };
|
|
37190
|
+
}
|
|
37191
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
37192
|
+
return { ok: false, error: "config.json must be a JSON object." };
|
|
37193
|
+
}
|
|
37194
|
+
const next = { ...parsed };
|
|
37195
|
+
const current = readProfileConfig(profileDir2);
|
|
37196
|
+
for (const key of SERVER_MANAGED_KEYS) {
|
|
37197
|
+
if (current[key] !== void 0) next[key] = current[key];
|
|
37198
|
+
else delete next[key];
|
|
37199
|
+
}
|
|
37200
|
+
overwriteProfileConfig(profileDir2, next);
|
|
37201
|
+
return { ok: true };
|
|
37202
|
+
}
|
|
37203
|
+
|
|
37204
|
+
// src/core/worker-tools.ts
|
|
37205
|
+
import { spawn as spawn9 } from "child_process";
|
|
37206
|
+
var NPM_NAME = /^(?:@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/;
|
|
37207
|
+
function run10(cmd, args, onLine, timeoutMs = 18e4) {
|
|
37208
|
+
return new Promise((resolve8) => {
|
|
37209
|
+
let out = "";
|
|
37210
|
+
let buf = "";
|
|
37211
|
+
let child = null;
|
|
37212
|
+
const done = (code) => {
|
|
37213
|
+
clearTimeout(timer);
|
|
37214
|
+
if (onLine && buf.trim()) onLine(buf.trim());
|
|
37215
|
+
resolve8({ code, out });
|
|
37216
|
+
};
|
|
37217
|
+
const timer = setTimeout(() => {
|
|
37218
|
+
child?.kill();
|
|
37219
|
+
if (onLine) onLine(`timed out after ${Math.round(timeoutMs / 1e3)}s`);
|
|
37220
|
+
done(124);
|
|
37221
|
+
}, timeoutMs);
|
|
37222
|
+
timer.unref?.();
|
|
37223
|
+
const feed = (d) => {
|
|
37224
|
+
const text = d.toString("utf8");
|
|
37225
|
+
out += text;
|
|
37226
|
+
if (!onLine) return;
|
|
37227
|
+
buf += text;
|
|
37228
|
+
let nl;
|
|
37229
|
+
while ((nl = buf.indexOf("\n")) >= 0) {
|
|
37230
|
+
const line = buf.slice(0, nl).replace(/\r$/, "");
|
|
37231
|
+
buf = buf.slice(nl + 1);
|
|
37232
|
+
onLine(line);
|
|
37233
|
+
}
|
|
37234
|
+
};
|
|
37235
|
+
try {
|
|
37236
|
+
child = spawn9(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
37237
|
+
child.stdout?.on("data", feed);
|
|
37238
|
+
child.stderr?.on("data", feed);
|
|
37239
|
+
child.on("exit", (code) => done(code ?? 0));
|
|
37240
|
+
child.on("error", () => done(127));
|
|
37241
|
+
} catch {
|
|
37242
|
+
done(127);
|
|
37243
|
+
}
|
|
37244
|
+
});
|
|
37245
|
+
}
|
|
37246
|
+
async function detectOne(cmd, versionArg) {
|
|
37247
|
+
const { code, out } = await run10(cmd, [versionArg], void 0, 15e3);
|
|
37248
|
+
if (code !== 0) return null;
|
|
37249
|
+
const first = out.split("\n").map((l) => l.trim()).find(Boolean);
|
|
37250
|
+
return first ?? "";
|
|
37251
|
+
}
|
|
37252
|
+
async function listExtras() {
|
|
37253
|
+
const { out } = await run10("npm", ["ls", "-g", "--depth=0", "--json"], void 0, 2e4);
|
|
37254
|
+
if (!out.trim()) return [];
|
|
37255
|
+
let deps = {};
|
|
37256
|
+
try {
|
|
37257
|
+
deps = JSON.parse(out).dependencies ?? {};
|
|
37258
|
+
} catch {
|
|
37259
|
+
return [];
|
|
37260
|
+
}
|
|
37261
|
+
const catalogPkgs = new Set(WORKER_TOOL_BUNDLED_GLOBALS);
|
|
37262
|
+
for (const t of WORKER_TOOL_CATALOG) {
|
|
37263
|
+
catalogPkgs.add(t.id);
|
|
37264
|
+
if (t.installPackage) catalogPkgs.add(t.installPackage);
|
|
37265
|
+
}
|
|
37266
|
+
return Object.entries(deps).filter(([name]) => !catalogPkgs.has(name)).map(([name, info]) => ({
|
|
37267
|
+
id: name,
|
|
37268
|
+
label: name,
|
|
36853
37269
|
category: "runtime",
|
|
36854
37270
|
installed: true,
|
|
36855
37271
|
version: info.version ?? null,
|
|
@@ -36927,7 +37343,16 @@ var CONSOLE_WATCH_LEASE_MS = 45e3;
|
|
|
36927
37343
|
var METRICS_SAMPLE_INTERVAL_MS = 5e3;
|
|
36928
37344
|
var METRICS_WATCH_LEASE_MS = 45e3;
|
|
36929
37345
|
function toDtoEntry(e) {
|
|
36930
|
-
return {
|
|
37346
|
+
return {
|
|
37347
|
+
id: e.id,
|
|
37348
|
+
source: e.source,
|
|
37349
|
+
kind: e.kind,
|
|
37350
|
+
text: e.text,
|
|
37351
|
+
level: e.level,
|
|
37352
|
+
resultKind: e.resultKind,
|
|
37353
|
+
// Processing time on a terminal `exit` entry (ADR-0249) — the web renders it inline.
|
|
37354
|
+
...e.durationMs != null ? { durationMs: e.durationMs } : {}
|
|
37355
|
+
};
|
|
36931
37356
|
}
|
|
36932
37357
|
var WsClient = class {
|
|
36933
37358
|
constructor(context) {
|
|
@@ -36994,6 +37419,8 @@ var WsClient = class {
|
|
|
36994
37419
|
orgReconnectMaxSec = null;
|
|
36995
37420
|
/** Env override for the reconnect cap (seconds); resolved once at construction. */
|
|
36996
37421
|
envReconnectMaxSec = envReconnectMaxSec();
|
|
37422
|
+
/** Process start (ms) — uptime for a web-dispatched `/status` (ADR-0249). */
|
|
37423
|
+
startedAtMs = Date.now();
|
|
36997
37424
|
credential;
|
|
36998
37425
|
/** The physic project folder root this cli serves (null = no project) — fs.list is
|
|
36999
37426
|
* scoped to it so the web FsPicker can only browse inward, never out. */
|
|
@@ -37351,8 +37778,8 @@ var WsClient = class {
|
|
|
37351
37778
|
const oldRoot = this.physicFolderPath(sync.oldName);
|
|
37352
37779
|
if (oldRoot) void uninstallCron(oldRoot).catch(() => void 0);
|
|
37353
37780
|
}
|
|
37354
|
-
if (sync.oldName) rmSync4(
|
|
37355
|
-
mkdirSync5(
|
|
37781
|
+
if (sync.oldName) rmSync4(join25(this.context.profileDir, sync.oldName), { recursive: true, force: true });
|
|
37782
|
+
mkdirSync5(join25(this.context.profileDir, sync.newName), { recursive: true });
|
|
37356
37783
|
this.physicRoot = this.physicFolderPath(sync.newName);
|
|
37357
37784
|
this.bus.setProject(sync.newName);
|
|
37358
37785
|
this.bus.log(`Physic project folder synced \u2192 ${sync.newName}`);
|
|
@@ -37363,7 +37790,7 @@ var WsClient = class {
|
|
|
37363
37790
|
if (del.name) {
|
|
37364
37791
|
const delRoot = this.physicFolderPath(del.name);
|
|
37365
37792
|
if (delRoot) void uninstallCron(delRoot).catch(() => void 0);
|
|
37366
|
-
rmSync4(
|
|
37793
|
+
rmSync4(join25(this.context.profileDir, del.name), { recursive: true, force: true });
|
|
37367
37794
|
this.bus.log(`Physic project folder deleted (project removed) \u2192 ${del.name}`);
|
|
37368
37795
|
}
|
|
37369
37796
|
void manageSshKey("delete", this.context.profileDir).catch(() => void 0);
|
|
@@ -37384,7 +37811,11 @@ var WsClient = class {
|
|
|
37384
37811
|
ai: dispatch.ai ?? false
|
|
37385
37812
|
});
|
|
37386
37813
|
if (dispatch.ai) {
|
|
37387
|
-
|
|
37814
|
+
if (dispatch.cmd.trimStart().startsWith("/")) {
|
|
37815
|
+
void this.runWebSlashCommand(dispatch.cmd, dispatch.commandId);
|
|
37816
|
+
} else {
|
|
37817
|
+
void this.runAiPrompt(dispatch.cmd, dispatch.commandId, "server", dispatch.aiOneShot ?? false);
|
|
37818
|
+
}
|
|
37388
37819
|
break;
|
|
37389
37820
|
}
|
|
37390
37821
|
recordCommand({
|
|
@@ -37399,6 +37830,7 @@ var WsClient = class {
|
|
|
37399
37830
|
text: `$ ${dispatch.cmd} ${dispatch.args.join(" ")}`.trimEnd()
|
|
37400
37831
|
});
|
|
37401
37832
|
this.bus.startBusy(dispatch.cmd);
|
|
37833
|
+
const cmdStartMs = Date.now();
|
|
37402
37834
|
void runCommand(dispatch, (out) => {
|
|
37403
37835
|
if (out.chunk) {
|
|
37404
37836
|
this.bus.push({ source: "server", kind: "out", text: out.chunk });
|
|
@@ -37413,7 +37845,8 @@ var WsClient = class {
|
|
|
37413
37845
|
source: "server",
|
|
37414
37846
|
kind: "exit",
|
|
37415
37847
|
text: code === 0 ? "\u2713 done" : `\u2717 failed (exit ${code})`,
|
|
37416
|
-
level: code === 0 ? "info" : "error"
|
|
37848
|
+
level: code === 0 ? "info" : "error",
|
|
37849
|
+
durationMs: Date.now() - cmdStartMs
|
|
37417
37850
|
});
|
|
37418
37851
|
}
|
|
37419
37852
|
});
|
|
@@ -37736,7 +38169,7 @@ var WsClient = class {
|
|
|
37736
38169
|
}
|
|
37737
38170
|
case WsChannels.LOG_READ: {
|
|
37738
38171
|
const limit = Math.min(Math.max(payload.limit ?? 200, 1), 2e3);
|
|
37739
|
-
const lines = readRecentLogLines(
|
|
38172
|
+
const lines = readRecentLogLines(join25(this.context.profileDir, "logs"), limit);
|
|
37740
38173
|
this.send(WsChannels.LOG_READ, { lines }, message.id);
|
|
37741
38174
|
break;
|
|
37742
38175
|
}
|
|
@@ -37815,7 +38248,7 @@ var WsClient = class {
|
|
|
37815
38248
|
* exactly like typing the prompt in the cli (tries profiles, meters real tokens) instead
|
|
37816
38249
|
* of spawning the text as a raw command. Streams output to the transcript AND the server.
|
|
37817
38250
|
*/
|
|
37818
|
-
async runAiPrompt(prompt, commandId, origin) {
|
|
38251
|
+
async runAiPrompt(prompt, commandId, origin, oneShot = false) {
|
|
37819
38252
|
const config = readProfileConfig(this.context.profileDir);
|
|
37820
38253
|
const projectTimeoutSec = config.projectAiRunTimeoutSec ?? 0;
|
|
37821
38254
|
const machineTimeoutSec = config.aiRunTimeoutSec ?? 0;
|
|
@@ -37887,7 +38320,7 @@ ${this.aiMemory}
|
|
|
37887
38320
|
|
|
37888
38321
|
${guardedPrompt}`;
|
|
37889
38322
|
}
|
|
37890
|
-
const plan = planAiRun(effectivePrompt, config, hint, resume);
|
|
38323
|
+
const plan = planAiRun(effectivePrompt, config, hint, resume, oneShot);
|
|
37891
38324
|
const markerArgs = plan.attempts[0]?.args ?? [];
|
|
37892
38325
|
this.bus.push({ source: origin, kind: "aireq", text: `${plan.cmd} \u2039 ${prompt}` });
|
|
37893
38326
|
logger.info("command.ai", { commandId, origin, cmd: plan.cmd, profiles: plan.attempts.length });
|
|
@@ -37903,6 +38336,7 @@ ${guardedPrompt}`;
|
|
|
37903
38336
|
}
|
|
37904
38337
|
recordCommand({ commandId, cmd: plan.cmd, args: markerArgs });
|
|
37905
38338
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
38339
|
+
const aiStartedMs = Date.now();
|
|
37906
38340
|
this.bus.startBusy(plan.cmd);
|
|
37907
38341
|
let seq = 0;
|
|
37908
38342
|
let responseHeaderShown = false;
|
|
@@ -37946,7 +38380,7 @@ ${guardedPrompt}`;
|
|
|
37946
38380
|
`, log: true });
|
|
37947
38381
|
},
|
|
37948
38382
|
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`;
|
|
38383
|
+
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
38384
|
this.bus.push({ source: origin, kind: "log", text, level: "warn" });
|
|
37951
38385
|
this.send(WsChannels.COMMAND_OUTPUT, { commandId, seq: seq++, chunk: `${text}
|
|
37952
38386
|
`, log: true });
|
|
@@ -38019,7 +38453,8 @@ ${guardedPrompt}`;
|
|
|
38019
38453
|
source: origin,
|
|
38020
38454
|
kind: "exit",
|
|
38021
38455
|
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"
|
|
38456
|
+
level: result.exitCode === 0 ? "info" : "error",
|
|
38457
|
+
durationMs: Date.now() - aiStartedMs
|
|
38023
38458
|
});
|
|
38024
38459
|
if (memCfg.enabled && result.exitCode === 0) {
|
|
38025
38460
|
if (result.workedKey && result.sessionId) {
|
|
@@ -38029,6 +38464,114 @@ ${guardedPrompt}`;
|
|
|
38029
38464
|
});
|
|
38030
38465
|
}
|
|
38031
38466
|
}
|
|
38467
|
+
/**
|
|
38468
|
+
* Run a `/…` 4pm-cli slash command dispatched from the web Console (ADR-0249). Mirrors the TUI's
|
|
38469
|
+
* `runSlashCommand` with a **server-origin** context that streams `print` output back over
|
|
38470
|
+
* command.output + console.sync, so a web user gets the same commands as an operator at the
|
|
38471
|
+
* machine — EXCEPT any the operator disabled via `webBlockedCommands`. `/claude-cmd <x>` forwards
|
|
38472
|
+
* `x` to the AI CLI (a real AI run). Fold ops (`/expand`/`/collapse`) are TUI-only — the web has
|
|
38473
|
+
* its own fold viewer — so they just print a hint. Interactive `confirm` (e.g. `/config init`) is
|
|
38474
|
+
* not supported from the web.
|
|
38475
|
+
*/
|
|
38476
|
+
async runWebSlashCommand(line, commandId) {
|
|
38477
|
+
const origin = "server";
|
|
38478
|
+
const config = readProfileConfig(this.context.profileDir);
|
|
38479
|
+
const trimmed = line.trim();
|
|
38480
|
+
const startedMs = Date.now();
|
|
38481
|
+
this.bus.push({ source: origin, kind: "cmd", text: trimmed });
|
|
38482
|
+
recordCommand({ commandId, cmd: trimmed, args: [] });
|
|
38483
|
+
let seq = 0;
|
|
38484
|
+
const emit = (text) => {
|
|
38485
|
+
appendCommandOutput(commandId, `${text}
|
|
38486
|
+
`);
|
|
38487
|
+
this.send(WsChannels.COMMAND_OUTPUT, { commandId, seq: seq++, chunk: `${text}
|
|
38488
|
+
` });
|
|
38489
|
+
};
|
|
38490
|
+
const settle = (exitCode) => {
|
|
38491
|
+
this.send(WsChannels.COMMAND_OUTPUT, { commandId, seq: seq++, chunk: "", done: true, exitCode });
|
|
38492
|
+
finishCommand(commandId, exitCode);
|
|
38493
|
+
this.send(WsChannels.COMMAND_HISTORY, {
|
|
38494
|
+
commandId,
|
|
38495
|
+
cmd: trimmed,
|
|
38496
|
+
args: [],
|
|
38497
|
+
status: exitCode === 0 ? "done" : "failed",
|
|
38498
|
+
exitCode
|
|
38499
|
+
});
|
|
38500
|
+
this.bus.push({
|
|
38501
|
+
source: origin,
|
|
38502
|
+
kind: "exit",
|
|
38503
|
+
text: exitCode === 0 ? "\u2713 done" : `\u2717 failed (exit ${exitCode})`,
|
|
38504
|
+
level: exitCode === 0 ? "info" : "error",
|
|
38505
|
+
durationMs: Date.now() - startedMs
|
|
38506
|
+
});
|
|
38507
|
+
};
|
|
38508
|
+
const rawName = trimmed.replace(/^\//, "").trim().split(/\s+/)[0]?.toLowerCase() ?? "";
|
|
38509
|
+
const name = rawName === "exit" ? "quit" : rawName;
|
|
38510
|
+
const blocked = resolveWebBlockedCommands(config);
|
|
38511
|
+
if (blocked.has(name) || blocked.has(rawName)) {
|
|
38512
|
+
const msg = `Command /${rawName} is disabled on this worker (blocked by the machine config).`;
|
|
38513
|
+
this.bus.push({ source: origin, kind: "log", text: msg, level: "error" });
|
|
38514
|
+
emit(msg);
|
|
38515
|
+
settle(1);
|
|
38516
|
+
return;
|
|
38517
|
+
}
|
|
38518
|
+
if (name === "claude-cmd") {
|
|
38519
|
+
const rest = trimmed.replace(/^\/claude-cmd\s*/i, "").trim();
|
|
38520
|
+
if (!rest) {
|
|
38521
|
+
emit("usage: /claude-cmd /context (forwards to the AI CLI)");
|
|
38522
|
+
settle(1);
|
|
38523
|
+
return;
|
|
38524
|
+
}
|
|
38525
|
+
await this.runAiPrompt(rest, commandId, origin, false);
|
|
38526
|
+
return;
|
|
38527
|
+
}
|
|
38528
|
+
const info = {
|
|
38529
|
+
version: CLI_VERSION,
|
|
38530
|
+
scope: this.bus.scope ?? "project",
|
|
38531
|
+
profile: basename4(this.context.profileDir),
|
|
38532
|
+
profileDir: this.context.profileDir,
|
|
38533
|
+
serverUrl: this.context.credential.serverUrl,
|
|
38534
|
+
physicPath: this.physicRoot ?? config.physicPath ?? null,
|
|
38535
|
+
aiCli: config.aiCli || "claude",
|
|
38536
|
+
whoami: async () => null
|
|
38537
|
+
};
|
|
38538
|
+
runSlashCommand(trimmed, {
|
|
38539
|
+
info,
|
|
38540
|
+
print: (text, level) => {
|
|
38541
|
+
this.bus.push({ source: origin, kind: "log", text, level });
|
|
38542
|
+
emit(text);
|
|
38543
|
+
},
|
|
38544
|
+
clear: () => {
|
|
38545
|
+
this.bus.clear();
|
|
38546
|
+
this.bus.clearSession();
|
|
38547
|
+
},
|
|
38548
|
+
quit: () => {
|
|
38549
|
+
emit("Quit requested from the web console \u2014 the worker cli is shutting down.");
|
|
38550
|
+
setTimeout(() => process.exit(0), 200);
|
|
38551
|
+
},
|
|
38552
|
+
confirm: (prompt) => {
|
|
38553
|
+
const note = `${prompt} \u2014 interactive prompts aren't supported from the web console; run it on the machine.`;
|
|
38554
|
+
this.bus.push({ source: origin, kind: "log", text: note, level: "warn" });
|
|
38555
|
+
emit(note);
|
|
38556
|
+
},
|
|
38557
|
+
submitAi: () => {
|
|
38558
|
+
},
|
|
38559
|
+
// only /claude-cmd uses this, and it is special-cased above
|
|
38560
|
+
reconnect: () => this.reconnectNow(),
|
|
38561
|
+
expand: () => emit("Use the web console's fold viewer to expand blocks."),
|
|
38562
|
+
collapse: () => emit("Use the web console's fold viewer to collapse blocks."),
|
|
38563
|
+
maxBlock: 0,
|
|
38564
|
+
live: {
|
|
38565
|
+
status: this.bus.status,
|
|
38566
|
+
scope: this.bus.scope ?? "",
|
|
38567
|
+
worker: this.bus.worker,
|
|
38568
|
+
project: this.bus.project,
|
|
38569
|
+
activeProfile: this.bus.activeProfile,
|
|
38570
|
+
startedAt: this.startedAtMs
|
|
38571
|
+
}
|
|
38572
|
+
});
|
|
38573
|
+
settle(0);
|
|
38574
|
+
}
|
|
38032
38575
|
/**
|
|
38033
38576
|
* Fold the latest exchange into the shared AI memory (ADR-0245): a background claude compaction to a
|
|
38034
38577
|
* budget-bounded summary, cached locally + written back to the server (`memory.update`). Best-effort —
|
|
@@ -38380,7 +38923,7 @@ ${guardedPrompt}`;
|
|
|
38380
38923
|
/** Create the physic folder at an absolute path if missing (best-effort). */
|
|
38381
38924
|
ensurePhysicFolderPath(folder) {
|
|
38382
38925
|
try {
|
|
38383
|
-
if (!
|
|
38926
|
+
if (!existsSync13(folder)) {
|
|
38384
38927
|
mkdirSync5(folder, { recursive: true });
|
|
38385
38928
|
this.bus.log(`Created physic project folder: ${folder}`);
|
|
38386
38929
|
}
|
|
@@ -38395,7 +38938,7 @@ ${guardedPrompt}`;
|
|
|
38395
38938
|
*/
|
|
38396
38939
|
physicFolderPath(projectName) {
|
|
38397
38940
|
const safe = projectName.replace(/[/\\]/g, "_").replace(/\.\./g, "_").trim();
|
|
38398
|
-
return safe ?
|
|
38941
|
+
return safe ? join25(this.context.profileDir, safe) : null;
|
|
38399
38942
|
}
|
|
38400
38943
|
/**
|
|
38401
38944
|
* Report machine.status: version + the state of the physic project being served
|
|
@@ -38409,7 +38952,7 @@ ${guardedPrompt}`;
|
|
|
38409
38952
|
projects: [],
|
|
38410
38953
|
version: CLI_VERSION,
|
|
38411
38954
|
physicPath,
|
|
38412
|
-
physicPathExists: physicPath ?
|
|
38955
|
+
physicPathExists: physicPath ? existsSync13(physicPath) : void 0,
|
|
38413
38956
|
// Real coarse state (ADR-0152): !paused and a recent tick (was hard-coded false).
|
|
38414
38957
|
autonomousRunning: physicPath ? isAutonomousRunning(physicPath) : false
|
|
38415
38958
|
});
|
|
@@ -38499,7 +39042,7 @@ ${guardedPrompt}`;
|
|
|
38499
39042
|
*/
|
|
38500
39043
|
uploadLog() {
|
|
38501
39044
|
try {
|
|
38502
|
-
const upload = readLogUpload(
|
|
39045
|
+
const upload = readLogUpload(join25(this.context.profileDir, "logs"));
|
|
38503
39046
|
if (!upload) return;
|
|
38504
39047
|
const sig = `${upload.fileName}:${upload.totalBytes}`;
|
|
38505
39048
|
if (sig === this.lastLogSig) return;
|
|
@@ -38528,9 +39071,9 @@ ${guardedPrompt}`;
|
|
|
38528
39071
|
|
|
38529
39072
|
// src/core/instance-lock.ts
|
|
38530
39073
|
import { readFileSync as readFileSync16, rmSync as rmSync5, writeFileSync as writeFileSync7 } from "fs";
|
|
38531
|
-
import { join as
|
|
39074
|
+
import { join as join26 } from "path";
|
|
38532
39075
|
function lockPath(profileDir2) {
|
|
38533
|
-
return
|
|
39076
|
+
return join26(profileDir2, "start.lock");
|
|
38534
39077
|
}
|
|
38535
39078
|
function isAlive(pid) {
|
|
38536
39079
|
try {
|
|
@@ -38830,7 +39373,7 @@ var SessionBus = class {
|
|
|
38830
39373
|
// src/core/control-server.ts
|
|
38831
39374
|
import { createServer } from "net";
|
|
38832
39375
|
import { rmSync as rmSync6 } from "fs";
|
|
38833
|
-
import { join as
|
|
39376
|
+
import { join as join27 } from "path";
|
|
38834
39377
|
|
|
38835
39378
|
// src/core/control-protocol.ts
|
|
38836
39379
|
var CONTROL_SOCKET_FILE = "control.sock";
|
|
@@ -38859,7 +39402,7 @@ function createFrameParser() {
|
|
|
38859
39402
|
|
|
38860
39403
|
// src/core/control-server.ts
|
|
38861
39404
|
function startControlServer(bus, profileDir2, info) {
|
|
38862
|
-
const socketPath =
|
|
39405
|
+
const socketPath = join27(profileDir2, CONTROL_SOCKET_FILE);
|
|
38863
39406
|
try {
|
|
38864
39407
|
rmSync6(socketPath, { force: true });
|
|
38865
39408
|
} catch {
|
|
@@ -44590,15 +45133,15 @@ var import_react21 = __toESM(require_react(), 1);
|
|
|
44590
45133
|
var import_react26 = __toESM(require_react(), 1);
|
|
44591
45134
|
|
|
44592
45135
|
// src/core/input-history.ts
|
|
44593
|
-
import { existsSync as
|
|
44594
|
-
import { join as
|
|
45136
|
+
import { existsSync as existsSync15, readFileSync as readFileSync18, writeFileSync as writeFileSync8 } from "fs";
|
|
45137
|
+
import { join as join28 } from "path";
|
|
44595
45138
|
var MAX_HISTORY = 200;
|
|
44596
45139
|
function historyPath(profileDir2) {
|
|
44597
|
-
return
|
|
45140
|
+
return join28(profileDir2, "input-history.json");
|
|
44598
45141
|
}
|
|
44599
45142
|
function loadInputHistory(profileDir2) {
|
|
44600
45143
|
const path = historyPath(profileDir2);
|
|
44601
|
-
if (!
|
|
45144
|
+
if (!existsSync15(path)) return [];
|
|
44602
45145
|
try {
|
|
44603
45146
|
const data = JSON.parse(readFileSync18(path, "utf8"));
|
|
44604
45147
|
return Array.isArray(data) ? data.filter((x) => typeof x === "string") : [];
|
|
@@ -44750,438 +45293,114 @@ function wrapToWidth(text, width) {
|
|
|
44750
45293
|
line = word.trimStart();
|
|
44751
45294
|
} else {
|
|
44752
45295
|
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;
|
|
45296
|
+
}
|
|
45006
45297
|
}
|
|
45007
|
-
|
|
45298
|
+
if (line) rows.push(line.trimEnd());
|
|
45299
|
+
return rows.length > 0 ? rows : [""];
|
|
45008
45300
|
}
|
|
45009
|
-
|
|
45010
|
-
|
|
45011
|
-
|
|
45012
|
-
|
|
45013
|
-
if (lines.length === 0) {
|
|
45014
|
-
ctx.print("No log lines yet.");
|
|
45015
|
-
return;
|
|
45301
|
+
var DETAIL_RE = /<detail\s+title="([^"]*)">([\s\S]*?)<\/detail>/g;
|
|
45302
|
+
function toSegments(raw) {
|
|
45303
|
+
if (!raw.includes("<detail")) {
|
|
45304
|
+
return raw.split("\n").map((text) => ({ text, collapsed: false }));
|
|
45016
45305
|
}
|
|
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
|
-
}
|
|
45306
|
+
const out = [];
|
|
45307
|
+
const re = new RegExp(DETAIL_RE);
|
|
45308
|
+
let last = 0;
|
|
45309
|
+
let m;
|
|
45310
|
+
while ((m = re.exec(raw)) !== null) {
|
|
45311
|
+
const before = raw.slice(last, m.index).replace(/\n$/, "");
|
|
45312
|
+
if (before.length > 0) for (const p of before.split("\n")) out.push({ text: p, collapsed: false });
|
|
45313
|
+
out.push({ text: m[1] || "details", collapsed: true, body: m[2] ?? "" });
|
|
45314
|
+
last = m.index + m[0].length;
|
|
45315
|
+
if (raw[last] === "\n") last += 1;
|
|
45034
45316
|
}
|
|
45317
|
+
const rest = raw.slice(last);
|
|
45318
|
+
if (rest.length > 0) for (const p of rest.split("\n")) out.push({ text: p, collapsed: false });
|
|
45319
|
+
if (out.length === 0) out.push({ text: "", collapsed: false });
|
|
45320
|
+
return out;
|
|
45035
45321
|
}
|
|
45036
|
-
function
|
|
45037
|
-
|
|
45038
|
-
|
|
45039
|
-
|
|
45040
|
-
|
|
45041
|
-
|
|
45042
|
-
}
|
|
45043
|
-
|
|
45044
|
-
|
|
45045
|
-
|
|
45322
|
+
function pushCollapsible(lines, entry, opts) {
|
|
45323
|
+
const { blockNo, title, body, expanded, firstRow, textWidth, keyBase } = opts;
|
|
45324
|
+
const base = { source: entry.source, level: entry.level, kind: entry.kind };
|
|
45325
|
+
lines.push({
|
|
45326
|
+
...base,
|
|
45327
|
+
key: `${keyBase}:m`,
|
|
45328
|
+
text: `${expanded ? "\u25BE" : "\u25B8"}[${blockNo}] ${title}`,
|
|
45329
|
+
showTag: firstRow,
|
|
45330
|
+
collapsed: true,
|
|
45331
|
+
spacer: false,
|
|
45332
|
+
blockIndex: blockNo
|
|
45046
45333
|
});
|
|
45334
|
+
if (!expanded) return;
|
|
45335
|
+
prettyResult(body).split("\n").forEach(
|
|
45336
|
+
(seg, i) => wrapToWidth(seg, textWidth).forEach(
|
|
45337
|
+
(row, j) => lines.push({ ...base, key: `${keyBase}:b:${i}:${j}`, text: row, showTag: false, collapsed: false, spacer: false })
|
|
45338
|
+
)
|
|
45339
|
+
);
|
|
45047
45340
|
}
|
|
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}`);
|
|
45341
|
+
function flattenEntries(entries2, textWidth, expanded = /* @__PURE__ */ new Set()) {
|
|
45342
|
+
const lines = [];
|
|
45343
|
+
let prevKey = null;
|
|
45344
|
+
let blockNo = 0;
|
|
45345
|
+
for (const entry of entries2) {
|
|
45346
|
+
const blockKey = `${entry.source}|${entry.kind}`;
|
|
45347
|
+
const newBlock = blockKey !== prevKey;
|
|
45348
|
+
if (newBlock && lines.length > 0) {
|
|
45349
|
+
lines.push({
|
|
45350
|
+
key: `sp:${entry.id}`,
|
|
45351
|
+
source: entry.source,
|
|
45352
|
+
level: entry.level,
|
|
45353
|
+
kind: entry.kind,
|
|
45354
|
+
text: "",
|
|
45355
|
+
showTag: false,
|
|
45356
|
+
collapsed: false,
|
|
45357
|
+
spacer: true
|
|
45358
|
+
});
|
|
45069
45359
|
}
|
|
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}`);
|
|
45360
|
+
prevKey = blockKey;
|
|
45361
|
+
let firstRow = newBlock;
|
|
45362
|
+
if (entry.kind === "result") {
|
|
45363
|
+
blockNo += 1;
|
|
45364
|
+
const count = prettyResult(entry.text).split("\n").length;
|
|
45365
|
+
const title = `result \xB7 ${entry.resultKind ?? "json"} \xB7 ${count} ${count === 1 ? "line" : "lines"}`;
|
|
45366
|
+
pushCollapsible(lines, entry, { blockNo, title, body: entry.text, expanded: expanded.has(blockNo), firstRow, textWidth, keyBase: entry.id });
|
|
45367
|
+
continue;
|
|
45093
45368
|
}
|
|
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");
|
|
45369
|
+
const raw = entry.kind === "out" ? entry.text.replace(/\n$/, "") : entry.text;
|
|
45370
|
+
toSegments(raw).forEach((seg, i) => {
|
|
45371
|
+
if (seg.collapsed) {
|
|
45372
|
+
blockNo += 1;
|
|
45373
|
+
pushCollapsible(lines, entry, { blockNo, title: seg.text, body: seg.body ?? "", expanded: expanded.has(blockNo), firstRow, textWidth, keyBase: `${entry.id}:${i}` });
|
|
45374
|
+
firstRow = false;
|
|
45151
45375
|
return;
|
|
45152
45376
|
}
|
|
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;
|
|
45377
|
+
wrapToWidth(seg.text, textWidth).forEach((row, j) => {
|
|
45378
|
+
lines.push({
|
|
45379
|
+
key: `${entry.id}:${i}:${j}`,
|
|
45380
|
+
source: entry.source,
|
|
45381
|
+
level: entry.level,
|
|
45382
|
+
kind: entry.kind,
|
|
45383
|
+
text: row,
|
|
45384
|
+
showTag: firstRow,
|
|
45385
|
+
collapsed: false,
|
|
45386
|
+
spacer: false
|
|
45387
|
+
});
|
|
45388
|
+
firstRow = false;
|
|
45389
|
+
});
|
|
45390
|
+
});
|
|
45180
45391
|
}
|
|
45181
|
-
|
|
45392
|
+
return lines;
|
|
45393
|
+
}
|
|
45394
|
+
function TranscriptLine({ line }) {
|
|
45395
|
+
const tag = sourceTag(line.source);
|
|
45396
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Box_default, { flexDirection: "row", children: [
|
|
45397
|
+
/* @__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 }),
|
|
45398
|
+
/* @__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 || " " }) })
|
|
45399
|
+
] });
|
|
45182
45400
|
}
|
|
45183
45401
|
|
|
45184
45402
|
// src/ui/command-input.tsx
|
|
45403
|
+
var import_react24 = __toESM(require_react(), 1);
|
|
45185
45404
|
var import_jsx_runtime3 = __toESM(require_jsx_runtime(), 1);
|
|
45186
45405
|
function Hint() {
|
|
45187
45406
|
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" });
|