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,94 @@
|
|
|
1
|
+
// APPROVAL-WWWH (owner 2026-10-16 "why? what command for 17 hosts! insufficient info to authorise. who?? what? why???"):
|
|
2
|
+
// the four facts an approver needs, read from the item and nothing else: WHAT (the exact command or tool input), WHERE
|
|
3
|
+
// (host count and names), WHO (the delegation chain and the human it acts for) and WHY (reason, the user's own words,
|
|
4
|
+
// when). Pure, so the card, the /approvals row and /approvals N detail cannot drift apart.
|
|
5
|
+
|
|
6
|
+
export const CMD_CARD_MAX = 400;
|
|
7
|
+
const str = (v) => (typeof v === 'string' ? v : v == null ? '' : (() => { try { return JSON.stringify(v); } catch { return String(v); } })());
|
|
8
|
+
|
|
9
|
+
/** Text cut at `max` with a visible, honest tail. `n` is the /approvals number when known. */
|
|
10
|
+
export function clipVisible(text, max, n = null) {
|
|
11
|
+
const s = String(text ?? '');
|
|
12
|
+
if (s.length <= max) return s;
|
|
13
|
+
return `${s.slice(0, max)} (+${s.length - max} chars, /approvals ${n ?? 'N'} detail for full)`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** The exact command or tool input, verbatim: method.command, else the reach request, else args. */
|
|
17
|
+
export function commandOf(it) {
|
|
18
|
+
const p = it?.payload || {}; const c = p.approval_context || {}; const m = c.method || {};
|
|
19
|
+
if (typeof m.command === 'string' && m.command) return { text: m.command, kind: 'command' };
|
|
20
|
+
const rq = (p.reach?.requests || []).find(r => r && r.kind === 'command' && typeof r.id === 'string' && r.id);
|
|
21
|
+
if (rq) return { text: rq.id, kind: 'command' };
|
|
22
|
+
const a = m.args ?? c.args ?? p.args ?? null;
|
|
23
|
+
if (a != null && a !== '') return { text: str(a), kind: 'input' };
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Hosts (or other comma-separated targets) with the count. The full target string wins over the clipped what.target. */
|
|
28
|
+
export function hostsOf(it) {
|
|
29
|
+
const p = it?.payload || {}; const c = p.approval_context || {};
|
|
30
|
+
const raw = [c.method?.target, c.target, p.targetId, p.what?.target].find(v => typeof v === 'string' && v.trim() && !v.trim().endsWith('…')) || p.what?.target || '';
|
|
31
|
+
const list = String(raw).split(',').map(x => x.trim()).filter(Boolean);
|
|
32
|
+
return { list, count: list.length };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** "17 hosts: a, b, c (+14 more, /approvals N detail for all)". `all` lists every name. */
|
|
36
|
+
export function whereText(it, { all = false, first = 3, n = null } = {}) {
|
|
37
|
+
const { list, count } = hostsOf(it);
|
|
38
|
+
if (!count) return null;
|
|
39
|
+
if (count === 1) return list[0];
|
|
40
|
+
const tool = String(it?.payload?.toolName || it?.payload?.approval_context?.method?.tool || '');
|
|
41
|
+
const w = /ssh|exec|host|shell/i.test(tool) ? 'hosts' : 'targets';
|
|
42
|
+
if (all) return `${count} ${w}: ${list.join(', ')}`;
|
|
43
|
+
const shown = list.slice(0, first).join(', ');
|
|
44
|
+
return count > first ? `${count} ${w}: ${shown} (+${count - first} more, /approvals ${n ?? 'N'} detail for all)` : `${count} ${w}: ${shown}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Read-only or write, only as the server classified it. Null when it did not say. */
|
|
48
|
+
export function accessOf(it) {
|
|
49
|
+
const c = it?.payload?.approval_context || {};
|
|
50
|
+
const k = c.method?.access || c.access;
|
|
51
|
+
if (k === 'read_only' || k === 'read') return 'read-only';
|
|
52
|
+
if (k === 'write') return 'write';
|
|
53
|
+
const lab = String(c.why_approval?.label || '');
|
|
54
|
+
if (/not on the read-only allowlist/i.test(lab)) return 'NOT read-only: not on the read-only allowlist, so it may change things';
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** WHO: "QUOX → SENTINEL (session claude in app) for Adam DEV SERVER". */
|
|
59
|
+
export function whoText(it) {
|
|
60
|
+
const c = it?.payload?.approval_context || {}; const rb = c.requested_by || {}; const ob = c.on_behalf_of || {};
|
|
61
|
+
const chain = Array.isArray(rb.delegation_chain) && rb.delegation_chain.length ? rb.delegation_chain.join(' → ') : (rb.agent_label || c.source_label || null);
|
|
62
|
+
const caller = rb.caller?.name || rb.session_label || null;
|
|
63
|
+
const via = [caller && `via ${rb.caller?.kind === 'session' || rb.session_label ? 'session ' : ''}${caller}`, rb.surface_label && `${rb.surface_label}`, rb.profile && `profile ${rb.profile}`].filter(Boolean).join(', ');
|
|
64
|
+
const forWhom = ob.user_name || ob.user_id || null;
|
|
65
|
+
const head = [chain, via && `(${via})`].filter(Boolean).join(' ');
|
|
66
|
+
if (!head && !forWhom) return null;
|
|
67
|
+
return `${head || 'unknown requester'}${forWhom ? ` acting for ${forWhom}` : ''}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** WHY: the reason text (marked inferred when the caller did not state it). */
|
|
71
|
+
export function whyText(it) {
|
|
72
|
+
const c = it?.payload?.approval_context || {}; const r = c.reason;
|
|
73
|
+
if (r && typeof r === 'object') return r.text ? `${r.text}${r.source === 'inferred' ? ' (inferred, not stated by the caller)' : ''}` : null;
|
|
74
|
+
return r || it?.payload?.reason || null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** The user's own words that started this, when the server kept them. */
|
|
78
|
+
export const excerptOf = (it) => it?.payload?.approval_context?.on_behalf_of?.request_excerpt || null;
|
|
79
|
+
|
|
80
|
+
/** One bag of the facts, for the card. */
|
|
81
|
+
export function approvalFacts(it, { n = null } = {}) {
|
|
82
|
+
const cmd = commandOf(it);
|
|
83
|
+
const when = it?.payload?.approval_context?.created_at || it?.created_at || it?.createdAt || null;
|
|
84
|
+
return {
|
|
85
|
+
command: cmd ? { ...cmd, shown: clipVisible(cmd.text, CMD_CARD_MAX, n) } : null,
|
|
86
|
+
where: whereText(it, { n }),
|
|
87
|
+
hostList: hostsOf(it).list,
|
|
88
|
+
who: whoText(it),
|
|
89
|
+
why: whyText(it),
|
|
90
|
+
asked: excerptOf(it),
|
|
91
|
+
when: when ? String(when).replace('T', ' ').slice(0, 16) + ' UTC' : null,
|
|
92
|
+
access: accessOf(it)
|
|
93
|
+
};
|
|
94
|
+
}
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
// QUOXON-APPROVALS: pure helpers for /approvals rows, filters, detail and /approve|/deny argument parsing.
|
|
2
|
+
import { describeInboxItem, approvalSourceLabel, isPrivileged } from './events.js';
|
|
3
|
+
import { commandOf, hostsOf, whereText, whoText, whyText, excerptOf, accessOf, clipVisible } from './approvalFacts.js';
|
|
4
|
+
|
|
5
|
+
// quox ask: a decision request is an inbox item of kind question with payload.type 'decision'.
|
|
6
|
+
export const isDecision = (it) => it?.payload?.type === 'decision' && Array.isArray(it.payload.options);
|
|
7
|
+
/** Who stands behind a recommendation: an explicit recommended_by, else the asking session or agent, else Quox. Never invented when there is no recommendation. */
|
|
8
|
+
export const recommenderOf = (p) => String(p.recommended_by || p.session || p.agent || 'Quox');
|
|
9
|
+
/** Display order only: the recommended option first, the rest in their original order. Keys never move with it. No recommendation = original order. */
|
|
10
|
+
export const displayOptions = (p) => { const rec = p.options.find(o => o.key === p.recommend); return rec ? [rec, ...p.options.filter(o => o !== rec)] : p.options.slice(); };
|
|
11
|
+
const left = (it, now) => { const t = Date.parse(it?.due_at ?? it?.payload?.expires_at); if (!Number.isFinite(t)) return null; const ms = t - now; if (ms <= 0) return { text: 'expired', soon: true }; const m = Math.round(ms / 60000); return { text: m >= 120 ? `${Math.round(m / 60)}h left` : m >= 1 ? `${m}m left` : '<1m left', soon: ms <= 15 * 60000 }; };
|
|
12
|
+
|
|
13
|
+
/** One line for a decision: [3] SEER asks: Publish the guide now? (a) Ship now (b) Wait (c) Drop · recommends a · 2h left. The tail never gets clipped away; the middle gives way. */
|
|
14
|
+
export function decisionRow(it, n, { cols = 100, now = Date.now() } = {}) {
|
|
15
|
+
const p = it.payload;
|
|
16
|
+
const head = ` [${n}] ${p.session || approvalSourceLabel(it)} asks: ${cleanTitle(it)}`;
|
|
17
|
+
const opts = p.options.map((o) => `(${o.key}) ${o.label}`).join(' ');
|
|
18
|
+
const l = left(it, now);
|
|
19
|
+
const tail = [p.recommend ? `recommends ${p.recommend}` : null, l ? `${l.soon ? '⚠ ' : ''}${l.text}` : null, p.high_risk ? 'high risk' : null].filter(Boolean).join(' · ');
|
|
20
|
+
const body = `${head} ${opts}`;
|
|
21
|
+
const room = cols - (tail ? tail.length + 3 : 0);
|
|
22
|
+
const mid = body.length > room ? body.slice(0, Math.max(10, room - 1)) + '…' : body;
|
|
23
|
+
return tail ? `${mid} · ${tail}` : mid;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Full detail for a decision: who, what, why, every option. */
|
|
27
|
+
export function decisionDetail(it, n, { now = Date.now() } = {}) {
|
|
28
|
+
const p = it.payload; const l = left(it, now);
|
|
29
|
+
const rows = [
|
|
30
|
+
['item', `[${n}] ${it.title || it.id}`],
|
|
31
|
+
['id', it.id],
|
|
32
|
+
['from', [p.session, p.profile && `profile ${p.profile}`, p.agent && `agent ${p.agent}`].filter(Boolean).join(', ')],
|
|
33
|
+
['why', p.why],
|
|
34
|
+
...displayOptions(p).map((o, i) => [i === 0 ? 'options' : '', `${o.key === p.recommend ? '★ ' : ''}(${o.key}) ${o.label}${o.key === p.recommend ? ` ★ recommended by ${recommenderOf(p)}` : ''}${o.key === p.default ? ' default' : ''}`]),
|
|
35
|
+
['expires', l ? `${l.soon ? '⚠ ' : ''}${l.text} (${it.due_at || p.expires_at})` : null],
|
|
36
|
+
['risk', p.high_risk ? 'high risk: answering needs your step-up PIN' : null],
|
|
37
|
+
].filter((r) => r[1]);
|
|
38
|
+
return rows.map(([k, v]) => ` ${String(k).padEnd(8)}${v}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* /answer arguments after the command word: "3 b after Friday", or "b after Friday" when an item is focused.
|
|
43
|
+
* Returns { n (null = the only open decision), key, note, bad }. A quoted note loses its quotes.
|
|
44
|
+
*/
|
|
45
|
+
/**
|
|
46
|
+
* QUOXON-1191b: pair syntax "1 y 2 n", "1 y, 2 n", "1y 2n". Returns [{ k, key }] (key lowercase y|n), null when the
|
|
47
|
+
* words are not entirely pairs, or { bad: true } for a 0 or a repeated number.
|
|
48
|
+
*/
|
|
49
|
+
export function parsePairs(words) {
|
|
50
|
+
const text = words.join(' ').trim();
|
|
51
|
+
if (!/^(\d+\s*[yn](\s*,\s*|\s+|$))+$/i.test(text)) return null;
|
|
52
|
+
const pairs = [...text.matchAll(/(\d+)\s*([yn])/gi)].map(m => ({ k: Number(m[1]), key: m[2].toLowerCase() }));
|
|
53
|
+
if (!pairs.length) return null;
|
|
54
|
+
if (pairs.some(x => x.k < 1) || new Set(pairs.map(x => x.k)).size !== pairs.length) return { bad: true };
|
|
55
|
+
return pairs;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function parseAnswer(words, { focusN = null, raw = null } = {}) {
|
|
59
|
+
const out = { n: null, key: null, note: '', bad: false };
|
|
60
|
+
let w = words.slice();
|
|
61
|
+
const pr = parsePairs(w);
|
|
62
|
+
if (pr && (pr.bad || pr.length >= 2 || (w.length === 1 && /^\d+[yn]$/i.test(w[0])))) {
|
|
63
|
+
if (pr.bad) { out.bad = true; return out; }
|
|
64
|
+
out.ns = pr.map(x => x.k); out.n = out.ns[0]; out.keys = Object.fromEntries(pr.map(x => [x.k, x.key])); out.key = pr[0].key;
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
if (w[0] && /^\d+$/.test(w[0])) { out.n = Number(w[0]); w = w.slice(1); }
|
|
68
|
+
else if (w[0] && /^\d+(-\d+)?(,\d+(-\d+)?)+$|^\d+-\d+$/.test(w[0])) { // QUOXON-1191: /answer 1-5 y, /answer 1,3,4 y
|
|
69
|
+
const ns = [];
|
|
70
|
+
for (const part of w[0].split(',')) { const m = /^(\d+)(?:-(\d+))?$/.exec(part); const a = Number(m[1]); const b = Number(m[2] ?? m[1]); if (a < 1 || b < a || b - a > 199) { out.bad = true; return out; } for (let k = a; k <= b; k++) if (!ns.includes(k)) ns.push(k); }
|
|
71
|
+
out.ns = ns; out.n = ns[0]; w = w.slice(1);
|
|
72
|
+
}
|
|
73
|
+
else if (w.length >= 2 && /^n$/i.test(w[0])) w = w.slice(1); // the hint's literal "N" is the same as no number
|
|
74
|
+
else out.n = focusN;
|
|
75
|
+
if (!w[0]) { out.bad = true; return out; }
|
|
76
|
+
out.key = w[0].toLowerCase();
|
|
77
|
+
const noteWords = w.slice(1);
|
|
78
|
+
out.note = raw != null && noteWords.length
|
|
79
|
+
? cleanNote(rawTail(raw, words.length - noteWords.length))
|
|
80
|
+
: cleanNote(noteWords.join(' '));
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const ctxOf = (it) => it?.payload?.approval_context || {};
|
|
85
|
+
const obOf = (it) => ctxOf(it).on_behalf_of || {};
|
|
86
|
+
|
|
87
|
+
/** Every name an item answers to as its requester (agent, session, source label). */
|
|
88
|
+
export function requesterNames(it) {
|
|
89
|
+
const ob = obOf(it); const c = ctxOf(it);
|
|
90
|
+
return [ob.agent, ob.agent_id, ob.session_name, ob.session, c.requested_by?.agent_label, c.source_label, it?.payload?.source, it?.source]
|
|
91
|
+
.filter(Boolean).map(String);
|
|
92
|
+
}
|
|
93
|
+
export const isAgentItem = (it) => { const ob = obOf(it); return Boolean(ob.agent || ob.agent_id || ob.session_name || ob.session || ctxOf(it).requested_by?.agent_label); };
|
|
94
|
+
export const matchesAgent = (it, name) => { const n = String(name || '').toLowerCase(); return Boolean(n) && requesterNames(it).some(x => x.toLowerCase() === n || x.toLowerCase().includes(n)); };
|
|
95
|
+
|
|
96
|
+
export function ageText(iso, now = Date.now()) {
|
|
97
|
+
const t = typeof iso === 'number' ? iso : Date.parse(iso);
|
|
98
|
+
if (!Number.isFinite(t)) return '?';
|
|
99
|
+
const m = Math.max(0, Math.round((now - t) / 60000));
|
|
100
|
+
return m < 1 ? 'now' : m < 60 ? `${m}m` : m < 2880 ? `${Math.round(m / 60)}h` : `${Math.round(m / 1440)}d`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** 'mine' | 'agents' | 'old' | <agent name> | null -> predicate. */
|
|
104
|
+
export function filterPredicate(f, { userId = null, conversationId = null, now = Date.now(), oldMs = 24 * 3600 * 1000 } = {}) {
|
|
105
|
+
if (!f) return () => true;
|
|
106
|
+
const k = String(f).toLowerCase();
|
|
107
|
+
if (k === 'mine') return (it) => { const ob = obOf(it); return Boolean((userId && ob.user_id && String(ob.user_id) === String(userId)) || (conversationId && ob.conversation_id === conversationId)); };
|
|
108
|
+
if (k === 'agents') return isAgentItem;
|
|
109
|
+
if (k === 'old') return (it) => { const t = Date.parse(it?.created_at ?? it?.createdAt); return Number.isFinite(t) && now - t > oldMs; };
|
|
110
|
+
return (it) => matchesAgent(it, f);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** One line: [N] age who what (risk). Clipped to `cols`. */
|
|
114
|
+
export function approvalRow(it, n, { cols = 100, now = Date.now() } = {}) {
|
|
115
|
+
if (isDecision(it)) return decisionRow(it, n, { cols, now });
|
|
116
|
+
const d = describeInboxItem(it);
|
|
117
|
+
const who = requesterNames(it)[0] || approvalSourceLabel(it);
|
|
118
|
+
let what = [d.tool, d.target && d.target !== d.tool ? d.target : null].filter(Boolean).join(' ') || d.title || it.id;
|
|
119
|
+
const risk = ctxOf(it).risk ? ` (${ctxOf(it).risk})` : '';
|
|
120
|
+
const priv = isPrivileged({ tool: d.tool, risk: ctxOf(it).risk, flag: it?.payload?.privileged === true || ctxOf(it).privileged === true, title: it.title }) ? ' [privileged]' : '';
|
|
121
|
+
const cmd = commandOf(it);
|
|
122
|
+
if (cmd && cmd.kind === 'command') { // APPROVAL-WWWH: the exact command and the host count, never just the tool name; the command gives way last, with a visible tail
|
|
123
|
+
const { count, list } = hostsOf(it);
|
|
124
|
+
const on = count > 1 ? ` on ${count} ${/ssh|exec|host|shell/i.test(String(d.tool || it?.payload?.toolName || '')) ? 'hosts' : 'targets'} (${list.slice(0, 2).join(', ')}, +${count - 2} more)` : (count === 1 ? ` on ${list[0]}` : '');
|
|
125
|
+
const one = cmd.text.replace(/\s*\n\s*/g, ' ; ');
|
|
126
|
+
const head = ` [${n}] ${ageText(d.at, now).padStart(3)} ${String(who).slice(0, 18).padEnd(18)} ${d.tool || 'run'} `;
|
|
127
|
+
const room = Math.max(24, cols - head.length - on.length - risk.length - priv.length - 2);
|
|
128
|
+
return `${head}\`${clipVisible(one, room, n)}\`${on}${risk}${priv}`;
|
|
129
|
+
}
|
|
130
|
+
const line = ` [${n}] ${ageText(d.at, now).padStart(3)} ${String(who).slice(0, 18).padEnd(18)} ${what}${risk}${priv}`;
|
|
131
|
+
return line.length > cols ? line.slice(0, Math.max(10, cols - 1)) + '…' : line;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Full detail for /approvals N: one line per fact. */
|
|
135
|
+
export function approvalDetail(it, n, { now = Date.now() } = {}) {
|
|
136
|
+
if (isDecision(it)) return decisionDetail(it, n, { now });
|
|
137
|
+
const d = describeInboxItem(it); const c = ctxOf(it); const m = c.method || {}; const p = it?.payload || {};
|
|
138
|
+
const args = m.argv ?? m.args ?? c.argv ?? c.args ?? p.args ?? null;
|
|
139
|
+
const cmdFull = commandOf(it);
|
|
140
|
+
const why = whyText(it) || c.why || p.reason || it?.summary || it?.description || null;
|
|
141
|
+
const rc = it?.receipt || p.receipt || c.receipt || null;
|
|
142
|
+
const receipt = rc ? (typeof rc === 'string' ? rc : (rc.ward_seq != null ? `ward seq ${rc.ward_seq}${rc.verified ? ' verified' : ''}` : JSON.stringify(rc).slice(0, 100))) : (it?.aee_correlation_id || p.approval_id || null);
|
|
143
|
+
const rows = [
|
|
144
|
+
['item', `[${n}] ${it.title || it.id}`],
|
|
145
|
+
['id', it.id],
|
|
146
|
+
['tool', d.tool], ['target', hostsOf(it).count > 1 ? null : d.target],
|
|
147
|
+
['command', cmdFull && cmdFull.kind === 'command' ? cmdFull.text : null],
|
|
148
|
+
['args', cmdFull && cmdFull.kind === 'command' ? null : (args == null ? null : (typeof args === 'string' ? args : JSON.stringify(args)).slice(0, 300))],
|
|
149
|
+
['where', hostsOf(it).count ? whereText(it, { all: true }) : null],
|
|
150
|
+
['who', whoText(it)],
|
|
151
|
+
['why', why ? String(why) : null],
|
|
152
|
+
['request', excerptOf(it) ? `"${excerptOf(it)}"` : null],
|
|
153
|
+
['class', accessOf(it)],
|
|
154
|
+
['from', requesterNames(it).join(', ') || approvalSourceLabel(it)],
|
|
155
|
+
['asked', d.at ? `${ageText(d.at, now)} ago (${String(d.at)})` : null],
|
|
156
|
+
['risk', c.risk || null],
|
|
157
|
+
['receipt', receipt],
|
|
158
|
+
['url', it.dashboard_url || null]
|
|
159
|
+
].filter(r => r[1]);
|
|
160
|
+
return rows.flatMap(([k, v]) => String(v).split('\n').map((line, i) => ` ${(i ? '' : k).padEnd(8)}${line}`));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* /approve|/deny arguments after the command word. Forms: N, 1,2, 1-3, all, agent <name>; deny may end with a free-text
|
|
165
|
+
* reason. 'confirm' (approve only) is stripped. Returns { nums, all, agent, reason, confirm, bad }.
|
|
166
|
+
*/
|
|
167
|
+
export const NOTE_MAX = 4000;
|
|
168
|
+
/** The raw text after the first `skip` whitespace-separated tokens, whitespace and line breaks kept. */
|
|
169
|
+
export function rawTail(raw, skip) {
|
|
170
|
+
const m = new RegExp(`^\\s*(?:\\S+\\s+){${Math.max(0, skip - 1)}}\\S+\\s*`).exec(String(raw ?? ''));
|
|
171
|
+
return skip > 0 ? (m ? String(raw).slice(m[0].length) : '') : String(raw ?? '');
|
|
172
|
+
}
|
|
173
|
+
/** A typed note: a balanced pair of quotes is removed; an opening quote with no close just goes, and the rest stays. Nothing else is dropped. */
|
|
174
|
+
export function cleanNote(t) {
|
|
175
|
+
let s = String(t ?? '').trim();
|
|
176
|
+
const q = s[0];
|
|
177
|
+
if (q === '"' || q === "'" || q === '\u201c') {
|
|
178
|
+
const close = q === '\u201c' ? '\u201d' : q;
|
|
179
|
+
s = s.slice(1);
|
|
180
|
+
if (s.endsWith(close)) s = s.slice(0, -1);
|
|
181
|
+
}
|
|
182
|
+
return s.trim().slice(0, NOTE_MAX);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function parseDecision(words, verb, { raw = null } = {}) {
|
|
186
|
+
const out = { nums: [], all: false, agent: null, reason: '', confirm: false, bad: false };
|
|
187
|
+
let w = words.slice();
|
|
188
|
+
if (verb === 'approve') { out.confirm = w.includes('confirm'); w = w.filter(x => x !== 'confirm'); }
|
|
189
|
+
let i = 0;
|
|
190
|
+
if (w[0] && w[0].toLowerCase() === 'agent') {
|
|
191
|
+
if (!w[1]) { out.bad = true; return out; }
|
|
192
|
+
out.agent = w[1]; i = 2;
|
|
193
|
+
} else {
|
|
194
|
+
for (; i < w.length; i++) {
|
|
195
|
+
const tok = w[i];
|
|
196
|
+
if (tok.toLowerCase() === 'all') { out.all = true; continue; }
|
|
197
|
+
if (!/^[\d,\-]+$/.test(tok)) break;
|
|
198
|
+
let ok = false;
|
|
199
|
+
for (const part of tok.split(',').filter(Boolean)) {
|
|
200
|
+
const m = /^(\d+)(?:-(\d+))?$/.exec(part);
|
|
201
|
+
if (m && Number(m[1]) >= 1 && Number(m[2] ?? m[1]) >= Number(m[1]) && Number(m[2] ?? m[1]) - Number(m[1]) < 200) { ok = true; for (let k = Number(m[1]); k <= Number(m[2] ?? m[1]); k++) out.nums.push(k); }
|
|
202
|
+
else { out.bad = true; return out; }
|
|
203
|
+
}
|
|
204
|
+
if (!ok) { out.bad = true; return out; }
|
|
205
|
+
if (out.all) break;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
const restWords = w.slice(i);
|
|
209
|
+
const rest = restWords.join(' ').trim();
|
|
210
|
+
if (rest) {
|
|
211
|
+
if (verb === 'reject') out.reason = raw != null ? cleanNote(rawTail(raw, words.length - restWords.length)) : rest.slice(0, NOTE_MAX);
|
|
212
|
+
else out.bad = true;
|
|
213
|
+
}
|
|
214
|
+
if (!out.all && !out.agent && !out.nums.length) out.bad = true;
|
|
215
|
+
return out;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* WALKTHROUGH (owner 2026-10-17: "guide me simply one at a time, explain, advise"). One decision as one plain card.
|
|
220
|
+
* Identity is the decision id, never a position. -> { lines, choices:[{key,label,word}], recommendKey, source }.
|
|
221
|
+
*/
|
|
222
|
+
const isLabelled = (o) => o.label && String(o.label).toLowerCase() !== String(o.key).toLowerCase();
|
|
223
|
+
const optWord = (o) => (isLabelled(o) ? o.label : `option ${o.key} (no label given)`);
|
|
224
|
+
/** "Type 1: <label> · Type 2: <label>" plus s to skip, for a non-yes/no decision. */
|
|
225
|
+
export function walkChoicesLine(it) {
|
|
226
|
+
return it.payload.options.map(o => `Type ${o.key}: ${optWord(o)}`).join(' · ') + ' · Type s to skip';
|
|
227
|
+
}
|
|
228
|
+
export function walkNotYesNo(it) {
|
|
229
|
+
return `This one isn't yes/no. ${walkChoicesLine(it).replace(/ · Type s to skip$/, '')}, or s to skip.`;
|
|
230
|
+
}
|
|
231
|
+
export function walkNoCatch(it) {
|
|
232
|
+
const ks = it.payload.options.map(o => o.key);
|
|
233
|
+
const keys = ks.length > 1 ? `${ks.slice(0, -1).join(', ')} or ${ks[ks.length - 1]}` : ks.join('');
|
|
234
|
+
return `I did not catch a choice. Type ${keys}. ${walkChoicesLine(it)} (? asks about it, Esc stops).`;
|
|
235
|
+
}
|
|
236
|
+
/** A title without the asker's own name in front of it ("SEER: Publish?" asked by SEER -> "Publish?"), whitespace and line breaks folded. */
|
|
237
|
+
export function cleanTitle(it) {
|
|
238
|
+
const p = it.payload || {}; const t = String(it.title || it.id || '').replace(/\s+/g, ' ').trim();
|
|
239
|
+
for (const name of [p.session, p.agent].filter(Boolean).map(String)) {
|
|
240
|
+
if (t.toLowerCase().startsWith(name.toLowerCase() + ':')) { const rest = t.slice(name.length + 1).trim(); if (rest) return rest; }
|
|
241
|
+
}
|
|
242
|
+
return t;
|
|
243
|
+
}
|
|
244
|
+
/** Word-wrap `text` into lines no wider than `cols`, the label on the first line and a hanging indent of the same width under it. */
|
|
245
|
+
export function hang(label, text, cols) {
|
|
246
|
+
const lead = ' ' + label.padEnd(8); const pad = ' '.repeat(lead.length);
|
|
247
|
+
if (!cols) return [lead + text];
|
|
248
|
+
const room = Math.max(20, cols - lead.length - 1); const out = []; let cur = '';
|
|
249
|
+
for (const w of String(text).split(' ')) {
|
|
250
|
+
if (cur && cur.length + 1 + w.length > room) { out.push(cur); cur = w; } else cur = cur ? cur + ' ' + w : w;
|
|
251
|
+
}
|
|
252
|
+
if (cur || !out.length) out.push(cur);
|
|
253
|
+
return out.map((l, k) => (k ? pad : lead) + l);
|
|
254
|
+
}
|
|
255
|
+
/** Owner 2026-10-17: a test or guard loosening needs a plain justification. True when the title, kind or tags say the ask would make a check easier to pass. */
|
|
256
|
+
export const LOOSEN_RE = /loosen|weaken|relax|widen|(?:delete|remove|skip|disable|replace|rewrite|change|edit|update)\w*\s+(?:\w+\s+){0,3}?(?:tests?|guards?|checks?|assertions?|proof cases?)\b|test.?pin|goal ?post/i;
|
|
257
|
+
export const isLoosening = (it) => { const p = it?.payload || {}; return p.loosening === true || LOOSEN_RE.test([it?.title, p.kind, p.category, ...(Array.isArray(p.tags) ? p.tags : [])].filter(Boolean).join(' ')); };
|
|
258
|
+
export const NO_REASON = 'No reason given: ask the session, or say no.';
|
|
259
|
+
export const NO_JUSTIFICATION = 'No plain justification given: ask the session, or say no.';
|
|
260
|
+
const justificationOf = (p) => String(p.justification || p.plain_justification || '').replace(/\s+/g, ' ').trim();
|
|
261
|
+
export function walkCard(it, i, total, { cols = 0 } = {}) {
|
|
262
|
+
const p = it.payload; const who = p.session || p.agent || 'A session';
|
|
263
|
+
const loose = isLoosening(it); const just = justificationOf(p); // JUSTIFY shows whenever the asker gave one, loosening title or not
|
|
264
|
+
const bareLoose = loose && !just; // no clear justification: "no" is the safe default and nothing is starred
|
|
265
|
+
const yn = p.options.some(o => o.key === 'y') && p.options.some(o => o.key === 'n');
|
|
266
|
+
const rec = bareLoose ? null : (p.options.find(o => o.key === p.recommend) || null);
|
|
267
|
+
const dflt = bareLoose ? (p.options.find(o => o.key === 'n') || null) : (p.options.find(o => o.key === p.default) || null);
|
|
268
|
+
const why = String(p.why || '').replace(/\s+/g, ' ').trim();
|
|
269
|
+
const what = yn
|
|
270
|
+
? `Yes lets ${who} go ahead with this; no stops it.${p.high_risk ? ' High risk: a yes needs your PIN.' : ''}`
|
|
271
|
+
: `${who} needs you to choose how to settle this.${p.high_risk ? ' High risk: a yes needs your PIN.' : ''}`;
|
|
272
|
+
const advice = bareLoose
|
|
273
|
+
? `${yn ? 'I would say no' : 'Do not approve this yet'}: this makes a check easier to pass and nothing explains why that is safe.${dflt ? ` Safe default: ${optWord(dflt)} (${dflt.key}).` : ''}`
|
|
274
|
+
: rec
|
|
275
|
+
? `I would ${yn ? (rec.key === 'y' ? 'say yes' : 'say no') : `go with ${optWord(rec)} (${rec.key})`}. The asker recommended it.`
|
|
276
|
+
: `${who} gave no recommendation, and I will not guess one.${dflt ? ` If nobody answers it would take ${optWord(dflt)} (${dflt.key}).` : ''}`;
|
|
277
|
+
const keyClash = (k) => p.options.some(o => o.key === k);
|
|
278
|
+
const choices = [
|
|
279
|
+
...p.options.map(o => ({ key: o.key, label: optWord(o), word: yn ? (o.key === 'y' ? 'Yes' : o.key === 'n' ? 'No' : optWord(o)) : optWord(o) })),
|
|
280
|
+
{ key: keyClash('s') ? 'skip' : 's', label: 'Skip for now', defer: true },
|
|
281
|
+
{ key: keyClash('?') ? 'ask' : '?', label: 'Ask about it', ask: true },
|
|
282
|
+
];
|
|
283
|
+
const lines = [
|
|
284
|
+
`Decision ${i} of ${total} · ${who} asks`,
|
|
285
|
+
...hang('', cleanTitle(it), cols).map(l => l.slice(8)),
|
|
286
|
+
...hang('WHAT', what, cols),
|
|
287
|
+
...(why ? hang('WHY', why, cols) : []),
|
|
288
|
+
...(loose || just ? hang('JUSTIFY', just || NO_JUSTIFICATION, cols) : []),
|
|
289
|
+
...(!why && !just && !loose ? hang('WHY', NO_REASON, cols) : []),
|
|
290
|
+
...hang('ADVICE', advice, cols),
|
|
291
|
+
];
|
|
292
|
+
// RECO-HIGHLIGHT: a real recommendation is its own first line (the TUI paints it bold in the accent colour), marked in text so a no-colour terminal still sees it.
|
|
293
|
+
let recoLine = null;
|
|
294
|
+
if (rec) { recoLine = lines.length; lines.push(` ★ [${rec.key}] ${optWord(rec)} ★ recommended by ${recommenderOf(p)}`); }
|
|
295
|
+
lines.push(' ' + choices.filter(c => !(rec && c.key === rec.key)).map(c => `[${c.key}] ${c.label}`).join(' ')); // the recommended key is on the star line above: each key is offered once
|
|
296
|
+
return { lines, choices, recommendKey: rec ? rec.key : null, recoLine, displayKeys: displayOptions(p).map(o => o.key), source: rec ? 'asker' : null };
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** The reply to a walkthrough card -> { key, note } | { defer:true } | { ask:true } | null. "y because X": text after the choice is the note, kept in full. */
|
|
300
|
+
export function walkReply(it, text) {
|
|
301
|
+
const p = it.payload; const raw = String(text ?? '').trim();
|
|
302
|
+
if (!raw) return null;
|
|
303
|
+
const yn = p.options.some(o => o.key === 'y') && p.options.some(o => o.key === 'n');
|
|
304
|
+
const notYesNo = !yn;
|
|
305
|
+
const first = raw.split(/\s+/)[0]; const f = first.toLowerCase().replace(/[.!,]+$/, '');
|
|
306
|
+
const note = () => cleanNote(rawTail(raw, 1));
|
|
307
|
+
const hasKey = (k) => p.options.some(o => o.key.toLowerCase() === k);
|
|
308
|
+
if (hasKey(f)) return { key: p.options.find(o => o.key.toLowerCase() === f).key, note: note() };
|
|
309
|
+
if (/^(skip|s|later|next)$/.test(f)) return { defer: true };
|
|
310
|
+
if (f === '?' || /^(ask|why|explain|help)$/.test(f)) return { ask: true };
|
|
311
|
+
if (/^(yes|yep|yeah|ok|okay)$/.test(f)) return yn ? { key: 'y', note: note() } : (notYesNo ? { notYesNo: true } : null);
|
|
312
|
+
if (/^(no|nope)$/.test(f)) return yn ? { key: 'n', note: note() } : { notYesNo: true };
|
|
313
|
+
const t = raw.toLowerCase().replace(/[.!]+$/, '');
|
|
314
|
+
const hit = p.options.find(o => isLabelled(o) && t.startsWith(String(o.label).toLowerCase()));
|
|
315
|
+
return hit ? { key: hit.key, note: cleanNote(raw.slice(String(hit.label).length)) } : null;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/** The end summary of a walkthrough. state = { answered:[{title,word}], skipped:[title], left:[title] } */
|
|
319
|
+
export function walkSummary(st) {
|
|
320
|
+
const out = [`Walkthrough done: ${st.answered.length} answered, ${st.skipped.length} skipped, ${st.left.length} left.`];
|
|
321
|
+
for (const a of st.answered) out.push(` ✔ ${a.title}: ${a.word}`);
|
|
322
|
+
for (const t of st.skipped) out.push(` ↷ skipped: ${t}`);
|
|
323
|
+
for (const t of st.left) out.push(` · still waiting: ${t}`);
|
|
324
|
+
return out;
|
|
325
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* /aside in the TUI (pure parts). The answer is a history note with tone 'aside'; its text is
|
|
3
|
+
* "aside: <question>\n<answer...>\n· <footer>"
|
|
4
|
+
* so the transcript and /share read naturally, and NoteView draws it as a separate block: a violet bar down
|
|
5
|
+
* the left, the question as the header, a read-only footer. Nothing here touches the conversation.
|
|
6
|
+
*/
|
|
7
|
+
import { asideExcerpt } from '../core/aside.js';
|
|
8
|
+
|
|
9
|
+
export const ASIDE_FOOTER = 'read only: no tools, not added to this conversation';
|
|
10
|
+
|
|
11
|
+
/** History + live items -> the last `max` user/assistant exchanges, assistant chunks merged; /commands and !lines skipped. */
|
|
12
|
+
export function turnsFromItems(items, max = 10) {
|
|
13
|
+
const out = [];
|
|
14
|
+
for (const it of Array.isArray(items) ? items : []) {
|
|
15
|
+
if (!it || typeof it.text !== 'string') continue;
|
|
16
|
+
if (it.kind === 'user' && !it.queued) { if (!/^[/!]/.test(it.text.trim())) out.push({ role: 'user', text: it.text }); } // a /command or !shell line is not conversation
|
|
17
|
+
else if (it.kind === 'text') {
|
|
18
|
+
const last = out[out.length - 1];
|
|
19
|
+
if (last?.role === 'assistant') last.text += it.text; else out.push({ role: 'assistant', text: it.text });
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return out.slice(-max);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** The note text for one aside answer. */
|
|
26
|
+
export function asideNoteText(question, answer, meta = {}) {
|
|
27
|
+
const who = meta.model ? ` · ${meta.provider ? `${meta.provider}/` : ''}${meta.model}` : '';
|
|
28
|
+
return `aside: ${String(question).trim()}\n${String(answer || '(no answer)').trim()}\n· ${ASIDE_FOOTER}${who}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** -> { head, body: [lines], foot } from a note text (tolerant of a hand-made one). */
|
|
32
|
+
export function parseAsideNote(text) {
|
|
33
|
+
const lines = String(text ?? '').split('\n');
|
|
34
|
+
const head = lines[0] || 'aside';
|
|
35
|
+
const footAt = lines.length > 1 && lines[lines.length - 1].startsWith('· ') ? lines.length - 1 : -1;
|
|
36
|
+
const body = lines.slice(1, footAt === -1 ? undefined : footAt);
|
|
37
|
+
return { head, body, foot: footAt === -1 ? null : lines[footAt] };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export { asideExcerpt };
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QUOXON-ASK (owner 2026-10-15): one shape for everything that needs the human's answer, so it is learnt once.
|
|
3
|
+
*
|
|
4
|
+
* ▶ <reason> <question> <keys>
|
|
5
|
+
*
|
|
6
|
+
* A fixed glyph first, the reason (why it is asking) next, the question, and the answer keys LAST. Rendered in the
|
|
7
|
+
* 'ask' tone (bold, the ask colour on the glyph and keys; never dim). Approval cards end in the same line.
|
|
8
|
+
* Every y/N the TUI says goes through askText; a test fails on a raw "y/N" anywhere else.
|
|
9
|
+
*/
|
|
10
|
+
export const ASK_GLYPH = '▶';
|
|
11
|
+
export const ASK_KEYS = 'y/N/c'; // y: do it · n or Enter: don't · c or any other text: chat about it first (owner 2026-10-15)
|
|
12
|
+
|
|
13
|
+
// The structured ask behind each line askText made, so the renderer can draw the card (who, action, why, keys)
|
|
14
|
+
// from the text a module said. Bounded; an unknown line is parsed back instead.
|
|
15
|
+
const MADE = new Map();
|
|
16
|
+
const MADE_MAX = 200;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The ask line's text (the transcript form; the TUI draws it as a card). reason is said first (a trailing "." is
|
|
20
|
+
* added when it has no end stop). who: who is asking (QUOX, an agent, a slot sender; default this terminal).
|
|
21
|
+
* action: exactly what a yes does, shown in full on the card.
|
|
22
|
+
*/
|
|
23
|
+
export function askText(question, { keys = ASK_KEYS, reason = '', who = '', action = '', title = '', plain = false } = {}) {
|
|
24
|
+
const r = String(reason || '').trim();
|
|
25
|
+
const q = String(question || '').trim();
|
|
26
|
+
const lead = r ? `${/[.!?:)]$/.test(r) ? r : `${r}.`} ` : '';
|
|
27
|
+
const text = `${ASK_GLYPH} ${lead}${q}${keys ? ` ${keys}` : ''}`;
|
|
28
|
+
MADE.set(text, { question: q, reason: r, keys: keys || '', who: String(who || '').trim(), action: String(action || '').trim(), title: String(title || '').trim(), plain: Boolean(plain) });
|
|
29
|
+
if (MADE.size > MADE_MAX) MADE.delete(MADE.keys().next().value);
|
|
30
|
+
return text;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** An ask line -> { question, reason, keys, who, action } (what askText was given; parsed back when unknown). */
|
|
34
|
+
export function parseAsk(text) {
|
|
35
|
+
const t = String(text ?? '');
|
|
36
|
+
if (MADE.has(t)) return { ...MADE.get(t) };
|
|
37
|
+
const question = askQuestion(t);
|
|
38
|
+
const body = t.replace(new RegExp(`^\\s*${ASK_GLYPH}\\s*`), '');
|
|
39
|
+
const qi = body.lastIndexOf(question);
|
|
40
|
+
const reason = qi > 0 ? body.slice(0, qi).trim() : '';
|
|
41
|
+
const keys = qi >= 0 ? body.slice(qi + question.length).trim() : '';
|
|
42
|
+
return { question, reason, keys, who: '', action: '' };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** The card footer's keys, spelled out the way approvals show theirs. */
|
|
46
|
+
export function keysLabel(keys) {
|
|
47
|
+
const k = String(keys || '').trim();
|
|
48
|
+
if (k === 'y/N/c') return 'y yes · n no · c chat';
|
|
49
|
+
if (k === 'y/N' || k === 'y/n') return 'y yes · n no';
|
|
50
|
+
if (/^\(?Y\/n\)?$/.test(k)) return 'y yes · n no (Enter is yes)';
|
|
51
|
+
return k;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** The question part of an ask line ("▶ reason. Question? keys" -> "Question?"), for "Tell me more about: ...". */
|
|
55
|
+
export function askQuestion(line) {
|
|
56
|
+
const s = String(line || '').replace(new RegExp(`^\\s*${ASK_GLYPH}\\s*`), '');
|
|
57
|
+
const end = s.lastIndexOf('?');
|
|
58
|
+
if (end < 0) return s.trim();
|
|
59
|
+
const head = s.slice(0, end + 1);
|
|
60
|
+
let start = 0;
|
|
61
|
+
for (const m of head.matchAll(/[.;!)] (?=[A-Z])/g)) start = m.index + 2; // the last sentence before the "?"
|
|
62
|
+
return head.slice(start).trim();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** y/N/c: what an answer line means. 'yes' | 'no' (n, no, empty) | 'command' (a slash line) | 'chat' (c, or any other text). */
|
|
66
|
+
export function askAnswer(text) {
|
|
67
|
+
const t = String(text ?? '').trim();
|
|
68
|
+
if (/^y(es)?$/i.test(t)) return 'yes';
|
|
69
|
+
if (!t || /^no?$/i.test(t)) return 'no';
|
|
70
|
+
if (t.startsWith('/')) return 'command';
|
|
71
|
+
return 'chat';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** The chat turn an ask's c/free text sends. Bare c (or "chat"): "Tell me more about: <question>, before I decide". */
|
|
75
|
+
export function askChatMessage(text, question) {
|
|
76
|
+
const t = String(text ?? '').trim();
|
|
77
|
+
if (/^c(hat)?$/i.test(t)) return `Tell me more about: ${question.replace(/\?$/, '')}, before I decide`;
|
|
78
|
+
return `${t}\n\n(The terminal is waiting for my y/N answer to: "${question}". I have not answered it yet.)`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ASK-SETTLE (owner 2026-10-17: "questions should go once answered, or change to answer"): an open ask is a live
|
|
82
|
+
// card; once answered, expired or no longer open it leaves the screen and ONE line stays in the scrollback.
|
|
83
|
+
/** HH:MM of a timestamp, in this terminal's local time. */
|
|
84
|
+
export function askClock(at = Date.now()) {
|
|
85
|
+
const d = new Date(at);
|
|
86
|
+
return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** The one line an ask leaves behind. outcome.kind: yes | no | run | skipped (outcome.cmd) | expired | closed. */
|
|
90
|
+
export function settledAskLine(question, outcome, at = Date.now()) {
|
|
91
|
+
const q = String(question || '').trim();
|
|
92
|
+
const t = askClock(at);
|
|
93
|
+
switch (outcome?.kind) {
|
|
94
|
+
case 'yes': return `✔ ${q} yes (${t})`;
|
|
95
|
+
case 'run': return `✔ ${q} run (${t})`;
|
|
96
|
+
case 'no': return `✔ ${q} no (${t})`;
|
|
97
|
+
case 'skipped': return `✔ ${q} skipped, you ran ${String(outcome.cmd || 'a command').split(/\s/)[0]} (${t})`;
|
|
98
|
+
case 'dismissed': return `↷ ${q} offer dismissed (${t})`;
|
|
99
|
+
case 'expired': return `⌛ ${q} expired, not answered (${t})`;
|
|
100
|
+
default: return `↷ ${q} no longer open (${t})`;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/** The note tone a settled line is shown in (never dim: no grey text). */
|
|
104
|
+
export const settledAskTone = (kind) => (kind === 'yes' || kind === 'run' ? 'ok' : 'plain');
|
|
105
|
+
|
|
106
|
+
/** What a submitted line does to an open ask: { kind } for an answer, or null (c / free text keeps it open). */
|
|
107
|
+
export function askOutcomeOf(text, { runWord = false } = {}) {
|
|
108
|
+
const t = String(text ?? '').trim();
|
|
109
|
+
if (runWord && /^run$/i.test(t)) return { kind: 'run' };
|
|
110
|
+
const a = askAnswer(t);
|
|
111
|
+
if (a === 'yes') return { kind: 'yes' };
|
|
112
|
+
if (a === 'no') return { kind: 'no' };
|
|
113
|
+
if (a === 'command') return { kind: 'skipped', cmd: t };
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** A chat line that asks about the open offer (so the card stays): a question, "c", or a why/what/how/explain opener. Anything else is the user moving on, which dismisses the offer. */
|
|
118
|
+
export function isAskClarification(text) {
|
|
119
|
+
const t = String(text ?? '').trim();
|
|
120
|
+
return /^c(hat)?$/i.test(t) || /\?\s*$/.test(t) || /^(why|what|how|which|who|when|where|explain|tell me|can you|could you|does|is it|is this)\b/i.test(t);
|
|
121
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* E5 local state: when this profile was last seen in the client (~/.config/quox/away.json, keyed by
|
|
3
|
+
* profile). Only a timestamp is kept; the digest itself always comes from the server. Writes are
|
|
4
|
+
* best-effort: a read-only home must never break the client.
|
|
5
|
+
*/
|
|
6
|
+
import { readFileSync, writeFileSync, mkdirSync, renameSync } from 'node:fs';
|
|
7
|
+
import { homedir } from 'node:os';
|
|
8
|
+
import { join, dirname } from 'node:path';
|
|
9
|
+
|
|
10
|
+
export const AWAY_AFTER_MS = 2 * 3600000;
|
|
11
|
+
export const awayStatePath = () => join(homedir(), '.config', 'quox', 'away.json');
|
|
12
|
+
|
|
13
|
+
function readAll(path) {
|
|
14
|
+
try { const j = JSON.parse(readFileSync(path, 'utf8')); return j && typeof j === 'object' && !Array.isArray(j) ? j : {}; } catch { return {}; }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** -> epoch ms or null (never seen, or unreadable). */
|
|
18
|
+
export function readLastSeen(profile, path = awayStatePath()) {
|
|
19
|
+
const v = readAll(path)[profile || 'default'];
|
|
20
|
+
const t = typeof v === 'number' ? v : Date.parse(v);
|
|
21
|
+
return Number.isFinite(t) ? t : null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function writeLastSeen(profile, ts = Date.now(), path = awayStatePath()) {
|
|
25
|
+
try {
|
|
26
|
+
const all = readAll(path);
|
|
27
|
+
all[profile || 'default'] = new Date(ts).toISOString();
|
|
28
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
29
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
30
|
+
writeFileSync(tmp, JSON.stringify(all) + '\n');
|
|
31
|
+
renameSync(tmp, path);
|
|
32
|
+
return true;
|
|
33
|
+
} catch { return false; }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Show the launch card only when the last session ended more than 2 h ago (never on a first launch). */
|
|
37
|
+
export const awayDue = (lastSeen, now = Date.now()) => lastSeen != null && now - lastSeen > AWAY_AFTER_MS;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* /away [since]: a window (90m, 3h, 2d), an ISO time, or nothing (last seen, else 24 h).
|
|
41
|
+
* -> { ok: true, since: iso } | { ok: false, error }
|
|
42
|
+
*/
|
|
43
|
+
export function parseSince(arg, { now = Date.now(), lastSeen = null } = {}) {
|
|
44
|
+
const a = String(arg || '').trim();
|
|
45
|
+
if (!a) return { ok: true, since: new Date(lastSeen ?? now - 24 * 3600000).toISOString() };
|
|
46
|
+
const m = a.match(/^(\d+)\s*([mhd])$/i);
|
|
47
|
+
if (m) return { ok: true, since: new Date(now - Number(m[1]) * { m: 60000, h: 3600000, d: 86400000 }[m[2].toLowerCase()]).toISOString() };
|
|
48
|
+
const t = /^\d{4}-\d\d-\d\d/.test(a) ? Date.parse(a) : NaN;
|
|
49
|
+
if (!Number.isFinite(t)) return { ok: false, error: `Usage: /away [since] (e.g. /away 3h, /away 2d, /away 2026-10-13T08:00:00Z)` };
|
|
50
|
+
if (t > now) return { ok: false, error: 'That time is in the future.' };
|
|
51
|
+
return { ok: true, since: new Date(t).toISOString() };
|
|
52
|
+
}
|