truthtrail 0.1.0 → 0.2.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 -0
  2. package/dist/index.js +289 -64
  3. package/package.json +24 -7
package/README.md CHANGED
@@ -41,6 +41,24 @@ npx truthtrail check --base main
41
41
 
42
42
  `--approver <name>` is only for when someone explicitly approved. TruthTrail never guesses approvers.
43
43
 
44
+ ## MCP (Claude Code, Cursor, Copilot)
45
+
46
+ `npx truthtrail init` also writes a `truthtrail` MCP server entry for each AI tool:
47
+
48
+ | Tool | File |
49
+ |---|---|
50
+ | Claude Code | `.mcp.json` |
51
+ | Cursor | `.cursor/mcp.json` |
52
+ | Copilot (VS Code) | `.vscode/mcp.json` |
53
+
54
+ Commit these files, and every teammate's AI gets the TruthTrail tools: `record_decision`, `list_decisions`, `why`, `check_branch`, `list_pending` and `resolve_pending`. When you ask "A or B?" and answer, the AI saves the decision itself.
55
+
56
+ - Each tool asks once to allow or enable the `truthtrail` server. In Claude Code, approve the project MCP server. In Cursor, enable it under Settings → MCP. In VS Code, start or trust it from `mcp.json` and use Copilot Chat in **Agent** mode.
57
+ - Existing entries and comments in these files are left alone. Only a missing `truthtrail` entry is added.
58
+ - Skip all of this with `npx truthtrail init --no-mcp`.
59
+ - If your company blocks MCP servers, the AI falls back to the `npx truthtrail` commands. The rules in `AGENTS.md` describe both.
60
+ - For faster startup, install locally (`npm i -D truthtrail`) and point the entry at `node_modules/.bin/truthtrail`. `init` won't overwrite your change.
61
+
44
62
  ## Claude Code plugin
45
63
 
46
64
  The plugin (in `plugin/` of the TruthTrail repo) adds:
package/dist/index.js CHANGED
@@ -4,8 +4,8 @@
4
4
  import { Command } from "commander";
5
5
 
6
6
  // src/commands/init.ts
7
- import { access, appendFile, mkdir as mkdir3, readFile as readFile5, realpath, writeFile as writeFile3 } from "fs/promises";
8
- import { join as join5 } from "path";
7
+ import { access, appendFile, mkdir as mkdir3, readFile as readFile5, realpath as realpath2, writeFile as writeFile3 } from "fs/promises";
8
+ import { dirname as dirname2, join as join5 } from "path";
9
9
 
10
10
  // ../core/src/types.ts
11
11
  var DECISION_TYPES = ["decision", "assumption", "impact"];
@@ -293,6 +293,8 @@ This repo records product decisions in \`.decisions/\`. Follow these rules.
293
293
 
294
294
  **Find the ticket.** Take the ticket key from the branch name (e.g. \`feature/JIRA-231-\u2026\`). If there is none, ask the user once.
295
295
 
296
+ **Tools.** If TruthTrail MCP tools are available (\`record_decision\`, \`list_decisions\`, \`why\`, \`check_branch\`, \`list_pending\`, \`resolve_pending\`), use them. Otherwise use the \`npx truthtrail\` commands below.
297
+
296
298
  **Before you implement,** run \`npx truthtrail list --ticket <KEY>\` and follow the decisions it shows. Treat \`rejected\` ones as must-rework.
297
299
 
298
300
  **Record a decision** with \`npx truthtrail decide\` when:
@@ -510,10 +512,109 @@ async function stopDecision(root, payload) {
510
512
  decision: "block",
511
513
  reason: `TruthTrail: ${pending.length} answered questions are not classified yet:
512
514
  ${items}
