quoxcli 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/CHANGELOG.md +50 -1
  2. package/lib/api/endpoints/governance.js +7 -0
  3. package/lib/api/endpoints/inboxq.js +39 -0
  4. package/lib/api/endpoints/index.js +2 -0
  5. package/lib/api/endpoints/lucid.js +17 -0
  6. package/lib/api/endpoints/mcp.js +35 -0
  7. package/lib/api/endpoints/notesq.js +2 -0
  8. package/lib/api/endpoints/quoxplan.js +51 -0
  9. package/lib/commands/actions.js +51 -13
  10. package/lib/commands/contacts.js +2 -0
  11. package/lib/commands/lucid.js +6 -0
  12. package/lib/commands/lucidGoals.js +389 -0
  13. package/lib/commands/mail.js +266 -5
  14. package/lib/commands/mcp.js +190 -0
  15. package/lib/commands/mon.js +14 -0
  16. package/lib/commands/monitoring.js +68 -0
  17. package/lib/commands/notesSeal.js +380 -22
  18. package/lib/commands/offload.js +51 -10
  19. package/lib/commands/plan.js +471 -53
  20. package/lib/commands/volt.js +6 -1
  21. package/lib/commands/wazuh.js +65 -9
  22. package/lib/commands/wazuhIntake.js +46 -5
  23. package/lib/core/commandRegistrars.js +1 -0
  24. package/lib/core/config.js +14 -1
  25. package/lib/core/offload.js +83 -0
  26. package/lib/core/output.js +29 -4
  27. package/lib/core/selfUpdate.js +2 -0
  28. package/lib/core/version.js +1 -1
  29. package/lib/quoxon/api.js +4 -0
  30. package/lib/quoxon/app.js +257 -48
  31. package/lib/quoxon/approvalsView.js +3 -1
  32. package/lib/quoxon/ask.js +4 -2
  33. package/lib/quoxon/bang.js +7 -1
  34. package/lib/quoxon/commandShort.js +1 -1
  35. package/lib/quoxon/commands.js +4 -4
  36. package/lib/quoxon/delegationDragon.js +49 -0
  37. package/lib/quoxon/dnd.js +47 -0
  38. package/lib/quoxon/events.js +12 -1
  39. package/lib/quoxon/extensions.js +2 -0
  40. package/lib/quoxon/foreignText.js +49 -0
  41. package/lib/quoxon/inboxView.js +5 -5
  42. package/lib/quoxon/launcher.js +2 -1
  43. package/lib/quoxon/loopCard.js +2 -1
  44. package/lib/quoxon/main.js +8 -2
  45. package/lib/quoxon/onboarding/dockerOffer.js +3 -1
  46. package/lib/quoxon/onboarding/view.js +1 -1
  47. package/lib/quoxon/render.js +5 -2
  48. package/lib/quoxon/resend.js +14 -0
  49. package/lib/quoxon/rows.js +3 -1
  50. package/lib/quoxon/secretShape.js +2 -0
  51. package/lib/quoxon/slash/broadcast.js +30 -6
  52. package/lib/quoxon/slash/broadcastRun.js +12 -6
  53. package/lib/quoxon/slash/cliLeaves.json +35 -0
  54. package/lib/quoxon/slash/dashboard.js +2 -0
  55. package/lib/quoxon/slash/listView.js +2 -1
  56. package/lib/quoxon/slash/palette.js +41 -0
  57. package/lib/quoxon/slash/paletteController.js +6 -2
  58. package/lib/quoxon/slash/restart.js +2 -0
  59. package/lib/quoxon/slotChat.js +7 -3
  60. package/lib/quoxon/slotRun.js +2 -0
  61. package/lib/quoxon/webPty.js +15 -0
  62. package/lib/quoxon/whatsnew.jsonl +6 -0
  63. package/lib/render/planBrief.js +614 -0
  64. package/lib/toolOutputShape.js +6 -3
  65. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -4,7 +4,49 @@ User-visible changes to the `quoxcli` package. Items marked *(unproven)* ship in
