@4pm/cli 1.5.0 → 1.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +983 -602
- 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,30 @@ 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: "ai-profile", usage: "/ai-profile [list|use <n>|reset]", description: "List AI profiles / switch which one runs" },
|
|
32994
|
+
{ name: "config", usage: "/config [show|init|set <k> <v>|delete <k>]", description: "View / init / update / delete profile config" },
|
|
32995
|
+
{ name: "claude-cmd", usage: "/claude-cmd /context", description: "Run an AI-CLI slash command (e.g. /context, /usage)" },
|
|
32996
|
+
{ name: "clear", usage: "/clear", description: "Clear the transcript" },
|
|
32997
|
+
{ name: "quit", usage: "/quit", description: "Quit the cli (also /exit)" }
|
|
32998
|
+
];
|
|
32999
|
+
var CLI_SLASH_COMMAND_NAMES = CLI_SLASH_COMMANDS.map((c) => c.name);
|
|
32962
33000
|
|
|
32963
33001
|
// vendor/@4pm/validation/schemas.ts
|
|
32964
33002
|
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 +33039,10 @@ var listWorkersQuerySchema = baseRequestSchema.extend({
|
|
|
33001
33039
|
/** Only workers whose live CPU/memory/disk usage % ≥ the given threshold (0–100). */
|
|
33002
33040
|
minCpuPct: external_exports.coerce.number().min(0).max(100).optional(),
|
|
33003
33041
|
minMemPct: external_exports.coerce.number().min(0).max(100).optional(),
|
|
33004
|
-
minDiskPct: external_exports.coerce.number().min(0).max(100).optional()
|
|
33042
|
+
minDiskPct: external_exports.coerce.number().min(0).max(100).optional(),
|
|
33043
|
+
/** Hide workers with no cli attached (clis = 0). The web defaults this on; unchecking the
|
|
33044
|
+
* "show empty CLIs" box drops it so empty workers are listed too. */
|
|
33045
|
+
hideEmptyClis: external_exports.enum(["true", "false"]).transform((v) => v === "true").optional()
|
|
33005
33046
|
});
|
|
33006
33047
|
var updateWorkerRequestSchema = external_exports.object({
|
|
33007
33048
|
name: external_exports.string().min(1).max(100).optional()
|
|
@@ -33792,7 +33833,7 @@ function decryptPayload(key, payloadBase64, nonceBase64) {
|
|
|
33792
33833
|
}
|
|
33793
33834
|
|
|
33794
33835
|
// src/core/ws-client.ts
|
|
33795
|
-
import { existsSync as
|
|
33836
|
+
import { existsSync as existsSync13, mkdirSync as mkdirSync5, rmSync as rmSync4 } from "fs";
|
|
33796
33837
|
import { randomUUID as randomCommandId } from "crypto";
|
|
33797
33838
|
|
|
33798
33839
|
// src/core/update-scheduler.ts
|
|
@@ -34014,11 +34055,29 @@ import { spawn as spawn2 } from "child_process";
|
|
|
34014
34055
|
var KILL_GRACE_MS = 5e3;
|
|
34015
34056
|
async function runCommand(dispatch, emit, opts) {
|
|
34016
34057
|
let seq = 0;
|
|
34017
|
-
|
|
34018
|
-
|
|
34019
|
-
|
|
34020
|
-
|
|
34058
|
+
let child;
|
|
34059
|
+
try {
|
|
34060
|
+
child = spawn2(dispatch.cmd, dispatch.args ?? [], {
|
|
34061
|
+
cwd: dispatch.path,
|
|
34062
|
+
env: { ...process.env, ...dispatch.env },
|
|
34063
|
+
shell: false
|
|
34064
|
+
});
|
|
34065
|
+
} catch (err) {
|
|
34066
|
+
emit({
|
|
34067
|
+
commandId: dispatch.commandId,
|
|
34068
|
+
seq: seq++,
|
|
34069
|
+
chunk: `spawn error: ${err.message}
|
|
34070
|
+
`,
|
|
34071
|
+
done: true,
|
|
34072
|
+
exitCode: -1
|
|
34073
|
+
});
|
|
34074
|
+
return;
|
|
34075
|
+
}
|
|
34076
|
+
child.stdin?.on("error", () => {
|
|
34021
34077
|
});
|
|
34078
|
+
if (opts?.stdin != null) {
|
|
34079
|
+
child.stdin?.write(opts.stdin);
|
|
34080
|
+
}
|
|
34022
34081
|
child.stdin?.end();
|
|
34023
34082
|
const timeoutMs = opts?.timeoutMs && opts.timeoutMs > 0 ? opts.timeoutMs : 0;
|
|
34024
34083
|
let timedOut = false;
|
|
@@ -34117,15 +34176,24 @@ function createAiStreamParser(cli) {
|
|
|
34117
34176
|
let buffer = "";
|
|
34118
34177
|
const acc = { tokens: 0, input: 0, output: 0, cacheRead: 0, cacheCreation: 0 };
|
|
34119
34178
|
let sessionId = "";
|
|
34179
|
+
let apiError = { isError: false, status: null };
|
|
34120
34180
|
function handleClaude(ev) {
|
|
34121
34181
|
if (typeof ev.session_id === "string" && ev.session_id) sessionId = ev.session_id;
|
|
34122
|
-
if (ev.type === "result"
|
|
34123
|
-
|
|
34124
|
-
|
|
34125
|
-
|
|
34126
|
-
|
|
34127
|
-
|
|
34128
|
-
|
|
34182
|
+
if (ev.type === "result") {
|
|
34183
|
+
if (ev.is_error === true) {
|
|
34184
|
+
apiError = {
|
|
34185
|
+
isError: true,
|
|
34186
|
+
status: typeof ev.api_error_status === "number" ? ev.api_error_status : null
|
|
34187
|
+
};
|
|
34188
|
+
}
|
|
34189
|
+
if (ev.usage) {
|
|
34190
|
+
acc.input = ev.usage.input_tokens ?? 0;
|
|
34191
|
+
acc.output = ev.usage.output_tokens ?? 0;
|
|
34192
|
+
acc.cacheRead = ev.usage.cache_read_input_tokens ?? 0;
|
|
34193
|
+
acc.cacheCreation = ev.usage.cache_creation_input_tokens ?? 0;
|
|
34194
|
+
acc.tokens = acc.input + acc.output;
|
|
34195
|
+
if (typeof ev.total_cost_usd === "number") acc.costUsd = ev.total_cost_usd;
|
|
34196
|
+
}
|
|
34129
34197
|
return "";
|
|
34130
34198
|
}
|
|
34131
34199
|
if (ev.type === "assistant") {
|
|
@@ -34185,7 +34253,8 @@ function createAiStreamParser(cli) {
|
|
|
34185
34253
|
return handled === null ? line : handled;
|
|
34186
34254
|
},
|
|
34187
34255
|
usage: () => ({ ...acc }),
|
|
34188
|
-
sessionId: () => sessionId
|
|
34256
|
+
sessionId: () => sessionId,
|
|
34257
|
+
apiError: () => ({ ...apiError })
|
|
34189
34258
|
};
|
|
34190
34259
|
}
|
|
34191
34260
|
function estimateTokens(text) {
|
|
@@ -34203,12 +34272,21 @@ function isSessionLimit(text) {
|
|
|
34203
34272
|
text
|
|
34204
34273
|
);
|
|
34205
34274
|
}
|
|
34206
|
-
function
|
|
34275
|
+
function isOutOfCredits(text) {
|
|
34276
|
+
return /out of (?:usage )?credits|credits[_ ]required|out[_ ]of[_ ]credits|insufficient credits/i.test(
|
|
34277
|
+
text
|
|
34278
|
+
);
|
|
34279
|
+
}
|
|
34280
|
+
function runAttempt(commandId, cmd, args, stdin, cwd2, env3, onChunk, timeoutMs) {
|
|
34207
34281
|
return new Promise((resolve8) => {
|
|
34208
|
-
|
|
34209
|
-
|
|
34210
|
-
|
|
34211
|
-
|
|
34282
|
+
runCommand(
|
|
34283
|
+
{ commandId, cmd, args, path: cwd2, env: env3 },
|
|
34284
|
+
(out) => {
|
|
34285
|
+
if (out.chunk) onChunk(out.chunk);
|
|
34286
|
+
if (out.done) resolve8(out.exitCode ?? -1);
|
|
34287
|
+
},
|
|
34288
|
+
{ stdin, ...timeoutMs > 0 ? { timeoutMs } : {} }
|
|
34289
|
+
).catch(() => resolve8(-1));
|
|
34212
34290
|
});
|
|
34213
34291
|
}
|
|
34214
34292
|
async function runAiFailover(plan, commandId, cwd2, handlers, timeoutMs = 0) {
|
|
@@ -34229,14 +34307,15 @@ async function runAiFailover(plan, commandId, cwd2, handlers, timeoutMs = 0) {
|
|
|
34229
34307
|
handlers.onChunk(text);
|
|
34230
34308
|
}
|
|
34231
34309
|
};
|
|
34232
|
-
finalExit = await runAttempt(commandId, attempt.cmd, attempt.args, cwd2, attempt.env, emit, timeoutMs);
|
|
34310
|
+
finalExit = await runAttempt(commandId, attempt.cmd, attempt.args, attempt.stdin, cwd2, attempt.env, emit, timeoutMs);
|
|
34233
34311
|
const tail = parser.flush();
|
|
34234
34312
|
if (tail) {
|
|
34235
34313
|
captured += tail;
|
|
34236
34314
|
handlers.onChunk(tail);
|
|
34237
34315
|
}
|
|
34238
34316
|
lastCaptured = captured;
|
|
34239
|
-
|
|
34317
|
+
const apiErr = parser.apiError();
|
|
34318
|
+
if (finalExit === 0 && !apiErr.isError) {
|
|
34240
34319
|
const usage = parser.usage();
|
|
34241
34320
|
if (usage.tokens === 0) usage.tokens = estimateTokens(captured);
|
|
34242
34321
|
reportToolResult(attempt.cmd, true);
|
|
@@ -34249,7 +34328,8 @@ async function runAiFailover(plan, commandId, cwd2, handlers, timeoutMs = 0) {
|
|
|
34249
34328
|
sessionId: parser.sessionId()
|
|
34250
34329
|
};
|
|
34251
34330
|
}
|
|
34252
|
-
|
|
34331
|
+
if (finalExit === 0 && apiErr.isError) finalExit = apiErr.status && apiErr.status > 0 ? apiErr.status : 1;
|
|
34332
|
+
const reason = isAuthFailure(captured) ? "auth" : apiErr.status === 429 || isOutOfCredits(captured) ? "credits" : isSessionLimit(captured) ? "limit" : "other";
|
|
34253
34333
|
if (i === total - 1) break;
|
|
34254
34334
|
handlers.onAttemptFail(attempt.label, reason);
|
|
34255
34335
|
}
|
|
@@ -34258,6 +34338,7 @@ async function runAiFailover(plan, commandId, cwd2, handlers, timeoutMs = 0) {
|
|
|
34258
34338
|
}
|
|
34259
34339
|
function summarizeAiFailure(captured, exitCode) {
|
|
34260
34340
|
if (isAuthFailure(captured)) return "Not logged in";
|
|
34341
|
+
if (isOutOfCredits(captured)) return "Out of usage credits";
|
|
34261
34342
|
if (isSessionLimit(captured)) return "Usage limit reached";
|
|
34262
34343
|
const firstLine = captured.split("\n").map((l) => l.trim()).find((l) => l.length > 0);
|
|
34263
34344
|
return firstLine || `exited ${exitCode}`;
|
|
@@ -34303,6 +34384,52 @@ function setWorkingCredential(profileDir2, key) {
|
|
|
34303
34384
|
} catch {
|
|
34304
34385
|
}
|
|
34305
34386
|
}
|
|
34387
|
+
var PINNED_CREDENTIAL_KEY = "__pinned__";
|
|
34388
|
+
function getPinnedCredential(profileDir2) {
|
|
34389
|
+
return readState(profileDir2)[PINNED_CREDENTIAL_KEY] ?? null;
|
|
34390
|
+
}
|
|
34391
|
+
function setPinnedCredential(profileDir2, key) {
|
|
34392
|
+
const state = readState(profileDir2);
|
|
34393
|
+
if (state[PINNED_CREDENTIAL_KEY] === key) return;
|
|
34394
|
+
state[PINNED_CREDENTIAL_KEY] = key;
|
|
34395
|
+
try {
|
|
34396
|
+
writeFileSync4(statePath(profileDir2), JSON.stringify(state, null, 2), "utf8");
|
|
34397
|
+
} catch {
|
|
34398
|
+
}
|
|
34399
|
+
}
|
|
34400
|
+
function clearPinnedCredential(profileDir2) {
|
|
34401
|
+
const state = readState(profileDir2);
|
|
34402
|
+
if (!(PINNED_CREDENTIAL_KEY in state)) return;
|
|
34403
|
+
delete state[PINNED_CREDENTIAL_KEY];
|
|
34404
|
+
try {
|
|
34405
|
+
writeFileSync4(statePath(profileDir2), JSON.stringify(state, null, 2), "utf8");
|
|
34406
|
+
} catch {
|
|
34407
|
+
}
|
|
34408
|
+
}
|
|
34409
|
+
|
|
34410
|
+
// src/utils/time.ts
|
|
34411
|
+
function formatTimestampInZone(date, timeZone) {
|
|
34412
|
+
const build = (tz) => {
|
|
34413
|
+
const parts = new Intl.DateTimeFormat("en-CA", {
|
|
34414
|
+
...tz ? { timeZone: tz } : {},
|
|
34415
|
+
hour12: false,
|
|
34416
|
+
year: "numeric",
|
|
34417
|
+
month: "2-digit",
|
|
34418
|
+
day: "2-digit",
|
|
34419
|
+
hour: "2-digit",
|
|
34420
|
+
minute: "2-digit",
|
|
34421
|
+
second: "2-digit"
|
|
34422
|
+
}).formatToParts(date);
|
|
34423
|
+
const get2 = (type) => parts.find((p) => p.type === type)?.value ?? "";
|
|
34424
|
+
const hour = get2("hour") === "24" ? "00" : get2("hour");
|
|
34425
|
+
return `${get2("year")}/${get2("month")}/${get2("day")} ${hour}:${get2("minute")}:${get2("second")}`;
|
|
34426
|
+
};
|
|
34427
|
+
try {
|
|
34428
|
+
return build(timeZone);
|
|
34429
|
+
} catch {
|
|
34430
|
+
return build();
|
|
34431
|
+
}
|
|
34432
|
+
}
|
|
34306
34433
|
|
|
34307
34434
|
// src/utils/ai-cli.ts
|
|
34308
34435
|
import { readFileSync as readFileSync8 } from "fs";
|
|
@@ -34313,6 +34440,42 @@ var REQUIRED_AI_ARGS = {
|
|
|
34313
34440
|
codex: ["exec", "--json"],
|
|
34314
34441
|
antigravity: []
|
|
34315
34442
|
};
|
|
34443
|
+
var ONE_SHOT_DISALLOWED_CLAUDE_TOOLS = [
|
|
34444
|
+
"Bash",
|
|
34445
|
+
"Edit",
|
|
34446
|
+
"Write",
|
|
34447
|
+
"Read",
|
|
34448
|
+
"Glob",
|
|
34449
|
+
"Grep",
|
|
34450
|
+
"NotebookEdit",
|
|
34451
|
+
"Task",
|
|
34452
|
+
"WebSearch",
|
|
34453
|
+
"WebFetch",
|
|
34454
|
+
"Skill",
|
|
34455
|
+
"SlashCommand",
|
|
34456
|
+
"ToolSearch",
|
|
34457
|
+
"TaskCreate",
|
|
34458
|
+
"TaskGet",
|
|
34459
|
+
"TaskList",
|
|
34460
|
+
"TaskOutput",
|
|
34461
|
+
"TaskStop",
|
|
34462
|
+
"TaskUpdate",
|
|
34463
|
+
"Monitor",
|
|
34464
|
+
"DesignSync",
|
|
34465
|
+
"CronCreate",
|
|
34466
|
+
"CronDelete",
|
|
34467
|
+
"CronList",
|
|
34468
|
+
"EnterWorktree",
|
|
34469
|
+
"ExitWorktree",
|
|
34470
|
+
"RemoteTrigger",
|
|
34471
|
+
"ScheduleWakeup",
|
|
34472
|
+
"SendMessage",
|
|
34473
|
+
"PushNotification"
|
|
34474
|
+
].join(",");
|
|
34475
|
+
function oneShotArgs(cmd) {
|
|
34476
|
+
if (!cmd.includes("claude")) return [];
|
|
34477
|
+
return ["--max-turns=1", "--disallowedTools", ONE_SHOT_DISALLOWED_CLAUDE_TOOLS, "--permission-mode", "default"];
|
|
34478
|
+
}
|
|
34316
34479
|
function requiredArgs(cmd) {
|
|
34317
34480
|
if (cmd.includes("claude")) return REQUIRED_AI_ARGS.claude;
|
|
34318
34481
|
if (cmd.includes("codex")) return REQUIRED_AI_ARGS.codex;
|
|
@@ -34372,12 +34535,15 @@ function folderScopeGuard(folder, prompt) {
|
|
|
34372
34535
|
prompt
|
|
34373
34536
|
].join("\n");
|
|
34374
34537
|
}
|
|
34375
|
-
function buildRunArgs(profile, cmd,
|
|
34538
|
+
function buildRunArgs(profile, cmd, resumeId, oneShot = false) {
|
|
34376
34539
|
const extras = profile.args ?? [];
|
|
34377
34540
|
const model = profile.model?.trim();
|
|
34378
34541
|
const modelArgs = model ? ["--model", model] : [];
|
|
34379
34542
|
const resumeArgs = resumeId && cmd.includes("claude") ? ["--resume", resumeId] : [];
|
|
34380
|
-
|
|
34543
|
+
const turns = profile.maxTurns;
|
|
34544
|
+
const perProfileTurns = !oneShot && cmd.includes("claude") && typeof turns === "number" && turns > 0 ? [`--max-turns=${Math.floor(turns)}`] : [];
|
|
34545
|
+
const oneShotExtra = oneShot ? oneShotArgs(cmd) : perProfileTurns;
|
|
34546
|
+
return [...requiredArgs(cmd), ...resumeArgs, ...extras, ...modelArgs, ...oneShotExtra];
|
|
34381
34547
|
}
|
|
34382
34548
|
function claudeHomeDirs(config) {
|
|
34383
34549
|
const dirs = profileDirs(claudeProfilesOf(config));
|
|
@@ -34416,10 +34582,10 @@ function resolveClaudeProfiles(config, workingDir) {
|
|
|
34416
34582
|
if (idx > 0) profiles.unshift(profiles.splice(idx, 1)[0]);
|
|
34417
34583
|
return profiles;
|
|
34418
34584
|
}
|
|
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);
|
|
34585
|
+
function planAiRun(prompt, config, hint = {}, resume = /* @__PURE__ */ new Map(), oneShot = false) {
|
|
34586
|
+
return isUnifiedConfig(config) ? planUnifiedRun(prompt, config, hint.credential ?? null, resume, oneShot) : planLegacyRun(prompt, config, hint.dir ?? null, resume, oneShot);
|
|
34421
34587
|
}
|
|
34422
|
-
function planUnifiedRun(prompt, config, workingCredential, resume) {
|
|
34588
|
+
function planUnifiedRun(prompt, config, workingCredential, resume, oneShot) {
|
|
34423
34589
|
const baseEnv = config.aiEnv ?? {};
|
|
34424
34590
|
const active = activeProvider(config);
|
|
34425
34591
|
const usable = config.aiProfiles.filter(isUsableCredential);
|
|
@@ -34428,7 +34594,7 @@ function planUnifiedRun(prompt, config, workingCredential, resume) {
|
|
|
34428
34594
|
return {
|
|
34429
34595
|
cmd: active,
|
|
34430
34596
|
attempts: [
|
|
34431
|
-
{ cmd: active, label: "default", dir: null, key: null, args: buildRunArgs({ profile: "" }, active,
|
|
34597
|
+
{ cmd: active, label: "default", dir: null, key: null, args: buildRunArgs({ profile: "" }, active, void 0, oneShot), stdin: prompt, env: { ...baseEnv } }
|
|
34432
34598
|
]
|
|
34433
34599
|
};
|
|
34434
34600
|
}
|
|
@@ -34446,19 +34612,20 @@ function planUnifiedRun(prompt, config, workingCredential, resume) {
|
|
|
34446
34612
|
label: cred.label?.trim() || profileDisplayLabel(dir2),
|
|
34447
34613
|
dir: dir2,
|
|
34448
34614
|
key,
|
|
34449
|
-
args: buildRunArgs(cred, cmd,
|
|
34615
|
+
args: buildRunArgs(cred, cmd, resume.get(key), oneShot),
|
|
34616
|
+
stdin: prompt,
|
|
34450
34617
|
env: envVar ? { ...baseEnv, [envVar]: dir2 } : { ...baseEnv }
|
|
34451
34618
|
}));
|
|
34452
34619
|
return { cmd: attempts[0]?.cmd ?? DEFAULT_AI_CLI, attempts };
|
|
34453
34620
|
}
|
|
34454
|
-
function planLegacyRun(prompt, config, workingDir, resume) {
|
|
34621
|
+
function planLegacyRun(prompt, config, workingDir, resume, oneShot) {
|
|
34455
34622
|
const cmd = config.aiCli || DEFAULT_AI_CLI;
|
|
34456
34623
|
const baseEnv = config.aiEnv ?? {};
|
|
34457
34624
|
const envVar = profileEnvVar(cmd);
|
|
34458
34625
|
const defaultPlan = {
|
|
34459
34626
|
cmd,
|
|
34460
34627
|
attempts: [
|
|
34461
|
-
{ cmd, label: "default", dir: null, key: null, args: buildRunArgs({ profile: "" }, cmd,
|
|
34628
|
+
{ cmd, label: "default", dir: null, key: null, args: buildRunArgs({ profile: "" }, cmd, void 0, oneShot), stdin: prompt, env: { ...baseEnv } }
|
|
34462
34629
|
]
|
|
34463
34630
|
};
|
|
34464
34631
|
if (!envVar) return defaultPlan;
|
|
@@ -34475,7 +34642,8 @@ function planLegacyRun(prompt, config, workingDir, resume) {
|
|
|
34475
34642
|
label: profileDisplayLabel(dir2),
|
|
34476
34643
|
dir: dir2,
|
|
34477
34644
|
key,
|
|
34478
|
-
args: buildRunArgs(profile, cmd,
|
|
34645
|
+
args: buildRunArgs(profile, cmd, resume.get(key), oneShot),
|
|
34646
|
+
stdin: prompt,
|
|
34479
34647
|
env: { ...baseEnv, [envVar]: dir2 }
|
|
34480
34648
|
};
|
|
34481
34649
|
});
|
|
@@ -36668,138 +36836,530 @@ async function addProject(payload, profileDir2, onProgress) {
|
|
|
36668
36836
|
}
|
|
36669
36837
|
}
|
|
36670
36838
|
|
|
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";
|
|
36839
|
+
// src/ui/slash-commands.ts
|
|
36840
|
+
import { existsSync as existsSync11 } from "fs";
|
|
36742
36841
|
import { join as join23 } from "path";
|
|
36743
|
-
var
|
|
36842
|
+
var CONFIG_KEYS = [
|
|
36843
|
+
"aiCli",
|
|
36844
|
+
"claudeHome",
|
|
36845
|
+
"codexHome",
|
|
36846
|
+
"antigravityHome",
|
|
36744
36847
|
"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"
|
|
36848
|
+
"commandHistoryUploadMinutes",
|
|
36849
|
+
"autoUpdate",
|
|
36850
|
+
"aiEnv"
|
|
36757
36851
|
];
|
|
36758
|
-
function
|
|
36759
|
-
|
|
36760
|
-
|
|
36761
|
-
|
|
36762
|
-
|
|
36763
|
-
|
|
36852
|
+
function configPath(profileDir2) {
|
|
36853
|
+
return join23(profileDir2, "config.json");
|
|
36854
|
+
}
|
|
36855
|
+
function parseConfigAssignment(key, valueTokens) {
|
|
36856
|
+
const value = valueTokens.join(" ").trim();
|
|
36857
|
+
switch (key) {
|
|
36858
|
+
case "aiCli":
|
|
36859
|
+
case "physicPath":
|
|
36860
|
+
if (!value) return { error: `Missing value for "${key}".` };
|
|
36861
|
+
return { patch: { [key]: value } };
|
|
36862
|
+
case "claudeHome":
|
|
36863
|
+
case "codexHome":
|
|
36864
|
+
case "antigravityHome": {
|
|
36865
|
+
const dirs = valueTokens.filter((t) => t.trim());
|
|
36866
|
+
if (dirs.length === 0) return { error: `Missing value for "${key}".` };
|
|
36867
|
+
return { patch: { [key]: dirs.map((profile) => ({ profile, model: "" })) } };
|
|
36868
|
+
}
|
|
36869
|
+
case "commandHistoryUploadMinutes": {
|
|
36870
|
+
const n = Number(value);
|
|
36871
|
+
if (!Number.isFinite(n) || n <= 0) return { error: `"${value}" is not a positive number.` };
|
|
36872
|
+
return { patch: { commandHistoryUploadMinutes: n } };
|
|
36764
36873
|
}
|
|
36874
|
+
case "autoUpdate":
|
|
36875
|
+
if (value !== "true" && value !== "false") return { error: `Use true|false, not "${value}".` };
|
|
36876
|
+
return { patch: { autoUpdate: value === "true" } };
|
|
36877
|
+
case "aiEnv":
|
|
36878
|
+
try {
|
|
36879
|
+
return { patch: { aiEnv: JSON.parse(value) } };
|
|
36880
|
+
} catch {
|
|
36881
|
+
return { error: `aiEnv must be JSON, e.g. {"KEY":"val"}.` };
|
|
36882
|
+
}
|
|
36883
|
+
default:
|
|
36884
|
+
return { error: `Unknown key "${key}". Allowed: ${CONFIG_KEYS.join(", ")}.` };
|
|
36765
36885
|
}
|
|
36766
|
-
return JSON.stringify(defaultProfileConfig(), null, 2);
|
|
36767
36886
|
}
|
|
36768
|
-
function
|
|
36769
|
-
|
|
36770
|
-
|
|
36771
|
-
|
|
36772
|
-
|
|
36773
|
-
|
|
36887
|
+
function runConfig(ctx) {
|
|
36888
|
+
const [sub, ...rest] = ctx.args;
|
|
36889
|
+
const dir2 = ctx.info.profileDir;
|
|
36890
|
+
const path = configPath(dir2);
|
|
36891
|
+
if (!sub || sub === "show") {
|
|
36892
|
+
ctx.print(`config: ${path}`);
|
|
36893
|
+
const json = JSON.stringify(readProfileConfig(dir2), null, 2);
|
|
36894
|
+
for (const line of json.split("\n")) ctx.print(line);
|
|
36895
|
+
return;
|
|
36774
36896
|
}
|
|
36775
|
-
if (
|
|
36776
|
-
|
|
36897
|
+
if (sub === "init") {
|
|
36898
|
+
const writeDefaults = (verb) => {
|
|
36899
|
+
overwriteProfileConfig(dir2, defaultProfileConfig());
|
|
36900
|
+
ctx.print(`\u2714 ${verb} default config: ${path}`);
|
|
36901
|
+
ctx.print("add claude profiles: /config set claudeHome .claude .claude-1 (tried in order)");
|
|
36902
|
+
};
|
|
36903
|
+
if (existsSync11(path)) {
|
|
36904
|
+
ctx.confirm(
|
|
36905
|
+
`config exists: ${path} \u2014 replace it with defaults? (y/N)`,
|
|
36906
|
+
() => writeDefaults("replaced with")
|
|
36907
|
+
);
|
|
36908
|
+
return;
|
|
36909
|
+
}
|
|
36910
|
+
writeDefaults("wrote");
|
|
36911
|
+
return;
|
|
36777
36912
|
}
|
|
36778
|
-
|
|
36779
|
-
|
|
36780
|
-
|
|
36781
|
-
|
|
36782
|
-
|
|
36913
|
+
if (sub === "set") {
|
|
36914
|
+
const [key, ...valueTokens] = rest;
|
|
36915
|
+
if (!key) {
|
|
36916
|
+
ctx.print("usage: /config set <key> <value>", "error");
|
|
36917
|
+
return;
|
|
36918
|
+
}
|
|
36919
|
+
const result = parseConfigAssignment(key, valueTokens);
|
|
36920
|
+
if ("error" in result) {
|
|
36921
|
+
ctx.print(result.error, "error");
|
|
36922
|
+
return;
|
|
36923
|
+
}
|
|
36924
|
+
const merged = writeProfileConfig(dir2, result.patch);
|
|
36925
|
+
ctx.print(`\u2714 ${key} = ${JSON.stringify(merged[key])} (applies to the next prompt)`);
|
|
36926
|
+
return;
|
|
36783
36927
|
}
|
|
36784
|
-
|
|
36785
|
-
|
|
36928
|
+
if (sub === "delete" || sub === "unset") {
|
|
36929
|
+
const key = rest[0];
|
|
36930
|
+
if (!key) {
|
|
36931
|
+
ctx.print("usage: /config delete <key>", "error");
|
|
36932
|
+
return;
|
|
36933
|
+
}
|
|
36934
|
+
if (!CONFIG_KEYS.includes(key)) {
|
|
36935
|
+
ctx.print(`Unknown key "${key}". Allowed: ${CONFIG_KEYS.join(", ")}.`, "error");
|
|
36936
|
+
return;
|
|
36937
|
+
}
|
|
36938
|
+
deleteProfileConfigKey(dir2, key);
|
|
36939
|
+
ctx.print(`\u2714 deleted ${key}`);
|
|
36940
|
+
return;
|
|
36941
|
+
}
|
|
36942
|
+
ctx.print(`Unknown /config subcommand "${sub}". Use: show | init | set | delete.`, "error");
|
|
36786
36943
|
}
|
|
36787
|
-
|
|
36788
|
-
|
|
36789
|
-
|
|
36790
|
-
|
|
36791
|
-
|
|
36792
|
-
return
|
|
36793
|
-
|
|
36794
|
-
|
|
36795
|
-
|
|
36796
|
-
|
|
36797
|
-
|
|
36798
|
-
|
|
36799
|
-
|
|
36800
|
-
|
|
36801
|
-
|
|
36802
|
-
|
|
36944
|
+
function fmtDuration(totalSec) {
|
|
36945
|
+
const h = Math.floor(totalSec / 3600);
|
|
36946
|
+
const m = Math.floor(totalSec % 3600 / 60);
|
|
36947
|
+
const s = totalSec % 60;
|
|
36948
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
36949
|
+
return h > 0 ? `${h}h ${pad(m)}m ${pad(s)}s` : `${m}m ${pad(s)}s`;
|
|
36950
|
+
}
|
|
36951
|
+
function commandLine(cmd, args) {
|
|
36952
|
+
return `${cmd} ${args.join(" ")}`.trim();
|
|
36953
|
+
}
|
|
36954
|
+
function runHistory(ctx) {
|
|
36955
|
+
const n = Math.max(1, Math.min(50, Number(ctx.args[0]) || 15));
|
|
36956
|
+
const all = getCommandHistory();
|
|
36957
|
+
if (all.length === 0) {
|
|
36958
|
+
ctx.print("No commands run yet.");
|
|
36959
|
+
return;
|
|
36960
|
+
}
|
|
36961
|
+
const recent = all.slice(-n).reverse();
|
|
36962
|
+
ctx.print(`Last ${recent.length} of ${all.length} commands (newest first) \xB7 /output <n> to view:`);
|
|
36963
|
+
recent.forEach((e, i) => {
|
|
36964
|
+
const time = new Date(e.startedAt).toLocaleTimeString();
|
|
36965
|
+
const state = e.status === "done" ? "\u2713" : e.status === "failed" ? `\u2717 exit ${e.exitCode}` : "\xB7 running";
|
|
36966
|
+
ctx.print(` [${i + 1}] ${time} ${commandLine(e.cmd, e.args)} \xB7 ${state}`);
|
|
36967
|
+
});
|
|
36968
|
+
}
|
|
36969
|
+
function runOutput(ctx) {
|
|
36970
|
+
const all = getCommandHistory();
|
|
36971
|
+
const n = Number(ctx.args[0]);
|
|
36972
|
+
if (!Number.isInteger(n) || n < 1 || n > all.length) {
|
|
36973
|
+
ctx.print("usage: /output <n> (n = index from /history)", "error");
|
|
36974
|
+
return;
|
|
36975
|
+
}
|
|
36976
|
+
const entry = all.slice().reverse()[n - 1];
|
|
36977
|
+
const state = entry.exitCode != null ? ` (exit ${entry.exitCode})` : "";
|
|
36978
|
+
ctx.print(`\u2500\u2500 output [${n}] ${commandLine(entry.cmd, entry.args)} \xB7 ${entry.status}${state} \u2500\u2500`);
|
|
36979
|
+
const out = readCommandOutput(entry.commandId);
|
|
36980
|
+
if (out == null) {
|
|
36981
|
+
ctx.print("(no output stored \u2014 it ran in a previous session or produced none)", "warn");
|
|
36982
|
+
return;
|
|
36983
|
+
}
|
|
36984
|
+
for (const line of out.replace(/\n$/, "").split("\n")) ctx.print(line);
|
|
36985
|
+
}
|
|
36986
|
+
function runLogs(ctx) {
|
|
36987
|
+
const n = Math.max(1, Math.min(200, Number(ctx.args[0]) || 20));
|
|
36988
|
+
const dir2 = join23(ctx.info.profileDir, "logs");
|
|
36989
|
+
const lines = readRecentLogLines(dir2, n);
|
|
36990
|
+
if (lines.length === 0) {
|
|
36991
|
+
ctx.print("No log lines yet.");
|
|
36992
|
+
return;
|
|
36993
|
+
}
|
|
36994
|
+
ctx.print(`Last ${lines.length} log lines (${dir2}):`);
|
|
36995
|
+
for (const raw of lines) {
|
|
36996
|
+
try {
|
|
36997
|
+
const j = JSON.parse(raw);
|
|
36998
|
+
const time = String(j.timestamp ?? "").slice(11, 19);
|
|
36999
|
+
const level = String(j.level ?? "").padEnd(5);
|
|
37000
|
+
const { timestamp, level: _l, version, profile, scope, event, ...fields } = j;
|
|
37001
|
+
void timestamp;
|
|
37002
|
+
void _l;
|
|
37003
|
+
void version;
|
|
37004
|
+
void profile;
|
|
37005
|
+
void scope;
|
|
37006
|
+
const extra = Object.keys(fields).length ? " " + JSON.stringify(fields) : "";
|
|
37007
|
+
ctx.print(` ${time} ${level} ${String(event ?? "")}${extra}`);
|
|
37008
|
+
} catch {
|
|
37009
|
+
ctx.print(` ${raw}`);
|
|
37010
|
+
}
|
|
37011
|
+
}
|
|
37012
|
+
}
|
|
37013
|
+
function runWhoami(ctx) {
|
|
37014
|
+
ctx.print("whoami: fetching from the server\u2026");
|
|
37015
|
+
void ctx.info.whoami().then((w) => {
|
|
37016
|
+
if (!w) {
|
|
37017
|
+
ctx.print("whoami: unavailable (server unreachable or link invalid)", "warn");
|
|
37018
|
+
return;
|
|
37019
|
+
}
|
|
37020
|
+
ctx.print(`account: ${w.username} \xB7 scope: ${w.scope} \xB7 roles: ${w.roles.join(", ") || "\u2014"}`);
|
|
37021
|
+
ctx.print(`teams: ${w.teams.length ? w.teams.map((t) => t.name).join(", ") : "(none)"}`);
|
|
37022
|
+
ctx.print(`projects: ${w.projects.length ? w.projects.map((p) => p.name).join(", ") : "(none)"}`);
|
|
37023
|
+
});
|
|
37024
|
+
}
|
|
37025
|
+
function aiProfileLabel(c) {
|
|
37026
|
+
return c.label?.trim() || profileDisplayLabel(resolveHomePath(c.profile));
|
|
37027
|
+
}
|
|
37028
|
+
function runAiProfile(ctx) {
|
|
37029
|
+
const dir2 = ctx.info.profileDir;
|
|
37030
|
+
const config = readProfileConfig(dir2);
|
|
37031
|
+
const list = Array.isArray(config.aiProfiles) ? config.aiProfiles : [];
|
|
37032
|
+
const [sub, ...rest] = ctx.args;
|
|
37033
|
+
const action = (sub ?? "list").toLowerCase();
|
|
37034
|
+
if (action === "list") {
|
|
37035
|
+
if (list.length === 0) {
|
|
37036
|
+
ctx.print("No AI profiles configured (config.aiProfiles is empty).", "warn");
|
|
37037
|
+
ctx.print("This worker may use a legacy per-provider config \u2014 /config show to inspect.");
|
|
37038
|
+
return;
|
|
37039
|
+
}
|
|
37040
|
+
const pinned = getPinnedCredential(dir2);
|
|
37041
|
+
const working = getWorkingCredential(dir2);
|
|
37042
|
+
ctx.print("AI profiles (config order = failover priority) \xB7 /ai-profile use <n> to switch:");
|
|
37043
|
+
list.forEach((c, i) => {
|
|
37044
|
+
const usable = isUsableCredential(c);
|
|
37045
|
+
const key = usable ? credentialKey(c.provider, resolveHomePath(c.profile)) : null;
|
|
37046
|
+
const flags = [];
|
|
37047
|
+
if (key && key === pinned) flags.push("pinned");
|
|
37048
|
+
else if (key && !pinned && key === working) flags.push("active");
|
|
37049
|
+
if (c.enabled === false) flags.push("disabled");
|
|
37050
|
+
const model = c.model?.trim() ? ` \xB7 model=${c.model.trim()}` : "";
|
|
37051
|
+
const flagStr = flags.length ? ` (${flags.join(", ")})` : "";
|
|
37052
|
+
const marker = key && key === pinned ? "*" : " ";
|
|
37053
|
+
ctx.print(` ${marker}[${i + 1}] ${c.provider.padEnd(11)} ${aiProfileLabel(c)}${model}${flagStr}`);
|
|
37054
|
+
});
|
|
37055
|
+
if (pinned) {
|
|
37056
|
+
ctx.print("A profile is pinned \u2014 tried first every prompt; /ai-profile reset returns to auto.");
|
|
37057
|
+
}
|
|
37058
|
+
return;
|
|
37059
|
+
}
|
|
37060
|
+
if (action === "reset" || action === "auto" || action === "clear") {
|
|
37061
|
+
clearPinnedCredential(dir2);
|
|
37062
|
+
ctx.print("\u2714 AI profile pin cleared \u2014 failover is back to automatic (config order / remembered).");
|
|
37063
|
+
const working = getWorkingCredential(dir2);
|
|
37064
|
+
ctx.setActiveProfile(working ? labelFromCredentialKey(working) : null);
|
|
37065
|
+
return;
|
|
37066
|
+
}
|
|
37067
|
+
if (action === "use" || action === "switch") {
|
|
37068
|
+
const n = Number(rest[0]);
|
|
37069
|
+
if (!Number.isInteger(n) || n < 1 || n > list.length) {
|
|
37070
|
+
ctx.print(`usage: /ai-profile use <n> (n = 1-${list.length || "?"} from /ai-profile list)`, "error");
|
|
37071
|
+
return;
|
|
37072
|
+
}
|
|
37073
|
+
const cred = list[n - 1];
|
|
37074
|
+
if (!isUsableCredential(cred)) {
|
|
37075
|
+
const why = cred.enabled === false ? "it is disabled" : "it has no usable profile dir / provider";
|
|
37076
|
+
ctx.print(`Profile [${n}] "${aiProfileLabel(cred)}" can't be used \u2014 ${why}.`, "error");
|
|
37077
|
+
return;
|
|
37078
|
+
}
|
|
37079
|
+
const credDir = resolveHomePath(cred.profile);
|
|
37080
|
+
setPinnedCredential(dir2, credentialKey(cred.provider, credDir));
|
|
37081
|
+
ctx.setActiveProfile(profileDisplayLabel(credDir));
|
|
37082
|
+
ctx.print(
|
|
37083
|
+
`\u2714 switched to ${cred.provider} profile "${aiProfileLabel(cred)}" \u2014 tried first from the next prompt (failover keeps the rest as backup).`
|
|
37084
|
+
);
|
|
37085
|
+
return;
|
|
37086
|
+
}
|
|
37087
|
+
ctx.print(`Unknown /ai-profile subcommand "${sub}". Use: list | use <n> | reset.`, "error");
|
|
37088
|
+
}
|
|
37089
|
+
function runFold(ctx, action) {
|
|
37090
|
+
if (ctx.maxBlock === 0) {
|
|
37091
|
+
ctx.print("No collapsible blocks on screen.", "warn");
|
|
37092
|
+
return;
|
|
37093
|
+
}
|
|
37094
|
+
const n = ctx.args[0] != null ? Number(ctx.args[0]) : ctx.maxBlock;
|
|
37095
|
+
if (!Number.isInteger(n) || n < 1 || n > ctx.maxBlock) {
|
|
37096
|
+
ctx.print(`usage: /${action} [1-${ctx.maxBlock}] (no number \u21D2 the newest block)`, "error");
|
|
37097
|
+
return;
|
|
37098
|
+
}
|
|
37099
|
+
if (action === "expand") ctx.expand(n);
|
|
37100
|
+
else ctx.collapse(n);
|
|
37101
|
+
}
|
|
37102
|
+
var COMMANDS = [
|
|
37103
|
+
{
|
|
37104
|
+
name: "help",
|
|
37105
|
+
usage: "/help",
|
|
37106
|
+
description: "List slash commands",
|
|
37107
|
+
run: (ctx) => {
|
|
37108
|
+
ctx.print("Slash commands:");
|
|
37109
|
+
for (const cmd of COMMANDS) ctx.print(` ${cmd.usage.padEnd(28)} ${cmd.description}`);
|
|
37110
|
+
}
|
|
37111
|
+
},
|
|
37112
|
+
{
|
|
37113
|
+
name: "version",
|
|
37114
|
+
usage: "/version",
|
|
37115
|
+
description: "Show the cli version",
|
|
37116
|
+
run: (ctx) => ctx.print(`4PM CLI v${ctx.info.version} \xB7 profile ${ctx.info.profile}`)
|
|
37117
|
+
},
|
|
37118
|
+
{
|
|
37119
|
+
name: "status",
|
|
37120
|
+
usage: "/status",
|
|
37121
|
+
description: "Show connection + profile + session status",
|
|
37122
|
+
run: (ctx) => {
|
|
37123
|
+
const s = ctx.live;
|
|
37124
|
+
const uptime = fmtDuration(Math.max(0, Math.floor((Date.now() - s.startedAt) / 1e3)));
|
|
37125
|
+
ctx.print(`connection: ${s.status}`);
|
|
37126
|
+
ctx.print(`scope: ${s.scope}`);
|
|
37127
|
+
ctx.print(`worker: ${s.worker ?? "(unknown)"}`);
|
|
37128
|
+
if (s.project) ctx.print(`project: ${s.project}`);
|
|
37129
|
+
ctx.print(`profile: ${ctx.info.profile}`);
|
|
37130
|
+
ctx.print(`server: ${ctx.info.serverUrl}`);
|
|
37131
|
+
ctx.print(`aiCli: ${ctx.info.aiCli}${s.activeProfile ? ` \xB7 profile ${s.activeProfile}` : ""}`);
|
|
37132
|
+
ctx.print(`serving: ${ctx.info.physicPath ?? "(none)"}`);
|
|
37133
|
+
ctx.print(`uptime: ${uptime} \xB7 commands: ${getCommandHistory().length}`);
|
|
37134
|
+
}
|
|
37135
|
+
},
|
|
37136
|
+
{
|
|
37137
|
+
name: "history",
|
|
37138
|
+
usage: "/history [N]",
|
|
37139
|
+
description: "List the last N executed commands (+ time)",
|
|
37140
|
+
run: runHistory
|
|
37141
|
+
},
|
|
37142
|
+
{
|
|
37143
|
+
name: "output",
|
|
37144
|
+
usage: "/output <n>",
|
|
37145
|
+
description: "Replay a command's output (n from /history)",
|
|
37146
|
+
run: runOutput
|
|
37147
|
+
},
|
|
37148
|
+
{
|
|
37149
|
+
name: "logs",
|
|
37150
|
+
usage: "/logs [N]",
|
|
37151
|
+
description: "Tail the profile's structured log",
|
|
37152
|
+
run: runLogs
|
|
37153
|
+
},
|
|
37154
|
+
{
|
|
37155
|
+
name: "expand",
|
|
37156
|
+
usage: "/expand [N]",
|
|
37157
|
+
description: "Expand a collapsed \u25B8[N] block (default: newest); pretty-prints json/code",
|
|
37158
|
+
run: (ctx) => runFold(ctx, "expand")
|
|
37159
|
+
},
|
|
37160
|
+
{
|
|
37161
|
+
name: "collapse",
|
|
37162
|
+
usage: "/collapse [N]",
|
|
37163
|
+
description: "Collapse an expanded \u25B8[N] block (default: newest)",
|
|
37164
|
+
run: (ctx) => runFold(ctx, "collapse")
|
|
37165
|
+
},
|
|
37166
|
+
{
|
|
37167
|
+
name: "reconnect",
|
|
37168
|
+
usage: "/reconnect",
|
|
37169
|
+
description: "Reconnect to the server now (skip the backoff)",
|
|
37170
|
+
run: (ctx) => ctx.reconnect()
|
|
37171
|
+
},
|
|
37172
|
+
{
|
|
37173
|
+
name: "whoami",
|
|
37174
|
+
usage: "/whoami",
|
|
37175
|
+
description: "Show this machine account + its teams & projects",
|
|
37176
|
+
run: runWhoami
|
|
37177
|
+
},
|
|
37178
|
+
{
|
|
37179
|
+
name: "ai-profile",
|
|
37180
|
+
usage: "/ai-profile [list|use <n>|reset]",
|
|
37181
|
+
description: "List AI profiles / switch which one runs (ADR-0250)",
|
|
37182
|
+
run: runAiProfile
|
|
37183
|
+
},
|
|
37184
|
+
{
|
|
37185
|
+
name: "config",
|
|
37186
|
+
usage: "/config [show|init|set <k> <v>|delete <k>]",
|
|
37187
|
+
description: "View / init / update / delete profile config",
|
|
37188
|
+
run: runConfig
|
|
37189
|
+
},
|
|
37190
|
+
{
|
|
37191
|
+
name: "claude-cmd",
|
|
37192
|
+
usage: "/claude-cmd /context",
|
|
37193
|
+
description: "Run an AI-CLI slash command (e.g. /context, /usage, /status)",
|
|
37194
|
+
run: (ctx) => {
|
|
37195
|
+
const rest = ctx.args.join(" ").trim();
|
|
37196
|
+
if (!rest) {
|
|
37197
|
+
ctx.print("usage: /claude-cmd /context (forwards to the AI CLI)", "error");
|
|
37198
|
+
return;
|
|
37199
|
+
}
|
|
37200
|
+
ctx.submitAi(rest);
|
|
37201
|
+
}
|
|
37202
|
+
},
|
|
37203
|
+
{
|
|
37204
|
+
name: "clear",
|
|
37205
|
+
usage: "/clear",
|
|
37206
|
+
description: "Clear the transcript",
|
|
37207
|
+
run: (ctx) => ctx.clear()
|
|
37208
|
+
},
|
|
37209
|
+
{
|
|
37210
|
+
name: "quit",
|
|
37211
|
+
usage: "/quit",
|
|
37212
|
+
description: "Quit the cli (also /exit, Ctrl+C)",
|
|
37213
|
+
run: (ctx) => ctx.quit()
|
|
37214
|
+
}
|
|
37215
|
+
];
|
|
37216
|
+
var ALIASES = { exit: "quit" };
|
|
37217
|
+
var SLASH_COMMANDS = COMMANDS.map((c) => ({ name: c.name, usage: c.usage, description: c.description }));
|
|
37218
|
+
function runSlashCommand(line, base) {
|
|
37219
|
+
const tokens = line.replace(/^\//, "").trim().split(/\s+/);
|
|
37220
|
+
const rawName = tokens[0] ?? "";
|
|
37221
|
+
const name = ALIASES[rawName] ?? rawName;
|
|
37222
|
+
const command = COMMANDS.find((c) => c.name === name);
|
|
37223
|
+
const ctx = { ...base, args: tokens.slice(1) };
|
|
37224
|
+
if (!command) {
|
|
37225
|
+
ctx.print(`Unknown command: /${rawName} \u2014 try /help`, "error");
|
|
37226
|
+
return;
|
|
37227
|
+
}
|
|
37228
|
+
command.run(ctx);
|
|
37229
|
+
}
|
|
37230
|
+
|
|
37231
|
+
// src/core/memory-compact.ts
|
|
37232
|
+
import { spawn as spawn8 } from "child_process";
|
|
37233
|
+
var COMPACT_TIMEOUT_MS = 6e4;
|
|
37234
|
+
function buildPrompt3(input) {
|
|
37235
|
+
return [
|
|
37236
|
+
"You maintain a COMPACT running memory of an ongoing assistant conversation so it can continue",
|
|
37237
|
+
"across sessions and accounts. Merge the latest exchange into the existing memory and return the",
|
|
37238
|
+
`UPDATED memory. Keep it UNDER ${input.budgetChars} characters. Use terse Markdown bullets grouped`,
|
|
37239
|
+
"as: Decisions, Constraints, Open tasks, Glossary. Keep durable facts; drop chit-chat and anything",
|
|
37240
|
+
"now obsolete. Output ONLY the updated memory text \u2014 no preamble, no code fences.",
|
|
37241
|
+
"",
|
|
37242
|
+
"=== EXISTING MEMORY ===",
|
|
37243
|
+
input.oldMemory || "(empty)",
|
|
37244
|
+
"",
|
|
37245
|
+
"=== LATEST USER PROMPT ===",
|
|
37246
|
+
input.prompt,
|
|
37247
|
+
"",
|
|
37248
|
+
"=== LATEST ASSISTANT ANSWER ===",
|
|
37249
|
+
input.answer
|
|
37250
|
+
].join("\n");
|
|
37251
|
+
}
|
|
37252
|
+
function runOnce2(cmd, profile, prompt, cwd2, extraEnv) {
|
|
37253
|
+
return new Promise((resolve8, reject) => {
|
|
37254
|
+
const args = ["-p", ...profile?.model ? ["--model", profile.model] : []];
|
|
37255
|
+
const env3 = {
|
|
37256
|
+
...process.env,
|
|
37257
|
+
...extraEnv,
|
|
37258
|
+
...profile ? { CLAUDE_CONFIG_DIR: profile.dir } : {}
|
|
37259
|
+
};
|
|
37260
|
+
const child = spawn8(cmd, args, { stdio: ["pipe", "pipe", "pipe"], cwd: cwd2, env: env3 });
|
|
37261
|
+
let out = "";
|
|
37262
|
+
let err = "";
|
|
37263
|
+
const timer = setTimeout(() => {
|
|
37264
|
+
child.kill("SIGKILL");
|
|
37265
|
+
reject(new Error("memory compaction timed out"));
|
|
37266
|
+
}, COMPACT_TIMEOUT_MS);
|
|
37267
|
+
timer.unref();
|
|
37268
|
+
child.stdout.on("data", (d) => out += d.toString());
|
|
37269
|
+
child.stderr.on("data", (d) => err += d.toString());
|
|
37270
|
+
child.on("error", (e) => {
|
|
37271
|
+
clearTimeout(timer);
|
|
37272
|
+
reject(e);
|
|
37273
|
+
});
|
|
37274
|
+
child.on("close", (code) => {
|
|
37275
|
+
clearTimeout(timer);
|
|
37276
|
+
resolve8({ code: code ?? -1, out, err });
|
|
37277
|
+
});
|
|
37278
|
+
child.stdin.write(prompt);
|
|
37279
|
+
child.stdin.end();
|
|
37280
|
+
});
|
|
37281
|
+
}
|
|
37282
|
+
async function runMemoryCompaction(ai, cwd2, input) {
|
|
37283
|
+
const prompt = buildPrompt3(input);
|
|
37284
|
+
const attempts = ai.profiles.length > 0 ? ai.profiles : [null];
|
|
37285
|
+
for (let i = 0; i < attempts.length; i++) {
|
|
37286
|
+
try {
|
|
37287
|
+
const { code, out } = await runOnce2(ai.cmd, attempts[i], prompt, cwd2, ai.env);
|
|
37288
|
+
if (code === 0 && out.trim()) {
|
|
37289
|
+
reportToolResult(ai.cmd, true);
|
|
37290
|
+
return out.trim().slice(0, input.budgetChars);
|
|
37291
|
+
}
|
|
37292
|
+
if (i === attempts.length - 1) break;
|
|
37293
|
+
} catch {
|
|
37294
|
+
break;
|
|
37295
|
+
}
|
|
37296
|
+
}
|
|
37297
|
+
return "";
|
|
37298
|
+
}
|
|
37299
|
+
|
|
37300
|
+
// src/core/config-sync.ts
|
|
37301
|
+
import { existsSync as existsSync12, readFileSync as readFileSync15 } from "fs";
|
|
37302
|
+
import { join as join24 } from "path";
|
|
37303
|
+
var SERVER_MANAGED_KEYS = [
|
|
37304
|
+
"physicPath",
|
|
37305
|
+
"sessionSwitchPct",
|
|
37306
|
+
"perPromptTokenLimit",
|
|
37307
|
+
// Project AI-run timeout override mirror (ADR-0243) — the ws_token refreshes it; a Worker-config
|
|
37308
|
+
// edit must not fight it. NOTE: the machine-user's own `aiRunTimeoutSec` is NOT here (operator-editable).
|
|
37309
|
+
"projectAiRunTimeoutSec",
|
|
37310
|
+
// Project idle auto-clear override mirror (ADR-0244) — same rule; the machine-user's own
|
|
37311
|
+
// `autoClearIdleMinutes` is operator-editable and NOT listed here.
|
|
37312
|
+
"projectAutoClearIdleMinutes",
|
|
37313
|
+
// Project shared-AI-memory override mirrors (ADR-0245) — the machine-user's own `aiMemoryEnabled`
|
|
37314
|
+
// / `aiMemoryBudgetChars` are operator-editable and NOT listed here.
|
|
37315
|
+
"projectAiMemoryMode",
|
|
37316
|
+
"projectAiMemoryBudgetChars"
|
|
37317
|
+
];
|
|
37318
|
+
function readConfigText(profileDir2) {
|
|
37319
|
+
const path = join24(profileDir2, "config.json");
|
|
37320
|
+
if (existsSync12(path)) {
|
|
37321
|
+
try {
|
|
37322
|
+
return readFileSync15(path, "utf8");
|
|
37323
|
+
} catch {
|
|
37324
|
+
}
|
|
37325
|
+
}
|
|
37326
|
+
return JSON.stringify(defaultProfileConfig(), null, 2);
|
|
37327
|
+
}
|
|
37328
|
+
function applyConfigText(profileDir2, text) {
|
|
37329
|
+
let parsed;
|
|
37330
|
+
try {
|
|
37331
|
+
parsed = JSON.parse(text);
|
|
37332
|
+
} catch (err) {
|
|
37333
|
+
return { ok: false, error: `Invalid JSON: ${err.message}` };
|
|
37334
|
+
}
|
|
37335
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
37336
|
+
return { ok: false, error: "config.json must be a JSON object." };
|
|
37337
|
+
}
|
|
37338
|
+
const next = { ...parsed };
|
|
37339
|
+
const current = readProfileConfig(profileDir2);
|
|
37340
|
+
for (const key of SERVER_MANAGED_KEYS) {
|
|
37341
|
+
if (current[key] !== void 0) next[key] = current[key];
|
|
37342
|
+
else delete next[key];
|
|
37343
|
+
}
|
|
37344
|
+
overwriteProfileConfig(profileDir2, next);
|
|
37345
|
+
return { ok: true };
|
|
37346
|
+
}
|
|
37347
|
+
|
|
37348
|
+
// src/core/worker-tools.ts
|
|
37349
|
+
import { spawn as spawn9 } from "child_process";
|
|
37350
|
+
var NPM_NAME = /^(?:@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/;
|
|
37351
|
+
function run10(cmd, args, onLine, timeoutMs = 18e4) {
|
|
37352
|
+
return new Promise((resolve8) => {
|
|
37353
|
+
let out = "";
|
|
37354
|
+
let buf = "";
|
|
37355
|
+
let child = null;
|
|
37356
|
+
const done = (code) => {
|
|
37357
|
+
clearTimeout(timer);
|
|
37358
|
+
if (onLine && buf.trim()) onLine(buf.trim());
|
|
37359
|
+
resolve8({ code, out });
|
|
37360
|
+
};
|
|
37361
|
+
const timer = setTimeout(() => {
|
|
37362
|
+
child?.kill();
|
|
36803
37363
|
if (onLine) onLine(`timed out after ${Math.round(timeoutMs / 1e3)}s`);
|
|
36804
37364
|
done(124);
|
|
36805
37365
|
}, timeoutMs);
|
|
@@ -36927,7 +37487,16 @@ var CONSOLE_WATCH_LEASE_MS = 45e3;
|
|
|
36927
37487
|
var METRICS_SAMPLE_INTERVAL_MS = 5e3;
|
|
36928
37488
|
var METRICS_WATCH_LEASE_MS = 45e3;
|
|
36929
37489
|
function toDtoEntry(e) {
|
|
36930
|
-
return {
|
|
37490
|
+
return {
|
|
37491
|
+
id: e.id,
|
|
37492
|
+
source: e.source,
|
|
37493
|
+
kind: e.kind,
|
|
37494
|
+
text: e.text,
|
|
37495
|
+
level: e.level,
|
|
37496
|
+
resultKind: e.resultKind,
|
|
37497
|
+
// Processing time on a terminal `exit` entry (ADR-0249) — the web renders it inline.
|
|
37498
|
+
...e.durationMs != null ? { durationMs: e.durationMs } : {}
|
|
37499
|
+
};
|
|
36931
37500
|
}
|
|
36932
37501
|
var WsClient = class {
|
|
36933
37502
|
constructor(context) {
|
|
@@ -36992,8 +37561,13 @@ var WsClient = class {
|
|
|
36992
37561
|
reconnectEntryId = null;
|
|
36993
37562
|
/** Org-configured reconnect cap (seconds) from the last ws_token — overrides env. */
|
|
36994
37563
|
orgReconnectMaxSec = null;
|
|
37564
|
+
/** Org timezone (IANA) from the last ws_token (ADR-0132) — a rented worker follows its renter's
|
|
37565
|
+
* org; used to stamp the AI-run start time on the `aireq` transcript marker (ADR-0249). */
|
|
37566
|
+
orgTimezone = "UTC";
|
|
36995
37567
|
/** Env override for the reconnect cap (seconds); resolved once at construction. */
|
|
36996
37568
|
envReconnectMaxSec = envReconnectMaxSec();
|
|
37569
|
+
/** Process start (ms) — uptime for a web-dispatched `/status` (ADR-0249). */
|
|
37570
|
+
startedAtMs = Date.now();
|
|
36997
37571
|
credential;
|
|
36998
37572
|
/** The physic project folder root this cli serves (null = no project) — fs.list is
|
|
36999
37573
|
* scoped to it so the web FsPicker can only browse inward, never out. */
|
|
@@ -37174,6 +37748,9 @@ var WsClient = class {
|
|
|
37174
37748
|
if (typeof token.reconnectMaxBackoffSec === "number") {
|
|
37175
37749
|
this.orgReconnectMaxSec = token.reconnectMaxBackoffSec;
|
|
37176
37750
|
}
|
|
37751
|
+
if (typeof token.timezone === "string" && token.timezone) {
|
|
37752
|
+
this.orgTimezone = token.timezone;
|
|
37753
|
+
}
|
|
37177
37754
|
if (typeof token.autoUpdateDaily === "boolean") {
|
|
37178
37755
|
this.updateScheduler.configure({
|
|
37179
37756
|
autoUpdateDaily: token.autoUpdateDaily,
|
|
@@ -37351,8 +37928,8 @@ var WsClient = class {
|
|
|
37351
37928
|
const oldRoot = this.physicFolderPath(sync.oldName);
|
|
37352
37929
|
if (oldRoot) void uninstallCron(oldRoot).catch(() => void 0);
|
|
37353
37930
|
}
|
|
37354
|
-
if (sync.oldName) rmSync4(
|
|
37355
|
-
mkdirSync5(
|
|
37931
|
+
if (sync.oldName) rmSync4(join25(this.context.profileDir, sync.oldName), { recursive: true, force: true });
|
|
37932
|
+
mkdirSync5(join25(this.context.profileDir, sync.newName), { recursive: true });
|
|
37356
37933
|
this.physicRoot = this.physicFolderPath(sync.newName);
|
|
37357
37934
|
this.bus.setProject(sync.newName);
|
|
37358
37935
|
this.bus.log(`Physic project folder synced \u2192 ${sync.newName}`);
|
|
@@ -37363,7 +37940,7 @@ var WsClient = class {
|
|
|
37363
37940
|
if (del.name) {
|
|
37364
37941
|
const delRoot = this.physicFolderPath(del.name);
|
|
37365
37942
|
if (delRoot) void uninstallCron(delRoot).catch(() => void 0);
|
|
37366
|
-
rmSync4(
|
|
37943
|
+
rmSync4(join25(this.context.profileDir, del.name), { recursive: true, force: true });
|
|
37367
37944
|
this.bus.log(`Physic project folder deleted (project removed) \u2192 ${del.name}`);
|
|
37368
37945
|
}
|
|
37369
37946
|
void manageSshKey("delete", this.context.profileDir).catch(() => void 0);
|
|
@@ -37384,7 +37961,11 @@ var WsClient = class {
|
|
|
37384
37961
|
ai: dispatch.ai ?? false
|
|
37385
37962
|
});
|
|
37386
37963
|
if (dispatch.ai) {
|
|
37387
|
-
|
|
37964
|
+
if (dispatch.cmd.trimStart().startsWith("/")) {
|
|
37965
|
+
void this.runWebSlashCommand(dispatch.cmd, dispatch.commandId);
|
|
37966
|
+
} else {
|
|
37967
|
+
void this.runAiPrompt(dispatch.cmd, dispatch.commandId, "server", dispatch.aiOneShot ?? false);
|
|
37968
|
+
}
|
|
37388
37969
|
break;
|
|
37389
37970
|
}
|
|
37390
37971
|
recordCommand({
|
|
@@ -37399,6 +37980,7 @@ var WsClient = class {
|
|
|
37399
37980
|
text: `$ ${dispatch.cmd} ${dispatch.args.join(" ")}`.trimEnd()
|
|
37400
37981
|
});
|
|
37401
37982
|
this.bus.startBusy(dispatch.cmd);
|
|
37983
|
+
const cmdStartMs = Date.now();
|
|
37402
37984
|
void runCommand(dispatch, (out) => {
|
|
37403
37985
|
if (out.chunk) {
|
|
37404
37986
|
this.bus.push({ source: "server", kind: "out", text: out.chunk });
|
|
@@ -37413,7 +37995,8 @@ var WsClient = class {
|
|
|
37413
37995
|
source: "server",
|
|
37414
37996
|
kind: "exit",
|
|
37415
37997
|
text: code === 0 ? "\u2713 done" : `\u2717 failed (exit ${code})`,
|
|
37416
|
-
level: code === 0 ? "info" : "error"
|
|
37998
|
+
level: code === 0 ? "info" : "error",
|
|
37999
|
+
durationMs: Date.now() - cmdStartMs
|
|
37417
38000
|
});
|
|
37418
38001
|
}
|
|
37419
38002
|
});
|
|
@@ -37736,7 +38319,7 @@ var WsClient = class {
|
|
|
37736
38319
|
}
|
|
37737
38320
|
case WsChannels.LOG_READ: {
|
|
37738
38321
|
const limit = Math.min(Math.max(payload.limit ?? 200, 1), 2e3);
|
|
37739
|
-
const lines = readRecentLogLines(
|
|
38322
|
+
const lines = readRecentLogLines(join25(this.context.profileDir, "logs"), limit);
|
|
37740
38323
|
this.send(WsChannels.LOG_READ, { lines }, message.id);
|
|
37741
38324
|
break;
|
|
37742
38325
|
}
|
|
@@ -37815,7 +38398,7 @@ var WsClient = class {
|
|
|
37815
38398
|
* exactly like typing the prompt in the cli (tries profiles, meters real tokens) instead
|
|
37816
38399
|
* of spawning the text as a raw command. Streams output to the transcript AND the server.
|
|
37817
38400
|
*/
|
|
37818
|
-
async runAiPrompt(prompt, commandId, origin) {
|
|
38401
|
+
async runAiPrompt(prompt, commandId, origin, oneShot = false) {
|
|
37819
38402
|
const config = readProfileConfig(this.context.profileDir);
|
|
37820
38403
|
const projectTimeoutSec = config.projectAiRunTimeoutSec ?? 0;
|
|
37821
38404
|
const machineTimeoutSec = config.aiRunTimeoutSec ?? 0;
|
|
@@ -37855,9 +38438,10 @@ var WsClient = class {
|
|
|
37855
38438
|
const unified = isUnifiedConfig(config);
|
|
37856
38439
|
const mode = config.aiFailoverMode ?? "remember";
|
|
37857
38440
|
const workingDir = unified ? null : getWorkingProfile(this.context.profileDir, cmd);
|
|
37858
|
-
const
|
|
38441
|
+
const pinnedCred = unified ? getPinnedCredential(this.context.profileDir) : null;
|
|
38442
|
+
const workingCred = unified ? pinnedCred ?? (mode === "remember" ? getWorkingCredential(this.context.profileDir) : null) : null;
|
|
37859
38443
|
const startDir = unified ? null : this.profileUnderSessionPressure(config, cmd, workingDir);
|
|
37860
|
-
const startCred = unified ? this.credentialUnderSessionPressure(config, workingCred) : null;
|
|
38444
|
+
const startCred = unified && !pinnedCred ? this.credentialUnderSessionPressure(config, workingCred) : null;
|
|
37861
38445
|
if (startDir && startDir !== workingDir) {
|
|
37862
38446
|
this.bus.push({
|
|
37863
38447
|
source: origin,
|
|
@@ -37887,9 +38471,10 @@ ${this.aiMemory}
|
|
|
37887
38471
|
|
|
37888
38472
|
${guardedPrompt}`;
|
|
37889
38473
|
}
|
|
37890
|
-
const plan = planAiRun(effectivePrompt, config, hint, resume);
|
|
38474
|
+
const plan = planAiRun(effectivePrompt, config, hint, resume, oneShot);
|
|
37891
38475
|
const markerArgs = plan.attempts[0]?.args ?? [];
|
|
37892
|
-
|
|
38476
|
+
const startStamp = formatTimestampInZone(/* @__PURE__ */ new Date(), this.orgTimezone);
|
|
38477
|
+
this.bus.push({ source: origin, kind: "aireq", text: `${startStamp} ${plan.cmd} \u2039 ${prompt}` });
|
|
37893
38478
|
logger.info("command.ai", { commandId, origin, cmd: plan.cmd, profiles: plan.attempts.length });
|
|
37894
38479
|
if (origin === "local") {
|
|
37895
38480
|
this.send(WsChannels.COMMAND_ANNOUNCE, {
|
|
@@ -37903,6 +38488,7 @@ ${guardedPrompt}`;
|
|
|
37903
38488
|
}
|
|
37904
38489
|
recordCommand({ commandId, cmd: plan.cmd, args: markerArgs });
|
|
37905
38490
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
38491
|
+
const aiStartedMs = Date.now();
|
|
37906
38492
|
this.bus.startBusy(plan.cmd);
|
|
37907
38493
|
let seq = 0;
|
|
37908
38494
|
let responseHeaderShown = false;
|
|
@@ -37946,7 +38532,7 @@ ${guardedPrompt}`;
|
|
|
37946
38532
|
`, log: true });
|
|
37947
38533
|
},
|
|
37948
38534
|
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`;
|
|
38535
|
+
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
38536
|
this.bus.push({ source: origin, kind: "log", text, level: "warn" });
|
|
37951
38537
|
this.send(WsChannels.COMMAND_OUTPUT, { commandId, seq: seq++, chunk: `${text}
|
|
37952
38538
|
`, log: true });
|
|
@@ -38019,7 +38605,8 @@ ${guardedPrompt}`;
|
|
|
38019
38605
|
source: origin,
|
|
38020
38606
|
kind: "exit",
|
|
38021
38607
|
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"
|
|
38608
|
+
level: result.exitCode === 0 ? "info" : "error",
|
|
38609
|
+
durationMs: Date.now() - aiStartedMs
|
|
38023
38610
|
});
|
|
38024
38611
|
if (memCfg.enabled && result.exitCode === 0) {
|
|
38025
38612
|
if (result.workedKey && result.sessionId) {
|
|
@@ -38029,6 +38616,116 @@ ${guardedPrompt}`;
|
|
|
38029
38616
|
});
|
|
38030
38617
|
}
|
|
38031
38618
|
}
|
|
38619
|
+
/**
|
|
38620
|
+
* Run a `/…` 4pm-cli slash command dispatched from the web Console (ADR-0249). Mirrors the TUI's
|
|
38621
|
+
* `runSlashCommand` with a **server-origin** context that streams `print` output back over
|
|
38622
|
+
* command.output + console.sync, so a web user gets the same commands as an operator at the
|
|
38623
|
+
* machine — EXCEPT any the operator disabled via `webBlockedCommands`. `/claude-cmd <x>` forwards
|
|
38624
|
+
* `x` to the AI CLI (a real AI run). Fold ops (`/expand`/`/collapse`) are TUI-only — the web has
|
|
38625
|
+
* its own fold viewer — so they just print a hint. Interactive `confirm` (e.g. `/config init`) is
|
|
38626
|
+
* not supported from the web.
|
|
38627
|
+
*/
|
|
38628
|
+
async runWebSlashCommand(line, commandId) {
|
|
38629
|
+
const origin = "server";
|
|
38630
|
+
const config = readProfileConfig(this.context.profileDir);
|
|
38631
|
+
const trimmed = line.trim();
|
|
38632
|
+
const startedMs = Date.now();
|
|
38633
|
+
this.bus.push({ source: origin, kind: "cmd", text: trimmed });
|
|
38634
|
+
recordCommand({ commandId, cmd: trimmed, args: [] });
|
|
38635
|
+
let seq = 0;
|
|
38636
|
+
const emit = (text) => {
|
|
38637
|
+
appendCommandOutput(commandId, `${text}
|
|
38638
|
+
`);
|
|
38639
|
+
this.send(WsChannels.COMMAND_OUTPUT, { commandId, seq: seq++, chunk: `${text}
|
|
38640
|
+
` });
|
|
38641
|
+
};
|
|
38642
|
+
const settle = (exitCode) => {
|
|
38643
|
+
this.send(WsChannels.COMMAND_OUTPUT, { commandId, seq: seq++, chunk: "", done: true, exitCode });
|
|
38644
|
+
finishCommand(commandId, exitCode);
|
|
38645
|
+
this.send(WsChannels.COMMAND_HISTORY, {
|
|
38646
|
+
commandId,
|
|
38647
|
+
cmd: trimmed,
|
|
38648
|
+
args: [],
|
|
38649
|
+
status: exitCode === 0 ? "done" : "failed",
|
|
38650
|
+
exitCode
|
|
38651
|
+
});
|
|
38652
|
+
this.bus.push({
|
|
38653
|
+
source: origin,
|
|
38654
|
+
kind: "exit",
|
|
38655
|
+
text: exitCode === 0 ? "\u2713 done" : `\u2717 failed (exit ${exitCode})`,
|
|
38656
|
+
level: exitCode === 0 ? "info" : "error",
|
|
38657
|
+
durationMs: Date.now() - startedMs
|
|
38658
|
+
});
|
|
38659
|
+
};
|
|
38660
|
+
const rawName = trimmed.replace(/^\//, "").trim().split(/\s+/)[0]?.toLowerCase() ?? "";
|
|
38661
|
+
const name = rawName === "exit" ? "quit" : rawName;
|
|
38662
|
+
const blocked = resolveWebBlockedCommands(config);
|
|
38663
|
+
if (blocked.has(name) || blocked.has(rawName)) {
|
|
38664
|
+
const msg = `Command /${rawName} is disabled on this worker (blocked by the machine config).`;
|
|
38665
|
+
this.bus.push({ source: origin, kind: "log", text: msg, level: "error" });
|
|
38666
|
+
emit(msg);
|
|
38667
|
+
settle(1);
|
|
38668
|
+
return;
|
|
38669
|
+
}
|
|
38670
|
+
if (name === "claude-cmd") {
|
|
38671
|
+
const rest = trimmed.replace(/^\/claude-cmd\s*/i, "").trim();
|
|
38672
|
+
if (!rest) {
|
|
38673
|
+
emit("usage: /claude-cmd /context (forwards to the AI CLI)");
|
|
38674
|
+
settle(1);
|
|
38675
|
+
return;
|
|
38676
|
+
}
|
|
38677
|
+
await this.runAiPrompt(rest, commandId, origin, false);
|
|
38678
|
+
return;
|
|
38679
|
+
}
|
|
38680
|
+
const info = {
|
|
38681
|
+
version: CLI_VERSION,
|
|
38682
|
+
scope: this.bus.scope ?? "project",
|
|
38683
|
+
profile: basename4(this.context.profileDir),
|
|
38684
|
+
profileDir: this.context.profileDir,
|
|
38685
|
+
serverUrl: this.context.credential.serverUrl,
|
|
38686
|
+
physicPath: this.physicRoot ?? config.physicPath ?? null,
|
|
38687
|
+
aiCli: config.aiCli || "claude",
|
|
38688
|
+
whoami: async () => null
|
|
38689
|
+
};
|
|
38690
|
+
runSlashCommand(trimmed, {
|
|
38691
|
+
info,
|
|
38692
|
+
print: (text, level) => {
|
|
38693
|
+
this.bus.push({ source: origin, kind: "log", text, level });
|
|
38694
|
+
emit(text);
|
|
38695
|
+
},
|
|
38696
|
+
clear: () => {
|
|
38697
|
+
this.bus.clear();
|
|
38698
|
+
this.bus.clearSession();
|
|
38699
|
+
},
|
|
38700
|
+
quit: () => {
|
|
38701
|
+
emit("Quit requested from the web console \u2014 the worker cli is shutting down.");
|
|
38702
|
+
setTimeout(() => process.exit(0), 200);
|
|
38703
|
+
},
|
|
38704
|
+
confirm: (prompt) => {
|
|
38705
|
+
const note = `${prompt} \u2014 interactive prompts aren't supported from the web console; run it on the machine.`;
|
|
38706
|
+
this.bus.push({ source: origin, kind: "log", text: note, level: "warn" });
|
|
38707
|
+
emit(note);
|
|
38708
|
+
},
|
|
38709
|
+
submitAi: () => {
|
|
38710
|
+
},
|
|
38711
|
+
// only /claude-cmd uses this, and it is special-cased above
|
|
38712
|
+
// /ai-profile use/reset from the web Console updates the shared header too (ADR-0250).
|
|
38713
|
+
setActiveProfile: (label) => this.bus.setActiveProfile(label),
|
|
38714
|
+
reconnect: () => this.reconnectNow(),
|
|
38715
|
+
expand: () => emit("Use the web console's fold viewer to expand blocks."),
|
|
38716
|
+
collapse: () => emit("Use the web console's fold viewer to collapse blocks."),
|
|
38717
|
+
maxBlock: 0,
|
|
38718
|
+
live: {
|
|
38719
|
+
status: this.bus.status,
|
|
38720
|
+
scope: this.bus.scope ?? "",
|
|
38721
|
+
worker: this.bus.worker,
|
|
38722
|
+
project: this.bus.project,
|
|
38723
|
+
activeProfile: this.bus.activeProfile,
|
|
38724
|
+
startedAt: this.startedAtMs
|
|
38725
|
+
}
|
|
38726
|
+
});
|
|
38727
|
+
settle(0);
|
|
38728
|
+
}
|
|
38032
38729
|
/**
|
|
38033
38730
|
* Fold the latest exchange into the shared AI memory (ADR-0245): a background claude compaction to a
|
|
38034
38731
|
* budget-bounded summary, cached locally + written back to the server (`memory.update`). Best-effort —
|
|
@@ -38380,7 +39077,7 @@ ${guardedPrompt}`;
|
|
|
38380
39077
|
/** Create the physic folder at an absolute path if missing (best-effort). */
|
|
38381
39078
|
ensurePhysicFolderPath(folder) {
|
|
38382
39079
|
try {
|
|
38383
|
-
if (!
|
|
39080
|
+
if (!existsSync13(folder)) {
|
|
38384
39081
|
mkdirSync5(folder, { recursive: true });
|
|
38385
39082
|
this.bus.log(`Created physic project folder: ${folder}`);
|
|
38386
39083
|
}
|
|
@@ -38395,7 +39092,7 @@ ${guardedPrompt}`;
|
|
|
38395
39092
|
*/
|
|
38396
39093
|
physicFolderPath(projectName) {
|
|
38397
39094
|
const safe = projectName.replace(/[/\\]/g, "_").replace(/\.\./g, "_").trim();
|
|
38398
|
-
return safe ?
|
|
39095
|
+
return safe ? join25(this.context.profileDir, safe) : null;
|
|
38399
39096
|
}
|
|
38400
39097
|
/**
|
|
38401
39098
|
* Report machine.status: version + the state of the physic project being served
|
|
@@ -38409,7 +39106,7 @@ ${guardedPrompt}`;
|
|
|
38409
39106
|
projects: [],
|
|
38410
39107
|
version: CLI_VERSION,
|
|
38411
39108
|
physicPath,
|
|
38412
|
-
physicPathExists: physicPath ?
|
|
39109
|
+
physicPathExists: physicPath ? existsSync13(physicPath) : void 0,
|
|
38413
39110
|
// Real coarse state (ADR-0152): !paused and a recent tick (was hard-coded false).
|
|
38414
39111
|
autonomousRunning: physicPath ? isAutonomousRunning(physicPath) : false
|
|
38415
39112
|
});
|
|
@@ -38499,7 +39196,7 @@ ${guardedPrompt}`;
|
|
|
38499
39196
|
*/
|
|
38500
39197
|
uploadLog() {
|
|
38501
39198
|
try {
|
|
38502
|
-
const upload = readLogUpload(
|
|
39199
|
+
const upload = readLogUpload(join25(this.context.profileDir, "logs"));
|
|
38503
39200
|
if (!upload) return;
|
|
38504
39201
|
const sig = `${upload.fileName}:${upload.totalBytes}`;
|
|
38505
39202
|
if (sig === this.lastLogSig) return;
|
|
@@ -38528,9 +39225,9 @@ ${guardedPrompt}`;
|
|
|
38528
39225
|
|
|
38529
39226
|
// src/core/instance-lock.ts
|
|
38530
39227
|
import { readFileSync as readFileSync16, rmSync as rmSync5, writeFileSync as writeFileSync7 } from "fs";
|
|
38531
|
-
import { join as
|
|
39228
|
+
import { join as join26 } from "path";
|
|
38532
39229
|
function lockPath(profileDir2) {
|
|
38533
|
-
return
|
|
39230
|
+
return join26(profileDir2, "start.lock");
|
|
38534
39231
|
}
|
|
38535
39232
|
function isAlive(pid) {
|
|
38536
39233
|
try {
|
|
@@ -38830,7 +39527,7 @@ var SessionBus = class {
|
|
|
38830
39527
|
// src/core/control-server.ts
|
|
38831
39528
|
import { createServer } from "net";
|
|
38832
39529
|
import { rmSync as rmSync6 } from "fs";
|
|
38833
|
-
import { join as
|
|
39530
|
+
import { join as join27 } from "path";
|
|
38834
39531
|
|
|
38835
39532
|
// src/core/control-protocol.ts
|
|
38836
39533
|
var CONTROL_SOCKET_FILE = "control.sock";
|
|
@@ -38859,7 +39556,7 @@ function createFrameParser() {
|
|
|
38859
39556
|
|
|
38860
39557
|
// src/core/control-server.ts
|
|
38861
39558
|
function startControlServer(bus, profileDir2, info) {
|
|
38862
|
-
const socketPath =
|
|
39559
|
+
const socketPath = join27(profileDir2, CONTROL_SOCKET_FILE);
|
|
38863
39560
|
try {
|
|
38864
39561
|
rmSync6(socketPath, { force: true });
|
|
38865
39562
|
} catch {
|
|
@@ -44590,15 +45287,15 @@ var import_react21 = __toESM(require_react(), 1);
|
|
|
44590
45287
|
var import_react26 = __toESM(require_react(), 1);
|
|
44591
45288
|
|
|
44592
45289
|
// src/core/input-history.ts
|
|
44593
|
-
import { existsSync as
|
|
44594
|
-
import { join as
|
|
45290
|
+
import { existsSync as existsSync15, readFileSync as readFileSync18, writeFileSync as writeFileSync8 } from "fs";
|
|
45291
|
+
import { join as join28 } from "path";
|
|
44595
45292
|
var MAX_HISTORY = 200;
|
|
44596
45293
|
function historyPath(profileDir2) {
|
|
44597
|
-
return
|
|
45294
|
+
return join28(profileDir2, "input-history.json");
|
|
44598
45295
|
}
|
|
44599
45296
|
function loadInputHistory(profileDir2) {
|
|
44600
45297
|
const path = historyPath(profileDir2);
|
|
44601
|
-
if (!
|
|
45298
|
+
if (!existsSync15(path)) return [];
|
|
44602
45299
|
try {
|
|
44603
45300
|
const data = JSON.parse(readFileSync18(path, "utf8"));
|
|
44604
45301
|
return Array.isArray(data) ? data.filter((x) => typeof x === "string") : [];
|
|
@@ -44720,12 +45417,17 @@ function textColor(line) {
|
|
|
44720
45417
|
if (line.kind === "exit") return "green";
|
|
44721
45418
|
return void 0;
|
|
44722
45419
|
}
|
|
45420
|
+
var START_STAMP_RE = /^(\d{4}\/\d{2}\/\d{2} \d{2}:\d{2}:\d{2} )/;
|
|
44723
45421
|
function AiMarkerLine({ line }) {
|
|
44724
45422
|
const arrow = line.kind === "aireq" ? "\u2039" : "\u203A";
|
|
44725
45423
|
const idx = line.text.indexOf(arrow);
|
|
44726
45424
|
if (idx < 0) return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text, { children: line.text || " " });
|
|
45425
|
+
const before = line.text.slice(0, idx);
|
|
45426
|
+
const stamp = line.kind === "aireq" ? START_STAMP_RE.exec(before)?.[1] ?? "" : "";
|
|
45427
|
+
const name = stamp ? before.slice(stamp.length) : before;
|
|
44727
45428
|
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Text, { children: [
|
|
44728
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text, {
|
|
45429
|
+
stamp ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text, { dimColor: true, children: stamp }) : null,
|
|
45430
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text, { color: "cyan", bold: true, children: name }),
|
|
44729
45431
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text, { dimColor: true, children: arrow }),
|
|
44730
45432
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text, { children: line.text.slice(idx + 1) })
|
|
44731
45433
|
] });
|
|
@@ -44750,438 +45452,114 @@ function wrapToWidth(text, width) {
|
|
|
44750
45452
|
line = word.trimStart();
|
|
44751
45453
|
} else {
|
|
44752
45454
|
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;
|
|
45455
|
+
}
|
|
45006
45456
|
}
|
|
45007
|
-
|
|
45457
|
+
if (line) rows.push(line.trimEnd());
|
|
45458
|
+
return rows.length > 0 ? rows : [""];
|
|
45008
45459
|
}
|
|
45009
|
-
|
|
45010
|
-
|
|
45011
|
-
|
|
45012
|
-
|
|
45013
|
-
if (lines.length === 0) {
|
|
45014
|
-
ctx.print("No log lines yet.");
|
|
45015
|
-
return;
|
|
45460
|
+
var DETAIL_RE = /<detail\s+title="([^"]*)">([\s\S]*?)<\/detail>/g;
|
|
45461
|
+
function toSegments(raw) {
|
|
45462
|
+
if (!raw.includes("<detail")) {
|
|
45463
|
+
return raw.split("\n").map((text) => ({ text, collapsed: false }));
|
|
45016
45464
|
}
|
|
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
|
-
}
|
|
45465
|
+
const out = [];
|
|
45466
|
+
const re = new RegExp(DETAIL_RE);
|
|
45467
|
+
let last = 0;
|
|
45468
|
+
let m;
|
|
45469
|
+
while ((m = re.exec(raw)) !== null) {
|
|
45470
|
+
const before = raw.slice(last, m.index).replace(/\n$/, "");
|
|
45471
|
+
if (before.length > 0) for (const p of before.split("\n")) out.push({ text: p, collapsed: false });
|
|
45472
|
+
out.push({ text: m[1] || "details", collapsed: true, body: m[2] ?? "" });
|
|
45473
|
+
last = m.index + m[0].length;
|
|
45474
|
+
if (raw[last] === "\n") last += 1;
|
|
45034
45475
|
}
|
|
45476
|
+
const rest = raw.slice(last);
|
|
45477
|
+
if (rest.length > 0) for (const p of rest.split("\n")) out.push({ text: p, collapsed: false });
|
|
45478
|
+
if (out.length === 0) out.push({ text: "", collapsed: false });
|
|
45479
|
+
return out;
|
|
45035
45480
|
}
|
|
45036
|
-
function
|
|
45037
|
-
|
|
45038
|
-
|
|
45039
|
-
|
|
45040
|
-
|
|
45041
|
-
|
|
45042
|
-
}
|
|
45043
|
-
|
|
45044
|
-
|
|
45045
|
-
|
|
45481
|
+
function pushCollapsible(lines, entry, opts) {
|
|
45482
|
+
const { blockNo, title, body, expanded, firstRow, textWidth, keyBase } = opts;
|
|
45483
|
+
const base = { source: entry.source, level: entry.level, kind: entry.kind };
|
|
45484
|
+
lines.push({
|
|
45485
|
+
...base,
|
|
45486
|
+
key: `${keyBase}:m`,
|
|
45487
|
+
text: `${expanded ? "\u25BE" : "\u25B8"}[${blockNo}] ${title}`,
|
|
45488
|
+
showTag: firstRow,
|
|
45489
|
+
collapsed: true,
|
|
45490
|
+
spacer: false,
|
|
45491
|
+
blockIndex: blockNo
|
|
45046
45492
|
});
|
|
45493
|
+
if (!expanded) return;
|
|
45494
|
+
prettyResult(body).split("\n").forEach(
|
|
45495
|
+
(seg, i) => wrapToWidth(seg, textWidth).forEach(
|
|
45496
|
+
(row, j) => lines.push({ ...base, key: `${keyBase}:b:${i}:${j}`, text: row, showTag: false, collapsed: false, spacer: false })
|
|
45497
|
+
)
|
|
45498
|
+
);
|
|
45047
45499
|
}
|
|
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}`);
|
|
45500
|
+
function flattenEntries(entries2, textWidth, expanded = /* @__PURE__ */ new Set()) {
|
|
45501
|
+
const lines = [];
|
|
45502
|
+
let prevKey = null;
|
|
45503
|
+
let blockNo = 0;
|
|
45504
|
+
for (const entry of entries2) {
|
|
45505
|
+
const blockKey = `${entry.source}|${entry.kind}`;
|
|
45506
|
+
const newBlock = blockKey !== prevKey;
|
|
45507
|
+
if (newBlock && lines.length > 0) {
|
|
45508
|
+
lines.push({
|
|
45509
|
+
key: `sp:${entry.id}`,
|
|
45510
|
+
source: entry.source,
|
|
45511
|
+
level: entry.level,
|
|
45512
|
+
kind: entry.kind,
|
|
45513
|
+
text: "",
|
|
45514
|
+
showTag: false,
|
|
45515
|
+
collapsed: false,
|
|
45516
|
+
spacer: true
|
|
45517
|
+
});
|
|
45069
45518
|
}
|
|
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}`);
|
|
45519
|
+
prevKey = blockKey;
|
|
45520
|
+
let firstRow = newBlock;
|
|
45521
|
+
if (entry.kind === "result") {
|
|
45522
|
+
blockNo += 1;
|
|
45523
|
+
const count = prettyResult(entry.text).split("\n").length;
|
|
45524
|
+
const title = `result \xB7 ${entry.resultKind ?? "json"} \xB7 ${count} ${count === 1 ? "line" : "lines"}`;
|
|
45525
|
+
pushCollapsible(lines, entry, { blockNo, title, body: entry.text, expanded: expanded.has(blockNo), firstRow, textWidth, keyBase: entry.id });
|
|
45526
|
+
continue;
|
|
45093
45527
|
}
|
|
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");
|
|
45528
|
+
const raw = entry.kind === "out" ? entry.text.replace(/\n$/, "") : entry.text;
|
|
45529
|
+
toSegments(raw).forEach((seg, i) => {
|
|
45530
|
+
if (seg.collapsed) {
|
|
45531
|
+
blockNo += 1;
|
|
45532
|
+
pushCollapsible(lines, entry, { blockNo, title: seg.text, body: seg.body ?? "", expanded: expanded.has(blockNo), firstRow, textWidth, keyBase: `${entry.id}:${i}` });
|
|
45533
|
+
firstRow = false;
|
|
45151
45534
|
return;
|
|
45152
45535
|
}
|
|
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;
|
|
45536
|
+
wrapToWidth(seg.text, textWidth).forEach((row, j) => {
|
|
45537
|
+
lines.push({
|
|
45538
|
+
key: `${entry.id}:${i}:${j}`,
|
|
45539
|
+
source: entry.source,
|
|
45540
|
+
level: entry.level,
|
|
45541
|
+
kind: entry.kind,
|
|
45542
|
+
text: row,
|
|
45543
|
+
showTag: firstRow,
|
|
45544
|
+
collapsed: false,
|
|
45545
|
+
spacer: false
|
|
45546
|
+
});
|
|
45547
|
+
firstRow = false;
|
|
45548
|
+
});
|
|
45549
|
+
});
|
|
45180
45550
|
}
|
|
45181
|
-
|
|
45551
|
+
return lines;
|
|
45552
|
+
}
|
|
45553
|
+
function TranscriptLine({ line }) {
|
|
45554
|
+
const tag = sourceTag(line.source);
|
|
45555
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Box_default, { flexDirection: "row", children: [
|
|
45556
|
+
/* @__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 }),
|
|
45557
|
+
/* @__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 || " " }) })
|
|
45558
|
+
] });
|
|
45182
45559
|
}
|
|
45183
45560
|
|
|
45184
45561
|
// src/ui/command-input.tsx
|
|
45562
|
+
var import_react24 = __toESM(require_react(), 1);
|
|
45185
45563
|
var import_jsx_runtime3 = __toESM(require_jsx_runtime(), 1);
|
|
45186
45564
|
function Hint() {
|
|
45187
45565
|
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 +45844,9 @@ function App2({
|
|
|
45466
45844
|
setPendingConfirm({ prompt, onYes });
|
|
45467
45845
|
},
|
|
45468
45846
|
submitAi: (aiInput) => bus.submitLocal(aiInput),
|
|
45847
|
+
// /ai-profile use/reset updates the header's active-profile label at once (ADR-0250);
|
|
45848
|
+
// the bus event feeds the React state (see the onActiveProfile subscription above).
|
|
45849
|
+
setActiveProfile: (label) => bus.setActiveProfile(label),
|
|
45469
45850
|
reconnect: () => bus.requestReconnect(),
|
|
45470
45851
|
expand: expandFold,
|
|
45471
45852
|
collapse: collapseFold,
|