@pebblehouse/odin-cli 0.14.1 → 0.16.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 (2) hide show
  1. package/dist/index.js +159 -21
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -208,7 +208,7 @@ async function apiRequest(path, options = {}) {
208
208
  const { method = "GET", body, params } = options;
209
209
  const token = await getValidToken();
210
210
  let url = `${BASE_URL}/api${path}`;
211
- if (params) {
211
+ if (params && Object.keys(params).length > 0) {
212
212
  const searchParams = new URLSearchParams(params);
213
213
  url += `?${searchParams.toString()}`;
214
214
  }
@@ -262,8 +262,13 @@ pebblesCmd.command("update <slug>").description("Update a pebble").option("--slu
262
262
  import { Command as Command2 } from "commander";
263
263
  import { readFileSync as readFileSync3 } from "fs";
264
264
  var docsCmd = new Command2("docs").description("Manage documents");
265
- docsCmd.command("list <slug>").description("List documents for a pebble").action(async (slug) => {
266
- const res = await apiRequest(`/pebbles/${slug}/documents`);
265
+ docsCmd.command("list <slug>").description("List documents for a pebble").option(
266
+ "--doc-type <type>",
267
+ "Filter by document type (architecture, product, api, readme, spec, research, marketing, other, reference, org_chart, design, schema)"
268
+ ).action(async (slug, opts) => {
269
+ const params = {};
270
+ if (opts.docType) params.doc_type = opts.docType;
271
+ const res = await apiRequest(`/pebbles/${slug}/documents`, { params });
267
272
  process.stdout.write(JSON.stringify(res) + "\n");
268
273
  if (res.error) process.exit(1);
269
274
  });
@@ -307,11 +312,119 @@ docsCmd.command("update <slug> <type>").description("Update document content").r
307
312
 
308
313
  // src/commands/decisions.ts
309
314
  import { Command as Command3 } from "commander";
310
- var decisionsCmd = new Command3("decisions").description("Manage decisions");
311
- decisionsCmd.command("list <slug>").description("List decisions for a pebble").action(async (slug) => {
312
- const res = await apiRequest(`/pebbles/${slug}/decisions`);
315
+
316
+ // src/list-flags.ts
317
+ var LIST_DEFAULT_LIMIT = 50;
318
+ var LIST_MAX_LIMIT = 100;
319
+ function withListFlags(cmd) {
320
+ return cmd.option("--limit <n>", `Rows per request (max ${LIST_MAX_LIMIT})`).option("--offset <n>", "Rows to skip").option("--page <n>", "1-based page number; shorthand for --offset (page - 1) * limit");
321
+ }
322
+ function parseInteger(raw) {
323
+ if (raw === void 0 || raw === "") return null;
324
+ const n = Number(raw);
325
+ if (!Number.isFinite(n)) return null;
326
+ return Math.trunc(n);
327
+ }
328
+ function resolveListParams(opts) {
329
+ const notices = [];
330
+ const params = {};
331
+ if (opts.page !== void 0 && opts.offset !== void 0) {
332
+ return {
333
+ params: {},
334
+ limit: LIST_DEFAULT_LIMIT,
335
+ offset: 0,
336
+ notices: [],
337
+ fatal: "--page and --offset are mutually exclusive \u2014 use one"
338
+ };
339
+ }
340
+ let effectiveLimit = LIST_DEFAULT_LIMIT;
341
+ if (opts.limit !== void 0) {
342
+ const raw = parseInteger(opts.limit);
343
+ if (raw === null) {
344
+ notices.push(`--limit "${opts.limit}" is not a number \u2014 ignoring`);
345
+ } else if (raw > LIST_MAX_LIMIT) {
346
+ notices.push(`limit ${raw} exceeds the server maximum of ${LIST_MAX_LIMIT} \u2014 using ${LIST_MAX_LIMIT}`);
347
+ effectiveLimit = LIST_MAX_LIMIT;
348
+ params.limit = String(LIST_MAX_LIMIT);
349
+ } else if (raw < 1) {
350
+ notices.push(`--limit must be 1 or greater \u2014 using 1`);
351
+ effectiveLimit = 1;
352
+ params.limit = "1";
353
+ } else {
354
+ effectiveLimit = raw;
355
+ params.limit = String(raw);
356
+ }
357
+ }
358
+ let effectiveOffset = 0;
359
+ if (opts.offset !== void 0) {
360
+ const raw = parseInteger(opts.offset);
361
+ if (raw === null) {
362
+ notices.push(`--offset "${opts.offset}" is not a number \u2014 ignoring`);
363
+ } else if (raw < 0) {
364
+ notices.push(`--offset must be 0 or greater \u2014 using 0`);
365
+ params.offset = "0";
366
+ } else {
367
+ effectiveOffset = raw;
368
+ params.offset = String(raw);
369
+ }
370
+ } else if (opts.page !== void 0) {
371
+ const raw = parseInteger(opts.page);
372
+ if (raw === null) {
373
+ notices.push(`--page "${opts.page}" is not a number \u2014 ignoring`);
374
+ } else {
375
+ const page = raw < 1 ? 1 : raw;
376
+ if (raw < 1) notices.push(`--page must be 1 or greater \u2014 using page 1`);
377
+ effectiveOffset = (page - 1) * effectiveLimit;
378
+ params.offset = String(effectiveOffset);
379
+ }
380
+ }
381
+ return { params, limit: effectiveLimit, offset: effectiveOffset, notices, fatal: null };
382
+ }
383
+ function renderListFooter(noun, rowCount, total, offset, limit) {
384
+ if (total === void 0) {
385
+ return `\u2014 showing ${rowCount} ${noun} (total unavailable)`;
386
+ }
387
+ if (rowCount === 0) {
388
+ return offset > 0 ? `\u2014 no ${noun} in this window (${total} total) \xB7 try --offset 0` : `\u2014 no ${noun}`;
389
+ }
390
+ const first = offset + 1;
391
+ const last = offset + rowCount;
392
+ if (offset === 0 && rowCount >= total) {
393
+ return `\u2014 showing all ${rowCount} ${noun}`;
394
+ }
395
+ const nextOffset = offset + limit;
396
+ const more = last < total ? ` \xB7 more: --offset ${nextOffset}` : "";
397
+ return `\u2014 showing ${rowCount} of ${total} ${noun} (rows ${first}\u2013${last})${more}`;
398
+ }
399
+ function emitListResult(res, noun, resolved) {
313
400
  process.stdout.write(JSON.stringify(res) + "\n");
401
+ for (const notice of resolved.notices) {
402
+ process.stderr.write(notice + "\n");
403
+ }
404
+ const rowCount = Array.isArray(res.data) ? res.data.length : 0;
405
+ const footer = renderListFooter(noun, rowCount, res.total, resolved.offset, resolved.limit);
406
+ if (footer) process.stderr.write(footer + "\n");
314
407
  if (res.error) process.exit(1);
408
+ }
409
+
410
+ // src/commands/decisions.ts
411
+ var decisionsCmd = new Command3("decisions").description("Manage decisions");
412
+ withListFlags(
413
+ decisionsCmd.command("list <slug>").description("List decisions for a pebble").option(
414
+ "--status <status>",
415
+ "Filter by status (active, superseded, all). Defaults to active on the server."
416
+ )
417
+ ).action(async (slug, opts) => {
418
+ const resolved = resolveListParams(opts);
419
+ if (resolved.fatal) {
420
+ process.stderr.write(resolved.fatal + "\n");
421
+ process.exit(1);
422
+ return;
423
+ }
424
+ const params = { ...resolved.params };
425
+ if (opts.status) params.status = opts.status;
426
+ const res = await apiRequest(`/pebbles/${slug}/decisions`, { params });
427
+ emitListResult(res, "decisions", resolved);
315
428
  });
316
429
  decisionsCmd.command("get <slug> <id>").description("Get a decision by ID").action(async (slug, id) => {
317
430
  const res = await apiRequest(`/pebbles/${slug}/decisions/${id}`);
@@ -379,7 +492,7 @@ decisionsCmd.command("log <slug> <title>").description("Log a decision").option(
379
492
  const threeDaysAgo = /* @__PURE__ */ new Date();
380
493
  threeDaysAgo.setDate(threeDaysAgo.getDate() - 3);
381
494
  const recentRes = await apiRequest(`/pebbles/${slug}/decisions`, {
382
- params: { limit: "100" }
495
+ params: { limit: String(LIST_MAX_LIMIT) }
383
496
  });
384
497
  if (recentRes.data) {
385
498
  const recentActive = recentRes.data.filter(
@@ -444,8 +557,12 @@ var searchCmd = new Command5("search").description("Full-text search across Odin
444
557
  import { Command as Command6 } from "commander";
445
558
  import { readFileSync as readFileSync4 } from "fs";
446
559
  var plansCmd = new Command6("plans").description("Manage plans");
447
- plansCmd.command("list <slug>").description("List plans for a pebble").action(async (slug) => {
448
- const res = await apiRequest(`/pebbles/${slug}/plans`);
560
+ plansCmd.command("list <slug>").description("List plans for a pebble").option("--status <status>", "Filter by status (active, done, archived)").option("--bet <id>", "Filter to plans linked to this bet").option("--task <id>", "Filter to plans linked to this task").action(async (slug, opts) => {
561
+ const params = {};
562
+ if (opts.status) params.status = opts.status;
563
+ if (opts.bet) params.bet_id = opts.bet;
564
+ if (opts.task) params.task_id = opts.task;
565
+ const res = await apiRequest(`/pebbles/${slug}/plans`, { params });
449
566
  process.stdout.write(JSON.stringify(res) + "\n");
450
567
  if (res.error) process.exit(1);
451
568
  });
@@ -558,12 +675,19 @@ plansCmd.command("remove-phase-link <slug> <planId> <phaseId> <linkId>").descrip
558
675
  import { Command as Command7 } from "commander";
559
676
  import { readFileSync as readFileSync5 } from "fs";
560
677
  var artifactsCmd = new Command7("artifacts").description("Manage artifacts");
561
- artifactsCmd.command("list <slug>").description("List artifacts for a pebble").option("--source <source>", "Filter by source (claude-code, superpowers, etc.)").action(async (slug, opts) => {
562
- const params = {};
678
+ withListFlags(
679
+ artifactsCmd.command("list <slug>").description("List artifacts for a pebble").option("--source <source>", "Filter by source (claude-code, superpowers, etc.)")
680
+ ).action(async (slug, opts) => {
681
+ const resolved = resolveListParams(opts);
682
+ if (resolved.fatal) {
683
+ process.stderr.write(resolved.fatal + "\n");
684
+ process.exit(1);
685
+ return;
686
+ }
687
+ const params = { ...resolved.params };
563
688
  if (opts.source) params.source = opts.source;
564
689
  const res = await apiRequest(`/pebbles/${slug}/artifacts`, { params });
565
- process.stdout.write(JSON.stringify(res) + "\n");
566
- if (res.error) process.exit(1);
690
+ emitListResult(res, "artifacts", resolved);
567
691
  });
568
692
  artifactsCmd.command("get <slug> <id>").description("Get an artifact by ID").action(async (slug, id) => {
569
693
  const res = await apiRequest(`/pebbles/${slug}/artifacts/${id}`);
@@ -929,12 +1053,19 @@ depsCmd.command("remove <slug> <bet-id>").description("Remove a blocked-by depen
929
1053
 
930
1054
  // src/commands/bets.ts
931
1055
  var betsCmd = new Command14("bets").description("Manage bets");
932
- betsCmd.command("list <slug>").description("List bets for a pebble").option("--status <status>", "Filter by status (pitch, shaped, betting, active, blocked, building, done, archived)").action(async (slug, opts) => {
933
- const params = {};
1056
+ withListFlags(
1057
+ betsCmd.command("list <slug>").description("List bets for a pebble").option("--status <status>", "Filter by status (pitch, shaped, betting, active, blocked, building, done, archived)")
1058
+ ).action(async (slug, opts) => {
1059
+ const resolved = resolveListParams(opts);
1060
+ if (resolved.fatal) {
1061
+ process.stderr.write(resolved.fatal + "\n");
1062
+ process.exit(1);
1063
+ return;
1064
+ }
1065
+ const params = { ...resolved.params };
934
1066
  if (opts.status) params.status = opts.status;
935
1067
  const res = await apiRequest(`/pebbles/${slug}/bets`, { params });
936
- process.stdout.write(JSON.stringify(res) + "\n");
937
- if (res.error) process.exit(1);
1068
+ emitListResult(res, "bets", resolved);
938
1069
  });
939
1070
  betsCmd.command("get <slug> <id>").description("Get a bet by ID").action(async (slug, id) => {
940
1071
  const res = await apiRequest(`/pebbles/${slug}/bets/${id}`);
@@ -1006,13 +1137,20 @@ betCommentsCmd.command("create <slug> <betId> [body]").description("Post a comme
1006
1137
  // src/commands/tasks.ts
1007
1138
  import { Command as Command16 } from "commander";
1008
1139
  var tasksCmd = new Command16("tasks").description("Manage tasks");
1009
- tasksCmd.command("list <slug>").description("List tasks for a pebble").option("--status <status>", "Filter by status").option("--bet <bet-id>", "Filter by bet ID").action(async (slug, opts) => {
1010
- const params = {};
1140
+ withListFlags(
1141
+ tasksCmd.command("list <slug>").description("List tasks for a pebble").option("--status <status>", "Filter by status").option("--bet <bet-id>", "Filter by bet ID")
1142
+ ).action(async (slug, opts) => {
1143
+ const resolved = resolveListParams(opts);
1144
+ if (resolved.fatal) {
1145
+ process.stderr.write(resolved.fatal + "\n");
1146
+ process.exit(1);
1147
+ return;
1148
+ }
1149
+ const params = { ...resolved.params };
1011
1150
  if (opts.status) params.status = opts.status;
1012
1151
  if (opts.bet) params.bet_id = opts.bet;
1013
1152
  const res = await apiRequest(`/pebbles/${slug}/tasks`, { params });
1014
- process.stdout.write(JSON.stringify(res) + "\n");
1015
- if (res.error) process.exit(1);
1153
+ emitListResult(res, "tasks", resolved);
1016
1154
  });
1017
1155
  tasksCmd.command("get <slug> <id>").description("Get a task by ID").action(async (slug, id) => {
1018
1156
  const res = await apiRequest(`/pebbles/${slug}/tasks/${id}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pebblehouse/odin-cli",
3
- "version": "0.14.1",
3
+ "version": "0.16.0",
4
4
  "type": "module",
5
5
  "description": "CLI for Odin — the knowledge backbone for Pebble House",
6
6
  "bin": {