checkbox-cli 2.0.1 → 3.0.1

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