@takuhon/cli 0.12.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-XMSWT5NY.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";
@@ -355,18 +355,32 @@ import { applyPublicPrivacyFilter, normalize, validate } from "@takuhon/core";
355
355
 
356
356
  // src/build-html.ts
357
357
  import { generateJsonLd, renderActivitySvg } from "@takuhon/core";
358
+
359
+ // src/html-helpers.ts
358
360
  function escapeHtml(value) {
359
361
  return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
360
362
  }
361
- function escapeJsonLd(json) {
362
- return json.replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026");
363
- }
364
363
  function safeUrl(url) {
365
364
  const trimmed = url.trim();
366
365
  const scheme = /^([a-zA-Z][a-zA-Z0-9+.-]*):/.exec(trimmed)?.[1]?.toLowerCase();
367
366
  if (scheme === void 0) return trimmed;
368
367
  return scheme === "http" || scheme === "https" || scheme === "mailto" ? trimmed : void 0;
369
368
  }
369
+ function dateRange(start, end, isCurrent) {
370
+ const left = start ?? "";
371
+ const right = isCurrent === true || end === null ? "Present" : end ?? "";
372
+ if (left && right) return `${left} \u2013 ${right}`;
373
+ return left || right;
374
+ }
375
+ function nonEmpty(values, separator) {
376
+ const joined = values.filter((v) => typeof v === "string" && v.length > 0).join(separator);
377
+ return joined.length > 0 ? joined : void 0;
378
+ }
379
+
380
+ // src/build-html.ts
381
+ function escapeJsonLd(json) {
382
+ return json.replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026");
383
+ }
370
384
  var CSS = `:root{--fg:#1a1a1a;--muted:#666;--accent:#0b5fff;--line:#e5e5e5}
371
385
  *{box-sizing:border-box}
372
386
  body{margin:0;color:var(--fg);font:16px/1.6 system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;background:#fff}
@@ -392,16 +406,6 @@ ul{padding:0;margin:0;list-style:none}
392
406
  nav.locales{display:flex;gap:.75rem;margin-bottom:1rem;font-size:.9rem}
393
407
  .activity svg{max-width:100%;height:auto}
394
408
  footer.powered{max-width:42rem;margin:0 auto;padding:1.5rem 1.25rem;color:var(--muted);font-size:.85rem}`;
