quoxcli 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +2 -0
- package/bin/quox.js +99 -126
- package/lib/api/endpoints/actions.js +15 -0
- package/lib/api/endpoints/admin.js +46 -0
- package/lib/api/endpoints/agent-schedules.js +26 -0
- package/lib/api/endpoints/agents.js +7 -0
- package/lib/api/endpoints/assistants.js +12 -0
- package/lib/api/endpoints/away.js +15 -0
- package/lib/api/endpoints/blog.js +16 -0
- package/lib/api/endpoints/chat.js +14 -0
- package/lib/api/endpoints/contactsq.js +6 -0
- package/lib/api/endpoints/conversations.js +14 -0
- package/lib/api/endpoints/devServerRegistry.js +25 -0
- package/lib/api/endpoints/engagement.js +26 -0
- package/lib/api/endpoints/evidence.js +20 -0
- package/lib/api/endpoints/executions.js +13 -0
- package/lib/api/endpoints/fetchAllowlist.js +27 -0
- package/lib/api/endpoints/files.js +14 -0
- package/lib/api/endpoints/hat.js +44 -0
- package/lib/api/endpoints/inbox.js +20 -0
- package/lib/api/endpoints/index.js +26 -0
- package/lib/api/endpoints/loops.js +26 -0
- package/lib/api/endpoints/memory.js +19 -0
- package/lib/api/endpoints/mind.js +13 -0
- package/lib/api/endpoints/mon.js +179 -0
- package/lib/api/endpoints/objectives.js +12 -0
- package/lib/api/endpoints/plugins.js +18 -0
- package/lib/api/endpoints/presence.js +33 -0
- package/lib/api/endpoints/quoxplan.js +7 -0
- package/lib/api/endpoints/quoxseo.js +12 -0
- package/lib/api/endpoints/router.js +17 -0
- package/lib/api/endpoints/safety.js +7 -0
- package/lib/api/endpoints/server.js +21 -0
- package/lib/api/endpoints/setup.js +6 -0
- package/lib/api/endpoints/skills.js +20 -0
- package/lib/api/endpoints/stepup.js +20 -0
- package/lib/api/endpoints/teams.js +2 -2
- package/lib/api/endpoints/triggers.js +26 -0
- package/lib/api/endpoints/v1.js +120 -0
- package/lib/api/endpoints/ward.js +7 -0
- package/lib/api/endpoints/workflows.js +7 -0
- package/lib/api/endpoints/x.js +8 -0
- package/lib/commands/actions.js +131 -0
- package/lib/commands/admin.js +16 -0
- package/lib/commands/agent.js +170 -27
- package/lib/commands/agentAttach.js +140 -13
- package/lib/commands/approvals.js +78 -1
- package/lib/commands/aside.js +24 -0
- package/lib/commands/ask.js +146 -0
- package/lib/commands/assistant.js +59 -4
- package/lib/commands/auth.js +8 -2
- package/lib/commands/backup.js +8 -4
- package/lib/commands/blog.js +110 -0
- package/lib/commands/brain.js +105 -14
- package/lib/commands/build.js +19 -5
- package/lib/commands/chat.js +180 -26
- package/lib/commands/chatCompanion.js +27 -0
- package/lib/commands/chatState.js +45 -0
- package/lib/commands/claude.js +35 -38
- package/lib/commands/contacts.js +134 -4
- package/lib/commands/contactsSendAt.js +21 -0
- package/lib/commands/conversation.js +35 -4
- package/lib/commands/dashboard.js +43 -0
- package/lib/commands/department.js +28 -7
- package/lib/commands/devResident.js +15 -3
- package/lib/commands/devSupervisor.js +13 -1
- package/lib/commands/discord.js +75 -39
- package/lib/commands/done.js +98 -28
- package/lib/commands/engagement.js +107 -6
- package/lib/commands/evidence.js +154 -0
- package/lib/commands/ext.js +23 -0
- package/lib/commands/fetchAllowlist.js +71 -0
- package/lib/commands/file.js +38 -1
- package/lib/commands/fleet.js +336 -4
- package/lib/commands/governance.js +12 -3
- package/lib/commands/gripe.js +44 -13
- package/lib/commands/hat.js +254 -9
- package/lib/commands/helpers.js +18 -1
- package/lib/commands/hyperion.js +58 -0
- package/lib/commands/image.js +68 -0
- package/lib/commands/inbox.js +74 -23
- package/lib/commands/kuma.js +1050 -0
- package/lib/commands/kumaSla.js +174 -0
- package/lib/commands/licence.js +52 -155
- package/lib/commands/loop.js +291 -0
- package/lib/commands/lucid.js +10 -4
- package/lib/commands/memory.js +85 -8
- package/lib/commands/mind.js +71 -0
- package/lib/commands/mon.js +69 -2
- package/lib/commands/monitoring.js +1 -1
- package/lib/commands/motion.js +2 -2
- package/lib/commands/nucleus.js +48 -17
- package/lib/commands/objective.js +59 -1
- package/lib/commands/offload.js +89 -0
- package/lib/commands/onboard.js +2 -1
- package/lib/commands/org.js +30 -3
- package/lib/commands/peek.js +28 -0
- package/lib/commands/plan.js +38 -0
- package/lib/commands/plugin.js +110 -0
- package/lib/commands/presence.js +89 -0
- package/lib/commands/provider.js +217 -0
- package/lib/commands/qlarity.js +19 -3
- package/lib/commands/quoxchat.js +13 -4
- package/lib/commands/quoxseo.js +69 -0
- package/lib/commands/reach.js +2 -1
- package/lib/commands/research.js +79 -0
- package/lib/commands/router.js +77 -0
- package/lib/commands/rows.js +367 -0
- package/lib/commands/run.js +82 -10
- package/lib/commands/scope.js +2 -2
- package/lib/commands/server.js +405 -0
- package/lib/commands/setup.js +33 -7
- package/lib/commands/shortcut.js +41 -0
- package/lib/commands/skills.js +68 -9
- package/lib/commands/slot.js +260 -7
- package/lib/commands/status.js +1 -1
- package/lib/commands/team.js +3 -1
- package/lib/commands/tips.js +23 -0
- package/lib/commands/tool.js +133 -45
- package/lib/commands/trace.js +12 -7
- package/lib/commands/trigger.js +95 -0
- package/lib/commands/update.js +46 -0
- package/lib/commands/vault.js +31 -8
- package/lib/commands/vm.js +237 -0
- package/lib/commands/voice.js +34 -3
- package/lib/commands/volt.js +45 -3
- package/lib/commands/walkthrough.js +32 -0
- package/lib/commands/watch.js +73 -21
- package/lib/commands/welcome.js +14 -0
- package/lib/commands/workflow.js +189 -32
- package/lib/commands/x.js +13 -4
- package/lib/commands/youtube.js +172 -0
- package/lib/core/actingAs.js +2 -2
- package/lib/core/actionItems.js +115 -0
- package/lib/core/activeSummary.js +186 -0
- package/lib/core/agentAlias.js +2 -0
- package/lib/core/agentFamilies.js +7 -0
- package/lib/core/aside.js +54 -0
- package/lib/core/askDecision.js +60 -0
- package/lib/core/auth.js +164 -13
- package/lib/core/callerSource.js +1 -1
- package/lib/core/cliBuild.js +62 -0
- package/lib/core/client.js +152 -35
- package/lib/core/commandRegistrars.js +130 -0
- package/lib/core/config.js +143 -25
- package/lib/core/customHats.js +168 -0
- package/lib/core/doneEngineFetch.js +8 -0
- package/lib/core/errors.js +329 -20
- package/lib/core/findingReconcile.js +90 -0
- package/lib/core/global-flags.js +105 -5
- package/lib/core/governedSession.js +1 -3
- package/lib/core/hyperion.js +102 -0
- package/lib/core/inboxResolution.js +65 -0
- package/lib/core/lazyCommands.js +4 -1
- package/lib/core/liveSessionsReport.js +1 -1
- package/lib/core/loopGlyph.js +97 -0
- package/lib/core/offload.js +533 -0
- package/lib/core/org-override.js +1 -1
- package/lib/core/orgResolve.js +70 -0
- package/lib/core/output.js +11 -11
- package/lib/core/peek.js +126 -0
- package/lib/core/profileUsers.js +51 -0
- package/lib/core/promptCancel.js +18 -0
- package/lib/core/repoPath.js +11 -0
- package/lib/core/residentSeed.js +3 -1
- package/lib/core/rootOptions.js +39 -0
- package/lib/core/router.js +168 -0
- package/lib/core/selfUpdate.js +188 -0
- package/lib/core/sessionHeartbeat.js +12 -8
- package/lib/core/sessionsOverview.js +64 -0
- package/lib/core/shortcut.js +83 -0
- package/lib/core/slotKeeper.js +248 -3
- package/lib/core/slotWake.js +120 -0
- package/lib/core/supervisorEngine.js +71 -2
- package/lib/core/version.js +1 -1
- package/lib/core/vm.js +366 -0
- package/lib/core/vmTui.js +86 -0
- package/lib/core/walkthrough.js +35 -0
- package/lib/dragon/companionView.js +34 -0
- package/lib/dragon/frames.js +118 -0
- package/lib/dragon/jobs.js +123 -0
- package/lib/dragon/renderWorker.js +15 -0
- package/lib/dragon/select.js +72 -0
- package/lib/evidence/bundleExport.js +47 -0
- package/lib/evidence/bundleVerify.js +77 -0
- package/lib/hyperion/deck.js +29 -0
- package/lib/lucid/LucidCard.jsx +68 -0
- package/lib/lucid/LucidIntro.jsx +45 -58
- package/lib/lucid/cards.js +86 -0
- package/lib/lucid/markdown.js +81 -0
- package/lib/lucid/motion.js +56 -0
- package/lib/quoxon/EXTENSIONS.md +23 -0
- package/lib/quoxon/activity.js +14 -2
- package/lib/quoxon/aocl.js +117 -0
- package/lib/quoxon/api.js +690 -25
- package/lib/quoxon/app.js +3586 -250
- package/lib/quoxon/approvalFacts.js +96 -0
- package/lib/quoxon/approvalSentence.js +44 -0
- package/lib/quoxon/approvalsView.js +384 -0
- package/lib/quoxon/asideView.js +40 -0
- package/lib/quoxon/ask.js +121 -0
- package/lib/quoxon/away.js +52 -0
- package/lib/quoxon/bang.js +224 -0
- package/lib/quoxon/bulkRisk.js +17 -0
- package/lib/quoxon/capabilities.js +13 -6
- package/lib/quoxon/cells.js +53 -0
- package/lib/quoxon/commandShort.js +124 -0
- package/lib/quoxon/commands.js +429 -39
- package/lib/quoxon/connect.js +210 -23
- package/lib/quoxon/connectCard.js +19 -2
- package/lib/quoxon/consent.js +86 -0
- package/lib/quoxon/copy.js +4 -3
- package/lib/quoxon/countdownLine.js +20 -0
- package/lib/quoxon/deck.js +30 -0
- package/lib/quoxon/demo.autoplay.js +29 -0
- package/lib/quoxon/demo.fixtures.js +1 -1
- package/lib/quoxon/demo.js +46 -3
- package/lib/quoxon/devObserver.js +20 -0
- package/lib/quoxon/dividerAocl.js +192 -0
- package/lib/quoxon/dividerRule.js +57 -0
- package/lib/quoxon/dragonCompanion.js +255 -0
- package/lib/quoxon/dragonModes.js +151 -0
- package/lib/quoxon/dragonMoments.js +82 -0
- package/lib/quoxon/dragonReads.js +68 -0
- package/lib/quoxon/dragonView.js +41 -0
- package/lib/quoxon/earlyInput.js +40 -0
- package/lib/quoxon/echo.js +15 -0
- package/lib/quoxon/egg.js +2 -2
- package/lib/quoxon/events.js +266 -26
- package/lib/quoxon/extensions.js +276 -0
- package/lib/quoxon/feedback.js +50 -0
- package/lib/quoxon/gallery.js +21 -7
- package/lib/quoxon/handoffOffer.js +78 -0
- package/lib/quoxon/hatSwitch.js +37 -0
- package/lib/quoxon/headless.js +161 -0
- package/lib/quoxon/hostUrls.js +41 -0
- package/lib/quoxon/inboxView.js +79 -0
- package/lib/quoxon/indicator.js +6 -2
- package/lib/quoxon/installCli.js +79 -0
- package/lib/quoxon/intent.js +12 -1
- package/lib/quoxon/keys.js +51 -8
- package/lib/quoxon/launcher.js +103 -0
- package/lib/quoxon/loopCard.js +456 -0
- package/lib/quoxon/loopCards.js +180 -0
- package/lib/quoxon/loopStream.js +87 -0
- package/lib/quoxon/main.js +101 -160
- package/lib/quoxon/markdown.js +45 -21
- package/lib/quoxon/markdownSettle.js +19 -0
- package/lib/quoxon/matrixDomains.json +46 -0
- package/lib/quoxon/mentions.js +90 -0
- package/lib/quoxon/mind.js +103 -0
- package/lib/quoxon/mode.js +46 -0
- package/lib/quoxon/model.js +132 -26
- package/lib/quoxon/modelPrefs.js +345 -0
- package/lib/quoxon/motion/clock.js +3 -2
- package/lib/quoxon/motion/level.js +91 -0
- package/lib/quoxon/mouse.js +222 -0
- package/lib/quoxon/noai.js +57 -0
- package/lib/quoxon/objectiveOffer.js +82 -0
- package/lib/quoxon/objects/away.js +72 -0
- package/lib/quoxon/objects/fetch.js +17 -5
- package/lib/quoxon/objects/fleet.js +149 -0
- package/lib/quoxon/objects/health.js +186 -0
- package/lib/quoxon/objects/index.js +46 -7
- package/lib/quoxon/objects/mirror.js +137 -0
- package/lib/quoxon/objects/model.js +33 -0
- package/lib/quoxon/objects/peek.js +17 -0
- package/lib/quoxon/objects/proof.js +12 -4
- package/lib/quoxon/objects/prove.js +130 -0
- package/lib/quoxon/objects/replay.js +125 -0
- package/lib/quoxon/objects/sitrep.js +58 -0
- package/lib/quoxon/objects/view.js +11 -0
- package/lib/quoxon/onboarding/copy.js +32 -4
- package/lib/quoxon/onboarding/credential.js +17 -4
- package/lib/quoxon/onboarding/dockerOffer.js +45 -0
- package/lib/quoxon/onboarding/failureCause.js +33 -0
- package/lib/quoxon/onboarding/finish.js +15 -0
- package/lib/quoxon/onboarding/index.js +34 -7
- package/lib/quoxon/onboarding/installRunner.js +64 -5
- package/lib/quoxon/onboarding/preflight.js +17 -3
- package/lib/quoxon/onboarding/probe.js +65 -0
- package/lib/quoxon/onboarding/shortcutOffer.js +28 -0
- package/lib/quoxon/onboarding/view.js +76 -19
- package/lib/quoxon/orgLabel.js +10 -0
- package/lib/quoxon/orgSwitch.js +59 -0
- package/lib/quoxon/ownBroadcast.js +22 -0
- package/lib/quoxon/palette.js +43 -0
- package/lib/quoxon/planSteps.js +51 -0
- package/lib/quoxon/print.js +12 -5
- package/lib/quoxon/profileSwitch.js +53 -0
- package/lib/quoxon/promptHistory.js +41 -0
- package/lib/quoxon/providers.js +48 -0
- package/lib/quoxon/qart.js +20 -51
- package/lib/quoxon/qdragon.js +410 -0
- package/lib/quoxon/qdragonFly.js +62 -0
- package/lib/quoxon/qdragonScenes.js +282 -0
- package/lib/quoxon/qeyes.js +213 -0
- package/lib/quoxon/qmotion.js +75 -37
- package/lib/quoxon/render.js +740 -124
- package/lib/quoxon/resend.js +72 -0
- package/lib/quoxon/roster.js +117 -17
- package/lib/quoxon/route.js +6 -5
- package/lib/quoxon/rows.js +954 -0
- package/lib/quoxon/rowsStore.js +80 -0
- package/lib/quoxon/screenContext.js +150 -0
- package/lib/quoxon/screenFrames.js +254 -0
- package/lib/quoxon/serverChip.js +42 -0
- package/lib/quoxon/session.js +182 -2
- package/lib/quoxon/sessionDead.js +17 -0
- package/lib/quoxon/signin.js +125 -0
- package/lib/quoxon/slash/agents.js +112 -0
- package/lib/quoxon/slash/argSources.js +65 -0
- package/lib/quoxon/slash/args.js +65 -0
- package/lib/quoxon/slash/automate.js +89 -0
- package/lib/quoxon/slash/broadcast.js +140 -0
- package/lib/quoxon/slash/broadcastRun.js +108 -0
- package/lib/quoxon/slash/card.js +30 -0
- package/lib/quoxon/slash/challenge.js +114 -0
- package/lib/quoxon/slash/cliCommand.js +94 -0
- package/lib/quoxon/slash/cliLeaves.json +1023 -0
- package/lib/quoxon/slash/compact.js +1 -1
- package/lib/quoxon/slash/confirm.js +30 -0
- package/lib/quoxon/slash/crystallize.js +69 -0
- package/lib/quoxon/slash/dashboard.js +136 -0
- package/lib/quoxon/slash/draft.js +54 -0
- package/lib/quoxon/slash/evidenceCmd.js +34 -0
- package/lib/quoxon/slash/hatWalk.js +206 -0
- package/lib/quoxon/slash/inverses.js +77 -0
- package/lib/quoxon/slash/killer.js +24 -0
- package/lib/quoxon/slash/killerApi.js +16 -0
- package/lib/quoxon/slash/ledger.js +13 -8
- package/lib/quoxon/slash/listView.js +3 -1
- package/lib/quoxon/slash/modelCmd.js +261 -0
- package/lib/quoxon/slash/palette.js +22 -8
- package/lib/quoxon/slash/paletteController.js +117 -7
- package/lib/quoxon/slash/panels.js +21 -6
- package/lib/quoxon/slash/picker.js +20 -2
- package/lib/quoxon/slash/qpeCmd.js +35 -0
- package/lib/quoxon/slash/restart.js +95 -13
- package/lib/quoxon/slash/resume.js +18 -1
- package/lib/quoxon/slash/rowCmd.js +157 -0
- package/lib/quoxon/slash/rowWalk.js +175 -0
- package/lib/quoxon/slash/settings.js +16 -6
- package/lib/quoxon/slash/spawn.js +177 -0
- package/lib/quoxon/slash/stepUp.js +274 -0
- package/lib/quoxon/slash/stepUpCard.js +60 -0
- package/lib/quoxon/slash/stepUpCmds.js +78 -0
- package/lib/quoxon/slash/todoCmd.js +122 -0
- package/lib/quoxon/slash/undoApi.js +38 -0
- package/lib/quoxon/slash/undoCmd.js +49 -0
- package/lib/quoxon/slash/undoWish.js +15 -0
- package/lib/quoxon/slash/watch.js +164 -0
- package/lib/quoxon/slash/wish.js +23 -0
- package/lib/quoxon/slash/wishCmd.js +32 -0
- package/lib/quoxon/slotChat.js +300 -0
- package/lib/quoxon/slotFormat.js +195 -0
- package/lib/quoxon/slotRun.js +131 -0
- package/lib/quoxon/sound/mapping.js +2 -1
- package/lib/quoxon/stray.js +22 -0
- package/lib/quoxon/suggestCommand.js +90 -0
- package/lib/quoxon/term.js +46 -11
- package/lib/quoxon/textBuffer.js +176 -2
- package/lib/quoxon/theme.js +58 -25
- package/lib/quoxon/tips.js +433 -0
- package/lib/quoxon/toolCards.js +30 -0
- package/lib/quoxon/toolError.js +41 -0
- package/lib/quoxon/transcript.js +42 -4
- package/lib/quoxon/web/qface.js +18 -10
- package/lib/quoxon/welcome.js +1 -1
- package/lib/quoxon/whatsnew.js +136 -0
- package/lib/quoxon/whatsnew.jsonl +134 -0
- package/lib/quoxon/windowOrg.js +123 -0
- package/lib/server/devServers.js +137 -0
- package/lib/server/diag.js +129 -0
- package/lib/server/doctor.js +71 -0
- package/lib/server/install.js +149 -0
- package/lib/server/pluginEnable.js +161 -0
- package/lib/server/skew.js +56 -0
- package/lib/server/snapshot.js +131 -0
- package/lib/server/status.js +128 -0
- package/lib/server/sys.js +65 -0
- package/lib/server/uninstall.js +63 -0
- package/lib/server/update.js +234 -0
- package/lib/toolOutputShape.js +315 -0
- package/lib/tui/views/HyperionDeck.jsx +79 -0
- package/lib/tui/views/LucidDeck.jsx +209 -217
- package/package.json +5 -1
- package/vendor/dragonlab/SOURCE +4 -0
- package/vendor/dragonlab/design/cipher/v1/cipher-master.svg +46 -0
- package/vendor/dragonlab/design/cipher/v2/character.mjs +49 -0
- package/vendor/dragonlab/design/cipher/v2/review.mjs +33 -0
- package/vendor/dragonlab/design/cipher/v2/watch.mjs +56 -0
- package/vendor/dragonlab/design/drago/v1/character.mjs +19 -0
- package/vendor/dragonlab/design/drago/v1/drago-master.svg +56 -0
- package/vendor/dragonlab/design/drago/v2/character.mjs +87 -0
- package/vendor/dragonlab/design/drago/v4/character.mjs +67 -0
- package/vendor/dragonlab/design/drago/v4/profile.mjs +35 -0
- package/vendor/dragonlab/design/drago/v4/review.mjs +33 -0
- package/vendor/dragonlab/design/drago/v4/watch.mjs +56 -0
- package/vendor/dragonlab/design/hyperion/v1/hyperion-master.svg +56 -0
- package/vendor/dragonlab/design/hyperion/v2/character.mjs +79 -0
- package/vendor/dragonlab/design/hyperion/v2/review.mjs +33 -0
- package/vendor/dragonlab/design/hyperion/v2/watch.mjs +56 -0
- package/vendor/dragonlab/design/lucid/recovery/arrival.mjs +22 -0
- package/vendor/dragonlab/design/lucid/recovery/character.mjs +106 -0
- package/vendor/dragonlab/design/lucid/recovery/layout.mjs +50 -0
- package/vendor/dragonlab/design/lucid/recovery/lucid-master.svg +78 -0
- package/vendor/dragonlab/design/lucid/recovery/motion.mjs +42 -0
- package/vendor/dragonlab/design/lucid/recovery/render.mjs +121 -0
- package/vendor/dragonlab/design/lucid/recovery/review.mjs +68 -0
- package/vendor/dragonlab/design/lucid/recovery/verify.mjs +84 -0
- package/vendor/dragonlab/design/lucid/recovery/watch.mjs +56 -0
- package/vendor/dragonlab/design/seer/v1/seer-master.svg +53 -0
- package/vendor/dragonlab/design/seer/v2/character.mjs +88 -0
- package/vendor/dragonlab/design/seer/v3/character.mjs +33 -0
- package/vendor/dragonlab/design/seer/v4/character.mjs +66 -0
- package/vendor/dragonlab/design/seer/v4/review.mjs +33 -0
- package/vendor/dragonlab/design/seer/v4/watch.mjs +56 -0
- package/vendor/dragonlab/design/stage3/companion-review.mjs +28 -0
- package/vendor/dragonlab/design/stage3/sentinel-art.js +39 -0
- package/vendor/dragonlab/design/stage3/sentinel-braille.js +216 -0
- package/vendor/dragonlab/design/stage3/sentinel-character.js +257 -0
- package/vendor/dragonlab/design/stage3/sentinel-icon.js +103 -0
- package/vendor/dragonlab/design/stage3/sentinel-warden-eyes.js +95 -0
- package/vendor/dragonlab/design/stage3/svg-coverage.js +148 -0
- package/vendor/dragonlab/factory/svg-rig.mjs +63 -0
- package/vendor/dragonlab/integration/companions-v1/characters.mjs +88 -0
- package/vendor/dragonlab/integration/companions-v1/cipher.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/companion.mjs +63 -0
- package/vendor/dragonlab/integration/companions-v1/drago.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/hyperion.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/lucid.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/seer.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/sentinel.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/view.mjs +28 -0
- package/vendor/dragonlab/integration/hyperion-v3/activity.mjs +76 -0
- package/vendor/dragonlab/lib/power-effects.js +294 -0
- package/vendor/dragonlab/lib/power-registry.js +74 -0
- package/vendor/dragonlab/vendor/quox-dragon/qart.js +348 -0
- package/vendor/dragonlab/vendor/quox-dragon/qeyes.js +213 -0
- package/lib/core/websiteSession.js +0 -148
|
@@ -5,18 +5,24 @@
|
|
|
5
5
|
* which case the shell wins and the card says so.
|
|
6
6
|
*/
|
|
7
7
|
import { PROFILE_IDS } from '../sound/mapping.js';
|
|
8
|
+
import { LEVELS, DEFAULT_LEVEL, normaliseLevel } from '../motion/level.js';
|
|
8
9
|
|
|
9
10
|
// Captured at import, before anything here writes it: what the user's shell set.
|
|
10
|
-
const SHELL = {
|
|
11
|
+
const SHELL = {
|
|
12
|
+
theme: process.env.QUOX_THEME || null,
|
|
13
|
+
motion: process.env.QUOX_REDUCED_MOTION === '1' ? 'quiet' : (normaliseLevel(process.env.QUOX_MOTION) || null),
|
|
14
|
+
motionVar: process.env.QUOX_REDUCED_MOTION === '1' ? 'QUOX_REDUCED_MOTION' : 'QUOX_MOTION',
|
|
15
|
+
};
|
|
11
16
|
|
|
12
17
|
|
|
13
18
|
/** The settings this build can honour. Sound is the /sound controller's own profile (server-side soundProfile). */
|
|
14
19
|
export function settingsList({ sound = true } = {}) {
|
|
15
20
|
return [
|
|
16
21
|
{ id: 'theme', label: 'Theme', values: ['auto', 'dark', 'light'], def: 'auto', help: 'auto follows your terminal background' },
|
|
17
|
-
{ id: 'motion', label: 'Motion', values:
|
|
22
|
+
{ id: 'motion', label: 'Motion', values: LEVELS, def: DEFAULT_LEVEL, help: 'quiet stills it all; expressive adds glows; cinematic adds the dragon' },
|
|
18
23
|
...(sound ? [{ id: 'sound', label: 'Sound', values: PROFILE_IDS, def: 'minimal', help: 'same as /sound; saved to your account' }] : []),
|
|
19
24
|
{ id: 'verbose', label: 'Verbose', values: ['off', 'on', 'full'], def: 'on', help: 'protocol lines in turns' },
|
|
25
|
+
{ id: 'slotBell', label: 'Slot bell', values: ['off', 'on'], def: 'on', help: 'a terminal bell when a slot message arrives' },
|
|
20
26
|
{ id: 'model', label: 'Model', action: 'model', help: 'Enter opens /model' }
|
|
21
27
|
];
|
|
22
28
|
}
|
|
@@ -27,7 +33,9 @@ export function applySettings(s = {}, env = process.env, shell = SHELL) {
|
|
|
27
33
|
if (s.theme === 'dark' || s.theme === 'light') env.QUOX_THEME = s.theme; else delete env.QUOX_THEME;
|
|
28
34
|
}
|
|
29
35
|
if (!shell.motion) {
|
|
30
|
-
|
|
36
|
+
const level = normaliseLevel(s.motion) || DEFAULT_LEVEL; // pre-levels profiles saved full / reduced
|
|
37
|
+
if (level === DEFAULT_LEVEL) delete env.QUOX_MOTION; else env.QUOX_MOTION = level;
|
|
38
|
+
if (level === 'quiet') env.QUOX_REDUCED_MOTION = '1'; else delete env.QUOX_REDUCED_MOTION; // every older reader agrees
|
|
31
39
|
}
|
|
32
40
|
return { themeFromShell: Boolean(shell.theme), motionFromShell: Boolean(shell.motion) };
|
|
33
41
|
}
|
|
@@ -38,13 +46,15 @@ export const shellOverrides = () => ({ ...SHELL });
|
|
|
38
46
|
export function configRows(list, values, { shell = SHELL, model = 'auto' } = {}) {
|
|
39
47
|
return list.map(it => {
|
|
40
48
|
if (it.action === 'model') return { key: it.label, text: model, note: it.help };
|
|
41
|
-
const v = values[it.id] ?? it.def;
|
|
42
|
-
const
|
|
49
|
+
const v = it.id === 'motion' ? (normaliseLevel(values.motion) || it.def) : (values[it.id] ?? it.def);
|
|
50
|
+
const motionVar = shell.motionVar || 'QUOX_MOTION';
|
|
51
|
+
const forced = (it.id === 'theme' && shell.theme) ? `QUOX_THEME=${shell.theme} in your shell wins`
|
|
52
|
+
: (it.id === 'motion' && shell.motion) ? (motionVar === 'QUOX_MOTION' ? `QUOX_MOTION=${shell.motion} in your shell wins` : `${motionVar} in your shell wins`) : null;
|
|
43
53
|
return { key: it.label, text: it.values.map(x => (x === v ? `[${x}]` : x)).join(' '), note: forced || it.help };
|
|
44
54
|
});
|
|
45
55
|
}
|
|
46
56
|
|
|
47
57
|
export const cycle = (it, cur, dir = 1) => {
|
|
48
|
-
const i = it.values.indexOf(cur ?? it.def);
|
|
58
|
+
const i = it.values.indexOf((it.id === 'motion' ? normaliseLevel(cur) : cur) ?? it.def);
|
|
49
59
|
return it.values[(i + dir + it.values.length) % it.values.length];
|
|
50
60
|
};
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* /spawn: ask HYPERION (agent atlas) for a disposable VM. VMs are requested ONLY by posting to slot `provisioner`
|
|
3
|
+
* ("VM please <c>c <r>G <d>G for <purpose>") and released with "done <vmid>"; nothing here talks to the pool tool.
|
|
4
|
+
*
|
|
5
|
+
* Lifespan is required (max 24h). At most 3 live VMs per session, counted from local state. Leases are stored
|
|
6
|
+
* locally with their expiry. No server-side slot reminder exists, so expiry is client side: a timer while the TUI
|
|
7
|
+
* runs, `sweep()` on the next start, and /spawn list shows overdue leases in red.
|
|
8
|
+
*/
|
|
9
|
+
import { askText } from '../ask.js';
|
|
10
|
+
import { mkdirSync, readFileSync, writeFileSync, renameSync } from 'node:fs';
|
|
11
|
+
import { homedir } from 'node:os';
|
|
12
|
+
import { dirname, join } from 'node:path';
|
|
13
|
+
|
|
14
|
+
export const PROVISIONER = 'provisioner';
|
|
15
|
+
export const MAX_LIVE = 3;
|
|
16
|
+
export const MAX_LIFESPAN_MS = 24 * 3600000;
|
|
17
|
+
const MAX_TIMER = 2147483647;
|
|
18
|
+
export const spawnStatePath = () => join(homedir(), '.config', 'quox', 'quoxon-spawns.json');
|
|
19
|
+
|
|
20
|
+
const UNIT = '(?:d|days?|h|hrs?|hours?|m|mins?|minutes?)';
|
|
21
|
+
const unitMs = (u) => (/^d/i.test(u) ? 86400000 : /^h/i.test(u) ? 3600000 : 60000);
|
|
22
|
+
|
|
23
|
+
/** Loose text -> { cores, ramG, diskG, lifespanMs, objective } plus `missing` (names) and `error` when it cannot proceed. */
|
|
24
|
+
export function parseSpawn(text) {
|
|
25
|
+
let rest = String(text || '').trim();
|
|
26
|
+
const out = { cores: null, ramG: null, diskG: null, lifespanMs: null, objective: null };
|
|
27
|
+
const om = new RegExp(`\\b(?:for|to)\\s+(?!\\d+(?:\\.\\d+)?\\s*${UNIT}\\b)(.+)$`, 'i').exec(rest);
|
|
28
|
+
if (om) { out.objective = om[1].trim(); rest = rest.slice(0, om.index); }
|
|
29
|
+
const take = (re, fn) => { const m = re.exec(rest); if (m) { fn(m); rest = rest.replace(m[0], ' '); } };
|
|
30
|
+
take(/(\d+)\s*(?:c|cores?|cpus?|vcpus?)\b/i, (m) => { out.cores = Number(m[1]); });
|
|
31
|
+
take(/(\d+)\s*g(?:i?b)?\s*(?:of\s+)?(?:ram|mem(?:ory)?)\b/i, (m) => { out.ramG = Number(m[1]); });
|
|
32
|
+
take(/(\d+)\s*g(?:i?b)?\s*(?:of\s+)?(?:disk|storage)\b/i, (m) => { out.diskG = Number(m[1]); });
|
|
33
|
+
take(new RegExp(`(?:\\bfor\\s+)?\\b(\\d+(?:\\.\\d+)?)\\s*(${UNIT})\\b`, 'i'), (m) => { out.lifespanMs = Math.round(Number(m[1]) * unitMs(m[2])); });
|
|
34
|
+
for (let m = /(\d+)\s*g(?:i?b)?\b/i.exec(rest); m && (out.ramG == null || out.diskG == null); m = /(\d+)\s*g(?:i?b)?\b/i.exec(rest)) {
|
|
35
|
+
if (out.ramG == null) out.ramG = Number(m[1]); else out.diskG = Number(m[1]);
|
|
36
|
+
rest = rest.replace(m[0], ' ');
|
|
37
|
+
}
|
|
38
|
+
if (out.diskG == null) out.diskG = 20;
|
|
39
|
+
const missing = [];
|
|
40
|
+
if (!out.cores) missing.push('cores');
|
|
41
|
+
if (!out.ramG) missing.push('ram');
|
|
42
|
+
if (!out.lifespanMs) missing.push('lifespan');
|
|
43
|
+
if (!out.objective) missing.push('objective');
|
|
44
|
+
if (missing.length) out.missing = missing;
|
|
45
|
+
if (out.lifespanMs && out.lifespanMs > MAX_LIFESPAN_MS) out.error = 'Lifespan is capped at 24h.';
|
|
46
|
+
if (out.lifespanMs != null && out.lifespanMs <= 0) out.error = 'Lifespan must be more than zero.';
|
|
47
|
+
return out;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** HYPERION's free-text reply -> { vmid, ip, user } | null (no vmid = not a lease). */
|
|
51
|
+
export function parseLease(text) {
|
|
52
|
+
const t = String(text || '');
|
|
53
|
+
const v = /vm\s*id\D{0,3}(\d{3,6})/i.exec(t) || /\b(9\d{3})\b/.exec(t);
|
|
54
|
+
if (!v) return null;
|
|
55
|
+
const ip = /\b(\d{1,3}(?:\.\d{1,3}){3})\b/.exec(t);
|
|
56
|
+
const u = /\buser\W{0,3}([a-z_][\w-]*)/i.exec(t);
|
|
57
|
+
return { vmid: Number(v[1]), ip: ip ? ip[1] : null, user: u ? u[1] : 'hyp' };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export const fmtSpan = (ms) => { const m = Math.round(ms / 60000); return m >= 60 && m % 60 === 0 ? `${m / 60}h` : m >= 60 ? `${Math.floor(m / 60)}h${m % 60}m` : `${Math.max(m, 0)}m`; };
|
|
61
|
+
|
|
62
|
+
const readAll = (p) => { try { const d = JSON.parse(readFileSync(p, 'utf8')); return Array.isArray(d?.leases) ? d : { leases: [] }; } catch { return { leases: [] }; } };
|
|
63
|
+
const writeAll = (d, p) => { try { mkdirSync(dirname(p), { recursive: true }); const tmp = `${p}.${process.pid}.tmp`; writeFileSync(tmp, JSON.stringify(d, null, 2), { mode: 0o600 }); renameSync(tmp, p); } catch { /* a read-only home never breaks the TUI */ } };
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* deps: api (slotSay, slotStream), say, session() -> this window's session name, from() -> slot signature,
|
|
67
|
+
* statePath, now, setTimer/clearTimer (tests), replyTimeoutMs.
|
|
68
|
+
*/
|
|
69
|
+
export function createSpawnCmd({ api, say, session, from, statePath = spawnStatePath(), now = Date.now, setTimer = (fn, ms) => { const t = setTimeout(fn, ms); t.unref?.(); return t; }, clearTimer = clearTimeout, replyTimeoutMs = 180000 }) {
|
|
70
|
+
const st = { pending: null, waiting: null, timers: new Map() };
|
|
71
|
+
|
|
72
|
+
const leases = ({ live = false } = {}) => readAll(statePath).leases.filter((l) => l.session === session() && (!live || l.state === 'live'));
|
|
73
|
+
const save = (fn) => { const d = readAll(statePath); d.leases = fn(d.leases); writeAll(d, statePath); };
|
|
74
|
+
|
|
75
|
+
function arm(l) {
|
|
76
|
+
clearTimer(st.timers.get(l.vmid));
|
|
77
|
+
const ms = Math.min(Math.max(l.expiresAt - now(), 0), MAX_TIMER);
|
|
78
|
+
st.timers.set(l.vmid, setTimer(() => { st.timers.delete(l.vmid); return release(l.vmid, 'lifespan reached'); }, ms));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function release(vmid, why) {
|
|
82
|
+
const l = leases({ live: true }).find((x) => x.vmid === vmid);
|
|
83
|
+
if (!l) return say(`No live VM ${vmid} in this session. /spawn list shows yours.`, 'warn');
|
|
84
|
+
const r = await api.slotSay(PROVISIONER, `done ${vmid}`, { from: from() });
|
|
85
|
+
if (!r?.ok) return say(`Could not post "done ${vmid}" to ${PROVISIONER}: ${r?.message || 'failed'}. It is still listed; retry /spawn done ${vmid}.`, 'error');
|
|
86
|
+
clearTimer(st.timers.get(vmid)); st.timers.delete(vmid);
|
|
87
|
+
save((all) => all.map((x) => (x.vmid === vmid && x.session === l.session ? { ...x, state: 'released', releasedAt: now() } : x)));
|
|
88
|
+
return say(`VM ${vmid} released to HYPERION (${why}).`, 'ok');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function usage() {
|
|
92
|
+
return say('Usage: /spawn <cores>c <ram>G <disk>G <lifespan> for <objective> e.g. /spawn 4 cores 8G 20G for 2h to test the installer. Also: /spawn list, /spawn done <vmid>.', 'warn');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function run(arg = '') {
|
|
96
|
+
const t = String(arg || '').trim();
|
|
97
|
+
if (!t) return usage();
|
|
98
|
+
if (/^list$/i.test(t)) return list();
|
|
99
|
+
const d = /^done\s+(\d+)$/i.exec(t);
|
|
100
|
+
if (d) return release(Number(d[1]), 'released by you');
|
|
101
|
+
st.pending = null;
|
|
102
|
+
const p = parseSpawn(t);
|
|
103
|
+
if (p.error) return say(p.error, 'warn');
|
|
104
|
+
if (p.missing?.includes('lifespan')) return say('How long should it live? Lifespan is required (max 24h): add e.g. 2h or 90m, then run /spawn again.', 'warn');
|
|
105
|
+
if (p.missing) return say(`Missing: ${p.missing.join(', ')}. ${p.missing.includes('objective') ? 'Say what it is for: "for <objective>". ' : ''}Example: /spawn 4c 8G 20G 2h for test the installer`, 'warn');
|
|
106
|
+
const live = leases({ live: true });
|
|
107
|
+
if (live.length >= MAX_LIVE) return say(`This session already has ${live.length} live VMs (cap ${MAX_LIVE}). /spawn done <vmid> frees one: ${live.map((l) => l.vmid).join(', ')}.`, 'warn');
|
|
108
|
+
if (!api.slotSay || !api.slotStream) return say('/spawn needs slots, which this server or mode does not have.', 'warn');
|
|
109
|
+
st.pending = { p, at: now() };
|
|
110
|
+
say(`Request a VM from HYPERION: ${p.cores}c ${p.ramG}G ${p.diskG}G, lifespan ${fmtSpan(p.lifespanMs)}, for: ${p.objective}.`, 'accent');
|
|
111
|
+
say(askText('Go?', { action: `post "VM please ${p.cores}c ${p.ramG}G ${p.diskG}G" to slot ${PROVISIONER}`, reason: `This posts to slot ${PROVISIONER} (HYPERION, agent atlas), signed ${from()}; it releases the VM at expiry.` }), 'ask');
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function answer(text) {
|
|
116
|
+
const pe = st.pending;
|
|
117
|
+
if (!pe) return false;
|
|
118
|
+
const t = String(text || '').trim();
|
|
119
|
+
if (t.startsWith('/')) { st.pending = null; return false; }
|
|
120
|
+
st.pending = null;
|
|
121
|
+
if (/^y(es)?$/i.test(t) && now() - pe.at < 600000) { st.waiting = request(pe.p); return true; }
|
|
122
|
+
say('Cancelled; nothing posted.', 'dim');
|
|
123
|
+
return /^no?$/i.test(t);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function request(p) {
|
|
127
|
+
const me = from();
|
|
128
|
+
const since = new Date(now() - 1000).toISOString();
|
|
129
|
+
const ac = new AbortController();
|
|
130
|
+
const timer = setTimeout(() => ac.abort(), replyTimeoutMs);
|
|
131
|
+
try {
|
|
132
|
+
const stream = api.slotStream(PROVISIONER, { since, excludeFrom: me }, ac.signal);
|
|
133
|
+
const reader = (async () => {
|
|
134
|
+
for await (const ev of stream) {
|
|
135
|
+
if (ev?.type !== 'slot.message' || !ev.message || ev.message.from === me) continue;
|
|
136
|
+
const lease = parseLease(ev.message.text);
|
|
137
|
+
if (lease) return { lease, msg: ev.message };
|
|
138
|
+
if (/refus|cannot|can't|no capacity|denied|unable|error|not allowed/i.test(ev.message.text || '')) return { refusal: ev.message.text };
|
|
139
|
+
}
|
|
140
|
+
return null;
|
|
141
|
+
})();
|
|
142
|
+
const purpose = `${p.objective} (lifespan ${fmtSpan(p.lifespanMs)}, released by the requester at expiry)`;
|
|
143
|
+
const r = await api.slotSay(PROVISIONER, `VM please ${p.cores}c ${p.ramG}G ${p.diskG}G for ${purpose}`, { from: me });
|
|
144
|
+
if (!r?.ok) { ac.abort(); await reader.catch(() => {}); return say(`Could not post to ${PROVISIONER}: ${r?.message || 'failed'}. Nothing was requested.`, 'error'); }
|
|
145
|
+
say(`Posted to ${PROVISIONER}; waiting for HYPERION's lease reply.`, 'dim');
|
|
146
|
+
const got = await reader.catch(() => null);
|
|
147
|
+
if (got?.refusal) return say(`HYPERION: ${got.refusal}`, 'warn');
|
|
148
|
+
if (!got?.lease) return say(`No lease reply from HYPERION within ${Math.round(replyTimeoutMs / 1000)}s. The request is posted; check slot ${PROVISIONER} (/slot log) and, if a VM arrives, /spawn done <vmid> when finished.`, 'warn');
|
|
149
|
+
const { lease } = got;
|
|
150
|
+
const rec = { vmid: lease.vmid, ip: lease.ip, user: lease.user, cores: p.cores, ramG: p.ramG, diskG: p.diskG, objective: p.objective, session: session(), leasedAt: now(), expiresAt: now() + p.lifespanMs, state: 'live' };
|
|
151
|
+
save((all) => [...all, rec]);
|
|
152
|
+
arm(rec);
|
|
153
|
+
say(`VM ${lease.vmid} leased${lease.ip ? ` at ${lease.ip}` : ''}, user ${lease.user}, expires in ${fmtSpan(p.lifespanMs)}.`, 'ok');
|
|
154
|
+
if (lease.ip) say(`Connect: ssh ${lease.user}@${lease.ip} (use the key HYPERION's reply names)`, 'ok');
|
|
155
|
+
say(`Release early with /spawn done ${lease.vmid}.`, 'dim');
|
|
156
|
+
return undefined;
|
|
157
|
+
} finally { clearTimeout(timer); }
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function list() {
|
|
161
|
+
const all = leases();
|
|
162
|
+
if (!all.length) return say('No VMs spawned by this session.', 'dim');
|
|
163
|
+
for (const l of all) {
|
|
164
|
+
const overdue = l.state === 'live' && l.expiresAt < now();
|
|
165
|
+
const when = l.state === 'released' ? 'released' : overdue ? `OVERDUE by ${fmtSpan(now() - l.expiresAt)}, releases on /spawn done ${l.vmid} or next start` : `expires in ${fmtSpan(l.expiresAt - now())}`;
|
|
166
|
+
say(`${l.vmid} ${l.ip || '?'} ${l.user}@ ${l.cores}c ${l.ramG}G ${l.diskG}G ${when} ${l.objective}`, overdue ? 'error' : l.state === 'released' ? 'dim' : undefined);
|
|
167
|
+
}
|
|
168
|
+
return undefined;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** On start: release this session's overdue leases, and re-arm timers for the rest. */
|
|
172
|
+
async function sweep() {
|
|
173
|
+
for (const l of leases({ live: true })) { if (l.expiresAt <= now()) await release(l.vmid, 'overdue at start'); else arm(l); }
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return { run, answer, sweep, leases, get pending() { return st.pending; }, get waiting() { return st.waiting; } };
|
|
177
|
+
}
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QUOXON-STEPUP: the human step-up PIN, TUI side.
|
|
3
|
+
*
|
|
4
|
+
* Why this exists: a typed "y" cannot prove a person typed it (an agent can drive this terminal, and some
|
|
5
|
+
* hold a human profile's CLI login). The SERVER decides what needs a person (agent-raised or high-risk
|
|
6
|
+
* approvals, policy overrides) and refuses with 403 step_up_required. This module only collects a PIN,
|
|
7
|
+
* trades it for a short-lived step-up token (POST /api/me/step-up) and carries that token on the retry.
|
|
8
|
+
* Nothing here is a security boundary on its own: it is the front half of a server-enforced check.
|
|
9
|
+
*
|
|
10
|
+
* The PIN rules, all deliberate:
|
|
11
|
+
* - the digits live in a closure box; they never enter dispatch, the composer buffer, history, the
|
|
12
|
+
* undo/redo stacks, the dev transcript, the observer, or any log; the box is zeroed on every exit
|
|
13
|
+
* - the card renders a COUNT of filled dots, never a digit
|
|
14
|
+
* - 3 wrong tries lock PIN entry for this window (the server also rate-limits per user and locks)
|
|
15
|
+
* - agent logins never get a PIN field: they are told a person must approve
|
|
16
|
+
*/
|
|
17
|
+
import { graphemes } from '../textBuffer.js';
|
|
18
|
+
|
|
19
|
+
export const WINDOW_MS = 10 * 60 * 1000;
|
|
20
|
+
export const MAX_TRIES = 3;
|
|
21
|
+
export const PIN_LEN = 6;
|
|
22
|
+
export const BEAT_MS = 300; // cosmetic floor for the "verifying" beat; the request itself is real
|
|
23
|
+
export const RESULT_HOLD_MS = 900; // how long AUTHORISED / DENIED stays on screen
|
|
24
|
+
export const LOCK_MS = WINDOW_MS; // local lock for this window after MAX_TRIES wrong tries
|
|
25
|
+
export const NEEDS_PERSON = 'needs a person: approve from your phone or a human session';
|
|
26
|
+
export const NO_PIN_LINE = 'Set a PIN in the dashboard: Settings > Account > Approval PIN';
|
|
27
|
+
|
|
28
|
+
/** The PIN digits. Closure only: no accessor but take(), nothing serialisable. */
|
|
29
|
+
export function createPinBox() {
|
|
30
|
+
let d = '';
|
|
31
|
+
return {
|
|
32
|
+
push(t) { for (const ch of String(t || '')) { if (d.length >= PIN_LEN) break; if (ch >= '0' && ch <= '9') d += ch; } return d.length; },
|
|
33
|
+
backspace() { d = d.slice(0, -1); return d.length; },
|
|
34
|
+
clear() { d = ''; },
|
|
35
|
+
count: () => d.length,
|
|
36
|
+
take() { const out = d; d = ''; return out; },
|
|
37
|
+
toJSON: () => '[pin]',
|
|
38
|
+
toString: () => '[pin]',
|
|
39
|
+
[Symbol.for('nodejs.util.inspect.custom')]: () => '[pin]'
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const mmss = (ms) => { const s = Math.max(0, Math.ceil(ms / 1000)); return `${Math.floor(s / 60)}:${String(s % 60).padStart(2, '0')}`; };
|
|
44
|
+
|
|
45
|
+
/** Terminal cell width: emoji-presentation glyphs take two cells. */
|
|
46
|
+
export function cellWidth(s) {
|
|
47
|
+
let w = 0;
|
|
48
|
+
for (const g of graphemes(String(s ?? ''))) w += /\p{Emoji_Presentation}/u.test(g.text) ? 2 : 1;
|
|
49
|
+
return w;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
/** One-line collapse under this many columns. */
|
|
55
|
+
export const NARROW_COLS = 50;
|
|
56
|
+
export const CARD_W = 58;
|
|
57
|
+
/** Below this many columns the card is drawn still: no pulse, no shimmer. */
|
|
58
|
+
export const ANIM_MIN_COLS = 60;
|
|
59
|
+
|
|
60
|
+
/** Word-wrap to `w` cells; a word longer than a line is split, nothing is ever dropped. */
|
|
61
|
+
export function wrapCells(text, w) {
|
|
62
|
+
const out = []; let cur = '';
|
|
63
|
+
const push = () => { if (cur) out.push(cur); cur = ''; };
|
|
64
|
+
for (const word of String(text ?? '').split(/\s+/).filter(Boolean)) {
|
|
65
|
+
let rest = word;
|
|
66
|
+
while (cellWidth(rest) > w) { // hard-split an over-long word
|
|
67
|
+
push(); let part = ''; let n = 0;
|
|
68
|
+
for (const g of graphemes(rest)) { const gw = cellWidth(g.text); if (n + gw > w) break; part += g.text; n += gw; }
|
|
69
|
+
out.push(part); rest = rest.slice(part.length);
|
|
70
|
+
}
|
|
71
|
+
if (!rest) continue;
|
|
72
|
+
if (cur && cellWidth(cur) + 1 + cellWidth(rest) > w) push();
|
|
73
|
+
cur = cur ? `${cur} ${rest}` : rest;
|
|
74
|
+
}
|
|
75
|
+
push();
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Pure: the card as lines of [{ text, role? }] segments. roles: gold (frame, title, filled slots, countdown),
|
|
81
|
+
* prose (ink, empty slots, subject), ok, error, key (the per-digit pulse and the verifying sweep). Never grey.
|
|
82
|
+
* `view` carries counts only, never digits. Optional animation inputs, both plain numbers: `pulse` (slot index
|
|
83
|
+
* just filled) and `tick` (shimmer frame). Without them the card is static, which is what narrow, slow and
|
|
84
|
+
* NO_COLOR terminals get.
|
|
85
|
+
*/
|
|
86
|
+
export function cardLines(view, cols) {
|
|
87
|
+
const { phase, filled = 0, subject = {}, tries = MAX_TRIES, offset = 0, until = 0, now = 0, error = null, pulse = -1, tick = null } = view;
|
|
88
|
+
const shown = phase === 'verifying' || phase === 'ok' ? PIN_LEN : filled; // the box was taken at submit: 6 were typed
|
|
89
|
+
const dots = Array.from({ length: PIN_LEN }, (_, i) => (i < shown ? '●' : '○'));
|
|
90
|
+
const shift = offset ? 1 : 0; // the "tiny shake": one redraw one cell to the right
|
|
91
|
+
const verifyText = '▰▰▰▰ verifying';
|
|
92
|
+
const status = (() => {
|
|
93
|
+
switch (phase) {
|
|
94
|
+
case 'verifying': {
|
|
95
|
+
if (tick == null) return [{ text: verifyText, role: 'gold' }];
|
|
96
|
+
const chars = [...verifyText]; const at = tick % (chars.length + 3);
|
|
97
|
+
return chars.map((ch, i) => ({ text: ch, role: i >= at - 2 && i <= at ? 'text' : 'gold' }));
|
|
98
|
+
}
|
|
99
|
+
case 'ok': return [{ text: '✔ AUTHORISED', role: 'ok' }, { text: ` · human present for ${mmss(until - now)}`, role: 'gold' }];
|
|
100
|
+
case 'denied': return [{ text: `✖ DENIED · ${tries} ${tries === 1 ? 'try' : 'tries'} left`, role: 'error' }];
|
|
101
|
+
case 'locked': return [{ text: '⛔ LOCKED · use your phone or a human session', role: 'error' }];
|
|
102
|
+
case 'nopin': return [{ text: NO_PIN_LINE, role: 'error' }];
|
|
103
|
+
case 'agent': return [{ text: NEEDS_PERSON, role: 'error' }];
|
|
104
|
+
default: return null;
|
|
105
|
+
}
|
|
106
|
+
})();
|
|
107
|
+
const pinSeg = [{ text: 'PIN ', role: 'prose' }, ...dots.flatMap((d, i) => [{ text: d, role: d === '●' ? 'gold' : 'prose' }, { text: i < PIN_LEN - 1 ? ' ' : '', role: 'prose' }])];
|
|
108
|
+
const segW = (segs) => segs.reduce((n, s) => n + cellWidth(s.text), 0);
|
|
109
|
+
const riskSeg = (r) => (r === 'high' || r === 'urgent' ? { text: String(r), role: 'error' } : { text: String(r), role: 'prose' });
|
|
110
|
+
|
|
111
|
+
if (cols < NARROW_COLS) { // one line: no frame, nothing lost that matters
|
|
112
|
+
const head = [{ text: '🔐 ', role: 'gold' }];
|
|
113
|
+
if (status) return [[...head, ...status]];
|
|
114
|
+
return [[...head, ...pinSeg, { text: ' · Esc cancels', role: 'prose' }]];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const W = Math.min(CARD_W, cols);
|
|
118
|
+
const inner = W - 2;
|
|
119
|
+
const row = (segs, sh = 0) => { // │ + content centred + │; `sh` nudges it right (the shake)
|
|
120
|
+
const used = segW(segs);
|
|
121
|
+
const lp = Math.max(0, Math.floor((inner - used) / 2) + sh);
|
|
122
|
+
return [{ text: '│', role: 'gold' }, { text: ' '.repeat(lp), role: 'prose' }, ...segs, { text: ' '.repeat(Math.max(0, inner - used - lp)), role: 'prose' }, { text: '│', role: 'gold' }];
|
|
123
|
+
};
|
|
124
|
+
const blank = () => row([{ text: '', role: 'prose' }]);
|
|
125
|
+
const title = view.title || 'AUTHORISATION REQUIRED';
|
|
126
|
+
const topHead = `╭─ 🔐 ${title} `;
|
|
127
|
+
const top = [{ text: topHead + '─'.repeat(Math.max(1, W - 1 - cellWidth(topHead))) + '╮', role: 'gold' }];
|
|
128
|
+
const footText = phase === 'locked' || phase === 'agent' || phase === 'nopin' ? ' esc closes ' : ' a person must confirm this · Esc cancels ';
|
|
129
|
+
const foot = [{ text: '╰─' + footText + '─'.repeat(Math.max(1, W - 3 - cellWidth(footText))) + '╯', role: 'gold' }];
|
|
130
|
+
|
|
131
|
+
// The six PIN slots: three rows tall, heavy gold border once filled, a teal flash on the digit just typed,
|
|
132
|
+
// a teal sweep while verifying, green ticks when authorised, red when denied.
|
|
133
|
+
const slotRows = () => {
|
|
134
|
+
const rows = [[], [], []];
|
|
135
|
+
for (let i = 0; i < PIN_LEN; i++) {
|
|
136
|
+
const full = i < shown;
|
|
137
|
+
let role = full ? 'gold' : 'prose'; let mid = full ? '◉' : ' ';
|
|
138
|
+
if (phase === 'input' && i === pulse && full) role = 'key';
|
|
139
|
+
if (phase === 'verifying' && tick != null && i === tick % (PIN_LEN + 2)) role = 'key';
|
|
140
|
+
if (phase === 'ok') { role = 'ok'; mid = '✔'; }
|
|
141
|
+
if (phase === 'denied') { role = 'error'; mid = '✖'; }
|
|
142
|
+
const heavy = full || phase === 'denied';
|
|
143
|
+
const [t, m, b] = heavy ? ['┏━━━┓', `┃ ${mid} ┃`, '┗━━━┛'] : ['╭───╮', '│ │', '╰───╯'];
|
|
144
|
+
const gap = i < PIN_LEN - 1 ? ' ' : '';
|
|
145
|
+
rows[0].push({ text: t + gap, role }); rows[1].push({ text: m + gap, role }); rows[2].push({ text: b + gap, role });
|
|
146
|
+
}
|
|
147
|
+
return rows.map((r) => row(r, shift));
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const out = [top, blank()];
|
|
151
|
+
const innerW = inner - 4;
|
|
152
|
+
for (const l of wrapCells(subject.line1 || '', innerW)) out.push(row([{ text: l, role: 'prose' }]));
|
|
153
|
+
if (subject.raisedBy || subject.risk) {
|
|
154
|
+
const segs = [];
|
|
155
|
+
if (subject.raisedBy) segs.push({ text: `raised by ${subject.raisedBy}`, role: 'prose' });
|
|
156
|
+
if (subject.risk) segs.push({ text: subject.raisedBy ? ' · risk ' : 'risk ', role: 'prose' }, riskSeg(subject.risk));
|
|
157
|
+
if (segW(segs) <= innerW) out.push(row(segs));
|
|
158
|
+
else { // too wide for one row: raised-by wraps, risk gets its own row
|
|
159
|
+
if (subject.raisedBy) for (const l of wrapCells(`raised by ${subject.raisedBy}`, innerW)) out.push(row([{ text: l, role: 'prose' }]));
|
|
160
|
+
if (subject.risk) out.push(row([{ text: 'risk ', role: 'prose' }, riskSeg(subject.risk)]));
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
out.push(blank());
|
|
164
|
+
if (phase === 'input' || phase === 'denied' || phase === 'verifying') {
|
|
165
|
+
out.push(...slotRows());
|
|
166
|
+
out.push(row([...pinSeg], shift));
|
|
167
|
+
out.push(blank());
|
|
168
|
+
}
|
|
169
|
+
if (phase === 'ok') { out.push(...slotRows()); out.push(blank()); }
|
|
170
|
+
if (phase !== 'input') out.push(row(status, shift));
|
|
171
|
+
if (phase === 'ok') out.push(blank());
|
|
172
|
+
if (error) for (const l of wrapCells(error, innerW)) out.push(row([{ text: l, role: 'error' }]));
|
|
173
|
+
out.push(foot);
|
|
174
|
+
return out;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* The controller (same shape as the /login and /connect controllers: isOpen / view / onEvent).
|
|
179
|
+
* deps.api { stepUpVerify(pin, {orgId, scopes}) } -> { step_up_token, expires_at, scopes }
|
|
180
|
+
* deps.agentLogin this window is an agent login: no PIN field, ever
|
|
181
|
+
* deps.refresh re-render
|
|
182
|
+
* deps.note (text, tone) -> a line to the transcript pane (never carries digits)
|
|
183
|
+
* deps.emit dispatch({ type: 'stepup.result', ok }) (sound cue; carries no digits)
|
|
184
|
+
*/
|
|
185
|
+
export function createStepUp({ api, agentLogin = false, onAgentLogin = () => {}, refresh = () => {}, note = () => {}, emit = () => {}, settingsUrl = () => null, now = () => Date.now(), sleep = (ms) => new Promise((r) => setTimeout(r, ms)) } = {}) {
|
|
186
|
+
const box = createPinBox();
|
|
187
|
+
let card = null;
|
|
188
|
+
let resolveCard = null;
|
|
189
|
+
let tries = MAX_TRIES;
|
|
190
|
+
let lockedUntil = 0;
|
|
191
|
+
let busy = false;
|
|
192
|
+
const tokens = new Map(); // scope -> { token, until }
|
|
193
|
+
let presentUntil = 0; // set only by /authenticate: the "human present" window shown in the status bar
|
|
194
|
+
|
|
195
|
+
const finish = (result) => { box.clear(); const r = resolveCard; card = null; resolveCard = null; busy = false; refresh(); r?.(result); };
|
|
196
|
+
const live = (scope) => { const t = tokens.get(scope); if (t && t.until > now()) return t; tokens.delete(scope); return null; };
|
|
197
|
+
|
|
198
|
+
async function submit() {
|
|
199
|
+
if (busy || !card) return;
|
|
200
|
+
try { await submitInner(); } finally { busy = false; }
|
|
201
|
+
}
|
|
202
|
+
async function submitInner() {
|
|
203
|
+
const pin = box.take(); // out of the box in one move; the local `pin` is dropped when this function returns
|
|
204
|
+
busy = true; card.phase = 'verifying'; card.filled = 0; refresh();
|
|
205
|
+
const t0 = now();
|
|
206
|
+
let res = null; let err = null;
|
|
207
|
+
try { res = await api.stepUpVerify(pin, { orgId: card.orgId, scopes: card.scopes }); }
|
|
208
|
+
catch (e) { err = e; }
|
|
209
|
+
const waited = now() - t0;
|
|
210
|
+
if (waited < BEAT_MS) await sleep(BEAT_MS - waited); // cosmetic floor only; a slow call is never padded
|
|
211
|
+
if (!card) return;
|
|
212
|
+
if (res?.step_up_token) {
|
|
213
|
+
const until = now() + WINDOW_MS;
|
|
214
|
+
for (const s of res.scopes || card.scopes) tokens.set(s, { token: res.step_up_token, until });
|
|
215
|
+
if (card.window) presentUntil = until;
|
|
216
|
+
card.phase = 'ok'; card.until = until; card.now = now(); refresh(); emit({ type: 'stepup.result', ok: true });
|
|
217
|
+
await sleep(RESULT_HOLD_MS);
|
|
218
|
+
return finish({ ok: true, token: res.step_up_token, scopes: res.scopes || card.scopes });
|
|
219
|
+
}
|
|
220
|
+
const status = err?.status; const code = err?.body?.error || err?.code;
|
|
221
|
+
if (status === 409 || code === 'no_pin_set') { card.phase = 'nopin'; Promise.resolve(settingsUrl()).then((u) => note(u ? `${NO_PIN_LINE} (${u})` : NO_PIN_LINE, 'warn')).catch(() => {}); refresh(); emit({ type: 'stepup.result', ok: false }); return; }
|
|
222
|
+
if (status === 423 || code === 'pin_locked') { lockedUntil = now() + LOCK_MS; card.phase = 'locked'; refresh(); emit({ type: 'stepup.result', ok: false }); return; }
|
|
223
|
+
if (status === 401 || code === 'wrong_pin') {
|
|
224
|
+
tries = Number.isFinite(err?.body?.tries_left) ? Math.min(tries - 1, err.body.tries_left) : tries - 1;
|
|
225
|
+
emit({ type: 'stepup.result', ok: false });
|
|
226
|
+
if (tries <= 0) { lockedUntil = now() + LOCK_MS; card.phase = 'locked'; refresh(); return; }
|
|
227
|
+
card.phase = 'denied'; card.tries = tries; card.offset = 1; refresh();
|
|
228
|
+
await sleep(80); if (!card) return; card.offset = 0; refresh();
|
|
229
|
+
await sleep(RESULT_HOLD_MS - 80); if (!card) return;
|
|
230
|
+
card.phase = 'input'; card.error = null; refresh();
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
if (status === 403 && (code === 'step_up_human_only')) { card.phase = 'agent'; refresh(); return; }
|
|
234
|
+
card.phase = 'input'; card.error = (err?.message || 'could not verify').replace(/\s+/g, ' ').slice(0, 80); refresh();
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return {
|
|
238
|
+
/** Live token for a scope, or null. */
|
|
239
|
+
token: (scope) => live(scope)?.token || null,
|
|
240
|
+
headers: (scope) => { const t = live(scope); return t ? { 'x-step-up-token': t.token } : {}; },
|
|
241
|
+
presentMs: () => Math.max(0, presentUntil - now()),
|
|
242
|
+
drop(scope) { tokens.delete(scope); },
|
|
243
|
+
clear() { tokens.clear(); presentUntil = 0; box.clear(); },
|
|
244
|
+
isOpen: () => Boolean(card),
|
|
245
|
+
view() { return card ? { ...card, filled: box.count(), tries, now: now() } : null; },
|
|
246
|
+
/**
|
|
247
|
+
* Ask for a person. Resolves { ok, token?, reason? } and never throws. Reuses a live token for the scope.
|
|
248
|
+
* opts: { scope: 'approve'|'override', scopes?, orgId?, subject?, window?: true (the /authenticate window) }
|
|
249
|
+
*/
|
|
250
|
+
request({ scope = 'approve', scopes = null, orgId = null, subject = {}, window: asWindow = false, title = null } = {}) {
|
|
251
|
+
if (typeof agentLogin === 'function' ? agentLogin() : agentLogin) { note(`This window is an agent login: ${NEEDS_PERSON}.`, 'warn'); onAgentLogin(); return Promise.resolve({ ok: false, reason: 'agent_login' }); }
|
|
252
|
+
if (!asWindow && live(scope)) return Promise.resolve({ ok: true, token: live(scope).token, scopes: [scope], reused: true });
|
|
253
|
+
if (card) return Promise.resolve({ ok: false, reason: 'busy' });
|
|
254
|
+
if (lockedUntil > now()) { note(`⛔ LOCKED · PIN entry is locked for ${mmss(lockedUntil - now())}. Use your phone or a human session.`, 'error'); return Promise.resolve({ ok: false, reason: 'locked' }); }
|
|
255
|
+
tries = MAX_TRIES;
|
|
256
|
+
box.clear();
|
|
257
|
+
card = { phase: 'input', subject, title, scope, scopes: scopes || (asWindow ? ['approve', 'override'] : [scope]), orgId, window: asWindow, error: null, offset: 0 };
|
|
258
|
+
refresh();
|
|
259
|
+
return new Promise((r) => { resolveCard = r; });
|
|
260
|
+
},
|
|
261
|
+
onEvent(e) {
|
|
262
|
+
if (!card) return false;
|
|
263
|
+
if (busy || card.phase === 'verifying' || card.phase === 'ok' || card.phase === 'denied') return true; // swallowed while the call is in flight and during the DENIED beat
|
|
264
|
+
const ph = card.phase;
|
|
265
|
+
if (ph === 'locked' || ph === 'nopin' || ph === 'agent') { if (e.type === 'key' && (e.name === 'escape' || e.name === 'enter' || e.name === 'ctrl-c')) finish({ ok: false, reason: ph }); return true; }
|
|
266
|
+
if (e.type === 'paste' || e.type === 'text') { box.push(e.text); if (box.count() >= PIN_LEN) submit(); else refresh(); return true; }
|
|
267
|
+
if (e.type !== 'key') return true;
|
|
268
|
+
if (e.name === 'escape' || e.name === 'ctrl-c') { finish({ ok: false, reason: 'cancelled' }); return true; }
|
|
269
|
+
if (e.name === 'backspace') { box.backspace(); refresh(); return true; }
|
|
270
|
+
if (e.name === 'kill-line' || e.name === 'delete-word') { box.clear(); refresh(); return true; }
|
|
271
|
+
return true; // every other key is swallowed: nothing falls through to the composer
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ink face of the step-up PIN card and the "human present" bar. All text comes from cardLines() (pure,
|
|
3
|
+
* counts only, never a digit); this file only paints roles. Roles: gold frame/title/filled dots/countdown,
|
|
4
|
+
* prose for ink, ok green, error red. Never grey.
|
|
5
|
+
*/
|
|
6
|
+
import React from 'react';
|
|
7
|
+
import { Box, Text } from 'ink';
|
|
8
|
+
import { c, mono } from '../theme.js';
|
|
9
|
+
import { cardLines, mmss, ANIM_MIN_COLS } from './stepUp.js';
|
|
10
|
+
|
|
11
|
+
const h = React.createElement;
|
|
12
|
+
|
|
13
|
+
/** Animate only on a roomy, coloured terminal that is keeping up. */
|
|
14
|
+
export const animationOn = (cols, slow = false) => cols >= ANIM_MIN_COLS && !slow && !mono() && process.env.TERM !== 'dumb';
|
|
15
|
+
const FRAME_MS = 90;
|
|
16
|
+
|
|
17
|
+
export function StepUpCard({ view, cols }) {
|
|
18
|
+
const [tick, setTick] = React.useState(0);
|
|
19
|
+
const [pulse, setPulse] = React.useState(-1);
|
|
20
|
+
const [slow, setSlow] = React.useState(false);
|
|
21
|
+
const on = animationOn(cols, slow);
|
|
22
|
+
const phase = view?.phase;
|
|
23
|
+
// verifying shimmer + sweep: one timer, only while verifying. A tick that lands far late means the terminal
|
|
24
|
+
// cannot keep up, so the card goes still for good rather than stutter.
|
|
25
|
+
React.useEffect(() => {
|
|
26
|
+
if (!on || phase !== 'verifying') return undefined;
|
|
27
|
+
let last = Date.now();
|
|
28
|
+
const t = setInterval(() => {
|
|
29
|
+
const n = Date.now(); if (n - last > FRAME_MS * 4) setSlow(true);
|
|
30
|
+
last = n; setTick((x) => x + 1);
|
|
31
|
+
}, FRAME_MS);
|
|
32
|
+
return () => clearInterval(t);
|
|
33
|
+
}, [on, phase === 'verifying']);
|
|
34
|
+
// per-digit pulse: flash the slot just filled for ~150 ms. Reads only the COUNT of digits, never a digit.
|
|
35
|
+
const filled = view?.filled || 0; const prev = React.useRef(0);
|
|
36
|
+
React.useEffect(() => {
|
|
37
|
+
const was = prev.current; prev.current = filled;
|
|
38
|
+
if (!on || filled <= was || phase !== 'input') { setPulse(-1); return undefined; }
|
|
39
|
+
setPulse(filled - 1);
|
|
40
|
+
const t = setTimeout(() => setPulse(-1), 150);
|
|
41
|
+
return () => clearTimeout(t);
|
|
42
|
+
}, [filled, on]);
|
|
43
|
+
const lines = cardLines(on ? { ...view, pulse, tick: phase === 'verifying' ? tick : null } : view, cols);
|
|
44
|
+
return h(Box, { flexDirection: 'column', width: cols, marginBottom: 1 }, // QUOXON-1191b: the spacer Box above supplies the one blank row above; this adds the one below
|
|
45
|
+
...lines.map((segs, i) => h(Text, { key: i, wrap: 'truncate-end' },
|
|
46
|
+
...segs.map((s, j) => h(Text, { key: j, color: c(s.role === 'prose' ? 'text' : s.role), bold: (s.role === 'gold' && j === 0 && i === 0) || s.role === 'key' || (s.role === 'ok' && s.text.startsWith('✔ AUTH')) }, s.text)))));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** One gold line above the status bar while the /authenticate window is live; ticks once a second. */
|
|
50
|
+
export function PresentBar({ stepUp, cols }) {
|
|
51
|
+
const [, tick] = React.useReducer((n) => n + 1, 0);
|
|
52
|
+
const left = stepUp.presentMs();
|
|
53
|
+
React.useEffect(() => {
|
|
54
|
+
if (!left) return undefined;
|
|
55
|
+
const t = setInterval(() => tick(), 1000);
|
|
56
|
+
return () => clearInterval(t);
|
|
57
|
+
}, [left > 0]);
|
|
58
|
+
if (!left) return null;
|
|
59
|
+
return h(Box, { width: cols }, h(Text, { wrap: 'truncate-end' }, ' ', h(Text, { color: c('gold'), bold: true }, '◉ human present '), h(Text, { color: c('gold') }, mmss(left)), h(Text, { color: c('text') }, ' · /authenticate renews')));
|
|
60
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QUOXON-STEPUP: /authenticate, /authorise and /override. Logic only; app.js wires the three entries.
|
|
3
|
+
*
|
|
4
|
+
* /authenticate PIN once: "human present" for 10 minutes (gold bar), so approvals and overrides
|
|
5
|
+
* in that window do not ask again
|
|
6
|
+
* /authorise <n> approve item n from /approvals with a PIN (prompts when no live step-up)
|
|
7
|
+
* /override <id|last> <reason> a person with a fresh PIN overrides ONE blocked action; recorded and
|
|
8
|
+
* witnessed server-side. Honest about block types no gate honours yet.
|
|
9
|
+
*/
|
|
10
|
+
import { mmss, NEEDS_PERSON } from './stepUp.js';
|
|
11
|
+
|
|
12
|
+
export const BLOCK_TYPES = ['safety', 'budget', 'aocl', 'ward'];
|
|
13
|
+
export const MIN_REASON = 8;
|
|
14
|
+
export const overrideUsage = '/override <block id|last> <reason> (a person must confirm with a PIN; the reason is recorded)';
|
|
15
|
+
|
|
16
|
+
/** "safety:org:agent:RED" -> "safety". Anything that does not start with a known block type -> null. */
|
|
17
|
+
export const blockTypeOf = (id) => { const t = String(id || '').split(':')[0].toLowerCase(); return BLOCK_TYPES.includes(t) ? t : null; };
|
|
18
|
+
|
|
19
|
+
/** -> { ok:true, blockId, reason } | { ok:false, error } */
|
|
20
|
+
export function parseOverride(args, last = null) {
|
|
21
|
+
const words = String(args || '').trim().split(/\s+/).filter(Boolean);
|
|
22
|
+
if (!words.length) return { ok: false, error: `Usage: ${overrideUsage}` };
|
|
23
|
+
let blockId = words[0];
|
|
24
|
+
if (blockId.toLowerCase() === 'last') {
|
|
25
|
+
if (!last?.blockId) return { ok: false, error: 'No policy block has stopped anything in this window yet. Pass the block id: /override <block id> <reason>' };
|
|
26
|
+
blockId = last.blockId;
|
|
27
|
+
}
|
|
28
|
+
const reason = words.slice(1).join(' ').trim();
|
|
29
|
+
if (reason.length < MIN_REASON) return { ok: false, error: `Give a reason (at least ${MIN_REASON} characters): it is recorded with your name. Usage: ${overrideUsage}` };
|
|
30
|
+
if (!blockTypeOf(blockId)) return { ok: false, error: `"${blockId}" is not a block id (expected ${BLOCK_TYPES.map((t) => `${t}:...`).join(', ')}). Use "last" for the most recent block.` };
|
|
31
|
+
return { ok: true, blockId, blockType: blockTypeOf(blockId), reason };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** What a failed step-up means to the person, in one line. */
|
|
35
|
+
export function declinedText(reason) {
|
|
36
|
+
switch (reason) {
|
|
37
|
+
case 'agent_login': return NEEDS_PERSON;
|
|
38
|
+
case 'cancelled': return 'PIN cancelled, nothing was sent';
|
|
39
|
+
case 'locked': return 'PIN entry is locked: use your phone or a human session';
|
|
40
|
+
case 'nopin': return 'no PIN set: set one in the dashboard (Settings > Account > Approval PIN)';
|
|
41
|
+
case 'busy': return 'a PIN prompt is already open';
|
|
42
|
+
default: return 'a person has not confirmed this';
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function createStepUpCmds({ api, stepUp, say, lastBlock = () => null, orgOf = () => null }) {
|
|
47
|
+
return {
|
|
48
|
+
async authenticate() {
|
|
49
|
+
const r = await stepUp.request({ window: true, orgId: orgOf(), subject: { line1: 'confirm a person is at this keyboard' } });
|
|
50
|
+
if (!r.ok) return say(`Not authenticated: ${declinedText(r.reason)}.`, 'warn');
|
|
51
|
+
return say(`✔ AUTHORISED · human present for ${mmss(stepUp.presentMs())}. Approvals and /override will not ask again in that window.`, 'ok');
|
|
52
|
+
},
|
|
53
|
+
/** One override. Returns the server answer or null. */
|
|
54
|
+
async override(args) {
|
|
55
|
+
const p = parseOverride(args, lastBlock());
|
|
56
|
+
if (!p.ok) return say(p.error, 'warn');
|
|
57
|
+
const lb = lastBlock();
|
|
58
|
+
const body = { blockId: p.blockId, blockType: p.blockType, reason: p.reason, agentId: lb?.blockId === p.blockId ? lb.agentId : null, action: lb?.blockId === p.blockId ? lb.text : null };
|
|
59
|
+
const subject = { line1: `override ${p.blockId}`, raisedBy: lb?.agentId ? `${lb.agentId} (blocked)` : null, risk: p.blockType === 'safety' ? 'high' : null };
|
|
60
|
+
const go = () => api.override({ ...body, stepUp: stepUp.headers('override') });
|
|
61
|
+
const ask = async () => {
|
|
62
|
+
const r = await stepUp.request({ scope: 'override', orgId: orgOf(), subject, title: 'OVERRIDE REQUIRES A PERSON' });
|
|
63
|
+
return r.ok ? null : declinedText(r.reason);
|
|
64
|
+
};
|
|
65
|
+
let res;
|
|
66
|
+
try {
|
|
67
|
+
if (!stepUp.token('override')) { const no = await ask(); if (no) return say(`Override not made: ${no}.`, 'warn'); }
|
|
68
|
+
try { res = await go(); }
|
|
69
|
+
catch (e) {
|
|
70
|
+
if (e?.status === 403 && e?.body?.error === 'step_up_required') { stepUp.drop('override'); const no = await ask(); if (no) return say(`Override not made: ${no}.`, 'warn'); res = await go(); }
|
|
71
|
+
else throw e;
|
|
72
|
+
}
|
|
73
|
+
} catch (e) { return say(`Override failed: ${e?.body?.message || e?.message || e}`, 'error'); }
|
|
74
|
+
say(`✔ Override ${res.override_id} recorded for ${p.blockId} by you, reason witnessed (AEE/WARD). ${res.note || ''}`.trim(), res.enforced ? 'ok' : 'warn');
|
|
75
|
+
return res;
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
}
|