@inbeat/standards 0.7.1 → 0.9.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 +19 -0
- package/agents/standard.md +1 -0
- package/package.json +6 -3
- package/skills/ib-toolkit/SKILL.md +1 -1
- package/src/cli.ts +0 -2
- package/src/mcp-servers.ts +8 -1
- package/src/mcp.ts +58 -56
- package/template/.agents/skills/ib-toolkit/SKILL.md +1 -1
- package/template/.github/workflows/ci.yml +8 -2
- package/template/AGENTS.md +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.9.0](https://github.com/Genoux/ib-toolkit/compare/standards-v0.8.0...standards-v0.9.0) (2026-10-02)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **standards:** gate production on vercel deployment checks ([#39](https://github.com/Genoux/ib-toolkit/issues/39)) ([98effa0](https://github.com/Genoux/ib-toolkit/commit/98effa011a8704b8ec53c334867a5ac2676b28e9))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* **standards:** drop github from the curated mcp servers ([#37](https://github.com/Genoux/ib-toolkit/issues/37)) ([093a632](https://github.com/Genoux/ib-toolkit/commit/093a6325033de84bb264ab9b42d37cbef5d4c3bb))
|
|
14
|
+
|
|
15
|
+
## [0.8.0](https://github.com/Genoux/ib-toolkit/compare/standards-v0.7.1...standards-v0.8.0) (2026-10-01)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Features
|
|
19
|
+
|
|
20
|
+
* **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))
|
|
21
|
+
|
|
3
22
|
## [0.7.1](https://github.com/Genoux/ib-toolkit/compare/standards-v0.7.0...standards-v0.7.1) (2026-10-01)
|
|
4
23
|
|
|
5
24
|
|
package/agents/standard.md
CHANGED
|
@@ -107,6 +107,7 @@ Server actions are queued per client; never use them for reads.
|
|
|
107
107
|
- Vitest colocated `*.test.ts` for every `server/` command, policy and `lib/` function;
|
|
108
108
|
`*.integration.test.ts` against a Neon branch; Playwright in `tests/e2e` against previews.
|
|
109
109
|
- CI: biome, tsc, dependency-cruiser, vitest, migration check, `ib check`, gitleaks.
|
|
110
|
+
- Release: production holds on the Vercel Deployment Check `release`, set by the toolkit `release-gate.yml` after re-verifying (and migrating) the deployed commit.
|
|
110
111
|
|
|
111
112
|
### Commits
|
|
112
113
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inbeat/standards",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.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.
|
|
34
|
+
"@inbeat/config": "^0.9.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
|
}
|
|
@@ -64,7 +64,7 @@ export const GET = route({
|
|
|
64
64
|
|
|
65
65
|
Sentry is already wired in the template.
|
|
66
66
|
|
|
67
|
-
Run `ib mcp` to give your AI tool access to Vercel,
|
|
67
|
+
Run `ib mcp` to give your AI tool access to Vercel, Sentry, Neon, Clerk, Cloudflare. For GitHub use the `gh` CLI.
|
|
68
68
|
|
|
69
69
|
### Auth (Clerk)
|
|
70
70
|
|
package/src/cli.ts
CHANGED
package/src/mcp-servers.ts
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
export const CURATED_SERVERS: Record<string, string> = {
|
|
2
2
|
vercel: "https://mcp.vercel.com",
|
|
3
|
-
github: "https://api.githubcopilot.com/mcp/readonly",
|
|
4
3
|
sentry: "https://mcp.sentry.dev/mcp",
|
|
5
4
|
neon: "https://mcp.neon.tech/mcp",
|
|
6
5
|
clerk: "https://mcp.clerk.com/mcp",
|
|
7
6
|
cloudflare: "https://mcp.cloudflare.com/mcp",
|
|
8
7
|
};
|
|
8
|
+
|
|
9
|
+
export const SERVER_HINTS: Record<string, string> = {
|
|
10
|
+
vercel: "Vercel projects and deployments",
|
|
11
|
+
sentry: "Sentry issues and errors",
|
|
12
|
+
neon: "Neon Postgres",
|
|
13
|
+
clerk: "Clerk auth docs and SDKs",
|
|
14
|
+
cloudflare: "Cloudflare API",
|
|
15
|
+
};
|
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
|
|
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
|
-
|
|
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
|
-
|
|
353
|
-
|
|
354
|
-
|
|
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
|
-
|
|
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
|
}
|
|
@@ -64,7 +64,7 @@ export const GET = route({
|
|
|
64
64
|
|
|
65
65
|
Sentry is already wired in the template.
|
|
66
66
|
|
|
67
|
-
Run `ib mcp` to give your AI tool access to Vercel,
|
|
67
|
+
Run `ib mcp` to give your AI tool access to Vercel, Sentry, Neon, Clerk, Cloudflare. For GitHub use the `gh` CLI.
|
|
68
68
|
|
|
69
69
|
### Auth (Clerk)
|
|
70
70
|
|
|
@@ -2,13 +2,19 @@ name: CI
|
|
|
2
2
|
|
|
3
3
|
on:
|
|
4
4
|
pull_request:
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
repository_dispatch:
|
|
6
|
+
types: [vercel.deployment.ready]
|
|
7
7
|
|
|
8
8
|
permissions:
|
|
9
9
|
contents: read
|
|
10
10
|
pull-requests: read
|
|
11
|
+
statuses: write
|
|
11
12
|
|
|
12
13
|
jobs:
|
|
13
14
|
verify:
|
|
15
|
+
if: github.event_name == 'pull_request'
|
|
14
16
|
uses: Genoux/ib-toolkit/.github/workflows/verify.yml@main
|
|
17
|
+
|
|
18
|
+
release:
|
|
19
|
+
if: github.event_name == 'repository_dispatch' && github.event.client_payload.environment == 'production'
|
|
20
|
+
uses: Genoux/ib-toolkit/.github/workflows/release-gate.yml@main
|
package/template/AGENTS.md
CHANGED
|
@@ -110,6 +110,7 @@ Server actions are queued per client; never use them for reads.
|
|
|
110
110
|
- Vitest colocated `*.test.ts` for every `server/` command, policy and `lib/` function;
|
|
111
111
|
`*.integration.test.ts` against a Neon branch; Playwright in `tests/e2e` against previews.
|
|
112
112
|
- CI: biome, tsc, dependency-cruiser, vitest, migration check, `ib check`, gitleaks.
|
|
113
|
+
- Release: production holds on the Vercel Deployment Check `release`, set by the toolkit `release-gate.yml` after re-verifying (and migrating) the deployed commit.
|
|
113
114
|
|
|
114
115
|
### Commits
|
|
115
116
|
|