@takuhon/cli 0.13.0 → 0.14.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/dist/index.js CHANGED
@@ -2,10 +2,10 @@
2
2
  import {
3
3
  ADMIN_DIST_DIRNAME,
4
4
  resolveAdminBundleDir
5
- } from "./chunk-TNTKQL7R.js";
5
+ } from "./chunk-YLSMA3YM.js";
6
6
 
7
7
  // src/index.ts
8
- import { readFileSync as readFileSync15, realpathSync } from "fs";
8
+ import { readFileSync as readFileSync16, realpathSync } from "fs";
9
9
  import { stdin, stdout } from "process";
10
10
  import { createInterface } from "readline/promises";
11
11
  import { fileURLToPath } from "url";
@@ -2147,18 +2147,144 @@ function isNotFound5(error) {
2147
2147
  return typeof error === "object" && error !== null && error.code === "ENOENT";
2148
2148
  }
2149
2149
 
2150
- // src/migrate-command.ts
2150
+ // src/mcp-command.ts
2151
2151
  import { readFileSync as readFileSync10 } from "fs";
2152
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
2153
+ import { validate as validate4 } from "@takuhon/core";
2154
+ import { createTakuhonMcpServer } from "@takuhon/mcp";
2155
+ var DEFAULT_PATH7 = "takuhon.json";
2156
+ var SERVER_NAME = "takuhon";
2157
+ var USAGE6 = `Usage: takuhon mcp [path]
2158
+
2159
+ Serve a takuhon.json over the Model Context Protocol on stdio, so an MCP client
2160
+ (e.g. Claude Desktop) can read the profile read-only. With no path, serves
2161
+ ./takuhon.json. The file is re-read on every request, so edits are reflected
2162
+ without restarting.
2163
+
2164
+ Exposes (read-only, the same surface as the public API, privacy filter applied):
2165
+ tools: get_profile, get_section, get_jsonld, list_locales
2166
+ resources: takuhon://profile, takuhon://schema
2167
+
2168
+ stdin/stdout carry the protocol stream; diagnostics go to stderr. The server
2169
+ runs until the client disconnects or you press Ctrl-C.
2170
+
2171
+ Example Claude Desktop config (claude_desktop_config.json):
2172
+ {
2173
+ "mcpServers": {
2174
+ "my-profile": { "command": "takuhon", "args": ["mcp", "/path/to/takuhon.json"] }
2175
+ }
2176
+ }
2177
+
2178
+ Exit codes: 0 = served then stopped, 2 = bad arguments / file missing /
2179
+ unreadable / invalid.
2180
+ `;
2181
+ var VERSION = readCliVersion();
2182
+ function loadMcpProfile(path) {
2183
+ let raw;
2184
+ try {
2185
+ raw = readFileSync10(path, "utf8");
2186
+ } catch {
2187
+ throw new Error(`cannot read '${path}'.`);
2188
+ }
2189
+ let data;
2190
+ try {
2191
+ data = JSON.parse(raw);
2192
+ } catch (error) {
2193
+ const detail = error instanceof Error ? error.message : String(error);
2194
+ throw new Error(`'${path}' is not valid JSON: ${detail}`, { cause: error });
2195
+ }
2196
+ const result = validate4(data);
2197
+ if (!result.ok) {
2198
+ const lines = result.errors.map((e) => ` ${e.pointer || "/"}: ${e.message}`);
2199
+ throw new Error(`'${path}' is not a valid takuhon profile:
2200
+ ${lines.join("\n")}`);
2201
+ }
2202
+ return result.data;
2203
+ }
2204
+ async function runMcp(args = [], deps = {}) {
2205
+ const err = deps.stderr ?? ((text) => void process.stderr.write(text));
2206
+ if (args[0] === "--help" || args[0] === "-h") {
2207
+ const out = deps.stdout ?? ((text) => void process.stdout.write(text));
2208
+ out(USAGE6);
2209
+ return 0;
2210
+ }
2211
+ const parsed = parseArgs6(args);
2212
+ if ("error" in parsed) {
2213
+ err(`${parsed.error}
2214
+ Run \`takuhon mcp --help\` for usage.
2215
+ `);
2216
+ return 2;
2217
+ }
2218
+ try {
2219
+ loadMcpProfile(parsed.path);
2220
+ } catch (error) {
2221
+ err(`takuhon: ${error instanceof Error ? error.message : String(error)}
2222
+ `);
2223
+ return 2;
2224
+ }
2225
+ const server = createTakuhonMcpServer({
2226
+ // Re-read per request so edits are reflected; if the file later becomes
2227
+ // invalid, the load failure surfaces to the client as a tool error result.
2228
+ loadProfile: () => loadMcpProfile(parsed.path),
2229
+ name: SERVER_NAME,
2230
+ version: VERSION
2231
+ });
2232
+ const transport = (deps.createTransport ?? (() => new StdioServerTransport()))();
2233
+ await server.connect(transport);
2234
+ err(`takuhon mcp: serving ${parsed.path} (read-only) on stdio
2235
+ `);
2236
+ return await new Promise((resolve7) => {
2237
+ let done = false;
2238
+ function finish() {
2239
+ if (done) return;
2240
+ done = true;
2241
+ process.removeListener("SIGINT", shutdown);
2242
+ process.removeListener("SIGTERM", shutdown);
2243
+ resolve7(0);
2244
+ }
2245
+ function shutdown() {
2246
+ void server.close();
2247
+ finish();
2248
+ }
2249
+ server.server.onclose = finish;
2250
+ process.once("SIGINT", shutdown);
2251
+ process.once("SIGTERM", shutdown);
2252
+ });
2253
+ }
2254
+ function parseArgs6(args) {
2255
+ let path;
2256
+ for (const arg of args) {
2257
+ if (arg.startsWith("-")) {
2258
+ return { error: `takuhon: unknown option \`${arg}\` for \`mcp\`.` };
2259
+ }
2260
+ if (path !== void 0) {
2261
+ return { error: "takuhon: `mcp` takes at most one path argument." };
2262
+ }
2263
+ path = arg;
2264
+ }
2265
+ return { path: path ?? DEFAULT_PATH7 };
2266
+ }
2267
+ function readCliVersion() {
2268
+ try {
2269
+ const pkg2 = JSON.parse(readFileSync10(new URL("../package.json", import.meta.url), "utf8"));
2270
+ return pkg2.version;
2271
+ } catch {
2272
+ return "0.0.0";
2273
+ }
2274
+ }
2275
+
2276
+ // src/migrate-command.ts
2277
+ import { readFileSync as readFileSync11 } from "fs";
2152
2278
  import { resolve as resolve5 } from "path";
