quoxcli 0.0.0-stage → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +35 -0
- package/README.md +144 -2
- package/bin/quox.js +265 -0
- package/bin/quoxon.js +8 -0
- package/lib/api/endpoints/admin.js +151 -0
- package/lib/api/endpoints/aee.js +38 -0
- package/lib/api/endpoints/agent-attach.js +126 -0
- package/lib/api/endpoints/agent-schedules.js +26 -0
- package/lib/api/endpoints/agents.js +63 -0
- package/lib/api/endpoints/assistants.js +62 -0
- package/lib/api/endpoints/audit.js +14 -0
- package/lib/api/endpoints/auth.js +50 -0
- package/lib/api/endpoints/awareness.js +34 -0
- package/lib/api/endpoints/away.js +15 -0
- package/lib/api/endpoints/backup-schedules.js +20 -0
- package/lib/api/endpoints/backups.js +86 -0
- package/lib/api/endpoints/brain.js +140 -0
- package/lib/api/endpoints/budgets.js +62 -0
- package/lib/api/endpoints/calendarq.js +71 -0
- package/lib/api/endpoints/catalog.js +14 -0
- package/lib/api/endpoints/chat.js +61 -0
- package/lib/api/endpoints/consent.js +26 -0
- package/lib/api/endpoints/contactsq.js +68 -0
- package/lib/api/endpoints/conversations.js +46 -0
- package/lib/api/endpoints/custom-roles.js +92 -0
- package/lib/api/endpoints/divisions.js +56 -0
- package/lib/api/endpoints/domains.js +38 -0
- package/lib/api/endpoints/engagement.js +231 -0
- package/lib/api/endpoints/engine.js +11 -0
- package/lib/api/endpoints/entity.js +50 -0
- package/lib/api/endpoints/executions.js +45 -0
- package/lib/api/endpoints/fetchAllowlist.js +27 -0
- package/lib/api/endpoints/file.js +20 -0
- package/lib/api/endpoints/files.js +40 -0
- package/lib/api/endpoints/github.js +21 -0
- package/lib/api/endpoints/governance.js +43 -0
- package/lib/api/endpoints/hat.js +66 -0
- package/lib/api/endpoints/inbox.js +52 -0
- package/lib/api/endpoints/inboxq.js +120 -0
- package/lib/api/endpoints/index.js +242 -0
- package/lib/api/endpoints/invites.js +19 -0
- package/lib/api/endpoints/llm-policy.js +43 -0
- package/lib/api/endpoints/lucid.js +89 -0
- package/lib/api/endpoints/members.js +32 -0
- package/lib/api/endpoints/memory.js +94 -0
- package/lib/api/endpoints/mfa.js +32 -0
- package/lib/api/endpoints/mind.js +13 -0
- package/lib/api/endpoints/mon.js +184 -0
- package/lib/api/endpoints/notesq.js +43 -0
- package/lib/api/endpoints/objectives.js +68 -0
- package/lib/api/endpoints/orgs.js +38 -0
- package/lib/api/endpoints/plugins.js +152 -0
- package/lib/api/endpoints/powers.js +75 -0
- package/lib/api/endpoints/presence.js +33 -0
- package/lib/api/endpoints/prompts.js +69 -0
- package/lib/api/endpoints/qlarity.js +122 -0
- package/lib/api/endpoints/quoxbox.js +55 -0
- package/lib/api/endpoints/quoxchat.js +54 -0
- package/lib/api/endpoints/quoxflow.js +32 -0
- package/lib/api/endpoints/quoxplan.js +121 -0
- package/lib/api/endpoints/quoxseo.js +114 -0
- package/lib/api/endpoints/reach.js +47 -0
- package/lib/api/endpoints/router.js +17 -0
- package/lib/api/endpoints/runs.js +38 -0
- package/lib/api/endpoints/safety.js +53 -0
- package/lib/api/endpoints/scim.js +46 -0
- package/lib/api/endpoints/scopes.js +14 -0
- package/lib/api/endpoints/security.js +69 -0
- package/lib/api/endpoints/setup.js +90 -0
- package/lib/api/endpoints/skills.js +200 -0
- package/lib/api/endpoints/sso.js +67 -0
- package/lib/api/endpoints/stepup.js +20 -0
- package/lib/api/endpoints/tags.js +32 -0
- package/lib/api/endpoints/teams.js +56 -0
- package/lib/api/endpoints/telephony.js +46 -0
- package/lib/api/endpoints/tg.js +56 -0
- package/lib/api/endpoints/tools.js +50 -0
- package/lib/api/endpoints/traces.js +157 -0
- package/lib/api/endpoints/triggers.js +96 -0
- package/lib/api/endpoints/user-library.js +41 -0
- package/lib/api/endpoints/users.js +20 -0
- package/lib/api/endpoints/v1.js +585 -0
- package/lib/api/endpoints/vault.js +192 -0
- package/lib/api/endpoints/voice.js +22 -0
- package/lib/api/endpoints/volt.js +84 -0
- package/lib/api/endpoints/ward.js +47 -0
- package/lib/api/endpoints/webhooks.js +46 -0
- package/lib/api/endpoints/workflows.js +169 -0
- package/lib/api/endpoints/x.js +32 -0
- package/lib/api/endpoints.js +15 -0
- package/lib/commands/actions.js +61 -0
- package/lib/commands/admin.js +375 -0
- package/lib/commands/aee.js +99 -0
- package/lib/commands/agent.js +337 -0
- package/lib/commands/agentAttach.js +1528 -0
- package/lib/commands/approvals.js +119 -0
- package/lib/commands/aside.js +24 -0
- package/lib/commands/ask.js +144 -0
- package/lib/commands/assistant.js +169 -0
- package/lib/commands/auth.js +464 -0
- package/lib/commands/awareness.js +88 -0
- package/lib/commands/backup.js +377 -0
- package/lib/commands/brain.js +1085 -0
- package/lib/commands/budget.js +241 -0
- package/lib/commands/build.js +170 -0
- package/lib/commands/calendar.js +216 -0
- package/lib/commands/capabilities.js +34 -0
- package/lib/commands/chat.js +295 -0
- package/lib/commands/claude.js +380 -0
- package/lib/commands/config-cmd.js +106 -0
- package/lib/commands/consent.js +84 -0
- package/lib/commands/contacts.js +1016 -0
- package/lib/commands/contactsSelftest.js +132 -0
- package/lib/commands/conversation.js +105 -0
- package/lib/commands/dashboard.js +43 -0
- package/lib/commands/department.js +96 -0
- package/lib/commands/dev.js +620 -0
- package/lib/commands/devResident.js +326 -0
- package/lib/commands/devSupervisor.js +1062 -0
- package/lib/commands/discord.js +132 -0
- package/lib/commands/doctor.js +181 -0
- package/lib/commands/domain.js +114 -0
- package/lib/commands/done.js +347 -0
- package/lib/commands/engagement.js +597 -0
- package/lib/commands/entity.js +119 -0
- package/lib/commands/facebook.js +212 -0
- package/lib/commands/fetchAllowlist.js +71 -0
- package/lib/commands/file.js +165 -0
- package/lib/commands/fleet.js +1189 -0
- package/lib/commands/governance.js +647 -0
- package/lib/commands/gripe.js +355 -0
- package/lib/commands/hat.js +374 -0
- package/lib/commands/helpers.js +96 -0
- package/lib/commands/hyperion.js +58 -0
- package/lib/commands/inbox.js +173 -0
- package/lib/commands/instagram.js +264 -0
- package/lib/commands/integration.js +100 -0
- package/lib/commands/inventory.js +115 -0
- package/lib/commands/invites.js +45 -0
- package/lib/commands/licence.js +139 -0
- package/lib/commands/linkedin.js +178 -0
- package/lib/commands/llm-policy.js +117 -0
- package/lib/commands/llm.js +45 -0
- package/lib/commands/lucid.js +597 -0
- package/lib/commands/mail.js +531 -0
- package/lib/commands/memory.js +361 -0
- package/lib/commands/mind.js +71 -0
- package/lib/commands/mon.js +581 -0
- package/lib/commands/monitoring.js +86 -0
- package/lib/commands/motion.js +22 -0
- package/lib/commands/notes.js +1246 -0
- package/lib/commands/notesBatch.js +160 -0
- package/lib/commands/notesBridges.js +56 -0
- package/lib/commands/notesLinks.js +60 -0
- package/lib/commands/notification.js +57 -0
- package/lib/commands/nucleus.js +123 -0
- package/lib/commands/objective.js +240 -0
- package/lib/commands/offload.js +89 -0
- package/lib/commands/onboard.js +329 -0
- package/lib/commands/open.js +56 -0
- package/lib/commands/org.js +276 -0
- package/lib/commands/peek.js +28 -0
- package/lib/commands/plan.js +944 -0
- package/lib/commands/plugin-doctor.js +1091 -0
- package/lib/commands/plugin.js +649 -0
- package/lib/commands/policy.js +78 -0
- package/lib/commands/powers.js +296 -0
- package/lib/commands/presence.js +89 -0
- package/lib/commands/prompt.js +350 -0
- package/lib/commands/provider.js +159 -0
- package/lib/commands/qlarity.js +706 -0
- package/lib/commands/quoxbox.js +270 -0
- package/lib/commands/quoxchat.js +275 -0
- package/lib/commands/quoxseo.js +378 -0
- package/lib/commands/reach.js +159 -0
- package/lib/commands/role.js +286 -0
- package/lib/commands/router.js +77 -0
- package/lib/commands/rows.js +358 -0
- package/lib/commands/run.js +187 -0
- package/lib/commands/safety.js +107 -0
- package/lib/commands/scim.js +88 -0
- package/lib/commands/scope.js +58 -0
- package/lib/commands/security.js +157 -0
- package/lib/commands/server.js +292 -0
- package/lib/commands/service.js +89 -0
- package/lib/commands/settings.js +99 -0
- package/lib/commands/setup.js +569 -0
- package/lib/commands/skillManifest.js +113 -0
- package/lib/commands/skillRightsFormat.js +38 -0
- package/lib/commands/skills.js +687 -0
- package/lib/commands/slot.js +1451 -0
- package/lib/commands/sso.js +229 -0
- package/lib/commands/status.js +137 -0
- package/lib/commands/tag.js +91 -0
- package/lib/commands/team.js +142 -0
- package/lib/commands/tg.js +240 -0
- package/lib/commands/tiktok.js +218 -0
- package/lib/commands/tips.js +23 -0
- package/lib/commands/tool.js +304 -0
- package/lib/commands/trace.js +530 -0
- package/lib/commands/trigger.js +223 -0
- package/lib/commands/update.js +46 -0
- package/lib/commands/vault.js +1447 -0
- package/lib/commands/vm.js +237 -0
- package/lib/commands/voice.js +213 -0
- package/lib/commands/volt.js +570 -0
- package/lib/commands/walkthrough.js +32 -0
- package/lib/commands/watch.js +167 -0
- package/lib/commands/webhook.js +137 -0
- package/lib/commands/welcome.js +14 -0
- package/lib/commands/workflow.js +660 -0
- package/lib/commands/x.js +448 -0
- package/lib/core/actingAs.js +136 -0
- package/lib/core/activeSummary.js +186 -0
- package/lib/core/aee.js +53 -0
- package/lib/core/agentAlias.js +163 -0
- package/lib/core/approvalContext.js +61 -0
- package/lib/core/aside.js +54 -0
- package/lib/core/askDecision.js +60 -0
- package/lib/core/auth.js +501 -0
- package/lib/core/banner.js +143 -0
- package/lib/core/brainPrime.js +90 -0
- package/lib/core/callerSource.js +129 -0
- package/lib/core/claudeLs.js +94 -0
- package/lib/core/cliBuild.js +62 -0
- package/lib/core/client.js +693 -0
- package/lib/core/commandRegistrars.js +121 -0
- package/lib/core/completions.js +163 -0
- package/lib/core/config.js +669 -0
- package/lib/core/confirm.js +37 -0
- package/lib/core/customHats.js +168 -0
- package/lib/core/deeplinks.js +76 -0
- package/lib/core/doneEngineFetch.js +227 -0
- package/lib/core/errors.js +441 -0
- package/lib/core/global-flags.js +190 -0
- package/lib/core/governedSession.js +402 -0
- package/lib/core/hyperion.js +102 -0
- package/lib/core/inboxResolution.js +65 -0
- package/lib/core/lazyCommands.js +122 -0
- package/lib/core/liveSessionsReport.js +112 -0
- package/lib/core/offload.js +504 -0
- package/lib/core/org-override.js +52 -0
- package/lib/core/orgResolve.js +70 -0
- package/lib/core/output.js +239 -0
- package/lib/core/peek.js +126 -0
- package/lib/core/prodEnv.js +7 -0
- package/lib/core/profileUsers.js +51 -0
- package/lib/core/promptCancel.js +18 -0
- package/lib/core/quoxglyph.js +158 -0
- package/lib/core/repoPath.js +11 -0
- package/lib/core/residentAlarm.js +282 -0
- package/lib/core/residentAuth.js +243 -0
- package/lib/core/residentPump.js +229 -0
- package/lib/core/residentSeed.js +106 -0
- package/lib/core/rootOptions.js +39 -0
- package/lib/core/router.js +168 -0
- package/lib/core/selfUpdate.js +188 -0
- package/lib/core/sessionHeartbeat.js +192 -0
- package/lib/core/sessionsOverview.js +224 -0
- package/lib/core/slotKeeper.js +527 -0
- package/lib/core/slotWake.js +120 -0
- package/lib/core/supervisorEngine.js +933 -0
- package/lib/core/systemdInstaller.js +256 -0
- package/lib/core/version.js +87 -0
- package/lib/core/vm.js +361 -0
- package/lib/core/vmTui.js +86 -0
- package/lib/core/walkthrough.js +35 -0
- package/lib/hyperion/deck.js +29 -0
- package/lib/lucid/LucidCard.jsx +68 -0
- package/lib/lucid/LucidIntro.jsx +92 -0
- package/lib/lucid/blind.js +71 -0
- package/lib/lucid/cards.js +86 -0
- package/lib/lucid/deck.js +32 -0
- package/lib/lucid/fmt.js +139 -0
- package/lib/lucid/markdown.js +81 -0
- package/lib/lucid/motion.js +56 -0
- package/lib/lucid/sitrep.js +173 -0
- package/lib/plugins/loader.js +93 -0
- package/lib/quoxon/activity.js +116 -0
- package/lib/quoxon/aocl.js +117 -0
- package/lib/quoxon/api.js +952 -0
- package/lib/quoxon/app.js +4032 -0
- package/lib/quoxon/approvalFacts.js +94 -0
- package/lib/quoxon/approvalsView.js +325 -0
- package/lib/quoxon/asideView.js +40 -0
- package/lib/quoxon/ask.js +121 -0
- package/lib/quoxon/away.js +52 -0
- package/lib/quoxon/bang.js +224 -0
- package/lib/quoxon/bulkRisk.js +17 -0
- package/lib/quoxon/capabilities.js +82 -0
- package/lib/quoxon/cells.js +53 -0
- package/lib/quoxon/commandShort.js +112 -0
- package/lib/quoxon/commands.js +479 -0
- package/lib/quoxon/connect.js +654 -0
- package/lib/quoxon/connectCard.js +45 -0
- package/lib/quoxon/consent.js +86 -0
- package/lib/quoxon/context.js +68 -0
- package/lib/quoxon/copy.js +34 -0
- package/lib/quoxon/countdownLine.js +11 -0
- package/lib/quoxon/demo.autoplay.js +29 -0
- package/lib/quoxon/demo.fixtures.js +24 -0
- package/lib/quoxon/demo.js +76 -0
- package/lib/quoxon/devObserver.js +20 -0
- package/lib/quoxon/dividerAocl.js +191 -0
- package/lib/quoxon/dividerRule.js +57 -0
- package/lib/quoxon/dragonModes.js +150 -0
- package/lib/quoxon/dragonView.js +41 -0
- package/lib/quoxon/earlyInput.js +40 -0
- package/lib/quoxon/echo.js +15 -0
- package/lib/quoxon/egg.js +36 -0
- package/lib/quoxon/events.js +677 -0
- package/lib/quoxon/feedback.js +50 -0
- package/lib/quoxon/gallery.js +237 -0
- package/lib/quoxon/handoffOffer.js +78 -0
- package/lib/quoxon/hatSwitch.js +37 -0
- package/lib/quoxon/headless.js +161 -0
- package/lib/quoxon/hostUrls.js +41 -0
- package/lib/quoxon/indicator.js +197 -0
- package/lib/quoxon/intent.js +52 -0
- package/lib/quoxon/keys.js +164 -0
- package/lib/quoxon/launcher.js +103 -0
- package/lib/quoxon/main.js +176 -0
- package/lib/quoxon/markdown.js +341 -0
- package/lib/quoxon/markdownSettle.js +19 -0
- package/lib/quoxon/matrixDomains.json +46 -0
- package/lib/quoxon/mentions.js +90 -0
- package/lib/quoxon/mind.js +103 -0
- package/lib/quoxon/mode.js +46 -0
- package/lib/quoxon/model.js +311 -0
- package/lib/quoxon/modelPrefs.js +345 -0
- package/lib/quoxon/motion/clock.js +70 -0
- package/lib/quoxon/motion/color.js +35 -0
- package/lib/quoxon/motion/frame.js +14 -0
- package/lib/quoxon/motion/level.js +91 -0
- package/lib/quoxon/motion/reveal.js +54 -0
- package/lib/quoxon/motion/ring.js +79 -0
- package/lib/quoxon/mouse.js +211 -0
- package/lib/quoxon/noai.js +57 -0
- package/lib/quoxon/objects/agent.js +61 -0
- package/lib/quoxon/objects/away.js +72 -0
- package/lib/quoxon/objects/crystal.js +100 -0
- package/lib/quoxon/objects/fetch.js +182 -0
- package/lib/quoxon/objects/fleet.js +149 -0
- package/lib/quoxon/objects/health.js +186 -0
- package/lib/quoxon/objects/index.js +106 -0
- package/lib/quoxon/objects/ladder.js +71 -0
- package/lib/quoxon/objects/memory.js +44 -0
- package/lib/quoxon/objects/mirror.js +137 -0
- package/lib/quoxon/objects/model.js +118 -0
- package/lib/quoxon/objects/peek.js +17 -0
- package/lib/quoxon/objects/proof.js +110 -0
- package/lib/quoxon/objects/prove.js +130 -0
- package/lib/quoxon/objects/replay.js +125 -0
- package/lib/quoxon/objects/sitrep.js +58 -0
- package/lib/quoxon/objects/skill.js +105 -0
- package/lib/quoxon/objects/view.js +87 -0
- package/lib/quoxon/objects/workflow.js +119 -0
- package/lib/quoxon/observers.js +42 -0
- package/lib/quoxon/onboarding/copy.js +263 -0
- package/lib/quoxon/onboarding/credential.js +108 -0
- package/lib/quoxon/onboarding/detect.js +22 -0
- package/lib/quoxon/onboarding/dockerOffer.js +45 -0
- package/lib/quoxon/onboarding/failureCause.js +33 -0
- package/lib/quoxon/onboarding/finish.js +134 -0
- package/lib/quoxon/onboarding/index.js +94 -0
- package/lib/quoxon/onboarding/installRunner.js +284 -0
- package/lib/quoxon/onboarding/intent.js +53 -0
- package/lib/quoxon/onboarding/journey.js +73 -0
- package/lib/quoxon/onboarding/preflight.js +156 -0
- package/lib/quoxon/onboarding/probe.js +65 -0
- package/lib/quoxon/onboarding/runner.js +58 -0
- package/lib/quoxon/onboarding/view.js +682 -0
- package/lib/quoxon/orgLabel.js +10 -0
- package/lib/quoxon/orgSwitch.js +33 -0
- package/lib/quoxon/ownBroadcast.js +22 -0
- package/lib/quoxon/palette.js +43 -0
- package/lib/quoxon/print.js +98 -0
- package/lib/quoxon/profileSwitch.js +53 -0
- package/lib/quoxon/promptHistory.js +41 -0
- package/lib/quoxon/providers.js +48 -0
- package/lib/quoxon/qart.js +348 -0
- package/lib/quoxon/qdragon.js +410 -0
- package/lib/quoxon/qdragonFly.js +61 -0
- package/lib/quoxon/qdragonScenes.js +282 -0
- package/lib/quoxon/qeyes.js +213 -0
- package/lib/quoxon/qglyph.js +143 -0
- package/lib/quoxon/qhello.js +58 -0
- package/lib/quoxon/qmotion.js +132 -0
- package/lib/quoxon/render.js +1167 -0
- package/lib/quoxon/resend.js +72 -0
- package/lib/quoxon/roster.js +307 -0
- package/lib/quoxon/route.js +39 -0
- package/lib/quoxon/rows.js +711 -0
- package/lib/quoxon/rowsStore.js +80 -0
- package/lib/quoxon/rules.js +130 -0
- package/lib/quoxon/screenContext.js +150 -0
- package/lib/quoxon/screenFrames.js +254 -0
- package/lib/quoxon/session.js +238 -0
- package/lib/quoxon/sessionDead.js +17 -0
- package/lib/quoxon/signin.js +125 -0
- package/lib/quoxon/skills.js +30 -0
- package/lib/quoxon/slash/agents.js +112 -0
- package/lib/quoxon/slash/argSources.js +65 -0
- package/lib/quoxon/slash/args.js +65 -0
- package/lib/quoxon/slash/automate.js +89 -0
- package/lib/quoxon/slash/broadcast.js +140 -0
- package/lib/quoxon/slash/broadcastRun.js +108 -0
- package/lib/quoxon/slash/card.js +30 -0
- package/lib/quoxon/slash/challenge.js +114 -0
- package/lib/quoxon/slash/cliCommand.js +53 -0
- package/lib/quoxon/slash/cliLeaves.json +948 -0
- package/lib/quoxon/slash/compact.js +90 -0
- package/lib/quoxon/slash/confirm.js +30 -0
- package/lib/quoxon/slash/crystallize.js +69 -0
- package/lib/quoxon/slash/dashboard.js +136 -0
- package/lib/quoxon/slash/draft.js +54 -0
- package/lib/quoxon/slash/hatWalk.js +206 -0
- package/lib/quoxon/slash/inverses.js +77 -0
- package/lib/quoxon/slash/killer.js +24 -0
- package/lib/quoxon/slash/killerApi.js +16 -0
- package/lib/quoxon/slash/ledger.js +88 -0
- package/lib/quoxon/slash/listView.js +30 -0
- package/lib/quoxon/slash/modelCmd.js +261 -0
- package/lib/quoxon/slash/palette.js +118 -0
- package/lib/quoxon/slash/paletteController.js +169 -0
- package/lib/quoxon/slash/panels.js +116 -0
- package/lib/quoxon/slash/picker.js +56 -0
- package/lib/quoxon/slash/restart.js +202 -0
- package/lib/quoxon/slash/resume.js +59 -0
- package/lib/quoxon/slash/rowCmd.js +157 -0
- package/lib/quoxon/slash/rowWalk.js +175 -0
- package/lib/quoxon/slash/settings.js +60 -0
- package/lib/quoxon/slash/spawn.js +177 -0
- package/lib/quoxon/slash/stepUp.js +274 -0
- package/lib/quoxon/slash/stepUpCard.js +60 -0
- package/lib/quoxon/slash/stepUpCmds.js +78 -0
- package/lib/quoxon/slash/undoApi.js +38 -0
- package/lib/quoxon/slash/undoCmd.js +49 -0
- package/lib/quoxon/slash/undoWish.js +15 -0
- package/lib/quoxon/slash/watch.js +164 -0
- package/lib/quoxon/slash/wish.js +23 -0
- package/lib/quoxon/slash/wishCmd.js +32 -0
- package/lib/quoxon/slotChat.js +300 -0
- package/lib/quoxon/slotFormat.js +195 -0
- package/lib/quoxon/slotRun.js +131 -0
- package/lib/quoxon/sound/index.js +186 -0
- package/lib/quoxon/sound/mapping.js +120 -0
- package/lib/quoxon/sound/output.js +52 -0
- package/lib/quoxon/sound/synth.js +122 -0
- package/lib/quoxon/stray.js +22 -0
- package/lib/quoxon/suggestCommand.js +59 -0
- package/lib/quoxon/term.js +141 -0
- package/lib/quoxon/textBuffer.js +416 -0
- package/lib/quoxon/theme.js +286 -0
- package/lib/quoxon/tips.js +433 -0
- package/lib/quoxon/toolError.js +41 -0
- package/lib/quoxon/transcript.js +188 -0
- package/lib/quoxon/web/qface.js +89 -0
- package/lib/quoxon/welcome.js +52 -0
- package/lib/quoxon/whatsnew.js +136 -0
- package/lib/quoxon/whatsnew.jsonl +89 -0
- package/lib/quoxon/windowOrg.js +113 -0
- package/lib/server/diag.js +129 -0
- package/lib/server/doctor.js +71 -0
- package/lib/server/install.js +149 -0
- package/lib/server/pluginEnable.js +161 -0
- package/lib/server/skew.js +56 -0
- package/lib/server/snapshot.js +131 -0
- package/lib/server/status.js +128 -0
- package/lib/server/sys.js +65 -0
- package/lib/server/uninstall.js +63 -0
- package/lib/server/update.js +234 -0
- package/lib/toolOutputShape.js +315 -0
- package/lib/tui/app.jsx +289 -0
- package/lib/tui/components/Breadcrumb.jsx +42 -0
- package/lib/tui/components/ChatInput.jsx +68 -0
- package/lib/tui/components/Confirm.jsx +32 -0
- package/lib/tui/components/DataTable.jsx +270 -0
- package/lib/tui/components/DetailPanel.jsx +241 -0
- package/lib/tui/components/HelpOverlay.jsx +67 -0
- package/lib/tui/components/Layout.jsx +129 -0
- package/lib/tui/components/Palette.jsx +141 -0
- package/lib/tui/components/SearchInput.jsx +125 -0
- package/lib/tui/components/Sparkline.jsx +55 -0
- package/lib/tui/components/StatusBar.jsx +133 -0
- package/lib/tui/components/Toast.jsx +52 -0
- package/lib/tui/hooks/useApi.js +44 -0
- package/lib/tui/hooks/useHealth.js +90 -0
- package/lib/tui/hooks/usePolledApi.js +86 -0
- package/lib/tui/hooks/useTerminalSize.js +46 -0
- package/lib/tui/index.js +56 -0
- package/lib/tui/intro/Intro.jsx +332 -0
- package/lib/tui/jsx-loader.js +32 -0
- package/lib/tui/keys.js +87 -0
- package/lib/tui/registry.js +49 -0
- package/lib/tui/theme.js +103 -0
- package/lib/tui/views/AdminView.jsx +398 -0
- package/lib/tui/views/AgentView.jsx +204 -0
- package/lib/tui/views/BackupView.jsx +225 -0
- package/lib/tui/views/BrainView.jsx +223 -0
- package/lib/tui/views/ChatView.jsx +468 -0
- package/lib/tui/views/FilesView.jsx +218 -0
- package/lib/tui/views/FleetView.jsx +234 -0
- package/lib/tui/views/GovernanceView.jsx +453 -0
- package/lib/tui/views/HealthView.jsx +216 -0
- package/lib/tui/views/HyperionDeck.jsx +79 -0
- package/lib/tui/views/InboxView.jsx +228 -0
- package/lib/tui/views/LucidDeck.jsx +719 -0
- package/lib/tui/views/MainMenu.jsx +123 -0
- package/lib/tui/views/MemoryView.jsx +377 -0
- package/lib/tui/views/ObjectivesView.jsx +230 -0
- package/lib/tui/views/OrgView.jsx +229 -0
- package/lib/tui/views/ToolView.jsx +221 -0
- package/lib/tui/views/VaultView.jsx +422 -0
- package/lib/tui/views/WorkflowView.jsx +329 -0
- package/package.json +75 -4
|
@@ -0,0 +1,527 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slot hold keeper (SLOTS-PERFECT, quox#662/B).
|
|
3
|
+
*
|
|
4
|
+
* Why: the collector frees a held slot 15 minutes after its last heartbeat,
|
|
5
|
+
* and the only thing heartbeating was a `quox slot wait` the session had to
|
|
6
|
+
* remember to re-arm. A session that forgot lost its hold silently. The
|
|
7
|
+
* keeper is a detached background loop, spawned by `slot checkout`, that
|
|
8
|
+
* heartbeats on a timer and dies with the Claude session it serves.
|
|
9
|
+
*
|
|
10
|
+
* Loop hygiene (house rule): failures are counted distinctly from successes,
|
|
11
|
+
* back off exponentially (30s doubling, cap 5 min) and the keeper retires
|
|
12
|
+
* after a bounded run of consecutive failures (24; 3 for 401). Everything
|
|
13
|
+
* environmental (clock, sleep, probe, signals, fs dir) is injected.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { decideWake, wakeText, draftLines, promptBoxState, isWakeLine, wakeQueued, WAKE_SETTLE_MS } from './slotWake.js';
|
|
17
|
+
import {
|
|
18
|
+
openSync, closeSync, writeSync, readFileSync, unlinkSync, appendFileSync,
|
|
19
|
+
statSync, writeFileSync, mkdirSync, chmodSync,
|
|
20
|
+
} from 'node:fs';
|
|
21
|
+
import { homedir } from 'node:os';
|
|
22
|
+
import { join } from 'node:path';
|
|
23
|
+
import { execFile as execFileCb } from 'node:child_process';
|
|
24
|
+
import { createHash } from 'node:crypto';
|
|
25
|
+
import { promisify } from 'node:util';
|
|
26
|
+
import { QuoxApiError } from './errors.js';
|
|
27
|
+
|
|
28
|
+
export const KEEP_INTERVAL_MS = 5 * 60 * 1000;
|
|
29
|
+
export const BACKOFF_BASE_MS = 30 * 1000;
|
|
30
|
+
export const BACKOFF_CAP_MS = 5 * 60 * 1000;
|
|
31
|
+
// 24 consecutive failures at the 5-min backoff cap rides out ~2h of
|
|
32
|
+
// collector/auth outage; each failure is one cheap HTTP call, so outlasting a
|
|
33
|
+
// long outage beats letting the hold lapse (the collector revives a stale
|
|
34
|
+
// slot for its own holder on the next successful heartbeat).
|
|
35
|
+
export const MAX_FAILS = 24;
|
|
36
|
+
// #893: consecutive re-leases allowed without a good heartbeat in between.
|
|
37
|
+
export const MAX_REACQUIRES = 3;
|
|
38
|
+
export const MAX_AUTH_FAILS = 3;
|
|
39
|
+
export const LOG_MAX_BYTES = 256 * 1024;
|
|
40
|
+
// QUOX-CLAUDE-3: the control queue is polled far more often than the 5-min
|
|
41
|
+
// heartbeat so `!compact` feels immediate. Poll failures back off (doubling,
|
|
42
|
+
// capped at CONTROL_BACKOFF_CAP_TICKS skipped ticks) and are counted
|
|
43
|
+
// distinctly from heartbeat failures; they never retire the keeper (the
|
|
44
|
+
// heartbeat owns hold-loss).
|
|
45
|
+
export const CONTROL_POLL_MS = 20 * 1000;
|
|
46
|
+
export const CONTROL_BACKOFF_CAP_TICKS = 15;
|
|
47
|
+
export const BUSY_WINDOW_MS = 60 * 1000;
|
|
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
|
|
61
|
+
|
|
62
|
+
export function keeperDir() {
|
|
63
|
+
return join(homedir(), '.quox', 'keepers');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Real process probe: liveness via kill -0, start ticks via /proc. */
|
|
67
|
+
export function makeProcessProbe(procStartTime) {
|
|
68
|
+
return {
|
|
69
|
+
alive(pid) {
|
|
70
|
+
try { process.kill(pid, 0); return true; } catch (e) { return e && e.code === 'EPERM'; }
|
|
71
|
+
},
|
|
72
|
+
starttime: procStartTime,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Is `pid` the same process it was (alive, and start ticks unchanged)? */
|
|
77
|
+
export function sameProcess(probe, pid, starttime) {
|
|
78
|
+
if (!pid || !probe.alive(pid)) return false;
|
|
79
|
+
return starttime == null || String(probe.starttime(pid)) === String(starttime);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function isHoldLost(err) {
|
|
83
|
+
if (!(err instanceof QuoxApiError)) return false;
|
|
84
|
+
// 409: held by someone else or freed. 404: the slot itself is gone.
|
|
85
|
+
if (err.status === 409 || err.status === 404) return true;
|
|
86
|
+
const said = [err.message, err.code, err.body?.error].filter(Boolean).join(' ');
|
|
87
|
+
return err.status === 403 && /not (the current |slot )?holder/i.test(said);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const execFileP = promisify(execFileCb);
|
|
91
|
+
function defaultExecFile(file, args) {
|
|
92
|
+
return execFileP(file, args, { timeout: 10000 });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export async function runKeeper(opts) {
|
|
96
|
+
const {
|
|
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,
|
|
101
|
+
sleep = (ms) => new Promise((r) => setTimeout(r, ms)),
|
|
102
|
+
rand = Math.random,
|
|
103
|
+
dir = keeperDir(),
|
|
104
|
+
// Remote control (all optional; absent pollControl = heartbeat-only keeper).
|
|
105
|
+
pollControl, ackControl, tmuxPane = null,
|
|
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),
|
|
114
|
+
now = () => Date.now(),
|
|
115
|
+
installSignals = (cleanup) => {
|
|
116
|
+
const h = () => { cleanup(); process.exit(0); };
|
|
117
|
+
process.once('SIGTERM', h);
|
|
118
|
+
process.once('SIGINT', h);
|
|
119
|
+
},
|
|
120
|
+
} = opts;
|
|
121
|
+
|
|
122
|
+
mkdirSync(dir, { recursive: true });
|
|
123
|
+
const lockPath = join(dir, `${name}.pid`);
|
|
124
|
+
const buildPath = join(dir, `${name}.build`);
|
|
125
|
+
const startBuild = buildId();
|
|
126
|
+
const logPath = join(dir, `${name}.log`);
|
|
127
|
+
const log = (msg) => {
|
|
128
|
+
try {
|
|
129
|
+
let size = 0;
|
|
130
|
+
try { size = statSync(logPath).size; } catch { /* no log yet */ }
|
|
131
|
+
if (size > LOG_MAX_BYTES) writeFileSync(logPath, '');
|
|
132
|
+
appendFileSync(logPath, `${new Date().toISOString()} [${name}] ${msg}\n`);
|
|
133
|
+
} catch { /* logging must never kill the keeper */ }
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
// Single instance per slot: O_EXCL lockfile, taken over when its pid is dead.
|
|
137
|
+
let locked = false;
|
|
138
|
+
for (let attempt = 0; attempt < 2 && !locked; attempt += 1) {
|
|
139
|
+
try {
|
|
140
|
+
const fd = openSync(lockPath, 'wx');
|
|
141
|
+
writeSync(fd, String(process.pid));
|
|
142
|
+
closeSync(fd);
|
|
143
|
+
locked = true;
|
|
144
|
+
} catch (e) {
|
|
145
|
+
if (e.code !== 'EEXIST') throw e;
|
|
146
|
+
let other = NaN;
|
|
147
|
+
try { other = parseInt(readFileSync(lockPath, 'utf-8'), 10); } catch { /* raced away */ }
|
|
148
|
+
if (Number.isFinite(other) && other !== process.pid && probe.alive(other)) {
|
|
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
|
+
}
|
|
164
|
+
}
|
|
165
|
+
try { unlinkSync(lockPath); } catch { /* raced away */ }
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (!locked) {
|
|
169
|
+
log('could not acquire keeper lock; exiting');
|
|
170
|
+
return { code: 1, reason: 'lock-failed' };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const cleanup = () => {
|
|
174
|
+
try {
|
|
175
|
+
if (parseInt(readFileSync(lockPath, 'utf-8'), 10) === process.pid) unlinkSync(lockPath);
|
|
176
|
+
} catch { /* already gone */ }
|
|
177
|
+
};
|
|
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
|
+
};
|
|
199
|
+
|
|
200
|
+
try {
|
|
201
|
+
log(`keeper started session=${session} claude=${claude?.pid}:${claude?.starttime}`);
|
|
202
|
+
let fails = 0;
|
|
203
|
+
let reacquires = 0;
|
|
204
|
+
let delay = KEEP_INTERVAL_MS * (1 + rand() * 0.2);
|
|
205
|
+
|
|
206
|
+
// Control queue state: ids already executed (never re-run if an ack failed,
|
|
207
|
+
// only re-acked), and distinct failure accounting with tick-skip backoff.
|
|
208
|
+
const handled = new Map();
|
|
209
|
+
const received = new Set();
|
|
210
|
+
const startedAt = now();
|
|
211
|
+
let lastHeartbeatAt = null;
|
|
212
|
+
// Busy/idle: only a hash of the pane is kept (pane text can hold secrets
|
|
213
|
+
// and the status result is posted to Discord). Armed by the first status
|
|
214
|
+
// request, then refreshed on every control tick.
|
|
215
|
+
let armed = false;
|
|
216
|
+
let paneHash = null;
|
|
217
|
+
let paneChangedAt = null;
|
|
218
|
+
const samplePane = async () => {
|
|
219
|
+
if (!tmuxPane) return false;
|
|
220
|
+
try {
|
|
221
|
+
const out = await execFile('tmux', ['capture-pane', '-p', '-t', tmuxPane]);
|
|
222
|
+
const text = typeof out === 'string' ? out : String(out?.stdout ?? '');
|
|
223
|
+
const h = createHash('sha256').update(text).digest('hex');
|
|
224
|
+
if (paneHash !== null && h !== paneHash) paneChangedAt = now();
|
|
225
|
+
paneHash = h;
|
|
226
|
+
return true;
|
|
227
|
+
} catch { return false; }
|
|
228
|
+
};
|
|
229
|
+
let ctlFails = 0;
|
|
230
|
+
let ctlSkip = 0;
|
|
231
|
+
const controlTick = async () => {
|
|
232
|
+
if (ctlSkip > 0) { ctlSkip -= 1; return; }
|
|
233
|
+
if (claude && !sameProcess(probe, claude.pid, claude.starttime)) return;
|
|
234
|
+
try {
|
|
235
|
+
if (armed) await samplePane();
|
|
236
|
+
const rows = (await pollControl()) || [];
|
|
237
|
+
for (const row of rows) {
|
|
238
|
+
let outcome = handled.get(row.id);
|
|
239
|
+
if (!outcome) {
|
|
240
|
+
if (!received.has(row.id)) { received.add(row.id); log(`control ${row.command} ${row.id} received from ${row.requested_via || '?'}`); }
|
|
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;
|
|
244
|
+
handled.set(row.id, outcome);
|
|
245
|
+
received.delete(row.id);
|
|
246
|
+
log(`control ${row.command} ${row.id} from ${row.requested_via || '?'}: ${outcome.status} (${outcome.result})`);
|
|
247
|
+
}
|
|
248
|
+
await ackControl(row.id, outcome.status, outcome.result);
|
|
249
|
+
handled.delete(row.id);
|
|
250
|
+
}
|
|
251
|
+
if (ctlFails > 0) log(`control poll recovered after ${ctlFails} failure(s)`);
|
|
252
|
+
ctlFails = 0;
|
|
253
|
+
} catch (err) {
|
|
254
|
+
ctlFails += 1;
|
|
255
|
+
ctlSkip = Math.min(2 ** (ctlFails - 1) - 1, CONTROL_BACKOFF_CAP_TICKS);
|
|
256
|
+
log(`control poll FAILED (${ctlFails} consecutive, skipping ${ctlSkip} tick(s)): ${err.status || ''} ${err.message}`);
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
const fmtDur = (sec) => {
|
|
260
|
+
const s = Math.max(0, Math.floor(sec));
|
|
261
|
+
const d = Math.floor(s / 86400); const h = Math.floor((s % 86400) / 3600); const m = Math.floor((s % 3600) / 60);
|
|
262
|
+
return d ? `${d}d${String(h).padStart(2, '0')}h` : `${h}h${String(m).padStart(2, '0')}m`;
|
|
263
|
+
};
|
|
264
|
+
const statusResult = async () => {
|
|
265
|
+
const parts = [`session=${session}`, `slot=${name}`];
|
|
266
|
+
let claudeUp = null;
|
|
267
|
+
if (claude) {
|
|
268
|
+
const alive = sameProcess(probe, claude.pid, claude.starttime);
|
|
269
|
+
if (alive) {
|
|
270
|
+
try {
|
|
271
|
+
const out = await execFile('ps', ['-o', 'etimes=', '-p', String(claude.pid)]);
|
|
272
|
+
const n = parseInt(String(typeof out === 'string' ? out : out?.stdout).trim(), 10);
|
|
273
|
+
if (Number.isFinite(n)) claudeUp = n;
|
|
274
|
+
} catch { /* uptime unknown */ }
|
|
275
|
+
}
|
|
276
|
+
parts.push(`claude pid ${claude.pid} ${alive ? 'alive' : 'gone'}${alive && claudeUp != null ? ` up ${fmtDur(claudeUp)}` : ''}`);
|
|
277
|
+
}
|
|
278
|
+
if (!tmuxPane) {
|
|
279
|
+
parts.push('not under tmux');
|
|
280
|
+
} else {
|
|
281
|
+
parts.push(`pane=${tmuxPane}`);
|
|
282
|
+
const wasArmed = armed;
|
|
283
|
+
armed = true;
|
|
284
|
+
const ok = await samplePane();
|
|
285
|
+
if (ok) {
|
|
286
|
+
if (!wasArmed) parts.push('activity unknown (first check)');
|
|
287
|
+
else parts.push(paneChangedAt !== null && now() - paneChangedAt <= BUSY_WINDOW_MS ? 'busy' : 'idle');
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
parts.push(lastHeartbeatAt === null ? 'last heartbeat none yet' : `last heartbeat ${new Date(lastHeartbeatAt).toISOString()}`);
|
|
291
|
+
parts.push(`keeper up ${fmtDur((now() - startedAt) / 1000)}`);
|
|
292
|
+
return parts.join(', ').slice(0, 500);
|
|
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
|
+
};
|
|
454
|
+
const runControlCommand = async (row) => {
|
|
455
|
+
if (row.command === 'status') return { status: 'done', result: await statusResult() };
|
|
456
|
+
if (row.command === 'wake') return runWake(row);
|
|
457
|
+
if (row.command !== 'compact') return { status: 'failed', result: `unsupported command ${row.command}` };
|
|
458
|
+
if (!tmuxPane) return { status: 'failed', result: NO_TMUX_RESULT };
|
|
459
|
+
try {
|
|
460
|
+
await execFile('tmux', ['send-keys', '-t', tmuxPane, '/compact', 'Enter']);
|
|
461
|
+
return { status: 'done', result: `sent /compact to ${tmuxPane}` };
|
|
462
|
+
} catch (err) {
|
|
463
|
+
return { status: 'failed', result: `tmux send-keys failed: ${err.message}` };
|
|
464
|
+
}
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
for (;;) {
|
|
468
|
+
let remaining = delay;
|
|
469
|
+
while (pollControl && remaining > CONTROL_POLL_MS) {
|
|
470
|
+
await sleep(CONTROL_POLL_MS);
|
|
471
|
+
remaining -= CONTROL_POLL_MS;
|
|
472
|
+
await controlTick();
|
|
473
|
+
if (staleBuild()) return { code: 0, reason: 'reexec' };
|
|
474
|
+
}
|
|
475
|
+
await sleep(remaining);
|
|
476
|
+
if (pollControl) await controlTick();
|
|
477
|
+
if (staleBuild()) return { code: 0, reason: 'reexec' };
|
|
478
|
+
|
|
479
|
+
if (claude && !sameProcess(probe, claude.pid, claude.starttime)) {
|
|
480
|
+
log(`claude session gone (pid ${claude.pid}); releasing ${name} and exiting`);
|
|
481
|
+
try { await release(); } catch (e) { log(`release failed: ${e.message}`); }
|
|
482
|
+
return { code: 0, reason: 'session-gone' };
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
try {
|
|
486
|
+
await heartbeat();
|
|
487
|
+
lastHeartbeatAt = now();
|
|
488
|
+
if (fails > 0) log(`heartbeat recovered after ${fails} failure(s)`);
|
|
489
|
+
fails = 0;
|
|
490
|
+
reacquires = 0;
|
|
491
|
+
log('heartbeat ok');
|
|
492
|
+
delay = KEEP_INTERVAL_MS * (1 + rand() * 0.2);
|
|
493
|
+
} catch (err) {
|
|
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
|
+
}
|
|
510
|
+
log(`HOLD LOST: ${name} is no longer held by ${session}; keeper exiting`);
|
|
511
|
+
try { forget(); } catch { /* best effort */ }
|
|
512
|
+
return { code: 0, reason: 'hold-lost' };
|
|
513
|
+
}
|
|
514
|
+
fails += 1;
|
|
515
|
+
const limit = err instanceof QuoxApiError && err.status === 401 ? MAX_AUTH_FAILS : MAX_FAILS;
|
|
516
|
+
log(`heartbeat FAILED (${fails}/${limit}): ${err.status || ''} ${err.message}`);
|
|
517
|
+
if (fails >= limit) {
|
|
518
|
+
log(`RETIRED after ${fails} consecutive failures; hold will lapse`);
|
|
519
|
+
return { code: 1, reason: 'retired' };
|
|
520
|
+
}
|
|
521
|
+
delay = Math.min(BACKOFF_BASE_MS * 2 ** (fails - 1), BACKOFF_CAP_MS);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
} finally {
|
|
525
|
+
cleanup();
|
|
526
|
+
}
|
|
527
|
+
}
|
|
@@ -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
|
+
}
|