quoxcli 0.2.0 → 0.4.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/LICENSE +1 -1
- package/README.md +2 -0
- package/bin/quox.js +99 -126
- package/lib/api/endpoints/actions.js +15 -0
- package/lib/api/endpoints/admin.js +46 -0
- package/lib/api/endpoints/agent-schedules.js +26 -0
- package/lib/api/endpoints/agents.js +7 -0
- package/lib/api/endpoints/assistants.js +12 -0
- package/lib/api/endpoints/away.js +15 -0
- package/lib/api/endpoints/blog.js +16 -0
- package/lib/api/endpoints/chat.js +14 -0
- package/lib/api/endpoints/contactsq.js +6 -0
- package/lib/api/endpoints/conversations.js +14 -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/executions.js +13 -0
- package/lib/api/endpoints/fetchAllowlist.js +27 -0
- package/lib/api/endpoints/files.js +14 -0
- package/lib/api/endpoints/hat.js +44 -0
- package/lib/api/endpoints/inbox.js +20 -0
- package/lib/api/endpoints/index.js +26 -0
- package/lib/api/endpoints/loops.js +26 -0
- package/lib/api/endpoints/memory.js +19 -0
- package/lib/api/endpoints/mind.js +13 -0
- package/lib/api/endpoints/mon.js +179 -0
- package/lib/api/endpoints/objectives.js +12 -0
- package/lib/api/endpoints/plugins.js +18 -0
- package/lib/api/endpoints/presence.js +33 -0
- package/lib/api/endpoints/quoxplan.js +7 -0
- package/lib/api/endpoints/quoxseo.js +12 -0
- package/lib/api/endpoints/router.js +17 -0
- package/lib/api/endpoints/safety.js +7 -0
- package/lib/api/endpoints/server.js +21 -0
- package/lib/api/endpoints/setup.js +6 -0
- package/lib/api/endpoints/skills.js +20 -0
- package/lib/api/endpoints/stepup.js +20 -0
- package/lib/api/endpoints/teams.js +2 -2
- package/lib/api/endpoints/triggers.js +26 -0
- package/lib/api/endpoints/v1.js +120 -0
- package/lib/api/endpoints/ward.js +7 -0
- package/lib/api/endpoints/workflows.js +7 -0
- package/lib/api/endpoints/x.js +8 -0
- package/lib/commands/actions.js +131 -0
- package/lib/commands/admin.js +16 -0
- package/lib/commands/agent.js +170 -27
- package/lib/commands/agentAttach.js +140 -13
- package/lib/commands/approvals.js +78 -1
- package/lib/commands/aside.js +24 -0
- package/lib/commands/ask.js +146 -0
- package/lib/commands/assistant.js +59 -4
- package/lib/commands/auth.js +8 -2
- package/lib/commands/backup.js +8 -4
- package/lib/commands/blog.js +110 -0
- package/lib/commands/brain.js +105 -14
- package/lib/commands/build.js +19 -5
- package/lib/commands/chat.js +180 -26
- package/lib/commands/chatCompanion.js +27 -0
- package/lib/commands/chatState.js +45 -0
- package/lib/commands/claude.js +35 -38
- package/lib/commands/contacts.js +134 -4
- package/lib/commands/contactsSendAt.js +21 -0
- package/lib/commands/conversation.js +35 -4
- package/lib/commands/dashboard.js +43 -0
- package/lib/commands/department.js +28 -7
- package/lib/commands/devResident.js +15 -3
- package/lib/commands/devSupervisor.js +13 -1
- package/lib/commands/discord.js +75 -39
- package/lib/commands/done.js +98 -28
- package/lib/commands/engagement.js +107 -6
- package/lib/commands/evidence.js +154 -0
- package/lib/commands/ext.js +23 -0
- package/lib/commands/fetchAllowlist.js +71 -0
- package/lib/commands/file.js +38 -1
- package/lib/commands/fleet.js +336 -4
- package/lib/commands/governance.js +12 -3
- package/lib/commands/gripe.js +44 -13
- package/lib/commands/hat.js +254 -9
- package/lib/commands/helpers.js +18 -1
- package/lib/commands/hyperion.js +58 -0
- package/lib/commands/image.js +68 -0
- package/lib/commands/inbox.js +74 -23
- package/lib/commands/kuma.js +1050 -0
- package/lib/commands/kumaSla.js +174 -0
- package/lib/commands/licence.js +52 -155
- package/lib/commands/loop.js +291 -0
- package/lib/commands/lucid.js +10 -4
- package/lib/commands/memory.js +85 -8
- package/lib/commands/mind.js +71 -0
- package/lib/commands/mon.js +69 -2
- package/lib/commands/monitoring.js +1 -1
- package/lib/commands/motion.js +2 -2
- package/lib/commands/nucleus.js +48 -17
- package/lib/commands/objective.js +59 -1
- package/lib/commands/offload.js +89 -0
- package/lib/commands/onboard.js +2 -1
- package/lib/commands/org.js +30 -3
- package/lib/commands/peek.js +28 -0
- package/lib/commands/plan.js +38 -0
- package/lib/commands/plugin.js +110 -0
- package/lib/commands/presence.js +89 -0
- package/lib/commands/provider.js +217 -0
- package/lib/commands/qlarity.js +19 -3
- package/lib/commands/quoxchat.js +13 -4
- package/lib/commands/quoxseo.js +69 -0
- package/lib/commands/reach.js +2 -1
- package/lib/commands/research.js +79 -0
- package/lib/commands/router.js +77 -0
- package/lib/commands/rows.js +367 -0
- package/lib/commands/run.js +82 -10
- package/lib/commands/scope.js +2 -2
- package/lib/commands/server.js +405 -0
- package/lib/commands/setup.js +33 -7
- package/lib/commands/shortcut.js +41 -0
- package/lib/commands/skills.js +68 -9
- package/lib/commands/slot.js +260 -7
- package/lib/commands/status.js +1 -1
- package/lib/commands/team.js +3 -1
- package/lib/commands/tips.js +23 -0
- package/lib/commands/tool.js +133 -45
- package/lib/commands/trace.js +12 -7
- package/lib/commands/trigger.js +95 -0
- package/lib/commands/update.js +46 -0
- package/lib/commands/vault.js +31 -8
- package/lib/commands/vm.js +237 -0
- package/lib/commands/voice.js +34 -3
- package/lib/commands/volt.js +45 -3
- package/lib/commands/walkthrough.js +32 -0
- package/lib/commands/watch.js +73 -21
- package/lib/commands/welcome.js +14 -0
- package/lib/commands/workflow.js +189 -32
- package/lib/commands/x.js +13 -4
- package/lib/commands/youtube.js +172 -0
- package/lib/core/actingAs.js +2 -2
- package/lib/core/actionItems.js +115 -0
- package/lib/core/activeSummary.js +186 -0
- package/lib/core/agentAlias.js +2 -0
- package/lib/core/agentFamilies.js +7 -0
- package/lib/core/aside.js +54 -0
- package/lib/core/askDecision.js +60 -0
- package/lib/core/auth.js +164 -13
- package/lib/core/callerSource.js +1 -1
- package/lib/core/cliBuild.js +62 -0
- package/lib/core/client.js +152 -35
- package/lib/core/commandRegistrars.js +130 -0
- package/lib/core/config.js +143 -25
- package/lib/core/customHats.js +168 -0
- package/lib/core/doneEngineFetch.js +8 -0
- package/lib/core/errors.js +329 -20
- package/lib/core/findingReconcile.js +90 -0
- package/lib/core/global-flags.js +105 -5
- package/lib/core/governedSession.js +1 -3
- package/lib/core/hyperion.js +102 -0
- package/lib/core/inboxResolution.js +65 -0
- package/lib/core/lazyCommands.js +4 -1
- package/lib/core/liveSessionsReport.js +1 -1
- package/lib/core/loopGlyph.js +97 -0
- package/lib/core/offload.js +533 -0
- package/lib/core/org-override.js +1 -1
- package/lib/core/orgResolve.js +70 -0
- package/lib/core/output.js +11 -11
- package/lib/core/peek.js +126 -0
- package/lib/core/profileUsers.js +51 -0
- package/lib/core/promptCancel.js +18 -0
- package/lib/core/repoPath.js +11 -0
- package/lib/core/residentSeed.js +3 -1
- package/lib/core/rootOptions.js +39 -0
- package/lib/core/router.js +168 -0
- package/lib/core/selfUpdate.js +188 -0
- package/lib/core/sessionHeartbeat.js +12 -8
- package/lib/core/sessionsOverview.js +64 -0
- package/lib/core/shortcut.js +83 -0
- package/lib/core/slotKeeper.js +248 -3
- package/lib/core/slotWake.js +120 -0
- package/lib/core/supervisorEngine.js +71 -2
- package/lib/core/version.js +1 -1
- package/lib/core/vm.js +366 -0
- package/lib/core/vmTui.js +86 -0
- package/lib/core/walkthrough.js +35 -0
- 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/hyperion/deck.js +29 -0
- package/lib/lucid/LucidCard.jsx +68 -0
- package/lib/lucid/LucidIntro.jsx +45 -58
- package/lib/lucid/cards.js +86 -0
- package/lib/lucid/markdown.js +81 -0
- package/lib/lucid/motion.js +56 -0
- package/lib/quoxon/EXTENSIONS.md +23 -0
- package/lib/quoxon/activity.js +14 -2
- package/lib/quoxon/aocl.js +117 -0
- package/lib/quoxon/api.js +690 -25
- package/lib/quoxon/app.js +3586 -250
- package/lib/quoxon/approvalFacts.js +96 -0
- package/lib/quoxon/approvalSentence.js +44 -0
- package/lib/quoxon/approvalsView.js +384 -0
- package/lib/quoxon/asideView.js +40 -0
- package/lib/quoxon/ask.js +121 -0
- package/lib/quoxon/away.js +52 -0
- package/lib/quoxon/bang.js +224 -0
- package/lib/quoxon/bulkRisk.js +17 -0
- package/lib/quoxon/capabilities.js +13 -6
- package/lib/quoxon/cells.js +53 -0
- package/lib/quoxon/commandShort.js +124 -0
- package/lib/quoxon/commands.js +429 -39
- package/lib/quoxon/connect.js +210 -23
- package/lib/quoxon/connectCard.js +19 -2
- package/lib/quoxon/consent.js +86 -0
- package/lib/quoxon/copy.js +4 -3
- package/lib/quoxon/countdownLine.js +20 -0
- package/lib/quoxon/deck.js +30 -0
- package/lib/quoxon/demo.autoplay.js +29 -0
- package/lib/quoxon/demo.fixtures.js +1 -1
- package/lib/quoxon/demo.js +46 -3
- package/lib/quoxon/devObserver.js +20 -0
- package/lib/quoxon/dividerAocl.js +192 -0
- package/lib/quoxon/dividerRule.js +57 -0
- package/lib/quoxon/dragonCompanion.js +255 -0
- package/lib/quoxon/dragonModes.js +151 -0
- package/lib/quoxon/dragonMoments.js +82 -0
- package/lib/quoxon/dragonReads.js +68 -0
- package/lib/quoxon/dragonView.js +41 -0
- package/lib/quoxon/earlyInput.js +40 -0
- package/lib/quoxon/echo.js +15 -0
- package/lib/quoxon/egg.js +2 -2
- package/lib/quoxon/events.js +266 -26
- package/lib/quoxon/extensions.js +276 -0
- package/lib/quoxon/feedback.js +50 -0
- package/lib/quoxon/gallery.js +21 -7
- package/lib/quoxon/handoffOffer.js +78 -0
- package/lib/quoxon/hatSwitch.js +37 -0
- package/lib/quoxon/headless.js +161 -0
- package/lib/quoxon/hostUrls.js +41 -0
- package/lib/quoxon/inboxView.js +79 -0
- package/lib/quoxon/indicator.js +6 -2
- package/lib/quoxon/installCli.js +79 -0
- package/lib/quoxon/intent.js +12 -1
- package/lib/quoxon/keys.js +51 -8
- package/lib/quoxon/launcher.js +103 -0
- package/lib/quoxon/loopCard.js +456 -0
- package/lib/quoxon/loopCards.js +180 -0
- package/lib/quoxon/loopStream.js +87 -0
- package/lib/quoxon/main.js +101 -160
- package/lib/quoxon/markdown.js +45 -21
- package/lib/quoxon/markdownSettle.js +19 -0
- package/lib/quoxon/matrixDomains.json +46 -0
- package/lib/quoxon/mentions.js +90 -0
- package/lib/quoxon/mind.js +103 -0
- package/lib/quoxon/mode.js +46 -0
- package/lib/quoxon/model.js +132 -26
- package/lib/quoxon/modelPrefs.js +345 -0
- package/lib/quoxon/motion/clock.js +3 -2
- package/lib/quoxon/motion/level.js +91 -0
- package/lib/quoxon/mouse.js +222 -0
- package/lib/quoxon/noai.js +57 -0
- package/lib/quoxon/objectiveOffer.js +82 -0
- package/lib/quoxon/objects/away.js +72 -0
- package/lib/quoxon/objects/fetch.js +17 -5
- package/lib/quoxon/objects/fleet.js +149 -0
- package/lib/quoxon/objects/health.js +186 -0
- package/lib/quoxon/objects/index.js +46 -7
- package/lib/quoxon/objects/mirror.js +137 -0
- package/lib/quoxon/objects/model.js +33 -0
- package/lib/quoxon/objects/peek.js +17 -0
- package/lib/quoxon/objects/proof.js +12 -4
- package/lib/quoxon/objects/prove.js +130 -0
- package/lib/quoxon/objects/replay.js +125 -0
- package/lib/quoxon/objects/sitrep.js +58 -0
- package/lib/quoxon/objects/view.js +11 -0
- package/lib/quoxon/onboarding/copy.js +32 -4
- package/lib/quoxon/onboarding/credential.js +17 -4
- package/lib/quoxon/onboarding/dockerOffer.js +45 -0
- package/lib/quoxon/onboarding/failureCause.js +33 -0
- package/lib/quoxon/onboarding/finish.js +15 -0
- package/lib/quoxon/onboarding/index.js +34 -7
- package/lib/quoxon/onboarding/installRunner.js +64 -5
- package/lib/quoxon/onboarding/preflight.js +17 -3
- package/lib/quoxon/onboarding/probe.js +65 -0
- package/lib/quoxon/onboarding/shortcutOffer.js +28 -0
- package/lib/quoxon/onboarding/view.js +76 -19
- package/lib/quoxon/orgLabel.js +10 -0
- package/lib/quoxon/orgSwitch.js +59 -0
- package/lib/quoxon/ownBroadcast.js +22 -0
- package/lib/quoxon/palette.js +43 -0
- package/lib/quoxon/planSteps.js +51 -0
- package/lib/quoxon/print.js +12 -5
- package/lib/quoxon/profileSwitch.js +53 -0
- package/lib/quoxon/promptHistory.js +41 -0
- package/lib/quoxon/providers.js +48 -0
- package/lib/quoxon/qart.js +20 -51
- package/lib/quoxon/qdragon.js +410 -0
- package/lib/quoxon/qdragonFly.js +62 -0
- package/lib/quoxon/qdragonScenes.js +282 -0
- package/lib/quoxon/qeyes.js +213 -0
- package/lib/quoxon/qmotion.js +75 -37
- package/lib/quoxon/render.js +740 -124
- package/lib/quoxon/resend.js +72 -0
- package/lib/quoxon/roster.js +117 -17
- package/lib/quoxon/route.js +6 -5
- package/lib/quoxon/rows.js +954 -0
- package/lib/quoxon/rowsStore.js +80 -0
- package/lib/quoxon/screenContext.js +150 -0
- package/lib/quoxon/screenFrames.js +254 -0
- package/lib/quoxon/serverChip.js +42 -0
- package/lib/quoxon/session.js +182 -2
- package/lib/quoxon/sessionDead.js +17 -0
- package/lib/quoxon/signin.js +125 -0
- package/lib/quoxon/slash/agents.js +112 -0
- package/lib/quoxon/slash/argSources.js +65 -0
- package/lib/quoxon/slash/args.js +65 -0
- package/lib/quoxon/slash/automate.js +89 -0
- package/lib/quoxon/slash/broadcast.js +140 -0
- package/lib/quoxon/slash/broadcastRun.js +108 -0
- package/lib/quoxon/slash/card.js +30 -0
- package/lib/quoxon/slash/challenge.js +114 -0
- package/lib/quoxon/slash/cliCommand.js +94 -0
- package/lib/quoxon/slash/cliLeaves.json +1023 -0
- package/lib/quoxon/slash/compact.js +1 -1
- package/lib/quoxon/slash/confirm.js +30 -0
- package/lib/quoxon/slash/crystallize.js +69 -0
- package/lib/quoxon/slash/dashboard.js +136 -0
- package/lib/quoxon/slash/draft.js +54 -0
- package/lib/quoxon/slash/evidenceCmd.js +34 -0
- package/lib/quoxon/slash/hatWalk.js +206 -0
- package/lib/quoxon/slash/inverses.js +77 -0
- package/lib/quoxon/slash/killer.js +24 -0
- package/lib/quoxon/slash/killerApi.js +16 -0
- package/lib/quoxon/slash/ledger.js +13 -8
- package/lib/quoxon/slash/listView.js +3 -1
- package/lib/quoxon/slash/modelCmd.js +261 -0
- package/lib/quoxon/slash/palette.js +22 -8
- package/lib/quoxon/slash/paletteController.js +117 -7
- package/lib/quoxon/slash/panels.js +21 -6
- package/lib/quoxon/slash/picker.js +20 -2
- package/lib/quoxon/slash/qpeCmd.js +35 -0
- package/lib/quoxon/slash/restart.js +95 -13
- package/lib/quoxon/slash/resume.js +18 -1
- package/lib/quoxon/slash/rowCmd.js +157 -0
- package/lib/quoxon/slash/rowWalk.js +175 -0
- package/lib/quoxon/slash/settings.js +16 -6
- package/lib/quoxon/slash/spawn.js +177 -0
- package/lib/quoxon/slash/stepUp.js +274 -0
- package/lib/quoxon/slash/stepUpCard.js +60 -0
- package/lib/quoxon/slash/stepUpCmds.js +78 -0
- package/lib/quoxon/slash/todoCmd.js +122 -0
- package/lib/quoxon/slash/undoApi.js +38 -0
- package/lib/quoxon/slash/undoCmd.js +49 -0
- package/lib/quoxon/slash/undoWish.js +15 -0
- package/lib/quoxon/slash/watch.js +164 -0
- package/lib/quoxon/slash/wish.js +23 -0
- package/lib/quoxon/slash/wishCmd.js +32 -0
- package/lib/quoxon/slotChat.js +300 -0
- package/lib/quoxon/slotFormat.js +195 -0
- package/lib/quoxon/slotRun.js +131 -0
- package/lib/quoxon/sound/mapping.js +2 -1
- package/lib/quoxon/stray.js +22 -0
- package/lib/quoxon/suggestCommand.js +90 -0
- package/lib/quoxon/term.js +46 -11
- package/lib/quoxon/textBuffer.js +176 -2
- package/lib/quoxon/theme.js +58 -25
- package/lib/quoxon/tips.js +433 -0
- package/lib/quoxon/toolCards.js +30 -0
- package/lib/quoxon/toolError.js +41 -0
- package/lib/quoxon/transcript.js +42 -4
- package/lib/quoxon/web/qface.js +18 -10
- package/lib/quoxon/welcome.js +1 -1
- package/lib/quoxon/whatsnew.js +136 -0
- package/lib/quoxon/whatsnew.jsonl +134 -0
- package/lib/quoxon/windowOrg.js +123 -0
- package/lib/server/devServers.js +137 -0
- package/lib/server/diag.js +129 -0
- package/lib/server/doctor.js +71 -0
- package/lib/server/install.js +149 -0
- package/lib/server/pluginEnable.js +161 -0
- package/lib/server/skew.js +56 -0
- package/lib/server/snapshot.js +131 -0
- package/lib/server/status.js +128 -0
- package/lib/server/sys.js +65 -0
- package/lib/server/uninstall.js +63 -0
- package/lib/server/update.js +234 -0
- package/lib/toolOutputShape.js +315 -0
- package/lib/tui/views/HyperionDeck.jsx +79 -0
- package/lib/tui/views/LucidDeck.jsx +209 -217
- package/package.json +5 -1
- 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/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/factory/svg-rig.mjs +63 -0
- package/vendor/dragonlab/integration/companions-v1/characters.mjs +88 -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/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/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/core/websiteSession.js +0 -148
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* C4: `quox server update` and `quox server rollback`.
|
|
3
|
+
*
|
|
4
|
+
* update: preflight -> backup -> fetch -> build -> migrate -> health, with the state written after every
|
|
5
|
+
* step (resumable: a re-run continues after the last completed step). A failure from fetch on
|
|
6
|
+
* rolls back automatically: before `migrate` only the code moves back (data was never touched);
|
|
7
|
+
* from `migrate` on, the full rollback restores the pre-update snapshot.
|
|
8
|
+
* rollback: previous snapshot + previous version: checkout, build, restore DB + config, start, health.
|
|
9
|
+
*
|
|
10
|
+
* Migrations run on service boot (each service applies its own on start), so `migrate` = `up -d`, and
|
|
11
|
+
* `health` proves they finished: every service answers and the collector's /version is the new commit.
|
|
12
|
+
*/
|
|
13
|
+
import { join } from 'node:path';
|
|
14
|
+
import { existsSync } from 'node:fs';
|
|
15
|
+
import { readInstalled, readEnvKeys, fetchAvailable, cmpSemver, parseSemver, vtag, compareUrl, releaseNotesUrl, PUBLIC_REPO, SIBLINGS } from './install.js';
|
|
16
|
+
import { readState, writeState, appendHistory, compose, diskFree, runningVersion, probeHttp } from './status.js';
|
|
17
|
+
import { createSnapshot, restoreSnapshot, latestSnapshot, listSnapshots } from './snapshot.js';
|
|
18
|
+
import { gate } from './sys.js';
|
|
19
|
+
|
|
20
|
+
export const UPDATE_STEPS = ['preflight', 'backup', 'fetch', 'build', 'migrate', 'health'];
|
|
21
|
+
export const MIN_FREE_GB = 5;
|
|
22
|
+
|
|
23
|
+
const sleepDefault = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
24
|
+
const lastLine = (s) => String(s || '').trim().split('\n').pop();
|
|
25
|
+
|
|
26
|
+
/** Poll auth + collector /health, and (when known) the collector /version == expectCommit. */
|
|
27
|
+
export async function waitHealthy({ urls, fetchImpl, expectCommit = null, tries = 40, intervalMs = 3000, sleep = sleepDefault, log = () => {} }) {
|
|
28
|
+
let last = '';
|
|
29
|
+
for (let i = 0; i < Math.max(1, tries); i++) {
|
|
30
|
+
const http = await probeHttp(urls, fetchImpl);
|
|
31
|
+
const bad = http.filter((h) => !h.ok);
|
|
32
|
+
if (!bad.length) {
|
|
33
|
+
if (!expectCommit) return { ok: true };
|
|
34
|
+
const v = await runningVersion(urls.collector, fetchImpl);
|
|
35
|
+
if (v.ok && v.commit && (String(v.commit).startsWith(expectCommit) || String(expectCommit).startsWith(v.commit))) return { ok: true, commit: v.commit };
|
|
36
|
+
last = v.ok ? `collector reports ${v.commit}, expected ${expectCommit}` : `collector /version: ${v.error}`;
|
|
37
|
+
} else last = bad.map((h) => `${h.name}: ${h.detail}`).join(', ');
|
|
38
|
+
log(`waiting for health: ${last}`);
|
|
39
|
+
if (i + 1 < tries) await sleep(intervalMs);
|
|
40
|
+
}
|
|
41
|
+
return { ok: false, error: last || 'not healthy' };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function checkoutCode({ install, g, ref, siblings = {}, pullSiblings = false, log }) {
|
|
45
|
+
const r = await g.mut('git', ['-C', install.repoDir, 'checkout', '--detach', ref]);
|
|
46
|
+
if (r.code !== 0) return { ok: false, error: `git checkout ${ref} failed: ${lastLine(r.stderr)}` };
|
|
47
|
+
for (const s of SIBLINGS) {
|
|
48
|
+
const d = join(install.root, s);
|
|
49
|
+
if (!existsSync(join(d, '.git'))) continue;
|
|
50
|
+
const sr = pullSiblings ? await g.mut('git', ['-C', d, 'pull', '--ff-only'])
|
|
51
|
+
: siblings[s] ? await g.mut('git', ['-C', d, 'checkout', '--detach', siblings[s]]) : { code: 0 };
|
|
52
|
+
if (sr.code !== 0) log(`warning: ${s} did not move (${lastLine(sr.stderr) || `exit ${sr.code}`}); it stays where it was`);
|
|
53
|
+
}
|
|
54
|
+
return { ok: true };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function composeBuild({ install, g, commit }) {
|
|
58
|
+
const gid = readEnvKeys(install.repoDir, ['DOCKER_GID']).DOCKER_GID;
|
|
59
|
+
const r = await g.mut('docker', compose(install, ['build']), { cwd: install.repoDir, env: { QUOXCORE_VERSION: commit, ...(gid ? { DOCKER_GID: gid } : {}) } });
|
|
60
|
+
return r.code === 0 ? { ok: true } : { ok: false, error: `docker compose build failed (exit ${r.code}): ${lastLine(r.stderr)}` };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function composeUp({ install, g }) {
|
|
64
|
+
const r = await g.mut('docker', compose(install, ['up', '-d', '--remove-orphans']), { cwd: install.repoDir });
|
|
65
|
+
return r.code === 0 ? { ok: true } : { ok: false, error: `docker compose up -d failed (exit ${r.code}): ${lastLine(r.stderr)}` };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Plan + run an update. opts: { install, sys, backups, urls, fetchImpl, dryRun, to, force, restart, noRollback,
|
|
70
|
+
* log, now, healthTries, healthIntervalMs, sleep, repo }
|
|
71
|
+
* -> { ok, upToDate?, dryRun?, plan, planned?, failedStep?, error?, rolledBack?, rollback? }
|
|
72
|
+
*/
|
|
73
|
+
export async function runUpdate(opts) {
|
|
74
|
+
const { install, sys, backups, urls, fetchImpl = globalThis.fetch, dryRun = false, force = false, restart = false, noRollback = false,
|
|
75
|
+
log = () => {}, now = () => new Date(), healthTries = 40, healthIntervalMs = 3000, sleep = sleepDefault, repo = PUBLIC_REPO } = opts;
|
|
76
|
+
const g = gate(sys, { dryRun, log });
|
|
77
|
+
|
|
78
|
+
// ---- preflight (read-only) ----------------------------------------------------------------------
|
|
79
|
+
const installed = await readInstalled(install, sys);
|
|
80
|
+
let target = opts.to ? vtag(opts.to) : null;
|
|
81
|
+
if (!target) {
|
|
82
|
+
const av = await fetchAvailable({ fetchImpl, repo });
|
|
83
|
+
if (!av.ok) return { ok: false, failedStep: 'preflight', error: `could not read the available versions: ${av.error}` };
|
|
84
|
+
target = av.latest;
|
|
85
|
+
} else if (!parseSemver(target)) return { ok: false, failedStep: 'preflight', error: `--to ${opts.to} is not a version like 4.3.0` };
|
|
86
|
+
const current = installed.release ? vtag(installed.release) : null;
|
|
87
|
+
const plan = {
|
|
88
|
+
current, currentCommit: installed.commit, target,
|
|
89
|
+
changes: current ? compareUrl(current, target, repo) : null, releaseNotes: releaseNotesUrl(target, repo),
|
|
90
|
+
backups, steps: UPDATE_STEPS
|
|
91
|
+
};
|
|
92
|
+
const cmp = current ? cmpSemver(target, current) : 1;
|
|
93
|
+
if (cmp !== null && cmp <= 0 && !force) return { ok: true, upToDate: true, plan };
|
|
94
|
+
|
|
95
|
+
const problems = [];
|
|
96
|
+
const forceable = [];
|
|
97
|
+
if (installed.dirty) forceable.push(`the checkout at ${install.repoDir} has local changes (git status); commit or discard them, or pass --force`);
|
|
98
|
+
if (installed.dirty === null) problems.push(`${install.repoDir} is not a git checkout, so it cannot be moved to ${target}`);
|
|
99
|
+
const disk = await diskFree(install.root, sys);
|
|
100
|
+
if (disk.ok && disk.freeGB < MIN_FREE_GB) forceable.push(`only ${disk.freeGB} GB free at ${install.root}; an update needs at least ${MIN_FREE_GB} GB (backup + images)`);
|
|
101
|
+
const dv = await sys.run('docker', ['compose', 'version', '--short']);
|
|
102
|
+
if (dv.code !== 0) problems.push('docker compose is not available to this user');
|
|
103
|
+
plan.problems = [...problems, ...forceable];
|
|
104
|
+
if (problems.length || (forceable.length && !force)) return { ok: false, failedStep: 'preflight', error: plan.problems.join('; '), plan };
|
|
105
|
+
|
|
106
|
+
// ---- resume -------------------------------------------------------------------------------------
|
|
107
|
+
const prev = readState(backups);
|
|
108
|
+
let state = null;
|
|
109
|
+
if (prev && prev.op === 'update' && prev.status === 'running') {
|
|
110
|
+
if (prev.target !== target && !restart) {
|
|
111
|
+
return { ok: false, failedStep: 'preflight', error: `an update to ${prev.target} stopped after ${prev.done.slice(-1)[0] || 'preflight'}. Re-run without --to to resume it, or pass --restart to abandon it.`, plan };
|
|
112
|
+
}
|
|
113
|
+
if (prev.target === target) {
|
|
114
|
+
state = prev;
|
|
115
|
+
plan.resumedAfter = state.done.slice(-1)[0] || 'preflight';
|
|
116
|
+
log(`resuming the update to ${target} after ${plan.resumedAfter}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (!state) state = { op: 'update', status: 'running', target, from: { release: installed.release, commit: installed.commit, siblings: installed.siblings }, done: ['preflight'], snapshot: null, startedAt: now().toISOString() };
|
|
120
|
+
const save = () => { if (!dryRun) writeState(backups, { ...state, updatedAt: now().toISOString() }); };
|
|
121
|
+
const mark = (s) => { if (!state.done.includes(s)) state.done.push(s); save(); };
|
|
122
|
+
const doneStep = (s) => state.done.includes(s);
|
|
123
|
+
const finish = (status, extra = {}) => {
|
|
124
|
+
state.status = status; save();
|
|
125
|
+
if (!dryRun) appendHistory(backups, { op: 'update', status, at: now().toISOString(), from: state.from.release || state.from.commit, to: target, snapshot: state.snapshot, ...extra });
|
|
126
|
+
};
|
|
127
|
+
save();
|
|
128
|
+
|
|
129
|
+
const fail = async (step, error) => {
|
|
130
|
+
log(`✖ ${step}: ${error}`);
|
|
131
|
+
if (noRollback || dryRun) { finish('failed', { failedStep: step, error }); return { ok: false, failedStep: step, error, plan, rolledBack: false }; }
|
|
132
|
+
log('rolling back automatically');
|
|
133
|
+
let rb;
|
|
134
|
+
if (['fetch', 'build'].includes(step)) {
|
|
135
|
+
const c = await checkoutCode({ install, g, ref: state.from.commit, siblings: state.from.siblings, log });
|
|
136
|
+
const h = c.ok ? await waitHealthy({ urls, fetchImpl, tries: healthTries, intervalMs: healthIntervalMs, sleep, log }) : c;
|
|
137
|
+
rb = { ok: c.ok && h.ok, scope: 'code only (data was never touched)', error: c.ok ? h.error : c.error };
|
|
138
|
+
} else {
|
|
139
|
+
rb = await runRollback({ ...opts, snapshotDir: state.snapshot, fromUpdate: true });
|
|
140
|
+
rb.scope = 'code + data from the pre-update snapshot';
|
|
141
|
+
}
|
|
142
|
+
finish(rb.ok ? 'rolled-back' : 'rollback-failed', { failedStep: step, error });
|
|
143
|
+
return { ok: false, failedStep: step, error, plan, rolledBack: rb.ok, rollback: rb };
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
// ---- backup -------------------------------------------------------------------------------------
|
|
147
|
+
if (!doneStep('backup')) {
|
|
148
|
+
const snap = await createSnapshot({ install, installed, backups, g, reason: `before update to ${target}`, now, log });
|
|
149
|
+
if (!snap.ok) {
|
|
150
|
+
finish('failed', { failedStep: 'backup', error: snap.error });
|
|
151
|
+
return { ok: false, failedStep: 'backup', error: `${snap.error}. Nothing was changed.`, plan, rolledBack: false };
|
|
152
|
+
}
|
|
153
|
+
state.snapshot = snap.dir; mark('backup');
|
|
154
|
+
}
|
|
155
|
+
plan.snapshot = state.snapshot;
|
|
156
|
+
|
|
157
|
+
// ---- fetch --------------------------------------------------------------------------------------
|
|
158
|
+
if (!doneStep('fetch')) {
|
|
159
|
+
const tagRef = `refs/tags/${target}`;
|
|
160
|
+
const has = await g.ro('git', ['-C', install.repoDir, 'rev-parse', '-q', '--verify', tagRef]);
|
|
161
|
+
if (has.code !== 0) {
|
|
162
|
+
const f1 = await g.mut('git', ['-C', install.repoDir, 'fetch', '--tags', 'origin']);
|
|
163
|
+
const again = dryRun ? { code: 1 } : await g.ro('git', ['-C', install.repoDir, 'rev-parse', '-q', '--verify', tagRef]);
|
|
164
|
+
if (f1.code !== 0 || again.code !== 0) {
|
|
165
|
+
const f2 = await g.mut('git', ['-C', install.repoDir, 'fetch', `https://github.com/${repo}.git`, `+${tagRef}:${tagRef}`]);
|
|
166
|
+
if (f2.code !== 0) return fail('fetch', `could not fetch ${target} from origin or ${repo}`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
const c = await checkoutCode({ install, g, ref: target, pullSiblings: true, log });
|
|
170
|
+
if (!c.ok) return fail('fetch', c.error);
|
|
171
|
+
mark('fetch');
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ---- build --------------------------------------------------------------------------------------
|
|
175
|
+
const rc = await g.ro('git', ['-C', install.repoDir, 'rev-parse', '--short', `${target}^{commit}`]);
|
|
176
|
+
const newCommit = rc.code === 0 && rc.stdout.trim() ? rc.stdout.trim() : null;
|
|
177
|
+
plan.targetCommit = newCommit;
|
|
178
|
+
if (!doneStep('build')) {
|
|
179
|
+
const b = await composeBuild({ install, g, commit: newCommit || target });
|
|
180
|
+
if (!b.ok) return fail('build', b.error);
|
|
181
|
+
mark('build');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ---- migrate (on boot) --------------------------------------------------------------------------
|
|
185
|
+
if (!doneStep('migrate')) {
|
|
186
|
+
const u = await composeUp({ install, g });
|
|
187
|
+
if (!u.ok) return fail('migrate', u.error);
|
|
188
|
+
mark('migrate');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ---- health -------------------------------------------------------------------------------------
|
|
192
|
+
if (dryRun) {
|
|
193
|
+
log(`[dry run] would wait for auth + collector /health and the collector /version to report ${newCommit || target}`);
|
|
194
|
+
return { ok: true, dryRun: true, plan, planned: g.planned };
|
|
195
|
+
}
|
|
196
|
+
const h = await waitHealthy({ urls, fetchImpl, expectCommit: newCommit, tries: healthTries, intervalMs: healthIntervalMs, sleep, log });
|
|
197
|
+
if (!h.ok) return fail('health', h.error);
|
|
198
|
+
mark('health');
|
|
199
|
+
finish('ok');
|
|
200
|
+
return { ok: true, plan };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** opts: { install, sys, backups, urls, fetchImpl, dryRun, snapshotDir, log, healthTries, ... } */
|
|
204
|
+
export async function runRollback(opts) {
|
|
205
|
+
const { install, sys, backups, urls, fetchImpl = globalThis.fetch, dryRun = false, log = () => {}, now = () => new Date(),
|
|
206
|
+
healthTries = 40, healthIntervalMs = 3000, sleep = sleepDefault, fromUpdate = false } = opts;
|
|
207
|
+
const g = gate(sys, { dryRun, log });
|
|
208
|
+
const snaps = listSnapshots(backups);
|
|
209
|
+
const snap = opts.snapshotDir ? snaps.find((s) => s.dir === opts.snapshotDir || s.dir.endsWith(`/${opts.snapshotDir}`)) : latestSnapshot(backups);
|
|
210
|
+
if (!snap) return { ok: false, error: opts.snapshotDir ? `no complete snapshot at ${opts.snapshotDir}` : `no snapshot in ${backups}; nothing to roll back to` };
|
|
211
|
+
const plan = { snapshot: snap.dir, release: snap.release, commit: snap.commit, createdAt: snap.createdAt };
|
|
212
|
+
const record = (status, extra = {}) => {
|
|
213
|
+
if (!dryRun && !fromUpdate) appendHistory(backups, { op: 'rollback', status, at: now().toISOString(), to: snap.release || snap.commit, snapshot: snap.dir, ...extra });
|
|
214
|
+
};
|
|
215
|
+
if (!snap.commit) return { ok: false, error: 'the snapshot does not record which commit it came from', plan };
|
|
216
|
+
|
|
217
|
+
const c = await checkoutCode({ install, g, ref: snap.commit, siblings: snap.siblings, log });
|
|
218
|
+
if (!c.ok) { record('failed', { error: c.error }); return { ok: false, step: 'checkout', error: c.error, plan }; }
|
|
219
|
+
const b = await composeBuild({ install, g, commit: snap.commit });
|
|
220
|
+
if (!b.ok) { record('failed', { error: b.error }); return { ok: false, step: 'build', error: b.error, plan }; }
|
|
221
|
+
const r = await restoreSnapshot({ install, snap, g, sleep });
|
|
222
|
+
if (!r.ok) { record('failed', { error: r.error }); return { ok: false, step: 'restore', error: r.error, plan }; }
|
|
223
|
+
const u = await composeUp({ install, g });
|
|
224
|
+
if (!u.ok) { record('failed', { error: u.error }); return { ok: false, step: 'start', error: u.error, plan }; }
|
|
225
|
+
if (dryRun) {
|
|
226
|
+
log(`[dry run] would wait for health and the collector /version to report ${snap.commit}`);
|
|
227
|
+
return { ok: true, dryRun: true, plan, planned: g.planned };
|
|
228
|
+
}
|
|
229
|
+
const h = await waitHealthy({ urls, fetchImpl, expectCommit: snap.commit, tries: healthTries, intervalMs: healthIntervalMs, sleep, log });
|
|
230
|
+
if (!h.ok) { record('failed', { error: h.error }); return { ok: false, step: 'health', error: h.error, plan }; }
|
|
231
|
+
if (!fromUpdate) writeState(backups, { op: 'rollback', status: 'ok', snapshot: snap.dir, at: now().toISOString() });
|
|
232
|
+
record('ok');
|
|
233
|
+
return { ok: true, plan };
|
|
234
|
+
}
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #970: governed output shaping for `quox tool exec` (--select / --summary / --limit).
|
|
3
|
+
*
|
|
4
|
+
* On the chat CLI-tools path every shell call except `quox tool exec` is denied, so a model
|
|
5
|
+
* facing a large result (wazuh agents, seo urls, a 618 KB calendar overlay) has no honest
|
|
6
|
+
* way to narrow it. This is that way, inside the governed call: the path grammar is the
|
|
7
|
+
* safe evaluator QuoxOn rows already use (parsePath: a.b, a[0], list[field=value], list[field~text], list[*], {a,b},
|
|
8
|
+
* count(path)), no eval and no new dependency. The shape is sent to the collector so the
|
|
9
|
+
* receipt records what view of the result the caller was shown.
|
|
10
|
+
*
|
|
11
|
+
* Honesty rules: a truncated list always says "showing N of M"; a path that matches
|
|
12
|
+
* nothing says so rather than returning null; a malformed path is an error BEFORE the
|
|
13
|
+
* tool runs.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { parsePath } from './quoxon/rows.js';
|
|
17
|
+
|
|
18
|
+
const BLOCKED = new Set(['__proto__', 'constructor', 'prototype']);
|
|
19
|
+
const GRAMMAR = 'use a.b, a[0], list[field=value] (exact), list[field~text] (contains, any case), list[*].field (that field of every item), list[*].{a,b} (several fields of every item, one call), list[field=value].name, count(list), or several paths at once: total,count(list)';
|
|
20
|
+
export const DEFAULT_SAMPLE = 10;
|
|
21
|
+
const COUNT_KEY_RE = /^(status|state|type|kind|level|severity|health|healthy|enabled|active|category|source|platform|os|role|priority|result|outcome|connected|online)$/i;
|
|
22
|
+
|
|
23
|
+
// Split on commas outside [], {} and (): "total, count(a[x=1]), b.{c,d}" -> three paths. null when there is no
|
|
24
|
+
// top-level comma (a single path), [] entries kept so an empty part is an error, never silently dropped.
|
|
25
|
+
function splitTopLevel(e) {
|
|
26
|
+
const parts = [];
|
|
27
|
+
let depth = 0, start = 0;
|
|
28
|
+
for (let i = 0; i < e.length; i++) {
|
|
29
|
+
const c = e[i];
|
|
30
|
+
if (c === '[' || c === '{' || c === '(') depth++;
|
|
31
|
+
else if (c === ']' || c === '}' || c === ')') depth = Math.max(0, depth - 1);
|
|
32
|
+
else if (c === ',' && depth === 0) { parts.push(e.slice(start, i).trim()); start = i + 1; }
|
|
33
|
+
}
|
|
34
|
+
if (!parts.length) return null;
|
|
35
|
+
parts.push(e.slice(start).trim());
|
|
36
|
+
return parts;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function parseSelect(expr) {
|
|
40
|
+
const e = String(expr ?? '').trim();
|
|
41
|
+
// GUIDES-FIX: several paths in one call ("total,active_total"): each must be a valid path on its own.
|
|
42
|
+
const parts = splitTopLevel(e);
|
|
43
|
+
if (parts) {
|
|
44
|
+
if (parts.some((x) => !x)) return { error: `--select: empty path in ${JSON.stringify(e)}; separate several paths with commas, e.g. total,count(list); ${GRAMMAR}` };
|
|
45
|
+
const multi = [];
|
|
46
|
+
for (const x of parts) {
|
|
47
|
+
const p = parseSelect(x);
|
|
48
|
+
if (p.error) return p;
|
|
49
|
+
multi.push({ expr: x, ...p });
|
|
50
|
+
}
|
|
51
|
+
return { multi };
|
|
52
|
+
}
|
|
53
|
+
const fn = /^([A-Za-z_]+)\s*\(([\s\S]*)\)$/.exec(e);
|
|
54
|
+
if (fn) {
|
|
55
|
+
if (fn[1] !== 'count') return { error: `--select: unknown function ${fn[1]}() (only count); ${GRAMMAR}` };
|
|
56
|
+
const steps = parsePath(fn[2]);
|
|
57
|
+
return steps ? { fn: 'count', steps } : { error: `--select: count() needs a path; ${GRAMMAR}` };
|
|
58
|
+
}
|
|
59
|
+
const steps = parsePath(e);
|
|
60
|
+
return steps ? { steps } : { error: `--select: bad path ${JSON.stringify(e)}; ${GRAMMAR}` };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function parseLimit(v) {
|
|
64
|
+
if (v === undefined || v === null || v === true) return { value: undefined };
|
|
65
|
+
const n = Number(v);
|
|
66
|
+
if (!Number.isInteger(n) || n < 1 || n > 10000) return { error: `--limit: expected a whole number from 1 to 10000, got ${JSON.stringify(String(v))}` };
|
|
67
|
+
return { value: n };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Validate a shape before the tool runs -> error string or null. */
|
|
71
|
+
export function checkShape({ select, limit } = {}) {
|
|
72
|
+
if (select !== undefined) { const p = parseSelect(select); if (p.error) return p.error; }
|
|
73
|
+
const l = parseLimit(limit);
|
|
74
|
+
return l.error || null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** The content-free record of the shape, as sent to the collector for the receipt. */
|
|
78
|
+
export function shapeRecord({ select, summary, limit } = {}) {
|
|
79
|
+
const r = {};
|
|
80
|
+
if (select !== undefined) r.select = String(select);
|
|
81
|
+
if (summary) r.summary = true;
|
|
82
|
+
const l = parseLimit(limit);
|
|
83
|
+
if (l.value !== undefined) r.limit = l.value;
|
|
84
|
+
return Object.keys(r).length ? r : null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const isObj = (o) => o !== null && typeof o === 'object' && !Array.isArray(o);
|
|
88
|
+
const own = (o, k) => isObj(o) && !BLOCKED.has(k) && Object.hasOwn(o, k);
|
|
89
|
+
|
|
90
|
+
/** Like the rows evaluator, plus projection: a key step on a list maps over its items. */
|
|
91
|
+
function walk(data, steps) {
|
|
92
|
+
let cur = data;
|
|
93
|
+
for (const st of steps) {
|
|
94
|
+
if (cur === undefined || cur === null) return undefined;
|
|
95
|
+
if ('key' in st) {
|
|
96
|
+
if (Array.isArray(cur)) cur = cur.filter((it) => own(it, st.key)).map((it) => it[st.key]);
|
|
97
|
+
else { if (!own(cur, st.key)) return undefined; cur = cur[st.key]; }
|
|
98
|
+
} else if ('index' in st) {
|
|
99
|
+
if (!Array.isArray(cur)) return undefined; cur = cur[st.index];
|
|
100
|
+
} else if (st.wildcard) {
|
|
101
|
+
// list[*]: every item; the key steps after it already map over a list.
|
|
102
|
+
if (!Array.isArray(cur)) return undefined;
|
|
103
|
+
} else if (st.fields) {
|
|
104
|
+
// #1044: {a,b}: only these fields, of every item on a list. Blocked keys are never copied.
|
|
105
|
+
const pick = (o) => { const r = {}; for (const f of st.fields) if (own(o, f)) r[f] = o[f]; return r; };
|
|
106
|
+
cur = Array.isArray(cur) ? cur.map((it) => (isObj(it) ? pick(it) : {})) : isObj(cur) ? pick(cur) : undefined;
|
|
107
|
+
} else {
|
|
108
|
+
if (!Array.isArray(cur)) return undefined;
|
|
109
|
+
const sub = parsePath(st.field);
|
|
110
|
+
cur = cur.filter((it) => {
|
|
111
|
+
const v = sub ? walk(it, sub) : undefined;
|
|
112
|
+
if (v === undefined || v === null || typeof v === 'object') return false;
|
|
113
|
+
return 'contains' in st ? String(v).toLowerCase().includes(st.contains) : String(v) === st.value;
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return cur;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const countOf = (v) => (Array.isArray(v) ? v.length : isObj(v) ? Object.keys(v).length : v === undefined ? 0 : 1);
|
|
121
|
+
|
|
122
|
+
/** The main list of a value: itself, else its largest array (top level, then one level down). */
|
|
123
|
+
function mainList(value) {
|
|
124
|
+
if (Array.isArray(value)) return { path: '', list: value };
|
|
125
|
+
if (!isObj(value)) return null;
|
|
126
|
+
let best = null;
|
|
127
|
+
const consider = (path, v) => { if (Array.isArray(v) && (!best || v.length > best.list.length)) best = { path, list: v }; };
|
|
128
|
+
for (const [k, v] of Object.entries(value)) consider(k, v);
|
|
129
|
+
if (!best) for (const [k, v] of Object.entries(value)) if (isObj(v)) for (const [k2, v2] of Object.entries(v)) consider(`${k}.${k2}`, v2);
|
|
130
|
+
return best;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function fieldCounts(list) {
|
|
134
|
+
const counts = {};
|
|
135
|
+
const keys = new Set();
|
|
136
|
+
for (const it of list) if (isObj(it)) for (const k of Object.keys(it)) if (COUNT_KEY_RE.test(k)) keys.add(k);
|
|
137
|
+
for (const k of keys) {
|
|
138
|
+
const c = {};
|
|
139
|
+
for (const it of list) {
|
|
140
|
+
const v = isObj(it) ? it[k] : undefined;
|
|
141
|
+
if (v === undefined || v === null || typeof v === 'object') continue;
|
|
142
|
+
const key = String(v);
|
|
143
|
+
c[key] = (c[key] || 0) + 1;
|
|
144
|
+
}
|
|
145
|
+
if (Object.keys(c).length && Object.keys(c).length <= 20) counts[k] = c;
|
|
146
|
+
}
|
|
147
|
+
return counts;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Honest summary: total, per-field counts where obvious, a bounded sample, "showing N of M". */
|
|
151
|
+
export function summarize(value, { limit = DEFAULT_SAMPLE } = {}) {
|
|
152
|
+
const found = mainList(value);
|
|
153
|
+
if (!found) {
|
|
154
|
+
return isObj(value)
|
|
155
|
+
? { note: 'no list in this result; showing its keys', keys: Object.keys(value) }
|
|
156
|
+
: { note: 'no list in this result', value };
|
|
157
|
+
}
|
|
158
|
+
const M = found.list.length;
|
|
159
|
+
const N = Math.min(limit, M);
|
|
160
|
+
const out = { list_path: found.path || '(root)', total: M, showing: N, note: `showing ${N} of ${M}` };
|
|
161
|
+
const counts = fieldCounts(found.list);
|
|
162
|
+
if (Object.keys(counts).length) out.counts = counts;
|
|
163
|
+
if (isObj(value)) {
|
|
164
|
+
const scalars = Object.fromEntries(Object.entries(value).filter(([, v]) => v === null || typeof v !== 'object'));
|
|
165
|
+
if (Object.keys(scalars).length) out.fields = scalars;
|
|
166
|
+
}
|
|
167
|
+
out.sample = found.list.slice(0, N);
|
|
168
|
+
return out;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function truncate(value, n) {
|
|
172
|
+
if (Array.isArray(value)) {
|
|
173
|
+
if (value.length <= n) return value;
|
|
174
|
+
return { total: value.length, showing: n, note: `showing ${n} of ${value.length}`, items: value.slice(0, n) };
|
|
175
|
+
}
|
|
176
|
+
const found = mainList(value);
|
|
177
|
+
if (!found || !found.path || found.list.length <= n) return value;
|
|
178
|
+
const copy = structuredClone(value);
|
|
179
|
+
const parts = found.path.split('.');
|
|
180
|
+
let host = copy;
|
|
181
|
+
for (const p of parts.slice(0, -1)) host = host[p];
|
|
182
|
+
host[parts[parts.length - 1]] = found.list.slice(0, n);
|
|
183
|
+
copy._truncated = { path: found.path, total: found.list.length, showing: n, note: `showing ${n} of ${found.list.length}` };
|
|
184
|
+
return copy;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function asData(raw) {
|
|
188
|
+
if (typeof raw !== 'string') return raw;
|
|
189
|
+
const t = raw.trim();
|
|
190
|
+
if (t.startsWith('{') || t.startsWith('[')) { try { return JSON.parse(t); } catch { /* text */ } }
|
|
191
|
+
return raw;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Apply select, then summary or limit. -> { value } (throws on a malformed shape). */
|
|
195
|
+
export function shapeOutput(raw, shape = {}) {
|
|
196
|
+
const err = checkShape(shape);
|
|
197
|
+
if (err) throw new Error(err);
|
|
198
|
+
let value = asData(raw);
|
|
199
|
+
if (shape.select !== undefined) {
|
|
200
|
+
const p = parseSelect(shape.select);
|
|
201
|
+
if (p.multi) {
|
|
202
|
+
// each path under its own expression; a path that matched nothing is named, never dropped
|
|
203
|
+
const out = {};
|
|
204
|
+
const unmatched = [];
|
|
205
|
+
for (const q of p.multi) {
|
|
206
|
+
const v = walk(value, q.steps);
|
|
207
|
+
if (q.fn === 'count') out[q.expr] = countOf(v);
|
|
208
|
+
else if (v === undefined) unmatched.push(q.expr);
|
|
209
|
+
else out[q.expr] = v;
|
|
210
|
+
}
|
|
211
|
+
if (!Object.keys(out).length) {
|
|
212
|
+
return { value: { select: String(shape.select), matched: false, note: 'none of the --select paths matched anything in the result', top_keys: isObj(value) ? Object.keys(value) : [] } };
|
|
213
|
+
}
|
|
214
|
+
if (unmatched.length) { out._unmatched = unmatched; out._note = `${unmatched.length} of ${p.multi.length} --select paths matched nothing in the result`; }
|
|
215
|
+
value = out;
|
|
216
|
+
} else {
|
|
217
|
+
const v = walk(value, p.steps);
|
|
218
|
+
if (p.fn === 'count') value = countOf(v);
|
|
219
|
+
else if (v === undefined) {
|
|
220
|
+
return { value: { select: String(shape.select), matched: false, note: 'the --select path matched nothing in the result', top_keys: isObj(value) ? Object.keys(value) : [] } };
|
|
221
|
+
} else value = v;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
const { value: limit } = parseLimit(shape.limit);
|
|
225
|
+
if (shape.summary) return { value: summarize(value, { limit: limit ?? DEFAULT_SAMPLE }) };
|
|
226
|
+
if (limit !== undefined) return { value: truncate(value, limit) };
|
|
227
|
+
return { value };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* BASH-FALLBACK: keep an unshaped result inline on the model-driven path.
|
|
232
|
+
*
|
|
233
|
+
* Claude Code saves Bash output above its inline limit (30000 chars by default) to a file
|
|
234
|
+
* and shows only a path plus a 2 KB preview. The model then reaches for jq/python/grep on
|
|
235
|
+
* that file, which the CLI-tools Bash guard denies (55 of 64 Bash attempts in the
|
|
236
|
+
* 2026-10-14/15 proof evidence). Capping here, below that limit, means the model never
|
|
237
|
+
* gets a file to post-process: it gets the honest --summary view plus how to re-run with
|
|
238
|
+
* --select. The cap only ever shrinks what is SHOWN; the call itself already ran governed.
|
|
239
|
+
*/
|
|
240
|
+
export const INLINE_MAX_CHARS = 20000;
|
|
241
|
+
|
|
242
|
+
const jsonLen = (v) => (typeof v === 'string' ? v.length : JSON.stringify(v, null, 2).length);
|
|
243
|
+
|
|
244
|
+
/** A {output: "<json>"} envelope (quox_cli and some tools) -> its parsed body, else null. */
|
|
245
|
+
function innerOutput(value) {
|
|
246
|
+
if (!isObj(value) || typeof value.output !== 'string') return null;
|
|
247
|
+
const parsed = asData(value.output);
|
|
248
|
+
return typeof parsed === 'string' ? null : parsed;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* FLEET-UNHEALTHY: the small non-scalar top-level fields beside the main list (a short "not_healthy" list, a
|
|
253
|
+
* "by_state" count object) are often the answer itself; the summary used to drop every non-scalar. Each kept
|
|
254
|
+
* whole or left out whole, never cut: at most SMALL_FIELD_MAX chars each and SMALL_FIELDS_MAX in total.
|
|
255
|
+
*/
|
|
256
|
+
const SMALL_FIELD_MAX = 6000;
|
|
257
|
+
const SMALL_FIELDS_MAX = 9000;
|
|
258
|
+
function smallTopFields(body, found) {
|
|
259
|
+
if (!isObj(body)) return null;
|
|
260
|
+
const mainKey = found && found.path ? found.path.split('.')[0] : null;
|
|
261
|
+
const out = {};
|
|
262
|
+
let used = 0;
|
|
263
|
+
for (const [k, v] of Object.entries(body)) {
|
|
264
|
+
if (k === mainKey || BLOCKED.has(k) || v === null || typeof v !== 'object') continue;
|
|
265
|
+
const n = jsonLen(v);
|
|
266
|
+
if (n > SMALL_FIELD_MAX || used + n > SMALL_FIELDS_MAX) continue;
|
|
267
|
+
out[k] = v;
|
|
268
|
+
used += n;
|
|
269
|
+
}
|
|
270
|
+
return Object.keys(out).length ? out : null;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export function capInline(value, { maxChars = INLINE_MAX_CHARS } = {}) {
|
|
274
|
+
const size = jsonLen(value);
|
|
275
|
+
if (size <= maxChars) return { value, capped: false };
|
|
276
|
+
const fullKb = Math.round(size / 1024);
|
|
277
|
+
const data = asData(value);
|
|
278
|
+
if (typeof data === 'string') {
|
|
279
|
+
const wrap = (keep) => ({
|
|
280
|
+
oversized: true,
|
|
281
|
+
full_size_kb: fullKb,
|
|
282
|
+
note: `showing the first ${keep} of ${data.length} characters`,
|
|
283
|
+
next: "Too large to show whole. Run the same call again with narrower --args, or --select '<path>' / --limit N if it is JSON. Never read or post-process a saved output with a shell.",
|
|
284
|
+
text: data.slice(0, keep),
|
|
285
|
+
});
|
|
286
|
+
// JSON escaping (newlines, quotes) inflates the text: shrink until the wrapper fits.
|
|
287
|
+
let keep = Math.max(0, maxChars - 1200);
|
|
288
|
+
let out = wrap(keep);
|
|
289
|
+
while (keep > 0 && jsonLen(out) > maxChars) { keep = Math.floor(keep * 0.8); out = wrap(keep); }
|
|
290
|
+
return { capped: true, value: out };
|
|
291
|
+
}
|
|
292
|
+
const inner = innerOutput(data);
|
|
293
|
+
const body = inner ?? data;
|
|
294
|
+
const via = inner ? 'output (a JSON string: --select cannot reach inside it, so narrow it with --args or the tool\'s own filters)' : null;
|
|
295
|
+
const found = mainList(body);
|
|
296
|
+
const lp = found && found.path ? found.path : (found ? '' : null);
|
|
297
|
+
const example = lp === null
|
|
298
|
+
? "--select '<key>'"
|
|
299
|
+
: `--select '${lp ? `${lp}[field=value].name` : '[0]'}', --select 'count(${lp || ''})' or --select '${lp ? `${lp}[*].field` : '[*].field'}'`;
|
|
300
|
+
const next = `Result is ${fullKb} KB, too large to show whole, so this is the --summary view. For the exact part you need, run the same quox tool exec again with ${example} (or --summary --limit N for a bigger sample)${via ? `; the list is inside ${via}` : ''}. Never read or post-process a saved output with a shell.`;
|
|
301
|
+
const head = { oversized: true, full_size_kb: fullKb, next, top_keys: isObj(body) ? Object.keys(body).slice(0, 40) : undefined };
|
|
302
|
+
const extra = smallTopFields(body, found);
|
|
303
|
+
for (const limit of [DEFAULT_SAMPLE, 5, 3, 1, 0]) {
|
|
304
|
+
const s = summarize(body, { limit });
|
|
305
|
+
const out = { ...head, ...s, ...(extra ? { fields: { ...(s.fields || {}), ...extra } } : {}) };
|
|
306
|
+
if (jsonLen(out) <= maxChars) return { capped: true, value: out };
|
|
307
|
+
}
|
|
308
|
+
for (const limit of [DEFAULT_SAMPLE, 5, 3, 1, 0]) {
|
|
309
|
+
const out = { ...head, ...summarize(body, { limit }) };
|
|
310
|
+
if (jsonLen(out) <= maxChars) return { capped: true, value: out };
|
|
311
|
+
}
|
|
312
|
+
// Even an empty sample is too big (a huge scalar field): keep only the counts and shape.
|
|
313
|
+
const s = summarize(body, { limit: 0 });
|
|
314
|
+
return { capped: true, value: { ...head, list_path: s.list_path, total: s.total, showing: 0, note: s.note || 'fields too large to show', counts: s.counts } };
|
|
315
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HyperionDeck: the full-screen live HYPERION activity view (`quox hyperion deck`), in the Quoxon look.
|
|
3
|
+
* Refreshes every intervalMs; r refreshes now, q quits. A failed fetch never blanks the screen: the last
|
|
4
|
+
* good data stays, marked STALE, with the error named in the header. Props are injected (api, intervalMs)
|
|
5
|
+
* so the component is testable without a network. api = { activity() }.
|
|
6
|
+
*
|
|
7
|
+
* Refresh props live in refs so the fetch loop is created once (the LUCID deck ran out of memory when a
|
|
8
|
+
* changing prop restarted its loop on every redraw).
|
|
9
|
+
*/
|
|
10
|
+
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
|
11
|
+
import { Box, Text, useInput, useApp, useStdout } from 'ink';
|
|
12
|
+
import { statusLines, summaryText } from '../../core/hyperion.js';
|
|
13
|
+
import { c, gradient } from '../../quoxon/theme.js';
|
|
14
|
+
|
|
15
|
+
function roleFor(line) {
|
|
16
|
+
if (/^(Node load|Pool|Recent ops)/.test(line)) return 'sky';
|
|
17
|
+
if (/^VMID/.test(line)) return 'label';
|
|
18
|
+
if (/unavailable|^⚠/.test(line)) return 'warn';
|
|
19
|
+
return 'value';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function HyperionDeck({ api, intervalMs = 10000 }) {
|
|
23
|
+
const { exit } = useApp();
|
|
24
|
+
const { stdout } = useStdout();
|
|
25
|
+
const rows = (stdout && stdout.rows) || 30;
|
|
26
|
+
const [data, setData] = useState(null);
|
|
27
|
+
const [err, setErr] = useState(null);
|
|
28
|
+
const [at, setAt] = useState(null);
|
|
29
|
+
const inflight = useRef(false);
|
|
30
|
+
const apiRef = useRef(api);
|
|
31
|
+
apiRef.current = api;
|
|
32
|
+
|
|
33
|
+
const refresh = useCallback(async () => {
|
|
34
|
+
if (inflight.current) return;
|
|
35
|
+
inflight.current = true;
|
|
36
|
+
try {
|
|
37
|
+
setData(await apiRef.current.activity());
|
|
38
|
+
setErr(null);
|
|
39
|
+
setAt(Date.now());
|
|
40
|
+
} catch (e) {
|
|
41
|
+
setErr(e);
|
|
42
|
+
} finally {
|
|
43
|
+
inflight.current = false;
|
|
44
|
+
}
|
|
45
|
+
}, []);
|
|
46
|
+
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
refresh();
|
|
49
|
+
const t = setInterval(refresh, intervalMs);
|
|
50
|
+
return () => clearInterval(t);
|
|
51
|
+
}, [refresh, intervalMs]);
|
|
52
|
+
|
|
53
|
+
useInput((input) => {
|
|
54
|
+
if (input === 'q') exit();
|
|
55
|
+
else if (input === 'r') refresh();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const lines = data ? statusLines(data, { now: Date.now() }) : [];
|
|
59
|
+
const shown = lines.slice(0, Math.max(5, rows - 4));
|
|
60
|
+
const stale = err && data;
|
|
61
|
+
return (
|
|
62
|
+
<Box flexDirection="column">
|
|
63
|
+
<Text wrap="truncate">
|
|
64
|
+
{gradient(' HYPERION ').map((g, i) => <Text key={i} bold color={g.color}>{g.ch}</Text>)}
|
|
65
|
+
<Text color={c('value')}> {data ? summaryText(data) : 'loading'}</Text>
|
|
66
|
+
{stale ? <Text color={c('warn')}> STALE: {String(err.message || err).slice(0, 60)}</Text> : null}
|
|
67
|
+
{err && !data ? <Text color={c('error')}> unreachable: {String(err.message || err).slice(0, 60)}</Text> : null}
|
|
68
|
+
</Text>
|
|
69
|
+
<Text> </Text>
|
|
70
|
+
{shown.map((l, i) => <Text key={i} wrap="truncate" color={c(roleFor(l))}>{l}</Text>)}
|
|
71
|
+
<Text> </Text>
|
|
72
|
+
<Text wrap="truncate">
|
|
73
|
+
<Text bold color={c('key')}>r</Text><Text color={c('value')}> refresh </Text>
|
|
74
|
+
<Text bold color={c('key')}>q</Text><Text color={c('value')}> quit </Text>
|
|
75
|
+
<Text color={c('value')}>{at ? `updated ${new Date(at).toISOString().slice(11, 19)} UTC, every ${Math.round(intervalMs / 1000)}s` : ''}</Text>
|
|
76
|
+
</Text>
|
|
77
|
+
</Box>
|
|
78
|
+
);
|
|
79
|
+
}
|