@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.
- package/README.md +9 -0
- package/dist/index.js +84 -21
- 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
|
|
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 ?
|
|
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: {
|
|
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 =
|
|
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
|
|
1466
|
-
import { join as
|
|
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(
|
|
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 ??
|
|
1564
|
+
const markerPath = opts.marker ?? join3(homedir2(), ".claude", "odin-context", `${opts.sessionId}.json`);
|
|
1502
1565
|
let marker = {};
|
|
1503
1566
|
try {
|
|
1504
|
-
marker = JSON.parse(
|
|
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 =
|
|
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
|
|
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(
|
|
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(
|
|
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
|
|
2648
|
+
import { readFileSync as readFileSync17 } from "fs";
|
|
2586
2649
|
import { fileURLToPath } from "url";
|
|
2587
|
-
import { dirname as
|
|
2588
|
-
var __dirname =
|
|
2589
|
-
var pkg = JSON.parse(
|
|
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";
|