@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 +155 -8
- package/dist/index.d.ts +2 -1
- package/dist/index.js +18 -1
- package/dist/mcp.js +114 -18
- package/package.json +1 -1
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) =>
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
).
|
|
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.
|
|
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) =>
|
|
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
|
|
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
|
|
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) =>
|
|
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
|
|
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 =
|
|
1908
|
-
const snapshotsDir =
|
|
1909
|
-
await
|
|
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 =
|
|
1931
|
-
const wrapDir =
|
|
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 =
|
|
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(
|
|
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(
|
|
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
|
|
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 =
|
|
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.
|
|
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 =
|
|
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(
|
|
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);
|