kadai 0.3.0 → 0.4.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.
Files changed (3) hide show
  1. package/README.md +18 -9
  2. package/dist/cli.js +143 -12
  3. package/package.json +2 -1
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- <img width="1024" height="1024" alt="image" src="https://github.com/user-attachments/assets/daa548e6-5984-4cde-93d3-7c4684de639a" />
1
+ <img width="919" height="228" alt="image" src="https://github.com/user-attachments/assets/2c1fca9f-3484-409f-b3b4-214edc0387a4" />
2
2
 
3
3
  # kadai
4
4
 
@@ -6,8 +6,6 @@ A terminal UI for discovering and running project scripts. Drop scripts into `.k
6
6
 
7
7
  ## Getting Started
8
8
 
9
- <img width="950" height="205" alt="image" src="https://github.com/user-attachments/assets/b694bfaa-146b-41c7-a44c-d197c7cea08e" />
10
-
11
9
  ```bash
12
10
  bunx kadai
13
11
  # OR
@@ -70,11 +68,6 @@ For JS/TS, use `//` comments:
70
68
  | `confirm` | boolean | Require confirmation before running |
71
69
  | `hidden` | boolean | Hide from menu (still runnable via CLI) |
72
70
  | `fullscreen` | boolean | Use alternate screen buffer (`.tsx` only) |
73
- | `interactive` | boolean | Hand over the full terminal to the script |
74
-
75
- ### Interactive Scripts
76
-
77
- Scripts marked `interactive` get full terminal control — kadai exits its UI, runs the script with inherited stdio, then returns to the menu. Use this for scripts that need user input (readline prompts, password entry, etc.).
78
71
 
79
72
  ### Ink TUI Actions
80
73
 
@@ -148,13 +141,29 @@ kadai # Interactive menu
148
141
  kadai list --json # List actions as JSON
149
142
  kadai list --json --all # Include hidden actions
150
143
  kadai run <action-id> # Run an action directly
