@pebblehouse/odin-cli 0.18.2 → 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 +84 -21
  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,10 +1416,31 @@ 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
- roadmapsCmd.command("list <slug>").description("List roadmaps for a pebble").option("--status <status>", "Filter by status").action(async (slug, opts) => {
1422
- const params = {};
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) => {
1443
+ const params = { nesting: opts.all ? "all" : "top" };
1423
1444
  if (opts.status) params.status = opts.status;
1424
1445
  const res = await apiRequest(`/pebbles/${slug}/roadmaps`, { params });
1425
1446
  process.stdout.write(JSON.stringify(res) + "\n");
@@ -1430,21 +1451,33 @@ roadmapsCmd.command("get <slug> <id>").description("Get a roadmap by ID (include
1430
1451
  process.stdout.write(JSON.stringify(res) + "\n");
1431
1452
  if (res.error) process.exit(1);
1432
1453
  });
1433
- 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").action(async (slug, title, opts) => {
1434
- const content = opts.file ? readFileSync13(opts.file, "utf-8") : opts.content ?? "";
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) => {
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
- body: { title, source: opts.source, content, status: opts.status }
1458
+ body: {
1459
+ title,
1460
+ source: opts.source,
1461
+ content,
1462
+ status: opts.status,
1463
+ parent_roadmap_id: opts.parent,
1464
+ starts_on: opts.startsOn,
1465
+ ends_on: opts.endsOn
1466
+ }
1438
1467
  });
1439
1468
  process.stdout.write(JSON.stringify(res) + "\n");
1440
1469
  if (res.error) process.exit(1);
1441
1470
  });
1442
- 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").action(async (slug, id, opts) => {
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) => {
1443
1472
  const body = {};
1444
1473
  if (opts.title) body.title = opts.title;
1445
- if (opts.file) body.content = readFileSync13(opts.file, "utf-8");
1474
+ if (opts.file) body.content = readFileSync14(opts.file, "utf-8");
1446
1475
  else if (opts.content) body.content = opts.content;
1447
1476
  if (opts.status) body.status = opts.status;
1477
+ const nullable = (v) => v === void 0 ? void 0 : v === "null" ? null : v;
1478
+ if (opts.parent !== void 0) body.parent_roadmap_id = nullable(opts.parent);
1479
+ if (opts.startsOn !== void 0) body.starts_on = nullable(opts.startsOn);
1480
+ if (opts.endsOn !== void 0) body.ends_on = nullable(opts.endsOn);
1448
1481
  const res = await apiRequest(`/pebbles/${slug}/roadmaps/${id}`, {
1449
1482
  method: "PUT",
1450
1483
  body
@@ -1459,11 +1492,41 @@ roadmapsCmd.command("delete <slug> <id>").description("Delete a roadmap").action
1459
1492
  process.stdout.write(JSON.stringify(res) + "\n");
1460
1493
  if (res.error) process.exit(1);
1461
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
+ });
1462
1525
 
1463
1526
  // src/commands/persona-runs.ts
1464
1527
  import { Command as Command19 } from "commander";
1465
- import { readFileSync as readFileSync14 } from "fs";
1466
- import { join as join2 } from "path";
1528
+ import { readFileSync as readFileSync15 } from "fs";
1529
+ import { join as join3 } from "path";
1467
1530
  import { homedir as homedir2 } from "os";
1468
1531
  var personaRunsCmd = new Command19("persona-runs").description(
1469
1532
  "Manage per-persona per-session token usage rows"
@@ -1489,7 +1552,7 @@ personaRunsCmd.command("get <slug> <id>").description("Get a persona run by id")
1489
1552
  if (res.error) process.exit(1);
1490
1553
  });
1491
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) => {
1492
- const body = JSON.parse(readFileSync14(opts.file, "utf-8"));
1555
+ const body = JSON.parse(readFileSync15(opts.file, "utf-8"));
1493
1556
  const res = await apiRequest(`/pebbles/${slug}/persona-runs`, {
1494
1557
  method: "POST",
1495
1558
  body
@@ -1498,10 +1561,10 @@ personaRunsCmd.command("create <slug>").description("Create/upsert a persona run
1498
1561
  if (res.error) process.exit(1);
1499
1562
  });
1500
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) => {
1501
- 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`);
1502
1565
  let marker = {};
1503
1566
  try {
1504
- marker = JSON.parse(readFileSync14(markerPath, "utf-8"));
1567
+ marker = JSON.parse(readFileSync15(markerPath, "utf-8"));
1505
1568
  } catch {
1506
1569
  process.exit(0);
1507
1570
  }
@@ -1511,7 +1574,7 @@ personaRunsCmd.command("capture").description("Read marker + transcript, compute
1511
1574
  let input = 0, output = 0, cacheRead = 0, cacheCreate = 0;
1512
1575
  let minTs = null, maxTs = null;
1513
1576
  try {
1514
- const raw = readFileSync14(opts.transcript, "utf-8");
1577
+ const raw = readFileSync15(opts.transcript, "utf-8");
1515
1578
  for (const line of raw.split("\n")) {
1516
1579
  if (!line.trim()) continue;
1517
1580
  let obj;
@@ -1563,7 +1626,7 @@ personaRunsCmd.command("capture").description("Read marker + transcript, compute
1563
1626
 
1564
1627
  // src/commands/blog.ts
1565
1628
  import { Command as Command20 } from "commander";
1566
- import { readFileSync as readFileSync15 } from "fs";
1629
+ import { readFileSync as readFileSync16 } from "fs";
1567
1630
  var blogCmd = new Command20("blog").description("Manage blog posts");
1568
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) => {
1569
1632
  const params = {};
@@ -1583,7 +1646,7 @@ blogCmd.command("get <slug> <idOrSlug>").description("Get a blog post by UUID or
1583
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) => {
1584
1647
  let blocks = [];
1585
1648
  if (opts.file) {
1586
- blocks = JSON.parse(readFileSync15(opts.file, "utf-8"));
1649
+ blocks = JSON.parse(readFileSync16(opts.file, "utf-8"));
1587
1650
  } else if (opts.blocks) {
1588
1651
  blocks = JSON.parse(opts.blocks);
1589
1652
  }
@@ -1607,7 +1670,7 @@ blogCmd.command("update <slug> <id>").description("Update a blog post").option("
1607
1670
  const body = {};
1608
1671
  if (opts.title) body.title = opts.title;
1609
1672
  if (opts.postSlug) body.slug = opts.postSlug;
1610
- 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"));
1611
1674
  else if (opts.blocks) body.blocks = JSON.parse(opts.blocks);
1612
1675
  if (opts.status) body.status = opts.status;
1613
1676
  if (opts.scheduledPublishAt !== void 0)
@@ -2582,11 +2645,11 @@ var setupCmd = new Command23("setup").description("Alias for `create project`").
2582
2645
  );
2583
2646
 
2584
2647
  // src/index.ts
2585
- import { readFileSync as readFileSync16 } from "fs";
2648
+ import { readFileSync as readFileSync17 } from "fs";
2586
2649
  import { fileURLToPath } from "url";
2587
- import { dirname as dirname2, resolve as resolve2 } from "path";
2588
- var __dirname = dirname2(fileURLToPath(import.meta.url));
2589
- 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"));
2590
2653
  var GOLD = "\x1B[33m";
2591
2654
  var DIM = "\x1B[2m";
2592
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.2",
3
+ "version": "0.19.0",
4
4
  "type": "module",
5
5
  "description": "CLI for Odin — the knowledge backbone for Pebble House",
6
6
  "bin": {