2153
2279
  import {
2154
2280
  MigrationError as MigrationError2,
2155
2281
  SCHEMA_VERSION as SCHEMA_VERSION2,
2156
2282
  SUPPORTED_SCHEMA_VERSIONS,
2157
2283
  migrateTakuhon as migrateTakuhon2,
2158
- validate as validate4
2284
+ validate as validate5
2159
2285
  } from "@takuhon/core";
2160
- var DEFAULT_PATH7 = "takuhon.json";
2161
- var USAGE6 = `Usage: takuhon migrate [path] [--to <version>] [--out <file>] [--dry-run]
2286
+ var DEFAULT_PATH8 = "takuhon.json";
2287
+ var USAGE7 = `Usage: takuhon migrate [path] [--to <version>] [--out <file>] [--dry-run]
2162
2288
 
2163
2289
  Forward-migrate a takuhon.json to a newer schema version. The source version
2164
2290
  is read from the file's own schemaVersion. With no path, migrates
@@ -2180,9 +2306,9 @@ Exit codes: 0 = migrated / already current / dry-run, 1 = cannot migrate,
2180
2306
  `;
2181
2307
  function runMigrate(args = [], deps = {}) {
2182
2308
  if (args[0] === "--help" || args[0] === "-h") {
2183
- return { code: 0, stdout: USAGE6, stderr: "" };
2309
+ return { code: 0, stdout: USAGE7, stderr: "" };
2184
2310
  }
2185
- const parsed = parseArgs6(args);
2311
+ const parsed = parseArgs7(args);
2186
2312
  if ("error" in parsed) {
2187
2313
  return {
2188
2314
  code: 2,
@@ -2195,7 +2321,7 @@ Run \`takuhon migrate --help\` for usage.
2195
2321
  const now = deps.now ?? (() => /* @__PURE__ */ new Date());
2196
2322
  return migrateFile(parsed, now);
2197
2323
  }
2198
- function parseArgs6(args) {
2324
+ function parseArgs7(args) {
2199
2325
  let path;
2200
2326
  let to;
2201
2327
  let out;
@@ -2238,13 +2364,13 @@ function parseArgs6(args) {
2238
2364
  error: `takuhon: unsupported --to version "${target}". Supported: ${SUPPORTED_SCHEMA_VERSIONS.join(", ")}.`
2239
2365
  };
2240
2366
  }
2241
- return { path: path ?? DEFAULT_PATH7, to: target, out, dryRun };
2367
+ return { path: path ?? DEFAULT_PATH8, to: target, out, dryRun };
2242
2368
  }
2243
2369
  function migrateFile(parsed, now) {
2244
2370
  const { path, to: target, out, dryRun } = parsed;
2245
2371
  let raw;
2246
2372
  try {
2247
- raw = readFileSync10(path, "utf8");
2373
+ raw = readFileSync11(path, "utf8");
2248
2374
  } catch {
2249
2375
  return {
2250
2376
  code: 2,
@@ -2295,7 +2421,7 @@ Migrations are forward-only; to move to an older schema, restore from a backup w
2295
2421
  }
2296
2422
  throw error;
2297
2423
  }
2298
- const revalidated = validate4(migrated);
2424
+ const revalidated = validate5(migrated);
2299
2425
  if (!revalidated.ok) {
2300
2426
  const lines2 = revalidated.errors.map((e) => ` ${e.pointer || "/"}: ${e.message}`);
2301
2427
  return {
@@ -2358,11 +2484,11 @@ ${lines2.join("\n")}
2358
2484
  }
2359
2485
 
2360
2486
  // src/refresh-admin-command.ts
2361
- import { readFileSync as readFileSync11 } from "fs";
2487
+ import { readFileSync as readFileSync12 } from "fs";
2362
2488
  import { cp, mkdtemp, readdir, rename, rm, stat } from "fs/promises";
2363
2489
  import { join as join7, resolve as resolve6 } from "path";
2364
2490
  var PROFILE_FILENAME = "takuhon.json";
2365
- var USAGE7 = `Usage: takuhon admin update [path]
2491
+ var USAGE8 = `Usage: takuhon admin update [path]
2366
2492
 
2367
2493
  Refresh a project's ${ADMIN_DIST_DIRNAME}/ with the admin form-UI bundle shipped in
2368
2494
  this @takuhon/cli. Use it after upgrading @takuhon/cli to pick up a newer admin
@@ -2375,7 +2501,7 @@ no ${ADMIN_DIST_DIRNAME}/ to refresh / copy failed.
2375
2501
  `;
