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,669 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config management for QuoxCLI
|
|
3
|
+
*
|
|
4
|
+
* Stores profiles and auth tokens in ~/.config/quox/config.json
|
|
5
|
+
* File permissions: 0600 (owner read/write only)
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync, renameSync, openSync, closeSync, statSync, unlinkSync, copyFileSync } from 'node:fs';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
import { homedir, networkInterfaces, hostname as osHostname } from 'node:os';
|
|
11
|
+
|
|
12
|
+
function getConfigDir() { return join(homedir(), '.config', 'quox'); }
|
|
13
|
+
function getConfigFile() { return join(getConfigDir(), 'config.json'); }
|
|
14
|
+
|
|
15
|
+
// quox#713: cross-process lock around read-merge-write of config.json.
|
|
16
|
+
const LOCK_STALE_MS = 10000;
|
|
17
|
+
const LOCK_WAIT_MS = 15000;
|
|
18
|
+
function sleepSync(ms) { Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms); }
|
|
19
|
+
function debugLog(msg) { if (process.env.QUOX_DEBUG) process.stderr.write(`[quox config] ${msg}\n`); }
|
|
20
|
+
function acquireLock(lockFile) {
|
|
21
|
+
const deadline = Date.now() + LOCK_WAIT_MS;
|
|
22
|
+
for (;;) {
|
|
23
|
+
try {
|
|
24
|
+
const fd = openSync(lockFile, 'wx', 0o600);
|
|
25
|
+
closeSync(fd);
|
|
26
|
+
return true;
|
|
27
|
+
} catch (e) {
|
|
28
|
+
if (e.code !== 'EEXIST') return false;
|
|
29
|
+
try {
|
|
30
|
+
if (Date.now() - statSync(lockFile).mtimeMs > LOCK_STALE_MS) {
|
|
31
|
+
debugLog('breaking stale config lock');
|
|
32
|
+
try { unlinkSync(lockFile); } catch { /* another breaker won */ }
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
} catch { continue; /* lock vanished, retry */ }
|
|
36
|
+
if (Date.now() > deadline) return false;
|
|
37
|
+
sleepSync(15);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function releaseLock(lockFile) { try { unlinkSync(lockFile); } catch { /* already gone */ } }
|
|
42
|
+
const J = (v) => JSON.stringify(v === undefined ? null : v);
|
|
43
|
+
|
|
44
|
+
const DEFAULT_CONFIG = {
|
|
45
|
+
version: 1,
|
|
46
|
+
activeProfile: 'local',
|
|
47
|
+
profiles: {
|
|
48
|
+
local: {
|
|
49
|
+
url: 'http://localhost:3000',
|
|
50
|
+
auth: null,
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// Service port map matching docker-compose.yml
|
|
56
|
+
export const SERVICE_PORTS = {
|
|
57
|
+
auth: 3101,
|
|
58
|
+
files: 3102,
|
|
59
|
+
memory: 3103,
|
|
60
|
+
orchestrator: 3100,
|
|
61
|
+
tasks: 3104,
|
|
62
|
+
collector: 9848,
|
|
63
|
+
// WP2.2: quoxflow exposes 127.0.0.1:${QUOXFLOW_PORT:-3200}:3200 in
|
|
64
|
+
// docker-compose.yml. This entry was previously absent, which silently
|
|
65
|
+
// broke getServiceUrl('quoxflow') — step 3 below only rewrites the port
|
|
66
|
+
// when a SERVICE_PORTS entry exists, so without it the call fell through
|
|
67
|
+
// to the unmodified base URL (wrong port). doctor.js has referenced
|
|
68
|
+
// `service: 'quoxflow', port: 3200` since CLI-3b without this entry ever
|
|
69
|
+
// backing it — that health check was likely silently probing the wrong
|
|
70
|
+
// port. See cli/lib/api/endpoints/triggers.js header for the full
|
|
71
|
+
// quoxflow-access decision this fixes.
|
|
72
|
+
quoxflow: 3200,
|
|
73
|
+
quoxbrain: 3106,
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// Hosts where a SERVICE_PORTS rewrite necessarily lands on a plain node HTTP
|
|
77
|
+
// server rather than on nginx. URL.hostname keeps the brackets for IPv6, so
|
|
78
|
+
// '[::1]' is the form to match. See getServiceUrl step 3 for the rule.
|
|
79
|
+
const LOOPBACK_HOSTS = new Set(['localhost', '127.0.0.1', '::1', '[::1]']);
|
|
80
|
+
|
|
81
|
+
// #572/#623: RFC1918 private-LAN hosts (+ *.local). On a single-box Quox stack
|
|
82
|
+
// the compose services bind 127.0.0.1 and ONLY the edge (:3000) terminates TLS
|
|
83
|
+
// and answers on the LAN address — so a service-port derived from a private-LAN
|
|
84
|
+
// base (e.g. https://quox.example.com:3000) must point at the reachable loopback
|
|
85
|
+
// bind, not https://<lan-ip>:PORT (which returns 000/refused, made `quox doctor`
|
|
86
|
+
// cry CRITICAL and broke run/objective/memory/volt). PUBLIC/remote hosts are
|
|
87
|
+
// deliberately NOT matched: they may run a real per-port TLS-terminating
|
|
88
|
+
// deployment, and an explicit per-service override always wins regardless.
|
|
89
|
+
function isPrivateHost(hostname) {
|
|
90
|
+
if (/\.local$/i.test(hostname)) return true;
|
|
91
|
+
const m = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(hostname);
|
|
92
|
+
if (!m) return false;
|
|
93
|
+
const a = Number(m[1]); const b = Number(m[2]);
|
|
94
|
+
return a === 10 || (a === 172 && b >= 16 && b <= 31) || (a === 192 && b === 168);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// QUOXON-Z2H lane R (2026-10-14): where the edge (nginx on the profile's base
|
|
98
|
+
// URL) serves each compose service. Mirrors nginx.conf: /api/auth, /api/setup,
|
|
99
|
+
// /api/orgs ... go to auth at the SAME path, so auth's prefix is empty.
|
|
100
|
+
export const EDGE_PATHS = {
|
|
101
|
+
auth: '',
|
|
102
|
+
collector: '/collector',
|
|
103
|
+
memory: '/api/memory',
|
|
104
|
+
tasks: '/tasks',
|
|
105
|
+
orchestrator: '/orchestrator',
|
|
106
|
+
quoxflow: '/quoxflow',
|
|
107
|
+
quoxbrain: '/api/quoxbrain',
|
|
108
|
+
files: '/files-api',
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
// The local edge's default port. A loopback base on this port is the stack on
|
|
112
|
+
// THIS box, whose services are reachable on their plain-http loopback binds.
|
|
113
|
+
// Any other loopback port is a tunnel (ssh -L 13105:vm:3000) to another box.
|
|
114
|
+
const LOCAL_EDGE_PORTS = new Set(['3000']);
|
|
115
|
+
|
|
116
|
+
function localIdentity(opts = {}) {
|
|
117
|
+
let localAddrs = opts.localAddrs;
|
|
118
|
+
if (!localAddrs) {
|
|
119
|
+
localAddrs = [];
|
|
120
|
+
try {
|
|
121
|
+
for (const list of Object.values(networkInterfaces())) {
|
|
122
|
+
for (const a of list || []) localAddrs.push(a.address);
|
|
123
|
+
}
|
|
124
|
+
} catch { /* no interfaces readable: treat nothing as local */ }
|
|
125
|
+
}
|
|
126
|
+
let host = opts.hostname;
|
|
127
|
+
if (host === undefined) { try { host = osHostname(); } catch { host = ''; } }
|
|
128
|
+
return { localAddrs: new Set(localAddrs), hostname: String(host || '').toLowerCase() };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function isThisMachine(hostname, id) {
|
|
132
|
+
const h = String(hostname || '').toLowerCase().replace(/^\[|\]$/g, '');
|
|
133
|
+
if (LOOPBACK_HOSTS.has(h) || LOOPBACK_HOSTS.has(`[${h}]`)) return true;
|
|
134
|
+
if (id.localAddrs.has(h)) return true;
|
|
135
|
+
return !!id.hostname && (h === id.hostname || h === `${id.hostname}.local`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* True when a service URL lands on THIS machine (loopback bind, or a base whose
|
|
140
|
+
* host is one of this box's own addresses) rather than another host or a tunnel
|
|
141
|
+
* to one. Used to keep this box's ambient QUOX_SERVICE_KEY from being offered
|
|
142
|
+
* to a remote instance.
|
|
143
|
+
*/
|
|
144
|
+
export function isLocalServiceTarget(serviceUrl, opts = {}) {
|
|
145
|
+
try {
|
|
146
|
+
const u = new URL(serviceUrl);
|
|
147
|
+
const id = localIdentity(opts);
|
|
148
|
+
if (LOOPBACK_HOSTS.has(u.hostname)) {
|
|
149
|
+
// Loopback is this box when it is the local edge or a compose service
|
|
150
|
+
// bind; any other loopback port is a tunnel to another box.
|
|
151
|
+
const port = u.port || (u.protocol === 'https:' ? '443' : '80');
|
|
152
|
+
return LOCAL_EDGE_PORTS.has(port) || Object.values(SERVICE_PORTS).map(String).includes(port);
|
|
153
|
+
}
|
|
154
|
+
return isThisMachine(u.hostname, id);
|
|
155
|
+
} catch {
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Join an absolute or relative path onto a service URL WITHOUT dropping its path prefix (new URL('/x', base) would). */
|
|
161
|
+
export function joinServiceUrl(serviceUrl, path) {
|
|
162
|
+
const base = new URL(serviceUrl);
|
|
163
|
+
const prefix = base.pathname.replace(/\/+$/, '');
|
|
164
|
+
const rel = new URL(String(path).replace(/^\/*/, '/'), 'http://x');
|
|
165
|
+
base.pathname = `${prefix}${rel.pathname}`;
|
|
166
|
+
base.search = rel.search;
|
|
167
|
+
base.hash = '';
|
|
168
|
+
return base;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Derivation of a compose service's URL from a profile base URL (normally the
|
|
172
|
+
// ACTIVE profile's own `url`, never a global default).
|
|
173
|
+
//
|
|
174
|
+
// QUOXON-Z2H lane R: the base URL is the single source of truth. Port guessing
|
|
175
|
+
// is ONLY for the stack on this very machine:
|
|
176
|
+
// - loopback base on the local edge port (:3000): plain-http loopback port
|
|
177
|
+
// swap (BRAIN2-AUDIT: compose services are plain HTTP);
|
|
178
|
+
// - private-LAN base whose host is one of THIS box's addresses: the
|
|
179
|
+
// loopback bind (#572/#623: services bind 127.0.0.1, only :3000 is on LAN).
|
|
180
|
+
// Everything else (another LAN host, a tunnel on a loopback port, a public
|
|
181
|
+
// host) goes through that base's edge at the service's nginx path. The old
|
|
182
|
+
// rule rewrote ANY RFC1918 base to 127.0.0.1:<port>, so `quox -p vm9110 login`
|
|
183
|
+
// against https://192.0.2.68:3000 was sent to this box's own auth service
|
|
184
|
+
// and got 401 invalid_credentials, and a tunnel profile hit local ports.
|
|
185
|
+
//
|
|
186
|
+
// DEVSUITE-GRIPE-P7 / Bug A: computed from the profile url, not getBaseUrl(),
|
|
187
|
+
// so a stale QUOX_URL cannot defeat -p.
|
|
188
|
+
export function deriveServiceUrl(baseUrl, port, service, opts = {}) {
|
|
189
|
+
try {
|
|
190
|
+
const url = new URL(baseUrl);
|
|
191
|
+
const id = localIdentity(opts);
|
|
192
|
+
const basePort = url.port || (url.protocol === 'https:' ? '443' : '80');
|
|
193
|
+
if (LOOPBACK_HOSTS.has(url.hostname) && LOCAL_EDGE_PORTS.has(basePort)) {
|
|
194
|
+
url.port = String(port);
|
|
195
|
+
url.protocol = 'http:';
|
|
196
|
+
return url.origin;
|
|
197
|
+
}
|
|
198
|
+
if (!LOOPBACK_HOSTS.has(url.hostname) && isPrivateHost(url.hostname) && isThisMachine(url.hostname, id)) {
|
|
199
|
+
url.port = String(port);
|
|
200
|
+
url.protocol = 'http:';
|
|
201
|
+
url.hostname = '127.0.0.1';
|
|
202
|
+
return url.origin;
|
|
203
|
+
}
|
|
204
|
+
if (service && Object.prototype.hasOwnProperty.call(EDGE_PATHS, service)) {
|
|
205
|
+
return `${url.origin}${url.pathname.replace(/\/+$/, '')}${EDGE_PATHS[service]}`;
|
|
206
|
+
}
|
|
207
|
+
// A port-mapped service with no edge route: the old port swap is the only guess left.
|
|
208
|
+
url.port = String(port);
|
|
209
|
+
return url.origin;
|
|
210
|
+
} catch {
|
|
211
|
+
return baseUrl;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// -p/--profile must be resolved BEFORE commander parses argv: bin/quox.js
|
|
216
|
+
// builds the shared ctx (loadConfig) at module load, when program.opts() is
|
|
217
|
+
// still empty. Reading the flag there always yielded undefined, so -p
|
|
218
|
+
// silently fell back to activeProfile — a login aimed at a dedicated profile
|
|
219
|
+
// clobbered another profile's session on 2026-08-15. This pre-scan is the
|
|
220
|
+
// only correct source for the flag at ctx-build time.
|
|
221
|
+
// Commands whose remaining argv belongs to another program (`quox claude -p
|
|
222
|
+
// "prompt"` is claude's --print, not quox's --profile).
|
|
223
|
+
const WRAPPED_PROGRAM_COMMANDS = new Set(['claude', 'codex', 'gemini', 'wrap']);
|
|
224
|
+
|
|
225
|
+
export function resolveProfileFlagFromArgv(argv) {
|
|
226
|
+
for (let i = 0; i < argv.length; i++) {
|
|
227
|
+
const a = argv[i];
|
|
228
|
+
if (WRAPPED_PROGRAM_COMMANDS.has(a)) return undefined;
|
|
229
|
+
if (a === '-p' || a === '--profile') return argv[i + 1];
|
|
230
|
+
if (typeof a === 'string' && a.startsWith('--profile=')) return a.slice('--profile='.length);
|
|
231
|
+
}
|
|
232
|
+
return undefined;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// fleet #479: -p/--profile and QUOX_PROFILE are documented as a one-shot
|
|
236
|
+
// override, but loadConfig used to write the override straight onto
|
|
237
|
+
// config.activeProfile. Any later save() (e.g. a login) then persisted it,
|
|
238
|
+
// silently changing the user's default profile for every command after.
|
|
239
|
+
// An override is a lens, not a switch: it must never survive to disk unless
|
|
240
|
+
// the caller explicitly asks for that with setActiveProfile(). The fix
|
|
241
|
+
// keeps the persisted default in _data.activeProfile untouched and tracks
|
|
242
|
+
// the "profile this invocation is using" separately on the wrapper as
|
|
243
|
+
// _effectiveProfile. Every reader that means "current" reads that; only
|
|
244
|
+
// setActiveProfile() is allowed to move the persisted default.
|
|
245
|
+
// #736 (2026-10-09): a config file that cannot be parsed must never be
|
|
246
|
+
// replaced by defaults in silence. The disk filled, the file became
|
|
247
|
+
// unreadable for a moment, and the next save wrote a near-empty config over
|
|
248
|
+
// every profile. Recover from the last good copy, or keep the bad file aside.
|
|
249
|
+
function moveConfigAside(reason) {
|
|
250
|
+
try {
|
|
251
|
+
const aside = `${getConfigFile()}.corrupt-${new Date().toISOString().replace(/[:.]/g, '-')}`;
|
|
252
|
+
renameSync(getConfigFile(), aside);
|
|
253
|
+
process.stderr.write(`Warning: ${getConfigFile()} was unreadable (${reason}); kept it as ${aside}.\n`);
|
|
254
|
+
} catch { /* nothing to move */ }
|
|
255
|
+
}
|
|
256
|
+
function recoverConfig(reason) {
|
|
257
|
+
const bak = `${getConfigFile()}.bak`;
|
|
258
|
+
try {
|
|
259
|
+
const parsed = JSON.parse(readFileSync(bak, 'utf-8'));
|
|
260
|
+
if (parsed && typeof parsed === 'object' && parsed.profiles) {
|
|
261
|
+
moveConfigAside(reason);
|
|
262
|
+
process.stderr.write(`Recovered quox config from ${bak}.\n`);
|
|
263
|
+
return parsed;
|
|
264
|
+
}
|
|
265
|
+
} catch { /* no usable backup */ }
|
|
266
|
+
moveConfigAside(reason);
|
|
267
|
+
return JSON.parse(JSON.stringify(DEFAULT_CONFIG));
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export function loadConfig(profileOverride) {
|
|
271
|
+
let config;
|
|
272
|
+
|
|
273
|
+
if (existsSync(getConfigFile())) {
|
|
274
|
+
try {
|
|
275
|
+
config = JSON.parse(readFileSync(getConfigFile(), 'utf-8'));
|
|
276
|
+
if (!config || typeof config !== 'object') throw new Error('not an object');
|
|
277
|
+
} catch (err) {
|
|
278
|
+
config = recoverConfig(err.message);
|
|
279
|
+
}
|
|
280
|
+
} else {
|
|
281
|
+
config = JSON.parse(JSON.stringify(DEFAULT_CONFIG));
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Ensure structure
|
|
285
|
+
if (!config.profiles) config.profiles = {};
|
|
286
|
+
if (!config.activeProfile) config.activeProfile = 'local';
|
|
287
|
+
|
|
288
|
+
// Profile override from CLI flag or env. This is session-scoped state
|
|
289
|
+
// only: it must never be assigned onto config.activeProfile (the
|
|
290
|
+
// persisted default), see comment block above.
|
|
291
|
+
const effectiveProfile = profileOverride || process.env.QUOX_PROFILE || config.activeProfile;
|
|
292
|
+
// A NAMED override that doesn't exist yet is only legitimate for commands
|
|
293
|
+
// that create it (login, config profile add) — warn instead of silently
|
|
294
|
+
// proceeding on an empty profile, so a typo'd -p never masquerades as a
|
|
295
|
+
// logged-out state.
|
|
296
|
+
if (profileOverride && config.profiles && !config.profiles[profileOverride]) {
|
|
297
|
+
process.stderr.write(`Note: profile "${profileOverride}" does not exist yet; it will be created on login.\n`);
|
|
298
|
+
// Vivify in memory so login's setAuth() has a slot to write (persisted
|
|
299
|
+
// only when a mutation saves); read-only commands just see a logged-out
|
|
300
|
+
// profile, which is the accurate answer. A later save() (e.g. login)
|
|
301
|
+
// persists this profile entry, but never the activeProfile default.
|
|
302
|
+
config.profiles[profileOverride] = {
|
|
303
|
+
url: (config.profiles[config.activeProfile] || {}).url || DEFAULT_CONFIG.profiles.local.url,
|
|
304
|
+
auth: null,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return {
|
|
309
|
+
_data: config,
|
|
310
|
+
// The profile this invocation is using (persisted default, or the -p /
|
|
311
|
+
// QUOX_PROFILE override for this process only). Never written to _data.
|
|
312
|
+
_effectiveProfile: effectiveProfile,
|
|
313
|
+
// quox#805: a profile passed explicitly to loadConfig(profile) means "act as
|
|
314
|
+
// that profile's user", same as -p on the CLI (no implicit attach default).
|
|
315
|
+
_explicitProfile: Boolean(profileOverride),
|
|
316
|
+
|
|
317
|
+
getActiveProfile() {
|
|
318
|
+
const name = this._effectiveProfile;
|
|
319
|
+
return this._data.profiles[name] || null;
|
|
320
|
+
},
|
|
321
|
+
|
|
322
|
+
getActiveProfileName() {
|
|
323
|
+
return this._effectiveProfile;
|
|
324
|
+
},
|
|
325
|
+
|
|
326
|
+
// The ONLY deliberate persistent switch: moves the on-disk default AND
|
|
327
|
+
// the effective name, then saves. Everything else in this file is a
|
|
328
|
+
// session-scoped lens onto whichever name is current.
|
|
329
|
+
setActiveProfile(name) {
|
|
330
|
+
if (!this._data.profiles[name]) {
|
|
331
|
+
throw new Error(`Profile "${name}" does not exist.`);
|
|
332
|
+
}
|
|
333
|
+
this._data.activeProfile = name;
|
|
334
|
+
this._effectiveProfile = name;
|
|
335
|
+
this.save();
|
|
336
|
+
},
|
|
337
|
+
|
|
338
|
+
addProfile(name, opts = {}) {
|
|
339
|
+
if (this._data.profiles[name]) {
|
|
340
|
+
throw new Error(`Profile "${name}" already exists.`);
|
|
341
|
+
}
|
|
342
|
+
this._data.profiles[name] = {
|
|
343
|
+
url: opts.url || 'http://localhost:3000',
|
|
344
|
+
auth: null,
|
|
345
|
+
};
|
|
346
|
+
this.save();
|
|
347
|
+
},
|
|
348
|
+
|
|
349
|
+
removeProfile(name) {
|
|
350
|
+
if (name === this._effectiveProfile) {
|
|
351
|
+
throw new Error('Cannot remove the active profile. Switch first.');
|
|
352
|
+
}
|
|
353
|
+
delete this._data.profiles[name];
|
|
354
|
+
// quox#527: save() merges profiles it finds on disk but does not know
|
|
355
|
+
// about (so a parallel session's new profile survives our write). An
|
|
356
|
+
// EXPLICIT removal must not be resurrected by that merge.
|
|
357
|
+
this._removedProfiles.add(name);
|
|
358
|
+
this.save();
|
|
359
|
+
},
|
|
360
|
+
|
|
361
|
+
listProfiles() {
|
|
362
|
+
return Object.entries(this._data.profiles).map(([name, profile]) => ({
|
|
363
|
+
name,
|
|
364
|
+
url: profile.url,
|
|
365
|
+
active: name === this._effectiveProfile,
|
|
366
|
+
loggedIn: !!profile.auth?.access_token,
|
|
367
|
+
email: profile.auth?.email || null,
|
|
368
|
+
orgId: profile.auth?.org_id || null,
|
|
369
|
+
}));
|
|
370
|
+
},
|
|
371
|
+
|
|
372
|
+
// Resident profiles (auth.kind === 'resident') are the durable,
|
|
373
|
+
// passwordless credential for unattended automation: no refresh token and
|
|
374
|
+
// no server-side session row to revoke, they re-authenticate via a signed
|
|
375
|
+
// Ed25519 challenge on every 401 (see residentAuth.js). When a password
|
|
376
|
+
// session dead-ends (revoked/expired) the CLI used to loop agents through
|
|
377
|
+
// `quox login` — a human action an unattended agent cannot take — so this
|
|
378
|
+
// lets the dead-end paths point at the resident profile instead.
|
|
379
|
+
// Added devtooling-fix 2026-09-10 after the owner was re-logging-in for an
|
|
380
|
+
// agent repeatedly while a working resident profile sat unused.
|
|
381
|
+
listResidentProfiles() {
|
|
382
|
+
return Object.entries(this._data.profiles)
|
|
383
|
+
.filter(([, p]) => p?.auth?.kind === 'resident')
|
|
384
|
+
.map(([name, p]) => ({
|
|
385
|
+
name,
|
|
386
|
+
orgId: p.auth?.org_id || null,
|
|
387
|
+
email: p.auth?.email || null,
|
|
388
|
+
}));
|
|
389
|
+
},
|
|
390
|
+
|
|
391
|
+
getAuth() {
|
|
392
|
+
// Env var override
|
|
393
|
+
if (process.env.QUOX_TOKEN) {
|
|
394
|
+
return { access_token: process.env.QUOX_TOKEN };
|
|
395
|
+
}
|
|
396
|
+
return this.getActiveProfile()?.auth || null;
|
|
397
|
+
},
|
|
398
|
+
|
|
399
|
+
// quox#455 gap 2: a resident bootstrap carries per-service reach-back
|
|
400
|
+
// URLs discovered at enable time (the instance edge, the collector's
|
|
401
|
+
// real scheme+port). Persist them onto the ACTIVE profile so every
|
|
402
|
+
// later command resolves services from what the host actually reaches,
|
|
403
|
+
// instead of deriving them by port/scheme guesswork.
|
|
404
|
+
setProfileConnection({ url, services } = {}) {
|
|
405
|
+
const profile = this.getActiveProfile();
|
|
406
|
+
if (!profile) throw new Error('No active profile.');
|
|
407
|
+
if (url) profile.url = url;
|
|
408
|
+
if (services && typeof services === 'object') {
|
|
409
|
+
profile.services = { ...(profile.services || {}), ...services };
|
|
410
|
+
}
|
|
411
|
+
this.save();
|
|
412
|
+
},
|
|
413
|
+
|
|
414
|
+
// Stale-sign-in fix: another window or `quox login` may have written fresher credentials for this
|
|
415
|
+
// profile since this process loaded. Re-read ONLY the active profile's auth from disk and adopt it in
|
|
416
|
+
// place (callers hold profile refs). Returns true when the auth changed. Never logs or returns a token.
|
|
417
|
+
reloadAuth() {
|
|
418
|
+
try {
|
|
419
|
+
const name = this._effectiveProfile;
|
|
420
|
+
const disk = JSON.parse(readFileSync(getConfigFile(), 'utf-8'));
|
|
421
|
+
const dAuth = disk?.profiles?.[name]?.auth ?? null;
|
|
422
|
+
const prof = this._data.profiles?.[name];
|
|
423
|
+
if (!prof || dAuth == null) return false;
|
|
424
|
+
if (J(prof.auth) === J(dAuth)) return false;
|
|
425
|
+
prof.auth = dAuth;
|
|
426
|
+
// The loaded snapshot now agrees with disk for auth, so save() does not treat it as our change.
|
|
427
|
+
try {
|
|
428
|
+
const snap = JSON.parse(this._loadedProfiles[name] || '{}');
|
|
429
|
+
snap.auth = dAuth;
|
|
430
|
+
this._loadedProfiles[name] = J(snap);
|
|
431
|
+
} catch { /* snapshot is advisory */ }
|
|
432
|
+
this._clearedAuth.delete(name);
|
|
433
|
+
this._refreshedAuth.delete(name);
|
|
434
|
+
return true;
|
|
435
|
+
} catch { return false; }
|
|
436
|
+
},
|
|
437
|
+
|
|
438
|
+
/** mtime (ms) of the credentials file, or 0. A cheap stat for "did anything sign in elsewhere". */
|
|
439
|
+
credentialsMtime() {
|
|
440
|
+
try { return statSync(getConfigFile()).mtimeMs; } catch { return 0; }
|
|
441
|
+
},
|
|
442
|
+
|
|
443
|
+
/** Cross-process refresh lock file, next to the credentials. */
|
|
444
|
+
refreshLockFile() { return `${getConfigFile()}.refresh.lock`; },
|
|
445
|
+
|
|
446
|
+
setAuth(authData, opts = {}) {
|
|
447
|
+
const profile = this.getActiveProfile();
|
|
448
|
+
if (!profile) throw new Error('No active profile.');
|
|
449
|
+
profile.auth = authData;
|
|
450
|
+
if (opts.refresh) this._refreshedAuth.add(this._effectiveProfile);
|
|
451
|
+
else this._refreshedAuth.delete(this._effectiveProfile);
|
|
452
|
+
this.save();
|
|
453
|
+
},
|
|
454
|
+
|
|
455
|
+
// opts.refresh: the clear is a reaction to a refresh rejection from this
|
|
456
|
+
// process's (possibly stale) session; it must not wipe a newer login.
|
|
457
|
+
clearAuth(opts = {}) {
|
|
458
|
+
const profile = this.getActiveProfile();
|
|
459
|
+
if (profile) {
|
|
460
|
+
profile.auth = null;
|
|
461
|
+
this._clearedAuth.add(this._effectiveProfile);
|
|
462
|
+
if (opts.refresh) this._refreshedAuth.add(this._effectiveProfile);
|
|
463
|
+
else this._refreshedAuth.delete(this._effectiveProfile);
|
|
464
|
+
}
|
|
465
|
+
this.save();
|
|
466
|
+
},
|
|
467
|
+
|
|
468
|
+
getBaseUrl() {
|
|
469
|
+
// Env var override
|
|
470
|
+
if (process.env.QUOX_URL) return process.env.QUOX_URL;
|
|
471
|
+
return this.getActiveProfile()?.url || 'http://localhost:3000';
|
|
472
|
+
},
|
|
473
|
+
|
|
474
|
+
getServiceUrl(service) {
|
|
475
|
+
// 1. Per-service env var override (e.g. QUOX_AUTH_URL, QUOX_BASTION_URL)
|
|
476
|
+
const envKey = `QUOX_${service.toUpperCase()}_URL`;
|
|
477
|
+
if (process.env[envKey]) return process.env[envKey];
|
|
478
|
+
|
|
479
|
+
// 1.5. Per-profile service overrides (quox#455: stamped by resident
|
|
480
|
+
// login from the enable-time bootstrap; what the host truly reaches)
|
|
481
|
+
const profileServices = this.getActiveProfile()?.services;
|
|
482
|
+
if (profileServices && profileServices[service]) return profileServices[service];
|
|
483
|
+
|
|
484
|
+
// 2. Shared services config (for external services: bastion, quoxagent, etc.)
|
|
485
|
+
const services = this._data.services;
|
|
486
|
+
if (services && services[service]) return services[service];
|
|
487
|
+
|
|
488
|
+
// 3. Known service port map (for docker-compose services).
|
|
489
|
+
//
|
|
490
|
+
// DEVSUITE-GRIPE-P7 / Bug A: derive from the ACTIVE PROFILE's own url,
|
|
491
|
+
// never getBaseUrl() — getBaseUrl() honours the global QUOX_URL env
|
|
492
|
+
// override ahead of the profile, which let a stale QUOX_URL silently
|
|
493
|
+
// defeat an explicit -p/--profile selection for every service-port
|
|
494
|
+
// derivation (auth, memory, etc.) while the profile still correctly
|
|
495
|
+
// held its own token/services. -p must be authoritative here the same
|
|
496
|
+
// way it already is for the overrides above. A default-config fallback
|
|
497
|
+
// (no active profile at all) still falls back to the documented
|
|
498
|
+
// default base, matching getBaseUrl()'s own no-profile fallback.
|
|
499
|
+
const profileUrl = this.getActiveProfile()?.url
|
|
500
|
+
|| (process.env.QUOX_URL) // no active profile: nothing more specific to prefer
|
|
501
|
+
|| 'http://localhost:3000';
|
|
502
|
+
const port = SERVICE_PORTS[service];
|
|
503
|
+
if (!port) return this.getBaseUrl();
|
|
504
|
+
|
|
505
|
+
// Replace the port in the base URL.
|
|
506
|
+
//
|
|
507
|
+
// BRAIN2-AUDIT 2026-09-12: the port rewrite used to keep the base URL's
|
|
508
|
+
// scheme, so a profile pointed at https://localhost:3000 (nginx, the
|
|
509
|
+
// ONLY TLS terminator in docker-compose.yml) derived
|
|
510
|
+
// https://localhost:3106 for quoxbrain — every docker-compose service
|
|
511
|
+
// port is a plain node HTTP server, so the TLS handshake never
|
|
512
|
+
// completed and the call died as "fetch failed". `quox brain status`
|
|
513
|
+
// had plausibly never worked against this deployment shape (the
|
|
514
|
+
// product's own usage ledger recorded zero CLI brain calls).
|
|
515
|
+
// On a LOOPBACK host, a port-map rewrite therefore forces http:. This
|
|
516
|
+
// is deliberately narrow: it applies only to this derivation step, so
|
|
517
|
+
// env-var overrides (step 1), per-profile service overrides (step 1.5)
|
|
518
|
+
// and the shared services config (step 2) still say exactly what they
|
|
519
|
+
// say, and a remote/non-loopback host keeps its scheme (a real
|
|
520
|
+
// deployment may terminate TLS per port).
|
|
521
|
+
return deriveServiceUrl(profileUrl, port, service);
|
|
522
|
+
},
|
|
523
|
+
|
|
524
|
+
get(key) {
|
|
525
|
+
// quox#570: url is a PROFILE field. Every reader (getBaseUrl, service
|
|
526
|
+
// derivation) prefers the active profile's url, so get must read the
|
|
527
|
+
// same place set writes or a set/get round-trip lies.
|
|
528
|
+
if (key === 'url') {
|
|
529
|
+
const profile = this.getActiveProfile();
|
|
530
|
+
if (profile) return profile.url;
|
|
531
|
+
}
|
|
532
|
+
return this._data[key];
|
|
533
|
+
},
|
|
534
|
+
|
|
535
|
+
set(key, value) {
|
|
536
|
+
const PROTECTED = new Set(['version', 'profiles', 'activeProfile']);
|
|
537
|
+
if (PROTECTED.has(key)) {
|
|
538
|
+
throw new Error(`Cannot set protected key "${key}". Use profile commands instead.`);
|
|
539
|
+
}
|
|
540
|
+
// quox#570: `config set url` used to write a top-level `url` key that
|
|
541
|
+
// no reader consults, so repointing a profile silently kept the old
|
|
542
|
+
// host. Route it to the effective profile (which honours -p) when one
|
|
543
|
+
// exists; top-level stays the fallback for a profile-less config.
|
|
544
|
+
if (key === 'url') {
|
|
545
|
+
const profile = this.getActiveProfile();
|
|
546
|
+
if (profile) {
|
|
547
|
+
profile.url = value;
|
|
548
|
+
this.save();
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
this._data[key] = value;
|
|
553
|
+
this.save();
|
|
554
|
+
},
|
|
555
|
+
|
|
556
|
+
// quox#527: parallel sessions share this file. Two protections:
|
|
557
|
+
// (1) merge-on-save: profiles present on disk that THIS process has
|
|
558
|
+
// never seen are kept (a sibling session's fresh login/profile is
|
|
559
|
+
// not wiped by our unrelated write); explicit removals recorded in
|
|
560
|
+
// _removedProfiles win over the merge so deletion stays deletion.
|
|
561
|
+
// (2) atomic write: temp file + rename, so a concurrent reader never
|
|
562
|
+
// sees a torn/partial JSON file.
|
|
563
|
+
// The narrower last-writer-wins on a SHARED profile's fields remains —
|
|
564
|
+
// that needs a lock, not a merge — but "my profile vanished mid-session"
|
|
565
|
+
// (the filed symptom) is closed by (1).
|
|
566
|
+
_removedProfiles: new Set(),
|
|
567
|
+
// (3) 2026-10-05: a profile this process LOADED but never changed is not
|
|
568
|
+
// ours to write. Background processes refreshing `resident` were
|
|
569
|
+
// writing their stale copy of `moo` over a fresh `quox -p moo login`
|
|
570
|
+
// seconds later. Snapshot each profile at load; an unchanged one
|
|
571
|
+
// takes the on-disk version on save.
|
|
572
|
+
_loadedProfiles: Object.fromEntries(Object.entries(config.profiles || {}).map(([n, v]) => [n, JSON.stringify(v)])),
|
|
573
|
+
|
|
574
|
+
// quox#713: save() is PROFILE-SCOPED and FIELD-SCOPED. Under config.json.lock
|
|
575
|
+
// it re-reads the file and applies only the fields this process changed
|
|
576
|
+
// relative to what it loaded (snapshot diff). Rules:
|
|
577
|
+
// - an unchanged profile/field is never written;
|
|
578
|
+
// - auth becomes null only via an explicit clearAuth() on that profile;
|
|
579
|
+
// - a REFRESH (setAuth(a, {refresh:true})) never overwrites an on-disk
|
|
580
|
+
// auth that differs from the one this process loaded (a newer login or
|
|
581
|
+
// sibling refresh happened meanwhile): disk wins.
|
|
582
|
+
_clearedAuth: new Set(),
|
|
583
|
+
_refreshedAuth: new Set(),
|
|
584
|
+
_loadedTop: Object.fromEntries(Object.entries(config).filter(([k]) => k !== 'profiles').map(([k, v]) => [k, J(v)])),
|
|
585
|
+
|
|
586
|
+
save() {
|
|
587
|
+
if (!existsSync(getConfigDir())) {
|
|
588
|
+
mkdirSync(getConfigDir(), { recursive: true });
|
|
589
|
+
}
|
|
590
|
+
const lockFile = `${getConfigFile()}.lock`;
|
|
591
|
+
const locked = acquireLock(lockFile);
|
|
592
|
+
if (!locked) debugLog('could not acquire config lock, writing unlocked');
|
|
593
|
+
try {
|
|
594
|
+
let disk = null;
|
|
595
|
+
try {
|
|
596
|
+
disk = JSON.parse(readFileSync(getConfigFile(), 'utf-8'));
|
|
597
|
+
if (!disk || typeof disk !== 'object') disk = null;
|
|
598
|
+
} catch { /* no file yet, or unreadable JSON */ }
|
|
599
|
+
// #736: an existing file we cannot parse is kept aside, never overwritten.
|
|
600
|
+
if (!disk && existsSync(getConfigFile())) moveConfigAside('unreadable at save');
|
|
601
|
+
const out = disk || JSON.parse(JSON.stringify(this._data));
|
|
602
|
+
if (!out.profiles) out.profiles = {};
|
|
603
|
+
if (disk) {
|
|
604
|
+
// top-level keys (activeProfile, services, ...): only ones we changed
|
|
605
|
+
for (const [k, v] of Object.entries(this._data)) {
|
|
606
|
+
if (k === 'profiles') continue;
|
|
607
|
+
if (this._loadedTop[k] !== J(v)) out[k] = v;
|
|
608
|
+
}
|
|
609
|
+
for (const name of Object.keys(this._data.profiles)) {
|
|
610
|
+
if (this._removedProfiles.has(name)) continue;
|
|
611
|
+
const mine = this._data.profiles[name];
|
|
612
|
+
const loaded = this._loadedProfiles[name] === undefined ? undefined : JSON.parse(this._loadedProfiles[name]);
|
|
613
|
+
const wasCleared = this._clearedAuth.has(name);
|
|
614
|
+
if (loaded !== undefined && J(mine) === J(loaded) && !wasCleared) continue; // untouched
|
|
615
|
+
const target = out.profiles[name];
|
|
616
|
+
if (target === undefined) { out.profiles[name] = JSON.parse(JSON.stringify(mine)); continue; }
|
|
617
|
+
for (const key of new Set([...Object.keys(mine), ...Object.keys(loaded || {})])) {
|
|
618
|
+
if (loaded && J(mine[key]) === J(loaded[key]) && !(key === 'auth' && wasCleared)) continue;
|
|
619
|
+
if (key === 'auth') {
|
|
620
|
+
if (mine.auth == null && !wasCleared) { debugLog(`refusing implicit null auth for ${name}`); continue; }
|
|
621
|
+
if (this._refreshedAuth.has(name) && target.auth != null && J(target.auth) !== J(loaded?.auth)) {
|
|
622
|
+
debugLog(`keeping newer on-disk auth for ${name} over refresh from older session`);
|
|
623
|
+
continue;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
if (mine[key] === undefined) delete target[key]; else target[key] = mine[key];
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
for (const name of this._removedProfiles) delete out.profiles[name];
|
|
630
|
+
}
|
|
631
|
+
// adopt merged result in memory (in place, callers hold profile refs)
|
|
632
|
+
for (const [name, prof] of Object.entries(out.profiles)) {
|
|
633
|
+
const cur = this._data.profiles[name];
|
|
634
|
+
if (cur && cur !== prof) {
|
|
635
|
+
for (const k of Object.keys(cur)) delete cur[k];
|
|
636
|
+
Object.assign(cur, prof);
|
|
637
|
+
} else if (!cur) {
|
|
638
|
+
this._data.profiles[name] = prof;
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
for (const [k, v] of Object.entries(out)) if (k !== 'profiles') this._data[k] = v;
|
|
642
|
+
const tmpFile = `${getConfigFile()}.tmp-${process.pid}`;
|
|
643
|
+
try {
|
|
644
|
+
writeFileSync(tmpFile, JSON.stringify(out, null, 2) + '\n', { encoding: 'utf-8', mode: 0o600 });
|
|
645
|
+
} catch (err) {
|
|
646
|
+
// ENOSPC and friends: never leave a stray tmp, never touch config.json.
|
|
647
|
+
try { unlinkSync(tmpFile); } catch { /* not created */ }
|
|
648
|
+
throw err;
|
|
649
|
+
}
|
|
650
|
+
try {
|
|
651
|
+
chmodSync(tmpFile, 0o600);
|
|
652
|
+
} catch {
|
|
653
|
+
// May fail on Windows, acceptable
|
|
654
|
+
}
|
|
655
|
+
// #736: keep the last GOOD config as .bak before replacing it.
|
|
656
|
+
if (disk) {
|
|
657
|
+
try { copyFileSync(getConfigFile(), `${getConfigFile()}.bak`); chmodSync(`${getConfigFile()}.bak`, 0o600); } catch { /* best effort */ }
|
|
658
|
+
}
|
|
659
|
+
renameSync(tmpFile, getConfigFile());
|
|
660
|
+
this._loadedProfiles = Object.fromEntries(Object.entries(this._data.profiles || {}).map(([n, v]) => [n, J(v)]));
|
|
661
|
+
this._loadedTop = Object.fromEntries(Object.entries(this._data).filter(([k]) => k !== 'profiles').map(([k, v]) => [k, J(v)]));
|
|
662
|
+
this._clearedAuth.clear();
|
|
663
|
+
this._refreshedAuth.clear();
|
|
664
|
+
} finally {
|
|
665
|
+
if (locked) releaseLock(lockFile);
|
|
666
|
+
}
|
|
667
|
+
},
|
|
668
|
+
};
|
|
669
|
+
}
|