clixad 0.0.1-beta.46 → 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
|
|
|
@@ -13,6 +13,10 @@ function isAdFormat(value) {
|
|
|
13
13
|
// src/sponsor/direct.ts
|
|
14
14
|
var DIRECT_ASK_TIMEOUT_MS = 1500;
|
|
15
15
|
var MAX_TEXT = 200;
|
|
16
|
+
function parseMeasure(body) {
|
|
17
|
+
const raw = body?.measure;
|
|
18
|
+
return typeof raw === "string" && raw.length > 0 && raw.length <= 2e3 ? raw : void 0;
|
|
19
|
+
}
|
|
16
20
|
function sanitizeAdText(raw) {
|
|
17
21
|
return raw.replace(/\u001b\[[0-?]*[ -/]*[@-~]/g, "").replace(/\u001b\][^\u0007\u001b]*(?:\u0007|\u001b\\)?/g, "").replace(/[\u0000-\u001f\u007f-\u009f]/g, " ").replace(/[--]/g, "").replace(/[-]/g, "").replace(/\s+/g, " ").trim();
|
|
18
22
|
}
|
|
@@ -62,7 +66,8 @@ function readServedLine(e) {
|
|
|
62
66
|
} catch {
|
|
63
67
|
return null;
|
|
64
68
|
}
|
|
65
|
-
|
|
69
|
+
const shortUrl = typeof e.short_url === "string" && shortLinkText(e.short_url) ? e.short_url : void 0;
|
|
70
|
+
return { format: e.format, text, serveToken: e.serve_token, clickUrl, ...shortUrl ? { shortUrl } : {} };
|
|
66
71
|
}
|
|
67
72
|
var MAX_LINK_TEXT = 40;
|
|
68
73
|
function shortLinkText(clickUrl) {
|
|
@@ -638,16 +643,16 @@ async function httpFailure(res, what) {
|
|
|
638
643
|
return detail ? `${what} failed: ${res.status} \u2014 ${detail}` : `${what} failed: ${res.status}`;
|
|
639
644
|
}
|
|
640
645
|
function sleep(ms, signal) {
|
|
641
|
-
return new Promise((
|
|
646
|
+
return new Promise((resolve6) => {
|
|
642
647
|
if (!signal) {
|
|
643
|
-
setTimeout(
|
|
648
|
+
setTimeout(resolve6, ms);
|
|
644
649
|
return;
|
|
645
650
|
}
|
|
646
|
-
if (signal.aborted) return
|
|
651
|
+
if (signal.aborted) return resolve6();
|
|
647
652
|
const done = () => {
|
|
648
653
|
clearTimeout(timer);
|
|
649
654
|
signal.removeEventListener("abort", done);
|
|
650
|
-
|
|
655
|
+
resolve6();
|
|
651
656
|
};
|
|
652
657
|
const timer = setTimeout(done, ms);
|
|
653
658
|
signal.addEventListener("abort", done, { once: true });
|
|
@@ -1140,7 +1145,13 @@ var GatewayClient = class {
|
|
|
1140
1145
|
if (!res.ok) return { supported: true, lines: [], failed: true };
|
|
1141
1146
|
const body = await res.json();
|
|
1142
1147
|
const fallbacks = parseFallbackLines(body);
|
|
1143
|
-
|
|
1148
|
+
const measure = parseMeasure(body);
|
|
1149
|
+
return {
|
|
1150
|
+
supported: true,
|
|
1151
|
+
lines: parseDirectLines(body),
|
|
1152
|
+
...fallbacks.length > 0 ? { fallbacks } : {},
|
|
1153
|
+
...measure ? { measure } : {}
|
|
1154
|
+
};
|
|
1144
1155
|
} catch {
|
|
1145
1156
|
return { supported: true, lines: [], failed: true };
|
|
1146
1157
|
}
|
|
@@ -1157,7 +1168,14 @@ var GatewayClient = class {
|
|
|
1157
1168
|
method: "POST",
|
|
1158
1169
|
headers: this.headers(),
|
|
1159
1170
|
...signal ? { signal } : {},
|
|
1160
|
-
body: JSON.stringify({
|
|
1171
|
+
body: JSON.stringify({
|
|
1172
|
+
impressions: impressions.map((i) => ({
|
|
1173
|
+
format: i.format,
|
|
1174
|
+
serve_token: i.serveToken,
|
|
1175
|
+
...i.visibleMs !== void 0 ? { visible_ms: Math.round(i.visibleMs) } : {},
|
|
1176
|
+
...i.link ? { link: i.link } : {}
|
|
1177
|
+
}))
|
|
1178
|
+
})
|
|
1161
1179
|
});
|
|
1162
1180
|
if (!res.ok) return 0;
|
|
1163
1181
|
const body = await res.json();
|
|
@@ -1166,6 +1184,27 @@ var GatewayClient = class {
|
|
|
1166
1184
|
return 0;
|
|
1167
1185
|
}
|
|
1168
1186
|
}
|
|
1187
|
+
/**
|
|
1188
|
+
* Report how long each place a turn asked about stayed on screen with a
|
|
1189
|
+
* line in it, with the turn's measure token: what a booked week is
|
|
1190
|
+
* forecast on. Never throws; answers how many places counted.
|
|
1191
|
+
*/
|
|
1192
|
+
async reportSponsorViews(measure, places, signal) {
|
|
1193
|
+
if (places.length === 0) return 0;
|
|
1194
|
+
try {
|
|
1195
|
+
const res = await gatewayFetch(`${this.config.gatewayUrl}/v1/sponsor/direct/views`, {
|
|
1196
|
+
method: "POST",
|
|
1197
|
+
headers: this.headers(),
|
|
1198
|
+
...signal ? { signal } : {},
|
|
1199
|
+
body: JSON.stringify({ measure, places: places.map((p) => ({ format: p.format, visible_ms: Math.round(p.visibleMs) })) })
|
|
1200
|
+
});
|
|
1201
|
+
if (!res.ok) return 0;
|
|
1202
|
+
const body = await res.json();
|
|
1203
|
+
return typeof body.counted === "number" ? body.counted : 0;
|
|
1204
|
+
} catch {
|
|
1205
|
+
return 0;
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1169
1208
|
/**
|
|
1170
1209
|
* Report the fallback lines a turn drew, in one call, after the turn is
|
|
1171
1210
|
* over. They are counted and earn nothing, because nobody pays for them being
|
|
@@ -1993,16 +2032,16 @@ async function devLogin(client, config, email, invite) {
|
|
|
1993
2032
|
};
|
|
1994
2033
|
}
|
|
1995
2034
|
function sleep2(ms, signal) {
|
|
1996
|
-
return new Promise((
|
|
2035
|
+
return new Promise((resolve6) => {
|
|
1997
2036
|
if (!signal) {
|
|
1998
|
-
setTimeout(
|
|
2037
|
+
setTimeout(resolve6, ms);
|
|
1999
2038
|
return;
|
|
2000
2039
|
}
|
|
2001
|
-
if (signal.aborted) return
|
|
2040
|
+
if (signal.aborted) return resolve6();
|
|
2002
2041
|
const done = () => {
|
|
2003
2042
|
clearTimeout(timer);
|
|
2004
2043
|
signal.removeEventListener("abort", done);
|
|
2005
|
-
|
|
2044
|
+
resolve6();
|
|
2006
2045
|
};
|
|
2007
2046
|
const timer = setTimeout(done, ms);
|
|
2008
2047
|
signal.addEventListener("abort", done, { once: true });
|
|
@@ -2297,8 +2336,18 @@ ${renderTodos(items)}${warning}`;
|
|
|
2297
2336
|
|
|
2298
2337
|
// src/tools.ts
|
|
2299
2338
|
import { spawn as spawn4 } from "node:child_process";
|
|
2300
|
-
import {
|
|
2301
|
-
|
|
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";
|
|
2302
2351
|
|
|
2303
2352
|
// src/diff.ts
|
|
2304
2353
|
var R = "\x1B[0m";
|
|
@@ -2987,9 +3036,10 @@ function decodeEntities(text) {
|
|
|
2987
3036
|
|
|
2988
3037
|
// src/mcp.ts
|
|
2989
3038
|
import { spawn as spawn3 } from "node:child_process";
|
|
2990
|
-
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";
|
|
2991
3041
|
import { homedir as homedir2 } from "node:os";
|
|
2992
|
-
import { join as join2 } from "node:path";
|
|
3042
|
+
import { dirname as dirname2, join as join2, resolve } from "node:path";
|
|
2993
3043
|
var CONNECT_TIMEOUT_MS = 2e4;
|
|
2994
3044
|
var DEFAULT_CALL_TIMEOUT_MS = 12e4;
|
|
2995
3045
|
var PROTOCOL_VERSION = "2025-06-18";
|
|
@@ -3000,14 +3050,20 @@ var McpError = class extends Error {
|
|
|
3000
3050
|
function isHttpConfig(c) {
|
|
3001
3051
|
return c.url !== void 0;
|
|
3002
3052
|
}
|
|
3003
|
-
function
|
|
3004
|
-
|
|
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;
|
|
3005
3060
|
}
|
|
3006
3061
|
function loadMcpConfig(root) {
|
|
3007
3062
|
const servers = {};
|
|
3063
|
+
const origins = {};
|
|
3008
3064
|
const sources = [];
|
|
3009
3065
|
const errors = [];
|
|
3010
|
-
for (const path of
|
|
3066
|
+
for (const { path, scope } of mcpConfigFiles(root)) {
|
|
3011
3067
|
if (!existsSync(path)) continue;
|
|
3012
3068
|
let parsed;
|
|
3013
3069
|
try {
|
|
@@ -3028,13 +3084,24 @@ function loadMcpConfig(root) {
|
|
|
3028
3084
|
sources.push(short(path));
|
|
3029
3085
|
for (const [name, raw] of Object.entries(declared)) {
|
|
3030
3086
|
const config = validateServer(name, raw);
|
|
3031
|
-
if (typeof config === "string")
|
|
3032
|
-
|
|
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
|
+
};
|
|
3033
3099
|
}
|
|
3034
3100
|
}
|
|
3035
|
-
return { servers, sources, errors };
|
|
3101
|
+
return { servers, origins, sources, errors };
|
|
3036
3102
|
}
|
|
3037
3103
|
function validateServer(name, raw) {
|
|
3104
|
+
if (/[\u0000-\u001f\u007f-\u009f]/.test(name)) return `server ${JSON.stringify(name)} has control characters in its name`;
|
|
3038
3105
|
if (typeof raw !== "object" || raw === null) return `server "${name}" is not an object`;
|
|
3039
3106
|
const c = raw;
|
|
3040
3107
|
if (typeof c.url === "string") {
|
|
@@ -3075,6 +3142,109 @@ function expandMap(map) {
|
|
|
3075
3142
|
function short(path) {
|
|
3076
3143
|
return path.replace(homedir2(), "~");
|
|
3077
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
|
+
}
|
|
3078
3248
|
var StdioTransport = class {
|
|
3079
3249
|
child;
|
|
3080
3250
|
buffer = "";
|
|
@@ -3085,10 +3255,13 @@ var StdioTransport = class {
|
|
|
3085
3255
|
constructor(config, root) {
|
|
3086
3256
|
this.child = spawn3(config.command, config.args ?? [], {
|
|
3087
3257
|
cwd: config.cwd ?? root,
|
|
3088
|
-
//
|
|
3089
|
-
//
|
|
3090
|
-
//
|
|
3091
|
-
|
|
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),
|
|
3092
3265
|
stdio: ["pipe", "pipe", "pipe"],
|
|
3093
3266
|
windowsHide: true,
|
|
3094
3267
|
// npx and friends are .cmd shims on Windows, which CreateProcess cannot
|
|
@@ -3138,7 +3311,7 @@ var StdioTransport = class {
|
|
|
3138
3311
|
if (this.exited) return Promise.reject(new McpError(this.exited));
|
|
3139
3312
|
const id = ++this.seq;
|
|
3140
3313
|
const payload = JSON.stringify({ jsonrpc: "2.0", id, method, params }) + "\n";
|
|
3141
|
-
return new Promise((
|
|
3314
|
+
return new Promise((resolve6, reject) => {
|
|
3142
3315
|
const settle = (fn) => {
|
|
3143
3316
|
clearTimeout(timer);
|
|
3144
3317
|
signal?.removeEventListener("abort", onAbort);
|
|
@@ -3152,7 +3325,7 @@ var StdioTransport = class {
|
|
|
3152
3325
|
const onAbort = () => settle(() => reject(new McpError("cancelled")));
|
|
3153
3326
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
3154
3327
|
this.pending.set(id, {
|
|
3155
|
-
resolve: (v) => settle(() =>
|
|
3328
|
+
resolve: (v) => settle(() => resolve6(v)),
|
|
3156
3329
|
reject: (e) => settle(() => reject(e))
|
|
3157
3330
|
});
|
|
3158
3331
|
this.child.stdin?.write(payload, (err) => {
|
|
@@ -3276,23 +3449,42 @@ async function readSseResponse(res, id) {
|
|
|
3276
3449
|
return null;
|
|
3277
3450
|
}
|
|
3278
3451
|
var McpServerConnection = class {
|
|
3279
|
-
constructor(name, config, root) {
|
|
3452
|
+
constructor(name, config, root, origin, held) {
|
|
3280
3453
|
this.name = name;
|
|
3281
3454
|
this.config = config;
|
|
3282
|
-
this.
|
|
3455
|
+
this.root = root;
|
|
3456
|
+
this.origin = origin;
|
|
3457
|
+
this.state = held ?? "connecting";
|
|
3283
3458
|
}
|
|
3284
3459
|
name;
|
|
3285
3460
|
config;
|
|
3286
|
-
|
|
3461
|
+
root;
|
|
3462
|
+
origin;
|
|
3463
|
+
state;
|
|
3287
3464
|
error;
|
|
3288
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
|
+
*/
|
|
3289
3470
|
transport;
|
|
3290
3471
|
get kind() {
|
|
3291
3472
|
return isHttpConfig(this.config) ? "http" : "stdio";
|
|
3292
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
|
+
}
|
|
3293
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;
|
|
3294
3486
|
try {
|
|
3295
|
-
await
|
|
3487
|
+
await transport.request(
|
|
3296
3488
|
"initialize",
|
|
3297
3489
|
{
|
|
3298
3490
|
protocolVersion: PROTOCOL_VERSION,
|
|
@@ -3301,8 +3493,8 @@ var McpServerConnection = class {
|
|
|
3301
3493
|
},
|
|
3302
3494
|
timeoutMs
|
|
3303
3495
|
);
|
|
3304
|
-
await
|
|
3305
|
-
const listed = await
|
|
3496
|
+
await transport.notify("notifications/initialized", {});
|
|
3497
|
+
const listed = await transport.request("tools/list", {}, timeoutMs);
|
|
3306
3498
|
this.tools = (listed?.tools ?? []).filter(
|
|
3307
3499
|
(t) => typeof t?.name === "string" && t.name.length > 0
|
|
3308
3500
|
).map((t) => ({
|
|
@@ -3313,17 +3505,19 @@ var McpServerConnection = class {
|
|
|
3313
3505
|
...typeof t.description === "string" ? { description: t.description } : {},
|
|
3314
3506
|
...t.inputSchema && typeof t.inputSchema === "object" ? { inputSchema: t.inputSchema } : {}
|
|
3315
3507
|
}));
|
|
3508
|
+
if (this.state !== "connecting") return;
|
|
3316
3509
|
this.state = "ready";
|
|
3317
3510
|
} catch (err) {
|
|
3511
|
+
if (this.state !== "connecting") return;
|
|
3318
3512
|
this.state = "failed";
|
|
3319
|
-
const diagnostics =
|
|
3513
|
+
const diagnostics = transport.diagnostics();
|
|
3320
3514
|
this.error = err.message + (diagnostics ? `
|
|
3321
3515
|
${diagnostics.split("\n").slice(-3).join("\n ")}` : "");
|
|
3322
|
-
await
|
|
3516
|
+
await transport.close().catch(() => void 0);
|
|
3323
3517
|
}
|
|
3324
3518
|
}
|
|
3325
3519
|
async call(tool, args, timeoutMs, signal) {
|
|
3326
|
-
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}`);
|
|
3327
3521
|
const raw = await this.transport.request(
|
|
3328
3522
|
"tools/call",
|
|
3329
3523
|
{ name: tool, arguments: args ?? {} },
|
|
@@ -3336,7 +3530,7 @@ var McpServerConnection = class {
|
|
|
3336
3530
|
}
|
|
3337
3531
|
async close() {
|
|
3338
3532
|
this.state = "closed";
|
|
3339
|
-
await this.transport
|
|
3533
|
+
await this.transport?.close().catch(() => void 0);
|
|
3340
3534
|
}
|
|
3341
3535
|
};
|
|
3342
3536
|
function renderContent(content, structured) {
|
|
@@ -3379,34 +3573,118 @@ function isMcpToolName(name) {
|
|
|
3379
3573
|
return name.startsWith("mcp__");
|
|
3380
3574
|
}
|
|
3381
3575
|
var McpHub = class _McpHub {
|
|
3382
|
-
constructor(servers) {
|
|
3576
|
+
constructor(servers, root) {
|
|
3383
3577
|
this.servers = servers;
|
|
3578
|
+
this.root = root;
|
|
3384
3579
|
}
|
|
3385
3580
|
servers;
|
|
3581
|
+
root;
|
|
3386
3582
|
/**
|
|
3387
|
-
* Connect to every
|
|
3583
|
+
* Connect to every server given, in parallel, and never reject.
|
|
3388
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.
|
|
3389
3587
|
* Parallel because a slow server must not delay the others, and one that
|
|
3390
3588
|
* cannot start must not delay anything: the whole point is that the REPL opens
|
|
3391
3589
|
* whether or not somebody's Docker daemon is running today.
|
|
3392
3590
|
*/
|
|
3393
3591
|
static async connect(servers, opts = { root: process.cwd() }) {
|
|
3394
3592
|
const connections = Object.entries(servers).map(([name, config]) => new McpServerConnection(name, config, opts.root));
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
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) {
|
|
3398
3641
|
for (const tool of conn.tools) {
|
|
3642
|
+
if (tool.qualified) continue;
|
|
3399
3643
|
let name = qualifiedToolName(conn.name, tool.name);
|
|
3400
3644
|
for (let i = 2; taken.has(name); i++) name = `${name.slice(0, 61)}_${i}`;
|
|
3401
3645
|
taken.add(name);
|
|
3402
3646
|
tool.qualified = name;
|
|
3403
3647
|
}
|
|
3404
3648
|
}
|
|
3405
|
-
return new _McpHub(connections);
|
|
3406
3649
|
}
|
|
3407
|
-
/**
|
|
3408
|
-
|
|
3409
|
-
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
|
+
}
|
|
3410
3688
|
}
|
|
3411
3689
|
tools() {
|
|
3412
3690
|
return this.servers.filter((s) => s.state === "ready").flatMap((s) => s.tools);
|
|
@@ -3470,13 +3748,60 @@ var PermissionDeniedError = class extends ToolError {
|
|
|
3470
3748
|
}
|
|
3471
3749
|
};
|
|
3472
3750
|
function safeResolve(root, p) {
|
|
3473
|
-
const abs = isAbsolute(p) ? p :
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
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`);
|
|
3477
3764
|
}
|
|
3478
3765
|
return abs;
|
|
3479
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
|
+
}
|
|
3480
3805
|
function rel(root, abs) {
|
|
3481
3806
|
return relative(root, abs).replace(/\\/g, "/") || ".";
|
|
3482
3807
|
}
|
|
@@ -3550,10 +3875,11 @@ function describeValue(value) {
|
|
|
3550
3875
|
if (typeof value === "number") return "an unusable number";
|
|
3551
3876
|
return typeof value === "object" ? "an object" : `a ${typeof value}`;
|
|
3552
3877
|
}
|
|
3553
|
-
function entryKind(entry, abs) {
|
|
3878
|
+
function entryKind(entry, abs, realRoot) {
|
|
3554
3879
|
if (entry.isDirectory()) return "dir";
|
|
3555
3880
|
if (entry.isFile()) return "file";
|
|
3556
3881
|
if (!entry.isSymbolicLink()) return "other";
|
|
3882
|
+
if (!linkStaysInside(abs, realRoot)) return "other";
|
|
3557
3883
|
try {
|
|
3558
3884
|
const stat = statSync(abs);
|
|
3559
3885
|
return stat.isDirectory() ? "dir" : stat.isFile() ? "file" : "other";
|
|
@@ -3691,9 +4017,10 @@ ${body2}` + (pdf.text.length > MAX_FILE_BYTES ? "\n\u2026 [truncated]" : "");
|
|
|
3691
4017
|
if (!fsCall(`cannot list ${name}`, () => statSync(abs)).isDirectory()) {
|
|
3692
4018
|
throw new ToolError(`${path} is a file \u2014 use read_file`);
|
|
3693
4019
|
}
|
|
4020
|
+
const realRoot = realRootOf(ctx.root);
|
|
3694
4021
|
const entries = fsCall(`cannot list ${name}`, () => readdirSync(abs, { withFileTypes: true })).map(
|
|
3695
4022
|
(entry) => {
|
|
3696
|
-
const isDir = entryKind(entry,
|
|
4023
|
+
const isDir = entryKind(entry, resolve2(abs, entry.name), realRoot) === "dir";
|
|
3697
4024
|
return { name: isDir ? `${entry.name}/` : entry.name, isDir };
|
|
3698
4025
|
}
|
|
3699
4026
|
);
|
|
@@ -3773,9 +4100,10 @@ ${body2}` + (pdf.text.length > MAX_FILE_BYTES ? "\n\u2026 [truncated]" : "");
|
|
|
3773
4100
|
path: name,
|
|
3774
4101
|
root: ctx.root
|
|
3775
4102
|
});
|
|
4103
|
+
safeResolve(ctx.root, abs);
|
|
3776
4104
|
fsCall(`cannot write ${name}`, () => {
|
|
3777
|
-
|
|
3778
|
-
|
|
4105
|
+
mkdirSync3(dirname3(abs), { recursive: true });
|
|
4106
|
+
writeFileSync3(abs, content, "utf8");
|
|
3779
4107
|
});
|
|
3780
4108
|
ctx.present?.(view);
|
|
3781
4109
|
return `wrote ${content.length} bytes to ${name}`;
|
|
@@ -3817,7 +4145,8 @@ ${body2}` + (pdf.text.length > MAX_FILE_BYTES ? "\n\u2026 [truncated]" : "");
|
|
|
3817
4145
|
path: name,
|
|
3818
4146
|
root: ctx.root
|
|
3819
4147
|
});
|
|
3820
|
-
|
|
4148
|
+
safeResolve(ctx.root, abs);
|
|
4149
|
+
fsCall(`cannot edit ${name}`, () => writeFileSync3(abs, after, "utf8"));
|
|
3821
4150
|
ctx.present?.(view);
|
|
3822
4151
|
return `edited ${name} (${count > 1 ? `${count} occurrences` : "1 occurrence"})`;
|
|
3823
4152
|
},
|
|
@@ -4223,6 +4552,7 @@ function walk(dir, root, visit) {
|
|
|
4223
4552
|
visit(dir);
|
|
4224
4553
|
return;
|
|
4225
4554
|
}
|
|
4555
|
+
const realRoot = realRootOf(root);
|
|
4226
4556
|
let seen = 0;
|
|
4227
4557
|
const stack = [dir];
|
|
4228
4558
|
while (stack.length > 0) {
|
|
@@ -4235,7 +4565,7 @@ function walk(dir, root, visit) {
|
|
|
4235
4565
|
}
|
|
4236
4566
|
for (const entry of entries) {
|
|
4237
4567
|
const abs = join3(current, entry.name);
|
|
4238
|
-
const kind = entryKind(entry, abs);
|
|
4568
|
+
const kind = entryKind(entry, abs, realRoot);
|
|
4239
4569
|
if (kind === "dir") {
|
|
4240
4570
|
if (entry.isSymbolicLink() || SKIP_DIRS.has(entry.name)) continue;
|
|
4241
4571
|
stack.push(abs);
|
|
@@ -4246,7 +4576,6 @@ function walk(dir, root, visit) {
|
|
|
4246
4576
|
if (!visit(abs)) return;
|
|
4247
4577
|
}
|
|
4248
4578
|
}
|
|
4249
|
-
void root;
|
|
4250
4579
|
}
|
|
4251
4580
|
function globToRegExp(pattern) {
|
|
4252
4581
|
const p = requireString(pattern, "glob", "pattern").replace(/\\/g, "/").replace(/^\.\//, "");
|
|
@@ -4551,7 +4880,7 @@ var TOOL_SCHEMA = [
|
|
|
4551
4880
|
|
|
4552
4881
|
// src/auto-mode.ts
|
|
4553
4882
|
import { homedir as homedir3 } from "node:os";
|
|
4554
|
-
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";
|
|
4555
4884
|
function parseCommand(src) {
|
|
4556
4885
|
const pipelines = [];
|
|
4557
4886
|
let pipeline = [];
|
|
@@ -4724,9 +5053,9 @@ function expandHome(word, home) {
|
|
|
4724
5053
|
function resolveWord(word, ctx) {
|
|
4725
5054
|
const w = expandHome(word, ctx.home);
|
|
4726
5055
|
if (w === null) return null;
|
|
4727
|
-
if (isAbsolute2(w)) return
|
|
5056
|
+
if (isAbsolute2(w)) return resolve3(w);
|
|
4728
5057
|
if (ctx.cwd === null) return null;
|
|
4729
|
-
return
|
|
5058
|
+
return resolve3(ctx.cwd, w);
|
|
4730
5059
|
}
|
|
4731
5060
|
function within(parent, child) {
|
|
4732
5061
|
const rel2 = relative2(parent, child);
|
|
@@ -5260,7 +5589,7 @@ function classify(command, root, home, depth) {
|
|
|
5260
5589
|
if (FORK_BOMB.test(command)) return { kind: "dangerous", reason: "it is a fork bomb" };
|
|
5261
5590
|
if (FETCH_AND_RUN.some((re) => re.test(command))) return { kind: "dangerous", reason: "it downloads code and runs it" };
|
|
5262
5591
|
const parsed = parseCommand(command);
|
|
5263
|
-
const ctx = { root:
|
|
5592
|
+
const ctx = { root: resolve3(root), home: resolve3(home), cwd: resolve3(root) };
|
|
5264
5593
|
for (const pipeline of parsed.pipelines) {
|
|
5265
5594
|
let fetched = false;
|
|
5266
5595
|
for (const cmd of pipeline) {
|
|
@@ -5777,7 +6106,7 @@ async function dispatch(ctx, name, args) {
|
|
|
5777
6106
|
import { spawnSync } from "node:child_process";
|
|
5778
6107
|
import { existsSync as existsSync3, readdirSync as readdirSync2, readFileSync as readFileSync4 } from "node:fs";
|
|
5779
6108
|
import { homedir as homedir4, platform } from "node:os";
|
|
5780
|
-
import { join as join4, parse, resolve as
|
|
6109
|
+
import { join as join4, parse, resolve as resolve4 } from "node:path";
|
|
5781
6110
|
var MAX_CONTEXT_FILE_CHARS = 2e4;
|
|
5782
6111
|
function contextFileCandidates(root) {
|
|
5783
6112
|
return [join4(homedir4(), ".clixad", "CLIXAD.md"), join4(root, "AGENTS.md"), join4(root, "CLIXAD.md")];
|
|
@@ -5785,10 +6114,12 @@ function contextFileCandidates(root) {
|
|
|
5785
6114
|
function collectContext(root, base = SYSTEM_PROMPT) {
|
|
5786
6115
|
const parts = [base, environmentBlock(root)];
|
|
5787
6116
|
const files = [];
|
|
6117
|
+
const personal = join4(homedir4(), ".clixad", "CLIXAD.md");
|
|
5788
6118
|
for (const path of contextFileCandidates(root)) {
|
|
5789
6119
|
if (!existsSync3(path)) continue;
|
|
5790
6120
|
let text;
|
|
5791
6121
|
try {
|
|
6122
|
+
if (path !== personal) safeResolve(root, path);
|
|
5792
6123
|
text = readFileSync4(path, "utf8").trim();
|
|
5793
6124
|
} catch {
|
|
5794
6125
|
continue;
|
|
@@ -5843,8 +6174,8 @@ var INIT_PROMPT = "Create a CLIXAD.md file in the workspace root that tells a fu
|
|
|
5843
6174
|
var INIT_HINT = "No CLIXAD.md in this project yet. /init writes one, and every later session here starts out knowing the layout.";
|
|
5844
6175
|
var PROJECT_MARKERS = [".git", "package.json", "pyproject.toml", "Cargo.toml", "go.mod", "pom.xml", "Gemfile"];
|
|
5845
6176
|
function shouldSuggestInit(root) {
|
|
5846
|
-
const dir =
|
|
5847
|
-
if (dir ===
|
|
6177
|
+
const dir = resolve4(root);
|
|
6178
|
+
if (dir === resolve4(homedir4()) || dir === parse(dir).root) return false;
|
|
5848
6179
|
if (existsSync3(join4(dir, "CLIXAD.md")) || existsSync3(join4(dir, "AGENTS.md"))) return false;
|
|
5849
6180
|
return PROJECT_MARKERS.some((name) => existsSync3(join4(dir, name)));
|
|
5850
6181
|
}
|
|
@@ -6016,17 +6347,17 @@ var denyAll = async (req) => ({
|
|
|
6016
6347
|
import {
|
|
6017
6348
|
closeSync as closeSync2,
|
|
6018
6349
|
existsSync as existsSync4,
|
|
6019
|
-
mkdirSync as
|
|
6350
|
+
mkdirSync as mkdirSync4,
|
|
6020
6351
|
openSync as openSync2,
|
|
6021
6352
|
readdirSync as readdirSync3,
|
|
6022
6353
|
readFileSync as readFileSync5,
|
|
6023
6354
|
readSync,
|
|
6024
6355
|
rmSync,
|
|
6025
6356
|
statSync as statSync2,
|
|
6026
|
-
writeFileSync as
|
|
6357
|
+
writeFileSync as writeFileSync4
|
|
6027
6358
|
} from "node:fs";
|
|
6028
6359
|
import { homedir as homedir5 } from "node:os";
|
|
6029
|
-
import { join as join5, resolve as
|
|
6360
|
+
import { join as join5, resolve as resolve5 } from "node:path";
|
|
6030
6361
|
var ROOT = join5(homedir5(), ".clixad");
|
|
6031
6362
|
var SESSION_DIR = join5(ROOT, "sessions");
|
|
6032
6363
|
var HISTORY_FILE = join5(ROOT, "history");
|
|
@@ -6037,7 +6368,7 @@ function isSessionId(id) {
|
|
|
6037
6368
|
return SESSION_ID.test(id);
|
|
6038
6369
|
}
|
|
6039
6370
|
function sameDirectory(a, b) {
|
|
6040
|
-
const norm = (p) => process.platform === "win32" ?
|
|
6371
|
+
const norm = (p) => process.platform === "win32" ? resolve5(p).toLowerCase() : resolve5(p);
|
|
6041
6372
|
return norm(a) === norm(b);
|
|
6042
6373
|
}
|
|
6043
6374
|
function newSessionId() {
|
|
@@ -6046,9 +6377,9 @@ function newSessionId() {
|
|
|
6046
6377
|
}
|
|
6047
6378
|
function saveSession(session) {
|
|
6048
6379
|
try {
|
|
6049
|
-
|
|
6380
|
+
mkdirSync4(SESSION_DIR, { recursive: true });
|
|
6050
6381
|
const stored = { ...session, messages: withoutImages(session.messages) };
|
|
6051
|
-
|
|
6382
|
+
writeFileSync4(join5(SESSION_DIR, `${session.id}.json`), JSON.stringify(stored), "utf8");
|
|
6052
6383
|
prune();
|
|
6053
6384
|
} catch {
|
|
6054
6385
|
}
|
|
@@ -6136,17 +6467,17 @@ function loadHistory() {
|
|
|
6136
6467
|
}
|
|
6137
6468
|
function saveHistory(entries) {
|
|
6138
6469
|
try {
|
|
6139
|
-
|
|
6140
|
-
|
|
6470
|
+
mkdirSync4(ROOT, { recursive: true });
|
|
6471
|
+
writeFileSync4(HISTORY_FILE, entries.slice(0, MAX_HISTORY).reverse().join("\n") + "\n", "utf8");
|
|
6141
6472
|
} catch {
|
|
6142
6473
|
}
|
|
6143
6474
|
}
|
|
6144
6475
|
|
|
6145
6476
|
// src/kimi.ts
|
|
6146
6477
|
import { spawn as spawn5, spawnSync as spawnSync2 } from "node:child_process";
|
|
6147
|
-
import { existsSync as existsSync5, mkdirSync as
|
|
6478
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync5, writeFileSync as writeFileSync5 } from "node:fs";
|
|
6148
6479
|
import { homedir as homedir6 } from "node:os";
|
|
6149
|
-
import { dirname as
|
|
6480
|
+
import { dirname as dirname4, join as join6 } from "node:path";
|
|
6150
6481
|
var CONTEXT_TOKENS = {
|
|
6151
6482
|
"gpt-5-nano": 4e5,
|
|
6152
6483
|
"gemini-2.5-flash-lite": 1048576,
|
|
@@ -6226,8 +6557,8 @@ function runKimiCode(config, task, opts = {}) {
|
|
|
6226
6557
|
return Promise.resolve(1);
|
|
6227
6558
|
}
|
|
6228
6559
|
const path = kimiConfigPath();
|
|
6229
|
-
|
|
6230
|
-
|
|
6560
|
+
mkdirSync5(dirname4(path), { recursive: true });
|
|
6561
|
+
writeFileSync5(path, buildKimiConfig({ gatewayUrl: config.gatewayUrl, token: config.token, modelId: config.model }), "utf8");
|
|
6231
6562
|
const bin = process.env.CLIXAD_KIMI_BIN || "kimi";
|
|
6232
6563
|
if (!binExists(bin)) {
|
|
6233
6564
|
console.log("\n" + INSTALL_HELP);
|
|
@@ -6241,14 +6572,14 @@ function runKimiCode(config, task, opts = {}) {
|
|
|
6241
6572
|
OPENAI_BASE_URL: `${config.gatewayUrl.replace(/\/+$/, "")}/v1`,
|
|
6242
6573
|
OPENAI_API_KEY: config.token
|
|
6243
6574
|
};
|
|
6244
|
-
return new Promise((
|
|
6575
|
+
return new Promise((resolve6) => {
|
|
6245
6576
|
const child = process.platform === "win32" ? spawn5([bin, ...args].map(quoteWin).join(" "), { stdio: "inherit", env, shell: true }) : spawn5(bin, args, { stdio: "inherit", env });
|
|
6246
6577
|
child.on("error", (err) => {
|
|
6247
6578
|
if (err.code === "ENOENT") console.log("\n" + INSTALL_HELP);
|
|
6248
6579
|
else console.error(`failed to launch Kimi CLI: ${err.message}`);
|
|
6249
|
-
|
|
6580
|
+
resolve6(127);
|
|
6250
6581
|
});
|
|
6251
|
-
child.on("exit", (code) =>
|
|
6582
|
+
child.on("exit", (code) => resolve6(code ?? 0));
|
|
6252
6583
|
});
|
|
6253
6584
|
}
|
|
6254
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";
|
|
@@ -287,19 +287,25 @@ function sidebarCard(line2, cols, linkText) {
|
|
|
287
287
|
const link = linkText && visibleLength(label) + visibleLength(LINK_SEPARATOR) + visibleLength(linkText) <= SIDEBAR_BODY_COLS ? linkText : null;
|
|
288
288
|
return { rule: SIDEBAR_RULE, label, link, body };
|
|
289
289
|
}
|
|
290
|
-
function sponsorRow(line2, cols, origin, linkText) {
|
|
290
|
+
function sponsorRow(line2, cols, origin, linkText, linkRequired = false) {
|
|
291
291
|
const marker = origin === "house" || cols >= AD_FULL_PREFIX_MIN_COLS ? HOUSE_MARKER_ROW : AD_MARKER_ROW;
|
|
292
292
|
const label = origin === "network" ? LABEL_ROW : null;
|
|
293
293
|
if (origin === "network" && !adFitsWidth(cols)) return null;
|
|
294
294
|
const room = cols - visibleLength(marker) - (label ? visibleLength(label) : 0);
|
|
295
295
|
if (room < MIN_BODY_COLS) return null;
|
|
296
296
|
if (origin === "network" && linkText) {
|
|
297
|
-
const
|
|
297
|
+
const linkCols = visibleLength(LINK_SEPARATOR) + visibleLength(linkText);
|
|
298
|
+
const whole = visibleLength(line2) + linkCols;
|
|
298
299
|
if (whole <= room) return { marker, label, body: line2, link: linkText, origin };
|
|
300
|
+
if (linkRequired && room - linkCols >= MIN_LINKED_COPY_COLS) {
|
|
301
|
+
const copy = room - linkCols;
|
|
302
|
+
return { marker, label, body: `${sliceColumns(line2, copy - 1).trimEnd()}\u2026`, link: linkText, origin };
|
|
303
|
+
}
|
|
299
304
|
}
|
|
300
305
|
const body = visibleLength(line2) <= room ? line2 : `${sliceColumns(line2, room - 1).trimEnd()}\u2026`;
|
|
301
306
|
return { marker, label, body, link: null, origin };
|
|
302
307
|
}
|
|
308
|
+
var MIN_LINKED_COPY_COLS = 12;
|
|
303
309
|
function adColorEnabled({ isTTY, env }) {
|
|
304
310
|
if (!colorEnabled({ isTTY, env })) return false;
|
|
305
311
|
if (env.CI !== void 0 && env.CI !== "") return false;
|
|
@@ -356,6 +362,46 @@ function createSponsorSlot(config, format = "top_bar") {
|
|
|
356
362
|
};
|
|
357
363
|
}
|
|
358
364
|
|
|
365
|
+
// src/sponsor/visibility.ts
|
|
366
|
+
var VisibilityClock = class {
|
|
367
|
+
tracks = /* @__PURE__ */ new Map();
|
|
368
|
+
/** The keys on screen as of `now`. Every key not in `onScreen` is off screen from `now`. */
|
|
369
|
+
observe(onScreen, now) {
|
|
370
|
+
const visible = new Set(onScreen);
|
|
371
|
+
for (const [key, track] of this.tracks) {
|
|
372
|
+
if (track.since !== null && !visible.has(key)) {
|
|
373
|
+
track.done.push([track.since, now]);
|
|
374
|
+
track.since = null;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
for (const key of visible) {
|
|
378
|
+
const track = this.tracks.get(key) ?? { since: null, done: [] };
|
|
379
|
+
if (track.since === null) track.since = now;
|
|
380
|
+
this.tracks.set(key, track);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* The longest unbroken stretch `key` was on screen up to `now`, counting
|
|
385
|
+
* only time from `from` on (a place is measured from the moment a booked
|
|
386
|
+
* line could first have been drawn in it). Zero for a key never seen.
|
|
387
|
+
*/
|
|
388
|
+
longest(key, now, from = Number.NEGATIVE_INFINITY) {
|
|
389
|
+
const track = this.tracks.get(key);
|
|
390
|
+
if (!track) return 0;
|
|
391
|
+
const stretches = track.since === null ? track.done : [...track.done, [track.since, now]];
|
|
392
|
+
let best = 0;
|
|
393
|
+
for (const [start, end] of stretches) best = Math.max(best, Math.min(end, now) - Math.max(start, from));
|
|
394
|
+
return Math.max(0, best);
|
|
395
|
+
}
|
|
396
|
+
/** Forget everything: a new turn measures from nothing. */
|
|
397
|
+
reset() {
|
|
398
|
+
this.tracks.clear();
|
|
399
|
+
}
|
|
400
|
+
};
|
|
401
|
+
function placeKey(format) {
|
|
402
|
+
return `place:${format}`;
|
|
403
|
+
}
|
|
404
|
+
|
|
359
405
|
// src/sponsor/hyperlink.ts
|
|
360
406
|
function version(v) {
|
|
361
407
|
const m = /^(\d+)(?:\.(\d+))?/.exec(v ?? "");
|
|
@@ -440,7 +486,7 @@ var COMMANDS = [
|
|
|
440
486
|
// enter complete the command instead of running it, and listing is what both
|
|
441
487
|
// are for. `/bg kill <id>` still works when typed in full.
|
|
442
488
|
{ name: "bg", desc: "background commands the agent started" },
|
|
443
|
-
{ name: "mcp", desc: "
|
|
489
|
+
{ name: "mcp", desc: "MCP servers and their tools (approve: start one from this project)" },
|
|
444
490
|
{ name: "exit", desc: "quit clixad" }
|
|
445
491
|
];
|
|
446
492
|
function commandLabel(c) {
|
|
@@ -2018,6 +2064,9 @@ function App({
|
|
|
2018
2064
|
const directSupportedRef = useRef(true);
|
|
2019
2065
|
const knownBookedRef = useRef(/* @__PURE__ */ new Set());
|
|
2020
2066
|
const drawnRef = useRef(/* @__PURE__ */ new Set());
|
|
2067
|
+
const visibilityRef = useRef(new VisibilityClock());
|
|
2068
|
+
const linkModeRef = useRef(/* @__PURE__ */ new Map());
|
|
2069
|
+
const measureRef = useRef(null);
|
|
2021
2070
|
const sponsorSlotRef = useRef(
|
|
2022
2071
|
/* @__PURE__ */ new Map()
|
|
2023
2072
|
);
|
|
@@ -2222,17 +2271,10 @@ function App({
|
|
|
2222
2271
|
}
|
|
2223
2272
|
}).catch(() => void 0);
|
|
2224
2273
|
}, []);
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
if (names.length === 0) return;
|
|
2230
|
-
let cancelled = false;
|
|
2231
|
-
void McpHub.connect(servers, { root }).then((hub) => {
|
|
2232
|
-
if (cancelled) return void hub.close();
|
|
2233
|
-
mcpRef.current = hub;
|
|
2234
|
-
const ready = hub.status().filter((s) => s.state === "ready");
|
|
2235
|
-
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");
|
|
2236
2278
|
if (ready.length) {
|
|
2237
2279
|
push({
|
|
2238
2280
|
kind: "notice",
|
|
@@ -2242,10 +2284,63 @@ function App({
|
|
|
2242
2284
|
for (const server of failed2) {
|
|
2243
2285
|
push({ kind: "notice", tone: "warn", text: ` mcp: ${server.name} did not start \u2014 ${server.error}` });
|
|
2244
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);
|
|
2245
2339
|
});
|
|
2340
|
+
askMcpApproval(hub, hub.pending());
|
|
2246
2341
|
return () => {
|
|
2247
2342
|
cancelled = true;
|
|
2248
|
-
void
|
|
2343
|
+
void hub.close();
|
|
2249
2344
|
};
|
|
2250
2345
|
}, []);
|
|
2251
2346
|
useEffect(() => {
|
|
@@ -2260,6 +2355,9 @@ function App({
|
|
|
2260
2355
|
const nextSponsor = useCallback(
|
|
2261
2356
|
(modelId) => {
|
|
2262
2357
|
const call = ++sponsorCallRef.current;
|
|
2358
|
+
visibilityRef.current.reset();
|
|
2359
|
+
linkModeRef.current = /* @__PURE__ */ new Map();
|
|
2360
|
+
measureRef.current = null;
|
|
2263
2361
|
if (!sponsorEnabled({
|
|
2264
2362
|
isTTY: stdout.isTTY,
|
|
2265
2363
|
env: process.env,
|
|
@@ -2312,11 +2410,14 @@ function App({
|
|
|
2312
2410
|
if (format !== "top_bar" && modalOpenRef.current) return;
|
|
2313
2411
|
const drawnId = `direct:${call}:${format}`;
|
|
2314
2412
|
const link = line2.clickUrl;
|
|
2413
|
+
const linkable2 = hyperlinksEnabled({ isTTY: stdout.isTTY, env: process.env });
|
|
2414
|
+
const linkText = !linkable2 && line2.shortUrl ? shortLinkText(line2.shortUrl) : void 0;
|
|
2415
|
+
const extra = linkText ? { link, linkText, linkRequired: true } : { link };
|
|
2315
2416
|
if (format === "top_bar") {
|
|
2316
|
-
if (!sponsorRow(line2.text, colsRef.current, "network")) return;
|
|
2317
|
-
setSponsor({ text: line2.text, origin: "network", impressionId: drawnId,
|
|
2417
|
+
if (!sponsorRow(line2.text, colsRef.current, "network", linkText, true)) return;
|
|
2418
|
+
setSponsor({ text: line2.text, origin: "network", impressionId: drawnId, ...extra });
|
|
2318
2419
|
} else {
|
|
2319
|
-
setAdSlots((slots) => ({ ...slots, [format]: { text: line2.text, impressionId: drawnId,
|
|
2420
|
+
setAdSlots((slots) => ({ ...slots, [format]: { text: line2.text, impressionId: drawnId, ...extra } }));
|
|
2320
2421
|
}
|
|
2321
2422
|
pendingDirectRef.current.set(format, { call, serveToken: line2.serveToken, drawnId });
|
|
2322
2423
|
};
|
|
@@ -2354,6 +2455,9 @@ function App({
|
|
|
2354
2455
|
failed2 = answer.failed === true;
|
|
2355
2456
|
direct = answer.lines;
|
|
2356
2457
|
fallbacks = answer.fallbacks ?? [];
|
|
2458
|
+
if (answer.measure && call === sponsorCallRef.current) {
|
|
2459
|
+
measureRef.current = { call, token: answer.measure, answeredAt: Date.now(), formats: [...fitting] };
|
|
2460
|
+
}
|
|
2357
2461
|
if (!failed2) knownBookedRef.current = new Set(direct.map((line2) => line2.format));
|
|
2358
2462
|
}
|
|
2359
2463
|
if (call !== sponsorCallRef.current) return;
|
|
@@ -2374,12 +2478,29 @@ function App({
|
|
|
2374
2478
|
const pendingDirect = [...pendingDirectRef.current.entries()];
|
|
2375
2479
|
const pendingFallback = [...pendingFallbackRef.current.entries()];
|
|
2376
2480
|
const drawn = drawnRef.current;
|
|
2481
|
+
const clock = visibilityRef.current;
|
|
2482
|
+
const linkModes = linkModeRef.current;
|
|
2483
|
+
const measure = measureRef.current;
|
|
2484
|
+
const at = Date.now();
|
|
2377
2485
|
pendingImpressionRef.current = /* @__PURE__ */ new Map();
|
|
2378
2486
|
pendingDirectRef.current = /* @__PURE__ */ new Map();
|
|
2379
2487
|
pendingFallbackRef.current = /* @__PURE__ */ new Map();
|
|
2380
2488
|
drawnRef.current = /* @__PURE__ */ new Set();
|
|
2381
|
-
|
|
2489
|
+
measureRef.current = null;
|
|
2490
|
+
const directReports = pendingDirect.filter(([, p]) => p.call === sponsorCallRef.current && drawn.has(p.drawnId)).map(([format, p]) => ({
|
|
2491
|
+
format,
|
|
2492
|
+
serveToken: p.serveToken,
|
|
2493
|
+
visibleMs: clock.longest(p.drawnId, at),
|
|
2494
|
+
link: linkModes.get(p.drawnId) ?? "none"
|
|
2495
|
+
}));
|
|
2382
2496
|
if (directReports.length > 0) void client.reportDirectImpressions(directReports);
|
|
2497
|
+
if (measure && measure.call === sponsorCallRef.current) {
|
|
2498
|
+
const places = measure.formats.map((format) => ({
|
|
2499
|
+
format,
|
|
2500
|
+
visibleMs: clock.longest(placeKey(format), at, measure.answeredAt)
|
|
2501
|
+
}));
|
|
2502
|
+
void client.reportSponsorViews(measure.token, places);
|
|
2503
|
+
}
|
|
2383
2504
|
const fallbackReports = pendingFallback.filter(([, p]) => p.call === sponsorCallRef.current && drawn.has(p.drawnId)).map(([format, p]) => ({ format, serveToken: p.serveToken }));
|
|
2384
2505
|
if (fallbackReports.length > 0) void client.reportAffiliateImpressions(fallbackReports);
|
|
2385
2506
|
const reports = pending.filter((p) => p.call === sponsorCallRef.current).filter((p) => drawn.has(p.impression.impressionId)).map((p) => p.impression);
|
|
@@ -3569,7 +3690,27 @@ ${preferencesText}` : ""),
|
|
|
3569
3690
|
return;
|
|
3570
3691
|
}
|
|
3571
3692
|
case "mcp": {
|
|
3572
|
-
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();
|
|
3573
3714
|
if (!status.length) {
|
|
3574
3715
|
push({
|
|
3575
3716
|
kind: "notice",
|
|
@@ -3577,14 +3718,16 @@ ${preferencesText}` : ""),
|
|
|
3577
3718
|
});
|
|
3578
3719
|
return;
|
|
3579
3720
|
}
|
|
3721
|
+
const label = { unapproved: "not approved", declined: "declined" };
|
|
3722
|
+
const anyHeld = status.some((s) => s.state === "unapproved" || s.state === "declined");
|
|
3580
3723
|
push({
|
|
3581
3724
|
kind: "notice",
|
|
3582
3725
|
text: status.map((s) => {
|
|
3583
|
-
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}`;
|
|
3584
3727
|
if (s.state === "ready") return `${head} ${s.toolCount} tool${s.toolCount === 1 ? "" : "s"}`;
|
|
3585
3728
|
return s.error ? `${head}
|
|
3586
3729
|
${s.error.split("\n").join("\n ")}` : head;
|
|
3587
|
-
}).join("\n")
|
|
3730
|
+
}).join("\n") + (anyHeld ? "\n /mcp approve shows what a server runs and starts it" : "")
|
|
3588
3731
|
});
|
|
3589
3732
|
return;
|
|
3590
3733
|
}
|
|
@@ -3627,7 +3770,7 @@ ${preferencesText}` : ""),
|
|
|
3627
3770
|
push({ kind: "notice", tone: "warn", text: ` unknown command: /${cmd} \u2014 try /help` });
|
|
3628
3771
|
}
|
|
3629
3772
|
},
|
|
3630
|
-
[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]
|
|
3631
3774
|
);
|
|
3632
3775
|
const runShell = useCallback(
|
|
3633
3776
|
async (command2) => {
|
|
@@ -4023,13 +4166,13 @@ ${dropped.map((l) => ` \xB7 ${l}`).join("\n")}`
|
|
|
4023
4166
|
});
|
|
4024
4167
|
const elapsed = busy && startedAt ? Math.floor((Date.now() - startedAt) / 1e3) : 0;
|
|
4025
4168
|
const spinner = spinnerRef.current[Math.floor(tick / SPINNER_TICKS_PER_FRAME) % spinnerRef.current.length];
|
|
4026
|
-
const sponsorLine = busy && sponsor ? sponsorRow(sponsor.text, cols, sponsor.origin, sponsor.linkText) : null;
|
|
4169
|
+
const sponsorLine = busy && sponsor ? sponsorRow(sponsor.text, cols, sponsor.origin, sponsor.linkText, sponsor.linkRequired === true) : null;
|
|
4027
4170
|
const adColor = adColorEnabled({ isTTY: stdout.isTTY, env: process.env });
|
|
4028
4171
|
const linkable = hyperlinksEnabled({ isTTY: stdout.isTTY, env: process.env });
|
|
4029
4172
|
const modalOpen = Boolean(picker || ask || plan || buy);
|
|
4030
4173
|
modalOpenRef.current = modalOpen;
|
|
4031
4174
|
const card = busy && !modalOpen && adSlots.sidebar_card ? sidebarCard(adSlots.sidebar_card.text, cols, adSlots.sidebar_card.linkText) : null;
|
|
4032
|
-
const statusBarLine = busy && !modalOpen && adSlots.status_bar ? sponsorRow(adSlots.status_bar.text, cols, "network", adSlots.status_bar.linkText) : null;
|
|
4175
|
+
const statusBarLine = busy && !modalOpen && adSlots.status_bar ? sponsorRow(adSlots.status_bar.text, cols, "network", adSlots.status_bar.linkText, adSlots.status_bar.linkRequired === true) : null;
|
|
4033
4176
|
const viewport = Math.max(1, rows - 1);
|
|
4034
4177
|
const inputRows = Math.max(1, Math.min(MAX_INPUT_ROWS, editor.lines.length));
|
|
4035
4178
|
const inputFrom = windowStart(editor.row, editor.lines.length, inputRows);
|
|
@@ -4060,9 +4203,24 @@ ${dropped.map((l) => ` \xB7 ${l}`).join("\n")}`
|
|
|
4060
4203
|
showCard ? adSlots.sidebar_card?.impressionId : void 0,
|
|
4061
4204
|
showStatusBar ? adSlots.status_bar?.impressionId : void 0
|
|
4062
4205
|
].filter(Boolean).join(" ");
|
|
4206
|
+
const placesKey = [
|
|
4207
|
+
showSponsor && sponsorLine ? placeKey("top_bar") : void 0,
|
|
4208
|
+
showCard ? placeKey("sidebar_card") : void 0,
|
|
4209
|
+
showStatusBar ? placeKey("status_bar") : void 0
|
|
4210
|
+
].filter(Boolean).join(" ");
|
|
4211
|
+
if (showSponsor && sponsorLine && sponsor?.impressionId?.startsWith("direct:")) {
|
|
4212
|
+
linkModeRef.current.set(sponsor.impressionId, linkable ? "link" : sponsorLine.link ? "address" : "none");
|
|
4213
|
+
}
|
|
4214
|
+
if (showCard && card && adSlots.sidebar_card?.impressionId.startsWith("direct:")) {
|
|
4215
|
+
linkModeRef.current.set(adSlots.sidebar_card.impressionId, linkable ? "link" : card.link ? "address" : "none");
|
|
4216
|
+
}
|
|
4217
|
+
if (showStatusBar && statusBarLine && adSlots.status_bar?.impressionId.startsWith("direct:")) {
|
|
4218
|
+
linkModeRef.current.set(adSlots.status_bar.impressionId, linkable ? "link" : statusBarLine.link ? "address" : "none");
|
|
4219
|
+
}
|
|
4063
4220
|
useEffect(() => {
|
|
4064
4221
|
for (const id of drawnKey.split(" ")) if (id) drawnRef.current.add(id);
|
|
4065
|
-
|
|
4222
|
+
visibilityRef.current.observe([...drawnKey.split(" "), ...placesKey.split(" ")].filter(Boolean), Date.now());
|
|
4223
|
+
}, [drawnKey, placesKey]);
|
|
4066
4224
|
let budget = Math.max(0, viewport - chromeHeight - menu.length - busyHeight);
|
|
4067
4225
|
const pickerCompact = Boolean(picker) && budget < PICKER_CHROME + 1;
|
|
4068
4226
|
const pickerChrome = pickerCompact ? PICKER_CHROME_COMPACT : PICKER_CHROME;
|