2376
2502
  async function runRefreshAdmin(args = [], opts = {}) {
2377
2503
  if (args[0] === "--help" || args[0] === "-h") {
2378
- return { code: 0, stdout: USAGE7, stderr: "" };
2504
+ return { code: 0, stdout: USAGE8, stderr: "" };
2379
2505
  }
2380
2506
  const unknownFlag = args.find((arg) => arg.startsWith("-"));
2381
2507
  if (unknownFlag !== void 0) {
@@ -2458,7 +2584,7 @@ Run \`takuhon admin update --help\` for usage.
2458
2584
  };
2459
2585
  }
2460
2586
  function readVersion() {
2461
- const pkg2 = JSON.parse(readFileSync11(new URL("../package.json", import.meta.url), "utf8"));
2587
+ const pkg2 = JSON.parse(readFileSync12(new URL("../package.json", import.meta.url), "utf8"));
2462
2588
  return pkg2.version;
2463
2589
  }
2464
2590
  function isNodeErrnoException(err) {
@@ -2466,10 +2592,10 @@ function isNodeErrnoException(err) {
2466
2592
  }
2467
2593
 
2468
2594
  // src/restore-command.ts
2469
- import { readFileSync as readFileSync12 } from "fs";
2470
- import { validate as validate5 } from "@takuhon/core";
2471
- var DEFAULT_PATH8 = "takuhon.json";
2472
- var USAGE8 = `Usage: takuhon restore --from <backup> [path] [--yes]
2595
+ import { readFileSync as readFileSync13 } from "fs";
2596
+ import { validate as validate6 } from "@takuhon/core";
2597
+ var DEFAULT_PATH9 = "takuhon.json";
2598
+ var USAGE9 = `Usage: takuhon restore --from <backup> [path] [--yes]
2473
2599
 
2474
2600
  Overwrite a profile with a previously saved backup. With no path, restores
2475
2601
  ./takuhon.json in the current working directory.
@@ -2486,9 +2612,9 @@ Exit codes: 0 = restored / aborted, 1 = backup failed validation,
2486
2612
  `;
2487
2613
  async function runRestore(args = [], deps = {}) {
2488
2614
  if (args[0] === "--help" || args[0] === "-h") {
2489
- return { code: 0, stdout: USAGE8, stderr: "" };
2615
+ return { code: 0, stdout: USAGE9, stderr: "" };
2490
2616
  }
2491
- const parsed = parseArgs7(args);
2617
+ const parsed = parseArgs8(args);
2492
2618
  if ("error" in parsed) {
2493
2619
  return {
2494
2620
  code: 2,
@@ -2501,7 +2627,7 @@ Run \`takuhon restore --help\` for usage.
2501
2627
  const now = deps.now ?? (() => /* @__PURE__ */ new Date());
2502
2628
  return restoreFile(parsed, now, deps.confirm);
2503
2629
  }
2504
- function parseArgs7(args) {
2630
+ function parseArgs8(args) {
2505
2631
  let from;
2506
2632
  let path;
2507
2633
  let yes = false;
@@ -2535,13 +2661,13 @@ function parseArgs7(args) {
2535
2661
  if (from === void 0 || from.length === 0) {
2536
2662
  return { error: "takuhon: `restore` requires `--from <backup>`." };
2537
2663
  }
2538
- return { from, path: path ?? DEFAULT_PATH8, yes };
2664
+ return { from, path: path ?? DEFAULT_PATH9, yes };
2539
2665
  }
2540
2666
  async function restoreFile(parsed, now, confirm) {
2541
2667
  const { from, path, yes } = parsed;
2542
2668
  let backupRaw;
2543
2669
  try {
2544
- backupRaw = readFileSync12(from, "utf8");
2670
+ backupRaw = readFileSync13(from, "utf8");
2545
2671
  } catch {
2546
2672
  return { code: 2, stdout: "", stderr: `takuhon: cannot read backup '${from}'.
2547
2673
  ` };
@@ -2558,7 +2684,7 @@ async function restoreFile(parsed, now, confirm) {
2558
2684
  `
2559
2685
  };
2560
2686
  }
2561
- const result = validate5(backupData);
2687
+ const result = validate6(backupData);
2562
2688
  if (!result.ok) {
2563
2689
  const lines2 = result.errors.map((e) => ` ${e.pointer || "/"}: ${e.message}`);
2564
2690
  return {
@@ -2571,7 +2697,7 @@ ${lines2.join("\n")}
2571
2697
  }
2572
2698
  let currentRaw;
2573
2699
  try {
2574
- currentRaw = readFileSync12(path, "utf8");
2700
+ currentRaw = readFileSync13(path, "utf8");
2575
2701
  } catch (error) {
2576
2702
  if (!isNotFound6(error)) {
2577
2703
  return { code: 2, stdout: "", stderr: `takuhon: cannot read current profile '${path}'.
@@ -2641,12 +2767,12 @@ function isNotFound6(error) {
2641
2767
  }
2642
2768
 
2643
2769
  // src/sync-command.ts
2644
- import { readFileSync as readFileSync13 } from "fs";
2645
- import { validate as validate6 } from "@takuhon/core";
2646
- var DEFAULT_PATH9 = "takuhon.json";
2770
+ import { readFileSync as readFileSync14 } from "fs";
2771
+ import { validate as validate7 } from "@takuhon/core";
2772
+ var DEFAULT_PATH10 = "takuhon.json";
2647
2773
  var ADMIN_PROFILE_PATH = "/api/admin/profile";
2648
2774
  var TOKEN_ENV = "TAKUHON_ADMIN_TOKEN";
2649
- var USAGE9 = `Usage: takuhon sync [path] --url <base-url> [--if-match <etag>] [--dry-run]
2775
+ var USAGE10 = `Usage: takuhon sync [path] --url <base-url> [--if-match <etag>] [--dry-run]
2650
2776
 
2651
2777
  Push a local takuhon.json to a deployed takuhon instance by calling its admin
2652
2778
  write endpoint (PUT <base-url>/api/admin/profile). With no path, syncs
@@ -2672,9 +2798,9 @@ unset / auth failure / network error / other non-success response.
2672
2798
  `;
2673
2799
  async function runSync(args = [], deps = {}) {
2674
2800
  if (args[0] === "--help" || args[0] === "-h") {
2675
- return { code: 0, stdout: USAGE9, stderr: "" };
2801
+ return { code: 0, stdout: USAGE10, stderr: "" };
2676
2802
  }
2677
- const parsed = parseArgs8(args);
2803
+ const parsed = parseArgs9(args);
2678
2804
  if ("error" in parsed) {
2679
2805
  return {
2680
2806
  code: 2,
@@ -2686,7 +2812,7 @@ Run \`takuhon sync --help\` for usage.
2686
2812
  }
2687
2813
  return syncProfile(parsed, deps);
2688
2814
  }
2689
- function parseArgs8(args) {
2815
+ function parseArgs9(args) {
2690
2816
  let path;
2691
2817
  let url;
2692
2818
  let ifMatch;
@@ -2732,7 +2858,7 @@ function parseArgs8(args) {
2732
2858
  }
2733
2859
  const base = parseOrigin(url);
2734
2860
  if ("error" in base) return base;
2735
- return { path: path ?? DEFAULT_PATH9, url: base.origin, ifMatch, dryRun };
2861
+ return { path: path ?? DEFAULT_PATH10, url: base.origin, ifMatch, dryRun };
2736
2862
  }
2737
2863
  function parseOrigin(value) {
2738
2864
  let parsed;
@@ -2765,7 +2891,7 @@ async function syncProfile(parsed, deps) {
2765
2891
  const { path, url, ifMatch, dryRun } = parsed;
2766
2892
  let raw;
2767
2893
  try {
2768
- raw = readFileSync13(path, "utf8");
2894
+ raw = readFileSync14(path, "utf8");
2769
2895
  } catch {
2770
2896
  return { code: 2, stdout: "", stderr: `takuhon: cannot read '${path}'.
2771
2897
  ` };
@@ -2778,7 +2904,7 @@ async function syncProfile(parsed, deps) {
2778
2904
  return { code: 2, stdout: "", stderr: `takuhon: '${path}' is not valid JSON: ${detail}
2779
2905
  ` };
2780
2906
  }
2781
- const result = validate6(data);
2907
+ const result = validate7(data);
2782
2908
  if (!result.ok) {
2783
2909
  const lines = result.errors.map((e) => ` ${e.pointer || "/"}: ${e.message}`);
2784
2910
  return {
@@ -2902,10 +3028,10 @@ async function readProblem(res) {
2902
3028
  }
2903
3029
 
2904
3030
  // src/validate-command.ts
2905
- import { readFileSync as readFileSync14 } from "fs";
2906
- import { validate as validate7 } from "@takuhon/core";
2907
- var DEFAULT_PATH10 = "takuhon.json";
2908
- var USAGE10 = `Usage: takuhon validate [path]
3031
+ import { readFileSync as readFileSync15 } from "fs";
3032
+ import { validate as validate8 } from "@takuhon/core";
3033
+ var DEFAULT_PATH11 = "takuhon.json";
3034
+ var USAGE11 = `Usage: takuhon validate [path]
2909
3035
 
2910
3036
  Validate a takuhon.json against the takuhon schema. With no path, validates
2911
3037
  ./takuhon.json in the current working directory.
@@ -2914,7 +3040,7 @@ Exit codes: 0 = valid, 1 = invalid, 2 = file missing / unreadable / not JSON.
2914
3040
  `;
2915
3041
  function runValidate(args = []) {
2916
3042
  if (args[0] === "--help" || args[0] === "-h") {
2917
- return { code: 0, stdout: USAGE10, stderr: "" };
3043
+ return { code: 0, stdout: USAGE11, stderr: "" };
2918
3044
  }
2919
3045
  if (args.length > 1) {
2920
3046
  return {
@@ -2926,10 +3052,10 @@ function runValidate(args = []) {
2926
3052
  return validateFile(args[0]);
2927
3053
  }
2928
3054
  function validateFile(pathArg) {
2929
- const target = pathArg ?? DEFAULT_PATH10;
3055
+ const target = pathArg ?? DEFAULT_PATH11;
2930
3056
  let raw;
2931
3057
  try {
2932
- raw = readFileSync14(target, "utf8");
3058
+ raw = readFileSync15(target, "utf8");
2933
3059
  } catch {
2934
3060
  return {
2935
3061
  code: 2,
@@ -2950,7 +3076,7 @@ function validateFile(pathArg) {
2950
3076
  `
2951
3077
  };
2952
3078
  }
2953
- const result = validate7(data);
3079
+ const result = validate8(data);
2954
3080
  if (result.ok) {
2955
3081
  return {
2956
3082
  code: 0,
@@ -2971,9 +3097,9 @@ ${lines.join("\n")}
2971
3097
  }
2972
3098
 
2973
3099
  // src/index.ts
2974
- var pkg = JSON.parse(readFileSync15(new URL("../package.json", import.meta.url), "utf8"));
2975
- var VERSION = pkg.version;
2976
- var HELP = `takuhon ${VERSION}
3100
+ var pkg = JSON.parse(readFileSync16(new URL("../package.json", import.meta.url), "utf8"));
3101
+ var VERSION2 = pkg.version;
3102
+ var HELP = `takuhon ${VERSION2}
2977
3103
 
2978
3104
  Takuhon \u2014 open-source portable profile API server.
2979
3105
 
@@ -3004,6 +3130,9 @@ Commands:
3004
3130
  takuhon admin update [path] Refresh a project's admin-dist/ with the admin form UI
3005
3131
  bundled in this @takuhon/cli (use after upgrading the
3006
3132
  CLI). Updates an existing admin-dist/ only.
3133
+ takuhon mcp [path] Serve a takuhon.json over the Model Context Protocol on
3134
+ stdio (read-only), so an MCP client such as Claude Desktop
3135
+ can read the profile. Re-reads the file per request.
3007
3136
  takuhon sync [path] --url <url> Push a takuhon.json to a deployment's admin API
3008
3137
  (PUT <url>/api/admin/profile). Reads the admin token
3009
3138
  from TAKUHON_ADMIN_TOKEN. --if-match <etag> opts into
@@ -3021,7 +3150,7 @@ Scaffolding a new profile project:
3021
3150
  async function main(argv) {
3022
3151
  const first = argv[0];
3023
3152
  if (first === "--version" || first === "-v") {
3024
- process.stdout.write(`${VERSION}
3153
+ process.stdout.write(`${VERSION2}
3025
3154
  `);
3026
3155
  return 0;
3027
3156
  }
@@ -3050,6 +3179,9 @@ async function main(argv) {
3050
3179
  }
3051
3180
  return runAdmin(argv.slice(1));
3052
3181
  }
3182
+ if (first === "mcp") {
3183
+ return runMcp(argv.slice(1));
3184
+ }
3053
3185
  if (first === "import") {
3054
3186
  return emit(runImport(argv.slice(1)));
3055
3187
  }