@anthropologies/claudestory 0.1.4 → 0.1.5

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/cli.js CHANGED
@@ -484,6 +484,12 @@ function formatHandoverContent(filename, content, format) {
484
484
  }
485
485
  return content;
486
486
  }
487
+ function formatHandoverCreateResult(filename, format) {
488
+ if (format === "json") {
489
+ return JSON.stringify(successEnvelope({ filename }), null, 2);
490
+ }
491
+ return `Created handover: ${filename}`;
492
+ }
487
493
  function formatSnapshotResult(result, format) {
488
494
  if (format === "json") {
489
495
  return JSON.stringify(successEnvelope(result), null, 2);
@@ -1119,6 +1125,7 @@ import { readdir, readFile } from "fs/promises";
1119
1125
  import { existsSync } from "fs";
1120
1126
  import { join, relative, extname } from "path";
1121
1127
  var HANDOVER_DATE_REGEX = /^\d{4}-\d{2}-\d{2}/;
1128
+ var HANDOVER_SEQ_REGEX = /^(\d{4}-\d{2}-\d{2})-(\d{2})-/;
1122
1129
  async function listHandovers(handoversDir, root, warnings) {
1123
1130
  if (!existsSync(handoversDir)) return [];
1124
1131
  let entries;
@@ -1148,7 +1155,16 @@ async function listHandovers(handoversDir, root, warnings) {
1148
1155
  });
1149
1156
  }
1150
1157
  }
1151
- conforming.sort((a, b) => b.localeCompare(a));
1158
+ conforming.sort((a, b) => {
1159
+ const dateA = a.slice(0, 10);
1160
+ const dateB = b.slice(0, 10);
1161
+ if (dateA !== dateB) return dateB.localeCompare(dateA);
1162
+ const seqA = a.match(HANDOVER_SEQ_REGEX);
1163
+ const seqB = b.match(HANDOVER_SEQ_REGEX);
1164
+ if (seqA && !seqB) return -1;
1165
+ if (!seqA && seqB) return 1;
1166
+ return b.localeCompare(a);
1167
+ });
1152
1168
  return [...conforming, ...nonConforming];
1153
1169
  }
1154
1170
  async function readHandover(handoversDir, filename) {
@@ -2524,6 +2540,8 @@ function handleValidate(ctx) {
2524
2540
 
2525
2541
  // src/cli/commands/handover.ts
2526
2542
  init_esm_shims();
2543
+ import { mkdir as mkdir3 } from "fs/promises";
2544
+ import { join as join7, resolve as resolve6 } from "path";
2527
2545
  function handleHandoverList(ctx) {
2528
2546
  return { output: formatHandoverList(ctx.state.handoverFilenames, ctx.format) };
2529
2547
  }
@@ -2575,6 +2593,59 @@ async function handleHandoverGet(filename, ctx) {
2575
2593
  };
2576
2594
  }
2577
2595
  }
2596
+ function normalizeSlug(raw) {
2597
+ let slug = raw.trim().toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "").replace(/-+/g, "-").replace(/^-|-$/g, "");
2598
+ if (slug.length > 60) slug = slug.slice(0, 60).replace(/-$/, "");
2599
+ if (!slug) {
2600
+ throw new CliValidationError(
2601
+ "invalid_input",
2602
+ `Slug is empty after normalization: "${raw}"`
2603
+ );
2604
+ }
2605
+ return slug;
2606
+ }
2607
+ async function handleHandoverCreate(content, slugRaw, format, root) {
2608
+ if (!content.trim()) {
2609
+ throw new CliValidationError("invalid_input", "Handover content is empty");
2610
+ }
2611
+ const slug = normalizeSlug(slugRaw);
2612
+ const date = todayISO();
2613
+ let filename;
2614
+ await withProjectLock(root, { strict: false }, async () => {
2615
+ const absRoot = resolve6(root);
2616
+ const handoversDir = join7(absRoot, ".story", "handovers");
2617
+ await mkdir3(handoversDir, { recursive: true });
2618
+ const wrapDir = join7(absRoot, ".story");
2619
+ const datePrefix = `${date}-`;
2620
+ const seqRegex = new RegExp(`^${date}-(\\d{2})-`);
2621
+ let maxSeq = 0;
2622
+ const { readdirSync } = await import("fs");
2623
+ try {
2624
+ for (const f of readdirSync(handoversDir)) {
2625
+ const m = f.match(seqRegex);
2626
+ if (m) {
2627
+ const n = parseInt(m[1], 10);
2628
+ if (n > maxSeq) maxSeq = n;
2629
+ }
2630
+ }
2631
+ } catch {
2632
+ }
2633
+ const nextSeq = maxSeq + 1;
2634
+ if (nextSeq > 99) {
2635
+ throw new CliValidationError(
2636
+ "conflict",
2637
+ `Too many handovers for ${date}; limit is 99 per day`
2638
+ );
2639
+ }
2640
+ const candidate = `${date}-${String(nextSeq).padStart(2, "0")}-${slug}.md`;
2641
+ const candidatePath = join7(handoversDir, candidate);
2642
+ await parseHandoverFilename(candidate, handoversDir);
2643
+ await guardPath(candidatePath, wrapDir);
2644
+ await atomicWrite(candidatePath, content);
2645
+ filename = candidate;
2646
+ });
2647
+ return { output: formatHandoverCreateResult(filename, format) };
2648
+ }
2578
2649
 
