@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.
- package/dist/index.js +148 -7
- 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
|
|
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
|
-
|
|
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
|
-
|
|
324
|
-
|
|
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/
|
|
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(
|
|
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
|
|
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
|
});
|