@mrciphersmith/keryx 0.2.99 → 0.2.100

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 +556 -193
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -48173,7 +48173,7 @@ import path47 from "path";
48173
48173
  // package.json
48174
48174
  var package_default = {
48175
48175
  name: "@mrciphersmith/keryx",
48176
- version: "0.2.99",
48176
+ version: "0.2.100",
48177
48177
  description: "Version-controlled project context for AI coding agents: code graph, architecture wiki, project memory, relevant tests, quality signals, and task flows.",
48178
48178
  private: false,
48179
48179
  publishConfig: {
@@ -73168,7 +73168,7 @@ async function connectStdioMcpServer(argv, options, handshakeTimeoutMs, signal,
73168
73168
  }
73169
73169
 
73170
73170
  // src/mcp-servers/runtime.ts
73171
- import path168 from "path";
73171
+ import path169 from "path";
73172
73172
 
73173
73173
  // src/mcp-servers/manager.ts
73174
73174
  var DEFAULT_CONNECT_CONCURRENCY = 4;
@@ -73356,6 +73356,152 @@ function credentialSummary(raw) {
73356
73356
  return parts.join(" + ");
73357
73357
  }
73358
73358
 
73359
+ // src/mcp-servers/store.ts
73360
+ init_config_dir();
73361
+ import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync7 } from "fs";
73362
+ import path168 from "path";
73363
+ var SERVER_NAME_PATTERN = /^[A-Za-z0-9_-]+$/;
73364
+ function userConfigFile(configDir) {
73365
+ return path168.join(configDir ?? ensureKeryxConfigDir(), "mcp-servers.json");
73366
+ }
73367
+ function projectConfigFile(projectRoot) {
73368
+ return path168.join(projectRoot, ".keryx", "mcp-servers.json");
73369
+ }
73370
+ function overlayFile(configDir) {
73371
+ return path168.join(configDir ?? ensureKeryxConfigDir(), "mcp-servers-disabled.json");
73372
+ }
73373
+ function readForWrite(file) {
73374
+ const read = readConfigFile(file);
73375
+ if (!read.ok) {
73376
+ return isDefiniteAbsence(read.reason) ? { doc: { schemaVersion: 1, servers: Object.create(null) }, existed: false } : { error: `${file} could not be read (${read.reason}); nothing was written` };
73377
+ }
73378
+ let parsed;
73379
+ try {
73380
+ parsed = parseJsonTolerant(read.text);
73381
+ } catch (error) {
73382
+ return {
73383
+ error: `${file} is not valid JSON (${error instanceof Error ? error.message : String(error)}); nothing was written. Fix it by hand first.`
73384
+ };
73385
+ }
73386
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
73387
+ return { error: `${file} is not a JSON object; nothing was written` };
73388
+ }
73389
+ const doc = parsed;
73390
+ const servers = doc.servers;
73391
+ if (servers !== undefined && (typeof servers !== "object" || servers === null || Array.isArray(servers))) {
73392
+ return { error: `${file} has a \`servers\` that is not an object; nothing was written` };
73393
+ }
73394
+ return {
73395
+ doc: {
73396
+ ...parsed,
73397
+ schemaVersion: typeof doc.schemaVersion === "number" ? doc.schemaVersion : 1,
73398
+ servers: Object.assign(Object.create(null), servers)
73399
+ },
73400
+ existed: true
73401
+ };
73402
+ }
73403
+ function writeDocument(file, doc, scope) {
73404
+ const body = `${JSON.stringify(doc, null, 2)}
73405
+ `;
73406
+ if (scope === "user") {
73407
+ writeOwnerOnlyFileAtomic(file, body);
73408
+ return;
73409
+ }
73410
+ mkdirSync6(path168.dirname(file), { recursive: true });
73411
+ writeFileSync7(file, body, { encoding: "utf8" });
73412
+ }
73413
+ function addServer(options) {
73414
+ if (!SERVER_NAME_PATTERN.test(options.name)) {
73415
+ return { ok: false, error: `server name "${options.name}" must match ${SERVER_NAME_PATTERN.source}` };
73416
+ }
73417
+ if (!/^[A-Za-z_]/.test(options.name)) {
73418
+ return {
73419
+ ok: false,
73420
+ error: `server name "${options.name}" must start with a letter or underscore, or none of its tools can be named`
73421
+ };
73422
+ }
73423
+ if (options.name.length > MAX_SERVER_NAME_LENGTH) {
73424
+ return {
73425
+ ok: false,
73426
+ error: `server name "${options.name}" is ${options.name.length} characters; the limit is ${MAX_SERVER_NAME_LENGTH} so that "<server>__<tool>" fits in 64`
73427
+ };
73428
+ }
73429
+ const file = fileForScope(options.scope, options);
73430
+ if (typeof file !== "string")
73431
+ return file;
73432
+ const loaded = readForWrite(file);
73433
+ if ("error" in loaded)
73434
+ return { ok: false, error: loaded.error };
73435
+ if (loaded.doc.servers[options.name] !== undefined && options.force !== true) {
73436
+ return {
73437
+ ok: false,
73438
+ error: `server "${options.name}" already exists in ${file}. Pass --force to replace it, or remove it first.`
73439
+ };
73440
+ }
73441
+ loaded.doc.servers[options.name] = options.entry;
73442
+ writeDocument(file, loaded.doc, options.scope);
73443
+ return { ok: true, file, created: !loaded.existed };
73444
+ }
73445
+ function removeServer(options) {
73446
+ const file = fileForScope(options.scope, options);
73447
+ if (typeof file !== "string")
73448
+ return file;
73449
+ const loaded = readForWrite(file);
73450
+ if ("error" in loaded)
73451
+ return { ok: false, error: loaded.error };
73452
+ if (loaded.doc.servers[options.name] === undefined) {
73453
+ return { ok: false, error: `server "${options.name}" is not defined in ${file}` };
73454
+ }
73455
+ delete loaded.doc.servers[options.name];
73456
+ writeDocument(file, loaded.doc, options.scope);
73457
+ return { ok: true, file, created: false };
73458
+ }
73459
+ function setServerEnabled(options) {
73460
+ const file = overlayFile(options.configDir);
73461
+ const read = readConfigFile(file);
73462
+ let overlay = {};
73463
+ if (read.ok) {
73464
+ try {
73465
+ const parsed = parseJsonTolerant(read.text);
73466
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
73467
+ overlay = parsed;
73468
+ }
73469
+ } catch {
73470
+ overlay = {};
73471
+ }
73472
+ } else if (!isDefiniteAbsence(read.reason)) {
73473
+ return { ok: false, error: `${file} could not be read (${read.reason}); nothing was written` };
73474
+ }
73475
+ const overrides = { ...overlay.overrides, [options.name]: options.enabled };
73476
+ writeOwnerOnlyFileAtomic(file, `${JSON.stringify({ ...overlay, overrides }, null, 2)}
73477
+ `);
73478
+ if (options.source === "user") {
73479
+ clearStickyUserFlag(options.name, options.enabled, options.configDir);
73480
+ }
73481
+ return { ok: true, file, created: !read.ok };
73482
+ }
73483
+ function clearStickyUserFlag(name, enabled, configDir) {
73484
+ const file = userConfigFile(configDir);
73485
+ const loaded = readForWrite(file);
73486
+ if ("error" in loaded)
73487
+ return;
73488
+ const entry = loaded.doc.servers[name];
73489
+ if (entry === undefined)
73490
+ return;
73491
+ if (entry.enabled === undefined || entry.enabled === enabled)
73492
+ return;
73493
+ delete entry.enabled;
73494
+ writeDocument(file, loaded.doc, "user");
73495
+ }
73496
+ function fileForScope(scope, targets) {
73497
+ if (scope === "user")
73498
+ return userConfigFile(targets.configDir);
73499
+ if (targets.projectRoot === undefined) {
73500
+ return { ok: false, error: "--scope project needs a project root; none was resolved" };
73501
+ }
73502
+ return projectConfigFile(targets.projectRoot);
73503
+ }
73504
+
73359
73505
  // src/harness/external/env.ts
