@mrciphersmith/keryx 0.2.99 → 0.2.101

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.
Files changed (2) hide show
  1. package/dist/cli.js +697 -279
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -298,6 +298,91 @@ var init_provider_config = __esm(() => {
298
298
  ];
299
299
  });
300
300
 
301
+ // src/lib/oauth/catalog.ts
302
+ function extraRequestHeaders(provider) {
303
+ if (provider === "github-copilot") {
304
+ return GITHUB_COPILOT_REQUEST_HEADERS;
305
+ }
306
+ return;
307
+ }
308
+ function authCatalogEntry(provider) {
309
+ return PROVIDER_AUTH_CATALOG.find((entry) => entry.provider === provider);
310
+ }
311
+ function catalogMethods(provider) {
312
+ return authCatalogEntry(provider)?.methods ?? ["api-key"];
313
+ }
314
+ function catalogAllows(provider, method) {
315
+ return catalogMethods(provider).includes(method);
316
+ }
317
+ function catalogRefusal(provider) {
318
+ return authCatalogEntry(provider)?.refusal;
319
+ }
320
+ function deviceCodeMethodLabel(provider) {
321
+ if (provider === "grok")
322
+ return "SuperGrok Subscription";
323
+ if (provider === "github-copilot")
324
+ return "Login with GitHub Copilot";
325
+ if (provider === "openai")
326
+ return "ChatGPT Plus/Pro (headless)";
327
+ return "Device authorization";
328
+ }
329
+ var XAI_GROK_DEVICE, GITHUB_COPILOT_REQUEST_HEADERS, GITHUB_COPILOT_OAUTH_CLIENT_ID = "Iv1.b507a08c87ecfe98", GITHUB_COPILOT_DEVICE, PROVIDER_AUTH_CATALOG;
330
+ var init_catalog = __esm(() => {
331
+ XAI_GROK_DEVICE = {
332
+ deviceAuthorizationEndpoint: "https://auth.x.ai/oauth2/device/code",
333
+ tokenEndpoint: "https://auth.x.ai/oauth2/token",
334
+ clientId: "b1a00492-073a-47ea-816f-4c329264a828",
335
+ scope: "openid profile email offline_access grok-cli:access api:access",
336
+ extraForm: { referrer: "keryx" },
337
+ headers: { "User-Agent": "keryx" }
338
+ };
339
+ GITHUB_COPILOT_REQUEST_HEADERS = {
340
+ Accept: "application/json",
341
+ "User-Agent": "GitHubCopilotChat/0.26.7",
342
+ "Editor-Version": "vscode/1.99.3",
343
+ "Editor-Plugin-Version": "copilot-chat/0.26.7",
344
+ "Copilot-Integration-Id": "vscode-chat"
345
+ };
346
+ GITHUB_COPILOT_DEVICE = {
347
+ deviceAuthorizationEndpoint: "https://github.com/login/device/code",
348
+ tokenEndpoint: "https://github.com/login/oauth/access_token",
349
+ clientId: GITHUB_COPILOT_OAUTH_CLIENT_ID,
350
+ scope: "read:user",
351
+ headers: GITHUB_COPILOT_REQUEST_HEADERS,
352
+ afterToken: "github-copilot"
353
+ };
354
+ PROVIDER_AUTH_CATALOG = [
355
+ {
356
+ provider: "grok",
357
+ label: "xAI (Grok)",
358
+ methods: ["device-code", "api-key"],
359
+ device: XAI_GROK_DEVICE
360
+ },
361
+ {
362
+ provider: "openai",
363
+ label: "OpenAI",
364
+ methods: ["oauth-pkce-loopback", "device-code", "api-key"]
365
+ },
366
+ {
367
+ provider: "github-copilot",
368
+ label: "GitHub Copilot",
369
+ methods: ["device-code"],
370
+ device: GITHUB_COPILOT_DEVICE
371
+ },
372
+ { provider: "anthropic", label: "Anthropic", methods: ["api-key"], refusal: "Anthropic does not permit third-party Claude Pro/Max login. Use an API key from the Anthropic Console." },
373
+ { provider: "gemini", label: "Google Gemini", methods: ["api-key"], refusal: "Gemini Google-account login is for Gemini CLI. In keryx use GEMINI_API_KEY from Google AI Studio." },
374
+ { provider: "deepseek", label: "DeepSeek", methods: ["api-key"], refusal: "DeepSeek has no consumer OAuth grant. Use DEEPSEEK_API_KEY." },
375
+ { provider: "openrouter", label: "OpenRouter", methods: ["api-key"] },
376
+ { provider: "zai", label: "Z.AI (GLM)", methods: ["api-key"] },
377
+ { provider: "zai-coding", label: "Z.AI GLM Coding Plan", methods: ["api-key"] },
378
+ { provider: "cerebras", label: "Cerebras", methods: ["api-key"] },
379
+ { provider: "groq", label: "Groq", methods: ["api-key"] },
380
+ { provider: "moonshot", label: "Moonshot (Kimi)", methods: ["api-key"] },
381
+ { provider: "ollama", label: "Ollama", methods: ["none"] },
382
+ { provider: "rapid-mlx", label: "Rapid-MLX (Local)", methods: ["none"] }
383
+ ];
384
+ });
385
+
301
386
  // src/lib/shell-config.ts
302
387
  import { existsSync as existsSync2 } from "fs";
303
388
  import path3 from "path";
@@ -641,8 +726,13 @@ async function fetchOpenAiCompatModelsDetailed(fetchFn, provider, apiKey, opts)
641
726
  const timer = setTimeout(() => controller.abort(), timeoutMs);
