@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.
- package/dist/index.js +103 -23
- 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
|
-
|
|
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
|
-
|
|
325
|
-
|
|
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 (
|
|
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/
|
|
532
|
+
// src/commands/artifacts.ts
|
|
452
533
|
import { Command as Command7 } from "commander";
|
|
453
534
|
import { readFileSync as readFileSync5 } from "fs";
|
|
454
|
-
var
|
|
455
|
-
|
|
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}/
|
|
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
|
-
|
|
463
|
-
const res = await apiRequest(`/pebbles/${slug}/
|
|
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
|
-
|
|
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}/
|
|
550
|
+
const res = await apiRequest(`/pebbles/${slug}/artifacts`, {
|
|
470
551
|
method: "POST",
|
|
471
552
|
body: {
|
|
472
553
|
title,
|
|
473
|
-
|
|
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
|
-
|
|
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.
|
|
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}/
|
|
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
|
-
|
|
497
|
-
const res = await apiRequest(`/pebbles/${slug}/
|
|
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(
|
|
707
|
+
program.addCommand(artifactsCmd);
|
|
628
708
|
program.addCommand(versionsCmd);
|
|
629
709
|
program.addCommand(guidelinesCmd);
|
|
630
710
|
process.on("exit", () => {
|