quoxcli 0.4.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 (199) hide show
  1. package/CHANGELOG.md +110 -0
  2. package/README.md +2 -2
  3. package/bin/quox.js +19 -1
  4. package/bin/quoxon.js +6 -1
  5. package/lib/api/endpoints/admin.js +7 -0
  6. package/lib/api/endpoints/agent-attach.js +7 -0
  7. package/lib/api/endpoints/compliance.js +25 -0
  8. package/lib/api/endpoints/governance.js +7 -0
  9. package/lib/api/endpoints/inbox.js +6 -0
  10. package/lib/api/endpoints/inboxq.js +58 -0
  11. package/lib/api/endpoints/index.js +4 -0
  12. package/lib/api/endpoints/lucid.js +17 -0
  13. package/lib/api/endpoints/mcp.js +35 -0
  14. package/lib/api/endpoints/mon.js +122 -0
  15. package/lib/api/endpoints/notesq.js +2 -0
  16. package/lib/api/endpoints/quoxplan.js +51 -0
  17. package/lib/api/endpoints/tools.js +13 -0
  18. package/lib/api/endpoints/v1.js +13 -0
  19. package/lib/commands/actions.js +51 -13
  20. package/lib/commands/admin.js +35 -0
  21. package/lib/commands/agentAttach.js +15 -0
  22. package/lib/commands/approvals.js +193 -3
  23. package/lib/commands/ask.js +1 -1
  24. package/lib/commands/auth.js +64 -11
  25. package/lib/commands/backup.js +11 -4
  26. package/lib/commands/capabilities.js +3 -2
  27. package/lib/commands/chat.js +15 -0
  28. package/lib/commands/claude.js +4 -1
  29. package/lib/commands/compliance.js +109 -0
  30. package/lib/commands/connect.js +69 -0
  31. package/lib/commands/contacts.js +2 -0
  32. package/lib/commands/dev.js +6 -8
  33. package/lib/commands/doctor.js +22 -3
  34. package/lib/commands/evidence.js +2 -1
  35. package/lib/commands/gripe.js +24 -13
  36. package/lib/commands/inbox.js +37 -3
  37. package/lib/commands/inboxq.js +147 -0
  38. package/lib/commands/kuma.js +78 -5
  39. package/lib/commands/licenceLinkView.js +311 -0
  40. package/lib/commands/linkedin.js +19 -0
  41. package/lib/commands/loop.js +30 -3
  42. package/lib/commands/lucid.js +7 -0
  43. package/lib/commands/lucidGoals.js +389 -0
  44. package/lib/commands/mail.js +427 -25
  45. package/lib/commands/mcp.js +190 -0
  46. package/lib/commands/memory.js +68 -4
  47. package/lib/commands/memorySync.js +158 -0
  48. package/lib/commands/mon.js +69 -144
  49. package/lib/commands/monitoring.js +68 -0
  50. package/lib/commands/notes.js +29 -7
  51. package/lib/commands/notesSeal.js +617 -0
  52. package/lib/commands/objective.js +5 -2
  53. package/lib/commands/offload.js +52 -12
  54. package/lib/commands/plan.js +487 -52
  55. package/lib/commands/provider.js +29 -8
  56. package/lib/commands/qlarity.js +32 -1
  57. package/lib/commands/run.js +2 -1
  58. package/lib/commands/server.js +37 -0
  59. package/lib/commands/setup.js +127 -75
  60. package/lib/commands/skills.js +4 -1
  61. package/lib/commands/slot.js +49 -9
  62. package/lib/commands/status.js +92 -44
  63. package/lib/commands/tool.js +30 -0
  64. package/lib/commands/vault.js +6 -3
  65. package/lib/commands/volt.js +6 -1
  66. package/lib/commands/walkthrough.js +14 -2
  67. package/lib/commands/watch.js +11 -2
  68. package/lib/commands/wazuh.js +880 -0
  69. package/lib/commands/wazuhIntake.js +358 -0
  70. package/lib/commands/wazuhReport.js +192 -0
  71. package/lib/commands/workflow.js +47 -4
  72. package/lib/commands/x.js +50 -0
  73. package/lib/core/announce.js +86 -0
  74. package/lib/core/auth.js +20 -2
  75. package/lib/core/callMe.js +118 -0
  76. package/lib/core/client.js +27 -1
  77. package/lib/core/commandRegistrars.js +9 -0
  78. package/lib/core/config.js +42 -3
  79. package/lib/core/confirm.js +68 -7
  80. package/lib/core/crossOrgApprovals.js +69 -0
  81. package/lib/core/errors.js +52 -1
  82. package/lib/core/governedSession.js +135 -5
  83. package/lib/core/inboxResolution.js +3 -2
  84. package/lib/core/loginGuard.js +73 -0
  85. package/lib/core/offload.js +120 -0
  86. package/lib/core/org-override.js +5 -1
  87. package/lib/core/output.js +30 -5
  88. package/lib/core/profileTrust.js +182 -0
  89. package/lib/core/quoxglyph.js +22 -1
  90. package/lib/core/router.js +14 -0
  91. package/lib/core/selfUpdate.js +2 -0
  92. package/lib/core/stepUpPrompt.js +99 -0
  93. package/lib/core/version.js +1 -1
  94. package/lib/core/walkthrough.js +9 -5
  95. package/lib/dragon/select.js +2 -2
  96. package/lib/quoxon/EXTENSIONS.md +142 -19
  97. package/lib/quoxon/api.js +189 -34
  98. package/lib/quoxon/app.js +1277 -188
  99. package/lib/quoxon/approvalBatch.js +117 -0
  100. package/lib/quoxon/approvalFacts.js +56 -2
  101. package/lib/quoxon/approvalSentence.js +8 -4
  102. package/lib/quoxon/approvalsView.js +72 -12
  103. package/lib/quoxon/ask.js +4 -2
  104. package/lib/quoxon/autoOpen.js +94 -0
  105. package/lib/quoxon/backgroundWork.js +59 -0
  106. package/lib/quoxon/bang.js +8 -2
  107. package/lib/quoxon/capabilities.js +7 -2
  108. package/lib/quoxon/commandShort.js +8 -1
  109. package/lib/quoxon/commands.js +42 -12
  110. package/lib/quoxon/connect.js +5 -3
  111. package/lib/quoxon/deckLazy.js +58 -0
  112. package/lib/quoxon/delegationDragon.js +49 -0
  113. package/lib/quoxon/destructive.js +35 -0
  114. package/lib/quoxon/dnd.js +118 -0
  115. package/lib/quoxon/dragonCompanion.js +37 -7
  116. package/lib/quoxon/dragonMoments.js +78 -0
  117. package/lib/quoxon/echo.js +1 -0
  118. package/lib/quoxon/events.js +55 -8
  119. package/lib/quoxon/extensions.js +400 -38
  120. package/lib/quoxon/foreignText.js +49 -0
  121. package/lib/quoxon/gripeQueue.js +135 -0
  122. package/lib/quoxon/inboxView.js +125 -22
  123. package/lib/quoxon/keys.js +2 -2
  124. package/lib/quoxon/launcher.js +2 -1
  125. package/lib/quoxon/loopCard.js +7 -2
  126. package/lib/quoxon/loopCards.js +17 -8
  127. package/lib/quoxon/loopStream.js +20 -9
  128. package/lib/quoxon/mailView.js +71 -0
  129. package/lib/quoxon/main.js +27 -6
  130. package/lib/quoxon/markdown.js +1 -1
  131. package/lib/quoxon/model.js +15 -0
  132. package/lib/quoxon/neverAnswered.js +20 -0
  133. package/lib/quoxon/objects/away.js +43 -5
  134. package/lib/quoxon/onboarding/connectServer.js +162 -0
  135. package/lib/quoxon/onboarding/copy.js +6 -0
  136. package/lib/quoxon/onboarding/dockerOffer.js +3 -1
  137. package/lib/quoxon/onboarding/index.js +5 -43
  138. package/lib/quoxon/onboarding/probe.js +20 -1
  139. package/lib/quoxon/onboarding/view.js +16 -3
  140. package/lib/quoxon/orgSwitch.js +22 -0
  141. package/lib/quoxon/print.js +2 -0
  142. package/lib/quoxon/profileSwitch.js +7 -0
  143. package/lib/quoxon/promptHistory.js +3 -1
  144. package/lib/quoxon/render.js +68 -16
  145. package/lib/quoxon/resend.js +14 -0
  146. package/lib/quoxon/roster.js +10 -5
  147. package/lib/quoxon/rosterSessions.js +44 -0
  148. package/lib/quoxon/rows.js +3 -1
  149. package/lib/quoxon/screenContext.js +1 -1
  150. package/lib/quoxon/secretShape.js +88 -0
  151. package/lib/quoxon/slash/agents.js +2 -1
  152. package/lib/quoxon/slash/argSources.js +9 -2
  153. package/lib/quoxon/slash/args.js +4 -2
  154. package/lib/quoxon/slash/broadcast.js +48 -11
  155. package/lib/quoxon/slash/broadcastRun.js +24 -7
  156. package/lib/quoxon/slash/cliLeaves.json +98 -1
  157. package/lib/quoxon/slash/dashboard.js +2 -0
  158. package/lib/quoxon/slash/hatWalk.js +1 -1
  159. package/lib/quoxon/slash/listView.js +2 -1
  160. package/lib/quoxon/slash/palette.js +63 -1
  161. package/lib/quoxon/slash/paletteController.js +15 -5
  162. package/lib/quoxon/slash/restart.js +9 -4
  163. package/lib/quoxon/slash/stepUp.js +5 -5
  164. package/lib/quoxon/slash/stepUpCmds.js +74 -1
  165. package/lib/quoxon/slashSpinner.js +35 -0
  166. package/lib/quoxon/slotAttach.js +8 -0
  167. package/lib/quoxon/slotChat.js +24 -3
  168. package/lib/quoxon/slotFormat.js +1 -1
  169. package/lib/quoxon/slotOutbox.js +113 -0
  170. package/lib/quoxon/slotRun.js +2 -0
  171. package/lib/quoxon/suggestCommand.js +1 -1
  172. package/lib/quoxon/term.js +25 -1
  173. package/lib/quoxon/transcript.js +2 -1
  174. package/lib/quoxon/webPty.js +15 -0
  175. package/lib/quoxon/welcome.js +1 -1
  176. package/lib/quoxon/whatsnew.js +1 -1
  177. package/lib/quoxon/whatsnew.jsonl +42 -0
  178. package/lib/render/planBrief.js +614 -0
  179. package/lib/server/installFlow.js +168 -0
  180. package/lib/server/status.js +15 -1
  181. package/lib/toolOutputShape.js +61 -3
  182. package/package.json +3 -2
  183. package/vendor/dragonlab/SOURCE +3 -3
  184. package/vendor/dragonlab/design/quoxperticus/v2/quoxperticus-master.svg +59 -0
  185. package/vendor/dragonlab/design/quoxperticus/v3/character.mjs +43 -0
  186. package/vendor/dragonlab/design/quoxperticus/v4/character.mjs +57 -0
  187. package/vendor/dragonlab/design/quoxperticus/v4/review.mjs +35 -0
  188. package/vendor/dragonlab/design/quoxperticus/v4/watch.mjs +56 -0
  189. package/vendor/dragonlab/design/vulcan/v1/character.mjs +34 -0
  190. package/vendor/dragonlab/design/vulcan/v1/vulcan-master.svg +57 -0
  191. package/vendor/dragonlab/design/vulcan/v2/character.mjs +39 -0
  192. package/vendor/dragonlab/design/vulcan/v3/character.mjs +41 -0
  193. package/vendor/dragonlab/design/vulcan/v3/review.mjs +35 -0
  194. package/vendor/dragonlab/design/vulcan/v3/watch.mjs +56 -0
  195. package/vendor/dragonlab/factory/svg-rig-contour.mjs +64 -0
  196. package/vendor/dragonlab/integration/companions-v1/characters.mjs +20 -0
  197. package/vendor/dragonlab/integration/companions-v1/quoxperticus.mjs +2 -0
  198. package/vendor/dragonlab/integration/companions-v1/view.mjs +1 -1
  199. package/vendor/dragonlab/integration/companions-v1/vulcan.mjs +2 -0
