codecartographer-pi 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -13,9 +13,9 @@
13
13
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
14
14
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
15
15
  import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from "@modelcontextprotocol/sdk/types.js";
16
- import { cp, mkdir, rm, writeFile } from "node:fs/promises";
16
+ import { cp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
17
17
  import { basename, isAbsolute, join } from "node:path";
18
- import { buildPhasePrompt, buildSkillPrompt, buildThreadLogEntry, buildValidationSummary, canonicalPath, closeoutFileName, createEmptyStatus, dateOnly, DEFAULT_PIPELINE_PATH, ensureCloseoutStub, getNextEligiblePhase, getPipelineLabel, getWorkspaceState, listSkillNames, loadYamlFile, normalizeForComparison, normalizeStatus, packagedWorkspaceDir, pathExists, PIPELINE_ALIASES, resolvePhase, resolvePipelineChoice, stringifySimpleYaml, uniqueStrings, updateStatusAtomically, validatePhaseOutput, } from "../core/index.js";
18
+ import { buildPhasePrompt, buildSkillPrompt, buildThreadLogEntry, buildValidationSummary, canonicalPath, closeoutFileName, createEmptyStatus, dateOnly, DEFAULT_PIPELINE_PATH, deriveSlug, discoverLibrary, ensureCloseoutStub, getNextEligiblePhase, getPipelineLabel, getWorkspaceState, isValidSlug, listEntries, listSkillNames, loadCodecartoConfig, loadYamlFile, normalizeForComparison, normalizeStatus, PACKAGE_VERSION, packagedWorkspaceDir, pathExists, publishEntry, reindex as libraryReindex, resolvePhase, resolvePipelineChoice, stringifySimpleYaml, uniqueStrings, updateStatusAtomically, validatePhaseOutput, } from "../core/index.js";
19
19
  // ---------- input helpers ----------
20
20
  async function validateCwd(cwd) {
21
21
  if (typeof cwd !== "string" || !cwd.trim()) {
@@ -268,6 +268,241 @@ export async function handleSkill(args) {
268
268
  const prompt = await buildSkillPrompt(state, args.name);
269
269
  return textResult(prompt, { skill: args.name });
270
270
  }
271
+ // ---------- library helpers ----------
272
+ async function resolveLibraryPath(args) {
273
+ const explicit = typeof args.library_path === "string" && args.library_path.trim() !== ""
274
+ ? args.library_path.trim()
275
+ : null;
276
+ if (explicit) {
277
+ if (!isAbsolute(explicit)) {
278
+ throw new McpError(ErrorCode.InvalidParams, `library_path must be absolute, got: ${explicit}`);
279
+ }
280
+ return explicit;
281
+ }
282
+ if (typeof args.cwd === "string" && args.cwd.trim() !== "") {
283
+ const cwd = args.cwd.trim();
284
+ if (!isAbsolute(cwd)) {
285
+ throw new McpError(ErrorCode.InvalidParams, `cwd must be absolute, got: ${cwd}`);
286
+ }
287
+ // loadCodecartoConfig merges user-global under per-workspace and tolerates
288
+ // a missing workspace file, so a single call covers both cases.
289
+ const workspaceDir = join(cwd, ".codecarto");
290
+ const config = await loadCodecartoConfig(workspaceDir);
291
+ if (config.library.path)
292
+ return config.library.path;
293
+ }
294
+ throw new McpError(ErrorCode.InvalidParams, "library_path is required (pass it explicitly, or pass cwd and configure library.path in ~/.codecarto/config.yaml or .codecarto/workflow/config.yaml).");
295
+ }
296
+ function asStringArray(value, fieldName) {
297
+ if (!Array.isArray(value)) {
298
+ throw new McpError(ErrorCode.InvalidParams, `${fieldName} must be an array of strings`);
299
+ }
300
+ const out = [];
301
+ for (const v of value) {
302
+ if (typeof v !== "string") {
303
+ throw new McpError(ErrorCode.InvalidParams, `${fieldName} must contain only strings`);
304
+ }
305
+ out.push(v);
306
+ }
307
+ return out;
308
+ }
309
+ const ALLOWED_REASONING = ["high", "medium", "low", "default", "unknown"];
310
+ function buildGenerationFromArg(model_metadata) {
311
+ const surface = "mcp-server";
312
+ const defaults = {
313
+ surface,
314
+ agent: "unknown",
315
+ agent_version: "unknown",
316
+ model: "unknown",
317
+ model_vendor: "unknown",
318
+ reasoning: "unknown",
319
+ notes: "",
320
+ };
321
+ if (model_metadata === undefined || model_metadata === null)
322
+ return defaults;
323
+ if (typeof model_metadata !== "object") {
324
+ throw new McpError(ErrorCode.InvalidParams, "model_metadata must be an object");
325
+ }
326
+ const m = model_metadata;
327
+ const out = { ...defaults };
328
+ if (typeof m.agent === "string")
329
+ out.agent = m.agent;
330
+ if (typeof m.agent_version === "string")
331
+ out.agent_version = m.agent_version;
332
+ if (typeof m.model === "string")
333
+ out.model = m.model;
334
+ if (typeof m.model_vendor === "string")
335
+ out.model_vendor = m.model_vendor;
336
+ if (typeof m.reasoning === "string" && ALLOWED_REASONING.includes(m.reasoning)) {
337
+ out.reasoning = m.reasoning;
338
+ }
339
+ if (typeof m.notes === "string")
340
+ out.notes = m.notes;
341
+ return out;
342
+ }
343
+ async function readSpecArg(args) {
344
+ if (typeof args.spec === "string" && args.spec.length > 0)
345
+ return args.spec;
346
+ if (typeof args.spec_path === "string" && args.spec_path.length > 0) {
347
+ if (!isAbsolute(args.spec_path)) {
348
+ throw new McpError(ErrorCode.InvalidParams, `spec_path must be absolute, got: ${args.spec_path}`);
349
+ }
350
+ if (!(await pathExists(args.spec_path))) {
351
+ throw new McpError(ErrorCode.InvalidParams, `spec_path does not exist: ${args.spec_path}`);
352
+ }
353
+ return readFile(args.spec_path, "utf8");
354
+ }
355
+ throw new McpError(ErrorCode.InvalidParams, "Either spec (inline content) or spec_path (absolute file path) is required");
356
+ }
357
+ async function resolveDefaultsFromWorkspace(cwd, overrides) {
358
+ let pipeline = typeof overrides.pipeline === "string" && overrides.pipeline.trim() !== ""
359
+ ? overrides.pipeline.trim()
360
+ : "unknown";
361
+ let namespace = typeof overrides.namespace === "string" && overrides.namespace.trim() !== ""
362
+ ? overrides.namespace.trim()
363
+ : null;
364
+ if (typeof cwd === "string" && cwd.trim() !== "" && isAbsolute(cwd)) {
365
+ const workspaceDir = join(cwd.trim(), ".codecarto");
366
+ if (await pathExists(workspaceDir)) {
367
+ if (pipeline === "unknown") {
368
+ try {
369
+ const state = await getWorkspaceState(cwd.trim());
370
+ if (state?.status.pipeline)
371
+ pipeline = state.status.pipeline;
372
+ }
373
+ catch {
374
+ // ignore — pipeline stays "unknown"
375
+ }
376
+ }
377
+ if (!namespace) {
378
+ const config = await loadCodecartoConfig(workspaceDir);
379
+ if (config.library.namespace)
380
+ namespace = config.library.namespace;
381
+ }
382
+ }
383
+ }
384
+ return { pipeline, namespace };
385
+ }
386
+ // ---------- library handlers ----------
387
+ export async function handlePublish(args) {
388
+ const libraryPath = await resolveLibraryPath(args);
389
+ const marker = await discoverLibrary(libraryPath);
390
+ if (!marker) {
391
+ throw new McpError(ErrorCode.InvalidParams, `No CodeCartographer library at ${libraryPath} (missing .codecarto-library marker). Create one before publishing.`);
392
+ }
393
+ const spec = await readSpecArg(args);
394
+ if (typeof args.source_repo !== "string" || args.source_repo.trim() === "") {
395
+ throw new McpError(ErrorCode.InvalidParams, "source_repo is required");
396
+ }
397
+ if (typeof args.headline !== "string" || args.headline.trim() === "") {
398
+ throw new McpError(ErrorCode.InvalidParams, "headline is required");
399
+ }
400
+ const tags = asStringArray(args.tags ?? [], "tags");
401
+ const capabilities = asStringArray(args.capabilities ?? [], "capabilities");
402
+ const sourceRepo = args.source_repo.trim();
403
+ const slugInput = typeof args.slug === "string" && args.slug.trim() !== "" ? args.slug.trim() : null;
404
+ const slug = slugInput ?? deriveSlug(sourceRepo);
405
+ if (!isValidSlug(slug)) {
406
+ throw new McpError(ErrorCode.InvalidParams, `Resolved slug "${slug}" is invalid. Provide an explicit slug (lowercase ASCII, starts with a letter, max 64 chars).`);
407
+ }
408
+ const defaults = await resolveDefaultsFromWorkspace(args.cwd, {
409
+ pipeline: args.pipeline,
410
+ namespace: args.namespace,
411
+ });
412
+ const namespace = marker.namespaced
413
+ ? (typeof args.namespace === "string" && args.namespace.trim() !== ""
414
+ ? args.namespace.trim()
415
+ : defaults.namespace ?? undefined)
416
+ : undefined;
417
+ if (marker.namespaced && !namespace) {
418
+ throw new McpError(ErrorCode.InvalidParams, "Library is namespaced — namespace argument is required (or set library.namespace in config.yaml).");
419
+ }
420
+ const generation = buildGenerationFromArg(args.model_metadata);
421
+ const confidentiality = args.confidentiality === "internal" || args.confidentiality === "shared" || args.confidentiality === "public"
422
+ ? args.confidentiality
423
+ : undefined;
424
+ const analyzedAt = typeof args.analyzed_at === "string" && args.analyzed_at !== ""
425
+ ? args.analyzed_at
426
+ : new Date().toISOString();
427
+ const result = await publishEntry(libraryPath, spec, {
428
+ slug,
429
+ namespace: namespace ?? undefined,
430
+ source_repo: sourceRepo,
431
+ source_commit: typeof args.source_commit === "string" ? args.source_commit : undefined,
432
+ source_branch: typeof args.source_branch === "string" ? args.source_branch : undefined,
433
+ source_dirty: typeof args.source_dirty === "boolean" ? args.source_dirty : undefined,
434
+ analyzed_at: analyzedAt,
435
+ pipeline: defaults.pipeline,
436
+ codecarto_version: PACKAGE_VERSION,
437
+ headline: args.headline.trim(),
438
+ tags,
439
+ capabilities,
440
+ confidentiality,
441
+ generation,
442
+ }, { forceNewVersion: args.force_new_version === true });
443
+ const lines = [
444
+ `Published ${result.namespace ? `${result.namespace}/` : ""}${result.slug} v${result.version} to ${libraryPath}`,
445
+ result.isNewVersion ? `New version: v${result.version}` : `Metadata-only update (content hash matched v${result.version}).`,
446
+ `Entry directory: ${result.versionDir}`,
447
+ ];
448
+ return textResult(lines.join("\n"), {
449
+ libraryPath,
450
+ slug: result.slug,
451
+ namespace: result.namespace ?? null,
452
+ version: result.version,
453
+ isNewVersion: result.isNewVersion,
454
+ versionDir: result.versionDir,
455
+ });
456
+ }
457
+ export async function handleLibraryList(args) {
458
+ const libraryPath = await resolveLibraryPath(args);
459
+ const marker = await discoverLibrary(libraryPath);
460
+ if (!marker) {
461
+ throw new McpError(ErrorCode.InvalidParams, `No CodeCartographer library at ${libraryPath} (missing .codecarto-library marker).`);
462
+ }
463
+ const filter = {};
464
+ if (typeof args.namespace === "string" && args.namespace !== "")
465
+ filter.namespace = args.namespace;
466
+ if (typeof args.tag === "string" && args.tag !== "")
467
+ filter.tag = args.tag;
468
+ if (typeof args.slug === "string" && args.slug !== "")
469
+ filter.slug = args.slug;
470
+ if (typeof args.source_repo === "string" && args.source_repo !== "")
471
+ filter.source_repo = args.source_repo;
472
+ const entries = await listEntries(libraryPath, filter);
473
+ const summary = entries.length === 0
474
+ ? `No entries match the filter in ${libraryPath}.`
475
+ : [
476
+ `${entries.length} ${entries.length === 1 ? "entry" : "entries"} in ${libraryPath}:`,
477
+ ...entries.map((e) => {
478
+ const ns = e.namespace ? `${e.namespace}/` : "";
479
+ const tags = e.tags.length > 0 ? ` [${e.tags.slice(0, 4).join(", ")}${e.tags.length > 4 ? ", ..." : ""}]` : "";
480
+ return ` ${ns}${e.slug} v${e.latest_version} — ${e.headline}${tags}`;
481
+ }),
482
+ ].join("\n");
483
+ return textResult(summary, {
484
+ libraryPath,
485
+ libraryName: marker.name,
486
+ namespaced: marker.namespaced,
487
+ count: entries.length,
488
+ entries,
489
+ });
490
+ }
491
+ export async function handleLibraryReindex(args) {
492
+ const libraryPath = await resolveLibraryPath(args);
493
+ const marker = await discoverLibrary(libraryPath);
494
+ if (!marker) {
495
+ throw new McpError(ErrorCode.InvalidParams, `No CodeCartographer library at ${libraryPath} (missing .codecarto-library marker).`);
496
+ }
497
+ const index = await libraryReindex(libraryPath);
498
+ const namespaces = index.namespaces.length > 0 ? index.namespaces.join(", ") : "(none)";
499
+ return textResult(`Reindexed ${libraryPath}: ${index.entry_count} ${index.entry_count === 1 ? "entry" : "entries"} across namespaces [${namespaces}].`, {
500
+ libraryPath,
501
+ libraryName: index.library_name,
502
+ entry_count: index.entry_count,
503
+ namespaces: index.namespaces,
504
+ });
505
+ }
271
506
  // ---------- tool registry ----------
272
507
  const TOOLS = [
273
508
  {
@@ -279,7 +514,7 @@ const TOOLS = [
279
514
  cwd: { type: "string", description: "Absolute path to the target repository." },
280
515
  pipeline: {
281
516
  type: "string",
282
- description: `Pipeline alias (one of ${Object.keys(PIPELINE_ALIASES).join(", ")}) or workflow/*.yaml path. Defaults to the framework's default pipeline.`,
517
+ description: `Pipeline alias or workflow/*.yaml path. Defaults to the framework's default pipeline.`,
283
518
  },
284
519
  force: {
285
520
  type: "boolean",
@@ -355,6 +590,70 @@ const TOOLS = [
355
590
  required: ["cwd", "name"],
356
591
  },
357
592
  },
593
+ {
594
+ name: "codecarto_publish",
595
+ description: "Publish a reimplementation-spec to a CodeCartographer library. Identified by library_path (absolute) or cwd's config.yaml. Content-hash idempotent — re-publishing identical spec bytes updates metadata in place rather than bumping the version. Required: source_repo, headline, and either spec (inline) or spec_path (absolute file). Slug derives from source_repo if not provided. If the library is namespaced, namespace is required (or pass cwd to inherit from config). Generation context (agent, model, vendor, reasoning) is passed via model_metadata so the host can record provenance; omitted fields default to 'unknown'.",
596
+ inputSchema: {
597
+ type: "object",
598
+ properties: {
599
+ library_path: { type: "string", description: "Absolute path to the library directory." },
600
+ cwd: { type: "string", description: "Absolute path to a workspace. Used to read defaults from config.yaml and status.yaml." },
601
+ spec: { type: "string", description: "Inline spec markdown content (mutually exclusive with spec_path)." },
602
+ spec_path: { type: "string", description: "Absolute path to a file containing the spec markdown (mutually exclusive with spec)." },
603
+ slug: { type: "string", description: "Entry slug. Derived from source_repo if omitted." },
604
+ namespace: { type: "string", description: "Namespace under entries/. Required for namespaced libraries." },
605
+ source_repo: { type: "string", description: "URL or path to the analyzed repository." },
606
+ source_commit: { type: "string" },
607
+ source_branch: { type: "string" },
608
+ source_dirty: { type: "boolean" },
609
+ analyzed_at: { type: "string", description: "ISO 8601 UTC timestamp. Defaults to now." },
610
+ pipeline: { type: "string", description: "Pipeline used. Inherited from cwd's status.yaml if available." },
611
+ headline: { type: "string" },
612
+ tags: { type: "array", items: { type: "string" } },
613
+ capabilities: { type: "array", items: { type: "string" } },
614
+ confidentiality: { type: "string", enum: ["internal", "shared", "public"] },
615
+ model_metadata: {
616
+ type: "object",
617
+ properties: {
618
+ agent: { type: "string" },
619
+ agent_version: { type: "string" },
620
+ model: { type: "string" },
621
+ model_vendor: { type: "string" },
622
+ reasoning: { type: "string", enum: ["high", "medium", "low", "default", "unknown"] },
623
+ notes: { type: "string" },
624
+ },
625
+ },
626
+ force_new_version: { type: "boolean" },
627
+ },
628
+ required: ["source_repo", "headline"],
629
+ },
630
+ },
631
+ {
632
+ name: "codecarto_library_list",
633
+ description: "List entries in a CodeCartographer library, optionally filtered by namespace, tag, slug, or source_repo. The library is identified by library_path (absolute) or by cwd's config.yaml.",
634
+ inputSchema: {
635
+ type: "object",
636
+ properties: {
637
+ library_path: { type: "string" },
638
+ cwd: { type: "string" },
639
+ namespace: { type: "string" },
640
+ tag: { type: "string" },
641
+ slug: { type: "string" },
642
+ source_repo: { type: "string" },
643
+ },
644
+ },
645
+ },
646
+ {
647
+ name: "codecarto_library_reindex",
648
+ description: "Regenerate index.yaml and INDEX.md for a CodeCartographer library from filesystem state. Use after manual edits or to resolve a git merge conflict on index.yaml.",
649
+ inputSchema: {
650
+ type: "object",
651
+ properties: {
652
+ library_path: { type: "string" },
653
+ cwd: { type: "string" },
654
+ },
655
+ },
656
+ },
358
657
  ];
359
658
  const HANDLERS = {
360
659
  codecarto_init: handleInit,
@@ -364,6 +663,9 @@ const HANDLERS = {
364
663
  codecarto_validate: handleValidate,
365
664
  codecarto_complete: handleComplete,
366
665
  codecarto_skill: handleSkill,
666
+ codecarto_publish: handlePublish,
667
+ codecarto_library_list: handleLibraryList,
668
+ codecarto_library_reindex: handleLibraryReindex,
367
669
  };
368
670
  // ---------- server bootstrap ----------
369
671
  export function buildServer() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codecartographer-pi",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "CodeCartographer packaged for Pi as an extension-driven workflow wrapper.",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -26,11 +26,12 @@
26
26
  "files": [
27
27
  ".codecarto/**/*",
28
28
  "dist/**/*",
29
+ "assets/logo.svg",
29
30
  "README.md",
30
31
  "LICENSE"
31
32
  ],
32
33
  "bin": {
33
- "codecarto-mcp": "./dist/mcp-server/bin.mjs"
34
+ "codecarto-mcp": "dist/mcp-server/bin.mjs"
34
35
  },
35
36
  "scripts": {
36
37
  "build": "tsc",