@spinalfluid/ultrac 0.1.5 → 0.1.9
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 +64 -7
- package/dist/cli.js +1635 -452
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import
|
|
4
|
+
import chalk16 from "chalk";
|
|
5
5
|
import { Command } from "commander";
|
|
6
6
|
|
|
7
7
|
// src/commands/ask.ts
|
|
@@ -154,10 +154,10 @@ function readEnvBaseUrl() {
|
|
|
154
154
|
}
|
|
155
155
|
function resolveBaseUrl(stored) {
|
|
156
156
|
if (baseUrlFlag) return { baseUrl: baseUrlFlag, source: "flag" };
|
|
157
|
-
const
|
|
158
|
-
if (
|
|
159
|
-
assertAllowedUltracBaseUrl(
|
|
160
|
-
return { baseUrl:
|
|
157
|
+
const env2 = readEnvBaseUrl();
|
|
158
|
+
if (env2) {
|
|
159
|
+
assertAllowedUltracBaseUrl(env2);
|
|
160
|
+
return { baseUrl: env2, source: "env" };
|
|
161
161
|
}
|
|
162
162
|
if (stored?.baseUrl) {
|
|
163
163
|
const safe = safeStoredBaseUrl(stored.baseUrl, ULTRAC_DEFAULT_BASE_URL);
|
|
@@ -242,42 +242,51 @@ import { z as z5 } from "zod";
|
|
|
242
242
|
|
|
243
243
|
// src/core/modes.ts
|
|
244
244
|
var agentModeNames = ["default", "plan", "security", "coding", "review"];
|
|
245
|
-
var sharedAgentTools = [
|
|
246
|
-
|
|
245
|
+
var sharedAgentTools = [
|
|
246
|
+
"task",
|
|
247
|
+
"skill",
|
|
248
|
+
"list_skills",
|
|
249
|
+
"memory_read",
|
|
250
|
+
"ask_user"
|
|
251
|
+
];
|
|
252
|
+
var sharedWriteMemory = ["memory_write"];
|
|
253
|
+
var readExploreTools = [
|
|
247
254
|
"list_files",
|
|
248
255
|
"read_file",
|
|
256
|
+
"read_many_files",
|
|
249
257
|
"search_files",
|
|
258
|
+
"inspect_project",
|
|
259
|
+
"repo_map",
|
|
260
|
+
"git_status",
|
|
261
|
+
"git_worktree_summary",
|
|
250
262
|
"git_diff",
|
|
251
|
-
"git_log"
|
|
252
|
-
...sharedAgentTools
|
|
263
|
+
"git_log"
|
|
253
264
|
];
|
|
265
|
+
var readOnlyCoreTools = [...readExploreTools, ...sharedAgentTools];
|
|
254
266
|
var coreTools = [
|
|
255
|
-
|
|
256
|
-
"read_file",
|
|
257
|
-
"search_files",
|
|
267
|
+
...readExploreTools,
|
|
258
268
|
"run_shell",
|
|
259
269
|
"write_file",
|
|
260
270
|
"replace_in_file",
|
|
261
271
|
"multi_edit",
|
|
262
272
|
"apply_patch",
|
|
263
|
-
"
|
|
264
|
-
"
|
|
273
|
+
"git_commit",
|
|
274
|
+
"fetch_url",
|
|
265
275
|
"todo_write",
|
|
266
276
|
"todo_read",
|
|
267
|
-
...sharedAgentTools
|
|
277
|
+
...sharedAgentTools,
|
|
278
|
+
...sharedWriteMemory
|
|
268
279
|
];
|
|
269
280
|
var agentModePresets = {
|
|
270
281
|
default: {
|
|
271
282
|
name: "default",
|
|
272
283
|
label: "Default",
|
|
273
284
|
instructions: [
|
|
274
|
-
"Gather repository context before acting.",
|
|
285
|
+
"Gather repository context with tools before acting; the startup header is not the repository.",
|
|
275
286
|
"Prefer narrow, reversible changes.",
|
|
276
287
|
"Keep final answers concise and include verification status."
|
|
277
288
|
],
|
|
278
|
-
|
|
279
|
-
// way the task will go, so it pays only for what every task needs.
|
|
280
|
-
tools: coreTools,
|
|
289
|
+
tools: [...coreTools, "list_sessions", "read_session", "search_sessions"],
|
|
281
290
|
externalTools: true
|
|
282
291
|
},
|
|
283
292
|
plan: {
|
|
@@ -288,10 +297,7 @@ var agentModePresets = {
|
|
|
288
297
|
"Use read and search tools to build an accurate picture, then use todo_write to lay out the concrete steps.",
|
|
289
298
|
"Finish by presenting the plan and the files it will touch, and wait for approval before switching to an editing mode."
|
|
290
299
|
],
|
|
291
|
-
|
|
292
|
-
// written list of steps. No editor, no shell - the mode promises not to
|
|
293
|
-
// change anything, and the advertised set says the same.
|
|
294
|
-
tools: [...readOnlyCoreTools, "repo_map", "todo_write", "todo_read"],
|
|
300
|
+
tools: [...readOnlyCoreTools, "repo_handbook", "verification_plan", "todo_write", "todo_read"],
|
|
295
301
|
externalTools: false
|
|
296
302
|
},
|
|
297
303
|
security: {
|
|
@@ -304,14 +310,8 @@ var agentModePresets = {
|
|
|
304
310
|
"Use fetch_url only against hosts the user named. It is an HTTP client, not a scanner.",
|
|
305
311
|
"Separate confirmed findings from hypotheses and call out missing authorization or scope."
|
|
306
312
|
],
|
|
307
|
-
// Core plus local evidence collection and the HTTP client. Editors stay so
|
|
308
|
-
// a finding can become a patch in the same pass. list_processes is
|
|
309
|
-
// callable by name under shell; it is not advertised here because a
|
|
310
|
-
// security review of a checkout rarely needs the host process table.
|
|
311
313
|
tools: [
|
|
312
314
|
...coreTools,
|
|
313
|
-
"fetch_url",
|
|
314
|
-
"inspect_project",
|
|
315
315
|
"inspect_artifact",
|
|
316
316
|
"hex_dump",
|
|
317
317
|
"extract_strings",
|
|
@@ -329,10 +329,20 @@ var agentModePresets = {
|
|
|
329
329
|
"Make the smallest coherent patch that satisfies the task.",
|
|
330
330
|
"Run the most relevant verification when shell permission is available; otherwise say what was not run."
|
|
331
331
|
],
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
332
|
+
tools: [
|
|
333
|
+
...coreTools,
|
|
334
|
+
"ast_search",
|
|
335
|
+
"repo_handbook",
|
|
336
|
+
"verification_plan",
|
|
337
|
+
"git_show",
|
|
338
|
+
"git_push",
|
|
339
|
+
"run_tests",
|
|
340
|
+
"diagnostics",
|
|
341
|
+
"run_package_script",
|
|
342
|
+
"list_sessions",
|
|
343
|
+
"read_session",
|
|
344
|
+
"search_sessions"
|
|
345
|
+
],
|
|
336
346
|
externalTools: true
|
|
337
347
|
},
|
|
338
348
|
review: {
|
|
@@ -343,10 +353,14 @@ var agentModePresets = {
|
|
|
343
353
|
"Lead with findings grounded in concrete file paths, lines, commands, or behavior.",
|
|
344
354
|
"Avoid broad rewrites unless the user explicitly asks for an implementation pass."
|
|
345
355
|
],
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
356
|
+
tools: [
|
|
357
|
+
...readOnlyCoreTools,
|
|
358
|
+
"git_show",
|
|
359
|
+
"git_blame",
|
|
360
|
+
"diagnostics",
|
|
361
|
+
"verification_plan",
|
|
362
|
+
"verification_report"
|
|
363
|
+
],
|
|
350
364
|
externalTools: false
|
|
351
365
|
}
|
|
352
366
|
};
|
|
@@ -534,6 +548,9 @@ function ensurePrivateDirectory(path) {
|
|
|
534
548
|
}
|
|
535
549
|
chmodSync(path, 448);
|
|
536
550
|
}
|
|
551
|
+
function currentDiagnosticLogPath() {
|
|
552
|
+
return diagnosticPath;
|
|
553
|
+
}
|
|
537
554
|
function diagnosticEvent(event, fields = {}) {
|
|
538
555
|
if (!diagnosticPath) return;
|
|
539
556
|
const record3 = {
|
|
@@ -878,20 +895,32 @@ function contextUsageStatus(messages, usage, disposition) {
|
|
|
878
895
|
...disposition.error ? { error: disposition.error } : {}
|
|
879
896
|
};
|
|
880
897
|
}
|
|
898
|
+
function contextOverflowMessage(usage, freedChars, safeWindowChars, pinned) {
|
|
899
|
+
const numbers = `system ${count(usage.systemChars)} + tools ${count(usage.toolsChars)} + history ${count(
|
|
900
|
+
usage.historyChars
|
|
901
|
+
)} = ${count(usage.totalChars)} characters against a ${count(usage.windowChars)}-character window (${count(
|
|
902
|
+
usage.contextTokens
|
|
903
|
+
)} tokens at ${CHARS_PER_TOKEN} chars/token)` + (safeWindowChars < usage.windowChars ? ` with a ${count(safeWindowChars)}-character safe wire ceiling` : "") + `${freedChars > 0 ? `, after dropping ${count(freedChars)} characters of earlier turns` : ""}`;
|
|
904
|
+
if (pinned) {
|
|
905
|
+
return `Pinned user instructions cannot fit the model's context window and will not be discarded: ${numbers}. Shorten the prompt, advertise fewer tools (--tools, or a narrower --mode), or raise --context-tokens / ULTRAC_MODEL_CONTEXT_TOKENS if the host window really is larger.`;
|
|
906
|
+
}
|
|
907
|
+
return `Request does not fit the model's context window: ${numbers}. Advertise fewer tools (--tools, or a narrower --mode), shorten the prompt, or raise --context-tokens / ULTRAC_MODEL_CONTEXT_TOKENS if the host window really is larger.`;
|
|
908
|
+
}
|
|
881
909
|
var ContextBudgetError = class extends Error {
|
|
882
910
|
usage;
|
|
883
911
|
constructor(usage, freedChars, safeWindowChars = usage.windowChars) {
|
|
884
|
-
super(
|
|
885
|
-
`Request does not fit the model's context window: system ${count(usage.systemChars)} + tools ${count(
|
|
886
|
-
usage.toolsChars
|
|
887
|
-
)} + history ${count(usage.historyChars)} = ${count(usage.totalChars)} characters against a ${count(
|
|
888
|
-
usage.windowChars
|
|
889
|
-
)}-character window (${count(usage.contextTokens)} tokens at ${CHARS_PER_TOKEN} chars/token)` + (safeWindowChars < usage.windowChars ? ` with a ${count(safeWindowChars)}-character safe wire ceiling` : "") + `${freedChars > 0 ? `, after dropping ${count(freedChars)} characters of earlier turns` : ""}. Advertise fewer tools (--tools, or a narrower --mode), shorten the prompt, or raise --context-tokens / ULTRAC_MODEL_CONTEXT_TOKENS if the host window really is larger.`
|
|
890
|
-
);
|
|
912
|
+
super(contextOverflowMessage(usage, freedChars, safeWindowChars, false));
|
|
891
913
|
this.name = "ContextBudgetError";
|
|
892
914
|
this.usage = usage;
|
|
893
915
|
}
|
|
894
916
|
};
|
|
917
|
+
var PinnedContextBudgetError = class extends ContextBudgetError {
|
|
918
|
+
constructor(usage, freedChars, safeWindowChars = usage.windowChars) {
|
|
919
|
+
super(usage, freedChars, safeWindowChars);
|
|
920
|
+
this.name = "PinnedContextBudgetError";
|
|
921
|
+
this.message = contextOverflowMessage(usage, freedChars, safeWindowChars, true);
|
|
922
|
+
}
|
|
923
|
+
};
|
|
895
924
|
function planToolCallRequest(input) {
|
|
896
925
|
return planRequest({
|
|
897
926
|
units: groupNativeMessages(input.messages, input.isPinned),
|
|
@@ -964,6 +993,10 @@ function planRequest(input) {
|
|
|
964
993
|
const usage = summarize(kept, input.toolsChars, window);
|
|
965
994
|
const safeWindowChars = Math.floor(usage.windowChars * HARD_WINDOW_SAFETY_FRACTION);
|
|
966
995
|
if (usage.totalChars > safeWindowChars) {
|
|
996
|
+
const droppableLeft = kept.some((unit) => !unit.pinned && !unit.system);
|
|
997
|
+
if (!droppableLeft && kept.some((unit) => unit.pinned && !unit.system)) {
|
|
998
|
+
throw new PinnedContextBudgetError(usage, freedChars, safeWindowChars);
|
|
999
|
+
}
|
|
967
1000
|
throw new ContextBudgetError(usage, freedChars, safeWindowChars);
|
|
968
1001
|
}
|
|
969
1002
|
return {
|
|
@@ -3769,12 +3802,17 @@ import { z as z6 } from "zod";
|
|
|
3769
3802
|
|
|
3770
3803
|
// src/core/ask-runtime.ts
|
|
3771
3804
|
var askUserRuntime;
|
|
3772
|
-
|
|
3805
|
+
var askUserRuntimeRemote = false;
|
|
3806
|
+
function setAskUserRuntime(fn, options = {}) {
|
|
3773
3807
|
askUserRuntime = fn;
|
|
3808
|
+
askUserRuntimeRemote = fn !== void 0 && options.remote === true;
|
|
3774
3809
|
}
|
|
3775
3810
|
function getAskUserRuntime() {
|
|
3776
3811
|
return askUserRuntime;
|
|
3777
3812
|
}
|
|
3813
|
+
function isAskUserRuntimeRemote() {
|
|
3814
|
+
return askUserRuntimeRemote;
|
|
3815
|
+
}
|
|
3778
3816
|
|
|
3779
3817
|
// src/tools/workspace/ask-user.ts
|
|
3780
3818
|
var AskUserInput = z6.object({
|
|
@@ -3796,7 +3834,7 @@ var askUserTool = {
|
|
|
3796
3834
|
async execute(input, context) {
|
|
3797
3835
|
const ask2 = getAskUserRuntime();
|
|
3798
3836
|
if (!ask2) return { ok: false, content: NO_ONE_TO_ASK };
|
|
3799
|
-
if (!stdin.isTTY) return { ok: false, content: NO_ONE_TO_ASK };
|
|
3837
|
+
if (!isAskUserRuntimeRemote() && !stdin.isTTY) return { ok: false, content: NO_ONE_TO_ASK };
|
|
3800
3838
|
const answer = await ask2(input.question, input.options, context.signal);
|
|
3801
3839
|
if (context.signal?.aborted) {
|
|
3802
3840
|
return { ok: false, content: "Question cancelled; no answer was recorded." };
|
|
@@ -3927,17 +3965,17 @@ var SENSITIVE_ENV_PATTERNS = [
|
|
|
3927
3965
|
/(?:^|_)WEBHOOK_SECRET$/
|
|
3928
3966
|
];
|
|
3929
3967
|
function createSanitizedProcessEnv(source = process.env) {
|
|
3930
|
-
const
|
|
3968
|
+
const env2 = {};
|
|
3931
3969
|
const redactedEnvKeys = [];
|
|
3932
3970
|
for (const [key, value] of Object.entries(source)) {
|
|
3933
3971
|
if (isSensitiveEnvName(key)) {
|
|
3934
3972
|
redactedEnvKeys.push(key);
|
|
3935
3973
|
continue;
|
|
3936
3974
|
}
|
|
3937
|
-
|
|
3975
|
+
env2[key] = value;
|
|
3938
3976
|
}
|
|
3939
3977
|
redactedEnvKeys.sort();
|
|
3940
|
-
return { env, redactedEnvKeys };
|
|
3978
|
+
return { env: env2, redactedEnvKeys };
|
|
3941
3979
|
}
|
|
3942
3980
|
function isSensitiveEnvName(name) {
|
|
3943
3981
|
const normalized = name.toUpperCase();
|
|
@@ -4026,12 +4064,12 @@ function runProcess(command, args, options) {
|
|
|
4026
4064
|
redactedEnvKeys: []
|
|
4027
4065
|
});
|
|
4028
4066
|
}
|
|
4029
|
-
const { env, redactedEnvKeys } = createSanitizedProcessEnv();
|
|
4067
|
+
const { env: env2, redactedEnvKeys } = createSanitizedProcessEnv();
|
|
4030
4068
|
return new Promise((resolve15, reject) => {
|
|
4031
4069
|
const child = spawn(command, args, {
|
|
4032
4070
|
cwd: options.cwd,
|
|
4033
4071
|
stdio: ["pipe", "pipe", "pipe"],
|
|
4034
|
-
env,
|
|
4072
|
+
env: env2,
|
|
4035
4073
|
shell: options.shell ?? false,
|
|
4036
4074
|
// Its own process group, so the timeout can kill what the child started
|
|
4037
4075
|
// as well as the child. Killing only the direct child leaves the grand-
|
|
@@ -5116,7 +5154,7 @@ function defaultContextFiles(files) {
|
|
|
5116
5154
|
}
|
|
5117
5155
|
function isPriorityContextFile(path) {
|
|
5118
5156
|
const normalized = path.toLowerCase();
|
|
5119
|
-
const
|
|
5157
|
+
const basename4 = normalized.split("/").at(-1) ?? normalized;
|
|
5120
5158
|
return [
|
|
5121
5159
|
"agents.md",
|
|
5122
5160
|
"claude.md",
|
|
@@ -5126,12 +5164,12 @@ function isPriorityContextFile(path) {
|
|
|
5126
5164
|
"pyproject.toml",
|
|
5127
5165
|
"readme.md",
|
|
5128
5166
|
"tsconfig.json"
|
|
5129
|
-
].includes(
|
|
5167
|
+
].includes(basename4) || normalized === "docs/architecture.md" || normalized === "docs/roadmap.md";
|
|
5130
5168
|
}
|
|
5131
5169
|
function isLikelyEntrypoint(path) {
|
|
5132
5170
|
const normalized = path.toLowerCase();
|
|
5133
|
-
const
|
|
5134
|
-
return
|
|
5171
|
+
const basename4 = normalized.split("/").at(-1) ?? normalized;
|
|
5172
|
+
return basename4 === "main.ts" || basename4 === "main.tsx" || basename4 === "cli.ts" || basename4 === "index.ts" || basename4 === "app.ts" || basename4.startsWith("next.config.") || basename4.startsWith("vite.config.") || normalized === "src/app/page.tsx" || normalized === "src/app/layout.tsx";
|
|
5135
5173
|
}
|
|
5136
5174
|
function summarizeRepository(files) {
|
|
5137
5175
|
return {
|
|
@@ -7137,8 +7175,8 @@ var scanSecretsTool = {
|
|
|
7137
7175
|
}
|
|
7138
7176
|
scannedFiles += 1;
|
|
7139
7177
|
const text = bytes.toString("utf8");
|
|
7140
|
-
const
|
|
7141
|
-
const skipEntropy = lockfileNames.has(
|
|
7178
|
+
const basename4 = file.split("/").pop() ?? file;
|
|
7179
|
+
const skipEntropy = lockfileNames.has(basename4);
|
|
7142
7180
|
collectLineFindings(file, text, rules, skipEntropy, findings, input.maxFindings);
|
|
7143
7181
|
}
|
|
7144
7182
|
const visible = findings.slice(0, input.maxFindings);
|
|
@@ -7166,7 +7204,7 @@ function compileCustomRules(rules) {
|
|
|
7166
7204
|
}
|
|
7167
7205
|
function collectLineFindings(path, text, rules, skipLockfileRules, findings, maxFindings) {
|
|
7168
7206
|
const lines = text.split(/\r?\n/);
|
|
7169
|
-
const
|
|
7207
|
+
const basename4 = path.split("/").pop() ?? path;
|
|
7170
7208
|
for (const [index, line] of lines.entries()) {
|
|
7171
7209
|
if (findings.length >= maxFindings) return;
|
|
7172
7210
|
if (line.length > 4e3) continue;
|
|
@@ -7183,7 +7221,7 @@ function collectLineFindings(path, text, rules, skipLockfileRules, findings, max
|
|
|
7183
7221
|
snippet: redactSecret(match[0])
|
|
7184
7222
|
});
|
|
7185
7223
|
}
|
|
7186
|
-
if (skipLockfileRules || lockfileNames.has(
|
|
7224
|
+
if (skipLockfileRules || lockfileNames.has(basename4) || findings.length >= maxFindings) continue;
|
|
7187
7225
|
collectEntropyFinding(path, index + 1, line, findings);
|
|
7188
7226
|
}
|
|
7189
7227
|
}
|
|
@@ -8005,7 +8043,9 @@ async function createSkill(workspace, input) {
|
|
|
8005
8043
|
name,
|
|
8006
8044
|
description: input.description.trim(),
|
|
8007
8045
|
...input.whenToUse ? { when_to_use: input.whenToUse.trim() } : {},
|
|
8008
|
-
...permissions.length ? { permissions } : {}
|
|
8046
|
+
...permissions.length ? { permissions } : {},
|
|
8047
|
+
...input.command ? { command: input.command } : {},
|
|
8048
|
+
...input.argumentHint ? { "argument-hint": input.argumentHint } : {}
|
|
8009
8049
|
},
|
|
8010
8050
|
input.body?.trim() || `# ${name}
|
|
8011
8051
|
|
|
@@ -8022,7 +8062,8 @@ Describe the steps the agent should follow when this skill is invoked.`
|
|
|
8022
8062
|
whenToUse: input.whenToUse?.trim(),
|
|
8023
8063
|
permissions,
|
|
8024
8064
|
body: document,
|
|
8025
|
-
path
|
|
8065
|
+
path,
|
|
8066
|
+
...input.command ? { command: { raw: input.command, argumentHint: input.argumentHint } } : {}
|
|
8026
8067
|
};
|
|
8027
8068
|
}
|
|
8028
8069
|
async function loadSkillsContext(workspace) {
|
|
@@ -8542,7 +8583,7 @@ function summarizeExtensions(files) {
|
|
|
8542
8583
|
}
|
|
8543
8584
|
function isImportantFile(path) {
|
|
8544
8585
|
const normalized = path.toLowerCase();
|
|
8545
|
-
const
|
|
8586
|
+
const basename4 = normalized.split("/").at(-1) ?? normalized;
|
|
8546
8587
|
if ([
|
|
8547
8588
|
"agents.md",
|
|
8548
8589
|
"claude.md",
|
|
@@ -8555,7 +8596,7 @@ function isImportantFile(path) {
|
|
|
8555
8596
|
].includes(normalized)) {
|
|
8556
8597
|
return true;
|
|
8557
8598
|
}
|
|
8558
|
-
return
|
|
8599
|
+
return basename4.startsWith("next.config.") || basename4.startsWith("vite.config.") || normalized.startsWith("docs/") || normalized.startsWith("infra/") || normalized === "src/cli.ts" || normalized === "src/main.tsx" || normalized === "src/app/page.tsx";
|
|
8559
8600
|
}
|
|
8560
8601
|
function formatBytes(bytes) {
|
|
8561
8602
|
if (bytes < 1024) return `${bytes} B`;
|
|
@@ -8796,6 +8837,124 @@ import { z as z25 } from "zod";
|
|
|
8796
8837
|
import { mkdir as mkdir11, writeFile as writeFile8 } from "fs/promises";
|
|
8797
8838
|
import { join as join9 } from "path";
|
|
8798
8839
|
|
|
8840
|
+
// src/core/periodic-compaction.ts
|
|
8841
|
+
var DEFAULT_SUMMARY_INTERVAL_STEPS = 12;
|
|
8842
|
+
var MIN_PERIODIC_HISTORY_CHARS = 12e3;
|
|
8843
|
+
function summaryIntervalSteps(value = process.env.ULTRAC_SUMMARY_INTERVAL_STEPS) {
|
|
8844
|
+
if (value === void 0 || value.trim() === "") return DEFAULT_SUMMARY_INTERVAL_STEPS;
|
|
8845
|
+
if (!/^\d+$/.test(value) || !Number.isSafeInteger(Number(value)) || Number(value) > 1e3) throw new Error("ULTRAC_SUMMARY_INTERVAL_STEPS must be an integer from 0 to 1000.");
|
|
8846
|
+
return Number(value);
|
|
8847
|
+
}
|
|
8848
|
+
function completeContextUnits(messages) {
|
|
8849
|
+
const units = [];
|
|
8850
|
+
for (const message of messages) {
|
|
8851
|
+
if (message.role === "tool") {
|
|
8852
|
+
const previous = units.at(-1);
|
|
8853
|
+
if (!previous || previous[0].role !== "assistant") return null;
|
|
8854
|
+
previous.push(message);
|
|
8855
|
+
} else units.push([message]);
|
|
8856
|
+
}
|
|
8857
|
+
for (const unit of units) {
|
|
8858
|
+
const first = unit[0];
|
|
8859
|
+
if (first.role !== "assistant") continue;
|
|
8860
|
+
if (!first.toolCalls?.length) {
|
|
8861
|
+
if (unit.slice(1).some((message) => "toolCallId" in message)) return null;
|
|
8862
|
+
continue;
|
|
8863
|
+
}
|
|
8864
|
+
const ids = new Set(first.toolCalls.map((call) => call.id));
|
|
8865
|
+
const results = unit.slice(1).map((message) => message.toolCallId);
|
|
8866
|
+
if (ids.size !== first.toolCalls.length || results.length !== ids.size || results.some((id) => !id || !ids.has(id)) || new Set(results).size !== ids.size) return null;
|
|
8867
|
+
}
|
|
8868
|
+
return units;
|
|
8869
|
+
}
|
|
8870
|
+
var PeriodicCompactor = class {
|
|
8871
|
+
constructor(options = {}) {
|
|
8872
|
+
this.options = options;
|
|
8873
|
+
}
|
|
8874
|
+
options;
|
|
8875
|
+
lastAttemptStep = 0;
|
|
8876
|
+
async compact(messages, pinned, provider, model, completedSteps, signal) {
|
|
8877
|
+
const unchanged = () => ({ messages: [...messages], compacted: false });
|
|
8878
|
+
const interval = this.options.intervalSteps ?? summaryIntervalSteps();
|
|
8879
|
+
if (!interval || signal?.aborted || completedSteps - this.lastAttemptStep < interval) return unchanged();
|
|
8880
|
+
const units = completeContextUnits(messages);
|
|
8881
|
+
if (!units) return unchanged();
|
|
8882
|
+
const recent = new Set(units.filter((unit) => unit[0].role === "assistant").slice(-2));
|
|
8883
|
+
const pinnedSet = new Set(pinned);
|
|
8884
|
+
const history = units.filter((unit) => !recent.has(unit) && !unit.some((message) => message.role === "user" || message.role === "system" || pinnedSet.has(message) || isCompactionSummary(message))).flat();
|
|
8885
|
+
const removedChars = history.reduce((size, message) => size + JSON.stringify(message).length, 0);
|
|
8886
|
+
if (history.length < 2 || removedChars < (this.options.minHistoryChars ?? MIN_PERIODIC_HISTORY_CHARS)) return unchanged();
|
|
8887
|
+
this.lastAttemptStep = completedSteps;
|
|
8888
|
+
const trace = startDiagnosticSpan("context.compaction", { path: "periodic", completedSteps, compactedMessageCount: history.length }, 5e3);
|
|
8889
|
+
const controller = new AbortController();
|
|
8890
|
+
let rejectCancelled;
|
|
8891
|
+
const interrupted2 = new Promise((_resolve, reject) => {
|
|
8892
|
+
rejectCancelled = reject;
|
|
8893
|
+
});
|
|
8894
|
+
const abort = () => {
|
|
8895
|
+
controller.abort();
|
|
8896
|
+
rejectCancelled(new Error("summary_interrupted"));
|
|
8897
|
+
};
|
|
8898
|
+
const timer = setTimeout(abort, this.options.timeoutMs ?? DEFAULT_SUMMARY_TIMEOUT_MS);
|
|
8899
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
8900
|
+
if (signal?.aborted) abort();
|
|
8901
|
+
try {
|
|
8902
|
+
const completion = await Promise.race([provider.complete({ model, system: summaryInstructions(DEFAULT_MAX_SUMMARY_CHARS), messages: [{ role: "user", content: renderDroppedTranscript(history, { maxTranscriptChars: DEFAULT_MAX_TRANSCRIPT_CHARS }) }], signal: controller.signal, context: { purpose: "compaction_summary", compactedMessages: history.length } }), interrupted2]);
|
|
8903
|
+
if (controller.signal.aborted) {
|
|
8904
|
+
trace.end({ outcome: "skipped", reason: "interrupted" });
|
|
8905
|
+
return unchanged();
|
|
8906
|
+
}
|
|
8907
|
+
const summary = completion.text.trim();
|
|
8908
|
+
if (!summary || summary.length > DEFAULT_MAX_SUMMARY_CHARS || summary.length + 300 >= removedChars) {
|
|
8909
|
+
trace.end({ outcome: "skipped", reason: "invalid-summary" });
|
|
8910
|
+
return unchanged();
|
|
8911
|
+
}
|
|
8912
|
+
const removed = new Set(history);
|
|
8913
|
+
const compacted = messages.filter((message) => !removed.has(message));
|
|
8914
|
+
const firstRemoved = messages.findIndex((message) => removed.has(message));
|
|
8915
|
+
const insertion = messages.slice(0, firstRemoved).filter((message) => !removed.has(message)).length;
|
|
8916
|
+
compacted.splice(insertion, 0, compactionSummaryMessage(summary));
|
|
8917
|
+
if (!completeContextUnits(compacted)) {
|
|
8918
|
+
trace.end({ outcome: "skipped", reason: "invalid-pairing" });
|
|
8919
|
+
return unchanged();
|
|
8920
|
+
}
|
|
8921
|
+
trace.end({ outcome: "compacted", summaryChars: summary.length });
|
|
8922
|
+
return { messages: compacted, compacted: true };
|
|
8923
|
+
} catch {
|
|
8924
|
+
trace.end({ outcome: "failed", reason: controller.signal.aborted ? "interrupted" : "summary-failed" });
|
|
8925
|
+
return unchanged();
|
|
8926
|
+
} finally {
|
|
8927
|
+
clearTimeout(timer);
|
|
8928
|
+
signal?.removeEventListener("abort", abort);
|
|
8929
|
+
}
|
|
8930
|
+
}
|
|
8931
|
+
};
|
|
8932
|
+
function snapshotMessages(messages) {
|
|
8933
|
+
const result = [];
|
|
8934
|
+
for (let index = 0; index < messages.length; index += 1) {
|
|
8935
|
+
const message = messages[index];
|
|
8936
|
+
const native = message;
|
|
8937
|
+
if (native.role !== "assistant" || !native.toolCalls?.length) {
|
|
8938
|
+
result.push(message);
|
|
8939
|
+
continue;
|
|
8940
|
+
}
|
|
8941
|
+
const replies = [];
|
|
8942
|
+
while (messages[index + 1]?.role === "tool") replies.push(messages[++index]);
|
|
8943
|
+
const ids = new Set(replies.map((reply) => reply.toolCallId));
|
|
8944
|
+
const calls = native.toolCalls.filter((call) => ids.has(call.id));
|
|
8945
|
+
const completedIds = new Set(calls.map((call) => call.id));
|
|
8946
|
+
const base = { role: native.role, content: native.content };
|
|
8947
|
+
result.push({ ...base, ...calls.length ? { toolCalls: calls } : {} });
|
|
8948
|
+
result.push(...replies.filter((reply) => completedIds.has(reply.toolCallId)));
|
|
8949
|
+
}
|
|
8950
|
+
return result;
|
|
8951
|
+
}
|
|
8952
|
+
function snapshotWithConstraints(messages, constraints) {
|
|
8953
|
+
const snapshot = snapshotMessages(messages);
|
|
8954
|
+
for (const constraint of constraints) if (!snapshot.some((message) => message.role === "user" && message.content === constraint.content)) snapshot.push({ role: "user", content: constraint.content });
|
|
8955
|
+
return snapshot;
|
|
8956
|
+
}
|
|
8957
|
+
|
|
8799
8958
|
// src/core/progress-events.ts
|
|
8800
8959
|
var listeners = /* @__PURE__ */ new Set();
|
|
8801
8960
|
function onAgentProgress(listener) {
|
|
@@ -8962,18 +9121,18 @@ function isConcurrencySafe(tool) {
|
|
|
8962
9121
|
}
|
|
8963
9122
|
function planToolBatches(calls, tools) {
|
|
8964
9123
|
const batches = [];
|
|
8965
|
-
let
|
|
9124
|
+
let open9;
|
|
8966
9125
|
for (const call of calls) {
|
|
8967
9126
|
if (!isConcurrencySafe(tools.get(call.tool))) {
|
|
8968
9127
|
batches.push([call]);
|
|
8969
|
-
|
|
9128
|
+
open9 = void 0;
|
|
8970
9129
|
continue;
|
|
8971
9130
|
}
|
|
8972
|
-
if (!
|
|
8973
|
-
|
|
8974
|
-
batches.push(
|
|
9131
|
+
if (!open9) {
|
|
9132
|
+
open9 = [];
|
|
9133
|
+
batches.push(open9);
|
|
8975
9134
|
}
|
|
8976
|
-
|
|
9135
|
+
open9.push(call);
|
|
8977
9136
|
}
|
|
8978
9137
|
return batches;
|
|
8979
9138
|
}
|
|
@@ -9511,6 +9670,17 @@ function readSessionUserConstraints(session) {
|
|
|
9511
9670
|
}
|
|
9512
9671
|
return constraints;
|
|
9513
9672
|
}
|
|
9673
|
+
function readSessionContextSnapshot(session) {
|
|
9674
|
+
if (session.contextSnapshot !== void 0) {
|
|
9675
|
+
const snapshot = session.contextSnapshot;
|
|
9676
|
+
if (!snapshot || typeof snapshot !== "object" || snapshot.version !== 1 || !["native", "text", "run"].includes(snapshot.kind) || !Number.isSafeInteger(snapshot.completedSteps) || snapshot.completedSteps < 0 || !Array.isArray(snapshot.messages) || snapshot.messages.some((message) => {
|
|
9677
|
+
if (!message || typeof message !== "object" || !["user", "assistant", "system", "tool"].includes(message.role) || typeof message.content !== "string") return true;
|
|
9678
|
+
const native = message;
|
|
9679
|
+
return snapshot.kind !== "native" && (message.role === "system" || native.toolCalls !== void 0 || native.toolCallId !== void 0) || native.toolCalls !== void 0 && message.role !== "assistant" || native.toolCallId !== void 0 && message.role !== "tool" || snapshot.kind === "native" && message.role === "tool" && typeof native.toolCallId !== "string" || native.toolCallId !== void 0 && typeof native.toolCallId !== "string" || native.toolCalls !== void 0 && (!Array.isArray(native.toolCalls) || native.toolCalls.some((call) => !call || typeof call.id !== "string" || typeof call.name !== "string" || typeof call.arguments !== "string"));
|
|
9680
|
+
}) || !completeContextUnits(snapshot.messages) || (readSessionUserConstraints(session) ?? [{ content: session.task }]).some((constraint) => !snapshot.messages.some((message) => message.role === "user" && message.content === constraint.content))) throw new Error(`Session ${session.id} has an invalid saved context snapshot and cannot be continued safely.`);
|
|
9681
|
+
}
|
|
9682
|
+
return session.contextSnapshot;
|
|
9683
|
+
}
|
|
9514
9684
|
function createSessionId(date = /* @__PURE__ */ new Date()) {
|
|
9515
9685
|
const stamp = date.toISOString().replace(/[:.]/g, "-");
|
|
9516
9686
|
return `${stamp}-${randomBytes3(4).toString("hex")}`;
|
|
@@ -9788,6 +9958,7 @@ function validateSession(value, expectedId, label) {
|
|
|
9788
9958
|
if (!isSafeSessionId(sessionId) || sessionId !== expectedId || session.parentSessionId !== void 0 && (typeof session.parentSessionId !== "string" || !isSafeSessionId(session.parentSessionId) || normalizedSessionId(session.parentSessionId) !== session.parentSessionId) || typeof session.task !== "string" || typeof session.workspace !== "string" || typeof session.provider !== "string" || typeof session.model !== "string" || typeof session.startedAt !== "string" || typeof session.endedAt !== "string" || typeof session.finalMessage !== "string" || !isToolPermissions(session.permissions) || !Array.isArray(session.steps) || session.mcpResultArchiveVersion !== void 0 && session.mcpResultArchiveVersion !== 1 || session.mode !== void 0 && !isAgentModeName2(session.mode) || session.stopReason !== void 0 && !isStopReason(session.stopReason) || session.subagents !== void 0 && !Array.isArray(session.subagents)) {
|
|
9789
9959
|
invalid();
|
|
9790
9960
|
}
|
|
9961
|
+
readSessionContextSnapshot(value);
|
|
9791
9962
|
const steps = session.steps;
|
|
9792
9963
|
for (const stepValue of steps) {
|
|
9793
9964
|
if (!stepValue || typeof stepValue !== "object" || Array.isArray(stepValue)) invalid();
|
|
@@ -9864,59 +10035,41 @@ var AgentRunner = class {
|
|
|
9864
10035
|
const mode = options.mode ?? "default";
|
|
9865
10036
|
const modePreset = getAgentModePreset(mode);
|
|
9866
10037
|
const modePermissions = getEffectiveModePermissions(mode, options.permissions);
|
|
9867
|
-
const
|
|
9868
|
-
|
|
9869
|
-
|
|
9870
|
-
|
|
9871
|
-
|
|
9872
|
-
|
|
9873
|
-
|
|
9874
|
-
this.registry.execute({ tool: "repo_map", input: { maxItems: 10 } }, readOnlyContext),
|
|
9875
|
-
this.registry.execute({ tool: "repo_handbook", input: { maxFilesPerBehavior: 6 } }, readOnlyContext),
|
|
9876
|
-
this.registry.execute({ tool: "verification_plan", input: {} }, readOnlyContext),
|
|
9877
|
-
this.registry.execute({ tool: "git_worktree_summary", input: { includeStats: false } }, readOnlyContext),
|
|
9878
|
-
this.registry.execute(
|
|
9879
|
-
{ tool: "list_files", input: { patterns: ["**/*"], includeHidden: false, maxResults: 80 } },
|
|
9880
|
-
readOnlyContext
|
|
9881
|
-
)
|
|
9882
|
-
]);
|
|
9883
|
-
const [memory, skills] = await Promise.all([
|
|
9884
|
-
loadMemoryContext(options.workspace),
|
|
9885
|
-
this.registry.list().some((tool) => tool.name === "skill") ? loadSkillsContext(options.workspace) : Promise.resolve("Skills are unavailable in this tool set.")
|
|
9886
|
-
]);
|
|
10038
|
+
const registered = this.registry.list();
|
|
10039
|
+
const advertised = resolveAdvertisedTools(registered, mode, options.tools ?? "mode");
|
|
10040
|
+
const workspaceContext = await gatherWorkspaceContext(
|
|
10041
|
+
this.registry,
|
|
10042
|
+
options.workspace,
|
|
10043
|
+
advertised.map((tool) => tool.name)
|
|
10044
|
+
);
|
|
9887
10045
|
const fileProvenance = /* @__PURE__ */ new WeakMap();
|
|
9888
10046
|
const restored = restorePreviousTranscript(previousSessions, options.maxToolResultChars, fileProvenance);
|
|
9889
10047
|
if (previousSession) {
|
|
9890
10048
|
noteContextEvent(describeRestore(previousSession, previousSessions, restored, priorConstraints));
|
|
9891
10049
|
}
|
|
10050
|
+
const workspaceMessage = {
|
|
10051
|
+
role: "user",
|
|
10052
|
+
content: workspaceContext
|
|
10053
|
+
};
|
|
9892
10054
|
const taskMessage = {
|
|
9893
10055
|
role: "user",
|
|
9894
|
-
content:
|
|
9895
|
-
options.task,
|
|
9896
|
-
options.workspace,
|
|
9897
|
-
contextSection(project),
|
|
9898
|
-
contextSection(repoMap),
|
|
9899
|
-
contextSection(repoHandbook),
|
|
9900
|
-
contextSection(verificationPlan),
|
|
9901
|
-
worktree.ok ? contextSection(worktree) : "No Git worktree detected.",
|
|
9902
|
-
contextSection(snapshot),
|
|
9903
|
-
modePreset.label,
|
|
9904
|
-
memory,
|
|
9905
|
-
skills,
|
|
9906
|
-
options.previousSession,
|
|
9907
|
-
restored
|
|
9908
|
-
)
|
|
10056
|
+
content: buildTaskUserMessage(options.task, modePreset.label, options.previousSession, restored)
|
|
9909
10057
|
};
|
|
10058
|
+
fileProvenance.set(workspaceMessage, unknownContextProvenance());
|
|
9910
10059
|
fileProvenance.set(taskMessage, unknownContextProvenance());
|
|
9911
10060
|
const constraintMessages = inheritedConstraints.map((constraint) => ({
|
|
9912
10061
|
role: "user",
|
|
9913
10062
|
content: constraint.content
|
|
9914
10063
|
}));
|
|
9915
|
-
const messages = [
|
|
10064
|
+
const messages = [
|
|
10065
|
+
...restored.messages,
|
|
10066
|
+
...constraintMessages,
|
|
10067
|
+
workspaceMessage,
|
|
10068
|
+
taskMessage
|
|
10069
|
+
];
|
|
9916
10070
|
const pinnedMessages = [...constraintMessages];
|
|
9917
10071
|
const steps = [];
|
|
9918
|
-
const
|
|
9919
|
-
const advertised = resolveAdvertisedTools(registered, mode, options.tools ?? "mode");
|
|
10072
|
+
const periodicCompactor = new PeriodicCompactor();
|
|
9920
10073
|
const toolsByName = new Map(
|
|
9921
10074
|
registered.map((tool) => [tool.name, tool])
|
|
9922
10075
|
);
|
|
@@ -9939,7 +10092,8 @@ var AgentRunner = class {
|
|
|
9939
10092
|
...subagents2.length ? { subagents: subagents2 } : {},
|
|
9940
10093
|
constraintVersion: 1,
|
|
9941
10094
|
userConstraints: [...userConstraints],
|
|
9942
|
-
mcpResultArchiveVersion: 1
|
|
10095
|
+
mcpResultArchiveVersion: 1,
|
|
10096
|
+
...messages.some((message) => message.role === "user" && message.content.startsWith("[compacted-context]")) ? { contextSnapshot: { version: 1, kind: "run", messages: snapshotWithConstraints(messages, userConstraints), completedSteps: steps.length } } : {}
|
|
9943
10097
|
});
|
|
9944
10098
|
const depth = options.depth ?? 0;
|
|
9945
10099
|
const isTopLevel = depth === 0;
|
|
@@ -9948,6 +10102,7 @@ var AgentRunner = class {
|
|
|
9948
10102
|
try {
|
|
9949
10103
|
await options.onCheckpoint(buildSession(void 0, listSubagentTranscripts()));
|
|
9950
10104
|
} catch {
|
|
10105
|
+
diagnosticEvent("session.checkpoint", { outcome: "failed", path: "run" });
|
|
9951
10106
|
}
|
|
9952
10107
|
};
|
|
9953
10108
|
let stopReason = "max-steps";
|
|
@@ -9957,7 +10112,7 @@ var AgentRunner = class {
|
|
|
9957
10112
|
break;
|
|
9958
10113
|
}
|
|
9959
10114
|
const nativeTools = options.nativeTools !== false && Boolean(options.provider.supportsToolCalling && options.provider.chatWithTools);
|
|
9960
|
-
const system = buildSystemPrompt(advertised, modePermissions, modePreset, nativeTools);
|
|
10115
|
+
const system = buildSystemPrompt(advertised, modePermissions, modePreset, nativeTools, options.autonomous);
|
|
9961
10116
|
const requestContext = {
|
|
9962
10117
|
turn: index + 1,
|
|
9963
10118
|
mode,
|
|
@@ -9965,6 +10120,11 @@ var AgentRunner = class {
|
|
|
9965
10120
|
transcriptKind: "run",
|
|
9966
10121
|
fileProvenance
|
|
9967
10122
|
};
|
|
10123
|
+
const periodic = await periodicCompactor.compact(messages, [...pinnedMessages, taskMessage], options.provider, options.model, steps.length, options.signal);
|
|
10124
|
+
if (periodic.compacted) {
|
|
10125
|
+
messages.splice(0, messages.length, ...periodic.messages);
|
|
10126
|
+
await checkpoint();
|
|
10127
|
+
}
|
|
9968
10128
|
await compactIfNeeded(options, messages, system, taskMessage, pinnedMessages, requestContext);
|
|
9969
10129
|
if (options.signal?.aborted) {
|
|
9970
10130
|
stopReason = "aborted";
|
|
@@ -10027,6 +10187,8 @@ var AgentRunner = class {
|
|
|
10027
10187
|
role: "assistant",
|
|
10028
10188
|
content: JSON.stringify(parsed)
|
|
10029
10189
|
});
|
|
10190
|
+
steps.push(step);
|
|
10191
|
+
await checkpoint();
|
|
10030
10192
|
const modeAllowlist = getEnforcedModeToolAllowlist(mode);
|
|
10031
10193
|
const toolContext = {
|
|
10032
10194
|
workspace: options.workspace,
|
|
@@ -10055,6 +10217,7 @@ var AgentRunner = class {
|
|
|
10055
10217
|
pinnedMessages,
|
|
10056
10218
|
fileProvenance
|
|
10057
10219
|
);
|
|
10220
|
+
await checkpoint();
|
|
10058
10221
|
if (options.signal?.aborted) {
|
|
10059
10222
|
stopReason = "aborted";
|
|
10060
10223
|
break;
|
|
@@ -10107,6 +10270,7 @@ var AgentRunner = class {
|
|
|
10107
10270
|
pinnedMessages,
|
|
10108
10271
|
fileProvenance
|
|
10109
10272
|
);
|
|
10273
|
+
await checkpoint();
|
|
10110
10274
|
}
|
|
10111
10275
|
publishAgentProgress({ type: "batch-end", batchId });
|
|
10112
10276
|
if (options.signal?.aborted) {
|
|
@@ -10114,8 +10278,6 @@ var AgentRunner = class {
|
|
|
10114
10278
|
break;
|
|
10115
10279
|
}
|
|
10116
10280
|
}
|
|
10117
|
-
steps.push(step);
|
|
10118
|
-
await checkpoint();
|
|
10119
10281
|
if (stopReason === "aborted") break;
|
|
10120
10282
|
if (parsed.done && parsed.toolCalls.length === 0) {
|
|
10121
10283
|
stopReason = "completed";
|
|
@@ -10159,6 +10321,10 @@ async function compactIfNeeded(options, messages, system, task, constraints, con
|
|
|
10159
10321
|
summarisedTurns: plan.summarisedTurns
|
|
10160
10322
|
});
|
|
10161
10323
|
} catch (error) {
|
|
10324
|
+
if (error instanceof PinnedContextBudgetError) {
|
|
10325
|
+
trace.end({ outcome: "budget-error", error });
|
|
10326
|
+
throw error;
|
|
10327
|
+
}
|
|
10162
10328
|
if (!(error instanceof ContextBudgetError)) {
|
|
10163
10329
|
trace.end({ outcome: "error", error });
|
|
10164
10330
|
throw error;
|
|
@@ -10220,52 +10386,28 @@ function capResultForTranscript(result, maxChars) {
|
|
|
10220
10386
|
});
|
|
10221
10387
|
return capped.truncated ? { ...result, content: capped.content } : result;
|
|
10222
10388
|
}
|
|
10223
|
-
|
|
10224
|
-
|
|
10225
|
-
|
|
10226
|
-
|
|
10227
|
-
|
|
10228
|
-
|
|
10229
|
-
|
|
10230
|
-
|
|
10231
|
-
|
|
10232
|
-
|
|
10233
|
-
|
|
10234
|
-
|
|
10235
|
-
|
|
10236
|
-
|
|
10237
|
-
|
|
10238
|
-
const [project, repoMap, repoHandbook, verificationPlan, worktree, snapshot] = await Promise.all([
|
|
10239
|
-
registry.execute({ tool: "inspect_project", input: {} }, readOnlyContext),
|
|
10240
|
-
registry.execute({ tool: "repo_map", input: { maxItems: 10 } }, readOnlyContext),
|
|
10241
|
-
registry.execute({ tool: "repo_handbook", input: { maxFilesPerBehavior: 6 } }, readOnlyContext),
|
|
10242
|
-
registry.execute({ tool: "verification_plan", input: {} }, readOnlyContext),
|
|
10243
|
-
registry.execute({ tool: "git_worktree_summary", input: { includeStats: false } }, readOnlyContext),
|
|
10244
|
-
registry.execute(
|
|
10245
|
-
{ tool: "list_files", input: { patterns: ["**/*"], includeHidden: false, maxResults: 80 } },
|
|
10246
|
-
readOnlyContext
|
|
10247
|
-
)
|
|
10389
|
+
var STARTUP_EXPLORE_TOOLS = [
|
|
10390
|
+
"inspect_project",
|
|
10391
|
+
"repo_map",
|
|
10392
|
+
"repo_handbook",
|
|
10393
|
+
"git_worktree_summary",
|
|
10394
|
+
"git_status",
|
|
10395
|
+
"list_files",
|
|
10396
|
+
"read_file",
|
|
10397
|
+
"read_many_files",
|
|
10398
|
+
"search_files"
|
|
10399
|
+
];
|
|
10400
|
+
async function gatherWorkspaceContext(registry, workspace, advertisedNames) {
|
|
10401
|
+
const [memory, skills] = await Promise.all([
|
|
10402
|
+
loadMemoryContext(workspace),
|
|
10403
|
+
registry.list().some((tool) => tool.name === "skill") ? loadSkillsContext(workspace) : Promise.resolve("Skills are unavailable in this tool set.")
|
|
10248
10404
|
]);
|
|
10249
|
-
const
|
|
10405
|
+
const available = new Set(advertisedNames ?? registry.list().map((tool) => tool.name));
|
|
10406
|
+
const explore = STARTUP_EXPLORE_TOOLS.filter((name) => available.has(name));
|
|
10407
|
+
const exploreLine = explore.length ? `Call ${explore.join(", ")} for the files and structure this task needs. This header is a catalog, not the repository.` : "Inspect the workspace with the advertised tools before acting. This header is a catalog, not the repository.";
|
|
10250
10408
|
return `Workspace: ${workspace}
|
|
10251
10409
|
|
|
10252
|
-
|
|
10253
|
-
${contextSection(project)}
|
|
10254
|
-
|
|
10255
|
-
Repository map:
|
|
10256
|
-
${contextSection(repoMap)}
|
|
10257
|
-
|
|
10258
|
-
Repo handbook:
|
|
10259
|
-
${contextSection(repoHandbook)}
|
|
10260
|
-
|
|
10261
|
-
Verification plan:
|
|
10262
|
-
${contextSection(verificationPlan)}
|
|
10263
|
-
|
|
10264
|
-
Worktree summary:
|
|
10265
|
-
${worktree.ok ? contextSection(worktree) : "No Git worktree detected."}
|
|
10266
|
-
|
|
10267
|
-
Workspace snapshot:
|
|
10268
|
-
${contextSection(snapshot)}
|
|
10410
|
+
${exploreLine}
|
|
10269
10411
|
|
|
10270
10412
|
Memory (stored facts - load a specific one with memory_read):
|
|
10271
10413
|
${memory}
|
|
@@ -10273,42 +10415,21 @@ ${memory}
|
|
|
10273
10415
|
Skills (playbooks - load full steps with the skill tool before acting on a matching task):
|
|
10274
10416
|
${skills}`;
|
|
10275
10417
|
}
|
|
10276
|
-
function
|
|
10418
|
+
function buildTaskUserMessage(task, modeLabel, previousSession, restored) {
|
|
10277
10419
|
const previousContext = previousSession ? `
|
|
10278
10420
|
Previous session:
|
|
10279
10421
|
${summarizePreviousSession(previousSession, restored)}
|
|
10280
10422
|
` : "";
|
|
10281
|
-
return `
|
|
10282
|
-
Mode: ${modeLabel}
|
|
10423
|
+
return `Mode: ${modeLabel}
|
|
10283
10424
|
${previousContext}
|
|
10284
|
-
|
|
10285
|
-
Project summary:
|
|
10286
|
-
${project}
|
|
10287
|
-
|
|
10288
|
-
Repository map:
|
|
10289
|
-
${repoMap}
|
|
10290
|
-
|
|
10291
|
-
Repo handbook:
|
|
10292
|
-
${repoHandbook}
|
|
10293
|
-
|
|
10294
|
-
Verification plan:
|
|
10295
|
-
${verificationPlan}
|
|
10296
|
-
|
|
10297
|
-
Worktree summary:
|
|
10298
|
-
${worktree}
|
|
10299
|
-
|
|
10300
|
-
Workspace snapshot:
|
|
10301
|
-
${snapshot}
|
|
10302
|
-
|
|
10303
|
-
Memory (stored facts - load a specific one with memory_read):
|
|
10304
|
-
${memory}
|
|
10305
|
-
|
|
10306
|
-
Skills (playbooks - load full steps with the skill tool before acting on a matching task):
|
|
10307
|
-
${skills}
|
|
10308
|
-
|
|
10309
10425
|
Task:
|
|
10310
10426
|
${task}`;
|
|
10311
10427
|
}
|
|
10428
|
+
function advertisedExploreGuidance(advertised) {
|
|
10429
|
+
const names = STARTUP_EXPLORE_TOOLS.filter((name) => advertised.has(name));
|
|
10430
|
+
if (!names.length) return void 0;
|
|
10431
|
+
return `The startup header is a catalog, not the repository. Call ${names.join(", ")} for the files and structure this task needs; do not assume a file exists because it was mentioned earlier.`;
|
|
10432
|
+
}
|
|
10312
10433
|
var NOTHING_RESTORED = {
|
|
10313
10434
|
messages: [],
|
|
10314
10435
|
steps: 0,
|
|
@@ -10317,7 +10438,7 @@ var NOTHING_RESTORED = {
|
|
|
10317
10438
|
legacyMcpResults: 0
|
|
10318
10439
|
};
|
|
10319
10440
|
function restorePreviousTranscript(sessions, maxToolResultChars, fileProvenance) {
|
|
10320
|
-
if (sessions.length === 0 || sessions.every((session) => session.steps.length === 0)) return NOTHING_RESTORED;
|
|
10441
|
+
if (sessions.length === 0 || sessions.every((session) => session.steps.length === 0 && !readSessionContextSnapshot(session))) return NOTHING_RESTORED;
|
|
10321
10442
|
const messages = [];
|
|
10322
10443
|
let toolResults = 0;
|
|
10323
10444
|
let truncatedResults = 0;
|
|
@@ -10325,6 +10446,11 @@ function restorePreviousTranscript(sessions, maxToolResultChars, fileProvenance)
|
|
|
10325
10446
|
let steps = 0;
|
|
10326
10447
|
for (const session of sessions) {
|
|
10327
10448
|
steps += session.steps.length;
|
|
10449
|
+
const snapshot = readSessionContextSnapshot(session);
|
|
10450
|
+
if (snapshot?.kind === "run" || snapshot?.kind === "text") {
|
|
10451
|
+
messages.splice(0, messages.length, ...snapshot.messages);
|
|
10452
|
+
continue;
|
|
10453
|
+
}
|
|
10328
10454
|
for (const step of session.steps) {
|
|
10329
10455
|
const content = step.parsed ? JSON.stringify(step.parsed) : step.modelText;
|
|
10330
10456
|
if (content) messages.push({ role: "assistant", content });
|
|
@@ -10339,6 +10465,7 @@ function restorePreviousTranscript(sessions, maxToolResultChars, fileProvenance)
|
|
|
10339
10465
|
fileProvenance?.set(entry.message, toolResultFileProvenance(result, entry.truncated));
|
|
10340
10466
|
}
|
|
10341
10467
|
}
|
|
10468
|
+
if (snapshot) messages.push(...snapshot.messages.filter((message) => message.role === "user" && message.content.startsWith("[compacted-context]")).map((message) => ({ role: "user", content: message.content })));
|
|
10342
10469
|
}
|
|
10343
10470
|
return { messages, steps, toolResults, truncatedResults, legacyMcpResults };
|
|
10344
10471
|
}
|
|
@@ -10372,7 +10499,15 @@ function summarizePreviousSession(session, restored) {
|
|
|
10372
10499
|
function plural3(value, noun) {
|
|
10373
10500
|
return `${value.toLocaleString("en-US")} ${noun}${value === 1 ? "" : "s"}`;
|
|
10374
10501
|
}
|
|
10375
|
-
function
|
|
10502
|
+
function autonomousGuidance(phone, askUserAdvertised) {
|
|
10503
|
+
const ask2 = !askUserAdvertised ? "" : phone ? "\n- ask_user reaches the user's phone and interrupts them. Use it only for what you cannot decide or obtain yourself: a credential, a payment, an irreversible action outside this workspace, or a choice between materially different outcomes. For everything else, decide, state the assumption, and continue." : "\n- ask_user has no one to reach in this run. Decide, state the assumption, and continue.";
|
|
10504
|
+
return `
|
|
10505
|
+
Autonomous run: no human is watching and every permission is granted.
|
|
10506
|
+
- Carry the task through to a verified finish. Do not stop to ask for confirmation, and do not end with a plan, a question or a list of next steps you could do yourself.
|
|
10507
|
+
- When something fails, diagnose it and try another approach before giving up.${ask2}
|
|
10508
|
+
`;
|
|
10509
|
+
}
|
|
10510
|
+
function buildSystemPrompt(tools, permissions, mode, nativeTools = false, autonomous) {
|
|
10376
10511
|
const renderedTools = tools.map((tool) => {
|
|
10377
10512
|
const permissionText = tool.permissions.length ? ` Requires: ${tool.permissions.join(", ")}.` : "";
|
|
10378
10513
|
const optionalText = tool.optionalPermissions.length ? ` Uses when granted: ${tool.optionalPermissions.join(", ")}.` : "";
|
|
@@ -10380,6 +10515,7 @@ function buildSystemPrompt(tools, permissions, mode, nativeTools = false) {
|
|
|
10380
10515
|
}).join("\n");
|
|
10381
10516
|
const advertised = new Set(tools.map((tool) => tool.name));
|
|
10382
10517
|
const guidance = [
|
|
10518
|
+
advertisedExploreGuidance(advertised),
|
|
10383
10519
|
advertised.has("context_bundle") ? "Use context_bundle when a task needs a portable working set for model handoff, eval fixtures, desktop copy flows, or future replay. Save it only when write permission is available or the user asked for an artifact." : void 0,
|
|
10384
10520
|
advertised.has("verification_report") ? "When you have run or accounted for verification, use verification_report to summarize the outcome and evidence. Save it only when write permission is available or the user asked for an artifact." : void 0,
|
|
10385
10521
|
advertised.has("diagnostics") ? "After any edit, call diagnostics with paths set to the files you just wrote, and fix what it reports before declaring the work done." : void 0,
|
|
@@ -10389,11 +10525,12 @@ function buildSystemPrompt(tools, permissions, mode, nativeTools = false) {
|
|
|
10389
10525
|
advertised.has("memory_write") ? "When you learn a durable fact, preference, or a correction from the user, persist it with memory_write so it survives future sessions; read the full text of a relevant memory with memory_read." : void 0,
|
|
10390
10526
|
advertised.has("scan_secrets") ? "For local evidence prefer inspect_artifact, hex_dump, extract_strings, scan_secrets, and file_timeline over a shell. scan_secrets redacts matches; do not round-trip those findings into a write that stores live secrets." : void 0
|
|
10391
10527
|
].filter((line) => Boolean(line));
|
|
10528
|
+
const autonomyLines = autonomous ? autonomousGuidance(autonomous.phone, advertised.has("ask_user")) : "";
|
|
10392
10529
|
return `You are ultrac, the terminal-native coding agent for Ultrac.
|
|
10393
10530
|
|
|
10394
10531
|
Mode: ${mode.label}
|
|
10395
10532
|
${mode.instructions.map((instruction) => `- ${instruction}`).join("\n")}
|
|
10396
|
-
|
|
10533
|
+
${autonomyLines}
|
|
10397
10534
|
${UNTRUSTED_CONTENT_GUIDANCE}
|
|
10398
10535
|
|
|
10399
10536
|
${nativeTools ? "Write complete code files or append complete sections across tool calls for large implementations. Verify files with tests or a compiler. Never replace requested code with placeholders or ellipses. Keep final explanations concise. Use the provided native function tools for actions. Do not put a JSON tool-call protocol in your answer. When the requested work is complete, give the final answer without calling more tools." : `Respond only with a JSON object:
|
|
@@ -13535,7 +13672,10 @@ function buildHarnessManifest() {
|
|
|
13535
13672
|
}
|
|
13536
13673
|
|
|
13537
13674
|
// src/commands/mcp.ts
|
|
13538
|
-
import { resolve as resolve9 } from "path";
|
|
13675
|
+
import { resolve as resolve9, dirname as dirname12, join as join12 } from "path";
|
|
13676
|
+
import { constants as constants5 } from "fs";
|
|
13677
|
+
import { lstat as lstat3, mkdir as mkdir14, open as open8, rename as rename2, unlink as unlink3 } from "fs/promises";
|
|
13678
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
13539
13679
|
import chalk5 from "chalk";
|
|
13540
13680
|
|
|
13541
13681
|
// src/mcp/client.ts
|
|
@@ -13953,24 +14093,24 @@ var McpConnection = class {
|
|
|
13953
14093
|
};
|
|
13954
14094
|
function childEnvFor(spec, source = process.env) {
|
|
13955
14095
|
const { env: sanitized } = createSanitizedProcessEnv(source);
|
|
13956
|
-
const
|
|
14096
|
+
const env2 = { ...sanitized };
|
|
13957
14097
|
for (const key of spec.envPassthrough) {
|
|
13958
14098
|
if (isSensitiveEnvName(key)) continue;
|
|
13959
14099
|
const value = source[key];
|
|
13960
14100
|
if (value !== void 0) {
|
|
13961
|
-
|
|
14101
|
+
env2[key] = value;
|
|
13962
14102
|
}
|
|
13963
14103
|
}
|
|
13964
|
-
Object.assign(
|
|
13965
|
-
return
|
|
14104
|
+
Object.assign(env2, spec.env);
|
|
14105
|
+
return env2;
|
|
13966
14106
|
}
|
|
13967
14107
|
function spawnStdioTransport(spec, workspace) {
|
|
13968
|
-
const
|
|
14108
|
+
const env2 = childEnvFor(spec);
|
|
13969
14109
|
const cwd = spec.cwd ? resolveInsideWorkspace(workspace, spec.cwd) : workspace;
|
|
13970
14110
|
const child = spawn3(spec.command, spec.args, {
|
|
13971
14111
|
cwd,
|
|
13972
14112
|
stdio: ["pipe", "pipe", "pipe"],
|
|
13973
|
-
env,
|
|
14113
|
+
env: env2,
|
|
13974
14114
|
detached: process.platform !== "win32",
|
|
13975
14115
|
windowsHide: true
|
|
13976
14116
|
});
|
|
@@ -14361,7 +14501,47 @@ function asError(error) {
|
|
|
14361
14501
|
|
|
14362
14502
|
// src/commands/mcp.ts
|
|
14363
14503
|
function registerMcpCommand(program2) {
|
|
14364
|
-
const mcpCommand = program2.command("mcp").description("
|
|
14504
|
+
const mcpCommand = program2.command("mcp").description("Configure and inspect workspace MCP servers");
|
|
14505
|
+
mcpCommand.command("add <name> <command> [args...]").description("Add a stdio MCP server without starting it").option("-w, --workspace <path>", "Workspace directory", process.cwd()).option("--env <NAME=value>", "Explicit child environment value; prefer --env-pass for credentials", collect, []).option("--env-pass <NAME>", "Forward a named parent environment variable", collect, []).option("--cwd <path>", "Server working directory").option("--timeout <milliseconds>", "Per-request timeout", "30000").option("--disabled", "Keep the server disabled", false).option("--force", "Replace an existing server with this name", false).action(async (name, command, args, options) => {
|
|
14506
|
+
validateServerName(name);
|
|
14507
|
+
const env2 = {};
|
|
14508
|
+
for (const entry of options.env) {
|
|
14509
|
+
const separator = entry.indexOf("=");
|
|
14510
|
+
if (separator < 1) throw new Error("--env must use NAME=value.");
|
|
14511
|
+
const key = entry.slice(0, separator);
|
|
14512
|
+
validateEnvironmentName(key);
|
|
14513
|
+
Object.defineProperty(env2, key, { value: entry.slice(separator + 1), enumerable: true, configurable: true });
|
|
14514
|
+
}
|
|
14515
|
+
options.envPass.forEach(validateEnvironmentName);
|
|
14516
|
+
const spec = McpServerSchema.parse({
|
|
14517
|
+
command,
|
|
14518
|
+
args,
|
|
14519
|
+
env: env2,
|
|
14520
|
+
envPassthrough: [...new Set(options.envPass)],
|
|
14521
|
+
cwd: options.cwd,
|
|
14522
|
+
enabled: !options.disabled,
|
|
14523
|
+
timeoutMs: Number(options.timeout)
|
|
14524
|
+
});
|
|
14525
|
+
const workspace = await assertWorkspaceExists(options.workspace);
|
|
14526
|
+
await updateMcpConfig(workspace, (config) => {
|
|
14527
|
+
const servers = { ...config.mcpServers ?? {} };
|
|
14528
|
+
if (Object.hasOwn(servers, name) && !options.force) throw new Error(`MCP server ${name} already exists. Use --force to replace it.`);
|
|
14529
|
+
servers[name] = spec;
|
|
14530
|
+
return { ...config, mcpServers: servers };
|
|
14531
|
+
});
|
|
14532
|
+
console.log(`Added MCP server ${name}. Run ultrac trust in this workspace to approve the updated settings.`);
|
|
14533
|
+
});
|
|
14534
|
+
mcpCommand.command("remove <name>").description("Remove a workspace MCP server").option("-w, --workspace <path>", "Workspace directory", process.cwd()).action(async (name, options) => {
|
|
14535
|
+
validateServerName(name);
|
|
14536
|
+
const workspace = await assertWorkspaceExists(options.workspace);
|
|
14537
|
+
await updateMcpConfig(workspace, (config) => {
|
|
14538
|
+
const servers = { ...config.mcpServers ?? {} };
|
|
14539
|
+
if (!Object.hasOwn(servers, name)) throw new Error(`No MCP server named ${name}.`);
|
|
14540
|
+
delete servers[name];
|
|
14541
|
+
return { ...config, mcpServers: servers };
|
|
14542
|
+
});
|
|
14543
|
+
console.log(`Removed MCP server ${name}. Reapprove changed workspace settings with ultrac trust if required.`);
|
|
14544
|
+
});
|
|
14365
14545
|
mcpCommand.command("list", { isDefault: true }).description("Connect every configured server and report what it exports").option("-w, --workspace <path>", "Workspace directory", process.cwd()).option("--json", "Print the result as JSON", false).action(async (options) => {
|
|
14366
14546
|
const workspace = resolve9(options.workspace);
|
|
14367
14547
|
const loaded = await loadConfigWithTrust(workspace);
|
|
@@ -14387,6 +14567,7 @@ function registerMcpCommand(program2) {
|
|
|
14387
14567
|
}
|
|
14388
14568
|
const runtime = await withRuntime(workspace, servers, (active) => active);
|
|
14389
14569
|
try {
|
|
14570
|
+
if (runtime.statuses.some((status) => !status.ok)) process.exitCode = 1;
|
|
14390
14571
|
if (options.json) {
|
|
14391
14572
|
console.log(JSON.stringify({ servers: runtime.statuses }, null, 2));
|
|
14392
14573
|
return;
|
|
@@ -14409,7 +14590,7 @@ function registerMcpCommand(program2) {
|
|
|
14409
14590
|
runtime.close();
|
|
14410
14591
|
}
|
|
14411
14592
|
});
|
|
14412
|
-
mcpCommand.command("call <server> <tool>").description("Call one MCP tool directly, for checking a server by hand").option("-w, --workspace <path>", "Workspace directory", process.cwd()).option("--input <json>", "Tool arguments as a JSON object", "{}").action(async (server, tool, options) => {
|
|
14593
|
+
mcpCommand.command("call <server> <tool>").description("Call one MCP tool directly, for checking a server by hand").option("-w, --workspace <path>", "Workspace directory", process.cwd()).option("--input <json>", "Tool arguments as a JSON object", "{}").option("--json", "Print the result as JSON", false).action(async (server, tool, options) => {
|
|
14413
14594
|
const workspace = resolve9(options.workspace);
|
|
14414
14595
|
const loaded = await loadConfigWithTrust(workspace);
|
|
14415
14596
|
const spec = loaded.config.mcpServers?.[server];
|
|
@@ -14427,6 +14608,7 @@ function registerMcpCommand(program2) {
|
|
|
14427
14608
|
} catch (error) {
|
|
14428
14609
|
throw new Error(`--input must be a JSON object: ${error instanceof Error ? error.message : String(error)}`);
|
|
14429
14610
|
}
|
|
14611
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) throw new Error("--input must be a JSON object.");
|
|
14430
14612
|
const runtime = await withRuntime(workspace, { [server]: spec }, (active) => active);
|
|
14431
14613
|
try {
|
|
14432
14614
|
const status = runtime.statuses[0];
|
|
@@ -14451,6 +14633,68 @@ function registerMcpCommand(program2) {
|
|
|
14451
14633
|
}
|
|
14452
14634
|
});
|
|
14453
14635
|
}
|
|
14636
|
+
function collect(value, previous) {
|
|
14637
|
+
return [...previous, value];
|
|
14638
|
+
}
|
|
14639
|
+
function validateServerName(name) {
|
|
14640
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9_.-]{0,79}$/.test(name) || name.includes("__") || ["constructor", "prototype"].includes(name)) {
|
|
14641
|
+
throw new Error("MCP server names must contain letters, numbers, dots, single underscores, or hyphens.");
|
|
14642
|
+
}
|
|
14643
|
+
}
|
|
14644
|
+
function validateEnvironmentName(name) {
|
|
14645
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) throw new Error("Environment variable names must contain letters, numbers, or underscores.");
|
|
14646
|
+
}
|
|
14647
|
+
async function updateMcpConfig(workspace, update) {
|
|
14648
|
+
const target = configPath(workspace);
|
|
14649
|
+
await resolveInsideWorkspaceReal(workspace, target);
|
|
14650
|
+
await mkdir14(dirname12(target), { recursive: true, mode: 448 });
|
|
14651
|
+
await resolveInsideWorkspaceReal(workspace, target);
|
|
14652
|
+
const lockPath = `${target}.mcp-lock`;
|
|
14653
|
+
const lock = await open8(lockPath, "wx", 384).catch((error) => {
|
|
14654
|
+
if (error.code === "EEXIST") throw new Error("Another MCP configuration update is in progress.");
|
|
14655
|
+
throw error;
|
|
14656
|
+
});
|
|
14657
|
+
const temporary = join12(dirname12(target), `.mcp-config-${randomUUID4()}.tmp`);
|
|
14658
|
+
try {
|
|
14659
|
+
let config = defaultConfig();
|
|
14660
|
+
const existing = await lstat3(target).catch((error) => {
|
|
14661
|
+
if (error.code === "ENOENT") return null;
|
|
14662
|
+
throw error;
|
|
14663
|
+
});
|
|
14664
|
+
if (existing) {
|
|
14665
|
+
if (!existing.isFile() || existing.isSymbolicLink() || existing.size > 1048576) throw new Error("MCP config must be a regular JSON file no larger than 1 MiB.");
|
|
14666
|
+
const handle2 = await open8(target, constants5.O_RDONLY | constants5.O_NOFOLLOW);
|
|
14667
|
+
try {
|
|
14668
|
+
const content = await handle2.readFile("utf8");
|
|
14669
|
+
try {
|
|
14670
|
+
config = JSON.parse(content);
|
|
14671
|
+
} catch {
|
|
14672
|
+
throw new Error("Workspace config is not valid JSON.");
|
|
14673
|
+
}
|
|
14674
|
+
} finally {
|
|
14675
|
+
await handle2.close();
|
|
14676
|
+
}
|
|
14677
|
+
if (!config || typeof config !== "object" || Array.isArray(config)) throw new Error("Workspace config must be a JSON object.");
|
|
14678
|
+
if (!UltracConfigSchema.safeParse(config).success) throw new Error("Workspace config does not match the Ultrac configuration schema.");
|
|
14679
|
+
}
|
|
14680
|
+
const next = update(config);
|
|
14681
|
+
UltracConfigSchema.parse(next);
|
|
14682
|
+
const handle = await open8(temporary, "wx", 384);
|
|
14683
|
+
try {
|
|
14684
|
+
await handle.writeFile(`${JSON.stringify(next, null, 2)}
|
|
14685
|
+
`, "utf8");
|
|
14686
|
+
await handle.sync();
|
|
14687
|
+
} finally {
|
|
14688
|
+
await handle.close();
|
|
14689
|
+
}
|
|
14690
|
+
await resolveInsideWorkspaceReal(workspace, target);
|
|
14691
|
+
await rename2(temporary, target);
|
|
14692
|
+
} finally {
|
|
14693
|
+
await unlink3(temporary).catch(() => void 0);
|
|
14694
|
+
await lock.close();
|
|
14695
|
+
await unlink3(lockPath).catch(() => void 0);
|
|
14696
|
+
}
|
|
14697
|
+
}
|
|
14454
14698
|
async function withRuntime(workspace, servers, use) {
|
|
14455
14699
|
const packageInfo = readPackageInfo();
|
|
14456
14700
|
const runtime = await connectMcpServers(servers, {
|
|
@@ -14562,10 +14806,479 @@ async function readStdin() {
|
|
|
14562
14806
|
return Buffer.concat(chunks).toString("utf8");
|
|
14563
14807
|
}
|
|
14564
14808
|
|
|
14809
|
+
// src/commands/notify.ts
|
|
14810
|
+
import chalk6 from "chalk";
|
|
14811
|
+
|
|
14812
|
+
// src/notify/config.ts
|
|
14813
|
+
import { chmod as chmod3, readFile as readFile12, rm as rm4, writeFile as writeFile11 } from "fs/promises";
|
|
14814
|
+
import { join as join13 } from "path";
|
|
14815
|
+
import { z as z31 } from "zod";
|
|
14816
|
+
var TelegramSchema = z31.object({
|
|
14817
|
+
botToken: z31.string().min(1),
|
|
14818
|
+
chatId: z31.string().min(1)
|
|
14819
|
+
});
|
|
14820
|
+
var TwilioSchema = z31.object({
|
|
14821
|
+
accountSid: z31.string().regex(/^AC[0-9a-fA-F]{32}$/, "Twilio account SIDs start with AC followed by 32 hex characters."),
|
|
14822
|
+
authToken: z31.string().min(1),
|
|
14823
|
+
/** The Twilio number Ultrac texts and calls from, E.164. */
|
|
14824
|
+
from: z31.string().regex(/^\+[1-9]\d{6,14}$/, "Use E.164 format, e.g. +14155550100."),
|
|
14825
|
+
/** The user's phone, E.164. */
|
|
14826
|
+
to: z31.string().regex(/^\+[1-9]\d{6,14}$/, "Use E.164 format, e.g. +919812345678."),
|
|
14827
|
+
/** Escalate an unanswered question to a voice call. */
|
|
14828
|
+
call: z31.boolean().default(true)
|
|
14829
|
+
});
|
|
14830
|
+
var NotifyConfigSchema = z31.object({
|
|
14831
|
+
version: z31.literal(1).default(1),
|
|
14832
|
+
telegram: TelegramSchema.optional(),
|
|
14833
|
+
twilio: TwilioSchema.optional(),
|
|
14834
|
+
/** How long a question waits for a text reply before Ultrac phones. */
|
|
14835
|
+
callAfterSeconds: z31.number().int().min(0).max(3600).default(180),
|
|
14836
|
+
/** How long a question waits in total before the agent proceeds on its own judgement. */
|
|
14837
|
+
answerTimeoutSeconds: z31.number().int().min(30).max(86400).default(1800)
|
|
14838
|
+
});
|
|
14839
|
+
function notifyConfigPath() {
|
|
14840
|
+
return join13(ultracHomeDir(), "notify.json");
|
|
14841
|
+
}
|
|
14842
|
+
async function readStoredNotifyConfig() {
|
|
14843
|
+
let text;
|
|
14844
|
+
try {
|
|
14845
|
+
text = await readFile12(notifyConfigPath(), "utf8");
|
|
14846
|
+
} catch (error) {
|
|
14847
|
+
if (error.code === "ENOENT") return null;
|
|
14848
|
+
throw error;
|
|
14849
|
+
}
|
|
14850
|
+
return NotifyConfigSchema.parse(JSON.parse(text));
|
|
14851
|
+
}
|
|
14852
|
+
async function writeNotifyConfig(config) {
|
|
14853
|
+
await ensureUltracHome();
|
|
14854
|
+
const path = notifyConfigPath();
|
|
14855
|
+
const parsed = NotifyConfigSchema.parse(config);
|
|
14856
|
+
await writeFile11(path, `${JSON.stringify(parsed, null, 2)}
|
|
14857
|
+
`, { mode: 384 });
|
|
14858
|
+
await chmod3(path, 384).catch(() => void 0);
|
|
14859
|
+
return path;
|
|
14860
|
+
}
|
|
14861
|
+
async function removeNotifyConfig() {
|
|
14862
|
+
await rm4(notifyConfigPath(), { force: true });
|
|
14863
|
+
}
|
|
14864
|
+
function env(name) {
|
|
14865
|
+
return process.env[name]?.trim() || void 0;
|
|
14866
|
+
}
|
|
14867
|
+
async function loadNotifyConfig() {
|
|
14868
|
+
const stored = await readStoredNotifyConfig();
|
|
14869
|
+
const base = stored ?? NotifyConfigSchema.parse({});
|
|
14870
|
+
const telegramToken = env("ULTRAC_TELEGRAM_BOT_TOKEN");
|
|
14871
|
+
const telegramChat = env("ULTRAC_TELEGRAM_CHAT_ID");
|
|
14872
|
+
const telegram = telegramToken && telegramChat ? { botToken: telegramToken, chatId: telegramChat } : base.telegram;
|
|
14873
|
+
const sid = env("ULTRAC_TWILIO_ACCOUNT_SID");
|
|
14874
|
+
const token = env("ULTRAC_TWILIO_AUTH_TOKEN");
|
|
14875
|
+
const from = env("ULTRAC_TWILIO_FROM");
|
|
14876
|
+
const to = env("ULTRAC_NOTIFY_PHONE");
|
|
14877
|
+
const twilio = sid && token && from && to ? { accountSid: sid, authToken: token, from, to, call: base.twilio?.call ?? true } : base.twilio;
|
|
14878
|
+
const merged = NotifyConfigSchema.parse({ ...base, telegram, twilio });
|
|
14879
|
+
return merged.telegram || merged.twilio ? merged : null;
|
|
14880
|
+
}
|
|
14881
|
+
function redact(secret) {
|
|
14882
|
+
return secret.length <= 4 ? "****" : `****${secret.slice(-4)}`;
|
|
14883
|
+
}
|
|
14884
|
+
|
|
14885
|
+
// src/notify/channel.ts
|
|
14886
|
+
function sleep(ms, signal) {
|
|
14887
|
+
if (signal?.aborted) return Promise.resolve(false);
|
|
14888
|
+
return new Promise((resolve15) => {
|
|
14889
|
+
const timer = setTimeout(() => {
|
|
14890
|
+
signal?.removeEventListener("abort", onAbort);
|
|
14891
|
+
resolve15(true);
|
|
14892
|
+
}, ms);
|
|
14893
|
+
const onAbort = () => {
|
|
14894
|
+
clearTimeout(timer);
|
|
14895
|
+
resolve15(false);
|
|
14896
|
+
};
|
|
14897
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
14898
|
+
});
|
|
14899
|
+
}
|
|
14900
|
+
function withTimeout2(signal, ms) {
|
|
14901
|
+
const timeout = AbortSignal.timeout(ms);
|
|
14902
|
+
return signal ? AbortSignal.any([signal, timeout]) : timeout;
|
|
14903
|
+
}
|
|
14904
|
+
var NotifyChannelError = class extends Error {
|
|
14905
|
+
constructor(channel, message) {
|
|
14906
|
+
super(`${channel}: ${message}`);
|
|
14907
|
+
this.name = "NotifyChannelError";
|
|
14908
|
+
}
|
|
14909
|
+
};
|
|
14910
|
+
|
|
14911
|
+
// src/notify/telegram.ts
|
|
14912
|
+
var API = "https://api.telegram.org";
|
|
14913
|
+
var LONG_POLL_SECONDS = 25;
|
|
14914
|
+
var RETRY_DELAY_MS = 5e3;
|
|
14915
|
+
var MAX_MESSAGE_CHARS = 4e3;
|
|
14916
|
+
async function callTelegram(fetchFn, botToken, method, body, signal) {
|
|
14917
|
+
const response = await fetchFn(`${API}/bot${botToken}/${method}`, {
|
|
14918
|
+
method: "POST",
|
|
14919
|
+
headers: { "content-type": "application/json" },
|
|
14920
|
+
body: JSON.stringify(body),
|
|
14921
|
+
signal
|
|
14922
|
+
});
|
|
14923
|
+
const json = await response.json().catch(() => ({ ok: false }));
|
|
14924
|
+
if (!response.ok || !json.ok || json.result === void 0) {
|
|
14925
|
+
throw new NotifyChannelError("telegram", json.description ?? `HTTP ${response.status}`);
|
|
14926
|
+
}
|
|
14927
|
+
return json.result;
|
|
14928
|
+
}
|
|
14929
|
+
var TelegramChannel = class {
|
|
14930
|
+
constructor(config, fetchFn = fetch) {
|
|
14931
|
+
this.config = config;
|
|
14932
|
+
this.fetchFn = fetchFn;
|
|
14933
|
+
}
|
|
14934
|
+
config;
|
|
14935
|
+
fetchFn;
|
|
14936
|
+
name = "telegram";
|
|
14937
|
+
offset;
|
|
14938
|
+
async send(text, signal) {
|
|
14939
|
+
await callTelegram(
|
|
14940
|
+
this.fetchFn,
|
|
14941
|
+
this.config.botToken,
|
|
14942
|
+
"sendMessage",
|
|
14943
|
+
{ chat_id: this.config.chatId, text: text.slice(0, MAX_MESSAGE_CHARS) },
|
|
14944
|
+
withTimeout2(signal, 15e3)
|
|
14945
|
+
);
|
|
14946
|
+
}
|
|
14947
|
+
async waitForReply(since, signal) {
|
|
14948
|
+
const sinceSeconds = Math.floor(since.getTime() / 1e3);
|
|
14949
|
+
while (!signal.aborted) {
|
|
14950
|
+
let updates;
|
|
14951
|
+
try {
|
|
14952
|
+
updates = await callTelegram(
|
|
14953
|
+
this.fetchFn,
|
|
14954
|
+
this.config.botToken,
|
|
14955
|
+
"getUpdates",
|
|
14956
|
+
{
|
|
14957
|
+
timeout: LONG_POLL_SECONDS,
|
|
14958
|
+
allowed_updates: ["message"],
|
|
14959
|
+
...this.offset !== void 0 ? { offset: this.offset } : {}
|
|
14960
|
+
},
|
|
14961
|
+
withTimeout2(signal, (LONG_POLL_SECONDS + 10) * 1e3)
|
|
14962
|
+
);
|
|
14963
|
+
} catch {
|
|
14964
|
+
if (signal.aborted) return null;
|
|
14965
|
+
await sleep(RETRY_DELAY_MS, signal);
|
|
14966
|
+
continue;
|
|
14967
|
+
}
|
|
14968
|
+
for (const update of updates) {
|
|
14969
|
+
this.offset = update.update_id + 1;
|
|
14970
|
+
const message = update.message;
|
|
14971
|
+
if (!message?.text) continue;
|
|
14972
|
+
if (String(message.chat.id) !== this.config.chatId) continue;
|
|
14973
|
+
if (message.date < sinceSeconds) continue;
|
|
14974
|
+
return message.text;
|
|
14975
|
+
}
|
|
14976
|
+
}
|
|
14977
|
+
return null;
|
|
14978
|
+
}
|
|
14979
|
+
};
|
|
14980
|
+
async function discoverTelegramChat(botToken, signal, fetchFn = fetch) {
|
|
14981
|
+
const startedSeconds = Math.floor(Date.now() / 1e3) - 60;
|
|
14982
|
+
let offset;
|
|
14983
|
+
while (!signal.aborted) {
|
|
14984
|
+
let updates;
|
|
14985
|
+
try {
|
|
14986
|
+
updates = await callTelegram(
|
|
14987
|
+
fetchFn,
|
|
14988
|
+
botToken,
|
|
14989
|
+
"getUpdates",
|
|
14990
|
+
{ timeout: LONG_POLL_SECONDS, allowed_updates: ["message"], ...offset !== void 0 ? { offset } : {} },
|
|
14991
|
+
withTimeout2(signal, (LONG_POLL_SECONDS + 10) * 1e3)
|
|
14992
|
+
);
|
|
14993
|
+
} catch (error) {
|
|
14994
|
+
if (signal.aborted) return null;
|
|
14995
|
+
throw error;
|
|
14996
|
+
}
|
|
14997
|
+
for (const update of updates) {
|
|
14998
|
+
offset = update.update_id + 1;
|
|
14999
|
+
const message = update.message;
|
|
15000
|
+
if (!message || message.date < startedSeconds) continue;
|
|
15001
|
+
const chat = message.chat;
|
|
15002
|
+
return { chatId: String(chat.id), name: chat.title ?? chat.username ?? chat.first_name ?? String(chat.id) };
|
|
15003
|
+
}
|
|
15004
|
+
}
|
|
15005
|
+
return null;
|
|
15006
|
+
}
|
|
15007
|
+
|
|
15008
|
+
// src/notify/twilio.ts
|
|
15009
|
+
var API2 = "https://api.twilio.com/2010-04-01";
|
|
15010
|
+
var POLL_INTERVAL_MS = 1e4;
|
|
15011
|
+
var MAX_SMS_CHARS = 1500;
|
|
15012
|
+
var MAX_SPOKEN_CHARS = 1e3;
|
|
15013
|
+
function escapeXml(text) {
|
|
15014
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
15015
|
+
}
|
|
15016
|
+
var TwilioChannel = class {
|
|
15017
|
+
constructor(config, fetchFn = fetch) {
|
|
15018
|
+
this.config = config;
|
|
15019
|
+
this.fetchFn = fetchFn;
|
|
15020
|
+
}
|
|
15021
|
+
config;
|
|
15022
|
+
fetchFn;
|
|
15023
|
+
name = "sms";
|
|
15024
|
+
seen = /* @__PURE__ */ new Set();
|
|
15025
|
+
get authHeader() {
|
|
15026
|
+
return `Basic ${Buffer.from(`${this.config.accountSid}:${this.config.authToken}`).toString("base64")}`;
|
|
15027
|
+
}
|
|
15028
|
+
async request(method, path, params, signal) {
|
|
15029
|
+
const search = new URLSearchParams(params);
|
|
15030
|
+
const url = `${API2}/Accounts/${this.config.accountSid}/${path}${method === "GET" ? `?${search}` : ""}`;
|
|
15031
|
+
const response = await this.fetchFn(url, {
|
|
15032
|
+
method,
|
|
15033
|
+
headers: {
|
|
15034
|
+
authorization: this.authHeader,
|
|
15035
|
+
...method === "POST" ? { "content-type": "application/x-www-form-urlencoded" } : {}
|
|
15036
|
+
},
|
|
15037
|
+
...method === "POST" ? { body: search.toString() } : {},
|
|
15038
|
+
signal: withTimeout2(signal, 15e3)
|
|
15039
|
+
});
|
|
15040
|
+
const json = await response.json().catch(() => ({}));
|
|
15041
|
+
if (!response.ok) {
|
|
15042
|
+
throw new NotifyChannelError("twilio", json.message ?? `HTTP ${response.status}`);
|
|
15043
|
+
}
|
|
15044
|
+
return json;
|
|
15045
|
+
}
|
|
15046
|
+
async send(text, signal) {
|
|
15047
|
+
await this.request(
|
|
15048
|
+
"POST",
|
|
15049
|
+
"Messages.json",
|
|
15050
|
+
{ To: this.config.to, From: this.config.from, Body: text.slice(0, MAX_SMS_CHARS) },
|
|
15051
|
+
signal
|
|
15052
|
+
);
|
|
15053
|
+
}
|
|
15054
|
+
async call(text, signal) {
|
|
15055
|
+
if (!this.config.call) return;
|
|
15056
|
+
const spoken = escapeXml(text.slice(0, MAX_SPOKEN_CHARS));
|
|
15057
|
+
const twiml = `<Response><Say>${spoken}</Say><Pause length="2"/><Say>Again. ${spoken}</Say></Response>`;
|
|
15058
|
+
await this.request("POST", "Calls.json", { To: this.config.to, From: this.config.from, Twiml: twiml }, signal);
|
|
15059
|
+
}
|
|
15060
|
+
async waitForReply(since, signal) {
|
|
15061
|
+
const day = since.toISOString().slice(0, 10);
|
|
15062
|
+
while (!signal.aborted) {
|
|
15063
|
+
try {
|
|
15064
|
+
const page = await this.request(
|
|
15065
|
+
"GET",
|
|
15066
|
+
"Messages.json",
|
|
15067
|
+
{ To: this.config.from, From: this.config.to, "DateSent>": day, PageSize: "50" },
|
|
15068
|
+
signal
|
|
15069
|
+
);
|
|
15070
|
+
const replies = page.messages.filter((message) => message.direction === "inbound" && !this.seen.has(message.sid)).map((message) => ({ message, at: new Date(message.date_sent ?? message.date_created) })).filter(({ at }) => at.getTime() >= since.getTime()).sort((left, right) => left.at.getTime() - right.at.getTime());
|
|
15071
|
+
const first = replies[0];
|
|
15072
|
+
if (first) {
|
|
15073
|
+
this.seen.add(first.message.sid);
|
|
15074
|
+
return first.message.body;
|
|
15075
|
+
}
|
|
15076
|
+
} catch {
|
|
15077
|
+
if (signal.aborted) return null;
|
|
15078
|
+
}
|
|
15079
|
+
await sleep(POLL_INTERVAL_MS, signal);
|
|
15080
|
+
}
|
|
15081
|
+
return null;
|
|
15082
|
+
}
|
|
15083
|
+
};
|
|
15084
|
+
|
|
15085
|
+
// src/notify/notifier.ts
|
|
15086
|
+
var CLOCK_SKEW_MS = 5e3;
|
|
15087
|
+
var Notifier = class {
|
|
15088
|
+
constructor(channels, timing, heading = "Ultrac", now = () => /* @__PURE__ */ new Date()) {
|
|
15089
|
+
this.channels = channels;
|
|
15090
|
+
this.timing = timing;
|
|
15091
|
+
this.heading = heading;
|
|
15092
|
+
this.now = now;
|
|
15093
|
+
if (channels.length === 0) throw new Error("A notifier needs at least one channel.");
|
|
15094
|
+
}
|
|
15095
|
+
channels;
|
|
15096
|
+
timing;
|
|
15097
|
+
heading;
|
|
15098
|
+
now;
|
|
15099
|
+
describe() {
|
|
15100
|
+
return this.channels.map((channel) => channel.name).join(" + ");
|
|
15101
|
+
}
|
|
15102
|
+
/**
|
|
15103
|
+
* Fire-and-report: send `text` everywhere and return the channels that
|
|
15104
|
+
* failed. Throws only when every channel failed.
|
|
15105
|
+
*/
|
|
15106
|
+
async send(text, signal) {
|
|
15107
|
+
const results = await Promise.allSettled(this.channels.map((channel) => channel.send(`${this.heading}: ${text}`, signal)));
|
|
15108
|
+
const failures = results.flatMap(
|
|
15109
|
+
(result, index) => result.status === "rejected" ? [`${this.channels[index].name}: ${String(result.reason?.message ?? result.reason)}`] : []
|
|
15110
|
+
);
|
|
15111
|
+
if (failures.length === this.channels.length) {
|
|
15112
|
+
throw new Error(`Could not reach the user on any channel (${failures.join("; ")}).`);
|
|
15113
|
+
}
|
|
15114
|
+
return failures;
|
|
15115
|
+
}
|
|
15116
|
+
async ask(question, options, signal) {
|
|
15117
|
+
const since = new Date(this.now().getTime() - CLOCK_SKEW_MS);
|
|
15118
|
+
const optionLines = options?.length ? `
|
|
15119
|
+
|
|
15120
|
+
${options.map((option, index) => `${index + 1}. ${option}`).join("\n")}
|
|
15121
|
+
|
|
15122
|
+
Reply with a number or your own answer.` : "\n\nReply to this message with your answer.";
|
|
15123
|
+
await this.send(`needs your input.
|
|
15124
|
+
|
|
15125
|
+
${question}${optionLines}`, signal);
|
|
15126
|
+
if (signal?.aborted) return null;
|
|
15127
|
+
const wait = new AbortController();
|
|
15128
|
+
const stop = () => wait.abort();
|
|
15129
|
+
signal?.addEventListener("abort", stop, { once: true });
|
|
15130
|
+
try {
|
|
15131
|
+
const replies = this.channels.map(
|
|
15132
|
+
(channel) => channel.waitForReply(since, wait.signal).catch(() => null).then((reply2) => {
|
|
15133
|
+
if (reply2 === null) return new Promise(() => void 0);
|
|
15134
|
+
return reply2;
|
|
15135
|
+
})
|
|
15136
|
+
);
|
|
15137
|
+
void (async () => {
|
|
15138
|
+
if (!await sleep(this.timing.callAfterMs, wait.signal)) return;
|
|
15139
|
+
const spoken = `This is Ultrac. Your agent needs your input. ${question} ${options?.length ? `Options: ${options.map((option, index) => `${index + 1}, ${option}.`).join(" ")}` : ""} Reply to the text message to answer.`;
|
|
15140
|
+
await Promise.allSettled(this.channels.map((channel) => channel.call?.(spoken, wait.signal)));
|
|
15141
|
+
})();
|
|
15142
|
+
const timeout = sleep(this.timing.timeoutMs, wait.signal).then(() => null);
|
|
15143
|
+
const cancelled = new Promise((resolve15) => wait.signal.addEventListener("abort", () => resolve15(null), { once: true }));
|
|
15144
|
+
const reply = await Promise.race([...replies, timeout, cancelled]);
|
|
15145
|
+
return reply === null ? null : pickOption(reply, options);
|
|
15146
|
+
} finally {
|
|
15147
|
+
signal?.removeEventListener("abort", stop);
|
|
15148
|
+
wait.abort();
|
|
15149
|
+
}
|
|
15150
|
+
}
|
|
15151
|
+
};
|
|
15152
|
+
function pickOption(reply, options) {
|
|
15153
|
+
const answer = reply.trim();
|
|
15154
|
+
const picked = Number.parseInt(answer, 10);
|
|
15155
|
+
if (options?.length && String(picked) === answer && picked >= 1 && picked <= options.length) {
|
|
15156
|
+
return options[picked - 1];
|
|
15157
|
+
}
|
|
15158
|
+
return answer;
|
|
15159
|
+
}
|
|
15160
|
+
function createNotifier(config, heading, fetchFn = fetch) {
|
|
15161
|
+
const channels = [];
|
|
15162
|
+
if (config.telegram) channels.push(new TelegramChannel(config.telegram, fetchFn));
|
|
15163
|
+
if (config.twilio) channels.push(new TwilioChannel(config.twilio, fetchFn));
|
|
15164
|
+
return new Notifier(
|
|
15165
|
+
channels,
|
|
15166
|
+
{ callAfterMs: config.callAfterSeconds * 1e3, timeoutMs: config.answerTimeoutSeconds * 1e3 },
|
|
15167
|
+
heading
|
|
15168
|
+
);
|
|
15169
|
+
}
|
|
15170
|
+
|
|
15171
|
+
// src/commands/notify.ts
|
|
15172
|
+
var DISCOVERY_TIMEOUT_MS = 12e4;
|
|
15173
|
+
function parseSeconds(value, flag) {
|
|
15174
|
+
if (value === void 0) return void 0;
|
|
15175
|
+
const parsed = Number.parseInt(value, 10);
|
|
15176
|
+
if (!Number.isInteger(parsed) || parsed < 0 || String(parsed) !== value.trim()) {
|
|
15177
|
+
throw new Error(`${flag} must be a whole number of seconds.`);
|
|
15178
|
+
}
|
|
15179
|
+
return parsed;
|
|
15180
|
+
}
|
|
15181
|
+
function describeNotifyConfig(config) {
|
|
15182
|
+
const lines = [];
|
|
15183
|
+
if (config.telegram) {
|
|
15184
|
+
lines.push(`telegram chat ${config.telegram.chatId}, bot token ${redact(config.telegram.botToken)}`);
|
|
15185
|
+
}
|
|
15186
|
+
if (config.twilio) {
|
|
15187
|
+
lines.push(
|
|
15188
|
+
`sms ${config.twilio.from} -> ${config.twilio.to}, voice calls ${config.twilio.call ? "on" : "off"}, account ${redact(config.twilio.accountSid)}`
|
|
15189
|
+
);
|
|
15190
|
+
}
|
|
15191
|
+
lines.push(`timing calls after ${config.callAfterSeconds}s unanswered, gives up after ${config.answerTimeoutSeconds}s`);
|
|
15192
|
+
return lines;
|
|
15193
|
+
}
|
|
15194
|
+
function registerNotifyCommand(program2) {
|
|
15195
|
+
const notify = program2.command("notify").description("Let Ultrac reach your phone: questions from autonomous runs, and results when they finish");
|
|
15196
|
+
notify.command("setup").description("Configure Telegram and/or Twilio (SMS + voice calls). Stored in ~/.ultrac/notify.json, never in a workspace").option("--telegram-token <token>", "Telegram bot token from @BotFather").option("--telegram-chat <id>", "Telegram chat id (omit to detect it: send your bot any message during setup)").option("--twilio-sid <sid>", "Twilio account SID").option("--twilio-token <token>", "Twilio auth token").option("--twilio-from <number>", "Your Twilio phone number, E.164 (e.g. +14155550100)").option("--phone <number>", "Your phone number, E.164 (e.g. +919812345678)").option("--no-call", "Text only; never escalate an unanswered question to a voice call").option("--call-after <seconds>", "Seconds a question waits for a text reply before Ultrac calls (default 180)").option("--timeout <seconds>", "Seconds a question waits before the agent proceeds on its own (default 1800)").option("--no-test", "Skip the test message").action(async (options) => {
|
|
15197
|
+
const stored = await readStoredNotifyConfig() ?? NotifyConfigSchema.parse({});
|
|
15198
|
+
const next = { ...stored };
|
|
15199
|
+
if (options.telegramToken) {
|
|
15200
|
+
let chatId = options.telegramChat;
|
|
15201
|
+
if (!chatId) {
|
|
15202
|
+
console.log(chalk6.cyan("Open Telegram and send any message to your bot now. Waiting up to 2 minutes\u2026"));
|
|
15203
|
+
const found = await discoverTelegramChat(options.telegramToken, AbortSignal.timeout(DISCOVERY_TIMEOUT_MS));
|
|
15204
|
+
if (!found) throw new Error("No message reached the bot. Send it a message and run setup again, or pass --telegram-chat.");
|
|
15205
|
+
console.log(chalk6.green(`Found chat: ${found.name} (${found.chatId})`));
|
|
15206
|
+
chatId = found.chatId;
|
|
15207
|
+
}
|
|
15208
|
+
next.telegram = { botToken: options.telegramToken, chatId };
|
|
15209
|
+
} else if (options.telegramChat) {
|
|
15210
|
+
throw new Error("--telegram-chat needs --telegram-token.");
|
|
15211
|
+
}
|
|
15212
|
+
const twilioFlags = [options.twilioSid, options.twilioToken, options.twilioFrom, options.phone];
|
|
15213
|
+
if (twilioFlags.some(Boolean)) {
|
|
15214
|
+
if (!twilioFlags.every(Boolean)) {
|
|
15215
|
+
throw new Error("Twilio needs all four of --twilio-sid, --twilio-token, --twilio-from and --phone.");
|
|
15216
|
+
}
|
|
15217
|
+
next.twilio = {
|
|
15218
|
+
accountSid: options.twilioSid,
|
|
15219
|
+
authToken: options.twilioToken,
|
|
15220
|
+
from: options.twilioFrom,
|
|
15221
|
+
to: options.phone,
|
|
15222
|
+
call: options.call
|
|
15223
|
+
};
|
|
15224
|
+
} else if (next.twilio && options.call === false) {
|
|
15225
|
+
next.twilio = { ...next.twilio, call: false };
|
|
15226
|
+
}
|
|
15227
|
+
const callAfter = parseSeconds(options.callAfter, "--call-after");
|
|
15228
|
+
const timeout = parseSeconds(options.timeout, "--timeout");
|
|
15229
|
+
if (callAfter !== void 0) next.callAfterSeconds = callAfter;
|
|
15230
|
+
if (timeout !== void 0) next.answerTimeoutSeconds = timeout;
|
|
15231
|
+
if (!next.telegram && !next.twilio) {
|
|
15232
|
+
throw new Error(
|
|
15233
|
+
"Configure at least one channel: --telegram-token, or --twilio-sid/--twilio-token/--twilio-from/--phone."
|
|
15234
|
+
);
|
|
15235
|
+
}
|
|
15236
|
+
const path = await writeNotifyConfig(next);
|
|
15237
|
+
console.log(chalk6.green(`Saved ${path}`));
|
|
15238
|
+
for (const line of describeNotifyConfig(NotifyConfigSchema.parse(next))) console.log(` ${line}`);
|
|
15239
|
+
if (options.test) {
|
|
15240
|
+
const failures = await createNotifier(NotifyConfigSchema.parse(next)).send(
|
|
15241
|
+
"is connected. This is where your agent will reach you."
|
|
15242
|
+
);
|
|
15243
|
+
for (const failure of failures) console.error(chalk6.yellow(` ${failure}`));
|
|
15244
|
+
console.log(chalk6.green("Test message sent."));
|
|
15245
|
+
}
|
|
15246
|
+
});
|
|
15247
|
+
notify.command("status").description("Show the configured phone channels").action(async () => {
|
|
15248
|
+
const config = await loadNotifyConfig();
|
|
15249
|
+
if (!config) {
|
|
15250
|
+
console.log("No phone channel configured. Run `ultrac notify setup`.");
|
|
15251
|
+
return;
|
|
15252
|
+
}
|
|
15253
|
+
console.log(chalk6.bold(`Phone channels (${notifyConfigPath()} + environment)`));
|
|
15254
|
+
for (const line of describeNotifyConfig(config)) console.log(` ${line}`);
|
|
15255
|
+
});
|
|
15256
|
+
notify.command("test").description("Send a test message; with --ask, wait for your reply (and call you if it does not come)").option("--ask", "Ask a question and wait for the reply", false).action(async (options) => {
|
|
15257
|
+
const config = await loadNotifyConfig();
|
|
15258
|
+
if (!config) throw new Error("No phone channel configured. Run `ultrac notify setup`.");
|
|
15259
|
+
const notifier = createNotifier(config);
|
|
15260
|
+
if (!options.ask) {
|
|
15261
|
+
const failures = await notifier.send("test message. Your phone channel works.");
|
|
15262
|
+
for (const failure of failures) console.error(chalk6.yellow(failure));
|
|
15263
|
+
console.log(chalk6.green(`Sent via ${notifier.describe()}.`));
|
|
15264
|
+
return;
|
|
15265
|
+
}
|
|
15266
|
+
const controller = new AbortController();
|
|
15267
|
+
process.once("SIGINT", () => controller.abort());
|
|
15268
|
+
console.log(chalk6.cyan(`Asked via ${notifier.describe()}. Reply from your phone (Ctrl-C to stop)\u2026`));
|
|
15269
|
+
const answer = await notifier.ask("This is a test. Reply with anything.", ["Works", "Something is off"], controller.signal);
|
|
15270
|
+
console.log(answer === null ? chalk6.yellow("No reply before the timeout.") : chalk6.green(`Reply: ${answer}`));
|
|
15271
|
+
});
|
|
15272
|
+
notify.command("remove").description("Delete ~/.ultrac/notify.json (environment variables are not affected)").action(async () => {
|
|
15273
|
+
await removeNotifyConfig();
|
|
15274
|
+
console.log(`Removed ${notifyConfigPath()}`);
|
|
15275
|
+
});
|
|
15276
|
+
}
|
|
15277
|
+
|
|
14565
15278
|
// src/commands/repl.ts
|
|
14566
|
-
import { createInterface as createInterface3 } from "readline";
|
|
15279
|
+
import { createInterface as createInterface3, emitKeypressEvents } from "readline";
|
|
14567
15280
|
import { stdin as stdin5, stdout as stdout5 } from "process";
|
|
14568
|
-
import
|
|
15281
|
+
import chalk11 from "chalk";
|
|
14569
15282
|
|
|
14570
15283
|
// src/config/budget.ts
|
|
14571
15284
|
function applyBudgetFlags(flags, config) {
|
|
@@ -14595,7 +15308,7 @@ function parseIntFlag(raw, flag) {
|
|
|
14595
15308
|
|
|
14596
15309
|
// src/commands/trust.ts
|
|
14597
15310
|
import { resolve as resolve11 } from "path";
|
|
14598
|
-
import
|
|
15311
|
+
import chalk7 from "chalk";
|
|
14599
15312
|
|
|
14600
15313
|
// src/ui/confirm.ts
|
|
14601
15314
|
import { stdin as stdin4, stdout as stdout2 } from "process";
|
|
@@ -14666,41 +15379,41 @@ function redactCommand(command) {
|
|
|
14666
15379
|
}
|
|
14667
15380
|
function printTrustReview(workspace, raw, escalations, status) {
|
|
14668
15381
|
if (escalations) {
|
|
14669
|
-
console.log(
|
|
15382
|
+
console.log(chalk7.yellow(`
|
|
14670
15383
|
This workspace's .ultrac/config.json asks for ${describeEscalations(escalations)}.`));
|
|
14671
15384
|
} else if (status === "legacy") {
|
|
14672
|
-
console.log(
|
|
15385
|
+
console.log(chalk7.yellow("\n This workspace has an older approval that was not bound to its config."));
|
|
14673
15386
|
} else if (status === "changed") {
|
|
14674
|
-
console.log(
|
|
15387
|
+
console.log(chalk7.yellow("\n This workspace's security-sensitive config changed since it was approved."));
|
|
14675
15388
|
} else {
|
|
14676
|
-
console.log(
|
|
15389
|
+
console.log(chalk7.dim("\n This workspace's config asks for no additional permissions or MCP servers."));
|
|
14677
15390
|
}
|
|
14678
15391
|
const preview = renderMcpTrustPreview(raw);
|
|
14679
|
-
for (const line of preview.lines) console.log(
|
|
14680
|
-
console.log(
|
|
14681
|
-
console.log(
|
|
14682
|
-
console.log(
|
|
14683
|
-
console.log(
|
|
15392
|
+
for (const line of preview.lines) console.log(chalk7.dim(` ${line}`));
|
|
15393
|
+
console.log(chalk7.dim(" Literal MCP env values are hidden; only variable names are shown."));
|
|
15394
|
+
console.log(chalk7.dim(" MCP processes are not sandboxed and run with your OS user's privileges."));
|
|
15395
|
+
console.log(chalk7.dim(" Argument redaction is best-effort; review .ultrac/config.json locally for secrets before approval."));
|
|
15396
|
+
console.log(chalk7.dim(" Approval is local to this workspace and bound to its current permissions and MCP launch config."));
|
|
14684
15397
|
return { hasInvalidServers: preview.hasInvalidServers };
|
|
14685
15398
|
}
|
|
14686
15399
|
async function offerWorkspaceTrust(workspace, escalations, snapshot, status) {
|
|
14687
15400
|
const raw = await readRawConfig(workspace);
|
|
14688
15401
|
if (workspaceTrustSnapshot(raw).securityFingerprint !== snapshot.securityFingerprint) {
|
|
14689
|
-
console.log(
|
|
15402
|
+
console.log(chalk7.yellow("\n Workspace security config changed during review. Nothing was approved; review it again."));
|
|
14690
15403
|
return false;
|
|
14691
15404
|
}
|
|
14692
15405
|
const { hasInvalidServers } = printTrustReview(workspace, raw, escalations, status);
|
|
14693
15406
|
if (hasInvalidServers) {
|
|
14694
|
-
console.log(
|
|
15407
|
+
console.log(chalk7.yellow(" Invalid MCP entries cannot be approved. Fix the config and review it again.\n"));
|
|
14695
15408
|
return false;
|
|
14696
15409
|
}
|
|
14697
15410
|
const granted = await confirmYesNo(` Trust ${escapeTerminalControls(workspace)}? [y/N]`, false);
|
|
14698
15411
|
if (granted) {
|
|
14699
15412
|
const recorded = await trustWorkspace(workspace, snapshot);
|
|
14700
|
-
console.log(
|
|
15413
|
+
console.log(chalk7.green(` Trusted ${escapeTerminalControls(recorded)}.`));
|
|
14701
15414
|
return true;
|
|
14702
15415
|
}
|
|
14703
|
-
console.log(
|
|
15416
|
+
console.log(chalk7.dim(` Not trusted. Those settings were not applied; run ${chalk7.cyan("ultrac trust")} here to allow them.
|
|
14704
15417
|
`));
|
|
14705
15418
|
return false;
|
|
14706
15419
|
}
|
|
@@ -14709,7 +15422,7 @@ function registerTrustCommand(program2) {
|
|
|
14709
15422
|
if (options.list) {
|
|
14710
15423
|
const trusted = await listTrustedWorkspaces();
|
|
14711
15424
|
if (trusted.length === 0) {
|
|
14712
|
-
console.log(
|
|
15425
|
+
console.log(chalk7.dim("No trusted workspaces."));
|
|
14713
15426
|
return;
|
|
14714
15427
|
}
|
|
14715
15428
|
for (const path of trusted) console.log(escapeTerminalControls(path));
|
|
@@ -14719,7 +15432,7 @@ function registerTrustCommand(program2) {
|
|
|
14719
15432
|
if (options.revoke) {
|
|
14720
15433
|
const had = await untrustWorkspace(workspace);
|
|
14721
15434
|
const safeWorkspace = escapeTerminalControls(workspace);
|
|
14722
|
-
console.log(had ? `No longer trusting ${safeWorkspace}.` :
|
|
15435
|
+
console.log(had ? `No longer trusting ${safeWorkspace}.` : chalk7.dim(`${safeWorkspace} was not trusted.`));
|
|
14723
15436
|
return;
|
|
14724
15437
|
}
|
|
14725
15438
|
const raw = await readRawConfig(workspace);
|
|
@@ -14728,10 +15441,10 @@ function registerTrustCommand(program2) {
|
|
|
14728
15441
|
if (status === "trusted") {
|
|
14729
15442
|
const preview = printTrustReview(workspace, raw, snapshot.escalations, status);
|
|
14730
15443
|
if (preview.hasInvalidServers) {
|
|
14731
|
-
console.log(
|
|
15444
|
+
console.log(chalk7.yellow("This stored approval no longer has valid MCP launch config. Revoke it before changing the config."));
|
|
14732
15445
|
return;
|
|
14733
15446
|
}
|
|
14734
|
-
console.log(
|
|
15447
|
+
console.log(chalk7.green(`Already trusted for ${escapeTerminalControls(workspace)}.`));
|
|
14735
15448
|
return;
|
|
14736
15449
|
}
|
|
14737
15450
|
await offerWorkspaceTrust(workspace, snapshot.escalations, snapshot, status);
|
|
@@ -14741,6 +15454,9 @@ function registerTrustCommand(program2) {
|
|
|
14741
15454
|
// src/core/manual-compaction.ts
|
|
14742
15455
|
async function compactManually(messages, pinned, provider, model, focus = "", signal) {
|
|
14743
15456
|
const retained = new Set(pinned);
|
|
15457
|
+
const units = completeContextUnits(messages);
|
|
15458
|
+
if (!units) throw new Error("Compaction found incomplete tool units; original context was kept.");
|
|
15459
|
+
for (const unit of units) if (unit.some((message) => retained.has(message))) for (const message of unit) retained.add(message);
|
|
14744
15460
|
const history = messages.filter(
|
|
14745
15461
|
(message) => message.role !== "system" && !retained.has(message) && !isCompactionSummary(message)
|
|
14746
15462
|
);
|
|
@@ -14796,6 +15512,10 @@ Give extra attention to this user-supplied focus: ${focus}` : ""
|
|
|
14796
15512
|
}
|
|
14797
15513
|
|
|
14798
15514
|
// src/core/interactive.ts
|
|
15515
|
+
var SKIPPED_FOR_QUEUED_MESSAGE = "Skipped; a new user message was queued after the previous tool. Follow that message instead of retrying this call.";
|
|
15516
|
+
function skippedForQueuedMessage(tool) {
|
|
15517
|
+
return { tool, ok: false, content: SKIPPED_FOR_QUEUED_MESSAGE };
|
|
15518
|
+
}
|
|
14799
15519
|
function permissionDeniedMessage(tool, permission, sticky) {
|
|
14800
15520
|
const denied = `User denied ${permission} permission for ${tool}.`;
|
|
14801
15521
|
if (!sticky) return denied;
|
|
@@ -14854,6 +15574,7 @@ var InteractiveSession = class {
|
|
|
14854
15574
|
started = false;
|
|
14855
15575
|
/** How the last turn ended. Absent until one has. */
|
|
14856
15576
|
lastStopReason;
|
|
15577
|
+
periodicCompactor = new PeriodicCompactor();
|
|
14857
15578
|
pinnedMessages = [];
|
|
14858
15579
|
/** Tools described in this turn's system prompt. `/mode` can change it. */
|
|
14859
15580
|
advertisedTools() {
|
|
@@ -14892,6 +15613,7 @@ var InteractiveSession = class {
|
|
|
14892
15613
|
}
|
|
14893
15614
|
async restoreSessions(sessions) {
|
|
14894
15615
|
if (this.steps.length || this.userConstraints.length) throw new Error("Restore requires a fresh session.");
|
|
15616
|
+
for (const saved of sessions) readSessionContextSnapshot(saved);
|
|
14895
15617
|
await this.start();
|
|
14896
15618
|
this.parentSessionId = sessions.at(-1)?.id;
|
|
14897
15619
|
this.firstTask = sessions.at(-1)?.task ?? "";
|
|
@@ -14912,6 +15634,17 @@ var InteractiveSession = class {
|
|
|
14912
15634
|
for (const { call, result } of step.toolResults) this.messages.push({ role: "tool", content: JSON.stringify({ call, result: capResultForTranscript(result, this.options.maxToolResultChars) }) });
|
|
14913
15635
|
}
|
|
14914
15636
|
}
|
|
15637
|
+
const snapshot = sessions.at(-1)?.contextSnapshot;
|
|
15638
|
+
if (snapshot && snapshot.kind !== "text") this.messages.push(...snapshot.messages.filter(isCompactionSummary).map((message) => ({ role: "user", content: message.content })));
|
|
15639
|
+
if (snapshot?.kind === "text") {
|
|
15640
|
+
this.messages.splice(0, this.messages.length, ...snapshot.messages);
|
|
15641
|
+
this.pinnedMessages.splice(0, this.pinnedMessages.length, ...this.messages.filter((message) => message.role === "user" && !isCompactionSummary(message)));
|
|
15642
|
+
for (const constraint of this.userConstraints) if (!this.messages.some((message) => message.role === "user" && message.content === constraint.content)) {
|
|
15643
|
+
const message = { role: "user", content: constraint.content };
|
|
15644
|
+
this.messages.push(message);
|
|
15645
|
+
this.pinnedMessages.push(message);
|
|
15646
|
+
}
|
|
15647
|
+
}
|
|
14915
15648
|
}
|
|
14916
15649
|
contextStatus() {
|
|
14917
15650
|
const mode = getAgentModePreset(this.activeMode);
|
|
@@ -14951,7 +15684,11 @@ var InteractiveSession = class {
|
|
|
14951
15684
|
}
|
|
14952
15685
|
async start() {
|
|
14953
15686
|
if (this.started) return;
|
|
14954
|
-
const context = await gatherWorkspaceContext(
|
|
15687
|
+
const context = await gatherWorkspaceContext(
|
|
15688
|
+
this.registry,
|
|
15689
|
+
this.options.workspace,
|
|
15690
|
+
this.advertisedToolNames
|
|
15691
|
+
);
|
|
14955
15692
|
const modeLabel = getAgentModePreset(this.activeMode).label;
|
|
14956
15693
|
const contextMessage = {
|
|
14957
15694
|
role: "user",
|
|
@@ -14965,6 +15702,14 @@ Acknowledge that you have the context and wait for the user's first request.`
|
|
|
14965
15702
|
this.fileProvenance.set(contextMessage, unknownContextProvenance());
|
|
14966
15703
|
this.started = true;
|
|
14967
15704
|
}
|
|
15705
|
+
async checkpoint(handlers) {
|
|
15706
|
+
if (!handlers.onCheckpoint) return;
|
|
15707
|
+
try {
|
|
15708
|
+
await handlers.onCheckpoint(this.toSession());
|
|
15709
|
+
} catch {
|
|
15710
|
+
diagnosticEvent("session.checkpoint", { outcome: "failed", path: "interactive" });
|
|
15711
|
+
}
|
|
15712
|
+
}
|
|
14968
15713
|
appendQueuedMessages(handlers) {
|
|
14969
15714
|
const queued = handlers.drainQueuedMessages?.() ?? [];
|
|
14970
15715
|
let appended = 0;
|
|
@@ -15005,6 +15750,11 @@ Acknowledge that you have the context and wait for the user's first request.`
|
|
|
15005
15750
|
modePreset
|
|
15006
15751
|
);
|
|
15007
15752
|
const turn = this.steps.length + 1;
|
|
15753
|
+
const periodic = await this.periodicCompactor.compact(this.messages, this.pinnedMessages, this.options.provider, this.activeModel, this.steps.length, handlers.signal);
|
|
15754
|
+
if (periodic.compacted) {
|
|
15755
|
+
this.messages.splice(0, this.messages.length, ...periodic.messages);
|
|
15756
|
+
await this.checkpoint(handlers);
|
|
15757
|
+
}
|
|
15008
15758
|
await this.compactIfNeeded(system, handlers, turn);
|
|
15009
15759
|
handlers.onModelStart?.();
|
|
15010
15760
|
let text;
|
|
@@ -15043,12 +15793,15 @@ Acknowledge that you have the context and wait for the user's first request.`
|
|
|
15043
15793
|
finalMessage = parsed.message;
|
|
15044
15794
|
if (parsed.message) handlers.onAssistantMessage?.(parsed.message);
|
|
15045
15795
|
this.messages.push({ role: "assistant", content: JSON.stringify(parsed) });
|
|
15046
|
-
|
|
15796
|
+
this.steps.push(step);
|
|
15797
|
+
await this.checkpoint(handlers);
|
|
15798
|
+
for (let callIndex = 0; callIndex < parsed.toolCalls.length; callIndex += 1) {
|
|
15799
|
+
const call = parsed.toolCalls[callIndex];
|
|
15047
15800
|
if (handlers.signal?.aborted) break;
|
|
15048
15801
|
const authorization = await this.authorize(call, handlers);
|
|
15049
15802
|
if (handlers.signal?.aborted) break;
|
|
15050
15803
|
if (authorization.denied) {
|
|
15051
|
-
const
|
|
15804
|
+
const result = {
|
|
15052
15805
|
tool: call.tool,
|
|
15053
15806
|
ok: false,
|
|
15054
15807
|
content: authorization.message ?? permissionDeniedMessage(
|
|
@@ -15058,24 +15811,34 @@ Acknowledge that you have the context and wait for the user's first request.`
|
|
|
15058
15811
|
)
|
|
15059
15812
|
};
|
|
15060
15813
|
if (authorization.permission) handlers.onToolDenied?.(call, authorization.permission);
|
|
15061
|
-
step.toolResults.push({ call, result
|
|
15062
|
-
this.pushToolResult(call,
|
|
15063
|
-
|
|
15814
|
+
step.toolResults.push({ call, result });
|
|
15815
|
+
this.pushToolResult(call, result);
|
|
15816
|
+
await this.checkpoint(handlers);
|
|
15817
|
+
} else {
|
|
15818
|
+
handlers.onToolStart?.(call);
|
|
15819
|
+
const modeAllowlist = getEnforcedModeToolAllowlist(this.activeMode);
|
|
15820
|
+
const rawResult = await this.registry.execute(call, {
|
|
15821
|
+
workspace: this.options.workspace,
|
|
15822
|
+
permissions: getEffectiveModePermissions(this.activeMode, authorization.permissions),
|
|
15823
|
+
...handlers.signal ? { signal: handlers.signal } : {},
|
|
15824
|
+
...modeAllowlist ? { modeAllowlist } : {}
|
|
15825
|
+
});
|
|
15826
|
+
const result = await this.mcpResultArchive.store(rawResult, this.options.maxToolResultChars);
|
|
15827
|
+
handlers.onToolResult?.(call, result);
|
|
15828
|
+
step.toolResults.push({ call, result });
|
|
15829
|
+
this.pushToolResult(call, result);
|
|
15830
|
+
await this.checkpoint(handlers);
|
|
15831
|
+
}
|
|
15832
|
+
if (this.appendQueuedMessages(handlers) > 0) {
|
|
15833
|
+
for (const skipped of parsed.toolCalls.slice(callIndex + 1)) {
|
|
15834
|
+
const result = skippedForQueuedMessage(skipped.tool);
|
|
15835
|
+
step.toolResults.push({ call: skipped, result });
|
|
15836
|
+
this.pushToolResult(skipped, result);
|
|
15837
|
+
await this.checkpoint(handlers);
|
|
15838
|
+
}
|
|
15839
|
+
break;
|
|
15064
15840
|
}
|
|
15065
|
-
handlers.onToolStart?.(call);
|
|
15066
|
-
const modeAllowlist = getEnforcedModeToolAllowlist(this.activeMode);
|
|
15067
|
-
const rawResult = await this.registry.execute(call, {
|
|
15068
|
-
workspace: this.options.workspace,
|
|
15069
|
-
permissions: getEffectiveModePermissions(this.activeMode, authorization.permissions),
|
|
15070
|
-
...handlers.signal ? { signal: handlers.signal } : {},
|
|
15071
|
-
...modeAllowlist ? { modeAllowlist } : {}
|
|
15072
|
-
});
|
|
15073
|
-
const result = await this.mcpResultArchive.store(rawResult, this.options.maxToolResultChars);
|
|
15074
|
-
handlers.onToolResult?.(call, result);
|
|
15075
|
-
step.toolResults.push({ call, result });
|
|
15076
|
-
this.pushToolResult(call, result);
|
|
15077
15841
|
}
|
|
15078
|
-
this.steps.push(step);
|
|
15079
15842
|
if (handlers.signal?.aborted) {
|
|
15080
15843
|
stopReason = "aborted";
|
|
15081
15844
|
handlers.onInterrupted?.();
|
|
@@ -15134,7 +15897,7 @@ Acknowledge that you have the context and wait for the user's first request.`
|
|
|
15134
15897
|
mode: this.activeMode,
|
|
15135
15898
|
purpose: "turn"
|
|
15136
15899
|
}),
|
|
15137
|
-
// The turn's own signal:
|
|
15900
|
+
// The turn's own signal: Esc abandons the summary, not the turn.
|
|
15138
15901
|
...handlers.signal ? { signal: handlers.signal } : {}
|
|
15139
15902
|
}
|
|
15140
15903
|
);
|
|
@@ -15151,6 +15914,10 @@ Acknowledge that you have the context and wait for the user's first request.`
|
|
|
15151
15914
|
summarisedTurns: plan.summarisedTurns
|
|
15152
15915
|
});
|
|
15153
15916
|
} catch (error) {
|
|
15917
|
+
if (error instanceof PinnedContextBudgetError) {
|
|
15918
|
+
trace.end({ outcome: "budget-error", error });
|
|
15919
|
+
throw error;
|
|
15920
|
+
}
|
|
15154
15921
|
if (!(error instanceof ContextBudgetError)) {
|
|
15155
15922
|
trace.end({ outcome: "error", error });
|
|
15156
15923
|
throw error;
|
|
@@ -15277,6 +16044,7 @@ Acknowledge that you have the context and wait for the user's first request.`
|
|
|
15277
16044
|
constraintVersion: 1,
|
|
15278
16045
|
userConstraints: [...this.userConstraints],
|
|
15279
16046
|
mcpResultArchiveVersion: 1,
|
|
16047
|
+
...this.messages.some(isCompactionSummary) ? { contextSnapshot: { version: 1, kind: "text", messages: snapshotWithConstraints(this.messages, this.userConstraints), completedSteps: this.steps.length } } : {},
|
|
15280
16048
|
// How the last turn ended, so a saved interactive transcript says whether
|
|
15281
16049
|
// its final message is an answer or the place it ran out of room - the
|
|
15282
16050
|
// same question `ultrac run` already answers for a one-shot run.
|
|
@@ -15329,6 +16097,7 @@ var NativeSession = class {
|
|
|
15329
16097
|
started = false;
|
|
15330
16098
|
/** How the last turn ended. Absent until one has. */
|
|
15331
16099
|
lastStopReason;
|
|
16100
|
+
periodicCompactor = new PeriodicCompactor();
|
|
15332
16101
|
pinnedMessages = [];
|
|
15333
16102
|
/**
|
|
15334
16103
|
* The tools this turn advertises. Resolved per turn rather than once in the
|
|
@@ -15370,6 +16139,7 @@ var NativeSession = class {
|
|
|
15370
16139
|
}
|
|
15371
16140
|
async restoreSessions(sessions) {
|
|
15372
16141
|
if (this.steps.length || this.userConstraints.length) throw new Error("Restore requires a fresh session.");
|
|
16142
|
+
for (const saved of sessions) readSessionContextSnapshot(saved);
|
|
15373
16143
|
await this.start();
|
|
15374
16144
|
this.parentSessionId = sessions.at(-1)?.id;
|
|
15375
16145
|
this.firstTask = sessions.at(-1)?.task ?? "";
|
|
@@ -15397,6 +16167,17 @@ var NativeSession = class {
|
|
|
15397
16167
|
}
|
|
15398
16168
|
}
|
|
15399
16169
|
}
|
|
16170
|
+
const snapshot = sessions.at(-1)?.contextSnapshot;
|
|
16171
|
+
if (snapshot && snapshot.kind !== "native") this.messages.push(...snapshot.messages.filter(isCompactionSummary).map((message) => ({ role: "user", content: message.content })));
|
|
16172
|
+
if (snapshot?.kind === "native") {
|
|
16173
|
+
this.messages.splice(0, this.messages.length, ...snapshot.messages);
|
|
16174
|
+
this.pinnedMessages.splice(0, this.pinnedMessages.length, ...this.messages.filter((message) => message.role === "user" && !isCompactionSummary(message)));
|
|
16175
|
+
for (const constraint of this.userConstraints) if (!this.messages.some((message) => message.role === "user" && message.content === constraint.content)) {
|
|
16176
|
+
const message = { role: "user", content: constraint.content };
|
|
16177
|
+
this.messages.push(message);
|
|
16178
|
+
this.pinnedMessages.push(message);
|
|
16179
|
+
}
|
|
16180
|
+
}
|
|
15400
16181
|
}
|
|
15401
16182
|
contextStatus() {
|
|
15402
16183
|
const system = this.systemPrompt();
|
|
@@ -15436,7 +16217,11 @@ var NativeSession = class {
|
|
|
15436
16217
|
}
|
|
15437
16218
|
async start() {
|
|
15438
16219
|
if (this.started) return;
|
|
15439
|
-
const context = await gatherWorkspaceContext(
|
|
16220
|
+
const context = await gatherWorkspaceContext(
|
|
16221
|
+
this.registry,
|
|
16222
|
+
this.options.workspace,
|
|
16223
|
+
this.advertisedToolNames
|
|
16224
|
+
);
|
|
15440
16225
|
this.messages.push({ role: "system", content: this.systemPrompt() });
|
|
15441
16226
|
const contextMessage = {
|
|
15442
16227
|
role: "user",
|
|
@@ -15448,6 +16233,14 @@ ${context}`
|
|
|
15448
16233
|
this.fileProvenance.set(contextMessage, unknownContextProvenance());
|
|
15449
16234
|
this.started = true;
|
|
15450
16235
|
}
|
|
16236
|
+
async checkpoint(handlers) {
|
|
16237
|
+
if (!handlers.onCheckpoint) return;
|
|
16238
|
+
try {
|
|
16239
|
+
await handlers.onCheckpoint(this.toSession());
|
|
16240
|
+
} catch {
|
|
16241
|
+
diagnosticEvent("session.checkpoint", { outcome: "failed", path: "interactive" });
|
|
16242
|
+
}
|
|
16243
|
+
}
|
|
15451
16244
|
appendQueuedMessages(handlers) {
|
|
15452
16245
|
const queued = handlers.drainQueuedMessages?.() ?? [];
|
|
15453
16246
|
let appended = 0;
|
|
@@ -15484,6 +16277,11 @@ ${context}`
|
|
|
15484
16277
|
this.refreshSystemPrompt();
|
|
15485
16278
|
const tools = toToolSchemas(this.advertisedTools());
|
|
15486
16279
|
const turn = this.steps.length + 1;
|
|
16280
|
+
const periodic = await this.periodicCompactor.compact(this.messages, this.pinnedMessages, this.options.provider, this.activeModel, this.steps.length, handlers.signal);
|
|
16281
|
+
if (periodic.compacted) {
|
|
16282
|
+
this.messages.splice(0, this.messages.length, ...periodic.messages);
|
|
16283
|
+
await this.checkpoint(handlers);
|
|
16284
|
+
}
|
|
15487
16285
|
await this.compactIfNeeded(tools, handlers, turn);
|
|
15488
16286
|
handlers.onModelStart?.();
|
|
15489
16287
|
let response;
|
|
@@ -15530,8 +16328,9 @@ ${context}`
|
|
|
15530
16328
|
toolCalls: response.toolCalls
|
|
15531
16329
|
});
|
|
15532
16330
|
}
|
|
16331
|
+
this.steps.push(step);
|
|
16332
|
+
await this.checkpoint(handlers);
|
|
15533
16333
|
if (!response.toolCalls.length) {
|
|
15534
|
-
this.steps.push(step);
|
|
15535
16334
|
if (handlers.signal?.aborted) {
|
|
15536
16335
|
stopReason = "aborted";
|
|
15537
16336
|
handlers.onInterrupted?.();
|
|
@@ -15543,13 +16342,14 @@ ${context}`
|
|
|
15543
16342
|
}
|
|
15544
16343
|
continue;
|
|
15545
16344
|
}
|
|
15546
|
-
for (
|
|
16345
|
+
for (let callIndex = 0; callIndex < response.toolCalls.length; callIndex += 1) {
|
|
16346
|
+
const call = response.toolCalls[callIndex];
|
|
15547
16347
|
if (handlers.signal?.aborted) break;
|
|
15548
16348
|
const displayCall = { tool: call.name, input: safeParseArgs(call.arguments) };
|
|
15549
16349
|
const authorization = await this.authorize(call.name, handlers, displayCall);
|
|
15550
16350
|
if (handlers.signal?.aborted) break;
|
|
15551
16351
|
if (authorization.denied) {
|
|
15552
|
-
const
|
|
16352
|
+
const result = {
|
|
15553
16353
|
tool: call.name,
|
|
15554
16354
|
ok: false,
|
|
15555
16355
|
content: authorization.message ?? permissionDeniedMessage(
|
|
@@ -15559,27 +16359,38 @@ ${context}`
|
|
|
15559
16359
|
)
|
|
15560
16360
|
};
|
|
15561
16361
|
if (authorization.permission) handlers.onToolDenied?.(displayCall, authorization.permission);
|
|
15562
|
-
step.toolResults.push({ call: displayCall, result
|
|
15563
|
-
this.pushToolResult(call,
|
|
15564
|
-
|
|
16362
|
+
step.toolResults.push({ call: displayCall, result });
|
|
16363
|
+
this.pushToolResult(call, result);
|
|
16364
|
+
await this.checkpoint(handlers);
|
|
16365
|
+
} else {
|
|
16366
|
+
handlers.onToolStart?.(displayCall);
|
|
16367
|
+
const modeAllowlist = getEnforcedModeToolAllowlist(this.activeMode);
|
|
16368
|
+
const rawResult = await this.registry.execute(
|
|
16369
|
+
{ tool: call.name, input: displayCall.input },
|
|
16370
|
+
{
|
|
16371
|
+
workspace: this.options.workspace,
|
|
16372
|
+
permissions: getEffectiveModePermissions(this.activeMode, authorization.permissions),
|
|
16373
|
+
...handlers.signal ? { signal: handlers.signal } : {},
|
|
16374
|
+
...modeAllowlist ? { modeAllowlist } : {}
|
|
16375
|
+
}
|
|
16376
|
+
);
|
|
16377
|
+
const result = await this.mcpResultArchive.store(rawResult, this.options.maxToolResultChars);
|
|
16378
|
+
handlers.onToolResult?.(displayCall, result);
|
|
16379
|
+
step.toolResults.push({ call: displayCall, result });
|
|
16380
|
+
this.pushToolResult(call, result);
|
|
16381
|
+
await this.checkpoint(handlers);
|
|
15565
16382
|
}
|
|
15566
|
-
|
|
15567
|
-
|
|
15568
|
-
|
|
15569
|
-
|
|
15570
|
-
|
|
15571
|
-
|
|
15572
|
-
|
|
15573
|
-
...handlers.signal ? { signal: handlers.signal } : {},
|
|
15574
|
-
...modeAllowlist ? { modeAllowlist } : {}
|
|
16383
|
+
if (this.appendQueuedMessages(handlers) > 0) {
|
|
16384
|
+
for (const skipped of response.toolCalls.slice(callIndex + 1)) {
|
|
16385
|
+
const skippedCall = { tool: skipped.name, input: safeParseArgs(skipped.arguments) };
|
|
16386
|
+
const result = skippedForQueuedMessage(skipped.name);
|
|
16387
|
+
step.toolResults.push({ call: skippedCall, result });
|
|
16388
|
+
this.pushToolResult(skipped, result);
|
|
16389
|
+
await this.checkpoint(handlers);
|
|
15575
16390
|
}
|
|
15576
|
-
|
|
15577
|
-
|
|
15578
|
-
handlers.onToolResult?.(displayCall, result);
|
|
15579
|
-
step.toolResults.push({ call: displayCall, result });
|
|
15580
|
-
this.pushToolResult(call, result);
|
|
16391
|
+
break;
|
|
16392
|
+
}
|
|
15581
16393
|
}
|
|
15582
|
-
this.steps.push(step);
|
|
15583
16394
|
if (handlers.signal?.aborted) {
|
|
15584
16395
|
stopReason = "aborted";
|
|
15585
16396
|
handlers.onInterrupted?.();
|
|
@@ -15634,7 +16445,7 @@ ${context}`
|
|
|
15634
16445
|
mode: this.activeMode,
|
|
15635
16446
|
purpose: "turn"
|
|
15636
16447
|
}),
|
|
15637
|
-
// The turn's own signal:
|
|
16448
|
+
// The turn's own signal: Esc abandons the summary, not the turn.
|
|
15638
16449
|
...handlers.signal ? { signal: handlers.signal } : {}
|
|
15639
16450
|
}
|
|
15640
16451
|
);
|
|
@@ -15651,6 +16462,10 @@ ${context}`
|
|
|
15651
16462
|
summarisedTurns: plan.summarisedTurns
|
|
15652
16463
|
});
|
|
15653
16464
|
} catch (error) {
|
|
16465
|
+
if (error instanceof PinnedContextBudgetError) {
|
|
16466
|
+
trace.end({ outcome: "budget-error", error });
|
|
16467
|
+
throw error;
|
|
16468
|
+
}
|
|
15654
16469
|
if (!(error instanceof ContextBudgetError)) {
|
|
15655
16470
|
trace.end({ outcome: "error", error });
|
|
15656
16471
|
throw error;
|
|
@@ -15750,6 +16565,7 @@ ${context}`
|
|
|
15750
16565
|
const mode = getAgentModePreset(this.activeMode);
|
|
15751
16566
|
const advertised = new Set(this.advertisedToolNames);
|
|
15752
16567
|
const guidance = [
|
|
16568
|
+
advertisedExploreGuidance(advertised),
|
|
15753
16569
|
advertised.has("diagnostics") ? "After any edit, call diagnostics with paths set to the files you just wrote, and fix what it reports before declaring the work done." : void 0,
|
|
15754
16570
|
advertised.has("run_tests") && advertised.has("run_package_script") ? "Prefer run_tests over run_package_script when you want classified test failures." : void 0,
|
|
15755
16571
|
advertised.has("task") ? "Delegate search-heavy exploration to task so its transcript stays out of this conversation." : void 0,
|
|
@@ -15793,6 +16609,7 @@ ${guidance.join("\n")}` : ""}`;
|
|
|
15793
16609
|
constraintVersion: 1,
|
|
15794
16610
|
userConstraints: [...this.userConstraints],
|
|
15795
16611
|
mcpResultArchiveVersion: 1,
|
|
16612
|
+
...this.messages.some(isCompactionSummary) ? { contextSnapshot: { version: 1, kind: "native", messages: snapshotWithConstraints(this.messages, this.userConstraints), completedSteps: this.steps.length } } : {},
|
|
15796
16613
|
// How the last turn ended, so a saved interactive transcript says whether
|
|
15797
16614
|
// its final message is an answer or the place it ran out of room - the
|
|
15798
16615
|
// same question `ultrac run` already answers for a one-shot run.
|
|
@@ -15843,8 +16660,18 @@ function createSessionCheckpointWriter(workspace, onFailure) {
|
|
|
15843
16660
|
return Object.assign(writeCheckpoint, { warnFailure });
|
|
15844
16661
|
}
|
|
15845
16662
|
|
|
16663
|
+
// src/ui/ghost.ts
|
|
16664
|
+
var quietSessions = /* @__PURE__ */ new WeakSet();
|
|
16665
|
+
function isGhostEnabled(session) {
|
|
16666
|
+
return quietSessions.has(session);
|
|
16667
|
+
}
|
|
16668
|
+
function setGhostEnabled(session, enabled) {
|
|
16669
|
+
if (enabled) quietSessions.add(session);
|
|
16670
|
+
else quietSessions.delete(session);
|
|
16671
|
+
}
|
|
16672
|
+
|
|
15846
16673
|
// src/ui/output.ts
|
|
15847
|
-
import
|
|
16674
|
+
import chalk8 from "chalk";
|
|
15848
16675
|
var stopReasonDetail = {
|
|
15849
16676
|
"max-steps": "partial answer - hit the step cap before the model said it was done",
|
|
15850
16677
|
aborted: "partial answer - the run was interrupted before the model said it was done",
|
|
@@ -15856,15 +16683,15 @@ function stopReasonLine(stopReason) {
|
|
|
15856
16683
|
const detail = stopReasonDetail[reason] ?? "partial answer";
|
|
15857
16684
|
return `Stopped: ${reason} (${detail})`;
|
|
15858
16685
|
}
|
|
15859
|
-
function printSession(session, sessionPath2) {
|
|
16686
|
+
function printSession(session, sessionPath2, options = {}) {
|
|
15860
16687
|
for (const step of session.steps) {
|
|
15861
16688
|
if (step.parsed?.message) {
|
|
15862
|
-
console.log(
|
|
16689
|
+
console.log(chalk8.gray(`step ${step.index + 1}:`), step.parsed.message);
|
|
15863
16690
|
}
|
|
15864
16691
|
for (const toolResult of step.toolResults) {
|
|
15865
|
-
const status = toolResult.result.ok ?
|
|
15866
|
-
console.log(
|
|
15867
|
-
console.log(
|
|
16692
|
+
const status = toolResult.result.ok ? chalk8.green("ok") : chalk8.red("failed");
|
|
16693
|
+
console.log(chalk8.cyan(toolResult.call.tool), status);
|
|
16694
|
+
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." }))));
|
|
15868
16695
|
}
|
|
15869
16696
|
}
|
|
15870
16697
|
if (session.finalMessage) {
|
|
@@ -15874,30 +16701,44 @@ function printSession(session, sessionPath2) {
|
|
|
15874
16701
|
console.log();
|
|
15875
16702
|
const stopped = stopReasonLine(session.stopReason);
|
|
15876
16703
|
if (stopped) {
|
|
15877
|
-
console.log(
|
|
15878
|
-
console.log(
|
|
16704
|
+
console.log(chalk8.yellow(stopped));
|
|
16705
|
+
console.log(chalk8.gray(`Resume with: ultrac run --continue ${session.id}`));
|
|
15879
16706
|
}
|
|
15880
|
-
console.log(
|
|
16707
|
+
console.log(chalk8.gray(`Session saved: ${sessionPath2}`));
|
|
15881
16708
|
}
|
|
15882
16709
|
function printTools(tools) {
|
|
15883
16710
|
for (const tool of tools) {
|
|
15884
|
-
const permissionText = tool.permissions.length ?
|
|
15885
|
-
const optionalText = tool.optionalPermissions.length ?
|
|
15886
|
-
console.log(`${
|
|
15887
|
-
console.log(
|
|
16711
|
+
const permissionText = tool.permissions.length ? chalk8.yellow(` [${tool.permissions.join(", ")}]`) : "";
|
|
16712
|
+
const optionalText = tool.optionalPermissions.length ? chalk8.gray(` [optional: ${tool.optionalPermissions.join(", ")}]`) : "";
|
|
16713
|
+
console.log(`${chalk8.cyan(tool.name)}${permissionText}${optionalText}`);
|
|
16714
|
+
console.log(chalk8.gray(indent2(tool.description)));
|
|
15888
16715
|
}
|
|
15889
16716
|
}
|
|
15890
|
-
function printToolResult(result) {
|
|
15891
|
-
const status = result.ok ?
|
|
15892
|
-
console.log(`${
|
|
15893
|
-
console.log(
|
|
16717
|
+
function printToolResult(result, options = {}) {
|
|
16718
|
+
const status = result.ok ? chalk8.green("ok") : chalk8.red("failed");
|
|
16719
|
+
console.log(`${chalk8.cyan(result.tool)} ${status}`);
|
|
16720
|
+
console.log(chalk8.gray(indent2(formatToolOutput(result.content, { verbose: options.verboseTools, failed: !result.ok, hint: "Use --verbose-tools or --result-json for full output." }))));
|
|
15894
16721
|
}
|
|
15895
16722
|
function indent2(value) {
|
|
15896
16723
|
return value.split("\n").map((line) => ` ${line}`).join("\n");
|
|
15897
16724
|
}
|
|
16725
|
+
function formatToolOutput(content, options = {}) {
|
|
16726
|
+
if (options.verbose) return content;
|
|
16727
|
+
const lines = content.split(/\r?\n/);
|
|
16728
|
+
const shown = lines.length <= 6 ? lines : options.failed ? [...lines.slice(0, 3), ...lines.slice(-3)] : lines.slice(0, 6);
|
|
16729
|
+
const clipped = shown.some((line) => line.length > 200);
|
|
16730
|
+
const preview = shown.map((line) => line.length > 200 ? `${line.slice(0, 199)}\u2026` : line).join("\n");
|
|
16731
|
+
const hidden = lines.length - shown.length;
|
|
16732
|
+
if (!hidden && !clipped) return preview;
|
|
16733
|
+
return `${preview}
|
|
16734
|
+
\u2026 ${hidden ? `${hidden} more lines` : "Long lines shortened"}. ${options.hint ?? "Full output remains available in the saved session."}`;
|
|
16735
|
+
}
|
|
15898
16736
|
|
|
15899
16737
|
// src/skills/commands.ts
|
|
15900
16738
|
var BUILTIN_SLASH_COMMANDS = [
|
|
16739
|
+
"logs",
|
|
16740
|
+
"ghost",
|
|
16741
|
+
"output",
|
|
15901
16742
|
"usage",
|
|
15902
16743
|
"max-steps",
|
|
15903
16744
|
"resume",
|
|
@@ -16096,7 +16937,7 @@ function formatSkillCommandHelp(index) {
|
|
|
16096
16937
|
|
|
16097
16938
|
// src/ui/progress.ts
|
|
16098
16939
|
import { stdout as stdout3 } from "process";
|
|
16099
|
-
import
|
|
16940
|
+
import chalk9 from "chalk";
|
|
16100
16941
|
var PENDING_GLYPH = "\u25CB";
|
|
16101
16942
|
var DONE_GLYPH = "\u25CF";
|
|
16102
16943
|
var REPAINT_MS = 100;
|
|
@@ -16123,12 +16964,12 @@ function formatElapsed(ms) {
|
|
|
16123
16964
|
}
|
|
16124
16965
|
function formatProgressLine(snapshot) {
|
|
16125
16966
|
const finished = snapshot.done >= snapshot.total;
|
|
16126
|
-
const glyph = finished ?
|
|
16967
|
+
const glyph = finished ? chalk9.green(DONE_GLYPH) : chalk9.cyan(PENDING_GLYPH);
|
|
16127
16968
|
const noun = snapshot.total === 1 ? "agent" : "agents";
|
|
16128
16969
|
const stats = [`${snapshot.done}/${snapshot.total} ${noun} done`, formatElapsed(snapshot.elapsedMs)];
|
|
16129
16970
|
if (snapshot.tokens !== void 0) stats.push(`\u2193 ${formatTokens(snapshot.tokens)} tokens`);
|
|
16130
|
-
const rendered = stats.map((stat11) =>
|
|
16131
|
-
return ` ${glyph} ${
|
|
16971
|
+
const rendered = stats.map((stat11) => chalk9.dim(stat11)).join(chalk9.dim(" \xB7 "));
|
|
16972
|
+
return ` ${glyph} ${chalk9.cyan(snapshot.label)} ${snapshot.description} ${rendered}`;
|
|
16132
16973
|
}
|
|
16133
16974
|
var ProgressLine = class {
|
|
16134
16975
|
constructor(label, description, total) {
|
|
@@ -16264,7 +17105,10 @@ var Spinner = class {
|
|
|
16264
17105
|
label;
|
|
16265
17106
|
timer;
|
|
16266
17107
|
index = 0;
|
|
16267
|
-
|
|
17108
|
+
holdWhile;
|
|
17109
|
+
held = false;
|
|
17110
|
+
start(holdWhile) {
|
|
17111
|
+
this.holdWhile = holdWhile;
|
|
16268
17112
|
if (!stdout4.isTTY || this.timer) return;
|
|
16269
17113
|
this.render();
|
|
16270
17114
|
this.timer = setInterval(() => this.render(), 90);
|
|
@@ -16283,6 +17127,15 @@ var Spinner = class {
|
|
|
16283
17127
|
}
|
|
16284
17128
|
}
|
|
16285
17129
|
render() {
|
|
17130
|
+
if (this.holdWhile?.()) {
|
|
17131
|
+
if (!this.held) {
|
|
17132
|
+
stdout4.clearLine(0);
|
|
17133
|
+
stdout4.cursorTo(0);
|
|
17134
|
+
this.held = true;
|
|
17135
|
+
}
|
|
17136
|
+
return;
|
|
17137
|
+
}
|
|
17138
|
+
this.held = false;
|
|
16286
17139
|
this.index = (this.index + 1) % FRAMES.length;
|
|
16287
17140
|
stdout4.clearLine(0);
|
|
16288
17141
|
stdout4.cursorTo(0);
|
|
@@ -16362,7 +17215,7 @@ function humanize(value) {
|
|
|
16362
17215
|
}
|
|
16363
17216
|
|
|
16364
17217
|
// src/ui/welcome.ts
|
|
16365
|
-
import
|
|
17218
|
+
import chalk10, { Chalk } from "chalk";
|
|
16366
17219
|
var LOGO = [
|
|
16367
17220
|
"\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557",
|
|
16368
17221
|
"\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u255A\u2550\u2550\u2588\u2588\u2554\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D",
|
|
@@ -16373,7 +17226,7 @@ var LOGO = [
|
|
|
16373
17226
|
];
|
|
16374
17227
|
function renderWelcome(info, options = {}) {
|
|
16375
17228
|
const columns = normalizeColumns(options.columns);
|
|
16376
|
-
const paint = new Chalk({ level: options.color === false ? 0 :
|
|
17229
|
+
const paint = new Chalk({ level: options.color === false ? 0 : chalk10.level });
|
|
16377
17230
|
const cardWidth = Math.min(88, Math.max(42, columns));
|
|
16378
17231
|
const innerWidth = cardWidth - 4;
|
|
16379
17232
|
const wide = columns >= 64;
|
|
@@ -16398,12 +17251,12 @@ function renderWelcome(info, options = {}) {
|
|
|
16398
17251
|
lines.push(cardLine(`logs ${shortenMiddle(info.diagnosticPath, Math.max(8, innerWidth - 5))}`, innerWidth, paint.gray));
|
|
16399
17252
|
lines.push(paint.cyan(`\u2570${"\u2500".repeat(cardWidth - 2)}\u256F`));
|
|
16400
17253
|
lines.push(
|
|
16401
|
-
wide ? `${paint.gray(" /help commands")} ${paint.dim("\xB7")} ${paint.gray("
|
|
17254
|
+
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")
|
|
16402
17255
|
);
|
|
16403
17256
|
return lines.join("\n");
|
|
16404
17257
|
}
|
|
16405
17258
|
function renderComposer(columns = 80, color = true) {
|
|
16406
|
-
const paint = new Chalk({ level: color ?
|
|
17259
|
+
const paint = new Chalk({ level: color ? chalk10.level : 0 });
|
|
16407
17260
|
const width = Math.min(88, Math.max(28, normalizeColumns(columns)));
|
|
16408
17261
|
const label = " message ";
|
|
16409
17262
|
const fill = Math.max(1, width - label.length - 2);
|
|
@@ -16436,14 +17289,14 @@ function shortenMiddle(value, width) {
|
|
|
16436
17289
|
// src/update/check.ts
|
|
16437
17290
|
import { spawn as spawn4 } from "child_process";
|
|
16438
17291
|
import { randomBytes as randomBytes4 } from "crypto";
|
|
16439
|
-
import { readFile as
|
|
16440
|
-
import { join as
|
|
17292
|
+
import { readFile as readFile13, rename as rename3, unlink as unlink4, writeFile as writeFile12 } from "fs/promises";
|
|
17293
|
+
import { join as join14 } from "path";
|
|
16441
17294
|
var DEFAULT_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
16442
17295
|
var QUERY_TIMEOUT_MS = 5e3;
|
|
16443
17296
|
var INSTALL_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
16444
17297
|
async function checkForUpdate(options) {
|
|
16445
17298
|
const now = options.now ?? Date.now();
|
|
16446
|
-
const cachePath = options.cachePath ??
|
|
17299
|
+
const cachePath = options.cachePath ?? join14(await ensureUltracHome(), "update-check.json");
|
|
16447
17300
|
const maxAgeMs = options.maxAgeMs ?? DEFAULT_MAX_AGE_MS;
|
|
16448
17301
|
let latestVersion;
|
|
16449
17302
|
if (!options.force) {
|
|
@@ -16511,7 +17364,7 @@ function parseVersion(version) {
|
|
|
16511
17364
|
}
|
|
16512
17365
|
async function readCache(path) {
|
|
16513
17366
|
try {
|
|
16514
|
-
const raw = await
|
|
17367
|
+
const raw = await readFile13(path, "utf8");
|
|
16515
17368
|
if (raw.length > 1024) return void 0;
|
|
16516
17369
|
const parsed = JSON.parse(raw);
|
|
16517
17370
|
if (typeof parsed !== "object" || parsed === null || !("checkedAt" in parsed) || typeof parsed.checkedAt !== "number" || !("latestVersion" in parsed) || parsed.latestVersion !== null && typeof parsed.latestVersion !== "string") {
|
|
@@ -16525,21 +17378,21 @@ async function readCache(path) {
|
|
|
16525
17378
|
async function writeCache(path, cache) {
|
|
16526
17379
|
const temporary = `${path}.${randomBytes4(8).toString("hex")}.tmp`;
|
|
16527
17380
|
try {
|
|
16528
|
-
await
|
|
17381
|
+
await writeFile12(temporary, `${JSON.stringify(cache)}
|
|
16529
17382
|
`, { encoding: "utf8", mode: 384, flag: "wx" });
|
|
16530
|
-
await
|
|
17383
|
+
await rename3(temporary, path);
|
|
16531
17384
|
} catch {
|
|
16532
|
-
await
|
|
17385
|
+
await unlink4(temporary).catch(() => void 0);
|
|
16533
17386
|
}
|
|
16534
17387
|
}
|
|
16535
17388
|
async function runNpm(args, options) {
|
|
16536
|
-
const
|
|
16537
|
-
for (const key of Object.keys(
|
|
16538
|
-
if (key.startsWith("ULTRAC_")) delete
|
|
17389
|
+
const env2 = { ...process.env };
|
|
17390
|
+
for (const key of Object.keys(env2)) {
|
|
17391
|
+
if (key.startsWith("ULTRAC_")) delete env2[key];
|
|
16539
17392
|
}
|
|
16540
17393
|
return new Promise((resolve15) => {
|
|
16541
17394
|
const child = spawn4(process.platform === "win32" ? "npm.cmd" : "npm", args, {
|
|
16542
|
-
env,
|
|
17395
|
+
env: env2,
|
|
16543
17396
|
stdio: options.capture ? ["ignore", "pipe", "ignore"] : "inherit",
|
|
16544
17397
|
windowsHide: true
|
|
16545
17398
|
});
|
|
@@ -16587,11 +17440,18 @@ var HELP = `Commands:
|
|
|
16587
17440
|
/allow <perm> Grant a permission for this session (write, shell, network)
|
|
16588
17441
|
/whoami Show sign-in state
|
|
16589
17442
|
/tools List available tools
|
|
17443
|
+
/logs Show the private diagnostic log path
|
|
17444
|
+
/ghost [on|off|status] Quiet activity output; diagnostics stay enabled
|
|
17445
|
+
/output [number|list] Inspect saved tool output (latest by default)
|
|
16590
17446
|
/memory List and inspect stored memories
|
|
16591
17447
|
/skills List installed skills
|
|
16592
17448
|
/update Show how to install an available CLI update
|
|
16593
17449
|
/clear Start a fresh conversation
|
|
16594
17450
|
/exit, /quit End the session
|
|
17451
|
+
Keys:
|
|
17452
|
+
Type during a turn Queue a follow-up for after the current tool call
|
|
17453
|
+
Esc Stop the current turn; the session stays open
|
|
17454
|
+
Ctrl-C Exit and save the session
|
|
16595
17455
|
Custom commands come from skills that declare a 'command:' key in their
|
|
16596
17456
|
frontmatter; built-ins always win a name clash. Type anything else to talk to Ultrac.`;
|
|
16597
17457
|
var EXIT_REPL_NOT_STARTED = 1;
|
|
@@ -16659,14 +17519,14 @@ async function runRepl(options, initialTask = "") {
|
|
|
16659
17519
|
if (loaded.needsApproval && await offerWorkspaceTrust(workspace, loaded.withheld, loaded.trustSnapshot, loaded.trustStatus)) {
|
|
16660
17520
|
loaded = await loadConfigWithTrust(workspace);
|
|
16661
17521
|
if (loaded.needsApproval) {
|
|
16662
|
-
console.log(
|
|
17522
|
+
console.log(chalk11.yellow(`
|
|
16663
17523
|
${withheldNotice(workspace, loaded.withheld, loaded.trustStatus)}
|
|
16664
17524
|
`));
|
|
16665
17525
|
}
|
|
16666
17526
|
}
|
|
16667
17527
|
const config = loaded.config;
|
|
16668
17528
|
const toolSelection = applyBudgetFlags(options, config);
|
|
16669
|
-
setContextBudgetNotice((message) => console.log(
|
|
17529
|
+
setContextBudgetNotice((message) => console.log(chalk11.yellow(`
|
|
16670
17530
|
! ${message}`)));
|
|
16671
17531
|
const providerName = options.provider ?? config.provider;
|
|
16672
17532
|
if (!isProviderName(providerName)) {
|
|
@@ -16682,8 +17542,8 @@ async function runRepl(options, initialTask = "") {
|
|
|
16682
17542
|
}
|
|
16683
17543
|
const hasFallbackChain = Boolean(config.fallback?.chain?.length);
|
|
16684
17544
|
if (providerName === "ultrac" && !hasFallbackChain && !await resolveUltracCredentials()) {
|
|
16685
|
-
console.log(
|
|
16686
|
-
console.log(`Run ${
|
|
17545
|
+
console.log(chalk11.yellow("You are not signed in to Ultrac."));
|
|
17546
|
+
console.log(`Run ${chalk11.cyan("ultrac login")} first, or start with ${chalk11.cyan("--provider echo")}.`);
|
|
16687
17547
|
process.exitCode = EXIT_REPL_NOT_STARTED;
|
|
16688
17548
|
return;
|
|
16689
17549
|
}
|
|
@@ -16695,26 +17555,26 @@ async function runRepl(options, initialTask = "") {
|
|
|
16695
17555
|
model,
|
|
16696
17556
|
fallback: config.fallback,
|
|
16697
17557
|
onFailover: (event) => {
|
|
16698
|
-
console.log(
|
|
17558
|
+
console.log(chalk11.yellow(formatFailoverNotice(event)));
|
|
16699
17559
|
if (event.to) {
|
|
16700
|
-
console.log(
|
|
17560
|
+
console.log(chalk11.gray(` now answering: provider ${event.to.provider} \xB7 model ${event.to.model}`));
|
|
16701
17561
|
}
|
|
16702
17562
|
},
|
|
16703
17563
|
// A tool-calling turn served by an entry without native tool calling still
|
|
16704
17564
|
// answers, but over the JSON tool protocol. Say so rather than letting the
|
|
16705
17565
|
// banner's "native tools" claim go stale.
|
|
16706
|
-
onDowngrade: (event) => console.log(
|
|
17566
|
+
onDowngrade: (event) => console.log(chalk11.yellow(formatDowngradeNotice(event))),
|
|
16707
17567
|
onRetry: (event) => {
|
|
16708
17568
|
diagnosticEvent("provider.retry", event);
|
|
16709
17569
|
console.log(
|
|
16710
|
-
|
|
17570
|
+
chalk11.yellow(
|
|
16711
17571
|
` retrying ${event.target.provider} (${event.target.model}) in ${event.delayMs}ms after ${event.classification.reason}`
|
|
16712
17572
|
)
|
|
16713
17573
|
);
|
|
16714
17574
|
}
|
|
16715
17575
|
});
|
|
16716
17576
|
} catch (error) {
|
|
16717
|
-
console.log(
|
|
17577
|
+
console.log(chalk11.red(error instanceof Error ? error.message : String(error)));
|
|
16718
17578
|
process.exitCode = EXIT_REPL_NOT_STARTED;
|
|
16719
17579
|
return;
|
|
16720
17580
|
}
|
|
@@ -16746,7 +17606,7 @@ async function runRepl(options, initialTask = "") {
|
|
|
16746
17606
|
permissions: basePermissions
|
|
16747
17607
|
});
|
|
16748
17608
|
const writeCheckpoint = createSessionCheckpointWriter(workspace, (message) => {
|
|
16749
|
-
console.error(
|
|
17609
|
+
console.error(chalk11.yellow(message));
|
|
16750
17610
|
});
|
|
16751
17611
|
const persistSession = async (target = session) => {
|
|
16752
17612
|
const snapshot = target.toSession();
|
|
@@ -16764,7 +17624,7 @@ async function runRepl(options, initialTask = "") {
|
|
|
16764
17624
|
diagnosticPath2
|
|
16765
17625
|
);
|
|
16766
17626
|
if (mcpRuntime && mcpRuntime.statuses.length > 0) {
|
|
16767
|
-
console.log(
|
|
17627
|
+
console.log(chalk11.bold("\nMCP servers"));
|
|
16768
17628
|
console.log(renderMcpStatuses(mcpRuntime.statuses));
|
|
16769
17629
|
}
|
|
16770
17630
|
try {
|
|
@@ -16774,9 +17634,9 @@ async function runRepl(options, initialTask = "") {
|
|
|
16774
17634
|
}
|
|
16775
17635
|
const update = await updateCheck;
|
|
16776
17636
|
if (update?.updateAvailable && update.latestVersion) {
|
|
16777
|
-
console.log(
|
|
17637
|
+
console.log(chalk11.yellow(`
|
|
16778
17638
|
\u2191 Update available: ${update.currentVersion} \u2192 ${update.latestVersion}`));
|
|
16779
|
-
console.log(
|
|
17639
|
+
console.log(chalk11.gray(" Run `ultrac update` after this session."));
|
|
16780
17640
|
}
|
|
16781
17641
|
const rl = createInterface3({ input: stdin5, output: stdout5 });
|
|
16782
17642
|
const reader = new LineReader(rl);
|
|
@@ -16786,13 +17646,13 @@ async function runRepl(options, initialTask = "") {
|
|
|
16786
17646
|
};
|
|
16787
17647
|
const drainQueuedMessages = () => reader.drainQueuedMessages();
|
|
16788
17648
|
setAskUserRuntime(async (text, options2, signal) => {
|
|
16789
|
-
console.log(
|
|
17649
|
+
console.log(chalk11.cyan(`
|
|
16790
17650
|
? ${text}`));
|
|
16791
17651
|
if (options2?.length) {
|
|
16792
|
-
options2.forEach((option, index) => console.log(
|
|
16793
|
-
console.log(
|
|
17652
|
+
options2.forEach((option, index) => console.log(chalk11.gray(` ${index + 1}. ${option}`)));
|
|
17653
|
+
console.log(chalk11.gray(" (type a number, or answer in your own words)"));
|
|
16794
17654
|
}
|
|
16795
|
-
const rawAnswer = await question(
|
|
17655
|
+
const rawAnswer = await question(chalk11.cyan(" \u203A "), signal);
|
|
16796
17656
|
if (signal?.aborted) return null;
|
|
16797
17657
|
const answer = (rawAnswer ?? "").trim();
|
|
16798
17658
|
const picked = Number.parseInt(answer, 10);
|
|
@@ -16803,12 +17663,21 @@ async function runRepl(options, initialTask = "") {
|
|
|
16803
17663
|
});
|
|
16804
17664
|
let running = true;
|
|
16805
17665
|
let turnAbort;
|
|
16806
|
-
|
|
16807
|
-
|
|
17666
|
+
const exitSession = () => {
|
|
17667
|
+
handleReplExit(turnAbort);
|
|
16808
17668
|
running = false;
|
|
16809
|
-
console.log(
|
|
17669
|
+
console.log(chalk11.gray("\nExiting ultrac\u2026"));
|
|
16810
17670
|
rl.close();
|
|
16811
|
-
}
|
|
17671
|
+
};
|
|
17672
|
+
rl.on("SIGINT", exitSession);
|
|
17673
|
+
const onKeypress = (_input, key) => {
|
|
17674
|
+
if (!key || key.name !== "escape" || key.ctrl || key.meta) return;
|
|
17675
|
+
handleReplEscape(turnAbort);
|
|
17676
|
+
};
|
|
17677
|
+
if (stdin5.isTTY) {
|
|
17678
|
+
emitKeypressEvents(stdin5, rl);
|
|
17679
|
+
stdin5.on("keypress", onKeypress);
|
|
17680
|
+
}
|
|
16812
17681
|
const terminate = () => {
|
|
16813
17682
|
running = false;
|
|
16814
17683
|
turnAbort?.abort();
|
|
@@ -16825,9 +17694,9 @@ async function runRepl(options, initialTask = "") {
|
|
|
16825
17694
|
};
|
|
16826
17695
|
try {
|
|
16827
17696
|
if (initialTask) {
|
|
16828
|
-
console.log(
|
|
17697
|
+
console.log(chalk11.cyan(`
|
|
16829
17698
|
\u203A ${initialTask}`));
|
|
16830
|
-
await withInterrupt((signal) => runAndCheckpointTurn(session, initialTask, question, signal, persistSession, drainQueuedMessages));
|
|
17699
|
+
await withInterrupt((signal) => runAndCheckpointTurn(session, initialTask, question, signal, persistSession, drainQueuedMessages, () => reader.composing));
|
|
16831
17700
|
}
|
|
16832
17701
|
while (running) {
|
|
16833
17702
|
if (stdout5.isTTY) {
|
|
@@ -16835,7 +17704,7 @@ async function runRepl(options, initialTask = "") {
|
|
|
16835
17704
|
console.log(composer.frame);
|
|
16836
17705
|
stdout5.write(composer.prompt);
|
|
16837
17706
|
} else {
|
|
16838
|
-
stdout5.write(
|
|
17707
|
+
stdout5.write(chalk11.cyan("\n\u203A "));
|
|
16839
17708
|
}
|
|
16840
17709
|
const line = await reader.next();
|
|
16841
17710
|
if (line === null) break;
|
|
@@ -16852,9 +17721,9 @@ async function runRepl(options, initialTask = "") {
|
|
|
16852
17721
|
permissions: session.currentPermissions
|
|
16853
17722
|
});
|
|
16854
17723
|
resetContextBudgetNotices();
|
|
16855
|
-
console.log(
|
|
17724
|
+
console.log(chalk11.gray("Started a fresh conversation."));
|
|
16856
17725
|
}, async (prompt) => {
|
|
16857
|
-
await runAndCheckpointTurn(session, prompt, question, signal, persistSession, drainQueuedMessages);
|
|
17726
|
+
await runAndCheckpointTurn(session, prompt, question, signal, persistSession, drainQueuedMessages, () => reader.composing);
|
|
16858
17727
|
}, signal, async (id) => {
|
|
16859
17728
|
const lineage = await loadSessionLineage(workspace, id);
|
|
16860
17729
|
const restored = createSession({ model: session.model, mode: session.mode, permissions: session.currentPermissions });
|
|
@@ -16883,35 +17752,43 @@ async function runRepl(options, initialTask = "") {
|
|
|
16883
17752
|
}
|
|
16884
17753
|
continue;
|
|
16885
17754
|
}
|
|
16886
|
-
await withInterrupt((signal) => runAndCheckpointTurn(session, raw, question, signal, persistSession, drainQueuedMessages));
|
|
17755
|
+
await withInterrupt((signal) => runAndCheckpointTurn(session, raw, question, signal, persistSession, drainQueuedMessages, () => reader.composing));
|
|
16887
17756
|
}
|
|
16888
17757
|
} finally {
|
|
16889
17758
|
process.off("SIGTERM", terminate);
|
|
17759
|
+
if (stdin5.isTTY) stdin5.off("keypress", onKeypress);
|
|
16890
17760
|
setAskUserRuntime(void 0);
|
|
16891
17761
|
rl.close();
|
|
16892
17762
|
await saveTranscript(session, writeCheckpoint);
|
|
16893
17763
|
mcpRuntime?.close();
|
|
16894
17764
|
}
|
|
16895
|
-
console.log(
|
|
17765
|
+
console.log(chalk11.gray("Goodbye."));
|
|
16896
17766
|
}
|
|
16897
|
-
async function runTurn(session, input, question, signal, drainQueuedMessages) {
|
|
17767
|
+
async function runTurn(session, input, question, signal, drainQueuedMessages, persist, isComposing) {
|
|
16898
17768
|
const spinner = new Spinner();
|
|
16899
|
-
const
|
|
17769
|
+
const quiet = isGhostEnabled(session);
|
|
17770
|
+
const detachProgress = quiet ? () => {
|
|
17771
|
+
} : attachProgressUi();
|
|
16900
17772
|
let deltaChars = 0;
|
|
16901
17773
|
try {
|
|
16902
17774
|
await session.ask(input, {
|
|
16903
17775
|
...signal ? { signal } : {},
|
|
17776
|
+
...persist ? {
|
|
17777
|
+
onCheckpoint: async () => {
|
|
17778
|
+
await persist(session);
|
|
17779
|
+
}
|
|
17780
|
+
} : {},
|
|
16904
17781
|
onInterrupted: () => {
|
|
16905
17782
|
spinner.stop();
|
|
16906
|
-
console.log(
|
|
17783
|
+
console.log(chalk11.dim("\n Interrupted \u2014 current work stopped. Session preserved."));
|
|
16907
17784
|
},
|
|
16908
17785
|
onModelStart: () => {
|
|
16909
17786
|
deltaChars = 0;
|
|
16910
|
-
spinner.start();
|
|
17787
|
+
if (!quiet) spinner.start(isComposing);
|
|
16911
17788
|
},
|
|
16912
17789
|
onModelDelta: (text) => {
|
|
16913
17790
|
deltaChars += text.length;
|
|
16914
|
-
spinner.update(`thinking (${deltaChars} chars)`);
|
|
17791
|
+
if (!quiet) spinner.update(`thinking (${deltaChars} chars)`);
|
|
16915
17792
|
},
|
|
16916
17793
|
onModelSettled: () => spinner.stop(),
|
|
16917
17794
|
onAssistantMessage: (message) => {
|
|
@@ -16921,41 +17798,41 @@ ${message.trim()}`);
|
|
|
16921
17798
|
}
|
|
16922
17799
|
},
|
|
16923
17800
|
onToolStart: (call) => {
|
|
16924
|
-
console.log(
|
|
17801
|
+
if (!quiet) console.log(chalk11.dim(` ${toolActivityLabel(call)}`));
|
|
16925
17802
|
},
|
|
16926
17803
|
onToolResult: (_call, result) => printToolResult2(result),
|
|
16927
17804
|
...drainQueuedMessages ? {
|
|
16928
17805
|
drainQueuedMessages,
|
|
16929
|
-
onQueuedMessage: () => {
|
|
17806
|
+
onQueuedMessage: (message) => {
|
|
16930
17807
|
spinner.stop();
|
|
16931
|
-
console.log(
|
|
17808
|
+
console.log(chalk11.dim(` Queued for after the current tool: ${message}`));
|
|
16932
17809
|
}
|
|
16933
17810
|
} : {},
|
|
16934
17811
|
onToolDenied: (call, permission) => {
|
|
16935
|
-
console.log(
|
|
17812
|
+
console.log(chalk11.yellow(` \u2717 denied ${permission} for ${call.tool}`));
|
|
16936
17813
|
},
|
|
16937
17814
|
onProviderNotice: (notice) => {
|
|
16938
|
-
console.log(
|
|
17815
|
+
console.log(chalk11.yellow(` ! ${notice}`));
|
|
16939
17816
|
},
|
|
16940
17817
|
onStopped: (reason) => {
|
|
16941
17818
|
if (reason === "aborted") return;
|
|
16942
17819
|
const line = stopReasonLine(reason);
|
|
16943
17820
|
const hint = turnStopHint(reason, session.maxSteps);
|
|
16944
17821
|
if (!line || !hint) return;
|
|
16945
|
-
console.log(
|
|
17822
|
+
console.log(chalk11.yellow(`
|
|
16946
17823
|
\u26A0 ${line}`));
|
|
16947
|
-
console.log(
|
|
17824
|
+
console.log(chalk11.gray(` ${hint}`));
|
|
16948
17825
|
},
|
|
16949
17826
|
requestPermission: async (call, permission) => {
|
|
16950
17827
|
const prompt = permission === "shell" && call.tool === "run_shell" ? shellApprovalPrompt(call, session.toSession().workspace) : permission === "shell" ? ` ${call.tool} wants shell access. [o]nce (this call only) / [a]lways (all shell tools for this session) / [n]o (this call) / n[e]ver (this session) \u203A ` : ` ${call.tool} wants ${permission} access. [o]nce / [a]lways / [n]o / n[e]ver \u203A `;
|
|
16951
|
-
const rawAnswer = await question(
|
|
17828
|
+
const rawAnswer = await question(chalk11.yellow(prompt), signal);
|
|
16952
17829
|
if (signal?.aborted || rawAnswer === null) return "deny";
|
|
16953
17830
|
const answer = rawAnswer.trim().toLowerCase();
|
|
16954
17831
|
if (answer === "a" || answer === "always") return "always";
|
|
16955
17832
|
if (answer === "o" || answer === "once" || answer === "y" || answer === "yes") return "once";
|
|
16956
17833
|
if (answer === "e" || answer === "never") {
|
|
16957
17834
|
console.log(
|
|
16958
|
-
|
|
17835
|
+
chalk11.gray(
|
|
16959
17836
|
` ${permission} refused for the rest of the session \u2014 nothing will ask again. /allow ${permission} undoes it.`
|
|
16960
17837
|
)
|
|
16961
17838
|
);
|
|
@@ -16973,9 +17850,9 @@ ${message.trim()}`);
|
|
|
16973
17850
|
detachProgress();
|
|
16974
17851
|
}
|
|
16975
17852
|
}
|
|
16976
|
-
async function runAndCheckpointTurn(session, input, question, signal, persist, drainQueuedMessages) {
|
|
17853
|
+
async function runAndCheckpointTurn(session, input, question, signal, persist, drainQueuedMessages, isComposing) {
|
|
16977
17854
|
try {
|
|
16978
|
-
return await runTurn(session, input, question, signal, drainQueuedMessages);
|
|
17855
|
+
return await runTurn(session, input, question, signal, drainQueuedMessages, persist, isComposing);
|
|
16979
17856
|
} finally {
|
|
16980
17857
|
await persist(session);
|
|
16981
17858
|
}
|
|
@@ -16992,27 +17869,27 @@ function turnStopHint(reason, maxSteps) {
|
|
|
16992
17869
|
}
|
|
16993
17870
|
function printTurnError(error) {
|
|
16994
17871
|
if (error instanceof ProviderChainError) {
|
|
16995
|
-
console.log(
|
|
17872
|
+
console.log(chalk11.red("\n \u2717 Turn failed: no provider in the chain could answer."));
|
|
16996
17873
|
for (const failure of error.failures) {
|
|
16997
17874
|
const attempts = failure.attempts === 1 ? "1 attempt" : `${failure.attempts} attempts`;
|
|
16998
17875
|
console.log(
|
|
16999
|
-
|
|
17876
|
+
chalk11.red(` - ${failure.provider} (${failure.model}), ${attempts}: ${errorMessage2(failure.error)}`)
|
|
17000
17877
|
);
|
|
17001
17878
|
}
|
|
17002
17879
|
console.log(
|
|
17003
|
-
|
|
17880
|
+
chalk11.gray(" Fix the provider (ultrac login, or --provider echo) and try again; the session is still open.")
|
|
17004
17881
|
);
|
|
17005
17882
|
return;
|
|
17006
17883
|
}
|
|
17007
|
-
console.log(
|
|
17884
|
+
console.log(chalk11.red(`
|
|
17008
17885
|
\u2717 Turn failed: ${errorMessage2(error)}`));
|
|
17009
|
-
console.log(
|
|
17886
|
+
console.log(chalk11.gray(" The session is still open. Try again, or /exit to end it."));
|
|
17010
17887
|
}
|
|
17011
17888
|
async function saveTranscript(session, writeCheckpoint) {
|
|
17012
17889
|
const snapshot = session.toSession();
|
|
17013
17890
|
if (!hasSavedSessionContent(snapshot)) return;
|
|
17014
17891
|
const stored = await writeCheckpoint(snapshot);
|
|
17015
|
-
if (stored) console.log(
|
|
17892
|
+
if (stored) console.log(chalk11.gray(`
|
|
17016
17893
|
Session saved: ${stored}`));
|
|
17017
17894
|
}
|
|
17018
17895
|
function hasSavedSessionContent(session) {
|
|
@@ -17047,6 +17924,21 @@ async function handleSlashCommand(raw, session, providerName, workspace, onClear
|
|
|
17047
17924
|
const command = (name ?? "").toLowerCase();
|
|
17048
17925
|
const argument = rest.join(" ").trim();
|
|
17049
17926
|
switch (command) {
|
|
17927
|
+
case "logs": {
|
|
17928
|
+
console.log(currentDiagnosticLogPath() ?? "Diagnostic logging has not been configured for this process.");
|
|
17929
|
+
return "ok";
|
|
17930
|
+
}
|
|
17931
|
+
case "ghost": {
|
|
17932
|
+
if (argument && !["on", "off", "status"].includes(argument.toLowerCase())) {
|
|
17933
|
+
console.log("Usage: /ghost [on|off|status]");
|
|
17934
|
+
return "ok";
|
|
17935
|
+
}
|
|
17936
|
+
if (argument.toLowerCase() !== "status") {
|
|
17937
|
+
setGhostEnabled(session, argument ? argument.toLowerCase() === "on" : !isGhostEnabled(session));
|
|
17938
|
+
}
|
|
17939
|
+
console.log(`Ghost mode: ${isGhostEnabled(session) ? "on" : "off"}. Quiet activity output; private diagnostics remain enabled. Network routing and permissions are unchanged.`);
|
|
17940
|
+
return "ok";
|
|
17941
|
+
}
|
|
17050
17942
|
case "help": {
|
|
17051
17943
|
console.log(HELP);
|
|
17052
17944
|
const index = await listSkillCommands(workspace);
|
|
@@ -17054,7 +17946,7 @@ async function handleSlashCommand(raw, session, providerName, workspace, onClear
|
|
|
17054
17946
|
if (extra) console.log(`
|
|
17055
17947
|
${extra}`);
|
|
17056
17948
|
for (const conflict of index.conflicts) {
|
|
17057
|
-
console.log(
|
|
17949
|
+
console.log(chalk11.yellow(` ! ${conflict.message}`));
|
|
17058
17950
|
}
|
|
17059
17951
|
return "ok";
|
|
17060
17952
|
}
|
|
@@ -17068,7 +17960,7 @@ ${extra}`);
|
|
|
17068
17960
|
case "model":
|
|
17069
17961
|
if (argument) {
|
|
17070
17962
|
session.setModel(argument);
|
|
17071
|
-
console.log(
|
|
17963
|
+
console.log(chalk11.gray(`Model set to ${argument}.`));
|
|
17072
17964
|
} else {
|
|
17073
17965
|
console.log(`Model: ${session.model}`);
|
|
17074
17966
|
}
|
|
@@ -17076,10 +17968,10 @@ ${extra}`);
|
|
|
17076
17968
|
case "mode":
|
|
17077
17969
|
if (argument) {
|
|
17078
17970
|
if (!isAgentModeName(argument)) {
|
|
17079
|
-
console.log(
|
|
17971
|
+
console.log(chalk11.red(`Unknown mode. Expected one of: ${agentModeNames.join(", ")}.`));
|
|
17080
17972
|
} else {
|
|
17081
17973
|
session.setMode(argument);
|
|
17082
|
-
console.log(
|
|
17974
|
+
console.log(chalk11.gray(`Mode set to ${argument}.`));
|
|
17083
17975
|
}
|
|
17084
17976
|
} else {
|
|
17085
17977
|
console.log(`Mode: ${session.mode}`);
|
|
@@ -17128,7 +18020,7 @@ ${extra}`);
|
|
|
17128
18020
|
console.log(
|
|
17129
18021
|
`Selected ${info.selectedMessages.toLocaleString("en-US")} of ${info.messages.toLocaleString("en-US")} messages; would omit ${info.omittedMessages.toLocaleString("en-US")} messages in ${info.omittedTurns.toLocaleString("en-US")} earlier turns. Prompt headroom ${info.promptRemainingTokens.toLocaleString("en-US")} tokens; window headroom ${info.windowRemainingTokens.toLocaleString("en-US")} of ${info.contextTokens.toLocaleString("en-US")} tokens.`
|
|
17130
18022
|
);
|
|
17131
|
-
if (info.error) console.log(
|
|
18023
|
+
if (info.error) console.log(chalk11.yellow(`Context error: ${info.error}`));
|
|
17132
18024
|
return "ok";
|
|
17133
18025
|
}
|
|
17134
18026
|
case "compact":
|
|
@@ -17136,7 +18028,7 @@ ${extra}`);
|
|
|
17136
18028
|
await session.compactContext(argument, signal);
|
|
17137
18029
|
console.log("Context compacted. User instructions were retained; earlier activity was replaced by a bounded continuation record.");
|
|
17138
18030
|
} catch (error) {
|
|
17139
|
-
console.log(
|
|
18031
|
+
console.log(chalk11.red(errorMessage2(error)));
|
|
17140
18032
|
}
|
|
17141
18033
|
return "ok";
|
|
17142
18034
|
case "resume":
|
|
@@ -17147,7 +18039,7 @@ ${extra}`);
|
|
|
17147
18039
|
try {
|
|
17148
18040
|
await resume(argument);
|
|
17149
18041
|
} catch (error) {
|
|
17150
|
-
console.log(
|
|
18042
|
+
console.log(chalk11.red(errorMessage2(error)));
|
|
17151
18043
|
}
|
|
17152
18044
|
return "ok";
|
|
17153
18045
|
case "save": {
|
|
@@ -17198,7 +18090,7 @@ ${extra}`);
|
|
|
17198
18090
|
const refused = session.refusedPermissions;
|
|
17199
18091
|
if (refused.length) {
|
|
17200
18092
|
console.log(
|
|
17201
|
-
|
|
18093
|
+
chalk11.gray(`refused for this session: ${refused.join(", ")} (undo with /allow <perm>)`)
|
|
17202
18094
|
);
|
|
17203
18095
|
}
|
|
17204
18096
|
return "ok";
|
|
@@ -17207,9 +18099,9 @@ ${extra}`);
|
|
|
17207
18099
|
case "allow": {
|
|
17208
18100
|
if (argument === "write" || argument === "shell" || argument === "network") {
|
|
17209
18101
|
session.setPermission(argument, command === "allow");
|
|
17210
|
-
console.log(
|
|
18102
|
+
console.log(chalk11.gray(`${command === "allow" ? "Granted" : "Revoked"} ${argument} for this session.`));
|
|
17211
18103
|
} else {
|
|
17212
|
-
console.log(
|
|
18104
|
+
console.log(chalk11.red(`Usage: /${command} <write|shell|network>`));
|
|
17213
18105
|
}
|
|
17214
18106
|
return "ok";
|
|
17215
18107
|
}
|
|
@@ -17224,11 +18116,32 @@ ${extra}`);
|
|
|
17224
18116
|
}
|
|
17225
18117
|
return "ok";
|
|
17226
18118
|
}
|
|
18119
|
+
case "output": {
|
|
18120
|
+
const results = session.toSession().steps.flatMap((step) => step.toolResults);
|
|
18121
|
+
if (!results.length) {
|
|
18122
|
+
console.log("No saved tool output in this session.");
|
|
18123
|
+
return "ok";
|
|
18124
|
+
}
|
|
18125
|
+
if (argument === "list") {
|
|
18126
|
+
for (const [index, entry2] of results.entries()) console.log(`${index + 1}. ${toolActivityLabel(entry2.call)} (${entry2.result.ok ? "ok" : "failed"})`);
|
|
18127
|
+
return "ok";
|
|
18128
|
+
}
|
|
18129
|
+
const selected = argument ? Number(argument) : results.length;
|
|
18130
|
+
if (!Number.isSafeInteger(selected) || selected < 1 || selected > results.length) {
|
|
18131
|
+
console.log(`Usage: /output [1-${results.length}|list]`);
|
|
18132
|
+
return "ok";
|
|
18133
|
+
}
|
|
18134
|
+
const entry = results[selected - 1];
|
|
18135
|
+
console.log(`${selected}. ${toolActivityLabel(entry.call)} (${entry.result.ok ? "ok" : "failed"})`);
|
|
18136
|
+
console.log(entry.result.content);
|
|
18137
|
+
for (const warning of getMcpArchiveWarnings(entry.result)) console.log(chalk11.yellow(`Warning: ${warning}`));
|
|
18138
|
+
return "ok";
|
|
18139
|
+
}
|
|
17227
18140
|
case "tools": {
|
|
17228
18141
|
const names = session.advertisedToolNames;
|
|
17229
18142
|
console.log(`Advertised to the model this turn (${names.length}): ${names.join(", ")}`);
|
|
17230
18143
|
console.log(
|
|
17231
|
-
|
|
18144
|
+
chalk11.gray(
|
|
17232
18145
|
"Registered tools remain subject to permission and mode checks. `ultrac tools` lists them all with schemas; --tools all advertises them all."
|
|
17233
18146
|
)
|
|
17234
18147
|
);
|
|
@@ -17237,7 +18150,7 @@ ${extra}`);
|
|
|
17237
18150
|
case "memory": {
|
|
17238
18151
|
const memories = await listMemories(workspace);
|
|
17239
18152
|
if (!memories.length) {
|
|
17240
|
-
console.log(
|
|
18153
|
+
console.log(chalk11.gray("No stored memories. Add one with `ultrac memory add`."));
|
|
17241
18154
|
} else {
|
|
17242
18155
|
for (const memory of memories) {
|
|
17243
18156
|
console.log(`- [${memory.scope}/${memory.name}] (${memory.type}) ${memory.description}`);
|
|
@@ -17248,7 +18161,7 @@ ${extra}`);
|
|
|
17248
18161
|
case "skills": {
|
|
17249
18162
|
const skills = await listSkills(workspace);
|
|
17250
18163
|
if (!skills.length) {
|
|
17251
|
-
console.log(
|
|
18164
|
+
console.log(chalk11.gray("No skills installed. Create one with `ultrac skills create`."));
|
|
17252
18165
|
} else {
|
|
17253
18166
|
for (const skill of skills) {
|
|
17254
18167
|
console.log(`- ${skill.name} (${skill.scope}): ${skill.description}`);
|
|
@@ -17264,12 +18177,12 @@ ${extra}`);
|
|
|
17264
18177
|
force: true
|
|
17265
18178
|
});
|
|
17266
18179
|
if (!update.latestVersion) {
|
|
17267
|
-
console.log(
|
|
18180
|
+
console.log(chalk11.yellow("Could not check npm. Confirm npm access, then run `ultrac update --check` outside this session."));
|
|
17268
18181
|
} else if (update.updateAvailable) {
|
|
17269
|
-
console.log(
|
|
17270
|
-
console.log(
|
|
18182
|
+
console.log(chalk11.yellow(`Update available: ${update.currentVersion} \u2192 ${update.latestVersion}`));
|
|
18183
|
+
console.log(chalk11.gray("Exit this session, then run `ultrac update`."));
|
|
17271
18184
|
} else {
|
|
17272
|
-
console.log(
|
|
18185
|
+
console.log(chalk11.green(`ultrac ${update.currentVersion} is up to date.`));
|
|
17273
18186
|
}
|
|
17274
18187
|
return "ok";
|
|
17275
18188
|
}
|
|
@@ -17281,33 +18194,31 @@ async function runSkillCommand(session, raw, workspace, question) {
|
|
|
17281
18194
|
const resolved = await resolveSkillCommand(workspace, raw);
|
|
17282
18195
|
if (resolved.status === "ok") {
|
|
17283
18196
|
console.log(
|
|
17284
|
-
|
|
18197
|
+
chalk11.gray(` /${resolved.command.command} \u2192 skill ${resolved.command.skillName} (${resolved.command.scope})`)
|
|
17285
18198
|
);
|
|
17286
18199
|
await runTurn(session, resolved.prompt, question);
|
|
17287
18200
|
return;
|
|
17288
18201
|
}
|
|
17289
18202
|
if (resolved.status === "conflict") {
|
|
17290
|
-
console.log(
|
|
18203
|
+
console.log(chalk11.red(resolved.message));
|
|
17291
18204
|
return;
|
|
17292
18205
|
}
|
|
17293
18206
|
if (resolved.status === "builtin") {
|
|
17294
|
-
console.log(
|
|
18207
|
+
console.log(chalk11.red(`/${resolved.command} is a built-in command and was not handled. Try /help.`));
|
|
17295
18208
|
return;
|
|
17296
18209
|
}
|
|
17297
18210
|
if (resolved.status === "unknown") {
|
|
17298
|
-
console.log(
|
|
18211
|
+
console.log(chalk11.red(`Unknown command: /${resolved.command}. Try /help.`));
|
|
17299
18212
|
if (resolved.available.length) {
|
|
17300
|
-
console.log(
|
|
18213
|
+
console.log(chalk11.gray(`Skill commands: ${resolved.available.map((name) => `/${name}`).join(", ")}`));
|
|
17301
18214
|
}
|
|
17302
18215
|
return;
|
|
17303
18216
|
}
|
|
17304
|
-
console.log(
|
|
18217
|
+
console.log(chalk11.red("Unknown command. Try /help."));
|
|
17305
18218
|
}
|
|
17306
18219
|
var LineReader = class {
|
|
17307
|
-
queue = [];
|
|
17308
|
-
waiters = [];
|
|
17309
|
-
closed = false;
|
|
17310
18220
|
constructor(rl) {
|
|
18221
|
+
this.rl = rl;
|
|
17311
18222
|
rl.on("line", (line) => {
|
|
17312
18223
|
const waiter = this.waiters.shift();
|
|
17313
18224
|
if (waiter) {
|
|
@@ -17323,6 +18234,10 @@ var LineReader = class {
|
|
|
17323
18234
|
}
|
|
17324
18235
|
});
|
|
17325
18236
|
}
|
|
18237
|
+
rl;
|
|
18238
|
+
queue = [];
|
|
18239
|
+
waiters = [];
|
|
18240
|
+
closed = false;
|
|
17326
18241
|
next(signal) {
|
|
17327
18242
|
return this.waitForLine(signal, true);
|
|
17328
18243
|
}
|
|
@@ -17333,6 +18248,10 @@ var LineReader = class {
|
|
|
17333
18248
|
nextFresh(signal) {
|
|
17334
18249
|
return this.waitForLine(signal, false);
|
|
17335
18250
|
}
|
|
18251
|
+
/** True while the user is typing a line that has not been submitted yet. */
|
|
18252
|
+
get composing() {
|
|
18253
|
+
return this.rl.line.length > 0;
|
|
18254
|
+
}
|
|
17336
18255
|
/**
|
|
17337
18256
|
* Drain ordinary messages in arrival order. A slash command is a control
|
|
17338
18257
|
* boundary and remains queued for the main REPL loop along with everything
|
|
@@ -17378,11 +18297,15 @@ var LineReader = class {
|
|
|
17378
18297
|
waiter.resolve(line);
|
|
17379
18298
|
}
|
|
17380
18299
|
};
|
|
17381
|
-
function
|
|
18300
|
+
function handleReplEscape(activeTurn) {
|
|
17382
18301
|
if (activeTurn && !activeTurn.signal.aborted) {
|
|
17383
18302
|
activeTurn.abort();
|
|
17384
18303
|
return "abort-turn";
|
|
17385
18304
|
}
|
|
18305
|
+
return "idle";
|
|
18306
|
+
}
|
|
18307
|
+
function handleReplExit(activeTurn) {
|
|
18308
|
+
activeTurn?.abort();
|
|
17386
18309
|
return "exit-session";
|
|
17387
18310
|
}
|
|
17388
18311
|
function printBanner(version, providerName, model, mode, workspace, protocol, budget, diagnosticPath2) {
|
|
@@ -17416,9 +18339,9 @@ function describeAdvertisedTools(mode, selection, registry = createDefaultToolRe
|
|
|
17416
18339
|
}
|
|
17417
18340
|
function printToolResult2(result) {
|
|
17418
18341
|
const failure = toolFailurePreview(result);
|
|
17419
|
-
if (failure) console.log(
|
|
18342
|
+
if (failure) console.log(chalk11.red(` Failed: ${failure}`));
|
|
17420
18343
|
for (const warning of getMcpArchiveWarnings(result)) {
|
|
17421
|
-
console.log(
|
|
18344
|
+
console.log(chalk11.yellow(indent3(`Warning: ${warning}`)));
|
|
17422
18345
|
}
|
|
17423
18346
|
}
|
|
17424
18347
|
function shellApprovalPrompt(call, workspace) {
|
|
@@ -17445,31 +18368,186 @@ function indent3(value) {
|
|
|
17445
18368
|
}
|
|
17446
18369
|
|
|
17447
18370
|
// src/commands/run.ts
|
|
17448
|
-
import
|
|
18371
|
+
import { basename as basename3 } from "path";
|
|
18372
|
+
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
|
+
function parsePositiveInt(value, flag) {
|
|
18516
|
+
const parsed = Number.parseInt(value, 10);
|
|
18517
|
+
if (!Number.isInteger(parsed) || parsed < 1 || String(parsed) !== value.trim()) {
|
|
18518
|
+
throw new Error(`${flag} must be a positive integer.`);
|
|
18519
|
+
}
|
|
18520
|
+
return parsed;
|
|
18521
|
+
}
|
|
17449
18522
|
var EXIT_INCOMPLETE_RUN = 2;
|
|
18523
|
+
var NOTIFY_SUMMARY_CHARS = 1200;
|
|
17450
18524
|
function registerRunCommand(program2) {
|
|
17451
18525
|
program2.command("run").alias("chat").description("Run a one-shot coding agent task").argument("<task...>", "Task for the agent").option("-w, --workspace <path>", "Workspace path", ".").option("--provider <name>", "Provider: ultrac (default) or echo (offline test stub)").option("--model <model>", "Model name").option("--base-url <url>", "Ultrac API base URL (overrides ULTRAC_BASE_URL and the stored login)").option("--mode <name>", `Agent mode: ${agentModeNames.join(", ")}`).option("--max-steps <number>", "Maximum agent steps").option("--no-native-tools", "Use the legacy JSON tool protocol").option("--allow-write", "Allow file writes", false).option("--allow-shell", "Grant the model broad, unsandboxed shell access", false).option("--allow-network", "Allow HTTP(S) URL fetches", false).option("--continue <id>", "Continue from a saved session id").option("--tools <list>", "Tools to advertise: `all`, or a comma-separated list. Default: the mode's set").option("--context-tokens <number>", "Model context window in tokens (default 262144)").option("--max-tool-result-chars <number>", "Cap on one tool result in the transcript (default 6000)").option(
|
|
17452
18526
|
"--allow-incomplete",
|
|
17453
18527
|
`Exit 0 even when the run stops without finishing. By default an unfinished run (step cap, abort, error) exits ${EXIT_INCOMPLETE_RUN} so a script or a \`&&\` chain can tell a partial answer from a real one; pass this when hitting --max-steps on a long task is an acceptable outcome. The stop reason is still printed either way.`,
|
|
17454
18528
|
false
|
|
17455
|
-
).option(
|
|
18529
|
+
).option(
|
|
18530
|
+
"--autonomous",
|
|
18531
|
+
`Run with no human in the loop: write, shell and network are granted, each round may take up to ${AUTONOMOUS_MAX_STEPS} steps, and a round that runs out of steps or fails resumes on its own. With a phone channel configured (\`ultrac notify setup\`), questions go to your phone and you are told when it finishes.`,
|
|
18532
|
+
false
|
|
18533
|
+
).option("--max-rounds <number>", `With --autonomous: how many times a run may resume itself (default ${DEFAULT_AUTONOMOUS_MAX_ROUNDS})`).option("--notify", "Send ask_user questions and the final result to your phone (see `ultrac notify setup`)", false).option("--verbose-tools", "Print complete tool results instead of previews", false).option("--json", "Print the session JSON", false).action(async (taskParts, options) => {
|
|
17456
18534
|
const task = taskParts.join(" ").trim();
|
|
17457
18535
|
if (!task) {
|
|
17458
18536
|
throw new Error("A task is required.");
|
|
17459
18537
|
}
|
|
17460
18538
|
const workspace = await assertWorkspaceExists(options.workspace);
|
|
17461
18539
|
const diagnosticPath2 = configureDiagnosticLog(workspace, "run");
|
|
17462
|
-
console.error(
|
|
18540
|
+
console.error(chalk12.gray(`Diagnostic log: ${diagnosticPath2}`));
|
|
17463
18541
|
if (options.baseUrl) {
|
|
17464
18542
|
setBaseUrlFlag(options.baseUrl);
|
|
17465
18543
|
}
|
|
17466
18544
|
const loaded = await loadConfigWithTrust(workspace);
|
|
17467
18545
|
const config = loaded.config;
|
|
17468
18546
|
if (loaded.needsApproval) {
|
|
17469
|
-
console.error(
|
|
18547
|
+
console.error(chalk12.yellow(withheldNotice(workspace, loaded.withheld, loaded.trustStatus)));
|
|
17470
18548
|
}
|
|
17471
18549
|
const toolSelection = applyBudgetFlags(options, config);
|
|
17472
|
-
setContextBudgetNotice((message) => console.error(
|
|
18550
|
+
setContextBudgetNotice((message) => console.error(chalk12.yellow(message)));
|
|
17473
18551
|
const providerName = options.provider ?? config.provider;
|
|
17474
18552
|
const mode = options.mode ?? config.mode;
|
|
17475
18553
|
const previousSessionLineage = options.continue ? await loadSessionLineage(workspace, options.continue) : void 0;
|
|
@@ -17480,24 +18558,49 @@ function registerRunCommand(program2) {
|
|
|
17480
18558
|
if (!isAgentModeName(mode)) {
|
|
17481
18559
|
throw new Error(`Unknown mode: ${mode}. Expected one of: ${agentModeNames.join(", ")}.`);
|
|
17482
18560
|
}
|
|
17483
|
-
const
|
|
18561
|
+
const autonomous = options.autonomous;
|
|
18562
|
+
const maxSteps = Number.parseInt(
|
|
18563
|
+
options.maxSteps ?? String(autonomous ? AUTONOMOUS_MAX_STEPS : config.maxSteps),
|
|
18564
|
+
10
|
|
18565
|
+
);
|
|
17484
18566
|
if (!Number.isInteger(maxSteps) || maxSteps < 1) {
|
|
17485
18567
|
throw new Error("--max-steps must be a positive integer.");
|
|
17486
18568
|
}
|
|
18569
|
+
if (options.maxRounds !== void 0 && !autonomous) {
|
|
18570
|
+
throw new Error("--max-rounds only applies with --autonomous.");
|
|
18571
|
+
}
|
|
18572
|
+
const maxRounds = autonomous ? parsePositiveInt(options.maxRounds ?? String(DEFAULT_AUTONOMOUS_MAX_ROUNDS), "--max-rounds") : 1;
|
|
18573
|
+
const notifyConfig = options.notify || autonomous ? await loadNotifyConfig() : null;
|
|
18574
|
+
if (options.notify && !notifyConfig) {
|
|
18575
|
+
throw new Error("--notify needs a phone channel. Run `ultrac notify setup` first.");
|
|
18576
|
+
}
|
|
18577
|
+
const notifier = notifyConfig ? createNotifier(notifyConfig, `Ultrac (${basename3(workspace)})`) : void 0;
|
|
18578
|
+
if (autonomous) {
|
|
18579
|
+
console.error(
|
|
18580
|
+
chalk12.yellow(
|
|
18581
|
+
`Autonomous run: file writes, unsandboxed shell and network are granted with no approvals, up to ${maxRounds} round${maxRounds === 1 ? "" : "s"} of ${maxSteps} steps.`
|
|
18582
|
+
)
|
|
18583
|
+
);
|
|
18584
|
+
console.error(
|
|
18585
|
+
chalk12.gray(
|
|
18586
|
+
notifier ? `Questions and the result go to your phone via ${notifier.describe()}.` : "No phone channel is configured, so the agent decides on its own. Run `ultrac notify setup` to be reached when it needs you."
|
|
18587
|
+
)
|
|
18588
|
+
);
|
|
18589
|
+
}
|
|
17487
18590
|
const model = options.model ?? (options.provider ? providerDefaultModels[providerName] : config.model);
|
|
17488
18591
|
const provider = createFallbackProvider({
|
|
17489
18592
|
provider: providerName,
|
|
17490
18593
|
model,
|
|
17491
18594
|
fallback: config.fallback,
|
|
17492
18595
|
// stderr, so --json keeps stdout parseable.
|
|
17493
|
-
onFailover: (event) => console.error(
|
|
18596
|
+
onFailover: (event) => console.error(chalk12.yellow(formatFailoverNotice(event))),
|
|
17494
18597
|
// AgentRunner uses native tool calls when supported; wired
|
|
17495
18598
|
// anyway so a non-interactive run stays honest if that ever changes.
|
|
17496
|
-
onDowngrade: (event) => console.error(
|
|
18599
|
+
onDowngrade: (event) => console.error(chalk12.yellow(formatDowngradeNotice(event))),
|
|
17497
18600
|
onRetry: (event) => {
|
|
17498
18601
|
diagnosticEvent("provider.retry", event);
|
|
17499
18602
|
console.error(
|
|
17500
|
-
|
|
18603
|
+
chalk12.yellow(
|
|
17501
18604
|
`Retrying ${event.target.provider} (${event.target.model}) in ${event.delayMs}ms after ${event.classification.reason}`
|
|
17502
18605
|
)
|
|
17503
18606
|
);
|
|
@@ -17506,47 +18609,106 @@ function registerRunCommand(program2) {
|
|
|
17506
18609
|
const registry = createDefaultToolRegistry();
|
|
17507
18610
|
const mcpRuntime = await connectWorkspaceMcpServers(config.mcpServers, { workspace, registry });
|
|
17508
18611
|
setSubagentRuntime({ provider, model, mode, registry });
|
|
18612
|
+
if (notifier) {
|
|
18613
|
+
setAskUserRuntime(
|
|
18614
|
+
async (question, choices, signal) => {
|
|
18615
|
+
console.error(chalk12.cyan(`Asking you on ${notifier.describe()}: ${question}`));
|
|
18616
|
+
try {
|
|
18617
|
+
const answer = await notifier.ask(question, choices, signal);
|
|
18618
|
+
console.error(chalk12.gray(answer === null ? "No reply; the agent will proceed on its own judgement." : `Reply: ${answer}`));
|
|
18619
|
+
return answer;
|
|
18620
|
+
} catch (error) {
|
|
18621
|
+
console.error(chalk12.yellow(`Could not reach your phone: ${error.message}`));
|
|
18622
|
+
return null;
|
|
18623
|
+
}
|
|
18624
|
+
},
|
|
18625
|
+
{ remote: true }
|
|
18626
|
+
);
|
|
18627
|
+
}
|
|
17509
18628
|
try {
|
|
17510
18629
|
if (mcpRuntime && mcpRuntime.statuses.length > 0) {
|
|
17511
|
-
console.error(
|
|
18630
|
+
console.error(chalk12.bold("MCP servers"));
|
|
17512
18631
|
console.error(renderMcpStatuses(mcpRuntime.statuses));
|
|
17513
18632
|
}
|
|
17514
18633
|
const detachProgress = attachProgressUi();
|
|
17515
18634
|
const runner = new AgentRunner(registry);
|
|
17516
18635
|
const writeCheckpoint = createSessionCheckpointWriter(workspace, (message) => {
|
|
17517
|
-
console.error(
|
|
18636
|
+
console.error(chalk12.yellow(message));
|
|
17518
18637
|
});
|
|
17519
18638
|
const controller = new AbortController();
|
|
17520
18639
|
const abortRun = () => controller.abort();
|
|
17521
18640
|
process.on("SIGINT", abortRun);
|
|
17522
18641
|
process.on("SIGTERM", abortRun);
|
|
18642
|
+
const permissions = autonomous ? { write: true, shell: true, network: true } : {
|
|
18643
|
+
write: options.allowWrite || config.permissions.write,
|
|
18644
|
+
shell: options.allowShell || config.permissions.shell,
|
|
18645
|
+
network: options.allowNetwork || config.permissions.network
|
|
18646
|
+
};
|
|
18647
|
+
const roundFailures = /* @__PURE__ */ new Map();
|
|
17523
18648
|
let result;
|
|
17524
18649
|
try {
|
|
17525
|
-
result = await
|
|
17526
|
-
|
|
17527
|
-
workspace,
|
|
17528
|
-
provider,
|
|
17529
|
-
model,
|
|
17530
|
-
mode,
|
|
17531
|
-
nativeTools: options.nativeTools,
|
|
17532
|
-
maxSteps,
|
|
17533
|
-
previousSession,
|
|
17534
|
-
previousSessionLineage,
|
|
17535
|
-
...toolSelection ? { tools: toolSelection } : {},
|
|
17536
|
-
permissions: {
|
|
17537
|
-
write: options.allowWrite || config.permissions.write,
|
|
17538
|
-
shell: options.allowShell || config.permissions.shell,
|
|
17539
|
-
network: options.allowNetwork || config.permissions.network
|
|
17540
|
-
},
|
|
18650
|
+
result = await runAutonomousLoop({
|
|
18651
|
+
maxRounds,
|
|
17541
18652
|
signal: controller.signal,
|
|
17542
|
-
|
|
17543
|
-
|
|
17544
|
-
|
|
17545
|
-
|
|
17546
|
-
|
|
17547
|
-
|
|
18653
|
+
classifyFailure: (session) => roundFailures.get(session.id),
|
|
18654
|
+
runRound: async (round, previous, roundSignal) => {
|
|
18655
|
+
const lineage = previous ? await loadSessionLineage(workspace, previous.id) : previousSessionLineage;
|
|
18656
|
+
let lastCheckpoint;
|
|
18657
|
+
let checkpointFailed = false;
|
|
18658
|
+
try {
|
|
18659
|
+
const { session } = await runner.run({
|
|
18660
|
+
task: previous ? AUTONOMOUS_CONTINUE_TASK : task,
|
|
18661
|
+
workspace,
|
|
18662
|
+
provider,
|
|
18663
|
+
model,
|
|
18664
|
+
mode,
|
|
18665
|
+
nativeTools: options.nativeTools,
|
|
18666
|
+
maxSteps,
|
|
18667
|
+
previousSession: previous ?? previousSession,
|
|
18668
|
+
previousSessionLineage: lineage,
|
|
18669
|
+
...toolSelection ? { tools: toolSelection } : {},
|
|
18670
|
+
permissions,
|
|
18671
|
+
signal: roundSignal,
|
|
18672
|
+
...autonomous ? { autonomous: { phone: Boolean(notifier) } } : {},
|
|
18673
|
+
// Write the session after every step, atomically, so a run that dies
|
|
18674
|
+
// partway - a crash, a kill, a closed laptop mid-scan - is on disk up to
|
|
18675
|
+
// its last completed step and can be resumed with --continue rather than
|
|
18676
|
+
// restarted from nothing.
|
|
18677
|
+
onCheckpoint: async (session2) => {
|
|
18678
|
+
try {
|
|
18679
|
+
const checkpointPath = await writeCheckpoint(session2);
|
|
18680
|
+
checkpointFailed = !checkpointPath;
|
|
18681
|
+
if (checkpointPath) lastCheckpoint = await loadSession(workspace, session2.id);
|
|
18682
|
+
} catch (error) {
|
|
18683
|
+
checkpointFailed = true;
|
|
18684
|
+
throw error;
|
|
18685
|
+
}
|
|
18686
|
+
}
|
|
18687
|
+
});
|
|
18688
|
+
return session;
|
|
18689
|
+
} catch (error) {
|
|
18690
|
+
if (!autonomous || !lastCheckpoint || checkpointFailed || roundSignal.aborted) throw error;
|
|
18691
|
+
const classification = classifyProviderError(error);
|
|
18692
|
+
roundFailures.set(lastCheckpoint.id, classification);
|
|
18693
|
+
console.error(
|
|
18694
|
+
chalk12.yellow(`Round ${round} failed (${classification.kind}). The latest checkpoint is saved.`)
|
|
18695
|
+
);
|
|
18696
|
+
return { ...lastCheckpoint, stopReason: "error" };
|
|
18697
|
+
}
|
|
18698
|
+
},
|
|
18699
|
+
onRoundEnd: async (round, session, willResume) => {
|
|
18700
|
+
if (!willResume) return;
|
|
18701
|
+
await saveSession(workspace, session);
|
|
18702
|
+
console.error(
|
|
18703
|
+
chalk12.yellow(
|
|
18704
|
+
`Round ${round} stopped (${session.stopReason}); resuming in round ${round + 1} of ${maxRounds}.`
|
|
18705
|
+
)
|
|
18706
|
+
);
|
|
17548
18707
|
}
|
|
17549
18708
|
});
|
|
18709
|
+
} catch (error) {
|
|
18710
|
+
await notifier?.send(`stopped with an error: ${error.message}`).catch(() => void 0);
|
|
18711
|
+
throw error;
|
|
17550
18712
|
} finally {
|
|
17551
18713
|
process.off("SIGINT", abortRun);
|
|
17552
18714
|
process.off("SIGTERM", abortRun);
|
|
@@ -17562,21 +18724,33 @@ function registerRunCommand(program2) {
|
|
|
17562
18724
|
{ cause: error }
|
|
17563
18725
|
);
|
|
17564
18726
|
}
|
|
18727
|
+
if (["stalled", "elapsed_budget", "permanent_error"].includes(result.haltReason ?? "")) {
|
|
18728
|
+
console.error(chalk12.yellow(`Autonomous recovery stopped: ${result.haltReason}. The latest checkpoint is saved.`));
|
|
18729
|
+
}
|
|
17565
18730
|
const stopReason = result.session.stopReason;
|
|
17566
18731
|
for (const warning of getSessionMcpArchiveWarnings(result.session)) {
|
|
17567
|
-
console.error(
|
|
18732
|
+
console.error(chalk12.yellow(`Warning: ${warning}`));
|
|
17568
18733
|
}
|
|
17569
18734
|
if (options.json) {
|
|
17570
18735
|
console.log(
|
|
17571
|
-
JSON.stringify({ sessionPath: sessionPath2, stopReason: stopReason ?? null, session: result.session }, null, 2)
|
|
18736
|
+
JSON.stringify({ sessionPath: sessionPath2, stopReason: stopReason ?? null, haltReason: result.haltReason ?? null, rounds: result.rounds, session: result.session }, null, 2)
|
|
17572
18737
|
);
|
|
17573
18738
|
} else {
|
|
17574
|
-
printSession(result.session, sessionPath2);
|
|
18739
|
+
printSession(result.session, sessionPath2, { verboseTools: options.verboseTools });
|
|
18740
|
+
}
|
|
18741
|
+
if (notifier) {
|
|
18742
|
+
const outcome = stopReason === "completed" ? "finished" : `stopped without finishing (${stopReason ?? "unknown"})`;
|
|
18743
|
+
const rounds = result.rounds > 1 ? ` after ${result.rounds} rounds` : "";
|
|
18744
|
+
const summary = result.session.finalMessage.trim().slice(0, NOTIFY_SUMMARY_CHARS);
|
|
18745
|
+
await notifier.send(`${outcome}${rounds}.${summary ? `
|
|
18746
|
+
|
|
18747
|
+
${summary}` : ""}`).catch((error) => console.error(chalk12.yellow(`Could not send the result to your phone: ${error.message}`)));
|
|
17575
18748
|
}
|
|
17576
18749
|
if (isIncompleteStop(stopReason) && !options.allowIncomplete) {
|
|
17577
18750
|
process.exitCode = EXIT_INCOMPLETE_RUN;
|
|
17578
18751
|
}
|
|
17579
18752
|
} finally {
|
|
18753
|
+
if (notifier) setAskUserRuntime(void 0);
|
|
17580
18754
|
mcpRuntime?.close();
|
|
17581
18755
|
}
|
|
17582
18756
|
});
|
|
@@ -17587,7 +18761,7 @@ import { resolve as resolve12 } from "path";
|
|
|
17587
18761
|
function registerSessionsCommand(program2) {
|
|
17588
18762
|
const sessions = program2.command("sessions").description("Inspect saved Ultrac session transcripts");
|
|
17589
18763
|
sessions.command("list").description("List saved sessions").option("-w, --workspace <path>", "Workspace path", ".").option("--json", "Print JSON output", false).option("--max-results <number>", "Maximum sessions to show", "20").action(async (options) => {
|
|
17590
|
-
await runSessionTool("list_sessions", { maxResults:
|
|
18764
|
+
await runSessionTool("list_sessions", { maxResults: parsePositiveInt2(options.maxResults) }, options);
|
|
17591
18765
|
});
|
|
17592
18766
|
sessions.command("show").description("Show a saved session by id").argument("<id>", "Session id").option("-w, --workspace <path>", "Workspace path", ".").option("--json", "Print JSON output", false).action(async (id, options) => {
|
|
17593
18767
|
await runSessionTool("read_session", { id }, options);
|
|
@@ -17595,14 +18769,14 @@ function registerSessionsCommand(program2) {
|
|
|
17595
18769
|
sessions.command("search").description("Search saved sessions").argument("<query...>", "Search query").option("-w, --workspace <path>", "Workspace path", ".").option("--json", "Print JSON output", false).option("--max-results <number>", "Maximum sessions to show", "20").action(async (queryParts, options) => {
|
|
17596
18770
|
await runSessionTool(
|
|
17597
18771
|
"search_sessions",
|
|
17598
|
-
{ query: queryParts.join(" ").trim(), maxResults:
|
|
18772
|
+
{ query: queryParts.join(" ").trim(), maxResults: parsePositiveInt2(options.maxResults) },
|
|
17599
18773
|
options
|
|
17600
18774
|
);
|
|
17601
18775
|
});
|
|
17602
18776
|
sessions.command("export").description("Export a saved session as an auditable episode package").argument("<id>", "Session id").option("-w, --workspace <path>", "Workspace path", ".").option("--format <format>", "Output format: markdown or json", "markdown").option("--max-chars <number>", "Maximum characters per model/tool text block in markdown", "4000").option("--outcome <outcome>", "Outcome status: pass, fail, blocked, or unknown", "unknown").option("--notes <text>", "Outcome notes to include in the episode package").option("--out <path>", "Write export to a path inside the workspace instead of stdout").action(async (id, options) => {
|
|
17603
18777
|
const workspace = resolve12(options.workspace);
|
|
17604
18778
|
const format3 = parseExportFormat(options.format);
|
|
17605
|
-
const maxChars =
|
|
18779
|
+
const maxChars = parsePositiveInt2(options.maxChars);
|
|
17606
18780
|
const outcome = parseOutcomeStatus(options.outcome);
|
|
17607
18781
|
const session = await loadSession(workspace, id);
|
|
17608
18782
|
const episode = buildEpisodePackage(session, { outcome, notes: options.notes ?? "" });
|
|
@@ -17631,7 +18805,7 @@ async function runSessionTool(tool, input, options) {
|
|
|
17631
18805
|
}
|
|
17632
18806
|
console.log(result.content);
|
|
17633
18807
|
}
|
|
17634
|
-
function
|
|
18808
|
+
function parsePositiveInt2(value) {
|
|
17635
18809
|
const parsed = Number.parseInt(value, 10);
|
|
17636
18810
|
if (!Number.isInteger(parsed) || parsed < 1) {
|
|
17637
18811
|
throw new Error("Value must be a positive integer.");
|
|
@@ -17816,17 +18990,25 @@ function registerSkillsCommand(program2) {
|
|
|
17816
18990
|
console.log("");
|
|
17817
18991
|
console.log(skill.body);
|
|
17818
18992
|
});
|
|
17819
|
-
skills.command("create").description("Scaffold a new SKILL.md").argument("<name>", "Skill name (kebab-case)").requiredOption("--description <text>", "One-line description").option("--when <text>", "When the agent should use this skill").option("--permissions <list>", "Comma-separated: write,shell,network").option("--scope <scope>", "global or project", "project").option("-w, --workspace <path>", "Workspace path", ".").action(
|
|
18993
|
+
skills.command("create").description("Scaffold a new SKILL.md").argument("<name>", "Skill name (kebab-case)").requiredOption("--description <text>", "One-line description").option("--when <text>", "When the agent should use this skill").option("--permissions <list>", "Comma-separated: write,shell,network").option("--scope <scope>", "global or project", "project").option("--command <name>", "Expose this skill as a custom slash command").option("--argument-hint <text>", "Describe the slash command arguments").option("-w, --workspace <path>", "Workspace path", ".").action(
|
|
17820
18994
|
async (name, options) => {
|
|
18995
|
+
const command = options.command ? normalizeCommandName(options.command) : void 0;
|
|
18996
|
+
if (command && isBuiltinSlashCommand(command)) throw new Error(`/${command} is a built-in command. Choose another skill command name.`);
|
|
18997
|
+
if (options.argumentHint && !command) throw new Error("--argument-hint requires --command.");
|
|
18998
|
+
const argumentHint = options.argumentHint?.trim();
|
|
18999
|
+
if (argumentHint && (argumentHint.length > 256 || /[\r\n]/.test(argumentHint))) throw new Error("Argument hints must be one line, at most 256 characters.");
|
|
17821
19000
|
const skill = await createSkill(resolve13(options.workspace), {
|
|
17822
19001
|
name,
|
|
17823
19002
|
description: options.description,
|
|
17824
19003
|
whenToUse: options.when,
|
|
17825
19004
|
permissions: parsePermissions(options.permissions),
|
|
17826
|
-
scope: parseScope2(options.scope)
|
|
19005
|
+
scope: parseScope2(options.scope),
|
|
19006
|
+
command,
|
|
19007
|
+
argumentHint
|
|
17827
19008
|
});
|
|
17828
19009
|
console.log(`Created ${skill.scope} skill "${skill.name}" at ${skill.path}`);
|
|
17829
19010
|
console.log("Edit that file to add the step-by-step instructions.");
|
|
19011
|
+
if (command) console.log(`Invoke this skill with /${command}${argumentHint ? ` ${argumentHint}` : ""} in a trusted workspace.`);
|
|
17830
19012
|
}
|
|
17831
19013
|
);
|
|
17832
19014
|
skills.command("path").description("Print the skills directory path for a scope").option("-w, --workspace <path>", "Workspace path", ".").option("--scope <scope>", "global or project", "project").action((options) => {
|
|
@@ -17849,7 +19031,7 @@ function parsePermissions(list) {
|
|
|
17849
19031
|
}
|
|
17850
19032
|
|
|
17851
19033
|
// src/commands/tasks.ts
|
|
17852
|
-
import
|
|
19034
|
+
import chalk13 from "chalk";
|
|
17853
19035
|
|
|
17854
19036
|
// src/runs/api.ts
|
|
17855
19037
|
var RUN_STATUSES = ["queued", "running", "succeeded", "failed", "cancelled"];
|
|
@@ -18154,7 +19336,7 @@ function record2(value) {
|
|
|
18154
19336
|
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
18155
19337
|
}
|
|
18156
19338
|
async function followRun(client, runId, options = {}) {
|
|
18157
|
-
const
|
|
19339
|
+
const sleep2 = options.sleep ?? defaultSleep2;
|
|
18158
19340
|
const minBackoffMs = options.minBackoffMs ?? 1e3;
|
|
18159
19341
|
const maxBackoffMs = options.maxBackoffMs ?? 15e3;
|
|
18160
19342
|
const signal = options.signal;
|
|
@@ -18266,7 +19448,7 @@ async function followRun(client, runId, options = {}) {
|
|
|
18266
19448
|
failures,
|
|
18267
19449
|
message: errorMessage3(cause)
|
|
18268
19450
|
});
|
|
18269
|
-
await
|
|
19451
|
+
await sleep2(delayMs2, signal);
|
|
18270
19452
|
continue;
|
|
18271
19453
|
}
|
|
18272
19454
|
if (!response.ok) {
|
|
@@ -18281,7 +19463,7 @@ async function followRun(client, runId, options = {}) {
|
|
|
18281
19463
|
failures,
|
|
18282
19464
|
message: apiError.message
|
|
18283
19465
|
});
|
|
18284
|
-
await
|
|
19466
|
+
await sleep2(delayMs2, signal);
|
|
18285
19467
|
continue;
|
|
18286
19468
|
}
|
|
18287
19469
|
throw apiError;
|
|
@@ -18290,7 +19472,7 @@ async function followRun(client, runId, options = {}) {
|
|
|
18290
19472
|
failures += 1;
|
|
18291
19473
|
const delayMs2 = backoff();
|
|
18292
19474
|
options.onReconnect?.({ reason: "stream_failed", delayMs: delayMs2, lastEventId: state.lastEventId, failures, message: "empty body" });
|
|
18293
|
-
await
|
|
19475
|
+
await sleep2(delayMs2, signal);
|
|
18294
19476
|
continue;
|
|
18295
19477
|
}
|
|
18296
19478
|
let broke = null;
|
|
@@ -18308,7 +19490,7 @@ async function followRun(client, runId, options = {}) {
|
|
|
18308
19490
|
const event = { id: message.id, event: message.event, data: parseData(message.data) };
|
|
18309
19491
|
const applied = apply(event);
|
|
18310
19492
|
options.onEvent?.(event, state);
|
|
18311
|
-
const result = applied && "reconcileStatus" in applied ? await reconcileTerminalOutput(client, runId, applied, state, signal,
|
|
19493
|
+
const result = applied && "reconcileStatus" in applied ? await reconcileTerminalOutput(client, runId, applied, state, signal, sleep2) : applied;
|
|
18312
19494
|
if (result) return result;
|
|
18313
19495
|
throwIfAborted();
|
|
18314
19496
|
}
|
|
@@ -18328,12 +19510,12 @@ async function followRun(client, runId, options = {}) {
|
|
|
18328
19510
|
if (!sawEvent) failures = 0;
|
|
18329
19511
|
options.onReconnect?.({ reason: "stream_ended", delayMs, lastEventId: state.lastEventId, failures });
|
|
18330
19512
|
}
|
|
18331
|
-
await
|
|
19513
|
+
await sleep2(delayMs, signal);
|
|
18332
19514
|
}
|
|
18333
19515
|
}
|
|
18334
19516
|
var OUTPUT_READ_TIMEOUT_MS = 1500;
|
|
18335
19517
|
var OUTPUT_READ_DELAYS_MS = [100, 300];
|
|
18336
|
-
async function reconcileTerminalOutput(client, runId, pending, state, signal,
|
|
19518
|
+
async function reconcileTerminalOutput(client, runId, pending, state, signal, sleep2) {
|
|
18337
19519
|
const { reconcileStatus, ...result } = pending;
|
|
18338
19520
|
for (let attempt = 0; attempt < OUTPUT_READ_DELAYS_MS.length + 1; attempt += 1) {
|
|
18339
19521
|
if (signal?.aborted) throw new FollowAbortedError(state);
|
|
@@ -18351,7 +19533,7 @@ async function reconcileTerminalOutput(client, runId, pending, state, signal, sl
|
|
|
18351
19533
|
} catch {
|
|
18352
19534
|
if (signal?.aborted) throw new FollowAbortedError(state);
|
|
18353
19535
|
}
|
|
18354
|
-
if (attempt < OUTPUT_READ_DELAYS_MS.length) await
|
|
19536
|
+
if (attempt < OUTPUT_READ_DELAYS_MS.length) await sleep2(OUTPUT_READ_DELAYS_MS[attempt], signal);
|
|
18355
19537
|
}
|
|
18356
19538
|
return { ...result, text: state.text, outputSource: "incomplete" };
|
|
18357
19539
|
}
|
|
@@ -18385,7 +19567,7 @@ function registerTasksCommand(program2, deps = {}) {
|
|
|
18385
19567
|
}
|
|
18386
19568
|
r.out(formatRunTable(page.data, r.now()));
|
|
18387
19569
|
if (page.next_cursor) {
|
|
18388
|
-
r.err(
|
|
19570
|
+
r.err(chalk13.gray(`More: ultrac tasks --cursor ${page.next_cursor}
|
|
18389
19571
|
`));
|
|
18390
19572
|
}
|
|
18391
19573
|
});
|
|
@@ -18444,7 +19626,7 @@ async function runTask(promptParts, options, deps) {
|
|
|
18444
19626
|
`);
|
|
18445
19627
|
} else {
|
|
18446
19628
|
r.err(
|
|
18447
|
-
|
|
19629
|
+
chalk13.gray(
|
|
18448
19630
|
`Run ${run.id} queued. Following; Ctrl-C ${options.detach ? "leaves it running" : "cancels it"}.
|
|
18449
19631
|
`
|
|
18450
19632
|
)
|
|
@@ -18462,7 +19644,7 @@ async function follow(r, runId, options) {
|
|
|
18462
19644
|
controller.abort();
|
|
18463
19645
|
return;
|
|
18464
19646
|
}
|
|
18465
|
-
r.err(
|
|
19647
|
+
r.err(chalk13.yellow(`
|
|
18466
19648
|
Leaving run ${runId} as it is.
|
|
18467
19649
|
`));
|
|
18468
19650
|
r.exit(EXIT_CANCELLED);
|
|
@@ -18478,7 +19660,7 @@ Leaving run ${runId} as it is.
|
|
|
18478
19660
|
if (status === "running") {
|
|
18479
19661
|
const attempt = typeof data?.attempt === "number" ? data.attempt : null;
|
|
18480
19662
|
if (data?.restarted === true || attempt !== null && attempt > 1) {
|
|
18481
|
-
r.err(
|
|
19663
|
+
r.err(chalk13.yellow(`${lead()}[attempt ${attempt ?? "?"} restarted; streaming from the start]
|
|
18482
19664
|
`));
|
|
18483
19665
|
}
|
|
18484
19666
|
printed = "";
|
|
@@ -18493,7 +19675,7 @@ Leaving run ${runId} as it is.
|
|
|
18493
19675
|
return;
|
|
18494
19676
|
}
|
|
18495
19677
|
case "tool_call": {
|
|
18496
|
-
r.err(
|
|
19678
|
+
r.err(chalk13.cyan(`${lead()}[tool_call ${String(data?.name ?? "")} ${String(data?.arguments ?? "")}]
|
|
18497
19679
|
`));
|
|
18498
19680
|
return;
|
|
18499
19681
|
}
|
|
@@ -18533,7 +19715,7 @@ Leaving run ${runId} as it is.
|
|
|
18533
19715
|
`);
|
|
18534
19716
|
} else {
|
|
18535
19717
|
const seconds = Math.round(info.delayMs / 100) / 10;
|
|
18536
|
-
r.err(
|
|
19718
|
+
r.err(chalk13.gray(`${lead()}[reconnecting in ${seconds}s${info.message ? `: ${info.message}` : ""}]
|
|
18537
19719
|
`));
|
|
18538
19720
|
}
|
|
18539
19721
|
}
|
|
@@ -18565,27 +19747,27 @@ Leaving run ${runId} as it is.
|
|
|
18565
19747
|
return result.outputSource === "incomplete" ? EXIT_FAILED : exitCodeFor(result.status);
|
|
18566
19748
|
}
|
|
18567
19749
|
if (result.outputSource === "incomplete") {
|
|
18568
|
-
r.err(
|
|
19750
|
+
r.err(chalk13.yellow(lead() + "[stored task output could not be confirmed; showing text received from the stream]\n"));
|
|
18569
19751
|
}
|
|
18570
19752
|
switch (result.status) {
|
|
18571
19753
|
case "succeeded":
|
|
18572
19754
|
finishHuman(result.text);
|
|
18573
19755
|
if (result.toolCalls.length && !result.text) {
|
|
18574
19756
|
const count3 = result.toolCalls.length;
|
|
18575
|
-
r.err(
|
|
19757
|
+
r.err(chalk13.gray(`Done with ${count3} tool call${count3 === 1 ? "" : "s"} and no text.
|
|
18576
19758
|
`));
|
|
18577
19759
|
}
|
|
18578
19760
|
return result.outputSource === "incomplete" ? EXIT_FAILED : EXIT_DONE;
|
|
18579
19761
|
case "failed": {
|
|
18580
19762
|
finishHuman(result.text);
|
|
18581
19763
|
const code = result.error?.code ? ` (${result.error.code})` : "";
|
|
18582
|
-
r.err(
|
|
19764
|
+
r.err(chalk13.red(`Run ${runId} failed${code}: ${result.error?.message ?? "The task failed."}
|
|
18583
19765
|
`));
|
|
18584
19766
|
return EXIT_FAILED;
|
|
18585
19767
|
}
|
|
18586
19768
|
case "cancelled":
|
|
18587
19769
|
finishHuman(result.text);
|
|
18588
|
-
r.err(
|
|
19770
|
+
r.err(chalk13.yellow(`Run ${runId} was cancelled.
|
|
18589
19771
|
`));
|
|
18590
19772
|
return EXIT_CANCELLED;
|
|
18591
19773
|
}
|
|
@@ -18598,13 +19780,13 @@ async function interrupted(r, runId, options, printed) {
|
|
|
18598
19780
|
`);
|
|
18599
19781
|
} else {
|
|
18600
19782
|
r.err(
|
|
18601
|
-
|
|
19783
|
+
chalk13.yellow(`${lead}Detached. Run ${runId} continues; follow it again with \`ultrac tasks follow ${runId}\`.
|
|
18602
19784
|
`)
|
|
18603
19785
|
);
|
|
18604
19786
|
}
|
|
18605
19787
|
return EXIT_CANCELLED;
|
|
18606
19788
|
}
|
|
18607
|
-
if (!options.json) r.err(
|
|
19789
|
+
if (!options.json) r.err(chalk13.yellow(`${lead}Cancelling run ${runId}...
|
|
18608
19790
|
`));
|
|
18609
19791
|
try {
|
|
18610
19792
|
const cancelled = await r.client.cancelRun(runId);
|
|
@@ -18612,7 +19794,7 @@ async function interrupted(r, runId, options, printed) {
|
|
|
18612
19794
|
r.out(`${JSON.stringify({ id: null, event: "cancel", data: cancelled })}
|
|
18613
19795
|
`);
|
|
18614
19796
|
} else {
|
|
18615
|
-
r.err(
|
|
19797
|
+
r.err(chalk13.yellow(`${describeCancel(cancelled)}
|
|
18616
19798
|
`));
|
|
18617
19799
|
}
|
|
18618
19800
|
} catch (error) {
|
|
@@ -18621,12 +19803,12 @@ async function interrupted(r, runId, options, printed) {
|
|
|
18621
19803
|
r.out(`${JSON.stringify({ id: null, event: "cancel_failed", data: { runId, message } })}
|
|
18622
19804
|
`);
|
|
18623
19805
|
} else if (error instanceof UltracApiError && error.code === "run_already_finished") {
|
|
18624
|
-
r.err(
|
|
19806
|
+
r.err(chalk13.yellow(`${error.message} Nothing to cancel.
|
|
18625
19807
|
`));
|
|
18626
19808
|
} else {
|
|
18627
|
-
r.err(
|
|
19809
|
+
r.err(chalk13.red(`Could not cancel run ${runId}: ${message}
|
|
18628
19810
|
`));
|
|
18629
|
-
r.err(
|
|
19811
|
+
r.err(chalk13.gray(`Try again with \`ultrac tasks cancel ${runId}\`.
|
|
18630
19812
|
`));
|
|
18631
19813
|
}
|
|
18632
19814
|
}
|
|
@@ -18738,7 +19920,7 @@ ${run.output}${run.output.endsWith("\n") ? "" : "\n"}`;
|
|
|
18738
19920
|
|
|
18739
19921
|
// src/commands/tools.ts
|
|
18740
19922
|
import { resolve as resolve14 } from "path";
|
|
18741
|
-
import
|
|
19923
|
+
import chalk14 from "chalk";
|
|
18742
19924
|
function registerToolsCommand(program2) {
|
|
18743
19925
|
const toolsCommand = program2.command("tools");
|
|
18744
19926
|
toolsCommand.description("List built-in agent tools").option("--json", "Print tool metadata as JSON", false).option("--mode <name>", `Only the tools a mode advertises: ${agentModeNames.join(", ")}`).option("--budget", "Print what each mode's tool set costs in characters", false).action((options) => {
|
|
@@ -18758,7 +19940,7 @@ function registerToolsCommand(program2) {
|
|
|
18758
19940
|
printTools(tools);
|
|
18759
19941
|
if (options.mode) {
|
|
18760
19942
|
console.log(
|
|
18761
|
-
|
|
19943
|
+
chalk14.gray(
|
|
18762
19944
|
`
|
|
18763
19945
|
${tools.length} of ${all.length} tools advertised in ${options.mode} mode, ${serializedToolsSize(
|
|
18764
19946
|
toToolSchemas(tools)
|
|
@@ -18767,7 +19949,7 @@ ${tools.length} of ${all.length} tools advertised in ${options.mode} mode, ${ser
|
|
|
18767
19949
|
);
|
|
18768
19950
|
}
|
|
18769
19951
|
});
|
|
18770
|
-
toolsCommand.command("run").description("Execute one built-in agent tool").argument("<tool>", "Tool name").argument("[input]", "JSON object input", "{}").option("-w, --workspace <path>", "Workspace path", ".").option("--allow-write", "Allow file writes", false).option("--allow-shell", "Allow this tool to execute unsandboxed shell commands", false).option("--allow-network", "Allow HTTP(S) URL fetches", false).option("--result-json", "Print the tool result as JSON", false).action(async (tool, inputText, options) => {
|
|
19952
|
+
toolsCommand.command("run").description("Execute one built-in agent tool").argument("<tool>", "Tool name").argument("[input]", "JSON object input", "{}").option("-w, --workspace <path>", "Workspace path", ".").option("--allow-write", "Allow file writes", false).option("--allow-shell", "Allow this tool to execute unsandboxed shell commands", false).option("--allow-network", "Allow HTTP(S) URL fetches", false).option("--verbose-tools", "Print the complete tool result", false).option("--result-json", "Print the tool result as JSON", false).action(async (tool, inputText, options) => {
|
|
18771
19953
|
const input = parseToolInput(inputText);
|
|
18772
19954
|
const result = await createDefaultToolRegistry().execute(
|
|
18773
19955
|
{ tool, input },
|
|
@@ -18783,7 +19965,7 @@ ${tools.length} of ${all.length} tools advertised in ${options.mode} mode, ${ser
|
|
|
18783
19965
|
if (options.resultJson) {
|
|
18784
19966
|
console.log(JSON.stringify({ result }, null, 2));
|
|
18785
19967
|
} else {
|
|
18786
|
-
printToolResult(result);
|
|
19968
|
+
printToolResult(result, { verboseTools: options.verboseTools });
|
|
18787
19969
|
}
|
|
18788
19970
|
if (!result.ok) {
|
|
18789
19971
|
process.exitCode = 1;
|
|
@@ -18807,11 +19989,11 @@ function printToolBudget(all, json) {
|
|
|
18807
19989
|
}
|
|
18808
19990
|
for (const row of [...rows, { ...everything, names: all.map((tool) => tool.name) }]) {
|
|
18809
19991
|
console.log(
|
|
18810
|
-
`${
|
|
19992
|
+
`${chalk14.cyan(row.mode.padEnd(9))} ${String(row.tools).padStart(2)} tools ${String(
|
|
18811
19993
|
row.chars.toLocaleString("en-US")
|
|
18812
19994
|
).padStart(7)} chars`
|
|
18813
19995
|
);
|
|
18814
|
-
console.log(
|
|
19996
|
+
console.log(chalk14.gray(` ${row.names.join(", ")}`));
|
|
18815
19997
|
}
|
|
18816
19998
|
}
|
|
18817
19999
|
function parseToolInput(inputText) {
|
|
@@ -18823,7 +20005,7 @@ function parseToolInput(inputText) {
|
|
|
18823
20005
|
}
|
|
18824
20006
|
|
|
18825
20007
|
// src/commands/update.ts
|
|
18826
|
-
import
|
|
20008
|
+
import chalk15 from "chalk";
|
|
18827
20009
|
function registerUpdateCommand(program2) {
|
|
18828
20010
|
const packageInfo = readPackageInfo();
|
|
18829
20011
|
program2.command("update").alias("upgrade").description(`Check for and install the latest ${packageInfo.name} release`).option("--check", "Only check; do not install an available update", false).action(async (options) => {
|
|
@@ -18838,7 +20020,7 @@ function registerUpdateCommand(program2) {
|
|
|
18838
20020
|
);
|
|
18839
20021
|
}
|
|
18840
20022
|
if (!status.updateAvailable) {
|
|
18841
|
-
console.log(
|
|
20023
|
+
console.log(chalk15.green(`ultrac ${status.currentVersion} is up to date.`));
|
|
18842
20024
|
return;
|
|
18843
20025
|
}
|
|
18844
20026
|
if (options.check) {
|
|
@@ -18847,12 +20029,12 @@ function registerUpdateCommand(program2) {
|
|
|
18847
20029
|
}
|
|
18848
20030
|
console.log(`Updating ultrac ${status.currentVersion} \u2192 ${status.latestVersion}\u2026`);
|
|
18849
20031
|
await installLatestVersion(packageInfo.name);
|
|
18850
|
-
console.log(
|
|
20032
|
+
console.log(chalk15.green(`Updated to ultrac ${status.latestVersion}. Restart ultrac to use the new version.`));
|
|
18851
20033
|
});
|
|
18852
20034
|
}
|
|
18853
20035
|
function printAvailable(currentVersion, latestVersion) {
|
|
18854
|
-
console.log(
|
|
18855
|
-
console.log(
|
|
20036
|
+
console.log(chalk15.yellow(`Update available: ultrac ${currentVersion} \u2192 ${latestVersion}`));
|
|
20037
|
+
console.log(chalk15.gray("Run `ultrac update` to install it."));
|
|
18856
20038
|
}
|
|
18857
20039
|
|
|
18858
20040
|
// src/cli.ts
|
|
@@ -18878,10 +20060,11 @@ registerUpdateCommand(program);
|
|
|
18878
20060
|
registerSessionsCommand(program);
|
|
18879
20061
|
registerMemoryCommand(program);
|
|
18880
20062
|
registerMcpCommand(program);
|
|
20063
|
+
registerNotifyCommand(program);
|
|
18881
20064
|
registerSkillsCommand(program);
|
|
18882
20065
|
registerReplCommand(program);
|
|
18883
20066
|
program.parseAsync(process.argv).catch((error) => {
|
|
18884
20067
|
const message = error instanceof Error ? error.message : String(error);
|
|
18885
|
-
console.error(
|
|
20068
|
+
console.error(chalk16.red(message));
|
|
18886
20069
|
process.exitCode = 1;
|
|
18887
20070
|
});
|