runboard 0.1.0 → 1.0.1

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/README.md CHANGED
@@ -44,10 +44,22 @@ history becomes your trajectory record.
44
44
 
45
45
  ## Use it from your AI assistant
46
46
 
47
- Portable `skills/` (SKILL.md) and an MCP server (`npx runboard-mcp`) let Claude, Cursor,
48
- Codex, Copilot, Gemini, and other agents run the assessment conversation and persist
49
- results through the same core — the numbers are always computed by the tool, never by the
50
- model.
47
+ Portable `skills/` (SKILL.md) and an MCP server let Claude, Cursor, Codex, Copilot, Gemini,
48
+ and other agents run the assessment conversation and persist results through the same core
49
+ — the numbers are always computed by the tool, never by the model.
50
+
51
+ The MCP server ships in this same package and runs locally over stdio (no network, no
52
+ account). Add it to your client with one zero-install command:
53
+
54
+ ```jsonc
55
+ {
56
+ "mcpServers": {
57
+ "runboard": { "command": "npx", "args": ["-y", "runboard", "mcp"] }
58
+ }
59
+ }
60
+ ```
61
+
62
+ See [docs/mcp.md](./docs/mcp.md) for Claude Desktop, Cursor, and VS Code setup.
51
63
 
52
64
  ## Contributing
53
65
 
@@ -1,42 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // src/core/errors.ts
4
- var UserError = class extends Error {
5
- };
3
+ // mcp/server.ts
4
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
+ import { z } from "zod";
6
7
 
7
- // src/commands/shared.ts
8
- import { existsSync } from "fs";
9
-
10
- // src/data/paths.ts
11
- import path from "path";
12
- function runboardPaths(root = process.cwd()) {
13
- const dir = path.join(root, ".runboard");
14
- return {
15
- root,
16
- dir,
17
- config: path.join(dir, "config.yaml"),
18
- rubric: path.join(dir, "rubric.yaml"),
19
- assessmentsDir: path.join(dir, "assessments"),
20
- reportsDir: path.join(dir, "reports"),
21
- roadmap: path.join(dir, "roadmap.md"),
22
- boardHtml: path.join(dir, "board.html")
23
- };
24
- }
25
- function assessmentFile(root, date) {
26
- return path.join(runboardPaths(root).assessmentsDir, `${date}.md`);
27
- }
28
-
29
- // src/commands/shared.ts
30
- function requireInit(root = process.cwd()) {
31
- if (!existsSync(runboardPaths(root).dir)) {
32
- throw new UserError("No .runboard/ found here. Run `runboard init` first.");
33
- }
34
- }
35
- function requireAssessments(dates) {
36
- if (dates.length === 0) {
37
- throw new UserError("No assessments yet. Run `runboard assess` to record one.");
38
- }
39
- }
8
+ // src/version.ts
9
+ import { readFileSync } from "fs";
10
+ var pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
11
+ var VERSION = pkg.version;
40
12
 
41
13
  // src/commands/assess.ts
42
14
  import { cancel, intro, isCancel, note, outro, select, text } from "@clack/prompts";
@@ -79,9 +51,30 @@ function isLevel(value) {
79
51
  }
80
52
 
81
53
  // src/data/assessments.ts
82
- import { existsSync as existsSync2, mkdirSync, readFileSync, readdirSync, writeFileSync } from "fs";
54
+ import { existsSync, mkdirSync, readFileSync as readFileSync2, readdirSync, writeFileSync } from "fs";
83
55
  import path2 from "path";
84
56
  import { parse, stringify } from "yaml";
57
+
58
+ // src/data/paths.ts
59
+ import path from "path";
60
+ function runboardPaths(root = process.cwd()) {
61
+ const dir = path.join(root, ".runboard");
62
+ return {
63
+ root,
64
+ dir,
65
+ config: path.join(dir, "config.yaml"),
66
+ rubric: path.join(dir, "rubric.yaml"),
67
+ assessmentsDir: path.join(dir, "assessments"),
68
+ reportsDir: path.join(dir, "reports"),
69
+ roadmap: path.join(dir, "roadmap.md"),
70
+ boardHtml: path.join(dir, "board.html")
71
+ };
72
+ }
73
+ function assessmentFile(root, date) {
74
+ return path.join(runboardPaths(root).assessmentsDir, `${date}.md`);
75
+ }
76
+
77
+ // src/data/assessments.ts
85
78
  var DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
