@pebblehouse/odin-cli 0.17.0 → 0.18.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 +8 -0
  2. package/dist/index.js +62 -37
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -65,6 +65,14 @@ odin search "authentication" # Cross-pebble
65
65
  odin search "RLS policies" --pebble odin # Scoped
66
66
  ```
67
67
 
68
+ ### Tickets
69
+
70
+ ```bash
71
+ odin tickets resolve ODIN-12 # Resolve a ticket key to its bet or task
72
+ odin tasks promote <slug> <task-id> --file ./bet.md # Promote a task to a bet (keeps its ticket ID)
73
+ odin tasks create <slug> "Fix" --follow-up-of ODIN-12 # Allocate ODIN-12A as a follow-up
74
+ ```
75
+
68
76
  ### Auth
69
77
 
70
78
  ```bash
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { Command as Command23 } from "commander";
4
+ import { Command as Command24 } from "commander";
5
5
 
6
6
  // src/auth/login.ts
7
7
  import open from "open";
@@ -407,10 +407,10 @@ pebblesCmd.command("get <slug>").description("Get pebble details").action(async
407
407
  process.stdout.write(JSON.stringify(res) + "\n");
408
408
  if (res.error) process.exit(1);
409
409
  });
410
- pebblesCmd.command("create <slug>").description("Create a pebble").requiredOption("--name <name>", "Pebble name").option("--status <status>", "Initial status", "idea").option("--summary <summary>", "Summary").action(async (slug, opts) => {
410
+ pebblesCmd.command("create <slug>").description("Create a pebble").requiredOption("--name <name>", "Pebble name").option("--status <status>", "Initial status", "idea").option("--summary <summary>", "Summary").option("--ticket-prefix <PREFIX>", "Four uppercase letters for ticket IDs (derived from slug when omitted)").action(async (slug, opts) => {
411
411
  const res = await apiRequest("/pebbles", {
412
412
  method: "POST",
413
- body: { slug, name: opts.name, status: opts.status, summary: opts.summary }
413
+ body: { slug, name: opts.name, status: opts.status, summary: opts.summary, ticket_prefix: opts.ticketPrefix }
414
414
  });
415
415
  process.stdout.write(JSON.stringify(res) + "\n");
416
416
  if (res.error) process.exit(1);
@@ -1242,11 +1242,11 @@ betsCmd.command("get <slug> <id>").description("Get a bet by ID").action(async (
1242
1242
  process.stdout.write(JSON.stringify(res) + "\n");
1243
1243
  if (res.error) process.exit(1);
1244
1244
  });
1245
- betsCmd.command("create <slug> <title>").description("Create a bet").option("--source <source>", "Source agent", "claude-code").option("--content <content>", "Bet content").option("--file <path>", "Read content from file").option("--status <status>", "Bet status (pitch, shaped, betting, active, blocked, building, done, archived)").option("--priority <priority>", "Bet priority").option("--appetite <appetite>", "Bet appetite").action(async (slug, title, opts) => {
1245
+ betsCmd.command("create <slug> <title>").description("Create a bet").option("--source <source>", "Source agent", "claude-code").option("--content <content>", "Bet content").option("--file <path>", "Read content from file").option("--status <status>", "Bet status (pitch, shaped, betting, active, blocked, building, done, archived)").option("--priority <priority>", "Bet priority").option("--appetite <appetite>", "Bet appetite").option("--follow-up-of <key-or-uuid>", "Allocate a suffixed follow-up ticket in this ticket's family (e.g. ODIN-12 \u2192 ODIN-12A)").action(async (slug, title, opts) => {
1246
1246
  const content = opts.file ? readFileSync10(opts.file, "utf-8") : opts.content ?? "";
1247
1247
  const res = await apiRequest(`/pebbles/${slug}/bets`, {
1248
1248
  method: "POST",
1249
- body: { title, source: opts.source, content, status: opts.status, priority: opts.priority, appetite: opts.appetite }
1249
+ body: { title, source: opts.source, content, status: opts.status, priority: opts.priority, appetite: opts.appetite, follow_up_of: opts.followUpOf }
1250
1250
  });
1251
1251
  process.stdout.write(JSON.stringify(res) + "\n");
1252
1252
  if (res.error) process.exit(1);
@@ -1275,10 +1275,24 @@ betsCmd.command("delete <slug> <id>").description("Delete a bet").action(async (
1275
1275
  });
1276
1276
  betsCmd.addCommand(depsCmd);
1277
1277
 
1278
- // src/commands/bet-comments.ts
1278
+ // src/commands/tickets.ts
1279
1279
  import { Command as Command15 } from "commander";
1280
+ var ticketsCmd = new Command15("tickets").description("Resolve human-readable ticket IDs");
1281
+ ticketsCmd.command("resolve <key>").description("Resolve a ticket key (e.g. ODIN-12, RATE-36A) to its bet or task, with historical candidates").action(async (key) => {
1282
+ const res = await apiRequest(`/tickets/${encodeURIComponent(key.trim().toUpperCase())}`);
1283
+ process.stdout.write(JSON.stringify(res) + "\n");
1284
+ if (res.data?.legacy_ambiguous) {
1285
+ const others = res.data.candidates.filter((c) => c.entity_id !== res.data.entity_id);
1286
+ process.stderr.write(`note: ${key.toUpperCase()} was a duplicated legacy label; also matches ${others.map((c) => c.ticket_key ?? c.entity_id).join(", ")}
1287
+ `);
1288
+ }
1289
+ if (res.error) process.exit(1);
1290
+ });
1291
+
1292
+ // src/commands/bet-comments.ts
1293
+ import { Command as Command16 } from "commander";
1280
1294
  import { readFileSync as readFileSync11 } from "fs";
1281
- var betCommentsCmd = new Command15("bet-comments").description("Manage bet comments");
1295
+ var betCommentsCmd = new Command16("bet-comments").description("Manage bet comments");
1282
1296
  betCommentsCmd.command("list <slug> <betId>").description("List comments for a bet (oldest first)").action(async (slug, betId) => {
1283
1297
  const res = await apiRequest(`/pebbles/${slug}/bets/${betId}/comments`);
1284
1298
  process.stdout.write(JSON.stringify(res) + "\n");
@@ -1305,8 +1319,9 @@ betCommentsCmd.command("create <slug> <betId> [body]").description("Post a comme
1305
1319
  });
1306
1320
 
1307
1321
  // src/commands/tasks.ts
1308
- import { Command as Command16 } from "commander";
1309
- var tasksCmd = new Command16("tasks").description("Manage tasks");
1322
+ import { Command as Command17 } from "commander";
1323
+ import { readFileSync as readFileSync12 } from "fs";
1324
+ var tasksCmd = new Command17("tasks").description("Manage tasks");
1310
1325
  withListFlags(
1311
1326
  tasksCmd.command("list <slug>").description("List tasks for a pebble").option("--status <status>", "Filter by status").option("--bet <bet-id>", "Filter by bet ID")
1312
1327
  ).action(async (slug, opts) => {
@@ -1327,10 +1342,10 @@ tasksCmd.command("get <slug> <id>").description("Get a task by ID").action(async
1327
1342
  process.stdout.write(JSON.stringify(res) + "\n");
1328
1343
  if (res.error) process.exit(1);
1329
1344
  });
1330
- tasksCmd.command("create <slug> <title>").description("Create a task").option("--source <source>", "Source agent", "claude-code").option("--description <description>", "Task description").option("--bet <bet-id>", "Parent bet ID").option("--status <status>", "Task status").option("--priority <priority>", "Task priority").action(async (slug, title, opts) => {
1345
+ tasksCmd.command("create <slug> <title>").description("Create a task").option("--source <source>", "Source agent", "claude-code").option("--description <description>", "Task description").option("--bet <bet-id>", "Parent bet ID").option("--status <status>", "Task status").option("--priority <priority>", "Task priority").option("--follow-up-of <key-or-uuid>", "Allocate a suffixed follow-up ticket in this ticket's family (e.g. ODIN-12 \u2192 ODIN-12A)").action(async (slug, title, opts) => {
1331
1346
  const res = await apiRequest(`/pebbles/${slug}/tasks`, {
1332
1347
  method: "POST",
1333
- body: { title, source: opts.source, description: opts.description, bet_id: opts.bet, status: opts.status, priority: opts.priority }
1348
+ body: { title, source: opts.source, description: opts.description, bet_id: opts.bet, status: opts.status, priority: opts.priority, follow_up_of: opts.followUpOf }
1334
1349
  });
1335
1350
  process.stdout.write(JSON.stringify(res) + "\n");
1336
1351
  if (res.error) process.exit(1);
@@ -1349,6 +1364,15 @@ tasksCmd.command("update <slug> <id>").description("Update a task").option("--ti
1349
1364
  process.stdout.write(JSON.stringify(res) + "\n");
1350
1365
  if (res.error) process.exit(1);
1351
1366
  });
1367
+ tasksCmd.command("promote <slug> <id>").description("Promote a task to a bet, keeping its ticket ID and history").option("--content <content>", "Shaped bet markdown").option("--file <path>", "Read bet markdown from file").option("--status <status>", "Bet status (pitch | shaped)", "shaped").option("--priority <priority>", "Bet priority").action(async (slug, id, opts) => {
1368
+ const content = opts.file ? readFileSync12(opts.file, "utf-8") : opts.content;
1369
+ const res = await apiRequest(`/pebbles/${slug}/tasks/${id}/promote`, {
1370
+ method: "POST",
1371
+ body: { content, status: opts.status, priority: opts.priority }
1372
+ });
1373
+ process.stdout.write(JSON.stringify(res) + "\n");
1374
+ if (res.error) process.exit(1);
1375
+ });
1352
1376
  tasksCmd.command("delete <slug> <id>").description("Delete a task").action(async (slug, id) => {
1353
1377
  const res = await apiRequest(`/pebbles/${slug}/tasks/${id}`, {
1354
1378
  method: "DELETE"
@@ -1358,9 +1382,9 @@ tasksCmd.command("delete <slug> <id>").description("Delete a task").action(async
1358
1382
  });
1359
1383
 
1360
1384
  // src/commands/roadmaps.ts
1361
- import { Command as Command17 } from "commander";
1362
- import { readFileSync as readFileSync12 } from "fs";
1363
- var roadmapsCmd = new Command17("roadmaps").description("Manage roadmaps");
1385
+ import { Command as Command18 } from "commander";
1386
+ import { readFileSync as readFileSync13 } from "fs";
1387
+ var roadmapsCmd = new Command18("roadmaps").description("Manage roadmaps");
1364
1388
  roadmapsCmd.command("list <slug>").description("List roadmaps for a pebble").option("--status <status>", "Filter by status").action(async (slug, opts) => {
1365
1389
  const params = {};
1366
1390
  if (opts.status) params.status = opts.status;
@@ -1374,7 +1398,7 @@ roadmapsCmd.command("get <slug> <id>").description("Get a roadmap by ID (include
1374
1398
  if (res.error) process.exit(1);
1375
1399
  });
1376
1400
  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) => {
1377
- const content = opts.file ? readFileSync12(opts.file, "utf-8") : opts.content ?? "";
1401
+ const content = opts.file ? readFileSync13(opts.file, "utf-8") : opts.content ?? "";
1378
1402
  const res = await apiRequest(`/pebbles/${slug}/roadmaps`, {
1379
1403
  method: "POST",
1380
1404
  body: { title, source: opts.source, content, status: opts.status }
@@ -1385,7 +1409,7 @@ roadmapsCmd.command("create <slug> <title>").description("Create a roadmap").opt
1385
1409
  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) => {
1386
1410
  const body = {};
1387
1411
  if (opts.title) body.title = opts.title;
1388
- if (opts.file) body.content = readFileSync12(opts.file, "utf-8");
1412
+ if (opts.file) body.content = readFileSync13(opts.file, "utf-8");
1389
1413
  else if (opts.content) body.content = opts.content;
1390
1414
  if (opts.status) body.status = opts.status;
1391
1415
  const res = await apiRequest(`/pebbles/${slug}/roadmaps/${id}`, {
@@ -1404,11 +1428,11 @@ roadmapsCmd.command("delete <slug> <id>").description("Delete a roadmap").action
1404
1428
  });
1405
1429
 
1406
1430
  // src/commands/persona-runs.ts
1407
- import { Command as Command18 } from "commander";
1408
- import { readFileSync as readFileSync13 } from "fs";
1431
+ import { Command as Command19 } from "commander";
1432
+ import { readFileSync as readFileSync14 } from "fs";
1409
1433
  import { join as join2 } from "path";
1410
1434
  import { homedir as homedir2 } from "os";
1411
- var personaRunsCmd = new Command18("persona-runs").description(
1435
+ var personaRunsCmd = new Command19("persona-runs").description(
1412
1436
  "Manage per-persona per-session token usage rows"
1413
1437
  );
1414
1438
  personaRunsCmd.command("list <slug>").description("List persona runs for a pebble").option("--bet-id <id>", "Filter by bet_id").option("--plan-id <id>", "Filter by plan_id").option("--persona <name>", "Filter by persona name").option("--limit <n>", "Max rows", "50").action(async (slug, opts) => {
@@ -1432,7 +1456,7 @@ personaRunsCmd.command("get <slug> <id>").description("Get a persona run by id")
1432
1456
  if (res.error) process.exit(1);
1433
1457
  });
1434
1458
  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) => {
1435
- const body = JSON.parse(readFileSync13(opts.file, "utf-8"));
1459
+ const body = JSON.parse(readFileSync14(opts.file, "utf-8"));
1436
1460
  const res = await apiRequest(`/pebbles/${slug}/persona-runs`, {
1437
1461
  method: "POST",
1438
1462
  body
@@ -1444,7 +1468,7 @@ personaRunsCmd.command("capture").description("Read marker + transcript, compute
1444
1468
  const markerPath = opts.marker ?? join2(homedir2(), ".claude", "odin-context", `${opts.sessionId}.json`);
1445
1469
  let marker = {};
1446
1470
  try {
1447
- marker = JSON.parse(readFileSync13(markerPath, "utf-8"));
1471
+ marker = JSON.parse(readFileSync14(markerPath, "utf-8"));
1448
1472
  } catch {
1449
1473
  process.exit(0);
1450
1474
  }
@@ -1454,7 +1478,7 @@ personaRunsCmd.command("capture").description("Read marker + transcript, compute
1454
1478
  let input = 0, output = 0, cacheRead = 0, cacheCreate = 0;
1455
1479
  let minTs = null, maxTs = null;
1456
1480
  try {
1457
- const raw = readFileSync13(opts.transcript, "utf-8");
1481
+ const raw = readFileSync14(opts.transcript, "utf-8");
1458
1482
  for (const line of raw.split("\n")) {
1459
1483
  if (!line.trim()) continue;
1460
1484
  let obj;
@@ -1505,9 +1529,9 @@ personaRunsCmd.command("capture").description("Read marker + transcript, compute
1505
1529
  });
1506
1530
 
1507
1531
  // src/commands/blog.ts
1508
- import { Command as Command19 } from "commander";
1509
- import { readFileSync as readFileSync14 } from "fs";
1510
- var blogCmd = new Command19("blog").description("Manage blog posts");
1532
+ import { Command as Command20 } from "commander";
1533
+ import { readFileSync as readFileSync15 } from "fs";
1534
+ var blogCmd = new Command20("blog").description("Manage blog posts");
1511
1535
  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) => {
1512
1536
  const params = {};
1513
1537
  if (opts.status) params.status = opts.status;
@@ -1526,7 +1550,7 @@ blogCmd.command("get <slug> <idOrSlug>").description("Get a blog post by UUID or
1526
1550
  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) => {
1527
1551
  let blocks = [];
1528
1552
  if (opts.file) {
1529
- blocks = JSON.parse(readFileSync14(opts.file, "utf-8"));
1553
+ blocks = JSON.parse(readFileSync15(opts.file, "utf-8"));
1530
1554
  } else if (opts.blocks) {
1531
1555
  blocks = JSON.parse(opts.blocks);
1532
1556
  }
@@ -1550,7 +1574,7 @@ blogCmd.command("update <slug> <id>").description("Update a blog post").option("
1550
1574
  const body = {};
1551
1575
  if (opts.title) body.title = opts.title;
1552
1576
  if (opts.postSlug) body.slug = opts.postSlug;
1553
- if (opts.file) body.blocks = JSON.parse(readFileSync14(opts.file, "utf-8"));
1577
+ if (opts.file) body.blocks = JSON.parse(readFileSync15(opts.file, "utf-8"));
1554
1578
  else if (opts.blocks) body.blocks = JSON.parse(opts.blocks);
1555
1579
  if (opts.status) body.status = opts.status;
1556
1580
  if (opts.scheduledPublishAt !== void 0)
@@ -1572,8 +1596,8 @@ blogCmd.command("delete <slug> <id>").description("Delete a blog post").action(a
1572
1596
  });
1573
1597
 
1574
1598
  // src/commands/api-keys.ts
1575
- import { Command as Command20 } from "commander";
1576
- var apiKeysCmd = new Command20("api-keys").description("Manage API keys for server-to-server access");
1599
+ import { Command as Command21 } from "commander";
1600
+ var apiKeysCmd = new Command21("api-keys").description("Manage API keys for server-to-server access");
1577
1601
  apiKeysCmd.command("list <slug>").description("List API keys for a pebble").action(async (slug) => {
1578
1602
  const res = await apiRequest(`/pebbles/${slug}/api-keys`);
1579
1603
  process.stdout.write(JSON.stringify(res) + "\n");
@@ -1615,7 +1639,7 @@ apiKeysCmd.command("delete <slug> <id>").description("Permanently delete an API
1615
1639
  });
1616
1640
 
1617
1641
  // src/commands/pebble/index.ts
1618
- import { Command as Command21 } from "commander";
1642
+ import { Command as Command22 } from "commander";
1619
1643
 
1620
1644
  // src/commands/pebble/provisioner.ts
1621
1645
  async function runProvisioner(ctx, steps) {
@@ -2028,7 +2052,7 @@ Hand off: /odin:eng-manager bet:${bet.data.id}
2028
2052
  }
2029
2053
 
2030
2054
  // src/commands/pebble/index.ts
2031
- var pebbleCmd = new Command21("pebble").description(
2055
+ var pebbleCmd = new Command22("pebble").description(
2032
2056
  "Provision and initialize pebbles (eng-infra, run-once)"
2033
2057
  );
2034
2058
  pebbleCmd.command("new <name>").description("Provision a new pebble from pebble-template (day-one)").option("--run", "Hand off to eng-manager on the init bet").option("--dir <path>", "Parent dir to clone into").option("--dry-run", "Print the plan; mutate nothing").action(async (name, opts) => {
@@ -2049,7 +2073,7 @@ pebbleCmd.command("provision <provider> <slug>").description("Run one provider s
2049
2073
  });
2050
2074
 
2051
2075
  // src/commands/create/index.ts
2052
- import { Command as Command22 } from "commander";
2076
+ import { Command as Command23 } from "commander";
2053
2077
 
2054
2078
  // src/commands/create/topology.ts
2055
2079
  var ENVS = ["prd", "stg", "dev"];
@@ -2514,22 +2538,22 @@ function assembleInputs(ctx) {
2514
2538
  }
2515
2539
 
2516
2540
  // src/commands/create/index.ts
2517
- var createCmd = new Command22("create").description(
2541
+ var createCmd = new Command23("create").description(
2518
2542
  "Create and provision a new project end to end"
2519
2543
  );
2520
2544
  createCmd.command("project [name]").description("Guided self-serve project + parallel-env provisioning").option("--dry-run", "Print the plan; mutate nothing").action(
2521
2545
  async (name, opts) => process.exit(await runCreate(name, { dryRun: opts.dryRun }))
2522
2546
  );
2523
- var setupCmd = new Command22("setup").description("Alias for `create project`").arguments("[name]").option("--dry-run", "Print the plan; mutate nothing").action(
2547
+ var setupCmd = new Command23("setup").description("Alias for `create project`").arguments("[name]").option("--dry-run", "Print the plan; mutate nothing").action(
2524
2548
  async (name, opts) => process.exit(await runCreate(name, { dryRun: opts.dryRun }))
2525
2549
  );
2526
2550
 
2527
2551
  // src/index.ts
2528
- import { readFileSync as readFileSync15 } from "fs";
2552
+ import { readFileSync as readFileSync16 } from "fs";
2529
2553
  import { fileURLToPath } from "url";
2530
2554
  import { dirname as dirname2, resolve as resolve2 } from "path";
2531
2555
  var __dirname = dirname2(fileURLToPath(import.meta.url));
2532
- var pkg = JSON.parse(readFileSync15(resolve2(__dirname, "../package.json"), "utf-8"));
2556
+ var pkg = JSON.parse(readFileSync16(resolve2(__dirname, "../package.json"), "utf-8"));
2533
2557
  var GOLD = "\x1B[33m";
2534
2558
  var DIM = "\x1B[2m";
2535
2559
  var RESET = "\x1B[0m";
@@ -2547,7 +2571,7 @@ ${RESET}
2547
2571
  ${DIM}${cwd}${RESET}
2548
2572
  `);
2549
2573
  }
2550
- var program = new Command23();
2574
+ var program = new Command24();
2551
2575
  program.name("odin").description("CLI for Odin \u2014 the knowledge backbone for Pebble House").version(VERSION);
2552
2576
  program.command("login").description("Authenticate with Odin via browser").option("--device", "Use the device-code flow (for headless or remote machines)").action(async (options) => {
2553
2577
  printBanner();
@@ -2576,6 +2600,7 @@ program.addCommand(rulesCmd);
2576
2600
  program.addCommand(briefsCmd);
2577
2601
  program.addCommand(blueprintsCmd);
2578
2602
  program.addCommand(betsCmd);
2603
+ program.addCommand(ticketsCmd);
2579
2604
  program.addCommand(betCommentsCmd);
2580
2605
  program.addCommand(tasksCmd);
2581
2606
  program.addCommand(roadmapsCmd);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pebblehouse/odin-cli",
3
- "version": "0.17.0",
3
+ "version": "0.18.0",
4
4
  "type": "module",
5
5
  "description": "CLI for Odin — the knowledge backbone for Pebble House",
6
6
  "bin": {