2579
2650
  // src/cli/commands/blocker.ts
2580
2651
  init_esm_shims();
@@ -3000,7 +3071,7 @@ async function handleIssueDelete(id, format, root) {
3000
3071
 
3001
3072
  // src/cli/commands/phase.ts
3002
3073
  init_esm_shims();
3003
- import { join as join7, resolve as resolve6 } from "path";
3074
+ import { join as join8, resolve as resolve7 } from "path";
3004
3075
  var PHASE_ID_REGEX = /^[a-z0-9]+(-[a-z0-9]+)*$/;
3005
3076
  var PHASE_ID_MAX_LENGTH = 40;
3006
3077
  function validatePhaseId(id) {
@@ -3183,7 +3254,7 @@ async function handlePhaseDelete(id, reassign, format, root) {
3183
3254
  }
3184
3255
  const targetLeaves = state.phaseTickets(reassign);
3185
3256
  let maxOrder = targetLeaves.length > 0 ? targetLeaves[targetLeaves.length - 1].order : 0;
3186
- const wrapDir = resolve6(root, ".story");
3257
+ const wrapDir = resolve7(root, ".story");
3187
3258
  const operations = [];
3188
3259
  const sortedTickets = [...affectedTickets].sort((a, b) => a.order - b.order);
3189
3260
  for (const ticket of sortedTickets) {
@@ -3191,21 +3262,21 @@ async function handlePhaseDelete(id, reassign, format, root) {
3191
3262
  const updated = { ...ticket, phase: reassign, order: maxOrder };
3192
3263
  const parsed = TicketSchema.parse(updated);
3193
3264
  const content = serializeJSON(parsed);
3194
- const target = join7(wrapDir, "tickets", `${parsed.id}.json`);
3265
+ const target = join8(wrapDir, "tickets", `${parsed.id}.json`);
3195
3266
  operations.push({ op: "write", target, content });
3196
3267
  }
3197
3268
  for (const issue of affectedIssues) {
3198
3269
  const updated = { ...issue, phase: reassign };
3199
3270
  const parsed = IssueSchema.parse(updated);
3200
3271
  const content = serializeJSON(parsed);
3201
- const target = join7(wrapDir, "issues", `${parsed.id}.json`);
3272
+ const target = join8(wrapDir, "issues", `${parsed.id}.json`);
3202
3273
  operations.push({ op: "write", target, content });
3203
3274
  }
3204
3275
  const newPhases = state.roadmap.phases.filter((p) => p.id !== id);
3205
3276
  const newRoadmap = { ...state.roadmap, phases: newPhases };
3206
3277
  const parsedRoadmap = RoadmapSchema.parse(newRoadmap);
3207
3278
  const roadmapContent = serializeJSON(parsedRoadmap);
3208
- const roadmapTarget = join7(wrapDir, "roadmap.json");
3279
+ const roadmapTarget = join8(wrapDir, "roadmap.json");
3209
3280
  operations.push({ op: "write", target: roadmapTarget, content: roadmapContent });
3210
3281
  await runTransactionUnlocked(root, operations);
3211
3282
  } else {
@@ -3383,7 +3454,83 @@ function registerHandoverCommand(yargs2) {
3383
3454
  (ctx) => handleHandoverGet(filename, ctx)
3384
3455
  );
3385
3456
  }
3386
- ).demandCommand(1, "Specify a handover subcommand: list, latest, get").strict(),
3457
+ ).command(
3458
+ "create",
3459
+ "Create a new handover document",
3460
+ (y2) => addFormatOption(
3461
+ y2.option("content", {
3462
+ type: "string",
3463
+ describe: "Handover content (markdown string)"
3464
+ }).option("stdin", {
3465
+ type: "boolean",
3466
+ describe: "Read content from stdin"
3467
+ }).option("slug", {
3468
+ type: "string",
3469
+ default: "session",
3470
+ describe: "Slug for filename (e.g. phase5b-wrapup)"
3471
+ }).conflicts("content", "stdin").check((argv) => {
3472
+ if (!argv.content && !argv.stdin) {
3473
+ throw new Error(
3474
+ "Specify either --content or --stdin"
3475
+ );
3476
+ }
3477
+ return true;
3478
+ })
3479
+ ),
3480
+ async (argv) => {
3481
+ const format = parseOutputFormat(argv.format);
3482
+ const root = (await Promise.resolve().then(() => (init_project_root_discovery(), project_root_discovery_exports))).discoverProjectRoot();
3483
+ if (!root) {
3484
+ writeOutput(
3485
+ formatError("not_found", "No .story/ project found.", format)
3486
+ );
3487
+ process.exitCode = ExitCode.USER_ERROR;
3488
+ return;
3489
+ }
3490
+ let content;
3491
+ if (argv.stdin) {
3492
+ if (process.stdin.isTTY) {
3493
+ writeOutput(
3494
+ formatError("invalid_input", "Cannot read from stdin: no pipe detected. Use --content instead.", format)
3495
+ );
3496
+ process.exitCode = ExitCode.USER_ERROR;
3497
+ return;
3498
+ }
3499
+ const chunks = [];
3500
+ for await (const chunk of process.stdin) {
3501
+ chunks.push(chunk);
3502
+ }
3503
+ content = Buffer.concat(chunks).toString("utf-8");
3504
+ } else {
3505
+ content = argv.content;
3506
+ }
3507
+ try {
3508
+ const result = await handleHandoverCreate(
3509
+ content,
3510
+ argv.slug,
3511
+ format,
3512
+ root
3513
+ );
3514
+ writeOutput(result.output);
3515
+ process.exitCode = result.exitCode ?? ExitCode.OK;
3516
+ } catch (err) {
3517
+ if (err instanceof CliValidationError) {
3518
+ writeOutput(formatError(err.code, err.message, format));
3519
+ process.exitCode = ExitCode.USER_ERROR;
3520
+ return;
3521
+ }
3522
+ const { ProjectLoaderError: ProjectLoaderError2 } = await Promise.resolve().then(() => (init_errors(), errors_exports));
3523
+ if (err instanceof ProjectLoaderError2) {
3524
+ writeOutput(formatError(err.code, err.message, format));
3525
+ process.exitCode = ExitCode.USER_ERROR;
3526
+ return;
3527
+ }
3528
+ const message = err instanceof Error ? err.message : String(err);
3529
+ writeOutput(formatError("io_error", message, format));
3530
+ process.exitCode = ExitCode.USER_ERROR;
3531
+ }
3532
+ }
3533
+ ).demandCommand(1, "Specify a handover subcommand: list, latest, get, create").strict(),
3387
3534
  () => {
3388
3535
  }
