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
package/lib/quoxon/api.js
CHANGED
|
@@ -3,13 +3,32 @@
|
|
|
3
3
|
* driven by a fake (demo + tests). All routes per docs/QUOXON_EVENTS_v1.md.
|
|
4
4
|
*/
|
|
5
5
|
import { resolveEndpoint } from '../api/endpoints.js';
|
|
6
|
+
import { rememberEmail } from '../core/auth.js';
|
|
7
|
+
import { isSessionDead } from './sessionDead.js'; // not signin.js: that loads React + Ink (QUOXON-LATENCY)
|
|
6
8
|
import { buildStreamQuery } from '../commands/chat.js';
|
|
7
9
|
import { loadBundledAssistantsRoster } from '../core/agentAlias.js';
|
|
10
|
+
import { bundledIdentities } from './objects/fetch.js';
|
|
8
11
|
import { recordHeldSlot } from '../commands/slot.js';
|
|
9
12
|
import { askGripe, settleGripe } from '../commands/gripe.js';
|
|
13
|
+
import { sendFeedback } from './feedback.js';
|
|
10
14
|
import { fromObjective, fromExecution, fromApproval, sortRows } from './roster.js';
|
|
11
15
|
import { normChoice } from './model.js';
|
|
16
|
+
import { normPrefs as normModelPrefs } from './modelPrefs.js';
|
|
17
|
+
import { createRowsStore } from './rowsStore.js';
|
|
12
18
|
import { createObjectsApi } from './objects/fetch.js';
|
|
19
|
+
import { createDragonReads } from './dragonReads.js';
|
|
20
|
+
import { createKillerApi } from './slash/killerApi.js';
|
|
21
|
+
import { createUndoOps } from './slash/undoApi.js';
|
|
22
|
+
import { resolveDashboardBase, resolveCollectorBase, detectLanIp } from './hostUrls.js';
|
|
23
|
+
import { grantBody } from './consent.js';
|
|
24
|
+
import { loadJourney } from './onboarding/journey.js';
|
|
25
|
+
import { joinServiceUrl } from '../core/config.js';
|
|
26
|
+
import { runVmCommand } from '../core/vmTui.js';
|
|
27
|
+
import { fetchActivity, heldLines } from '../core/hyperion.js';
|
|
28
|
+
import { createPeekFetchers, peekId } from '../core/peek.js';
|
|
29
|
+
import { askAside } from '../core/aside.js';
|
|
30
|
+
import { exportConversationBundle } from '../evidence/bundleExport.js';
|
|
31
|
+
import { addActionItem, listBoard, moveActionItem } from '../core/actionItems.js';
|
|
13
32
|
|
|
14
33
|
const missing = (e) => e?.status === 404 || e?.status === 405 || e?.status === 501;
|
|
15
34
|
/** Server steer refusal label -> client reason. Unknown labels are 'ambiguous', never "finished". */
|
|
@@ -48,12 +67,61 @@ async function* readTurn(open, cur, signal) {
|
|
|
48
67
|
const sleepFor = (ms, signal) => new Promise((r) => { if (signal?.aborted) return r(); const t = setTimeout(done, ms); function done() { clearTimeout(t); signal?.removeEventListener?.('abort', done); r(); } signal?.addEventListener?.('abort', done, { once: true }); });
|
|
49
68
|
const listOf = (d, ...keys) => { for (const k of keys) if (Array.isArray(d?.[k])) return d[k]; return Array.isArray(d) ? d : null; };
|
|
50
69
|
|
|
51
|
-
export
|
|
70
|
+
export const SLOTS_UNLICENSED = 'Slots need a licence. This Quox does not have one, so /slot is off. See: quox setup license status';
|
|
71
|
+
// Z2H run-6: every slot write (checkout, /say, /broadcast) names the licence reason the same way.
|
|
72
|
+
/** #1288: a refused vault write, keeping the server's "confirm this private address" hint. */
|
|
73
|
+
const vaultFailure = (e) => ({ ok: false, status: e?.status, message: e?.message || 'failed', ...(e?.body?.confirmable === true ? { confirmable: true, origin: typeof e.body.origin === 'string' ? e.body.origin : null } : {}) });
|
|
74
|
+
const slotsUnlicensed = (e) => e?.status === 403 && /not licen[cs]ed/i.test(`${e?.code || ''} ${e?.body?.error || ''} ${e?.message || ''}`);
|
|
75
|
+
|
|
76
|
+
/** PRESENCE: only a person at a real terminal is "back". --print, pipes, proof runs and agents are not. */
|
|
77
|
+
const NOTES_OFF = 'NotesQ is not available for this org. Ask an org admin to enable the NotesQ plugin (dashboard, Plugins), or check `quox notes ls` in a terminal.';
|
|
78
|
+
export function noteParts(text) {
|
|
79
|
+
const t = String(text || '').trim();
|
|
80
|
+
const first = t.split('\n')[0].trim();
|
|
81
|
+
const title = first.length > 60 ? `${first.slice(0, 60).trimEnd()}…` : first;
|
|
82
|
+
return { title, markdown: `# ${title}\n\n${t}\n` };
|
|
83
|
+
}
|
|
84
|
+
async function createNoteFrom(client, text) {
|
|
85
|
+
const { title, markdown } = noteParts(text);
|
|
86
|
+
try {
|
|
87
|
+
const ep = resolveEndpoint('notesq.notes.create');
|
|
88
|
+
const d = await client.post(ep.path, { service: ep.service, retries: 0, body: { scope: 'org', markdown, properties: {} } });
|
|
89
|
+
const n = d?.note || {};
|
|
90
|
+
if (!n.id) return { ok: false, message: 'NotesQ answered without a note id, so nothing is confirmed saved. Check quox notes ls.' };
|
|
91
|
+
return { ok: true, id: n.id, title: n.title || title };
|
|
92
|
+
} catch (e) {
|
|
93
|
+
if (e?.status === 402) return { ok: false, message: 'NotesQ is not licensed for this org. ' + NOTES_OFF };
|
|
94
|
+
if (e?.status === 403 || e?.status === 404 || e?.status === 503) return { ok: false, message: NOTES_OFF };
|
|
95
|
+
return { ok: false, message: `Could not save the note: ${e?.message || 'request failed'}${e?.status ? ` (HTTP ${e.status})` : ''}.` };
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
export function isInteractiveHuman({ stdin = process.stdin, stdout = process.stdout, env = process.env } = {}) {
|
|
99
|
+
return Boolean(stdin?.isTTY && stdout?.isTTY) && !env.CI;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** A one-line title within the tasks service's 200 character cap. */
|
|
103
|
+
const clipTitle = (g) => { const t = String(g ?? '').replace(/\s+/g, ' ').trim(); return t.length > 200 ? `${t.slice(0, 199)}…` : t; };
|
|
104
|
+
|
|
105
|
+
export function createApi({ client, config, auth, windowOrg = null, clientForOrg = null, reattachBackoff = REATTACH_BACKOFF_MS }) {
|
|
106
|
+
let holderSessionFn = null;
|
|
52
107
|
const orgId = () => { try { return auth.getOrgId(); } catch { return null; } };
|
|
108
|
+
const rowsStore = createRowsStore({ client, config, orgId });
|
|
109
|
+
let credsSeen = null; // credentials-file mtime last seen (credentialsChanged)
|
|
53
110
|
const userId = () => { try { return config.getAuth?.()?.user_id || null; } catch { return null; } };
|
|
111
|
+
/** Z2H-R3: the server's public app URL (instance_config app_base_url), or null (not admin, old server, unset). Never throws. */
|
|
112
|
+
const serverAppUrl = async () => {
|
|
113
|
+
try { const ep = resolveEndpoint('admin.instance.basics.get'); const r = await client.get(ep.path, { service: ep.service, retries: 0 }); return r?.effective?.appBaseUrl || null; } catch { return null; }
|
|
114
|
+
};
|
|
115
|
+
const journeyUrl = () => { try { return loadJourney()?.dashboardUrl || null; } catch { return null; } };
|
|
54
116
|
const listApprovals = async () => {
|
|
55
|
-
|
|
56
|
-
const
|
|
117
|
+
// quox#827: the set comes from the server's one open-approvals function (the same one behind the dashboard badge and LUCID).
|
|
118
|
+
const ep = resolveEndpoint('inbox.approvals.open', { orgId: orgId() });
|
|
119
|
+
try {
|
|
120
|
+
const data = await client.get(ep.path, { service: ep.service });
|
|
121
|
+
if (Array.isArray(data?.approvals)) return data.approvals.map((a) => a.item).filter(Boolean);
|
|
122
|
+
} catch (e) { if (!missing(e)) throw e; } // an older server without the route: fall back to the inbox list below
|
|
123
|
+
const old = resolveEndpoint('inbox.list', { orgId: orgId() });
|
|
124
|
+
const data = await client.get(old.path, { service: old.service, query: { kind: 'approval', include_snoozed: 'true', limit: 200 } }); // status omitted = every ACTIVE status
|
|
57
125
|
const items = data.items || data.data || data || [];
|
|
58
126
|
return Array.isArray(items) ? items : [];
|
|
59
127
|
};
|
|
@@ -93,11 +161,99 @@ export function createApi({ client, config, auth, reattachBackoff = REATTACH_BAC
|
|
|
93
161
|
err.code = 'reattach_failed';
|
|
94
162
|
throw err;
|
|
95
163
|
}
|
|
164
|
+
/** PRESENCE auto-clear: the human's own first real action. -> { ok, cleared, welcome } (welcome once: { away_text, via }). Never throws. */
|
|
165
|
+
async function noteActivity(action, { quiet = false, interactive = isInteractiveHuman() } = {}) {
|
|
166
|
+
if (!interactive) return { ok: false, cleared: false, welcome: null, skipped: 'not interactive' };
|
|
167
|
+
try {
|
|
168
|
+
const ep = resolveEndpoint('presence.activity');
|
|
169
|
+
const d = await client.post(ep.path, { service: ep.service, retries: 0, timeout: 6000, body: { action, org_id: orgId(), ...(quiet ? { quiet: true } : {}) } });
|
|
170
|
+
return { ok: true, cleared: d?.cleared === true, welcome: d?.welcome || null };
|
|
171
|
+
} catch (e) { return { ok: false, cleared: false, welcome: null, status: e?.status || null }; }
|
|
172
|
+
}
|
|
96
173
|
return {
|
|
174
|
+
...createKillerApi({ client }),
|
|
97
175
|
orgId,
|
|
176
|
+
undoOps: createUndoOps({ client, orgId }), // /undo
|
|
177
|
+
/** /note: same call as `quox notes new` (org scope). -> { ok, id, title } | { ok:false, message } */
|
|
178
|
+
createNote: (text) => createNoteFrom(client, text),
|
|
98
179
|
userId,
|
|
180
|
+
/** /peek <id>: the same lookup as `quox peek`, in this window's org. -> peekId result */
|
|
181
|
+
peek: (id) => peekId(id, createPeekFetchers({ client, orgId })),
|
|
182
|
+
/** /evidence, /ward: the signed bundle of one conversation, written to ~/quox-evidence (same export as `quox evidence bundle`). */
|
|
183
|
+
evidenceExport: (corr) => exportConversationBundle({ client, corr }),
|
|
184
|
+
/** /aside: a no-tools, no-conversation question (core/aside.js). turns = recent exchanges, read-only context. */
|
|
185
|
+
aside: (question, turns = []) => askAside(client, { question, turns }),
|
|
186
|
+
/** @mention hosts: the Bastion inventory (same read as `quox fleet hosts`). -> [{ id, group, health }] */
|
|
187
|
+
async mentionHosts() {
|
|
188
|
+
const ep = resolveEndpoint('v1.fleet.hosts.list');
|
|
189
|
+
const d = await client.get(ep.path, { service: ep.service, retries: 0, timeout: 8000 });
|
|
190
|
+
return (listOf(d, 'hosts') || []).map(h => ({ id: h.id, group: h.group || '', health: h.status?.health || '' }));
|
|
191
|
+
},
|
|
192
|
+
/** QUOXON objective offer: `quox objective create <goal> --checkpoint` + `start` (same endpoints). -> { id, status } */
|
|
193
|
+
async startObjective(goal, loop = null) {
|
|
194
|
+
const c = resolveEndpoint('objectives.create');
|
|
195
|
+
// QLOOP: a loop offer creates a LOOP (server-derived scope + goal), never a plan under the loop's name.
|
|
196
|
+
const body = loop ? { title: String(goal).slice(0, 200), description: String(goal), execution_mode: 'loop', capability_scope: loop.capability_scope, goal_condition: loop.goal_condition,
|
|
197
|
+
...(loop.until ? { until: loop.until } : {}), ...(loop.every_seconds ? { every_seconds: loop.every_seconds } : {}), ...(loop.max_iterations ? { max_iterations: loop.max_iterations } : {}), ...(loop.max_duration_seconds ? { max_duration_ms: loop.max_duration_seconds * 1000 } : {}), ...(Array.isArray(loop.done_check) && loop.done_check.length ? { done_check: loop.done_check } : {}) }
|
|
198
|
+
: { title: clipTitle(goal), description: String(goal), checkpoint_after_each_task: true }; // the tasks service caps a title at 200; the whole ask rides in the description
|
|
199
|
+
const made = await client.post(c.path, { service: c.service, body });
|
|
200
|
+
if (!made?.id) throw new Error('the server answered without an objective id');
|
|
201
|
+
const st = resolveEndpoint('objectives.start', { id: made.id });
|
|
202
|
+
const started = await client.post(st.path, { service: st.service, body: {} });
|
|
203
|
+
return { id: made.id, status: started?.status || 'started' };
|
|
204
|
+
},
|
|
205
|
+
/** An objective with its tasks (the same read as `quox objective get`), for the chat's report-back. */
|
|
206
|
+
async objectiveGet(id) {
|
|
207
|
+
const ep = resolveEndpoint('objectives.get', { id: encodeURIComponent(id) });
|
|
208
|
+
const d = await client.get(ep.path, { service: ep.service, retries: 0, timeout: 8000 });
|
|
209
|
+
return d?.objective || d;
|
|
210
|
+
},
|
|
211
|
+
/** @mention tasks: the newest objectives (one read, cached by the composer for the session). -> [{ id, title, status }] */
|
|
212
|
+
async mentionTasks() {
|
|
213
|
+
const ep = resolveEndpoint('objectives.list');
|
|
214
|
+
const d = await client.get(ep.path, { service: ep.service, query: { limit: 30 }, retries: 0, timeout: 8000 });
|
|
215
|
+
return (listOf(d, 'objectives', 'data') || []).map(o => ({ id: o.id, title: o.title || '', status: o.status || '' }));
|
|
216
|
+
},
|
|
217
|
+
/** Z2H: the same login path as `quox login` (createAuth().login). Returns only what the card needs; tokens are stored by auth, never surfaced. */
|
|
218
|
+
async signIn(email, password) {
|
|
219
|
+
const r = await auth.login(config.getServiceUrl('auth'), email, password);
|
|
220
|
+
return { mfa_required: Boolean(r.mfa_required), access_token: r.mfa_required ? r.access_token : undefined, user: r.user ? { email: r.user.email } : null };
|
|
221
|
+
},
|
|
222
|
+
verifyMfa: (token, code) => auth.verifyMfa(config.getServiceUrl('auth'), token, code),
|
|
223
|
+
/** The last email this profile signed in with (prefill for /login), or ''. */
|
|
224
|
+
lastEmail() { try { return config.getAuth?.()?.email || config.getActiveProfile?.()?.last_email || ''; } catch { return ''; } },
|
|
225
|
+
/**
|
|
226
|
+
* Startup/ongoing check: is the stored session dead? Only a locally expired token is probed, via the refresh seam.
|
|
227
|
+
* -> { alive: true } | { alive: false, reason: 'idle_timeout'|'max_session'|null }. A transient failure reads as alive (we learned nothing).
|
|
228
|
+
*/
|
|
229
|
+
async probeSession() {
|
|
230
|
+
try {
|
|
231
|
+
if (!auth.isExpired?.()) return { alive: true };
|
|
232
|
+
try { rememberEmail(config, config.getAuth?.()?.email); } catch { /* best-effort */ }
|
|
233
|
+
await auth.refresh(config.getServiceUrl('auth'));
|
|
234
|
+
return { alive: true };
|
|
235
|
+
} catch (e) {
|
|
236
|
+
return isSessionDead(e) ? { alive: false, reason: e.reason || null } : { alive: true };
|
|
237
|
+
}
|
|
238
|
+
},
|
|
239
|
+
/**
|
|
240
|
+
* Stale sign-in: true once each time the credentials file changed on disk since the last call (another window
|
|
241
|
+
* or `quox login` wrote it). A cheap stat; the first call only records the baseline.
|
|
242
|
+
*/
|
|
243
|
+
credentialsChanged() {
|
|
244
|
+
try {
|
|
245
|
+
const m = config.credentialsMtime?.() || 0;
|
|
246
|
+
const was = credsSeen; credsSeen = m;
|
|
247
|
+
return was !== null && m !== was;
|
|
248
|
+
} catch { return false; }
|
|
249
|
+
},
|
|
250
|
+
/** Re-read this profile's credentials from disk. -> true when a signed-in profile is there to retry with. */
|
|
251
|
+
reloadCredentials() {
|
|
252
|
+
try { config.reloadAuth?.(); return Boolean(config.getAuth?.()?.access_token); } catch { return false; }
|
|
253
|
+
},
|
|
99
254
|
/** Inline object views (objects/): skill, workflow, crystal, agent, memory, rules, proof, reconcile. */
|
|
100
|
-
objects: createObjectsApi({ client, orgId }),
|
|
255
|
+
objects: createObjectsApi({ client, orgId, profile: config?._effectiveProfile || null }),
|
|
256
|
+
dragonReads: createDragonReads({ client }), // the companion dragons' jobs: bounded read-only reads (dragonReads.js)
|
|
101
257
|
/** /model choice persisted on the active profile ({ provider, model? } only, never a key); null = auto. */
|
|
102
258
|
modelChoice() { try { return normChoice(config.getActiveProfile?.()?.chat_model); } catch { return null; } },
|
|
103
259
|
setModelChoice(choice) {
|
|
@@ -119,6 +275,46 @@ export function createApi({ client, config, auth, reattachBackoff = REATTACH_BAC
|
|
|
119
275
|
await client.patch(ep.path, { service: ep.service, body: { preferences: { soundProfile: value } } });
|
|
120
276
|
return true;
|
|
121
277
|
},
|
|
278
|
+
/**
|
|
279
|
+
* QUOXON-FW6: model favourites + default. The server is the source of truth: the user's
|
|
280
|
+
* `quoxonModels` preference (PATCH /api/auth/me merges top-level namespaces, the same store as
|
|
281
|
+
* soundProfile). The active CLI profile keeps a copy (model_prefs) for offline starts.
|
|
282
|
+
* -> { prefs, source: 'server' | 'local', error? }.
|
|
283
|
+
*/
|
|
284
|
+
async modelPrefs() {
|
|
285
|
+
const local = () => normModelPrefs(config.getActiveProfile?.()?.model_prefs);
|
|
286
|
+
try {
|
|
287
|
+
const ep = resolveEndpoint('auth.me.list');
|
|
288
|
+
const d = await client.get(ep.path, { service: ep.service, retries: 0 });
|
|
289
|
+
const raw = (d?.user?.preferences ?? d?.preferences)?.quoxonModels;
|
|
290
|
+
const prefs = normModelPrefs(raw);
|
|
291
|
+
const prof = config.getActiveProfile?.();
|
|
292
|
+
if (prof && raw !== undefined) { prof.model_prefs = prefs; config.save?.(); }
|
|
293
|
+
const useLocal = raw === undefined && local().favourites.length;
|
|
294
|
+
return { prefs: useLocal ? local() : prefs, source: 'server' };
|
|
295
|
+
} catch (e) { return { prefs: local(), source: 'local', error: e?.message || 'failed' }; }
|
|
296
|
+
},
|
|
297
|
+
/** -> { ok, synced, error? }: the local copy is always written; synced=false: refused. */
|
|
298
|
+
async setModelPrefs(value) {
|
|
299
|
+
const prefs = normModelPrefs(value);
|
|
300
|
+
const prof = config.getActiveProfile?.();
|
|
301
|
+
if (prof) { prof.model_prefs = prefs; config.save?.(); }
|
|
302
|
+
try {
|
|
303
|
+
const ep = resolveEndpoint('auth.me.update');
|
|
304
|
+
const body = { preferences: { quoxonModels: prefs } };
|
|
305
|
+
await client.patch(ep.path, { service: ep.service, body, retries: 0 });
|
|
306
|
+
return { ok: true, synced: true };
|
|
307
|
+
} catch (e) { return { ok: Boolean(prof), synced: false, error: e?.message || 'failed' }; }
|
|
308
|
+
},
|
|
309
|
+
localModelPrefs() {
|
|
310
|
+
try { return normModelPrefs(config.getActiveProfile?.()?.model_prefs); }
|
|
311
|
+
catch { return normModelPrefs(null); }
|
|
312
|
+
},
|
|
313
|
+
/**
|
|
314
|
+
* QUOXON-FW17 / ROWS: custom rows. url, quox and plugin rows sync per user (preference namespace `quoxonRows`);
|
|
315
|
+
* cmd rows stay in a local file. One implementation, shared with `quox rows` (rowsStore.js).
|
|
316
|
+
*/
|
|
317
|
+
...rowsStore,
|
|
122
318
|
/** Local copy on the active CLI profile: offline start + fallback store. */
|
|
123
319
|
localSoundProfile() { try { return config.getActiveProfile?.()?.sound_profile || null; } catch { return null; } },
|
|
124
320
|
setLocalSoundProfile(p) { const prof = config.getActiveProfile?.(); if (!prof) return false; prof.sound_profile = p; config.save?.(); return true; },
|
|
@@ -129,17 +325,21 @@ export function createApi({ client, config, auth, reattachBackoff = REATTACH_BAC
|
|
|
129
325
|
* by seq. While reconnecting it yields { type:'reconnecting', attempt } and, once back, { type:'resumed', after }.
|
|
130
326
|
* A server without v1.1 (no seq on its events) gets the v1 behaviour: no reattach.
|
|
131
327
|
*/
|
|
132
|
-
async *stream({ turnId, message, agent, conversationId, context, provider, model, promptProfile, durable = true }, signal) {
|
|
328
|
+
async *stream({ turnId, message, agent, conversationId, context, provider, model, promptProfile, heldSlot = null, durable = true, overrideId = undefined, turnBudgetMs = undefined, mode = undefined, handoffFrom = undefined }, signal) {
|
|
133
329
|
const body = buildStreamQuery({ queryId: turnId, message, agent: agent || 'quox', sessionId: conversationId, context });
|
|
134
330
|
body.turn_id = turnId;
|
|
135
331
|
if (durable) body.durable = true; // the server keeps the turn running when we disconnect (v1.1)
|
|
332
|
+
if (Number(turnBudgetMs) > 0) body.turn_budget_ms = Math.floor(Number(turnBudgetMs)); // quox#1090: a delegation hop may use the turn's budget
|
|
136
333
|
if (provider) body.provider = provider; // a request: the server policy decides, turn.started says what ran
|
|
137
334
|
if (provider && model) body.model = model;
|
|
138
335
|
if (promptProfile === 'compact' || promptProfile === 'full') body.prompt_profile = promptProfile; // D3: server default when absent
|
|
139
336
|
if (conversationId) body.conversation_id = conversationId;
|
|
337
|
+
if (mode === 'plan') body.mode = 'plan'; // PLAN MODE: the server makes no state-changing tool eligible and refuses writes
|
|
338
|
+
if (heldSlot?.name && heldSlot?.session) body.held_slot = heldSlot; // QUOXON-MESH: slot_say signs as this window's slot
|
|
339
|
+
if (handoffFrom) body.handoff_from = String(handoffFrom).slice(0, 80); // a handed-off turn: this agent stays attached, the server labels the exchange for it
|
|
140
340
|
const ep = resolveEndpoint('chat.stream');
|
|
141
341
|
const cur = { lastSeq: 0, finished: false, conversationId: conversationId || null };
|
|
142
|
-
const drop = yield* readTurn((sig) => client.stream(ep.path, { service: ep.service, body, signal: sig, untilClose: true }), cur, signal);
|
|
342
|
+
const drop = yield* readTurn((sig) => client.stream(ep.path, { service: ep.service, body, signal: sig, untilClose: true, ...(overrideId ? { headers: { 'x-override-id': overrideId } } : {}) }), cur, signal);
|
|
143
343
|
if (cur.finished || !drop || !durable || signal?.aborted) return;
|
|
144
344
|
if (!cur.lastSeq || !cur.conversationId) { if (drop.error) throw drop.error; return; } // v1 server: nothing to reattach to
|
|
145
345
|
yield* reattachLoop({ turnId, cur, signal, first: drop.error, backoff: reattachBackoff });
|
|
@@ -208,20 +408,24 @@ export function createApi({ client, config, auth, reattachBackoff = REATTACH_BAC
|
|
|
208
408
|
const roster = loadBundledAssistantsRoster();
|
|
209
409
|
const nameOf = new Map(roster.map(r => [r.id, r.name || r.shortName || r.id]));
|
|
210
410
|
const out = new Map(); let ok = 0; let lastErr = null;
|
|
411
|
+
let ids = null; const roleOf = (id) => { ids ??= bundledIdentities(); const i = ids.get(id); return i?.tagline || i?.description || null; };
|
|
211
412
|
try {
|
|
212
413
|
const ep = resolveEndpoint('v1.tools.agents.list');
|
|
213
|
-
const d = await client.get(ep.path, { service: ep.service,
|
|
214
|
-
for (const id of Object.keys(d?.agentTools || {})) out.set(id, { agent_id: id, name: nameOf.get(id) || id });
|
|
414
|
+
const d = await client.get(ep.path, { service: ep.service, retries: 0 }); // authenticated: the collector's default-deny gate 401s a noAuth read
|
|
415
|
+
for (const id of Object.keys(d?.agentTools || {})) out.set(id, { agent_id: id, name: nameOf.get(id) || id, role: roleOf(id), status: 'registered' });
|
|
215
416
|
ok++;
|
|
216
|
-
} catch (e) { lastErr = e; for (const r of roster) out.set(r.id, { agent_id: r.id, name: nameOf.get(r.id) }); if (roster.length) ok++; }
|
|
417
|
+
} catch (e) { lastErr = e; for (const r of roster) out.set(r.id, { agent_id: r.id, name: nameOf.get(r.id), role: roleOf(r.id) }); if (roster.length) ok++; }
|
|
217
418
|
try {
|
|
218
419
|
const ep = resolveEndpoint('agents.list', { orgId: orgId() });
|
|
219
420
|
const rows = listOf(await client.get(ep.path, { service: ep.service, query: { includeTools: 'false' }, retries: 0 }), 'agents', 'data', 'items');
|
|
220
|
-
for (const r of rows || []) if (r.agent_id && !out.has(r.agent_id)) out.set(r.agent_id, { agent_id: r.agent_id, name: r.name || r.agent_id });
|
|
421
|
+
for (const r of rows || []) if (r.agent_id && !out.has(r.agent_id)) out.set(r.agent_id, { agent_id: r.agent_id, name: r.name || r.agent_id, role: r.description || null, status: r.status || null });
|
|
221
422
|
if (rows) ok++;
|
|
222
423
|
} catch (e) { lastErr = lastErr || e; }
|
|
223
424
|
if (!ok) throw lastErr || new Error('no agent list available');
|
|
224
|
-
|
|
425
|
+
const list = [...out.values()];
|
|
426
|
+
// The live registry failed and the bundled roster stood in: say so, so a miss is never read as "no such agent".
|
|
427
|
+
if (lastErr) Object.defineProperty(list, 'listError', { value: lastErr });
|
|
428
|
+
return list;
|
|
225
429
|
},
|
|
226
430
|
/** -> saved turns of one of YOUR conversations, oldest first [{ id, role, content }]; [] when none or not yours. */
|
|
227
431
|
async conversationTurns(conversationId) {
|
|
@@ -236,6 +440,24 @@ export function createApi({ client, config, auth, reattachBackoff = REATTACH_BAC
|
|
|
236
440
|
if (!d || !Array.isArray(d.groups)) throw new Error('unexpected response');
|
|
237
441
|
return d;
|
|
238
442
|
},
|
|
443
|
+
/** GET /api/consent: this user's AI data sharing record ({ ai_data_sharing_consent, provider_consent }). */
|
|
444
|
+
async consentStatus() {
|
|
445
|
+
const ep = resolveEndpoint('consent.list');
|
|
446
|
+
return client.get(ep.path, { service: ep.service, retries: 0 });
|
|
447
|
+
},
|
|
448
|
+
/** Grant AI data sharing consent for one provider (the user's own act; keeps the other providers' settings). */
|
|
449
|
+
async grantConsent(provider) {
|
|
450
|
+
const lst = resolveEndpoint('consent.list');
|
|
451
|
+
let cur = null; try { cur = await client.get(lst.path, { service: lst.service, retries: 0 }); } catch { cur = null; }
|
|
452
|
+
const ep = resolveEndpoint('consent.create');
|
|
453
|
+
await client.post(ep.path, { service: ep.service, body: grantBody(cur, provider), retries: 0 });
|
|
454
|
+
return true;
|
|
455
|
+
},
|
|
456
|
+
/** The dashboard address to print (never localhost when a better one is known). */
|
|
457
|
+
async dashboardBase() {
|
|
458
|
+
const profileUrl = config.getActiveProfile?.()?.url || config.getBaseUrl();
|
|
459
|
+
return resolveDashboardBase({ serverApp: await serverAppUrl(), profileUrl, journeyUrl: journeyUrl(), lanIp: detectLanIp() }).url;
|
|
460
|
+
},
|
|
239
461
|
/** POST /api/v1/conversations/:id/compact (server-side summary; a model call, so a long timeout and never retried). */
|
|
240
462
|
async compactConversation(conversationId) {
|
|
241
463
|
return client.post(`/api/v1/conversations/${encodeURIComponent(conversationId)}/compact`, { service: 'collector', body: {}, retries: 0, timeout: 180000 });
|
|
@@ -254,6 +476,57 @@ export function createApi({ client, config, auth, reattachBackoff = REATTACH_BAC
|
|
|
254
476
|
},
|
|
255
477
|
/** The `quox doctor` checks, run in this process. */
|
|
256
478
|
async doctorChecks() { const { runDoctorChecks } = await import('../commands/doctor.js'); return runDoctorChecks({ config }); },
|
|
479
|
+
/** C4: the `quox server status` panel rows for the local install. -> { ok, rows } | { ok:false, error }. */
|
|
480
|
+
async serverStatus() {
|
|
481
|
+
const { createSys } = await import('../server/sys.js');
|
|
482
|
+
const { resolveInstall, backupsRoot } = await import('../server/install.js');
|
|
483
|
+
const { gatherStatus, statusRows, defaultUrls } = await import('../server/status.js');
|
|
484
|
+
const sys = createSys();
|
|
485
|
+
const install = await resolveInstall({ sys });
|
|
486
|
+
if (install.error) return { ok: false, error: install.error };
|
|
487
|
+
const st = await gatherStatus({ install, sys, urls: defaultUrls(config), backups: backupsRoot() });
|
|
488
|
+
return { ok: true, rows: statusRows(st) };
|
|
489
|
+
},
|
|
490
|
+
/** /dashboard: mint a one-time sign-in link for this profile (see slash/dashboard.js). Never throws; kind 'link' | 'plain' | 'error'. */
|
|
491
|
+
async dashboardLink(page) {
|
|
492
|
+
const { runDashboard } = await import('./slash/dashboard.js');
|
|
493
|
+
const profileUrl = config.getActiveProfile?.()?.url || config.getBaseUrl();
|
|
494
|
+
const baseUrl = resolveDashboardBase({ serverApp: await serverAppUrl(), profileUrl, journeyUrl: journeyUrl(), lanIp: detectLanIp() }).url;
|
|
495
|
+
return runDashboard({ page, baseUrl, mint: () => { const ep = resolveEndpoint('auth.login-link'); return client.post(ep.path, { service: ep.service, body: {}, retries: 0 }); } });
|
|
496
|
+
},
|
|
497
|
+
/** Z2H-R3: the address to print for the host installer (never localhost when a better one is known). */
|
|
498
|
+
async hostsBaseUrl() {
|
|
499
|
+
let collectorUrl = null; try { collectorUrl = new URL(config.getServiceUrl('collector')).origin; } catch { /* none */ }
|
|
500
|
+
return resolveCollectorBase({ collectorUrl, serverApp: await serverAppUrl(), journeyUrl: journeyUrl(), lanIp: detectLanIp() }).url;
|
|
501
|
+
},
|
|
502
|
+
/** Z2H-3c: can `${base}/install` be served? -> { ok, status } or null when unreachable (never throws, 4 s cap). */
|
|
503
|
+
async hostInstallCheck(base) {
|
|
504
|
+
if (!base || /^<|example/.test(String(base))) return null;
|
|
505
|
+
const ctl = new AbortController(); const t = setTimeout(() => ctl.abort(), 4000);
|
|
506
|
+
try { const r = await fetch(`${String(base).replace(/\/+$/, '')}/install`, { method: 'GET', signal: ctl.signal }); try { await r.body?.cancel?.(); } catch { /* drained */ } return { ok: r.ok, status: r.status }; }
|
|
507
|
+
catch { return null; } finally { clearTimeout(t); }
|
|
508
|
+
},
|
|
509
|
+
/** TUI-STATUS: the registered server this quox runs on -> { name, role, remote } | null (unregistered or any failure). */
|
|
510
|
+
async serverChip() {
|
|
511
|
+
const { resolveServerChip } = await import('./serverChip.js');
|
|
512
|
+
const { loadRegistry } = await import('../server/devServers.js');
|
|
513
|
+
let registry = { servers: {} };
|
|
514
|
+
try { registry = { servers: (await loadRegistry({ client })).servers }; } catch { /* unreadable registry: no purpose shown */ }
|
|
515
|
+
return resolveServerChip({
|
|
516
|
+
registry,
|
|
517
|
+
url: config.getActiveProfile?.()?.url || config.getBaseUrl?.() || null,
|
|
518
|
+
listHosts: async () => { const ep = resolveEndpoint('v1.fleet.hosts.list'); const d = await client.get(ep.path, { service: ep.service, retries: 0, timeout: 5000 }); return d?.hosts || d; }
|
|
519
|
+
});
|
|
520
|
+
},
|
|
521
|
+
/** DEVFLEET: the registered dev servers (same rows as `quox server list`) -> rows[]; throws on a Bastion read failure. */
|
|
522
|
+
async devServers() {
|
|
523
|
+
const [{ loadRegistry, buildRows, hereIdentity }] = [await import('../server/devServers.js')];
|
|
524
|
+
const ep = resolveEndpoint('v1.fleet.hosts.list');
|
|
525
|
+
const d = await client.get(ep.path, { service: ep.service, retries: 0, timeout: 5000 });
|
|
526
|
+
const hosts = d?.hosts || d;
|
|
527
|
+
if (!Array.isArray(hosts)) throw new Error('unexpected response');
|
|
528
|
+
{ const reg = await loadRegistry({ client }); return buildRows(hosts, { servers: reg.servers, defaults: reg.defaults }, hereIdentity()); }
|
|
529
|
+
},
|
|
257
530
|
serverUrl() { try { return new URL(config.getServiceUrl('collector')).origin; } catch { return null; } },
|
|
258
531
|
/** /config settings on the active profile. */
|
|
259
532
|
settings() { try { const q = config.getActiveProfile?.()?.quoxon; return q && typeof q === 'object' ? { ...q } : {}; } catch { return {}; } },
|
|
@@ -287,17 +560,269 @@ export function createApi({ client, config, auth, reattachBackoff = REATTACH_BAC
|
|
|
287
560
|
return { ok: false, message: e?.status ? `HTTP ${e.status}: ${e.message}` : (e?.message || 'failed') };
|
|
288
561
|
}
|
|
289
562
|
},
|
|
290
|
-
|
|
563
|
+
/**
|
|
564
|
+
* /feedback: POST the previewed payload to the public quox.ai feedback API (not the org's server, not GitHub).
|
|
565
|
+
* Anonymous by design: no token, cookie, org or profile goes with it. -> { ok:true, id, url } | { ok:false, message }.
|
|
566
|
+
*/
|
|
567
|
+
async feedback(payload, { version = null } = {}) { return sendFeedback(payload, { version }); },
|
|
568
|
+
/** Installed, enabled plugin ids for the org (GET plugins.list, as `quox plugin list`). Throws on failure so it never reads as "none". */
|
|
569
|
+
async installedPlugins() {
|
|
570
|
+
const id = orgId();
|
|
571
|
+
if (!id) throw new Error('no org');
|
|
572
|
+
const ep = resolveEndpoint('plugins.list', { orgId: id });
|
|
573
|
+
const data = await client.get(ep.path, { service: ep.service, retries: 0 });
|
|
574
|
+
const rows = listOf(data, 'plugins', 'data', 'items');
|
|
575
|
+
if (!rows) throw new Error('unexpected response');
|
|
576
|
+
return rows.filter(r => r.is_enabled !== false && r.is_enabled !== 0).map(r => r.plugin_id).filter(Boolean);
|
|
577
|
+
},
|
|
578
|
+
/** /todo: add an Action Tracker item as the logged-in user (the same core as `quox actions add`). Throws on failure. -> the item. */
|
|
579
|
+
async todoAdd(text) { return addActionItem(client, { title: text }); },
|
|
580
|
+
/** /todos: the whole board { backlog, doing, done }, every page. Throws on failure so it never reads as "none". */
|
|
581
|
+
async todoList() { return listBoard(client); },
|
|
582
|
+
/** /todo move: move one item (real id) to todo, doing or done. -> the item. */
|
|
583
|
+
async todoMove(id, status) { return moveActionItem(client, id, status); },
|
|
584
|
+
/** -> [{ plugin_id, version, is_enabled, ... }] every plugin installed for the org (GET plugins.list, as `quox plugin list`). Throws on failure. */
|
|
585
|
+
async pluginList() {
|
|
586
|
+
const id = orgId();
|
|
587
|
+
if (!id) throw new Error('no org');
|
|
588
|
+
const ep = resolveEndpoint('plugins.list', { orgId: id });
|
|
589
|
+
const rows = listOf(await client.get(ep.path, { service: ep.service, retries: 0 }), 'plugins', 'data', 'items');
|
|
590
|
+
if (!rows) throw new Error('unexpected response');
|
|
591
|
+
return rows;
|
|
592
|
+
},
|
|
593
|
+
/** One installed plugin's record (GET plugins.get, as `quox plugin get`), or null when unknown (404). */
|
|
594
|
+
async pluginGet(pluginId) {
|
|
595
|
+
const id = orgId();
|
|
596
|
+
if (!id) throw new Error('no org');
|
|
597
|
+
const ep = resolveEndpoint('plugins.get', { orgId: id, pluginId });
|
|
598
|
+
try { const d = await client.get(ep.path, { service: ep.service, retries: 0 }); return d?.plugin || d; }
|
|
599
|
+
catch (e) { if (e?.status === 404) return null; throw e; }
|
|
600
|
+
},
|
|
601
|
+
/** Fleet hosts already connected (tips skip the QuoxAgent tip when > 0). GET agents list; throws on failure so a failure never reads as "no hosts". */
|
|
602
|
+
async hostCount() {
|
|
603
|
+
const ep = resolveEndpoint('v1.agents.list');
|
|
604
|
+
const rows = listOf(await client.get(ep.path, { service: ep.service, retries: 0 }), 'agents', 'data', 'items');
|
|
605
|
+
if (!rows) throw new Error('unexpected response');
|
|
606
|
+
return rows.length;
|
|
607
|
+
},
|
|
608
|
+
/** -> [{ name, host_name, assigned_to, ... }] ; GET /api/v1/slots (throws on failure, never reads as empty). */
|
|
609
|
+
async slots() {
|
|
610
|
+
const ep = resolveEndpoint('v1.slots.list');
|
|
611
|
+
const rows = listOf(await client.get(ep.path, { service: ep.service, retries: 0 }), 'slots');
|
|
612
|
+
if (!rows) throw new Error('unexpected response');
|
|
613
|
+
return rows;
|
|
614
|
+
},
|
|
615
|
+
/**
|
|
616
|
+
* Post a message into a slot room (same call as `quox slot say`). QUOXON-MESH: `from` signs it as this window
|
|
617
|
+
* ("quox1 (Adam)"); `fast` answers once the line is in the slot, without waiting on the chat-app bridges.
|
|
618
|
+
* -> { ok:true, message } | { ok:false, message, status }.
|
|
619
|
+
*/
|
|
620
|
+
/** SLOTS-REPLY2: the window registers how to name its checkout session; slotSay then sends it as holder_session. */
|
|
621
|
+
bindHolderSession(fn) { holderSessionFn = typeof fn === 'function' ? fn : null; },
|
|
622
|
+
async slotSay(name, text, { from = null, fast = false, roomSession = null, fromSlot = null, holderSession = holderSessionFn ? holderSessionFn() : null, anyOrg = true } = {}) {
|
|
623
|
+
const send = async (cl, oid) => {
|
|
624
|
+
const ep = resolveEndpoint('v1.slots.say', { name });
|
|
625
|
+
// SLOTS-REPLY: fromSlot is this window's held slot, its reply address (the server verifies the hold).
|
|
626
|
+
const d = await cl.post(ep.path, { service: ep.service, retries: 0, body: { text, org_id: oid, ...(roomSession ? { room_session: roomSession } : from ? { from } : {}), ...(!roomSession && fromSlot ? { from_slot: fromSlot } : {}), ...(!roomSession && holderSession ? { holder_session: holderSession } : {}), ...(fast ? { bridge_report: false } : {}) } });
|
|
627
|
+
return { ok: true, message: d?.message || null, roomPending: d?.room_pending === true, presence: d?.presence || null, humanPresence: d?.human_presence || null };
|
|
628
|
+
};
|
|
629
|
+
const fail = (e) => {
|
|
630
|
+
if (slotsUnlicensed(e)) return { ok: false, unlicensed: true, status: 403, message: SLOTS_UNLICENSED };
|
|
631
|
+
return { ok: false, message: e?.status === 404 ? `no slot named ${name}` : (e?.message || 'failed'), status: e?.status || null };
|
|
632
|
+
};
|
|
633
|
+
let first;
|
|
634
|
+
try { return await send(client, orgId()); } catch (e) { first = fail(e); }
|
|
635
|
+
// HITL-INBOX-BAR: a window in another org cannot see a slot that lives in the profile's home org. Same user, same
|
|
636
|
+
// sessions (not a cross-tenant leak): try the home org, then each of the user's orgs, each on a token for THAT org.
|
|
637
|
+
if (!anyOrg || first.status !== 404 || !clientForOrg) return first;
|
|
638
|
+
const tried = new Set([orgId()]);
|
|
639
|
+
const order = [];
|
|
640
|
+
try { const home = config.getAuth?.()?.org_id; if (home) order.push(home); } catch { /* no home org known */ }
|
|
641
|
+
try {
|
|
642
|
+
const oe = resolveEndpoint('orgs.list'); const d = await client.get(oe.path, { service: oe.service });
|
|
643
|
+
for (const o of (Array.isArray(d) ? d : (d?.organizations || d?.orgs || d?.data || []))) if (o?.id) order.push(o.id);
|
|
644
|
+
} catch { /* the home org alone */ }
|
|
645
|
+
for (const oid of order) {
|
|
646
|
+
if (tried.has(oid)) continue; tried.add(oid);
|
|
647
|
+
try {
|
|
648
|
+
const cl = await clientForOrg(oid);
|
|
649
|
+
const r = await send(cl, oid);
|
|
650
|
+
return { ...r, viaOrgId: oid };
|
|
651
|
+
} catch (e) { const f = fail(e); if (f.status !== 404) return { ...f, viaOrgId: oid }; } // the slot is there but refused: that is the real answer
|
|
652
|
+
}
|
|
653
|
+
return first;
|
|
654
|
+
},
|
|
655
|
+
/** QUOXON-BROADCAST: fan a message out to the org's held slots. -> { ok:true, data } | { ok:false, message, status }. */
|
|
656
|
+
async slotBroadcast({ text, slots = null, priority = 'normal', ack = false, from = null } = {}) {
|
|
657
|
+
const ep = resolveEndpoint('v1.slots.broadcast');
|
|
658
|
+
try {
|
|
659
|
+
const data = await client.post(ep.path, { service: ep.service, retries: 0, body: { text, priority, ack, org_id: orgId(), ...(slots ? { slots } : {}), ...(from ? { from } : {}) } });
|
|
660
|
+
return { ok: true, data };
|
|
661
|
+
} catch (e) {
|
|
662
|
+
if (slotsUnlicensed(e)) return { ok: false, unlicensed: true, status: 403, message: SLOTS_UNLICENSED };
|
|
663
|
+
return { ok: false, message: e?.message || 'failed', status: e?.status || null };
|
|
664
|
+
}
|
|
665
|
+
},
|
|
666
|
+
async slotBroadcastStatus(id) {
|
|
667
|
+
const ep = resolveEndpoint('v1.slots.broadcast.get', { id });
|
|
668
|
+
try { return { ok: true, data: await client.get(ep.path, { service: ep.service, retries: 0, query: { org_id: orgId() } }) }; } catch (e) { return { ok: false, message: e?.message || 'failed', status: e?.status || null }; }
|
|
669
|
+
},
|
|
670
|
+
// POKE-WAKE: slot control (server allows a human org owner/admin only; agents are refused).
|
|
671
|
+
async slotControl(slot, command) {
|
|
672
|
+
const ep = resolveEndpoint('v1.slots.control', { name: slot });
|
|
673
|
+
try { return { ok: true, data: await client.post(ep.path, { service: ep.service, retries: 0, body: { command, org_id: orgId() } }) }; } catch (e) { return { ok: false, message: e?.message || 'failed', status: e?.status || null }; }
|
|
674
|
+
},
|
|
675
|
+
async slotControlGet(slot, id) {
|
|
676
|
+
const ep = resolveEndpoint('v1.slots.control.get', { name: slot, id });
|
|
677
|
+
try { return { ok: true, data: await client.get(ep.path, { service: ep.service, retries: 0, query: { org_id: orgId() } }) }; } catch (e) { return { ok: false, message: e?.message || 'failed', status: e?.status || null }; }
|
|
678
|
+
},
|
|
679
|
+
async slotAck(id, slot) {
|
|
680
|
+
const ep = resolveEndpoint('v1.slots.broadcast.ack', { id });
|
|
681
|
+
try { return { ok: true, data: await client.post(ep.path, { service: ep.service, retries: 0, body: { slot, org_id: orgId() } }) }; } catch (e) { return { ok: false, message: e?.message || 'failed', status: e?.status || null }; }
|
|
682
|
+
},
|
|
683
|
+
/** QLOOP-LIVE: the org's loops, newest first (GET /api/v1/loops) -> { loops:[{ objective_id, title, state, created_at }] }. */
|
|
684
|
+
async loopList() {
|
|
685
|
+
return client.get('/api/v1/loops', { service: 'collector', retries: 0, timeout: 8000, query: { limit: 20 } });
|
|
686
|
+
},
|
|
687
|
+
/** QLOOP-LIVE: one loop's snapshot (GET /api/v1/loops/:id). Throws on failure (the card decides what a failure means). */
|
|
688
|
+
async loopSnapshot(id) {
|
|
689
|
+
return client.get(`/api/v1/loops/${encodeURIComponent(id)}`, { service: 'collector', retries: 0, timeout: 8000 });
|
|
690
|
+
},
|
|
691
|
+
/** QLOOP-LIVE: one loop's events (SSE; ?since resumes). Yields { event_seq, type, ... }; ends after loop.finished or a drop. */
|
|
692
|
+
async *loopEvents(id, { since = null } = {}, signal) {
|
|
693
|
+
yield* client.stream(`/api/v1/loops/${encodeURIComponent(id)}/events`, { service: 'collector', query: { since }, signal, untilClose: true });
|
|
694
|
+
},
|
|
695
|
+
/** QUOXON-MESH: the slot's live messages (SSE). Yields { type: 'connected' | 'slot.message' | 'reconnect' }. */
|
|
696
|
+
async *slotStream(name, { since = null, excludeFrom = null, session = null } = {}, signal) {
|
|
697
|
+
const ep = resolveEndpoint('v1.slots.stream', { name });
|
|
698
|
+
yield* client.stream(ep.path, { service: ep.service, query: { org_id: orgId(), since, exclude_from: excludeFrom, ...(session ? { session } : {}) }, signal, untilClose: true });
|
|
699
|
+
},
|
|
700
|
+
/** QUOXON-ROOMS: join / leave a slot as a room (no hold). -> { ok, already? | removed? } | { ok:false, message, status }. */
|
|
701
|
+
async slotJoin(name, session, person = null) {
|
|
702
|
+
const ep = resolveEndpoint('v1.slots.join', { name });
|
|
703
|
+
try { const d = await client.post(ep.path, { service: ep.service, retries: 0, body: { session, ...(person ? { person } : {}), org_id: orgId() } }); return { ok: true, already: d?.already === true }; }
|
|
704
|
+
catch (e) { return { ok: false, message: e?.status === 404 ? `no slot named ${name}` : (e?.message || 'failed'), status: e?.status || null }; }
|
|
705
|
+
},
|
|
706
|
+
async slotLeave(name, session) {
|
|
707
|
+
const ep = resolveEndpoint('v1.slots.leave', { name });
|
|
708
|
+
try { const d = await client.post(ep.path, { service: ep.service, retries: 0, body: { session, org_id: orgId() } }); return { ok: true, removed: d?.removed === true }; }
|
|
709
|
+
catch (e) { return { ok: false, message: e?.message || 'failed', status: e?.status || null }; }
|
|
710
|
+
},
|
|
711
|
+
/** The rooms this session has joined (server truth, survives a restart). -> [{ slot_name, joined_at }]; throws on failure. */
|
|
712
|
+
async slotRooms(session) {
|
|
713
|
+
const ep = resolveEndpoint('v1.slots.rooms');
|
|
714
|
+
const d = await client.get(ep.path, { service: ep.service, retries: 0, query: { session, org_id: orgId() } });
|
|
715
|
+
return Array.isArray(d?.rooms) ? d.rooms : [];
|
|
716
|
+
},
|
|
717
|
+
/** Long-poll fallback for a server without the stream route (same call as `quox slot wait`). */
|
|
718
|
+
async slotWait(name, { since = null, excludeFrom = null } = {}) {
|
|
719
|
+
const ep = resolveEndpoint('v1.slots.wait', { name });
|
|
720
|
+
return client.get(ep.path, { service: ep.service, retries: 0, timeout: 40000, query: { org_id: orgId(), since, exclude_from: excludeFrom, timeout_ms: 25000 } });
|
|
721
|
+
},
|
|
722
|
+
/** Last `limit` messages of a slot, oldest first (same call as `quox slot read`). Throws on failure. */
|
|
723
|
+
async slotMessages(name, limit = 20) {
|
|
724
|
+
const ep = resolveEndpoint('v1.slots.messages', { name });
|
|
725
|
+
const rows = listOf(await client.get(ep.path, { service: ep.service, retries: 0, query: { org_id: orgId(), limit } }), 'messages');
|
|
726
|
+
if (!rows) throw new Error('unexpected response');
|
|
727
|
+
return rows;
|
|
728
|
+
},
|
|
729
|
+
/** The signed-in human's display name for slot lines (display_name, else the email's local part). */
|
|
730
|
+
displayName() {
|
|
731
|
+
try { const a = config.getAuth?.() || {}; return a.display_name || (a.email ? String(a.email).split('@')[0] : null); } catch { return null; }
|
|
732
|
+
},
|
|
733
|
+
/** `take` = the same signed-in user's other/renamed/restarted session takes a LIVE hold (stale holds of yours are reclaimed regardless). */
|
|
734
|
+
async checkoutSlot(name, session, { take = false } = {}) {
|
|
291
735
|
const ep = resolveEndpoint('v1.slots.checkout', { name });
|
|
292
736
|
try {
|
|
293
|
-
const data = await client.post(ep.path, { service: ep.service, body: { holder_session: session, holder_kind: 'interactive', host_id: null, org_id: orgId(), force: false, create: true } });
|
|
737
|
+
const data = await client.post(ep.path, { service: ep.service, body: { holder_session: session, holder_kind: 'interactive', host_id: null, org_id: orgId(), force: false, create: true, ...(take ? { take: true } : {}) } });
|
|
294
738
|
recordHeldSlot(name, session);
|
|
295
|
-
return { ok: true, slot: data?.slot || data };
|
|
739
|
+
return { ok: true, slot: data?.slot || data, tookOver: data?.took_over || null };
|
|
296
740
|
} catch (e) {
|
|
297
|
-
if (e?.status === 409)
|
|
298
|
-
|
|
741
|
+
if (e?.status === 409) {
|
|
742
|
+
const d = e.detail || {};
|
|
743
|
+
if (d.assigned_to) return { ok: false, message: `slot ${name} is assigned to ${d.assigned_to}` };
|
|
744
|
+
return { ok: false, message: `slot ${name} is held by ${d.holder_session || 'another session'}`, held: { holder_session: d.holder_session || null, same_user: d.same_user === true, state: d.state || null, since: d.checked_out_at || null, last_seen: d.last_heartbeat_at || null } };
|
|
745
|
+
}
|
|
746
|
+
// Z2H-R5 C: a free install answers 403 "QuoxSlots is not licensed": say that, not a bare 403.
|
|
747
|
+
if (slotsUnlicensed(e)) {
|
|
748
|
+
return { ok: false, unlicensed: true, transient: false, status: 403, message: SLOTS_UNLICENSED };
|
|
749
|
+
}
|
|
750
|
+
// No answer, or the server failing (timeout, network, 5xx, e.g. a collector mid-redeploy): worth retrying.
|
|
751
|
+
const st = e?.status || null;
|
|
752
|
+
return { ok: false, message: e?.message || 'failed', status: st, transient: !st || st >= 500 || st === 429 || st === 408 };
|
|
299
753
|
}
|
|
300
754
|
},
|
|
755
|
+
/** Keep a TUI-held slot alive (the hold lapses to stale after 15m without one). -> { ok } */
|
|
756
|
+
async slotHeartbeat(name, session) {
|
|
757
|
+
const ep = resolveEndpoint('v1.slots.heartbeat', { name });
|
|
758
|
+
try { await client.post(ep.path, { service: ep.service, body: { holder_session: session, org_id: orgId() } }); return { ok: true }; }
|
|
759
|
+
catch (e) { return { ok: false, message: e?.message || 'failed' }; }
|
|
760
|
+
},
|
|
761
|
+
/** quox#1183: the org's hats -> [{ id, key, name, definition }]. Same endpoint as `quox hat list`. */
|
|
762
|
+
async hats() {
|
|
763
|
+
const ep = resolveEndpoint('hat.list', { orgId: orgId() });
|
|
764
|
+
const d = await client.get(ep.path, { service: ep.service });
|
|
765
|
+
return Array.isArray(d?.hats) ? d.hats.filter(h => h.status !== 'archived') : [];
|
|
766
|
+
},
|
|
767
|
+
/** CUSTOM-HATS: create an org hat of your own -> { hat, warnings }. Throws the server's refusal unchanged. */
|
|
768
|
+
async createHat({ key, name, definition, visibility = 'org' }) {
|
|
769
|
+
const ep = resolveEndpoint('hat.create', { orgId: orgId() });
|
|
770
|
+
return client.post(ep.path, { service: ep.service, retries: 0, body: { key, name, definition, visibility } });
|
|
771
|
+
},
|
|
772
|
+
/** CUSTOM-HATS: a new revision of a hat -> { hat, warnings }. */
|
|
773
|
+
async updateHat(hatId, { definition, name, change_reason }) {
|
|
774
|
+
const ep = resolveEndpoint('hat.update', { orgId: orgId(), hatId });
|
|
775
|
+
return client.put(ep.path, { service: ep.service, retries: 0, body: { definition, ...(name ? { name } : {}), change_reason } });
|
|
776
|
+
},
|
|
777
|
+
/** CUSTOM-HATS: delete a custom hat -> { deleted: { key, revision, bindings_cleared, user_prefs_cleared } }. */
|
|
778
|
+
async deleteHat(hatId) {
|
|
779
|
+
const ep = resolveEndpoint('hat.delete', { orgId: orgId(), hatId });
|
|
780
|
+
return client.delete(ep.path, { service: ep.service, retries: 0 });
|
|
781
|
+
},
|
|
782
|
+
/** quox#1183: the hat this conversation is on (or would start on) -> { key, name } | null. */
|
|
783
|
+
async activeHat(conversationId) {
|
|
784
|
+
const ep = resolveEndpoint('hat.resolve', { orgId: orgId() });
|
|
785
|
+
const d = await client.get(ep.path, { service: ep.service, ...(conversationId ? { query: { conversation_id: conversationId } } : {}) });
|
|
786
|
+
return d?.hat ? { key: d.hat.key, name: d.hat.name, source: d.source || null } : null;
|
|
787
|
+
},
|
|
788
|
+
/** quox#1183: the person's own hat switch for this conversation (audited on the server). Throws the refusal unchanged. */
|
|
789
|
+
async bindHat(conversationId, hatId) {
|
|
790
|
+
const ep = resolveEndpoint('hat.bind', { orgId: orgId(), conversationId });
|
|
791
|
+
const d = await client.put(ep.path, { service: ep.service, retries: 0, body: { hat_id: hatId } });
|
|
792
|
+
return d?.binding || d;
|
|
793
|
+
},
|
|
794
|
+
/** QUOXON-FW3: the orgs the signed-in user belongs to -> [{ id, name, slug, role }]. Same endpoint as `quox org list`. */
|
|
795
|
+
async orgs() {
|
|
796
|
+
const ep = resolveEndpoint('orgs.list');
|
|
797
|
+
const d = await client.get(ep.path, { service: ep.service });
|
|
798
|
+
const rows = Array.isArray(d) ? d : (d?.organizations || d?.orgs || d?.data || []);
|
|
799
|
+
return Array.isArray(rows) ? rows : [];
|
|
800
|
+
},
|
|
801
|
+
/**
|
|
802
|
+
* QUOXON-FW3, #1034: re-scope THIS WINDOW to another org. The org-scoped token is held in this window's memory
|
|
803
|
+
* (windowOrg); the shared profile, its file and every other window keep their org. `quox org switch` in a shell is
|
|
804
|
+
* the explicit way to change a profile's default. -> { id, name }. Throws the server's refusal unchanged.
|
|
805
|
+
*/
|
|
806
|
+
async switchOrg(targetId) {
|
|
807
|
+
if (!windowOrg) { const e = new Error('Org switching is not available in this mode.'); e.code = 'no_window_org'; throw e; }
|
|
808
|
+
return windowOrg.switchTo(targetId);
|
|
809
|
+
},
|
|
810
|
+
/**
|
|
811
|
+
* "also make default" from the /org picker: the same profile-wide change as `quox org switch <org>` in a shell
|
|
812
|
+
* (moves the shared session and rewrites the profile's stored org + token). This window's own org is untouched.
|
|
813
|
+
*/
|
|
814
|
+
async makeOrgDefault(targetId) {
|
|
815
|
+
const ep = resolveEndpoint('auth.switch-org');
|
|
816
|
+
const data = await client.post(ep.path, { service: ep.service, body: { org_id: targetId } });
|
|
817
|
+
if (!data?.org?.id) { const e = new Error('The server did not confirm the switch.'); e.code = 'switch_failed'; throw e; }
|
|
818
|
+
const a = config.getAuth() || {};
|
|
819
|
+
a.org_id = data.org.id; a.org_name = data.org.name; a.org_slug = data.org.slug; a.org_role = data.role;
|
|
820
|
+
if (data.access_token) a.access_token = data.access_token;
|
|
821
|
+
config.setAuth(a);
|
|
822
|
+
return { id: data.org.id, name: data.org.name };
|
|
823
|
+
},
|
|
824
|
+
/** The window's own org id when it differs from the profile's (carried across /restart), else null. */
|
|
825
|
+
windowOrgId: () => windowOrg?.current()?.id || null,
|
|
301
826
|
/** -> [{ id, name, description, edition, version, decayState }] ; the org's QSKL procedural skills (read-only list; Crystals are a separate store). */
|
|
302
827
|
async skills() {
|
|
303
828
|
const ep = resolveEndpoint('quoxflow.skills.list');
|
|
@@ -361,6 +886,29 @@ export function createApi({ client, config, auth, reattachBackoff = REATTACH_BAC
|
|
|
361
886
|
}
|
|
362
887
|
return { ok: false, unavailable: true };
|
|
363
888
|
},
|
|
889
|
+
/** /sitrep: the LUCID situation (cursor not moved) and the approvals inbox, each part on its own. */
|
|
890
|
+
async sitrep() {
|
|
891
|
+
const part = async (fn) => { try { return { ok: true, v: await fn() }; } catch (e) { return { ok: false, error: e?.status ? `HTTP ${e.status}` : /timed out/i.test(e?.message || '') ? 'timed out' : (e?.message || 'failed') }; } };
|
|
892
|
+
const [situation, approvals] = await Promise.all([
|
|
893
|
+
part(async () => { const ep = resolveEndpoint('lucid.situation'); return client.get(ep.path, { service: ep.service, query: { since: 'last', surface: 'cli' }, retries: 0, timeout: 12000 }); }),
|
|
894
|
+
part(() => listApprovals())
|
|
895
|
+
]);
|
|
896
|
+
return { situation, approvals };
|
|
897
|
+
},
|
|
898
|
+
/** PRESENCE: PUT /api/v1/presence (/afk, /sleep, /back, a typed turn). -> { ok:true, presence, notice } | { ok:false, message, status }. */
|
|
899
|
+
async setPresence(state, { note, slots } = {}) {
|
|
900
|
+
const ep = resolveEndpoint('presence.set');
|
|
901
|
+
try {
|
|
902
|
+
const d = await client.put(ep.path, { service: ep.service, retries: 0, timeout: 8000, body: { state, org_id: orgId(), ...(note ? { note } : {}), ...(Array.isArray(slots) && slots.length ? { slots } : {}) } });
|
|
903
|
+
return { ok: true, presence: d?.presence || null, notice: d?.notice || null };
|
|
904
|
+
} catch (e) { return { ok: false, message: e?.message || 'failed', status: e?.status || null }; }
|
|
905
|
+
},
|
|
906
|
+
noteActivity,
|
|
907
|
+
/** E5 /away: GET /api/v1/away?since=<iso> -> { since, until, items, activity, blind }. Throws on non-2xx. */
|
|
908
|
+
async away(since) {
|
|
909
|
+
const ep = resolveEndpoint('away.digest');
|
|
910
|
+
return client.get(ep.path, { service: ep.service, query: { since }, retries: 0, timeout: 15000 });
|
|
911
|
+
},
|
|
364
912
|
/** Facts for /observers, each part on its own: { lucid, learning:[{ id, label, state, text }] }. */
|
|
365
913
|
async observers() {
|
|
366
914
|
const part = async (fn) => { try { return { ok: true, v: await fn() }; } catch (e) { return { ok: false, unavailable: missing(e), error: e?.message || 'failed' }; } };
|
|
@@ -377,8 +925,27 @@ export function createApi({ client, config, auth, reattachBackoff = REATTACH_BAC
|
|
|
377
925
|
]);
|
|
378
926
|
return { situation: sit, lessons, learning: [reflect, learn, insight] };
|
|
379
927
|
},
|
|
928
|
+
/** /mind [id]: QuoxMind implants from the collector. -> { implants, health } | { implant }. Throws on non-2xx. */
|
|
929
|
+
async mind(id) {
|
|
930
|
+
if (id) { const ep = resolveEndpoint('mind.implant', { id }); return { implant: await client.get(ep.path, { service: ep.service, retries: 0, timeout: 15000 }) }; }
|
|
931
|
+
const [l, h] = await Promise.all([resolveEndpoint('mind.implants'), resolveEndpoint('mind.health')].map((ep) => client.get(ep.path, { service: ep.service, retries: 0, timeout: 15000 })));
|
|
932
|
+
return { implants: l.implants || [], health: h };
|
|
933
|
+
},
|
|
934
|
+
/** /mirror: the Mirror as CommanderQ sees it (compact System Status block + age). Throws on non-2xx. */
|
|
935
|
+
async mirror() {
|
|
936
|
+
const ep = resolveEndpoint('mind.mirror');
|
|
937
|
+
return client.get(ep.path, { service: ep.service, retries: 0, timeout: 10000 });
|
|
938
|
+
},
|
|
939
|
+
/** #904 part 3: providers refusing turns right now (weekly limit, no credits). -> [{ provider, state, line }] | null on error. */
|
|
940
|
+
async providerHealth() {
|
|
941
|
+
try {
|
|
942
|
+
const ep = resolveEndpoint('chat.status.list');
|
|
943
|
+
const d = await client.get(ep.path, { service: ep.service, retries: 0, timeout: 8000 });
|
|
944
|
+
return Array.isArray(d?.provider_health) ? d.provider_health : [];
|
|
945
|
+
} catch { return null; }
|
|
946
|
+
},
|
|
380
947
|
async health(timeoutMs = 12000) {
|
|
381
|
-
const url =
|
|
948
|
+
const url = joinServiceUrl(config.getServiceUrl('collector'), '/health');
|
|
382
949
|
const ctrl = new AbortController();
|
|
383
950
|
const t = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
384
951
|
try { const r = await fetch(url, { signal: ctrl.signal }); return { ok: r.ok, status: r.status, url: url.origin }; }
|
|
@@ -399,6 +966,31 @@ export function createApi({ client, config, auth, reattachBackoff = REATTACH_BAC
|
|
|
399
966
|
}
|
|
400
967
|
},
|
|
401
968
|
/** /connect status: the org vault list (the only source a user can read). -> { ok:true, rows } | { ok:false, error }. */
|
|
969
|
+
/** LAZY-AI-CLI: which vendor CLIs (claude-code, codex) this server has. Never throws. */
|
|
970
|
+
async providerCliStatus() {
|
|
971
|
+
const ep = resolveEndpoint('v1.providers.cli.status');
|
|
972
|
+
try {
|
|
973
|
+
const d = await client.get(ep.path, { service: ep.service, retries: 0 });
|
|
974
|
+
return Array.isArray(d?.tools) ? { ok: true, tools: d.tools } : { ok: false, error: 'unexpected response' };
|
|
975
|
+
} catch (e) { return { ok: false, status: e?.status, missing: missing(e), error: e?.status ? `HTTP ${e.status} ${e.message || ''}`.trim() : (e?.message || 'failed') }; }
|
|
976
|
+
},
|
|
977
|
+
/** The install plan: source, exact package + version, download size. Instance admin only (403 otherwise). Never throws. */
|
|
978
|
+
async providerCliPlan(tool) {
|
|
979
|
+
const ep = resolveEndpoint('v1.providers.cli.plan', { tool });
|
|
980
|
+
try { const d = await client.get(ep.path, { service: ep.service, retries: 0 }); return d?.ok ? d : { ok: false, error: d?.message || 'unexpected response' }; }
|
|
981
|
+
catch (e) { return { ok: false, status: e?.status, missing: missing(e), error: e?.body?.message || e?.message || 'failed' }; }
|
|
982
|
+
},
|
|
983
|
+
/** Start an install job (the server audits it). Never throws. */
|
|
984
|
+
async providerCliInstall(tool, { approvalId = null } = {}) {
|
|
985
|
+
const ep = resolveEndpoint('v1.providers.cli.install', { tool });
|
|
986
|
+
try { return await client.post(ep.path, { service: ep.service, body: approvalId ? { approval_id: approvalId } : {}, retries: 0 }); }
|
|
987
|
+
catch (e) { return { ok: false, status: e?.status, error: e?.body?.message || e?.message || 'failed' }; }
|
|
988
|
+
},
|
|
989
|
+
async providerCliJob(id) {
|
|
990
|
+
const ep = resolveEndpoint('v1.providers.cli.job', { id });
|
|
991
|
+
try { return await client.get(ep.path, { service: ep.service, retries: 0 }); }
|
|
992
|
+
catch (e) { return { state: 'unknown', error: e?.message || 'failed', status: e?.status }; }
|
|
993
|
+
},
|
|
402
994
|
async connections() {
|
|
403
995
|
try {
|
|
404
996
|
const ep = resolveEndpoint('vault.list', { orgId: orgId() });
|
|
@@ -419,20 +1011,32 @@ export function createApi({ client, config, auth, reattachBackoff = REATTACH_BAC
|
|
|
419
1011
|
} catch (e) { return { ok: false, status: e?.status, message: e?.message || 'failed' }; }
|
|
420
1012
|
},
|
|
421
1013
|
/** The governed vault write (same route as `quox vault create`): audited, AEE envelope carries type + label only. */
|
|
422
|
-
async saveProviderKey({ type, label, credentials, isDefault }) {
|
|
1014
|
+
async saveProviderKey({ type, label, credentials, isDefault, confirmPrivateOrigin }) {
|
|
423
1015
|
const ep = resolveEndpoint('vault.create', { orgId: orgId() });
|
|
424
1016
|
try {
|
|
425
|
-
const r = await client.post(ep.path, { service: ep.service, body: { integration_type: type, label, credentials, is_default: Boolean(isDefault) }, retries: 0, sensitive: true });
|
|
1017
|
+
const r = await client.post(ep.path, { service: ep.service, body: { integration_type: type, label, credentials, is_default: Boolean(isDefault), ...(confirmPrivateOrigin ? { confirm_private_origin: confirmPrivateOrigin } : {}) }, retries: 0, sensitive: true });
|
|
426
1018
|
return { ok: Boolean(r?.id), id: r?.id || null, message: r?.id ? null : 'unexpected response' };
|
|
427
|
-
} catch (e) { return
|
|
1019
|
+
} catch (e) { return vaultFailure(e); }
|
|
428
1020
|
},
|
|
429
1021
|
/** B3: refresh an existing vault credential (same governed route as `quox vault update`). */
|
|
430
|
-
async updateProviderCredential({ id, credentials, isDefault }) {
|
|
1022
|
+
async updateProviderCredential({ id, credentials, isDefault, confirmPrivateOrigin }) {
|
|
431
1023
|
const ep = resolveEndpoint('vault.update', { orgId: orgId(), id });
|
|
432
1024
|
try {
|
|
433
|
-
const r = await client.put(ep.path, { service: ep.service, body: { credentials, is_default: Boolean(isDefault) }, retries: 0, sensitive: true });
|
|
1025
|
+
const r = await client.put(ep.path, { service: ep.service, body: { credentials, is_default: Boolean(isDefault), ...(confirmPrivateOrigin ? { confirm_private_origin: confirmPrivateOrigin } : {}) }, retries: 0, sensitive: true });
|
|
434
1026
|
return { ok: Boolean(r && (r.id || r.success || r.credential)), id, message: null };
|
|
435
|
-
} catch (e) { return
|
|
1027
|
+
} catch (e) { return vaultFailure(e); }
|
|
1028
|
+
},
|
|
1029
|
+
/** Z2H: read the instance LAN-target gate -> { ok, enabled } | { ok:false, status, message }. */
|
|
1030
|
+
async lanTargets() {
|
|
1031
|
+
const ep = resolveEndpoint('admin.instance.lan_targets.get');
|
|
1032
|
+
try { const r = await client.get(ep.path, { service: ep.service, retries: 0 }); return { ok: true, enabled: Boolean(r?.enabled) }; }
|
|
1033
|
+
catch (e) { return { ok: false, status: e?.status, message: e?.message || 'failed' }; }
|
|
1034
|
+
},
|
|
1035
|
+
/** Z2H: set the instance LAN-target gate (instance admin only; audited server side). */
|
|
1036
|
+
async setLanTargets(on) {
|
|
1037
|
+
const ep = resolveEndpoint('admin.instance.lan_targets.set');
|
|
1038
|
+
try { const r = await client.put(ep.path, { service: ep.service, body: { enabled: Boolean(on) }, retries: 0 }); return { ok: true, enabled: Boolean(r?.enabled) }; }
|
|
1039
|
+
catch (e) { return { ok: false, status: e?.status, message: e?.message || 'failed' }; }
|
|
436
1040
|
},
|
|
437
1041
|
/** B3: server-side probe of an Ollama URL (GET <url>/api/tags). Nothing stored. */
|
|
438
1042
|
async ollamaProbe(baseUrl) {
|
|
@@ -446,6 +1050,12 @@ export function createApi({ client, config, auth, reattachBackoff = REATTACH_BAC
|
|
|
446
1050
|
try { return await client.get(ep.path, { service: ep.service, retries: 0 }); }
|
|
447
1051
|
catch (e) { return { ok: false, status: e?.status, error: e?.code || null, message: e?.message || 'failed' }; }
|
|
448
1052
|
},
|
|
1053
|
+
/** QUOXON-MODEL: live model list for openai | anthropic -> { ok, models:[{id,family,created}], default_model } | { ok:false, error, message }. */
|
|
1054
|
+
async providerModels(provider, { refresh = false } = {}) {
|
|
1055
|
+
const ep = resolveEndpoint('chat.providers.models', { provider });
|
|
1056
|
+
try { return await client.get(ep.path, { service: ep.service, query: refresh ? { refresh: 1 } : undefined, retries: 0 }); }
|
|
1057
|
+
catch (e) { return { ok: false, status: e?.status, error: e?.code || null, message: e?.message || 'failed' }; }
|
|
1058
|
+
},
|
|
449
1059
|
/** B5: server-side probe of a self-hosted / custom OpenAI-compatible base URL (lmstudio|vllm|custom). Nothing stored. */
|
|
450
1060
|
async compatProbe(preset, baseUrl) {
|
|
451
1061
|
const ep = resolveEndpoint('chat.providers.compat.probe', { preset });
|
|
@@ -458,16 +1068,71 @@ export function createApi({ client, config, auth, reattachBackoff = REATTACH_BAC
|
|
|
458
1068
|
try { return await client.get(ep.path, { service: ep.service, retries: 0 }); }
|
|
459
1069
|
catch (e) { return { ok: false, status: e?.status, error: e?.code || null, message: e?.message || 'failed' }; }
|
|
460
1070
|
},
|
|
1071
|
+
/** /vm <verb> ...: the HYPERION Proxmox tools through POST /api/v1/tools/execute. Returns { lines, tone }. */
|
|
1072
|
+
async vm(argv) { return runVmCommand({ client, orgId: orgId(), argv }); },
|
|
1073
|
+
/** /hyperion: held VMs and who holds them, one line each (read only, GET /api/v1/proxmox/activity). */
|
|
1074
|
+
async hyperion() { const data = await fetchActivity(client); return { lines: heldLines(data), tone: data.nodes_error || data.pool_error ? 'warn' : undefined }; },
|
|
461
1075
|
approvals: listApprovals,
|
|
1076
|
+
/** HITL-INBOX-BAR: the owner's latest inbox items (any kind, active), newest first. Throws on failure, never reads as empty. */
|
|
1077
|
+
async inboxItems({ limit = 30 } = {}) {
|
|
1078
|
+
const ep = resolveEndpoint('inbox.list', { orgId: orgId() });
|
|
1079
|
+
const data = await client.get(ep.path, { service: ep.service, query: { limit } });
|
|
1080
|
+
const items = data?.items || data?.data || data || [];
|
|
1081
|
+
return Array.isArray(items) ? items : [];
|
|
1082
|
+
},
|
|
1083
|
+
/** One inbox item in full (the server marks it read for this user as a side effect). */
|
|
1084
|
+
async inboxItem(itemId) {
|
|
1085
|
+
const ep = resolveEndpoint('inbox.get', { orgId: orgId(), itemId: encodeURIComponent(itemId) });
|
|
1086
|
+
const data = await client.get(ep.path, { service: ep.service });
|
|
1087
|
+
return data?.item || data;
|
|
1088
|
+
},
|
|
1089
|
+
/** Mark one inbox item read (PATCH .../inbox/:id/read). */
|
|
1090
|
+
async inboxMarkRead(itemId, isRead = true) {
|
|
1091
|
+
const ep = resolveEndpoint('inbox.get', { orgId: orgId(), itemId: encodeURIComponent(itemId) });
|
|
1092
|
+
return client.patch(`${ep.path}/read`, { service: ep.service, body: { is_read: Boolean(isRead) } });
|
|
1093
|
+
},
|
|
1094
|
+
/** Org-wide approvals awaiting a human (the dashboard header's server-side count, inbox stats awaiting_human). */
|
|
1095
|
+
async approvalStats() {
|
|
1096
|
+
const ep = resolveEndpoint('inbox.stats.list', { orgId: orgId() });
|
|
1097
|
+
const d = await client.get(ep.path, { service: ep.service });
|
|
1098
|
+
return { pending: Number(d?.pending_approvals ?? 0), awaiting: Number(d?.awaiting_human ?? 0) };
|
|
1099
|
+
},
|
|
462
1100
|
/** GET /api/v1/approvals/:id/result -> { status, result, executed_at } ; null when unknown (404) */
|
|
463
1101
|
async approvalResult(approvalId) {
|
|
464
1102
|
const ep = resolveEndpoint('v1.approvals.result', { id: approvalId });
|
|
465
1103
|
try { return await client.get(ep.path, { service: ep.service, query: { org_id: orgId() || undefined } }); }
|
|
466
1104
|
catch (e) { if (e?.status === 404) return null; throw e; }
|
|
467
1105
|
},
|
|
468
|
-
|
|
1106
|
+
/** quox ask: answer a decision with one of its option keys (and an optional note). */
|
|
1107
|
+
async answerDecision(itemId, option, { note = null, stepUp = null, bulk = false } = {}) {
|
|
1108
|
+
const ep = resolveEndpoint('inbox.respond', { orgId: orgId(), itemId });
|
|
1109
|
+
const r = await client.post(ep.path, { service: ep.service, headers: stepUp || {}, body: { action: 'answer', option, ...(bulk ? { bulk: true } : {}), ...(note ? { comment: String(note).slice(0, 4000) } : {}) } });
|
|
1110
|
+
if (stepUp && Object.keys(stepUp).length) noteActivity('answer', { quiet: true }); // PRESENCE: answering is the owner being back
|
|
1111
|
+
return r;
|
|
1112
|
+
},
|
|
1113
|
+
async respond(itemId, action, { via = null, stepUp = null, reason = null, bulk = false } = {}) {
|
|
469
1114
|
const ep = resolveEndpoint('inbox.respond', { orgId: orgId(), itemId });
|
|
470
|
-
|
|
1115
|
+
const tag = via ? `via quoxon ${via}` : 'via quoxon';
|
|
1116
|
+
const why = reason ? String(reason).trim().slice(0, 4000) : '';
|
|
1117
|
+
const r = await client.post(ep.path, { service: ep.service, headers: stepUp || {}, body: { action, ...(bulk ? { bulk: true } : {}), comment: why ? `${why} (${tag})` : tag, ...(why ? { reason: why } : {}) } });
|
|
1118
|
+
if (stepUp && Object.keys(stepUp).length) noteActivity('approval', { quiet: true }); // PRESENCE
|
|
1119
|
+
return r;
|
|
1120
|
+
},
|
|
1121
|
+
/** QUOXON-STEPUP: PIN -> short-lived step-up token. The PIN goes straight into the request body; never logged, never stored. */
|
|
1122
|
+
async stepUpVerify(pin, { orgId: org = null, scopes = ['approve'] } = {}) {
|
|
1123
|
+
const ep = resolveEndpoint('stepup.verify');
|
|
1124
|
+
const r = await client.post(ep.path, { service: ep.service, body: { pin, org_id: org || orgId() || undefined, scopes } });
|
|
1125
|
+
noteActivity('pin-step-up', { quiet: true }); // PRESENCE: a PIN step-up is a live human
|
|
1126
|
+
return r;
|
|
1127
|
+
},
|
|
1128
|
+
async stepUpStatus() {
|
|
1129
|
+
const ep = resolveEndpoint('stepup.status');
|
|
1130
|
+
return client.get(ep.path, { service: ep.service });
|
|
1131
|
+
},
|
|
1132
|
+
/** QUOXON-STEPUP part B: override ONE blocked action. Needs a live override-scoped step-up token. */
|
|
1133
|
+
async override({ blockId, blockType, reason, agentId = null, action = null, stepUp = {} }) {
|
|
1134
|
+
const ep = resolveEndpoint('safety.override');
|
|
1135
|
+
return client.post(ep.path, { service: ep.service, headers: stepUp, body: { block_id: blockId, block_type: blockType, reason, agent_id: agentId || undefined, action: action || undefined } });
|
|
471
1136
|
}
|
|
472
1137
|
};
|
|
473
1138
|
}
|