395
- function dateRange(start, end, isCurrent) {
396
- const left = start ?? "";
397
- const right = isCurrent === true || end === null ? "Present" : end ?? "";
398
- if (left && right) return `${left} \u2013 ${right}`;
399
- return left || right;
400
- }
401
- function nonEmpty(values, separator) {
402
- const joined = values.filter((v) => typeof v === "string" && v.length > 0).join(separator);
403
- return joined.length > 0 ? joined : void 0;
404
- }
405
409
  function renderEntry(entry) {
406
410
  const href = entry.url ? safeUrl(entry.url) : void 0;
407
411
  const heading = href ? `<a href="${escapeHtml(href)}">${escapeHtml(entry.heading)}</a>` : escapeHtml(entry.heading);
@@ -648,14 +652,226 @@ ${footer ? `${footer}
648
652
  }
649
653
 
650
654
  // src/site.ts
651
- import { resolveLocale } from "@takuhon/core";
655
+ import { deriveCv, resolveLocale } from "@takuhon/core";
656
+
657
+ // src/cv-html.ts
658
+ var SECTION_TITLES = {
659
+ experience: "Experience",
660
+ education: "Education",
661
+ skills: "Skills",
662
+ certifications: "Certifications",
663
+ publications: "Publications",
664
+ honors: "Honors & Awards",
665
+ courses: "Courses",
666
+ patents: "Patents",
667
+ languages: "Languages",
668
+ volunteering: "Volunteering",
669
+ memberships: "Memberships"
670
+ };
671
+ var CSS2 = `:root{--fg:#1a1a1a;--muted:#555;--accent:#0b5fff;--line:#d9d9d9}
672
+ *{box-sizing:border-box}
673
+ body{margin:0;background:#f3f4f6;color:var(--fg);font:13px/1.5 system-ui,-apple-system,"Segoe UI",Roboto,sans-serif}
674
+ main{background:#fff;width:210mm;min-height:297mm;margin:1.5rem auto;padding:18mm 16mm;box-shadow:0 1px 6px rgba(0,0,0,.15)}
675
+ h1{font-size:1.7rem;margin:0}
676
+ .tagline{font-size:1.05rem;color:var(--muted);margin:.15rem 0 0}
677
+ .contact{color:var(--muted);font-size:.85rem;margin:.4rem 0 0;display:flex;flex-wrap:wrap;gap:.25rem 1rem}
678
+ .contact a{color:var(--accent)}
679
+ .bio{margin:.75rem 0 0}
680
+ header{border-bottom:2px solid var(--fg);padding-bottom:.6rem;margin-bottom:.4rem}
681
+ section{margin-top:1.1rem}
682
+ h2{font-size:.95rem;text-transform:uppercase;letter-spacing:.05em;color:var(--accent);border-bottom:1px solid var(--line);margin:0 0 .5rem;padding-bottom:.15rem}
683
+ ul{padding:0;margin:0;list-style:none}
684
+ .entry{margin:0 0 .7rem;break-inside:avoid}
685
+ .entry .row{display:flex;justify-content:space-between;gap:1rem;align-items:baseline}
686
+ .entry h3{font-size:.95rem;margin:0}
687
+ .entry .dates{color:var(--muted);font-size:.8rem;white-space:nowrap}
688
+ .entry .sub{color:var(--muted);margin:.05rem 0}
689
+ .entry p{margin:.2rem 0 0}
690
+ .entry a{color:var(--accent)}
691
+ .chips{display:flex;flex-wrap:wrap;gap:.3rem}
692
+ .chips li{border:1px solid var(--line);border-radius:1rem;padding:.05rem .55rem;font-size:.8rem}
693
+ @media print{
694
+ body{background:#fff}
695
+ main{width:auto;min-height:0;margin:0;padding:0;box-shadow:none}
696
+ a{color:var(--fg)}
697
+ }
698
+ @page{size:A4;margin:14mm}`;
699
+ function renderEntry2(entry) {
700
+ const href = entry.url ? safeUrl(entry.url) : void 0;
701
+ const title = href ? `<a href="${escapeHtml(href)}">${escapeHtml(entry.heading)}</a>` : escapeHtml(entry.heading);
702
+ const dates = entry.dates ? `<span class="dates">${escapeHtml(entry.dates)}</span>` : "";
703
+ const parts = [`<div class="row"><h3>${title}</h3>${dates}</div>`];
704
+ if (entry.sub) parts.push(`<p class="sub">${escapeHtml(entry.sub)}</p>`);
705
+ if (entry.body) parts.push(`<p>${escapeHtml(entry.body)}</p>`);
706
+ return `<li class="entry">${parts.join("")}</li>`;
707
+ }
708
+ function entryListSection(title, entries) {
709
+ return `<section><h2>${escapeHtml(title)}</h2><ul>${entries.map(renderEntry2).join("")}</ul></section>`;
710
+ }
711
+ function chipSection(title, labels) {
712
+ const chips = labels.map((l) => `<li>${escapeHtml(l)}</li>`).join("");
713
+ return `<section><h2>${escapeHtml(title)}</h2><ul class="chips">${chips}</ul></section>`;
714
+ }
715
+ function languageLabel(l) {
716
+ return `${l.displayName ?? l.language} \u2014 ${l.proficiency}`;
717
+ }
718
+ function renderSection(section) {
719
+ const title = SECTION_TITLES[section.kind];
720
+ switch (section.kind) {
721
+ case "experience":
722
+ return entryListSection(
723
+ title,
724
+ section.entries.map((c) => ({
725
+ heading: c.role,
726
+ sub: c.organization,
727
+ dates: dateRange(c.startDate, c.endDate, c.isCurrent),
728
+ body: c.description,
729
+ url: c.url
730
+ }))
731
+ );
732
+ case "education":
733
+ return entryListSection(
734
+ title,
735
+ section.entries.map((e) => {
736
+ const degree = nonEmpty([e.degree, e.fieldOfStudy], ", ");
737
+ return {
738
+ heading: degree ?? e.institution,
739
+ sub: degree ? e.institution : void 0,
740
+ dates: dateRange(e.startDate, e.endDate, e.isCurrent),
741
+ body: e.description,
742
+ url: e.url
743
+ };
744
+ })
745
+ );
746
+ case "skills":
747
+ return chipSection(
748
+ title,
749
+ section.entries.map((s) => s.label)
750
+ );
751
+ case "languages":
752
+ return chipSection(title, section.entries.map(languageLabel));
753
+ case "certifications":
754
+ return entryListSection(
755
+ title,
756
+ section.entries.map((c) => ({
757
+ heading: c.title,
758
+ sub: c.issuingOrganization,
759
+ dates: dateRange(c.issueDate, c.expirationDate),
760
+ url: c.url
761
+ }))
762
+ );
763
+ case "publications":
764
+ return entryListSection(
765
+ title,
766
+ section.entries.map((x) => ({
767
+ heading: x.title,
768
+ sub: nonEmpty([x.publisher, x.coAuthors?.join(", ")], " \xB7 "),
769
+ dates: dateRange(x.date),
770
+ body: x.description,
771
+ url: x.url ?? (x.doi ? `https://doi.org/${x.doi}` : void 0)
772
+ }))
773
+ );
774
+ case "honors":
775
+ return entryListSection(
776
+ title,
777
+ section.entries.map((x) => ({
778
+ heading: x.title,
779
+ sub: x.issuer,
780
+ dates: dateRange(x.date),
781
+ body: x.description,
782
+ url: x.url
783
+ }))
784
+ );
785
+ case "courses":
786
+ return entryListSection(
787
+ title,
788
+ section.entries.map((x) => ({
789
+ heading: x.title,
790
+ sub: x.provider,
791
+ dates: dateRange(x.completionDate),
792
+ body: x.description,
793
+ url: x.certificateUrl
794
+ }))
795
+ );
796
+ case "patents":
797
+ return entryListSection(
798
+ title,
799
+ section.entries.map((x) => ({
800
+ heading: x.title,
801
+ sub: nonEmpty([x.patentNumber, x.office, x.status], " \xB7 "),
802
+ dates: dateRange(x.filingDate ?? x.grantDate),
803
+ body: x.description,
804
+ url: x.url
805
+ }))
806
+ );
807
+ case "volunteering":
808
+ return entryListSection(
809
+ title,
810
+ section.entries.map((x) => ({
811
+ heading: x.role,
812
+ sub: nonEmpty([x.organization, x.cause], " \xB7 "),
813
+ dates: dateRange(x.startDate, x.endDate, x.isCurrent),
814
+ body: x.description,
815
+ url: x.url
816
+ }))
817
+ );
818
+ case "memberships":
819
+ return entryListSection(
820
+ title,
821
+ section.entries.map((x) => ({
822
+ heading: x.role ?? x.organization,
823
+ sub: x.role ? x.organization : void 0,
824
+ dates: dateRange(x.startDate, x.endDate, x.isCurrent),
825
+ body: x.description,
826
+ url: x.url
827
+ }))
828
+ );
829
+ }
830
+ }
831
+ function renderHeader2(cv) {
832
+ const h = cv.header;
833
+ const parts = [`<h1>${escapeHtml(h.displayName)}</h1>`];
834
+ if (h.tagline) parts.push(`<p class="tagline">${escapeHtml(h.tagline)}</p>`);
835
+ const contact = [];
836
+ if (h.location) contact.push(`<span>${escapeHtml(h.location)}</span>`);
837
+ if (h.email) {
838
+ contact.push(`<a href="mailto:${escapeHtml(h.email)}">${escapeHtml(h.email)}</a>`);
839
+ }
840
+ const formHref = h.formUrl ? safeUrl(h.formUrl) : void 0;
841
+ if (formHref) contact.push(`<a href="${escapeHtml(formHref)}">Contact</a>`);
842
+ if (contact.length > 0) parts.push(`<div class="contact">${contact.join("")}</div>`);
843
+ if (h.bio) parts.push(`<p class="bio">${escapeHtml(h.bio)}</p>`);
844
+ return `<header>${parts.join("")}</header>`;
845
+ }
846
+ function renderCvHtml(cv) {
847
+ const title = `${cv.header.displayName} \u2014 CV`;
848
+ const body = [renderHeader2(cv), ...cv.sections.map(renderSection)].join("\n");
849
+ return `<!DOCTYPE html>
850
+ <html lang="${escapeHtml(cv.resolvedLocale)}">
851
+ <head>
852
+ <meta charset="utf-8">
853
+ <meta name="viewport" content="width=device-width, initial-scale=1">
854
+ <title>${escapeHtml(title)}</title>
855
+ <style>${CSS2}</style>
856
+ </head>
857
+ <body>
858
+ <main>
859
+ ${body}
860
+ </main>
861
+ </body>
862
+ </html>
863
+ `;
864
+ }
865
+
866
+ // src/site.ts
652
867
  function generateSite(profile, options = {}) {
653
868
  const { baseUrl } = options;
654
869
  const defaultLocale = profile.settings.defaultLocale;
655
870
  const locales = [.../* @__PURE__ */ new Set([defaultLocale, ...profile.settings.availableLocales])];
656
871
  const jsonLd = profile.settings.enableJsonLd !== false;
657
872
  const activitySnapshot = profile.settings.activity?.enabled === true ? options.activitySnapshot ?? void 0 : void 0;
658
- return locales.map((locale) => {
873
+ const pages = [];
874
+ for (const locale of locales) {
659
875
  const localized = resolveLocale(profile, locale);
660
876
  const isDefault = locale === defaultLocale;
661
877
  const localeNav = locales.map((to) => ({
@@ -673,12 +889,20 @@ function generateSite(profile, options = {}) {
673
889
  jsonLd,
674
890
  activitySnapshot
675
891
  });
676
- return {
892
+ pages.push({
677
893
  route: isDefault ? "/" : `/${locale}/`,
678
894
  file: isDefault ? "index.html" : `${locale}/index.html`,
679
895
  html
680
- };
681
- });
896
+ });
897
+ if (options.cv === true) {
898
+ pages.push({
899
+ route: isDefault ? "/cv/" : `/${locale}/cv/`,
900
+ file: isDefault ? "cv.html" : `${locale}/cv.html`,
901
+ html: renderCvHtml(deriveCv(localized))
902
+ });
903
+ }
904
+ }
905
+ return pages;
682
906
  }
683
907
  function absoluteUrl(baseUrl, locale, defaultLocale) {
684
908
  return locale === defaultLocale ? `${baseUrl}/` : `${baseUrl}/${locale}/`;
@@ -706,10 +930,10 @@ var DEFAULT_PATH2 = "takuhon.json";
706
930
  var DEFAULT_PORT = 4321;
707
931
  var USAGE = `Usage: takuhon dev [path] [--port <n>] [--base-url <url>]
708
932
 
709
- Serve a takuhon.json as a local static preview (one page per locale) \u2014 the same
710
- surface \`takuhon build\` produces. With no path, serves ./takuhon.json. The file
711
- is re-read and re-rendered on every request, so edit it and reload the browser
712
- to see changes. Stop with Ctrl-C.
933
+ Serve a takuhon.json as a local static preview (one page per locale, plus a
934
+ print-ready CV page at /cv) \u2014 the same surface \`takuhon build\` produces. With
935
+ no path, serves ./takuhon.json. The file is re-read and re-rendered on every
936
+ request, so edit it and reload the browser to see changes. Stop with Ctrl-C.
713
937
 
714
938
  Options:
715
939
  --port <n> Port to listen on (default: ${DEFAULT_PORT}).
@@ -749,7 +973,7 @@ ${lines.join("\n")}`
749
973
  const filtered = applyPublicPrivacyFilter(normalize(result.data));
750
974
  const activitySnapshot = filtered.settings.activity?.enabled === true ? readActivitySnapshotSync(path) : null;
751
975
  const pages = new Map(
752
- generateSite(filtered, { baseUrl, activitySnapshot }).map((p) => [p.route, p.html])
976
+ generateSite(filtered, { baseUrl, activitySnapshot, cv: true }).map((p) => [p.route, p.html])
753
977
  );
754
978
  return { ok: true, pages };
755
979
  }
@@ -1472,7 +1696,7 @@ import { dirname as dirname5, join as join6 } from "path";
1472
1696
  import { applyPublicPrivacyFilter as applyPublicPrivacyFilter2, normalize as normalize2, validate as validate2 } from "@takuhon/core";
1473
1697
  var DEFAULT_PATH4 = "takuhon.json";
1474
1698
  var DEFAULT_OUTPUT = "dist";
1475
- var USAGE3 = `Usage: takuhon build [path] [--output <dir>] [--base-url <url>]
1699
+ var USAGE3 = `Usage: takuhon build [path] [--output <dir>] [--base-url <url>] [--cv]
1476
1700
 
1477
1701
  Render a takuhon.json into a static site (one HTML page per locale, with
1478
1702
  build-time Schema.org JSON-LD). With no path, builds ./takuhon.json.
@@ -1483,6 +1707,9 @@ Options:
1483
1707
  to <dir>/<locale>/index.html.
1484
1708
  --base-url <url> Site origin (e.g. https://me.example). Enables absolute
1485
1709
  canonical and hreflang links; without it those are omitted.
1710
+ --cv Also emit a print-ready CV/r\xE9sum\xE9 page per locale
1711
+ (<dir>/cv.html and <dir>/<locale>/cv.html). Open it and use
1712
+ the browser's "Save as PDF" to produce a r\xE9sum\xE9 PDF.
1486
1713
 
1487
1714
  The public privacy filter is applied (meta.privacy is honoured). Asset URLs are
1488
1715
  referenced as-is and are not copied. The output directory is written into, not
@@ -1511,8 +1738,13 @@ function parseArgs3(args) {
1511
1738
  let path;
1512
1739
  let output;
1513
1740
  let baseUrl;
1741
+ let cv = false;
1514
1742
  for (let i = 0; i < args.length; i++) {
1515
1743
  const arg = args[i];
1744
+ if (arg === "--cv") {
1745
+ cv = true;
1746
+ continue;
1747
+ }
1516
1748
  if (arg === "--output" || arg === "--base-url") {
1517
1749
  const value = args[i + 1];
1518
1750
  if (value === void 0 || value === "" || value.startsWith("-")) {
@@ -1550,7 +1782,8 @@ function parseArgs3(args) {
1550
1782
  path: path ?? DEFAULT_PATH4,
1551
1783
  output: output ?? DEFAULT_OUTPUT,
1552
1784
  // Drop any trailing slash so URL joins are predictable.
1553
- baseUrl: baseUrl?.replace(/\/+$/, "")
1785
+ baseUrl: baseUrl?.replace(/\/+$/, ""),
1786
+ cv
1554
1787
  };
1555
1788
  }
1556
1789
  function isHttpUrl3(value) {
@@ -1562,7 +1795,7 @@ function isHttpUrl3(value) {
1562
1795
  }
1563
1796
  }
1564
1797
  function buildSite(parsed) {
1565
- const { path, output, baseUrl } = parsed;
1798
+ const { path, output, baseUrl, cv } = parsed;
1566
1799
  let raw;
1567
1800
  try {
1568
1801
  raw = readFileSync7(path, "utf8");
@@ -1597,7 +1830,7 @@ ${lines.join("\n")}
1597
1830
  const activitySnapshot = filtered.settings.activity?.enabled === true ? readActivitySnapshotSync(path) : null;
1598
1831
  const written = [];
1599
1832
  try {
1600
- for (const page of generateSite(filtered, { baseUrl, activitySnapshot })) {
1833
+ for (const page of generateSite(filtered, { baseUrl, activitySnapshot, cv })) {
1601
1834
  const outFile = join6(output, page.file);
1602
1835
  mkdirSync3(dirname5(outFile), { recursive: true });
1603
1836
  writeFileAtomic(outFile, page.html);
@@ -1914,18 +2147,144 @@ function isNotFound5(error) {
1914
2147
  return typeof error === "object" && error !== null && error.code === "ENOENT";
1915
2148
  }
1916
2149
 
1917
- // src/migrate-command.ts
2150
+ // src/mcp-command.ts
1918
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";
1919
2278
  import { resolve as resolve5 } from "path";
1920
2279
  import {
1921
2280
  MigrationError as MigrationError2,
1922
2281
  SCHEMA_VERSION as SCHEMA_VERSION2,
1923
2282
  SUPPORTED_SCHEMA_VERSIONS,
1924
2283
  migrateTakuhon as migrateTakuhon2,
1925
- validate as validate4
2284
+ validate as validate5
1926
2285
  } from "@takuhon/core";
1927
- var DEFAULT_PATH7 = "takuhon.json";
1928
- 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]
1929
2288
 
1930
2289
  Forward-migrate a takuhon.json to a newer schema version. The source version
1931
2290
  is read from the file's own schemaVersion. With no path, migrates
@@ -1947,9 +2306,9 @@ Exit codes: 0 = migrated / already current / dry-run, 1 = cannot migrate,
1947
2306
  `;
1948
2307
  function runMigrate(args = [], deps = {}) {
1949
2308
  if (args[0] === "--help" || args[0] === "-h") {
1950
- return { code: 0, stdout: USAGE6, stderr: "" };
2309
+ return { code: 0, stdout: USAGE7, stderr: "" };
1951
2310
  }
1952
- const parsed = parseArgs6(args);
2311
+ const parsed = parseArgs7(args);
1953
2312
  if ("error" in parsed) {
1954
2313
  return {
1955
2314
  code: 2,
@@ -1962,7 +2321,7 @@ Run \`takuhon migrate --help\` for usage.
1962
2321
  const now = deps.now ?? (() => /* @__PURE__ */ new Date());
1963
2322
  return migrateFile(parsed, now);
1964
2323
  }
1965
- function parseArgs6(args) {
2324
+ function parseArgs7(args) {
1966
2325
  let path;
1967
2326
  let to;
1968
2327
  let out;
@@ -2005,13 +2364,13 @@ function parseArgs6(args) {
2005
2364
  error: `takuhon: unsupported --to version "${target}". Supported: ${SUPPORTED_SCHEMA_VERSIONS.join(", ")}.`
2006
2365
  };
2007
2366
  }
2008
- return { path: path ?? DEFAULT_PATH7, to: target, out, dryRun };
2367
+ return { path: path ?? DEFAULT_PATH8, to: target, out, dryRun };
2009
2368
  }
2010
2369
  function migrateFile(parsed, now) {
2011
2370
  const { path, to: target, out, dryRun } = parsed;
2012
2371
  let raw;
2013
2372
  try {
2014
- raw = readFileSync10(path, "utf8");
2373
+ raw = readFileSync11(path, "utf8");
2015
2374
  } catch {
2016
2375
  return {
2017
2376
  code: 2,
@@ -2062,7 +2421,7 @@ Migrations are forward-only; to move to an older schema, restore from a backup w
2062
2421
  }
2063
2422
  throw error;
2064
2423
  }
2065
- const revalidated = validate4(migrated);
2424
+ const revalidated = validate5(migrated);
2066
2425
  if (!revalidated.ok) {
2067
2426
  const lines2 = revalidated.errors.map((e) => ` ${e.pointer || "/"}: ${e.message}`);
2068
2427
  return {
@@ -2125,11 +2484,11 @@ ${lines2.join("\n")}
2125
2484
  }
2126
2485
 
2127
2486
  // src/refresh-admin-command.ts
2128
- import { readFileSync as readFileSync11 } from "fs";
2487
+ import { readFileSync as readFileSync12 } from "fs";
2129
2488
  import { cp, mkdtemp, readdir, rename, rm, stat } from "fs/promises";
2130
2489
  import { join as join7, resolve as resolve6 } from "path";
2131
2490
  var PROFILE_FILENAME = "takuhon.json";
2132
- var USAGE7 = `Usage: takuhon admin update [path]
2491
+ var USAGE8 = `Usage: takuhon admin update [path]
2133
2492
 
2134
2493
  Refresh a project's ${ADMIN_DIST_DIRNAME}/ with the admin form-UI bundle shipped in
2135
2494
  this @takuhon/cli. Use it after upgrading @takuhon/cli to pick up a newer admin
@@ -2142,7 +2501,7 @@ no ${ADMIN_DIST_DIRNAME}/ to refresh / copy failed.
2142
2501
  `;
2143
2502
  async function runRefreshAdmin(args = [], opts = {}) {
2144
2503
  if (args[0] === "--help" || args[0] === "-h") {
2145
- return { code: 0, stdout: USAGE7, stderr: "" };
2504
+ return { code: 0, stdout: USAGE8, stderr: "" };
2146
2505
  }
2147
2506
  const unknownFlag = args.find((arg) => arg.startsWith("-"));
2148
2507
  if (unknownFlag !== void 0) {
@@ -2225,7 +2584,7 @@ Run \`takuhon admin update --help\` for usage.
2225
2584
  };
2226
2585
  }
2227
2586
  function readVersion() {
2228
- 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"));
2229
2588
  return pkg2.version;
2230
2589
  }
2231
2590
  function isNodeErrnoException(err) {
@@ -2233,10 +2592,10 @@ function isNodeErrnoException(err) {
2233
2592
  }
2234
2593
 
2235
2594
  // src/restore-command.ts
2236
- import { readFileSync as readFileSync12 } from "fs";
2237
- import { validate as validate5 } from "@takuhon/core";
2238
- var DEFAULT_PATH8 = "takuhon.json";
2239
- 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]
2240
2599
 
2241
2600
  Overwrite a profile with a previously saved backup. With no path, restores
2242
2601
  ./takuhon.json in the current working directory.
@@ -2253,9 +2612,9 @@ Exit codes: 0 = restored / aborted, 1 = backup failed validation,
2253
2612
  `;
2254
2613
  async function runRestore(args = [], deps = {}) {
2255
2614
  if (args[0] === "--help" || args[0] === "-h") {
2256
- return { code: 0, stdout: USAGE8, stderr: "" };
2615
+ return { code: 0, stdout: USAGE9, stderr: "" };
2257
2616
  }
2258
- const parsed = parseArgs7(args);
2617
+ const parsed = parseArgs8(args);
2259
2618
  if ("error" in parsed) {
2260
2619
  return {
2261
2620
  code: 2,
@@ -2268,7 +2627,7 @@ Run \`takuhon restore --help\` for usage.
2268
2627
  const now = deps.now ?? (() => /* @__PURE__ */ new Date());
2269
2628
  return restoreFile(parsed, now, deps.confirm);
2270
2629
  }
2271
- function parseArgs7(args) {
2630
+ function parseArgs8(args) {
2272
2631
  let from;
2273
2632
  let path;
2274
2633
  let yes = false;
@@ -2302,13 +2661,13 @@ function parseArgs7(args) {
2302
2661
  if (from === void 0 || from.length === 0) {
2303
2662
  return { error: "takuhon: `restore` requires `--from <backup>`." };
2304
2663
  }
2305
- return { from, path: path ?? DEFAULT_PATH8, yes };
2664
+ return { from, path: path ?? DEFAULT_PATH9, yes };
2306
2665
  }
2307
2666
  async function restoreFile(parsed, now, confirm) {
2308
2667
  const { from, path, yes } = parsed;
2309
2668
  let backupRaw;
2310
2669
  try {
2311
- backupRaw = readFileSync12(from, "utf8");
2670
+ backupRaw = readFileSync13(from, "utf8");
2312
2671
  } catch {
2313
2672
  return { code: 2, stdout: "", stderr: `takuhon: cannot read backup '${from}'.
2314
2673
  ` };
@@ -2325,7 +2684,7 @@ async function restoreFile(parsed, now, confirm) {
2325
2684
  `
2326
2685
  };
2327
2686
  }
2328
- const result = validate5(backupData);
2687
+ const result = validate6(backupData);
2329
2688
  if (!result.ok) {
2330
2689
  const lines2 = result.errors.map((e) => ` ${e.pointer || "/"}: ${e.message}`);
2331
2690
  return {
@@ -2338,7 +2697,7 @@ ${lines2.join("\n")}
2338
2697
  }
2339
2698
  let currentRaw;
2340
2699
  try {
2341
- currentRaw = readFileSync12(path, "utf8");
2700
+ currentRaw = readFileSync13(path, "utf8");
2342
2701
  } catch (error) {
2343
2702
  if (!isNotFound6(error)) {
2344
2703
  return { code: 2, stdout: "", stderr: `takuhon: cannot read current profile '${path}'.
@@ -2408,12 +2767,12 @@ function isNotFound6(error) {
2408
2767
  }
2409
2768
 
2410
2769
  // src/sync-command.ts
2411
- import { readFileSync as readFileSync13 } from "fs";
2412
- import { validate as validate6 } from "@takuhon/core";
2413
- 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";
2414
2773
  var ADMIN_PROFILE_PATH = "/api/admin/profile";
2415
2774
  var TOKEN_ENV = "TAKUHON_ADMIN_TOKEN";
2416
- 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]
2417
2776
 
2418
2777
  Push a local takuhon.json to a deployed takuhon instance by calling its admin
2419
2778
  write endpoint (PUT <base-url>/api/admin/profile). With no path, syncs
@@ -2439,9 +2798,9 @@ unset / auth failure / network error / other non-success response.
2439
2798
  `;
2440
2799
  async function runSync(args = [], deps = {}) {
2441
2800
  if (args[0] === "--help" || args[0] === "-h") {
2442
- return { code: 0, stdout: USAGE9, stderr: "" };
2801
+ return { code: 0, stdout: USAGE10, stderr: "" };
2443
2802
  }
2444
- const parsed = parseArgs8(args);
2803
+ const parsed = parseArgs9(args);
2445
2804
  if ("error" in parsed) {
2446
2805
  return {
2447
2806
  code: 2,
@@ -2453,7 +2812,7 @@ Run \`takuhon sync --help\` for usage.
2453
2812
  }
2454
2813
  return syncProfile(parsed, deps);
2455
2814
  }
2456
- function parseArgs8(args) {
2815
+ function parseArgs9(args) {
2457
2816
  let path;
2458
2817
  let url;
2459
2818
  let ifMatch;
@@ -2499,7 +2858,7 @@ function parseArgs8(args) {
2499
2858
  }
2500
2859
  const base = parseOrigin(url);
2501
2860
  if ("error" in base) return base;
2502
- return { path: path ?? DEFAULT_PATH9, url: base.origin, ifMatch, dryRun };
2861
+ return { path: path ?? DEFAULT_PATH10, url: base.origin, ifMatch, dryRun };
2503
2862
  }
2504
2863
  function parseOrigin(value) {
2505
2864
  let parsed;
@@ -2532,7 +2891,7 @@ async function syncProfile(parsed, deps) {
2532
2891
  const { path, url, ifMatch, dryRun } = parsed;
2533
2892
  let raw;
2534
2893
  try {
2535
- raw = readFileSync13(path, "utf8");
2894
+ raw = readFileSync14(path, "utf8");
2536
2895
  } catch {
2537
2896
  return { code: 2, stdout: "", stderr: `takuhon: cannot read '${path}'.
2538
2897
  ` };
@@ -2545,7 +2904,7 @@ async function syncProfile(parsed, deps) {
2545
2904
  return { code: 2, stdout: "", stderr: `takuhon: '${path}' is not valid JSON: ${detail}
2546
2905
  ` };
2547
2906
  }
2548
- const result = validate6(data);
2907
+ const result = validate7(data);
2549
2908
  if (!result.ok) {
2550
2909
  const lines = result.errors.map((e) => ` ${e.pointer || "/"}: ${e.message}`);
2551
2910
  return {
@@ -2669,10 +3028,10 @@ async function readProblem(res) {
2669
3028
  }
2670
3029
 
2671
3030
  // src/validate-command.ts
2672
- import { readFileSync as readFileSync14 } from "fs";
2673
- import { validate as validate7 } from "@takuhon/core";
2674
- var DEFAULT_PATH10 = "takuhon.json";
2675
- 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]
2676
3035
 
2677
3036
  Validate a takuhon.json against the takuhon schema. With no path, validates
2678
3037
  ./takuhon.json in the current working directory.
@@ -2681,7 +3040,7 @@ Exit codes: 0 = valid, 1 = invalid, 2 = file missing / unreadable / not JSON.
2681
3040
  `;
2682
3041
  function runValidate(args = []) {
2683
3042
  if (args[0] === "--help" || args[0] === "-h") {
2684
- return { code: 0, stdout: USAGE10, stderr: "" };
3043
+ return { code: 0, stdout: USAGE11, stderr: "" };
2685
3044
  }
2686
3045
  if (args.length > 1) {
2687
3046
  return {
@@ -2693,10 +3052,10 @@ function runValidate(args = []) {
2693
3052
  return validateFile(args[0]);
2694
3053
  }
2695
3054
  function validateFile(pathArg) {
2696
- const target = pathArg ?? DEFAULT_PATH10;
3055
+ const target = pathArg ?? DEFAULT_PATH11;
2697
3056
  let raw;
2698
3057
  try {
2699
- raw = readFileSync14(target, "utf8");
3058
+ raw = readFileSync15(target, "utf8");
2700
3059
  } catch {
2701
3060
  return {
2702
3061
  code: 2,
@@ -2717,7 +3076,7 @@ function validateFile(pathArg) {
2717
3076
  `
2718
3077
  };
2719
3078
  }
2720
- const result = validate7(data);
3079
+ const result = validate8(data);
2721
3080
  if (result.ok) {
2722
3081
  return {
2723
3082
  code: 0,
@@ -2738,9 +3097,9 @@ ${lines.join("\n")}
2738
3097
  }
2739
3098
 
2740
3099
  // src/index.ts
2741
- var pkg = JSON.parse(readFileSync15(new URL("../package.json", import.meta.url), "utf8"));
2742
- var VERSION = pkg.version;
2743
- 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}
2744
3103
 
2745
3104
  Takuhon \u2014 open-source portable profile API server.
2746
3105
 
@@ -2771,6 +3130,9 @@ Commands:
2771
3130
  takuhon admin update [path] Refresh a project's admin-dist/ with the admin form UI
2772
3131
  bundled in this @takuhon/cli (use after upgrading the
2773
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.
2774
3136
  takuhon sync [path] --url <url> Push a takuhon.json to a deployment's admin API
2775
3137
  (PUT <url>/api/admin/profile). Reads the admin token
2776
3138
  from TAKUHON_ADMIN_TOKEN. --if-match <etag> opts into
@@ -2788,7 +3150,7 @@ Scaffolding a new profile project:
2788
3150
  async function main(argv) {
2789
3151
  const first = argv[0];
2790
3152
  if (first === "--version" || first === "-v") {
2791
- process.stdout.write(`${VERSION}
3153
+ process.stdout.write(`${VERSION2}
2792
3154
  `);
2793
3155
  return 0;
2794
3156
  }
@@ -2817,6 +3179,9 @@ async function main(argv) {
2817
3179
  }
2818
3180
  return runAdmin(argv.slice(1));
2819
3181
  }
3182
+ if (first === "mcp") {
3183
+ return runMcp(argv.slice(1));
3184
+ }
2820
3185
  if (first === "import") {
2821
3186
  return emit(runImport(argv.slice(1)));
2822
3187
  }