@@ -0,0 +1,147 @@
1
+ /**
2
+ * `quox inboxq`: InboxQ email namespace. Today: `inboxq watch`, awaited-mail
3
+ * alerts (collector: /api/v1/inboxq/watches, org resolved server-side).
4
+ *
5
+ * `quox inbox` is the HITL approvals inbox and `quox watch` is fleet
6
+ * monitoring; neither is touched here.
7
+ */
8
+
9
+ import { formatOutput } from '../core/output.js';
10
+ import { QuoxApiError, QuoxError } from '../core/errors.js';
11
+ import { resolveEndpoint } from '../api/endpoints.js';
12
+
13
+ const ZONES = ['focus', 'fyi', 'auto', 'screened'];
14
+ const CHANNELS = ['hitl', 'lucid', 'telegram', 'discord', 'call', 'sms'];
15
+ const NEEDS_DESTINATION = ['telegram', 'discord', 'call', 'sms'];
16
+
17
+ const isJson = (program) => program.opts().output === 'json';
18
+ const printJson = (d) => console.log(JSON.stringify(d, null, 2));
19
+
20
+ async function call(fn) {
21
+ try {
22
+ return await fn();
23
+ } catch (e) {
24
+ if (e instanceof QuoxApiError && e.status === 404) throw new QuoxError('Watch not found.', 1);
25
+ throw e;
26
+ }
27
+ }
28
+
29
+ export function describeMatch(match = {}) {
30
+ const parts = [];
31
+ if (match.from) parts.push(`from~${match.from}`);
32
+ if (match.subject) parts.push(`subject~${match.subject}`);
33
+ if (match.text) parts.push(`text~${match.text}`);
34
+ if (match.zone) parts.push(`zone=${match.zone}`);
35
+ return parts.join(' ') || '(any)';
36
+ }
37
+
38
+ export function parseNotify(csv) {
39
+ const list = String(csv ?? 'hitl').split(',').map((s) => s.trim().toLowerCase()).filter(Boolean);
40
+ if (list.length === 0) throw new QuoxError(`--notify needs at least one of: ${CHANNELS.join(', ')}`, 2);
41
+ const bad = list.filter((c) => !CHANNELS.includes(c));
42
+ if (bad.length) throw new QuoxError(`Unknown --notify channel: ${bad.join(', ')}. Use: ${CHANNELS.join(', ')}`, 2);
43
+ return [...new Set(list)];
44
+ }
45
+
46
+ export function registerInboxqCommands(program, ctx) {
47
+ const { client, auth } = ctx;
48
+
49
+ const inboxq = program
50
+ .command('inboxq')
51
+ .description('InboxQ email: awaited-mail watches (not the HITL approvals inbox, see `quox inbox`)');
52
+
53
+ const watch = inboxq
54
+ .command('watch')
55
+ .description('Awaited-mail alerts: get told when a matching email arrives');
56
+
57
+ watch
58
+ .command('add')
59
+ .description('Create a watch. At least one of --from, --subject, --text, --zone is required')
60
+ .option('--account <id>', 'mail account id; omit to watch across all your mailboxes')
61
+ .requiredOption('--label <text>', 'short name for this watch')
62
+ .option('--from <addr>', 'sender contains this text')
63
+ .option('--subject <text>', 'subject contains this text')
64
+ .option('--text <keyword>', 'message text contains this keyword')
65
+ .option('--zone <zone>', `only mail triaged into this zone (${ZONES.join('|')})`)
66
+ .option('--notify <csv>', `alert channels, comma list of ${CHANNELS.join('|')}`, 'hitl')
67
+ .action(async (options) => {
68
+ auth.requireAuth();
69
+ const match = {};
70
+ if (options.from) match.from = options.from;
71
+ if (options.subject) match.subject = options.subject;
72
+ if (options.text) match.text = options.text;
73
+ if (options.zone) {
74
+ if (!ZONES.includes(options.zone)) throw new QuoxError(`--zone must be one of: ${ZONES.join(', ')}`, 2);
75
+ match.zone = options.zone;
76
+ }
77
+ if (Object.keys(match).length === 0) {
78
+ throw new QuoxError('Give at least one of --from, --subject, --text or --zone, or the watch would match nothing in particular.', 2);
79
+ }
80
+ const notify = parseNotify(options.notify);
81
+ const ep = resolveEndpoint('inboxq.watches.create');
82
+ const data = await call(() => client.post(ep.path, {
83
+ service: ep.service,
84
+ body: { accountId: options.account, label: options.label, match, notify },
85
+ }));
86
+ if (isJson(program)) return printJson(data);
87
+ const w = data.watch || {};
88
+ console.log(`Watch ${w.id} created: "${options.label}" watches ${describeMatch(match)}; alerts via ${notify.join(', ')}.`);
89
+ const needs = notify.filter((c) => NEEDS_DESTINATION.includes(c));
90
+ if (needs.length) {
91
+ console.log(`Note: ${needs.join(', ')} need a destination configured, otherwise they will report unconfigured. hitl and lucid work out of the box.`);
92
+ }
93
+ });
94
+
95
+ watch
96
+ .command('list')
97
+ .description('List watches')
98
+ .option('--account <id>', 'only watches for this mail account')
99
+ .option('--json', 'print JSON')
100
+ .action(async (options) => {
101
+ auth.requireAuth();
102
+ const ep = resolveEndpoint('inboxq.watches.list');
103
+ const data = await call(() => client.get(ep.path, { service: ep.service }));
104
+ let watches = data.watches || [];
105
+ if (options.account) watches = watches.filter((w) => w.accountId === options.account);
106
+ if (options.json || isJson(program)) return printJson({ ...data, watches });
107
+ if (watches.length === 0) return console.log('No watches.');
108
+ formatOutput({
109
+ columns: ['id', 'label', 'matches', 'notify', 'active', 'hits'],
110
+ headers: ['ID', 'Label', 'Matches', 'Notify', 'Active', 'Hits'],
111
+ rows: watches.map((w) => ({
112
+ id: w.id,
113
+ label: w.label,
114
+ matches: describeMatch(w.match),
115
+ notify: (w.notify || []).join(','),
116
+ active: w.active ? 'yes' : 'no',
117
+ hits: w.hitCount ?? 0,
118
+ })),
119
+ }, { format: program.opts().output });
120
+ });
121
+
122
+ for (const [name, active, past] of [['pause', false, 'paused'], ['resume', true, 'resumed']]) {
123
+ watch
124
+ .command(name)
125
+ .description(`${name === 'pause' ? 'Pause' : 'Resume'} a watch`)
126
+ .argument('<watchId>', 'watch id')
127
+ .action(async (watchId) => {
128
+ auth.requireAuth();
129
+ const ep = resolveEndpoint('inboxq.watches.active', { watchId });
130
+ const data = await call(() => client.post(ep.path, { service: ep.service, body: { active } }));
131
+ if (isJson(program)) return printJson(data);
132
+ console.log(`Watch ${watchId} ${past}.`);
133
+ });
134
+ }
135
+
136
+ watch
137
+ .command('rm')
138
+ .description('Delete a watch')
139
+ .argument('<watchId>', 'watch id')
140
+ .action(async (watchId) => {
141
+ auth.requireAuth();
142
+ const ep = resolveEndpoint('inboxq.watches.delete', { watchId });
143
+ const data = await call(() => client.delete(ep.path, { service: ep.service }));
144
+ if (isJson(program)) return printJson(data);
145
+ console.log(`Watch ${watchId} deleted.`);
146
+ });
147
+ }
@@ -7,8 +7,9 @@
7
7
  * quox kuma down [slug] only monitors that are not up (exit 3 when any)