4
4
  the package and pass their tests, but nobody has run them end to end from the
5
5
  published package yet.
6
6
 
7
- ## 0.5.0 (unreleased)
7
+ ## 0.6.0 (2026-11-10)
8
+
9
+ Nothing below has been run end to end from the published 0.6.0 package yet: treat every
10
+ item as *(unproven from the package)*. Items marked *(driven live)* were driven against a
11
+ live instance from the source tree before release.
12
+
13
+ ### New commands
14
+ - `quox notes seal convert <id>` and `quox notes seal purge <id>` (human sessions only):
15
+ seal an existing plaintext note in place, then wipe its old plaintext revisions. Convert
16
+ names any attachments and says they are NOT protected. *(driven live)*
17
+ - Sealed notes harden: each sealed note is bound to its note id (another note's envelope
18
+ is refused), the version comes from the authenticated envelope, and a local rollback
19
+ floor fails closed under a lock.
20
+ - `quox notes seal` finds ENQRYPT without setup: `QUOX_ENQRYPT_PATH`, then
21
+ `~/.quox/enqrypt-src` (a clone of quoxai/enqrypt main plus `npm ci`); a copy missing a
22
+ function the CLI calls is refused by name. ENQRYPT is not on npm, and the npm package
23
+ named `enqrypt` is unrelated malware. *(driven live)*
24
+ - `quox mail block | unblock | blocked | junk | trust`.
25
+ - `quox mcp token create | list | revoke`.
26
+ - `quox lucid goals`, `quox lucid insights`, `quox lucid brief`.
27
+ - `quox plan brief`, `quox plan watch`, `quox plan followups` / `followup add | done | status`,
28
+ `quox plan roadmap archive | unarchive`, `quox plan roadmaps --archived`.
29
+ - `quox monitoring incidents claim <id>` and `quox monitoring incidents status <id> <status>`: claim and move a
30
+ governance incident (witnessed, org scoped). `quox wazuh connect --indexer-ca` and
31
+ `--password-stdin` / `--indexer-password-stdin`.
32
+
33
+ ### Changes
34
+ - `quox plan add` honours `roadmap_id` in the JSON (streams used to land in the first
35
+ roadmap). `quox plan focus` counts substreams and caps its output; unblockers name the
36
+ whole chain.
37
+ - `quox wazuh vulns` totals are exact or labelled a lower bound (10,000+); `quox wazuh status`
38
+ says its agent count includes the manager; no MAINT? column when the org has no Uptime Kuma.
39
+ - `quox offload` lists every worker lease and warns 12h before one expires; `offload release
40
+ --worker` targets that worker, never the shared default.
41
+ - `quox tool exec --select .` returns the whole result. `quox actions list` fits the terminal.
42
+ - Conversation app (`quox` with no arguments): `/wake`, `/poke` and `/page` take several
43
+ slots; `/inbox show N` reads a whole item; `/morningreport` digests what happened since you
44
+ were last here; a typed `/org switch` or `/org back` no longer asks a second y/n; an inline
45
+ approval says approved only after the server records it and no longer re-opens as a popup;
46
+ decision cards never echo the asker's pick as their own advice; contact-consent approvals ask
47
+ for the lawful basis; slot messages from other sessions never show raw colour codes.
48
+
49
+ ## 0.5.0 (2026-11-06)
8
50
 
9
51
  ### New commands
10
52
  - `quox wazuh`: first-class Wazuh commands. `agents`, `fim`, `sca`, `rootcheck`
@@ -32,6 +74,13 @@ published package yet.
32
74
  - `quox notes seal init | status | new | open | edit | print`: ENQRYPT sealed
33
75
  notes. Encryption happens on your machine and the server stores only the
34
76
  envelope. *(unproven from the package)*
