@skein-code/cli 0.3.25 → 0.3.27
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 +42 -7
- package/dist/cli.js +1411 -162
- package/dist/cli.js.map +1 -1
- package/docs/ARCHITECTURE.md +34 -18
- package/docs/NEXT_STEPS.md +56 -23
- package/docs/PRODUCT_BENCHMARK.md +5 -5
- package/docs/headless-output.schema.json +36 -0
- package/examples/config.yaml +30 -0
- package/package.json +14 -1
package/dist/cli.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { createInterface as createInterface2 } from "node:readline/promises";
|
|
5
5
|
import { stdin as input, stdout as output } from "node:process";
|
|
6
6
|
import { writeFile as writeFile3 } from "node:fs/promises";
|
|
7
|
-
import { basename as
|
|
7
|
+
import { basename as basename14, resolve as resolve27 } from "node:path";
|
|
8
8
|
import { Command, Option } from "commander";
|
|
9
9
|
import chalk4 from "chalk";
|
|
10
10
|
|
|
@@ -220,7 +220,7 @@ function isSqliteBusy(error) {
|
|
|
220
220
|
// package.json
|
|
221
221
|
var package_default = {
|
|
222
222
|
name: "@skein-code/cli",
|
|
223
|
-
version: "0.3.
|
|
223
|
+
version: "0.3.27",
|
|
224
224
|
description: "A context-first, model-agnostic coding agent with an auditable terminal workspace.",
|
|
225
225
|
type: "module",
|
|
226
226
|
license: "MIT",
|
|
@@ -236,6 +236,19 @@ var package_default = {
|
|
|
236
236
|
},
|
|
237
237
|
skein: {
|
|
238
238
|
releaseNotes: [
|
|
239
|
+
"MCP now follows a no-network search and inspect review before explicit workspace-bound manifest trust and lazy activation",
|
|
240
|
+
"Declarative capability manifests expose redacted source, version, tools, permissions, network, command, path, sensitive-field, process, and completion-evidence effects",
|
|
241
|
+
"Persistent disable and revoke unload active remote schemas while manifest fingerprint changes fail closed to untrusted",
|
|
242
|
+
"Server annotations cannot lower local permissions, named manifests reject injected undeclared schemas, and optional server failures remain isolated",
|
|
243
|
+
"Only explicitly required trusted MCP servers may block startup, while status inspection no longer connects optional servers",
|
|
244
|
+
"Sensitive MCP arguments are redacted from terminal, TUI, JSON, and approval events; unsupported external mutations remain completion-unresolved",
|
|
245
|
+
"Explain and review intents expose smaller built-in schema sets, and MCP activation reports deterministic eager-versus-loaded schema estimates plus top-match evidence",
|
|
246
|
+
"First-run setup now states that the primary agent needs API credentials and that provider subscriptions or signed-in coding CLIs are separate delegated tools",
|
|
247
|
+
"Permission prompts show the policy reason, redacted target, working directory, category risk, and explicit once, session, deny, and stop choices",
|
|
248
|
+
"The Recovery Center joins last-run state, failure repair hints, changed files, checkpoints, diff, audit, rollback, retry, and safe session resume",
|
|
249
|
+
"Read-only review commands pin working-tree, commit, or branch scope and inject a content-free redacted evidence bundle without storing it in the user transcript",
|
|
250
|
+
"JSON and JSONL terminal records now follow the published headless v1 schema with stable completed, verification, input, blocked, cancellation, turn, token, and error exit codes",
|
|
251
|
+
"Short terminal viewports clip long lists without overflow and pending clarifications cannot be consumed by recovery commands",
|
|
239
252
|
"Durable sessions now separate a 250k-token context epoch from a 1m-token lifetime ceiling without changing the session id or deleting transcript history",
|
|
240
253
|
"Content-free epoch handoffs preserve Task Contract criteria, unresolved failure circuits, changed files, and verification receipts across long runs",
|
|
241
254
|
"Intent Sufficiency routes clear requests to execution, repository-inferable gaps to inspection, and genuine product choices to one persisted clarification",
|
|
@@ -1949,8 +1962,22 @@ var agentTeamConfigSchema = z2.object({
|
|
|
1949
1962
|
}).partial();
|
|
1950
1963
|
var mcpServerSchema = z2.object({
|
|
1951
1964
|
enabled: z2.boolean().optional(),
|
|
1965
|
+
required: z2.boolean().optional(),
|
|
1952
1966
|
transport: z2.enum(["stdio", "http"]).optional(),
|
|
1953
1967
|
description: z2.string().min(1).max(500).optional(),
|
|
1968
|
+
version: z2.string().min(1).max(128).optional(),
|
|
1969
|
+
tools: z2.array(z2.object({
|
|
1970
|
+
name: z2.string().min(1).max(256),
|
|
1971
|
+
description: z2.string().min(1).max(500).optional(),
|
|
1972
|
+
permissions: z2.array(z2.enum(["read", "write", "shell", "git", "network"])).min(1).max(5),
|
|
1973
|
+
network: z2.array(z2.string().min(1).max(500)).max(32).optional(),
|
|
1974
|
+
commands: z2.array(z2.string().min(1).max(500)).max(32).optional(),
|
|
1975
|
+
paths: z2.array(z2.string().min(1).max(4e3)).max(64).optional(),
|
|
1976
|
+
sensitiveFields: z2.array(z2.string().min(1).max(256)).max(64).optional(),
|
|
1977
|
+
background: z2.boolean().optional(),
|
|
1978
|
+
processTree: z2.boolean().optional(),
|
|
1979
|
+
completionEvidence: z2.enum(["full", "partial", "none"]).optional()
|
|
1980
|
+
}).strict()).max(256).optional(),
|
|
1954
1981
|
command: z2.string().min(1).max(512).optional(),
|
|
1955
1982
|
args: z2.array(z2.string().max(4e3)).max(64).optional(),
|
|
1956
1983
|
cwd: z2.string().max(4e3).optional(),
|
|
@@ -1959,7 +1986,33 @@ var mcpServerSchema = z2.object({
|
|
|
1959
1986
|
headers: z2.record(z2.string(), z2.string().max(2e4)).optional(),
|
|
1960
1987
|
timeoutMs: z2.number().int().positive().max(3e5).optional(),
|
|
1961
1988
|
toolPrefix: z2.string().regex(/^[a-z][a-z0-9_-]{0,24}$/).optional()
|
|
1962
|
-
}).strict()
|
|
1989
|
+
}).strict().superRefine((server, context) => {
|
|
1990
|
+
const names = /* @__PURE__ */ new Set();
|
|
1991
|
+
for (const [index, tool] of (server.tools ?? []).entries()) {
|
|
1992
|
+
if (names.has(tool.name)) {
|
|
1993
|
+
context.addIssue({
|
|
1994
|
+
code: z2.ZodIssueCode.custom,
|
|
1995
|
+
path: ["tools", index, "name"],
|
|
1996
|
+
message: `duplicate MCP capability tool: ${tool.name}`
|
|
1997
|
+
});
|
|
1998
|
+
}
|
|
1999
|
+
names.add(tool.name);
|
|
2000
|
+
if (tool.permissions.includes("shell") && !tool.commands?.length) {
|
|
2001
|
+
context.addIssue({
|
|
2002
|
+
code: z2.ZodIssueCode.custom,
|
|
2003
|
+
path: ["tools", index, "commands"],
|
|
2004
|
+
message: "MCP shell capability must declare command scopes"
|
|
2005
|
+
});
|
|
2006
|
+
}
|
|
2007
|
+
if (tool.permissions.includes("write") && !tool.paths?.length) {
|
|
2008
|
+
context.addIssue({
|
|
2009
|
+
code: z2.ZodIssueCode.custom,
|
|
2010
|
+
path: ["tools", index, "paths"],
|
|
2011
|
+
message: "MCP write capability must declare path scopes"
|
|
2012
|
+
});
|
|
2013
|
+
}
|
|
2014
|
+
}
|
|
2015
|
+
});
|
|
1963
2016
|
var mcpConfigSchema = z2.object({
|
|
1964
2017
|
enabled: z2.boolean().optional(),
|
|
1965
2018
|
connectTimeoutMs: z2.number().int().positive().max(3e5).optional(),
|
|
@@ -3136,7 +3189,7 @@ function executableNames(command2) {
|
|
|
3136
3189
|
return [command2, ...extensions.map((extension) => `${command2}${extension}`)];
|
|
3137
3190
|
}
|
|
3138
3191
|
function runProcess(command2, args, options) {
|
|
3139
|
-
return new Promise((
|
|
3192
|
+
return new Promise((resolve28, reject) => {
|
|
3140
3193
|
const started = Date.now();
|
|
3141
3194
|
const environment = options.inheritEnv === false ? {} : { ...process.env };
|
|
3142
3195
|
for (const name of options.unsetEnv ?? []) delete environment[name];
|
|
@@ -3228,7 +3281,7 @@ function runProcess(command2, args, options) {
|
|
|
3228
3281
|
reject(callbackError);
|
|
3229
3282
|
return;
|
|
3230
3283
|
}
|
|
3231
|
-
|
|
3284
|
+
resolve28({
|
|
3232
3285
|
command: [command2, ...args].join(" "),
|
|
3233
3286
|
exitCode: code ?? (timedOut ? 124 : 1),
|
|
3234
3287
|
stdout,
|
|
@@ -5563,12 +5616,12 @@ function rankMentionSuggestions(candidates, partial, limit = 6) {
|
|
|
5563
5616
|
}
|
|
5564
5617
|
function rankMention(path, needle) {
|
|
5565
5618
|
const lower = normalizeForMatch(path);
|
|
5566
|
-
const
|
|
5619
|
+
const basename15 = lower.slice(lower.lastIndexOf("/") + 1);
|
|
5567
5620
|
const segments = lower.split("/");
|
|
5568
5621
|
if (!needle) return segments.length * 100 + lower.length;
|
|
5569
5622
|
if (lower === needle) return 0;
|
|
5570
|
-
if (
|
|
5571
|
-
if (
|
|
5623
|
+
if (basename15 === needle) return 10 + lower.length;
|
|
5624
|
+
if (basename15.startsWith(needle)) return 100 + basename15.length + lower.length / 1e3;
|
|
5572
5625
|
if (segments.some((segment) => segment.startsWith(needle))) {
|
|
5573
5626
|
return 200 + lower.indexOf(needle) + lower.length / 1e3;
|
|
5574
5627
|
}
|
|
@@ -6315,11 +6368,11 @@ var CheckpointStore = class {
|
|
|
6315
6368
|
}
|
|
6316
6369
|
async capture(sessionId, paths, options = {}) {
|
|
6317
6370
|
validateIdentifier(sessionId, "session");
|
|
6318
|
-
const
|
|
6319
|
-
if (!
|
|
6320
|
-
return this.withManagedLease(() => this.captureUnlocked(sessionId,
|
|
6371
|
+
const unique4 = [...new Set(paths)];
|
|
6372
|
+
if (!unique4.length) return void 0;
|
|
6373
|
+
return this.withManagedLease(() => this.captureUnlocked(sessionId, unique4, options));
|
|
6321
6374
|
}
|
|
6322
|
-
async captureUnlocked(sessionId,
|
|
6375
|
+
async captureUnlocked(sessionId, unique4, options) {
|
|
6323
6376
|
const id = `${Date.now().toString(36)}-${randomUUID9()}`;
|
|
6324
6377
|
const target = join9(this.directory, sessionId, id);
|
|
6325
6378
|
const blobDirectory = join9(target, "blobs");
|
|
@@ -6328,7 +6381,7 @@ var CheckpointStore = class {
|
|
|
6328
6381
|
await mkdir7(blobDirectory, { recursive: true });
|
|
6329
6382
|
await this.assertManagedPath(blobDirectory);
|
|
6330
6383
|
const entries = [];
|
|
6331
|
-
for (const input2 of
|
|
6384
|
+
for (const input2 of unique4) {
|
|
6332
6385
|
const path = await this.workspace.resolvePath(input2, { allowMissing: true });
|
|
6333
6386
|
try {
|
|
6334
6387
|
const info = await lstat10(path);
|
|
@@ -8607,6 +8660,16 @@ var ToolRegistry = class {
|
|
|
8607
8660
|
has(name) {
|
|
8608
8661
|
return this.tools.has(name) || this.aliases.has(name);
|
|
8609
8662
|
}
|
|
8663
|
+
/** Remove one exact tool instance without letting extensions evict core tools. */
|
|
8664
|
+
unregister(name, expected) {
|
|
8665
|
+
const current = this.tools.get(name);
|
|
8666
|
+
if (!current || expected && current !== expected) return false;
|
|
8667
|
+
this.tools.delete(name);
|
|
8668
|
+
for (const [alias, target] of this.aliases) {
|
|
8669
|
+
if (target === name) this.aliases.delete(alias);
|
|
8670
|
+
}
|
|
8671
|
+
return true;
|
|
8672
|
+
}
|
|
8610
8673
|
list() {
|
|
8611
8674
|
return [...this.tools.values()];
|
|
8612
8675
|
}
|
|
@@ -11489,6 +11552,7 @@ ${resolvedInput.decision}
|
|
|
11489
11552
|
this.session.audit ?? [],
|
|
11490
11553
|
this.session.duplicationSuppressions ?? []
|
|
11491
11554
|
).size > 0,
|
|
11555
|
+
turnDirective.intent,
|
|
11492
11556
|
request,
|
|
11493
11557
|
loadedProgressiveTools
|
|
11494
11558
|
);
|
|
@@ -11785,7 +11849,8 @@ ${input2}`
|
|
|
11785
11849
|
}
|
|
11786
11850
|
await this.persist();
|
|
11787
11851
|
throwIfAborted(options.signal);
|
|
11788
|
-
|
|
11852
|
+
const displayCall = redactToolCallForDisplay(call, tool.definition.sensitiveFields ?? []);
|
|
11853
|
+
await emit({ type: "tool_start", call: displayCall, category: tool.definition.category });
|
|
11789
11854
|
const executionContext = {
|
|
11790
11855
|
config: this.config,
|
|
11791
11856
|
workspace: this.workspace,
|
|
@@ -11975,13 +12040,17 @@ ${completeContent}`;
|
|
|
11975
12040
|
this.recordPermission(call, category, "allow", "Approved for this session.");
|
|
11976
12041
|
return true;
|
|
11977
12042
|
}
|
|
11978
|
-
|
|
12043
|
+
const displayCall = redactToolCallForDisplay(
|
|
12044
|
+
call,
|
|
12045
|
+
this.tools.get(call.name)?.definition.sensitiveFields ?? []
|
|
12046
|
+
);
|
|
12047
|
+
await emit({ type: "permission", call: displayCall, category, reason: decision.reason });
|
|
11979
12048
|
if (!options.requestPermission) {
|
|
11980
12049
|
this.recordPermission(call, category, "deny", "No permission handler was available.");
|
|
11981
12050
|
return false;
|
|
11982
12051
|
}
|
|
11983
12052
|
try {
|
|
11984
|
-
const grant = await options.requestPermission(
|
|
12053
|
+
const grant = await options.requestPermission(displayCall, category, decision.reason);
|
|
11985
12054
|
const allowed = grant === true || grant === "session";
|
|
11986
12055
|
if (grant === "session") this.sessionApprovals.add(approvalKey);
|
|
11987
12056
|
this.recordPermission(
|
|
@@ -11996,13 +12065,13 @@ ${completeContent}`;
|
|
|
11996
12065
|
return false;
|
|
11997
12066
|
}
|
|
11998
12067
|
}
|
|
11999
|
-
recordPermission(call, category,
|
|
12068
|
+
recordPermission(call, category, outcome2, reason) {
|
|
12000
12069
|
this.appendAudit({
|
|
12001
12070
|
type: "permission",
|
|
12002
12071
|
toolCallId: call.id,
|
|
12003
12072
|
tool: call.name,
|
|
12004
12073
|
category,
|
|
12005
|
-
outcome,
|
|
12074
|
+
outcome: outcome2,
|
|
12006
12075
|
reason
|
|
12007
12076
|
});
|
|
12008
12077
|
}
|
|
@@ -12387,6 +12456,20 @@ function combineMeasurementSources(input2, output2, inputTokens, outputTokens) {
|
|
|
12387
12456
|
function uniqueCategories(categories) {
|
|
12388
12457
|
return [...new Set(categories)];
|
|
12389
12458
|
}
|
|
12459
|
+
function redactToolCallForDisplay(call, sensitiveFields) {
|
|
12460
|
+
if (!sensitiveFields.length) return call;
|
|
12461
|
+
const sensitive = new Set(sensitiveFields.map((field) => field.toLocaleLowerCase()));
|
|
12462
|
+
const redact2 = (value, path = []) => {
|
|
12463
|
+
if (Array.isArray(value)) return value.map((item, index) => redact2(item, [...path, String(index)]));
|
|
12464
|
+
if (!value || typeof value !== "object") return value;
|
|
12465
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => {
|
|
12466
|
+
const fieldPath = [...path, key].join(".").toLocaleLowerCase();
|
|
12467
|
+
const field = key.toLocaleLowerCase();
|
|
12468
|
+
return [key, sensitive.has(field) || sensitive.has(fieldPath) ? "<redacted>" : redact2(item, [...path, key])];
|
|
12469
|
+
}));
|
|
12470
|
+
};
|
|
12471
|
+
return { ...call, arguments: redact2(call.arguments) };
|
|
12472
|
+
}
|
|
12390
12473
|
function failedResult(call, content, failure) {
|
|
12391
12474
|
return {
|
|
12392
12475
|
toolCallId: call.id,
|
|
@@ -12402,9 +12485,9 @@ function failureSignatureFromMetadata(value) {
|
|
|
12402
12485
|
const signature = value.signature;
|
|
12403
12486
|
return typeof signature === "string" && signature ? signature : void 0;
|
|
12404
12487
|
}
|
|
12405
|
-
function visibleToolDefinitions(tools, askMode, contractEnabled, artifactReadAvailable, duplicationAvailable, request, loadedProgressiveTools) {
|
|
12488
|
+
function visibleToolDefinitions(tools, askMode, contractEnabled, artifactReadAvailable, duplicationAvailable, intent, request, loadedProgressiveTools) {
|
|
12406
12489
|
const eligible = tools.definitions().filter(
|
|
12407
|
-
(tool) => (!askMode || tool.category === "read") && (contractEnabled || tool.name !== "task_contract") && (artifactReadAvailable || tool.name !== "read_tool_artifact") && (duplicationAvailable || tool.name !== "duplication_audit")
|
|
12490
|
+
(tool) => (!askMode || tool.category === "read") && intentAllowsTool(intent, tool) && (contractEnabled || tool.name !== "task_contract") && (artifactReadAvailable || tool.name !== "read_tool_artifact") && (duplicationAvailable || tool.name !== "duplication_audit")
|
|
12408
12491
|
);
|
|
12409
12492
|
const progressive = eligible.filter((tool) => tool.progressive);
|
|
12410
12493
|
if (progressive.length <= 8) return eligible;
|
|
@@ -12413,6 +12496,26 @@ function visibleToolDefinitions(tools, askMode, contractEnabled, artifactReadAva
|
|
|
12413
12496
|
}
|
|
12414
12497
|
return eligible.filter((tool) => !tool.progressive || loadedProgressiveTools.has(tool.name));
|
|
12415
12498
|
}
|
|
12499
|
+
function intentAllowsTool(intent, tool) {
|
|
12500
|
+
if (!BUILTIN_TOOL_NAMES.has(tool.name)) return true;
|
|
12501
|
+
if (intent === "explain") return tool.category === "read";
|
|
12502
|
+
if (intent === "review") return tool.category === "read" || tool.name === "git";
|
|
12503
|
+
return true;
|
|
12504
|
+
}
|
|
12505
|
+
var BUILTIN_TOOL_NAMES = /* @__PURE__ */ new Set([
|
|
12506
|
+
"read_file",
|
|
12507
|
+
"read_tool_artifact",
|
|
12508
|
+
"list_files",
|
|
12509
|
+
"search_code",
|
|
12510
|
+
"write_file",
|
|
12511
|
+
"apply_patch",
|
|
12512
|
+
"shell",
|
|
12513
|
+
"git",
|
|
12514
|
+
"task",
|
|
12515
|
+
"task_contract",
|
|
12516
|
+
"duplication_audit",
|
|
12517
|
+
"working_memory"
|
|
12518
|
+
]);
|
|
12416
12519
|
function selectProgressiveTools(tools, request, limit) {
|
|
12417
12520
|
const terms = new Set(request.toLocaleLowerCase().match(/[\p{L}\p{N}_-]{2,}/gu) ?? []);
|
|
12418
12521
|
return tools.map((tool) => {
|
|
@@ -13464,6 +13567,8 @@ var DelegationManager = class {
|
|
|
13464
13567
|
return {
|
|
13465
13568
|
definition: {
|
|
13466
13569
|
name: "delegate",
|
|
13570
|
+
source: "agent",
|
|
13571
|
+
activation: "always",
|
|
13467
13572
|
description: "Run independent read-only investigations with specialized isolated agents and return concise evidence-backed summaries.",
|
|
13468
13573
|
category: "read",
|
|
13469
13574
|
inputSchema: jsonSchema({
|
|
@@ -13511,6 +13616,8 @@ var DelegationManager = class {
|
|
|
13511
13616
|
return {
|
|
13512
13617
|
definition: {
|
|
13513
13618
|
name: "team_run",
|
|
13619
|
+
source: "agent",
|
|
13620
|
+
activation: "always",
|
|
13514
13621
|
description: "Run a visible multi-model council: parallel read-only specialists share findings, a reviewer challenges them, and one bounded revision round runs before returning an acceptance report.",
|
|
13515
13622
|
category: "read",
|
|
13516
13623
|
inputSchema: jsonSchema({
|
|
@@ -13555,6 +13662,8 @@ var DelegationManager = class {
|
|
|
13555
13662
|
return {
|
|
13556
13663
|
definition: {
|
|
13557
13664
|
name: "writer_run",
|
|
13665
|
+
source: "agent",
|
|
13666
|
+
activation: "always",
|
|
13558
13667
|
description: "Create one reviewed patch in a disposable Git worktree. This never changes the main workspace; use writer_integrate explicitly after review.",
|
|
13559
13668
|
category: "write",
|
|
13560
13669
|
inputSchema: jsonSchema({
|
|
@@ -13587,6 +13696,8 @@ var DelegationManager = class {
|
|
|
13587
13696
|
return {
|
|
13588
13697
|
definition: {
|
|
13589
13698
|
name: "writer_integrate",
|
|
13699
|
+
source: "agent",
|
|
13700
|
+
activation: "always",
|
|
13590
13701
|
description: "Explicitly apply one accepted writer patch to the main workspace after SHA, HEAD, cleanliness, path, and checkpoint gates pass.",
|
|
13591
13702
|
category: "write",
|
|
13592
13703
|
inputSchema: jsonSchema({
|
|
@@ -13649,18 +13760,18 @@ var DelegationManager = class {
|
|
|
13649
13760
|
await this.recordAgent(board.id, writer, "write");
|
|
13650
13761
|
const writerFailed = !writer.ok || !draft.patch || !draft.worktreeCleaned || signal?.aborted;
|
|
13651
13762
|
if (writerFailed) {
|
|
13652
|
-
const
|
|
13763
|
+
const outcome2 = writer.termination === "cancelled" || signal?.aborted ? "cancelled" : "failed";
|
|
13653
13764
|
await this.teamStore.recordWriterLane(board.id, {
|
|
13654
13765
|
profile: profile.name,
|
|
13655
13766
|
reviewer: reviewer.name,
|
|
13656
13767
|
baseCommit: draft.baseCommit,
|
|
13657
|
-
outcome,
|
|
13768
|
+
outcome: outcome2,
|
|
13658
13769
|
patch: draft.patch,
|
|
13659
13770
|
files: draft.files,
|
|
13660
13771
|
worktreeCleaned: draft.worktreeCleaned
|
|
13661
13772
|
});
|
|
13662
13773
|
await this.teamStore.complete(board.id, { accepted: false, reviewRounds: 0, failed: true });
|
|
13663
|
-
const status2 =
|
|
13774
|
+
const status2 = outcome2 === "cancelled" ? "cancelled" : "failed";
|
|
13664
13775
|
const detail2 = !draft.worktreeCleaned ? "Writer worktree cleanup could not be verified; integration is blocked." : !draft.patch ? "Writer returned no patch." : writer.summary;
|
|
13665
13776
|
await emit?.({ type: "writer_lane", id: board.id, status: status2, detail: detail2, files: draft.files });
|
|
13666
13777
|
await emit?.({ type: "team_done", id: board.id, accepted: false, reviewRounds: 0 });
|
|
@@ -15009,6 +15120,61 @@ function keyEnvironmentName(provider) {
|
|
|
15009
15120
|
// src/cli/output.ts
|
|
15010
15121
|
import { createInterface } from "node:readline/promises";
|
|
15011
15122
|
import chalk2, { Chalk } from "chalk";
|
|
15123
|
+
|
|
15124
|
+
// src/cli/headless-contract.ts
|
|
15125
|
+
var HEADLESS_SCHEMA_VERSION = 1;
|
|
15126
|
+
var HEADLESS_EXIT_CODES = {
|
|
15127
|
+
completed: 0,
|
|
15128
|
+
error: 1,
|
|
15129
|
+
needsInput: 2,
|
|
15130
|
+
unverified: 3,
|
|
15131
|
+
verificationFailed: 4,
|
|
15132
|
+
blocked: 5,
|
|
15133
|
+
cancelled: 6,
|
|
15134
|
+
maxTurns: 7,
|
|
15135
|
+
tokenBudget: 8
|
|
15136
|
+
};
|
|
15137
|
+
function resolveHeadlessOutcome(input2) {
|
|
15138
|
+
if (input2.error !== void 0 || input2.reason === "error") {
|
|
15139
|
+
return outcome("error", HEADLESS_EXIT_CODES.error, input2.reason ?? "error");
|
|
15140
|
+
}
|
|
15141
|
+
if (input2.reason === "needs_input") {
|
|
15142
|
+
return outcome("needs_input", HEADLESS_EXIT_CODES.needsInput, input2.reason);
|
|
15143
|
+
}
|
|
15144
|
+
if (input2.reason === "blocked" || input2.completion?.acceptance?.state === "blocked") {
|
|
15145
|
+
return outcome("blocked", HEADLESS_EXIT_CODES.blocked, input2.reason ?? "blocked");
|
|
15146
|
+
}
|
|
15147
|
+
if (input2.reason === "aborted" || input2.reason === "cancelled") {
|
|
15148
|
+
return outcome("cancelled", HEADLESS_EXIT_CODES.cancelled, input2.reason);
|
|
15149
|
+
}
|
|
15150
|
+
if (input2.reason === "max_turns") {
|
|
15151
|
+
return outcome("max_turns", HEADLESS_EXIT_CODES.maxTurns, input2.reason);
|
|
15152
|
+
}
|
|
15153
|
+
if (input2.reason === "token_budget") {
|
|
15154
|
+
return outcome("token_budget", HEADLESS_EXIT_CODES.tokenBudget, input2.reason);
|
|
15155
|
+
}
|
|
15156
|
+
if (input2.reason === "verification_failed" || input2.completion?.status === "verification_failed") {
|
|
15157
|
+
return outcome("verification_failed", HEADLESS_EXIT_CODES.verificationFailed, input2.reason ?? "verification_failed");
|
|
15158
|
+
}
|
|
15159
|
+
if (input2.reason === "unverified" || input2.completion?.status === "unverified") {
|
|
15160
|
+
return outcome("unverified", HEADLESS_EXIT_CODES.unverified, input2.reason ?? "unverified");
|
|
15161
|
+
}
|
|
15162
|
+
if (input2.completion?.status === "verified") {
|
|
15163
|
+
return outcome("verified", HEADLESS_EXIT_CODES.completed, input2.reason ?? "completed");
|
|
15164
|
+
}
|
|
15165
|
+
return outcome("completed", HEADLESS_EXIT_CODES.completed, input2.reason ?? "completed");
|
|
15166
|
+
}
|
|
15167
|
+
function outcome(status, exitCode, reason) {
|
|
15168
|
+
return {
|
|
15169
|
+
schemaVersion: HEADLESS_SCHEMA_VERSION,
|
|
15170
|
+
ok: exitCode === HEADLESS_EXIT_CODES.completed,
|
|
15171
|
+
status,
|
|
15172
|
+
exitCode,
|
|
15173
|
+
reason
|
|
15174
|
+
};
|
|
15175
|
+
}
|
|
15176
|
+
|
|
15177
|
+
// src/cli/output.ts
|
|
15012
15178
|
var HeadlessReporter = class {
|
|
15013
15179
|
constructor(options) {
|
|
15014
15180
|
this.options = options;
|
|
@@ -15018,7 +15184,6 @@ var HeadlessReporter = class {
|
|
|
15018
15184
|
options;
|
|
15019
15185
|
finalResponse = "";
|
|
15020
15186
|
tools = [];
|
|
15021
|
-
eventError;
|
|
15022
15187
|
context;
|
|
15023
15188
|
streamedAssistant = false;
|
|
15024
15189
|
completion;
|
|
@@ -15028,7 +15193,6 @@ var HeadlessReporter = class {
|
|
|
15028
15193
|
onEvent = (event) => {
|
|
15029
15194
|
if (event.type === "assistant") this.finalResponse = event.content;
|
|
15030
15195
|
if (event.type === "tool_result") this.tools.push(event.result);
|
|
15031
|
-
if (event.type === "error") this.eventError = event.error.message;
|
|
15032
15196
|
if (event.type === "done") {
|
|
15033
15197
|
this.doneReason = event.reason;
|
|
15034
15198
|
this.completion = event.completion;
|
|
@@ -15046,26 +15210,33 @@ var HeadlessReporter = class {
|
|
|
15046
15210
|
this.printText(event);
|
|
15047
15211
|
};
|
|
15048
15212
|
finish(session) {
|
|
15213
|
+
const completion = this.completion ?? session.lastRun;
|
|
15214
|
+
const reason = this.doneReason ?? session.lastRun?.reason;
|
|
15215
|
+
const outcome2 = resolveHeadlessOutcome({
|
|
15216
|
+
...reason ? { reason } : {},
|
|
15217
|
+
...completion ? { completion } : {}
|
|
15218
|
+
});
|
|
15049
15219
|
if (this.options.format === "stream-json") {
|
|
15050
15220
|
process.stdout.write(`${JSON.stringify({
|
|
15051
15221
|
type: "session",
|
|
15222
|
+
...outcome2,
|
|
15052
15223
|
session: sessionSummary(session)
|
|
15053
15224
|
})}
|
|
15054
15225
|
`);
|
|
15055
|
-
return;
|
|
15226
|
+
return outcome2;
|
|
15056
15227
|
}
|
|
15057
15228
|
if (this.options.format === "json") {
|
|
15058
15229
|
process.stdout.write(`${JSON.stringify({
|
|
15059
|
-
|
|
15230
|
+
type: "result",
|
|
15231
|
+
...outcome2,
|
|
15060
15232
|
response: this.finalResponse,
|
|
15061
15233
|
session: sessionSummary(session),
|
|
15062
|
-
...
|
|
15063
|
-
...this.completion ? { completion: this.completion } : {},
|
|
15234
|
+
...completion ? { completion } : {},
|
|
15064
15235
|
...this.context ? { context: this.context } : {},
|
|
15065
15236
|
tools: this.tools
|
|
15066
15237
|
}, null, 2)}
|
|
15067
15238
|
`);
|
|
15068
|
-
return;
|
|
15239
|
+
return outcome2;
|
|
15069
15240
|
}
|
|
15070
15241
|
if (this.options.quiet && this.finalResponse.trim()) {
|
|
15071
15242
|
process.stdout.write(`${this.finalResponse.trim()}
|
|
@@ -15080,17 +15251,34 @@ ${this.glyphs.meta} ${session.changedFiles.length} changed files ${this.glyphs.s
|
|
|
15080
15251
|
`
|
|
15081
15252
|
));
|
|
15082
15253
|
}
|
|
15254
|
+
return outcome2;
|
|
15083
15255
|
}
|
|
15084
|
-
fail(error) {
|
|
15256
|
+
fail(error, session) {
|
|
15085
15257
|
const message2 = error instanceof Error ? error.message : String(error);
|
|
15086
|
-
|
|
15087
|
-
if (this.options.format === "
|
|
15088
|
-
process.stdout.write(`${JSON.stringify({
|
|
15258
|
+
const outcome2 = resolveHeadlessOutcome({ reason: "error", error });
|
|
15259
|
+
if (this.options.format === "stream-json") {
|
|
15260
|
+
process.stdout.write(`${JSON.stringify({
|
|
15261
|
+
type: "final",
|
|
15262
|
+
...outcome2,
|
|
15263
|
+
error: message2,
|
|
15264
|
+
...session ? { session: sessionSummary(session) } : {}
|
|
15265
|
+
})}
|
|
15089
15266
|
`);
|
|
15090
|
-
return;
|
|
15267
|
+
return outcome2;
|
|
15268
|
+
}
|
|
15269
|
+
if (this.options.format === "json") {
|
|
15270
|
+
process.stdout.write(`${JSON.stringify({
|
|
15271
|
+
type: "result",
|
|
15272
|
+
...outcome2,
|
|
15273
|
+
error: message2,
|
|
15274
|
+
...session ? { session: sessionSummary(session) } : {}
|
|
15275
|
+
}, null, 2)}
|
|
15276
|
+
`);
|
|
15277
|
+
return outcome2;
|
|
15091
15278
|
}
|
|
15092
15279
|
process.stderr.write(`${this.paint.red(this.glyphs.error)} ${message2}
|
|
15093
15280
|
`);
|
|
15281
|
+
return outcome2;
|
|
15094
15282
|
}
|
|
15095
15283
|
printText(event) {
|
|
15096
15284
|
const { quiet, compact: compact3 } = this.options;
|
|
@@ -15197,6 +15385,7 @@ ${this.glyphs.meta} ${session.changedFiles.length} changed files ${this.glyphs.s
|
|
|
15197
15385
|
break;
|
|
15198
15386
|
case "done":
|
|
15199
15387
|
this.printCompletion(event.completion);
|
|
15388
|
+
this.printStopReason(event.reason);
|
|
15200
15389
|
break;
|
|
15201
15390
|
case "error":
|
|
15202
15391
|
break;
|
|
@@ -15227,8 +15416,20 @@ ${this.glyphs.meta} ${session.changedFiles.length} changed files ${this.glyphs.s
|
|
|
15227
15416
|
`
|
|
15228
15417
|
));
|
|
15229
15418
|
}
|
|
15419
|
+
printStopReason(reason) {
|
|
15420
|
+
if (reason === "aborted") {
|
|
15421
|
+
process.stderr.write(this.paint.yellow(`${this.glyphs.warning} cancelled ${this.glyphs.separator} resume with --resume after inspecting the saved session
|
|
15422
|
+
`));
|
|
15423
|
+
} else if (reason === "max_turns") {
|
|
15424
|
+
process.stderr.write(this.paint.yellow(`${this.glyphs.warning} turn limit reached ${this.glyphs.separator} resume with --resume and a larger --max-turns value
|
|
15425
|
+
`));
|
|
15426
|
+
} else if (reason === "token_budget") {
|
|
15427
|
+
process.stderr.write(this.paint.yellow(`${this.glyphs.warning} token budget reached ${this.glyphs.separator} inspect the saved session before resuming with a larger --token-budget
|
|
15428
|
+
`));
|
|
15429
|
+
}
|
|
15430
|
+
}
|
|
15230
15431
|
};
|
|
15231
|
-
async function askConsolePermission(call, category, color = !process.env.NO_COLOR) {
|
|
15432
|
+
async function askConsolePermission(call, category, color = !process.env.NO_COLOR, reason = `${category} tools require approval.`) {
|
|
15232
15433
|
if (!process.stdin.isTTY || !process.stderr.isTTY) return false;
|
|
15233
15434
|
const paint = color ? chalk2 : new Chalk({ level: 0 });
|
|
15234
15435
|
const glyphs = resolveCliGlyphs();
|
|
@@ -15236,6 +15437,10 @@ async function askConsolePermission(call, category, color = !process.env.NO_COLO
|
|
|
15236
15437
|
${paint.yellow("Permission required")} ${paint.dim(`(${category})`)}
|
|
15237
15438
|
`);
|
|
15238
15439
|
process.stderr.write(`${paint.bold(call.name)}${formatToolDetail(call, paint)}
|
|
15440
|
+
`);
|
|
15441
|
+
process.stderr.write(`${paint.dim(`Reason: ${reason}`)}
|
|
15442
|
+
`);
|
|
15443
|
+
process.stderr.write(`${paint.yellow(`Risk: ${permissionRisk(category)}`)}
|
|
15239
15444
|
`);
|
|
15240
15445
|
const readline = createInterface({ input: process.stdin, output: process.stderr });
|
|
15241
15446
|
try {
|
|
@@ -15245,6 +15450,13 @@ ${paint.yellow("Permission required")} ${paint.dim(`(${category})`)}
|
|
|
15245
15450
|
readline.close();
|
|
15246
15451
|
}
|
|
15247
15452
|
}
|
|
15453
|
+
function permissionRisk(category) {
|
|
15454
|
+
if (category === "read") return "workspace content may enter model context";
|
|
15455
|
+
if (category === "write") return "workspace files may be created, replaced, or deleted";
|
|
15456
|
+
if (category === "shell") return "a local process may read or change workspace state";
|
|
15457
|
+
if (category === "git") return "repository state or remotes may change";
|
|
15458
|
+
return "data may leave this machine or remote state may change";
|
|
15459
|
+
}
|
|
15248
15460
|
function eventToJson(event) {
|
|
15249
15461
|
if (event.type === "error") {
|
|
15250
15462
|
return { type: event.type, error: event.error.message };
|
|
@@ -15350,7 +15562,7 @@ function commandNames(command2) {
|
|
|
15350
15562
|
// src/ui/tui.tsx
|
|
15351
15563
|
import { useCallback, useEffect as useEffect2, useMemo, useRef as useRef2, useState as useState2 } from "react";
|
|
15352
15564
|
import { Box as Box2, render as render2, Text as Text3, useApp, useInput as useInput2, useWindowSize } from "ink";
|
|
15353
|
-
import { relative as
|
|
15565
|
+
import { relative as relative10 } from "node:path";
|
|
15354
15566
|
|
|
15355
15567
|
// src/ui/components.tsx
|
|
15356
15568
|
import React2 from "react";
|
|
@@ -15369,11 +15581,13 @@ var commandDefinitions = [
|
|
|
15369
15581
|
command("skills", "List discovered task playbooks"),
|
|
15370
15582
|
command("agents", "List built-in and installed expert profiles"),
|
|
15371
15583
|
command("connections", "Inspect shared model endpoints and setup status", "/connections [setup]"),
|
|
15372
|
-
command("mcp", "
|
|
15584
|
+
command("mcp", "Search, inspect, trust, activate, disable, or revoke MCP capabilities", "/mcp [search|inspect|trust|activate|disable|revoke] [...]"),
|
|
15373
15585
|
command("tools", "List built-in and MCP tools with permission categories"),
|
|
15374
15586
|
command("permissions", "Inspect the active permission policy"),
|
|
15375
15587
|
command("changes", "List files changed in the active session"),
|
|
15376
15588
|
command("diff", "Open the current workspace diff in the transcript"),
|
|
15589
|
+
command("review", "Run a read-only review with a fixed, redacted scope", "/review [working-tree|commit <ref>|branch <base-ref>]"),
|
|
15590
|
+
command("recover", "Open recovery actions for the last incomplete run", "/recover [retry|resume|diff|rollback|audit]"),
|
|
15377
15591
|
command("checkpoints", "List recoverable pre-mutation snapshots"),
|
|
15378
15592
|
command("audit", "Review the hash-chained tool and permission timeline"),
|
|
15379
15593
|
command("rollback", "Restore workspace files from a checkpoint", "/rollback [checkpoint-id]"),
|
|
@@ -15423,6 +15637,21 @@ function commandSuggestions(input2, options = {}) {
|
|
|
15423
15637
|
description: item.description
|
|
15424
15638
|
}));
|
|
15425
15639
|
}
|
|
15640
|
+
if (firstSpace >= 0 && commandName === "mcp") {
|
|
15641
|
+
const query = argument.trim().toLocaleLowerCase();
|
|
15642
|
+
return [
|
|
15643
|
+
{ name: "search", description: "Search redacted local capability manifests" },
|
|
15644
|
+
{ name: "inspect", description: "Review one manifest before trust" },
|
|
15645
|
+
{ name: "trust", description: "Trust the current manifest after confirmation" },
|
|
15646
|
+
{ name: "activate", description: "Connect a trusted server and load relevant schemas" },
|
|
15647
|
+
{ name: "disable", description: "Persistently disable a capability" },
|
|
15648
|
+
{ name: "revoke", description: "Revoke trust after confirmation" }
|
|
15649
|
+
].filter((item) => item.name.includes(query)).map((item) => ({
|
|
15650
|
+
value: `/mcp ${item.name} `,
|
|
15651
|
+
label: item.name,
|
|
15652
|
+
description: item.description
|
|
15653
|
+
}));
|
|
15654
|
+
}
|
|
15426
15655
|
if (firstSpace >= 0 && commandName === "connections") {
|
|
15427
15656
|
return [{
|
|
15428
15657
|
value: "/connections setup",
|
|
@@ -15430,6 +15659,28 @@ function commandSuggestions(input2, options = {}) {
|
|
|
15430
15659
|
description: "Show the secure shared-connection setup command"
|
|
15431
15660
|
}].filter((item) => item.label.includes(argument.trim().toLocaleLowerCase()));
|
|
15432
15661
|
}
|
|
15662
|
+
if (firstSpace >= 0 && commandName === "review") {
|
|
15663
|
+
const query = argument.trim().toLocaleLowerCase();
|
|
15664
|
+
return [
|
|
15665
|
+
{ value: "/review working-tree", label: "working-tree", description: "Review only the current working tree" },
|
|
15666
|
+
{ value: "/review commit ", label: "commit", description: "Review exactly one Git commit" },
|
|
15667
|
+
{ value: "/review branch ", label: "branch", description: "Review the current branch against one base ref" }
|
|
15668
|
+
].filter((item) => item.label.includes(query) || item.value.slice("/review ".length).startsWith(query));
|
|
15669
|
+
}
|
|
15670
|
+
if (firstSpace >= 0 && commandName === "recover") {
|
|
15671
|
+
const query = argument.trim().toLocaleLowerCase();
|
|
15672
|
+
return [
|
|
15673
|
+
{ name: "retry", description: "Repair and retry the latest failed operation once" },
|
|
15674
|
+
{ name: "resume", description: "Continue the most recent incomplete logical run" },
|
|
15675
|
+
{ name: "diff", description: "Inspect the current workspace patch" },
|
|
15676
|
+
{ name: "audit", description: "Review permission and tool evidence" },
|
|
15677
|
+
{ name: "rollback", description: "Choose a checkpoint to restore" }
|
|
15678
|
+
].filter((item) => item.name.includes(query)).map((item) => ({
|
|
15679
|
+
value: `/recover ${item.name}`,
|
|
15680
|
+
label: item.name,
|
|
15681
|
+
description: item.description
|
|
15682
|
+
}));
|
|
15683
|
+
}
|
|
15433
15684
|
if (firstSpace >= 0 && commandName === "memory") {
|
|
15434
15685
|
const query = argument.trim().toLocaleLowerCase();
|
|
15435
15686
|
return [
|
|
@@ -16407,7 +16658,7 @@ function TaskRail({ tasks, width = 80, glyphMode = "auto", maxItems }) {
|
|
|
16407
16658
|
] }) : null
|
|
16408
16659
|
] });
|
|
16409
16660
|
}
|
|
16410
|
-
function PermissionCard({ call, category, width = 80, glyphMode = "auto", workspace, compact: compact3 = false }) {
|
|
16661
|
+
function PermissionCard({ call, category, reason, width = 80, glyphMode = "auto", workspace, compact: compact3 = false }) {
|
|
16411
16662
|
const theme = useTheme();
|
|
16412
16663
|
const glyphs = resolveGlyphs(glyphMode);
|
|
16413
16664
|
const summary = permissionSummary(call);
|
|
@@ -16415,7 +16666,9 @@ function PermissionCard({ call, category, width = 80, glyphMode = "auto", worksp
|
|
|
16415
16666
|
const innerWidth = Math.max(1, rowWidth - 2);
|
|
16416
16667
|
const title = truncateDisplay(`Permission required ${glyphs.separator} ${category}`, innerWidth);
|
|
16417
16668
|
const tool = truncateDisplay(`tool ${sanitizeInlineTerminalText(call.name)}`, innerWidth);
|
|
16418
|
-
const summaryLine = truncateDisplay(
|
|
16669
|
+
const summaryLine = truncateDisplay(`target ${summary.label} ${summary.value}`, innerWidth);
|
|
16670
|
+
const reasonLine = truncateDisplay(`reason ${redactPermissionText(reason ?? `${category} tools require approval.`)}`, innerWidth);
|
|
16671
|
+
const riskLine = truncateDisplay(`risk ${permissionRisk2(category)}`, innerWidth);
|
|
16419
16672
|
const argumentCwd = typeof call.arguments.cwd === "string" ? call.arguments.cwd : void 0;
|
|
16420
16673
|
const cwd = sanitizeInlineTerminalText(argumentCwd || workspace || "");
|
|
16421
16674
|
const shortcuts = [
|
|
@@ -16440,6 +16693,8 @@ function PermissionCard({ call, category, width = 80, glyphMode = "auto", worksp
|
|
|
16440
16693
|
/* @__PURE__ */ jsx(PermissionLine, { marker, children: /* @__PURE__ */ jsx(Text, { bold: true, color: theme.warning, children: title }) }),
|
|
16441
16694
|
/* @__PURE__ */ jsx(PermissionLine, { marker, children: /* @__PURE__ */ jsx(Text, { color: theme.muted, children: tool }) }),
|
|
16442
16695
|
/* @__PURE__ */ jsx(PermissionLine, { marker, children: /* @__PURE__ */ jsx(Text, { color: theme.text, children: summaryLine }) }),
|
|
16696
|
+
/* @__PURE__ */ jsx(PermissionLine, { marker, children: /* @__PURE__ */ jsx(Text, { color: theme.muted, children: reasonLine }) }),
|
|
16697
|
+
/* @__PURE__ */ jsx(PermissionLine, { marker, children: /* @__PURE__ */ jsx(Text, { color: theme.warning, children: riskLine }) }),
|
|
16443
16698
|
cwd ? /* @__PURE__ */ jsx(PermissionLine, { marker, children: /* @__PURE__ */ jsx(Text, { color: theme.muted, children: truncateDisplay(`cwd ${compactDisplayPath(cwd, Math.max(1, innerWidth - 4))}`, innerWidth) }) }) : null,
|
|
16444
16699
|
rowWidth >= 64 ? /* @__PURE__ */ jsx(Box, { paddingLeft: 2, children: /* @__PURE__ */ jsx(InlineRow, { parts: shortcuts, width: innerWidth, separator: ` ${glyphs.separator} `, separatorColor: theme.border }) }) : rowWidth >= 28 ? /* @__PURE__ */ jsxs(Box, { paddingLeft: 2, flexDirection: "column", children: [
|
|
16445
16700
|
/* @__PURE__ */ jsx(InlineRow, { parts: shortcuts.slice(0, 2), width: innerWidth, separator: " ", separatorColor: theme.border }),
|
|
@@ -16460,6 +16715,13 @@ function PermissionLine({ marker, children }) {
|
|
|
16460
16715
|
children
|
|
16461
16716
|
] });
|
|
16462
16717
|
}
|
|
16718
|
+
function permissionRisk2(category) {
|
|
16719
|
+
if (category === "read") return "workspace content may enter model context";
|
|
16720
|
+
if (category === "write") return "workspace files may be created, replaced, or deleted";
|
|
16721
|
+
if (category === "shell") return "a local process may read or change workspace state";
|
|
16722
|
+
if (category === "git") return "repository state or remotes may change";
|
|
16723
|
+
return "data may leave this machine or remote state may change";
|
|
16724
|
+
}
|
|
16463
16725
|
function PromptBar({ busy, value, placeholder, width = 80, mode = "chat", queueCount = 0, queuePreview, attachments = [], glyphMode = "auto", children }) {
|
|
16464
16726
|
const theme = useTheme();
|
|
16465
16727
|
const glyphs = resolveGlyphs(glyphMode);
|
|
@@ -17744,6 +18006,40 @@ function clipTimelineItem(item, options) {
|
|
|
17744
18006
|
if (item.kind === "notice") {
|
|
17745
18007
|
return { ...item, text: tailText(item.text, width, options.rows) };
|
|
17746
18008
|
}
|
|
18009
|
+
if (item.kind === "list") {
|
|
18010
|
+
if (options.rows <= 1) return { id: item.id, kind: "notice", text: truncateDisplay(item.title, width) };
|
|
18011
|
+
const available = Math.max(1, options.rows - 2);
|
|
18012
|
+
const entries = [];
|
|
18013
|
+
let used = 0;
|
|
18014
|
+
let firstIncluded = item.entries.length;
|
|
18015
|
+
for (let index = item.entries.length - 1; index >= 0; index -= 1) {
|
|
18016
|
+
const entry = item.entries[index];
|
|
18017
|
+
const rows = 1 + (entry.detail ? 1 : 0);
|
|
18018
|
+
if (entries.length && used + rows > available) break;
|
|
18019
|
+
if (!entries.length && rows > available) {
|
|
18020
|
+
const { detail: _detail, ...compactEntry } = entry;
|
|
18021
|
+
entries.unshift(compactEntry);
|
|
18022
|
+
firstIncluded = index;
|
|
18023
|
+
used = 1;
|
|
18024
|
+
break;
|
|
18025
|
+
}
|
|
18026
|
+
entries.unshift(entry);
|
|
18027
|
+
firstIncluded = index;
|
|
18028
|
+
used += rows;
|
|
18029
|
+
}
|
|
18030
|
+
if (firstIncluded > 0) {
|
|
18031
|
+
while (entries.length > 1 && used >= available) {
|
|
18032
|
+
const removed = entries.shift();
|
|
18033
|
+
if (!removed) break;
|
|
18034
|
+
firstIncluded += 1;
|
|
18035
|
+
used -= 1 + (removed.detail ? 1 : 0);
|
|
18036
|
+
}
|
|
18037
|
+
if (used < available) {
|
|
18038
|
+
entries.unshift({ label: `${terminalEllipsis()} ${firstIncluded} earlier entries hidden` });
|
|
18039
|
+
}
|
|
18040
|
+
}
|
|
18041
|
+
return { ...item, entries };
|
|
18042
|
+
}
|
|
17747
18043
|
if (item.kind === "update") {
|
|
17748
18044
|
const baseRows = width < 48 ? 3 : 2;
|
|
17749
18045
|
if (options.rows < baseRows) {
|
|
@@ -17841,6 +18137,91 @@ function wrappedRows(value, width) {
|
|
|
17841
18137
|
return sanitizeTerminalText(value).split("\n").reduce((rows, line) => rows + Math.max(1, Math.ceil(displayWidth(line) / safeWidth2)), 0);
|
|
17842
18138
|
}
|
|
17843
18139
|
|
|
18140
|
+
// src/ui/review-bundle.ts
|
|
18141
|
+
import { relative as relative9 } from "node:path";
|
|
18142
|
+
var failureClasses = /* @__PURE__ */ new Set([
|
|
18143
|
+
"schema_input",
|
|
18144
|
+
"unknown_tool",
|
|
18145
|
+
"permission_denied",
|
|
18146
|
+
"command_exit",
|
|
18147
|
+
"timeout",
|
|
18148
|
+
"cancelled",
|
|
18149
|
+
"hook",
|
|
18150
|
+
"execution",
|
|
18151
|
+
"no_progress",
|
|
18152
|
+
"contract_required"
|
|
18153
|
+
]);
|
|
18154
|
+
function parseReviewScope(argument) {
|
|
18155
|
+
const value = argument.trim();
|
|
18156
|
+
if (!value || value === "working-tree") return { kind: "working-tree" };
|
|
18157
|
+
const [kind, ref, ...extra] = value.split(/\s+/u);
|
|
18158
|
+
if (kind !== "commit" && kind !== "branch" || !ref || extra.length) {
|
|
18159
|
+
throw new Error("Usage: /review [working-tree|commit <ref>|branch <base-ref>]");
|
|
18160
|
+
}
|
|
18161
|
+
if (!/^[A-Za-z0-9._/@{}~^+-]{1,200}$/u.test(ref) || ref.startsWith("-")) {
|
|
18162
|
+
throw new Error("Review refs must be a single Git revision without control characters or leading dashes.");
|
|
18163
|
+
}
|
|
18164
|
+
return { kind, ref };
|
|
18165
|
+
}
|
|
18166
|
+
function buildRedactedReviewBundle(session, workspaceRoot, scope) {
|
|
18167
|
+
const lastRun = session.lastRun;
|
|
18168
|
+
const failures = (session.audit ?? []).filter((event) => event.type === "tool" && event.outcome === "failure").slice(-8).map((event) => {
|
|
18169
|
+
const receipt = failureReceipt2(event.metadata?.failure);
|
|
18170
|
+
return {
|
|
18171
|
+
tool: event.tool,
|
|
18172
|
+
...event.category ? { category: event.category } : {},
|
|
18173
|
+
...receipt?.class ? { class: receipt.class } : {},
|
|
18174
|
+
...receipt?.retryable !== void 0 ? { retryable: receipt.retryable } : {},
|
|
18175
|
+
...receipt?.circuitOpen !== void 0 ? { circuitOpen: receipt.circuitOpen } : {}
|
|
18176
|
+
};
|
|
18177
|
+
});
|
|
18178
|
+
return {
|
|
18179
|
+
version: 1,
|
|
18180
|
+
redacted: true,
|
|
18181
|
+
sessionId: session.id,
|
|
18182
|
+
scope,
|
|
18183
|
+
changedFiles: [...new Set(session.changedFiles.map((path) => relative9(workspaceRoot, path) || "."))].sort(),
|
|
18184
|
+
...lastRun ? {
|
|
18185
|
+
lastRun: {
|
|
18186
|
+
status: lastRun.status,
|
|
18187
|
+
reason: lastRun.reason,
|
|
18188
|
+
checks: lastRun.checks.map((check) => ({ tool: check.tool, kind: check.kind, ok: check.ok })),
|
|
18189
|
+
...lastRun.acceptance ? { acceptance: {
|
|
18190
|
+
state: lastRun.acceptance.state,
|
|
18191
|
+
satisfied: lastRun.acceptance.satisfied,
|
|
18192
|
+
pending: lastRun.acceptance.pending,
|
|
18193
|
+
blocked: lastRun.acceptance.blocked
|
|
18194
|
+
} } : {}
|
|
18195
|
+
}
|
|
18196
|
+
} : {},
|
|
18197
|
+
failures
|
|
18198
|
+
};
|
|
18199
|
+
}
|
|
18200
|
+
function reviewRequest(scope) {
|
|
18201
|
+
if (scope.kind === "working-tree") return "Review the current working tree changes.";
|
|
18202
|
+
if (scope.kind === "commit") return `Review commit ${scope.ref}.`;
|
|
18203
|
+
return `Review the current branch against ${scope.ref}.`;
|
|
18204
|
+
}
|
|
18205
|
+
function reviewTurnInstructions(bundle) {
|
|
18206
|
+
return `A read-only review is active. Keep the review scope fixed to the bundle below. Do not mutate files, run write-capable tools, or widen the scope. Report actionable findings first with file and line evidence. Never reproduce credentials or secret values. The bundle is deliberately content-free and redacted.
|
|
18207
|
+
|
|
18208
|
+
<redacted-review-bundle>
|
|
18209
|
+
${JSON.stringify(bundle, null, 2)}
|
|
18210
|
+
</redacted-review-bundle>`;
|
|
18211
|
+
}
|
|
18212
|
+
function failureReceipt2(value) {
|
|
18213
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
18214
|
+
const record = value;
|
|
18215
|
+
return {
|
|
18216
|
+
...isToolFailureClass(record.class) ? { class: record.class } : {},
|
|
18217
|
+
...typeof record.retryable === "boolean" ? { retryable: record.retryable } : {},
|
|
18218
|
+
...typeof record.circuitOpen === "boolean" ? { circuitOpen: record.circuitOpen } : {}
|
|
18219
|
+
};
|
|
18220
|
+
}
|
|
18221
|
+
function isToolFailureClass(value) {
|
|
18222
|
+
return typeof value === "string" && failureClasses.has(value);
|
|
18223
|
+
}
|
|
18224
|
+
|
|
17844
18225
|
// src/ui/timeline-reducers.ts
|
|
17845
18226
|
var itemCounter = 0;
|
|
17846
18227
|
var nextId = () => `ui-${Date.now()}-${itemCounter++}`;
|
|
@@ -18184,8 +18565,13 @@ function SkeinApp({ runner, config, extensions, initialPrompt, askMode = false,
|
|
|
18184
18565
|
const timer = setInterval(() => setFrameIndex((value) => (value + 1) % spinnerFrames().length), 120);
|
|
18185
18566
|
return () => clearInterval(timer);
|
|
18186
18567
|
}, [busy]);
|
|
18187
|
-
const requestPermission = useCallback((call, category) => {
|
|
18188
|
-
return new Promise((
|
|
18568
|
+
const requestPermission = useCallback((call, category, reason) => {
|
|
18569
|
+
return new Promise((resolve28) => setPermission({
|
|
18570
|
+
call,
|
|
18571
|
+
category,
|
|
18572
|
+
...reason ? { reason } : {},
|
|
18573
|
+
resolve: resolve28
|
|
18574
|
+
}));
|
|
18189
18575
|
}, []);
|
|
18190
18576
|
const onEvent = useCallback((event) => {
|
|
18191
18577
|
switch (event.type) {
|
|
@@ -18205,7 +18591,7 @@ function SkeinApp({ runner, config, extensions, initialPrompt, askMode = false,
|
|
|
18205
18591
|
...event.packed.budgetReason ? { budgetReason: event.packed.budgetReason } : {},
|
|
18206
18592
|
truncated: event.packed.truncated,
|
|
18207
18593
|
spans: event.packed.hits.slice(0, 5).map((hit) => ({
|
|
18208
|
-
path:
|
|
18594
|
+
path: relative10(runner.workspace.primaryRoot, hit.path) || hit.path,
|
|
18209
18595
|
startLine: hit.startLine,
|
|
18210
18596
|
endLine: hit.endLine,
|
|
18211
18597
|
score: hit.score,
|
|
@@ -18309,7 +18695,7 @@ function SkeinApp({ runner, config, extensions, initialPrompt, askMode = false,
|
|
|
18309
18695
|
id: nextId(),
|
|
18310
18696
|
kind: "notice",
|
|
18311
18697
|
tone: event.status === "ready" || event.status === "integrated" ? "success" : "error",
|
|
18312
|
-
text: `Writer ${event.id.slice(0, 8)} ${event.status}${separator}${event.detail}`
|
|
18698
|
+
text: `Writer ${event.id.slice(0, 8)} ${event.status}${separator}${event.detail}${event.status === "conflict" || event.status === "failed" || event.status === "cancelled" ? `${separator}Run /recover before retrying or restoring.` : ""}`
|
|
18313
18699
|
});
|
|
18314
18700
|
break;
|
|
18315
18701
|
case "agent_done":
|
|
@@ -18348,7 +18734,7 @@ function SkeinApp({ runner, config, extensions, initialPrompt, askMode = false,
|
|
|
18348
18734
|
case "error":
|
|
18349
18735
|
lastEventError.current = event.error.message;
|
|
18350
18736
|
setTimeline(endStreamingAssistants);
|
|
18351
|
-
append({ id: nextId(), kind: "notice", tone: "error", text: event.error.message });
|
|
18737
|
+
append({ id: nextId(), kind: "notice", tone: "error", text: `${event.error.message}${separator}Run /recover to inspect evidence, changes, and safe next actions.` });
|
|
18352
18738
|
setActivity(void 0);
|
|
18353
18739
|
break;
|
|
18354
18740
|
case "done":
|
|
@@ -18372,7 +18758,7 @@ function SkeinApp({ runner, config, extensions, initialPrompt, askMode = false,
|
|
|
18372
18758
|
id: nextId(),
|
|
18373
18759
|
kind: "notice",
|
|
18374
18760
|
tone: event.reason === "aborted" ? "info" : "error",
|
|
18375
|
-
text: event.reason === "aborted" ? "Run interrupted." : event.reason === "max_turns" ? "Stopped at the configured turn limit." : event.reason === "token_budget" ? "Stopped at the configured token budget." : event.reason
|
|
18761
|
+
text: event.reason === "aborted" ? "Run interrupted. Use /recover to inspect changes or resume safely." : event.reason === "max_turns" ? "Stopped at the configured turn limit. Use /recover resume after adjusting the limit." : event.reason === "token_budget" ? "Stopped at the configured token budget. Use /recover to inspect state before resuming with a larger budget." : event.reason
|
|
18376
18762
|
});
|
|
18377
18763
|
}
|
|
18378
18764
|
break;
|
|
@@ -18386,9 +18772,16 @@ function SkeinApp({ runner, config, extensions, initialPrompt, askMode = false,
|
|
|
18386
18772
|
const runLocalCommand = useCallback(async (value) => {
|
|
18387
18773
|
if (!value.startsWith("/")) return false;
|
|
18388
18774
|
const [rawCommand = "", ...rest] = value.slice(1).trim().split(/\s+/);
|
|
18389
|
-
|
|
18390
|
-
|
|
18775
|
+
let command2 = rawCommand.toLocaleLowerCase();
|
|
18776
|
+
let argument = rest.join(" ").trim();
|
|
18391
18777
|
if (!command2) return true;
|
|
18778
|
+
if (command2 === "recover") {
|
|
18779
|
+
const [action = "", ...actionRest] = argument.split(/\s+/u);
|
|
18780
|
+
if (action === "diff" || action === "audit" || action === "rollback") {
|
|
18781
|
+
command2 = action;
|
|
18782
|
+
argument = actionRest.join(" ").trim();
|
|
18783
|
+
}
|
|
18784
|
+
}
|
|
18392
18785
|
if (command2 === "exit" || command2 === "quit") {
|
|
18393
18786
|
exit();
|
|
18394
18787
|
return true;
|
|
@@ -18470,10 +18863,77 @@ function SkeinApp({ runner, config, extensions, initialPrompt, askMode = false,
|
|
|
18470
18863
|
});
|
|
18471
18864
|
return true;
|
|
18472
18865
|
}
|
|
18866
|
+
if (command2 === "review") {
|
|
18867
|
+
const scope = parseReviewScope(argument);
|
|
18868
|
+
const bundle = buildRedactedReviewBundle(runner.getSession(), runner.workspace.primaryRoot, scope);
|
|
18869
|
+
return {
|
|
18870
|
+
kind: "agent",
|
|
18871
|
+
display: `/review ${scope.kind}${scope.kind === "working-tree" ? "" : ` ${scope.ref}`}`,
|
|
18872
|
+
runInput: reviewRequest(scope),
|
|
18873
|
+
turnInstructions: reviewTurnInstructions(bundle),
|
|
18874
|
+
readOnly: true
|
|
18875
|
+
};
|
|
18876
|
+
}
|
|
18877
|
+
if (command2 === "recover") {
|
|
18878
|
+
const action = argument.toLocaleLowerCase();
|
|
18879
|
+
const currentSession = runner.getSession();
|
|
18880
|
+
const lastRun = currentSession.lastRun;
|
|
18881
|
+
const lastFailure = (currentSession.audit ?? []).findLast((event) => event.type === "tool" && event.outcome === "failure");
|
|
18882
|
+
const failure = recoveryFailure(lastFailure?.metadata?.failure);
|
|
18883
|
+
if (action === "retry") {
|
|
18884
|
+
if (currentSession.pendingInput) throw new Error("Answer the pending clarification before retrying an operation.");
|
|
18885
|
+
if (!lastFailure) throw new Error("No failed operation is available to retry.");
|
|
18886
|
+
return {
|
|
18887
|
+
kind: "agent",
|
|
18888
|
+
display: "/recover retry",
|
|
18889
|
+
runInput: `Retry the most recent failed ${lastFailure.tool} operation after inspecting the current workspace state.`,
|
|
18890
|
+
turnInstructions: "Resume the existing session. Use the recorded failure receipt and current files as authority. Apply its repair hint before one targeted retry; do not replay a circuit-open or non-retryable operation unchanged."
|
|
18891
|
+
};
|
|
18892
|
+
}
|
|
18893
|
+
if (action === "resume") {
|
|
18894
|
+
if (currentSession.pendingInput) throw new Error("Answer the pending clarification in the composer to resume the same logical run.");
|
|
18895
|
+
if (!lastRun || lastRun.reason === "completed") throw new Error("The latest run is already complete.");
|
|
18896
|
+
return {
|
|
18897
|
+
kind: "agent",
|
|
18898
|
+
display: "/recover resume",
|
|
18899
|
+
runInput: "Resume the most recent incomplete run from its persisted state.",
|
|
18900
|
+
turnInstructions: "Resume the existing session from its Task Contract, changed-file set, last-run receipt, and unresolved failures. Inspect current state before acting, keep prior successful evidence, and run only missing verification."
|
|
18901
|
+
};
|
|
18902
|
+
}
|
|
18903
|
+
if (action) throw new Error("Usage: /recover [retry|resume|diff|rollback|audit]");
|
|
18904
|
+
const checkpoints = await runner.checkpointStore.list(currentSession.id);
|
|
18905
|
+
const latestCheckpoint = checkpoints[0];
|
|
18906
|
+
appendList("Recovery Center", [
|
|
18907
|
+
{
|
|
18908
|
+
label: `Last run ${lastRun?.status ?? "none"}${lastRun ? `${separator}${lastRun.reason}` : ""}`,
|
|
18909
|
+
detail: lastRun?.detail ?? "No completed or interrupted run has been recorded.",
|
|
18910
|
+
tone: lastRun?.status === "verified" || lastRun?.status === "no_changes" ? "success" : lastRun ? "warning" : "normal"
|
|
18911
|
+
},
|
|
18912
|
+
...lastFailure ? [{
|
|
18913
|
+
label: `Failure ${lastFailure.tool}${failure?.class ? `${separator}${failure.class}` : ""}`,
|
|
18914
|
+
detail: failure?.repairHint ?? "Inspect the audit timeline before retrying.",
|
|
18915
|
+
tone: "error"
|
|
18916
|
+
}] : [],
|
|
18917
|
+
{
|
|
18918
|
+
label: `Workspace ${currentSession.changedFiles.length} changed file${currentSession.changedFiles.length === 1 ? "" : "s"}`,
|
|
18919
|
+
detail: currentSession.changedFiles.length ? "/diff inspects the current patch." : "No tracked session changes."
|
|
18920
|
+
},
|
|
18921
|
+
{
|
|
18922
|
+
label: `Checkpoint ${latestCheckpoint ? latestCheckpoint.id.slice(0, 12) : "none"}`,
|
|
18923
|
+
detail: latestCheckpoint ? `${latestCheckpoint.reason}${separator}${latestCheckpoint.entries.length} files${separator}/recover rollback` : "No pre-mutation snapshot is available for this session."
|
|
18924
|
+
},
|
|
18925
|
+
{ label: "/recover retry", detail: lastFailure ? "Apply the repair hint, then retry the latest failure once." : "Unavailable until an operation fails." },
|
|
18926
|
+
{ label: "/recover resume", detail: currentSession.pendingInput ? "Unavailable: answer the pending clarification in the composer." : lastRun && lastRun.reason !== "completed" ? "Continue the incomplete logical run." : "No incomplete run to resume." },
|
|
18927
|
+
{ label: "/recover diff", detail: "Inspect the current workspace patch." },
|
|
18928
|
+
{ label: "/recover audit", detail: "Review permission and tool evidence." },
|
|
18929
|
+
{ label: "/recover rollback", detail: "Choose a checkpoint to restore." }
|
|
18930
|
+
]);
|
|
18931
|
+
return true;
|
|
18932
|
+
}
|
|
18473
18933
|
if (command2 === "changes") {
|
|
18474
18934
|
const changed = runner.getSession().changedFiles;
|
|
18475
18935
|
appendList("Changed files", changed.length ? changed.map((path) => ({
|
|
18476
|
-
label:
|
|
18936
|
+
label: relative10(runner.workspace.primaryRoot, path) || ".",
|
|
18477
18937
|
detail: path
|
|
18478
18938
|
})) : [{ label: "No recorded changes." }]);
|
|
18479
18939
|
return true;
|
|
@@ -18626,25 +19086,109 @@ function SkeinApp({ runner, config, extensions, initialPrompt, askMode = false,
|
|
|
18626
19086
|
const skills = extensions?.listSkills() ?? [];
|
|
18627
19087
|
appendList("Skills", skills.length ? skills.map((skill) => ({
|
|
18628
19088
|
label: `${skill.name} ${skill.scope}${skill.trusted ? "" : `${separator}untrusted`}`,
|
|
18629
|
-
detail: `${skill.description}${separator}${
|
|
19089
|
+
detail: `${skill.description}${separator}${relative10(runner.workspace.primaryRoot, skill.path) || skill.path}`,
|
|
18630
19090
|
tone: skill.trusted ? "normal" : "warning"
|
|
18631
19091
|
})) : [{ label: "No skills discovered.", detail: "Add SKILL.md playbooks under .agents/skills, .claude/skills, or a configured directory." }]);
|
|
18632
19092
|
return true;
|
|
18633
19093
|
}
|
|
18634
19094
|
if (command2 === "mcp") {
|
|
19095
|
+
const [action = "list", server = "", ...tail] = argument.split(/\s+/u).filter(Boolean);
|
|
19096
|
+
if (action === "search") {
|
|
19097
|
+
const results = extensions?.mcpSearch([server, ...tail].filter(Boolean).join(" ")) ?? [];
|
|
19098
|
+
appendList("MCP capability search", results.length ? results.map((result) => ({
|
|
19099
|
+
label: `${result.name} ${result.trust}${result.required ? `${separator}required` : ""}`,
|
|
19100
|
+
detail: `${result.description}${separator}${result.version}${separator}${result.declaredTools || "dynamic"} tools`,
|
|
19101
|
+
tone: result.trust === "trusted" ? "success" : result.trust === "revoked" ? "error" : "warning"
|
|
19102
|
+
})) : [{ label: "No configured MCP capability matched." }]);
|
|
19103
|
+
return true;
|
|
19104
|
+
}
|
|
19105
|
+
if (action === "inspect" || action === "trust") {
|
|
19106
|
+
if (!server) {
|
|
19107
|
+
append({ id: nextId(), kind: "notice", tone: "error", text: `Usage: /mcp ${action} <server>${action === "trust" ? " [--confirm]" : ""}` });
|
|
19108
|
+
return true;
|
|
19109
|
+
}
|
|
19110
|
+
const manifest = extensions?.mcpInspect(server);
|
|
19111
|
+
if (!manifest) {
|
|
19112
|
+
append({ id: nextId(), kind: "notice", tone: "error", text: "MCP is disabled or unavailable." });
|
|
19113
|
+
return true;
|
|
19114
|
+
}
|
|
19115
|
+
appendList(`MCP trust review \xB7 ${manifest.name}`, [
|
|
19116
|
+
{ label: `${manifest.source.kind}:${manifest.name} ${manifest.version}`, detail: `${manifest.transport}${separator}${manifest.target}${manifest.required ? `${separator}required` : ""}` },
|
|
19117
|
+
...manifest.tools.length ? manifest.tools.flatMap((tool) => {
|
|
19118
|
+
const tone = tool.permissions.some((category) => category !== "read" && category !== "network") ? "warning" : "normal";
|
|
19119
|
+
return [
|
|
19120
|
+
{ label: `${tool.name} ${tool.permissions.join("+")}`, detail: `completion evidence ${tool.completionEvidence}`, tone },
|
|
19121
|
+
{ label: "network scopes", detail: tool.network.join(", ") || "unspecified", tone },
|
|
19122
|
+
{ label: "command scopes", detail: tool.commands.join(", ") || "none", tone },
|
|
19123
|
+
{ label: "path scopes", detail: tool.paths.join(", ") || "none", tone },
|
|
19124
|
+
{ label: `sensitive fields ${tool.sensitiveFields.join(", ") || "none"}`, detail: `${tool.background ? "background" : "foreground"}${separator}${tool.processTree ? "process-tree" : "single-process"}`, tone }
|
|
19125
|
+
];
|
|
19126
|
+
}) : [{ label: "Dynamic remote tools", detail: "Undeclared tools stay network-only and do not receive Skein completion-evidence protection.", tone: "warning" }]
|
|
19127
|
+
]);
|
|
19128
|
+
if (action === "trust") {
|
|
19129
|
+
if (manifest.dynamicTools) {
|
|
19130
|
+
append({ id: nextId(), kind: "notice", tone: "error", text: `Declare tools and effects for ${server} in user-owned config before trust can be granted.` });
|
|
19131
|
+
return true;
|
|
19132
|
+
}
|
|
19133
|
+
if (!tail.includes("--confirm")) {
|
|
19134
|
+
append({ id: nextId(), kind: "notice", tone: "warning", text: `Review complete. Run /mcp trust ${server} --confirm to trust this exact manifest fingerprint.` });
|
|
19135
|
+
} else {
|
|
19136
|
+
const status = await extensions?.mcpTrust(server);
|
|
19137
|
+
append({ id: nextId(), kind: "notice", tone: status ? "success" : "error", text: status ? `Trusted MCP capability ${server}. Activation remains explicit.` : "MCP is unavailable." });
|
|
19138
|
+
}
|
|
19139
|
+
}
|
|
19140
|
+
return true;
|
|
19141
|
+
}
|
|
19142
|
+
if (action === "activate") {
|
|
19143
|
+
const query = tail.join(" ").trim();
|
|
19144
|
+
if (!server || !query) {
|
|
19145
|
+
append({ id: nextId(), kind: "notice", tone: "error", text: "Usage: /mcp activate <server> <capability query>" });
|
|
19146
|
+
return true;
|
|
19147
|
+
}
|
|
19148
|
+
const result = await extensions?.mcpActivate(server, query);
|
|
19149
|
+
append({
|
|
19150
|
+
id: nextId(),
|
|
19151
|
+
kind: "notice",
|
|
19152
|
+
tone: result?.ok ? "success" : "error",
|
|
19153
|
+
text: result?.ok ? `Activated ${server}; loaded ${result.registeredTools.length} of ${result.availableTools} schemas.` : `Could not activate ${server}: ${result?.status.error ?? result?.status.trust ?? "MCP unavailable"}`
|
|
19154
|
+
});
|
|
19155
|
+
return true;
|
|
19156
|
+
}
|
|
19157
|
+
if (action === "disable") {
|
|
19158
|
+
if (!server) {
|
|
19159
|
+
append({ id: nextId(), kind: "notice", tone: "error", text: "Usage: /mcp disable <server>" });
|
|
19160
|
+
return true;
|
|
19161
|
+
}
|
|
19162
|
+
const status = await extensions?.mcpDisable(server);
|
|
19163
|
+
append({ id: nextId(), kind: "notice", tone: status ? "success" : "error", text: status ? `Disabled MCP capability ${server}.` : "MCP is unavailable." });
|
|
19164
|
+
return true;
|
|
19165
|
+
}
|
|
19166
|
+
if (action === "revoke") {
|
|
19167
|
+
if (!server || !tail.includes("--confirm")) {
|
|
19168
|
+
append({ id: nextId(), kind: "notice", tone: "warning", text: `Revocation removes persisted trust. Run /mcp revoke ${server || "<server>"} --confirm to continue.` });
|
|
19169
|
+
return true;
|
|
19170
|
+
}
|
|
19171
|
+
const status = await extensions?.mcpRevoke(server);
|
|
19172
|
+
append({ id: nextId(), kind: "notice", tone: status ? "success" : "error", text: status ? `Revoked MCP capability ${server}. Re-inspection and trust are required before reuse.` : "MCP is unavailable." });
|
|
19173
|
+
return true;
|
|
19174
|
+
}
|
|
19175
|
+
if (action !== "list") {
|
|
19176
|
+
append({ id: nextId(), kind: "notice", tone: "error", text: "Usage: /mcp [search|inspect|trust|activate|disable|revoke] [...]" });
|
|
19177
|
+
return true;
|
|
19178
|
+
}
|
|
18635
19179
|
const servers = extensions?.mcpStatus() ?? [];
|
|
18636
|
-
appendList("MCP", servers.length ? servers.map((
|
|
18637
|
-
label: `${
|
|
18638
|
-
detail: `${
|
|
18639
|
-
tone:
|
|
19180
|
+
appendList("MCP", servers.length ? servers.map((item) => ({
|
|
19181
|
+
label: `${item.name} ${item.state}${item.required ? `${separator}required` : ""}${item.serverVersion ? `${separator}v${item.serverVersion}` : ""}`,
|
|
19182
|
+
detail: `${item.transport}${separator}${item.trust}${separator}${item.toolCount} tools${item.connectedAt ? `${separator}connected ${item.connectedAt.slice(11, 19)}` : ""}${item.error ? `${separator}${item.error}` : ""}`,
|
|
19183
|
+
tone: item.state === "connected" ? "success" : item.state === "error" || item.state === "revoked" ? "error" : "warning"
|
|
18640
19184
|
})) : [{ label: "No MCP servers configured." }]);
|
|
18641
19185
|
return true;
|
|
18642
19186
|
}
|
|
18643
19187
|
if (command2 === "tools") {
|
|
18644
19188
|
const definitions = runner.tools.definitions();
|
|
18645
19189
|
appendList("Tools", definitions.map((tool) => ({
|
|
18646
|
-
label: `${tool.name} ${tool.category}`,
|
|
18647
|
-
detail: tool.description
|
|
19190
|
+
label: `${tool.name} ${tool.source ?? "builtin"}${separator}${(tool.permissionCategories ?? [tool.category]).join("+")}`,
|
|
19191
|
+
detail: `${tool.activation ?? "always"}${tool.completionEvidence ? `${separator}evidence ${tool.completionEvidence}` : ""}${separator}${tool.description}`,
|
|
18648
19192
|
tone: tool.category === "read" ? "normal" : tool.category === "network" ? "warning" : "normal"
|
|
18649
19193
|
})));
|
|
18650
19194
|
return true;
|
|
@@ -18956,7 +19500,7 @@ function SkeinApp({ runner, config, extensions, initialPrompt, askMode = false,
|
|
|
18956
19500
|
lastEventError.current = void 0;
|
|
18957
19501
|
try {
|
|
18958
19502
|
const nextSession = await runner.run(current.runInput, {
|
|
18959
|
-
askMode: interactionMode !== "build",
|
|
19503
|
+
askMode: current.readOnly === true || interactionMode !== "build",
|
|
18960
19504
|
signal: abortController.signal,
|
|
18961
19505
|
...current.turnInstructions || interactionMode === "plan" ? {
|
|
18962
19506
|
turnInstructions: [
|
|
@@ -19026,10 +19570,10 @@ function SkeinApp({ runner, config, extensions, initialPrompt, askMode = false,
|
|
|
19026
19570
|
}, [append, exit, interactionMode, onEvent, requestPermission, runLocalCommand, runner]);
|
|
19027
19571
|
const submitFromComposer = useCallback((raw, mode = "normal") => {
|
|
19028
19572
|
if (historySearch) {
|
|
19029
|
-
const
|
|
19573
|
+
const selected2 = resolveHistorySearch(historySearch, "select");
|
|
19030
19574
|
setHistorySearch(void 0);
|
|
19031
19575
|
setHistoryIndex(-1);
|
|
19032
|
-
void submit(
|
|
19576
|
+
void submit(selected2, mode === "normal" && processing.current ? "steer" : mode);
|
|
19033
19577
|
return;
|
|
19034
19578
|
}
|
|
19035
19579
|
if (suggestionMode === "mention" && selectedSuggestion) {
|
|
@@ -19040,7 +19584,8 @@ function SkeinApp({ runner, config, extensions, initialPrompt, askMode = false,
|
|
|
19040
19584
|
return;
|
|
19041
19585
|
}
|
|
19042
19586
|
}
|
|
19043
|
-
const
|
|
19587
|
+
const selected = selectedSuggestion;
|
|
19588
|
+
const suggestion = selected && !(raw.startsWith(selected.value) && raw.slice(selected.value.length).trim()) ? selected : void 0;
|
|
19044
19589
|
const normalized = raw.trimEnd();
|
|
19045
19590
|
if (suggestion && raw.startsWith("/") && suggestion.value !== raw && suggestion.value.endsWith(" ") && suggestion.label !== normalized) {
|
|
19046
19591
|
setInput(suggestion.value);
|
|
@@ -19068,8 +19613,8 @@ function SkeinApp({ runner, config, extensions, initialPrompt, askMode = false,
|
|
|
19068
19613
|
}, [append]);
|
|
19069
19614
|
function settlePermission(grant, stop = false) {
|
|
19070
19615
|
if (!permission) return;
|
|
19071
|
-
const { call, category, resolve:
|
|
19072
|
-
|
|
19616
|
+
const { call, category, resolve: resolve28 } = permission;
|
|
19617
|
+
resolve28(grant);
|
|
19073
19618
|
setPermission(void 0);
|
|
19074
19619
|
if (grant === "session") {
|
|
19075
19620
|
append({
|
|
@@ -19079,6 +19624,14 @@ function SkeinApp({ runner, config, extensions, initialPrompt, askMode = false,
|
|
|
19079
19624
|
text: `Allowed ${call.name} for this exact ${category} target during the session.`
|
|
19080
19625
|
});
|
|
19081
19626
|
}
|
|
19627
|
+
if (grant === false) {
|
|
19628
|
+
append({
|
|
19629
|
+
id: nextId(),
|
|
19630
|
+
kind: "notice",
|
|
19631
|
+
tone: "info",
|
|
19632
|
+
text: `Denied ${call.name}; the requested ${category} action was not run. Use /permissions to inspect policy or /recover to review the run.`
|
|
19633
|
+
});
|
|
19634
|
+
}
|
|
19082
19635
|
if (stop) {
|
|
19083
19636
|
requestRunStop();
|
|
19084
19637
|
}
|
|
@@ -19422,7 +19975,7 @@ function SkeinApp({ runner, config, extensions, initialPrompt, askMode = false,
|
|
|
19422
19975
|
)
|
|
19423
19976
|
}
|
|
19424
19977
|
)
|
|
19425
|
-
] }) : /* @__PURE__ */ jsx3(PermissionCard, { call: permission.call, category: permission.category, workspace: runner.workspace.primaryRoot, width: contentWidth, glyphMode, compact: constrainedHeight }),
|
|
19978
|
+
] }) : /* @__PURE__ */ jsx3(PermissionCard, { call: permission.call, category: permission.category, ...permission.reason ? { reason: permission.reason } : {}, workspace: runner.workspace.primaryRoot, width: contentWidth, glyphMode, compact: constrainedHeight }),
|
|
19426
19979
|
showFooter ? /* @__PURE__ */ jsx3(
|
|
19427
19980
|
Footer,
|
|
19428
19981
|
{
|
|
@@ -19562,6 +20115,14 @@ function cloneValue(value) {
|
|
|
19562
20115
|
if (value && typeof value === "object") return cloneRecord(value);
|
|
19563
20116
|
return value;
|
|
19564
20117
|
}
|
|
20118
|
+
function recoveryFailure(value) {
|
|
20119
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
20120
|
+
const receipt = value;
|
|
20121
|
+
return {
|
|
20122
|
+
...typeof receipt.class === "string" ? { class: sanitizeTerminalText(receipt.class).slice(0, 40) } : {},
|
|
20123
|
+
...typeof receipt.repairHint === "string" ? { repairHint: sanitizeTerminalText(receipt.repairHint).replace(/\s+/gu, " ").slice(0, 240) } : {}
|
|
20124
|
+
};
|
|
20125
|
+
}
|
|
19565
20126
|
function isExitCommand(value) {
|
|
19566
20127
|
const command2 = localCommandName(value);
|
|
19567
20128
|
return command2 === "exit" || command2 === "quit";
|
|
@@ -19579,6 +20140,9 @@ function shouldDeferLocalCommand(value) {
|
|
|
19579
20140
|
"remember",
|
|
19580
20141
|
"diff",
|
|
19581
20142
|
"checkpoints",
|
|
20143
|
+
"audit",
|
|
20144
|
+
"rollback",
|
|
20145
|
+
"recover",
|
|
19582
20146
|
"workflow",
|
|
19583
20147
|
"exit",
|
|
19584
20148
|
"quit"
|
|
@@ -19598,7 +20162,7 @@ function composerAttachments(value) {
|
|
|
19598
20162
|
return [...new Set(paths)].slice(-3);
|
|
19599
20163
|
}
|
|
19600
20164
|
function permissionRows(width, hasCwd, compact3) {
|
|
19601
|
-
const content =
|
|
20165
|
+
const content = 5 + (hasCwd ? 1 : 0);
|
|
19602
20166
|
if (width >= 64) return content + 2;
|
|
19603
20167
|
if (width >= 28) return content + 3;
|
|
19604
20168
|
if (compact3) return content + 3;
|
|
@@ -20392,7 +20956,7 @@ function titleForStep(step2) {
|
|
|
20392
20956
|
return "Saving configuration";
|
|
20393
20957
|
}
|
|
20394
20958
|
function descriptionForStep(state) {
|
|
20395
|
-
if (state.step === "method") return "
|
|
20959
|
+
if (state.step === "method") return "Skein's primary agent needs an API credential. OpenAI, Anthropic, and Gemini subscription logins are not API keys; signed-in coding CLIs are separate delegated tools.";
|
|
20396
20960
|
if (state.step === "official-provider") return "Use the API credential issued by the selected provider.";
|
|
20397
20961
|
if (state.step === "relay-protocol") return "The protocol is explicit so requests and credentials are never guessed.";
|
|
20398
20962
|
if (state.step === "endpoint") return "Remote endpoints require HTTPS. Loopback development servers may use HTTP.";
|
|
@@ -20442,13 +21006,13 @@ async function runFirstRunOnboarding(initialConfig, options = {}) {
|
|
|
20442
21006
|
}
|
|
20443
21007
|
|
|
20444
21008
|
// src/runtime/extensions.ts
|
|
20445
|
-
import { resolve as
|
|
21009
|
+
import { resolve as resolve26 } from "node:path";
|
|
20446
21010
|
|
|
20447
21011
|
// src/mcp/manager.ts
|
|
20448
21012
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
20449
21013
|
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
20450
21014
|
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
20451
|
-
import
|
|
21015
|
+
import stripAnsi6 from "strip-ansi";
|
|
20452
21016
|
|
|
20453
21017
|
// src/mcp/tool.ts
|
|
20454
21018
|
import { createHash as createHash18 } from "node:crypto";
|
|
@@ -20460,36 +21024,61 @@ var MAX_SCHEMA_BYTES = 1e5;
|
|
|
20460
21024
|
function createMcpToolAdapter(options) {
|
|
20461
21025
|
const { remoteTool } = options;
|
|
20462
21026
|
const inputSchema13 = copyInputSchema(remoteTool.inputSchema);
|
|
21027
|
+
const permissionCategories = options.capability?.permissions ?? ["network"];
|
|
21028
|
+
const completionEvidence = options.capability?.completionEvidence ?? "none";
|
|
21029
|
+
const mutating = permissionCategories.some((category) => category === "write" || category === "shell" || category === "git");
|
|
20463
21030
|
return {
|
|
20464
21031
|
definition: {
|
|
20465
21032
|
name: options.exposedName,
|
|
20466
|
-
description: describeTool(options.serverName, remoteTool),
|
|
21033
|
+
description: describeTool(options.serverName, remoteTool, options.capability),
|
|
20467
21034
|
// MCP servers are an external trust boundary. Read-only annotations are
|
|
20468
21035
|
// hints from that server and must not lower the local permission level.
|
|
20469
21036
|
category: "network",
|
|
20470
21037
|
inputSchema: inputSchema13,
|
|
20471
|
-
progressive: true
|
|
21038
|
+
progressive: true,
|
|
21039
|
+
source: "mcp",
|
|
21040
|
+
permissionCategories,
|
|
21041
|
+
activation: "active",
|
|
21042
|
+
completionEvidence,
|
|
21043
|
+
...options.capability?.sensitiveFields.length ? { sensitiveFields: options.capability.sensitiveFields } : {}
|
|
20472
21044
|
},
|
|
20473
|
-
permissionCategories: () =>
|
|
21045
|
+
permissionCategories: () => permissionCategories,
|
|
21046
|
+
...mutating ? {
|
|
21047
|
+
affectedPaths: async (arguments_, context) => resolveDeclaredMutationPaths(
|
|
21048
|
+
arguments_,
|
|
21049
|
+
options.capability?.paths ?? [],
|
|
21050
|
+
context
|
|
21051
|
+
)
|
|
21052
|
+
} : {},
|
|
20474
21053
|
async execute(arguments_, context) {
|
|
20475
21054
|
assertArguments(arguments_);
|
|
21055
|
+
const remoteArguments = mutating && completionEvidence === "full" && context.checkpointId ? { ...arguments_, _skein: { checkpointId: context.checkpointId } } : arguments_;
|
|
20476
21056
|
const result = await options.callTool({
|
|
20477
21057
|
name: remoteTool.name,
|
|
20478
|
-
arguments:
|
|
21058
|
+
arguments: remoteArguments
|
|
20479
21059
|
}, {
|
|
20480
21060
|
timeout: options.timeoutMs,
|
|
20481
21061
|
maxTotalTimeout: options.timeoutMs,
|
|
20482
21062
|
...context.signal ? { signal: context.signal } : {}
|
|
20483
21063
|
});
|
|
20484
21064
|
const normalized = normalizeCallResult(result);
|
|
21065
|
+
const evidence = mutating && completionEvidence === "full" ? await validateCompletionEvidence(normalized.structuredContent, context) : void 0;
|
|
20485
21066
|
return {
|
|
20486
21067
|
ok: !normalized.isError,
|
|
20487
21068
|
content: normalized.content,
|
|
21069
|
+
...evidence ? { changedFiles: evidence.changedFiles } : {},
|
|
20488
21070
|
metadata: {
|
|
20489
21071
|
mcpServer: options.serverName,
|
|
20490
21072
|
mcpTool: remoteTool.name,
|
|
20491
21073
|
sourceBytes: normalized.sourceBytes,
|
|
20492
21074
|
sourceTruncated: normalized.sourceTruncated,
|
|
21075
|
+
capabilityPermissions: permissionCategories,
|
|
21076
|
+
completionEvidence,
|
|
21077
|
+
...mutating ? {
|
|
21078
|
+
changeTracking: evidence ? "complete" : "unresolved",
|
|
21079
|
+
completionEvidenceVerified: Boolean(evidence)
|
|
21080
|
+
} : {},
|
|
21081
|
+
...evidence ? evidence : {},
|
|
20493
21082
|
...normalized.isError ? { mcpError: true } : {}
|
|
20494
21083
|
}
|
|
20495
21084
|
};
|
|
@@ -20518,9 +21107,10 @@ function isUsableRemoteTool(tool) {
|
|
|
20518
21107
|
return false;
|
|
20519
21108
|
}
|
|
20520
21109
|
}
|
|
20521
|
-
function describeTool(serverName, tool) {
|
|
21110
|
+
function describeTool(serverName, tool, capability) {
|
|
20522
21111
|
const label = stripAnsi4(tool.title?.trim() || tool.name).replace(/[\u0000-\u001f\u007f]/g, " ").slice(0, 200);
|
|
20523
|
-
const
|
|
21112
|
+
const sourceDescription = capability?.description ?? tool.description;
|
|
21113
|
+
const description = sourceDescription ? stripAnsi4(sourceDescription).replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, " ").trim().slice(0, MAX_DESCRIPTION_LENGTH) : void 0;
|
|
20524
21114
|
return description ? `[MCP ${serverName}/${label}] ${description}` : `Call the ${label} tool provided by the ${serverName} MCP server.`;
|
|
20525
21115
|
}
|
|
20526
21116
|
function copyInputSchema(schema) {
|
|
@@ -20559,7 +21149,46 @@ ${safeJson(result.structuredContent)}`);
|
|
|
20559
21149
|
${safeJson(result.toolResult)}`);
|
|
20560
21150
|
}
|
|
20561
21151
|
const content = sections.filter(Boolean).join("\n\n") || (isError ? "The MCP tool reported an error." : "The MCP tool completed without output.");
|
|
20562
|
-
return boundResult(content, isError);
|
|
21152
|
+
return { ...boundResult(content, isError), ...result.structuredContent === void 0 ? {} : { structuredContent: result.structuredContent } };
|
|
21153
|
+
}
|
|
21154
|
+
async function validateCompletionEvidence(structuredContent, context) {
|
|
21155
|
+
if (!isRecord2(structuredContent)) return;
|
|
21156
|
+
const receipt = isRecord2(structuredContent.skeinEvidence) ? structuredContent.skeinEvidence : void 0;
|
|
21157
|
+
if (!receipt || !context.checkpointId || !Array.isArray(receipt.changedFiles) || receipt.changedFiles.length > 256 || typeof receipt.checkpointId !== "string" || !receipt.checkpointId.trim() || receipt.checkpointId !== context.checkpointId || !Array.isArray(receipt.artifactReceipts) || !receipt.artifactReceipts.length || !Array.isArray(receipt.completionEvidence) || !receipt.completionEvidence.length) return;
|
|
21158
|
+
const changedFiles = [];
|
|
21159
|
+
for (const path of receipt.changedFiles) {
|
|
21160
|
+
if (typeof path !== "string" || !path.trim()) return;
|
|
21161
|
+
try {
|
|
21162
|
+
changedFiles.push(await context.workspace.resolvePath(path, { allowMissing: true }));
|
|
21163
|
+
} catch {
|
|
21164
|
+
return;
|
|
21165
|
+
}
|
|
21166
|
+
}
|
|
21167
|
+
const artifactReceipts = receipt.artifactReceipts.filter((value) => typeof value === "string" && Boolean(value.trim())).map((value) => sanitizeInlineText(value).slice(0, 256));
|
|
21168
|
+
const completionEvidenceRefs = receipt.completionEvidence.filter((value) => typeof value === "string" && Boolean(value.trim())).map((value) => sanitizeInlineText(value).slice(0, 256));
|
|
21169
|
+
if (artifactReceipts.length !== receipt.artifactReceipts.length || completionEvidenceRefs.length !== receipt.completionEvidence.length) return;
|
|
21170
|
+
return {
|
|
21171
|
+
changedFiles,
|
|
21172
|
+
checkpointId: sanitizeInlineText(receipt.checkpointId).slice(0, 256),
|
|
21173
|
+
artifactReceipts,
|
|
21174
|
+
completionEvidenceRefs
|
|
21175
|
+
};
|
|
21176
|
+
}
|
|
21177
|
+
async function resolveDeclaredMutationPaths(arguments_, declaredPaths, context) {
|
|
21178
|
+
const candidates = [];
|
|
21179
|
+
for (const key of ["path", "file", "cwd"]) {
|
|
21180
|
+
const value = arguments_[key];
|
|
21181
|
+
if (typeof value === "string" && value.trim()) candidates.push(value);
|
|
21182
|
+
}
|
|
21183
|
+
if (Array.isArray(arguments_.paths)) {
|
|
21184
|
+
candidates.push(...arguments_.paths.filter((value) => typeof value === "string" && Boolean(value.trim())));
|
|
21185
|
+
}
|
|
21186
|
+
candidates.push(...declaredPaths.filter((path) => !/[?*{}\[\]]/u.test(path)));
|
|
21187
|
+
const resolved = [];
|
|
21188
|
+
for (const candidate of [...new Set(candidates)].slice(0, 256)) {
|
|
21189
|
+
resolved.push(await context.workspace.resolvePath(candidate, { allowMissing: true }));
|
|
21190
|
+
}
|
|
21191
|
+
return resolved;
|
|
20563
21192
|
}
|
|
20564
21193
|
function boundResult(content, isError) {
|
|
20565
21194
|
const sourceBytes = Buffer.byteLength(content);
|
|
@@ -20787,6 +21416,221 @@ function isLoopbackHost(hostname) {
|
|
|
20787
21416
|
return normalized === "localhost" || normalized === "::1" || normalized === "0:0:0:0:0:0:0:1" || isIpv4Loopback;
|
|
20788
21417
|
}
|
|
20789
21418
|
|
|
21419
|
+
// src/mcp/capabilities.ts
|
|
21420
|
+
import { createHash as createHash19 } from "node:crypto";
|
|
21421
|
+
import { homedir as homedir4 } from "node:os";
|
|
21422
|
+
import { basename as basename12, isAbsolute as isAbsolute9, relative as relative11, resolve as resolve23 } from "node:path";
|
|
21423
|
+
import stripAnsi5 from "strip-ansi";
|
|
21424
|
+
function buildMcpCapabilityManifest(name, config, workspace = process.cwd()) {
|
|
21425
|
+
const tools = (config.tools ?? []).map((tool) => normalizeTool(tool, workspace));
|
|
21426
|
+
return {
|
|
21427
|
+
schemaVersion: 1,
|
|
21428
|
+
id: `mcp:${sanitizeIdentifier(name, "server")}`,
|
|
21429
|
+
source: { kind: "mcp", owner: "user-config" },
|
|
21430
|
+
name: sanitizeText(name, 64),
|
|
21431
|
+
version: sanitizeText(config.version ?? "unversioned", 128),
|
|
21432
|
+
required: config.required === true,
|
|
21433
|
+
transport: config.transport ?? "stdio",
|
|
21434
|
+
target: redactTransportTarget(config),
|
|
21435
|
+
tools,
|
|
21436
|
+
dynamicTools: tools.length === 0
|
|
21437
|
+
};
|
|
21438
|
+
}
|
|
21439
|
+
function capabilityFingerprint(name, config, workspace = process.cwd()) {
|
|
21440
|
+
const manifest = buildMcpCapabilityManifest(name, config, workspace);
|
|
21441
|
+
const privateTransport = {
|
|
21442
|
+
command: config.command ?? null,
|
|
21443
|
+
args: config.args ?? [],
|
|
21444
|
+
cwd: config.cwd ? resolve23(workspace, config.cwd) : null,
|
|
21445
|
+
url: config.url ? redactUrl(config.url) : null,
|
|
21446
|
+
envNames: Object.keys(config.env ?? {}).sort(),
|
|
21447
|
+
headerNames: Object.keys(config.headers ?? {}).map((key) => key.toLocaleLowerCase()).sort()
|
|
21448
|
+
};
|
|
21449
|
+
return createHash19("sha256").update(stableJson2({ manifest, privateTransport })).digest("hex");
|
|
21450
|
+
}
|
|
21451
|
+
function searchMcpCapabilities(config, query) {
|
|
21452
|
+
const terms = new Set(sanitizeText(query, 500).toLocaleLowerCase().match(/[\p{L}\p{N}_-]{2,}/gu) ?? []);
|
|
21453
|
+
return Object.entries(config.servers ?? {}).map(([name, server]) => {
|
|
21454
|
+
const description = sanitizeText(server.description ?? `${server.transport ?? "stdio"} MCP server`, 200);
|
|
21455
|
+
const searchable = [name, description, server.version, ...(server.tools ?? []).flatMap((tool) => [
|
|
21456
|
+
tool.name,
|
|
21457
|
+
tool.description,
|
|
21458
|
+
...tool.permissions,
|
|
21459
|
+
...tool.commands ?? [],
|
|
21460
|
+
...tool.network ?? []
|
|
21461
|
+
])].filter(Boolean).join(" ").toLocaleLowerCase();
|
|
21462
|
+
let score = 0;
|
|
21463
|
+
for (const term of terms) if (searchable.includes(term)) score += term.length;
|
|
21464
|
+
return {
|
|
21465
|
+
name,
|
|
21466
|
+
description,
|
|
21467
|
+
version: sanitizeText(server.version ?? "unversioned", 128),
|
|
21468
|
+
required: server.required === true,
|
|
21469
|
+
transport: server.transport ?? "stdio",
|
|
21470
|
+
declaredTools: server.tools?.length ?? 0,
|
|
21471
|
+
score
|
|
21472
|
+
};
|
|
21473
|
+
}).filter((result) => terms.size === 0 || result.score > 0).sort((left, right) => right.score - left.score || left.name.localeCompare(right.name));
|
|
21474
|
+
}
|
|
21475
|
+
function declaredToolCapability(config, remoteName, workspace = process.cwd()) {
|
|
21476
|
+
const declaration = config.tools?.find((tool) => tool.name === remoteName);
|
|
21477
|
+
return declaration ? normalizeTool(declaration, workspace) : void 0;
|
|
21478
|
+
}
|
|
21479
|
+
function normalizeTool(tool, workspace) {
|
|
21480
|
+
const permissions = uniquePermissions(["network", ...tool.permissions]);
|
|
21481
|
+
return {
|
|
21482
|
+
name: sanitizeText(tool.name, 256),
|
|
21483
|
+
...tool.description ? { description: sanitizeText(tool.description, 500) } : {},
|
|
21484
|
+
permissions,
|
|
21485
|
+
network: unique3(tool.network ?? []).map(redactNetworkTarget),
|
|
21486
|
+
commands: unique3(tool.commands ?? []).map(redactCommand2),
|
|
21487
|
+
paths: unique3(tool.paths ?? []).map((path) => redactPath(path, workspace)),
|
|
21488
|
+
sensitiveFields: unique3(tool.sensitiveFields ?? []).map((field) => sanitizeText(field, 256)),
|
|
21489
|
+
background: tool.background === true,
|
|
21490
|
+
processTree: tool.processTree === true,
|
|
21491
|
+
completionEvidence: tool.completionEvidence ?? "none"
|
|
21492
|
+
};
|
|
21493
|
+
}
|
|
21494
|
+
function redactTransportTarget(config) {
|
|
21495
|
+
if (config.transport === "http") return config.url ? redactUrl(config.url) : "<unconfigured HTTP endpoint>";
|
|
21496
|
+
return config.command ? `<stdio command:${sanitizeText(basename12(config.command), 128)}>` : "<unconfigured stdio command>";
|
|
21497
|
+
}
|
|
21498
|
+
function redactUrl(value) {
|
|
21499
|
+
try {
|
|
21500
|
+
const url = new URL(value);
|
|
21501
|
+
return `${url.protocol}//${url.host}${url.pathname}`;
|
|
21502
|
+
} catch {
|
|
21503
|
+
return "<invalid URL>";
|
|
21504
|
+
}
|
|
21505
|
+
}
|
|
21506
|
+
function redactNetworkTarget(value) {
|
|
21507
|
+
if (/^https?:\/\//iu.test(value)) return redactUrl(value);
|
|
21508
|
+
return sanitizeText(value, 500).replace(/([?&](?:key|token|secret|password)=)[^&\s]*/giu, "$1<redacted>");
|
|
21509
|
+
}
|
|
21510
|
+
function redactCommand2(value) {
|
|
21511
|
+
const command2 = sanitizeText(value, 500).split(/\s+/u)[0];
|
|
21512
|
+
return command2 ? basename12(command2) : "<redacted command>";
|
|
21513
|
+
}
|
|
21514
|
+
function redactPath(value, workspace) {
|
|
21515
|
+
const clean2 = sanitizeText(value, 4e3);
|
|
21516
|
+
if (!isAbsolute9(clean2)) return clean2;
|
|
21517
|
+
const absolute = resolve23(clean2);
|
|
21518
|
+
const workspaceRelative = relative11(resolve23(workspace), absolute);
|
|
21519
|
+
if (!workspaceRelative.startsWith("..") && !isAbsolute9(workspaceRelative)) {
|
|
21520
|
+
return `<workspace>/${workspaceRelative || "."}`;
|
|
21521
|
+
}
|
|
21522
|
+
const homeRelative = relative11(homedir4(), absolute);
|
|
21523
|
+
if (!homeRelative.startsWith("..") && !isAbsolute9(homeRelative)) return `~/${homeRelative}`;
|
|
21524
|
+
return `<absolute>/${basename12(absolute)}`;
|
|
21525
|
+
}
|
|
21526
|
+
function uniquePermissions(values) {
|
|
21527
|
+
const order = ["read", "write", "shell", "git", "network"];
|
|
21528
|
+
const present = new Set(values);
|
|
21529
|
+
return order.filter((category) => present.has(category));
|
|
21530
|
+
}
|
|
21531
|
+
function unique3(values) {
|
|
21532
|
+
return [...new Set(values)].slice(0, 256);
|
|
21533
|
+
}
|
|
21534
|
+
function sanitizeIdentifier(value, fallback) {
|
|
21535
|
+
return sanitizeText(value, 64).toLocaleLowerCase().replace(/[^a-z0-9_-]+/gu, "_").replace(/^_+|_+$/gu, "") || fallback;
|
|
21536
|
+
}
|
|
21537
|
+
function sanitizeText(value, maxLength) {
|
|
21538
|
+
return stripAnsi5(value).replace(/[\u0000-\u001f\u007f]/gu, " ").replace(/\s+/gu, " ").trim().slice(0, maxLength);
|
|
21539
|
+
}
|
|
21540
|
+
function stableJson2(value) {
|
|
21541
|
+
if (Array.isArray(value)) return `[${value.map(stableJson2).join(",")}]`;
|
|
21542
|
+
if (value && typeof value === "object") {
|
|
21543
|
+
const object = value;
|
|
21544
|
+
return `{${Object.keys(object).sort().map((key) => `${JSON.stringify(key)}:${stableJson2(object[key])}`).join(",")}}`;
|
|
21545
|
+
}
|
|
21546
|
+
return JSON.stringify(value);
|
|
21547
|
+
}
|
|
21548
|
+
|
|
21549
|
+
// src/mcp/trust-store.ts
|
|
21550
|
+
import { lstat as lstat21, mkdir as mkdir10, readFile as readFile20, realpath as realpath9 } from "node:fs/promises";
|
|
21551
|
+
import { dirname as dirname10, join as join21, resolve as resolve24 } from "node:path";
|
|
21552
|
+
import { z as z20 } from "zod";
|
|
21553
|
+
var trustRegistrySchema = z20.object({
|
|
21554
|
+
version: z20.literal(1),
|
|
21555
|
+
entries: z20.array(z20.object({
|
|
21556
|
+
workspace: z20.string(),
|
|
21557
|
+
server: z20.string().regex(/^[a-z][a-z0-9_-]{0,63}$/),
|
|
21558
|
+
fingerprint: z20.string().regex(/^[a-f0-9]{64}$/),
|
|
21559
|
+
state: z20.enum(["trusted", "disabled", "revoked"]),
|
|
21560
|
+
updatedAt: z20.string()
|
|
21561
|
+
}).strict()).max(1e3)
|
|
21562
|
+
}).strict();
|
|
21563
|
+
var McpTrustStore = class {
|
|
21564
|
+
path;
|
|
21565
|
+
usesDefaultPath;
|
|
21566
|
+
constructor(options = {}) {
|
|
21567
|
+
this.usesDefaultPath = options.path === void 0;
|
|
21568
|
+
this.path = options.path ?? join21(resolveHomeNamespace(), "mcp-capability-trust.json");
|
|
21569
|
+
}
|
|
21570
|
+
async state(workspace, server, fingerprint) {
|
|
21571
|
+
const resolvedWorkspace = await resolveWorkspace(workspace);
|
|
21572
|
+
const registry = await this.read();
|
|
21573
|
+
const entry = [...registry.entries].reverse().find((candidate) => candidate.workspace === resolvedWorkspace && candidate.server === server);
|
|
21574
|
+
if (!entry) return "untrusted";
|
|
21575
|
+
if (entry.state === "disabled" || entry.state === "revoked") return entry.state;
|
|
21576
|
+
return entry.fingerprint === fingerprint ? "trusted" : "untrusted";
|
|
21577
|
+
}
|
|
21578
|
+
trust(workspace, server, fingerprint) {
|
|
21579
|
+
return this.writeDecision(workspace, server, fingerprint, "trusted");
|
|
21580
|
+
}
|
|
21581
|
+
disable(workspace, server, fingerprint) {
|
|
21582
|
+
return this.writeDecision(workspace, server, fingerprint, "disabled");
|
|
21583
|
+
}
|
|
21584
|
+
revoke(workspace, server, fingerprint) {
|
|
21585
|
+
return this.writeDecision(workspace, server, fingerprint, "revoked");
|
|
21586
|
+
}
|
|
21587
|
+
async writeDecision(workspace, server, fingerprint, state) {
|
|
21588
|
+
const operation = async () => {
|
|
21589
|
+
const resolvedWorkspace = await resolveWorkspace(workspace);
|
|
21590
|
+
const registry = await this.read();
|
|
21591
|
+
const entries = registry.entries.filter((entry) => entry.workspace !== resolvedWorkspace || entry.server !== server);
|
|
21592
|
+
entries.push({
|
|
21593
|
+
workspace: resolvedWorkspace,
|
|
21594
|
+
server,
|
|
21595
|
+
fingerprint,
|
|
21596
|
+
state,
|
|
21597
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
21598
|
+
});
|
|
21599
|
+
if (this.usesDefaultPath) {
|
|
21600
|
+
const home = resolveHomeNamespace();
|
|
21601
|
+
assertActiveHomeNamespacePath(this.path);
|
|
21602
|
+
await assertNoSymlinkPath(dirname10(home), home);
|
|
21603
|
+
}
|
|
21604
|
+
await mkdir10(dirname10(this.path), { recursive: true, mode: 448 });
|
|
21605
|
+
if (this.usesDefaultPath) {
|
|
21606
|
+
const home = resolveHomeNamespace();
|
|
21607
|
+
await assertNoSymlinkPath(dirname10(home), home);
|
|
21608
|
+
}
|
|
21609
|
+
await atomicWrite(
|
|
21610
|
+
this.path,
|
|
21611
|
+
`${JSON.stringify({ version: 1, entries: entries.slice(-1e3) }, null, 2)}
|
|
21612
|
+
`,
|
|
21613
|
+
384
|
|
21614
|
+
);
|
|
21615
|
+
};
|
|
21616
|
+
return this.usesDefaultPath ? withNamespaceLease(homeNamespacePaths().canonical, "shared", operation) : operation();
|
|
21617
|
+
}
|
|
21618
|
+
async read() {
|
|
21619
|
+
try {
|
|
21620
|
+
const info = await lstat21(this.path);
|
|
21621
|
+
if (!info.isFile() || info.isSymbolicLink() || info.size > 1e6) {
|
|
21622
|
+
return { version: 1, entries: [] };
|
|
21623
|
+
}
|
|
21624
|
+
return trustRegistrySchema.parse(JSON.parse(await readFile20(this.path, "utf8")));
|
|
21625
|
+
} catch {
|
|
21626
|
+
return { version: 1, entries: [] };
|
|
21627
|
+
}
|
|
21628
|
+
}
|
|
21629
|
+
};
|
|
21630
|
+
async function resolveWorkspace(workspace) {
|
|
21631
|
+
return realpath9(resolve24(workspace)).catch(() => resolve24(workspace));
|
|
21632
|
+
}
|
|
21633
|
+
|
|
20790
21634
|
// src/mcp/manager.ts
|
|
20791
21635
|
var MAX_SERVERS = 32;
|
|
20792
21636
|
var MAX_TOOLS_PER_SERVER = 256;
|
|
@@ -20798,6 +21642,9 @@ var McpManager = class {
|
|
|
20798
21642
|
constructor(config, options = {}) {
|
|
20799
21643
|
this.config = config;
|
|
20800
21644
|
this.options = options;
|
|
21645
|
+
this.trustStore = options.trustStore ?? new McpTrustStore();
|
|
21646
|
+
this.workspace = options.cwd ?? process.cwd();
|
|
21647
|
+
this.requireTrust = options.requireTrust !== false;
|
|
20801
21648
|
const entries = Object.entries(config.servers ?? {});
|
|
20802
21649
|
for (const [index, [name, server]] of entries.entries()) {
|
|
20803
21650
|
const transport = server.transport ?? "stdio";
|
|
@@ -20807,12 +21654,22 @@ var McpManager = class {
|
|
|
20807
21654
|
state: "error",
|
|
20808
21655
|
transport,
|
|
20809
21656
|
toolCount: 0,
|
|
21657
|
+
required: server.required === true,
|
|
21658
|
+
trust: "untrusted",
|
|
20810
21659
|
error: `MCP server limit exceeded (maximum ${MAX_SERVERS})`
|
|
20811
21660
|
});
|
|
20812
21661
|
continue;
|
|
20813
21662
|
}
|
|
20814
|
-
const
|
|
20815
|
-
|
|
21663
|
+
const disabled = config.enabled === false || server.enabled === false;
|
|
21664
|
+
const state = disabled ? "disabled" : this.requireTrust ? "untrusted" : "disconnected";
|
|
21665
|
+
this.statuses.set(name, {
|
|
21666
|
+
name,
|
|
21667
|
+
state,
|
|
21668
|
+
transport,
|
|
21669
|
+
toolCount: 0,
|
|
21670
|
+
required: server.required === true,
|
|
21671
|
+
trust: disabled ? "disabled" : this.requireTrust ? "untrusted" : "trusted"
|
|
21672
|
+
});
|
|
20816
21673
|
}
|
|
20817
21674
|
}
|
|
20818
21675
|
config;
|
|
@@ -20822,12 +21679,108 @@ var McpManager = class {
|
|
|
20822
21679
|
statuses = /* @__PURE__ */ new Map();
|
|
20823
21680
|
toolOwners = /* @__PURE__ */ new Map();
|
|
20824
21681
|
stableAdapters = /* @__PURE__ */ new Map();
|
|
21682
|
+
registries = /* @__PURE__ */ new Set();
|
|
20825
21683
|
options;
|
|
21684
|
+
trustStore;
|
|
21685
|
+
workspace;
|
|
21686
|
+
requireTrust;
|
|
20826
21687
|
shutdownController = new AbortController();
|
|
21688
|
+
trustLoaded = false;
|
|
20827
21689
|
closed = false;
|
|
21690
|
+
/** Load persisted trust and prove availability for explicitly required servers. */
|
|
21691
|
+
async initialize(signal) {
|
|
21692
|
+
await this.ensureTrustLoaded();
|
|
21693
|
+
const required = [...this.statuses.values()].filter((status) => status.required && status.state !== "disabled").map((status) => status.name);
|
|
21694
|
+
const failures = [];
|
|
21695
|
+
for (const name of required) {
|
|
21696
|
+
const status = this.statuses.get(name);
|
|
21697
|
+
if (status?.trust !== "trusted") {
|
|
21698
|
+
failures.push(`${name}: capability manifest is ${status?.trust ?? "untrusted"}`);
|
|
21699
|
+
continue;
|
|
21700
|
+
}
|
|
21701
|
+
const result = await this.connect(name, signal);
|
|
21702
|
+
if (!result.ok) failures.push(`${name}: ${result.status.error ?? result.status.state}`);
|
|
21703
|
+
}
|
|
21704
|
+
if (failures.length) {
|
|
21705
|
+
throw new Error(`Required MCP server unavailable: ${failures.join("; ")}`);
|
|
21706
|
+
}
|
|
21707
|
+
}
|
|
21708
|
+
/** Refresh local trust state without connecting; used by status and review UIs. */
|
|
21709
|
+
async loadTrust() {
|
|
21710
|
+
await this.ensureTrustLoaded();
|
|
21711
|
+
}
|
|
21712
|
+
/** Compact, no-network capability controls advertised before remote schemas. */
|
|
21713
|
+
catalogTools(registry) {
|
|
21714
|
+
const names = this.catalogServerNames();
|
|
21715
|
+
if (!names.length) return [];
|
|
21716
|
+
const search = {
|
|
21717
|
+
definition: {
|
|
21718
|
+
name: "mcp_search",
|
|
21719
|
+
description: "Search configured MCP capability manifests without connecting to a server or loading remote schemas.",
|
|
21720
|
+
category: "read",
|
|
21721
|
+
source: "mcp",
|
|
21722
|
+
permissionCategories: ["read"],
|
|
21723
|
+
activation: "catalog",
|
|
21724
|
+
inputSchema: {
|
|
21725
|
+
type: "object",
|
|
21726
|
+
properties: {
|
|
21727
|
+
query: { type: "string", maxLength: 500, description: "Capability, tool, permission, or server to find." }
|
|
21728
|
+
},
|
|
21729
|
+
required: ["query"],
|
|
21730
|
+
additionalProperties: false
|
|
21731
|
+
}
|
|
21732
|
+
},
|
|
21733
|
+
permissionCategories: () => ["read"],
|
|
21734
|
+
execute: async (arguments_) => {
|
|
21735
|
+
const query = typeof arguments_.query === "string" ? arguments_.query.trim() : "";
|
|
21736
|
+
if (query.length > 500) throw new ToolInputError("MCP search query must be at most 500 characters");
|
|
21737
|
+
await this.ensureTrustLoaded();
|
|
21738
|
+
const results = this.search(query);
|
|
21739
|
+
return {
|
|
21740
|
+
content: results.length ? results.map((result) => `${result.name}: ${result.description} (${result.trust}, ${result.declaredTools || "dynamic"} tools)`).join("\n") : "No configured MCP capability matched the query.",
|
|
21741
|
+
metadata: { mcpSearch: results }
|
|
21742
|
+
};
|
|
21743
|
+
}
|
|
21744
|
+
};
|
|
21745
|
+
const inspect = {
|
|
21746
|
+
definition: {
|
|
21747
|
+
name: "mcp_inspect",
|
|
21748
|
+
description: "Inspect one redacted declarative MCP capability manifest and its local trust state without connecting.",
|
|
21749
|
+
category: "read",
|
|
21750
|
+
source: "mcp",
|
|
21751
|
+
permissionCategories: ["read"],
|
|
21752
|
+
activation: "catalog",
|
|
21753
|
+
inputSchema: {
|
|
21754
|
+
type: "object",
|
|
21755
|
+
properties: {
|
|
21756
|
+
server: { type: "string", enum: names, description: "Configured MCP server to inspect." }
|
|
21757
|
+
},
|
|
21758
|
+
required: ["server"],
|
|
21759
|
+
additionalProperties: false
|
|
21760
|
+
}
|
|
21761
|
+
},
|
|
21762
|
+
permissionCategories: () => ["read"],
|
|
21763
|
+
execute: async (arguments_) => {
|
|
21764
|
+
const server = typeof arguments_.server === "string" ? arguments_.server : "";
|
|
21765
|
+
if (!names.includes(server)) throw new ToolInputError("MCP server is not available for inspection");
|
|
21766
|
+
await this.ensureTrustLoaded();
|
|
21767
|
+
const manifest = this.inspect(server);
|
|
21768
|
+
return {
|
|
21769
|
+
content: JSON.stringify({ manifest, trust: this.status(server)?.trust ?? "untrusted" }, null, 2),
|
|
21770
|
+
metadata: {
|
|
21771
|
+
mcpServer: server,
|
|
21772
|
+
trust: this.status(server)?.trust ?? "untrusted",
|
|
21773
|
+
manifestFingerprint: this.fingerprint(server)
|
|
21774
|
+
}
|
|
21775
|
+
};
|
|
21776
|
+
}
|
|
21777
|
+
};
|
|
21778
|
+
const activation = this.activationTool(registry);
|
|
21779
|
+
return activation ? [search, inspect, activation] : [search, inspect];
|
|
21780
|
+
}
|
|
20828
21781
|
/** Compact model-visible catalog; transport and remote discovery stay lazy. */
|
|
20829
21782
|
activationTool(registry) {
|
|
20830
|
-
const names = this.
|
|
21783
|
+
const names = this.catalogServerNames();
|
|
20831
21784
|
if (!names.length) return;
|
|
20832
21785
|
const catalog = names.map((name) => {
|
|
20833
21786
|
const server = this.config.servers[name];
|
|
@@ -20837,8 +21790,11 @@ var McpManager = class {
|
|
|
20837
21790
|
return {
|
|
20838
21791
|
definition: {
|
|
20839
21792
|
name: "mcp_activate",
|
|
20840
|
-
description: `
|
|
21793
|
+
description: `Activate one already trusted MCP capability after mcp_search and mcp_inspect, then load at most ${LAZY_SCHEMA_LIMIT} relevant schemas. This tool cannot grant trust. Available servers: ${catalog}`,
|
|
20841
21794
|
category: "network",
|
|
21795
|
+
source: "mcp",
|
|
21796
|
+
permissionCategories: ["network"],
|
|
21797
|
+
activation: "catalog",
|
|
20842
21798
|
inputSchema: {
|
|
20843
21799
|
type: "object",
|
|
20844
21800
|
properties: {
|
|
@@ -20859,9 +21815,10 @@ var McpManager = class {
|
|
|
20859
21815
|
}
|
|
20860
21816
|
const result = await this.activate(server, query, registry, context.signal);
|
|
20861
21817
|
if (!result.ok) {
|
|
21818
|
+
const trust = result.status.trust;
|
|
20862
21819
|
return {
|
|
20863
21820
|
ok: false,
|
|
20864
|
-
content: `MCP server ${server} could not be activated: ${result.status.error ?? result.status.state}`,
|
|
21821
|
+
content: trust !== "trusted" ? `MCP server ${server} is ${trust}. Review it with mcp_inspect; only the user can trust it with the CLI or /mcp trust confirmation flow.` : `MCP server ${server} could not be activated: ${result.status.error ?? result.status.state}`,
|
|
20865
21822
|
metadata: activationMetadata(result)
|
|
20866
21823
|
};
|
|
20867
21824
|
}
|
|
@@ -20876,6 +21833,7 @@ var McpManager = class {
|
|
|
20876
21833
|
}
|
|
20877
21834
|
/** Connect/discover one server, then register only request-relevant schemas. */
|
|
20878
21835
|
async activate(name, query, registry, signal) {
|
|
21836
|
+
await this.ensureTrustLoaded();
|
|
20879
21837
|
const connected = await this.connect(name, signal);
|
|
20880
21838
|
if (!connected.ok) {
|
|
20881
21839
|
return { ...connected, registeredTools: [], availableTools: 0, deferredTools: 0 };
|
|
@@ -20887,16 +21845,27 @@ var McpManager = class {
|
|
|
20887
21845
|
const tools = [...connection.tools.values()];
|
|
20888
21846
|
const selected = tools.length <= LAZY_SCHEMA_LIMIT ? tools : selectRelevantTools(tools, query, LAZY_SCHEMA_LIMIT);
|
|
20889
21847
|
this.registerSelectedTools(registry, selected);
|
|
21848
|
+
const ranked = rankRelevantTools(tools, query);
|
|
21849
|
+
const eagerTokens = estimateTokens(JSON.stringify(tools.map((tool) => tool.definition)));
|
|
21850
|
+
const loadedTokens = estimateTokens(JSON.stringify(selected.map((tool) => tool.definition)));
|
|
20890
21851
|
return {
|
|
20891
21852
|
...connected,
|
|
20892
21853
|
registeredTools: selected.map((tool) => tool.definition.name),
|
|
20893
21854
|
availableTools: tools.length,
|
|
20894
|
-
deferredTools: Math.max(0, tools.length - selected.length)
|
|
21855
|
+
deferredTools: Math.max(0, tools.length - selected.length),
|
|
21856
|
+
schemaBudget: {
|
|
21857
|
+
eagerTokens,
|
|
21858
|
+
loadedTokens,
|
|
21859
|
+
savedTokens: Math.max(0, eagerTokens - loadedTokens),
|
|
21860
|
+
topMatch: ranked[0]?.tool.definition.name ?? null,
|
|
21861
|
+
queryMatched: (ranked[0]?.score ?? 0) > 0
|
|
21862
|
+
}
|
|
20895
21863
|
};
|
|
20896
21864
|
}
|
|
20897
21865
|
/** Connect enabled servers with a small concurrency bound. */
|
|
20898
21866
|
async connectAll(signal) {
|
|
20899
21867
|
if (this.closed) throw new Error("MCP manager is closed");
|
|
21868
|
+
await this.ensureTrustLoaded();
|
|
20900
21869
|
const configuredNames = Object.keys(this.config.servers ?? {});
|
|
20901
21870
|
const names = configuredNames.slice(0, MAX_SERVERS);
|
|
20902
21871
|
if (this.config.enabled === false) {
|
|
@@ -20917,10 +21886,14 @@ var McpManager = class {
|
|
|
20917
21886
|
/** Connect one configured server. Connection errors are captured in status. */
|
|
20918
21887
|
async connect(name, signal) {
|
|
20919
21888
|
if (this.closed) throw new Error("MCP manager is closed");
|
|
21889
|
+
await this.ensureTrustLoaded();
|
|
20920
21890
|
const status = this.statuses.get(name);
|
|
20921
21891
|
if (status?.state === "error" && status.error?.includes("server limit exceeded")) {
|
|
20922
21892
|
return this.resultFor(name, false, 0);
|
|
20923
21893
|
}
|
|
21894
|
+
if (this.requireTrust && status?.trust !== "trusted") {
|
|
21895
|
+
return this.resultFor(name, false, 0);
|
|
21896
|
+
}
|
|
20924
21897
|
const existing = this.pending.get(name);
|
|
20925
21898
|
if (existing) return existing;
|
|
20926
21899
|
const connectionController = new AbortController();
|
|
@@ -20948,9 +21921,11 @@ var McpManager = class {
|
|
|
20948
21921
|
if (!current) throw new Error(`Unknown MCP server: ${name}`);
|
|
20949
21922
|
const status = {
|
|
20950
21923
|
name,
|
|
20951
|
-
state: current.state === "disabled"
|
|
21924
|
+
state: current.state === "disabled" || current.state === "revoked" || current.state === "untrusted" ? current.state : "disconnected",
|
|
20952
21925
|
transport: current.transport,
|
|
20953
|
-
toolCount: 0
|
|
21926
|
+
toolCount: 0,
|
|
21927
|
+
required: current.required,
|
|
21928
|
+
trust: current.trust
|
|
20954
21929
|
};
|
|
20955
21930
|
this.statuses.set(name, status);
|
|
20956
21931
|
return status;
|
|
@@ -20960,7 +21935,9 @@ var McpManager = class {
|
|
|
20960
21935
|
if (this.closed) throw new Error("MCP manager is closed");
|
|
20961
21936
|
const status = this.statuses.get(name);
|
|
20962
21937
|
if (!status) throw new Error(`Unknown MCP server: ${name}`);
|
|
20963
|
-
if (status.state === "disabled"
|
|
21938
|
+
if (status.state === "disabled" || status.state === "revoked" || status.state === "untrusted") {
|
|
21939
|
+
return this.resultFor(name, false, 0);
|
|
21940
|
+
}
|
|
20964
21941
|
const pending = this.pending.get(name);
|
|
20965
21942
|
if (pending) await pending;
|
|
20966
21943
|
await this.disconnect(name);
|
|
@@ -20984,9 +21961,11 @@ var McpManager = class {
|
|
|
20984
21961
|
for (const [name, status] of this.statuses) {
|
|
20985
21962
|
this.statuses.set(name, {
|
|
20986
21963
|
name,
|
|
20987
|
-
state: status.state === "disabled"
|
|
21964
|
+
state: status.state === "disabled" || status.state === "revoked" || status.state === "untrusted" ? status.state : "closed",
|
|
20988
21965
|
transport: status.transport,
|
|
20989
|
-
toolCount: 0
|
|
21966
|
+
toolCount: 0,
|
|
21967
|
+
required: status.required,
|
|
21968
|
+
trust: status.trust
|
|
20990
21969
|
});
|
|
20991
21970
|
}
|
|
20992
21971
|
}
|
|
@@ -20997,6 +21976,71 @@ var McpManager = class {
|
|
|
20997
21976
|
const status = this.statuses.get(name);
|
|
20998
21977
|
return status ? { ...status } : void 0;
|
|
20999
21978
|
}
|
|
21979
|
+
search(query = "") {
|
|
21980
|
+
return searchMcpCapabilities(this.config, query).map((result) => {
|
|
21981
|
+
const status = this.statuses.get(result.name);
|
|
21982
|
+
return {
|
|
21983
|
+
...result,
|
|
21984
|
+
state: status?.state ?? "error",
|
|
21985
|
+
trust: status?.trust ?? "untrusted"
|
|
21986
|
+
};
|
|
21987
|
+
});
|
|
21988
|
+
}
|
|
21989
|
+
inspect(name) {
|
|
21990
|
+
const server = this.config.servers?.[name];
|
|
21991
|
+
if (!server) throw new Error(`Unknown MCP server: ${name}`);
|
|
21992
|
+
return buildMcpCapabilityManifest(name, server, this.workspace);
|
|
21993
|
+
}
|
|
21994
|
+
fingerprint(name) {
|
|
21995
|
+
const server = this.config.servers?.[name];
|
|
21996
|
+
if (!server) throw new Error(`Unknown MCP server: ${name}`);
|
|
21997
|
+
return capabilityFingerprint(name, server, this.workspace);
|
|
21998
|
+
}
|
|
21999
|
+
async trust(name) {
|
|
22000
|
+
const server = this.config.servers?.[name];
|
|
22001
|
+
if (!server) throw new Error(`Unknown MCP server: ${name}`);
|
|
22002
|
+
if (this.config.enabled === false || server.enabled === false) {
|
|
22003
|
+
throw new Error(`MCP server is disabled by configuration: ${name}`);
|
|
22004
|
+
}
|
|
22005
|
+
if (this.inspect(name).dynamicTools) {
|
|
22006
|
+
throw new Error(`MCP capability ${name} cannot be trusted until its tools and effects are declared.`);
|
|
22007
|
+
}
|
|
22008
|
+
await this.trustStore.trust(this.workspace, name, this.fingerprint(name));
|
|
22009
|
+
this.trustLoaded = true;
|
|
22010
|
+
return this.setStatus(name, {
|
|
22011
|
+
state: "disconnected",
|
|
22012
|
+
trust: "trusted",
|
|
22013
|
+
required: server.required === true,
|
|
22014
|
+
transport: server.transport ?? "stdio",
|
|
22015
|
+
toolCount: 0
|
|
22016
|
+
});
|
|
22017
|
+
}
|
|
22018
|
+
async disable(name) {
|
|
22019
|
+
const server = this.config.servers?.[name];
|
|
22020
|
+
if (!server) throw new Error(`Unknown MCP server: ${name}`);
|
|
22021
|
+
await this.disconnect(name);
|
|
22022
|
+
this.unregisterServerTools(name);
|
|
22023
|
+
await this.trustStore.disable(this.workspace, name, this.fingerprint(name));
|
|
22024
|
+
return this.setStatus(name, {
|
|
22025
|
+
state: "disabled",
|
|
22026
|
+
trust: "disabled",
|
|
22027
|
+
required: server.required === true,
|
|
22028
|
+
toolCount: 0
|
|
22029
|
+
});
|
|
22030
|
+
}
|
|
22031
|
+
async revoke(name) {
|
|
22032
|
+
const server = this.config.servers?.[name];
|
|
22033
|
+
if (!server) throw new Error(`Unknown MCP server: ${name}`);
|
|
22034
|
+
await this.disconnect(name);
|
|
22035
|
+
this.unregisterServerTools(name);
|
|
22036
|
+
await this.trustStore.revoke(this.workspace, name, this.fingerprint(name));
|
|
22037
|
+
return this.setStatus(name, {
|
|
22038
|
+
state: "revoked",
|
|
22039
|
+
trust: "revoked",
|
|
22040
|
+
required: server.required === true,
|
|
22041
|
+
toolCount: 0
|
|
22042
|
+
});
|
|
22043
|
+
}
|
|
21000
22044
|
tools() {
|
|
21001
22045
|
return [...this.connections.values()].flatMap((connection) => [...connection.tools.values()]).sort((a, b) => a.definition.name.localeCompare(b.definition.name));
|
|
21002
22046
|
}
|
|
@@ -21008,6 +22052,7 @@ var McpManager = class {
|
|
|
21008
22052
|
return this.registerSelectedTools(registry, this.tools());
|
|
21009
22053
|
}
|
|
21010
22054
|
registerSelectedTools(registry, tools) {
|
|
22055
|
+
this.registries.add(registry);
|
|
21011
22056
|
const registered = [];
|
|
21012
22057
|
for (const tool of tools) {
|
|
21013
22058
|
const existing = registry.get(tool.definition.name);
|
|
@@ -21022,9 +22067,38 @@ var McpManager = class {
|
|
|
21022
22067
|
}
|
|
21023
22068
|
return registered;
|
|
21024
22069
|
}
|
|
21025
|
-
|
|
21026
|
-
|
|
21027
|
-
|
|
22070
|
+
unregisterServerTools(name) {
|
|
22071
|
+
for (const [identity2, tool] of this.stableAdapters) {
|
|
22072
|
+
if (!identity2.startsWith(`${name}\0`)) continue;
|
|
22073
|
+
for (const registry of this.registries) {
|
|
22074
|
+
registry.unregister(tool.definition.name, tool);
|
|
22075
|
+
}
|
|
22076
|
+
}
|
|
22077
|
+
}
|
|
22078
|
+
catalogServerNames() {
|
|
22079
|
+
return [...this.statuses.values()].filter((status) => !status.error?.includes("server limit exceeded")).map((status) => status.name).sort((left, right) => left.localeCompare(right));
|
|
22080
|
+
}
|
|
22081
|
+
async ensureTrustLoaded() {
|
|
22082
|
+
if (this.trustLoaded) return;
|
|
22083
|
+
for (const [name, server] of Object.entries(this.config.servers ?? {}).slice(0, MAX_SERVERS)) {
|
|
22084
|
+
const current = this.statuses.get(name);
|
|
22085
|
+
if (!current) continue;
|
|
22086
|
+
if (this.config.enabled === false || server.enabled === false) {
|
|
22087
|
+
this.setStatus(name, { state: "disabled", trust: "disabled", toolCount: 0 });
|
|
22088
|
+
continue;
|
|
22089
|
+
}
|
|
22090
|
+
if (!this.requireTrust) {
|
|
22091
|
+
this.setStatus(name, { state: "disconnected", trust: "trusted", toolCount: 0 });
|
|
22092
|
+
continue;
|
|
22093
|
+
}
|
|
22094
|
+
const trust = await this.trustStore.state(this.workspace, name, this.fingerprint(name));
|
|
22095
|
+
this.setStatus(name, {
|
|
22096
|
+
state: trust === "trusted" ? "disconnected" : trust,
|
|
22097
|
+
trust,
|
|
22098
|
+
toolCount: 0
|
|
22099
|
+
});
|
|
22100
|
+
}
|
|
22101
|
+
this.trustLoaded = true;
|
|
21028
22102
|
}
|
|
21029
22103
|
async connectInternal(name, signal) {
|
|
21030
22104
|
const configured = this.config.servers?.[name];
|
|
@@ -21040,7 +22114,12 @@ var McpManager = class {
|
|
|
21040
22114
|
return { name, ok: false, status, skippedTools: 0 };
|
|
21041
22115
|
}
|
|
21042
22116
|
if (this.config.enabled === false || configured.enabled === false) {
|
|
21043
|
-
const status = this.setStatus(name, {
|
|
22117
|
+
const status = this.setStatus(name, {
|
|
22118
|
+
state: "disabled",
|
|
22119
|
+
trust: "disabled",
|
|
22120
|
+
transport: transportKind,
|
|
22121
|
+
toolCount: 0
|
|
22122
|
+
});
|
|
21044
22123
|
return { name, ok: false, status, skippedTools: 0 };
|
|
21045
22124
|
}
|
|
21046
22125
|
if (this.connections.has(name)) {
|
|
@@ -21090,6 +22169,7 @@ var McpManager = class {
|
|
|
21090
22169
|
const listed = await this.listRemoteTools(client, timeoutMs, signal);
|
|
21091
22170
|
if (closedDuringConnect) throw new Error("MCP server closed during connection setup");
|
|
21092
22171
|
const toolMap = this.buildAdapters(name, configured, listed.tools);
|
|
22172
|
+
const undeclaredTools = configured.tools?.length ? listed.tools.filter((tool) => !configured.tools?.some((declared) => declared.name === tool.name)).length : 0;
|
|
21093
22173
|
const connection = {
|
|
21094
22174
|
name,
|
|
21095
22175
|
client,
|
|
@@ -21111,7 +22191,12 @@ var McpManager = class {
|
|
|
21111
22191
|
);
|
|
21112
22192
|
}
|
|
21113
22193
|
const status = this.setStatus(name, statusPatch);
|
|
21114
|
-
return {
|
|
22194
|
+
return {
|
|
22195
|
+
name,
|
|
22196
|
+
ok: true,
|
|
22197
|
+
status,
|
|
22198
|
+
skippedTools: listed.skippedTools + listed.truncatedTools + undeclaredTools
|
|
22199
|
+
};
|
|
21115
22200
|
} catch (error) {
|
|
21116
22201
|
if (client) await closeQuietly(client);
|
|
21117
22202
|
else if (transport) await closeTransportQuietly(transport);
|
|
@@ -21150,7 +22235,7 @@ var McpManager = class {
|
|
|
21150
22235
|
stderr: "pipe"
|
|
21151
22236
|
});
|
|
21152
22237
|
transport.stderr?.on("data", (chunk) => {
|
|
21153
|
-
const text =
|
|
22238
|
+
const text = stripAnsi6(chunk.toString()).replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, "").trim();
|
|
21154
22239
|
if (text) this.options.logger?.(`MCP ${name} stderr`, { text: text.slice(0, 2e3) });
|
|
21155
22240
|
});
|
|
21156
22241
|
return transport;
|
|
@@ -21200,6 +22285,8 @@ var McpManager = class {
|
|
|
21200
22285
|
DEFAULT_TOOL_TIMEOUT
|
|
21201
22286
|
);
|
|
21202
22287
|
for (const remoteTool of remoteTools) {
|
|
22288
|
+
const capability = declaredToolCapability(config, remoteTool.name, this.workspace);
|
|
22289
|
+
if (config.tools?.length && !capability) continue;
|
|
21203
22290
|
let exposedName = makeMcpToolName(namespace, remoteTool.name);
|
|
21204
22291
|
const identity2 = `${serverName}\0${remoteTool.name}`;
|
|
21205
22292
|
if (seen.has(exposedName) || this.isToolNameOwnedByAnother(exposedName, identity2)) {
|
|
@@ -21229,7 +22316,8 @@ var McpManager = class {
|
|
|
21229
22316
|
exposedName,
|
|
21230
22317
|
remoteTool,
|
|
21231
22318
|
timeoutMs,
|
|
21232
|
-
callTool
|
|
22319
|
+
callTool,
|
|
22320
|
+
...capability ? { capability } : {}
|
|
21233
22321
|
});
|
|
21234
22322
|
this.stableAdapters.set(identity2, adapter);
|
|
21235
22323
|
}
|
|
@@ -21264,6 +22352,8 @@ var McpManager = class {
|
|
|
21264
22352
|
state: patch.state ?? current?.state ?? "disconnected",
|
|
21265
22353
|
transport: patch.transport ?? current?.transport ?? "stdio",
|
|
21266
22354
|
toolCount: patch.toolCount ?? current?.toolCount ?? 0,
|
|
22355
|
+
required: patch.required ?? current?.required ?? false,
|
|
22356
|
+
trust: patch.trust ?? current?.trust ?? "untrusted",
|
|
21267
22357
|
...patch.connectedAt !== void 0 ? { connectedAt: patch.connectedAt } : current?.connectedAt !== void 0 ? { connectedAt: current.connectedAt } : {},
|
|
21268
22358
|
...patch.serverVersion !== void 0 ? { serverVersion: patch.serverVersion } : current?.serverVersion !== void 0 ? { serverVersion: current.serverVersion } : {},
|
|
21269
22359
|
...patch.error !== void 0 ? { error: patch.error } : {}
|
|
@@ -21284,20 +22374,24 @@ function activationMetadata(result) {
|
|
|
21284
22374
|
availableTools: result.availableTools,
|
|
21285
22375
|
loadedTools: result.registeredTools,
|
|
21286
22376
|
deferredTools: result.deferredTools,
|
|
21287
|
-
skippedTools: result.skippedTools
|
|
22377
|
+
skippedTools: result.skippedTools,
|
|
22378
|
+
...result.schemaBudget ? { schemaBudget: result.schemaBudget } : {}
|
|
21288
22379
|
};
|
|
21289
22380
|
}
|
|
21290
22381
|
function selectRelevantTools(tools, query, limit) {
|
|
22382
|
+
return rankRelevantTools(tools, query).slice(0, limit).map(({ tool }) => tool);
|
|
22383
|
+
}
|
|
22384
|
+
function rankRelevantTools(tools, query) {
|
|
21291
22385
|
const terms = new Set(query.toLocaleLowerCase().match(/[\p{L}\p{N}_-]{2,}/gu) ?? []);
|
|
21292
22386
|
return tools.map((tool) => {
|
|
21293
22387
|
const searchable = `${tool.definition.name.replaceAll("_", " ")} ${tool.definition.description}`.toLocaleLowerCase();
|
|
21294
22388
|
let score = 0;
|
|
21295
22389
|
for (const term of terms) if (searchable.includes(term)) score += term.length;
|
|
21296
22390
|
return { tool, score };
|
|
21297
|
-
}).sort((left, right) => right.score - left.score || left.tool.definition.name.localeCompare(right.tool.definition.name))
|
|
22391
|
+
}).sort((left, right) => right.score - left.score || left.tool.definition.name.localeCompare(right.tool.definition.name));
|
|
21298
22392
|
}
|
|
21299
22393
|
function sanitizeCatalogText(value) {
|
|
21300
|
-
return value ?
|
|
22394
|
+
return value ? stripAnsi6(value).replace(/[\u0000-\u001f\u007f]/g, " ").replace(/\s+/g, " ").trim().slice(0, 200) : "";
|
|
21301
22395
|
}
|
|
21302
22396
|
function requestOptions(timeoutMs, signal) {
|
|
21303
22397
|
return {
|
|
@@ -21331,7 +22425,7 @@ function errorMessage2(error) {
|
|
|
21331
22425
|
return sanitizeStatusText(message2) || "Unknown MCP error";
|
|
21332
22426
|
}
|
|
21333
22427
|
function sanitizeStatusText(value) {
|
|
21334
|
-
return
|
|
22428
|
+
return stripAnsi6(value).replace(/[\u0000-\u001f\u007f]/g, " ").trim().slice(0, 1e3);
|
|
21335
22429
|
}
|
|
21336
22430
|
function timeoutError(timeoutMs) {
|
|
21337
22431
|
const error = new Error(`MCP request timed out after ${timeoutMs} ms`);
|
|
@@ -21357,9 +22451,9 @@ async function closeTransportQuietly(transport) {
|
|
|
21357
22451
|
}
|
|
21358
22452
|
|
|
21359
22453
|
// src/memory/tools.ts
|
|
21360
|
-
import { z as
|
|
21361
|
-
var scopeSchema =
|
|
21362
|
-
var kindSchema =
|
|
22454
|
+
import { z as z21 } from "zod";
|
|
22455
|
+
var scopeSchema = z21.enum(["user", "workspace", "session", "agent"]);
|
|
22456
|
+
var kindSchema = z21.enum(["semantic", "episodic", "procedural"]);
|
|
21363
22457
|
function createMemoryTools(store) {
|
|
21364
22458
|
return [
|
|
21365
22459
|
{
|
|
@@ -21367,6 +22461,8 @@ function createMemoryTools(store) {
|
|
|
21367
22461
|
name: "memory_search",
|
|
21368
22462
|
description: "Search durable user, workspace, session, and agent memories relevant to the task.",
|
|
21369
22463
|
category: "read",
|
|
22464
|
+
source: "memory",
|
|
22465
|
+
activation: "always",
|
|
21370
22466
|
inputSchema: jsonSchema({
|
|
21371
22467
|
query: { type: "string", description: "Natural-language memory query." },
|
|
21372
22468
|
scope: { type: "string", enum: ["user", "workspace", "session", "agent"] },
|
|
@@ -21374,10 +22470,10 @@ function createMemoryTools(store) {
|
|
|
21374
22470
|
}, ["query"])
|
|
21375
22471
|
},
|
|
21376
22472
|
async execute(arguments_, context) {
|
|
21377
|
-
const input2 =
|
|
21378
|
-
query:
|
|
22473
|
+
const input2 = z21.object({
|
|
22474
|
+
query: z21.string().max(4e3),
|
|
21379
22475
|
scope: scopeSchema.optional(),
|
|
21380
|
-
limit:
|
|
22476
|
+
limit: z21.number().int().min(1).max(20).optional()
|
|
21381
22477
|
}).parse(arguments_);
|
|
21382
22478
|
const scopes = input2.scope ? [{ scope: input2.scope, scopeKey: scopeKey(input2.scope, context) }] : availableScopes(context);
|
|
21383
22479
|
const records = store.search(input2.query, { scopes, limit: input2.limit ?? 8 });
|
|
@@ -21395,6 +22491,8 @@ ${record.content}`
|
|
|
21395
22491
|
name: "memory_propose",
|
|
21396
22492
|
description: "Propose a non-secret durable memory for user review. The candidate remains inactive until the user explicitly approves it.",
|
|
21397
22493
|
category: "write",
|
|
22494
|
+
source: "memory",
|
|
22495
|
+
activation: "always",
|
|
21398
22496
|
inputSchema: jsonSchema({
|
|
21399
22497
|
content: { type: "string", description: "Concise fact, preference, experience, or procedure to propose. Never include secrets." },
|
|
21400
22498
|
rationale: { type: "string", description: "Why this is useful beyond the current response and what evidence supports it." },
|
|
@@ -21409,17 +22507,17 @@ ${record.content}`
|
|
|
21409
22507
|
}, ["content", "rationale"])
|
|
21410
22508
|
},
|
|
21411
22509
|
async execute(arguments_, context) {
|
|
21412
|
-
const input2 =
|
|
21413
|
-
content:
|
|
21414
|
-
rationale:
|
|
22510
|
+
const input2 = z21.object({
|
|
22511
|
+
content: z21.string().min(1).max(12e3),
|
|
22512
|
+
rationale: z21.string().min(1).max(1e3),
|
|
21415
22513
|
scope: scopeSchema.optional(),
|
|
21416
22514
|
kind: kindSchema.optional(),
|
|
21417
|
-
tags:
|
|
21418
|
-
importance:
|
|
21419
|
-
confidence:
|
|
21420
|
-
agent:
|
|
21421
|
-
revision:
|
|
21422
|
-
conflictKey:
|
|
22515
|
+
tags: z21.array(z21.string()).max(24).optional(),
|
|
22516
|
+
importance: z21.number().min(0).max(1).optional(),
|
|
22517
|
+
confidence: z21.number().min(0).max(1).optional(),
|
|
22518
|
+
agent: z21.string().max(64).optional(),
|
|
22519
|
+
revision: z21.string().max(240).optional(),
|
|
22520
|
+
conflictKey: z21.string().max(240).optional()
|
|
21423
22521
|
}).strict().parse(arguments_);
|
|
21424
22522
|
const scope = input2.scope ?? "workspace";
|
|
21425
22523
|
const candidate = store.propose({
|
|
@@ -21451,15 +22549,17 @@ ${record.content}`
|
|
|
21451
22549
|
name: "memory_forget",
|
|
21452
22550
|
description: "Archive or permanently delete a memory by id.",
|
|
21453
22551
|
category: "write",
|
|
22552
|
+
source: "memory",
|
|
22553
|
+
activation: "always",
|
|
21454
22554
|
inputSchema: jsonSchema({
|
|
21455
22555
|
id: { type: "string" },
|
|
21456
22556
|
permanent: { type: "boolean" }
|
|
21457
22557
|
}, ["id"])
|
|
21458
22558
|
},
|
|
21459
22559
|
async execute(arguments_) {
|
|
21460
|
-
const input2 =
|
|
21461
|
-
id:
|
|
21462
|
-
permanent:
|
|
22560
|
+
const input2 = z21.object({
|
|
22561
|
+
id: z21.string().uuid(),
|
|
22562
|
+
permanent: z21.boolean().optional()
|
|
21463
22563
|
}).parse(arguments_);
|
|
21464
22564
|
const changed = input2.permanent ? store.remove(input2.id) : store.archive(input2.id);
|
|
21465
22565
|
return {
|
|
@@ -21486,9 +22586,9 @@ function scopeKey(scope, context) {
|
|
|
21486
22586
|
}
|
|
21487
22587
|
|
|
21488
22588
|
// src/skills/catalog.ts
|
|
21489
|
-
import { lstat as
|
|
21490
|
-
import { homedir as
|
|
21491
|
-
import { basename as
|
|
22589
|
+
import { lstat as lstat22, readFile as readFile21, readdir as readdir9, realpath as realpath10 } from "node:fs/promises";
|
|
22590
|
+
import { homedir as homedir5 } from "node:os";
|
|
22591
|
+
import { basename as basename13, join as join22, resolve as resolve25 } from "node:path";
|
|
21492
22592
|
import { parse as parseYaml3 } from "yaml";
|
|
21493
22593
|
var SkillCatalog = class {
|
|
21494
22594
|
constructor(workspace, config) {
|
|
@@ -21508,7 +22608,7 @@ var SkillCatalog = class {
|
|
|
21508
22608
|
for (const location of locations) {
|
|
21509
22609
|
const entries = await safeDirectories(location.path);
|
|
21510
22610
|
for (const entry of entries) {
|
|
21511
|
-
const skillPath =
|
|
22611
|
+
const skillPath = join22(location.path, entry, "SKILL.md");
|
|
21512
22612
|
const metadata = await readMetadata(skillPath);
|
|
21513
22613
|
if (!metadata) continue;
|
|
21514
22614
|
const descriptor = {
|
|
@@ -21556,30 +22656,30 @@ ${skill.content}
|
|
|
21556
22656
|
</active-skills>`;
|
|
21557
22657
|
}
|
|
21558
22658
|
function discoveryLocations(workspace, configured) {
|
|
21559
|
-
const home =
|
|
21560
|
-
const workspaceRoot =
|
|
22659
|
+
const home = homedir5();
|
|
22660
|
+
const workspaceRoot = resolve25(workspace);
|
|
21561
22661
|
return [
|
|
21562
|
-
{ path:
|
|
21563
|
-
{ path:
|
|
21564
|
-
{ path:
|
|
21565
|
-
{ path:
|
|
22662
|
+
{ path: join22(home, ".agents", "skills"), scope: "user", trusted: true },
|
|
22663
|
+
{ path: join22(home, ".claude", "skills"), scope: "user", trusted: true },
|
|
22664
|
+
{ path: join22(home, ".augment", "skills"), scope: "user", trusted: true },
|
|
22665
|
+
{ path: join22(resolveHomeNamespace(), "skills"), scope: "user", trusted: true },
|
|
21566
22666
|
...configured.map((path) => {
|
|
21567
|
-
const resolved =
|
|
22667
|
+
const resolved = resolve25(workspaceRoot, path);
|
|
21568
22668
|
return {
|
|
21569
22669
|
path: resolved,
|
|
21570
22670
|
scope: "configured",
|
|
21571
22671
|
trusted: !isInside(workspaceRoot, resolved)
|
|
21572
22672
|
};
|
|
21573
22673
|
}),
|
|
21574
|
-
{ path:
|
|
21575
|
-
{ path:
|
|
21576
|
-
{ path:
|
|
21577
|
-
{ path:
|
|
22674
|
+
{ path: join22(workspace, ".agents", "skills"), scope: "workspace", trusted: false },
|
|
22675
|
+
{ path: join22(workspace, ".claude", "skills"), scope: "workspace", trusted: false },
|
|
22676
|
+
{ path: join22(workspace, ".augment", "skills"), scope: "workspace", trusted: false },
|
|
22677
|
+
{ path: join22(resolveProjectNamespaceSync(workspaceRoot).active, "skills"), scope: "workspace", trusted: false }
|
|
21578
22678
|
];
|
|
21579
22679
|
}
|
|
21580
22680
|
async function safeDirectories(path) {
|
|
21581
22681
|
try {
|
|
21582
|
-
const info = await
|
|
22682
|
+
const info = await lstat22(path);
|
|
21583
22683
|
if (!info.isDirectory() || info.isSymbolicLink()) return [];
|
|
21584
22684
|
const entries = await readdir9(path, { withFileTypes: true });
|
|
21585
22685
|
return entries.filter((entry) => entry.isDirectory() && !entry.isSymbolicLink()).map((entry) => entry.name).slice(0, 500);
|
|
@@ -21593,7 +22693,7 @@ async function readMetadata(path) {
|
|
|
21593
22693
|
const { frontmatter } = splitFrontmatter(raw);
|
|
21594
22694
|
if (!frontmatter) return void 0;
|
|
21595
22695
|
const parsed = parseYaml3(frontmatter);
|
|
21596
|
-
const name = typeof parsed?.name === "string" ? parsed.name.trim() :
|
|
22696
|
+
const name = typeof parsed?.name === "string" ? parsed.name.trim() : basename13(resolve25(path, ".."));
|
|
21597
22697
|
const description = typeof parsed?.description === "string" ? parsed.description.trim() : "";
|
|
21598
22698
|
if (!/^[a-z][a-z0-9_-]{0,63}$/.test(name) || !description || description.length > 1e3) {
|
|
21599
22699
|
return void 0;
|
|
@@ -21612,13 +22712,13 @@ async function readSkill(path, maxChars) {
|
|
|
21612
22712
|
}
|
|
21613
22713
|
async function safeRead(path, maxBytes) {
|
|
21614
22714
|
try {
|
|
21615
|
-
const info = await
|
|
22715
|
+
const info = await lstat22(path);
|
|
21616
22716
|
if (!info.isFile() || info.isSymbolicLink() || info.size > maxBytes) return void 0;
|
|
21617
|
-
const parent =
|
|
21618
|
-
const resolvedParent = await
|
|
21619
|
-
const resolvedPath = await
|
|
22717
|
+
const parent = resolve25(path, "..");
|
|
22718
|
+
const resolvedParent = await realpath10(parent);
|
|
22719
|
+
const resolvedPath = await realpath10(path);
|
|
21620
22720
|
if (!resolvedPath.startsWith(`${resolvedParent}/`)) return void 0;
|
|
21621
|
-
return await
|
|
22721
|
+
return await readFile21(path, "utf8");
|
|
21622
22722
|
} catch {
|
|
21623
22723
|
return void 0;
|
|
21624
22724
|
}
|
|
@@ -21663,7 +22763,7 @@ function escapeAttribute6(value) {
|
|
|
21663
22763
|
}
|
|
21664
22764
|
|
|
21665
22765
|
// src/workflows/catalog.ts
|
|
21666
|
-
import { z as
|
|
22766
|
+
import { z as z22 } from "zod";
|
|
21667
22767
|
var builtInWorkflows = [
|
|
21668
22768
|
{
|
|
21669
22769
|
name: "implement",
|
|
@@ -21748,13 +22848,15 @@ function createWorkflowTool(catalog) {
|
|
|
21748
22848
|
name: "workflow_plan",
|
|
21749
22849
|
description: "Load a built-in typed workflow for implementation, debugging, review, or refactoring.",
|
|
21750
22850
|
category: "read",
|
|
22851
|
+
source: "workflow",
|
|
22852
|
+
activation: "always",
|
|
21751
22853
|
inputSchema: jsonSchema({
|
|
21752
22854
|
name: { type: "string", enum: catalog.list().map((workflow) => workflow.name) },
|
|
21753
22855
|
task: { type: "string" }
|
|
21754
22856
|
}, ["name", "task"])
|
|
21755
22857
|
},
|
|
21756
22858
|
async execute(arguments_) {
|
|
21757
|
-
const input2 =
|
|
22859
|
+
const input2 = z22.object({ name: z22.string(), task: z22.string().min(1).max(2e4) }).parse(arguments_);
|
|
21758
22860
|
const workflow = catalog.get(input2.name);
|
|
21759
22861
|
if (!workflow) return { ok: false, content: `Unknown workflow: ${input2.name}` };
|
|
21760
22862
|
return {
|
|
@@ -21797,9 +22899,10 @@ var ExtensionRuntime = class _ExtensionRuntime {
|
|
|
21797
22899
|
workflows = new WorkflowCatalog();
|
|
21798
22900
|
options;
|
|
21799
22901
|
delegation;
|
|
22902
|
+
registry;
|
|
21800
22903
|
initialized = false;
|
|
21801
22904
|
static async create(config, registry, options = {}) {
|
|
21802
|
-
const runtime = new _ExtensionRuntime(config,
|
|
22905
|
+
const runtime = new _ExtensionRuntime(config, resolve26(config.workspaceRoots[0] ?? process.cwd()), options);
|
|
21803
22906
|
try {
|
|
21804
22907
|
await runtime.initialize(registry, options.signal);
|
|
21805
22908
|
return runtime;
|
|
@@ -21820,8 +22923,8 @@ var ExtensionRuntime = class _ExtensionRuntime {
|
|
|
21820
22923
|
}
|
|
21821
22924
|
await this.skills?.discover();
|
|
21822
22925
|
if (this.mcp) {
|
|
21823
|
-
|
|
21824
|
-
|
|
22926
|
+
await this.mcp.initialize(_signal);
|
|
22927
|
+
for (const tool of this.mcp.catalogTools(registry)) registry.register(tool);
|
|
21825
22928
|
}
|
|
21826
22929
|
await this.profiles?.discover();
|
|
21827
22930
|
if (this.config.agents?.enabled && this.profiles && this.options.provider && this.options.contextEngine) {
|
|
@@ -21842,6 +22945,7 @@ var ExtensionRuntime = class _ExtensionRuntime {
|
|
|
21842
22945
|
}
|
|
21843
22946
|
}
|
|
21844
22947
|
registry.register(createWorkflowTool(this.workflows));
|
|
22948
|
+
this.registry = registry;
|
|
21845
22949
|
this.initialized = true;
|
|
21846
22950
|
}
|
|
21847
22951
|
async prepare(input2, session) {
|
|
@@ -21916,6 +23020,25 @@ var ExtensionRuntime = class _ExtensionRuntime {
|
|
|
21916
23020
|
mcpStatus() {
|
|
21917
23021
|
return this.mcp?.list() ?? [];
|
|
21918
23022
|
}
|
|
23023
|
+
mcpSearch(query = "") {
|
|
23024
|
+
return this.mcp?.search(query) ?? [];
|
|
23025
|
+
}
|
|
23026
|
+
mcpInspect(name) {
|
|
23027
|
+
return this.mcp?.inspect(name);
|
|
23028
|
+
}
|
|
23029
|
+
async mcpTrust(name) {
|
|
23030
|
+
return this.mcp?.trust(name);
|
|
23031
|
+
}
|
|
23032
|
+
async mcpDisable(name) {
|
|
23033
|
+
return this.mcp?.disable(name);
|
|
23034
|
+
}
|
|
23035
|
+
async mcpRevoke(name) {
|
|
23036
|
+
return this.mcp?.revoke(name);
|
|
23037
|
+
}
|
|
23038
|
+
async mcpActivate(name, query, signal) {
|
|
23039
|
+
if (!this.mcp || !this.registry) return;
|
|
23040
|
+
return this.mcp.activate(name, query, this.registry, signal);
|
|
23041
|
+
}
|
|
21919
23042
|
cancelAgent(id) {
|
|
21920
23043
|
return this.delegation?.cancelAgent(id) ?? false;
|
|
21921
23044
|
}
|
|
@@ -22004,15 +23127,15 @@ function upgradeCommandOverride(env = process.env) {
|
|
|
22004
23127
|
return trimmed ? trimmed : void 0;
|
|
22005
23128
|
}
|
|
22006
23129
|
function runUpgrade(plan) {
|
|
22007
|
-
return new Promise((
|
|
23130
|
+
return new Promise((resolve28) => {
|
|
22008
23131
|
const child = spawn2(plan.command, plan.args, {
|
|
22009
23132
|
stdio: "inherit",
|
|
22010
23133
|
shell: plan.shell ?? false
|
|
22011
23134
|
});
|
|
22012
|
-
child.on("error", () =>
|
|
23135
|
+
child.on("error", () => resolve28({ ok: false, exitCode: 127, display: plan.display }));
|
|
22013
23136
|
child.on("close", (code) => {
|
|
22014
23137
|
const exitCode = code ?? 1;
|
|
22015
|
-
|
|
23138
|
+
resolve28({ ok: exitCode === 0, exitCode, display: plan.display });
|
|
22016
23139
|
});
|
|
22017
23140
|
});
|
|
22018
23141
|
}
|
|
@@ -22193,7 +23316,7 @@ program.command("migrate").description("Inspect or migrate legacy .mosaic state
|
|
|
22193
23316
|
process.stdout.write(`${recovery.status === "recovered" ? "Recovered" : "Recovery candidates"}: ${recovery.destination}
|
|
22194
23317
|
`);
|
|
22195
23318
|
for (const candidate of recovery.candidates) {
|
|
22196
|
-
process.stdout.write(` ${
|
|
23319
|
+
process.stdout.write(` ${basename14(candidate.path)} ${candidate.kind} ${candidate.action} ${candidate.detail}
|
|
22197
23320
|
`);
|
|
22198
23321
|
}
|
|
22199
23322
|
if (!options.yes && recovery.status === "ready") {
|
|
@@ -22274,7 +23397,7 @@ sessionCommand.command("export <id>").description("Export a session as Markdown"
|
|
|
22274
23397
|
const store = new SessionStore(workspaceOption(options.workspace));
|
|
22275
23398
|
const session = await requireSessionSelector(store, id);
|
|
22276
23399
|
const markdown = sessionMarkdown(session);
|
|
22277
|
-
if (options.output) await writeFile3(
|
|
23400
|
+
if (options.output) await writeFile3(resolve27(options.output), markdown, "utf8");
|
|
22278
23401
|
else process.stdout.write(markdown);
|
|
22279
23402
|
});
|
|
22280
23403
|
var checkpointCommand = program.command("checkpoint").description("Inspect and restore pre-mutation snapshots");
|
|
@@ -22587,7 +23710,7 @@ memoryCommand.command("reject <id>").description("Reject a memory proposal witho
|
|
|
22587
23710
|
}
|
|
22588
23711
|
});
|
|
22589
23712
|
var mcpCommand = program.command("mcp").description("Inspect configured MCP servers");
|
|
22590
|
-
mcpCommand.command("status").description("
|
|
23713
|
+
mcpCommand.command("status").description("Report redacted MCP trust and connection status without connecting optional servers").option("-w, --workspace <path>", "workspace root").option("--config <path>", "explicit config file").option("--json", "print JSON").action(async (options) => {
|
|
22591
23714
|
const config = await runtimeConfig(workspaceOption(options.workspace), runtimeOptions(options));
|
|
22592
23715
|
if (!config.mcp) return printObject([], options.json === true);
|
|
22593
23716
|
const manager = new McpManager(config.mcp, {
|
|
@@ -22595,18 +23718,99 @@ mcpCommand.command("status").description("Connect and report MCP server/tool sta
|
|
|
22595
23718
|
workspaceRoots: config.workspaceRoots
|
|
22596
23719
|
});
|
|
22597
23720
|
try {
|
|
22598
|
-
await manager.
|
|
23721
|
+
await manager.loadTrust();
|
|
22599
23722
|
const status = manager.list();
|
|
22600
23723
|
if (options.json) printObject(status, true);
|
|
22601
23724
|
else if (!status.length) process.stdout.write("No MCP servers configured.\n");
|
|
22602
23725
|
else for (const server of status) {
|
|
22603
|
-
process.stdout.write(`${server.name.padEnd(18)} ${server.state.padEnd(12)} ${server.toolCount} tools${server.error ? ` ${server.error}` : ""}
|
|
23726
|
+
process.stdout.write(`${server.name.padEnd(18)} ${server.state.padEnd(12)} ${server.trust.padEnd(10)} ${server.required ? "required" : "optional"} ${server.toolCount} tools${server.error ? ` ${server.error}` : ""}
|
|
22604
23727
|
`);
|
|
22605
23728
|
}
|
|
22606
23729
|
} finally {
|
|
22607
23730
|
await manager.close();
|
|
22608
23731
|
}
|
|
22609
23732
|
});
|
|
23733
|
+
mcpCommand.command("search [query...]").description("Search configured capability manifests without network access").option("-w, --workspace <path>", "workspace root").option("--config <path>", "explicit config file").option("--json", "print JSON").action(async (query, options) => {
|
|
23734
|
+
const config = await runtimeConfig(workspaceOption(options.workspace), runtimeOptions(options));
|
|
23735
|
+
if (!config.mcp) return printObject([], options.json === true);
|
|
23736
|
+
const manager = createMcpManager(config);
|
|
23737
|
+
await manager.loadTrust();
|
|
23738
|
+
const results = manager.search(query.join(" "));
|
|
23739
|
+
if (options.json) printObject(results, true);
|
|
23740
|
+
else if (!results.length) process.stdout.write("No configured MCP capability matched.\n");
|
|
23741
|
+
else for (const result of results) {
|
|
23742
|
+
process.stdout.write(`${result.name.padEnd(18)} ${result.trust.padEnd(10)} ${result.version} ${result.description}
|
|
23743
|
+
`);
|
|
23744
|
+
}
|
|
23745
|
+
});
|
|
23746
|
+
mcpCommand.command("inspect <server>").description("Review a redacted declarative capability manifest without connecting").option("-w, --workspace <path>", "workspace root").option("--config <path>", "explicit config file").option("--json", "print JSON").action(async (server, options) => {
|
|
23747
|
+
const config = await runtimeConfig(workspaceOption(options.workspace), runtimeOptions(options));
|
|
23748
|
+
if (!config.mcp) throw new Error("MCP is not configured.");
|
|
23749
|
+
const manager = createMcpManager(config);
|
|
23750
|
+
await manager.loadTrust();
|
|
23751
|
+
const review = {
|
|
23752
|
+
manifest: manager.inspect(server),
|
|
23753
|
+
fingerprint: manager.fingerprint(server),
|
|
23754
|
+
trust: manager.status(server)?.trust ?? "untrusted"
|
|
23755
|
+
};
|
|
23756
|
+
if (options.json) printObject(review, true);
|
|
23757
|
+
else printMcpManifest(review);
|
|
23758
|
+
});
|
|
23759
|
+
mcpCommand.command("trust <server>").description("Trust the current redacted manifest fingerprint after review").option("-w, --workspace <path>", "workspace root").option("--config <path>", "explicit config file").option("--json", "print JSON").option("--yes", "confirm trust non-interactively").action(async (server, options) => {
|
|
23760
|
+
const config = await runtimeConfig(workspaceOption(options.workspace), runtimeOptions(options));
|
|
23761
|
+
if (!config.mcp) throw new Error("MCP is not configured.");
|
|
23762
|
+
const manager = createMcpManager(config);
|
|
23763
|
+
await manager.loadTrust();
|
|
23764
|
+
const review = {
|
|
23765
|
+
manifest: manager.inspect(server),
|
|
23766
|
+
fingerprint: manager.fingerprint(server),
|
|
23767
|
+
trust: manager.status(server)?.trust ?? "untrusted"
|
|
23768
|
+
};
|
|
23769
|
+
if (!options.json) printMcpManifest(review);
|
|
23770
|
+
if (!options.yes && !await confirm(`Trust this exact capability manifest for ${server}?`)) return;
|
|
23771
|
+
const status = await manager.trust(server);
|
|
23772
|
+
if (options.json) printObject({ manifest: review.manifest, fingerprint: review.fingerprint, status }, true);
|
|
23773
|
+
else process.stdout.write(`Trusted ${server}. Activation remains explicit.
|
|
23774
|
+
`);
|
|
23775
|
+
});
|
|
23776
|
+
mcpCommand.command("activate <server> <query...>").description("Connect one trusted server and load only query-relevant schemas").option("-w, --workspace <path>", "workspace root").option("--config <path>", "explicit config file").option("--json", "print JSON").action(async (server, query, options) => {
|
|
23777
|
+
const config = await runtimeConfig(workspaceOption(options.workspace), runtimeOptions(options));
|
|
23778
|
+
if (!config.mcp) throw new Error("MCP is not configured.");
|
|
23779
|
+
const manager = createMcpManager(config);
|
|
23780
|
+
const registry = createDefaultToolRegistry();
|
|
23781
|
+
try {
|
|
23782
|
+
await manager.loadTrust();
|
|
23783
|
+
const result = await manager.activate(server, query.join(" "), registry);
|
|
23784
|
+
if (options.json) printObject(result, true);
|
|
23785
|
+
else process.stdout.write(result.ok ? `Activated ${server}; loaded ${result.registeredTools.length}/${result.availableTools} relevant schemas.
|
|
23786
|
+
` : `Could not activate ${server}: ${result.status.error ?? result.status.trust}.
|
|
23787
|
+
`);
|
|
23788
|
+
if (!result.ok) process.exitCode = 1;
|
|
23789
|
+
} finally {
|
|
23790
|
+
await manager.close();
|
|
23791
|
+
}
|
|
23792
|
+
});
|
|
23793
|
+
mcpCommand.command("disable <server>").description("Persistently disable an MCP capability and unload its schemas").option("-w, --workspace <path>", "workspace root").option("--config <path>", "explicit config file").option("--json", "print JSON").action(async (server, options) => {
|
|
23794
|
+
const config = await runtimeConfig(workspaceOption(options.workspace), runtimeOptions(options));
|
|
23795
|
+
if (!config.mcp) throw new Error("MCP is not configured.");
|
|
23796
|
+
const manager = createMcpManager(config);
|
|
23797
|
+
await manager.loadTrust();
|
|
23798
|
+
const status = await manager.disable(server);
|
|
23799
|
+
if (options.json) printObject(status, true);
|
|
23800
|
+
else process.stdout.write(`Disabled ${server}.
|
|
23801
|
+
`);
|
|
23802
|
+
});
|
|
23803
|
+
mcpCommand.command("revoke <server>").description("Revoke persisted MCP trust and require a fresh review").option("-w, --workspace <path>", "workspace root").option("--config <path>", "explicit config file").option("--json", "print JSON").option("--yes", "confirm revocation non-interactively").action(async (server, options) => {
|
|
23804
|
+
const config = await runtimeConfig(workspaceOption(options.workspace), runtimeOptions(options));
|
|
23805
|
+
if (!config.mcp) throw new Error("MCP is not configured.");
|
|
23806
|
+
if (!options.yes && !await confirm(`Revoke persisted trust for ${server}?`)) return;
|
|
23807
|
+
const manager = createMcpManager(config);
|
|
23808
|
+
await manager.loadTrust();
|
|
23809
|
+
const status = await manager.revoke(server);
|
|
23810
|
+
if (options.json) printObject(status, true);
|
|
23811
|
+
else process.stdout.write(`Revoked ${server}; inspect and trust the manifest before reuse.
|
|
23812
|
+
`);
|
|
23813
|
+
});
|
|
22610
23814
|
program.command("rules").description("List user and workspace rules loaded into the agent").option("-w, --workspace <path>", "workspace root").option("--json", "print JSON").action(async (options) => {
|
|
22611
23815
|
const rules = await discoverWorkspaceRules(workspaceOption(options.workspace));
|
|
22612
23816
|
if (options.json) {
|
|
@@ -22636,14 +23840,26 @@ async function runCli() {
|
|
|
22636
23840
|
try {
|
|
22637
23841
|
await program.parseAsync(process.argv);
|
|
22638
23842
|
} catch (error) {
|
|
22639
|
-
const
|
|
22640
|
-
|
|
23843
|
+
const format = structuredOutputFormat(process.argv);
|
|
23844
|
+
if (format) {
|
|
23845
|
+
const reporter = new HeadlessReporter({ format, color: false });
|
|
23846
|
+
process.exitCode = reporter.fail(error).exitCode;
|
|
23847
|
+
} else {
|
|
23848
|
+
const message2 = error instanceof Error ? error.message : String(error);
|
|
23849
|
+
process.stderr.write(`${chalk4.red(cliGlyphs.error)} ${message2}
|
|
22641
23850
|
`);
|
|
22642
|
-
|
|
23851
|
+
process.exitCode = 1;
|
|
23852
|
+
}
|
|
22643
23853
|
} finally {
|
|
22644
23854
|
releaseCliNamespaceLeases(cliNamespaceLeases);
|
|
22645
23855
|
}
|
|
22646
23856
|
}
|
|
23857
|
+
function structuredOutputFormat(argv) {
|
|
23858
|
+
const inline = argv.find((argument) => argument.startsWith("--output-format="))?.slice("--output-format=".length);
|
|
23859
|
+
const index = argv.indexOf("--output-format");
|
|
23860
|
+
const value = inline ?? (index >= 0 ? argv[index + 1] : void 0);
|
|
23861
|
+
return value === "json" || value === "stream-json" ? value : void 0;
|
|
23862
|
+
}
|
|
22647
23863
|
async function runChat(prompts, options) {
|
|
22648
23864
|
const shouldPrint = options.print === true || !process.stdin.isTTY || !process.stdout.isTTY;
|
|
22649
23865
|
if (options.ask && options.plan) throw new Error("--ask and --plan cannot be used together.");
|
|
@@ -22651,7 +23867,7 @@ async function runChat(prompts, options) {
|
|
|
22651
23867
|
const stdinPrompt = !process.stdin.isTTY ? await readStdin() : "";
|
|
22652
23868
|
const firstPrompt = [...prompts, stdinPrompt].filter(Boolean).join("\n\n").trim();
|
|
22653
23869
|
if (shouldPrint && !firstPrompt) throw new Error("Provide a prompt argument or pipe input on stdin.");
|
|
22654
|
-
const workspace =
|
|
23870
|
+
const workspace = resolve27(options.workspace);
|
|
22655
23871
|
let config = await runtimeConfig(workspace, options);
|
|
22656
23872
|
let completedOnboarding = false;
|
|
22657
23873
|
if (!shouldPrint && needsFirstRunOnboarding(config)) {
|
|
@@ -22711,7 +23927,8 @@ async function runChat(prompts, options) {
|
|
|
22711
23927
|
color: (options.color ?? config.ui.color) && !process.env.NO_COLOR
|
|
22712
23928
|
});
|
|
22713
23929
|
const colorOutput = (options.color ?? config.ui.color) && !process.env.NO_COLOR;
|
|
22714
|
-
const requestPermission = options.yes ? async () => true : options.autoEdit ? async (_call, category) => category === "read" || category === "write" ? true : askConsolePermission(_call, category, colorOutput) : async (call, category) => askConsolePermission(call, category, colorOutput);
|
|
23930
|
+
const requestPermission = options.yes ? async () => true : options.autoEdit ? async (_call, category, reason) => category === "read" || category === "write" ? true : askConsolePermission(_call, category, colorOutput, reason) : async (call, category, reason) => askConsolePermission(call, category, colorOutput, reason);
|
|
23931
|
+
let extensionsClosed = false;
|
|
22715
23932
|
try {
|
|
22716
23933
|
let session = await runner.run(firstPrompt, {
|
|
22717
23934
|
askMode: options.ask === true || options.plan === true,
|
|
@@ -22730,13 +23947,15 @@ async function runChat(prompts, options) {
|
|
|
22730
23947
|
requestPermission
|
|
22731
23948
|
});
|
|
22732
23949
|
}
|
|
22733
|
-
|
|
22734
|
-
|
|
23950
|
+
await extensions.close();
|
|
23951
|
+
extensionsClosed = true;
|
|
23952
|
+
const outcome2 = reporter.finish(session);
|
|
23953
|
+
process.exitCode = outcome2.exitCode;
|
|
22735
23954
|
} catch (error) {
|
|
22736
|
-
reporter.fail(error);
|
|
22737
|
-
process.exitCode =
|
|
23955
|
+
const outcome2 = reporter.fail(error, runner.getSession());
|
|
23956
|
+
process.exitCode = outcome2.exitCode;
|
|
22738
23957
|
} finally {
|
|
22739
|
-
await extensions.close();
|
|
23958
|
+
if (!extensionsClosed) await extensions.close().catch(() => void 0);
|
|
22740
23959
|
}
|
|
22741
23960
|
}
|
|
22742
23961
|
async function openMemoryStore(config) {
|
|
@@ -22878,14 +24097,14 @@ async function runAgentSetup(options) {
|
|
|
22878
24097
|
`);
|
|
22879
24098
|
}
|
|
22880
24099
|
async function runtimeConfig(workspaceInput, options) {
|
|
22881
|
-
const workspace =
|
|
24100
|
+
const workspace = resolve27(workspaceInput);
|
|
22882
24101
|
const loaded = await loadConfig(workspace, options.config, {
|
|
22883
24102
|
trustProjectConfig: options.trustProjectConfig === true
|
|
22884
24103
|
});
|
|
22885
24104
|
const roots = [
|
|
22886
24105
|
workspace,
|
|
22887
24106
|
...loaded.workspaceRoots,
|
|
22888
|
-
...(options.addWorkspace ?? []).map((root) =>
|
|
24107
|
+
...(options.addWorkspace ?? []).map((root) => resolve27(workspace, root))
|
|
22889
24108
|
];
|
|
22890
24109
|
const provider = options.provider ? validateProvider(options.provider) : loaded.model.provider;
|
|
22891
24110
|
return {
|
|
@@ -23023,6 +24242,36 @@ function sessionMarkdown(session) {
|
|
|
23023
24242
|
return `${lines.join("\n")}
|
|
23024
24243
|
`;
|
|
23025
24244
|
}
|
|
24245
|
+
function createMcpManager(config) {
|
|
24246
|
+
if (!config.mcp) throw new Error("MCP is not configured.");
|
|
24247
|
+
return new McpManager(config.mcp, {
|
|
24248
|
+
cwd: config.workspaceRoots[0] ?? process.cwd(),
|
|
24249
|
+
workspaceRoots: config.workspaceRoots
|
|
24250
|
+
});
|
|
24251
|
+
}
|
|
24252
|
+
function printMcpManifest(review) {
|
|
24253
|
+
const { manifest } = review;
|
|
24254
|
+
process.stdout.write(`MCP capability ${manifest.name} (${manifest.version})
|
|
24255
|
+
`);
|
|
24256
|
+
process.stdout.write(` Source: ${manifest.source.kind}/${manifest.source.owner}
|
|
24257
|
+
`);
|
|
24258
|
+
process.stdout.write(` Target: ${manifest.transport} ${manifest.target}
|
|
24259
|
+
`);
|
|
24260
|
+
process.stdout.write(` Trust: ${review.trust} ${manifest.required ? "required" : "optional"}
|
|
24261
|
+
`);
|
|
24262
|
+
process.stdout.write(` Fingerprint: ${review.fingerprint}
|
|
24263
|
+
`);
|
|
24264
|
+
if (!manifest.tools.length) {
|
|
24265
|
+
process.stdout.write(" Tools: dynamically discovered; network-only, completion evidence unsupported\n");
|
|
24266
|
+
return;
|
|
24267
|
+
}
|
|
24268
|
+
for (const tool of manifest.tools) {
|
|
24269
|
+
process.stdout.write(` ${tool.name}: ${tool.permissions.join("+")} evidence=${tool.completionEvidence}
|
|
24270
|
+
`);
|
|
24271
|
+
process.stdout.write(` network=${tool.network.join(", ") || "unspecified"} commands=${tool.commands.join(", ") || "none"} paths=${tool.paths.join(", ") || "none"} sensitive=${tool.sensitiveFields.join(", ") || "none"}
|
|
24272
|
+
`);
|
|
24273
|
+
}
|
|
24274
|
+
}
|
|
23026
24275
|
async function confirm(prompt) {
|
|
23027
24276
|
if (!process.stdin.isTTY || !process.stdout.isTTY) return false;
|
|
23028
24277
|
const readline = createInterface2({ input, output });
|
|
@@ -23042,7 +24291,7 @@ function collect(value, previous) {
|
|
|
23042
24291
|
}
|
|
23043
24292
|
function workspaceOption(value) {
|
|
23044
24293
|
const rootOptions = program.opts();
|
|
23045
|
-
return
|
|
24294
|
+
return resolve27(value ?? rootOptions.workspace ?? process.cwd());
|
|
23046
24295
|
}
|
|
23047
24296
|
function runtimeOptions(options) {
|
|
23048
24297
|
const root = program.opts();
|