@pebblehouse/odin-cli 0.6.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 +85 -3
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pebblehouse/odin-cli",
3
- "version": "0.6.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": {