8
8
  * quox kuma uptime [slug] 24h/30d uptime table, worst first
9
9
  * quox kuma refresh [slug] ask the collector to re-scrape the status page(s)
10
- * quox kuma monitors list|get|add|edit|pause|resume|remove manage monitors (writes need org admin + human approval)
10
+ * quox kuma monitors list|get|add|edit|pause|resume|wire-alerts|remove manage monitors (writes need org admin + human approval)
11
11
  * quox kuma incidents [--open] Kuma alerts that became Quox incidents (NQ-KUMA-4)
12
+ * quox kuma incidents show <id> one incident, its diagnosis and the Wazuh picture for its host (WAZ P7)
12
13
  * quox kuma hooks list|create|delete per-org signed alert hook Kuma posts to (org admin)
13
14
  * quox kuma diagnose <incidentId>|--monitor <id> why is it down: outside-in + governed host checks (NQ-KUMA-5)
14
15
  * quox kuma sla [--month|--from --to] | list | verify <id|file> signed, verifiable uptime reports (NQ-KUMA-7)
@@ -443,11 +444,17 @@ export function registerKumaCommands(program, ctx) {
443
444
  if (jsonOut()) { console.log(JSON.stringify(data, null, 2)); return; }
444
445
  console.log(glyphLine('kuma', 'monitors list', { result: `${data.monitors.length} monitors${data.kumaVersion ? ` · Kuma ${data.kumaVersion}` : ''}`, status: 'ok' }));
445
446
  if (!data.monitors.length) return;
446
- const rows = [['ID', 'ACTIVE', 'TYPE', 'NAME', 'TARGET', 'INTERVAL']];
447
+ const rows = [['ID', 'ACTIVE', 'TYPE', 'NAME', 'TARGET', 'INTERVAL', 'ALERTS']];
448
+ let noAlert = 0;
447
449
  for (const m of data.monitors) {
448
- rows.push([m.id ?? '-', m.active === null || m.active === undefined ? '-' : (m.active ? 'yes' : 'no'), m.type || '-', m.name || '-', targetOf(m), m.interval ? `${m.interval}s` : '-']);
450
+ let alerts;
451
+ if (!Array.isArray(m.alerts)) alerts = '?';
452
+ else if (!m.alerts.length) { alerts = '⚠ NONE'; noAlert += 1; }
453
+ else alerts = m.alerts.map((a) => (a && a.name) || a).join(',');
454
+ rows.push([m.id ?? '-', m.active === null || m.active === undefined ? '-' : (m.active ? 'yes' : 'no'), m.type || '-', m.name || '-', targetOf(m), m.interval ? `${m.interval}s` : '-', alerts]);
449
455
  }
450
456
  console.log(pad(rows).join('\n'));
457
+ if (noAlert) console.log(`${noAlert} monitor(s) have no Quox alert, so outages on them are not seen. Fix: quox kuma monitors wire-alerts <id...> (each waits for a human approval)`);
451
458
  }));
