@anthropologies/claudestory 0.1.10 → 0.1.11

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/mcp.js CHANGED
@@ -2,7 +2,8 @@
2
2
 
3
3
  // src/mcp/index.ts
4
4
  import { realpathSync, existsSync as existsSync6 } from "fs";
5
- import { resolve as resolve7, join as join8, isAbsolute } from "path";
5
+ import { resolve as resolve8, join as join9, isAbsolute } from "path";
6
+ import { z as z9 } from "zod";
6
7
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
7
8
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
8
9
 
@@ -155,15 +156,9 @@ var IssueSchema = z3.object({
155
156
  import { z as z4 } from "zod";
156
157
  var NoteSchema = z4.object({
157
158
  id: NoteIdSchema,
158
- title: z4.preprocess((v) => v ?? null, z4.string().nullable()),
159
+ title: z4.string().nullable(),
159
160
  content: z4.string().refine((v) => v.trim().length > 0, "Content cannot be empty"),
160
- tags: z4.preprocess(
161
- (v) => {
162
- const raw = Array.isArray(v) ? v : [];
163
- return raw.filter((t) => typeof t === "string").map((t) => t.trim().toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "").replace(/-+/g, "-").replace(/^-|-$/g, "")).filter((t) => t.length > 0).filter((t, i, a) => a.indexOf(t) === i);
164
- },
165
- z4.array(z4.string())
166
- ),
161
+ tags: z4.array(z4.string()),
167
162
  status: z4.enum(NOTE_STATUSES),
168
163
  createdDate: DateSchema,
169
164
  updatedDate: DateSchema
@@ -589,6 +584,12 @@ async function writeTicketUnlocked(ticket, root) {
589
584
  const json = serializeJSON(parsed);
590
585
  await atomicWrite(targetPath, json);
591
586
  }
587
+ async function writeTicket(ticket, root) {
588
+ const wrapDir = resolve2(root, ".story");
589
+ await withLock(wrapDir, async () => {
590
+ await writeTicketUnlocked(ticket, root);
591
+ });
592
+ }
592
593
  async function writeIssueUnlocked(issue, root) {
593
594
  const parsed = IssueSchema.parse(issue);
594
595
  if (!ISSUE_ID_REGEX.test(parsed.id)) {
@@ -603,6 +604,104 @@ async function writeIssueUnlocked(issue, root) {
603
604
  const json = serializeJSON(parsed);
604
605
  await atomicWrite(targetPath, json);
605
606
  }
607
+ async function writeIssue(issue, root) {
608
+ const wrapDir = resolve2(root, ".story");
609
+ await withLock(wrapDir, async () => {
610
+ await writeIssueUnlocked(issue, root);
611
+ });
612
+ }
613
+ async function writeRoadmapUnlocked(roadmap, root) {
614
+ const parsed = RoadmapSchema.parse(roadmap);
615
+ const wrapDir = resolve2(root, ".story");
616
+ const targetPath = join3(wrapDir, "roadmap.json");
617
+ await guardPath(targetPath, wrapDir);
618
+ const json = serializeJSON(parsed);
619
+ await atomicWrite(targetPath, json);
620
+ }
621
+ async function writeRoadmap(roadmap, root) {
622
+ const wrapDir = resolve2(root, ".story");
623
+ await withLock(wrapDir, async () => {
624
+ await writeRoadmapUnlocked(roadmap, root);
625
+ });
626
+ }
627
+ async function writeConfig(config, root) {
628
+ const parsed = ConfigSchema.parse(config);
629
+ const wrapDir = resolve2(root, ".story");
630
+ const targetPath = join3(wrapDir, "config.json");
631
+ await guardPath(targetPath, wrapDir);
632
+ const json = serializeJSON(parsed);
633
+ await withLock(wrapDir, async () => {
634
+ await atomicWrite(targetPath, json);
635
+ });
636
+ }
637
+ async function deleteTicket(id, root, options) {
638
+ if (!TICKET_ID_REGEX.test(id)) {
639
+ throw new ProjectLoaderError(
640
+ "invalid_input",
641
+ `Invalid ticket ID: ${id}`
642
+ );
643
+ }
644
+ const wrapDir = resolve2(root, ".story");
645
+ const targetPath = join3(wrapDir, "tickets", `${id}.json`);
646
+ await guardPath(targetPath, wrapDir);
647
+ await withLock(wrapDir, async () => {
648
+ if (!options?.force) {
649
+ const { state } = await loadProjectUnlocked(resolve2(root));
650
+ const blocking = state.ticketsBlocking(id);
651
+ if (blocking.length > 0) {
652
+ throw new ProjectLoaderError(
653
+ "conflict",
654
+ `Cannot delete ${id}: referenced in blockedBy by ${blocking.join(", ")}`
655
+ );
656
+ }
657
+ const children = state.childrenOf(id);
658
+ if (children.length > 0) {
659
+ throw new ProjectLoaderError(
660
+ "conflict",
661
+ `Cannot delete ${id}: has child tickets ${children.join(", ")}`
662
+ );
663
+ }
664
+ const refs = state.issuesReferencing(id);
665
+ if (refs.length > 0) {
666
+ throw new ProjectLoaderError(
667
+ "conflict",
668
+ `Cannot delete ${id}: referenced by issues ${refs.join(", ")}`
669
+ );
670
+ }
671
+ }
672
+ try {
673
+ await stat(targetPath);
674
+ } catch {
675
+ throw new ProjectLoaderError(
676
+ "not_found",
677
+ `Ticket file not found: tickets/${id}.json`
678
+ );
679
+ }
680
+ await unlink(targetPath);
681
+ });
682
+ }
683
+ async function deleteIssue(id, root) {
684
+ if (!ISSUE_ID_REGEX.test(id)) {
685
+ throw new ProjectLoaderError(
686
+ "invalid_input",
687
+ `Invalid issue ID: ${id}`
688
+ );
689
+ }
690
+ const wrapDir = resolve2(root, ".story");
691
+ const targetPath = join3(wrapDir, "issues", `${id}.json`);
692
+ await guardPath(targetPath, wrapDir);
693
+ await withLock(wrapDir, async () => {
694
+ try {
695
+ await stat(targetPath);
696
+ } catch {
697
+ throw new ProjectLoaderError(
698
+ "not_found",
699
+ `Issue file not found: issues/${id}.json`
700
+ );
701
+ }
702
+ await unlink(targetPath);
703
+ });
704
+ }
606
705
  async function writeNoteUnlocked(note, root) {
607
706
  const parsed = NoteSchema.parse(note);
608
707
  if (!NOTE_ID_REGEX.test(parsed.id)) {
@@ -618,6 +717,34 @@ async function writeNoteUnlocked(note, root) {
618
717
  const json = serializeJSON(parsed);
619
718
  await atomicWrite(targetPath, json);
620
719
  }
720
+ async function writeNote(note, root) {
721
+ const wrapDir = resolve2(root, ".story");
722
+ await withLock(wrapDir, async () => {
723
+ await writeNoteUnlocked(note, root);
724
+ });
725
+ }
726
+ async function deleteNote(id, root) {
727
+ if (!NOTE_ID_REGEX.test(id)) {
728
+ throw new ProjectLoaderError(
729
+ "invalid_input",
730
+ `Invalid note ID: ${id}`
731
+ );
732
+ }
733
+ const wrapDir = resolve2(root, ".story");
734
+ const targetPath = join3(wrapDir, "notes", `${id}.json`);
735
+ await guardPath(targetPath, wrapDir);
736
+ await withLock(wrapDir, async () => {
737
+ try {
738
+ await stat(targetPath);
739
+ } catch {
740
+ throw new ProjectLoaderError(
741
+ "not_found",
742
+ `Note file not found: notes/${id}.json`
743
+ );
744
+ }
745
+ await unlink(targetPath);
746
+ });
747
+ }
621
748
  async function withProjectLock(root, options, handler) {
622
749
  const absRoot = resolve2(root);
623
750
  const wrapDir = join3(absRoot, ".story");
@@ -1586,6 +1713,16 @@ function formatRecap(recap, state, format) {
1586
1713
  lines.push(`- ${escapeMarkdownInline(name)} \u2014 **new**`);
1587
1714
  }
1588
1715
  }
1716
+ if (changes.handovers && (changes.handovers.added.length > 0 || changes.handovers.removed.length > 0)) {
1717
+ lines.push("");
1718
+ lines.push("## Handovers");
1719
+ for (const h of changes.handovers.added) {
1720
+ lines.push(`- ${h} \u2014 **new**`);
1721
+ }
1722
+ for (const h of changes.handovers.removed) {
1723
+ lines.push(`- ${h} \u2014 removed`);
1724
+ }
1725
+ }
1589
1726
  if (changes.notes && (changes.notes.added.length > 0 || changes.notes.removed.length > 0 || changes.notes.updated.length > 0)) {
1590
1727
  lines.push("");
1591
1728
  lines.push("## Notes");
@@ -1807,7 +1944,35 @@ function formatFullExport(state, format) {
1807
1944
  return lines.join("\n");
1808
1945
  }
1809
1946
  function hasAnyChanges(diff) {
1810
- return diff.tickets.added.length > 0 || diff.tickets.removed.length > 0 || diff.tickets.statusChanged.length > 0 || diff.tickets.descriptionChanged.length > 0 || diff.issues.added.length > 0 || diff.issues.resolved.length > 0 || diff.issues.statusChanged.length > 0 || diff.issues.impactChanged.length > 0 || diff.blockers.added.length > 0 || diff.blockers.cleared.length > 0 || diff.phases.added.length > 0 || diff.phases.removed.length > 0 || diff.phases.statusChanged.length > 0 || (diff.notes?.added.length ?? 0) > 0 || (diff.notes?.removed.length ?? 0) > 0 || (diff.notes?.updated.length ?? 0) > 0;
1947
+ return diff.tickets.added.length > 0 || diff.tickets.removed.length > 0 || diff.tickets.statusChanged.length > 0 || diff.tickets.descriptionChanged.length > 0 || diff.issues.added.length > 0 || diff.issues.resolved.length > 0 || diff.issues.statusChanged.length > 0 || diff.issues.impactChanged.length > 0 || diff.blockers.added.length > 0 || diff.blockers.cleared.length > 0 || diff.phases.added.length > 0 || diff.phases.removed.length > 0 || diff.phases.statusChanged.length > 0 || (diff.notes?.added.length ?? 0) > 0 || (diff.notes?.removed.length ?? 0) > 0 || (diff.notes?.updated.length ?? 0) > 0 || (diff.handovers?.added.length ?? 0) > 0 || (diff.handovers?.removed.length ?? 0) > 0;
1948
+ }
1949
+ function formatSelftestResult(result, format) {
1950
+ if (format === "json") {
1951
+ return JSON.stringify(successEnvelope(result), null, 2);
1952
+ }
1953
+ const lines = ["# Self-test Report", ""];
1954
+ const entities = ["ticket", "issue", "note"];
1955
+ for (const entity of entities) {
1956
+ const checks = result.results.filter((r) => r.entity === entity);
1957
+ if (checks.length === 0) continue;
1958
+ lines.push(`## ${entity.charAt(0).toUpperCase() + entity.slice(1)}`);
1959
+ for (const check of checks) {
1960
+ const mark = check.passed ? "[x]" : "[ ]";
1961
+ const suffix = check.passed ? "" : ` \u2014 ${check.detail}`;
1962
+ lines.push(`- ${mark} ${check.step}${suffix}`);
1963
+ }
1964
+ lines.push("");
1965
+ }
1966
+ if (result.cleanupErrors.length > 0) {
1967
+ lines.push("## Cleanup Warnings");
1968
+ lines.push("");
1969
+ for (const err of result.cleanupErrors) {
1970
+ lines.push(`- ${err}`);
1971
+ }
1972
+ lines.push("");
1973
+ }
1974
+ lines.push(`Result: ${result.passed}/${result.total} passed`);
1975
+ return lines.join("\n");
1811
1976
  }
1812
1977
  function truncate(text, maxLen) {
1813
1978
  if (text.length <= maxLen) return text;
@@ -2446,6 +2611,12 @@ async function handleTicketUpdate(id, updates, format, root) {
2446
2611
  `Unknown ticket status "${updates.status}": must be one of ${TICKET_STATUSES.join(", ")}`
2447
2612
  );
2448
2613
  }
2614
+ if (updates.type !== void 0 && !TICKET_TYPES.includes(updates.type)) {
2615
+ throw new CliValidationError(
2616
+ "invalid_input",
2617
+ `Unknown ticket type "${updates.type}": must be one of ${TICKET_TYPES.join(", ")}`
2618
+ );
2619
+ }
2449
2620
  let updatedTicket;
2450
2621
  await withProjectLock(root, { strict: true }, async ({ state }) => {
2451
2622
  const existing = state.ticketByID(id);
@@ -2461,21 +2632,26 @@ async function handleTicketUpdate(id, updates, format, root) {
2461
2632
  if (updates.parentTicket) {
2462
2633
  validateParentTicket(updates.parentTicket, id, state);
2463
2634
  }
2464
- const ticket = { ...existing };
2465
- if (updates.title !== void 0) ticket.title = updates.title;
2466
- if (updates.description !== void 0) ticket.description = updates.description;
2467
- if (updates.phase !== void 0) ticket.phase = updates.phase;
2468
- if (updates.order !== void 0) ticket.order = updates.order;
2469
- if (updates.blockedBy !== void 0) ticket.blockedBy = updates.blockedBy;
2470
- if (updates.parentTicket !== void 0) ticket.parentTicket = updates.parentTicket === null ? void 0 : updates.parentTicket;
2635
+ const statusChanges = {};
2471
2636
  if (updates.status !== void 0 && updates.status !== existing.status) {
2472
- ticket.status = updates.status;
2637
+ statusChanges.status = updates.status;
2473
2638
  if (updates.status === "complete" && existing.status !== "complete") {
2474
- ticket.completedDate = todayISO();
2639
+ statusChanges.completedDate = todayISO();
2475
2640
  } else if (updates.status !== "complete" && existing.status === "complete") {
2476
- ticket.completedDate = null;
2641
+ statusChanges.completedDate = null;
2477
2642
  }
2478
2643
  }
2644
+ const ticket = {
2645
+ ...existing,
2646
+ ...updates.title !== void 0 && { title: updates.title },
2647
+ ...updates.type !== void 0 && { type: updates.type },
2648
+ ...updates.description !== void 0 && { description: updates.description },
2649
+ ...updates.phase !== void 0 && { phase: updates.phase },
2650
+ ...updates.order !== void 0 && { order: updates.order },
2651
+ ...updates.blockedBy !== void 0 && { blockedBy: updates.blockedBy },
2652
+ ...updates.parentTicket !== void 0 && { parentTicket: updates.parentTicket },
2653
+ ...statusChanges
2654
+ };
2479
2655
  validatePostWriteState(ticket, state, false);
2480
2656
  await writeTicketUnlocked(ticket, root);
2481
2657
  updatedTicket = ticket;
@@ -2564,6 +2740,9 @@ async function handleIssueCreate(args, format, root) {
2564
2740
  }
2565
2741
  let createdIssue;
2566
2742
  await withProjectLock(root, { strict: true }, async ({ state }) => {
2743
+ if (args.phase && !state.roadmap.phases.some((p) => p.id === args.phase)) {
2744
+ throw new CliValidationError("invalid_input", `Phase "${args.phase}" not found in roadmap`);
2745
+ }
2567
2746
  if (args.relatedTickets.length > 0) {
2568
2747
  validateRelatedTickets(args.relatedTickets, state);
2569
2748
  }
@@ -2611,25 +2790,36 @@ async function handleIssueUpdate(id, updates, format, root) {
2611
2790
  if (!existing) {
2612
2791
  throw new CliValidationError("not_found", `Issue ${id} not found`);
2613
2792
  }
2793
+ if (updates.phase !== void 0 && updates.phase !== null) {
2794
+ if (!state.roadmap.phases.some((p) => p.id === updates.phase)) {
2795
+ throw new CliValidationError("invalid_input", `Phase "${updates.phase}" not found in roadmap`);
2796
+ }
2797
+ }
2614
2798
  if (updates.relatedTickets) {
2615
2799
  validateRelatedTickets(updates.relatedTickets, state);
2616
2800
  }
2617
- const issue = { ...existing };
2618
- if (updates.title !== void 0) issue.title = updates.title;
2619
- if (updates.severity !== void 0) issue.severity = updates.severity;
2620
- if (updates.impact !== void 0) issue.impact = updates.impact;
2621
- if (updates.resolution !== void 0) issue.resolution = updates.resolution;
2622
- if (updates.components !== void 0) issue.components = updates.components;
2623
- if (updates.relatedTickets !== void 0) issue.relatedTickets = updates.relatedTickets;
2624
- if (updates.location !== void 0) issue.location = updates.location;
2801
+ const statusChanges = {};
2625
2802
  if (updates.status !== void 0 && updates.status !== existing.status) {
2626
- issue.status = updates.status;
2803
+ statusChanges.status = updates.status;
2627
2804
  if (updates.status === "resolved" && existing.status !== "resolved") {
2628
- issue.resolvedDate = todayISO();
2805
+ statusChanges.resolvedDate = todayISO();
2629
2806
  } else if (updates.status !== "resolved" && existing.status === "resolved") {
2630
- issue.resolvedDate = null;
2807
+ statusChanges.resolvedDate = null;
2631
2808
  }
2632
2809
  }
2810
+ const issue = {
2811
+ ...existing,
2812
+ ...updates.title !== void 0 && { title: updates.title },
2813
+ ...updates.severity !== void 0 && { severity: updates.severity },
2814
+ ...updates.impact !== void 0 && { impact: updates.impact },
2815
+ ...updates.resolution !== void 0 && { resolution: updates.resolution },
2816
+ ...updates.components !== void 0 && { components: updates.components },
2817
+ ...updates.relatedTickets !== void 0 && { relatedTickets: updates.relatedTickets },
2818
+ ...updates.location !== void 0 && { location: updates.location },
2819
+ ...updates.order !== void 0 && { order: updates.order },
2820
+ ...updates.phase !== void 0 && { phase: updates.phase },
2821
+ ...statusChanges
2822
+ };
2633
2823
  validatePostWriteIssueState(issue, state, false);
2634
2824
  await writeIssueUnlocked(issue, root);
2635
2825
  updatedIssue = issue;
@@ -2660,6 +2850,7 @@ var SnapshotV1Schema = z7.object({
2660
2850
  tickets: z7.array(TicketSchema),
2661
2851
  issues: z7.array(IssueSchema),
2662
2852
  notes: z7.array(NoteSchema).optional().default([]),
2853
+ handoverFilenames: z7.array(z7.string()).optional().default([]),
2663
2854
  warnings: z7.array(LoadWarningSchema).optional()
2664
2855
  });
2665
2856
  var MAX_SNAPSHOTS = 20;
@@ -2679,6 +2870,7 @@ async function saveSnapshot(root, loadResult) {
2679
2870
  tickets: [...state.tickets],
2680
2871
  issues: [...state.issues],
2681
2872
  notes: [...state.notes],
2873
+ handoverFilenames: [...state.handoverFilenames],
2682
2874
  ...warnings.length > 0 ? {
2683
2875
  warnings: warnings.map((w) => ({
2684
2876
  type: w.type,
@@ -2831,12 +3023,23 @@ function diffStates(snapshotState, currentState) {
2831
3023
  notesRemoved.push({ id, title: snap.title });
2832
3024
  }
2833
3025
  }
3026
+ const snapHandovers = new Set(snapshotState.handoverFilenames);
3027
+ const curHandovers = new Set(currentState.handoverFilenames);
3028
+ const handoversAdded = [];
3029
+ const handoversRemoved = [];
3030
+ for (const h of curHandovers) {
3031
+ if (!snapHandovers.has(h)) handoversAdded.push(h);
3032
+ }
3033
+ for (const h of snapHandovers) {
3034
+ if (!curHandovers.has(h)) handoversRemoved.push(h);
3035
+ }
2834
3036
  return {
2835
3037
  tickets: { added: ticketsAdded, removed: ticketsRemoved, statusChanged: ticketsStatusChanged, descriptionChanged: ticketsDescriptionChanged },
2836
3038
  issues: { added: issuesAdded, resolved: issuesResolved, statusChanged: issuesStatusChanged, impactChanged: issuesImpactChanged },
2837
3039
  blockers: { added: blockersAdded, cleared: blockersCleared },
2838
3040
  phases: { added: phasesAdded, removed: phasesRemoved, statusChanged: phasesStatusChanged },
2839
- notes: { added: notesAdded, removed: notesRemoved, updated: notesUpdated }
3041
+ notes: { added: notesAdded, removed: notesRemoved, updated: notesUpdated },
3042
+ handovers: { added: handoversAdded, removed: handoversRemoved }
2840
3043
  };
2841
3044
  }
2842
3045
  function buildRecap(currentState, snapshotInfo) {
@@ -2864,7 +3067,7 @@ function buildRecap(currentState, snapshotInfo) {
2864
3067
  notes: snapshot.notes ?? [],
2865
3068
  roadmap: snapshot.roadmap,
2866
3069
  config: snapshot.config,
2867
- handoverFilenames: []
3070
+ handoverFilenames: snapshot.handoverFilenames ?? []
2868
3071
  });
2869
3072
  const changes = diffStates(snapshotState, currentState);
2870
3073
  const recentlyClearedBlockers = changes.blockers.cleared;
@@ -2996,23 +3199,22 @@ async function handleNoteUpdate(id, updates, format, root) {
2996
3199
  if (!existing) {
2997
3200
  throw new CliValidationError("not_found", `Note ${id} not found`);
2998
3201
  }
2999
- const note = { ...existing };
3000
- if (updates.content !== void 0) {
3001
- note.content = updates.content;
3002
- }
3003
- if (updates.title !== void 0) {
3004
- const trimmed = updates.title?.trim();
3005
- note.title = !trimmed ? null : updates.title;
3006
- }
3202
+ const tagsUpdate = {};
3007
3203
  if (updates.clearTags) {
3008
- note.tags = [];
3204
+ tagsUpdate.tags = [];
3009
3205
  } else if (updates.tags !== void 0) {
3010
- note.tags = normalizeTags(updates.tags);
3011
- }
3012
- if (updates.status !== void 0) {
3013
- note.status = updates.status;
3206
+ tagsUpdate.tags = normalizeTags(updates.tags);
3014
3207
  }
3015
- note.updatedDate = todayISO();
3208
+ const note = {
3209
+ ...existing,
3210
+ ...updates.content !== void 0 && { content: updates.content },
3211
+ ...updates.title !== void 0 && {
3212
+ title: !updates.title?.trim() ? null : updates.title
3213
+ },
3214
+ ...tagsUpdate,
3215
+ ...updates.status !== void 0 && { status: updates.status },
3216
+ updatedDate: todayISO()
3217
+ };
3016
3218
  await writeNoteUnlocked(note, root);
3017
3219
  updatedNote = note;
3018
3220
  });
@@ -3286,8 +3488,258 @@ function handleExport(ctx, mode, phaseId) {
3286
3488
  return { output: formatExport(ctx.state, mode, phaseId, ctx.format) };
3287
3489
  }
3288
3490
 
3491
+ // src/cli/commands/selftest.ts
3492
+ async function handleSelftest(root, format, failAfter) {
3493
+ const results = [];
3494
+ const createdIds = [];
3495
+ const cleanupErrors = [];
3496
+ function record(entity, step, passed2, detail) {
3497
+ results.push({ entity, step, passed: passed2, detail });
3498
+ if (failAfter !== void 0 && results.filter((r) => r.passed).length >= failAfter) {
3499
+ throw new Error(`failAfter(${failAfter}): induced failure for testing`);
3500
+ }
3501
+ }
3502
+ try {
3503
+ let ticketId;
3504
+ try {
3505
+ const { state } = await loadProject(root);
3506
+ ticketId = nextTicketID(state.tickets);
3507
+ const today = todayISO();
3508
+ const ticket = {
3509
+ id: ticketId,
3510
+ title: "selftest ticket",
3511
+ type: "chore",
3512
+ status: "open",
3513
+ phase: null,
3514
+ order: 0,
3515
+ description: "Integration smoke test \u2014 will be deleted.",
3516
+ createdDate: today,
3517
+ completedDate: null,
3518
+ blockedBy: [],
3519
+ parentTicket: null
3520
+ };
3521
+ await writeTicket(ticket, root);
3522
+ createdIds.push({ type: "ticket", id: ticketId });
3523
+ record("ticket", "create", true, `Created ${ticketId}`);
3524
+ } catch (err) {
3525
+ record("ticket", "create", false, errMsg(err));
3526
+ }
3527
+ if (ticketId) {
3528
+ try {
3529
+ const { state } = await loadProject(root);
3530
+ const found = state.ticketByID(ticketId);
3531
+ if (!found) throw new Error(`${ticketId} not found after create`);
3532
+ record("ticket", "get", true, `Found ${ticketId}`);
3533
+ } catch (err) {
3534
+ record("ticket", "get", false, errMsg(err));
3535
+ }
3536
+ try {
3537
+ const { state } = await loadProject(root);
3538
+ const existing = state.ticketByID(ticketId);
3539
+ if (!existing) throw new Error(`${ticketId} not found for update`);
3540
+ const updated = { ...existing, status: "inprogress" };
3541
+ await writeTicket(updated, root);
3542
+ record("ticket", "update", true, `Updated ${ticketId} status \u2192 inprogress`);
3543
+ } catch (err) {
3544
+ record("ticket", "update", false, errMsg(err));
3545
+ }
3546
+ try {
3547
+ const { state } = await loadProject(root);
3548
+ const found = state.ticketByID(ticketId);
3549
+ if (!found) throw new Error(`${ticketId} not found for verify`);
3550
+ if (found.status !== "inprogress") throw new Error(`Expected inprogress, got ${found.status}`);
3551
+ record("ticket", "verify update", true, `Verified ${ticketId} status = inprogress`);
3552
+ } catch (err) {
3553
+ record("ticket", "verify update", false, errMsg(err));
3554
+ }
3555
+ try {
3556
+ await deleteTicket(ticketId, root, { force: true });
3557
+ createdIds.splice(createdIds.findIndex((c) => c.id === ticketId), 1);
3558
+ record("ticket", "delete", true, `Deleted ${ticketId}`);
3559
+ } catch (err) {
3560
+ record("ticket", "delete", false, errMsg(err));
3561
+ }
3562
+ try {
3563
+ const { state } = await loadProject(root);
3564
+ const found = state.ticketByID(ticketId);
3565
+ if (found) throw new Error(`${ticketId} still exists after delete`);
3566
+ record("ticket", "verify delete", true, `Confirmed ${ticketId} absent`);
3567
+ } catch (err) {
3568
+ record("ticket", "verify delete", false, errMsg(err));
3569
+ }
3570
+ }
3571
+ let issueId;
3572
+ try {
3573
+ const { state } = await loadProject(root);
3574
+ issueId = nextIssueID(state.issues);
3575
+ const today = todayISO();
3576
+ const issue = {
3577
+ id: issueId,
3578
+ title: "selftest issue",
3579
+ status: "open",
3580
+ severity: "low",
3581
+ components: [],
3582
+ impact: "Integration smoke test \u2014 will be deleted.",
3583
+ resolution: null,
3584
+ location: [],
3585
+ discoveredDate: today,
3586
+ resolvedDate: null,
3587
+ relatedTickets: [],
3588
+ order: 0,
3589
+ phase: null
3590
+ };
3591
+ await writeIssue(issue, root);
3592
+ createdIds.push({ type: "issue", id: issueId });
3593
+ record("issue", "create", true, `Created ${issueId}`);
3594
+ } catch (err) {
3595
+ record("issue", "create", false, errMsg(err));
3596
+ }
3597
+ if (issueId) {
3598
+ try {
3599
+ const { state } = await loadProject(root);
3600
+ const found = state.issueByID(issueId);
3601
+ if (!found) throw new Error(`${issueId} not found after create`);
3602
+ record("issue", "get", true, `Found ${issueId}`);
3603
+ } catch (err) {
3604
+ record("issue", "get", false, errMsg(err));
3605
+ }
3606
+ try {
3607
+ const { state } = await loadProject(root);
3608
+ const existing = state.issueByID(issueId);
3609
+ if (!existing) throw new Error(`${issueId} not found for update`);
3610
+ const updated = { ...existing, status: "inprogress" };
3611
+ await writeIssue(updated, root);
3612
+ record("issue", "update", true, `Updated ${issueId} status \u2192 inprogress`);
3613
+ } catch (err) {
3614
+ record("issue", "update", false, errMsg(err));
3615
+ }
3616
+ try {
3617
+ const { state } = await loadProject(root);
3618
+ const found = state.issueByID(issueId);
3619
+ if (!found) throw new Error(`${issueId} not found for verify`);
3620
+ if (found.status !== "inprogress") throw new Error(`Expected inprogress, got ${found.status}`);
3621
+ record("issue", "verify update", true, `Verified ${issueId} status = inprogress`);
3622
+ } catch (err) {
3623
+ record("issue", "verify update", false, errMsg(err));
3624
+ }
3625
+ try {
3626
+ await deleteIssue(issueId, root);
3627
+ createdIds.splice(createdIds.findIndex((c) => c.id === issueId), 1);
3628
+ record("issue", "delete", true, `Deleted ${issueId}`);
3629
+ } catch (err) {
3630
+ record("issue", "delete", false, errMsg(err));
3631
+ }
3632
+ try {
3633
+ const { state } = await loadProject(root);
3634
+ const found = state.issueByID(issueId);
3635
+ if (found) throw new Error(`${issueId} still exists after delete`);
3636
+ record("issue", "verify delete", true, `Confirmed ${issueId} absent`);
3637
+ } catch (err) {
3638
+ record("issue", "verify delete", false, errMsg(err));
3639
+ }
3640
+ }
3641
+ let noteId;
3642
+ try {
3643
+ const { state } = await loadProject(root);
3644
+ noteId = nextNoteID(state.notes);
3645
+ const today = todayISO();
3646
+ const note = {
3647
+ id: noteId,
3648
+ title: "selftest note",
3649
+ content: "Integration smoke test \u2014 will be deleted.",
3650
+ tags: [],
3651
+ status: "active",
3652
+ createdDate: today,
3653
+ updatedDate: today
3654
+ };
3655
+ await writeNote(note, root);
3656
+ createdIds.push({ type: "note", id: noteId });
3657
+ record("note", "create", true, `Created ${noteId}`);
3658
+ } catch (err) {
3659
+ record("note", "create", false, errMsg(err));
3660
+ }
3661
+ if (noteId) {
3662
+ try {
3663
+ const { state } = await loadProject(root);
3664
+ const found = state.noteByID(noteId);
3665
+ if (!found) throw new Error(`${noteId} not found after create`);
3666
+ record("note", "get", true, `Found ${noteId}`);
3667
+ } catch (err) {
3668
+ record("note", "get", false, errMsg(err));
3669
+ }
3670
+ try {
3671
+ const { state } = await loadProject(root);
3672
+ const existing = state.noteByID(noteId);
3673
+ if (!existing) throw new Error(`${noteId} not found for update`);
3674
+ const updated = { ...existing, status: "archived", updatedDate: todayISO() };
3675
+ await writeNote(updated, root);
3676
+ record("note", "update", true, `Updated ${noteId} status \u2192 archived`);
3677
+ } catch (err) {
3678
+ record("note", "update", false, errMsg(err));
3679
+ }
3680
+ try {
3681
+ const { state } = await loadProject(root);
3682
+ const found = state.noteByID(noteId);
3683
+ if (!found) throw new Error(`${noteId} not found for verify`);
3684
+ if (found.status !== "archived") throw new Error(`Expected archived, got ${found.status}`);
3685
+ record("note", "verify update", true, `Verified ${noteId} status = archived`);
3686
+ } catch (err) {
3687
+ record("note", "verify update", false, errMsg(err));
3688
+ }
3689
+ try {
3690
+ await deleteNote(noteId, root);
3691
+ createdIds.splice(createdIds.findIndex((c) => c.id === noteId), 1);
3692
+ record("note", "delete", true, `Deleted ${noteId}`);
3693
+ } catch (err) {
3694
+ record("note", "delete", false, errMsg(err));
3695
+ }
3696
+ try {
3697
+ const { state } = await loadProject(root);
3698
+ const found = state.noteByID(noteId);
3699
+ if (found) throw new Error(`${noteId} still exists after delete`);
3700
+ record("note", "verify delete", true, `Confirmed ${noteId} absent`);
3701
+ } catch (err) {
3702
+ record("note", "verify delete", false, errMsg(err));
3703
+ }
3704
+ }
3705
+ } finally {
3706
+ for (const { type, id } of createdIds.reverse()) {
3707
+ try {
3708
+ if (type === "ticket") await deleteTicket(id, root, { force: true });
3709
+ else if (type === "issue") await deleteIssue(id, root);
3710
+ else await deleteNote(id, root);
3711
+ } catch (err) {
3712
+ cleanupErrors.push(`Failed to delete ${type} ${id}: ${errMsg(err)}`);
3713
+ }
3714
+ }
3715
+ }
3716
+ const passed = results.filter((r) => r.passed).length;
3717
+ const failed = results.filter((r) => !r.passed).length;
3718
+ const result = {
3719
+ passed,
3720
+ failed,
3721
+ total: results.length,
3722
+ results,
3723
+ cleanupErrors
3724
+ };
3725
+ return { output: formatSelftestResult(result, format) };
3726
+ }
3727
+ function errMsg(err) {
3728
+ return err instanceof Error ? err.message : String(err);
3729
+ }
3730
+
3289
3731
  // src/cli/commands/phase.ts
3290
3732
  import { join as join6, resolve as resolve6 } from "path";
3733
+ var PHASE_ID_REGEX = /^[a-z0-9]+(-[a-z0-9]+)*$/;
3734
+ var PHASE_ID_MAX_LENGTH = 40;
3735
+ function validatePhaseId(id) {
3736
+ if (id.length > PHASE_ID_MAX_LENGTH) {
3737
+ throw new CliValidationError("invalid_input", `Phase ID "${id}" exceeds ${PHASE_ID_MAX_LENGTH} characters`);
3738
+ }
3739
+ if (!PHASE_ID_REGEX.test(id)) {
3740
+ throw new CliValidationError("invalid_input", `Phase ID "${id}" must be lowercase alphanumeric with hyphens (e.g. "my-phase")`);
3741
+ }
3742
+ }
3291
3743
  function handlePhaseList(ctx) {
3292
3744
  return { output: formatPhaseList(ctx.state, ctx.format) };
3293
3745
  }
@@ -3333,6 +3785,46 @@ function handlePhaseTickets(phaseId, ctx) {
3333
3785
  }
3334
3786
  return { output: formatPhaseTickets(phaseId, ctx.state, ctx.format) };
3335
3787
  }
3788
+ async function handlePhaseCreate(args, format, root) {
3789
+ validatePhaseId(args.id);
3790
+ if (args.atStart && args.after) {
3791
+ throw new CliValidationError("invalid_input", "Cannot use both --after and --at-start");
3792
+ }
3793
+ if (!args.atStart && !args.after) {
3794
+ throw new CliValidationError("invalid_input", "Must specify either --after <phase-id> or --at-start");
3795
+ }
3796
+ let createdPhase;
3797
+ await withProjectLock(root, { strict: true }, async ({ state }) => {
3798
+ if (state.roadmap.phases.some((p) => p.id === args.id)) {
3799
+ throw new CliValidationError("conflict", `Phase "${args.id}" already exists`);
3800
+ }
3801
+ const phase = {
3802
+ id: args.id,
3803
+ label: args.label,
3804
+ name: args.name,
3805
+ description: args.description,
3806
+ ...args.summary !== void 0 && { summary: args.summary }
3807
+ };
3808
+ const newPhases = [...state.roadmap.phases];
3809
+ if (args.atStart) {
3810
+ newPhases.unshift(phase);
3811
+ } else {
3812
+ const afterIdx = newPhases.findIndex((p) => p.id === args.after);
3813
+ if (afterIdx < 0) {
3814
+ throw new CliValidationError("not_found", `Phase "${args.after}" not found`);
3815
+ }
3816
+ newPhases.splice(afterIdx + 1, 0, phase);
3817
+ }
3818
+ const newRoadmap = { ...state.roadmap, phases: newPhases };
3819
+ await writeRoadmapUnlocked(newRoadmap, root);
3820
+ createdPhase = phase;
3821
+ });
3822
+ if (!createdPhase) throw new Error("Phase not created");
3823
+ if (format === "json") {
3824
+ return { output: JSON.stringify(successEnvelope(createdPhase), null, 2) };
3825
+ }
3826
+ return { output: `Created phase ${createdPhase.id}: ${createdPhase.name}` };
3827
+ }
3336
3828
 
3337
3829
  // src/mcp/tools.ts
3338
3830
  var INFRASTRUCTURE_ERROR_CODES = [
@@ -3592,7 +4084,8 @@ function registerAllTools(server, pinnedRoot) {
3592
4084
  id: z8.string().regex(TICKET_ID_REGEX).describe("Ticket ID (e.g. T-001)"),
3593
4085
  status: z8.enum(TICKET_STATUSES).optional().describe("New status: open, inprogress, complete"),
3594
4086
  title: z8.string().optional().describe("New title"),
3595
- order: z8.number().optional().describe("New sort order"),
4087
+ type: z8.enum(TICKET_TYPES).optional().describe("New type: task, feature, chore"),
4088
+ order: z8.number().int().optional().describe("New sort order"),
3596
4089
  description: z8.string().optional().describe("New description"),
3597
4090
  phase: z8.string().nullable().optional().describe("New phase ID (null to clear)"),
3598
4091
  parentTicket: z8.string().regex(TICKET_ID_REGEX).nullable().optional().describe("Parent ticket ID (null to clear)"),
@@ -3605,6 +4098,7 @@ function registerAllTools(server, pinnedRoot) {
3605
4098
  {
3606
4099
  status: args.status,
3607
4100
  title: args.title,
4101
+ type: args.type,
3608
4102
  order: args.order,
3609
4103
  description: args.description,
3610
4104
  phase: args.phase,
@@ -3653,7 +4147,9 @@ function registerAllTools(server, pinnedRoot) {
3653
4147
  resolution: z8.string().nullable().optional().describe("Resolution description (null to clear)"),
3654
4148
  components: z8.array(z8.string()).optional().describe("Affected components"),
3655
4149
  relatedTickets: z8.array(z8.string().regex(TICKET_ID_REGEX)).optional().describe("Related ticket IDs"),
3656
- location: z8.array(z8.string()).optional().describe("File locations")
4150
+ location: z8.array(z8.string()).optional().describe("File locations"),
4151
+ order: z8.number().int().optional().describe("New sort order"),
4152
+ phase: z8.string().nullable().optional().describe("New phase ID (null to clear)")
3657
4153
  }
3658
4154
  }, (args) => runMcpWriteTool(
3659
4155
  pinnedRoot,
@@ -3667,7 +4163,9 @@ function registerAllTools(server, pinnedRoot) {
3667
4163
  resolution: args.resolution,
3668
4164
  components: args.components,
3669
4165
  relatedTickets: args.relatedTickets,
3670
- location: args.location
4166
+ location: args.location,
4167
+ order: args.order,
4168
+ phase: args.phase
3671
4169
  },
3672
4170
  format,
3673
4171
  root
@@ -3732,6 +4230,125 @@ function registerAllTools(server, pinnedRoot) {
3732
4230
  root
3733
4231
  )
3734
4232
  ));
4233
+ server.registerTool("claudestory_phase_create", {
4234
+ description: "Create a new phase in the roadmap. Exactly one of after or atStart is required for positioning.",
4235
+ inputSchema: {
4236
+ id: z8.string().describe("Phase ID \u2014 lowercase alphanumeric with hyphens (e.g. 'my-phase')"),
4237
+ name: z8.string().describe("Phase display name"),
4238
+ label: z8.string().describe("Phase label (e.g. 'PHASE 1')"),
4239
+ description: z8.string().describe("Phase description"),
4240
+ summary: z8.string().optional().describe("One-line summary for compact display"),
4241
+ after: z8.string().optional().describe("Insert after this phase ID"),
4242
+ atStart: z8.boolean().optional().describe("Insert at beginning of roadmap")
4243
+ }
4244
+ }, (args) => runMcpWriteTool(
4245
+ pinnedRoot,
4246
+ (root, format) => handlePhaseCreate(
4247
+ {
4248
+ id: args.id,
4249
+ name: args.name,
4250
+ label: args.label,
4251
+ description: args.description,
4252
+ summary: args.summary,
4253
+ after: args.after,
4254
+ atStart: args.atStart ?? false
4255
+ },
4256
+ format,
4257
+ root
4258
+ )
4259
+ ));
4260
+ server.registerTool("claudestory_selftest", {
4261
+ description: "Integration smoke test \u2014 creates, updates, and deletes test entities to verify the full pipeline"
4262
+ }, () => runMcpWriteTool(
4263
+ pinnedRoot,
4264
+ (root, format) => handleSelftest(root, format)
4265
+ ));
4266
+ }
4267
+
4268
+ // src/core/init.ts
4269
+ import { mkdir as mkdir4, stat as stat2 } from "fs/promises";
4270
+ import { join as join8, resolve as resolve7 } from "path";
4271
+ async function initProject(root, options) {
4272
+ const absRoot = resolve7(root);
4273
+ const wrapDir = join8(absRoot, ".story");
4274
+ let exists = false;
4275
+ try {
4276
+ const s = await stat2(wrapDir);
4277
+ if (s.isDirectory()) exists = true;
4278
+ } catch (err) {
4279
+ if (err.code !== "ENOENT") {
4280
+ throw new ProjectLoaderError(
4281
+ "io_error",
4282
+ `Cannot check .story/ directory: ${err.message}`,
4283
+ err
4284
+ );
4285
+ }
4286
+ }
4287
+ if (exists && !options.force) {
4288
+ throw new ProjectLoaderError(
4289
+ "conflict",
4290
+ ".story/ already exists. Use --force to overwrite config and roadmap."
4291
+ );
4292
+ }
4293
+ await mkdir4(join8(wrapDir, "tickets"), { recursive: true });
4294
+ await mkdir4(join8(wrapDir, "issues"), { recursive: true });
4295
+ await mkdir4(join8(wrapDir, "handovers"), { recursive: true });
4296
+ await mkdir4(join8(wrapDir, "notes"), { recursive: true });
4297
+ const created = [
4298
+ ".story/config.json",
4299
+ ".story/roadmap.json",
4300
+ ".story/tickets/",
4301
+ ".story/issues/",
4302
+ ".story/handovers/",
4303
+ ".story/notes/"
4304
+ ];
4305
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
4306
+ const config = {
4307
+ version: 2,
4308
+ schemaVersion: CURRENT_SCHEMA_VERSION,
4309
+ project: options.name,
4310
+ type: options.type ?? "generic",
4311
+ language: options.language ?? "unknown",
4312
+ features: {
4313
+ tickets: true,
4314
+ issues: true,
4315
+ handovers: true,
4316
+ roadmap: true,
4317
+ reviews: true
4318
+ }
4319
+ };
4320
+ const roadmap = {
4321
+ title: options.name,
4322
+ date: today,
4323
+ phases: options.phases ?? [
4324
+ {
4325
+ id: "p0",
4326
+ label: "PHASE 0",
4327
+ name: "Setup",
4328
+ description: "Initial project setup."
4329
+ }
4330
+ ],
4331
+ blockers: []
4332
+ };
4333
+ await writeConfig(config, absRoot);
4334
+ await writeRoadmap(roadmap, absRoot);
4335
+ const warnings = [];
4336
+ if (options.force && exists) {
4337
+ try {
4338
+ const { warnings: loadWarnings } = await loadProject(absRoot);
4339
+ for (const w of loadWarnings) {
4340
+ if (INTEGRITY_WARNING_TYPES.includes(w.type)) {
4341
+ warnings.push(`${w.file}: ${w.message}`);
4342
+ }
4343
+ }
4344
+ } catch {
4345
+ }
4346
+ }
4347
+ return {
4348
+ root: absRoot,
4349
+ created,
4350
+ warnings
4351
+ };
3735
4352
  }
3736
4353
 
3737
4354
  // src/mcp/index.ts
@@ -3746,10 +4363,10 @@ function tryDiscoverRoot() {
3746
4363
  `);
3747
4364
  return null;
3748
4365
  }
3749
- const resolved = resolve7(envRoot);
4366
+ const resolved = resolve8(envRoot);
3750
4367
  try {
3751
4368
  const canonical = realpathSync(resolved);
3752
- if (existsSync6(join8(canonical, CONFIG_PATH2))) {
4369
+ if (existsSync6(join9(canonical, CONFIG_PATH2))) {
3753
4370
  return canonical;
3754
4371
  }
3755
4372
  process.stderr.write(`Warning: No .story/config.json at ${canonical}
@@ -3767,12 +4384,68 @@ function tryDiscoverRoot() {
3767
4384
  return null;
3768
4385
  }
3769
4386
  }
4387
+ function registerDegradedTools(server) {
4388
+ const degradedStatus = server.registerTool("claudestory_status", {
4389
+ description: "Project summary \u2014 returns guidance if no .story/ project found"
4390
+ }, () => Promise.resolve({
4391
+ content: [{ type: "text", text: "No .story/ project found. Use claudestory_init to create one, or navigate to a directory with .story/." }],
4392
+ isError: true
4393
+ }));
4394
+ const degradedInit = server.registerTool("claudestory_init", {
4395
+ description: "Initialize a new .story/ project in the current directory",
4396
+ inputSchema: {
4397
+ name: z9.string().describe("Project name"),
4398
+ type: z9.string().optional().describe("Project type (e.g. npm, macapp, cargo, generic)"),
4399
+ language: z9.string().optional().describe("Primary language (e.g. typescript, swift, rust)")
4400
+ }
4401
+ }, async (args) => {
4402
+ let result;
4403
+ try {
4404
+ const projectRoot = realpathSync(process.cwd());
4405
+ result = await initProject(projectRoot, {
4406
+ name: args.name,
4407
+ type: args.type,
4408
+ language: args.language,
4409
+ phases: []
4410
+ });
4411
+ } catch (err) {
4412
+ const msg = err instanceof Error ? err.message : String(err);
4413
+ return { content: [{ type: "text", text: `[init_error] ${msg}` }], isError: true };
4414
+ }
4415
+ try {
4416
+ degradedStatus.remove();
4417
+ degradedInit.remove();
4418
+ registerAllTools(server, result.root);
4419
+ } catch (swapErr) {
4420
+ process.stderr.write(`claudestory: tool-swap failed after init: ${swapErr instanceof Error ? swapErr.message : String(swapErr)}
4421
+ `);
4422
+ try {
4423
+ registerDegradedTools(server);
4424
+ } catch {
4425
+ }
4426
+ return { content: [{ type: "text", text: `Initialized .story/ project "${args.name}" at ${result.root}
4427
+
4428
+ Warning: tool registration failed. Restart the MCP server for full tool access.` }] };
4429
+ }
4430
+ process.stderr.write(`claudestory: initialized at ${result.root}
4431
+ `);
4432
+ const lines = [
4433
+ `Initialized .story/ project "${args.name}" at ${result.root}`,
4434
+ `Created: ${result.created.join(", ")}`
4435
+ ];
4436
+ if (result.warnings.length > 0) {
4437
+ lines.push(`Warnings: ${result.warnings.join("; ")}`);
4438
+ }
4439
+ lines.push("", "All claudestory tools are now available. Use claudestory_phase_create to add phases and claudestory_ticket_create to add tickets.");
4440
+ return { content: [{ type: "text", text: lines.join("\n") }] };
4441
+ });
4442
+ }
3770
4443
  async function main() {
3771
4444
  const root = tryDiscoverRoot();
3772
4445
  const server = new McpServer(
3773
4446
  { name: "claudestory", version },
3774
4447
  {
3775
- instructions: root ? "Start with claudestory_status for a project overview, then claudestory_ticket_next for the highest-priority work, then claudestory_handover_latest for session context." : "No .story/ project found in the current directory. Navigate to a project with a .story/ directory, or set CLAUDESTORY_PROJECT_ROOT."
4448
+ instructions: root ? "Start with claudestory_status for a project overview, then claudestory_ticket_next for the highest-priority work, then claudestory_handover_latest for session context." : "No .story/ project found. Use claudestory_init to initialize a new project, or navigate to a directory with .story/."
3776
4449
  }
3777
4450
  );
3778
4451
  if (root) {
@@ -3780,13 +4453,8 @@ async function main() {
3780
4453
  process.stderr.write(`claudestory MCP server running (root: ${root})
3781
4454
  `);
3782
4455
  } else {
3783
- server.registerTool("claudestory_status", {
3784
- description: "Project summary \u2014 returns error if no .story/ project found"
3785
- }, () => Promise.resolve({
3786
- content: [{ type: "text", text: "No .story/ project found. Navigate to a directory containing .story/ or set CLAUDESTORY_PROJECT_ROOT." }],
3787
- isError: true
3788
- }));
3789
- process.stderr.write("claudestory MCP server running (no project found \u2014 tools will report errors)\n");
4456
+ registerDegradedTools(server);
4457
+ process.stderr.write("claudestory MCP server running (no project \u2014 claudestory_init available)\n");
3790
4458
  }
3791
4459
  const transport = new StdioServerTransport();
3792
4460
  await server.connect(transport);