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,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* /watch <anything>: turn a site, repo, server, competitor page, metric or condition into an ongoing observer.
|
|
3
|
+
* Backend (real): an agent schedule on the tasks service (POST /api/agent-schedules). Each tick the scheduler runs
|
|
4
|
+
* the watch prompt as an objective and escalates to the owner's Approvals inbox per the escalation policy.
|
|
5
|
+
*/
|
|
6
|
+
const DAY = 1440;
|
|
7
|
+
const PAGE_WORDS = /\b(competitor|competitors|pricing|price|prices|plans?|changelog|blog|docs|landing)\b/i;
|
|
8
|
+
const SERVER_WORDS = /\b(server|host|box|vm|node|gitea|nginx|postgres|redis|db|database|service|container|docker|disk|cpu|memory|ram|load|health|healthy|status)\b/i;
|
|
9
|
+
const METRIC_WORDS = /\b(metric|signups?|sign-ups?|revenue|mrr|arr|traffic|errors?|latency|conversion|sales|users?|churn|orders?|visits?)\b/i;
|
|
10
|
+
const URL_RE = /\bhttps?:\/\/[^\s]+/i;
|
|
11
|
+
const GITHUB_RE = /\b(?:https?:\/\/)?github\.com\/[\w.-]+\/[\w.-]+/i;
|
|
12
|
+
const DOMAIN_RE = /\b(?:[a-z0-9-]+\.)+[a-z]{2,}(?:\/[^\s]*)?/i;
|
|
13
|
+
|
|
14
|
+
const DEFAULT_CONDITION = {
|
|
15
|
+
site: 'it is down, returns an error status, or responds very slowly',
|
|
16
|
+
server: 'it is unhealthy or unreachable',
|
|
17
|
+
page: 'the page changes in a way that matters (prices, plans, wording, new sections)',
|
|
18
|
+
repo: 'something needs attention: failing CI, new issues or security alerts'
|
|
19
|
+
};
|
|
20
|
+
const unitMin = (n, u) => { const x = u.toLowerCase(); return x.startsWith('h') ? n * 60 : x.startsWith('d') ? n * DAY : n; };
|
|
21
|
+
const strip = (s) => s.replace(/[),.;]+$/, '');
|
|
22
|
+
|
|
23
|
+
/** "quox.ai for downtime every 5 minutes" -> { kind, target, condition, everyMin|null } */
|
|
24
|
+
export function parseWatch(text) {
|
|
25
|
+
let t = String(text || '').trim();
|
|
26
|
+
let everyMin = null;
|
|
27
|
+
const ev = t.match(/\b(?:every|each)\s+(\d+)\s*(minutes?|mins?|m|hours?|hrs?|h|days?|d)\b/i);
|
|
28
|
+
if (ev) { everyMin = unitMin(Number(ev[1]), ev[2]); t = t.replace(ev[0], ' '); }
|
|
29
|
+
else {
|
|
30
|
+
const w = t.match(/\b(hourly|daily)\b/i);
|
|
31
|
+
if (w) { everyMin = w[1].toLowerCase() === 'hourly' ? 60 : DAY; t = t.replace(w[0], ' '); }
|
|
32
|
+
}
|
|
33
|
+
t = t.replace(/\s+/g, ' ').trim();
|
|
34
|
+
if (!t) return { kind: 'none', target: '', condition: '', everyMin };
|
|
35
|
+
|
|
36
|
+
let left = t, cond = '';
|
|
37
|
+
const m = t.match(/^(.*?)\s+(?:for|when|whenever|if)\s+(.+)$/i);
|
|
38
|
+
if (m && m[1].trim()) { left = m[1].trim(); cond = m[2].trim(); }
|
|
39
|
+
|
|
40
|
+
const url = left.match(URL_RE)?.[0];
|
|
41
|
+
const gh = left.match(GITHUB_RE)?.[0];
|
|
42
|
+
const dom = !url && !gh ? left.match(DOMAIN_RE)?.[0] : null;
|
|
43
|
+
const out = (kind, target, condition) => ({ kind, target, condition, everyMin });
|
|
44
|
+
|
|
45
|
+
if (gh || /\brepo(sitory)?\b/i.test(left)) {
|
|
46
|
+
return out('repo', gh ? strip(gh).replace(/^https?:\/\//, '') : (left.replace(/\brepo(sitory)?\b/i, '').trim() || left), cond || DEFAULT_CONDITION.repo);
|
|
47
|
+
}
|
|
48
|
+
if (url || dom) {
|
|
49
|
+
const target = strip(url || dom);
|
|
50
|
+
const pageish = PAGE_WORDS.test(left) && !/\b(down|downtime|uptime|outage)\b/i.test(cond);
|
|
51
|
+
return pageish ? out('page', target, cond || DEFAULT_CONDITION.page) : out('site', target, cond || DEFAULT_CONDITION.site);
|
|
52
|
+
}
|
|
53
|
+
if (PAGE_WORDS.test(left) && !METRIC_WORDS.test(left)) return out('page', left, cond || DEFAULT_CONDITION.page);
|
|
54
|
+
if (METRIC_WORDS.test(left)) return out('metric', left, cond || left);
|
|
55
|
+
if (SERVER_WORDS.test(left) && left.split(' ').length <= 3) {
|
|
56
|
+
const target = left.replace(/\b(health|healthy|status)\b/gi, '').replace(/\s+/g, ' ').trim() || left;
|
|
57
|
+
return out('server', target, cond || DEFAULT_CONDITION.server);
|
|
58
|
+
}
|
|
59
|
+
return out('condition', cond ? left : t, cond || t);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Cron-friendly interval in minutes (floor 5, whole hours above an hour, cap one day). */
|
|
63
|
+
export function normalizeEvery(min) {
|
|
64
|
+
const n = Number(min);
|
|
65
|
+
if (!Number.isFinite(n) || n <= 0) return 15;
|
|
66
|
+
if (n >= DAY) return DAY;
|
|
67
|
+
if (n < 5) return 5;
|
|
68
|
+
if (n < 60) return [5, 10, 15, 20, 30, 60].reduce((a, b) => (Math.abs(b - n) < Math.abs(a - n) ? b : a));
|
|
69
|
+
return Math.min(23, Math.max(1, Math.round(n / 60))) * 60;
|
|
70
|
+
}
|
|
71
|
+
export function everyToCron(min) {
|
|
72
|
+
const n = normalizeEvery(min);
|
|
73
|
+
if (n >= DAY) return '0 9 * * *';
|
|
74
|
+
if (n === 60) return '0 * * * *';
|
|
75
|
+
if (n > 60) return `0 */${n / 60} * * *`;
|
|
76
|
+
return `*/${n} * * * *`;
|
|
77
|
+
}
|
|
78
|
+
export const everyLabel = (min) => { const n = normalizeEvery(min); return n >= DAY ? 'day' : n === 60 ? 'hour' : n > 60 ? `${n / 60} h` : `${n} min`; };
|
|
79
|
+
|
|
80
|
+
const KIND = { site: 'website', server: 'server', page: 'web page', repo: 'repository', metric: 'metric', condition: 'condition' };
|
|
81
|
+
const slug = (s) => { const x = String(s).replace(/^https?:\/\//i, '').replace(/\/+$/, ''); return x.length > 60 ? `${x.slice(0, 59)}…` : x; };
|
|
82
|
+
|
|
83
|
+
const watchPrompt = (name, w) => [
|
|
84
|
+
`You are running a scheduled watch named "${name}".`,
|
|
85
|
+
`Target (${KIND[w.kind] || w.kind}): ${w.target}.`,
|
|
86
|
+
`Condition to detect: ${w.condition}.`,
|
|
87
|
+
'Check the target now using the read-only tools you have. Change nothing.',
|
|
88
|
+
'If the condition is NOT met, finish with the single line "ok" and raise nothing: stay quiet.',
|
|
89
|
+
'If it IS met, raise it to the owner as a notification or inbox item: what you saw, when, and the evidence, in 3 lines or fewer.',
|
|
90
|
+
'If you cannot check it at all (no tool, no access), say exactly that once; do not pretend it is fine.'
|
|
91
|
+
].join('\n');
|
|
92
|
+
|
|
93
|
+
/** -> { body (POST /api/agent-schedules), card[] , every } */
|
|
94
|
+
export function buildWatchPlan(w) {
|
|
95
|
+
const every = normalizeEvery(w.everyMin ?? 15);
|
|
96
|
+
const cron = everyToCron(every);
|
|
97
|
+
const name = `watch:${slug(w.target)}`;
|
|
98
|
+
const budget = { maxTokensPerRun: 20000, maxToolCallsPerRun: 10, maxDurationMs: 120000 };
|
|
99
|
+
const body = {
|
|
100
|
+
agentId: 'quox', name, cronExpression: cron, timezone: 'UTC',
|
|
101
|
+
taskTemplate: { title: name, prompt: watchPrompt(name, w) },
|
|
102
|
+
escalationPolicy: { onFailure: 'ESCALATE', escalateTo: null },
|
|
103
|
+
budget, maxConsecutiveFailures: 5
|
|
104
|
+
};
|
|
105
|
+
const card = [
|
|
106
|
+
['Watch preview (nothing is created until you answer):', 'accent'],
|
|
107
|
+
` Watching ${w.target} (${KIND[w.kind] || w.kind})`,
|
|
108
|
+
` Triggers ${w.condition}`,
|
|
109
|
+
` Checks every ${everyLabel(every)} (cron ${cron}, UTC)`,
|
|
110
|
+
' Notifies you, in your Approvals inbox, only when it triggers or the watch keeps failing',
|
|
111
|
+
` Runs as agent quox, read-only by instruction, ${budget.maxToolCallsPerRun} tool calls per check`
|
|
112
|
+
];
|
|
113
|
+
return { body, card, every };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const USAGE = 'Usage: /watch <what> [for <condition>] [every 5 min] e.g. /watch quox.ai for downtime, /watch gitea health. Also /watch list, /watch stop <id>.';
|
|
117
|
+
const why = (e) => `${e?.status ? `HTTP ${e.status}: ` : ''}${e?.message || 'failed'}`;
|
|
118
|
+
|
|
119
|
+
export function createWatchCmd({ api, say, confirm }) {
|
|
120
|
+
const listed = async () => {
|
|
121
|
+
const rows = await api.watchList();
|
|
122
|
+
return (Array.isArray(rows) ? rows : rows?.schedules || []).filter((s) => String(s.name || '').startsWith('watch:'));
|
|
123
|
+
};
|
|
124
|
+
async function list() {
|
|
125
|
+
let rows;
|
|
126
|
+
try { rows = await listed(); } catch (e) { return say(`Could not list watches: ${why(e)}`, 'warn'); }
|
|
127
|
+
if (!rows.length) return say('No watches yet. /watch quox.ai for downtime sets one up.');
|
|
128
|
+
say(`Watches (${rows.length}):`);
|
|
129
|
+
for (const s of rows) say(` ${s.id} ${s.name.slice(6)} cron ${s.cron_expression} ${Number(s.enabled) === 0 ? 'stopped' : (s.last_status || 'no check yet')}`);
|
|
130
|
+
return say('Stop one with /watch stop <id>.', 'dim');
|
|
131
|
+
}
|
|
132
|
+
async function stop(idArg) {
|
|
133
|
+
if (!idArg) return say('Usage: /watch stop <id> (/watch list shows ids)', 'warn');
|
|
134
|
+
let rows;
|
|
135
|
+
try { rows = await listed(); } catch (e) { return say(`Could not read watches: ${why(e)}`, 'warn'); }
|
|
136
|
+
const hits = rows.filter((s) => s.id === idArg || s.id.toLowerCase().startsWith(idArg.toLowerCase()));
|
|
137
|
+
if (hits.length !== 1) return say(hits.length ? `"${idArg}" matches ${hits.length} watches; use more of the id.` : `No watch "${idArg}". /watch list shows them.`, 'warn');
|
|
138
|
+
try { await api.watchStop(hits[0].id); } catch (e) { return say(`Could not stop ${hits[0].id}: ${why(e)}`, 'warn'); }
|
|
139
|
+
return say(`✔ Stopped watch ${hits[0].id} (${hits[0].name.slice(6)}).`, 'ok');
|
|
140
|
+
}
|
|
141
|
+
function create(text) {
|
|
142
|
+
const w = parseWatch(text);
|
|
143
|
+
if (!w.target) return say(USAGE, 'warn');
|
|
144
|
+
const plan = buildWatchPlan(w);
|
|
145
|
+
confirm.ask(plan.card, async () => {
|
|
146
|
+
let r;
|
|
147
|
+
try { r = await api.watchCreate(plan.body); } catch (e) { return say(`Could not create the watch: ${why(e)}. Nothing is watching.`, 'warn'); }
|
|
148
|
+
if (!r?.id) return say('The server returned no watch id, so I cannot confirm it exists. /watch list shows what is there.', 'warn');
|
|
149
|
+
say(`✔ Watching ${w.target} (id ${r.id}). First check within ${everyLabel(plan.every)}.`, 'ok');
|
|
150
|
+
say(` Stop it: /watch stop ${r.id} See all: /watch list`);
|
|
151
|
+
}, { prompt: 'Create this watch?' });
|
|
152
|
+
return undefined;
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
handle(arg) {
|
|
156
|
+
const a = String(arg || '').trim();
|
|
157
|
+
if (!a) return say(USAGE);
|
|
158
|
+
if (!api.watchCreate) return say('Watches are not available in this mode.', 'warn');
|
|
159
|
+
if (/^list$/i.test(a)) return list();
|
|
160
|
+
const s = a.match(/^(?:stop|rm)\b\s*(.*)$/i);
|
|
161
|
+
return s ? stop(s[1].trim()) : create(a);
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** /wish: the prompt that makes Quox draft a spec, reading its shape back, and the follow-up prompts. */
|
|
2
|
+
export const SHAPE_LABEL = { skill: 'a skill', workflow: 'a workflow', power: 'a power from a repo', code: 'needs code' };
|
|
3
|
+
|
|
4
|
+
export const wishPrompt = (wish) => 'The user wishes Quox could do something it cannot do today. Do NOT build or install anything and do not call any state-changing tool. '
|
|
5
|
+
+ 'Reply with a SHORT design spec, plain lines, no preamble, using exactly these labels:\n'
|
|
6
|
+
+ 'What it does: one or two sentences.\n'
|
|
7
|
+
+ 'Uses: which existing Quox tools, CLI commands or APIs it would call.\n'
|
|
8
|
+
+ 'Hat: which hat and permissions it needs.\n'
|
|
9
|
+
+ 'Tests: what would prove it works.\n'
|
|
10
|
+
+ 'Shape: exactly one of skill | workflow | power | code (skill = a procedure an agent follows; workflow = a scheduled or triggered sequence of existing steps; power = packaged from a repo; code = needs new code).\n\n'
|
|
11
|
+
+ `Wish: ${String(wish).trim().slice(0, 1500)}`;
|
|
12
|
+
|
|
13
|
+
/** -> 'skill' | 'workflow' | 'power' | 'code' | null (no readable Shape line). */
|
|
14
|
+
export function parseShape(spec) {
|
|
15
|
+
const m = /^\s*shape\s*:\s*(skill|workflow|power|code)\b/im.exec(String(spec || ''));
|
|
16
|
+
return m ? m[1].toLowerCase() : null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const creationPrompt = (shape, wish, spec) => (shape === 'skill'
|
|
20
|
+
? `The user approved this spec. Create it now as a skill with skill_manage (op=create; it lands experimental and queued for review, which is expected). Use only what the spec says.\n\nWish: ${wish}\n\n${spec}`
|
|
21
|
+
: `The user approved this spec. Create it now as a workflow with your workflow tool, left inactive: do not activate it or deploy it, and do not schedule it. Use only what the spec says.\n\nWish: ${wish}\n\n${spec}`);
|
|
22
|
+
|
|
23
|
+
export const requestText = (wish, spec) => `Feature request from /wish: ${wish}\n\nDrafted spec:\n${String(spec).trim()}\n\nNo shape of skill or workflow fit, so this needs the normal build and review path.`;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** /wish: draft a governed spec, show it as one card, and on y create (skill, workflow) or file (power, code). Never installs code. */
|
|
2
|
+
import { wishPrompt, parseShape, creationPrompt, requestText, SHAPE_LABEL } from './wish.js';
|
|
3
|
+
|
|
4
|
+
const clip = (s, n) => { const t = String(s || '').replace(/\s+/g, ' ').trim(); return t.length > n ? `${t.slice(0, n - 1)}…` : t; };
|
|
5
|
+
|
|
6
|
+
export function createWishCmd({ api, say, card, runTurn, draft }) {
|
|
7
|
+
return async function wish(arg) {
|
|
8
|
+
const text = String(arg || '').trim();
|
|
9
|
+
if (!text) return say('Usage: /wish <what you wish Quox could do>', 'warn');
|
|
10
|
+
if (!draft) return say('/wish is not available in this mode.', 'warn');
|
|
11
|
+
say('Drafting a spec for that…', 'dim');
|
|
12
|
+
let spec;
|
|
13
|
+
try { spec = await draft(wishPrompt(text)); } catch (e) { spec = null; say(`Draft failed: ${e?.message || e}`, 'warn'); }
|
|
14
|
+
if (!spec || !String(spec).trim()) return say('Quox could not draft a spec for that, so nothing was proposed. Try /wish again.', 'warn');
|
|
15
|
+
const shape = parseShape(spec);
|
|
16
|
+
const body = String(spec).trim().split('\n').slice(0, 14).map(l => clip(l, 110));
|
|
17
|
+
const plan = shape === 'skill' || shape === 'workflow'
|
|
18
|
+
? `On y: Quox creates it as ${shape === 'skill' ? 'an experimental skill queued for human review' : 'an inactive workflow'} through the normal governed path (you will see its tool call and receipt).`
|
|
19
|
+
: `On y: the spec is filed as a feature request (${shape ? SHAPE_LABEL[shape] : 'shape unclear, treated as needing code'}). Nothing is installed; code goes through the normal build and review path.`;
|
|
20
|
+
card.ask([`wish │ ${clip(text, 80)}`, ...body], // the plan is the card's "do" row
|
|
21
|
+
async () => {
|
|
22
|
+
if (shape === 'skill' || shape === 'workflow') { say(`Creating the ${shape}…`, 'dim'); return runTurn(creationPrompt(shape, text, spec)); }
|
|
23
|
+
if (!api.gripe) return say('Filing is not available in this mode; the spec is above.', 'warn');
|
|
24
|
+
const g = await api.gripe(requestText(text, spec).slice(0, 4000), { yes: true });
|
|
25
|
+
if (!g.ok) return say(`Not filed: ${g.message}`, 'warn');
|
|
26
|
+
const url = (g.notes || []).find(n => n.kind === 'created')?.url;
|
|
27
|
+
say(url ? `Filed as a feature request: ${url}. Nothing was installed.` : 'Filed as a feature request. Nothing was installed.', 'ok');
|
|
28
|
+
return undefined;
|
|
29
|
+
}, 'Cancelled; nothing was created or filed.', { who: 'Quox', question: shape === 'skill' || shape === 'workflow' ? `Create the ${shape}?` : 'File it as a governed spec?', action: plan.replace(/^On y: /, '') });
|
|
30
|
+
return undefined;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QUOXON-MESH: slot chat in the quox TUI. Owner, 2026-10-14: "it would be cool if session to session messaging
|
|
3
|
+
* was faster, more responsive... instant... like chat". A window holding a slot listens on the collector's slot
|
|
4
|
+
* stream (GET /api/v1/slots/:name/stream, SSE, pushed from the same notify bus as `quox slot wait`) and renders
|
|
5
|
+
* each message as a chat line above the composer. `@slot text`, `/say` and `/send` post without a model turn.
|
|
6
|
+
*
|
|
7
|
+
* Pure helpers and the listener live here; app.js wires them.
|
|
8
|
+
*/
|
|
9
|
+
import { readFileSync, writeFileSync, mkdirSync, statSync, readdirSync, renameSync, existsSync } from 'node:fs';
|
|
10
|
+
import { homedir } from 'node:os';
|
|
11
|
+
import { join, dirname, basename, resolve, isAbsolute } from 'node:path';
|
|
12
|
+
import { scrub } from './transcript.js';
|
|
13
|
+
import { looksLikeKey } from './connect.js';
|
|
14
|
+
import { addressLabel } from './session.js';
|
|
15
|
+
|
|
16
|
+
// ---- outbound ------------------------------------------------------------------------------------
|
|
17
|
+
const AT_RE = /^@([A-Za-z0-9][A-Za-z0-9-]{0,31}):?[ \t]+(\S[\s\S]*)$/;
|
|
18
|
+
// The server's own leading-address forms (devSlotMessagesStore parseAddressedTo) stay message text.
|
|
19
|
+
const RESERVED = /^(all|S\d+|M\d+)$/i;
|
|
20
|
+
|
|
21
|
+
/** `@quox2 hello` -> { slot: 'quox2', text: 'hello' }; anything else -> null. */
|
|
22
|
+
export function parseAtSend(text) {
|
|
23
|
+
const m = AT_RE.exec(String(text || ''));
|
|
24
|
+
if (!m || RESERVED.test(m[1])) return null;
|
|
25
|
+
const body = m[2].replace(/\s+$/, '');
|
|
26
|
+
return body ? { slot: m[1].toLowerCase(), text: body } : null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const labelSafe = (s) => String(s || '').replace(/[^A-Za-z0-9._@:\- ]/g, '').replace(/\s+/g, ' ').trim().slice(0, 24);
|
|
30
|
+
|
|
31
|
+
/** What a window signs its lines as: "quox1 (Adam)", the slot alone, the name alone, or null (server derives). */
|
|
32
|
+
export function senderLabel({ slot, display }) {
|
|
33
|
+
const d = labelSafe(display);
|
|
34
|
+
if (slot) return d ? `${slot} (${d})` : slot;
|
|
35
|
+
return d || null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ---- files ---------------------------------------------------------------------------------------
|
|
39
|
+
export const FILE_CAP = 64 * 1024;
|
|
40
|
+
const FILE_HEAD = '📎 ';
|
|
41
|
+
const FENCE = '~~~~~~'; // a fence a pasted Markdown file is very unlikely to contain
|
|
42
|
+
|
|
43
|
+
/** The wire form of a file: one header line, then the body inside a long tilde fence (renders as a block in Discord/Matrix too). */
|
|
44
|
+
export function fileMessage({ name, body, size }) {
|
|
45
|
+
const lines = String(body).split('\n').length;
|
|
46
|
+
return `${FILE_HEAD}${basename(String(name))} (${fmtSize(size ?? Buffer.byteLength(String(body)))}, ${lines} line${lines === 1 ? '' : 's'})\n${FENCE}\n${body}\n${FENCE}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** -> { name, meta, body } for a file message, else null. */
|
|
50
|
+
export function parseFileMessage(text) {
|
|
51
|
+
const t = String(text || '');
|
|
52
|
+
if (!t.startsWith(FILE_HEAD)) return null;
|
|
53
|
+
const m = /^📎 (.+?) \(([^)\n]*)\)\n~~~~~~\n([\s\S]*)\n~~~~~~$/.exec(t);
|
|
54
|
+
return m ? { name: m[1], meta: m[2], body: m[3] } : null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function fmtSize(n) { return n < 1024 ? `${n} B` : `${(n / 1024).toFixed(n < 10240 ? 1 : 0)} KB`; }
|
|
58
|
+
|
|
59
|
+
const EXTRA_SECRETS = [/\bgsk_[A-Za-z0-9]{20,}/g, /\bhf_[A-Za-z0-9]{20,}/g, /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g];
|
|
60
|
+
|
|
61
|
+
/** The CLI redactor (transcript.js scrub) plus the paste guard's key shapes. -> { text, hits }. */
|
|
62
|
+
export function redactSecrets(text) {
|
|
63
|
+
const src = String(text);
|
|
64
|
+
let out = scrub(src);
|
|
65
|
+
for (const re of EXTRA_SECRETS) out = out.replace(re, '[redacted-key]');
|
|
66
|
+
const count = (s) => (s.match(/\[redacted[^\]]*\]/g) || []).length;
|
|
67
|
+
let hits = count(out) - count(src);
|
|
68
|
+
if (!hits && looksLikeKey(src)) hits = 1; // a key shape the patterns above missed: never call it clean
|
|
69
|
+
return { text: out, hits };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Read a local file for /send. Text up to FILE_CAP goes inline; larger or binary files are refused with the cap
|
|
74
|
+
* (slots have no attachment store). -> { ok, name, path, body, redacted, hits, size } | { ok:false, message }.
|
|
75
|
+
*/
|
|
76
|
+
export function prepareFile(p, { cwd = process.cwd() } = {}) {
|
|
77
|
+
const raw = String(p || '').trim().replace(/^['"]|['"]$/g, '');
|
|
78
|
+
if (!raw) return { ok: false, message: 'Usage: /send <slot> <file>' };
|
|
79
|
+
const full = isAbsolute(raw) ? raw : resolve(cwd, raw.replace(/^~(?=\/)/, homedir()));
|
|
80
|
+
let st;
|
|
81
|
+
try { st = statSync(full); } catch { return { ok: false, message: `No such file: ${raw}` }; }
|
|
82
|
+
if (st.isDirectory()) return { ok: false, message: `${raw} is a directory; /send takes one file.` };
|
|
83
|
+
if (st.size > FILE_CAP) return { ok: false, message: `${raw} is ${fmtSize(st.size)}; /send sends text files up to 64 KB inline (slots have no attachment store yet).` };
|
|
84
|
+
const buf = readFileSync(full);
|
|
85
|
+
if (buf.subarray(0, 8192).includes(0)) return { ok: false, message: `${raw} looks binary; /send sends text files only (up to 64 KB).` };
|
|
86
|
+
const body = buf.toString('utf8');
|
|
87
|
+
const { text, hits } = redactSecrets(body);
|
|
88
|
+
return { ok: true, name: basename(full), path: full, body, redacted: text, hits, size: st.size };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Tab completion for /send's path argument: entries of the typed directory, directories with a trailing slash. */
|
|
92
|
+
export function pathItems(partial, { cwd = process.cwd(), max = 200 } = {}) {
|
|
93
|
+
const p = String(partial || '');
|
|
94
|
+
const slash = p.lastIndexOf('/');
|
|
95
|
+
const dirPart = slash >= 0 ? p.slice(0, slash + 1) : '';
|
|
96
|
+
const base = dirPart.startsWith('~/') ? join(homedir(), dirPart.slice(2)) : isAbsolute(dirPart) ? dirPart : resolve(cwd, dirPart || '.');
|
|
97
|
+
let names = [];
|
|
98
|
+
try { names = readdirSync(base, { withFileTypes: true }); } catch { return []; }
|
|
99
|
+
const showHidden = p.slice(slash + 1).startsWith('.');
|
|
100
|
+
return names.filter(d => showHidden || !d.name.startsWith('.')).slice(0, max)
|
|
101
|
+
.map(d => ({ value: dirPart + d.name + (d.isDirectory() ? '/' : ''), desc: d.isDirectory() ? 'dir' : '' }));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ---- inbound -------------------------------------------------------------------------------------
|
|
105
|
+
const hhmm = (iso) => { const d = new Date(iso || Date.now()); return Number.isNaN(d.getTime()) ? '' : `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`; };
|
|
106
|
+
|
|
107
|
+
/** Slot text arrives HTML-escaped from the Matrix bridge ("&&"); decode for display. Never renders HTML. */
|
|
108
|
+
export function decodeEntities(t) {
|
|
109
|
+
return String(t ?? '').replace(/&(amp|lt|gt|quot|apos|#39|#x27);/gi, (_, e) => ({ amp: '&', lt: '<', gt: '>', quot: '"', apos: "'", '#39': "'", '#x27': "'" })[e.toLowerCase()]);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** The slot a session name currently holds (from api.slots() rows), or null. Lets a broadcast show its reply address. */
|
|
113
|
+
export function slotOfHolder(rows, name) {
|
|
114
|
+
const n = String(name || '').replace(/\s*\(.*\)\s*$/, '').trim().toLowerCase(); if (!n) return null;
|
|
115
|
+
const r = (Array.isArray(rows) ? rows : []).find(x => x?.name && x.state !== 'free' && String(x.holder_session || '').toLowerCase() === n);
|
|
116
|
+
return r ? r.name : null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** A slot message as a chat line: { glyph, who, time, text, file }. */
|
|
120
|
+
export function slotLine(m) {
|
|
121
|
+
m = m && typeof m.text === 'string' ? { ...m, text: decodeEntities(m.text) } : m;
|
|
122
|
+
const f = parseFileMessage(m?.text);
|
|
123
|
+
const file = f ? { name: f.name, meta: f.meta, body: f.body, lines: f.body.split('\n').length, preview: f.body.split('\n').slice(0, 3) } : null;
|
|
124
|
+
// SLOTS-REPLY: the slot is the address, so it leads; the session/display name follows in brackets. A sender
|
|
125
|
+
// with no verified slot keeps its name and is flagged as having no reply address.
|
|
126
|
+
const from = m?.from || m?.author_identity || 'someone';
|
|
127
|
+
const who = m?.from_slot ? addressLabel(m.from_slot, m?.from) : from;
|
|
128
|
+
return { glyph: '◆', who, noReply: !m?.from_slot, time: hhmm(m?.created_at), text: file ? `📎 ${f.name} (${f.meta})` : String(m?.text ?? ''), file };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Keep a slot's stream open: reconnect with the cursor, count failures distinctly, back off on failures (capped),
|
|
133
|
+
* and fall back to the wait long-poll on a server without the stream route. Never polls while the stream is up.
|
|
134
|
+
* open(query, signal) -> async iterable of SSE events; wait({ since, signal }) -> { messages, cursor }.
|
|
135
|
+
*/
|
|
136
|
+
// A slot cursor names a message ('<created_at>|<seq>'), not just a time: resuming from a bare timestamp skipped a
|
|
137
|
+
// second message in the same millisecond (collector devSlotMessagesStore parseCursor/cursorOf/isAfterCursor).
|
|
138
|
+
export function slotCursorOf(m) { return m.seq == null ? m.created_at : `${m.created_at}|${m.seq}`; }
|
|
139
|
+
export function isAfterSlotCursor(m, cursor) {
|
|
140
|
+
if (!cursor) return true;
|
|
141
|
+
const x = /^(.*)\|(\d+)$/.exec(String(cursor));
|
|
142
|
+
const ts = x ? (x[1] || null) : String(cursor); const seq = x ? Number(x[2]) : null;
|
|
143
|
+
if (seq != null && !ts) return m.seq != null && m.seq > seq;
|
|
144
|
+
if (m.created_at !== ts) return m.created_at > ts;
|
|
145
|
+
return seq == null ? true : (m.seq != null && m.seq > seq);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function createSlotListener({ open, wait = null, onMessage, onState = () => {}, delays = [1000, 2000, 5000, 10000, 30000] }) {
|
|
149
|
+
const st = { running: false, failures: 0, mode: 'stream', cursor: null, connected: false };
|
|
150
|
+
let ac = null;
|
|
151
|
+
const seen = new Set();
|
|
152
|
+
const deliver = (m) => {
|
|
153
|
+
if (!m) return;
|
|
154
|
+
if (m.created_at && isAfterSlotCursor(m, st.cursor)) st.cursor = slotCursorOf(m);
|
|
155
|
+
if (m.id) { if (seen.has(m.id)) return; seen.add(m.id); if (seen.size > 1000) seen.delete(seen.values().next().value); }
|
|
156
|
+
try { onMessage(m); } catch { /* a render failure never kills the listener */ }
|
|
157
|
+
};
|
|
158
|
+
const sleep = (ms, signal) => new Promise((r) => { const t = setTimeout(r, ms); t.unref?.(); signal.addEventListener('abort', () => { clearTimeout(t); r(); }, { once: true }); });
|
|
159
|
+
const setConn = (v) => { if (st.connected !== v) { st.connected = v; onState({ ...st }); } };
|
|
160
|
+
|
|
161
|
+
async function loop(signal, since) {
|
|
162
|
+
st.cursor = since || null;
|
|
163
|
+
let streak = 0;
|
|
164
|
+
while (!signal.aborted) {
|
|
165
|
+
try {
|
|
166
|
+
if (st.mode === 'stream') {
|
|
167
|
+
for await (const ev of open({ since: st.cursor }, signal)) {
|
|
168
|
+
if (signal.aborted) break;
|
|
169
|
+
if (ev?.type === 'connected') { streak = 0; if (ev.cursor && !st.cursor) st.cursor = ev.cursor; setConn(true); }
|
|
170
|
+
else if (ev?.type === 'slot.message') { streak = 0; deliver(ev.message); }
|
|
171
|
+
}
|
|
172
|
+
setConn(false);
|
|
173
|
+
if (signal.aborted) break;
|
|
174
|
+
await sleep(delays[0], signal); // a clean end (lifetime) reconnects promptly
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
const r = await wait({ since: st.cursor, signal });
|
|
178
|
+
if (signal.aborted) break;
|
|
179
|
+
streak = 0; setConn(true);
|
|
180
|
+
for (const m of r?.messages || []) deliver(m);
|
|
181
|
+
if (r?.cursor) st.cursor = r.cursor;
|
|
182
|
+
} catch (e) {
|
|
183
|
+
if (signal.aborted) break;
|
|
184
|
+
setConn(false);
|
|
185
|
+
if (st.mode === 'stream' && wait && e?.status === 404 && e?.code !== 'unknown slot') { st.mode = 'wait'; onState({ ...st }); continue; }
|
|
186
|
+
st.failures += 1; streak += 1;
|
|
187
|
+
onState({ ...st, error: e?.message || 'failed' });
|
|
188
|
+
await sleep(delays[Math.min(streak - 1, delays.length - 1)], signal);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return {
|
|
193
|
+
start({ since } = {}) {
|
|
194
|
+
if (st.running) return;
|
|
195
|
+
st.running = true; ac = new AbortController();
|
|
196
|
+
const sig = ac.signal;
|
|
197
|
+
loop(sig, since).finally(() => { st.running = false; st.connected = false; });
|
|
198
|
+
},
|
|
199
|
+
stop() { if (ac) ac.abort(); ac = null; st.running = false; st.connected = false; },
|
|
200
|
+
state: () => ({ ...st }),
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** SLOTS-REPLY2: is this line in slot X from X's own holder (verified from_slot, else the sender name)? */
|
|
205
|
+
export function isHolderLine(m, slot) {
|
|
206
|
+
const x = String(slot || '').toLowerCase(); if (!x) return false;
|
|
207
|
+
if (String(m?.from_slot || '').toLowerCase() === x) return true;
|
|
208
|
+
const f = String(m?.from || '').toLowerCase();
|
|
209
|
+
return f === x || f.startsWith(`${x} (`);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* SLOTS-REPLY2: after this window sends into slot X it also watches X (not just its own slot) for lines from
|
|
214
|
+
* X's holder, from the sent message's cursor on, until `ttlMs` after the last send or stop(). A reply posted
|
|
215
|
+
* in X (a sender with no reply address) is then still shown. onReply(slot, message) fires once per line.
|
|
216
|
+
* open/wait are api.slotStream / api.slotWait shaped, bound per slot by the caller: open(slot, q, signal).
|
|
217
|
+
*/
|
|
218
|
+
export function createReplyWatches({ open, wait = null, onReply, ttlMs = 30 * 60 * 1000, now = Date.now, isMine = () => false, skipSlot = () => false, setTimer = setTimeout, clearTimer = clearTimeout }) {
|
|
219
|
+
const watches = new Map();
|
|
220
|
+
const drop = (slot) => { const w = watches.get(slot); if (!w) return; clearTimer(w.timer); w.l.stop(); watches.delete(slot); };
|
|
221
|
+
const arm = (slot, sent) => {
|
|
222
|
+
if (!slot || skipSlot(slot)) return false;
|
|
223
|
+
const until = now() + ttlMs;
|
|
224
|
+
const ex = watches.get(slot);
|
|
225
|
+
if (ex) { clearTimer(ex.timer); ex.timer = setTimer(() => drop(slot), ttlMs); ex.timer.unref?.(); ex.until = until; return true; }
|
|
226
|
+
const w = { until, timer: null, l: null };
|
|
227
|
+
w.l = createSlotListener({
|
|
228
|
+
open: (q, signal) => open(slot, q, signal),
|
|
229
|
+
wait: wait ? ({ since, signal }) => wait(slot, { since, signal }) : null,
|
|
230
|
+
onMessage: (m) => {
|
|
231
|
+
if (now() > w.until) { drop(slot); return; }
|
|
232
|
+
if (isMine(m) || !isHolderLine(m, slot)) return;
|
|
233
|
+
onReply(slot, m);
|
|
234
|
+
},
|
|
235
|
+
});
|
|
236
|
+
w.timer = setTimer(() => drop(slot), ttlMs); w.timer.unref?.();
|
|
237
|
+
watches.set(slot, w);
|
|
238
|
+
w.l.start({ since: sent && sent.created_at ? slotCursorOf(sent) : undefined });
|
|
239
|
+
return true;
|
|
240
|
+
};
|
|
241
|
+
return { arm, stop: () => { for (const s of [...watches.keys()]) drop(s); }, has: (s) => watches.has(s), size: () => watches.size };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// ---- reattach record -------------------------------------------------------------------------------
|
|
245
|
+
export const lastSlotPath = () => join(homedir(), '.config', 'quox', 'quoxon-slots.json');
|
|
246
|
+
const readAll = (path) => { try { return existsSync(path) ? JSON.parse(readFileSync(path, 'utf8')) || {} : {}; } catch { return {}; } };
|
|
247
|
+
const keyOf = (profile, cwd) => `${profile || 'default'}|${cwd || ''}`;
|
|
248
|
+
function writeAll(all, path) {
|
|
249
|
+
try { mkdirSync(dirname(path), { recursive: true }); const tmp = `${path}.${process.pid}.tmp`; writeFileSync(tmp, JSON.stringify(all, null, 2)); renameSync(tmp, path); } catch { /* best effort */ }
|
|
250
|
+
}
|
|
251
|
+
/** The slot this profile last held in this directory, or null. */
|
|
252
|
+
export function loadLastSlot({ profile, cwd, path = lastSlotPath() }) {
|
|
253
|
+
const e = readAll(path)[keyOf(profile, cwd)];
|
|
254
|
+
return e && typeof e.slot === 'string' ? e : null;
|
|
255
|
+
}
|
|
256
|
+
export function saveLastSlot({ profile, cwd, slot, session, path = lastSlotPath() }) {
|
|
257
|
+
const all = readAll(path); all[keyOf(profile, cwd)] = { slot, session: session || null, at: new Date().toISOString() }; writeAll(all, path);
|
|
258
|
+
}
|
|
259
|
+
export function forgetLastSlot({ profile, cwd, path = lastSlotPath() }) {
|
|
260
|
+
const all = readAll(path); const k = keyOf(profile, cwd);
|
|
261
|
+
if (k in all) { delete all[k]; writeAll(all, path); }
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// ---- QUOXON-ROOMS: a slot as a group room ----------------------------------------------------------
|
|
265
|
+
const ROOM_RE = /^#([A-Za-z0-9][A-Za-z0-9-]{0,31}):?(?:[ \t]+(\S[\s\S]*))?$/;
|
|
266
|
+
/** `#group-chat hello` -> { room, text }; bare `#group-chat` -> { room, text: null }; `# heading` or anything else -> null. */
|
|
267
|
+
export function parseRoomSend(text) {
|
|
268
|
+
const m = ROOM_RE.exec(String(text || '').replace(/\s+$/, ''));
|
|
269
|
+
return m ? { room: m[1].toLowerCase(), text: m[2] ? m[2] : null } : null;
|
|
270
|
+
}
|
|
271
|
+
/** `#group-chat` / `group-chat` -> 'group-chat'; junk -> null. */
|
|
272
|
+
export function roomName(arg) {
|
|
273
|
+
const m = /^#?([A-Za-z0-9][A-Za-z0-9-]{0,31})$/.exec(String(arg || '').trim());
|
|
274
|
+
return m ? m[1].toLowerCase() : null;
|
|
275
|
+
}
|
|
276
|
+
/** "QUOXON (resident)" -> "QUOXON": the sender as a chat name. */
|
|
277
|
+
export function senderName(from) { return String(from || 'someone').replace(/\s*\(.*$/, '').trim() || 'someone'; }
|
|
278
|
+
const SENDER_ROLES = ['sky', 'teal', 'ok', 'warn', 'indigo', 'violet'];
|
|
279
|
+
/** A stable colour role per sender (same name, same colour, every time and every window). */
|
|
280
|
+
export function senderColor(name) {
|
|
281
|
+
let h = 0; for (const ch of String(name)) h = (h * 31 + ch.codePointAt(0)) >>> 0;
|
|
282
|
+
return SENDER_ROLES[h % SENDER_ROLES.length];
|
|
283
|
+
}
|
|
284
|
+
/** A room message as a chat line: `#group-chat QUOXON: text`. */
|
|
285
|
+
export function roomLine(room, m, { out = false } = {}) {
|
|
286
|
+
const base = slotLine(m);
|
|
287
|
+
const who = senderName(m?.from);
|
|
288
|
+
return { ...base, glyph: '#', room, who, color: senderColor(who), text: base.text, out, roomLine: true };
|
|
289
|
+
}
|
|
290
|
+
/** The text form of a room line, for /rooms backlog tests and plain output. */
|
|
291
|
+
export const roomLineText = (l) => `#${l.room} ${l.who}: ${l.text}`;
|
|
292
|
+
|
|
293
|
+
/** QUOXON-MESH: what a send to a slot tells the user about the recipient. null when the server sent no presence. */
|
|
294
|
+
export function presenceLine(slot, presence, agoText) {
|
|
295
|
+
if (!presence || !presence.state) return null;
|
|
296
|
+
if (presence.state === 'listening') return { text: `✔ delivered to ${slot} (listening now)`, tone: 'ok', listening: true };
|
|
297
|
+
if (presence.state === 'nobody_holds') return { text: `✔ delivered to ${slot} · nobody holds this slot`, tone: 'ok', listening: false };
|
|
298
|
+
const seen = presence.last_checked_in_at ? `last checked in ${agoText(presence.last_checked_in_at)}` : 'no check-in on record';
|
|
299
|
+
return { text: `✔ delivered to ${slot} · not listening right now (${seen}); it sees this when it next checks`, tone: 'ok', listening: false };
|
|
300
|
+
}
|