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,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* /compact: POST /api/v1/conversations/:id/compact asks the server to summarise the conversation (keeping
|
|
3
|
+
* decisions); the summary becomes the head of the conversation for later turns. This module only words the
|
|
4
|
+
* result and the refusals; nothing is summarised on the client.
|
|
5
|
+
*/
|
|
6
|
+
export const SUMMARY_LINES = 6;
|
|
7
|
+
|
|
8
|
+
/** 18400 -> "18.4k", 950 -> "950". */
|
|
9
|
+
export function kTokens(n) {
|
|
10
|
+
if (n == null || n === '') return '?';
|
|
11
|
+
const v = Number(n);
|
|
12
|
+
if (!Number.isFinite(v)) return '?';
|
|
13
|
+
if (v < 1000) return String(Math.round(v));
|
|
14
|
+
return `${(v / 1000).toFixed(1).replace(/\.0$/, '')}k`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** The result -> { head, tone, summaryLines, more, notes[] }. */
|
|
18
|
+
export function compactResult(r, { max = SUMMARY_LINES } = {}) {
|
|
19
|
+
const lines = String(r?.summary || '').split('\n').map(l => l.replace(/\s+$/, '')).filter((l, i, a) => l || (i > 0 && a[i - 1]));
|
|
20
|
+
const n = Number(r?.turns_compacted) || 0;
|
|
21
|
+
const head = r?.unchanged
|
|
22
|
+
? `Nothing new to compact; the summary is already up to date (${kTokens(r?.tokens_after)} tokens).`
|
|
23
|
+
: `Compacted ${n} turn${n === 1 ? '' : 's'}: ${kTokens(r?.tokens_before)} -> ${kTokens(r?.tokens_after)} tokens`;
|
|
24
|
+
const notes = [];
|
|
25
|
+
if (!r?.unchanged && Number(r?.tokens_after) > Number(r?.tokens_before)) notes.push('The summary is longer than what it replaced: this conversation was too short to gain from compacting.');
|
|
26
|
+
if (r?.provider || r?.model) notes.push(`Summarised by ${[r.provider, r.model].filter(Boolean).join(' ')}.`);
|
|
27
|
+
if (Array.isArray(r?.tool_activity) && r.tool_activity.some(t => t?.tools?.length)) notes.push('/compact show lists the tool activity recorded in those turns, the basis for any action the summary names.');
|
|
28
|
+
if (r?.cli_session_reset) notes.push('The Claude CLI session behind this conversation was reset, so it starts from the summary.');
|
|
29
|
+
return { head, tone: r?.unchanged ? undefined : 'ok', summaryLines: lines.slice(0, max), more: Math.max(0, lines.length - max), allLines: lines, notes, basisLines: toolBasisLines(r?.tool_activity) };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The server's recorded tool activity per compacted turn -> lines. Read from the stored turns, never
|
|
34
|
+
* from the model, so any action the summary names can be checked here. No row is "none recorded",
|
|
35
|
+
* not "none ran": only some paths record tool activity.
|
|
36
|
+
*/
|
|
37
|
+
export function toolBasisLines(activity) {
|
|
38
|
+
if (!Array.isArray(activity)) return ['The server sent no tool activity record for this compaction.'];
|
|
39
|
+
if (!activity.length) return ['No turns were compacted, so there is no tool activity.'];
|
|
40
|
+
return activity.map((t) => {
|
|
41
|
+
const tools = (Array.isArray(t?.tools) ? t.tools : []).map(x => `${x?.name || '?'} ${x?.status || '?'}`);
|
|
42
|
+
if (Number(t?.more) > 0) tools.push(`and ${Number(t.more)} more`);
|
|
43
|
+
return `turn ${t?.turn ?? '?'}: ${tools.length ? tools.join(', ') : 'no tools recorded'}`;
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** /compact show: the whole summary, then the recorded tool activity it was made from. */
|
|
48
|
+
export function compactShowLines(v) {
|
|
49
|
+
const basis = Array.isArray(v?.basisLines) ? v.basisLines : toolBasisLines(undefined);
|
|
50
|
+
return [
|
|
51
|
+
{ kind: 'plain', text: `Summary (${v.allLines.length} lines)` },
|
|
52
|
+
...v.allLines.map(t => ({ kind: 'dim', text: t || ' ' })),
|
|
53
|
+
{ kind: 'plain', text: 'Recorded tool activity (from the stored turns, not the model)' },
|
|
54
|
+
...basis.map(t => ({ kind: 'dim', text: t })),
|
|
55
|
+
];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** A refused or failed compaction -> one honest line. */
|
|
59
|
+
export function compactError(e) {
|
|
60
|
+
const code = e?.code || e?.body?.error || null;
|
|
61
|
+
const msg = e?.body?.message || null;
|
|
62
|
+
if (e?.status === 409) {
|
|
63
|
+
if (code === 'turn_running') return 'A turn is still running in this conversation. Compact after it finishes.';
|
|
64
|
+
if (code === 'compact_in_progress') return 'A compaction of this conversation is already running.';
|
|
65
|
+
if (code === 'transcripts_disabled') return msg || 'Transcript storage is off for this org, so no summary can be stored.';
|
|
66
|
+
return msg || 'The server refused to compact right now (409).';
|
|
67
|
+
}
|
|
68
|
+
if (e?.status === 403 && code === 'ai_consent_required') return 'Compacting sends this conversation to your AI provider, and you have not given AI processing consent. /consent grant gives it (Settings > Privacy in the dashboard does too).';
|
|
69
|
+
if (e?.status === 403) return 'Not allowed to compact this conversation (403).';
|
|
70
|
+
if (e?.status === 404) return 'The server has no stored turns for this conversation (expired after 7 days, or nothing sent yet).';
|
|
71
|
+
if (e?.status === 502) return 'The model call failed; nothing was changed. Try /compact again.';
|
|
72
|
+
if (e?.status === 503 && code === 'no_provider') return 'No AI provider is configured for this org, so nothing can summarise it. /connect openai';
|
|
73
|
+
if (e?.status === 500 && code === 'summary_not_stored') return 'The summary was made but could not be stored; nothing was changed.';
|
|
74
|
+
if (e?.status === 404 || e?.status === 405) return 'This server cannot compact conversations yet.';
|
|
75
|
+
return `compact failed: ${e?.status ? `HTTP ${e.status} ` : ''}${e?.message || 'failed'}`.trim();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** GET .../context -> the /context rows' facts. */
|
|
79
|
+
export function contextFacts(c) {
|
|
80
|
+
if (!c || typeof c !== 'object') return null;
|
|
81
|
+
const t = c.tokens || {};
|
|
82
|
+
const how = t.source === 'cli_session_usage' ? 'from the CLI session usage' : t.source === 'chars_div_4' ? 'estimated from stored text (chars / 4)' : t.source ? `source ${t.source}` : 'source not given';
|
|
83
|
+
return {
|
|
84
|
+
tokens: t.estimate,
|
|
85
|
+
tokensText: `${kTokens(t.estimate)} tokens${t.context_window ? ` of ${kTokens(t.context_window)}` : ''}, ${how}`,
|
|
86
|
+
turns: c.turns,
|
|
87
|
+
turnsText: c.turns == null ? null : c.summary_present ? `${c.turns} turn${c.turns === 1 ? '' : 's'} since the summary` : `${c.turns} stored turn${c.turns === 1 ? '' : 's'}`,
|
|
88
|
+
summaryText: c.summary_present ? `yes, ${c.compacted_turns ?? '?'} turns folded in${c.summary_at ? ` (${String(c.summary_at).slice(0, 16).replace('T', ' ')} UTC)` : ''}` : 'none yet (/compact)'
|
|
89
|
+
};
|
|
90
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One preview card, one y/n. Shared by /watch, /automate and /crystallize: a direct human ask is already the
|
|
3
|
+
* authorization, so the preview is the only confirmation. The answer is the next composer line (same pattern as the
|
|
4
|
+
* /approve "y/N" grant); a slash command typed instead drops the preview and runs normally.
|
|
5
|
+
*/
|
|
6
|
+
import { askText } from '../ask.js';
|
|
7
|
+
export function createConfirm({ say, ttlMs = 120000, now = () => Date.now() }) {
|
|
8
|
+
let p = null;
|
|
9
|
+
return {
|
|
10
|
+
pending: () => Boolean(p),
|
|
11
|
+
/** y/N/c: a chat about the ask keeps it open with a fresh window. */
|
|
12
|
+
touch() { if (p) p.at = now(); },
|
|
13
|
+
ask(lines, onYes, { prompt = 'Go ahead?' } = {}) {
|
|
14
|
+
p = { onYes, at: now() };
|
|
15
|
+
for (const l of lines) (Array.isArray(l) ? say(l[0], l[1]) : say(l));
|
|
16
|
+
say(askText(String(prompt).replace(/\s*y\/n$/i, '')), 'ask'); // QUOXON-ASK: one shape
|
|
17
|
+
},
|
|
18
|
+
/** -> true when this line was the answer (consumed); false when the caller should treat it as an ordinary line. */
|
|
19
|
+
answer(text) {
|
|
20
|
+
if (!p) return false;
|
|
21
|
+
const q = p; p = null;
|
|
22
|
+
const t = String(text || '').trim();
|
|
23
|
+
if (t.startsWith('/')) return false;
|
|
24
|
+
if (now() - q.at > ttlMs) { say('That preview expired, so nothing was created. Run the command again.', 'dim'); return true; }
|
|
25
|
+
if (/^y(es)?$/i.test(t)) { Promise.resolve(q.onYes()).catch((e) => say(`Failed: ${e?.message || e}`, 'warn')); return true; }
|
|
26
|
+
say('Cancelled; nothing was created.', 'dim');
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* /crystallize [what]: turn what was just accomplished into a permanent reusable skill (rules, tools, tests).
|
|
3
|
+
* Real: one drafting model call (draft.js), a dry-run through the skill firewall for the preview, and creation in
|
|
4
|
+
* the QSKL store (POST /api/quox-skills). /crystal (is a workflow eligible to become a Crystal) is a different check.
|
|
5
|
+
*/
|
|
6
|
+
import { draftJson, transcriptText } from './draft.js';
|
|
7
|
+
|
|
8
|
+
const why = (e) => `${e?.status ? `HTTP ${e.status}: ` : ''}${e?.message || 'failed'}`;
|
|
9
|
+
const NAME_RE = /^[a-z0-9][a-z0-9-]{1,60}$/;
|
|
10
|
+
|
|
11
|
+
export const crystallizePrompt = (context) => [
|
|
12
|
+
'Turn what was just accomplished in the conversation below into a reusable skill. Reply with ONE JSON object and nothing else. Use no tools.',
|
|
13
|
+
'Shape: {"name":"kebab-case-name","category":str,"description":"one sentence","rules":[str,...],"tools":[tool name,...],',
|
|
14
|
+
' "tests":[{"name":str,"input":str,"expect":str}]}.',
|
|
15
|
+
'Rules are the steps and guardrails to repeat next time. Never include secrets, keys, hostnames or personal data. Tools are only ones actually used above.',
|
|
16
|
+
'', '--- conversation ---', context, '--- end ---'
|
|
17
|
+
].join('\n');
|
|
18
|
+
|
|
19
|
+
/** -> { body (POST /api/quox-skills), card[] } or { error } */
|
|
20
|
+
export function buildSkill(d, { orgId, author }) {
|
|
21
|
+
const name = String(d?.name || '').trim().toLowerCase();
|
|
22
|
+
if (!NAME_RE.test(name)) return { error: 'the draft has no valid kebab-case name' };
|
|
23
|
+
const rules = (Array.isArray(d.rules) ? d.rules : []).map((r) => String(r).trim()).filter(Boolean);
|
|
24
|
+
if (!rules.length) return { error: 'the draft has no rules' };
|
|
25
|
+
const description = String(d.description || '').trim();
|
|
26
|
+
if (!description) return { error: 'the draft has no description' };
|
|
27
|
+
const tools = (Array.isArray(d.tools) ? d.tools : []).map((t) => String(t).trim()).filter(Boolean);
|
|
28
|
+
const tests = {};
|
|
29
|
+
for (const [i, t] of (Array.isArray(d.tests) ? d.tests : []).entries()) {
|
|
30
|
+
if (t && (t.input || t.expect)) tests[String(t.name || `case-${i + 1}`)] = { input: String(t.input || ''), expect: String(t.expect || '') };
|
|
31
|
+
}
|
|
32
|
+
const bodyMd = [`# ${name}`, '', description, '', '## Rules', ...rules.map((r, i) => `${i + 1}. ${r}`),
|
|
33
|
+
...(tools.length ? ['', '## Tools', ...tools.map((t) => `- ${t}`)] : [])].join('\n');
|
|
34
|
+
const body = { orgId, scopeId: null, name, category: String(d.category || 'general'), description, bodyMd, author: author || 'quoxon', requiredTools: tools, tests, tags: ['crystallized'] };
|
|
35
|
+
const card = [
|
|
36
|
+
['Skill draft (nothing is saved until you answer):', 'accent'],
|
|
37
|
+
` Name ${name}`,
|
|
38
|
+
` About ${description}`,
|
|
39
|
+
` Rules ${rules.length} first: ${rules[0]}`,
|
|
40
|
+
` Tools ${tools.length ? tools.join(', ') : 'none'}`,
|
|
41
|
+
` Tests ${Object.keys(tests).length}${Object.keys(tests).length ? ' ' + Object.keys(tests).join(', ') : ''}`
|
|
42
|
+
];
|
|
43
|
+
return { body, card, name };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function createCrystallizeCmd({ api, say, confirm, getState, getFields }) {
|
|
47
|
+
return {
|
|
48
|
+
async handle(arg) {
|
|
49
|
+
if (!api.skillCreate) return say('Skills are not available in this mode.', 'warn');
|
|
50
|
+
const ctx = [transcriptText(getState()), String(arg || '').trim() ? `Focus: ${String(arg).trim()}` : ''].filter(Boolean).join('\n');
|
|
51
|
+
if (!ctx) return say('Nothing to crystallize yet. Do the task with Quox first, or say what: /crystallize how we rotate the API key.');
|
|
52
|
+
say('Drafting the skill…', 'dim');
|
|
53
|
+
let d;
|
|
54
|
+
try { d = await draftJson({ api, prompt: crystallizePrompt(ctx), fields: getFields() }); }
|
|
55
|
+
catch (e) { return say(`Could not draft a skill: ${e.message}. Nothing was saved.`, 'warn'); }
|
|
56
|
+
const b = buildSkill(d, { orgId: api.orgId?.(), author: api.userId?.() });
|
|
57
|
+
if (b.error) return say(`Could not draft a usable skill: ${b.error}. Nothing was saved.`, 'warn');
|
|
58
|
+
try { await api.skillCreate(b.body, { dryRun: true }); } // the server's skill firewall, before the card
|
|
59
|
+
catch (e) { return say(`The skill was not saved: ${why(e)}`, 'warn'); }
|
|
60
|
+
b.card.push(' Firewall passed the server dry run');
|
|
61
|
+
confirm.ask(b.card, async () => {
|
|
62
|
+
try { await api.skillCreate(b.body); } catch (e) { return say(`Could not save the skill: ${why(e)}`, 'warn'); }
|
|
63
|
+
say(`✔ Saved skill ${b.name}.`, 'ok');
|
|
64
|
+
say(` See it: /skills ${b.name}`);
|
|
65
|
+
}, { prompt: 'Save this skill?' });
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* /dashboard [page] and `quox dashboard [page]`: a one-time sign-in link to the web dashboard.
|
|
3
|
+
*
|
|
4
|
+
* The link comes from POST /api/auth/login-link (10 minutes, single use, bound to this user and org) and is
|
|
5
|
+
* joined to the active profile's dashboard URL. It is a credential: callers print it on the visible line only,
|
|
6
|
+
* never into the dev transcript or any history. Opened in the default browser when running locally; never over
|
|
7
|
+
* SSH (the browser would open on the wrong machine), where it is only printed.
|
|
8
|
+
*
|
|
9
|
+
* `next` asks the consume route to land on the page after sign-in. A server that does not know `next` lands
|
|
10
|
+
* on the dashboard home instead; nothing breaks.
|
|
11
|
+
*/
|
|
12
|
+
import { spawn as nodeSpawn } from 'node:child_process';
|
|
13
|
+
|
|
14
|
+
/** Page name -> real route in src/config/routeRegistry.js (approvals = the HITL /inbox, inbox = InboxQ mail). */
|
|
15
|
+
export const PAGES = {
|
|
16
|
+
sessions: '/sessions',
|
|
17
|
+
approvals: '/inbox',
|
|
18
|
+
agents: '/agents',
|
|
19
|
+
lucid: '/lucid',
|
|
20
|
+
inbox: '/inboxq',
|
|
21
|
+
seo: '/quoxseo',
|
|
22
|
+
settings: '/settings'
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export function resolvePage(arg) {
|
|
26
|
+
const name = String(arg ?? '').trim().toLowerCase();
|
|
27
|
+
if (!name) return { ok: true, page: null, path: '/' };
|
|
28
|
+
if (Object.hasOwn(PAGES, name)) return { ok: true, page: name, path: PAGES[name] };
|
|
29
|
+
return { ok: false, error: `Unknown page "${name}". Pages: ${Object.keys(PAGES).join(', ')}.` };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const isRemoteSession = (env = process.env) => Boolean(env.SSH_CONNECTION || env.SSH_CLIENT || env.SSH_TTY);
|
|
33
|
+
|
|
34
|
+
export function openerFor(platform = process.platform) {
|
|
35
|
+
if (platform === 'linux') return { cmd: 'xdg-open', args: [] };
|
|
36
|
+
if (platform === 'darwin') return { cmd: 'open', args: [] };
|
|
37
|
+
if (platform === 'win32') return { cmd: 'rundll32', args: ['url.dll,FileProtocolHandler'] }; // no shell, so & in the URL is safe
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** -> { opened: true } | { opened: false, reason: 'ssh' | 'no-opener' | 'failed' }. Never throws, never uses a shell. */
|
|
42
|
+
export function openInBrowser(url, { env = process.env, platform = process.platform, spawn = nodeSpawn } = {}) {
|
|
43
|
+
if (isRemoteSession(env)) return Promise.resolve({ opened: false, reason: 'ssh' });
|
|
44
|
+
const op = openerFor(platform);
|
|
45
|
+
if (!op) return Promise.resolve({ opened: false, reason: 'no-opener' });
|
|
46
|
+
return new Promise((resolve) => {
|
|
47
|
+
let done = false;
|
|
48
|
+
const fin = (r) => { if (!done) { done = true; resolve(r); } };
|
|
49
|
+
try {
|
|
50
|
+
const child = spawn(op.cmd, [...op.args, url], { stdio: 'ignore', detached: true });
|
|
51
|
+
child.on('error', () => fin({ opened: false, reason: 'failed' }));
|
|
52
|
+
child.unref?.();
|
|
53
|
+
setTimeout(() => fin({ opened: true }), 150); // an error (opener missing) arrives well inside this
|
|
54
|
+
} catch { fin({ opened: false, reason: 'failed' }); }
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const missing = (e) => e?.status === 404 || e?.status === 405 || e?.status === 501;
|
|
59
|
+
const join = (base, path) => new URL(path, base).href;
|
|
60
|
+
const isLoopbackUrl = (u) => /^(localhost|127\.\d+\.\d+\.\d+|\[::1\]|0\.0\.0\.0)$/i.test(new URL(u).hostname);
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* mint: async () -> { path, expires_at, ttl_seconds } (POST /api/auth/login-link).
|
|
64
|
+
* -> { kind: 'link' | 'plain' | 'error', url, plainUrl, page, remote, opened, openFailed, ttlSeconds, expiresAt, error }
|
|
65
|
+
*/
|
|
66
|
+
export async function runDashboard({ page, mint, baseUrl, env = process.env, open = (u) => openInBrowser(u, { env }) } = {}) {
|
|
67
|
+
const p = resolvePage(page);
|
|
68
|
+
if (!p.ok) return { kind: 'error', error: p.error };
|
|
69
|
+
const remote = isRemoteSession(env);
|
|
70
|
+
let plainUrl = null;
|
|
71
|
+
try { plainUrl = join(baseUrl, p.path); } catch { /* no profile URL: the server may still give one */ }
|
|
72
|
+
const base = { page: p.page, plainUrl, remote };
|
|
73
|
+
const noUrl = 'This profile has no dashboard URL. Set one with: quox config set url <https://host:3000>';
|
|
74
|
+
let r;
|
|
75
|
+
try { r = await mint(); } catch (e) {
|
|
76
|
+
if (missing(e)) return plainUrl ? { ...base, kind: 'plain', url: plainUrl } : { kind: 'error', error: noUrl };
|
|
77
|
+
const why = e?.status ? `${e.message || 'refused'} (HTTP ${e.status})` : (e?.message || 'failed');
|
|
78
|
+
return { ...base, kind: 'error', url: plainUrl, error: `Could not create a sign-in link: ${why}` };
|
|
79
|
+
}
|
|
80
|
+
if (!r?.path || !/^\/api\/auth\/login-link\/consume\?t=[\w-]+$/.test(r.path)) {
|
|
81
|
+
return { ...base, kind: 'error', url: plainUrl, error: 'Could not create a sign-in link: unexpected answer from the auth service.' };
|
|
82
|
+
}
|
|
83
|
+
// The server knows the public address of the instance; the profile URL may be localhost on this box.
|
|
84
|
+
let origin = null;
|
|
85
|
+
if (typeof r.url === 'string') {
|
|
86
|
+
try {
|
|
87
|
+
const u = new URL(r.url);
|
|
88
|
+
if (/^https?:$/.test(u.protocol) && r.url === u.origin + r.path) origin = u.origin;
|
|
89
|
+
} catch { /* ignore a malformed server url */ }
|
|
90
|
+
}
|
|
91
|
+
let linkBase = origin || baseUrl;
|
|
92
|
+
let local = false;
|
|
93
|
+
if (!origin) {
|
|
94
|
+
try { local = isLoopbackUrl(join(baseUrl, '/')); } catch { /* handled below */ }
|
|
95
|
+
}
|
|
96
|
+
let link;
|
|
97
|
+
try { link = join(linkBase, r.path); } catch { return { kind: 'error', error: noUrl }; }
|
|
98
|
+
const url = link + (p.page ? `&next=${encodeURIComponent(p.path)}` : '');
|
|
99
|
+
if (origin) base.plainUrl = join(origin, p.path);
|
|
100
|
+
const out = { ...base, kind: 'link', url, localOnly: local, ttlSeconds: r.ttl_seconds ?? 600, expiresAt: r.expires_at, opened: false };
|
|
101
|
+
if (!remote) {
|
|
102
|
+
const o = await open(url).catch(() => ({ opened: false, reason: 'failed' }));
|
|
103
|
+
out.opened = Boolean(o?.opened);
|
|
104
|
+
out.openFailed = !o?.opened;
|
|
105
|
+
}
|
|
106
|
+
return out;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const osc8 = (url, text) => `\x1b]8;;${url}\x1b\\${text}\x1b]8;;\x1b\\`;
|
|
110
|
+
|
|
111
|
+
/** Result -> [{ text, tone }] for the visible lines. The link line is the only place the credential is printed. */
|
|
112
|
+
export function dashboardLines(r, { hyperlinks = false } = {}) {
|
|
113
|
+
const where = r.page ? `the ${r.page} page` : 'the dashboard';
|
|
114
|
+
if (r.kind === 'error') {
|
|
115
|
+
return [
|
|
116
|
+
{ text: r.error, tone: 'warn' },
|
|
117
|
+
...(r.url ? [{ text: `The dashboard is at ${r.url} (you will need to sign in).`, tone: 'dim' }] : [])
|
|
118
|
+
];
|
|
119
|
+
}
|
|
120
|
+
if (r.kind === 'plain') {
|
|
121
|
+
return [
|
|
122
|
+
{ text: `This server does not offer one-time sign-in links (an older server). Open ${where} here and sign in:`, tone: 'warn' },
|
|
123
|
+
{ text: r.url }
|
|
124
|
+
];
|
|
125
|
+
}
|
|
126
|
+
const mins = Math.round((r.ttlSeconds || 600) / 60);
|
|
127
|
+
return [
|
|
128
|
+
{ text: r.opened ? `Opened ${where} in your browser, already signed in.` : `Sign-in link for ${where}:` },
|
|
129
|
+
...(hyperlinks ? [{ text: osc8(r.url, `Open ${where} (signed in)`) }] : []),
|
|
130
|
+
{ text: r.url },
|
|
131
|
+
{ text: `Valid for ${mins} minutes, works once, tied to your account. Anyone holding it can sign in as you until then.`, tone: 'dim' },
|
|
132
|
+
...(r.localOnly ? [{ text: 'This address only works on this machine; set the app URL with `quox server config app-url <url>` or in Settings > General > App URL.', tone: 'dim' }] : []),
|
|
133
|
+
...(r.remote ? [{ text: 'Over SSH, so it was not opened here: open it in a browser on your own machine.', tone: 'dim' }] : []),
|
|
134
|
+
...(r.openFailed ? [{ text: 'Could not open a browser from here: use the link above.', tone: 'warn' }] : [])
|
|
135
|
+
];
|
|
136
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One drafting model call for /automate and /crystallize, through the same governed turn path as chat (api.stream)
|
|
3
|
+
* but in its own throwaway conversation, so it never lands in the visible transcript. The model is told to answer
|
|
4
|
+
* with JSON only and use no tools; an approval request or error aborts the draft. The JSON is validated by the
|
|
5
|
+
* caller before any card is shown.
|
|
6
|
+
*/
|
|
7
|
+
import { textGate } from '../events.js';
|
|
8
|
+
|
|
9
|
+
const newId = () => `QXN_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
10
|
+
|
|
11
|
+
/** The visible conversation as plain text for the drafting prompt (last `max` chars). */
|
|
12
|
+
export function transcriptText(state, max = 12000) {
|
|
13
|
+
const out = [];
|
|
14
|
+
for (const it of [...(state?.history || []), ...(state?.blocks || [])]) {
|
|
15
|
+
if (it.kind === 'user' && !it.echo && it.text && !it.text.startsWith('/')) out.push(`User: ${it.text}`);
|
|
16
|
+
else if (it.kind === 'text' && it.text) out.push(`Quox: ${it.text}`);
|
|
17
|
+
else if (it.kind === 'tool' && it.tool) out.push(`Tool used: ${[it.tool, it.target].filter(Boolean).join(' ')}`);
|
|
18
|
+
}
|
|
19
|
+
const t = out.join('\n');
|
|
20
|
+
return t.length > max ? t.slice(t.length - max) : t;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** First JSON object in a model reply (plain, fenced, or surrounded by prose), or null. */
|
|
24
|
+
export function extractJson(text) {
|
|
25
|
+
const s = String(text || '');
|
|
26
|
+
const fence = s.match(/```(?:json)?\s*([\s\S]*?)```/i);
|
|
27
|
+
const cand = fence ? fence[1] : s;
|
|
28
|
+
const a = cand.indexOf('{'), b = cand.lastIndexOf('}');
|
|
29
|
+
if (a < 0 || b <= a) return null;
|
|
30
|
+
try { const v = JSON.parse(cand.slice(a, b + 1)); return v && typeof v === 'object' && !Array.isArray(v) ? v : null; } catch { return null; }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** -> parsed object, or throws Error(reason). `fields` = streamFields(model choice). */
|
|
34
|
+
export async function draftJson({ api, prompt, fields = {}, timeoutMs = 90000 }) {
|
|
35
|
+
if (!api.stream) throw new Error('no model connection in this mode');
|
|
36
|
+
const ctrl = new AbortController();
|
|
37
|
+
const timer = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
38
|
+
let text = '', gate = null;
|
|
39
|
+
try {
|
|
40
|
+
for await (const ev of api.stream({ turnId: newId(), message: prompt, agent: 'quox', ...fields }, ctrl.signal)) {
|
|
41
|
+
if (ev.type === 'error') throw new Error(String(ev.error || 'the model call failed'));
|
|
42
|
+
if (ev.type === 'approval.requested') throw new Error('the model tried to use a tool instead of drafting');
|
|
43
|
+
const g = textGate(gate, ev); gate = g.st;
|
|
44
|
+
if (g.append) text += ev.content;
|
|
45
|
+
if (ev.type === 'turn.finished' || ev.type === 'done') break;
|
|
46
|
+
}
|
|
47
|
+
} catch (e) {
|
|
48
|
+
if (ctrl.signal.aborted) throw new Error('the model took too long');
|
|
49
|
+
throw e;
|
|
50
|
+
} finally { clearTimeout(timer); }
|
|
51
|
+
const j = extractJson(text);
|
|
52
|
+
if (!j) throw new Error('the model did not return a usable draft');
|
|
53
|
+
return j;
|
|
54
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CUSTOM-HATS (owner 2026-10-17): /hat new | edit <hat> | delete <hat> in the quox window. One small step machine:
|
|
3
|
+
* every question is either a y/N/c card (askText, the same shape as every other ask) or a one-line text capture
|
|
4
|
+
* (a name, a pick by number or name, a list of agents). Esc, or any other slash command, ends the walk and
|
|
5
|
+
* nothing is sent. Nothing reaches the server until the final summary card is answered y.
|
|
6
|
+
* The definition maths is shared with `quox hat create|edit` (core/customHats.js); this file has no I/O of its own
|
|
7
|
+
* beyond the api it is given.
|
|
8
|
+
*/
|
|
9
|
+
import { buildCustomDefinition, applyHatEdits } from '../../core/customHats.js';
|
|
10
|
+
import { matchHat, hatChoices } from '../hatSwitch.js';
|
|
11
|
+
import { askText } from '../ask.js';
|
|
12
|
+
|
|
13
|
+
export const slugify = (name) => String(name ?? '').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|
|
14
|
+
const isCustom = (hat) => hat?.template_key == null;
|
|
15
|
+
const nameOf = (hat) => hat?.name || hat?.key || 'hat';
|
|
16
|
+
const joinNice = (xs) => xs.join(', ');
|
|
17
|
+
|
|
18
|
+
/** "1, 3 sec" -> picks resolved against rows [{ id }] -> { ids, unknown } (numbers are 1-based positions). */
|
|
19
|
+
export function pickAgents(line, rows) {
|
|
20
|
+
const ids = []; const unknown = [];
|
|
21
|
+
for (const raw of String(line ?? '').split(/[\s,]+/).map(s => s.trim()).filter(Boolean)) {
|
|
22
|
+
let id = null;
|
|
23
|
+
if (/^\d+$/.test(raw)) id = rows[Number(raw) - 1]?.id ?? null;
|
|
24
|
+
else id = rows.find(r => r.id.toLowerCase() === raw.toLowerCase())?.id ?? null;
|
|
25
|
+
if (!id) unknown.push(raw); else if (!ids.includes(id)) ids.push(id);
|
|
26
|
+
}
|
|
27
|
+
return { ids, unknown };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function createHatWalk({ api, say, bump = () => {} }) {
|
|
31
|
+
let W = null; // the walk in progress, or null
|
|
32
|
+
|
|
33
|
+
const ask = (question, reason = '') => { const st = { at: Date.now() }; W.card = st; W.wait = { ...W.wait, kind: 'yn' }; say(askText(question, { reason }), 'ask'); return st; };
|
|
34
|
+
const yn = (question, on, reason) => { W.wait = { kind: 'yn', on }; ask(question, reason); bump(); };
|
|
35
|
+
const text = (prompt, on) => { W.wait = { kind: 'text', on }; W.card = null; say(prompt); bump(); };
|
|
36
|
+
const end = () => { W = null; bump(); };
|
|
37
|
+
const fail = (what, e) => { say(`${what}: ${e?.message || 'the server refused'}.`, 'warn'); end(); };
|
|
38
|
+
|
|
39
|
+
async function loadAgents() {
|
|
40
|
+
if (W.agents) return W.agents;
|
|
41
|
+
try {
|
|
42
|
+
const rows = await api.agents();
|
|
43
|
+
W.agents = (Array.isArray(rows) ? rows : []).map(a => ({ id: String(a.agent_id ?? a.id ?? ''), name: a.name || a.agent_id || a.id })).filter(a => a.id);
|
|
44
|
+
} catch { W.agents = null; }
|
|
45
|
+
return W.agents;
|
|
46
|
+
}
|
|
47
|
+
const agentLabel = (id) => { const a = W.agents?.find(x => x.id === id); return a && a.name && a.name !== id ? `${id} (${a.name})` : id; };
|
|
48
|
+
|
|
49
|
+
// Shared tail: keep cards for each current specialist, then "Add agents?". done(kept, added).
|
|
50
|
+
async function agentSteps(current, done) {
|
|
51
|
+
await loadAgents();
|
|
52
|
+
const kept = []; const queue = [...current];
|
|
53
|
+
const nextKeep = () => {
|
|
54
|
+
if (!queue.length) return askAdd();
|
|
55
|
+
const id = queue.shift();
|
|
56
|
+
yn(`Keep ${agentLabel(id)}?`, (yes) => { if (yes) kept.push(id); nextKeep(); }, 'This hat lists it as a specialist');
|
|
57
|
+
};
|
|
58
|
+
const askAdd = () => yn('Add agents?', async (yes) => {
|
|
59
|
+
if (!yes) return done(kept, []);
|
|
60
|
+
if (!W.agents?.length) { say('Could not list the org agents, so none were added.', 'warn'); return done(kept, []); }
|
|
61
|
+
const rows = W.agents.filter(a => !kept.includes(a.id));
|
|
62
|
+
if (!rows.length) { say('Every agent in this org is already in the hat.'); return done(kept, []); }
|
|
63
|
+
rows.forEach((a, i) => say(` ${i + 1}. ${a.id}${a.name && a.name !== a.id ? ` ${a.name}` : ''}`));
|
|
64
|
+
const pick = () => text('Which agents? Numbers and/or ids, separated by commas or spaces.', (line) => {
|
|
65
|
+
const { ids, unknown } = pickAgents(line, rows);
|
|
66
|
+
if (unknown.length) { say(`Not in the list above: ${unknown.join(', ')}.`, 'warn'); return pick(); }
|
|
67
|
+
if (!ids.length) { say('No agent picked.', 'warn'); return pick(); }
|
|
68
|
+
done(kept, ids);
|
|
69
|
+
});
|
|
70
|
+
pick();
|
|
71
|
+
});
|
|
72
|
+
nextKeep();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function startNew() {
|
|
76
|
+
let hats;
|
|
77
|
+
try { hats = await api.hats(); } catch (e) { return fail("Couldn't list hats", e); }
|
|
78
|
+
W = { mode: 'new', hats };
|
|
79
|
+
const askName = () => text('What should the new hat be called?', (line) => {
|
|
80
|
+
const name = line.trim(); const key = slugify(name);
|
|
81
|
+
if (key.length < 2 || key.length > 41) { say('A hat name needs 2 to 41 letters or digits. Try another.', 'warn'); return askName(); }
|
|
82
|
+
if (hats.some(h => h.key === key)) { say(`A hat with the key "${key}" already exists. Try another name.`, 'warn'); return askName(); }
|
|
83
|
+
W.name = name; W.key = key; askBase();
|
|
84
|
+
});
|
|
85
|
+
const askBase = () => yn('Start from an empty hat?', (yes) => {
|
|
86
|
+
if (yes) { W.base = null; return agents(); }
|
|
87
|
+
hats.forEach((h, i) => say(` ${i + 1}. ${nameOf(h)} ${h.key}`));
|
|
88
|
+
const pick = () => text('Which hat should it start from? A number or a name.', (line) => {
|
|
89
|
+
const t = line.trim();
|
|
90
|
+
const m = /^\d+$/.test(t) ? { hat: hats[Number(t) - 1] } : matchHat(t, hats);
|
|
91
|
+
if (m.ambiguous) { say(`"${t}" matches more than one hat: ${hatChoices(m.ambiguous)}. Be more specific.`, 'warn'); return pick(); }
|
|
92
|
+
if (!m.hat) { say(`No hat matches "${t}".`, 'warn'); return pick(); }
|
|
93
|
+
W.base = m.hat; agents();
|
|
94
|
+
});
|
|
95
|
+
pick();
|
|
96
|
+
});
|
|
97
|
+
const agents = () => agentSteps(W.base?.definition?.agents?.specialists || [], (kept, added) => {
|
|
98
|
+
const list = [...kept, ...added];
|
|
99
|
+
let built;
|
|
100
|
+
try { built = buildCustomDefinition({ base: W.base?.definition || null, name: W.name, agents: list }); } catch (e) { return fail('Could not build the hat', e); }
|
|
101
|
+
yn(`Create the ${W.name} hat with ${list.length ? joinNice(list) : 'no agents'}? It offers only its own tools and agents.`, async (yes) => {
|
|
102
|
+
if (!yes) { say('Nothing created.', 'dim'); return end(); }
|
|
103
|
+
const { key, name } = W;
|
|
104
|
+
try {
|
|
105
|
+
const r = await api.createHat({ key, name, definition: built.definition, visibility: 'org' });
|
|
106
|
+
say(`Hat ${name} created (rev ${r?.hat?.current_revision ?? 1}). It is in your hat list now, ready to wear.`, 'ok');
|
|
107
|
+
for (const w of Array.isArray(r?.warnings) ? r.warnings : []) say(`Note: ${typeof w === 'string' ? w : w?.message || JSON.stringify(w)}`, 'warn');
|
|
108
|
+
} catch (e) { say(`Hat not created: ${e?.message || 'the server refused'}.`, 'warn'); }
|
|
109
|
+
end();
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
askName();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function findCustom(arg, verb) {
|
|
116
|
+
let hats;
|
|
117
|
+
try { hats = await api.hats(); } catch (e) { say(`Couldn't list hats: ${e?.message || 'failed'}.`, 'warn'); return null; }
|
|
118
|
+
const mine = hats.filter(isCustom);
|
|
119
|
+
if (!arg) { say(`Which hat? ${mine.length ? `Your own hats: ${hatChoices(mine)}.` : 'You have no hats of your own yet. /hat new makes one.'}`, 'warn'); return null; }
|
|
120
|
+
const m = matchHat(arg, hats);
|
|
121
|
+
if (m.ambiguous) { say(`"${arg}" matches more than one hat: ${hatChoices(m.ambiguous)}. Be more specific.`, 'warn'); return null; }
|
|
122
|
+
if (!m.hat) { say(`No hat matches "${arg}". Your own hats: ${hatChoices(mine) || 'none yet'}.`, 'warn'); return null; }
|
|
123
|
+
if (!isCustom(m.hat)) {
|
|
124
|
+
say(verb === 'edit'
|
|
125
|
+
? `${nameOf(m.hat)} is a built-in hat, so it is not edited here. It can be reset, or used as a starting point with /hat new.`
|
|
126
|
+
: `${nameOf(m.hat)} is a built-in hat and cannot be deleted. It can be reset, or used as a starting point with /hat new.`, 'warn');
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
return m.hat;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function startEdit(arg) {
|
|
133
|
+
const hat = await findCustom(arg, 'edit'); if (!hat) return;
|
|
134
|
+
W = { mode: 'edit', hat };
|
|
135
|
+
const def = hat.definition || {};
|
|
136
|
+
agentSteps(def.agents?.specialists || [], (kept, added) => {
|
|
137
|
+
const current = def.agents?.specialists || [];
|
|
138
|
+
const removeAgents = current.filter(a => !kept.includes(a));
|
|
139
|
+
const rename = () => yn('Rename it?', (yes) => {
|
|
140
|
+
if (!yes) return summary(null);
|
|
141
|
+
const ask2 = () => text('What is the new name?', (line) => { const n = line.trim(); if (!n) { say('A name cannot be empty.', 'warn'); return ask2(); } summary(n); });
|
|
142
|
+
ask2();
|
|
143
|
+
});
|
|
144
|
+
const summary = (newName) => {
|
|
145
|
+
let ed;
|
|
146
|
+
try { ed = applyHatEdits(def, { addAgents: added, removeAgents }); } catch (e) { return fail('Could not apply the edit', e); }
|
|
147
|
+
const changes = [...ed.changes];
|
|
148
|
+
if (newName && newName !== hat.name) changes.push(`name ${newName}`); else newName = null;
|
|
149
|
+
if (!changes.length) { say(`Nothing changed on the ${nameOf(hat)} hat. Nothing was sent.`); return end(); }
|
|
150
|
+
yn(`Update the ${nameOf(hat)} hat: ${changes.join('; ')}?`, async (yes) => {
|
|
151
|
+
if (!yes) { say('Nothing changed.', 'dim'); return end(); }
|
|
152
|
+
try {
|
|
153
|
+
const r = await api.updateHat(hat.id, { definition: ed.definition, ...(newName ? { name: newName } : {}), change_reason: 'Edited from the quox window' });
|
|
154
|
+
say(`Hat ${newName || nameOf(hat)} updated (rev ${r?.hat?.current_revision ?? 'next'}).`, 'ok');
|
|
155
|
+
for (const w of Array.isArray(r?.warnings) ? r.warnings : []) say(`Note: ${typeof w === 'string' ? w : w?.message || JSON.stringify(w)}`, 'warn');
|
|
156
|
+
} catch (e) { say(`Hat not updated: ${e?.message || 'the server refused'}.`, 'warn'); }
|
|
157
|
+
end();
|
|
158
|
+
});
|
|
159
|
+
};
|
|
160
|
+
rename();
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function startDelete(arg) {
|
|
165
|
+
const hat = await findCustom(arg, 'delete'); if (!hat) return;
|
|
166
|
+
W = { mode: 'delete', hat };
|
|
167
|
+
yn(`Delete the ${nameOf(hat)} hat? Conversations wearing it go back to the default hat.`, async (yes) => {
|
|
168
|
+
if (!yes) { say('Nothing deleted.', 'dim'); return end(); }
|
|
169
|
+
try {
|
|
170
|
+
const r = await api.deleteHat(hat.id);
|
|
171
|
+
const d = r?.deleted || {};
|
|
172
|
+
say(`Hat ${nameOf(hat)} deleted. Conversations moved off it: ${d.bindings_cleared ?? 0}. Personal choices cleared: ${d.user_prefs_cleared ?? 0}.`, 'ok');
|
|
173
|
+
} catch (e) { say(`Hat not deleted: ${e?.message || 'the server refused'}.`, 'warn'); }
|
|
174
|
+
end();
|
|
175
|
+
}, 'It cannot be undone');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
active: () => Boolean(W),
|
|
180
|
+
/** the y/N/c card currently on screen (for the ask machinery), or null */
|
|
181
|
+
card: () => (W?.wait?.kind === 'yn' ? W.card : null),
|
|
182
|
+
start(kind, arg = '') {
|
|
183
|
+
if (W) { say('A hat walk is already open. Esc cancels it.', 'warn'); return; }
|
|
184
|
+
if (kind === 'new') return startNew();
|
|
185
|
+
if (kind === 'edit') return startEdit(arg);
|
|
186
|
+
return startDelete(arg);
|
|
187
|
+
},
|
|
188
|
+
/** One typed line. true = consumed. A slash command ends the walk and is left to run. */
|
|
189
|
+
answer(line) {
|
|
190
|
+
if (!W?.wait) return false;
|
|
191
|
+
const t = String(line ?? '').trim();
|
|
192
|
+
if (t.startsWith('/')) { W = null; say('Hat walk ended. Nothing was sent.', 'dim'); bump(); return false; }
|
|
193
|
+
const w = W.wait;
|
|
194
|
+
if (w.kind === 'yn') {
|
|
195
|
+
const yes = /^y(es)?$/i.test(t);
|
|
196
|
+
if (!yes && t && !/^no?$/i.test(t)) return false; // c or free text: the card's normal chat behaviour
|
|
197
|
+
W.wait = null; Promise.resolve(w.on(yes)).catch((e) => fail('Hat walk failed', e));
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
if (!t) return true;
|
|
201
|
+
W.wait = null; Promise.resolve(w.on(t)).catch((e) => fail('Hat walk failed', e));
|
|
202
|
+
return true;
|
|
203
|
+
},
|
|
204
|
+
cancel() { if (!W) return false; W = null; say('Hat walk cancelled. Nothing was sent.', 'dim'); bump(); return true; },
|
|
205
|
+
};
|
|
206
|
+
}
|