77
+ - `quox notes seal convert <id> --yes [--keep-title]` seals an existing note and
78
+ purges its earlier plaintext from the server (revisions, embeddings, derived
79
+ beliefs; human sessions only); `quox notes seal purge <id> --yes` reruns the
80
+ purge. Both say that backups taken earlier still hold the old text until they
81
+ expire. `seal edit` now takes the version from the sealed envelope, not from a
82
+ server field, and the local rollback floor refuses a damaged or missing floor
83
+ file instead of starting again from zero. *(unproven from the package)*
35
84
  - `quox admin users verify-email <email> --reason` (step-up PIN).
36
85
  - `quox memory sync`: Claude file memory is saved to Quox memory too
37
86
  (idempotent; credential-shaped text is refused).
@@ -22,6 +22,13 @@ export const governance = {
22
22
  path: () => `/governance/incidents`,
23
23
  tags: ['live-safe'],
24
24
  },
25
+ // WAZ P11 slice 1b: the witnessed lifecycle move (claim = self, status with a resolution for terminal states).
26
+ // Collector: lib/governanceIncidentLifecycle.js. Writes, so not live-safe.
27
+ 'governance.incidents.transition': {
28
+ method: 'POST',
29
+ service: 'collector',
30
+ path: ({ id }) => `/governance/incidents/${encodeURIComponent(id)}/transition`,
31
+ },
25
32
  'governance.policy.compile': {
26
33
  method: 'POST',
27
34
  service: 'collector',
@@ -136,4 +136,43 @@ export const inboxq = {
136
136
  path: ({ proposalId }) => `/api/v1/inboxq/proposals/${proposalId}/resolve`,
137
137
  tags: ['live-write'],
138
138
  },
139
+ // EMAIL-Q CLI audit (2026-11-09): the sender/junk routes the dashboard already
140
+ // calls (src/components/inboxq/lib/api.js), wrapped by `quox mail block|unblock|
141
+ // blocked|junk|trust`. No new server logic: these are the existing InboxQ routes.
142
+ 'inboxq.messages.search': {
143
+ method: 'GET',
144
+ service: 'collector',
145
+ path: () => `/api/v1/inboxq/messages/search`,
146
+ tags: ['live-safe'],
147
+ },
148
+ 'inboxq.messages.confirmJunk': {
149
+ method: 'POST',
150
+ service: 'collector',
151
+ path: () => `/api/v1/inboxq/messages/confirm-junk`,
152
+ tags: ['live-write'],
153
+ },
154
+ 'inboxq.senders.block': {
155
+ method: 'POST',
156
+ service: 'collector',
157
+ path: () => `/api/v1/inboxq/senders/block`,
158
+ tags: ['live-write'],
159
+ },
160
+ 'inboxq.senders.unblock': {
161
+ method: 'POST',
162
+ service: 'collector',
163
+ path: () => `/api/v1/inboxq/senders/unblock`,
164
+ tags: ['live-write'],
165
+ },
166
+ 'inboxq.senders.blocked': {
167
+ method: 'GET',
168
+ service: 'collector',
169
+ path: () => `/api/v1/inboxq/senders/blocked`,
170
+ tags: ['live-safe'],
171
+ },
172
+ 'inboxq.senders.trust': {
173
+ method: 'POST',
174
+ service: 'collector',
175
+ path: () => `/api/v1/inboxq/senders/trust`,
176
+ tags: ['live-write'],
177
+ },
139
178
  };
@@ -96,6 +96,7 @@ import { contactsq } from './contactsq.js';
96
96
  import { x } from './x.js';
97
97
  import { llmPolicy } from './llm-policy.js';
98
98
  import { hat } from './hat.js';
99
+ import { mcp } from './mcp.js';
99
100
  import { members } from './members.js';
100
101
  import { memory } from './memory.js';
101
102
  import { mfa } from './mfa.js';
