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,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QLOOP-LIVE: the loop cards in the chat stream. One card per loop, updated in place; observer points and the finish line
|
|
3
|
+
* append as ordinary chat lines beneath it, so the history reads as the loop's story.
|
|
4
|
+
*
|
|
5
|
+
* Glue only (the logic is in loopCard.js and loopStream.js): it owns the watchers, the redraw throttle (<= 8 per second),
|
|
6
|
+
* the keys of a focused card and the finish moment. The host (app.js) supplies say / addItem / bump / dragon, which keeps this
|
|
7
|
+
* testable without Ink.
|
|
8
|
+
*/
|
|
9
|
+
import { loopPinned, initLoop, applySnapshot, applyProgress, reduceLoop, setConn, observerLine, finishLines, startLine, createThrottle, REDRAW_MIN_MS, REDRAW_UNFOCUSED_MS, TERMINAL } from './loopCard.js';
|
|
10
|
+
import { watchLoop } from './loopStream.js';
|
|
11
|
+
import { objectiveMoment } from './dragonMoments.js';
|
|
12
|
+
|
|
13
|
+
const TICK_MS = 250; // a focused card's spinner and clock
|
|
14
|
+
const TICK_UNFOCUSED_EVERY = 4; // nobody on the card: every 4th tick (1 s), so idle cards cost little
|
|
15
|
+
const REFRESH_MIN_MS = 2000; // the runner's own turn is read from the snapshot at most this often per card
|
|
16
|
+
const REFRESH_CAP_MS = 1500; // and the finish never waits longer than this for the last read
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The roster sees a loop finish (its row turns done) up to a minute BEFORE the stream's loop.finished reaches the card (the server
|
|
20
|
+
* drains receipts first), and its dragon then lands on a card that is still running and overflows the screen. A loop with a live card
|
|
21
|
+
* summons its dragon from the card, once, at its finish: the roster's watch skips those rows.
|
|
22
|
+
*/
|
|
23
|
+
export const rowsWithoutLoopCards = (rows, has) => (rows || []).filter((r) => !(r?.kind === 'loop' && has(r.id)));
|
|
24
|
+
|
|
25
|
+
export function createLoopCards({ api, say, addItem, bump, dragon = null, now = Date.now, setTick = (f, ms) => { const t = setInterval(f, ms); t.unref?.(); return () => clearInterval(t); }, throttleTimer, watch = watchLoop, sleep } = {}) {
|
|
26
|
+
const cards = new Map(); // id -> ctrl
|
|
27
|
+
let focusId = null, cancelTick = null;
|
|
28
|
+
const throttle = createThrottle(() => bump(), { minMs: () => (focusId ? REDRAW_MIN_MS : REDRAW_UNFOCUSED_MS), now, ...(throttleTimer ? { timer: throttleTimer } : {}) });
|
|
29
|
+
|
|
30
|
+
const running = () => [...cards.values()].some((c) => !c.state.done);
|
|
31
|
+
/**
|
|
32
|
+
* A runner loop's stream `turn` counts model messages; its own turn is the snapshot's `iteration`. Read it again when a turn moves
|
|
33
|
+
* (at most every REFRESH_MIN_MS; a request inside the window is made by the next tick). -> a promise that settles when the read is over.
|
|
34
|
+
*/
|
|
35
|
+
function refresh(c, force = false) {
|
|
36
|
+
if (!api.loopSnapshot || c.refreshing) return c.refreshing || Promise.resolve();
|
|
37
|
+
if (!force && now() - (c.lastSnapAt ?? -Infinity) < REFRESH_MIN_MS) { c.refreshDue = true; return Promise.resolve(); }
|
|
38
|
+
c.lastSnapAt = now(); c.refreshDue = false;
|
|
39
|
+
c.refreshing = Promise.resolve().then(() => api.loopSnapshot(c.id)).then((snap) => { c.state = applyProgress(c.state, snap); throttle.poke(); }, () => {}).finally(() => { c.refreshing = null; });
|
|
40
|
+
return c.refreshing;
|
|
41
|
+
}
|
|
42
|
+
let tickN = 0;
|
|
43
|
+
function tick() { // elapsed time and the spinner move once per tick, never faster than the throttle allows
|
|
44
|
+
if (!running()) { cancelTick?.(); cancelTick = null; return; }
|
|
45
|
+
if (!focusId && ++tickN % TICK_UNFOCUSED_EVERY) return;
|
|
46
|
+
for (const c of cards.values()) if (!c.state.done) { c.frame = (c.frame + 1) % 4; if (c.refreshDue) refresh(c); }
|
|
47
|
+
throttle.poke();
|
|
48
|
+
}
|
|
49
|
+
const ensureTick = () => { if (!cancelTick && running()) cancelTick = setTick(tick, TICK_MS); };
|
|
50
|
+
|
|
51
|
+
function finish(c, ev) {
|
|
52
|
+
if (!c.finishedSaid) { c.finishedSaid = true; for (const l of finishLines(c.state)) say(l.text, l.tone); }
|
|
53
|
+
c.confirm = false; // a focused card keeps its focus: Enter then expands the final output and Esc lets the frozen card go to scrollback
|
|
54
|
+
// The moment engine fires the dragon. Same call and same gates as a finished roster row (mode on, not typing, cooldown).
|
|
55
|
+
const st = c.state;
|
|
56
|
+
const m = !c.quiet && dragon?.summon && ['completed', 'completed_with_errors', 'failed'].includes(st.state)
|
|
57
|
+
? objectiveMoment({ kind: 'loop', state: st.state === 'failed' ? 'failed' : 'done', name: st.title || st.id, activity: st.reason || '' }) : null;
|
|
58
|
+
if (m) Promise.resolve(dragon.summon(st.agent && typeof st.agent === 'string' ? st.agent : m.dragon, m.caption)).then((shown) => { if (shown) bump(); }, () => {});
|
|
59
|
+
return ev;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function onEvent(c, ev) {
|
|
63
|
+
const before = c.state;
|
|
64
|
+
c.state = reduceLoop(before, ev);
|
|
65
|
+
if (c.state === before) return;
|
|
66
|
+
const line = c.quiet ? null : observerLine(ev, c.state); // replaying a loop that was already over: the card tells it, the chat does not re-read it
|
|
67
|
+
if (line) { c.state.observed = [...c.state.observed, line.text].slice(-50); say(line.text, line.tone); }
|
|
68
|
+
if (ev.type === 'loop.turn.started' || ev.type === 'loop.turn.completed' || ev.type === 'loop.observe') refresh(c);
|
|
69
|
+
if (ev.type === 'loop.finished') { // the last read of the runner's turn first: "completed, N turns" must be the loop's count
|
|
70
|
+
c.finishing = true;
|
|
71
|
+
let t; const cap = new Promise((res) => { t = setTimeout(res, REFRESH_CAP_MS); t.unref?.(); });
|
|
72
|
+
Promise.race([refresh(c, true), cap]).then(() => { clearTimeout(t); finish(c, ev); throttle.poke(); });
|
|
73
|
+
}
|
|
74
|
+
throttle.poke();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function run(c) {
|
|
78
|
+
const ctl = c.abort = new AbortController();
|
|
79
|
+
try {
|
|
80
|
+
const out = await watch({
|
|
81
|
+
api, id: c.id, signal: ctl.signal, sleep,
|
|
82
|
+
onSnapshot: (snap) => { c.state = applySnapshot(c.state, snap); if (c.state.done) c.quiet = true; throttle.poke(); }, // the finish line waits for the replay: it needs the receipts
|
|
83
|
+
onEvent: (ev) => onEvent(c, ev),
|
|
84
|
+
onConn: ({ conn, attempt, note }) => { if (!c.state.done) { c.state = setConn(c.state, conn, { attempt, note }); throttle.poke(); } },
|
|
85
|
+
});
|
|
86
|
+
if (out?.lost && !c.state.done) { c.state = setConn(c.state, 'lost', { attempt: 0, note: out.reason }); }
|
|
87
|
+
if (c.state.done && !c.finishedSaid && !c.finishing) { c.finishedSaid = true; for (const l of finishLines(c.state)) say(l.text, l.tone); } // finished before we looked (or the replay ended without a finish event)
|
|
88
|
+
} catch (e) {
|
|
89
|
+
c.state = setConn(c.state, 'lost', { note: e?.message || 'failed' });
|
|
90
|
+
} finally { throttle.poke(); }
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
/** Put a live card in the chat stream for this loop and start following it. -> the card, or the existing one. */
|
|
95
|
+
watch(id, { title = '', agent = null, announce = true } = {}) {
|
|
96
|
+
if (!id) return null;
|
|
97
|
+
if (cards.has(id)) return cards.get(id);
|
|
98
|
+
const c = { id, state: initLoop({ id, title, agent }), expanded: false, focused: false, confirm: false, frame: 0, abort: null, finishedSaid: false };
|
|
99
|
+
cards.set(id, c);
|
|
100
|
+
if (announce) { const l = startLine(c.state); say(l.text, l.tone); } // the start line first, then the card, then the story beneath
|
|
101
|
+
addItem({ id: `loopcard-${id}`, kind: 'loopcard', card: c });
|
|
102
|
+
ensureTick();
|
|
103
|
+
run(c).then(() => bump());
|
|
104
|
+
return c;
|
|
105
|
+
},
|
|
106
|
+
/**
|
|
107
|
+
* A loop the chat just made (its `objective` tool answered `Loop "<title>" created ...`): the tool result carries no id, so look
|
|
108
|
+
* the loop up in the org's list (newest first) and put a card in this conversation for the live, unwatched one that matches
|
|
109
|
+
* the title. One retry, because the list can lag the tool's answer. -> ids watched.
|
|
110
|
+
*/
|
|
111
|
+
async discover({ title = '' } = {}) {
|
|
112
|
+
if (!api.loopList) return [];
|
|
113
|
+
const want = String(title || '').replace(/…$/, '').trim().toLowerCase();
|
|
114
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
115
|
+
let rows = [];
|
|
116
|
+
try { const d = await api.loopList(); rows = Array.isArray(d) ? d : (d?.loops || []); } catch { rows = []; }
|
|
117
|
+
const hit = rows.filter((r) => {
|
|
118
|
+
const id = r.objective_id || r.id; if (!id || cards.has(id) || TERMINAL.has(r.state)) return false;
|
|
119
|
+
const age = now() - (Date.parse(String(r.created_at || '').replace(' ', 'T') + (/[zZ]|[+-]\d\d:?\d\d$/.test(String(r.created_at || '')) ? '' : 'Z')) || 0);
|
|
120
|
+
return age < 20 * 60 * 1000 && (!want || String(r.title || '').toLowerCase().startsWith(want));
|
|
121
|
+
});
|
|
122
|
+
if (hit.length) { const r = hit[0]; const id = r.objective_id || r.id; this.watch(id, { title: r.title || title }); return [id]; }
|
|
123
|
+
if (attempt === 0) await (sleep || ((ms) => new Promise((res) => setTimeout(res, ms))))(1500);
|
|
124
|
+
}
|
|
125
|
+
return [];
|
|
126
|
+
},
|
|
127
|
+
/** Read the runner's turn now (tests, and a card that was just focused). */
|
|
128
|
+
refresh: (id) => (cards.has(id) ? refresh(cards.get(id), true) : Promise.resolve()),
|
|
129
|
+
has: (id) => cards.has(id),
|
|
130
|
+
get: (id) => cards.get(id) || null,
|
|
131
|
+
all: () => [...cards.values()],
|
|
132
|
+
/** True while this history item must stay in the live region (commitLimit): its loop has not finished. */
|
|
133
|
+
held: (item) => Boolean(item?.kind === 'loopcard' && item.card && (!item.card.state.done || item.card.focused)), // a focused card stays live even when finished, so Enter / Esc work on it
|
|
134
|
+
anyRunning: running,
|
|
135
|
+
/** The cards drawn above the composer: running ones, and a finished one you are still on. */
|
|
136
|
+
pinned: () => [...cards.values()].filter(loopPinned),
|
|
137
|
+
/** The card the keys act on, or null. */
|
|
138
|
+
focused: () => (focusId && cards.get(focusId)) || null,
|
|
139
|
+
/** Focus the newest card that is still running (or the named one). -> the card or null. */
|
|
140
|
+
focus(id = null) {
|
|
141
|
+
const pick = id ? cards.get(id) : [...cards.values()].reverse().find((c) => !c.state.done) || [...cards.values()].pop();
|
|
142
|
+
if (!pick) return null;
|
|
143
|
+
const old = this.focused(); if (old) { old.focused = false; old.confirm = false; }
|
|
144
|
+
pick.focused = true; focusId = pick.id; throttle.poke(); bump(); return pick;
|
|
145
|
+
},
|
|
146
|
+
blur() { const c = this.focused(); if (c) { c.focused = false; c.confirm = false; } focusId = null; bump(); },
|
|
147
|
+
/**
|
|
148
|
+
* Keys while a card is focused. Enter toggles the expanded spool, s asks to halt (y confirms, anything else cancels),
|
|
149
|
+
* Esc collapses and then leaves. -> true when the key was consumed.
|
|
150
|
+
*/
|
|
151
|
+
key(name, text) {
|
|
152
|
+
const c = this.focused(); if (!c) return false;
|
|
153
|
+
if (c.confirm) {
|
|
154
|
+
c.confirm = false;
|
|
155
|
+
if (text === 'y') this.halt(c.id).then(() => bump(), () => bump()); else say('Halt cancelled.');
|
|
156
|
+
bump(); return true;
|
|
157
|
+
}
|
|
158
|
+
if (name === 'enter') { c.expanded = !c.expanded; bump(); return true; }
|
|
159
|
+
if (name === 'escape') { if (c.expanded) c.expanded = false; else this.blur(); bump(); return true; }
|
|
160
|
+
if (text === 's' && !c.state.done) { c.confirm = true; bump(); return true; }
|
|
161
|
+
if (name === 'up' || name === 'down' || name === 'tab') { // cycle between cards when there are several
|
|
162
|
+
const ids = [...cards.keys()]; if (ids.length > 1) { const i = ids.indexOf(c.id); this.focus(ids[(i + (name === 'up' ? ids.length - 1 : 1)) % ids.length]); }
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
if (text) { this.blur(); return false; } // ordinary typing goes to the draft
|
|
166
|
+
return true;
|
|
167
|
+
},
|
|
168
|
+
/** The halt goes through the same governed route as the roster's s: after an explicit y from the human, never silently. */
|
|
169
|
+
async halt(id) {
|
|
170
|
+
const c = cards.get(id); if (!c) return { ok: false, message: `no loop card for ${id}` };
|
|
171
|
+
if (!api.stopRow) { say('Halt is not available on this server.', 'warn'); return { ok: false }; }
|
|
172
|
+
const r = await api.stopRow({ source: 'objectives', kind: 'loop', id, name: c.state.title || id });
|
|
173
|
+
say(r.message || (r.ok ? 'Halt sent.' : 'Halt refused.'), r.ok ? undefined : 'warn');
|
|
174
|
+
return r;
|
|
175
|
+
},
|
|
176
|
+
dispose() { for (const c of cards.values()) c.abort?.abort(); cards.clear(); cancelTick?.(); cancelTick = null; throttle.dispose(); },
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export { TERMINAL };
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QLOOP-LIVE: the client for a running loop. Reads GET /api/v1/loops/:id (snapshot) and
|
|
3
|
+
* GET /api/v1/loops/:id/events (SSE; ?since=<event_seq> resumes; a heartbeat comment every 15 s; ends after loop.finished).
|
|
4
|
+
* Contract: ~/quoxon-work/qloop-live-design.md section 1. Every event carries a monotonic event_seq.
|
|
5
|
+
*
|
|
6
|
+
* Reconnect is the part that must not lie. A drop is counted, backed off (500 ms doubling to 10 s, reset by any event) and
|
|
7
|
+
* reported to the card as 'reconnecting'; after MAX_FAILS drops in a row with no event between, or at once on 401/403/404,
|
|
8
|
+
* the watcher stops and says 'lost' with the reason. It never reports a lost stream as a finished loop.
|
|
9
|
+
*/
|
|
10
|
+
export const BACKOFF_START_MS = 500;
|
|
11
|
+
export const BACKOFF_CAP_MS = 10000;
|
|
12
|
+
export const MAX_FAILS = 8;
|
|
13
|
+
const FATAL = new Set([401, 403, 404]);
|
|
14
|
+
const TERMINAL_STATES = new Set(['completed', 'completed_with_errors', 'failed', 'interrupted', 'halted', 'cancelled', 'aborted', 'stopped']);
|
|
15
|
+
const isTerminal = (snap) => Boolean(snap && (snap.finished === true || TERMINAL_STATES.has(snap.state)));
|
|
16
|
+
|
|
17
|
+
const sleepFor = (ms, signal) => new Promise((resolve) => {
|
|
18
|
+
if (signal?.aborted) return resolve();
|
|
19
|
+
const t = setTimeout(done, ms); t.unref?.();
|
|
20
|
+
function done() { signal?.removeEventListener?.('abort', done); clearTimeout(t); resolve(); }
|
|
21
|
+
signal?.addEventListener?.('abort', done, { once: true });
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Follow one loop until loop.finished, abort, or a fatal failure.
|
|
26
|
+
*
|
|
27
|
+
* api: { loopSnapshot(id) -> object, loopEvents(id, { since }, signal) -> async iterable of events }
|
|
28
|
+
* handlers: { onSnapshot(snap), onEvent(ev), onConn({ conn: 'live'|'reconnecting'|'lost', attempt, note }) }
|
|
29
|
+
* -> { finished: boolean, lost: boolean, lastSeq, reason? }
|
|
30
|
+
*/
|
|
31
|
+
export async function watchLoop({ api, id, signal, since = 0, onSnapshot = () => {}, onEvent = () => {}, onConn = () => {}, sleep = sleepFor, backoffStart = BACKOFF_START_MS, backoffCap = BACKOFF_CAP_MS, maxFails = MAX_FAILS }) {
|
|
32
|
+
let lastSeq = Number(since) || 0, fails = 0, delay = backoffStart, finished = false;
|
|
33
|
+
try { onSnapshot(await api.loopSnapshot(id)); }
|
|
34
|
+
catch (e) { // the snapshot is a head start, not a gate: the replayed events carry the same facts. A fatal status still stops.
|
|
35
|
+
if (FATAL.has(e?.status)) { onConn({ conn: 'lost', attempt: 0, note: describe(e) }); return { finished: false, lost: true, lastSeq, reason: describe(e) }; }
|
|
36
|
+
}
|
|
37
|
+
while (!signal?.aborted && !finished) {
|
|
38
|
+
let sawEvent = false;
|
|
39
|
+
try {
|
|
40
|
+
for await (const ev of api.loopEvents(id, { since: lastSeq || undefined }, signal)) {
|
|
41
|
+
if (signal?.aborted) break;
|
|
42
|
+
if (!ev || typeof ev.type !== 'string') continue;
|
|
43
|
+
const seq = Number(ev.event_seq);
|
|
44
|
+
if (Number.isFinite(seq) && seq <= lastSeq) continue; // the boundary event of a resume
|
|
45
|
+
if (Number.isFinite(seq)) lastSeq = seq;
|
|
46
|
+
if (!sawEvent) { sawEvent = true; fails = 0; delay = backoffStart; onConn({ conn: 'live', attempt: 0, note: null }); }
|
|
47
|
+
onEvent(ev);
|
|
48
|
+
if (ev.type === 'loop.finished') { finished = true; break; }
|
|
49
|
+
}
|
|
50
|
+
if (!finished && !signal?.aborted && !sawEvent) {
|
|
51
|
+
// A clean close with no events is not a failure: a loop's silent final turn, or a loop that already finished, ends this way.
|
|
52
|
+
// Ask the snapshot. Terminal: finish quietly with the final state. Still running: reconnect with ?since, failures reset.
|
|
53
|
+
// Only a snapshot read that itself fails counts (a real error).
|
|
54
|
+
let snap = null;
|
|
55
|
+
try { snap = await api.loopSnapshot(id); }
|
|
56
|
+
catch (e) {
|
|
57
|
+
if (FATAL.has(e?.status)) { onConn({ conn: 'lost', attempt: fails, note: describe(e) }); return { finished: false, lost: true, lastSeq, reason: describe(e) }; }
|
|
58
|
+
fails += 1;
|
|
59
|
+
if (fails >= maxFails) { onConn({ conn: 'lost', attempt: fails, note: describe(e) }); return { finished: false, lost: true, lastSeq, reason: describe(e) }; }
|
|
60
|
+
onConn({ conn: 'reconnecting', attempt: fails, note: describe(e) });
|
|
61
|
+
await sleep(delay, signal); delay = Math.min(delay * 2, backoffCap);
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (isTerminal(snap)) { onSnapshot(snap); return { finished: true, lost: false, lastSeq, final: snap }; }
|
|
65
|
+
fails = 0; // still running: quiet, not a failure
|
|
66
|
+
await sleep(backoffStart, signal);
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
else if (!finished && sawEvent) fails = 0;
|
|
70
|
+
} catch (e) {
|
|
71
|
+
if (signal?.aborted) break;
|
|
72
|
+
if (FATAL.has(e?.status)) { onConn({ conn: 'lost', attempt: fails, note: describe(e) }); return { finished: false, lost: true, lastSeq, reason: describe(e) }; }
|
|
73
|
+
fails += 1;
|
|
74
|
+
if (fails >= maxFails) { onConn({ conn: 'lost', attempt: fails, note: describe(e) }); return { finished: false, lost: true, lastSeq, reason: describe(e) }; }
|
|
75
|
+
onConn({ conn: 'reconnecting', attempt: fails, note: describe(e) });
|
|
76
|
+
await sleep(delay, signal); delay = Math.min(delay * 2, backoffCap);
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (finished || signal?.aborted) break;
|
|
80
|
+
if (fails >= maxFails) { onConn({ conn: 'lost', attempt: fails, note: 'the stream keeps closing' }); return { finished: false, lost: true, lastSeq, reason: 'the stream keeps closing' }; }
|
|
81
|
+
onConn({ conn: 'reconnecting', attempt: Math.max(1, fails), note: null });
|
|
82
|
+
await sleep(delay, signal); delay = Math.min(delay * 2, backoffCap);
|
|
83
|
+
}
|
|
84
|
+
return { finished, lost: false, lastSeq };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const describe = (e) => `${e?.status ? `HTTP ${e.status} ` : ''}${e?.message || 'failed'}`.trim();
|
package/lib/quoxon/main.js
CHANGED
|
@@ -5,119 +5,22 @@ import { App } from './app.js';
|
|
|
5
5
|
import { inkRenderOptions, inkStdout } from './term.js';
|
|
6
6
|
import { createDemoApi } from './demo.js';
|
|
7
7
|
import { DEMO_LABEL } from './demo.fixtures.js';
|
|
8
|
-
import {
|
|
8
|
+
import { AUTOPLAY_LABEL, autoplayOn } from './demo.autoplay.js';
|
|
9
|
+
import { captureEarlyInput } from './earlyInput.js';
|
|
9
10
|
import { shouldShowWelcome, markWelcomeSeen } from './welcome.js';
|
|
10
11
|
import { loadRules, rulesContext } from './rules.js';
|
|
11
12
|
import { createTranscript } from './transcript.js';
|
|
12
|
-
import {
|
|
13
|
+
import { createScreenRecorder, screenFramesEnabled } from './screenFrames.js';
|
|
13
14
|
import { applySettings } from './slash/settings.js';
|
|
14
|
-
import { headOf, RUNNING_ROOT, execRestart, checkRestart } from './slash/restart.js';
|
|
15
|
+
import { headOf, RUNNING_ROOT, execRestart, restartPlan, checkRestart, startBuildWatch, settleMsFrom, minGapMsFrom, restoreTerminal, RESTART_SLOT_ENV, RESTART_ATTACH_ENV, RESTART_NOTE_ENV, RESTART_MODE_ENV } from './slash/restart.js';
|
|
16
|
+
import { readLastSeen, writeLastSeen } from './away.js';
|
|
17
|
+
import { fileHistory } from './promptHistory.js';
|
|
18
|
+
import { takeLauncherHandoff, writeRestartHandoff, RESTART_EXIT_CODE } from './launcher.js';
|
|
15
19
|
|
|
16
|
-
|
|
17
|
-
|
|
20
|
+
export { parseArgs, USAGE, pickProfile, choosePick, chatCredentialProblem, noServerConfigured, printAuthMessage } from './headless.js';
|
|
21
|
+
import { parseArgs, USAGE, noServerConfigured, mainPrint, pickProfile, chatCredentialProblem } from './headless.js';
|
|
18
22
|
|
|
19
|
-
export
|
|
20
|
-
const o = { profile: null, verbose: 'on', demo: false, help: false, print: false, prompt: null, output: 'text', conversation: null, turnId: null, timeout: 0, color: false, noClear: false, name: null, serviceKey: false, demoOnboarding: false, provider: null, resume: null, verboseSet: false, errors: [] };
|
|
21
|
-
let rawProvider = null, rawModel = null;
|
|
22
|
-
const value = (flag, i) => { const v = argv[i + 1]; if (v === undefined || (v.startsWith('-') && isFlag(v))) { o.errors.push(`${flag} needs a value`); return undefined; } return v; };
|
|
23
|
-
for (let i = 0; i < argv.length; i++) {
|
|
24
|
-
const a = argv[i];
|
|
25
|
-
if (a === '--') { const rest = argv.slice(i + 1).join(' '); if (rest) o.prompt = rest; break; }
|
|
26
|
-
if (a === '-p' || a === '--profile') { const v = value(a, i); if (v !== undefined) { o.profile = v; i++; } }
|
|
27
|
-
else if (a.startsWith('--profile=')) o.profile = a.slice(10);
|
|
28
|
-
else if (a === '--verbose') { o.verboseSet = true; const v = argv[i + 1]; if (['off', 'on', 'full'].includes(v)) { o.verbose = v; i++; } else o.verbose = 'on'; } // no value means on
|
|
29
|
-
else if (a.startsWith('--verbose=')) { o.verboseSet = true; o.verbose = a.slice(10); }
|
|
30
|
-
else if (a === '--resume') { const v = argv[i + 1]; if (v !== undefined && !v.startsWith('-')) { o.resume = v; i++; } else o.resume = true; } // alone: the picker
|
|
31
|
-
else if (a.startsWith('--resume=')) o.resume = a.slice(9) || true;
|
|
32
|
-
else if (a === '-n' || a === '--name') { const v = value(a, i); if (v !== undefined) { o.name = v; i++; } }
|
|
33
|
-
else if (a.startsWith('--name=')) o.name = a.slice(7);
|
|
34
|
-
else if (a === '--demo') o.demo = true;
|
|
35
|
-
else if (a === '--demo-onboarding') o.demoOnboarding = true;
|
|
36
|
-
else if (a === '--print') { o.print = true; const n = argv[i + 1]; if (n !== undefined && !isFlag(n)) { o.prompt = n; i++; } }
|
|
37
|
-
else if (a.startsWith('--print=')) { o.print = true; o.prompt = a.slice(8); }
|
|
38
|
-
else if (a === '--output') { const v = value(a, i); if (v !== undefined) { o.output = v; i++; } }
|
|
39
|
-
else if (a.startsWith('--output=')) o.output = a.slice(9);
|
|
40
|
-
else if (a === '--conversation') { const v = value(a, i); if (v !== undefined) { o.conversation = v; i++; } }
|
|
41
|
-
else if (a === '--turn-id') { const v = value(a, i); if (v !== undefined) { o.turnId = v; i++; } }
|
|
42
|
-
else if (a === '--timeout') { const v = value(a, i); if (v !== undefined) { o.timeout = Number(v) || 0; i++; } }
|
|
43
|
-
else if (a === '--color') o.color = true;
|
|
44
|
-
else if (a === '--no-clear') o.noClear = true;
|
|
45
|
-
else if (a === '--service-key') o.serviceKey = true;
|
|
46
|
-
else if (a === '--provider' || a === '--model') { const v = value(a, i); if (v !== undefined) { if (a === '--provider') rawProvider = v; else rawModel = v; i++; } }
|
|
47
|
-
else if (a.startsWith('--provider=')) rawProvider = a.slice(11);
|
|
48
|
-
else if (a.startsWith('--model=')) rawModel = a.slice(8);
|
|
49
|
-
else if (a === '--prompt-profile') { const v = value(a, i); if (v !== undefined) { o.promptProfile = v; i++; } }
|
|
50
|
-
else if (a.startsWith('--prompt-profile=')) o.promptProfile = a.slice(17);
|
|
51
|
-
else if (a === '-h' || a === '--help') o.help = true;
|
|
52
|
-
else if (!a.startsWith('-') && o.prompt === null && argv.includes('--print')) o.prompt = a; // `--print` given earlier or later; this is its prompt
|
|
53
|
-
else o.errors.push(a.startsWith('-') ? `unknown option ${a}` : `unexpected argument ${a}`);
|
|
54
|
-
}
|
|
55
|
-
if (rawModel && !rawProvider) o.errors.push('--model needs --provider (e.g. --provider openai --model gpt-5.4)');
|
|
56
|
-
else if (rawProvider) {
|
|
57
|
-
const m = parseModelArg(rawModel ? `${rawProvider} ${rawModel}` : rawProvider);
|
|
58
|
-
if (!m.ok) o.errors.push(m.error); else o.provider = m.choice || 'auto'; // 'auto' = this run ignores the saved /model choice
|
|
59
|
-
}
|
|
60
|
-
if (o.promptProfile !== undefined && !['compact', 'full'].includes(o.promptProfile)) { o.errors.push(`invalid --prompt-profile "${o.promptProfile}" (use compact or full)`); delete o.promptProfile; }
|
|
61
|
-
if (!['off', 'on', 'full'].includes(o.verbose)) o.verbose = 'on';
|
|
62
|
-
if (!['text', 'json'].includes(o.output)) { o.errors.push(`invalid --output "${o.output}" (use text or json)`); o.output = 'text'; }
|
|
63
|
-
return o;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export const USAGE = 'quox [-p|--profile <name>] [-n|--name <session name>] [--conversation <id>] [--resume [id|name]] [--verbose off|on|full] [--demo] [--no-clear] [--service-key] [--provider openai|anthropic|ollama|openrouter|huggingface|groq|together|lmstudio|vllm|custom|auto [--model <id>]] (quoxon is an alias; the old menu is `quox tui`)\n quox --conversation <id> resume that conversation interactively (its saved turns load into the window)\n quox --resume [id|name] pick up a recent conversation; alone it opens a picker\n quox --print "<prompt>" [--output text|json] [--conversation <id>] [--turn-id <id>] [--timeout <s>] [--provider <p> [--model <id>]] [--prompt-profile compact|full] (prompt from stdin if absent or -; use -- before a prompt that starts with -)\n exit: 0 completed, 1 other, 2 held for approval, 3 interrupted or abandoned, 4 not signed in, 5 ended without server confirmation\n --provider asks the server for that provider this run (default: your /model choice for this profile, else auto); turn.started from the server says what ran\n --service-key lets chat use an ambient QUOX_SERVICE_KEY when the profile has no login (never used silently)\n --demo plays a simulated journey on its own (no server, nothing real runs)\n --demo-onboarding walks Getting started with a dry-run install (nothing is downloaded, built or started)';
|
|
67
|
-
|
|
68
|
-
/** Default profile: a resident (agent) login cannot chat, so use the single logged-in human profile. */
|
|
69
|
-
export function pickProfile(explicit, cfgMod) {
|
|
70
|
-
if (explicit) return { name: explicit, note: null };
|
|
71
|
-
const probe = cfgMod.loadConfig();
|
|
72
|
-
const active = probe.getActiveProfileName?.();
|
|
73
|
-
const residents = new Set((probe.listResidentProfiles?.() || []).map(r => r.name));
|
|
74
|
-
return choosePick(active, residents, probe.listProfiles?.() || []);
|
|
75
|
-
}
|
|
76
|
-
export function choosePick(active, residents, profiles) {
|
|
77
|
-
if (!residents.has(active)) return { name: undefined, note: null };
|
|
78
|
-
const humans = profiles.filter(p => !residents.has(p.name) && p.loggedIn);
|
|
79
|
-
if (humans.length === 1) return { name: humans[0].name, note: null }; // the status line shows the profile; no transcript noise
|
|
80
|
-
return { name: undefined, note: `The default profile "${active}" is an agent login and cannot chat. Pass -p <your profile>.` };
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Chat identity gate. A named profile that does not exist is an error (never created on the fly), and an
|
|
85
|
-
* ambient QUOX_SERVICE_KEY is only a credential when the user asked for it (--service-key or FORCE).
|
|
86
|
-
* -> null when chat may proceed, else { message }.
|
|
87
|
-
*/
|
|
88
|
-
export function chatCredentialProblem({ profileArg, profiles, hasStoredLogin, profileName, env = process.env, serviceKeyFlag = false }) {
|
|
89
|
-
if (profileArg && !profiles.some(p => p.name === profileArg)) return { message: `profile ${profileArg} does not exist; create it with: quox login -p ${profileArg}` };
|
|
90
|
-
if (hasStoredLogin) return null;
|
|
91
|
-
if ((serviceKeyFlag || env.QUOX_FORCE_SERVICE_KEY) && env.QUOX_SERVICE_KEY) return null;
|
|
92
|
-
return { message: `Not signed in for chat on profile ${profileName}. Run: quox login -p ${profileName}${env.QUOX_SERVICE_KEY ? ' (an ambient QUOX_SERVICE_KEY is ignored unless you pass --service-key)' : ''}` };
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Is this a machine with no Quox server configured at all? Then bare `quox` is Getting started, and --print
|
|
97
|
-
* says how to get there. Reads profiles only (no network).
|
|
98
|
-
*/
|
|
99
|
-
export async function noServerConfigured(opts, cfgMod, env = process.env) {
|
|
100
|
-
const { needsGettingStarted } = await import('./onboarding/detect.js');
|
|
101
|
-
const { loadJourney } = await import('./onboarding/journey.js');
|
|
102
|
-
const probe = cfgMod.loadConfig();
|
|
103
|
-
return needsGettingStarted({ profiles: probe.listProfiles?.() || [], residents: probe.listResidentProfiles?.() || [], profileArg: opts.profile, env, serviceKeyFlag: opts.serviceKey, journey: loadJourney() });
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/** --print without a usable login: today's message, plus the way in when no server exists yet. */
|
|
107
|
-
export function printAuthMessage(problem, gettingStarted, hint) {
|
|
108
|
-
return `quox: ${problem.message}\n${gettingStarted ? `quox: ${hint}\n` : ''}`;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/** Keys typed before the app is ready: raw mode now, so nothing is cooked-echoed, buffered for the composer. */
|
|
112
|
-
export function captureEarlyInput(stdin = process.stdin, stdout = process.stdout) {
|
|
113
|
-
if (!stdin?.isTTY) return null;
|
|
114
|
-
const chunks = [];
|
|
115
|
-
const fn = (d) => chunks.push(typeof d === 'string' ? d : d.toString('utf8'));
|
|
116
|
-
try { stdin.setRawMode?.(true); stdin.resume?.(); } catch { /* not a tty after all */ }
|
|
117
|
-
stdin.on('data', fn);
|
|
118
|
-
if (stdout?.isTTY) stdout.write(PASTE_ON);
|
|
119
|
-
return { chunks, detach: () => stdin.off('data', fn) };
|
|
120
|
-
}
|
|
23
|
+
export { captureEarlyInput } from './earlyInput.js';
|
|
121
24
|
|
|
122
25
|
/**
|
|
123
26
|
* Clear the screen before the chat client? Never right after Getting started: its finale (the one-time
|
|
@@ -129,13 +32,14 @@ export function shouldClearBeforeChat({ isTTY, noClear, fromGettingStarted }) {
|
|
|
129
32
|
|
|
130
33
|
export async function main(argv = process.argv.slice(2)) {
|
|
131
34
|
const opts = parseArgs(argv);
|
|
132
|
-
if (opts.help) { console.log(USAGE); return; }
|
|
133
|
-
if (opts.errors.length) { process.stderr.write(`quox: ${opts.errors.join('; ')}\n${USAGE}\n`); process.exitCode = 1; return; }
|
|
134
35
|
// Raw mode FIRST: the awaits below (module loads, config, health) must not leave the tty cooked.
|
|
36
|
+
// bin/quox.js already turned it on before loading anything heavy; this returns that same capture.
|
|
135
37
|
const early = !opts.print ? captureEarlyInput() : null;
|
|
38
|
+
if (opts.help) { early?.release(); console.log(USAGE); return; }
|
|
39
|
+
if (opts.errors.length) { early?.release(); process.stderr.write(`quox: ${opts.errors.join('; ')}\n${USAGE}\n`); process.exitCode = 1; return; }
|
|
136
40
|
const cwd = process.cwd().replace(process.env.HOME || '\0', '~');
|
|
137
41
|
const { resolveCliVersion } = await import('../core/version.js');
|
|
138
|
-
let version = '0.
|
|
42
|
+
let version = '0.4.0';
|
|
139
43
|
try { version = resolveCliVersion?.() || version; } catch { /* keep default */ }
|
|
140
44
|
if (opts.print) return mainPrint(opts);
|
|
141
45
|
let replay = null;
|
|
@@ -152,12 +56,23 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
152
56
|
// and a fresh install replays the original request as the first real turn
|
|
153
57
|
replay = r.replay || null;
|
|
154
58
|
fromGettingStarted = true;
|
|
59
|
+
// QUOXON-Q: ONE offer for a short `q` command, asked once (answer kept in the CLI config)
|
|
60
|
+
if (process.stdin.isTTY && process.stdout.isTTY && !opts.demoOnboarding) {
|
|
61
|
+
try {
|
|
62
|
+
const { offerShortcut } = await import('./onboarding/shortcutOffer.js');
|
|
63
|
+
const { createInterface } = await import('node:readline/promises');
|
|
64
|
+
const config = (await import('../core/config.js')).loadConfig();
|
|
65
|
+
await offerShortcut({ config, ask: async (line) => { const rl = createInterface({ input: process.stdin, output: process.stdout }); try { return await rl.question(`\n${line} `); } finally { rl.close(); } } });
|
|
66
|
+
} catch { /* an offer must never block the first conversation */ }
|
|
67
|
+
}
|
|
155
68
|
}
|
|
156
69
|
let props;
|
|
157
70
|
let restart = null; // set by /restart: run the installed quox here once the app has let go of the terminal
|
|
158
71
|
if (opts.demo) {
|
|
72
|
+
const auto = autoplayOn(opts.autoplay) ? { exitAfterMs: opts.autoplayExit != null ? opts.autoplayExit * 1000 : (process.env.QUOX_DEMO_AUTOPLAY_EXIT ? Number(process.env.QUOX_DEMO_AUTOPLAY_EXIT) * 1000 : null) } : null;
|
|
159
73
|
// /restart works in the demo too (a fresh demo, nothing to resume): the same hand-over, testable with no server
|
|
160
|
-
|
|
74
|
+
// QUOX_DEMO_DECISIONS: demo-only pty test seam for the walkthrough proofs (a decisions state file; never a real server)
|
|
75
|
+
props = { api: createDemoApi({ objects: true, decisionsFile: process.env.QUOX_DEMO_DECISIONS || null }), profile: 'demo', org: 'demo-org', demoLabel: auto ? AUTOPLAY_LABEL : DEMO_LABEL, autoplay: auto ? null : 'Show me what you can do (demo)', demoAuto: auto, onRestart: (r) => { restart = { ...r, conversationId: null }; }, onDemo: () => { restart = { demo: true }; }, restartCheck: checkRestart };
|
|
161
76
|
} else {
|
|
162
77
|
const cfgMod = await import('../core/config.js');
|
|
163
78
|
const { createAuth } = await import('../core/auth.js');
|
|
@@ -168,77 +83,103 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
168
83
|
const missing = opts.profile && !profiles.some(p => p.name === opts.profile);
|
|
169
84
|
const pick = pickProfile(opts.profile, cfgMod);
|
|
170
85
|
const config = missing ? probe : cfgMod.loadConfig(pick.name);
|
|
171
|
-
const
|
|
172
|
-
const
|
|
173
|
-
const api = createApi({
|
|
86
|
+
const { createWindowClient, carryOrg, RESTART_ORG_ENV } = await import('./windowOrg.js');
|
|
87
|
+
const win = createWindowClient({ config, auth: createAuth(config), createClient }); // #1034: /org switch is window-scoped
|
|
88
|
+
const api = createApi({ ...win, config });
|
|
174
89
|
const pname = missing ? opts.profile : (config.getActiveProfileName?.() || 'default');
|
|
175
90
|
const problem = chatCredentialProblem({ profileArg: opts.profile, profiles, hasStoredLogin: missing ? false : Boolean(config.getAuth?.()?.access_token), profileName: pname, serviceKeyFlag: opts.serviceKey });
|
|
91
|
+
// Z2H: a dead session (refresh rejected) or a cleared login on a human profile is ONE signed-out state, answered
|
|
92
|
+
// by a single card and /login, not scattered digest/approvals/chat errors. Transient failures read as alive.
|
|
93
|
+
let signedOut = null;
|
|
94
|
+
if (!missing && !opts.serviceKey) {
|
|
95
|
+
if (problem && !pick.note) signedOut = { reason: null };
|
|
96
|
+
else if (!problem) {
|
|
97
|
+
const pr = await Promise.race([api.probeSession(), new Promise(r => setTimeout(() => r({ alive: true }), 4000))]).catch(() => ({ alive: true }));
|
|
98
|
+
if (!pr.alive) signedOut = { reason: pr.reason || null };
|
|
99
|
+
}
|
|
100
|
+
}
|
|
176
101
|
// The health probe runs inside the app (background); the UI is up and typing works immediately.
|
|
177
102
|
const startup = async () => {
|
|
103
|
+
if (signedOut) return null;
|
|
178
104
|
if (problem) return problem.message;
|
|
179
105
|
if (pick.note) return pick.note;
|
|
180
106
|
const hl = await api.health(8000);
|
|
181
107
|
return hl.ok ? null : hl.absent ? `No QuoxCORE running at ${hl.url}` : `QuoxCORE at ${hl.url} is not answering (${hl.status}); messages may be slow`;
|
|
182
108
|
};
|
|
109
|
+
// /restart hand-over: re-enter the predecessor window's org in this window only (the profile is untouched).
|
|
110
|
+
const carried = !missing && !signedOut && !problem ? await carryOrg(api, process.env) : (delete process.env.QUOX_RESTART_ORG, null);
|
|
111
|
+
const startupCarry = async () => (carried?.error ? `Could not re-enter org ${carried.id} after the restart (${carried.error}). This window is in the profile's org.` : startup());
|
|
183
112
|
const saved = api.settings();
|
|
184
113
|
applySettings(saved);
|
|
185
|
-
if (!opts.verboseSet
|
|
186
|
-
|
|
114
|
+
if (!opts.verboseSet) opts.verbose = ['off', 'on', 'full'].includes(saved.verbose) ? saved.verbose : 'off'; // protocol lines are opt-in: /verbose on
|
|
115
|
+
// PROFILE-SWITCH: build a signed-in profile's api for an in-place window switch (the App only asks after a y or
|
|
116
|
+
// /profile). The window org rides along; the profile file's default is never moved (loadConfig(name) is a lens).
|
|
117
|
+
const residentNames = () => (cfgMod.loadConfig().listResidentProfiles?.() || []).map(r => r.name);
|
|
118
|
+
const switchProfile = async (name, { org: winOrg = null } = {}) => {
|
|
119
|
+
const row = (cfgMod.loadConfig().listProfiles?.() || []).find(p => p.name === name);
|
|
120
|
+
if (!row || !row.loggedIn) return { ok: false, reason: 'not_signed_in' };
|
|
121
|
+
const c2 = cfgMod.loadConfig(name);
|
|
122
|
+
const a2 = createApi({ ...createWindowClient({ config: c2, auth: createAuth(c2), createClient }), config: c2 });
|
|
123
|
+
const pr = await Promise.race([a2.probeSession(), new Promise(r => setTimeout(() => r({ alive: true }), 4000))]).catch(() => ({ alive: true }));
|
|
124
|
+
if (!pr.alive) return { ok: false, reason: 'not_signed_in' };
|
|
125
|
+
const c = winOrg ? await carryOrg(a2, { [RESTART_ORG_ENV]: winOrg }) : null;
|
|
126
|
+
const note = c?.error ? `Could not re-enter org ${winOrg} (${c.error}); this window is in ${name}'s org.` : null;
|
|
127
|
+
return { ok: true, api: a2, agentLogin: residentNames().includes(name), orgName: (c && !c.error && c.name) || c2.getAuth?.()?.org_name || null, note };
|
|
128
|
+
};
|
|
129
|
+
const loginCandidates = profiles.filter(p => !p.loggedIn && !residentNames().includes(p.name)).map(p => p.name);
|
|
130
|
+
props = { api, switchProfile, loginCandidates, away: { read: readLastSeen, write: writeLastSeen }, modelChoice: opts.provider, resume: opts.resume ?? null, onRestart: (r) => { restart = r; }, onDemo: () => { restart = { demo: true }; }, restartCheck: checkRestart, buildWatch: (o) => startBuildWatch({ ...o, settleMs: settleMsFrom(), minGapMs: minGapMsFrom() }), startSha: headOf(RUNNING_ROOT), rules: loadRules(), profile: pname, agentLogin: (probe.listResidentProfiles?.() || []).some(r => r.name === pname), humanProfiles: profiles.filter(p => p.loggedIn && !(probe.listResidentProfiles?.() || []).some(r => r.name === p.name)).map(p => p.name), org: missing ? null : ((carried && !carried.error && carried.name) || config.getAuth?.()?.org_name || null), startup: startupCarry, healthRecheck: () => api.health(8000), blocked: problem && !signedOut ? problem.message : null, signedOut, autoplay: replay };
|
|
187
131
|
}
|
|
188
132
|
// Interactive start: a clean screen under the intro (never when piped; NO_COLOR still clears),
|
|
189
133
|
// except straight after Getting started, whose finale must stay readable.
|
|
190
134
|
if (shouldClearBeforeChat({ isTTY: process.stdout.isTTY, noClear: opts.noClear, fromGettingStarted })) process.stdout.write('\x1b[2J\x1b[3J\x1b[H');
|
|
135
|
+
// Z2H-R5 C: do not advertise /slot where the licence check says no (the server's own capabilities answer, short wait).
|
|
136
|
+
let slotsAvailable = true;
|
|
137
|
+
if (!opts.demo && process.stdout.isTTY && props.api?.capabilities && !props.signedOut && !props.blocked) {
|
|
138
|
+
try {
|
|
139
|
+
const caps = await Promise.race([props.api.capabilities(), new Promise(res => setTimeout(() => res(null), 2000))]);
|
|
140
|
+
const c = caps?.ok ? caps.capabilities.find(x => x.id === 'slots') : null;
|
|
141
|
+
if (c && c.availability === 'unavailable' && /not in your plan/i.test(String(c.reason || ''))) slotsAvailable = false;
|
|
142
|
+
} catch { /* unknown: leave the invite as it was */ }
|
|
143
|
+
}
|
|
191
144
|
const welcome = shouldShowWelcome({ print: opts.print });
|
|
192
145
|
if (welcome) markWelcomeSeen();
|
|
193
146
|
const transcript = opts.demo ? null : createTranscript({ meta: { version, cols: process.stdout.columns, rows: process.stdout.rows, profile: props.profile, org: props.org, cwd } });
|
|
194
147
|
const onResize = () => transcript?.resize(process.stdout.columns, process.stdout.rows);
|
|
195
148
|
if (transcript) process.stdout.on('resize', onResize);
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
149
|
+
// SCREEN-FRAMES: what the screen showed, settled, into the same local transcript (QUOX_SCREEN_FRAMES=0 turns it off).
|
|
150
|
+
const screenRec = transcript && process.stdout.isTTY && screenFramesEnabled(process.env)
|
|
151
|
+
? createScreenRecorder({ write: (rec) => transcript.screen(rec), cols: () => process.stdout.columns, rows: () => process.stdout.rows })
|
|
152
|
+
: null;
|
|
153
|
+
const restartSlot = opts.demo ? null : (process.env[RESTART_SLOT_ENV] || null); delete process.env[RESTART_SLOT_ENV]; // one hand-over only
|
|
154
|
+
const restartAttach = opts.demo ? null : (process.env[RESTART_ATTACH_ENV] || null); delete process.env[RESTART_ATTACH_ENV];
|
|
155
|
+
const restartMode = opts.demo ? null : (process.env[RESTART_MODE_ENV] === 'plan' ? 'plan' : null); delete process.env[RESTART_MODE_ENV]; // PLAN MODE: window-scoped, carried once
|
|
156
|
+
const restartNote = opts.demo ? null : (process.env[RESTART_NOTE_ENV] || null); delete process.env[RESTART_NOTE_ENV]; // e.g. a failed /update, said once in the restarted client
|
|
157
|
+
if (restartNote && !props.introNote) props.introNote = restartNote;
|
|
158
|
+
const app = render(React.createElement(App, { ...props, restartSlot, restartAttach, ...(restartMode ? { mode: restartMode } : {}), welcome, tipStartup: !fromGettingStarted && !opts.demo && Boolean(process.stdout.isTTY), whatsNewStartup: !opts.demo && Boolean(process.stdout.isTTY), version, cwd, verbose: opts.verbose, earlyInput: early, sessionName: opts.name, resumeConversation: opts.conversation || null, invite: Boolean(process.stdout.isTTY) && !opts.name, slotsAvailable, transcript, historyStore: opts.demo ? null : fileHistory() }), inkRenderOptions(process.stdout, { tap: screenRec?.tap }));
|
|
159
|
+
try { await app.waitUntilExit(); screenRec?.stop(); transcript?.end(restart ? 'restart' : 'exit'); }
|
|
160
|
+
catch (e) { screenRec?.stop(); transcript?.end(`crash: ${e?.message || e}`); throw e; }
|
|
161
|
+
finally { screenRec?.stop(); process.stdout.off('resize', onResize); }
|
|
200
162
|
if (!restart) return;
|
|
163
|
+
if (restart.update) { // /update said y: the update owns the terminal (npm prompts, sudo), then the same restart either way
|
|
164
|
+
const { runUpdate } = await import('../core/selfUpdate.js');
|
|
165
|
+
const plan = restart.update;
|
|
166
|
+
restoreTerminal();
|
|
167
|
+
process.stdout.write(`\nUpdating quox: ${plan.command.display}\n`);
|
|
168
|
+
const u = runUpdate(plan);
|
|
169
|
+
if (u.ok) process.stdout.write(`\u2714 Update finished (exit 0). Restarting into this conversation\u2026\n`);
|
|
170
|
+
else {
|
|
171
|
+
const note = `Update failed: ${plan.command.display} ${u.error ? `could not start (${u.error})` : `exited ${u.code}`}. Restarting on the build that is installed now. Run it yourself to see why: ${plan.manual}`;
|
|
172
|
+
process.stdout.write(`\u2716 ${note}\n`);
|
|
173
|
+
process.env[RESTART_NOTE_ENV] = note; // the restarted client (same build) says it again in the conversation
|
|
174
|
+
}
|
|
175
|
+
}
|
|
201
176
|
// The app preflighted the installed quox before letting go, so a failure here is rare (it changed in between).
|
|
202
|
-
const
|
|
177
|
+
const how = { argv: restart.demo ? ['--demo', '--autoplay'] : argv, conversationId: restart.demo ? null : restart.conversationId, profile: opts.demo ? null : (restart.profile || props.profile), name: restart.name || opts.name || null, slot: restart.demo ? null : (restart.slot || null), attach: restart.demo ? null : (restart.attach || null), org: restart.demo ? null : (restart.org || null), mode: restart.demo ? null : (restart.mode || null), bin: restart.bin || undefined };
|
|
178
|
+
// QUOXON-RESTART-LEAK: under the launcher this process EXITS and the launcher runs the successor, so exactly one
|
|
179
|
+
// client stays alive per terminal. Without one (QUOX_NO_LAUNCHER=1) it falls back to waiting on a nested child.
|
|
180
|
+
const launched = takeLauncherHandoff();
|
|
181
|
+
if (launched && writeRestartHandoff(launched, restartPlan(how))) { restoreTerminal(); process.exitCode = RESTART_EXIT_CODE; return; }
|
|
182
|
+
const r = execRestart(how);
|
|
203
183
|
if (r.error) { process.stderr.write(`quox: could not restart (${r.bin}: ${r.error})${restart.conversationId ? `; resume with: quox --resume ${restart.conversationId}` : ''}\n`); process.exitCode = 1; return; }
|
|
204
184
|
process.exitCode = r.code;
|
|
205
185
|
}
|
|
206
|
-
|
|
207
|
-
async function mainPrint(opts) {
|
|
208
|
-
const { runPrint, resolvePrompt, EXIT } = await import('./print.js');
|
|
209
|
-
const out = (s) => process.stdout.write(s);
|
|
210
|
-
const err = (s) => process.stderr.write(s);
|
|
211
|
-
const prompt = await resolvePrompt(opts.prompt, process.stdin);
|
|
212
|
-
let api;
|
|
213
|
-
if (opts.demo) api = createDemoApi({ speed: 20 });
|
|
214
|
-
else {
|
|
215
|
-
const cfgMod = await import('../core/config.js');
|
|
216
|
-
const { createAuth } = await import('../core/auth.js');
|
|
217
|
-
const { createClient } = await import('../core/client.js');
|
|
218
|
-
const { createApi } = await import('./api.js');
|
|
219
|
-
const probe = cfgMod.loadConfig();
|
|
220
|
-
const profiles = probe.listProfiles?.() || [];
|
|
221
|
-
const missing = opts.profile && !profiles.some(p => p.name === opts.profile);
|
|
222
|
-
const pick = pickProfile(opts.profile, cfgMod);
|
|
223
|
-
if (pick.note) err(pick.note + '\n');
|
|
224
|
-
const config = missing ? probe : cfgMod.loadConfig(pick.name);
|
|
225
|
-
const auth = createAuth(config);
|
|
226
|
-
const pname = missing ? opts.profile : (config.getActiveProfileName?.() || 'default');
|
|
227
|
-
const problem = chatCredentialProblem({ profileArg: opts.profile, profiles, hasStoredLogin: missing ? false : Boolean(config.getAuth?.()?.access_token), profileName: pname, serviceKeyFlag: opts.serviceKey });
|
|
228
|
-
if (problem) {
|
|
229
|
-
const { NO_SERVER_PRINT } = await import('./onboarding/copy.js');
|
|
230
|
-
err(printAuthMessage(problem, await noServerConfigured(opts, cfgMod), NO_SERVER_PRINT));
|
|
231
|
-
process.exitCode = EXIT.auth; return;
|
|
232
|
-
}
|
|
233
|
-
api = createApi({ client: createClient(config, auth), config, auth });
|
|
234
|
-
if (opts.conversation && api.conversationExists) {
|
|
235
|
-
const c = await api.conversationExists(opts.conversation);
|
|
236
|
-
if (c.exists === false) { err(`quox: conversation not found: ${opts.conversation}\n`); process.exitCode = EXIT.error; return; }
|
|
237
|
-
if (c.exists === null) err(`quox: could not verify conversation ${opts.conversation} (${c.reason}); continuing\n`);
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
const rules = opts.demo ? null : loadRules();
|
|
241
|
-
for (const s of rules?.skipped || []) err(`quox: ignoring ${s.path}: ${s.reason}\n`);
|
|
242
|
-
const choice = opts.provider === 'auto' ? null : opts.provider || api.modelChoice?.() || null;
|
|
243
|
-
process.exitCode = await runPrint({ api, choice, promptProfile: opts.promptProfile, context: rulesContext(rules), prompt, output: opts.output, conversation: opts.conversation, turnId: opts.turnId || undefined, timeoutS: opts.timeout, color: opts.color, out, err });
|
|
244
|
-
}
|