513
- For each one that changes behaviour a user or QA can see, run \`npx truthtrail decide --from-pending <n> --reason "\u2026"\`. Otherwise run \`npx truthtrail pending dismiss <n>\`.`
515
+ Record each one that changes behaviour a user or QA can see; dismiss the rest. If TruthTrail MCP tools are available, use \`resolve_pending\` (action "record" or "dismiss"); otherwise run \`npx truthtrail decide --from-pending <n> --reason "\u2026"\` or \`npx truthtrail pending dismiss <n>\`.`
514
516
  };
515
517
  }
516
518
 
519
+ // ../core/src/decide.ts
520
+ import { realpath } from "fs/promises";
521
+ var clean = (xs) => (xs ?? []).map((x) => x.trim()).filter(Boolean);
522
+ async function recordDecision(root, cwd, cfg, input, now) {
523
+ if (!input.question?.trim()) throw new Error("--question is required");
524
+ if (!input.chosen?.trim()) throw new Error("--chosen is required");
525
+ const type = input.type ?? "decision";
526
+ if (!DECISION_TYPES.includes(type)) throw new Error(`--type must be one of ${DECISION_TYPES.join(", ")}`);
527
+ let ticket = input.ticket?.trim().toUpperCase() || null;
528
+ if (ticket && !new RegExp(`^(?:${cfg.ticketPattern})$`, "i").test(ticket)) {
529
+ throw new Error(`'${input.ticket}' is not a valid ticket key (expected pattern ${cfg.ticketPattern})`);
530
+ }
531
+ if (!ticket) {
532
+ const branch = await currentBranch(root);
533
+ ticket = detectTicket({ branch }, cfg.ticketPattern);
534
+ if (!ticket) throw new Error(`No ticket key found in branch '${branch ?? "(detached)"}'. Pass --ticket <KEY>.`);
535
+ }
536
+ const base = await realpath(cwd).catch(() => cwd);
537
+ const files = clean(input.files).map((f) => {
538
+ const rel = toRepoPath(root, base, f);
539
+ if (!rel) throw new Error(`--files: ${f} is outside the repository`);
540
+ return rel;
541
+ });
542
+ return writeRecord(
543
+ root,
544
+ cfg,
545
+ {
546
+ ticket,
547
+ type,
548
+ question: input.question.trim(),
549
+ options: clean(input.options),
550
+ chosen: input.chosen.trim(),
551
+ reason: input.reason?.trim() ?? "",
552
+ decided_by: await userName(root),
553
+ approver_claimed: input.approver?.trim() || null,
554
+ agent: input.agent,
555
+ source: input.source,
556
+ files,
557
+ supersedes: clean(input.supersedes),
558
+ body: input.body?.trim() ?? ""
559
+ },
560
+ now
561
+ );
562
+ }
563
+ async function resolvePending(root, cwd, cfg, index, overrides, now) {
564
+ const entry = (await listPending(root)).find((p) => p.index === index);
565
+ if (!entry) throw new Error(`No pending question #${index}. Run \`npx truthtrail pending list\`.`);
566
+ const given = Object.fromEntries(Object.entries(overrides).filter(([, v]) => v !== void 0));
567
+ const record = await recordDecision(
568
+ root,
569
+ cwd,
570
+ cfg,
571
+ { question: entry.question, chosen: entry.answer, options: entry.options, ...given },
572
+ now
573
+ );
574
+ await removePending(root, index);
575
+ return record;
576
+ }
577
+
578
+ // ../core/src/mcpConfig.ts
579
+ import { execFile as execFile2 } from "child_process";
580
+ import { promisify as promisify2 } from "util";
581
+ import { applyEdits, findNodeAtLocation, modify, parseTree } from "jsonc-parser";
582
+ var exec2 = promisify2(execFile2);
583
+ var ARGS = ["-y", "truthtrail", "mcp"];
584
+ var MCP_TARGETS = [
585
+ { file: ".mcp.json", key: "mcpServers", entry: { command: "npx", args: ARGS } },
586
+ { file: ".cursor/mcp.json", key: "mcpServers", entry: { command: "npx", args: ARGS } },
587
+ { file: ".vscode/mcp.json", key: "servers", entry: { type: "stdio", command: "npx", args: ARGS } }
588
+ ];
589
+ var FORMAT = {};
590
+ function upsertMcpEntry(existing, target) {
591
+ if (existing === null || existing.trim() === "") {
592
+ return { status: "created", text: `${JSON.stringify({ [target.key]: { truthtrail: target.entry } }, null, 2)}
593
+ ` };
594
+ }
595
+ const errors = [];
596
+ const tree = parseTree(existing, errors, { allowTrailingComma: true, disallowComments: false });
597
+ if (errors.length || !tree) return { status: "skipped", reason: "not valid JSON" };
598
+ if (tree.type !== "object") return { status: "skipped", reason: "top level is not a JSON object" };
599
+ const parent = findNodeAtLocation(tree, [target.key]);
600
+ if (parent && parent.type !== "object") return { status: "skipped", reason: `"${target.key}" is not an object` };
601
+ if (findNodeAtLocation(tree, [target.key, "truthtrail"])) return { status: "unchanged", text: existing };
602
+ try {
603
+ const text = applyEdits(existing, modify(existing, [target.key, "truthtrail"], target.entry, FORMAT));
604
+ return { status: "added", text };
605
+ } catch (e) {
606
+ return { status: "skipped", reason: e.message };
607
+ }
608
+ }
609
+ async function isGitIgnored(root, rel) {
610
+ try {
611
+ await exec2("git", ["check-ignore", "-q", "--", rel], { cwd: root });
612
+ return true;
613
+ } catch {
614
+ return false;
615
+ }
616
+ }
617
+
517
618
  // src/commands/init.ts