@@ -189,6 +190,7 @@ export const ENDPOINTS = {
189
190
  ...x,
190
191
  ...llmPolicy,
191
192
  ...hat,
193
+ ...mcp,
192
194
  ...members,
193
195
  ...memory,
194
196
  ...mfa,
@@ -86,4 +86,21 @@ export const lucid = {
86
86
  path: () => `/api/v1/lucid/herald/test`,
87
87
  tags: ['live-safe'],
88
88
  },
89
+ // LUCID-INTELLIGENCE World Model (LI-1): goals, initiatives, decisions, links. People confirm; agents propose.
90
+ 'lucid.world.list': { method: 'GET', service: 'collector', path: () => `/api/v1/lucid/intelligence/world`, tags: ['live-safe'] },
91
+ 'lucid.world.create': { method: 'POST', service: 'collector', path: () => `/api/v1/lucid/intelligence/world`, tags: ['live-write'] },
92
+ 'lucid.world.export': { method: 'GET', service: 'collector', path: () => `/api/v1/lucid/intelligence/world/export`, tags: ['live-safe'] },
93
+ 'lucid.world.erase': { method: 'DELETE', service: 'collector', path: () => `/api/v1/lucid/intelligence/world/mine`, tags: ['live-write'] },
94
+ 'lucid.world.get': { method: 'GET', service: 'collector', path: ({ id }) => `/api/v1/lucid/intelligence/world/${encodeURIComponent(id)}`, tags: ['live-safe'] },
95
+ 'lucid.world.amend': { method: 'PATCH', service: 'collector', path: ({ id }) => `/api/v1/lucid/intelligence/world/${encodeURIComponent(id)}`, tags: ['live-write'] },
96
+ 'lucid.world.confirm': { method: 'POST', service: 'collector', path: ({ id }) => `/api/v1/lucid/intelligence/world/${encodeURIComponent(id)}/confirm`, tags: ['live-write'] },
97
+ 'lucid.world.archive': { method: 'POST', service: 'collector', path: ({ id }) => `/api/v1/lucid/intelligence/world/${encodeURIComponent(id)}/archive`, tags: ['live-write'] },
98
+ 'lucid.world.restore': { method: 'POST', service: 'collector', path: ({ id }) => `/api/v1/lucid/intelligence/world/${encodeURIComponent(id)}/restore`, tags: ['live-write'] },
99
+ 'lucid.world.history': { method: 'GET', service: 'collector', path: ({ id }) => `/api/v1/lucid/intelligence/world/${encodeURIComponent(id)}/history`, tags: ['live-safe'] },
100
+ // LUCID-INTELLIGENCE LI-2 shadow insights (read-only).
101
+ 'lucid.insights.list': { method: 'GET', service: 'collector', path: () => `/api/v1/lucid/intelligence/insights`, tags: ['live-safe'] },
102
+ 'lucid.insights.status': { method: 'GET', service: 'collector', path: () => `/api/v1/lucid/intelligence/insights/status`, tags: ['live-safe'] },
103
+ 'lucid.insights.get': { method: 'GET', service: 'collector', path: ({ id }) => `/api/v1/lucid/intelligence/insights/${encodeURIComponent(id)}`, tags: ['live-safe'] },
104
+ // LUCID-INTELLIGENCE LI-3 brief (read-only, never moves a cursor).
105
+ 'lucid.brief': { method: 'GET', service: 'collector', path: () => `/api/v1/lucid/intelligence/brief`, tags: ['live-safe'] },
89
106
  };