642
727
  try {
643
728
  const init = { signal: controller.signal };
729
+ const extraHeaders = extraRequestHeaders(provider.name);
730
+ const headers = extraHeaders === undefined ? {} : { ...extraHeaders };
644
731
  if (apiKey !== undefined && apiKey.length > 0) {
645
- init.headers = { authorization: `Bearer ${apiKey}` };
732
+ headers.authorization = `Bearer ${apiKey}`;
733
+ }
734
+ if (Object.keys(headers).length > 0) {
735
+ init.headers = headers;
646
736
  }
647
737
  const res = await fetchFn(url, init);
648
738
  if (!res.ok) {
@@ -890,6 +980,7 @@ with a stated reason and exits 0 \u2014 that is a normal configuration, not an e
890
980
  var DEFAULT_MODELS_PATH = "/v1/models", OPENAI_COMPAT_PROVIDERS, MODELS_FETCH_TIMEOUT_MS = 1e4, MODELS_ERROR_BODY_LIMIT = 4096, BALANCE_FETCH_TIMEOUT_MS = 8000;
891
981
  var init_providers = __esm(() => {
892
982
  init_provider_config();
983
+ init_catalog();
893
984
  init_grants();
894
985
  init_shell_config();
895
986
  init_args();
@@ -8310,6 +8401,7 @@ function makeProvider(name, _model, opts) {
8310
8401
  if (needsKey && (apiKey === undefined || apiKey.length === 0)) {
8311
8402
  return new FakeProvider([]);
8312
8403
  }
8404
+ const extraHeaders = extraRequestHeaders(name);
8313
8405
  const grant = {
8314
8406
  network: true,
8315
8407
  ...compat.streamUsage === true ? { streamUsage: true } : {},
@@ -8317,7 +8409,8 @@ function makeProvider(name, _model, opts) {
8317
8409
  ...compat.allowLoopback === true ? { allowLoopback: true } : {},
8318
8410
  ...compat.allowPrivateLan === true ? { allowPrivateLan: true } : {},
8319
8411
  ...compat.chatPath !== undefined ? { chatPath: compat.chatPath } : {},
8320
- ...apiKey !== undefined ? { apiKey } : {}
8412
+ ...apiKey !== undefined ? { apiKey } : {},
8413
+ ...extraHeaders !== undefined ? { headers: extraHeaders } : {}
8321
8414
  };
8322
8415
  return new OpenAiCompatEngine({ fetch: opts.fetch, grant }, { ...OLLAMA_COMPAT_IDENTITY, providerLabel: compat.label });
8323
8416
  }
@@ -8326,6 +8419,7 @@ function makeProvider(name, _model, opts) {
8326
8419
  var OLLAMA_COMPAT_IDENTITY;
8327
8420
  var init_make_provider = __esm(() => {
8328
8421
  init_providers();
8422
+ init_catalog();
8329
8423
  init_anthropic_provider();
8330
8424
  init_openai_compat_provider();
8331
8425
  init_fake_provider();
@@ -40876,78 +40970,6 @@ async function startStdioTransport(server) {
40876
40970
  await server.connect(transport);
40877
40971
  }
40878
40972
 
40879
- // src/lib/oauth/catalog.ts
40880
- function authCatalogEntry(provider) {
40881
- return PROVIDER_AUTH_CATALOG.find((entry) => entry.provider === provider);
40882
- }
40883
- function catalogMethods(provider) {
40884
- return authCatalogEntry(provider)?.methods ?? ["api-key"];
40885
- }
40886
- function catalogAllows(provider, method) {
40887
- return catalogMethods(provider).includes(method);
40888
- }
40889
- function catalogRefusal(provider) {
40890
- return authCatalogEntry(provider)?.refusal;
40891
- }
40892
- function deviceCodeMethodLabel(provider) {
40893
- if (provider === "grok")
40894
- return "SuperGrok Subscription";
40895
- if (provider === "github-copilot")
40896
- return "Login with GitHub Copilot";
40897
- if (provider === "openai")
40898
- return "ChatGPT Plus/Pro (headless)";
40899
- return "Device authorization";
40900
- }
40901
- var XAI_GROK_DEVICE, GITHUB_COPILOT_DEVICE, PROVIDER_AUTH_CATALOG;
40902
- var init_catalog = __esm(() => {
40903
- XAI_GROK_DEVICE = {
40904
- deviceAuthorizationEndpoint: "https://auth.x.ai/oauth2/device/code",
40905
- tokenEndpoint: "https://auth.x.ai/oauth2/token",
40906
- clientId: "b1a00492-073a-47ea-816f-4c329264a828",
40907
- scope: "openid profile email offline_access grok-cli:access api:access",
40908
- extraForm: { referrer: "keryx" },
40909
- headers: { "User-Agent": "keryx" }
40910
- };
40911
- GITHUB_COPILOT_DEVICE = {
40912
- deviceAuthorizationEndpoint: "https://github.com/login/device/code",
40913
- tokenEndpoint: "https://github.com/login/oauth/access_token",
40914
- clientId: "Ov23li8tweQw6odWQebz",
40915
- scope: "read:user",
40916
- headers: { Accept: "application/json", "User-Agent": "keryx" },
40917
- afterToken: "github-copilot"
40918
- };
40919
- PROVIDER_AUTH_CATALOG = [
40920
- {
40921
- provider: "grok",
40922
- label: "xAI (Grok)",
40923
- methods: ["device-code", "api-key"],
40924
- device: XAI_GROK_DEVICE
40925
- },
40926
- {
40927
- provider: "openai",
40928
- label: "OpenAI",
40929
- methods: ["oauth-pkce-loopback", "device-code", "api-key"]
40930
- },
40931
- {
40932
- provider: "github-copilot",
40933
- label: "GitHub Copilot",
40934
- methods: ["device-code"],
40935
- device: GITHUB_COPILOT_DEVICE
40936
- },
40937
- { provider: "anthropic", label: "Anthropic", methods: ["api-key"], refusal: "Anthropic does not permit third-party Claude Pro/Max login. Use an API key from the Anthropic Console." },
40938
- { provider: "gemini", label: "Google Gemini", methods: ["api-key"], refusal: "Gemini Google-account login is for Gemini CLI. In keryx use GEMINI_API_KEY from Google AI Studio." },
40939
- { provider: "deepseek", label: "DeepSeek", methods: ["api-key"], refusal: "DeepSeek has no consumer OAuth grant. Use DEEPSEEK_API_KEY." },
40940
- { provider: "openrouter", label: "OpenRouter", methods: ["api-key"] },
40941
- { provider: "zai", label: "Z.AI (GLM)", methods: ["api-key"] },
40942
- { provider: "zai-coding", label: "Z.AI GLM Coding Plan", methods: ["api-key"] },
40943
- { provider: "cerebras", label: "Cerebras", methods: ["api-key"] },
40944
- { provider: "groq", label: "Groq", methods: ["api-key"] },
40945
- { provider: "moonshot", label: "Moonshot (Kimi)", methods: ["api-key"] },
40946
- { provider: "ollama", label: "Ollama", methods: ["none"] },
40947
- { provider: "rapid-mlx", label: "Rapid-MLX (Local)", methods: ["none"] }
40948
- ];
40949
- });
40950
-
40951
40973
  // src/lib/oauth/device-code.ts
40952
40974
  function requestInit(base, signal) {
40953
40975
  return signal === undefined ? base : { ...base, signal };
@@ -41115,38 +41137,71 @@ var init_device_code = __esm(() => {
41115
41137
  });
41116
41138
 
41117
41139
  // src/lib/oauth/copilot.ts
41140
+ function expiresInSecondsFrom(rec, now) {
41141
+ if (typeof rec.expires_at === "string") {
41142
+ const expires = Date.parse(rec.expires_at);
41143
+ if (Number.isFinite(expires)) {
41144
+ return Math.max(1, Math.floor((expires - now) / 1000));
41145
+ }
41146
+ }
41147
+ if (typeof rec.expires_at === "number" && Number.isFinite(rec.expires_at)) {
41148
+ const expiresMs = rec.expires_at < 1000000000000 ? rec.expires_at * 1000 : rec.expires_at;
41149
+ return Math.max(1, Math.floor((expiresMs - now) / 1000));
41150
+ }
41151
+ if (typeof rec.refresh_in === "number" && Number.isFinite(rec.refresh_in) && rec.refresh_in > 0) {
41152
+ return Math.floor(rec.refresh_in);
41153
+ }
41154
+ return;
41155
+ }
41156
+ function describeExchangeFailure(status, json, body) {
41157
+ if (typeof json === "object" && json !== null) {
41158
+ const rec = json;
41159
+ if (rec.error_details?.copilot_access_denied === true) {
41160
+ return `GitHub Copilot token exchange failed (HTTP ${status}): Copilot access denied \u2014 check the GitHub Copilot subscription on this account`;
41161
+ }
41162
+ if (typeof rec.message === "string" && rec.message.length > 0) {
41163
+ return `GitHub Copilot token exchange failed (HTTP ${status}): ${rec.message}`;
41164
+ }
41165
+ }
41166
+ if (/zscaler|blocked because it does not comply/i.test(body)) {
41167
+ return `GitHub Copilot token exchange failed (HTTP ${status}): request was blocked by a gateway or proxy`;
41168
+ }
41169
+ return `GitHub Copilot token exchange failed (HTTP ${status})`;
41170
+ }
41118
41171
  async function exchangeGithubTokenForCopilot(githubToken, http) {
41119
41172
  const init = {
41120
41173
  method: "GET",
41121
41174
  headers: {
41122
- Authorization: `token ${githubToken}`,
41123
- Accept: "application/json",
41124
- "User-Agent": "keryx"
41175
+ ...GITHUB_COPILOT_REQUEST_HEADERS,
41176
+ Authorization: `token ${githubToken}`
41125
41177
  }
41126
41178
  };
41127
41179
  if (http.signal !== undefined) {
41128
41180
  init.signal = http.signal;
41129
41181
  }
41130
41182
  const response = await http.fetch(COPILOT_TOKEN_URL, init);
41131
- const json = await response.json().catch(() => ({}));
41183
+ const body = await response.text().catch(() => "");
41184
+ let json = {};
41185
+ if (body.length > 0) {
41186
+ try {
41187
+ json = JSON.parse(body);
41188
+ } catch {
41189
+ json = {};
41190
+ }
41191
+ }
41132
41192
  if (!response.ok || typeof json !== "object" || json === null) {
41133
- throw new DeviceCodeError("failed", `GitHub Copilot token exchange failed (HTTP ${response.status})`);
41193
+ throw new DeviceCodeError("failed", describeExchangeFailure(response.status, json, body));
41134
41194
  }
41135
41195
  const rec = json;
41136
41196
  if (typeof rec.token !== "string" || rec.token.length === 0) {
41137
41197
  throw new DeviceCodeError("failed", "GitHub Copilot token exchange returned no token");
41138
41198
  }
41139
- let expiresInSeconds;
41140
- if (typeof rec.expires_at === "string") {
41141
- const expires = Date.parse(rec.expires_at);
41142
- if (Number.isFinite(expires)) {
41143
- expiresInSeconds = Math.max(1, Math.floor((expires - Date.now()) / 1000));
41144
- }
41145
- }
41199
+ const expiresInSeconds = expiresInSecondsFrom(rec, Date.now());
41146
41200
  return { accessToken: rec.token, ...expiresInSeconds !== undefined ? { expiresInSeconds } : {} };
41147
41201
  }
41148
41202
  var COPILOT_TOKEN_URL = "https://api.github.com/copilot_internal/v2/token";
41149
41203
  var init_copilot = __esm(() => {
41204
+ init_catalog();
41150
41205
  init_device_code();
41151
41206
  });
41152
41207
 
@@ -48173,7 +48228,7 @@ import path47 from "path";
48173
48228
  // package.json
48174
48229
  var package_default = {
48175
48230
  name: "@mrciphersmith/keryx",
48176
- version: "0.2.99",
48231
+ version: "0.2.101",
48177
48232
  description: "Version-controlled project context for AI coding agents: code graph, architecture wiki, project memory, relevant tests, quality signals, and task flows.",
48178
48233
  private: false,
48179
48234
  publishConfig: {
@@ -73168,7 +73223,7 @@ async function connectStdioMcpServer(argv, options, handshakeTimeoutMs, signal,
73168
73223
  }
73169
73224
 
73170
73225
  // src/mcp-servers/runtime.ts
73171
- import path168 from "path";
73226
+ import path169 from "path";
73172
73227
 
73173
73228
  // src/mcp-servers/manager.ts
73174
73229
  var DEFAULT_CONNECT_CONCURRENCY = 4;
@@ -73356,6 +73411,152 @@ function credentialSummary(raw) {
73356
73411
  return parts.join(" + ");
73357
73412
  }
73358
73413
 
73414
+ // src/mcp-servers/store.ts
73415
+ init_config_dir();
73416
+ import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync7 } from "fs";
73417
+ import path168 from "path";
73418
+ var SERVER_NAME_PATTERN = /^[A-Za-z0-9_-]+$/;
73419
+ function userConfigFile(configDir) {
73420
+ return path168.join(configDir ?? ensureKeryxConfigDir(), "mcp-servers.json");
73421
+ }
73422
+ function projectConfigFile(projectRoot) {
73423
+ return path168.join(projectRoot, ".keryx", "mcp-servers.json");
73424
+ }
73425
+ function overlayFile(configDir) {
73426
+ return path168.join(configDir ?? ensureKeryxConfigDir(), "mcp-servers-disabled.json");
73427
+ }
73428
+ function readForWrite(file) {
73429
+ const read = readConfigFile(file);
73430
+ if (!read.ok) {
73431
+ return isDefiniteAbsence(read.reason) ? { doc: { schemaVersion: 1, servers: Object.create(null) }, existed: false } : { error: `${file} could not be read (${read.reason}); nothing was written` };
73432
+ }
73433
+ let parsed;
73434
+ try {
73435
+ parsed = parseJsonTolerant(read.text);
73436
+ } catch (error) {
73437
+ return {
73438
+ error: `${file} is not valid JSON (${error instanceof Error ? error.message : String(error)}); nothing was written. Fix it by hand first.`
73439
+ };
73440
+ }
73441
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
73442
+ return { error: `${file} is not a JSON object; nothing was written` };
73443
+ }
73444
+ const doc = parsed;
73445
+ const servers = doc.servers;
73446
+ if (servers !== undefined && (typeof servers !== "object" || servers === null || Array.isArray(servers))) {
73447
+ return { error: `${file} has a \`servers\` that is not an object; nothing was written` };
73448
+ }
73449
+ return {
73450
+ doc: {
73451
+ ...parsed,
73452
+ schemaVersion: typeof doc.schemaVersion === "number" ? doc.schemaVersion : 1,
73453
+ servers: Object.assign(Object.create(null), servers)
73454
+ },
73455
+ existed: true
73456
+ };
73457
+ }
73458
+ function writeDocument(file, doc, scope) {
73459
+ const body = `${JSON.stringify(doc, null, 2)}
73460
+ `;
73461
+ if (scope === "user") {
73462
+ writeOwnerOnlyFileAtomic(file, body);
73463
+ return;
73464
+ }
73465
+ mkdirSync6(path168.dirname(file), { recursive: true });
73466
+ writeFileSync7(file, body, { encoding: "utf8" });
73467
+ }
73468
+ function addServer(options) {
73469
+ if (!SERVER_NAME_PATTERN.test(options.name)) {
73470
+ return { ok: false, error: `server name "${options.name}" must match ${SERVER_NAME_PATTERN.source}` };
73471
+ }
73472
+ if (!/^[A-Za-z_]/.test(options.name)) {
73473
+ return {
73474
+ ok: false,
73475
+ error: `server name "${options.name}" must start with a letter or underscore, or none of its tools can be named`
73476
+ };
73477
+ }
73478
+ if (options.name.length > MAX_SERVER_NAME_LENGTH) {
73479
+ return {
73480
+ ok: false,
73481
+ error: `server name "${options.name}" is ${options.name.length} characters; the limit is ${MAX_SERVER_NAME_LENGTH} so that "<server>__<tool>" fits in 64`
73482
+ };
73483
+ }
73484
+ const file = fileForScope(options.scope, options);
73485
+ if (typeof file !== "string")
73486
+ return file;
73487
+ const loaded = readForWrite(file);
73488
+ if ("error" in loaded)
73489
+ return { ok: false, error: loaded.error };
73490
+ if (loaded.doc.servers[options.name] !== undefined && options.force !== true) {
73491
+ return {
73492
+ ok: false,
73493
+ error: `server "${options.name}" already exists in ${file}. Pass --force to replace it, or remove it first.`
73494
+ };
73495
+ }
73496
+ loaded.doc.servers[options.name] = options.entry;
73497
+ writeDocument(file, loaded.doc, options.scope);
73498
+ return { ok: true, file, created: !loaded.existed };
73499
+ }
73500
+ function removeServer(options) {
73501
+ const file = fileForScope(options.scope, options);
73502
+ if (typeof file !== "string")
73503
+ return file;
73504
+ const loaded = readForWrite(file);
73505
+ if ("error" in loaded)
73506
+ return { ok: false, error: loaded.error };
73507
+ if (loaded.doc.servers[options.name] === undefined) {
73508
+ return { ok: false, error: `server "${options.name}" is not defined in ${file}` };
73509
+ }
73510
+ delete loaded.doc.servers[options.name];
73511
+ writeDocument(file, loaded.doc, options.scope);
73512
+ return { ok: true, file, created: false };
73513
+ }
73514
+ function setServerEnabled(options) {
73515
+ const file = overlayFile(options.configDir);
73516
+ const read = readConfigFile(file);
73517
+ let overlay = {};
73518
+ if (read.ok) {
73519
+ try {
73520
+ const parsed = parseJsonTolerant(read.text);
73521
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
73522
+ overlay = parsed;
73523
+ }
73524
+ } catch {
73525
+ overlay = {};
73526
+ }
73527
+ } else if (!isDefiniteAbsence(read.reason)) {
73528
+ return { ok: false, error: `${file} could not be read (${read.reason}); nothing was written` };
73529
+ }
73530
+ const overrides = { ...overlay.overrides, [options.name]: options.enabled };
73531
+ writeOwnerOnlyFileAtomic(file, `${JSON.stringify({ ...overlay, overrides }, null, 2)}
73532
+ `);
73533
+ if (options.source === "user") {
73534
+ clearStickyUserFlag(options.name, options.enabled, options.configDir);
73535
+ }
73536
+ return { ok: true, file, created: !read.ok };
73537
+ }
73538
+ function clearStickyUserFlag(name, enabled, configDir) {
73539
+ const file = userConfigFile(configDir);
73540
+ const loaded = readForWrite(file);
73541
+ if ("error" in loaded)
73542
+ return;
73543
+ const entry = loaded.doc.servers[name];
73544
+ if (entry === undefined)
73545
+ return;
73546
+ if (entry.enabled === undefined || entry.enabled === enabled)
73547
+ return;
73548
+ delete entry.enabled;
73549
+ writeDocument(file, loaded.doc, "user");
73550
+ }
73551
+ function fileForScope(scope, targets) {
73552
+ if (scope === "user")
73553
+ return userConfigFile(targets.configDir);
73554
+ if (targets.projectRoot === undefined) {
73555
+ return { ok: false, error: "--scope project needs a project root; none was resolved" };
73556
+ }
73557
+ return projectConfigFile(targets.projectRoot);
73558
+ }
73559
+
73359
73560
  // src/harness/external/env.ts
73360
73561
  var EXTERNAL_ENV_DENY = [
73361
73562
  "ANTHROPIC_API_KEY",
@@ -73585,11 +73786,32 @@ function createMcpRuntime(options) {
73585
73786
  const kills = [];
73586
73787
  const env = options.env ?? process.env;
73587
73788
  const connect = options.connect ?? ((server) => defaultConnect(server, env, dialling.signal, kills, options.configDir));
73789
+ const closedAlready = new WeakSet;
73790
+ const mutated = new Set;
73588
73791
  const settled = startServers(launchable, connect).then((result) => {
73589
- catalog = result.catalog;
73590
- states = [...heldStates, ...result.servers].sort((a, b) => a.name.localeCompare(b.name));
73792
+ for (const incoming of result.servers) {
73793
+ if (!mutated.has(incoming.name))
73794
+ continue;
73795
+ if (incoming.connection !== undefined && !closedAlready.has(incoming.connection)) {
73796
+ closedAlready.add(incoming.connection);
73797
+ incoming.connection.close().catch(() => {});
73798
+ }
73799
+ }
73800
+ const kept = states.filter((state) => mutated.has(state.name));
73801
+ const incoming = result.servers.filter((state) => !mutated.has(state.name));
73802
+ const heldStill = heldStates.filter((state) => !mutated.has(state.name));
73803
+ states = uniqueByName([...heldStill, ...incoming, ...kept]).sort((a, b) => a.name.localeCompare(b.name));
73804
+ catalog = mergeCatalogs([
73805
+ {
73806
+ entries: result.catalog.entries.filter((entry) => !mutated.has(entry.server)),
73807
+ skipped: result.catalog.skipped.filter((skip) => !mutated.has(skip.server))
73808
+ },
73809
+ {
73810
+ entries: catalog.entries.filter((entry) => mutated.has(entry.server)),
73811
+ skipped: catalog.skipped.filter((skip) => mutated.has(skip.server))
73812
+ }
73813
+ ]);
73591
73814
  }).catch(() => {});
73592
- const closedAlready = new WeakSet;
73593
73815
  const closeOnce = async (list) => {
73594
73816
  const fresh = list.filter((state) => state.connection !== undefined && !closedAlready.has(state.connection));
73595
73817
  for (const state of fresh) {
@@ -73599,6 +73821,7 @@ function createMcpRuntime(options) {
73599
73821
  await closeServers(fresh);
73600
73822
  };
73601
73823
  let closing;
73824
+ const inflight = new Map;
73602
73825
  return {
73603
73826
  catalog: () => catalog,
73604
73827
  servers: () => states,
@@ -73608,8 +73831,100 @@ function createMcpRuntime(options) {
73608
73831
  ready: async () => {
73609
73832
  await settled;
73610
73833
  },
73834
+ connectServer: (name) => runExclusive(name, () => connectOne(name)),
73835
+ disconnectServer: (name) => runExclusive(name, () => disconnectOne(name)),
73611
73836
  close: () => closing ??= closeNow()
73612
73837
  };
73838
+ function runExclusive(name, work) {
73839
+ const existing = inflight.get(name);
73840
+ if (existing !== undefined)
73841
+ return existing;
73842
+ const pending = work().finally(() => {
73843
+ inflight.delete(name);
73844
+ });
73845
+ inflight.set(name, pending);
73846
+ return pending;
73847
+ }
73848
+ function replaceState(next) {
73849
+ states = uniqueByName([...states.filter((state) => state.name !== next.name), next]).sort((a, b) => a.name.localeCompare(b.name));
73850
+ }
73851
+ function stripCatalog(name) {
73852
+ catalog = {
73853
+ entries: catalog.entries.filter((entry) => entry.server !== name),
73854
+ skipped: catalog.skipped.filter((skip) => skip.server !== name)
73855
+ };
73856
+ }
73857
+ async function persistEnabled(server, enabled) {
73858
+ const written = setServerEnabled({
73859
+ name: server.name,
73860
+ enabled,
73861
+ source: server.source,
73862
+ ...options.configDir === undefined ? {} : { configDir: options.configDir }
73863
+ });
73864
+ return written.ok ? undefined : { ok: false, message: written.error };
73865
+ }
73866
+ async function connectOne(name) {
73867
+ if (closing !== undefined)
73868
+ return { ok: false, message: "session is closing" };
73869
+ const server = byName.get(name);
73870
+ if (server === undefined)
73871
+ return { ok: false, message: `no server named "${name}"` };
73872
+ if (requiresApproval(server, loadTrustStore(options.configDir))) {
73873
+ return { ok: false, message: `project server not approved; run \`keryx mcp trust ${name}\`` };
73874
+ }
73875
+ const current = states.find((state) => state.name === name);
73876
+ if (current?.status === "connected")
73877
+ return { ok: true };
73878
+ if (current?.status === "connecting") {
73879
+ return { ok: false, message: `server "${name}" is already connecting` };
73880
+ }
73881
+ const persist = await persistEnabled(server, true);
73882
+ if (persist !== undefined)
73883
+ return persist;
73884
+ mutated.add(name);
73885
+ if (current?.connection !== undefined) {
73886
+ if (!closedAlready.has(current.connection))
73887
+ closedAlready.add(current.connection);
73888
+ await closeServers([current]);
73889
+ }
73890
+ stripCatalog(name);
73891
+ replaceState({ name, status: "connecting", toolCount: 0 });
73892
+ const result = await startServers([{ ...server, enabled: true }], connect);
73893
+ const next = result.servers[0];
73894
+ if (next === undefined) {
73895
+ replaceState({ name, status: "failed", error: "dial returned nothing", toolCount: 0 });
73896
+ return { ok: false, message: "dial returned nothing" };
73897
+ }
73898
+ replaceState(next);
73899
+ if (next.status === "connected") {
73900
+ catalog = mergeCatalogs([catalog, result.catalog]);
73901
+ return { ok: true };
73902
+ }
73903
+ return { ok: false, message: next.error ?? `server "${name}" did not connect` };
73904
+ }
73905
+ async function disconnectOne(name) {
73906
+ if (closing !== undefined)
73907
+ return { ok: false, message: "session is closing" };
73908
+ const server = byName.get(name);
73909
+ if (server === undefined)
73910
+ return { ok: false, message: `no server named "${name}"` };
73911
+ const current = states.find((state) => state.name === name);
73912
+ if (current?.status === "connecting") {
73913
+ return { ok: false, message: `server "${name}" is still connecting` };
73914
+ }
73915
+ const persist = await persistEnabled(server, false);
73916
+ if (persist !== undefined)
73917
+ return persist;
73918
+ mutated.add(name);
73919
+ if (current?.connection !== undefined) {
73920
+ if (!closedAlready.has(current.connection))
73921
+ closedAlready.add(current.connection);
73922
+ await closeServers([current]);
73923
+ }
73924
+ stripCatalog(name);
73925
+ replaceState({ name, status: "disabled", toolCount: 0 });
73926
+ return { ok: true };
73927
+ }
73613
73928
  async function closeNow() {
73614
73929
  {
73615
73930
  dialling.abort();
@@ -73620,6 +73935,17 @@ function createMcpRuntime(options) {
73620
73935
  }
73621
73936
  }
73622
73937
  }
73938
+ function uniqueByName(list) {
73939
+ const seen = new Set;
73940
+ const out = [];
73941
+ for (const state of list) {
73942
+ if (seen.has(state.name))
73943
+ continue;
73944
+ seen.add(state.name);
73945
+ out.push(state);
73946
+ }
73947
+ return out;
73948
+ }
73623
73949
  function handshakeBudgetMs(server) {
73624
73950
  const configured = server.startup_timeout_sec;
73625
73951
  if (configured === undefined || !Number.isFinite(configured) || configured <= 0) {
@@ -73685,153 +74011,7 @@ async function defaultConnect(server, env, signal, kills, configDir) {
73685
74011
  function defaultServerCwd(server, fallbackCwd = process.cwd()) {
73686
74012
  if (server.source !== "project")
73687
74013
  return fallbackCwd;
73688
- return path168.dirname(path168.dirname(server.file));
73689
- }
73690
-
73691
- // src/mcp-servers/store.ts
73692
- init_config_dir();
73693
- import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync7 } from "fs";
73694
- import path169 from "path";
73695
- var SERVER_NAME_PATTERN = /^[A-Za-z0-9_-]+$/;
73696
- function userConfigFile(configDir) {
73697
- return path169.join(configDir ?? ensureKeryxConfigDir(), "mcp-servers.json");
73698
- }
73699
- function projectConfigFile(projectRoot) {
73700
- return path169.join(projectRoot, ".keryx", "mcp-servers.json");
73701
- }
73702
- function overlayFile(configDir) {
73703
- return path169.join(configDir ?? ensureKeryxConfigDir(), "mcp-servers-disabled.json");
73704
- }
73705
- function readForWrite(file) {
73706
- const read = readConfigFile(file);
73707
- if (!read.ok) {
73708
- return isDefiniteAbsence(read.reason) ? { doc: { schemaVersion: 1, servers: Object.create(null) }, existed: false } : { error: `${file} could not be read (${read.reason}); nothing was written` };
73709
- }
73710
- let parsed;
73711
- try {
73712
- parsed = parseJsonTolerant(read.text);
73713
- } catch (error) {
73714
- return {
73715
- error: `${file} is not valid JSON (${error instanceof Error ? error.message : String(error)}); nothing was written. Fix it by hand first.`
73716
- };
73717
- }
73718
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
73719
- return { error: `${file} is not a JSON object; nothing was written` };
73720
- }
73721
- const doc = parsed;
73722
- const servers = doc.servers;
73723
- if (servers !== undefined && (typeof servers !== "object" || servers === null || Array.isArray(servers))) {
73724
- return { error: `${file} has a \`servers\` that is not an object; nothing was written` };
73725
- }
73726
- return {
73727
- doc: {
73728
- ...parsed,
73729
- schemaVersion: typeof doc.schemaVersion === "number" ? doc.schemaVersion : 1,
73730
- servers: Object.assign(Object.create(null), servers)
73731
- },
73732
- existed: true
73733
- };
73734
- }
73735
- function writeDocument(file, doc, scope) {
73736
- const body = `${JSON.stringify(doc, null, 2)}
73737
- `;
73738
- if (scope === "user") {
73739
- writeOwnerOnlyFileAtomic(file, body);
73740
- return;
73741
- }
73742
- mkdirSync6(path169.dirname(file), { recursive: true });
73743
- writeFileSync7(file, body, { encoding: "utf8" });
73744
- }
73745
- function addServer(options) {
73746
- if (!SERVER_NAME_PATTERN.test(options.name)) {
73747
- return { ok: false, error: `server name "${options.name}" must match ${SERVER_NAME_PATTERN.source}` };
73748
- }
73749
- if (!/^[A-Za-z_]/.test(options.name)) {
73750
- return {
73751
- ok: false,
73752
- error: `server name "${options.name}" must start with a letter or underscore, or none of its tools can be named`
73753
- };
73754
- }
73755
- if (options.name.length > MAX_SERVER_NAME_LENGTH) {
73756
- return {
73757
- ok: false,
73758
- error: `server name "${options.name}" is ${options.name.length} characters; the limit is ${MAX_SERVER_NAME_LENGTH} so that "<server>__<tool>" fits in 64`
73759
- };
73760
- }
73761
- const file = fileForScope(options.scope, options);
73762
- if (typeof file !== "string")
73763
- return file;
73764
- const loaded = readForWrite(file);
73765
- if ("error" in loaded)
73766
- return { ok: false, error: loaded.error };
73767
- if (loaded.doc.servers[options.name] !== undefined && options.force !== true) {
73768
- return {
73769
- ok: false,
73770
- error: `server "${options.name}" already exists in ${file}. Pass --force to replace it, or remove it first.`
73771
- };
73772
- }
73773
- loaded.doc.servers[options.name] = options.entry;
73774
- writeDocument(file, loaded.doc, options.scope);
73775
- return { ok: true, file, created: !loaded.existed };
73776
- }
73777
- function removeServer(options) {
73778
- const file = fileForScope(options.scope, options);
73779
- if (typeof file !== "string")
73780
- return file;
73781
- const loaded = readForWrite(file);
73782
- if ("error" in loaded)
73783
- return { ok: false, error: loaded.error };
73784
- if (loaded.doc.servers[options.name] === undefined) {
73785
- return { ok: false, error: `server "${options.name}" is not defined in ${file}` };
73786
- }
73787
- delete loaded.doc.servers[options.name];
73788
- writeDocument(file, loaded.doc, options.scope);
73789
- return { ok: true, file, created: false };
73790
- }
73791
- function setServerEnabled(options) {
73792
- const file = overlayFile(options.configDir);
73793
- const read = readConfigFile(file);
73794
- let overlay = {};
73795
- if (read.ok) {
73796
- try {
73797
- const parsed = parseJsonTolerant(read.text);
73798
- if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
73799
- overlay = parsed;
73800
- }
73801
- } catch {
73802
- overlay = {};
73803
- }
73804
- } else if (!isDefiniteAbsence(read.reason)) {
73805
- return { ok: false, error: `${file} could not be read (${read.reason}); nothing was written` };
73806
- }
73807
- const overrides = { ...overlay.overrides, [options.name]: options.enabled };
73808
- writeOwnerOnlyFileAtomic(file, `${JSON.stringify({ ...overlay, overrides }, null, 2)}
73809
- `);
73810
- if (options.source === "user") {
73811
- clearStickyUserFlag(options.name, options.enabled, options.configDir);
73812
- }
73813
- return { ok: true, file, created: !read.ok };
73814
- }
73815
- function clearStickyUserFlag(name, enabled, configDir) {
73816
- const file = userConfigFile(configDir);
73817
- const loaded = readForWrite(file);
73818
- if ("error" in loaded)
73819
- return;
73820
- const entry = loaded.doc.servers[name];
73821
- if (entry === undefined)
73822
- return;
73823
- if (entry.enabled === undefined || entry.enabled === enabled)
73824
- return;
73825
- delete entry.enabled;
73826
- writeDocument(file, loaded.doc, "user");
73827
- }
73828
- function fileForScope(scope, targets) {
73829
- if (scope === "user")
73830
- return userConfigFile(targets.configDir);
73831
- if (targets.projectRoot === undefined) {
73832
- return { ok: false, error: "--scope project needs a project root; none was resolved" };
73833
- }
73834
- return projectConfigFile(targets.projectRoot);
74014
+ return path169.dirname(path169.dirname(server.file));
73835
74015
  }
73836
74016
 
73837
74017
  // src/commands/mcp-servers.ts
@@ -88242,36 +88422,257 @@ function buildConsumerModel(input) {
88242
88422
  project: ${input.projectFile}`
88243
88423
  };
88244
88424
  }
88245
- function renderConsumerLines(model) {
88425
+ var MCP_CONSUMER_FOOTER = [
88426
+ { key: "\u2191/\u2193", label: "select" },
88427
+ { key: "click", label: "row: select/act" },
88428
+ { key: "c/d", label: "connect/disconnect" },
88429
+ { key: "y", label: "confirm" },
88430
+ { key: "esc", label: "close" }
88431
+ ];
88432
+ var MCP_CONSUMER_HEADER = "Servers keryx CONNECTS TO. c/d then y to connect or disconnect. Held project servers need `keryx mcp trust` first.";
88433
+ var STATUS_GLYPH = {
88434
+ connected: "\u25CF connected",
88435
+ connecting: "\u2026 connecting",
88436
+ failed: "\u2717 failed",
88437
+ disabled: "\u25CB disabled",
88438
+ "needs-approval": "! needs approval",
88439
+ needs_auth: "! needs auth"
88440
+ };
88441
+ function consumerActionFor(status) {
88442
+ if (status === "connected")
88443
+ return "disconnect";
88444
+ if (status === "disabled" || status === "failed")
88445
+ return "connect";
88446
+ return;
88447
+ }
88448
+ function statusGlyph(status) {
88449
+ return STATUS_GLYPH[status] ?? status;
88450
+ }
88451
+ function formatConsumerModalRow(row, isSelected, status) {
88452
+ const mark = isSelected ? ">" : " ";
88453
+ const name = row.name.padEnd(18);
88454
+ const source = row.source.padEnd(10);
88455
+ const transport = row.transport.padEnd(6);
88456
+ const glyph = statusGlyph(row.status).padEnd(18);
88457
+ const tools = row.status === "connected" ? `${row.toolCount} tool(s)` : "";
88458
+ let action;
88459
+ const offered = consumerActionFor(row.status);
88460
+ if (status.kind === "armed" && status.target.name === row.name) {
88461
+ action = ` [click again or press y to ${status.target.action}]`;
88462
+ } else if (status.kind === "running" && status.target.name === row.name) {
88463
+ action = ` ${status.target.action === "connect" ? "connecting\u2026" : "disconnecting\u2026"}`;
88464
+ } else if (status.kind === "done" && status.target.name === row.name) {
88465
+ action = status.outcome.ok ? " \u2713 done" : ` \u2717 ${status.outcome.message}`;
88466
+ } else if (offered === "disconnect") {
88467
+ action = " [d] disconnect";
88468
+ } else if (offered === "connect") {
88469
+ action = " [c] connect";
88470
+ } else if (row.action !== undefined) {
88471
+ action = ` \u2192 ${row.action}`;
88472
+ } else {
88473
+ action = "";
88474
+ }
88475
+ const creds = row.credentials.length > 0 ? ` [reads ${row.credentials.join(", ")}]` : "";
88476
+ return `${mark} ${name} ${source} ${transport} ${glyph}${tools}${action} ${row.target}${creds}`.trimEnd();
88477
+ }
88478
+ function formatConsumerModalLines(model, selected, status) {
88246
88479
  if (model === undefined) {
88247
88480
  return [
88248
88481
  "No MCP session yet \u2014 this shell has not built a tool list.",
88249
88482
  "Configured servers are listed by `keryx mcp list`; `keryx mcp doctor` dials them."
88250
88483
  ];
88251
88484
  }
88485
+ const header = [MCP_CONSUMER_HEADER];
88486
+ const problems = model.problems.flatMap((problem) => problem.split(`
88487
+ `).map((line, index) => index === 0 ? `config problem \u2014 ${line}` : ` ${line}`));
88488
+ if (model.emptyHint !== undefined) {
88489
+ return [...header, ...problems, ...model.emptyHint.split(`
88490
+ `)];
88491
+ }
88252
88492
  return [
88253
- ...model.problems.flatMap((problem) => problem.split(`
88254
- `).map((line, index) => index === 0 ? `config problem \u2014 ${line}` : ` ${line}`)),
88255
- ...model.emptyHint === undefined ? [] : model.emptyHint.split(`
88256
- `),
88257
- ...model.rows.flatMap((row) => {
88258
- const lines = [formatConsumerRow(row)];
88259
- if (row.detail !== undefined) {
88260
- for (const line of row.detail.split(`
88261
- `))
88262
- lines.push(` ${line}`);
88263
- }
88264
- if (row.action !== undefined)
88265
- lines.push(` \u2192 ${row.action}`);
88266
- return lines;
88267
- })
88493
+ ...header,
88494
+ ...problems,
88495
+ ...model.rows.map((row, index) => formatConsumerModalRow(row, index === selected, status))
88268
88496
  ];
88269
88497
  }
88270
- function formatConsumerRow(row) {
88271
- const tags = [row.source, row.transport, row.status].join(" ");
88272
- const tools = row.status === "connected" ? ` \u2014 ${row.toolCount} tool(s)` : "";
88273
- const creds = row.credentials.length > 0 ? ` [reads ${row.credentials.join(", ")}]` : "";
88274
- return `${row.name} (${tags})${tools} ${row.target}${creds}`;
88498
+ function asRowTarget2(body) {
88499
+ const parent = body;
88500
+ if (parent.add === undefined || parent.getChildren === undefined || parent.remove === undefined) {
88501
+ return;
88502
+ }
88503
+ return {
88504
+ add: parent.add.bind(parent),
88505
+ getChildren: parent.getChildren.bind(parent),
88506
+ remove: parent.remove.bind(parent)
88507
+ };
88508
+ }
88509
+ function presentMcpConsumer(openModalFn, otui, chrome, options) {
88510
+ let selected = 0;
88511
+ let scroll = 0;
88512
+ let status = { kind: "idle" };
88513
+ let body;
88514
+ let rowCtor;
88515
+ let activeRenderer;
88516
+ let unsubscribeKey;
88517
+ const rendererHint = options.renderer ?? chrome?.renderer;
88518
+ const bodyRows = options.visibleRows ?? (typeof rendererHint?.width === "number" && typeof rendererHint.height === "number" ? modalBodyRows(resolveModalPanelSize(rendererHint.width, rendererHint.height).height) : 13);
88519
+ const rowsOf = () => options.snapshot()?.rows ?? [];
88520
+ const paint = () => {
88521
+ if (body === undefined || rowCtor === undefined)
88522
+ return;
88523
+ clearTranscriptChildren(body);
88524
+ const model = options.snapshot();
88525
+ const rows = model?.rows ?? [];
88526
+ selected = rows.length === 0 ? 0 : Math.min(selected, rows.length - 1);
88527
+ scroll = scrollToReveal3(selected, scroll, bodyRows);
88528
+ scroll = clampScroll3(scroll, Math.max(rows.length, 1), bodyRows);
88529
+ const lines = formatConsumerModalLines(model, selected, status);
88530
+ if (rows.length === 0) {
88531
+ for (const [i, line] of lines.entries()) {
88532
+ body.add(new rowCtor(activeRenderer, { id: `mcp-consumer-line-${i}`, content: line }));
88533
+ }
88534
+ return;
88535
+ }
88536
+ body.add(new rowCtor(activeRenderer, { id: "mcp-consumer-header", content: MCP_CONSUMER_HEADER }));
88537
+ const problems = model?.problems ?? [];
88538
+ for (const [i, problem] of problems.entries()) {
88539
+ body.add(new rowCtor(activeRenderer, {
88540
+ id: `mcp-consumer-problem-${i}`,
88541
+ content: `config problem \u2014 ${problem}`
88542
+ }));
88543
+ }
88544
+ const start = clampScroll3(scroll, rows.length, bodyRows);
88545
+ for (const [i, row] of rows.slice(start, start + bodyRows).entries()) {
88546
+ const index = start + i;
88547
+ body.add(new rowCtor(activeRenderer, {
88548
+ id: `mcp-consumer-row-${row.name}`,
88549
+ content: formatConsumerModalRow(row, index === selected, status),
88550
+ onMouseDown: () => handleRowClick(row.name, index)
88551
+ }));
88552
+ }
88553
+ };
88554
+ const moveSelection = (next) => {
88555
+ const rows = rowsOf();
88556
+ if (rows.length === 0)
88557
+ return;
88558
+ const clamped = Math.min(rows.length - 1, Math.max(0, next));
88559
+ if (clamped === selected)
88560
+ return;
88561
+ selected = clamped;
88562
+ status = { kind: "idle" };
88563
+ paint();
88564
+ };
88565
+ const runAction = () => {
88566
+ if (status.kind !== "armed")
88567
+ return;
88568
+ const target = status.target;
88569
+ status = { kind: "running", target };
88570
+ paint();
88571
+ const fn = target.action === "connect" ? options.connect : options.disconnect;
88572
+ fn(target.name).then((outcome) => {
88573
+ status = { kind: "done", target, outcome };
88574
+ paint();
88575
+ });
88576
+ };
88577
+ const armFor = (name) => {
88578
+ const rows = rowsOf();
88579
+ const index = rows.findIndex((row) => row.name === name);
88580
+ const row = rows[index];
88581
+ const offered = row === undefined ? undefined : consumerActionFor(row.status);
88582
+ if (index < 0 || row === undefined || offered === undefined || status.kind === "running")
88583
+ return;
88584
+ selected = index;
88585
+ status = { kind: "armed", target: { name: row.name, action: offered } };
88586
+ paint();
88587
+ };
88588
+ const handleRowClick = (name, index) => {
88589
+ if (status.kind === "armed" && status.target.name === name) {
88590
+ runAction();
88591
+ return;
88592
+ }
88593
+ if (status.kind === "running")
88594
+ return;
88595
+ const row = rowsOf()[index];
88596
+ if (row === undefined || consumerActionFor(row.status) === undefined) {
88597
+ if (index !== selected) {
88598
+ selected = index;
88599
+ status = { kind: "idle" };
88600
+ paint();
88601
+ }
88602
+ return;
88603
+ }
88604
+ armFor(name);
88605
+ };
88606
+ const handle = openModalFn(otui, chrome, {
88607
+ title: "MCP servers",
88608
+ tabs: [{ id: "servers", label: "Servers" }],
88609
+ initialTab: "servers",
88610
+ footer: MCP_CONSUMER_FOOTER,
88611
+ renderTab: (_tabId, tabBody, ctx) => {
88612
+ const renderer = options.renderer ?? chrome?.renderer;
88613
+ const ctor = otui.TextRenderable;
88614
+ const target = asRowTarget2(tabBody);
88615
+ if (target === undefined || ctor === undefined)
88616
+ return;
88617
+ rowCtor = ctor;
88618
+ activeRenderer = renderer;
88619
+ body = target;
88620
+ paint();
88621
+ },
88622
+ onClose: () => {
88623
+ unsubscribeKey?.();
88624
+ }
88625
+ });
88626
+ if (handle === undefined)
88627
+ return;
88628
+ if (options.onKeypress !== undefined) {
88629
+ unsubscribeKey = options.onKeypress((key) => {
88630
+ const token = key.name || key.sequence;
88631
+ const rows = rowsOf();
88632
+ if (status.kind === "armed") {
88633
+ if (token === "y") {
88634
+ runAction();
88635
+ } else {
88636
+ status = { kind: "idle" };
88637
+ paint();
88638
+ }
88639
+ return;
88640
+ }
88641
+ if (token === "c") {
88642
+ const row = rows[selected];
88643
+ if (row !== undefined && consumerActionFor(row.status) === "connect" && status.kind !== "running") {
88644
+ status = { kind: "armed", target: { name: row.name, action: "connect" } };
88645
+ paint();
88646
+ }
88647
+ return;
88648
+ }
88649
+ if (token === "d") {
88650
+ const row = rows[selected];
88651
+ if (row !== undefined && consumerActionFor(row.status) === "disconnect" && status.kind !== "running") {
88652
+ status = { kind: "armed", target: { name: row.name, action: "disconnect" } };
88653
+ paint();
88654
+ }
88655
+ return;
88656
+ }
88657
+ if (token === "up" || token === "k") {
88658
+ moveSelection(selected - 1);
88659
+ return;
88660
+ }
88661
+ if (token === "down" || token === "j") {
88662
+ moveSelection(selected + 1);
88663
+ return;
88664
+ }
88665
+ if (token === "pageup" || token === "pagedown") {
88666
+ const step = token === "pageup" ? -bodyRows : bodyRows;
88667
+ scroll = clampScroll3(scroll + step, Math.max(rows.length, 1), bodyRows);
88668
+ paint();
88669
+ }
88670
+ });
88671
+ }
88672
+ return handle;
88673
+ }
88674
+ function openMcpConsumer(otui, chrome, options) {
88675
+ return presentMcpConsumer((hostOtui, hostChrome, input) => openModal(hostOtui, hostChrome, input), otui, chrome, options);
88275
88676
  }
88276
88677
 
88277
88678
  // src/tui/tui-shell.ts
@@ -89048,7 +89449,7 @@ var AGENT_SLASH_COMMANDS = [
89048
89449
  },
89049
89450
  {
89050
89451
  name: "/mcp",
89051
- description: "MCP servers keryx is connected to \u2014 status, tools, what needs trust",
89452
+ description: "MCP servers keryx is connected to \u2014 status, connect/disconnect",
89052
89453
  modes: AGENT_ONLY
89053
89454
  },
89054
89455
  {
@@ -91817,7 +92218,7 @@ function stepQueueNavAction(selected, direction) {
91817
92218
  }
91818
92219
 
91819
92220
  // src/tui/worker-fleet.ts
91820
- var STATUS_GLYPH = {
92221
+ var STATUS_GLYPH2 = {
91821
92222
  queued: "\u25CB",
91822
92223
  running: "\u25D0",
91823
92224
  done: "\u25CF",
@@ -91956,7 +92357,7 @@ function formatFleetSidebar(workers, maxLines = 14) {
91956
92357
  const room = Math.max(2, maxLines - lines.length);
91957
92358
  const shown = sorted.slice(0, room);
91958
92359
  for (const w of shown) {
91959
- const g = STATUS_GLYPH[w.status];
92360
+ const g = STATUS_GLYPH2[w.status];
91960
92361
  const phase = humanFleetPhase(w.status, w.detail);
91961
92362
  const label = clip4(w.label, 12);
91962
92363
  const det = phase.length > 0 ? ` ${clip4(phase, 12)}` : "";
@@ -92050,7 +92451,7 @@ class WorkerFleet {
92050
92451
  var MAX_SUBAGENT_EVENTS = 200;
92051
92452
  var MAX_SUBAGENT_EVENT_CHARS = 2000;
92052
92453
  var EXTERNAL_SUBAGENT_MARK = "\u2933";
92053
- var STATUS_GLYPH2 = {
92454
+ var STATUS_GLYPH3 = {
92054
92455
  queued: "\u25CB",
92055
92456
  running: "\u25D0",
92056
92457
  done: "\u25CF",
@@ -92092,7 +92493,7 @@ function formatElapsed(ms) {
92092
92493
  return minRem === 0 ? `${hours}h` : `${hours}h ${minRem}m`;
92093
92494
  }
92094
92495
  function formatSubagentRow(session, width = SIDEBAR_TEXT_WIDTH) {
92095
- const glyph = STATUS_GLYPH2[session.status];
92496
+ const glyph = STATUS_GLYPH3[session.status];
92096
92497
  const phase = humanFleetPhase(session.status, session.detail);
92097
92498
  const mark = session.runtime === "external" ? EXTERNAL_SUBAGENT_MARK : "";
92098
92499
  const budget = Math.max(4, width - glyph.length - 1);
@@ -93451,7 +93852,7 @@ function openExternalInspector(otui, chrome, options) {
93451
93852
 
93452
93853
  // src/tui/background-job-session.ts
93453
93854
  var MAX_BACKGROUND_JOB_OUTPUT_CHARS = 20000;
93454
- var STATUS_GLYPH3 = {
93855
+ var STATUS_GLYPH4 = {
93455
93856
  running: "\u25D0",
93456
93857
  exited: "\u25CF",
93457
93858
  killed: "\u2717"
@@ -93469,7 +93870,7 @@ function formatJobListHeader(count) {
93469
93870
  return `Background Jobs ${count}`;
93470
93871
  }
93471
93872
  function formatJobRow(entry, width = SIDEBAR_TEXT_WIDTH) {
93472
- const glyph = STATUS_GLYPH3[entry.status];
93873
+ const glyph = STATUS_GLYPH4[entry.status];
93473
93874
  const budget = Math.max(1, width - glyph.length - 1);
93474
93875
  const cmd = clip7(entry.command, budget);
93475
93876
  return clip7(`${glyph} ${cmd}`, width);
@@ -96059,19 +96460,36 @@ Staying in the current session.
96059
96460
  })();
96060
96461
  };
96061
96462
  const showMcpConsumer = () => {
96062
- const runtime = deps.mcpRuntime?.();
96063
96463
  const cwd = inspectorCwd();
96064
- const model = runtime === undefined ? undefined : buildConsumerModel({
96065
- configured: runtime.configured(),
96066
- states: runtime.servers(),
96067
- problems: runtime.problems(),
96068
- userFile: userConfigFile(),
96069
- projectFile: projectConfigFile(cwd)
96464
+ const snapshot = () => {
96465
+ const live = deps.mcpRuntime?.();
96466
+ if (live === undefined)
96467
+ return;
96468
+ return buildConsumerModel({
96469
+ configured: live.configured(),
96470
+ states: live.servers(),
96471
+ problems: live.problems(),
96472
+ userFile: userConfigFile(),
96473
+ projectFile: projectConfigFile(cwd)
96474
+ });
96475
+ };
96476
+ openMcpConsumer(otui, chrome, {
96477
+ snapshot,
96478
+ connect: async (name) => {
96479
+ const live = deps.mcpRuntime?.();
96480
+ if (live === undefined)
96481
+ return { ok: false, message: "No MCP session yet" };
96482
+ return live.connectServer(name);
96483
+ },
96484
+ disconnect: async (name) => {
96485
+ const live = deps.mcpRuntime?.();
96486
+ if (live === undefined)
96487
+ return { ok: false, message: "No MCP session yet" };
96488
+ return live.disconnectServer(name);
96489
+ },
96490
+ renderer: r,
96491
+ ...inspectorKeys
96070
96492
  });
96071
- const lines = renderConsumerLines(model);
96072
- for (const line of lines) {
96073
- transcript.add(new otui.TextRenderable(r, { id: `mcp${uid++}`, content: otui.t`${otui.dim(line)}` }));
96074
- }
96075
96493
  };
96076
96494
  const showTools = () => {
96077
96495
  (async () => {
@@ -103099,7 +103517,7 @@ function monitorCommand(args) {
103099
103517
  }
103100
103518
  renderAgentsTree(snapshot);
103101
103519
  }
103102
- var STATUS_GLYPH4 = {
103520
+ var STATUS_GLYPH5 = {
103103
103521
  running: "\u25D0",
103104
103522
  done: "\u25CF",
103105
103523
  blocked: "\u25FC",
@@ -103112,7 +103530,7 @@ function renderAgentsTree(snapshot) {
103112
103530
  console.log(`run ${snapshot.runId ?? "(unknown)"} \u2014 ${snapshot.agents.length} subagent(s)`);
103113
103531
  for (const agent of snapshot.agents) {
103114
103532
  const tokens = `\u2191${agent.usage.inputTokens} \u2193${agent.usage.outputTokens}${agent.usage.exact ? "" : "~"}`;
103115
- const glyph = STATUS_GLYPH4[agent.status] ?? "\u25CB";
103533
+ const glyph = STATUS_GLYPH5[agent.status] ?? "\u25CB";
103116
103534
  console.log(` ${glyph} ${agent.dispatchId} ${agent.status} ${agent.model ?? "-"} ${tokens}`);
103117
103535
  }
103118
103536
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrciphersmith/keryx",
3
- "version": "0.2.99",
3
+ "version": "0.2.101",
4
4
  "description": "Version-controlled project context for AI coding agents: code graph, architecture wiki, project memory, relevant tests, quality signals, and task flows.",
5
5
  "private": false,
6
6
  "publishConfig": {