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,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The palette's key handling, kept out of app.js. The palette is not modal: it shows while the draft is
|
|
3
|
+
* a bare `/word`, and only Up, Down, Tab, Enter and Esc go to it. Everything else is ordinary typing,
|
|
4
|
+
* which narrows the list because the draft IS the filter.
|
|
5
|
+
*/
|
|
6
|
+
import { findEntry } from '../commands.js';
|
|
7
|
+
import { signedOutNotice } from '../sessionDead.js';
|
|
8
|
+
import { paletteQuery, paletteItems, filterPalette, completion, paletteView, PALETTE_MAX } from './palette.js';
|
|
9
|
+
import { argContext, levelFor, staticItems, rankArgs, applyArg } from './args.js';
|
|
10
|
+
import { mentionContext, rankMentions, applyMention } from '../mentions.js';
|
|
11
|
+
|
|
12
|
+
const ARG_TTL = 30000; // a fetched list is reused this long
|
|
13
|
+
const ARG_FAIL_TTL = 8000; // a failed fetch is quiet (no rows) and retried after this
|
|
14
|
+
|
|
15
|
+
const startsLike = (q, it) => {
|
|
16
|
+
const w = String(q || '').toLowerCase().replace(/^\//, '');
|
|
17
|
+
return [it.name, ...(it.aliases || [])].some(n => String(n).toLowerCase().replace(/^\//, '').startsWith(w));
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export function createPalette({
|
|
21
|
+
getDraft, setDraft, run, say, ctx = () => ({}), source = null, refresh = () => {}, now = Date.now,
|
|
22
|
+
mentions = null // MENTIONS: { cache (mentions.js createMentionCache), local: () => receipt items }
|
|
23
|
+
}) {
|
|
24
|
+
const st = { sel: 0, q: null, dismissed: null, picked: false, aq: null, mq: null };
|
|
25
|
+
const cache = new Map(); // 'src:param' -> { at, ttl, items, pending }
|
|
26
|
+
/** Cached items for an async source; fetches when missing or stale. Never throws or blocks. */
|
|
27
|
+
const fetchItems = (src, param) => {
|
|
28
|
+
if (!source) return [];
|
|
29
|
+
const key = `${src}:${param ?? ''}`;
|
|
30
|
+
const hit = cache.get(key);
|
|
31
|
+
if (hit && (hit.pending || now() - hit.at < hit.ttl)) return hit.items;
|
|
32
|
+
const ent = { at: now(), ttl: ARG_TTL, items: hit?.items ?? [], pending: true };
|
|
33
|
+
cache.set(key, ent);
|
|
34
|
+
Promise.resolve().then(() => source(src, param)).then(
|
|
35
|
+
(items) => {
|
|
36
|
+
ent.items = Array.isArray(items) ? items : [];
|
|
37
|
+
ent.at = now(); ent.pending = false;
|
|
38
|
+
},
|
|
39
|
+
() => { ent.items = []; ent.ttl = ARG_FAIL_TTL; ent.at = now(); ent.pending = false; }
|
|
40
|
+
).then(() => refresh());
|
|
41
|
+
return ent.items;
|
|
42
|
+
};
|
|
43
|
+
/** The argument suggestions for the current draft, or null when it is not at an argument. */
|
|
44
|
+
const argState = () => {
|
|
45
|
+
const d = getDraft();
|
|
46
|
+
const cx = argContext(d);
|
|
47
|
+
if (!cx) return null;
|
|
48
|
+
const lv = levelFor(cx.entry, cx.words);
|
|
49
|
+
if (!lv) return null;
|
|
50
|
+
const param = lv.of === 'partial' ? cx.partial.slice(0, cx.partial.lastIndexOf('/') + 1) : lv.of != null ? cx.words[lv.of] : undefined; // 'partial': the directory typed so far (QUOXON-MESH /send paths)
|
|
51
|
+
const items = [...staticItems(lv), ...(lv.src ? fetchItems(lv.src, param) : [])];
|
|
52
|
+
const rows = rankArgs(items, cx.partial);
|
|
53
|
+
const key = d.slice(0, cx.start) + '\u0000' + cx.partial;
|
|
54
|
+
if (key !== st.aq) { st.aq = key; st.sel = 0; st.picked = false; }
|
|
55
|
+
return { cx, rows };
|
|
56
|
+
};
|
|
57
|
+
const argOpen = () => {
|
|
58
|
+
const d = getDraft();
|
|
59
|
+
if (st.dismissed !== null && st.dismissed !== d) st.dismissed = null;
|
|
60
|
+
if (st.dismissed === d) return null;
|
|
61
|
+
const a = argState();
|
|
62
|
+
return a && a.rows.length ? a : null;
|
|
63
|
+
};
|
|
64
|
+
/** MENTIONS: the @word at the end of the draft and its ranked rows from the session cache, or null. */
|
|
65
|
+
const mentionOpen = () => {
|
|
66
|
+
if (!mentions) return null;
|
|
67
|
+
const d = getDraft();
|
|
68
|
+
const cx = mentionContext(d);
|
|
69
|
+
if (!cx) return null;
|
|
70
|
+
if (st.dismissed !== null && st.dismissed !== d) st.dismissed = null;
|
|
71
|
+
if (st.dismissed === d) return null;
|
|
72
|
+
const rows = rankMentions([...mentions.cache.items(), ...(mentions.local?.() || [])], cx.partial);
|
|
73
|
+
const key = d.slice(0, cx.start) + '\u0000' + cx.partial;
|
|
74
|
+
if (key !== st.mq) { st.mq = key; st.sel = 0; st.picked = false; }
|
|
75
|
+
return rows.length ? { cx, rows } : null;
|
|
76
|
+
};
|
|
77
|
+
const rows = () => {
|
|
78
|
+
const q = paletteQuery(getDraft());
|
|
79
|
+
if (q !== st.q) { st.q = q; st.sel = 0; st.picked = false; }
|
|
80
|
+
return q === null ? [] : filterPalette(paletteItems(ctx()), q);
|
|
81
|
+
};
|
|
82
|
+
const isOpen = () => {
|
|
83
|
+
const d = getDraft();
|
|
84
|
+
if (st.dismissed !== null && st.dismissed !== d) st.dismissed = null;
|
|
85
|
+
if (paletteQuery(d) === null) return argOpen() !== null || mentionOpen() !== null;
|
|
86
|
+
return st.dismissed !== d;
|
|
87
|
+
};
|
|
88
|
+
/** MENTIONS keys: Up/Down choose, Tab or Enter accept, Esc closes. */
|
|
89
|
+
function onMentionKey(name, m) {
|
|
90
|
+
const n = m.rows.length;
|
|
91
|
+
switch (name) {
|
|
92
|
+
case 'up': st.sel = (st.sel - 1 + n) % n; st.picked = true; return true;
|
|
93
|
+
case 'down': st.sel = (st.sel + 1) % n; st.picked = true; return true;
|
|
94
|
+
case 'escape': st.dismissed = getDraft(); return true;
|
|
95
|
+
case 'tab': case 'enter': setDraft(applyMention(getDraft(), m.cx, m.rows[Math.min(st.sel, n - 1)].value)); return true;
|
|
96
|
+
default: return false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/** -> true when the key was the palette's. */
|
|
100
|
+
function onArgKey(name, a) {
|
|
101
|
+
const n = a.rows.length;
|
|
102
|
+
switch (name) {
|
|
103
|
+
case 'up': st.sel = (st.sel - 1 + n) % n; st.picked = true; return true;
|
|
104
|
+
case 'down': st.sel = (st.sel + 1) % n; st.picked = true; return true;
|
|
105
|
+
case 'escape': st.dismissed = getDraft(); return true;
|
|
106
|
+
case 'tab': case 'enter': {
|
|
107
|
+
if (name === 'enter' && !st.picked) return false; // Enter sends what was typed
|
|
108
|
+
const it = a.rows[Math.min(st.sel, n - 1)];
|
|
109
|
+
setDraft(applyArg(getDraft(), a.cx, it.value));
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
default: return false;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
function onKey(name) {
|
|
116
|
+
if (!isOpen()) return false;
|
|
117
|
+
if (paletteQuery(getDraft()) === null) { const a = argOpen(); return a ? onArgKey(name, a) : onMentionKey(name, mentionOpen()); }
|
|
118
|
+
const list = rows();
|
|
119
|
+
switch (name) {
|
|
120
|
+
case 'up': if (!list.length) return false; st.sel = (st.sel - 1 + list.length) % list.length; st.picked = true; return true;
|
|
121
|
+
case 'down': if (!list.length) return false; st.sel = (st.sel + 1) % list.length; st.picked = true; return true;
|
|
122
|
+
case 'escape': st.dismissed = getDraft(); return true;
|
|
123
|
+
case 'tab': {
|
|
124
|
+
const it = list[st.sel];
|
|
125
|
+
if (!it) return true;
|
|
126
|
+
setDraft(completion(it));
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
case 'enter': {
|
|
130
|
+
const typed = findEntry(getDraft());
|
|
131
|
+
if (typed && typed.kind === 'command') return false; // an exact command runs as typed (planned ones say so)
|
|
132
|
+
const it = list[st.sel];
|
|
133
|
+
if (!it) return false; // nothing matches: the normal path says "Unknown command"
|
|
134
|
+
// A loose (subsequence) match only runs when the user chose it with the arrows. Typing an unknown
|
|
135
|
+
// word and pressing Enter must not run some other command (z2h: "/hat" ran /approvals).
|
|
136
|
+
if (!st.picked && !startsLike(st.q, it)) return false;
|
|
137
|
+
if (it.disabled) { say(signedOutNotice(it.name, it.disabled), 'warn'); return true; }
|
|
138
|
+
setDraft('');
|
|
139
|
+
run(it.name);
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
default: return false;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
function view() {
|
|
146
|
+
if (paletteQuery(getDraft()) === null) {
|
|
147
|
+
const a = argOpen();
|
|
148
|
+
if (!a) {
|
|
149
|
+
const mo = mentionOpen();
|
|
150
|
+
if (!mo) return { rows: [], sel: 0, more: 0, empty: null, hint: null };
|
|
151
|
+
const mv = paletteView(mo.rows, st.sel, PALETTE_MAX);
|
|
152
|
+
return { rows: mv.rows.map(it => ({ key: `@${it.value}`, text: `${it.kind}${it.label ? ` · ${it.label}` : ''}` })), sel: mv.sel, more: mv.more, empty: null, hint: '↑↓ select · Tab or Enter accept · Esc close' };
|
|
153
|
+
}
|
|
154
|
+
const v = paletteView(a.rows, st.sel, PALETTE_MAX);
|
|
155
|
+
const rows = v.rows.map(it => ({ key: it.value, text: it.desc || '' }));
|
|
156
|
+
const hint = '↑↓ select · Tab complete · Esc close';
|
|
157
|
+
return { rows, sel: v.sel, more: v.more, empty: null, hint };
|
|
158
|
+
}
|
|
159
|
+
const list = rows();
|
|
160
|
+
const v = paletteView(list, st.sel, PALETTE_MAX);
|
|
161
|
+
return {
|
|
162
|
+
rows: v.rows.map(it => ({ key: it.name, text: (it.aliases.length ? `(also ${it.aliases.join(', ')}) ` : '') + it.desc, dim: Boolean(it.disabled), note: it.disabled || null })),
|
|
163
|
+
sel: v.sel, more: v.more,
|
|
164
|
+
empty: 'No command matches. Enter sends it anyway; /help lists everything.',
|
|
165
|
+
hint: '↑↓ select · Tab complete · Enter run · Esc close'
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
return { isOpen, onKey, view, state: st };
|
|
169
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure panel models for /status, /context and /doctor, rendered with the /help view (head + key/value rows).
|
|
3
|
+
* Every value comes from the caller's real data; anything not known is said plainly, never filled in.
|
|
4
|
+
*/
|
|
5
|
+
import { capabilityMap } from './palette.js';
|
|
6
|
+
import { CHOICES } from '../model.js';
|
|
7
|
+
import { providerState, capabilityName } from '../providers.js';
|
|
8
|
+
|
|
9
|
+
const n = (v) => Number(v || 0).toLocaleString('en-US');
|
|
10
|
+
const row = (key, text) => ({ kind: 'row', key, text: String(text) });
|
|
11
|
+
|
|
12
|
+
export function healthText(hl) {
|
|
13
|
+
if (!hl) return '? not checked';
|
|
14
|
+
if (hl.ok) return `✔ answering (HTTP ${hl.status})`;
|
|
15
|
+
if (hl.absent) return `✖ nothing running there (${hl.status})`;
|
|
16
|
+
return `✖ not healthy (${hl.status})`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** What actually ran (turn.started), else what was asked for, said as such. */
|
|
20
|
+
export function modelText(ctx = {}, choice = null) {
|
|
21
|
+
if (ctx.provider || ctx.model) return `${[ctx.provider, ctx.model].filter(Boolean).join(' ')} (as the server last reported)`;
|
|
22
|
+
const ask = choice ? [choice.provider, choice.model].filter(Boolean).join(' ') : 'auto (server policy)';
|
|
23
|
+
return `no turn yet; asking for ${ask}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function statusModel(d = {}) {
|
|
27
|
+
return [
|
|
28
|
+
{ kind: 'head', text: 'Status' },
|
|
29
|
+
row('Profile', d.profile || '?'),
|
|
30
|
+
row('Org', d.org || '? not in this login'),
|
|
31
|
+
row('Server', `${d.serverUrl || '?'} · ${healthText(d.health)}`),
|
|
32
|
+
row('Model', modelText(d.ctx, d.choice)),
|
|
33
|
+
row('Agent', d.agent || 'CommanderQ (quox)'),
|
|
34
|
+
row('Slot', d.slot || 'none (/slot <name>)'),
|
|
35
|
+
row('Session', d.sessionName || 'unnamed (/rename <name>)'),
|
|
36
|
+
row('Conversation', d.conversationId || 'new (starts with your next message)'),
|
|
37
|
+
row('Version', d.version || '?'),
|
|
38
|
+
...(d.local ? [{ kind: 'dim', text: 'Server health not re-checked for this tip; /status checks it live.' }] : [])
|
|
39
|
+
];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function contextModel(d = {}) {
|
|
43
|
+
const rules = d.rules;
|
|
44
|
+
const ruleFiles = rules?.files || [];
|
|
45
|
+
const out = [
|
|
46
|
+
{ kind: 'head', text: 'Context' },
|
|
47
|
+
row('Model', modelText(d.ctx, d.choice)),
|
|
48
|
+
row('Conversation', !d.conversationId ? 'new, nothing sent yet' : d.server ? `${d.server.tokensText}${d.server.turnsText ? ` · ${d.server.turnsText}` : ''}` : `${n(d.tokens)} tokens reported so far in this session${d.serverError ? ` (server context: ${d.serverError})` : ''}`),
|
|
49
|
+
...(d.conversationId && d.server ? [row('Summary', d.server.summaryText)] : []),
|
|
50
|
+
row('Rules', d.rulesOn === false ? 'off for this session (/rules on)' : ruleFiles.length ? `${ruleFiles.length} QUOX.md file${ruleFiles.length === 1 ? '' : 's'}, ${n(rules.totalChars)} chars${rules.truncated ? ' (truncated to fit)' : ''}` : 'none loaded')
|
|
51
|
+
];
|
|
52
|
+
if (d.rulesOn !== false) for (const f of ruleFiles) out.push({ kind: 'dim', key: '', text: ` ${f.label || f.path} ${n(f.chars)} chars` });
|
|
53
|
+
const sk = d.skills;
|
|
54
|
+
out.push(row('Skills', !sk ? '? not checked (/context checks it live)' : sk.ok ? `${sk.count} available in your org (/skills)` : `? ${sk.error}`));
|
|
55
|
+
const mem = d.memory;
|
|
56
|
+
out.push(row('Memory', !mem ? '? not checked (/context checks it live)' : mem.ok ? 'connected (persistent memory answers)' : `not reachable: ${mem.error}`));
|
|
57
|
+
out.push(row('Agent', d.agent || 'CommanderQ (quox)'));
|
|
58
|
+
if (d.screen) {
|
|
59
|
+
const l = d.screen.last;
|
|
60
|
+
out.push(row('Terminal', !d.screen.on ? 'off for this session (/context on)' : l ? `${l.bytes} bytes sent last turn: ${l.sections.join(', ') || 'nothing new'}${l.unchanged?.length ? ` (unchanged: ${l.unchanged.join(', ')})` : ''} · /context off stops it` : 'on, sent with your next message · /context off stops it'));
|
|
61
|
+
if (d.screen.on && l?.text) for (const t of l.text.split('\n')) out.push({ kind: 'dim', key: '', text: ' ' + t });
|
|
62
|
+
}
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const icon = (s) => (s === 'pass' ? '✔' : s === 'warn' ? '⚠' : s === 'skip' ? '·' : '✖');
|
|
67
|
+
|
|
68
|
+
/** checks: [{ name, status:'pass'|'warn'|'fail'|'error'|'skip', detail?, latency_ms? }] */
|
|
69
|
+
export function doctorModel(platform, chat, platformError = null) {
|
|
70
|
+
const rowOf = (c) => ({ kind: c.status === 'skip' ? 'dim' : 'row', key: `${icon(c.status)} ${c.name}`, text: [c.detail, c.latency_ms != null ? `${c.latency_ms}ms` : null].filter(Boolean).join(' · ') || (c.status === 'pass' ? 'ok' : c.status) });
|
|
71
|
+
const out = [{ kind: 'head', text: 'Doctor' }];
|
|
72
|
+
if (platformError) out.push(row('✖ quox doctor', platformError));
|
|
73
|
+
for (const c of platform || []) out.push(rowOf(c));
|
|
74
|
+
out.push({ kind: 'gap' }, { kind: 'head', text: 'Chat' });
|
|
75
|
+
for (const c of chat || []) out.push(rowOf(c));
|
|
76
|
+
const all = [...(platform || []), ...(chat || [])];
|
|
77
|
+
const count = (s) => all.filter(c => c.status === s || (s === 'fail' && c.status === 'error')).length;
|
|
78
|
+
out.push({ kind: 'gap' }, { kind: 'dim', text: `${count('pass')} passed · ${count('warn')} warning${count('warn') === 1 ? '' : 's'} · ${count('fail')} failed` });
|
|
79
|
+
return out;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Chat checks, each on its own: the chat routes answer, a provider is connected, the capability contract. */
|
|
83
|
+
export async function chatChecks(api, providerTypes = ['openai', 'anthropic', 'ollama']) {
|
|
84
|
+
const one = async (name, fn) => { try { return { name, ...(await fn()) }; } catch (e) { return { name, status: 'fail', detail: e?.status ? `HTTP ${e.status} ${e.message || ''}`.trim() : (e?.message || 'failed') }; } };
|
|
85
|
+
return Promise.all([
|
|
86
|
+
one('Chat server', async () => {
|
|
87
|
+
if (!api.conversations) return { status: 'skip', detail: 'not available in this mode' };
|
|
88
|
+
const t0 = Date.now(); await api.conversations(1);
|
|
89
|
+
return { status: 'pass', detail: 'conversation routes answer for this login', latency_ms: Date.now() - t0 };
|
|
90
|
+
}),
|
|
91
|
+
one('AI provider', async () => {
|
|
92
|
+
if (!api.connections) return { status: 'skip', detail: 'not available in this mode' };
|
|
93
|
+
// The same provider status as /model and /connect (providers.js): the server's capabilities first.
|
|
94
|
+
const [r, caps] = await Promise.all([api.connections(), api.capabilities ? api.capabilities() : null]);
|
|
95
|
+
const ready = CHOICES.filter(c => providerState({ cap: c.cap || c.id, vault: c.vault }, caps, r).state === 'connected').map(c => c.name);
|
|
96
|
+
if (ready.length) return { status: 'pass', detail: `ready: ${ready.join(', ')}` };
|
|
97
|
+
if (!r.ok && !caps?.ok) return { status: 'fail', detail: `could not read the vault: ${r.error}` };
|
|
98
|
+
return { status: 'warn', detail: 'no AI provider is ready; /connect openai' };
|
|
99
|
+
}),
|
|
100
|
+
one('Capabilities', async () => {
|
|
101
|
+
if (!api.capabilities) return { status: 'skip', detail: 'not available in this mode' };
|
|
102
|
+
const res = await api.capabilities();
|
|
103
|
+
if (res && res.ok === false) return res.missing ? { status: 'warn', detail: 'this server has no capability contract yet (GET /api/v1/capabilities)' } : { status: 'fail', detail: res.error || 'failed' };
|
|
104
|
+
const m = capabilityMap(res);
|
|
105
|
+
if (!m) return { status: 'warn', detail: 'unexpected capability response' };
|
|
106
|
+
const names = (a) => res.capabilities.filter(c => c?.availability === a).map(capabilityName);
|
|
107
|
+
const off = names('unavailable'), setup = names('requires_setup');
|
|
108
|
+
const more = (l) => `${l.slice(0, 4).join(', ')}${l.length > 4 ? ` +${l.length - 4} more` : ''}`;
|
|
109
|
+
const parts = [`${Object.keys(m).length} known`];
|
|
110
|
+
if (off.length) parts.push(`unavailable here: ${more(off)}`);
|
|
111
|
+
if (setup.length) parts.push(`needs setup: ${more(setup)}`);
|
|
112
|
+
if (!off.length && !setup.length) parts.push('all available');
|
|
113
|
+
return { status: off.length ? 'warn' : 'pass', detail: parts.join(', ') };
|
|
114
|
+
})
|
|
115
|
+
]);
|
|
116
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A modal list under the composer (the /resume picker, the /config card). While open it takes every key,
|
|
3
|
+
* like the /connect card: Up/Down select, Enter picks, Esc closes, Left/Right/Space go to onAdjust.
|
|
4
|
+
* Typing (any text but a Space the card uses) closes it and goes to the composer, so a slash command typed
|
|
5
|
+
* while it is open runs instead of being read as navigation (R3 D2). onEvent returns false for those keys.
|
|
6
|
+
*/
|
|
7
|
+
export function createPicker({ refresh = () => {} } = {}) {
|
|
8
|
+
let cur = null; // { title, rows, sel, hint, onPick, onAdjust, onClose, rowsFn }
|
|
9
|
+
const close = () => { const c = cur; cur = null; c?.onClose?.(); refresh(); };
|
|
10
|
+
return {
|
|
11
|
+
isOpen: () => Boolean(cur),
|
|
12
|
+
open(spec) { cur = { sel: 0, ...spec }; refresh(); },
|
|
13
|
+
close,
|
|
14
|
+
rows: () => (cur ? (cur.rowsFn ? cur.rowsFn() : cur.rows) : []),
|
|
15
|
+
onEvent(e) {
|
|
16
|
+
if (!cur) return false;
|
|
17
|
+
const rows = cur.rowsFn ? cur.rowsFn() : cur.rows;
|
|
18
|
+
// onChar(char, sel): a card key such as f or d. true, or a new selection index = handled.
|
|
19
|
+
if (e.type === 'text' && cur.onChar) {
|
|
20
|
+
const r = cur.onChar(e.text, cur.sel);
|
|
21
|
+
if (r === true || typeof r === 'number') {
|
|
22
|
+
if (typeof r === 'number') {
|
|
23
|
+
const n = (cur.rowsFn ? cur.rowsFn() : cur.rows).length;
|
|
24
|
+
cur.sel = Math.max(0, Math.min(r, Math.max(0, n - 1)));
|
|
25
|
+
}
|
|
26
|
+
refresh();
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (e.type === 'paste' || (e.type === 'text' && !(e.text === ' ' && cur.onAdjust))) { close(); return false; } // typing goes to the composer
|
|
31
|
+
if (e.type !== 'key' && !(e.type === 'text' && e.text === ' ')) return true;
|
|
32
|
+
const name = e.type === 'text' ? 'space' : e.name;
|
|
33
|
+
if (name === 'escape' || name === 'ctrl-c') { close(); return true; }
|
|
34
|
+
if (name === 'up' && rows.length) cur.sel = (cur.sel - 1 + rows.length) % rows.length;
|
|
35
|
+
else if (name === 'down' && rows.length) cur.sel = (cur.sel + 1) % rows.length;
|
|
36
|
+
else if ((name === 'left' || name === 'right' || name === 'space') && cur.onAdjust) cur.onAdjust(cur.sel, name === 'left' ? -1 : 1);
|
|
37
|
+
else if (name === 'enter') {
|
|
38
|
+
const c = cur; const pick = rows[c.sel];
|
|
39
|
+
if (c.keepOpen?.(c.sel)) c.onPick?.(pick, c.sel);
|
|
40
|
+
else { cur = null; c.onPick?.(pick, c.sel); }
|
|
41
|
+
}
|
|
42
|
+
refresh();
|
|
43
|
+
return true;
|
|
44
|
+
},
|
|
45
|
+
view() {
|
|
46
|
+
if (!cur) return null;
|
|
47
|
+
const rows = cur.rowsFn ? cur.rowsFn() : cur.rows;
|
|
48
|
+
const max = 10;
|
|
49
|
+
const start = Math.max(0, Math.min(cur.sel - max + 1, rows.length - max));
|
|
50
|
+
const title = cur.titleFn ? cur.titleFn() : cur.title;
|
|
51
|
+
const more = Math.max(0, rows.length - max);
|
|
52
|
+
const view = rows.slice(start, start + max);
|
|
53
|
+
return { title, rows: view, sel: cur.sel - start, more, hint: cur.hint, empty: cur.empty };
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* /restart and the "newer build installed" notice.
|
|
3
|
+
*
|
|
4
|
+
* The installed quox is whatever `quox` on PATH runs: a symlink to cli/bin/quox.js, or a small wrapper
|
|
5
|
+
* script that execs one (the dev box's ~/bin/quox). Its build is the git HEAD of that CLI tree, read
|
|
6
|
+
* locally (no fetch, no network). The running build is that HEAD captured when this client started, so a
|
|
7
|
+
* refresh of the tree under a running client shows up as "newer installed".
|
|
8
|
+
*/
|
|
9
|
+
import { execFile, execFileSync, spawnSync } from 'node:child_process';
|
|
10
|
+
import { accessSync, constants, existsSync, readFileSync, realpathSync, statSync } from 'node:fs';
|
|
11
|
+
import { delimiter, dirname, join, resolve } from 'node:path';
|
|
12
|
+
import { fileURLToPath } from 'node:url';
|
|
13
|
+
import os from 'node:os';
|
|
14
|
+
|
|
15
|
+
/** The CLI tree this code runs from (…/cli). */
|
|
16
|
+
export const RUNNING_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..', '..');
|
|
17
|
+
export const NEWER_NOTICE = 'A newer quox is installed. /restart to use it.';
|
|
18
|
+
export const DIFFERENT_NOTICE = 'A different quox build is installed. /restart to use it.';
|
|
19
|
+
|
|
20
|
+
export function findOnPath(name = 'quox', env = process.env) {
|
|
21
|
+
for (const dir of String(env.PATH || '').split(delimiter)) {
|
|
22
|
+
if (!dir) continue;
|
|
23
|
+
const p = join(dir, name);
|
|
24
|
+
try { accessSync(p, constants.X_OK); if (statSync(p).isFile()) return p; } catch { /* next */ }
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const isCliRoot = (d) => existsSync(join(d, 'lib', 'core', 'version.js')) && existsSync(join(d, 'bin', 'quox.js'));
|
|
30
|
+
|
|
31
|
+
/** The CLI root a `quox` executable runs, or null. */
|
|
32
|
+
export function cliRootOf(bin) {
|
|
33
|
+
if (!bin) return null;
|
|
34
|
+
let real;
|
|
35
|
+
try { real = realpathSync(bin); } catch { return null; }
|
|
36
|
+
if (/[/\\]bin[/\\]quox(\.js)?$/.test(real)) { const d = resolve(dirname(real), '..'); if (isCliRoot(d)) return d; }
|
|
37
|
+
try {
|
|
38
|
+
if (statSync(real).size > 65536) return null;
|
|
39
|
+
const m = /(\/[^\s"'$]+)\/bin\/quox\.js/.exec(readFileSync(real, 'utf8'));
|
|
40
|
+
if (m && isCliRoot(m[1])) return m[1];
|
|
41
|
+
} catch { /* unreadable */ }
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const git = (root, args) => execFileSync('git', ['-C', root, ...args], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 2000 }).trim();
|
|
46
|
+
export function headOf(root) { try { return root ? git(root, ['rev-parse', '--short', 'HEAD']) || null : null; } catch { return null; } }
|
|
47
|
+
/** true / false, or null when it cannot be told. */
|
|
48
|
+
export function isAncestor(root, a, b) {
|
|
49
|
+
try { execFileSync('git', ['-C', root, 'merge-base', '--is-ancestor', a, b], { stdio: 'ignore', timeout: 2000 }); return true; }
|
|
50
|
+
catch (e) { return e?.status === 1 ? false : null; }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Compare the running build with the installed one. -> notice text or null. `deps` lets tests stand in for
|
|
55
|
+
* git and PATH.
|
|
56
|
+
*/
|
|
57
|
+
export function newerBuild({ startSha, runningRoot = RUNNING_ROOT, env = process.env, deps = {} } = {}) {
|
|
58
|
+
const d = { findOnPath, cliRootOf, headOf, isAncestor, ...deps };
|
|
59
|
+
if (!startSha) return null;
|
|
60
|
+
const root = d.cliRootOf(d.findOnPath('quox', env)) || runningRoot;
|
|
61
|
+
const head = d.headOf(root);
|
|
62
|
+
if (!head || head === startSha || head.startsWith(startSha) || startSha.startsWith(head)) return null;
|
|
63
|
+
if (d.isAncestor(root, startSha, head) === true) return NEWER_NOTICE;
|
|
64
|
+
if (d.isAncestor(root, head, startSha) === true) return null; // the installed build is older than this one: nothing to gain
|
|
65
|
+
return DIFFERENT_NOTICE; // unrelated or unknown history: say so, never claim "newer
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** The installed build's id (its CLI tree's git HEAD), read locally. null when it cannot be told. */
|
|
69
|
+
export function installedBuildId({ runningRoot = RUNNING_ROOT, env = process.env } = {}) {
|
|
70
|
+
return headOf(cliRootOf(findOnPath('quox', env)) || runningRoot);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Poll at most every `everyMs`; calls onNotice(text, buildId) once. -> stop().
|
|
75
|
+
* With `repeat`, keeps watching and calls onNotice again for each LATER installed build (a new id).
|
|
76
|
+
*/
|
|
77
|
+
// settleMs defaults to 0 here (announce at once); the TUI (main.js) passes settleMsFrom() so real sessions settle.
|
|
78
|
+
export const RESTART_SETTLE_MS = 120000; // a newer build must stay the installed one this long before it is announced
|
|
79
|
+
export const RESTART_BOOT_WINDOW_MS = 5 * 60 * 1000; // after a start, the settle window runs from the start
|
|
80
|
+
|
|
81
|
+
export function settleMsFrom(env = process.env) {
|
|
82
|
+
const n = Number(env.QUOX_RESTART_SETTLE_MS);
|
|
83
|
+
return env.QUOX_RESTART_SETTLE_MS != null && env.QUOX_RESTART_SETTLE_MS !== '' && Number.isFinite(n) && n >= 0 ? n : RESTART_SETTLE_MS;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function startBuildWatch({ onNotice, everyMs = 60000, check = newerBuild, repeat = false, buildId = installedBuildId, settleMs = 0, now = Date.now, ...opts } = {}) {
|
|
87
|
+
let last; // repeat mode: the last build announced
|
|
88
|
+
let seen; let seenAt = 0; let seenNotice = null; // repeat mode: the build waiting to settle, and when it last changed
|
|
89
|
+
const bootAt = now();
|
|
90
|
+
const id = setInterval(() => {
|
|
91
|
+
let t = null;
|
|
92
|
+
try { t = check(opts); } catch { t = null; }
|
|
93
|
+
if (!t) return;
|
|
94
|
+
let b = null;
|
|
95
|
+
try { b = buildId(opts); } catch { b = null; }
|
|
96
|
+
if (!repeat) { clearInterval(id); onNotice(t, b); return; }
|
|
97
|
+
if (b === last && last !== undefined) return;
|
|
98
|
+
if (b !== seen || seen === undefined) { seen = b; seenAt = now(); } // a new build resets the settle timer
|
|
99
|
+
seenNotice = t;
|
|
100
|
+
const t0 = now() - bootAt < RESTART_BOOT_WINDOW_MS ? Math.max(seenAt, bootAt) : seenAt;
|
|
101
|
+
if (now() - t0 < settleMs) return;
|
|
102
|
+
last = b;
|
|
103
|
+
onNotice(seenNotice, b);
|
|
104
|
+
}, everyMs);
|
|
105
|
+
id.unref?.();
|
|
106
|
+
return () => clearInterval(id);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Same arguments, minus any earlier resume target, plus --resume <id> for this conversation. The profile and
|
|
111
|
+
* session name this client is using are carried over when the original command line did not name them.
|
|
112
|
+
*/
|
|
113
|
+
export function restartArgs(argv = [], conversationId = null, { profile = null, name = null } = {}) {
|
|
114
|
+
const out = [];
|
|
115
|
+
const pin = Boolean(profile) && !argv.includes('--demo'); // the profile the session RESOLVED wins over whatever argv said
|
|
116
|
+
for (let i = 0; i < argv.length; i++) {
|
|
117
|
+
const a = argv[i];
|
|
118
|
+
if (pin && (a === '-p' || a === '--profile')) { const n = argv[i + 1]; if (n !== undefined && !n.startsWith('-')) i++; continue; }
|
|
119
|
+
if (pin && a.startsWith('--profile=')) continue;
|
|
120
|
+
if (a === '--conversation') { i++; continue; }
|
|
121
|
+
if (a === '--resume') { const n = argv[i + 1]; if (n !== undefined && !n.startsWith('-')) i++; continue; }
|
|
122
|
+
if (/^--(resume|conversation)=/.test(a)) continue;
|
|
123
|
+
out.push(a);
|
|
124
|
+
}
|
|
125
|
+
const has = (...f) => out.some(a => f.includes(a) || f.some(x => a.startsWith(`${x}=`)));
|
|
126
|
+
if (pin) out.push('-p', profile);
|
|
127
|
+
if (name && !has('-n', '--name')) out.push('-n', name);
|
|
128
|
+
return conversationId ? [...out, '--resume', conversationId] : out;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** The quox a restart would run: the one on PATH, else this client's own tree. -> [command, leading args]. */
|
|
132
|
+
export function restartTarget(env = process.env, bin = findOnPath('quox', env)) {
|
|
133
|
+
return bin ? [bin, []] : [process.execPath, [join(RUNNING_ROOT, 'bin', 'quox.js')]];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Before the client lets go of the terminal: can the installed quox start at all? Runs `quox --version`
|
|
138
|
+
* (no network, no login). -> Promise<{ ok, bin (what runs), onPath (the PATH quox or null), version?, error? }>. A failure keeps the user in this client.
|
|
139
|
+
*/
|
|
140
|
+
export function checkRestart({ env = process.env, bin = findOnPath('quox', env), run = execFile, timeoutMs = 15000 } = {}) {
|
|
141
|
+
const [cmd, lead] = restartTarget(env, bin);
|
|
142
|
+
return new Promise((done) => {
|
|
143
|
+
run(cmd, [...lead, '--version'], { env, timeout: timeoutMs, encoding: 'utf8' }, (err, stdout, stderr) => {
|
|
144
|
+
if (err) {
|
|
145
|
+
const why = err.code === 'ENOENT' || err.code === 'EACCES' ? `${err.code}: it cannot be run`
|
|
146
|
+
: err.killed ? `no answer in ${Math.round(timeoutMs / 1000)} s`
|
|
147
|
+
: String(stderr || err.message || 'failed').trim().split('\n')[0].slice(0, 160);
|
|
148
|
+
return done({ ok: false, bin: cmd, onPath: bin || null, error: why });
|
|
149
|
+
}
|
|
150
|
+
done({ ok: true, bin: cmd, onPath: bin || null, version: String(stdout || '').trim().split('\n')[0] || null });
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Env var carrying the held slot name from the old client to its restarted successor. */
|
|
156
|
+
export const RESTART_SLOT_ENV = 'QUOX_RESTART_SLOT';
|
|
157
|
+
/** Env var carrying the /attach target (`agent:<id>` or `slot:<name>`) to the restarted successor. */
|
|
158
|
+
export const RESTART_ATTACH_ENV = 'QUOX_RESTART_ATTACH';
|
|
159
|
+
/** Env var carrying one line for the restarted client to say first (a failed /update says why there). */
|
|
160
|
+
export const RESTART_NOTE_ENV = 'QUOX_RESTART_NOTE';
|
|
161
|
+
/** Env var carrying the window's own org (#1034: /org switch is window-scoped) to the restarted successor. */
|
|
162
|
+
export const RESTART_ORG_ENV = 'QUOX_RESTART_ORG';
|
|
163
|
+
/** Env var carrying the window's /mode (PLAN MODE: only 'plan' is carried; ask is the default) to the restarted successor. */
|
|
164
|
+
export const RESTART_MODE_ENV = 'QUOX_RESTART_MODE';
|
|
165
|
+
|
|
166
|
+
/** Terminal modes the client turns on (bracketed paste, focus reporting, hidden cursor): all off before the hand-over. */
|
|
167
|
+
export const TERMINAL_RESTORE = '\x1b[?2004l\x1b[?1004l\x1b[?25h\x1b[?1006l\x1b[?1002l\x1b[?1000l'; // bracketed paste, focus, cursor; mouse tracking off (QUOXON-MOUSE)
|
|
168
|
+
|
|
169
|
+
/** Give the terminal back in a plain state: cooked mode, stdin paused, modes off. */
|
|
170
|
+
export function restoreTerminal({ stdin = process.stdin, stdout = process.stdout } = {}) {
|
|
171
|
+
try { if (stdin?.isTTY) stdin.setRawMode?.(false); stdin?.pause?.(); } catch { /* not a tty */ }
|
|
172
|
+
try { if (stdout?.isTTY) stdout.write(TERMINAL_RESTORE); } catch { /* closed */ }
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* What a restart runs: { cmd, args, env }. The installed quox (or this tree), the same arguments with --resume
|
|
177
|
+
* for this conversation, and the one-shot hand-over env (held slot, attach, org, plan mode) for the successor.
|
|
178
|
+
*/
|
|
179
|
+
export function restartPlan({ argv, conversationId, profile = null, name = null, slot = null, attach = null, org = null, mode = null, env = process.env, bin = findOnPath('quox', env) }) {
|
|
180
|
+
const args = restartArgs(argv, conversationId, { profile, name });
|
|
181
|
+
let childEnv = profile && !argv.includes('--demo') ? { ...env, QUOX_PROFILE: profile } : env; // belt and braces: same profile even if -p were lost
|
|
182
|
+
if (slot) childEnv = { ...childEnv, [RESTART_SLOT_ENV]: slot }; // the held slot rides along; the new client takes it over before this one is gone
|
|
183
|
+
if (attach) childEnv = { ...childEnv, [RESTART_ATTACH_ENV]: attach }; // so does the attached agent or slot
|
|
184
|
+
if (org) childEnv = { ...childEnv, [RESTART_ORG_ENV]: org }; // and the window's org (never the profile's file)
|
|
185
|
+
if (mode === 'plan') childEnv = { ...childEnv, [RESTART_MODE_ENV]: 'plan' }; // and the window's /mode (never the profile)
|
|
186
|
+
const [cmd, lead] = restartTarget(env, bin);
|
|
187
|
+
return { cmd, args: [...lead, ...args], env: childEnv };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Run the installed quox in this terminal, wait for it, and hand back { code } (its exit code), or
|
|
192
|
+
* { error } when it could not be started (the caller stays in the current client and says so).
|
|
193
|
+
* Only used when there is no launcher (QUOX_NO_LAUNCHER=1, or no handoff dir): it NESTS the successor under
|
|
194
|
+
* this process, which stays resident until the successor exits. The launcher path (launcher.js) does not.
|
|
195
|
+
*/
|
|
196
|
+
export function execRestart({ argv, conversationId, profile = null, name = null, slot = null, attach = null, org = null, mode = null, env = process.env, spawn = spawnSync, bin = findOnPath('quox', env), restore = restoreTerminal }) {
|
|
197
|
+
const plan = restartPlan({ argv, conversationId, profile, name, slot, attach, org, mode, env, bin });
|
|
198
|
+
restore(); // the child owns the terminal now
|
|
199
|
+
const r = spawn(plan.cmd, plan.args, { stdio: 'inherit', env: plan.env });
|
|
200
|
+
if (r.error) return { error: `${r.error.code || 'failed'}: ${r.error.message}`, bin: plan.cmd };
|
|
201
|
+
return { code: r.status ?? (r.signal ? 128 + (os.constants.signals[r.signal] || 0) : 1), bin: plan.cmd };
|
|
202
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* /resume and `quox --resume`: pick a recent conversation, show its last turns as ordinary history, and
|
|
3
|
+
* continue it. Conversations come from GET /api/v1/conversations (title, updated_at, turn_count); the agent
|
|
4
|
+
* comes from the session index (GET /api/v1/sessions) when that answers, and names set with /rename from
|
|
5
|
+
* the local name store.
|
|
6
|
+
*/
|
|
7
|
+
export const RESUME_TURNS = 20;
|
|
8
|
+
|
|
9
|
+
export function age(iso, now = Date.now()) {
|
|
10
|
+
const t = Date.parse(iso);
|
|
11
|
+
if (!Number.isFinite(t)) return '?';
|
|
12
|
+
const s = Math.max(0, Math.round((now - t) / 1000));
|
|
13
|
+
if (s < 60) return `${s}s ago`;
|
|
14
|
+
if (s < 3600) return `${Math.round(s / 60)}m ago`;
|
|
15
|
+
if (s < 86400) return `${Math.round(s / 3600)}h ago`;
|
|
16
|
+
return `${Math.round(s / 86400)}d ago`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** -> [{ id, title, agent, updated_at, turns }] newest first. */
|
|
20
|
+
export function resumeRows(conversations = [], index = null, names = {}) {
|
|
21
|
+
const agentOf = new Map();
|
|
22
|
+
for (const r of index?.rows || []) if (r.kind === 'conversation' && r.id) agentOf.set(r.id, r.agent || null);
|
|
23
|
+
return conversations.map(c => ({
|
|
24
|
+
id: c.id,
|
|
25
|
+
title: names[c.id]?.name || c.title || '(untitled)',
|
|
26
|
+
agent: agentOf.get(c.id) ?? null,
|
|
27
|
+
updated_at: c.updated_at || null,
|
|
28
|
+
turns: c.turn_count ?? null
|
|
29
|
+
})).sort((a, b) => String(b.updated_at || '').localeCompare(String(a.updated_at || '')));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const pickerRow = (r, now = Date.now()) => ({ key: r.id.slice(0, 12), text: `${r.title} · ${r.agent || 'agent not recorded'} · ${age(r.updated_at, now)}${r.turns != null ? ` · ${r.turns} turn${r.turns === 1 ? '' : 's'}` : ''}` });
|
|
33
|
+
|
|
34
|
+
/** An id, an id prefix (4+), or a session name/title -> { row } | { ambiguous:[ids] } | { none:true }. */
|
|
35
|
+
export function matchConversation(rows, q) {
|
|
36
|
+
const s = String(q || '').trim();
|
|
37
|
+
if (!s) return { none: true };
|
|
38
|
+
const exact = rows.find(r => r.id === s);
|
|
39
|
+
if (exact) return { row: exact };
|
|
40
|
+
const lower = s.toLowerCase();
|
|
41
|
+
const byName = rows.filter(r => r.title.toLowerCase() === lower);
|
|
42
|
+
if (byName.length === 1) return { row: byName[0] };
|
|
43
|
+
const pre = s.length >= 4 ? rows.filter(r => r.id.startsWith(s)) : [];
|
|
44
|
+
if (pre.length === 1) return { row: pre[0] };
|
|
45
|
+
const hits = byName.length ? byName : pre;
|
|
46
|
+
return hits.length > 1 ? { ambiguous: hits.map(r => r.id) } : { none: true };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Stored turns -> history items (user prompts and answers, rendered as normal history). */
|
|
50
|
+
export function turnsToHistory(turns = [], max = RESUME_TURNS) {
|
|
51
|
+
const list = turns.filter(t => (t.role === 'user' || t.role === 'assistant') && String(t.content || '').trim());
|
|
52
|
+
const shown = list.slice(-max);
|
|
53
|
+
return {
|
|
54
|
+
items: shown.map(t => (t.role === 'user' ? { kind: 'user', text: String(t.content) } : { kind: 'text', text: String(t.content) })),
|
|
55
|
+
answers: shown.filter(t => t.role === 'assistant').map((t, i) => ({ id: `resumed_${i}`, text: String(t.content) })),
|
|
56
|
+
shown: shown.length,
|
|
57
|
+
total: list.length
|
|
58
|
+
};
|
|
59
|
+
}
|