3389
3536
  );
@@ -4396,7 +4543,7 @@ function registerExportCommand(yargs2) {
4396
4543
  }
4397
4544
 
4398
4545
  // src/cli/index.ts
4399
- var version = "0.1.4";
4546
+ var version = "0.1.5";
4400
4547
  var HandledError = class extends Error {
4401
4548
  constructor() {
4402
4549
  super("HANDLED_ERROR");
package/dist/index.d.ts CHANGED
@@ -1364,6 +1364,7 @@ declare function formatInitResult(result: {
1364
1364
  }, format: OutputFormat): string;
1365
1365
  declare function formatHandoverList(filenames: readonly string[], format: OutputFormat): string;
1366
1366
  declare function formatHandoverContent(filename: string, content: string, format: OutputFormat): string;
1367
+ declare function formatHandoverCreateResult(filename: string, format: OutputFormat): string;
1367
1368
 
1368
1369
  declare function formatSnapshotResult(result: {
1369
1370
  filename: string;
@@ -1373,4 +1374,4 @@ declare function formatSnapshotResult(result: {
1373
1374
  declare function formatRecap(recap: RecapResult, state: ProjectState, format: OutputFormat): string;
1374
1375
  declare function formatExport(state: ProjectState, mode: "all" | "phase", phaseId: string | null, format: OutputFormat): string;
1375
1376
 
1376
- export { type Blocker, BlockerSchema, CURRENT_SCHEMA_VERSION, type Config, ConfigSchema, DATE_REGEX, DateSchema, ERROR_CODES, type ErrorCode, type ErrorEnvelope, ExitCode, type ExitCodeValue, type Features, FeaturesSchema, INTEGRITY_WARNING_TYPES, ISSUE_ID_REGEX, ISSUE_SEVERITIES, ISSUE_STATUSES, type InitOptions, type InitResult, type Issue, IssueIdSchema, IssueSchema, type IssueSeverity, type IssueStatus, type LoadOptions, type LoadResult, type LoadWarning, type LoadWarningType, type NextTicketAllBlocked, type NextTicketAllComplete, type NextTicketEmpty, type NextTicketOutcome, type NextTicketResult, OUTPUT_FORMATS, type OutputFormat, type PartialEnvelope, type Phase, PhaseSchema, type PhaseStatus, type PhaseWithStatus, ProjectLoaderError, ProjectState, type RecapResult, type Roadmap, RoadmapSchema, type SnapshotDiff, type SnapshotV1, SnapshotV1Schema, type SuccessEnvelope, TICKET_ID_REGEX, TICKET_STATUSES, TICKET_TYPES, type Ticket, TicketIdSchema, TicketSchema, type TicketStatus, type TicketType, type UmbrellaProgress, type UnblockImpact, type ValidationFinding, type ValidationLevel, type ValidationResult, type WithProjectLockOptions, atomicWrite, blockedTickets, buildRecap, currentPhase, deleteIssue, deleteTicket, diffStates, discoverProjectRoot, errorEnvelope, escapeMarkdownInline, extractHandoverDate, fencedBlock, formatBlockedTickets, formatBlockerList, formatError, formatExport, formatHandoverContent, formatHandoverList, formatInitResult, formatIssue, formatIssueList, formatNextTicketOutcome, formatPhaseList, formatPhaseTickets, formatRecap, formatSnapshotResult, formatStatus, formatTicket, formatTicketList, formatValidation, guardPath, initProject, isBlockerCleared, listHandovers, loadLatestSnapshot, loadProject, mergeValidation, nextIssueID, nextOrder, nextTicket, nextTicketID, partialEnvelope, phasesWithStatus, readHandover, runTransaction, runTransactionUnlocked, saveSnapshot, serializeJSON, sortKeysDeep, successEnvelope, ticketsUnblockedBy, umbrellaProgress, validateProject, withProjectLock, writeConfig, writeIssue, writeIssueUnlocked, writeRoadmap, writeRoadmapUnlocked, writeTicket, writeTicketUnlocked };
1377
+ export { type Blocker, BlockerSchema, CURRENT_SCHEMA_VERSION, type Config, ConfigSchema, DATE_REGEX, DateSchema, ERROR_CODES, type ErrorCode, type ErrorEnvelope, ExitCode, type ExitCodeValue, type Features, FeaturesSchema, INTEGRITY_WARNING_TYPES, ISSUE_ID_REGEX, ISSUE_SEVERITIES, ISSUE_STATUSES, type InitOptions, type InitResult, type Issue, IssueIdSchema, IssueSchema, type IssueSeverity, type IssueStatus, type LoadOptions, type LoadResult, type LoadWarning, type LoadWarningType, type NextTicketAllBlocked, type NextTicketAllComplete, type NextTicketEmpty, type NextTicketOutcome, type NextTicketResult, OUTPUT_FORMATS, type OutputFormat, type PartialEnvelope, type Phase, PhaseSchema, type PhaseStatus, type PhaseWithStatus, ProjectLoaderError, ProjectState, type RecapResult, type Roadmap, RoadmapSchema, type SnapshotDiff, type SnapshotV1, SnapshotV1Schema, type SuccessEnvelope, TICKET_ID_REGEX, TICKET_STATUSES, TICKET_TYPES, type Ticket, TicketIdSchema, TicketSchema, type TicketStatus, type TicketType, type UmbrellaProgress, type UnblockImpact, type ValidationFinding, type ValidationLevel, type ValidationResult, type WithProjectLockOptions, atomicWrite, blockedTickets, buildRecap, currentPhase, deleteIssue, deleteTicket, diffStates, discoverProjectRoot, errorEnvelope, escapeMarkdownInline, extractHandoverDate, fencedBlock, formatBlockedTickets, formatBlockerList, formatError, formatExport, formatHandoverContent, formatHandoverCreateResult, formatHandoverList, formatInitResult, formatIssue, formatIssueList, formatNextTicketOutcome, formatPhaseList, formatPhaseTickets, formatRecap, formatSnapshotResult, formatStatus, formatTicket, formatTicketList, formatValidation, guardPath, initProject, isBlockerCleared, listHandovers, loadLatestSnapshot, loadProject, mergeValidation, nextIssueID, nextOrder, nextTicket, nextTicketID, partialEnvelope, phasesWithStatus, readHandover, runTransaction, runTransactionUnlocked, saveSnapshot, serializeJSON, sortKeysDeep, successEnvelope, ticketsUnblockedBy, umbrellaProgress, validateProject, withProjectLock, writeConfig, writeIssue, writeIssueUnlocked, writeRoadmap, writeRoadmapUnlocked, writeTicket, writeTicketUnlocked };
package/dist/index.js CHANGED
@@ -358,6 +358,7 @@ import { readdir, readFile } from "fs/promises";
358
358
  import { existsSync } from "fs";
359
359
  import { join, relative, extname } from "path";
360
360
  var HANDOVER_DATE_REGEX = /^\d{4}-\d{2}-\d{2}/;
361
+ var HANDOVER_SEQ_REGEX = /^(\d{4}-\d{2}-\d{2})-(\d{2})-/;
361
362
  async function listHandovers(handoversDir, root, warnings) {
362
363
  if (!existsSync(handoversDir)) return [];
363
364
  let entries;
@@ -387,7 +388,16 @@ async function listHandovers(handoversDir, root, warnings) {
387
388
  });
388
389
  }
389
390
  }
390
- conforming.sort((a, b) => b.localeCompare(a));
391
+ conforming.sort((a, b) => {
392
+ const dateA = a.slice(0, 10);
393
+ const dateB = b.slice(0, 10);
394
+ if (dateA !== dateB) return dateB.localeCompare(dateA);
395
+ const seqA = a.match(HANDOVER_SEQ_REGEX);
396
+ const seqB = b.match(HANDOVER_SEQ_REGEX);
397
+ if (seqA && !seqB) return -1;
398
+ if (!seqA && seqB) return 1;
399
+ return b.localeCompare(a);
400
+ });
391
401
  return [...conforming, ...nonConforming];
392
402
  }
393
403
  async function readHandover(handoversDir, filename) {
@@ -1974,6 +1984,12 @@ function formatHandoverContent(filename, content, format) {
1974
1984
  }
1975
1985
  return content;
1976
1986
  }
1987
+ function formatHandoverCreateResult(filename, format) {
1988
+ if (format === "json") {
1989
+ return JSON.stringify(successEnvelope({ filename }), null, 2);
1990
+ }
1991
+ return `Created handover: ${filename}`;
1992
+ }
1977
1993
  function formatSnapshotResult(result, format) {
1978
1994
  if (format === "json") {
1979
1995
  return JSON.stringify(successEnvelope(result), null, 2);
@@ -2298,6 +2314,7 @@ export {
2298
2314
  formatError,
2299
2315
  formatExport,
2300
2316
  formatHandoverContent,
2317
+ formatHandoverCreateResult,
2301
2318
  formatHandoverList,
2302
2319
  formatInitResult,
2303
2320
  formatIssue,
package/dist/mcp.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  // src/mcp/index.ts
4
4
  import { realpathSync, existsSync as existsSync5 } from "fs";
5
- import { resolve as resolve6, join as join7, isAbsolute } from "path";
5
+ import { resolve as resolve7, join as join8, isAbsolute } from "path";
6
6
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
7
7
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
8
8
 
@@ -34,7 +34,7 @@ function discoverProjectRoot(startDir) {
34
34
 
35
35
  // src/mcp/tools.ts
36
36
  import { z as z7 } from "zod";
37
- import { join as join6 } from "path";
37
+ import { join as join7 } from "path";
38
38
 
39
39
  // src/core/project-loader.ts
40
40
  import {
@@ -386,6 +386,7 @@ import { readdir, readFile } from "fs/promises";
386
386
  import { existsSync as existsSync2 } from "fs";
387
387
  import { join as join2, relative, extname } from "path";
388
388
  var HANDOVER_DATE_REGEX = /^\d{4}-\d{2}-\d{2}/;
389
+ var HANDOVER_SEQ_REGEX = /^(\d{4}-\d{2}-\d{2})-(\d{2})-/;
389
390
  async function listHandovers(handoversDir, root, warnings) {
390
391
  if (!existsSync2(handoversDir)) return [];
391
392
  let entries;
@@ -415,7 +416,16 @@ async function listHandovers(handoversDir, root, warnings) {
415
416
  });
416
417
  }
417
418
  }
418
- conforming.sort((a, b) => b.localeCompare(a));
419
+ conforming.sort((a, b) => {
420
+ const dateA = a.slice(0, 10);
421
+ const dateB = b.slice(0, 10);
422
+ if (dateA !== dateB) return dateB.localeCompare(dateA);
423
+ const seqA = a.match(HANDOVER_SEQ_REGEX);
424
+ const seqB = b.match(HANDOVER_SEQ_REGEX);
425
+ if (seqA && !seqB) return -1;
426
+ if (!seqA && seqB) return 1;
427
+ return b.localeCompare(a);
428
+ });
419
429
  return [...conforming, ...nonConforming];
420
430
  }
421
431
  async function readHandover(handoversDir, filename) {
@@ -778,6 +788,13 @@ var CliValidationError = class extends Error {
778
788
  this.name = "CliValidationError";
779
789
  }
780
790
  };
791
+ function todayISO() {
792
+ const d = /* @__PURE__ */ new Date();
793
+ const y = d.getFullYear();
794
+ const m = String(d.getMonth() + 1).padStart(2, "0");
795
+ const day = String(d.getDate()).padStart(2, "0");
796
+ return `${y}-${m}-${day}`;
797
+ }
781
798
  async function parseHandoverFilename(raw, handoversDir) {
782
799
  if (raw.includes("/") || raw.includes("\\") || raw.includes("..") || raw.includes("\0")) {
783
800
  throw new CliValidationError(
@@ -1201,6 +1218,12 @@ function formatHandoverContent(filename, content, format) {
1201
1218
  }
1202
1219
  return content;
1203
1220
  }
1221
+ function formatHandoverCreateResult(filename, format) {
1222
+ if (format === "json") {
1223
+ return JSON.stringify(successEnvelope({ filename }), null, 2);
1224
+ }
1225
+ return `Created handover: ${filename}`;
1226
+ }
1204
1227
  function formatSnapshotResult(result, format) {
1205
1228
  if (format === "json") {
1206
1229
  return JSON.stringify(successEnvelope(result), null, 2);
@@ -1740,6 +1763,8 @@ function handleValidate(ctx) {
1740
1763
  }
1741
1764
 
1742
1765
  // src/cli/commands/handover.ts
1766
+ import { mkdir } from "fs/promises";
1767
+ import { join as join4, resolve as resolve4 } from "path";
1743
1768
  function handleHandoverList(ctx) {
1744
1769
  return { output: formatHandoverList(ctx.state.handoverFilenames, ctx.format) };
1745
1770
  }
@@ -1791,6 +1816,59 @@ async function handleHandoverGet(filename, ctx) {
1791
1816
  };
1792
1817
  }
1793
1818
  }
1819
+ function normalizeSlug(raw) {
1820
+ let slug = raw.trim().toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "").replace(/-+/g, "-").replace(/^-|-$/g, "");
1821
+ if (slug.length > 60) slug = slug.slice(0, 60).replace(/-$/, "");
1822
+ if (!slug) {
1823
+ throw new CliValidationError(
1824
+ "invalid_input",
1825
+ `Slug is empty after normalization: "${raw}"`
1826
+ );
1827
+ }
1828
+ return slug;
1829
+ }
1830
+ async function handleHandoverCreate(content, slugRaw, format, root) {
1831
+ if (!content.trim()) {
1832
+ throw new CliValidationError("invalid_input", "Handover content is empty");
1833
+ }
1834
+ const slug = normalizeSlug(slugRaw);
1835
+ const date = todayISO();
1836
+ let filename;
1837
+ await withProjectLock(root, { strict: false }, async () => {
1838
+ const absRoot = resolve4(root);
1839
+ const handoversDir = join4(absRoot, ".story", "handovers");
1840
+ await mkdir(handoversDir, { recursive: true });
1841
+ const wrapDir = join4(absRoot, ".story");
1842
+ const datePrefix = `${date}-`;
1843
+ const seqRegex = new RegExp(`^${date}-(\\d{2})-`);
1844
+ let maxSeq = 0;
1845
+ const { readdirSync } = await import("fs");
1846
+ try {
1847
+ for (const f of readdirSync(handoversDir)) {
1848
+ const m = f.match(seqRegex);
1849
+ if (m) {
1850
+ const n = parseInt(m[1], 10);
1851
+ if (n > maxSeq) maxSeq = n;
1852
+ }
1853
+ }
1854
+ } catch {
1855
+ }
1856
+ const nextSeq = maxSeq + 1;
1857
+ if (nextSeq > 99) {
1858
+ throw new CliValidationError(
1859
+ "conflict",
1860
+ `Too many handovers for ${date}; limit is 99 per day`
1861
+ );
1862
+ }
1863
+ const candidate = `${date}-${String(nextSeq).padStart(2, "0")}-${slug}.md`;
1864
+ const candidatePath = join4(handoversDir, candidate);
1865
+ await parseHandoverFilename(candidate, handoversDir);
1866
+ await guardPath(candidatePath, wrapDir);
1867
+ await atomicWrite(candidatePath, content);
1868
+ filename = candidate;
1869
+ });
1870
+ return { output: formatHandoverCreateResult(filename, format) };
1871
+ }
1794
1872
 
1795
1873
  // src/cli/commands/blocker.ts
1796
1874
  function handleBlockerList(ctx) {
@@ -1883,9 +1961,9 @@ function handleIssueGet(id, ctx) {
1883
1961
  }
1884
1962
 
1885
1963
  // src/core/snapshot.ts
1886
- import { readdir as readdir3, readFile as readFile3, mkdir, unlink as unlink2 } from "fs/promises";
1964
+ import { readdir as readdir3, readFile as readFile3, mkdir as mkdir2, unlink as unlink2 } from "fs/promises";
1887
1965
  import { existsSync as existsSync4 } from "fs";
1888
- import { join as join4, resolve as resolve4 } from "path";
1966
+ import { join as join5, resolve as resolve5 } from "path";
1889
1967
  import { z as z6 } from "zod";
1890
1968
  var LoadWarningSchema = z6.object({
1891
1969
  type: z6.string(),
@@ -1904,9 +1982,9 @@ var SnapshotV1Schema = z6.object({
1904
1982
  });
1905
1983
  var MAX_SNAPSHOTS = 20;
1906
1984
  async function saveSnapshot(root, loadResult) {
1907
- const absRoot = resolve4(root);
1908
- const snapshotsDir = join4(absRoot, ".story", "snapshots");
1909
- await mkdir(snapshotsDir, { recursive: true });
1985
+ const absRoot = resolve5(root);
1986
+ const snapshotsDir = join5(absRoot, ".story", "snapshots");
1987
+ await mkdir2(snapshotsDir, { recursive: true });
1910
1988
  const { state, warnings } = loadResult;
1911
1989
  const now = /* @__PURE__ */ new Date();
1912
1990
  const filename = formatSnapshotFilename(now);
@@ -1927,8 +2005,8 @@ async function saveSnapshot(root, loadResult) {
1927
2005
  } : {}
1928
2006
  };
1929
2007
  const json = JSON.stringify(snapshot, null, 2) + "\n";
1930
- const targetPath = join4(snapshotsDir, filename);
1931
- const wrapDir = join4(absRoot, ".story");
2008
+ const targetPath = join5(snapshotsDir, filename);
2009
+ const wrapDir = join5(absRoot, ".story");
1932
2010
  await guardPath(targetPath, wrapDir);
1933
2011
  await atomicWrite(targetPath, json);
1934
2012
  const pruned = await pruneSnapshots(snapshotsDir);
@@ -1936,13 +2014,13 @@ async function saveSnapshot(root, loadResult) {
1936
2014
  return { filename, retained: entries.length, pruned };
1937
2015
  }
1938
2016
  async function loadLatestSnapshot(root) {
1939
- const snapshotsDir = join4(resolve4(root), ".story", "snapshots");
2017
+ const snapshotsDir = join5(resolve5(root), ".story", "snapshots");
1940
2018
  if (!existsSync4(snapshotsDir)) return null;
1941
2019
  const files = await listSnapshotFiles(snapshotsDir);
1942
2020
  if (files.length === 0) return null;
1943
2021
  for (const filename of files) {
1944
2022
  try {
1945
- const content = await readFile3(join4(snapshotsDir, filename), "utf-8");
2023
+ const content = await readFile3(join5(snapshotsDir, filename), "utf-8");
1946
2024
  const parsed = JSON.parse(content);
1947
2025
  const snapshot = SnapshotV1Schema.parse(parsed);
1948
2026
  return { snapshot, filename };
@@ -2103,7 +2181,7 @@ async function pruneSnapshots(dir) {
2103
2181
  const toRemove = files.slice(MAX_SNAPSHOTS);
2104
2182
  for (const f of toRemove) {
2105
2183
  try {
2106
- await unlink2(join4(dir, f));
2184
+ await unlink2(join5(dir, f));
2107
2185
  } catch {
2108
2186
  }
2109
2187
  }
@@ -2141,7 +2219,7 @@ function handleExport(ctx, mode, phaseId) {
2141
2219
  }
2142
2220
 
2143
2221
  // src/cli/commands/phase.ts
2144
- import { join as join5, resolve as resolve5 } from "path";
2222
+ import { join as join6, resolve as resolve6 } from "path";
2145
2223
  function handlePhaseList(ctx) {
2146
2224
  return { output: formatPhaseList(ctx.state, ctx.format) };
2147
2225
  }
@@ -2200,7 +2278,7 @@ function formatMcpError(code, message) {
2200
2278
  async function runMcpReadTool(pinnedRoot, handler) {
2201
2279
  try {
2202
2280
  const { state, warnings } = await loadProject(pinnedRoot);
2203
- const handoversDir = join6(pinnedRoot, ".story", "handovers");
2281
+ const handoversDir = join7(pinnedRoot, ".story", "handovers");
2204
2282
  const ctx = { state, warnings, root: pinnedRoot, handoversDir, format: "md" };
2205
2283
  const result = await handler(ctx);
2206
2284
  if (result.errorCode && INFRASTRUCTURE_ERROR_CODES.includes(result.errorCode)) {
@@ -2375,12 +2453,30 @@ function registerAllTools(server, pinnedRoot) {
2375
2453
  const phaseId = args.phase ?? null;
2376
2454
  return runMcpReadTool(pinnedRoot, (ctx) => handleExport(ctx, mode, phaseId));
2377
2455
  });
2456
+ server.registerTool("claudestory_handover_create", {
2457
+ description: "Create a handover document from markdown content",
2458
+ inputSchema: {
2459
+ content: z7.string().describe("Markdown content of the handover"),
2460
+ slug: z7.string().optional().describe("Slug for filename (e.g. phase5b-wrapup). Default: session")
2461
+ }
2462
+ }, (args) => {
2463
+ if (!args.content?.trim()) {
2464
+ return Promise.resolve({
2465
+ content: [{ type: "text", text: formatMcpError("invalid_input", "Handover content is empty") }],
2466
+ isError: true
2467
+ });
2468
+ }
2469
+ return runMcpWriteTool(
2470
+ pinnedRoot,
2471
+ (root) => handleHandoverCreate(args.content, args.slug ?? "session", "md", root)
2472
+ );
2473
+ });
2378
2474
  }
2379
2475
 
2380
2476
  // src/mcp/index.ts
2381
2477
  var ENV_VAR2 = "CLAUDESTORY_PROJECT_ROOT";
2382
2478
  var CONFIG_PATH2 = ".story/config.json";
2383
- var version = "0.1.4";
2479
+ var version = "0.1.5";
2384
2480
  function pinProjectRoot() {
2385
2481
  const envRoot = process.env[ENV_VAR2];
2386
2482
  if (envRoot) {
@@ -2389,7 +2485,7 @@ function pinProjectRoot() {
2389
2485
  `);
2390
2486
  process.exit(1);
2391
2487
  }
2392
- const resolved = resolve6(envRoot);
2488
+ const resolved = resolve7(envRoot);
2393
2489
  let canonical;
2394
2490
  try {
2395
2491
  canonical = realpathSync(resolved);
@@ -2398,7 +2494,7 @@ function pinProjectRoot() {
2398
2494
  `);
2399
2495
  process.exit(1);
2400
2496
  }
2401
- if (!existsSync5(join7(canonical, CONFIG_PATH2))) {
2497
+ if (!existsSync5(join8(canonical, CONFIG_PATH2))) {
2402
2498
  process.stderr.write(`Error: No .story/config.json at ${canonical}
2403
2499
  `);
2404
2500
  process.exit(1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anthropologies/claudestory",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {