quoxcli 0.2.0 → 0.4.0
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 +1 -1
- package/README.md +2 -0
- package/bin/quox.js +99 -126
- package/lib/api/endpoints/actions.js +15 -0
- package/lib/api/endpoints/admin.js +46 -0
- package/lib/api/endpoints/agent-schedules.js +26 -0
- package/lib/api/endpoints/agents.js +7 -0
- package/lib/api/endpoints/assistants.js +12 -0
- package/lib/api/endpoints/away.js +15 -0
- package/lib/api/endpoints/blog.js +16 -0
- package/lib/api/endpoints/chat.js +14 -0
- package/lib/api/endpoints/contactsq.js +6 -0
- package/lib/api/endpoints/conversations.js +14 -0
- package/lib/api/endpoints/devServerRegistry.js +25 -0
- package/lib/api/endpoints/engagement.js +26 -0
- package/lib/api/endpoints/evidence.js +20 -0
- package/lib/api/endpoints/executions.js +13 -0
- package/lib/api/endpoints/fetchAllowlist.js +27 -0
- package/lib/api/endpoints/files.js +14 -0
- package/lib/api/endpoints/hat.js +44 -0
- package/lib/api/endpoints/inbox.js +20 -0
- package/lib/api/endpoints/index.js +26 -0
- package/lib/api/endpoints/loops.js +26 -0
- package/lib/api/endpoints/memory.js +19 -0
- package/lib/api/endpoints/mind.js +13 -0
- package/lib/api/endpoints/mon.js +179 -0
- package/lib/api/endpoints/objectives.js +12 -0
- package/lib/api/endpoints/plugins.js +18 -0
- package/lib/api/endpoints/presence.js +33 -0
- package/lib/api/endpoints/quoxplan.js +7 -0
- package/lib/api/endpoints/quoxseo.js +12 -0
- package/lib/api/endpoints/router.js +17 -0
- package/lib/api/endpoints/safety.js +7 -0
- package/lib/api/endpoints/server.js +21 -0
- package/lib/api/endpoints/setup.js +6 -0
- package/lib/api/endpoints/skills.js +20 -0
- package/lib/api/endpoints/stepup.js +20 -0
- package/lib/api/endpoints/teams.js +2 -2
- package/lib/api/endpoints/triggers.js +26 -0
- package/lib/api/endpoints/v1.js +120 -0
- package/lib/api/endpoints/ward.js +7 -0
- package/lib/api/endpoints/workflows.js +7 -0
- package/lib/api/endpoints/x.js +8 -0
- package/lib/commands/actions.js +131 -0
- package/lib/commands/admin.js +16 -0
- package/lib/commands/agent.js +170 -27
- package/lib/commands/agentAttach.js +140 -13
- package/lib/commands/approvals.js +78 -1
- package/lib/commands/aside.js +24 -0
- package/lib/commands/ask.js +146 -0
- package/lib/commands/assistant.js +59 -4
- package/lib/commands/auth.js +8 -2
- package/lib/commands/backup.js +8 -4
- package/lib/commands/blog.js +110 -0
- package/lib/commands/brain.js +105 -14
- package/lib/commands/build.js +19 -5
- package/lib/commands/chat.js +180 -26
- package/lib/commands/chatCompanion.js +27 -0
- package/lib/commands/chatState.js +45 -0
- package/lib/commands/claude.js +35 -38
- package/lib/commands/contacts.js +134 -4
- package/lib/commands/contactsSendAt.js +21 -0
- package/lib/commands/conversation.js +35 -4
- package/lib/commands/dashboard.js +43 -0
- package/lib/commands/department.js +28 -7
- package/lib/commands/devResident.js +15 -3
- package/lib/commands/devSupervisor.js +13 -1
- package/lib/commands/discord.js +75 -39
- package/lib/commands/done.js +98 -28
- package/lib/commands/engagement.js +107 -6
- package/lib/commands/evidence.js +154 -0
- package/lib/commands/ext.js +23 -0
- package/lib/commands/fetchAllowlist.js +71 -0
- package/lib/commands/file.js +38 -1
- package/lib/commands/fleet.js +336 -4
- package/lib/commands/governance.js +12 -3
- package/lib/commands/gripe.js +44 -13
- package/lib/commands/hat.js +254 -9
- package/lib/commands/helpers.js +18 -1
- package/lib/commands/hyperion.js +58 -0
- package/lib/commands/image.js +68 -0
- package/lib/commands/inbox.js +74 -23
- package/lib/commands/kuma.js +1050 -0
- package/lib/commands/kumaSla.js +174 -0
- package/lib/commands/licence.js +52 -155
- package/lib/commands/loop.js +291 -0
- package/lib/commands/lucid.js +10 -4
- package/lib/commands/memory.js +85 -8
- package/lib/commands/mind.js +71 -0
- package/lib/commands/mon.js +69 -2
- package/lib/commands/monitoring.js +1 -1
- package/lib/commands/motion.js +2 -2
- package/lib/commands/nucleus.js +48 -17
- package/lib/commands/objective.js +59 -1
- package/lib/commands/offload.js +89 -0
- package/lib/commands/onboard.js +2 -1
- package/lib/commands/org.js +30 -3
- package/lib/commands/peek.js +28 -0
- package/lib/commands/plan.js +38 -0
- package/lib/commands/plugin.js +110 -0
- package/lib/commands/presence.js +89 -0
- package/lib/commands/provider.js +217 -0
- package/lib/commands/qlarity.js +19 -3
- package/lib/commands/quoxchat.js +13 -4
- package/lib/commands/quoxseo.js +69 -0
- package/lib/commands/reach.js +2 -1
- package/lib/commands/research.js +79 -0
- package/lib/commands/router.js +77 -0
- package/lib/commands/rows.js +367 -0
- package/lib/commands/run.js +82 -10
- package/lib/commands/scope.js +2 -2
- package/lib/commands/server.js +405 -0
- package/lib/commands/setup.js +33 -7
- package/lib/commands/shortcut.js +41 -0
- package/lib/commands/skills.js +68 -9
- package/lib/commands/slot.js +260 -7
- package/lib/commands/status.js +1 -1
- package/lib/commands/team.js +3 -1
- package/lib/commands/tips.js +23 -0
- package/lib/commands/tool.js +133 -45
- package/lib/commands/trace.js +12 -7
- package/lib/commands/trigger.js +95 -0
- package/lib/commands/update.js +46 -0
- package/lib/commands/vault.js +31 -8
- package/lib/commands/vm.js +237 -0
- package/lib/commands/voice.js +34 -3
- package/lib/commands/volt.js +45 -3
- package/lib/commands/walkthrough.js +32 -0
- package/lib/commands/watch.js +73 -21
- package/lib/commands/welcome.js +14 -0
- package/lib/commands/workflow.js +189 -32
- package/lib/commands/x.js +13 -4
- package/lib/commands/youtube.js +172 -0
- package/lib/core/actingAs.js +2 -2
- package/lib/core/actionItems.js +115 -0
- package/lib/core/activeSummary.js +186 -0
- package/lib/core/agentAlias.js +2 -0
- package/lib/core/agentFamilies.js +7 -0
- package/lib/core/aside.js +54 -0
- package/lib/core/askDecision.js +60 -0
- package/lib/core/auth.js +164 -13
- package/lib/core/callerSource.js +1 -1
- package/lib/core/cliBuild.js +62 -0
- package/lib/core/client.js +152 -35
- package/lib/core/commandRegistrars.js +130 -0
- package/lib/core/config.js +143 -25
- package/lib/core/customHats.js +168 -0
- package/lib/core/doneEngineFetch.js +8 -0
- package/lib/core/errors.js +329 -20
- package/lib/core/findingReconcile.js +90 -0
- package/lib/core/global-flags.js +105 -5
- package/lib/core/governedSession.js +1 -3
- package/lib/core/hyperion.js +102 -0
- package/lib/core/inboxResolution.js +65 -0
- package/lib/core/lazyCommands.js +4 -1
- package/lib/core/liveSessionsReport.js +1 -1
- package/lib/core/loopGlyph.js +97 -0
- package/lib/core/offload.js +533 -0
- package/lib/core/org-override.js +1 -1
- package/lib/core/orgResolve.js +70 -0
- package/lib/core/output.js +11 -11
- package/lib/core/peek.js +126 -0
- package/lib/core/profileUsers.js +51 -0
- package/lib/core/promptCancel.js +18 -0
- package/lib/core/repoPath.js +11 -0
- package/lib/core/residentSeed.js +3 -1
- 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 +12 -8
- package/lib/core/sessionsOverview.js +64 -0
- package/lib/core/shortcut.js +83 -0
- package/lib/core/slotKeeper.js +248 -3
- package/lib/core/slotWake.js +120 -0
- package/lib/core/supervisorEngine.js +71 -2
- package/lib/core/version.js +1 -1
- package/lib/core/vm.js +366 -0
- package/lib/core/vmTui.js +86 -0
- package/lib/core/walkthrough.js +35 -0
- package/lib/dragon/companionView.js +34 -0
- package/lib/dragon/frames.js +118 -0
- package/lib/dragon/jobs.js +123 -0
- package/lib/dragon/renderWorker.js +15 -0
- package/lib/dragon/select.js +72 -0
- package/lib/evidence/bundleExport.js +47 -0
- package/lib/evidence/bundleVerify.js +77 -0
- package/lib/hyperion/deck.js +29 -0
- package/lib/lucid/LucidCard.jsx +68 -0
- package/lib/lucid/LucidIntro.jsx +45 -58
- package/lib/lucid/cards.js +86 -0
- package/lib/lucid/markdown.js +81 -0
- package/lib/lucid/motion.js +56 -0
- package/lib/quoxon/EXTENSIONS.md +23 -0
- package/lib/quoxon/activity.js +14 -2
- package/lib/quoxon/aocl.js +117 -0
- package/lib/quoxon/api.js +690 -25
- package/lib/quoxon/app.js +3586 -250
- package/lib/quoxon/approvalFacts.js +96 -0
- package/lib/quoxon/approvalSentence.js +44 -0
- package/lib/quoxon/approvalsView.js +384 -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 +13 -6
- package/lib/quoxon/cells.js +53 -0
- package/lib/quoxon/commandShort.js +124 -0
- package/lib/quoxon/commands.js +429 -39
- package/lib/quoxon/connect.js +210 -23
- package/lib/quoxon/connectCard.js +19 -2
- package/lib/quoxon/consent.js +86 -0
- package/lib/quoxon/copy.js +4 -3
- package/lib/quoxon/countdownLine.js +20 -0
- package/lib/quoxon/deck.js +30 -0
- package/lib/quoxon/demo.autoplay.js +29 -0
- package/lib/quoxon/demo.fixtures.js +1 -1
- package/lib/quoxon/demo.js +46 -3
- package/lib/quoxon/devObserver.js +20 -0
- package/lib/quoxon/dividerAocl.js +192 -0
- package/lib/quoxon/dividerRule.js +57 -0
- package/lib/quoxon/dragonCompanion.js +255 -0
- package/lib/quoxon/dragonModes.js +151 -0
- package/lib/quoxon/dragonMoments.js +82 -0
- package/lib/quoxon/dragonReads.js +68 -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 +2 -2
- package/lib/quoxon/events.js +266 -26
- package/lib/quoxon/extensions.js +276 -0
- package/lib/quoxon/feedback.js +50 -0
- package/lib/quoxon/gallery.js +21 -7
- 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/inboxView.js +79 -0
- package/lib/quoxon/indicator.js +6 -2
- package/lib/quoxon/installCli.js +79 -0
- package/lib/quoxon/intent.js +12 -1
- package/lib/quoxon/keys.js +51 -8
- package/lib/quoxon/launcher.js +103 -0
- package/lib/quoxon/loopCard.js +456 -0
- package/lib/quoxon/loopCards.js +180 -0
- package/lib/quoxon/loopStream.js +87 -0
- package/lib/quoxon/main.js +101 -160
- package/lib/quoxon/markdown.js +45 -21
- 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 +132 -26
- package/lib/quoxon/modelPrefs.js +345 -0
- package/lib/quoxon/motion/clock.js +3 -2
- package/lib/quoxon/motion/level.js +91 -0
- package/lib/quoxon/mouse.js +222 -0
- package/lib/quoxon/noai.js +57 -0
- package/lib/quoxon/objectiveOffer.js +82 -0
- package/lib/quoxon/objects/away.js +72 -0
- package/lib/quoxon/objects/fetch.js +17 -5
- package/lib/quoxon/objects/fleet.js +149 -0
- package/lib/quoxon/objects/health.js +186 -0
- package/lib/quoxon/objects/index.js +46 -7
- package/lib/quoxon/objects/mirror.js +137 -0
- package/lib/quoxon/objects/model.js +33 -0
- package/lib/quoxon/objects/peek.js +17 -0
- package/lib/quoxon/objects/proof.js +12 -4
- 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/view.js +11 -0
- package/lib/quoxon/onboarding/copy.js +32 -4
- package/lib/quoxon/onboarding/credential.js +17 -4
- package/lib/quoxon/onboarding/dockerOffer.js +45 -0
- package/lib/quoxon/onboarding/failureCause.js +33 -0
- package/lib/quoxon/onboarding/finish.js +15 -0
- package/lib/quoxon/onboarding/index.js +34 -7
- package/lib/quoxon/onboarding/installRunner.js +64 -5
- package/lib/quoxon/onboarding/preflight.js +17 -3
- package/lib/quoxon/onboarding/probe.js +65 -0
- package/lib/quoxon/onboarding/shortcutOffer.js +28 -0
- package/lib/quoxon/onboarding/view.js +76 -19
- package/lib/quoxon/orgLabel.js +10 -0
- package/lib/quoxon/orgSwitch.js +59 -0
- package/lib/quoxon/ownBroadcast.js +22 -0
- package/lib/quoxon/palette.js +43 -0
- package/lib/quoxon/planSteps.js +51 -0
- package/lib/quoxon/print.js +12 -5
- 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 +20 -51
- package/lib/quoxon/qdragon.js +410 -0
- package/lib/quoxon/qdragonFly.js +62 -0
- package/lib/quoxon/qdragonScenes.js +282 -0
- package/lib/quoxon/qeyes.js +213 -0
- package/lib/quoxon/qmotion.js +75 -37
- package/lib/quoxon/render.js +740 -124
- package/lib/quoxon/resend.js +72 -0
- package/lib/quoxon/roster.js +117 -17
- package/lib/quoxon/route.js +6 -5
- package/lib/quoxon/rows.js +954 -0
- package/lib/quoxon/rowsStore.js +80 -0
- package/lib/quoxon/screenContext.js +150 -0
- package/lib/quoxon/screenFrames.js +254 -0
- package/lib/quoxon/serverChip.js +42 -0
- package/lib/quoxon/session.js +182 -2
- package/lib/quoxon/sessionDead.js +17 -0
- package/lib/quoxon/signin.js +125 -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 +94 -0
- package/lib/quoxon/slash/cliLeaves.json +1023 -0
- package/lib/quoxon/slash/compact.js +1 -1
- 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/evidenceCmd.js +34 -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 +13 -8
- package/lib/quoxon/slash/listView.js +3 -1
- package/lib/quoxon/slash/modelCmd.js +261 -0
- package/lib/quoxon/slash/palette.js +22 -8
- package/lib/quoxon/slash/paletteController.js +117 -7
- package/lib/quoxon/slash/panels.js +21 -6
- package/lib/quoxon/slash/picker.js +20 -2
- package/lib/quoxon/slash/qpeCmd.js +35 -0
- package/lib/quoxon/slash/restart.js +95 -13
- package/lib/quoxon/slash/resume.js +18 -1
- package/lib/quoxon/slash/rowCmd.js +157 -0
- package/lib/quoxon/slash/rowWalk.js +175 -0
- package/lib/quoxon/slash/settings.js +16 -6
- 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/todoCmd.js +122 -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/mapping.js +2 -1
- package/lib/quoxon/stray.js +22 -0
- package/lib/quoxon/suggestCommand.js +90 -0
- package/lib/quoxon/term.js +46 -11
- package/lib/quoxon/textBuffer.js +176 -2
- package/lib/quoxon/theme.js +58 -25
- package/lib/quoxon/tips.js +433 -0
- package/lib/quoxon/toolCards.js +30 -0
- package/lib/quoxon/toolError.js +41 -0
- package/lib/quoxon/transcript.js +42 -4
- package/lib/quoxon/web/qface.js +18 -10
- package/lib/quoxon/welcome.js +1 -1
- package/lib/quoxon/whatsnew.js +136 -0
- package/lib/quoxon/whatsnew.jsonl +134 -0
- package/lib/quoxon/windowOrg.js +123 -0
- package/lib/server/devServers.js +137 -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/views/HyperionDeck.jsx +79 -0
- package/lib/tui/views/LucidDeck.jsx +209 -217
- package/package.json +5 -1
- package/vendor/dragonlab/SOURCE +4 -0
- package/vendor/dragonlab/design/cipher/v1/cipher-master.svg +46 -0
- package/vendor/dragonlab/design/cipher/v2/character.mjs +49 -0
- package/vendor/dragonlab/design/cipher/v2/review.mjs +33 -0
- package/vendor/dragonlab/design/cipher/v2/watch.mjs +56 -0
- package/vendor/dragonlab/design/drago/v1/character.mjs +19 -0
- package/vendor/dragonlab/design/drago/v1/drago-master.svg +56 -0
- package/vendor/dragonlab/design/drago/v2/character.mjs +87 -0
- package/vendor/dragonlab/design/drago/v4/character.mjs +67 -0
- package/vendor/dragonlab/design/drago/v4/profile.mjs +35 -0
- package/vendor/dragonlab/design/drago/v4/review.mjs +33 -0
- package/vendor/dragonlab/design/drago/v4/watch.mjs +56 -0
- package/vendor/dragonlab/design/hyperion/v1/hyperion-master.svg +56 -0
- package/vendor/dragonlab/design/hyperion/v2/character.mjs +79 -0
- package/vendor/dragonlab/design/hyperion/v2/review.mjs +33 -0
- package/vendor/dragonlab/design/hyperion/v2/watch.mjs +56 -0
- package/vendor/dragonlab/design/lucid/recovery/arrival.mjs +22 -0
- package/vendor/dragonlab/design/lucid/recovery/character.mjs +106 -0
- package/vendor/dragonlab/design/lucid/recovery/layout.mjs +50 -0
- package/vendor/dragonlab/design/lucid/recovery/lucid-master.svg +78 -0
- package/vendor/dragonlab/design/lucid/recovery/motion.mjs +42 -0
- package/vendor/dragonlab/design/lucid/recovery/render.mjs +121 -0
- package/vendor/dragonlab/design/lucid/recovery/review.mjs +68 -0
- package/vendor/dragonlab/design/lucid/recovery/verify.mjs +84 -0
- package/vendor/dragonlab/design/lucid/recovery/watch.mjs +56 -0
- package/vendor/dragonlab/design/seer/v1/seer-master.svg +53 -0
- package/vendor/dragonlab/design/seer/v2/character.mjs +88 -0
- package/vendor/dragonlab/design/seer/v3/character.mjs +33 -0
- package/vendor/dragonlab/design/seer/v4/character.mjs +66 -0
- package/vendor/dragonlab/design/seer/v4/review.mjs +33 -0
- package/vendor/dragonlab/design/seer/v4/watch.mjs +56 -0
- package/vendor/dragonlab/design/stage3/companion-review.mjs +28 -0
- package/vendor/dragonlab/design/stage3/sentinel-art.js +39 -0
- package/vendor/dragonlab/design/stage3/sentinel-braille.js +216 -0
- package/vendor/dragonlab/design/stage3/sentinel-character.js +257 -0
- package/vendor/dragonlab/design/stage3/sentinel-icon.js +103 -0
- package/vendor/dragonlab/design/stage3/sentinel-warden-eyes.js +95 -0
- package/vendor/dragonlab/design/stage3/svg-coverage.js +148 -0
- package/vendor/dragonlab/factory/svg-rig.mjs +63 -0
- package/vendor/dragonlab/integration/companions-v1/characters.mjs +88 -0
- package/vendor/dragonlab/integration/companions-v1/cipher.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/companion.mjs +63 -0
- package/vendor/dragonlab/integration/companions-v1/drago.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/hyperion.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/lucid.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/seer.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/sentinel.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/view.mjs +28 -0
- package/vendor/dragonlab/integration/hyperion-v3/activity.mjs +76 -0
- package/vendor/dragonlab/lib/power-effects.js +294 -0
- package/vendor/dragonlab/lib/power-registry.js +74 -0
- package/vendor/dragonlab/vendor/quox-dragon/qart.js +348 -0
- package/vendor/dragonlab/vendor/quox-dragon/qeyes.js +213 -0
- package/lib/core/websiteSession.js +0 -148
|
@@ -0,0 +1,96 @@
|
|
|
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
|
+
/** Server text can arrive HTML-escaped ("2>/dev/null"); decode for display. Never renders HTML. */
|
|
8
|
+
const dec = (t) => (typeof t === 'string' ? t.replace(/&(amp|lt|gt|quot|apos|#39|#x27);/gi, (_, e) => ({ amp: '&', lt: '<', gt: '>', quot: '"', apos: "'", '#39': "'", '#x27': "'" })[e.toLowerCase()]) : t);
|
|
9
|
+
const str = (v) => (typeof v === 'string' ? v : v == null ? '' : (() => { try { return JSON.stringify(v); } catch { return String(v); } })());
|
|
10
|
+
|
|
11
|
+
/** Text cut at `max` with a visible, honest tail. `n` is the /approvals number when known. */
|
|
12
|
+
export function clipVisible(text, max, n = null) {
|
|
13
|
+
const s = String(text ?? '');
|
|
14
|
+
if (s.length <= max) return s;
|
|
15
|
+
return `${s.slice(0, max)} (+${s.length - max} chars, /approvals ${n ?? 'N'} detail for full)`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** The exact command or tool input, verbatim: method.command, else the reach request, else args. */
|
|
19
|
+
export function commandOf(it) {
|
|
20
|
+
const p = it?.payload || {}; const c = p.approval_context || {}; const m = c.method || {};
|
|
21
|
+
if (typeof m.command === 'string' && m.command) return { text: dec(m.command), kind: 'command' };
|
|
22
|
+
const rq = (p.reach?.requests || []).find(r => r && r.kind === 'command' && typeof r.id === 'string' && r.id);
|
|
23
|
+
if (rq) return { text: dec(rq.id), kind: 'command' };
|
|
24
|
+
const a = m.args ?? c.args ?? p.args ?? null;
|
|
25
|
+
if (a != null && a !== '') return { text: dec(str(a)), kind: 'input' };
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Hosts (or other comma-separated targets) with the count. The full target string wins over the clipped what.target. */
|
|
30
|
+
export function hostsOf(it) {
|
|
31
|
+
const p = it?.payload || {}; const c = p.approval_context || {};
|
|
32
|
+
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 || '';
|
|
33
|
+
const list = String(raw).split(',').map(x => x.trim()).filter(Boolean);
|
|
34
|
+
return { list, count: list.length };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** "17 hosts: a, b, c (+14 more, /approvals N detail for all)". `all` lists every name. */
|
|
38
|
+
export function whereText(it, { all = false, first = 3, n = null } = {}) {
|
|
39
|
+
const { list, count } = hostsOf(it);
|
|
40
|
+
if (!count) return null;
|
|
41
|
+
if (count === 1) return list[0];
|
|
42
|
+
const tool = String(it?.payload?.toolName || it?.payload?.approval_context?.method?.tool || '');
|
|
43
|
+
const w = /ssh|exec|host|shell/i.test(tool) ? 'hosts' : 'targets';
|
|
44
|
+
if (all) return `${count} ${w}: ${list.join(', ')}`;
|
|
45
|
+
const shown = list.slice(0, first).join(', ');
|
|
46
|
+
return count > first ? `${count} ${w}: ${shown} (+${count - first} more, /approvals ${n ?? 'N'} detail for all)` : `${count} ${w}: ${shown}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Read-only or write, only as the server classified it. Null when it did not say. */
|
|
50
|
+
export function accessOf(it) {
|
|
51
|
+
const c = it?.payload?.approval_context || {};
|
|
52
|
+
const k = c.method?.access || c.access;
|
|
53
|
+
if (k === 'read_only' || k === 'read') return 'read-only';
|
|
54
|
+
if (k === 'write') return 'write';
|
|
55
|
+
const lab = String(c.why_approval?.label || '');
|
|
56
|
+
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';
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** WHO: "QUOX → SENTINEL (session claude in app) for Adam DEV SERVER". */
|
|
61
|
+
export function whoText(it) {
|
|
62
|
+
const c = it?.payload?.approval_context || {}; const rb = c.requested_by || {}; const ob = c.on_behalf_of || {};
|
|
63
|
+
const chain = Array.isArray(rb.delegation_chain) && rb.delegation_chain.length ? rb.delegation_chain.join(' → ') : (rb.agent_label || c.source_label || null);
|
|
64
|
+
const caller = rb.caller?.name || rb.session_label || null;
|
|
65
|
+
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(', ');
|
|
66
|
+
const forWhom = ob.user_name || ob.user_id || null;
|
|
67
|
+
const head = [chain, via && `(${via})`].filter(Boolean).join(' ');
|
|
68
|
+
if (!head && !forWhom) return null;
|
|
69
|
+
return `${head || 'unknown requester'}${forWhom ? ` acting for ${forWhom}` : ''}`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** WHY: the reason text (marked inferred when the caller did not state it). */
|
|
73
|
+
export function whyText(it) {
|
|
74
|
+
const c = it?.payload?.approval_context || {}; const r = c.reason;
|
|
75
|
+
if (r && typeof r === 'object') return r.text ? `${dec(r.text)}${r.source === 'inferred' ? ' (inferred, not stated by the caller)' : ''}` : null;
|
|
76
|
+
return dec(r || it?.payload?.reason || null);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** The user's own words that started this, when the server kept them. */
|
|
80
|
+
export const excerptOf = (it) => dec(it?.payload?.approval_context?.on_behalf_of?.request_excerpt) || null;
|
|
81
|
+
|
|
82
|
+
/** One bag of the facts, for the card. */
|
|
83
|
+
export function approvalFacts(it, { n = null } = {}) {
|
|
84
|
+
const cmd = commandOf(it);
|
|
85
|
+
const when = it?.payload?.approval_context?.created_at || it?.created_at || it?.createdAt || null;
|
|
86
|
+
return {
|
|
87
|
+
command: cmd ? { ...cmd, shown: clipVisible(cmd.text, CMD_CARD_MAX, n) } : null,
|
|
88
|
+
where: whereText(it, { n }),
|
|
89
|
+
hostList: hostsOf(it).list,
|
|
90
|
+
who: whoText(it),
|
|
91
|
+
why: whyText(it),
|
|
92
|
+
asked: excerptOf(it),
|
|
93
|
+
when: when ? String(when).replace('T', ' ').slice(0, 16) + ' UTC' : null,
|
|
94
|
+
access: accessOf(it)
|
|
95
|
+
};
|
|
96
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// TUI-APPROVE: "You approved WHO to do WHAT on WHERE", built only from fields the card already shows.
|
|
2
|
+
// A field that is unknown is left out, never printed as "unknown".
|
|
3
|
+
|
|
4
|
+
const PLAIN = { // tool -> [infinitive, gerund]
|
|
5
|
+
ssh_exec: ['run a shell command', 'running a shell command'],
|
|
6
|
+
host_exec: ['run a shell command', 'running a shell command'],
|
|
7
|
+
host_restart: ['restart a service', 'restarting a service']
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/** "VULCAN-SEC via FORGE (CLI) acting for Adam" -> "VULCAN-SEC". */
|
|
11
|
+
export function shortWho(raw) {
|
|
12
|
+
let w = String(raw || '').replace(/\s+acting for .*$/i, '').trim();
|
|
13
|
+
if (!w || /^unknown requester/i.test(w)) return '';
|
|
14
|
+
const sess = w.match(/\(via\s+session\s+([^,)]+)/i); // "FORGE (via session VULCAN-SEC, CLI)": the session is who asked
|
|
15
|
+
if (sess && sess[1].trim()) return sess[1].trim();
|
|
16
|
+
w = w.split('→').pop().split(/\s+via\s+/i)[0].replace(/\s*\(.*$/, '').trim();
|
|
17
|
+
return w;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const clean = (s) => String(s || '').replace(/\s+/g, ' ').replace(/[.\s]+$/, '').trim();
|
|
21
|
+
|
|
22
|
+
function whatOf(a) {
|
|
23
|
+
const plain = PLAIN[a.tool];
|
|
24
|
+
if (plain) return plain;
|
|
25
|
+
const w = clean(a.what && a.what !== a.tool ? a.what : '');
|
|
26
|
+
if (w) return [w, w];
|
|
27
|
+
const t = clean(String(a.tool || '').replace(/_/g, ' '));
|
|
28
|
+
return t ? [`use ${t}`, `using ${t}`] : ['', ''];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The one- or two-line confirmation for an answered card. */
|
|
32
|
+
export function approvalSentence(a, approved) {
|
|
33
|
+
const who = shortWho(a.who);
|
|
34
|
+
const [inf, ger] = whatOf(a);
|
|
35
|
+
const where = clean(a.where || a.target);
|
|
36
|
+
const said = (t) => where && new RegExp(`\\bon ${where.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*$`, 'i').test(t); // WHAT already ends "on <where>"
|
|
37
|
+
const at = where && !(said(inf) || said(ger) || new RegExp(`\\b${where.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'i').test(inf)) ? ` on ${where}` : '';
|
|
38
|
+
if (approved) {
|
|
39
|
+
const tail = a.foreign ? ' It runs now; its result isn\'t tracked here.' : '';
|
|
40
|
+
const head = who ? `${who}${inf ? ` to ${inf}` : ''}` : (ger || 'the request');
|
|
41
|
+
return `\u2714 You approved ${head}${at}.${tail}`;
|
|
42
|
+
}
|
|
43
|
+
return `\u2716 You denied ${who ? `${who} ` : ''}${ger || 'the request'}${at}.`;
|
|
44
|
+
}
|
|
@@ -0,0 +1,384 @@
|
|
|
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
|
+
/** What the item says it will do, in its own words: the server's what.summary or the post text, whichever is not just the generic "tool target". Null when there is nothing more specific. */
|
|
114
|
+
export function distinctWhat(it) {
|
|
115
|
+
const p = it?.payload || {}; const w = p.what && typeof p.what === 'object' ? p.what : {};
|
|
116
|
+
const generic = [w.action, w.target].filter(Boolean).join(' ').toLowerCase().replace(/\s+/g, ' ').trim();
|
|
117
|
+
const one = (t) => String(t ?? '').replace(/\s+/g, ' ').trim();
|
|
118
|
+
for (const c of [w.summary, p.action_params?.text, p.action_params?.body, p.action_params?.message]) {
|
|
119
|
+
const t = one(c); if (t && t.toLowerCase() !== generic) return t;
|
|
120
|
+
}
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
/** The content being approved, in full: summary, post text, images, body_md. */
|
|
124
|
+
export function contentRows(it) {
|
|
125
|
+
const p = it?.payload || {}; const w = p.what && typeof p.what === 'object' ? p.what : {}; const ap = p.action_params || {};
|
|
126
|
+
const imgs = [].concat(ap.image_paths || ap.images || []).filter(Boolean).map(String);
|
|
127
|
+
const text = ap.text ?? ap.body ?? ap.message ?? null;
|
|
128
|
+
const body = typeof p.body_md === 'string' ? p.body_md : null;
|
|
129
|
+
return [
|
|
130
|
+
['summary', w.summary ? String(w.summary) : null],
|
|
131
|
+
['content', text != null && String(text).trim() ? String(text).trim() : null],
|
|
132
|
+
['images', imgs.length ? imgs.join(', ') : null],
|
|
133
|
+
['details', body && body.trim() && body.trim() !== String(text ?? '').trim() ? body.trim() : null]
|
|
134
|
+
].filter(r => r[1]);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** A plain approval (not a quox ask) shaped as a y/n card so the walkthrough can show it. The original id stays, so /approve N routes by the same list. */
|
|
138
|
+
export function approvalAsWalkItem(it) {
|
|
139
|
+
if (isDecision(it)) return it;
|
|
140
|
+
const p = it?.payload || {}; const c = p.approval_context || {};
|
|
141
|
+
const dw = distinctWhat(it); const d = describeInboxItem(it);
|
|
142
|
+
const body = contentRows(it).filter(r => r[0] !== 'summary').map(r => `${r[0]}: ${r[1]}`).join('\n');
|
|
143
|
+
const why = [whyText(it) || c.why || p.reason || '', body].filter(Boolean).join('\n');
|
|
144
|
+
return { ...it, __approval: true, title: dw || d.title || it.title || it.id,
|
|
145
|
+
payload: { ...p, type: 'decision', session: requesterNames(it)[0] || approvalSourceLabel(it), why, high_risk: c.risk === 'high' || p.high_risk === true,
|
|
146
|
+
options: [{ key: 'y', label: 'Approve' }, { key: 'n', label: 'Deny' }] } };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** One line: [N] age who what (risk). Clipped to `cols`. */
|
|
150
|
+
export function approvalRow(it, n, { cols = 100, now = Date.now() } = {}) {
|
|
151
|
+
if (isDecision(it)) return decisionRow(it, n, { cols, now });
|
|
152
|
+
const d = describeInboxItem(it);
|
|
153
|
+
const who = requesterNames(it)[0] || approvalSourceLabel(it);
|
|
154
|
+
let what = [d.tool, d.target && d.target !== d.tool ? d.target : null].filter(Boolean).join(' ') || d.title || it.id;
|
|
155
|
+
const dw = distinctWhat(it); if (dw && commandOf(it)?.kind !== 'command') what = dw; // two posts to the same account must not read the same
|
|
156
|
+
const risk = ctxOf(it).risk ? ` (${ctxOf(it).risk})` : '';
|
|
157
|
+
const priv = isPrivileged({ tool: d.tool, risk: ctxOf(it).risk, flag: it?.payload?.privileged === true || ctxOf(it).privileged === true, title: it.title }) ? ' [privileged]' : '';
|
|
158
|
+
const cmd = commandOf(it);
|
|
159
|
+
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
|
|
160
|
+
const { count, list } = hostsOf(it);
|
|
161
|
+
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]}` : '');
|
|
162
|
+
const one = cmd.text.replace(/\s*\n\s*/g, ' ; ');
|
|
163
|
+
const head = ` [${n}] ${ageText(d.at, now).padStart(3)} ${String(who).slice(0, 18).padEnd(18)} ${d.tool || 'run'} `;
|
|
164
|
+
const room = Math.max(24, cols - head.length - on.length - risk.length - priv.length - 2);
|
|
165
|
+
return `${head}\`${clipVisible(one, room, n)}\`${on}${risk}${priv}`;
|
|
166
|
+
}
|
|
167
|
+
const line = ` [${n}] ${ageText(d.at, now).padStart(3)} ${String(who).slice(0, 18).padEnd(18)} ${what}${risk}${priv}`;
|
|
168
|
+
return line.length > cols ? line.slice(0, Math.max(10, cols - 1)) + '…' : line;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Full detail for /approvals N: one line per fact. */
|
|
172
|
+
export function approvalDetail(it, n, { now = Date.now() } = {}) {
|
|
173
|
+
if (isDecision(it)) return decisionDetail(it, n, { now });
|
|
174
|
+
const d = describeInboxItem(it); const c = ctxOf(it); const m = c.method || {}; const p = it?.payload || {};
|
|
175
|
+
const args = m.argv ?? m.args ?? c.argv ?? c.args ?? p.args ?? null;
|
|
176
|
+
const cmdFull = commandOf(it);
|
|
177
|
+
const why = whyText(it) || c.why || p.reason || it?.summary || it?.description || null;
|
|
178
|
+
const rc = it?.receipt || p.receipt || c.receipt || null;
|
|
179
|
+
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);
|
|
180
|
+
const rows = [
|
|
181
|
+
['item', `[${n}] ${it.title || it.id}`],
|
|
182
|
+
['id', it.id],
|
|
183
|
+
['tool', d.tool], ['target', hostsOf(it).count > 1 ? null : d.target],
|
|
184
|
+
['command', cmdFull && cmdFull.kind === 'command' ? cmdFull.text : null],
|
|
185
|
+
['args', cmdFull && cmdFull.kind === 'command' ? null : (args == null ? null : (typeof args === 'string' ? args : JSON.stringify(args)).slice(0, 300))],
|
|
186
|
+
['where', hostsOf(it).count ? whereText(it, { all: true }) : null],
|
|
187
|
+
['who', whoText(it)],
|
|
188
|
+
['why', why ? String(why) : null],
|
|
189
|
+
['request', excerptOf(it) ? `"${excerptOf(it)}"` : null],
|
|
190
|
+
...contentRows(it).filter(r => !(r[0] === 'summary' && String(r[1]) === String(why ?? ''))),
|
|
191
|
+
['class', accessOf(it)],
|
|
192
|
+
['from', requesterNames(it).join(', ') || approvalSourceLabel(it)],
|
|
193
|
+
['asked', d.at ? `${ageText(d.at, now)} ago (${String(d.at)})` : null],
|
|
194
|
+
['risk', c.risk || null],
|
|
195
|
+
['receipt', receipt],
|
|
196
|
+
['url', it.dashboard_url || null]
|
|
197
|
+
].filter(r => r[1]);
|
|
198
|
+
return rows.flatMap(([k, v]) => String(v).split('\n').map((line, i) => ` ${(i ? '' : k).padEnd(8)}${line}`));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* /approve|/deny arguments after the command word. Forms: N, 1,2, 1-3, all, agent <name>; deny may end with a free-text
|
|
203
|
+
* reason. 'confirm' (approve only) is stripped. Returns { nums, all, agent, reason, confirm, bad }.
|
|
204
|
+
*/
|
|
205
|
+
export const NOTE_MAX = 4000;
|
|
206
|
+
/** The raw text after the first `skip` whitespace-separated tokens, whitespace and line breaks kept. */
|
|
207
|
+
export function rawTail(raw, skip) {
|
|
208
|
+
const m = new RegExp(`^\\s*(?:\\S+\\s+){${Math.max(0, skip - 1)}}\\S+\\s*`).exec(String(raw ?? ''));
|
|
209
|
+
return skip > 0 ? (m ? String(raw).slice(m[0].length) : '') : String(raw ?? '');
|
|
210
|
+
}
|
|
211
|
+
/** 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. */
|
|
212
|
+
export function cleanNote(t) {
|
|
213
|
+
let s = String(t ?? '').trim();
|
|
214
|
+
const q = s[0];
|
|
215
|
+
if (q === '"' || q === "'" || q === '\u201c') {
|
|
216
|
+
const close = q === '\u201c' ? '\u201d' : q;
|
|
217
|
+
s = s.slice(1);
|
|
218
|
+
if (s.endsWith(close)) s = s.slice(0, -1);
|
|
219
|
+
}
|
|
220
|
+
return s.trim().slice(0, NOTE_MAX);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export function parseDecision(words, verb, { raw = null } = {}) {
|
|
224
|
+
const out = { nums: [], all: false, agent: null, reason: '', confirm: false, bad: false };
|
|
225
|
+
let w = words.slice();
|
|
226
|
+
if (verb === 'approve') { out.confirm = w.includes('confirm'); w = w.filter(x => x !== 'confirm'); }
|
|
227
|
+
let i = 0;
|
|
228
|
+
if (w[0] && w[0].toLowerCase() === 'agent') {
|
|
229
|
+
if (!w[1]) { out.bad = true; return out; }
|
|
230
|
+
out.agent = w[1]; i = 2;
|
|
231
|
+
} else {
|
|
232
|
+
for (; i < w.length; i++) {
|
|
233
|
+
const tok = w[i];
|
|
234
|
+
if (tok.toLowerCase() === 'all') { out.all = true; continue; }
|
|
235
|
+
if (!/^[\d,\-]+$/.test(tok)) break;
|
|
236
|
+
let ok = false;
|
|
237
|
+
for (const part of tok.split(',').filter(Boolean)) {
|
|
238
|
+
const m = /^(\d+)(?:-(\d+))?$/.exec(part);
|
|
239
|
+
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); }
|
|
240
|
+
else { out.bad = true; return out; }
|
|
241
|
+
}
|
|
242
|
+
if (!ok) { out.bad = true; return out; }
|
|
243
|
+
if (out.all) break;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
const restWords = w.slice(i);
|
|
247
|
+
const rest = restWords.join(' ').trim();
|
|
248
|
+
if (rest) {
|
|
249
|
+
if (verb === 'reject') out.reason = raw != null ? cleanNote(rawTail(raw, words.length - restWords.length)) : rest.slice(0, NOTE_MAX);
|
|
250
|
+
else out.bad = true;
|
|
251
|
+
}
|
|
252
|
+
if (!out.all && !out.agent && !out.nums.length) out.bad = true;
|
|
253
|
+
return out;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* WALKTHROUGH (owner 2026-10-17: "guide me simply one at a time, explain, advise"). One decision as one plain card.
|
|
258
|
+
* Identity is the decision id, never a position. -> { lines, choices:[{key,label,word}], recommendKey, source }.
|
|
259
|
+
*/
|
|
260
|
+
const isLabelled = (o) => o.label && String(o.label).toLowerCase() !== String(o.key).toLowerCase();
|
|
261
|
+
const optWord = (o) => (isLabelled(o) ? o.label : `option ${o.key} (no label given)`);
|
|
262
|
+
/** "Type 1: <label> · Type 2: <label>" plus s to skip, for a non-yes/no decision. */
|
|
263
|
+
export function walkChoicesLine(it) {
|
|
264
|
+
return it.payload.options.map(o => `Type ${o.key}: ${optWord(o)}`).join(' · ') + ' · Type s to skip';
|
|
265
|
+
}
|
|
266
|
+
export function walkNotYesNo(it) {
|
|
267
|
+
return `This one isn't yes/no. ${walkChoicesLine(it).replace(/ · Type s to skip$/, '')}, or s to skip.`;
|
|
268
|
+
}
|
|
269
|
+
export function walkNoCatch(it) {
|
|
270
|
+
const ks = it.payload.options.map(o => o.key);
|
|
271
|
+
const keys = ks.length > 1 ? `${ks.slice(0, -1).join(', ')} or ${ks[ks.length - 1]}` : ks.join('');
|
|
272
|
+
return `I did not catch a choice. Type ${keys}. ${walkChoicesLine(it)} (? asks about it, Esc stops).`;
|
|
273
|
+
}
|
|
274
|
+
/** A title without the asker's own name in front of it ("SEER: Publish?" asked by SEER -> "Publish?"), whitespace and line breaks folded. */
|
|
275
|
+
export function cleanTitle(it) {
|
|
276
|
+
const p = it.payload || {}; const t = decodeEntities(String(it.title || it.id || '')).replace(/\s+/g, ' ').trim();
|
|
277
|
+
for (const name of [p.session, p.agent].filter(Boolean).map(String)) {
|
|
278
|
+
if (t.toLowerCase().startsWith(name.toLowerCase() + ':')) { const rest = t.slice(name.length + 1).trim(); if (rest) return rest; }
|
|
279
|
+
}
|
|
280
|
+
return t;
|
|
281
|
+
}
|
|
282
|
+
/** 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. */
|
|
283
|
+
export function hang(label, text, cols) {
|
|
284
|
+
const lead = ' ' + label.padEnd(8); const pad = ' '.repeat(lead.length);
|
|
285
|
+
if (!cols) return [lead + text];
|
|
286
|
+
const room = Math.max(20, cols - lead.length - 1); const out = []; let cur = '';
|
|
287
|
+
for (const w of String(text).split(' ')) {
|
|
288
|
+
if (cur && cur.length + 1 + w.length > room) { out.push(cur); cur = w; } else cur = cur ? cur + ' ' + w : w;
|
|
289
|
+
}
|
|
290
|
+
if (cur || !out.length) out.push(cur);
|
|
291
|
+
return out.map((l, k) => (k ? pad : lead) + l);
|
|
292
|
+
}
|
|
293
|
+
/** Like hang, but keeps the text's own line breaks: each line (a "- " list item too) starts a fresh line under the label, wrapped items indented past the dash. */
|
|
294
|
+
export function hangBlock(label, text, cols) {
|
|
295
|
+
const segs = String(text ?? '').split('\n').map(l => l.replace(/\s+/g, ' ').trim()).filter(Boolean);
|
|
296
|
+
if (segs.length <= 1) return hang(label, segs[0] || '', cols);
|
|
297
|
+
const lead = ' ' + label.padEnd(8); const pad = ' '.repeat(lead.length);
|
|
298
|
+
const room = Math.max(20, (cols || 0) - lead.length - 1); const out = [];
|
|
299
|
+
for (const seg of segs) {
|
|
300
|
+
if (!cols) { out.push(seg); continue; }
|
|
301
|
+
const ind = /^[-*] /.test(seg) ? ' ' : '';
|
|
302
|
+
let cur = '';
|
|
303
|
+
for (const w of seg.split(' ')) {
|
|
304
|
+
if (cur && cur.length + 1 + w.length > room) { out.push(cur); cur = ind + w; } else cur = cur ? cur + ' ' + w : w;
|
|
305
|
+
}
|
|
306
|
+
if (cur) out.push(cur);
|
|
307
|
+
}
|
|
308
|
+
return out.map((l, k) => (k ? pad : lead) + l);
|
|
309
|
+
}
|
|
310
|
+
/** Slot and server text can arrive HTML-escaped ("->"); decode for display. Never renders HTML. */
|
|
311
|
+
export function decodeEntities(t) {
|
|
312
|
+
return String(t ?? '').replace(/&(amp|lt|gt|quot|apos|#39|#x27);/gi, (_, e) => ({ amp: '&', lt: '<', gt: '>', quot: '"', apos: "'", '#39': "'", '#x27': "'" })[e.toLowerCase()]);
|
|
313
|
+
}
|
|
314
|
+
/** 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. */
|
|
315
|
+
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;
|
|
316
|
+
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(' ')); };
|
|
317
|
+
export const NO_REASON = 'No reason given: ask the session, or say no.';
|
|
318
|
+
export const NO_JUSTIFICATION = 'No plain justification given: ask the session, or say no.';
|
|
319
|
+
const justificationOf = (p) => String(p.justification || p.plain_justification || '').replace(/\s+/g, ' ').trim();
|
|
320
|
+
export function walkCard(it, i, total, { cols = 0 } = {}) {
|
|
321
|
+
const p = it.payload; const who = p.session || p.agent || 'A session';
|
|
322
|
+
const loose = isLoosening(it); const just = justificationOf(p); // JUSTIFY shows whenever the asker gave one, loosening title or not
|
|
323
|
+
const bareLoose = loose && !just; // no clear justification: "no" is the safe default and nothing is starred
|
|
324
|
+
const yn = p.options.some(o => o.key === 'y') && p.options.some(o => o.key === 'n');
|
|
325
|
+
const rec = bareLoose ? null : (p.options.find(o => o.key === p.recommend) || null);
|
|
326
|
+
const dflt = bareLoose ? (p.options.find(o => o.key === 'n') || null) : (p.options.find(o => o.key === p.default) || null);
|
|
327
|
+
const why = decodeEntities(String(p.why || '')).split('\n').map(l => l.replace(/\s+/g, ' ').trim()).filter(Boolean).join('\n');
|
|
328
|
+
const what = yn
|
|
329
|
+
? `Yes lets ${who} go ahead with this; no stops it.${p.high_risk ? ' High risk: a yes needs your PIN.' : ''}`
|
|
330
|
+
: `${who} needs you to choose how to settle this.${p.high_risk ? ' High risk: a yes needs your PIN.' : ''}`;
|
|
331
|
+
const advice = bareLoose
|
|
332
|
+
? `${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}).` : ''}`
|
|
333
|
+
: rec
|
|
334
|
+
? `I would ${yn ? (rec.key === 'y' ? 'say yes' : 'say no') : `go with ${optWord(rec)} (${rec.key})`}. The asker recommended it.`
|
|
335
|
+
: `${who} gave no recommendation, and I will not guess one.${dflt ? ` If nobody answers it would take ${optWord(dflt)} (${dflt.key}).` : ''}`;
|
|
336
|
+
const keyClash = (k) => p.options.some(o => o.key === k);
|
|
337
|
+
const choices = [
|
|
338
|
+
...p.options.map(o => ({ key: o.key, label: optWord(o), word: yn ? (o.key === 'y' ? 'Yes' : o.key === 'n' ? 'No' : optWord(o)) : optWord(o) })),
|
|
339
|
+
{ key: keyClash('s') ? 'skip' : 's', label: 'Skip for now', defer: true },
|
|
340
|
+
{ key: keyClash('?') ? 'ask' : '?', label: 'Ask about it', ask: true },
|
|
341
|
+
];
|
|
342
|
+
const lines = [
|
|
343
|
+
`${p.__approval ? 'Approval' : 'Decision'} ${i} of ${total} · ${who} asks`,
|
|
344
|
+
...hang('', cleanTitle(it), cols).map(l => l.slice(8)),
|
|
345
|
+
...hang('WHAT', what, cols),
|
|
346
|
+
...(why ? hangBlock('WHY', why, cols) : []),
|
|
347
|
+
...(loose || just ? hang('JUSTIFY', just || NO_JUSTIFICATION, cols) : []),
|
|
348
|
+
...(!why && !just && !loose ? hang('WHY', NO_REASON, cols) : []),
|
|
349
|
+
...hang('ADVICE', advice, cols),
|
|
350
|
+
];
|
|
351
|
+
// 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.
|
|
352
|
+
let recoLine = null;
|
|
353
|
+
if (rec) { recoLine = lines.length; lines.push(` ★ [${rec.key}] ${optWord(rec)} ★ recommended by ${recommenderOf(p)}`); }
|
|
354
|
+
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
|
|
355
|
+
return { lines, choices, recommendKey: rec ? rec.key : null, recoLine, displayKeys: displayOptions(p).map(o => o.key), source: rec ? 'asker' : null };
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/** 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. */
|
|
359
|
+
export function walkReply(it, text) {
|
|
360
|
+
const p = it.payload; const raw = String(text ?? '').trim();
|
|
361
|
+
if (!raw) return null;
|
|
362
|
+
const yn = p.options.some(o => o.key === 'y') && p.options.some(o => o.key === 'n');
|
|
363
|
+
const notYesNo = !yn;
|
|
364
|
+
const first = raw.split(/\s+/)[0]; const f = first.toLowerCase().replace(/[.!,]+$/, '');
|
|
365
|
+
const note = () => cleanNote(rawTail(raw, 1));
|
|
366
|
+
const hasKey = (k) => p.options.some(o => o.key.toLowerCase() === k);
|
|
367
|
+
if (hasKey(f)) return { key: p.options.find(o => o.key.toLowerCase() === f).key, note: note() };
|
|
368
|
+
if (/^(skip|s|later|next)$/.test(f)) return { defer: true };
|
|
369
|
+
if (f === '?' || /^(ask|why|explain|help)$/.test(f)) return { ask: true };
|
|
370
|
+
if (/^(yes|yep|yeah|ok|okay)$/.test(f)) return yn ? { key: 'y', note: note() } : (notYesNo ? { notYesNo: true } : null);
|
|
371
|
+
if (/^(no|nope)$/.test(f)) return yn ? { key: 'n', note: note() } : { notYesNo: true };
|
|
372
|
+
const t = raw.toLowerCase().replace(/[.!]+$/, '');
|
|
373
|
+
const hit = p.options.find(o => isLabelled(o) && t.startsWith(String(o.label).toLowerCase()));
|
|
374
|
+
return hit ? { key: hit.key, note: cleanNote(raw.slice(String(hit.label).length)) } : null;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/** The end summary of a walkthrough. state = { answered:[{title,word}], skipped:[title], left:[title] } */
|
|
378
|
+
export function walkSummary(st) {
|
|
379
|
+
const out = [`Walkthrough done: ${st.answered.length} answered, ${st.skipped.length} skipped, ${st.left.length} left.`];
|
|
380
|
+
for (const a of st.answered) out.push(` ✔ ${a.title}: ${a.word}`);
|
|
381
|
+
for (const t of st.skipped) out.push(` ↷ skipped: ${t}`);
|
|
382
|
+
for (const t of st.left) out.push(` · still waiting: ${t}`);
|
|
383
|
+
return out;
|
|
384
|
+
}
|
|
@@ -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 };
|