@spinalfluid/ultrac 0.1.6 → 0.1.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +77 -6
- package/dist/cli.js +1945 -513
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -286,7 +286,7 @@ var agentModePresets = {
|
|
|
286
286
|
"Prefer narrow, reversible changes.",
|
|
287
287
|
"Keep final answers concise and include verification status."
|
|
288
288
|
],
|
|
289
|
-
tools: [...coreTools, "list_sessions", "read_session", "search_sessions"],
|
|
289
|
+
tools: [...coreTools, "create_skill", "list_sessions", "read_session", "search_sessions"],
|
|
290
290
|
externalTools: true
|
|
291
291
|
},
|
|
292
292
|
plan: {
|
|
@@ -331,6 +331,7 @@ var agentModePresets = {
|
|
|
331
331
|
],
|
|
332
332
|
tools: [
|
|
333
333
|
...coreTools,
|
|
334
|
+
"create_skill",
|
|
334
335
|
"ast_search",
|
|
335
336
|
"repo_handbook",
|
|
336
337
|
"verification_plan",
|
|
@@ -548,6 +549,9 @@ function ensurePrivateDirectory(path) {
|
|
|
548
549
|
}
|
|
549
550
|
chmodSync(path, 448);
|
|
550
551
|
}
|
|
552
|
+
function currentDiagnosticLogPath() {
|
|
553
|
+
return diagnosticPath;
|
|
554
|
+
}
|
|
551
555
|
function diagnosticEvent(event, fields = {}) {
|
|
552
556
|
if (!diagnosticPath) return;
|
|
553
557
|
const record3 = {
|
|
@@ -1219,20 +1223,20 @@ function compactRun(input, options = {}) {
|
|
|
1219
1223
|
}
|
|
1220
1224
|
async function compactShape(shape, options) {
|
|
1221
1225
|
const reserveChars = options.summarise ? Math.max(0, options.reserveChars ?? shape.reserveChars ?? 0) : 0;
|
|
1222
|
-
const
|
|
1226
|
+
const plain2 = planKeepingSummaries(shape, shape.messages, reserveChars);
|
|
1223
1227
|
const skip = (reason) => ({
|
|
1224
|
-
...reserveChars > 0 ? planKeepingSummaries(shape, shape.messages, shape.reserveChars ?? 0) :
|
|
1228
|
+
...reserveChars > 0 ? planKeepingSummaries(shape, shape.messages, shape.reserveChars ?? 0) : plain2,
|
|
1225
1229
|
compacted: false,
|
|
1226
1230
|
summarisedMessages: 0,
|
|
1227
1231
|
summarisedTurns: 0,
|
|
1228
1232
|
skipped: reason
|
|
1229
1233
|
});
|
|
1230
|
-
if (
|
|
1234
|
+
if (plain2.dropped.length === 0) return skip("nothing-dropped");
|
|
1231
1235
|
if (!options.summarise) return skip("no-summariser");
|
|
1232
|
-
const source =
|
|
1236
|
+
const source = plain2.dropped.filter((message2) => !isCompactionSummary(message2));
|
|
1233
1237
|
if (source.length === 0) return skip("only-summaries");
|
|
1234
1238
|
const ceiling = options.maxSummaryChars ?? DEFAULT_MAX_SUMMARY_CHARS;
|
|
1235
|
-
const slack =
|
|
1239
|
+
const slack = plain2.usage.promptLimitChars - plain2.usage.totalChars - summaryEnvelopeChars();
|
|
1236
1240
|
const maxChars = Math.min(ceiling, slack);
|
|
1237
1241
|
if (maxChars < MIN_SUMMARY_CHARS) return skip("no-room");
|
|
1238
1242
|
const transcript = renderDroppedTranscript(source, {
|
|
@@ -1257,21 +1261,21 @@ async function compactShape(shape, options) {
|
|
|
1257
1261
|
const text = truncateToLine(summary.trim(), maxChars);
|
|
1258
1262
|
if (!text) return skip("summariser-empty");
|
|
1259
1263
|
const message = compactionSummaryMessage(text);
|
|
1260
|
-
const plan = replan(shape,
|
|
1264
|
+
const plan = replan(shape, plain2, message);
|
|
1261
1265
|
if (!plan) return skip("would-drop-more");
|
|
1262
1266
|
return {
|
|
1263
1267
|
...plan,
|
|
1264
1268
|
// The re-plan saw a transcript the span was already out of, so it has
|
|
1265
1269
|
// nothing to report about it. What was dropped is what `plain` dropped.
|
|
1266
|
-
dropped:
|
|
1267
|
-
droppedMessages:
|
|
1268
|
-
droppedTurns:
|
|
1269
|
-
droppedChars:
|
|
1270
|
+
dropped: plain2.dropped,
|
|
1271
|
+
droppedMessages: plain2.droppedMessages,
|
|
1272
|
+
droppedTurns: plain2.droppedTurns,
|
|
1273
|
+
droppedChars: plain2.droppedChars,
|
|
1270
1274
|
compacted: true,
|
|
1271
1275
|
summary: text,
|
|
1272
|
-
summarisedMessages:
|
|
1273
|
-
summarisedTurns:
|
|
1274
|
-
warning: compactionWarning(plan,
|
|
1276
|
+
summarisedMessages: plain2.dropped.length,
|
|
1277
|
+
summarisedTurns: plain2.droppedTurns,
|
|
1278
|
+
warning: compactionWarning(plan, plain2)
|
|
1275
1279
|
};
|
|
1276
1280
|
}
|
|
1277
1281
|
function planKeepingSummaries(shape, messages, reserveChars) {
|
|
@@ -1287,8 +1291,8 @@ function planKeepingSummaries(shape, messages, reserveChars) {
|
|
|
1287
1291
|
return shape.plan(messages, callerPinned, reserveChars);
|
|
1288
1292
|
}
|
|
1289
1293
|
}
|
|
1290
|
-
function replan(shape,
|
|
1291
|
-
const kept =
|
|
1294
|
+
function replan(shape, plain2, summary) {
|
|
1295
|
+
const kept = plain2.messages;
|
|
1292
1296
|
const insertAt = kept.findIndex((message) => message.role !== "system");
|
|
1293
1297
|
const at = insertAt < 0 ? kept.length : insertAt;
|
|
1294
1298
|
const next = [...kept.slice(0, at), summary, ...kept.slice(at)];
|
|
@@ -1304,9 +1308,9 @@ function replan(shape, plain, summary) {
|
|
|
1304
1308
|
if (!kept.every((message) => survived.has(message))) return void 0;
|
|
1305
1309
|
return plan;
|
|
1306
1310
|
}
|
|
1307
|
-
function compactionWarning(plan,
|
|
1308
|
-
const turns = plural2(
|
|
1309
|
-
const messages = plural2(
|
|
1311
|
+
function compactionWarning(plan, plain2) {
|
|
1312
|
+
const turns = plural2(plain2.droppedTurns, "earlier turn");
|
|
1313
|
+
const messages = plural2(plain2.dropped.length, "message");
|
|
1310
1314
|
return `Context budget: compacted ${turns} (${messages}) into a summary to stay under the ${count2(plan.usage.promptLimitChars)}-character prompt limit. The summary is in the transcript in their place; the recent turns are untouched.`;
|
|
1311
1315
|
}
|
|
1312
1316
|
function truncateToLine(text, maxChars) {
|
|
@@ -4080,7 +4084,7 @@ function runProcess(command, args, options) {
|
|
|
4080
4084
|
});
|
|
4081
4085
|
const untrack = trackProcessGroup(child.pid);
|
|
4082
4086
|
const stdout6 = createCapture(maxBytes);
|
|
4083
|
-
const
|
|
4087
|
+
const stderr2 = createCapture(maxBytes);
|
|
4084
4088
|
let settled = false;
|
|
4085
4089
|
let started = false;
|
|
4086
4090
|
let termination;
|
|
@@ -4136,7 +4140,7 @@ function runProcess(command, args, options) {
|
|
|
4136
4140
|
};
|
|
4137
4141
|
const onAbort = () => beginTermination("aborted");
|
|
4138
4142
|
child.stdout.on("data", (chunk) => stdout6.push(chunk));
|
|
4139
|
-
child.stderr.on("data", (chunk) =>
|
|
4143
|
+
child.stderr.on("data", (chunk) => stderr2.push(chunk));
|
|
4140
4144
|
child.stdout.on("error", () => {
|
|
4141
4145
|
});
|
|
4142
4146
|
child.stderr.on("error", () => {
|
|
@@ -4149,7 +4153,7 @@ function runProcess(command, args, options) {
|
|
|
4149
4153
|
if (termination === "aborted") {
|
|
4150
4154
|
resolve15({
|
|
4151
4155
|
stdout: stdout6.text(),
|
|
4152
|
-
stderr:
|
|
4156
|
+
stderr: stderr2.text(),
|
|
4153
4157
|
started,
|
|
4154
4158
|
aborted: true,
|
|
4155
4159
|
exitCode: null,
|
|
@@ -4157,7 +4161,7 @@ function runProcess(command, args, options) {
|
|
|
4157
4161
|
timedOut: false,
|
|
4158
4162
|
timeoutMs,
|
|
4159
4163
|
...stdout6.truncation() ? { stdoutTruncation: stdout6.truncation() } : {},
|
|
4160
|
-
...
|
|
4164
|
+
...stderr2.truncation() ? { stderrTruncation: stderr2.truncation() } : {},
|
|
4161
4165
|
redactedEnvKeys
|
|
4162
4166
|
});
|
|
4163
4167
|
return;
|
|
@@ -4167,11 +4171,11 @@ function runProcess(command, args, options) {
|
|
|
4167
4171
|
});
|
|
4168
4172
|
child.on("close", (exitCode, signal) => {
|
|
4169
4173
|
const stdoutTruncation = stdout6.truncation();
|
|
4170
|
-
const stderrTruncation =
|
|
4174
|
+
const stderrTruncation = stderr2.truncation();
|
|
4171
4175
|
finish(
|
|
4172
4176
|
() => resolve15({
|
|
4173
4177
|
stdout: stdout6.text(),
|
|
4174
|
-
stderr:
|
|
4178
|
+
stderr: stderr2.text(),
|
|
4175
4179
|
started,
|
|
4176
4180
|
aborted: termination === "aborted",
|
|
4177
4181
|
exitCode,
|
|
@@ -5181,7 +5185,7 @@ function topCounts(values) {
|
|
|
5181
5185
|
for (const value of values) {
|
|
5182
5186
|
counts.set(value, (counts.get(value) ?? 0) + 1);
|
|
5183
5187
|
}
|
|
5184
|
-
return [...counts.entries()].map(([name,
|
|
5188
|
+
return [...counts.entries()].map(([name, count4]) => ({ name, count: count4 })).sort((a, b) => b.count - a.count || a.name.localeCompare(b.name));
|
|
5185
5189
|
}
|
|
5186
5190
|
async function collectGitContext(workspace, signal) {
|
|
5187
5191
|
const statusResult = await runGit(["status", "--short", "--branch"], { cwd: workspace, signal });
|
|
@@ -6966,8 +6970,8 @@ function shannonEntropy(value) {
|
|
|
6966
6970
|
counts.set(char, (counts.get(char) ?? 0) + 1);
|
|
6967
6971
|
}
|
|
6968
6972
|
let entropy = 0;
|
|
6969
|
-
for (const
|
|
6970
|
-
const probability =
|
|
6973
|
+
for (const count4 of counts.values()) {
|
|
6974
|
+
const probability = count4 / value.length;
|
|
6971
6975
|
entropy -= probability * Math.log2(probability);
|
|
6972
6976
|
}
|
|
6973
6977
|
return entropy;
|
|
@@ -8040,7 +8044,9 @@ async function createSkill(workspace, input) {
|
|
|
8040
8044
|
name,
|
|
8041
8045
|
description: input.description.trim(),
|
|
8042
8046
|
...input.whenToUse ? { when_to_use: input.whenToUse.trim() } : {},
|
|
8043
|
-
...permissions.length ? { permissions } : {}
|
|
8047
|
+
...permissions.length ? { permissions } : {},
|
|
8048
|
+
...input.command ? { command: input.command } : {},
|
|
8049
|
+
...input.argumentHint ? { "argument-hint": input.argumentHint } : {}
|
|
8044
8050
|
},
|
|
8045
8051
|
input.body?.trim() || `# ${name}
|
|
8046
8052
|
|
|
@@ -8057,7 +8063,8 @@ Describe the steps the agent should follow when this skill is invoked.`
|
|
|
8057
8063
|
whenToUse: input.whenToUse?.trim(),
|
|
8058
8064
|
permissions,
|
|
8059
8065
|
body: document,
|
|
8060
|
-
path
|
|
8066
|
+
path,
|
|
8067
|
+
...input.command ? { command: { raw: input.command, argumentHint: input.argumentHint } } : {}
|
|
8061
8068
|
};
|
|
8062
8069
|
}
|
|
8063
8070
|
async function loadSkillsContext(workspace) {
|
|
@@ -8076,6 +8083,213 @@ async function loadSkillsContext(workspace) {
|
|
|
8076
8083
|
}).join("\n") + notice;
|
|
8077
8084
|
}
|
|
8078
8085
|
|
|
8086
|
+
// src/skills/commands.ts
|
|
8087
|
+
var BUILTIN_SLASH_COMMANDS = [
|
|
8088
|
+
"doctor",
|
|
8089
|
+
"mcp",
|
|
8090
|
+
"continue",
|
|
8091
|
+
"autonomous",
|
|
8092
|
+
"skill",
|
|
8093
|
+
"update",
|
|
8094
|
+
"logs",
|
|
8095
|
+
"ghost",
|
|
8096
|
+
"output",
|
|
8097
|
+
"usage",
|
|
8098
|
+
"max-steps",
|
|
8099
|
+
"resume",
|
|
8100
|
+
"deny",
|
|
8101
|
+
"plan",
|
|
8102
|
+
"review",
|
|
8103
|
+
"coding",
|
|
8104
|
+
"status",
|
|
8105
|
+
"context",
|
|
8106
|
+
"compact",
|
|
8107
|
+
"diff",
|
|
8108
|
+
"sessions",
|
|
8109
|
+
"save",
|
|
8110
|
+
"test",
|
|
8111
|
+
"init",
|
|
8112
|
+
"new",
|
|
8113
|
+
"allow",
|
|
8114
|
+
"clear",
|
|
8115
|
+
"exit",
|
|
8116
|
+
"help",
|
|
8117
|
+
"memory",
|
|
8118
|
+
"mode",
|
|
8119
|
+
"model",
|
|
8120
|
+
"permissions",
|
|
8121
|
+
"quit",
|
|
8122
|
+
"skills",
|
|
8123
|
+
"tools",
|
|
8124
|
+
"whoami"
|
|
8125
|
+
];
|
|
8126
|
+
var COMMAND_NAME_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
8127
|
+
var MAX_COMMAND_LENGTH = 64;
|
|
8128
|
+
function parseSlashInput(input) {
|
|
8129
|
+
const trimmed = input.trim();
|
|
8130
|
+
if (!trimmed.startsWith("/")) return null;
|
|
8131
|
+
const rest = trimmed.slice(1);
|
|
8132
|
+
const match = rest.match(/^(\S+)(?:\s+([\s\S]*))?$/);
|
|
8133
|
+
if (!match) return null;
|
|
8134
|
+
return { command: match[1], args: (match[2] ?? "").trim() };
|
|
8135
|
+
}
|
|
8136
|
+
function isBuiltinSlashCommand(command) {
|
|
8137
|
+
return BUILTIN_SLASH_COMMANDS.includes(command.toLowerCase());
|
|
8138
|
+
}
|
|
8139
|
+
function normalizeCommandName(value) {
|
|
8140
|
+
const candidate = value.trim().replace(/^\/+/, "").trim();
|
|
8141
|
+
if (!candidate) {
|
|
8142
|
+
throw new Error("Slash command name is empty; expected kebab-case, for example: open-pr.");
|
|
8143
|
+
}
|
|
8144
|
+
if (candidate.length > MAX_COMMAND_LENGTH) {
|
|
8145
|
+
throw new Error(
|
|
8146
|
+
`Slash command name "${candidate}" is longer than ${MAX_COMMAND_LENGTH} characters.`
|
|
8147
|
+
);
|
|
8148
|
+
}
|
|
8149
|
+
if (!COMMAND_NAME_RE.test(candidate)) {
|
|
8150
|
+
throw new Error(
|
|
8151
|
+
`Invalid slash command name "${candidate}": use kebab-case - lowercase letters, digits, and single hyphens, no spaces or slashes (for example: open-pr).`
|
|
8152
|
+
);
|
|
8153
|
+
}
|
|
8154
|
+
return candidate;
|
|
8155
|
+
}
|
|
8156
|
+
async function collectEntries(workspace) {
|
|
8157
|
+
const skills = await discoverSkills(workspace);
|
|
8158
|
+
const claimed = /* @__PURE__ */ new Map();
|
|
8159
|
+
const conflicts = [];
|
|
8160
|
+
for (const skill of skills) {
|
|
8161
|
+
let declaration;
|
|
8162
|
+
try {
|
|
8163
|
+
declaration = skill.command ?? null;
|
|
8164
|
+
} catch (error) {
|
|
8165
|
+
conflicts.push({
|
|
8166
|
+
command: "",
|
|
8167
|
+
skillName: skill.name,
|
|
8168
|
+
scope: skill.scope,
|
|
8169
|
+
path: skill.path,
|
|
8170
|
+
reason: "invalid",
|
|
8171
|
+
message: `Skill "${skill.name}" (${skill.scope}) was skipped: ${error instanceof Error ? error.message : String(error)}`
|
|
8172
|
+
});
|
|
8173
|
+
continue;
|
|
8174
|
+
}
|
|
8175
|
+
if (!declaration) continue;
|
|
8176
|
+
let command;
|
|
8177
|
+
try {
|
|
8178
|
+
command = normalizeCommandName(declaration.raw);
|
|
8179
|
+
} catch (error) {
|
|
8180
|
+
conflicts.push({
|
|
8181
|
+
command: declaration.raw,
|
|
8182
|
+
skillName: skill.name,
|
|
8183
|
+
scope: skill.scope,
|
|
8184
|
+
path: skill.path,
|
|
8185
|
+
reason: "invalid",
|
|
8186
|
+
message: `Skill "${skill.name}" (${skill.scope}) declares a malformed command: ${error instanceof Error ? error.message : String(error)}`
|
|
8187
|
+
});
|
|
8188
|
+
continue;
|
|
8189
|
+
}
|
|
8190
|
+
if (isBuiltinSlashCommand(command)) {
|
|
8191
|
+
conflicts.push({
|
|
8192
|
+
command,
|
|
8193
|
+
skillName: skill.name,
|
|
8194
|
+
scope: skill.scope,
|
|
8195
|
+
path: skill.path,
|
|
8196
|
+
reason: "builtin",
|
|
8197
|
+
message: `Skill "${skill.name}" (${skill.scope}) declares /${command}, which is a built-in command. Built-ins always win - rename the command: key in ${skill.path}.`
|
|
8198
|
+
});
|
|
8199
|
+
continue;
|
|
8200
|
+
}
|
|
8201
|
+
const entry = {
|
|
8202
|
+
skill,
|
|
8203
|
+
command: {
|
|
8204
|
+
command,
|
|
8205
|
+
skillName: skill.name,
|
|
8206
|
+
description: skill.description,
|
|
8207
|
+
argumentHint: declaration.argumentHint,
|
|
8208
|
+
scope: skill.scope,
|
|
8209
|
+
path: skill.path
|
|
8210
|
+
}
|
|
8211
|
+
};
|
|
8212
|
+
const existing = claimed.get(command);
|
|
8213
|
+
if (!existing) {
|
|
8214
|
+
claimed.set(command, entry);
|
|
8215
|
+
continue;
|
|
8216
|
+
}
|
|
8217
|
+
const winner = existing.skill.scope === "global" && skill.scope === "project" ? entry : existing;
|
|
8218
|
+
const loser = winner === entry ? existing : entry;
|
|
8219
|
+
claimed.set(command, winner);
|
|
8220
|
+
conflicts.push({
|
|
8221
|
+
command,
|
|
8222
|
+
skillName: loser.skill.name,
|
|
8223
|
+
scope: loser.skill.scope,
|
|
8224
|
+
path: loser.skill.path,
|
|
8225
|
+
reason: "duplicate",
|
|
8226
|
+
message: `/${command} is declared by both "${winner.skill.name}" (${winner.skill.scope}) and "${loser.skill.name}" (${loser.skill.scope}). "${winner.skill.name}" wins; rename the command: key in ${loser.skill.path}.`
|
|
8227
|
+
});
|
|
8228
|
+
}
|
|
8229
|
+
const entries = [...claimed.values()].sort((a, b) => a.command.command.localeCompare(b.command.command));
|
|
8230
|
+
return { entries, conflicts };
|
|
8231
|
+
}
|
|
8232
|
+
async function listSkillCommands(workspace) {
|
|
8233
|
+
const { entries, conflicts } = await collectEntries(workspace);
|
|
8234
|
+
return { commands: entries.map((entry) => entry.command), conflicts };
|
|
8235
|
+
}
|
|
8236
|
+
function buildSkillCommandPrompt(skill, command, args) {
|
|
8237
|
+
const argumentLine = command.argumentHint ? `Arguments (${command.argumentHint}): ${args || "(none provided)"}` : `Arguments: ${args || "(none provided)"}`;
|
|
8238
|
+
const header = [
|
|
8239
|
+
`The user ran the /${command.command} slash command, which runs the "${skill.name}" skill (${skill.scope}).`,
|
|
8240
|
+
skill.description ? `Description: ${skill.description}` : void 0,
|
|
8241
|
+
argumentLine,
|
|
8242
|
+
skill.permissions.length ? `Permissions needed: ${skill.permissions.join(", ")}` : void 0,
|
|
8243
|
+
"Follow the instructions below now, applying the arguments where they are relevant."
|
|
8244
|
+
].filter(Boolean).join("\n");
|
|
8245
|
+
return `${header}
|
|
8246
|
+
|
|
8247
|
+
--- instructions ---
|
|
8248
|
+
${skill.body}`;
|
|
8249
|
+
}
|
|
8250
|
+
async function resolveSkillCommand(workspace, input) {
|
|
8251
|
+
const parsed = parseSlashInput(input);
|
|
8252
|
+
if (!parsed) return { status: "not-a-command" };
|
|
8253
|
+
const command = parsed.command.toLowerCase();
|
|
8254
|
+
if (isBuiltinSlashCommand(command)) {
|
|
8255
|
+
return { status: "builtin", command };
|
|
8256
|
+
}
|
|
8257
|
+
const { entries, conflicts } = await collectEntries(workspace);
|
|
8258
|
+
const match = entries.find((entry) => entry.command.command === command);
|
|
8259
|
+
if (!match) {
|
|
8260
|
+
const related = conflicts.filter((conflict) => conflict.command === command);
|
|
8261
|
+
if (related.length) {
|
|
8262
|
+
return {
|
|
8263
|
+
status: "conflict",
|
|
8264
|
+
command,
|
|
8265
|
+
message: related.map((conflict) => conflict.message).join("\n"),
|
|
8266
|
+
conflicts: related
|
|
8267
|
+
};
|
|
8268
|
+
}
|
|
8269
|
+
return {
|
|
8270
|
+
status: "unknown",
|
|
8271
|
+
command,
|
|
8272
|
+
available: entries.map((entry) => entry.command.command)
|
|
8273
|
+
};
|
|
8274
|
+
}
|
|
8275
|
+
return {
|
|
8276
|
+
status: "ok",
|
|
8277
|
+
command: match.command,
|
|
8278
|
+
skill: match.skill,
|
|
8279
|
+
args: parsed.args,
|
|
8280
|
+
prompt: buildSkillCommandPrompt(match.skill, match.command, parsed.args)
|
|
8281
|
+
};
|
|
8282
|
+
}
|
|
8283
|
+
function formatSkillCommandHelp(index) {
|
|
8284
|
+
if (!index.commands.length) return "";
|
|
8285
|
+
const lines = index.commands.map((command) => {
|
|
8286
|
+
const invocation = command.argumentHint ? `/${command.command} ${command.argumentHint}` : `/${command.command}`;
|
|
8287
|
+
const description = command.description || `Run the ${command.skillName} skill`;
|
|
8288
|
+
return ` ${invocation.padEnd(22)}${description} (skill: ${command.skillName})`;
|
|
8289
|
+
});
|
|
8290
|
+
return ["Skill commands:", ...lines].join("\n");
|
|
8291
|
+
}
|
|
8292
|
+
|
|
8079
8293
|
// src/tools/workspace/skill-tools.ts
|
|
8080
8294
|
var SkillInput = z17.object({
|
|
8081
8295
|
name: z17.string().min(1).describe("Name of the skill to load, as shown in the skills list.")
|
|
@@ -8122,6 +8336,26 @@ var listSkillsTool = {
|
|
|
8122
8336
|
};
|
|
8123
8337
|
}
|
|
8124
8338
|
};
|
|
8339
|
+
var CreateSkillInput = z17.object({
|
|
8340
|
+
name: z17.string().min(1).max(80),
|
|
8341
|
+
description: z17.string().trim().min(1).max(512),
|
|
8342
|
+
body: z17.string().trim().min(1).max(6e4),
|
|
8343
|
+
command: z17.string().min(1).max(64).optional(),
|
|
8344
|
+
argumentHint: z17.string().max(256).optional()
|
|
8345
|
+
});
|
|
8346
|
+
var createSkillTool = {
|
|
8347
|
+
name: "create_skill",
|
|
8348
|
+
description: "Create a project SKILL.md with instructions; never overwrites. Trust is required before loading project skills.",
|
|
8349
|
+
permissions: ["write"],
|
|
8350
|
+
inputSchema: CreateSkillInput,
|
|
8351
|
+
async execute(input, context) {
|
|
8352
|
+
const command = input.command ? normalizeCommandName(input.command) : void 0;
|
|
8353
|
+
if (command && isBuiltinSlashCommand(command)) throw new Error(`/${command} is reserved.`);
|
|
8354
|
+
if (input.argumentHint && (!command || /[\r\n]/.test(input.argumentHint))) throw new Error("Argument hint requires a command and a single line.");
|
|
8355
|
+
const skill = await createSkill(context.workspace, { ...input, command, scope: "project" });
|
|
8356
|
+
return { changed: true, content: `Created ${skill.name}: ${skill.path}. Project skills require workspace trust before use.`, data: { name: skill.name, path: skill.path } };
|
|
8357
|
+
}
|
|
8358
|
+
};
|
|
8125
8359
|
|
|
8126
8360
|
// src/tools/workspace/todo-tools.ts
|
|
8127
8361
|
import { z as z18 } from "zod";
|
|
@@ -8562,7 +8796,7 @@ function summarizeDirectories(files) {
|
|
|
8562
8796
|
const directory = dirname9(file.path).split(/[\\/]/)[0] || ".";
|
|
8563
8797
|
counts.set(directory, (counts.get(directory) ?? 0) + 1);
|
|
8564
8798
|
}
|
|
8565
|
-
return [...counts.entries()].map(([name,
|
|
8799
|
+
return [...counts.entries()].map(([name, count4]) => ({ name, count: count4 })).sort((a, b) => b.count - a.count || a.name.localeCompare(b.name));
|
|
8566
8800
|
}
|
|
8567
8801
|
function summarizeExtensions(files) {
|
|
8568
8802
|
const summaries = /* @__PURE__ */ new Map();
|
|
@@ -8831,6 +9065,124 @@ import { z as z25 } from "zod";
|
|
|
8831
9065
|
import { mkdir as mkdir11, writeFile as writeFile8 } from "fs/promises";
|
|
8832
9066
|
import { join as join9 } from "path";
|
|
8833
9067
|
|
|
9068
|
+
// src/core/periodic-compaction.ts
|
|
9069
|
+
var DEFAULT_SUMMARY_INTERVAL_STEPS = 12;
|
|
9070
|
+
var MIN_PERIODIC_HISTORY_CHARS = 12e3;
|
|
9071
|
+
function summaryIntervalSteps(value = process.env.ULTRAC_SUMMARY_INTERVAL_STEPS) {
|
|
9072
|
+
if (value === void 0 || value.trim() === "") return DEFAULT_SUMMARY_INTERVAL_STEPS;
|
|
9073
|
+
if (!/^\d+$/.test(value) || !Number.isSafeInteger(Number(value)) || Number(value) > 1e3) throw new Error("ULTRAC_SUMMARY_INTERVAL_STEPS must be an integer from 0 to 1000.");
|
|
9074
|
+
return Number(value);
|
|
9075
|
+
}
|
|
9076
|
+
function completeContextUnits(messages) {
|
|
9077
|
+
const units = [];
|
|
9078
|
+
for (const message of messages) {
|
|
9079
|
+
if (message.role === "tool") {
|
|
9080
|
+
const previous = units.at(-1);
|
|
9081
|
+
if (!previous || previous[0].role !== "assistant") return null;
|
|
9082
|
+
previous.push(message);
|
|
9083
|
+
} else units.push([message]);
|
|
9084
|
+
}
|
|
9085
|
+
for (const unit of units) {
|
|
9086
|
+
const first = unit[0];
|
|
9087
|
+
if (first.role !== "assistant") continue;
|
|
9088
|
+
if (!first.toolCalls?.length) {
|
|
9089
|
+
if (unit.slice(1).some((message) => "toolCallId" in message)) return null;
|
|
9090
|
+
continue;
|
|
9091
|
+
}
|
|
9092
|
+
const ids = new Set(first.toolCalls.map((call) => call.id));
|
|
9093
|
+
const results = unit.slice(1).map((message) => message.toolCallId);
|
|
9094
|
+
if (ids.size !== first.toolCalls.length || results.length !== ids.size || results.some((id) => !id || !ids.has(id)) || new Set(results).size !== ids.size) return null;
|
|
9095
|
+
}
|
|
9096
|
+
return units;
|
|
9097
|
+
}
|
|
9098
|
+
var PeriodicCompactor = class {
|
|
9099
|
+
constructor(options = {}) {
|
|
9100
|
+
this.options = options;
|
|
9101
|
+
}
|
|
9102
|
+
options;
|
|
9103
|
+
lastAttemptStep = 0;
|
|
9104
|
+
async compact(messages, pinned, provider, model, completedSteps, signal) {
|
|
9105
|
+
const unchanged = () => ({ messages: [...messages], compacted: false });
|
|
9106
|
+
const interval = this.options.intervalSteps ?? summaryIntervalSteps();
|
|
9107
|
+
if (!interval || signal?.aborted || completedSteps - this.lastAttemptStep < interval) return unchanged();
|
|
9108
|
+
const units = completeContextUnits(messages);
|
|
9109
|
+
if (!units) return unchanged();
|
|
9110
|
+
const recent = new Set(units.filter((unit) => unit[0].role === "assistant").slice(-2));
|
|
9111
|
+
const pinnedSet = new Set(pinned);
|
|
9112
|
+
const history = units.filter((unit) => !recent.has(unit) && !unit.some((message) => message.role === "user" || message.role === "system" || pinnedSet.has(message) || isCompactionSummary(message))).flat();
|
|
9113
|
+
const removedChars = history.reduce((size, message) => size + JSON.stringify(message).length, 0);
|
|
9114
|
+
if (history.length < 2 || removedChars < (this.options.minHistoryChars ?? MIN_PERIODIC_HISTORY_CHARS)) return unchanged();
|
|
9115
|
+
this.lastAttemptStep = completedSteps;
|
|
9116
|
+
const trace = startDiagnosticSpan("context.compaction", { path: "periodic", completedSteps, compactedMessageCount: history.length }, 5e3);
|
|
9117
|
+
const controller = new AbortController();
|
|
9118
|
+
let rejectCancelled;
|
|
9119
|
+
const interrupted2 = new Promise((_resolve, reject) => {
|
|
9120
|
+
rejectCancelled = reject;
|
|
9121
|
+
});
|
|
9122
|
+
const abort = () => {
|
|
9123
|
+
controller.abort();
|
|
9124
|
+
rejectCancelled(new Error("summary_interrupted"));
|
|
9125
|
+
};
|
|
9126
|
+
const timer = setTimeout(abort, this.options.timeoutMs ?? DEFAULT_SUMMARY_TIMEOUT_MS);
|
|
9127
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
9128
|
+
if (signal?.aborted) abort();
|
|
9129
|
+
try {
|
|
9130
|
+
const completion = await Promise.race([provider.complete({ model, system: summaryInstructions(DEFAULT_MAX_SUMMARY_CHARS), messages: [{ role: "user", content: renderDroppedTranscript(history, { maxTranscriptChars: DEFAULT_MAX_TRANSCRIPT_CHARS }) }], signal: controller.signal, context: { purpose: "compaction_summary", compactedMessages: history.length } }), interrupted2]);
|
|
9131
|
+
if (controller.signal.aborted) {
|
|
9132
|
+
trace.end({ outcome: "skipped", reason: "interrupted" });
|
|
9133
|
+
return unchanged();
|
|
9134
|
+
}
|
|
9135
|
+
const summary = completion.text.trim();
|
|
9136
|
+
if (!summary || summary.length > DEFAULT_MAX_SUMMARY_CHARS || summary.length + 300 >= removedChars) {
|
|
9137
|
+
trace.end({ outcome: "skipped", reason: "invalid-summary" });
|
|
9138
|
+
return unchanged();
|
|
9139
|
+
}
|
|
9140
|
+
const removed = new Set(history);
|
|
9141
|
+
const compacted = messages.filter((message) => !removed.has(message));
|
|
9142
|
+
const firstRemoved = messages.findIndex((message) => removed.has(message));
|
|
9143
|
+
const insertion = messages.slice(0, firstRemoved).filter((message) => !removed.has(message)).length;
|
|
9144
|
+
compacted.splice(insertion, 0, compactionSummaryMessage(summary));
|
|
9145
|
+
if (!completeContextUnits(compacted)) {
|
|
9146
|
+
trace.end({ outcome: "skipped", reason: "invalid-pairing" });
|
|
9147
|
+
return unchanged();
|
|
9148
|
+
}
|
|
9149
|
+
trace.end({ outcome: "compacted", summaryChars: summary.length });
|
|
9150
|
+
return { messages: compacted, compacted: true };
|
|
9151
|
+
} catch {
|
|
9152
|
+
trace.end({ outcome: "failed", reason: controller.signal.aborted ? "interrupted" : "summary-failed" });
|
|
9153
|
+
return unchanged();
|
|
9154
|
+
} finally {
|
|
9155
|
+
clearTimeout(timer);
|
|
9156
|
+
signal?.removeEventListener("abort", abort);
|
|
9157
|
+
}
|
|
9158
|
+
}
|
|
9159
|
+
};
|
|
9160
|
+
function snapshotMessages(messages) {
|
|
9161
|
+
const result = [];
|
|
9162
|
+
for (let index = 0; index < messages.length; index += 1) {
|
|
9163
|
+
const message = messages[index];
|
|
9164
|
+
const native = message;
|
|
9165
|
+
if (native.role !== "assistant" || !native.toolCalls?.length) {
|
|
9166
|
+
result.push(message);
|
|
9167
|
+
continue;
|
|
9168
|
+
}
|
|
9169
|
+
const replies = [];
|
|
9170
|
+
while (messages[index + 1]?.role === "tool") replies.push(messages[++index]);
|
|
9171
|
+
const ids = new Set(replies.map((reply) => reply.toolCallId));
|
|
9172
|
+
const calls = native.toolCalls.filter((call) => ids.has(call.id));
|
|
9173
|
+
const completedIds = new Set(calls.map((call) => call.id));
|
|
9174
|
+
const base = { role: native.role, content: native.content };
|
|
9175
|
+
result.push({ ...base, ...calls.length ? { toolCalls: calls } : {} });
|
|
9176
|
+
result.push(...replies.filter((reply) => completedIds.has(reply.toolCallId)));
|
|
9177
|
+
}
|
|
9178
|
+
return result;
|
|
9179
|
+
}
|
|
9180
|
+
function snapshotWithConstraints(messages, constraints) {
|
|
9181
|
+
const snapshot = snapshotMessages(messages);
|
|
9182
|
+
for (const constraint of constraints) if (!snapshot.some((message) => message.role === "user" && message.content === constraint.content)) snapshot.push({ role: "user", content: constraint.content });
|
|
9183
|
+
return snapshot;
|
|
9184
|
+
}
|
|
9185
|
+
|
|
8834
9186
|
// src/core/progress-events.ts
|
|
8835
9187
|
var listeners = /* @__PURE__ */ new Set();
|
|
8836
9188
|
function onAgentProgress(listener) {
|
|
@@ -8997,18 +9349,18 @@ function isConcurrencySafe(tool) {
|
|
|
8997
9349
|
}
|
|
8998
9350
|
function planToolBatches(calls, tools) {
|
|
8999
9351
|
const batches = [];
|
|
9000
|
-
let
|
|
9352
|
+
let open9;
|
|
9001
9353
|
for (const call of calls) {
|
|
9002
9354
|
if (!isConcurrencySafe(tools.get(call.tool))) {
|
|
9003
9355
|
batches.push([call]);
|
|
9004
|
-
|
|
9356
|
+
open9 = void 0;
|
|
9005
9357
|
continue;
|
|
9006
9358
|
}
|
|
9007
|
-
if (!
|
|
9008
|
-
|
|
9009
|
-
batches.push(
|
|
9359
|
+
if (!open9) {
|
|
9360
|
+
open9 = [];
|
|
9361
|
+
batches.push(open9);
|
|
9010
9362
|
}
|
|
9011
|
-
|
|
9363
|
+
open9.push(call);
|
|
9012
9364
|
}
|
|
9013
9365
|
return batches;
|
|
9014
9366
|
}
|
|
@@ -9546,6 +9898,17 @@ function readSessionUserConstraints(session) {
|
|
|
9546
9898
|
}
|
|
9547
9899
|
return constraints;
|
|
9548
9900
|
}
|
|
9901
|
+
function readSessionContextSnapshot(session) {
|
|
9902
|
+
if (session.contextSnapshot !== void 0) {
|
|
9903
|
+
const snapshot = session.contextSnapshot;
|
|
9904
|
+
if (!snapshot || typeof snapshot !== "object" || snapshot.version !== 1 || !["native", "text", "run"].includes(snapshot.kind) || !Number.isSafeInteger(snapshot.completedSteps) || snapshot.completedSteps < 0 || !Array.isArray(snapshot.messages) || snapshot.messages.some((message) => {
|
|
9905
|
+
if (!message || typeof message !== "object" || !["user", "assistant", "system", "tool"].includes(message.role) || typeof message.content !== "string") return true;
|
|
9906
|
+
const native = message;
|
|
9907
|
+
return snapshot.kind !== "native" && (message.role === "system" || native.toolCalls !== void 0 || native.toolCallId !== void 0) || native.toolCalls !== void 0 && message.role !== "assistant" || native.toolCallId !== void 0 && message.role !== "tool" || snapshot.kind === "native" && message.role === "tool" && typeof native.toolCallId !== "string" || native.toolCallId !== void 0 && typeof native.toolCallId !== "string" || native.toolCalls !== void 0 && (!Array.isArray(native.toolCalls) || native.toolCalls.some((call) => !call || typeof call.id !== "string" || typeof call.name !== "string" || typeof call.arguments !== "string"));
|
|
9908
|
+
}) || !completeContextUnits(snapshot.messages) || (readSessionUserConstraints(session) ?? [{ content: session.task }]).some((constraint) => !snapshot.messages.some((message) => message.role === "user" && message.content === constraint.content))) throw new Error(`Session ${session.id} has an invalid saved context snapshot and cannot be continued safely.`);
|
|
9909
|
+
}
|
|
9910
|
+
return session.contextSnapshot;
|
|
9911
|
+
}
|
|
9549
9912
|
function createSessionId(date = /* @__PURE__ */ new Date()) {
|
|
9550
9913
|
const stamp = date.toISOString().replace(/[:.]/g, "-");
|
|
9551
9914
|
return `${stamp}-${randomBytes3(4).toString("hex")}`;
|
|
@@ -9823,6 +10186,7 @@ function validateSession(value, expectedId, label) {
|
|
|
9823
10186
|
if (!isSafeSessionId(sessionId) || sessionId !== expectedId || session.parentSessionId !== void 0 && (typeof session.parentSessionId !== "string" || !isSafeSessionId(session.parentSessionId) || normalizedSessionId(session.parentSessionId) !== session.parentSessionId) || typeof session.task !== "string" || typeof session.workspace !== "string" || typeof session.provider !== "string" || typeof session.model !== "string" || typeof session.startedAt !== "string" || typeof session.endedAt !== "string" || typeof session.finalMessage !== "string" || !isToolPermissions(session.permissions) || !Array.isArray(session.steps) || session.mcpResultArchiveVersion !== void 0 && session.mcpResultArchiveVersion !== 1 || session.mode !== void 0 && !isAgentModeName2(session.mode) || session.stopReason !== void 0 && !isStopReason(session.stopReason) || session.subagents !== void 0 && !Array.isArray(session.subagents)) {
|
|
9824
10187
|
invalid();
|
|
9825
10188
|
}
|
|
10189
|
+
readSessionContextSnapshot(value);
|
|
9826
10190
|
const steps = session.steps;
|
|
9827
10191
|
for (const stepValue of steps) {
|
|
9828
10192
|
if (!stepValue || typeof stepValue !== "object" || Array.isArray(stepValue)) invalid();
|
|
@@ -9933,6 +10297,7 @@ var AgentRunner = class {
|
|
|
9933
10297
|
];
|
|
9934
10298
|
const pinnedMessages = [...constraintMessages];
|
|
9935
10299
|
const steps = [];
|
|
10300
|
+
const periodicCompactor = new PeriodicCompactor();
|
|
9936
10301
|
const toolsByName = new Map(
|
|
9937
10302
|
registered.map((tool) => [tool.name, tool])
|
|
9938
10303
|
);
|
|
@@ -9955,7 +10320,8 @@ var AgentRunner = class {
|
|
|
9955
10320
|
...subagents2.length ? { subagents: subagents2 } : {},
|
|
9956
10321
|
constraintVersion: 1,
|
|
9957
10322
|
userConstraints: [...userConstraints],
|
|
9958
|
-
mcpResultArchiveVersion: 1
|
|
10323
|
+
mcpResultArchiveVersion: 1,
|
|
10324
|
+
...messages.some((message) => message.role === "user" && message.content.startsWith("[compacted-context]")) ? { contextSnapshot: { version: 1, kind: "run", messages: snapshotWithConstraints(messages, userConstraints), completedSteps: steps.length } } : {}
|
|
9959
10325
|
});
|
|
9960
10326
|
const depth = options.depth ?? 0;
|
|
9961
10327
|
const isTopLevel = depth === 0;
|
|
@@ -9964,6 +10330,7 @@ var AgentRunner = class {
|
|
|
9964
10330
|
try {
|
|
9965
10331
|
await options.onCheckpoint(buildSession(void 0, listSubagentTranscripts()));
|
|
9966
10332
|
} catch {
|
|
10333
|
+
diagnosticEvent("session.checkpoint", { outcome: "failed", path: "run" });
|
|
9967
10334
|
}
|
|
9968
10335
|
};
|
|
9969
10336
|
let stopReason = "max-steps";
|
|
@@ -9981,6 +10348,11 @@ var AgentRunner = class {
|
|
|
9981
10348
|
transcriptKind: "run",
|
|
9982
10349
|
fileProvenance
|
|
9983
10350
|
};
|
|
10351
|
+
const periodic = await periodicCompactor.compact(messages, [...pinnedMessages, taskMessage], options.provider, options.model, steps.length, options.signal);
|
|
10352
|
+
if (periodic.compacted) {
|
|
10353
|
+
messages.splice(0, messages.length, ...periodic.messages);
|
|
10354
|
+
await checkpoint();
|
|
10355
|
+
}
|
|
9984
10356
|
await compactIfNeeded(options, messages, system, taskMessage, pinnedMessages, requestContext);
|
|
9985
10357
|
if (options.signal?.aborted) {
|
|
9986
10358
|
stopReason = "aborted";
|
|
@@ -10294,7 +10666,7 @@ var NOTHING_RESTORED = {
|
|
|
10294
10666
|
legacyMcpResults: 0
|
|
10295
10667
|
};
|
|
10296
10668
|
function restorePreviousTranscript(sessions, maxToolResultChars, fileProvenance) {
|
|
10297
|
-
if (sessions.length === 0 || sessions.every((session) => session.steps.length === 0)) return NOTHING_RESTORED;
|
|
10669
|
+
if (sessions.length === 0 || sessions.every((session) => session.steps.length === 0 && !readSessionContextSnapshot(session))) return NOTHING_RESTORED;
|
|
10298
10670
|
const messages = [];
|
|
10299
10671
|
let toolResults = 0;
|
|
10300
10672
|
let truncatedResults = 0;
|
|
@@ -10302,6 +10674,11 @@ function restorePreviousTranscript(sessions, maxToolResultChars, fileProvenance)
|
|
|
10302
10674
|
let steps = 0;
|
|
10303
10675
|
for (const session of sessions) {
|
|
10304
10676
|
steps += session.steps.length;
|
|
10677
|
+
const snapshot = readSessionContextSnapshot(session);
|
|
10678
|
+
if (snapshot?.kind === "run" || snapshot?.kind === "text") {
|
|
10679
|
+
messages.splice(0, messages.length, ...snapshot.messages);
|
|
10680
|
+
continue;
|
|
10681
|
+
}
|
|
10305
10682
|
for (const step of session.steps) {
|
|
10306
10683
|
const content = step.parsed ? JSON.stringify(step.parsed) : step.modelText;
|
|
10307
10684
|
if (content) messages.push({ role: "assistant", content });
|
|
@@ -10316,6 +10693,7 @@ function restorePreviousTranscript(sessions, maxToolResultChars, fileProvenance)
|
|
|
10316
10693
|
fileProvenance?.set(entry.message, toolResultFileProvenance(result, entry.truncated));
|
|
10317
10694
|
}
|
|
10318
10695
|
}
|
|
10696
|
+
if (snapshot) messages.push(...snapshot.messages.filter((message) => message.role === "user" && message.content.startsWith("[compacted-context]")).map((message) => ({ role: "user", content: message.content })));
|
|
10319
10697
|
}
|
|
10320
10698
|
return { messages, steps, toolResults, truncatedResults, legacyMcpResults };
|
|
10321
10699
|
}
|
|
@@ -11201,8 +11579,8 @@ function tokenizeCommand(command) {
|
|
|
11201
11579
|
function renderCommandLine(plan) {
|
|
11202
11580
|
return [plan.command, ...plan.args].join(" ");
|
|
11203
11581
|
}
|
|
11204
|
-
function parseTestOutput(runner, stdout6,
|
|
11205
|
-
const combined = [stdout6,
|
|
11582
|
+
function parseTestOutput(runner, stdout6, stderr2, workspace) {
|
|
11583
|
+
const combined = [stdout6, stderr2].filter(Boolean).join("\n");
|
|
11206
11584
|
if (runner === "vitest" || runner === "jest") {
|
|
11207
11585
|
const report = extractJestStyleReport(stdout6) ?? extractJestStyleReport(combined);
|
|
11208
11586
|
if (report) return parseJestStyleReport(report, workspace);
|
|
@@ -12648,64 +13026,67 @@ function decodeCodePoint(value, fallback) {
|
|
|
12648
13026
|
// src/tools/workspace.ts
|
|
12649
13027
|
function createDefaultToolRegistry() {
|
|
12650
13028
|
const { todoWriteTool, todoReadTool } = createTodoTools();
|
|
12651
|
-
return new ToolRegistry().register(listFilesTool).register(readFileTool).register(readManyFilesTool).register(searchFilesTool).register(astSearchTool).register(inspectProjectTool).register(inspectArtifactTool).register(hexDumpTool).register(extractStringsTool).register(scanSecretsTool).register(fileTimelineTool).register(repoMapTool).register(repoHandbookTool).register(contextBundleTool).register(verificationPlanTool).register(verificationReportTool).register(writeFileTool).register(replaceInFileTool).register(multiEditTool).register(applyPatchTool).register(gitStatusTool).register(gitWorktreeSummaryTool).register(gitDiffTool).register(gitLogTool).register(gitShowTool).register(gitBlameTool).register(gitCommitTool).register(gitPushTool).register(runPackageScriptTool).register(runTestsTool).register(diagnosticsTool).register(listSessionsTool).register(readSessionTool).register(searchSessionsTool).register(fetchUrlTool).register(runShellTool).register(listProcessesTool).register(memoryWriteTool).register(memoryReadTool).register(memoryListTool).register(memorySearchTool).register(memoryDeleteTool).register(skillTool).register(listSkillsTool).register(todoWriteTool).register(todoReadTool).register(taskTool).register(askUserTool);
|
|
13029
|
+
return new ToolRegistry().register(listFilesTool).register(readFileTool).register(readManyFilesTool).register(searchFilesTool).register(astSearchTool).register(inspectProjectTool).register(inspectArtifactTool).register(hexDumpTool).register(extractStringsTool).register(scanSecretsTool).register(fileTimelineTool).register(repoMapTool).register(repoHandbookTool).register(contextBundleTool).register(verificationPlanTool).register(verificationReportTool).register(writeFileTool).register(replaceInFileTool).register(multiEditTool).register(applyPatchTool).register(gitStatusTool).register(gitWorktreeSummaryTool).register(gitDiffTool).register(gitLogTool).register(gitShowTool).register(gitBlameTool).register(gitCommitTool).register(gitPushTool).register(runPackageScriptTool).register(runTestsTool).register(diagnosticsTool).register(listSessionsTool).register(readSessionTool).register(searchSessionsTool).register(fetchUrlTool).register(runShellTool).register(listProcessesTool).register(memoryWriteTool).register(memoryReadTool).register(memoryListTool).register(memorySearchTool).register(memoryDeleteTool).register(skillTool).register(listSkillsTool).register(createSkillTool).register(todoWriteTool).register(todoReadTool).register(taskTool).register(askUserTool);
|
|
12652
13030
|
}
|
|
12653
13031
|
|
|
12654
13032
|
// src/commands/doctor.ts
|
|
12655
13033
|
function registerDoctorCommand(program2) {
|
|
12656
13034
|
program2.command("doctor").description("Check the local ultrac setup").option("-w, --workspace <path>", "Workspace path", ".").option("--json", "Print JSON output", false).action(async (options) => {
|
|
12657
|
-
|
|
12658
|
-
const workspaceReady = await stat10(workspace).then((entry) => entry.isDirectory(), () => false);
|
|
12659
|
-
const [nodeMajor = 0, nodeMinor = 0] = process.versions.node.split(".").map(Number);
|
|
12660
|
-
const nodeReady = nodeMajor > 22 || nodeMajor === 22 && nodeMinor >= 12;
|
|
12661
|
-
const config = await loadConfig(workspace).catch(() => null);
|
|
12662
|
-
const configExists = await hasConfig(workspace);
|
|
12663
|
-
const isGitRepo = await pathExists(resolve7(workspace, ".git"));
|
|
12664
|
-
const ultracCredentials = await resolveUltracCredentials();
|
|
12665
|
-
const baseUrl = ultracCredentials ? { baseUrl: ultracCredentials.baseUrl, source: ultracCredentials.baseUrlSource } : resolveBaseUrl();
|
|
12666
|
-
const tools = createDefaultToolRegistry().list();
|
|
12667
|
-
const toolSchemasReady = tools.every((tool) => tool.inputSchema && typeof tool.inputSchema === "object");
|
|
12668
|
-
const checks = [
|
|
12669
|
-
{ label: "node", value: `${process.versions.node} (requires >=22.12.0)`, ok: nodeReady },
|
|
12670
|
-
{
|
|
12671
|
-
label: "workspace",
|
|
12672
|
-
value: workspaceReady ? workspace : `${workspace} - must be an accessible directory`,
|
|
12673
|
-
ok: workspaceReady
|
|
12674
|
-
},
|
|
12675
|
-
{ label: "git repository", value: isGitRepo ? "found" : "not found", ok: isGitRepo },
|
|
12676
|
-
{
|
|
12677
|
-
label: "config",
|
|
12678
|
-
value: config ? configExists ? configPath(workspace) : "using defaults" : `unable to load configuration - check ${configPath(workspace)} and local trust settings`,
|
|
12679
|
-
ok: config !== null
|
|
12680
|
-
},
|
|
12681
|
-
{
|
|
12682
|
-
label: "tool schemas",
|
|
12683
|
-
value: `${tools.length} exported`,
|
|
12684
|
-
ok: toolSchemasReady
|
|
12685
|
-
},
|
|
12686
|
-
{ label: "provider", value: config?.provider ?? "unavailable", ok: config !== null },
|
|
12687
|
-
{ label: "model", value: config?.model ?? "unavailable", ok: config !== null },
|
|
12688
|
-
{
|
|
12689
|
-
label: "Ultrac sign-in",
|
|
12690
|
-
value: ultracCredentials ? `signed in via ${describeKeySource(ultracCredentials)}` : "not signed in - run `ultrac login` or set ULTRAC_API_KEY",
|
|
12691
|
-
ok: config?.provider !== "ultrac" || Boolean(ultracCredentials)
|
|
12692
|
-
},
|
|
12693
|
-
{
|
|
12694
|
-
label: "Ultrac API base URL",
|
|
12695
|
-
value: `${baseUrl.baseUrl} via ${describeBaseUrlSource(baseUrl.source)}`,
|
|
12696
|
-
ok: true
|
|
12697
|
-
}
|
|
12698
|
-
];
|
|
12699
|
-
if (options.json) {
|
|
12700
|
-
console.log(JSON.stringify({ ok: checks.every((check) => check.ok), checks }, null, 2));
|
|
12701
|
-
} else {
|
|
12702
|
-
checks.forEach((check) => printCheck(check));
|
|
12703
|
-
}
|
|
12704
|
-
if (checks.some((check) => !check.ok)) {
|
|
12705
|
-
process.exitCode = 1;
|
|
12706
|
-
}
|
|
13035
|
+
await runDoctor(options.workspace, options.json);
|
|
12707
13036
|
});
|
|
12708
13037
|
}
|
|
13038
|
+
async function runDoctor(workspacePath, json = false, setExitCode = true) {
|
|
13039
|
+
const workspace = resolve7(workspacePath);
|
|
13040
|
+
const workspaceReady = await stat10(workspace).then((entry) => entry.isDirectory(), () => false);
|
|
13041
|
+
const [nodeMajor = 0, nodeMinor = 0] = process.versions.node.split(".").map(Number);
|
|
13042
|
+
const nodeReady = nodeMajor > 22 || nodeMajor === 22 && nodeMinor >= 12;
|
|
13043
|
+
const config = await loadConfig(workspace).catch(() => null);
|
|
13044
|
+
const configExists = await hasConfig(workspace);
|
|
13045
|
+
const isGitRepo = await pathExists(resolve7(workspace, ".git"));
|
|
13046
|
+
const ultracCredentials = await resolveUltracCredentials();
|
|
13047
|
+
const baseUrl = ultracCredentials ? { baseUrl: ultracCredentials.baseUrl, source: ultracCredentials.baseUrlSource } : resolveBaseUrl();
|
|
13048
|
+
const tools = createDefaultToolRegistry().list();
|
|
13049
|
+
const toolSchemasReady = tools.every((tool) => tool.inputSchema && typeof tool.inputSchema === "object");
|
|
13050
|
+
const checks = [
|
|
13051
|
+
{ label: "node", value: `${process.versions.node} (requires >=22.12.0)`, ok: nodeReady },
|
|
13052
|
+
{
|
|
13053
|
+
label: "workspace",
|
|
13054
|
+
value: workspaceReady ? workspace : `${workspace} - must be an accessible directory`,
|
|
13055
|
+
ok: workspaceReady
|
|
13056
|
+
},
|
|
13057
|
+
{ label: "git repository", value: isGitRepo ? "found" : "not found", ok: isGitRepo },
|
|
13058
|
+
{
|
|
13059
|
+
label: "config",
|
|
13060
|
+
value: config ? configExists ? configPath(workspace) : "using defaults" : `unable to load configuration - check ${configPath(workspace)} and local trust settings`,
|
|
13061
|
+
ok: config !== null
|
|
13062
|
+
},
|
|
13063
|
+
{
|
|
13064
|
+
label: "tool schemas",
|
|
13065
|
+
value: `${tools.length} exported`,
|
|
13066
|
+
ok: toolSchemasReady
|
|
13067
|
+
},
|
|
13068
|
+
{ label: "provider", value: config?.provider ?? "unavailable", ok: config !== null },
|
|
13069
|
+
{ label: "model", value: config?.model ?? "unavailable", ok: config !== null },
|
|
13070
|
+
{
|
|
13071
|
+
label: "Ultrac sign-in",
|
|
13072
|
+
value: ultracCredentials ? `signed in via ${describeKeySource(ultracCredentials)}` : "not signed in - run `ultrac login` or set ULTRAC_API_KEY",
|
|
13073
|
+
ok: config?.provider !== "ultrac" || Boolean(ultracCredentials)
|
|
13074
|
+
},
|
|
13075
|
+
{
|
|
13076
|
+
label: "Ultrac API base URL",
|
|
13077
|
+
value: `${baseUrl.baseUrl} via ${describeBaseUrlSource(baseUrl.source)}`,
|
|
13078
|
+
ok: true
|
|
13079
|
+
}
|
|
13080
|
+
];
|
|
13081
|
+
if (json) {
|
|
13082
|
+
console.log(JSON.stringify({ ok: checks.every((check) => check.ok), checks }, null, 2));
|
|
13083
|
+
} else {
|
|
13084
|
+
checks.forEach((check) => printCheck(check));
|
|
13085
|
+
}
|
|
13086
|
+
if (setExitCode && checks.some((check) => !check.ok)) {
|
|
13087
|
+
process.exitCode = 1;
|
|
13088
|
+
}
|
|
13089
|
+
}
|
|
12709
13090
|
async function pathExists(path) {
|
|
12710
13091
|
try {
|
|
12711
13092
|
await access2(path);
|
|
@@ -13522,7 +13903,10 @@ function buildHarnessManifest() {
|
|
|
13522
13903
|
}
|
|
13523
13904
|
|
|
13524
13905
|
// src/commands/mcp.ts
|
|
13525
|
-
import { resolve as resolve9 } from "path";
|
|
13906
|
+
import { resolve as resolve9, dirname as dirname12, join as join12 } from "path";
|
|
13907
|
+
import { constants as constants5 } from "fs";
|
|
13908
|
+
import { lstat as lstat3, mkdir as mkdir14, open as open8, rename as rename2, unlink as unlink3 } from "fs/promises";
|
|
13909
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
13526
13910
|
import chalk5 from "chalk";
|
|
13527
13911
|
|
|
13528
13912
|
// src/mcp/client.ts
|
|
@@ -14348,7 +14732,47 @@ function asError(error) {
|
|
|
14348
14732
|
|
|
14349
14733
|
// src/commands/mcp.ts
|
|
14350
14734
|
function registerMcpCommand(program2) {
|
|
14351
|
-
const mcpCommand = program2.command("mcp").description("
|
|
14735
|
+
const mcpCommand = program2.command("mcp").description("Configure and inspect workspace MCP servers");
|
|
14736
|
+
mcpCommand.command("add <name> <command> [args...]").description("Add a stdio MCP server without starting it").option("-w, --workspace <path>", "Workspace directory", process.cwd()).option("--env <NAME=value>", "Explicit child environment value; prefer --env-pass for credentials", collect, []).option("--env-pass <NAME>", "Forward a named parent environment variable", collect, []).option("--cwd <path>", "Server working directory").option("--timeout <milliseconds>", "Per-request timeout", "30000").option("--disabled", "Keep the server disabled", false).option("--force", "Replace an existing server with this name", false).action(async (name, command, args, options) => {
|
|
14737
|
+
validateServerName(name);
|
|
14738
|
+
const env2 = {};
|
|
14739
|
+
for (const entry of options.env) {
|
|
14740
|
+
const separator = entry.indexOf("=");
|
|
14741
|
+
if (separator < 1) throw new Error("--env must use NAME=value.");
|
|
14742
|
+
const key = entry.slice(0, separator);
|
|
14743
|
+
validateEnvironmentName(key);
|
|
14744
|
+
Object.defineProperty(env2, key, { value: entry.slice(separator + 1), enumerable: true, configurable: true });
|
|
14745
|
+
}
|
|
14746
|
+
options.envPass.forEach(validateEnvironmentName);
|
|
14747
|
+
const spec = McpServerSchema.parse({
|
|
14748
|
+
command,
|
|
14749
|
+
args,
|
|
14750
|
+
env: env2,
|
|
14751
|
+
envPassthrough: [...new Set(options.envPass)],
|
|
14752
|
+
cwd: options.cwd,
|
|
14753
|
+
enabled: !options.disabled,
|
|
14754
|
+
timeoutMs: Number(options.timeout)
|
|
14755
|
+
});
|
|
14756
|
+
const workspace = await assertWorkspaceExists(options.workspace);
|
|
14757
|
+
await updateMcpConfig(workspace, (config) => {
|
|
14758
|
+
const servers = { ...config.mcpServers ?? {} };
|
|
14759
|
+
if (Object.hasOwn(servers, name) && !options.force) throw new Error(`MCP server ${name} already exists. Use --force to replace it.`);
|
|
14760
|
+
servers[name] = spec;
|
|
14761
|
+
return { ...config, mcpServers: servers };
|
|
14762
|
+
});
|
|
14763
|
+
console.log(`Added MCP server ${name}. Run ultrac trust in this workspace to approve the updated settings.`);
|
|
14764
|
+
});
|
|
14765
|
+
mcpCommand.command("remove <name>").description("Remove a workspace MCP server").option("-w, --workspace <path>", "Workspace directory", process.cwd()).action(async (name, options) => {
|
|
14766
|
+
validateServerName(name);
|
|
14767
|
+
const workspace = await assertWorkspaceExists(options.workspace);
|
|
14768
|
+
await updateMcpConfig(workspace, (config) => {
|
|
14769
|
+
const servers = { ...config.mcpServers ?? {} };
|
|
14770
|
+
if (!Object.hasOwn(servers, name)) throw new Error(`No MCP server named ${name}.`);
|
|
14771
|
+
delete servers[name];
|
|
14772
|
+
return { ...config, mcpServers: servers };
|
|
14773
|
+
});
|
|
14774
|
+
console.log(`Removed MCP server ${name}. Reapprove changed workspace settings with ultrac trust if required.`);
|
|
14775
|
+
});
|
|
14352
14776
|
mcpCommand.command("list", { isDefault: true }).description("Connect every configured server and report what it exports").option("-w, --workspace <path>", "Workspace directory", process.cwd()).option("--json", "Print the result as JSON", false).action(async (options) => {
|
|
14353
14777
|
const workspace = resolve9(options.workspace);
|
|
14354
14778
|
const loaded = await loadConfigWithTrust(workspace);
|
|
@@ -14374,6 +14798,7 @@ function registerMcpCommand(program2) {
|
|
|
14374
14798
|
}
|
|
14375
14799
|
const runtime = await withRuntime(workspace, servers, (active) => active);
|
|
14376
14800
|
try {
|
|
14801
|
+
if (runtime.statuses.some((status) => !status.ok)) process.exitCode = 1;
|
|
14377
14802
|
if (options.json) {
|
|
14378
14803
|
console.log(JSON.stringify({ servers: runtime.statuses }, null, 2));
|
|
14379
14804
|
return;
|
|
@@ -14396,7 +14821,7 @@ function registerMcpCommand(program2) {
|
|
|
14396
14821
|
runtime.close();
|
|
14397
14822
|
}
|
|
14398
14823
|
});
|
|
14399
|
-
mcpCommand.command("call <server> <tool>").description("Call one MCP tool directly, for checking a server by hand").option("-w, --workspace <path>", "Workspace directory", process.cwd()).option("--input <json>", "Tool arguments as a JSON object", "{}").action(async (server, tool, options) => {
|
|
14824
|
+
mcpCommand.command("call <server> <tool>").description("Call one MCP tool directly, for checking a server by hand").option("-w, --workspace <path>", "Workspace directory", process.cwd()).option("--input <json>", "Tool arguments as a JSON object", "{}").option("--json", "Print the result as JSON", false).action(async (server, tool, options) => {
|
|
14400
14825
|
const workspace = resolve9(options.workspace);
|
|
14401
14826
|
const loaded = await loadConfigWithTrust(workspace);
|
|
14402
14827
|
const spec = loaded.config.mcpServers?.[server];
|
|
@@ -14414,6 +14839,7 @@ function registerMcpCommand(program2) {
|
|
|
14414
14839
|
} catch (error) {
|
|
14415
14840
|
throw new Error(`--input must be a JSON object: ${error instanceof Error ? error.message : String(error)}`);
|
|
14416
14841
|
}
|
|
14842
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) throw new Error("--input must be a JSON object.");
|
|
14417
14843
|
const runtime = await withRuntime(workspace, { [server]: spec }, (active) => active);
|
|
14418
14844
|
try {
|
|
14419
14845
|
const status = runtime.statuses[0];
|
|
@@ -14438,6 +14864,68 @@ function registerMcpCommand(program2) {
|
|
|
14438
14864
|
}
|
|
14439
14865
|
});
|
|
14440
14866
|
}
|
|
14867
|
+
function collect(value, previous) {
|
|
14868
|
+
return [...previous, value];
|
|
14869
|
+
}
|
|
14870
|
+
function validateServerName(name) {
|
|
14871
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9_.-]{0,79}$/.test(name) || name.includes("__") || ["constructor", "prototype"].includes(name)) {
|
|
14872
|
+
throw new Error("MCP server names must contain letters, numbers, dots, single underscores, or hyphens.");
|
|
14873
|
+
}
|
|
14874
|
+
}
|
|
14875
|
+
function validateEnvironmentName(name) {
|
|
14876
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) throw new Error("Environment variable names must contain letters, numbers, or underscores.");
|
|
14877
|
+
}
|
|
14878
|
+
async function updateMcpConfig(workspace, update) {
|
|
14879
|
+
const target = configPath(workspace);
|
|
14880
|
+
await resolveInsideWorkspaceReal(workspace, target);
|
|
14881
|
+
await mkdir14(dirname12(target), { recursive: true, mode: 448 });
|
|
14882
|
+
await resolveInsideWorkspaceReal(workspace, target);
|
|
14883
|
+
const lockPath = `${target}.mcp-lock`;
|
|
14884
|
+
const lock = await open8(lockPath, "wx", 384).catch((error) => {
|
|
14885
|
+
if (error.code === "EEXIST") throw new Error("Another MCP configuration update is in progress.");
|
|
14886
|
+
throw error;
|
|
14887
|
+
});
|
|
14888
|
+
const temporary = join12(dirname12(target), `.mcp-config-${randomUUID4()}.tmp`);
|
|
14889
|
+
try {
|
|
14890
|
+
let config = defaultConfig();
|
|
14891
|
+
const existing = await lstat3(target).catch((error) => {
|
|
14892
|
+
if (error.code === "ENOENT") return null;
|
|
14893
|
+
throw error;
|
|
14894
|
+
});
|
|
14895
|
+
if (existing) {
|
|
14896
|
+
if (!existing.isFile() || existing.isSymbolicLink() || existing.size > 1048576) throw new Error("MCP config must be a regular JSON file no larger than 1 MiB.");
|
|
14897
|
+
const handle2 = await open8(target, constants5.O_RDONLY | constants5.O_NOFOLLOW);
|
|
14898
|
+
try {
|
|
14899
|
+
const content = await handle2.readFile("utf8");
|
|
14900
|
+
try {
|
|
14901
|
+
config = JSON.parse(content);
|
|
14902
|
+
} catch {
|
|
14903
|
+
throw new Error("Workspace config is not valid JSON.");
|
|
14904
|
+
}
|
|
14905
|
+
} finally {
|
|
14906
|
+
await handle2.close();
|
|
14907
|
+
}
|
|
14908
|
+
if (!config || typeof config !== "object" || Array.isArray(config)) throw new Error("Workspace config must be a JSON object.");
|
|
14909
|
+
if (!UltracConfigSchema.safeParse(config).success) throw new Error("Workspace config does not match the Ultrac configuration schema.");
|
|
14910
|
+
}
|
|
14911
|
+
const next = update(config);
|
|
14912
|
+
UltracConfigSchema.parse(next);
|
|
14913
|
+
const handle = await open8(temporary, "wx", 384);
|
|
14914
|
+
try {
|
|
14915
|
+
await handle.writeFile(`${JSON.stringify(next, null, 2)}
|
|
14916
|
+
`, "utf8");
|
|
14917
|
+
await handle.sync();
|
|
14918
|
+
} finally {
|
|
14919
|
+
await handle.close();
|
|
14920
|
+
}
|
|
14921
|
+
await resolveInsideWorkspaceReal(workspace, target);
|
|
14922
|
+
await rename2(temporary, target);
|
|
14923
|
+
} finally {
|
|
14924
|
+
await unlink3(temporary).catch(() => void 0);
|
|
14925
|
+
await lock.close();
|
|
14926
|
+
await unlink3(lockPath).catch(() => void 0);
|
|
14927
|
+
}
|
|
14928
|
+
}
|
|
14441
14929
|
async function withRuntime(workspace, servers, use) {
|
|
14442
14930
|
const packageInfo = readPackageInfo();
|
|
14443
14931
|
const runtime = await connectMcpServers(servers, {
|
|
@@ -14554,7 +15042,7 @@ import chalk6 from "chalk";
|
|
|
14554
15042
|
|
|
14555
15043
|
// src/notify/config.ts
|
|
14556
15044
|
import { chmod as chmod3, readFile as readFile12, rm as rm4, writeFile as writeFile11 } from "fs/promises";
|
|
14557
|
-
import { join as
|
|
15045
|
+
import { join as join13 } from "path";
|
|
14558
15046
|
import { z as z31 } from "zod";
|
|
14559
15047
|
var TelegramSchema = z31.object({
|
|
14560
15048
|
botToken: z31.string().min(1),
|
|
@@ -14580,7 +15068,7 @@ var NotifyConfigSchema = z31.object({
|
|
|
14580
15068
|
answerTimeoutSeconds: z31.number().int().min(30).max(86400).default(1800)
|
|
14581
15069
|
});
|
|
14582
15070
|
function notifyConfigPath() {
|
|
14583
|
-
return
|
|
15071
|
+
return join13(ultracHomeDir(), "notify.json");
|
|
14584
15072
|
}
|
|
14585
15073
|
async function readStoredNotifyConfig() {
|
|
14586
15074
|
let text;
|
|
@@ -15019,8 +15507,8 @@ function registerNotifyCommand(program2) {
|
|
|
15019
15507
|
}
|
|
15020
15508
|
|
|
15021
15509
|
// src/commands/repl.ts
|
|
15022
|
-
import { createInterface as createInterface3 } from "readline";
|
|
15023
|
-
import { stdin as stdin5, stdout as stdout5 } from "process";
|
|
15510
|
+
import { createInterface as createInterface3, emitKeypressEvents } from "readline";
|
|
15511
|
+
import { stdin as stdin5, stdout as stdout5, stderr } from "process";
|
|
15024
15512
|
import chalk11 from "chalk";
|
|
15025
15513
|
|
|
15026
15514
|
// src/config/budget.ts
|
|
@@ -15197,6 +15685,9 @@ function registerTrustCommand(program2) {
|
|
|
15197
15685
|
// src/core/manual-compaction.ts
|
|
15198
15686
|
async function compactManually(messages, pinned, provider, model, focus = "", signal) {
|
|
15199
15687
|
const retained = new Set(pinned);
|
|
15688
|
+
const units = completeContextUnits(messages);
|
|
15689
|
+
if (!units) throw new Error("Compaction found incomplete tool units; original context was kept.");
|
|
15690
|
+
for (const unit of units) if (unit.some((message) => retained.has(message))) for (const message of unit) retained.add(message);
|
|
15200
15691
|
const history = messages.filter(
|
|
15201
15692
|
(message) => message.role !== "system" && !retained.has(message) && !isCompactionSummary(message)
|
|
15202
15693
|
);
|
|
@@ -15252,6 +15743,10 @@ Give extra attention to this user-supplied focus: ${focus}` : ""
|
|
|
15252
15743
|
}
|
|
15253
15744
|
|
|
15254
15745
|
// src/core/interactive.ts
|
|
15746
|
+
var SKIPPED_FOR_QUEUED_MESSAGE = "Skipped; a new user message was queued after the previous tool. Follow that message instead of retrying this call.";
|
|
15747
|
+
function skippedForQueuedMessage(tool) {
|
|
15748
|
+
return { tool, ok: false, content: SKIPPED_FOR_QUEUED_MESSAGE };
|
|
15749
|
+
}
|
|
15255
15750
|
function permissionDeniedMessage(tool, permission, sticky) {
|
|
15256
15751
|
const denied = `User denied ${permission} permission for ${tool}.`;
|
|
15257
15752
|
if (!sticky) return denied;
|
|
@@ -15310,6 +15805,7 @@ var InteractiveSession = class {
|
|
|
15310
15805
|
started = false;
|
|
15311
15806
|
/** How the last turn ended. Absent until one has. */
|
|
15312
15807
|
lastStopReason;
|
|
15808
|
+
periodicCompactor = new PeriodicCompactor();
|
|
15313
15809
|
pinnedMessages = [];
|
|
15314
15810
|
/** Tools described in this turn's system prompt. `/mode` can change it. */
|
|
15315
15811
|
advertisedTools() {
|
|
@@ -15348,6 +15844,7 @@ var InteractiveSession = class {
|
|
|
15348
15844
|
}
|
|
15349
15845
|
async restoreSessions(sessions) {
|
|
15350
15846
|
if (this.steps.length || this.userConstraints.length) throw new Error("Restore requires a fresh session.");
|
|
15847
|
+
for (const saved of sessions) readSessionContextSnapshot(saved);
|
|
15351
15848
|
await this.start();
|
|
15352
15849
|
this.parentSessionId = sessions.at(-1)?.id;
|
|
15353
15850
|
this.firstTask = sessions.at(-1)?.task ?? "";
|
|
@@ -15368,6 +15865,17 @@ var InteractiveSession = class {
|
|
|
15368
15865
|
for (const { call, result } of step.toolResults) this.messages.push({ role: "tool", content: JSON.stringify({ call, result: capResultForTranscript(result, this.options.maxToolResultChars) }) });
|
|
15369
15866
|
}
|
|
15370
15867
|
}
|
|
15868
|
+
const snapshot = sessions.at(-1)?.contextSnapshot;
|
|
15869
|
+
if (snapshot && snapshot.kind !== "text") this.messages.push(...snapshot.messages.filter(isCompactionSummary).map((message) => ({ role: "user", content: message.content })));
|
|
15870
|
+
if (snapshot?.kind === "text") {
|
|
15871
|
+
this.messages.splice(0, this.messages.length, ...snapshot.messages);
|
|
15872
|
+
this.pinnedMessages.splice(0, this.pinnedMessages.length, ...this.messages.filter((message) => message.role === "user" && !isCompactionSummary(message)));
|
|
15873
|
+
for (const constraint of this.userConstraints) if (!this.messages.some((message) => message.role === "user" && message.content === constraint.content)) {
|
|
15874
|
+
const message = { role: "user", content: constraint.content };
|
|
15875
|
+
this.messages.push(message);
|
|
15876
|
+
this.pinnedMessages.push(message);
|
|
15877
|
+
}
|
|
15878
|
+
}
|
|
15371
15879
|
}
|
|
15372
15880
|
contextStatus() {
|
|
15373
15881
|
const mode = getAgentModePreset(this.activeMode);
|
|
@@ -15430,6 +15938,7 @@ Acknowledge that you have the context and wait for the user's first request.`
|
|
|
15430
15938
|
try {
|
|
15431
15939
|
await handlers.onCheckpoint(this.toSession());
|
|
15432
15940
|
} catch {
|
|
15941
|
+
diagnosticEvent("session.checkpoint", { outcome: "failed", path: "interactive" });
|
|
15433
15942
|
}
|
|
15434
15943
|
}
|
|
15435
15944
|
appendQueuedMessages(handlers) {
|
|
@@ -15472,6 +15981,11 @@ Acknowledge that you have the context and wait for the user's first request.`
|
|
|
15472
15981
|
modePreset
|
|
15473
15982
|
);
|
|
15474
15983
|
const turn = this.steps.length + 1;
|
|
15984
|
+
const periodic = await this.periodicCompactor.compact(this.messages, this.pinnedMessages, this.options.provider, this.activeModel, this.steps.length, handlers.signal);
|
|
15985
|
+
if (periodic.compacted) {
|
|
15986
|
+
this.messages.splice(0, this.messages.length, ...periodic.messages);
|
|
15987
|
+
await this.checkpoint(handlers);
|
|
15988
|
+
}
|
|
15475
15989
|
await this.compactIfNeeded(system, handlers, turn);
|
|
15476
15990
|
handlers.onModelStart?.();
|
|
15477
15991
|
let text;
|
|
@@ -15512,12 +16026,13 @@ Acknowledge that you have the context and wait for the user's first request.`
|
|
|
15512
16026
|
this.messages.push({ role: "assistant", content: JSON.stringify(parsed) });
|
|
15513
16027
|
this.steps.push(step);
|
|
15514
16028
|
await this.checkpoint(handlers);
|
|
15515
|
-
for (
|
|
16029
|
+
for (let callIndex = 0; callIndex < parsed.toolCalls.length; callIndex += 1) {
|
|
16030
|
+
const call = parsed.toolCalls[callIndex];
|
|
15516
16031
|
if (handlers.signal?.aborted) break;
|
|
15517
16032
|
const authorization = await this.authorize(call, handlers);
|
|
15518
16033
|
if (handlers.signal?.aborted) break;
|
|
15519
16034
|
if (authorization.denied) {
|
|
15520
|
-
const
|
|
16035
|
+
const result = {
|
|
15521
16036
|
tool: call.tool,
|
|
15522
16037
|
ok: false,
|
|
15523
16038
|
content: authorization.message ?? permissionDeniedMessage(
|
|
@@ -15527,24 +16042,33 @@ Acknowledge that you have the context and wait for the user's first request.`
|
|
|
15527
16042
|
)
|
|
15528
16043
|
};
|
|
15529
16044
|
if (authorization.permission) handlers.onToolDenied?.(call, authorization.permission);
|
|
15530
|
-
step.toolResults.push({ call, result
|
|
15531
|
-
this.pushToolResult(call,
|
|
16045
|
+
step.toolResults.push({ call, result });
|
|
16046
|
+
this.pushToolResult(call, result);
|
|
16047
|
+
await this.checkpoint(handlers);
|
|
16048
|
+
} else {
|
|
16049
|
+
handlers.onToolStart?.(call);
|
|
16050
|
+
const modeAllowlist = getEnforcedModeToolAllowlist(this.activeMode);
|
|
16051
|
+
const rawResult = await this.registry.execute(call, {
|
|
16052
|
+
workspace: this.options.workspace,
|
|
16053
|
+
permissions: getEffectiveModePermissions(this.activeMode, authorization.permissions),
|
|
16054
|
+
...handlers.signal ? { signal: handlers.signal } : {},
|
|
16055
|
+
...modeAllowlist ? { modeAllowlist } : {}
|
|
16056
|
+
});
|
|
16057
|
+
const result = await this.mcpResultArchive.store(rawResult, this.options.maxToolResultChars);
|
|
16058
|
+
handlers.onToolResult?.(call, result);
|
|
16059
|
+
step.toolResults.push({ call, result });
|
|
16060
|
+
this.pushToolResult(call, result);
|
|
15532
16061
|
await this.checkpoint(handlers);
|
|
15533
|
-
continue;
|
|
15534
16062
|
}
|
|
15535
|
-
|
|
15536
|
-
|
|
15537
|
-
|
|
15538
|
-
|
|
15539
|
-
|
|
15540
|
-
|
|
15541
|
-
|
|
15542
|
-
|
|
15543
|
-
|
|
15544
|
-
handlers.onToolResult?.(call, result);
|
|
15545
|
-
step.toolResults.push({ call, result });
|
|
15546
|
-
this.pushToolResult(call, result);
|
|
15547
|
-
await this.checkpoint(handlers);
|
|
16063
|
+
if (this.appendQueuedMessages(handlers) > 0) {
|
|
16064
|
+
for (const skipped of parsed.toolCalls.slice(callIndex + 1)) {
|
|
16065
|
+
const result = skippedForQueuedMessage(skipped.tool);
|
|
16066
|
+
step.toolResults.push({ call: skipped, result });
|
|
16067
|
+
this.pushToolResult(skipped, result);
|
|
16068
|
+
await this.checkpoint(handlers);
|
|
16069
|
+
}
|
|
16070
|
+
break;
|
|
16071
|
+
}
|
|
15548
16072
|
}
|
|
15549
16073
|
if (handlers.signal?.aborted) {
|
|
15550
16074
|
stopReason = "aborted";
|
|
@@ -15604,7 +16128,7 @@ Acknowledge that you have the context and wait for the user's first request.`
|
|
|
15604
16128
|
mode: this.activeMode,
|
|
15605
16129
|
purpose: "turn"
|
|
15606
16130
|
}),
|
|
15607
|
-
// The turn's own signal:
|
|
16131
|
+
// The turn's own signal: Esc abandons the summary, not the turn.
|
|
15608
16132
|
...handlers.signal ? { signal: handlers.signal } : {}
|
|
15609
16133
|
}
|
|
15610
16134
|
);
|
|
@@ -15751,6 +16275,7 @@ Acknowledge that you have the context and wait for the user's first request.`
|
|
|
15751
16275
|
constraintVersion: 1,
|
|
15752
16276
|
userConstraints: [...this.userConstraints],
|
|
15753
16277
|
mcpResultArchiveVersion: 1,
|
|
16278
|
+
...this.messages.some(isCompactionSummary) ? { contextSnapshot: { version: 1, kind: "text", messages: snapshotWithConstraints(this.messages, this.userConstraints), completedSteps: this.steps.length } } : {},
|
|
15754
16279
|
// How the last turn ended, so a saved interactive transcript says whether
|
|
15755
16280
|
// its final message is an answer or the place it ran out of room - the
|
|
15756
16281
|
// same question `ultrac run` already answers for a one-shot run.
|
|
@@ -15803,6 +16328,7 @@ var NativeSession = class {
|
|
|
15803
16328
|
started = false;
|
|
15804
16329
|
/** How the last turn ended. Absent until one has. */
|
|
15805
16330
|
lastStopReason;
|
|
16331
|
+
periodicCompactor = new PeriodicCompactor();
|
|
15806
16332
|
pinnedMessages = [];
|
|
15807
16333
|
/**
|
|
15808
16334
|
* The tools this turn advertises. Resolved per turn rather than once in the
|
|
@@ -15844,6 +16370,7 @@ var NativeSession = class {
|
|
|
15844
16370
|
}
|
|
15845
16371
|
async restoreSessions(sessions) {
|
|
15846
16372
|
if (this.steps.length || this.userConstraints.length) throw new Error("Restore requires a fresh session.");
|
|
16373
|
+
for (const saved of sessions) readSessionContextSnapshot(saved);
|
|
15847
16374
|
await this.start();
|
|
15848
16375
|
this.parentSessionId = sessions.at(-1)?.id;
|
|
15849
16376
|
this.firstTask = sessions.at(-1)?.task ?? "";
|
|
@@ -15871,6 +16398,17 @@ var NativeSession = class {
|
|
|
15871
16398
|
}
|
|
15872
16399
|
}
|
|
15873
16400
|
}
|
|
16401
|
+
const snapshot = sessions.at(-1)?.contextSnapshot;
|
|
16402
|
+
if (snapshot && snapshot.kind !== "native") this.messages.push(...snapshot.messages.filter(isCompactionSummary).map((message) => ({ role: "user", content: message.content })));
|
|
16403
|
+
if (snapshot?.kind === "native") {
|
|
16404
|
+
this.messages.splice(0, this.messages.length, ...snapshot.messages);
|
|
16405
|
+
this.pinnedMessages.splice(0, this.pinnedMessages.length, ...this.messages.filter((message) => message.role === "user" && !isCompactionSummary(message)));
|
|
16406
|
+
for (const constraint of this.userConstraints) if (!this.messages.some((message) => message.role === "user" && message.content === constraint.content)) {
|
|
16407
|
+
const message = { role: "user", content: constraint.content };
|
|
16408
|
+
this.messages.push(message);
|
|
16409
|
+
this.pinnedMessages.push(message);
|
|
16410
|
+
}
|
|
16411
|
+
}
|
|
15874
16412
|
}
|
|
15875
16413
|
contextStatus() {
|
|
15876
16414
|
const system = this.systemPrompt();
|
|
@@ -15931,6 +16469,7 @@ ${context}`
|
|
|
15931
16469
|
try {
|
|
15932
16470
|
await handlers.onCheckpoint(this.toSession());
|
|
15933
16471
|
} catch {
|
|
16472
|
+
diagnosticEvent("session.checkpoint", { outcome: "failed", path: "interactive" });
|
|
15934
16473
|
}
|
|
15935
16474
|
}
|
|
15936
16475
|
appendQueuedMessages(handlers) {
|
|
@@ -15969,6 +16508,11 @@ ${context}`
|
|
|
15969
16508
|
this.refreshSystemPrompt();
|
|
15970
16509
|
const tools = toToolSchemas(this.advertisedTools());
|
|
15971
16510
|
const turn = this.steps.length + 1;
|
|
16511
|
+
const periodic = await this.periodicCompactor.compact(this.messages, this.pinnedMessages, this.options.provider, this.activeModel, this.steps.length, handlers.signal);
|
|
16512
|
+
if (periodic.compacted) {
|
|
16513
|
+
this.messages.splice(0, this.messages.length, ...periodic.messages);
|
|
16514
|
+
await this.checkpoint(handlers);
|
|
16515
|
+
}
|
|
15972
16516
|
await this.compactIfNeeded(tools, handlers, turn);
|
|
15973
16517
|
handlers.onModelStart?.();
|
|
15974
16518
|
let response;
|
|
@@ -16029,13 +16573,14 @@ ${context}`
|
|
|
16029
16573
|
}
|
|
16030
16574
|
continue;
|
|
16031
16575
|
}
|
|
16032
|
-
for (
|
|
16576
|
+
for (let callIndex = 0; callIndex < response.toolCalls.length; callIndex += 1) {
|
|
16577
|
+
const call = response.toolCalls[callIndex];
|
|
16033
16578
|
if (handlers.signal?.aborted) break;
|
|
16034
16579
|
const displayCall = { tool: call.name, input: safeParseArgs(call.arguments) };
|
|
16035
16580
|
const authorization = await this.authorize(call.name, handlers, displayCall);
|
|
16036
16581
|
if (handlers.signal?.aborted) break;
|
|
16037
16582
|
if (authorization.denied) {
|
|
16038
|
-
const
|
|
16583
|
+
const result = {
|
|
16039
16584
|
tool: call.name,
|
|
16040
16585
|
ok: false,
|
|
16041
16586
|
content: authorization.message ?? permissionDeniedMessage(
|
|
@@ -16045,27 +16590,37 @@ ${context}`
|
|
|
16045
16590
|
)
|
|
16046
16591
|
};
|
|
16047
16592
|
if (authorization.permission) handlers.onToolDenied?.(displayCall, authorization.permission);
|
|
16048
|
-
step.toolResults.push({ call: displayCall, result
|
|
16049
|
-
this.pushToolResult(call,
|
|
16593
|
+
step.toolResults.push({ call: displayCall, result });
|
|
16594
|
+
this.pushToolResult(call, result);
|
|
16595
|
+
await this.checkpoint(handlers);
|
|
16596
|
+
} else {
|
|
16597
|
+
handlers.onToolStart?.(displayCall);
|
|
16598
|
+
const modeAllowlist = getEnforcedModeToolAllowlist(this.activeMode);
|
|
16599
|
+
const rawResult = await this.registry.execute(
|
|
16600
|
+
{ tool: call.name, input: displayCall.input },
|
|
16601
|
+
{
|
|
16602
|
+
workspace: this.options.workspace,
|
|
16603
|
+
permissions: getEffectiveModePermissions(this.activeMode, authorization.permissions),
|
|
16604
|
+
...handlers.signal ? { signal: handlers.signal } : {},
|
|
16605
|
+
...modeAllowlist ? { modeAllowlist } : {}
|
|
16606
|
+
}
|
|
16607
|
+
);
|
|
16608
|
+
const result = await this.mcpResultArchive.store(rawResult, this.options.maxToolResultChars);
|
|
16609
|
+
handlers.onToolResult?.(displayCall, result);
|
|
16610
|
+
step.toolResults.push({ call: displayCall, result });
|
|
16611
|
+
this.pushToolResult(call, result);
|
|
16050
16612
|
await this.checkpoint(handlers);
|
|
16051
|
-
continue;
|
|
16052
16613
|
}
|
|
16053
|
-
|
|
16054
|
-
|
|
16055
|
-
|
|
16056
|
-
|
|
16057
|
-
|
|
16058
|
-
|
|
16059
|
-
|
|
16060
|
-
...handlers.signal ? { signal: handlers.signal } : {},
|
|
16061
|
-
...modeAllowlist ? { modeAllowlist } : {}
|
|
16614
|
+
if (this.appendQueuedMessages(handlers) > 0) {
|
|
16615
|
+
for (const skipped of response.toolCalls.slice(callIndex + 1)) {
|
|
16616
|
+
const skippedCall = { tool: skipped.name, input: safeParseArgs(skipped.arguments) };
|
|
16617
|
+
const result = skippedForQueuedMessage(skipped.name);
|
|
16618
|
+
step.toolResults.push({ call: skippedCall, result });
|
|
16619
|
+
this.pushToolResult(skipped, result);
|
|
16620
|
+
await this.checkpoint(handlers);
|
|
16062
16621
|
}
|
|
16063
|
-
|
|
16064
|
-
|
|
16065
|
-
handlers.onToolResult?.(displayCall, result);
|
|
16066
|
-
step.toolResults.push({ call: displayCall, result });
|
|
16067
|
-
this.pushToolResult(call, result);
|
|
16068
|
-
await this.checkpoint(handlers);
|
|
16622
|
+
break;
|
|
16623
|
+
}
|
|
16069
16624
|
}
|
|
16070
16625
|
if (handlers.signal?.aborted) {
|
|
16071
16626
|
stopReason = "aborted";
|
|
@@ -16121,7 +16676,7 @@ ${context}`
|
|
|
16121
16676
|
mode: this.activeMode,
|
|
16122
16677
|
purpose: "turn"
|
|
16123
16678
|
}),
|
|
16124
|
-
// The turn's own signal:
|
|
16679
|
+
// The turn's own signal: Esc abandons the summary, not the turn.
|
|
16125
16680
|
...handlers.signal ? { signal: handlers.signal } : {}
|
|
16126
16681
|
}
|
|
16127
16682
|
);
|
|
@@ -16285,6 +16840,7 @@ ${guidance.join("\n")}` : ""}`;
|
|
|
16285
16840
|
constraintVersion: 1,
|
|
16286
16841
|
userConstraints: [...this.userConstraints],
|
|
16287
16842
|
mcpResultArchiveVersion: 1,
|
|
16843
|
+
...this.messages.some(isCompactionSummary) ? { contextSnapshot: { version: 1, kind: "native", messages: snapshotWithConstraints(this.messages, this.userConstraints), completedSteps: this.steps.length } } : {},
|
|
16288
16844
|
// How the last turn ended, so a saved interactive transcript says whether
|
|
16289
16845
|
// its final message is an answer or the place it ran out of room - the
|
|
16290
16846
|
// same question `ultrac run` already answers for a one-shot run.
|
|
@@ -16308,6 +16864,152 @@ function noteNativeCompactionFallback(reason) {
|
|
|
16308
16864
|
);
|
|
16309
16865
|
}
|
|
16310
16866
|
|
|
16867
|
+
// src/core/autonomous.ts
|
|
16868
|
+
import { createHash as createHash7, randomUUID as randomUUID5 } from "crypto";
|
|
16869
|
+
var AUTONOMOUS_CONTINUE_TASK = "Continue the task from where the previous round stopped. Check what is already done, finish the rest, and verify it.";
|
|
16870
|
+
var DEFAULT_AUTONOMOUS_MAX_ROUNDS = 5;
|
|
16871
|
+
var AUTONOMOUS_MAX_STEPS = 200;
|
|
16872
|
+
var DEFAULT_AUTONOMOUS_MAX_ELAPSED_MS = 6 * 60 * 60 * 1e3;
|
|
16873
|
+
var DEFAULT_AUTONOMOUS_BASE_DELAY_MS = 1e3;
|
|
16874
|
+
var DEFAULT_AUTONOMOUS_MAX_DELAY_MS = 3e4;
|
|
16875
|
+
var DEFAULT_AUTONOMOUS_STALL_ROUNDS = 3;
|
|
16876
|
+
function shouldResume(stopReason) {
|
|
16877
|
+
return stopReason === "max-steps" || stopReason === "error";
|
|
16878
|
+
}
|
|
16879
|
+
function boundedInteger(value, name, min, max) {
|
|
16880
|
+
if (!Number.isSafeInteger(value) || value < min || value > max) throw new Error(`${name} must be an integer from ${min} to ${max}.`);
|
|
16881
|
+
return value;
|
|
16882
|
+
}
|
|
16883
|
+
function abortableSleep(milliseconds, signal) {
|
|
16884
|
+
signal.throwIfAborted();
|
|
16885
|
+
return new Promise((resolve15, reject) => {
|
|
16886
|
+
const timer = setTimeout(() => {
|
|
16887
|
+
signal.removeEventListener("abort", abort);
|
|
16888
|
+
resolve15();
|
|
16889
|
+
}, milliseconds);
|
|
16890
|
+
const abort = () => {
|
|
16891
|
+
clearTimeout(timer);
|
|
16892
|
+
signal.removeEventListener("abort", abort);
|
|
16893
|
+
reject(signal.reason);
|
|
16894
|
+
};
|
|
16895
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
16896
|
+
if (signal.aborted) abort();
|
|
16897
|
+
});
|
|
16898
|
+
}
|
|
16899
|
+
function progress(session, startStep = 0) {
|
|
16900
|
+
const hash = createHash7("sha256");
|
|
16901
|
+
let completedTools = 0;
|
|
16902
|
+
hash.update(session.finalMessage ?? "");
|
|
16903
|
+
for (const step of (session.steps ?? []).slice(startStep)) {
|
|
16904
|
+
hash.update(step.modelText ?? "");
|
|
16905
|
+
for (const tool of step.toolResults ?? []) {
|
|
16906
|
+
completedTools += 1;
|
|
16907
|
+
hash.update(tool.call.tool);
|
|
16908
|
+
hash.update(tool.result.ok ? "ok" : "failed");
|
|
16909
|
+
hash.update(tool.result.content ?? "");
|
|
16910
|
+
try {
|
|
16911
|
+
hash.update(JSON.stringify(tool.call.input));
|
|
16912
|
+
} catch {
|
|
16913
|
+
hash.update("unserializable-input");
|
|
16914
|
+
}
|
|
16915
|
+
}
|
|
16916
|
+
}
|
|
16917
|
+
return { fingerprint: hash.digest("hex"), completedTools };
|
|
16918
|
+
}
|
|
16919
|
+
async function runAutonomousLoop(options) {
|
|
16920
|
+
boundedInteger(options.maxRounds, "maxRounds", 1, 1e3);
|
|
16921
|
+
const maxElapsedMs = boundedInteger(options.maxElapsedMs ?? DEFAULT_AUTONOMOUS_MAX_ELAPSED_MS, "maxElapsedMs", 1, 7 * 24 * 60 * 60 * 1e3);
|
|
16922
|
+
const baseDelay = boundedInteger(options.baseDelayMs ?? DEFAULT_AUTONOMOUS_BASE_DELAY_MS, "baseDelayMs", 0, 3e5);
|
|
16923
|
+
const maxDelay = boundedInteger(options.maxDelayMs ?? DEFAULT_AUTONOMOUS_MAX_DELAY_MS, "maxDelayMs", baseDelay, 3e5);
|
|
16924
|
+
const stallLimit = boundedInteger(options.stallRounds ?? DEFAULT_AUTONOMOUS_STALL_ROUNDS, "stallRounds", 1, 1e3);
|
|
16925
|
+
const now = options.now ?? (() => performance.now());
|
|
16926
|
+
const started = now();
|
|
16927
|
+
const elapsed = () => Math.max(0, now() - started);
|
|
16928
|
+
const budget = new AbortController();
|
|
16929
|
+
const deadline = setTimeout(() => budget.abort(new DOMException("Autonomous elapsed budget reached.", "TimeoutError")), maxElapsedMs);
|
|
16930
|
+
deadline.unref?.();
|
|
16931
|
+
const signal = options.signal ? AbortSignal.any([options.signal, budget.signal]) : budget.signal;
|
|
16932
|
+
const sleep2 = options.sleep ?? abortableSleep;
|
|
16933
|
+
const id = randomUUID5();
|
|
16934
|
+
let previous;
|
|
16935
|
+
let lastFingerprint;
|
|
16936
|
+
let lastSessionId;
|
|
16937
|
+
let lastStepCount = 0;
|
|
16938
|
+
let stalls = 0;
|
|
16939
|
+
let consecutiveErrors = 0;
|
|
16940
|
+
try {
|
|
16941
|
+
signal.throwIfAborted();
|
|
16942
|
+
for (let round = 1; ; round += 1) {
|
|
16943
|
+
const roundStart = now();
|
|
16944
|
+
diagnosticEvent("round.start", { id, round, maxRounds: options.maxRounds, elapsedMs: elapsed() });
|
|
16945
|
+
let session;
|
|
16946
|
+
try {
|
|
16947
|
+
session = await options.runRound(round, previous, signal);
|
|
16948
|
+
} catch (error) {
|
|
16949
|
+
diagnosticEvent("round.end", { id, round, reason: signal.aborted ? "aborted" : "exception", elapsedMs: elapsed(), durationMs: Math.max(0, now() - roundStart) });
|
|
16950
|
+
throw error;
|
|
16951
|
+
}
|
|
16952
|
+
const stepCount = session.steps?.length ?? 0;
|
|
16953
|
+
const startStep = session.id === lastSessionId && stepCount > lastStepCount ? lastStepCount : 0;
|
|
16954
|
+
const current = progress(session, startStep);
|
|
16955
|
+
lastSessionId = session.id;
|
|
16956
|
+
lastStepCount = stepCount;
|
|
16957
|
+
const unchanged = lastFingerprint !== void 0 && current.fingerprint === lastFingerprint;
|
|
16958
|
+
stalls = unchanged ? stalls + 1 : 0;
|
|
16959
|
+
lastFingerprint = current.fingerprint;
|
|
16960
|
+
const classification = session.stopReason === "error" ? options.classifyFailure?.(session) : void 0;
|
|
16961
|
+
consecutiveErrors = session.stopReason === "error" ? consecutiveErrors + 1 : 0;
|
|
16962
|
+
let haltReason;
|
|
16963
|
+
if (session.stopReason === "completed") haltReason = "completed";
|
|
16964
|
+
else if (budget.signal.aborted || elapsed() >= maxElapsedMs) haltReason = "elapsed_budget";
|
|
16965
|
+
else if (signal.aborted || session.stopReason === "aborted") haltReason = "aborted";
|
|
16966
|
+
else if (classification && !classification.retryable) haltReason = "permanent_error";
|
|
16967
|
+
else if (!shouldResume(session.stopReason)) haltReason = "stopped";
|
|
16968
|
+
else if (stalls >= stallLimit) haltReason = "stalled";
|
|
16969
|
+
else if (round >= options.maxRounds) haltReason = "max_rounds";
|
|
16970
|
+
const retryDelay = Math.min(maxDelay, baseDelay * 2 ** Math.min(30, consecutiveErrors - 1));
|
|
16971
|
+
if (!haltReason && session.stopReason === "error" && elapsed() + retryDelay >= maxElapsedMs) haltReason = "elapsed_budget";
|
|
16972
|
+
diagnosticEvent("round.end", {
|
|
16973
|
+
id,
|
|
16974
|
+
round,
|
|
16975
|
+
sessionId: session.id,
|
|
16976
|
+
reason: haltReason ?? session.stopReason,
|
|
16977
|
+
failureKind: classification?.kind,
|
|
16978
|
+
attempt: consecutiveErrors,
|
|
16979
|
+
completedTools: current.completedTools,
|
|
16980
|
+
elapsedMs: elapsed(),
|
|
16981
|
+
durationMs: Math.max(0, now() - roundStart)
|
|
16982
|
+
});
|
|
16983
|
+
if (haltReason === "stalled") diagnosticEvent("round.stall", { id, round, repeats: stalls, completedTools: current.completedTools, elapsedMs: elapsed() });
|
|
16984
|
+
await options.onRoundEnd?.(round, session, !haltReason);
|
|
16985
|
+
if (haltReason) return { session, rounds: round, haltReason };
|
|
16986
|
+
if (session.stopReason === "error") {
|
|
16987
|
+
const delayMs = retryDelay;
|
|
16988
|
+
if (elapsed() + delayMs >= maxElapsedMs) return { session, rounds: round, haltReason: "elapsed_budget" };
|
|
16989
|
+
diagnosticEvent("round.retry", {
|
|
16990
|
+
id,
|
|
16991
|
+
round,
|
|
16992
|
+
nextRound: round + 1,
|
|
16993
|
+
attempt: consecutiveErrors,
|
|
16994
|
+
failureKind: classification?.kind ?? "unclassified",
|
|
16995
|
+
delayMs,
|
|
16996
|
+
elapsedMs: elapsed()
|
|
16997
|
+
});
|
|
16998
|
+
try {
|
|
16999
|
+
await sleep2(delayMs, signal);
|
|
17000
|
+
} catch (error) {
|
|
17001
|
+
if (!signal.aborted) throw error;
|
|
17002
|
+
return { session, rounds: round, haltReason: budget.signal.aborted ? "elapsed_budget" : "aborted" };
|
|
17003
|
+
}
|
|
17004
|
+
}
|
|
17005
|
+
if (signal.aborted || elapsed() >= maxElapsedMs) return { session, rounds: round, haltReason: budget.signal.aborted || elapsed() >= maxElapsedMs ? "elapsed_budget" : "aborted" };
|
|
17006
|
+
previous = session;
|
|
17007
|
+
}
|
|
17008
|
+
} finally {
|
|
17009
|
+
clearTimeout(deadline);
|
|
17010
|
+
}
|
|
17011
|
+
}
|
|
17012
|
+
|
|
16311
17013
|
// src/commands/session-checkpoint.ts
|
|
16312
17014
|
var SESSION_CHECKPOINT_FAILURE_WARNING = "Warning: the latest session checkpoint could not be saved; progress since the previous checkpoint may be lost.";
|
|
16313
17015
|
function createSessionCheckpointWriter(workspace, onFailure) {
|
|
@@ -16335,6 +17037,16 @@ function createSessionCheckpointWriter(workspace, onFailure) {
|
|
|
16335
17037
|
return Object.assign(writeCheckpoint, { warnFailure });
|
|
16336
17038
|
}
|
|
16337
17039
|
|
|
17040
|
+
// src/ui/ghost.ts
|
|
17041
|
+
var quietSessions = /* @__PURE__ */ new WeakSet();
|
|
17042
|
+
function isGhostEnabled(session) {
|
|
17043
|
+
return quietSessions.has(session);
|
|
17044
|
+
}
|
|
17045
|
+
function setGhostEnabled(session, enabled) {
|
|
17046
|
+
if (enabled) quietSessions.add(session);
|
|
17047
|
+
else quietSessions.delete(session);
|
|
17048
|
+
}
|
|
17049
|
+
|
|
16338
17050
|
// src/ui/output.ts
|
|
16339
17051
|
import chalk8 from "chalk";
|
|
16340
17052
|
var stopReasonDetail = {
|
|
@@ -16348,15 +17060,15 @@ function stopReasonLine(stopReason) {
|
|
|
16348
17060
|
const detail = stopReasonDetail[reason] ?? "partial answer";
|
|
16349
17061
|
return `Stopped: ${reason} (${detail})`;
|
|
16350
17062
|
}
|
|
16351
|
-
function printSession(session, sessionPath2) {
|
|
17063
|
+
function printSession(session, sessionPath2, options = {}) {
|
|
16352
17064
|
for (const step of session.steps) {
|
|
16353
17065
|
if (step.parsed?.message) {
|
|
16354
17066
|
console.log(chalk8.gray(`step ${step.index + 1}:`), step.parsed.message);
|
|
16355
17067
|
}
|
|
16356
17068
|
for (const toolResult of step.toolResults) {
|
|
16357
17069
|
const status = toolResult.result.ok ? chalk8.green("ok") : chalk8.red("failed");
|
|
16358
|
-
console.log(chalk8.
|
|
16359
|
-
console.log(chalk8.gray(indent2(toolResult.result.content)));
|
|
17070
|
+
console.log(chalk8.white(toolResult.call.tool), status);
|
|
17071
|
+
console.log(chalk8.gray(indent2(formatToolOutput(toolResult.result.content, { verbose: options.verboseTools, failed: !toolResult.result.ok, hint: "Use --verbose-tools or inspect the saved session for full output." }))));
|
|
16360
17072
|
}
|
|
16361
17073
|
}
|
|
16362
17074
|
if (session.finalMessage) {
|
|
@@ -16375,279 +17087,92 @@ function printTools(tools) {
|
|
|
16375
17087
|
for (const tool of tools) {
|
|
16376
17088
|
const permissionText = tool.permissions.length ? chalk8.yellow(` [${tool.permissions.join(", ")}]`) : "";
|
|
16377
17089
|
const optionalText = tool.optionalPermissions.length ? chalk8.gray(` [optional: ${tool.optionalPermissions.join(", ")}]`) : "";
|
|
16378
|
-
console.log(`${chalk8.
|
|
17090
|
+
console.log(`${chalk8.white(tool.name)}${permissionText}${optionalText}`);
|
|
16379
17091
|
console.log(chalk8.gray(indent2(tool.description)));
|
|
16380
17092
|
}
|
|
16381
17093
|
}
|
|
16382
|
-
function printToolResult(result) {
|
|
17094
|
+
function printToolResult(result, options = {}) {
|
|
16383
17095
|
const status = result.ok ? chalk8.green("ok") : chalk8.red("failed");
|
|
16384
|
-
console.log(`${chalk8.
|
|
16385
|
-
console.log(chalk8.gray(indent2(result.content)));
|
|
17096
|
+
console.log(`${chalk8.white(result.tool)} ${status}`);
|
|
17097
|
+
console.log(chalk8.gray(indent2(formatToolOutput(result.content, { verbose: options.verboseTools, failed: !result.ok, hint: "Use --verbose-tools or --result-json for full output." }))));
|
|
16386
17098
|
}
|
|
16387
17099
|
function indent2(value) {
|
|
16388
17100
|
return value.split("\n").map((line) => ` ${line}`).join("\n");
|
|
16389
17101
|
}
|
|
17102
|
+
function formatToolOutput(content, options = {}) {
|
|
17103
|
+
if (options.verbose) return content;
|
|
17104
|
+
const lines = content.split(/\r?\n/);
|
|
17105
|
+
const shown = lines.length <= 6 ? lines : options.failed ? [...lines.slice(0, 3), ...lines.slice(-3)] : lines.slice(0, 6);
|
|
17106
|
+
const clipped = shown.some((line) => line.length > 200);
|
|
17107
|
+
const preview = shown.map((line) => line.length > 200 ? `${line.slice(0, 199)}\u2026` : line).join("\n");
|
|
17108
|
+
const hidden = lines.length - shown.length;
|
|
17109
|
+
if (!hidden && !clipped) return preview;
|
|
17110
|
+
return `${preview}
|
|
17111
|
+
\u2026 ${hidden ? `${hidden} more lines` : "Long lines shortened"}. ${options.hint ?? "Full output remains available in the saved session."}`;
|
|
17112
|
+
}
|
|
16390
17113
|
|
|
16391
|
-
// src/
|
|
16392
|
-
|
|
16393
|
-
|
|
16394
|
-
|
|
16395
|
-
|
|
16396
|
-
|
|
16397
|
-
|
|
16398
|
-
"
|
|
16399
|
-
|
|
16400
|
-
|
|
16401
|
-
|
|
16402
|
-
|
|
16403
|
-
|
|
16404
|
-
|
|
16405
|
-
|
|
16406
|
-
"test",
|
|
16407
|
-
"init",
|
|
16408
|
-
"new",
|
|
16409
|
-
"allow",
|
|
16410
|
-
"clear",
|
|
16411
|
-
"exit",
|
|
16412
|
-
"help",
|
|
16413
|
-
"memory",
|
|
16414
|
-
"mode",
|
|
16415
|
-
"model",
|
|
16416
|
-
"permissions",
|
|
16417
|
-
"quit",
|
|
16418
|
-
"skills",
|
|
16419
|
-
"tools",
|
|
16420
|
-
"whoami"
|
|
16421
|
-
];
|
|
16422
|
-
var COMMAND_NAME_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
16423
|
-
var MAX_COMMAND_LENGTH = 64;
|
|
16424
|
-
function parseSlashInput(input) {
|
|
16425
|
-
const trimmed = input.trim();
|
|
16426
|
-
if (!trimmed.startsWith("/")) return null;
|
|
16427
|
-
const rest = trimmed.slice(1);
|
|
16428
|
-
const match = rest.match(/^(\S+)(?:\s+([\s\S]*))?$/);
|
|
16429
|
-
if (!match) return null;
|
|
16430
|
-
return { command: match[1], args: (match[2] ?? "").trim() };
|
|
17114
|
+
// src/ui/progress.ts
|
|
17115
|
+
import { stdout as stdout3 } from "process";
|
|
17116
|
+
import chalk9 from "chalk";
|
|
17117
|
+
var PENDING_GLYPH = "\u25CB";
|
|
17118
|
+
var DONE_GLYPH = "\u25CF";
|
|
17119
|
+
var REPAINT_MS = 100;
|
|
17120
|
+
function formatTokens(tokens) {
|
|
17121
|
+
if (!Number.isFinite(tokens) || tokens <= 0) return "0";
|
|
17122
|
+
if (tokens < 1e3) return String(Math.floor(tokens));
|
|
17123
|
+
if (tokens < 1e6) {
|
|
17124
|
+
const tenths2 = Math.floor(tokens / 100);
|
|
17125
|
+
return `${Math.floor(tenths2 / 10)}.${tenths2 % 10}k`;
|
|
17126
|
+
}
|
|
17127
|
+
const tenths = Math.floor(tokens / 1e5);
|
|
17128
|
+
return `${Math.floor(tenths / 10)}.${tenths % 10}M`;
|
|
16431
17129
|
}
|
|
16432
|
-
function
|
|
16433
|
-
|
|
17130
|
+
function formatElapsed(ms) {
|
|
17131
|
+
if (!Number.isFinite(ms) || ms <= 0) return "0s";
|
|
17132
|
+
const totalSeconds = Math.floor(ms / 1e3);
|
|
17133
|
+
const seconds = totalSeconds % 60;
|
|
17134
|
+
const totalMinutes = Math.floor(totalSeconds / 60);
|
|
17135
|
+
const minutes = totalMinutes % 60;
|
|
17136
|
+
const hours = Math.floor(totalMinutes / 60);
|
|
17137
|
+
if (totalMinutes === 0) return `${seconds}s`;
|
|
17138
|
+
if (hours === 0) return `${minutes}m ${seconds}s`;
|
|
17139
|
+
return `${hours}h ${String(minutes).padStart(2, "0")}m`;
|
|
16434
17140
|
}
|
|
16435
|
-
function
|
|
16436
|
-
const
|
|
16437
|
-
|
|
16438
|
-
|
|
17141
|
+
function formatProgressLine(snapshot) {
|
|
17142
|
+
const finished = snapshot.done >= snapshot.total;
|
|
17143
|
+
const glyph = finished ? chalk9.green(DONE_GLYPH) : chalk9.white(PENDING_GLYPH);
|
|
17144
|
+
const noun = snapshot.total === 1 ? "agent" : "agents";
|
|
17145
|
+
const stats = [`${snapshot.done}/${snapshot.total} ${noun} done`, formatElapsed(snapshot.elapsedMs)];
|
|
17146
|
+
if (snapshot.tokens !== void 0) stats.push(`\u2193 ${formatTokens(snapshot.tokens)} tokens`);
|
|
17147
|
+
const rendered = stats.map((stat11) => chalk9.dim(stat11)).join(chalk9.dim(" \xB7 "));
|
|
17148
|
+
return ` ${glyph} ${chalk9.white(snapshot.label)} ${snapshot.description} ${rendered}`;
|
|
17149
|
+
}
|
|
17150
|
+
var ProgressLine = class {
|
|
17151
|
+
constructor(label, description, total) {
|
|
17152
|
+
this.label = label;
|
|
17153
|
+
this.description = description;
|
|
17154
|
+
this.total = total;
|
|
16439
17155
|
}
|
|
16440
|
-
|
|
16441
|
-
|
|
16442
|
-
|
|
16443
|
-
|
|
17156
|
+
label;
|
|
17157
|
+
description;
|
|
17158
|
+
total;
|
|
17159
|
+
timer;
|
|
17160
|
+
startedAt;
|
|
17161
|
+
doneCount = 0;
|
|
17162
|
+
tokens;
|
|
17163
|
+
/** True only while this instance owns a painted line it still has to release. */
|
|
17164
|
+
live = false;
|
|
17165
|
+
start() {
|
|
17166
|
+
if (this.startedAt !== void 0) return;
|
|
17167
|
+
this.startedAt = Date.now();
|
|
17168
|
+
if (!stdout3.isTTY) return;
|
|
17169
|
+
this.live = true;
|
|
17170
|
+
this.render();
|
|
17171
|
+
this.timer = setInterval(() => this.render(), REPAINT_MS);
|
|
16444
17172
|
}
|
|
16445
|
-
|
|
16446
|
-
|
|
16447
|
-
|
|
16448
|
-
);
|
|
16449
|
-
}
|
|
16450
|
-
return candidate;
|
|
16451
|
-
}
|
|
16452
|
-
async function collectEntries(workspace) {
|
|
16453
|
-
const skills = await discoverSkills(workspace);
|
|
16454
|
-
const claimed = /* @__PURE__ */ new Map();
|
|
16455
|
-
const conflicts = [];
|
|
16456
|
-
for (const skill of skills) {
|
|
16457
|
-
let declaration;
|
|
16458
|
-
try {
|
|
16459
|
-
declaration = skill.command ?? null;
|
|
16460
|
-
} catch (error) {
|
|
16461
|
-
conflicts.push({
|
|
16462
|
-
command: "",
|
|
16463
|
-
skillName: skill.name,
|
|
16464
|
-
scope: skill.scope,
|
|
16465
|
-
path: skill.path,
|
|
16466
|
-
reason: "invalid",
|
|
16467
|
-
message: `Skill "${skill.name}" (${skill.scope}) was skipped: ${error instanceof Error ? error.message : String(error)}`
|
|
16468
|
-
});
|
|
16469
|
-
continue;
|
|
16470
|
-
}
|
|
16471
|
-
if (!declaration) continue;
|
|
16472
|
-
let command;
|
|
16473
|
-
try {
|
|
16474
|
-
command = normalizeCommandName(declaration.raw);
|
|
16475
|
-
} catch (error) {
|
|
16476
|
-
conflicts.push({
|
|
16477
|
-
command: declaration.raw,
|
|
16478
|
-
skillName: skill.name,
|
|
16479
|
-
scope: skill.scope,
|
|
16480
|
-
path: skill.path,
|
|
16481
|
-
reason: "invalid",
|
|
16482
|
-
message: `Skill "${skill.name}" (${skill.scope}) declares a malformed command: ${error instanceof Error ? error.message : String(error)}`
|
|
16483
|
-
});
|
|
16484
|
-
continue;
|
|
16485
|
-
}
|
|
16486
|
-
if (isBuiltinSlashCommand(command)) {
|
|
16487
|
-
conflicts.push({
|
|
16488
|
-
command,
|
|
16489
|
-
skillName: skill.name,
|
|
16490
|
-
scope: skill.scope,
|
|
16491
|
-
path: skill.path,
|
|
16492
|
-
reason: "builtin",
|
|
16493
|
-
message: `Skill "${skill.name}" (${skill.scope}) declares /${command}, which is a built-in command. Built-ins always win - rename the command: key in ${skill.path}.`
|
|
16494
|
-
});
|
|
16495
|
-
continue;
|
|
16496
|
-
}
|
|
16497
|
-
const entry = {
|
|
16498
|
-
skill,
|
|
16499
|
-
command: {
|
|
16500
|
-
command,
|
|
16501
|
-
skillName: skill.name,
|
|
16502
|
-
description: skill.description,
|
|
16503
|
-
argumentHint: declaration.argumentHint,
|
|
16504
|
-
scope: skill.scope,
|
|
16505
|
-
path: skill.path
|
|
16506
|
-
}
|
|
16507
|
-
};
|
|
16508
|
-
const existing = claimed.get(command);
|
|
16509
|
-
if (!existing) {
|
|
16510
|
-
claimed.set(command, entry);
|
|
16511
|
-
continue;
|
|
16512
|
-
}
|
|
16513
|
-
const winner = existing.skill.scope === "global" && skill.scope === "project" ? entry : existing;
|
|
16514
|
-
const loser = winner === entry ? existing : entry;
|
|
16515
|
-
claimed.set(command, winner);
|
|
16516
|
-
conflicts.push({
|
|
16517
|
-
command,
|
|
16518
|
-
skillName: loser.skill.name,
|
|
16519
|
-
scope: loser.skill.scope,
|
|
16520
|
-
path: loser.skill.path,
|
|
16521
|
-
reason: "duplicate",
|
|
16522
|
-
message: `/${command} is declared by both "${winner.skill.name}" (${winner.skill.scope}) and "${loser.skill.name}" (${loser.skill.scope}). "${winner.skill.name}" wins; rename the command: key in ${loser.skill.path}.`
|
|
16523
|
-
});
|
|
16524
|
-
}
|
|
16525
|
-
const entries = [...claimed.values()].sort((a, b) => a.command.command.localeCompare(b.command.command));
|
|
16526
|
-
return { entries, conflicts };
|
|
16527
|
-
}
|
|
16528
|
-
async function listSkillCommands(workspace) {
|
|
16529
|
-
const { entries, conflicts } = await collectEntries(workspace);
|
|
16530
|
-
return { commands: entries.map((entry) => entry.command), conflicts };
|
|
16531
|
-
}
|
|
16532
|
-
function buildSkillCommandPrompt(skill, command, args) {
|
|
16533
|
-
const argumentLine = command.argumentHint ? `Arguments (${command.argumentHint}): ${args || "(none provided)"}` : `Arguments: ${args || "(none provided)"}`;
|
|
16534
|
-
const header = [
|
|
16535
|
-
`The user ran the /${command.command} slash command, which runs the "${skill.name}" skill (${skill.scope}).`,
|
|
16536
|
-
skill.description ? `Description: ${skill.description}` : void 0,
|
|
16537
|
-
argumentLine,
|
|
16538
|
-
skill.permissions.length ? `Permissions needed: ${skill.permissions.join(", ")}` : void 0,
|
|
16539
|
-
"Follow the instructions below now, applying the arguments where they are relevant."
|
|
16540
|
-
].filter(Boolean).join("\n");
|
|
16541
|
-
return `${header}
|
|
16542
|
-
|
|
16543
|
-
--- instructions ---
|
|
16544
|
-
${skill.body}`;
|
|
16545
|
-
}
|
|
16546
|
-
async function resolveSkillCommand(workspace, input) {
|
|
16547
|
-
const parsed = parseSlashInput(input);
|
|
16548
|
-
if (!parsed) return { status: "not-a-command" };
|
|
16549
|
-
const command = parsed.command.toLowerCase();
|
|
16550
|
-
if (isBuiltinSlashCommand(command)) {
|
|
16551
|
-
return { status: "builtin", command };
|
|
16552
|
-
}
|
|
16553
|
-
const { entries, conflicts } = await collectEntries(workspace);
|
|
16554
|
-
const match = entries.find((entry) => entry.command.command === command);
|
|
16555
|
-
if (!match) {
|
|
16556
|
-
const related = conflicts.filter((conflict) => conflict.command === command);
|
|
16557
|
-
if (related.length) {
|
|
16558
|
-
return {
|
|
16559
|
-
status: "conflict",
|
|
16560
|
-
command,
|
|
16561
|
-
message: related.map((conflict) => conflict.message).join("\n"),
|
|
16562
|
-
conflicts: related
|
|
16563
|
-
};
|
|
16564
|
-
}
|
|
16565
|
-
return {
|
|
16566
|
-
status: "unknown",
|
|
16567
|
-
command,
|
|
16568
|
-
available: entries.map((entry) => entry.command.command)
|
|
16569
|
-
};
|
|
16570
|
-
}
|
|
16571
|
-
return {
|
|
16572
|
-
status: "ok",
|
|
16573
|
-
command: match.command,
|
|
16574
|
-
skill: match.skill,
|
|
16575
|
-
args: parsed.args,
|
|
16576
|
-
prompt: buildSkillCommandPrompt(match.skill, match.command, parsed.args)
|
|
16577
|
-
};
|
|
16578
|
-
}
|
|
16579
|
-
function formatSkillCommandHelp(index) {
|
|
16580
|
-
if (!index.commands.length) return "";
|
|
16581
|
-
const lines = index.commands.map((command) => {
|
|
16582
|
-
const invocation = command.argumentHint ? `/${command.command} ${command.argumentHint}` : `/${command.command}`;
|
|
16583
|
-
const description = command.description || `Run the ${command.skillName} skill`;
|
|
16584
|
-
return ` ${invocation.padEnd(22)}${description} (skill: ${command.skillName})`;
|
|
16585
|
-
});
|
|
16586
|
-
return ["Skill commands:", ...lines].join("\n");
|
|
16587
|
-
}
|
|
16588
|
-
|
|
16589
|
-
// src/ui/progress.ts
|
|
16590
|
-
import { stdout as stdout3 } from "process";
|
|
16591
|
-
import chalk9 from "chalk";
|
|
16592
|
-
var PENDING_GLYPH = "\u25CB";
|
|
16593
|
-
var DONE_GLYPH = "\u25CF";
|
|
16594
|
-
var REPAINT_MS = 100;
|
|
16595
|
-
function formatTokens(tokens) {
|
|
16596
|
-
if (!Number.isFinite(tokens) || tokens <= 0) return "0";
|
|
16597
|
-
if (tokens < 1e3) return String(Math.floor(tokens));
|
|
16598
|
-
if (tokens < 1e6) {
|
|
16599
|
-
const tenths2 = Math.floor(tokens / 100);
|
|
16600
|
-
return `${Math.floor(tenths2 / 10)}.${tenths2 % 10}k`;
|
|
16601
|
-
}
|
|
16602
|
-
const tenths = Math.floor(tokens / 1e5);
|
|
16603
|
-
return `${Math.floor(tenths / 10)}.${tenths % 10}M`;
|
|
16604
|
-
}
|
|
16605
|
-
function formatElapsed(ms) {
|
|
16606
|
-
if (!Number.isFinite(ms) || ms <= 0) return "0s";
|
|
16607
|
-
const totalSeconds = Math.floor(ms / 1e3);
|
|
16608
|
-
const seconds = totalSeconds % 60;
|
|
16609
|
-
const totalMinutes = Math.floor(totalSeconds / 60);
|
|
16610
|
-
const minutes = totalMinutes % 60;
|
|
16611
|
-
const hours = Math.floor(totalMinutes / 60);
|
|
16612
|
-
if (totalMinutes === 0) return `${seconds}s`;
|
|
16613
|
-
if (hours === 0) return `${minutes}m ${seconds}s`;
|
|
16614
|
-
return `${hours}h ${String(minutes).padStart(2, "0")}m`;
|
|
16615
|
-
}
|
|
16616
|
-
function formatProgressLine(snapshot) {
|
|
16617
|
-
const finished = snapshot.done >= snapshot.total;
|
|
16618
|
-
const glyph = finished ? chalk9.green(DONE_GLYPH) : chalk9.cyan(PENDING_GLYPH);
|
|
16619
|
-
const noun = snapshot.total === 1 ? "agent" : "agents";
|
|
16620
|
-
const stats = [`${snapshot.done}/${snapshot.total} ${noun} done`, formatElapsed(snapshot.elapsedMs)];
|
|
16621
|
-
if (snapshot.tokens !== void 0) stats.push(`\u2193 ${formatTokens(snapshot.tokens)} tokens`);
|
|
16622
|
-
const rendered = stats.map((stat11) => chalk9.dim(stat11)).join(chalk9.dim(" \xB7 "));
|
|
16623
|
-
return ` ${glyph} ${chalk9.cyan(snapshot.label)} ${snapshot.description} ${rendered}`;
|
|
16624
|
-
}
|
|
16625
|
-
var ProgressLine = class {
|
|
16626
|
-
constructor(label, description, total) {
|
|
16627
|
-
this.label = label;
|
|
16628
|
-
this.description = description;
|
|
16629
|
-
this.total = total;
|
|
16630
|
-
}
|
|
16631
|
-
label;
|
|
16632
|
-
description;
|
|
16633
|
-
total;
|
|
16634
|
-
timer;
|
|
16635
|
-
startedAt;
|
|
16636
|
-
doneCount = 0;
|
|
16637
|
-
tokens;
|
|
16638
|
-
/** True only while this instance owns a painted line it still has to release. */
|
|
16639
|
-
live = false;
|
|
16640
|
-
start() {
|
|
16641
|
-
if (this.startedAt !== void 0) return;
|
|
16642
|
-
this.startedAt = Date.now();
|
|
16643
|
-
if (!stdout3.isTTY) return;
|
|
16644
|
-
this.live = true;
|
|
16645
|
-
this.render();
|
|
16646
|
-
this.timer = setInterval(() => this.render(), REPAINT_MS);
|
|
16647
|
-
}
|
|
16648
|
-
/** One agent finished. */
|
|
16649
|
-
complete() {
|
|
16650
|
-
if (this.doneCount < this.total) this.doneCount += 1;
|
|
17173
|
+
/** One agent finished. */
|
|
17174
|
+
complete() {
|
|
17175
|
+
if (this.doneCount < this.total) this.doneCount += 1;
|
|
16651
17176
|
}
|
|
16652
17177
|
addTokens(tokens) {
|
|
16653
17178
|
if (!Number.isFinite(tokens)) return;
|
|
@@ -16756,7 +17281,10 @@ var Spinner = class {
|
|
|
16756
17281
|
label;
|
|
16757
17282
|
timer;
|
|
16758
17283
|
index = 0;
|
|
16759
|
-
|
|
17284
|
+
holdWhile;
|
|
17285
|
+
held = false;
|
|
17286
|
+
start(holdWhile) {
|
|
17287
|
+
this.holdWhile = holdWhile;
|
|
16760
17288
|
if (!stdout4.isTTY || this.timer) return;
|
|
16761
17289
|
this.render();
|
|
16762
17290
|
this.timer = setInterval(() => this.render(), 90);
|
|
@@ -16775,6 +17303,15 @@ var Spinner = class {
|
|
|
16775
17303
|
}
|
|
16776
17304
|
}
|
|
16777
17305
|
render() {
|
|
17306
|
+
if (this.holdWhile?.()) {
|
|
17307
|
+
if (!this.held) {
|
|
17308
|
+
stdout4.clearLine(0);
|
|
17309
|
+
stdout4.cursorTo(0);
|
|
17310
|
+
this.held = true;
|
|
17311
|
+
}
|
|
17312
|
+
return;
|
|
17313
|
+
}
|
|
17314
|
+
this.held = false;
|
|
16778
17315
|
this.index = (this.index + 1) % FRAMES.length;
|
|
16779
17316
|
stdout4.clearLine(0);
|
|
16780
17317
|
stdout4.cursorTo(0);
|
|
@@ -16853,6 +17390,682 @@ function humanize(value) {
|
|
|
16853
17390
|
return words ? `${words[0].toUpperCase()}${words.slice(1)}` : "Tool";
|
|
16854
17391
|
}
|
|
16855
17392
|
|
|
17393
|
+
// src/ui/interactive-choice.ts
|
|
17394
|
+
var MOUSE_ON = "\x1B[?1006h\x1B[?1000h";
|
|
17395
|
+
var MOUSE_OFF = "\x1B[?1000l\x1B[?1006l";
|
|
17396
|
+
function displayLabel(value, width2) {
|
|
17397
|
+
const clean = value.replace(/\x1b(?:\[[0-?]*[ -/]*[@-~]|\][^\x07]*(?:\x07|\x1b\\)|.)/g, "").replace(/[\x00-\x1f\x7f-\x9f]/g, " ");
|
|
17398
|
+
let result = "";
|
|
17399
|
+
let used = 0;
|
|
17400
|
+
for (const character of clean) {
|
|
17401
|
+
const size = /[\p{Mark}\u200d\ufe0e\ufe0f]/u.test(character) ? 0 : character.codePointAt(0) < 128 ? 1 : 2;
|
|
17402
|
+
if (used + size > width2 - 1) return `${result}\u2026`;
|
|
17403
|
+
result += character;
|
|
17404
|
+
used += size;
|
|
17405
|
+
}
|
|
17406
|
+
return result;
|
|
17407
|
+
}
|
|
17408
|
+
function attachInteractiveChoices(readline, options, settings = {}) {
|
|
17409
|
+
const rl = readline;
|
|
17410
|
+
const output = settings.output ?? rl.output;
|
|
17411
|
+
const prompt = settings.prompt ?? " \u203A ";
|
|
17412
|
+
let resolveReady;
|
|
17413
|
+
const ready = new Promise((resolve15) => {
|
|
17414
|
+
resolveReady = resolve15;
|
|
17415
|
+
});
|
|
17416
|
+
const original = rl._ttyWrite;
|
|
17417
|
+
const priorPrompt = rl.getPrompt();
|
|
17418
|
+
const priorLine = rl.line;
|
|
17419
|
+
const priorCursor = rl.cursor;
|
|
17420
|
+
const priorHistory = rl.history?.slice();
|
|
17421
|
+
let injected = false;
|
|
17422
|
+
let selected = 0;
|
|
17423
|
+
let disposed = false;
|
|
17424
|
+
let mouse = false;
|
|
17425
|
+
let optionRow = null;
|
|
17426
|
+
let reportPending = false;
|
|
17427
|
+
let reportTimer;
|
|
17428
|
+
let sequenceTimer;
|
|
17429
|
+
let sequence = "";
|
|
17430
|
+
const maxRow = () => settings.maxRow?.() ?? output?.rows ?? 1e4;
|
|
17431
|
+
const columns = () => Math.max(16, output?.columns ?? 80);
|
|
17432
|
+
const write = (text) => {
|
|
17433
|
+
output?.write(text);
|
|
17434
|
+
};
|
|
17435
|
+
const labels = () => options.map((option, index) => {
|
|
17436
|
+
const prefix = ` ${selected === index ? "\u203A" : " "} ${index + 1}. `;
|
|
17437
|
+
const text = `${prefix}${displayLabel(option, columns() - 9)}`;
|
|
17438
|
+
return selected === index ? `\x1B[7m${text}\x1B[0m` : text;
|
|
17439
|
+
});
|
|
17440
|
+
const restoreLine = () => {
|
|
17441
|
+
if (!original || !rl.terminal || rl.closed) return;
|
|
17442
|
+
if (rl.line) {
|
|
17443
|
+
original.call(rl, void 0, { name: "end" });
|
|
17444
|
+
original.call(rl, void 0, { name: "u", ctrl: true });
|
|
17445
|
+
}
|
|
17446
|
+
if (priorLine) {
|
|
17447
|
+
original.call(rl, priorLine, {});
|
|
17448
|
+
for (const _character of Array.from(priorLine.slice(priorCursor))) original.call(rl, void 0, { name: "left" });
|
|
17449
|
+
}
|
|
17450
|
+
};
|
|
17451
|
+
const cleanup = () => {
|
|
17452
|
+
if (disposed) return;
|
|
17453
|
+
disposed = true;
|
|
17454
|
+
clearTimeout(reportTimer);
|
|
17455
|
+
clearTimeout(sequenceTimer);
|
|
17456
|
+
if (rl._ttyWrite === intercept) rl._ttyWrite = original;
|
|
17457
|
+
rl.off("line", onLine);
|
|
17458
|
+
rl.off("close", cleanup);
|
|
17459
|
+
output?.off("resize", onResize);
|
|
17460
|
+
output?.off("error", cleanup);
|
|
17461
|
+
settings.signal?.removeEventListener("abort", cleanup);
|
|
17462
|
+
try {
|
|
17463
|
+
if (mouse) write(MOUSE_OFF);
|
|
17464
|
+
} catch {
|
|
17465
|
+
}
|
|
17466
|
+
mouse = false;
|
|
17467
|
+
rl.setPrompt(priorPrompt);
|
|
17468
|
+
if (injected && priorHistory && rl.history) rl.history.splice(0, rl.history.length, ...priorHistory);
|
|
17469
|
+
try {
|
|
17470
|
+
restoreLine();
|
|
17471
|
+
} catch {
|
|
17472
|
+
}
|
|
17473
|
+
resolveReady(false);
|
|
17474
|
+
};
|
|
17475
|
+
function onLine() {
|
|
17476
|
+
cleanup();
|
|
17477
|
+
}
|
|
17478
|
+
function redraw() {
|
|
17479
|
+
if (disposed || rl.line || options.length + 2 > maxRow()) return;
|
|
17480
|
+
write("\x1B7");
|
|
17481
|
+
if (optionRow !== null) write(`\x1B[${optionRow};1H`);
|
|
17482
|
+
else write(`\x1B[${options.length + 1}A\r`);
|
|
17483
|
+
for (const label of labels()) write(`\x1B[2K${label}
|
|
17484
|
+
\r`);
|
|
17485
|
+
write("\x1B8");
|
|
17486
|
+
}
|
|
17487
|
+
function choose(index) {
|
|
17488
|
+
if (disposed || !original || index < 0 || index >= options.length) return;
|
|
17489
|
+
selected = index;
|
|
17490
|
+
injected = true;
|
|
17491
|
+
if (rl.line) {
|
|
17492
|
+
original.call(rl, void 0, { name: "end" });
|
|
17493
|
+
original.call(rl, void 0, { name: "u", ctrl: true });
|
|
17494
|
+
}
|
|
17495
|
+
original.call(rl, String(index + 1), {});
|
|
17496
|
+
original.call(rl, "\r", { name: "return", sequence: "\r" });
|
|
17497
|
+
}
|
|
17498
|
+
function requestPosition() {
|
|
17499
|
+
if (disposed) return;
|
|
17500
|
+
optionRow = null;
|
|
17501
|
+
reportPending = true;
|
|
17502
|
+
clearTimeout(reportTimer);
|
|
17503
|
+
write("\x1B[6n");
|
|
17504
|
+
reportTimer = setTimeout(() => {
|
|
17505
|
+
reportPending = false;
|
|
17506
|
+
resolveReady(false);
|
|
17507
|
+
}, Math.max(1, settings.cursorReportTimeoutMs ?? 250));
|
|
17508
|
+
}
|
|
17509
|
+
function onResize() {
|
|
17510
|
+
if (disposed) return;
|
|
17511
|
+
if (mouse) write(MOUSE_OFF);
|
|
17512
|
+
mouse = false;
|
|
17513
|
+
optionRow = null;
|
|
17514
|
+
requestPosition();
|
|
17515
|
+
}
|
|
17516
|
+
function consumeControl(value) {
|
|
17517
|
+
const position = /^\x1b\[(\d+);(\d+)R$/.exec(value);
|
|
17518
|
+
if (position) {
|
|
17519
|
+
const row = Number(position[1]);
|
|
17520
|
+
const column = Number(position[2]);
|
|
17521
|
+
if (reportPending && row > options.length + 1 && row <= maxRow() && column >= 1 && column <= columns()) {
|
|
17522
|
+
optionRow = row - options.length - 1;
|
|
17523
|
+
reportPending = false;
|
|
17524
|
+
clearTimeout(reportTimer);
|
|
17525
|
+
write(MOUSE_ON);
|
|
17526
|
+
mouse = true;
|
|
17527
|
+
resolveReady(true);
|
|
17528
|
+
}
|
|
17529
|
+
return true;
|
|
17530
|
+
}
|
|
17531
|
+
const click = /^\x1b\[<(\d+);(\d+);(\d+)([Mm])$/.exec(value);
|
|
17532
|
+
if (click) {
|
|
17533
|
+
const button = Number(click[1]);
|
|
17534
|
+
const x = Number(click[2]);
|
|
17535
|
+
const y = Number(click[3]);
|
|
17536
|
+
if (mouse && optionRow !== null && click[4] === "M" && button < 32 && (button & 3) === 0 && x >= 1 && x <= columns() && y <= maxRow()) choose(y - optionRow);
|
|
17537
|
+
return true;
|
|
17538
|
+
}
|
|
17539
|
+
return false;
|
|
17540
|
+
}
|
|
17541
|
+
function intercept(text, key) {
|
|
17542
|
+
if (disposed) {
|
|
17543
|
+
original?.call(rl, text, key);
|
|
17544
|
+
return;
|
|
17545
|
+
}
|
|
17546
|
+
const value = key?.sequence ?? text ?? "";
|
|
17547
|
+
if (sequence || value.startsWith("\x1B[<") || /^\x1b\[\d/.test(value)) {
|
|
17548
|
+
sequence += value;
|
|
17549
|
+
clearTimeout(sequenceTimer);
|
|
17550
|
+
if (consumeControl(sequence)) {
|
|
17551
|
+
sequence = "";
|
|
17552
|
+
return;
|
|
17553
|
+
}
|
|
17554
|
+
if (/^\x1b\[(?:<[0-9;]*|[0-9;]*)$/.test(sequence) && sequence.length < 64) {
|
|
17555
|
+
sequenceTimer = setTimeout(() => {
|
|
17556
|
+
sequence = "";
|
|
17557
|
+
}, 250);
|
|
17558
|
+
return;
|
|
17559
|
+
}
|
|
17560
|
+
const unrecognized = sequence;
|
|
17561
|
+
sequence = "";
|
|
17562
|
+
original?.call(rl, unrecognized, key);
|
|
17563
|
+
return;
|
|
17564
|
+
}
|
|
17565
|
+
if (!rl.line && (key?.name === "up" || key?.name === "down")) {
|
|
17566
|
+
selected = (selected + (key.name === "down" ? 1 : options.length - 1)) % options.length;
|
|
17567
|
+
redraw();
|
|
17568
|
+
return;
|
|
17569
|
+
}
|
|
17570
|
+
if (!rl.line && (key?.name === "return" || key?.name === "enter")) {
|
|
17571
|
+
choose(selected);
|
|
17572
|
+
return;
|
|
17573
|
+
}
|
|
17574
|
+
if (key?.ctrl && key.name === "c") cleanup();
|
|
17575
|
+
if (key?.ctrl && key.name === "d" && !rl.line) {
|
|
17576
|
+
original?.call(rl, text, key);
|
|
17577
|
+
cleanup();
|
|
17578
|
+
return;
|
|
17579
|
+
}
|
|
17580
|
+
original?.call(rl, text, key);
|
|
17581
|
+
}
|
|
17582
|
+
if (settings.signal?.aborted) {
|
|
17583
|
+
disposed = true;
|
|
17584
|
+
resolveReady(false);
|
|
17585
|
+
return { cleanup, ready };
|
|
17586
|
+
}
|
|
17587
|
+
if (!output || !rl.terminal || !output.isTTY || process.env.TERM === "dumb" || !original || !options.length) {
|
|
17588
|
+
options.forEach((option, index) => write(` ${index + 1}. ${displayLabel(option, columns() - 6)}
|
|
17589
|
+
`));
|
|
17590
|
+
write(prompt);
|
|
17591
|
+
resolveReady(false);
|
|
17592
|
+
disposed = true;
|
|
17593
|
+
return { cleanup, ready };
|
|
17594
|
+
}
|
|
17595
|
+
try {
|
|
17596
|
+
if (priorLine) {
|
|
17597
|
+
original.call(rl, void 0, { name: "end" });
|
|
17598
|
+
original.call(rl, void 0, { name: "u", ctrl: true });
|
|
17599
|
+
}
|
|
17600
|
+
rl.setPrompt(prompt);
|
|
17601
|
+
rl._ttyWrite = intercept;
|
|
17602
|
+
rl.once("line", onLine);
|
|
17603
|
+
rl.once("close", cleanup);
|
|
17604
|
+
output.on("resize", onResize);
|
|
17605
|
+
output.on("error", cleanup);
|
|
17606
|
+
settings.signal?.addEventListener("abort", cleanup, { once: true });
|
|
17607
|
+
for (const label of labels()) write(`${label}\r
|
|
17608
|
+
`);
|
|
17609
|
+
write(" \u2191/\u2193 and Enter, a number, or your own answer.\r\n");
|
|
17610
|
+
rl.prompt();
|
|
17611
|
+
requestPosition();
|
|
17612
|
+
} catch (error) {
|
|
17613
|
+
cleanup();
|
|
17614
|
+
throw error;
|
|
17615
|
+
}
|
|
17616
|
+
return { cleanup, ready };
|
|
17617
|
+
}
|
|
17618
|
+
|
|
17619
|
+
// src/ui/slash-completion.ts
|
|
17620
|
+
function attachSlashCompletion(readline, loadEntries, render, initialEntries = []) {
|
|
17621
|
+
const rl = readline;
|
|
17622
|
+
const original = rl._ttyWrite;
|
|
17623
|
+
let entries = [...initialEntries];
|
|
17624
|
+
let matches = [];
|
|
17625
|
+
let selected = 0;
|
|
17626
|
+
let disposed = false;
|
|
17627
|
+
let suppressed = false;
|
|
17628
|
+
let opened = false;
|
|
17629
|
+
let epoch = 0;
|
|
17630
|
+
let handledEscape = false;
|
|
17631
|
+
const clear = () => {
|
|
17632
|
+
matches = [];
|
|
17633
|
+
selected = 0;
|
|
17634
|
+
render([], 0);
|
|
17635
|
+
};
|
|
17636
|
+
const refresh = () => {
|
|
17637
|
+
if (disposed || suppressed) return;
|
|
17638
|
+
const line = rl.line;
|
|
17639
|
+
const skillPrefix = /^\/skill\s+([a-z0-9-]*)$/i.exec(line);
|
|
17640
|
+
if (!/^\/[a-z0-9-]*$/i.test(line) && !skillPrefix) {
|
|
17641
|
+
opened = false;
|
|
17642
|
+
epoch += 1;
|
|
17643
|
+
clear();
|
|
17644
|
+
return;
|
|
17645
|
+
}
|
|
17646
|
+
if (!opened) {
|
|
17647
|
+
opened = true;
|
|
17648
|
+
const request = ++epoch;
|
|
17649
|
+
void loadEntries().then((items) => {
|
|
17650
|
+
if (disposed || request !== epoch || !opened) return;
|
|
17651
|
+
entries = items;
|
|
17652
|
+
refresh();
|
|
17653
|
+
}).catch(() => {
|
|
17654
|
+
if (!disposed && request === epoch) clear();
|
|
17655
|
+
});
|
|
17656
|
+
}
|
|
17657
|
+
const query = (skillPrefix?.[1] ?? line.slice(1)).toLowerCase();
|
|
17658
|
+
matches = entries.filter((entry) => (!skillPrefix || entry.skill) && (entry.text.toLowerCase().includes(query) || entry.label.toLowerCase().includes(query)));
|
|
17659
|
+
selected = Math.min(selected, Math.max(0, matches.length - 1));
|
|
17660
|
+
render(matches.map((entry) => `${entry.label}${entry.description ? ` ${entry.description}` : ""}`.replace(/[\x00-\x1f\x7f]/g, " ")), selected);
|
|
17661
|
+
};
|
|
17662
|
+
const wrapper = function(text, key) {
|
|
17663
|
+
if (!suppressed && matches.length) {
|
|
17664
|
+
if (key?.name === "escape") {
|
|
17665
|
+
handledEscape = true;
|
|
17666
|
+
setTimeout(() => {
|
|
17667
|
+
handledEscape = false;
|
|
17668
|
+
}, 0);
|
|
17669
|
+
opened = false;
|
|
17670
|
+
epoch += 1;
|
|
17671
|
+
clear();
|
|
17672
|
+
return;
|
|
17673
|
+
}
|
|
17674
|
+
if (key?.name === "up" || key?.name === "down") {
|
|
17675
|
+
selected = (selected + (key.name === "up" ? -1 : 1) + matches.length) % matches.length;
|
|
17676
|
+
render(matches.map((entry) => `${entry.label}${entry.description ? ` ${entry.description}` : ""}`.replace(/[\x00-\x1f\x7f]/g, " ")), selected);
|
|
17677
|
+
return;
|
|
17678
|
+
}
|
|
17679
|
+
if (key?.name === "tab" || key?.name === "return" || key?.name === "enter") {
|
|
17680
|
+
const choice = matches[selected];
|
|
17681
|
+
suppressed = true;
|
|
17682
|
+
try {
|
|
17683
|
+
rl.write(null, { ctrl: true, name: "u" });
|
|
17684
|
+
rl.write(`${choice.text} `);
|
|
17685
|
+
} finally {
|
|
17686
|
+
suppressed = false;
|
|
17687
|
+
opened = false;
|
|
17688
|
+
epoch += 1;
|
|
17689
|
+
clear();
|
|
17690
|
+
}
|
|
17691
|
+
return;
|
|
17692
|
+
}
|
|
17693
|
+
}
|
|
17694
|
+
original.call(this, text, key);
|
|
17695
|
+
refresh();
|
|
17696
|
+
};
|
|
17697
|
+
rl._ttyWrite = wrapper;
|
|
17698
|
+
return {
|
|
17699
|
+
handledEscape: () => handledEscape,
|
|
17700
|
+
suspend: () => {
|
|
17701
|
+
suppressed = true;
|
|
17702
|
+
opened = false;
|
|
17703
|
+
epoch += 1;
|
|
17704
|
+
clear();
|
|
17705
|
+
},
|
|
17706
|
+
resume: () => {
|
|
17707
|
+
suppressed = false;
|
|
17708
|
+
refresh();
|
|
17709
|
+
},
|
|
17710
|
+
cleanup: () => {
|
|
17711
|
+
disposed = true;
|
|
17712
|
+
epoch += 1;
|
|
17713
|
+
clear();
|
|
17714
|
+
if (rl._ttyWrite === wrapper) rl._ttyWrite = original;
|
|
17715
|
+
}
|
|
17716
|
+
};
|
|
17717
|
+
}
|
|
17718
|
+
|
|
17719
|
+
// src/ui/terminal-layout.ts
|
|
17720
|
+
import { Writable } from "stream";
|
|
17721
|
+
import { stripVTControlCharacters } from "util";
|
|
17722
|
+
var CSI = "\x1B[";
|
|
17723
|
+
var RESET_REGION = `${CSI}r`;
|
|
17724
|
+
var MOUSE_OFF2 = `${CSI}?1000l${CSI}?1002l${CSI}?1003l${CSI}?1006l`;
|
|
17725
|
+
var BASE_FOOTER_ROWS = 5;
|
|
17726
|
+
var segmenter = new Intl.Segmenter(void 0, { granularity: "grapheme" });
|
|
17727
|
+
var cells = (value) => [...segmenter.segment(value)].map(({ segment }) => {
|
|
17728
|
+
const code = segment.codePointAt(0) ?? 0;
|
|
17729
|
+
const wide = /\p{Extended_Pictographic}/u.test(segment) || code >= 4352 && (code <= 4447 || code === 9001 || code === 9002 || code >= 11904 && code <= 42191 || code >= 44032 && code <= 55203 || code >= 63744 && code <= 64255 || code >= 65040 && code <= 65135 || code >= 65281 && code <= 65376 || code >= 65504 && code <= 65510 || code >= 131072);
|
|
17730
|
+
return { text: segment, width: /^\p{Mark}+$/u.test(segment) ? 0 : wide ? 2 : 1 };
|
|
17731
|
+
});
|
|
17732
|
+
var width = (text) => cells(text).reduce((sum, cell) => sum + cell.width, 0);
|
|
17733
|
+
function plain(value) {
|
|
17734
|
+
return stripVTControlCharacters(value).replace(/[\x00-\x1f\x7f]/g, " ");
|
|
17735
|
+
}
|
|
17736
|
+
function fit(text, columns) {
|
|
17737
|
+
if (columns <= 0) return "";
|
|
17738
|
+
let result = "";
|
|
17739
|
+
let used = 0;
|
|
17740
|
+
for (const cell of cells(plain(text))) {
|
|
17741
|
+
if (used + cell.width > columns) break;
|
|
17742
|
+
result += cell.text;
|
|
17743
|
+
used += cell.width;
|
|
17744
|
+
}
|
|
17745
|
+
return result;
|
|
17746
|
+
}
|
|
17747
|
+
var cup = (row, column = 1) => `${CSI}${row};${column}H`;
|
|
17748
|
+
var count3 = (value) => value !== void 0 && Number.isFinite(value) && value >= 0 ? Math.floor(value).toLocaleString("en-US") : "unknown";
|
|
17749
|
+
var ComposerOutput = class extends Writable {
|
|
17750
|
+
constructor(layout) {
|
|
17751
|
+
super();
|
|
17752
|
+
this.layout = layout;
|
|
17753
|
+
}
|
|
17754
|
+
layout;
|
|
17755
|
+
get isTTY() {
|
|
17756
|
+
return this.layout.enabled;
|
|
17757
|
+
}
|
|
17758
|
+
get columns() {
|
|
17759
|
+
return this.layout.columns;
|
|
17760
|
+
}
|
|
17761
|
+
get rows() {
|
|
17762
|
+
return this.layout.rows;
|
|
17763
|
+
}
|
|
17764
|
+
_write(chunk, _encoding, callback) {
|
|
17765
|
+
try {
|
|
17766
|
+
this.layout.writeComposer(chunk);
|
|
17767
|
+
callback();
|
|
17768
|
+
} catch (error) {
|
|
17769
|
+
callback(error instanceof Error ? error : new Error("Terminal output failed"));
|
|
17770
|
+
}
|
|
17771
|
+
}
|
|
17772
|
+
};
|
|
17773
|
+
var TerminalLayout = class {
|
|
17774
|
+
outputProxy;
|
|
17775
|
+
enabled;
|
|
17776
|
+
output;
|
|
17777
|
+
errorOutput;
|
|
17778
|
+
originalWrite;
|
|
17779
|
+
originalErrorWrite;
|
|
17780
|
+
rawWrite;
|
|
17781
|
+
details;
|
|
17782
|
+
rl;
|
|
17783
|
+
prompt = "\u203A ";
|
|
17784
|
+
transcriptRow = 1;
|
|
17785
|
+
transcriptColumn = 1;
|
|
17786
|
+
wrapPending = false;
|
|
17787
|
+
savedTranscript;
|
|
17788
|
+
lastFooterTop = 0;
|
|
17789
|
+
suggestions = [];
|
|
17790
|
+
selected = 0;
|
|
17791
|
+
installed = false;
|
|
17792
|
+
closed = false;
|
|
17793
|
+
suspended = 0;
|
|
17794
|
+
paintQueued = false;
|
|
17795
|
+
bindSignals;
|
|
17796
|
+
resize = () => {
|
|
17797
|
+
if (this.closed || !this.enabled) return;
|
|
17798
|
+
this.clearPreviousFooter();
|
|
17799
|
+
this.transcriptRow = Math.min(this.transcriptRow, this.contentRows);
|
|
17800
|
+
this.transcriptColumn = Math.min(this.transcriptColumn, this.columns);
|
|
17801
|
+
this.rawWrite(`${CSI}1;${this.contentRows}r`);
|
|
17802
|
+
this.outputProxy.emit("resize");
|
|
17803
|
+
this.paintFooter();
|
|
17804
|
+
};
|
|
17805
|
+
exit = () => {
|
|
17806
|
+
this.close();
|
|
17807
|
+
};
|
|
17808
|
+
constructor(options) {
|
|
17809
|
+
this.output = options.output;
|
|
17810
|
+
this.errorOutput = options.errorOutput;
|
|
17811
|
+
this.details = { ...options.details };
|
|
17812
|
+
this.bindSignals = options.bindSignals !== false;
|
|
17813
|
+
this.originalWrite = options.output.write;
|
|
17814
|
+
this.rawWrite = options.output.write.bind(options.output);
|
|
17815
|
+
this.originalErrorWrite = options.errorOutput?.write;
|
|
17816
|
+
this.enabled = Boolean(options.output.isTTY && (options.output.columns ?? 80) >= 12 && (options.output.rows ?? 24) >= 7);
|
|
17817
|
+
this.outputProxy = new ComposerOutput(this);
|
|
17818
|
+
}
|
|
17819
|
+
get columns() {
|
|
17820
|
+
return Math.max(12, this.output.columns ?? 80);
|
|
17821
|
+
}
|
|
17822
|
+
get rows() {
|
|
17823
|
+
return Math.max(7, this.output.rows ?? 24);
|
|
17824
|
+
}
|
|
17825
|
+
get suggestionRows() {
|
|
17826
|
+
return Math.min(8, this.suggestions.length, Math.max(0, this.rows - BASE_FOOTER_ROWS - 2));
|
|
17827
|
+
}
|
|
17828
|
+
get contentRows() {
|
|
17829
|
+
return this.rows - BASE_FOOTER_ROWS - this.suggestionRows;
|
|
17830
|
+
}
|
|
17831
|
+
get inputRow() {
|
|
17832
|
+
return this.rows - 3;
|
|
17833
|
+
}
|
|
17834
|
+
install() {
|
|
17835
|
+
if (this.installed || this.closed || !this.enabled) return this;
|
|
17836
|
+
this.installed = true;
|
|
17837
|
+
this.output.write = ((chunk, encoding, callback) => this.writeTranscript(chunk, encoding, callback));
|
|
17838
|
+
if (this.errorOutput && this.errorOutput !== this.output) this.errorOutput.write = this.output.write;
|
|
17839
|
+
this.output.on("resize", this.resize);
|
|
17840
|
+
this.output.on("error", this.exit);
|
|
17841
|
+
if (this.errorOutput !== this.output) this.errorOutput?.on("error", this.exit);
|
|
17842
|
+
process.once("exit", this.exit);
|
|
17843
|
+
if (this.bindSignals) {
|
|
17844
|
+
process.once("SIGINT", this.exit);
|
|
17845
|
+
process.once("SIGTERM", this.exit);
|
|
17846
|
+
}
|
|
17847
|
+
this.rawWrite(`${CSI}2J${cup(1)}${CSI}1;${this.contentRows}r`);
|
|
17848
|
+
this.paintFooter();
|
|
17849
|
+
return this;
|
|
17850
|
+
}
|
|
17851
|
+
attachReadline(rl) {
|
|
17852
|
+
this.rl = rl;
|
|
17853
|
+
rl.setPrompt(this.prompt);
|
|
17854
|
+
this.paintFooter();
|
|
17855
|
+
}
|
|
17856
|
+
setPrompt(prompt) {
|
|
17857
|
+
this.prompt = prompt;
|
|
17858
|
+
this.rl?.setPrompt(prompt);
|
|
17859
|
+
this.paintFooter();
|
|
17860
|
+
}
|
|
17861
|
+
updateDetails(details) {
|
|
17862
|
+
this.details = { ...this.details, ...details };
|
|
17863
|
+
this.paintFooter();
|
|
17864
|
+
}
|
|
17865
|
+
setSuggestions(items, selectedIndex = 0) {
|
|
17866
|
+
if (this.enabled && this.installed && !this.closed) this.clearPreviousFooter();
|
|
17867
|
+
this.suggestions = [...items];
|
|
17868
|
+
this.selected = Math.max(0, Math.min(items.length - 1, selectedIndex));
|
|
17869
|
+
if (this.enabled && this.installed && !this.closed) {
|
|
17870
|
+
this.transcriptRow = Math.min(this.transcriptRow, this.contentRows);
|
|
17871
|
+
this.rawWrite(`${CSI}1;${this.contentRows}r`);
|
|
17872
|
+
this.paintFooter();
|
|
17873
|
+
}
|
|
17874
|
+
}
|
|
17875
|
+
/** Freeze the footer and focus choices in the transcript; its reserved rows stay visible. */
|
|
17876
|
+
suspend() {
|
|
17877
|
+
if (this.closed || !this.enabled) return;
|
|
17878
|
+
this.suspended += 1;
|
|
17879
|
+
this.rawWrite(cup(this.transcriptRow, this.transcriptColumn));
|
|
17880
|
+
}
|
|
17881
|
+
resume() {
|
|
17882
|
+
if (this.suspended > 0) this.suspended -= 1;
|
|
17883
|
+
if (!this.suspended) this.paintFooter();
|
|
17884
|
+
}
|
|
17885
|
+
writeComposer(chunk) {
|
|
17886
|
+
if (this.closed || !this.enabled || !this.installed) {
|
|
17887
|
+
this.rawWrite(chunk);
|
|
17888
|
+
return;
|
|
17889
|
+
}
|
|
17890
|
+
if (this.suspended) {
|
|
17891
|
+
this.writeTranscript(chunk);
|
|
17892
|
+
return;
|
|
17893
|
+
}
|
|
17894
|
+
if (!this.paintQueued) {
|
|
17895
|
+
this.paintQueued = true;
|
|
17896
|
+
queueMicrotask(() => {
|
|
17897
|
+
this.paintQueued = false;
|
|
17898
|
+
if (!this.closed && !this.suspended) this.paintFooter();
|
|
17899
|
+
});
|
|
17900
|
+
}
|
|
17901
|
+
}
|
|
17902
|
+
writeTranscript(chunk, encoding, callback) {
|
|
17903
|
+
if (this.closed || !this.enabled || !this.installed) return this.rawWrite(chunk, encoding, callback);
|
|
17904
|
+
const text = typeof chunk === "string" ? chunk : Buffer.from(chunk).toString(typeof encoding === "string" ? encoding : "utf8");
|
|
17905
|
+
const formatted = text.replace(/(?<!\r)\n/g, "\r\n").replace(/\x1b\[([012]?)J/g, (_escape, mode) => {
|
|
17906
|
+
const saved = cup(this.transcriptRow, this.transcriptColumn);
|
|
17907
|
+
const first = mode === "2" || mode === "1" ? 1 : this.transcriptRow;
|
|
17908
|
+
const last = mode === "1" ? this.transcriptRow : this.contentRows;
|
|
17909
|
+
let erased = "";
|
|
17910
|
+
for (let row = first; row <= last; row += 1) erased += `${cup(row)}${CSI}2K`;
|
|
17911
|
+
return erased + saved;
|
|
17912
|
+
});
|
|
17913
|
+
if (this.wrapPending && !/^[\r\n]/.test(stripVTControlCharacters(formatted))) {
|
|
17914
|
+
this.rawWrite(`${cup(this.transcriptRow, this.columns)}\r
|
|
17915
|
+
`);
|
|
17916
|
+
this.transcriptRow = Math.min(this.contentRows, this.transcriptRow + 1);
|
|
17917
|
+
this.transcriptColumn = 1;
|
|
17918
|
+
this.wrapPending = false;
|
|
17919
|
+
}
|
|
17920
|
+
this.rawWrite(cup(this.transcriptRow, this.transcriptColumn));
|
|
17921
|
+
const accepted = this.rawWrite(
|
|
17922
|
+
formatted,
|
|
17923
|
+
typeof encoding === "string" ? encoding : void 0,
|
|
17924
|
+
typeof encoding === "function" ? encoding : callback
|
|
17925
|
+
);
|
|
17926
|
+
this.trackTranscript(formatted);
|
|
17927
|
+
if (!this.suspended) this.placeInputCursor();
|
|
17928
|
+
return accepted;
|
|
17929
|
+
}
|
|
17930
|
+
trackTranscript(text) {
|
|
17931
|
+
const parts = text.split(/(\x1b\[[0-?]*[ -/]*[@-~]|\x1b\][^\x07]*(?:\x07|\x1b\\)|\x1b.)/g);
|
|
17932
|
+
for (const part of parts) {
|
|
17933
|
+
if (part === "\x1B7") {
|
|
17934
|
+
this.savedTranscript = { row: this.transcriptRow, column: this.transcriptColumn, wrapPending: this.wrapPending };
|
|
17935
|
+
} else if (part === "\x1B8") {
|
|
17936
|
+
if (this.savedTranscript) {
|
|
17937
|
+
this.transcriptRow = this.savedTranscript.row;
|
|
17938
|
+
this.transcriptColumn = this.savedTranscript.column;
|
|
17939
|
+
this.wrapPending = this.savedTranscript.wrapPending;
|
|
17940
|
+
}
|
|
17941
|
+
} else if (part.startsWith("\x1B[")) {
|
|
17942
|
+
const match = /^\x1b\[([\d;]*)([A-GHf])$/.exec(part);
|
|
17943
|
+
if (!match) continue;
|
|
17944
|
+
const values = match[1].split(";").map((value) => Number(value) || 1);
|
|
17945
|
+
const n = values[0];
|
|
17946
|
+
this.wrapPending = false;
|
|
17947
|
+
if (match[2] === "A") this.transcriptRow -= n;
|
|
17948
|
+
else if (match[2] === "B") this.transcriptRow += n;
|
|
17949
|
+
else if (match[2] === "C") this.transcriptColumn += n;
|
|
17950
|
+
else if (match[2] === "D") this.transcriptColumn -= n;
|
|
17951
|
+
else if (match[2] === "G") this.transcriptColumn = n;
|
|
17952
|
+
else if (match[2] === "H" || match[2] === "f") {
|
|
17953
|
+
this.transcriptRow = n;
|
|
17954
|
+
this.transcriptColumn = values[1] ?? 1;
|
|
17955
|
+
}
|
|
17956
|
+
} else if (!part.startsWith("\x1B")) {
|
|
17957
|
+
for (const cell of cells(part)) {
|
|
17958
|
+
if (cell.text === "\r" || cell.text === "\r\n") {
|
|
17959
|
+
this.wrapPending = false;
|
|
17960
|
+
this.transcriptColumn = 1;
|
|
17961
|
+
if (cell.text === "\r\n") this.transcriptRow += 1;
|
|
17962
|
+
} else if (cell.text === "\n") {
|
|
17963
|
+
this.wrapPending = false;
|
|
17964
|
+
this.transcriptColumn = 1;
|
|
17965
|
+
this.transcriptRow += 1;
|
|
17966
|
+
} else if (cell.text === "\b") this.transcriptColumn -= 1;
|
|
17967
|
+
else if (cell.text === " ") this.transcriptColumn += 8 - (this.transcriptColumn - 1) % 8;
|
|
17968
|
+
else {
|
|
17969
|
+
if (this.wrapPending || this.transcriptColumn + cell.width - 1 > this.columns) {
|
|
17970
|
+
this.transcriptColumn = 1;
|
|
17971
|
+
this.transcriptRow += 1;
|
|
17972
|
+
this.wrapPending = false;
|
|
17973
|
+
}
|
|
17974
|
+
this.transcriptColumn += cell.width;
|
|
17975
|
+
if (this.transcriptColumn > this.columns) {
|
|
17976
|
+
this.transcriptColumn = this.columns;
|
|
17977
|
+
this.wrapPending = true;
|
|
17978
|
+
}
|
|
17979
|
+
}
|
|
17980
|
+
this.transcriptRow = Math.min(this.contentRows, this.transcriptRow);
|
|
17981
|
+
}
|
|
17982
|
+
}
|
|
17983
|
+
this.transcriptRow = Math.max(1, Math.min(this.contentRows, this.transcriptRow));
|
|
17984
|
+
this.transcriptColumn = Math.max(1, Math.min(this.columns, this.transcriptColumn));
|
|
17985
|
+
}
|
|
17986
|
+
}
|
|
17987
|
+
composer() {
|
|
17988
|
+
const available = this.columns - 4;
|
|
17989
|
+
const prompt = fit(this.rl?.getPrompt() ?? this.prompt, Math.min(24, Math.max(2, Math.floor(available / 3))));
|
|
17990
|
+
const input = plain(this.rl?.line ?? "");
|
|
17991
|
+
const cursor = width(plain((this.rl?.line ?? "").slice(0, this.rl?.cursor ?? 0)));
|
|
17992
|
+
const capacity = Math.max(1, available - width(prompt));
|
|
17993
|
+
const offset = Math.max(0, cursor - capacity + 1);
|
|
17994
|
+
let visible = "";
|
|
17995
|
+
let used = 0;
|
|
17996
|
+
let skipped = 0;
|
|
17997
|
+
for (const cell of cells(input)) {
|
|
17998
|
+
if (skipped + cell.width <= offset) {
|
|
17999
|
+
skipped += cell.width;
|
|
18000
|
+
continue;
|
|
18001
|
+
}
|
|
18002
|
+
if (used + cell.width > capacity) break;
|
|
18003
|
+
visible += cell.text;
|
|
18004
|
+
used += cell.width;
|
|
18005
|
+
}
|
|
18006
|
+
return { text: `${prompt}${visible}`, column: Math.min(this.columns - 1, 3 + width(prompt) + cursor - skipped) };
|
|
18007
|
+
}
|
|
18008
|
+
placeInputCursor() {
|
|
18009
|
+
const composer = this.composer();
|
|
18010
|
+
this.rawWrite(`${cup(this.inputRow, composer.column)}${CSI}?25h`);
|
|
18011
|
+
}
|
|
18012
|
+
clearPreviousFooter() {
|
|
18013
|
+
if (!this.lastFooterTop) return;
|
|
18014
|
+
let clear = "";
|
|
18015
|
+
for (let row = this.lastFooterTop; row <= this.rows; row += 1) clear += `${cup(row)}${CSI}2K`;
|
|
18016
|
+
this.rawWrite(clear);
|
|
18017
|
+
}
|
|
18018
|
+
paintFooter() {
|
|
18019
|
+
if (!this.enabled || !this.installed || this.closed || this.suspended) return;
|
|
18020
|
+
this.lastFooterTop = this.contentRows + 1;
|
|
18021
|
+
const inner = this.columns - 2;
|
|
18022
|
+
const border = "\u2500".repeat(inner);
|
|
18023
|
+
const composer = this.composer();
|
|
18024
|
+
const status = this.details.status ? `${plain(this.details.status)} \xB7 ` : "";
|
|
18025
|
+
const modelRoom = Math.max(6, this.columns - width(`${status}${this.details.provider} \xB7 \xB7 ${this.details.mode}`));
|
|
18026
|
+
const metadata = `${status}${this.details.provider} \xB7 ${fit(this.details.model, modelRoom)} \xB7 ${this.details.mode}`;
|
|
18027
|
+
const usage = `${count3(this.details.tokens)} reported tokens \xB7 ${count3(this.details.steps)} steps \xB7 est ctx ${count3(this.details.contextUsed)}/${count3(this.details.contextMax)}`;
|
|
18028
|
+
let frame = "";
|
|
18029
|
+
const suggestions = this.suggestionRows;
|
|
18030
|
+
const first = Math.max(0, Math.min(this.selected - Math.floor(suggestions / 2), this.suggestions.length - suggestions));
|
|
18031
|
+
for (let i = 0; i < suggestions; i += 1) {
|
|
18032
|
+
const index = first + i;
|
|
18033
|
+
const selected = index === this.selected;
|
|
18034
|
+
frame += `${cup(this.contentRows + 1 + i)}${CSI}2K${selected ? `${CSI}7m` : ""}${fit(`${selected ? "\u203A" : " "} ${this.suggestions[index] ?? ""}`, this.columns)}${CSI}0m`;
|
|
18035
|
+
}
|
|
18036
|
+
const inputText = fit(composer.text, this.columns - 4);
|
|
18037
|
+
const paddedInput = inputText + " ".repeat(Math.max(0, this.columns - 4 - width(inputText)));
|
|
18038
|
+
const lines = [`\u250C${border}\u2510`, `\u2502 ${paddedInput} \u2502`, `\u2514${border}\u2518`, fit(metadata, this.columns), fit(usage, this.columns)];
|
|
18039
|
+
lines.forEach((line, index) => {
|
|
18040
|
+
frame += `${cup(this.rows - 4 + index)}${CSI}2K${line}`;
|
|
18041
|
+
});
|
|
18042
|
+
this.rawWrite(`${frame}${cup(this.inputRow, composer.column)}${CSI}?25h`);
|
|
18043
|
+
}
|
|
18044
|
+
close() {
|
|
18045
|
+
if (this.closed) return;
|
|
18046
|
+
this.closed = true;
|
|
18047
|
+
if (!this.installed) return;
|
|
18048
|
+
this.output.write = this.originalWrite;
|
|
18049
|
+
if (this.errorOutput && this.errorOutput !== this.output && this.originalErrorWrite) this.errorOutput.write = this.originalErrorWrite;
|
|
18050
|
+
this.output.off("resize", this.resize);
|
|
18051
|
+
this.output.off("error", this.exit);
|
|
18052
|
+
if (this.errorOutput !== this.output) this.errorOutput?.off("error", this.exit);
|
|
18053
|
+
process.off("exit", this.exit);
|
|
18054
|
+
process.off("SIGINT", this.exit);
|
|
18055
|
+
process.off("SIGTERM", this.exit);
|
|
18056
|
+
try {
|
|
18057
|
+
this.rawWrite(`${RESET_REGION}${MOUSE_OFF2}${CSI}?25h`);
|
|
18058
|
+
this.clearPreviousFooter();
|
|
18059
|
+
this.rawWrite(`${cup(this.transcriptRow, this.transcriptColumn)}\r
|
|
18060
|
+
`);
|
|
18061
|
+
} catch {
|
|
18062
|
+
}
|
|
18063
|
+
}
|
|
18064
|
+
};
|
|
18065
|
+
function createTerminalLayout(options) {
|
|
18066
|
+
return new TerminalLayout(options).install();
|
|
18067
|
+
}
|
|
18068
|
+
|
|
16856
18069
|
// src/ui/welcome.ts
|
|
16857
18070
|
import chalk10, { Chalk } from "chalk";
|
|
16858
18071
|
var LOGO = [
|
|
@@ -16866,76 +18079,75 @@ var LOGO = [
|
|
|
16866
18079
|
function renderWelcome(info, options = {}) {
|
|
16867
18080
|
const columns = normalizeColumns(options.columns);
|
|
16868
18081
|
const paint = new Chalk({ level: options.color === false ? 0 : chalk10.level });
|
|
16869
|
-
const cardWidth =
|
|
18082
|
+
const cardWidth = columns;
|
|
16870
18083
|
const innerWidth = cardWidth - 4;
|
|
16871
18084
|
const wide = columns >= 64;
|
|
16872
18085
|
const lines = [];
|
|
16873
18086
|
if (wide) {
|
|
16874
18087
|
lines.push("");
|
|
16875
18088
|
for (let index = 0; index < LOGO.length; index += 1) {
|
|
16876
|
-
|
|
16877
|
-
lines.push(` ${color(LOGO[index])}`);
|
|
18089
|
+
lines.push(` ${paint.bold.white(LOGO[index])}`);
|
|
16878
18090
|
}
|
|
16879
18091
|
} else {
|
|
16880
|
-
lines.push("", ` ${paint.bold.
|
|
18092
|
+
lines.push("", ` ${paint.bold.white("ULTRAC")} ${paint.gray(`v${info.version}`)}`);
|
|
16881
18093
|
}
|
|
16882
18094
|
const topLabel = wide ? ` ultrac v${info.version} ` : " session ";
|
|
16883
18095
|
const topFill = Math.max(0, cardWidth - topLabel.length - 2);
|
|
16884
|
-
lines.push(paint.
|
|
18096
|
+
lines.push(paint.gray(`\u256D${topLabel}${"\u2500".repeat(topFill)}\u256E`));
|
|
16885
18097
|
lines.push(cardLine(`${info.model} via ${info.providerName}`, innerWidth, paint.bold));
|
|
16886
18098
|
lines.push(cardLine(`${info.mode} \xB7 ${info.protocol}`, innerWidth, paint.white));
|
|
16887
18099
|
lines.push(cardLine(shortenMiddle(info.workspace, innerWidth), innerWidth, paint.gray));
|
|
16888
|
-
lines.push(paint.
|
|
18100
|
+
lines.push(paint.gray(`\u251C${"\u2500".repeat(cardWidth - 2)}\u2524`));
|
|
16889
18101
|
lines.push(cardLine(info.budget, innerWidth, paint.gray));
|
|
16890
18102
|
lines.push(cardLine(`logs ${shortenMiddle(info.diagnosticPath, Math.max(8, innerWidth - 5))}`, innerWidth, paint.gray));
|
|
16891
|
-
lines.push(paint.
|
|
18103
|
+
lines.push(paint.gray(`\u2570${"\u2500".repeat(cardWidth - 2)}\u256F`));
|
|
16892
18104
|
lines.push(
|
|
16893
|
-
wide ? `${paint.gray(" /help commands")} ${paint.dim("\xB7")} ${paint.gray("
|
|
18105
|
+
wide ? `${paint.gray(" /help commands")} ${paint.dim("\xB7")} ${paint.gray("Esc stop")} ${paint.dim("\xB7")} ${paint.gray("Ctrl-C exit")}` : paint.gray(" /help \xB7 Esc \xB7 Ctrl-C")
|
|
16894
18106
|
);
|
|
16895
18107
|
return lines.join("\n");
|
|
16896
18108
|
}
|
|
16897
18109
|
function renderComposer(columns = 80, color = true) {
|
|
16898
18110
|
const paint = new Chalk({ level: color ? chalk10.level : 0 });
|
|
16899
|
-
const
|
|
18111
|
+
const width2 = normalizeColumns(columns);
|
|
16900
18112
|
const label = " message ";
|
|
16901
|
-
const fill = Math.max(1,
|
|
18113
|
+
const fill = Math.max(1, width2 - label.length - 2);
|
|
16902
18114
|
return {
|
|
16903
18115
|
frame: `
|
|
16904
|
-
${paint.
|
|
16905
|
-
${paint.
|
|
16906
|
-
prompt: paint.bold.
|
|
18116
|
+
${paint.gray(`\u256D\u2500${label}${"\u2500".repeat(fill)}`)}
|
|
18117
|
+
${paint.gray("\u2502")}`,
|
|
18118
|
+
prompt: paint.bold.white("\u2570\u2500\u276F ")
|
|
16907
18119
|
};
|
|
16908
18120
|
}
|
|
16909
|
-
function cardLine(value,
|
|
16910
|
-
const clipped = truncate3(value,
|
|
16911
|
-
return `\u2502 ${style(clipped)}${" ".repeat(Math.max(0,
|
|
18121
|
+
function cardLine(value, width2, style) {
|
|
18122
|
+
const clipped = truncate3(value, width2);
|
|
18123
|
+
return `\u2502 ${style(clipped)}${" ".repeat(Math.max(0, width2 - clipped.length))} \u2502`;
|
|
16912
18124
|
}
|
|
16913
18125
|
function normalizeColumns(columns) {
|
|
16914
|
-
return Number.isFinite(columns) ? Math.max(
|
|
18126
|
+
return Number.isFinite(columns) ? Math.max(16, Math.floor(columns)) : 80;
|
|
16915
18127
|
}
|
|
16916
|
-
function truncate3(value,
|
|
16917
|
-
if (value.length <=
|
|
16918
|
-
if (
|
|
16919
|
-
return `${value.slice(0,
|
|
18128
|
+
function truncate3(value, width2) {
|
|
18129
|
+
if (value.length <= width2) return value;
|
|
18130
|
+
if (width2 <= 1) return "\u2026";
|
|
18131
|
+
return `${value.slice(0, width2 - 1)}\u2026`;
|
|
16920
18132
|
}
|
|
16921
|
-
function shortenMiddle(value,
|
|
16922
|
-
if (value.length <=
|
|
16923
|
-
if (
|
|
16924
|
-
const side = Math.floor((
|
|
16925
|
-
return `${value.slice(0, side)}\u2026${value.slice(value.length - (
|
|
18133
|
+
function shortenMiddle(value, width2) {
|
|
18134
|
+
if (value.length <= width2) return value;
|
|
18135
|
+
if (width2 < 9) return truncate3(value, width2);
|
|
18136
|
+
const side = Math.floor((width2 - 1) / 2);
|
|
18137
|
+
return `${value.slice(0, side)}\u2026${value.slice(value.length - (width2 - side - 1))}`;
|
|
16926
18138
|
}
|
|
16927
18139
|
|
|
16928
18140
|
// src/update/check.ts
|
|
16929
18141
|
import { spawn as spawn4 } from "child_process";
|
|
16930
18142
|
import { randomBytes as randomBytes4 } from "crypto";
|
|
16931
|
-
import { readFile as readFile13, rename as
|
|
16932
|
-
import { join as
|
|
18143
|
+
import { readFile as readFile13, rename as rename3, unlink as unlink4, writeFile as writeFile12 } from "fs/promises";
|
|
18144
|
+
import { join as join14 } from "path";
|
|
16933
18145
|
var DEFAULT_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
16934
18146
|
var QUERY_TIMEOUT_MS = 5e3;
|
|
16935
18147
|
var INSTALL_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
16936
18148
|
async function checkForUpdate(options) {
|
|
16937
18149
|
const now = options.now ?? Date.now();
|
|
16938
|
-
const cachePath = options.cachePath ??
|
|
18150
|
+
const cachePath = options.cachePath ?? join14(await ensureUltracHome(), "update-check.json");
|
|
16939
18151
|
const maxAgeMs = options.maxAgeMs ?? DEFAULT_MAX_AGE_MS;
|
|
16940
18152
|
let latestVersion;
|
|
16941
18153
|
if (!options.force) {
|
|
@@ -17019,9 +18231,9 @@ async function writeCache(path, cache) {
|
|
|
17019
18231
|
try {
|
|
17020
18232
|
await writeFile12(temporary, `${JSON.stringify(cache)}
|
|
17021
18233
|
`, { encoding: "utf8", mode: 384, flag: "wx" });
|
|
17022
|
-
await
|
|
18234
|
+
await rename3(temporary, path);
|
|
17023
18235
|
} catch {
|
|
17024
|
-
await
|
|
18236
|
+
await unlink4(temporary).catch(() => void 0);
|
|
17025
18237
|
}
|
|
17026
18238
|
}
|
|
17027
18239
|
async function runNpm(args, options) {
|
|
@@ -17057,6 +18269,10 @@ async function runNpm(args, options) {
|
|
|
17057
18269
|
// src/commands/repl.ts
|
|
17058
18270
|
var HELP = `Commands:
|
|
17059
18271
|
/help Show this help
|
|
18272
|
+
/doctor Check local setup
|
|
18273
|
+
/mcp Show connected MCP servers
|
|
18274
|
+
/continue [focus] Continue unfinished work
|
|
18275
|
+
/autonomous [on|off|status] Continue step-limited turns (up to five rounds)
|
|
17060
18276
|
/model [name] Show or set the model
|
|
17061
18277
|
/mode [name] Show or set the agent mode (${agentModeNames.join(", ")})
|
|
17062
18278
|
/permissions [readonly|workspace|full] Show or set permissions
|
|
@@ -17079,11 +18295,19 @@ var HELP = `Commands:
|
|
|
17079
18295
|
/allow <perm> Grant a permission for this session (write, shell, network)
|
|
17080
18296
|
/whoami Show sign-in state
|
|
17081
18297
|
/tools List available tools
|
|
18298
|
+
/logs Show the private diagnostic log path
|
|
18299
|
+
/ghost [on|off|status] Quiet activity output; diagnostics stay enabled
|
|
18300
|
+
/output [number|list] Inspect saved tool output (latest by default)
|
|
17082
18301
|
/memory List and inspect stored memories
|
|
18302
|
+
/skill <name> [args] Run an installed skill
|
|
17083
18303
|
/skills List installed skills
|
|
17084
18304
|
/update Show how to install an available CLI update
|
|
17085
18305
|
/clear Start a fresh conversation
|
|
17086
18306
|
/exit, /quit End the session
|
|
18307
|
+
Keys:
|
|
18308
|
+
Type during a turn Queue a follow-up for after the current tool call
|
|
18309
|
+
Esc Stop the current turn; the session stays open
|
|
18310
|
+
Ctrl-C Exit and save the session
|
|
17087
18311
|
Custom commands come from skills that declare a 'command:' key in their
|
|
17088
18312
|
frontmatter; built-ins always win a name clash. Type anything else to talk to Ultrac.`;
|
|
17089
18313
|
var EXIT_REPL_NOT_STARTED = 1;
|
|
@@ -17175,7 +18399,7 @@ async function runRepl(options, initialTask = "") {
|
|
|
17175
18399
|
const hasFallbackChain = Boolean(config.fallback?.chain?.length);
|
|
17176
18400
|
if (providerName === "ultrac" && !hasFallbackChain && !await resolveUltracCredentials()) {
|
|
17177
18401
|
console.log(chalk11.yellow("You are not signed in to Ultrac."));
|
|
17178
|
-
console.log(`Run ${chalk11.
|
|
18402
|
+
console.log(`Run ${chalk11.white("ultrac login")} first, or start with ${chalk11.white("--provider echo")}.`);
|
|
17179
18403
|
process.exitCode = EXIT_REPL_NOT_STARTED;
|
|
17180
18404
|
return;
|
|
17181
18405
|
}
|
|
@@ -17237,10 +18461,37 @@ async function runRepl(options, initialTask = "") {
|
|
|
17237
18461
|
mode,
|
|
17238
18462
|
permissions: basePermissions
|
|
17239
18463
|
});
|
|
18464
|
+
const layout = stdin5.isTTY && stdout5.isTTY && process.env.TERM !== "dumb" ? createTerminalLayout({
|
|
18465
|
+
output: stdout5,
|
|
18466
|
+
errorOutput: stderr,
|
|
18467
|
+
bindSignals: false,
|
|
18468
|
+
details: { provider: provider.name, model: session.model, mode: session.mode, status: "Idle" }
|
|
18469
|
+
}) : void 0;
|
|
18470
|
+
const refreshDetails = () => {
|
|
18471
|
+
if (!layout?.enabled) return;
|
|
18472
|
+
const steps = session.toSession().steps;
|
|
18473
|
+
const sum = (key) => {
|
|
18474
|
+
const values = steps.flatMap((step) => typeof step.usage?.[key] === "number" ? [step.usage[key]] : []);
|
|
18475
|
+
return values.length ? values.reduce((a, b) => a + b, 0) : void 0;
|
|
18476
|
+
};
|
|
18477
|
+
const context = session.contextStatus();
|
|
18478
|
+
layout.updateDetails({
|
|
18479
|
+
contextUsed: context.estimatedPromptTokens,
|
|
18480
|
+
contextMax: context.contextTokens,
|
|
18481
|
+
provider: provider.name,
|
|
18482
|
+
model: session.model,
|
|
18483
|
+
mode: session.mode,
|
|
18484
|
+
tokens: sum("totalTokens"),
|
|
18485
|
+
inputTokens: sum("inputTokens"),
|
|
18486
|
+
outputTokens: sum("outputTokens"),
|
|
18487
|
+
steps: steps.length
|
|
18488
|
+
});
|
|
18489
|
+
};
|
|
17240
18490
|
const writeCheckpoint = createSessionCheckpointWriter(workspace, (message) => {
|
|
17241
18491
|
console.error(chalk11.yellow(message));
|
|
17242
18492
|
});
|
|
17243
18493
|
const persistSession = async (target = session) => {
|
|
18494
|
+
refreshDetails();
|
|
17244
18495
|
const snapshot = target.toSession();
|
|
17245
18496
|
if (!hasSavedSessionContent(snapshot)) return null;
|
|
17246
18497
|
return writeCheckpoint(snapshot);
|
|
@@ -17262,6 +18513,7 @@ async function runRepl(options, initialTask = "") {
|
|
|
17262
18513
|
try {
|
|
17263
18514
|
await session.start();
|
|
17264
18515
|
} catch (error) {
|
|
18516
|
+
layout?.close();
|
|
17265
18517
|
throw new Error(`Could not start the session: ${errorMessage2(error)}`, { cause: error });
|
|
17266
18518
|
}
|
|
17267
18519
|
const update = await updateCheck;
|
|
@@ -17270,21 +18522,58 @@ async function runRepl(options, initialTask = "") {
|
|
|
17270
18522
|
\u2191 Update available: ${update.currentVersion} \u2192 ${update.latestVersion}`));
|
|
17271
18523
|
console.log(chalk11.gray(" Run `ultrac update` after this session."));
|
|
17272
18524
|
}
|
|
17273
|
-
const rl = createInterface3({ input: stdin5, output: stdout5 });
|
|
18525
|
+
const rl = createInterface3({ input: stdin5, output: layout?.enabled ? layout.outputProxy : stdout5 });
|
|
18526
|
+
layout?.attachReadline(rl);
|
|
18527
|
+
refreshDetails();
|
|
18528
|
+
const builtinEntries = BUILTIN_SLASH_COMMANDS.map((command) => ({ text: `/${command}`, label: `/${command}` }));
|
|
18529
|
+
const slashCompletion = layout?.enabled ? attachSlashCompletion(rl, async () => {
|
|
18530
|
+
const [commands, skills] = await Promise.all([listSkillCommands(workspace), listSkills(workspace)]);
|
|
18531
|
+
const declared = new Set(commands.commands.map((command) => command.skillName));
|
|
18532
|
+
return [
|
|
18533
|
+
...builtinEntries,
|
|
18534
|
+
...commands.commands.map((command) => ({ text: `/${command.command}`, label: `/${command.command}`, description: command.description, skill: true })),
|
|
18535
|
+
...skills.filter((skill) => !declared.has(skill.name)).map((skill) => ({ text: `/skill ${skill.name}`, label: `/skill ${skill.name}`, description: skill.description, skill: true }))
|
|
18536
|
+
];
|
|
18537
|
+
}, (rows, index) => layout.setSuggestions(rows, index), builtinEntries) : void 0;
|
|
17274
18538
|
const reader = new LineReader(rl);
|
|
17275
18539
|
const question = async (prompt, signal) => {
|
|
17276
|
-
|
|
17277
|
-
|
|
18540
|
+
slashCompletion?.suspend();
|
|
18541
|
+
try {
|
|
18542
|
+
if (layout?.enabled) {
|
|
18543
|
+
console.log(prompt);
|
|
18544
|
+
layout.setPrompt("\u203A ");
|
|
18545
|
+
} else stdout5.write(prompt);
|
|
18546
|
+
return await reader.nextFresh(signal);
|
|
18547
|
+
} finally {
|
|
18548
|
+
layout?.setPrompt("\u203A ");
|
|
18549
|
+
slashCompletion?.resume();
|
|
18550
|
+
}
|
|
17278
18551
|
};
|
|
17279
18552
|
const drainQueuedMessages = () => reader.drainQueuedMessages();
|
|
18553
|
+
let autonomousRounds = 1;
|
|
17280
18554
|
setAskUserRuntime(async (text, options2, signal) => {
|
|
17281
|
-
console.log(chalk11.
|
|
18555
|
+
console.log(chalk11.white(`
|
|
17282
18556
|
? ${text}`));
|
|
17283
|
-
|
|
17284
|
-
|
|
17285
|
-
|
|
18557
|
+
let rawAnswer;
|
|
18558
|
+
if (options2?.length && layout?.enabled) {
|
|
18559
|
+
slashCompletion?.suspend();
|
|
18560
|
+
layout.setPrompt("\u203A ");
|
|
18561
|
+
let choices;
|
|
18562
|
+
try {
|
|
18563
|
+
choices = attachInteractiveChoices(rl, options2, { signal, output: stdout5, prompt: "\u203A ", maxRow: () => layout.contentRows });
|
|
18564
|
+
rawAnswer = await reader.nextFresh(signal);
|
|
18565
|
+
} finally {
|
|
18566
|
+
choices?.cleanup();
|
|
18567
|
+
layout.setPrompt("\u203A ");
|
|
18568
|
+
slashCompletion?.resume();
|
|
18569
|
+
}
|
|
18570
|
+
} else {
|
|
18571
|
+
if (options2?.length) {
|
|
18572
|
+
options2.forEach((option, index) => console.log(chalk11.gray(` ${index + 1}. ${option}`)));
|
|
18573
|
+
console.log(chalk11.gray(" (type a number, or answer in your own words)"));
|
|
18574
|
+
}
|
|
18575
|
+
rawAnswer = await question(chalk11.white(" \u203A "), signal);
|
|
17286
18576
|
}
|
|
17287
|
-
const rawAnswer = await question(chalk11.cyan(" \u203A "), signal);
|
|
17288
18577
|
if (signal?.aborted) return null;
|
|
17289
18578
|
const answer = (rawAnswer ?? "").trim();
|
|
17290
18579
|
const picked = Number.parseInt(answer, 10);
|
|
@@ -17295,42 +18584,65 @@ async function runRepl(options, initialTask = "") {
|
|
|
17295
18584
|
});
|
|
17296
18585
|
let running = true;
|
|
17297
18586
|
let turnAbort;
|
|
17298
|
-
|
|
17299
|
-
|
|
18587
|
+
const exitSession = () => {
|
|
18588
|
+
handleReplExit(turnAbort);
|
|
17300
18589
|
running = false;
|
|
17301
18590
|
console.log(chalk11.gray("\nExiting ultrac\u2026"));
|
|
17302
18591
|
rl.close();
|
|
17303
|
-
}
|
|
18592
|
+
};
|
|
18593
|
+
rl.on("SIGINT", exitSession);
|
|
18594
|
+
const onKeypress = (_input, key) => {
|
|
18595
|
+
if (!key || key.name !== "escape" || key.ctrl || key.meta) return;
|
|
18596
|
+
if (!slashCompletion?.handledEscape()) handleReplEscape(turnAbort);
|
|
18597
|
+
};
|
|
18598
|
+
if (stdin5.isTTY) {
|
|
18599
|
+
emitKeypressEvents(stdin5, rl);
|
|
18600
|
+
stdin5.on("keypress", onKeypress);
|
|
18601
|
+
}
|
|
17304
18602
|
const terminate = () => {
|
|
17305
18603
|
running = false;
|
|
17306
18604
|
turnAbort?.abort();
|
|
17307
18605
|
rl.close();
|
|
17308
18606
|
};
|
|
17309
18607
|
process.on("SIGTERM", terminate);
|
|
18608
|
+
let turnStartedAt;
|
|
18609
|
+
const footerTimer = setInterval(() => {
|
|
18610
|
+
const seconds = turnStartedAt === void 0 ? void 0 : Math.floor((Date.now() - turnStartedAt) / 1e3);
|
|
18611
|
+
layout?.updateDetails({ status: seconds === void 0 ? "Idle" : `Running ${Math.floor(seconds / 60).toString().padStart(2, "0")}:${(seconds % 60).toString().padStart(2, "0")}` });
|
|
18612
|
+
}, 1e3);
|
|
18613
|
+
footerTimer.unref();
|
|
17310
18614
|
const withInterrupt = async (run) => {
|
|
17311
18615
|
turnAbort = new AbortController();
|
|
18616
|
+
turnStartedAt = Date.now();
|
|
18617
|
+
layout?.updateDetails({ status: "Running 00:00" });
|
|
17312
18618
|
try {
|
|
17313
18619
|
return await run(turnAbort.signal);
|
|
17314
18620
|
} finally {
|
|
17315
18621
|
turnAbort = void 0;
|
|
18622
|
+
turnStartedAt = void 0;
|
|
18623
|
+
refreshDetails();
|
|
18624
|
+
layout?.updateDetails({ status: "Idle" });
|
|
17316
18625
|
}
|
|
17317
18626
|
};
|
|
17318
18627
|
try {
|
|
17319
18628
|
if (initialTask) {
|
|
17320
|
-
console.log(chalk11.
|
|
18629
|
+
console.log(chalk11.white(`
|
|
17321
18630
|
\u203A ${initialTask}`));
|
|
17322
|
-
await withInterrupt((signal) => runAndCheckpointTurn(session, initialTask, question, signal, persistSession, drainQueuedMessages));
|
|
18631
|
+
await withInterrupt((signal) => runAndCheckpointTurn(session, initialTask, question, signal, persistSession, drainQueuedMessages, () => reader.composing, autonomousRounds));
|
|
17323
18632
|
}
|
|
17324
18633
|
while (running) {
|
|
17325
|
-
if (
|
|
18634
|
+
if (layout?.enabled) {
|
|
18635
|
+
layout.setPrompt("\u203A ");
|
|
18636
|
+
} else if (stdout5.isTTY) {
|
|
17326
18637
|
const composer = renderComposer(stdout5.columns, true);
|
|
17327
18638
|
console.log(composer.frame);
|
|
17328
18639
|
stdout5.write(composer.prompt);
|
|
17329
18640
|
} else {
|
|
17330
|
-
stdout5.write(chalk11.
|
|
18641
|
+
stdout5.write(chalk11.white("\n\u203A "));
|
|
17331
18642
|
}
|
|
17332
18643
|
const line = await reader.next();
|
|
17333
18644
|
if (line === null) break;
|
|
18645
|
+
if (layout?.enabled && line.trim()) console.log(`\u203A ${line.replace(/[\x00-\x1f\x7f]/g, " ")}`);
|
|
17334
18646
|
const raw = line.trim();
|
|
17335
18647
|
if (!raw) continue;
|
|
17336
18648
|
if (raw.startsWith("/")) {
|
|
@@ -17346,7 +18658,7 @@ async function runRepl(options, initialTask = "") {
|
|
|
17346
18658
|
resetContextBudgetNotices();
|
|
17347
18659
|
console.log(chalk11.gray("Started a fresh conversation."));
|
|
17348
18660
|
}, async (prompt) => {
|
|
17349
|
-
await runAndCheckpointTurn(session, prompt, question, signal, persistSession, drainQueuedMessages);
|
|
18661
|
+
await runAndCheckpointTurn(session, prompt, question, signal, persistSession, drainQueuedMessages, () => reader.composing, autonomousRounds);
|
|
17350
18662
|
}, signal, async (id) => {
|
|
17351
18663
|
const lineage = await loadSessionLineage(workspace, id);
|
|
17352
18664
|
const restored = createSession({ model: session.model, mode: session.mode, permissions: session.currentPermissions });
|
|
@@ -17355,7 +18667,11 @@ async function runRepl(options, initialTask = "") {
|
|
|
17355
18667
|
session = restored;
|
|
17356
18668
|
resetContextBudgetNotices();
|
|
17357
18669
|
console.log(`Restored ${lineage.sessions.length} saved session(s). Permissions were kept from the current session.`);
|
|
17358
|
-
})
|
|
18670
|
+
}, { mcpStatus: () => mcpRuntime?.statuses ?? [], autonomous: (setting) => {
|
|
18671
|
+
if (setting === "on") autonomousRounds = 5;
|
|
18672
|
+
if (setting === "off") autonomousRounds = 1;
|
|
18673
|
+
return autonomousRounds;
|
|
18674
|
+
} });
|
|
17359
18675
|
});
|
|
17360
18676
|
if (handled === "exit") {
|
|
17361
18677
|
running = false;
|
|
@@ -17364,7 +18680,9 @@ async function runRepl(options, initialTask = "") {
|
|
|
17364
18680
|
setSubagentRuntime({ provider, model: session.model, mode: session.mode, registry });
|
|
17365
18681
|
if (handled === "unhandled") {
|
|
17366
18682
|
try {
|
|
17367
|
-
await runSkillCommand(session, raw, workspace, question)
|
|
18683
|
+
await withInterrupt((signal) => runSkillCommand(session, raw, workspace, question, async (prompt) => {
|
|
18684
|
+
await runAndCheckpointTurn(session, prompt, question, signal, persistSession, drainQueuedMessages, () => reader.composing, autonomousRounds);
|
|
18685
|
+
}));
|
|
17368
18686
|
} finally {
|
|
17369
18687
|
await persistSession();
|
|
17370
18688
|
}
|
|
@@ -17375,20 +18693,29 @@ async function runRepl(options, initialTask = "") {
|
|
|
17375
18693
|
}
|
|
17376
18694
|
continue;
|
|
17377
18695
|
}
|
|
17378
|
-
await withInterrupt((signal) => runAndCheckpointTurn(session, raw, question, signal, persistSession, drainQueuedMessages));
|
|
18696
|
+
await withInterrupt((signal) => runAndCheckpointTurn(session, raw, question, signal, persistSession, drainQueuedMessages, () => reader.composing, autonomousRounds));
|
|
17379
18697
|
}
|
|
17380
18698
|
} finally {
|
|
18699
|
+
clearInterval(footerTimer);
|
|
18700
|
+
slashCompletion?.cleanup();
|
|
17381
18701
|
process.off("SIGTERM", terminate);
|
|
18702
|
+
if (stdin5.isTTY) stdin5.off("keypress", onKeypress);
|
|
17382
18703
|
setAskUserRuntime(void 0);
|
|
17383
18704
|
rl.close();
|
|
17384
|
-
|
|
17385
|
-
|
|
18705
|
+
try {
|
|
18706
|
+
await saveTranscript(session, writeCheckpoint);
|
|
18707
|
+
} finally {
|
|
18708
|
+
mcpRuntime?.close();
|
|
18709
|
+
layout?.close();
|
|
18710
|
+
}
|
|
17386
18711
|
}
|
|
17387
18712
|
console.log(chalk11.gray("Goodbye."));
|
|
17388
18713
|
}
|
|
17389
|
-
async function runTurn(session, input, question, signal, drainQueuedMessages, persist) {
|
|
18714
|
+
async function runTurn(session, input, question, signal, drainQueuedMessages, persist, isComposing) {
|
|
17390
18715
|
const spinner = new Spinner();
|
|
17391
|
-
const
|
|
18716
|
+
const quiet = isGhostEnabled(session);
|
|
18717
|
+
const detachProgress = quiet ? () => {
|
|
18718
|
+
} : attachProgressUi();
|
|
17392
18719
|
let deltaChars = 0;
|
|
17393
18720
|
try {
|
|
17394
18721
|
await session.ask(input, {
|
|
@@ -17404,11 +18731,11 @@ async function runTurn(session, input, question, signal, drainQueuedMessages, pe
|
|
|
17404
18731
|
},
|
|
17405
18732
|
onModelStart: () => {
|
|
17406
18733
|
deltaChars = 0;
|
|
17407
|
-
spinner.start();
|
|
18734
|
+
if (!quiet) spinner.start(isComposing);
|
|
17408
18735
|
},
|
|
17409
18736
|
onModelDelta: (text) => {
|
|
17410
18737
|
deltaChars += text.length;
|
|
17411
|
-
spinner.update(`thinking (${deltaChars} chars)`);
|
|
18738
|
+
if (!quiet) spinner.update(`thinking (${deltaChars} chars)`);
|
|
17412
18739
|
},
|
|
17413
18740
|
onModelSettled: () => spinner.stop(),
|
|
17414
18741
|
onAssistantMessage: (message) => {
|
|
@@ -17418,14 +18745,14 @@ ${message.trim()}`);
|
|
|
17418
18745
|
}
|
|
17419
18746
|
},
|
|
17420
18747
|
onToolStart: (call) => {
|
|
17421
|
-
console.log(chalk11.dim(` ${toolActivityLabel(call)}`));
|
|
18748
|
+
if (!quiet) console.log(chalk11.dim(` ${toolActivityLabel(call)}`));
|
|
17422
18749
|
},
|
|
17423
18750
|
onToolResult: (_call, result) => printToolResult2(result),
|
|
17424
18751
|
...drainQueuedMessages ? {
|
|
17425
18752
|
drainQueuedMessages,
|
|
17426
|
-
onQueuedMessage: () => {
|
|
18753
|
+
onQueuedMessage: (message) => {
|
|
17427
18754
|
spinner.stop();
|
|
17428
|
-
console.log(chalk11.dim(
|
|
18755
|
+
console.log(chalk11.dim(` Queued for after the current tool: ${message}`));
|
|
17429
18756
|
}
|
|
17430
18757
|
} : {},
|
|
17431
18758
|
onToolDenied: (call, permission) => {
|
|
@@ -17470,9 +18797,26 @@ ${message.trim()}`);
|
|
|
17470
18797
|
detachProgress();
|
|
17471
18798
|
}
|
|
17472
18799
|
}
|
|
17473
|
-
async function runAndCheckpointTurn(session, input, question, signal, persist, drainQueuedMessages) {
|
|
18800
|
+
async function runAndCheckpointTurn(session, input, question, signal, persist, drainQueuedMessages, isComposing, maxRounds = 1) {
|
|
17474
18801
|
try {
|
|
17475
|
-
return await runTurn(session, input, question, signal, drainQueuedMessages, persist);
|
|
18802
|
+
if (maxRounds === 1) return await runTurn(session, input, question, signal, drainQueuedMessages, persist, isComposing);
|
|
18803
|
+
let successful = true;
|
|
18804
|
+
const result = await runAutonomousLoop({
|
|
18805
|
+
maxRounds,
|
|
18806
|
+
signal,
|
|
18807
|
+
classifyFailure: () => ({ kind: "unknown", retryable: false, failoverable: false, reason: "Interactive provider retries already exhausted." }),
|
|
18808
|
+
runRound: async (round, _previous, roundSignal) => {
|
|
18809
|
+
successful = await runTurn(session, round === 1 ? input : AUTONOMOUS_CONTINUE_TASK, question, roundSignal, drainQueuedMessages, persist, isComposing);
|
|
18810
|
+
await persist(session);
|
|
18811
|
+
const snapshot = session.toSession();
|
|
18812
|
+
return successful ? snapshot : { ...snapshot, stopReason: roundSignal.aborted ? "aborted" : "error" };
|
|
18813
|
+
},
|
|
18814
|
+
onRoundEnd: (round, _snapshot, willResume) => {
|
|
18815
|
+
if (willResume) console.log(chalk11.gray(`Continuing unfinished work: round ${round + 1}/${maxRounds}.`));
|
|
18816
|
+
}
|
|
18817
|
+
});
|
|
18818
|
+
if (result.haltReason !== "completed" && result.haltReason !== "aborted") console.log(chalk11.gray(`Automatic continuation stopped: ${result.haltReason}. Session preserved.`));
|
|
18819
|
+
return successful;
|
|
17476
18820
|
} finally {
|
|
17477
18821
|
await persist(session);
|
|
17478
18822
|
}
|
|
@@ -17539,11 +18883,56 @@ function slashCommandChangesSession(raw) {
|
|
|
17539
18883
|
function errorMessage2(error) {
|
|
17540
18884
|
return error instanceof Error ? error.message : String(error);
|
|
17541
18885
|
}
|
|
17542
|
-
async function handleSlashCommand(raw, session, providerName, workspace, onClear, runPrompt, signal, resume) {
|
|
18886
|
+
async function handleSlashCommand(raw, session, providerName, workspace, onClear, runPrompt, signal, resume, services) {
|
|
17543
18887
|
const [name, ...rest] = raw.slice(1).split(/\s+/);
|
|
17544
18888
|
const command = (name ?? "").toLowerCase();
|
|
17545
18889
|
const argument = rest.join(" ").trim();
|
|
17546
18890
|
switch (command) {
|
|
18891
|
+
case "doctor":
|
|
18892
|
+
await runDoctor(workspace, false, false);
|
|
18893
|
+
return "ok";
|
|
18894
|
+
case "mcp": {
|
|
18895
|
+
const statuses = services?.mcpStatus?.() ?? [];
|
|
18896
|
+
console.log(statuses.length ? renderMcpStatuses(statuses) : "No connected MCP servers. Configure with ultrac mcp add, approve with ultrac trust, then restart the session.");
|
|
18897
|
+
return "ok";
|
|
18898
|
+
}
|
|
18899
|
+
case "autonomous": {
|
|
18900
|
+
const setting = argument || "status";
|
|
18901
|
+
if (setting !== "on" && setting !== "off" && setting !== "status") {
|
|
18902
|
+
console.log("Usage: /autonomous on|off|status");
|
|
18903
|
+
return "ok";
|
|
18904
|
+
}
|
|
18905
|
+
if (!services?.autonomous) {
|
|
18906
|
+
console.log("Automatic continuation is available in the interactive session.");
|
|
18907
|
+
return "ok";
|
|
18908
|
+
}
|
|
18909
|
+
const rounds = services.autonomous(setting);
|
|
18910
|
+
console.log(`Automatic continuation ${rounds > 1 ? "on" : "off"}: up to ${rounds} round(s), with checkpoint, elapsed and stall limits.`);
|
|
18911
|
+
return "ok";
|
|
18912
|
+
}
|
|
18913
|
+
case "continue": {
|
|
18914
|
+
if (!runPrompt || !hasSavedSessionContent(session.toSession())) {
|
|
18915
|
+
console.log("No current task to continue. Start a task or use /resume <id>.");
|
|
18916
|
+
return "ok";
|
|
18917
|
+
}
|
|
18918
|
+
await runPrompt(`${AUTONOMOUS_CONTINUE_TASK}${argument ? ` Focus: ${argument}` : ""}`);
|
|
18919
|
+
return "ok";
|
|
18920
|
+
}
|
|
18921
|
+
case "logs": {
|
|
18922
|
+
console.log(currentDiagnosticLogPath() ?? "Diagnostic logging has not been configured for this process.");
|
|
18923
|
+
return "ok";
|
|
18924
|
+
}
|
|
18925
|
+
case "ghost": {
|
|
18926
|
+
if (argument && !["on", "off", "status"].includes(argument.toLowerCase())) {
|
|
18927
|
+
console.log("Usage: /ghost [on|off|status]");
|
|
18928
|
+
return "ok";
|
|
18929
|
+
}
|
|
18930
|
+
if (argument.toLowerCase() !== "status") {
|
|
18931
|
+
setGhostEnabled(session, argument ? argument.toLowerCase() === "on" : !isGhostEnabled(session));
|
|
18932
|
+
}
|
|
18933
|
+
console.log(`Ghost mode: ${isGhostEnabled(session) ? "on" : "off"}. Quiet activity output; private diagnostics remain enabled. Network routing and permissions are unchanged.`);
|
|
18934
|
+
return "ok";
|
|
18935
|
+
}
|
|
17547
18936
|
case "help": {
|
|
17548
18937
|
console.log(HELP);
|
|
17549
18938
|
const index = await listSkillCommands(workspace);
|
|
@@ -17611,7 +19000,7 @@ ${extra}`);
|
|
|
17611
19000
|
else {
|
|
17612
19001
|
for (const field of ["inputTokens", "outputTokens", "totalTokens"]) {
|
|
17613
19002
|
const counts = reported.flatMap((step) => typeof step.usage?.[field] === "number" ? [step.usage[field]] : []);
|
|
17614
|
-
console.log(`${field}: ${counts.length ? counts.reduce((sum,
|
|
19003
|
+
console.log(`${field}: ${counts.length ? counts.reduce((sum, count4) => sum + count4, 0) : "not reported"}`);
|
|
17615
19004
|
}
|
|
17616
19005
|
console.log(`Counts cover ${reported.length}/${steps.length} agent steps in this session. Compaction calls and ancestor sessions are excluded. Card charges are not inferred from tokens.`);
|
|
17617
19006
|
}
|
|
@@ -17721,6 +19110,27 @@ ${extra}`);
|
|
|
17721
19110
|
}
|
|
17722
19111
|
return "ok";
|
|
17723
19112
|
}
|
|
19113
|
+
case "output": {
|
|
19114
|
+
const results = session.toSession().steps.flatMap((step) => step.toolResults);
|
|
19115
|
+
if (!results.length) {
|
|
19116
|
+
console.log("No saved tool output in this session.");
|
|
19117
|
+
return "ok";
|
|
19118
|
+
}
|
|
19119
|
+
if (argument === "list") {
|
|
19120
|
+
for (const [index, entry2] of results.entries()) console.log(`${index + 1}. ${toolActivityLabel(entry2.call)} (${entry2.result.ok ? "ok" : "failed"})`);
|
|
19121
|
+
return "ok";
|
|
19122
|
+
}
|
|
19123
|
+
const selected = argument ? Number(argument) : results.length;
|
|
19124
|
+
if (!Number.isSafeInteger(selected) || selected < 1 || selected > results.length) {
|
|
19125
|
+
console.log(`Usage: /output [1-${results.length}|list]`);
|
|
19126
|
+
return "ok";
|
|
19127
|
+
}
|
|
19128
|
+
const entry = results[selected - 1];
|
|
19129
|
+
console.log(`${selected}. ${toolActivityLabel(entry.call)} (${entry.result.ok ? "ok" : "failed"})`);
|
|
19130
|
+
console.log(entry.result.content);
|
|
19131
|
+
for (const warning of getMcpArchiveWarnings(entry.result)) console.log(chalk11.yellow(`Warning: ${warning}`));
|
|
19132
|
+
return "ok";
|
|
19133
|
+
}
|
|
17724
19134
|
case "tools": {
|
|
17725
19135
|
const names = session.advertisedToolNames;
|
|
17726
19136
|
console.log(`Advertised to the model this turn (${names.length}): ${names.join(", ")}`);
|
|
@@ -17742,6 +19152,20 @@ ${extra}`);
|
|
|
17742
19152
|
}
|
|
17743
19153
|
return "ok";
|
|
17744
19154
|
}
|
|
19155
|
+
case "skill": {
|
|
19156
|
+
const [skillName, ...args] = argument.split(/\s+/);
|
|
19157
|
+
if (!skillName || !runPrompt) {
|
|
19158
|
+
console.log("Usage: /skill <name> [args]");
|
|
19159
|
+
return "ok";
|
|
19160
|
+
}
|
|
19161
|
+
const skill = await readSkill(workspace, skillName);
|
|
19162
|
+
if (!skill) {
|
|
19163
|
+
console.log(`No available skill named ${skillName}. Use /skills to list them.`);
|
|
19164
|
+
return "ok";
|
|
19165
|
+
}
|
|
19166
|
+
await runPrompt(buildSkillCommandPrompt(skill, { command: "skill", skillName: skill.name, description: skill.description, scope: skill.scope, path: skill.path }, args.join(" ")));
|
|
19167
|
+
return "ok";
|
|
19168
|
+
}
|
|
17745
19169
|
case "skills": {
|
|
17746
19170
|
const skills = await listSkills(workspace);
|
|
17747
19171
|
if (!skills.length) {
|
|
@@ -17774,13 +19198,14 @@ ${extra}`);
|
|
|
17774
19198
|
return "unhandled";
|
|
17775
19199
|
}
|
|
17776
19200
|
}
|
|
17777
|
-
async function runSkillCommand(session, raw, workspace, question) {
|
|
19201
|
+
async function runSkillCommand(session, raw, workspace, question, runPrompt) {
|
|
17778
19202
|
const resolved = await resolveSkillCommand(workspace, raw);
|
|
17779
19203
|
if (resolved.status === "ok") {
|
|
17780
19204
|
console.log(
|
|
17781
19205
|
chalk11.gray(` /${resolved.command.command} \u2192 skill ${resolved.command.skillName} (${resolved.command.scope})`)
|
|
17782
19206
|
);
|
|
17783
|
-
await
|
|
19207
|
+
if (runPrompt) await runPrompt(resolved.prompt);
|
|
19208
|
+
else await runTurn(session, resolved.prompt, question);
|
|
17784
19209
|
return;
|
|
17785
19210
|
}
|
|
17786
19211
|
if (resolved.status === "conflict") {
|
|
@@ -17801,10 +19226,8 @@ async function runSkillCommand(session, raw, workspace, question) {
|
|
|
17801
19226
|
console.log(chalk11.red("Unknown command. Try /help."));
|
|
17802
19227
|
}
|
|
17803
19228
|
var LineReader = class {
|
|
17804
|
-
queue = [];
|
|
17805
|
-
waiters = [];
|
|
17806
|
-
closed = false;
|
|
17807
19229
|
constructor(rl) {
|
|
19230
|
+
this.rl = rl;
|
|
17808
19231
|
rl.on("line", (line) => {
|
|
17809
19232
|
const waiter = this.waiters.shift();
|
|
17810
19233
|
if (waiter) {
|
|
@@ -17820,6 +19243,10 @@ var LineReader = class {
|
|
|
17820
19243
|
}
|
|
17821
19244
|
});
|
|
17822
19245
|
}
|
|
19246
|
+
rl;
|
|
19247
|
+
queue = [];
|
|
19248
|
+
waiters = [];
|
|
19249
|
+
closed = false;
|
|
17823
19250
|
next(signal) {
|
|
17824
19251
|
return this.waitForLine(signal, true);
|
|
17825
19252
|
}
|
|
@@ -17830,6 +19257,10 @@ var LineReader = class {
|
|
|
17830
19257
|
nextFresh(signal) {
|
|
17831
19258
|
return this.waitForLine(signal, false);
|
|
17832
19259
|
}
|
|
19260
|
+
/** True while the user is typing a line that has not been submitted yet. */
|
|
19261
|
+
get composing() {
|
|
19262
|
+
return this.rl.line.length > 0;
|
|
19263
|
+
}
|
|
17833
19264
|
/**
|
|
17834
19265
|
* Drain ordinary messages in arrival order. A slash command is a control
|
|
17835
19266
|
* boundary and remains queued for the main REPL loop along with everything
|
|
@@ -17875,11 +19306,15 @@ var LineReader = class {
|
|
|
17875
19306
|
waiter.resolve(line);
|
|
17876
19307
|
}
|
|
17877
19308
|
};
|
|
17878
|
-
function
|
|
19309
|
+
function handleReplEscape(activeTurn) {
|
|
17879
19310
|
if (activeTurn && !activeTurn.signal.aborted) {
|
|
17880
19311
|
activeTurn.abort();
|
|
17881
19312
|
return "abort-turn";
|
|
17882
19313
|
}
|
|
19314
|
+
return "idle";
|
|
19315
|
+
}
|
|
19316
|
+
function handleReplExit(activeTurn) {
|
|
19317
|
+
activeTurn?.abort();
|
|
17883
19318
|
return "exit-session";
|
|
17884
19319
|
}
|
|
17885
19320
|
function printBanner(version, providerName, model, mode, workspace, protocol, budget, diagnosticPath2) {
|
|
@@ -17944,29 +19379,6 @@ function indent3(value) {
|
|
|
17944
19379
|
// src/commands/run.ts
|
|
17945
19380
|
import { basename as basename3 } from "path";
|
|
17946
19381
|
import chalk12 from "chalk";
|
|
17947
|
-
|
|
17948
|
-
// src/core/autonomous.ts
|
|
17949
|
-
var AUTONOMOUS_CONTINUE_TASK = "Continue the task from where the previous round stopped. Check what is already done, finish the rest, and verify it.";
|
|
17950
|
-
var DEFAULT_AUTONOMOUS_MAX_ROUNDS = 5;
|
|
17951
|
-
var AUTONOMOUS_MAX_STEPS = 200;
|
|
17952
|
-
function shouldResume(stopReason) {
|
|
17953
|
-
return stopReason === "max-steps" || stopReason === "error";
|
|
17954
|
-
}
|
|
17955
|
-
async function runAutonomousLoop(options) {
|
|
17956
|
-
if (!Number.isInteger(options.maxRounds) || options.maxRounds < 1) {
|
|
17957
|
-
throw new Error("maxRounds must be a positive integer.");
|
|
17958
|
-
}
|
|
17959
|
-
let previous;
|
|
17960
|
-
for (let round = 1; ; round += 1) {
|
|
17961
|
-
const session = await options.runRound(round, previous);
|
|
17962
|
-
const willResume = shouldResume(session.stopReason) && round < options.maxRounds && !options.signal?.aborted;
|
|
17963
|
-
await options.onRoundEnd?.(round, session, willResume);
|
|
17964
|
-
if (!willResume) return { session, rounds: round };
|
|
17965
|
-
previous = session;
|
|
17966
|
-
}
|
|
17967
|
-
}
|
|
17968
|
-
|
|
17969
|
-
// src/commands/run.ts
|
|
17970
19382
|
function parsePositiveInt(value, flag) {
|
|
17971
19383
|
const parsed = Number.parseInt(value, 10);
|
|
17972
19384
|
if (!Number.isInteger(parsed) || parsed < 1 || String(parsed) !== value.trim()) {
|
|
@@ -17975,7 +19387,6 @@ function parsePositiveInt(value, flag) {
|
|
|
17975
19387
|
return parsed;
|
|
17976
19388
|
}
|
|
17977
19389
|
var EXIT_INCOMPLETE_RUN = 2;
|
|
17978
|
-
var AUTONOMOUS_ERROR_BACKOFF_MS = 15e3;
|
|
17979
19390
|
var NOTIFY_SUMMARY_CHARS = 1200;
|
|
17980
19391
|
function registerRunCommand(program2) {
|
|
17981
19392
|
program2.command("run").alias("chat").description("Run a one-shot coding agent task").argument("<task...>", "Task for the agent").option("-w, --workspace <path>", "Workspace path", ".").option("--provider <name>", "Provider: ultrac (default) or echo (offline test stub)").option("--model <model>", "Model name").option("--base-url <url>", "Ultrac API base URL (overrides ULTRAC_BASE_URL and the stored login)").option("--mode <name>", `Agent mode: ${agentModeNames.join(", ")}`).option("--max-steps <number>", "Maximum agent steps").option("--no-native-tools", "Use the legacy JSON tool protocol").option("--allow-write", "Allow file writes", false).option("--allow-shell", "Grant the model broad, unsandboxed shell access", false).option("--allow-network", "Allow HTTP(S) URL fetches", false).option("--continue <id>", "Continue from a saved session id").option("--tools <list>", "Tools to advertise: `all`, or a comma-separated list. Default: the mode's set").option("--context-tokens <number>", "Model context window in tokens (default 262144)").option("--max-tool-result-chars <number>", "Cap on one tool result in the transcript (default 6000)").option(
|
|
@@ -17986,7 +19397,7 @@ function registerRunCommand(program2) {
|
|
|
17986
19397
|
"--autonomous",
|
|
17987
19398
|
`Run with no human in the loop: write, shell and network are granted, each round may take up to ${AUTONOMOUS_MAX_STEPS} steps, and a round that runs out of steps or fails resumes on its own. With a phone channel configured (\`ultrac notify setup\`), questions go to your phone and you are told when it finishes.`,
|
|
17988
19399
|
false
|
|
17989
|
-
).option("--max-rounds <number>", `With --autonomous: how many times a run may resume itself (default ${DEFAULT_AUTONOMOUS_MAX_ROUNDS})`).option("--notify", "Send ask_user questions and the final result to your phone (see `ultrac notify setup`)", false).option("--json", "Print the session JSON", false).action(async (taskParts, options) => {
|
|
19400
|
+
).option("--max-rounds <number>", `With --autonomous: how many times a run may resume itself (default ${DEFAULT_AUTONOMOUS_MAX_ROUNDS})`).option("--notify", "Send ask_user questions and the final result to your phone (see `ultrac notify setup`)", false).option("--verbose-tools", "Print complete tool results instead of previews", false).option("--json", "Print the session JSON", false).action(async (taskParts, options) => {
|
|
17990
19401
|
const task = taskParts.join(" ").trim();
|
|
17991
19402
|
if (!task) {
|
|
17992
19403
|
throw new Error("A task is required.");
|
|
@@ -18100,14 +19511,17 @@ function registerRunCommand(program2) {
|
|
|
18100
19511
|
shell: options.allowShell || config.permissions.shell,
|
|
18101
19512
|
network: options.allowNetwork || config.permissions.network
|
|
18102
19513
|
};
|
|
19514
|
+
const roundFailures = /* @__PURE__ */ new Map();
|
|
18103
19515
|
let result;
|
|
18104
19516
|
try {
|
|
18105
19517
|
result = await runAutonomousLoop({
|
|
18106
19518
|
maxRounds,
|
|
18107
19519
|
signal: controller.signal,
|
|
18108
|
-
|
|
19520
|
+
classifyFailure: (session) => roundFailures.get(session.id),
|
|
19521
|
+
runRound: async (round, previous, roundSignal) => {
|
|
18109
19522
|
const lineage = previous ? await loadSessionLineage(workspace, previous.id) : previousSessionLineage;
|
|
18110
19523
|
let lastCheckpoint;
|
|
19524
|
+
let checkpointFailed = false;
|
|
18111
19525
|
try {
|
|
18112
19526
|
const { session } = await runner.run({
|
|
18113
19527
|
task: previous ? AUTONOMOUS_CONTINUE_TASK : task,
|
|
@@ -18121,22 +19535,30 @@ function registerRunCommand(program2) {
|
|
|
18121
19535
|
previousSessionLineage: lineage,
|
|
18122
19536
|
...toolSelection ? { tools: toolSelection } : {},
|
|
18123
19537
|
permissions,
|
|
18124
|
-
signal:
|
|
19538
|
+
signal: roundSignal,
|
|
18125
19539
|
...autonomous ? { autonomous: { phone: Boolean(notifier) } } : {},
|
|
18126
19540
|
// Write the session after every step, atomically, so a run that dies
|
|
18127
19541
|
// partway - a crash, a kill, a closed laptop mid-scan - is on disk up to
|
|
18128
19542
|
// its last completed step and can be resumed with --continue rather than
|
|
18129
19543
|
// restarted from nothing.
|
|
18130
19544
|
onCheckpoint: async (session2) => {
|
|
18131
|
-
|
|
18132
|
-
|
|
19545
|
+
try {
|
|
19546
|
+
const checkpointPath = await writeCheckpoint(session2);
|
|
19547
|
+
checkpointFailed = !checkpointPath;
|
|
19548
|
+
if (checkpointPath) lastCheckpoint = await loadSession(workspace, session2.id);
|
|
19549
|
+
} catch (error) {
|
|
19550
|
+
checkpointFailed = true;
|
|
19551
|
+
throw error;
|
|
19552
|
+
}
|
|
18133
19553
|
}
|
|
18134
19554
|
});
|
|
18135
19555
|
return session;
|
|
18136
19556
|
} catch (error) {
|
|
18137
|
-
if (!autonomous || !lastCheckpoint ||
|
|
19557
|
+
if (!autonomous || !lastCheckpoint || checkpointFailed || roundSignal.aborted) throw error;
|
|
19558
|
+
const classification = classifyProviderError(error);
|
|
19559
|
+
roundFailures.set(lastCheckpoint.id, classification);
|
|
18138
19560
|
console.error(
|
|
18139
|
-
chalk12.yellow(`Round ${round} failed
|
|
19561
|
+
chalk12.yellow(`Round ${round} failed (${classification.kind}). The latest checkpoint is saved.`)
|
|
18140
19562
|
);
|
|
18141
19563
|
return { ...lastCheckpoint, stopReason: "error" };
|
|
18142
19564
|
}
|
|
@@ -18149,7 +19571,6 @@ function registerRunCommand(program2) {
|
|
|
18149
19571
|
`Round ${round} stopped (${session.stopReason}); resuming in round ${round + 1} of ${maxRounds}.`
|
|
18150
19572
|
)
|
|
18151
19573
|
);
|
|
18152
|
-
if (session.stopReason === "error") await sleep(AUTONOMOUS_ERROR_BACKOFF_MS, controller.signal);
|
|
18153
19574
|
}
|
|
18154
19575
|
});
|
|
18155
19576
|
} catch (error) {
|
|
@@ -18170,16 +19591,19 @@ function registerRunCommand(program2) {
|
|
|
18170
19591
|
{ cause: error }
|
|
18171
19592
|
);
|
|
18172
19593
|
}
|
|
19594
|
+
if (["stalled", "elapsed_budget", "permanent_error"].includes(result.haltReason ?? "")) {
|
|
19595
|
+
console.error(chalk12.yellow(`Autonomous recovery stopped: ${result.haltReason}. The latest checkpoint is saved.`));
|
|
19596
|
+
}
|
|
18173
19597
|
const stopReason = result.session.stopReason;
|
|
18174
19598
|
for (const warning of getSessionMcpArchiveWarnings(result.session)) {
|
|
18175
19599
|
console.error(chalk12.yellow(`Warning: ${warning}`));
|
|
18176
19600
|
}
|
|
18177
19601
|
if (options.json) {
|
|
18178
19602
|
console.log(
|
|
18179
|
-
JSON.stringify({ sessionPath: sessionPath2, stopReason: stopReason ?? null, session: result.session }, null, 2)
|
|
19603
|
+
JSON.stringify({ sessionPath: sessionPath2, stopReason: stopReason ?? null, haltReason: result.haltReason ?? null, rounds: result.rounds, session: result.session }, null, 2)
|
|
18180
19604
|
);
|
|
18181
19605
|
} else {
|
|
18182
|
-
printSession(result.session, sessionPath2);
|
|
19606
|
+
printSession(result.session, sessionPath2, { verboseTools: options.verboseTools });
|
|
18183
19607
|
}
|
|
18184
19608
|
if (notifier) {
|
|
18185
19609
|
const outcome = stopReason === "completed" ? "finished" : `stopped without finishing (${stopReason ?? "unknown"})`;
|
|
@@ -18433,17 +19857,25 @@ function registerSkillsCommand(program2) {
|
|
|
18433
19857
|
console.log("");
|
|
18434
19858
|
console.log(skill.body);
|
|
18435
19859
|
});
|
|
18436
|
-
skills.command("create").description("Scaffold a new SKILL.md").argument("<name>", "Skill name (kebab-case)").requiredOption("--description <text>", "One-line description").option("--when <text>", "When the agent should use this skill").option("--permissions <list>", "Comma-separated: write,shell,network").option("--scope <scope>", "global or project", "project").option("-w, --workspace <path>", "Workspace path", ".").action(
|
|
19860
|
+
skills.command("create").description("Scaffold a new SKILL.md").argument("<name>", "Skill name (kebab-case)").requiredOption("--description <text>", "One-line description").option("--when <text>", "When the agent should use this skill").option("--permissions <list>", "Comma-separated: write,shell,network").option("--scope <scope>", "global or project", "project").option("--command <name>", "Expose this skill as a custom slash command").option("--argument-hint <text>", "Describe the slash command arguments").option("-w, --workspace <path>", "Workspace path", ".").action(
|
|
18437
19861
|
async (name, options) => {
|
|
19862
|
+
const command = options.command ? normalizeCommandName(options.command) : void 0;
|
|
19863
|
+
if (command && isBuiltinSlashCommand(command)) throw new Error(`/${command} is a built-in command. Choose another skill command name.`);
|
|
19864
|
+
if (options.argumentHint && !command) throw new Error("--argument-hint requires --command.");
|
|
19865
|
+
const argumentHint = options.argumentHint?.trim();
|
|
19866
|
+
if (argumentHint && (argumentHint.length > 256 || /[\r\n]/.test(argumentHint))) throw new Error("Argument hints must be one line, at most 256 characters.");
|
|
18438
19867
|
const skill = await createSkill(resolve13(options.workspace), {
|
|
18439
19868
|
name,
|
|
18440
19869
|
description: options.description,
|
|
18441
19870
|
whenToUse: options.when,
|
|
18442
19871
|
permissions: parsePermissions(options.permissions),
|
|
18443
|
-
scope: parseScope2(options.scope)
|
|
19872
|
+
scope: parseScope2(options.scope),
|
|
19873
|
+
command,
|
|
19874
|
+
argumentHint
|
|
18444
19875
|
});
|
|
18445
19876
|
console.log(`Created ${skill.scope} skill "${skill.name}" at ${skill.path}`);
|
|
18446
19877
|
console.log("Edit that file to add the step-by-step instructions.");
|
|
19878
|
+
if (command) console.log(`Invoke this skill with /${command}${argumentHint ? ` ${argumentHint}` : ""} in a trusted workspace.`);
|
|
18447
19879
|
}
|
|
18448
19880
|
);
|
|
18449
19881
|
skills.command("path").description("Print the skills directory path for a scope").option("-w, --workspace <path>", "Workspace path", ".").option("--scope <scope>", "global or project", "project").action((options) => {
|
|
@@ -19188,8 +20620,8 @@ Leaving run ${runId} as it is.
|
|
|
19188
20620
|
case "succeeded":
|
|
19189
20621
|
finishHuman(result.text);
|
|
19190
20622
|
if (result.toolCalls.length && !result.text) {
|
|
19191
|
-
const
|
|
19192
|
-
r.err(chalk13.gray(`Done with ${
|
|
20623
|
+
const count4 = result.toolCalls.length;
|
|
20624
|
+
r.err(chalk13.gray(`Done with ${count4} tool call${count4 === 1 ? "" : "s"} and no text.
|
|
19193
20625
|
`));
|
|
19194
20626
|
}
|
|
19195
20627
|
return result.outputSource === "incomplete" ? EXIT_FAILED : EXIT_DONE;
|
|
@@ -19384,7 +20816,7 @@ ${tools.length} of ${all.length} tools advertised in ${options.mode} mode, ${ser
|
|
|
19384
20816
|
);
|
|
19385
20817
|
}
|
|
19386
20818
|
});
|
|
19387
|
-
toolsCommand.command("run").description("Execute one built-in agent tool").argument("<tool>", "Tool name").argument("[input]", "JSON object input", "{}").option("-w, --workspace <path>", "Workspace path", ".").option("--allow-write", "Allow file writes", false).option("--allow-shell", "Allow this tool to execute unsandboxed shell commands", false).option("--allow-network", "Allow HTTP(S) URL fetches", false).option("--result-json", "Print the tool result as JSON", false).action(async (tool, inputText, options) => {
|
|
20819
|
+
toolsCommand.command("run").description("Execute one built-in agent tool").argument("<tool>", "Tool name").argument("[input]", "JSON object input", "{}").option("-w, --workspace <path>", "Workspace path", ".").option("--allow-write", "Allow file writes", false).option("--allow-shell", "Allow this tool to execute unsandboxed shell commands", false).option("--allow-network", "Allow HTTP(S) URL fetches", false).option("--verbose-tools", "Print the complete tool result", false).option("--result-json", "Print the tool result as JSON", false).action(async (tool, inputText, options) => {
|
|
19388
20820
|
const input = parseToolInput(inputText);
|
|
19389
20821
|
const result = await createDefaultToolRegistry().execute(
|
|
19390
20822
|
{ tool, input },
|
|
@@ -19400,7 +20832,7 @@ ${tools.length} of ${all.length} tools advertised in ${options.mode} mode, ${ser
|
|
|
19400
20832
|
if (options.resultJson) {
|
|
19401
20833
|
console.log(JSON.stringify({ result }, null, 2));
|
|
19402
20834
|
} else {
|
|
19403
|
-
printToolResult(result);
|
|
20835
|
+
printToolResult(result, { verboseTools: options.verboseTools });
|
|
19404
20836
|
}
|
|
19405
20837
|
if (!result.ok) {
|
|
19406
20838
|
process.exitCode = 1;
|