@@ -0,0 +1,35 @@
1
+ /**
2
+ * 'mcp.*' endpoints: per-user QuoxMCP tokens and the personalised Claude Desktop bundle.
3
+ *
4
+ * - mcp.tokens.* auth service, services/auth/routes/mcp-tokens.js (mounted /api/auth/tokens/mcp, requireAuth,
5
+ * scoped to the caller: list/revoke only ever see the caller's own tokens; create needs an org context).
6
+ * - mcp.bundle collector, services/collector/routes/mcp-bundle.js (JWT only). POST { token, collector_url, agent_id }
7
+ * returns a .mcpb zip whose manifest EMBEDS the token as QUOX_SERVICE_KEY. Binary, read with raw: true.
8
+ */
9
+
10
+ export const mcp = {
11
+ 'mcp.tokens.create': {
12
+ method: 'POST',
13
+ service: 'auth',
14
+ path: () => '/api/auth/tokens/mcp',
15
+ tags: ['live-write'],
16
+ },
17
+ 'mcp.tokens.list': {
18
+ method: 'GET',
19
+ service: 'auth',
20
+ path: () => '/api/auth/tokens/mcp',
21
+ tags: ['live-safe'],
22
+ },
23
+ 'mcp.tokens.revoke': {
24
+ method: 'DELETE',
25
+ service: 'auth',
26
+ path: ({ id }) => `/api/auth/tokens/mcp/${encodeURIComponent(id)}`,
27
+ tags: ['live-write'],
28
+ },
29
+ 'mcp.bundle': {
30
+ method: 'POST',
31
+ service: 'collector',
32
+ path: () => '/api/v1/mcp/bundle',
33
+ tags: ['live-write'],
34
+ },
35
+ };
@@ -19,6 +19,8 @@ export const notesq = {
19
19
  'notesq.notes.revisions': { method: 'GET', service: 'collector', path: ({ id }) => `${B}/notes/${id}/revisions`, tags: ['live-safe'] },
20
20
  'notesq.notes.revision': { method: 'GET', service: 'collector', path: ({ id, rev }) => `${B}/notes/${id}/revisions/${rev}`, tags: ['live-safe'] },
21
21
  'notesq.notes.restore': { method: 'POST', service: 'collector', path: ({ id }) => `${B}/notes/${id}/restore`, tags: ['live-write'] },
22
+ // ENQRYPT-NOTES: human-only purge of a sealed note's earlier plaintext (revisions, embeddings, derived beliefs).
23
+ 'notesq.notes.sealPurge': { method: 'POST', service: 'collector', path: ({ id }) => `${B}/notes/${id}/seal-purge`, tags: ['live-write'] },
22
24
  'notesq.import': { method: 'POST', service: 'collector', path: () => `${B}/import`, tags: ['live-write'] },
23
25
  // AQ-9D P4: Action-Q item create (auth, org-scoped), used by `quox notes actions`.
24
26
  'notesq.actionq.create': { method: 'POST', service: 'auth', path: ({ orgId }) => `/api/orgs/${orgId}/actions`, tags: ['live-write'] },
@@ -40,6 +40,14 @@ export const quoxplan = {
40
40
  path: ({ roadmapId }) => `/api/v1/quoxplan/roadmaps/${roadmapId}/streams`,
41
41
  tags: ['live-safe'],
42
42
  },
43
+ // One-screen roadmap brief (QUOXPLAN-TERMINAL): top priorities, needs-you,
44
+ // unblockers, recent movers, stale count. Query: top (1..25), days (1..60).
45
+ 'quoxplan.roadmap.brief.get': {
46
+ method: 'GET',
47
+ service: 'collector',
48
+ path: ({ roadmapId }) => `/api/v1/quoxplan/roadmaps/${roadmapId}/brief`,
49
+ tags: ['live-safe'],
50
+ },
43
51
  'quoxplan.roadmap.timeline.get': {
44
52
  method: 'GET',
45
53
  service: 'collector',
@@ -58,6 +66,21 @@ export const quoxplan = {
58
66
  path: ({ roadmapId }) => `/api/v1/quoxplan/roadmaps/${roadmapId}/streams`,
59
67
  tags: ['live-write'],
60
68
  },
69
+ // Archive / unarchive a roadmap (QUOXPLAN-TERMINAL, gripe quoxai/quox#1586):
70
+ // hides it from GET /roadmaps and /search and refuses writes to its streams;
71
+ // reads by id keep working. Both idempotent server-side.
72
+ 'quoxplan.roadmap.archive': {
73
+ method: 'POST',
74
+ service: 'collector',
75
+ path: ({ roadmapId }) => `/api/v1/quoxplan/roadmaps/${roadmapId}/archive`,
76
+ tags: ['live-write'],
77
+ },
78
+ 'quoxplan.roadmap.unarchive': {
79
+ method: 'POST',
80
+ service: 'collector',
81
+ path: ({ roadmapId }) => `/api/v1/quoxplan/roadmaps/${roadmapId}/unarchive`,
82
+ tags: ['live-write'],
83
+ },
61
84
  'quoxplan.roadmap.export.get': {
62
85
  method: 'GET',
63
86
  service: 'collector',
@@ -118,4 +141,32 @@ export const quoxplan = {
118
141
  path: ({ id }) => `/api/v1/quoxplan/streams/${id}/plan/versions`,
119
142
  tags: ['live-safe'],
120
143
  },
144
+ // Action items (QUOXPLAN-TERMINAL: the dashboard's action-item panel had no
145
+ // CLI counterpart). Server: quoxplan/server/src/api/actionItems.js. Auto
146
+ // items (kind blocked/decision) are raised by stream status transitions;
147
+ // manual items are added here. Statuses: OPEN | DONE | CANCELLED.
148
+ 'quoxplan.roadmap.actionItems.list': {
149
+ method: 'GET',
150
+ service: 'collector',
151
+ path: ({ roadmapId }) => `/api/v1/quoxplan/roadmaps/${roadmapId}/action-items`,
152
+ tags: ['live-safe'],
153
+ },
154
+ 'quoxplan.stream.actionItems.list': {
155
+ method: 'GET',
156
+ service: 'collector',
157
+ path: ({ id }) => `/api/v1/quoxplan/streams/${id}/action-items`,
158
+ tags: ['live-safe'],
159
+ },
160
+ 'quoxplan.stream.actionItems.create': {
161
+ method: 'POST',
162
+ service: 'collector',
163
+ path: ({ id }) => `/api/v1/quoxplan/streams/${id}/action-items`,
164
+ tags: ['live-write'],
165
+ },
166
+ 'quoxplan.actionItem.update': {
167
+ method: 'PATCH',
168
+ service: 'collector',
169
+ path: ({ actionId }) => `/api/v1/quoxplan/action-items/${actionId}`,
170
+ tags: ['live-write'],
171
+ },
121
172
  };
@@ -41,6 +41,39 @@ async function listItems(ctx, { status = 'all', limit = 100, scope } = {}) {
41
41
  throw new QuoxError(`More than ${MAX_PAGES * pageSize} items; narrow with --status or --scope.`);
42
42
  }
43
43
 
44
+ // Short ids, as `quox approvals` shows them: the last 6 characters of the ULID. --json keeps the full id.
45
+ const SHORT_LEN = 6;
46
+ export const shortActionId = (id) => String(id ?? '').slice(-SHORT_LEN);
47
+
48
+ // The width to fit: the terminal when there is one, else COLUMNS (the Quox TUI sets it for ! and /<verb> runs).
49
+ function outputWidth() {
50
+ if (process.stdout.isTTY && process.stdout.columns) return process.stdout.columns;
51
+ const env = Number(process.env.COLUMNS);
52
+ return env > 0 ? env : 0;
53
+ }
54
+
55
+ /** An exact id, or a unique id suffix (the short id the list shows). -> item | null; ambiguous throws. */
56
+ function findByIdOrSuffix(items, given) {
57
+ const want = String(given || '').trim().toUpperCase();
58
+ if (!want) return null;
59
+ const exact = items.find((i) => String(i.id).toUpperCase() === want);
60
+ if (exact) return exact;
61
+ const hits = items.filter((i) => String(i.id).toUpperCase().endsWith(want));
62
+ if (hits.length > 1) {
63
+ throw new QuoxError(`"${given}" matches ${hits.length} action items (${hits.map((h) => h.id).join(', ')}); use more of the id.`, 2);
64
+ }
65
+ return hits[0] || null;
66
+ }
67
+
68
+ // A short id (base32, shorter than a ULID) is resolved against the list; anything else goes to the server as given.
69
+ const looksShort = (id) => /^[0-9A-Z]{4,25}$/i.test(String(id || '').trim());
70
+ async function resolveActionId(ctx, given) {
71
+ if (!looksShort(given)) return given;
72
+ const item = findByIdOrSuffix(await listItems(ctx, { status: 'all', limit: 100 }), given);
73
+ if (!item) throw new QuoxError(`No action item matches ${given}.`, 5);
74
+ return item.id;
75
+ }
76
+
44
77
  export function registerActionsCommands(program, ctx) {
45
78
  const actions = program
46
79
  .command('actions')
@@ -54,20 +87,24 @@ export function registerActionsCommands(program, ctx) {
54
87
  .option('--scope <scopeId>', 'only items in this team/department/division scope')
55
88
  .action(async (options) => {
56
89
  const items = await listItems({ ...ctx, program }, options);
90
+ // Fit the terminal (owner 2026-11-07): short id, title cut with an ellipsis, no all-empty Due column.
91
+ const hasDue = items.some((i) => i.dueDate);
92
+ const columns = ['id', 'status', 'priority', 'title', ...(hasDue ? ['dueDate'] : [])];
93
+ const headers = ['ID', 'Status', 'Priority', 'Title', ...(hasDue ? ['Due'] : [])];
57
94
  formatOutput({
58
- columns: ['id', 'status', 'priority', 'title', 'dueDate'],
59
- headers: ['ID', 'Status', 'Priority', 'Title', 'Due'],
60
- rows: items.map((i) => ({ ...i, dueDate: i.dueDate || '-' })),
61
- }, { raw: items });
95
+ columns,
96
+ headers,
97
+ rows: items.map((i) => ({ ...i, fullId: i.id, id: shortActionId(i.id), dueDate: i.dueDate ? String(i.dueDate).slice(0, 10) : '' })),
98
+ }, { raw: items, quietColumn: 'fullId', fit: { width: outputWidth(), flex: 'title' } });
62
99
  });
63
100
 
64
101
  actions
65
102
  .command('get')
66
103
  .description('Show one action item by id')
67
- .argument('<id>', 'action item id (from `quox actions list`)')
104
+ .argument('<id>', 'action item id, or the short id `quox actions list` shows')
68
105
  .action(async (id) => {
69
106
  const items = await listItems({ ...ctx, program }, { status: 'all', limit: 100 });
70
- const item = items.find((i) => i.id === id);
107
+ const item = findByIdOrSuffix(items, id);
71
108
  if (!item) throw new QuoxError(`No action item ${id} among the latest 100 items.`);
72
109
  formatDetail(item);
73
110
  });
@@ -90,10 +127,10 @@ export function registerActionsCommands(program, ctx) {
90
127
  actions
91
128
  .command('done')
92
129
  .description('Mark an Action Tracker item done')
93
- .argument('<id>', 'action item id (from `quox actions list`)')
130
+ .argument('<id>', 'action item id, or the short id `quox actions list` shows')
94
131
  .action(async (id) => {
95
132
  ctx.auth.requireAuth();
96
- const item = await completeActionItem(ctx.client, id);
133
+ const item = await completeActionItem(ctx.client, await resolveActionId({ ...ctx, program }, id));
97
134
  if (program.opts().output === 'json') return formatDetail(item);
98
135
  printSuccess(`Marked done ${item.id}: ${item.title}`);
99
136
  return undefined;
@@ -102,11 +139,11 @@ export function registerActionsCommands(program, ctx) {
102
139
  actions
103
140
  .command('move')
104
141
  .description('Move an Action Tracker item to todo, doing or done')
105
- .argument('<id>', 'action item id (from `quox actions list`)')
142
+ .argument('<id>', 'action item id, or the short id `quox actions list` shows')
106
143
  .argument('<status>', STATUS_COLUMNS.join(', '))
107
144
  .action(async (id, status) => {
108
145
  ctx.auth.requireAuth();
109
- const item = await moveActionItem(ctx.client, id, status);
146
+ const item = await moveActionItem(ctx.client, await resolveActionId({ ...ctx, program }, id), status);
110
147
  if (program.opts().output === 'json') return formatDetail(item);
111
148
  printSuccess(`Moved ${item.id} to ${item.status}: ${item.title}`);
112
149
  return undefined;
@@ -115,15 +152,16 @@ export function registerActionsCommands(program, ctx) {
115
152
  actions
116
153
  .command('delete')
117
154
  .description('Delete one of your Action Tracker items (asks first; --yes skips, required without a terminal)')
118
- .argument('<id>', 'action item id (from `quox actions list`)')
155
+ .argument('<id>', 'action item id, or the short id `quox actions list` shows')
119
156
  .option('-y, --yes', 'do not ask')
120
157
  .action(async (id, options) => {
121
158
  ctx.auth.requireAuth();
122
- if (!(await confirmDestructive(program, { force: options.yes === true }, `Delete action item ${id}?`))) {
159
+ const fullId = await resolveActionId({ ...ctx, program }, id);
160
+ if (!(await confirmDestructive(program, { force: options.yes === true }, `Delete action item ${fullId}?`))) {
123
161
  console.log('Not deleted.');
124
162
  return undefined;
125
163
  }
126
- const r = await deleteActionItem(ctx.client, id);
164
+ const r = await deleteActionItem(ctx.client, fullId);
127
165
  if (program.opts().output === 'json') return formatDetail(r);
128
166
  printSuccess(`Deleted ${r.id}${r.title ? `: ${r.title}` : ''}`);
129
167
  return undefined;
@@ -408,6 +408,8 @@ export function registerContactsCommands(program, ctx) {
408
408
  if (isJson(program)) return printJson(d);
409
409
  console.log(`channel: ${d.channel || 'none allowed'}`);
410
410
  for (const a of d.alternatives || []) console.log(` ${a.channel}: ${a.decision}${(a.reasons || []).length ? ` (${a.reasons.join(', ')})` : ''}`);
411
+ if (d.evaluated_for) console.log(`for: ${d.evaluated_for === 'agent' ? 'an agent' : 'you (a person)'}`);
412
+ if ((d.quiet_hours_for_agents || []).length) console.log(`quiet hours: an agent reaching them by ${d.quiet_hours_for_agents.join(', ')} now needs approval`);
411
413
  announce('contacts best-channel', d.channel || 'none allowed');
412
414
  });
413
415
 
@@ -9,12 +9,14 @@
9
9
  * quox lucid teach <target> <field>=<value> | lessons | forget <id>
10
10
  * quox lucid herald [set key=value... | test] when LUCID speaks first, and what it has said
11
11
  * quox lucid blind sources live/silent/failing/never heard, and what doesn't add up
12
+ * quox lucid goals ... your goals and decisions, so LUCID ranks against them (see lucidGoals.js)
12
13
  */
13
14
  import { formatOutput, printSuccess } from '../core/output.js';
14
15
  import { line as glyphLine, printEvent } from '../core/quoxglyph.js';
15
16
  import { resolveEndpoint } from '../api/endpoints.js';
16
17
  import { confirmDestructive } from '../core/confirm.js';
17
18
  import { resolveOrgId } from './helpers.js';
19
+ import { registerLucidGoalsCommands, registerLucidInsightsCommands, registerLucidBriefCommand } from './lucidGoals.js';
18
20
 
19
21
  const SURFACE = 'cli';
20
22
 
@@ -505,6 +507,10 @@ export function registerLucidCommands(program, ctx) {
505
507
  if (st.length) console.log(`Stats: ${st.map(([k, v]) => `${k}=${Array.isArray(v) ? v.join(',') : v}`).join(' · ')}`);
506
508
  });
507
509
 
510
+ registerLucidGoalsCommands(lu, program, ctx);
511
+ registerLucidInsightsCommands(lu, program, ctx);
512
+ registerLucidBriefCommand(lu, program, ctx);
513
+
508
514
  // ---- herald: when LUCID reaches out on its own ----
509
515
  const LEVELS = { l1_inbox: 'filed in your Approvals inbox', l2_slot: 'nudged you in your slot', l3_call: 'called you', resolved: 'resolved' };
510
516
  const levelWords = (l) => LEVELS[l] || l || 'not yet reached out';