@pebblehouse/odin-cli 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +103 -23
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -311,18 +311,99 @@ decisionsCmd.command("get <slug> <id>").description("Get a decision by ID").acti
311
311
  process.stdout.write(JSON.stringify(res) + "\n");
312
312
  if (res.error) process.exit(1);
313
313
  });
314
- decisionsCmd.command("log <slug> <title>").description("Log a decision").option("--rationale <rationale>", "Decision rationale").option("--alternatives <alternatives>", "Alternatives considered").option("--session-id <id>", "Link to active session").action(async (slug, title, opts) => {
314
+ async function detectSupersedes(newTitle, newRationale, recentDecisions) {
315
+ const apiKey = process.env.ANTHROPIC_API_KEY;
316
+ if (!apiKey || recentDecisions.length === 0) return [];
317
+ const decisionList = recentDecisions.map((d) => `- ID: ${d.id} | Title: "${d.title}" | Rationale: "${d.rationale ?? "none"}"`).join("\n");
318
+ const prompt = `You are an architectural decision tracker. A new decision is being logged:
319
+
320
+ Title: "${newTitle}"
321
+ Rationale: "${newRationale ?? "none"}"
322
+
323
+ Here are recent active decisions (last 3 days):
324
+ ${decisionList}
325
+
326
+ Which of these recent decisions, if any, does the new decision SUPERSEDE (i.e., replace, override, or contradict)?
327
+
328
+ Respond with ONLY a JSON array of IDs that are superseded. If none are superseded, respond with [].
329
+ Example: ["uuid-1", "uuid-2"] or []`;
330
+ try {
331
+ const res = await fetch("https://api.anthropic.com/v1/messages", {
332
+ method: "POST",
333
+ headers: {
334
+ "x-api-key": apiKey,
335
+ "anthropic-version": "2023-06-01",
336
+ "content-type": "application/json"
337
+ },
338
+ body: JSON.stringify({
339
+ model: "claude-haiku-4-5-20251001",
340
+ max_tokens: 256,
341
+ messages: [{ role: "user", content: prompt }]
342
+ })
343
+ });
344
+ if (!res.ok) return [];
345
+ const data = await res.json();
346
+ const text = data?.content?.[0]?.text?.trim() ?? "[]";
347
+ const match = text.match(/\[.*\]/s);
348
+ if (!match) return [];
349
+ const ids = JSON.parse(match[0]);
350
+ const validIds = new Set(recentDecisions.map((d) => d.id));
351
+ return ids.filter((id) => validIds.has(id));
352
+ } catch {
353
+ return [];
354
+ }
355
+ }
356
+ decisionsCmd.command("log <slug> <title>").description("Log a decision").option("--rationale <rationale>", "Decision rationale").option("--alternatives <alternatives>", "Alternatives considered").option("--no-auto-supersede", "Skip automatic supersede detection").action(async (slug, title, opts) => {
315
357
  const res = await apiRequest(`/pebbles/${slug}/decisions`, {
316
358
  method: "POST",
317
359
  body: {
318
360
  title,
319
361
  rationale: opts.rationale,
320
- alternatives_considered: opts.alternatives,
321
- session_id: opts.sessionId
362
+ alternatives_considered: opts.alternatives
322
363
  }
323
364
  });
324
- process.stdout.write(JSON.stringify(res) + "\n");
325
- if (res.error) process.exit(1);
365
+ if (res.error || !res.data) {
366
+ process.stdout.write(JSON.stringify(res) + "\n");
367
+ if (res.error) process.exit(1);
368
+ return;
369
+ }
370
+ const newDecision = res.data;
371
+ if (opts.autoSupersede) {
372
+ const threeDaysAgo = /* @__PURE__ */ new Date();
373
+ threeDaysAgo.setDate(threeDaysAgo.getDate() - 3);
374
+ const recentRes = await apiRequest(`/pebbles/${slug}/decisions`, {
375
+ params: { limit: "100" }
376
+ });
377
+ if (recentRes.data) {
378
+ const recentActive = recentRes.data.filter(
379
+ (d) => d.id !== newDecision.id && d.status === "active" && new Date(d.created_at) >= threeDaysAgo
380
+ );
381
+ const supersededIds = await detectSupersedes(title, opts.rationale, recentActive);
382
+ if (supersededIds.length > 0) {
383
+ const updateRes = await apiRequest(`/pebbles/${slug}/decisions/${newDecision.id}`, {
384
+ method: "PUT",
385
+ body: { supersedes: supersededIds }
386
+ });
387
+ for (const id of supersededIds) {
388
+ await apiRequest(`/pebbles/${slug}/decisions/${id}`, {
389
+ method: "PUT",
390
+ body: { status: "superseded" }
391
+ });
392
+ }
393
+ if (updateRes.data) {
394
+ const supersededTitles = recentActive.filter((d) => supersededIds.includes(d.id)).map((d) => d.title);
395
+ process.stderr.write(
396
+ `\u26A1 Auto-superseded ${supersededIds.length} decision(s):
397
+ ${supersededTitles.map((t) => ` \u21B3 "${t}"`).join("\n")}
398
+ `
399
+ );
400
+ process.stdout.write(JSON.stringify({ data: updateRes.data, error: null }) + "\n");
401
+ return;
402
+ }
403
+ }
404
+ }
405
+ }
406
+ process.stdout.write(JSON.stringify({ data: newDecision, error: null }) + "\n");
326
407
  });
327
408
 
328
409
  // src/commands/context.ts
@@ -428,7 +509,7 @@ plansCmd.command("delete-phase <slug> <planId> <phaseId>").description("Delete a
428
509
  process.stdout.write(JSON.stringify(res) + "\n");
429
510
  if (res.error) process.exit(1);
430
511
  });
431
- plansCmd.command("add-phase-link <slug> <planId> <phaseId>").description("Add a link from a phase to a spec or document").requiredOption("--link-type <type>", "Link type (spec|plan_doc)").requiredOption("--target-type <type>", "Target type (agent_spec|document)").requiredOption("--target-id <id>", "Target entity UUID").action(async (slug, planId, phaseId, opts) => {
512
+ plansCmd.command("add-phase-link <slug> <planId> <phaseId>").description("Add a link from a phase to a spec or document").requiredOption("--link-type <type>", "Link type (spec|plan_doc)").requiredOption("--target-type <type>", "Target type (artifact|document)").requiredOption("--target-id <id>", "Target entity UUID").action(async (slug, planId, phaseId, opts) => {
432
513
  const res = await apiRequest(`/pebbles/${slug}/plans/${planId}/phases/${phaseId}/links`, {
433
514
  method: "POST",
434
515
  body: {
@@ -448,53 +529,52 @@ plansCmd.command("remove-phase-link <slug> <planId> <phaseId> <linkId>").descrip
448
529
  if (res.error) process.exit(1);
449
530
  });
450
531
 
451
- // src/commands/specs.ts
532
+ // src/commands/artifacts.ts
452
533
  import { Command as Command7 } from "commander";
453
534
  import { readFileSync as readFileSync5 } from "fs";
454
- var specsCmd = new Command7("specs").description("Manage agent specs");
455
- specsCmd.command("list <slug>").description("List specs for a pebble").option("--source <source>", "Filter by source (claude-code, superpowers, etc.)").action(async (slug, opts) => {
535
+ var artifactsCmd = new Command7("artifacts").description("Manage artifacts");
536
+ artifactsCmd.command("list <slug>").description("List artifacts for a pebble").option("--source <source>", "Filter by source (claude-code, superpowers, etc.)").action(async (slug, opts) => {
456
537
  const params = {};
457
538
  if (opts.source) params.source = opts.source;
458
- const res = await apiRequest(`/pebbles/${slug}/agent-specs`, { params });
539
+ const res = await apiRequest(`/pebbles/${slug}/artifacts`, { params });
459
540
  process.stdout.write(JSON.stringify(res) + "\n");
460
541
  if (res.error) process.exit(1);
461
542
  });
462
- specsCmd.command("get <slug> <id>").description("Get a spec by ID").action(async (slug, id) => {
463
- const res = await apiRequest(`/pebbles/${slug}/agent-specs/${id}`);
543
+ artifactsCmd.command("get <slug> <id>").description("Get an artifact by ID").action(async (slug, id) => {
544
+ const res = await apiRequest(`/pebbles/${slug}/artifacts/${id}`);
464
545
  process.stdout.write(JSON.stringify(res) + "\n");
465
546
  if (res.error) process.exit(1);
466
547
  });
467
- specsCmd.command("create <slug> <title>").description("Create a spec").requiredOption("--spec-type <type>", "Spec type (e.g. brainstorm, plan, research)").option("--source <source>", "Source agent", "claude-code").option("--content <content>", "Spec content").option("--file <path>", "Read content from file").option("--session-id <id>", "Link to active session").action(async (slug, title, opts) => {
548
+ artifactsCmd.command("create <slug> <title>").description("Create an artifact").requiredOption("--type <type>", "Artifact type (e.g. brainstorm, plan, research, spec)").option("--source <source>", "Source agent", "claude-code").option("--content <content>", "Artifact content").option("--file <path>", "Read content from file").action(async (slug, title, opts) => {
468
549
  const content = opts.file ? readFileSync5(opts.file, "utf-8") : opts.content ?? "";
469
- const res = await apiRequest(`/pebbles/${slug}/agent-specs`, {
550
+ const res = await apiRequest(`/pebbles/${slug}/artifacts`, {
470
551
  method: "POST",
471
552
  body: {
472
553
  title,
473
- spec_type: opts.specType,
554
+ type: opts.type,
474
555
  source: opts.source,
475
- content,
476
- session_id: opts.sessionId ?? null
556
+ content
477
557
  }
478
558
  });
479
559
  process.stdout.write(JSON.stringify(res) + "\n");
480
560
  if (res.error) process.exit(1);
481
561
  });
482
- specsCmd.command("update <slug> <id>").description("Update a spec").option("--title <title>", "New title").option("--spec-type <type>", "New spec type").option("--source <source>", "New source").option("--content <content>", "New content").option("--file <path>", "Read content from file").action(async (slug, id, opts) => {
562
+ artifactsCmd.command("update <slug> <id>").description("Update an artifact").option("--title <title>", "New title").option("--type <type>", "New type").option("--source <source>", "New source").option("--content <content>", "New content").option("--file <path>", "Read content from file").action(async (slug, id, opts) => {
483
563
  const body = {};
484
564
  if (opts.title) body.title = opts.title;
485
- if (opts.specType) body.spec_type = opts.specType;
565
+ if (opts.type) body.type = opts.type;
486
566
  if (opts.source) body.source = opts.source;
487
567
  if (opts.file) body.content = readFileSync5(opts.file, "utf-8");
488
568
  else if (opts.content) body.content = opts.content;
489
- const res = await apiRequest(`/pebbles/${slug}/agent-specs/${id}`, {
569
+ const res = await apiRequest(`/pebbles/${slug}/artifacts/${id}`, {
490
570
  method: "PUT",
491
571
  body
492
572
  });
493
573
  process.stdout.write(JSON.stringify(res) + "\n");
494
574
  if (res.error) process.exit(1);
495
575
  });
496
- specsCmd.command("delete <slug> <id>").description("Delete a spec").action(async (slug, id) => {
497
- const res = await apiRequest(`/pebbles/${slug}/agent-specs/${id}`, {
576
+ artifactsCmd.command("delete <slug> <id>").description("Delete an artifact").action(async (slug, id) => {
577
+ const res = await apiRequest(`/pebbles/${slug}/artifacts/${id}`, {
498
578
  method: "DELETE"
499
579
  });
500
580
  process.stdout.write(JSON.stringify(res) + "\n");
@@ -624,7 +704,7 @@ program.addCommand(decisionsCmd);
624
704
  program.addCommand(contextCmd);
625
705
  program.addCommand(searchCmd);
626
706
  program.addCommand(plansCmd);
627
- program.addCommand(specsCmd);
707
+ program.addCommand(artifactsCmd);
628
708
  program.addCommand(versionsCmd);
629
709
  program.addCommand(guidelinesCmd);
630
710
  process.on("exit", () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pebblehouse/odin-cli",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "type": "module",
5
5
  "description": "CLI for Odin — the knowledge backbone for Pebble House",
6
6
  "bin": {