@mrciphersmith/keryx 0.2.50 → 0.2.52

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 +463 -36
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -17055,7 +17055,7 @@ function renderWrapUpMemoryEntry(input2) {
17055
17055
 
17056
17056
  Version: 0.1.0
17057
17057
  Type: task-note
17058
- Status: draft
17058
+ Status: accepted
17059
17059
  Confidence: medium
17060
17060
 
17061
17061
  ## Summary
@@ -17155,7 +17155,7 @@ function renderWrapUpDecisionPage(input2) {
17155
17155
 
17156
17156
  Version: 0.1.0
17157
17157
  Type: decision
17158
- Status: draft
17158
+ Status: accepted
17159
17159
 
17160
17160
  ## Summary
17161
17161
 
@@ -33709,6 +33709,21 @@ function renderMcpClientSnippet(projectRoot) {
33709
33709
  return `${JSON.stringify({ mcpServers: { [MCP_SERVER_NAME]: mcpServerEntry(projectRoot) } }, null, 2)}
33710
33710
  `;
33711
33711
  }
33712
+ async function mcpClientStatus(projectRoot, ids = mcpRuntimeIds()) {
33713
+ const absoluteProjectRoot = path40.resolve(projectRoot);
33714
+ const { runtimes } = resolveMcpRuntimes(ids);
33715
+ const statuses = [];
33716
+ for (const runtime of runtimes) {
33717
+ const file = runtime.settingsPath(absoluteProjectRoot);
33718
+ if (file === null) {
33719
+ statuses.push({ id: runtime.id, filePath: null, connected: false });
33720
+ continue;
33721
+ }
33722
+ const settings = await readSettings2(file);
33723
+ statuses.push({ id: runtime.id, filePath: file, connected: runtime.hasManaged(settings) });
33724
+ }
33725
+ return statuses;
33726
+ }
33712
33727
  async function readSettings2(file) {
33713
33728
  if (!await pathExists(file)) {
33714
33729
  return {};
@@ -33752,7 +33767,7 @@ function buildMcpModuleEntry() {
33752
33767
  expose: {
33753
33768
  tools: true,
33754
33769
  resources: true,
33755
- modules: ["gdgraph", "gdctx", "security", "flow", "memory", "health", "testing", "wiki", "standard", "sac"]
33770
+ modules: ["gdgraph", "gdctx", "security", "flow", "memory", "health", "testing", "wiki", "standard", "sac", "gdskills"]
33756
33771
  }
33757
33772
  };
33758
33773
  }
@@ -47120,6 +47135,8 @@ init_workspace_service();
47120
47135
  var HARNESS_PROVIDER_OPTIONS = [
47121
47136
  "fake",
47122
47137
  "anthropic",
47138
+ "openai",
47139
+ "gemini",
47123
47140
  "ollama",
47124
47141
  ...OPENAI_COMPAT_PROVIDERS.map((provider) => provider.name)
47125
47142
  ];
@@ -47285,6 +47302,20 @@ async function harnessCommand(args2, deps) {
47285
47302
  return;
47286
47303
  }
47287
47304
  }
47305
+ if (provider === "openai") {
47306
+ const apiKey = env.OPENAI_API_KEY;
47307
+ if (apiKey === undefined || apiKey.length === 0) {
47308
+ console.log("OPENAI_API_KEY is not set: the openai provider is required to have a credential and fails closed (no network was contacted).");
47309
+ return;
47310
+ }
47311
+ }
47312
+ if (provider === "gemini") {
47313
+ const apiKey = env.GEMINI_API_KEY !== undefined && env.GEMINI_API_KEY.length > 0 ? env.GEMINI_API_KEY : env.GOOGLE_API_KEY;
47314
+ if (apiKey === undefined || apiKey.length === 0) {
47315
+ console.log("GEMINI_API_KEY (or GOOGLE_API_KEY) is not set: the gemini provider is required to have a credential and fails closed (no network was contacted).");
47316
+ return;
47317
+ }
47318
+ }
47288
47319
  const providerPort = makeProvider(provider, model, {
47289
47320
  fetch: fetchImpl,
47290
47321
  env,
@@ -52920,7 +52951,7 @@ import { spawnSync as spawnSync2 } from "child_process";
52920
52951
  // package.json
52921
52952
  var package_default = {
52922
52953
  name: "@mrciphersmith/keryx",
52923
- version: "0.2.50",
52954
+ version: "0.2.52",
52924
52955
  description: "Version-controlled project context for AI coding agents: code graph, architecture wiki, project memory, relevant tests, quality signals, and task flows.",
52925
52956
  private: false,
52926
52957
  publishConfig: {
@@ -53808,16 +53839,15 @@ var MODAL_PANEL_MARGIN = 1;
53808
53839
  var MODAL_PANEL_CHROME_X = 4;
53809
53840
  var MODAL_PANEL_MIN_WIDTH = 72;
53810
53841
  var MODAL_PANEL_MIN_HEIGHT = 18;
53811
- var MODAL_PANEL_TARGET_WIDTH = 96;
53812
- var MODAL_PANEL_TARGET_HEIGHT = 28;
53842
+ var MODAL_PANEL_SIZE_RATIO = 0.95;
53813
53843
  var MODAL_CHROME_ROWS = 5;
53814
53844
  var MODAL_PANEL_WIDTH = MODAL_PANEL_MIN_WIDTH;
53815
53845
  var MODAL_PANEL_HEIGHT = MODAL_PANEL_MIN_HEIGHT;
53816
53846
  var MODAL_PANEL_INNER_WIDTH = MODAL_PANEL_MIN_WIDTH - MODAL_PANEL_CHROME_X;
53817
53847
  function resolveModalPanelSize(cols, rows) {
53818
53848
  return {
53819
- width: Math.min(MODAL_PANEL_TARGET_WIDTH, Math.max(MODAL_PANEL_MIN_WIDTH, cols - 4)),
53820
- height: Math.min(MODAL_PANEL_TARGET_HEIGHT, Math.max(MODAL_PANEL_MIN_HEIGHT, rows - 4))
53849
+ width: Math.max(MODAL_PANEL_MIN_WIDTH, Math.round(cols * MODAL_PANEL_SIZE_RATIO)),
53850
+ height: Math.max(MODAL_PANEL_MIN_HEIGHT, Math.round(rows * MODAL_PANEL_SIZE_RATIO))
53821
53851
  };
53822
53852
  }
53823
53853
  function modalBodyRows(panelHeight) {
@@ -54231,6 +54261,7 @@ async function computeLifecycleFlags(cwd, now = () => new Date) {
54231
54261
  }
54232
54262
 
54233
54263
  // src/sac/catch-up.ts
54264
+ init_proposal_evidence();
54234
54265
  async function buildCatchUp(input2) {
54235
54266
  const [proposals, sessionCategories, lifecycleFlagsAll] = await Promise.all([
54236
54267
  collectProposals(input2.cwd, input2.workspaceId),
@@ -54250,8 +54281,20 @@ async function collectProposals(cwd, workspaceId) {
54250
54281
  const scoped = workspaceId === undefined ? groups : groups.filter((group) => group.workspace.id === workspaceId);
54251
54282
  const flattened = scoped.flatMap((group) => group.proposals.map((proposal) => ({ group, proposal })));
54252
54283
  return Promise.all(flattened.map(async ({ group, proposal }) => {
54253
- const fresh = await proposalService.isEvidenceFresh(proposal, actor);
54254
- return { type: "proposal", workspaceId: group.workspace.id, proposalId: proposal.id, fresh };
54284
+ const [fresh, note2] = await Promise.all([
54285
+ proposalService.isEvidenceFresh(proposal, actor),
54286
+ readSidecarNote(cwd, group.workspace.id, proposal.id)
54287
+ ]);
54288
+ return {
54289
+ type: "proposal",
54290
+ workspaceId: group.workspace.id,
54291
+ proposalId: proposal.id,
54292
+ fresh,
54293
+ kind: proposal.kind,
54294
+ author: proposal.author,
54295
+ createdAt: proposal.createdAt,
54296
+ note: note2
54297
+ };
54255
54298
  }));
54256
54299
  }
54257
54300
  async function classifySession(session) {
@@ -54822,7 +54865,7 @@ function openFlows(otui, chrome, options) {
54822
54865
 
54823
54866
  // src/tui/busy-dispatch.ts
54824
54867
  function classifyBusyDispatch(params) {
54825
- const { line, commandName, isSessionInfo, isFlows, isWorkspace, isReview } = params;
54868
+ const { line, commandName, isSessionInfo, isFlows, isWorkspace, isReview, isMcp } = params;
54826
54869
  if (commandName === "/exit")
54827
54870
  return "exit";
54828
54871
  if (commandName === "/help")
@@ -54841,7 +54884,7 @@ function classifyBusyDispatch(params) {
54841
54884
  return "copy";
54842
54885
  if (commandName === "/mode")
54843
54886
  return "mode";
54844
- const isBusyReadonlyCommand = isSessionInfo || isFlows || isWorkspace || isReview;
54887
+ const isBusyReadonlyCommand = isSessionInfo || isFlows || isWorkspace || isReview || isMcp;
54845
54888
  if (isBusyReadonlyCommand && isSessionInfo)
54846
54889
  return "session-info";
54847
54890
  if (isBusyReadonlyCommand && isFlows)
@@ -54850,6 +54893,8 @@ function classifyBusyDispatch(params) {
54850
54893
  return "workspace";
54851
54894
  if (isBusyReadonlyCommand && isReview)
54852
54895
  return "review";
54896
+ if (isBusyReadonlyCommand && isMcp)
54897
+ return "mcp";
54853
54898
  if (commandName !== undefined || line.startsWith("/"))
54854
54899
  return "deferred";
54855
54900
  return "not-a-command";
@@ -55109,7 +55154,8 @@ function openWorkspace(otui, chrome, options) {
55109
55154
  var REVIEW_COMMAND = "/review";
55110
55155
  var REVIEW_FOOTER = [
55111
55156
  { key: "[/]", label: "item" },
55112
- { key: "a y", label: "accept" },
55157
+ { key: "a y", label: "accept proposal" },
55158
+ { key: "d y", label: "decline proposal" },
55113
55159
  { key: "\u2191/\u2193", label: "scroll" },
55114
55160
  { key: "\u2190/\u2192", label: "tabs" },
55115
55161
  { key: "esc", label: "close" }
@@ -55127,7 +55173,7 @@ var TYPE_LABEL = {
55127
55173
  function summarizeReviewItem(item) {
55128
55174
  switch (item.type) {
55129
55175
  case "proposal":
55130
- return `${item.proposalId} in ${item.workspaceId}${item.fresh ? "" : " (stale)"}`;
55176
+ return `${item.kind} ${item.proposalId} in ${item.workspaceId}${item.fresh ? "" : " (stale)"}`;
55131
55177
  case "blocked":
55132
55178
  return `${item.sessionId} \u2014 ${item.terminalState.reason}`;
55133
55179
  case "unbound-candidate":
@@ -55151,9 +55197,13 @@ function describeReviewItem(item) {
55151
55197
  return [
55152
55198
  `Proposal ${item.proposalId}`,
55153
55199
  `Workspace ${item.workspaceId}`,
55200
+ `Kind ${item.kind}`,
55201
+ `Author ${item.author}`,
55202
+ `Created ${item.createdAt}`,
55154
55203
  `Evidence ${item.fresh ? "fresh" : "stale \u2014 evidence has drifted since this proposal was created; re-run wrap-up before deciding"}`,
55204
+ ...item.note !== undefined ? ["", `Note ${item.note}`] : [],
55155
55205
  "",
55156
- `Reject/dismiss from a terminal: keryx workspace review ${item.workspaceId} ${item.proposalId} --decision <rejected|dismissed>`
55206
+ `Dismiss (archive with no decision) from a terminal: keryx workspace review ${item.workspaceId} ${item.proposalId} --decision dismissed`
55157
55207
  ];
55158
55208
  case "blocked":
55159
55209
  return [
@@ -55206,25 +55256,37 @@ function describeGroupOutcome(g) {
55206
55256
  }
55207
55257
  }
55208
55258
  }
55259
+ var DECISION_VERB = { accept: "Accept", decline: "Decline" };
55260
+ var DECISION_ING = { accept: "Accepting", decline: "Declining" };
55261
+ var DECISION_DONE = { accept: "Accepted", decline: "Declined" };
55262
+ var DECISION_COMMAND = {
55263
+ accept: "running `keryx workspace confirm-review` then `keryx workspace review`",
55264
+ decline: "running `keryx workspace review --decision rejected`"
55265
+ };
55209
55266
  function formatReviewDetailLines(item, status) {
55210
55267
  if (item === undefined) {
55211
55268
  return ["No item selected.", "", "Press Enter (or click a row) on the Review tab to view one."];
55212
55269
  }
55213
55270
  const lines = describeReviewItem(item);
55214
55271
  if (item.type !== "proposal") {
55272
+ if (status.kind === "unavailable") {
55273
+ return [...lines, "", `[${status.decision === "accept" ? "a" : "d"}] does nothing here \u2014 accept/decline only apply to a pending proposal, not to this item.`];
55274
+ }
55215
55275
  return lines;
55216
55276
  }
55217
55277
  const withAction = [...lines, ""];
55218
55278
  if (status.kind === "armed") {
55219
- withAction.push("Press [y] to CONFIRM accept, any other key cancels.");
55279
+ withAction.push(`Press [y] to CONFIRM ${status.decision}, any other key cancels.`);
55220
55280
  } else if (status.kind === "running") {
55221
- withAction.push("Accepting\u2026 running `keryx workspace confirm-review` then `keryx workspace review`.");
55281
+ withAction.push(`${DECISION_ING[status.decision]}\u2026 ${DECISION_COMMAND[status.decision]}.`);
55222
55282
  } else if (status.kind === "done" && status.outcome.ok) {
55223
- withAction.push("\u2713 Accepted.");
55283
+ withAction.push(`\u2713 ${DECISION_DONE[status.decision]}.`);
55224
55284
  } else if (status.kind === "done" && !status.outcome.ok) {
55225
- withAction.push(`\u2717 Accept failed: ${status.outcome.message}`);
55285
+ withAction.push(`\u2717 ${DECISION_VERB[status.decision]} failed: ${status.outcome.message}`);
55286
+ } else if (status.kind === "unavailable") {
55287
+ withAction.push(`[${status.decision === "accept" ? "a" : "d"}] does nothing \u2014 no ${status.decision} handler is configured for this modal.`);
55226
55288
  } else {
55227
- withAction.push("[a] Accept this proposal");
55289
+ withAction.push("[a] Accept this proposal [d] Decline this proposal");
55228
55290
  }
55229
55291
  return withAction;
55230
55292
  }
@@ -55321,17 +55383,19 @@ function presentReview(openModal2, otui, chrome, options) {
55321
55383
  status = { kind: "idle" };
55322
55384
  paintSelection();
55323
55385
  };
55324
- const runAccept = () => {
55386
+ const handlerFor = (decision) => decision === "accept" ? options.acceptProposal : options.declineProposal;
55387
+ const runDecision = (decision) => {
55325
55388
  const item = items[selected];
55326
- if (item === undefined || item.type !== "proposal" || options.acceptProposal === undefined) {
55389
+ const run = handlerFor(decision);
55390
+ if (item === undefined || item.type !== "proposal" || run === undefined) {
55327
55391
  return;
55328
55392
  }
55329
- status = { kind: "running" };
55393
+ status = { kind: "running", decision };
55330
55394
  paintSelection();
55331
- options.acceptProposal(item).then((outcome) => {
55332
- status = { kind: "done", outcome };
55395
+ run(item).then((outcome) => {
55396
+ status = { kind: "done", decision, outcome };
55333
55397
  if (outcome.ok) {
55334
- options.onAccepted?.(item);
55398
+ options.onResolved?.(item);
55335
55399
  }
55336
55400
  paintSelection();
55337
55401
  });
@@ -55371,7 +55435,7 @@ function presentReview(openModal2, otui, chrome, options) {
55371
55435
  const onDetail = handle.activeTab() === "detail";
55372
55436
  if (onDetail && status.kind === "armed") {
55373
55437
  if (token === "y") {
55374
- runAccept();
55438
+ runDecision(status.decision);
55375
55439
  } else {
55376
55440
  status = { kind: "idle" };
55377
55441
  paintSelection();
@@ -55390,8 +55454,9 @@ function presentReview(openModal2, otui, chrome, options) {
55390
55454
  handle.setTab("detail");
55391
55455
  return;
55392
55456
  }
55393
- if (onDetail && token === "a" && items[selected]?.type === "proposal" && options.acceptProposal !== undefined && status.kind !== "running") {
55394
- status = { kind: "armed" };
55457
+ if (onDetail && (token === "a" || token === "d") && status.kind !== "running") {
55458
+ const decision = token === "a" ? "accept" : "decline";
55459
+ status = items[selected]?.type === "proposal" && handlerFor(decision) !== undefined ? { kind: "armed", decision } : { kind: "unavailable", decision };
55395
55460
  paintSelection();
55396
55461
  return;
55397
55462
  }
@@ -55455,6 +55520,285 @@ async function acceptProposalViaShell(run, workspaceId, proposalId) {
55455
55520
  }
55456
55521
  return { ok: true };
55457
55522
  }
55523
+ async function declineProposalViaShell(run, workspaceId, proposalId) {
55524
+ const decline = await run(`keryx workspace review ${shQuote(workspaceId)} ${shQuote(proposalId)} --decision rejected`);
55525
+ if (decline.isError) {
55526
+ return { ok: false, message: decline.output };
55527
+ }
55528
+ return { ok: true };
55529
+ }
55530
+
55531
+ // src/tui/mcp-inspector.ts
55532
+ var MCP_INSPECTOR_FOOTER = [
55533
+ { key: "\u2191/\u2193", label: "select" },
55534
+ { key: "click", label: "row: select/act" },
55535
+ { key: "c/d", label: "connect/disconnect" },
55536
+ { key: "y", label: "confirm" },
55537
+ { key: "\u2190/\u2192", label: "tabs" },
55538
+ { key: "esc", label: "close" }
55539
+ ];
55540
+ var MCP_TOOLS_COMMAND = "/mcp";
55541
+ function isMcpToolsCommand(line) {
55542
+ const token = line.trim().split(/\s+/)[0] ?? "";
55543
+ return token === MCP_TOOLS_COMMAND;
55544
+ }
55545
+ var RUNTIME_LABELS = {
55546
+ cursor: "Cursor",
55547
+ claude: "Claude Code",
55548
+ opencode: "opencode",
55549
+ vscode: "VS Code",
55550
+ generic: "Generic (manual)"
55551
+ };
55552
+ function runtimeLabel(id) {
55553
+ return RUNTIME_LABELS[id] ?? id;
55554
+ }
55555
+ function formatToolRowLine(tool) {
55556
+ const risk = (tool.risk ?? "read").padEnd(6);
55557
+ const name = tool.name.padEnd(28);
55558
+ return `${name} ${risk} ${tool.description ?? ""}`.trimEnd();
55559
+ }
55560
+ function isActionable(id) {
55561
+ return id !== "generic";
55562
+ }
55563
+ function formatMcpRowLine(runtime, isSelected, status) {
55564
+ const mark = isSelected ? ">" : " ";
55565
+ const label = runtimeLabel(runtime.id).padEnd(20);
55566
+ const statusText = runtime.connected ? "\u25CF connected" : "\u25CB not connected";
55567
+ let action = "";
55568
+ if (!isActionable(runtime.id)) {
55569
+ action = " (copy snippet manually)";
55570
+ } else if (status.kind === "armed" && status.target.id === runtime.id) {
55571
+ action = ` [click again or press y to ${status.target.action}]`;
55572
+ } else if (status.kind === "running" && status.target.id === runtime.id) {
55573
+ action = ` ${status.target.action === "connect" ? "connecting\u2026" : "disconnecting\u2026"}`;
55574
+ } else if (status.kind === "done" && status.target.id === runtime.id) {
55575
+ action = status.outcome.ok ? " \u2713 done" : ` \u2717 ${status.outcome.message}`;
55576
+ } else {
55577
+ action = runtime.connected ? " [d] disconnect" : " [c] connect";
55578
+ }
55579
+ return `${mark} ${label} ${statusText}${action}`;
55580
+ }
55581
+ function asRowTarget(body) {
55582
+ const parent = body;
55583
+ if (parent.add === undefined || parent.getChildren === undefined || parent.remove === undefined) {
55584
+ return;
55585
+ }
55586
+ return {
55587
+ add: parent.add.bind(parent),
55588
+ getChildren: parent.getChildren.bind(parent),
55589
+ remove: parent.remove.bind(parent)
55590
+ };
55591
+ }
55592
+ function presentMcpTools(openModal2, otui, chrome, options) {
55593
+ const runtimes = options.runtimes.map((r) => ({ ...r }));
55594
+ let mcpSelected = 0;
55595
+ let toolsScroll = 0;
55596
+ let mcpScroll = 0;
55597
+ let status = { kind: "idle" };
55598
+ let toolsBody;
55599
+ let mcpBody;
55600
+ let rowCtor;
55601
+ let activeRenderer;
55602
+ let unsubscribeKey;
55603
+ const rendererHint = options.renderer ?? chrome?.renderer;
55604
+ const bodyRows = options.visibleRows ?? (typeof rendererHint?.width === "number" && typeof rendererHint.height === "number" ? modalBodyRows(resolveModalPanelSize(rendererHint.width, rendererHint.height).height) : 13);
55605
+ const paintToolsRows = () => {
55606
+ if (toolsBody === undefined || rowCtor === undefined) {
55607
+ return;
55608
+ }
55609
+ clearTranscriptChildren(toolsBody);
55610
+ if (options.tools.length === 0) {
55611
+ toolsBody.add(new rowCtor(activeRenderer, { id: "mcp-tools-empty", content: "No tools available." }));
55612
+ return;
55613
+ }
55614
+ const start = clampScroll3(toolsScroll, options.tools.length, bodyRows);
55615
+ for (const [i, tool] of options.tools.slice(start, start + bodyRows).entries()) {
55616
+ toolsBody.add(new rowCtor(activeRenderer, { id: `mcp-tool-row-${start + i}`, content: formatToolRowLine(tool) }));
55617
+ }
55618
+ };
55619
+ const paintMcpRows = () => {
55620
+ if (mcpBody === undefined || rowCtor === undefined) {
55621
+ return;
55622
+ }
55623
+ clearTranscriptChildren(mcpBody);
55624
+ if (runtimes.length === 0) {
55625
+ mcpBody.add(new rowCtor(activeRenderer, { id: "mcp-mcp-empty", content: "No MCP client runtimes registered." }));
55626
+ return;
55627
+ }
55628
+ const start = clampScroll3(mcpScroll, runtimes.length, bodyRows);
55629
+ for (const [i, runtime] of runtimes.slice(start, start + bodyRows).entries()) {
55630
+ const index = start + i;
55631
+ mcpBody.add(new rowCtor(activeRenderer, {
55632
+ id: `mcp-row-${runtime.id}`,
55633
+ content: formatMcpRowLine(runtime, index === mcpSelected, status),
55634
+ onMouseDown: () => handleRowClick(runtime.id, index)
55635
+ }));
55636
+ }
55637
+ };
55638
+ const paint = () => {
55639
+ toolsScroll = clampScroll3(toolsScroll, options.tools.length, bodyRows);
55640
+ mcpScroll = scrollToReveal3(mcpSelected, mcpScroll, bodyRows);
55641
+ mcpScroll = clampScroll3(mcpScroll, runtimes.length, bodyRows);
55642
+ paintToolsRows();
55643
+ paintMcpRows();
55644
+ };
55645
+ const moveMcpSelection = (next) => {
55646
+ if (runtimes.length === 0) {
55647
+ return;
55648
+ }
55649
+ const clamped = Math.min(runtimes.length - 1, Math.max(0, next));
55650
+ if (clamped === mcpSelected) {
55651
+ return;
55652
+ }
55653
+ mcpSelected = clamped;
55654
+ status = { kind: "idle" };
55655
+ paint();
55656
+ };
55657
+ const runAction = () => {
55658
+ if (status.kind !== "armed") {
55659
+ return;
55660
+ }
55661
+ const target = status.target;
55662
+ status = { kind: "running", target };
55663
+ paint();
55664
+ const fn = target.action === "connect" ? options.connect : options.disconnect;
55665
+ fn(target.id).then((outcome) => {
55666
+ status = { kind: "done", target, outcome };
55667
+ if (outcome.ok) {
55668
+ const row = runtimes.find((r) => r.id === target.id);
55669
+ if (row !== undefined) {
55670
+ row.connected = target.action === "connect";
55671
+ }
55672
+ }
55673
+ options.onStatusChange?.(runtimes);
55674
+ paint();
55675
+ });
55676
+ };
55677
+ const armFor = (id) => {
55678
+ const index = runtimes.findIndex((r) => r.id === id);
55679
+ const row = runtimes[index];
55680
+ if (index < 0 || row === undefined || !isActionable(row.id) || status.kind === "running") {
55681
+ return;
55682
+ }
55683
+ mcpSelected = index;
55684
+ status = { kind: "armed", target: { id: row.id, action: row.connected ? "disconnect" : "connect" } };
55685
+ paint();
55686
+ };
55687
+ const handleRowClick = (id, index) => {
55688
+ if (status.kind === "armed" && status.target.id === id) {
55689
+ runAction();
55690
+ return;
55691
+ }
55692
+ if (status.kind === "running") {
55693
+ return;
55694
+ }
55695
+ if (!isActionable(id)) {
55696
+ if (index !== mcpSelected) {
55697
+ mcpSelected = index;
55698
+ status = { kind: "idle" };
55699
+ paint();
55700
+ }
55701
+ return;
55702
+ }
55703
+ armFor(id);
55704
+ };
55705
+ const handle = openModal2(otui, chrome, {
55706
+ title: "Tools & MCP",
55707
+ tabs: [
55708
+ { id: "tools", label: "Tools" },
55709
+ { id: "mcp", label: "MCP" }
55710
+ ],
55711
+ initialTab: "tools",
55712
+ footer: MCP_INSPECTOR_FOOTER,
55713
+ renderTab: (tabId, body, ctx) => {
55714
+ const renderer = options.renderer ?? chrome?.renderer;
55715
+ const ctor = otui.TextRenderable;
55716
+ const target = asRowTarget(body);
55717
+ if (target === undefined || ctor === undefined) {
55718
+ return;
55719
+ }
55720
+ rowCtor = ctor;
55721
+ activeRenderer = renderer;
55722
+ if (tabId === "tools") {
55723
+ toolsBody = target;
55724
+ toolsScroll = clampScroll3(toolsScroll, options.tools.length, bodyRows);
55725
+ paintToolsRows();
55726
+ return;
55727
+ }
55728
+ mcpBody = target;
55729
+ mcpScroll = scrollToReveal3(mcpSelected, mcpScroll, bodyRows);
55730
+ paintMcpRows();
55731
+ },
55732
+ onClose: () => {
55733
+ unsubscribeKey?.();
55734
+ }
55735
+ });
55736
+ if (handle === undefined) {
55737
+ return;
55738
+ }
55739
+ if (options.onKeypress !== undefined) {
55740
+ unsubscribeKey = options.onKeypress((key) => {
55741
+ const token = key.name || key.sequence;
55742
+ const onMcp = handle.activeTab() === "mcp";
55743
+ if (onMcp && status.kind === "armed") {
55744
+ if (token === "y") {
55745
+ runAction();
55746
+ } else {
55747
+ status = { kind: "idle" };
55748
+ paint();
55749
+ }
55750
+ return;
55751
+ }
55752
+ if (onMcp && token === "c") {
55753
+ const row = runtimes[mcpSelected];
55754
+ if (row !== undefined && isActionable(row.id) && !row.connected && status.kind !== "running") {
55755
+ status = { kind: "armed", target: { id: row.id, action: "connect" } };
55756
+ paint();
55757
+ }
55758
+ return;
55759
+ }
55760
+ if (onMcp && token === "d") {
55761
+ const row = runtimes[mcpSelected];
55762
+ if (row !== undefined && isActionable(row.id) && row.connected && status.kind !== "running") {
55763
+ status = { kind: "armed", target: { id: row.id, action: "disconnect" } };
55764
+ paint();
55765
+ }
55766
+ return;
55767
+ }
55768
+ if (token === "up" || token === "k") {
55769
+ if (onMcp) {
55770
+ moveMcpSelection(mcpSelected - 1);
55771
+ } else {
55772
+ toolsScroll = clampScroll3(toolsScroll - 1, options.tools.length, bodyRows);
55773
+ paint();
55774
+ }
55775
+ return;
55776
+ }
55777
+ if (token === "down" || token === "j") {
55778
+ if (onMcp) {
55779
+ moveMcpSelection(mcpSelected + 1);
55780
+ } else {
55781
+ toolsScroll = clampScroll3(toolsScroll + 1, options.tools.length, bodyRows);
55782
+ paint();
55783
+ }
55784
+ return;
55785
+ }
55786
+ if (token === "pageup" || token === "pagedown") {
55787
+ const step = token === "pageup" ? -bodyRows : bodyRows;
55788
+ if (onMcp) {
55789
+ mcpScroll = clampScroll3(mcpScroll + step, runtimes.length, bodyRows);
55790
+ } else {
55791
+ toolsScroll = clampScroll3(toolsScroll + step, options.tools.length, bodyRows);
55792
+ }
55793
+ paint();
55794
+ }
55795
+ });
55796
+ }
55797
+ return handle;
55798
+ }
55799
+ function openMcpTools(otui, chrome, options) {
55800
+ return presentMcpTools((hostOtui, hostChrome, input2) => openModal(hostOtui, hostChrome, input2), otui, chrome, options);
55801
+ }
55458
55802
 
55459
55803
  // src/tui/tui-shell.ts
55460
55804
  init_slate();
@@ -55825,6 +56169,11 @@ var AGENT_SLASH_COMMANDS = [
55825
56169
  description: "Show project-wide items needing review (proposals, blocked sessions)",
55826
56170
  modes: AGENT_ONLY
55827
56171
  },
56172
+ {
56173
+ name: "/mcp",
56174
+ description: "Show available tools and MCP client connect status",
56175
+ modes: AGENT_ONLY
56176
+ },
55828
56177
  {
55829
56178
  name: "/compact",
55830
56179
  description: "Compact model context \u2014 /compact [focus] (archive kept)",
@@ -56403,6 +56752,10 @@ function onKeypress2(r, handler) {
56403
56752
  return () => r._internalKeyInput.offInternal("keypress", handler);
56404
56753
  }
56405
56754
  function showComposerChoice(otui, r, dock, request) {
56755
+ if (dock.visible === true) {
56756
+ request.onBusy?.();
56757
+ return Promise.resolve(request.cancelId);
56758
+ }
56406
56759
  return new Promise((resolve3) => {
56407
56760
  const options = request.options.map((o) => ({
56408
56761
  ...o,
@@ -60626,7 +60979,13 @@ async function launchTuiAgentShell(opts) {
60626
60979
  const sbContext = new otui.TextRenderable(r, { id: "sb-ctx-v", content: otui.t`${otui.dim("0 tokens")}` });
60627
60980
  sidebar.add(sbContext);
60628
60981
  sidebar.add(new otui.TextRenderable(r, { id: "sb-tools-k", content: otui.t`${otui.dim("Tools")}`, marginTop: 1 }));
60629
- sidebar.add(new otui.TextRenderable(r, { id: "sb-tools-v", content: otui.t`${otui.dim(`${deps.tools.length} available`)}` }));
60982
+ sidebar.add(new otui.TextRenderable(r, {
60983
+ id: "sb-tools-v",
60984
+ content: otui.t`${otui.dim(`${deps.tools.length} available`)}`,
60985
+ onMouseDown: () => {
60986
+ showTools();
60987
+ }
60988
+ }));
60630
60989
  sidebar.add(new otui.TextRenderable(r, { id: "sb-status-k", content: otui.t`${otui.dim("Status")}`, marginTop: 1 }));
60631
60990
  const sbWorkers = new otui.TextRenderable(r, {
60632
60991
  id: "sb-status-v",
@@ -61401,7 +61760,8 @@ Staying in the current session.
61401
61760
  openReview(otui, chrome, {
61402
61761
  items,
61403
61762
  acceptProposal: (item) => acceptProposalViaShell(makeCommandRunner(cwd), item.workspaceId, item.proposalId),
61404
- onAccepted: () => {
61763
+ declineProposal: (item) => declineProposalViaShell(makeCommandRunner(cwd), item.workspaceId, item.proposalId),
61764
+ onResolved: () => {
61405
61765
  refreshReviewSidebar();
61406
61766
  },
61407
61767
  renderer: r,
@@ -61409,6 +61769,38 @@ Staying in the current session.
61409
61769
  });
61410
61770
  })();
61411
61771
  };
61772
+ const showTools = () => {
61773
+ (async () => {
61774
+ const cwd = inspectorCwd();
61775
+ const runtimes = await mcpClientStatus(cwd, mcpRuntimeIds());
61776
+ openMcpTools(otui, chrome, {
61777
+ tools: deps.tools.map((t) => t.definition),
61778
+ runtimes,
61779
+ connect: async (id) => {
61780
+ try {
61781
+ const report = await installMcpClient(cwd, [id]);
61782
+ const outcome = report.outcomes[0];
61783
+ if (outcome !== undefined && outcome.errors.length > 0) {
61784
+ return { ok: false, message: outcome.errors.join("; ") };
61785
+ }
61786
+ return { ok: true };
61787
+ } catch (error2) {
61788
+ return { ok: false, message: error2 instanceof Error ? error2.message : String(error2) };
61789
+ }
61790
+ },
61791
+ disconnect: async (id) => {
61792
+ try {
61793
+ await uninstallMcpClient(cwd, [id]);
61794
+ return { ok: true };
61795
+ } catch (error2) {
61796
+ return { ok: false, message: error2 instanceof Error ? error2.message : String(error2) };
61797
+ }
61798
+ },
61799
+ renderer: r,
61800
+ ...inspectorKeys
61801
+ });
61802
+ })();
61803
+ };
61412
61804
  const runModeCommand = (line) => {
61413
61805
  const modeArgs = line.trim().split(/\s+/).slice(1).filter((p) => p.length > 0);
61414
61806
  const wanted = modeArgs[0] ?? "";
@@ -61416,6 +61808,7 @@ Staying in the current session.
61416
61808
  const applyMode = async (next) => {
61417
61809
  if (next === "auto") {
61418
61810
  chrome.hideMenu();
61811
+ let blockedByOpenDialog = false;
61419
61812
  const confirmId = await chrome.withOverlay(() => showComposerChoice(otui, r, chrome.dock, {
61420
61813
  title: "Switch to auto mode?",
61421
61814
  subtitle: "Skips confirmation for EVERY action, including destructive commands. Only credential-touching commands still ask.",
@@ -61423,9 +61816,16 @@ Staying in the current session.
61423
61816
  options: [
61424
61817
  { id: "confirm", label: "Confirm", description: "I understand the risk" },
61425
61818
  { id: "cancel", label: "Cancel", description: "Keep the current mode", recommended: true }
61426
- ]
61819
+ ],
61820
+ onBusy: () => {
61821
+ blockedByOpenDialog = true;
61822
+ chrome.showToast("Answer the open approval first, then retry /mode.");
61823
+ }
61427
61824
  }));
61428
61825
  input2.focus();
61826
+ if (blockedByOpenDialog) {
61827
+ return;
61828
+ }
61429
61829
  if (confirmId !== "confirm") {
61430
61830
  chrome.showToast("Cancelled \u2014 mode unchanged.");
61431
61831
  return;
@@ -61455,6 +61855,7 @@ Staying in the current session.
61455
61855
  const stored = getProjectPermissionMode(sessionCwd);
61456
61856
  chrome.hideMenu();
61457
61857
  (async () => {
61858
+ let blockedByOpenDialog = false;
61458
61859
  const id = await chrome.withOverlay(() => showComposerChoice(otui, r, chrome.dock, {
61459
61860
  title: `Permission mode (current: ${permissionMode})`,
61460
61861
  subtitle: stored !== undefined ? `Project default: ${stored}` : "No project default set.",
@@ -61464,9 +61865,16 @@ Staying in the current session.
61464
61865
  label: m,
61465
61866
  description: MODE_PICKER_DESCRIPTIONS[m],
61466
61867
  recommended: m === permissionMode
61467
- }))
61868
+ })),
61869
+ onBusy: () => {
61870
+ blockedByOpenDialog = true;
61871
+ chrome.showToast("Answer the open approval first, then retry /mode.");
61872
+ }
61468
61873
  }));
61469
61874
  input2.focus();
61875
+ if (blockedByOpenDialog) {
61876
+ return;
61877
+ }
61470
61878
  if (isPermissionMode(id) && id !== permissionMode) {
61471
61879
  await applyMode(id);
61472
61880
  }
@@ -61864,7 +62272,8 @@ Staying in the current session.
61864
62272
  isSessionInfo: isSessionInfoCommand(line),
61865
62273
  isFlows: isFlowsCommand(line),
61866
62274
  isWorkspace: isWorkspaceCommand(line),
61867
- isReview: isReviewCommand(line)
62275
+ isReview: isReviewCommand(line),
62276
+ isMcp: isMcpToolsCommand(line)
61868
62277
  });
61869
62278
  switch (decision) {
61870
62279
  case "exit": {
@@ -61972,6 +62381,10 @@ Staying in the current session.
61972
62381
  showReview();
61973
62382
  return;
61974
62383
  }
62384
+ case "mcp": {
62385
+ showTools();
62386
+ return;
62387
+ }
61975
62388
  case "deferred": {
61976
62389
  transcript.add(new otui.TextRenderable(r, {
61977
62390
  id: `c${uid++}`,
@@ -61995,6 +62408,7 @@ Staying in the current session.
61995
62408
  return;
61996
62409
  }
61997
62410
  (async () => {
62411
+ let blockedByOpenDialog = false;
61998
62412
  const chosen = await showComposerChoice(otui, r, chrome.dock, {
61999
62413
  title: "Main agent is busy",
62000
62414
  subtitle: line,
@@ -62002,8 +62416,17 @@ Staying in the current session.
62002
62416
  { id: "main", label: "Main queue", description: "queue for the main agent; remove/edit/force later", recommended: true },
62003
62417
  { id: "side", label: "Side-1", description: "read-only answer, outside main history (as before)" }
62004
62418
  ],
62005
- cancelId: "side"
62419
+ cancelId: "side",
62420
+ onBusy: () => {
62421
+ blockedByOpenDialog = true;
62422
+ chrome.showToast("Answer the open approval first, then resend.");
62423
+ }
62006
62424
  });
62425
+ if (blockedByOpenDialog) {
62426
+ input2.value = line;
62427
+ input2.focus();
62428
+ return;
62429
+ }
62007
62430
  if (chosen === "main") {
62008
62431
  const id = `mq${mainQueueSeq++}`;
62009
62432
  mainQueue.push({ id, question: line, displayQuestion: displayLine });
@@ -62182,6 +62605,10 @@ Staying in the current session.
62182
62605
  showReview();
62183
62606
  return;
62184
62607
  }
62608
+ if (isMcpToolsCommand(command.name)) {
62609
+ showTools();
62610
+ return;
62611
+ }
62185
62612
  if (command.name === "/copy") {
62186
62613
  const target = newestBlock();
62187
62614
  if (target === undefined || !copyBlock(target.id)) {
@@ -70868,7 +71295,7 @@ keryx workspace list-proposals [<workspace-id>]`);
70868
71295
  }
70869
71296
  function renderCatchUp(report, includeLifecycleFlags = true) {
70870
71297
  const sections = [];
70871
- sections.push(renderSection("Pending proposals", report.proposals, (item) => `- Accept, reject, or dismiss proposal ${item.proposalId} in workspace ${item.workspaceId}? ` + `Recommendation: ${item.fresh ? "evidence is fresh \u2014 review now (`keryx workspace review " + item.workspaceId + " " + item.proposalId + " --decision <accepted|rejected|dismissed>`)" : "evidence has drifted since this proposal was created \u2014 treat as stale, re-run wrap-up before deciding"}.`));
71298
+ sections.push(renderSection("Pending proposals", report.proposals, (item) => `- Accept, reject, or dismiss ${item.kind} proposal ${item.proposalId} in workspace ${item.workspaceId}` + `${item.note !== undefined ? `: "${item.note}"` : ""}? ` + `Recommendation: ${item.fresh ? "evidence is fresh \u2014 review now (`keryx workspace review " + item.workspaceId + " " + item.proposalId + " --decision <accepted|rejected|dismissed>`)" : "evidence has drifted since this proposal was created \u2014 treat as stale, re-run wrap-up before deciding"}.`));
70872
71299
  sections.push(renderSection("Blocked sessions (stopped unattended)", report.blocked, (item) => `- Session ${item.sessionId} stopped unattended (${item.terminalState.reason}) at ${item.terminalState.occurredAt}. Resume it, or archive and move on? ` + `Recommendation: \`keryx shell -r ${item.sessionId}\` to resume and unblock it.`));
70873
71300
  sections.push(renderSection("Unbound candidates (wrap-up ran, no workspace bound)", report.unboundCandidates, (item) => `- Session ${item.sessionId} produced untriaged seeds with no workspace bound (${item.summary}). Bind to a workspace and propose, or discard? ` + `Recommendation: pick a workspace, then \`keryx workspace propose <workspace-id> --kind <kind> --session ${item.sessionId}\` (evidence: ${item.evidencePath}).`));
70874
71301
  sections.push(renderSection("Unknown (no resolution recorded)", report.unknown, (item) => `- Session ${item.sessionId} was last seen ${item.lastSeenAt} with no proposal, terminal state, or unbound-candidate artifact recorded. Investigate, or ignore? ` + `Recommendation: \`keryx sessions list\` / \`keryx shell -r ${item.sessionId}\` to see what happened.`));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrciphersmith/keryx",
3
- "version": "0.2.50",
3
+ "version": "0.2.52",
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": {