@spinalfluid/ultrac 0.1.0 → 0.1.6
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 +23 -5
- package/dist/cli.js +1309 -427
- 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
|
};
|
|
@@ -878,20 +892,32 @@ function contextUsageStatus(messages, usage, disposition) {
|
|
|
878
892
|
...disposition.error ? { error: disposition.error } : {}
|
|
879
893
|
};
|
|
880
894
|
}
|
|
895
|
+
function contextOverflowMessage(usage, freedChars, safeWindowChars, pinned) {
|
|
896
|
+
const numbers = `system ${count(usage.systemChars)} + tools ${count(usage.toolsChars)} + history ${count(
|
|
897
|
+
usage.historyChars
|
|
898
|
+
)} = ${count(usage.totalChars)} characters against a ${count(usage.windowChars)}-character window (${count(
|
|
899
|
+
usage.contextTokens
|
|
900
|
+
)} 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` : ""}`;
|
|
901
|
+
if (pinned) {
|
|
902
|
+
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.`;
|
|
903
|
+
}
|
|
904
|
+
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.`;
|
|
905
|
+
}
|
|
881
906
|
var ContextBudgetError = class extends Error {
|
|
882
907
|
usage;
|
|
883
908
|
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
|
-
);
|
|
909
|
+
super(contextOverflowMessage(usage, freedChars, safeWindowChars, false));
|
|
891
910
|
this.name = "ContextBudgetError";
|
|
892
911
|
this.usage = usage;
|
|
893
912
|
}
|
|
894
913
|
};
|
|
914
|
+
var PinnedContextBudgetError = class extends ContextBudgetError {
|
|
915
|
+
constructor(usage, freedChars, safeWindowChars = usage.windowChars) {
|
|
916
|
+
super(usage, freedChars, safeWindowChars);
|
|
917
|
+
this.name = "PinnedContextBudgetError";
|
|
918
|
+
this.message = contextOverflowMessage(usage, freedChars, safeWindowChars, true);
|
|
919
|
+
}
|
|
920
|
+
};
|
|
895
921
|
function planToolCallRequest(input) {
|
|
896
922
|
return planRequest({
|
|
897
923
|
units: groupNativeMessages(input.messages, input.isPinned),
|
|
@@ -964,6 +990,10 @@ function planRequest(input) {
|
|
|
964
990
|
const usage = summarize(kept, input.toolsChars, window);
|
|
965
991
|
const safeWindowChars = Math.floor(usage.windowChars * HARD_WINDOW_SAFETY_FRACTION);
|
|
966
992
|
if (usage.totalChars > safeWindowChars) {
|
|
993
|
+
const droppableLeft = kept.some((unit) => !unit.pinned && !unit.system);
|
|
994
|
+
if (!droppableLeft && kept.some((unit) => unit.pinned && !unit.system)) {
|
|
995
|
+
throw new PinnedContextBudgetError(usage, freedChars, safeWindowChars);
|
|
996
|
+
}
|
|
967
997
|
throw new ContextBudgetError(usage, freedChars, safeWindowChars);
|
|
968
998
|
}
|
|
969
999
|
return {
|
|
@@ -3769,12 +3799,17 @@ import { z as z6 } from "zod";
|
|
|
3769
3799
|
|
|
3770
3800
|
// src/core/ask-runtime.ts
|
|
3771
3801
|
var askUserRuntime;
|
|
3772
|
-
|
|
3802
|
+
var askUserRuntimeRemote = false;
|
|
3803
|
+
function setAskUserRuntime(fn, options = {}) {
|
|
3773
3804
|
askUserRuntime = fn;
|
|
3805
|
+
askUserRuntimeRemote = fn !== void 0 && options.remote === true;
|
|
3774
3806
|
}
|
|
3775
3807
|
function getAskUserRuntime() {
|
|
3776
3808
|
return askUserRuntime;
|
|
3777
3809
|
}
|
|
3810
|
+
function isAskUserRuntimeRemote() {
|
|
3811
|
+
return askUserRuntimeRemote;
|
|
3812
|
+
}
|
|
3778
3813
|
|
|
3779
3814
|
// src/tools/workspace/ask-user.ts
|
|
3780
3815
|
var AskUserInput = z6.object({
|
|
@@ -3796,7 +3831,7 @@ var askUserTool = {
|
|
|
3796
3831
|
async execute(input, context) {
|
|
3797
3832
|
const ask2 = getAskUserRuntime();
|
|
3798
3833
|
if (!ask2) return { ok: false, content: NO_ONE_TO_ASK };
|
|
3799
|
-
if (!stdin.isTTY) return { ok: false, content: NO_ONE_TO_ASK };
|
|
3834
|
+
if (!isAskUserRuntimeRemote() && !stdin.isTTY) return { ok: false, content: NO_ONE_TO_ASK };
|
|
3800
3835
|
const answer = await ask2(input.question, input.options, context.signal);
|
|
3801
3836
|
if (context.signal?.aborted) {
|
|
3802
3837
|
return { ok: false, content: "Question cancelled; no answer was recorded." };
|
|
@@ -3927,17 +3962,17 @@ var SENSITIVE_ENV_PATTERNS = [
|
|
|
3927
3962
|
/(?:^|_)WEBHOOK_SECRET$/
|
|
3928
3963
|
];
|
|
3929
3964
|
function createSanitizedProcessEnv(source = process.env) {
|
|
3930
|
-
const
|
|
3965
|
+
const env2 = {};
|
|
3931
3966
|
const redactedEnvKeys = [];
|
|
3932
3967
|
for (const [key, value] of Object.entries(source)) {
|
|
3933
3968
|
if (isSensitiveEnvName(key)) {
|
|
3934
3969
|
redactedEnvKeys.push(key);
|
|
3935
3970
|
continue;
|
|
3936
3971
|
}
|
|
3937
|
-
|
|
3972
|
+
env2[key] = value;
|
|
3938
3973
|
}
|
|
3939
3974
|
redactedEnvKeys.sort();
|
|
3940
|
-
return { env, redactedEnvKeys };
|
|
3975
|
+
return { env: env2, redactedEnvKeys };
|
|
3941
3976
|
}
|
|
3942
3977
|
function isSensitiveEnvName(name) {
|
|
3943
3978
|
const normalized = name.toUpperCase();
|
|
@@ -4026,12 +4061,12 @@ function runProcess(command, args, options) {
|
|
|
4026
4061
|
redactedEnvKeys: []
|
|
4027
4062
|
});
|
|
4028
4063
|
}
|
|
4029
|
-
const { env, redactedEnvKeys } = createSanitizedProcessEnv();
|
|
4064
|
+
const { env: env2, redactedEnvKeys } = createSanitizedProcessEnv();
|
|
4030
4065
|
return new Promise((resolve15, reject) => {
|
|
4031
4066
|
const child = spawn(command, args, {
|
|
4032
4067
|
cwd: options.cwd,
|
|
4033
4068
|
stdio: ["pipe", "pipe", "pipe"],
|
|
4034
|
-
env,
|
|
4069
|
+
env: env2,
|
|
4035
4070
|
shell: options.shell ?? false,
|
|
4036
4071
|
// Its own process group, so the timeout can kill what the child started
|
|
4037
4072
|
// as well as the child. Killing only the direct child leaves the grand-
|
|
@@ -5116,7 +5151,7 @@ function defaultContextFiles(files) {
|
|
|
5116
5151
|
}
|
|
5117
5152
|
function isPriorityContextFile(path) {
|
|
5118
5153
|
const normalized = path.toLowerCase();
|
|
5119
|
-
const
|
|
5154
|
+
const basename4 = normalized.split("/").at(-1) ?? normalized;
|
|
5120
5155
|
return [
|
|
5121
5156
|
"agents.md",
|
|
5122
5157
|
"claude.md",
|
|
@@ -5126,12 +5161,12 @@ function isPriorityContextFile(path) {
|
|
|
5126
5161
|
"pyproject.toml",
|
|
5127
5162
|
"readme.md",
|
|
5128
5163
|
"tsconfig.json"
|
|
5129
|
-
].includes(
|
|
5164
|
+
].includes(basename4) || normalized === "docs/architecture.md" || normalized === "docs/roadmap.md";
|
|
5130
5165
|
}
|
|
5131
5166
|
function isLikelyEntrypoint(path) {
|
|
5132
5167
|
const normalized = path.toLowerCase();
|
|
5133
|
-
const
|
|
5134
|
-
return
|
|
5168
|
+
const basename4 = normalized.split("/").at(-1) ?? normalized;
|
|
5169
|
+
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
5170
|
}
|
|
5136
5171
|
function summarizeRepository(files) {
|
|
5137
5172
|
return {
|
|
@@ -7137,8 +7172,8 @@ var scanSecretsTool = {
|
|
|
7137
7172
|
}
|
|
7138
7173
|
scannedFiles += 1;
|
|
7139
7174
|
const text = bytes.toString("utf8");
|
|
7140
|
-
const
|
|
7141
|
-
const skipEntropy = lockfileNames.has(
|
|
7175
|
+
const basename4 = file.split("/").pop() ?? file;
|
|
7176
|
+
const skipEntropy = lockfileNames.has(basename4);
|
|
7142
7177
|
collectLineFindings(file, text, rules, skipEntropy, findings, input.maxFindings);
|
|
7143
7178
|
}
|
|
7144
7179
|
const visible = findings.slice(0, input.maxFindings);
|
|
@@ -7166,7 +7201,7 @@ function compileCustomRules(rules) {
|
|
|
7166
7201
|
}
|
|
7167
7202
|
function collectLineFindings(path, text, rules, skipLockfileRules, findings, maxFindings) {
|
|
7168
7203
|
const lines = text.split(/\r?\n/);
|
|
7169
|
-
const
|
|
7204
|
+
const basename4 = path.split("/").pop() ?? path;
|
|
7170
7205
|
for (const [index, line] of lines.entries()) {
|
|
7171
7206
|
if (findings.length >= maxFindings) return;
|
|
7172
7207
|
if (line.length > 4e3) continue;
|
|
@@ -7183,7 +7218,7 @@ function collectLineFindings(path, text, rules, skipLockfileRules, findings, max
|
|
|
7183
7218
|
snippet: redactSecret(match[0])
|
|
7184
7219
|
});
|
|
7185
7220
|
}
|
|
7186
|
-
if (skipLockfileRules || lockfileNames.has(
|
|
7221
|
+
if (skipLockfileRules || lockfileNames.has(basename4) || findings.length >= maxFindings) continue;
|
|
7187
7222
|
collectEntropyFinding(path, index + 1, line, findings);
|
|
7188
7223
|
}
|
|
7189
7224
|
}
|
|
@@ -8542,7 +8577,7 @@ function summarizeExtensions(files) {
|
|
|
8542
8577
|
}
|
|
8543
8578
|
function isImportantFile(path) {
|
|
8544
8579
|
const normalized = path.toLowerCase();
|
|
8545
|
-
const
|
|
8580
|
+
const basename4 = normalized.split("/").at(-1) ?? normalized;
|
|
8546
8581
|
if ([
|
|
8547
8582
|
"agents.md",
|
|
8548
8583
|
"claude.md",
|
|
@@ -8555,7 +8590,7 @@ function isImportantFile(path) {
|
|
|
8555
8590
|
].includes(normalized)) {
|
|
8556
8591
|
return true;
|
|
8557
8592
|
}
|
|
8558
|
-
return
|
|
8593
|
+
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
8594
|
}
|
|
8560
8595
|
function formatBytes(bytes) {
|
|
8561
8596
|
if (bytes < 1024) return `${bytes} B`;
|
|
@@ -9864,59 +9899,40 @@ var AgentRunner = class {
|
|
|
9864
9899
|
const mode = options.mode ?? "default";
|
|
9865
9900
|
const modePreset = getAgentModePreset(mode);
|
|
9866
9901
|
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
|
-
]);
|
|
9902
|
+
const registered = this.registry.list();
|
|
9903
|
+
const advertised = resolveAdvertisedTools(registered, mode, options.tools ?? "mode");
|
|
9904
|
+
const workspaceContext = await gatherWorkspaceContext(
|
|
9905
|
+
this.registry,
|
|
9906
|
+
options.workspace,
|
|
9907
|
+
advertised.map((tool) => tool.name)
|
|
9908
|
+
);
|
|
9887
9909
|
const fileProvenance = /* @__PURE__ */ new WeakMap();
|
|
9888
9910
|
const restored = restorePreviousTranscript(previousSessions, options.maxToolResultChars, fileProvenance);
|
|
9889
9911
|
if (previousSession) {
|
|
9890
9912
|
noteContextEvent(describeRestore(previousSession, previousSessions, restored, priorConstraints));
|
|
9891
9913
|
}
|
|
9914
|
+
const workspaceMessage = {
|
|
9915
|
+
role: "user",
|
|
9916
|
+
content: workspaceContext
|
|
9917
|
+
};
|
|
9892
9918
|
const taskMessage = {
|
|
9893
9919
|
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
|
-
)
|
|
9920
|
+
content: buildTaskUserMessage(options.task, modePreset.label, options.previousSession, restored)
|
|
9909
9921
|
};
|
|
9922
|
+
fileProvenance.set(workspaceMessage, unknownContextProvenance());
|
|
9910
9923
|
fileProvenance.set(taskMessage, unknownContextProvenance());
|
|
9911
9924
|
const constraintMessages = inheritedConstraints.map((constraint) => ({
|
|
9912
9925
|
role: "user",
|
|
9913
9926
|
content: constraint.content
|
|
9914
9927
|
}));
|
|
9915
|
-
const messages = [
|
|
9928
|
+
const messages = [
|
|
9929
|
+
...restored.messages,
|
|
9930
|
+
...constraintMessages,
|
|
9931
|
+
workspaceMessage,
|
|
9932
|
+
taskMessage
|
|
9933
|
+
];
|
|
9916
9934
|
const pinnedMessages = [...constraintMessages];
|
|
9917
9935
|
const steps = [];
|
|
9918
|
-
const registered = this.registry.list();
|
|
9919
|
-
const advertised = resolveAdvertisedTools(registered, mode, options.tools ?? "mode");
|
|
9920
9936
|
const toolsByName = new Map(
|
|
9921
9937
|
registered.map((tool) => [tool.name, tool])
|
|
9922
9938
|
);
|
|
@@ -9957,7 +9973,7 @@ var AgentRunner = class {
|
|
|
9957
9973
|
break;
|
|
9958
9974
|
}
|
|
9959
9975
|
const nativeTools = options.nativeTools !== false && Boolean(options.provider.supportsToolCalling && options.provider.chatWithTools);
|
|
9960
|
-
const system = buildSystemPrompt(advertised, modePermissions, modePreset, nativeTools);
|
|
9976
|
+
const system = buildSystemPrompt(advertised, modePermissions, modePreset, nativeTools, options.autonomous);
|
|
9961
9977
|
const requestContext = {
|
|
9962
9978
|
turn: index + 1,
|
|
9963
9979
|
mode,
|
|
@@ -10027,6 +10043,8 @@ var AgentRunner = class {
|
|
|
10027
10043
|
role: "assistant",
|
|
10028
10044
|
content: JSON.stringify(parsed)
|
|
10029
10045
|
});
|
|
10046
|
+
steps.push(step);
|
|
10047
|
+
await checkpoint();
|
|
10030
10048
|
const modeAllowlist = getEnforcedModeToolAllowlist(mode);
|
|
10031
10049
|
const toolContext = {
|
|
10032
10050
|
workspace: options.workspace,
|
|
@@ -10055,6 +10073,7 @@ var AgentRunner = class {
|
|
|
10055
10073
|
pinnedMessages,
|
|
10056
10074
|
fileProvenance
|
|
10057
10075
|
);
|
|
10076
|
+
await checkpoint();
|
|
10058
10077
|
if (options.signal?.aborted) {
|
|
10059
10078
|
stopReason = "aborted";
|
|
10060
10079
|
break;
|
|
@@ -10107,6 +10126,7 @@ var AgentRunner = class {
|
|
|
10107
10126
|
pinnedMessages,
|
|
10108
10127
|
fileProvenance
|
|
10109
10128
|
);
|
|
10129
|
+
await checkpoint();
|
|
10110
10130
|
}
|
|
10111
10131
|
publishAgentProgress({ type: "batch-end", batchId });
|
|
10112
10132
|
if (options.signal?.aborted) {
|
|
@@ -10114,8 +10134,6 @@ var AgentRunner = class {
|
|
|
10114
10134
|
break;
|
|
10115
10135
|
}
|
|
10116
10136
|
}
|
|
10117
|
-
steps.push(step);
|
|
10118
|
-
await checkpoint();
|
|
10119
10137
|
if (stopReason === "aborted") break;
|
|
10120
10138
|
if (parsed.done && parsed.toolCalls.length === 0) {
|
|
10121
10139
|
stopReason = "completed";
|
|
@@ -10159,6 +10177,10 @@ async function compactIfNeeded(options, messages, system, task, constraints, con
|
|
|
10159
10177
|
summarisedTurns: plan.summarisedTurns
|
|
10160
10178
|
});
|
|
10161
10179
|
} catch (error) {
|
|
10180
|
+
if (error instanceof PinnedContextBudgetError) {
|
|
10181
|
+
trace.end({ outcome: "budget-error", error });
|
|
10182
|
+
throw error;
|
|
10183
|
+
}
|
|
10162
10184
|
if (!(error instanceof ContextBudgetError)) {
|
|
10163
10185
|
trace.end({ outcome: "error", error });
|
|
10164
10186
|
throw error;
|
|
@@ -10220,52 +10242,28 @@ function capResultForTranscript(result, maxChars) {
|
|
|
10220
10242
|
});
|
|
10221
10243
|
return capped.truncated ? { ...result, content: capped.content } : result;
|
|
10222
10244
|
}
|
|
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
|
-
)
|
|
10245
|
+
var STARTUP_EXPLORE_TOOLS = [
|
|
10246
|
+
"inspect_project",
|
|
10247
|
+
"repo_map",
|
|
10248
|
+
"repo_handbook",
|
|
10249
|
+
"git_worktree_summary",
|
|
10250
|
+
"git_status",
|
|
10251
|
+
"list_files",
|
|
10252
|
+
"read_file",
|
|
10253
|
+
"read_many_files",
|
|
10254
|
+
"search_files"
|
|
10255
|
+
];
|
|
10256
|
+
async function gatherWorkspaceContext(registry, workspace, advertisedNames) {
|
|
10257
|
+
const [memory, skills] = await Promise.all([
|
|
10258
|
+
loadMemoryContext(workspace),
|
|
10259
|
+
registry.list().some((tool) => tool.name === "skill") ? loadSkillsContext(workspace) : Promise.resolve("Skills are unavailable in this tool set.")
|
|
10248
10260
|
]);
|
|
10249
|
-
const
|
|
10261
|
+
const available = new Set(advertisedNames ?? registry.list().map((tool) => tool.name));
|
|
10262
|
+
const explore = STARTUP_EXPLORE_TOOLS.filter((name) => available.has(name));
|
|
10263
|
+
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
10264
|
return `Workspace: ${workspace}
|
|
10251
10265
|
|
|
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)}
|
|
10266
|
+
${exploreLine}
|
|
10269
10267
|
|
|
10270
10268
|
Memory (stored facts - load a specific one with memory_read):
|
|
10271
10269
|
${memory}
|
|
@@ -10273,42 +10271,21 @@ ${memory}
|
|
|
10273
10271
|
Skills (playbooks - load full steps with the skill tool before acting on a matching task):
|
|
10274
10272
|
${skills}`;
|
|
10275
10273
|
}
|
|
10276
|
-
function
|
|
10274
|
+
function buildTaskUserMessage(task, modeLabel, previousSession, restored) {
|
|
10277
10275
|
const previousContext = previousSession ? `
|
|
10278
10276
|
Previous session:
|
|
10279
10277
|
${summarizePreviousSession(previousSession, restored)}
|
|
10280
10278
|
` : "";
|
|
10281
|
-
return `
|
|
10282
|
-
Mode: ${modeLabel}
|
|
10279
|
+
return `Mode: ${modeLabel}
|
|
10283
10280
|
${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
10281
|
Task:
|
|
10310
10282
|
${task}`;
|
|
10311
10283
|
}
|
|
10284
|
+
function advertisedExploreGuidance(advertised) {
|
|
10285
|
+
const names = STARTUP_EXPLORE_TOOLS.filter((name) => advertised.has(name));
|
|
10286
|
+
if (!names.length) return void 0;
|
|
10287
|
+
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.`;
|
|
10288
|
+
}
|
|
10312
10289
|
var NOTHING_RESTORED = {
|
|
10313
10290
|
messages: [],
|
|
10314
10291
|
steps: 0,
|
|
@@ -10372,7 +10349,15 @@ function summarizePreviousSession(session, restored) {
|
|
|
10372
10349
|
function plural3(value, noun) {
|
|
10373
10350
|
return `${value.toLocaleString("en-US")} ${noun}${value === 1 ? "" : "s"}`;
|
|
10374
10351
|
}
|
|
10375
|
-
function
|
|
10352
|
+
function autonomousGuidance(phone, askUserAdvertised) {
|
|
10353
|
+
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.";
|
|
10354
|
+
return `
|
|
10355
|
+
Autonomous run: no human is watching and every permission is granted.
|
|
10356
|
+
- 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.
|
|
10357
|
+
- When something fails, diagnose it and try another approach before giving up.${ask2}
|
|
10358
|
+
`;
|
|
10359
|
+
}
|
|
10360
|
+
function buildSystemPrompt(tools, permissions, mode, nativeTools = false, autonomous) {
|
|
10376
10361
|
const renderedTools = tools.map((tool) => {
|
|
10377
10362
|
const permissionText = tool.permissions.length ? ` Requires: ${tool.permissions.join(", ")}.` : "";
|
|
10378
10363
|
const optionalText = tool.optionalPermissions.length ? ` Uses when granted: ${tool.optionalPermissions.join(", ")}.` : "";
|
|
@@ -10380,6 +10365,7 @@ function buildSystemPrompt(tools, permissions, mode, nativeTools = false) {
|
|
|
10380
10365
|
}).join("\n");
|
|
10381
10366
|
const advertised = new Set(tools.map((tool) => tool.name));
|
|
10382
10367
|
const guidance = [
|
|
10368
|
+
advertisedExploreGuidance(advertised),
|
|
10383
10369
|
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
10370
|
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
10371
|
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 +10375,12 @@ function buildSystemPrompt(tools, permissions, mode, nativeTools = false) {
|
|
|
10389
10375
|
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
10376
|
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
10377
|
].filter((line) => Boolean(line));
|
|
10378
|
+
const autonomyLines = autonomous ? autonomousGuidance(autonomous.phone, advertised.has("ask_user")) : "";
|
|
10392
10379
|
return `You are ultrac, the terminal-native coding agent for Ultrac.
|
|
10393
10380
|
|
|
10394
10381
|
Mode: ${mode.label}
|
|
10395
10382
|
${mode.instructions.map((instruction) => `- ${instruction}`).join("\n")}
|
|
10396
|
-
|
|
10383
|
+
${autonomyLines}
|
|
10397
10384
|
${UNTRUSTED_CONTENT_GUIDANCE}
|
|
10398
10385
|
|
|
10399
10386
|
${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:
|
|
@@ -11186,16 +11173,16 @@ function normalizeFilter(filter, workspace) {
|
|
|
11186
11173
|
function tokenizeCommand(command) {
|
|
11187
11174
|
const tokens = [];
|
|
11188
11175
|
let current = "";
|
|
11189
|
-
let
|
|
11176
|
+
let quote2 = null;
|
|
11190
11177
|
let started = false;
|
|
11191
11178
|
for (const character of command) {
|
|
11192
|
-
if (
|
|
11193
|
-
if (character ===
|
|
11179
|
+
if (quote2) {
|
|
11180
|
+
if (character === quote2) quote2 = null;
|
|
11194
11181
|
else current += character;
|
|
11195
11182
|
continue;
|
|
11196
11183
|
}
|
|
11197
11184
|
if (character === '"' || character === "'") {
|
|
11198
|
-
|
|
11185
|
+
quote2 = character;
|
|
11199
11186
|
started = true;
|
|
11200
11187
|
continue;
|
|
11201
11188
|
}
|
|
@@ -12900,6 +12887,7 @@ import { stdin as stdin3 } from "process";
|
|
|
12900
12887
|
|
|
12901
12888
|
// src/auth/browser-login.ts
|
|
12902
12889
|
import { hostname as osHostname } from "os";
|
|
12890
|
+
import { randomInt } from "crypto";
|
|
12903
12891
|
|
|
12904
12892
|
// src/auth/loopback.ts
|
|
12905
12893
|
import http from "http";
|
|
@@ -13097,6 +13085,7 @@ function errorMessage(error) {
|
|
|
13097
13085
|
|
|
13098
13086
|
// src/auth/browser-login.ts
|
|
13099
13087
|
var BROWSER_LOGIN_REQUEST_TIMEOUT_MS = 15e3;
|
|
13088
|
+
var BROWSER_LOGIN_CALLBACK_TIMEOUT_MS = 2 * 60 * 1e3;
|
|
13100
13089
|
var BrowserLoginError = class extends Error {
|
|
13101
13090
|
code;
|
|
13102
13091
|
constructor(code, message) {
|
|
@@ -13110,16 +13099,20 @@ async function loginWithBrowser(input) {
|
|
|
13110
13099
|
const hostname = (input.hostname ?? osHostname()).trim();
|
|
13111
13100
|
const fetchImpl = input.fetchImpl ?? ((url, init) => fetch(url, init));
|
|
13112
13101
|
const requestTimeoutMs = input.requestTimeoutMs ?? BROWSER_LOGIN_REQUEST_TIMEOUT_MS;
|
|
13102
|
+
const callbackTimeoutMs = input.timeoutMs ?? BROWSER_LOGIN_CALLBACK_TIMEOUT_MS;
|
|
13113
13103
|
const pkce = generatePkce();
|
|
13114
13104
|
const state = generateOAuthState();
|
|
13115
13105
|
const log = input.onInfo ?? (() => void 0);
|
|
13106
|
+
const startLoopback = input.startLoopback ?? startLoopbackServer;
|
|
13116
13107
|
let loopback;
|
|
13108
|
+
let redirectUri;
|
|
13117
13109
|
try {
|
|
13118
|
-
loopback = await
|
|
13110
|
+
loopback = await startLoopback({ expectedState: state, timeoutMs: callbackTimeoutMs });
|
|
13111
|
+
redirectUri = loopback.redirectUri;
|
|
13119
13112
|
} catch (error) {
|
|
13120
|
-
|
|
13121
|
-
|
|
13122
|
-
`Could not bind a local callback server (${error instanceof Error ? error.message : String(error)})
|
|
13113
|
+
redirectUri = `http://127.0.0.1:${randomInt(49152, 65536)}/callback`;
|
|
13114
|
+
log(
|
|
13115
|
+
`Could not bind a local callback server (${error instanceof Error ? error.message : String(error)}).`
|
|
13123
13116
|
);
|
|
13124
13117
|
}
|
|
13125
13118
|
try {
|
|
@@ -13129,7 +13122,7 @@ async function loginWithBrowser(input) {
|
|
|
13129
13122
|
{
|
|
13130
13123
|
client: "ultrac",
|
|
13131
13124
|
hostname,
|
|
13132
|
-
redirect_uri:
|
|
13125
|
+
redirect_uri: redirectUri,
|
|
13133
13126
|
state,
|
|
13134
13127
|
code_challenge: pkce.challenge,
|
|
13135
13128
|
code_challenge_method: pkce.method
|
|
@@ -13153,26 +13146,27 @@ async function loginWithBrowser(input) {
|
|
|
13153
13146
|
if (input.openBrowser(verificationUrl) === false) {
|
|
13154
13147
|
log("If a browser did not open, paste the URL above.");
|
|
13155
13148
|
}
|
|
13156
|
-
|
|
13157
|
-
if (
|
|
13158
|
-
|
|
13159
|
-
|
|
13160
|
-
|
|
13161
|
-
|
|
13162
|
-
|
|
13149
|
+
let code;
|
|
13150
|
+
if (loopback) {
|
|
13151
|
+
const callback = await loopback.wait;
|
|
13152
|
+
if (callback.ok) {
|
|
13153
|
+
code = callback.code;
|
|
13154
|
+
} else {
|
|
13155
|
+
if (callback.error === "access_denied") {
|
|
13156
|
+
throw new BrowserLoginError("access_denied", "Sign-in was denied in the browser.");
|
|
13157
|
+
}
|
|
13158
|
+
code = await requestRecoveryCode(input, log, callback.description);
|
|
13163
13159
|
}
|
|
13164
|
-
|
|
13165
|
-
|
|
13166
|
-
callback.description ?? `Browser login failed (${callback.error}).`
|
|
13167
|
-
);
|
|
13160
|
+
} else {
|
|
13161
|
+
code = await requestRecoveryCode(input, log);
|
|
13168
13162
|
}
|
|
13169
13163
|
const exchanged = await postJson(
|
|
13170
13164
|
fetchImpl,
|
|
13171
13165
|
`${origin}/api/cli/login/exchange`,
|
|
13172
13166
|
{
|
|
13173
|
-
code
|
|
13167
|
+
code,
|
|
13174
13168
|
code_verifier: pkce.verifier,
|
|
13175
|
-
redirect_uri:
|
|
13169
|
+
redirect_uri: redirectUri
|
|
13176
13170
|
},
|
|
13177
13171
|
requestTimeoutMs
|
|
13178
13172
|
);
|
|
@@ -13193,7 +13187,32 @@ async function loginWithBrowser(input) {
|
|
|
13193
13187
|
label: exchanged.body.label
|
|
13194
13188
|
};
|
|
13195
13189
|
} finally {
|
|
13196
|
-
await loopback
|
|
13190
|
+
await loopback?.close().catch(() => void 0);
|
|
13191
|
+
}
|
|
13192
|
+
}
|
|
13193
|
+
async function requestRecoveryCode(input, log, reason) {
|
|
13194
|
+
if (!input.requestCompletionCode) {
|
|
13195
|
+
throw new BrowserLoginError(
|
|
13196
|
+
"callback_failed",
|
|
13197
|
+
`${reason ? `${reason} ` : ""}Browser completion did not reach this terminal. Run ultrac login again.`
|
|
13198
|
+
);
|
|
13199
|
+
}
|
|
13200
|
+
log("Browser completion did not reach this terminal.");
|
|
13201
|
+
log("Copy the short-lived completion code (or the failed localhost callback URL) from the browser.");
|
|
13202
|
+
const supplied = (await input.requestCompletionCode()).trim();
|
|
13203
|
+
const code = completionCodeFromInput(supplied);
|
|
13204
|
+
if (!code) {
|
|
13205
|
+
throw new BrowserLoginError("missing_code", "No completion code was provided. Nothing was saved.");
|
|
13206
|
+
}
|
|
13207
|
+
return code;
|
|
13208
|
+
}
|
|
13209
|
+
function completionCodeFromInput(value) {
|
|
13210
|
+
if (!value) return "";
|
|
13211
|
+
try {
|
|
13212
|
+
const url = new URL(value);
|
|
13213
|
+
return url.searchParams.get("code")?.trim() ?? "";
|
|
13214
|
+
} catch {
|
|
13215
|
+
return value;
|
|
13197
13216
|
}
|
|
13198
13217
|
}
|
|
13199
13218
|
async function postJson(fetchImpl, url, body, timeoutMs) {
|
|
@@ -13376,7 +13395,8 @@ async function browserLogin(baseUrl, shouldOpen) {
|
|
|
13376
13395
|
baseUrl,
|
|
13377
13396
|
hostname: osHostname2(),
|
|
13378
13397
|
openBrowser: (url) => shouldOpen ? openBrowser(url) : false,
|
|
13379
|
-
onInfo: (line) => console.log(line)
|
|
13398
|
+
onInfo: (line) => console.log(line),
|
|
13399
|
+
requestCompletionCode: () => promptSecret("Paste the one-time completion code: ")
|
|
13380
13400
|
});
|
|
13381
13401
|
} catch (error) {
|
|
13382
13402
|
if (error instanceof BrowserLoginError) throw new Error(error.message);
|
|
@@ -13400,7 +13420,10 @@ async function verifyAndStore(input) {
|
|
|
13400
13420
|
} else {
|
|
13401
13421
|
console.log(chalk4.yellow("unverified"));
|
|
13402
13422
|
console.log(chalk4.yellow(` Could not verify the key (${check.message}).`));
|
|
13403
|
-
|
|
13423
|
+
if (!input.pasted) {
|
|
13424
|
+
throw new Error("Nothing was saved. Browser sign-in keys are stored only after the API accepts them.");
|
|
13425
|
+
}
|
|
13426
|
+
console.log(chalk4.yellow(" Saving the explicitly supplied key; run `ultrac doctor` once the API is reachable."));
|
|
13404
13427
|
}
|
|
13405
13428
|
const path = await writeCredentials({
|
|
13406
13429
|
apiKey: input.apiKey,
|
|
@@ -13917,24 +13940,24 @@ var McpConnection = class {
|
|
|
13917
13940
|
};
|
|
13918
13941
|
function childEnvFor(spec, source = process.env) {
|
|
13919
13942
|
const { env: sanitized } = createSanitizedProcessEnv(source);
|
|
13920
|
-
const
|
|
13943
|
+
const env2 = { ...sanitized };
|
|
13921
13944
|
for (const key of spec.envPassthrough) {
|
|
13922
13945
|
if (isSensitiveEnvName(key)) continue;
|
|
13923
13946
|
const value = source[key];
|
|
13924
13947
|
if (value !== void 0) {
|
|
13925
|
-
|
|
13948
|
+
env2[key] = value;
|
|
13926
13949
|
}
|
|
13927
13950
|
}
|
|
13928
|
-
Object.assign(
|
|
13929
|
-
return
|
|
13951
|
+
Object.assign(env2, spec.env);
|
|
13952
|
+
return env2;
|
|
13930
13953
|
}
|
|
13931
13954
|
function spawnStdioTransport(spec, workspace) {
|
|
13932
|
-
const
|
|
13955
|
+
const env2 = childEnvFor(spec);
|
|
13933
13956
|
const cwd = spec.cwd ? resolveInsideWorkspace(workspace, spec.cwd) : workspace;
|
|
13934
13957
|
const child = spawn3(spec.command, spec.args, {
|
|
13935
13958
|
cwd,
|
|
13936
13959
|
stdio: ["pipe", "pipe", "pipe"],
|
|
13937
|
-
env,
|
|
13960
|
+
env: env2,
|
|
13938
13961
|
detached: process.platform !== "win32",
|
|
13939
13962
|
windowsHide: true
|
|
13940
13963
|
});
|
|
@@ -14526,10 +14549,479 @@ async function readStdin() {
|
|
|
14526
14549
|
return Buffer.concat(chunks).toString("utf8");
|
|
14527
14550
|
}
|
|
14528
14551
|
|
|
14552
|
+
// src/commands/notify.ts
|
|
14553
|
+
import chalk6 from "chalk";
|
|
14554
|
+
|
|
14555
|
+
// src/notify/config.ts
|
|
14556
|
+
import { chmod as chmod3, readFile as readFile12, rm as rm4, writeFile as writeFile11 } from "fs/promises";
|
|
14557
|
+
import { join as join12 } from "path";
|
|
14558
|
+
import { z as z31 } from "zod";
|
|
14559
|
+
var TelegramSchema = z31.object({
|
|
14560
|
+
botToken: z31.string().min(1),
|
|
14561
|
+
chatId: z31.string().min(1)
|
|
14562
|
+
});
|
|
14563
|
+
var TwilioSchema = z31.object({
|
|
14564
|
+
accountSid: z31.string().regex(/^AC[0-9a-fA-F]{32}$/, "Twilio account SIDs start with AC followed by 32 hex characters."),
|
|
14565
|
+
authToken: z31.string().min(1),
|
|
14566
|
+
/** The Twilio number Ultrac texts and calls from, E.164. */
|
|
14567
|
+
from: z31.string().regex(/^\+[1-9]\d{6,14}$/, "Use E.164 format, e.g. +14155550100."),
|
|
14568
|
+
/** The user's phone, E.164. */
|
|
14569
|
+
to: z31.string().regex(/^\+[1-9]\d{6,14}$/, "Use E.164 format, e.g. +919812345678."),
|
|
14570
|
+
/** Escalate an unanswered question to a voice call. */
|
|
14571
|
+
call: z31.boolean().default(true)
|
|
14572
|
+
});
|
|
14573
|
+
var NotifyConfigSchema = z31.object({
|
|
14574
|
+
version: z31.literal(1).default(1),
|
|
14575
|
+
telegram: TelegramSchema.optional(),
|
|
14576
|
+
twilio: TwilioSchema.optional(),
|
|
14577
|
+
/** How long a question waits for a text reply before Ultrac phones. */
|
|
14578
|
+
callAfterSeconds: z31.number().int().min(0).max(3600).default(180),
|
|
14579
|
+
/** How long a question waits in total before the agent proceeds on its own judgement. */
|
|
14580
|
+
answerTimeoutSeconds: z31.number().int().min(30).max(86400).default(1800)
|
|
14581
|
+
});
|
|
14582
|
+
function notifyConfigPath() {
|
|
14583
|
+
return join12(ultracHomeDir(), "notify.json");
|
|
14584
|
+
}
|
|
14585
|
+
async function readStoredNotifyConfig() {
|
|
14586
|
+
let text;
|
|
14587
|
+
try {
|
|
14588
|
+
text = await readFile12(notifyConfigPath(), "utf8");
|
|
14589
|
+
} catch (error) {
|
|
14590
|
+
if (error.code === "ENOENT") return null;
|
|
14591
|
+
throw error;
|
|
14592
|
+
}
|
|
14593
|
+
return NotifyConfigSchema.parse(JSON.parse(text));
|
|
14594
|
+
}
|
|
14595
|
+
async function writeNotifyConfig(config) {
|
|
14596
|
+
await ensureUltracHome();
|
|
14597
|
+
const path = notifyConfigPath();
|
|
14598
|
+
const parsed = NotifyConfigSchema.parse(config);
|
|
14599
|
+
await writeFile11(path, `${JSON.stringify(parsed, null, 2)}
|
|
14600
|
+
`, { mode: 384 });
|
|
14601
|
+
await chmod3(path, 384).catch(() => void 0);
|
|
14602
|
+
return path;
|
|
14603
|
+
}
|
|
14604
|
+
async function removeNotifyConfig() {
|
|
14605
|
+
await rm4(notifyConfigPath(), { force: true });
|
|
14606
|
+
}
|
|
14607
|
+
function env(name) {
|
|
14608
|
+
return process.env[name]?.trim() || void 0;
|
|
14609
|
+
}
|
|
14610
|
+
async function loadNotifyConfig() {
|
|
14611
|
+
const stored = await readStoredNotifyConfig();
|
|
14612
|
+
const base = stored ?? NotifyConfigSchema.parse({});
|
|
14613
|
+
const telegramToken = env("ULTRAC_TELEGRAM_BOT_TOKEN");
|
|
14614
|
+
const telegramChat = env("ULTRAC_TELEGRAM_CHAT_ID");
|
|
14615
|
+
const telegram = telegramToken && telegramChat ? { botToken: telegramToken, chatId: telegramChat } : base.telegram;
|
|
14616
|
+
const sid = env("ULTRAC_TWILIO_ACCOUNT_SID");
|
|
14617
|
+
const token = env("ULTRAC_TWILIO_AUTH_TOKEN");
|
|
14618
|
+
const from = env("ULTRAC_TWILIO_FROM");
|
|
14619
|
+
const to = env("ULTRAC_NOTIFY_PHONE");
|
|
14620
|
+
const twilio = sid && token && from && to ? { accountSid: sid, authToken: token, from, to, call: base.twilio?.call ?? true } : base.twilio;
|
|
14621
|
+
const merged = NotifyConfigSchema.parse({ ...base, telegram, twilio });
|
|
14622
|
+
return merged.telegram || merged.twilio ? merged : null;
|
|
14623
|
+
}
|
|
14624
|
+
function redact(secret) {
|
|
14625
|
+
return secret.length <= 4 ? "****" : `****${secret.slice(-4)}`;
|
|
14626
|
+
}
|
|
14627
|
+
|
|
14628
|
+
// src/notify/channel.ts
|
|
14629
|
+
function sleep(ms, signal) {
|
|
14630
|
+
if (signal?.aborted) return Promise.resolve(false);
|
|
14631
|
+
return new Promise((resolve15) => {
|
|
14632
|
+
const timer = setTimeout(() => {
|
|
14633
|
+
signal?.removeEventListener("abort", onAbort);
|
|
14634
|
+
resolve15(true);
|
|
14635
|
+
}, ms);
|
|
14636
|
+
const onAbort = () => {
|
|
14637
|
+
clearTimeout(timer);
|
|
14638
|
+
resolve15(false);
|
|
14639
|
+
};
|
|
14640
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
14641
|
+
});
|
|
14642
|
+
}
|
|
14643
|
+
function withTimeout2(signal, ms) {
|
|
14644
|
+
const timeout = AbortSignal.timeout(ms);
|
|
14645
|
+
return signal ? AbortSignal.any([signal, timeout]) : timeout;
|
|
14646
|
+
}
|
|
14647
|
+
var NotifyChannelError = class extends Error {
|
|
14648
|
+
constructor(channel, message) {
|
|
14649
|
+
super(`${channel}: ${message}`);
|
|
14650
|
+
this.name = "NotifyChannelError";
|
|
14651
|
+
}
|
|
14652
|
+
};
|
|
14653
|
+
|
|
14654
|
+
// src/notify/telegram.ts
|
|
14655
|
+
var API = "https://api.telegram.org";
|
|
14656
|
+
var LONG_POLL_SECONDS = 25;
|
|
14657
|
+
var RETRY_DELAY_MS = 5e3;
|
|
14658
|
+
var MAX_MESSAGE_CHARS = 4e3;
|
|
14659
|
+
async function callTelegram(fetchFn, botToken, method, body, signal) {
|
|
14660
|
+
const response = await fetchFn(`${API}/bot${botToken}/${method}`, {
|
|
14661
|
+
method: "POST",
|
|
14662
|
+
headers: { "content-type": "application/json" },
|
|
14663
|
+
body: JSON.stringify(body),
|
|
14664
|
+
signal
|
|
14665
|
+
});
|
|
14666
|
+
const json = await response.json().catch(() => ({ ok: false }));
|
|
14667
|
+
if (!response.ok || !json.ok || json.result === void 0) {
|
|
14668
|
+
throw new NotifyChannelError("telegram", json.description ?? `HTTP ${response.status}`);
|
|
14669
|
+
}
|
|
14670
|
+
return json.result;
|
|
14671
|
+
}
|
|
14672
|
+
var TelegramChannel = class {
|
|
14673
|
+
constructor(config, fetchFn = fetch) {
|
|
14674
|
+
this.config = config;
|
|
14675
|
+
this.fetchFn = fetchFn;
|
|
14676
|
+
}
|
|
14677
|
+
config;
|
|
14678
|
+
fetchFn;
|
|
14679
|
+
name = "telegram";
|
|
14680
|
+
offset;
|
|
14681
|
+
async send(text, signal) {
|
|
14682
|
+
await callTelegram(
|
|
14683
|
+
this.fetchFn,
|
|
14684
|
+
this.config.botToken,
|
|
14685
|
+
"sendMessage",
|
|
14686
|
+
{ chat_id: this.config.chatId, text: text.slice(0, MAX_MESSAGE_CHARS) },
|
|
14687
|
+
withTimeout2(signal, 15e3)
|
|
14688
|
+
);
|
|
14689
|
+
}
|
|
14690
|
+
async waitForReply(since, signal) {
|
|
14691
|
+
const sinceSeconds = Math.floor(since.getTime() / 1e3);
|
|
14692
|
+
while (!signal.aborted) {
|
|
14693
|
+
let updates;
|
|
14694
|
+
try {
|
|
14695
|
+
updates = await callTelegram(
|
|
14696
|
+
this.fetchFn,
|
|
14697
|
+
this.config.botToken,
|
|
14698
|
+
"getUpdates",
|
|
14699
|
+
{
|
|
14700
|
+
timeout: LONG_POLL_SECONDS,
|
|
14701
|
+
allowed_updates: ["message"],
|
|
14702
|
+
...this.offset !== void 0 ? { offset: this.offset } : {}
|
|
14703
|
+
},
|
|
14704
|
+
withTimeout2(signal, (LONG_POLL_SECONDS + 10) * 1e3)
|
|
14705
|
+
);
|
|
14706
|
+
} catch {
|
|
14707
|
+
if (signal.aborted) return null;
|
|
14708
|
+
await sleep(RETRY_DELAY_MS, signal);
|
|
14709
|
+
continue;
|
|
14710
|
+
}
|
|
14711
|
+
for (const update of updates) {
|
|
14712
|
+
this.offset = update.update_id + 1;
|
|
14713
|
+
const message = update.message;
|
|
14714
|
+
if (!message?.text) continue;
|
|
14715
|
+
if (String(message.chat.id) !== this.config.chatId) continue;
|
|
14716
|
+
if (message.date < sinceSeconds) continue;
|
|
14717
|
+
return message.text;
|
|
14718
|
+
}
|
|
14719
|
+
}
|
|
14720
|
+
return null;
|
|
14721
|
+
}
|
|
14722
|
+
};
|
|
14723
|
+
async function discoverTelegramChat(botToken, signal, fetchFn = fetch) {
|
|
14724
|
+
const startedSeconds = Math.floor(Date.now() / 1e3) - 60;
|
|
14725
|
+
let offset;
|
|
14726
|
+
while (!signal.aborted) {
|
|
14727
|
+
let updates;
|
|
14728
|
+
try {
|
|
14729
|
+
updates = await callTelegram(
|
|
14730
|
+
fetchFn,
|
|
14731
|
+
botToken,
|
|
14732
|
+
"getUpdates",
|
|
14733
|
+
{ timeout: LONG_POLL_SECONDS, allowed_updates: ["message"], ...offset !== void 0 ? { offset } : {} },
|
|
14734
|
+
withTimeout2(signal, (LONG_POLL_SECONDS + 10) * 1e3)
|
|
14735
|
+
);
|
|
14736
|
+
} catch (error) {
|
|
14737
|
+
if (signal.aborted) return null;
|
|
14738
|
+
throw error;
|
|
14739
|
+
}
|
|
14740
|
+
for (const update of updates) {
|
|
14741
|
+
offset = update.update_id + 1;
|
|
14742
|
+
const message = update.message;
|
|
14743
|
+
if (!message || message.date < startedSeconds) continue;
|
|
14744
|
+
const chat = message.chat;
|
|
14745
|
+
return { chatId: String(chat.id), name: chat.title ?? chat.username ?? chat.first_name ?? String(chat.id) };
|
|
14746
|
+
}
|
|
14747
|
+
}
|
|
14748
|
+
return null;
|
|
14749
|
+
}
|
|
14750
|
+
|
|
14751
|
+
// src/notify/twilio.ts
|
|
14752
|
+
var API2 = "https://api.twilio.com/2010-04-01";
|
|
14753
|
+
var POLL_INTERVAL_MS = 1e4;
|
|
14754
|
+
var MAX_SMS_CHARS = 1500;
|
|
14755
|
+
var MAX_SPOKEN_CHARS = 1e3;
|
|
14756
|
+
function escapeXml(text) {
|
|
14757
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
14758
|
+
}
|
|
14759
|
+
var TwilioChannel = class {
|
|
14760
|
+
constructor(config, fetchFn = fetch) {
|
|
14761
|
+
this.config = config;
|
|
14762
|
+
this.fetchFn = fetchFn;
|
|
14763
|
+
}
|
|
14764
|
+
config;
|
|
14765
|
+
fetchFn;
|
|
14766
|
+
name = "sms";
|
|
14767
|
+
seen = /* @__PURE__ */ new Set();
|
|
14768
|
+
get authHeader() {
|
|
14769
|
+
return `Basic ${Buffer.from(`${this.config.accountSid}:${this.config.authToken}`).toString("base64")}`;
|
|
14770
|
+
}
|
|
14771
|
+
async request(method, path, params, signal) {
|
|
14772
|
+
const search = new URLSearchParams(params);
|
|
14773
|
+
const url = `${API2}/Accounts/${this.config.accountSid}/${path}${method === "GET" ? `?${search}` : ""}`;
|
|
14774
|
+
const response = await this.fetchFn(url, {
|
|
14775
|
+
method,
|
|
14776
|
+
headers: {
|
|
14777
|
+
authorization: this.authHeader,
|
|
14778
|
+
...method === "POST" ? { "content-type": "application/x-www-form-urlencoded" } : {}
|
|
14779
|
+
},
|
|
14780
|
+
...method === "POST" ? { body: search.toString() } : {},
|
|
14781
|
+
signal: withTimeout2(signal, 15e3)
|
|
14782
|
+
});
|
|
14783
|
+
const json = await response.json().catch(() => ({}));
|
|
14784
|
+
if (!response.ok) {
|
|
14785
|
+
throw new NotifyChannelError("twilio", json.message ?? `HTTP ${response.status}`);
|
|
14786
|
+
}
|
|
14787
|
+
return json;
|
|
14788
|
+
}
|
|
14789
|
+
async send(text, signal) {
|
|
14790
|
+
await this.request(
|
|
14791
|
+
"POST",
|
|
14792
|
+
"Messages.json",
|
|
14793
|
+
{ To: this.config.to, From: this.config.from, Body: text.slice(0, MAX_SMS_CHARS) },
|
|
14794
|
+
signal
|
|
14795
|
+
);
|
|
14796
|
+
}
|
|
14797
|
+
async call(text, signal) {
|
|
14798
|
+
if (!this.config.call) return;
|
|
14799
|
+
const spoken = escapeXml(text.slice(0, MAX_SPOKEN_CHARS));
|
|
14800
|
+
const twiml = `<Response><Say>${spoken}</Say><Pause length="2"/><Say>Again. ${spoken}</Say></Response>`;
|
|
14801
|
+
await this.request("POST", "Calls.json", { To: this.config.to, From: this.config.from, Twiml: twiml }, signal);
|
|
14802
|
+
}
|
|
14803
|
+
async waitForReply(since, signal) {
|
|
14804
|
+
const day = since.toISOString().slice(0, 10);
|
|
14805
|
+
while (!signal.aborted) {
|
|
14806
|
+
try {
|
|
14807
|
+
const page = await this.request(
|
|
14808
|
+
"GET",
|
|
14809
|
+
"Messages.json",
|
|
14810
|
+
{ To: this.config.from, From: this.config.to, "DateSent>": day, PageSize: "50" },
|
|
14811
|
+
signal
|
|
14812
|
+
);
|
|
14813
|
+
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());
|
|
14814
|
+
const first = replies[0];
|
|
14815
|
+
if (first) {
|
|
14816
|
+
this.seen.add(first.message.sid);
|
|
14817
|
+
return first.message.body;
|
|
14818
|
+
}
|
|
14819
|
+
} catch {
|
|
14820
|
+
if (signal.aborted) return null;
|
|
14821
|
+
}
|
|
14822
|
+
await sleep(POLL_INTERVAL_MS, signal);
|
|
14823
|
+
}
|
|
14824
|
+
return null;
|
|
14825
|
+
}
|
|
14826
|
+
};
|
|
14827
|
+
|
|
14828
|
+
// src/notify/notifier.ts
|
|
14829
|
+
var CLOCK_SKEW_MS = 5e3;
|
|
14830
|
+
var Notifier = class {
|
|
14831
|
+
constructor(channels, timing, heading = "Ultrac", now = () => /* @__PURE__ */ new Date()) {
|
|
14832
|
+
this.channels = channels;
|
|
14833
|
+
this.timing = timing;
|
|
14834
|
+
this.heading = heading;
|
|
14835
|
+
this.now = now;
|
|
14836
|
+
if (channels.length === 0) throw new Error("A notifier needs at least one channel.");
|
|
14837
|
+
}
|
|
14838
|
+
channels;
|
|
14839
|
+
timing;
|
|
14840
|
+
heading;
|
|
14841
|
+
now;
|
|
14842
|
+
describe() {
|
|
14843
|
+
return this.channels.map((channel) => channel.name).join(" + ");
|
|
14844
|
+
}
|
|
14845
|
+
/**
|
|
14846
|
+
* Fire-and-report: send `text` everywhere and return the channels that
|
|
14847
|
+
* failed. Throws only when every channel failed.
|
|
14848
|
+
*/
|
|
14849
|
+
async send(text, signal) {
|
|
14850
|
+
const results = await Promise.allSettled(this.channels.map((channel) => channel.send(`${this.heading}: ${text}`, signal)));
|
|
14851
|
+
const failures = results.flatMap(
|
|
14852
|
+
(result, index) => result.status === "rejected" ? [`${this.channels[index].name}: ${String(result.reason?.message ?? result.reason)}`] : []
|
|
14853
|
+
);
|
|
14854
|
+
if (failures.length === this.channels.length) {
|
|
14855
|
+
throw new Error(`Could not reach the user on any channel (${failures.join("; ")}).`);
|
|
14856
|
+
}
|
|
14857
|
+
return failures;
|
|
14858
|
+
}
|
|
14859
|
+
async ask(question, options, signal) {
|
|
14860
|
+
const since = new Date(this.now().getTime() - CLOCK_SKEW_MS);
|
|
14861
|
+
const optionLines = options?.length ? `
|
|
14862
|
+
|
|
14863
|
+
${options.map((option, index) => `${index + 1}. ${option}`).join("\n")}
|
|
14864
|
+
|
|
14865
|
+
Reply with a number or your own answer.` : "\n\nReply to this message with your answer.";
|
|
14866
|
+
await this.send(`needs your input.
|
|
14867
|
+
|
|
14868
|
+
${question}${optionLines}`, signal);
|
|
14869
|
+
if (signal?.aborted) return null;
|
|
14870
|
+
const wait = new AbortController();
|
|
14871
|
+
const stop = () => wait.abort();
|
|
14872
|
+
signal?.addEventListener("abort", stop, { once: true });
|
|
14873
|
+
try {
|
|
14874
|
+
const replies = this.channels.map(
|
|
14875
|
+
(channel) => channel.waitForReply(since, wait.signal).catch(() => null).then((reply2) => {
|
|
14876
|
+
if (reply2 === null) return new Promise(() => void 0);
|
|
14877
|
+
return reply2;
|
|
14878
|
+
})
|
|
14879
|
+
);
|
|
14880
|
+
void (async () => {
|
|
14881
|
+
if (!await sleep(this.timing.callAfterMs, wait.signal)) return;
|
|
14882
|
+
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.`;
|
|
14883
|
+
await Promise.allSettled(this.channels.map((channel) => channel.call?.(spoken, wait.signal)));
|
|
14884
|
+
})();
|
|
14885
|
+
const timeout = sleep(this.timing.timeoutMs, wait.signal).then(() => null);
|
|
14886
|
+
const cancelled = new Promise((resolve15) => wait.signal.addEventListener("abort", () => resolve15(null), { once: true }));
|
|
14887
|
+
const reply = await Promise.race([...replies, timeout, cancelled]);
|
|
14888
|
+
return reply === null ? null : pickOption(reply, options);
|
|
14889
|
+
} finally {
|
|
14890
|
+
signal?.removeEventListener("abort", stop);
|
|
14891
|
+
wait.abort();
|
|
14892
|
+
}
|
|
14893
|
+
}
|
|
14894
|
+
};
|
|
14895
|
+
function pickOption(reply, options) {
|
|
14896
|
+
const answer = reply.trim();
|
|
14897
|
+
const picked = Number.parseInt(answer, 10);
|
|
14898
|
+
if (options?.length && String(picked) === answer && picked >= 1 && picked <= options.length) {
|
|
14899
|
+
return options[picked - 1];
|
|
14900
|
+
}
|
|
14901
|
+
return answer;
|
|
14902
|
+
}
|
|
14903
|
+
function createNotifier(config, heading, fetchFn = fetch) {
|
|
14904
|
+
const channels = [];
|
|
14905
|
+
if (config.telegram) channels.push(new TelegramChannel(config.telegram, fetchFn));
|
|
14906
|
+
if (config.twilio) channels.push(new TwilioChannel(config.twilio, fetchFn));
|
|
14907
|
+
return new Notifier(
|
|
14908
|
+
channels,
|
|
14909
|
+
{ callAfterMs: config.callAfterSeconds * 1e3, timeoutMs: config.answerTimeoutSeconds * 1e3 },
|
|
14910
|
+
heading
|
|
14911
|
+
);
|
|
14912
|
+
}
|
|
14913
|
+
|
|
14914
|
+
// src/commands/notify.ts
|
|
14915
|
+
var DISCOVERY_TIMEOUT_MS = 12e4;
|
|
14916
|
+
function parseSeconds(value, flag) {
|
|
14917
|
+
if (value === void 0) return void 0;
|
|
14918
|
+
const parsed = Number.parseInt(value, 10);
|
|
14919
|
+
if (!Number.isInteger(parsed) || parsed < 0 || String(parsed) !== value.trim()) {
|
|
14920
|
+
throw new Error(`${flag} must be a whole number of seconds.`);
|
|
14921
|
+
}
|
|
14922
|
+
return parsed;
|
|
14923
|
+
}
|
|
14924
|
+
function describeNotifyConfig(config) {
|
|
14925
|
+
const lines = [];
|
|
14926
|
+
if (config.telegram) {
|
|
14927
|
+
lines.push(`telegram chat ${config.telegram.chatId}, bot token ${redact(config.telegram.botToken)}`);
|
|
14928
|
+
}
|
|
14929
|
+
if (config.twilio) {
|
|
14930
|
+
lines.push(
|
|
14931
|
+
`sms ${config.twilio.from} -> ${config.twilio.to}, voice calls ${config.twilio.call ? "on" : "off"}, account ${redact(config.twilio.accountSid)}`
|
|
14932
|
+
);
|
|
14933
|
+
}
|
|
14934
|
+
lines.push(`timing calls after ${config.callAfterSeconds}s unanswered, gives up after ${config.answerTimeoutSeconds}s`);
|
|
14935
|
+
return lines;
|
|
14936
|
+
}
|
|
14937
|
+
function registerNotifyCommand(program2) {
|
|
14938
|
+
const notify = program2.command("notify").description("Let Ultrac reach your phone: questions from autonomous runs, and results when they finish");
|
|
14939
|
+
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) => {
|
|
14940
|
+
const stored = await readStoredNotifyConfig() ?? NotifyConfigSchema.parse({});
|
|
14941
|
+
const next = { ...stored };
|
|
14942
|
+
if (options.telegramToken) {
|
|
14943
|
+
let chatId = options.telegramChat;
|
|
14944
|
+
if (!chatId) {
|
|
14945
|
+
console.log(chalk6.cyan("Open Telegram and send any message to your bot now. Waiting up to 2 minutes\u2026"));
|
|
14946
|
+
const found = await discoverTelegramChat(options.telegramToken, AbortSignal.timeout(DISCOVERY_TIMEOUT_MS));
|
|
14947
|
+
if (!found) throw new Error("No message reached the bot. Send it a message and run setup again, or pass --telegram-chat.");
|
|
14948
|
+
console.log(chalk6.green(`Found chat: ${found.name} (${found.chatId})`));
|
|
14949
|
+
chatId = found.chatId;
|
|
14950
|
+
}
|
|
14951
|
+
next.telegram = { botToken: options.telegramToken, chatId };
|
|
14952
|
+
} else if (options.telegramChat) {
|
|
14953
|
+
throw new Error("--telegram-chat needs --telegram-token.");
|
|
14954
|
+
}
|
|
14955
|
+
const twilioFlags = [options.twilioSid, options.twilioToken, options.twilioFrom, options.phone];
|
|
14956
|
+
if (twilioFlags.some(Boolean)) {
|
|
14957
|
+
if (!twilioFlags.every(Boolean)) {
|
|
14958
|
+
throw new Error("Twilio needs all four of --twilio-sid, --twilio-token, --twilio-from and --phone.");
|
|
14959
|
+
}
|
|
14960
|
+
next.twilio = {
|
|
14961
|
+
accountSid: options.twilioSid,
|
|
14962
|
+
authToken: options.twilioToken,
|
|
14963
|
+
from: options.twilioFrom,
|
|
14964
|
+
to: options.phone,
|
|
14965
|
+
call: options.call
|
|
14966
|
+
};
|
|
14967
|
+
} else if (next.twilio && options.call === false) {
|
|
14968
|
+
next.twilio = { ...next.twilio, call: false };
|
|
14969
|
+
}
|
|
14970
|
+
const callAfter = parseSeconds(options.callAfter, "--call-after");
|
|
14971
|
+
const timeout = parseSeconds(options.timeout, "--timeout");
|
|
14972
|
+
if (callAfter !== void 0) next.callAfterSeconds = callAfter;
|
|
14973
|
+
if (timeout !== void 0) next.answerTimeoutSeconds = timeout;
|
|
14974
|
+
if (!next.telegram && !next.twilio) {
|
|
14975
|
+
throw new Error(
|
|
14976
|
+
"Configure at least one channel: --telegram-token, or --twilio-sid/--twilio-token/--twilio-from/--phone."
|
|
14977
|
+
);
|
|
14978
|
+
}
|
|
14979
|
+
const path = await writeNotifyConfig(next);
|
|
14980
|
+
console.log(chalk6.green(`Saved ${path}`));
|
|
14981
|
+
for (const line of describeNotifyConfig(NotifyConfigSchema.parse(next))) console.log(` ${line}`);
|
|
14982
|
+
if (options.test) {
|
|
14983
|
+
const failures = await createNotifier(NotifyConfigSchema.parse(next)).send(
|
|
14984
|
+
"is connected. This is where your agent will reach you."
|
|
14985
|
+
);
|
|
14986
|
+
for (const failure of failures) console.error(chalk6.yellow(` ${failure}`));
|
|
14987
|
+
console.log(chalk6.green("Test message sent."));
|
|
14988
|
+
}
|
|
14989
|
+
});
|
|
14990
|
+
notify.command("status").description("Show the configured phone channels").action(async () => {
|
|
14991
|
+
const config = await loadNotifyConfig();
|
|
14992
|
+
if (!config) {
|
|
14993
|
+
console.log("No phone channel configured. Run `ultrac notify setup`.");
|
|
14994
|
+
return;
|
|
14995
|
+
}
|
|
14996
|
+
console.log(chalk6.bold(`Phone channels (${notifyConfigPath()} + environment)`));
|
|
14997
|
+
for (const line of describeNotifyConfig(config)) console.log(` ${line}`);
|
|
14998
|
+
});
|
|
14999
|
+
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) => {
|
|
15000
|
+
const config = await loadNotifyConfig();
|
|
15001
|
+
if (!config) throw new Error("No phone channel configured. Run `ultrac notify setup`.");
|
|
15002
|
+
const notifier = createNotifier(config);
|
|
15003
|
+
if (!options.ask) {
|
|
15004
|
+
const failures = await notifier.send("test message. Your phone channel works.");
|
|
15005
|
+
for (const failure of failures) console.error(chalk6.yellow(failure));
|
|
15006
|
+
console.log(chalk6.green(`Sent via ${notifier.describe()}.`));
|
|
15007
|
+
return;
|
|
15008
|
+
}
|
|
15009
|
+
const controller = new AbortController();
|
|
15010
|
+
process.once("SIGINT", () => controller.abort());
|
|
15011
|
+
console.log(chalk6.cyan(`Asked via ${notifier.describe()}. Reply from your phone (Ctrl-C to stop)\u2026`));
|
|
15012
|
+
const answer = await notifier.ask("This is a test. Reply with anything.", ["Works", "Something is off"], controller.signal);
|
|
15013
|
+
console.log(answer === null ? chalk6.yellow("No reply before the timeout.") : chalk6.green(`Reply: ${answer}`));
|
|
15014
|
+
});
|
|
15015
|
+
notify.command("remove").description("Delete ~/.ultrac/notify.json (environment variables are not affected)").action(async () => {
|
|
15016
|
+
await removeNotifyConfig();
|
|
15017
|
+
console.log(`Removed ${notifyConfigPath()}`);
|
|
15018
|
+
});
|
|
15019
|
+
}
|
|
15020
|
+
|
|
14529
15021
|
// src/commands/repl.ts
|
|
14530
15022
|
import { createInterface as createInterface3 } from "readline";
|
|
14531
15023
|
import { stdin as stdin5, stdout as stdout5 } from "process";
|
|
14532
|
-
import
|
|
15024
|
+
import chalk11 from "chalk";
|
|
14533
15025
|
|
|
14534
15026
|
// src/config/budget.ts
|
|
14535
15027
|
function applyBudgetFlags(flags, config) {
|
|
@@ -14559,7 +15051,7 @@ function parseIntFlag(raw, flag) {
|
|
|
14559
15051
|
|
|
14560
15052
|
// src/commands/trust.ts
|
|
14561
15053
|
import { resolve as resolve11 } from "path";
|
|
14562
|
-
import
|
|
15054
|
+
import chalk7 from "chalk";
|
|
14563
15055
|
|
|
14564
15056
|
// src/ui/confirm.ts
|
|
14565
15057
|
import { stdin as stdin4, stdout as stdout2 } from "process";
|
|
@@ -14630,41 +15122,41 @@ function redactCommand(command) {
|
|
|
14630
15122
|
}
|
|
14631
15123
|
function printTrustReview(workspace, raw, escalations, status) {
|
|
14632
15124
|
if (escalations) {
|
|
14633
|
-
console.log(
|
|
15125
|
+
console.log(chalk7.yellow(`
|
|
14634
15126
|
This workspace's .ultrac/config.json asks for ${describeEscalations(escalations)}.`));
|
|
14635
15127
|
} else if (status === "legacy") {
|
|
14636
|
-
console.log(
|
|
15128
|
+
console.log(chalk7.yellow("\n This workspace has an older approval that was not bound to its config."));
|
|
14637
15129
|
} else if (status === "changed") {
|
|
14638
|
-
console.log(
|
|
15130
|
+
console.log(chalk7.yellow("\n This workspace's security-sensitive config changed since it was approved."));
|
|
14639
15131
|
} else {
|
|
14640
|
-
console.log(
|
|
15132
|
+
console.log(chalk7.dim("\n This workspace's config asks for no additional permissions or MCP servers."));
|
|
14641
15133
|
}
|
|
14642
15134
|
const preview = renderMcpTrustPreview(raw);
|
|
14643
|
-
for (const line of preview.lines) console.log(
|
|
14644
|
-
console.log(
|
|
14645
|
-
console.log(
|
|
14646
|
-
console.log(
|
|
14647
|
-
console.log(
|
|
15135
|
+
for (const line of preview.lines) console.log(chalk7.dim(` ${line}`));
|
|
15136
|
+
console.log(chalk7.dim(" Literal MCP env values are hidden; only variable names are shown."));
|
|
15137
|
+
console.log(chalk7.dim(" MCP processes are not sandboxed and run with your OS user's privileges."));
|
|
15138
|
+
console.log(chalk7.dim(" Argument redaction is best-effort; review .ultrac/config.json locally for secrets before approval."));
|
|
15139
|
+
console.log(chalk7.dim(" Approval is local to this workspace and bound to its current permissions and MCP launch config."));
|
|
14648
15140
|
return { hasInvalidServers: preview.hasInvalidServers };
|
|
14649
15141
|
}
|
|
14650
15142
|
async function offerWorkspaceTrust(workspace, escalations, snapshot, status) {
|
|
14651
15143
|
const raw = await readRawConfig(workspace);
|
|
14652
15144
|
if (workspaceTrustSnapshot(raw).securityFingerprint !== snapshot.securityFingerprint) {
|
|
14653
|
-
console.log(
|
|
15145
|
+
console.log(chalk7.yellow("\n Workspace security config changed during review. Nothing was approved; review it again."));
|
|
14654
15146
|
return false;
|
|
14655
15147
|
}
|
|
14656
15148
|
const { hasInvalidServers } = printTrustReview(workspace, raw, escalations, status);
|
|
14657
15149
|
if (hasInvalidServers) {
|
|
14658
|
-
console.log(
|
|
15150
|
+
console.log(chalk7.yellow(" Invalid MCP entries cannot be approved. Fix the config and review it again.\n"));
|
|
14659
15151
|
return false;
|
|
14660
15152
|
}
|
|
14661
15153
|
const granted = await confirmYesNo(` Trust ${escapeTerminalControls(workspace)}? [y/N]`, false);
|
|
14662
15154
|
if (granted) {
|
|
14663
15155
|
const recorded = await trustWorkspace(workspace, snapshot);
|
|
14664
|
-
console.log(
|
|
15156
|
+
console.log(chalk7.green(` Trusted ${escapeTerminalControls(recorded)}.`));
|
|
14665
15157
|
return true;
|
|
14666
15158
|
}
|
|
14667
|
-
console.log(
|
|
15159
|
+
console.log(chalk7.dim(` Not trusted. Those settings were not applied; run ${chalk7.cyan("ultrac trust")} here to allow them.
|
|
14668
15160
|
`));
|
|
14669
15161
|
return false;
|
|
14670
15162
|
}
|
|
@@ -14673,7 +15165,7 @@ function registerTrustCommand(program2) {
|
|
|
14673
15165
|
if (options.list) {
|
|
14674
15166
|
const trusted = await listTrustedWorkspaces();
|
|
14675
15167
|
if (trusted.length === 0) {
|
|
14676
|
-
console.log(
|
|
15168
|
+
console.log(chalk7.dim("No trusted workspaces."));
|
|
14677
15169
|
return;
|
|
14678
15170
|
}
|
|
14679
15171
|
for (const path of trusted) console.log(escapeTerminalControls(path));
|
|
@@ -14683,7 +15175,7 @@ function registerTrustCommand(program2) {
|
|
|
14683
15175
|
if (options.revoke) {
|
|
14684
15176
|
const had = await untrustWorkspace(workspace);
|
|
14685
15177
|
const safeWorkspace = escapeTerminalControls(workspace);
|
|
14686
|
-
console.log(had ? `No longer trusting ${safeWorkspace}.` :
|
|
15178
|
+
console.log(had ? `No longer trusting ${safeWorkspace}.` : chalk7.dim(`${safeWorkspace} was not trusted.`));
|
|
14687
15179
|
return;
|
|
14688
15180
|
}
|
|
14689
15181
|
const raw = await readRawConfig(workspace);
|
|
@@ -14692,10 +15184,10 @@ function registerTrustCommand(program2) {
|
|
|
14692
15184
|
if (status === "trusted") {
|
|
14693
15185
|
const preview = printTrustReview(workspace, raw, snapshot.escalations, status);
|
|
14694
15186
|
if (preview.hasInvalidServers) {
|
|
14695
|
-
console.log(
|
|
15187
|
+
console.log(chalk7.yellow("This stored approval no longer has valid MCP launch config. Revoke it before changing the config."));
|
|
14696
15188
|
return;
|
|
14697
15189
|
}
|
|
14698
|
-
console.log(
|
|
15190
|
+
console.log(chalk7.green(`Already trusted for ${escapeTerminalControls(workspace)}.`));
|
|
14699
15191
|
return;
|
|
14700
15192
|
}
|
|
14701
15193
|
await offerWorkspaceTrust(workspace, snapshot.escalations, snapshot, status);
|
|
@@ -14915,7 +15407,11 @@ var InteractiveSession = class {
|
|
|
14915
15407
|
}
|
|
14916
15408
|
async start() {
|
|
14917
15409
|
if (this.started) return;
|
|
14918
|
-
const context = await gatherWorkspaceContext(
|
|
15410
|
+
const context = await gatherWorkspaceContext(
|
|
15411
|
+
this.registry,
|
|
15412
|
+
this.options.workspace,
|
|
15413
|
+
this.advertisedToolNames
|
|
15414
|
+
);
|
|
14919
15415
|
const modeLabel = getAgentModePreset(this.activeMode).label;
|
|
14920
15416
|
const contextMessage = {
|
|
14921
15417
|
role: "user",
|
|
@@ -14929,6 +15425,28 @@ Acknowledge that you have the context and wait for the user's first request.`
|
|
|
14929
15425
|
this.fileProvenance.set(contextMessage, unknownContextProvenance());
|
|
14930
15426
|
this.started = true;
|
|
14931
15427
|
}
|
|
15428
|
+
async checkpoint(handlers) {
|
|
15429
|
+
if (!handlers.onCheckpoint) return;
|
|
15430
|
+
try {
|
|
15431
|
+
await handlers.onCheckpoint(this.toSession());
|
|
15432
|
+
} catch {
|
|
15433
|
+
}
|
|
15434
|
+
}
|
|
15435
|
+
appendQueuedMessages(handlers) {
|
|
15436
|
+
const queued = handlers.drainQueuedMessages?.() ?? [];
|
|
15437
|
+
let appended = 0;
|
|
15438
|
+
for (const raw of queued) {
|
|
15439
|
+
const content = raw.trim();
|
|
15440
|
+
if (!content) continue;
|
|
15441
|
+
const message = { role: "user", content };
|
|
15442
|
+
this.messages.push(message);
|
|
15443
|
+
this.pinnedMessages.push(message);
|
|
15444
|
+
this.userConstraints.push({ kind: "message", content });
|
|
15445
|
+
handlers.onQueuedMessage?.(content);
|
|
15446
|
+
appended += 1;
|
|
15447
|
+
}
|
|
15448
|
+
return appended;
|
|
15449
|
+
}
|
|
14932
15450
|
async ask(input, handlers = {}) {
|
|
14933
15451
|
if (!this.started) await this.start();
|
|
14934
15452
|
if (!this.firstTask) this.firstTask = input;
|
|
@@ -14947,6 +15465,7 @@ Acknowledge that you have the context and wait for the user's first request.`
|
|
|
14947
15465
|
handlers.onInterrupted?.();
|
|
14948
15466
|
break;
|
|
14949
15467
|
}
|
|
15468
|
+
this.appendQueuedMessages(handlers);
|
|
14950
15469
|
const system = buildSystemPrompt(
|
|
14951
15470
|
this.advertisedTools(),
|
|
14952
15471
|
getEffectiveModePermissions(this.activeMode, this.permissions),
|
|
@@ -14991,6 +15510,8 @@ Acknowledge that you have the context and wait for the user's first request.`
|
|
|
14991
15510
|
finalMessage = parsed.message;
|
|
14992
15511
|
if (parsed.message) handlers.onAssistantMessage?.(parsed.message);
|
|
14993
15512
|
this.messages.push({ role: "assistant", content: JSON.stringify(parsed) });
|
|
15513
|
+
this.steps.push(step);
|
|
15514
|
+
await this.checkpoint(handlers);
|
|
14994
15515
|
for (const call of parsed.toolCalls) {
|
|
14995
15516
|
if (handlers.signal?.aborted) break;
|
|
14996
15517
|
const authorization = await this.authorize(call, handlers);
|
|
@@ -15008,6 +15529,7 @@ Acknowledge that you have the context and wait for the user's first request.`
|
|
|
15008
15529
|
if (authorization.permission) handlers.onToolDenied?.(call, authorization.permission);
|
|
15009
15530
|
step.toolResults.push({ call, result: result2 });
|
|
15010
15531
|
this.pushToolResult(call, result2);
|
|
15532
|
+
await this.checkpoint(handlers);
|
|
15011
15533
|
continue;
|
|
15012
15534
|
}
|
|
15013
15535
|
handlers.onToolStart?.(call);
|
|
@@ -15022,14 +15544,15 @@ Acknowledge that you have the context and wait for the user's first request.`
|
|
|
15022
15544
|
handlers.onToolResult?.(call, result);
|
|
15023
15545
|
step.toolResults.push({ call, result });
|
|
15024
15546
|
this.pushToolResult(call, result);
|
|
15547
|
+
await this.checkpoint(handlers);
|
|
15025
15548
|
}
|
|
15026
|
-
this.steps.push(step);
|
|
15027
15549
|
if (handlers.signal?.aborted) {
|
|
15028
15550
|
stopReason = "aborted";
|
|
15029
15551
|
handlers.onInterrupted?.();
|
|
15030
15552
|
break;
|
|
15031
15553
|
}
|
|
15032
|
-
|
|
15554
|
+
const queuedMessages = this.appendQueuedMessages(handlers);
|
|
15555
|
+
if (parsed.done && parsed.toolCalls.length === 0 && queuedMessages === 0) {
|
|
15033
15556
|
stopReason = "completed";
|
|
15034
15557
|
break;
|
|
15035
15558
|
}
|
|
@@ -15098,6 +15621,10 @@ Acknowledge that you have the context and wait for the user's first request.`
|
|
|
15098
15621
|
summarisedTurns: plan.summarisedTurns
|
|
15099
15622
|
});
|
|
15100
15623
|
} catch (error) {
|
|
15624
|
+
if (error instanceof PinnedContextBudgetError) {
|
|
15625
|
+
trace.end({ outcome: "budget-error", error });
|
|
15626
|
+
throw error;
|
|
15627
|
+
}
|
|
15101
15628
|
if (!(error instanceof ContextBudgetError)) {
|
|
15102
15629
|
trace.end({ outcome: "error", error });
|
|
15103
15630
|
throw error;
|
|
@@ -15383,7 +15910,11 @@ var NativeSession = class {
|
|
|
15383
15910
|
}
|
|
15384
15911
|
async start() {
|
|
15385
15912
|
if (this.started) return;
|
|
15386
|
-
const context = await gatherWorkspaceContext(
|
|
15913
|
+
const context = await gatherWorkspaceContext(
|
|
15914
|
+
this.registry,
|
|
15915
|
+
this.options.workspace,
|
|
15916
|
+
this.advertisedToolNames
|
|
15917
|
+
);
|
|
15387
15918
|
this.messages.push({ role: "system", content: this.systemPrompt() });
|
|
15388
15919
|
const contextMessage = {
|
|
15389
15920
|
role: "user",
|
|
@@ -15395,6 +15926,28 @@ ${context}`
|
|
|
15395
15926
|
this.fileProvenance.set(contextMessage, unknownContextProvenance());
|
|
15396
15927
|
this.started = true;
|
|
15397
15928
|
}
|
|
15929
|
+
async checkpoint(handlers) {
|
|
15930
|
+
if (!handlers.onCheckpoint) return;
|
|
15931
|
+
try {
|
|
15932
|
+
await handlers.onCheckpoint(this.toSession());
|
|
15933
|
+
} catch {
|
|
15934
|
+
}
|
|
15935
|
+
}
|
|
15936
|
+
appendQueuedMessages(handlers) {
|
|
15937
|
+
const queued = handlers.drainQueuedMessages?.() ?? [];
|
|
15938
|
+
let appended = 0;
|
|
15939
|
+
for (const raw of queued) {
|
|
15940
|
+
const content = raw.trim();
|
|
15941
|
+
if (!content) continue;
|
|
15942
|
+
const message = { role: "user", content };
|
|
15943
|
+
this.messages.push(message);
|
|
15944
|
+
this.pinnedMessages.push(message);
|
|
15945
|
+
this.userConstraints.push({ kind: "message", content });
|
|
15946
|
+
handlers.onQueuedMessage?.(content);
|
|
15947
|
+
appended += 1;
|
|
15948
|
+
}
|
|
15949
|
+
return appended;
|
|
15950
|
+
}
|
|
15398
15951
|
async ask(input, handlers = {}) {
|
|
15399
15952
|
if (!this.started) await this.start();
|
|
15400
15953
|
if (!this.firstTask) this.firstTask = input;
|
|
@@ -15412,6 +15965,7 @@ ${context}`
|
|
|
15412
15965
|
handlers.onInterrupted?.();
|
|
15413
15966
|
break;
|
|
15414
15967
|
}
|
|
15968
|
+
this.appendQueuedMessages(handlers);
|
|
15415
15969
|
this.refreshSystemPrompt();
|
|
15416
15970
|
const tools = toToolSchemas(this.advertisedTools());
|
|
15417
15971
|
const turn = this.steps.length + 1;
|
|
@@ -15461,10 +16015,19 @@ ${context}`
|
|
|
15461
16015
|
toolCalls: response.toolCalls
|
|
15462
16016
|
});
|
|
15463
16017
|
}
|
|
16018
|
+
this.steps.push(step);
|
|
16019
|
+
await this.checkpoint(handlers);
|
|
15464
16020
|
if (!response.toolCalls.length) {
|
|
15465
|
-
|
|
15466
|
-
|
|
15467
|
-
|
|
16021
|
+
if (handlers.signal?.aborted) {
|
|
16022
|
+
stopReason = "aborted";
|
|
16023
|
+
handlers.onInterrupted?.();
|
|
16024
|
+
break;
|
|
16025
|
+
}
|
|
16026
|
+
if (this.appendQueuedMessages(handlers) === 0) {
|
|
16027
|
+
stopReason = "completed";
|
|
16028
|
+
break;
|
|
16029
|
+
}
|
|
16030
|
+
continue;
|
|
15468
16031
|
}
|
|
15469
16032
|
for (const call of response.toolCalls) {
|
|
15470
16033
|
if (handlers.signal?.aborted) break;
|
|
@@ -15484,6 +16047,7 @@ ${context}`
|
|
|
15484
16047
|
if (authorization.permission) handlers.onToolDenied?.(displayCall, authorization.permission);
|
|
15485
16048
|
step.toolResults.push({ call: displayCall, result: result2 });
|
|
15486
16049
|
this.pushToolResult(call, result2);
|
|
16050
|
+
await this.checkpoint(handlers);
|
|
15487
16051
|
continue;
|
|
15488
16052
|
}
|
|
15489
16053
|
handlers.onToolStart?.(displayCall);
|
|
@@ -15501,13 +16065,14 @@ ${context}`
|
|
|
15501
16065
|
handlers.onToolResult?.(displayCall, result);
|
|
15502
16066
|
step.toolResults.push({ call: displayCall, result });
|
|
15503
16067
|
this.pushToolResult(call, result);
|
|
16068
|
+
await this.checkpoint(handlers);
|
|
15504
16069
|
}
|
|
15505
|
-
this.steps.push(step);
|
|
15506
16070
|
if (handlers.signal?.aborted) {
|
|
15507
16071
|
stopReason = "aborted";
|
|
15508
16072
|
handlers.onInterrupted?.();
|
|
15509
16073
|
break;
|
|
15510
16074
|
}
|
|
16075
|
+
this.appendQueuedMessages(handlers);
|
|
15511
16076
|
}
|
|
15512
16077
|
} catch (error) {
|
|
15513
16078
|
stopReason = "error";
|
|
@@ -15573,6 +16138,10 @@ ${context}`
|
|
|
15573
16138
|
summarisedTurns: plan.summarisedTurns
|
|
15574
16139
|
});
|
|
15575
16140
|
} catch (error) {
|
|
16141
|
+
if (error instanceof PinnedContextBudgetError) {
|
|
16142
|
+
trace.end({ outcome: "budget-error", error });
|
|
16143
|
+
throw error;
|
|
16144
|
+
}
|
|
15576
16145
|
if (!(error instanceof ContextBudgetError)) {
|
|
15577
16146
|
trace.end({ outcome: "error", error });
|
|
15578
16147
|
throw error;
|
|
@@ -15672,6 +16241,7 @@ ${context}`
|
|
|
15672
16241
|
const mode = getAgentModePreset(this.activeMode);
|
|
15673
16242
|
const advertised = new Set(this.advertisedToolNames);
|
|
15674
16243
|
const guidance = [
|
|
16244
|
+
advertisedExploreGuidance(advertised),
|
|
15675
16245
|
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,
|
|
15676
16246
|
advertised.has("run_tests") && advertised.has("run_package_script") ? "Prefer run_tests over run_package_script when you want classified test failures." : void 0,
|
|
15677
16247
|
advertised.has("task") ? "Delegate search-heavy exploration to task so its transcript stays out of this conversation." : void 0,
|
|
@@ -15766,7 +16336,7 @@ function createSessionCheckpointWriter(workspace, onFailure) {
|
|
|
15766
16336
|
}
|
|
15767
16337
|
|
|
15768
16338
|
// src/ui/output.ts
|
|
15769
|
-
import
|
|
16339
|
+
import chalk8 from "chalk";
|
|
15770
16340
|
var stopReasonDetail = {
|
|
15771
16341
|
"max-steps": "partial answer - hit the step cap before the model said it was done",
|
|
15772
16342
|
aborted: "partial answer - the run was interrupted before the model said it was done",
|
|
@@ -15781,12 +16351,12 @@ function stopReasonLine(stopReason) {
|
|
|
15781
16351
|
function printSession(session, sessionPath2) {
|
|
15782
16352
|
for (const step of session.steps) {
|
|
15783
16353
|
if (step.parsed?.message) {
|
|
15784
|
-
console.log(
|
|
16354
|
+
console.log(chalk8.gray(`step ${step.index + 1}:`), step.parsed.message);
|
|
15785
16355
|
}
|
|
15786
16356
|
for (const toolResult of step.toolResults) {
|
|
15787
|
-
const status = toolResult.result.ok ?
|
|
15788
|
-
console.log(
|
|
15789
|
-
console.log(
|
|
16357
|
+
const status = toolResult.result.ok ? chalk8.green("ok") : chalk8.red("failed");
|
|
16358
|
+
console.log(chalk8.cyan(toolResult.call.tool), status);
|
|
16359
|
+
console.log(chalk8.gray(indent2(toolResult.result.content)));
|
|
15790
16360
|
}
|
|
15791
16361
|
}
|
|
15792
16362
|
if (session.finalMessage) {
|
|
@@ -15796,23 +16366,23 @@ function printSession(session, sessionPath2) {
|
|
|
15796
16366
|
console.log();
|
|
15797
16367
|
const stopped = stopReasonLine(session.stopReason);
|
|
15798
16368
|
if (stopped) {
|
|
15799
|
-
console.log(
|
|
15800
|
-
console.log(
|
|
16369
|
+
console.log(chalk8.yellow(stopped));
|
|
16370
|
+
console.log(chalk8.gray(`Resume with: ultrac run --continue ${session.id}`));
|
|
15801
16371
|
}
|
|
15802
|
-
console.log(
|
|
16372
|
+
console.log(chalk8.gray(`Session saved: ${sessionPath2}`));
|
|
15803
16373
|
}
|
|
15804
16374
|
function printTools(tools) {
|
|
15805
16375
|
for (const tool of tools) {
|
|
15806
|
-
const permissionText = tool.permissions.length ?
|
|
15807
|
-
const optionalText = tool.optionalPermissions.length ?
|
|
15808
|
-
console.log(`${
|
|
15809
|
-
console.log(
|
|
16376
|
+
const permissionText = tool.permissions.length ? chalk8.yellow(` [${tool.permissions.join(", ")}]`) : "";
|
|
16377
|
+
const optionalText = tool.optionalPermissions.length ? chalk8.gray(` [optional: ${tool.optionalPermissions.join(", ")}]`) : "";
|
|
16378
|
+
console.log(`${chalk8.cyan(tool.name)}${permissionText}${optionalText}`);
|
|
16379
|
+
console.log(chalk8.gray(indent2(tool.description)));
|
|
15810
16380
|
}
|
|
15811
16381
|
}
|
|
15812
16382
|
function printToolResult(result) {
|
|
15813
|
-
const status = result.ok ?
|
|
15814
|
-
console.log(`${
|
|
15815
|
-
console.log(
|
|
16383
|
+
const status = result.ok ? chalk8.green("ok") : chalk8.red("failed");
|
|
16384
|
+
console.log(`${chalk8.cyan(result.tool)} ${status}`);
|
|
16385
|
+
console.log(chalk8.gray(indent2(result.content)));
|
|
15816
16386
|
}
|
|
15817
16387
|
function indent2(value) {
|
|
15818
16388
|
return value.split("\n").map((line) => ` ${line}`).join("\n");
|
|
@@ -16018,7 +16588,7 @@ function formatSkillCommandHelp(index) {
|
|
|
16018
16588
|
|
|
16019
16589
|
// src/ui/progress.ts
|
|
16020
16590
|
import { stdout as stdout3 } from "process";
|
|
16021
|
-
import
|
|
16591
|
+
import chalk9 from "chalk";
|
|
16022
16592
|
var PENDING_GLYPH = "\u25CB";
|
|
16023
16593
|
var DONE_GLYPH = "\u25CF";
|
|
16024
16594
|
var REPAINT_MS = 100;
|
|
@@ -16045,12 +16615,12 @@ function formatElapsed(ms) {
|
|
|
16045
16615
|
}
|
|
16046
16616
|
function formatProgressLine(snapshot) {
|
|
16047
16617
|
const finished = snapshot.done >= snapshot.total;
|
|
16048
|
-
const glyph = finished ?
|
|
16618
|
+
const glyph = finished ? chalk9.green(DONE_GLYPH) : chalk9.cyan(PENDING_GLYPH);
|
|
16049
16619
|
const noun = snapshot.total === 1 ? "agent" : "agents";
|
|
16050
16620
|
const stats = [`${snapshot.done}/${snapshot.total} ${noun} done`, formatElapsed(snapshot.elapsedMs)];
|
|
16051
16621
|
if (snapshot.tokens !== void 0) stats.push(`\u2193 ${formatTokens(snapshot.tokens)} tokens`);
|
|
16052
|
-
const rendered = stats.map((stat11) =>
|
|
16053
|
-
return ` ${glyph} ${
|
|
16622
|
+
const rendered = stats.map((stat11) => chalk9.dim(stat11)).join(chalk9.dim(" \xB7 "));
|
|
16623
|
+
return ` ${glyph} ${chalk9.cyan(snapshot.label)} ${snapshot.description} ${rendered}`;
|
|
16054
16624
|
}
|
|
16055
16625
|
var ProgressLine = class {
|
|
16056
16626
|
constructor(label, description, total) {
|
|
@@ -16212,17 +16782,160 @@ var Spinner = class {
|
|
|
16212
16782
|
}
|
|
16213
16783
|
};
|
|
16214
16784
|
|
|
16785
|
+
// src/ui/tool-activity.ts
|
|
16786
|
+
var VERBS = {
|
|
16787
|
+
read_file: "Read",
|
|
16788
|
+
read_many_files: "Read",
|
|
16789
|
+
write_file: "Edit",
|
|
16790
|
+
replace_in_file: "Edit",
|
|
16791
|
+
multi_edit: "Edit",
|
|
16792
|
+
apply_patch: "Apply patch",
|
|
16793
|
+
search_files: "Search",
|
|
16794
|
+
list_files: "List files",
|
|
16795
|
+
inspect_project: "Inspect project",
|
|
16796
|
+
repo_map: "Map repository",
|
|
16797
|
+
context_bundle: "Gather context",
|
|
16798
|
+
run_shell: "Run",
|
|
16799
|
+
run_package_script: "Run package script",
|
|
16800
|
+
run_tests: "Run tests",
|
|
16801
|
+
diagnostics: "Check diagnostics",
|
|
16802
|
+
task: "Delegate",
|
|
16803
|
+
fetch_url: "Fetch",
|
|
16804
|
+
git_status: "Check Git status",
|
|
16805
|
+
git_diff: "Read Git diff",
|
|
16806
|
+
git_log: "Read Git log",
|
|
16807
|
+
git_show: "Read Git object",
|
|
16808
|
+
git_blame: "Read Git blame",
|
|
16809
|
+
git_commit: "Commit changes",
|
|
16810
|
+
git_push: "Push changes"
|
|
16811
|
+
};
|
|
16812
|
+
function toolActivityLabel(call) {
|
|
16813
|
+
const verb = VERBS[call.tool] ?? humanize(call.tool);
|
|
16814
|
+
const target = toolTarget(call);
|
|
16815
|
+
return target ? `${verb} ${target}` : verb;
|
|
16816
|
+
}
|
|
16817
|
+
function toolFailurePreview(result) {
|
|
16818
|
+
if (result.ok) return void 0;
|
|
16819
|
+
const detail = result.content.split("\n").map((line) => line.trim()).filter(Boolean).slice(0, 2).join(" \xB7 ");
|
|
16820
|
+
return detail ? truncate2(detail, 240) : `${humanize(result.tool)} failed`;
|
|
16821
|
+
}
|
|
16822
|
+
function toolTarget(call) {
|
|
16823
|
+
const input = call.input;
|
|
16824
|
+
const path = stringValue(input.path);
|
|
16825
|
+
if (path) return compact2(path);
|
|
16826
|
+
const paths = Array.isArray(input.paths) ? input.paths.filter((value) => typeof value === "string") : [];
|
|
16827
|
+
if (paths.length === 1) return compact2(paths[0]);
|
|
16828
|
+
if (paths.length > 1) return `${paths.length} files`;
|
|
16829
|
+
if (call.tool === "run_shell") return compact2(stringValue(input.command), 72);
|
|
16830
|
+
if (call.tool === "run_package_script") return compact2(stringValue(input.script), 50);
|
|
16831
|
+
if (call.tool === "search_files") return quote(stringValue(input.query) || stringValue(input.pattern));
|
|
16832
|
+
if (call.tool === "fetch_url") return compact2(stringValue(input.url), 72);
|
|
16833
|
+
if (call.tool === "task") return compact2(stringValue(input.description) || stringValue(input.task), 64);
|
|
16834
|
+
if (call.tool === "git_show") return compact2(stringValue(input.ref), 40);
|
|
16835
|
+
if (call.tool === "git_commit") return quote(stringValue(input.message), 56);
|
|
16836
|
+
return "";
|
|
16837
|
+
}
|
|
16838
|
+
function stringValue(value) {
|
|
16839
|
+
return typeof value === "string" ? value.trim() : "";
|
|
16840
|
+
}
|
|
16841
|
+
function compact2(value, max = 64) {
|
|
16842
|
+
if (!value) return "";
|
|
16843
|
+
return truncate2(value.replace(/\s+/g, " "), max);
|
|
16844
|
+
}
|
|
16845
|
+
function quote(value, max = 48) {
|
|
16846
|
+
return value ? `\u201C${compact2(value, max)}\u201D` : "";
|
|
16847
|
+
}
|
|
16848
|
+
function truncate2(value, max) {
|
|
16849
|
+
return value.length > max ? `${value.slice(0, Math.max(1, max - 1))}\u2026` : value;
|
|
16850
|
+
}
|
|
16851
|
+
function humanize(value) {
|
|
16852
|
+
const words = value.replace(/[_-]+/g, " ").trim();
|
|
16853
|
+
return words ? `${words[0].toUpperCase()}${words.slice(1)}` : "Tool";
|
|
16854
|
+
}
|
|
16855
|
+
|
|
16856
|
+
// src/ui/welcome.ts
|
|
16857
|
+
import chalk10, { Chalk } from "chalk";
|
|
16858
|
+
var LOGO = [
|
|
16859
|
+
"\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",
|
|
16860
|
+
"\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",
|
|
16861
|
+
"\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551 ",
|
|
16862
|
+
"\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2551 ",
|
|
16863
|
+
"\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2557",
|
|
16864
|
+
" \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D"
|
|
16865
|
+
];
|
|
16866
|
+
function renderWelcome(info, options = {}) {
|
|
16867
|
+
const columns = normalizeColumns(options.columns);
|
|
16868
|
+
const paint = new Chalk({ level: options.color === false ? 0 : chalk10.level });
|
|
16869
|
+
const cardWidth = Math.min(88, Math.max(42, columns));
|
|
16870
|
+
const innerWidth = cardWidth - 4;
|
|
16871
|
+
const wide = columns >= 64;
|
|
16872
|
+
const lines = [];
|
|
16873
|
+
if (wide) {
|
|
16874
|
+
lines.push("");
|
|
16875
|
+
for (let index = 0; index < LOGO.length; index += 1) {
|
|
16876
|
+
const color = index < 2 ? paint.hex("#38BDF8") : index < 4 ? paint.hex("#22D3EE") : paint.hex("#A78BFA");
|
|
16877
|
+
lines.push(` ${color(LOGO[index])}`);
|
|
16878
|
+
}
|
|
16879
|
+
} else {
|
|
16880
|
+
lines.push("", ` ${paint.bold.hex("#22D3EE")("ULTRAC")} ${paint.gray(`v${info.version}`)}`);
|
|
16881
|
+
}
|
|
16882
|
+
const topLabel = wide ? ` ultrac v${info.version} ` : " session ";
|
|
16883
|
+
const topFill = Math.max(0, cardWidth - topLabel.length - 2);
|
|
16884
|
+
lines.push(paint.cyan(`\u256D${topLabel}${"\u2500".repeat(topFill)}\u256E`));
|
|
16885
|
+
lines.push(cardLine(`${info.model} via ${info.providerName}`, innerWidth, paint.bold));
|
|
16886
|
+
lines.push(cardLine(`${info.mode} \xB7 ${info.protocol}`, innerWidth, paint.white));
|
|
16887
|
+
lines.push(cardLine(shortenMiddle(info.workspace, innerWidth), innerWidth, paint.gray));
|
|
16888
|
+
lines.push(paint.cyan(`\u251C${"\u2500".repeat(cardWidth - 2)}\u2524`));
|
|
16889
|
+
lines.push(cardLine(info.budget, innerWidth, paint.gray));
|
|
16890
|
+
lines.push(cardLine(`logs ${shortenMiddle(info.diagnosticPath, Math.max(8, innerWidth - 5))}`, innerWidth, paint.gray));
|
|
16891
|
+
lines.push(paint.cyan(`\u2570${"\u2500".repeat(cardWidth - 2)}\u256F`));
|
|
16892
|
+
lines.push(
|
|
16893
|
+
wide ? `${paint.gray(" /help commands")} ${paint.dim("\xB7")} ${paint.gray("/exit quit")} ${paint.dim("\xB7")} ${paint.gray("Ctrl-C interrupt")}` : paint.gray(" /help \xB7 /exit \xB7 Ctrl-C")
|
|
16894
|
+
);
|
|
16895
|
+
return lines.join("\n");
|
|
16896
|
+
}
|
|
16897
|
+
function renderComposer(columns = 80, color = true) {
|
|
16898
|
+
const paint = new Chalk({ level: color ? chalk10.level : 0 });
|
|
16899
|
+
const width = Math.min(88, Math.max(28, normalizeColumns(columns)));
|
|
16900
|
+
const label = " message ";
|
|
16901
|
+
const fill = Math.max(1, width - label.length - 2);
|
|
16902
|
+
return {
|
|
16903
|
+
frame: `
|
|
16904
|
+
${paint.cyan(`\u256D\u2500${label}${"\u2500".repeat(fill)}`)}
|
|
16905
|
+
${paint.cyan("\u2502")}`,
|
|
16906
|
+
prompt: paint.bold.hex("#22D3EE")("\u2570\u2500\u276F ")
|
|
16907
|
+
};
|
|
16908
|
+
}
|
|
16909
|
+
function cardLine(value, width, style) {
|
|
16910
|
+
const clipped = truncate3(value, width);
|
|
16911
|
+
return `\u2502 ${style(clipped)}${" ".repeat(Math.max(0, width - clipped.length))} \u2502`;
|
|
16912
|
+
}
|
|
16913
|
+
function normalizeColumns(columns) {
|
|
16914
|
+
return Number.isFinite(columns) ? Math.max(28, Math.floor(columns)) : 80;
|
|
16915
|
+
}
|
|
16916
|
+
function truncate3(value, width) {
|
|
16917
|
+
if (value.length <= width) return value;
|
|
16918
|
+
if (width <= 1) return "\u2026";
|
|
16919
|
+
return `${value.slice(0, width - 1)}\u2026`;
|
|
16920
|
+
}
|
|
16921
|
+
function shortenMiddle(value, width) {
|
|
16922
|
+
if (value.length <= width) return value;
|
|
16923
|
+
if (width < 9) return truncate3(value, width);
|
|
16924
|
+
const side = Math.floor((width - 1) / 2);
|
|
16925
|
+
return `${value.slice(0, side)}\u2026${value.slice(value.length - (width - side - 1))}`;
|
|
16926
|
+
}
|
|
16927
|
+
|
|
16215
16928
|
// src/update/check.ts
|
|
16216
16929
|
import { spawn as spawn4 } from "child_process";
|
|
16217
16930
|
import { randomBytes as randomBytes4 } from "crypto";
|
|
16218
|
-
import { readFile as
|
|
16219
|
-
import { join as
|
|
16931
|
+
import { readFile as readFile13, rename as rename2, unlink as unlink3, writeFile as writeFile12 } from "fs/promises";
|
|
16932
|
+
import { join as join13 } from "path";
|
|
16220
16933
|
var DEFAULT_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
16221
16934
|
var QUERY_TIMEOUT_MS = 5e3;
|
|
16222
16935
|
var INSTALL_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
16223
16936
|
async function checkForUpdate(options) {
|
|
16224
16937
|
const now = options.now ?? Date.now();
|
|
16225
|
-
const cachePath = options.cachePath ??
|
|
16938
|
+
const cachePath = options.cachePath ?? join13(await ensureUltracHome(), "update-check.json");
|
|
16226
16939
|
const maxAgeMs = options.maxAgeMs ?? DEFAULT_MAX_AGE_MS;
|
|
16227
16940
|
let latestVersion;
|
|
16228
16941
|
if (!options.force) {
|
|
@@ -16290,7 +17003,7 @@ function parseVersion(version) {
|
|
|
16290
17003
|
}
|
|
16291
17004
|
async function readCache(path) {
|
|
16292
17005
|
try {
|
|
16293
|
-
const raw = await
|
|
17006
|
+
const raw = await readFile13(path, "utf8");
|
|
16294
17007
|
if (raw.length > 1024) return void 0;
|
|
16295
17008
|
const parsed = JSON.parse(raw);
|
|
16296
17009
|
if (typeof parsed !== "object" || parsed === null || !("checkedAt" in parsed) || typeof parsed.checkedAt !== "number" || !("latestVersion" in parsed) || parsed.latestVersion !== null && typeof parsed.latestVersion !== "string") {
|
|
@@ -16304,7 +17017,7 @@ async function readCache(path) {
|
|
|
16304
17017
|
async function writeCache(path, cache) {
|
|
16305
17018
|
const temporary = `${path}.${randomBytes4(8).toString("hex")}.tmp`;
|
|
16306
17019
|
try {
|
|
16307
|
-
await
|
|
17020
|
+
await writeFile12(temporary, `${JSON.stringify(cache)}
|
|
16308
17021
|
`, { encoding: "utf8", mode: 384, flag: "wx" });
|
|
16309
17022
|
await rename2(temporary, path);
|
|
16310
17023
|
} catch {
|
|
@@ -16312,13 +17025,13 @@ async function writeCache(path, cache) {
|
|
|
16312
17025
|
}
|
|
16313
17026
|
}
|
|
16314
17027
|
async function runNpm(args, options) {
|
|
16315
|
-
const
|
|
16316
|
-
for (const key of Object.keys(
|
|
16317
|
-
if (key.startsWith("ULTRAC_")) delete
|
|
17028
|
+
const env2 = { ...process.env };
|
|
17029
|
+
for (const key of Object.keys(env2)) {
|
|
17030
|
+
if (key.startsWith("ULTRAC_")) delete env2[key];
|
|
16318
17031
|
}
|
|
16319
17032
|
return new Promise((resolve15) => {
|
|
16320
17033
|
const child = spawn4(process.platform === "win32" ? "npm.cmd" : "npm", args, {
|
|
16321
|
-
env,
|
|
17034
|
+
env: env2,
|
|
16322
17035
|
stdio: options.capture ? ["ignore", "pipe", "ignore"] : "inherit",
|
|
16323
17036
|
windowsHide: true
|
|
16324
17037
|
});
|
|
@@ -16438,14 +17151,14 @@ async function runRepl(options, initialTask = "") {
|
|
|
16438
17151
|
if (loaded.needsApproval && await offerWorkspaceTrust(workspace, loaded.withheld, loaded.trustSnapshot, loaded.trustStatus)) {
|
|
16439
17152
|
loaded = await loadConfigWithTrust(workspace);
|
|
16440
17153
|
if (loaded.needsApproval) {
|
|
16441
|
-
console.log(
|
|
17154
|
+
console.log(chalk11.yellow(`
|
|
16442
17155
|
${withheldNotice(workspace, loaded.withheld, loaded.trustStatus)}
|
|
16443
17156
|
`));
|
|
16444
17157
|
}
|
|
16445
17158
|
}
|
|
16446
17159
|
const config = loaded.config;
|
|
16447
17160
|
const toolSelection = applyBudgetFlags(options, config);
|
|
16448
|
-
setContextBudgetNotice((message) => console.log(
|
|
17161
|
+
setContextBudgetNotice((message) => console.log(chalk11.yellow(`
|
|
16449
17162
|
! ${message}`)));
|
|
16450
17163
|
const providerName = options.provider ?? config.provider;
|
|
16451
17164
|
if (!isProviderName(providerName)) {
|
|
@@ -16461,8 +17174,8 @@ async function runRepl(options, initialTask = "") {
|
|
|
16461
17174
|
}
|
|
16462
17175
|
const hasFallbackChain = Boolean(config.fallback?.chain?.length);
|
|
16463
17176
|
if (providerName === "ultrac" && !hasFallbackChain && !await resolveUltracCredentials()) {
|
|
16464
|
-
console.log(
|
|
16465
|
-
console.log(`Run ${
|
|
17177
|
+
console.log(chalk11.yellow("You are not signed in to Ultrac."));
|
|
17178
|
+
console.log(`Run ${chalk11.cyan("ultrac login")} first, or start with ${chalk11.cyan("--provider echo")}.`);
|
|
16466
17179
|
process.exitCode = EXIT_REPL_NOT_STARTED;
|
|
16467
17180
|
return;
|
|
16468
17181
|
}
|
|
@@ -16474,26 +17187,26 @@ async function runRepl(options, initialTask = "") {
|
|
|
16474
17187
|
model,
|
|
16475
17188
|
fallback: config.fallback,
|
|
16476
17189
|
onFailover: (event) => {
|
|
16477
|
-
console.log(
|
|
17190
|
+
console.log(chalk11.yellow(formatFailoverNotice(event)));
|
|
16478
17191
|
if (event.to) {
|
|
16479
|
-
console.log(
|
|
17192
|
+
console.log(chalk11.gray(` now answering: provider ${event.to.provider} \xB7 model ${event.to.model}`));
|
|
16480
17193
|
}
|
|
16481
17194
|
},
|
|
16482
17195
|
// A tool-calling turn served by an entry without native tool calling still
|
|
16483
17196
|
// answers, but over the JSON tool protocol. Say so rather than letting the
|
|
16484
17197
|
// banner's "native tools" claim go stale.
|
|
16485
|
-
onDowngrade: (event) => console.log(
|
|
17198
|
+
onDowngrade: (event) => console.log(chalk11.yellow(formatDowngradeNotice(event))),
|
|
16486
17199
|
onRetry: (event) => {
|
|
16487
17200
|
diagnosticEvent("provider.retry", event);
|
|
16488
17201
|
console.log(
|
|
16489
|
-
|
|
17202
|
+
chalk11.yellow(
|
|
16490
17203
|
` retrying ${event.target.provider} (${event.target.model}) in ${event.delayMs}ms after ${event.classification.reason}`
|
|
16491
17204
|
)
|
|
16492
17205
|
);
|
|
16493
17206
|
}
|
|
16494
17207
|
});
|
|
16495
17208
|
} catch (error) {
|
|
16496
|
-
console.log(
|
|
17209
|
+
console.log(chalk11.red(error instanceof Error ? error.message : String(error)));
|
|
16497
17210
|
process.exitCode = EXIT_REPL_NOT_STARTED;
|
|
16498
17211
|
return;
|
|
16499
17212
|
}
|
|
@@ -16525,7 +17238,7 @@ async function runRepl(options, initialTask = "") {
|
|
|
16525
17238
|
permissions: basePermissions
|
|
16526
17239
|
});
|
|
16527
17240
|
const writeCheckpoint = createSessionCheckpointWriter(workspace, (message) => {
|
|
16528
|
-
console.error(
|
|
17241
|
+
console.error(chalk11.yellow(message));
|
|
16529
17242
|
});
|
|
16530
17243
|
const persistSession = async (target = session) => {
|
|
16531
17244
|
const snapshot = target.toSession();
|
|
@@ -16543,7 +17256,7 @@ async function runRepl(options, initialTask = "") {
|
|
|
16543
17256
|
diagnosticPath2
|
|
16544
17257
|
);
|
|
16545
17258
|
if (mcpRuntime && mcpRuntime.statuses.length > 0) {
|
|
16546
|
-
console.log(
|
|
17259
|
+
console.log(chalk11.bold("\nMCP servers"));
|
|
16547
17260
|
console.log(renderMcpStatuses(mcpRuntime.statuses));
|
|
16548
17261
|
}
|
|
16549
17262
|
try {
|
|
@@ -16553,24 +17266,25 @@ async function runRepl(options, initialTask = "") {
|
|
|
16553
17266
|
}
|
|
16554
17267
|
const update = await updateCheck;
|
|
16555
17268
|
if (update?.updateAvailable && update.latestVersion) {
|
|
16556
|
-
console.log(
|
|
17269
|
+
console.log(chalk11.yellow(`
|
|
16557
17270
|
\u2191 Update available: ${update.currentVersion} \u2192 ${update.latestVersion}`));
|
|
16558
|
-
console.log(
|
|
17271
|
+
console.log(chalk11.gray(" Run `ultrac update` after this session."));
|
|
16559
17272
|
}
|
|
16560
17273
|
const rl = createInterface3({ input: stdin5, output: stdout5 });
|
|
16561
17274
|
const reader = new LineReader(rl);
|
|
16562
17275
|
const question = async (prompt, signal) => {
|
|
16563
17276
|
stdout5.write(prompt);
|
|
16564
|
-
return reader.
|
|
17277
|
+
return reader.nextFresh(signal);
|
|
16565
17278
|
};
|
|
17279
|
+
const drainQueuedMessages = () => reader.drainQueuedMessages();
|
|
16566
17280
|
setAskUserRuntime(async (text, options2, signal) => {
|
|
16567
|
-
console.log(
|
|
17281
|
+
console.log(chalk11.cyan(`
|
|
16568
17282
|
? ${text}`));
|
|
16569
17283
|
if (options2?.length) {
|
|
16570
|
-
options2.forEach((option, index) => console.log(
|
|
16571
|
-
console.log(
|
|
17284
|
+
options2.forEach((option, index) => console.log(chalk11.gray(` ${index + 1}. ${option}`)));
|
|
17285
|
+
console.log(chalk11.gray(" (type a number, or answer in your own words)"));
|
|
16572
17286
|
}
|
|
16573
|
-
const rawAnswer = await question(
|
|
17287
|
+
const rawAnswer = await question(chalk11.cyan(" \u203A "), signal);
|
|
16574
17288
|
if (signal?.aborted) return null;
|
|
16575
17289
|
const answer = (rawAnswer ?? "").trim();
|
|
16576
17290
|
const picked = Number.parseInt(answer, 10);
|
|
@@ -16582,11 +17296,10 @@ async function runRepl(options, initialTask = "") {
|
|
|
16582
17296
|
let running = true;
|
|
16583
17297
|
let turnAbort;
|
|
16584
17298
|
rl.on("SIGINT", () => {
|
|
16585
|
-
if (turnAbort
|
|
16586
|
-
|
|
16587
|
-
|
|
16588
|
-
|
|
16589
|
-
console.log("\nUse /exit to end the session.");
|
|
17299
|
+
if (handleReplInterrupt(turnAbort) === "abort-turn") return;
|
|
17300
|
+
running = false;
|
|
17301
|
+
console.log(chalk11.gray("\nExiting ultrac\u2026"));
|
|
17302
|
+
rl.close();
|
|
16590
17303
|
});
|
|
16591
17304
|
const terminate = () => {
|
|
16592
17305
|
running = false;
|
|
@@ -16604,12 +17317,18 @@ async function runRepl(options, initialTask = "") {
|
|
|
16604
17317
|
};
|
|
16605
17318
|
try {
|
|
16606
17319
|
if (initialTask) {
|
|
16607
|
-
console.log(
|
|
17320
|
+
console.log(chalk11.cyan(`
|
|
16608
17321
|
\u203A ${initialTask}`));
|
|
16609
|
-
await withInterrupt((signal) => runAndCheckpointTurn(session, initialTask, question, signal, persistSession));
|
|
17322
|
+
await withInterrupt((signal) => runAndCheckpointTurn(session, initialTask, question, signal, persistSession, drainQueuedMessages));
|
|
16610
17323
|
}
|
|
16611
17324
|
while (running) {
|
|
16612
|
-
stdout5.
|
|
17325
|
+
if (stdout5.isTTY) {
|
|
17326
|
+
const composer = renderComposer(stdout5.columns, true);
|
|
17327
|
+
console.log(composer.frame);
|
|
17328
|
+
stdout5.write(composer.prompt);
|
|
17329
|
+
} else {
|
|
17330
|
+
stdout5.write(chalk11.cyan("\n\u203A "));
|
|
17331
|
+
}
|
|
16613
17332
|
const line = await reader.next();
|
|
16614
17333
|
if (line === null) break;
|
|
16615
17334
|
const raw = line.trim();
|
|
@@ -16625,9 +17344,9 @@ async function runRepl(options, initialTask = "") {
|
|
|
16625
17344
|
permissions: session.currentPermissions
|
|
16626
17345
|
});
|
|
16627
17346
|
resetContextBudgetNotices();
|
|
16628
|
-
console.log(
|
|
17347
|
+
console.log(chalk11.gray("Started a fresh conversation."));
|
|
16629
17348
|
}, async (prompt) => {
|
|
16630
|
-
await runAndCheckpointTurn(session, prompt, question, signal, persistSession);
|
|
17349
|
+
await runAndCheckpointTurn(session, prompt, question, signal, persistSession, drainQueuedMessages);
|
|
16631
17350
|
}, signal, async (id) => {
|
|
16632
17351
|
const lineage = await loadSessionLineage(workspace, id);
|
|
16633
17352
|
const restored = createSession({ model: session.model, mode: session.mode, permissions: session.currentPermissions });
|
|
@@ -16656,7 +17375,7 @@ async function runRepl(options, initialTask = "") {
|
|
|
16656
17375
|
}
|
|
16657
17376
|
continue;
|
|
16658
17377
|
}
|
|
16659
|
-
await withInterrupt((signal) => runAndCheckpointTurn(session, raw, question, signal, persistSession));
|
|
17378
|
+
await withInterrupt((signal) => runAndCheckpointTurn(session, raw, question, signal, persistSession, drainQueuedMessages));
|
|
16660
17379
|
}
|
|
16661
17380
|
} finally {
|
|
16662
17381
|
process.off("SIGTERM", terminate);
|
|
@@ -16665,18 +17384,23 @@ async function runRepl(options, initialTask = "") {
|
|
|
16665
17384
|
await saveTranscript(session, writeCheckpoint);
|
|
16666
17385
|
mcpRuntime?.close();
|
|
16667
17386
|
}
|
|
16668
|
-
console.log(
|
|
17387
|
+
console.log(chalk11.gray("Goodbye."));
|
|
16669
17388
|
}
|
|
16670
|
-
async function runTurn(session, input, question, signal) {
|
|
17389
|
+
async function runTurn(session, input, question, signal, drainQueuedMessages, persist) {
|
|
16671
17390
|
const spinner = new Spinner();
|
|
16672
17391
|
const detachProgress = attachProgressUi();
|
|
16673
17392
|
let deltaChars = 0;
|
|
16674
17393
|
try {
|
|
16675
17394
|
await session.ask(input, {
|
|
16676
17395
|
...signal ? { signal } : {},
|
|
17396
|
+
...persist ? {
|
|
17397
|
+
onCheckpoint: async () => {
|
|
17398
|
+
await persist(session);
|
|
17399
|
+
}
|
|
17400
|
+
} : {},
|
|
16677
17401
|
onInterrupted: () => {
|
|
16678
17402
|
spinner.stop();
|
|
16679
|
-
console.log(
|
|
17403
|
+
console.log(chalk11.dim("\n Interrupted \u2014 current work stopped. Session preserved."));
|
|
16680
17404
|
},
|
|
16681
17405
|
onModelStart: () => {
|
|
16682
17406
|
deltaChars = 0;
|
|
@@ -16694,34 +17418,41 @@ ${message.trim()}`);
|
|
|
16694
17418
|
}
|
|
16695
17419
|
},
|
|
16696
17420
|
onToolStart: (call) => {
|
|
16697
|
-
console.log(
|
|
17421
|
+
console.log(chalk11.dim(` ${toolActivityLabel(call)}`));
|
|
16698
17422
|
},
|
|
16699
|
-
onToolResult: (
|
|
17423
|
+
onToolResult: (_call, result) => printToolResult2(result),
|
|
17424
|
+
...drainQueuedMessages ? {
|
|
17425
|
+
drainQueuedMessages,
|
|
17426
|
+
onQueuedMessage: () => {
|
|
17427
|
+
spinner.stop();
|
|
17428
|
+
console.log(chalk11.dim(" Queued message added to the current turn."));
|
|
17429
|
+
}
|
|
17430
|
+
} : {},
|
|
16700
17431
|
onToolDenied: (call, permission) => {
|
|
16701
|
-
console.log(
|
|
17432
|
+
console.log(chalk11.yellow(` \u2717 denied ${permission} for ${call.tool}`));
|
|
16702
17433
|
},
|
|
16703
17434
|
onProviderNotice: (notice) => {
|
|
16704
|
-
console.log(
|
|
17435
|
+
console.log(chalk11.yellow(` ! ${notice}`));
|
|
16705
17436
|
},
|
|
16706
17437
|
onStopped: (reason) => {
|
|
16707
17438
|
if (reason === "aborted") return;
|
|
16708
17439
|
const line = stopReasonLine(reason);
|
|
16709
17440
|
const hint = turnStopHint(reason, session.maxSteps);
|
|
16710
17441
|
if (!line || !hint) return;
|
|
16711
|
-
console.log(
|
|
17442
|
+
console.log(chalk11.yellow(`
|
|
16712
17443
|
\u26A0 ${line}`));
|
|
16713
|
-
console.log(
|
|
17444
|
+
console.log(chalk11.gray(` ${hint}`));
|
|
16714
17445
|
},
|
|
16715
17446
|
requestPermission: async (call, permission) => {
|
|
16716
17447
|
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 `;
|
|
16717
|
-
const rawAnswer = await question(
|
|
17448
|
+
const rawAnswer = await question(chalk11.yellow(prompt), signal);
|
|
16718
17449
|
if (signal?.aborted || rawAnswer === null) return "deny";
|
|
16719
17450
|
const answer = rawAnswer.trim().toLowerCase();
|
|
16720
17451
|
if (answer === "a" || answer === "always") return "always";
|
|
16721
17452
|
if (answer === "o" || answer === "once" || answer === "y" || answer === "yes") return "once";
|
|
16722
17453
|
if (answer === "e" || answer === "never") {
|
|
16723
17454
|
console.log(
|
|
16724
|
-
|
|
17455
|
+
chalk11.gray(
|
|
16725
17456
|
` ${permission} refused for the rest of the session \u2014 nothing will ask again. /allow ${permission} undoes it.`
|
|
16726
17457
|
)
|
|
16727
17458
|
);
|
|
@@ -16739,9 +17470,9 @@ ${message.trim()}`);
|
|
|
16739
17470
|
detachProgress();
|
|
16740
17471
|
}
|
|
16741
17472
|
}
|
|
16742
|
-
async function runAndCheckpointTurn(session, input, question, signal, persist) {
|
|
17473
|
+
async function runAndCheckpointTurn(session, input, question, signal, persist, drainQueuedMessages) {
|
|
16743
17474
|
try {
|
|
16744
|
-
return await runTurn(session, input, question, signal);
|
|
17475
|
+
return await runTurn(session, input, question, signal, drainQueuedMessages, persist);
|
|
16745
17476
|
} finally {
|
|
16746
17477
|
await persist(session);
|
|
16747
17478
|
}
|
|
@@ -16758,27 +17489,27 @@ function turnStopHint(reason, maxSteps) {
|
|
|
16758
17489
|
}
|
|
16759
17490
|
function printTurnError(error) {
|
|
16760
17491
|
if (error instanceof ProviderChainError) {
|
|
16761
|
-
console.log(
|
|
17492
|
+
console.log(chalk11.red("\n \u2717 Turn failed: no provider in the chain could answer."));
|
|
16762
17493
|
for (const failure of error.failures) {
|
|
16763
17494
|
const attempts = failure.attempts === 1 ? "1 attempt" : `${failure.attempts} attempts`;
|
|
16764
17495
|
console.log(
|
|
16765
|
-
|
|
17496
|
+
chalk11.red(` - ${failure.provider} (${failure.model}), ${attempts}: ${errorMessage2(failure.error)}`)
|
|
16766
17497
|
);
|
|
16767
17498
|
}
|
|
16768
17499
|
console.log(
|
|
16769
|
-
|
|
17500
|
+
chalk11.gray(" Fix the provider (ultrac login, or --provider echo) and try again; the session is still open.")
|
|
16770
17501
|
);
|
|
16771
17502
|
return;
|
|
16772
17503
|
}
|
|
16773
|
-
console.log(
|
|
17504
|
+
console.log(chalk11.red(`
|
|
16774
17505
|
\u2717 Turn failed: ${errorMessage2(error)}`));
|
|
16775
|
-
console.log(
|
|
17506
|
+
console.log(chalk11.gray(" The session is still open. Try again, or /exit to end it."));
|
|
16776
17507
|
}
|
|
16777
17508
|
async function saveTranscript(session, writeCheckpoint) {
|
|
16778
17509
|
const snapshot = session.toSession();
|
|
16779
17510
|
if (!hasSavedSessionContent(snapshot)) return;
|
|
16780
17511
|
const stored = await writeCheckpoint(snapshot);
|
|
16781
|
-
if (stored) console.log(
|
|
17512
|
+
if (stored) console.log(chalk11.gray(`
|
|
16782
17513
|
Session saved: ${stored}`));
|
|
16783
17514
|
}
|
|
16784
17515
|
function hasSavedSessionContent(session) {
|
|
@@ -16820,7 +17551,7 @@ async function handleSlashCommand(raw, session, providerName, workspace, onClear
|
|
|
16820
17551
|
if (extra) console.log(`
|
|
16821
17552
|
${extra}`);
|
|
16822
17553
|
for (const conflict of index.conflicts) {
|
|
16823
|
-
console.log(
|
|
17554
|
+
console.log(chalk11.yellow(` ! ${conflict.message}`));
|
|
16824
17555
|
}
|
|
16825
17556
|
return "ok";
|
|
16826
17557
|
}
|
|
@@ -16834,7 +17565,7 @@ ${extra}`);
|
|
|
16834
17565
|
case "model":
|
|
16835
17566
|
if (argument) {
|
|
16836
17567
|
session.setModel(argument);
|
|
16837
|
-
console.log(
|
|
17568
|
+
console.log(chalk11.gray(`Model set to ${argument}.`));
|
|
16838
17569
|
} else {
|
|
16839
17570
|
console.log(`Model: ${session.model}`);
|
|
16840
17571
|
}
|
|
@@ -16842,10 +17573,10 @@ ${extra}`);
|
|
|
16842
17573
|
case "mode":
|
|
16843
17574
|
if (argument) {
|
|
16844
17575
|
if (!isAgentModeName(argument)) {
|
|
16845
|
-
console.log(
|
|
17576
|
+
console.log(chalk11.red(`Unknown mode. Expected one of: ${agentModeNames.join(", ")}.`));
|
|
16846
17577
|
} else {
|
|
16847
17578
|
session.setMode(argument);
|
|
16848
|
-
console.log(
|
|
17579
|
+
console.log(chalk11.gray(`Mode set to ${argument}.`));
|
|
16849
17580
|
}
|
|
16850
17581
|
} else {
|
|
16851
17582
|
console.log(`Mode: ${session.mode}`);
|
|
@@ -16894,7 +17625,7 @@ ${extra}`);
|
|
|
16894
17625
|
console.log(
|
|
16895
17626
|
`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.`
|
|
16896
17627
|
);
|
|
16897
|
-
if (info.error) console.log(
|
|
17628
|
+
if (info.error) console.log(chalk11.yellow(`Context error: ${info.error}`));
|
|
16898
17629
|
return "ok";
|
|
16899
17630
|
}
|
|
16900
17631
|
case "compact":
|
|
@@ -16902,7 +17633,7 @@ ${extra}`);
|
|
|
16902
17633
|
await session.compactContext(argument, signal);
|
|
16903
17634
|
console.log("Context compacted. User instructions were retained; earlier activity was replaced by a bounded continuation record.");
|
|
16904
17635
|
} catch (error) {
|
|
16905
|
-
console.log(
|
|
17636
|
+
console.log(chalk11.red(errorMessage2(error)));
|
|
16906
17637
|
}
|
|
16907
17638
|
return "ok";
|
|
16908
17639
|
case "resume":
|
|
@@ -16913,7 +17644,7 @@ ${extra}`);
|
|
|
16913
17644
|
try {
|
|
16914
17645
|
await resume(argument);
|
|
16915
17646
|
} catch (error) {
|
|
16916
|
-
console.log(
|
|
17647
|
+
console.log(chalk11.red(errorMessage2(error)));
|
|
16917
17648
|
}
|
|
16918
17649
|
return "ok";
|
|
16919
17650
|
case "save": {
|
|
@@ -16964,7 +17695,7 @@ ${extra}`);
|
|
|
16964
17695
|
const refused = session.refusedPermissions;
|
|
16965
17696
|
if (refused.length) {
|
|
16966
17697
|
console.log(
|
|
16967
|
-
|
|
17698
|
+
chalk11.gray(`refused for this session: ${refused.join(", ")} (undo with /allow <perm>)`)
|
|
16968
17699
|
);
|
|
16969
17700
|
}
|
|
16970
17701
|
return "ok";
|
|
@@ -16973,9 +17704,9 @@ ${extra}`);
|
|
|
16973
17704
|
case "allow": {
|
|
16974
17705
|
if (argument === "write" || argument === "shell" || argument === "network") {
|
|
16975
17706
|
session.setPermission(argument, command === "allow");
|
|
16976
|
-
console.log(
|
|
17707
|
+
console.log(chalk11.gray(`${command === "allow" ? "Granted" : "Revoked"} ${argument} for this session.`));
|
|
16977
17708
|
} else {
|
|
16978
|
-
console.log(
|
|
17709
|
+
console.log(chalk11.red(`Usage: /${command} <write|shell|network>`));
|
|
16979
17710
|
}
|
|
16980
17711
|
return "ok";
|
|
16981
17712
|
}
|
|
@@ -16994,7 +17725,7 @@ ${extra}`);
|
|
|
16994
17725
|
const names = session.advertisedToolNames;
|
|
16995
17726
|
console.log(`Advertised to the model this turn (${names.length}): ${names.join(", ")}`);
|
|
16996
17727
|
console.log(
|
|
16997
|
-
|
|
17728
|
+
chalk11.gray(
|
|
16998
17729
|
"Registered tools remain subject to permission and mode checks. `ultrac tools` lists them all with schemas; --tools all advertises them all."
|
|
16999
17730
|
)
|
|
17000
17731
|
);
|
|
@@ -17003,7 +17734,7 @@ ${extra}`);
|
|
|
17003
17734
|
case "memory": {
|
|
17004
17735
|
const memories = await listMemories(workspace);
|
|
17005
17736
|
if (!memories.length) {
|
|
17006
|
-
console.log(
|
|
17737
|
+
console.log(chalk11.gray("No stored memories. Add one with `ultrac memory add`."));
|
|
17007
17738
|
} else {
|
|
17008
17739
|
for (const memory of memories) {
|
|
17009
17740
|
console.log(`- [${memory.scope}/${memory.name}] (${memory.type}) ${memory.description}`);
|
|
@@ -17014,7 +17745,7 @@ ${extra}`);
|
|
|
17014
17745
|
case "skills": {
|
|
17015
17746
|
const skills = await listSkills(workspace);
|
|
17016
17747
|
if (!skills.length) {
|
|
17017
|
-
console.log(
|
|
17748
|
+
console.log(chalk11.gray("No skills installed. Create one with `ultrac skills create`."));
|
|
17018
17749
|
} else {
|
|
17019
17750
|
for (const skill of skills) {
|
|
17020
17751
|
console.log(`- ${skill.name} (${skill.scope}): ${skill.description}`);
|
|
@@ -17030,12 +17761,12 @@ ${extra}`);
|
|
|
17030
17761
|
force: true
|
|
17031
17762
|
});
|
|
17032
17763
|
if (!update.latestVersion) {
|
|
17033
|
-
console.log(
|
|
17764
|
+
console.log(chalk11.yellow("Could not check npm. Confirm npm access, then run `ultrac update --check` outside this session."));
|
|
17034
17765
|
} else if (update.updateAvailable) {
|
|
17035
|
-
console.log(
|
|
17036
|
-
console.log(
|
|
17766
|
+
console.log(chalk11.yellow(`Update available: ${update.currentVersion} \u2192 ${update.latestVersion}`));
|
|
17767
|
+
console.log(chalk11.gray("Exit this session, then run `ultrac update`."));
|
|
17037
17768
|
} else {
|
|
17038
|
-
console.log(
|
|
17769
|
+
console.log(chalk11.green(`ultrac ${update.currentVersion} is up to date.`));
|
|
17039
17770
|
}
|
|
17040
17771
|
return "ok";
|
|
17041
17772
|
}
|
|
@@ -17047,27 +17778,27 @@ async function runSkillCommand(session, raw, workspace, question) {
|
|
|
17047
17778
|
const resolved = await resolveSkillCommand(workspace, raw);
|
|
17048
17779
|
if (resolved.status === "ok") {
|
|
17049
17780
|
console.log(
|
|
17050
|
-
|
|
17781
|
+
chalk11.gray(` /${resolved.command.command} \u2192 skill ${resolved.command.skillName} (${resolved.command.scope})`)
|
|
17051
17782
|
);
|
|
17052
17783
|
await runTurn(session, resolved.prompt, question);
|
|
17053
17784
|
return;
|
|
17054
17785
|
}
|
|
17055
17786
|
if (resolved.status === "conflict") {
|
|
17056
|
-
console.log(
|
|
17787
|
+
console.log(chalk11.red(resolved.message));
|
|
17057
17788
|
return;
|
|
17058
17789
|
}
|
|
17059
17790
|
if (resolved.status === "builtin") {
|
|
17060
|
-
console.log(
|
|
17791
|
+
console.log(chalk11.red(`/${resolved.command} is a built-in command and was not handled. Try /help.`));
|
|
17061
17792
|
return;
|
|
17062
17793
|
}
|
|
17063
17794
|
if (resolved.status === "unknown") {
|
|
17064
|
-
console.log(
|
|
17795
|
+
console.log(chalk11.red(`Unknown command: /${resolved.command}. Try /help.`));
|
|
17065
17796
|
if (resolved.available.length) {
|
|
17066
|
-
console.log(
|
|
17797
|
+
console.log(chalk11.gray(`Skill commands: ${resolved.available.map((name) => `/${name}`).join(", ")}`));
|
|
17067
17798
|
}
|
|
17068
17799
|
return;
|
|
17069
17800
|
}
|
|
17070
|
-
console.log(
|
|
17801
|
+
console.log(chalk11.red("Unknown command. Try /help."));
|
|
17071
17802
|
}
|
|
17072
17803
|
var LineReader = class {
|
|
17073
17804
|
queue = [];
|
|
@@ -17090,9 +17821,36 @@ var LineReader = class {
|
|
|
17090
17821
|
});
|
|
17091
17822
|
}
|
|
17092
17823
|
next(signal) {
|
|
17824
|
+
return this.waitForLine(signal, true);
|
|
17825
|
+
}
|
|
17826
|
+
/**
|
|
17827
|
+
* Nested prompts must not consume steering messages entered before the
|
|
17828
|
+
* question appeared. They wait for the next line while the queue stays put.
|
|
17829
|
+
*/
|
|
17830
|
+
nextFresh(signal) {
|
|
17831
|
+
return this.waitForLine(signal, false);
|
|
17832
|
+
}
|
|
17833
|
+
/**
|
|
17834
|
+
* Drain ordinary messages in arrival order. A slash command is a control
|
|
17835
|
+
* boundary and remains queued for the main REPL loop along with everything
|
|
17836
|
+
* after it.
|
|
17837
|
+
*/
|
|
17838
|
+
drainQueuedMessages() {
|
|
17839
|
+
const messages = [];
|
|
17840
|
+
while (this.queue.length) {
|
|
17841
|
+
const next = this.queue[0].trim();
|
|
17842
|
+
if (next.startsWith("/")) break;
|
|
17843
|
+
this.queue.shift();
|
|
17844
|
+
if (next) messages.push(next);
|
|
17845
|
+
}
|
|
17846
|
+
return messages;
|
|
17847
|
+
}
|
|
17848
|
+
waitForLine(signal, includeBuffered) {
|
|
17093
17849
|
if (signal?.aborted) return Promise.resolve(null);
|
|
17094
|
-
|
|
17095
|
-
|
|
17850
|
+
if (includeBuffered) {
|
|
17851
|
+
const buffered = this.queue.shift();
|
|
17852
|
+
if (buffered !== void 0) return Promise.resolve(buffered);
|
|
17853
|
+
}
|
|
17096
17854
|
if (this.closed) return Promise.resolve(null);
|
|
17097
17855
|
return new Promise((resolve15) => {
|
|
17098
17856
|
const waiter = { resolve: resolve15, ...signal ? { signal } : {} };
|
|
@@ -17117,13 +17875,27 @@ var LineReader = class {
|
|
|
17117
17875
|
waiter.resolve(line);
|
|
17118
17876
|
}
|
|
17119
17877
|
};
|
|
17878
|
+
function handleReplInterrupt(activeTurn) {
|
|
17879
|
+
if (activeTurn && !activeTurn.signal.aborted) {
|
|
17880
|
+
activeTurn.abort();
|
|
17881
|
+
return "abort-turn";
|
|
17882
|
+
}
|
|
17883
|
+
return "exit-session";
|
|
17884
|
+
}
|
|
17120
17885
|
function printBanner(version, providerName, model, mode, workspace, protocol, budget, diagnosticPath2) {
|
|
17121
|
-
console.log(
|
|
17122
|
-
|
|
17123
|
-
|
|
17124
|
-
|
|
17125
|
-
|
|
17126
|
-
|
|
17886
|
+
console.log(renderWelcome(
|
|
17887
|
+
{
|
|
17888
|
+
version,
|
|
17889
|
+
providerName,
|
|
17890
|
+
model,
|
|
17891
|
+
mode,
|
|
17892
|
+
workspace,
|
|
17893
|
+
protocol,
|
|
17894
|
+
budget,
|
|
17895
|
+
diagnosticPath: diagnosticPath2
|
|
17896
|
+
},
|
|
17897
|
+
{ columns: stdout5.columns, color: stdout5.isTTY }
|
|
17898
|
+
));
|
|
17127
17899
|
}
|
|
17128
17900
|
function describeBudget(mode, selection, registry) {
|
|
17129
17901
|
const window = resolveContextWindow();
|
|
@@ -17140,20 +17912,12 @@ function describeAdvertisedTools(mode, selection, registry = createDefaultToolRe
|
|
|
17140
17912
|
};
|
|
17141
17913
|
}
|
|
17142
17914
|
function printToolResult2(result) {
|
|
17143
|
-
const
|
|
17144
|
-
console.log(
|
|
17145
|
-
const preview = result.content.split("\n").slice(0, 6).join("\n");
|
|
17146
|
-
if (preview.trim()) {
|
|
17147
|
-
console.log(chalk9.gray(indent3(truncate2(preview, 800))));
|
|
17148
|
-
}
|
|
17915
|
+
const failure = toolFailurePreview(result);
|
|
17916
|
+
if (failure) console.log(chalk11.red(` Failed: ${failure}`));
|
|
17149
17917
|
for (const warning of getMcpArchiveWarnings(result)) {
|
|
17150
|
-
console.log(
|
|
17918
|
+
console.log(chalk11.yellow(indent3(`Warning: ${warning}`)));
|
|
17151
17919
|
}
|
|
17152
17920
|
}
|
|
17153
|
-
function compactInput(input) {
|
|
17154
|
-
const text = JSON.stringify(input);
|
|
17155
|
-
return text.length > 80 ? `${text.slice(0, 77)}...` : text;
|
|
17156
|
-
}
|
|
17157
17921
|
function shellApprovalPrompt(call, workspace) {
|
|
17158
17922
|
const command = typeof call.input.command === "string" ? call.input.command : "<invalid command>";
|
|
17159
17923
|
const requestedCwd = typeof call.input.cwd === "string" ? call.input.cwd : ".";
|
|
@@ -17173,39 +17937,73 @@ function shellApprovalPrompt(call, workspace) {
|
|
|
17173
17937
|
" [o]nce runs only this command / [a]lways grants shell access for this whole session, including other shell-using tools / [n]o (this call) / n[e]ver (this session) \u203A "
|
|
17174
17938
|
].join("\n");
|
|
17175
17939
|
}
|
|
17176
|
-
function truncate2(value, max) {
|
|
17177
|
-
return value.length > max ? `${value.slice(0, max)}\u2026` : value;
|
|
17178
|
-
}
|
|
17179
17940
|
function indent3(value) {
|
|
17180
17941
|
return value.split("\n").map((line) => ` ${line}`).join("\n");
|
|
17181
17942
|
}
|
|
17182
17943
|
|
|
17183
17944
|
// src/commands/run.ts
|
|
17184
|
-
import
|
|
17945
|
+
import { basename as basename3 } from "path";
|
|
17946
|
+
import chalk12 from "chalk";
|
|
17947
|
+
|
|
17948
|
+
// src/core/autonomous.ts
|
|
17949
|
+
var AUTONOMOUS_CONTINUE_TASK = "Continue the task from where the previous round stopped. Check what is already done, finish the rest, and verify it.";
|
|
17950
|
+
var DEFAULT_AUTONOMOUS_MAX_ROUNDS = 5;
|
|
17951
|
+
var AUTONOMOUS_MAX_STEPS = 200;
|
|
17952
|
+
function shouldResume(stopReason) {
|
|
17953
|
+
return stopReason === "max-steps" || stopReason === "error";
|
|
17954
|
+
}
|
|
17955
|
+
async function runAutonomousLoop(options) {
|
|
17956
|
+
if (!Number.isInteger(options.maxRounds) || options.maxRounds < 1) {
|
|
17957
|
+
throw new Error("maxRounds must be a positive integer.");
|
|
17958
|
+
}
|
|
17959
|
+
let previous;
|
|
17960
|
+
for (let round = 1; ; round += 1) {
|
|
17961
|
+
const session = await options.runRound(round, previous);
|
|
17962
|
+
const willResume = shouldResume(session.stopReason) && round < options.maxRounds && !options.signal?.aborted;
|
|
17963
|
+
await options.onRoundEnd?.(round, session, willResume);
|
|
17964
|
+
if (!willResume) return { session, rounds: round };
|
|
17965
|
+
previous = session;
|
|
17966
|
+
}
|
|
17967
|
+
}
|
|
17968
|
+
|
|
17969
|
+
// src/commands/run.ts
|
|
17970
|
+
function parsePositiveInt(value, flag) {
|
|
17971
|
+
const parsed = Number.parseInt(value, 10);
|
|
17972
|
+
if (!Number.isInteger(parsed) || parsed < 1 || String(parsed) !== value.trim()) {
|
|
17973
|
+
throw new Error(`${flag} must be a positive integer.`);
|
|
17974
|
+
}
|
|
17975
|
+
return parsed;
|
|
17976
|
+
}
|
|
17185
17977
|
var EXIT_INCOMPLETE_RUN = 2;
|
|
17978
|
+
var AUTONOMOUS_ERROR_BACKOFF_MS = 15e3;
|
|
17979
|
+
var NOTIFY_SUMMARY_CHARS = 1200;
|
|
17186
17980
|
function registerRunCommand(program2) {
|
|
17187
17981
|
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(
|
|
17188
17982
|
"--allow-incomplete",
|
|
17189
17983
|
`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.`,
|
|
17190
17984
|
false
|
|
17191
|
-
).option(
|
|
17985
|
+
).option(
|
|
17986
|
+
"--autonomous",
|
|
17987
|
+
`Run with no human in the loop: write, shell and network are granted, each round may take up to ${AUTONOMOUS_MAX_STEPS} steps, and a round that runs out of steps or fails resumes on its own. With a phone channel configured (\`ultrac notify setup\`), questions go to your phone and you are told when it finishes.`,
|
|
17988
|
+
false
|
|
17989
|
+
).option("--max-rounds <number>", `With --autonomous: how many times a run may resume itself (default ${DEFAULT_AUTONOMOUS_MAX_ROUNDS})`).option("--notify", "Send ask_user questions and the final result to your phone (see `ultrac notify setup`)", false).option("--json", "Print the session JSON", false).action(async (taskParts, options) => {
|
|
17192
17990
|
const task = taskParts.join(" ").trim();
|
|
17193
17991
|
if (!task) {
|
|
17194
17992
|
throw new Error("A task is required.");
|
|
17195
17993
|
}
|
|
17196
17994
|
const workspace = await assertWorkspaceExists(options.workspace);
|
|
17197
17995
|
const diagnosticPath2 = configureDiagnosticLog(workspace, "run");
|
|
17198
|
-
console.error(
|
|
17996
|
+
console.error(chalk12.gray(`Diagnostic log: ${diagnosticPath2}`));
|
|
17199
17997
|
if (options.baseUrl) {
|
|
17200
17998
|
setBaseUrlFlag(options.baseUrl);
|
|
17201
17999
|
}
|
|
17202
18000
|
const loaded = await loadConfigWithTrust(workspace);
|
|
17203
18001
|
const config = loaded.config;
|
|
17204
18002
|
if (loaded.needsApproval) {
|
|
17205
|
-
console.error(
|
|
18003
|
+
console.error(chalk12.yellow(withheldNotice(workspace, loaded.withheld, loaded.trustStatus)));
|
|
17206
18004
|
}
|
|
17207
18005
|
const toolSelection = applyBudgetFlags(options, config);
|
|
17208
|
-
setContextBudgetNotice((message) => console.error(
|
|
18006
|
+
setContextBudgetNotice((message) => console.error(chalk12.yellow(message)));
|
|
17209
18007
|
const providerName = options.provider ?? config.provider;
|
|
17210
18008
|
const mode = options.mode ?? config.mode;
|
|
17211
18009
|
const previousSessionLineage = options.continue ? await loadSessionLineage(workspace, options.continue) : void 0;
|
|
@@ -17216,24 +18014,49 @@ function registerRunCommand(program2) {
|
|
|
17216
18014
|
if (!isAgentModeName(mode)) {
|
|
17217
18015
|
throw new Error(`Unknown mode: ${mode}. Expected one of: ${agentModeNames.join(", ")}.`);
|
|
17218
18016
|
}
|
|
17219
|
-
const
|
|
18017
|
+
const autonomous = options.autonomous;
|
|
18018
|
+
const maxSteps = Number.parseInt(
|
|
18019
|
+
options.maxSteps ?? String(autonomous ? AUTONOMOUS_MAX_STEPS : config.maxSteps),
|
|
18020
|
+
10
|
|
18021
|
+
);
|
|
17220
18022
|
if (!Number.isInteger(maxSteps) || maxSteps < 1) {
|
|
17221
18023
|
throw new Error("--max-steps must be a positive integer.");
|
|
17222
18024
|
}
|
|
18025
|
+
if (options.maxRounds !== void 0 && !autonomous) {
|
|
18026
|
+
throw new Error("--max-rounds only applies with --autonomous.");
|
|
18027
|
+
}
|
|
18028
|
+
const maxRounds = autonomous ? parsePositiveInt(options.maxRounds ?? String(DEFAULT_AUTONOMOUS_MAX_ROUNDS), "--max-rounds") : 1;
|
|
18029
|
+
const notifyConfig = options.notify || autonomous ? await loadNotifyConfig() : null;
|
|
18030
|
+
if (options.notify && !notifyConfig) {
|
|
18031
|
+
throw new Error("--notify needs a phone channel. Run `ultrac notify setup` first.");
|
|
18032
|
+
}
|
|
18033
|
+
const notifier = notifyConfig ? createNotifier(notifyConfig, `Ultrac (${basename3(workspace)})`) : void 0;
|
|
18034
|
+
if (autonomous) {
|
|
18035
|
+
console.error(
|
|
18036
|
+
chalk12.yellow(
|
|
18037
|
+
`Autonomous run: file writes, unsandboxed shell and network are granted with no approvals, up to ${maxRounds} round${maxRounds === 1 ? "" : "s"} of ${maxSteps} steps.`
|
|
18038
|
+
)
|
|
18039
|
+
);
|
|
18040
|
+
console.error(
|
|
18041
|
+
chalk12.gray(
|
|
18042
|
+
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."
|
|
18043
|
+
)
|
|
18044
|
+
);
|
|
18045
|
+
}
|
|
17223
18046
|
const model = options.model ?? (options.provider ? providerDefaultModels[providerName] : config.model);
|
|
17224
18047
|
const provider = createFallbackProvider({
|
|
17225
18048
|
provider: providerName,
|
|
17226
18049
|
model,
|
|
17227
18050
|
fallback: config.fallback,
|
|
17228
18051
|
// stderr, so --json keeps stdout parseable.
|
|
17229
|
-
onFailover: (event) => console.error(
|
|
18052
|
+
onFailover: (event) => console.error(chalk12.yellow(formatFailoverNotice(event))),
|
|
17230
18053
|
// AgentRunner uses native tool calls when supported; wired
|
|
17231
18054
|
// anyway so a non-interactive run stays honest if that ever changes.
|
|
17232
|
-
onDowngrade: (event) => console.error(
|
|
18055
|
+
onDowngrade: (event) => console.error(chalk12.yellow(formatDowngradeNotice(event))),
|
|
17233
18056
|
onRetry: (event) => {
|
|
17234
18057
|
diagnosticEvent("provider.retry", event);
|
|
17235
18058
|
console.error(
|
|
17236
|
-
|
|
18059
|
+
chalk12.yellow(
|
|
17237
18060
|
`Retrying ${event.target.provider} (${event.target.model}) in ${event.delayMs}ms after ${event.classification.reason}`
|
|
17238
18061
|
)
|
|
17239
18062
|
);
|
|
@@ -17242,47 +18065,96 @@ function registerRunCommand(program2) {
|
|
|
17242
18065
|
const registry = createDefaultToolRegistry();
|
|
17243
18066
|
const mcpRuntime = await connectWorkspaceMcpServers(config.mcpServers, { workspace, registry });
|
|
17244
18067
|
setSubagentRuntime({ provider, model, mode, registry });
|
|
18068
|
+
if (notifier) {
|
|
18069
|
+
setAskUserRuntime(
|
|
18070
|
+
async (question, choices, signal) => {
|
|
18071
|
+
console.error(chalk12.cyan(`Asking you on ${notifier.describe()}: ${question}`));
|
|
18072
|
+
try {
|
|
18073
|
+
const answer = await notifier.ask(question, choices, signal);
|
|
18074
|
+
console.error(chalk12.gray(answer === null ? "No reply; the agent will proceed on its own judgement." : `Reply: ${answer}`));
|
|
18075
|
+
return answer;
|
|
18076
|
+
} catch (error) {
|
|
18077
|
+
console.error(chalk12.yellow(`Could not reach your phone: ${error.message}`));
|
|
18078
|
+
return null;
|
|
18079
|
+
}
|
|
18080
|
+
},
|
|
18081
|
+
{ remote: true }
|
|
18082
|
+
);
|
|
18083
|
+
}
|
|
17245
18084
|
try {
|
|
17246
18085
|
if (mcpRuntime && mcpRuntime.statuses.length > 0) {
|
|
17247
|
-
console.error(
|
|
18086
|
+
console.error(chalk12.bold("MCP servers"));
|
|
17248
18087
|
console.error(renderMcpStatuses(mcpRuntime.statuses));
|
|
17249
18088
|
}
|
|
17250
18089
|
const detachProgress = attachProgressUi();
|
|
17251
18090
|
const runner = new AgentRunner(registry);
|
|
17252
18091
|
const writeCheckpoint = createSessionCheckpointWriter(workspace, (message) => {
|
|
17253
|
-
console.error(
|
|
18092
|
+
console.error(chalk12.yellow(message));
|
|
17254
18093
|
});
|
|
17255
18094
|
const controller = new AbortController();
|
|
17256
18095
|
const abortRun = () => controller.abort();
|
|
17257
18096
|
process.on("SIGINT", abortRun);
|
|
17258
18097
|
process.on("SIGTERM", abortRun);
|
|
18098
|
+
const permissions = autonomous ? { write: true, shell: true, network: true } : {
|
|
18099
|
+
write: options.allowWrite || config.permissions.write,
|
|
18100
|
+
shell: options.allowShell || config.permissions.shell,
|
|
18101
|
+
network: options.allowNetwork || config.permissions.network
|
|
18102
|
+
};
|
|
17259
18103
|
let result;
|
|
17260
18104
|
try {
|
|
17261
|
-
result = await
|
|
17262
|
-
|
|
17263
|
-
workspace,
|
|
17264
|
-
provider,
|
|
17265
|
-
model,
|
|
17266
|
-
mode,
|
|
17267
|
-
nativeTools: options.nativeTools,
|
|
17268
|
-
maxSteps,
|
|
17269
|
-
previousSession,
|
|
17270
|
-
previousSessionLineage,
|
|
17271
|
-
...toolSelection ? { tools: toolSelection } : {},
|
|
17272
|
-
permissions: {
|
|
17273
|
-
write: options.allowWrite || config.permissions.write,
|
|
17274
|
-
shell: options.allowShell || config.permissions.shell,
|
|
17275
|
-
network: options.allowNetwork || config.permissions.network
|
|
17276
|
-
},
|
|
18105
|
+
result = await runAutonomousLoop({
|
|
18106
|
+
maxRounds,
|
|
17277
18107
|
signal: controller.signal,
|
|
17278
|
-
|
|
17279
|
-
|
|
17280
|
-
|
|
17281
|
-
|
|
17282
|
-
|
|
17283
|
-
|
|
18108
|
+
runRound: async (round, previous) => {
|
|
18109
|
+
const lineage = previous ? await loadSessionLineage(workspace, previous.id) : previousSessionLineage;
|
|
18110
|
+
let lastCheckpoint;
|
|
18111
|
+
try {
|
|
18112
|
+
const { session } = await runner.run({
|
|
18113
|
+
task: previous ? AUTONOMOUS_CONTINUE_TASK : task,
|
|
18114
|
+
workspace,
|
|
18115
|
+
provider,
|
|
18116
|
+
model,
|
|
18117
|
+
mode,
|
|
18118
|
+
nativeTools: options.nativeTools,
|
|
18119
|
+
maxSteps,
|
|
18120
|
+
previousSession: previous ?? previousSession,
|
|
18121
|
+
previousSessionLineage: lineage,
|
|
18122
|
+
...toolSelection ? { tools: toolSelection } : {},
|
|
18123
|
+
permissions,
|
|
18124
|
+
signal: controller.signal,
|
|
18125
|
+
...autonomous ? { autonomous: { phone: Boolean(notifier) } } : {},
|
|
18126
|
+
// Write the session after every step, atomically, so a run that dies
|
|
18127
|
+
// partway - a crash, a kill, a closed laptop mid-scan - is on disk up to
|
|
18128
|
+
// its last completed step and can be resumed with --continue rather than
|
|
18129
|
+
// restarted from nothing.
|
|
18130
|
+
onCheckpoint: async (session2) => {
|
|
18131
|
+
lastCheckpoint = session2;
|
|
18132
|
+
await writeCheckpoint(session2);
|
|
18133
|
+
}
|
|
18134
|
+
});
|
|
18135
|
+
return session;
|
|
18136
|
+
} catch (error) {
|
|
18137
|
+
if (!autonomous || !lastCheckpoint || controller.signal.aborted) throw error;
|
|
18138
|
+
console.error(
|
|
18139
|
+
chalk12.yellow(`Round ${round} failed: ${error.message}. Resuming from its last checkpoint.`)
|
|
18140
|
+
);
|
|
18141
|
+
return { ...lastCheckpoint, stopReason: "error" };
|
|
18142
|
+
}
|
|
18143
|
+
},
|
|
18144
|
+
onRoundEnd: async (round, session, willResume) => {
|
|
18145
|
+
if (!willResume) return;
|
|
18146
|
+
await saveSession(workspace, session);
|
|
18147
|
+
console.error(
|
|
18148
|
+
chalk12.yellow(
|
|
18149
|
+
`Round ${round} stopped (${session.stopReason}); resuming in round ${round + 1} of ${maxRounds}.`
|
|
18150
|
+
)
|
|
18151
|
+
);
|
|
18152
|
+
if (session.stopReason === "error") await sleep(AUTONOMOUS_ERROR_BACKOFF_MS, controller.signal);
|
|
17284
18153
|
}
|
|
17285
18154
|
});
|
|
18155
|
+
} catch (error) {
|
|
18156
|
+
await notifier?.send(`stopped with an error: ${error.message}`).catch(() => void 0);
|
|
18157
|
+
throw error;
|
|
17286
18158
|
} finally {
|
|
17287
18159
|
process.off("SIGINT", abortRun);
|
|
17288
18160
|
process.off("SIGTERM", abortRun);
|
|
@@ -17300,7 +18172,7 @@ function registerRunCommand(program2) {
|
|
|
17300
18172
|
}
|
|
17301
18173
|
const stopReason = result.session.stopReason;
|
|
17302
18174
|
for (const warning of getSessionMcpArchiveWarnings(result.session)) {
|
|
17303
|
-
console.error(
|
|
18175
|
+
console.error(chalk12.yellow(`Warning: ${warning}`));
|
|
17304
18176
|
}
|
|
17305
18177
|
if (options.json) {
|
|
17306
18178
|
console.log(
|
|
@@ -17309,10 +18181,19 @@ function registerRunCommand(program2) {
|
|
|
17309
18181
|
} else {
|
|
17310
18182
|
printSession(result.session, sessionPath2);
|
|
17311
18183
|
}
|
|
18184
|
+
if (notifier) {
|
|
18185
|
+
const outcome = stopReason === "completed" ? "finished" : `stopped without finishing (${stopReason ?? "unknown"})`;
|
|
18186
|
+
const rounds = result.rounds > 1 ? ` after ${result.rounds} rounds` : "";
|
|
18187
|
+
const summary = result.session.finalMessage.trim().slice(0, NOTIFY_SUMMARY_CHARS);
|
|
18188
|
+
await notifier.send(`${outcome}${rounds}.${summary ? `
|
|
18189
|
+
|
|
18190
|
+
${summary}` : ""}`).catch((error) => console.error(chalk12.yellow(`Could not send the result to your phone: ${error.message}`)));
|
|
18191
|
+
}
|
|
17312
18192
|
if (isIncompleteStop(stopReason) && !options.allowIncomplete) {
|
|
17313
18193
|
process.exitCode = EXIT_INCOMPLETE_RUN;
|
|
17314
18194
|
}
|
|
17315
18195
|
} finally {
|
|
18196
|
+
if (notifier) setAskUserRuntime(void 0);
|
|
17316
18197
|
mcpRuntime?.close();
|
|
17317
18198
|
}
|
|
17318
18199
|
});
|
|
@@ -17323,7 +18204,7 @@ import { resolve as resolve12 } from "path";
|
|
|
17323
18204
|
function registerSessionsCommand(program2) {
|
|
17324
18205
|
const sessions = program2.command("sessions").description("Inspect saved Ultrac session transcripts");
|
|
17325
18206
|
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) => {
|
|
17326
|
-
await runSessionTool("list_sessions", { maxResults:
|
|
18207
|
+
await runSessionTool("list_sessions", { maxResults: parsePositiveInt2(options.maxResults) }, options);
|
|
17327
18208
|
});
|
|
17328
18209
|
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) => {
|
|
17329
18210
|
await runSessionTool("read_session", { id }, options);
|
|
@@ -17331,14 +18212,14 @@ function registerSessionsCommand(program2) {
|
|
|
17331
18212
|
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) => {
|
|
17332
18213
|
await runSessionTool(
|
|
17333
18214
|
"search_sessions",
|
|
17334
|
-
{ query: queryParts.join(" ").trim(), maxResults:
|
|
18215
|
+
{ query: queryParts.join(" ").trim(), maxResults: parsePositiveInt2(options.maxResults) },
|
|
17335
18216
|
options
|
|
17336
18217
|
);
|
|
17337
18218
|
});
|
|
17338
18219
|
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) => {
|
|
17339
18220
|
const workspace = resolve12(options.workspace);
|
|
17340
18221
|
const format3 = parseExportFormat(options.format);
|
|
17341
|
-
const maxChars =
|
|
18222
|
+
const maxChars = parsePositiveInt2(options.maxChars);
|
|
17342
18223
|
const outcome = parseOutcomeStatus(options.outcome);
|
|
17343
18224
|
const session = await loadSession(workspace, id);
|
|
17344
18225
|
const episode = buildEpisodePackage(session, { outcome, notes: options.notes ?? "" });
|
|
@@ -17367,7 +18248,7 @@ async function runSessionTool(tool, input, options) {
|
|
|
17367
18248
|
}
|
|
17368
18249
|
console.log(result.content);
|
|
17369
18250
|
}
|
|
17370
|
-
function
|
|
18251
|
+
function parsePositiveInt2(value) {
|
|
17371
18252
|
const parsed = Number.parseInt(value, 10);
|
|
17372
18253
|
if (!Number.isInteger(parsed) || parsed < 1) {
|
|
17373
18254
|
throw new Error("Value must be a positive integer.");
|
|
@@ -17585,7 +18466,7 @@ function parsePermissions(list) {
|
|
|
17585
18466
|
}
|
|
17586
18467
|
|
|
17587
18468
|
// src/commands/tasks.ts
|
|
17588
|
-
import
|
|
18469
|
+
import chalk13 from "chalk";
|
|
17589
18470
|
|
|
17590
18471
|
// src/runs/api.ts
|
|
17591
18472
|
var RUN_STATUSES = ["queued", "running", "succeeded", "failed", "cancelled"];
|
|
@@ -17890,7 +18771,7 @@ function record2(value) {
|
|
|
17890
18771
|
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
17891
18772
|
}
|
|
17892
18773
|
async function followRun(client, runId, options = {}) {
|
|
17893
|
-
const
|
|
18774
|
+
const sleep2 = options.sleep ?? defaultSleep2;
|
|
17894
18775
|
const minBackoffMs = options.minBackoffMs ?? 1e3;
|
|
17895
18776
|
const maxBackoffMs = options.maxBackoffMs ?? 15e3;
|
|
17896
18777
|
const signal = options.signal;
|
|
@@ -18002,7 +18883,7 @@ async function followRun(client, runId, options = {}) {
|
|
|
18002
18883
|
failures,
|
|
18003
18884
|
message: errorMessage3(cause)
|
|
18004
18885
|
});
|
|
18005
|
-
await
|
|
18886
|
+
await sleep2(delayMs2, signal);
|
|
18006
18887
|
continue;
|
|
18007
18888
|
}
|
|
18008
18889
|
if (!response.ok) {
|
|
@@ -18017,7 +18898,7 @@ async function followRun(client, runId, options = {}) {
|
|
|
18017
18898
|
failures,
|
|
18018
18899
|
message: apiError.message
|
|
18019
18900
|
});
|
|
18020
|
-
await
|
|
18901
|
+
await sleep2(delayMs2, signal);
|
|
18021
18902
|
continue;
|
|
18022
18903
|
}
|
|
18023
18904
|
throw apiError;
|
|
@@ -18026,7 +18907,7 @@ async function followRun(client, runId, options = {}) {
|
|
|
18026
18907
|
failures += 1;
|
|
18027
18908
|
const delayMs2 = backoff();
|
|
18028
18909
|
options.onReconnect?.({ reason: "stream_failed", delayMs: delayMs2, lastEventId: state.lastEventId, failures, message: "empty body" });
|
|
18029
|
-
await
|
|
18910
|
+
await sleep2(delayMs2, signal);
|
|
18030
18911
|
continue;
|
|
18031
18912
|
}
|
|
18032
18913
|
let broke = null;
|
|
@@ -18044,7 +18925,7 @@ async function followRun(client, runId, options = {}) {
|
|
|
18044
18925
|
const event = { id: message.id, event: message.event, data: parseData(message.data) };
|
|
18045
18926
|
const applied = apply(event);
|
|
18046
18927
|
options.onEvent?.(event, state);
|
|
18047
|
-
const result = applied && "reconcileStatus" in applied ? await reconcileTerminalOutput(client, runId, applied, state, signal,
|
|
18928
|
+
const result = applied && "reconcileStatus" in applied ? await reconcileTerminalOutput(client, runId, applied, state, signal, sleep2) : applied;
|
|
18048
18929
|
if (result) return result;
|
|
18049
18930
|
throwIfAborted();
|
|
18050
18931
|
}
|
|
@@ -18064,12 +18945,12 @@ async function followRun(client, runId, options = {}) {
|
|
|
18064
18945
|
if (!sawEvent) failures = 0;
|
|
18065
18946
|
options.onReconnect?.({ reason: "stream_ended", delayMs, lastEventId: state.lastEventId, failures });
|
|
18066
18947
|
}
|
|
18067
|
-
await
|
|
18948
|
+
await sleep2(delayMs, signal);
|
|
18068
18949
|
}
|
|
18069
18950
|
}
|
|
18070
18951
|
var OUTPUT_READ_TIMEOUT_MS = 1500;
|
|
18071
18952
|
var OUTPUT_READ_DELAYS_MS = [100, 300];
|
|
18072
|
-
async function reconcileTerminalOutput(client, runId, pending, state, signal,
|
|
18953
|
+
async function reconcileTerminalOutput(client, runId, pending, state, signal, sleep2) {
|
|
18073
18954
|
const { reconcileStatus, ...result } = pending;
|
|
18074
18955
|
for (let attempt = 0; attempt < OUTPUT_READ_DELAYS_MS.length + 1; attempt += 1) {
|
|
18075
18956
|
if (signal?.aborted) throw new FollowAbortedError(state);
|
|
@@ -18087,7 +18968,7 @@ async function reconcileTerminalOutput(client, runId, pending, state, signal, sl
|
|
|
18087
18968
|
} catch {
|
|
18088
18969
|
if (signal?.aborted) throw new FollowAbortedError(state);
|
|
18089
18970
|
}
|
|
18090
|
-
if (attempt < OUTPUT_READ_DELAYS_MS.length) await
|
|
18971
|
+
if (attempt < OUTPUT_READ_DELAYS_MS.length) await sleep2(OUTPUT_READ_DELAYS_MS[attempt], signal);
|
|
18091
18972
|
}
|
|
18092
18973
|
return { ...result, text: state.text, outputSource: "incomplete" };
|
|
18093
18974
|
}
|
|
@@ -18121,7 +19002,7 @@ function registerTasksCommand(program2, deps = {}) {
|
|
|
18121
19002
|
}
|
|
18122
19003
|
r.out(formatRunTable(page.data, r.now()));
|
|
18123
19004
|
if (page.next_cursor) {
|
|
18124
|
-
r.err(
|
|
19005
|
+
r.err(chalk13.gray(`More: ultrac tasks --cursor ${page.next_cursor}
|
|
18125
19006
|
`));
|
|
18126
19007
|
}
|
|
18127
19008
|
});
|
|
@@ -18180,7 +19061,7 @@ async function runTask(promptParts, options, deps) {
|
|
|
18180
19061
|
`);
|
|
18181
19062
|
} else {
|
|
18182
19063
|
r.err(
|
|
18183
|
-
|
|
19064
|
+
chalk13.gray(
|
|
18184
19065
|
`Run ${run.id} queued. Following; Ctrl-C ${options.detach ? "leaves it running" : "cancels it"}.
|
|
18185
19066
|
`
|
|
18186
19067
|
)
|
|
@@ -18198,7 +19079,7 @@ async function follow(r, runId, options) {
|
|
|
18198
19079
|
controller.abort();
|
|
18199
19080
|
return;
|
|
18200
19081
|
}
|
|
18201
|
-
r.err(
|
|
19082
|
+
r.err(chalk13.yellow(`
|
|
18202
19083
|
Leaving run ${runId} as it is.
|
|
18203
19084
|
`));
|
|
18204
19085
|
r.exit(EXIT_CANCELLED);
|
|
@@ -18214,7 +19095,7 @@ Leaving run ${runId} as it is.
|
|
|
18214
19095
|
if (status === "running") {
|
|
18215
19096
|
const attempt = typeof data?.attempt === "number" ? data.attempt : null;
|
|
18216
19097
|
if (data?.restarted === true || attempt !== null && attempt > 1) {
|
|
18217
|
-
r.err(
|
|
19098
|
+
r.err(chalk13.yellow(`${lead()}[attempt ${attempt ?? "?"} restarted; streaming from the start]
|
|
18218
19099
|
`));
|
|
18219
19100
|
}
|
|
18220
19101
|
printed = "";
|
|
@@ -18229,7 +19110,7 @@ Leaving run ${runId} as it is.
|
|
|
18229
19110
|
return;
|
|
18230
19111
|
}
|
|
18231
19112
|
case "tool_call": {
|
|
18232
|
-
r.err(
|
|
19113
|
+
r.err(chalk13.cyan(`${lead()}[tool_call ${String(data?.name ?? "")} ${String(data?.arguments ?? "")}]
|
|
18233
19114
|
`));
|
|
18234
19115
|
return;
|
|
18235
19116
|
}
|
|
@@ -18269,7 +19150,7 @@ Leaving run ${runId} as it is.
|
|
|
18269
19150
|
`);
|
|
18270
19151
|
} else {
|
|
18271
19152
|
const seconds = Math.round(info.delayMs / 100) / 10;
|
|
18272
|
-
r.err(
|
|
19153
|
+
r.err(chalk13.gray(`${lead()}[reconnecting in ${seconds}s${info.message ? `: ${info.message}` : ""}]
|
|
18273
19154
|
`));
|
|
18274
19155
|
}
|
|
18275
19156
|
}
|
|
@@ -18301,27 +19182,27 @@ Leaving run ${runId} as it is.
|
|
|
18301
19182
|
return result.outputSource === "incomplete" ? EXIT_FAILED : exitCodeFor(result.status);
|
|
18302
19183
|
}
|
|
18303
19184
|
if (result.outputSource === "incomplete") {
|
|
18304
|
-
r.err(
|
|
19185
|
+
r.err(chalk13.yellow(lead() + "[stored task output could not be confirmed; showing text received from the stream]\n"));
|
|
18305
19186
|
}
|
|
18306
19187
|
switch (result.status) {
|
|
18307
19188
|
case "succeeded":
|
|
18308
19189
|
finishHuman(result.text);
|
|
18309
19190
|
if (result.toolCalls.length && !result.text) {
|
|
18310
19191
|
const count3 = result.toolCalls.length;
|
|
18311
|
-
r.err(
|
|
19192
|
+
r.err(chalk13.gray(`Done with ${count3} tool call${count3 === 1 ? "" : "s"} and no text.
|
|
18312
19193
|
`));
|
|
18313
19194
|
}
|
|
18314
19195
|
return result.outputSource === "incomplete" ? EXIT_FAILED : EXIT_DONE;
|
|
18315
19196
|
case "failed": {
|
|
18316
19197
|
finishHuman(result.text);
|
|
18317
19198
|
const code = result.error?.code ? ` (${result.error.code})` : "";
|
|
18318
|
-
r.err(
|
|
19199
|
+
r.err(chalk13.red(`Run ${runId} failed${code}: ${result.error?.message ?? "The task failed."}
|
|
18319
19200
|
`));
|
|
18320
19201
|
return EXIT_FAILED;
|
|
18321
19202
|
}
|
|
18322
19203
|
case "cancelled":
|
|
18323
19204
|
finishHuman(result.text);
|
|
18324
|
-
r.err(
|
|
19205
|
+
r.err(chalk13.yellow(`Run ${runId} was cancelled.
|
|
18325
19206
|
`));
|
|
18326
19207
|
return EXIT_CANCELLED;
|
|
18327
19208
|
}
|
|
@@ -18334,13 +19215,13 @@ async function interrupted(r, runId, options, printed) {
|
|
|
18334
19215
|
`);
|
|
18335
19216
|
} else {
|
|
18336
19217
|
r.err(
|
|
18337
|
-
|
|
19218
|
+
chalk13.yellow(`${lead}Detached. Run ${runId} continues; follow it again with \`ultrac tasks follow ${runId}\`.
|
|
18338
19219
|
`)
|
|
18339
19220
|
);
|
|
18340
19221
|
}
|
|
18341
19222
|
return EXIT_CANCELLED;
|
|
18342
19223
|
}
|
|
18343
|
-
if (!options.json) r.err(
|
|
19224
|
+
if (!options.json) r.err(chalk13.yellow(`${lead}Cancelling run ${runId}...
|
|
18344
19225
|
`));
|
|
18345
19226
|
try {
|
|
18346
19227
|
const cancelled = await r.client.cancelRun(runId);
|
|
@@ -18348,7 +19229,7 @@ async function interrupted(r, runId, options, printed) {
|
|
|
18348
19229
|
r.out(`${JSON.stringify({ id: null, event: "cancel", data: cancelled })}
|
|
18349
19230
|
`);
|
|
18350
19231
|
} else {
|
|
18351
|
-
r.err(
|
|
19232
|
+
r.err(chalk13.yellow(`${describeCancel(cancelled)}
|
|
18352
19233
|
`));
|
|
18353
19234
|
}
|
|
18354
19235
|
} catch (error) {
|
|
@@ -18357,12 +19238,12 @@ async function interrupted(r, runId, options, printed) {
|
|
|
18357
19238
|
r.out(`${JSON.stringify({ id: null, event: "cancel_failed", data: { runId, message } })}
|
|
18358
19239
|
`);
|
|
18359
19240
|
} else if (error instanceof UltracApiError && error.code === "run_already_finished") {
|
|
18360
|
-
r.err(
|
|
19241
|
+
r.err(chalk13.yellow(`${error.message} Nothing to cancel.
|
|
18361
19242
|
`));
|
|
18362
19243
|
} else {
|
|
18363
|
-
r.err(
|
|
19244
|
+
r.err(chalk13.red(`Could not cancel run ${runId}: ${message}
|
|
18364
19245
|
`));
|
|
18365
|
-
r.err(
|
|
19246
|
+
r.err(chalk13.gray(`Try again with \`ultrac tasks cancel ${runId}\`.
|
|
18366
19247
|
`));
|
|
18367
19248
|
}
|
|
18368
19249
|
}
|
|
@@ -18474,7 +19355,7 @@ ${run.output}${run.output.endsWith("\n") ? "" : "\n"}`;
|
|
|
18474
19355
|
|
|
18475
19356
|
// src/commands/tools.ts
|
|
18476
19357
|
import { resolve as resolve14 } from "path";
|
|
18477
|
-
import
|
|
19358
|
+
import chalk14 from "chalk";
|
|
18478
19359
|
function registerToolsCommand(program2) {
|
|
18479
19360
|
const toolsCommand = program2.command("tools");
|
|
18480
19361
|
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) => {
|
|
@@ -18494,7 +19375,7 @@ function registerToolsCommand(program2) {
|
|
|
18494
19375
|
printTools(tools);
|
|
18495
19376
|
if (options.mode) {
|
|
18496
19377
|
console.log(
|
|
18497
|
-
|
|
19378
|
+
chalk14.gray(
|
|
18498
19379
|
`
|
|
18499
19380
|
${tools.length} of ${all.length} tools advertised in ${options.mode} mode, ${serializedToolsSize(
|
|
18500
19381
|
toToolSchemas(tools)
|
|
@@ -18543,11 +19424,11 @@ function printToolBudget(all, json) {
|
|
|
18543
19424
|
}
|
|
18544
19425
|
for (const row of [...rows, { ...everything, names: all.map((tool) => tool.name) }]) {
|
|
18545
19426
|
console.log(
|
|
18546
|
-
`${
|
|
19427
|
+
`${chalk14.cyan(row.mode.padEnd(9))} ${String(row.tools).padStart(2)} tools ${String(
|
|
18547
19428
|
row.chars.toLocaleString("en-US")
|
|
18548
19429
|
).padStart(7)} chars`
|
|
18549
19430
|
);
|
|
18550
|
-
console.log(
|
|
19431
|
+
console.log(chalk14.gray(` ${row.names.join(", ")}`));
|
|
18551
19432
|
}
|
|
18552
19433
|
}
|
|
18553
19434
|
function parseToolInput(inputText) {
|
|
@@ -18559,7 +19440,7 @@ function parseToolInput(inputText) {
|
|
|
18559
19440
|
}
|
|
18560
19441
|
|
|
18561
19442
|
// src/commands/update.ts
|
|
18562
|
-
import
|
|
19443
|
+
import chalk15 from "chalk";
|
|
18563
19444
|
function registerUpdateCommand(program2) {
|
|
18564
19445
|
const packageInfo = readPackageInfo();
|
|
18565
19446
|
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) => {
|
|
@@ -18574,7 +19455,7 @@ function registerUpdateCommand(program2) {
|
|
|
18574
19455
|
);
|
|
18575
19456
|
}
|
|
18576
19457
|
if (!status.updateAvailable) {
|
|
18577
|
-
console.log(
|
|
19458
|
+
console.log(chalk15.green(`ultrac ${status.currentVersion} is up to date.`));
|
|
18578
19459
|
return;
|
|
18579
19460
|
}
|
|
18580
19461
|
if (options.check) {
|
|
@@ -18583,12 +19464,12 @@ function registerUpdateCommand(program2) {
|
|
|
18583
19464
|
}
|
|
18584
19465
|
console.log(`Updating ultrac ${status.currentVersion} \u2192 ${status.latestVersion}\u2026`);
|
|
18585
19466
|
await installLatestVersion(packageInfo.name);
|
|
18586
|
-
console.log(
|
|
19467
|
+
console.log(chalk15.green(`Updated to ultrac ${status.latestVersion}. Restart ultrac to use the new version.`));
|
|
18587
19468
|
});
|
|
18588
19469
|
}
|
|
18589
19470
|
function printAvailable(currentVersion, latestVersion) {
|
|
18590
|
-
console.log(
|
|
18591
|
-
console.log(
|
|
19471
|
+
console.log(chalk15.yellow(`Update available: ultrac ${currentVersion} \u2192 ${latestVersion}`));
|
|
19472
|
+
console.log(chalk15.gray("Run `ultrac update` to install it."));
|
|
18592
19473
|
}
|
|
18593
19474
|
|
|
18594
19475
|
// src/cli.ts
|
|
@@ -18614,10 +19495,11 @@ registerUpdateCommand(program);
|
|
|
18614
19495
|
registerSessionsCommand(program);
|
|
18615
19496
|
registerMemoryCommand(program);
|
|
18616
19497
|
registerMcpCommand(program);
|
|
19498
|
+
registerNotifyCommand(program);
|
|
18617
19499
|
registerSkillsCommand(program);
|
|
18618
19500
|
registerReplCommand(program);
|
|
18619
19501
|
program.parseAsync(process.argv).catch((error) => {
|
|
18620
19502
|
const message = error instanceof Error ? error.message : String(error);
|
|
18621
|
-
console.error(
|
|
19503
|
+
console.error(chalk16.red(message));
|
|
18622
19504
|
process.exitCode = 1;
|
|
18623
19505
|
});
|