checkbox-cli 2.0.0 → 2.0.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 (2) hide show
  1. package/dist/checkbox.js +154 -183
  2. package/package.json +1 -1
package/dist/checkbox.js CHANGED
@@ -20,7 +20,7 @@ import { join } from 'path';
20
20
  // ── Config ──────────────────────────────────────────────────────────
21
21
  const CONFIG_DIR = join(homedir(), '.checkbox');
22
22
  const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
23
- const VERSION = '2.0.0';
23
+ const VERSION = '2.0.1';
24
24
  function loadConfig() {
25
25
  try {
26
26
  if (!existsSync(CONFIG_FILE))
@@ -44,7 +44,6 @@ function saveConfig(cfg) {
44
44
  mkdirSync(CONFIG_DIR, { recursive: true });
45
45
  writeFileSync(CONFIG_FILE, JSON.stringify(cfg, null, 2));
46
46
  }
47
- // Allow env vars to override config file
48
47
  function resolveConfig() {
49
48
  const envKey = process.env.CHECKBOX_API_KEY || process.env.CHECKBOX_API_TOKEN;
50
49
  const fileCfg = loadConfig();
@@ -59,6 +58,20 @@ function resolveConfig() {
59
58
  return fileCfg;
60
59
  }
61
60
  // ── API ─────────────────────────────────────────────────────────────
61
+ /** Extract a human-readable error message from any API error shape */
62
+ function extractError(obj) {
63
+ if (!obj)
64
+ return 'Unknown error';
65
+ if (typeof obj === 'string')
66
+ return obj;
67
+ if (typeof obj.error === 'string')
68
+ return obj.error;
69
+ if (obj.error?.message)
70
+ return obj.error.message;
71
+ if (obj.message)
72
+ return obj.message;
73
+ return JSON.stringify(obj);
74
+ }
62
75
  function api(cfg) {
63
76
  const headers = {
64
77
  'Content-Type': 'application/json',
@@ -70,22 +83,58 @@ function api(cfg) {
70
83
  headers,
71
84
  body: JSON.stringify(body),
72
85
  });
86
+ const json = await res.json().catch(() => null);
73
87
  if (!res.ok) {
74
- const err = await res.json().catch(() => ({}));
75
- throw new Error(err.error || `Request failed (${res.status})`);
88
+ throw new Error(extractError(json) || `Request failed (${res.status})`);
89
+ }
90
+ if (json && json.success === false) {
91
+ throw new Error(extractError(json) || 'Request failed');
76
92
  }
77
- return res.json();
93
+ return json;
78
94
  }
79
95
  async function get(path) {
80
96
  const res = await fetch(`${cfg.apiUrl}${path}`, { headers });
97
+ const json = await res.json().catch(() => null);
81
98
  if (!res.ok) {
82
- const err = await res.json().catch(() => ({}));
83
- throw new Error(err.error || `Request failed (${res.status})`);
99
+ throw new Error(extractError(json) || `Request failed (${res.status})`);
84
100
  }
85
- return res.json();
101
+ if (json && json.success === false) {
102
+ throw new Error(extractError(json) || 'Request failed');
103
+ }
104
+ return json;
86
105
  }
87
106
  return { post, get };
88
107
  }
108
+ // ── Query helpers ───────────────────────────────────────────────────
109
+ // The actual query types are: list_entities, get_entity, list_my_organizations,
110
+ // get_task_states, workspace_graph, entity_discovery, schema_discovery,
111
+ // permission_discovery, list_approvals.
112
+ // There is NO list_plans or list_tasks — use list_entities with entity_type.
113
+ async function listEntities(post, orgId, entityType) {
114
+ const res = await post('/api/v2/query', {
115
+ type: 'list_entities',
116
+ organization_id: orgId,
117
+ payload: { entity_type: entityType, organization_id: orgId },
118
+ });
119
+ // list_entities returns { success, data: [...], meta }
120
+ return Array.isArray(res.data) ? res.data : [];
121
+ }
122
+ async function searchEntities(post, orgId, search, limit = 20) {
123
+ // entity_discovery returns { success, data: { organization_id, entities: [...] } }
124
+ const res = await post('/api/v2/introspection/entities', {
125
+ organization_id: orgId,
126
+ search,
127
+ limit,
128
+ });
129
+ return res.data?.entities || [];
130
+ }
131
+ async function getWorkspaceNodes(post, orgId) {
132
+ // workspace returns { success, data: { organization_id, nodes: [...], edges: [...] } }
133
+ const res = await post('/api/v2/introspection/workspace', {
134
+ organization_id: orgId,
135
+ });
136
+ return res.data?.nodes || [];
137
+ }
89
138
  // ── Formatters ──────────────────────────────────────────────────────
90
139
  function truncate(s, max) {
91
140
  return s.length > max ? s.slice(0, max - 1) + '\u2026' : s;
@@ -135,16 +184,13 @@ async function runSetup(existingConfig) {
135
184
  orgId: '',
136
185
  orgName: '',
137
186
  };
138
- // Test the key & fetch organizations
139
187
  const s = p.spinner();
140
188
  s.start('Connecting to Checkbox...');
141
189
  let orgs = [];
142
190
  try {
143
191
  const { post } = api(cfg);
144
192
  const res = await post('/api/v2/query', { type: 'list_my_organizations', payload: {} });
145
- if (!res.success || !res.data)
146
- throw new Error(res.error?.message || 'Failed to fetch organizations');
147
- orgs = res.data;
193
+ orgs = Array.isArray(res.data) ? res.data : [];
148
194
  s.stop(`Connected! Found ${orgs.length} workspace${orgs.length === 1 ? '' : 's'}`);
149
195
  }
150
196
  catch (err) {
@@ -154,7 +200,6 @@ async function runSetup(existingConfig) {
154
200
  p.outro(pc.dim('Try again with a valid key.'));
155
201
  process.exit(1);
156
202
  }
157
- // Pick workspace
158
203
  if (orgs.length === 0) {
159
204
  p.log.warn('No workspaces found. Create one at checkbox.my first.');
160
205
  saveConfig(cfg);
@@ -193,7 +238,7 @@ async function runSetup(existingConfig) {
193
238
  }
194
239
  // ── Interactive Menu ────────────────────────────────────────────────
195
240
  async function interactiveMenu(cfg) {
196
- const { post, get } = api(cfg);
241
+ const { post } = api(cfg);
197
242
  console.log();
198
243
  p.intro(pc.bgCyan(pc.black(' Checkbox ')) +
199
244
  pc.dim(` v${VERSION}`) +
@@ -217,20 +262,13 @@ async function interactiveMenu(cfg) {
217
262
  return;
218
263
  }
219
264
  switch (action) {
220
- case 'plans':
221
- return menuPlans(cfg, post);
222
- case 'tasks':
223
- return menuTasks(cfg, post);
224
- case 'status':
225
- return menuStatus(cfg, post);
226
- case 'search':
227
- return menuSearch(cfg, post);
228
- case 'complete':
229
- return menuComplete(cfg, post);
230
- case 'create-task':
231
- return menuCreateTask(cfg, post);
232
- case 'switch':
233
- return menuSwitch(cfg, post);
265
+ case 'plans': return menuPlans(cfg, post);
266
+ case 'tasks': return menuTasks(cfg, post);
267
+ case 'status': return menuStatus(cfg, post);
268
+ case 'search': return menuSearch(cfg, post);
269
+ case 'complete': return menuComplete(cfg, post);
270
+ case 'create-task': return menuCreateTask(cfg, post);
271
+ case 'switch': return menuSwitch(cfg, post);
234
272
  case 'setup':
235
273
  await runSetup(cfg);
236
274
  return;
@@ -241,27 +279,19 @@ async function menuPlans(cfg, post) {
241
279
  const s = p.spinner();
242
280
  s.start('Loading plans...');
243
281
  try {
244
- const res = await post('/api/v2/query', {
245
- type: 'list_plans',
246
- organization_id: cfg.orgId,
247
- payload: { organization_id: cfg.orgId },
248
- });
249
- if (!res.success)
250
- throw new Error(res.error?.message || 'Failed');
251
- const plans = res.data || [];
282
+ const plans = await listEntities(post, cfg.orgId, 'plan');
252
283
  s.stop(`Found ${plans.length} plan${plans.length === 1 ? '' : 's'}`);
253
284
  if (plans.length === 0) {
254
285
  p.log.info('No plans yet. Create one at checkbox.my');
255
- p.outro('');
256
- return;
286
+ return interactiveMenu(cfg);
257
287
  }
258
288
  const lines = plans.map((plan) => {
259
289
  const name = truncate(plan.name || 'Untitled', 40);
260
- const created = plan.created_at ? formatDate(plan.created_at) : '';
290
+ const meta = plan.metadata || {};
291
+ const created = meta.created_at ? formatDate(meta.created_at) : '';
261
292
  return `${pc.cyan(pad(name, 42))} ${pc.dim(created)}`;
262
293
  });
263
294
  p.note(lines.join('\n'), `Plans in ${cfg.orgName}`);
264
- // Offer drill-down
265
295
  const selected = await p.select({
266
296
  message: 'Select a plan to view its tasks',
267
297
  options: [
@@ -275,33 +305,36 @@ async function menuPlans(cfg, post) {
275
305
  if (p.isCancel(selected) || selected === '__back') {
276
306
  return interactiveMenu(cfg);
277
307
  }
278
- return menuPlanTasks(cfg, post, selected, plans.find((p) => p.id === selected)?.name || '');
308
+ return menuPlanTasks(cfg, post, selected, plans.find((pl) => pl.id === selected)?.name || '');
279
309
  }
280
310
  catch (err) {
281
311
  s.stop(pc.red('Failed'));
282
- p.log.error(err.message);
312
+ p.log.error(err.message || 'Could not load plans');
313
+ return interactiveMenu(cfg);
283
314
  }
284
315
  }
285
316
  async function menuPlanTasks(cfg, post, planId, planName) {
286
317
  const s = p.spinner();
287
318
  s.start('Loading tasks...');
288
319
  try {
320
+ // Search entities by plan — use entity_discovery with search for plan-specific tasks
321
+ // Or query list_entities for tasks and filter by parent_id
289
322
  const res = await post('/api/v2/query', {
290
- type: 'list_tasks',
323
+ type: 'list_entities',
291
324
  organization_id: cfg.orgId,
292
- payload: { organization_id: cfg.orgId, plan_id: planId },
325
+ payload: { entity_type: 'task', organization_id: cfg.orgId },
293
326
  });
294
- if (!res.success)
295
- throw new Error(res.error?.message || 'Failed');
296
- const tasks = res.data || [];
327
+ const allTasks = Array.isArray(res.data) ? res.data : [];
328
+ // Filter tasks that belong to this plan via parent_id chain
329
+ const tasks = allTasks.filter((t) => t.parent_id === planId || t.metadata?.plan_id === planId);
297
330
  s.stop(`${tasks.length} task${tasks.length === 1 ? '' : 's'} in ${pc.cyan(planName)}`);
298
331
  if (tasks.length === 0) {
299
- p.log.info('No tasks in this plan.');
332
+ p.log.info('No tasks found for this plan.');
300
333
  return interactiveMenu(cfg);
301
334
  }
302
335
  const lines = tasks.map((t) => {
303
336
  const name = truncate(t.name || 'Untitled', 50);
304
- const freq = t.frequency || '';
337
+ const freq = t.metadata?.frequency || '';
305
338
  return ` ${pc.cyan(pad(name, 52))} ${pc.dim(freq)}`;
306
339
  });
307
340
  p.note(lines.join('\n'), planName);
@@ -309,7 +342,7 @@ async function menuPlanTasks(cfg, post, planId, planName) {
309
342
  }
310
343
  catch (err) {
311
344
  s.stop(pc.red('Failed'));
312
- p.log.error(err.message);
345
+ p.log.error(err.message || 'Could not load tasks');
313
346
  return interactiveMenu(cfg);
314
347
  }
315
348
  }
@@ -318,14 +351,7 @@ async function menuTasks(cfg, post) {
318
351
  const s = p.spinner();
319
352
  s.start('Loading tasks...');
320
353
  try {
321
- const res = await post('/api/v2/query', {
322
- type: 'list_tasks',
323
- organization_id: cfg.orgId,
324
- payload: { organization_id: cfg.orgId },
325
- });
326
- if (!res.success)
327
- throw new Error(res.error?.message || 'Failed');
328
- const tasks = res.data || [];
354
+ const tasks = await listEntities(post, cfg.orgId, 'task');
329
355
  s.stop(`Found ${tasks.length} task${tasks.length === 1 ? '' : 's'}`);
330
356
  if (tasks.length === 0) {
331
357
  p.log.info('No tasks yet.');
@@ -337,7 +363,7 @@ async function menuTasks(cfg, post) {
337
363
  ...tasks.slice(0, 25).map((t) => ({
338
364
  value: t.id,
339
365
  label: truncate(t.name || 'Untitled', 60),
340
- hint: t.frequency || '',
366
+ hint: t.metadata?.frequency || '',
341
367
  })),
342
368
  ...(tasks.length > 25 ? [{ value: '__more', label: pc.dim(`...and ${tasks.length - 25} more`) }] : []),
343
369
  { value: '__back', label: pc.dim('Back to menu') },
@@ -346,14 +372,13 @@ async function menuTasks(cfg, post) {
346
372
  if (p.isCancel(selected) || selected === '__back' || selected === '__more') {
347
373
  return interactiveMenu(cfg);
348
374
  }
349
- // Show task detail
350
375
  const task = tasks.find((t) => t.id === selected);
351
376
  if (task) {
377
+ const meta = task.metadata || {};
352
378
  const lines = [
353
379
  `${pad('Name', 14)} ${pc.cyan(task.name || 'Untitled')}`,
354
- `${pad('Frequency', 14)} ${task.frequency || pc.dim('none')}`,
355
- `${pad('Plan', 14)} ${task.plan_name || task.plan_id || pc.dim('none')}`,
356
- `${pad('Assigned to', 14)} ${task.assigned_to || pc.dim('unassigned')}`,
380
+ `${pad('Frequency', 14)} ${meta.frequency || pc.dim('none')}`,
381
+ `${pad('Assigned to', 14)} ${meta.assigned_to || pc.dim('unassigned')}`,
357
382
  `${pad('ID', 14)} ${pc.dim(task.id)}`,
358
383
  ];
359
384
  p.note(lines.join('\n'), task.name || 'Task');
@@ -372,7 +397,7 @@ async function menuTasks(cfg, post) {
372
397
  }
373
398
  catch (err) {
374
399
  s.stop(pc.red('Failed'));
375
- p.log.error(err.message);
400
+ p.log.error(err.message || 'Could not load tasks');
376
401
  return interactiveMenu(cfg);
377
402
  }
378
403
  }
@@ -381,30 +406,30 @@ async function menuStatus(cfg, post) {
381
406
  const s = p.spinner();
382
407
  s.start('Loading workspace...');
383
408
  try {
384
- const res = await post('/api/v2/introspection/workspace', {
385
- organization_id: cfg.orgId,
386
- });
387
- if (!res.success)
388
- throw new Error(res.error?.message || 'Failed');
389
- const ws = res.data || {};
409
+ const nodes = await getWorkspaceNodes(post, cfg.orgId);
390
410
  s.stop('Workspace loaded');
391
- const counts = ws.entity_counts || {};
411
+ // Count nodes by type
412
+ const counts = {};
413
+ for (const node of nodes) {
414
+ const t = node.type || 'unknown';
415
+ counts[t] = (counts[t] || 0) + 1;
416
+ }
392
417
  const lines = [
393
418
  `${pad('Workspace', 16)} ${pc.cyan(cfg.orgName)}`,
394
419
  '',
395
- `${pad('Plans', 16)} ${pc.bold(String(counts.plans || 0))}`,
396
- `${pad('Tasks', 16)} ${pc.bold(String(counts.tasks || 0))}`,
397
- `${pad('Requirements', 16)} ${pc.bold(String(counts.requirements || 0))}`,
398
- `${pad('Goals', 16)} ${pc.bold(String(counts.goals || 0))}`,
399
- `${pad('Documents', 16)} ${pc.bold(String(counts.documents || 0))}`,
400
- `${pad('Tables', 16)} ${pc.bold(String(counts.tables || 0))}`,
401
- `${pad('Members', 16)} ${pc.bold(String(counts.members || counts.users || 0))}`,
420
+ `${pad('Plans', 16)} ${pc.bold(String(counts.plan || 0))}`,
421
+ `${pad('Tasks', 16)} ${pc.bold(String(counts.task || 0))}`,
422
+ `${pad('Requirements', 16)} ${pc.bold(String(counts.requirement || 0))}`,
423
+ `${pad('Goals', 16)} ${pc.bold(String(counts.goal || 0))}`,
424
+ `${pad('Rules', 16)} ${pc.bold(String(counts.rule || 0))}`,
425
+ `${pad('Documents', 16)} ${pc.bold(String(counts.document || 0))}`,
426
+ `${pad('Tables', 16)} ${pc.bold(String(counts.table || 0))}`,
402
427
  ];
403
428
  p.note(lines.join('\n'), 'Workspace Overview');
404
429
  }
405
430
  catch (err) {
406
431
  s.stop(pc.red('Failed'));
407
- p.log.error(err.message);
432
+ p.log.error(err.message || 'Could not load workspace');
408
433
  }
409
434
  return interactiveMenu(cfg);
410
435
  }
@@ -423,21 +448,14 @@ async function menuSearch(cfg, post) {
423
448
  const s = p.spinner();
424
449
  s.start('Searching...');
425
450
  try {
426
- const res = await post('/api/v2/introspection/entities', {
427
- organization_id: cfg.orgId,
428
- search: query,
429
- limit: 20,
430
- });
431
- if (!res.success)
432
- throw new Error(res.error?.message || 'Failed');
433
- const entities = res.data || [];
451
+ const entities = await searchEntities(post, cfg.orgId, query);
434
452
  s.stop(`Found ${entities.length} result${entities.length === 1 ? '' : 's'}`);
435
453
  if (entities.length === 0) {
436
454
  p.log.info('No results found.');
437
455
  }
438
456
  else {
439
457
  const lines = entities.map((e) => {
440
- const type = pad(e.type || 'unknown', 14);
458
+ const type = pad(e.entity_type || 'unknown', 14);
441
459
  const name = truncate(e.name || 'Untitled', 50);
442
460
  return ` ${pc.dim(type)} ${pc.cyan(name)}`;
443
461
  });
@@ -446,7 +464,7 @@ async function menuSearch(cfg, post) {
446
464
  }
447
465
  catch (err) {
448
466
  s.stop(pc.red('Failed'));
449
- p.log.error(err.message);
467
+ p.log.error(err.message || 'Search failed');
450
468
  }
451
469
  return interactiveMenu(cfg);
452
470
  }
@@ -455,14 +473,7 @@ async function menuComplete(cfg, post) {
455
473
  const s = p.spinner();
456
474
  s.start('Loading tasks...');
457
475
  try {
458
- const res = await post('/api/v2/query', {
459
- type: 'list_tasks',
460
- organization_id: cfg.orgId,
461
- payload: { organization_id: cfg.orgId },
462
- });
463
- if (!res.success)
464
- throw new Error(res.error?.message || 'Failed');
465
- const tasks = res.data || [];
476
+ const tasks = await listEntities(post, cfg.orgId, 'task');
466
477
  s.stop(`${tasks.length} task${tasks.length === 1 ? '' : 's'} available`);
467
478
  if (tasks.length === 0) {
468
479
  p.log.info('No tasks to complete.');
@@ -474,7 +485,7 @@ async function menuComplete(cfg, post) {
474
485
  ...tasks.slice(0, 30).map((t) => ({
475
486
  value: t.id,
476
487
  label: truncate(t.name || 'Untitled', 60),
477
- hint: t.frequency || '',
488
+ hint: t.metadata?.frequency || '',
478
489
  })),
479
490
  { value: '__back', label: pc.dim('Back to menu') },
480
491
  ],
@@ -487,7 +498,7 @@ async function menuComplete(cfg, post) {
487
498
  }
488
499
  catch (err) {
489
500
  s.stop(pc.red('Failed'));
490
- p.log.error(err.message);
501
+ p.log.error(err.message || 'Could not load tasks');
491
502
  }
492
503
  return interactiveMenu(cfg);
493
504
  }
@@ -495,33 +506,25 @@ async function doComplete(cfg, post, taskId, taskName) {
495
506
  const s = p.spinner();
496
507
  s.start('Completing task...');
497
508
  try {
498
- const res = await post('/api/v2/commands', {
509
+ await post('/api/v2/commands', {
499
510
  type: 'complete_task',
500
511
  organization_id: cfg.orgId,
501
512
  payload: { task_id: taskId },
502
513
  });
503
- if (!res.success)
504
- throw new Error(res.error?.message || 'Failed');
505
514
  s.stop(pc.green(`Done! "${taskName}" completed`));
506
515
  }
507
516
  catch (err) {
508
517
  s.stop(pc.red('Failed'));
509
- p.log.error(err.message);
518
+ p.log.error(err.message || 'Could not complete task');
510
519
  }
511
520
  }
512
521
  // ── Create Task ─────────────────────────────────────────────────────
513
522
  async function menuCreateTask(cfg, post) {
514
- // First, get plans to attach task to
515
523
  const s = p.spinner();
516
524
  s.start('Loading plans...');
517
525
  let plans = [];
518
526
  try {
519
- const res = await post('/api/v2/query', {
520
- type: 'list_plans',
521
- organization_id: cfg.orgId,
522
- payload: { organization_id: cfg.orgId },
523
- });
524
- plans = res.data || [];
527
+ plans = await listEntities(post, cfg.orgId, 'plan');
525
528
  s.stop('');
526
529
  }
527
530
  catch {
@@ -577,18 +580,16 @@ async function menuCreateTask(cfg, post) {
577
580
  };
578
581
  if (result.planId)
579
582
  payload.plan_id = result.planId;
580
- const res = await post('/api/v2/commands', {
583
+ await post('/api/v2/commands', {
581
584
  type: 'create_task',
582
585
  organization_id: cfg.orgId,
583
586
  payload,
584
587
  });
585
- if (!res.success)
586
- throw new Error(res.error?.message || 'Failed');
587
588
  spinner.stop(pc.green(`Task "${result.name}" created!`));
588
589
  }
589
590
  catch (err) {
590
591
  spinner.stop(pc.red('Failed'));
591
- p.log.error(err.message);
592
+ p.log.error(err.message || 'Could not create task');
592
593
  }
593
594
  return interactiveMenu(cfg);
594
595
  }
@@ -601,9 +602,7 @@ async function menuSwitch(cfg, post) {
601
602
  type: 'list_my_organizations',
602
603
  payload: {},
603
604
  });
604
- if (!res.success)
605
- throw new Error(res.error?.message || 'Failed');
606
- const orgs = res.data || [];
605
+ const orgs = Array.isArray(res.data) ? res.data : [];
607
606
  s.stop(`${orgs.length} workspace${orgs.length === 1 ? '' : 's'}`);
608
607
  const selected = await p.select({
609
608
  message: 'Switch to workspace',
@@ -623,58 +622,40 @@ async function menuSwitch(cfg, post) {
623
622
  }
624
623
  catch (err) {
625
624
  s.stop(pc.red('Failed'));
626
- p.log.error(err.message);
625
+ p.log.error(err.message || 'Could not load workspaces');
627
626
  }
628
627
  return interactiveMenu(cfg);
629
628
  }
630
- // ── Direct Commands (non-interactive, for scripts) ──────────────────
629
+ // ── Direct Commands (non-interactive) ───────────────────────────────
631
630
  async function directPlans(cfg) {
632
631
  const { post } = api(cfg);
633
632
  const s = p.spinner();
634
633
  s.start('Loading plans...');
635
- const res = await post('/api/v2/query', {
636
- type: 'list_plans',
637
- organization_id: cfg.orgId,
638
- payload: { organization_id: cfg.orgId },
639
- });
640
- if (!res.success)
641
- throw new Error(res.error?.message || 'Failed');
634
+ const plans = await listEntities(post, cfg.orgId, 'plan');
642
635
  s.stop('');
643
- const plans = res.data || [];
644
636
  if (plans.length === 0) {
645
637
  p.log.info('No plans found.');
646
638
  return;
647
639
  }
648
640
  const lines = plans.map((plan) => {
649
641
  const name = truncate(plan.name || 'Untitled', 40);
650
- const id = plan.id;
651
- return ` ${pc.cyan(pad(name, 42))} ${pc.dim(id)}`;
642
+ return ` ${pc.cyan(pad(name, 42))} ${pc.dim(plan.id)}`;
652
643
  });
653
644
  p.note(lines.join('\n'), `${plans.length} plan${plans.length === 1 ? '' : 's'}`);
654
645
  }
655
- async function directTasks(cfg, planId) {
646
+ async function directTasks(cfg) {
656
647
  const { post } = api(cfg);
657
648
  const s = p.spinner();
658
649
  s.start('Loading tasks...');
659
- const payload = { organization_id: cfg.orgId };
660
- if (planId)
661
- payload.plan_id = planId;
662
- const res = await post('/api/v2/query', {
663
- type: 'list_tasks',
664
- organization_id: cfg.orgId,
665
- payload,
666
- });
667
- if (!res.success)
668
- throw new Error(res.error?.message || 'Failed');
650
+ const tasks = await listEntities(post, cfg.orgId, 'task');
669
651
  s.stop('');
670
- const tasks = res.data || [];
671
652
  if (tasks.length === 0) {
672
653
  p.log.info('No tasks found.');
673
654
  return;
674
655
  }
675
656
  const lines = tasks.map((t) => {
676
657
  const name = truncate(t.name || 'Untitled', 50);
677
- const freq = t.frequency || '';
658
+ const freq = t.metadata?.frequency || '';
678
659
  return ` ${pc.cyan(pad(name, 52))} ${pc.dim(freq)} ${pc.dim(t.id)}`;
679
660
  });
680
661
  p.note(lines.join('\n'), `${tasks.length} task${tasks.length === 1 ? '' : 's'}`);
@@ -683,39 +664,52 @@ async function directComplete(cfg, taskId) {
683
664
  const { post } = api(cfg);
684
665
  const s = p.spinner();
685
666
  s.start('Completing task...');
686
- const res = await post('/api/v2/commands', {
667
+ await post('/api/v2/commands', {
687
668
  type: 'complete_task',
688
669
  organization_id: cfg.orgId,
689
670
  payload: { task_id: taskId },
690
671
  });
691
- if (!res.success)
692
- throw new Error(res.error?.message || 'Failed');
693
672
  s.stop(pc.green('Task completed!'));
694
673
  }
695
674
  async function directStatus(cfg) {
696
675
  const { post } = api(cfg);
697
676
  const s = p.spinner();
698
677
  s.start('Loading workspace...');
699
- const res = await post('/api/v2/introspection/workspace', {
700
- organization_id: cfg.orgId,
701
- });
702
- if (!res.success)
703
- throw new Error(res.error?.message || 'Failed');
678
+ const nodes = await getWorkspaceNodes(post, cfg.orgId);
704
679
  s.stop('');
705
- const counts = res.data?.entity_counts || {};
680
+ const counts = {};
681
+ for (const node of nodes) {
682
+ const t = node.type || 'unknown';
683
+ counts[t] = (counts[t] || 0) + 1;
684
+ }
706
685
  const lines = [
707
686
  `${pad('Workspace', 16)} ${pc.cyan(cfg.orgName)}`,
708
687
  '',
709
- `${pad('Plans', 16)} ${pc.bold(String(counts.plans || 0))}`,
710
- `${pad('Tasks', 16)} ${pc.bold(String(counts.tasks || 0))}`,
711
- `${pad('Requirements', 16)} ${pc.bold(String(counts.requirements || 0))}`,
712
- `${pad('Goals', 16)} ${pc.bold(String(counts.goals || 0))}`,
713
- `${pad('Documents', 16)} ${pc.bold(String(counts.documents || 0))}`,
714
- `${pad('Tables', 16)} ${pc.bold(String(counts.tables || 0))}`,
688
+ `${pad('Plans', 16)} ${pc.bold(String(counts.plan || 0))}`,
689
+ `${pad('Tasks', 16)} ${pc.bold(String(counts.task || 0))}`,
690
+ `${pad('Requirements', 16)} ${pc.bold(String(counts.requirement || 0))}`,
691
+ `${pad('Goals', 16)} ${pc.bold(String(counts.goal || 0))}`,
692
+ `${pad('Rules', 16)} ${pc.bold(String(counts.rule || 0))}`,
693
+ `${pad('Documents', 16)} ${pc.bold(String(counts.document || 0))}`,
694
+ `${pad('Tables', 16)} ${pc.bold(String(counts.table || 0))}`,
715
695
  ];
716
696
  p.note(lines.join('\n'), 'Workspace Overview');
717
697
  }
718
- // Raw command passthrough for power users and automation
698
+ async function directSearch(cfg, term) {
699
+ const { post } = api(cfg);
700
+ const s = p.spinner();
701
+ s.start('Searching...');
702
+ const entities = await searchEntities(post, cfg.orgId, term);
703
+ s.stop('');
704
+ if (entities.length === 0) {
705
+ p.log.info('No results found.');
706
+ }
707
+ else {
708
+ const lines = entities.map((e) => ` ${pc.dim(pad(e.entity_type || '', 14))} ${pc.cyan(truncate(e.name || '', 50))} ${pc.dim(e.id)}`);
709
+ p.note(lines.join('\n'), `${entities.length} result${entities.length === 1 ? '' : 's'} for "${term}"`);
710
+ }
711
+ }
712
+ // Raw passthrough for power users
719
713
  async function directRaw(cfg, type, args) {
720
714
  const { post } = api(cfg);
721
715
  const payloadRaw = args[0];
@@ -750,17 +744,14 @@ async function main() {
750
744
  const args = process.argv.slice(2);
751
745
  const sub = args[0];
752
746
  const rest = args.slice(1);
753
- // Setup command always available
754
747
  if (sub === 'setup' || sub === 'init' || sub === 'login') {
755
748
  await runSetup(resolveConfig());
756
749
  return;
757
750
  }
758
- // Version
759
751
  if (sub === '--version' || sub === '-v') {
760
752
  console.log(`checkbox v${VERSION}`);
761
753
  return;
762
754
  }
763
- // Help
764
755
  if (sub === '--help' || sub === '-h' || sub === 'help') {
765
756
  console.log();
766
757
  p.intro(pc.bgCyan(pc.black(' Checkbox CLI ')) + pc.dim(` v${VERSION}`));
@@ -780,23 +771,19 @@ async function main() {
780
771
  p.outro(pc.dim('Run `checkbox setup` to get started'));
781
772
  return;
782
773
  }
783
- // Check if configured
784
774
  const cfg = resolveConfig();
785
775
  if (!cfg) {
786
- // First-time user — run setup
787
776
  await runSetup(null);
788
777
  return;
789
778
  }
790
- // Direct commands
791
779
  try {
792
780
  switch (sub) {
793
781
  case 'plans':
794
782
  return await directPlans(cfg);
795
783
  case 'tasks':
796
- return await directTasks(cfg, getFlag(rest, '--plan'));
784
+ return await directTasks(cfg);
797
785
  case 'complete':
798
786
  if (!rest[0]) {
799
- // Interactive complete
800
787
  const { post } = api(cfg);
801
788
  return await menuComplete(cfg, post);
802
789
  }
@@ -809,24 +796,9 @@ async function main() {
809
796
  const { post } = api(cfg);
810
797
  return await menuSearch(cfg, post);
811
798
  }
812
- // Direct search
813
- const { post: searchPost } = api(cfg);
814
- const searchRes = await searchPost('/api/v2/introspection/entities', {
815
- organization_id: cfg.orgId,
816
- search: rest.join(' '),
817
- limit: 20,
818
- });
819
- if (searchRes.data?.length) {
820
- const lines = searchRes.data.map((e) => ` ${pc.dim(pad(e.type || '', 14))} ${pc.cyan(truncate(e.name || '', 50))} ${pc.dim(e.id)}`);
821
- p.note(lines.join('\n'), `Results for "${rest.join(' ')}"`);
822
- }
823
- else {
824
- p.log.info('No results found.');
825
- }
826
- return;
799
+ return await directSearch(cfg, rest.join(' '));
827
800
  case 'switch':
828
801
  return await menuSwitch(cfg, api(cfg).post);
829
- // Power user commands
830
802
  case 'cmd':
831
803
  case 'command':
832
804
  return await directRaw(cfg, rest[0], rest.slice(1));
@@ -836,7 +808,6 @@ async function main() {
836
808
  case 'organizations':
837
809
  return await directQuery(cfg, 'list_my_organizations', []);
838
810
  case undefined:
839
- // No args → interactive menu
840
811
  return await interactiveMenu(cfg);
841
812
  default:
842
813
  p.log.error(`Unknown command: ${sub}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "checkbox-cli",
3
- "version": "2.0.0",
3
+ "version": "2.0.1",
4
4
  "description": "Beautiful, interactive CLI for Checkbox compliance management. Setup wizard, guided workflows, and full workspace control from your terminal.",
5
5
  "type": "module",
6
6
  "main": "dist/checkbox.js",