73360
73506
  var EXTERNAL_ENV_DENY = [
73361
73507
  "ANTHROPIC_API_KEY",
@@ -73585,11 +73731,32 @@ function createMcpRuntime(options) {
73585
73731
  const kills = [];
73586
73732
  const env = options.env ?? process.env;
73587
73733
  const connect = options.connect ?? ((server) => defaultConnect(server, env, dialling.signal, kills, options.configDir));
73734
+ const closedAlready = new WeakSet;
73735
+ const mutated = new Set;
73588
73736
  const settled = startServers(launchable, connect).then((result) => {
73589
- catalog = result.catalog;
73590
- states = [...heldStates, ...result.servers].sort((a, b) => a.name.localeCompare(b.name));
73737
+ for (const incoming of result.servers) {
73738
+ if (!mutated.has(incoming.name))
73739
+ continue;
73740
+ if (incoming.connection !== undefined && !closedAlready.has(incoming.connection)) {
73741
+ closedAlready.add(incoming.connection);
73742
+ incoming.connection.close().catch(() => {});
73743
+ }
73744
+ }
73745
+ const kept = states.filter((state) => mutated.has(state.name));
73746
+ const incoming = result.servers.filter((state) => !mutated.has(state.name));
73747
+ const heldStill = heldStates.filter((state) => !mutated.has(state.name));
73748
+ states = uniqueByName([...heldStill, ...incoming, ...kept]).sort((a, b) => a.name.localeCompare(b.name));
73749
+ catalog = mergeCatalogs([
73750
+ {
73751
+ entries: result.catalog.entries.filter((entry) => !mutated.has(entry.server)),
73752
+ skipped: result.catalog.skipped.filter((skip) => !mutated.has(skip.server))
73753
+ },
73754
+ {
73755
+ entries: catalog.entries.filter((entry) => mutated.has(entry.server)),
73756
+ skipped: catalog.skipped.filter((skip) => mutated.has(skip.server))
73757
+ }
73758
+ ]);
73591
73759
  }).catch(() => {});
73592
- const closedAlready = new WeakSet;
73593
73760
  const closeOnce = async (list) => {
73594
73761
  const fresh = list.filter((state) => state.connection !== undefined && !closedAlready.has(state.connection));
73595
73762
  for (const state of fresh) {
@@ -73599,6 +73766,7 @@ function createMcpRuntime(options) {
73599
73766
  await closeServers(fresh);
73600
73767
  };
73601
73768
  let closing;
73769
+ const inflight = new Map;
73602
73770
  return {
73603
73771
  catalog: () => catalog,
73604
73772
  servers: () => states,
@@ -73608,8 +73776,100 @@ function createMcpRuntime(options) {
73608
73776
  ready: async () => {
73609
73777
  await settled;
73610
73778
  },
73779
+ connectServer: (name) => runExclusive(name, () => connectOne(name)),
73780
+ disconnectServer: (name) => runExclusive(name, () => disconnectOne(name)),
73611
73781
  close: () => closing ??= closeNow()
73612
73782
  };
73783
+ function runExclusive(name, work) {
73784
+ const existing = inflight.get(name);
73785
+ if (existing !== undefined)
73786
+ return existing;
73787
+ const pending = work().finally(() => {
73788
+ inflight.delete(name);
73789
+ });
73790
+ inflight.set(name, pending);
73791
+ return pending;
73792
+ }
73793
+ function replaceState(next) {
73794
+ states = uniqueByName([...states.filter((state) => state.name !== next.name), next]).sort((a, b) => a.name.localeCompare(b.name));
73795
+ }
73796
+ function stripCatalog(name) {
73797
+ catalog = {
73798
+ entries: catalog.entries.filter((entry) => entry.server !== name),
73799
+ skipped: catalog.skipped.filter((skip) => skip.server !== name)
73800
+ };
73801
+ }
73802
+ async function persistEnabled(server, enabled) {
73803
+ const written = setServerEnabled({
73804
+ name: server.name,
73805
+ enabled,
73806
+ source: server.source,
73807
+ ...options.configDir === undefined ? {} : { configDir: options.configDir }
73808
+ });
73809
+ return written.ok ? undefined : { ok: false, message: written.error };
73810
+ }
73811
+ async function connectOne(name) {
73812
+ if (closing !== undefined)
73813
+ return { ok: false, message: "session is closing" };
73814
+ const server = byName.get(name);
73815
+ if (server === undefined)
73816
+ return { ok: false, message: `no server named "${name}"` };
73817
+ if (requiresApproval(server, loadTrustStore(options.configDir))) {
73818
+ return { ok: false, message: `project server not approved; run \`keryx mcp trust ${name}\`` };
73819
+ }
73820
+ const current = states.find((state) => state.name === name);
73821
+ if (current?.status === "connected")
73822
+ return { ok: true };
73823
+ if (current?.status === "connecting") {
73824
+ return { ok: false, message: `server "${name}" is already connecting` };
73825
+ }
73826
+ const persist = await persistEnabled(server, true);
73827
+ if (persist !== undefined)
73828
+ return persist;
73829
+ mutated.add(name);
73830
+ if (current?.connection !== undefined) {
73831
+ if (!closedAlready.has(current.connection))
73832
+ closedAlready.add(current.connection);
73833
+ await closeServers([current]);
73834
+ }
73835
+ stripCatalog(name);
73836
+ replaceState({ name, status: "connecting", toolCount: 0 });
73837
+ const result = await startServers([{ ...server, enabled: true }], connect);
73838
+ const next = result.servers[0];
73839
+ if (next === undefined) {
73840
+ replaceState({ name, status: "failed", error: "dial returned nothing", toolCount: 0 });
73841
+ return { ok: false, message: "dial returned nothing" };
73842
+ }
73843
+ replaceState(next);
73844
+ if (next.status === "connected") {
73845
+ catalog = mergeCatalogs([catalog, result.catalog]);
73846
+ return { ok: true };
73847
+ }
73848
+ return { ok: false, message: next.error ?? `server "${name}" did not connect` };
73849
+ }
73850
+ async function disconnectOne(name) {
73851
+ if (closing !== undefined)
73852
+ return { ok: false, message: "session is closing" };
73853
+ const server = byName.get(name);
73854
+ if (server === undefined)
73855
+ return { ok: false, message: `no server named "${name}"` };
73856
+ const current = states.find((state) => state.name === name);
73857
+ if (current?.status === "connecting") {
73858
+ return { ok: false, message: `server "${name}" is still connecting` };
73859
+ }
73860
+ const persist = await persistEnabled(server, false);
73861
+ if (persist !== undefined)
73862
+ return persist;
73863
+ mutated.add(name);
73864
+ if (current?.connection !== undefined) {
73865
+ if (!closedAlready.has(current.connection))
73866
+ closedAlready.add(current.connection);
73867
+ await closeServers([current]);
73868
+ }
73869
+ stripCatalog(name);
73870
+ replaceState({ name, status: "disabled", toolCount: 0 });
73871
+ return { ok: true };
73872
+ }
73613
73873
  async function closeNow() {
73614
73874
  {
73615
73875
  dialling.abort();
@@ -73620,6 +73880,17 @@ function createMcpRuntime(options) {
73620
73880
  }
73621
73881
  }
73622
73882
  }
73883
+ function uniqueByName(list) {
73884
+ const seen = new Set;
73885
+ const out = [];
73886
+ for (const state of list) {
73887
+ if (seen.has(state.name))
73888
+ continue;
73889
+ seen.add(state.name);
73890
+ out.push(state);
73891
+ }
73892
+ return out;
73893
+ }
73623
73894
  function handshakeBudgetMs(server) {
73624
73895
  const configured = server.startup_timeout_sec;
73625
73896
  if (configured === undefined || !Number.isFinite(configured) || configured <= 0) {
@@ -73685,153 +73956,7 @@ async function defaultConnect(server, env, signal, kills, configDir) {
73685
73956
  function defaultServerCwd(server, fallbackCwd = process.cwd()) {
73686
73957
  if (server.source !== "project")
73687
73958
  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);
73959
+ return path169.dirname(path169.dirname(server.file));
73835
73960
  }
73836
73961
 
73837
73962
  // src/commands/mcp-servers.ts
@@ -88242,36 +88367,257 @@ function buildConsumerModel(input) {
88242
88367
  project: ${input.projectFile}`
88243
88368
  };
88244
88369
  }
88245
- function renderConsumerLines(model) {
88370
+ var MCP_CONSUMER_FOOTER = [
88371
+ { key: "\u2191/\u2193", label: "select" },
88372
+ { key: "click", label: "row: select/act" },
88373
+ { key: "c/d", label: "connect/disconnect" },
88374
+ { key: "y", label: "confirm" },
88375
+ { key: "esc", label: "close" }
88376
+ ];
88377
+ var MCP_CONSUMER_HEADER = "Servers keryx CONNECTS TO. c/d then y to connect or disconnect. Held project servers need `keryx mcp trust` first.";
88378
+ var STATUS_GLYPH = {
88379
+ connected: "\u25CF connected",
88380
+ connecting: "\u2026 connecting",
88381
+ failed: "\u2717 failed",
88382
+ disabled: "\u25CB disabled",
88383
+ "needs-approval": "! needs approval",
88384
+ needs_auth: "! needs auth"
88385
+ };
88386
+ function consumerActionFor(status) {
88387
+ if (status === "connected")
88388
+ return "disconnect";
88389
+ if (status === "disabled" || status === "failed")
88390
+ return "connect";
88391
+ return;
88392
+ }
88393
+ function statusGlyph(status) {
88394
+ return STATUS_GLYPH[status] ?? status;
88395
+ }
88396
+ function formatConsumerModalRow(row, isSelected, status) {
88397
+ const mark = isSelected ? ">" : " ";
88398
+ const name = row.name.padEnd(18);
88399
+ const source = row.source.padEnd(10);
88400
+ const transport = row.transport.padEnd(6);
88401
+ const glyph = statusGlyph(row.status).padEnd(18);
88402
+ const tools = row.status === "connected" ? `${row.toolCount} tool(s)` : "";
88403
+ let action;
88404
+ const offered = consumerActionFor(row.status);
88405
+ if (status.kind === "armed" && status.target.name === row.name) {
88406
+ action = ` [click again or press y to ${status.target.action}]`;
88407
+ } else if (status.kind === "running" && status.target.name === row.name) {
88408
+ action = ` ${status.target.action === "connect" ? "connecting\u2026" : "disconnecting\u2026"}`;
88409
+ } else if (status.kind === "done" && status.target.name === row.name) {
88410
+ action = status.outcome.ok ? " \u2713 done" : ` \u2717 ${status.outcome.message}`;
88411
+ } else if (offered === "disconnect") {
88412
+ action = " [d] disconnect";
88413
+ } else if (offered === "connect") {
88414
+ action = " [c] connect";
88415
+ } else if (row.action !== undefined) {
88416
+ action = ` \u2192 ${row.action}`;
88417
+ } else {
88418
+ action = "";
88419
+ }
88420
+ const creds = row.credentials.length > 0 ? ` [reads ${row.credentials.join(", ")}]` : "";
88421
+ return `${mark} ${name} ${source} ${transport} ${glyph}${tools}${action} ${row.target}${creds}`.trimEnd();
88422
+ }
88423
+ function formatConsumerModalLines(model, selected, status) {
88246
88424
  if (model === undefined) {
88247
88425
  return [
88248
88426
  "No MCP session yet \u2014 this shell has not built a tool list.",
88249
88427
  "Configured servers are listed by `keryx mcp list`; `keryx mcp doctor` dials them."
88250
88428
  ];
88251
88429
  }
88430
+ const header = [MCP_CONSUMER_HEADER];
88431
+ const problems = model.problems.flatMap((problem) => problem.split(`
88432
+ `).map((line, index) => index === 0 ? `config problem \u2014 ${line}` : ` ${line}`));
88433
+ if (model.emptyHint !== undefined) {
88434
+ return [...header, ...problems, ...model.emptyHint.split(`
88435
+ `)];
88436
+ }
88252
88437
  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
- })
88438
+ ...header,
88439
+ ...problems,
88440
+ ...model.rows.map((row, index) => formatConsumerModalRow(row, index === selected, status))
88268
88441
  ];
88269
88442
  }
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}`;
88443
+ function asRowTarget2(body) {
88444
+ const parent = body;
88445
+ if (parent.add === undefined || parent.getChildren === undefined || parent.remove === undefined) {
88446
+ return;
88447
+ }
88448
+ return {
88449
+ add: parent.add.bind(parent),
88450
+ getChildren: parent.getChildren.bind(parent),
88451
+ remove: parent.remove.bind(parent)
88452
+ };
88453
+ }
88454
+ function presentMcpConsumer(openModalFn, otui, chrome, options) {
88455
+ let selected = 0;
88456
+ let scroll = 0;
88457
+ let status = { kind: "idle" };
88458
+ let body;
88459
+ let rowCtor;
88460
+ let activeRenderer;
88461
+ let unsubscribeKey;
88462
+ const rendererHint = options.renderer ?? chrome?.renderer;
88463
+ const bodyRows = options.visibleRows ?? (typeof rendererHint?.width === "number" && typeof rendererHint.height === "number" ? modalBodyRows(resolveModalPanelSize(rendererHint.width, rendererHint.height).height) : 13);
88464
+ const rowsOf = () => options.snapshot()?.rows ?? [];
88465
+ const paint = () => {
88466
+ if (body === undefined || rowCtor === undefined)
88467
+ return;
88468
+ clearTranscriptChildren(body);
88469
+ const model = options.snapshot();
88470
+ const rows = model?.rows ?? [];
88471
+ selected = rows.length === 0 ? 0 : Math.min(selected, rows.length - 1);
88472
+ scroll = scrollToReveal3(selected, scroll, bodyRows);
88473
+ scroll = clampScroll3(scroll, Math.max(rows.length, 1), bodyRows);
88474
+ const lines = formatConsumerModalLines(model, selected, status);
88475
+ if (rows.length === 0) {
88476
+ for (const [i, line] of lines.entries()) {
88477
+ body.add(new rowCtor(activeRenderer, { id: `mcp-consumer-line-${i}`, content: line }));
88478
+ }
88479
+ return;
88480
+ }
88481
+ body.add(new rowCtor(activeRenderer, { id: "mcp-consumer-header", content: MCP_CONSUMER_HEADER }));
88482
+ const problems = model?.problems ?? [];
88483
+ for (const [i, problem] of problems.entries()) {
88484
+ body.add(new rowCtor(activeRenderer, {
88485
+ id: `mcp-consumer-problem-${i}`,
88486
+ content: `config problem \u2014 ${problem}`
88487
+ }));
88488
+ }
88489
+ const start = clampScroll3(scroll, rows.length, bodyRows);
88490
+ for (const [i, row] of rows.slice(start, start + bodyRows).entries()) {
88491
+ const index = start + i;
88492
+ body.add(new rowCtor(activeRenderer, {
88493
+ id: `mcp-consumer-row-${row.name}`,
88494
+ content: formatConsumerModalRow(row, index === selected, status),
88495
+ onMouseDown: () => handleRowClick(row.name, index)
88496
+ }));
88497
+ }
88498
+ };
88499
+ const moveSelection = (next) => {
88500
+ const rows = rowsOf();
88501
+ if (rows.length === 0)
88502
+ return;
88503
+ const clamped = Math.min(rows.length - 1, Math.max(0, next));
88504
+ if (clamped === selected)
88505
+ return;
88506
+ selected = clamped;
88507
+ status = { kind: "idle" };
88508
+ paint();
88509
+ };
88510
+ const runAction = () => {
88511
+ if (status.kind !== "armed")
88512
+ return;
88513
+ const target = status.target;
88514
+ status = { kind: "running", target };
88515
+ paint();
88516
+ const fn = target.action === "connect" ? options.connect : options.disconnect;
88517
+ fn(target.name).then((outcome) => {
88518
+ status = { kind: "done", target, outcome };
88519
+ paint();
88520
+ });
88521
+ };
88522
+ const armFor = (name) => {
88523
+ const rows = rowsOf();
88524
+ const index = rows.findIndex((row) => row.name === name);
88525
+ const row = rows[index];
88526
+ const offered = row === undefined ? undefined : consumerActionFor(row.status);
88527
+ if (index < 0 || row === undefined || offered === undefined || status.kind === "running")
88528
+ return;
88529
+ selected = index;
88530
+ status = { kind: "armed", target: { name: row.name, action: offered } };
88531
+ paint();
88532
+ };
88533
+ const handleRowClick = (name, index) => {
88534
+ if (status.kind === "armed" && status.target.name === name) {
88535
+ runAction();
88536
+ return;
88537
+ }
88538
+ if (status.kind === "running")
88539
+ return;
88540
+ const row = rowsOf()[index];
88541
+ if (row === undefined || consumerActionFor(row.status) === undefined) {
88542
+ if (index !== selected) {
88543
+ selected = index;
88544
+ status = { kind: "idle" };
88545
+ paint();
88546
+ }
88547
+ return;
88548
+ }
88549
+ armFor(name);
88550
+ };
88551
+ const handle = openModalFn(otui, chrome, {
88552
+ title: "MCP servers",
88553
+ tabs: [{ id: "servers", label: "Servers" }],
88554
+ initialTab: "servers",
88555
+ footer: MCP_CONSUMER_FOOTER,
88556
+ renderTab: (_tabId, tabBody, ctx) => {
88557
+ const renderer = options.renderer ?? chrome?.renderer;
88558
+ const ctor = otui.TextRenderable;
88559
+ const target = asRowTarget2(tabBody);
88560
+ if (target === undefined || ctor === undefined)
88561
+ return;
88562
+ rowCtor = ctor;
88563
+ activeRenderer = renderer;
88564
+ body = target;
88565
+ paint();
88566
+ },
88567
+ onClose: () => {
88568
+ unsubscribeKey?.();
88569
+ }
88570
+ });
88571
+ if (handle === undefined)
88572
+ return;
88573
+ if (options.onKeypress !== undefined) {
88574
+ unsubscribeKey = options.onKeypress((key) => {
88575
+ const token = key.name || key.sequence;
88576
+ const rows = rowsOf();
88577
+ if (status.kind === "armed") {
88578
+ if (token === "y") {
88579
+ runAction();
88580
+ } else {
88581
+ status = { kind: "idle" };
88582
+ paint();
88583
+ }
88584
+ return;
88585
+ }
88586
+ if (token === "c") {
88587
+ const row = rows[selected];
88588
+ if (row !== undefined && consumerActionFor(row.status) === "connect" && status.kind !== "running") {
88589
+ status = { kind: "armed", target: { name: row.name, action: "connect" } };
88590
+ paint();
88591
+ }
88592
+ return;
88593
+ }
88594
+ if (token === "d") {
88595
+ const row = rows[selected];
88596
+ if (row !== undefined && consumerActionFor(row.status) === "disconnect" && status.kind !== "running") {
88597
+ status = { kind: "armed", target: { name: row.name, action: "disconnect" } };
88598
+ paint();
88599
+ }
88600
+ return;
88601
+ }
88602
+ if (token === "up" || token === "k") {
88603
+ moveSelection(selected - 1);
88604
+ return;
88605
+ }
88606
+ if (token === "down" || token === "j") {
88607
+ moveSelection(selected + 1);
88608
+ return;
88609
+ }
88610
+ if (token === "pageup" || token === "pagedown") {
88611
+ const step = token === "pageup" ? -bodyRows : bodyRows;
88612
+ scroll = clampScroll3(scroll + step, Math.max(rows.length, 1), bodyRows);
88613
+ paint();
88614
+ }
88615
+ });
88616
+ }
88617
+ return handle;
88618
+ }
88619
+ function openMcpConsumer(otui, chrome, options) {
88620
+ return presentMcpConsumer((hostOtui, hostChrome, input) => openModal(hostOtui, hostChrome, input), otui, chrome, options);
88275
88621
  }
88276
88622
 
88277
88623
  // src/tui/tui-shell.ts
@@ -89048,7 +89394,7 @@ var AGENT_SLASH_COMMANDS = [
89048
89394
  },
89049
89395
  {
89050
89396
  name: "/mcp",
89051
- description: "MCP servers keryx is connected to \u2014 status, tools, what needs trust",
89397
+ description: "MCP servers keryx is connected to \u2014 status, connect/disconnect",
89052
89398
  modes: AGENT_ONLY
89053
89399
  },
89054
89400
  {
@@ -91817,7 +92163,7 @@ function stepQueueNavAction(selected, direction) {
91817
92163
  }
91818
92164
 
91819
92165
  // src/tui/worker-fleet.ts
91820
- var STATUS_GLYPH = {
92166
+ var STATUS_GLYPH2 = {
91821
92167
  queued: "\u25CB",
91822
92168
  running: "\u25D0",
91823
92169
  done: "\u25CF",
@@ -91956,7 +92302,7 @@ function formatFleetSidebar(workers, maxLines = 14) {
91956
92302
  const room = Math.max(2, maxLines - lines.length);
91957
92303
  const shown = sorted.slice(0, room);
91958
92304
  for (const w of shown) {
91959
- const g = STATUS_GLYPH[w.status];
92305
+ const g = STATUS_GLYPH2[w.status];
91960
92306
  const phase = humanFleetPhase(w.status, w.detail);
91961
92307
  const label = clip4(w.label, 12);
91962
92308
  const det = phase.length > 0 ? ` ${clip4(phase, 12)}` : "";
@@ -92050,7 +92396,7 @@ class WorkerFleet {
92050
92396
  var MAX_SUBAGENT_EVENTS = 200;
92051
92397
  var MAX_SUBAGENT_EVENT_CHARS = 2000;
92052
92398
  var EXTERNAL_SUBAGENT_MARK = "\u2933";
92053
- var STATUS_GLYPH2 = {
92399
+ var STATUS_GLYPH3 = {
92054
92400
  queued: "\u25CB",
92055
92401
  running: "\u25D0",
92056
92402
  done: "\u25CF",
@@ -92092,7 +92438,7 @@ function formatElapsed(ms) {
92092
92438
  return minRem === 0 ? `${hours}h` : `${hours}h ${minRem}m`;
92093
92439
  }
92094
92440
  function formatSubagentRow(session, width = SIDEBAR_TEXT_WIDTH) {
92095
- const glyph = STATUS_GLYPH2[session.status];
92441
+ const glyph = STATUS_GLYPH3[session.status];
92096
92442
  const phase = humanFleetPhase(session.status, session.detail);
92097
92443
  const mark = session.runtime === "external" ? EXTERNAL_SUBAGENT_MARK : "";
92098
92444
  const budget = Math.max(4, width - glyph.length - 1);
@@ -93451,7 +93797,7 @@ function openExternalInspector(otui, chrome, options) {
93451
93797
 
93452
93798
  // src/tui/background-job-session.ts
93453
93799
  var MAX_BACKGROUND_JOB_OUTPUT_CHARS = 20000;
93454
- var STATUS_GLYPH3 = {
93800
+ var STATUS_GLYPH4 = {
93455
93801
  running: "\u25D0",
93456
93802
  exited: "\u25CF",
93457
93803
  killed: "\u2717"
@@ -93469,7 +93815,7 @@ function formatJobListHeader(count) {
93469
93815
  return `Background Jobs ${count}`;
93470
93816
  }
93471
93817
  function formatJobRow(entry, width = SIDEBAR_TEXT_WIDTH) {
93472
- const glyph = STATUS_GLYPH3[entry.status];
93818
+ const glyph = STATUS_GLYPH4[entry.status];
93473
93819
  const budget = Math.max(1, width - glyph.length - 1);
93474
93820
  const cmd = clip7(entry.command, budget);
93475
93821
  return clip7(`${glyph} ${cmd}`, width);
@@ -96059,19 +96405,36 @@ Staying in the current session.
96059
96405
  })();
96060
96406
  };
96061
96407
  const showMcpConsumer = () => {
96062
- const runtime = deps.mcpRuntime?.();
96063
96408
  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)
96409
+ const snapshot = () => {
96410
+ const live = deps.mcpRuntime?.();
96411
+ if (live === undefined)
96412
+ return;
96413
+ return buildConsumerModel({
96414
+ configured: live.configured(),
96415
+ states: live.servers(),
96416
+ problems: live.problems(),
96417
+ userFile: userConfigFile(),
96418
+ projectFile: projectConfigFile(cwd)
96419
+ });
96420
+ };
96421
+ openMcpConsumer(otui, chrome, {
96422
+ snapshot,
96423
+ connect: async (name) => {
96424
+ const live = deps.mcpRuntime?.();
96425
+ if (live === undefined)
96426
+ return { ok: false, message: "No MCP session yet" };
96427
+ return live.connectServer(name);
96428
+ },
96429
+ disconnect: async (name) => {
96430
+ const live = deps.mcpRuntime?.();
96431
+ if (live === undefined)
96432
+ return { ok: false, message: "No MCP session yet" };
96433
+ return live.disconnectServer(name);
96434
+ },
96435
+ renderer: r,
96436
+ ...inspectorKeys
96070
96437
  });
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
96438
  };
96076
96439
  const showTools = () => {
96077
96440
  (async () => {
@@ -103099,7 +103462,7 @@ function monitorCommand(args) {
103099
103462
  }
103100
103463
  renderAgentsTree(snapshot);
103101
103464
  }
103102
- var STATUS_GLYPH4 = {
103465
+ var STATUS_GLYPH5 = {
103103
103466
  running: "\u25D0",
103104
103467
  done: "\u25CF",
103105
103468
  blocked: "\u25FC",
@@ -103112,7 +103475,7 @@ function renderAgentsTree(snapshot) {
103112
103475
  console.log(`run ${snapshot.runId ?? "(unknown)"} \u2014 ${snapshot.agents.length} subagent(s)`);
103113
103476
  for (const agent of snapshot.agents) {
103114
103477
  const tokens = `\u2191${agent.usage.inputTokens} \u2193${agent.usage.outputTokens}${agent.usage.exact ? "" : "~"}`;
103115
- const glyph = STATUS_GLYPH4[agent.status] ?? "\u25CB";
103478
+ const glyph = STATUS_GLYPH5[agent.status] ?? "\u25CB";
103116
103479
  console.log(` ${glyph} ${agent.dispatchId} ${agent.status} ${agent.model ?? "-"} ${tokens}`);
103117
103480
  }
103118
103481
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrciphersmith/keryx",
3
- "version": "0.2.99",
3
+ "version": "0.2.100",
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": {