quoxcli 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +2 -0
- package/bin/quox.js +99 -126
- package/lib/api/endpoints/actions.js +15 -0
- package/lib/api/endpoints/admin.js +46 -0
- package/lib/api/endpoints/agent-schedules.js +26 -0
- package/lib/api/endpoints/agents.js +7 -0
- package/lib/api/endpoints/assistants.js +12 -0
- package/lib/api/endpoints/away.js +15 -0
- package/lib/api/endpoints/blog.js +16 -0
- package/lib/api/endpoints/chat.js +14 -0
- package/lib/api/endpoints/contactsq.js +6 -0
- package/lib/api/endpoints/conversations.js +14 -0
- package/lib/api/endpoints/devServerRegistry.js +25 -0
- package/lib/api/endpoints/engagement.js +26 -0
- package/lib/api/endpoints/evidence.js +20 -0
- package/lib/api/endpoints/executions.js +13 -0
- package/lib/api/endpoints/fetchAllowlist.js +27 -0
- package/lib/api/endpoints/files.js +14 -0
- package/lib/api/endpoints/hat.js +44 -0
- package/lib/api/endpoints/inbox.js +20 -0
- package/lib/api/endpoints/index.js +26 -0
- package/lib/api/endpoints/loops.js +26 -0
- package/lib/api/endpoints/memory.js +19 -0
- package/lib/api/endpoints/mind.js +13 -0
- package/lib/api/endpoints/mon.js +179 -0
- package/lib/api/endpoints/objectives.js +12 -0
- package/lib/api/endpoints/plugins.js +18 -0
- package/lib/api/endpoints/presence.js +33 -0
- package/lib/api/endpoints/quoxplan.js +7 -0
- package/lib/api/endpoints/quoxseo.js +12 -0
- package/lib/api/endpoints/router.js +17 -0
- package/lib/api/endpoints/safety.js +7 -0
- package/lib/api/endpoints/server.js +21 -0
- package/lib/api/endpoints/setup.js +6 -0
- package/lib/api/endpoints/skills.js +20 -0
- package/lib/api/endpoints/stepup.js +20 -0
- package/lib/api/endpoints/teams.js +2 -2
- package/lib/api/endpoints/triggers.js +26 -0
- package/lib/api/endpoints/v1.js +120 -0
- package/lib/api/endpoints/ward.js +7 -0
- package/lib/api/endpoints/workflows.js +7 -0
- package/lib/api/endpoints/x.js +8 -0
- package/lib/commands/actions.js +131 -0
- package/lib/commands/admin.js +16 -0
- package/lib/commands/agent.js +170 -27
- package/lib/commands/agentAttach.js +140 -13
- package/lib/commands/approvals.js +78 -1
- package/lib/commands/aside.js +24 -0
- package/lib/commands/ask.js +146 -0
- package/lib/commands/assistant.js +59 -4
- package/lib/commands/auth.js +8 -2
- package/lib/commands/backup.js +8 -4
- package/lib/commands/blog.js +110 -0
- package/lib/commands/brain.js +105 -14
- package/lib/commands/build.js +19 -5
- package/lib/commands/chat.js +180 -26
- package/lib/commands/chatCompanion.js +27 -0
- package/lib/commands/chatState.js +45 -0
- package/lib/commands/claude.js +35 -38
- package/lib/commands/contacts.js +134 -4
- package/lib/commands/contactsSendAt.js +21 -0
- package/lib/commands/conversation.js +35 -4
- package/lib/commands/dashboard.js +43 -0
- package/lib/commands/department.js +28 -7
- package/lib/commands/devResident.js +15 -3
- package/lib/commands/devSupervisor.js +13 -1
- package/lib/commands/discord.js +75 -39
- package/lib/commands/done.js +98 -28
- package/lib/commands/engagement.js +107 -6
- package/lib/commands/evidence.js +154 -0
- package/lib/commands/ext.js +23 -0
- package/lib/commands/fetchAllowlist.js +71 -0
- package/lib/commands/file.js +38 -1
- package/lib/commands/fleet.js +336 -4
- package/lib/commands/governance.js +12 -3
- package/lib/commands/gripe.js +44 -13
- package/lib/commands/hat.js +254 -9
- package/lib/commands/helpers.js +18 -1
- package/lib/commands/hyperion.js +58 -0
- package/lib/commands/image.js +68 -0
- package/lib/commands/inbox.js +74 -23
- package/lib/commands/kuma.js +1050 -0
- package/lib/commands/kumaSla.js +174 -0
- package/lib/commands/licence.js +52 -155
- package/lib/commands/loop.js +291 -0
- package/lib/commands/lucid.js +10 -4
- package/lib/commands/memory.js +85 -8
- package/lib/commands/mind.js +71 -0
- package/lib/commands/mon.js +69 -2
- package/lib/commands/monitoring.js +1 -1
- package/lib/commands/motion.js +2 -2
- package/lib/commands/nucleus.js +48 -17
- package/lib/commands/objective.js +59 -1
- package/lib/commands/offload.js +89 -0
- package/lib/commands/onboard.js +2 -1
- package/lib/commands/org.js +30 -3
- package/lib/commands/peek.js +28 -0
- package/lib/commands/plan.js +38 -0
- package/lib/commands/plugin.js +110 -0
- package/lib/commands/presence.js +89 -0
- package/lib/commands/provider.js +217 -0
- package/lib/commands/qlarity.js +19 -3
- package/lib/commands/quoxchat.js +13 -4
- package/lib/commands/quoxseo.js +69 -0
- package/lib/commands/reach.js +2 -1
- package/lib/commands/research.js +79 -0
- package/lib/commands/router.js +77 -0
- package/lib/commands/rows.js +367 -0
- package/lib/commands/run.js +82 -10
- package/lib/commands/scope.js +2 -2
- package/lib/commands/server.js +405 -0
- package/lib/commands/setup.js +33 -7
- package/lib/commands/shortcut.js +41 -0
- package/lib/commands/skills.js +68 -9
- package/lib/commands/slot.js +260 -7
- package/lib/commands/status.js +1 -1
- package/lib/commands/team.js +3 -1
- package/lib/commands/tips.js +23 -0
- package/lib/commands/tool.js +133 -45
- package/lib/commands/trace.js +12 -7
- package/lib/commands/trigger.js +95 -0
- package/lib/commands/update.js +46 -0
- package/lib/commands/vault.js +31 -8
- package/lib/commands/vm.js +237 -0
- package/lib/commands/voice.js +34 -3
- package/lib/commands/volt.js +45 -3
- package/lib/commands/walkthrough.js +32 -0
- package/lib/commands/watch.js +73 -21
- package/lib/commands/welcome.js +14 -0
- package/lib/commands/workflow.js +189 -32
- package/lib/commands/x.js +13 -4
- package/lib/commands/youtube.js +172 -0
- package/lib/core/actingAs.js +2 -2
- package/lib/core/actionItems.js +115 -0
- package/lib/core/activeSummary.js +186 -0
- package/lib/core/agentAlias.js +2 -0
- package/lib/core/agentFamilies.js +7 -0
- package/lib/core/aside.js +54 -0
- package/lib/core/askDecision.js +60 -0
- package/lib/core/auth.js +164 -13
- package/lib/core/callerSource.js +1 -1
- package/lib/core/cliBuild.js +62 -0
- package/lib/core/client.js +152 -35
- package/lib/core/commandRegistrars.js +130 -0
- package/lib/core/config.js +143 -25
- package/lib/core/customHats.js +168 -0
- package/lib/core/doneEngineFetch.js +8 -0
- package/lib/core/errors.js +329 -20
- package/lib/core/findingReconcile.js +90 -0
- package/lib/core/global-flags.js +105 -5
- package/lib/core/governedSession.js +1 -3
- package/lib/core/hyperion.js +102 -0
- package/lib/core/inboxResolution.js +65 -0
- package/lib/core/lazyCommands.js +4 -1
- package/lib/core/liveSessionsReport.js +1 -1
- package/lib/core/loopGlyph.js +97 -0
- package/lib/core/offload.js +533 -0
- package/lib/core/org-override.js +1 -1
- package/lib/core/orgResolve.js +70 -0
- package/lib/core/output.js +11 -11
- package/lib/core/peek.js +126 -0
- package/lib/core/profileUsers.js +51 -0
- package/lib/core/promptCancel.js +18 -0
- package/lib/core/repoPath.js +11 -0
- package/lib/core/residentSeed.js +3 -1
- package/lib/core/rootOptions.js +39 -0
- package/lib/core/router.js +168 -0
- package/lib/core/selfUpdate.js +188 -0
- package/lib/core/sessionHeartbeat.js +12 -8
- package/lib/core/sessionsOverview.js +64 -0
- package/lib/core/shortcut.js +83 -0
- package/lib/core/slotKeeper.js +248 -3
- package/lib/core/slotWake.js +120 -0
- package/lib/core/supervisorEngine.js +71 -2
- package/lib/core/version.js +1 -1
- package/lib/core/vm.js +366 -0
- package/lib/core/vmTui.js +86 -0
- package/lib/core/walkthrough.js +35 -0
- package/lib/dragon/companionView.js +34 -0
- package/lib/dragon/frames.js +118 -0
- package/lib/dragon/jobs.js +123 -0
- package/lib/dragon/renderWorker.js +15 -0
- package/lib/dragon/select.js +72 -0
- package/lib/evidence/bundleExport.js +47 -0
- package/lib/evidence/bundleVerify.js +77 -0
- package/lib/hyperion/deck.js +29 -0
- package/lib/lucid/LucidCard.jsx +68 -0
- package/lib/lucid/LucidIntro.jsx +45 -58
- package/lib/lucid/cards.js +86 -0
- package/lib/lucid/markdown.js +81 -0
- package/lib/lucid/motion.js +56 -0
- package/lib/quoxon/EXTENSIONS.md +23 -0
- package/lib/quoxon/activity.js +14 -2
- package/lib/quoxon/aocl.js +117 -0
- package/lib/quoxon/api.js +690 -25
- package/lib/quoxon/app.js +3586 -250
- package/lib/quoxon/approvalFacts.js +96 -0
- package/lib/quoxon/approvalSentence.js +44 -0
- package/lib/quoxon/approvalsView.js +384 -0
- package/lib/quoxon/asideView.js +40 -0
- package/lib/quoxon/ask.js +121 -0
- package/lib/quoxon/away.js +52 -0
- package/lib/quoxon/bang.js +224 -0
- package/lib/quoxon/bulkRisk.js +17 -0
- package/lib/quoxon/capabilities.js +13 -6
- package/lib/quoxon/cells.js +53 -0
- package/lib/quoxon/commandShort.js +124 -0
- package/lib/quoxon/commands.js +429 -39
- package/lib/quoxon/connect.js +210 -23
- package/lib/quoxon/connectCard.js +19 -2
- package/lib/quoxon/consent.js +86 -0
- package/lib/quoxon/copy.js +4 -3
- package/lib/quoxon/countdownLine.js +20 -0
- package/lib/quoxon/deck.js +30 -0
- package/lib/quoxon/demo.autoplay.js +29 -0
- package/lib/quoxon/demo.fixtures.js +1 -1
- package/lib/quoxon/demo.js +46 -3
- package/lib/quoxon/devObserver.js +20 -0
- package/lib/quoxon/dividerAocl.js +192 -0
- package/lib/quoxon/dividerRule.js +57 -0
- package/lib/quoxon/dragonCompanion.js +255 -0
- package/lib/quoxon/dragonModes.js +151 -0
- package/lib/quoxon/dragonMoments.js +82 -0
- package/lib/quoxon/dragonReads.js +68 -0
- package/lib/quoxon/dragonView.js +41 -0
- package/lib/quoxon/earlyInput.js +40 -0
- package/lib/quoxon/echo.js +15 -0
- package/lib/quoxon/egg.js +2 -2
- package/lib/quoxon/events.js +266 -26
- package/lib/quoxon/extensions.js +276 -0
- package/lib/quoxon/feedback.js +50 -0
- package/lib/quoxon/gallery.js +21 -7
- package/lib/quoxon/handoffOffer.js +78 -0
- package/lib/quoxon/hatSwitch.js +37 -0
- package/lib/quoxon/headless.js +161 -0
- package/lib/quoxon/hostUrls.js +41 -0
- package/lib/quoxon/inboxView.js +79 -0
- package/lib/quoxon/indicator.js +6 -2
- package/lib/quoxon/installCli.js +79 -0
- package/lib/quoxon/intent.js +12 -1
- package/lib/quoxon/keys.js +51 -8
- package/lib/quoxon/launcher.js +103 -0
- package/lib/quoxon/loopCard.js +456 -0
- package/lib/quoxon/loopCards.js +180 -0
- package/lib/quoxon/loopStream.js +87 -0
- package/lib/quoxon/main.js +101 -160
- package/lib/quoxon/markdown.js +45 -21
- package/lib/quoxon/markdownSettle.js +19 -0
- package/lib/quoxon/matrixDomains.json +46 -0
- package/lib/quoxon/mentions.js +90 -0
- package/lib/quoxon/mind.js +103 -0
- package/lib/quoxon/mode.js +46 -0
- package/lib/quoxon/model.js +132 -26
- package/lib/quoxon/modelPrefs.js +345 -0
- package/lib/quoxon/motion/clock.js +3 -2
- package/lib/quoxon/motion/level.js +91 -0
- package/lib/quoxon/mouse.js +222 -0
- package/lib/quoxon/noai.js +57 -0
- package/lib/quoxon/objectiveOffer.js +82 -0
- package/lib/quoxon/objects/away.js +72 -0
- package/lib/quoxon/objects/fetch.js +17 -5
- package/lib/quoxon/objects/fleet.js +149 -0
- package/lib/quoxon/objects/health.js +186 -0
- package/lib/quoxon/objects/index.js +46 -7
- package/lib/quoxon/objects/mirror.js +137 -0
- package/lib/quoxon/objects/model.js +33 -0
- package/lib/quoxon/objects/peek.js +17 -0
- package/lib/quoxon/objects/proof.js +12 -4
- package/lib/quoxon/objects/prove.js +130 -0
- package/lib/quoxon/objects/replay.js +125 -0
- package/lib/quoxon/objects/sitrep.js +58 -0
- package/lib/quoxon/objects/view.js +11 -0
- package/lib/quoxon/onboarding/copy.js +32 -4
- package/lib/quoxon/onboarding/credential.js +17 -4
- package/lib/quoxon/onboarding/dockerOffer.js +45 -0
- package/lib/quoxon/onboarding/failureCause.js +33 -0
- package/lib/quoxon/onboarding/finish.js +15 -0
- package/lib/quoxon/onboarding/index.js +34 -7
- package/lib/quoxon/onboarding/installRunner.js +64 -5
- package/lib/quoxon/onboarding/preflight.js +17 -3
- package/lib/quoxon/onboarding/probe.js +65 -0
- package/lib/quoxon/onboarding/shortcutOffer.js +28 -0
- package/lib/quoxon/onboarding/view.js +76 -19
- package/lib/quoxon/orgLabel.js +10 -0
- package/lib/quoxon/orgSwitch.js +59 -0
- package/lib/quoxon/ownBroadcast.js +22 -0
- package/lib/quoxon/palette.js +43 -0
- package/lib/quoxon/planSteps.js +51 -0
- package/lib/quoxon/print.js +12 -5
- package/lib/quoxon/profileSwitch.js +53 -0
- package/lib/quoxon/promptHistory.js +41 -0
- package/lib/quoxon/providers.js +48 -0
- package/lib/quoxon/qart.js +20 -51
- package/lib/quoxon/qdragon.js +410 -0
- package/lib/quoxon/qdragonFly.js +62 -0
- package/lib/quoxon/qdragonScenes.js +282 -0
- package/lib/quoxon/qeyes.js +213 -0
- package/lib/quoxon/qmotion.js +75 -37
- package/lib/quoxon/render.js +740 -124
- package/lib/quoxon/resend.js +72 -0
- package/lib/quoxon/roster.js +117 -17
- package/lib/quoxon/route.js +6 -5
- package/lib/quoxon/rows.js +954 -0
- package/lib/quoxon/rowsStore.js +80 -0
- package/lib/quoxon/screenContext.js +150 -0
- package/lib/quoxon/screenFrames.js +254 -0
- package/lib/quoxon/serverChip.js +42 -0
- package/lib/quoxon/session.js +182 -2
- package/lib/quoxon/sessionDead.js +17 -0
- package/lib/quoxon/signin.js +125 -0
- package/lib/quoxon/slash/agents.js +112 -0
- package/lib/quoxon/slash/argSources.js +65 -0
- package/lib/quoxon/slash/args.js +65 -0
- package/lib/quoxon/slash/automate.js +89 -0
- package/lib/quoxon/slash/broadcast.js +140 -0
- package/lib/quoxon/slash/broadcastRun.js +108 -0
- package/lib/quoxon/slash/card.js +30 -0
- package/lib/quoxon/slash/challenge.js +114 -0
- package/lib/quoxon/slash/cliCommand.js +94 -0
- package/lib/quoxon/slash/cliLeaves.json +1023 -0
- package/lib/quoxon/slash/compact.js +1 -1
- package/lib/quoxon/slash/confirm.js +30 -0
- package/lib/quoxon/slash/crystallize.js +69 -0
- package/lib/quoxon/slash/dashboard.js +136 -0
- package/lib/quoxon/slash/draft.js +54 -0
- package/lib/quoxon/slash/evidenceCmd.js +34 -0
- package/lib/quoxon/slash/hatWalk.js +206 -0
- package/lib/quoxon/slash/inverses.js +77 -0
- package/lib/quoxon/slash/killer.js +24 -0
- package/lib/quoxon/slash/killerApi.js +16 -0
- package/lib/quoxon/slash/ledger.js +13 -8
- package/lib/quoxon/slash/listView.js +3 -1
- package/lib/quoxon/slash/modelCmd.js +261 -0
- package/lib/quoxon/slash/palette.js +22 -8
- package/lib/quoxon/slash/paletteController.js +117 -7
- package/lib/quoxon/slash/panels.js +21 -6
- package/lib/quoxon/slash/picker.js +20 -2
- package/lib/quoxon/slash/qpeCmd.js +35 -0
- package/lib/quoxon/slash/restart.js +95 -13
- package/lib/quoxon/slash/resume.js +18 -1
- package/lib/quoxon/slash/rowCmd.js +157 -0
- package/lib/quoxon/slash/rowWalk.js +175 -0
- package/lib/quoxon/slash/settings.js +16 -6
- package/lib/quoxon/slash/spawn.js +177 -0
- package/lib/quoxon/slash/stepUp.js +274 -0
- package/lib/quoxon/slash/stepUpCard.js +60 -0
- package/lib/quoxon/slash/stepUpCmds.js +78 -0
- package/lib/quoxon/slash/todoCmd.js +122 -0
- package/lib/quoxon/slash/undoApi.js +38 -0
- package/lib/quoxon/slash/undoCmd.js +49 -0
- package/lib/quoxon/slash/undoWish.js +15 -0
- package/lib/quoxon/slash/watch.js +164 -0
- package/lib/quoxon/slash/wish.js +23 -0
- package/lib/quoxon/slash/wishCmd.js +32 -0
- package/lib/quoxon/slotChat.js +300 -0
- package/lib/quoxon/slotFormat.js +195 -0
- package/lib/quoxon/slotRun.js +131 -0
- package/lib/quoxon/sound/mapping.js +2 -1
- package/lib/quoxon/stray.js +22 -0
- package/lib/quoxon/suggestCommand.js +90 -0
- package/lib/quoxon/term.js +46 -11
- package/lib/quoxon/textBuffer.js +176 -2
- package/lib/quoxon/theme.js +58 -25
- package/lib/quoxon/tips.js +433 -0
- package/lib/quoxon/toolCards.js +30 -0
- package/lib/quoxon/toolError.js +41 -0
- package/lib/quoxon/transcript.js +42 -4
- package/lib/quoxon/web/qface.js +18 -10
- package/lib/quoxon/welcome.js +1 -1
- package/lib/quoxon/whatsnew.js +136 -0
- package/lib/quoxon/whatsnew.jsonl +134 -0
- package/lib/quoxon/windowOrg.js +123 -0
- package/lib/server/devServers.js +137 -0
- package/lib/server/diag.js +129 -0
- package/lib/server/doctor.js +71 -0
- package/lib/server/install.js +149 -0
- package/lib/server/pluginEnable.js +161 -0
- package/lib/server/skew.js +56 -0
- package/lib/server/snapshot.js +131 -0
- package/lib/server/status.js +128 -0
- package/lib/server/sys.js +65 -0
- package/lib/server/uninstall.js +63 -0
- package/lib/server/update.js +234 -0
- package/lib/toolOutputShape.js +315 -0
- package/lib/tui/views/HyperionDeck.jsx +79 -0
- package/lib/tui/views/LucidDeck.jsx +209 -217
- package/package.json +5 -1
- package/vendor/dragonlab/SOURCE +4 -0
- package/vendor/dragonlab/design/cipher/v1/cipher-master.svg +46 -0
- package/vendor/dragonlab/design/cipher/v2/character.mjs +49 -0
- package/vendor/dragonlab/design/cipher/v2/review.mjs +33 -0
- package/vendor/dragonlab/design/cipher/v2/watch.mjs +56 -0
- package/vendor/dragonlab/design/drago/v1/character.mjs +19 -0
- package/vendor/dragonlab/design/drago/v1/drago-master.svg +56 -0
- package/vendor/dragonlab/design/drago/v2/character.mjs +87 -0
- package/vendor/dragonlab/design/drago/v4/character.mjs +67 -0
- package/vendor/dragonlab/design/drago/v4/profile.mjs +35 -0
- package/vendor/dragonlab/design/drago/v4/review.mjs +33 -0
- package/vendor/dragonlab/design/drago/v4/watch.mjs +56 -0
- package/vendor/dragonlab/design/hyperion/v1/hyperion-master.svg +56 -0
- package/vendor/dragonlab/design/hyperion/v2/character.mjs +79 -0
- package/vendor/dragonlab/design/hyperion/v2/review.mjs +33 -0
- package/vendor/dragonlab/design/hyperion/v2/watch.mjs +56 -0
- package/vendor/dragonlab/design/lucid/recovery/arrival.mjs +22 -0
- package/vendor/dragonlab/design/lucid/recovery/character.mjs +106 -0
- package/vendor/dragonlab/design/lucid/recovery/layout.mjs +50 -0
- package/vendor/dragonlab/design/lucid/recovery/lucid-master.svg +78 -0
- package/vendor/dragonlab/design/lucid/recovery/motion.mjs +42 -0
- package/vendor/dragonlab/design/lucid/recovery/render.mjs +121 -0
- package/vendor/dragonlab/design/lucid/recovery/review.mjs +68 -0
- package/vendor/dragonlab/design/lucid/recovery/verify.mjs +84 -0
- package/vendor/dragonlab/design/lucid/recovery/watch.mjs +56 -0
- package/vendor/dragonlab/design/seer/v1/seer-master.svg +53 -0
- package/vendor/dragonlab/design/seer/v2/character.mjs +88 -0
- package/vendor/dragonlab/design/seer/v3/character.mjs +33 -0
- package/vendor/dragonlab/design/seer/v4/character.mjs +66 -0
- package/vendor/dragonlab/design/seer/v4/review.mjs +33 -0
- package/vendor/dragonlab/design/seer/v4/watch.mjs +56 -0
- package/vendor/dragonlab/design/stage3/companion-review.mjs +28 -0
- package/vendor/dragonlab/design/stage3/sentinel-art.js +39 -0
- package/vendor/dragonlab/design/stage3/sentinel-braille.js +216 -0
- package/vendor/dragonlab/design/stage3/sentinel-character.js +257 -0
- package/vendor/dragonlab/design/stage3/sentinel-icon.js +103 -0
- package/vendor/dragonlab/design/stage3/sentinel-warden-eyes.js +95 -0
- package/vendor/dragonlab/design/stage3/svg-coverage.js +148 -0
- package/vendor/dragonlab/factory/svg-rig.mjs +63 -0
- package/vendor/dragonlab/integration/companions-v1/characters.mjs +88 -0
- package/vendor/dragonlab/integration/companions-v1/cipher.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/companion.mjs +63 -0
- package/vendor/dragonlab/integration/companions-v1/drago.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/hyperion.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/lucid.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/seer.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/sentinel.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/view.mjs +28 -0
- package/vendor/dragonlab/integration/hyperion-v3/activity.mjs +76 -0
- package/vendor/dragonlab/lib/power-effects.js +294 -0
- package/vendor/dragonlab/lib/power-registry.js +74 -0
- package/vendor/dragonlab/vendor/quox-dragon/qart.js +348 -0
- package/vendor/dragonlab/vendor/quox-dragon/qeyes.js +213 -0
- package/lib/core/websiteSession.js +0 -148
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QUOXON-MESH: slot chat in the quox TUI. Owner, 2026-10-14: "it would be cool if session to session messaging
|
|
3
|
+
* was faster, more responsive... instant... like chat". A window holding a slot listens on the collector's slot
|
|
4
|
+
* stream (GET /api/v1/slots/:name/stream, SSE, pushed from the same notify bus as `quox slot wait`) and renders
|
|
5
|
+
* each message as a chat line above the composer. `@slot text`, `/say` and `/send` post without a model turn.
|
|
6
|
+
*
|
|
7
|
+
* Pure helpers and the listener live here; app.js wires them.
|
|
8
|
+
*/
|
|
9
|
+
import { readFileSync, writeFileSync, mkdirSync, statSync, readdirSync, renameSync, existsSync } from 'node:fs';
|
|
10
|
+
import { homedir } from 'node:os';
|
|
11
|
+
import { join, dirname, basename, resolve, isAbsolute } from 'node:path';
|
|
12
|
+
import { scrub } from './transcript.js';
|
|
13
|
+
import { looksLikeKey } from './connect.js';
|
|
14
|
+
import { addressLabel } from './session.js';
|
|
15
|
+
|
|
16
|
+
// ---- outbound ------------------------------------------------------------------------------------
|
|
17
|
+
const AT_RE = /^@([A-Za-z0-9][A-Za-z0-9-]{0,31}):?[ \t]+(\S[\s\S]*)$/;
|
|
18
|
+
// The server's own leading-address forms (devSlotMessagesStore parseAddressedTo) stay message text.
|
|
19
|
+
const RESERVED = /^(all|S\d+|M\d+)$/i;
|
|
20
|
+
|
|
21
|
+
/** `@quox2 hello` -> { slot: 'quox2', text: 'hello' }; anything else -> null. */
|
|
22
|
+
export function parseAtSend(text) {
|
|
23
|
+
const m = AT_RE.exec(String(text || ''));
|
|
24
|
+
if (!m || RESERVED.test(m[1])) return null;
|
|
25
|
+
const body = m[2].replace(/\s+$/, '');
|
|
26
|
+
return body ? { slot: m[1].toLowerCase(), text: body } : null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const labelSafe = (s) => String(s || '').replace(/[^A-Za-z0-9._@:\- ]/g, '').replace(/\s+/g, ' ').trim().slice(0, 24);
|
|
30
|
+
|
|
31
|
+
/** What a window signs its lines as: "quox1 (Adam)", the slot alone, the name alone, or null (server derives). */
|
|
32
|
+
export function senderLabel({ slot, display }) {
|
|
33
|
+
const d = labelSafe(display);
|
|
34
|
+
if (slot) return d ? `${slot} (${d})` : slot;
|
|
35
|
+
return d || null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ---- files ---------------------------------------------------------------------------------------
|
|
39
|
+
export const FILE_CAP = 64 * 1024;
|
|
40
|
+
const FILE_HEAD = '📎 ';
|
|
41
|
+
const FENCE = '~~~~~~'; // a fence a pasted Markdown file is very unlikely to contain
|
|
42
|
+
|
|
43
|
+
/** The wire form of a file: one header line, then the body inside a long tilde fence (renders as a block in Discord/Matrix too). */
|
|
44
|
+
export function fileMessage({ name, body, size }) {
|
|
45
|
+
const lines = String(body).split('\n').length;
|
|
46
|
+
return `${FILE_HEAD}${basename(String(name))} (${fmtSize(size ?? Buffer.byteLength(String(body)))}, ${lines} line${lines === 1 ? '' : 's'})\n${FENCE}\n${body}\n${FENCE}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** -> { name, meta, body } for a file message, else null. */
|
|
50
|
+
export function parseFileMessage(text) {
|
|
51
|
+
const t = String(text || '');
|
|
52
|
+
if (!t.startsWith(FILE_HEAD)) return null;
|
|
53
|
+
const m = /^📎 (.+?) \(([^)\n]*)\)\n~~~~~~\n([\s\S]*)\n~~~~~~$/.exec(t);
|
|
54
|
+
return m ? { name: m[1], meta: m[2], body: m[3] } : null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function fmtSize(n) { return n < 1024 ? `${n} B` : `${(n / 1024).toFixed(n < 10240 ? 1 : 0)} KB`; }
|
|
58
|
+
|
|
59
|
+
const EXTRA_SECRETS = [/\bgsk_[A-Za-z0-9]{20,}/g, /\bhf_[A-Za-z0-9]{20,}/g, /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g];
|
|
60
|
+
|
|
61
|
+
/** The CLI redactor (transcript.js scrub) plus the paste guard's key shapes. -> { text, hits }. */
|
|
62
|
+
export function redactSecrets(text) {
|
|
63
|
+
const src = String(text);
|
|
64
|
+
let out = scrub(src);
|
|
65
|
+
for (const re of EXTRA_SECRETS) out = out.replace(re, '[redacted-key]');
|
|
66
|
+
const count = (s) => (s.match(/\[redacted[^\]]*\]/g) || []).length;
|
|
67
|
+
let hits = count(out) - count(src);
|
|
68
|
+
if (!hits && looksLikeKey(src)) hits = 1; // a key shape the patterns above missed: never call it clean
|
|
69
|
+
return { text: out, hits };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Read a local file for /send. Text up to FILE_CAP goes inline; larger or binary files are refused with the cap
|
|
74
|
+
* (slots have no attachment store). -> { ok, name, path, body, redacted, hits, size } | { ok:false, message }.
|
|
75
|
+
*/
|
|
76
|
+
export function prepareFile(p, { cwd = process.cwd() } = {}) {
|
|
77
|
+
const raw = String(p || '').trim().replace(/^['"]|['"]$/g, '');
|
|
78
|
+
if (!raw) return { ok: false, message: 'Usage: /send <slot> <file>' };
|
|
79
|
+
const full = isAbsolute(raw) ? raw : resolve(cwd, raw.replace(/^~(?=\/)/, homedir()));
|
|
80
|
+
let st;
|
|
81
|
+
try { st = statSync(full); } catch { return { ok: false, message: `No such file: ${raw}` }; }
|
|
82
|
+
if (st.isDirectory()) return { ok: false, message: `${raw} is a directory; /send takes one file.` };
|
|
83
|
+
if (st.size > FILE_CAP) return { ok: false, message: `${raw} is ${fmtSize(st.size)}; /send sends text files up to 64 KB inline (slots have no attachment store yet).` };
|
|
84
|
+
const buf = readFileSync(full);
|
|
85
|
+
if (buf.subarray(0, 8192).includes(0)) return { ok: false, message: `${raw} looks binary; /send sends text files only (up to 64 KB).` };
|
|
86
|
+
const body = buf.toString('utf8');
|
|
87
|
+
const { text, hits } = redactSecrets(body);
|
|
88
|
+
return { ok: true, name: basename(full), path: full, body, redacted: text, hits, size: st.size };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Tab completion for /send's path argument: entries of the typed directory, directories with a trailing slash. */
|
|
92
|
+
export function pathItems(partial, { cwd = process.cwd(), max = 200 } = {}) {
|
|
93
|
+
const p = String(partial || '');
|
|
94
|
+
const slash = p.lastIndexOf('/');
|
|
95
|
+
const dirPart = slash >= 0 ? p.slice(0, slash + 1) : '';
|
|
96
|
+
const base = dirPart.startsWith('~/') ? join(homedir(), dirPart.slice(2)) : isAbsolute(dirPart) ? dirPart : resolve(cwd, dirPart || '.');
|
|
97
|
+
let names = [];
|
|
98
|
+
try { names = readdirSync(base, { withFileTypes: true }); } catch { return []; }
|
|
99
|
+
const showHidden = p.slice(slash + 1).startsWith('.');
|
|
100
|
+
return names.filter(d => showHidden || !d.name.startsWith('.')).slice(0, max)
|
|
101
|
+
.map(d => ({ value: dirPart + d.name + (d.isDirectory() ? '/' : ''), desc: d.isDirectory() ? 'dir' : '' }));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ---- inbound -------------------------------------------------------------------------------------
|
|
105
|
+
const hhmm = (iso) => { const d = new Date(iso || Date.now()); return Number.isNaN(d.getTime()) ? '' : `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`; };
|
|
106
|
+
|
|
107
|
+
/** Slot text arrives HTML-escaped from the Matrix bridge ("&&"); decode for display. Never renders HTML. */
|
|
108
|
+
export function decodeEntities(t) {
|
|
109
|
+
return String(t ?? '').replace(/&(amp|lt|gt|quot|apos|#39|#x27);/gi, (_, e) => ({ amp: '&', lt: '<', gt: '>', quot: '"', apos: "'", '#39': "'", '#x27': "'" })[e.toLowerCase()]);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** The slot a session name currently holds (from api.slots() rows), or null. Lets a broadcast show its reply address. */
|
|
113
|
+
export function slotOfHolder(rows, name) {
|
|
114
|
+
const n = String(name || '').replace(/\s*\(.*\)\s*$/, '').trim().toLowerCase(); if (!n) return null;
|
|
115
|
+
const r = (Array.isArray(rows) ? rows : []).find(x => x?.name && x.state !== 'free' && String(x.holder_session || '').toLowerCase() === n);
|
|
116
|
+
return r ? r.name : null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** A slot message as a chat line: { glyph, who, time, text, file }. */
|
|
120
|
+
export function slotLine(m) {
|
|
121
|
+
m = m && typeof m.text === 'string' ? { ...m, text: decodeEntities(m.text) } : m;
|
|
122
|
+
const f = parseFileMessage(m?.text);
|
|
123
|
+
const file = f ? { name: f.name, meta: f.meta, body: f.body, lines: f.body.split('\n').length, preview: f.body.split('\n').slice(0, 3) } : null;
|
|
124
|
+
// SLOTS-REPLY: the slot is the address, so it leads; the session/display name follows in brackets. A sender
|
|
125
|
+
// with no verified slot keeps its name and is flagged as having no reply address.
|
|
126
|
+
const from = m?.from || m?.author_identity || 'someone';
|
|
127
|
+
const who = m?.from_slot ? addressLabel(m.from_slot, m?.from) : from;
|
|
128
|
+
return { glyph: '◆', who, noReply: !m?.from_slot, time: hhmm(m?.created_at), text: file ? `📎 ${f.name} (${f.meta})` : String(m?.text ?? ''), file };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Keep a slot's stream open: reconnect with the cursor, count failures distinctly, back off on failures (capped),
|
|
133
|
+
* and fall back to the wait long-poll on a server without the stream route. Never polls while the stream is up.
|
|
134
|
+
* open(query, signal) -> async iterable of SSE events; wait({ since, signal }) -> { messages, cursor }.
|
|
135
|
+
*/
|
|
136
|
+
// A slot cursor names a message ('<created_at>|<seq>'), not just a time: resuming from a bare timestamp skipped a
|
|
137
|
+
// second message in the same millisecond (collector devSlotMessagesStore parseCursor/cursorOf/isAfterCursor).
|
|
138
|
+
export function slotCursorOf(m) { return m.seq == null ? m.created_at : `${m.created_at}|${m.seq}`; }
|
|
139
|
+
export function isAfterSlotCursor(m, cursor) {
|
|
140
|
+
if (!cursor) return true;
|
|
141
|
+
const x = /^(.*)\|(\d+)$/.exec(String(cursor));
|
|
142
|
+
const ts = x ? (x[1] || null) : String(cursor); const seq = x ? Number(x[2]) : null;
|
|
143
|
+
if (seq != null && !ts) return m.seq != null && m.seq > seq;
|
|
144
|
+
if (m.created_at !== ts) return m.created_at > ts;
|
|
145
|
+
return seq == null ? true : (m.seq != null && m.seq > seq);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function createSlotListener({ open, wait = null, onMessage, onState = () => {}, delays = [1000, 2000, 5000, 10000, 30000] }) {
|
|
149
|
+
const st = { running: false, failures: 0, mode: 'stream', cursor: null, connected: false };
|
|
150
|
+
let ac = null;
|
|
151
|
+
const seen = new Set();
|
|
152
|
+
const deliver = (m) => {
|
|
153
|
+
if (!m) return;
|
|
154
|
+
if (m.created_at && isAfterSlotCursor(m, st.cursor)) st.cursor = slotCursorOf(m);
|
|
155
|
+
if (m.id) { if (seen.has(m.id)) return; seen.add(m.id); if (seen.size > 1000) seen.delete(seen.values().next().value); }
|
|
156
|
+
try { onMessage(m); } catch { /* a render failure never kills the listener */ }
|
|
157
|
+
};
|
|
158
|
+
const sleep = (ms, signal) => new Promise((r) => { const t = setTimeout(r, ms); t.unref?.(); signal.addEventListener('abort', () => { clearTimeout(t); r(); }, { once: true }); });
|
|
159
|
+
const setConn = (v) => { if (st.connected !== v) { st.connected = v; onState({ ...st }); } };
|
|
160
|
+
|
|
161
|
+
async function loop(signal, since) {
|
|
162
|
+
st.cursor = since || null;
|
|
163
|
+
let streak = 0;
|
|
164
|
+
while (!signal.aborted) {
|
|
165
|
+
try {
|
|
166
|
+
if (st.mode === 'stream') {
|
|
167
|
+
for await (const ev of open({ since: st.cursor }, signal)) {
|
|
168
|
+
if (signal.aborted) break;
|
|
169
|
+
if (ev?.type === 'connected') { streak = 0; if (ev.cursor && !st.cursor) st.cursor = ev.cursor; setConn(true); }
|
|
170
|
+
else if (ev?.type === 'slot.message') { streak = 0; deliver(ev.message); }
|
|
171
|
+
}
|
|
172
|
+
setConn(false);
|
|
173
|
+
if (signal.aborted) break;
|
|
174
|
+
await sleep(delays[0], signal); // a clean end (lifetime) reconnects promptly
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
const r = await wait({ since: st.cursor, signal });
|
|
178
|
+
if (signal.aborted) break;
|
|
179
|
+
streak = 0; setConn(true);
|
|
180
|
+
for (const m of r?.messages || []) deliver(m);
|
|
181
|
+
if (r?.cursor) st.cursor = r.cursor;
|
|
182
|
+
} catch (e) {
|
|
183
|
+
if (signal.aborted) break;
|
|
184
|
+
setConn(false);
|
|
185
|
+
if (st.mode === 'stream' && wait && e?.status === 404 && e?.code !== 'unknown slot') { st.mode = 'wait'; onState({ ...st }); continue; }
|
|
186
|
+
st.failures += 1; streak += 1;
|
|
187
|
+
onState({ ...st, error: e?.message || 'failed' });
|
|
188
|
+
await sleep(delays[Math.min(streak - 1, delays.length - 1)], signal);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return {
|
|
193
|
+
start({ since } = {}) {
|
|
194
|
+
if (st.running) return;
|
|
195
|
+
st.running = true; ac = new AbortController();
|
|
196
|
+
const sig = ac.signal;
|
|
197
|
+
loop(sig, since).finally(() => { st.running = false; st.connected = false; });
|
|
198
|
+
},
|
|
199
|
+
stop() { if (ac) ac.abort(); ac = null; st.running = false; st.connected = false; },
|
|
200
|
+
state: () => ({ ...st }),
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** SLOTS-REPLY2: is this line in slot X from X's own holder (verified from_slot, else the sender name)? */
|
|
205
|
+
export function isHolderLine(m, slot) {
|
|
206
|
+
const x = String(slot || '').toLowerCase(); if (!x) return false;
|
|
207
|
+
if (String(m?.from_slot || '').toLowerCase() === x) return true;
|
|
208
|
+
const f = String(m?.from || '').toLowerCase();
|
|
209
|
+
return f === x || f.startsWith(`${x} (`);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* SLOTS-REPLY2: after this window sends into slot X it also watches X (not just its own slot) for lines from
|
|
214
|
+
* X's holder, from the sent message's cursor on, until `ttlMs` after the last send or stop(). A reply posted
|
|
215
|
+
* in X (a sender with no reply address) is then still shown. onReply(slot, message) fires once per line.
|
|
216
|
+
* open/wait are api.slotStream / api.slotWait shaped, bound per slot by the caller: open(slot, q, signal).
|
|
217
|
+
*/
|
|
218
|
+
export function createReplyWatches({ open, wait = null, onReply, ttlMs = 30 * 60 * 1000, now = Date.now, isMine = () => false, skipSlot = () => false, setTimer = setTimeout, clearTimer = clearTimeout }) {
|
|
219
|
+
const watches = new Map();
|
|
220
|
+
const drop = (slot) => { const w = watches.get(slot); if (!w) return; clearTimer(w.timer); w.l.stop(); watches.delete(slot); };
|
|
221
|
+
const arm = (slot, sent) => {
|
|
222
|
+
if (!slot || skipSlot(slot)) return false;
|
|
223
|
+
const until = now() + ttlMs;
|
|
224
|
+
const ex = watches.get(slot);
|
|
225
|
+
if (ex) { clearTimer(ex.timer); ex.timer = setTimer(() => drop(slot), ttlMs); ex.timer.unref?.(); ex.until = until; return true; }
|
|
226
|
+
const w = { until, timer: null, l: null };
|
|
227
|
+
w.l = createSlotListener({
|
|
228
|
+
open: (q, signal) => open(slot, q, signal),
|
|
229
|
+
wait: wait ? ({ since, signal }) => wait(slot, { since, signal }) : null,
|
|
230
|
+
onMessage: (m) => {
|
|
231
|
+
if (now() > w.until) { drop(slot); return; }
|
|
232
|
+
if (isMine(m) || !isHolderLine(m, slot)) return;
|
|
233
|
+
onReply(slot, m);
|
|
234
|
+
},
|
|
235
|
+
});
|
|
236
|
+
w.timer = setTimer(() => drop(slot), ttlMs); w.timer.unref?.();
|
|
237
|
+
watches.set(slot, w);
|
|
238
|
+
w.l.start({ since: sent && sent.created_at ? slotCursorOf(sent) : undefined });
|
|
239
|
+
return true;
|
|
240
|
+
};
|
|
241
|
+
return { arm, stop: () => { for (const s of [...watches.keys()]) drop(s); }, has: (s) => watches.has(s), size: () => watches.size };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// ---- reattach record -------------------------------------------------------------------------------
|
|
245
|
+
export const lastSlotPath = () => join(homedir(), '.config', 'quox', 'quoxon-slots.json');
|
|
246
|
+
const readAll = (path) => { try { return existsSync(path) ? JSON.parse(readFileSync(path, 'utf8')) || {} : {}; } catch { return {}; } };
|
|
247
|
+
const keyOf = (profile, cwd) => `${profile || 'default'}|${cwd || ''}`;
|
|
248
|
+
function writeAll(all, path) {
|
|
249
|
+
try { mkdirSync(dirname(path), { recursive: true }); const tmp = `${path}.${process.pid}.tmp`; writeFileSync(tmp, JSON.stringify(all, null, 2)); renameSync(tmp, path); } catch { /* best effort */ }
|
|
250
|
+
}
|
|
251
|
+
/** The slot this profile last held in this directory, or null. */
|
|
252
|
+
export function loadLastSlot({ profile, cwd, path = lastSlotPath() }) {
|
|
253
|
+
const e = readAll(path)[keyOf(profile, cwd)];
|
|
254
|
+
return e && typeof e.slot === 'string' ? e : null;
|
|
255
|
+
}
|
|
256
|
+
export function saveLastSlot({ profile, cwd, slot, session, path = lastSlotPath() }) {
|
|
257
|
+
const all = readAll(path); all[keyOf(profile, cwd)] = { slot, session: session || null, at: new Date().toISOString() }; writeAll(all, path);
|
|
258
|
+
}
|
|
259
|
+
export function forgetLastSlot({ profile, cwd, path = lastSlotPath() }) {
|
|
260
|
+
const all = readAll(path); const k = keyOf(profile, cwd);
|
|
261
|
+
if (k in all) { delete all[k]; writeAll(all, path); }
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// ---- QUOXON-ROOMS: a slot as a group room ----------------------------------------------------------
|
|
265
|
+
const ROOM_RE = /^#([A-Za-z0-9][A-Za-z0-9-]{0,31}):?(?:[ \t]+(\S[\s\S]*))?$/;
|
|
266
|
+
/** `#group-chat hello` -> { room, text }; bare `#group-chat` -> { room, text: null }; `# heading` or anything else -> null. */
|
|
267
|
+
export function parseRoomSend(text) {
|
|
268
|
+
const m = ROOM_RE.exec(String(text || '').replace(/\s+$/, ''));
|
|
269
|
+
return m ? { room: m[1].toLowerCase(), text: m[2] ? m[2] : null } : null;
|
|
270
|
+
}
|
|
271
|
+
/** `#group-chat` / `group-chat` -> 'group-chat'; junk -> null. */
|
|
272
|
+
export function roomName(arg) {
|
|
273
|
+
const m = /^#?([A-Za-z0-9][A-Za-z0-9-]{0,31})$/.exec(String(arg || '').trim());
|
|
274
|
+
return m ? m[1].toLowerCase() : null;
|
|
275
|
+
}
|
|
276
|
+
/** "QUOXON (resident)" -> "QUOXON": the sender as a chat name. */
|
|
277
|
+
export function senderName(from) { return String(from || 'someone').replace(/\s*\(.*$/, '').trim() || 'someone'; }
|
|
278
|
+
const SENDER_ROLES = ['sky', 'teal', 'ok', 'warn', 'indigo', 'violet'];
|
|
279
|
+
/** A stable colour role per sender (same name, same colour, every time and every window). */
|
|
280
|
+
export function senderColor(name) {
|
|
281
|
+
let h = 0; for (const ch of String(name)) h = (h * 31 + ch.codePointAt(0)) >>> 0;
|
|
282
|
+
return SENDER_ROLES[h % SENDER_ROLES.length];
|
|
283
|
+
}
|
|
284
|
+
/** A room message as a chat line: `#group-chat QUOXON: text`. */
|
|
285
|
+
export function roomLine(room, m, { out = false } = {}) {
|
|
286
|
+
const base = slotLine(m);
|
|
287
|
+
const who = senderName(m?.from);
|
|
288
|
+
return { ...base, glyph: '#', room, who, color: senderColor(who), text: base.text, out, roomLine: true };
|
|
289
|
+
}
|
|
290
|
+
/** The text form of a room line, for /rooms backlog tests and plain output. */
|
|
291
|
+
export const roomLineText = (l) => `#${l.room} ${l.who}: ${l.text}`;
|
|
292
|
+
|
|
293
|
+
/** QUOXON-MESH: what a send to a slot tells the user about the recipient. null when the server sent no presence. */
|
|
294
|
+
export function presenceLine(slot, presence, agoText) {
|
|
295
|
+
if (!presence || !presence.state) return null;
|
|
296
|
+
if (presence.state === 'listening') return { text: `✔ delivered to ${slot} (listening now)`, tone: 'ok', listening: true };
|
|
297
|
+
if (presence.state === 'nobody_holds') return { text: `✔ delivered to ${slot} · nobody holds this slot`, tone: 'ok', listening: false };
|
|
298
|
+
const seen = presence.last_checked_in_at ? `last checked in ${agoText(presence.last_checked_in_at)}` : 'no check-in on record';
|
|
299
|
+
return { text: `✔ delivered to ${slot} · not listening right now (${seen}); it sees this when it next checks`, tone: 'ok', listening: false };
|
|
300
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SLOT-RUN A (owner 2026-10-16, about a slot message that arrived as a wrapped wall of text around a long
|
|
3
|
+
* shell command: "messy needs a format, structure, styling"). Incoming slot message bodies render light
|
|
4
|
+
* markdown: paragraphs, - bullets, **bold**, `inline code` and fenced blocks.
|
|
5
|
+
*
|
|
6
|
+
* Code is never wrapped mid-token. A long inline command is lifted into its own block; a block line that does
|
|
7
|
+
* not fit breaks ONLY at unquoted whitespace and ends in " \" (shell line continuation), so a copied block is
|
|
8
|
+
* still the same command. A single token longer than the row hard-breaks with a visible " ↩".
|
|
9
|
+
*
|
|
10
|
+
* Pure layout: rows of segments { t, role, bold? }. render.js paints them; no grey roles (owner rule).
|
|
11
|
+
*/
|
|
12
|
+
import { graphemes } from './textBuffer.js';
|
|
13
|
+
|
|
14
|
+
export const CONT = ' \\';
|
|
15
|
+
export const HARD = ' ↩';
|
|
16
|
+
export const GUTTER = '│ ';
|
|
17
|
+
export const LONG_INLINE = 32; // an inline code span longer than this gets its own block
|
|
18
|
+
|
|
19
|
+
const len = (s) => graphemes(String(s)).length;
|
|
20
|
+
|
|
21
|
+
/** Where a line may break: unquoted, unescaped whitespace before any unquoted # comment. */
|
|
22
|
+
function safeBreaks(chars) {
|
|
23
|
+
const safe = new Array(chars.length).fill(false);
|
|
24
|
+
let q = null; let comment = false;
|
|
25
|
+
for (let i = 0; i < chars.length; i++) {
|
|
26
|
+
const ch = chars[i];
|
|
27
|
+
if (comment) continue;
|
|
28
|
+
if (q === "'") { if (ch === "'") q = null; continue; }
|
|
29
|
+
if (q === '"') { if (ch === '\\') i++; else if (ch === '"') q = null; continue; }
|
|
30
|
+
if (ch === '\\') { i++; continue; }
|
|
31
|
+
if (ch === "'" || ch === '"') { q = ch; continue; }
|
|
32
|
+
if (ch === '#' && (i === 0 || /\s/.test(chars[i - 1]))) { comment = true; continue; }
|
|
33
|
+
if (/\s/.test(ch)) safe[i] = true;
|
|
34
|
+
}
|
|
35
|
+
return safe;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** One command line as rows of at most `width` columns. Continuation rows are indented two spaces. */
|
|
39
|
+
export function wrapShell(line, width) {
|
|
40
|
+
const w = Math.max(12, width);
|
|
41
|
+
const chars = [...String(line)];
|
|
42
|
+
if (chars.length <= w) return [chars.join('')];
|
|
43
|
+
const safe = safeBreaks(chars);
|
|
44
|
+
const out = [];
|
|
45
|
+
let start = 0;
|
|
46
|
+
for (;;) {
|
|
47
|
+
const indent = out.length ? ' ' : '';
|
|
48
|
+
const avail = w - indent.length;
|
|
49
|
+
if (chars.length - start <= avail) { out.push(indent + chars.slice(start).join('')); break; }
|
|
50
|
+
const lim = avail - CONT.length;
|
|
51
|
+
let cut = -1;
|
|
52
|
+
for (let i = start + lim; i > start; i--) if (safe[i]) { cut = i; break; }
|
|
53
|
+
if (cut > start) {
|
|
54
|
+
out.push(indent + chars.slice(start, cut).join('').replace(/\s+$/, '') + CONT);
|
|
55
|
+
start = cut;
|
|
56
|
+
while (start < chars.length && safe[start]) start++;
|
|
57
|
+
} else {
|
|
58
|
+
const n = avail - HARD.length;
|
|
59
|
+
out.push(indent + chars.slice(start, start + n).join('') + HARD);
|
|
60
|
+
start += n;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Inline runs of one line: plain text, **bold**, `code`. */
|
|
67
|
+
function inlineRuns(text) {
|
|
68
|
+
const out = [];
|
|
69
|
+
const re = /(`+)([^`]+?)\1|\*\*([^*]+?)\*\*/g;
|
|
70
|
+
let last = 0; let m;
|
|
71
|
+
const s = String(text);
|
|
72
|
+
while ((m = re.exec(s))) {
|
|
73
|
+
if (m.index > last) out.push({ kind: 'text', t: s.slice(last, m.index) });
|
|
74
|
+
if (m[1]) out.push({ kind: 'code', t: m[2].trim() });
|
|
75
|
+
else out.push({ kind: 'bold', t: m[3] });
|
|
76
|
+
last = m.index + m[0].length;
|
|
77
|
+
}
|
|
78
|
+
if (last < s.length) out.push({ kind: 'text', t: s.slice(last) });
|
|
79
|
+
return out;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Block structure: code (fenced, `$ ` lines, long inline spans), bullets, paragraphs, gaps. */
|
|
83
|
+
function blocks(text) {
|
|
84
|
+
const src = String(text ?? '').replace(/\r\n?/g, '\n').split('\n');
|
|
85
|
+
const out = [];
|
|
86
|
+
const push = (b) => {
|
|
87
|
+
if (b.type === 'gap' && (!out.length || out[out.length - 1].type === 'gap')) return;
|
|
88
|
+
out.push(b);
|
|
89
|
+
};
|
|
90
|
+
for (let i = 0; i < src.length; i++) {
|
|
91
|
+
const line = src[i];
|
|
92
|
+
const fence = /^\s{0,3}(`{3,}|~{3,})/.exec(line);
|
|
93
|
+
if (fence) {
|
|
94
|
+
const lines = [];
|
|
95
|
+
for (i++; i < src.length && !src[i].trim().startsWith(fence[1]); i++) lines.push(src[i]);
|
|
96
|
+
push({ type: 'code', lines });
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (/^\s*\$ \S/.test(line)) {
|
|
100
|
+
const prev = out[out.length - 1];
|
|
101
|
+
if (prev?.type === 'code' && prev.dollar) prev.lines.push(line.trim()); else push({ type: 'code', lines: [line.trim()], dollar: true });
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (!line.trim()) { push({ type: 'gap' }); continue; }
|
|
105
|
+
const bullet = /^\s*(?:[-*•]|(\d+[.)]))\s+(.*)$/.exec(line);
|
|
106
|
+
const heading = /^\s*#{1,6}\s+(.*)$/.exec(line);
|
|
107
|
+
const kind = bullet ? 'bullet' : 'para';
|
|
108
|
+
const body = bullet ? bullet[2] : heading ? `**${heading[1]}**` : line;
|
|
109
|
+
const mark = bullet ? (bullet[1] ? `${bullet[1]} ` : '• ') : '';
|
|
110
|
+
// A long inline command is lifted out into its own block between the prose around it.
|
|
111
|
+
let pending = [];
|
|
112
|
+
let first = true;
|
|
113
|
+
const flush = () => {
|
|
114
|
+
if (!pending.length) return;
|
|
115
|
+
if (pending.some(r => r.kind !== 'text' || r.t.trim())) push({ type: kind, runs: pending, mark: first ? mark : (mark ? ' ' : '') });
|
|
116
|
+
first = false; pending = [];
|
|
117
|
+
};
|
|
118
|
+
for (const r of inlineRuns(body)) {
|
|
119
|
+
if (r.kind === 'code' && len(r.t) > LONG_INLINE) { flush(); push({ type: 'code', lines: [r.t] }); first = false; }
|
|
120
|
+
else pending.push(r);
|
|
121
|
+
}
|
|
122
|
+
flush();
|
|
123
|
+
}
|
|
124
|
+
while (out.length && out[out.length - 1].type === 'gap') out.pop();
|
|
125
|
+
return out;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Word-wrap inline runs to `width`; code spans are unbreakable atoms (hard-split only if wider than the row). */
|
|
129
|
+
function wrapRuns(runs, width, role, prefix) {
|
|
130
|
+
const atoms = [];
|
|
131
|
+
for (const r of runs) {
|
|
132
|
+
if (r.kind === 'code') { atoms.push({ t: r.t, role: 'code', sp: /^\s/.test(r.t) }); continue; }
|
|
133
|
+
const parts = r.t.split(/(\s+)/);
|
|
134
|
+
for (const p of parts) {
|
|
135
|
+
if (!p) continue;
|
|
136
|
+
if (/^\s+$/.test(p)) { atoms.push({ space: true }); continue; }
|
|
137
|
+
atoms.push({ t: p, role, bold: r.kind === 'bold' || undefined });
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
const rows = [];
|
|
141
|
+
let cur = []; let w = 0; let gap = false;
|
|
142
|
+
const ind = len(prefix);
|
|
143
|
+
const room = Math.max(8, width - ind);
|
|
144
|
+
const add = (seg) => {
|
|
145
|
+
const last = cur[cur.length - 1];
|
|
146
|
+
if (last && last.role === seg.role && last.bold === seg.bold) last.t += seg.t; else cur.push({ ...seg });
|
|
147
|
+
};
|
|
148
|
+
const newRow = () => { rows.push(cur); cur = []; w = 0; };
|
|
149
|
+
for (const a of atoms) {
|
|
150
|
+
if (a.space) { gap = true; continue; }
|
|
151
|
+
const sep = cur.length && gap ? 1 : 0; // runs that touch (`a`**b**) stay touching
|
|
152
|
+
|
|
153
|
+
let t = a.t;
|
|
154
|
+
if (w + sep + len(t) > room && cur.length) newRow();
|
|
155
|
+
else if (sep) { add({ t: ' ', role, bold: undefined }); w += 1; }
|
|
156
|
+
while (len(t) > room) { const g = graphemes(t); add({ t: g.slice(0, room - w).join(''), role: a.role, bold: a.bold }); t = g.slice(room - w).join(''); newRow(); }
|
|
157
|
+
add({ t, role: a.role, bold: a.bold }); w += len(t);
|
|
158
|
+
gap = false;
|
|
159
|
+
}
|
|
160
|
+
if (cur.length) rows.push(cur);
|
|
161
|
+
return rows.map((r, i) => (prefix ? [{ t: i === 0 ? prefix : ' '.repeat(ind), role: 'bullet1' }, ...r] : r));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** A code block's rows: gutter, the code, and the continuation marker in the label colour. */
|
|
165
|
+
export function codeRows(lines, width) {
|
|
166
|
+
const rows = [];
|
|
167
|
+
for (const line of lines) {
|
|
168
|
+
for (const piece of wrapShell(line, Math.max(12, width - len(GUTTER)))) {
|
|
169
|
+
const mk = piece.endsWith(CONT) ? CONT : piece.endsWith(HARD) ? HARD : '';
|
|
170
|
+
const body = mk ? piece.slice(0, -mk.length) : piece;
|
|
171
|
+
rows.push([{ t: GUTTER, role: 'label' }, { t: body || ' ', role: 'code' }, ...(mk ? [{ t: mk, role: 'label' }] : [])]);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return rows;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** The body of a slot message as rows of segments, at most `width` columns each. */
|
|
178
|
+
export function formatSlotBody(text, width, { role = 'text' } = {}) {
|
|
179
|
+
const rows = [];
|
|
180
|
+
for (const b of blocks(text)) {
|
|
181
|
+
if (b.type === 'gap') rows.push([]);
|
|
182
|
+
else if (b.type === 'code') rows.push(...codeRows(b.lines, width));
|
|
183
|
+
else rows.push(...wrapRuns(b.runs, width, role, b.mark));
|
|
184
|
+
}
|
|
185
|
+
return rows;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Plain text of each row (tests, NO_COLOR). */
|
|
189
|
+
export const rowsText = (rows) => rows.map(r => r.map(s => s.t).join(''));
|
|
190
|
+
|
|
191
|
+
/** A short plain one-liner keeps the chat shape: on the header line after the time. */
|
|
192
|
+
export function inlineable(text, room) {
|
|
193
|
+
const t = String(text ?? '');
|
|
194
|
+
return !/[\n`]|\*\*/.test(t) && !/^\s*(?:[-*•]\s|\d+[.)]\s|\$ |#{1,6}\s)/.test(t) && len(t) <= room;
|
|
195
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SLOT-RUN B (owner 2026-10-16: "if its a request to run the command, let me just say yes"). A slot message may
|
|
3
|
+
* carry ONE command for the human to run here (`quox slot say <slot> --run '<command>' "<why>"`, stored by the
|
|
4
|
+
* collector as the message's `run` field, never executed server-side).
|
|
5
|
+
*
|
|
6
|
+
* The TUI shows it with the shared ask: "▶ SENDER asks you to run this here: why", the FULL command in a code
|
|
7
|
+
* block (wrapped, never truncated: this is a confirm-what-you-see boundary), then "Run it? y/N/c". y hands the
|
|
8
|
+
* terminal over (like /restart: the user's own shell, stdio inherited, so OTP and login prompts work), then
|
|
9
|
+
* the TUI resumes and prints the exit code.
|
|
10
|
+
*
|
|
11
|
+
* Rules (non-negotiable): never auto-run; only from a verified sender (author_identity) in this org, otherwise
|
|
12
|
+
* shown but refused; one command; the ask expires after RUN_TTL_MS; never through the model (no turn, never a
|
|
13
|
+
* terminal_suggest_command offer); secret-shaped or destructive commands need "run" typed instead of y.
|
|
14
|
+
*/
|
|
15
|
+
import { spawnSync } from 'node:child_process';
|
|
16
|
+
import { askText, askAnswer } from './ask.js';
|
|
17
|
+
import { looksLikeKey } from './connect.js';
|
|
18
|
+
import { resolveCliSlash } from './slash/cliCommand.js';
|
|
19
|
+
import { tokenize } from './bang.js';
|
|
20
|
+
import { codeRows } from './slotFormat.js';
|
|
21
|
+
import { graphemes } from './textBuffer.js';
|
|
22
|
+
import { TERMINAL_RESTORE } from './slash/restart.js';
|
|
23
|
+
import { PASTE_ON } from './keys.js';
|
|
24
|
+
|
|
25
|
+
export const RUN_TTL_MS = 10 * 60 * 1000;
|
|
26
|
+
export const RUN_MAX_LEN = 2000;
|
|
27
|
+
// Same rule as the collector (devSlotMessagesStore validateRunCommand): one line, nothing that can make the
|
|
28
|
+
// screen differ from what runs (control/ESC/CR, bidi overrides, zero-width, line separators).
|
|
29
|
+
const RUN_BAD_RE = /[\u0000-\u001f\u007f\u0080-\u009f\u200b-\u200f\u2028\u2029\u202a-\u202e\u2066-\u2069\ufeff]/;
|
|
30
|
+
|
|
31
|
+
/** -> null (no run request) | { id, cmd, why, who, runnable, refuse, danger, expiresAt }. */
|
|
32
|
+
export function runRequest(m, { orgId = null, now = Date.now() } = {}) {
|
|
33
|
+
if (!m || m.run === undefined || m.run === null || m.run === '') return null;
|
|
34
|
+
const cmd = String(m.run);
|
|
35
|
+
const who = String(m.from || m.author_identity || 'someone').replace(/\s*\(.*$/, '').trim() || 'someone';
|
|
36
|
+
let refuse = null;
|
|
37
|
+
if (!m.author_identity) refuse = `${who} is not a verified sender, so this window will not run it`;
|
|
38
|
+
else if (orgId && m.org_id && m.org_id !== orgId) refuse = `it came from another org, so this window will not run it`;
|
|
39
|
+
else if (cmd.length > RUN_MAX_LEN || !cmd.trim() || RUN_BAD_RE.test(cmd)) refuse = 'it is not one plain command on one line, so this window will not run it';
|
|
40
|
+
return { id: m.id || null, cmd, why: String(m.text ?? '').trim(), who, runnable: !refuse, refuse, danger: refuse ? null : runDanger(cmd), expiresAt: now + RUN_TTL_MS };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Destructive shell shapes (the bang runner has no shell danger list of its own; these are the ones that cost
|
|
44
|
+
// real work to undo). Matching one means "type run", never a refusal: it is the human's machine.
|
|
45
|
+
const DESTRUCTIVE = [
|
|
46
|
+
/(^|[\s;&|(])rm\s+(-[a-zA-Z]*[rf][a-zA-Z]*\s+)*-[a-zA-Z]*[rRf]/, /(^|[\s;&|(])sudo\s/, /(^|[\s;&|(])doas\s/,
|
|
47
|
+
/(^|[\s;&|(])mkfs(\.\w+)?\s/, /(^|[\s;&|(])dd\s+[^|;&]*\bof=/, />\s*\/dev\/(sd|nvme|hd|vd|disk)/,
|
|
48
|
+
/\|\s*(sudo\s+)?(ba|z|da|k)?sh\b/, /\bgit\s+push\b[^|;&]*\s(--force\b|-f\b|--force-with-lease\b|--delete\b)/,
|
|
49
|
+
/\bgit\s+reset\s+--hard\b/, /\bgit\s+clean\s+-[a-zA-Z]*f/, /\bchmod\s+-R\b/, /\bchown\s+-R\b/,
|
|
50
|
+
/:\(\)\s*\{/, /(^|[\s;&|(])(shutdown|reboot|halt|poweroff)\b/, /\bdrop\s+(table|database)\b/i, /\bdocker\s+(system\s+prune|volume\s+rm)\b/,
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
/** null when y is enough; otherwise the reason "run" must be typed. Reuses the bang secret check and the quox CLI leaf policy. */
|
|
54
|
+
export function runDanger(cmd) {
|
|
55
|
+
const c = String(cmd || '');
|
|
56
|
+
if (looksLikeKey(c)) return 'It contains something that looks like a key or token';
|
|
57
|
+
const words = tokenize(c);
|
|
58
|
+
if (words[0] === 'quox' || words[0] === 'q') {
|
|
59
|
+
const hit = resolveCliSlash(`/${words[1] || ''}`, words.slice(2).map(w => (/[\s"']/.test(w) ? JSON.stringify(w) : w)).join(' '));
|
|
60
|
+
if (hit?.cls === 'never') return `${hit.display} is a never-from-chat quox command`;
|
|
61
|
+
}
|
|
62
|
+
if (DESTRUCTIVE.some(re => re.test(c))) return /(^|[\s;&|(])(sudo|doas)\s/.test(c) ? 'It runs as root' : 'It looks destructive';
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** 'run' | 'needs-run' (y on a dangerous one) | 'no' | 'chat' | 'command'. */
|
|
67
|
+
export function runAnswer(text, req) {
|
|
68
|
+
const t = String(text ?? '').trim();
|
|
69
|
+
if (/^run$/i.test(t)) return 'run';
|
|
70
|
+
const a = askAnswer(t);
|
|
71
|
+
if (a === 'yes') return req?.danger ? 'needs-run' : 'run';
|
|
72
|
+
return a;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const len = (s) => graphemes(String(s)).length;
|
|
76
|
+
function wrapWords(text, width) {
|
|
77
|
+
const out = []; let line = '';
|
|
78
|
+
for (const w of String(text).split(/\s+/).filter(Boolean)) {
|
|
79
|
+
if (line && len(line) + 1 + len(w) > width) { out.push(line); line = w; } else line = line ? `${line} ${w}` : w;
|
|
80
|
+
}
|
|
81
|
+
if (line) out.push(line);
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** The ask as rows of segments (render.js paints them; slotFormat rows shape). */
|
|
86
|
+
export function runAskRows(req, width) {
|
|
87
|
+
const w = Math.max(20, width);
|
|
88
|
+
const head = askText(`${req.who} asks you to run this here:`, { keys: '' }) + (req.why ? ` ${req.why}` : '');
|
|
89
|
+
const rows = wrapWords(head, w).map((l, i) => [{ t: i ? ` ${l}` : l, role: 'ask', bold: true }]);
|
|
90
|
+
// wrapWords may push an indented continuation over the width: re-wrap continuations at w - 2
|
|
91
|
+
const fixed = [];
|
|
92
|
+
for (const r of rows) { if (len(r[0].t) <= w) fixed.push(r); else for (const p of wrapWords(r[0].t, w - 2)) fixed.push([{ t: ` ${p}`, role: 'ask', bold: true }]); }
|
|
93
|
+
fixed.push(...codeRows([req.cmd], w));
|
|
94
|
+
let last;
|
|
95
|
+
if (!req.runnable) last = { t: `✖ Not offered: ${req.refuse}. Copy it yourself only if you trust it.`, role: 'warn' };
|
|
96
|
+
else if (req.danger) last = { t: askText('Type run to run it, n to skip?', { keys: 'run/N/c', reason: req.danger }), role: 'ask', bold: true };
|
|
97
|
+
else last = { t: askText('Run it?'), role: 'ask', bold: true };
|
|
98
|
+
for (const l of wrapWords(last.t, w)) fixed.push([{ ...last, t: l }]);
|
|
99
|
+
return fixed;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Run the command in the user's own shell with the terminal handed over (stdio inherited), then take it back.
|
|
104
|
+
* The live frame is cleared first and padded after, so the TUI redraws below the command's output instead of
|
|
105
|
+
* over it. -> { code, signal, ms } | { error, ms }.
|
|
106
|
+
*/
|
|
107
|
+
export function handOverRun({ cmd, file = null, args = null, env = process.env, cwd = process.cwd(), stdout = null, setRawMode = null, spawn = spawnSync, shell, now = Date.now, resume = '' }) {
|
|
108
|
+
const tty = Boolean(stdout?.isTTY);
|
|
109
|
+
const rows = tty ? Math.max(0, Number(stdout.frameRows) || 0) : 0;
|
|
110
|
+
const put = (s) => { try { stdout.write(Buffer.from(s)); } catch { /* closed */ } };
|
|
111
|
+
if (tty) put(`\r${rows ? `\x1b[${rows}A` : ''}\x1b[J${TERMINAL_RESTORE}`);
|
|
112
|
+
try { setRawMode?.(false); } catch { /* not a tty */ }
|
|
113
|
+
const t0 = now();
|
|
114
|
+
let r;
|
|
115
|
+
try { // file + args (BANG-TTY: !quox runs this binary's argv) or a shell command line
|
|
116
|
+
r = file ? spawn(file, args || [], { stdio: 'inherit', cwd, env }) : spawn(shell || env.SHELL || '/bin/sh', ['-lc', cmd], { stdio: 'inherit', cwd, env });
|
|
117
|
+
} catch (e) { r = { error: e }; }
|
|
118
|
+
const ms = now() - t0;
|
|
119
|
+
try { setRawMode?.(true); } catch { /* not a tty */ }
|
|
120
|
+
if (tty) put(`\n${'\n'.repeat(rows)}${PASTE_ON}${resume}`); // resume: mouse tracking back on when /mouse on (QUOXON-MOUSE)
|
|
121
|
+
if (r?.error) return { error: `${r.error.code || 'failed'}: ${r.error.message}`, ms };
|
|
122
|
+
return { code: r?.status ?? null, signal: r?.signal || null, ms };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** The footer after a hand-over run. */
|
|
126
|
+
export function runFooter(r) {
|
|
127
|
+
const secs = `${(r.ms / 1000).toFixed(1)}s`;
|
|
128
|
+
if (r.error) return `Could not start it: ${r.error}`;
|
|
129
|
+
if (r.signal) return `killed by ${r.signal} · ${secs}`;
|
|
130
|
+
return `exit ${r.code} · ${secs}`;
|
|
131
|
+
}
|
|
@@ -78,8 +78,9 @@ export function soundForEvent(prev, action, ctx = {}) {
|
|
|
78
78
|
const now = ctx.now ?? action?.now ?? 0;
|
|
79
79
|
if (!prev || !action) return null;
|
|
80
80
|
// An approval card appeared (stream event or inbox poll).
|
|
81
|
-
if (next.approvals?.length > (prev.approvals?.length || 0)) return 'hitl';
|
|
81
|
+
if (next.approvals?.length > (prev.approvals?.length || 0) || next.waiting?.length > (prev.waiting?.length || 0)) return 'hitl'; // a card, or another session's notice
|
|
82
82
|
if (action.type === 'approval.result') return action.outcome === 'failed' ? 'denied' : null;
|
|
83
|
+
if (action.type === 'stepup.result') return action.ok ? 'success' : 'denied'; // QUOXON-STEPUP: reuse the existing cues
|
|
83
84
|
if (action.type !== 'event' || !action.ev) return null;
|
|
84
85
|
const ev = action.ev;
|
|
85
86
|
switch (ev.type) {
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TUI-INPUT: a letter then "/" ("n/help") is a card answer that leaked into the composer ahead of a command.
|
|
3
|
+
* Pure: -> { letter, command } when the text looks like that and the rest names a known slash command, else null.
|
|
4
|
+
* (Ordinary "w/o", "b/c" or "and/or" never match: the word after the slash must be a command.)
|
|
5
|
+
*/
|
|
6
|
+
export function strayCardAnswer(text, names = []) {
|
|
7
|
+
const t = String(text ?? '').trim();
|
|
8
|
+
const m = /^([a-z])\/([a-z][\w-]*)(?=\s|$)/i.exec(t);
|
|
9
|
+
if (!m) return null;
|
|
10
|
+
if (!names.includes(`/${m[2].toLowerCase()}`)) return null;
|
|
11
|
+
return { letter: m[1].toLowerCase(), command: t.slice(1) };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** The one-line question (askText adds the y/N/c keys). */
|
|
15
|
+
export const strayQuestion = (s) => `Did you mean ${s.command.split(/\s+/)[0]}? (the '${s.letter}' looks like a stray card answer)`;
|
|
16
|
+
|
|
17
|
+
/** True when the same message is already queued, steered or waiting for the server. */
|
|
18
|
+
export function alreadyQueued(text, { pending = [], steering = [], waiting = null } = {}) {
|
|
19
|
+
const t = String(text ?? '').trim();
|
|
20
|
+
if (!t) return false;
|
|
21
|
+
return [...pending, ...steering].some(p => String(p.text ?? '').trim() === t) || String(waiting?.text ?? '').trim() === t;
|
|
22
|
+
}
|