checkbox-cli 2.0.1 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/checkbox.js CHANGED
@@ -1,16 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * Checkbox CLI — Beautiful, interactive compliance management from your terminal.
3
+ * Checkbox CLI v3.0.0 Full-featured compliance management from your terminal.
4
+ *
5
+ * Every capability available in the Checkbox web app is accessible here:
6
+ * tasks, plans, tables, documents, dashboards, forms, team management, and more.
4
7
  *
5
8
  * First-time users: just run `checkbox` and follow the guided setup.
6
9
  * Returning users: `checkbox` opens the interactive menu.
7
- *
8
- * Direct commands (for scripts & automation):
9
- * checkbox plans — List your compliance plans
10
- * checkbox tasks — Browse tasks
11
- * checkbox complete <id> — Complete a task
12
- * checkbox status — Workspace overview
13
- * checkbox setup — Re-run the setup wizard
14
10
  */
15
11
  import * as p from '@clack/prompts';
16
12
  import pc from 'picocolors';
@@ -20,7 +16,7 @@ import { join } from 'path';
20
16
  // ── Config ──────────────────────────────────────────────────────────
21
17
  const CONFIG_DIR = join(homedir(), '.checkbox');
22
18
  const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
23
- const VERSION = '2.0.1';
19
+ const VERSION = '3.0.0';
24
20
  function loadConfig() {
25
21
  try {
26
22
  if (!existsSync(CONFIG_FILE))
@@ -58,7 +54,6 @@ function resolveConfig() {
58
54
  return fileCfg;
59
55
  }
60
56
  // ── API ─────────────────────────────────────────────────────────────
61
- /** Extract a human-readable error message from any API error shape */
62
57
  function extractError(obj) {
63
58
  if (!obj)
64
59
  return 'Unknown error';
@@ -84,43 +79,33 @@ function api(cfg) {
84
79
  body: JSON.stringify(body),
85
80
  });
86
81
  const json = await res.json().catch(() => null);
87
- if (!res.ok) {
82
+ if (!res.ok)
88
83
  throw new Error(extractError(json) || `Request failed (${res.status})`);
89
- }
90
- if (json && json.success === false) {
84
+ if (json && json.success === false)
91
85
  throw new Error(extractError(json) || 'Request failed');
92
- }
93
86
  return json;
94
87
  }
95
88
  async function get(path) {
96
89
  const res = await fetch(`${cfg.apiUrl}${path}`, { headers });
97
90
  const json = await res.json().catch(() => null);
98
- if (!res.ok) {
91
+ if (!res.ok)
99
92
  throw new Error(extractError(json) || `Request failed (${res.status})`);
100
- }
101
- if (json && json.success === false) {
93
+ if (json && json.success === false)
102
94
  throw new Error(extractError(json) || 'Request failed');
103
- }
104
95
  return json;
105
96
  }
106
97
  return { post, get };
107
98
  }
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) {
99
+ // ── Data Helpers ────────────────────────────────────────────────────
100
+ async function listEntities(post, orgId, entityType, filters) {
114
101
  const res = await post('/api/v2/query', {
115
102
  type: 'list_entities',
116
103
  organization_id: orgId,
117
- payload: { entity_type: entityType, organization_id: orgId },
104
+ payload: { entity_type: entityType, organization_id: orgId, ...filters },
118
105
  });
119
- // list_entities returns { success, data: [...], meta }
120
106
  return Array.isArray(res.data) ? res.data : [];
121
107
  }
122
108
  async function searchEntities(post, orgId, search, limit = 20) {
123
- // entity_discovery returns { success, data: { organization_id, entities: [...] } }
124
109
  const res = await post('/api/v2/introspection/entities', {
125
110
  organization_id: orgId,
126
111
  search,
@@ -129,7 +114,6 @@ async function searchEntities(post, orgId, search, limit = 20) {
129
114
  return res.data?.entities || [];
130
115
  }
131
116
  async function getWorkspaceNodes(post, orgId) {
132
- // workspace returns { success, data: { organization_id, nodes: [...], edges: [...] } }
133
117
  const res = await post('/api/v2/introspection/workspace', {
134
118
  organization_id: orgId,
135
119
  });
@@ -145,6 +129,106 @@ function formatDate(iso) {
145
129
  function pad(s, n) {
146
130
  return s.padEnd(n);
147
131
  }
132
+ // ── UI Helpers ──────────────────────────────────────────────────────
133
+ async function pickEntity(post, orgId, entityType, label, filters) {
134
+ const s = p.spinner();
135
+ s.start(`Loading ${label}...`);
136
+ try {
137
+ const items = await listEntities(post, orgId, entityType, filters);
138
+ s.stop(`${items.length} ${label}`);
139
+ if (items.length === 0) {
140
+ p.log.info(`No ${label} found.`);
141
+ return null;
142
+ }
143
+ const selected = await p.select({
144
+ message: `Choose a ${label.replace(/s$/, '')}`,
145
+ options: [
146
+ ...items.slice(0, 50).map((e) => ({
147
+ value: e.id,
148
+ label: truncate(e.name || 'Untitled', 60),
149
+ hint: e.metadata?.frequency || '',
150
+ })),
151
+ ...(items.length > 50 ? [{ value: '__more', label: pc.dim(`\u2026${items.length - 50} more (use search)`) }] : []),
152
+ { value: '__back', label: pc.dim('\u2190 Back') },
153
+ ],
154
+ });
155
+ if (p.isCancel(selected) || selected === '__back' || selected === '__more')
156
+ return null;
157
+ return items.find((e) => e.id === selected) || null;
158
+ }
159
+ catch (err) {
160
+ s.stop(pc.red('Failed'));
161
+ p.log.error(err.message || `Could not load ${label}`);
162
+ return null;
163
+ }
164
+ }
165
+ async function txt(message, required = false, placeholder) {
166
+ const val = await p.text({
167
+ message,
168
+ placeholder,
169
+ validate: required ? (v) => { if (!v)
170
+ return 'This field is required'; } : undefined,
171
+ });
172
+ if (p.isCancel(val))
173
+ return null;
174
+ return val || null;
175
+ }
176
+ async function sel(message, options) {
177
+ const val = await p.select({ message, options });
178
+ if (p.isCancel(val))
179
+ return null;
180
+ return val;
181
+ }
182
+ async function yn(message, initial = false) {
183
+ const val = await p.confirm({ message, initialValue: initial });
184
+ if (p.isCancel(val))
185
+ return null;
186
+ return val;
187
+ }
188
+ async function cmd(post, orgId, type, payload, label = 'Working...') {
189
+ const s = p.spinner();
190
+ s.start(label);
191
+ try {
192
+ const body = { type, payload };
193
+ if (orgId)
194
+ body.organization_id = orgId;
195
+ const res = await post('/api/v2/commands', body);
196
+ s.stop(pc.green('Done!'));
197
+ return res;
198
+ }
199
+ catch (err) {
200
+ s.stop(pc.red('Failed'));
201
+ p.log.error(err.message || 'Something went wrong');
202
+ return null;
203
+ }
204
+ }
205
+ async function cmdNoOrg(post, type, payload, label = 'Working...') {
206
+ const s = p.spinner();
207
+ s.start(label);
208
+ try {
209
+ const res = await post('/api/v2/commands', { type, payload });
210
+ s.stop(pc.green('Done!'));
211
+ return res;
212
+ }
213
+ catch (err) {
214
+ s.stop(pc.red('Failed'));
215
+ p.log.error(err.message || 'Something went wrong');
216
+ return null;
217
+ }
218
+ }
219
+ async function qry(post, orgId, type, payload = {}) {
220
+ try {
221
+ return await post('/api/v2/query', {
222
+ type,
223
+ organization_id: orgId,
224
+ payload: { ...payload, organization_id: orgId },
225
+ });
226
+ }
227
+ catch (err) {
228
+ p.log.error(err.message || 'Query failed');
229
+ return null;
230
+ }
231
+ }
148
232
  // ── Setup Wizard ────────────────────────────────────────────────────
149
233
  async function runSetup(existingConfig) {
150
234
  p.intro(pc.bgCyan(pc.black(' Checkbox Setup ')));
@@ -236,7 +320,9 @@ async function runSetup(existingConfig) {
236
320
  p.outro(pc.green('Setup complete! Run `checkbox` to get started.'));
237
321
  return cfg;
238
322
  }
239
- // ── Interactive Menu ────────────────────────────────────────────────
323
+ // ══════════════════════════════════════════════════════════════════════
324
+ // MAIN MENU
325
+ // ══════════════════════════════════════════════════════════════════════
240
326
  async function interactiveMenu(cfg) {
241
327
  const { post } = api(cfg);
242
328
  console.log();
@@ -247,14 +333,19 @@ async function interactiveMenu(cfg) {
247
333
  const action = await p.select({
248
334
  message: 'What would you like to do?',
249
335
  options: [
250
- { value: 'plans', label: 'View plans', hint: 'see your compliance plans' },
251
- { value: 'tasks', label: 'Browse tasks', hint: 'view and complete tasks' },
252
- { value: 'status', label: 'Workspace overview', hint: 'summary of your workspace' },
253
- { value: 'search', label: 'Search', hint: 'find anything in your workspace' },
254
- { value: 'complete', label: 'Complete a task', hint: 'mark a task as done' },
255
- { value: 'create-task', label: 'Create a task', hint: 'add a new task' },
256
- { value: 'switch', label: 'Switch workspace', hint: 'change organization' },
257
- { value: 'setup', label: 'Settings', hint: 'API key, workspace config' },
336
+ { value: 'tasks', label: 'My Tasks', hint: 'view, complete, create, and manage tasks' },
337
+ { value: 'plans', label: 'Plans', hint: 'compliance plans, goals, rules, requirements' },
338
+ { value: 'tables', label: 'Data Tables', hint: 'manage tables and records' },
339
+ { value: 'documents', label: 'Documents', hint: 'files, folders, and attachments' },
340
+ { value: 'assets', label: 'Assets', hint: 'photos, locations, checklists' },
341
+ { value: 'automation', label: 'Rules & Automation', hint: 'validation rules, triggers' },
342
+ { value: 'dashboards', label: 'Dashboards & Sharing', hint: 'dashboards, shares, reports' },
343
+ { value: 'forms', label: 'Forms', hint: 'public data collection forms' },
344
+ { value: 'approvals', label: 'Approvals', hint: 'review and manage approvals' },
345
+ { value: 'team', label: 'Team', hint: 'invite members, manage roles' },
346
+ { value: 'organization', label: 'Organization', hint: 'settings, branding, membership' },
347
+ { value: 'overview', label: 'Workspace Overview', hint: 'search and status' },
348
+ { value: 'settings', label: 'Settings', hint: 'API key, switch workspace' },
258
349
  ],
259
350
  });
260
351
  if (p.isCancel(action)) {
@@ -262,371 +353,1898 @@ async function interactiveMenu(cfg) {
262
353
  return;
263
354
  }
264
355
  switch (action) {
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);
272
- case 'setup':
356
+ case 'tasks': return mTasks(cfg, post);
357
+ case 'plans': return mPlans(cfg, post);
358
+ case 'tables': return mTables(cfg, post);
359
+ case 'documents': return mDocs(cfg, post);
360
+ case 'assets': return mAssets(cfg, post);
361
+ case 'automation': return mAutomation(cfg, post);
362
+ case 'dashboards': return mDashboards(cfg, post);
363
+ case 'forms': return mForms(cfg, post);
364
+ case 'approvals': return mApprovals(cfg, post);
365
+ case 'team': return mTeam(cfg, post);
366
+ case 'organization': return mOrg(cfg, post);
367
+ case 'overview': return mOverview(cfg, post);
368
+ case 'settings':
273
369
  await runSetup(cfg);
274
370
  return;
275
371
  }
276
372
  }
277
- // ── Plans ───────────────────────────────────────────────────────────
278
- async function menuPlans(cfg, post) {
279
- const s = p.spinner();
280
- s.start('Loading plans...');
281
- try {
282
- const plans = await listEntities(post, cfg.orgId, 'plan');
283
- s.stop(`Found ${plans.length} plan${plans.length === 1 ? '' : 's'}`);
284
- if (plans.length === 0) {
285
- p.log.info('No plans yet. Create one at checkbox.my');
286
- return interactiveMenu(cfg);
287
- }
288
- const lines = plans.map((plan) => {
289
- const name = truncate(plan.name || 'Untitled', 40);
290
- const meta = plan.metadata || {};
291
- const created = meta.created_at ? formatDate(meta.created_at) : '';
292
- return `${pc.cyan(pad(name, 42))} ${pc.dim(created)}`;
293
- });
294
- p.note(lines.join('\n'), `Plans in ${cfg.orgName}`);
295
- const selected = await p.select({
296
- message: 'Select a plan to view its tasks',
297
- options: [
298
- ...plans.map((plan) => ({
299
- value: plan.id,
300
- label: plan.name || 'Untitled',
301
- })),
302
- { value: '__back', label: pc.dim('Back to menu') },
303
- ],
304
- });
305
- if (p.isCancel(selected) || selected === '__back') {
306
- return interactiveMenu(cfg);
373
+ // ══════════════════════════════════════════════════════════════════════
374
+ // TASKS 9 commands + send_nudge + create_corrective_action + queries
375
+ // ══════════════════════════════════════════════════════════════════════
376
+ async function mTasks(cfg, post) {
377
+ const a = await sel('My Tasks', [
378
+ { value: 'today', label: "Today's tasks", hint: 'see what needs to be done' },
379
+ { value: 'all', label: 'All tasks', hint: 'browse all tasks' },
380
+ { value: 'complete', label: 'Complete a task' },
381
+ { value: 'reopen', label: 'Reopen a completed task' },
382
+ { value: 'create', label: 'Create a new task' },
383
+ { value: 'edit', label: 'Edit a task' },
384
+ { value: 'delete', label: 'Delete a task' },
385
+ { value: 'validate', label: 'Submit proof', hint: 'photo, document, location, checklist' },
386
+ { value: 'approval', label: 'Request approval for a task' },
387
+ { value: 'answer', label: 'Answer a conditional question' },
388
+ { value: 'nudge', label: 'Send a reminder', hint: 'nudge someone about a task' },
389
+ { value: 'corrective', label: 'Corrective action', hint: 'for a missed task' },
390
+ { value: '__back', label: pc.dim('\u2190 Back to main menu') },
391
+ ]);
392
+ if (!a || a === '__back')
393
+ return interactiveMenu(cfg);
394
+ switch (a) {
395
+ case 'today': {
396
+ const s = p.spinner();
397
+ s.start('Loading...');
398
+ const today = new Date().toISOString().split('T')[0];
399
+ const res = await qry(post, cfg.orgId, 'get_task_states', { date: today });
400
+ const states = Array.isArray(res?.data) ? res.data : [];
401
+ s.stop(`${states.length} tasks today`);
402
+ if (states.length > 0) {
403
+ const lines = states.map((t) => {
404
+ const icon = t.status === 'completed' ? pc.green('\u2713') : t.status === 'missed' ? pc.red('\u2717') : pc.yellow('\u25CB');
405
+ return ` ${icon} ${pc.cyan(truncate(t.task_name || t.name || 'Untitled', 50))} ${pc.dim(t.frequency || '')}`;
406
+ });
407
+ p.note(lines.join('\n'), `Today (${today})`);
408
+ }
409
+ else {
410
+ p.log.info('No tasks scheduled for today.');
411
+ }
412
+ return mTasks(cfg, post);
413
+ }
414
+ case 'all': {
415
+ const s = p.spinner();
416
+ s.start('Loading tasks...');
417
+ const tasks = await listEntities(post, cfg.orgId, 'task');
418
+ s.stop(`${tasks.length} tasks`);
419
+ if (tasks.length > 0) {
420
+ const lines = tasks.map((t) => ` ${pc.cyan(pad(truncate(t.name || 'Untitled', 50), 52))} ${pc.dim(t.metadata?.frequency || '')}`);
421
+ p.note(lines.join('\n'), 'All Tasks');
422
+ }
423
+ return mTasks(cfg, post);
424
+ }
425
+ case 'complete': {
426
+ const t = await pickEntity(post, cfg.orgId, 'task', 'tasks');
427
+ if (t)
428
+ await cmd(post, cfg.orgId, 'complete_task', { task_id: t.id }, `Completing "${t.name}"...`);
429
+ return mTasks(cfg, post);
430
+ }
431
+ case 'reopen': {
432
+ const t = await pickEntity(post, cfg.orgId, 'task', 'tasks');
433
+ if (t)
434
+ await cmd(post, cfg.orgId, 'uncomplete_task', { task_id: t.id }, `Reopening "${t.name}"...`);
435
+ return mTasks(cfg, post);
436
+ }
437
+ case 'create': {
438
+ const name = await txt('Task name', true, 'e.g. Daily safety walkthrough');
439
+ if (!name)
440
+ return mTasks(cfg, post);
441
+ const frequency = await sel('How often?', [
442
+ { value: 'daily', label: 'Daily' },
443
+ { value: 'weekly', label: 'Weekly' },
444
+ { value: 'monthly', label: 'Monthly' },
445
+ { value: 'yearly', label: 'Yearly' },
446
+ { value: 'one-time', label: 'One time' },
447
+ ]);
448
+ if (!frequency)
449
+ return mTasks(cfg, post);
450
+ const payload = { name, frequency };
451
+ const addPlan = await yn('Add to a plan?');
452
+ if (addPlan) {
453
+ const plan = await pickEntity(post, cfg.orgId, 'plan', 'plans');
454
+ if (plan)
455
+ payload.plan_id = plan.id;
456
+ }
457
+ const more = await yn('Add more details? (description, time, reminders, etc.)');
458
+ if (more) {
459
+ const desc = await txt('Description');
460
+ if (desc)
461
+ payload.description = desc;
462
+ const priv = await yn('Private task?');
463
+ if (priv)
464
+ payload.is_private = true;
465
+ const time = await txt('Scheduled time (HH:MM)', false, '09:00');
466
+ if (time)
467
+ payload.time = time;
468
+ const reminder = await sel('Reminder', [
469
+ { value: 'none', label: 'No reminder' },
470
+ { value: 'notification', label: 'Push notification' },
471
+ { value: 'email', label: 'Email' },
472
+ ]);
473
+ if (reminder && reminder !== 'none') {
474
+ payload.reminder_mode = reminder;
475
+ const mins = await txt('Minutes before', false, '30');
476
+ if (mins)
477
+ payload.reminder_minutes = parseInt(mins, 10);
478
+ }
479
+ const approval = await yn('Requires approval?');
480
+ if (approval)
481
+ payload.approval_required = true;
482
+ const cond = await txt('Conditional question (blank to skip)', false, 'e.g. Is the equipment available?');
483
+ if (cond)
484
+ payload.conditional_question = cond;
485
+ }
486
+ await cmd(post, cfg.orgId, 'create_task', payload, 'Creating task...');
487
+ return mTasks(cfg, post);
488
+ }
489
+ case 'edit': {
490
+ const t = await pickEntity(post, cfg.orgId, 'task', 'tasks');
491
+ if (!t)
492
+ return mTasks(cfg, post);
493
+ p.log.info(`Editing: ${pc.cyan(t.name)} ${pc.dim('(leave blank to skip)')}`);
494
+ const updates = {};
495
+ const n = await txt('New name');
496
+ if (n === null)
497
+ return mTasks(cfg, post);
498
+ if (n)
499
+ updates.name = n;
500
+ const f = await sel('New frequency', [
501
+ { value: '__skip', label: pc.dim('Keep current') },
502
+ { value: 'daily', label: 'Daily' }, { value: 'weekly', label: 'Weekly' },
503
+ { value: 'monthly', label: 'Monthly' }, { value: 'yearly', label: 'Yearly' },
504
+ { value: 'one-time', label: 'One time' },
505
+ ]);
506
+ if (f && f !== '__skip')
507
+ updates.frequency = f;
508
+ const d = await txt('New description');
509
+ if (d)
510
+ updates.description = d;
511
+ if (Object.keys(updates).length > 0) {
512
+ await cmd(post, cfg.orgId, 'update_task', { task_id: t.id, updates }, 'Updating...');
513
+ }
514
+ else
515
+ p.log.info('No changes.');
516
+ return mTasks(cfg, post);
517
+ }
518
+ case 'delete': {
519
+ const t = await pickEntity(post, cfg.orgId, 'task', 'tasks');
520
+ if (!t)
521
+ return mTasks(cfg, post);
522
+ const sure = await yn(`Delete "${t.name}"? This cannot be undone.`);
523
+ if (sure)
524
+ await cmd(post, cfg.orgId, 'delete_task', { task_id: t.id }, 'Deleting...');
525
+ return mTasks(cfg, post);
526
+ }
527
+ case 'validate': {
528
+ const t = await pickEntity(post, cfg.orgId, 'task', 'tasks');
529
+ if (!t)
530
+ return mTasks(cfg, post);
531
+ const vtype = await sel('Proof type', [
532
+ { value: 'photo', label: 'Photo' },
533
+ { value: 'document', label: 'Document' },
534
+ { value: 'geolocation', label: 'Location check-in' },
535
+ { value: 'checklist', label: 'Checklist' },
536
+ { value: 'table', label: 'Table record' },
537
+ ]);
538
+ if (!vtype)
539
+ return mTasks(cfg, post);
540
+ let vid = null;
541
+ let data = {};
542
+ if (vtype === 'photo') {
543
+ const ph = await pickEntity(post, cfg.orgId, 'photo', 'photo assets');
544
+ if (!ph)
545
+ return mTasks(cfg, post);
546
+ vid = ph.id;
547
+ const url = await txt('Photo URL', true);
548
+ if (!url)
549
+ return mTasks(cfg, post);
550
+ data = { url };
551
+ }
552
+ else if (vtype === 'document') {
553
+ const doc = await pickEntity(post, cfg.orgId, 'document', 'documents');
554
+ if (!doc)
555
+ return mTasks(cfg, post);
556
+ vid = doc.id;
557
+ const url = await txt('Document URL', true);
558
+ if (!url)
559
+ return mTasks(cfg, post);
560
+ data = { url };
561
+ }
562
+ else if (vtype === 'geolocation') {
563
+ const area = await pickEntity(post, cfg.orgId, 'geolocation_area', 'location areas');
564
+ if (!area)
565
+ return mTasks(cfg, post);
566
+ vid = area.id;
567
+ const lat = await txt('Your latitude', true);
568
+ const lng = await txt('Your longitude', true);
569
+ if (!lat || !lng)
570
+ return mTasks(cfg, post);
571
+ data = { latitude: parseFloat(lat), longitude: parseFloat(lng) };
572
+ }
573
+ else if (vtype === 'checklist') {
574
+ const cl = await pickEntity(post, cfg.orgId, 'checklist', 'checklists');
575
+ if (!cl)
576
+ return mTasks(cfg, post);
577
+ vid = cl.id;
578
+ data = { completed: true };
579
+ }
580
+ else if (vtype === 'table') {
581
+ const tbl = await pickEntity(post, cfg.orgId, 'table', 'tables');
582
+ if (!tbl)
583
+ return mTasks(cfg, post);
584
+ vid = tbl.id;
585
+ const rid = await txt('Record ID', true);
586
+ if (!rid)
587
+ return mTasks(cfg, post);
588
+ data = { record_id: rid };
589
+ }
590
+ if (vid) {
591
+ await cmd(post, cfg.orgId, 'submit_validation', {
592
+ task_id: t.id, validation_type: vtype, validation_id: vid, data,
593
+ }, 'Submitting proof...');
594
+ }
595
+ return mTasks(cfg, post);
596
+ }
597
+ case 'approval': {
598
+ const t = await pickEntity(post, cfg.orgId, 'task', 'tasks');
599
+ if (!t)
600
+ return mTasks(cfg, post);
601
+ const payload = { task_id: t.id };
602
+ const specify = await yn('Specify an approver?');
603
+ if (specify) {
604
+ const aid = await txt('Approver user ID', true);
605
+ if (aid)
606
+ payload.approver_id = aid;
607
+ }
608
+ await cmd(post, cfg.orgId, 'request_approval', payload, 'Requesting approval...');
609
+ return mTasks(cfg, post);
610
+ }
611
+ case 'answer': {
612
+ const t = await pickEntity(post, cfg.orgId, 'task', 'tasks');
613
+ if (!t)
614
+ return mTasks(cfg, post);
615
+ const answer = await sel(`Answer for "${t.name}"`, [
616
+ { value: 'yes', label: 'Yes' }, { value: 'no', label: 'No' },
617
+ ]);
618
+ if (answer)
619
+ await cmd(post, cfg.orgId, 'answer_conditional', { task_id: t.id, answer }, 'Submitting answer...');
620
+ return mTasks(cfg, post);
621
+ }
622
+ case 'nudge': {
623
+ const t = await pickEntity(post, cfg.orgId, 'task', 'tasks');
624
+ if (!t)
625
+ return mTasks(cfg, post);
626
+ const rid = await txt('Who should get the reminder? (user ID)', true);
627
+ if (rid)
628
+ await cmd(post, cfg.orgId, 'send_nudge', { task_id: t.id, receiver_id: rid }, 'Sending reminder...');
629
+ return mTasks(cfg, post);
630
+ }
631
+ case 'corrective': {
632
+ const hid = await txt('Task history ID (from a missed task)', true);
633
+ if (!hid)
634
+ return mTasks(cfg, post);
635
+ const desc = await txt('What happened?', true);
636
+ if (!desc)
637
+ return mTasks(cfg, post);
638
+ const action = await txt('What was done to fix it?', true);
639
+ if (!action)
640
+ return mTasks(cfg, post);
641
+ await cmd(post, cfg.orgId, 'create_corrective_action', {
642
+ task_history_id: hid, description: desc, action_taken: action,
643
+ }, 'Recording corrective action...');
644
+ return mTasks(cfg, post);
307
645
  }
308
- return menuPlanTasks(cfg, post, selected, plans.find((pl) => pl.id === selected)?.name || '');
309
646
  }
310
- catch (err) {
311
- s.stop(pc.red('Failed'));
312
- p.log.error(err.message || 'Could not load plans');
647
+ return mTasks(cfg, post);
648
+ }
649
+ // ══════════════════════════════════════════════════════════════════════
650
+ // PLANS — plan + goal + rule + requirement CRUD (12 commands)
651
+ // ══════════════════════════════════════════════════════════════════════
652
+ async function mPlans(cfg, post) {
653
+ const a = await sel('Plans', [
654
+ { value: 'list', label: 'View plans' },
655
+ { value: 'create', label: 'Create a plan' },
656
+ { value: 'edit', label: 'Edit a plan' },
657
+ { value: 'delete', label: 'Delete a plan' },
658
+ { value: 'goals', label: 'Manage goals', hint: 'within a plan' },
659
+ { value: 'rules', label: 'Manage rules', hint: 'within a goal' },
660
+ { value: 'reqs', label: 'Manage requirements', hint: 'within a rule' },
661
+ { value: '__back', label: pc.dim('\u2190 Back to main menu') },
662
+ ]);
663
+ if (!a || a === '__back')
313
664
  return interactiveMenu(cfg);
665
+ switch (a) {
666
+ case 'list': {
667
+ const s = p.spinner();
668
+ s.start('Loading plans...');
669
+ const plans = await listEntities(post, cfg.orgId, 'plan');
670
+ s.stop(`${plans.length} plans`);
671
+ if (plans.length > 0) {
672
+ const lines = plans.map((pl) => {
673
+ const progress = pl.metadata?.progress != null ? `${pl.metadata.progress}%` : '';
674
+ const status = pl.metadata?.status || '';
675
+ return ` ${pc.cyan(pad(truncate(pl.name || 'Untitled', 40), 42))} ${pc.dim(pad(status, 10))} ${pc.dim(progress)}`;
676
+ });
677
+ p.note(lines.join('\n'), 'Plans');
678
+ }
679
+ return mPlans(cfg, post);
680
+ }
681
+ case 'create': {
682
+ const name = await txt('Plan name', true);
683
+ if (!name)
684
+ return mPlans(cfg, post);
685
+ const payload = { name };
686
+ const desc = await txt('Description');
687
+ if (desc)
688
+ payload.description = desc;
689
+ const priv = await yn('Private plan?');
690
+ if (priv)
691
+ payload.is_private = true;
692
+ const addGoals = await yn('Add goals now?');
693
+ if (addGoals) {
694
+ const goals = [];
695
+ let more = true;
696
+ while (more) {
697
+ const gn = await txt('Goal name', true);
698
+ if (!gn)
699
+ break;
700
+ goals.push({ name: gn });
701
+ more = !!(await yn('Add another goal?'));
702
+ }
703
+ if (goals.length > 0)
704
+ payload.goals = goals;
705
+ }
706
+ await cmd(post, cfg.orgId, 'create_plan', payload, 'Creating plan...');
707
+ return mPlans(cfg, post);
708
+ }
709
+ case 'edit': {
710
+ const plan = await pickEntity(post, cfg.orgId, 'plan', 'plans');
711
+ if (!plan)
712
+ return mPlans(cfg, post);
713
+ p.log.info(`Editing: ${pc.cyan(plan.name)}`);
714
+ const updates = {};
715
+ const n = await txt('New name (blank to skip)');
716
+ if (n === null)
717
+ return mPlans(cfg, post);
718
+ if (n)
719
+ updates.name = n;
720
+ const d = await txt('New description (blank to skip)');
721
+ if (d)
722
+ updates.description = d;
723
+ const st = await sel('Status', [
724
+ { value: '__skip', label: pc.dim('Keep current') },
725
+ { value: 'active', label: 'Active' }, { value: 'draft', label: 'Draft' }, { value: 'archived', label: 'Archived' },
726
+ ]);
727
+ if (st && st !== '__skip')
728
+ updates.status = st;
729
+ if (Object.keys(updates).length > 0) {
730
+ await cmd(post, cfg.orgId, 'update_plan', { plan_id: plan.id, updates }, 'Updating plan...');
731
+ }
732
+ else
733
+ p.log.info('No changes.');
734
+ return mPlans(cfg, post);
735
+ }
736
+ case 'delete': {
737
+ const plan = await pickEntity(post, cfg.orgId, 'plan', 'plans');
738
+ if (!plan)
739
+ return mPlans(cfg, post);
740
+ const sure = await yn(`Delete "${plan.name}" and everything inside it? This cannot be undone.`);
741
+ if (sure)
742
+ await cmd(post, cfg.orgId, 'delete_plan', { plan_id: plan.id }, 'Deleting plan...');
743
+ return mPlans(cfg, post);
744
+ }
745
+ case 'goals': return mGoals(cfg, post);
746
+ case 'rules': return mPlanRules(cfg, post);
747
+ case 'reqs': return mReqs(cfg, post);
314
748
  }
749
+ return mPlans(cfg, post);
315
750
  }
316
- async function menuPlanTasks(cfg, post, planId, planName) {
317
- const s = p.spinner();
318
- s.start('Loading tasks...');
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
322
- const res = await post('/api/v2/query', {
323
- type: 'list_entities',
324
- organization_id: cfg.orgId,
325
- payload: { entity_type: 'task', organization_id: cfg.orgId },
326
- });
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);
330
- s.stop(`${tasks.length} task${tasks.length === 1 ? '' : 's'} in ${pc.cyan(planName)}`);
331
- if (tasks.length === 0) {
332
- p.log.info('No tasks found for this plan.');
333
- return interactiveMenu(cfg);
334
- }
335
- const lines = tasks.map((t) => {
336
- const name = truncate(t.name || 'Untitled', 50);
337
- const freq = t.metadata?.frequency || '';
338
- return ` ${pc.cyan(pad(name, 52))} ${pc.dim(freq)}`;
339
- });
340
- p.note(lines.join('\n'), planName);
341
- return interactiveMenu(cfg);
751
+ async function mGoals(cfg, post) {
752
+ const a = await sel('Goals', [
753
+ { value: 'list', label: 'View goals in a plan' },
754
+ { value: 'create', label: 'Add a goal to a plan' },
755
+ { value: 'edit', label: 'Edit a goal' },
756
+ { value: 'delete', label: 'Delete a goal' },
757
+ { value: '__back', label: pc.dim('\u2190 Back') },
758
+ ]);
759
+ if (!a || a === '__back')
760
+ return mPlans(cfg, post);
761
+ switch (a) {
762
+ case 'list': {
763
+ const plan = await pickEntity(post, cfg.orgId, 'plan', 'plans');
764
+ if (!plan)
765
+ return mGoals(cfg, post);
766
+ const s = p.spinner();
767
+ s.start('Loading goals...');
768
+ const goals = await listEntities(post, cfg.orgId, 'goal', { plan_id: plan.id });
769
+ s.stop(`${goals.length} goals in "${plan.name}"`);
770
+ if (goals.length > 0) {
771
+ const lines = goals.map((g) => ` ${pc.cyan(g.name || 'Untitled')}`);
772
+ p.note(lines.join('\n'), plan.name);
773
+ }
774
+ return mGoals(cfg, post);
775
+ }
776
+ case 'create': {
777
+ const plan = await pickEntity(post, cfg.orgId, 'plan', 'plans');
778
+ if (!plan)
779
+ return mGoals(cfg, post);
780
+ const name = await txt('Goal name', true);
781
+ if (!name)
782
+ return mGoals(cfg, post);
783
+ await cmd(post, cfg.orgId, 'create_goal', { plan_id: plan.id, name }, 'Creating goal...');
784
+ return mGoals(cfg, post);
785
+ }
786
+ case 'edit': {
787
+ const goal = await pickEntity(post, cfg.orgId, 'goal', 'goals');
788
+ if (!goal)
789
+ return mGoals(cfg, post);
790
+ const name = await txt('New name', true);
791
+ if (!name)
792
+ return mGoals(cfg, post);
793
+ await cmd(post, cfg.orgId, 'update_goal', { goal_id: goal.id, updates: { name } }, 'Updating goal...');
794
+ return mGoals(cfg, post);
795
+ }
796
+ case 'delete': {
797
+ const goal = await pickEntity(post, cfg.orgId, 'goal', 'goals');
798
+ if (!goal)
799
+ return mGoals(cfg, post);
800
+ const sure = await yn(`Delete "${goal.name}"?`);
801
+ if (sure)
802
+ await cmd(post, cfg.orgId, 'delete_goal', { goal_id: goal.id }, 'Deleting goal...');
803
+ return mGoals(cfg, post);
804
+ }
342
805
  }
343
- catch (err) {
344
- s.stop(pc.red('Failed'));
345
- p.log.error(err.message || 'Could not load tasks');
346
- return interactiveMenu(cfg);
806
+ return mGoals(cfg, post);
807
+ }
808
+ async function mPlanRules(cfg, post) {
809
+ const a = await sel('Rules (within goals)', [
810
+ { value: 'list', label: 'View rules in a goal' },
811
+ { value: 'create', label: 'Add a rule to a goal' },
812
+ { value: 'edit', label: 'Edit a rule' },
813
+ { value: 'delete', label: 'Delete a rule' },
814
+ { value: '__back', label: pc.dim('\u2190 Back') },
815
+ ]);
816
+ if (!a || a === '__back')
817
+ return mPlans(cfg, post);
818
+ switch (a) {
819
+ case 'list': {
820
+ const s = p.spinner();
821
+ s.start('Loading rules...');
822
+ const rules = await listEntities(post, cfg.orgId, 'rule');
823
+ s.stop(`${rules.length} rules`);
824
+ if (rules.length > 0) {
825
+ const lines = rules.slice(0, 50).map((r) => ` ${pc.cyan(truncate(r.name || 'Untitled', 60))}`);
826
+ p.note(lines.join('\n'), 'Rules');
827
+ }
828
+ return mPlanRules(cfg, post);
829
+ }
830
+ case 'create': {
831
+ const plan = await pickEntity(post, cfg.orgId, 'plan', 'plans');
832
+ if (!plan)
833
+ return mPlanRules(cfg, post);
834
+ const goal = await pickEntity(post, cfg.orgId, 'goal', 'goals', { plan_id: plan.id });
835
+ if (!goal)
836
+ return mPlanRules(cfg, post);
837
+ const name = await txt('Rule name', true);
838
+ if (!name)
839
+ return mPlanRules(cfg, post);
840
+ await cmd(post, cfg.orgId, 'create_rule', { plan_id: plan.id, goal_id: goal.id, name }, 'Creating rule...');
841
+ return mPlanRules(cfg, post);
842
+ }
843
+ case 'edit': {
844
+ const rule = await pickEntity(post, cfg.orgId, 'rule', 'rules');
845
+ if (!rule)
846
+ return mPlanRules(cfg, post);
847
+ const name = await txt('New name', true);
848
+ if (!name)
849
+ return mPlanRules(cfg, post);
850
+ await cmd(post, cfg.orgId, 'update_rule', { rule_id: rule.id, updates: { name } }, 'Updating rule...');
851
+ return mPlanRules(cfg, post);
852
+ }
853
+ case 'delete': {
854
+ const rule = await pickEntity(post, cfg.orgId, 'rule', 'rules');
855
+ if (!rule)
856
+ return mPlanRules(cfg, post);
857
+ const sure = await yn(`Delete "${rule.name}"?`);
858
+ if (sure)
859
+ await cmd(post, cfg.orgId, 'delete_rule', { rule_id: rule.id }, 'Deleting rule...');
860
+ return mPlanRules(cfg, post);
861
+ }
347
862
  }
863
+ return mPlanRules(cfg, post);
348
864
  }
349
- // ── Tasks ───────────────────────────────────────────────────────────
350
- async function menuTasks(cfg, post) {
351
- const s = p.spinner();
352
- s.start('Loading tasks...');
353
- try {
354
- const tasks = await listEntities(post, cfg.orgId, 'task');
355
- s.stop(`Found ${tasks.length} task${tasks.length === 1 ? '' : 's'}`);
356
- if (tasks.length === 0) {
357
- p.log.info('No tasks yet.');
358
- return interactiveMenu(cfg);
865
+ async function mReqs(cfg, post) {
866
+ const a = await sel('Requirements', [
867
+ { value: 'list', label: 'View requirements' },
868
+ { value: 'create', label: 'Add a requirement' },
869
+ { value: 'edit', label: 'Edit a requirement' },
870
+ { value: 'delete', label: 'Delete a requirement' },
871
+ { value: '__back', label: pc.dim('\u2190 Back') },
872
+ ]);
873
+ if (!a || a === '__back')
874
+ return mPlans(cfg, post);
875
+ switch (a) {
876
+ case 'list': {
877
+ const s = p.spinner();
878
+ s.start('Loading requirements...');
879
+ const reqs = await listEntities(post, cfg.orgId, 'requirement');
880
+ s.stop(`${reqs.length} requirements`);
881
+ if (reqs.length > 0) {
882
+ const lines = reqs.slice(0, 50).map((r) => ` ${pc.cyan(truncate(r.name || 'Untitled', 60))}`);
883
+ p.note(lines.join('\n'), 'Requirements');
884
+ }
885
+ return mReqs(cfg, post);
359
886
  }
360
- const selected = await p.select({
361
- message: 'Select a task',
362
- options: [
363
- ...tasks.slice(0, 25).map((t) => ({
364
- value: t.id,
365
- label: truncate(t.name || 'Untitled', 60),
366
- hint: t.metadata?.frequency || '',
367
- })),
368
- ...(tasks.length > 25 ? [{ value: '__more', label: pc.dim(`...and ${tasks.length - 25} more`) }] : []),
369
- { value: '__back', label: pc.dim('Back to menu') },
370
- ],
371
- });
372
- if (p.isCancel(selected) || selected === '__back' || selected === '__more') {
373
- return interactiveMenu(cfg);
887
+ case 'create': {
888
+ const plan = await pickEntity(post, cfg.orgId, 'plan', 'plans');
889
+ if (!plan)
890
+ return mReqs(cfg, post);
891
+ const rule = await pickEntity(post, cfg.orgId, 'rule', 'rules', { plan_id: plan.id });
892
+ if (!rule)
893
+ return mReqs(cfg, post);
894
+ const name = await txt('Requirement name', true);
895
+ if (!name)
896
+ return mReqs(cfg, post);
897
+ const payload = { plan_id: plan.id, rule_id: rule.id, name };
898
+ const desc = await txt('Description');
899
+ if (desc)
900
+ payload.description = desc;
901
+ await cmd(post, cfg.orgId, 'create_requirement', payload, 'Creating requirement...');
902
+ return mReqs(cfg, post);
374
903
  }
375
- const task = tasks.find((t) => t.id === selected);
376
- if (task) {
377
- const meta = task.metadata || {};
378
- const lines = [
379
- `${pad('Name', 14)} ${pc.cyan(task.name || 'Untitled')}`,
380
- `${pad('Frequency', 14)} ${meta.frequency || pc.dim('none')}`,
381
- `${pad('Assigned to', 14)} ${meta.assigned_to || pc.dim('unassigned')}`,
382
- `${pad('ID', 14)} ${pc.dim(task.id)}`,
383
- ];
384
- p.note(lines.join('\n'), task.name || 'Task');
385
- const action = await p.select({
386
- message: 'What would you like to do?',
387
- options: [
388
- { value: 'complete', label: 'Complete this task' },
389
- { value: 'back', label: pc.dim('Back to menu') },
390
- ],
391
- });
392
- if (action === 'complete' && !p.isCancel(action)) {
393
- await doComplete(cfg, post, task.id, task.name);
904
+ case 'edit': {
905
+ const req = await pickEntity(post, cfg.orgId, 'requirement', 'requirements');
906
+ if (!req)
907
+ return mReqs(cfg, post);
908
+ const updates = {};
909
+ const n = await txt('New name (blank to skip)');
910
+ if (n === null)
911
+ return mReqs(cfg, post);
912
+ if (n)
913
+ updates.name = n;
914
+ const d = await txt('New description (blank to skip)');
915
+ if (d)
916
+ updates.description = d;
917
+ if (Object.keys(updates).length > 0) {
918
+ await cmd(post, cfg.orgId, 'update_requirement', { requirement_id: req.id, updates }, 'Updating...');
394
919
  }
920
+ return mReqs(cfg, post);
921
+ }
922
+ case 'delete': {
923
+ const req = await pickEntity(post, cfg.orgId, 'requirement', 'requirements');
924
+ if (!req)
925
+ return mReqs(cfg, post);
926
+ const sure = await yn(`Delete "${req.name}"?`);
927
+ if (sure)
928
+ await cmd(post, cfg.orgId, 'delete_requirement', { requirement_id: req.id }, 'Deleting...');
929
+ return mReqs(cfg, post);
395
930
  }
396
- return interactiveMenu(cfg);
397
931
  }
398
- catch (err) {
399
- s.stop(pc.red('Failed'));
400
- p.log.error(err.message || 'Could not load tasks');
932
+ return mReqs(cfg, post);
933
+ }
934
+ // ══════════════════════════════════════════════════════════════════════
935
+ // DATA TABLES — 7 commands
936
+ // ══════════════════════════════════════════════════════════════════════
937
+ async function mTables(cfg, post) {
938
+ const a = await sel('Data Tables', [
939
+ { value: 'list', label: 'View tables' },
940
+ { value: 'create', label: 'Create a table' },
941
+ { value: 'edit', label: 'Edit a table' },
942
+ { value: 'delete', label: 'Delete a table' },
943
+ { value: 'add-record', label: 'Add a record', hint: 'add data to a table' },
944
+ { value: 'edit-record', label: 'Edit a record' },
945
+ { value: 'delete-record', label: 'Delete a record' },
946
+ { value: 'bulk-delete', label: 'Bulk delete records' },
947
+ { value: '__back', label: pc.dim('\u2190 Back to main menu') },
948
+ ]);
949
+ if (!a || a === '__back')
401
950
  return interactiveMenu(cfg);
951
+ switch (a) {
952
+ case 'list': {
953
+ const s = p.spinner();
954
+ s.start('Loading tables...');
955
+ const tables = await listEntities(post, cfg.orgId, 'table');
956
+ s.stop(`${tables.length} tables`);
957
+ if (tables.length > 0) {
958
+ const lines = tables.map((t) => ` ${pc.cyan(truncate(t.name || 'Untitled', 50))} ${pc.dim(t.id)}`);
959
+ p.note(lines.join('\n'), 'Tables');
960
+ }
961
+ return mTables(cfg, post);
962
+ }
963
+ case 'create': {
964
+ const name = await txt('Table name', true);
965
+ if (!name)
966
+ return mTables(cfg, post);
967
+ const desc = await txt('Description');
968
+ const payload = { name };
969
+ if (desc)
970
+ payload.description = desc;
971
+ // Build schema interactively
972
+ const fields = [];
973
+ p.log.info('Define your table columns:');
974
+ let addMore = true;
975
+ while (addMore) {
976
+ const fn = await txt('Column name', true);
977
+ if (!fn)
978
+ break;
979
+ const ft = await sel('Column type', [
980
+ { value: 'text', label: 'Text' }, { value: 'number', label: 'Number' },
981
+ { value: 'date', label: 'Date' }, { value: 'select', label: 'Dropdown' },
982
+ { value: 'checkbox', label: 'Checkbox' }, { value: 'file', label: 'File' },
983
+ ]);
984
+ if (!ft)
985
+ break;
986
+ const req = await yn('Required?');
987
+ fields.push({ name: fn, type: ft, ...(req ? { required: true } : {}) });
988
+ addMore = !!(await yn('Add another column?'));
989
+ }
990
+ if (fields.length > 0)
991
+ payload.schema = fields;
992
+ await cmd(post, cfg.orgId, 'create_table', payload, 'Creating table...');
993
+ return mTables(cfg, post);
994
+ }
995
+ case 'edit': {
996
+ const table = await pickEntity(post, cfg.orgId, 'table', 'tables');
997
+ if (!table)
998
+ return mTables(cfg, post);
999
+ const updates = {};
1000
+ const n = await txt('New name (blank to skip)');
1001
+ if (n === null)
1002
+ return mTables(cfg, post);
1003
+ if (n)
1004
+ updates.name = n;
1005
+ const d = await txt('New description (blank to skip)');
1006
+ if (d)
1007
+ updates.description = d;
1008
+ if (Object.keys(updates).length > 0) {
1009
+ await cmd(post, cfg.orgId, 'update_table', { table_id: table.id, updates }, 'Updating table...');
1010
+ }
1011
+ return mTables(cfg, post);
1012
+ }
1013
+ case 'delete': {
1014
+ const table = await pickEntity(post, cfg.orgId, 'table', 'tables');
1015
+ if (!table)
1016
+ return mTables(cfg, post);
1017
+ const sure = await yn(`Delete "${table.name}" and all its data? This cannot be undone.`);
1018
+ if (sure)
1019
+ await cmd(post, cfg.orgId, 'delete_table', { table_id: table.id }, 'Deleting table...');
1020
+ return mTables(cfg, post);
1021
+ }
1022
+ case 'add-record': {
1023
+ const table = await pickEntity(post, cfg.orgId, 'table', 'tables');
1024
+ if (!table)
1025
+ return mTables(cfg, post);
1026
+ p.log.info(`Adding record to: ${pc.cyan(table.name)}`);
1027
+ p.log.info(pc.dim('Enter field values. Leave blank to skip optional fields.'));
1028
+ const data = {};
1029
+ // Get schema fields from metadata
1030
+ const schema = table.metadata?.schema || [];
1031
+ if (Array.isArray(schema) && schema.length > 0) {
1032
+ for (const field of schema) {
1033
+ const val = await txt(`${field.name || field.label || 'Field'}`, !!field.required);
1034
+ if (val === null)
1035
+ return mTables(cfg, post);
1036
+ if (val)
1037
+ data[field.name || field.label] = val;
1038
+ }
1039
+ }
1040
+ else {
1041
+ p.log.info('Could not load table schema. Enter field name/value pairs:');
1042
+ let addMore = true;
1043
+ while (addMore) {
1044
+ const fn = await txt('Field name', true);
1045
+ if (!fn)
1046
+ break;
1047
+ const fv = await txt('Value', true);
1048
+ if (!fv)
1049
+ break;
1050
+ data[fn] = fv;
1051
+ addMore = !!(await yn('Add another field?'));
1052
+ }
1053
+ }
1054
+ if (Object.keys(data).length > 0) {
1055
+ await cmd(post, cfg.orgId, 'create_table_record', { table_id: table.id, data }, 'Adding record...');
1056
+ }
1057
+ return mTables(cfg, post);
1058
+ }
1059
+ case 'edit-record': {
1060
+ const table = await pickEntity(post, cfg.orgId, 'table', 'tables');
1061
+ if (!table)
1062
+ return mTables(cfg, post);
1063
+ const record = await pickEntity(post, cfg.orgId, 'table_record', 'records', { table_id: table.id });
1064
+ if (!record)
1065
+ return mTables(cfg, post);
1066
+ const updates = {};
1067
+ let addMore = true;
1068
+ while (addMore) {
1069
+ const fn = await txt('Field to update');
1070
+ if (!fn)
1071
+ break;
1072
+ const fv = await txt('New value', true);
1073
+ if (!fv)
1074
+ break;
1075
+ updates[fn] = fv;
1076
+ addMore = !!(await yn('Update another field?'));
1077
+ }
1078
+ if (Object.keys(updates).length > 0) {
1079
+ await cmd(post, cfg.orgId, 'update_table_record', { record_id: record.id, updates }, 'Updating record...');
1080
+ }
1081
+ return mTables(cfg, post);
1082
+ }
1083
+ case 'delete-record': {
1084
+ const table = await pickEntity(post, cfg.orgId, 'table', 'tables');
1085
+ if (!table)
1086
+ return mTables(cfg, post);
1087
+ const record = await pickEntity(post, cfg.orgId, 'table_record', 'records', { table_id: table.id });
1088
+ if (!record)
1089
+ return mTables(cfg, post);
1090
+ const sure = await yn('Delete this record?');
1091
+ if (sure)
1092
+ await cmd(post, cfg.orgId, 'delete_table_record', { record_id: record.id }, 'Deleting record...');
1093
+ return mTables(cfg, post);
1094
+ }
1095
+ case 'bulk-delete': {
1096
+ const table = await pickEntity(post, cfg.orgId, 'table', 'tables');
1097
+ if (!table)
1098
+ return mTables(cfg, post);
1099
+ const ids = await txt('Record IDs to delete (comma-separated)', true);
1100
+ if (!ids)
1101
+ return mTables(cfg, post);
1102
+ const recordIds = ids.split(',').map(s => s.trim()).filter(Boolean);
1103
+ const sure = await yn(`Delete ${recordIds.length} records? This cannot be undone.`);
1104
+ if (sure) {
1105
+ await cmd(post, cfg.orgId, 'bulk_delete_table_records', { table_id: table.id, record_ids: recordIds }, 'Deleting records...');
1106
+ }
1107
+ return mTables(cfg, post);
1108
+ }
402
1109
  }
1110
+ return mTables(cfg, post);
403
1111
  }
404
- // ── Status ──────────────────────────────────────────────────────────
405
- async function menuStatus(cfg, post) {
406
- const s = p.spinner();
407
- s.start('Loading workspace...');
408
- try {
409
- const nodes = await getWorkspaceNodes(post, cfg.orgId);
410
- s.stop('Workspace loaded');
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
- }
417
- const lines = [
418
- `${pad('Workspace', 16)} ${pc.cyan(cfg.orgName)}`,
419
- '',
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))}`,
427
- ];
428
- p.note(lines.join('\n'), 'Workspace Overview');
429
- }
430
- catch (err) {
431
- s.stop(pc.red('Failed'));
432
- p.log.error(err.message || 'Could not load workspace');
433
- }
434
- return interactiveMenu(cfg);
435
- }
436
- // ── Search ──────────────────────────────────────────────────────────
437
- async function menuSearch(cfg, post) {
438
- const query = await p.text({
439
- message: 'Search for...',
440
- placeholder: 'task name, plan, document...',
441
- validate: (v) => {
442
- if (!v)
443
- return 'Enter a search term';
444
- },
445
- });
446
- if (p.isCancel(query))
1112
+ // ══════════════════════════════════════════════════════════════════════
1113
+ // DOCUMENTS 6 commands
1114
+ // ══════════════════════════════════════════════════════════════════════
1115
+ async function mDocs(cfg, post) {
1116
+ const a = await sel('Documents', [
1117
+ { value: 'list', label: 'View documents' },
1118
+ { value: 'create', label: 'Create a document or folder' },
1119
+ { value: 'edit', label: 'Edit a document' },
1120
+ { value: 'delete', label: 'Delete a document' },
1121
+ { value: 'attach', label: 'Attach files', hint: 'upload files to a document' },
1122
+ { value: 'edit-file', label: 'Edit a file attachment' },
1123
+ { value: 'delete-file', label: 'Remove a file attachment' },
1124
+ { value: '__back', label: pc.dim('\u2190 Back to main menu') },
1125
+ ]);
1126
+ if (!a || a === '__back')
447
1127
  return interactiveMenu(cfg);
448
- const s = p.spinner();
449
- s.start('Searching...');
450
- try {
451
- const entities = await searchEntities(post, cfg.orgId, query);
452
- s.stop(`Found ${entities.length} result${entities.length === 1 ? '' : 's'}`);
453
- if (entities.length === 0) {
454
- p.log.info('No results found.');
1128
+ switch (a) {
1129
+ case 'list': {
1130
+ const s = p.spinner();
1131
+ s.start('Loading documents...');
1132
+ const docs = await listEntities(post, cfg.orgId, 'document');
1133
+ s.stop(`${docs.length} documents`);
1134
+ if (docs.length > 0) {
1135
+ const lines = docs.map((d) => ` ${pc.cyan(truncate(d.name || 'Untitled', 50))} ${pc.dim(d.id)}`);
1136
+ p.note(lines.join('\n'), 'Documents');
1137
+ }
1138
+ return mDocs(cfg, post);
1139
+ }
1140
+ case 'create': {
1141
+ const name = await txt('Name', true);
1142
+ if (!name)
1143
+ return mDocs(cfg, post);
1144
+ const isFolder = await yn('Is this a folder?');
1145
+ const payload = { name };
1146
+ if (isFolder)
1147
+ payload.is_folder = true;
1148
+ const url = isFolder ? null : await txt('File URL (blank to skip)');
1149
+ if (url)
1150
+ payload.file_url = url;
1151
+ await cmd(post, cfg.orgId, 'create_document', payload, 'Creating...');
1152
+ return mDocs(cfg, post);
1153
+ }
1154
+ case 'edit': {
1155
+ const doc = await pickEntity(post, cfg.orgId, 'document', 'documents');
1156
+ if (!doc)
1157
+ return mDocs(cfg, post);
1158
+ const updates = {};
1159
+ const n = await txt('New name (blank to skip)');
1160
+ if (n === null)
1161
+ return mDocs(cfg, post);
1162
+ if (n)
1163
+ updates.name = n;
1164
+ const url = await txt('New file URL (blank to skip)');
1165
+ if (url)
1166
+ updates.file_url = url;
1167
+ if (Object.keys(updates).length > 0) {
1168
+ await cmd(post, cfg.orgId, 'update_document', { document_id: doc.id, updates }, 'Updating...');
1169
+ }
1170
+ return mDocs(cfg, post);
1171
+ }
1172
+ case 'delete': {
1173
+ const doc = await pickEntity(post, cfg.orgId, 'document', 'documents');
1174
+ if (!doc)
1175
+ return mDocs(cfg, post);
1176
+ const sure = await yn(`Delete "${doc.name}"?`);
1177
+ if (sure)
1178
+ await cmd(post, cfg.orgId, 'delete_document', { document_id: doc.id }, 'Deleting...');
1179
+ return mDocs(cfg, post);
1180
+ }
1181
+ case 'attach': {
1182
+ const doc = await pickEntity(post, cfg.orgId, 'document', 'documents');
1183
+ if (!doc)
1184
+ return mDocs(cfg, post);
1185
+ const entries = [];
1186
+ let addMore = true;
1187
+ while (addMore) {
1188
+ const url = await txt('File URL', true);
1189
+ if (!url)
1190
+ break;
1191
+ const name = await txt('File name (blank for auto)');
1192
+ entries.push({ document_url: url, ...(name ? { name } : {}) });
1193
+ addMore = !!(await yn('Attach another file?'));
1194
+ }
1195
+ if (entries.length > 0) {
1196
+ await cmd(post, cfg.orgId, 'create_document_completion', { document_id: doc.id, entries }, 'Attaching files...');
1197
+ }
1198
+ return mDocs(cfg, post);
1199
+ }
1200
+ case 'edit-file': {
1201
+ const comp = await pickEntity(post, cfg.orgId, 'document_completion', 'file attachments');
1202
+ if (!comp)
1203
+ return mDocs(cfg, post);
1204
+ const updates = {};
1205
+ const n = await txt('New file name (blank to skip)');
1206
+ if (n)
1207
+ updates.name = n;
1208
+ if (Object.keys(updates).length > 0) {
1209
+ await cmd(post, cfg.orgId, 'update_document_completion', { completion_id: comp.id, updates }, 'Updating...');
1210
+ }
1211
+ return mDocs(cfg, post);
455
1212
  }
456
- else {
457
- const lines = entities.map((e) => {
458
- const type = pad(e.entity_type || 'unknown', 14);
459
- const name = truncate(e.name || 'Untitled', 50);
460
- return ` ${pc.dim(type)} ${pc.cyan(name)}`;
461
- });
462
- p.note(lines.join('\n'), `Results for "${query}"`);
1213
+ case 'delete-file': {
1214
+ const comp = await pickEntity(post, cfg.orgId, 'document_completion', 'file attachments');
1215
+ if (!comp)
1216
+ return mDocs(cfg, post);
1217
+ const sure = await yn('Remove this file attachment?');
1218
+ if (sure)
1219
+ await cmd(post, cfg.orgId, 'delete_document_completion', { completion_id: comp.id }, 'Removing...');
1220
+ return mDocs(cfg, post);
463
1221
  }
464
1222
  }
465
- catch (err) {
466
- s.stop(pc.red('Failed'));
467
- p.log.error(err.message || 'Search failed');
1223
+ return mDocs(cfg, post);
1224
+ }
1225
+ // ══════════════════════════════════════════════════════════════════════
1226
+ // ASSETS — photos (3) + geolocation (3) + checklists (3) = 9 commands
1227
+ // ══════════════════════════════════════════════════════════════════════
1228
+ async function mAssets(cfg, post) {
1229
+ const a = await sel('Assets', [
1230
+ { value: 'photos', label: 'Photos', hint: 'photo validation assets' },
1231
+ { value: 'locations', label: 'Locations', hint: 'geofenced areas for check-ins' },
1232
+ { value: 'checklists', label: 'Checklists', hint: 'checklist templates' },
1233
+ { value: '__back', label: pc.dim('\u2190 Back to main menu') },
1234
+ ]);
1235
+ if (!a || a === '__back')
1236
+ return interactiveMenu(cfg);
1237
+ switch (a) {
1238
+ case 'photos': return mPhotos(cfg, post);
1239
+ case 'locations': return mGeo(cfg, post);
1240
+ case 'checklists': return mChecklists(cfg, post);
468
1241
  }
469
- return interactiveMenu(cfg);
1242
+ return mAssets(cfg, post);
470
1243
  }
471
- // ── Complete Task ───────────────────────────────────────────────────
472
- async function menuComplete(cfg, post) {
473
- const s = p.spinner();
474
- s.start('Loading tasks...');
475
- try {
476
- const tasks = await listEntities(post, cfg.orgId, 'task');
477
- s.stop(`${tasks.length} task${tasks.length === 1 ? '' : 's'} available`);
478
- if (tasks.length === 0) {
479
- p.log.info('No tasks to complete.');
480
- return interactiveMenu(cfg);
1244
+ async function mPhotos(cfg, post) {
1245
+ const a = await sel('Photos', [
1246
+ { value: 'list', label: 'View photo assets' },
1247
+ { value: 'create', label: 'Create a photo asset' },
1248
+ { value: 'edit', label: 'Edit a photo asset' },
1249
+ { value: 'delete', label: 'Delete a photo asset' },
1250
+ { value: '__back', label: pc.dim('\u2190 Back') },
1251
+ ]);
1252
+ if (!a || a === '__back')
1253
+ return mAssets(cfg, post);
1254
+ switch (a) {
1255
+ case 'list': {
1256
+ const s = p.spinner();
1257
+ s.start('Loading...');
1258
+ const items = await listEntities(post, cfg.orgId, 'photo');
1259
+ s.stop(`${items.length} photo assets`);
1260
+ if (items.length > 0) {
1261
+ const lines = items.map((i) => ` ${pc.cyan(i.name || 'Untitled')}`);
1262
+ p.note(lines.join('\n'), 'Photos');
1263
+ }
1264
+ return mPhotos(cfg, post);
481
1265
  }
482
- const selected = await p.select({
483
- message: 'Which task did you complete?',
484
- options: [
485
- ...tasks.slice(0, 30).map((t) => ({
486
- value: t.id,
487
- label: truncate(t.name || 'Untitled', 60),
488
- hint: t.metadata?.frequency || '',
489
- })),
490
- { value: '__back', label: pc.dim('Back to menu') },
491
- ],
492
- });
493
- if (p.isCancel(selected) || selected === '__back') {
494
- return interactiveMenu(cfg);
1266
+ case 'create': {
1267
+ const name = await txt('Photo asset name', true);
1268
+ if (!name)
1269
+ return mPhotos(cfg, post);
1270
+ const desc = await txt('Description');
1271
+ const payload = { name };
1272
+ if (desc)
1273
+ payload.description = desc;
1274
+ await cmd(post, cfg.orgId, 'create_photo', payload, 'Creating...');
1275
+ return mPhotos(cfg, post);
1276
+ }
1277
+ case 'edit': {
1278
+ const photo = await pickEntity(post, cfg.orgId, 'photo', 'photo assets');
1279
+ if (!photo)
1280
+ return mPhotos(cfg, post);
1281
+ const updates = {};
1282
+ const n = await txt('New name (blank to skip)');
1283
+ if (n)
1284
+ updates.name = n;
1285
+ const d = await txt('New description (blank to skip)');
1286
+ if (d)
1287
+ updates.description = d;
1288
+ if (Object.keys(updates).length > 0) {
1289
+ await cmd(post, cfg.orgId, 'update_photo', { photo_id: photo.id, updates }, 'Updating...');
1290
+ }
1291
+ return mPhotos(cfg, post);
1292
+ }
1293
+ case 'delete': {
1294
+ const photo = await pickEntity(post, cfg.orgId, 'photo', 'photo assets');
1295
+ if (!photo)
1296
+ return mPhotos(cfg, post);
1297
+ const sure = await yn(`Delete "${photo.name}"?`);
1298
+ if (sure)
1299
+ await cmd(post, cfg.orgId, 'delete_photo', { photo_id: photo.id }, 'Deleting...');
1300
+ return mPhotos(cfg, post);
495
1301
  }
496
- const task = tasks.find((t) => t.id === selected);
497
- await doComplete(cfg, post, selected, task?.name || '');
498
1302
  }
499
- catch (err) {
500
- s.stop(pc.red('Failed'));
501
- p.log.error(err.message || 'Could not load tasks');
1303
+ return mPhotos(cfg, post);
1304
+ }
1305
+ async function mGeo(cfg, post) {
1306
+ const a = await sel('Locations', [
1307
+ { value: 'list', label: 'View location areas' },
1308
+ { value: 'create', label: 'Create a location area' },
1309
+ { value: 'edit', label: 'Edit a location area' },
1310
+ { value: 'delete', label: 'Delete a location area' },
1311
+ { value: '__back', label: pc.dim('\u2190 Back') },
1312
+ ]);
1313
+ if (!a || a === '__back')
1314
+ return mAssets(cfg, post);
1315
+ switch (a) {
1316
+ case 'list': {
1317
+ const s = p.spinner();
1318
+ s.start('Loading...');
1319
+ const items = await listEntities(post, cfg.orgId, 'geolocation_area');
1320
+ s.stop(`${items.length} location areas`);
1321
+ if (items.length > 0) {
1322
+ const lines = items.map((i) => ` ${pc.cyan(i.name || 'Untitled')}`);
1323
+ p.note(lines.join('\n'), 'Location Areas');
1324
+ }
1325
+ return mGeo(cfg, post);
1326
+ }
1327
+ case 'create': {
1328
+ const label = await txt('Area name', true, 'e.g. Main warehouse');
1329
+ if (!label)
1330
+ return mGeo(cfg, post);
1331
+ const lat = await txt('Latitude', true, 'e.g. 25.7617');
1332
+ if (!lat)
1333
+ return mGeo(cfg, post);
1334
+ const lng = await txt('Longitude', true, 'e.g. -80.1918');
1335
+ if (!lng)
1336
+ return mGeo(cfg, post);
1337
+ const radius = await txt('Radius in meters', true, '100');
1338
+ if (!radius)
1339
+ return mGeo(cfg, post);
1340
+ await cmd(post, cfg.orgId, 'create_geolocation_area', {
1341
+ label, latitude: parseFloat(lat), longitude: parseFloat(lng), radius: parseFloat(radius),
1342
+ }, 'Creating location area...');
1343
+ return mGeo(cfg, post);
1344
+ }
1345
+ case 'edit': {
1346
+ const area = await pickEntity(post, cfg.orgId, 'geolocation_area', 'location areas');
1347
+ if (!area)
1348
+ return mGeo(cfg, post);
1349
+ const updates = {};
1350
+ const n = await txt('New name (blank to skip)');
1351
+ if (n)
1352
+ updates.label = n;
1353
+ const lat = await txt('New latitude (blank to skip)');
1354
+ if (lat)
1355
+ updates.latitude = parseFloat(lat);
1356
+ const lng = await txt('New longitude (blank to skip)');
1357
+ if (lng)
1358
+ updates.longitude = parseFloat(lng);
1359
+ const r = await txt('New radius (blank to skip)');
1360
+ if (r)
1361
+ updates.radius = parseFloat(r);
1362
+ if (Object.keys(updates).length > 0) {
1363
+ await cmd(post, cfg.orgId, 'update_geolocation_area', { area_id: area.id, updates }, 'Updating...');
1364
+ }
1365
+ return mGeo(cfg, post);
1366
+ }
1367
+ case 'delete': {
1368
+ const area = await pickEntity(post, cfg.orgId, 'geolocation_area', 'location areas');
1369
+ if (!area)
1370
+ return mGeo(cfg, post);
1371
+ const sure = await yn(`Delete "${area.name}"?`);
1372
+ if (sure)
1373
+ await cmd(post, cfg.orgId, 'delete_geolocation_area', { area_id: area.id }, 'Deleting...');
1374
+ return mGeo(cfg, post);
1375
+ }
502
1376
  }
503
- return interactiveMenu(cfg);
1377
+ return mGeo(cfg, post);
504
1378
  }
505
- async function doComplete(cfg, post, taskId, taskName) {
506
- const s = p.spinner();
507
- s.start('Completing task...');
508
- try {
509
- await post('/api/v2/commands', {
510
- type: 'complete_task',
511
- organization_id: cfg.orgId,
512
- payload: { task_id: taskId },
513
- });
514
- s.stop(pc.green(`Done! "${taskName}" completed`));
1379
+ async function mChecklists(cfg, post) {
1380
+ const a = await sel('Checklists', [
1381
+ { value: 'list', label: 'View checklists' },
1382
+ { value: 'create', label: 'Create a checklist' },
1383
+ { value: 'edit', label: 'Edit a checklist' },
1384
+ { value: 'delete', label: 'Delete a checklist' },
1385
+ { value: '__back', label: pc.dim('\u2190 Back') },
1386
+ ]);
1387
+ if (!a || a === '__back')
1388
+ return mAssets(cfg, post);
1389
+ switch (a) {
1390
+ case 'list': {
1391
+ const s = p.spinner();
1392
+ s.start('Loading...');
1393
+ const items = await listEntities(post, cfg.orgId, 'checklist');
1394
+ s.stop(`${items.length} checklists`);
1395
+ if (items.length > 0) {
1396
+ const lines = items.map((i) => ` ${pc.cyan(i.name || 'Untitled')}`);
1397
+ p.note(lines.join('\n'), 'Checklists');
1398
+ }
1399
+ return mChecklists(cfg, post);
1400
+ }
1401
+ case 'create': {
1402
+ const name = await txt('Checklist name', true);
1403
+ if (!name)
1404
+ return mChecklists(cfg, post);
1405
+ const desc = await txt('Description');
1406
+ const items = [];
1407
+ p.log.info('Add checklist items:');
1408
+ let addMore = true;
1409
+ while (addMore) {
1410
+ const item = await txt('Item', true);
1411
+ if (!item)
1412
+ break;
1413
+ items.push(item);
1414
+ addMore = !!(await yn('Add another item?'));
1415
+ }
1416
+ const payload = { name, items };
1417
+ if (desc)
1418
+ payload.description = desc;
1419
+ await cmd(post, cfg.orgId, 'create_checklist', payload, 'Creating checklist...');
1420
+ return mChecklists(cfg, post);
1421
+ }
1422
+ case 'edit': {
1423
+ const cl = await pickEntity(post, cfg.orgId, 'checklist', 'checklists');
1424
+ if (!cl)
1425
+ return mChecklists(cfg, post);
1426
+ const updates = {};
1427
+ const n = await txt('New name (blank to skip)');
1428
+ if (n)
1429
+ updates.name = n;
1430
+ const d = await txt('New description (blank to skip)');
1431
+ if (d)
1432
+ updates.description = d;
1433
+ if (Object.keys(updates).length > 0) {
1434
+ await cmd(post, cfg.orgId, 'update_checklist', { checklist_id: cl.id, updates }, 'Updating...');
1435
+ }
1436
+ return mChecklists(cfg, post);
1437
+ }
1438
+ case 'delete': {
1439
+ const cl = await pickEntity(post, cfg.orgId, 'checklist', 'checklists');
1440
+ if (!cl)
1441
+ return mChecklists(cfg, post);
1442
+ const sure = await yn(`Delete "${cl.name}"?`);
1443
+ if (sure)
1444
+ await cmd(post, cfg.orgId, 'delete_checklist', { checklist_id: cl.id }, 'Deleting...');
1445
+ return mChecklists(cfg, post);
1446
+ }
515
1447
  }
516
- catch (err) {
517
- s.stop(pc.red('Failed'));
518
- p.log.error(err.message || 'Could not complete task');
1448
+ return mChecklists(cfg, post);
1449
+ }
1450
+ // ══════════════════════════════════════════════════════════════════════
1451
+ // RULES & AUTOMATION — validation rules (4) + trigger rules (3)
1452
+ // ══════════════════════════════════════════════════════════════════════
1453
+ async function mAutomation(cfg, post) {
1454
+ const a = await sel('Rules & Automation', [
1455
+ { value: 'val-list', label: 'View validation rules' },
1456
+ { value: 'val-create', label: 'Create a validation rule', hint: 'require proof for a task' },
1457
+ { value: 'val-edit', label: 'Edit a validation rule' },
1458
+ { value: 'val-batch', label: 'Reorder validation rules' },
1459
+ { value: 'val-delete', label: 'Delete a validation rule' },
1460
+ { value: 'trig-list', label: 'View trigger rules' },
1461
+ { value: 'trig-create', label: 'Create a trigger rule', hint: 'automate task activation' },
1462
+ { value: 'trig-edit', label: 'Edit a trigger rule' },
1463
+ { value: 'trig-delete', label: 'Delete a trigger rule' },
1464
+ { value: '__back', label: pc.dim('\u2190 Back to main menu') },
1465
+ ]);
1466
+ if (!a || a === '__back')
1467
+ return interactiveMenu(cfg);
1468
+ switch (a) {
1469
+ case 'val-list': {
1470
+ const s = p.spinner();
1471
+ s.start('Loading...');
1472
+ const items = await listEntities(post, cfg.orgId, 'validation_rule');
1473
+ s.stop(`${items.length} validation rules`);
1474
+ if (items.length > 0) {
1475
+ const lines = items.map((i) => ` ${pc.cyan(truncate(i.name || 'Untitled', 50))} ${pc.dim(i.metadata?.rule_type || '')}`);
1476
+ p.note(lines.join('\n'), 'Validation Rules');
1477
+ }
1478
+ return mAutomation(cfg, post);
1479
+ }
1480
+ case 'val-create': {
1481
+ const task = await pickEntity(post, cfg.orgId, 'task', 'tasks');
1482
+ if (!task)
1483
+ return mAutomation(cfg, post);
1484
+ const name = await txt('Rule name', true, 'e.g. Require photo proof');
1485
+ if (!name)
1486
+ return mAutomation(cfg, post);
1487
+ const ruleType = await sel('Rule type', [
1488
+ { value: 'proof_required', label: 'Proof required', hint: 'photo, document, location, etc.' },
1489
+ { value: 'field_constraint', label: 'Field constraint', hint: 'validate a data field' },
1490
+ { value: 'approval_required', label: 'Approval required', hint: 'needs manager sign-off' },
1491
+ { value: 'time_window', label: 'Time window', hint: 'must be done within a time range' },
1492
+ { value: 'record_exists', label: 'Record exists', hint: 'verify data was entered' },
1493
+ { value: 'cross_field', label: 'Cross-field validation' },
1494
+ { value: 'aggregate', label: 'Aggregate check' },
1495
+ ]);
1496
+ if (!ruleType)
1497
+ return mAutomation(cfg, post);
1498
+ const condition = {};
1499
+ if (ruleType === 'proof_required') {
1500
+ const proofType = await sel('What kind of proof?', [
1501
+ { value: 'photo', label: 'Photo' }, { value: 'document', label: 'Document' },
1502
+ { value: 'geolocation', label: 'Location' }, { value: 'checklist', label: 'Checklist' },
1503
+ ]);
1504
+ if (proofType)
1505
+ condition.proof_type = proofType;
1506
+ }
1507
+ else if (ruleType === 'time_window') {
1508
+ const start = await txt('Start time (HH:MM)', false, '08:00');
1509
+ const end = await txt('End time (HH:MM)', false, '17:00');
1510
+ if (start)
1511
+ condition.start_time = start;
1512
+ if (end)
1513
+ condition.end_time = end;
1514
+ }
1515
+ await cmd(post, cfg.orgId, 'create_validation_rule', {
1516
+ task_id: task.id, name, rule_type: ruleType, condition,
1517
+ }, 'Creating validation rule...');
1518
+ return mAutomation(cfg, post);
1519
+ }
1520
+ case 'val-edit': {
1521
+ const rule = await pickEntity(post, cfg.orgId, 'validation_rule', 'validation rules');
1522
+ if (!rule)
1523
+ return mAutomation(cfg, post);
1524
+ const updates = {};
1525
+ const n = await txt('New name (blank to skip)');
1526
+ if (n)
1527
+ updates.name = n;
1528
+ const enabled = await sel('Status', [
1529
+ { value: '__skip', label: pc.dim('Keep current') },
1530
+ { value: 'true', label: 'Enabled' }, { value: 'false', label: 'Disabled' },
1531
+ ]);
1532
+ if (enabled && enabled !== '__skip')
1533
+ updates.enabled = enabled === 'true';
1534
+ if (Object.keys(updates).length > 0) {
1535
+ await cmd(post, cfg.orgId, 'update_validation_rule', { id: rule.id, updates }, 'Updating...');
1536
+ }
1537
+ return mAutomation(cfg, post);
1538
+ }
1539
+ case 'val-batch': {
1540
+ const task = await pickEntity(post, cfg.orgId, 'task', 'tasks');
1541
+ if (!task)
1542
+ return mAutomation(cfg, post);
1543
+ const s = p.spinner();
1544
+ s.start('Loading rules...');
1545
+ const rules = await listEntities(post, cfg.orgId, 'validation_rule', { task_id: task.id });
1546
+ s.stop(`${rules.length} rules`);
1547
+ if (rules.length === 0) {
1548
+ p.log.info('No validation rules for this task.');
1549
+ return mAutomation(cfg, post);
1550
+ }
1551
+ const batch = [];
1552
+ for (let i = 0; i < rules.length; i++) {
1553
+ const r = rules[i];
1554
+ p.log.info(`${i + 1}. ${r.name || 'Untitled'}`);
1555
+ const pos = await txt(`New position for "${r.name}" (blank to keep ${i + 1})`);
1556
+ if (pos)
1557
+ batch.push({ id: r.id, updates: { position: parseInt(pos, 10) } });
1558
+ }
1559
+ if (batch.length > 0) {
1560
+ await cmd(post, cfg.orgId, 'batch_update_validation_rules', { batch }, 'Updating order...');
1561
+ }
1562
+ return mAutomation(cfg, post);
1563
+ }
1564
+ case 'val-delete': {
1565
+ const rule = await pickEntity(post, cfg.orgId, 'validation_rule', 'validation rules');
1566
+ if (!rule)
1567
+ return mAutomation(cfg, post);
1568
+ const sure = await yn(`Delete "${rule.name}"?`);
1569
+ if (sure)
1570
+ await cmd(post, cfg.orgId, 'delete_validation_rule', { id: rule.id }, 'Deleting...');
1571
+ return mAutomation(cfg, post);
1572
+ }
1573
+ case 'trig-list': {
1574
+ const s = p.spinner();
1575
+ s.start('Loading...');
1576
+ const items = await listEntities(post, cfg.orgId, 'trigger_rule');
1577
+ s.stop(`${items.length} trigger rules`);
1578
+ if (items.length > 0) {
1579
+ const lines = items.map((i) => ` ${pc.cyan(truncate(i.name || 'Untitled', 50))} ${pc.dim(i.metadata?.event_type || '')}`);
1580
+ p.note(lines.join('\n'), 'Trigger Rules');
1581
+ }
1582
+ return mAutomation(cfg, post);
1583
+ }
1584
+ case 'trig-create': {
1585
+ const task = await pickEntity(post, cfg.orgId, 'task', 'tasks');
1586
+ if (!task)
1587
+ return mAutomation(cfg, post);
1588
+ const name = await txt('Trigger name', true, 'e.g. Activate when record added');
1589
+ if (!name)
1590
+ return mAutomation(cfg, post);
1591
+ const eventType = await sel('What triggers this?', [
1592
+ { value: 'record_created', label: 'New record added to a table' },
1593
+ { value: 'record_updated', label: 'Record updated in a table' },
1594
+ { value: 'task_completed', label: 'Another task completed' },
1595
+ { value: 'task_missed', label: 'A task was missed' },
1596
+ { value: 'approval_resolved', label: 'An approval was resolved' },
1597
+ ]);
1598
+ if (!eventType)
1599
+ return mAutomation(cfg, post);
1600
+ const sourceType = await sel('Source type', [
1601
+ { value: 'table', label: 'Table' }, { value: 'task', label: 'Task' },
1602
+ { value: 'approval', label: 'Approval' },
1603
+ ]);
1604
+ if (!sourceType)
1605
+ return mAutomation(cfg, post);
1606
+ const payload = { task_id: task.id, name, event_type: eventType, source_entity_type: sourceType };
1607
+ const source = await pickEntity(post, cfg.orgId, sourceType, `${sourceType}s`);
1608
+ if (source)
1609
+ payload.source_entity_id = source.id;
1610
+ await cmd(post, cfg.orgId, 'create_trigger_rule', payload, 'Creating trigger rule...');
1611
+ return mAutomation(cfg, post);
1612
+ }
1613
+ case 'trig-edit': {
1614
+ const rule = await pickEntity(post, cfg.orgId, 'trigger_rule', 'trigger rules');
1615
+ if (!rule)
1616
+ return mAutomation(cfg, post);
1617
+ const updates = {};
1618
+ const n = await txt('New name (blank to skip)');
1619
+ if (n)
1620
+ updates.name = n;
1621
+ const enabled = await sel('Status', [
1622
+ { value: '__skip', label: pc.dim('Keep current') },
1623
+ { value: 'true', label: 'Enabled' }, { value: 'false', label: 'Disabled' },
1624
+ ]);
1625
+ if (enabled && enabled !== '__skip')
1626
+ updates.enabled = enabled === 'true';
1627
+ if (Object.keys(updates).length > 0) {
1628
+ await cmd(post, cfg.orgId, 'update_trigger_rule', { id: rule.id, updates }, 'Updating...');
1629
+ }
1630
+ return mAutomation(cfg, post);
1631
+ }
1632
+ case 'trig-delete': {
1633
+ const rule = await pickEntity(post, cfg.orgId, 'trigger_rule', 'trigger rules');
1634
+ if (!rule)
1635
+ return mAutomation(cfg, post);
1636
+ const sure = await yn(`Delete "${rule.name}"?`);
1637
+ if (sure)
1638
+ await cmd(post, cfg.orgId, 'delete_trigger_rule', { id: rule.id }, 'Deleting...');
1639
+ return mAutomation(cfg, post);
1640
+ }
519
1641
  }
1642
+ return mAutomation(cfg, post);
520
1643
  }
521
- // ── Create Task ─────────────────────────────────────────────────────
522
- async function menuCreateTask(cfg, post) {
523
- const s = p.spinner();
524
- s.start('Loading plans...');
525
- let plans = [];
526
- try {
527
- plans = await listEntities(post, cfg.orgId, 'plan');
528
- s.stop('');
1644
+ // ══════════════════════════════════════════════════════════════════════
1645
+ // DASHBOARDS & SHARING — dashboards (3) + shares (5) = 8 commands
1646
+ // ══════════════════════════════════════════════════════════════════════
1647
+ async function mDashboards(cfg, post) {
1648
+ const a = await sel('Dashboards & Sharing', [
1649
+ { value: 'list', label: 'View dashboards' },
1650
+ { value: 'create', label: 'Create a dashboard' },
1651
+ { value: 'edit', label: 'Edit a dashboard' },
1652
+ { value: 'delete', label: 'Delete a dashboard' },
1653
+ { value: 'share', label: 'Share a dashboard', hint: 'public link, password, invite' },
1654
+ { value: 'unshare', label: 'Stop sharing a dashboard' },
1655
+ { value: 'invite', label: 'Invite people to a shared dashboard' },
1656
+ { value: 'uninvite', label: 'Remove an invite' },
1657
+ { value: 'regen', label: 'Regenerate share link' },
1658
+ { value: '__back', label: pc.dim('\u2190 Back to main menu') },
1659
+ ]);
1660
+ if (!a || a === '__back')
1661
+ return interactiveMenu(cfg);
1662
+ switch (a) {
1663
+ case 'list': {
1664
+ const s = p.spinner();
1665
+ s.start('Loading dashboards...');
1666
+ const items = await listEntities(post, cfg.orgId, 'dashboard');
1667
+ s.stop(`${items.length} dashboards`);
1668
+ if (items.length > 0) {
1669
+ const lines = items.map((d) => ` ${pc.cyan(truncate(d.name || 'Untitled', 50))} ${pc.dim(d.id)}`);
1670
+ p.note(lines.join('\n'), 'Dashboards');
1671
+ }
1672
+ return mDashboards(cfg, post);
1673
+ }
1674
+ case 'create': {
1675
+ const title = await txt('Dashboard title', true);
1676
+ if (!title)
1677
+ return mDashboards(cfg, post);
1678
+ const desc = await txt('Description');
1679
+ const payload = { title };
1680
+ if (desc)
1681
+ payload.description = desc;
1682
+ await cmd(post, cfg.orgId, 'create_dashboard', payload, 'Creating dashboard...');
1683
+ return mDashboards(cfg, post);
1684
+ }
1685
+ case 'edit': {
1686
+ const dash = await pickEntity(post, cfg.orgId, 'dashboard', 'dashboards');
1687
+ if (!dash)
1688
+ return mDashboards(cfg, post);
1689
+ const updates = {};
1690
+ const t = await txt('New title (blank to skip)');
1691
+ if (t)
1692
+ updates.title = t;
1693
+ const d = await txt('New description (blank to skip)');
1694
+ if (d)
1695
+ updates.description = d;
1696
+ if (Object.keys(updates).length > 0) {
1697
+ await cmd(post, cfg.orgId, 'update_dashboard', { dashboard_id: dash.id, updates }, 'Updating...');
1698
+ }
1699
+ return mDashboards(cfg, post);
1700
+ }
1701
+ case 'delete': {
1702
+ const dash = await pickEntity(post, cfg.orgId, 'dashboard', 'dashboards');
1703
+ if (!dash)
1704
+ return mDashboards(cfg, post);
1705
+ const sure = await yn(`Delete "${dash.name}"?`);
1706
+ if (sure)
1707
+ await cmd(post, cfg.orgId, 'delete_dashboard', { dashboard_id: dash.id }, 'Deleting...');
1708
+ return mDashboards(cfg, post);
1709
+ }
1710
+ case 'share': {
1711
+ const dash = await pickEntity(post, cfg.orgId, 'dashboard', 'dashboards');
1712
+ if (!dash)
1713
+ return mDashboards(cfg, post);
1714
+ const shareType = await sel('How should this be shared?', [
1715
+ { value: 'public', label: 'Public link', hint: 'anyone with the link can view' },
1716
+ { value: 'password', label: 'Password protected' },
1717
+ { value: 'login', label: 'Login required', hint: 'must be signed in' },
1718
+ { value: 'invite', label: 'Invite only', hint: 'only invited emails' },
1719
+ ]);
1720
+ if (!shareType)
1721
+ return mDashboards(cfg, post);
1722
+ const payload = { dashboard_id: dash.id, share_type: shareType };
1723
+ if (shareType === 'password') {
1724
+ const pw = await txt('Set a password', true);
1725
+ if (pw)
1726
+ payload.password = pw;
1727
+ }
1728
+ const branding = await sel('Branding', [
1729
+ { value: 'branded', label: 'Show your logo and colors' },
1730
+ { value: 'minimal', label: 'Minimal / clean' },
1731
+ ]);
1732
+ if (branding)
1733
+ payload.branding_mode = branding;
1734
+ await cmd(post, cfg.orgId, 'upsert_dashboard_share', payload, 'Setting up sharing...');
1735
+ return mDashboards(cfg, post);
1736
+ }
1737
+ case 'unshare': {
1738
+ const dash = await pickEntity(post, cfg.orgId, 'dashboard', 'dashboards');
1739
+ if (!dash)
1740
+ return mDashboards(cfg, post);
1741
+ const sure = await yn(`Stop sharing "${dash.name}"? Anyone with the link will lose access.`);
1742
+ if (sure)
1743
+ await cmd(post, cfg.orgId, 'delete_dashboard_share', { dashboard_id: dash.id }, 'Revoking share...');
1744
+ return mDashboards(cfg, post);
1745
+ }
1746
+ case 'invite': {
1747
+ const dash = await pickEntity(post, cfg.orgId, 'dashboard', 'dashboards');
1748
+ if (!dash)
1749
+ return mDashboards(cfg, post);
1750
+ const emailsRaw = await txt('Email addresses (comma-separated)', true);
1751
+ if (!emailsRaw)
1752
+ return mDashboards(cfg, post);
1753
+ const emails = emailsRaw.split(',').map(e => e.trim()).filter(Boolean);
1754
+ await cmd(post, cfg.orgId, 'invite_dashboard_share', { dashboard_id: dash.id, emails }, 'Sending invitations...');
1755
+ return mDashboards(cfg, post);
1756
+ }
1757
+ case 'uninvite': {
1758
+ const dash = await pickEntity(post, cfg.orgId, 'dashboard', 'dashboards');
1759
+ if (!dash)
1760
+ return mDashboards(cfg, post);
1761
+ const inviteId = await txt('Invite ID to remove', true);
1762
+ if (!inviteId)
1763
+ return mDashboards(cfg, post);
1764
+ await cmd(post, cfg.orgId, 'delete_dashboard_share_invite', { dashboard_id: dash.id, invite_id: inviteId }, 'Removing invite...');
1765
+ return mDashboards(cfg, post);
1766
+ }
1767
+ case 'regen': {
1768
+ const dash = await pickEntity(post, cfg.orgId, 'dashboard', 'dashboards');
1769
+ if (!dash)
1770
+ return mDashboards(cfg, post);
1771
+ const sure = await yn('Regenerate the share link? The old link will stop working.');
1772
+ if (sure)
1773
+ await cmd(post, cfg.orgId, 'regenerate_dashboard_share', { dashboard_id: dash.id }, 'Regenerating link...');
1774
+ return mDashboards(cfg, post);
1775
+ }
529
1776
  }
530
- catch {
531
- s.stop('');
1777
+ return mDashboards(cfg, post);
1778
+ }
1779
+ // ══════════════════════════════════════════════════════════════════════
1780
+ // FORMS — 3 commands
1781
+ // ══════════════════════════════════════════════════════════════════════
1782
+ async function mForms(cfg, post) {
1783
+ const a = await sel('Public Forms', [
1784
+ { value: 'list', label: 'View forms' },
1785
+ { value: 'create', label: 'Create a form' },
1786
+ { value: 'edit', label: 'Edit a form' },
1787
+ { value: 'delete', label: 'Delete a form' },
1788
+ { value: '__back', label: pc.dim('\u2190 Back to main menu') },
1789
+ ]);
1790
+ if (!a || a === '__back')
1791
+ return interactiveMenu(cfg);
1792
+ switch (a) {
1793
+ case 'list': {
1794
+ const s = p.spinner();
1795
+ s.start('Loading forms...');
1796
+ const items = await listEntities(post, cfg.orgId, 'public_form');
1797
+ s.stop(`${items.length} forms`);
1798
+ if (items.length > 0) {
1799
+ const lines = items.map((f) => ` ${pc.cyan(truncate(f.name || 'Untitled', 50))} ${pc.dim(f.id)}`);
1800
+ p.note(lines.join('\n'), 'Forms');
1801
+ }
1802
+ return mForms(cfg, post);
1803
+ }
1804
+ case 'create': {
1805
+ const table = await pickEntity(post, cfg.orgId, 'table', 'tables');
1806
+ if (!table)
1807
+ return mForms(cfg, post);
1808
+ const title = await txt('Form title', true);
1809
+ if (!title)
1810
+ return mForms(cfg, post);
1811
+ const payload = { table_id: table.id, title };
1812
+ const desc = await txt('Description');
1813
+ if (desc)
1814
+ payload.description = desc;
1815
+ const access = await sel('Who can see submitted data?', [
1816
+ { value: 'own', label: 'Only their own submissions' },
1817
+ { value: 'all', label: 'All submissions' },
1818
+ { value: 'none', label: 'No one (admin only)' },
1819
+ ]);
1820
+ if (access)
1821
+ payload.submission_access = access;
1822
+ const share = await sel('Sharing', [
1823
+ { value: 'public', label: 'Public link' },
1824
+ { value: 'password', label: 'Password protected' },
1825
+ { value: 'invite', label: 'Invite only' },
1826
+ ]);
1827
+ if (share)
1828
+ payload.share_type = share;
1829
+ if (share === 'password') {
1830
+ const pw = await txt('Password', true);
1831
+ if (pw)
1832
+ payload.password = pw;
1833
+ }
1834
+ await cmd(post, cfg.orgId, 'create_public_form', payload, 'Creating form...');
1835
+ return mForms(cfg, post);
1836
+ }
1837
+ case 'edit': {
1838
+ const form = await pickEntity(post, cfg.orgId, 'public_form', 'forms');
1839
+ if (!form)
1840
+ return mForms(cfg, post);
1841
+ const updates = {};
1842
+ const t = await txt('New title (blank to skip)');
1843
+ if (t)
1844
+ updates.title = t;
1845
+ const d = await txt('New description (blank to skip)');
1846
+ if (d)
1847
+ updates.description = d;
1848
+ const active = await sel('Active?', [
1849
+ { value: '__skip', label: pc.dim('Keep current') },
1850
+ { value: 'true', label: 'Active (accepting submissions)' },
1851
+ { value: 'false', label: 'Inactive (closed)' },
1852
+ ]);
1853
+ if (active && active !== '__skip')
1854
+ updates.is_active = active === 'true';
1855
+ if (Object.keys(updates).length > 0) {
1856
+ await cmd(post, cfg.orgId, 'update_public_form', { form_id: form.id, updates }, 'Updating form...');
1857
+ }
1858
+ return mForms(cfg, post);
1859
+ }
1860
+ case 'delete': {
1861
+ const form = await pickEntity(post, cfg.orgId, 'public_form', 'forms');
1862
+ if (!form)
1863
+ return mForms(cfg, post);
1864
+ const sure = await yn(`Delete "${form.name}"?`);
1865
+ if (sure)
1866
+ await cmd(post, cfg.orgId, 'delete_public_form', { form_id: form.id }, 'Deleting form...');
1867
+ return mForms(cfg, post);
1868
+ }
532
1869
  }
533
- const result = await p.group({
534
- name: () => p.text({
535
- message: 'Task name',
536
- placeholder: 'e.g. Daily safety walkthrough',
537
- validate: (v) => {
538
- if (!v)
539
- return 'Task name is required';
540
- },
541
- }),
542
- planId: () => {
543
- if (plans.length === 0)
544
- return Promise.resolve(undefined);
545
- return p.select({
546
- message: 'Add to which plan?',
547
- options: [
548
- { value: undefined, label: pc.dim('No plan (standalone task)') },
549
- ...plans.map((plan) => ({
550
- value: plan.id,
551
- label: plan.name || 'Untitled',
552
- })),
553
- ],
554
- });
555
- },
556
- frequency: () => p.select({
557
- message: 'How often?',
558
- options: [
559
- { value: 'daily', label: 'Daily' },
560
- { value: 'weekly', label: 'Weekly' },
561
- { value: 'monthly', label: 'Monthly' },
562
- { value: 'one-time', label: 'One-time' },
563
- ],
564
- }),
565
- }, {
566
- onCancel: () => {
567
- p.cancel('Cancelled.');
568
- return;
569
- },
570
- });
571
- if (!result.name)
1870
+ return mForms(cfg, post);
1871
+ }
1872
+ // ══════════════════════════════════════════════════════════════════════
1873
+ // APPROVALS resolve_approval + list_approvals query
1874
+ // ══════════════════════════════════════════════════════════════════════
1875
+ async function mApprovals(cfg, post) {
1876
+ const a = await sel('Approvals', [
1877
+ { value: 'list', label: 'View pending approvals' },
1878
+ { value: 'all', label: 'View all approvals', hint: 'including resolved' },
1879
+ { value: 'resolve', label: 'Approve or reject' },
1880
+ { value: '__back', label: pc.dim('\u2190 Back to main menu') },
1881
+ ]);
1882
+ if (!a || a === '__back')
572
1883
  return interactiveMenu(cfg);
573
- const spinner = p.spinner();
574
- spinner.start('Creating task...');
575
- try {
576
- const payload = {
577
- name: result.name,
578
- organization_id: cfg.orgId,
579
- frequency: result.frequency || 'daily',
580
- };
581
- if (result.planId)
582
- payload.plan_id = result.planId;
583
- await post('/api/v2/commands', {
584
- type: 'create_task',
585
- organization_id: cfg.orgId,
586
- payload,
587
- });
588
- spinner.stop(pc.green(`Task "${result.name}" created!`));
1884
+ switch (a) {
1885
+ case 'list': {
1886
+ const s = p.spinner();
1887
+ s.start('Loading approvals...');
1888
+ const res = await qry(post, cfg.orgId, 'list_approvals', { status: 'pending' });
1889
+ const items = Array.isArray(res?.data) ? res.data : [];
1890
+ s.stop(`${items.length} pending approvals`);
1891
+ if (items.length > 0) {
1892
+ const lines = items.map((ap) => {
1893
+ const target = ap.target_name || ap.target_id || '';
1894
+ return ` ${pc.yellow('\u25CB')} ${pc.cyan(truncate(target, 40))} ${pc.dim(ap.id)}`;
1895
+ });
1896
+ p.note(lines.join('\n'), 'Pending Approvals');
1897
+ }
1898
+ return mApprovals(cfg, post);
1899
+ }
1900
+ case 'all': {
1901
+ const s = p.spinner();
1902
+ s.start('Loading approvals...');
1903
+ const res = await qry(post, cfg.orgId, 'list_approvals', { status: 'all' });
1904
+ const items = Array.isArray(res?.data) ? res.data : [];
1905
+ s.stop(`${items.length} approvals`);
1906
+ if (items.length > 0) {
1907
+ const lines = items.map((ap) => {
1908
+ const icon = ap.status === 'approved' ? pc.green('\u2713') : ap.status === 'rejected' ? pc.red('\u2717') : pc.yellow('\u25CB');
1909
+ const target = ap.target_name || ap.target_id || '';
1910
+ return ` ${icon} ${pc.cyan(truncate(target, 40))} ${pc.dim(ap.status || '')}`;
1911
+ });
1912
+ p.note(lines.join('\n'), 'All Approvals');
1913
+ }
1914
+ return mApprovals(cfg, post);
1915
+ }
1916
+ case 'resolve': {
1917
+ const approval = await pickEntity(post, cfg.orgId, 'approval', 'approvals');
1918
+ if (!approval)
1919
+ return mApprovals(cfg, post);
1920
+ const resolution = await sel('Decision', [
1921
+ { value: 'approved', label: 'Approve' },
1922
+ { value: 'rejected', label: 'Reject' },
1923
+ ]);
1924
+ if (!resolution)
1925
+ return mApprovals(cfg, post);
1926
+ const note = await txt('Add a note (optional)');
1927
+ const payload = { approval_id: approval.id, resolution };
1928
+ if (note)
1929
+ payload.resolution_note = note;
1930
+ await cmd(post, cfg.orgId, 'resolve_approval', payload, resolution === 'approved' ? 'Approving...' : 'Rejecting...');
1931
+ return mApprovals(cfg, post);
1932
+ }
589
1933
  }
590
- catch (err) {
591
- spinner.stop(pc.red('Failed'));
592
- p.log.error(err.message || 'Could not create task');
1934
+ return mApprovals(cfg, post);
1935
+ }
1936
+ // ══════════════════════════════════════════════════════════════════════
1937
+ // TEAM — 4 commands
1938
+ // ══════════════════════════════════════════════════════════════════════
1939
+ async function mTeam(cfg, post) {
1940
+ const a = await sel('Team', [
1941
+ { value: 'invite', label: 'Invite a member' },
1942
+ { value: 'revoke', label: 'Revoke an invitation' },
1943
+ { value: 'role', label: 'Change a member\u2019s role' },
1944
+ { value: 'remove', label: 'Remove a member' },
1945
+ { value: '__back', label: pc.dim('\u2190 Back to main menu') },
1946
+ ]);
1947
+ if (!a || a === '__back')
1948
+ return interactiveMenu(cfg);
1949
+ switch (a) {
1950
+ case 'invite': {
1951
+ const email = await txt('Email address', true);
1952
+ if (!email)
1953
+ return mTeam(cfg, post);
1954
+ const role = await sel('Role', [
1955
+ { value: 'user', label: 'Member', hint: 'can complete tasks and view data' },
1956
+ { value: 'admin', label: 'Admin', hint: 'can manage plans, tables, and team' },
1957
+ { value: 'super_admin', label: 'Owner', hint: 'full control' },
1958
+ ]);
1959
+ if (!role)
1960
+ return mTeam(cfg, post);
1961
+ await cmd(post, cfg.orgId, 'invite_member', { email, role }, 'Sending invitation...');
1962
+ return mTeam(cfg, post);
1963
+ }
1964
+ case 'revoke': {
1965
+ const invId = await txt('Invitation ID', true);
1966
+ if (!invId)
1967
+ return mTeam(cfg, post);
1968
+ await cmd(post, cfg.orgId, 'revoke_invitation', { invitation_id: invId }, 'Revoking invitation...');
1969
+ return mTeam(cfg, post);
1970
+ }
1971
+ case 'role': {
1972
+ const userId = await txt('Member user ID', true);
1973
+ if (!userId)
1974
+ return mTeam(cfg, post);
1975
+ const role = await sel('New role', [
1976
+ { value: 'user', label: 'Member' },
1977
+ { value: 'admin', label: 'Admin' },
1978
+ { value: 'super_admin', label: 'Owner' },
1979
+ ]);
1980
+ if (!role)
1981
+ return mTeam(cfg, post);
1982
+ await cmd(post, cfg.orgId, 'update_member_role', { user_id: userId, role }, 'Updating role...');
1983
+ return mTeam(cfg, post);
1984
+ }
1985
+ case 'remove': {
1986
+ const userId = await txt('Member user ID', true);
1987
+ if (!userId)
1988
+ return mTeam(cfg, post);
1989
+ const sure = await yn('Remove this member? They will lose access to the workspace.');
1990
+ if (sure)
1991
+ await cmd(post, cfg.orgId, 'remove_member', { user_id: userId }, 'Removing member...');
1992
+ return mTeam(cfg, post);
1993
+ }
593
1994
  }
594
- return interactiveMenu(cfg);
1995
+ return mTeam(cfg, post);
595
1996
  }
596
- // ── Switch Workspace ────────────────────────────────────────────────
597
- async function menuSwitch(cfg, post) {
598
- const s = p.spinner();
599
- s.start('Loading workspaces...');
600
- try {
601
- const res = await post('/api/v2/query', {
602
- type: 'list_my_organizations',
603
- payload: {},
604
- });
605
- const orgs = Array.isArray(res.data) ? res.data : [];
606
- s.stop(`${orgs.length} workspace${orgs.length === 1 ? '' : 's'}`);
607
- const selected = await p.select({
608
- message: 'Switch to workspace',
609
- options: orgs.map((o) => ({
1997
+ // ══════════════════════════════════════════════════════════════════════
1998
+ // ORGANIZATION 6 commands
1999
+ // ══════════════════════════════════════════════════════════════════════
2000
+ async function mOrg(cfg, post) {
2001
+ const a = await sel('Organization', [
2002
+ { value: 'branding', label: 'Update branding', hint: 'name, logo, accent color' },
2003
+ { value: 'create', label: 'Create a new organization' },
2004
+ { value: 'join', label: 'Join an organization' },
2005
+ { value: 'accept', label: 'Accept an invitation' },
2006
+ { value: 'leave', label: 'Leave this organization' },
2007
+ { value: 'delete', label: 'Delete this organization', hint: 'owner only, irreversible' },
2008
+ { value: 'switch', label: 'Switch workspace' },
2009
+ { value: 'permissions', label: 'View my permissions' },
2010
+ { value: '__back', label: pc.dim('\u2190 Back to main menu') },
2011
+ ]);
2012
+ if (!a || a === '__back')
2013
+ return interactiveMenu(cfg);
2014
+ switch (a) {
2015
+ case 'branding': {
2016
+ const updates = { organization_id: cfg.orgId };
2017
+ const name = await txt('Organization name (blank to skip)');
2018
+ if (name)
2019
+ updates.name = name;
2020
+ const color = await txt('Accent color (blank to skip)', false, '#6366f1');
2021
+ if (color)
2022
+ updates.accent_color = color;
2023
+ const logo = await txt('Logo URL (blank to skip)');
2024
+ if (logo)
2025
+ updates.logo_url = logo;
2026
+ if (Object.keys(updates).length > 1) {
2027
+ await cmd(post, cfg.orgId, 'update_organization_branding', updates, 'Updating branding...');
2028
+ }
2029
+ return mOrg(cfg, post);
2030
+ }
2031
+ case 'create': {
2032
+ const name = await txt('Organization name', true);
2033
+ if (!name)
2034
+ return mOrg(cfg, post);
2035
+ const payload = { name };
2036
+ const color = await txt('Accent color', false, '#6366f1');
2037
+ if (color)
2038
+ payload.accent_color = color;
2039
+ await cmdNoOrg(post, 'create_organization', payload, 'Creating organization...');
2040
+ return mOrg(cfg, post);
2041
+ }
2042
+ case 'join': {
2043
+ const orgId = await txt('Organization ID', true);
2044
+ if (!orgId)
2045
+ return mOrg(cfg, post);
2046
+ await cmdNoOrg(post, 'join_organization', { organization_id: orgId }, 'Joining...');
2047
+ return mOrg(cfg, post);
2048
+ }
2049
+ case 'accept': {
2050
+ const token = await txt('Invitation token', true);
2051
+ if (!token)
2052
+ return mOrg(cfg, post);
2053
+ await cmdNoOrg(post, 'accept_invitation', { token }, 'Accepting invitation...');
2054
+ return mOrg(cfg, post);
2055
+ }
2056
+ case 'leave': {
2057
+ const sure = await yn(`Leave "${cfg.orgName}"? You will lose access.`);
2058
+ if (sure)
2059
+ await cmd(post, cfg.orgId, 'leave_organization', {}, 'Leaving organization...');
2060
+ return mOrg(cfg, post);
2061
+ }
2062
+ case 'delete': {
2063
+ const sure1 = await yn(`DELETE "${cfg.orgName}"? This permanently destroys ALL data.`);
2064
+ if (!sure1)
2065
+ return mOrg(cfg, post);
2066
+ const sure2 = await yn('Are you absolutely sure? This CANNOT be undone.');
2067
+ if (sure2)
2068
+ await cmd(post, cfg.orgId, 'delete_organization', {}, 'Deleting organization...');
2069
+ return mOrg(cfg, post);
2070
+ }
2071
+ case 'switch': {
2072
+ const s = p.spinner();
2073
+ s.start('Loading workspaces...');
2074
+ const res = await qry(post, cfg.orgId, 'list_my_organizations');
2075
+ const orgs = Array.isArray(res?.data) ? res.data : [];
2076
+ s.stop(`${orgs.length} workspaces`);
2077
+ const selected = await sel('Switch to', orgs.map((o) => ({
610
2078
  value: o.id,
611
2079
  label: o.name,
612
2080
  hint: o.id === cfg.orgId ? pc.green('current') : o.role,
613
- })),
614
- });
615
- if (p.isCancel(selected))
616
- return interactiveMenu(cfg);
617
- const org = orgs.find((o) => o.id === selected);
618
- cfg.orgId = selected;
619
- cfg.orgName = org?.name || '';
620
- saveConfig(cfg);
621
- p.log.success(`Switched to ${pc.cyan(cfg.orgName)}`);
2081
+ })));
2082
+ if (selected) {
2083
+ const org = orgs.find((o) => o.id === selected);
2084
+ cfg.orgId = selected;
2085
+ cfg.orgName = org?.name || '';
2086
+ saveConfig(cfg);
2087
+ p.log.success(`Switched to ${pc.cyan(cfg.orgName)}`);
2088
+ }
2089
+ return mOrg(cfg, post);
2090
+ }
2091
+ case 'permissions': {
2092
+ const s = p.spinner();
2093
+ s.start('Checking permissions...');
2094
+ try {
2095
+ const res = await post('/api/v2/introspection/permissions', { organization_id: cfg.orgId });
2096
+ s.stop('');
2097
+ const data = res.data || res;
2098
+ if (data.role)
2099
+ p.log.info(`Your role: ${pc.cyan(data.role)}`);
2100
+ if (data.capabilities) {
2101
+ const allowed = Object.entries(data.capabilities).filter(([, v]) => v).map(([k]) => k);
2102
+ const denied = Object.entries(data.capabilities).filter(([, v]) => !v).map(([k]) => k);
2103
+ if (allowed.length > 0)
2104
+ p.log.info(`${pc.green('Allowed')}: ${allowed.length} capabilities`);
2105
+ if (denied.length > 0)
2106
+ p.log.info(`${pc.red('Denied')}: ${denied.length} capabilities`);
2107
+ }
2108
+ else {
2109
+ console.log(JSON.stringify(data, null, 2));
2110
+ }
2111
+ }
2112
+ catch (err) {
2113
+ s.stop(pc.red('Failed'));
2114
+ p.log.error(err.message);
2115
+ }
2116
+ return mOrg(cfg, post);
2117
+ }
622
2118
  }
623
- catch (err) {
624
- s.stop(pc.red('Failed'));
625
- p.log.error(err.message || 'Could not load workspaces');
2119
+ return mOrg(cfg, post);
2120
+ }
2121
+ // ══════════════════════════════════════════════════════════════════════
2122
+ // WORKSPACE OVERVIEW — search, status, entity details, schema
2123
+ // ══════════════════════════════════════════════════════════════════════
2124
+ async function mOverview(cfg, post) {
2125
+ const a = await sel('Workspace', [
2126
+ { value: 'status', label: 'Workspace status', hint: 'summary of everything' },
2127
+ { value: 'search', label: 'Search', hint: 'find anything in your workspace' },
2128
+ { value: 'details', label: 'View entity details', hint: 'look up any item by type' },
2129
+ { value: 'schema', label: 'View entity schemas', hint: 'see available fields per type' },
2130
+ { value: '__back', label: pc.dim('\u2190 Back to main menu') },
2131
+ ]);
2132
+ if (!a || a === '__back')
2133
+ return interactiveMenu(cfg);
2134
+ switch (a) {
2135
+ case 'status': {
2136
+ const s = p.spinner();
2137
+ s.start('Loading workspace...');
2138
+ const nodes = await getWorkspaceNodes(post, cfg.orgId);
2139
+ s.stop('Workspace loaded');
2140
+ const counts = {};
2141
+ for (const node of nodes) {
2142
+ const t = node.type || 'unknown';
2143
+ counts[t] = (counts[t] || 0) + 1;
2144
+ }
2145
+ const lines = [
2146
+ `${pad('Workspace', 16)} ${pc.cyan(cfg.orgName)}`,
2147
+ '',
2148
+ `${pad('Plans', 16)} ${pc.bold(String(counts.plan || 0))}`,
2149
+ `${pad('Tasks', 16)} ${pc.bold(String(counts.task || 0))}`,
2150
+ `${pad('Requirements', 16)} ${pc.bold(String(counts.requirement || 0))}`,
2151
+ `${pad('Goals', 16)} ${pc.bold(String(counts.goal || 0))}`,
2152
+ `${pad('Rules', 16)} ${pc.bold(String(counts.rule || 0))}`,
2153
+ `${pad('Documents', 16)} ${pc.bold(String(counts.document || 0))}`,
2154
+ `${pad('Tables', 16)} ${pc.bold(String(counts.table || 0))}`,
2155
+ `${pad('Dashboards', 16)} ${pc.bold(String(counts.dashboard || 0))}`,
2156
+ `${pad('Photos', 16)} ${pc.bold(String(counts.photo || 0))}`,
2157
+ `${pad('Locations', 16)} ${pc.bold(String(counts.geolocation_area || 0))}`,
2158
+ `${pad('Checklists', 16)} ${pc.bold(String(counts.checklist || 0))}`,
2159
+ ];
2160
+ p.note(lines.join('\n'), 'Workspace Overview');
2161
+ return mOverview(cfg, post);
2162
+ }
2163
+ case 'search': {
2164
+ const query = await txt('Search for...', true, 'task name, plan, document...');
2165
+ if (!query)
2166
+ return mOverview(cfg, post);
2167
+ const s = p.spinner();
2168
+ s.start('Searching...');
2169
+ const entities = await searchEntities(post, cfg.orgId, query);
2170
+ s.stop(`${entities.length} results`);
2171
+ if (entities.length > 0) {
2172
+ const lines = entities.map((e) => ` ${pc.dim(pad(e.entity_type || '', 18))} ${pc.cyan(truncate(e.name || 'Untitled', 50))}`);
2173
+ p.note(lines.join('\n'), `Results for "${query}"`);
2174
+ }
2175
+ else {
2176
+ p.log.info('No results found.');
2177
+ }
2178
+ return mOverview(cfg, post);
2179
+ }
2180
+ case 'details': {
2181
+ const entityType = await sel('What type of item?', [
2182
+ { value: 'plan', label: 'Plan' }, { value: 'task', label: 'Task' },
2183
+ { value: 'table', label: 'Table' }, { value: 'document', label: 'Document' },
2184
+ { value: 'dashboard', label: 'Dashboard' }, { value: 'goal', label: 'Goal' },
2185
+ { value: 'rule', label: 'Rule' }, { value: 'requirement', label: 'Requirement' },
2186
+ { value: 'photo', label: 'Photo' }, { value: 'geolocation_area', label: 'Location area' },
2187
+ { value: 'checklist', label: 'Checklist' }, { value: 'public_form', label: 'Form' },
2188
+ ]);
2189
+ if (!entityType)
2190
+ return mOverview(cfg, post);
2191
+ const entity = await pickEntity(post, cfg.orgId, entityType, `${entityType}s`);
2192
+ if (!entity)
2193
+ return mOverview(cfg, post);
2194
+ // Fetch full details
2195
+ const s = p.spinner();
2196
+ s.start('Loading details...');
2197
+ const res = await qry(post, cfg.orgId, 'get_entity', { entity_type: entityType, id: entity.id });
2198
+ s.stop('');
2199
+ if (res?.data) {
2200
+ const data = res.data;
2201
+ const lines = Object.entries(data.metadata || data).map(([k, v]) => {
2202
+ if (v === null || v === undefined)
2203
+ return null;
2204
+ const val = typeof v === 'object' ? JSON.stringify(v) : String(v);
2205
+ return ` ${pc.dim(pad(k, 24))} ${truncate(val, 60)}`;
2206
+ }).filter(Boolean);
2207
+ p.note(lines.join('\n'), entity.name || 'Details');
2208
+ }
2209
+ return mOverview(cfg, post);
2210
+ }
2211
+ case 'schema': {
2212
+ const entityType = await sel('Schema for which type?', [
2213
+ { value: 'plan', label: 'Plan' }, { value: 'task', label: 'Task' },
2214
+ { value: 'table', label: 'Table' }, { value: 'document', label: 'Document' },
2215
+ { value: 'dashboard', label: 'Dashboard' }, { value: 'goal', label: 'Goal' },
2216
+ { value: 'rule', label: 'Rule' }, { value: 'requirement', label: 'Requirement' },
2217
+ ]);
2218
+ if (!entityType)
2219
+ return mOverview(cfg, post);
2220
+ const s = p.spinner();
2221
+ s.start('Loading schema...');
2222
+ try {
2223
+ const res = await post('/api/v2/introspection/schema', {
2224
+ organization_id: cfg.orgId, entity_type: entityType,
2225
+ });
2226
+ s.stop('');
2227
+ const schema = res.data || res;
2228
+ console.log(JSON.stringify(schema, null, 2));
2229
+ }
2230
+ catch (err) {
2231
+ s.stop(pc.red('Failed'));
2232
+ p.log.error(err.message);
2233
+ }
2234
+ return mOverview(cfg, post);
2235
+ }
626
2236
  }
627
- return interactiveMenu(cfg);
2237
+ return mOverview(cfg, post);
2238
+ }
2239
+ // ══════════════════════════════════════════════════════════════════════
2240
+ // DIRECT COMMANDS (non-interactive, for scripts & power users)
2241
+ // ══════════════════════════════════════════════════════════════════════
2242
+ function getFlag(args, flag) {
2243
+ const idx = args.indexOf(flag);
2244
+ if (idx === -1 || idx + 1 >= args.length)
2245
+ return undefined;
2246
+ return args[idx + 1];
628
2247
  }
629
- // ── Direct Commands (non-interactive) ───────────────────────────────
630
2248
  async function directPlans(cfg) {
631
2249
  const { post } = api(cfg);
632
2250
  const s = p.spinner();
@@ -637,11 +2255,8 @@ async function directPlans(cfg) {
637
2255
  p.log.info('No plans found.');
638
2256
  return;
639
2257
  }
640
- const lines = plans.map((plan) => {
641
- const name = truncate(plan.name || 'Untitled', 40);
642
- return ` ${pc.cyan(pad(name, 42))} ${pc.dim(plan.id)}`;
643
- });
644
- p.note(lines.join('\n'), `${plans.length} plan${plans.length === 1 ? '' : 's'}`);
2258
+ const lines = plans.map((pl) => ` ${pc.cyan(pad(truncate(pl.name || 'Untitled', 40), 42))} ${pc.dim(pl.id)}`);
2259
+ p.note(lines.join('\n'), `${plans.length} plans`);
645
2260
  }
646
2261
  async function directTasks(cfg) {
647
2262
  const { post } = api(cfg);
@@ -653,23 +2268,12 @@ async function directTasks(cfg) {
653
2268
  p.log.info('No tasks found.');
654
2269
  return;
655
2270
  }
656
- const lines = tasks.map((t) => {
657
- const name = truncate(t.name || 'Untitled', 50);
658
- const freq = t.metadata?.frequency || '';
659
- return ` ${pc.cyan(pad(name, 52))} ${pc.dim(freq)} ${pc.dim(t.id)}`;
660
- });
661
- p.note(lines.join('\n'), `${tasks.length} task${tasks.length === 1 ? '' : 's'}`);
2271
+ const lines = tasks.map((t) => ` ${pc.cyan(pad(truncate(t.name || 'Untitled', 50), 52))} ${pc.dim(t.metadata?.frequency || '')} ${pc.dim(t.id)}`);
2272
+ p.note(lines.join('\n'), `${tasks.length} tasks`);
662
2273
  }
663
2274
  async function directComplete(cfg, taskId) {
664
2275
  const { post } = api(cfg);
665
- const s = p.spinner();
666
- s.start('Completing task...');
667
- await post('/api/v2/commands', {
668
- type: 'complete_task',
669
- organization_id: cfg.orgId,
670
- payload: { task_id: taskId },
671
- });
672
- s.stop(pc.green('Task completed!'));
2276
+ await cmd(post, cfg.orgId, 'complete_task', { task_id: taskId }, 'Completing task...');
673
2277
  }
674
2278
  async function directStatus(cfg) {
675
2279
  const { post } = api(cfg);
@@ -683,8 +2287,7 @@ async function directStatus(cfg) {
683
2287
  counts[t] = (counts[t] || 0) + 1;
684
2288
  }
685
2289
  const lines = [
686
- `${pad('Workspace', 16)} ${pc.cyan(cfg.orgName)}`,
687
- '',
2290
+ `${pad('Workspace', 16)} ${pc.cyan(cfg.orgName)}`, '',
688
2291
  `${pad('Plans', 16)} ${pc.bold(String(counts.plan || 0))}`,
689
2292
  `${pad('Tasks', 16)} ${pc.bold(String(counts.task || 0))}`,
690
2293
  `${pad('Requirements', 16)} ${pc.bold(String(counts.requirement || 0))}`,
@@ -703,13 +2306,11 @@ async function directSearch(cfg, term) {
703
2306
  s.stop('');
704
2307
  if (entities.length === 0) {
705
2308
  p.log.info('No results found.');
2309
+ return;
706
2310
  }
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
- }
2311
+ const lines = entities.map((e) => ` ${pc.dim(pad(e.entity_type || '', 14))} ${pc.cyan(truncate(e.name || '', 50))} ${pc.dim(e.id)}`);
2312
+ p.note(lines.join('\n'), `${entities.length} results for "${term}"`);
711
2313
  }
712
- // Raw passthrough for power users
713
2314
  async function directRaw(cfg, type, args) {
714
2315
  const { post } = api(cfg);
715
2316
  const payloadRaw = args[0];
@@ -733,12 +2334,6 @@ async function directQuery(cfg, type, args) {
733
2334
  });
734
2335
  console.log(JSON.stringify(res, null, 2));
735
2336
  }
736
- function getFlag(args, flag) {
737
- const idx = args.indexOf(flag);
738
- if (idx === -1 || idx + 1 >= args.length)
739
- return undefined;
740
- return args[idx + 1];
741
- }
742
2337
  // ── Main ────────────────────────────────────────────────────────────
743
2338
  async function main() {
744
2339
  const args = process.argv.slice(2);
@@ -756,19 +2351,19 @@ async function main() {
756
2351
  console.log();
757
2352
  p.intro(pc.bgCyan(pc.black(' Checkbox CLI ')) + pc.dim(` v${VERSION}`));
758
2353
  p.note([
759
- `${pc.cyan('checkbox')} Interactive menu`,
760
- `${pc.cyan('checkbox setup')} Setup wizard (API key & workspace)`,
2354
+ `${pc.cyan('checkbox')} Interactive menu (all features)`,
2355
+ `${pc.cyan('checkbox setup')} Setup wizard`,
761
2356
  `${pc.cyan('checkbox plans')} List your plans`,
762
2357
  `${pc.cyan('checkbox tasks')} List your tasks`,
763
2358
  `${pc.cyan('checkbox complete <id>')} Complete a task`,
764
2359
  `${pc.cyan('checkbox status')} Workspace overview`,
765
2360
  `${pc.cyan('checkbox search <term>')} Search your workspace`,
766
2361
  '',
767
- pc.dim('Advanced (for automation):'),
768
- `${pc.dim('checkbox cmd <type> [json] [--org <id>]')}`,
769
- `${pc.dim('checkbox query <type> [json] [--org <id>]')}`,
2362
+ pc.dim('Automation & scripting:'),
2363
+ `${pc.dim('checkbox cmd <type> [json] [--org <id>]')} Run any command`,
2364
+ `${pc.dim('checkbox query <type> [json] [--org <id>]')} Run any query`,
770
2365
  ].join('\n'), 'Commands');
771
- p.outro(pc.dim('Run `checkbox setup` to get started'));
2366
+ p.outro(pc.dim('Run `checkbox` with no arguments for the full interactive menu'));
772
2367
  return;
773
2368
  }
774
2369
  const cfg = resolveConfig();
@@ -778,14 +2373,12 @@ async function main() {
778
2373
  }
779
2374
  try {
780
2375
  switch (sub) {
781
- case 'plans':
782
- return await directPlans(cfg);
783
- case 'tasks':
784
- return await directTasks(cfg);
2376
+ case 'plans': return await directPlans(cfg);
2377
+ case 'tasks': return await directTasks(cfg);
785
2378
  case 'complete':
786
2379
  if (!rest[0]) {
787
2380
  const { post } = api(cfg);
788
- return await menuComplete(cfg, post);
2381
+ return await mTasks(cfg, post);
789
2382
  }
790
2383
  return await directComplete(cfg, rest[0]);
791
2384
  case 'status':
@@ -794,11 +2387,11 @@ async function main() {
794
2387
  case 'search':
795
2388
  if (!rest[0]) {
796
2389
  const { post } = api(cfg);
797
- return await menuSearch(cfg, post);
2390
+ return await mOverview(cfg, post);
798
2391
  }
799
2392
  return await directSearch(cfg, rest.join(' '));
800
2393
  case 'switch':
801
- return await menuSwitch(cfg, api(cfg).post);
2394
+ return await mOrg(cfg, api(cfg).post);
802
2395
  case 'cmd':
803
2396
  case 'command':
804
2397
  return await directRaw(cfg, rest[0], rest.slice(1));