clixad 0.0.1-beta.47 → 0.0.1-beta.48
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
CHANGED
|
@@ -63,7 +63,11 @@ before it changes anything. `shift+tab` cycles through the permission modes. Bey
|
|
|
63
63
|
- **Keeps a task list** on multi-step work, and **asks you** when guessing wrong would waste real
|
|
64
64
|
work.
|
|
65
65
|
- **Uses MCP servers** you configure in `.mcp.json` or `~/.clixad/mcp.json`; `/mcp` shows their
|
|
66
|
-
status. Every tool a server offers is put to you before it runs.
|
|
66
|
+
status. Every tool a server offers is put to you before it runs. A server from a project's
|
|
67
|
+
`.mcp.json` or `.clixad/mcp.json` starts only after you approve it (asked once per project, and
|
|
68
|
+
again if its entry changes; `/mcp approve` starts one later). A server gets `PATH`, `HOME` and the
|
|
69
|
+
locale from your environment and nothing else, so pass what it needs in its `env`, for example
|
|
70
|
+
`"env": { "GITHUB_TOKEN": "${GITHUB_TOKEN}" }`.
|
|
67
71
|
|
|
68
72
|
Type `/help` for the commands, `@` to attach a file, and `!` to run a shell command yourself.
|
|
69
73
|
|
|
@@ -95,8 +99,8 @@ github.com/clixad/cli is an automatic one-way copy of the CLI's part of it, so a
|
|
|
95
99
|
nowhere to be merged and is closed automatically. If you have a fix in mind, describe it in an
|
|
96
100
|
issue.
|
|
97
101
|
|
|
98
|
-
Please report security problems
|
|
99
|
-
|
|
102
|
+
Please report security problems by email to [info@clixad.io](mailto:info@clixad.io), not in a
|
|
103
|
+
public GitHub issue. See [SECURITY.md](https://github.com/clixad/cli/blob/main/SECURITY.md).
|
|
100
104
|
|
|
101
105
|
## Status
|
|
102
106
|
|
|
@@ -643,16 +643,16 @@ async function httpFailure(res, what) {
|
|
|
643
643
|
return detail ? `${what} failed: ${res.status} \u2014 ${detail}` : `${what} failed: ${res.status}`;
|
|
644
644
|
}
|
|
645
645
|
function sleep(ms, signal) {
|
|
646
|
-
return new Promise((
|
|
646
|
+
return new Promise((resolve6) => {
|
|
647
647
|
if (!signal) {
|
|
648
|
-
setTimeout(
|
|
648
|
+
setTimeout(resolve6, ms);
|
|
649
649
|
return;
|
|
650
650
|
}
|
|
651
|
-
if (signal.aborted) return
|
|
651
|
+
if (signal.aborted) return resolve6();
|
|
652
652
|
const done = () => {
|
|
653
653
|
clearTimeout(timer);
|
|
654
654
|
signal.removeEventListener("abort", done);
|
|
655
|
-
|
|
655
|
+
resolve6();
|
|
656
656
|
};
|
|
657
657
|
const timer = setTimeout(done, ms);
|
|
658
658
|
signal.addEventListener("abort", done, { once: true });
|
|
@@ -2032,16 +2032,16 @@ async function devLogin(client, config, email, invite) {
|
|
|
2032
2032
|
};
|
|
2033
2033
|
}
|
|
2034
2034
|
function sleep2(ms, signal) {
|
|
2035
|
-
return new Promise((
|
|
2035
|
+
return new Promise((resolve6) => {
|
|
2036
2036
|
if (!signal) {
|
|
2037
|
-
setTimeout(
|
|
2037
|
+
setTimeout(resolve6, ms);
|
|
2038
2038
|
return;
|
|
2039
2039
|
}
|
|
2040
|
-
if (signal.aborted) return
|
|
2040
|
+
if (signal.aborted) return resolve6();
|
|
2041
2041
|
const done = () => {
|
|
2042
2042
|
clearTimeout(timer);
|
|
2043
2043
|
signal.removeEventListener("abort", done);
|
|
2044
|
-
|
|
2044
|
+
resolve6();
|
|
2045
2045
|
};
|
|
2046
2046
|
const timer = setTimeout(done, ms);
|
|
2047
2047
|
signal.addEventListener("abort", done, { once: true });
|
|
@@ -2336,8 +2336,18 @@ ${renderTodos(items)}${warning}`;
|
|
|
2336
2336
|
|
|
2337
2337
|
// src/tools.ts
|
|
2338
2338
|
import { spawn as spawn4 } from "node:child_process";
|
|
2339
|
-
import {
|
|
2340
|
-
|
|
2339
|
+
import {
|
|
2340
|
+
existsSync as existsSync2,
|
|
2341
|
+
lstatSync,
|
|
2342
|
+
mkdirSync as mkdirSync3,
|
|
2343
|
+
readdirSync,
|
|
2344
|
+
readFileSync as readFileSync3,
|
|
2345
|
+
readlinkSync,
|
|
2346
|
+
realpathSync as realpathSync2,
|
|
2347
|
+
statSync,
|
|
2348
|
+
writeFileSync as writeFileSync3
|
|
2349
|
+
} from "node:fs";
|
|
2350
|
+
import { basename, isAbsolute, relative, resolve as resolve2, dirname as dirname3, join as join3, sep } from "node:path";
|
|
2341
2351
|
|
|
2342
2352
|
// src/diff.ts
|
|
2343
2353
|
var R = "\x1B[0m";
|
|
@@ -3026,9 +3036,10 @@ function decodeEntities(text) {
|
|
|
3026
3036
|
|
|
3027
3037
|
// src/mcp.ts
|
|
3028
3038
|
import { spawn as spawn3 } from "node:child_process";
|
|
3029
|
-
import {
|
|
3039
|
+
import { createHash } from "node:crypto";
|
|
3040
|
+
import { existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync2, realpathSync, renameSync as renameSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
3030
3041
|
import { homedir as homedir2 } from "node:os";
|
|
3031
|
-
import { join as join2 } from "node:path";
|
|
3042
|
+
import { dirname as dirname2, join as join2, resolve } from "node:path";
|
|
3032
3043
|
var CONNECT_TIMEOUT_MS = 2e4;
|
|
3033
3044
|
var DEFAULT_CALL_TIMEOUT_MS = 12e4;
|
|
3034
3045
|
var PROTOCOL_VERSION = "2025-06-18";
|
|
@@ -3039,14 +3050,20 @@ var McpError = class extends Error {
|
|
|
3039
3050
|
function isHttpConfig(c) {
|
|
3040
3051
|
return c.url !== void 0;
|
|
3041
3052
|
}
|
|
3042
|
-
function
|
|
3043
|
-
|
|
3053
|
+
function mcpConfigFiles(root) {
|
|
3054
|
+
const personal = join2(homedir2(), ".clixad", "mcp.json");
|
|
3055
|
+
const files = [{ path: personal, scope: "user" }];
|
|
3056
|
+
for (const path of [join2(root, ".mcp.json"), join2(root, ".clixad", "mcp.json")]) {
|
|
3057
|
+
if (resolve(path) !== resolve(personal)) files.push({ path, scope: "project" });
|
|
3058
|
+
}
|
|
3059
|
+
return files;
|
|
3044
3060
|
}
|
|
3045
3061
|
function loadMcpConfig(root) {
|
|
3046
3062
|
const servers = {};
|
|
3063
|
+
const origins = {};
|
|
3047
3064
|
const sources = [];
|
|
3048
3065
|
const errors = [];
|
|
3049
|
-
for (const path of
|
|
3066
|
+
for (const { path, scope } of mcpConfigFiles(root)) {
|
|
3050
3067
|
if (!existsSync(path)) continue;
|
|
3051
3068
|
let parsed;
|
|
3052
3069
|
try {
|
|
@@ -3067,13 +3084,24 @@ function loadMcpConfig(root) {
|
|
|
3067
3084
|
sources.push(short(path));
|
|
3068
3085
|
for (const [name, raw] of Object.entries(declared)) {
|
|
3069
3086
|
const config = validateServer(name, raw);
|
|
3070
|
-
if (typeof config === "string")
|
|
3071
|
-
|
|
3087
|
+
if (typeof config === "string") {
|
|
3088
|
+
errors.push(`${short(path)}: ${config}`);
|
|
3089
|
+
continue;
|
|
3090
|
+
}
|
|
3091
|
+
servers[name] = config;
|
|
3092
|
+
origins[name] = {
|
|
3093
|
+
scope,
|
|
3094
|
+
file: short(path),
|
|
3095
|
+
fingerprint: fingerprint(raw),
|
|
3096
|
+
display: describeEntry(raw),
|
|
3097
|
+
variables: [...new Set(variablesIn(raw))].sort()
|
|
3098
|
+
};
|
|
3072
3099
|
}
|
|
3073
3100
|
}
|
|
3074
|
-
return { servers, sources, errors };
|
|
3101
|
+
return { servers, origins, sources, errors };
|
|
3075
3102
|
}
|
|
3076
3103
|
function validateServer(name, raw) {
|
|
3104
|
+
if (/[\u0000-\u001f\u007f-\u009f]/.test(name)) return `server ${JSON.stringify(name)} has control characters in its name`;
|
|
3077
3105
|
if (typeof raw !== "object" || raw === null) return `server "${name}" is not an object`;
|
|
3078
3106
|
const c = raw;
|
|
3079
3107
|
if (typeof c.url === "string") {
|
|
@@ -3114,6 +3142,109 @@ function expandMap(map) {
|
|
|
3114
3142
|
function short(path) {
|
|
3115
3143
|
return path.replace(homedir2(), "~");
|
|
3116
3144
|
}
|
|
3145
|
+
function canonicalJson(value) {
|
|
3146
|
+
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
|
|
3147
|
+
if (value && typeof value === "object") {
|
|
3148
|
+
const entries = Object.keys(value).sort().map((k) => `${JSON.stringify(k)}:${canonicalJson(value[k])}`);
|
|
3149
|
+
return `{${entries.join(",")}}`;
|
|
3150
|
+
}
|
|
3151
|
+
return JSON.stringify(value) ?? "null";
|
|
3152
|
+
}
|
|
3153
|
+
function fingerprint(raw) {
|
|
3154
|
+
return createHash("sha256").update(canonicalJson(raw)).digest("hex");
|
|
3155
|
+
}
|
|
3156
|
+
function variablesIn(value) {
|
|
3157
|
+
if (typeof value === "string") return [...value.matchAll(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g)].map((m) => m[1]);
|
|
3158
|
+
if (Array.isArray(value)) return value.flatMap(variablesIn);
|
|
3159
|
+
if (value && typeof value === "object") return Object.values(value).flatMap(variablesIn);
|
|
3160
|
+
return [];
|
|
3161
|
+
}
|
|
3162
|
+
var MAX_DISPLAY_CHARS = 2e3;
|
|
3163
|
+
function shellWord(word) {
|
|
3164
|
+
if (/^[\x21\x23-\x5b\x5d-\x7e]+$/.test(word)) return word;
|
|
3165
|
+
return JSON.stringify(word).replace(/[^\x20-\x7e]/g, (ch) => `\\u${ch.charCodeAt(0).toString(16).padStart(4, "0")}`);
|
|
3166
|
+
}
|
|
3167
|
+
function describeEntry(raw) {
|
|
3168
|
+
const strings = (v) => Array.isArray(v) ? v.filter((x) => typeof x === "string") : [];
|
|
3169
|
+
const pairs = (v) => isStringMap(v) ? Object.entries(v) : [];
|
|
3170
|
+
const lines = [];
|
|
3171
|
+
if (typeof raw.url === "string") {
|
|
3172
|
+
lines.push(`${raw.type === "sse" ? "sse" : "http"} ${shellWord(raw.url)}`);
|
|
3173
|
+
for (const [k, v] of pairs(raw.headers)) lines.push(`header ${shellWord(k)}: ${shellWord(v)}`);
|
|
3174
|
+
} else {
|
|
3175
|
+
lines.push([String(raw.command ?? ""), ...strings(raw.args)].map(shellWord).join(" "));
|
|
3176
|
+
for (const [k, v] of pairs(raw.env)) lines.push(`env ${shellWord(k)}=${shellWord(v)}`);
|
|
3177
|
+
if (typeof raw.cwd === "string") lines.push(`cwd ${shellWord(raw.cwd)}`);
|
|
3178
|
+
}
|
|
3179
|
+
const text = lines.join("\n");
|
|
3180
|
+
return text.length > MAX_DISPLAY_CHARS ? `${text.slice(0, MAX_DISPLAY_CHARS)}\u2026 (${text.length - MAX_DISPLAY_CHARS} more characters; read the file before approving)` : text;
|
|
3181
|
+
}
|
|
3182
|
+
var INHERITED_ENV = {
|
|
3183
|
+
posix: ["HOME", "LOGNAME", "PATH", "SHELL", "TERM", "USER", "LANG", "LC_ALL", "LC_CTYPE", "TMPDIR", "TZ"],
|
|
3184
|
+
win32: [
|
|
3185
|
+
"APPDATA",
|
|
3186
|
+
"COMSPEC",
|
|
3187
|
+
"HOMEDRIVE",
|
|
3188
|
+
"HOMEPATH",
|
|
3189
|
+
"LOCALAPPDATA",
|
|
3190
|
+
"PATH",
|
|
3191
|
+
"PATHEXT",
|
|
3192
|
+
"PROCESSOR_ARCHITECTURE",
|
|
3193
|
+
"PROGRAMFILES",
|
|
3194
|
+
"SYSTEMDRIVE",
|
|
3195
|
+
"SYSTEMROOT",
|
|
3196
|
+
"TEMP",
|
|
3197
|
+
"TMP",
|
|
3198
|
+
"USERNAME",
|
|
3199
|
+
"USERPROFILE",
|
|
3200
|
+
"WINDIR"
|
|
3201
|
+
]
|
|
3202
|
+
};
|
|
3203
|
+
function mcpServerEnv(own = {}, source = process.env, platform2 = process.platform) {
|
|
3204
|
+
const env = {};
|
|
3205
|
+
const wanted = INHERITED_ENV[platform2 === "win32" ? "win32" : "posix"];
|
|
3206
|
+
const byName = new Map(Object.keys(source).map((k) => [platform2 === "win32" ? k.toUpperCase() : k, k]));
|
|
3207
|
+
for (const name of wanted) {
|
|
3208
|
+
const key = byName.get(name);
|
|
3209
|
+
const value = key === void 0 ? void 0 : source[key];
|
|
3210
|
+
if (value !== void 0 && !value.startsWith("()")) env[name] = value;
|
|
3211
|
+
}
|
|
3212
|
+
return { ...env, ...own };
|
|
3213
|
+
}
|
|
3214
|
+
function mcpApprovalsPath() {
|
|
3215
|
+
return join2(homedir2(), ".clixad", "mcp-approvals.json");
|
|
3216
|
+
}
|
|
3217
|
+
function workspaceKey(root) {
|
|
3218
|
+
try {
|
|
3219
|
+
return realpathSync.native(root);
|
|
3220
|
+
} catch {
|
|
3221
|
+
return resolve(root);
|
|
3222
|
+
}
|
|
3223
|
+
}
|
|
3224
|
+
function readApprovals() {
|
|
3225
|
+
try {
|
|
3226
|
+
const parsed = JSON.parse(readFileSync2(mcpApprovalsPath(), "utf8"));
|
|
3227
|
+
if (parsed?.version === 1 && parsed.projects && typeof parsed.projects === "object") return parsed;
|
|
3228
|
+
} catch {
|
|
3229
|
+
}
|
|
3230
|
+
return { version: 1, projects: {} };
|
|
3231
|
+
}
|
|
3232
|
+
function mcpDecision(root, name, fingerprint2) {
|
|
3233
|
+
const entry = readApprovals().projects[workspaceKey(root)]?.[name];
|
|
3234
|
+
return entry && entry.fingerprint === fingerprint2 ? entry.decision : void 0;
|
|
3235
|
+
}
|
|
3236
|
+
function recordMcpDecision(root, servers, decision, at = /* @__PURE__ */ new Date()) {
|
|
3237
|
+
const file = readApprovals();
|
|
3238
|
+
const key = workspaceKey(root);
|
|
3239
|
+
const project = file.projects[key] ??= {};
|
|
3240
|
+
for (const server of servers) project[server.name] = { fingerprint: server.fingerprint, decision, at: at.toISOString() };
|
|
3241
|
+
const path = mcpApprovalsPath();
|
|
3242
|
+
mkdirSync2(dirname2(path), { recursive: true });
|
|
3243
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
3244
|
+
writeFileSync2(tmp, `${JSON.stringify(file, null, 2)}
|
|
3245
|
+
`, { encoding: "utf8", mode: 384 });
|
|
3246
|
+
renameSync2(tmp, path);
|
|
3247
|
+
}
|
|
3117
3248
|
var StdioTransport = class {
|
|
3118
3249
|
child;
|
|
3119
3250
|
buffer = "";
|
|
@@ -3124,10 +3255,13 @@ var StdioTransport = class {
|
|
|
3124
3255
|
constructor(config, root) {
|
|
3125
3256
|
this.child = spawn3(config.command, config.args ?? [], {
|
|
3126
3257
|
cwd: config.cwd ?? root,
|
|
3127
|
-
//
|
|
3128
|
-
//
|
|
3129
|
-
//
|
|
3130
|
-
|
|
3258
|
+
// What a program needs to run plus what the entry names, and never the
|
|
3259
|
+
// rest of ours: this process's environment holds whatever the user
|
|
3260
|
+
// exported, API keys included, and a server is somebody else's program.
|
|
3261
|
+
// `shell: false` on purpose: the command and its args are already
|
|
3262
|
+
// separate, so there is nothing for a shell to re-split, and a path with
|
|
3263
|
+
// a space in it would be split if there were.
|
|
3264
|
+
env: mcpServerEnv(config.env),
|
|
3131
3265
|
stdio: ["pipe", "pipe", "pipe"],
|
|
3132
3266
|
windowsHide: true,
|
|
3133
3267
|
// npx and friends are .cmd shims on Windows, which CreateProcess cannot
|
|
@@ -3177,7 +3311,7 @@ var StdioTransport = class {
|
|
|
3177
3311
|
if (this.exited) return Promise.reject(new McpError(this.exited));
|
|
3178
3312
|
const id = ++this.seq;
|
|
3179
3313
|
const payload = JSON.stringify({ jsonrpc: "2.0", id, method, params }) + "\n";
|
|
3180
|
-
return new Promise((
|
|
3314
|
+
return new Promise((resolve6, reject) => {
|
|
3181
3315
|
const settle = (fn) => {
|
|
3182
3316
|
clearTimeout(timer);
|
|
3183
3317
|
signal?.removeEventListener("abort", onAbort);
|
|
@@ -3191,7 +3325,7 @@ var StdioTransport = class {
|
|
|
3191
3325
|
const onAbort = () => settle(() => reject(new McpError("cancelled")));
|
|
3192
3326
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
3193
3327
|
this.pending.set(id, {
|
|
3194
|
-
resolve: (v) => settle(() =>
|
|
3328
|
+
resolve: (v) => settle(() => resolve6(v)),
|
|
3195
3329
|
reject: (e) => settle(() => reject(e))
|
|
3196
3330
|
});
|
|
3197
3331
|
this.child.stdin?.write(payload, (err) => {
|
|
@@ -3315,23 +3449,42 @@ async function readSseResponse(res, id) {
|
|
|
3315
3449
|
return null;
|
|
3316
3450
|
}
|
|
3317
3451
|
var McpServerConnection = class {
|
|
3318
|
-
constructor(name, config, root) {
|
|
3452
|
+
constructor(name, config, root, origin, held) {
|
|
3319
3453
|
this.name = name;
|
|
3320
3454
|
this.config = config;
|
|
3321
|
-
this.
|
|
3455
|
+
this.root = root;
|
|
3456
|
+
this.origin = origin;
|
|
3457
|
+
this.state = held ?? "connecting";
|
|
3322
3458
|
}
|
|
3323
3459
|
name;
|
|
3324
3460
|
config;
|
|
3325
|
-
|
|
3461
|
+
root;
|
|
3462
|
+
origin;
|
|
3463
|
+
state;
|
|
3326
3464
|
error;
|
|
3327
3465
|
tools = [];
|
|
3466
|
+
/**
|
|
3467
|
+
* Created by `connect()` and nowhere else, because creating a stdio
|
|
3468
|
+
* transport *is* starting the program. A held connection never has one.
|
|
3469
|
+
*/
|
|
3328
3470
|
transport;
|
|
3329
3471
|
get kind() {
|
|
3330
3472
|
return isHttpConfig(this.config) ? "http" : "stdio";
|
|
3331
3473
|
}
|
|
3474
|
+
/** Lift the hold, after the user approved. Starts nothing by itself. */
|
|
3475
|
+
release() {
|
|
3476
|
+
if (this.state === "unapproved" || this.state === "declined") this.state = "connecting";
|
|
3477
|
+
}
|
|
3478
|
+
/** Remember a refusal, so `/mcp` can say why it is not running. */
|
|
3479
|
+
decline() {
|
|
3480
|
+
if (this.state === "unapproved") this.state = "declined";
|
|
3481
|
+
}
|
|
3332
3482
|
async connect(timeoutMs = CONNECT_TIMEOUT_MS) {
|
|
3483
|
+
if (this.state !== "connecting" || this.transport) return;
|
|
3484
|
+
const transport = isHttpConfig(this.config) ? new HttpTransport(this.config) : new StdioTransport(this.config, this.root);
|
|
3485
|
+
this.transport = transport;
|
|
3333
3486
|
try {
|
|
3334
|
-
await
|
|
3487
|
+
await transport.request(
|
|
3335
3488
|
"initialize",
|
|
3336
3489
|
{
|
|
3337
3490
|
protocolVersion: PROTOCOL_VERSION,
|
|
@@ -3340,8 +3493,8 @@ var McpServerConnection = class {
|
|
|
3340
3493
|
},
|
|
3341
3494
|
timeoutMs
|
|
3342
3495
|
);
|
|
3343
|
-
await
|
|
3344
|
-
const listed = await
|
|
3496
|
+
await transport.notify("notifications/initialized", {});
|
|
3497
|
+
const listed = await transport.request("tools/list", {}, timeoutMs);
|
|
3345
3498
|
this.tools = (listed?.tools ?? []).filter(
|
|
3346
3499
|
(t) => typeof t?.name === "string" && t.name.length > 0
|
|
3347
3500
|
).map((t) => ({
|
|
@@ -3352,17 +3505,19 @@ var McpServerConnection = class {
|
|
|
3352
3505
|
...typeof t.description === "string" ? { description: t.description } : {},
|
|
3353
3506
|
...t.inputSchema && typeof t.inputSchema === "object" ? { inputSchema: t.inputSchema } : {}
|
|
3354
3507
|
}));
|
|
3508
|
+
if (this.state !== "connecting") return;
|
|
3355
3509
|
this.state = "ready";
|
|
3356
3510
|
} catch (err) {
|
|
3511
|
+
if (this.state !== "connecting") return;
|
|
3357
3512
|
this.state = "failed";
|
|
3358
|
-
const diagnostics =
|
|
3513
|
+
const diagnostics = transport.diagnostics();
|
|
3359
3514
|
this.error = err.message + (diagnostics ? `
|
|
3360
3515
|
${diagnostics.split("\n").slice(-3).join("\n ")}` : "");
|
|
3361
|
-
await
|
|
3516
|
+
await transport.close().catch(() => void 0);
|
|
3362
3517
|
}
|
|
3363
3518
|
}
|
|
3364
3519
|
async call(tool, args, timeoutMs, signal) {
|
|
3365
|
-
if (this.state !== "ready") throw new McpError(`server "${this.name}" is ${this.state}`);
|
|
3520
|
+
if (this.state !== "ready" || !this.transport) throw new McpError(`server "${this.name}" is ${this.state}`);
|
|
3366
3521
|
const raw = await this.transport.request(
|
|
3367
3522
|
"tools/call",
|
|
3368
3523
|
{ name: tool, arguments: args ?? {} },
|
|
@@ -3375,7 +3530,7 @@ var McpServerConnection = class {
|
|
|
3375
3530
|
}
|
|
3376
3531
|
async close() {
|
|
3377
3532
|
this.state = "closed";
|
|
3378
|
-
await this.transport
|
|
3533
|
+
await this.transport?.close().catch(() => void 0);
|
|
3379
3534
|
}
|
|
3380
3535
|
};
|
|
3381
3536
|
function renderContent(content, structured) {
|
|
@@ -3418,34 +3573,118 @@ function isMcpToolName(name) {
|
|
|
3418
3573
|
return name.startsWith("mcp__");
|
|
3419
3574
|
}
|
|
3420
3575
|
var McpHub = class _McpHub {
|
|
3421
|
-
constructor(servers) {
|
|
3576
|
+
constructor(servers, root) {
|
|
3422
3577
|
this.servers = servers;
|
|
3578
|
+
this.root = root;
|
|
3423
3579
|
}
|
|
3424
3580
|
servers;
|
|
3581
|
+
root;
|
|
3425
3582
|
/**
|
|
3426
|
-
* Connect to every
|
|
3583
|
+
* Connect to every server given, in parallel, and never reject.
|
|
3427
3584
|
*
|
|
3585
|
+
* For servers the caller has already decided may run; a config read from
|
|
3586
|
+
* disk goes through `fromConfig`, which holds the repository's servers.
|
|
3428
3587
|
* Parallel because a slow server must not delay the others, and one that
|
|
3429
3588
|
* cannot start must not delay anything: the whole point is that the REPL opens
|
|
3430
3589
|
* whether or not somebody's Docker daemon is running today.
|
|
3431
3590
|
*/
|
|
3432
3591
|
static async connect(servers, opts = { root: process.cwd() }) {
|
|
3433
3592
|
const connections = Object.entries(servers).map(([name, config]) => new McpServerConnection(name, config, opts.root));
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3593
|
+
const hub = new _McpHub(connections, opts.root);
|
|
3594
|
+
await hub.startReleased(opts.timeoutMs);
|
|
3595
|
+
return hub;
|
|
3596
|
+
}
|
|
3597
|
+
/**
|
|
3598
|
+
* A hub for the config on disk, with nothing started yet.
|
|
3599
|
+
*
|
|
3600
|
+
* A server from `~/.clixad/mcp.json` is ready to start. A server from the
|
|
3601
|
+
* workspace is held unless the user approved this exact entry for this
|
|
3602
|
+
* workspace before, and stays held if they declined it. `startReleased`
|
|
3603
|
+
* then starts what may start; `approve` releases the rest.
|
|
3604
|
+
*/
|
|
3605
|
+
static fromConfig(loaded, opts) {
|
|
3606
|
+
const connections = Object.entries(loaded.servers).map(([name, config]) => {
|
|
3607
|
+
const origin = loaded.origins[name];
|
|
3608
|
+
let held;
|
|
3609
|
+
if (!origin || origin.scope !== "user") {
|
|
3610
|
+
const decision = origin ? mcpDecision(opts.root, name, origin.fingerprint) : void 0;
|
|
3611
|
+
held = decision === "approved" ? void 0 : decision === "declined" ? "declined" : "unapproved";
|
|
3612
|
+
}
|
|
3613
|
+
return new McpServerConnection(name, config, opts.root, origin, held);
|
|
3614
|
+
});
|
|
3615
|
+
return new _McpHub(connections, opts.root);
|
|
3616
|
+
}
|
|
3617
|
+
/** An empty hub — what a run with no config, or a scripted run, gets. */
|
|
3618
|
+
static empty() {
|
|
3619
|
+
return new _McpHub([], process.cwd());
|
|
3620
|
+
}
|
|
3621
|
+
/**
|
|
3622
|
+
* Start every server that may start and has not been, in parallel. Returns
|
|
3623
|
+
* the ones it started, so the caller can say how each one went.
|
|
3624
|
+
*/
|
|
3625
|
+
async startReleased(timeoutMs) {
|
|
3626
|
+
const starting = this.servers.filter((s) => s.state === "connecting");
|
|
3627
|
+
await Promise.all(starting.map((c) => c.connect(timeoutMs)));
|
|
3628
|
+
this.assignNames();
|
|
3629
|
+
const names = new Set(starting.map((s) => s.name));
|
|
3630
|
+
return this.status().filter((s) => names.has(s.name));
|
|
3631
|
+
}
|
|
3632
|
+
/**
|
|
3633
|
+
* Give every listed tool its model-facing name. Names already given keep
|
|
3634
|
+
* theirs, so a server approved later cannot rename a tool the model has
|
|
3635
|
+
* already been told about. A collision (two servers with the same tool, or
|
|
3636
|
+
* two long names that truncate together) is broken deterministically.
|
|
3637
|
+
*/
|
|
3638
|
+
assignNames() {
|
|
3639
|
+
const taken = new Set(this.servers.flatMap((s) => s.tools.map((t) => t.qualified)).filter(Boolean));
|
|
3640
|
+
for (const conn of this.servers) {
|
|
3437
3641
|
for (const tool of conn.tools) {
|
|
3642
|
+
if (tool.qualified) continue;
|
|
3438
3643
|
let name = qualifiedToolName(conn.name, tool.name);
|
|
3439
3644
|
for (let i = 2; taken.has(name); i++) name = `${name.slice(0, 61)}_${i}`;
|
|
3440
3645
|
taken.add(name);
|
|
3441
3646
|
tool.qualified = name;
|
|
3442
3647
|
}
|
|
3443
3648
|
}
|
|
3444
|
-
return new _McpHub(connections);
|
|
3445
3649
|
}
|
|
3446
|
-
/**
|
|
3447
|
-
|
|
3448
|
-
return
|
|
3650
|
+
/** The servers waiting for the user, with what the prompt shows. */
|
|
3651
|
+
pending(opts = {}) {
|
|
3652
|
+
return this.servers.filter((s) => s.state === "unapproved" || opts.includeDeclined && s.state === "declined").map((s) => ({
|
|
3653
|
+
name: s.name,
|
|
3654
|
+
file: s.origin?.file ?? "",
|
|
3655
|
+
display: s.origin?.display ?? "",
|
|
3656
|
+
variables: s.origin?.variables ?? [],
|
|
3657
|
+
fingerprint: s.origin?.fingerprint ?? ""
|
|
3658
|
+
}));
|
|
3659
|
+
}
|
|
3660
|
+
/**
|
|
3661
|
+
* The user approved these held servers: remember it (bound to each entry as
|
|
3662
|
+
* written) when asked to, and start them. Names that are not held are
|
|
3663
|
+
* ignored. Returns how each one started.
|
|
3664
|
+
*/
|
|
3665
|
+
async approve(names, opts) {
|
|
3666
|
+
const chosen = this.held(names);
|
|
3667
|
+
if (opts.remember) this.remember(chosen, "approved");
|
|
3668
|
+
for (const s of chosen) s.release();
|
|
3669
|
+
return this.startReleased(opts.timeoutMs);
|
|
3670
|
+
}
|
|
3671
|
+
/** The user declined these held servers: remember it, so they are not asked about again until the entry changes. */
|
|
3672
|
+
decline(names) {
|
|
3673
|
+
const chosen = this.held(names).filter((s) => s.state === "unapproved");
|
|
3674
|
+
this.remember(chosen, "declined");
|
|
3675
|
+
for (const s of chosen) s.decline();
|
|
3676
|
+
}
|
|
3677
|
+
held(names) {
|
|
3678
|
+
const wanted = new Set(names);
|
|
3679
|
+
return this.servers.filter((s) => wanted.has(s.name) && (s.state === "unapproved" || s.state === "declined"));
|
|
3680
|
+
}
|
|
3681
|
+
remember(servers, decision) {
|
|
3682
|
+
const entries = servers.filter((s) => s.origin).map((s) => ({ name: s.name, fingerprint: s.origin.fingerprint }));
|
|
3683
|
+
if (!entries.length) return;
|
|
3684
|
+
try {
|
|
3685
|
+
recordMcpDecision(this.root, entries, decision);
|
|
3686
|
+
} catch {
|
|
3687
|
+
}
|
|
3449
3688
|
}
|
|
3450
3689
|
tools() {
|
|
3451
3690
|
return this.servers.filter((s) => s.state === "ready").flatMap((s) => s.tools);
|
|
@@ -3509,13 +3748,60 @@ var PermissionDeniedError = class extends ToolError {
|
|
|
3509
3748
|
}
|
|
3510
3749
|
};
|
|
3511
3750
|
function safeResolve(root, p) {
|
|
3512
|
-
const abs = isAbsolute(p) ? p :
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3751
|
+
const abs = isAbsolute(p) ? p : resolve2(root, p);
|
|
3752
|
+
if (escapes(root, abs)) throw new ToolError(`path escapes workspace: ${p}`);
|
|
3753
|
+
let real;
|
|
3754
|
+
let realRoot;
|
|
3755
|
+
try {
|
|
3756
|
+
real = realLocation(abs);
|
|
3757
|
+
realRoot = realLocation(resolve2(root));
|
|
3758
|
+
} catch (err) {
|
|
3759
|
+
const code = err.code;
|
|
3760
|
+
throw new ToolError(code === "ELOOP" ? `${p}: the path goes through a symlink loop` : `cannot resolve ${p}`);
|
|
3761
|
+
}
|
|
3762
|
+
if (escapes(realRoot, real)) {
|
|
3763
|
+
throw new ToolError(`path escapes workspace: ${p} is a symlink (or inside one) that points outside the workspace`);
|
|
3516
3764
|
}
|
|
3517
3765
|
return abs;
|
|
3518
3766
|
}
|
|
3767
|
+
function escapes(root, abs) {
|
|
3768
|
+
const rel2 = relative(root, abs);
|
|
3769
|
+
return rel2 === ".." || rel2.startsWith(".." + sep) || isAbsolute(rel2);
|
|
3770
|
+
}
|
|
3771
|
+
var MAX_LINK_HOPS = 40;
|
|
3772
|
+
function realLocation(abs, hops = 0) {
|
|
3773
|
+
try {
|
|
3774
|
+
return realpathSync2.native(abs);
|
|
3775
|
+
} catch (err) {
|
|
3776
|
+
const code = err.code;
|
|
3777
|
+
if (code !== "ENOENT" && code !== "ENOTDIR") throw err;
|
|
3778
|
+
}
|
|
3779
|
+
const parent = dirname3(abs);
|
|
3780
|
+
if (parent === abs) return abs;
|
|
3781
|
+
let link;
|
|
3782
|
+
try {
|
|
3783
|
+
if (lstatSync(abs).isSymbolicLink()) link = readlinkSync(abs);
|
|
3784
|
+
} catch {
|
|
3785
|
+
}
|
|
3786
|
+
const realParent = realLocation(parent, hops);
|
|
3787
|
+
if (link === void 0) return join3(realParent, basename(abs));
|
|
3788
|
+
if (hops >= MAX_LINK_HOPS) throw Object.assign(new Error("too many symlinks"), { code: "ELOOP" });
|
|
3789
|
+
return realLocation(resolve2(realParent, link), hops + 1);
|
|
3790
|
+
}
|
|
3791
|
+
function realRootOf(root) {
|
|
3792
|
+
try {
|
|
3793
|
+
return realLocation(resolve2(root));
|
|
3794
|
+
} catch {
|
|
3795
|
+
return resolve2(root);
|
|
3796
|
+
}
|
|
3797
|
+
}
|
|
3798
|
+
function linkStaysInside(abs, realRoot) {
|
|
3799
|
+
try {
|
|
3800
|
+
return !escapes(realRoot, realpathSync2.native(abs));
|
|
3801
|
+
} catch {
|
|
3802
|
+
return false;
|
|
3803
|
+
}
|
|
3804
|
+
}
|
|
3519
3805
|
function rel(root, abs) {
|
|
3520
3806
|
return relative(root, abs).replace(/\\/g, "/") || ".";
|
|
3521
3807
|
}
|
|
@@ -3589,10 +3875,11 @@ function describeValue(value) {
|
|
|
3589
3875
|
if (typeof value === "number") return "an unusable number";
|
|
3590
3876
|
return typeof value === "object" ? "an object" : `a ${typeof value}`;
|
|
3591
3877
|
}
|
|
3592
|
-
function entryKind(entry, abs) {
|
|
3878
|
+
function entryKind(entry, abs, realRoot) {
|
|
3593
3879
|
if (entry.isDirectory()) return "dir";
|
|
3594
3880
|
if (entry.isFile()) return "file";
|
|
3595
3881
|
if (!entry.isSymbolicLink()) return "other";
|
|
3882
|
+
if (!linkStaysInside(abs, realRoot)) return "other";
|
|
3596
3883
|
try {
|
|
3597
3884
|
const stat = statSync(abs);
|
|
3598
3885
|
return stat.isDirectory() ? "dir" : stat.isFile() ? "file" : "other";
|
|
@@ -3730,9 +4017,10 @@ ${body2}` + (pdf.text.length > MAX_FILE_BYTES ? "\n\u2026 [truncated]" : "");
|
|
|
3730
4017
|
if (!fsCall(`cannot list ${name}`, () => statSync(abs)).isDirectory()) {
|
|
3731
4018
|
throw new ToolError(`${path} is a file \u2014 use read_file`);
|
|
3732
4019
|
}
|
|
4020
|
+
const realRoot = realRootOf(ctx.root);
|
|
3733
4021
|
const entries = fsCall(`cannot list ${name}`, () => readdirSync(abs, { withFileTypes: true })).map(
|
|
3734
4022
|
(entry) => {
|
|
3735
|
-
const isDir = entryKind(entry,
|
|
4023
|
+
const isDir = entryKind(entry, resolve2(abs, entry.name), realRoot) === "dir";
|
|
3736
4024
|
return { name: isDir ? `${entry.name}/` : entry.name, isDir };
|
|
3737
4025
|
}
|
|
3738
4026
|
);
|
|
@@ -3812,9 +4100,10 @@ ${body2}` + (pdf.text.length > MAX_FILE_BYTES ? "\n\u2026 [truncated]" : "");
|
|
|
3812
4100
|
path: name,
|
|
3813
4101
|
root: ctx.root
|
|
3814
4102
|
});
|
|
4103
|
+
safeResolve(ctx.root, abs);
|
|
3815
4104
|
fsCall(`cannot write ${name}`, () => {
|
|
3816
|
-
|
|
3817
|
-
|
|
4105
|
+
mkdirSync3(dirname3(abs), { recursive: true });
|
|
4106
|
+
writeFileSync3(abs, content, "utf8");
|
|
3818
4107
|
});
|
|
3819
4108
|
ctx.present?.(view);
|
|
3820
4109
|
return `wrote ${content.length} bytes to ${name}`;
|
|
@@ -3856,7 +4145,8 @@ ${body2}` + (pdf.text.length > MAX_FILE_BYTES ? "\n\u2026 [truncated]" : "");
|
|
|
3856
4145
|
path: name,
|
|
3857
4146
|
root: ctx.root
|
|
3858
4147
|
});
|
|
3859
|
-
|
|
4148
|
+
safeResolve(ctx.root, abs);
|
|
4149
|
+
fsCall(`cannot edit ${name}`, () => writeFileSync3(abs, after, "utf8"));
|
|
3860
4150
|
ctx.present?.(view);
|
|
3861
4151
|
return `edited ${name} (${count > 1 ? `${count} occurrences` : "1 occurrence"})`;
|
|
3862
4152
|
},
|
|
@@ -4262,6 +4552,7 @@ function walk(dir, root, visit) {
|
|
|
4262
4552
|
visit(dir);
|
|
4263
4553
|
return;
|
|
4264
4554
|
}
|
|
4555
|
+
const realRoot = realRootOf(root);
|
|
4265
4556
|
let seen = 0;
|
|
4266
4557
|
const stack = [dir];
|
|
4267
4558
|
while (stack.length > 0) {
|
|
@@ -4274,7 +4565,7 @@ function walk(dir, root, visit) {
|
|
|
4274
4565
|
}
|
|
4275
4566
|
for (const entry of entries) {
|
|
4276
4567
|
const abs = join3(current, entry.name);
|
|
4277
|
-
const kind = entryKind(entry, abs);
|
|
4568
|
+
const kind = entryKind(entry, abs, realRoot);
|
|
4278
4569
|
if (kind === "dir") {
|
|
4279
4570
|
if (entry.isSymbolicLink() || SKIP_DIRS.has(entry.name)) continue;
|
|
4280
4571
|
stack.push(abs);
|
|
@@ -4285,7 +4576,6 @@ function walk(dir, root, visit) {
|
|
|
4285
4576
|
if (!visit(abs)) return;
|
|
4286
4577
|
}
|
|
4287
4578
|
}
|
|
4288
|
-
void root;
|
|
4289
4579
|
}
|
|
4290
4580
|
function globToRegExp(pattern) {
|
|
4291
4581
|
const p = requireString(pattern, "glob", "pattern").replace(/\\/g, "/").replace(/^\.\//, "");
|
|
@@ -4590,7 +4880,7 @@ var TOOL_SCHEMA = [
|
|
|
4590
4880
|
|
|
4591
4881
|
// src/auto-mode.ts
|
|
4592
4882
|
import { homedir as homedir3 } from "node:os";
|
|
4593
|
-
import { isAbsolute as isAbsolute2, relative as relative2, resolve as
|
|
4883
|
+
import { isAbsolute as isAbsolute2, relative as relative2, resolve as resolve3, sep as sep2 } from "node:path";
|
|
4594
4884
|
function parseCommand(src) {
|
|
4595
4885
|
const pipelines = [];
|
|
4596
4886
|
let pipeline = [];
|
|
@@ -4763,9 +5053,9 @@ function expandHome(word, home) {
|
|
|
4763
5053
|
function resolveWord(word, ctx) {
|
|
4764
5054
|
const w = expandHome(word, ctx.home);
|
|
4765
5055
|
if (w === null) return null;
|
|
4766
|
-
if (isAbsolute2(w)) return
|
|
5056
|
+
if (isAbsolute2(w)) return resolve3(w);
|
|
4767
5057
|
if (ctx.cwd === null) return null;
|
|
4768
|
-
return
|
|
5058
|
+
return resolve3(ctx.cwd, w);
|
|
4769
5059
|
}
|
|
4770
5060
|
function within(parent, child) {
|
|
4771
5061
|
const rel2 = relative2(parent, child);
|
|
@@ -5299,7 +5589,7 @@ function classify(command, root, home, depth) {
|
|
|
5299
5589
|
if (FORK_BOMB.test(command)) return { kind: "dangerous", reason: "it is a fork bomb" };
|
|
5300
5590
|
if (FETCH_AND_RUN.some((re) => re.test(command))) return { kind: "dangerous", reason: "it downloads code and runs it" };
|
|
5301
5591
|
const parsed = parseCommand(command);
|
|
5302
|
-
const ctx = { root:
|
|
5592
|
+
const ctx = { root: resolve3(root), home: resolve3(home), cwd: resolve3(root) };
|
|
5303
5593
|
for (const pipeline of parsed.pipelines) {
|
|
5304
5594
|
let fetched = false;
|
|
5305
5595
|
for (const cmd of pipeline) {
|
|
@@ -5816,7 +6106,7 @@ async function dispatch(ctx, name, args) {
|
|
|
5816
6106
|
import { spawnSync } from "node:child_process";
|
|
5817
6107
|
import { existsSync as existsSync3, readdirSync as readdirSync2, readFileSync as readFileSync4 } from "node:fs";
|
|
5818
6108
|
import { homedir as homedir4, platform } from "node:os";
|
|
5819
|
-
import { join as join4, parse, resolve as
|
|
6109
|
+
import { join as join4, parse, resolve as resolve4 } from "node:path";
|
|
5820
6110
|
var MAX_CONTEXT_FILE_CHARS = 2e4;
|
|
5821
6111
|
function contextFileCandidates(root) {
|
|
5822
6112
|
return [join4(homedir4(), ".clixad", "CLIXAD.md"), join4(root, "AGENTS.md"), join4(root, "CLIXAD.md")];
|
|
@@ -5824,10 +6114,12 @@ function contextFileCandidates(root) {
|
|
|
5824
6114
|
function collectContext(root, base = SYSTEM_PROMPT) {
|
|
5825
6115
|
const parts = [base, environmentBlock(root)];
|
|
5826
6116
|
const files = [];
|
|
6117
|
+
const personal = join4(homedir4(), ".clixad", "CLIXAD.md");
|
|
5827
6118
|
for (const path of contextFileCandidates(root)) {
|
|
5828
6119
|
if (!existsSync3(path)) continue;
|
|
5829
6120
|
let text;
|
|
5830
6121
|
try {
|
|
6122
|
+
if (path !== personal) safeResolve(root, path);
|
|
5831
6123
|
text = readFileSync4(path, "utf8").trim();
|
|
5832
6124
|
} catch {
|
|
5833
6125
|
continue;
|
|
@@ -5882,8 +6174,8 @@ var INIT_PROMPT = "Create a CLIXAD.md file in the workspace root that tells a fu
|
|
|
5882
6174
|
var INIT_HINT = "No CLIXAD.md in this project yet. /init writes one, and every later session here starts out knowing the layout.";
|
|
5883
6175
|
var PROJECT_MARKERS = [".git", "package.json", "pyproject.toml", "Cargo.toml", "go.mod", "pom.xml", "Gemfile"];
|
|
5884
6176
|
function shouldSuggestInit(root) {
|
|
5885
|
-
const dir =
|
|
5886
|
-
if (dir ===
|
|
6177
|
+
const dir = resolve4(root);
|
|
6178
|
+
if (dir === resolve4(homedir4()) || dir === parse(dir).root) return false;
|
|
5887
6179
|
if (existsSync3(join4(dir, "CLIXAD.md")) || existsSync3(join4(dir, "AGENTS.md"))) return false;
|
|
5888
6180
|
return PROJECT_MARKERS.some((name) => existsSync3(join4(dir, name)));
|
|
5889
6181
|
}
|
|
@@ -6055,17 +6347,17 @@ var denyAll = async (req) => ({
|
|
|
6055
6347
|
import {
|
|
6056
6348
|
closeSync as closeSync2,
|
|
6057
6349
|
existsSync as existsSync4,
|
|
6058
|
-
mkdirSync as
|
|
6350
|
+
mkdirSync as mkdirSync4,
|
|
6059
6351
|
openSync as openSync2,
|
|
6060
6352
|
readdirSync as readdirSync3,
|
|
6061
6353
|
readFileSync as readFileSync5,
|
|
6062
6354
|
readSync,
|
|
6063
6355
|
rmSync,
|
|
6064
6356
|
statSync as statSync2,
|
|
6065
|
-
writeFileSync as
|
|
6357
|
+
writeFileSync as writeFileSync4
|
|
6066
6358
|
} from "node:fs";
|
|
6067
6359
|
import { homedir as homedir5 } from "node:os";
|
|
6068
|
-
import { join as join5, resolve as
|
|
6360
|
+
import { join as join5, resolve as resolve5 } from "node:path";
|
|
6069
6361
|
var ROOT = join5(homedir5(), ".clixad");
|
|
6070
6362
|
var SESSION_DIR = join5(ROOT, "sessions");
|
|
6071
6363
|
var HISTORY_FILE = join5(ROOT, "history");
|
|
@@ -6076,7 +6368,7 @@ function isSessionId(id) {
|
|
|
6076
6368
|
return SESSION_ID.test(id);
|
|
6077
6369
|
}
|
|
6078
6370
|
function sameDirectory(a, b) {
|
|
6079
|
-
const norm = (p) => process.platform === "win32" ?
|
|
6371
|
+
const norm = (p) => process.platform === "win32" ? resolve5(p).toLowerCase() : resolve5(p);
|
|
6080
6372
|
return norm(a) === norm(b);
|
|
6081
6373
|
}
|
|
6082
6374
|
function newSessionId() {
|
|
@@ -6085,9 +6377,9 @@ function newSessionId() {
|
|
|
6085
6377
|
}
|
|
6086
6378
|
function saveSession(session) {
|
|
6087
6379
|
try {
|
|
6088
|
-
|
|
6380
|
+
mkdirSync4(SESSION_DIR, { recursive: true });
|
|
6089
6381
|
const stored = { ...session, messages: withoutImages(session.messages) };
|
|
6090
|
-
|
|
6382
|
+
writeFileSync4(join5(SESSION_DIR, `${session.id}.json`), JSON.stringify(stored), "utf8");
|
|
6091
6383
|
prune();
|
|
6092
6384
|
} catch {
|
|
6093
6385
|
}
|
|
@@ -6175,17 +6467,17 @@ function loadHistory() {
|
|
|
6175
6467
|
}
|
|
6176
6468
|
function saveHistory(entries) {
|
|
6177
6469
|
try {
|
|
6178
|
-
|
|
6179
|
-
|
|
6470
|
+
mkdirSync4(ROOT, { recursive: true });
|
|
6471
|
+
writeFileSync4(HISTORY_FILE, entries.slice(0, MAX_HISTORY).reverse().join("\n") + "\n", "utf8");
|
|
6180
6472
|
} catch {
|
|
6181
6473
|
}
|
|
6182
6474
|
}
|
|
6183
6475
|
|
|
6184
6476
|
// src/kimi.ts
|
|
6185
6477
|
import { spawn as spawn5, spawnSync as spawnSync2 } from "node:child_process";
|
|
6186
|
-
import { existsSync as existsSync5, mkdirSync as
|
|
6478
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync5, writeFileSync as writeFileSync5 } from "node:fs";
|
|
6187
6479
|
import { homedir as homedir6 } from "node:os";
|
|
6188
|
-
import { dirname as
|
|
6480
|
+
import { dirname as dirname4, join as join6 } from "node:path";
|
|
6189
6481
|
var CONTEXT_TOKENS = {
|
|
6190
6482
|
"gpt-5-nano": 4e5,
|
|
6191
6483
|
"gemini-2.5-flash-lite": 1048576,
|
|
@@ -6265,8 +6557,8 @@ function runKimiCode(config, task, opts = {}) {
|
|
|
6265
6557
|
return Promise.resolve(1);
|
|
6266
6558
|
}
|
|
6267
6559
|
const path = kimiConfigPath();
|
|
6268
|
-
|
|
6269
|
-
|
|
6560
|
+
mkdirSync5(dirname4(path), { recursive: true });
|
|
6561
|
+
writeFileSync5(path, buildKimiConfig({ gatewayUrl: config.gatewayUrl, token: config.token, modelId: config.model }), "utf8");
|
|
6270
6562
|
const bin = process.env.CLIXAD_KIMI_BIN || "kimi";
|
|
6271
6563
|
if (!binExists(bin)) {
|
|
6272
6564
|
console.log("\n" + INSTALL_HELP);
|
|
@@ -6280,14 +6572,14 @@ function runKimiCode(config, task, opts = {}) {
|
|
|
6280
6572
|
OPENAI_BASE_URL: `${config.gatewayUrl.replace(/\/+$/, "")}/v1`,
|
|
6281
6573
|
OPENAI_API_KEY: config.token
|
|
6282
6574
|
};
|
|
6283
|
-
return new Promise((
|
|
6575
|
+
return new Promise((resolve6) => {
|
|
6284
6576
|
const child = process.platform === "win32" ? spawn5([bin, ...args].map(quoteWin).join(" "), { stdio: "inherit", env, shell: true }) : spawn5(bin, args, { stdio: "inherit", env });
|
|
6285
6577
|
child.on("error", (err) => {
|
|
6286
6578
|
if (err.code === "ENOENT") console.log("\n" + INSTALL_HELP);
|
|
6287
6579
|
else console.error(`failed to launch Kimi CLI: ${err.message}`);
|
|
6288
|
-
|
|
6580
|
+
resolve6(127);
|
|
6289
6581
|
});
|
|
6290
|
-
child.on("exit", (code) =>
|
|
6582
|
+
child.on("exit", (code) => resolve6(code ?? 0));
|
|
6291
6583
|
});
|
|
6292
6584
|
}
|
|
6293
6585
|
function quoteWin(s) {
|
package/dist/clixad.mjs
CHANGED
|
@@ -54,7 +54,7 @@ import {
|
|
|
54
54
|
takeConfigNotices,
|
|
55
55
|
toolSchemaFor,
|
|
56
56
|
usd
|
|
57
|
-
} from "./chunk-
|
|
57
|
+
} from "./chunk-CNLFGTYS.mjs";
|
|
58
58
|
|
|
59
59
|
// src/tui/load.ts
|
|
60
60
|
import * as nodeModule from "node:module";
|
|
@@ -133,7 +133,7 @@ async function withProductionReact(load) {
|
|
|
133
133
|
}
|
|
134
134
|
function loadTui() {
|
|
135
135
|
boundInkCaches();
|
|
136
|
-
return withProductionReact(() => import("./tui-
|
|
136
|
+
return withProductionReact(() => import("./tui-M5SJRCHM.mjs"));
|
|
137
137
|
}
|
|
138
138
|
|
|
139
139
|
// src/title.ts
|
|
@@ -105,7 +105,7 @@ import {
|
|
|
105
105
|
visibleLength,
|
|
106
106
|
webSearchCostLine,
|
|
107
107
|
welcomeBanner
|
|
108
|
-
} from "./chunk-
|
|
108
|
+
} from "./chunk-CNLFGTYS.mjs";
|
|
109
109
|
|
|
110
110
|
// src/tui/index.tsx
|
|
111
111
|
import "react";
|
|
@@ -486,7 +486,7 @@ var COMMANDS = [
|
|
|
486
486
|
// enter complete the command instead of running it, and listing is what both
|
|
487
487
|
// are for. `/bg kill <id>` still works when typed in full.
|
|
488
488
|
{ name: "bg", desc: "background commands the agent started" },
|
|
489
|
-
{ name: "mcp", desc: "
|
|
489
|
+
{ name: "mcp", desc: "MCP servers and their tools (approve: start one from this project)" },
|
|
490
490
|
{ name: "exit", desc: "quit clixad" }
|
|
491
491
|
];
|
|
492
492
|
function commandLabel(c) {
|
|
@@ -2271,17 +2271,10 @@ function App({
|
|
|
2271
2271
|
}
|
|
2272
2272
|
}).catch(() => void 0);
|
|
2273
2273
|
}, []);
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
if (names.length === 0) return;
|
|
2279
|
-
let cancelled = false;
|
|
2280
|
-
void McpHub.connect(servers, { root }).then((hub) => {
|
|
2281
|
-
if (cancelled) return void hub.close();
|
|
2282
|
-
mcpRef.current = hub;
|
|
2283
|
-
const ready = hub.status().filter((s) => s.state === "ready");
|
|
2284
|
-
const failed2 = hub.status().filter((s) => s.state === "failed");
|
|
2274
|
+
const reportMcpStarted = useCallback(
|
|
2275
|
+
(started) => {
|
|
2276
|
+
const ready = started.filter((s) => s.state === "ready");
|
|
2277
|
+
const failed2 = started.filter((s) => s.state === "failed");
|
|
2285
2278
|
if (ready.length) {
|
|
2286
2279
|
push({
|
|
2287
2280
|
kind: "notice",
|
|
@@ -2291,10 +2284,63 @@ function App({
|
|
|
2291
2284
|
for (const server of failed2) {
|
|
2292
2285
|
push({ kind: "notice", tone: "warn", text: ` mcp: ${server.name} did not start \u2014 ${server.error}` });
|
|
2293
2286
|
}
|
|
2287
|
+
},
|
|
2288
|
+
[push]
|
|
2289
|
+
);
|
|
2290
|
+
const askMcpApproval = useCallback(
|
|
2291
|
+
(hub, pending) => {
|
|
2292
|
+
if (!pending.length) return;
|
|
2293
|
+
const files = [...new Set(pending.map((p) => p.file))].join(" and ");
|
|
2294
|
+
push({
|
|
2295
|
+
kind: "notice",
|
|
2296
|
+
tone: "warn",
|
|
2297
|
+
text: ` ${files} in this project ${pending.length === 1 ? "wants to start an MCP server" : `wants to start ${pending.length} MCP servers`}. A server is a program that runs on this machine; nothing has been started.
|
|
2298
|
+
` + pending.map(
|
|
2299
|
+
(p) => ` ${p.name}
|
|
2300
|
+
` + p.display.split("\n").map((line2) => ` ${line2}`).join("\n") + `
|
|
2301
|
+
environment it gets: ${p.variables.length ? p.variables.join(", ") : "none beyond PATH, HOME and the locale"}`
|
|
2302
|
+
).join("\n")
|
|
2303
|
+
});
|
|
2304
|
+
const names = pending.map((p) => p.name);
|
|
2305
|
+
setPickerSel(1);
|
|
2306
|
+
setPicker({
|
|
2307
|
+
title: pending.length === 1 ? `Start the MCP server "${names[0]}"?` : `Start these ${pending.length} MCP servers?`,
|
|
2308
|
+
subtitle: "Only if you trust this project \xB7 esc: not now",
|
|
2309
|
+
items: [
|
|
2310
|
+
{ value: "approve", label: "Yes, start", hint: "and remember it for this project until the config changes" },
|
|
2311
|
+
{ value: "later", label: "Not now", hint: "ask again next time \xB7 /mcp approve starts them" },
|
|
2312
|
+
{ value: "decline", label: "No", hint: "and don't ask again until the config changes" }
|
|
2313
|
+
],
|
|
2314
|
+
onPick: (choice) => {
|
|
2315
|
+
if (choice.value === "approve") {
|
|
2316
|
+
push({ kind: "notice", text: ` mcp: starting ${names.join(", ")}` });
|
|
2317
|
+
void hub.approve(names, { remember: true }).then(reportMcpStarted);
|
|
2318
|
+
} else if (choice.value === "decline") {
|
|
2319
|
+
hub.decline(names);
|
|
2320
|
+
push({ kind: "notice", text: ` mcp: not starting ${names.join(", ")} \xB7 /mcp approve starts them` });
|
|
2321
|
+
} else {
|
|
2322
|
+
push({ kind: "notice", text: " mcp: not started \xB7 /mcp approve starts them" });
|
|
2323
|
+
}
|
|
2324
|
+
},
|
|
2325
|
+
onCancel: () => push({ kind: "notice", text: " mcp: not started \xB7 /mcp approve starts them" })
|
|
2326
|
+
});
|
|
2327
|
+
},
|
|
2328
|
+
[push, reportMcpStarted]
|
|
2329
|
+
);
|
|
2330
|
+
useEffect(() => {
|
|
2331
|
+
const loaded = loadMcpConfig(root);
|
|
2332
|
+
for (const problem of loaded.errors) push({ kind: "notice", tone: "warn", text: ` mcp config \u2014 ${problem}` });
|
|
2333
|
+
if (Object.keys(loaded.servers).length === 0) return;
|
|
2334
|
+
const hub = McpHub.fromConfig(loaded, { root });
|
|
2335
|
+
mcpRef.current = hub;
|
|
2336
|
+
let cancelled = false;
|
|
2337
|
+
void hub.startReleased().then((started) => {
|
|
2338
|
+
if (!cancelled) reportMcpStarted(started);
|
|
2294
2339
|
});
|
|
2340
|
+
askMcpApproval(hub, hub.pending());
|
|
2295
2341
|
return () => {
|
|
2296
2342
|
cancelled = true;
|
|
2297
|
-
void
|
|
2343
|
+
void hub.close();
|
|
2298
2344
|
};
|
|
2299
2345
|
}, []);
|
|
2300
2346
|
useEffect(() => {
|
|
@@ -3644,7 +3690,27 @@ ${preferencesText}` : ""),
|
|
|
3644
3690
|
return;
|
|
3645
3691
|
}
|
|
3646
3692
|
case "mcp": {
|
|
3647
|
-
const
|
|
3693
|
+
const hub = mcpRef.current;
|
|
3694
|
+
const [verb, ...targets] = arg.split(/\s+/).filter(Boolean);
|
|
3695
|
+
if (verb === "approve") {
|
|
3696
|
+
const held = hub.pending({ includeDeclined: true });
|
|
3697
|
+
if (!held.length) {
|
|
3698
|
+
push({ kind: "notice", text: " no MCP servers waiting for approval" });
|
|
3699
|
+
return;
|
|
3700
|
+
}
|
|
3701
|
+
if (!targets.length || targets.includes("all")) return askMcpApproval(hub, held);
|
|
3702
|
+
const unknown = targets.filter((t) => !held.some((h) => h.name === t));
|
|
3703
|
+
if (unknown.length) {
|
|
3704
|
+
push({
|
|
3705
|
+
kind: "notice",
|
|
3706
|
+
tone: "warn",
|
|
3707
|
+
text: ` not waiting for approval: ${unknown.join(", ")} \xB7 waiting: ${held.map((h) => h.name).join(", ")}`
|
|
3708
|
+
});
|
|
3709
|
+
return;
|
|
3710
|
+
}
|
|
3711
|
+
return askMcpApproval(hub, held.filter((h) => targets.includes(h.name)));
|
|
3712
|
+
}
|
|
3713
|
+
const status = hub.status();
|
|
3648
3714
|
if (!status.length) {
|
|
3649
3715
|
push({
|
|
3650
3716
|
kind: "notice",
|
|
@@ -3652,14 +3718,16 @@ ${preferencesText}` : ""),
|
|
|
3652
3718
|
});
|
|
3653
3719
|
return;
|
|
3654
3720
|
}
|
|
3721
|
+
const label = { unapproved: "not approved", declined: "declined" };
|
|
3722
|
+
const anyHeld = status.some((s) => s.state === "unapproved" || s.state === "declined");
|
|
3655
3723
|
push({
|
|
3656
3724
|
kind: "notice",
|
|
3657
3725
|
text: status.map((s) => {
|
|
3658
|
-
const head = ` ${s.name.padEnd(16)} ${s.state.padEnd(
|
|
3726
|
+
const head = ` ${s.name.padEnd(16)} ${(label[s.state] ?? s.state).padEnd(12)} ${s.transport}`;
|
|
3659
3727
|
if (s.state === "ready") return `${head} ${s.toolCount} tool${s.toolCount === 1 ? "" : "s"}`;
|
|
3660
3728
|
return s.error ? `${head}
|
|
3661
3729
|
${s.error.split("\n").join("\n ")}` : head;
|
|
3662
|
-
}).join("\n")
|
|
3730
|
+
}).join("\n") + (anyHeld ? "\n /mcp approve shows what a server runs and starts it" : "")
|
|
3663
3731
|
});
|
|
3664
3732
|
return;
|
|
3665
3733
|
}
|
|
@@ -3702,7 +3770,7 @@ ${preferencesText}` : ""),
|
|
|
3702
3770
|
push({ kind: "notice", tone: "warn", text: ` unknown command: /${cmd} \u2014 try /help` });
|
|
3703
3771
|
}
|
|
3704
3772
|
},
|
|
3705
|
-
[client, cols, config, cycleMode, exit, model, modelTaskHints, premium, push, replayConversation, root, runAdWall, runBusy, runBuy, runLogin, runTurn, setPremium, writeToStdout]
|
|
3773
|
+
[askMcpApproval, client, cols, config, cycleMode, exit, model, modelTaskHints, premium, push, replayConversation, root, runAdWall, runBusy, runBuy, runLogin, runTurn, setPremium, writeToStdout]
|
|
3706
3774
|
);
|
|
3707
3775
|
const runShell = useCallback(
|
|
3708
3776
|
async (command2) => {
|