@mrciphersmith/keryx 0.2.50 → 0.2.51

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 +403 -32
  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.51",
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: {
@@ -54231,6 +54262,7 @@ async function computeLifecycleFlags(cwd, now = () => new Date) {
54231
54262
  }
54232
54263
 
54233
54264
  // src/sac/catch-up.ts
54265
+ init_proposal_evidence();
54234
54266
  async function buildCatchUp(input2) {
54235
54267
  const [proposals, sessionCategories, lifecycleFlagsAll] = await Promise.all([
54236
54268
  collectProposals(input2.cwd, input2.workspaceId),
@@ -54250,8 +54282,20 @@ async function collectProposals(cwd, workspaceId) {
54250
54282
  const scoped = workspaceId === undefined ? groups : groups.filter((group) => group.workspace.id === workspaceId);
54251
54283
  const flattened = scoped.flatMap((group) => group.proposals.map((proposal) => ({ group, proposal })));
54252
54284
  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 };
54285
+ const [fresh, note2] = await Promise.all([
54286
+ proposalService.isEvidenceFresh(proposal, actor),
54287
+ readSidecarNote(cwd, group.workspace.id, proposal.id)
54288
+ ]);
54289
+ return {
54290
+ type: "proposal",
54291
+ workspaceId: group.workspace.id,
54292
+ proposalId: proposal.id,
54293
+ fresh,
54294
+ kind: proposal.kind,
54295
+ author: proposal.author,
54296
+ createdAt: proposal.createdAt,
54297
+ note: note2
54298
+ };
54255
54299
  }));
54256
54300
  }