86
79
  var AssessmentError = class extends Error {
87
80
  };
@@ -159,14 +152,14 @@ function parseAssessment(text2, fallbackDate) {
159
152
  }
160
153
  function listAssessmentDates(root = process.cwd()) {
161
154
  const dir = runboardPaths(root).assessmentsDir;
162
- if (!existsSync2(dir)) {
155
+ if (!existsSync(dir)) {
163
156
  return [];
164
157
  }
165
158
  return readdirSync(dir).filter((f) => f.endsWith(".md")).map((f) => path2.basename(f, ".md")).filter((d) => DATE_RE.test(d)).sort();
166
159
  }
167
160
  function loadAssessment(date, root = process.cwd()) {
168
161
  const file = assessmentFile(root, date);
169
- return parseAssessment(readFileSync(file, "utf8"), date);
162
+ return parseAssessment(readFileSync2(file, "utf8"), date);
170
163
  }
171
164
  function loadAllAssessments(root = process.cwd()) {
172
165
  return listAssessmentDates(root).map((date) => loadAssessment(date, root));
@@ -180,7 +173,7 @@ function saveAssessment(a, root = process.cwd(), options = {}) {
180
173
  const dir = runboardPaths(root).assessmentsDir;
181
174
  mkdirSync(dir, { recursive: true });
182
175
  const file = assessmentFile(root, a.date);
183
- if (existsSync2(file) && !options.force) {
176
+ if (existsSync(file) && !options.force) {
184
177
  throw new AssessmentError(
185
178
  `An assessment for ${a.date} already exists. Re-run with --force to overwrite.`
186
179
  );
@@ -190,7 +183,7 @@ function saveAssessment(a, root = process.cwd(), options = {}) {
190
183
  }
191
184
 
192
185
  // src/data/rubric.ts
193
- import { readFileSync as readFileSync2 } from "fs";
186
+ import { readFileSync as readFileSync3 } from "fs";
194
187
  import path3 from "path";
195
188
  import { fileURLToPath } from "url";
196
189
  import { parse as parse2 } from "yaml";
@@ -202,7 +195,7 @@ function shippedRubricPath() {
202
195
  ];
203
196
  for (const candidate of candidates) {
204
197
  try {
205
- readFileSync2(candidate);
198
+ readFileSync3(candidate);
206
199
  return candidate;
207
200
  } catch {
208
201
  }
@@ -262,13 +255,32 @@ function parseDimension(raw) {
262
255
  function loadRubric(filePath = shippedRubricPath()) {
263
256
  let text2;
264
257
  try {
265
- text2 = readFileSync2(filePath, "utf8");
258
+ text2 = readFileSync3(filePath, "utf8");
266
259
  } catch {
267
260
  throw new Error(`Could not read rubric at ${filePath}.`);
268
261
  }
269
262
  return parseRubric(text2);
270
263
  }
271
264
 
265
+ // src/commands/shared.ts
266
+ import { existsSync as existsSync2 } from "fs";
267
+
268
+ // src/core/errors.ts
269
+ var UserError = class extends Error {
270
+ };
271
+
272
+ // src/commands/shared.ts
273
+ function requireInit(root = process.cwd()) {
274
+ if (!existsSync2(runboardPaths(root).dir)) {
275
+ throw new UserError("No .runboard/ found here. Run `runboard init` first.");
276
+ }
277
+ }
278
+ function requireAssessments(dates) {
279
+ if (dates.length === 0) {
280
+ throw new UserError("No assessments yet. Run `runboard assess` to record one.");
281
+ }
282
+ }
283
+
272
284
  // src/commands/assess.ts
273
285
  function today() {
274
286
  const d = /* @__PURE__ */ new Date();
@@ -609,7 +621,7 @@ function detectTriggers(assessmentsChrono) {
609
621
  }
610
622
 
611
623
  // src/render/reports.ts
612
- import { readFileSync as readFileSync3 } from "fs";
624
+ import { readFileSync as readFileSync4 } from "fs";
613
625
  import path4 from "path";
614
626
  import { fileURLToPath as fileURLToPath2 } from "url";
615
627
  import { Eta } from "eta";
@@ -638,7 +650,7 @@ function templatesDir() {
638
650
  const candidates = [path4.resolve(here, "../templates"), path4.resolve(here, "../../templates")];
639
651
  for (const candidate of candidates) {
640
652
  try {
641
- readFileSync3(path4.join(candidate, "roadmap.eta"));
653
+ readFileSync4(path4.join(candidate, "roadmap.eta"));
642
654
  return candidate;
643
655
  } catch {
644
656
  }
@@ -881,24 +893,129 @@ function registerStatus(program) {
881
893
  });
882
894
  }
883
895
 
896
+ // mcp/handlers.ts
897
+ function handleAssess(args) {
898
+ const sets = Object.entries(args.scores).map(
899
+ ([key, s]) => `${key}=${s.level}:${s.trajectory}:${s.evidence ?? ""}`
900
+ );
901
+ const { date, path: path7 } = runAssess({
902
+ root: args.root,
903
+ sets,
904
+ type: args.type,
905
+ force: args.force,
906
+ date: args.date
907
+ });
908
+ return { date, path: path7, written: true };
909
+ }
910
+ function handleBoard(ctx) {
911
+ const { summary, htmlPath } = runBoard({ root: ctx.root, html: ctx.html });
912
+ return {
913
+ cells: summary.cells,
914
+ average: formatAverage(summary.average),
915
+ trajectoryCounts: summary.trajectoryCounts,
916
+ ...htmlPath ? { htmlPath } : {}
917
+ };
918
+ }
919
+ function handlePulse(ctx) {
920
+ const { path: path7, triggers } = runPulse({ root: ctx.root });
921
+ return { path: path7, triggers };
922
+ }
923
+ function handleRoadmap(ctx) {
924
+ const { path: path7 } = runRoadmap({ root: ctx.root });
925
+ const latest = latestAssessment(ctx.root);
926
+ if (!latest) throw new Error("unreachable");
927
+ return { path: path7, bindingConstraint: bindingConstraint(latest) };
928
+ }
929
+ function handleReport(ctx) {
930
+ const { path: path7 } = runReport({ root: ctx.root, type: ctx.type });
931
+ return { path: path7 };
932
+ }
933
+ function handleStatus(ctx) {
934
+ const s = runStatus(ctx.root);
935
+ return {
936
+ latestDate: s.latestDate ?? null,
937
+ average: s.average ?? null,
938
+ trajectoryCounts: s.trajectoryCounts ?? {},
939
+ activeTriggers: s.activeTriggers
940
+ };
941
+ }
942
+
943
+ // mcp/server.ts
944
+ var scoreShape = z.object({
945
+ level: z.number().int().min(1).max(5),
946
+ trajectory: z.enum(["up", "flat", "down", "volatile"]),
947
+ evidence: z.string().default("")
948
+ });
949
+ function json(value) {
950
+ return { content: [{ type: "text", text: JSON.stringify(value, null, 2) }] };
951
+ }
952
+ function buildServer() {
953
+ const server = new McpServer({ name: "runboard", version: VERSION });
954
+ server.registerTool(
955
+ "runboard_assess",
956
+ {
957
+ description: "Record a 9-dimension assessment (the model supplies scores).",
958
+ inputSchema: {
959
+ scores: z.record(z.string(), scoreShape),
960
+ type: z.enum(["baseline", "pulse", "quarterly", "event"]).optional(),
961
+ force: z.boolean().optional()
962
+ }
963
+ },
964
+ async (args) => json(handleAssess(args))
965
+ );
966
+ server.registerTool(
967
+ "runboard_board",
968
+ {
969
+ description: "Render the board summary; set html to also write board.html.",
970
+ inputSchema: { html: z.boolean().optional() }
971
+ },
972
+ async (args) => json(handleBoard({ html: args.html }))
973
+ );
974
+ server.registerTool(
975
+ "runboard_pulse",
976
+ {
977
+ description: "Compare the two latest assessments and flag stuck dimensions.",
978
+ inputSchema: {}
979
+ },
980
+ async () => json(handlePulse({}))
981
+ );
982
+ server.registerTool(
983
+ "runboard_roadmap",
984
+ { description: "Generate a Now/Next/Later plan from the binding constraint.", inputSchema: {} },
985
+ async () => json(handleRoadmap({}))
986
+ );
987
+ server.registerTool(
988
+ "runboard_report",
989
+ {
990
+ description: "Render a report (board-update | baseline | monthly).",
991
+ inputSchema: { type: z.enum(["board-update", "baseline", "monthly"]) }
992
+ },
993
+ async (args) => json(handleReport({ type: args.type }))
994
+ );
995
+ server.registerTool(
996
+ "runboard_status",
997
+ { description: "One-screen current state.", inputSchema: {} },
998
+ async () => json(handleStatus({}))
999
+ );
1000
+ return server;
1001
+ }
1002
+ async function startMcpServer() {
1003
+ const server = buildServer();
1004
+ const transport = new StdioServerTransport();
1005
+ await server.connect(transport);
1006
+ }
1007
+
884
1008
  export {
1009
+ VERSION,
885
1010
  runboardPaths,
886
- latestAssessment,
887
1011
  shippedRubricPath,
888
1012
  loadRubric,
889
1013
  UserError,
890
- runAssess,
891
1014
  registerAssess,
892
- formatAverage,
893
- bindingConstraint,
894
- runBoard,
895
1015
  registerBoard,
896
- runPulse,
897
1016
  registerPulse,
898
- runReport,
899
1017
  registerReport,
900
- runRoadmap,
901
1018
  registerRoadmap,
902
- runStatus,
903
- registerStatus
1019
+ registerStatus,
1020
+ startMcpServer
904
1021
  };
package/dist/cli.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  UserError,
4
+ VERSION,
4
5
  loadRubric,
5
6
  registerAssess,
6
7
  registerBoard,
@@ -9,8 +10,9 @@ import {
9
10
  registerRoadmap,
10
11
  registerStatus,
11
12
  runboardPaths,
12
- shippedRubricPath
13
- } from "./chunk-U4SVYBXI.js";
13
+ shippedRubricPath,
14
+ startMcpServer
15
+ } from "./chunk-3NNMYWCN.js";
14
16
 
15
17
  // src/cli.ts
16
18
  import { Command } from "commander";
@@ -64,7 +66,6 @@ ${created.map((c) => ` ${c}`).join("\n")}
64
66
  }
65
67
 
66
68
  // src/cli.ts
67
- var VERSION = "0.1.0";
68
69
  function buildProgram() {
69
70
  const program = new Command();
70
71
  program.name("runboard").description("Local-first technical-leadership maturity scorecard.").version(VERSION, "-v, --version");
@@ -75,6 +76,9 @@ function buildProgram() {
75
76
  registerRoadmap(program);
76
77
  registerReport(program);
77
78
  registerStatus(program);
79
+ program.command("mcp").description("Start the MCP server over stdio (for tool-calling AI clients).").action(async () => {
80
+ await startMcpServer();
81
+ });
78
82
  return program;
79
83
  }
80
84
  async function main(argv) {
@@ -91,12 +95,10 @@ async function main(argv) {
91
95
  throw err;
92
96
  }
93
97
  }
98
+
99
+ // src/main.ts
94
100
  main(process.argv).catch((err) => {
95
101
  process.stderr.write(`${err instanceof Error ? err.message : String(err)}
96
102
  `);
97
103
  process.exit(1);
98
104
  });
99
- export {
100
- buildProgram,
101
- main
102
- };
package/dist/mcp.js CHANGED
@@ -1,137 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- bindingConstraint,
4
- formatAverage,
5
- latestAssessment,
6
- runAssess,
7
- runBoard,
8
- runPulse,
9
- runReport,
10
- runRoadmap,
11
- runStatus
12
- } from "./chunk-U4SVYBXI.js";
3
+ startMcpServer
4
+ } from "./chunk-3NNMYWCN.js";
13
5
 
14
- // mcp/server.ts
15
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
16
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
17
- import { z } from "zod";
18
-
19
- // mcp/handlers.ts
20
- function handleAssess(args) {
21
- const sets = Object.entries(args.scores).map(
22
- ([key, s]) => `${key}=${s.level}:${s.trajectory}:${s.evidence ?? ""}`
23
- );
24
- const { date, path } = runAssess({
25
- root: args.root,
26
- sets,
27
- type: args.type,
28
- force: args.force,
29
- date: args.date
30
- });
31
- return { date, path, written: true };
32
- }
33
- function handleBoard(ctx) {
34
- const { summary, htmlPath } = runBoard({ root: ctx.root, html: ctx.html });
35
- return {
36
- cells: summary.cells,
37
- average: formatAverage(summary.average),
38
- trajectoryCounts: summary.trajectoryCounts,
39
- ...htmlPath ? { htmlPath } : {}
40
- };
41
- }
42
- function handlePulse(ctx) {
43
- const { path, triggers } = runPulse({ root: ctx.root });
44
- return { path, triggers };
45
- }
46
- function handleRoadmap(ctx) {
47
- const { path } = runRoadmap({ root: ctx.root });
48
- const latest = latestAssessment(ctx.root);
49
- if (!latest) throw new Error("unreachable");
50
- return { path, bindingConstraint: bindingConstraint(latest) };
51
- }
52
- function handleReport(ctx) {
53
- const { path } = runReport({ root: ctx.root, type: ctx.type });
54
- return { path };
55
- }
56
- function handleStatus(ctx) {
57
- const s = runStatus(ctx.root);
58
- return {
59
- latestDate: s.latestDate ?? null,
60
- average: s.average ?? null,
61
- trajectoryCounts: s.trajectoryCounts ?? {},
62
- activeTriggers: s.activeTriggers
63
- };
64
- }
65
-
66
- // mcp/server.ts
67
- var scoreShape = z.object({
68
- level: z.number().int().min(1).max(5),
69
- trajectory: z.enum(["up", "flat", "down", "volatile"]),
70
- evidence: z.string().default("")
71
- });
72
- function json(value) {
73
- return { content: [{ type: "text", text: JSON.stringify(value, null, 2) }] };
74
- }
75
- function buildServer() {
76
- const server = new McpServer({ name: "runboard", version: "0.1.0" });
77
- server.registerTool(
78
- "runboard_assess",
79
- {
80
- description: "Record a 9-dimension assessment (the model supplies scores).",
81
- inputSchema: {
82
- scores: z.record(z.string(), scoreShape),
83
- type: z.enum(["baseline", "pulse", "quarterly", "event"]).optional(),
84
- force: z.boolean().optional()
85
- }
86
- },
87
- async (args) => json(handleAssess(args))
88
- );
89
- server.registerTool(
90
- "runboard_board",
91
- {
92
- description: "Render the board summary; set html to also write board.html.",
93
- inputSchema: { html: z.boolean().optional() }
94
- },
95
- async (args) => json(handleBoard({ html: args.html }))
96
- );
97
- server.registerTool(
98
- "runboard_pulse",
99
- {
100
- description: "Compare the two latest assessments and flag stuck dimensions.",
101
- inputSchema: {}
102
- },
103
- async () => json(handlePulse({}))
104
- );
105
- server.registerTool(
106
- "runboard_roadmap",
107
- { description: "Generate a Now/Next/Later plan from the binding constraint.", inputSchema: {} },
108
- async () => json(handleRoadmap({}))
109
- );
110
- server.registerTool(
111
- "runboard_report",
112
- {
113
- description: "Render a report (board-update | baseline | monthly).",
114
- inputSchema: { type: z.enum(["board-update", "baseline", "monthly"]) }
115
- },
116
- async (args) => json(handleReport({ type: args.type }))
117
- );
118
- server.registerTool(
119
- "runboard_status",
120
- { description: "One-screen current state.", inputSchema: {} },
121
- async () => json(handleStatus({}))
122
- );
123
- return server;
124
- }
125
- async function main() {
126
- const server = buildServer();
127
- const transport = new StdioServerTransport();
128
- await server.connect(transport);
129
- }
130
- main().catch((err) => {
6
+ // mcp/main.ts
7
+ startMcpServer().catch((err) => {
131
8
  process.stderr.write(`${err instanceof Error ? err.message : String(err)}
132
9
  `);
133
10
  process.exit(1);
134
11
  });
135
- export {
136
- buildServer
137
- };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "runboard",
3
- "version": "0.1.0",
3
+ "version": "1.0.1",
4
4
  "description": "Local-first CLI for the Runboard technical-leadership maturity framework. Deterministic scoring core, portable AI adapters, no phone-home.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -28,7 +28,8 @@
28
28
  "test:watch": "vitest",
29
29
  "lint": "biome check .",
30
30
  "format": "biome format --write .",
31
- "prepublishOnly": "npm run lint && npm run typecheck && npm run test && npm run build"
31
+ "mcp:smoke": "node scripts/mcp-smoke.mjs",
32
+ "prepublishOnly": "npm run lint && npm run typecheck && npm run test && npm run build && npm run mcp:smoke"
32
33
  },
33
34
  "dependencies": {
34
35
  "@clack/prompts": "^0.7.0",