@spinalfluid/ultrac 0.1.6 → 0.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +61 -6
- package/dist/cli.js +675 -110
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -548,6 +548,9 @@ function ensurePrivateDirectory(path) {
|
|
|
548
548
|
}
|
|
549
549
|
chmodSync(path, 448);
|
|
550
550
|
}
|
|
551
|
+
function currentDiagnosticLogPath() {
|
|
552
|
+
return diagnosticPath;
|
|
553
|
+
}
|
|
551
554
|
function diagnosticEvent(event, fields = {}) {
|
|
552
555
|
if (!diagnosticPath) return;
|
|
553
556
|
const record3 = {
|
|
@@ -8040,7 +8043,9 @@ async function createSkill(workspace, input) {
|
|
|
8040
8043
|
name,
|
|
8041
8044
|
description: input.description.trim(),
|
|
8042
8045
|
...input.whenToUse ? { when_to_use: input.whenToUse.trim() } : {},
|
|
8043
|
-
...permissions.length ? { permissions } : {}
|
|
8046
|
+
...permissions.length ? { permissions } : {},
|
|
8047
|
+
...input.command ? { command: input.command } : {},
|
|
8048
|
+
...input.argumentHint ? { "argument-hint": input.argumentHint } : {}
|
|
8044
8049
|
},
|
|
8045
8050
|
input.body?.trim() || `# ${name}
|
|
8046
8051
|
|
|
@@ -8057,7 +8062,8 @@ Describe the steps the agent should follow when this skill is invoked.`
|
|
|
8057
8062
|
whenToUse: input.whenToUse?.trim(),
|
|
8058
8063
|
permissions,
|
|
8059
8064
|
body: document,
|
|
8060
|
-
path
|
|
8065
|
+
path,
|
|
8066
|
+
...input.command ? { command: { raw: input.command, argumentHint: input.argumentHint } } : {}
|
|
8061
8067
|
};
|
|
8062
8068
|
}
|
|
8063
8069
|
async function loadSkillsContext(workspace) {
|
|
@@ -8831,6 +8837,124 @@ import { z as z25 } from "zod";
|
|
|
8831
8837
|
import { mkdir as mkdir11, writeFile as writeFile8 } from "fs/promises";
|
|
8832
8838
|
import { join as join9 } from "path";
|
|
8833
8839
|
|
|
8840
|
+
// src/core/periodic-compaction.ts
|
|
8841
|
+
var DEFAULT_SUMMARY_INTERVAL_STEPS = 12;
|
|
8842
|
+
var MIN_PERIODIC_HISTORY_CHARS = 12e3;
|
|
8843
|
+
function summaryIntervalSteps(value = process.env.ULTRAC_SUMMARY_INTERVAL_STEPS) {
|
|
8844
|
+
if (value === void 0 || value.trim() === "") return DEFAULT_SUMMARY_INTERVAL_STEPS;
|
|
8845
|
+
if (!/^\d+$/.test(value) || !Number.isSafeInteger(Number(value)) || Number(value) > 1e3) throw new Error("ULTRAC_SUMMARY_INTERVAL_STEPS must be an integer from 0 to 1000.");
|
|
8846
|
+
return Number(value);
|
|
8847
|
+
}
|
|
8848
|
+
function completeContextUnits(messages) {
|
|
8849
|
+
const units = [];
|
|
8850
|
+
for (const message of messages) {
|
|
8851
|
+
if (message.role === "tool") {
|
|
8852
|
+
const previous = units.at(-1);
|
|
8853
|
+
if (!previous || previous[0].role !== "assistant") return null;
|
|
8854
|
+
previous.push(message);
|
|
8855
|
+
} else units.push([message]);
|
|
8856
|
+
}
|
|
8857
|
+
for (const unit of units) {
|
|
8858
|
+
const first = unit[0];
|
|
8859
|
+
if (first.role !== "assistant") continue;
|
|
8860
|
+
if (!first.toolCalls?.length) {
|
|
8861
|
+
if (unit.slice(1).some((message) => "toolCallId" in message)) return null;
|
|
8862
|
+
continue;
|
|
8863
|
+
}
|
|
8864
|
+
const ids = new Set(first.toolCalls.map((call) => call.id));
|
|
8865
|
+
const results = unit.slice(1).map((message) => message.toolCallId);
|
|
8866
|
+
if (ids.size !== first.toolCalls.length || results.length !== ids.size || results.some((id) => !id || !ids.has(id)) || new Set(results).size !== ids.size) return null;
|
|
8867
|
+
}
|
|
8868
|
+
return units;
|
|
8869
|
+
}
|
|
8870
|
+
var PeriodicCompactor = class {
|
|
8871
|
+
constructor(options = {}) {
|
|
8872
|
+
this.options = options;
|
|
8873
|
+
}
|
|
8874
|
+
options;
|
|
8875
|
+
lastAttemptStep = 0;
|
|
8876
|
+
async compact(messages, pinned, provider, model, completedSteps, signal) {
|
|
8877
|
+
const unchanged = () => ({ messages: [...messages], compacted: false });
|
|
8878
|
+
const interval = this.options.intervalSteps ?? summaryIntervalSteps();
|
|
8879
|
+
if (!interval || signal?.aborted || completedSteps - this.lastAttemptStep < interval) return unchanged();
|
|
8880
|
+
const units = completeContextUnits(messages);
|
|
8881
|
+
if (!units) return unchanged();
|
|
8882
|
+
const recent = new Set(units.filter((unit) => unit[0].role === "assistant").slice(-2));
|
|
8883
|
+
const pinnedSet = new Set(pinned);
|
|
8884
|
+
const history = units.filter((unit) => !recent.has(unit) && !unit.some((message) => message.role === "user" || message.role === "system" || pinnedSet.has(message) || isCompactionSummary(message))).flat();
|
|
8885
|
+
const removedChars = history.reduce((size, message) => size + JSON.stringify(message).length, 0);
|
|
8886
|
+
if (history.length < 2 || removedChars < (this.options.minHistoryChars ?? MIN_PERIODIC_HISTORY_CHARS)) return unchanged();
|
|
8887
|
+
this.lastAttemptStep = completedSteps;
|
|
8888
|
+
const trace = startDiagnosticSpan("context.compaction", { path: "periodic", completedSteps, compactedMessageCount: history.length }, 5e3);
|
|
8889
|
+
const controller = new AbortController();
|
|
8890
|
+
let rejectCancelled;
|
|
8891
|
+
const interrupted2 = new Promise((_resolve, reject) => {
|
|
8892
|
+
rejectCancelled = reject;
|
|
8893
|
+
});
|
|
8894
|
+
const abort = () => {
|
|
8895
|
+
controller.abort();
|
|
8896
|
+
rejectCancelled(new Error("summary_interrupted"));
|
|
8897
|
+
};
|
|
8898
|
+
const timer = setTimeout(abort, this.options.timeoutMs ?? DEFAULT_SUMMARY_TIMEOUT_MS);
|
|
8899
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
8900
|
+
if (signal?.aborted) abort();
|
|
8901
|
+
try {
|
|
8902
|
+
const completion = await Promise.race([provider.complete({ model, system: summaryInstructions(DEFAULT_MAX_SUMMARY_CHARS), messages: [{ role: "user", content: renderDroppedTranscript(history, { maxTranscriptChars: DEFAULT_MAX_TRANSCRIPT_CHARS }) }], signal: controller.signal, context: { purpose: "compaction_summary", compactedMessages: history.length } }), interrupted2]);
|
|
8903
|
+
if (controller.signal.aborted) {
|
|
8904
|
+
trace.end({ outcome: "skipped", reason: "interrupted" });
|
|
8905
|
+
return unchanged();
|
|
8906
|
+
}
|
|
8907
|
+
const summary = completion.text.trim();
|
|
8908
|
+
if (!summary || summary.length > DEFAULT_MAX_SUMMARY_CHARS || summary.length + 300 >= removedChars) {
|
|
8909
|
+
trace.end({ outcome: "skipped", reason: "invalid-summary" });
|
|
8910
|
+
return unchanged();
|
|
8911
|
+
}
|
|
8912
|
+
const removed = new Set(history);
|
|
8913
|
+
const compacted = messages.filter((message) => !removed.has(message));
|
|
8914
|
+
const firstRemoved = messages.findIndex((message) => removed.has(message));
|
|
8915
|
+
const insertion = messages.slice(0, firstRemoved).filter((message) => !removed.has(message)).length;
|
|
8916
|
+
compacted.splice(insertion, 0, compactionSummaryMessage(summary));
|
|
8917
|
+
if (!completeContextUnits(compacted)) {
|
|
8918
|
+
trace.end({ outcome: "skipped", reason: "invalid-pairing" });
|
|
8919
|
+
return unchanged();
|
|
8920
|
+
}
|
|
8921
|
+
trace.end({ outcome: "compacted", summaryChars: summary.length });
|
|
8922
|
+
return { messages: compacted, compacted: true };
|
|
8923
|
+
} catch {
|
|
8924
|
+
trace.end({ outcome: "failed", reason: controller.signal.aborted ? "interrupted" : "summary-failed" });
|
|
8925
|
+
return unchanged();
|
|
8926
|
+
} finally {
|
|
8927
|
+
clearTimeout(timer);
|
|
8928
|
+
signal?.removeEventListener("abort", abort);
|
|
8929
|
+
}
|
|
8930
|
+
}
|
|
8931
|
+
};
|
|
8932
|
+
function snapshotMessages(messages) {
|
|
8933
|
+
const result = [];
|
|
8934
|
+
for (let index = 0; index < messages.length; index += 1) {
|
|
8935
|
+
const message = messages[index];
|
|
8936
|
+
const native = message;
|
|
8937
|
+
if (native.role !== "assistant" || !native.toolCalls?.length) {
|
|
8938
|
+
result.push(message);
|
|
8939
|
+
continue;
|
|
8940
|
+
}
|
|
8941
|
+
const replies = [];
|
|
8942
|
+
while (messages[index + 1]?.role === "tool") replies.push(messages[++index]);
|
|
8943
|
+
const ids = new Set(replies.map((reply) => reply.toolCallId));
|
|
8944
|
+
const calls = native.toolCalls.filter((call) => ids.has(call.id));
|
|
8945
|
+
const completedIds = new Set(calls.map((call) => call.id));
|
|
8946
|
+
const base = { role: native.role, content: native.content };
|
|
8947
|
+
result.push({ ...base, ...calls.length ? { toolCalls: calls } : {} });
|
|
8948
|
+
result.push(...replies.filter((reply) => completedIds.has(reply.toolCallId)));
|
|
8949
|
+
}
|
|
8950
|
+
return result;
|
|
8951
|
+
}
|
|
8952
|
+
function snapshotWithConstraints(messages, constraints) {
|
|
8953
|
+
const snapshot = snapshotMessages(messages);
|
|
8954
|
+
for (const constraint of constraints) if (!snapshot.some((message) => message.role === "user" && message.content === constraint.content)) snapshot.push({ role: "user", content: constraint.content });
|
|
8955
|
+
return snapshot;
|
|
8956
|
+
}
|
|
8957
|
+
|
|
8834
8958
|
// src/core/progress-events.ts
|
|
8835
8959
|
var listeners = /* @__PURE__ */ new Set();
|
|
8836
8960
|
function onAgentProgress(listener) {
|
|
@@ -8997,18 +9121,18 @@ function isConcurrencySafe(tool) {
|
|
|
8997
9121
|
}
|
|
8998
9122
|
function planToolBatches(calls, tools) {
|
|
8999
9123
|
const batches = [];
|
|
9000
|
-
let
|
|
9124
|
+
let open9;
|
|
9001
9125
|
for (const call of calls) {
|
|
9002
9126
|
if (!isConcurrencySafe(tools.get(call.tool))) {
|
|
9003
9127
|
batches.push([call]);
|
|
9004
|
-
|
|
9128
|
+
open9 = void 0;
|
|
9005
9129
|
continue;
|
|
9006
9130
|
}
|
|
9007
|
-
if (!
|
|
9008
|
-
|
|
9009
|
-
batches.push(
|
|
9131
|
+
if (!open9) {
|
|
9132
|
+
open9 = [];
|
|
9133
|
+
batches.push(open9);
|
|
9010
9134
|
}
|
|
9011
|
-
|
|
9135
|
+
open9.push(call);
|
|
9012
9136
|
}
|
|
9013
9137
|
return batches;
|
|
9014
9138
|
}
|
|
@@ -9546,6 +9670,17 @@ function readSessionUserConstraints(session) {
|
|
|
9546
9670
|
}
|
|
9547
9671
|
return constraints;
|
|
9548
9672
|
}
|
|
9673
|
+
function readSessionContextSnapshot(session) {
|
|
9674
|
+
if (session.contextSnapshot !== void 0) {
|
|
9675
|
+
const snapshot = session.contextSnapshot;
|
|
9676
|
+
if (!snapshot || typeof snapshot !== "object" || snapshot.version !== 1 || !["native", "text", "run"].includes(snapshot.kind) || !Number.isSafeInteger(snapshot.completedSteps) || snapshot.completedSteps < 0 || !Array.isArray(snapshot.messages) || snapshot.messages.some((message) => {
|
|
9677
|
+
if (!message || typeof message !== "object" || !["user", "assistant", "system", "tool"].includes(message.role) || typeof message.content !== "string") return true;
|
|
9678
|
+
const native = message;
|
|
9679
|
+
return snapshot.kind !== "native" && (message.role === "system" || native.toolCalls !== void 0 || native.toolCallId !== void 0) || native.toolCalls !== void 0 && message.role !== "assistant" || native.toolCallId !== void 0 && message.role !== "tool" || snapshot.kind === "native" && message.role === "tool" && typeof native.toolCallId !== "string" || native.toolCallId !== void 0 && typeof native.toolCallId !== "string" || native.toolCalls !== void 0 && (!Array.isArray(native.toolCalls) || native.toolCalls.some((call) => !call || typeof call.id !== "string" || typeof call.name !== "string" || typeof call.arguments !== "string"));
|
|
9680
|
+
}) || !completeContextUnits(snapshot.messages) || (readSessionUserConstraints(session) ?? [{ content: session.task }]).some((constraint) => !snapshot.messages.some((message) => message.role === "user" && message.content === constraint.content))) throw new Error(`Session ${session.id} has an invalid saved context snapshot and cannot be continued safely.`);
|
|
9681
|
+
}
|
|
9682
|
+
return session.contextSnapshot;
|
|
9683
|
+
}
|
|
9549
9684
|
function createSessionId(date = /* @__PURE__ */ new Date()) {
|
|
9550
9685
|
const stamp = date.toISOString().replace(/[:.]/g, "-");
|
|
9551
9686
|
return `${stamp}-${randomBytes3(4).toString("hex")}`;
|
|
@@ -9823,6 +9958,7 @@ function validateSession(value, expectedId, label) {
|
|
|
9823
9958
|
if (!isSafeSessionId(sessionId) || sessionId !== expectedId || session.parentSessionId !== void 0 && (typeof session.parentSessionId !== "string" || !isSafeSessionId(session.parentSessionId) || normalizedSessionId(session.parentSessionId) !== session.parentSessionId) || typeof session.task !== "string" || typeof session.workspace !== "string" || typeof session.provider !== "string" || typeof session.model !== "string" || typeof session.startedAt !== "string" || typeof session.endedAt !== "string" || typeof session.finalMessage !== "string" || !isToolPermissions(session.permissions) || !Array.isArray(session.steps) || session.mcpResultArchiveVersion !== void 0 && session.mcpResultArchiveVersion !== 1 || session.mode !== void 0 && !isAgentModeName2(session.mode) || session.stopReason !== void 0 && !isStopReason(session.stopReason) || session.subagents !== void 0 && !Array.isArray(session.subagents)) {
|
|
9824
9959
|
invalid();
|
|
9825
9960
|
}
|
|
9961
|
+
readSessionContextSnapshot(value);
|
|
9826
9962
|
const steps = session.steps;
|
|
9827
9963
|
for (const stepValue of steps) {
|
|
9828
9964
|
if (!stepValue || typeof stepValue !== "object" || Array.isArray(stepValue)) invalid();
|
|
@@ -9933,6 +10069,7 @@ var AgentRunner = class {
|
|
|
9933
10069
|
];
|
|
9934
10070
|
const pinnedMessages = [...constraintMessages];
|
|
9935
10071
|
const steps = [];
|
|
10072
|
+
const periodicCompactor = new PeriodicCompactor();
|
|
9936
10073
|
const toolsByName = new Map(
|
|
9937
10074
|
registered.map((tool) => [tool.name, tool])
|
|
9938
10075
|
);
|
|
@@ -9955,7 +10092,8 @@ var AgentRunner = class {
|
|
|
9955
10092
|
...subagents2.length ? { subagents: subagents2 } : {},
|
|
9956
10093
|
constraintVersion: 1,
|
|
9957
10094
|
userConstraints: [...userConstraints],
|
|
9958
|
-
mcpResultArchiveVersion: 1
|
|
10095
|
+
mcpResultArchiveVersion: 1,
|
|
10096
|
+
...messages.some((message) => message.role === "user" && message.content.startsWith("[compacted-context]")) ? { contextSnapshot: { version: 1, kind: "run", messages: snapshotWithConstraints(messages, userConstraints), completedSteps: steps.length } } : {}
|
|
9959
10097
|
});
|
|
9960
10098
|
const depth = options.depth ?? 0;
|
|
9961
10099
|
const isTopLevel = depth === 0;
|
|
@@ -9964,6 +10102,7 @@ var AgentRunner = class {
|
|
|
9964
10102
|
try {
|
|
9965
10103
|
await options.onCheckpoint(buildSession(void 0, listSubagentTranscripts()));
|
|
9966
10104
|
} catch {
|
|
10105
|
+
diagnosticEvent("session.checkpoint", { outcome: "failed", path: "run" });
|
|
9967
10106
|
}
|
|
9968
10107
|
};
|
|
9969
10108
|
let stopReason = "max-steps";
|
|
@@ -9981,6 +10120,11 @@ var AgentRunner = class {
|
|
|
9981
10120
|
transcriptKind: "run",
|
|
9982
10121
|
fileProvenance
|
|
9983
10122
|
};
|
|
10123
|
+
const periodic = await periodicCompactor.compact(messages, [...pinnedMessages, taskMessage], options.provider, options.model, steps.length, options.signal);
|
|
10124
|
+
if (periodic.compacted) {
|
|
10125
|
+
messages.splice(0, messages.length, ...periodic.messages);
|
|
10126
|
+
await checkpoint();
|
|
10127
|
+
}
|
|
9984
10128
|
await compactIfNeeded(options, messages, system, taskMessage, pinnedMessages, requestContext);
|
|
9985
10129
|
if (options.signal?.aborted) {
|
|
9986
10130
|
stopReason = "aborted";
|
|
@@ -10294,7 +10438,7 @@ var NOTHING_RESTORED = {
|
|
|
10294
10438
|
legacyMcpResults: 0
|
|
10295
10439
|
};
|
|
10296
10440
|
function restorePreviousTranscript(sessions, maxToolResultChars, fileProvenance) {
|
|
10297
|
-
if (sessions.length === 0 || sessions.every((session) => session.steps.length === 0)) return NOTHING_RESTORED;
|
|
10441
|
+
if (sessions.length === 0 || sessions.every((session) => session.steps.length === 0 && !readSessionContextSnapshot(session))) return NOTHING_RESTORED;
|
|
10298
10442
|
const messages = [];
|
|
10299
10443
|
let toolResults = 0;
|
|
10300
10444
|
let truncatedResults = 0;
|
|
@@ -10302,6 +10446,11 @@ function restorePreviousTranscript(sessions, maxToolResultChars, fileProvenance)
|
|
|
10302
10446
|
let steps = 0;
|
|
10303
10447
|
for (const session of sessions) {
|
|
10304
10448
|
steps += session.steps.length;
|
|
10449
|
+
const snapshot = readSessionContextSnapshot(session);
|
|
10450
|
+
if (snapshot?.kind === "run" || snapshot?.kind === "text") {
|
|
10451
|
+
messages.splice(0, messages.length, ...snapshot.messages);
|
|
10452
|
+
continue;
|
|
10453
|
+
}
|
|
10305
10454
|
for (const step of session.steps) {
|
|
10306
10455
|
const content = step.parsed ? JSON.stringify(step.parsed) : step.modelText;
|
|
10307
10456
|
if (content) messages.push({ role: "assistant", content });
|
|
@@ -10316,6 +10465,7 @@ function restorePreviousTranscript(sessions, maxToolResultChars, fileProvenance)
|
|
|
10316
10465
|
fileProvenance?.set(entry.message, toolResultFileProvenance(result, entry.truncated));
|
|
10317
10466
|
}
|
|
10318
10467
|
}
|
|
10468
|
+
if (snapshot) messages.push(...snapshot.messages.filter((message) => message.role === "user" && message.content.startsWith("[compacted-context]")).map((message) => ({ role: "user", content: message.content })));
|
|
10319
10469
|
}
|
|
10320
10470
|
return { messages, steps, toolResults, truncatedResults, legacyMcpResults };
|
|
10321
10471
|
}
|
|
@@ -13522,7 +13672,10 @@ function buildHarnessManifest() {
|
|
|
13522
13672
|
}
|
|
13523
13673
|
|
|
13524
13674
|
// src/commands/mcp.ts
|
|
13525
|
-
import { resolve as resolve9 } from "path";
|
|
13675
|
+
import { resolve as resolve9, dirname as dirname12, join as join12 } from "path";
|
|
13676
|
+
import { constants as constants5 } from "fs";
|
|
13677
|
+
import { lstat as lstat3, mkdir as mkdir14, open as open8, rename as rename2, unlink as unlink3 } from "fs/promises";
|
|
13678
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
13526
13679
|
import chalk5 from "chalk";
|
|
13527
13680
|
|
|
13528
13681
|
// src/mcp/client.ts
|
|
@@ -14348,7 +14501,47 @@ function asError(error) {
|
|
|
14348
14501
|
|
|
14349
14502
|
// src/commands/mcp.ts
|
|
14350
14503
|
function registerMcpCommand(program2) {
|
|
14351
|
-
const mcpCommand = program2.command("mcp").description("
|
|
14504
|
+
const mcpCommand = program2.command("mcp").description("Configure and inspect workspace MCP servers");
|
|
14505
|
+
mcpCommand.command("add <name> <command> [args...]").description("Add a stdio MCP server without starting it").option("-w, --workspace <path>", "Workspace directory", process.cwd()).option("--env <NAME=value>", "Explicit child environment value; prefer --env-pass for credentials", collect, []).option("--env-pass <NAME>", "Forward a named parent environment variable", collect, []).option("--cwd <path>", "Server working directory").option("--timeout <milliseconds>", "Per-request timeout", "30000").option("--disabled", "Keep the server disabled", false).option("--force", "Replace an existing server with this name", false).action(async (name, command, args, options) => {
|
|
14506
|
+
validateServerName(name);
|
|
14507
|
+
const env2 = {};
|
|
14508
|
+
for (const entry of options.env) {
|
|
14509
|
+
const separator = entry.indexOf("=");
|
|
14510
|
+
if (separator < 1) throw new Error("--env must use NAME=value.");
|
|
14511
|
+
const key = entry.slice(0, separator);
|
|
14512
|
+
validateEnvironmentName(key);
|
|
14513
|
+
Object.defineProperty(env2, key, { value: entry.slice(separator + 1), enumerable: true, configurable: true });
|
|
14514
|
+
}
|
|
14515
|
+
options.envPass.forEach(validateEnvironmentName);
|
|
14516
|
+
const spec = McpServerSchema.parse({
|
|
14517
|
+
command,
|
|
14518
|
+
args,
|
|
14519
|
+
env: env2,
|
|
14520
|
+
envPassthrough: [...new Set(options.envPass)],
|
|
14521
|
+
cwd: options.cwd,
|
|
14522
|
+
enabled: !options.disabled,
|
|
14523
|
+
timeoutMs: Number(options.timeout)
|
|
14524
|
+
});
|
|
14525
|
+
const workspace = await assertWorkspaceExists(options.workspace);
|
|
14526
|
+
await updateMcpConfig(workspace, (config) => {
|
|
14527
|
+
const servers = { ...config.mcpServers ?? {} };
|
|
14528
|
+
if (Object.hasOwn(servers, name) && !options.force) throw new Error(`MCP server ${name} already exists. Use --force to replace it.`);
|
|
14529
|
+
servers[name] = spec;
|
|
14530
|
+
return { ...config, mcpServers: servers };
|
|
14531
|
+
});
|
|
14532
|
+
console.log(`Added MCP server ${name}. Run ultrac trust in this workspace to approve the updated settings.`);
|
|
14533
|
+
});
|
|
14534
|
+
mcpCommand.command("remove <name>").description("Remove a workspace MCP server").option("-w, --workspace <path>", "Workspace directory", process.cwd()).action(async (name, options) => {
|
|
14535
|
+
validateServerName(name);
|
|
14536
|
+
const workspace = await assertWorkspaceExists(options.workspace);
|
|
14537
|
+
await updateMcpConfig(workspace, (config) => {
|
|
14538
|
+
const servers = { ...config.mcpServers ?? {} };
|
|
14539
|
+
if (!Object.hasOwn(servers, name)) throw new Error(`No MCP server named ${name}.`);
|
|
14540
|
+
delete servers[name];
|
|
14541
|
+
return { ...config, mcpServers: servers };
|
|
14542
|
+
});
|
|
14543
|
+
console.log(`Removed MCP server ${name}. Reapprove changed workspace settings with ultrac trust if required.`);
|
|
14544
|
+
});
|
|
14352
14545
|
mcpCommand.command("list", { isDefault: true }).description("Connect every configured server and report what it exports").option("-w, --workspace <path>", "Workspace directory", process.cwd()).option("--json", "Print the result as JSON", false).action(async (options) => {
|
|
14353
14546
|
const workspace = resolve9(options.workspace);
|
|
14354
14547
|
const loaded = await loadConfigWithTrust(workspace);
|
|
@@ -14374,6 +14567,7 @@ function registerMcpCommand(program2) {
|
|
|
14374
14567
|
}
|
|
14375
14568
|
const runtime = await withRuntime(workspace, servers, (active) => active);
|
|
14376
14569
|
try {
|
|
14570
|
+
if (runtime.statuses.some((status) => !status.ok)) process.exitCode = 1;
|
|
14377
14571
|
if (options.json) {
|
|
14378
14572
|
console.log(JSON.stringify({ servers: runtime.statuses }, null, 2));
|
|
14379
14573
|
return;
|
|
@@ -14396,7 +14590,7 @@ function registerMcpCommand(program2) {
|
|
|
14396
14590
|
runtime.close();
|
|
14397
14591
|
}
|
|
14398
14592
|
});
|
|
14399
|
-
mcpCommand.command("call <server> <tool>").description("Call one MCP tool directly, for checking a server by hand").option("-w, --workspace <path>", "Workspace directory", process.cwd()).option("--input <json>", "Tool arguments as a JSON object", "{}").action(async (server, tool, options) => {
|
|
14593
|
+
mcpCommand.command("call <server> <tool>").description("Call one MCP tool directly, for checking a server by hand").option("-w, --workspace <path>", "Workspace directory", process.cwd()).option("--input <json>", "Tool arguments as a JSON object", "{}").option("--json", "Print the result as JSON", false).action(async (server, tool, options) => {
|
|
14400
14594
|
const workspace = resolve9(options.workspace);
|
|
14401
14595
|
const loaded = await loadConfigWithTrust(workspace);
|
|
14402
14596
|
const spec = loaded.config.mcpServers?.[server];
|
|
@@ -14414,6 +14608,7 @@ function registerMcpCommand(program2) {
|
|
|
14414
14608
|
} catch (error) {
|
|
14415
14609
|
throw new Error(`--input must be a JSON object: ${error instanceof Error ? error.message : String(error)}`);
|
|
14416
14610
|
}
|
|
14611
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) throw new Error("--input must be a JSON object.");
|
|
14417
14612
|
const runtime = await withRuntime(workspace, { [server]: spec }, (active) => active);
|
|
14418
14613
|
try {
|
|
14419
14614
|
const status = runtime.statuses[0];
|
|
@@ -14438,6 +14633,68 @@ function registerMcpCommand(program2) {
|
|
|
14438
14633
|
}
|
|
14439
14634
|
});
|
|
14440
14635
|
}
|
|
14636
|
+
function collect(value, previous) {
|
|
14637
|
+
return [...previous, value];
|
|
14638
|
+
}
|
|
14639
|
+
function validateServerName(name) {
|
|
14640
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9_.-]{0,79}$/.test(name) || name.includes("__") || ["constructor", "prototype"].includes(name)) {
|
|
14641
|
+
throw new Error("MCP server names must contain letters, numbers, dots, single underscores, or hyphens.");
|
|
14642
|
+
}
|
|
14643
|
+
}
|
|
14644
|
+
function validateEnvironmentName(name) {
|
|
14645
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) throw new Error("Environment variable names must contain letters, numbers, or underscores.");
|
|
14646
|
+
}
|
|
14647
|
+
async function updateMcpConfig(workspace, update) {
|
|
14648
|
+
const target = configPath(workspace);
|
|
14649
|
+
await resolveInsideWorkspaceReal(workspace, target);
|
|
14650
|
+
await mkdir14(dirname12(target), { recursive: true, mode: 448 });
|
|
14651
|
+
await resolveInsideWorkspaceReal(workspace, target);
|
|
14652
|
+
const lockPath = `${target}.mcp-lock`;
|
|
14653
|
+
const lock = await open8(lockPath, "wx", 384).catch((error) => {
|
|
14654
|
+
if (error.code === "EEXIST") throw new Error("Another MCP configuration update is in progress.");
|
|
14655
|
+
throw error;
|
|
14656
|
+
});
|
|
14657
|
+
const temporary = join12(dirname12(target), `.mcp-config-${randomUUID4()}.tmp`);
|
|
14658
|
+
try {
|
|
14659
|
+
let config = defaultConfig();
|
|
14660
|
+
const existing = await lstat3(target).catch((error) => {
|
|
14661
|
+
if (error.code === "ENOENT") return null;
|
|
14662
|
+
throw error;
|
|
14663
|
+
});
|
|
14664
|
+
if (existing) {
|
|
14665
|
+
if (!existing.isFile() || existing.isSymbolicLink() || existing.size > 1048576) throw new Error("MCP config must be a regular JSON file no larger than 1 MiB.");
|
|
14666
|
+
const handle2 = await open8(target, constants5.O_RDONLY | constants5.O_NOFOLLOW);
|
|
14667
|
+
try {
|
|
14668
|
+
const content = await handle2.readFile("utf8");
|
|
14669
|
+
try {
|
|
14670
|
+
config = JSON.parse(content);
|
|
14671
|
+
} catch {
|
|
14672
|
+
throw new Error("Workspace config is not valid JSON.");
|
|
14673
|
+
}
|
|
14674
|
+
} finally {
|
|
14675
|
+
await handle2.close();
|
|
14676
|
+
}
|
|
14677
|
+
if (!config || typeof config !== "object" || Array.isArray(config)) throw new Error("Workspace config must be a JSON object.");
|
|
14678
|
+
if (!UltracConfigSchema.safeParse(config).success) throw new Error("Workspace config does not match the Ultrac configuration schema.");
|
|
14679
|
+
}
|
|
14680
|
+
const next = update(config);
|
|
14681
|
+
UltracConfigSchema.parse(next);
|
|
14682
|
+
const handle = await open8(temporary, "wx", 384);
|
|
14683
|
+
try {
|
|
14684
|
+
await handle.writeFile(`${JSON.stringify(next, null, 2)}
|
|
14685
|
+
`, "utf8");
|
|
14686
|
+
await handle.sync();
|
|
14687
|
+
} finally {
|
|
14688
|
+
await handle.close();
|
|
14689
|
+
}
|
|
14690
|
+
await resolveInsideWorkspaceReal(workspace, target);
|
|
14691
|
+
await rename2(temporary, target);
|
|
14692
|
+
} finally {
|
|
14693
|
+
await unlink3(temporary).catch(() => void 0);
|
|
14694
|
+
await lock.close();
|
|
14695
|
+
await unlink3(lockPath).catch(() => void 0);
|
|
14696
|
+
}
|
|
14697
|
+
}
|
|
14441
14698
|
async function withRuntime(workspace, servers, use) {
|
|
14442
14699
|
const packageInfo = readPackageInfo();
|
|
14443
14700
|
const runtime = await connectMcpServers(servers, {
|
|
@@ -14554,7 +14811,7 @@ import chalk6 from "chalk";
|
|
|
14554
14811
|
|
|
14555
14812
|
// src/notify/config.ts
|
|
14556
14813
|
import { chmod as chmod3, readFile as readFile12, rm as rm4, writeFile as writeFile11 } from "fs/promises";
|
|
14557
|
-
import { join as
|
|
14814
|
+
import { join as join13 } from "path";
|
|
14558
14815
|
import { z as z31 } from "zod";
|
|
14559
14816
|
var TelegramSchema = z31.object({
|
|
14560
14817
|
botToken: z31.string().min(1),
|
|
@@ -14580,7 +14837,7 @@ var NotifyConfigSchema = z31.object({
|
|
|
14580
14837
|
answerTimeoutSeconds: z31.number().int().min(30).max(86400).default(1800)
|
|
14581
14838
|
});
|
|
14582
14839
|
function notifyConfigPath() {
|
|
14583
|
-
return
|
|
14840
|
+
return join13(ultracHomeDir(), "notify.json");
|
|
14584
14841
|
}
|
|
14585
14842
|
async function readStoredNotifyConfig() {
|
|
14586
14843
|
let text;
|
|
@@ -15019,7 +15276,7 @@ function registerNotifyCommand(program2) {
|
|
|
15019
15276
|
}
|
|
15020
15277
|
|
|
15021
15278
|
// src/commands/repl.ts
|
|
15022
|
-
import { createInterface as createInterface3 } from "readline";
|
|
15279
|
+
import { createInterface as createInterface3, emitKeypressEvents } from "readline";
|
|
15023
15280
|
import { stdin as stdin5, stdout as stdout5 } from "process";
|
|
15024
15281
|
import chalk11 from "chalk";
|
|
15025
15282
|
|
|
@@ -15197,6 +15454,9 @@ function registerTrustCommand(program2) {
|
|
|
15197
15454
|
// src/core/manual-compaction.ts
|
|
15198
15455
|
async function compactManually(messages, pinned, provider, model, focus = "", signal) {
|
|
15199
15456
|
const retained = new Set(pinned);
|
|
15457
|
+
const units = completeContextUnits(messages);
|
|
15458
|
+
if (!units) throw new Error("Compaction found incomplete tool units; original context was kept.");
|
|
15459
|
+
for (const unit of units) if (unit.some((message) => retained.has(message))) for (const message of unit) retained.add(message);
|
|
15200
15460
|
const history = messages.filter(
|
|
15201
15461
|
(message) => message.role !== "system" && !retained.has(message) && !isCompactionSummary(message)
|
|
15202
15462
|
);
|
|
@@ -15252,6 +15512,10 @@ Give extra attention to this user-supplied focus: ${focus}` : ""
|
|
|
15252
15512
|
}
|
|
15253
15513
|
|
|
15254
15514
|
// src/core/interactive.ts
|
|
15515
|
+
var SKIPPED_FOR_QUEUED_MESSAGE = "Skipped; a new user message was queued after the previous tool. Follow that message instead of retrying this call.";
|
|
15516
|
+
function skippedForQueuedMessage(tool) {
|
|
15517
|
+
return { tool, ok: false, content: SKIPPED_FOR_QUEUED_MESSAGE };
|
|
15518
|
+
}
|
|
15255
15519
|
function permissionDeniedMessage(tool, permission, sticky) {
|
|
15256
15520
|
const denied = `User denied ${permission} permission for ${tool}.`;
|
|
15257
15521
|
if (!sticky) return denied;
|
|
@@ -15310,6 +15574,7 @@ var InteractiveSession = class {
|
|
|
15310
15574
|
started = false;
|
|
15311
15575
|
/** How the last turn ended. Absent until one has. */
|
|
15312
15576
|
lastStopReason;
|
|
15577
|
+
periodicCompactor = new PeriodicCompactor();
|
|
15313
15578
|
pinnedMessages = [];
|
|
15314
15579
|
/** Tools described in this turn's system prompt. `/mode` can change it. */
|
|
15315
15580
|
advertisedTools() {
|
|
@@ -15348,6 +15613,7 @@ var InteractiveSession = class {
|
|
|
15348
15613
|
}
|
|
15349
15614
|
async restoreSessions(sessions) {
|
|
15350
15615
|
if (this.steps.length || this.userConstraints.length) throw new Error("Restore requires a fresh session.");
|
|
15616
|
+
for (const saved of sessions) readSessionContextSnapshot(saved);
|
|
15351
15617
|
await this.start();
|
|
15352
15618
|
this.parentSessionId = sessions.at(-1)?.id;
|
|
15353
15619
|
this.firstTask = sessions.at(-1)?.task ?? "";
|
|
@@ -15368,6 +15634,17 @@ var InteractiveSession = class {
|
|
|
15368
15634
|
for (const { call, result } of step.toolResults) this.messages.push({ role: "tool", content: JSON.stringify({ call, result: capResultForTranscript(result, this.options.maxToolResultChars) }) });
|
|
15369
15635
|
}
|
|
15370
15636
|
}
|
|
15637
|
+
const snapshot = sessions.at(-1)?.contextSnapshot;
|
|
15638
|
+
if (snapshot && snapshot.kind !== "text") this.messages.push(...snapshot.messages.filter(isCompactionSummary).map((message) => ({ role: "user", content: message.content })));
|
|
15639
|
+
if (snapshot?.kind === "text") {
|
|
15640
|
+
this.messages.splice(0, this.messages.length, ...snapshot.messages);
|
|
15641
|
+
this.pinnedMessages.splice(0, this.pinnedMessages.length, ...this.messages.filter((message) => message.role === "user" && !isCompactionSummary(message)));
|
|
15642
|
+
for (const constraint of this.userConstraints) if (!this.messages.some((message) => message.role === "user" && message.content === constraint.content)) {
|
|
15643
|
+
const message = { role: "user", content: constraint.content };
|
|
15644
|
+
this.messages.push(message);
|
|
15645
|
+
this.pinnedMessages.push(message);
|
|
15646
|
+
}
|
|
15647
|
+
}
|
|
15371
15648
|
}
|
|
15372
15649
|
contextStatus() {
|
|
15373
15650
|
const mode = getAgentModePreset(this.activeMode);
|
|
@@ -15430,6 +15707,7 @@ Acknowledge that you have the context and wait for the user's first request.`
|
|
|
15430
15707
|
try {
|
|
15431
15708
|
await handlers.onCheckpoint(this.toSession());
|
|
15432
15709
|
} catch {
|
|
15710
|
+
diagnosticEvent("session.checkpoint", { outcome: "failed", path: "interactive" });
|
|
15433
15711
|
}
|
|
15434
15712
|
}
|
|
15435
15713
|
appendQueuedMessages(handlers) {
|
|
@@ -15472,6 +15750,11 @@ Acknowledge that you have the context and wait for the user's first request.`
|
|
|
15472
15750
|
modePreset
|
|
15473
15751
|
);
|
|
15474
15752
|
const turn = this.steps.length + 1;
|
|
15753
|
+
const periodic = await this.periodicCompactor.compact(this.messages, this.pinnedMessages, this.options.provider, this.activeModel, this.steps.length, handlers.signal);
|
|
15754
|
+
if (periodic.compacted) {
|
|
15755
|
+
this.messages.splice(0, this.messages.length, ...periodic.messages);
|
|
15756
|
+
await this.checkpoint(handlers);
|
|
15757
|
+
}
|
|
15475
15758
|
await this.compactIfNeeded(system, handlers, turn);
|
|
15476
15759
|
handlers.onModelStart?.();
|
|
15477
15760
|
let text;
|
|
@@ -15512,12 +15795,13 @@ Acknowledge that you have the context and wait for the user's first request.`
|
|
|
15512
15795
|
this.messages.push({ role: "assistant", content: JSON.stringify(parsed) });
|
|
15513
15796
|
this.steps.push(step);
|
|
15514
15797
|
await this.checkpoint(handlers);
|
|
15515
|
-
for (
|
|
15798
|
+
for (let callIndex = 0; callIndex < parsed.toolCalls.length; callIndex += 1) {
|
|
15799
|
+
const call = parsed.toolCalls[callIndex];
|
|
15516
15800
|
if (handlers.signal?.aborted) break;
|
|
15517
15801
|
const authorization = await this.authorize(call, handlers);
|
|
15518
15802
|
if (handlers.signal?.aborted) break;
|
|
15519
15803
|
if (authorization.denied) {
|
|
15520
|
-
const
|
|
15804
|
+
const result = {
|
|
15521
15805
|
tool: call.tool,
|
|
15522
15806
|
ok: false,
|
|
15523
15807
|
content: authorization.message ?? permissionDeniedMessage(
|
|
@@ -15527,24 +15811,33 @@ Acknowledge that you have the context and wait for the user's first request.`
|
|
|
15527
15811
|
)
|
|
15528
15812
|
};
|
|
15529
15813
|
if (authorization.permission) handlers.onToolDenied?.(call, authorization.permission);
|
|
15530
|
-
step.toolResults.push({ call, result
|
|
15531
|
-
this.pushToolResult(call,
|
|
15814
|
+
step.toolResults.push({ call, result });
|
|
15815
|
+
this.pushToolResult(call, result);
|
|
15816
|
+
await this.checkpoint(handlers);
|
|
15817
|
+
} else {
|
|
15818
|
+
handlers.onToolStart?.(call);
|
|
15819
|
+
const modeAllowlist = getEnforcedModeToolAllowlist(this.activeMode);
|
|
15820
|
+
const rawResult = await this.registry.execute(call, {
|
|
15821
|
+
workspace: this.options.workspace,
|
|
15822
|
+
permissions: getEffectiveModePermissions(this.activeMode, authorization.permissions),
|
|
15823
|
+
...handlers.signal ? { signal: handlers.signal } : {},
|
|
15824
|
+
...modeAllowlist ? { modeAllowlist } : {}
|
|
15825
|
+
});
|
|
15826
|
+
const result = await this.mcpResultArchive.store(rawResult, this.options.maxToolResultChars);
|
|
15827
|
+
handlers.onToolResult?.(call, result);
|
|
15828
|
+
step.toolResults.push({ call, result });
|
|
15829
|
+
this.pushToolResult(call, result);
|
|
15532
15830
|
await this.checkpoint(handlers);
|
|
15533
|
-
continue;
|
|
15534
15831
|
}
|
|
15535
|
-
|
|
15536
|
-
|
|
15537
|
-
|
|
15538
|
-
|
|
15539
|
-
|
|
15540
|
-
|
|
15541
|
-
|
|
15542
|
-
|
|
15543
|
-
|
|
15544
|
-
handlers.onToolResult?.(call, result);
|
|
15545
|
-
step.toolResults.push({ call, result });
|
|
15546
|
-
this.pushToolResult(call, result);
|
|
15547
|
-
await this.checkpoint(handlers);
|
|
15832
|
+
if (this.appendQueuedMessages(handlers) > 0) {
|
|
15833
|
+
for (const skipped of parsed.toolCalls.slice(callIndex + 1)) {
|
|
15834
|
+
const result = skippedForQueuedMessage(skipped.tool);
|
|
15835
|
+
step.toolResults.push({ call: skipped, result });
|
|
15836
|
+
this.pushToolResult(skipped, result);
|
|
15837
|
+
await this.checkpoint(handlers);
|
|
15838
|
+
}
|
|
15839
|
+
break;
|
|
15840
|
+
}
|
|
15548
15841
|
}
|
|
15549
15842
|
if (handlers.signal?.aborted) {
|
|
15550
15843
|
stopReason = "aborted";
|
|
@@ -15604,7 +15897,7 @@ Acknowledge that you have the context and wait for the user's first request.`
|
|
|
15604
15897
|
mode: this.activeMode,
|
|
15605
15898
|
purpose: "turn"
|
|
15606
15899
|
}),
|
|
15607
|
-
// The turn's own signal:
|
|
15900
|
+
// The turn's own signal: Esc abandons the summary, not the turn.
|
|
15608
15901
|
...handlers.signal ? { signal: handlers.signal } : {}
|
|
15609
15902
|
}
|
|
15610
15903
|
);
|
|
@@ -15751,6 +16044,7 @@ Acknowledge that you have the context and wait for the user's first request.`
|
|
|
15751
16044
|
constraintVersion: 1,
|
|
15752
16045
|
userConstraints: [...this.userConstraints],
|
|
15753
16046
|
mcpResultArchiveVersion: 1,
|
|
16047
|
+
...this.messages.some(isCompactionSummary) ? { contextSnapshot: { version: 1, kind: "text", messages: snapshotWithConstraints(this.messages, this.userConstraints), completedSteps: this.steps.length } } : {},
|
|
15754
16048
|
// How the last turn ended, so a saved interactive transcript says whether
|
|
15755
16049
|
// its final message is an answer or the place it ran out of room - the
|
|
15756
16050
|
// same question `ultrac run` already answers for a one-shot run.
|
|
@@ -15803,6 +16097,7 @@ var NativeSession = class {
|
|
|
15803
16097
|
started = false;
|
|
15804
16098
|
/** How the last turn ended. Absent until one has. */
|
|
15805
16099
|
lastStopReason;
|
|
16100
|
+
periodicCompactor = new PeriodicCompactor();
|
|
15806
16101
|
pinnedMessages = [];
|
|
15807
16102
|
/**
|
|
15808
16103
|
* The tools this turn advertises. Resolved per turn rather than once in the
|
|
@@ -15844,6 +16139,7 @@ var NativeSession = class {
|
|
|
15844
16139
|
}
|
|
15845
16140
|
async restoreSessions(sessions) {
|
|
15846
16141
|
if (this.steps.length || this.userConstraints.length) throw new Error("Restore requires a fresh session.");
|
|
16142
|
+
for (const saved of sessions) readSessionContextSnapshot(saved);
|
|
15847
16143
|
await this.start();
|
|
15848
16144
|
this.parentSessionId = sessions.at(-1)?.id;
|
|
15849
16145
|
this.firstTask = sessions.at(-1)?.task ?? "";
|
|
@@ -15871,6 +16167,17 @@ var NativeSession = class {
|
|
|
15871
16167
|
}
|
|
15872
16168
|
}
|
|
15873
16169
|
}
|
|
16170
|
+
const snapshot = sessions.at(-1)?.contextSnapshot;
|
|
16171
|
+
if (snapshot && snapshot.kind !== "native") this.messages.push(...snapshot.messages.filter(isCompactionSummary).map((message) => ({ role: "user", content: message.content })));
|
|
16172
|
+
if (snapshot?.kind === "native") {
|
|
16173
|
+
this.messages.splice(0, this.messages.length, ...snapshot.messages);
|
|
16174
|
+
this.pinnedMessages.splice(0, this.pinnedMessages.length, ...this.messages.filter((message) => message.role === "user" && !isCompactionSummary(message)));
|
|
16175
|
+
for (const constraint of this.userConstraints) if (!this.messages.some((message) => message.role === "user" && message.content === constraint.content)) {
|
|
16176
|
+
const message = { role: "user", content: constraint.content };
|
|
16177
|
+
this.messages.push(message);
|
|
16178
|
+
this.pinnedMessages.push(message);
|
|
16179
|
+
}
|
|
16180
|
+
}
|
|
15874
16181
|
}
|
|
15875
16182
|
contextStatus() {
|
|
15876
16183
|
const system = this.systemPrompt();
|
|
@@ -15931,6 +16238,7 @@ ${context}`
|
|
|
15931
16238
|
try {
|
|
15932
16239
|
await handlers.onCheckpoint(this.toSession());
|
|
15933
16240
|
} catch {
|
|
16241
|
+
diagnosticEvent("session.checkpoint", { outcome: "failed", path: "interactive" });
|
|
15934
16242
|
}
|
|
15935
16243
|
}
|
|
15936
16244
|
appendQueuedMessages(handlers) {
|
|
@@ -15969,6 +16277,11 @@ ${context}`
|
|
|
15969
16277
|
this.refreshSystemPrompt();
|
|
15970
16278
|
const tools = toToolSchemas(this.advertisedTools());
|
|
15971
16279
|
const turn = this.steps.length + 1;
|
|
16280
|
+
const periodic = await this.periodicCompactor.compact(this.messages, this.pinnedMessages, this.options.provider, this.activeModel, this.steps.length, handlers.signal);
|
|
16281
|
+
if (periodic.compacted) {
|
|
16282
|
+
this.messages.splice(0, this.messages.length, ...periodic.messages);
|
|
16283
|
+
await this.checkpoint(handlers);
|
|
16284
|
+
}
|
|
15972
16285
|
await this.compactIfNeeded(tools, handlers, turn);
|
|
15973
16286
|
handlers.onModelStart?.();
|
|
15974
16287
|
let response;
|
|
@@ -16029,13 +16342,14 @@ ${context}`
|
|
|
16029
16342
|
}
|
|
16030
16343
|
continue;
|
|
16031
16344
|
}
|
|
16032
|
-
for (
|
|
16345
|
+
for (let callIndex = 0; callIndex < response.toolCalls.length; callIndex += 1) {
|
|
16346
|
+
const call = response.toolCalls[callIndex];
|
|
16033
16347
|
if (handlers.signal?.aborted) break;
|
|
16034
16348
|
const displayCall = { tool: call.name, input: safeParseArgs(call.arguments) };
|
|
16035
16349
|
const authorization = await this.authorize(call.name, handlers, displayCall);
|
|
16036
16350
|
if (handlers.signal?.aborted) break;
|
|
16037
16351
|
if (authorization.denied) {
|
|
16038
|
-
const
|
|
16352
|
+
const result = {
|
|
16039
16353
|
tool: call.name,
|
|
16040
16354
|
ok: false,
|
|
16041
16355
|
content: authorization.message ?? permissionDeniedMessage(
|
|
@@ -16045,27 +16359,37 @@ ${context}`
|
|
|
16045
16359
|
)
|
|
16046
16360
|
};
|
|
16047
16361
|
if (authorization.permission) handlers.onToolDenied?.(displayCall, authorization.permission);
|
|
16048
|
-
step.toolResults.push({ call: displayCall, result
|
|
16049
|
-
this.pushToolResult(call,
|
|
16362
|
+
step.toolResults.push({ call: displayCall, result });
|
|
16363
|
+
this.pushToolResult(call, result);
|
|
16364
|
+
await this.checkpoint(handlers);
|
|
16365
|
+
} else {
|
|
16366
|
+
handlers.onToolStart?.(displayCall);
|
|
16367
|
+
const modeAllowlist = getEnforcedModeToolAllowlist(this.activeMode);
|
|
16368
|
+
const rawResult = await this.registry.execute(
|
|
16369
|
+
{ tool: call.name, input: displayCall.input },
|
|
16370
|
+
{
|
|
16371
|
+
workspace: this.options.workspace,
|
|
16372
|
+
permissions: getEffectiveModePermissions(this.activeMode, authorization.permissions),
|
|
16373
|
+
...handlers.signal ? { signal: handlers.signal } : {},
|
|
16374
|
+
...modeAllowlist ? { modeAllowlist } : {}
|
|
16375
|
+
}
|
|
16376
|
+
);
|
|
16377
|
+
const result = await this.mcpResultArchive.store(rawResult, this.options.maxToolResultChars);
|
|
16378
|
+
handlers.onToolResult?.(displayCall, result);
|
|
16379
|
+
step.toolResults.push({ call: displayCall, result });
|
|
16380
|
+
this.pushToolResult(call, result);
|
|
16050
16381
|
await this.checkpoint(handlers);
|
|
16051
|
-
continue;
|
|
16052
16382
|
}
|
|
16053
|
-
|
|
16054
|
-
|
|
16055
|
-
|
|
16056
|
-
|
|
16057
|
-
|
|
16058
|
-
|
|
16059
|
-
|
|
16060
|
-
...handlers.signal ? { signal: handlers.signal } : {},
|
|
16061
|
-
...modeAllowlist ? { modeAllowlist } : {}
|
|
16383
|
+
if (this.appendQueuedMessages(handlers) > 0) {
|
|
16384
|
+
for (const skipped of response.toolCalls.slice(callIndex + 1)) {
|
|
16385
|
+
const skippedCall = { tool: skipped.name, input: safeParseArgs(skipped.arguments) };
|
|
16386
|
+
const result = skippedForQueuedMessage(skipped.name);
|
|
16387
|
+
step.toolResults.push({ call: skippedCall, result });
|
|
16388
|
+
this.pushToolResult(skipped, result);
|
|
16389
|
+
await this.checkpoint(handlers);
|
|
16062
16390
|
}
|
|
16063
|
-
|
|
16064
|
-
|
|
16065
|
-
handlers.onToolResult?.(displayCall, result);
|
|
16066
|
-
step.toolResults.push({ call: displayCall, result });
|
|
16067
|
-
this.pushToolResult(call, result);
|
|
16068
|
-
await this.checkpoint(handlers);
|
|
16391
|
+
break;
|
|
16392
|
+
}
|
|
16069
16393
|
}
|
|
16070
16394
|
if (handlers.signal?.aborted) {
|
|
16071
16395
|
stopReason = "aborted";
|
|
@@ -16121,7 +16445,7 @@ ${context}`
|
|
|
16121
16445
|
mode: this.activeMode,
|
|
16122
16446
|
purpose: "turn"
|
|
16123
16447
|
}),
|
|
16124
|
-
// The turn's own signal:
|
|
16448
|
+
// The turn's own signal: Esc abandons the summary, not the turn.
|
|
16125
16449
|
...handlers.signal ? { signal: handlers.signal } : {}
|
|
16126
16450
|
}
|
|
16127
16451
|
);
|
|
@@ -16285,6 +16609,7 @@ ${guidance.join("\n")}` : ""}`;
|
|
|
16285
16609
|
constraintVersion: 1,
|
|
16286
16610
|
userConstraints: [...this.userConstraints],
|
|
16287
16611
|
mcpResultArchiveVersion: 1,
|
|
16612
|
+
...this.messages.some(isCompactionSummary) ? { contextSnapshot: { version: 1, kind: "native", messages: snapshotWithConstraints(this.messages, this.userConstraints), completedSteps: this.steps.length } } : {},
|
|
16288
16613
|
// How the last turn ended, so a saved interactive transcript says whether
|
|
16289
16614
|
// its final message is an answer or the place it ran out of room - the
|
|
16290
16615
|
// same question `ultrac run` already answers for a one-shot run.
|
|
@@ -16335,6 +16660,16 @@ function createSessionCheckpointWriter(workspace, onFailure) {
|
|
|
16335
16660
|
return Object.assign(writeCheckpoint, { warnFailure });
|
|
16336
16661
|
}
|
|
16337
16662
|
|
|
16663
|
+
// src/ui/ghost.ts
|
|
16664
|
+
var quietSessions = /* @__PURE__ */ new WeakSet();
|
|
16665
|
+
function isGhostEnabled(session) {
|
|
16666
|
+
return quietSessions.has(session);
|
|
16667
|
+
}
|
|
16668
|
+
function setGhostEnabled(session, enabled) {
|
|
16669
|
+
if (enabled) quietSessions.add(session);
|
|
16670
|
+
else quietSessions.delete(session);
|
|
16671
|
+
}
|
|
16672
|
+
|
|
16338
16673
|
// src/ui/output.ts
|
|
16339
16674
|
import chalk8 from "chalk";
|
|
16340
16675
|
var stopReasonDetail = {
|
|
@@ -16348,7 +16683,7 @@ function stopReasonLine(stopReason) {
|
|
|
16348
16683
|
const detail = stopReasonDetail[reason] ?? "partial answer";
|
|
16349
16684
|
return `Stopped: ${reason} (${detail})`;
|
|
16350
16685
|
}
|
|
16351
|
-
function printSession(session, sessionPath2) {
|
|
16686
|
+
function printSession(session, sessionPath2, options = {}) {
|
|
16352
16687
|
for (const step of session.steps) {
|
|
16353
16688
|
if (step.parsed?.message) {
|
|
16354
16689
|
console.log(chalk8.gray(`step ${step.index + 1}:`), step.parsed.message);
|
|
@@ -16356,7 +16691,7 @@ function printSession(session, sessionPath2) {
|
|
|
16356
16691
|
for (const toolResult of step.toolResults) {
|
|
16357
16692
|
const status = toolResult.result.ok ? chalk8.green("ok") : chalk8.red("failed");
|
|
16358
16693
|
console.log(chalk8.cyan(toolResult.call.tool), status);
|
|
16359
|
-
console.log(chalk8.gray(indent2(toolResult.result.content)));
|
|
16694
|
+
console.log(chalk8.gray(indent2(formatToolOutput(toolResult.result.content, { verbose: options.verboseTools, failed: !toolResult.result.ok, hint: "Use --verbose-tools or inspect the saved session for full output." }))));
|
|
16360
16695
|
}
|
|
16361
16696
|
}
|
|
16362
16697
|
if (session.finalMessage) {
|
|
@@ -16379,17 +16714,31 @@ function printTools(tools) {
|
|
|
16379
16714
|
console.log(chalk8.gray(indent2(tool.description)));
|
|
16380
16715
|
}
|
|
16381
16716
|
}
|
|
16382
|
-
function printToolResult(result) {
|
|
16717
|
+
function printToolResult(result, options = {}) {
|
|
16383
16718
|
const status = result.ok ? chalk8.green("ok") : chalk8.red("failed");
|
|
16384
16719
|
console.log(`${chalk8.cyan(result.tool)} ${status}`);
|
|
16385
|
-
console.log(chalk8.gray(indent2(result.content)));
|
|
16720
|
+
console.log(chalk8.gray(indent2(formatToolOutput(result.content, { verbose: options.verboseTools, failed: !result.ok, hint: "Use --verbose-tools or --result-json for full output." }))));
|
|
16386
16721
|
}
|
|
16387
16722
|
function indent2(value) {
|
|
16388
16723
|
return value.split("\n").map((line) => ` ${line}`).join("\n");
|
|
16389
16724
|
}
|
|
16725
|
+
function formatToolOutput(content, options = {}) {
|
|
16726
|
+
if (options.verbose) return content;
|
|
16727
|
+
const lines = content.split(/\r?\n/);
|
|
16728
|
+
const shown = lines.length <= 6 ? lines : options.failed ? [...lines.slice(0, 3), ...lines.slice(-3)] : lines.slice(0, 6);
|
|
16729
|
+
const clipped = shown.some((line) => line.length > 200);
|
|
16730
|
+
const preview = shown.map((line) => line.length > 200 ? `${line.slice(0, 199)}\u2026` : line).join("\n");
|
|
16731
|
+
const hidden = lines.length - shown.length;
|
|
16732
|
+
if (!hidden && !clipped) return preview;
|
|
16733
|
+
return `${preview}
|
|
16734
|
+
\u2026 ${hidden ? `${hidden} more lines` : "Long lines shortened"}. ${options.hint ?? "Full output remains available in the saved session."}`;
|
|
16735
|
+
}
|
|
16390
16736
|
|
|
16391
16737
|
// src/skills/commands.ts
|
|
16392
16738
|
var BUILTIN_SLASH_COMMANDS = [
|
|
16739
|
+
"logs",
|
|
16740
|
+
"ghost",
|
|
16741
|
+
"output",
|
|
16393
16742
|
"usage",
|
|
16394
16743
|
"max-steps",
|
|
16395
16744
|
"resume",
|
|
@@ -16756,7 +17105,10 @@ var Spinner = class {
|
|
|
16756
17105
|
label;
|
|
16757
17106
|
timer;
|
|
16758
17107
|
index = 0;
|
|
16759
|
-
|
|
17108
|
+
holdWhile;
|
|
17109
|
+
held = false;
|
|
17110
|
+
start(holdWhile) {
|
|
17111
|
+
this.holdWhile = holdWhile;
|
|
16760
17112
|
if (!stdout4.isTTY || this.timer) return;
|
|
16761
17113
|
this.render();
|
|
16762
17114
|
this.timer = setInterval(() => this.render(), 90);
|
|
@@ -16775,6 +17127,15 @@ var Spinner = class {
|
|
|
16775
17127
|
}
|
|
16776
17128
|
}
|
|
16777
17129
|
render() {
|
|
17130
|
+
if (this.holdWhile?.()) {
|
|
17131
|
+
if (!this.held) {
|
|
17132
|
+
stdout4.clearLine(0);
|
|
17133
|
+
stdout4.cursorTo(0);
|
|
17134
|
+
this.held = true;
|
|
17135
|
+
}
|
|
17136
|
+
return;
|
|
17137
|
+
}
|
|
17138
|
+
this.held = false;
|
|
16778
17139
|
this.index = (this.index + 1) % FRAMES.length;
|
|
16779
17140
|
stdout4.clearLine(0);
|
|
16780
17141
|
stdout4.cursorTo(0);
|
|
@@ -16890,7 +17251,7 @@ function renderWelcome(info, options = {}) {
|
|
|
16890
17251
|
lines.push(cardLine(`logs ${shortenMiddle(info.diagnosticPath, Math.max(8, innerWidth - 5))}`, innerWidth, paint.gray));
|
|
16891
17252
|
lines.push(paint.cyan(`\u2570${"\u2500".repeat(cardWidth - 2)}\u256F`));
|
|
16892
17253
|
lines.push(
|
|
16893
|
-
wide ? `${paint.gray(" /help commands")} ${paint.dim("\xB7")} ${paint.gray("
|
|
17254
|
+
wide ? `${paint.gray(" /help commands")} ${paint.dim("\xB7")} ${paint.gray("Esc stop")} ${paint.dim("\xB7")} ${paint.gray("Ctrl-C exit")}` : paint.gray(" /help \xB7 Esc \xB7 Ctrl-C")
|
|
16894
17255
|
);
|
|
16895
17256
|
return lines.join("\n");
|
|
16896
17257
|
}
|
|
@@ -16928,14 +17289,14 @@ function shortenMiddle(value, width) {
|
|
|
16928
17289
|
// src/update/check.ts
|
|
16929
17290
|
import { spawn as spawn4 } from "child_process";
|
|
16930
17291
|
import { randomBytes as randomBytes4 } from "crypto";
|
|
16931
|
-
import { readFile as readFile13, rename as
|
|
16932
|
-
import { join as
|
|
17292
|
+
import { readFile as readFile13, rename as rename3, unlink as unlink4, writeFile as writeFile12 } from "fs/promises";
|
|
17293
|
+
import { join as join14 } from "path";
|
|
16933
17294
|
var DEFAULT_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
16934
17295
|
var QUERY_TIMEOUT_MS = 5e3;
|
|
16935
17296
|
var INSTALL_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
16936
17297
|
async function checkForUpdate(options) {
|
|
16937
17298
|
const now = options.now ?? Date.now();
|
|
16938
|
-
const cachePath = options.cachePath ??
|
|
17299
|
+
const cachePath = options.cachePath ?? join14(await ensureUltracHome(), "update-check.json");
|
|
16939
17300
|
const maxAgeMs = options.maxAgeMs ?? DEFAULT_MAX_AGE_MS;
|
|
16940
17301
|
let latestVersion;
|
|
16941
17302
|
if (!options.force) {
|
|
@@ -17019,9 +17380,9 @@ async function writeCache(path, cache) {
|
|
|
17019
17380
|
try {
|
|
17020
17381
|
await writeFile12(temporary, `${JSON.stringify(cache)}
|
|
17021
17382
|
`, { encoding: "utf8", mode: 384, flag: "wx" });
|
|
17022
|
-
await
|
|
17383
|
+
await rename3(temporary, path);
|
|
17023
17384
|
} catch {
|
|
17024
|
-
await
|
|
17385
|
+
await unlink4(temporary).catch(() => void 0);
|
|
17025
17386
|
}
|
|
17026
17387
|
}
|
|
17027
17388
|
async function runNpm(args, options) {
|
|
@@ -17079,11 +17440,18 @@ var HELP = `Commands:
|
|
|
17079
17440
|
/allow <perm> Grant a permission for this session (write, shell, network)
|
|
17080
17441
|
/whoami Show sign-in state
|
|
17081
17442
|
/tools List available tools
|
|
17443
|
+
/logs Show the private diagnostic log path
|
|
17444
|
+
/ghost [on|off|status] Quiet activity output; diagnostics stay enabled
|
|
17445
|
+
/output [number|list] Inspect saved tool output (latest by default)
|
|
17082
17446
|
/memory List and inspect stored memories
|
|
17083
17447
|
/skills List installed skills
|
|
17084
17448
|
/update Show how to install an available CLI update
|
|
17085
17449
|
/clear Start a fresh conversation
|
|
17086
17450
|
/exit, /quit End the session
|
|
17451
|
+
Keys:
|
|
17452
|
+
Type during a turn Queue a follow-up for after the current tool call
|
|
17453
|
+
Esc Stop the current turn; the session stays open
|
|
17454
|
+
Ctrl-C Exit and save the session
|
|
17087
17455
|
Custom commands come from skills that declare a 'command:' key in their
|
|
17088
17456
|
frontmatter; built-ins always win a name clash. Type anything else to talk to Ultrac.`;
|
|
17089
17457
|
var EXIT_REPL_NOT_STARTED = 1;
|
|
@@ -17295,12 +17663,21 @@ async function runRepl(options, initialTask = "") {
|
|
|
17295
17663
|
});
|
|
17296
17664
|
let running = true;
|
|
17297
17665
|
let turnAbort;
|
|
17298
|
-
|
|
17299
|
-
|
|
17666
|
+
const exitSession = () => {
|
|
17667
|
+
handleReplExit(turnAbort);
|
|
17300
17668
|
running = false;
|
|
17301
17669
|
console.log(chalk11.gray("\nExiting ultrac\u2026"));
|
|
17302
17670
|
rl.close();
|
|
17303
|
-
}
|
|
17671
|
+
};
|
|
17672
|
+
rl.on("SIGINT", exitSession);
|
|
17673
|
+
const onKeypress = (_input, key) => {
|
|
17674
|
+
if (!key || key.name !== "escape" || key.ctrl || key.meta) return;
|
|
17675
|
+
handleReplEscape(turnAbort);
|
|
17676
|
+
};
|
|
17677
|
+
if (stdin5.isTTY) {
|
|
17678
|
+
emitKeypressEvents(stdin5, rl);
|
|
17679
|
+
stdin5.on("keypress", onKeypress);
|
|
17680
|
+
}
|
|
17304
17681
|
const terminate = () => {
|
|
17305
17682
|
running = false;
|
|
17306
17683
|
turnAbort?.abort();
|
|
@@ -17319,7 +17696,7 @@ async function runRepl(options, initialTask = "") {
|
|
|
17319
17696
|
if (initialTask) {
|
|
17320
17697
|
console.log(chalk11.cyan(`
|
|
17321
17698
|
\u203A ${initialTask}`));
|
|
17322
|
-
await withInterrupt((signal) => runAndCheckpointTurn(session, initialTask, question, signal, persistSession, drainQueuedMessages));
|
|
17699
|
+
await withInterrupt((signal) => runAndCheckpointTurn(session, initialTask, question, signal, persistSession, drainQueuedMessages, () => reader.composing));
|
|
17323
17700
|
}
|
|
17324
17701
|
while (running) {
|
|
17325
17702
|
if (stdout5.isTTY) {
|
|
@@ -17346,7 +17723,7 @@ async function runRepl(options, initialTask = "") {
|
|
|
17346
17723
|
resetContextBudgetNotices();
|
|
17347
17724
|
console.log(chalk11.gray("Started a fresh conversation."));
|
|
17348
17725
|
}, async (prompt) => {
|
|
17349
|
-
await runAndCheckpointTurn(session, prompt, question, signal, persistSession, drainQueuedMessages);
|
|
17726
|
+
await runAndCheckpointTurn(session, prompt, question, signal, persistSession, drainQueuedMessages, () => reader.composing);
|
|
17350
17727
|
}, signal, async (id) => {
|
|
17351
17728
|
const lineage = await loadSessionLineage(workspace, id);
|
|
17352
17729
|
const restored = createSession({ model: session.model, mode: session.mode, permissions: session.currentPermissions });
|
|
@@ -17375,10 +17752,11 @@ async function runRepl(options, initialTask = "") {
|
|
|
17375
17752
|
}
|
|
17376
17753
|
continue;
|
|
17377
17754
|
}
|
|
17378
|
-
await withInterrupt((signal) => runAndCheckpointTurn(session, raw, question, signal, persistSession, drainQueuedMessages));
|
|
17755
|
+
await withInterrupt((signal) => runAndCheckpointTurn(session, raw, question, signal, persistSession, drainQueuedMessages, () => reader.composing));
|
|
17379
17756
|
}
|
|
17380
17757
|
} finally {
|
|
17381
17758
|
process.off("SIGTERM", terminate);
|
|
17759
|
+
if (stdin5.isTTY) stdin5.off("keypress", onKeypress);
|
|
17382
17760
|
setAskUserRuntime(void 0);
|
|
17383
17761
|
rl.close();
|
|
17384
17762
|
await saveTranscript(session, writeCheckpoint);
|
|
@@ -17386,9 +17764,11 @@ async function runRepl(options, initialTask = "") {
|
|
|
17386
17764
|
}
|
|
17387
17765
|
console.log(chalk11.gray("Goodbye."));
|
|
17388
17766
|
}
|
|
17389
|
-
async function runTurn(session, input, question, signal, drainQueuedMessages, persist) {
|
|
17767
|
+
async function runTurn(session, input, question, signal, drainQueuedMessages, persist, isComposing) {
|
|
17390
17768
|
const spinner = new Spinner();
|
|
17391
|
-
const
|
|
17769
|
+
const quiet = isGhostEnabled(session);
|
|
17770
|
+
const detachProgress = quiet ? () => {
|
|
17771
|
+
} : attachProgressUi();
|
|
17392
17772
|
let deltaChars = 0;
|
|
17393
17773
|
try {
|
|
17394
17774
|
await session.ask(input, {
|
|
@@ -17404,11 +17784,11 @@ async function runTurn(session, input, question, signal, drainQueuedMessages, pe
|
|
|
17404
17784
|
},
|
|
17405
17785
|
onModelStart: () => {
|
|
17406
17786
|
deltaChars = 0;
|
|
17407
|
-
spinner.start();
|
|
17787
|
+
if (!quiet) spinner.start(isComposing);
|
|
17408
17788
|
},
|
|
17409
17789
|
onModelDelta: (text) => {
|
|
17410
17790
|
deltaChars += text.length;
|
|
17411
|
-
spinner.update(`thinking (${deltaChars} chars)`);
|
|
17791
|
+
if (!quiet) spinner.update(`thinking (${deltaChars} chars)`);
|
|
17412
17792
|
},
|
|
17413
17793
|
onModelSettled: () => spinner.stop(),
|
|
17414
17794
|
onAssistantMessage: (message) => {
|
|
@@ -17418,14 +17798,14 @@ ${message.trim()}`);
|
|
|
17418
17798
|
}
|
|
17419
17799
|
},
|
|
17420
17800
|
onToolStart: (call) => {
|
|
17421
|
-
console.log(chalk11.dim(` ${toolActivityLabel(call)}`));
|
|
17801
|
+
if (!quiet) console.log(chalk11.dim(` ${toolActivityLabel(call)}`));
|
|
17422
17802
|
},
|
|
17423
17803
|
onToolResult: (_call, result) => printToolResult2(result),
|
|
17424
17804
|
...drainQueuedMessages ? {
|
|
17425
17805
|
drainQueuedMessages,
|
|
17426
|
-
onQueuedMessage: () => {
|
|
17806
|
+
onQueuedMessage: (message) => {
|
|
17427
17807
|
spinner.stop();
|
|
17428
|
-
console.log(chalk11.dim(
|
|
17808
|
+
console.log(chalk11.dim(` Queued for after the current tool: ${message}`));
|
|
17429
17809
|
}
|
|
17430
17810
|
} : {},
|
|
17431
17811
|
onToolDenied: (call, permission) => {
|
|
@@ -17470,9 +17850,9 @@ ${message.trim()}`);
|
|
|
17470
17850
|
detachProgress();
|
|
17471
17851
|
}
|
|
17472
17852
|
}
|
|
17473
|
-
async function runAndCheckpointTurn(session, input, question, signal, persist, drainQueuedMessages) {
|
|
17853
|
+
async function runAndCheckpointTurn(session, input, question, signal, persist, drainQueuedMessages, isComposing) {
|
|
17474
17854
|
try {
|
|
17475
|
-
return await runTurn(session, input, question, signal, drainQueuedMessages, persist);
|
|
17855
|
+
return await runTurn(session, input, question, signal, drainQueuedMessages, persist, isComposing);
|
|
17476
17856
|
} finally {
|
|
17477
17857
|
await persist(session);
|
|
17478
17858
|
}
|
|
@@ -17544,6 +17924,21 @@ async function handleSlashCommand(raw, session, providerName, workspace, onClear
|
|
|
17544
17924
|
const command = (name ?? "").toLowerCase();
|
|
17545
17925
|
const argument = rest.join(" ").trim();
|
|
17546
17926
|
switch (command) {
|
|
17927
|
+
case "logs": {
|
|
17928
|
+
console.log(currentDiagnosticLogPath() ?? "Diagnostic logging has not been configured for this process.");
|
|
17929
|
+
return "ok";
|
|
17930
|
+
}
|
|
17931
|
+
case "ghost": {
|
|
17932
|
+
if (argument && !["on", "off", "status"].includes(argument.toLowerCase())) {
|
|
17933
|
+
console.log("Usage: /ghost [on|off|status]");
|
|
17934
|
+
return "ok";
|
|
17935
|
+
}
|
|
17936
|
+
if (argument.toLowerCase() !== "status") {
|
|
17937
|
+
setGhostEnabled(session, argument ? argument.toLowerCase() === "on" : !isGhostEnabled(session));
|
|
17938
|
+
}
|
|
17939
|
+
console.log(`Ghost mode: ${isGhostEnabled(session) ? "on" : "off"}. Quiet activity output; private diagnostics remain enabled. Network routing and permissions are unchanged.`);
|
|
17940
|
+
return "ok";
|
|
17941
|
+
}
|
|
17547
17942
|
case "help": {
|
|
17548
17943
|
console.log(HELP);
|
|
17549
17944
|
const index = await listSkillCommands(workspace);
|
|
@@ -17721,6 +18116,27 @@ ${extra}`);
|
|
|
17721
18116
|
}
|
|
17722
18117
|
return "ok";
|
|
17723
18118
|
}
|
|
18119
|
+
case "output": {
|
|
18120
|
+
const results = session.toSession().steps.flatMap((step) => step.toolResults);
|
|
18121
|
+
if (!results.length) {
|
|
18122
|
+
console.log("No saved tool output in this session.");
|
|
18123
|
+
return "ok";
|
|
18124
|
+
}
|
|
18125
|
+
if (argument === "list") {
|
|
18126
|
+
for (const [index, entry2] of results.entries()) console.log(`${index + 1}. ${toolActivityLabel(entry2.call)} (${entry2.result.ok ? "ok" : "failed"})`);
|
|
18127
|
+
return "ok";
|
|
18128
|
+
}
|
|
18129
|
+
const selected = argument ? Number(argument) : results.length;
|
|
18130
|
+
if (!Number.isSafeInteger(selected) || selected < 1 || selected > results.length) {
|
|
18131
|
+
console.log(`Usage: /output [1-${results.length}|list]`);
|
|
18132
|
+
return "ok";
|
|
18133
|
+
}
|
|
18134
|
+
const entry = results[selected - 1];
|
|
18135
|
+
console.log(`${selected}. ${toolActivityLabel(entry.call)} (${entry.result.ok ? "ok" : "failed"})`);
|
|
18136
|
+
console.log(entry.result.content);
|
|
18137
|
+
for (const warning of getMcpArchiveWarnings(entry.result)) console.log(chalk11.yellow(`Warning: ${warning}`));
|
|
18138
|
+
return "ok";
|
|
18139
|
+
}
|
|
17724
18140
|
case "tools": {
|
|
17725
18141
|
const names = session.advertisedToolNames;
|
|
17726
18142
|
console.log(`Advertised to the model this turn (${names.length}): ${names.join(", ")}`);
|
|
@@ -17801,10 +18217,8 @@ async function runSkillCommand(session, raw, workspace, question) {
|
|
|
17801
18217
|
console.log(chalk11.red("Unknown command. Try /help."));
|
|
17802
18218
|
}
|
|
17803
18219
|
var LineReader = class {
|
|
17804
|
-
queue = [];
|
|
17805
|
-
waiters = [];
|
|
17806
|
-
closed = false;
|
|
17807
18220
|
constructor(rl) {
|
|
18221
|
+
this.rl = rl;
|
|
17808
18222
|
rl.on("line", (line) => {
|
|
17809
18223
|
const waiter = this.waiters.shift();
|
|
17810
18224
|
if (waiter) {
|
|
@@ -17820,6 +18234,10 @@ var LineReader = class {
|
|
|
17820
18234
|
}
|
|
17821
18235
|
});
|
|
17822
18236
|
}
|
|
18237
|
+
rl;
|
|
18238
|
+
queue = [];
|
|
18239
|
+
waiters = [];
|
|
18240
|
+
closed = false;
|
|
17823
18241
|
next(signal) {
|
|
17824
18242
|
return this.waitForLine(signal, true);
|
|
17825
18243
|
}
|
|
@@ -17830,6 +18248,10 @@ var LineReader = class {
|
|
|
17830
18248
|
nextFresh(signal) {
|
|
17831
18249
|
return this.waitForLine(signal, false);
|
|
17832
18250
|
}
|
|
18251
|
+
/** True while the user is typing a line that has not been submitted yet. */
|
|
18252
|
+
get composing() {
|
|
18253
|
+
return this.rl.line.length > 0;
|
|
18254
|
+
}
|
|
17833
18255
|
/**
|
|
17834
18256
|
* Drain ordinary messages in arrival order. A slash command is a control
|
|
17835
18257
|
* boundary and remains queued for the main REPL loop along with everything
|
|
@@ -17875,11 +18297,15 @@ var LineReader = class {
|
|
|
17875
18297
|
waiter.resolve(line);
|
|
17876
18298
|
}
|
|
17877
18299
|
};
|
|
17878
|
-
function
|
|
18300
|
+
function handleReplEscape(activeTurn) {
|
|
17879
18301
|
if (activeTurn && !activeTurn.signal.aborted) {
|
|
17880
18302
|
activeTurn.abort();
|
|
17881
18303
|
return "abort-turn";
|
|
17882
18304
|
}
|
|
18305
|
+
return "idle";
|
|
18306
|
+
}
|
|
18307
|
+
function handleReplExit(activeTurn) {
|
|
18308
|
+
activeTurn?.abort();
|
|
17883
18309
|
return "exit-session";
|
|
17884
18310
|
}
|
|
17885
18311
|
function printBanner(version, providerName, model, mode, workspace, protocol, budget, diagnosticPath2) {
|
|
@@ -17946,23 +18372,142 @@ import { basename as basename3 } from "path";
|
|
|
17946
18372
|
import chalk12 from "chalk";
|
|
17947
18373
|
|
|
17948
18374
|
// src/core/autonomous.ts
|
|
18375
|
+
import { createHash as createHash7, randomUUID as randomUUID5 } from "crypto";
|
|
17949
18376
|
var AUTONOMOUS_CONTINUE_TASK = "Continue the task from where the previous round stopped. Check what is already done, finish the rest, and verify it.";
|
|
17950
18377
|
var DEFAULT_AUTONOMOUS_MAX_ROUNDS = 5;
|
|
17951
18378
|
var AUTONOMOUS_MAX_STEPS = 200;
|
|
18379
|
+
var DEFAULT_AUTONOMOUS_MAX_ELAPSED_MS = 6 * 60 * 60 * 1e3;
|
|
18380
|
+
var DEFAULT_AUTONOMOUS_BASE_DELAY_MS = 1e3;
|
|
18381
|
+
var DEFAULT_AUTONOMOUS_MAX_DELAY_MS = 3e4;
|
|
18382
|
+
var DEFAULT_AUTONOMOUS_STALL_ROUNDS = 3;
|
|
17952
18383
|
function shouldResume(stopReason) {
|
|
17953
18384
|
return stopReason === "max-steps" || stopReason === "error";
|
|
17954
18385
|
}
|
|
17955
|
-
|
|
17956
|
-
if (!Number.
|
|
17957
|
-
|
|
18386
|
+
function boundedInteger(value, name, min, max) {
|
|
18387
|
+
if (!Number.isSafeInteger(value) || value < min || value > max) throw new Error(`${name} must be an integer from ${min} to ${max}.`);
|
|
18388
|
+
return value;
|
|
18389
|
+
}
|
|
18390
|
+
function abortableSleep(milliseconds, signal) {
|
|
18391
|
+
signal.throwIfAborted();
|
|
18392
|
+
return new Promise((resolve15, reject) => {
|
|
18393
|
+
const timer = setTimeout(() => {
|
|
18394
|
+
signal.removeEventListener("abort", abort);
|
|
18395
|
+
resolve15();
|
|
18396
|
+
}, milliseconds);
|
|
18397
|
+
const abort = () => {
|
|
18398
|
+
clearTimeout(timer);
|
|
18399
|
+
signal.removeEventListener("abort", abort);
|
|
18400
|
+
reject(signal.reason);
|
|
18401
|
+
};
|
|
18402
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
18403
|
+
if (signal.aborted) abort();
|
|
18404
|
+
});
|
|
18405
|
+
}
|
|
18406
|
+
function progress(session) {
|
|
18407
|
+
const hash = createHash7("sha256");
|
|
18408
|
+
let completedTools = 0;
|
|
18409
|
+
hash.update(session.finalMessage ?? "");
|
|
18410
|
+
for (const step of session.steps ?? []) {
|
|
18411
|
+
hash.update(step.modelText ?? "");
|
|
18412
|
+
for (const tool of step.toolResults ?? []) {
|
|
18413
|
+
completedTools += 1;
|
|
18414
|
+
hash.update(tool.call.tool);
|
|
18415
|
+
hash.update(tool.result.ok ? "ok" : "failed");
|
|
18416
|
+
hash.update(tool.result.content ?? "");
|
|
18417
|
+
try {
|
|
18418
|
+
hash.update(JSON.stringify(tool.call.input));
|
|
18419
|
+
} catch {
|
|
18420
|
+
hash.update("unserializable-input");
|
|
18421
|
+
}
|
|
18422
|
+
}
|
|
17958
18423
|
}
|
|
18424
|
+
return { fingerprint: hash.digest("hex"), completedTools };
|
|
18425
|
+
}
|
|
18426
|
+
async function runAutonomousLoop(options) {
|
|
18427
|
+
boundedInteger(options.maxRounds, "maxRounds", 1, 1e3);
|
|
18428
|
+
const maxElapsedMs = boundedInteger(options.maxElapsedMs ?? DEFAULT_AUTONOMOUS_MAX_ELAPSED_MS, "maxElapsedMs", 1, 7 * 24 * 60 * 60 * 1e3);
|
|
18429
|
+
const baseDelay = boundedInteger(options.baseDelayMs ?? DEFAULT_AUTONOMOUS_BASE_DELAY_MS, "baseDelayMs", 0, 3e5);
|
|
18430
|
+
const maxDelay = boundedInteger(options.maxDelayMs ?? DEFAULT_AUTONOMOUS_MAX_DELAY_MS, "maxDelayMs", baseDelay, 3e5);
|
|
18431
|
+
const stallLimit = boundedInteger(options.stallRounds ?? DEFAULT_AUTONOMOUS_STALL_ROUNDS, "stallRounds", 1, 1e3);
|
|
18432
|
+
const now = options.now ?? (() => performance.now());
|
|
18433
|
+
const started = now();
|
|
18434
|
+
const elapsed = () => Math.max(0, now() - started);
|
|
18435
|
+
const budget = new AbortController();
|
|
18436
|
+
const deadline = setTimeout(() => budget.abort(new DOMException("Autonomous elapsed budget reached.", "TimeoutError")), maxElapsedMs);
|
|
18437
|
+
deadline.unref?.();
|
|
18438
|
+
const signal = options.signal ? AbortSignal.any([options.signal, budget.signal]) : budget.signal;
|
|
18439
|
+
const sleep2 = options.sleep ?? abortableSleep;
|
|
18440
|
+
const id = randomUUID5();
|
|
17959
18441
|
let previous;
|
|
17960
|
-
|
|
17961
|
-
|
|
17962
|
-
|
|
17963
|
-
|
|
17964
|
-
|
|
17965
|
-
|
|
18442
|
+
let lastFingerprint;
|
|
18443
|
+
let stalls = 0;
|
|
18444
|
+
let consecutiveErrors = 0;
|
|
18445
|
+
try {
|
|
18446
|
+
signal.throwIfAborted();
|
|
18447
|
+
for (let round = 1; ; round += 1) {
|
|
18448
|
+
const roundStart = now();
|
|
18449
|
+
diagnosticEvent("round.start", { id, round, maxRounds: options.maxRounds, elapsedMs: elapsed() });
|
|
18450
|
+
let session;
|
|
18451
|
+
try {
|
|
18452
|
+
session = await options.runRound(round, previous, signal);
|
|
18453
|
+
} catch (error) {
|
|
18454
|
+
diagnosticEvent("round.end", { id, round, reason: signal.aborted ? "aborted" : "exception", elapsedMs: elapsed(), durationMs: Math.max(0, now() - roundStart) });
|
|
18455
|
+
throw error;
|
|
18456
|
+
}
|
|
18457
|
+
const current = progress(session);
|
|
18458
|
+
const unchanged = lastFingerprint !== void 0 && current.fingerprint === lastFingerprint;
|
|
18459
|
+
stalls = unchanged ? stalls + 1 : 0;
|
|
18460
|
+
lastFingerprint = current.fingerprint;
|
|
18461
|
+
const classification = session.stopReason === "error" ? options.classifyFailure?.(session) : void 0;
|
|
18462
|
+
consecutiveErrors = session.stopReason === "error" ? consecutiveErrors + 1 : 0;
|
|
18463
|
+
let haltReason;
|
|
18464
|
+
if (session.stopReason === "completed") haltReason = "completed";
|
|
18465
|
+
else if (budget.signal.aborted || elapsed() >= maxElapsedMs) haltReason = "elapsed_budget";
|
|
18466
|
+
else if (signal.aborted || session.stopReason === "aborted") haltReason = "aborted";
|
|
18467
|
+
else if (classification && !classification.retryable) haltReason = "permanent_error";
|
|
18468
|
+
else if (!shouldResume(session.stopReason)) haltReason = "stopped";
|
|
18469
|
+
else if (stalls >= stallLimit) haltReason = "stalled";
|
|
18470
|
+
else if (round >= options.maxRounds) haltReason = "max_rounds";
|
|
18471
|
+
const retryDelay = Math.min(maxDelay, baseDelay * 2 ** Math.min(30, consecutiveErrors - 1));
|
|
18472
|
+
if (!haltReason && session.stopReason === "error" && elapsed() + retryDelay >= maxElapsedMs) haltReason = "elapsed_budget";
|
|
18473
|
+
diagnosticEvent("round.end", {
|
|
18474
|
+
id,
|
|
18475
|
+
round,
|
|
18476
|
+
sessionId: session.id,
|
|
18477
|
+
reason: haltReason ?? session.stopReason,
|
|
18478
|
+
failureKind: classification?.kind,
|
|
18479
|
+
attempt: consecutiveErrors,
|
|
18480
|
+
completedTools: current.completedTools,
|
|
18481
|
+
elapsedMs: elapsed(),
|
|
18482
|
+
durationMs: Math.max(0, now() - roundStart)
|
|
18483
|
+
});
|
|
18484
|
+
if (haltReason === "stalled") diagnosticEvent("round.stall", { id, round, repeats: stalls, completedTools: current.completedTools, elapsedMs: elapsed() });
|
|
18485
|
+
await options.onRoundEnd?.(round, session, !haltReason);
|
|
18486
|
+
if (haltReason) return { session, rounds: round, haltReason };
|
|
18487
|
+
if (session.stopReason === "error") {
|
|
18488
|
+
const delayMs = retryDelay;
|
|
18489
|
+
if (elapsed() + delayMs >= maxElapsedMs) return { session, rounds: round, haltReason: "elapsed_budget" };
|
|
18490
|
+
diagnosticEvent("round.retry", {
|
|
18491
|
+
id,
|
|
18492
|
+
round,
|
|
18493
|
+
nextRound: round + 1,
|
|
18494
|
+
attempt: consecutiveErrors,
|
|
18495
|
+
failureKind: classification?.kind ?? "unclassified",
|
|
18496
|
+
delayMs,
|
|
18497
|
+
elapsedMs: elapsed()
|
|
18498
|
+
});
|
|
18499
|
+
try {
|
|
18500
|
+
await sleep2(delayMs, signal);
|
|
18501
|
+
} catch (error) {
|
|
18502
|
+
if (!signal.aborted) throw error;
|
|
18503
|
+
return { session, rounds: round, haltReason: budget.signal.aborted ? "elapsed_budget" : "aborted" };
|
|
18504
|
+
}
|
|
18505
|
+
}
|
|
18506
|
+
if (signal.aborted || elapsed() >= maxElapsedMs) return { session, rounds: round, haltReason: budget.signal.aborted || elapsed() >= maxElapsedMs ? "elapsed_budget" : "aborted" };
|
|
18507
|
+
previous = session;
|
|
18508
|
+
}
|
|
18509
|
+
} finally {
|
|
18510
|
+
clearTimeout(deadline);
|
|
17966
18511
|
}
|
|
17967
18512
|
}
|
|
17968
18513
|
|
|
@@ -17975,7 +18520,6 @@ function parsePositiveInt(value, flag) {
|
|
|
17975
18520
|
return parsed;
|
|
17976
18521
|
}
|
|
17977
18522
|
var EXIT_INCOMPLETE_RUN = 2;
|
|
17978
|
-
var AUTONOMOUS_ERROR_BACKOFF_MS = 15e3;
|
|
17979
18523
|
var NOTIFY_SUMMARY_CHARS = 1200;
|
|
17980
18524
|
function registerRunCommand(program2) {
|
|
17981
18525
|
program2.command("run").alias("chat").description("Run a one-shot coding agent task").argument("<task...>", "Task for the agent").option("-w, --workspace <path>", "Workspace path", ".").option("--provider <name>", "Provider: ultrac (default) or echo (offline test stub)").option("--model <model>", "Model name").option("--base-url <url>", "Ultrac API base URL (overrides ULTRAC_BASE_URL and the stored login)").option("--mode <name>", `Agent mode: ${agentModeNames.join(", ")}`).option("--max-steps <number>", "Maximum agent steps").option("--no-native-tools", "Use the legacy JSON tool protocol").option("--allow-write", "Allow file writes", false).option("--allow-shell", "Grant the model broad, unsandboxed shell access", false).option("--allow-network", "Allow HTTP(S) URL fetches", false).option("--continue <id>", "Continue from a saved session id").option("--tools <list>", "Tools to advertise: `all`, or a comma-separated list. Default: the mode's set").option("--context-tokens <number>", "Model context window in tokens (default 262144)").option("--max-tool-result-chars <number>", "Cap on one tool result in the transcript (default 6000)").option(
|
|
@@ -17986,7 +18530,7 @@ function registerRunCommand(program2) {
|
|
|
17986
18530
|
"--autonomous",
|
|
17987
18531
|
`Run with no human in the loop: write, shell and network are granted, each round may take up to ${AUTONOMOUS_MAX_STEPS} steps, and a round that runs out of steps or fails resumes on its own. With a phone channel configured (\`ultrac notify setup\`), questions go to your phone and you are told when it finishes.`,
|
|
17988
18532
|
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) => {
|
|
18533
|
+
).option("--max-rounds <number>", `With --autonomous: how many times a run may resume itself (default ${DEFAULT_AUTONOMOUS_MAX_ROUNDS})`).option("--notify", "Send ask_user questions and the final result to your phone (see `ultrac notify setup`)", false).option("--verbose-tools", "Print complete tool results instead of previews", false).option("--json", "Print the session JSON", false).action(async (taskParts, options) => {
|
|
17990
18534
|
const task = taskParts.join(" ").trim();
|
|
17991
18535
|
if (!task) {
|
|
17992
18536
|
throw new Error("A task is required.");
|
|
@@ -18100,14 +18644,17 @@ function registerRunCommand(program2) {
|
|
|
18100
18644
|
shell: options.allowShell || config.permissions.shell,
|
|
18101
18645
|
network: options.allowNetwork || config.permissions.network
|
|
18102
18646
|
};
|
|
18647
|
+
const roundFailures = /* @__PURE__ */ new Map();
|
|
18103
18648
|
let result;
|
|
18104
18649
|
try {
|
|
18105
18650
|
result = await runAutonomousLoop({
|
|
18106
18651
|
maxRounds,
|
|
18107
18652
|
signal: controller.signal,
|
|
18108
|
-
|
|
18653
|
+
classifyFailure: (session) => roundFailures.get(session.id),
|
|
18654
|
+
runRound: async (round, previous, roundSignal) => {
|
|
18109
18655
|
const lineage = previous ? await loadSessionLineage(workspace, previous.id) : previousSessionLineage;
|
|
18110
18656
|
let lastCheckpoint;
|
|
18657
|
+
let checkpointFailed = false;
|
|
18111
18658
|
try {
|
|
18112
18659
|
const { session } = await runner.run({
|
|
18113
18660
|
task: previous ? AUTONOMOUS_CONTINUE_TASK : task,
|
|
@@ -18121,22 +18668,30 @@ function registerRunCommand(program2) {
|
|
|
18121
18668
|
previousSessionLineage: lineage,
|
|
18122
18669
|
...toolSelection ? { tools: toolSelection } : {},
|
|
18123
18670
|
permissions,
|
|
18124
|
-
signal:
|
|
18671
|
+
signal: roundSignal,
|
|
18125
18672
|
...autonomous ? { autonomous: { phone: Boolean(notifier) } } : {},
|
|
18126
18673
|
// Write the session after every step, atomically, so a run that dies
|
|
18127
18674
|
// partway - a crash, a kill, a closed laptop mid-scan - is on disk up to
|
|
18128
18675
|
// its last completed step and can be resumed with --continue rather than
|
|
18129
18676
|
// restarted from nothing.
|
|
18130
18677
|
onCheckpoint: async (session2) => {
|
|
18131
|
-
|
|
18132
|
-
|
|
18678
|
+
try {
|
|
18679
|
+
const checkpointPath = await writeCheckpoint(session2);
|
|
18680
|
+
checkpointFailed = !checkpointPath;
|
|
18681
|
+
if (checkpointPath) lastCheckpoint = await loadSession(workspace, session2.id);
|
|
18682
|
+
} catch (error) {
|
|
18683
|
+
checkpointFailed = true;
|
|
18684
|
+
throw error;
|
|
18685
|
+
}
|
|
18133
18686
|
}
|
|
18134
18687
|
});
|
|
18135
18688
|
return session;
|
|
18136
18689
|
} catch (error) {
|
|
18137
|
-
if (!autonomous || !lastCheckpoint ||
|
|
18690
|
+
if (!autonomous || !lastCheckpoint || checkpointFailed || roundSignal.aborted) throw error;
|
|
18691
|
+
const classification = classifyProviderError(error);
|
|
18692
|
+
roundFailures.set(lastCheckpoint.id, classification);
|
|
18138
18693
|
console.error(
|
|
18139
|
-
chalk12.yellow(`Round ${round} failed
|
|
18694
|
+
chalk12.yellow(`Round ${round} failed (${classification.kind}). The latest checkpoint is saved.`)
|
|
18140
18695
|
);
|
|
18141
18696
|
return { ...lastCheckpoint, stopReason: "error" };
|
|
18142
18697
|
}
|
|
@@ -18149,7 +18704,6 @@ function registerRunCommand(program2) {
|
|
|
18149
18704
|
`Round ${round} stopped (${session.stopReason}); resuming in round ${round + 1} of ${maxRounds}.`
|
|
18150
18705
|
)
|
|
18151
18706
|
);
|
|
18152
|
-
if (session.stopReason === "error") await sleep(AUTONOMOUS_ERROR_BACKOFF_MS, controller.signal);
|
|
18153
18707
|
}
|
|
18154
18708
|
});
|
|
18155
18709
|
} catch (error) {
|
|
@@ -18170,16 +18724,19 @@ function registerRunCommand(program2) {
|
|
|
18170
18724
|
{ cause: error }
|
|
18171
18725
|
);
|
|
18172
18726
|
}
|
|
18727
|
+
if (["stalled", "elapsed_budget", "permanent_error"].includes(result.haltReason ?? "")) {
|
|
18728
|
+
console.error(chalk12.yellow(`Autonomous recovery stopped: ${result.haltReason}. The latest checkpoint is saved.`));
|
|
18729
|
+
}
|
|
18173
18730
|
const stopReason = result.session.stopReason;
|
|
18174
18731
|
for (const warning of getSessionMcpArchiveWarnings(result.session)) {
|
|
18175
18732
|
console.error(chalk12.yellow(`Warning: ${warning}`));
|
|
18176
18733
|
}
|
|
18177
18734
|
if (options.json) {
|
|
18178
18735
|
console.log(
|
|
18179
|
-
JSON.stringify({ sessionPath: sessionPath2, stopReason: stopReason ?? null, session: result.session }, null, 2)
|
|
18736
|
+
JSON.stringify({ sessionPath: sessionPath2, stopReason: stopReason ?? null, haltReason: result.haltReason ?? null, rounds: result.rounds, session: result.session }, null, 2)
|
|
18180
18737
|
);
|
|
18181
18738
|
} else {
|
|
18182
|
-
printSession(result.session, sessionPath2);
|
|
18739
|
+
printSession(result.session, sessionPath2, { verboseTools: options.verboseTools });
|
|
18183
18740
|
}
|
|
18184
18741
|
if (notifier) {
|
|
18185
18742
|
const outcome = stopReason === "completed" ? "finished" : `stopped without finishing (${stopReason ?? "unknown"})`;
|
|
@@ -18433,17 +18990,25 @@ function registerSkillsCommand(program2) {
|
|
|
18433
18990
|
console.log("");
|
|
18434
18991
|
console.log(skill.body);
|
|
18435
18992
|
});
|
|
18436
|
-
skills.command("create").description("Scaffold a new SKILL.md").argument("<name>", "Skill name (kebab-case)").requiredOption("--description <text>", "One-line description").option("--when <text>", "When the agent should use this skill").option("--permissions <list>", "Comma-separated: write,shell,network").option("--scope <scope>", "global or project", "project").option("-w, --workspace <path>", "Workspace path", ".").action(
|
|
18993
|
+
skills.command("create").description("Scaffold a new SKILL.md").argument("<name>", "Skill name (kebab-case)").requiredOption("--description <text>", "One-line description").option("--when <text>", "When the agent should use this skill").option("--permissions <list>", "Comma-separated: write,shell,network").option("--scope <scope>", "global or project", "project").option("--command <name>", "Expose this skill as a custom slash command").option("--argument-hint <text>", "Describe the slash command arguments").option("-w, --workspace <path>", "Workspace path", ".").action(
|
|
18437
18994
|
async (name, options) => {
|
|
18995
|
+
const command = options.command ? normalizeCommandName(options.command) : void 0;
|
|
18996
|
+
if (command && isBuiltinSlashCommand(command)) throw new Error(`/${command} is a built-in command. Choose another skill command name.`);
|
|
18997
|
+
if (options.argumentHint && !command) throw new Error("--argument-hint requires --command.");
|
|
18998
|
+
const argumentHint = options.argumentHint?.trim();
|
|
18999
|
+
if (argumentHint && (argumentHint.length > 256 || /[\r\n]/.test(argumentHint))) throw new Error("Argument hints must be one line, at most 256 characters.");
|
|
18438
19000
|
const skill = await createSkill(resolve13(options.workspace), {
|
|
18439
19001
|
name,
|
|
18440
19002
|
description: options.description,
|
|
18441
19003
|
whenToUse: options.when,
|
|
18442
19004
|
permissions: parsePermissions(options.permissions),
|
|
18443
|
-
scope: parseScope2(options.scope)
|
|
19005
|
+
scope: parseScope2(options.scope),
|
|
19006
|
+
command,
|
|
19007
|
+
argumentHint
|
|
18444
19008
|
});
|
|
18445
19009
|
console.log(`Created ${skill.scope} skill "${skill.name}" at ${skill.path}`);
|
|
18446
19010
|
console.log("Edit that file to add the step-by-step instructions.");
|
|
19011
|
+
if (command) console.log(`Invoke this skill with /${command}${argumentHint ? ` ${argumentHint}` : ""} in a trusted workspace.`);
|
|
18447
19012
|
}
|
|
18448
19013
|
);
|
|
18449
19014
|
skills.command("path").description("Print the skills directory path for a scope").option("-w, --workspace <path>", "Workspace path", ".").option("--scope <scope>", "global or project", "project").action((options) => {
|
|
@@ -19384,7 +19949,7 @@ ${tools.length} of ${all.length} tools advertised in ${options.mode} mode, ${ser
|
|
|
19384
19949
|
);
|
|
19385
19950
|
}
|
|
19386
19951
|
});
|
|
19387
|
-
toolsCommand.command("run").description("Execute one built-in agent tool").argument("<tool>", "Tool name").argument("[input]", "JSON object input", "{}").option("-w, --workspace <path>", "Workspace path", ".").option("--allow-write", "Allow file writes", false).option("--allow-shell", "Allow this tool to execute unsandboxed shell commands", false).option("--allow-network", "Allow HTTP(S) URL fetches", false).option("--result-json", "Print the tool result as JSON", false).action(async (tool, inputText, options) => {
|
|
19952
|
+
toolsCommand.command("run").description("Execute one built-in agent tool").argument("<tool>", "Tool name").argument("[input]", "JSON object input", "{}").option("-w, --workspace <path>", "Workspace path", ".").option("--allow-write", "Allow file writes", false).option("--allow-shell", "Allow this tool to execute unsandboxed shell commands", false).option("--allow-network", "Allow HTTP(S) URL fetches", false).option("--verbose-tools", "Print the complete tool result", false).option("--result-json", "Print the tool result as JSON", false).action(async (tool, inputText, options) => {
|
|
19388
19953
|
const input = parseToolInput(inputText);
|
|
19389
19954
|
const result = await createDefaultToolRegistry().execute(
|
|
19390
19955
|
{ tool, input },
|
|
@@ -19400,7 +19965,7 @@ ${tools.length} of ${all.length} tools advertised in ${options.mode} mode, ${ser
|
|
|
19400
19965
|
if (options.resultJson) {
|
|
19401
19966
|
console.log(JSON.stringify({ result }, null, 2));
|
|
19402
19967
|
} else {
|
|
19403
|
-
printToolResult(result);
|
|
19968
|
+
printToolResult(result, { verboseTools: options.verboseTools });
|
|
19404
19969
|
}
|
|
19405
19970
|
if (!result.ok) {
|
|
19406
19971
|
process.exitCode = 1;
|