quoxcli 0.3.1 → 0.5.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 (306) hide show
  1. package/CHANGELOG.md +61 -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/actions.js +15 -0
  6. package/lib/api/endpoints/admin.js +27 -0
  7. package/lib/api/endpoints/agent-attach.js +7 -0
  8. package/lib/api/endpoints/blog.js +16 -0
  9. package/lib/api/endpoints/chat.js +7 -0
  10. package/lib/api/endpoints/compliance.js +25 -0
  11. package/lib/api/endpoints/contactsq.js +4 -0
  12. package/lib/api/endpoints/devServerRegistry.js +25 -0
  13. package/lib/api/endpoints/engagement.js +26 -0
  14. package/lib/api/endpoints/evidence.js +20 -0
  15. package/lib/api/endpoints/hat.js +14 -0
  16. package/lib/api/endpoints/inbox.js +6 -0
  17. package/lib/api/endpoints/inboxq.js +19 -0
  18. package/lib/api/endpoints/index.js +14 -0
  19. package/lib/api/endpoints/loops.js +26 -0
  20. package/lib/api/endpoints/mon.js +295 -0
  21. package/lib/api/endpoints/objectives.js +6 -0
  22. package/lib/api/endpoints/quoxseo.js +6 -0
  23. package/lib/api/endpoints/server.js +21 -0
  24. package/lib/api/endpoints/setup.js +6 -0
  25. package/lib/api/endpoints/tools.js +13 -0
  26. package/lib/api/endpoints/v1.js +47 -0
  27. package/lib/commands/actions.js +81 -11
  28. package/lib/commands/admin.js +35 -0
  29. package/lib/commands/agent.js +11 -4
  30. package/lib/commands/agentAttach.js +61 -7
  31. package/lib/commands/approvals.js +198 -3
  32. package/lib/commands/ask.js +4 -2
  33. package/lib/commands/auth.js +64 -11
  34. package/lib/commands/backup.js +11 -4
  35. package/lib/commands/blog.js +110 -0
  36. package/lib/commands/capabilities.js +3 -2
  37. package/lib/commands/chat.js +142 -28
  38. package/lib/commands/chatCompanion.js +27 -0
  39. package/lib/commands/chatState.js +45 -0
  40. package/lib/commands/claude.js +7 -33
  41. package/lib/commands/compliance.js +109 -0
  42. package/lib/commands/connect.js +69 -0
  43. package/lib/commands/contacts.js +85 -4
  44. package/lib/commands/contactsSendAt.js +21 -0
  45. package/lib/commands/dev.js +6 -8
  46. package/lib/commands/doctor.js +22 -3
  47. package/lib/commands/engagement.js +90 -0
  48. package/lib/commands/evidence.js +155 -0
  49. package/lib/commands/ext.js +23 -0
  50. package/lib/commands/fleet.js +25 -0
  51. package/lib/commands/gripe.js +24 -13
  52. package/lib/commands/hat.js +76 -0
  53. package/lib/commands/image.js +68 -0
  54. package/lib/commands/inbox.js +37 -3
  55. package/lib/commands/inboxq.js +147 -0
  56. package/lib/commands/kuma.js +1123 -0
  57. package/lib/commands/kumaSla.js +174 -0
  58. package/lib/commands/licence.js +39 -1
  59. package/lib/commands/licenceLinkView.js +311 -0
  60. package/lib/commands/linkedin.js +19 -0
  61. package/lib/commands/loop.js +318 -0
  62. package/lib/commands/lucid.js +1 -0
  63. package/lib/commands/mail.js +161 -20
  64. package/lib/commands/memory.js +68 -4
  65. package/lib/commands/memorySync.js +158 -0
  66. package/lib/commands/mon.js +55 -144
  67. package/lib/commands/notes.js +29 -7
  68. package/lib/commands/notesSeal.js +259 -0
  69. package/lib/commands/objective.js +53 -3
  70. package/lib/commands/offload.js +2 -3
  71. package/lib/commands/org.js +21 -4
  72. package/lib/commands/plan.js +17 -0
  73. package/lib/commands/provider.js +87 -8
  74. package/lib/commands/qlarity.js +32 -1
  75. package/lib/commands/quoxseo.js +31 -0
  76. package/lib/commands/research.js +79 -0
  77. package/lib/commands/rows.js +9 -0
  78. package/lib/commands/run.js +20 -2
  79. package/lib/commands/scope.js +2 -2
  80. package/lib/commands/server.js +150 -0
  81. package/lib/commands/setup.js +127 -75
  82. package/lib/commands/shortcut.js +41 -0
  83. package/lib/commands/skills.js +24 -5
  84. package/lib/commands/slot.js +49 -9
  85. package/lib/commands/status.js +92 -44
  86. package/lib/commands/tool.js +30 -0
  87. package/lib/commands/vault.js +6 -3
  88. package/lib/commands/vm.js +2 -2
  89. package/lib/commands/walkthrough.js +14 -2
  90. package/lib/commands/watch.js +11 -2
  91. package/lib/commands/wazuh.js +824 -0
  92. package/lib/commands/wazuhIntake.js +317 -0
  93. package/lib/commands/wazuhReport.js +192 -0
  94. package/lib/commands/workflow.js +47 -4
  95. package/lib/commands/x.js +50 -0
  96. package/lib/commands/youtube.js +172 -0
  97. package/lib/core/actionItems.js +115 -0
  98. package/lib/core/agentAlias.js +2 -0
  99. package/lib/core/agentFamilies.js +7 -0
  100. package/lib/core/announce.js +86 -0
  101. package/lib/core/auth.js +20 -2
  102. package/lib/core/callMe.js +118 -0
  103. package/lib/core/client.js +37 -3
  104. package/lib/core/commandRegistrars.js +17 -0
  105. package/lib/core/config.js +29 -3
  106. package/lib/core/confirm.js +68 -7
  107. package/lib/core/crossOrgApprovals.js +69 -0
  108. package/lib/core/errors.js +52 -1
  109. package/lib/core/findingReconcile.js +90 -0
  110. package/lib/core/global-flags.js +1 -1
  111. package/lib/core/governedSession.js +136 -8
  112. package/lib/core/inboxResolution.js +3 -2
  113. package/lib/core/liveSessionsReport.js +1 -1
  114. package/lib/core/loginGuard.js +73 -0
  115. package/lib/core/loopGlyph.js +97 -0
  116. package/lib/core/offload.js +73 -7
  117. package/lib/core/org-override.js +5 -1
  118. package/lib/core/output.js +8 -8
  119. package/lib/core/profileTrust.js +182 -0
  120. package/lib/core/quoxglyph.js +22 -1
  121. package/lib/core/router.js +14 -0
  122. package/lib/core/sessionHeartbeat.js +12 -8
  123. package/lib/core/shortcut.js +83 -0
  124. package/lib/core/stepUpPrompt.js +99 -0
  125. package/lib/core/version.js +1 -1
  126. package/lib/core/vm.js +5 -0
  127. package/lib/core/walkthrough.js +9 -5
  128. package/lib/dragon/companionView.js +34 -0
  129. package/lib/dragon/frames.js +118 -0
  130. package/lib/dragon/jobs.js +123 -0
  131. package/lib/dragon/renderWorker.js +15 -0
  132. package/lib/dragon/select.js +72 -0
  133. package/lib/evidence/bundleExport.js +47 -0
  134. package/lib/evidence/bundleVerify.js +77 -0
  135. package/lib/quoxon/EXTENSIONS.md +146 -0
  136. package/lib/quoxon/api.js +364 -27
  137. package/lib/quoxon/app.js +1629 -183
  138. package/lib/quoxon/approvalBatch.js +117 -0
  139. package/lib/quoxon/approvalFacts.js +64 -8
  140. package/lib/quoxon/approvalSentence.js +48 -0
  141. package/lib/quoxon/approvalsView.js +124 -7
  142. package/lib/quoxon/autoOpen.js +94 -0
  143. package/lib/quoxon/backgroundWork.js +59 -0
  144. package/lib/quoxon/bang.js +1 -1
  145. package/lib/quoxon/capabilities.js +7 -2
  146. package/lib/quoxon/commandShort.js +22 -3
  147. package/lib/quoxon/commands.js +110 -15
  148. package/lib/quoxon/connect.js +5 -3
  149. package/lib/quoxon/countdownLine.js +10 -1
  150. package/lib/quoxon/deck.js +30 -0
  151. package/lib/quoxon/deckLazy.js +58 -0
  152. package/lib/quoxon/demo.autoplay.js +1 -1
  153. package/lib/quoxon/demo.js +3 -1
  154. package/lib/quoxon/destructive.js +35 -0
  155. package/lib/quoxon/dividerAocl.js +4 -3
  156. package/lib/quoxon/dividerRule.js +3 -3
  157. package/lib/quoxon/dnd.js +71 -0
  158. package/lib/quoxon/dragonCompanion.js +285 -0
  159. package/lib/quoxon/dragonModes.js +1 -0
  160. package/lib/quoxon/dragonMoments.js +160 -0
  161. package/lib/quoxon/dragonReads.js +68 -0
  162. package/lib/quoxon/dragonView.js +2 -2
  163. package/lib/quoxon/echo.js +1 -0
  164. package/lib/quoxon/events.js +67 -10
  165. package/lib/quoxon/extensions.js +636 -0
  166. package/lib/quoxon/gripeQueue.js +135 -0
  167. package/lib/quoxon/inboxView.js +182 -0
  168. package/lib/quoxon/installCli.js +79 -0
  169. package/lib/quoxon/keys.js +4 -4
  170. package/lib/quoxon/loopCard.js +460 -0
  171. package/lib/quoxon/loopCards.js +189 -0
  172. package/lib/quoxon/loopStream.js +98 -0
  173. package/lib/quoxon/mailView.js +71 -0
  174. package/lib/quoxon/main.js +29 -5
  175. package/lib/quoxon/markdown.js +1 -1
  176. package/lib/quoxon/model.js +15 -0
  177. package/lib/quoxon/mouse.js +12 -1
  178. package/lib/quoxon/neverAnswered.js +20 -0
  179. package/lib/quoxon/objectiveOffer.js +82 -0
  180. package/lib/quoxon/objects/away.js +43 -5
  181. package/lib/quoxon/objects/index.js +22 -4
  182. package/lib/quoxon/objects/model.js +33 -0
  183. package/lib/quoxon/objects/proof.js +12 -4
  184. package/lib/quoxon/onboarding/connectServer.js +162 -0
  185. package/lib/quoxon/onboarding/copy.js +6 -0
  186. package/lib/quoxon/onboarding/index.js +5 -43
  187. package/lib/quoxon/onboarding/probe.js +20 -1
  188. package/lib/quoxon/onboarding/shortcutOffer.js +28 -0
  189. package/lib/quoxon/onboarding/view.js +16 -3
  190. package/lib/quoxon/orgSwitch.js +49 -1
  191. package/lib/quoxon/planSteps.js +51 -0
  192. package/lib/quoxon/print.js +8 -3
  193. package/lib/quoxon/profileSwitch.js +7 -0
  194. package/lib/quoxon/promptHistory.js +3 -1
  195. package/lib/quoxon/qdragonFly.js +7 -6
  196. package/lib/quoxon/qmotion.js +17 -0
  197. package/lib/quoxon/render.js +187 -41
  198. package/lib/quoxon/roster.js +36 -17
  199. package/lib/quoxon/rosterSessions.js +44 -0
  200. package/lib/quoxon/rows.js +244 -1
  201. package/lib/quoxon/screenContext.js +1 -1
  202. package/lib/quoxon/secretShape.js +86 -0
  203. package/lib/quoxon/serverChip.js +42 -0
  204. package/lib/quoxon/session.js +10 -0
  205. package/lib/quoxon/slash/agents.js +2 -1
  206. package/lib/quoxon/slash/argSources.js +9 -2
  207. package/lib/quoxon/slash/args.js +4 -2
  208. package/lib/quoxon/slash/broadcast.js +18 -5
  209. package/lib/quoxon/slash/broadcastRun.js +13 -2
  210. package/lib/quoxon/slash/cliCommand.js +42 -1
  211. package/lib/quoxon/slash/cliLeaves.json +139 -2
  212. package/lib/quoxon/slash/evidenceCmd.js +34 -0
  213. package/lib/quoxon/slash/hatWalk.js +1 -1
  214. package/lib/quoxon/slash/palette.js +23 -2
  215. package/lib/quoxon/slash/paletteController.js +10 -4
  216. package/lib/quoxon/slash/qpeCmd.js +35 -0
  217. package/lib/quoxon/slash/restart.js +31 -4
  218. package/lib/quoxon/slash/resume.js +18 -1
  219. package/lib/quoxon/slash/stepUp.js +5 -5
  220. package/lib/quoxon/slash/stepUpCmds.js +74 -1
  221. package/lib/quoxon/slash/todoCmd.js +122 -0
  222. package/lib/quoxon/slashSpinner.js +35 -0
  223. package/lib/quoxon/slotAttach.js +8 -0
  224. package/lib/quoxon/slotChat.js +17 -0
  225. package/lib/quoxon/slotFormat.js +1 -1
  226. package/lib/quoxon/slotOutbox.js +113 -0
  227. package/lib/quoxon/suggestCommand.js +34 -3
  228. package/lib/quoxon/term.js +25 -1
  229. package/lib/quoxon/toolCards.js +30 -0
  230. package/lib/quoxon/transcript.js +2 -1
  231. package/lib/quoxon/welcome.js +1 -1
  232. package/lib/quoxon/whatsnew.js +1 -1
  233. package/lib/quoxon/whatsnew.jsonl +81 -0
  234. package/lib/quoxon/windowOrg.js +11 -1
  235. package/lib/server/devServers.js +137 -0
  236. package/lib/server/installFlow.js +168 -0
  237. package/lib/server/status.js +15 -1
  238. package/lib/toolOutputShape.js +55 -0
  239. package/package.json +4 -2
  240. package/vendor/dragonlab/SOURCE +4 -0
  241. package/vendor/dragonlab/design/cipher/v1/cipher-master.svg +46 -0
  242. package/vendor/dragonlab/design/cipher/v2/character.mjs +49 -0
  243. package/vendor/dragonlab/design/cipher/v2/review.mjs +33 -0
  244. package/vendor/dragonlab/design/cipher/v2/watch.mjs +56 -0
  245. package/vendor/dragonlab/design/drago/v1/character.mjs +19 -0
  246. package/vendor/dragonlab/design/drago/v1/drago-master.svg +56 -0
  247. package/vendor/dragonlab/design/drago/v2/character.mjs +87 -0
  248. package/vendor/dragonlab/design/drago/v4/character.mjs +67 -0
  249. package/vendor/dragonlab/design/drago/v4/profile.mjs +35 -0
  250. package/vendor/dragonlab/design/drago/v4/review.mjs +33 -0
  251. package/vendor/dragonlab/design/drago/v4/watch.mjs +56 -0
  252. package/vendor/dragonlab/design/hyperion/v1/hyperion-master.svg +56 -0
  253. package/vendor/dragonlab/design/hyperion/v2/character.mjs +79 -0
  254. package/vendor/dragonlab/design/hyperion/v2/review.mjs +33 -0
  255. package/vendor/dragonlab/design/hyperion/v2/watch.mjs +56 -0
  256. package/vendor/dragonlab/design/lucid/recovery/arrival.mjs +22 -0
  257. package/vendor/dragonlab/design/lucid/recovery/character.mjs +106 -0
  258. package/vendor/dragonlab/design/lucid/recovery/layout.mjs +50 -0
  259. package/vendor/dragonlab/design/lucid/recovery/lucid-master.svg +78 -0
  260. package/vendor/dragonlab/design/lucid/recovery/motion.mjs +42 -0
  261. package/vendor/dragonlab/design/lucid/recovery/render.mjs +121 -0
  262. package/vendor/dragonlab/design/lucid/recovery/review.mjs +68 -0
  263. package/vendor/dragonlab/design/lucid/recovery/verify.mjs +84 -0
  264. package/vendor/dragonlab/design/lucid/recovery/watch.mjs +56 -0
  265. package/vendor/dragonlab/design/quoxperticus/v2/quoxperticus-master.svg +59 -0
  266. package/vendor/dragonlab/design/quoxperticus/v3/character.mjs +43 -0
  267. package/vendor/dragonlab/design/quoxperticus/v4/character.mjs +57 -0
  268. package/vendor/dragonlab/design/quoxperticus/v4/review.mjs +35 -0
  269. package/vendor/dragonlab/design/quoxperticus/v4/watch.mjs +56 -0
  270. package/vendor/dragonlab/design/seer/v1/seer-master.svg +53 -0
  271. package/vendor/dragonlab/design/seer/v2/character.mjs +88 -0
  272. package/vendor/dragonlab/design/seer/v3/character.mjs +33 -0
  273. package/vendor/dragonlab/design/seer/v4/character.mjs +66 -0
  274. package/vendor/dragonlab/design/seer/v4/review.mjs +33 -0
  275. package/vendor/dragonlab/design/seer/v4/watch.mjs +56 -0
  276. package/vendor/dragonlab/design/stage3/companion-review.mjs +28 -0
  277. package/vendor/dragonlab/design/stage3/sentinel-art.js +39 -0
  278. package/vendor/dragonlab/design/stage3/sentinel-braille.js +216 -0
  279. package/vendor/dragonlab/design/stage3/sentinel-character.js +257 -0
  280. package/vendor/dragonlab/design/stage3/sentinel-icon.js +103 -0
  281. package/vendor/dragonlab/design/stage3/sentinel-warden-eyes.js +95 -0
  282. package/vendor/dragonlab/design/stage3/svg-coverage.js +148 -0
  283. package/vendor/dragonlab/design/vulcan/v1/character.mjs +34 -0
  284. package/vendor/dragonlab/design/vulcan/v1/vulcan-master.svg +57 -0
  285. package/vendor/dragonlab/design/vulcan/v2/character.mjs +39 -0
  286. package/vendor/dragonlab/design/vulcan/v3/character.mjs +41 -0
  287. package/vendor/dragonlab/design/vulcan/v3/review.mjs +35 -0
  288. package/vendor/dragonlab/design/vulcan/v3/watch.mjs +56 -0
  289. package/vendor/dragonlab/factory/svg-rig-contour.mjs +64 -0
  290. package/vendor/dragonlab/factory/svg-rig.mjs +63 -0
  291. package/vendor/dragonlab/integration/companions-v1/characters.mjs +108 -0
  292. package/vendor/dragonlab/integration/companions-v1/cipher.mjs +2 -0
  293. package/vendor/dragonlab/integration/companions-v1/companion.mjs +63 -0
  294. package/vendor/dragonlab/integration/companions-v1/drago.mjs +2 -0
  295. package/vendor/dragonlab/integration/companions-v1/hyperion.mjs +2 -0
  296. package/vendor/dragonlab/integration/companions-v1/lucid.mjs +2 -0
  297. package/vendor/dragonlab/integration/companions-v1/quoxperticus.mjs +2 -0
  298. package/vendor/dragonlab/integration/companions-v1/seer.mjs +2 -0
  299. package/vendor/dragonlab/integration/companions-v1/sentinel.mjs +2 -0
  300. package/vendor/dragonlab/integration/companions-v1/view.mjs +28 -0
  301. package/vendor/dragonlab/integration/companions-v1/vulcan.mjs +2 -0
  302. package/vendor/dragonlab/integration/hyperion-v3/activity.mjs +76 -0
  303. package/vendor/dragonlab/lib/power-effects.js +294 -0
  304. package/vendor/dragonlab/lib/power-registry.js +74 -0
  305. package/vendor/dragonlab/vendor/quox-dragon/qart.js +348 -0
  306. package/vendor/dragonlab/vendor/quox-dragon/qeyes.js +213 -0