518
619
  async function readOrNull(path) {
519
620
  try {
@@ -524,7 +625,7 @@ async function readOrNull(path) {
524
625
  }
525
626
  }
526
627
  var exists = (p) => access(p).then(() => true, () => false);
527
- var real = (p) => realpath(p).catch(() => p);
628
+ var real = (p) => realpath2(p).catch(() => p);
528
629
  function upsertIn(file, existing, inner) {
529
630
  try {
530
631
  return upsertBlock(existing, inner);
@@ -532,7 +633,7 @@ function upsertIn(file, existing, inner) {
532
633
  throw new Error(`${file}: ${e.message}`);
533
634
  }
534
635
  }
535
- async function runInit(cwd, log) {
636
+ async function runInit(cwd, log, opts = {}) {
536
637
  const root = await repoRoot(cwd);
537
638
  const changes = [];
538
639
  const note = (file, what) => {
@@ -546,6 +647,9 @@ async function runInit(cwd, log) {
546
647
  const claude = await readOrNull(claudePath);
547
648
  const claudeIsAgents = claude !== null && await real(claudePath) === await real(agentsPath);
548
649
  const c = claude !== null && !claudeIsAgents ? upsertIn("CLAUDE.md", claude, CLAUDE_POINTER) : null;
650
+ const mcpPlan = opts.mcp === false ? [] : await Promise.all(
651
+ MCP_TARGETS.map(async (t) => ({ target: t, result: upsertMcpEntry(await readOrNull(join5(root, t.file)), t) }))
652
+ );
549
653
  const keep = join5(root, DEFAULT_CONFIG.decisionsDir, ".gitkeep");
550
654
  if (!await exists(keep)) {
551
655
  await mkdir3(join5(root, DEFAULT_CONFIG.decisionsDir), { recursive: true });
@@ -560,6 +664,23 @@ async function runInit(cwd, log) {
560
664
  await writeFile3(claudePath, c.text);
561
665
  note("CLAUDE.md", "updated");
562
666
  }
667
+ for (const { target, result } of mcpPlan) {
668
+ if (result.status === "skipped") {
669
+ log(`skipped ${target.file} (${result.reason})`);
670
+ continue;
671
+ }
672
+ if (result.status === "unchanged") continue;
673
+ const path = join5(root, target.file);
674
+ await mkdir3(dirname2(path), { recursive: true });
675
+ await writeFile3(path, result.text);
676
+ note(target.file, result.status === "created" ? "added" : "updated");
677
+ if (await isGitIgnored(root, target.file)) {
678
+ const dir = target.file.split("/")[0];
679
+ log(
680
+ `note: ${target.file} is git-ignored, so teammates won't get it (to share it: git add -f ${target.file}, or change "${dir}/" to "${dir}/*" and add !${target.file})`
681
+ );
682
+ }
683
+ }
563
684
  const configPath = join5(root, CONFIG_FILE);
564
685
  if (!await exists(configPath)) {
565
686
  await writeFile3(configPath, `${JSON.stringify(DEFAULT_CONFIG, null, 2)}
@@ -579,16 +700,7 @@ async function runInit(cwd, log) {
579
700
  }
580
701
 
581
702
  // src/commands/decide.ts
582
- import { realpath as realpath2 } from "fs/promises";
583
- var list = (s, sep) => (s ?? "").split(sep).map((x) => x.trim()).filter(Boolean);
584
- async function repoFiles(root, cwd, files) {
585
- const base = await realpath2(cwd);
586
- return files.map((f) => {
587
- const rel = toRepoPath(root, base, f);
588
- if (!rel) throw new Error(`--files: ${f} is outside the repository`);
589
- return rel;
590
- });
591
- }
703
+ var split = (s, sep) => s === void 0 ? void 0 : s.split(sep);
592
704
  function detectAgent(o, env) {
593
705
  if (o.agent) {
594
706
  if (!AGENT_NAMES.includes(o.agent)) throw new Error(`--agent must be one of ${AGENT_NAMES.join(", ")}`);
@@ -596,55 +708,26 @@ function detectAgent(o, env) {
596
708
  }
597
709
  return env.CLAUDECODE === "1" ? "claude-code" : "none";
598
710
  }
599
- async function runDecide(cwd, input, env, now) {
600
- let o = input;
601
- if (input.fromPending !== void 0) {
602
- const entry = (await listPending(await repoRoot(cwd))).find((p) => p.index === Number(input.fromPending));
603
- if (!entry) throw new Error(`No pending question #${input.fromPending}. Run \`npx truthtrail pending list\`.`);
604
- const given = Object.fromEntries(Object.entries(input).filter(([, v]) => v !== void 0));
605
- o = { question: entry.question, chosen: entry.answer, options: entry.options.join("|"), ...given };
606
- }
607
- const record = await decide(cwd, o, env, now);
608
- if (input.fromPending !== void 0) await removePending(await repoRoot(cwd), Number(input.fromPending));
609
- return record;
610
- }
611
- async function decide(cwd, o, env, now) {
612
- if (!o.question?.trim()) throw new Error("--question is required");
613
- if (!o.chosen?.trim()) throw new Error("--chosen is required");
614
- const type = o.type ?? "decision";
615
- if (!DECISION_TYPES.includes(type)) throw new Error(`--type must be one of ${DECISION_TYPES.join(", ")}`);
711
+ async function runDecide(cwd, o, env, now) {
616
712
  const root = await repoRoot(cwd);
617
713
  const cfg = await loadConfig(root);
618
- let ticket = o.ticket?.trim().toUpperCase() || null;
619
- if (ticket && !new RegExp(`^(?:${cfg.ticketPattern})$`, "i").test(ticket)) {
620
- throw new Error(`'${o.ticket}' is not a valid ticket key (expected pattern ${cfg.ticketPattern})`);
621
- }
622
- if (!ticket) {
623
- const branch = await currentBranch(root);
624
- ticket = detectTicket({ branch }, cfg.ticketPattern);
625
- if (!ticket) throw new Error(`No ticket key found in branch '${branch ?? "(detached)"}'. Pass --ticket <KEY>.`);
626
- }
627
714
  const agent = detectAgent(o, env);
628
- return writeRecord(
629
- root,
630
- cfg,
631
- {
632
- ticket,
633
- type,
634
- question: o.question.trim(),
635
- options: list(o.options, "|"),
636
- chosen: o.chosen.trim(),
637
- reason: o.reason?.trim() ?? "",
638
- decided_by: await userName(root),
639
- approver_claimed: o.approver?.trim() || null,
640
- agent,
641
- source: agent === "none" ? "manual" : "ai-session",
642
- files: await repoFiles(root, cwd, list(o.files, ",")),
643
- supersedes: list(o.supersedes, ","),
644
- body: o.body?.trim() ?? ""
645
- },
646
- now
647
- );
715
+ const input = {
716
+ question: o.question,
717
+ chosen: o.chosen,
718
+ options: split(o.options, "|"),
719
+ reason: o.reason,
720
+ type: o.type,
721
+ ticket: o.ticket,
722
+ files: split(o.files, ","),
723
+ approver: o.approver,
724
+ supersedes: split(o.supersedes, ","),
725
+ body: o.body,
726
+ agent,
727
+ source: agent === "none" ? "manual" : "ai-session"
728
+ };
729
+ if (o.fromPending !== void 0) return resolvePending(root, cwd, cfg, Number(o.fromPending), input, now);
730
+ return recordDecision(root, cwd, cfg, input, now);
648
731
  }
649
732
 
650
733
  // src/commands/list.ts
@@ -764,12 +847,151 @@ async function readStdin() {
764
847
  return Buffer.concat(chunks).toString("utf8");
765
848
  }
766
849
 
850
+ // src/mcp/server.ts
851
+ import { fileURLToPath } from "url";
852
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
853
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
854
+ import { z } from "zod";
855
+
856
+ // src/mcp/agent.ts
857
+ function agentFromClient(name) {
858
+ const n = (name ?? "").toLowerCase();
859
+ if (n.includes("claude")) return "claude-code";
860
+ if (n.includes("cursor")) return "cursor";
861
+ if (n.includes("codex")) return "codex";
862
+ if (/visual studio code|vscode|copilot/.test(n)) return "copilot";
863
+ return "other";
864
+ }
865
+
866
+ // src/mcp/server.ts
867
+ var NO_REPO = "TruthTrail: not inside a git repository (open the project folder)";
868
+ var ok = (value) => ({ content: [{ type: "text", text: JSON.stringify(value, null, 2) }] });
869
+ var fail = (e) => ({
870
+ isError: true,
871
+ content: [{ type: "text", text: (e instanceof Error ? e.message : String(e)).split("\n")[0] }]
872
+ });
873
+ var decisionFields = {
874
+ question: z.string().describe("What needed deciding, e.g. 'Discount before or after tax?'"),
875
+ chosen: z.string().describe("What was picked"),
876
+ options: z.array(z.string()).optional().describe("The choices that were considered"),
877
+ reason: z.string().optional().describe("Why it was chosen"),
878
+ type: z.enum(DECISION_TYPES).optional().describe("decision (default), assumption (you assumed it without asking), or impact"),
879
+ ticket: z.string().optional().describe("Ticket key; defaults to the key in the branch name"),
880
+ files: z.array(z.string()).optional().describe("Files this decision affects"),
881
+ approver: z.string().optional().describe("Only if the user explicitly named who approved it. Never guess."),
882
+ supersedes: z.array(z.string()).optional().describe("Ids of older decisions this replaces"),
883
+ body: z.string().optional().describe("Short extra explanation")
884
+ };
885
+ function createServer(opts) {
886
+ const now = opts.now ?? (() => /* @__PURE__ */ new Date());
887
+ const server = new McpServer({ name: "truthtrail", version: "0.2.0" });
888
+ async function workspace() {
889
+ const candidates = [];
890
+ if (server.server.getClientCapabilities()?.roots) {
891
+ try {
892
+ const { roots } = await server.server.listRoots();
893
+ for (const r of roots) if (r.uri.startsWith("file://")) candidates.push(fileURLToPath(r.uri));
894
+ } catch {
895
+ }
896
+ }
897
+ candidates.push(opts.cwd);
898
+ for (const dir of candidates) {
899
+ try {
900
+ const root = await repoRoot(dir);
901
+ return { root, cwd: dir, cfg: await loadConfig(root) };
902
+ } catch {
903
+ }
904
+ }
905
+ throw new Error(NO_REPO);
906
+ }
907
+ const who = () => ({ agent: agentFromClient(server.server.getClientVersion()?.name), source: "ai-session" });
908
+ const run = (fn) => fn().then(ok, fail);
909
+ server.registerTool(
910
+ "record_decision",
911
+ {
912
+ description: "Record a product decision or assumption for the current ticket. Use it when the ticket was silent or ambiguous and the choice changes behaviour a user or QA can see, when following the ticket would break something, or when you assumed something visible (type: assumption). Not for pure implementation details.",
913
+ inputSchema: decisionFields
914
+ },
915
+ (args) => run(async () => {
916
+ const w = await workspace();
917
+ const r = await recordDecision(w.root, w.cwd, w.cfg, { ...args, ...who() }, now());
918
+ return { id: r.id, path: r.path, ticket: r.ticket };
919
+ })
920
+ );
921
+ server.registerTool(
922
+ "list_decisions",
923
+ {
924
+ description: "List recorded decisions (optionally for one ticket). Read these before implementing or testing a ticket and follow them.",
925
+ inputSchema: { ticket: z.string().optional() }
926
+ },
927
+ ({ ticket }) => run(async () => {
928
+ const w = await workspace();
929
+ return listRecords(w.root, w.cfg, ticket?.trim().toUpperCase() || void 0);
930
+ })
931
+ );
932
+ server.registerTool(
933
+ "why",
934
+ {
935
+ description: "Explain a file or line: last commit, AI or human authorship, linked decisions and the ticket.",
936
+ inputSchema: { target: z.string().describe("file or file:line, relative to the workspace") }
937
+ },
938
+ ({ target }) => run(async () => {
939
+ const w = await workspace();
940
+ return whyLocal(w.root, w.cfg, parseTarget(target), w.cwd);
941
+ })
942
+ );
943
+ server.registerTool(
944
+ "check_branch",
945
+ {
946
+ description: "Check this branch: ticket key, decisions and assumptions recorded, AI-assisted commits, and reminders.",
947
+ inputSchema: { base: z.string().optional().describe("branch to compare against (default main)") }
948
+ },
949
+ ({ base }) => run(async () => {
950
+ const w = await workspace();
951
+ return checkRepo(w.root, w.cfg, base ?? "main");
952
+ })
953
+ );
954
+ server.registerTool(
955
+ "list_pending",
956
+ { description: "Questions the user answered during this work that are not yet recorded or dismissed." },
957
+ () => run(async () => {
958
+ const w = await workspace();
959
+ return listPending(w.root);
960
+ })
961
+ );
962
+ server.registerTool(
963
+ "resolve_pending",
964
+ {
965
+ description: "Resolve a pending answered question: action 'record' if it changes behaviour a user or QA can see (fields override the captured values), otherwise 'dismiss'.",
966
+ inputSchema: {
967
+ index: z.number().int(),
968
+ action: z.enum(["record", "dismiss"]),
969
+ ...Object.fromEntries(Object.entries(decisionFields).map(([k, v]) => [k, v.optional()]))
970
+ }
971
+ },
972
+ (args) => run(async () => {
973
+ const w = await workspace();
974
+ const { index, action, ...fields } = args;
975
+ if (action === "dismiss") {
976
+ const hit = await removePending(w.root, index);
977
+ if (!hit) throw new Error(`No pending question #${index}.`);
978
+ return { dismissed: hit };
979
+ }
980
+ return resolvePending(w.root, w.cwd, w.cfg, index, { ...fields, ...who() }, now());
981
+ })
982
+ );
983
+ return server;
984
+ }
985
+ async function runMcp() {
986
+ await createServer({ cwd: process.cwd() }).connect(new StdioServerTransport());
987
+ }
988
+
767
989
  // src/index.ts
768
990
  function buildProgram() {
769
991
  const program = new Command();
770
- program.name("truthtrail").description("Record the decisions your AI coding agent makes, right next to the code.").version("0.1.0");
771
- program.command("init").description("Set up TruthTrail in this repo (.decisions/, AGENTS.md rules, config)").action(async () => {
772
- await runInit(process.cwd(), (l) => console.log(l));
992
+ program.name("truthtrail").description("Record the decisions your AI coding agent makes, right next to the code.").version("0.2.0");
993
+ program.command("init").description("Set up TruthTrail in this repo (.decisions/, AGENTS.md rules, MCP config, config)").option("--no-mcp", "don't write MCP config for Claude Code, Cursor and Copilot").action(async (o) => {
994
+ await runInit(process.cwd(), (l) => console.log(l), { mcp: o.mcp });
773
995
  });
774
996
  program.command("decide").description("Record a decision (or assumption) for the current ticket").option("--question <text>", "what needed deciding").option("--chosen <text>", "what was picked").option("--options <list>", "choices considered, separated by |").option("--reason <text>", "why").option("--type <type>", "decision | assumption | impact", "decision").option("--ticket <key>", "ticket key (default: from branch name)").option("--files <list>", "affected files, comma separated").option("--approver <name>", "who the user said approved it (never guess)").option("--supersedes <ids>", "ids of older decisions this replaces, comma separated").option("--agent <name>", "AI agent involved").option("--body <text>", "short extra explanation").option("--from-pending <n>", "record a question captured by the Claude Code hook", (v) => Number(v)).action(async (o) => {
775
997
  const r = await runDecide(process.cwd(), o, process.env, /* @__PURE__ */ new Date());
@@ -787,6 +1009,9 @@ function buildProgram() {
787
1009
  console.log(output);
788
1010
  process.exitCode = exitCode;
789
1011
  });
1012
+ program.command("mcp").description("Run the TruthTrail MCP server over stdio (used by Claude Code, Cursor, Copilot)").action(async () => {
1013
+ await runMcp();
1014
+ });
790
1015
  const pending = program.command("pending").description("Questions captured by the Claude Code hook, not yet classified");
791
1016
  pending.command("list").description("List pending questions").action(async () => console.log(await runPendingList(process.cwd())));
792
1017
  pending.command("dismiss").description("Drop a pending question that does not change visible behaviour").argument("<n>", "pending index", (v) => Number(v)).action(async (n) => console.log(await runPendingDismiss(process.cwd(), n)));
package/package.json CHANGED
@@ -1,13 +1,30 @@
1
1
  {
2
2
  "name": "truthtrail",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Record the decisions your AI coding agent makes, right next to the code.",
5
5
  "type": "module",
6
- "bin": { "truthtrail": "dist/index.js" },
7
- "files": ["dist", "README.md"],
8
- "engines": { "node": ">=20" },
9
- "scripts": { "build": "tsup" },
10
- "dependencies": { "commander": "^12.1.0", "yaml": "^2.5.0" },
11
- "devDependencies": { "@truthtrail/core": "*" },
6
+ "bin": {
7
+ "truthtrail": "dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "README.md"
12
+ ],
13
+ "engines": {
14
+ "node": ">=20"
15
+ },
16
+ "scripts": {
17
+ "build": "tsup"
18
+ },
19
+ "dependencies": {
20
+ "@modelcontextprotocol/sdk": "^1.32.2",
21
+ "commander": "^12.1.0",
22
+ "jsonc-parser": "^3.3.1",
23
+ "yaml": "^2.5.0",
24
+ "zod": "^4.6.7"
25
+ },
26
+ "devDependencies": {
27
+ "@truthtrail/core": "*"
28
+ },
12
29
  "license": "MIT"
13
30
  }