@spinalfluid/ultrac 0.1.9 → 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 +16 -0
- package/dist/cli.js +1374 -507
- 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",
|
|
@@ -1222,20 +1223,20 @@ function compactRun(input, options = {}) {
|
|
|
1222
1223
|
}
|
|
1223
1224
|
async function compactShape(shape, options) {
|
|
1224
1225
|
const reserveChars = options.summarise ? Math.max(0, options.reserveChars ?? shape.reserveChars ?? 0) : 0;
|
|
1225
|
-
const
|
|
1226
|
+
const plain2 = planKeepingSummaries(shape, shape.messages, reserveChars);
|
|
1226
1227
|
const skip = (reason) => ({
|
|
1227
|
-
...reserveChars > 0 ? planKeepingSummaries(shape, shape.messages, shape.reserveChars ?? 0) :
|
|
1228
|
+
...reserveChars > 0 ? planKeepingSummaries(shape, shape.messages, shape.reserveChars ?? 0) : plain2,
|
|
1228
1229
|
compacted: false,
|
|
1229
1230
|
summarisedMessages: 0,
|
|
1230
1231
|
summarisedTurns: 0,
|
|
1231
1232
|
skipped: reason
|
|
1232
1233
|
});
|
|
1233
|
-
if (
|
|
1234
|
+
if (plain2.dropped.length === 0) return skip("nothing-dropped");
|
|
1234
1235
|
if (!options.summarise) return skip("no-summariser");
|
|
1235
|
-
const source =
|
|
1236
|
+
const source = plain2.dropped.filter((message2) => !isCompactionSummary(message2));
|
|
1236
1237
|
if (source.length === 0) return skip("only-summaries");
|
|
1237
1238
|
const ceiling = options.maxSummaryChars ?? DEFAULT_MAX_SUMMARY_CHARS;
|
|
1238
|
-
const slack =
|
|
1239
|
+
const slack = plain2.usage.promptLimitChars - plain2.usage.totalChars - summaryEnvelopeChars();
|
|
1239
1240
|
const maxChars = Math.min(ceiling, slack);
|
|
1240
1241
|
if (maxChars < MIN_SUMMARY_CHARS) return skip("no-room");
|
|
1241
1242
|
const transcript = renderDroppedTranscript(source, {
|
|
@@ -1260,21 +1261,21 @@ async function compactShape(shape, options) {
|
|
|
1260
1261
|
const text = truncateToLine(summary.trim(), maxChars);
|
|
1261
1262
|
if (!text) return skip("summariser-empty");
|
|
1262
1263
|
const message = compactionSummaryMessage(text);
|
|
1263
|
-
const plan = replan(shape,
|
|
1264
|
+
const plan = replan(shape, plain2, message);
|
|
1264
1265
|
if (!plan) return skip("would-drop-more");
|
|
1265
1266
|
return {
|
|
1266
1267
|
...plan,
|
|
1267
1268
|
// The re-plan saw a transcript the span was already out of, so it has
|
|
1268
1269
|
// nothing to report about it. What was dropped is what `plain` dropped.
|
|
1269
|
-
dropped:
|
|
1270
|
-
droppedMessages:
|
|
1271
|
-
droppedTurns:
|
|
1272
|
-
droppedChars:
|
|
1270
|
+
dropped: plain2.dropped,
|
|
1271
|
+
droppedMessages: plain2.droppedMessages,
|
|
1272
|
+
droppedTurns: plain2.droppedTurns,
|
|
1273
|
+
droppedChars: plain2.droppedChars,
|
|
1273
1274
|
compacted: true,
|
|
1274
1275
|
summary: text,
|
|
1275
|
-
summarisedMessages:
|
|
1276
|
-
summarisedTurns:
|
|
1277
|
-
warning: compactionWarning(plan,
|
|
1276
|
+
summarisedMessages: plain2.dropped.length,
|
|
1277
|
+
summarisedTurns: plain2.droppedTurns,
|
|
1278
|
+
warning: compactionWarning(plan, plain2)
|
|
1278
1279
|
};
|
|
1279
1280
|
}
|
|
1280
1281
|
function planKeepingSummaries(shape, messages, reserveChars) {
|
|
@@ -1290,8 +1291,8 @@ function planKeepingSummaries(shape, messages, reserveChars) {
|
|
|
1290
1291
|
return shape.plan(messages, callerPinned, reserveChars);
|
|
1291
1292
|
}
|
|
1292
1293
|
}
|
|
1293
|
-
function replan(shape,
|
|
1294
|
-
const kept =
|
|
1294
|
+
function replan(shape, plain2, summary) {
|
|
1295
|
+
const kept = plain2.messages;
|
|
1295
1296
|
const insertAt = kept.findIndex((message) => message.role !== "system");
|
|
1296
1297
|
const at = insertAt < 0 ? kept.length : insertAt;
|
|
1297
1298
|
const next = [...kept.slice(0, at), summary, ...kept.slice(at)];
|
|
@@ -1307,9 +1308,9 @@ function replan(shape, plain, summary) {
|
|
|
1307
1308
|
if (!kept.every((message) => survived.has(message))) return void 0;
|
|
1308
1309
|
return plan;
|
|
1309
1310
|
}
|
|
1310
|
-
function compactionWarning(plan,
|
|
1311
|
-
const turns = plural2(
|
|
1312
|
-
const messages = plural2(
|
|
1311
|
+
function compactionWarning(plan, plain2) {
|
|
1312
|
+
const turns = plural2(plain2.droppedTurns, "earlier turn");
|
|
1313
|
+
const messages = plural2(plain2.dropped.length, "message");
|
|
1313
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.`;
|
|
1314
1315
|
}
|
|
1315
1316
|
function truncateToLine(text, maxChars) {
|
|
@@ -4083,7 +4084,7 @@ function runProcess(command, args, options) {
|
|
|
4083
4084
|
});
|
|
4084
4085
|
const untrack = trackProcessGroup(child.pid);
|
|
4085
4086
|
const stdout6 = createCapture(maxBytes);
|
|
4086
|
-
const
|
|
4087
|
+
const stderr2 = createCapture(maxBytes);
|
|
4087
4088
|
let settled = false;
|
|
4088
4089
|
let started = false;
|
|
4089
4090
|
let termination;
|
|
@@ -4139,7 +4140,7 @@ function runProcess(command, args, options) {
|
|
|
4139
4140
|
};
|
|
4140
4141
|
const onAbort = () => beginTermination("aborted");
|
|
4141
4142
|
child.stdout.on("data", (chunk) => stdout6.push(chunk));
|
|
4142
|
-
child.stderr.on("data", (chunk) =>
|
|
4143
|
+
child.stderr.on("data", (chunk) => stderr2.push(chunk));
|
|
4143
4144
|
child.stdout.on("error", () => {
|
|
4144
4145
|
});
|
|
4145
4146
|
child.stderr.on("error", () => {
|
|
@@ -4152,7 +4153,7 @@ function runProcess(command, args, options) {
|
|
|
4152
4153
|
if (termination === "aborted") {
|
|
4153
4154
|
resolve15({
|
|
4154
4155
|
stdout: stdout6.text(),
|
|
4155
|
-
stderr:
|
|
4156
|
+
stderr: stderr2.text(),
|
|
4156
4157
|
started,
|
|
4157
4158
|
aborted: true,
|
|
4158
4159
|
exitCode: null,
|
|
@@ -4160,7 +4161,7 @@ function runProcess(command, args, options) {
|
|
|
4160
4161
|
timedOut: false,
|
|
4161
4162
|
timeoutMs,
|
|
4162
4163
|
...stdout6.truncation() ? { stdoutTruncation: stdout6.truncation() } : {},
|
|
4163
|
-
...
|
|
4164
|
+
...stderr2.truncation() ? { stderrTruncation: stderr2.truncation() } : {},
|
|
4164
4165
|
redactedEnvKeys
|
|
4165
4166
|
});
|
|
4166
4167
|
return;
|
|
@@ -4170,11 +4171,11 @@ function runProcess(command, args, options) {
|
|
|
4170
4171
|
});
|
|
4171
4172
|
child.on("close", (exitCode, signal) => {
|
|
4172
4173
|
const stdoutTruncation = stdout6.truncation();
|
|
4173
|
-
const stderrTruncation =
|
|
4174
|
+
const stderrTruncation = stderr2.truncation();
|
|
4174
4175
|
finish(
|
|
4175
4176
|
() => resolve15({
|
|
4176
4177
|
stdout: stdout6.text(),
|
|
4177
|
-
stderr:
|
|
4178
|
+
stderr: stderr2.text(),
|
|
4178
4179
|
started,
|
|
4179
4180
|
aborted: termination === "aborted",
|
|
4180
4181
|
exitCode,
|
|
@@ -5184,7 +5185,7 @@ function topCounts(values) {
|
|
|
5184
5185
|
for (const value of values) {
|
|
5185
5186
|
counts.set(value, (counts.get(value) ?? 0) + 1);
|
|
5186
5187
|
}
|
|
5187
|
-
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));
|
|
5188
5189
|
}
|
|
5189
5190
|
async function collectGitContext(workspace, signal) {
|
|
5190
5191
|
const statusResult = await runGit(["status", "--short", "--branch"], { cwd: workspace, signal });
|
|
@@ -6969,8 +6970,8 @@ function shannonEntropy(value) {
|
|
|
6969
6970
|
counts.set(char, (counts.get(char) ?? 0) + 1);
|
|
6970
6971
|
}
|
|
6971
6972
|
let entropy = 0;
|
|
6972
|
-
for (const
|
|
6973
|
-
const probability =
|
|
6973
|
+
for (const count4 of counts.values()) {
|
|
6974
|
+
const probability = count4 / value.length;
|
|
6974
6975
|
entropy -= probability * Math.log2(probability);
|
|
6975
6976
|
}
|
|
6976
6977
|
return entropy;
|
|
@@ -8082,6 +8083,213 @@ async function loadSkillsContext(workspace) {
|
|
|
8082
8083
|
}).join("\n") + notice;
|
|
8083
8084
|
}
|
|
8084
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
|
+
|
|
8085
8293
|
// src/tools/workspace/skill-tools.ts
|
|
8086
8294
|
var SkillInput = z17.object({
|
|
8087
8295
|
name: z17.string().min(1).describe("Name of the skill to load, as shown in the skills list.")
|
|
@@ -8128,6 +8336,26 @@ var listSkillsTool = {
|
|
|
8128
8336
|
};
|
|
8129
8337
|
}
|
|
8130
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
|
+
};
|
|
8131
8359
|
|
|
8132
8360
|
// src/tools/workspace/todo-tools.ts
|
|
8133
8361
|
import { z as z18 } from "zod";
|
|
@@ -8568,7 +8796,7 @@ function summarizeDirectories(files) {
|
|
|
8568
8796
|
const directory = dirname9(file.path).split(/[\\/]/)[0] || ".";
|
|
8569
8797
|
counts.set(directory, (counts.get(directory) ?? 0) + 1);
|
|
8570
8798
|
}
|
|
8571
|
-
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));
|
|
8572
8800
|
}
|
|
8573
8801
|
function summarizeExtensions(files) {
|
|
8574
8802
|
const summaries = /* @__PURE__ */ new Map();
|
|
@@ -11351,8 +11579,8 @@ function tokenizeCommand(command) {
|
|
|
11351
11579
|
function renderCommandLine(plan) {
|
|
11352
11580
|
return [plan.command, ...plan.args].join(" ");
|
|
11353
11581
|
}
|
|
11354
|
-
function parseTestOutput(runner, stdout6,
|
|
11355
|
-
const combined = [stdout6,
|
|
11582
|
+
function parseTestOutput(runner, stdout6, stderr2, workspace) {
|
|
11583
|
+
const combined = [stdout6, stderr2].filter(Boolean).join("\n");
|
|
11356
11584
|
if (runner === "vitest" || runner === "jest") {
|
|
11357
11585
|
const report = extractJestStyleReport(stdout6) ?? extractJestStyleReport(combined);
|
|
11358
11586
|
if (report) return parseJestStyleReport(report, workspace);
|
|
@@ -12798,64 +13026,67 @@ function decodeCodePoint(value, fallback) {
|
|
|
12798
13026
|
// src/tools/workspace.ts
|
|
12799
13027
|
function createDefaultToolRegistry() {
|
|
12800
13028
|
const { todoWriteTool, todoReadTool } = createTodoTools();
|
|
12801
|
-
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);
|
|
12802
13030
|
}
|
|
12803
13031
|
|
|
12804
13032
|
// src/commands/doctor.ts
|
|
12805
13033
|
function registerDoctorCommand(program2) {
|
|
12806
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) => {
|
|
12807
|
-
|
|
12808
|
-
const workspaceReady = await stat10(workspace).then((entry) => entry.isDirectory(), () => false);
|
|
12809
|
-
const [nodeMajor = 0, nodeMinor = 0] = process.versions.node.split(".").map(Number);
|
|
12810
|
-
const nodeReady = nodeMajor > 22 || nodeMajor === 22 && nodeMinor >= 12;
|
|
12811
|
-
const config = await loadConfig(workspace).catch(() => null);
|
|
12812
|
-
const configExists = await hasConfig(workspace);
|
|
12813
|
-
const isGitRepo = await pathExists(resolve7(workspace, ".git"));
|
|
12814
|
-
const ultracCredentials = await resolveUltracCredentials();
|
|
12815
|
-
const baseUrl = ultracCredentials ? { baseUrl: ultracCredentials.baseUrl, source: ultracCredentials.baseUrlSource } : resolveBaseUrl();
|
|
12816
|
-
const tools = createDefaultToolRegistry().list();
|
|
12817
|
-
const toolSchemasReady = tools.every((tool) => tool.inputSchema && typeof tool.inputSchema === "object");
|
|
12818
|
-
const checks = [
|
|
12819
|
-
{ label: "node", value: `${process.versions.node} (requires >=22.12.0)`, ok: nodeReady },
|
|
12820
|
-
{
|
|
12821
|
-
label: "workspace",
|
|
12822
|
-
value: workspaceReady ? workspace : `${workspace} - must be an accessible directory`,
|
|
12823
|
-
ok: workspaceReady
|
|
12824
|
-
},
|
|
12825
|
-
{ label: "git repository", value: isGitRepo ? "found" : "not found", ok: isGitRepo },
|
|
12826
|
-
{
|
|
12827
|
-
label: "config",
|
|
12828
|
-
value: config ? configExists ? configPath(workspace) : "using defaults" : `unable to load configuration - check ${configPath(workspace)} and local trust settings`,
|
|
12829
|
-
ok: config !== null
|
|
12830
|
-
},
|
|
12831
|
-
{
|
|
12832
|
-
label: "tool schemas",
|
|
12833
|
-
value: `${tools.length} exported`,
|
|
12834
|
-
ok: toolSchemasReady
|
|
12835
|
-
},
|
|
12836
|
-
{ label: "provider", value: config?.provider ?? "unavailable", ok: config !== null },
|
|
12837
|
-
{ label: "model", value: config?.model ?? "unavailable", ok: config !== null },
|
|
12838
|
-
{
|
|
12839
|
-
label: "Ultrac sign-in",
|
|
12840
|
-
value: ultracCredentials ? `signed in via ${describeKeySource(ultracCredentials)}` : "not signed in - run `ultrac login` or set ULTRAC_API_KEY",
|
|
12841
|
-
ok: config?.provider !== "ultrac" || Boolean(ultracCredentials)
|
|
12842
|
-
},
|
|
12843
|
-
{
|
|
12844
|
-
label: "Ultrac API base URL",
|
|
12845
|
-
value: `${baseUrl.baseUrl} via ${describeBaseUrlSource(baseUrl.source)}`,
|
|
12846
|
-
ok: true
|
|
12847
|
-
}
|
|
12848
|
-
];
|
|
12849
|
-
if (options.json) {
|
|
12850
|
-
console.log(JSON.stringify({ ok: checks.every((check) => check.ok), checks }, null, 2));
|
|
12851
|
-
} else {
|
|
12852
|
-
checks.forEach((check) => printCheck(check));
|
|
12853
|
-
}
|
|
12854
|
-
if (checks.some((check) => !check.ok)) {
|
|
12855
|
-
process.exitCode = 1;
|
|
12856
|
-
}
|
|
13035
|
+
await runDoctor(options.workspace, options.json);
|
|
12857
13036
|
});
|
|
12858
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
|
+
}
|
|
12859
13090
|
async function pathExists(path) {
|
|
12860
13091
|
try {
|
|
12861
13092
|
await access2(path);
|
|
@@ -15277,7 +15508,7 @@ function registerNotifyCommand(program2) {
|
|
|
15277
15508
|
|
|
15278
15509
|
// src/commands/repl.ts
|
|
15279
15510
|
import { createInterface as createInterface3, emitKeypressEvents } from "readline";
|
|
15280
|
-
import { stdin as stdin5, stdout as stdout5 } from "process";
|
|
15511
|
+
import { stdin as stdin5, stdout as stdout5, stderr } from "process";
|
|
15281
15512
|
import chalk11 from "chalk";
|
|
15282
15513
|
|
|
15283
15514
|
// src/config/budget.ts
|
|
@@ -16633,22 +16864,168 @@ function noteNativeCompactionFallback(reason) {
|
|
|
16633
16864
|
);
|
|
16634
16865
|
}
|
|
16635
16866
|
|
|
16636
|
-
// src/
|
|
16637
|
-
|
|
16638
|
-
|
|
16639
|
-
|
|
16640
|
-
|
|
16641
|
-
|
|
16642
|
-
|
|
16643
|
-
|
|
16644
|
-
|
|
16645
|
-
|
|
16646
|
-
|
|
16647
|
-
|
|
16648
|
-
|
|
16649
|
-
|
|
16650
|
-
|
|
16651
|
-
|
|
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
|
+
|
|
17013
|
+
// src/commands/session-checkpoint.ts
|
|
17014
|
+
var SESSION_CHECKPOINT_FAILURE_WARNING = "Warning: the latest session checkpoint could not be saved; progress since the previous checkpoint may be lost.";
|
|
17015
|
+
function createSessionCheckpointWriter(workspace, onFailure) {
|
|
17016
|
+
let failureReported = false;
|
|
17017
|
+
const warnFailure = () => {
|
|
17018
|
+
if (failureReported) return;
|
|
17019
|
+
failureReported = true;
|
|
17020
|
+
onFailure(SESSION_CHECKPOINT_FAILURE_WARNING);
|
|
17021
|
+
};
|
|
17022
|
+
const writeCheckpoint = async (session) => {
|
|
17023
|
+
const trace = startDiagnosticSpan("session.checkpoint", {
|
|
17024
|
+
sessionId: session.id,
|
|
17025
|
+
stepCount: session.steps.length
|
|
17026
|
+
}, 5e3);
|
|
17027
|
+
try {
|
|
17028
|
+
const path = await checkpointSession(workspace, session);
|
|
16652
17029
|
if (!path) warnFailure();
|
|
16653
17030
|
trace.end({ outcome: path ? "ok" : "error", path });
|
|
16654
17031
|
return path;
|
|
@@ -16690,7 +17067,7 @@ function printSession(session, sessionPath2, options = {}) {
|
|
|
16690
17067
|
}
|
|
16691
17068
|
for (const toolResult of step.toolResults) {
|
|
16692
17069
|
const status = toolResult.result.ok ? chalk8.green("ok") : chalk8.red("failed");
|
|
16693
|
-
console.log(chalk8.
|
|
17070
|
+
console.log(chalk8.white(toolResult.call.tool), status);
|
|
16694
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." }))));
|
|
16695
17072
|
}
|
|
16696
17073
|
}
|
|
@@ -16710,13 +17087,13 @@ function printTools(tools) {
|
|
|
16710
17087
|
for (const tool of tools) {
|
|
16711
17088
|
const permissionText = tool.permissions.length ? chalk8.yellow(` [${tool.permissions.join(", ")}]`) : "";
|
|
16712
17089
|
const optionalText = tool.optionalPermissions.length ? chalk8.gray(` [optional: ${tool.optionalPermissions.join(", ")}]`) : "";
|
|
16713
|
-
console.log(`${chalk8.
|
|
17090
|
+
console.log(`${chalk8.white(tool.name)}${permissionText}${optionalText}`);
|
|
16714
17091
|
console.log(chalk8.gray(indent2(tool.description)));
|
|
16715
17092
|
}
|
|
16716
17093
|
}
|
|
16717
17094
|
function printToolResult(result, options = {}) {
|
|
16718
17095
|
const status = result.ok ? chalk8.green("ok") : chalk8.red("failed");
|
|
16719
|
-
console.log(`${chalk8.
|
|
17096
|
+
console.log(`${chalk8.white(result.tool)} ${status}`);
|
|
16720
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." }))));
|
|
16721
17098
|
}
|
|
16722
17099
|
function indent2(value) {
|
|
@@ -16734,207 +17111,6 @@ function formatToolOutput(content, options = {}) {
|
|
|
16734
17111
|
\u2026 ${hidden ? `${hidden} more lines` : "Long lines shortened"}. ${options.hint ?? "Full output remains available in the saved session."}`;
|
|
16735
17112
|
}
|
|
16736
17113
|
|
|
16737
|
-
// src/skills/commands.ts
|
|
16738
|
-
var BUILTIN_SLASH_COMMANDS = [
|
|
16739
|
-
"logs",
|
|
16740
|
-
"ghost",
|
|
16741
|
-
"output",
|
|
16742
|
-
"usage",
|
|
16743
|
-
"max-steps",
|
|
16744
|
-
"resume",
|
|
16745
|
-
"deny",
|
|
16746
|
-
"plan",
|
|
16747
|
-
"review",
|
|
16748
|
-
"coding",
|
|
16749
|
-
"status",
|
|
16750
|
-
"context",
|
|
16751
|
-
"compact",
|
|
16752
|
-
"diff",
|
|
16753
|
-
"sessions",
|
|
16754
|
-
"save",
|
|
16755
|
-
"test",
|
|
16756
|
-
"init",
|
|
16757
|
-
"new",
|
|
16758
|
-
"allow",
|
|
16759
|
-
"clear",
|
|
16760
|
-
"exit",
|
|
16761
|
-
"help",
|
|
16762
|
-
"memory",
|
|
16763
|
-
"mode",
|
|
16764
|
-
"model",
|
|
16765
|
-
"permissions",
|
|
16766
|
-
"quit",
|
|
16767
|
-
"skills",
|
|
16768
|
-
"tools",
|
|
16769
|
-
"whoami"
|
|
16770
|
-
];
|
|
16771
|
-
var COMMAND_NAME_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
16772
|
-
var MAX_COMMAND_LENGTH = 64;
|
|
16773
|
-
function parseSlashInput(input) {
|
|
16774
|
-
const trimmed = input.trim();
|
|
16775
|
-
if (!trimmed.startsWith("/")) return null;
|
|
16776
|
-
const rest = trimmed.slice(1);
|
|
16777
|
-
const match = rest.match(/^(\S+)(?:\s+([\s\S]*))?$/);
|
|
16778
|
-
if (!match) return null;
|
|
16779
|
-
return { command: match[1], args: (match[2] ?? "").trim() };
|
|
16780
|
-
}
|
|
16781
|
-
function isBuiltinSlashCommand(command) {
|
|
16782
|
-
return BUILTIN_SLASH_COMMANDS.includes(command.toLowerCase());
|
|
16783
|
-
}
|
|
16784
|
-
function normalizeCommandName(value) {
|
|
16785
|
-
const candidate = value.trim().replace(/^\/+/, "").trim();
|
|
16786
|
-
if (!candidate) {
|
|
16787
|
-
throw new Error("Slash command name is empty; expected kebab-case, for example: open-pr.");
|
|
16788
|
-
}
|
|
16789
|
-
if (candidate.length > MAX_COMMAND_LENGTH) {
|
|
16790
|
-
throw new Error(
|
|
16791
|
-
`Slash command name "${candidate}" is longer than ${MAX_COMMAND_LENGTH} characters.`
|
|
16792
|
-
);
|
|
16793
|
-
}
|
|
16794
|
-
if (!COMMAND_NAME_RE.test(candidate)) {
|
|
16795
|
-
throw new Error(
|
|
16796
|
-
`Invalid slash command name "${candidate}": use kebab-case - lowercase letters, digits, and single hyphens, no spaces or slashes (for example: open-pr).`
|
|
16797
|
-
);
|
|
16798
|
-
}
|
|
16799
|
-
return candidate;
|
|
16800
|
-
}
|
|
16801
|
-
async function collectEntries(workspace) {
|
|
16802
|
-
const skills = await discoverSkills(workspace);
|
|
16803
|
-
const claimed = /* @__PURE__ */ new Map();
|
|
16804
|
-
const conflicts = [];
|
|
16805
|
-
for (const skill of skills) {
|
|
16806
|
-
let declaration;
|
|
16807
|
-
try {
|
|
16808
|
-
declaration = skill.command ?? null;
|
|
16809
|
-
} catch (error) {
|
|
16810
|
-
conflicts.push({
|
|
16811
|
-
command: "",
|
|
16812
|
-
skillName: skill.name,
|
|
16813
|
-
scope: skill.scope,
|
|
16814
|
-
path: skill.path,
|
|
16815
|
-
reason: "invalid",
|
|
16816
|
-
message: `Skill "${skill.name}" (${skill.scope}) was skipped: ${error instanceof Error ? error.message : String(error)}`
|
|
16817
|
-
});
|
|
16818
|
-
continue;
|
|
16819
|
-
}
|
|
16820
|
-
if (!declaration) continue;
|
|
16821
|
-
let command;
|
|
16822
|
-
try {
|
|
16823
|
-
command = normalizeCommandName(declaration.raw);
|
|
16824
|
-
} catch (error) {
|
|
16825
|
-
conflicts.push({
|
|
16826
|
-
command: declaration.raw,
|
|
16827
|
-
skillName: skill.name,
|
|
16828
|
-
scope: skill.scope,
|
|
16829
|
-
path: skill.path,
|
|
16830
|
-
reason: "invalid",
|
|
16831
|
-
message: `Skill "${skill.name}" (${skill.scope}) declares a malformed command: ${error instanceof Error ? error.message : String(error)}`
|
|
16832
|
-
});
|
|
16833
|
-
continue;
|
|
16834
|
-
}
|
|
16835
|
-
if (isBuiltinSlashCommand(command)) {
|
|
16836
|
-
conflicts.push({
|
|
16837
|
-
command,
|
|
16838
|
-
skillName: skill.name,
|
|
16839
|
-
scope: skill.scope,
|
|
16840
|
-
path: skill.path,
|
|
16841
|
-
reason: "builtin",
|
|
16842
|
-
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}.`
|
|
16843
|
-
});
|
|
16844
|
-
continue;
|
|
16845
|
-
}
|
|
16846
|
-
const entry = {
|
|
16847
|
-
skill,
|
|
16848
|
-
command: {
|
|
16849
|
-
command,
|
|
16850
|
-
skillName: skill.name,
|
|
16851
|
-
description: skill.description,
|
|
16852
|
-
argumentHint: declaration.argumentHint,
|
|
16853
|
-
scope: skill.scope,
|
|
16854
|
-
path: skill.path
|
|
16855
|
-
}
|
|
16856
|
-
};
|
|
16857
|
-
const existing = claimed.get(command);
|
|
16858
|
-
if (!existing) {
|
|
16859
|
-
claimed.set(command, entry);
|
|
16860
|
-
continue;
|
|
16861
|
-
}
|
|
16862
|
-
const winner = existing.skill.scope === "global" && skill.scope === "project" ? entry : existing;
|
|
16863
|
-
const loser = winner === entry ? existing : entry;
|
|
16864
|
-
claimed.set(command, winner);
|
|
16865
|
-
conflicts.push({
|
|
16866
|
-
command,
|
|
16867
|
-
skillName: loser.skill.name,
|
|
16868
|
-
scope: loser.skill.scope,
|
|
16869
|
-
path: loser.skill.path,
|
|
16870
|
-
reason: "duplicate",
|
|
16871
|
-
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}.`
|
|
16872
|
-
});
|
|
16873
|
-
}
|
|
16874
|
-
const entries = [...claimed.values()].sort((a, b) => a.command.command.localeCompare(b.command.command));
|
|
16875
|
-
return { entries, conflicts };
|
|
16876
|
-
}
|
|
16877
|
-
async function listSkillCommands(workspace) {
|
|
16878
|
-
const { entries, conflicts } = await collectEntries(workspace);
|
|
16879
|
-
return { commands: entries.map((entry) => entry.command), conflicts };
|
|
16880
|
-
}
|
|
16881
|
-
function buildSkillCommandPrompt(skill, command, args) {
|
|
16882
|
-
const argumentLine = command.argumentHint ? `Arguments (${command.argumentHint}): ${args || "(none provided)"}` : `Arguments: ${args || "(none provided)"}`;
|
|
16883
|
-
const header = [
|
|
16884
|
-
`The user ran the /${command.command} slash command, which runs the "${skill.name}" skill (${skill.scope}).`,
|
|
16885
|
-
skill.description ? `Description: ${skill.description}` : void 0,
|
|
16886
|
-
argumentLine,
|
|
16887
|
-
skill.permissions.length ? `Permissions needed: ${skill.permissions.join(", ")}` : void 0,
|
|
16888
|
-
"Follow the instructions below now, applying the arguments where they are relevant."
|
|
16889
|
-
].filter(Boolean).join("\n");
|
|
16890
|
-
return `${header}
|
|
16891
|
-
|
|
16892
|
-
--- instructions ---
|
|
16893
|
-
${skill.body}`;
|
|
16894
|
-
}
|
|
16895
|
-
async function resolveSkillCommand(workspace, input) {
|
|
16896
|
-
const parsed = parseSlashInput(input);
|
|
16897
|
-
if (!parsed) return { status: "not-a-command" };
|
|
16898
|
-
const command = parsed.command.toLowerCase();
|
|
16899
|
-
if (isBuiltinSlashCommand(command)) {
|
|
16900
|
-
return { status: "builtin", command };
|
|
16901
|
-
}
|
|
16902
|
-
const { entries, conflicts } = await collectEntries(workspace);
|
|
16903
|
-
const match = entries.find((entry) => entry.command.command === command);
|
|
16904
|
-
if (!match) {
|
|
16905
|
-
const related = conflicts.filter((conflict) => conflict.command === command);
|
|
16906
|
-
if (related.length) {
|
|
16907
|
-
return {
|
|
16908
|
-
status: "conflict",
|
|
16909
|
-
command,
|
|
16910
|
-
message: related.map((conflict) => conflict.message).join("\n"),
|
|
16911
|
-
conflicts: related
|
|
16912
|
-
};
|
|
16913
|
-
}
|
|
16914
|
-
return {
|
|
16915
|
-
status: "unknown",
|
|
16916
|
-
command,
|
|
16917
|
-
available: entries.map((entry) => entry.command.command)
|
|
16918
|
-
};
|
|
16919
|
-
}
|
|
16920
|
-
return {
|
|
16921
|
-
status: "ok",
|
|
16922
|
-
command: match.command,
|
|
16923
|
-
skill: match.skill,
|
|
16924
|
-
args: parsed.args,
|
|
16925
|
-
prompt: buildSkillCommandPrompt(match.skill, match.command, parsed.args)
|
|
16926
|
-
};
|
|
16927
|
-
}
|
|
16928
|
-
function formatSkillCommandHelp(index) {
|
|
16929
|
-
if (!index.commands.length) return "";
|
|
16930
|
-
const lines = index.commands.map((command) => {
|
|
16931
|
-
const invocation = command.argumentHint ? `/${command.command} ${command.argumentHint}` : `/${command.command}`;
|
|
16932
|
-
const description = command.description || `Run the ${command.skillName} skill`;
|
|
16933
|
-
return ` ${invocation.padEnd(22)}${description} (skill: ${command.skillName})`;
|
|
16934
|
-
});
|
|
16935
|
-
return ["Skill commands:", ...lines].join("\n");
|
|
16936
|
-
}
|
|
16937
|
-
|
|
16938
17114
|
// src/ui/progress.ts
|
|
16939
17115
|
import { stdout as stdout3 } from "process";
|
|
16940
17116
|
import chalk9 from "chalk";
|
|
@@ -16964,12 +17140,12 @@ function formatElapsed(ms) {
|
|
|
16964
17140
|
}
|
|
16965
17141
|
function formatProgressLine(snapshot) {
|
|
16966
17142
|
const finished = snapshot.done >= snapshot.total;
|
|
16967
|
-
const glyph = finished ? chalk9.green(DONE_GLYPH) : chalk9.
|
|
17143
|
+
const glyph = finished ? chalk9.green(DONE_GLYPH) : chalk9.white(PENDING_GLYPH);
|
|
16968
17144
|
const noun = snapshot.total === 1 ? "agent" : "agents";
|
|
16969
17145
|
const stats = [`${snapshot.done}/${snapshot.total} ${noun} done`, formatElapsed(snapshot.elapsedMs)];
|
|
16970
17146
|
if (snapshot.tokens !== void 0) stats.push(`\u2193 ${formatTokens(snapshot.tokens)} tokens`);
|
|
16971
17147
|
const rendered = stats.map((stat11) => chalk9.dim(stat11)).join(chalk9.dim(" \xB7 "));
|
|
16972
|
-
return ` ${glyph} ${chalk9.
|
|
17148
|
+
return ` ${glyph} ${chalk9.white(snapshot.label)} ${snapshot.description} ${rendered}`;
|
|
16973
17149
|
}
|
|
16974
17150
|
var ProgressLine = class {
|
|
16975
17151
|
constructor(label, description, total) {
|
|
@@ -17203,15 +17379,691 @@ function compact2(value, max = 64) {
|
|
|
17203
17379
|
if (!value) return "";
|
|
17204
17380
|
return truncate2(value.replace(/\s+/g, " "), max);
|
|
17205
17381
|
}
|
|
17206
|
-
function quote(value, max = 48) {
|
|
17207
|
-
return value ? `\u201C${compact2(value, max)}\u201D` : "";
|
|
17382
|
+
function quote(value, max = 48) {
|
|
17383
|
+
return value ? `\u201C${compact2(value, max)}\u201D` : "";
|
|
17384
|
+
}
|
|
17385
|
+
function truncate2(value, max) {
|
|
17386
|
+
return value.length > max ? `${value.slice(0, Math.max(1, max - 1))}\u2026` : value;
|
|
17387
|
+
}
|
|
17388
|
+
function humanize(value) {
|
|
17389
|
+
const words = value.replace(/[_-]+/g, " ").trim();
|
|
17390
|
+
return words ? `${words[0].toUpperCase()}${words.slice(1)}` : "Tool";
|
|
17391
|
+
}
|
|
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, " ");
|
|
17208
17735
|
}
|
|
17209
|
-
function
|
|
17210
|
-
|
|
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;
|
|
17211
17746
|
}
|
|
17212
|
-
|
|
17213
|
-
|
|
17214
|
-
|
|
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();
|
|
17215
18067
|
}
|
|
17216
18068
|
|
|
17217
18069
|
// src/ui/welcome.ts
|
|
@@ -17227,29 +18079,28 @@ var LOGO = [
|
|
|
17227
18079
|
function renderWelcome(info, options = {}) {
|
|
17228
18080
|
const columns = normalizeColumns(options.columns);
|
|
17229
18081
|
const paint = new Chalk({ level: options.color === false ? 0 : chalk10.level });
|
|
17230
|
-
const cardWidth =
|
|
18082
|
+
const cardWidth = columns;
|
|
17231
18083
|
const innerWidth = cardWidth - 4;
|
|
17232
18084
|
const wide = columns >= 64;
|
|
17233
18085
|
const lines = [];
|
|
17234
18086
|
if (wide) {
|
|
17235
18087
|
lines.push("");
|
|
17236
18088
|
for (let index = 0; index < LOGO.length; index += 1) {
|
|
17237
|
-
|
|
17238
|
-
lines.push(` ${color(LOGO[index])}`);
|
|
18089
|
+
lines.push(` ${paint.bold.white(LOGO[index])}`);
|
|
17239
18090
|
}
|
|
17240
18091
|
} else {
|
|
17241
|
-
lines.push("", ` ${paint.bold.
|
|
18092
|
+
lines.push("", ` ${paint.bold.white("ULTRAC")} ${paint.gray(`v${info.version}`)}`);
|
|
17242
18093
|
}
|
|
17243
18094
|
const topLabel = wide ? ` ultrac v${info.version} ` : " session ";
|
|
17244
18095
|
const topFill = Math.max(0, cardWidth - topLabel.length - 2);
|
|
17245
|
-
lines.push(paint.
|
|
18096
|
+
lines.push(paint.gray(`\u256D${topLabel}${"\u2500".repeat(topFill)}\u256E`));
|
|
17246
18097
|
lines.push(cardLine(`${info.model} via ${info.providerName}`, innerWidth, paint.bold));
|
|
17247
18098
|
lines.push(cardLine(`${info.mode} \xB7 ${info.protocol}`, innerWidth, paint.white));
|
|
17248
18099
|
lines.push(cardLine(shortenMiddle(info.workspace, innerWidth), innerWidth, paint.gray));
|
|
17249
|
-
lines.push(paint.
|
|
18100
|
+
lines.push(paint.gray(`\u251C${"\u2500".repeat(cardWidth - 2)}\u2524`));
|
|
17250
18101
|
lines.push(cardLine(info.budget, innerWidth, paint.gray));
|
|
17251
18102
|
lines.push(cardLine(`logs ${shortenMiddle(info.diagnosticPath, Math.max(8, innerWidth - 5))}`, innerWidth, paint.gray));
|
|
17252
|
-
lines.push(paint.
|
|
18103
|
+
lines.push(paint.gray(`\u2570${"\u2500".repeat(cardWidth - 2)}\u256F`));
|
|
17253
18104
|
lines.push(
|
|
17254
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")
|
|
17255
18106
|
);
|
|
@@ -17257,33 +18108,33 @@ function renderWelcome(info, options = {}) {
|
|
|
17257
18108
|
}
|
|
17258
18109
|
function renderComposer(columns = 80, color = true) {
|
|
17259
18110
|
const paint = new Chalk({ level: color ? chalk10.level : 0 });
|
|
17260
|
-
const
|
|
18111
|
+
const width2 = normalizeColumns(columns);
|
|
17261
18112
|
const label = " message ";
|
|
17262
|
-
const fill = Math.max(1,
|
|
18113
|
+
const fill = Math.max(1, width2 - label.length - 2);
|
|
17263
18114
|
return {
|
|
17264
18115
|
frame: `
|
|
17265
|
-
${paint.
|
|
17266
|
-
${paint.
|
|
17267
|
-
prompt: paint.bold.
|
|
18116
|
+
${paint.gray(`\u256D\u2500${label}${"\u2500".repeat(fill)}`)}
|
|
18117
|
+
${paint.gray("\u2502")}`,
|
|
18118
|
+
prompt: paint.bold.white("\u2570\u2500\u276F ")
|
|
17268
18119
|
};
|
|
17269
18120
|
}
|
|
17270
|
-
function cardLine(value,
|
|
17271
|
-
const clipped = truncate3(value,
|
|
17272
|
-
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`;
|
|
17273
18124
|
}
|
|
17274
18125
|
function normalizeColumns(columns) {
|
|
17275
|
-
return Number.isFinite(columns) ? Math.max(
|
|
18126
|
+
return Number.isFinite(columns) ? Math.max(16, Math.floor(columns)) : 80;
|
|
17276
18127
|
}
|
|
17277
|
-
function truncate3(value,
|
|
17278
|
-
if (value.length <=
|
|
17279
|
-
if (
|
|
17280
|
-
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`;
|
|
17281
18132
|
}
|
|
17282
|
-
function shortenMiddle(value,
|
|
17283
|
-
if (value.length <=
|
|
17284
|
-
if (
|
|
17285
|
-
const side = Math.floor((
|
|
17286
|
-
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))}`;
|
|
17287
18138
|
}
|
|
17288
18139
|
|
|
17289
18140
|
// src/update/check.ts
|
|
@@ -17418,6 +18269,10 @@ async function runNpm(args, options) {
|
|
|
17418
18269
|
// src/commands/repl.ts
|
|
17419
18270
|
var HELP = `Commands:
|
|
17420
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)
|
|
17421
18276
|
/model [name] Show or set the model
|
|
17422
18277
|
/mode [name] Show or set the agent mode (${agentModeNames.join(", ")})
|
|
17423
18278
|
/permissions [readonly|workspace|full] Show or set permissions
|
|
@@ -17444,6 +18299,7 @@ var HELP = `Commands:
|
|
|
17444
18299
|
/ghost [on|off|status] Quiet activity output; diagnostics stay enabled
|
|
17445
18300
|
/output [number|list] Inspect saved tool output (latest by default)
|
|
17446
18301
|
/memory List and inspect stored memories
|
|
18302
|
+
/skill <name> [args] Run an installed skill
|
|
17447
18303
|
/skills List installed skills
|
|
17448
18304
|
/update Show how to install an available CLI update
|
|
17449
18305
|
/clear Start a fresh conversation
|
|
@@ -17543,7 +18399,7 @@ async function runRepl(options, initialTask = "") {
|
|
|
17543
18399
|
const hasFallbackChain = Boolean(config.fallback?.chain?.length);
|
|
17544
18400
|
if (providerName === "ultrac" && !hasFallbackChain && !await resolveUltracCredentials()) {
|
|
17545
18401
|
console.log(chalk11.yellow("You are not signed in to Ultrac."));
|
|
17546
|
-
console.log(`Run ${chalk11.
|
|
18402
|
+
console.log(`Run ${chalk11.white("ultrac login")} first, or start with ${chalk11.white("--provider echo")}.`);
|
|
17547
18403
|
process.exitCode = EXIT_REPL_NOT_STARTED;
|
|
17548
18404
|
return;
|
|
17549
18405
|
}
|
|
@@ -17605,10 +18461,37 @@ async function runRepl(options, initialTask = "") {
|
|
|
17605
18461
|
mode,
|
|
17606
18462
|
permissions: basePermissions
|
|
17607
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
|
+
};
|
|
17608
18490
|
const writeCheckpoint = createSessionCheckpointWriter(workspace, (message) => {
|
|
17609
18491
|
console.error(chalk11.yellow(message));
|
|
17610
18492
|
});
|
|
17611
18493
|
const persistSession = async (target = session) => {
|
|
18494
|
+
refreshDetails();
|
|
17612
18495
|
const snapshot = target.toSession();
|
|
17613
18496
|
if (!hasSavedSessionContent(snapshot)) return null;
|
|
17614
18497
|
return writeCheckpoint(snapshot);
|
|
@@ -17630,6 +18513,7 @@ async function runRepl(options, initialTask = "") {
|
|
|
17630
18513
|
try {
|
|
17631
18514
|
await session.start();
|
|
17632
18515
|
} catch (error) {
|
|
18516
|
+
layout?.close();
|
|
17633
18517
|
throw new Error(`Could not start the session: ${errorMessage2(error)}`, { cause: error });
|
|
17634
18518
|
}
|
|
17635
18519
|
const update = await updateCheck;
|
|
@@ -17638,21 +18522,58 @@ async function runRepl(options, initialTask = "") {
|
|
|
17638
18522
|
\u2191 Update available: ${update.currentVersion} \u2192 ${update.latestVersion}`));
|
|
17639
18523
|
console.log(chalk11.gray(" Run `ultrac update` after this session."));
|
|
17640
18524
|
}
|
|
17641
|
-
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;
|
|
17642
18538
|
const reader = new LineReader(rl);
|
|
17643
18539
|
const question = async (prompt, signal) => {
|
|
17644
|
-
|
|
17645
|
-
|
|
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
|
+
}
|
|
17646
18551
|
};
|
|
17647
18552
|
const drainQueuedMessages = () => reader.drainQueuedMessages();
|
|
18553
|
+
let autonomousRounds = 1;
|
|
17648
18554
|
setAskUserRuntime(async (text, options2, signal) => {
|
|
17649
|
-
console.log(chalk11.
|
|
18555
|
+
console.log(chalk11.white(`
|
|
17650
18556
|
? ${text}`));
|
|
17651
|
-
|
|
17652
|
-
|
|
17653
|
-
|
|
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);
|
|
17654
18576
|
}
|
|
17655
|
-
const rawAnswer = await question(chalk11.cyan(" \u203A "), signal);
|
|
17656
18577
|
if (signal?.aborted) return null;
|
|
17657
18578
|
const answer = (rawAnswer ?? "").trim();
|
|
17658
18579
|
const picked = Number.parseInt(answer, 10);
|
|
@@ -17672,7 +18593,7 @@ async function runRepl(options, initialTask = "") {
|
|
|
17672
18593
|
rl.on("SIGINT", exitSession);
|
|
17673
18594
|
const onKeypress = (_input, key) => {
|
|
17674
18595
|
if (!key || key.name !== "escape" || key.ctrl || key.meta) return;
|
|
17675
|
-
handleReplEscape(turnAbort);
|
|
18596
|
+
if (!slashCompletion?.handledEscape()) handleReplEscape(turnAbort);
|
|
17676
18597
|
};
|
|
17677
18598
|
if (stdin5.isTTY) {
|
|
17678
18599
|
emitKeypressEvents(stdin5, rl);
|
|
@@ -17684,30 +18605,44 @@ async function runRepl(options, initialTask = "") {
|
|
|
17684
18605
|
rl.close();
|
|
17685
18606
|
};
|
|
17686
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();
|
|
17687
18614
|
const withInterrupt = async (run) => {
|
|
17688
18615
|
turnAbort = new AbortController();
|
|
18616
|
+
turnStartedAt = Date.now();
|
|
18617
|
+
layout?.updateDetails({ status: "Running 00:00" });
|
|
17689
18618
|
try {
|
|
17690
18619
|
return await run(turnAbort.signal);
|
|
17691
18620
|
} finally {
|
|
17692
18621
|
turnAbort = void 0;
|
|
18622
|
+
turnStartedAt = void 0;
|
|
18623
|
+
refreshDetails();
|
|
18624
|
+
layout?.updateDetails({ status: "Idle" });
|
|
17693
18625
|
}
|
|
17694
18626
|
};
|
|
17695
18627
|
try {
|
|
17696
18628
|
if (initialTask) {
|
|
17697
|
-
console.log(chalk11.
|
|
18629
|
+
console.log(chalk11.white(`
|
|
17698
18630
|
\u203A ${initialTask}`));
|
|
17699
|
-
await withInterrupt((signal) => runAndCheckpointTurn(session, initialTask, question, signal, persistSession, drainQueuedMessages, () => reader.composing));
|
|
18631
|
+
await withInterrupt((signal) => runAndCheckpointTurn(session, initialTask, question, signal, persistSession, drainQueuedMessages, () => reader.composing, autonomousRounds));
|
|
17700
18632
|
}
|
|
17701
18633
|
while (running) {
|
|
17702
|
-
if (
|
|
18634
|
+
if (layout?.enabled) {
|
|
18635
|
+
layout.setPrompt("\u203A ");
|
|
18636
|
+
} else if (stdout5.isTTY) {
|
|
17703
18637
|
const composer = renderComposer(stdout5.columns, true);
|
|
17704
18638
|
console.log(composer.frame);
|
|
17705
18639
|
stdout5.write(composer.prompt);
|
|
17706
18640
|
} else {
|
|
17707
|
-
stdout5.write(chalk11.
|
|
18641
|
+
stdout5.write(chalk11.white("\n\u203A "));
|
|
17708
18642
|
}
|
|
17709
18643
|
const line = await reader.next();
|
|
17710
18644
|
if (line === null) break;
|
|
18645
|
+
if (layout?.enabled && line.trim()) console.log(`\u203A ${line.replace(/[\x00-\x1f\x7f]/g, " ")}`);
|
|
17711
18646
|
const raw = line.trim();
|
|
17712
18647
|
if (!raw) continue;
|
|
17713
18648
|
if (raw.startsWith("/")) {
|
|
@@ -17723,7 +18658,7 @@ async function runRepl(options, initialTask = "") {
|
|
|
17723
18658
|
resetContextBudgetNotices();
|
|
17724
18659
|
console.log(chalk11.gray("Started a fresh conversation."));
|
|
17725
18660
|
}, async (prompt) => {
|
|
17726
|
-
await runAndCheckpointTurn(session, prompt, question, signal, persistSession, drainQueuedMessages, () => reader.composing);
|
|
18661
|
+
await runAndCheckpointTurn(session, prompt, question, signal, persistSession, drainQueuedMessages, () => reader.composing, autonomousRounds);
|
|
17727
18662
|
}, signal, async (id) => {
|
|
17728
18663
|
const lineage = await loadSessionLineage(workspace, id);
|
|
17729
18664
|
const restored = createSession({ model: session.model, mode: session.mode, permissions: session.currentPermissions });
|
|
@@ -17732,7 +18667,11 @@ async function runRepl(options, initialTask = "") {
|
|
|
17732
18667
|
session = restored;
|
|
17733
18668
|
resetContextBudgetNotices();
|
|
17734
18669
|
console.log(`Restored ${lineage.sessions.length} saved session(s). Permissions were kept from the current session.`);
|
|
17735
|
-
})
|
|
18670
|
+
}, { mcpStatus: () => mcpRuntime?.statuses ?? [], autonomous: (setting) => {
|
|
18671
|
+
if (setting === "on") autonomousRounds = 5;
|
|
18672
|
+
if (setting === "off") autonomousRounds = 1;
|
|
18673
|
+
return autonomousRounds;
|
|
18674
|
+
} });
|
|
17736
18675
|
});
|
|
17737
18676
|
if (handled === "exit") {
|
|
17738
18677
|
running = false;
|
|
@@ -17741,7 +18680,9 @@ async function runRepl(options, initialTask = "") {
|
|
|
17741
18680
|
setSubagentRuntime({ provider, model: session.model, mode: session.mode, registry });
|
|
17742
18681
|
if (handled === "unhandled") {
|
|
17743
18682
|
try {
|
|
17744
|
-
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
|
+
}));
|
|
17745
18686
|
} finally {
|
|
17746
18687
|
await persistSession();
|
|
17747
18688
|
}
|
|
@@ -17752,15 +18693,21 @@ async function runRepl(options, initialTask = "") {
|
|
|
17752
18693
|
}
|
|
17753
18694
|
continue;
|
|
17754
18695
|
}
|
|
17755
|
-
await withInterrupt((signal) => runAndCheckpointTurn(session, raw, question, signal, persistSession, drainQueuedMessages, () => reader.composing));
|
|
18696
|
+
await withInterrupt((signal) => runAndCheckpointTurn(session, raw, question, signal, persistSession, drainQueuedMessages, () => reader.composing, autonomousRounds));
|
|
17756
18697
|
}
|
|
17757
18698
|
} finally {
|
|
18699
|
+
clearInterval(footerTimer);
|
|
18700
|
+
slashCompletion?.cleanup();
|
|
17758
18701
|
process.off("SIGTERM", terminate);
|
|
17759
18702
|
if (stdin5.isTTY) stdin5.off("keypress", onKeypress);
|
|
17760
18703
|
setAskUserRuntime(void 0);
|
|
17761
18704
|
rl.close();
|
|
17762
|
-
|
|
17763
|
-
|
|
18705
|
+
try {
|
|
18706
|
+
await saveTranscript(session, writeCheckpoint);
|
|
18707
|
+
} finally {
|
|
18708
|
+
mcpRuntime?.close();
|
|
18709
|
+
layout?.close();
|
|
18710
|
+
}
|
|
17764
18711
|
}
|
|
17765
18712
|
console.log(chalk11.gray("Goodbye."));
|
|
17766
18713
|
}
|
|
@@ -17850,9 +18797,26 @@ ${message.trim()}`);
|
|
|
17850
18797
|
detachProgress();
|
|
17851
18798
|
}
|
|
17852
18799
|
}
|
|
17853
|
-
async function runAndCheckpointTurn(session, input, question, signal, persist, drainQueuedMessages, isComposing) {
|
|
18800
|
+
async function runAndCheckpointTurn(session, input, question, signal, persist, drainQueuedMessages, isComposing, maxRounds = 1) {
|
|
17854
18801
|
try {
|
|
17855
|
-
return await runTurn(session, input, question, signal, drainQueuedMessages, persist, isComposing);
|
|
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;
|
|
17856
18820
|
} finally {
|
|
17857
18821
|
await persist(session);
|
|
17858
18822
|
}
|
|
@@ -17919,11 +18883,41 @@ function slashCommandChangesSession(raw) {
|
|
|
17919
18883
|
function errorMessage2(error) {
|
|
17920
18884
|
return error instanceof Error ? error.message : String(error);
|
|
17921
18885
|
}
|
|
17922
|
-
async function handleSlashCommand(raw, session, providerName, workspace, onClear, runPrompt, signal, resume) {
|
|
18886
|
+
async function handleSlashCommand(raw, session, providerName, workspace, onClear, runPrompt, signal, resume, services) {
|
|
17923
18887
|
const [name, ...rest] = raw.slice(1).split(/\s+/);
|
|
17924
18888
|
const command = (name ?? "").toLowerCase();
|
|
17925
18889
|
const argument = rest.join(" ").trim();
|
|
17926
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
|
+
}
|
|
17927
18921
|
case "logs": {
|
|
17928
18922
|
console.log(currentDiagnosticLogPath() ?? "Diagnostic logging has not been configured for this process.");
|
|
17929
18923
|
return "ok";
|
|
@@ -18006,7 +19000,7 @@ ${extra}`);
|
|
|
18006
19000
|
else {
|
|
18007
19001
|
for (const field of ["inputTokens", "outputTokens", "totalTokens"]) {
|
|
18008
19002
|
const counts = reported.flatMap((step) => typeof step.usage?.[field] === "number" ? [step.usage[field]] : []);
|
|
18009
|
-
console.log(`${field}: ${counts.length ? counts.reduce((sum,
|
|
19003
|
+
console.log(`${field}: ${counts.length ? counts.reduce((sum, count4) => sum + count4, 0) : "not reported"}`);
|
|
18010
19004
|
}
|
|
18011
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.`);
|
|
18012
19006
|
}
|
|
@@ -18158,6 +19152,20 @@ ${extra}`);
|
|
|
18158
19152
|
}
|
|
18159
19153
|
return "ok";
|
|
18160
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
|
+
}
|
|
18161
19169
|
case "skills": {
|
|
18162
19170
|
const skills = await listSkills(workspace);
|
|
18163
19171
|
if (!skills.length) {
|
|
@@ -18190,13 +19198,14 @@ ${extra}`);
|
|
|
18190
19198
|
return "unhandled";
|
|
18191
19199
|
}
|
|
18192
19200
|
}
|
|
18193
|
-
async function runSkillCommand(session, raw, workspace, question) {
|
|
19201
|
+
async function runSkillCommand(session, raw, workspace, question, runPrompt) {
|
|
18194
19202
|
const resolved = await resolveSkillCommand(workspace, raw);
|
|
18195
19203
|
if (resolved.status === "ok") {
|
|
18196
19204
|
console.log(
|
|
18197
19205
|
chalk11.gray(` /${resolved.command.command} \u2192 skill ${resolved.command.skillName} (${resolved.command.scope})`)
|
|
18198
19206
|
);
|
|
18199
|
-
await
|
|
19207
|
+
if (runPrompt) await runPrompt(resolved.prompt);
|
|
19208
|
+
else await runTurn(session, resolved.prompt, question);
|
|
18200
19209
|
return;
|
|
18201
19210
|
}
|
|
18202
19211
|
if (resolved.status === "conflict") {
|
|
@@ -18370,148 +19379,6 @@ function indent3(value) {
|
|
|
18370
19379
|
// src/commands/run.ts
|
|
18371
19380
|
import { basename as basename3 } from "path";
|
|
18372
19381
|
import chalk12 from "chalk";
|
|
18373
|
-
|
|
18374
|
-
// src/core/autonomous.ts
|
|
18375
|
-
import { createHash as createHash7, randomUUID as randomUUID5 } from "crypto";
|
|
18376
|
-
var AUTONOMOUS_CONTINUE_TASK = "Continue the task from where the previous round stopped. Check what is already done, finish the rest, and verify it.";
|
|
18377
|
-
var DEFAULT_AUTONOMOUS_MAX_ROUNDS = 5;
|
|
18378
|
-
var AUTONOMOUS_MAX_STEPS = 200;
|
|
18379
|
-
var DEFAULT_AUTONOMOUS_MAX_ELAPSED_MS = 6 * 60 * 60 * 1e3;
|
|
18380
|
-
var DEFAULT_AUTONOMOUS_BASE_DELAY_MS = 1e3;
|
|
18381
|
-
var DEFAULT_AUTONOMOUS_MAX_DELAY_MS = 3e4;
|
|
18382
|
-
var DEFAULT_AUTONOMOUS_STALL_ROUNDS = 3;
|
|
18383
|
-
function shouldResume(stopReason) {
|
|
18384
|
-
return stopReason === "max-steps" || stopReason === "error";
|
|
18385
|
-
}
|
|
18386
|
-
function boundedInteger(value, name, min, max) {
|
|
18387
|
-
if (!Number.isSafeInteger(value) || value < min || value > max) throw new Error(`${name} must be an integer from ${min} to ${max}.`);
|
|
18388
|
-
return value;
|
|
18389
|
-
}
|
|
18390
|
-
function abortableSleep(milliseconds, signal) {
|
|
18391
|
-
signal.throwIfAborted();
|
|
18392
|
-
return new Promise((resolve15, reject) => {
|
|
18393
|
-
const timer = setTimeout(() => {
|
|
18394
|
-
signal.removeEventListener("abort", abort);
|
|
18395
|
-
resolve15();
|
|
18396
|
-
}, milliseconds);
|
|
18397
|
-
const abort = () => {
|
|
18398
|
-
clearTimeout(timer);
|
|
18399
|
-
signal.removeEventListener("abort", abort);
|
|
18400
|
-
reject(signal.reason);
|
|
18401
|
-
};
|
|
18402
|
-
signal.addEventListener("abort", abort, { once: true });
|
|
18403
|
-
if (signal.aborted) abort();
|
|
18404
|
-
});
|
|
18405
|
-
}
|
|
18406
|
-
function progress(session) {
|
|
18407
|
-
const hash = createHash7("sha256");
|
|
18408
|
-
let completedTools = 0;
|
|
18409
|
-
hash.update(session.finalMessage ?? "");
|
|
18410
|
-
for (const step of session.steps ?? []) {
|
|
18411
|
-
hash.update(step.modelText ?? "");
|
|
18412
|
-
for (const tool of step.toolResults ?? []) {
|
|
18413
|
-
completedTools += 1;
|
|
18414
|
-
hash.update(tool.call.tool);
|
|
18415
|
-
hash.update(tool.result.ok ? "ok" : "failed");
|
|
18416
|
-
hash.update(tool.result.content ?? "");
|
|
18417
|
-
try {
|
|
18418
|
-
hash.update(JSON.stringify(tool.call.input));
|
|
18419
|
-
} catch {
|
|
18420
|
-
hash.update("unserializable-input");
|
|
18421
|
-
}
|
|
18422
|
-
}
|
|
18423
|
-
}
|
|
18424
|
-
return { fingerprint: hash.digest("hex"), completedTools };
|
|
18425
|
-
}
|
|
18426
|
-
async function runAutonomousLoop(options) {
|
|
18427
|
-
boundedInteger(options.maxRounds, "maxRounds", 1, 1e3);
|
|
18428
|
-
const maxElapsedMs = boundedInteger(options.maxElapsedMs ?? DEFAULT_AUTONOMOUS_MAX_ELAPSED_MS, "maxElapsedMs", 1, 7 * 24 * 60 * 60 * 1e3);
|
|
18429
|
-
const baseDelay = boundedInteger(options.baseDelayMs ?? DEFAULT_AUTONOMOUS_BASE_DELAY_MS, "baseDelayMs", 0, 3e5);
|
|
18430
|
-
const maxDelay = boundedInteger(options.maxDelayMs ?? DEFAULT_AUTONOMOUS_MAX_DELAY_MS, "maxDelayMs", baseDelay, 3e5);
|
|
18431
|
-
const stallLimit = boundedInteger(options.stallRounds ?? DEFAULT_AUTONOMOUS_STALL_ROUNDS, "stallRounds", 1, 1e3);
|
|
18432
|
-
const now = options.now ?? (() => performance.now());
|
|
18433
|
-
const started = now();
|
|
18434
|
-
const elapsed = () => Math.max(0, now() - started);
|
|
18435
|
-
const budget = new AbortController();
|
|
18436
|
-
const deadline = setTimeout(() => budget.abort(new DOMException("Autonomous elapsed budget reached.", "TimeoutError")), maxElapsedMs);
|
|
18437
|
-
deadline.unref?.();
|
|
18438
|
-
const signal = options.signal ? AbortSignal.any([options.signal, budget.signal]) : budget.signal;
|
|
18439
|
-
const sleep2 = options.sleep ?? abortableSleep;
|
|
18440
|
-
const id = randomUUID5();
|
|
18441
|
-
let previous;
|
|
18442
|
-
let lastFingerprint;
|
|
18443
|
-
let stalls = 0;
|
|
18444
|
-
let consecutiveErrors = 0;
|
|
18445
|
-
try {
|
|
18446
|
-
signal.throwIfAborted();
|
|
18447
|
-
for (let round = 1; ; round += 1) {
|
|
18448
|
-
const roundStart = now();
|
|
18449
|
-
diagnosticEvent("round.start", { id, round, maxRounds: options.maxRounds, elapsedMs: elapsed() });
|
|
18450
|
-
let session;
|
|
18451
|
-
try {
|
|
18452
|
-
session = await options.runRound(round, previous, signal);
|
|
18453
|
-
} catch (error) {
|
|
18454
|
-
diagnosticEvent("round.end", { id, round, reason: signal.aborted ? "aborted" : "exception", elapsedMs: elapsed(), durationMs: Math.max(0, now() - roundStart) });
|
|
18455
|
-
throw error;
|
|
18456
|
-
}
|
|
18457
|
-
const current = progress(session);
|
|
18458
|
-
const unchanged = lastFingerprint !== void 0 && current.fingerprint === lastFingerprint;
|
|
18459
|
-
stalls = unchanged ? stalls + 1 : 0;
|
|
18460
|
-
lastFingerprint = current.fingerprint;
|
|
18461
|
-
const classification = session.stopReason === "error" ? options.classifyFailure?.(session) : void 0;
|
|
18462
|
-
consecutiveErrors = session.stopReason === "error" ? consecutiveErrors + 1 : 0;
|
|
18463
|
-
let haltReason;
|
|
18464
|
-
if (session.stopReason === "completed") haltReason = "completed";
|
|
18465
|
-
else if (budget.signal.aborted || elapsed() >= maxElapsedMs) haltReason = "elapsed_budget";
|
|
18466
|
-
else if (signal.aborted || session.stopReason === "aborted") haltReason = "aborted";
|
|
18467
|
-
else if (classification && !classification.retryable) haltReason = "permanent_error";
|
|
18468
|
-
else if (!shouldResume(session.stopReason)) haltReason = "stopped";
|
|
18469
|
-
else if (stalls >= stallLimit) haltReason = "stalled";
|
|
18470
|
-
else if (round >= options.maxRounds) haltReason = "max_rounds";
|
|
18471
|
-
const retryDelay = Math.min(maxDelay, baseDelay * 2 ** Math.min(30, consecutiveErrors - 1));
|
|
18472
|
-
if (!haltReason && session.stopReason === "error" && elapsed() + retryDelay >= maxElapsedMs) haltReason = "elapsed_budget";
|
|
18473
|
-
diagnosticEvent("round.end", {
|
|
18474
|
-
id,
|
|
18475
|
-
round,
|
|
18476
|
-
sessionId: session.id,
|
|
18477
|
-
reason: haltReason ?? session.stopReason,
|
|
18478
|
-
failureKind: classification?.kind,
|
|
18479
|
-
attempt: consecutiveErrors,
|
|
18480
|
-
completedTools: current.completedTools,
|
|
18481
|
-
elapsedMs: elapsed(),
|
|
18482
|
-
durationMs: Math.max(0, now() - roundStart)
|
|
18483
|
-
});
|
|
18484
|
-
if (haltReason === "stalled") diagnosticEvent("round.stall", { id, round, repeats: stalls, completedTools: current.completedTools, elapsedMs: elapsed() });
|
|
18485
|
-
await options.onRoundEnd?.(round, session, !haltReason);
|
|
18486
|
-
if (haltReason) return { session, rounds: round, haltReason };
|
|
18487
|
-
if (session.stopReason === "error") {
|
|
18488
|
-
const delayMs = retryDelay;
|
|
18489
|
-
if (elapsed() + delayMs >= maxElapsedMs) return { session, rounds: round, haltReason: "elapsed_budget" };
|
|
18490
|
-
diagnosticEvent("round.retry", {
|
|
18491
|
-
id,
|
|
18492
|
-
round,
|
|
18493
|
-
nextRound: round + 1,
|
|
18494
|
-
attempt: consecutiveErrors,
|
|
18495
|
-
failureKind: classification?.kind ?? "unclassified",
|
|
18496
|
-
delayMs,
|
|
18497
|
-
elapsedMs: elapsed()
|
|
18498
|
-
});
|
|
18499
|
-
try {
|
|
18500
|
-
await sleep2(delayMs, signal);
|
|
18501
|
-
} catch (error) {
|
|
18502
|
-
if (!signal.aborted) throw error;
|
|
18503
|
-
return { session, rounds: round, haltReason: budget.signal.aborted ? "elapsed_budget" : "aborted" };
|
|
18504
|
-
}
|
|
18505
|
-
}
|
|
18506
|
-
if (signal.aborted || elapsed() >= maxElapsedMs) return { session, rounds: round, haltReason: budget.signal.aborted || elapsed() >= maxElapsedMs ? "elapsed_budget" : "aborted" };
|
|
18507
|
-
previous = session;
|
|
18508
|
-
}
|
|
18509
|
-
} finally {
|
|
18510
|
-
clearTimeout(deadline);
|
|
18511
|
-
}
|
|
18512
|
-
}
|
|
18513
|
-
|
|
18514
|
-
// src/commands/run.ts
|
|
18515
19382
|
function parsePositiveInt(value, flag) {
|
|
18516
19383
|
const parsed = Number.parseInt(value, 10);
|
|
18517
19384
|
if (!Number.isInteger(parsed) || parsed < 1 || String(parsed) !== value.trim()) {
|
|
@@ -19753,8 +20620,8 @@ Leaving run ${runId} as it is.
|
|
|
19753
20620
|
case "succeeded":
|
|
19754
20621
|
finishHuman(result.text);
|
|
19755
20622
|
if (result.toolCalls.length && !result.text) {
|
|
19756
|
-
const
|
|
19757
|
-
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.
|
|
19758
20625
|
`));
|
|
19759
20626
|
}
|
|
19760
20627
|
return result.outputSource === "incomplete" ? EXIT_FAILED : EXIT_DONE;
|