@inbeat/standards 0.7.1 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.8.0](https://github.com/Genoux/ib-toolkit/compare/standards-v0.7.1...standards-v0.8.0) (2026-10-01)
4
+
5
+
6
+ ### Features
7
+
8
+ * **standards:** arrow-key picker for ib mcp ([#34](https://github.com/Genoux/ib-toolkit/issues/34)) ([5349a74](https://github.com/Genoux/ib-toolkit/commit/5349a74c0bd8f828c19c845b6c33414558f3f3d3))
9
+
3
10
  ## [0.7.1](https://github.com/Genoux/ib-toolkit/compare/standards-v0.7.0...standards-v0.7.1) (2026-10-01)
4
11
 
5
12
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inbeat/standards",
3
- "version": "0.7.1",
3
+ "version": "0.8.0",
4
4
  "description": "Agent standards for inBeat apps: managed AGENTS.md block, skills under .agents/skills, ib CLI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -31,7 +31,7 @@
31
31
  "test": "bun --bun vitest run"
32
32
  },
33
33
  "devDependencies": {
34
- "@inbeat/config": "^0.7.1",
34
+ "@inbeat/config": "^0.8.0",
35
35
  "vitest": "^4.1.1"
36
36
  },
37
37
  "repository": {
@@ -42,5 +42,8 @@
42
42
  "publishConfig": {
43
43
  "access": "public"
44
44
  },
45
- "license": "UNLICENSED"
45
+ "license": "UNLICENSED",
46
+ "dependencies": {
47
+ "@clack/prompts": "^1.8.1"
48
+ }
46
49
  }
package/src/cli.ts CHANGED
@@ -61,8 +61,6 @@ const commands: Record<string, () => number | Promise<number>> = {
61
61
  mcp: () =>
62
62
  runMcp(args, {
63
63
  cwd: process.cwd(),
64
- stdin: process.stdin,
65
- output: process.stdout,
66
64
  isTTY: Boolean(process.stdin.isTTY),
67
65
  out: console.info,
68
66
  err: console.error,
@@ -6,3 +6,12 @@ export const CURATED_SERVERS: Record<string, string> = {
6
6
  clerk: "https://mcp.clerk.com/mcp",
7
7
  cloudflare: "https://mcp.cloudflare.com/mcp",
8
8
  };
9
+
10
+ export const SERVER_HINTS: Record<string, string> = {
11
+ vercel: "Vercel projects and deployments",
12
+ github: "GitHub (read-only)",
13
+ sentry: "Sentry issues and errors",
14
+ neon: "Neon Postgres",
15
+ clerk: "Clerk auth docs and SDKs",
16
+ cloudflare: "Cloudflare API",
17
+ };
package/src/mcp.ts CHANGED
@@ -1,9 +1,8 @@
1
1
  import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
2
2
  import { dirname, join } from "node:path";
3
- import { createInterface } from "node:readline";
4
- import type { Readable, Writable } from "node:stream";
5
3
  import { isDeepStrictEqual } from "node:util";
6
- import { CURATED_SERVERS } from "./mcp-servers";
4
+ import * as clack from "@clack/prompts";
5
+ import { CURATED_SERVERS, SERVER_HINTS } from "./mcp-servers";
7
6
 
8
7
  type Entry = (url: string) => Record<string, string>;
9
8
  type Applied = { text: string; notes: string[] };
@@ -192,7 +191,6 @@ export const USAGE = [
192
191
  ].join("\n");
193
192
 
194
193
  class UsageError extends Error {}
195
- class Aborted extends Error {}
196
194
 
197
195
  const FLAG = /^--(tools|servers)(?:=(.*))?$/;
198
196
  const splitList = (value: string) =>
@@ -227,54 +225,42 @@ function validate(label: string, names: string[], valid: string[]) {
227
225
  if (problem) throw new UsageError(problem);
228
226
  }
229
227
 
228
+ export type Choice = { value: string; hint: string };
229
+ export type Prompts = {
230
+ intro: (title: string) => void;
231
+ outro: (message: string) => void;
232
+ cancel: (message: string) => void;
233
+ select: (request: {
234
+ message: string;
235
+ choices: Choice[];
236
+ initialValues: string[];
237
+ required: boolean;
238
+ }) => Promise<string[] | null>;
239
+ };
240
+
241
+ const clackPrompts: Prompts = {
242
+ intro: clack.intro,
243
+ outro: clack.outro,
244
+ cancel: clack.cancel,
245
+ select: async ({ message, choices, initialValues, required }) => {
246
+ const picked = await clack.multiselect({
247
+ message,
248
+ options: choices.map(({ value, hint }) => ({ value, label: value, hint })),
249
+ initialValues,
250
+ required,
251
+ });
252
+ return clack.isCancel(picked) ? null : picked;
253
+ },
254
+ };
255
+
230
256
  export type McpIo = {
231
257
  cwd: string;
232
- stdin: Readable;
233
- output: Writable;
234
258
  isTTY: boolean;
259
+ prompts?: Prompts;
235
260
  out: (text: string) => void;
236
261
  err: (text: string) => void;
237
262
  };
238
263
 
239
- const ATTEMPTS = 2;
240
- const PLAIN_INTEGER = /^\d+$/;
241
-
242
- function prompter(io: McpIo) {
243
- const lines = createInterface({ input: io.stdin, output: io.output, terminal: true });
244
- const nextLine = lines[Symbol.asyncIterator]();
245
- // The workspace resolves two @types/node majors; readline's Interface loses its EventEmitter methods.
246
- const emitter = lines as unknown as { once(event: "SIGINT", listener: () => void): void };
247
- const interrupted = new Promise<never>((_, reject) =>
248
- emitter.once("SIGINT", () => reject(new Aborted())),
249
- );
250
- interrupted.catch(() => {});
251
-
252
- const ask = async (label: string, valid: string[], preselected: string[]) => {
253
- let problem = "";
254
- for (let attempt = 0; attempt < ATTEMPTS; attempt++) {
255
- io.out(`${label}:`);
256
- for (const [index, name] of valid.entries())
257
- io.out(` ${index + 1}) [${preselected.includes(name) ? "x" : " "}] ${name}`);
258
- io.out(
259
- `numbers or names, comma-separated; Enter keeps: ${preselected.join(", ") || "none"}; "none" clears`,
260
- );
261
- const { value, done } = await Promise.race([nextLine.next(), interrupted]);
262
- if (done) throw new Aborted();
263
- const answer = value.trim();
264
- if (!answer) return preselected;
265
- if (answer === "none") return [];
266
- const names = splitList(answer).map((token) =>
267
- PLAIN_INTEGER.test(token) ? (valid[Number(token) - 1] ?? token) : token,
268
- );
269
- problem = unknownNames(label, names, valid) ?? "";
270
- if (!problem) return names;
271
- io.err(problem);
272
- }
273
- throw new UsageError(problem);
274
- };
275
- return { ask, close: () => lines.close() };
276
- }
277
-
278
264
  const readIfExists = (path: string) => (existsSync(path) ? readFileSync(path, "utf8") : null);
279
265
 
280
266
  function currentlyConfigured(cwd: string) {
@@ -333,7 +319,8 @@ export async function runMcp(args: string[], io: McpIo): Promise<number> {
333
319
  io.out(USAGE);
334
320
  return 0;
335
321
  }
336
- let prompt: ReturnType<typeof prompter> | undefined;
322
+ const prompts = io.prompts ?? clackPrompts;
323
+ let interactive = false;
337
324
  try {
338
325
  const { tools: toolFlag, servers: serverFlag } = parseMcpArgs(args);
339
326
  if (!io.isTTY && !(toolFlag && serverFlag)) {
@@ -348,10 +335,29 @@ export async function runMcp(args: string[], io: McpIo): Promise<number> {
348
335
  let tools = toolFlag;
349
336
  let servers = serverFlag;
350
337
  if (!tools || !servers) {
338
+ interactive = true;
339
+ prompts.intro("ib mcp");
351
340
  const configured = currentlyConfigured(io.cwd);
352
- prompt = prompter(io);
353
- tools ??= await prompt.ask("tool", toolNames, configured.tools);
354
- servers ??= await prompt.ask("server", curatedNames, configured.servers);
341
+ tools ??=
342
+ (await prompts.select({
343
+ message: "AI tools",
344
+ choices: toolNames.map((value) => ({ value, hint: TOOLS[value].file })),
345
+ initialValues: configured.tools,
346
+ required: true,
347
+ })) ?? undefined;
348
+ servers ??=
349
+ tools &&
350
+ ((await prompts.select({
351
+ message: "MCP servers",
352
+ choices: curatedNames.map((value) => ({ value, hint: SERVER_HINTS[value] })),
353
+ initialValues: configured.servers,
354
+ required: false,
355
+ })) ??
356
+ undefined);
357
+ if (!tools || !servers) {
358
+ prompts.cancel("aborted");
359
+ return 130;
360
+ }
355
361
  }
356
362
 
357
363
  const { planned, failures } = plan(io.cwd, tools, servers);
@@ -367,17 +373,13 @@ export async function runMcp(args: string[], io: McpIo): Promise<number> {
367
373
  ? `updated:\n ${changed.map(({ file }) => file).join("\n ")}`
368
374
  : "already up to date",
369
375
  );
370
- io.out("authenticate each server in your AI tool on first use");
376
+ const reminder = "authenticate each server in your AI tool on first use";
377
+ if (interactive) prompts.outro(reminder);
378
+ else io.out(reminder);
371
379
  return 0;
372
380
  } catch (error) {
373
- if (error instanceof Aborted) {
374
- io.err("aborted");
375
- return 130;
376
- }
377
381
  const message = error instanceof Error ? error.message : String(error);
378
382
  io.err(error instanceof UsageError ? `${message}\n${USAGE}` : message);
379
383
  return 1;
380
- } finally {
381
- prompt?.close();
382
384
  }
383
385
  }