@@ -27,12 +27,18 @@ import { QuoxError } from '../core/errors.js';
27
27
  import { resolveOrgId } from './helpers.js';
28
28
  import { resolveMyAttach } from './agentAttach.js';
29
29
  import { hasExplicitProfile } from '../core/actingAs.js';
30
+ import { line as glyphLine, printEvent } from '../core/quoxglyph.js';
31
+ import { syncMemoryFiles, expandPaths, defaultMemoryDir } from './memorySync.js';
32
+ import { announce, announceFailure } from '../core/announce.js';
30
33
 
31
34
  // QUOXIDENTITY P9: which agent this session acts as is resolved session-scoped
32
35
  // (QUOX_T subtree / QUOX_ATTACH_AGENT override / single-attach fallback), so
33
36
  // two sessions on one box no longer collide. See agentAttach.js resolveMyAttach.
34
37
  const detectSoleAttachLease = resolveMyAttach;
35
38
 
39
+ // Short, single-line form of a memory's text for the announce line.
40
+ const clip = (t) => { const x = String(t || '').replace(/\s+/g, ' ').trim(); return x.length > 28 ? x.slice(0, 27) + '…' : x; };
41
+
36
42
  /**
37
43
  * Call a governed collector memory tool (POST /api/v1/tools/execute).
38
44
  * Lease-aware: when exactly one attach lease is active, the call is made
@@ -161,6 +167,7 @@ export function registerMemoryCommands(program, ctx) {
161
167
  auth.requireAuth();
162
168
  const ep = resolveEndpoint('memory.reinstate', { id });
163
169
  const data = await client.post(ep.path, { service: ep.service, body: {} });
170
+ announce('memory', `reinstated ${data.reinstated?.id || id}`);
164
171
  if (program.opts().output === 'json') { console.log(JSON.stringify(data, null, 2)); return; }
165
172
  printSuccess(`Reinstated ${data.reinstated?.id}; retired ${data.retired?.id} (marked disputed, Dream will not re-apply it)`);
166
173
  });
@@ -180,10 +187,56 @@ export function registerMemoryCommands(program, ctx) {
180
187
  };
181
188
  if (options.tags) body.tags = options.tags.split(',').map(t => t.trim());
182
189
  if (options.agent) body.agent_id = options.agent;
183
- await client.post(resolveEndpoint('memory.create').path, { service: resolveEndpoint('memory.create').service, body });
190
+ const label = `${options.type} "${clip(options.content)}"`;
191
+ let data;
192
+ try {
193
+ data = await client.post(resolveEndpoint('memory.create').path, { service: resolveEndpoint('memory.create').service, body });
194
+ } catch (err) { announceFailure('memory', `save ${label}`, err); throw err; }
195
+ announce('memory', `saved ${label}`, { result: data?.id || data?.memory?.id });
184
196
  printSuccess('Memory saved.');
185
197
  });
186
198
 
199
+ // Owner order 2026-10-29: do not replace Claude's file memory, ALSO save it to Quox memory.
200
+ // Additive and idempotent (see memorySync.js). One QuoxGlyph line per file saved or updated.
201
+ memory
202
+ .command('sync')
203
+ .description("Also save Claude Code's file memory (default: this project's memory dir) into Quox memory. Idempotent, additive, secrets refused")
204
+ .argument('[paths...]', 'memory files or directories (default: ~/.claude/projects/<this cwd>/memory)')
205
+ .option('--dry-run', 'show what would be saved without writing')
206
+ .action(async (paths, options) => {
207
+ auth.requireAuth();
208
+ const json = program.opts().output === 'json';
209
+ const targets = paths && paths.length ? paths : [defaultMemoryDir()];
210
+ const files = expandPaths(targets);
211
+ const ep = resolveEndpoint('memory.create');
212
+ const orgId = auth.getOrgId?.(program.opts().org) || 'default';
213
+ const results = await syncMemoryFiles({ client, orgId, files, dryRun: !!options.dryRun, memoryEp: ep });
214
+ const count = (a) => results.filter((r) => r.action === a).length;
215
+ const summary = {
216
+ created: count('created'), updated: count('updated'), unchanged: count('unchanged'),
217
+ refused: count('refused'), errors: count('error') + count('missing'),
218
+ would_create: count('would-create'), would_update: count('would-update'),
219
+ };
220
+ if (json) {
221
+ console.log(JSON.stringify({ dry_run: !!options.dryRun, summary, results }, null, 2));
222
+ } else {
223
+ const q = (r) => `"${r.name}"`;
224
+ for (const r of results) {
225
+ if (r.action === 'created') printEvent(glyphLine('memory', `also saved to Quox ${q(r)}`, { status: 'ok' }));
226
+ else if (r.action === 'updated') printEvent(glyphLine('memory', `updated in Quox ${q(r)}`, { status: 'ok' }));
227
+ else if (r.action === 'would-create') printEvent(glyphLine('memory', `would save to Quox ${q(r)}`, { status: 'busy' }));
228
+ else if (r.action === 'would-update') printEvent(glyphLine('memory', `would update in Quox ${q(r)}`, { status: 'busy' }));
229
+ else if (r.action === 'refused') printEvent(glyphLine('memory', `refused ${r.file.split('/').pop()}`, { result: `secret shape: ${r.kinds.join(', ')}`, status: 'warn' }));
230
+ else if (r.action === 'error' || r.action === 'missing') printEvent(glyphLine('memory', `sync ${r.file.split('/').pop()}`, { result: r.action === 'missing' ? 'file not found' : (r.error || 'failed').slice(0, 40), status: 'fail' }));
231
+ }
232
+ const bits = options.dryRun
233
+ ? `${summary.would_create} to save, ${summary.would_update} to update, ${summary.unchanged} unchanged`
234
+ : `${summary.created} saved, ${summary.updated} updated, ${summary.unchanged} unchanged`;
235
+ console.log(`Memory sync: ${bits}${summary.refused ? `, ${summary.refused} refused (secret shapes)` : ''}${summary.errors ? `, ${summary.errors} failed` : ''}.`);
236
+ }
237
+ if (summary.errors) process.exitCode = 1;
238
+ });
239
+
187
240
  memory
188
241
  .command('update')
189
242
  .description('Update a memory')
@@ -195,7 +248,10 @@ export function registerMemoryCommands(program, ctx) {
195
248
  const body = {};
196
249
  if (options.content) body.content = options.content;
197
250
  if (options.tags) body.tags = options.tags.split(',').map(t => t.trim());
198
- await client.patch(resolveEndpoint('memory.update', { id }).path, { service: resolveEndpoint('memory.update', { id }).service, body });
251
+ try {
252
+ await client.patch(resolveEndpoint('memory.update', { id }).path, { service: resolveEndpoint('memory.update', { id }).service, body });
253
+ } catch (err) { announceFailure('memory', `update ${id}`, err); throw err; }
254
+ announce('memory', `updated ${id}`, { result: options.content ? `"${clip(options.content)}"` : undefined });
199
255
  printSuccess('Memory updated.');
200
256
  });
201
257
 
@@ -208,7 +264,10 @@ export function registerMemoryCommands(program, ctx) {
208
264
  auth.requireAuth();
209
265
  const query = {};
210
266
  if (options.hard) query.hard = 'true';
211
- await client.delete(resolveEndpoint('memory.delete', { id }).path, { service: resolveEndpoint('memory.delete', { id }).service, query });
267
+ try {
268
+ await client.delete(resolveEndpoint('memory.delete', { id }).path, { service: resolveEndpoint('memory.delete', { id }).service, query });
269
+ } catch (err) { announceFailure('memory', `delete ${id}`, err); throw err; }
270
+ announce('memory', `deleted ${id}`, { result: options.hard ? 'permanent' : 'soft delete' });
212
271
  printSuccess('Memory deleted.');
213
272
  });
214
273
 
@@ -226,7 +285,10 @@ export function registerMemoryCommands(program, ctx) {
226
285
  const ep = resolveEndpoint('memory.delete', { id });
227
286
  const query = {};
228
287
  if (options.permanent) query.hard = 'true';
229
- await client.delete(ep.path, { service: ep.service, query });
288
+ try {
289
+ await client.delete(ep.path, { service: ep.service, query });
290
+ } catch (err) { announceFailure('memory', `forget ${id}`, err); throw err; }
291
+ announce('memory', `forgot ${id}`, { result: options.permanent ? 'erased for good' : 'soft delete' });
230
292
  printSuccess(options.permanent ? `Permanently erased memory ${id}.` : `Forgot memory ${id}.`);
231
293
  });
232
294
 
@@ -333,6 +395,7 @@ export function registerMemoryCommands(program, ctx) {
333
395
  auth.requireAuth();
334
396
  const ep = resolveEndpoint('memory.pin', { id });
335
397
  const data = await client.post(ep.path, { service: ep.service, body: {} });
398
+ announce('memory', `pinned ${id}`, { result: 'core set' });
336
399
  if (program.opts().output === 'json') { console.log(JSON.stringify(data, null, 2)); return; }
337
400
  printSuccess(`Pinned ${id} into the core set.`);
338
401
  });
@@ -345,6 +408,7 @@ export function registerMemoryCommands(program, ctx) {
345
408
  auth.requireAuth();
346
409
  const ep = resolveEndpoint('memory.unpin', { id });
347
410
  const data = await client.delete(ep.path, { service: ep.service });
411
+ announce('memory', `unpinned ${id}`, { result: 'out of core set' });
348
412
  if (program.opts().output === 'json') { console.log(JSON.stringify(data, null, 2)); return; }
349
413
  printSuccess(`Unpinned ${id}: it stays a memory, but is no longer in the core set.`);
350
414
  });
@@ -0,0 +1,158 @@
1
+ /**
2
+ * `quox memory sync`: also save agent file memory (Claude Code's
3
+ * ~/.claude/projects/<slug>/memory/*.md) into Quox memory. ADDITIVE: the Claude
4
+ * files are only read, never written, and a removed file never deletes a Quox
5
+ * memory. Idempotent: each file has a stable source key
6
+ * (claude-memory:<slug>/<file>) and a content hash; unchanged files are skipped,
7
+ * changed files PATCH the same Quox memory. Files containing secret shapes are
8
+ * refused (named, never echoed).
9
+ */
10
+ import { createHash } from 'node:crypto';
11
+ import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync, renameSync } from 'node:fs';
12
+ import { basename, dirname, join, resolve } from 'node:path';
13
+ import { homedir } from 'node:os';
14
+ import { secretKinds } from '../quoxon/secretShape.js';
15
+
16
+ const MAX_CONTENT = 20000;
17
+
18
+ /** Claude memory type -> closest Quox memory type (the API accepts fact, preference, decision, observation, entity). */
19
+ export function quoxTypeFor(claudeType) {
20
+ switch (String(claudeType || '').toLowerCase()) {
21
+ case 'user': return 'preference';
22
+ case 'feedback': return 'decision';
23
+ case 'project': return 'fact';
24
+ case 'reference': return 'fact';
25
+ default: return 'observation';
26
+ }
27
+ }
28
+
29
+ const unquote = (v) => String(v ?? '').trim().replace(/^(["'])(.*)\1$/, '$2');
30
+
31
+ /** -> { name, description, type, body } from a Claude memory file (frontmatter optional). */
32
+ export function parseMemoryFile(text, fallbackName = '') {
33
+ const src = String(text ?? '').replace(/\r\n/g, '\n');
34
+ let front = '';
35
+ let body = src;
36
+ const m = src.match(/^---\n([\s\S]*?)\n---\n?/);
37
+ if (m) { front = m[1]; body = src.slice(m[0].length); }
38
+ const out = { name: '', description: '', type: '' };
39
+ let inMeta = false;
40
+ for (const raw of front.split('\n')) {
41
+ const top = raw.match(/^([A-Za-z_]+):\s*(.*)$/);
42
+ if (top) {
43
+ inMeta = top[1] === 'metadata';
44
+ if (top[1] === 'name') out.name = unquote(top[2]);
45
+ else if (top[1] === 'description') out.description = unquote(top[2]);
46
+ else if (top[1] === 'type') out.type = unquote(top[2]);
47
+ continue;
48
+ }
49
+ const nested = raw.match(/^\s+type:\s*(.*)$/);
50
+ if (inMeta && nested) out.type = unquote(nested[1]);
51
+ }
52
+ return { name: out.name || fallbackName, description: out.description, type: out.type, body: body.trim() };
53
+ }
54
+
55
+ /** Stable key: claude-memory:<project slug>/<file>. */
56
+ export function sourceKeyFor(file) {
57
+ const abs = resolve(file);
58
+ const parts = abs.split('/');
59
+ const mi = parts.lastIndexOf('memory');
60
+ const slug = mi > 0 ? parts[mi - 1] : createHash('sha256').update(dirname(abs)).digest('hex').slice(0, 8);
61
+ const rel = mi >= 0 ? parts.slice(mi + 1).join('/') : basename(abs);
62
+ return `claude-memory:${slug}/${rel}`;
63
+ }
64
+
65
+ /** Default memory dir for a cwd: ~/.claude/projects/<cwd with non-alnum -> '-'>/memory. */
66
+ export function defaultMemoryDir(cwd = process.cwd()) {
67
+ return join(homedir(), '.claude', 'projects', cwd.replace(/[^A-Za-z0-9]/g, '-'), 'memory');
68
+ }
69
+
70
+ /** Expand dirs to their *.md files; MEMORY.md (the index) is skipped. */
71
+ export function expandPaths(paths) {
72
+ const files = [];
73
+ for (const p of paths) {
74
+ const abs = resolve(p);
75
+ if (!existsSync(abs)) { files.push({ file: abs, missing: true }); continue; }
76
+ if (statSync(abs).isDirectory()) {
77
+ for (const f of readdirSync(abs).sort()) if (f.endsWith('.md')) files.push({ file: join(abs, f) });
78
+ } else files.push({ file: abs });
79
+ }
80
+ return files.filter((f) => f.missing || basename(f.file) !== 'MEMORY.md');
81
+ }
82
+
83
+ const statePathDefault = () => join(homedir(), '.quox', 'memory-sync-state.json');
84
+ function loadState(p) { try { return JSON.parse(readFileSync(p, 'utf8')) || {}; } catch { return {}; } }
85
+ function saveState(p, state) {
86
+ mkdirSync(dirname(p), { recursive: true });
87
+ const tmp = `${p}.${process.pid}.tmp`;
88
+ writeFileSync(tmp, JSON.stringify(state, null, 2));
89
+ renameSync(tmp, p);
90
+ }
91
+
92
+ /**
93
+ * Sync files into Quox memory. client = the CLI http client; post/patch/get go to
94
+ * the memory service paths. Returns [{ file, key, name, action, id?, kinds?, error? }]
95
+ * where action is created | updated | unchanged | refused | missing | error | would-create | would-update.
96
+ */
97
+ export async function syncMemoryFiles({ client, orgId = 'default', files, dryRun = false, statePath = statePathDefault(), memoryEp }) {
98
+ const state = loadState(statePath);
99
+ const mine = (state[orgId] ||= {});
100
+ let remote; // lazily loaded recovery index: source_key -> {id, hash}
101
+ const recover = async (key) => {
102
+ if (!remote) {
103
+ remote = {};
104
+ try {
105
+ const data = await client.get(memoryEp.path, { service: memoryEp.service, query: { limit: 1000 } });
106
+ for (const m of (data?.memories || [])) {
107
+ const sk = m?.metadata?.source_key;
108
+ if (sk) remote[sk] = { id: m.id, hash: m.metadata.content_hash };
109
+ }
110
+ } catch { /* recovery is best effort; worst case a first-time key creates */ }
111
+ }
112
+ return remote[key];
113
+ };
114
+
115
+ const results = [];
116
+ for (const entry of files) {
117
+ const { file } = entry;
118
+ const key = sourceKeyFor(file);
119
+ if (entry.missing) { results.push({ file, key, name: basename(file), action: 'missing' }); continue; }
120
+ let text;
121
+ try { text = readFileSync(file, 'utf8'); } catch (e) { results.push({ file, key, name: basename(file), action: 'error', error: e.message }); continue; }
122
+ const parsed = parseMemoryFile(text, basename(file, '.md'));
123
+ const kinds = secretKinds(text, { precise: true });
124
+ if (kinds.length) { results.push({ file, key, name: parsed.name, action: 'refused', rule: kinds[0], kinds }); continue; }
125
+ let content = [parsed.name, parsed.description].filter(Boolean).join(': ');
126
+ content = (content ? content + '\n\n' : '') + parsed.body;
127
+ content = content.slice(0, MAX_CONTENT);
128
+ const hash = createHash('sha256').update(content + '\0' + parsed.type).digest('hex');
129
+ const known = mine[key] || await recover(key);
130
+ if (known && known.hash === hash) { mine[key] = { id: known.id, hash }; results.push({ file, key, name: parsed.name, action: 'unchanged', id: known.id }); continue; }
131
+ const metadata = { source: 'claude-memory', source_key: key, content_hash: hash, source_file: basename(file), claude_type: parsed.type || null };
132
+ const tags = ['claude-memory', ...(parsed.type ? [`claude-memory:${parsed.type}`] : [])];
133
+ if (dryRun) { results.push({ file, key, name: parsed.name, action: known ? 'would-update' : 'would-create', id: known?.id }); continue; }
134
+ try {
135
+ let id = known?.id;
136
+ let action = 'created';
137
+ if (id) {
138
+ try {
139
+ await client.patch(`${memoryEp.path}/${id}`, { service: memoryEp.service, body: { content, tags, metadata } });
140
+ action = 'updated';
141
+ } catch (e) {
142
+ if (e?.status !== 404) throw e;
143
+ id = null; // the Quox memory was removed; make a fresh one
144
+ }
145
+ }
146
+ if (!id) {
147
+ const created = await client.post(memoryEp.path, { service: memoryEp.service, body: { type: quoxTypeFor(parsed.type), content, tags, metadata } });
148
+ id = created?.id || created?.memory?.id || null;
149
+ }
150
+ if (id) mine[key] = { id, hash };
151
+ results.push({ file, key, name: parsed.name, action, id });
152
+ } catch (e) {
153
+ results.push({ file, key, name: parsed.name, action: 'error', error: e?.message || String(e) });
154
+ }
155
+ }
156
+ if (!dryRun) { try { saveState(statePath, state); } catch { /* state is a cache; recovery covers it */ } }
157
+ return results;
158
+ }
@@ -25,6 +25,7 @@
25
25
  * None of the three are org-scoped (single shared config/cache).
26
26
  */
27
27
 
28
+ import { attachWazuhVerbs } from './wazuh.js';
28
29
  import { formatOutput, formatDetail, printSuccess, printWarning, printInfo } from '../core/output.js';
29
30
  import { parseJSON } from './helpers.js';
30
31
  import { resolveEndpoint } from '../api/endpoints.js';
@@ -262,147 +263,48 @@ export function registerMonCommands(program, ctx) {
262
263
  .command('wazuh')
263
264
  .description('Wazuh security monitoring: manager status, agents, alerts, vulnerabilities');
264
265
 
265
- wazuh
266
- .command('status')
267
- .description('Show Wazuh manager status')
268
- .action(async () => {
269
- auth.requireAuth();
270
- const ep = resolveEndpoint('mon.wazuh.manager-status.get');
271
- const data = await client.get(ep.path, { service: ep.service });
272
- formatDetail(data.data || data, { format: program.opts().output, title: 'Wazuh Manager Status' });
273
- });
274
-
275
- wazuh
276
- .command('agents')
277
- .description('List Wazuh agents')
278
- .option('--status <status>', 'filter by agent status')
279
- .option('--limit <n>', 'max results')
280
- .option('--offset <n>', 'result offset')
281
- .option('--q <query>', 'query string passed through to the Wazuh API')
282
- .action(async (options) => {
283
- auth.requireAuth();
284
- const ep = resolveEndpoint('mon.wazuh.agents.list');
285
- const query = {};
286
- if (options.status) query.status = options.status;
287
- if (options.limit) query.limit = options.limit;
288
- if (options.offset) query.offset = options.offset;
289
- if (options.q) query.q = options.q;
290
- const data = await client.get(ep.path, { service: ep.service, query });
291
- const agents = data.data?.data?.affected_items || data.data?.affected_items || data.data || [];
292
- formatOutput({
293
- columns: ['id', 'name', 'ip', 'status', 'version'],
294
- headers: ['ID', 'Name', 'IP', 'Status', 'Version'],
295
- rows: Array.isArray(agents) ? agents : [],
296
- }, { format: program.opts().output, emptyMessage: 'No Wazuh agents found.' });
297
- });
298
-
299
- wazuh
300
- .command('alerts')
301
- .description('Query recent Wazuh alerts')
302
- .option('--q <query>', 'search text (rule description / agent name / rule groups)')
303
- .option('--limit <n>', 'max results (default 50, capped at 500)')
304
- .action(async (options) => {
305
- auth.requireAuth();
306
- const ep = resolveEndpoint('mon.wazuh.alerts.list');
307
- const query = {};
308
- if (options.q) query.q = options.q;
309
- if (options.limit) query.limit = options.limit;
310
- const data = await client.get(ep.path, { service: ep.service, query });
311
- formatOutput({
312
- columns: ['timestamp', 'rule', 'agent', 'level'],
313
- headers: ['Time', 'Rule', 'Agent', 'Level'],
314
- rows: (data.data || []).map(a => ({
315
- timestamp: a.timestamp,
316
- rule: a.rule?.description || '',
317
- agent: a.agent?.name || '',
318
- level: a.rule?.level ?? '',
319
- })),
320
- }, { format: program.opts().output, emptyMessage: 'No alerts found.' });
321
- });
322
-
323
- wazuh
324
- .command('vulns')
325
- .description('Query current vulnerability states')
326
- .option('--agent-id <id>', 'filter by agent ID')
327
- .option('--agent-name <name>', 'filter by agent name')
328
- .option('--severity <severity>', 'filter by severity')
329
- .option('--limit <n>', 'max results (default 100, capped at 1000)')
330
- .action(async (options) => {
331
- auth.requireAuth();
332
- const ep = resolveEndpoint('mon.wazuh.vulnerabilities.list');
333
- const query = {};
334
- if (options.agentId) query.agent_id = options.agentId;
335
- if (options.agentName) query.agent_name = options.agentName;
336
- if (options.severity) query.severity = options.severity;
337
- if (options.limit) query.limit = options.limit;
338
- const data = await client.get(ep.path, { service: ep.service, query });
339
- formatOutput({
340
- columns: ['agent', 'cve', 'severity', 'score'],
341
- headers: ['Agent', 'CVE', 'Severity', 'Score'],
342
- rows: (data.data || []).map(v => ({
343
- agent: v.agent?.name || '',
344
- cve: v.vulnerability?.id || '',
345
- severity: v.vulnerability?.severity || '',
346
- score: v.vulnerability?.score?.base ?? '',
347
- })),
348
- }, { format: program.opts().output, emptyMessage: 'No vulnerabilities found.' });
349
- });
350
-
351
- wazuh
352
- .command('deploy-history')
353
- .description('Show Wazuh agent deploy history')
354
- .option('--mode <mode>', 'latest|all', 'latest')
355
- .option('--limit <n>', 'max results (mode=all only)', '200')
356
- .action(async (options) => {
357
- auth.requireAuth();
358
- const ep = resolveEndpoint('mon.wazuh.deploy-history.list');
359
- const data = await client.get(ep.path, { service: ep.service, query: { mode: options.mode, limit: options.limit } });
360
- formatOutput({
361
- // fleet #385 class sweep: the route returns
362
- // { hostname, ip, ssh_user, success, message, deployed_at }, but this
363
- // declared hostIp/status/deployedAt — three of four columns rendered
364
- // as dashes with no error. `success` is a boolean, so it's mapped to
365
- // a readable status rather than printed raw.
366
- columns: ['hostname', 'ip', 'status', 'deployed_at'],
367
- headers: ['Host', 'IP', 'Status', 'Deployed'],
368
- rows: (data.deploys || []).map(d => ({
369
- ...d,
370
- status: d.success ? 'ok' : (d.message ? `failed: ${String(d.message).slice(0, 40)}` : 'failed'),
371
- })),
372
- }, { format: program.opts().output, emptyMessage: 'No deploy history found.' });
373
- });
266
+ // Read verbs (status, agents, alerts, vulns, deploy-history) are the top-level `quox wazuh` implementation: one code path.
267
+ // WAZ-10: the three legacy write verbs keep their flags but run through the SAME governed helpers as
268
+ // `quox wazuh deploy|deploy-bulk|active-response` (wazuh_* tools, held for a human approval). The old
269
+ // /api/v1/wazuh write routes are retired (410).
270
+ const { guard, writeTool, deployOne, WazFail } = attachWazuhVerbs(wazuh, program, { client, auth }, { writes: false });
271
+ const padId = (i) => (/^\d+$/.test(i) ? i.padStart(3, '0') : i);
272
+ const refuseTargetOverride = (verb, options) => {
273
+ if (options.wazuhServer || options.bastionHost) {
274
+ throw new WazFail(`quox mon wazuh ${verb}: --wazuh-server and --bastion-host are not accepted on the governed path, which always uses the org's configured Wazuh manager and bastion. Drop them, or change the Wazuh plugin configuration. Governed verb: quox wazuh ${verb === 'deploy-agent' ? 'deploy' : verb}`);
275
+ }
276
+ };
374
277
 
375
278
  wazuh
376
279
  .command('deploy-agent')
377
- .description('Queue a Wazuh agent deploy for one host (HITL-gated — requires HITL Inbox approval)')
280
+ .description('Install the Wazuh agent on one host (governed wazuh_deploy_agent, held for human approval; same as `quox wazuh deploy`)')
378
281
  .requiredOption('--hostname <hostname>', 'target hostname')
379
282
  .requiredOption('--host-ip <ip>', 'target host IP')
380
- .option('--wazuh-server <ip>', 'Wazuh manager IP (defaults to server config)')
381
- .option('--bastion-host <ip>', 'bastion host for the SSH hop')
283
+ .option('--wazuh-server <ip>', 'not accepted: the governed path uses the org\'s configured Wazuh manager')
284
+ .option('--bastion-host <ip>', 'not accepted: the governed path uses the org\'s configured bastion')
382
285
  .option('--ssh-user <user>', 'SSH user', 'control')
383
286
  .option('--ssh-port <n>', 'SSH port', '22')
384
287
  .option('--force', 'skip confirmation')
385
- .action(async (options) => {
288
+ .action(guard(async (options) => {
386
289
  auth.requireAuth();
387
- const confirmed = await confirmOrSkip(program, options, `Queue Wazuh agent deploy to ${options.hostname} (${options.hostIp})? This will require HITL Inbox approval before it runs.`);
290
+ refuseTargetOverride('deploy-agent', options);
291
+ const confirmed = await confirmOrSkip(program, options, `Request a Wazuh agent deploy to ${options.hostname} (${options.hostIp})? It is held for human approval before it runs.`);
388
292
  if (!confirmed) {
389
293
  printInfo('Cancelled.');
390
294
  return;
391
295
  }
392
- const body = { hostname: options.hostname, hostIp: options.hostIp, sshUser: options.sshUser, sshPort: options.sshPort };
393
- if (options.wazuhServer) body.wazuhServer = options.wazuhServer;
394
- if (options.bastionHost) body.bastionHost = options.bastionHost;
395
- const ep = resolveEndpoint('mon.wazuh.deploy-agent');
396
- const data = await client.post(ep.path, { service: ep.service, body });
397
- printSuccess(`Deploy queued — inbox item ${data.inbox_item_id} (awaiting approval).`);
398
- });
296
+ await deployOne(options.hostname, options.hostIp, {
297
+ sshUser: options.sshUser && options.sshUser !== 'control' ? options.sshUser : undefined,
298
+ sshPort: options.sshPort && String(options.sshPort) !== '22' ? options.sshPort : undefined,
299
+ });
300
+ }));
399
301
 
400
302
  wazuh
401
303
  .command('deploy-bulk')
402
- .description('Queue a Wazuh agent deploy for multiple hosts (HITL-gated — requires HITL Inbox approval)')
403
- .requiredOption('--hosts <json>', 'JSON array of {hostname, ip} objects')
404
- .option('--wazuh-server <ip>', 'Wazuh manager IP (defaults to server config)')
405
- .option('--bastion-host <ip>', 'bastion host for the SSH hop')
304
+ .description('Install the Wazuh agent on several hosts: one governed wazuh_deploy_agent call and one approval per host (same as `quox wazuh deploy-bulk`)')
305
+ .requiredOption('--hosts <json>', 'JSON array of {hostname, ip[, sshUser, sshPort]} objects')
306
+ .option('--wazuh-server <ip>', 'not accepted: the governed path uses the org\'s configured Wazuh manager')
307
+ .option('--bastion-host <ip>', 'not accepted: the governed path uses the org\'s configured bastion')
406
308
  .option('--force', 'skip confirmation')
407
309
  .action(async (options) => {
408
310
  auth.requireAuth();
@@ -410,40 +312,49 @@ export function registerMonCommands(program, ctx) {
410
312
  if (!Array.isArray(hosts) || hosts.length === 0) {
411
313
  throw new Error('--hosts must be a non-empty JSON array of {hostname, ip} objects.');
412
314
  }
413
- const confirmed = await confirmOrSkip(program, options, `Queue Wazuh agent deploy to ${hosts.length} host(s)? This will require HITL Inbox approval before it runs.`);
414
- if (!confirmed) {
415
- printInfo('Cancelled.');
416
- return;
417
- }
418
- const body = { hosts };
419
- if (options.wazuhServer) body.wazuhServer = options.wazuhServer;
420
- if (options.bastionHost) body.bastionHost = options.bastionHost;
421
- const ep = resolveEndpoint('mon.wazuh.deploy-bulk');
422
- const data = await client.post(ep.path, { service: ep.service, body });
423
- printSuccess(`Bulk deploy queued for ${data.host_count ?? hosts.length} host(s) — inbox item ${data.inbox_item_id} (awaiting approval).`);
315
+ await guard(async () => {
316
+ refuseTargetOverride('deploy-bulk', options);
317
+ const missing = hosts.filter((h) => !h || !h.hostname || !h.ip).map((h) => (h && h.hostname) || '?');
318
+ if (missing.length) throw new WazFail(`quox mon wazuh deploy-bulk: every host needs hostname and ip; missing for ${missing.join(', ')}`);
319
+ const confirmed = await confirmOrSkip(program, options, `Request a Wazuh agent deploy to ${hosts.length} host(s)? Each is held for human approval before it runs.`);
320
+ if (!confirmed) {
321
+ printInfo('Cancelled.');
322
+ return;
323
+ }
324
+ let failed = 0;
325
+ for (const h of hosts) {
326
+ try { await deployOne(String(h.hostname), String(h.ip), { sshUser: h.sshUser, sshPort: h.sshPort }); } catch (err) {
327
+ if (!(err instanceof WazFail)) throw err;
328
+ console.error(err.message); failed++;
329
+ }
330
+ }
331
+ if (failed) process.exitCode = 1;
332
+ })();
424
333
  });
425
334
 
426
335
  wazuh
427
336
  .command('active-response')
428
- .description('Trigger a Wazuh active response (HITL-gated — requires HITL Inbox approval)')
337
+ .description('Run a Wazuh active response (governed wazuh_active_response, held for human approval; same as `quox wazuh active-response`)')
429
338
  .requiredOption('--command <command>', 'firewall-block|host-isolate|restart-wazuh-agent')
430
339
  .requiredOption('--agents <ids>', 'comma-separated Wazuh agent IDs')
431
340
  .requiredOption('--reason <text>', 'justification (min 10 characters)')
432
341
  .option('--args <args>', 'comma-separated command arguments')
433
342
  .option('--force', 'skip confirmation')
434
- .action(async (options) => {
343
+ .action(guard(async (options) => {
435
344
  auth.requireAuth();
436
- const agentList = options.agents.split(',').map(a => a.trim()).filter(Boolean);
345
+ const agentList = options.agents.split(',').map(a => a.trim()).filter(Boolean).map(padId);
437
346
  const args = options.args ? options.args.split(',').map(a => a.trim()).filter(Boolean) : [];
438
- const confirmed = await confirmOrSkip(program, options, `Trigger active response "${options.command}" on ${agentList.length} agent(s)? This will require HITL Inbox approval before it executes.`);
347
+ if (!agentList.length) throw new WazFail('quox mon wazuh active-response: --agents needs at least one agent id');
348
+ if (!options.reason || options.reason.trim().length < 10) throw new WazFail('quox mon wazuh active-response: --reason "<why>" is required, at least 10 characters (the approver sees it)');
349
+ const confirmed = await confirmOrSkip(program, options, `Request active response "${options.command}" on ${agentList.length} agent(s)? It is held for human approval before it runs.`);
439
350
  if (!confirmed) {
440
351
  printInfo('Cancelled.');
441
352
  return;
442
353
  }
443
- const ep = resolveEndpoint('mon.wazuh.active-response');
444
- const data = await client.post(ep.path, { service: ep.service, body: { command: options.command, agentList, args, reason: options.reason } });
445
- printSuccess(`Active response queued — inbox item ${data.inbox_item_id} (awaiting approval).`);
446
- });
354
+ const input = { command: options.command, agent_ids: agentList, reason: options.reason.trim() };
355
+ if (args.length) input.args = args;
356
+ await writeTool(`active-response ${agentList.join(',')}`, 'wazuh_active_response', input, `${options.command} ran on ${agentList.length} agent(s)`);
357
+ }));
447
358
 
448
359
  // ══════════════════════════════════════════════════════════════════════
449
360
  // agh-chain
@@ -10,6 +10,7 @@ import { spawn } from 'child_process';
10
10
  import { fileURLToPath } from 'url';
11
11
  import { basename, dirname, join, resolve } from 'path';
12
12
  import { Option } from 'commander';
13
+ import { resolveProfileFlagFromArgv } from '../core/config.js';
13
14
  import { formatOutput, formatDetail, printSuccess } from '../core/output.js';
14
15
  import { QuoxApiError, QuoxError } from '../core/errors.js';
15
16
  import { line as glyphLine, printEvent } from '../core/quoxglyph.js';
@@ -18,6 +19,7 @@ import { resolveOrgId } from './helpers.js';
18
19
  import { buildBatchArchive, splitMarkdown, indexMarkdown, MAX_BATCH_NOTES } from './notesBatch.js';
19
20
  import { emailToNote, extractTasks, promoteBody } from './notesBridges.js';
20
21
  import { linkMention } from './notesLinks.js';
22
+ import { registerNotesSeal } from './notesSeal.js';
21
23
 
22
24
  const SCOPES = ['org', 'user'];
23
25
  const ARCHIVED = ['exclude', 'only', 'include'];
@@ -156,6 +158,8 @@ export function registerNotesCommands(program, ctx) {
156
158
  .command('notes')
157
159
  .description('NotesQ: governed markdown notes (list, read, write, history, restore)');
158
160
 
161
+ registerNotesSeal(notes, { program, client, auth, call, getNote, putNote, readContent, isJson, printJson, announce, loadEnqrypt: ctx.loadEnqrypt });
162
+
159
163
  const list = async (options, textQuery) => {
160
164
  auth.requireAuth();
161
165
  checkEnum(options.scope, [...SCOPES, 'all'], '--scope');
@@ -188,10 +192,18 @@ export function registerNotesCommands(program, ctx) {
188
192
  updated: when(n),
189
193
  }));
190
194
  formatOutput({
191
- columns: ['id', 'title', 'scope', 'collection', 'tags', 'updated', ...propKeys.map((_, i) => `prop${i}`)],
192
- headers: ['ID', 'Title', 'Scope', 'Collection', 'Tags', 'Updated', ...propKeys],
195
+ columns: ['title', 'scope', 'collection', 'tags', 'updated', ...propKeys.map((_, i) => `prop${i}`), 'id'],
196
+ headers: ['Title', 'Scope', 'Collection', 'Tags', 'Updated', ...propKeys, 'ID'],
193
197
  rows,
194
- }, { format: program.opts().output });
198
+ }, {
199
+ format: program.opts().output,
200
+ quietColumn: 'id',
201
+ emptyMessage: 'No notes found. Widen the filters (--scope all, drop --tag/--q), or create one with `quox notes new`.',
202
+ });
203
+ const total = data.total ?? rows.length;
204
+ if (program.opts().output !== 'json' && rows.length > 0 && rows.length < total) {
205
+ console.log(`showing ${rows.length} of ${total} · --limit to see more`);
206
+ }
195
207
  announce('notes ls', `${rows.length} of ${data.total ?? rows.length}`);
196
208
  };
197
209
 
@@ -1116,8 +1128,15 @@ export function registerNotesCommands(program, ctx) {
1116
1128
  auth.requireAuth();
1117
1129
  const ep = resolveEndpoint('notesq.notes.proposeSkill', { id });
1118
1130
  const body = { ...(options.name ? { name: options.name } : {}), ...(options.category ? { category: options.category } : {}) };
1131
+ // The route refuses a note under its minimum length with 400 {error:'too_short', min_words}: say so, not a bare 400.
1132
+ const propose = (b, opts = {}) => call(() => client.post(ep.path, { service: ep.service, body: b, ...opts })).catch((e) => {
1133
+ if (e instanceof QuoxApiError && e.status === 400 && e.body && e.body.error === 'too_short') {
1134
+ throw new QuoxError(`Note ${id} is too short to become a skill: it needs at least ${e.body.min_words || 'more'} words of procedure. Add the steps (quox notes edit ${id}) and run again.`, 1);
1135
+ }
1136
+ throw e;
1137
+ });
1119
1138
  if (!options.yes) {
1120
- const r = await call(() => client.post(ep.path, { service: ep.service, body: { ...body, dry_run: true } }));
1139
+ const r = await propose({ ...body, dry_run: true });
1121
1140
  if (isJson(program)) return printJson(r);
1122
1141
  const p = r.payload || {};
1123
1142
  console.log(`name: ${p.name}`);
@@ -1126,7 +1145,7 @@ export function registerNotesCommands(program, ctx) {
1126
1145
  console.log(String(p.bodyMd || '').split('\n').slice(0, 8).join('\n'));
1127
1146
  return console.log('Re-run with --yes to propose it. It lands as an experimental skill, outside the stable library; an automated safety check reviews it and anything flagged goes to the human review queue (quox skills review).');
1128
1147
  }
1129
- const r = await call(() => client.post(ep.path, { service: ep.service, body, retries: 0 }));
1148
+ const r = await propose(body, { retries: 0 });
1130
1149
  if (isJson(program)) return printJson(r);
1131
1150
  announce('propose-skill', `${r.skill.name} (${r.skill.id}) experimental`);
1132
1151
  if (!r.note_updated) console.log('Note: the skill was created but the note could not be updated with the link (it changed meanwhile).');
@@ -1225,7 +1244,7 @@ export function registerNotesCommands(program, ctx) {
1225
1244
 
1226
1245
  notes
1227
1246
  .command('scorecard')
1228
- .description('Run the NotesQ scorecard (scripts/notesq-scorecard.mjs in the dashboard repo), streaming its output')
1247
+ .description('Run the NotesQ scorecard (scripts/notesq-scorecard.mjs in the dashboard repo), streaming its output. WRITE: creates notes through chat (raising approval cards) and deletes them afterwards; the child runs as the same -p profile')
1229
1248
  .option('--keep', 'keep the scorecard\'s test data')
1230
1249
  .action(async (options) => {
1231
1250
  if (!existsSync(scorecardPath)) {
@@ -1235,7 +1254,10 @@ export function registerNotesCommands(program, ctx) {
1235
1254
  const args = [scorecardPath];
1236
1255
  if (options.keep) args.push('--keep');
1237
1256
  const code = await new Promise((done, fail) => {
1238
- const child = spawn(process.execPath, args, { cwd: resolve(dirname(scorecardPath), '..'), stdio: ['inherit', 'pipe', 'pipe'] });
1257
+ // The child must act as the profile the caller chose, never the resident/attached agent identity.
1258
+ const profile = ctx.profile || resolveProfileFlagFromArgv(process.argv.slice(2)) || process.env.QUOX_PROFILE;
1259
+ const env = profile ? { ...process.env, QUOX_PROFILE: profile } : process.env;
1260
+ const child = spawn(process.execPath, args, { cwd: resolve(dirname(scorecardPath), '..'), env, stdio: ['inherit', 'pipe', 'pipe'] });
1239
1261
  child.stdout.on('data', (c) => process.stdout.write(c));
1240
1262
  child.stderr.on('data', (c) => process.stderr.write(c));
1241
1263
  child.on('error', fail);