quoxcli 0.0.0-stage → 0.3.1
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 +35 -0
- package/README.md +144 -2
- package/bin/quox.js +265 -0
- package/bin/quoxon.js +8 -0
- package/lib/api/endpoints/admin.js +151 -0
- package/lib/api/endpoints/aee.js +38 -0
- package/lib/api/endpoints/agent-attach.js +126 -0
- package/lib/api/endpoints/agent-schedules.js +26 -0
- package/lib/api/endpoints/agents.js +63 -0
- package/lib/api/endpoints/assistants.js +62 -0
- package/lib/api/endpoints/audit.js +14 -0
- package/lib/api/endpoints/auth.js +50 -0
- package/lib/api/endpoints/awareness.js +34 -0
- package/lib/api/endpoints/away.js +15 -0
- package/lib/api/endpoints/backup-schedules.js +20 -0
- package/lib/api/endpoints/backups.js +86 -0
- package/lib/api/endpoints/brain.js +140 -0
- package/lib/api/endpoints/budgets.js +62 -0
- package/lib/api/endpoints/calendarq.js +71 -0
- package/lib/api/endpoints/catalog.js +14 -0
- package/lib/api/endpoints/chat.js +61 -0
- package/lib/api/endpoints/consent.js +26 -0
- package/lib/api/endpoints/contactsq.js +68 -0
- package/lib/api/endpoints/conversations.js +46 -0
- package/lib/api/endpoints/custom-roles.js +92 -0
- package/lib/api/endpoints/divisions.js +56 -0
- package/lib/api/endpoints/domains.js +38 -0
- package/lib/api/endpoints/engagement.js +231 -0
- package/lib/api/endpoints/engine.js +11 -0
- package/lib/api/endpoints/entity.js +50 -0
- package/lib/api/endpoints/executions.js +45 -0
- package/lib/api/endpoints/fetchAllowlist.js +27 -0
- package/lib/api/endpoints/file.js +20 -0
- package/lib/api/endpoints/files.js +40 -0
- package/lib/api/endpoints/github.js +21 -0
- package/lib/api/endpoints/governance.js +43 -0
- package/lib/api/endpoints/hat.js +66 -0
- package/lib/api/endpoints/inbox.js +52 -0
- package/lib/api/endpoints/inboxq.js +120 -0
- package/lib/api/endpoints/index.js +242 -0
- package/lib/api/endpoints/invites.js +19 -0
- package/lib/api/endpoints/llm-policy.js +43 -0
- package/lib/api/endpoints/lucid.js +89 -0
- package/lib/api/endpoints/members.js +32 -0
- package/lib/api/endpoints/memory.js +94 -0
- package/lib/api/endpoints/mfa.js +32 -0
- package/lib/api/endpoints/mind.js +13 -0
- package/lib/api/endpoints/mon.js +184 -0
- package/lib/api/endpoints/notesq.js +43 -0
- package/lib/api/endpoints/objectives.js +68 -0
- package/lib/api/endpoints/orgs.js +38 -0
- package/lib/api/endpoints/plugins.js +152 -0
- package/lib/api/endpoints/powers.js +75 -0
- package/lib/api/endpoints/presence.js +33 -0
- package/lib/api/endpoints/prompts.js +69 -0
- package/lib/api/endpoints/qlarity.js +122 -0
- package/lib/api/endpoints/quoxbox.js +55 -0
- package/lib/api/endpoints/quoxchat.js +54 -0
- package/lib/api/endpoints/quoxflow.js +32 -0
- package/lib/api/endpoints/quoxplan.js +121 -0
- package/lib/api/endpoints/quoxseo.js +114 -0
- package/lib/api/endpoints/reach.js +47 -0
- package/lib/api/endpoints/router.js +17 -0
- package/lib/api/endpoints/runs.js +38 -0
- package/lib/api/endpoints/safety.js +53 -0
- package/lib/api/endpoints/scim.js +46 -0
- package/lib/api/endpoints/scopes.js +14 -0
- package/lib/api/endpoints/security.js +69 -0
- package/lib/api/endpoints/setup.js +90 -0
- package/lib/api/endpoints/skills.js +200 -0
- package/lib/api/endpoints/sso.js +67 -0
- package/lib/api/endpoints/stepup.js +20 -0
- package/lib/api/endpoints/tags.js +32 -0
- package/lib/api/endpoints/teams.js +56 -0
- package/lib/api/endpoints/telephony.js +46 -0
- package/lib/api/endpoints/tg.js +56 -0
- package/lib/api/endpoints/tools.js +50 -0
- package/lib/api/endpoints/traces.js +157 -0
- package/lib/api/endpoints/triggers.js +96 -0
- package/lib/api/endpoints/user-library.js +41 -0
- package/lib/api/endpoints/users.js +20 -0
- package/lib/api/endpoints/v1.js +585 -0
- package/lib/api/endpoints/vault.js +192 -0
- package/lib/api/endpoints/voice.js +22 -0
- package/lib/api/endpoints/volt.js +84 -0
- package/lib/api/endpoints/ward.js +47 -0
- package/lib/api/endpoints/webhooks.js +46 -0
- package/lib/api/endpoints/workflows.js +169 -0
- package/lib/api/endpoints/x.js +32 -0
- package/lib/api/endpoints.js +15 -0
- package/lib/commands/actions.js +61 -0
- package/lib/commands/admin.js +375 -0
- package/lib/commands/aee.js +99 -0
- package/lib/commands/agent.js +337 -0
- package/lib/commands/agentAttach.js +1528 -0
- package/lib/commands/approvals.js +119 -0
- package/lib/commands/aside.js +24 -0
- package/lib/commands/ask.js +144 -0
- package/lib/commands/assistant.js +169 -0
- package/lib/commands/auth.js +464 -0
- package/lib/commands/awareness.js +88 -0
- package/lib/commands/backup.js +377 -0
- package/lib/commands/brain.js +1085 -0
- package/lib/commands/budget.js +241 -0
- package/lib/commands/build.js +170 -0
- package/lib/commands/calendar.js +216 -0
- package/lib/commands/capabilities.js +34 -0
- package/lib/commands/chat.js +295 -0
- package/lib/commands/claude.js +380 -0
- package/lib/commands/config-cmd.js +106 -0
- package/lib/commands/consent.js +84 -0
- package/lib/commands/contacts.js +1016 -0
- package/lib/commands/contactsSelftest.js +132 -0
- package/lib/commands/conversation.js +105 -0
- package/lib/commands/dashboard.js +43 -0
- package/lib/commands/department.js +96 -0
- package/lib/commands/dev.js +620 -0
- package/lib/commands/devResident.js +326 -0
- package/lib/commands/devSupervisor.js +1062 -0
- package/lib/commands/discord.js +132 -0
- package/lib/commands/doctor.js +181 -0
- package/lib/commands/domain.js +114 -0
- package/lib/commands/done.js +347 -0
- package/lib/commands/engagement.js +597 -0
- package/lib/commands/entity.js +119 -0
- package/lib/commands/facebook.js +212 -0
- package/lib/commands/fetchAllowlist.js +71 -0
- package/lib/commands/file.js +165 -0
- package/lib/commands/fleet.js +1189 -0
- package/lib/commands/governance.js +647 -0
- package/lib/commands/gripe.js +355 -0
- package/lib/commands/hat.js +374 -0
- package/lib/commands/helpers.js +96 -0
- package/lib/commands/hyperion.js +58 -0
- package/lib/commands/inbox.js +173 -0
- package/lib/commands/instagram.js +264 -0
- package/lib/commands/integration.js +100 -0
- package/lib/commands/inventory.js +115 -0
- package/lib/commands/invites.js +45 -0
- package/lib/commands/licence.js +139 -0
- package/lib/commands/linkedin.js +178 -0
- package/lib/commands/llm-policy.js +117 -0
- package/lib/commands/llm.js +45 -0
- package/lib/commands/lucid.js +597 -0
- package/lib/commands/mail.js +531 -0
- package/lib/commands/memory.js +361 -0
- package/lib/commands/mind.js +71 -0
- package/lib/commands/mon.js +581 -0
- package/lib/commands/monitoring.js +86 -0
- package/lib/commands/motion.js +22 -0
- package/lib/commands/notes.js +1246 -0
- package/lib/commands/notesBatch.js +160 -0
- package/lib/commands/notesBridges.js +56 -0
- package/lib/commands/notesLinks.js +60 -0
- package/lib/commands/notification.js +57 -0
- package/lib/commands/nucleus.js +123 -0
- package/lib/commands/objective.js +240 -0
- package/lib/commands/offload.js +89 -0
- package/lib/commands/onboard.js +329 -0
- package/lib/commands/open.js +56 -0
- package/lib/commands/org.js +276 -0
- package/lib/commands/peek.js +28 -0
- package/lib/commands/plan.js +944 -0
- package/lib/commands/plugin-doctor.js +1091 -0
- package/lib/commands/plugin.js +649 -0
- package/lib/commands/policy.js +78 -0
- package/lib/commands/powers.js +296 -0
- package/lib/commands/presence.js +89 -0
- package/lib/commands/prompt.js +350 -0
- package/lib/commands/provider.js +159 -0
- package/lib/commands/qlarity.js +706 -0
- package/lib/commands/quoxbox.js +270 -0
- package/lib/commands/quoxchat.js +275 -0
- package/lib/commands/quoxseo.js +378 -0
- package/lib/commands/reach.js +159 -0
- package/lib/commands/role.js +286 -0
- package/lib/commands/router.js +77 -0
- package/lib/commands/rows.js +358 -0
- package/lib/commands/run.js +187 -0
- package/lib/commands/safety.js +107 -0
- package/lib/commands/scim.js +88 -0
- package/lib/commands/scope.js +58 -0
- package/lib/commands/security.js +157 -0
- package/lib/commands/server.js +292 -0
- package/lib/commands/service.js +89 -0
- package/lib/commands/settings.js +99 -0
- package/lib/commands/setup.js +569 -0
- package/lib/commands/skillManifest.js +113 -0
- package/lib/commands/skillRightsFormat.js +38 -0
- package/lib/commands/skills.js +687 -0
- package/lib/commands/slot.js +1451 -0
- package/lib/commands/sso.js +229 -0
- package/lib/commands/status.js +137 -0
- package/lib/commands/tag.js +91 -0
- package/lib/commands/team.js +142 -0
- package/lib/commands/tg.js +240 -0
- package/lib/commands/tiktok.js +218 -0
- package/lib/commands/tips.js +23 -0
- package/lib/commands/tool.js +304 -0
- package/lib/commands/trace.js +530 -0
- package/lib/commands/trigger.js +223 -0
- package/lib/commands/update.js +46 -0
- package/lib/commands/vault.js +1447 -0
- package/lib/commands/vm.js +237 -0
- package/lib/commands/voice.js +213 -0
- package/lib/commands/volt.js +570 -0
- package/lib/commands/walkthrough.js +32 -0
- package/lib/commands/watch.js +167 -0
- package/lib/commands/webhook.js +137 -0
- package/lib/commands/welcome.js +14 -0
- package/lib/commands/workflow.js +660 -0
- package/lib/commands/x.js +448 -0
- package/lib/core/actingAs.js +136 -0
- package/lib/core/activeSummary.js +186 -0
- package/lib/core/aee.js +53 -0
- package/lib/core/agentAlias.js +163 -0
- package/lib/core/approvalContext.js +61 -0
- package/lib/core/aside.js +54 -0
- package/lib/core/askDecision.js +60 -0
- package/lib/core/auth.js +501 -0
- package/lib/core/banner.js +143 -0
- package/lib/core/brainPrime.js +90 -0
- package/lib/core/callerSource.js +129 -0
- package/lib/core/claudeLs.js +94 -0
- package/lib/core/cliBuild.js +62 -0
- package/lib/core/client.js +693 -0
- package/lib/core/commandRegistrars.js +121 -0
- package/lib/core/completions.js +163 -0
- package/lib/core/config.js +669 -0
- package/lib/core/confirm.js +37 -0
- package/lib/core/customHats.js +168 -0
- package/lib/core/deeplinks.js +76 -0
- package/lib/core/doneEngineFetch.js +227 -0
- package/lib/core/errors.js +441 -0
- package/lib/core/global-flags.js +190 -0
- package/lib/core/governedSession.js +402 -0
- package/lib/core/hyperion.js +102 -0
- package/lib/core/inboxResolution.js +65 -0
- package/lib/core/lazyCommands.js +122 -0
- package/lib/core/liveSessionsReport.js +112 -0
- package/lib/core/offload.js +504 -0
- package/lib/core/org-override.js +52 -0
- package/lib/core/orgResolve.js +70 -0
- package/lib/core/output.js +239 -0
- package/lib/core/peek.js +126 -0
- package/lib/core/prodEnv.js +7 -0
- package/lib/core/profileUsers.js +51 -0
- package/lib/core/promptCancel.js +18 -0
- package/lib/core/quoxglyph.js +158 -0
- package/lib/core/repoPath.js +11 -0
- package/lib/core/residentAlarm.js +282 -0
- package/lib/core/residentAuth.js +243 -0
- package/lib/core/residentPump.js +229 -0
- package/lib/core/residentSeed.js +106 -0
- 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 +192 -0
- package/lib/core/sessionsOverview.js +224 -0
- package/lib/core/slotKeeper.js +527 -0
- package/lib/core/slotWake.js +120 -0
- package/lib/core/supervisorEngine.js +933 -0
- package/lib/core/systemdInstaller.js +256 -0
- package/lib/core/version.js +87 -0
- package/lib/core/vm.js +361 -0
- package/lib/core/vmTui.js +86 -0
- package/lib/core/walkthrough.js +35 -0
- package/lib/hyperion/deck.js +29 -0
- package/lib/lucid/LucidCard.jsx +68 -0
- package/lib/lucid/LucidIntro.jsx +92 -0
- package/lib/lucid/blind.js +71 -0
- package/lib/lucid/cards.js +86 -0
- package/lib/lucid/deck.js +32 -0
- package/lib/lucid/fmt.js +139 -0
- package/lib/lucid/markdown.js +81 -0
- package/lib/lucid/motion.js +56 -0
- package/lib/lucid/sitrep.js +173 -0
- package/lib/plugins/loader.js +93 -0
- package/lib/quoxon/activity.js +116 -0
- package/lib/quoxon/aocl.js +117 -0
- package/lib/quoxon/api.js +952 -0
- package/lib/quoxon/app.js +4032 -0
- package/lib/quoxon/approvalFacts.js +94 -0
- package/lib/quoxon/approvalsView.js +325 -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 +82 -0
- package/lib/quoxon/cells.js +53 -0
- package/lib/quoxon/commandShort.js +112 -0
- package/lib/quoxon/commands.js +479 -0
- package/lib/quoxon/connect.js +654 -0
- package/lib/quoxon/connectCard.js +45 -0
- package/lib/quoxon/consent.js +86 -0
- package/lib/quoxon/context.js +68 -0
- package/lib/quoxon/copy.js +34 -0
- package/lib/quoxon/countdownLine.js +11 -0
- package/lib/quoxon/demo.autoplay.js +29 -0
- package/lib/quoxon/demo.fixtures.js +24 -0
- package/lib/quoxon/demo.js +76 -0
- package/lib/quoxon/devObserver.js +20 -0
- package/lib/quoxon/dividerAocl.js +191 -0
- package/lib/quoxon/dividerRule.js +57 -0
- package/lib/quoxon/dragonModes.js +150 -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 +36 -0
- package/lib/quoxon/events.js +677 -0
- package/lib/quoxon/feedback.js +50 -0
- package/lib/quoxon/gallery.js +237 -0
- 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/indicator.js +197 -0
- package/lib/quoxon/intent.js +52 -0
- package/lib/quoxon/keys.js +164 -0
- package/lib/quoxon/launcher.js +103 -0
- package/lib/quoxon/main.js +176 -0
- package/lib/quoxon/markdown.js +341 -0
- 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 +311 -0
- package/lib/quoxon/modelPrefs.js +345 -0
- package/lib/quoxon/motion/clock.js +70 -0
- package/lib/quoxon/motion/color.js +35 -0
- package/lib/quoxon/motion/frame.js +14 -0
- package/lib/quoxon/motion/level.js +91 -0
- package/lib/quoxon/motion/reveal.js +54 -0
- package/lib/quoxon/motion/ring.js +79 -0
- package/lib/quoxon/mouse.js +211 -0
- package/lib/quoxon/noai.js +57 -0
- package/lib/quoxon/objects/agent.js +61 -0
- package/lib/quoxon/objects/away.js +72 -0
- package/lib/quoxon/objects/crystal.js +100 -0
- package/lib/quoxon/objects/fetch.js +182 -0
- package/lib/quoxon/objects/fleet.js +149 -0
- package/lib/quoxon/objects/health.js +186 -0
- package/lib/quoxon/objects/index.js +106 -0
- package/lib/quoxon/objects/ladder.js +71 -0
- package/lib/quoxon/objects/memory.js +44 -0
- package/lib/quoxon/objects/mirror.js +137 -0
- package/lib/quoxon/objects/model.js +118 -0
- package/lib/quoxon/objects/peek.js +17 -0
- package/lib/quoxon/objects/proof.js +110 -0
- 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/skill.js +105 -0
- package/lib/quoxon/objects/view.js +87 -0
- package/lib/quoxon/objects/workflow.js +119 -0
- package/lib/quoxon/observers.js +42 -0
- package/lib/quoxon/onboarding/copy.js +263 -0
- package/lib/quoxon/onboarding/credential.js +108 -0
- package/lib/quoxon/onboarding/detect.js +22 -0
- package/lib/quoxon/onboarding/dockerOffer.js +45 -0
- package/lib/quoxon/onboarding/failureCause.js +33 -0
- package/lib/quoxon/onboarding/finish.js +134 -0
- package/lib/quoxon/onboarding/index.js +94 -0
- package/lib/quoxon/onboarding/installRunner.js +284 -0
- package/lib/quoxon/onboarding/intent.js +53 -0
- package/lib/quoxon/onboarding/journey.js +73 -0
- package/lib/quoxon/onboarding/preflight.js +156 -0
- package/lib/quoxon/onboarding/probe.js +65 -0
- package/lib/quoxon/onboarding/runner.js +58 -0
- package/lib/quoxon/onboarding/view.js +682 -0
- package/lib/quoxon/orgLabel.js +10 -0
- package/lib/quoxon/orgSwitch.js +33 -0
- package/lib/quoxon/ownBroadcast.js +22 -0
- package/lib/quoxon/palette.js +43 -0
- package/lib/quoxon/print.js +98 -0
- 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 +348 -0
- package/lib/quoxon/qdragon.js +410 -0
- package/lib/quoxon/qdragonFly.js +61 -0
- package/lib/quoxon/qdragonScenes.js +282 -0
- package/lib/quoxon/qeyes.js +213 -0
- package/lib/quoxon/qglyph.js +143 -0
- package/lib/quoxon/qhello.js +58 -0
- package/lib/quoxon/qmotion.js +132 -0
- package/lib/quoxon/render.js +1167 -0
- package/lib/quoxon/resend.js +72 -0
- package/lib/quoxon/roster.js +307 -0
- package/lib/quoxon/route.js +39 -0
- package/lib/quoxon/rows.js +711 -0
- package/lib/quoxon/rowsStore.js +80 -0
- package/lib/quoxon/rules.js +130 -0
- package/lib/quoxon/screenContext.js +150 -0
- package/lib/quoxon/screenFrames.js +254 -0
- package/lib/quoxon/session.js +238 -0
- package/lib/quoxon/sessionDead.js +17 -0
- package/lib/quoxon/signin.js +125 -0
- package/lib/quoxon/skills.js +30 -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 +53 -0
- package/lib/quoxon/slash/cliLeaves.json +948 -0
- package/lib/quoxon/slash/compact.js +90 -0
- 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/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 +88 -0
- package/lib/quoxon/slash/listView.js +30 -0
- package/lib/quoxon/slash/modelCmd.js +261 -0
- package/lib/quoxon/slash/palette.js +118 -0
- package/lib/quoxon/slash/paletteController.js +169 -0
- package/lib/quoxon/slash/panels.js +116 -0
- package/lib/quoxon/slash/picker.js +56 -0
- package/lib/quoxon/slash/restart.js +202 -0
- package/lib/quoxon/slash/resume.js +59 -0
- package/lib/quoxon/slash/rowCmd.js +157 -0
- package/lib/quoxon/slash/rowWalk.js +175 -0
- package/lib/quoxon/slash/settings.js +60 -0
- 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/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/index.js +186 -0
- package/lib/quoxon/sound/mapping.js +120 -0
- package/lib/quoxon/sound/output.js +52 -0
- package/lib/quoxon/sound/synth.js +122 -0
- package/lib/quoxon/stray.js +22 -0
- package/lib/quoxon/suggestCommand.js +59 -0
- package/lib/quoxon/term.js +141 -0
- package/lib/quoxon/textBuffer.js +416 -0
- package/lib/quoxon/theme.js +286 -0
- package/lib/quoxon/tips.js +433 -0
- package/lib/quoxon/toolError.js +41 -0
- package/lib/quoxon/transcript.js +188 -0
- package/lib/quoxon/web/qface.js +89 -0
- package/lib/quoxon/welcome.js +52 -0
- package/lib/quoxon/whatsnew.js +136 -0
- package/lib/quoxon/whatsnew.jsonl +89 -0
- package/lib/quoxon/windowOrg.js +113 -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/app.jsx +289 -0
- package/lib/tui/components/Breadcrumb.jsx +42 -0
- package/lib/tui/components/ChatInput.jsx +68 -0
- package/lib/tui/components/Confirm.jsx +32 -0
- package/lib/tui/components/DataTable.jsx +270 -0
- package/lib/tui/components/DetailPanel.jsx +241 -0
- package/lib/tui/components/HelpOverlay.jsx +67 -0
- package/lib/tui/components/Layout.jsx +129 -0
- package/lib/tui/components/Palette.jsx +141 -0
- package/lib/tui/components/SearchInput.jsx +125 -0
- package/lib/tui/components/Sparkline.jsx +55 -0
- package/lib/tui/components/StatusBar.jsx +133 -0
- package/lib/tui/components/Toast.jsx +52 -0
- package/lib/tui/hooks/useApi.js +44 -0
- package/lib/tui/hooks/useHealth.js +90 -0
- package/lib/tui/hooks/usePolledApi.js +86 -0
- package/lib/tui/hooks/useTerminalSize.js +46 -0
- package/lib/tui/index.js +56 -0
- package/lib/tui/intro/Intro.jsx +332 -0
- package/lib/tui/jsx-loader.js +32 -0
- package/lib/tui/keys.js +87 -0
- package/lib/tui/registry.js +49 -0
- package/lib/tui/theme.js +103 -0
- package/lib/tui/views/AdminView.jsx +398 -0
- package/lib/tui/views/AgentView.jsx +204 -0
- package/lib/tui/views/BackupView.jsx +225 -0
- package/lib/tui/views/BrainView.jsx +223 -0
- package/lib/tui/views/ChatView.jsx +468 -0
- package/lib/tui/views/FilesView.jsx +218 -0
- package/lib/tui/views/FleetView.jsx +234 -0
- package/lib/tui/views/GovernanceView.jsx +453 -0
- package/lib/tui/views/HealthView.jsx +216 -0
- package/lib/tui/views/HyperionDeck.jsx +79 -0
- package/lib/tui/views/InboxView.jsx +228 -0
- package/lib/tui/views/LucidDeck.jsx +719 -0
- package/lib/tui/views/MainMenu.jsx +123 -0
- package/lib/tui/views/MemoryView.jsx +377 -0
- package/lib/tui/views/ObjectivesView.jsx +230 -0
- package/lib/tui/views/OrgView.jsx +229 -0
- package/lib/tui/views/ToolView.jsx +221 -0
- package/lib/tui/views/VaultView.jsx +422 -0
- package/lib/tui/views/WorkflowView.jsx +329 -0
- package/package.json +75 -4
|
@@ -0,0 +1,952 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QUOXON server calls. Thin wrapper over the CLI client so the app can be
|
|
3
|
+
* driven by a fake (demo + tests). All routes per docs/QUOXON_EVENTS_v1.md.
|
|
4
|
+
*/
|
|
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)
|
|
8
|
+
import { buildStreamQuery } from '../commands/chat.js';
|
|
9
|
+
import { loadBundledAssistantsRoster } from '../core/agentAlias.js';
|
|
10
|
+
import { bundledIdentities } from './objects/fetch.js';
|
|
11
|
+
import { recordHeldSlot } from '../commands/slot.js';
|
|
12
|
+
import { askGripe, settleGripe } from '../commands/gripe.js';
|
|
13
|
+
import { sendFeedback } from './feedback.js';
|
|
14
|
+
import { fromObjective, fromExecution, fromApproval, sortRows } from './roster.js';
|
|
15
|
+
import { normChoice } from './model.js';
|
|
16
|
+
import { normPrefs as normModelPrefs } from './modelPrefs.js';
|
|
17
|
+
import { createRowsStore } from './rowsStore.js';
|
|
18
|
+
import { createObjectsApi } from './objects/fetch.js';
|
|
19
|
+
import { createKillerApi } from './slash/killerApi.js';
|
|
20
|
+
import { createUndoOps } from './slash/undoApi.js';
|
|
21
|
+
import { resolveDashboardBase, resolveCollectorBase, detectLanIp } from './hostUrls.js';
|
|
22
|
+
import { grantBody } from './consent.js';
|
|
23
|
+
import { loadJourney } from './onboarding/journey.js';
|
|
24
|
+
import { joinServiceUrl } from '../core/config.js';
|
|
25
|
+
import { runVmCommand } from '../core/vmTui.js';
|
|
26
|
+
import { fetchActivity, heldLines } from '../core/hyperion.js';
|
|
27
|
+
import { createPeekFetchers, peekId } from '../core/peek.js';
|
|
28
|
+
import { askAside } from '../core/aside.js';
|
|
29
|
+
|
|
30
|
+
const missing = (e) => e?.status === 404 || e?.status === 405 || e?.status === 501;
|
|
31
|
+
/** Server steer refusal label -> client reason. Unknown labels are 'ambiguous', never "finished". */
|
|
32
|
+
const steerReason = (label) => label === 'turn_finished' ? 'turn_finished' : (label === 'steer_unsupported' || label === 'unsupported') ? 'unsupported' : 'ambiguous';
|
|
33
|
+
const eventsPath = (c, t) => `/api/v1/conversations/${encodeURIComponent(c)}/turns/${encodeURIComponent(t)}/events`;
|
|
34
|
+
/** Reconnect backoff (ms) for a dropped durable turn; ~1 min in all, then the user is told /resume reattaches. */
|
|
35
|
+
export const REATTACH_BACKOFF_MS = [250, 500, 1000, 2000, 4000, 8000, 15000, 30000];
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Drain one SSE connection of a turn into the consumer, exactly once by seq. Returns null when the turn
|
|
39
|
+
* finished (or the caller aborted), else { error } describing why the connection ended early.
|
|
40
|
+
* turn.finished arrives just after `done`: keep reading to the close, but never hang (3 s grace).
|
|
41
|
+
*/
|
|
42
|
+
async function* readTurn(open, cur, signal) {
|
|
43
|
+
const inner = new AbortController();
|
|
44
|
+
const relay = () => inner.abort();
|
|
45
|
+
if (signal) { if (signal.aborted) inner.abort(); else signal.addEventListener('abort', relay, { once: true }); }
|
|
46
|
+
let grace = null;
|
|
47
|
+
try {
|
|
48
|
+
for await (const ev of open(inner.signal)) {
|
|
49
|
+
if (typeof ev?.seq === 'number') { if (ev.seq <= cur.lastSeq) continue; cur.lastSeq = ev.seq; }
|
|
50
|
+
if (ev?.type === 'turn.started' && ev.conversation_id) cur.conversationId = ev.conversation_id;
|
|
51
|
+
if (ev?.type === 'turn.finished') cur.finished = true;
|
|
52
|
+
yield ev;
|
|
53
|
+
if ((ev.type === 'done' || ev.type === 'error' || ev.type === 'turn.finished') && !grace) grace = setTimeout(() => inner.abort(), 3000);
|
|
54
|
+
if (ev.type === 'turn.finished') return null;
|
|
55
|
+
}
|
|
56
|
+
return signal?.aborted || grace ? null : { error: null }; // closed without turn.finished
|
|
57
|
+
} catch (e) {
|
|
58
|
+
if (signal?.aborted) return null;
|
|
59
|
+
if (grace && inner.signal.aborted) return null; // grace timeout, not a failure
|
|
60
|
+
if (e?.status) throw e; // the server answered with an HTTP error: not a dropped connection
|
|
61
|
+
return { error: e };
|
|
62
|
+
} finally { clearTimeout(grace); signal?.removeEventListener('abort', relay); inner.abort(); } // cancels the HTTP body
|
|
63
|
+
}
|
|
64
|
+
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 }); });
|
|
65
|
+
const listOf = (d, ...keys) => { for (const k of keys) if (Array.isArray(d?.[k])) return d[k]; return Array.isArray(d) ? d : null; };
|
|
66
|
+
|
|
67
|
+
export const SLOTS_UNLICENSED = 'Slots need a licence. This Quox does not have one, so /slot is off. See: quox setup license status';
|
|
68
|
+
// Z2H run-6: every slot write (checkout, /say, /broadcast) names the licence reason the same way.
|
|
69
|
+
/** #1288: a refused vault write, keeping the server's "confirm this private address" hint. */
|
|
70
|
+
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 } : {}) });
|
|
71
|
+
const slotsUnlicensed = (e) => e?.status === 403 && /not licen[cs]ed/i.test(`${e?.code || ''} ${e?.body?.error || ''} ${e?.message || ''}`);
|
|
72
|
+
|
|
73
|
+
/** PRESENCE: only a person at a real terminal is "back". --print, pipes, proof runs and agents are not. */
|
|
74
|
+
export function isInteractiveHuman({ stdin = process.stdin, stdout = process.stdout, env = process.env } = {}) {
|
|
75
|
+
return Boolean(stdin?.isTTY && stdout?.isTTY) && !env.CI;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function createApi({ client, config, auth, windowOrg = null, reattachBackoff = REATTACH_BACKOFF_MS }) {
|
|
79
|
+
let holderSessionFn = null;
|
|
80
|
+
const orgId = () => { try { return auth.getOrgId(); } catch { return null; } };
|
|
81
|
+
const rowsStore = createRowsStore({ client, config, orgId });
|
|
82
|
+
let credsSeen = null; // credentials-file mtime last seen (credentialsChanged)
|
|
83
|
+
const userId = () => { try { return config.getAuth?.()?.user_id || null; } catch { return null; } };
|
|
84
|
+
/** Z2H-R3: the server's public app URL (instance_config app_base_url), or null (not admin, old server, unset). Never throws. */
|
|
85
|
+
const serverAppUrl = async () => {
|
|
86
|
+
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; }
|
|
87
|
+
};
|
|
88
|
+
const journeyUrl = () => { try { return loadJourney()?.dashboardUrl || null; } catch { return null; } };
|
|
89
|
+
const listApprovals = async () => {
|
|
90
|
+
// quox#827: the set comes from the server's one open-approvals function (the same one behind the dashboard badge and LUCID).
|
|
91
|
+
const ep = resolveEndpoint('inbox.approvals.open', { orgId: orgId() });
|
|
92
|
+
try {
|
|
93
|
+
const data = await client.get(ep.path, { service: ep.service });
|
|
94
|
+
if (Array.isArray(data?.approvals)) return data.approvals.map((a) => a.item).filter(Boolean);
|
|
95
|
+
} catch (e) { if (!missing(e)) throw e; } // an older server without the route: fall back to the inbox list below
|
|
96
|
+
const old = resolveEndpoint('inbox.list', { orgId: orgId() });
|
|
97
|
+
const data = await client.get(old.path, { service: old.service, query: { kind: 'approval', include_snoozed: 'true', limit: 200 } }); // status omitted = every ACTIVE status
|
|
98
|
+
const items = data.items || data.data || data || [];
|
|
99
|
+
return Array.isArray(items) ? items : [];
|
|
100
|
+
};
|
|
101
|
+
const conv = (c, t) => `/api/v1/conversations/${encodeURIComponent(c)}/turns/${encodeURIComponent(t)}`;
|
|
102
|
+
/**
|
|
103
|
+
* After a drop: reconnect with backoff, replaying from the last seq we yielded. A 404/410 ends it with
|
|
104
|
+
* that error; running out of attempts throws err.code 'reattach_failed' (the turn may still be running).
|
|
105
|
+
*/
|
|
106
|
+
async function* reattachLoop({ turnId, cur, signal, first, backoff = REATTACH_BACKOFF_MS }) {
|
|
107
|
+
let last = first;
|
|
108
|
+
for (let attempt = 1; attempt <= backoff.length; attempt++) {
|
|
109
|
+
if (signal?.aborted) return;
|
|
110
|
+
yield { type: 'reconnecting', turn_id: turnId, attempt, after: cur.lastSeq };
|
|
111
|
+
await sleepFor(backoff[attempt - 1], signal);
|
|
112
|
+
if (signal?.aborted) return;
|
|
113
|
+
let announced = false;
|
|
114
|
+
const before = cur.lastSeq;
|
|
115
|
+
let drop;
|
|
116
|
+
try {
|
|
117
|
+
drop = yield* readTurn((sig) => (async function* () {
|
|
118
|
+
for await (const ev of client.stream(eventsPath(cur.conversationId, turnId), { service: 'collector', query: { after: cur.lastSeq }, signal: sig, untilClose: true })) {
|
|
119
|
+
if (!announced) { announced = true; yield { type: 'resumed', turn_id: turnId, after: before }; }
|
|
120
|
+
yield ev;
|
|
121
|
+
}
|
|
122
|
+
})(), cur, signal);
|
|
123
|
+
} catch (e) {
|
|
124
|
+
if (e?.status === 404) { const err = new Error('The server no longer knows this turn (it may have been restarted).'); err.code = 'turn_not_found'; err.status = 404; throw err; }
|
|
125
|
+
if (e?.status === 410) { const err = new Error('This turn finished too long ago to replay; its answer is in the conversation history (/resume).'); err.code = 'turn_events_expired'; err.status = 410; throw err; }
|
|
126
|
+
if (e?.status === 401 || e?.status === 403) throw e;
|
|
127
|
+
drop = { error: e };
|
|
128
|
+
}
|
|
129
|
+
if (cur.finished || !drop || signal?.aborted) return;
|
|
130
|
+
last = drop.error || last;
|
|
131
|
+
if (cur.lastSeq > before) attempt = 0; // progress: start the backoff again
|
|
132
|
+
}
|
|
133
|
+
const err = new Error(`Lost the connection to the server${last?.message ? ` (${last.message})` : ''}. Quox keeps working on the turn; /resume reattaches.`);
|
|
134
|
+
err.code = 'reattach_failed';
|
|
135
|
+
throw err;
|
|
136
|
+
}
|
|
137
|
+
/** PRESENCE auto-clear: the human's own first real action. -> { ok, cleared, welcome } (welcome once: { away_text, via }). Never throws. */
|
|
138
|
+
async function noteActivity(action, { quiet = false, interactive = isInteractiveHuman() } = {}) {
|
|
139
|
+
if (!interactive) return { ok: false, cleared: false, welcome: null, skipped: 'not interactive' };
|
|
140
|
+
try {
|
|
141
|
+
const ep = resolveEndpoint('presence.activity');
|
|
142
|
+
const d = await client.post(ep.path, { service: ep.service, retries: 0, timeout: 6000, body: { action, org_id: orgId(), ...(quiet ? { quiet: true } : {}) } });
|
|
143
|
+
return { ok: true, cleared: d?.cleared === true, welcome: d?.welcome || null };
|
|
144
|
+
} catch (e) { return { ok: false, cleared: false, welcome: null, status: e?.status || null }; }
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
...createKillerApi({ client }),
|
|
148
|
+
orgId,
|
|
149
|
+
undoOps: createUndoOps({ client, orgId }), // /undo
|
|
150
|
+
userId,
|
|
151
|
+
/** /peek <id>: the same lookup as `quox peek`, in this window's org. -> peekId result */
|
|
152
|
+
peek: (id) => peekId(id, createPeekFetchers({ client, orgId })),
|
|
153
|
+
/** /aside: a no-tools, no-conversation question (core/aside.js). turns = recent exchanges, read-only context. */
|
|
154
|
+
aside: (question, turns = []) => askAside(client, { question, turns }),
|
|
155
|
+
/** @mention hosts: the Bastion inventory (same read as `quox fleet hosts`). -> [{ id, group, health }] */
|
|
156
|
+
async mentionHosts() {
|
|
157
|
+
const ep = resolveEndpoint('v1.fleet.hosts.list');
|
|
158
|
+
const d = await client.get(ep.path, { service: ep.service, retries: 0, timeout: 8000 });
|
|
159
|
+
return (listOf(d, 'hosts') || []).map(h => ({ id: h.id, group: h.group || '', health: h.status?.health || '' }));
|
|
160
|
+
},
|
|
161
|
+
/** @mention tasks: the newest objectives (one read, cached by the composer for the session). -> [{ id, title, status }] */
|
|
162
|
+
async mentionTasks() {
|
|
163
|
+
const ep = resolveEndpoint('objectives.list');
|
|
164
|
+
const d = await client.get(ep.path, { service: ep.service, query: { limit: 30 }, retries: 0, timeout: 8000 });
|
|
165
|
+
return (listOf(d, 'objectives', 'data') || []).map(o => ({ id: o.id, title: o.title || '', status: o.status || '' }));
|
|
166
|
+
},
|
|
167
|
+
/** Z2H: the same login path as `quox login` (createAuth().login). Returns only what the card needs; tokens are stored by auth, never surfaced. */
|
|
168
|
+
async signIn(email, password) {
|
|
169
|
+
const r = await auth.login(config.getServiceUrl('auth'), email, password);
|
|
170
|
+
return { mfa_required: Boolean(r.mfa_required), access_token: r.mfa_required ? r.access_token : undefined, user: r.user ? { email: r.user.email } : null };
|
|
171
|
+
},
|
|
172
|
+
verifyMfa: (token, code) => auth.verifyMfa(config.getServiceUrl('auth'), token, code),
|
|
173
|
+
/** The last email this profile signed in with (prefill for /login), or ''. */
|
|
174
|
+
lastEmail() { try { return config.getAuth?.()?.email || config.getActiveProfile?.()?.last_email || ''; } catch { return ''; } },
|
|
175
|
+
/**
|
|
176
|
+
* Startup/ongoing check: is the stored session dead? Only a locally expired token is probed, via the refresh seam.
|
|
177
|
+
* -> { alive: true } | { alive: false, reason: 'idle_timeout'|'max_session'|null }. A transient failure reads as alive (we learned nothing).
|
|
178
|
+
*/
|
|
179
|
+
async probeSession() {
|
|
180
|
+
try {
|
|
181
|
+
if (!auth.isExpired?.()) return { alive: true };
|
|
182
|
+
try { rememberEmail(config, config.getAuth?.()?.email); } catch { /* best-effort */ }
|
|
183
|
+
await auth.refresh(config.getServiceUrl('auth'));
|
|
184
|
+
return { alive: true };
|
|
185
|
+
} catch (e) {
|
|
186
|
+
return isSessionDead(e) ? { alive: false, reason: e.reason || null } : { alive: true };
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
/**
|
|
190
|
+
* Stale sign-in: true once each time the credentials file changed on disk since the last call (another window
|
|
191
|
+
* or `quox login` wrote it). A cheap stat; the first call only records the baseline.
|
|
192
|
+
*/
|
|
193
|
+
credentialsChanged() {
|
|
194
|
+
try {
|
|
195
|
+
const m = config.credentialsMtime?.() || 0;
|
|
196
|
+
const was = credsSeen; credsSeen = m;
|
|
197
|
+
return was !== null && m !== was;
|
|
198
|
+
} catch { return false; }
|
|
199
|
+
},
|
|
200
|
+
/** Re-read this profile's credentials from disk. -> true when a signed-in profile is there to retry with. */
|
|
201
|
+
reloadCredentials() {
|
|
202
|
+
try { config.reloadAuth?.(); return Boolean(config.getAuth?.()?.access_token); } catch { return false; }
|
|
203
|
+
},
|
|
204
|
+
/** Inline object views (objects/): skill, workflow, crystal, agent, memory, rules, proof, reconcile. */
|
|
205
|
+
objects: createObjectsApi({ client, orgId, profile: config?._effectiveProfile || null }),
|
|
206
|
+
/** /model choice persisted on the active profile ({ provider, model? } only, never a key); null = auto. */
|
|
207
|
+
modelChoice() { try { return normChoice(config.getActiveProfile?.()?.chat_model); } catch { return null; } },
|
|
208
|
+
setModelChoice(choice) {
|
|
209
|
+
const prof = config.getActiveProfile?.();
|
|
210
|
+
if (!prof) return false;
|
|
211
|
+
const c = normChoice(choice);
|
|
212
|
+
if (c) prof.chat_model = c; else delete prof.chat_model;
|
|
213
|
+
config.save?.();
|
|
214
|
+
return true;
|
|
215
|
+
},
|
|
216
|
+
/** Server-side QuoxSound setting (the dashboard's `soundProfile` preference), or null when unset. */
|
|
217
|
+
async soundSettings() {
|
|
218
|
+
const ep = resolveEndpoint('auth.me.list');
|
|
219
|
+
const d = await client.get(ep.path, { service: ep.service, retries: 0 });
|
|
220
|
+
return (d?.user?.preferences ?? d?.preferences)?.soundProfile ?? null;
|
|
221
|
+
},
|
|
222
|
+
async setSoundSettings(value) {
|
|
223
|
+
const ep = resolveEndpoint('auth.me.update');
|
|
224
|
+
await client.patch(ep.path, { service: ep.service, body: { preferences: { soundProfile: value } } });
|
|
225
|
+
return true;
|
|
226
|
+
},
|
|
227
|
+
/**
|
|
228
|
+
* QUOXON-FW6: model favourites + default. The server is the source of truth: the user's
|
|
229
|
+
* `quoxonModels` preference (PATCH /api/auth/me merges top-level namespaces, the same store as
|
|
230
|
+
* soundProfile). The active CLI profile keeps a copy (model_prefs) for offline starts.
|
|
231
|
+
* -> { prefs, source: 'server' | 'local', error? }.
|
|
232
|
+
*/
|
|
233
|
+
async modelPrefs() {
|
|
234
|
+
const local = () => normModelPrefs(config.getActiveProfile?.()?.model_prefs);
|
|
235
|
+
try {
|
|
236
|
+
const ep = resolveEndpoint('auth.me.list');
|
|
237
|
+
const d = await client.get(ep.path, { service: ep.service, retries: 0 });
|
|
238
|
+
const raw = (d?.user?.preferences ?? d?.preferences)?.quoxonModels;
|
|
239
|
+
const prefs = normModelPrefs(raw);
|
|
240
|
+
const prof = config.getActiveProfile?.();
|
|
241
|
+
if (prof && raw !== undefined) { prof.model_prefs = prefs; config.save?.(); }
|
|
242
|
+
const useLocal = raw === undefined && local().favourites.length;
|
|
243
|
+
return { prefs: useLocal ? local() : prefs, source: 'server' };
|
|
244
|
+
} catch (e) { return { prefs: local(), source: 'local', error: e?.message || 'failed' }; }
|
|
245
|
+
},
|
|
246
|
+
/** -> { ok, synced, error? }: the local copy is always written; synced=false: refused. */
|
|
247
|
+
async setModelPrefs(value) {
|
|
248
|
+
const prefs = normModelPrefs(value);
|
|
249
|
+
const prof = config.getActiveProfile?.();
|
|
250
|
+
if (prof) { prof.model_prefs = prefs; config.save?.(); }
|
|
251
|
+
try {
|
|
252
|
+
const ep = resolveEndpoint('auth.me.update');
|
|
253
|
+
const body = { preferences: { quoxonModels: prefs } };
|
|
254
|
+
await client.patch(ep.path, { service: ep.service, body, retries: 0 });
|
|
255
|
+
return { ok: true, synced: true };
|
|
256
|
+
} catch (e) { return { ok: Boolean(prof), synced: false, error: e?.message || 'failed' }; }
|
|
257
|
+
},
|
|
258
|
+
localModelPrefs() {
|
|
259
|
+
try { return normModelPrefs(config.getActiveProfile?.()?.model_prefs); }
|
|
260
|
+
catch { return normModelPrefs(null); }
|
|
261
|
+
},
|
|
262
|
+
/**
|
|
263
|
+
* QUOXON-FW17 / ROWS: custom rows. url, quox and plugin rows sync per user (preference namespace `quoxonRows`);
|
|
264
|
+
* cmd rows stay in a local file. One implementation, shared with `quox rows` (rowsStore.js).
|
|
265
|
+
*/
|
|
266
|
+
...rowsStore,
|
|
267
|
+
/** Local copy on the active CLI profile: offline start + fallback store. */
|
|
268
|
+
localSoundProfile() { try { return config.getActiveProfile?.()?.sound_profile || null; } catch { return null; } },
|
|
269
|
+
setLocalSoundProfile(p) { const prof = config.getActiveProfile?.(); if (!prof) return false; prof.sound_profile = p; config.save?.(); return true; },
|
|
270
|
+
/**
|
|
271
|
+
* One turn. Durable by default (events v1.1, docs/QUOXON_EVENTS_v1_1.md): the server keeps the turn running
|
|
272
|
+
* if this connection drops, every event carries a `seq`, and a drop before turn.finished is followed by an
|
|
273
|
+
* automatic reattach (GET .../turns/:tid/events?after=<last seq>) with backoff. Events are yielded exactly once
|
|
274
|
+
* by seq. While reconnecting it yields { type:'reconnecting', attempt } and, once back, { type:'resumed', after }.
|
|
275
|
+
* A server without v1.1 (no seq on its events) gets the v1 behaviour: no reattach.
|
|
276
|
+
*/
|
|
277
|
+
async *stream({ turnId, message, agent, conversationId, context, provider, model, promptProfile, heldSlot = null, durable = true, overrideId = undefined, turnBudgetMs = undefined, mode = undefined, handoffFrom = undefined }, signal) {
|
|
278
|
+
const body = buildStreamQuery({ queryId: turnId, message, agent: agent || 'quox', sessionId: conversationId, context });
|
|
279
|
+
body.turn_id = turnId;
|
|
280
|
+
if (durable) body.durable = true; // the server keeps the turn running when we disconnect (v1.1)
|
|
281
|
+
if (Number(turnBudgetMs) > 0) body.turn_budget_ms = Math.floor(Number(turnBudgetMs)); // quox#1090: a delegation hop may use the turn's budget
|
|
282
|
+
if (provider) body.provider = provider; // a request: the server policy decides, turn.started says what ran
|
|
283
|
+
if (provider && model) body.model = model;
|
|
284
|
+
if (promptProfile === 'compact' || promptProfile === 'full') body.prompt_profile = promptProfile; // D3: server default when absent
|
|
285
|
+
if (conversationId) body.conversation_id = conversationId;
|
|
286
|
+
if (mode === 'plan') body.mode = 'plan'; // PLAN MODE: the server makes no state-changing tool eligible and refuses writes
|
|
287
|
+
if (heldSlot?.name && heldSlot?.session) body.held_slot = heldSlot; // QUOXON-MESH: slot_say signs as this window's slot
|
|
288
|
+
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
|
|
289
|
+
const ep = resolveEndpoint('chat.stream');
|
|
290
|
+
const cur = { lastSeq: 0, finished: false, conversationId: conversationId || null };
|
|
291
|
+
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);
|
|
292
|
+
if (cur.finished || !drop || !durable || signal?.aborted) return;
|
|
293
|
+
if (!cur.lastSeq || !cur.conversationId) { if (drop.error) throw drop.error; return; } // v1 server: nothing to reattach to
|
|
294
|
+
yield* reattachLoop({ turnId, cur, signal, first: drop.error, backoff: reattachBackoff });
|
|
295
|
+
},
|
|
296
|
+
/**
|
|
297
|
+
* Reattach to a running (or just finished) turn and yield its events after `after`, exactly once, until
|
|
298
|
+
* turn.finished. Same reconnect behaviour as stream(). Throws err.code 'turn_not_found' (404) or
|
|
299
|
+
* 'turn_events_expired' (410).
|
|
300
|
+
*/
|
|
301
|
+
async *reattach({ conversationId, turnId, after = 0 }, signal) {
|
|
302
|
+
const cur = { lastSeq: after, finished: false, conversationId };
|
|
303
|
+
const drop = yield* readTurn((sig) => client.stream(eventsPath(conversationId, turnId), { service: 'collector', query: { after }, signal: sig, untilClose: true }), cur, signal);
|
|
304
|
+
if (cur.finished || !drop || signal?.aborted) return;
|
|
305
|
+
yield* reattachLoop({ turnId, cur, signal, first: drop.error, backoff: reattachBackoff });
|
|
306
|
+
},
|
|
307
|
+
/** -> { running:true, turnId, lastSeq } | { running:false } | null (route missing or failed: never "not running"). */
|
|
308
|
+
async runningTurn(conversationId) {
|
|
309
|
+
if (!conversationId) return { running: false };
|
|
310
|
+
try {
|
|
311
|
+
const r = await client.get(`/api/v1/conversations/${encodeURIComponent(conversationId)}/running`, { service: 'collector', retries: 0 });
|
|
312
|
+
if (r?.running === true && r.turn_id) return { running: true, turnId: r.turn_id, lastSeq: r.last_seq ?? null };
|
|
313
|
+
return r?.running === false ? { running: false } : null;
|
|
314
|
+
} catch { return null; }
|
|
315
|
+
},
|
|
316
|
+
/**
|
|
317
|
+
* -> { ok:true } | { ok:false, reason }. reason: 'unsupported' | 'turn_finished' (409, safe to queue)
|
|
318
|
+
* | 'denied' (403 steer_denied_by_policy) | 'ambiguous' (network, timeout, 5xx: may have been applied).
|
|
319
|
+
* NEVER retried automatically; steerId lets the server de-duplicate a deliberate resend.
|
|
320
|
+
*/
|
|
321
|
+
async steer(conversationId, turnId, text, steerId = null) {
|
|
322
|
+
if (!conversationId) return { ok: false, reason: 'unsupported' };
|
|
323
|
+
try {
|
|
324
|
+
const body = steerId ? { text, steer_id: steerId } : { text };
|
|
325
|
+
const r = await client.post(`${conv(conversationId, turnId)}/steer`, { service: 'collector', body, retries: 0 });
|
|
326
|
+
if (r && r.queued === false) return { ok: false, reason: steerReason(r.reason) };
|
|
327
|
+
return { ok: true };
|
|
328
|
+
} catch (e) {
|
|
329
|
+
if (e?.status === 403) return { ok: false, reason: 'denied', message: e.code === 'steer_denied_by_policy' ? 'steer_denied_by_policy' : (e.message || 'forbidden') };
|
|
330
|
+
// Only say "finished" when the server says so; an unlabelled 409 may have been applied.
|
|
331
|
+
if (e?.status === 409) return { ok: false, reason: steerReason(e.code || e.detail?.code || e.detail?.reason || e.reason) };
|
|
332
|
+
if (e?.status === 404 || e?.status === 405) return { ok: false, reason: 'unsupported' };
|
|
333
|
+
return { ok: false, reason: 'ambiguous', message: e?.message || 'failed' };
|
|
334
|
+
}
|
|
335
|
+
},
|
|
336
|
+
/** -> { ok } ; ok:false means the server has no interrupt route (fall back to local abort) */
|
|
337
|
+
async interrupt(conversationId, turnId) {
|
|
338
|
+
if (!conversationId) return { ok: false };
|
|
339
|
+
try { await client.post(`${conv(conversationId, turnId)}/interrupt`, { service: 'collector', body: {} }); return { ok: true }; }
|
|
340
|
+
catch { return { ok: false }; }
|
|
341
|
+
},
|
|
342
|
+
/** -> { exists:true } | { exists:false } | { exists:null, reason } ; null = could not verify (never "not found"). */
|
|
343
|
+
async conversationExists(conversationId) {
|
|
344
|
+
try {
|
|
345
|
+
const r = await client.get(`/api/v1/conversations/${encodeURIComponent(conversationId)}/turns`, { service: 'collector', query: { limit: 1 }, retries: 0 });
|
|
346
|
+
const turns = r?.turns;
|
|
347
|
+
if (!Array.isArray(turns)) return { exists: null, reason: 'unexpected response' };
|
|
348
|
+
return { exists: turns.length > 0 };
|
|
349
|
+
} catch (e) { return { exists: null, reason: e?.status === 404 || e?.status === 405 ? 'route not available' : (e?.message || 'failed') }; }
|
|
350
|
+
},
|
|
351
|
+
/**
|
|
352
|
+
* -> [{ agent_id, name }] ; every agent id you can talk to: the live core registry (the list `quox agent attach`
|
|
353
|
+
* validates against, with display names from the bundled roster) plus your org's own agents (`quox agent list`).
|
|
354
|
+
* Throws only when no source answered at all.
|
|
355
|
+
*/
|
|
356
|
+
async agents() {
|
|
357
|
+
const roster = loadBundledAssistantsRoster();
|
|
358
|
+
const nameOf = new Map(roster.map(r => [r.id, r.name || r.shortName || r.id]));
|
|
359
|
+
const out = new Map(); let ok = 0; let lastErr = null;
|
|
360
|
+
let ids = null; const roleOf = (id) => { ids ??= bundledIdentities(); const i = ids.get(id); return i?.tagline || i?.description || null; };
|
|
361
|
+
try {
|
|
362
|
+
const ep = resolveEndpoint('v1.tools.agents.list');
|
|
363
|
+
const d = await client.get(ep.path, { service: ep.service, retries: 0 }); // authenticated: the collector's default-deny gate 401s a noAuth read
|
|
364
|
+
for (const id of Object.keys(d?.agentTools || {})) out.set(id, { agent_id: id, name: nameOf.get(id) || id, role: roleOf(id), status: 'registered' });
|
|
365
|
+
ok++;
|
|
366
|
+
} 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++; }
|
|
367
|
+
try {
|
|
368
|
+
const ep = resolveEndpoint('agents.list', { orgId: orgId() });
|
|
369
|
+
const rows = listOf(await client.get(ep.path, { service: ep.service, query: { includeTools: 'false' }, retries: 0 }), 'agents', 'data', 'items');
|
|
370
|
+
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 });
|
|
371
|
+
if (rows) ok++;
|
|
372
|
+
} catch (e) { lastErr = lastErr || e; }
|
|
373
|
+
if (!ok) throw lastErr || new Error('no agent list available');
|
|
374
|
+
const list = [...out.values()];
|
|
375
|
+
// The live registry failed and the bundled roster stood in: say so, so a miss is never read as "no such agent".
|
|
376
|
+
if (lastErr) Object.defineProperty(list, 'listError', { value: lastErr });
|
|
377
|
+
return list;
|
|
378
|
+
},
|
|
379
|
+
/** -> saved turns of one of YOUR conversations, oldest first [{ id, role, content }]; [] when none or not yours. */
|
|
380
|
+
async conversationTurns(conversationId) {
|
|
381
|
+
const d = await client.get(`/api/v1/conversations/${encodeURIComponent(conversationId)}/turns`, { service: 'collector', query: { limit: 200 }, retries: 0 });
|
|
382
|
+
const rows = listOf(d, 'turns');
|
|
383
|
+
if (!rows) throw new Error('unexpected response');
|
|
384
|
+
return rows;
|
|
385
|
+
},
|
|
386
|
+
/** -> the org-wide live index (GET /api/v1/sessions, QUOXON J1): { rows, groups, counts, sources }. */
|
|
387
|
+
async sessionsIndex() {
|
|
388
|
+
const d = await client.get('/api/v1/sessions', { service: 'collector', retries: 0 });
|
|
389
|
+
if (!d || !Array.isArray(d.groups)) throw new Error('unexpected response');
|
|
390
|
+
return d;
|
|
391
|
+
},
|
|
392
|
+
/** GET /api/consent: this user's AI data sharing record ({ ai_data_sharing_consent, provider_consent }). */
|
|
393
|
+
async consentStatus() {
|
|
394
|
+
const ep = resolveEndpoint('consent.list');
|
|
395
|
+
return client.get(ep.path, { service: ep.service, retries: 0 });
|
|
396
|
+
},
|
|
397
|
+
/** Grant AI data sharing consent for one provider (the user's own act; keeps the other providers' settings). */
|
|
398
|
+
async grantConsent(provider) {
|
|
399
|
+
const lst = resolveEndpoint('consent.list');
|
|
400
|
+
let cur = null; try { cur = await client.get(lst.path, { service: lst.service, retries: 0 }); } catch { cur = null; }
|
|
401
|
+
const ep = resolveEndpoint('consent.create');
|
|
402
|
+
await client.post(ep.path, { service: ep.service, body: grantBody(cur, provider), retries: 0 });
|
|
403
|
+
return true;
|
|
404
|
+
},
|
|
405
|
+
/** The dashboard address to print (never localhost when a better one is known). */
|
|
406
|
+
async dashboardBase() {
|
|
407
|
+
const profileUrl = config.getActiveProfile?.()?.url || config.getBaseUrl();
|
|
408
|
+
return resolveDashboardBase({ serverApp: await serverAppUrl(), profileUrl, journeyUrl: journeyUrl(), lanIp: detectLanIp() }).url;
|
|
409
|
+
},
|
|
410
|
+
/** POST /api/v1/conversations/:id/compact (server-side summary; a model call, so a long timeout and never retried). */
|
|
411
|
+
async compactConversation(conversationId) {
|
|
412
|
+
return client.post(`/api/v1/conversations/${encodeURIComponent(conversationId)}/compact`, { service: 'collector', body: {}, retries: 0, timeout: 180000 });
|
|
413
|
+
},
|
|
414
|
+
/** GET /api/v1/conversations/:id/context -> { turns, compacted_turns, summary_present, summary_at, tokens, provider, model }. */
|
|
415
|
+
async conversationContext(conversationId) {
|
|
416
|
+
return client.get(`/api/v1/conversations/${encodeURIComponent(conversationId)}/context`, { service: 'collector', retries: 0 });
|
|
417
|
+
},
|
|
418
|
+
/** Persistent memory answers for this login (the same call as `quox memory stats`). -> { ok } | { ok:false, error }. */
|
|
419
|
+
async memoryStatus() {
|
|
420
|
+
try {
|
|
421
|
+
const ep = resolveEndpoint('memory.stats.list');
|
|
422
|
+
await client.get(ep.path, { service: ep.service, retries: 0 });
|
|
423
|
+
return { ok: true };
|
|
424
|
+
} catch (e) { return { ok: false, error: e?.status ? `HTTP ${e.status}` : (e?.message || 'failed') }; }
|
|
425
|
+
},
|
|
426
|
+
/** The `quox doctor` checks, run in this process. */
|
|
427
|
+
async doctorChecks() { const { runDoctorChecks } = await import('../commands/doctor.js'); return runDoctorChecks({ config }); },
|
|
428
|
+
/** C4: the `quox server status` panel rows for the local install. -> { ok, rows } | { ok:false, error }. */
|
|
429
|
+
async serverStatus() {
|
|
430
|
+
const { createSys } = await import('../server/sys.js');
|
|
431
|
+
const { resolveInstall, backupsRoot } = await import('../server/install.js');
|
|
432
|
+
const { gatherStatus, statusRows, defaultUrls } = await import('../server/status.js');
|
|
433
|
+
const sys = createSys();
|
|
434
|
+
const install = await resolveInstall({ sys });
|
|
435
|
+
if (install.error) return { ok: false, error: install.error };
|
|
436
|
+
const st = await gatherStatus({ install, sys, urls: defaultUrls(config), backups: backupsRoot() });
|
|
437
|
+
return { ok: true, rows: statusRows(st) };
|
|
438
|
+
},
|
|
439
|
+
/** /dashboard: mint a one-time sign-in link for this profile (see slash/dashboard.js). Never throws; kind 'link' | 'plain' | 'error'. */
|
|
440
|
+
async dashboardLink(page) {
|
|
441
|
+
const { runDashboard } = await import('./slash/dashboard.js');
|
|
442
|
+
const profileUrl = config.getActiveProfile?.()?.url || config.getBaseUrl();
|
|
443
|
+
const baseUrl = resolveDashboardBase({ serverApp: await serverAppUrl(), profileUrl, journeyUrl: journeyUrl(), lanIp: detectLanIp() }).url;
|
|
444
|
+
return runDashboard({ page, baseUrl, mint: () => { const ep = resolveEndpoint('auth.login-link'); return client.post(ep.path, { service: ep.service, body: {}, retries: 0 }); } });
|
|
445
|
+
},
|
|
446
|
+
/** Z2H-R3: the address to print for the host installer (never localhost when a better one is known). */
|
|
447
|
+
async hostsBaseUrl() {
|
|
448
|
+
let collectorUrl = null; try { collectorUrl = new URL(config.getServiceUrl('collector')).origin; } catch { /* none */ }
|
|
449
|
+
return resolveCollectorBase({ collectorUrl, serverApp: await serverAppUrl(), journeyUrl: journeyUrl(), lanIp: detectLanIp() }).url;
|
|
450
|
+
},
|
|
451
|
+
/** Z2H-3c: can `${base}/install` be served? -> { ok, status } or null when unreachable (never throws, 4 s cap). */
|
|
452
|
+
async hostInstallCheck(base) {
|
|
453
|
+
if (!base || /^<|example/.test(String(base))) return null;
|
|
454
|
+
const ctl = new AbortController(); const t = setTimeout(() => ctl.abort(), 4000);
|
|
455
|
+
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 }; }
|
|
456
|
+
catch { return null; } finally { clearTimeout(t); }
|
|
457
|
+
},
|
|
458
|
+
serverUrl() { try { return new URL(config.getServiceUrl('collector')).origin; } catch { return null; } },
|
|
459
|
+
/** /config settings on the active profile. */
|
|
460
|
+
settings() { try { const q = config.getActiveProfile?.()?.quoxon; return q && typeof q === 'object' ? { ...q } : {}; } catch { return {}; } },
|
|
461
|
+
saveSetting(id, value) {
|
|
462
|
+
try { const prof = config.getActiveProfile?.(); if (!prof) return false; prof.quoxon = { ...(prof.quoxon || {}), [id]: value }; config.save?.(); return true; } catch { return false; }
|
|
463
|
+
},
|
|
464
|
+
/** -> [{ id, title, updated_at, turn_count }] ; GET /api/v1/conversations (title is server-derived, no rename route). */
|
|
465
|
+
async conversations(limit = 20) {
|
|
466
|
+
const d = await client.get('/api/v1/conversations', { service: 'collector', query: { limit }, retries: 0 });
|
|
467
|
+
const rows = listOf(d, 'conversations');
|
|
468
|
+
if (!rows) throw new Error('unexpected response');
|
|
469
|
+
return rows;
|
|
470
|
+
},
|
|
471
|
+
/**
|
|
472
|
+
* Same call as `quox slot checkout <name> --create --session <s>` (cli/lib/commands/slot.js), then the same
|
|
473
|
+
* local held-slots record. The hold keeper and slot wait loop are NOT started here.
|
|
474
|
+
* -> { ok:true, slot } | { ok:false, message }.
|
|
475
|
+
*/
|
|
476
|
+
/**
|
|
477
|
+
* /gripe: the same governed path as `quox gripe --yes` (the GRIPE agent over chat, then the deterministic
|
|
478
|
+
* file when the agent did not). The client asks the user first, so the agent is told to file without asking.
|
|
479
|
+
* -> { ok:true, response, url, superseded, notes } | { ok:false, message }.
|
|
480
|
+
*/
|
|
481
|
+
async gripe(text, { yes = true } = {}) {
|
|
482
|
+
try {
|
|
483
|
+
const options = { yes };
|
|
484
|
+
const data = await askGripe({ client, complaint: text, yes, orgId: orgId() });
|
|
485
|
+
const r = await settleGripe({ client, data, complaint: text, options, orgId: orgId() });
|
|
486
|
+
return { ok: true, response: r.response, url: r.url, superseded: r.superseded, notes: r.notes };
|
|
487
|
+
} catch (e) {
|
|
488
|
+
return { ok: false, message: e?.status ? `HTTP ${e.status}: ${e.message}` : (e?.message || 'failed') };
|
|
489
|
+
}
|
|
490
|
+
},
|
|
491
|
+
/**
|
|
492
|
+
* /feedback: POST the previewed payload to the public quox.ai feedback API (not the org's server, not GitHub).
|
|
493
|
+
* Anonymous by design: no token, cookie, org or profile goes with it. -> { ok:true, id, url } | { ok:false, message }.
|
|
494
|
+
*/
|
|
495
|
+
async feedback(payload, { version = null } = {}) { return sendFeedback(payload, { version }); },
|
|
496
|
+
/** Installed, enabled plugin ids for the org (GET plugins.list, as `quox plugin list`). Throws on failure so it never reads as "none". */
|
|
497
|
+
async installedPlugins() {
|
|
498
|
+
const id = orgId();
|
|
499
|
+
if (!id) throw new Error('no org');
|
|
500
|
+
const ep = resolveEndpoint('plugins.list', { orgId: id });
|
|
501
|
+
const data = await client.get(ep.path, { service: ep.service, retries: 0 });
|
|
502
|
+
const rows = listOf(data, 'plugins', 'data', 'items');
|
|
503
|
+
if (!rows) throw new Error('unexpected response');
|
|
504
|
+
return rows.filter(r => r.is_enabled !== false && r.is_enabled !== 0).map(r => r.plugin_id).filter(Boolean);
|
|
505
|
+
},
|
|
506
|
+
/** 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". */
|
|
507
|
+
async hostCount() {
|
|
508
|
+
const ep = resolveEndpoint('v1.agents.list');
|
|
509
|
+
const rows = listOf(await client.get(ep.path, { service: ep.service, retries: 0 }), 'agents', 'data', 'items');
|
|
510
|
+
if (!rows) throw new Error('unexpected response');
|
|
511
|
+
return rows.length;
|
|
512
|
+
},
|
|
513
|
+
/** -> [{ name, host_name, assigned_to, ... }] ; GET /api/v1/slots (throws on failure, never reads as empty). */
|
|
514
|
+
async slots() {
|
|
515
|
+
const ep = resolveEndpoint('v1.slots.list');
|
|
516
|
+
const rows = listOf(await client.get(ep.path, { service: ep.service, retries: 0 }), 'slots');
|
|
517
|
+
if (!rows) throw new Error('unexpected response');
|
|
518
|
+
return rows;
|
|
519
|
+
},
|
|
520
|
+
/**
|
|
521
|
+
* Post a message into a slot room (same call as `quox slot say`). QUOXON-MESH: `from` signs it as this window
|
|
522
|
+
* ("quox1 (Adam)"); `fast` answers once the line is in the slot, without waiting on the chat-app bridges.
|
|
523
|
+
* -> { ok:true, message } | { ok:false, message, status }.
|
|
524
|
+
*/
|
|
525
|
+
/** SLOTS-REPLY2: the window registers how to name its checkout session; slotSay then sends it as holder_session. */
|
|
526
|
+
bindHolderSession(fn) { holderSessionFn = typeof fn === 'function' ? fn : null; },
|
|
527
|
+
async slotSay(name, text, { from = null, fast = false, roomSession = null, fromSlot = null, holderSession = holderSessionFn ? holderSessionFn() : null } = {}) {
|
|
528
|
+
const ep = resolveEndpoint('v1.slots.say', { name });
|
|
529
|
+
try {
|
|
530
|
+
// SLOTS-REPLY: fromSlot is this window's held slot, its reply address (the server verifies the hold).
|
|
531
|
+
const d = await client.post(ep.path, { service: ep.service, retries: 0, body: { text, org_id: orgId(), ...(roomSession ? { room_session: roomSession } : from ? { from } : {}), ...(!roomSession && fromSlot ? { from_slot: fromSlot } : {}), ...(!roomSession && holderSession ? { holder_session: holderSession } : {}), ...(fast ? { bridge_report: false } : {}) } });
|
|
532
|
+
return { ok: true, message: d?.message || null, roomPending: d?.room_pending === true, presence: d?.presence || null, humanPresence: d?.human_presence || null };
|
|
533
|
+
} catch (e) {
|
|
534
|
+
if (slotsUnlicensed(e)) return { ok: false, unlicensed: true, status: 403, message: SLOTS_UNLICENSED };
|
|
535
|
+
return { ok: false, message: e?.status === 404 ? `no slot named ${name}` : (e?.message || 'failed'), status: e?.status || null };
|
|
536
|
+
}
|
|
537
|
+
},
|
|
538
|
+
/** QUOXON-BROADCAST: fan a message out to the org's held slots. -> { ok:true, data } | { ok:false, message, status }. */
|
|
539
|
+
async slotBroadcast({ text, slots = null, priority = 'normal', ack = false, from = null } = {}) {
|
|
540
|
+
const ep = resolveEndpoint('v1.slots.broadcast');
|
|
541
|
+
try {
|
|
542
|
+
const data = await client.post(ep.path, { service: ep.service, retries: 0, body: { text, priority, ack, org_id: orgId(), ...(slots ? { slots } : {}), ...(from ? { from } : {}) } });
|
|
543
|
+
return { ok: true, data };
|
|
544
|
+
} catch (e) {
|
|
545
|
+
if (slotsUnlicensed(e)) return { ok: false, unlicensed: true, status: 403, message: SLOTS_UNLICENSED };
|
|
546
|
+
return { ok: false, message: e?.message || 'failed', status: e?.status || null };
|
|
547
|
+
}
|
|
548
|
+
},
|
|
549
|
+
async slotBroadcastStatus(id) {
|
|
550
|
+
const ep = resolveEndpoint('v1.slots.broadcast.get', { id });
|
|
551
|
+
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 }; }
|
|
552
|
+
},
|
|
553
|
+
// POKE-WAKE: slot control (server allows a human org owner/admin only; agents are refused).
|
|
554
|
+
async slotControl(slot, command) {
|
|
555
|
+
const ep = resolveEndpoint('v1.slots.control', { name: slot });
|
|
556
|
+
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 }; }
|
|
557
|
+
},
|
|
558
|
+
async slotControlGet(slot, id) {
|
|
559
|
+
const ep = resolveEndpoint('v1.slots.control.get', { name: slot, id });
|
|
560
|
+
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 }; }
|
|
561
|
+
},
|
|
562
|
+
async slotAck(id, slot) {
|
|
563
|
+
const ep = resolveEndpoint('v1.slots.broadcast.ack', { id });
|
|
564
|
+
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 }; }
|
|
565
|
+
},
|
|
566
|
+
/** QUOXON-MESH: the slot's live messages (SSE). Yields { type: 'connected' | 'slot.message' | 'reconnect' }. */
|
|
567
|
+
async *slotStream(name, { since = null, excludeFrom = null, session = null } = {}, signal) {
|
|
568
|
+
const ep = resolveEndpoint('v1.slots.stream', { name });
|
|
569
|
+
yield* client.stream(ep.path, { service: ep.service, query: { org_id: orgId(), since, exclude_from: excludeFrom, ...(session ? { session } : {}) }, signal, untilClose: true });
|
|
570
|
+
},
|
|
571
|
+
/** QUOXON-ROOMS: join / leave a slot as a room (no hold). -> { ok, already? | removed? } | { ok:false, message, status }. */
|
|
572
|
+
async slotJoin(name, session, person = null) {
|
|
573
|
+
const ep = resolveEndpoint('v1.slots.join', { name });
|
|
574
|
+
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 }; }
|
|
575
|
+
catch (e) { return { ok: false, message: e?.status === 404 ? `no slot named ${name}` : (e?.message || 'failed'), status: e?.status || null }; }
|
|
576
|
+
},
|
|
577
|
+
async slotLeave(name, session) {
|
|
578
|
+
const ep = resolveEndpoint('v1.slots.leave', { name });
|
|
579
|
+
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 }; }
|
|
580
|
+
catch (e) { return { ok: false, message: e?.message || 'failed', status: e?.status || null }; }
|
|
581
|
+
},
|
|
582
|
+
/** The rooms this session has joined (server truth, survives a restart). -> [{ slot_name, joined_at }]; throws on failure. */
|
|
583
|
+
async slotRooms(session) {
|
|
584
|
+
const ep = resolveEndpoint('v1.slots.rooms');
|
|
585
|
+
const d = await client.get(ep.path, { service: ep.service, retries: 0, query: { session, org_id: orgId() } });
|
|
586
|
+
return Array.isArray(d?.rooms) ? d.rooms : [];
|
|
587
|
+
},
|
|
588
|
+
/** Long-poll fallback for a server without the stream route (same call as `quox slot wait`). */
|
|
589
|
+
async slotWait(name, { since = null, excludeFrom = null } = {}) {
|
|
590
|
+
const ep = resolveEndpoint('v1.slots.wait', { name });
|
|
591
|
+
return client.get(ep.path, { service: ep.service, retries: 0, timeout: 40000, query: { org_id: orgId(), since, exclude_from: excludeFrom, timeout_ms: 25000 } });
|
|
592
|
+
},
|
|
593
|
+
/** Last `limit` messages of a slot, oldest first (same call as `quox slot read`). Throws on failure. */
|
|
594
|
+
async slotMessages(name, limit = 20) {
|
|
595
|
+
const ep = resolveEndpoint('v1.slots.messages', { name });
|
|
596
|
+
const rows = listOf(await client.get(ep.path, { service: ep.service, retries: 0, query: { org_id: orgId(), limit } }), 'messages');
|
|
597
|
+
if (!rows) throw new Error('unexpected response');
|
|
598
|
+
return rows;
|
|
599
|
+
},
|
|
600
|
+
/** The signed-in human's display name for slot lines (display_name, else the email's local part). */
|
|
601
|
+
displayName() {
|
|
602
|
+
try { const a = config.getAuth?.() || {}; return a.display_name || (a.email ? String(a.email).split('@')[0] : null); } catch { return null; }
|
|
603
|
+
},
|
|
604
|
+
/** `take` = the same signed-in user's other/renamed/restarted session takes a LIVE hold (stale holds of yours are reclaimed regardless). */
|
|
605
|
+
async checkoutSlot(name, session, { take = false } = {}) {
|
|
606
|
+
const ep = resolveEndpoint('v1.slots.checkout', { name });
|
|
607
|
+
try {
|
|
608
|
+
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 } : {}) } });
|
|
609
|
+
recordHeldSlot(name, session);
|
|
610
|
+
return { ok: true, slot: data?.slot || data, tookOver: data?.took_over || null };
|
|
611
|
+
} catch (e) {
|
|
612
|
+
if (e?.status === 409) {
|
|
613
|
+
const d = e.detail || {};
|
|
614
|
+
if (d.assigned_to) return { ok: false, message: `slot ${name} is assigned to ${d.assigned_to}` };
|
|
615
|
+
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 } };
|
|
616
|
+
}
|
|
617
|
+
// Z2H-R5 C: a free install answers 403 "QuoxSlots is not licensed": say that, not a bare 403.
|
|
618
|
+
if (slotsUnlicensed(e)) {
|
|
619
|
+
return { ok: false, unlicensed: true, transient: false, status: 403, message: SLOTS_UNLICENSED };
|
|
620
|
+
}
|
|
621
|
+
// No answer, or the server failing (timeout, network, 5xx, e.g. a collector mid-redeploy): worth retrying.
|
|
622
|
+
const st = e?.status || null;
|
|
623
|
+
return { ok: false, message: e?.message || 'failed', status: st, transient: !st || st >= 500 || st === 429 || st === 408 };
|
|
624
|
+
}
|
|
625
|
+
},
|
|
626
|
+
/** Keep a TUI-held slot alive (the hold lapses to stale after 15m without one). -> { ok } */
|
|
627
|
+
async slotHeartbeat(name, session) {
|
|
628
|
+
const ep = resolveEndpoint('v1.slots.heartbeat', { name });
|
|
629
|
+
try { await client.post(ep.path, { service: ep.service, body: { holder_session: session, org_id: orgId() } }); return { ok: true }; }
|
|
630
|
+
catch (e) { return { ok: false, message: e?.message || 'failed' }; }
|
|
631
|
+
},
|
|
632
|
+
/** quox#1183: the org's hats -> [{ id, key, name, definition }]. Same endpoint as `quox hat list`. */
|
|
633
|
+
async hats() {
|
|
634
|
+
const ep = resolveEndpoint('hat.list', { orgId: orgId() });
|
|
635
|
+
const d = await client.get(ep.path, { service: ep.service });
|
|
636
|
+
return Array.isArray(d?.hats) ? d.hats.filter(h => h.status !== 'archived') : [];
|
|
637
|
+
},
|
|
638
|
+
/** CUSTOM-HATS: create an org hat of your own -> { hat, warnings }. Throws the server's refusal unchanged. */
|
|
639
|
+
async createHat({ key, name, definition, visibility = 'org' }) {
|
|
640
|
+
const ep = resolveEndpoint('hat.create', { orgId: orgId() });
|
|
641
|
+
return client.post(ep.path, { service: ep.service, retries: 0, body: { key, name, definition, visibility } });
|
|
642
|
+
},
|
|
643
|
+
/** CUSTOM-HATS: a new revision of a hat -> { hat, warnings }. */
|
|
644
|
+
async updateHat(hatId, { definition, name, change_reason }) {
|
|
645
|
+
const ep = resolveEndpoint('hat.update', { orgId: orgId(), hatId });
|
|
646
|
+
return client.put(ep.path, { service: ep.service, retries: 0, body: { definition, ...(name ? { name } : {}), change_reason } });
|
|
647
|
+
},
|
|
648
|
+
/** CUSTOM-HATS: delete a custom hat -> { deleted: { key, revision, bindings_cleared, user_prefs_cleared } }. */
|
|
649
|
+
async deleteHat(hatId) {
|
|
650
|
+
const ep = resolveEndpoint('hat.delete', { orgId: orgId(), hatId });
|
|
651
|
+
return client.delete(ep.path, { service: ep.service, retries: 0 });
|
|
652
|
+
},
|
|
653
|
+
/** quox#1183: the hat this conversation is on (or would start on) -> { key, name } | null. */
|
|
654
|
+
async activeHat(conversationId) {
|
|
655
|
+
const ep = resolveEndpoint('hat.resolve', { orgId: orgId() });
|
|
656
|
+
const d = await client.get(ep.path, { service: ep.service, ...(conversationId ? { query: { conversation_id: conversationId } } : {}) });
|
|
657
|
+
return d?.hat ? { key: d.hat.key, name: d.hat.name, source: d.source || null } : null;
|
|
658
|
+
},
|
|
659
|
+
/** quox#1183: the person's own hat switch for this conversation (audited on the server). Throws the refusal unchanged. */
|
|
660
|
+
async bindHat(conversationId, hatId) {
|
|
661
|
+
const ep = resolveEndpoint('hat.bind', { orgId: orgId(), conversationId });
|
|
662
|
+
const d = await client.put(ep.path, { service: ep.service, retries: 0, body: { hat_id: hatId } });
|
|
663
|
+
return d?.binding || d;
|
|
664
|
+
},
|
|
665
|
+
/** QUOXON-FW3: the orgs the signed-in user belongs to -> [{ id, name, slug, role }]. Same endpoint as `quox org list`. */
|
|
666
|
+
async orgs() {
|
|
667
|
+
const ep = resolveEndpoint('orgs.list');
|
|
668
|
+
const d = await client.get(ep.path, { service: ep.service });
|
|
669
|
+
const rows = Array.isArray(d) ? d : (d?.organizations || d?.orgs || d?.data || []);
|
|
670
|
+
return Array.isArray(rows) ? rows : [];
|
|
671
|
+
},
|
|
672
|
+
/**
|
|
673
|
+
* QUOXON-FW3, #1034: re-scope THIS WINDOW to another org. The org-scoped token is held in this window's memory
|
|
674
|
+
* (windowOrg); the shared profile, its file and every other window keep their org. `quox org switch` in a shell is
|
|
675
|
+
* the explicit way to change a profile's default. -> { id, name }. Throws the server's refusal unchanged.
|
|
676
|
+
*/
|
|
677
|
+
async switchOrg(targetId) {
|
|
678
|
+
if (!windowOrg) { const e = new Error('Org switching is not available in this mode.'); e.code = 'no_window_org'; throw e; }
|
|
679
|
+
return windowOrg.switchTo(targetId);
|
|
680
|
+
},
|
|
681
|
+
/** The window's own org id when it differs from the profile's (carried across /restart), else null. */
|
|
682
|
+
windowOrgId: () => windowOrg?.current()?.id || null,
|
|
683
|
+
/** -> [{ id, name, description, edition, version, decayState }] ; the org's QSKL procedural skills (read-only list; Crystals are a separate store). */
|
|
684
|
+
async skills() {
|
|
685
|
+
const ep = resolveEndpoint('quoxflow.skills.list');
|
|
686
|
+
const data = await client.get(ep.path, { service: ep.service, query: { orgId: orgId() } });
|
|
687
|
+
const rows = data?.data || data?.records || data;
|
|
688
|
+
return Array.isArray(rows) ? rows : [];
|
|
689
|
+
},
|
|
690
|
+
/** -> the full skill record, or null when unknown (404). */
|
|
691
|
+
async skill(id) {
|
|
692
|
+
const ep = resolveEndpoint('quoxflow.skills.get', { id });
|
|
693
|
+
try { const d = await client.get(ep.path, { service: ep.service, query: { orgId: orgId() } }); return d?.data || d; }
|
|
694
|
+
catch (e) { if (e?.status === 404) return null; throw e; }
|
|
695
|
+
},
|
|
696
|
+
/**
|
|
697
|
+
* Roster of what is running for this org -> { rows, sources:[{ id, label, state:'connected'|'unavailable'|'error', count, note }] }.
|
|
698
|
+
* Each source fails on its own; a source that cannot answer adds no rows (never a placeholder).
|
|
699
|
+
*/
|
|
700
|
+
async roster(now = Date.now()) {
|
|
701
|
+
const src = (id, label, fn) => fn().then(
|
|
702
|
+
(rows) => ({ id, label, state: 'connected', rows, count: rows.length }),
|
|
703
|
+
(e) => ({ id, label, rows: [], count: 0, state: missing(e) ? 'unavailable' : 'error', note: missing(e) ? 'no list endpoint on this server' : (e?.message || 'failed') }));
|
|
704
|
+
const parts = await Promise.all([
|
|
705
|
+
src('approvals', 'pending approvals (inbox)', async () => (await listApprovals()).map(i => fromApproval(i, now)).filter(Boolean)),
|
|
706
|
+
src('objectives', 'objectives and loops (tasks)', async () => {
|
|
707
|
+
const ep = resolveEndpoint('objectives.list');
|
|
708
|
+
const d = await client.get(ep.path, { service: ep.service, query: { limit: 100 }, retries: 0 });
|
|
709
|
+
const l = listOf(d, 'objectives'); if (!l) throw new Error('unexpected response');
|
|
710
|
+
return l.map(o => fromObjective(o, now)).filter(Boolean);
|
|
711
|
+
}),
|
|
712
|
+
src('executions', 'workflow executions (quoxflow)', async () => {
|
|
713
|
+
const ep = resolveEndpoint('executions.list');
|
|
714
|
+
const d = await client.get(ep.path, { service: ep.service, retries: 0 });
|
|
715
|
+
const l = listOf(d, 'data', 'items', 'executions'); if (!l) throw new Error('unexpected response');
|
|
716
|
+
return l.map(e => fromExecution(e, now, { userId: userId() })).filter(Boolean);
|
|
717
|
+
})
|
|
718
|
+
]);
|
|
719
|
+
const unlisted = [
|
|
720
|
+
{ id: 'agents', label: 'attached agents', state: 'unavailable', count: 0, note: 'no org-wide list endpoint' },
|
|
721
|
+
{ id: 'devsessions', label: 'dev sessions', state: 'unavailable', count: 0, note: 'listed per host only (quox dev sessions <host>)' }
|
|
722
|
+
];
|
|
723
|
+
return { rows: sortRows(parts.flatMap(p => p.rows)), sources: [...parts.map(({ rows, ...rest }) => rest), ...unlisted] };
|
|
724
|
+
},
|
|
725
|
+
/**
|
|
726
|
+
* Stop one roster row through the governed route that exists for it.
|
|
727
|
+
* -> { ok:true, message } | { ok:false, unavailable:true } | { ok:false, message }.
|
|
728
|
+
*/
|
|
729
|
+
async stopRow(row) {
|
|
730
|
+
if (row.source === 'objectives' && row.kind === 'loop') {
|
|
731
|
+
try {
|
|
732
|
+
const ep = resolveEndpoint('objectives.halt', { id: row.id });
|
|
733
|
+
const r = await client.post(ep.path, { service: ep.service, body: { notes: 'via quoxon' }, retries: 0 });
|
|
734
|
+
return r?.status === 'halted' ? { ok: true, message: `Loop halted: ${row.name}` } : { ok: false, message: `Not halted${r?.reason ? ` (${r.reason})` : ''}` };
|
|
735
|
+
} catch (e) { return { ok: false, message: `halt refused: ${e?.message || 'failed'}` }; }
|
|
736
|
+
}
|
|
737
|
+
if (row.source === 'executions') {
|
|
738
|
+
try {
|
|
739
|
+
const ep = resolveEndpoint('runs.cancel', { orgId: orgId(), runId: row.id });
|
|
740
|
+
await client.post(ep.path, { service: ep.service, body: { reason: 'via quoxon' }, retries: 0 });
|
|
741
|
+
return { ok: true, message: `Cancel sent for ${row.name}` };
|
|
742
|
+
} catch (e) { return { ok: false, message: `cancel refused: ${e?.message || 'failed'}` }; }
|
|
743
|
+
}
|
|
744
|
+
return { ok: false, unavailable: true };
|
|
745
|
+
},
|
|
746
|
+
/** /sitrep: the LUCID situation (cursor not moved) and the approvals inbox, each part on its own. */
|
|
747
|
+
async sitrep() {
|
|
748
|
+
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') }; } };
|
|
749
|
+
const [situation, approvals] = await Promise.all([
|
|
750
|
+
part(async () => { const ep = resolveEndpoint('lucid.situation'); return client.get(ep.path, { service: ep.service, query: { since: 'last', surface: 'cli' }, retries: 0, timeout: 12000 }); }),
|
|
751
|
+
part(() => listApprovals())
|
|
752
|
+
]);
|
|
753
|
+
return { situation, approvals };
|
|
754
|
+
},
|
|
755
|
+
/** PRESENCE: PUT /api/v1/presence (/afk, /sleep, /back, a typed turn). -> { ok:true, presence, notice } | { ok:false, message, status }. */
|
|
756
|
+
async setPresence(state, { note, slots } = {}) {
|
|
757
|
+
const ep = resolveEndpoint('presence.set');
|
|
758
|
+
try {
|
|
759
|
+
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 } : {}) } });
|
|
760
|
+
return { ok: true, presence: d?.presence || null, notice: d?.notice || null };
|
|
761
|
+
} catch (e) { return { ok: false, message: e?.message || 'failed', status: e?.status || null }; }
|
|
762
|
+
},
|
|
763
|
+
noteActivity,
|
|
764
|
+
/** E5 /away: GET /api/v1/away?since=<iso> -> { since, until, items, activity, blind }. Throws on non-2xx. */
|
|
765
|
+
async away(since) {
|
|
766
|
+
const ep = resolveEndpoint('away.digest');
|
|
767
|
+
return client.get(ep.path, { service: ep.service, query: { since }, retries: 0, timeout: 15000 });
|
|
768
|
+
},
|
|
769
|
+
/** Facts for /observers, each part on its own: { lucid, learning:[{ id, label, state, text }] }. */
|
|
770
|
+
async observers() {
|
|
771
|
+
const part = async (fn) => { try { return { ok: true, v: await fn() }; } catch (e) { return { ok: false, unavailable: missing(e), error: e?.message || 'failed' }; } };
|
|
772
|
+
const stat = (id, label, name) => part(async () => {
|
|
773
|
+
const ep = resolveEndpoint(name); const d = await client.get(ep.path, { service: ep.service, retries: 0 });
|
|
774
|
+
return { id, label, text: d?.primaryMetric?.label || null, fields: d?.fields || {} };
|
|
775
|
+
});
|
|
776
|
+
const [sit, lessons, reflect, learn, insight] = await Promise.all([
|
|
777
|
+
part(async () => { const ep = resolveEndpoint('lucid.situation'); return client.get(ep.path, { service: ep.service, query: { since: 'last', surface: 'cli' }, retries: 0 }); }),
|
|
778
|
+
part(async () => { const ep = resolveEndpoint('lucid.lessons'); return client.get(ep.path, { service: ep.service, retries: 0 }); }),
|
|
779
|
+
stat('reflect', 'QuoxReflect', 'engine.reflect.stats'),
|
|
780
|
+
stat('learn', 'QuoxLearn', 'engine.learn.stats'),
|
|
781
|
+
stat('insight', 'QuoxInsight', 'engine.insight.stats')
|
|
782
|
+
]);
|
|
783
|
+
return { situation: sit, lessons, learning: [reflect, learn, insight] };
|
|
784
|
+
},
|
|
785
|
+
/** /mind [id]: QuoxMind implants from the collector. -> { implants, health } | { implant }. Throws on non-2xx. */
|
|
786
|
+
async mind(id) {
|
|
787
|
+
if (id) { const ep = resolveEndpoint('mind.implant', { id }); return { implant: await client.get(ep.path, { service: ep.service, retries: 0, timeout: 15000 }) }; }
|
|
788
|
+
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 })));
|
|
789
|
+
return { implants: l.implants || [], health: h };
|
|
790
|
+
},
|
|
791
|
+
/** /mirror: the Mirror as CommanderQ sees it (compact System Status block + age). Throws on non-2xx. */
|
|
792
|
+
async mirror() {
|
|
793
|
+
const ep = resolveEndpoint('mind.mirror');
|
|
794
|
+
return client.get(ep.path, { service: ep.service, retries: 0, timeout: 10000 });
|
|
795
|
+
},
|
|
796
|
+
/** #904 part 3: providers refusing turns right now (weekly limit, no credits). -> [{ provider, state, line }] | null on error. */
|
|
797
|
+
async providerHealth() {
|
|
798
|
+
try {
|
|
799
|
+
const ep = resolveEndpoint('chat.status.list');
|
|
800
|
+
const d = await client.get(ep.path, { service: ep.service, retries: 0, timeout: 8000 });
|
|
801
|
+
return Array.isArray(d?.provider_health) ? d.provider_health : [];
|
|
802
|
+
} catch { return null; }
|
|
803
|
+
},
|
|
804
|
+
async health(timeoutMs = 12000) {
|
|
805
|
+
const url = joinServiceUrl(config.getServiceUrl('collector'), '/health');
|
|
806
|
+
const ctrl = new AbortController();
|
|
807
|
+
const t = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
808
|
+
try { const r = await fetch(url, { signal: ctrl.signal }); return { ok: r.ok, status: r.status, url: url.origin }; }
|
|
809
|
+
catch (e) {
|
|
810
|
+
const code = e.cause?.code || '';
|
|
811
|
+
return { ok: false, absent: /ECONNREFUSED|ENOTFOUND|EAI_AGAIN/.test(code), status: e.name === 'AbortError' ? 'slow to answer' : (code || e.message), url: url.origin };
|
|
812
|
+
} finally { clearTimeout(t); }
|
|
813
|
+
},
|
|
814
|
+
/** /capabilities (E1). -> { ok:true, capabilities } | { ok:false, status, missing?, error }. Never assumes. */
|
|
815
|
+
async capabilities() {
|
|
816
|
+
const ep = resolveEndpoint('v1.capabilities');
|
|
817
|
+
try {
|
|
818
|
+
const d = await client.get(ep.path, { service: ep.service, retries: 0 });
|
|
819
|
+
const caps = listOf(d, 'capabilities');
|
|
820
|
+
return caps ? { ok: true, capabilities: caps, computedAt: d?.computed_at || null } : { ok: false, error: 'unexpected response' };
|
|
821
|
+
} catch (e) {
|
|
822
|
+
return { ok: false, status: e?.status, missing: missing(e), error: e?.status ? `HTTP ${e.status} ${e.message || ''}`.trim() : (e?.message || 'failed') };
|
|
823
|
+
}
|
|
824
|
+
},
|
|
825
|
+
/** /connect status: the org vault list (the only source a user can read). -> { ok:true, rows } | { ok:false, error }. */
|
|
826
|
+
async connections() {
|
|
827
|
+
try {
|
|
828
|
+
const ep = resolveEndpoint('vault.list', { orgId: orgId() });
|
|
829
|
+
const d = await client.get(ep.path, { service: ep.service, retries: 0 });
|
|
830
|
+
const rows = listOf(d, 'credentials', 'data', 'items');
|
|
831
|
+
return rows ? { ok: true, rows } : { ok: false, error: 'unexpected response' };
|
|
832
|
+
} catch (e) { return { ok: false, error: e?.status ? `HTTP ${e.status} ${e.message || ''}`.trim() : (e?.message || 'failed') }; }
|
|
833
|
+
},
|
|
834
|
+
/**
|
|
835
|
+
* Server-side live test of a key, nothing persisted (auth test-credentials, SECRETS_WRITE).
|
|
836
|
+
* Sent once (no retries) and flagged sensitive so --debug never prints the body.
|
|
837
|
+
*/
|
|
838
|
+
async testProviderKey({ type, credentials }) {
|
|
839
|
+
const ep = resolveEndpoint('vault.test-credentials', { orgId: orgId() });
|
|
840
|
+
try {
|
|
841
|
+
const r = await client.post(ep.path, { service: ep.service, body: { integration_type: type, credentials }, retries: 0, sensitive: true });
|
|
842
|
+
return { ok: r?.success === true, message: r?.message || null };
|
|
843
|
+
} catch (e) { return { ok: false, status: e?.status, message: e?.message || 'failed' }; }
|
|
844
|
+
},
|
|
845
|
+
/** The governed vault write (same route as `quox vault create`): audited, AEE envelope carries type + label only. */
|
|
846
|
+
async saveProviderKey({ type, label, credentials, isDefault, confirmPrivateOrigin }) {
|
|
847
|
+
const ep = resolveEndpoint('vault.create', { orgId: orgId() });
|
|
848
|
+
try {
|
|
849
|
+
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 });
|
|
850
|
+
return { ok: Boolean(r?.id), id: r?.id || null, message: r?.id ? null : 'unexpected response' };
|
|
851
|
+
} catch (e) { return vaultFailure(e); }
|
|
852
|
+
},
|
|
853
|
+
/** B3: refresh an existing vault credential (same governed route as `quox vault update`). */
|
|
854
|
+
async updateProviderCredential({ id, credentials, isDefault, confirmPrivateOrigin }) {
|
|
855
|
+
const ep = resolveEndpoint('vault.update', { orgId: orgId(), id });
|
|
856
|
+
try {
|
|
857
|
+
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 });
|
|
858
|
+
return { ok: Boolean(r && (r.id || r.success || r.credential)), id, message: null };
|
|
859
|
+
} catch (e) { return vaultFailure(e); }
|
|
860
|
+
},
|
|
861
|
+
/** Z2H: read the instance LAN-target gate -> { ok, enabled } | { ok:false, status, message }. */
|
|
862
|
+
async lanTargets() {
|
|
863
|
+
const ep = resolveEndpoint('admin.instance.lan_targets.get');
|
|
864
|
+
try { const r = await client.get(ep.path, { service: ep.service, retries: 0 }); return { ok: true, enabled: Boolean(r?.enabled) }; }
|
|
865
|
+
catch (e) { return { ok: false, status: e?.status, message: e?.message || 'failed' }; }
|
|
866
|
+
},
|
|
867
|
+
/** Z2H: set the instance LAN-target gate (instance admin only; audited server side). */
|
|
868
|
+
async setLanTargets(on) {
|
|
869
|
+
const ep = resolveEndpoint('admin.instance.lan_targets.set');
|
|
870
|
+
try { const r = await client.put(ep.path, { service: ep.service, body: { enabled: Boolean(on) }, retries: 0 }); return { ok: true, enabled: Boolean(r?.enabled) }; }
|
|
871
|
+
catch (e) { return { ok: false, status: e?.status, message: e?.message || 'failed' }; }
|
|
872
|
+
},
|
|
873
|
+
/** B3: server-side probe of an Ollama URL (GET <url>/api/tags). Nothing stored. */
|
|
874
|
+
async ollamaProbe(baseUrl) {
|
|
875
|
+
const ep = resolveEndpoint('chat.providers.ollama.probe');
|
|
876
|
+
try { return await client.post(ep.path, { service: ep.service, body: { base_url: baseUrl }, retries: 0 }); }
|
|
877
|
+
catch (e) { return { ok: false, status: e?.status, error: e?.code || null, message: e?.message || 'failed' }; }
|
|
878
|
+
},
|
|
879
|
+
/** B3: the saved Ollama connection's installed models -> { ok, host, default_model, models } | { ok:false, error, message }. */
|
|
880
|
+
async ollamaModels() {
|
|
881
|
+
const ep = resolveEndpoint('chat.providers.ollama.models');
|
|
882
|
+
try { return await client.get(ep.path, { service: ep.service, retries: 0 }); }
|
|
883
|
+
catch (e) { return { ok: false, status: e?.status, error: e?.code || null, message: e?.message || 'failed' }; }
|
|
884
|
+
},
|
|
885
|
+
/** QUOXON-MODEL: live model list for openai | anthropic -> { ok, models:[{id,family,created}], default_model } | { ok:false, error, message }. */
|
|
886
|
+
async providerModels(provider, { refresh = false } = {}) {
|
|
887
|
+
const ep = resolveEndpoint('chat.providers.models', { provider });
|
|
888
|
+
try { return await client.get(ep.path, { service: ep.service, query: refresh ? { refresh: 1 } : undefined, retries: 0 }); }
|
|
889
|
+
catch (e) { return { ok: false, status: e?.status, error: e?.code || null, message: e?.message || 'failed' }; }
|
|
890
|
+
},
|
|
891
|
+
/** B5: server-side probe of a self-hosted / custom OpenAI-compatible base URL (lmstudio|vllm|custom). Nothing stored. */
|
|
892
|
+
async compatProbe(preset, baseUrl) {
|
|
893
|
+
const ep = resolveEndpoint('chat.providers.compat.probe', { preset });
|
|
894
|
+
try { return await client.post(ep.path, { service: ep.service, body: { base_url: baseUrl }, retries: 0 }); }
|
|
895
|
+
catch (e) { return { ok: false, status: e?.status, error: e?.code || null, message: e?.message || 'failed' }; }
|
|
896
|
+
},
|
|
897
|
+
/** B5: the saved connection's models for a preset -> { ok, host, default_model, models:[{id,context_length,tools}] } | { ok:false, error, message }. */
|
|
898
|
+
async compatModels(preset) {
|
|
899
|
+
const ep = resolveEndpoint('chat.providers.compat.models', { preset });
|
|
900
|
+
try { return await client.get(ep.path, { service: ep.service, retries: 0 }); }
|
|
901
|
+
catch (e) { return { ok: false, status: e?.status, error: e?.code || null, message: e?.message || 'failed' }; }
|
|
902
|
+
},
|
|
903
|
+
/** /vm <verb> ...: the HYPERION Proxmox tools through POST /api/v1/tools/execute. Returns { lines, tone }. */
|
|
904
|
+
async vm(argv) { return runVmCommand({ client, orgId: orgId(), argv }); },
|
|
905
|
+
/** /hyperion: held VMs and who holds them, one line each (read only, GET /api/v1/proxmox/activity). */
|
|
906
|
+
async hyperion() { const data = await fetchActivity(client); return { lines: heldLines(data), tone: data.nodes_error || data.pool_error ? 'warn' : undefined }; },
|
|
907
|
+
approvals: listApprovals,
|
|
908
|
+
/** Org-wide approvals awaiting a human (the dashboard header's server-side count, inbox stats awaiting_human). */
|
|
909
|
+
async approvalStats() {
|
|
910
|
+
const ep = resolveEndpoint('inbox.stats.list', { orgId: orgId() });
|
|
911
|
+
const d = await client.get(ep.path, { service: ep.service });
|
|
912
|
+
return { pending: Number(d?.pending_approvals ?? 0), awaiting: Number(d?.awaiting_human ?? 0) };
|
|
913
|
+
},
|
|
914
|
+
/** GET /api/v1/approvals/:id/result -> { status, result, executed_at } ; null when unknown (404) */
|
|
915
|
+
async approvalResult(approvalId) {
|
|
916
|
+
const ep = resolveEndpoint('v1.approvals.result', { id: approvalId });
|
|
917
|
+
try { return await client.get(ep.path, { service: ep.service, query: { org_id: orgId() || undefined } }); }
|
|
918
|
+
catch (e) { if (e?.status === 404) return null; throw e; }
|
|
919
|
+
},
|
|
920
|
+
/** quox ask: answer a decision with one of its option keys (and an optional note). */
|
|
921
|
+
async answerDecision(itemId, option, { note = null, stepUp = null, bulk = false } = {}) {
|
|
922
|
+
const ep = resolveEndpoint('inbox.respond', { orgId: orgId(), itemId });
|
|
923
|
+
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) } : {}) } });
|
|
924
|
+
if (stepUp && Object.keys(stepUp).length) noteActivity('answer', { quiet: true }); // PRESENCE: answering is the owner being back
|
|
925
|
+
return r;
|
|
926
|
+
},
|
|
927
|
+
async respond(itemId, action, { via = null, stepUp = null, reason = null, bulk = false } = {}) {
|
|
928
|
+
const ep = resolveEndpoint('inbox.respond', { orgId: orgId(), itemId });
|
|
929
|
+
const tag = via ? `via quoxon ${via}` : 'via quoxon';
|
|
930
|
+
const why = reason ? String(reason).trim().slice(0, 4000) : '';
|
|
931
|
+
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 } : {}) } });
|
|
932
|
+
if (stepUp && Object.keys(stepUp).length) noteActivity('approval', { quiet: true }); // PRESENCE
|
|
933
|
+
return r;
|
|
934
|
+
},
|
|
935
|
+
/** QUOXON-STEPUP: PIN -> short-lived step-up token. The PIN goes straight into the request body; never logged, never stored. */
|
|
936
|
+
async stepUpVerify(pin, { orgId: org = null, scopes = ['approve'] } = {}) {
|
|
937
|
+
const ep = resolveEndpoint('stepup.verify');
|
|
938
|
+
const r = await client.post(ep.path, { service: ep.service, body: { pin, org_id: org || orgId() || undefined, scopes } });
|
|
939
|
+
noteActivity('pin-step-up', { quiet: true }); // PRESENCE: a PIN step-up is a live human
|
|
940
|
+
return r;
|
|
941
|
+
},
|
|
942
|
+
async stepUpStatus() {
|
|
943
|
+
const ep = resolveEndpoint('stepup.status');
|
|
944
|
+
return client.get(ep.path, { service: ep.service });
|
|
945
|
+
},
|
|
946
|
+
/** QUOXON-STEPUP part B: override ONE blocked action. Needs a live override-scoped step-up token. */
|
|
947
|
+
async override({ blockId, blockType, reason, agentId = null, action = null, stepUp = {} }) {
|
|
948
|
+
const ep = resolveEndpoint('safety.override');
|
|
949
|
+
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 } });
|
|
950
|
+
}
|
|
951
|
+
};
|
|
952
|
+
}
|