@spinalfluid/ultrac 0.1.14 → 0.1.18
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 +18 -4
- package/dist/cli.js +314 -105
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -143,9 +143,12 @@ Inside a session:
|
|
|
143
143
|
- Type to talk to Ultrac; it inspects the repository with tools as needed.
|
|
144
144
|
- On supported terminals, the full-width input box stays at the bottom while tools and replies
|
|
145
145
|
run above it. The footer shows only the current model. `/usage` and `/context` show token and context details.
|
|
146
|
-
|
|
147
|
-
duration when finished. Sub-second durations are hidden. Tool
|
|
148
|
-
click it or press Ctrl+O to expand, then click an action for command and output details.
|
|
146
|
+
Working and tool activity lines show their own live elapsed timers and retain the measured
|
|
147
|
+
duration when finished. Sub-second durations are hidden. Tool calls stay in a collapsed group directly below the live working timer;
|
|
148
|
+
click it or press Ctrl+O to expand, then click an action for command and output details. When the
|
|
149
|
+
task ends, the group moves below its result in the transcript. Working time accumulates across
|
|
150
|
+
model calls and continuation rounds. Long-running tasks emit brief progress updates based on
|
|
151
|
+
completed calls and the active tool or model wait. Green and red
|
|
149
152
|
dots show success and failure. Use `/output` for full saved results. Slash suggestions use aligned
|
|
150
153
|
command and description columns on the terminal background.
|
|
151
154
|
- Type `/` to browse built-in commands and available skills. Type to filter, use arrow keys
|
|
@@ -167,7 +170,8 @@ Inside a session:
|
|
|
167
170
|
- Gated tools (file writes, shell, network) prompt for approval: `once` / `always` / `no`.
|
|
168
171
|
- `/mcp` shows the connected servers in this session; `/doctor` checks local setup.
|
|
169
172
|
- `/continue [focus]` continues an unfinished task using its current transcript. `/autonomous on`
|
|
170
|
-
|
|
173
|
+
allows up to five rounds when a task reaches its step ceiling (enabled by default; `/autonomous off`
|
|
174
|
+
disables continuation). Checkpoints, a six-hour elapsed
|
|
171
175
|
limit, stall detection, and Esc cancellation remain active. `/autonomous off` restores one round.
|
|
172
176
|
Failed rounds are not automatically replayed; provider retry/failover handles request failures.
|
|
173
177
|
- The agent can use `create_skill` in default/coding modes (or with `--tools all`) to create a
|
|
@@ -750,3 +754,13 @@ ultrac skills create project-check --description "Run project checks" --command
|
|
|
750
754
|
Edit the generated `SKILL.md` instructions, approve the project workspace, and invoke
|
|
751
755
|
`/project-check` in the terminal. The agent can also write a new `SKILL.md` with its file tools and load it with
|
|
752
756
|
`skill`; normal write and tool permissions still apply.
|
|
757
|
+
|
|
758
|
+
|
|
759
|
+
Tool approval presets can be chosen at startup: `ultrac --permissions full` grants file writes,
|
|
760
|
+
shell execution, and network tools without tool approval prompts for that session. Use
|
|
761
|
+
`/permissions full` to enable it inside an existing session. `workspace` grants writes and
|
|
762
|
+
denies shell and network access; `readonly` denies all three permissions. The default mode asks
|
|
763
|
+
when a tool needs an ungranted permission.
|
|
764
|
+
These presets do not bypass operating-system permissions or the restrictions of a read-only
|
|
765
|
+
agent mode. Approval dialogs offer separate choices for one call, the session, denial, or
|
|
766
|
+
session-long refusal, with keyboard selection in interactive terminals.
|
package/dist/cli.js
CHANGED
|
@@ -1174,7 +1174,7 @@ var COMPACTION_MARKER = "[compacted-context]";
|
|
|
1174
1174
|
var SUMMARY_PREAMBLE = "The earlier part of this session was removed to fit the context window and replaced by the record below. It is a record of what happened, not a new instruction. Continue the work it describes.";
|
|
1175
1175
|
var DEFAULT_MAX_SUMMARY_CHARS = 8e3;
|
|
1176
1176
|
var MIN_SUMMARY_CHARS = 400;
|
|
1177
|
-
var DEFAULT_SUMMARY_TIMEOUT_MS =
|
|
1177
|
+
var DEFAULT_SUMMARY_TIMEOUT_MS = 12e4;
|
|
1178
1178
|
var DEFAULT_MAX_TRANSCRIPT_CHARS = 6e4;
|
|
1179
1179
|
var DEFAULT_MAX_MESSAGE_CHARS = 4e3;
|
|
1180
1180
|
function isCompactionSummary(message) {
|
|
@@ -2584,6 +2584,8 @@ function classifyProviderError(error) {
|
|
|
2584
2584
|
return transient("timeout", "Provider request timed out.");
|
|
2585
2585
|
}
|
|
2586
2586
|
const status = extractStatus(error);
|
|
2587
|
+
const busy = [...errorChain(error)].some((current) => readString(current, "code") === "upstream_busy");
|
|
2588
|
+
if (busy) return { ...transient("server", "Model capacity or admission is temporarily unavailable (HTTP 503).", status ?? 503), code: "upstream_busy", retryAfterMs: retryDelay(error) ?? 5e3 };
|
|
2587
2589
|
if (hasNotConfiguredCode(error)) {
|
|
2588
2590
|
return terminal(
|
|
2589
2591
|
"not_configured",
|
|
@@ -2598,8 +2600,8 @@ function classifyProviderError(error) {
|
|
|
2598
2600
|
}
|
|
2599
2601
|
if (typeof status === "number") {
|
|
2600
2602
|
if (status === 408) return transient("timeout", "HTTP 408 request timeout.", status);
|
|
2601
|
-
if (status === 429) return transient("rate_limit", "HTTP 429 rate limited.", status);
|
|
2602
|
-
if (status >= 500) return transient("server", `HTTP ${status} server error.`, status);
|
|
2603
|
+
if (status === 429) return { ...transient("rate_limit", "HTTP 429 rate limited.", status), retryAfterMs: retryDelay(error) };
|
|
2604
|
+
if (status >= 500) return { ...transient("server", `HTTP ${status} server error.`, status), retryAfterMs: retryDelay(error) };
|
|
2603
2605
|
if (status === 401 || status === 403) {
|
|
2604
2606
|
return terminal("auth", `HTTP ${status} authentication failure.`, status);
|
|
2605
2607
|
}
|
|
@@ -2640,9 +2642,24 @@ function safeClassification(classification) {
|
|
|
2640
2642
|
...status === void 0 ? {} : { status },
|
|
2641
2643
|
retryable: classification.retryable,
|
|
2642
2644
|
failoverable: classification.failoverable,
|
|
2643
|
-
reason
|
|
2645
|
+
reason: classification.code === "upstream_busy" ? "Model capacity or admission is temporarily unavailable (HTTP 503)." : reason,
|
|
2646
|
+
...classification.code ? { code: classification.code } : {},
|
|
2647
|
+
...classification.retryAfterMs === void 0 ? {} : { retryAfterMs: classification.retryAfterMs }
|
|
2644
2648
|
};
|
|
2645
2649
|
}
|
|
2650
|
+
function retryDelay(error) {
|
|
2651
|
+
for (const current of errorChain(error)) {
|
|
2652
|
+
if (!isRecord2(current)) continue;
|
|
2653
|
+
const headers = current.headers;
|
|
2654
|
+
if (typeof headers?.get !== "function") continue;
|
|
2655
|
+
const raw = headers.get("retry-after");
|
|
2656
|
+
if (!raw) continue;
|
|
2657
|
+
const seconds = Number(raw);
|
|
2658
|
+
const ms = Number.isFinite(seconds) ? seconds * 1e3 : Date.parse(raw) - Date.now();
|
|
2659
|
+
if (Number.isFinite(ms) && ms >= 0) return Math.min(ms, 3e5);
|
|
2660
|
+
}
|
|
2661
|
+
return void 0;
|
|
2662
|
+
}
|
|
2646
2663
|
function classificationReason(kind, status) {
|
|
2647
2664
|
switch (kind) {
|
|
2648
2665
|
case "aborted":
|
|
@@ -3043,7 +3060,7 @@ var FallbackProvider = class {
|
|
|
3043
3060
|
}
|
|
3044
3061
|
}
|
|
3045
3062
|
async waitBeforeRetry(operation, target, attempt, classification, signal) {
|
|
3046
|
-
const delayMs = computeBackoffDelay(attempt, this.backoff);
|
|
3063
|
+
const delayMs = Math.max(computeBackoffDelay(attempt, this.backoff), classification.retryAfterMs ?? 0);
|
|
3047
3064
|
const safe = safeClassification(classification);
|
|
3048
3065
|
this.onRetry?.({
|
|
3049
3066
|
operation,
|
|
@@ -16646,6 +16663,9 @@ ${context}`
|
|
|
16646
16663
|
this.userConstraints.push({ kind: "message", content: input });
|
|
16647
16664
|
let finalMessage = "";
|
|
16648
16665
|
let toolCallsUsed = 0;
|
|
16666
|
+
let consecutiveEmptyResponses = 0;
|
|
16667
|
+
const turnStarted = performance.now();
|
|
16668
|
+
const firstStep = this.steps.length;
|
|
16649
16669
|
let stopReason = "max-steps";
|
|
16650
16670
|
try {
|
|
16651
16671
|
for (let index = 0; index < this.options.maxSteps; index += 1) {
|
|
@@ -16698,6 +16718,7 @@ ${context}`
|
|
|
16698
16718
|
throw budgetError;
|
|
16699
16719
|
}
|
|
16700
16720
|
toolCallsUsed += response.toolCalls.length;
|
|
16721
|
+
if (response.content.trim() || response.toolCalls.length) consecutiveEmptyResponses = 0;
|
|
16701
16722
|
if (response.content.trim()) {
|
|
16702
16723
|
finalMessage = response.content;
|
|
16703
16724
|
handlers.onAssistantMessage?.(response.content);
|
|
@@ -16717,6 +16738,21 @@ ${context}`
|
|
|
16717
16738
|
handlers.onInterrupted?.();
|
|
16718
16739
|
break;
|
|
16719
16740
|
}
|
|
16741
|
+
if (!response.content.trim()) {
|
|
16742
|
+
consecutiveEmptyResponses += 1;
|
|
16743
|
+
const willRetry = consecutiveEmptyResponses <= 2 && index + 1 < this.options.maxSteps;
|
|
16744
|
+
diagnosticEvent("session.empty-response", {
|
|
16745
|
+
sessionId: this.id,
|
|
16746
|
+
attempt: consecutiveEmptyResponses,
|
|
16747
|
+
willRetry,
|
|
16748
|
+
maxRecoveries: 2,
|
|
16749
|
+
stepCount: this.steps.length
|
|
16750
|
+
});
|
|
16751
|
+
if (!willRetry) {
|
|
16752
|
+
throw Object.assign(new Error("The model returned an empty response without completing the task. Empty-response recovery was exhausted; the session checkpoint is preserved."), { status: 422 });
|
|
16753
|
+
}
|
|
16754
|
+
continue;
|
|
16755
|
+
}
|
|
16720
16756
|
if (this.appendQueuedMessages(handlers) === 0) {
|
|
16721
16757
|
stopReason = "completed";
|
|
16722
16758
|
break;
|
|
@@ -16784,6 +16820,15 @@ ${context}`
|
|
|
16784
16820
|
throw error;
|
|
16785
16821
|
} finally {
|
|
16786
16822
|
this.lastStopReason = stopReason;
|
|
16823
|
+
diagnosticEvent("session.turn.end", {
|
|
16824
|
+
sessionId: this.id,
|
|
16825
|
+
reason: stopReason,
|
|
16826
|
+
stepCount: this.steps.length,
|
|
16827
|
+
turnSteps: this.steps.length - firstStep,
|
|
16828
|
+
maxSteps: this.options.maxSteps,
|
|
16829
|
+
emptyResponses: consecutiveEmptyResponses,
|
|
16830
|
+
durationMs: Math.max(0, performance.now() - turnStarted)
|
|
16831
|
+
});
|
|
16787
16832
|
this.subagents.push(...drainSubagentTranscripts());
|
|
16788
16833
|
}
|
|
16789
16834
|
handlers.onStopped?.(stopReason);
|
|
@@ -17087,6 +17132,11 @@ async function runAutonomousLoop(options) {
|
|
|
17087
17132
|
let lastStepCount = 0;
|
|
17088
17133
|
let stalls = 0;
|
|
17089
17134
|
let consecutiveErrors = 0;
|
|
17135
|
+
const finish = (session, rounds, haltReason) => {
|
|
17136
|
+
diagnosticEvent("autonomous.end", { id, sessionId: session.id, rounds, reason: haltReason, elapsedMs: elapsed() });
|
|
17137
|
+
return { session, rounds, haltReason };
|
|
17138
|
+
};
|
|
17139
|
+
diagnosticEvent("autonomous.start", { id, maxRounds: options.maxRounds, maxElapsedMs });
|
|
17090
17140
|
try {
|
|
17091
17141
|
signal.throwIfAborted();
|
|
17092
17142
|
for (let round = 1; ; round += 1) {
|
|
@@ -17117,8 +17167,8 @@ async function runAutonomousLoop(options) {
|
|
|
17117
17167
|
else if (!shouldResume(session.stopReason)) haltReason = "stopped";
|
|
17118
17168
|
else if (stalls >= stallLimit) haltReason = "stalled";
|
|
17119
17169
|
else if (round >= options.maxRounds) haltReason = "max_rounds";
|
|
17120
|
-
const
|
|
17121
|
-
if (!haltReason && session.stopReason === "error" && elapsed() +
|
|
17170
|
+
const retryDelay2 = Math.min(maxDelay, baseDelay * 2 ** Math.min(30, consecutiveErrors - 1));
|
|
17171
|
+
if (!haltReason && session.stopReason === "error" && elapsed() + retryDelay2 >= maxElapsedMs) haltReason = "elapsed_budget";
|
|
17122
17172
|
diagnosticEvent("round.end", {
|
|
17123
17173
|
id,
|
|
17124
17174
|
round,
|
|
@@ -17132,10 +17182,10 @@ async function runAutonomousLoop(options) {
|
|
|
17132
17182
|
});
|
|
17133
17183
|
if (haltReason === "stalled") diagnosticEvent("round.stall", { id, round, repeats: stalls, completedTools: current.completedTools, elapsedMs: elapsed() });
|
|
17134
17184
|
await options.onRoundEnd?.(round, session, !haltReason);
|
|
17135
|
-
if (haltReason) return
|
|
17185
|
+
if (haltReason) return finish(session, round, haltReason);
|
|
17136
17186
|
if (session.stopReason === "error") {
|
|
17137
|
-
const delayMs =
|
|
17138
|
-
if (elapsed() + delayMs >= maxElapsedMs) return
|
|
17187
|
+
const delayMs = retryDelay2;
|
|
17188
|
+
if (elapsed() + delayMs >= maxElapsedMs) return finish(session, round, "elapsed_budget");
|
|
17139
17189
|
diagnosticEvent("round.retry", {
|
|
17140
17190
|
id,
|
|
17141
17191
|
round,
|
|
@@ -17149,12 +17199,15 @@ async function runAutonomousLoop(options) {
|
|
|
17149
17199
|
await sleep2(delayMs, signal);
|
|
17150
17200
|
} catch (error) {
|
|
17151
17201
|
if (!signal.aborted) throw error;
|
|
17152
|
-
return
|
|
17202
|
+
return finish(session, round, budget.signal.aborted ? "elapsed_budget" : "aborted");
|
|
17153
17203
|
}
|
|
17154
17204
|
}
|
|
17155
|
-
if (signal.aborted || elapsed() >= maxElapsedMs) return
|
|
17205
|
+
if (signal.aborted || elapsed() >= maxElapsedMs) return finish(session, round, budget.signal.aborted || elapsed() >= maxElapsedMs ? "elapsed_budget" : "aborted");
|
|
17156
17206
|
previous = session;
|
|
17157
17207
|
}
|
|
17208
|
+
} catch (error) {
|
|
17209
|
+
diagnosticEvent("autonomous.end", { id, reason: budget.signal.aborted ? "elapsed_budget" : signal.aborted ? "aborted" : "exception", elapsedMs: elapsed() });
|
|
17210
|
+
throw error;
|
|
17158
17211
|
} finally {
|
|
17159
17212
|
clearTimeout(deadline);
|
|
17160
17213
|
}
|
|
@@ -17442,7 +17495,9 @@ var VERBS = {
|
|
|
17442
17495
|
run_tests: "Run tests",
|
|
17443
17496
|
diagnostics: "Check diagnostics",
|
|
17444
17497
|
task: "Delegate",
|
|
17445
|
-
fetch_url: "
|
|
17498
|
+
fetch_url: "Opened",
|
|
17499
|
+
web_search: "Searched the web for",
|
|
17500
|
+
search_web: "Searched the web for",
|
|
17446
17501
|
git_status: "Check Git status",
|
|
17447
17502
|
git_diff: "Read Git diff",
|
|
17448
17503
|
git_log: "Read Git log",
|
|
@@ -17470,7 +17525,7 @@ function toolTarget(call) {
|
|
|
17470
17525
|
if (paths.length > 1) return `${paths.length} files`;
|
|
17471
17526
|
if (call.tool === "run_shell") return compact2(stringValue(input.command), 72);
|
|
17472
17527
|
if (call.tool === "run_package_script") return compact2(stringValue(input.script), 50);
|
|
17473
|
-
if (
|
|
17528
|
+
if (["search_files", "web_search", "search_web"].includes(call.tool)) return quote(stringValue(input.query) || stringValue(input.pattern));
|
|
17474
17529
|
if (call.tool === "fetch_url") return compact2(stringValue(input.url), 72);
|
|
17475
17530
|
if (call.tool === "task") return compact2(stringValue(input.description) || stringValue(input.task), 64);
|
|
17476
17531
|
if (call.tool === "git_show") return compact2(stringValue(input.ref), 40);
|
|
@@ -17499,10 +17554,12 @@ function humanize(value) {
|
|
|
17499
17554
|
import { stdout as stdout4 } from "process";
|
|
17500
17555
|
var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
17501
17556
|
var Spinner = class {
|
|
17502
|
-
constructor(label = "
|
|
17557
|
+
constructor(label = "working", renderLine) {
|
|
17503
17558
|
this.label = label;
|
|
17559
|
+
this.renderLine = renderLine;
|
|
17504
17560
|
}
|
|
17505
17561
|
label;
|
|
17562
|
+
renderLine;
|
|
17506
17563
|
timer;
|
|
17507
17564
|
index = 0;
|
|
17508
17565
|
holdWhile;
|
|
@@ -17528,8 +17585,11 @@ var Spinner = class {
|
|
|
17528
17585
|
this.timer = void 0;
|
|
17529
17586
|
}
|
|
17530
17587
|
if (stdout4.isTTY && elapsed !== void 0) {
|
|
17531
|
-
|
|
17532
|
-
|
|
17588
|
+
if (this.renderLine) this.renderLine("");
|
|
17589
|
+
else {
|
|
17590
|
+
stdout4.clearLine(0);
|
|
17591
|
+
stdout4.cursorTo(0);
|
|
17592
|
+
}
|
|
17533
17593
|
if (completion && elapsed >= 1e3) stdout4.write(` ${completion} ${formatSeconds(elapsed)}.
|
|
17534
17594
|
`);
|
|
17535
17595
|
}
|
|
@@ -17537,16 +17597,21 @@ var Spinner = class {
|
|
|
17537
17597
|
render() {
|
|
17538
17598
|
if (this.holdWhile?.()) {
|
|
17539
17599
|
if (!this.held) {
|
|
17540
|
-
|
|
17541
|
-
|
|
17600
|
+
if (this.renderLine) this.renderLine("");
|
|
17601
|
+
else {
|
|
17602
|
+
stdout4.clearLine(0);
|
|
17603
|
+
stdout4.cursorTo(0);
|
|
17604
|
+
}
|
|
17542
17605
|
this.held = true;
|
|
17543
17606
|
}
|
|
17544
17607
|
return;
|
|
17545
17608
|
}
|
|
17546
17609
|
this.held = false;
|
|
17547
17610
|
this.index = (this.index + 1) % FRAMES.length;
|
|
17548
|
-
|
|
17549
|
-
|
|
17611
|
+
if (!this.renderLine) {
|
|
17612
|
+
stdout4.clearLine(0);
|
|
17613
|
+
stdout4.cursorTo(0);
|
|
17614
|
+
}
|
|
17550
17615
|
const milliseconds = performance.now() - (this.startedAt ?? performance.now());
|
|
17551
17616
|
const elapsed = milliseconds >= 1e3 ? formatElapsed(milliseconds) : "";
|
|
17552
17617
|
const limit = Math.max(0, (stdout4.columns ?? 80) - elapsed.length - 10);
|
|
@@ -17558,7 +17623,9 @@ var Spinner = class {
|
|
|
17558
17623
|
label += character;
|
|
17559
17624
|
cells2 += size;
|
|
17560
17625
|
}
|
|
17561
|
-
|
|
17626
|
+
const line = ` ${FRAMES[this.index]} ${label}\u2026${elapsed ? ` \xB7 ${elapsed}` : ""}`;
|
|
17627
|
+
if (this.renderLine) this.renderLine(line);
|
|
17628
|
+
else stdout4.write(line);
|
|
17562
17629
|
}
|
|
17563
17630
|
};
|
|
17564
17631
|
function formatSeconds(milliseconds) {
|
|
@@ -17569,6 +17636,13 @@ function formatSeconds(milliseconds) {
|
|
|
17569
17636
|
|
|
17570
17637
|
// src/ui/activity.ts
|
|
17571
17638
|
var sink;
|
|
17639
|
+
var workingSink;
|
|
17640
|
+
function setWorkingSink(value) {
|
|
17641
|
+
workingSink = value;
|
|
17642
|
+
}
|
|
17643
|
+
function getWorkingSink() {
|
|
17644
|
+
return workingSink;
|
|
17645
|
+
}
|
|
17572
17646
|
function setActivitySink(value) {
|
|
17573
17647
|
sink = value;
|
|
17574
17648
|
}
|
|
@@ -17592,10 +17666,16 @@ function activityEntry(call, result, durationMs) {
|
|
|
17592
17666
|
}
|
|
17593
17667
|
function activityRows(snapshot, expanded, selected, end) {
|
|
17594
17668
|
const failures = snapshot.entries.filter((entry) => !entry.ok).length;
|
|
17595
|
-
const elapsed = snapshot.thinkingMs >= 1e3 ? ` \xB7
|
|
17596
|
-
const summary = `${expanded ? "\u25BE" : "\u25B8"} \u25CF ${snapshot.entries.length}
|
|
17669
|
+
const elapsed = snapshot.thinkingMs >= 1e3 ? ` \xB7 Worked for ${formatSeconds(snapshot.thinkingMs)}` : "";
|
|
17670
|
+
const summary = `${expanded ? "\u25BE" : "\u25B8"} \u25CF ${snapshot.entries.length} ${snapshot.entries.length === 1 ? "tool call" : "tool calls"}${failures ? ` \xB7 ${failures} failed` : ""}${snapshot.finished ? elapsed : ""} \xB7 Ctrl+O details`;
|
|
17597
17671
|
const rows = [{ text: summary, ok: failures || snapshot.failed ? false : snapshot.finished ? true : void 0 }];
|
|
17598
|
-
if (!expanded)
|
|
17672
|
+
if (!expanded) {
|
|
17673
|
+
if (!snapshot.finished) for (let index = Math.max(0, snapshot.entries.length - 3); index < snapshot.entries.length; index += 1) {
|
|
17674
|
+
const entry = snapshot.entries[index];
|
|
17675
|
+
rows.push({ text: ` \u25CF ${entry.label}`, ok: entry.ok, entry: index });
|
|
17676
|
+
}
|
|
17677
|
+
return rows;
|
|
17678
|
+
}
|
|
17599
17679
|
const last = Math.min(snapshot.entries.length, end);
|
|
17600
17680
|
for (let index = Math.max(0, last - 5); index < last; index += 1) {
|
|
17601
17681
|
const entry = snapshot.entries[index];
|
|
@@ -17610,6 +17690,32 @@ function activityRows(snapshot, expanded, selected, end) {
|
|
|
17610
17690
|
return rows;
|
|
17611
17691
|
}
|
|
17612
17692
|
|
|
17693
|
+
// src/ui/task-progress.ts
|
|
17694
|
+
function startTaskProgress(options) {
|
|
17695
|
+
let lastMessage = performance.now();
|
|
17696
|
+
let lastState = "";
|
|
17697
|
+
let stopped = false;
|
|
17698
|
+
const timer = setInterval(() => {
|
|
17699
|
+
if (stopped || options.holdWhile?.()) return;
|
|
17700
|
+
const state = options.snapshot();
|
|
17701
|
+
if (!state.activeTool && !state.waitingForModel) return;
|
|
17702
|
+
const fingerprint = `${state.completed}:${state.failed}:${state.activeTool ?? "model"}`;
|
|
17703
|
+
if (performance.now() - lastMessage < (fingerprint === lastState ? 6e4 : 3e4)) return;
|
|
17704
|
+
const completed = state.completed ? `I have completed ${state.completed} tool ${state.completed === 1 ? "call" : "calls"}.${state.failed ? ` Of those, ${state.failed} failed.` : ""} ` : "";
|
|
17705
|
+
const current = state.activeTool ? `I am running the ${state.activeTool} tool.` : "I am waiting for the model's next response.";
|
|
17706
|
+
options.write(`${completed}${current}`);
|
|
17707
|
+
lastMessage = performance.now();
|
|
17708
|
+
lastState = fingerprint;
|
|
17709
|
+
}, 5e3);
|
|
17710
|
+
timer.unref?.();
|
|
17711
|
+
return { markMessage: () => {
|
|
17712
|
+
lastMessage = performance.now();
|
|
17713
|
+
}, stop: () => {
|
|
17714
|
+
stopped = true;
|
|
17715
|
+
clearInterval(timer);
|
|
17716
|
+
} };
|
|
17717
|
+
}
|
|
17718
|
+
|
|
17613
17719
|
// src/ui/interactive-choice.ts
|
|
17614
17720
|
var MOUSE_ON = "\x1B[?1006h\x1B[?1000h";
|
|
17615
17721
|
var MOUSE_OFF = "\x1B[?1000l\x1B[?1006l";
|
|
@@ -18018,9 +18124,8 @@ var TerminalLayout = class {
|
|
|
18018
18124
|
suggestions = [];
|
|
18019
18125
|
selected = 0;
|
|
18020
18126
|
activity;
|
|
18021
|
-
|
|
18022
|
-
|
|
18023
|
-
activityEnd = 0;
|
|
18127
|
+
liveActivity;
|
|
18128
|
+
completedActivities = [];
|
|
18024
18129
|
restoreActivityInput;
|
|
18025
18130
|
installed = false;
|
|
18026
18131
|
closed = false;
|
|
@@ -18030,6 +18135,7 @@ var TerminalLayout = class {
|
|
|
18030
18135
|
resize = () => {
|
|
18031
18136
|
if (this.closed || !this.enabled) return;
|
|
18032
18137
|
this.clearPreviousFooter();
|
|
18138
|
+
this.completedActivities = this.completedActivities.filter((item) => item.row >= 1 && item.row + item.height - 1 <= this.contentRows);
|
|
18033
18139
|
this.transcriptRow = Math.min(this.transcriptRow, this.contentRows);
|
|
18034
18140
|
this.transcriptColumn = Math.min(this.transcriptColumn, this.columns);
|
|
18035
18141
|
this.rawWrite(`${CSI}1;${this.contentRows}r`);
|
|
@@ -18059,12 +18165,8 @@ var TerminalLayout = class {
|
|
|
18059
18165
|
get suggestionRows() {
|
|
18060
18166
|
return Math.min(8, this.suggestions.length, Math.max(0, this.rows - BASE_FOOTER_ROWS - 2));
|
|
18061
18167
|
}
|
|
18062
|
-
get activityLines() {
|
|
18063
|
-
if (!this.activity) return [];
|
|
18064
|
-
return activityRows(this.activity, this.activityExpanded, this.activitySelected, this.activityEnd).slice(0, Math.max(1, Math.min(10, this.rows - BASE_FOOTER_ROWS - this.suggestionRows - 2)));
|
|
18065
|
-
}
|
|
18066
18168
|
get contentRows() {
|
|
18067
|
-
return this.rows - BASE_FOOTER_ROWS - this.suggestionRows
|
|
18169
|
+
return this.rows - BASE_FOOTER_ROWS - this.suggestionRows;
|
|
18068
18170
|
}
|
|
18069
18171
|
get inputRow() {
|
|
18070
18172
|
return this.rows - 2;
|
|
@@ -18094,7 +18196,7 @@ var TerminalLayout = class {
|
|
|
18094
18196
|
let sequence = "";
|
|
18095
18197
|
let sequenceTimer;
|
|
18096
18198
|
const wrapper = (text, key) => {
|
|
18097
|
-
if (this.activity && !this.suspended && key?.ctrl && key.name === "o") {
|
|
18199
|
+
if ((this.activity || this.completedActivities.length) && !this.suspended && key?.ctrl && key.name === "o") {
|
|
18098
18200
|
this.toggleActivity();
|
|
18099
18201
|
return;
|
|
18100
18202
|
}
|
|
@@ -18129,50 +18231,112 @@ var TerminalLayout = class {
|
|
|
18129
18231
|
this.paintFooter();
|
|
18130
18232
|
}
|
|
18131
18233
|
setActivity(snapshot) {
|
|
18132
|
-
this.clearPreviousFooter();
|
|
18133
|
-
const previous = this.activity?.entries.length ?? 0;
|
|
18134
|
-
if (!snapshot.entries.length && !snapshot.finished) {
|
|
18135
|
-
this.activityExpanded = false;
|
|
18136
|
-
this.activitySelected = -1;
|
|
18137
|
-
}
|
|
18138
18234
|
this.activity = snapshot;
|
|
18139
|
-
if (this.activityEnd >= previous || !previous) this.activityEnd = snapshot.entries.length;
|
|
18140
|
-
this.reflowActivity();
|
|
18141
|
-
}
|
|
18142
|
-
toggleActivity() {
|
|
18143
|
-
if (!this.activity) return;
|
|
18144
|
-
this.clearPreviousFooter();
|
|
18145
|
-
this.activityExpanded = !this.activityExpanded;
|
|
18146
|
-
this.reflowActivity();
|
|
18147
|
-
}
|
|
18148
|
-
reflowActivity() {
|
|
18149
18235
|
if (!this.enabled || !this.installed || this.closed) return;
|
|
18150
|
-
|
|
18151
|
-
|
|
18236
|
+
if (!this.liveActivity && !snapshot.finished) {
|
|
18237
|
+
this.writeTranscript(`
|
|
18238
|
+
${this.colorActivityRow(activityRows(snapshot, false, -1, snapshot.entries.length)[0])}
|
|
18239
|
+
`);
|
|
18240
|
+
this.liveActivity = { snapshot, row: this.transcriptRow - 1, height: 1, expanded: false, selected: -1, end: snapshot.entries.length };
|
|
18241
|
+
this.completedActivities.push(this.liveActivity);
|
|
18242
|
+
}
|
|
18243
|
+
const item = this.liveActivity;
|
|
18244
|
+
if (!item) return;
|
|
18245
|
+
const previous = item.snapshot.entries.length;
|
|
18246
|
+
item.snapshot = snapshot;
|
|
18247
|
+
if (item.end >= previous) item.end = snapshot.entries.length;
|
|
18248
|
+
if (!snapshot.finished) {
|
|
18249
|
+
this.redrawCompleted(item);
|
|
18250
|
+
return;
|
|
18251
|
+
}
|
|
18252
|
+
if (item.row >= 2) {
|
|
18253
|
+
const removed = item.height + 1;
|
|
18254
|
+
this.rawWrite(`${cup(item.row - 1)}${CSI}${removed}M`);
|
|
18255
|
+
if (this.transcriptRow > item.row) this.transcriptRow -= removed;
|
|
18256
|
+
for (const other of this.completedActivities) if (other !== item && other.row > item.row) other.row -= removed;
|
|
18257
|
+
}
|
|
18258
|
+
this.completedActivities = this.completedActivities.filter((entry) => entry !== item);
|
|
18259
|
+
this.liveActivity = void 0;
|
|
18260
|
+
this.writeTranscript(`${this.colorActivityRow(activityRows(snapshot, false, -1, snapshot.entries.length)[0])}
|
|
18261
|
+
`);
|
|
18262
|
+
this.completedActivities.push({ ...item, row: this.transcriptRow - 1, height: 1, expanded: false, selected: -1 });
|
|
18152
18263
|
this.paintFooter();
|
|
18153
18264
|
}
|
|
18265
|
+
setWorkingStatus(text) {
|
|
18266
|
+
const item = this.liveActivity;
|
|
18267
|
+
if (!item) return;
|
|
18268
|
+
if (item.row < 2 || item.row + item.height > this.contentRows) {
|
|
18269
|
+
if (item.row >= 1 && item.row <= this.contentRows) {
|
|
18270
|
+
this.rawWrite(`${cup(item.row)}${CSI}${item.height}M`);
|
|
18271
|
+
if (this.transcriptRow > item.row) this.transcriptRow -= item.height;
|
|
18272
|
+
for (const other of this.completedActivities) if (other !== item && other.row > item.row) other.row -= item.height;
|
|
18273
|
+
}
|
|
18274
|
+
this.completedActivities = this.completedActivities.filter((entry) => entry !== item);
|
|
18275
|
+
this.writeTranscript(`
|
|
18276
|
+
${this.colorActivityRow(activityRows(item.snapshot, false, -1, item.end)[0])}
|
|
18277
|
+
`);
|
|
18278
|
+
item.row = this.transcriptRow - 1;
|
|
18279
|
+
item.height = 1;
|
|
18280
|
+
item.expanded = false;
|
|
18281
|
+
this.completedActivities.push(item);
|
|
18282
|
+
}
|
|
18283
|
+
this.rawWrite(`${cup(item.row - 1)}${CSI}0m${CSI}2K${fit(text, this.columns)}`);
|
|
18284
|
+
this.placeInputCursor();
|
|
18285
|
+
}
|
|
18286
|
+
colorActivityRow(row) {
|
|
18287
|
+
const text = fit(row.text, this.columns);
|
|
18288
|
+
const color = process.env.NO_COLOR || row.ok === void 0 ? "" : `${CSI}${row.ok ? 32 : 31}m`;
|
|
18289
|
+
const dot = text.indexOf("\u25CF");
|
|
18290
|
+
return dot >= 0 && color ? `${text.slice(0, dot)}${color}\u25CF${CSI}39m${text.slice(dot + 1)}` : text;
|
|
18291
|
+
}
|
|
18292
|
+
shiftCompleted(rows) {
|
|
18293
|
+
for (const item of this.completedActivities) item.row -= rows;
|
|
18294
|
+
this.completedActivities = this.completedActivities.filter((item) => item.row >= 1);
|
|
18295
|
+
}
|
|
18296
|
+
redrawCompleted(item) {
|
|
18297
|
+
const rows = activityRows(item.snapshot, item.expanded, item.selected, item.end).slice(0, Math.max(1, this.contentRows - 2));
|
|
18298
|
+
const delta = rows.length - item.height;
|
|
18299
|
+
const scroll = Math.max(0, this.transcriptRow + delta - this.contentRows);
|
|
18300
|
+
if (scroll) {
|
|
18301
|
+
this.rawWrite(`${cup(this.contentRows)}${CSI}${scroll}S`);
|
|
18302
|
+
this.shiftCompleted(scroll);
|
|
18303
|
+
this.transcriptRow -= scroll;
|
|
18304
|
+
}
|
|
18305
|
+
if (item.row < 1) return;
|
|
18306
|
+
if (delta > 0) this.rawWrite(`${cup(item.row + item.height)}${CSI}${delta}L`);
|
|
18307
|
+
if (delta < 0) this.rawWrite(`${cup(item.row + rows.length)}${CSI}${-delta}M`);
|
|
18308
|
+
for (const other of this.completedActivities) if (other !== item && other.row > item.row) other.row += delta;
|
|
18309
|
+
if (this.transcriptRow > item.row) this.transcriptRow += delta;
|
|
18310
|
+
item.height = rows.length;
|
|
18311
|
+
rows.forEach((row, index) => this.rawWrite(`${cup(item.row + index)}${CSI}0m${CSI}2K${this.colorActivityRow(row)}`));
|
|
18312
|
+
this.paintFooter();
|
|
18313
|
+
}
|
|
18314
|
+
toggleActivity() {
|
|
18315
|
+
if (this.liveActivity && (this.liveActivity.row < 2 || this.liveActivity.row + this.liveActivity.height > this.contentRows)) this.setWorkingStatus("working\u2026");
|
|
18316
|
+
const item = this.liveActivity ?? this.completedActivities.at(-1);
|
|
18317
|
+
if (item) {
|
|
18318
|
+
item.expanded = !item.expanded;
|
|
18319
|
+
this.redrawCompleted(item);
|
|
18320
|
+
}
|
|
18321
|
+
}
|
|
18154
18322
|
activityMouse(button, row, press) {
|
|
18155
|
-
if (!
|
|
18156
|
-
|
|
18157
|
-
|
|
18158
|
-
|
|
18159
|
-
|
|
18160
|
-
|
|
18161
|
-
this.
|
|
18323
|
+
if (!press) return;
|
|
18324
|
+
const completed = this.completedActivities.find((item) => row >= item.row && row < item.row + item.height);
|
|
18325
|
+
if (completed && (button === 64 || button === 65)) {
|
|
18326
|
+
if (!completed.expanded) return;
|
|
18327
|
+
completed.end = Math.max(Math.min(5, completed.snapshot.entries.length), Math.min(completed.snapshot.entries.length, completed.end + (button === 64 ? -1 : 1)));
|
|
18328
|
+
completed.selected = -1;
|
|
18329
|
+
this.redrawCompleted(completed);
|
|
18162
18330
|
return;
|
|
18163
18331
|
}
|
|
18164
|
-
if (button
|
|
18165
|
-
|
|
18166
|
-
|
|
18167
|
-
|
|
18332
|
+
if (completed && button === 0) {
|
|
18333
|
+
const rows = activityRows(completed.snapshot, completed.expanded, completed.selected, completed.end);
|
|
18334
|
+
const entry = rows[row - completed.row]?.entry;
|
|
18335
|
+
if (row === completed.row) completed.expanded = !completed.expanded;
|
|
18336
|
+
else if (entry !== void 0) completed.selected = completed.selected === entry ? -1 : entry;
|
|
18337
|
+
this.redrawCompleted(completed);
|
|
18168
18338
|
return;
|
|
18169
18339
|
}
|
|
18170
|
-
const entry = this.activityLines[index]?.entry;
|
|
18171
|
-
if (entry !== void 0) {
|
|
18172
|
-
this.clearPreviousFooter();
|
|
18173
|
-
this.activitySelected = this.activitySelected === entry ? -1 : entry;
|
|
18174
|
-
this.reflowActivity();
|
|
18175
|
-
}
|
|
18176
18340
|
}
|
|
18177
18341
|
setInputFormatter(formatter) {
|
|
18178
18342
|
this.inputFormatter = formatter;
|
|
@@ -18238,6 +18402,7 @@ var TerminalLayout = class {
|
|
|
18238
18402
|
if (this.wrapPending && !/^[\r\n]/.test(stripVTControlCharacters2(formatted))) {
|
|
18239
18403
|
this.rawWrite(`${cup(this.transcriptRow, this.columns)}\r
|
|
18240
18404
|
`);
|
|
18405
|
+
if (this.transcriptRow === this.contentRows) this.shiftCompleted(1);
|
|
18241
18406
|
this.transcriptRow = Math.min(this.contentRows, this.transcriptRow + 1);
|
|
18242
18407
|
this.transcriptColumn = 1;
|
|
18243
18408
|
this.wrapPending = false;
|
|
@@ -18302,6 +18467,7 @@ var TerminalLayout = class {
|
|
|
18302
18467
|
this.wrapPending = true;
|
|
18303
18468
|
}
|
|
18304
18469
|
}
|
|
18470
|
+
if (this.transcriptRow > this.contentRows) this.shiftCompleted(this.transcriptRow - this.contentRows);
|
|
18305
18471
|
this.transcriptRow = Math.min(this.contentRows, this.transcriptRow);
|
|
18306
18472
|
}
|
|
18307
18473
|
}
|
|
@@ -18351,13 +18517,10 @@ var TerminalLayout = class {
|
|
|
18351
18517
|
const model = /^cosmic[- ]1$/i.test(this.details.model) ? "Cosmic 1" : this.details.model;
|
|
18352
18518
|
const metadata = fit(model, this.columns);
|
|
18353
18519
|
let frame = "";
|
|
18354
|
-
const activity =
|
|
18355
|
-
if (activity.length && !this.suspended) frame += `${CSI}?1006h${CSI}?1000h`;
|
|
18520
|
+
const activity = [];
|
|
18521
|
+
if ((activity.length || this.completedActivities.length) && !this.suspended) frame += `${CSI}?1006h${CSI}?1000h`;
|
|
18356
18522
|
activity.forEach((row, index) => {
|
|
18357
|
-
const
|
|
18358
|
-
const color = process.env.NO_COLOR || row.ok === void 0 ? "" : `${CSI}${row.ok ? 32 : 31}m`;
|
|
18359
|
-
const dot = text.indexOf("\u25CF");
|
|
18360
|
-
const rendered = dot >= 0 && color ? `${text.slice(0, dot)}${color}\u25CF${CSI}39m${text.slice(dot + 1)}` : text;
|
|
18523
|
+
const rendered = this.colorActivityRow(row);
|
|
18361
18524
|
frame += `${cup(this.contentRows + 1 + index)}${CSI}0m${CSI}2K${rendered}`;
|
|
18362
18525
|
});
|
|
18363
18526
|
const suggestions = this.suggestionRows;
|
|
@@ -18800,11 +18963,12 @@ Custom commands come from skills that declare a 'command:' key in their
|
|
|
18800
18963
|
frontmatter; built-ins always win a name clash. Type anything else to talk to Ultrac.`;
|
|
18801
18964
|
var EXIT_REPL_NOT_STARTED = 1;
|
|
18802
18965
|
function registerReplCommand(program2) {
|
|
18803
|
-
program2.command("interactive", { isDefault: true }).alias("repl").description("Start an interactive ultrac session").argument("[task...]", "Optional initial prompt to send on start").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 per turn").option("--allow-write", "Allow file writes without prompting", false).option("--allow-shell", "Grant broad, unsandboxed shell access without prompting", false).option("--allow-network", "Allow network access without prompting", false).option("--no-native-tools", "Use the JSON tool protocol instead of native tool calling").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)").action(async (taskParts, options, command) => {
|
|
18966
|
+
program2.command("interactive", { isDefault: true }).alias("repl").description("Start an interactive ultrac session").argument("[task...]", "Optional initial prompt to send on start").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 per turn").option("--permissions <mode>", "Tool permission preset: readonly, workspace, or full (no tool approval prompts)").option("--allow-write", "Allow file writes without prompting", false).option("--allow-shell", "Grant broad, unsandboxed shell access without prompting", false).option("--allow-network", "Allow network access without prompting", false).option("--no-native-tools", "Use the JSON tool protocol instead of native tool calling").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)").action(async (taskParts, options, command) => {
|
|
18804
18967
|
const task = (taskParts ?? []).join(" ").trim();
|
|
18805
18968
|
const invokedByName = namesReplCommand(command, command.parent?.args[0]);
|
|
18806
18969
|
const mistyped = invokedByName ? void 0 : mistypedCommand(task, knownCommandNames(program2));
|
|
18807
18970
|
if (mistyped) throw new Error(mistyped);
|
|
18971
|
+
if (options.permissions && !["readonly", "workspace", "full"].includes(options.permissions)) throw new Error("--permissions must be readonly, workspace, or full.");
|
|
18808
18972
|
await runRepl(options, task);
|
|
18809
18973
|
});
|
|
18810
18974
|
}
|
|
@@ -18912,7 +19076,7 @@ async function runRepl(options, initialTask = "") {
|
|
|
18912
19076
|
diagnosticEvent("provider.retry", event);
|
|
18913
19077
|
console.log(
|
|
18914
19078
|
chalk11.yellow(
|
|
18915
|
-
`
|
|
19079
|
+
` ${event.classification.reason} Retrying ${event.target.model} in ${(event.delayMs / 1e3).toFixed(1)} seconds (retry ${event.attempt}).`
|
|
18916
19080
|
)
|
|
18917
19081
|
);
|
|
18918
19082
|
}
|
|
@@ -18923,9 +19087,9 @@ async function runRepl(options, initialTask = "") {
|
|
|
18923
19087
|
return;
|
|
18924
19088
|
}
|
|
18925
19089
|
const basePermissions = {
|
|
18926
|
-
write: options.allowWrite || config.permissions.write,
|
|
18927
|
-
shell: options.allowShell || config.permissions.shell,
|
|
18928
|
-
network: options.allowNetwork || config.permissions.network
|
|
19090
|
+
write: options.permissions ? options.permissions !== "readonly" : options.allowWrite || config.permissions.write,
|
|
19091
|
+
shell: options.permissions ? options.permissions === "full" : options.allowShell || config.permissions.shell,
|
|
19092
|
+
network: options.permissions ? options.permissions === "full" : options.allowNetwork || config.permissions.network
|
|
18929
19093
|
};
|
|
18930
19094
|
const registry = createDefaultToolRegistry();
|
|
18931
19095
|
const mcpRuntime = await connectWorkspaceMcpServers(config.mcpServers, { workspace, registry });
|
|
@@ -18942,7 +19106,11 @@ async function runRepl(options, initialTask = "") {
|
|
|
18942
19106
|
...toolSelection ? { tools: toolSelection } : {}
|
|
18943
19107
|
};
|
|
18944
19108
|
setSubagentRuntime({ provider, model: init.model, mode: init.mode, registry });
|
|
18945
|
-
|
|
19109
|
+
const created = useNativeTools ? new NativeSession(opts) : new InteractiveSession(opts);
|
|
19110
|
+
if (options.permissions) for (const permission of ["write", "shell", "network"]) {
|
|
19111
|
+
created.setPermission(permission, init.permissions[permission]);
|
|
19112
|
+
}
|
|
19113
|
+
return created;
|
|
18946
19114
|
};
|
|
18947
19115
|
let session = createSession({
|
|
18948
19116
|
model,
|
|
@@ -18956,6 +19124,7 @@ async function runRepl(options, initialTask = "") {
|
|
|
18956
19124
|
details: { provider: provider.name, model: session.model, mode: session.mode, status: "Idle" }
|
|
18957
19125
|
}) : void 0;
|
|
18958
19126
|
setActivitySink(layout?.enabled ? (snapshot) => layout.setActivity(snapshot) : void 0);
|
|
19127
|
+
setWorkingSink(layout?.enabled ? (text) => layout.setWorkingStatus(text) : void 0);
|
|
18959
19128
|
const refreshDetails = () => {
|
|
18960
19129
|
if (!layout?.enabled) return;
|
|
18961
19130
|
const steps = session.toSession().steps;
|
|
@@ -19039,7 +19208,7 @@ async function runRepl(options, initialTask = "") {
|
|
|
19039
19208
|
}
|
|
19040
19209
|
};
|
|
19041
19210
|
const drainQueuedMessages = () => reader.drainQueuedMessages();
|
|
19042
|
-
let autonomousRounds =
|
|
19211
|
+
let autonomousRounds = 5;
|
|
19043
19212
|
setAskUserRuntime(async (text, options2, signal) => {
|
|
19044
19213
|
console.log(chalk11.white(`
|
|
19045
19214
|
? ${text}`));
|
|
@@ -19196,6 +19365,7 @@ async function runRepl(options, initialTask = "") {
|
|
|
19196
19365
|
if (stdin5.isTTY) stdin5.off("keypress", onKeypress);
|
|
19197
19366
|
setAskUserRuntime(void 0);
|
|
19198
19367
|
setActivitySink(void 0);
|
|
19368
|
+
setWorkingSink(void 0);
|
|
19199
19369
|
rl.close();
|
|
19200
19370
|
try {
|
|
19201
19371
|
await saveTranscript(session, writeCheckpoint);
|
|
@@ -19206,16 +19376,22 @@ async function runRepl(options, initialTask = "") {
|
|
|
19206
19376
|
}
|
|
19207
19377
|
console.log(chalk11.gray("Goodbye."));
|
|
19208
19378
|
}
|
|
19209
|
-
async function runTurn(session, input, question, signal, drainQueuedMessages, persist, isComposing, allowImageDrop = true) {
|
|
19379
|
+
async function runTurn(session, input, question, signal, drainQueuedMessages, persist, isComposing, allowImageDrop = true, activityState) {
|
|
19210
19380
|
const activity = isGhostEnabled(session) ? void 0 : getActivitySink();
|
|
19211
|
-
const entries = [];
|
|
19212
|
-
let thinkingMs = 0;
|
|
19381
|
+
const entries = activityState?.entries ?? [];
|
|
19382
|
+
let thinkingMs = activityState?.thinkingMs ?? 0;
|
|
19213
19383
|
let thinkingStarted;
|
|
19214
19384
|
let turnFailed = false;
|
|
19215
|
-
const refreshActivity = (finished = false) =>
|
|
19385
|
+
const refreshActivity = (finished = false) => {
|
|
19386
|
+
if (activityState) {
|
|
19387
|
+
activityState.thinkingMs = thinkingMs;
|
|
19388
|
+
activityState.failed ||= turnFailed;
|
|
19389
|
+
}
|
|
19390
|
+
activity?.({ entries: [...entries], thinkingMs, finished: finished && !activityState, failed: turnFailed });
|
|
19391
|
+
};
|
|
19216
19392
|
refreshActivity();
|
|
19217
|
-
const spinner = new Spinner();
|
|
19218
|
-
const workSpinner = new Spinner("working");
|
|
19393
|
+
const spinner = new Spinner("working", activity ? getWorkingSink() : void 0);
|
|
19394
|
+
const workSpinner = new Spinner("working", activity ? getWorkingSink() : void 0);
|
|
19219
19395
|
const activeTools = /* @__PURE__ */ new Map();
|
|
19220
19396
|
let permissionPrompts = 0;
|
|
19221
19397
|
const renderWorking = () => {
|
|
@@ -19229,6 +19405,17 @@ async function runTurn(session, input, question, signal, drainQueuedMessages, pe
|
|
|
19229
19405
|
const detachProgress = quiet ? () => {
|
|
19230
19406
|
} : attachProgressUi();
|
|
19231
19407
|
let deltaChars = 0;
|
|
19408
|
+
const progress2 = quiet ? void 0 : startTaskProgress({
|
|
19409
|
+
snapshot: () => ({
|
|
19410
|
+
completed: entries.length,
|
|
19411
|
+
failed: entries.filter((entry) => !entry.ok).length,
|
|
19412
|
+
activeTool: [...activeTools.keys()].at(-1)?.tool,
|
|
19413
|
+
waitingForModel: thinkingStarted !== void 0
|
|
19414
|
+
}),
|
|
19415
|
+
holdWhile: () => permissionPrompts > 0 || [...activeTools.keys()].some((call) => call.tool === "ask_user"),
|
|
19416
|
+
write: (text) => console.log(`
|
|
19417
|
+
${text}`)
|
|
19418
|
+
});
|
|
19232
19419
|
try {
|
|
19233
19420
|
const prepared = allowImageDrop ? await prepareImageInput(input) : { text: input };
|
|
19234
19421
|
await session.ask(prepared.text, {
|
|
@@ -19247,22 +19434,23 @@ async function runTurn(session, input, question, signal, drainQueuedMessages, pe
|
|
|
19247
19434
|
deltaChars = 0;
|
|
19248
19435
|
thinkingStarted = performance.now();
|
|
19249
19436
|
if (!quiet) {
|
|
19250
|
-
spinner.update("
|
|
19251
|
-
spinner.start(isComposing);
|
|
19437
|
+
spinner.update("working");
|
|
19438
|
+
spinner.start(isComposing, performance.now() - thinkingMs);
|
|
19252
19439
|
}
|
|
19253
19440
|
},
|
|
19254
19441
|
onModelDelta: (text) => {
|
|
19255
19442
|
deltaChars += text.length;
|
|
19256
|
-
if (!quiet) spinner.update(`
|
|
19443
|
+
if (!quiet) spinner.update(`working (${deltaChars} chars)`);
|
|
19257
19444
|
},
|
|
19258
19445
|
onModelSettled: () => {
|
|
19259
19446
|
if (thinkingStarted !== void 0) thinkingMs += performance.now() - thinkingStarted;
|
|
19260
19447
|
thinkingStarted = void 0;
|
|
19261
|
-
spinner.stop(activity ? void 0 : signal?.aborted ? "
|
|
19448
|
+
spinner.stop(activity ? void 0 : signal?.aborted ? "Work stopped after" : "Worked for");
|
|
19262
19449
|
refreshActivity();
|
|
19263
19450
|
},
|
|
19264
19451
|
onAssistantMessage: (message) => {
|
|
19265
19452
|
if (message.trim()) {
|
|
19453
|
+
progress2?.markMessage();
|
|
19266
19454
|
console.log(`
|
|
19267
19455
|
${message.trim()}`);
|
|
19268
19456
|
}
|
|
@@ -19300,7 +19488,7 @@ ${message.trim()}`);
|
|
|
19300
19488
|
console.log(chalk11.yellow(` ! ${notice}`));
|
|
19301
19489
|
},
|
|
19302
19490
|
onStopped: (reason) => {
|
|
19303
|
-
if (reason === "aborted") return;
|
|
19491
|
+
if (reason === "aborted" || reason === "max-steps" && activityState?.continuing) return;
|
|
19304
19492
|
const line = stopReasonLine(reason);
|
|
19305
19493
|
const hint = turnStopHint(reason, session.maxSteps);
|
|
19306
19494
|
if (!line || !hint) return;
|
|
@@ -19312,13 +19500,25 @@ ${message.trim()}`);
|
|
|
19312
19500
|
permissionPrompts += 1;
|
|
19313
19501
|
workSpinner.stop();
|
|
19314
19502
|
try {
|
|
19315
|
-
const prompt = permission === "shell" && call.tool === "run_shell" ? shellApprovalPrompt(call, session.toSession().workspace) : permission === "shell" ? ` ${call.tool} wants shell access.
|
|
19316
|
-
|
|
19503
|
+
const prompt = permission === "shell" && call.tool === "run_shell" ? shellApprovalPrompt(call, session.toSession().workspace) : permission === "shell" ? ` ${call.tool} wants shell access.
|
|
19504
|
+
1. [o]nce (this call only)
|
|
19505
|
+
2. [a]lways (this session)
|
|
19506
|
+
3. [n]o (this call)
|
|
19507
|
+
4. n[e]ver (this session)
|
|
19508
|
+
Answer: ` : ` ${call.tool} wants ${permission} access.
|
|
19509
|
+
1. [o]nce
|
|
19510
|
+
2. [a]lways (this session)
|
|
19511
|
+
3. [n]o
|
|
19512
|
+
4. n[e]ver (this session)
|
|
19513
|
+
Answer: `;
|
|
19514
|
+
const choices = ["Allow once", "Allow for this session", "Deny this call", "Never allow this permission this session"];
|
|
19515
|
+
const asker = getAskUserRuntime();
|
|
19516
|
+
const rawAnswer = asker ? await asker(prompt.split("\n").slice(0, -5).join("\n"), choices, signal) : await question(chalk11.yellow(prompt), signal);
|
|
19317
19517
|
if (signal?.aborted || rawAnswer === null) return "deny";
|
|
19318
19518
|
const answer = rawAnswer.trim().toLowerCase();
|
|
19319
|
-
if (answer === "a" || answer === "always") return "always";
|
|
19320
|
-
if (answer === "o" || answer === "once" || answer === "y" || answer === "yes") return "once";
|
|
19321
|
-
if (answer === "e" || answer === "never") {
|
|
19519
|
+
if (answer === "a" || answer === "always" || answer === "2" || answer === choices[1].toLowerCase()) return "always";
|
|
19520
|
+
if (answer === "o" || answer === "once" || answer === "y" || answer === "yes" || answer === "1" || answer === choices[0].toLowerCase()) return "once";
|
|
19521
|
+
if (answer === "e" || answer === "never" || answer === "4" || answer === choices[3].toLowerCase()) {
|
|
19322
19522
|
console.log(
|
|
19323
19523
|
chalk11.gray(
|
|
19324
19524
|
` ${permission} refused for the rest of the session \u2014 nothing will ask again. /allow ${permission} undoes it.`
|
|
@@ -19345,19 +19545,21 @@ ${message.trim()}`);
|
|
|
19345
19545
|
activeTools.clear();
|
|
19346
19546
|
if (thinkingStarted !== void 0) thinkingMs += performance.now() - thinkingStarted;
|
|
19347
19547
|
refreshActivity(true);
|
|
19548
|
+
progress2?.stop();
|
|
19348
19549
|
detachProgress();
|
|
19349
19550
|
}
|
|
19350
19551
|
}
|
|
19351
19552
|
async function runAndCheckpointTurn(session, input, question, signal, persist, drainQueuedMessages, isComposing, maxRounds = 1, allowImageDrop = true) {
|
|
19553
|
+
const activityState = { entries: [], thinkingMs: 0, continuing: maxRounds > 1 };
|
|
19352
19554
|
try {
|
|
19353
|
-
if (maxRounds === 1) return await runTurn(session, input, question, signal, drainQueuedMessages, persist, isComposing, allowImageDrop);
|
|
19555
|
+
if (maxRounds === 1) return await runTurn(session, input, question, signal, drainQueuedMessages, persist, isComposing, allowImageDrop, activityState);
|
|
19354
19556
|
let successful = true;
|
|
19355
19557
|
const result = await runAutonomousLoop({
|
|
19356
19558
|
maxRounds,
|
|
19357
19559
|
signal,
|
|
19358
19560
|
classifyFailure: () => ({ kind: "unknown", retryable: false, failoverable: false, reason: "Interactive provider retries already exhausted." }),
|
|
19359
19561
|
runRound: async (round, _previous, roundSignal) => {
|
|
19360
|
-
successful = await runTurn(session, round === 1 ? input : AUTONOMOUS_CONTINUE_TASK, question, roundSignal, drainQueuedMessages, persist, isComposing, allowImageDrop);
|
|
19562
|
+
successful = await runTurn(session, round === 1 ? input : AUTONOMOUS_CONTINUE_TASK, question, roundSignal, drainQueuedMessages, persist, isComposing, allowImageDrop, activityState);
|
|
19361
19563
|
await persist(session);
|
|
19362
19564
|
const snapshot = session.toSession();
|
|
19363
19565
|
return successful ? snapshot : { ...snapshot, stopReason: roundSignal.aborted ? "aborted" : "error" };
|
|
@@ -19369,6 +19571,7 @@ async function runAndCheckpointTurn(session, input, question, signal, persist, d
|
|
|
19369
19571
|
if (result.haltReason !== "completed" && result.haltReason !== "aborted") console.log(chalk11.gray(`Automatic continuation stopped: ${result.haltReason}. Session preserved.`));
|
|
19370
19572
|
return successful;
|
|
19371
19573
|
} finally {
|
|
19574
|
+
if (!isGhostEnabled(session)) getActivitySink()?.({ ...activityState, entries: [...activityState.entries], finished: true });
|
|
19372
19575
|
await persist(session);
|
|
19373
19576
|
}
|
|
19374
19577
|
}
|
|
@@ -19628,7 +19831,7 @@ ${extra}`);
|
|
|
19628
19831
|
for (const permission of ["write", "shell", "network"]) {
|
|
19629
19832
|
session.setPermission(permission, argument === "full" || argument === "workspace" && permission === "write");
|
|
19630
19833
|
}
|
|
19631
|
-
console.log("Permissions updated.
|
|
19834
|
+
console.log(argument === "full" ? "Full access: file, shell, and network tools run without approval prompts for this session." : "Permissions updated. Ungranted tools are denied without prompting; /allow restores access.");
|
|
19632
19835
|
}
|
|
19633
19836
|
const p = session.currentPermissions;
|
|
19634
19837
|
console.log(`write: ${p.write} shell: ${p.shell} network: ${p.network}`);
|
|
@@ -19919,11 +20122,16 @@ function shellApprovalPrompt(call, workspace) {
|
|
|
19919
20122
|
const timeoutMs = typeof call.input.timeoutMs === "number" ? call.input.timeoutMs : 3e4;
|
|
19920
20123
|
return [
|
|
19921
20124
|
" run_shell wants shell access.",
|
|
19922
|
-
"
|
|
20125
|
+
" Runs unsandboxed as your user.",
|
|
20126
|
+
" Can read or write your files and use your network connection.",
|
|
19923
20127
|
` Command: ${JSON.stringify(command)}`,
|
|
19924
20128
|
` Resolved cwd: ${JSON.stringify(resolvedCwd)}`,
|
|
19925
20129
|
` Timeout: ${timeoutMs.toLocaleString("en-US")} ms`,
|
|
19926
|
-
" [o]nce runs only this command
|
|
20130
|
+
" 1. [o]nce runs only this command",
|
|
20131
|
+
" 2. [a]lways grants shell access for this whole session, including other shell-using tools",
|
|
20132
|
+
" 3. [n]o (this call)",
|
|
20133
|
+
" 4. n[e]ver (this session)",
|
|
20134
|
+
" Answer: "
|
|
19927
20135
|
].join("\n");
|
|
19928
20136
|
}
|
|
19929
20137
|
function indent3(value) {
|
|
@@ -19943,7 +20151,7 @@ function parsePositiveInt(value, flag) {
|
|
|
19943
20151
|
var EXIT_INCOMPLETE_RUN = 2;
|
|
19944
20152
|
var NOTIFY_SUMMARY_CHARS = 1200;
|
|
19945
20153
|
function registerRunCommand(program2) {
|
|
19946
|
-
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(
|
|
20154
|
+
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("--permissions <mode>", "Tool permission preset: readonly, workspace, or full").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(
|
|
19947
20155
|
"--allow-incomplete",
|
|
19948
20156
|
`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.`,
|
|
19949
20157
|
false
|
|
@@ -20060,7 +20268,8 @@ function registerRunCommand(program2) {
|
|
|
20060
20268
|
const abortRun = () => controller.abort();
|
|
20061
20269
|
process.on("SIGINT", abortRun);
|
|
20062
20270
|
process.on("SIGTERM", abortRun);
|
|
20063
|
-
|
|
20271
|
+
if (options.permissions && !["readonly", "workspace", "full"].includes(options.permissions)) throw new Error("--permissions must be readonly, workspace, or full.");
|
|
20272
|
+
const permissions = options.permissions ? { write: options.permissions !== "readonly", shell: options.permissions === "full", network: options.permissions === "full" } : autonomous ? { write: true, shell: true, network: true } : {
|
|
20064
20273
|
write: options.allowWrite || config.permissions.write,
|
|
20065
20274
|
shell: options.allowShell || config.permissions.shell,
|
|
20066
20275
|
network: options.allowNetwork || config.permissions.network
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spinalfluid/ultrac",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.18",
|
|
4
4
|
"license": "SEE LICENSE IN LICENSE",
|
|
5
5
|
"description": "Ultrac terminal agent harness for Cosmic 1, with local tools, browser sign-in, streamed answers, and resumable sessions.",
|
|
6
6
|
"author": "Aditya Pundir",
|