452
459
 
453
460
  monitors
@@ -490,6 +497,12 @@ export function registerKumaCommands(program, ctx) {
490
497
  if (!/^\d+$/.test(String(v))) throw new KumaFail(`quox kuma ${verb}: the id must be a whole number, got "${v}"`);
491
498
  return Number(v);
492
499
  };
500
+ /** A monitor added with alerts [] is invisible to Quox incident capture: say so. */
501
+ const warnNoAlert = (d) => {
502
+ if (d && d.status !== 'awaiting_approval' && Array.isArray(d.alerts) && d.alerts.length === 0) {
503
+ console.log(`⚠ ${d.name || 'The new monitor'}${d.id ? ` (id ${d.id})` : ''} has no Quox alert, so outages on it are not seen. Fix: quox kuma monitors wire-alerts ${d.id ?? '<id>'}`);
504
+ }
505
+ };
493
506
  const awaitingLine = (id) => `◐ awaiting approval ${id}: a person approves it in the inbox; check with quox approvals result ${id}`;
494
507
  /** Run a governed kuma_* tool as the attached agent and report it like a human write. Returns the tool result. */
495
508
  async function agentTool(agentName, verb, tool, input, okLine) {
@@ -529,6 +542,7 @@ export function registerKumaCommands(program, ctx) {
529
542
  let data;
530
543
  try { data = await client.post(ep.path, { service: ep.service, body: { monitor } }); } catch (err) { throw monFail('add', ep, err); }
531
544
  reportWrite('add', ep, data, (d) => `added ${d.name || monitor.name}${d.id ? ` (id ${d.id})` : ''}`);
545
+ if (!jsonOut()) warnNoAlert(data);
532
546
  } catch (err) { fail(err); }
533
547
  });
534
548
 
@@ -581,6 +595,24 @@ export function registerKumaCommands(program, ctx) {
581
595
  } catch (err) { fail(err); }
582
596
  });
583
597
 
598
+ monitors
599
+ .command('wire-alerts')
600
+ .description('Attach the default Quox alert to monitors that have none, so their outages are seen (org admin; one human approval per monitor)')
601
+ .argument('<id...>', 'monitor id(s)')
602
+ .action(async (ids) => {
603
+ try {
604
+ auth.requireAuth();
605
+ const agentName = await actingAgent();
606
+ for (const id of ids) {
607
+ if (agentName) { await agentTool(agentName, 'monitors wire-alerts', 'kuma_monitor_wire_alerts', { id: wholeId('monitors wire-alerts', id) }, () => `wired Quox alerts on monitor ${id}`); continue; }
608
+ const ep = resolveEndpoint('mon.kuma.monitors.wire-alerts', { id });
609
+ let data;
610
+ try { data = await client.post(ep.path, { service: ep.service, body: {} }); } catch (err) { throw monFail('wire-alerts', ep, err, id); }
611
+ reportWrite('wire-alerts', ep, data, (d) => `wired Quox alerts on ${d.name || `monitor ${id}`}`);
612
+ }
613
+ } catch (err) { fail(err); }
614
+ });
615
+
584
616
  monitors
