@pebblehouse/odin-cli 0.17.0 → 0.18.1

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 +99 -40
  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";
@@ -384,7 +384,8 @@ async function apiRequest(path, options = {}) {
384
384
  }
385
385
  const headers = {
386
386
  Authorization: `Bearer ${token}`,
387
- "Content-Type": "application/json"
387
+ "Content-Type": "application/json",
388
+ "X-Odin-Client": "cli"
388
389
  };
389
390
  const res = await fetch(url, {
390
391
  method,
@@ -407,10 +408,10 @@ pebblesCmd.command("get <slug>").description("Get pebble details").action(async
407
408
  process.stdout.write(JSON.stringify(res) + "\n");
408
409
  if (res.error) process.exit(1);
409
410
  });
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) => {
411
+ 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
412
  const res = await apiRequest("/pebbles", {
412
413
  method: "POST",
413
- body: { slug, name: opts.name, status: opts.status, summary: opts.summary }
414
+ body: { slug, name: opts.name, status: opts.status, summary: opts.summary, ticket_prefix: opts.ticketPrefix }
414
415
  });
415
416
  process.stdout.write(JSON.stringify(res) + "\n");
416
417
  if (res.error) process.exit(1);
@@ -1190,6 +1191,7 @@ blueprintsCmd.command("assets <id>").description("List assets for a blueprint").
1190
1191
  // src/commands/bets.ts
1191
1192
  import { Command as Command14 } from "commander";
1192
1193
  import { readFileSync as readFileSync10 } from "fs";
1194
+ import { randomUUID } from "crypto";
1193
1195
 
1194
1196
  // src/commands/bet-dependencies.ts
1195
1197
  import { Command as Command13 } from "commander";
@@ -1223,8 +1225,9 @@ depsCmd.command("remove <slug> <bet-id>").description("Remove a blocked-by depen
1223
1225
 
1224
1226
  // src/commands/bets.ts
1225
1227
  var betsCmd = new Command14("bets").description("Manage bets");
1228
+ var STAGE_HELP = "pitch, shaping, ready, planning, building, review, qa, shipping, done, archived (legacy: shaped, betting, active, blocked)";
1226
1229
  withListFlags(
1227
- betsCmd.command("list <slug>").description("List bets for a pebble").option("--status <status>", "Filter by status (pitch, shaped, betting, active, blocked, building, done, archived)")
1230
+ betsCmd.command("list <slug>").description("List bets for a pebble").option("--status <status>", `Status (${STAGE_HELP})`)
1228
1231
  ).action(async (slug, opts) => {
1229
1232
  const resolved = resolveListParams(opts);
1230
1233
  if (resolved.fatal) {
@@ -1242,16 +1245,16 @@ betsCmd.command("get <slug> <id>").description("Get a bet by ID").action(async (
1242
1245
  process.stdout.write(JSON.stringify(res) + "\n");
1243
1246
  if (res.error) process.exit(1);
1244
1247
  });
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) => {
1248
+ 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>", "pitch (default) or shaped (transitional)").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
1249
  const content = opts.file ? readFileSync10(opts.file, "utf-8") : opts.content ?? "";
1247
1250
  const res = await apiRequest(`/pebbles/${slug}/bets`, {
1248
1251
  method: "POST",
1249
- body: { title, source: opts.source, content, status: opts.status, priority: opts.priority, appetite: opts.appetite }
1252
+ body: { title, source: opts.source, content, status: opts.status, priority: opts.priority, appetite: opts.appetite, follow_up_of: opts.followUpOf }
1250
1253
  });
1251
1254
  process.stdout.write(JSON.stringify(res) + "\n");
1252
1255
  if (res.error) process.exit(1);
1253
1256
  });
1254
- betsCmd.command("update <slug> <id>").description("Update a bet").option("--title <title>", "New title").option("--content <content>", "New content").option("--file <path>", "Read content from file").option("--status <status>", "New status (pitch, shaped, betting, active, blocked, building, done, archived)").option("--priority <priority>", "New priority").option("--appetite <appetite>", "New appetite").action(async (slug, id, opts) => {
1257
+ betsCmd.command("update <slug> <id>").description("Update a bet").option("--title <title>", "New title").option("--content <content>", "New content").option("--file <path>", "Read content from file").option("--status <status>", `New status \u2014 legacy-governed bets only; lifecycle-governed bets use \`odin bets transition\` (${STAGE_HELP})`).option("--priority <priority>", "New priority").option("--appetite <appetite>", "New appetite").action(async (slug, id, opts) => {
1255
1258
  const body = {};
1256
1259
  if (opts.title) body.title = opts.title;
1257
1260
  if (opts.file) body.content = readFileSync10(opts.file, "utf-8");
@@ -1274,11 +1277,56 @@ betsCmd.command("delete <slug> <id>").description("Delete a bet").action(async (
1274
1277
  if (res.error) process.exit(1);
1275
1278
  });
1276
1279
  betsCmd.addCommand(depsCmd);
1280
+ betsCmd.command("transition <slug> <id>").description("Apply a guarded lifecycle operation (never a raw status write)").requiredOption("--op <operation>", "start_shaping | approve_ready | handoff | complete | return_to_shaping | archive | correct_stage | open_blocker | resolve_blocker | set_queued | clear_queued").requiredOption("--expected-revision <n>", "lifecycle_revision from `odin bets get`").option("--to <stage>", `Target stage for handoff/correct_stage (${STAGE_HELP})`).option("--reason <text>", "Reason (required for return loops, skips and return_to_shaping)").option("--note <text>", "Optional correction/resolution note").option("--actor <label>", "Persona slug or handle recorded on the event", "cli").option("--actor-kind <kind>", "human | persona", "human").option("--idempotency-key <key>", "Reuse the same key when retrying after a timeout").option("--payload <json>", "Extra payload JSON merged last (check_token/exceptions, category, blocker_id, owner_persona\u2026)").action(async (slug, id, opts) => {
1281
+ let extra = {};
1282
+ if (opts.payload) {
1283
+ try {
1284
+ extra = JSON.parse(opts.payload);
1285
+ } catch {
1286
+ process.stderr.write("--payload must be valid JSON\n");
1287
+ process.exit(2);
1288
+ }
1289
+ }
1290
+ const payload = { ...opts.to ? { to_stage: opts.to } : {}, ...opts.reason ? { reason: opts.reason } : {}, ...opts.note ? { note: opts.note } : {}, ...extra };
1291
+ const body = { operation: opts.op, expected_revision: Number(opts.expectedRevision), idempotency_key: opts.idempotencyKey ?? randomUUID(), actor_label: opts.actor, actor_kind: opts.actorKind, payload };
1292
+ const res = await apiRequest(`/pebbles/${slug}/bets/${id}/lifecycle`, { method: "POST", body });
1293
+ process.stdout.write(JSON.stringify(res) + "\n");
1294
+ if (res.error) process.exit(1);
1295
+ });
1296
+ betsCmd.command("ready-check <slug> <id>").description("Evaluate Ready prerequisites").action(async (slug, id) => {
1297
+ const res = await apiRequest(`/pebbles/${slug}/bets/${id}/ready-check`);
1298
+ process.stdout.write(JSON.stringify(res) + "\n");
1299
+ if (res.error) process.exit(1);
1300
+ });
1301
+ betsCmd.command("activity <slug> <id>").description("Unified Activity (events + comments), newest first").option("--cursor <cursor>").option("--limit <n>", "1-100", "30").action(async (slug, id, opts) => {
1302
+ const res = await apiRequest(`/pebbles/${slug}/bets/${id}/activity`, { params: { ...opts.cursor ? { cursor: opts.cursor } : {}, limit: opts.limit } });
1303
+ process.stdout.write(JSON.stringify(res) + "\n");
1304
+ if (res.error) process.exit(1);
1305
+ });
1306
+ betsCmd.command("lifecycle-contract").description("Stages, transition matrix, Ready requirements, blocker categories").action(async () => {
1307
+ const res = await apiRequest(`/bet-lifecycle/contract`);
1308
+ process.stdout.write(JSON.stringify(res) + "\n");
1309
+ if (res.error) process.exit(1);
1310
+ });
1277
1311
 
1278
- // src/commands/bet-comments.ts
1312
+ // src/commands/tickets.ts
1279
1313
  import { Command as Command15 } from "commander";
1314
+ var ticketsCmd = new Command15("tickets").description("Resolve human-readable ticket IDs");
1315
+ 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) => {
1316
+ const res = await apiRequest(`/tickets/${encodeURIComponent(key.trim().toUpperCase())}`);
1317
+ process.stdout.write(JSON.stringify(res) + "\n");
1318
+ if (res.data?.legacy_ambiguous) {
1319
+ const others = res.data.candidates.filter((c) => c.entity_id !== res.data.entity_id);
1320
+ process.stderr.write(`note: ${key.toUpperCase()} was a duplicated legacy label; also matches ${others.map((c) => c.ticket_key ?? c.entity_id).join(", ")}
1321
+ `);
1322
+ }
1323
+ if (res.error) process.exit(1);
1324
+ });
1325
+
1326
+ // src/commands/bet-comments.ts
1327
+ import { Command as Command16 } from "commander";
1280
1328
  import { readFileSync as readFileSync11 } from "fs";
1281
- var betCommentsCmd = new Command15("bet-comments").description("Manage bet comments");
1329
+ var betCommentsCmd = new Command16("bet-comments").description("Manage bet comments");
1282
1330
  betCommentsCmd.command("list <slug> <betId>").description("List comments for a bet (oldest first)").action(async (slug, betId) => {
1283
1331
  const res = await apiRequest(`/pebbles/${slug}/bets/${betId}/comments`);
1284
1332
  process.stdout.write(JSON.stringify(res) + "\n");
@@ -1305,8 +1353,9 @@ betCommentsCmd.command("create <slug> <betId> [body]").description("Post a comme
1305
1353
  });
1306
1354
 
1307
1355
  // src/commands/tasks.ts
1308
- import { Command as Command16 } from "commander";
1309
- var tasksCmd = new Command16("tasks").description("Manage tasks");
1356
+ import { Command as Command17 } from "commander";
1357
+ import { readFileSync as readFileSync12 } from "fs";
1358
+ var tasksCmd = new Command17("tasks").description("Manage tasks");
1310
1359
  withListFlags(
1311
1360
  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
1361
  ).action(async (slug, opts) => {
@@ -1327,10 +1376,10 @@ tasksCmd.command("get <slug> <id>").description("Get a task by ID").action(async
1327
1376
  process.stdout.write(JSON.stringify(res) + "\n");
1328
1377
  if (res.error) process.exit(1);
1329
1378
  });
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) => {
1379
+ 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
1380
  const res = await apiRequest(`/pebbles/${slug}/tasks`, {
1332
1381
  method: "POST",
1333
- body: { title, source: opts.source, description: opts.description, bet_id: opts.bet, status: opts.status, priority: opts.priority }
1382
+ body: { title, source: opts.source, description: opts.description, bet_id: opts.bet, status: opts.status, priority: opts.priority, follow_up_of: opts.followUpOf }
1334
1383
  });
1335
1384
  process.stdout.write(JSON.stringify(res) + "\n");
1336
1385
  if (res.error) process.exit(1);
@@ -1349,6 +1398,15 @@ tasksCmd.command("update <slug> <id>").description("Update a task").option("--ti
1349
1398
  process.stdout.write(JSON.stringify(res) + "\n");
1350
1399
  if (res.error) process.exit(1);
1351
1400
  });
1401
+ 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) => {
1402
+ const content = opts.file ? readFileSync12(opts.file, "utf-8") : opts.content;
1403
+ const res = await apiRequest(`/pebbles/${slug}/tasks/${id}/promote`, {
1404
+ method: "POST",
1405
+ body: { content, status: opts.status, priority: opts.priority }
1406
+ });
1407
+ process.stdout.write(JSON.stringify(res) + "\n");
1408
+ if (res.error) process.exit(1);
1409
+ });
1352
1410
  tasksCmd.command("delete <slug> <id>").description("Delete a task").action(async (slug, id) => {
1353
1411
  const res = await apiRequest(`/pebbles/${slug}/tasks/${id}`, {
1354
1412
  method: "DELETE"
@@ -1358,9 +1416,9 @@ tasksCmd.command("delete <slug> <id>").description("Delete a task").action(async
1358
1416
  });
1359
1417
 
1360
1418
  // 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");
1419
+ import { Command as Command18 } from "commander";
1420
+ import { readFileSync as readFileSync13 } from "fs";
1421
+ var roadmapsCmd = new Command18("roadmaps").description("Manage roadmaps");
1364
1422
  roadmapsCmd.command("list <slug>").description("List roadmaps for a pebble").option("--status <status>", "Filter by status").action(async (slug, opts) => {
1365
1423
  const params = {};
1366
1424
  if (opts.status) params.status = opts.status;
@@ -1374,7 +1432,7 @@ roadmapsCmd.command("get <slug> <id>").description("Get a roadmap by ID (include
1374
1432
  if (res.error) process.exit(1);
1375
1433
  });
1376
1434
  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 ?? "";
1435
+ const content = opts.file ? readFileSync13(opts.file, "utf-8") : opts.content ?? "";
1378
1436
  const res = await apiRequest(`/pebbles/${slug}/roadmaps`, {
1379
1437
  method: "POST",
1380
1438
  body: { title, source: opts.source, content, status: opts.status }
@@ -1385,7 +1443,7 @@ roadmapsCmd.command("create <slug> <title>").description("Create a roadmap").opt
1385
1443
  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
1444
  const body = {};
1387
1445
  if (opts.title) body.title = opts.title;
1388
- if (opts.file) body.content = readFileSync12(opts.file, "utf-8");
1446
+ if (opts.file) body.content = readFileSync13(opts.file, "utf-8");
1389
1447
  else if (opts.content) body.content = opts.content;
1390
1448
  if (opts.status) body.status = opts.status;
1391
1449
  const res = await apiRequest(`/pebbles/${slug}/roadmaps/${id}`, {
@@ -1404,11 +1462,11 @@ roadmapsCmd.command("delete <slug> <id>").description("Delete a roadmap").action
1404
1462
  });
1405
1463
 
1406
1464
  // src/commands/persona-runs.ts
1407
- import { Command as Command18 } from "commander";
1408
- import { readFileSync as readFileSync13 } from "fs";
1465
+ import { Command as Command19 } from "commander";
1466
+ import { readFileSync as readFileSync14 } from "fs";
1409
1467
  import { join as join2 } from "path";
1410
1468
  import { homedir as homedir2 } from "os";
1411
- var personaRunsCmd = new Command18("persona-runs").description(
1469
+ var personaRunsCmd = new Command19("persona-runs").description(
1412
1470
  "Manage per-persona per-session token usage rows"
1413
1471
  );
1414
1472
  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 +1490,7 @@ personaRunsCmd.command("get <slug> <id>").description("Get a persona run by id")
1432
1490
  if (res.error) process.exit(1);
1433
1491
  });
1434
1492
  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"));
1493
+ const body = JSON.parse(readFileSync14(opts.file, "utf-8"));
1436
1494
  const res = await apiRequest(`/pebbles/${slug}/persona-runs`, {
1437
1495
  method: "POST",
1438
1496
  body
@@ -1444,7 +1502,7 @@ personaRunsCmd.command("capture").description("Read marker + transcript, compute
1444
1502
  const markerPath = opts.marker ?? join2(homedir2(), ".claude", "odin-context", `${opts.sessionId}.json`);
1445
1503
  let marker = {};
1446
1504
  try {
1447
- marker = JSON.parse(readFileSync13(markerPath, "utf-8"));
1505
+ marker = JSON.parse(readFileSync14(markerPath, "utf-8"));
1448
1506
  } catch {
1449
1507
  process.exit(0);
1450
1508
  }
@@ -1454,7 +1512,7 @@ personaRunsCmd.command("capture").description("Read marker + transcript, compute
1454
1512
  let input = 0, output = 0, cacheRead = 0, cacheCreate = 0;
1455
1513
  let minTs = null, maxTs = null;
1456
1514
  try {
1457
- const raw = readFileSync13(opts.transcript, "utf-8");
1515
+ const raw = readFileSync14(opts.transcript, "utf-8");
1458
1516
  for (const line of raw.split("\n")) {
1459
1517
  if (!line.trim()) continue;
1460
1518
  let obj;
@@ -1505,9 +1563,9 @@ personaRunsCmd.command("capture").description("Read marker + transcript, compute
1505
1563
  });
1506
1564
 
1507
1565
  // 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");
1566
+ import { Command as Command20 } from "commander";
1567
+ import { readFileSync as readFileSync15 } from "fs";
1568
+ var blogCmd = new Command20("blog").description("Manage blog posts");
1511
1569
  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
1570
  const params = {};
1513
1571
  if (opts.status) params.status = opts.status;
@@ -1526,7 +1584,7 @@ blogCmd.command("get <slug> <idOrSlug>").description("Get a blog post by UUID or
1526
1584
  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
1585
  let blocks = [];
1528
1586
  if (opts.file) {
1529
- blocks = JSON.parse(readFileSync14(opts.file, "utf-8"));
1587
+ blocks = JSON.parse(readFileSync15(opts.file, "utf-8"));
1530
1588
  } else if (opts.blocks) {
1531
1589
  blocks = JSON.parse(opts.blocks);
1532
1590
  }
@@ -1550,7 +1608,7 @@ blogCmd.command("update <slug> <id>").description("Update a blog post").option("
1550
1608
  const body = {};
1551
1609
  if (opts.title) body.title = opts.title;
1552
1610
  if (opts.postSlug) body.slug = opts.postSlug;
1553
- if (opts.file) body.blocks = JSON.parse(readFileSync14(opts.file, "utf-8"));
1611
+ if (opts.file) body.blocks = JSON.parse(readFileSync15(opts.file, "utf-8"));
1554
1612
  else if (opts.blocks) body.blocks = JSON.parse(opts.blocks);
1555
1613
  if (opts.status) body.status = opts.status;
1556
1614
  if (opts.scheduledPublishAt !== void 0)
@@ -1572,8 +1630,8 @@ blogCmd.command("delete <slug> <id>").description("Delete a blog post").action(a
1572
1630
  });
1573
1631
 
1574
1632
  // 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");
1633
+ import { Command as Command21 } from "commander";
1634
+ var apiKeysCmd = new Command21("api-keys").description("Manage API keys for server-to-server access");
1577
1635
  apiKeysCmd.command("list <slug>").description("List API keys for a pebble").action(async (slug) => {
1578
1636
  const res = await apiRequest(`/pebbles/${slug}/api-keys`);
1579
1637
  process.stdout.write(JSON.stringify(res) + "\n");
@@ -1615,7 +1673,7 @@ apiKeysCmd.command("delete <slug> <id>").description("Permanently delete an API
1615
1673
  });
1616
1674
 
1617
1675
  // src/commands/pebble/index.ts
1618
- import { Command as Command21 } from "commander";
1676
+ import { Command as Command22 } from "commander";
1619
1677
 
1620
1678
  // src/commands/pebble/provisioner.ts
1621
1679
  async function runProvisioner(ctx, steps) {
@@ -2028,7 +2086,7 @@ Hand off: /odin:eng-manager bet:${bet.data.id}
2028
2086
  }
2029
2087
 
2030
2088
  // src/commands/pebble/index.ts
2031
- var pebbleCmd = new Command21("pebble").description(
2089
+ var pebbleCmd = new Command22("pebble").description(
2032
2090
  "Provision and initialize pebbles (eng-infra, run-once)"
2033
2091
  );
2034
2092
  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 +2107,7 @@ pebbleCmd.command("provision <provider> <slug>").description("Run one provider s
2049
2107
  });
2050
2108
 
2051
2109
  // src/commands/create/index.ts
2052
- import { Command as Command22 } from "commander";
2110
+ import { Command as Command23 } from "commander";
2053
2111
 
2054
2112
  // src/commands/create/topology.ts
2055
2113
  var ENVS = ["prd", "stg", "dev"];
@@ -2514,22 +2572,22 @@ function assembleInputs(ctx) {
2514
2572
  }
2515
2573
 
2516
2574
  // src/commands/create/index.ts
2517
- var createCmd = new Command22("create").description(
2575
+ var createCmd = new Command23("create").description(
2518
2576
  "Create and provision a new project end to end"
2519
2577
  );
2520
2578
  createCmd.command("project [name]").description("Guided self-serve project + parallel-env provisioning").option("--dry-run", "Print the plan; mutate nothing").action(
2521
2579
  async (name, opts) => process.exit(await runCreate(name, { dryRun: opts.dryRun }))
2522
2580
  );
2523
- var setupCmd = new Command22("setup").description("Alias for `create project`").arguments("[name]").option("--dry-run", "Print the plan; mutate nothing").action(
2581
+ var setupCmd = new Command23("setup").description("Alias for `create project`").arguments("[name]").option("--dry-run", "Print the plan; mutate nothing").action(
2524
2582
  async (name, opts) => process.exit(await runCreate(name, { dryRun: opts.dryRun }))
2525
2583
  );
2526
2584
 
2527
2585
  // src/index.ts
2528
- import { readFileSync as readFileSync15 } from "fs";
2586
+ import { readFileSync as readFileSync16 } from "fs";
2529
2587
  import { fileURLToPath } from "url";
2530
2588
  import { dirname as dirname2, resolve as resolve2 } from "path";
2531
2589
  var __dirname = dirname2(fileURLToPath(import.meta.url));
2532
- var pkg = JSON.parse(readFileSync15(resolve2(__dirname, "../package.json"), "utf-8"));
2590
+ var pkg = JSON.parse(readFileSync16(resolve2(__dirname, "../package.json"), "utf-8"));
2533
2591
  var GOLD = "\x1B[33m";
2534
2592
  var DIM = "\x1B[2m";
2535
2593
  var RESET = "\x1B[0m";
@@ -2547,7 +2605,7 @@ ${RESET}
2547
2605
  ${DIM}${cwd}${RESET}
2548
2606
  `);
2549
2607
  }
2550
- var program = new Command23();
2608
+ var program = new Command24();
2551
2609
  program.name("odin").description("CLI for Odin \u2014 the knowledge backbone for Pebble House").version(VERSION);
2552
2610
  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
2611
  printBanner();
@@ -2576,6 +2634,7 @@ program.addCommand(rulesCmd);
2576
2634
  program.addCommand(briefsCmd);
2577
2635
  program.addCommand(blueprintsCmd);
2578
2636
  program.addCommand(betsCmd);
2637
+ program.addCommand(ticketsCmd);
2579
2638
  program.addCommand(betCommentsCmd);
2580
2639
  program.addCommand(tasksCmd);
2581
2640
  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.1",
4
4
  "type": "module",
5
5
  "description": "CLI for Odin — the knowledge backbone for Pebble House",
6
6
  "bin": {