@pebblehouse/odin-cli 0.6.0 → 0.8.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 +148 -7
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { Command as Command10 } from "commander";
4
+ import { Command as Command11 } from "commander";
5
5
 
6
6
  // src/auth/login.ts
7
7
  import { createServer } from "http";
@@ -311,7 +311,49 @@ 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").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: {
@@ -320,8 +362,48 @@ decisionsCmd.command("log <slug> <title>").description("Log a decision").option(
320
362
  alternatives_considered: opts.alternatives
321
363
  }
322
364
  });
323
- process.stdout.write(JSON.stringify(res) + "\n");
324
- 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");
325
407
  });
326
408
 
327
409
  // src/commands/context.ts
@@ -577,12 +659,70 @@ guidelinesCmd.command("deprecate <id>").description("Deprecate a guideline").act
577
659
  if (res.error) process.exit(1);
578
660
  });
579
661
 
580
- // src/index.ts
662
+ // src/commands/rules.ts
663
+ import { Command as Command10 } from "commander";
581
664
  import { readFileSync as readFileSync7 } from "fs";
665
+ var rulesCmd = new Command10("rules").description("Manage pebble rules");
666
+ rulesCmd.command("list <slug>").description("List rules for a pebble").option("--category <category>", "Filter by category").option("--status <status>", "Filter by status (default: active)").option("--all", "Include deprecated rules").action(async (slug, opts) => {
667
+ const params = {};
668
+ if (opts.category) params.category = opts.category;
669
+ if (opts.all) params.status = "all";
670
+ else if (opts.status) params.status = opts.status;
671
+ const res = await apiRequest(`/pebbles/${slug}/rules`, { params });
672
+ process.stdout.write(JSON.stringify(res) + "\n");
673
+ if (res.error) process.exit(1);
674
+ });
675
+ rulesCmd.command("get <slug> <id>").description("Get a rule by ID").action(async (slug, id) => {
676
+ const res = await apiRequest(`/pebbles/${slug}/rules/${id}`);
677
+ process.stdout.write(JSON.stringify(res) + "\n");
678
+ if (res.error) process.exit(1);
679
+ });
680
+ rulesCmd.command("create <slug> <title>").description("Create a rule").option("--category <category>", "Category", "other").option("--content <content>", "Rule content (max 500 chars)").option("--file <path>", "Read content from file").option("--source <source>", "Source", "manual").action(async (slug, title, opts) => {
681
+ const content = opts.file ? readFileSync7(opts.file, "utf-8") : opts.content ?? "";
682
+ const res = await apiRequest(`/pebbles/${slug}/rules`, {
683
+ method: "POST",
684
+ body: { category: opts.category, title, content, source: opts.source }
685
+ });
686
+ process.stdout.write(JSON.stringify(res) + "\n");
687
+ if (res.error) process.exit(1);
688
+ });
689
+ rulesCmd.command("update <slug> <id>").description("Update a rule").option("--title <title>", "New title").option("--content <content>", "New content (max 500 chars)").option("--file <path>", "Read content from file").option("--category <category>", "New category").option("--status <status>", "New status (active|deprecated)").option("--source <source>", "Source").action(async (slug, id, opts) => {
690
+ const body = {};
691
+ if (opts.title) body.title = opts.title;
692
+ if (opts.file) body.content = readFileSync7(opts.file, "utf-8");
693
+ else if (opts.content) body.content = opts.content;
694
+ if (opts.category) body.category = opts.category;
695
+ if (opts.status) body.status = opts.status;
696
+ if (opts.source) body.source = opts.source;
697
+ const res = await apiRequest(`/pebbles/${slug}/rules/${id}`, {
698
+ method: "PUT",
699
+ body
700
+ });
701
+ process.stdout.write(JSON.stringify(res) + "\n");
702
+ if (res.error) process.exit(1);
703
+ });
704
+ rulesCmd.command("delete <slug> <id>").description("Delete a rule").action(async (slug, id) => {
705
+ const res = await apiRequest(`/pebbles/${slug}/rules/${id}`, {
706
+ method: "DELETE"
707
+ });
708
+ process.stdout.write(JSON.stringify(res) + "\n");
709
+ if (res.error) process.exit(1);
710
+ });
711
+ rulesCmd.command("deprecate <slug> <id>").description("Deprecate a rule").action(async (slug, id) => {
712
+ const res = await apiRequest(`/pebbles/${slug}/rules/${id}`, {
713
+ method: "PUT",
714
+ body: { status: "deprecated" }
715
+ });
716
+ process.stdout.write(JSON.stringify(res) + "\n");
717
+ if (res.error) process.exit(1);
718
+ });
719
+
720
+ // src/index.ts
721
+ import { readFileSync as readFileSync8 } from "fs";
582
722
  import { fileURLToPath } from "url";
583
723
  import { dirname as dirname2, resolve } from "path";
584
724
  var __dirname = dirname2(fileURLToPath(import.meta.url));
585
- var pkg = JSON.parse(readFileSync7(resolve(__dirname, "../package.json"), "utf-8"));
725
+ var pkg = JSON.parse(readFileSync8(resolve(__dirname, "../package.json"), "utf-8"));
586
726
  var GOLD = "\x1B[33m";
587
727
  var DIM = "\x1B[2m";
588
728
  var RESET = "\x1B[0m";
@@ -600,7 +740,7 @@ ${RESET}
600
740
  ${DIM}${cwd}${RESET}
601
741
  `);
602
742
  }
603
- var program = new Command10();
743
+ var program = new Command11();
604
744
  program.name("odin").description("CLI for Odin \u2014 the knowledge backbone for Pebble House").version(VERSION);
605
745
  program.command("login").description("Authenticate with Odin via browser").action(async () => {
606
746
  printBanner();
@@ -625,6 +765,7 @@ program.addCommand(plansCmd);
625
765
  program.addCommand(artifactsCmd);
626
766
  program.addCommand(versionsCmd);
627
767
  program.addCommand(guidelinesCmd);
768
+ program.addCommand(rulesCmd);
628
769
  process.on("exit", () => {
629
770
  process.stderr.write("\n\n\n\n\n");
630
771
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pebblehouse/odin-cli",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "type": "module",
5
5
  "description": "CLI for Odin — the knowledge backbone for Pebble House",
6
6
  "bin": {