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.
- package/CHANGELOG.md +61 -0
- package/README.md +2 -2
- package/bin/quox.js +19 -1
- package/bin/quoxon.js +6 -1
- package/lib/api/endpoints/actions.js +15 -0
- package/lib/api/endpoints/admin.js +27 -0
- package/lib/api/endpoints/agent-attach.js +7 -0
- package/lib/api/endpoints/blog.js +16 -0
- package/lib/api/endpoints/chat.js +7 -0
- package/lib/api/endpoints/compliance.js +25 -0
- package/lib/api/endpoints/contactsq.js +4 -0
- package/lib/api/endpoints/devServerRegistry.js +25 -0
- package/lib/api/endpoints/engagement.js +26 -0
- package/lib/api/endpoints/evidence.js +20 -0
- package/lib/api/endpoints/hat.js +14 -0
- package/lib/api/endpoints/inbox.js +6 -0
- package/lib/api/endpoints/inboxq.js +19 -0
- package/lib/api/endpoints/index.js +14 -0
- package/lib/api/endpoints/loops.js +26 -0
- package/lib/api/endpoints/mon.js +295 -0
- package/lib/api/endpoints/objectives.js +6 -0
- package/lib/api/endpoints/quoxseo.js +6 -0
- package/lib/api/endpoints/server.js +21 -0
- package/lib/api/endpoints/setup.js +6 -0
- package/lib/api/endpoints/tools.js +13 -0
- package/lib/api/endpoints/v1.js +47 -0
- package/lib/commands/actions.js +81 -11
- package/lib/commands/admin.js +35 -0
- package/lib/commands/agent.js +11 -4
- package/lib/commands/agentAttach.js +61 -7
- package/lib/commands/approvals.js +198 -3
- package/lib/commands/ask.js +4 -2
- package/lib/commands/auth.js +64 -11
- package/lib/commands/backup.js +11 -4
- package/lib/commands/blog.js +110 -0
- package/lib/commands/capabilities.js +3 -2
- package/lib/commands/chat.js +142 -28
- package/lib/commands/chatCompanion.js +27 -0
- package/lib/commands/chatState.js +45 -0
- package/lib/commands/claude.js +7 -33
- package/lib/commands/compliance.js +109 -0
- package/lib/commands/connect.js +69 -0
- package/lib/commands/contacts.js +85 -4
- package/lib/commands/contactsSendAt.js +21 -0
- package/lib/commands/dev.js +6 -8
- package/lib/commands/doctor.js +22 -3
- package/lib/commands/engagement.js +90 -0
- package/lib/commands/evidence.js +155 -0
- package/lib/commands/ext.js +23 -0
- package/lib/commands/fleet.js +25 -0
- package/lib/commands/gripe.js +24 -13
- package/lib/commands/hat.js +76 -0
- package/lib/commands/image.js +68 -0
- package/lib/commands/inbox.js +37 -3
- package/lib/commands/inboxq.js +147 -0
- package/lib/commands/kuma.js +1123 -0
- package/lib/commands/kumaSla.js +174 -0
- package/lib/commands/licence.js +39 -1
- package/lib/commands/licenceLinkView.js +311 -0
- package/lib/commands/linkedin.js +19 -0
- package/lib/commands/loop.js +318 -0
- package/lib/commands/lucid.js +1 -0
- package/lib/commands/mail.js +161 -20
- package/lib/commands/memory.js +68 -4
- package/lib/commands/memorySync.js +158 -0
- package/lib/commands/mon.js +55 -144
- package/lib/commands/notes.js +29 -7
- package/lib/commands/notesSeal.js +259 -0
- package/lib/commands/objective.js +53 -3
- package/lib/commands/offload.js +2 -3
- package/lib/commands/org.js +21 -4
- package/lib/commands/plan.js +17 -0
- package/lib/commands/provider.js +87 -8
- package/lib/commands/qlarity.js +32 -1
- package/lib/commands/quoxseo.js +31 -0
- package/lib/commands/research.js +79 -0
- package/lib/commands/rows.js +9 -0
- package/lib/commands/run.js +20 -2
- package/lib/commands/scope.js +2 -2
- package/lib/commands/server.js +150 -0
- package/lib/commands/setup.js +127 -75
- package/lib/commands/shortcut.js +41 -0
- package/lib/commands/skills.js +24 -5
- package/lib/commands/slot.js +49 -9
- package/lib/commands/status.js +92 -44
- package/lib/commands/tool.js +30 -0
- package/lib/commands/vault.js +6 -3
- package/lib/commands/vm.js +2 -2
- package/lib/commands/walkthrough.js +14 -2
- package/lib/commands/watch.js +11 -2
- package/lib/commands/wazuh.js +824 -0
- package/lib/commands/wazuhIntake.js +317 -0
- package/lib/commands/wazuhReport.js +192 -0
- package/lib/commands/workflow.js +47 -4
- package/lib/commands/x.js +50 -0
- package/lib/commands/youtube.js +172 -0
- package/lib/core/actionItems.js +115 -0
- package/lib/core/agentAlias.js +2 -0
- package/lib/core/agentFamilies.js +7 -0
- package/lib/core/announce.js +86 -0
- package/lib/core/auth.js +20 -2
- package/lib/core/callMe.js +118 -0
- package/lib/core/client.js +37 -3
- package/lib/core/commandRegistrars.js +17 -0
- package/lib/core/config.js +29 -3
- package/lib/core/confirm.js +68 -7
- package/lib/core/crossOrgApprovals.js +69 -0
- package/lib/core/errors.js +52 -1
- package/lib/core/findingReconcile.js +90 -0
- package/lib/core/global-flags.js +1 -1
- package/lib/core/governedSession.js +136 -8
- package/lib/core/inboxResolution.js +3 -2
- package/lib/core/liveSessionsReport.js +1 -1
- package/lib/core/loginGuard.js +73 -0
- package/lib/core/loopGlyph.js +97 -0
- package/lib/core/offload.js +73 -7
- package/lib/core/org-override.js +5 -1
- package/lib/core/output.js +8 -8
- package/lib/core/profileTrust.js +182 -0
- package/lib/core/quoxglyph.js +22 -1
- package/lib/core/router.js +14 -0
- package/lib/core/sessionHeartbeat.js +12 -8
- package/lib/core/shortcut.js +83 -0
- package/lib/core/stepUpPrompt.js +99 -0
- package/lib/core/version.js +1 -1
- package/lib/core/vm.js +5 -0
- package/lib/core/walkthrough.js +9 -5
- package/lib/dragon/companionView.js +34 -0
- package/lib/dragon/frames.js +118 -0
- package/lib/dragon/jobs.js +123 -0
- package/lib/dragon/renderWorker.js +15 -0
- package/lib/dragon/select.js +72 -0
- package/lib/evidence/bundleExport.js +47 -0
- package/lib/evidence/bundleVerify.js +77 -0
- package/lib/quoxon/EXTENSIONS.md +146 -0
- package/lib/quoxon/api.js +364 -27
- package/lib/quoxon/app.js +1629 -183
- package/lib/quoxon/approvalBatch.js +117 -0
- package/lib/quoxon/approvalFacts.js +64 -8
- package/lib/quoxon/approvalSentence.js +48 -0
- package/lib/quoxon/approvalsView.js +124 -7
- package/lib/quoxon/autoOpen.js +94 -0
- package/lib/quoxon/backgroundWork.js +59 -0
- package/lib/quoxon/bang.js +1 -1
- package/lib/quoxon/capabilities.js +7 -2
- package/lib/quoxon/commandShort.js +22 -3
- package/lib/quoxon/commands.js +110 -15
- package/lib/quoxon/connect.js +5 -3
- package/lib/quoxon/countdownLine.js +10 -1
- package/lib/quoxon/deck.js +30 -0
- package/lib/quoxon/deckLazy.js +58 -0
- package/lib/quoxon/demo.autoplay.js +1 -1
- package/lib/quoxon/demo.js +3 -1
- package/lib/quoxon/destructive.js +35 -0
- package/lib/quoxon/dividerAocl.js +4 -3
- package/lib/quoxon/dividerRule.js +3 -3
- package/lib/quoxon/dnd.js +71 -0
- package/lib/quoxon/dragonCompanion.js +285 -0
- package/lib/quoxon/dragonModes.js +1 -0
- package/lib/quoxon/dragonMoments.js +160 -0
- package/lib/quoxon/dragonReads.js +68 -0
- package/lib/quoxon/dragonView.js +2 -2
- package/lib/quoxon/echo.js +1 -0
- package/lib/quoxon/events.js +67 -10
- package/lib/quoxon/extensions.js +636 -0
- package/lib/quoxon/gripeQueue.js +135 -0
- package/lib/quoxon/inboxView.js +182 -0
- package/lib/quoxon/installCli.js +79 -0
- package/lib/quoxon/keys.js +4 -4
- package/lib/quoxon/loopCard.js +460 -0
- package/lib/quoxon/loopCards.js +189 -0
- package/lib/quoxon/loopStream.js +98 -0
- package/lib/quoxon/mailView.js +71 -0
- package/lib/quoxon/main.js +29 -5
- package/lib/quoxon/markdown.js +1 -1
- package/lib/quoxon/model.js +15 -0
- package/lib/quoxon/mouse.js +12 -1
- package/lib/quoxon/neverAnswered.js +20 -0
- package/lib/quoxon/objectiveOffer.js +82 -0
- package/lib/quoxon/objects/away.js +43 -5
- package/lib/quoxon/objects/index.js +22 -4
- package/lib/quoxon/objects/model.js +33 -0
- package/lib/quoxon/objects/proof.js +12 -4
- package/lib/quoxon/onboarding/connectServer.js +162 -0
- package/lib/quoxon/onboarding/copy.js +6 -0
- package/lib/quoxon/onboarding/index.js +5 -43
- package/lib/quoxon/onboarding/probe.js +20 -1
- package/lib/quoxon/onboarding/shortcutOffer.js +28 -0
- package/lib/quoxon/onboarding/view.js +16 -3
- package/lib/quoxon/orgSwitch.js +49 -1
- package/lib/quoxon/planSteps.js +51 -0
- package/lib/quoxon/print.js +8 -3
- package/lib/quoxon/profileSwitch.js +7 -0
- package/lib/quoxon/promptHistory.js +3 -1
- package/lib/quoxon/qdragonFly.js +7 -6
- package/lib/quoxon/qmotion.js +17 -0
- package/lib/quoxon/render.js +187 -41
- package/lib/quoxon/roster.js +36 -17
- package/lib/quoxon/rosterSessions.js +44 -0
- package/lib/quoxon/rows.js +244 -1
- package/lib/quoxon/screenContext.js +1 -1
- package/lib/quoxon/secretShape.js +86 -0
- package/lib/quoxon/serverChip.js +42 -0
- package/lib/quoxon/session.js +10 -0
- package/lib/quoxon/slash/agents.js +2 -1
- package/lib/quoxon/slash/argSources.js +9 -2
- package/lib/quoxon/slash/args.js +4 -2
- package/lib/quoxon/slash/broadcast.js +18 -5
- package/lib/quoxon/slash/broadcastRun.js +13 -2
- package/lib/quoxon/slash/cliCommand.js +42 -1
- package/lib/quoxon/slash/cliLeaves.json +139 -2
- package/lib/quoxon/slash/evidenceCmd.js +34 -0
- package/lib/quoxon/slash/hatWalk.js +1 -1
- package/lib/quoxon/slash/palette.js +23 -2
- package/lib/quoxon/slash/paletteController.js +10 -4
- package/lib/quoxon/slash/qpeCmd.js +35 -0
- package/lib/quoxon/slash/restart.js +31 -4
- package/lib/quoxon/slash/resume.js +18 -1
- package/lib/quoxon/slash/stepUp.js +5 -5
- package/lib/quoxon/slash/stepUpCmds.js +74 -1
- package/lib/quoxon/slash/todoCmd.js +122 -0
- package/lib/quoxon/slashSpinner.js +35 -0
- package/lib/quoxon/slotAttach.js +8 -0
- package/lib/quoxon/slotChat.js +17 -0
- package/lib/quoxon/slotFormat.js +1 -1
- package/lib/quoxon/slotOutbox.js +113 -0
- package/lib/quoxon/suggestCommand.js +34 -3
- package/lib/quoxon/term.js +25 -1
- package/lib/quoxon/toolCards.js +30 -0
- package/lib/quoxon/transcript.js +2 -1
- package/lib/quoxon/welcome.js +1 -1
- package/lib/quoxon/whatsnew.js +1 -1
- package/lib/quoxon/whatsnew.jsonl +81 -0
- package/lib/quoxon/windowOrg.js +11 -1
- package/lib/server/devServers.js +137 -0
- package/lib/server/installFlow.js +168 -0
- package/lib/server/status.js +15 -1
- package/lib/toolOutputShape.js +55 -0
- package/package.json +4 -2
- package/vendor/dragonlab/SOURCE +4 -0
- package/vendor/dragonlab/design/cipher/v1/cipher-master.svg +46 -0
- package/vendor/dragonlab/design/cipher/v2/character.mjs +49 -0
- package/vendor/dragonlab/design/cipher/v2/review.mjs +33 -0
- package/vendor/dragonlab/design/cipher/v2/watch.mjs +56 -0
- package/vendor/dragonlab/design/drago/v1/character.mjs +19 -0
- package/vendor/dragonlab/design/drago/v1/drago-master.svg +56 -0
- package/vendor/dragonlab/design/drago/v2/character.mjs +87 -0
- package/vendor/dragonlab/design/drago/v4/character.mjs +67 -0
- package/vendor/dragonlab/design/drago/v4/profile.mjs +35 -0
- package/vendor/dragonlab/design/drago/v4/review.mjs +33 -0
- package/vendor/dragonlab/design/drago/v4/watch.mjs +56 -0
- package/vendor/dragonlab/design/hyperion/v1/hyperion-master.svg +56 -0
- package/vendor/dragonlab/design/hyperion/v2/character.mjs +79 -0
- package/vendor/dragonlab/design/hyperion/v2/review.mjs +33 -0
- package/vendor/dragonlab/design/hyperion/v2/watch.mjs +56 -0
- package/vendor/dragonlab/design/lucid/recovery/arrival.mjs +22 -0
- package/vendor/dragonlab/design/lucid/recovery/character.mjs +106 -0
- package/vendor/dragonlab/design/lucid/recovery/layout.mjs +50 -0
- package/vendor/dragonlab/design/lucid/recovery/lucid-master.svg +78 -0
- package/vendor/dragonlab/design/lucid/recovery/motion.mjs +42 -0
- package/vendor/dragonlab/design/lucid/recovery/render.mjs +121 -0
- package/vendor/dragonlab/design/lucid/recovery/review.mjs +68 -0
- package/vendor/dragonlab/design/lucid/recovery/verify.mjs +84 -0
- package/vendor/dragonlab/design/lucid/recovery/watch.mjs +56 -0
- package/vendor/dragonlab/design/quoxperticus/v2/quoxperticus-master.svg +59 -0
- package/vendor/dragonlab/design/quoxperticus/v3/character.mjs +43 -0
- package/vendor/dragonlab/design/quoxperticus/v4/character.mjs +57 -0
- package/vendor/dragonlab/design/quoxperticus/v4/review.mjs +35 -0
- package/vendor/dragonlab/design/quoxperticus/v4/watch.mjs +56 -0
- package/vendor/dragonlab/design/seer/v1/seer-master.svg +53 -0
- package/vendor/dragonlab/design/seer/v2/character.mjs +88 -0
- package/vendor/dragonlab/design/seer/v3/character.mjs +33 -0
- package/vendor/dragonlab/design/seer/v4/character.mjs +66 -0
- package/vendor/dragonlab/design/seer/v4/review.mjs +33 -0
- package/vendor/dragonlab/design/seer/v4/watch.mjs +56 -0
- package/vendor/dragonlab/design/stage3/companion-review.mjs +28 -0
- package/vendor/dragonlab/design/stage3/sentinel-art.js +39 -0
- package/vendor/dragonlab/design/stage3/sentinel-braille.js +216 -0
- package/vendor/dragonlab/design/stage3/sentinel-character.js +257 -0
- package/vendor/dragonlab/design/stage3/sentinel-icon.js +103 -0
- package/vendor/dragonlab/design/stage3/sentinel-warden-eyes.js +95 -0
- package/vendor/dragonlab/design/stage3/svg-coverage.js +148 -0
- package/vendor/dragonlab/design/vulcan/v1/character.mjs +34 -0
- package/vendor/dragonlab/design/vulcan/v1/vulcan-master.svg +57 -0
- package/vendor/dragonlab/design/vulcan/v2/character.mjs +39 -0
- package/vendor/dragonlab/design/vulcan/v3/character.mjs +41 -0
- package/vendor/dragonlab/design/vulcan/v3/review.mjs +35 -0
- package/vendor/dragonlab/design/vulcan/v3/watch.mjs +56 -0
- package/vendor/dragonlab/factory/svg-rig-contour.mjs +64 -0
- package/vendor/dragonlab/factory/svg-rig.mjs +63 -0
- package/vendor/dragonlab/integration/companions-v1/characters.mjs +108 -0
- package/vendor/dragonlab/integration/companions-v1/cipher.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/companion.mjs +63 -0
- package/vendor/dragonlab/integration/companions-v1/drago.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/hyperion.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/lucid.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/quoxperticus.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/seer.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/sentinel.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/view.mjs +28 -0
- package/vendor/dragonlab/integration/companions-v1/vulcan.mjs +2 -0
- package/vendor/dragonlab/integration/hyperion-v3/activity.mjs +76 -0
- package/vendor/dragonlab/lib/power-effects.js +294 -0
- package/vendor/dragonlab/lib/power-registry.js +74 -0
- package/vendor/dragonlab/vendor/quox-dragon/qart.js +348 -0
- package/vendor/dragonlab/vendor/quox-dragon/qeyes.js +213 -0
package/lib/commands/memory.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/lib/commands/mon.js
CHANGED
|
@@ -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
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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('
|
|
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>', '
|
|
381
|
-
.option('--bastion-host <ip>', '
|
|
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
|
-
|
|
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
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
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('
|
|
403
|
-
.requiredOption('--hosts <json>', 'JSON array of {hostname, ip} objects')
|
|
404
|
-
.option('--wazuh-server <ip>', '
|
|
405
|
-
.option('--bastion-host <ip>', '
|
|
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
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
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('
|
|
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
|
-
|
|
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
|
|
444
|
-
|
|
445
|
-
|
|
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
|
package/lib/commands/notes.js
CHANGED
|
@@ -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: ['
|
|
192
|
-
headers: ['
|
|
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
|
-
}, {
|
|
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
|
|
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
|
|
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
|
-
|
|
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);
|