585
617
  .command('remove')
586
618
  .description('Delete a monitor (org admin; held for human approval)')
@@ -614,12 +646,20 @@ export function registerKumaCommands(program, ctx) {
614
646
  return new KumaFail(`${head}: ${msg}`);
615
647
  };
616
648
 
649
+ // WAZ P7: `quox kuma incidents show <id>` is positional words on this ONE leaf (not a subcommand), so the list verb
650
+ // keeps its leaf-policy row for chat and the matrix; `show <id>` prints one incident with the Wazuh picture.
617
651
  kuma
618
652
  .command('incidents')
619
- .description('Kuma alerts that became Quox incidents: when down, when back, how long')
653
+ .description('Kuma alerts that became Quox incidents: when down, when back, how long. `incidents show <id>`: one incident with its diagnosis and the Wazuh agent + open Wazuh incidents for the same host')
654
+ .argument('[words...]', 'show <incidentId> for one incident')
620
655
  .option('--open', 'only outages still going')
621
656
  .option('--limit <n>', 'max rows (default 50, max 200)')
622
- .action(guard('incidents', async (options) => {
657
+ .action(guard('incidents', async (words, options) => {
658
+ const w = Array.isArray(words) ? words : [];
659
+ if (w.length) {
660
+ if (w[0] !== 'show' || w.length !== 2) throw new KumaFail(`quox kuma incidents: unexpected "${w.join(' ')}". Use \`quox kuma incidents [--open]\` or \`quox kuma incidents show <incidentId>\`.`);
661
+ return showIncident(w[1]);
662
+ }
623
663
  auth.requireAuth();
624
664
  const ep = resolveEndpoint('mon.kuma.incidents.list', { open: !!options.open, limit: options.limit });
625
665
  let data;
@@ -657,6 +697,38 @@ export function registerKumaCommands(program, ctx) {
657
697
  }
658
698
  }));
659
699
 
700
+ // WAZ P7: one incident with its diagnosis and the Wazuh picture for the host (attached when the outage opened).
701
+ async function showIncident(incidentId) {
702
+ auth.requireAuth();
703
+ const ep = resolveEndpoint('mon.kuma.incidents.get', { id: incidentId });
704
+ let data;
705
+ try { data = await client.get(ep.path, { service: ep.service }); } catch (err) {
706
+ const status = err && (err.status ?? err.statusCode);
707
+ if (status === 404) throw new KumaFail(`quox kuma incidents show: ${ep.method} ${ep.path} failed with HTTP 404: no such incident (${incidentId}) in this org. List them with \`quox kuma incidents\`.`);
708
+ throw new KumaFail(`quox kuma incidents show: ${ep.method} ${ep.path} failed${status ? ` with HTTP ${status}` : ''}: ${(err && err.message) || err}`);
709
+ }
710
+ if (!data || data.success === false || data.error || !data.incident) {
711
+ throw new KumaFail(`quox kuma incidents show: ${ep.method} ${ep.path} returned an error body: ${(data && data.error) || 'response had no incident'}`);
712
+ }
713
+ if (jsonOut()) { console.log(JSON.stringify(data, null, 2)); return; }
714
+ const i = data.incident;
715
+ console.log(glyphLine('kuma', 'incidents show', { result: `${i.id} · ${i.open ? 'DOWN' : 'resolved'}`, status: i.open ? 'warn' : 'ok' }));
716
+ const d = i.diagnosis;
717
+ const w = i.wazuh;
718
+ const rows = [
719
+ ['Monitor', `${i.monitor_name || '-'} (monitor ${i.monitor_id ?? '-'})`],
720
+ ['Target', i.target || '-'],
721
+ ['State', i.open ? 'down' : (i.close_reason === 'monitor_deleted' ? 'closed: monitor deleted' : 'resolved')],
722
+ ['Opened', i.opened_at || '-'],
723
+ ['Closed', i.closed_at || '-'],
724
+ ['Events', i.events ?? '-'],
725
+ ['Last message', i.last_msg || '-'],
726
+ ['Why', d && d.summary ? `${d.summary} [${d.verdict}]` : 'not diagnosed yet (quox kuma diagnose ' + i.id + ')'],
727
+ ['Wazuh', w && w.summary ? w.summary : 'not checked (Quox attaches the Wazuh picture when an outage opens and Wazuh is configured)'],
728
+ ];
729
+ console.log(pad(rows).join('\n'));
730
+ }
731
+
660
732
  const printDiagnosis = (d, verb) => {
661
733
  console.log(glyphLine('kuma', verb, { result: d.verdict, status: d.verdict === 'not_reproducible' ? 'warn' : 'fail' }));
662
734
  console.log(d.summary);
@@ -1019,6 +1091,7 @@ export function registerKumaCommands(program, ctx) {
1019
1091
  'monitors edit': 'quox kuma monitors edit 4 --interval 60',
1020
1092
  'monitors pause': 'quox kuma monitors pause 4',
1021
1093
  'monitors resume': 'quox kuma monitors resume 4',
1094
+ 'monitors wire-alerts': 'quox kuma monitors wire-alerts 4 7',
1022
1095
  'monitors remove': 'quox kuma monitors remove 4',
1023
1096
  incidents: 'quox kuma incidents --open',
1024
1097
  diagnose: 'quox kuma diagnose --monitor 7',
@@ -0,0 +1,311 @@
1
+ /**
2
+ * QUOXON (owner 2026-11-05): the render layer for `quox setup license link`.
3
+ *
4
+ * The owner linked the wrong licence twice in one day (one granting 0
5
+ * plugins) because neither the web page nor the CLI said what was being
6
+ * picked. This view frames the code and link, counts the code down live,
7
+ * and on approval names WHAT was linked: licence id, plan, plugins, seats.
8
+ *
9
+ * Everything here is pure (strings in, strings out) so it is testable without
10
+ * a terminal. Colour comes from the house QuoxGlyph module (lib/core/
11
+ * quoxglyph.js: line grammar, brand gradient, status colours) and the spinner
12
+ * frames from the TUI's house spinner; nothing here invents a parallel style.
13
+ * With colour off every function returns plain lines with no escape codes.
14
+ */
15
+
16
+ import { line, setColor, GRAD, STATUS, celebrate } from '../core/quoxglyph.js';
17
+ import { SPIN_FRAMES } from '../quoxon/slashSpinner.js';
18
+ import { motionAllowed } from '../quoxon/motion/clock.js';
19
+
20
+ const R = '\x1b[0m';
21
+ const BOLD = '\x1b[1m';
22
+ const DIM = '\x1b[2m';
23
+ const UNDERLINE = '\x1b[4m';
24
+ const fg = ([r, g, b]) => `\x1b[38;2;${r};${g};${b}m`;
25
+ const AMBER = STATUS.warn.rgb;
26
+ const GREEN = STATUS.ok.rgb;
27
+
28
+ // eslint-disable-next-line no-control-regex
29
+ const ESC = /\x1b\]8;;[^\x07]*\x07|\x1b\[[0-9;]*[A-Za-z]/g;
30
+ /** Visible width: escape codes stripped, one column per code point. */
31
+ const vis = (s) => [...String(s).replace(ESC, '')].length;
32
+ const padTo = (s, w) => s + ' '.repeat(Math.max(0, w - vis(s)));
33
+
34
+ /** Run a QuoxGlyph render with colour forced on or off, then hand control back to auto. */
35
+ function withColor(color, fn) {
36
+ setColor(color);
37
+ try { return fn(); } finally { setColor(null); }
38
+ }
39
+
40
+ /** Should this terminal get colour? Same rule as quoxglyph, decided for an injected stream. */
41
+ export function colorFor(term, env = process.env) {
42
+ if (env.QUOX_COLOR === '0') return false;
43
+ if (env.QUOX_COLOR === '1' || env.QUOX_COLOR === '2') return true;
44
+ return Boolean(term && term.isTTY) && !env.NO_COLOR;
45
+ }
46
+
47
+ /** Should the waiting line animate? TTY only, and never under reduced motion. */
48
+ export const animateFor = (term, env = process.env) => motionAllowed(Boolean(term && term.isTTY), env);
49
+
50
+ /** m:ss, floored at 0:00. */
51
+ export function fmtCountdown(ms) {
52
+ const total = Math.max(0, Math.floor(Number(ms) / 1000) || 0);
53
+ return `${Math.floor(total / 60)}:${String(total % 60).padStart(2, '0')}`;
54
+ }
55
+
56
+ /** WQXP-7T4K -> "W Q X P - 7 T 4 K". */
57
+ export const spacedCode = (code) => [...String(code || '')].join(' ');
58
+
59
+ /** Paint text in the brand gradient (per visible character), bold. */
60
+ function gradient(text) {
61
+ const chars = [...text];
62
+ const last = Math.max(1, chars.length - 1);
63
+ return BOLD + chars.map((ch, i) => (ch === ' ' ? ch
64
+ : fg(GRAD[0].map((v, k) => Math.round(v + (GRAD[1][k] - v) * (i / last)))) + ch)).join('') + R;
65
+ }
66
+
67
+ /** ▰▰▰▱▱ in the brand gradient, for a 0..1 ratio. */
68
+ function bar(ratio, width = 10) {
69
+ const filled = Math.max(0, Math.min(width, Math.round(ratio * width)));
70
+ const last = Math.max(1, width - 1);
71
+ let out = '';
72
+ for (let i = 0; i < filled; i++) out += fg(GRAD[0].map((v, k) => Math.round(v + (GRAD[1][k] - v) * (i / last)))) + '▰';
73
+ return out + (filled ? R : '') + DIM + '▱'.repeat(width - filled) + R;
74
+ }
75
+
76
+ /** OSC 8 hyperlink whose visible text is the full URL, so it is clickable either way. */
77
+ const hyperlink = (url) => `\x1b]8;;${url}\x07${UNDERLINE}${fg(GRAD[0])}${url}${R}\x1b]8;;\x07`;
78
+
79
+ /**
80
+ * A rounded frame around rows (already styled). The title sits in the top
81
+ * border. The frame grows to fit its widest row (a URL is never clipped).
82
+ */
83
+ function frame(rows, { title = '', cols = 80, tint = null } = {}) {
84
+ const b = (s) => (tint ? fg(tint) + s + R : DIM + s + R);
85
+ const titleText = title ? ` ${title} ` : '';
86
+ const inner = Math.max(44, Math.min(Math.max(44, cols - 6), 76), ...rows.map((r) => vis(r) + 4), vis(titleText) + 4);
87
+ const top = b('╭─') + titleText + b('─'.repeat(Math.max(0, inner - 1 - vis(titleText))) + '╮');
88
+ const body = rows.map((r) => b('│') + padTo(` ${r}`, inner) + b('│'));
89
+ const bottom = b('╰' + '─'.repeat(inner) + '╯');
90
+ return [top, ...body, bottom].map((l) => ` ${l}`).join('\n');
91
+ }
92
+
93
+ /** Greedy wrap of items into lines no wider than `width`, joined by `sep`. */
94
+ function wrapList(items, width, sep = ' · ') {
95
+ const lines = [];
96
+ let cur = '';
97
+ for (const item of items) {
98
+ const next = cur ? cur + sep + item : item;
99
+ if (cur && vis(next) > width) { lines.push(cur); cur = item; } else cur = next;
100
+ }
101
+ if (cur) lines.push(cur);
102
+ return lines;
103
+ }
104
+
105
+ const minutes = (ms) => Math.max(1, Math.round(ms / 60000));
106
+
107
+ /**
108
+ * The card shown while the code waits for approval.
109
+ * @param {{userCode:string,url:string,remainingMs:number,color:boolean,cols?:number}} o
110
+ */
111
+ export function renderLinkCard({ userCode, url, remainingMs, color, cols = 80 }) {
112
+ const header = withColor(color, () => line('quox', 'licence link', { result: 'approve on quox.ai', status: 'busy' }));
113
+ if (!color) {
114
+ return [
115
+ header,
116
+ `Open ${url} and enter this code:`,
117
+ '',
118
+ ` ${userCode}`,
119
+ '',
120
+ `The code is good for ${minutes(remainingMs)} minutes.`,
121
+ 'Pick the licence you want on quox.ai: this instance gets exactly what you approve.',
122
+ 'Waiting for approval...',
123
+ ].join('\n');
124
+ }
125
+ const label = (s) => `${DIM}${s.padEnd(11)}${R}`;
126
+ const rows = [
127
+ '',
128
+ `${label('Your code')}${gradient(spacedCode(userCode))}`,
129
+ '',
130
+ `${label('Open')}${hyperlink(url)}`,
131
+ `${label('Expires in')}${bar(1)} ${BOLD}${fmtCountdown(remainingMs)}${R}`,
132
+ '',
133
+ `${fg(AMBER)}Pick the licence you want on quox.ai.${R}`,
134
+ `${DIM}This instance gets exactly what you approve.${R}`,
135
+ ];
136
+ return [header, frame(rows, { title: `${BOLD}Link this instance${R}`, cols })].join('\n');
137
+ }
138
+
139
+ /**
140
+ * One frame of the live waiting line (TTY only): spinner, label, countdown.
141
+ * @param {{frame:number,remainingMs:number,totalMs?:number,color:boolean,note?:string}} o
142
+ */
143
+ export function renderWaitingLine({ frame: i, remainingMs, totalMs = 600000, color, note = '' }) {
144
+ const spin = SPIN_FRAMES[i % SPIN_FRAMES.length];
145
+ const left = `${fmtCountdown(remainingMs)} left`;
146
+ const extra = note ? ` · ${note}` : '';
147
+ if (!color) return `${spin} Waiting for approval on quox.ai · ${left}${extra}`;
148
+ const ratio = totalMs > 0 ? remainingMs / totalMs : 0;
149
+ const tone = ratio < 0.2 ? fg(AMBER) : '';
150
+ return ` ${fg(GRAD[0])}${spin}${R} Waiting for approval on quox.ai ${DIM}│${R} ${bar(ratio)} ${tone}${BOLD}${left}${R}${note ? `${DIM}${extra}${R}` : ''}`;
151
+ }
152
+
153
+ const bareId = (raw) => String(raw).split('@')[0];
154
+ const orgAllowance = (raw) => {
155
+ const m = /@orgs=(\d+)/.exec(String(raw));
156
+ return m ? Number(m[1]) : null;
157
+ };
158
+
159
+ /**
160
+ * What was linked, from the approved lease plus the read-back of
161
+ * GET /api/setup/licensing/entitlements (the route `quox licence status`
162
+ * reads). The read-back wins where it has the licence; the lease is the
163
+ * fallback, so a failed read never hides what was activated.
164
+ *
165
+ * @returns {{license_id, plans:string[], entitlements:string[], seats:Array<{plugin,used,total}>, warnings:string[], servers?:{used,total}}}
166
+ */
167
+ export function linkedDetails(lease = {}, status = null, poll = {}) {
168
+ const id = lease.license_id || null;
169
+ const row = (status && Array.isArray(status.licenses))
170
+ ? status.licenses.find((l) => l && l.license_id === id) : null;
171
+
172
+ let rawEntitlements;
173
+ let plans;
174
+ if (row) {
175
+ rawEntitlements = row.entitlements || [];
176
+ plans = row.plans || [];
177
+ } else {
178
+ const raw = lease.entitlements || [];
179
+ plans = raw.filter((e) => String(e).toLowerCase().startsWith('plan.')).map((e) => bareId(e).slice(5).toLowerCase()).filter(Boolean);
180
+ rawEntitlements = raw.filter((e) => !String(e).toLowerCase().startsWith('plan.'));
181
+ }
182
+
183
+ const bands = (status && status.org_bands && typeof status.org_bands === 'object') ? status.org_bands : {};
184
+ const entitlements = [...new Set(rawEntitlements.map(bareId))];
185
+ const seats = [];
186
+ for (const raw of rawEntitlements) {
187
+ const plugin = bareId(raw);
188
+ const band = bands[plugin];
189
+ if (band) seats.push({ plugin, used: Number(band.assigned_count ?? 0), total: Number(band.allowance ?? 0) });
190
+ else if (orgAllowance(raw) != null) seats.push({ plugin, used: null, total: orgAllowance(raw) });
191
+ }
192
+
193
+ const out = { license_id: id, plans, entitlements, seats, warnings: entitlements.length === 0 ? ['zero_entitlements'] : [] };
194
+ // Server seats travel only when the poll response carries them (the
195
+ // control plane sends max_servers and bound; additive, never guessed).
196
+ if (poll && poll.max_servers != null) {
197
+ out.servers = { used: Array.isArray(poll.bound) ? poll.bound.length : null, total: Number(poll.max_servers) };
198
+ }
199
+ return out;
200
+ }
201
+
202
+ const seatText = (s) => (s.used == null ? `${s.plugin} up to ${s.total} orgs` : `${s.plugin} ${s.used} of ${s.total} orgs`);
203
+ const leaseText = (iso) => {
204
+ const t = Date.parse(iso);
205
+ return Number.isFinite(t) ? new Date(t).toISOString().slice(0, 16).replace('T', ' ') + ' UTC' : null;
206
+ };
207
+
208
+ /** The exact command that unlinks this licence and starts a fresh link. */
209
+ export function relinkCommand(licenseId, profile) {
210
+ const q = profile ? `quox -p ${profile}` : 'quox';
211
+ return `${q} setup license deactivate --license-id ${licenseId} --yes && ${q} setup license link`;
212
+ }
213
+
214
+ /**
215
+ * The result after approval: compact, celebratory when the licence grants
216
+ * something, amber with the one fix command when it grants nothing.
217
+ * @param {ReturnType<typeof linkedDetails>} d
218
+ * @param {{color:boolean, cols?:number, leaseExpiresAt?:string, profile?:string, readBackError?:string}} o
219
+ */
220
+ export function renderLinkedCard(d, { color, cols = 80, leaseExpiresAt = null, profile = null, readBackError = null } = {}) {
221
+ const zero = d.warnings.includes('zero_entitlements');
222
+ const header = withColor(color, () => (zero
223
+ ? line('quox', 'licence linked', { result: 'grants 0 plugins', status: 'warn' })
224
+ : line('quox', 'licence linked', { result: color ? celebrate('instance activated') : 'instance activated', status: 'ok', max: 400 })));
225
+ const plan = d.plans.length ? d.plans.join(', ') : '(none on this licence)';
226
+ const renews = leaseText(leaseExpiresAt);
227
+ const fix = relinkCommand(d.license_id, profile);
228
+ const warnText = 'This licence grants 0 plugins, so nothing unlocks. You may have picked the wrong licence on quox.ai.';
229
+ const readBack = readBackError
230
+ ? `Could not read the licence details back (${readBackError}). Showing what the lease carried; run \`quox licence status\` to check.`
231
+ : null;
232
+
233
+ if (!color) {
234
+ const out = [header];
235
+ if (readBack) out.push(readBack);
236
+ out.push(`Licence: ${d.license_id}`);
237
+ out.push(`Plan: ${plan}`);
238
+ out.push(`Plugins (${d.entitlements.length}): ${d.entitlements.join(', ') || 'none'}`);
239
+ if (d.seats.length) out.push(`Seats: ${d.seats.map(seatText).join(', ')}`);
240
+ if (d.servers) out.push(`Servers: ${d.servers.used ?? '?'} of ${d.servers.total} in use`);
241
+ if (renews) out.push(`Lease renews by: ${renews}`);
242
+ if (zero) {
243
+ out.push(`⚠ ${warnText}`);
244
+ out.push('Unlink it and link the right one:');
245
+ out.push(` ${fix}`);
246
+ } else {
247
+ out.push('This instance is activated. No key was ever pasted into it.');
248
+ }
249
+ return out.join('\n');
250
+ }
251
+
252
+ const label = (s) => `${DIM}${s.padEnd(10)}${R}`;
253
+ const width = Math.min(Math.max(44, cols - 6), 76) - 14;
254
+ const rows = [
255
+ `${label('Licence')}${BOLD}${d.license_id}${R}`,
256
+ `${label('Plan')}${d.plans.length ? gradient(plan) : `${DIM}${plan}${R}`}`,
257
+ ];
258
+ if (zero) {
259
+ rows.push(`${label('Plugins')}${fg(AMBER)}${BOLD}0${R}${fg(AMBER)} entitled${R}`);
260
+ } else {
261
+ const list = wrapList(d.entitlements, width);
262
+ rows.push(`${label('Plugins')}${BOLD}${d.entitlements.length}${R} ${DIM}entitled${R}`);
263
+ for (const l of list) rows.push(`${' '.repeat(10)}${fg(GREEN)}${l}${R}`);
264
+ }
265
+ for (const [n, s] of d.seats.entries()) {
266
+ const ratio = s.used == null || !s.total ? 0 : s.used / s.total;
267
+ const text = s.used == null ? `up to ${s.total} orgs` : `${s.used} of ${s.total} orgs`;
268
+ rows.push(`${label(n === 0 ? 'Seats' : '')}${s.plugin} ${bar(ratio, 6)} ${text}`);
269
+ }
270
+ if (d.servers) rows.push(`${label('Servers')}${d.servers.used ?? '?'} of ${d.servers.total} in use`);
271
+ if (renews) rows.push(`${label('Lease')}${DIM}renews by ${renews}${R}`);
272
+
273
+ const title = zero
274
+ ? `${fg(AMBER)}${BOLD}${STATUS.warn.glyph} Linked, but it grants 0 plugins${R}`
275
+ : `${fg(GREEN)}${BOLD}${STATUS.ok.glyph} Licence linked${R}`;
276
+ const out = [header];
277
+ if (readBack) out.push(` ${fg(AMBER)}${readBack}${R}`);
278
+ out.push(frame(rows, { title, cols, tint: zero ? AMBER : GREEN }));
279
+ if (zero) {
280
+ out.push(` ${fg(AMBER)}${BOLD}${STATUS.warn.glyph}${R} ${fg(AMBER)}${warnText}${R}`);
281
+ out.push(` ${DIM}Unlink it and link the right one:${R}`);
282
+ out.push(` ${BOLD}${fix}${R}`);
283
+ } else {
284
+ out.push(` ${DIM}No key was ever pasted into this instance.${R}`);
285
+ }
286
+ return out.join('\n');
287
+ }
288
+
289
+ /**
290
+ * The live waiting indicator. On an animating TTY it redraws one line in
291
+ * place every 80 ms (house spinner pace) with the countdown; with motion off
292
+ * it draws once. `note()` prints a line above it without tearing the spinner.
293
+ * `stop()` clears the line. Never used when the stream is not a TTY.
294
+ */
295
+ export function startWaiting({ term, deadline, totalMs, color, animate, now = () => Date.now(), frameMs = 80 }) {
296
+ let i = 0;
297
+ let note = '';
298
+ const draw = () => {
299
+ term.write('\r\x1b[2K' + renderWaitingLine({ frame: i++, remainingMs: deadline - now(), totalMs, color, note }));
300
+ };
301
+ draw();
302
+ const timer = animate ? setInterval(draw, frameMs) : null;
303
+ if (timer && typeof timer.unref === 'function') timer.unref();
304
+ return {
305
+ note(text) { note = text; draw(); },
306
+ stop() {
307
+ if (timer) clearInterval(timer);
308
+ term.write('\r\x1b[2K');
309
+ },
310
+ };
311
+ }
@@ -4,6 +4,7 @@
4
4
  * quox linkedin post "<text>" --as <author_urn> [--visibility PUBLIC|CONNECTIONS] [--link <url>] [--image-url <https url>]
5
5
  * quox linkedin comment <post_urn> "<text>" --as <actor_urn>
6
6
  * quox linkedin posts <author_urn> [--count <n>]
7
+ * quox linkedin whoami
7
8
  * quox linkedin org <org_id>
8
9
  *
9
10
  * Both write verbs execute the linkedin connector tools through the governed
@@ -154,6 +155,24 @@ export function registerLinkedInCommands(program, ctx) {
154
155
  }
155
156
  });
156
157
 
158
+ // ── linkedin me (read) ───────────────────────────────────────────────────
159
+
160
+ linkedin
161
+ .command('whoami')
162
+ .description('Show the connected member identity and the member URN to use with --as on posts (read-only). Needs the openid and profile scopes on the token.')
163
+ .action(async () => {
164
+ const data = await executeLinkedInTool({ client, auth, program }, 'linkedin_get_me', {});
165
+
166
+ if (program.opts().output === 'json') {
167
+ formatDetail(data, { format: 'json' });
168
+ return;
169
+ }
170
+ const me = data.result || {};
171
+ printSuccess(`${me.name || 'Unknown'} (${me.member_urn || 'no member URN returned'})`);
172
+ if (me.email) printInfo(`Email: ${me.email}`);
173
+ if (me.member_urn) printInfo(`Use as author: --as ${me.member_urn}`);
174
+ });
175
+
157
176
  // ── linkedin org (read) ──────────────────────────────────────────────────
158
177
 
159
178
  linkedin