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
package/lib/core/slotKeeper.js
CHANGED
|
@@ -13,9 +13,10 @@
|
|
|
13
13
|
* environmental (clock, sleep, probe, signals, fs dir) is injected.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
+
import { decideWake, wakeText, draftLines, promptBoxState, isWakeLine, wakeQueued, WAKE_SETTLE_MS } from './slotWake.js';
|
|
16
17
|
import {
|
|
17
18
|
openSync, closeSync, writeSync, readFileSync, unlinkSync, appendFileSync,
|
|
18
|
-
statSync, writeFileSync, mkdirSync,
|
|
19
|
+
statSync, writeFileSync, mkdirSync, chmodSync,
|
|
19
20
|
} from 'node:fs';
|
|
20
21
|
import { homedir } from 'node:os';
|
|
21
22
|
import { join } from 'node:path';
|
|
@@ -32,6 +33,8 @@ export const BACKOFF_CAP_MS = 5 * 60 * 1000;
|
|
|
32
33
|
// long outage beats letting the hold lapse (the collector revives a stale
|
|
33
34
|
// slot for its own holder on the next successful heartbeat).
|
|
34
35
|
export const MAX_FAILS = 24;
|
|
36
|
+
// #893: consecutive re-leases allowed without a good heartbeat in between.
|
|
37
|
+
export const MAX_REACQUIRES = 3;
|
|
35
38
|
export const MAX_AUTH_FAILS = 3;
|
|
36
39
|
export const LOG_MAX_BYTES = 256 * 1024;
|
|
37
40
|
// QUOX-CLAUDE-3: the control queue is polled far more often than the 5-min
|
|
@@ -43,6 +46,18 @@ export const CONTROL_POLL_MS = 20 * 1000;
|
|
|
43
46
|
export const CONTROL_BACKOFF_CAP_TICKS = 15;
|
|
44
47
|
export const BUSY_WINDOW_MS = 60 * 1000;
|
|
45
48
|
export const NO_TMUX_RESULT = 'session not under tmux; start it with quox claude';
|
|
49
|
+
// FORCE-WAKE: only a wake the server accepted from a human (an org admin via the CLI, or an allowlisted
|
|
50
|
+
// Discord user) carries this provenance; anything else keeps the old refusal at a draft.
|
|
51
|
+
const HUMAN_VIA = new Set(['cli', 'discord']);
|
|
52
|
+
export const FORCE_STEP_MS = 400; // between a key and the capture that checks it landed
|
|
53
|
+
export const FORCE_SUBMIT_WAIT_STEPS = 15; // ~6s for the wake line to leave the box before the draft goes back
|
|
54
|
+
// WAKE-REAL (owner 2026-10-17 "wake just pipes text to the prompt but doesnt wake anyone"): Claude Code drops an
|
|
55
|
+
// Enter that reaches it in the same read as the typed line (a loaded session reads both bursts as one paste), so
|
|
56
|
+
// the line sits in the box unsent. Proven live: QUOXON's 00:36:41 wake stayed unsent 3.5 min. The keeper lets
|
|
57
|
+
// the line land before Enter, then re-presses Enter only while the box still holds exactly its own line.
|
|
58
|
+
export const WAKE_LAND_STEPS = 10; // ~4s for the typed line to show in the box
|
|
59
|
+
export const WAKE_ENTER_TRIES = 4; // Enters sent at most (each only while the box holds only the wake line)
|
|
60
|
+
export const WAKE_ENTER_WAIT_STEPS = 6; // ~2.4s for the box to clear after each Enter
|
|
46
61
|
|
|
47
62
|
export function keeperDir() {
|
|
48
63
|
return join(homedir(), '.quox', 'keepers');
|
|
@@ -80,12 +95,22 @@ function defaultExecFile(file, args) {
|
|
|
80
95
|
export async function runKeeper(opts) {
|
|
81
96
|
const {
|
|
82
97
|
name, session, claude, heartbeat, release, forget, probe,
|
|
98
|
+
// #893: optional. Re-lease after the hold LAPSED (expired, not taken over).
|
|
99
|
+
// Must never take over; a refusal (409) means someone else holds it.
|
|
100
|
+
reacquire = null,
|
|
83
101
|
sleep = (ms) => new Promise((r) => setTimeout(r, ms)),
|
|
84
102
|
rand = Math.random,
|
|
85
103
|
dir = keeperDir(),
|
|
86
104
|
// Remote control (all optional; absent pollControl = heartbeat-only keeper).
|
|
87
105
|
pollControl, ackControl, tmuxPane = null,
|
|
88
106
|
execFile = defaultExecFile,
|
|
107
|
+
// POKE-WAKE: the gap between the two pane samples a wake compares.
|
|
108
|
+
settle = (ms) => new Promise((r) => setTimeout(r, ms)),
|
|
109
|
+
// KEEPER-REEXEC: the installed build (null = unknown), how to start a successor on the new
|
|
110
|
+
// build, and how to stop a keeper on an older one. All optional; absent = old behaviour.
|
|
111
|
+
buildId = () => null,
|
|
112
|
+
reexec = null,
|
|
113
|
+
kill = (pid, sig) => process.kill(pid, sig),
|
|
89
114
|
now = () => Date.now(),
|
|
90
115
|
installSignals = (cleanup) => {
|
|
91
116
|
const h = () => { cleanup(); process.exit(0); };
|
|
@@ -96,6 +121,8 @@ export async function runKeeper(opts) {
|
|
|
96
121
|
|
|
97
122
|
mkdirSync(dir, { recursive: true });
|
|
98
123
|
const lockPath = join(dir, `${name}.pid`);
|
|
124
|
+
const buildPath = join(dir, `${name}.build`);
|
|
125
|
+
const startBuild = buildId();
|
|
99
126
|
const logPath = join(dir, `${name}.log`);
|
|
100
127
|
const log = (msg) => {
|
|
101
128
|
try {
|
|
@@ -119,8 +146,21 @@ export async function runKeeper(opts) {
|
|
|
119
146
|
let other = NaN;
|
|
120
147
|
try { other = parseInt(readFileSync(lockPath, 'utf-8'), 10); } catch { /* raced away */ }
|
|
121
148
|
if (Number.isFinite(other) && other !== process.pid && probe.alive(other)) {
|
|
122
|
-
|
|
123
|
-
|
|
149
|
+
// KEEPER-REEXEC: a keeper on an older (or unrecorded) build is replaced, so checkout always
|
|
150
|
+
// leaves the slot kept by the installed code. SIGTERM drops only its lock: the hold is kept.
|
|
151
|
+
const otherBuild = (() => { try { return readFileSync(buildPath, 'utf-8').trim() || null; } catch { return null; } })();
|
|
152
|
+
if (startBuild && otherBuild !== startBuild && attempt === 0) {
|
|
153
|
+
log(`replacing keeper pid ${other} (build ${otherBuild || 'unrecorded'}) with build ${startBuild}`);
|
|
154
|
+
try { kill(other, 'SIGTERM'); } catch { /* already gone */ }
|
|
155
|
+
for (let w = 0; w < 20 && probe.alive(other); w += 1) await settle(250);
|
|
156
|
+
if (probe.alive(other)) {
|
|
157
|
+
log(`keeper pid ${other} did not exit; leaving it`);
|
|
158
|
+
return { code: 0, reason: 'already-running' };
|
|
159
|
+
}
|
|
160
|
+
} else {
|
|
161
|
+
log(`another keeper (pid ${other}) already running; exiting`);
|
|
162
|
+
return { code: 0, reason: 'already-running' };
|
|
163
|
+
}
|
|
124
164
|
}
|
|
125
165
|
try { unlinkSync(lockPath); } catch { /* raced away */ }
|
|
126
166
|
}
|
|
@@ -136,15 +176,37 @@ export async function runKeeper(opts) {
|
|
|
136
176
|
} catch { /* already gone */ }
|
|
137
177
|
};
|
|
138
178
|
installSignals(cleanup);
|
|
179
|
+
if (startBuild) { try { writeFileSync(buildPath, startBuild); } catch { /* informational */ } }
|
|
180
|
+
// KEEPER-REEXEC: checked on every control poll. Lock is removed BEFORE the successor starts so it
|
|
181
|
+
// can take it; the hold is never released (the successor heartbeats the same slot).
|
|
182
|
+
let reexecFailedFor = null;
|
|
183
|
+
const staleBuild = () => {
|
|
184
|
+
if (!startBuild || typeof reexec !== 'function') return false;
|
|
185
|
+
const nowBuild = buildId();
|
|
186
|
+
if (!nowBuild || nowBuild === startBuild) return false;
|
|
187
|
+
if (reexecFailedFor === nowBuild) return false;
|
|
188
|
+
log(`installed build ${startBuild} -> ${nowBuild}: re-exec`);
|
|
189
|
+
cleanup();
|
|
190
|
+
try { reexec(); } catch (e) {
|
|
191
|
+
// No successor: take the lock back and keep heartbeating on the old code rather than drop the hold.
|
|
192
|
+
reexecFailedFor = nowBuild;
|
|
193
|
+
try { writeFileSync(lockPath, String(process.pid)); } catch { /* best effort */ }
|
|
194
|
+
log(`re-exec failed (${e.message}); staying on build ${startBuild}`);
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
return true;
|
|
198
|
+
};
|
|
139
199
|
|
|
140
200
|
try {
|
|
141
201
|
log(`keeper started session=${session} claude=${claude?.pid}:${claude?.starttime}`);
|
|
142
202
|
let fails = 0;
|
|
203
|
+
let reacquires = 0;
|
|
143
204
|
let delay = KEEP_INTERVAL_MS * (1 + rand() * 0.2);
|
|
144
205
|
|
|
145
206
|
// Control queue state: ids already executed (never re-run if an ack failed,
|
|
146
207
|
// only re-acked), and distinct failure accounting with tick-skip backoff.
|
|
147
208
|
const handled = new Map();
|
|
209
|
+
const received = new Set();
|
|
148
210
|
const startedAt = now();
|
|
149
211
|
let lastHeartbeatAt = null;
|
|
150
212
|
// Busy/idle: only a hash of the pane is kept (pane text can hold secrets
|
|
@@ -175,8 +237,12 @@ export async function runKeeper(opts) {
|
|
|
175
237
|
for (const row of rows) {
|
|
176
238
|
let outcome = handled.get(row.id);
|
|
177
239
|
if (!outcome) {
|
|
240
|
+
if (!received.has(row.id)) { received.add(row.id); log(`control ${row.command} ${row.id} received from ${row.requested_via || '?'}`); }
|
|
178
241
|
outcome = await runControlCommand(row);
|
|
242
|
+
// POKE-WAKE: a wake on a busy pane stays pending server-side and is retried next tick.
|
|
243
|
+
if (outcome === null) continue;
|
|
179
244
|
handled.set(row.id, outcome);
|
|
245
|
+
received.delete(row.id);
|
|
180
246
|
log(`control ${row.command} ${row.id} from ${row.requested_via || '?'}: ${outcome.status} (${outcome.result})`);
|
|
181
247
|
}
|
|
182
248
|
await ackControl(row.id, outcome.status, outcome.result);
|
|
@@ -225,8 +291,169 @@ export async function runKeeper(opts) {
|
|
|
225
291
|
parts.push(`keeper up ${fmtDur((now() - startedAt) / 1000)}`);
|
|
226
292
|
return parts.join(', ').slice(0, 500);
|
|
227
293
|
};
|
|
294
|
+
// POKE-WAKE: wake an idle session so it reads its slot page now. Types ONE fixed line (wakeText),
|
|
295
|
+
// literally (-l, no key-name parsing), only when the pane is still and the prompt box empty.
|
|
296
|
+
const wakeSeen = new Map();
|
|
297
|
+
const waitLogged = new Map();
|
|
298
|
+
let lastWokeAt = null;
|
|
299
|
+
const capture = async () => {
|
|
300
|
+
try {
|
|
301
|
+
// -e: escape-aware, so the faint empty-box placeholder is not mistaken for a draft (FORCE-WAKE).
|
|
302
|
+
const out = await execFile('tmux', ['capture-pane', '-p', '-e', '-t', tmuxPane]);
|
|
303
|
+
return typeof out === 'string' ? out : String(out?.stdout ?? '');
|
|
304
|
+
} catch { return null; }
|
|
305
|
+
};
|
|
306
|
+
const runWake = async (row) => {
|
|
307
|
+
if (!tmuxPane) return { status: 'failed', result: NO_TMUX_RESULT };
|
|
308
|
+
// The 10m budget runs from the REQUEST (server created_at), not from this keeper's first sight of
|
|
309
|
+
// it: a re-exec'd keeper continues the same budget, and an old request is not mistaken for a new one.
|
|
310
|
+
if (!wakeSeen.has(row.id)) {
|
|
311
|
+
const created = Date.parse(row.created_at || '');
|
|
312
|
+
wakeSeen.set(row.id, Number.isFinite(created) && created <= now() ? created : now());
|
|
313
|
+
}
|
|
314
|
+
const before = await capture();
|
|
315
|
+
await settle(WAKE_SETTLE_MS);
|
|
316
|
+
const after = await capture();
|
|
317
|
+
const force = HUMAN_VIA.has(row.requested_via);
|
|
318
|
+
const d = decideWake({ slot: name, before, after, firstSeenAt: wakeSeen.get(row.id), nowMs: now(), lastWokeAt, force });
|
|
319
|
+
if (d.action === 'wait') {
|
|
320
|
+
if (waitLogged.get(row.id) !== d.reason) { waitLogged.set(row.id, d.reason); log(`wake ${row.id} waiting: ${d.reason} (retrying each poll for up to 10m)`); }
|
|
321
|
+
return null;
|
|
322
|
+
}
|
|
323
|
+
wakeSeen.delete(row.id);
|
|
324
|
+
waitLogged.delete(row.id);
|
|
325
|
+
if (d.action === 'force') return forceWake(row, after);
|
|
326
|
+
if (d.action !== 'type' && d.action !== 'submit') return { status: d.action === 'done' ? 'done' : 'failed', result: d.result };
|
|
327
|
+
try {
|
|
328
|
+
if (d.action === 'submit') log(`wake ${row.id}: an earlier wake line is still unsent in the box; submitting it`);
|
|
329
|
+
const r = await submitWakeLine(row.id, { typed: d.action === 'submit' });
|
|
330
|
+
if (!r.ok) return { status: 'failed', result: `not woken: ${r.why}; the page shows at its next prompt` };
|
|
331
|
+
lastWokeAt = now();
|
|
332
|
+
return { status: 'done', result: wokeResult(r) };
|
|
333
|
+
} catch (err) {
|
|
334
|
+
return { status: 'failed', result: `tmux send-keys failed: ${err.message}` };
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
// WAKE-REAL: the two outcomes a waker is told apart: a turn started, or Claude Code queued the line mid-turn.
|
|
338
|
+
const wokeResult = (r, extra = '') => (r.queued
|
|
339
|
+
? `queued in ${tmuxPane}${extra}: the session is mid-turn, so Claude Code holds the wake line and sends it when the turn ends; it reads slot ${name} then`
|
|
340
|
+
: `woke ${tmuxPane}${extra}: the wake line was submitted and a turn started; it is reading slot ${name} now`);
|
|
341
|
+
// WAKE-REAL: type the fixed line (unless it is already in the box), let it land, Enter, and verify it left the
|
|
342
|
+
// box. Enter is re-pressed only while the box holds exactly this slot's wake line, so nothing else is ever sent.
|
|
343
|
+
// -> { ok: true, queued } | { ok: false, why }
|
|
344
|
+
const submitWakeLine = async (id, { typed = false, clearOnFail = true } = {}) => {
|
|
345
|
+
if (!typed) {
|
|
346
|
+
await keys('-l', wakeText(name));
|
|
347
|
+
for (let k = 0; k < WAKE_LAND_STEPS; k += 1) {
|
|
348
|
+
await settle(FORCE_STEP_MS);
|
|
349
|
+
const b = draftLines(await capture());
|
|
350
|
+
if (isWakeLine(b, name) || (Array.isArray(b) && b.length)) break;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
for (let t = 0; t < WAKE_ENTER_TRIES; t += 1) {
|
|
354
|
+
const b = draftLines(await capture());
|
|
355
|
+
if (Array.isArray(b) && b.length && !isWakeLine(b, name)) {
|
|
356
|
+
log(`wake ${id}: the box holds something other than the wake line; no Enter sent`);
|
|
357
|
+
return { ok: false, why: 'the prompt box changed while waking (someone is typing), so nothing was sent' };
|
|
358
|
+
}
|
|
359
|
+
if (t > 0 && !(Array.isArray(b) && b.length)) return { ok: true, queued: wakeQueued(await capture()) };
|
|
360
|
+
await keys('Enter');
|
|
361
|
+
if (t > 0) log(`wake ${id}: Enter was not taken; pressed it again (${t + 1} of ${WAKE_ENTER_TRIES})`);
|
|
362
|
+
for (let k = 0; k < WAKE_ENTER_WAIT_STEPS; k += 1) {
|
|
363
|
+
const screen = await capture();
|
|
364
|
+
const after = draftLines(screen);
|
|
365
|
+
if (Array.isArray(after) && !after.length) { log(`wake ${id}: wake line submitted (${t + 1} Enter${t ? 's' : ''})`); return { ok: true, queued: wakeQueued(screen) }; }
|
|
366
|
+
if (!isWakeLine(after, name)) break;
|
|
367
|
+
await settle(FORCE_STEP_MS);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
// Still ours in the box after every try: take it back out, so it never sits there as a fake draft (not on a
|
|
371
|
+
// force: C-u would overwrite the kill ring that holds the human's draft; the next wake submits it instead).
|
|
372
|
+
if (!clearOnFail) return { ok: false, why: `Claude Code did not take Enter (${WAKE_ENTER_TRIES} tries)` };
|
|
373
|
+
for (let k = 0; k < 12 && isWakeLine(draftLines(await capture()), name); k += 1) { await keys('C-u'); await settle(FORCE_STEP_MS); }
|
|
374
|
+
log(`wake ${id}: Claude Code did not take Enter after ${WAKE_ENTER_TRIES} tries; wake line cleared`);
|
|
375
|
+
return { ok: false, why: `Claude Code did not take Enter (${WAKE_ENTER_TRIES} tries), so the wake line was cleared from the box` };
|
|
376
|
+
};
|
|
377
|
+
// FORCE-WAKE (owner 2026-10-16: "wake needs to wake"): a human wake at a draft sets the draft aside and
|
|
378
|
+
// puts it back UNSENT. Proven live on Claude Code v2.1.288: C-u kills one visual line (consecutive kills
|
|
379
|
+
// accumulate in its kill ring), the ring survives a submit, and C-y yanks it back. Every step is checked
|
|
380
|
+
// by a capture; nothing of the user's is ever followed by Enter. The draft text never goes in the log;
|
|
381
|
+
// a private copy sits in <slot>.draft until it is verified back in the box.
|
|
382
|
+
const same = (a, b) => Array.isArray(a) && Array.isArray(b) && a.join('\n') === b.join('\n');
|
|
383
|
+
const tag = (lines) => `${lines.length} line(s), ${lines.join('\n').length} chars, sha ${createHash('sha256').update(lines.join('\n')).digest('hex').slice(0, 8)}`;
|
|
384
|
+
const keys = (...k) => execFile('tmux', ['send-keys', '-t', tmuxPane, ...k]);
|
|
385
|
+
const forceWake = async (row, screen) => {
|
|
386
|
+
const draft = draftLines(screen);
|
|
387
|
+
const draftPath = join(dir, `${name}.draft`);
|
|
388
|
+
const NOT = 'not woken: ';
|
|
389
|
+
try {
|
|
390
|
+
writeFileSync(draftPath, `${draft.join('\n')}\n`, { mode: 0o600 });
|
|
391
|
+
chmodSync(draftPath, 0o600);
|
|
392
|
+
} catch (e) {
|
|
393
|
+
log(`force ${row.id}: could not save a safety copy of the draft (${e.message}); refusing`);
|
|
394
|
+
return { status: 'failed', result: `${NOT}could not save a safety copy of the draft first, so it was left untouched; the page shows at its next prompt` };
|
|
395
|
+
}
|
|
396
|
+
log(`force ${row.id}: captured draft (${tag(draft)}); safety copy ${draftPath}`);
|
|
397
|
+
// A human may be typing: the box must still hold exactly what was captured.
|
|
398
|
+
if (!same(draftLines(await capture()), draft)) {
|
|
399
|
+
log(`force ${row.id}: draft changed before clearing; refusing`);
|
|
400
|
+
try { unlinkSync(draftPath); } catch { /* best effort */ }
|
|
401
|
+
return { status: 'failed', result: `${NOT}the draft changed while setting it aside (someone is typing); left untouched` };
|
|
402
|
+
}
|
|
403
|
+
try {
|
|
404
|
+
let box = draft;
|
|
405
|
+
for (let k = 0; k < draft.length + 3 && box && box.length; k += 1) {
|
|
406
|
+
await keys('C-u');
|
|
407
|
+
await settle(FORCE_STEP_MS);
|
|
408
|
+
box = draftLines(await capture());
|
|
409
|
+
}
|
|
410
|
+
if (!box || box.length) {
|
|
411
|
+
log(`force ${row.id}: box did not clear (${box ? 'text left' : 'no box'}); yanking the draft back, nothing sent`);
|
|
412
|
+
if (box && box.length) await keys('C-u'); // finish the kill run so the yank puts back the whole draft
|
|
413
|
+
await keys('C-y');
|
|
414
|
+
await settle(FORCE_STEP_MS);
|
|
415
|
+
const back = same(draftLines(await capture()), draft);
|
|
416
|
+
log(`force ${row.id}: draft ${back ? 'restored' : `NOT verified back; safety copy kept at ${draftPath}`}`);
|
|
417
|
+
if (back) { try { unlinkSync(draftPath); } catch { /* best effort */ } }
|
|
418
|
+
return { status: 'failed', result: `${NOT}could not clear the draft safely, so it was ${back ? 'put back as it was' : `left in place (a copy is saved at ${draftPath})`}; nothing was sent` };
|
|
419
|
+
}
|
|
420
|
+
log(`force ${row.id}: cleared the prompt box (${draft.length} C-u)`);
|
|
421
|
+
const sub = await submitWakeLine(row.id, { clearOnFail: false });
|
|
422
|
+
log(`force ${row.id}: typed wake line + Enter${sub.ok ? '' : ` (${sub.why})`}`);
|
|
423
|
+
let st = sub.ok ? 'empty' : null;
|
|
424
|
+
for (let k = 0; sub.ok && k < FORCE_SUBMIT_WAIT_STEPS && st !== 'empty'; k += 1) {
|
|
425
|
+
await settle(FORCE_STEP_MS);
|
|
426
|
+
st = promptBoxState(await capture());
|
|
427
|
+
}
|
|
428
|
+
if (st !== 'empty') {
|
|
429
|
+
log(`force ${row.id}: box not free after the wake line (${st}); draft NOT restored, safety copy kept at ${draftPath}`);
|
|
430
|
+
return { status: 'failed', result: `${NOT}the wake line did not leave the prompt box; your draft is saved at ${draftPath} on that host (in the box kill ring too: Ctrl+Y)` };
|
|
431
|
+
}
|
|
432
|
+
lastWokeAt = now();
|
|
433
|
+
await keys('C-y');
|
|
434
|
+
await settle(FORCE_STEP_MS);
|
|
435
|
+
let back = draftLines(await capture());
|
|
436
|
+
if (!same(back, draft) && Array.isArray(back) && !back.length && draft.length === 1) {
|
|
437
|
+
log(`force ${row.id}: yank did not restore; typing the single-line draft back literally (no Enter)`);
|
|
438
|
+
await keys('-l', draft[0]);
|
|
439
|
+
await settle(FORCE_STEP_MS);
|
|
440
|
+
back = draftLines(await capture());
|
|
441
|
+
}
|
|
442
|
+
if (!same(back, draft)) {
|
|
443
|
+
log(`force ${row.id}: woke, but the draft is NOT verified back; safety copy kept at ${draftPath}`);
|
|
444
|
+
return { status: 'done', result: `${wokeResult(sub)}; but its draft could not be verified back in the box, a copy is saved at ${draftPath} on that host` };
|
|
445
|
+
}
|
|
446
|
+
try { unlinkSync(draftPath); } catch { /* best effort */ }
|
|
447
|
+
log(`force ${row.id}: restored draft (verified, not sent)`);
|
|
448
|
+
return { status: 'done', result: wokeResult(sub, ' (its draft was set aside and put back unsent)') };
|
|
449
|
+
} catch (err) {
|
|
450
|
+
log(`force ${row.id}: tmux failed (${err.message}); safety copy kept at ${draftPath}`);
|
|
451
|
+
return { status: 'failed', result: `tmux send-keys failed mid-wake: ${err.message}; the draft is saved at ${draftPath} on that host` };
|
|
452
|
+
}
|
|
453
|
+
};
|
|
228
454
|
const runControlCommand = async (row) => {
|
|
229
455
|
if (row.command === 'status') return { status: 'done', result: await statusResult() };
|
|
456
|
+
if (row.command === 'wake') return runWake(row);
|
|
230
457
|
if (row.command !== 'compact') return { status: 'failed', result: `unsupported command ${row.command}` };
|
|
231
458
|
if (!tmuxPane) return { status: 'failed', result: NO_TMUX_RESULT };
|
|
232
459
|
try {
|
|
@@ -243,9 +470,11 @@ export async function runKeeper(opts) {
|
|
|
243
470
|
await sleep(CONTROL_POLL_MS);
|
|
244
471
|
remaining -= CONTROL_POLL_MS;
|
|
245
472
|
await controlTick();
|
|
473
|
+
if (staleBuild()) return { code: 0, reason: 'reexec' };
|
|
246
474
|
}
|
|
247
475
|
await sleep(remaining);
|
|
248
476
|
if (pollControl) await controlTick();
|
|
477
|
+
if (staleBuild()) return { code: 0, reason: 'reexec' };
|
|
249
478
|
|
|
250
479
|
if (claude && !sameProcess(probe, claude.pid, claude.starttime)) {
|
|
251
480
|
log(`claude session gone (pid ${claude.pid}); releasing ${name} and exiting`);
|
|
@@ -258,10 +487,26 @@ export async function runKeeper(opts) {
|
|
|
258
487
|
lastHeartbeatAt = now();
|
|
259
488
|
if (fails > 0) log(`heartbeat recovered after ${fails} failure(s)`);
|
|
260
489
|
fails = 0;
|
|
490
|
+
reacquires = 0;
|
|
261
491
|
log('heartbeat ok');
|
|
262
492
|
delay = KEEP_INTERVAL_MS * (1 + rand() * 0.2);
|
|
263
493
|
} catch (err) {
|
|
264
494
|
if (isHoldLost(err)) {
|
|
495
|
+
// #893: a lease that merely EXPIRED (e.g. auth stalled past the TTL)
|
|
496
|
+
// also answers 409. Try one non-takeover re-lease per lapse, at most
|
|
497
|
+
// MAX_REACQUIRES in a row without a good heartbeat in between.
|
|
498
|
+
if (reacquire && reacquires < MAX_REACQUIRES) {
|
|
499
|
+
reacquires += 1;
|
|
500
|
+
try {
|
|
501
|
+
await reacquire();
|
|
502
|
+
log(`hold lapsed (${err.status || ''} ${err.message}); re-acquired (${reacquires}/${MAX_REACQUIRES})`);
|
|
503
|
+
fails = 0;
|
|
504
|
+
delay = 0;
|
|
505
|
+
continue;
|
|
506
|
+
} catch (re) {
|
|
507
|
+
log(`re-acquire refused: ${re.status || ''} ${re.message}`);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
265
510
|
log(`HOLD LOST: ${name} is no longer held by ${session}; keeper exiting`);
|
|
266
511
|
try { forget(); } catch { /* best effort */ }
|
|
267
512
|
return { code: 0, reason: 'hold-lost' };
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POKE-WAKE (owner 2026-10-15: "/poke cli-site ... not listening right now ... this delay not ideal").
|
|
3
|
+
*
|
|
4
|
+
* A page is a slot message; an idle Claude Code session only reads slot mail when a turn starts
|
|
5
|
+
* (the UserPromptSubmit slot-inbox hook). Nothing starts a turn in an idle session except input.
|
|
6
|
+
* So `wake` is a slot CONTROL command (server-gated to a human org admin, never an agent) that the
|
|
7
|
+
* session's own keeper runs: it types ONE FIXED LINE into the session's tmux pane, and only when
|
|
8
|
+
* the pane is still and its prompt box is empty. The page text is never typed; the session reads
|
|
9
|
+
* it through the normal slot path. Pure decisions live here; the keeper does the I/O.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export const WAKE_TTL_MS = 10 * 60 * 1000; // a busy session is retried each control tick for this long
|
|
13
|
+
export const WAKE_COALESCE_MS = 60 * 1000; // repeat pokes inside this window do not type again
|
|
14
|
+
export const WAKE_SETTLE_MS = 2000; // two pane samples this far apart: any change = busy or a human typing
|
|
15
|
+
|
|
16
|
+
const SLOT_RE = /^[a-z0-9][a-z0-9._-]{0,63}$/i;
|
|
17
|
+
// A box rule; the TOP one may carry the session title ("──── QUOXON ─", seen live 2026-10-15).
|
|
18
|
+
const RULE_RE = /^\s*─{8,}(?:.*─)?\s*$/;
|
|
19
|
+
const PROMPT_RE = /^\s*❯(?:[\s ]|$)/;
|
|
20
|
+
// FORCE-WAKE: captures are escape-aware (`capture-pane -e`). Claude Code draws its empty-box suggestion FAINT
|
|
21
|
+
// (ESC[2m) straight after the prompt; plain capture read it as a draft (proven live, v2.1.288, 2026-10-16).
|
|
22
|
+
const ANSI_RE = /\x1b\[[0-9;?]*[A-Za-z]/g;
|
|
23
|
+
const PLACEHOLDER_RE = /❯[\s ]*(?:\x1b\[(?!2m)[0-9;]*m)*\x1b\[2m/;
|
|
24
|
+
const UNCAPTURABLE_RE = /\[(?:Pasted text|Image)\b/;
|
|
25
|
+
const plain = (line) => line.replace(ANSI_RE, '');
|
|
26
|
+
|
|
27
|
+
/** The only text a wake ever types. Names the slot, never carries the page. */
|
|
28
|
+
export function wakeText(slot) {
|
|
29
|
+
if (typeof slot !== 'string' || !SLOT_RE.test(slot)) throw new Error(`not a slot name: ${JSON.stringify(slot)}`);
|
|
30
|
+
return `[quox slot wake] a page is waiting on slot ${slot}: read it with \`quox slot read ${slot}\` and answer there with \`quox slot say ${slot}\`.`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const flat = (s) => String(s).replace(/\s+/g, '');
|
|
34
|
+
/** WAKE-REAL: do these box lines (draftLines) hold exactly this slot's wake line, as Claude Code wrapped it? */
|
|
35
|
+
export function isWakeLine(lines, slot) {
|
|
36
|
+
if (!Array.isArray(lines) || !lines.length || !SLOT_RE.test(String(slot || ''))) return false;
|
|
37
|
+
return flat(lines.join('')) === flat(wakeText(slot));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// WAKE-REAL: what Claude Code (v2.1.288, seen live 2026-10-17) shows while a submitted line waits for a running
|
|
41
|
+
// turn: the line above the box with "ctrl+x ctrl+s to send now", and "Press up to edit queued messages" in it.
|
|
42
|
+
const QUEUED_RE = /Press up to edit queued|ctrl\+x ctrl\+s to send now/;
|
|
43
|
+
/** Did the last submit land in Claude Code's queue (session mid-turn) rather than start a turn? */
|
|
44
|
+
export function wakeQueued(text) {
|
|
45
|
+
return typeof text === 'string' && QUEUED_RE.test(aboveFooter(text).split('\n').map(plain).join('\n'));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The state of the LAST Claude Code prompt box on screen: 'empty', 'draft' (typing would send a
|
|
50
|
+
* human's unsent text) or 'unknown' (no box: a dialog, menu or pager; never treated as empty).
|
|
51
|
+
*/
|
|
52
|
+
export function promptBoxState(text) {
|
|
53
|
+
const lines = draftLines(text);
|
|
54
|
+
if (lines === null) return 'unknown';
|
|
55
|
+
return lines.join('\n').replace(/\u00a0/g, ' ').trim() ? 'draft' : 'empty';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* FORCE-WAKE: the last prompt box's draft as rendered, one entry per visual line (plain text, prompt
|
|
60
|
+
* stripped, trailing spaces dropped); [] for an empty box (or the faint placeholder); null for no box.
|
|
61
|
+
*/
|
|
62
|
+
export function draftLines(text) {
|
|
63
|
+
if (typeof text !== 'string' || !text) return null;
|
|
64
|
+
const raw = text.split('\n');
|
|
65
|
+
const lines = raw.map(plain);
|
|
66
|
+
for (let i = lines.length - 1; i > 0; i -= 1) {
|
|
67
|
+
if (!PROMPT_RE.test(lines[i]) || !RULE_RE.test(lines[i - 1])) continue;
|
|
68
|
+
if (PLACEHOLDER_RE.test(raw[i])) return [];
|
|
69
|
+
const body = [lines[i].replace(PROMPT_RE, '')];
|
|
70
|
+
for (let j = i + 1; j < lines.length && !RULE_RE.test(lines[j]); j += 1) body.push(lines[j]);
|
|
71
|
+
const out = body.map((l) => l.replace(/\u00a0/g, ' ').replace(/\s+$/, ''));
|
|
72
|
+
while (out.length && !out[out.length - 1]) out.pop();
|
|
73
|
+
return out.join('').trim() ? out : [];
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** The screen down to the last prompt box's closing rule: the footer below it ticks (shell counts,
|
|
79
|
+
* agent timers) on an idle session, so it must not count as activity. No box = the whole screen. */
|
|
80
|
+
export function aboveFooter(text) {
|
|
81
|
+
if (typeof text !== 'string') return text;
|
|
82
|
+
const raw = text.split('\n');
|
|
83
|
+
const lines = raw.map(plain);
|
|
84
|
+
for (let i = lines.length - 1; i > 0; i -= 1) {
|
|
85
|
+
if (!PROMPT_RE.test(lines[i]) || !RULE_RE.test(lines[i - 1])) continue;
|
|
86
|
+
let j = i + 1;
|
|
87
|
+
while (j < lines.length && !RULE_RE.test(lines[j])) j += 1;
|
|
88
|
+
return raw.slice(0, Math.min(j + 1, raw.length)).join('\n');
|
|
89
|
+
}
|
|
90
|
+
return text;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* One wake attempt. before/after are two pane captures WAKE_SETTLE_MS apart (null = unreadable).
|
|
95
|
+
* force (FORCE-WAKE, human-requested rows only): a draft yields { action: 'force' } instead of a refusal.
|
|
96
|
+
* Returns { action: 'type' } | { action: 'force' } | { action: 'submit' } (our own stranded wake line) | { action: 'wait', reason } (leave the request pending) |
|
|
97
|
+
* { action: 'done' | 'fail', result }.
|
|
98
|
+
*/
|
|
99
|
+
export function decideWake({ slot, before, after, firstSeenAt, nowMs, lastWokeAt = null, force = false }) {
|
|
100
|
+
if (typeof before !== 'string' || typeof after !== 'string') return { action: 'fail', result: 'could not read the session pane' };
|
|
101
|
+
if (lastWokeAt != null && nowMs - lastWokeAt < WAKE_COALESCE_MS) return { action: 'done', result: `already woken ${Math.round((nowMs - lastWokeAt) / 1000)}s ago` };
|
|
102
|
+
const expired = nowMs - firstSeenAt > WAKE_TTL_MS;
|
|
103
|
+
if (aboveFooter(before) !== aboveFooter(after)) {
|
|
104
|
+
return expired ? { action: 'fail', result: `session still busy and the wake request is ${Math.floor((nowMs - firstSeenAt) / 60000)}m old (limit ${WAKE_TTL_MS / 60000}m); the page on ${slot} shows at its next prompt` } : { action: 'wait', reason: 'busy' };
|
|
105
|
+
}
|
|
106
|
+
const box = promptBoxState(after);
|
|
107
|
+
// WAKE-REAL: the box holds OUR wake line, typed earlier but never submitted (Claude Code dropped its Enter;
|
|
108
|
+
// QUOXON 2026-10-17 00:36:41, proven by sha c8d63692). It is not a human draft: submit it, never set it aside.
|
|
109
|
+
if (box === 'draft' && isWakeLine(draftLines(after), slot)) return { action: 'submit' };
|
|
110
|
+
if (box === 'draft' && force) {
|
|
111
|
+
// FORCE-WAKE: a human wake sets the draft aside and puts it back unsent; only if it can be put back exactly.
|
|
112
|
+
if (UNCAPTURABLE_RE.test(draftLines(after).join('\n'))) return { action: 'fail', result: 'not woken: the draft holds a collapsed pasted block or image, which cannot be set aside and put back exactly; the page shows at its next prompt' };
|
|
113
|
+
return { action: 'force' };
|
|
114
|
+
}
|
|
115
|
+
if (box === 'draft') return { action: 'fail', result: 'not woken: an unsent draft is in the prompt box (typing would send it); the page shows at its next prompt' };
|
|
116
|
+
if (box === 'unknown') {
|
|
117
|
+
return expired ? { action: 'fail', result: `no empty prompt and the wake request is ${Math.floor((nowMs - firstSeenAt) / 60000)}m old (a dialog or menu is open); the page shows at its next prompt` } : { action: 'wait', reason: 'no prompt box' };
|
|
118
|
+
}
|
|
119
|
+
return { action: 'type' };
|
|
120
|
+
}
|
|
@@ -278,18 +278,24 @@ export async function attemptSlotClaim({
|
|
|
278
278
|
return { claimed: false, reason: 'error', error: err };
|
|
279
279
|
}
|
|
280
280
|
claimState.held = false;
|
|
281
|
+
claimState.lapsed = true;
|
|
281
282
|
}
|
|
282
283
|
}
|
|
283
284
|
try {
|
|
285
|
+
const wasHeld = !!(claimState && claimState.everHeld);
|
|
284
286
|
const res = await checkoutSlot();
|
|
285
|
-
if (claimState) { claimState.held = true; claimState.claimedAt = now(); }
|
|
287
|
+
if (claimState) { claimState.held = true; claimState.everHeld = true; claimState.claimedAt = now(); }
|
|
286
288
|
const refreshed = !!(res && res.refreshed);
|
|
289
|
+
// GRIPE-797: log state transitions only. A periodic safety re-checkout
|
|
290
|
+
// of a hold this process already had is a renewal, not a new claim.
|
|
291
|
+
if (wasHeld && claimState && !claimState.lapsed) return { claimed: true, refreshed: true };
|
|
292
|
+
if (claimState) claimState.lapsed = false;
|
|
287
293
|
log('info', refreshed
|
|
288
294
|
? `slot ${slot}: already held by this resident, refreshed.`
|
|
289
295
|
: `slot ${slot}: claimed (was free), now present.`);
|
|
290
296
|
return refreshed ? { claimed: true, refreshed } : { claimed: true };
|
|
291
297
|
} catch (err) {
|
|
292
|
-
if (claimState) claimState.held = false;
|
|
298
|
+
if (claimState) { claimState.held = false; claimState.lapsed = true; }
|
|
293
299
|
if (err && err.status === 409) {
|
|
294
300
|
const holder = err.detail?.holder_session || null;
|
|
295
301
|
if (holder && holder !== selfIdentity) {
|
|
@@ -479,6 +485,40 @@ export function detectTurnError(result) {
|
|
|
479
485
|
return { type, message, category, signature };
|
|
480
486
|
}
|
|
481
487
|
|
|
488
|
+
/**
|
|
489
|
+
* GRIPE-797: an expired provider login (claude "Failed to authenticate:
|
|
490
|
+
* OAuth session expired and could not be refreshed", codex "not logged in"
|
|
491
|
+
* or a 401) is NOT a generic provider error: the fix is a human running a
|
|
492
|
+
* login command on this host. Returns {provider, command} or null.
|
|
493
|
+
*/
|
|
494
|
+
export function detectAuthExpiry(result) {
|
|
495
|
+
if (!result || result.timedOut || result.error) return null;
|
|
496
|
+
// Only a FAILED turn can be an expiry: a successful reply that merely
|
|
497
|
+
// talks about logins or 401s is a reply, never an alert.
|
|
498
|
+
if (!detectTurnError(result)) return null;
|
|
499
|
+
const parts = [result.text, result.stdout, result.stderr];
|
|
500
|
+
const raw = result.raw;
|
|
501
|
+
if (raw && typeof raw === 'object') {
|
|
502
|
+
parts.push(raw.result, raw.message, raw.error && (raw.error.message || raw.error));
|
|
503
|
+
}
|
|
504
|
+
const blob = parts.filter((x) => typeof x === 'string').join('\n');
|
|
505
|
+
if (!blob) return null;
|
|
506
|
+
if (/failed to authenticate|oauth session expired|could not be refreshed|invalid authentication credentials|please run \/login/i.test(blob)) {
|
|
507
|
+
return { provider: 'claude', command: 'claude /login' };
|
|
508
|
+
}
|
|
509
|
+
if (/not logged in|codex login|401 unauthorized|\b401\b[^\n]{0,40}(unauthorized|authentication)|token (has )?expired/i.test(blob)) {
|
|
510
|
+
const claudeish = /claude/i.test(blob) && !/codex/i.test(blob);
|
|
511
|
+
return claudeish ? { provider: 'claude', command: 'claude /login' } : { provider: 'codex', command: 'codex login' };
|
|
512
|
+
}
|
|
513
|
+
return null;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
export function buildAuthExpiryNoticeText(hostId, auth) {
|
|
517
|
+
const cmd = auth && auth.provider === 'codex' ? 'codex login' : 'claude /login';
|
|
518
|
+
const alt = auth && auth.provider === 'codex' ? '(or `claude /login`)' : '(or `codex login`)';
|
|
519
|
+
return `My login on ${hostId} has expired; run \`${cmd}\` ${alt} on ${hostId}.`;
|
|
520
|
+
}
|
|
521
|
+
|
|
482
522
|
/**
|
|
483
523
|
* One short, plain-language, single-sentence notice: no stack, no JSON, no
|
|
484
524
|
* raw provider payload. This is the ONE honest signal an operator gets per
|
|
@@ -644,6 +684,7 @@ export async function runPollOnce({
|
|
|
644
684
|
claimState = null,
|
|
645
685
|
postHeartbeat = null,
|
|
646
686
|
postTurnReceipt = null,
|
|
687
|
+
alertOwner = null,
|
|
647
688
|
state,
|
|
648
689
|
log = () => {},
|
|
649
690
|
now = () => Date.now(),
|
|
@@ -786,6 +827,29 @@ export async function runPollOnce({
|
|
|
786
827
|
// resident's own reply. Log the full error, record it in state for
|
|
787
828
|
// `status` to surface, and post at most one short plain-language
|
|
788
829
|
// notice per distinct error signature per hour.
|
|
830
|
+
const authExpiry = detectAuthExpiry(result);
|
|
831
|
+
if (authExpiry) {
|
|
832
|
+
log('error', `turn auth expiry for message ${message.id}: provider=${authExpiry.provider} (login expired; never posted raw)`);
|
|
833
|
+
await receipt({ outcome: 'error', message_id: message.id, message_from: message.from, duration_ms: durationMs, error_note: `auth_expired: ${authExpiry.provider}` });
|
|
834
|
+
const noticeText = buildAuthExpiryNoticeText(hostId, authExpiry);
|
|
835
|
+
nextState = {
|
|
836
|
+
...nextState,
|
|
837
|
+
last_turn_error: { type: 'auth_expired', message: noticeText, category: 'auth expiry', signature: `auth_expired:${authExpiry.provider}`, at: new Date(now()).toISOString() },
|
|
838
|
+
};
|
|
839
|
+
// ONE slot reply + ONE owner alert per expiry episode; the flag is
|
|
840
|
+
// cleared only by a successful reply below.
|
|
841
|
+
if (!nextState.auth_expiry_alerted_at) {
|
|
842
|
+
await postReply(noticeText);
|
|
843
|
+
if (alertOwner) {
|
|
844
|
+
try { await alertOwner(noticeText); } catch (err) { log('warn', `auth-expiry owner alert failed: ${err.message}`); }
|
|
845
|
+
}
|
|
846
|
+
nextState = { ...nextState, auth_expiry_alerted_at: new Date(now()).toISOString() };
|
|
847
|
+
log('warn', `posted auth-expiry notice and owner alert (once per episode): ${noticeText}`);
|
|
848
|
+
} else {
|
|
849
|
+
log('warn', 'auth-expiry notice suppressed (already alerted this episode; clears on next successful turn).');
|
|
850
|
+
}
|
|
851
|
+
continue;
|
|
852
|
+
}
|
|
789
853
|
const turnError = detectTurnError(result);
|
|
790
854
|
if (turnError) {
|
|
791
855
|
log('error', `turn error for message ${message.id}: type=${turnError.type} message=${turnError.message} (never posted to the channel)`);
|
|
@@ -813,6 +877,11 @@ export async function runPollOnce({
|
|
|
813
877
|
continue;
|
|
814
878
|
}
|
|
815
879
|
|
|
880
|
+
if (nextState.auth_expiry_alerted_at) {
|
|
881
|
+
const { auth_expiry_alerted_at: _cleared, ...rest } = nextState;
|
|
882
|
+
nextState = rest;
|
|
883
|
+
log('info', 'auth recovered: successful turn, auth-expiry alert re-armed.');
|
|
884
|
+
}
|
|
816
885
|
await postReply(replyText);
|
|
817
886
|
log('info', `replied to ${message.id} (${replyText.length} chars, session_id=${result.session_id || 'n/a'}, cost_usd=${result.total_cost_usd ?? 'n/a'})`);
|
|
818
887
|
await receipt({
|
package/lib/core/version.js
CHANGED
|
@@ -18,7 +18,7 @@ import { execFileSync } from 'node:child_process';
|
|
|
18
18
|
import { fileURLToPath } from 'node:url';
|
|
19
19
|
import { dirname, resolve } from 'node:path';
|
|
20
20
|
|
|
21
|
-
export const BASE_VERSION = '0.
|
|
21
|
+
export const BASE_VERSION = '0.4.0';
|
|
22
22
|
|
|
23
23
|
// The CLI source root (…/cli). version.js lives at cli/lib/core/, so two
|
|
24
24
|
// levels up is the tree whose HEAD identifies the running build.
|