@pebblehouse/odin-cli 0.18.3 → 0.19.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 (3) hide show
  1. package/README.md +9 -0
  2. package/dist/index.js +67 -16
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -73,6 +73,15 @@ odin tasks promote <slug> <task-id> --file ./bet.md # Promote a task to a bet
73
73
  odin tasks create <slug> "Fix" --follow-up-of ODIN-12 # Allocate ODIN-12A as a follow-up
74
74
  ```
75
75
 
76
+ ### Roadmaps
77
+
78
+ ```bash
79
+ odin roadmaps client-draft get <roadmap-id> [--pebble <slug>]
80
+ odin roadmaps client-draft save <roadmap-id> --expected-revision <n> --file <payload.json> [--pebble <slug>]
81
+ ```
82
+
83
+ Reads or saves the client-page draft of a master roadmap. `--pebble` defaults to the nearest `CLAUDE.md` `pebble:` line. Requires `odin login` as Kenny; API keys get `dashboard_approval_required`. Prints the `{ data, error, issues? }` JSON envelope and exits 1 on any error.
84
+
76
85
  ### Auth
77
86
 
78
87
  ```bash
package/dist/index.js CHANGED
@@ -1416,7 +1416,28 @@ tasksCmd.command("delete <slug> <id>").description("Delete a task").action(async
1416
1416
 
1417
1417
  // src/commands/roadmaps.ts
1418
1418
  import { Command as Command18 } from "commander";
1419
- import { readFileSync as readFileSync13 } from "fs";
1419
+ import { readFileSync as readFileSync14 } from "fs";
1420
+
1421
+ // src/resolve-pebble.ts
1422
+ import { existsSync as existsSync3, readFileSync as readFileSync13 } from "fs";
1423
+ import { dirname as dirname2, join as join2 } from "path";
1424
+ var PEBBLE_LINE = /^pebble:\s*([a-z0-9][a-z0-9-]*)\s*$/m;
1425
+ function resolvePebbleSlug(explicit, cwd = process.cwd()) {
1426
+ if (explicit) return explicit;
1427
+ let dir = cwd;
1428
+ for (; ; ) {
1429
+ const file = join2(dir, "CLAUDE.md");
1430
+ if (existsSync3(file)) {
1431
+ const m = readFileSync13(file, "utf-8").match(PEBBLE_LINE);
1432
+ if (m) return m[1];
1433
+ }
1434
+ const parent = dirname2(dir);
1435
+ if (parent === dir) return null;
1436
+ dir = parent;
1437
+ }
1438
+ }
1439
+
1440
+ // src/commands/roadmaps.ts
1420
1441
  var roadmapsCmd = new Command18("roadmaps").description("Manage roadmaps");
1421
1442
  roadmapsCmd.command("list <slug>").description("List roadmaps for a pebble").option("--status <status>", "Filter by status").option("--all", "Include child roadmaps (default: top-level only)").action(async (slug, opts) => {
1422
1443
  const params = { nesting: opts.all ? "all" : "top" };
@@ -1431,7 +1452,7 @@ roadmapsCmd.command("get <slug> <id>").description("Get a roadmap by ID (include
1431
1452
  if (res.error) process.exit(1);
1432
1453
  });
1433
1454
  roadmapsCmd.command("create <slug> <title>").description("Create a roadmap").option("--source <source>", "Source agent", "claude-code").option("--content <content>", "Roadmap content").option("--file <path>", "Read content from file").option("--status <status>", "Roadmap status").option("--parent <id>", "Parent roadmap UUID (nesting is capped at 2 levels)").option("--starts-on <date>", "Start date, YYYY-MM-DD").option("--ends-on <date>", "End date, YYYY-MM-DD").action(async (slug, title, opts) => {
1434
- const content = opts.file ? readFileSync13(opts.file, "utf-8") : opts.content ?? "";
1455
+ const content = opts.file ? readFileSync14(opts.file, "utf-8") : opts.content ?? "";
1435
1456
  const res = await apiRequest(`/pebbles/${slug}/roadmaps`, {
1436
1457
  method: "POST",
1437
1458
  body: {
@@ -1450,7 +1471,7 @@ roadmapsCmd.command("create <slug> <title>").description("Create a roadmap").opt
1450
1471
  roadmapsCmd.command("update <slug> <id>").description("Update a roadmap").option("--title <title>", "New title").option("--content <content>", "New content").option("--file <path>", "Read content from file").option("--status <status>", "New status").option("--parent <id>", 'New parent roadmap UUID, or the literal "null" to promote to top level').option("--starts-on <date>", 'New start date YYYY-MM-DD, or the literal "null" to clear').option("--ends-on <date>", 'New end date YYYY-MM-DD, or the literal "null" to clear').action(async (slug, id, opts) => {
1451
1472
  const body = {};
1452
1473
  if (opts.title) body.title = opts.title;
1453
- if (opts.file) body.content = readFileSync13(opts.file, "utf-8");
1474
+ if (opts.file) body.content = readFileSync14(opts.file, "utf-8");
1454
1475
  else if (opts.content) body.content = opts.content;
1455
1476
  if (opts.status) body.status = opts.status;
1456
1477
  const nullable = (v) => v === void 0 ? void 0 : v === "null" ? null : v;
@@ -1471,11 +1492,41 @@ roadmapsCmd.command("delete <slug> <id>").description("Delete a roadmap").action
1471
1492
  process.stdout.write(JSON.stringify(res) + "\n");
1472
1493
  if (res.error) process.exit(1);
1473
1494
  });
1495
+ function emit(res, failed) {
1496
+ process.stdout.write(JSON.stringify(res) + "\n");
1497
+ if (failed) process.exit(1);
1498
+ }
1499
+ function draftPath(pebble, roadmapId) {
1500
+ const slug = resolvePebbleSlug(pebble);
1501
+ return slug ? `/pebbles/${slug}/roadmaps/${roadmapId}/client-page/draft` : null;
1502
+ }
1503
+ var clientDraftCmd = roadmapsCmd.command("client-draft").description("Read or save the client-page draft of a master roadmap (Kenny's session only; draft only)");
1504
+ clientDraftCmd.command("get <roadmap-id>").description("Print the draft as { data: { revision, payload } | null, error }").option("--pebble <slug>", "Pebble slug (default: nearest CLAUDE.md `pebble:` line)").action(async (roadmapId, opts) => {
1505
+ const path = draftPath(opts.pebble, roadmapId);
1506
+ if (!path) return emit({ data: null, error: "pebble_unresolved" }, true);
1507
+ const res = await apiRequest(path);
1508
+ emit(res, Boolean(res.error));
1509
+ });
1510
+ clientDraftCmd.command("save <roadmap-id>").description("Save the draft; --file holds the payload only. Prints { data, error, issues? }").requiredOption("--expected-revision <n>", "Revision you read (0 creates the first draft)").requiredOption("--file <path>", "JSON file containing the RoadmapClientPayload").option("--pebble <slug>", "Pebble slug (default: nearest CLAUDE.md `pebble:` line)").action(async (roadmapId, opts) => {
1511
+ const path = draftPath(opts.pebble, roadmapId);
1512
+ if (!path) return emit({ data: null, error: "pebble_unresolved" }, true);
1513
+ if (!/^\d+$/.test(opts.expectedRevision) || !Number.isSafeInteger(Number(opts.expectedRevision))) {
1514
+ return emit({ data: null, error: "invalid_payload", issues: [{ path: "expectedRevision", message: "must be a non-negative integer" }] }, true);
1515
+ }
1516
+ let payload;
1517
+ try {
1518
+ payload = JSON.parse(readFileSync14(opts.file, "utf-8"));
1519
+ } catch (e) {
1520
+ return emit({ data: null, error: "invalid_payload", issues: [{ path: "(root)", message: `payload file is not valid JSON: ${e.message}` }] }, true);
1521
+ }
1522
+ const res = await apiRequest(path, { method: "PUT", body: { expectedRevision: Number(opts.expectedRevision), payload } });
1523
+ emit(res, Boolean(res.error));
1524
+ });
1474
1525
 
1475
1526
  // src/commands/persona-runs.ts
1476
1527
  import { Command as Command19 } from "commander";
1477
- import { readFileSync as readFileSync14 } from "fs";
1478
- import { join as join2 } from "path";
1528
+ import { readFileSync as readFileSync15 } from "fs";
1529
+ import { join as join3 } from "path";
1479
1530
  import { homedir as homedir2 } from "os";
1480
1531
  var personaRunsCmd = new Command19("persona-runs").description(
1481
1532
  "Manage per-persona per-session token usage rows"
@@ -1501,7 +1552,7 @@ personaRunsCmd.command("get <slug> <id>").description("Get a persona run by id")
1501
1552
  if (res.error) process.exit(1);
1502
1553
  });
1503
1554
  personaRunsCmd.command("create <slug>").description("Create/upsert a persona run from a JSON file").requiredOption("--file <path>", "JSON payload matching PersonaRunCreate").action(async (slug, opts) => {
1504
- const body = JSON.parse(readFileSync14(opts.file, "utf-8"));
1555
+ const body = JSON.parse(readFileSync15(opts.file, "utf-8"));
1505
1556
  const res = await apiRequest(`/pebbles/${slug}/persona-runs`, {
1506
1557
  method: "POST",
1507
1558
  body
@@ -1510,10 +1561,10 @@ personaRunsCmd.command("create <slug>").description("Create/upsert a persona run
1510
1561
  if (res.error) process.exit(1);
1511
1562
  });
1512
1563
  personaRunsCmd.command("capture").description("Read marker + transcript, compute tokens, POST to endpoint (hook path)").requiredOption("--session-id <id>", "Claude Code session id").requiredOption("--transcript <path>", "Path to transcript JSONL").option("--marker <path>", "Explicit marker path (default: ~/.claude/odin-context/<session-id>.json)").action(async (opts) => {
1513
- const markerPath = opts.marker ?? join2(homedir2(), ".claude", "odin-context", `${opts.sessionId}.json`);
1564
+ const markerPath = opts.marker ?? join3(homedir2(), ".claude", "odin-context", `${opts.sessionId}.json`);
1514
1565
  let marker = {};
1515
1566
  try {
1516
- marker = JSON.parse(readFileSync14(markerPath, "utf-8"));
1567
+ marker = JSON.parse(readFileSync15(markerPath, "utf-8"));
1517
1568
  } catch {
1518
1569
  process.exit(0);
1519
1570
  }
@@ -1523,7 +1574,7 @@ personaRunsCmd.command("capture").description("Read marker + transcript, compute
1523
1574
  let input = 0, output = 0, cacheRead = 0, cacheCreate = 0;
1524
1575
  let minTs = null, maxTs = null;
1525
1576
  try {
1526
- const raw = readFileSync14(opts.transcript, "utf-8");
1577
+ const raw = readFileSync15(opts.transcript, "utf-8");
1527
1578
  for (const line of raw.split("\n")) {
1528
1579
  if (!line.trim()) continue;
1529
1580
  let obj;
@@ -1575,7 +1626,7 @@ personaRunsCmd.command("capture").description("Read marker + transcript, compute
1575
1626
 
1576
1627
  // src/commands/blog.ts
1577
1628
  import { Command as Command20 } from "commander";
1578
- import { readFileSync as readFileSync15 } from "fs";
1629
+ import { readFileSync as readFileSync16 } from "fs";
1579
1630
  var blogCmd = new Command20("blog").description("Manage blog posts");
1580
1631
  blogCmd.command("list <slug>").description("List blog posts for a pebble").option("--status <status>", "Filter by status (draft, review, scheduled, published, archived)").option("--tag <tag>", "Filter by tag").option("--page <page>", "Page number", "1").option("--per-page <perPage>", "Results per page", "20").action(async (slug, opts) => {
1581
1632
  const params = {};
@@ -1595,7 +1646,7 @@ blogCmd.command("get <slug> <idOrSlug>").description("Get a blog post by UUID or
1595
1646
  blogCmd.command("create <slug> <title>").description("Create a blog post").requiredOption("--post-slug <postSlug>", "URL slug for the post").option("--blocks <blocks>", "Content blocks as JSON string").option("--file <path>", "Read content blocks from JSON file").option("--status <status>", "Initial status (draft, review, scheduled)", "draft").option("--scheduled-publish-at <iso>", "UTC ISO timestamp for scheduled publishing (required when --status scheduled)").option("--metadata <metadata>", "Post metadata as JSON string").option("--campaign-id <campaignId>", "Link to a marketing campaign").action(async (slug, title, opts) => {
1596
1647
  let blocks = [];
1597
1648
  if (opts.file) {
1598
- blocks = JSON.parse(readFileSync15(opts.file, "utf-8"));
1649
+ blocks = JSON.parse(readFileSync16(opts.file, "utf-8"));
1599
1650
  } else if (opts.blocks) {
1600
1651
  blocks = JSON.parse(opts.blocks);
1601
1652
  }
@@ -1619,7 +1670,7 @@ blogCmd.command("update <slug> <id>").description("Update a blog post").option("
1619
1670
  const body = {};
1620
1671
  if (opts.title) body.title = opts.title;
1621
1672
  if (opts.postSlug) body.slug = opts.postSlug;
1622
- if (opts.file) body.blocks = JSON.parse(readFileSync15(opts.file, "utf-8"));
1673
+ if (opts.file) body.blocks = JSON.parse(readFileSync16(opts.file, "utf-8"));
1623
1674
  else if (opts.blocks) body.blocks = JSON.parse(opts.blocks);
1624
1675
  if (opts.status) body.status = opts.status;
1625
1676
  if (opts.scheduledPublishAt !== void 0)
@@ -2594,11 +2645,11 @@ var setupCmd = new Command23("setup").description("Alias for `create project`").
2594
2645
  );
2595
2646
 
2596
2647
  // src/index.ts
2597
- import { readFileSync as readFileSync16 } from "fs";
2648
+ import { readFileSync as readFileSync17 } from "fs";
2598
2649
  import { fileURLToPath } from "url";
2599
- import { dirname as dirname2, resolve as resolve2 } from "path";
2600
- var __dirname = dirname2(fileURLToPath(import.meta.url));
2601
- var pkg = JSON.parse(readFileSync16(resolve2(__dirname, "../package.json"), "utf-8"));
2650
+ import { dirname as dirname3, resolve as resolve2 } from "path";
2651
+ var __dirname = dirname3(fileURLToPath(import.meta.url));
2652
+ var pkg = JSON.parse(readFileSync17(resolve2(__dirname, "../package.json"), "utf-8"));
2602
2653
  var GOLD = "\x1B[33m";
2603
2654
  var DIM = "\x1B[2m";
2604
2655
  var RESET = "\x1B[0m";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pebblehouse/odin-cli",
3
- "version": "0.18.3",
3
+ "version": "0.19.0",
4
4
  "type": "module",
5
5
  "description": "CLI for Odin — the knowledge backbone for Pebble House",
6
6
  "bin": {