54257
54301
  async function classifySession(session) {
@@ -54822,7 +54866,7 @@ function openFlows(otui, chrome, options) {
54822
54866
 
54823
54867
  // src/tui/busy-dispatch.ts
54824
54868
  function classifyBusyDispatch(params) {
54825
- const { line, commandName, isSessionInfo, isFlows, isWorkspace, isReview } = params;
54869
+ const { line, commandName, isSessionInfo, isFlows, isWorkspace, isReview, isMcp } = params;
54826
54870
  if (commandName === "/exit")
54827
54871
  return "exit";
54828
54872
  if (commandName === "/help")
@@ -54841,7 +54885,7 @@ function classifyBusyDispatch(params) {
54841
54885
  return "copy";
54842
54886
  if (commandName === "/mode")
54843
54887
  return "mode";
54844
- const isBusyReadonlyCommand = isSessionInfo || isFlows || isWorkspace || isReview;
54888
+ const isBusyReadonlyCommand = isSessionInfo || isFlows || isWorkspace || isReview || isMcp;
54845
54889
  if (isBusyReadonlyCommand && isSessionInfo)
54846
54890
  return "session-info";
54847
54891
  if (isBusyReadonlyCommand && isFlows)
@@ -54850,6 +54894,8 @@ function classifyBusyDispatch(params) {
54850
54894
  return "workspace";
54851
54895
  if (isBusyReadonlyCommand && isReview)
54852
54896
  return "review";
54897
+ if (isBusyReadonlyCommand && isMcp)
54898
+ return "mcp";
54853
54899
  if (commandName !== undefined || line.startsWith("/"))
54854
54900
  return "deferred";
54855
54901
  return "not-a-command";
@@ -55109,7 +55155,8 @@ function openWorkspace(otui, chrome, options) {
55109
55155
  var REVIEW_COMMAND = "/review";
55110
55156
  var REVIEW_FOOTER = [
55111
55157
  { key: "[/]", label: "item" },
55112
- { key: "a y", label: "accept" },
55158
+ { key: "a y", label: "accept proposal" },
55159
+ { key: "d y", label: "decline proposal" },
55113
55160
  { key: "\u2191/\u2193", label: "scroll" },
55114
55161
  { key: "\u2190/\u2192", label: "tabs" },
55115
55162
  { key: "esc", label: "close" }
@@ -55127,7 +55174,7 @@ var TYPE_LABEL = {
55127
55174
  function summarizeReviewItem(item) {
55128
55175
  switch (item.type) {
55129
55176
  case "proposal":
55130
- return `${item.proposalId} in ${item.workspaceId}${item.fresh ? "" : " (stale)"}`;
55177
+ return `${item.kind} ${item.proposalId} in ${item.workspaceId}${item.fresh ? "" : " (stale)"}`;
55131
55178
  case "blocked":
55132
55179
  return `${item.sessionId} \u2014 ${item.terminalState.reason}`;
55133
55180
  case "unbound-candidate":
@@ -55151,9 +55198,13 @@ function describeReviewItem(item) {
55151
55198
  return [
55152
55199
  `Proposal ${item.proposalId}`,
55153
55200
  `Workspace ${item.workspaceId}`,
55201
+ `Kind ${item.kind}`,
55202
+ `Author ${item.author}`,
55203
+ `Created ${item.createdAt}`,
55154
55204
  `Evidence ${item.fresh ? "fresh" : "stale \u2014 evidence has drifted since this proposal was created; re-run wrap-up before deciding"}`,
55205
+ ...item.note !== undefined ? ["", `Note ${item.note}`] : [],
55155
55206
  "",
55156
- `Reject/dismiss from a terminal: keryx workspace review ${item.workspaceId} ${item.proposalId} --decision <rejected|dismissed>`
55207
+ `Dismiss (archive with no decision) from a terminal: keryx workspace review ${item.workspaceId} ${item.proposalId} --decision dismissed`
55157
55208
  ];
55158
55209
  case "blocked":
55159
55210
  return [
@@ -55206,25 +55257,37 @@ function describeGroupOutcome(g) {
55206
55257
  }
55207
55258
  }
55208
55259
  }
55260
+ var DECISION_VERB = { accept: "Accept", decline: "Decline" };
55261
+ var DECISION_ING = { accept: "Accepting", decline: "Declining" };
55262
+ var DECISION_DONE = { accept: "Accepted", decline: "Declined" };
55263
+ var DECISION_COMMAND = {
55264
+ accept: "running `keryx workspace confirm-review` then `keryx workspace review`",
55265
+ decline: "running `keryx workspace review --decision rejected`"
55266
+ };
55209
55267
  function formatReviewDetailLines(item, status) {
55210
55268
  if (item === undefined) {
55211
55269
  return ["No item selected.", "", "Press Enter (or click a row) on the Review tab to view one."];
55212
55270
  }
55213
55271
  const lines = describeReviewItem(item);
55214
55272
  if (item.type !== "proposal") {
55273
+ if (status.kind === "unavailable") {
55274
+ return [...lines, "", `[${status.decision === "accept" ? "a" : "d"}] does nothing here \u2014 accept/decline only apply to a pending proposal, not to this item.`];
55275
+ }
55215
55276
  return lines;
55216
55277
  }
55217
55278
  const withAction = [...lines, ""];
55218
55279
  if (status.kind === "armed") {
55219
- withAction.push("Press [y] to CONFIRM accept, any other key cancels.");
55280
+ withAction.push(`Press [y] to CONFIRM ${status.decision}, any other key cancels.`);
55220
55281
  } else if (status.kind === "running") {
55221
- withAction.push("Accepting\u2026 running `keryx workspace confirm-review` then `keryx workspace review`.");
55282
+ withAction.push(`${DECISION_ING[status.decision]}\u2026 ${DECISION_COMMAND[status.decision]}.`);
55222
55283
  } else if (status.kind === "done" && status.outcome.ok) {
55223
- withAction.push("\u2713 Accepted.");
55284
+ withAction.push(`\u2713 ${DECISION_DONE[status.decision]}.`);
55224
55285
  } else if (status.kind === "done" && !status.outcome.ok) {
55225
- withAction.push(`\u2717 Accept failed: ${status.outcome.message}`);
55286
+ withAction.push(`\u2717 ${DECISION_VERB[status.decision]} failed: ${status.outcome.message}`);
55287
+ } else if (status.kind === "unavailable") {
55288
+ withAction.push(`[${status.decision === "accept" ? "a" : "d"}] does nothing \u2014 no ${status.decision} handler is configured for this modal.`);
55226
55289
  } else {
55227
- withAction.push("[a] Accept this proposal");
55290
+ withAction.push("[a] Accept this proposal [d] Decline this proposal");
55228
55291
  }
55229
55292
  return withAction;
55230
55293
  }
@@ -55321,17 +55384,19 @@ function presentReview(openModal2, otui, chrome, options) {
55321
55384
  status = { kind: "idle" };
55322
55385
  paintSelection();
55323
55386
  };
55324
- const runAccept = () => {
55387
+ const handlerFor = (decision) => decision === "accept" ? options.acceptProposal : options.declineProposal;
55388
+ const runDecision = (decision) => {
55325
55389
  const item = items[selected];
55326
- if (item === undefined || item.type !== "proposal" || options.acceptProposal === undefined) {
55390
+ const run = handlerFor(decision);
55391
+ if (item === undefined || item.type !== "proposal" || run === undefined) {
55327
55392
  return;
55328
55393
  }
55329
- status = { kind: "running" };
55394
+ status = { kind: "running", decision };
55330
55395
  paintSelection();
55331
- options.acceptProposal(item).then((outcome) => {
55332
- status = { kind: "done", outcome };
55396
+ run(item).then((outcome) => {
55397
+ status = { kind: "done", decision, outcome };
55333
55398
  if (outcome.ok) {
55334
- options.onAccepted?.(item);
55399
+ options.onResolved?.(item);
55335
55400
  }
55336
55401
  paintSelection();
55337
55402
  });
@@ -55371,7 +55436,7 @@ function presentReview(openModal2, otui, chrome, options) {
55371
55436
  const onDetail = handle.activeTab() === "detail";
55372
55437
  if (onDetail && status.kind === "armed") {
55373
55438
  if (token === "y") {
55374
- runAccept();
55439
+ runDecision(status.decision);
55375
55440
  } else {
55376
55441
  status = { kind: "idle" };
55377
55442
  paintSelection();
@@ -55390,8 +55455,9 @@ function presentReview(openModal2, otui, chrome, options) {
55390
55455
  handle.setTab("detail");
55391
55456
  return;
55392
55457
  }
55393
- if (onDetail && token === "a" && items[selected]?.type === "proposal" && options.acceptProposal !== undefined && status.kind !== "running") {
55394
- status = { kind: "armed" };
55458
+ if (onDetail && (token === "a" || token === "d") && status.kind !== "running") {
55459
+ const decision = token === "a" ? "accept" : "decline";
55460
+ status = items[selected]?.type === "proposal" && handlerFor(decision) !== undefined ? { kind: "armed", decision } : { kind: "unavailable", decision };
55395
55461
  paintSelection();
55396
55462
  return;
55397
55463
  }
@@ -55455,6 +55521,228 @@ async function acceptProposalViaShell(run, workspaceId, proposalId) {
55455
55521
  }
55456
55522
  return { ok: true };
55457
55523
  }
55524
+ async function declineProposalViaShell(run, workspaceId, proposalId) {
55525
+ const decline = await run(`keryx workspace review ${shQuote(workspaceId)} ${shQuote(proposalId)} --decision rejected`);
55526
+ if (decline.isError) {
55527
+ return { ok: false, message: decline.output };
55528
+ }
55529
+ return { ok: true };
55530
+ }
55531
+
55532
+ // src/tui/mcp-inspector.ts
55533
+ var MCP_INSPECTOR_FOOTER = [
55534
+ { key: "\u2191/\u2193", label: "select" },
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 formatToolsListLines(tools) {
55556
+ if (tools.length === 0) {
55557
+ return ["No tools available."];
55558
+ }
55559
+ return tools.map((tool) => {
55560
+ const risk = (tool.risk ?? "read").padEnd(6);
55561
+ const name = tool.name.padEnd(28);
55562
+ return `${name} ${risk} ${tool.description ?? ""}`.trimEnd();
55563
+ });
55564
+ }
55565
+ function isActionable(id) {
55566
+ return id !== "generic";
55567
+ }
55568
+ function formatMcpListLines(runtimes, selected, status) {
55569
+ if (runtimes.length === 0) {
55570
+ return ["No MCP client runtimes registered."];
55571
+ }
55572
+ return runtimes.map((runtime, index) => {
55573
+ const mark = index === selected ? ">" : " ";
55574
+ const label = runtimeLabel(runtime.id).padEnd(20);
55575
+ const statusText = runtime.connected ? "\u25CF connected" : "\u25CB not connected";
55576
+ let action = "";
55577
+ if (!isActionable(runtime.id)) {
55578
+ action = " (copy snippet manually)";
55579
+ } else if (status.kind === "armed" && status.target.id === runtime.id) {
55580
+ action = ` [press y to ${status.target.action}]`;
55581
+ } else if (status.kind === "running" && status.target.id === runtime.id) {
55582
+ action = ` ${status.target.action === "connect" ? "connecting\u2026" : "disconnecting\u2026"}`;
55583
+ } else if (status.kind === "done" && status.target.id === runtime.id) {
55584
+ action = status.outcome.ok ? " \u2713 done" : ` \u2717 ${status.outcome.message}`;
55585
+ } else {
55586
+ action = runtime.connected ? " [d] disconnect" : " [c] connect";
55587
+ }
55588
+ return `${mark} ${label} ${statusText}${action}`;
55589
+ });
55590
+ }
55591
+ function presentMcpTools(openModal2, otui, chrome, options) {
55592
+ const runtimes = options.runtimes.map((r) => ({ ...r }));
55593
+ let mcpSelected = 0;
55594
+ let toolsScroll = 0;
55595
+ let mcpScroll = 0;
55596
+ let status = { kind: "idle" };
55597
+ let toolsNode;
55598
+ let mcpNode;
55599
+ let unsubscribeKey;
55600
+ const rendererHint = options.renderer ?? chrome?.renderer;
55601
+ const bodyRows = options.visibleRows ?? (typeof rendererHint?.width === "number" && typeof rendererHint.height === "number" ? modalBodyRows(resolveModalPanelSize(rendererHint.width, rendererHint.height).height) : 13);
55602
+ const toolLines = () => formatToolsListLines(options.tools);
55603
+ const mcpLines = () => formatMcpListLines(runtimes, mcpSelected, status);
55604
+ const paint = () => {
55605
+ toolsScroll = clampScroll3(toolsScroll, toolLines().length, bodyRows);
55606
+ mcpScroll = scrollToReveal3(mcpSelected, mcpScroll, bodyRows);
55607
+ mcpScroll = clampScroll3(mcpScroll, mcpLines().length, bodyRows);
55608
+ if (toolsNode !== undefined) {
55609
+ toolsNode.content = windowLines3(toolLines(), toolsScroll, bodyRows).join(`
55610
+ `);
55611
+ }
55612
+ if (mcpNode !== undefined) {
55613
+ mcpNode.content = windowLines3(mcpLines(), mcpScroll, bodyRows).join(`
55614
+ `);
55615
+ }
55616
+ };
55617
+ const moveMcpSelection = (next) => {
55618
+ if (runtimes.length === 0) {
55619
+ return;
55620
+ }
55621
+ const clamped = Math.min(runtimes.length - 1, Math.max(0, next));
55622
+ if (clamped === mcpSelected) {
55623
+ return;
55624
+ }
55625
+ mcpSelected = clamped;
55626
+ status = { kind: "idle" };
55627
+ paint();
55628
+ };
55629
+ const runAction = () => {
55630
+ if (status.kind !== "armed") {
55631
+ return;
55632
+ }
55633
+ const target = status.target;
55634
+ status = { kind: "running", target };
55635
+ paint();
55636
+ const fn = target.action === "connect" ? options.connect : options.disconnect;
55637
+ fn(target.id).then((outcome) => {
55638
+ status = { kind: "done", target, outcome };
55639
+ if (outcome.ok) {
55640
+ const row = runtimes.find((r) => r.id === target.id);
55641
+ if (row !== undefined) {
55642
+ row.connected = target.action === "connect";
55643
+ }
55644
+ }
55645
+ options.onStatusChange?.(runtimes);
55646
+ paint();
55647
+ });
55648
+ };
55649
+ const handle = openModal2(otui, chrome, {
55650
+ title: "Tools & MCP",
55651
+ tabs: [
55652
+ { id: "tools", label: "Tools" },
55653
+ { id: "mcp", label: "MCP" }
55654
+ ],
55655
+ initialTab: "tools",
55656
+ footer: MCP_INSPECTOR_FOOTER,
55657
+ renderTab: (tabId, body, ctx) => {
55658
+ const renderer = options.renderer ?? chrome?.renderer;
55659
+ const parent = body;
55660
+ const ctor = otui.TextRenderable;
55661
+ if (parent.add === undefined || ctor === undefined) {
55662
+ return;
55663
+ }
55664
+ if (tabId === "tools") {
55665
+ toolsScroll = clampScroll3(toolsScroll, toolLines().length, bodyRows);
55666
+ toolsNode = new ctor(renderer, { id: "mcp-tools-body", content: windowLines3(toolLines(), toolsScroll, bodyRows).join(`
55667
+ `) });
55668
+ parent.add(toolsNode);
55669
+ return;
55670
+ }
55671
+ mcpScroll = scrollToReveal3(mcpSelected, mcpScroll, bodyRows);
55672
+ mcpNode = new ctor(renderer, { id: "mcp-mcp-body", content: windowLines3(mcpLines(), mcpScroll, bodyRows).join(`
55673
+ `) });
55674
+ parent.add(mcpNode);
55675
+ },
55676
+ onClose: () => {
55677
+ unsubscribeKey?.();
55678
+ }
55679
+ });
55680
+ if (handle === undefined) {
55681
+ return;
55682
+ }
55683
+ if (options.onKeypress !== undefined) {
55684
+ unsubscribeKey = options.onKeypress((key) => {
55685
+ const token = key.name || key.sequence;
55686
+ const onMcp = handle.activeTab() === "mcp";
55687
+ if (onMcp && status.kind === "armed") {
55688
+ if (token === "y") {
55689
+ runAction();
55690
+ } else {
55691
+ status = { kind: "idle" };
55692
+ paint();
55693
+ }
55694
+ return;
55695
+ }
55696
+ if (onMcp && token === "c") {
55697
+ const row = runtimes[mcpSelected];
55698
+ if (row !== undefined && isActionable(row.id) && !row.connected && status.kind !== "running") {
55699
+ status = { kind: "armed", target: { id: row.id, action: "connect" } };
55700
+ paint();
55701
+ }
55702
+ return;
55703
+ }
55704
+ if (onMcp && token === "d") {
55705
+ const row = runtimes[mcpSelected];
55706
+ if (row !== undefined && isActionable(row.id) && row.connected && status.kind !== "running") {
55707
+ status = { kind: "armed", target: { id: row.id, action: "disconnect" } };
55708
+ paint();
55709
+ }
55710
+ return;
55711
+ }
55712
+ if (token === "up" || token === "k") {
55713
+ if (onMcp) {
55714
+ moveMcpSelection(mcpSelected - 1);
55715
+ } else {
55716
+ toolsScroll = clampScroll3(toolsScroll - 1, toolLines().length, bodyRows);
55717
+ paint();
55718
+ }
55719
+ return;
55720
+ }
55721
+ if (token === "down" || token === "j") {
55722
+ if (onMcp) {
55723
+ moveMcpSelection(mcpSelected + 1);
55724
+ } else {
55725
+ toolsScroll = clampScroll3(toolsScroll + 1, toolLines().length, bodyRows);
55726
+ paint();
55727
+ }
55728
+ return;
55729
+ }
55730
+ if (token === "pageup" || token === "pagedown") {
55731
+ const step = token === "pageup" ? -bodyRows : bodyRows;
55732
+ if (onMcp) {
55733
+ mcpScroll = clampScroll3(mcpScroll + step, mcpLines().length, bodyRows);
55734
+ } else {
55735
+ toolsScroll = clampScroll3(toolsScroll + step, toolLines().length, bodyRows);
55736
+ }
55737
+ paint();
55738
+ }
55739
+ });
55740
+ }
55741
+ return handle;
55742
+ }
55743
+ function openMcpTools(otui, chrome, options) {
55744
+ return presentMcpTools((hostOtui, hostChrome, input2) => openModal(hostOtui, hostChrome, input2), otui, chrome, options);
55745
+ }
55458
55746
 
55459
55747
  // src/tui/tui-shell.ts
55460
55748
  init_slate();
@@ -55825,6 +56113,11 @@ var AGENT_SLASH_COMMANDS = [
55825
56113
  description: "Show project-wide items needing review (proposals, blocked sessions)",
55826
56114
  modes: AGENT_ONLY
55827
56115
  },
56116
+ {
56117
+ name: "/mcp",
56118
+ description: "Show available tools and MCP client connect status",
56119
+ modes: AGENT_ONLY
56120
+ },
55828
56121
  {
55829
56122
  name: "/compact",
55830
56123
  description: "Compact model context \u2014 /compact [focus] (archive kept)",
@@ -56403,6 +56696,10 @@ function onKeypress2(r, handler) {
56403
56696
  return () => r._internalKeyInput.offInternal("keypress", handler);
56404
56697
  }
56405
56698
  function showComposerChoice(otui, r, dock, request) {
56699
+ if (dock.visible === true) {
56700
+ request.onBusy?.();
56701
+ return Promise.resolve(request.cancelId);
56702
+ }
56406
56703
  return new Promise((resolve3) => {
56407
56704
  const options = request.options.map((o) => ({
56408
56705
  ...o,
@@ -60626,7 +60923,13 @@ async function launchTuiAgentShell(opts) {
60626
60923
  const sbContext = new otui.TextRenderable(r, { id: "sb-ctx-v", content: otui.t`${otui.dim("0 tokens")}` });
60627
60924
  sidebar.add(sbContext);
60628
60925
  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`)}` }));
60926
+ sidebar.add(new otui.TextRenderable(r, {
60927
+ id: "sb-tools-v",
60928
+ content: otui.t`${otui.dim(`${deps.tools.length} available`)}`,
60929
+ onMouseDown: () => {
60930
+ showTools();
60931
+ }
60932
+ }));
60630
60933
  sidebar.add(new otui.TextRenderable(r, { id: "sb-status-k", content: otui.t`${otui.dim("Status")}`, marginTop: 1 }));
60631
60934
  const sbWorkers = new otui.TextRenderable(r, {
60632
60935
  id: "sb-status-v",
@@ -61401,7 +61704,8 @@ Staying in the current session.
61401
61704
  openReview(otui, chrome, {
61402
61705
  items,
61403
61706
  acceptProposal: (item) => acceptProposalViaShell(makeCommandRunner(cwd), item.workspaceId, item.proposalId),
61404
- onAccepted: () => {
61707
+ declineProposal: (item) => declineProposalViaShell(makeCommandRunner(cwd), item.workspaceId, item.proposalId),
61708
+ onResolved: () => {
61405
61709
  refreshReviewSidebar();
61406
61710
  },
61407
61711
  renderer: r,
@@ -61409,6 +61713,38 @@ Staying in the current session.
61409
61713
  });
61410
61714
  })();
61411
61715
  };
61716
+ const showTools = () => {
61717
+ (async () => {
61718
+ const cwd = inspectorCwd();
61719
+ const runtimes = await mcpClientStatus(cwd, mcpRuntimeIds());
61720
+ openMcpTools(otui, chrome, {
61721
+ tools: deps.tools.map((t) => t.definition),
61722
+ runtimes,
61723
+ connect: async (id) => {
61724
+ try {
61725
+ const report = await installMcpClient(cwd, [id]);
61726
+ const outcome = report.outcomes[0];
61727
+ if (outcome !== undefined && outcome.errors.length > 0) {
61728
+ return { ok: false, message: outcome.errors.join("; ") };
61729
+ }
61730
+ return { ok: true };
61731
+ } catch (error2) {
61732
+ return { ok: false, message: error2 instanceof Error ? error2.message : String(error2) };
61733
+ }
61734
+ },
61735
+ disconnect: async (id) => {
61736
+ try {
61737
+ await uninstallMcpClient(cwd, [id]);
61738
+ return { ok: true };
61739
+ } catch (error2) {
61740
+ return { ok: false, message: error2 instanceof Error ? error2.message : String(error2) };
61741
+ }
61742
+ },
61743
+ renderer: r,
61744
+ ...inspectorKeys
61745
+ });
61746
+ })();
61747
+ };
61412
61748
  const runModeCommand = (line) => {
61413
61749
  const modeArgs = line.trim().split(/\s+/).slice(1).filter((p) => p.length > 0);
61414
61750
  const wanted = modeArgs[0] ?? "";
@@ -61416,6 +61752,7 @@ Staying in the current session.
61416
61752
  const applyMode = async (next) => {
61417
61753
  if (next === "auto") {
61418
61754
  chrome.hideMenu();
61755
+ let blockedByOpenDialog = false;
61419
61756
  const confirmId = await chrome.withOverlay(() => showComposerChoice(otui, r, chrome.dock, {
61420
61757
  title: "Switch to auto mode?",
61421
61758
  subtitle: "Skips confirmation for EVERY action, including destructive commands. Only credential-touching commands still ask.",
@@ -61423,9 +61760,16 @@ Staying in the current session.
61423
61760
  options: [
61424
61761
  { id: "confirm", label: "Confirm", description: "I understand the risk" },
61425
61762
  { id: "cancel", label: "Cancel", description: "Keep the current mode", recommended: true }
61426
- ]
61763
+ ],
61764
+ onBusy: () => {
61765
+ blockedByOpenDialog = true;
61766
+ chrome.showToast("Answer the open approval first, then retry /mode.");
61767
+ }
61427
61768
  }));
61428
61769
  input2.focus();
61770
+ if (blockedByOpenDialog) {
61771
+ return;
61772
+ }
61429
61773
  if (confirmId !== "confirm") {
61430
61774
  chrome.showToast("Cancelled \u2014 mode unchanged.");
61431
61775
  return;
@@ -61455,6 +61799,7 @@ Staying in the current session.
61455
61799
  const stored = getProjectPermissionMode(sessionCwd);
61456
61800
  chrome.hideMenu();
61457
61801
  (async () => {
61802
+ let blockedByOpenDialog = false;
61458
61803
  const id = await chrome.withOverlay(() => showComposerChoice(otui, r, chrome.dock, {
61459
61804
  title: `Permission mode (current: ${permissionMode})`,
61460
61805
  subtitle: stored !== undefined ? `Project default: ${stored}` : "No project default set.",
@@ -61464,9 +61809,16 @@ Staying in the current session.
61464
61809
  label: m,
61465
61810
  description: MODE_PICKER_DESCRIPTIONS[m],
61466
61811
  recommended: m === permissionMode
61467
- }))
61812
+ })),
61813
+ onBusy: () => {
61814
+ blockedByOpenDialog = true;
61815
+ chrome.showToast("Answer the open approval first, then retry /mode.");
61816
+ }
61468
61817
  }));
61469
61818
  input2.focus();
61819
+ if (blockedByOpenDialog) {
61820
+ return;
61821
+ }
61470
61822
  if (isPermissionMode(id) && id !== permissionMode) {
61471
61823
  await applyMode(id);
61472
61824
  }
@@ -61864,7 +62216,8 @@ Staying in the current session.
61864
62216
  isSessionInfo: isSessionInfoCommand(line),
61865
62217
  isFlows: isFlowsCommand(line),
61866
62218
  isWorkspace: isWorkspaceCommand(line),
61867
- isReview: isReviewCommand(line)
62219
+ isReview: isReviewCommand(line),
62220
+ isMcp: isMcpToolsCommand(line)
61868
62221
  });
61869
62222
  switch (decision) {
61870
62223
  case "exit": {
@@ -61972,6 +62325,10 @@ Staying in the current session.
61972
62325
  showReview();
61973
62326
  return;
61974
62327
  }
62328
+ case "mcp": {
62329
+ showTools();
62330
+ return;
62331
+ }
61975
62332
  case "deferred": {
61976
62333
  transcript.add(new otui.TextRenderable(r, {
61977
62334
  id: `c${uid++}`,
@@ -61995,6 +62352,7 @@ Staying in the current session.
61995
62352
  return;
61996
62353
  }
61997
62354
  (async () => {
62355
+ let blockedByOpenDialog = false;
61998
62356
  const chosen = await showComposerChoice(otui, r, chrome.dock, {
61999
62357
  title: "Main agent is busy",
62000
62358
  subtitle: line,
@@ -62002,8 +62360,17 @@ Staying in the current session.
62002
62360
  { id: "main", label: "Main queue", description: "queue for the main agent; remove/edit/force later", recommended: true },
62003
62361
  { id: "side", label: "Side-1", description: "read-only answer, outside main history (as before)" }
62004
62362
  ],
62005
- cancelId: "side"
62363
+ cancelId: "side",
62364
+ onBusy: () => {
62365
+ blockedByOpenDialog = true;
62366
+ chrome.showToast("Answer the open approval first, then resend.");
62367
+ }
62006
62368
  });
62369
+ if (blockedByOpenDialog) {
62370
+ input2.value = line;
62371
+ input2.focus();
62372
+ return;
62373
+ }
62007
62374
  if (chosen === "main") {
62008
62375
  const id = `mq${mainQueueSeq++}`;
62009
62376
  mainQueue.push({ id, question: line, displayQuestion: displayLine });
@@ -62182,6 +62549,10 @@ Staying in the current session.
62182
62549
  showReview();
62183
62550
  return;
62184
62551
  }
62552
+ if (isMcpToolsCommand(command.name)) {
62553
+ showTools();
62554
+ return;
62555
+ }
62185
62556
  if (command.name === "/copy") {
62186
62557
  const target = newestBlock();
62187
62558
  if (target === undefined || !copyBlock(target.id)) {
@@ -70868,7 +71239,7 @@ keryx workspace list-proposals [<workspace-id>]`);
70868
71239
  }
70869
71240
  function renderCatchUp(report, includeLifecycleFlags = true) {
70870
71241
  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"}.`));
71242
+ 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
71243
  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
71244
  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
71245
  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.51",
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": {