144
+ kadai mcp # Start MCP server (creates .mcp.json)
151
145
  ```
152
146
 
153
147
  ## AI
154
148
 
155
149
  kadai is designed to work well with AI coding agents like Claude Code.
156
150
 
157
- ### How It Works
151
+ ### MCP Server
152
+
153
+ kadai includes a built-in [MCP](https://modelcontextprotocol.io/) server that exposes your actions as tools. Any MCP-compatible client (Claude Code, Claude Desktop, etc.) can auto-discover and run your project's actions.
154
+
155
+ kadai will automatically configure a `.mcp.json` file in your project root so Claude can automatically discover any `kadai` actions you define.
156
+
157
+ ```bash
158
+ kadai mcp
159
+ ```
160
+
161
+ This creates the `.mcp.json` in your project root if it doesn't already exist (so `claude` will autodiscover it.)
162
+ It then starts the `mcp` server (this is the command `claude` uses to invoke `kadai` MCP.)
163
+
164
+ Each action becomes an MCP tool. Nested action IDs use `--` as a separator (e.g. `database/reset` becomes the tool `database--reset`) since MCP tool names don't allow slashes.
165
+
166
+ ### JSON API
158
167
 
159
168
  - `kadai list --json` gives agents a machine-readable list of available project actions
160
169
  - `kadai run <action-id>` runs actions non-interactively (confirmation prompts auto-skip in non-TTY)
package/dist/cli.js CHANGED
@@ -347,6 +347,127 @@ var init_runner = __esm(() => {
347
347
  };
348
348
  });
349
349
 
350
+ // src/core/mcp.ts
351
+ var exports_mcp = {};
352
+ __export(exports_mcp, {
353
+ toolNameToActionId: () => toolNameToActionId,
354
+ startMcpServer: () => startMcpServer,
355
+ ensureMcpConfig: () => ensureMcpConfig,
356
+ actionIdToToolName: () => actionIdToToolName
357
+ });
358
+ import { join as join4, resolve } from "path";
359
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
360
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
361
+ function resolveInvocationCommand() {
362
+ const script = process.argv[1] ?? "";
363
+ if (script.endsWith(".ts") || script.endsWith(".tsx")) {
364
+ return { command: "bun", args: [resolve(script), "mcp"] };
365
+ }
366
+ return { command: "bunx", args: ["kadai", "mcp"] };
367
+ }
368
+ function actionIdToToolName(id) {
369
+ return id.replace(/\//g, "--");
370
+ }
371
+ function toolNameToActionId(name) {
372
+ return name.replace(/--/g, "/");
373
+ }
374
+ function buildToolDescription(action) {
375
+ const parts = [];
376
+ if (action.meta.emoji)
377
+ parts.push(action.meta.emoji);
378
+ parts.push(action.meta.name);
379
+ if (action.meta.description) {
380
+ parts.push(`\u2014 ${action.meta.description}`);
381
+ }
382
+ return parts.join(" ");
383
+ }
384
+ async function ensureMcpConfig(projectRoot) {
385
+ const mcpJsonPath = join4(projectRoot, ".mcp.json");
386
+ const mcpFile = Bun.file(mcpJsonPath);
387
+ const kadaiEntry = resolveInvocationCommand();
388
+ if (await mcpFile.exists()) {
389
+ const existing = await mcpFile.json();
390
+ if (existing.mcpServers?.kadai) {
391
+ process.stderr.write(`kadai MCP server already configured in .mcp.json
392
+ `);
393
+ return;
394
+ }
395
+ existing.mcpServers = existing.mcpServers ?? {};
396
+ existing.mcpServers.kadai = kadaiEntry;
397
+ await Bun.write(mcpJsonPath, `${JSON.stringify(existing, null, 2)}
398
+ `);
399
+ process.stderr.write(`Added kadai entry to existing .mcp.json
400
+ `);
401
+ } else {
402
+ const config = {
403
+ mcpServers: {
404
+ kadai: kadaiEntry
405
+ }
406
+ };
407
+ await Bun.write(mcpJsonPath, `${JSON.stringify(config, null, 2)}
408
+ `);
409
+ process.stderr.write(`Created .mcp.json with kadai MCP server config
410
+ `);
411
+ }
412
+ }
413
+ async function startMcpServer(kadaiDir, cwd) {
414
+ let visibleActions = [];
415
+ let config = {};
416
+ if (kadaiDir) {
417
+ config = await loadConfig(kadaiDir);
418
+ const actionsDir = join4(kadaiDir, config.actionsDir ?? "actions");
419
+ visibleActions = (await loadActions(actionsDir)).filter((a) => !a.meta.hidden);
420
+ }
421
+ const server = new McpServer({ name: "kadai", version: "0.3.0" });
422
+ const env = {
423
+ ...process.env,
424
+ ...config.env ?? {}
425
+ };
426
+ for (const action of visibleActions) {
427
+ const toolName = actionIdToToolName(action.id);
428
+ const description = buildToolDescription(action);
429
+ server.registerTool(toolName, { description }, async () => {
430
+ const cmd = resolveCommand(action);
431
+ const proc = Bun.spawn(cmd, {
432
+ cwd,
433
+ stdout: "pipe",
434
+ stderr: "pipe",
435
+ stdin: "ignore",
436
+ env
437
+ });
438
+ const [stdout, stderr] = await Promise.all([
439
+ new Response(proc.stdout).text(),
440
+ new Response(proc.stderr).text()
441
+ ]);
442
+ const exitCode = await proc.exited;
443
+ const parts = [];
444
+ if (stdout)
445
+ parts.push(stdout);
446
+ if (stderr)
447
+ parts.push(`[stderr]
448
+ ${stderr}`);
449
+ if (exitCode !== 0)
450
+ parts.push(`[exit code: ${exitCode}]`);
451
+ return {
452
+ content: [
453
+ { type: "text", text: parts.join(`
454
+ `) || "(no output)" }
455
+ ],
456
+ isError: exitCode !== 0
457
+ };
458
+ });
459
+ }
460
+ const transport = new StdioServerTransport;
461
+ await server.connect(transport);
462
+ process.stderr.write(`kadai MCP server running (${visibleActions.length} tools)
463
+ `);
464
+ }
465
+ var init_mcp = __esm(() => {
466
+ init_config();
467
+ init_loader();
468
+ init_runner();
469
+ });
470
+
350
471
  // src/components/Breadcrumbs.tsx
351
472
  import { Box, Text } from "ink";
352
473
  import { jsxDEV } from "react/jsx-dev-runtime";
@@ -508,7 +629,7 @@ function StatusBar() {
508
629
  var init_StatusBar = () => {};
509
630
 
510
631
  // src/hooks/useActions.ts
511
- import { join as join4 } from "path";
632
+ import { join as join5 } from "path";
512
633
  import { useEffect as useEffect2, useRef, useState as useState2 } from "react";
513
634
  function useActions({ kadaiDir }) {
514
635
  const [actions, setActions] = useState2([]);
@@ -520,7 +641,7 @@ function useActions({ kadaiDir }) {
520
641
  (async () => {
521
642
  const cfg = await loadConfig(kadaiDir);
522
643
  setConfig(cfg);
523
- const actionsDir = join4(kadaiDir, cfg.actionsDir ?? "actions");
644
+ const actionsDir = join5(kadaiDir, cfg.actionsDir ?? "actions");
524
645
  const localActions = await loadActions(actionsDir);
525
646
  setActions(localActions);
526
647
  setLoading(false);
@@ -1785,7 +1906,7 @@ __export(exports_init_wizard, {
1785
1906
  generateConfigFile: () => generateConfigFile
1786
1907
  });
1787
1908
  import { existsSync, mkdirSync } from "fs";
1788
- import { join as join5 } from "path";
1909
+ import { join as join6 } from "path";
1789
1910
  function generateConfigFile() {
1790
1911
  const lines = [' // actionsDir: "actions",', " // env: {},"];
1791
1912
  return `export default {
@@ -1795,10 +1916,10 @@ ${lines.join(`
1795
1916
  `;
1796
1917
  }
1797
1918
  async function writeInitFiles(cwd) {
1798
- const kadaiDir = join5(cwd, ".kadai");
1799
- const actionsDir = join5(kadaiDir, "actions");
1919
+ const kadaiDir = join6(cwd, ".kadai");
1920
+ const actionsDir = join6(kadaiDir, "actions");
1800
1921
  mkdirSync(actionsDir, { recursive: true });
1801
- const sampleAction = join5(actionsDir, "hello.sh");
1922
+ const sampleAction = join6(actionsDir, "hello.sh");
1802
1923
  const sampleFile = Bun.file(sampleAction);
1803
1924
  let sampleCreated = false;
1804
1925
  if (!await sampleFile.exists()) {
@@ -1813,14 +1934,14 @@ echo "Add your own scripts to .kadai/actions/ to get started."
1813
1934
  sampleCreated = true;
1814
1935
  }
1815
1936
  const configContent = generateConfigFile();
1816
- const configPath = join5(kadaiDir, "config.ts");
1937
+ const configPath = join6(kadaiDir, "config.ts");
1817
1938
  await Bun.write(configPath, configContent);
1818
1939
  let skillCreated = false;
1819
- const hasClaudeDir = existsSync(join5(cwd, ".claude"));
1820
- const hasClaudeMd = existsSync(join5(cwd, "CLAUDE.md"));
1940
+ const hasClaudeDir = existsSync(join6(cwd, ".claude"));
1941
+ const hasClaudeMd = existsSync(join6(cwd, "CLAUDE.md"));
1821
1942
  if (hasClaudeDir || hasClaudeMd) {
1822
- const skillDir = join5(cwd, ".claude", "skills", "kadai");
1823
- const skillPath = join5(skillDir, "SKILL.md");
1943
+ const skillDir = join6(cwd, ".claude", "skills", "kadai");
1944
+ const skillPath = join6(skillDir, "SKILL.md");
1824
1945
  if (!await Bun.file(skillPath).exists()) {
1825
1946
  mkdirSync(skillDir, { recursive: true });
1826
1947
  await Bun.write(skillPath, generateSkillFile());
@@ -1950,10 +2071,12 @@ function parseArgs(argv) {
1950
2071
  }
1951
2072
  return { type: "run", actionId };
1952
2073
  }
2074
+ case "mcp":
2075
+ return { type: "mcp" };
1953
2076
  default:
1954
2077
  return {
1955
2078
  type: "error",
1956
- message: `Unknown command: ${command}. Available commands: list, run`
2079
+ message: `Unknown command: ${command}. Available commands: list, run, mcp`
1957
2080
  };
1958
2081
  }
1959
2082
  }
@@ -2039,6 +2162,14 @@ if (parsed.type === "error") {
2039
2162
  `);
2040
2163
  process.exit(1);
2041
2164
  }
2165
+ if (parsed.type === "mcp") {
2166
+ const { ensureMcpConfig: ensureMcpConfig2, startMcpServer: startMcpServer2 } = await Promise.resolve().then(() => (init_mcp(), exports_mcp));
2167
+ const kadaiDir = findZcliDir(cwd);
2168
+ const projectRoot = kadaiDir ? (await import("path")).dirname(kadaiDir) : cwd;
2169
+ await ensureMcpConfig2(projectRoot);
2170
+ await startMcpServer2(kadaiDir, cwd);
2171
+ await new Promise(() => {});
2172
+ }
2042
2173
  if (parsed.type === "list" || parsed.type === "run") {
2043
2174
  const kadaiDir = findZcliDir(cwd);
2044
2175
  if (!kadaiDir) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kadai",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "kadai": "./dist/cli.js"
@@ -24,6 +24,7 @@
24
24
  },
25
25
  "dependencies": {
26
26
  "@inkjs/ui": "^2.0.0",
27
+ "@modelcontextprotocol/sdk": "^1.12.1",
27
28
  "@types/react": "^19.2.14",
28
29
  "fuzzysort": "^3.1.0",
29
30
  "ink": "^6.7.0",