kadai 0.3.0 → 0.4.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.
Files changed (3) hide show
  1. package/README.md +22 -10
  2. package/dist/cli.js +148 -23
  3. package/package.json +2 -1
package/README.md CHANGED
@@ -1,13 +1,14 @@
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
 
5
- A terminal UI for discovering and running project scripts. Drop scripts into `.kadai/actions/`, and kadai gives you a fuzzy-searchable menu to run them.
5
+ 1. Drop scripts into `.kadai/actions/`.
6
+ 2. Run with `bunx kadai`.
7
+ 3. Share them with your team in the repo.
8
+ 4. Automatically make them discoverable by AI.
6
9
 
7
10
  ## Getting Started
8
11
 
9
- <img width="950" height="205" alt="image" src="https://github.com/user-attachments/assets/b694bfaa-146b-41c7-a44c-d197c7cea08e" />
10
-
11
12
  ```bash
12
13
  bunx kadai
13
14
  # OR
@@ -70,11 +71,6 @@ For JS/TS, use `//` comments:
70
71
  | `confirm` | boolean | Require confirmation before running |
71
72
  | `hidden` | boolean | Hide from menu (still runnable via CLI) |
72
73
  | `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
74
 
79
75
  ### Ink TUI Actions
80
76
 
@@ -148,13 +144,29 @@ kadai # Interactive menu
148
144
  kadai list --json # List actions as JSON
149
145
  kadai list --json --all # Include hidden actions
150
146
  kadai run <action-id> # Run an action directly
147
+ kadai mcp # Start MCP server (creates .mcp.json)
151
148
  ```
152
149
 
153
150
  ## AI
154
151
 
155
152
  kadai is designed to work well with AI coding agents like Claude Code.
156
153
 
157
- ### How It Works
154
+ ### MCP Server
155
+
156
+ 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.
157
+
158
+ kadai will automatically configure a `.mcp.json` file in your project root so Claude can automatically discover any `kadai` actions you define.
159
+
160
+ ```bash
161
+ kadai mcp
162
+ ```
163
+
164
+ This creates the `.mcp.json` in your project root if it doesn't already exist (so `claude` will autodiscover it.)
165
+ It then starts the `mcp` server (this is the command `claude` uses to invoke `kadai` MCP.)
166
+
167
+ 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.
168
+
169
+ ### JSON API
158
170
 
159
171
  - `kadai list --json` gives agents a machine-readable list of available project actions
160
172
  - `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) {
@@ -2168,22 +2299,16 @@ var env = {
2168
2299
  console.log(`${action.meta.emoji ? `${action.meta.emoji} ` : ""}${action.meta.name}
2169
2300
  `);
2170
2301
  process.stdin.removeAllListeners("data");
2302
+ if (process.stdin.isTTY) {
2303
+ process.stdin.setRawMode(false);
2304
+ }
2305
+ process.stdin.resume();
2171
2306
  var proc = Bun.spawn(cmd, {
2172
2307
  cwd,
2173
2308
  stdout: "inherit",
2174
2309
  stderr: "inherit",
2175
- stdin: "pipe",
2310
+ stdin: "inherit",
2176
2311
  env
2177
2312
  });
2178
- var forwardStdin = (data) => {
2179
- try {
2180
- const converted = Buffer.from(data.toString().replace(/\r/g, `
2181
- `));
2182
- proc.stdin.write(converted);
2183
- proc.stdin.flush();
2184
- } catch {}
2185
- };
2186
- process.stdin.on("data", forwardStdin);
2187
- process.stdin.resume();
2188
2313
  var exitCode = await proc.exited;
2189
2314
  process.exit(exitCode);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kadai",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
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",