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,27 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
2
|
+
import { pathToFileURL } from 'node:url';
|
|
3
|
+
|
|
4
|
+
/** Explicit local opt-in only. A display failure must not interrupt a chat. */
|
|
5
|
+
export async function loadChatCompanion(path, { output = process.stdout, warn = console.error, display = {} } = {}) {
|
|
6
|
+
if (!path || !output.isTTY) return null;
|
|
7
|
+
try {
|
|
8
|
+
const mod = await import(pathToFileURL(resolve(path)).href);
|
|
9
|
+
const c = await mod.createCompanion({ output, ...display });
|
|
10
|
+
if (!c || !['event', 'write', 'close', 'suspend'].every(k => typeof c[k] === 'function')) {
|
|
11
|
+
try { c?.close?.(); } catch {}
|
|
12
|
+
throw new Error('Expected createCompanion() with event/write/close/suspend');
|
|
13
|
+
}
|
|
14
|
+
let active = true;
|
|
15
|
+
const stop = async () => { active = false; try { await c.close(); } catch {} };
|
|
16
|
+
return {
|
|
17
|
+
event(e) { if (active) try { c.event(e); } catch { stop(); } },
|
|
18
|
+
write(text) { if (!active) output.write(text); else c.write(text); },
|
|
19
|
+
// Trace writes to stderr: relinquish the block first to prevent overlap.
|
|
20
|
+
suspend() { if (active) try { c.suspend(); } catch { stop(); } },
|
|
21
|
+
close: stop,
|
|
22
|
+
};
|
|
23
|
+
} catch (e) {
|
|
24
|
+
warn('Dragon companion unavailable; continuing normal chat: ' + e.message);
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Last conversation per (profile, cwd), so `quox chat --continue` keeps context without the user
|
|
3
|
+
* copying a session id. Best-effort: a read/write failure never breaks a chat turn.
|
|
4
|
+
*/
|
|
5
|
+
import { homedir } from 'node:os';
|
|
6
|
+
import { join, dirname } from 'node:path';
|
|
7
|
+
import { readFileSync, writeFileSync, mkdirSync, readlinkSync } from 'node:fs';
|
|
8
|
+
|
|
9
|
+
const stateFile = () => process.env.QUOX_CHAT_STATE || join(homedir(), '.config', 'quox', 'chat-last.json');
|
|
10
|
+
/**
|
|
11
|
+
* Who is calling: $QUOX_SESSION when set, else this terminal's tty, else nothing (a headless
|
|
12
|
+
* caller with no session id falls back to profile + cwd only). Parallel terminals/sessions must
|
|
13
|
+
* never share a "last conversation".
|
|
14
|
+
*/
|
|
15
|
+
export function callerScope(env = process.env) {
|
|
16
|
+
if (env.QUOX_SESSION) return `session:${String(env.QUOX_SESSION).slice(0, 64)}`;
|
|
17
|
+
for (const fd of [0, 1, 2]) {
|
|
18
|
+
try {
|
|
19
|
+
const t = readlinkSync(`/proc/self/fd/${fd}`);
|
|
20
|
+
if (/^\/dev\/(pts\/\d+|tty\w*)$/.test(t)) return `tty:${t}`;
|
|
21
|
+
} catch { /* not linux, or fd closed */ }
|
|
22
|
+
}
|
|
23
|
+
return '';
|
|
24
|
+
}
|
|
25
|
+
const keyOf = (profile, cwd) => `${profile || 'default'}|${cwd || process.cwd()}|${callerScope()}`;
|
|
26
|
+
|
|
27
|
+
function read() {
|
|
28
|
+
try { return JSON.parse(readFileSync(stateFile(), 'utf8')) || {}; } catch { return {}; }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function lastConversation(profile, cwd) {
|
|
32
|
+
const v = read()[keyOf(profile, cwd)];
|
|
33
|
+
return typeof v === 'string' && v ? v : undefined;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function saveLastConversation(profile, cwd, id) {
|
|
37
|
+
if (!id) return;
|
|
38
|
+
try {
|
|
39
|
+
const all = read();
|
|
40
|
+
all[keyOf(profile, cwd)] = id;
|
|
41
|
+
const f = stateFile();
|
|
42
|
+
mkdirSync(dirname(f), { recursive: true });
|
|
43
|
+
writeFileSync(f, JSON.stringify(all, null, 2));
|
|
44
|
+
} catch { /* best-effort */ }
|
|
45
|
+
}
|
package/lib/commands/claude.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* quox claude / codex /
|
|
2
|
+
* quox claude / codex / run / sessions: governed tmux sessions. The runner
|
|
3
3
|
* lives in core/governedSession.js; this file holds argument parsing and the
|
|
4
4
|
* commander wiring. argv arrays only; user input never reaches a shell string.
|
|
5
5
|
*/
|
|
6
|
+
import { markForwardsRawArgs } from '../core/global-flags.js';
|
|
6
7
|
import * as gs from '../core/governedSession.js';
|
|
7
8
|
import { validateName, PROGRAMS } from '../core/governedSession.js';
|
|
8
9
|
import { runClaudeLs } from '../core/claudeLs.js';
|
|
@@ -101,22 +102,6 @@ export const runClaude = ({ as, slot, ...opts } = {}, over) => gs.runGoverned({
|
|
|
101
102
|
program: 'claude',
|
|
102
103
|
...(as || slot ? { preattach: { agent: as, slot, session: opts.name } } : {}),
|
|
103
104
|
}, over);
|
|
104
|
-
/**
|
|
105
|
-
* quox gemini <args>: verbatim to `gemini`. Gemini resumes by index or
|
|
106
|
-
* "latest" (`--resume latest`, `-r 5`), not by name, so the tmux session is
|
|
107
|
-
* auto-named; reattach with `quox sessions attach NAME`.
|
|
108
|
-
*/
|
|
109
|
-
export function parseGeminiArgs(raw) {
|
|
110
|
-
let noTmux = false;
|
|
111
|
-
const args = [];
|
|
112
|
-
for (const a of raw) {
|
|
113
|
-
if (a === '--no-tmux') noTmux = true;
|
|
114
|
-
else args.push(a);
|
|
115
|
-
}
|
|
116
|
-
const resume = args.some((a) => a === '--resume' || a === '-r' || a.startsWith('--resume='));
|
|
117
|
-
return { args, name: undefined, resume, noTmux };
|
|
118
|
-
}
|
|
119
|
-
|
|
120
105
|
/**
|
|
121
106
|
* quox wrap <program> [args...]: wrap ANY command. Known programs use their
|
|
122
107
|
* registry entry (so `quox wrap codex` == `quox codex`); unknown ones get the
|
|
@@ -134,12 +119,11 @@ export function parseRunArgs(raw) {
|
|
|
134
119
|
}
|
|
135
120
|
|
|
136
121
|
export function runRun({ bin, args = [], noTmux = false } = {}, over) {
|
|
137
|
-
const known = ['claude', 'codex'
|
|
122
|
+
const known = ['claude', 'codex'].find((k) => PROGRAMS[k].bin === bin);
|
|
138
123
|
if (known) return gs.runGoverned({ program: known, args, noTmux }, over);
|
|
139
124
|
return gs.runGoverned({ program: 'run', bin, args, noTmux }, over);
|
|
140
125
|
}
|
|
141
126
|
|
|
142
|
-
export const runGemini = (opts, over) => gs.runGoverned({ ...opts, program: 'gemini' }, over);
|
|
143
127
|
export const runCodex = (opts, over) => gs.runGoverned({ ...opts, program: 'codex' }, over);
|
|
144
128
|
export const runTui = (opts, over) => gs.runGoverned({ ...opts, program: 'tui' }, over);
|
|
145
129
|
export const runHelp = gs.runHelp;
|
|
@@ -178,8 +162,8 @@ export function registerClaudeCommands(program, ctx, over) {
|
|
|
178
162
|
// `quox claude <args>` behaves exactly like `claude <args>`: every arg is
|
|
179
163
|
// forwarded verbatim. Only --no-tmux is quox's own. Management lives in
|
|
180
164
|
// `quox sessions` so it can never collide with claude's own arguments.
|
|
181
|
-
program
|
|
182
|
-
.command('claude')
|
|
165
|
+
markForwardsRawArgs(program
|
|
166
|
+
.command('claude'), 'forwards argv after the verb verbatim')
|
|
183
167
|
.description('Run Claude Code (same arguments as claude) in a tmux session that survives sleep and drops')
|
|
184
168
|
.argument('[args...]', 'passed to claude exactly as given (quox adds only --no-tmux, --as, --slot)')
|
|
185
169
|
.addHelpText('after', `
|
|
@@ -205,8 +189,8 @@ Claude's own --agent is untouched and passes through to claude.`)
|
|
|
205
189
|
const parsed = parseClaudeArgs(rest);
|
|
206
190
|
process.exitCode = runClaude(parsed, over);
|
|
207
191
|
});
|
|
208
|
-
program
|
|
209
|
-
.command('codex')
|
|
192
|
+
markForwardsRawArgs(program
|
|
193
|
+
.command('codex'), 'forwards argv after the verb verbatim')
|
|
210
194
|
.description('Run Codex (same arguments as codex) in a tmux session that survives sleep and drops')
|
|
211
195
|
.argument('[args...]', 'passed to codex exactly as given (quox adds only --no-tmux)')
|
|
212
196
|
.helpOption(false)
|
|
@@ -218,20 +202,8 @@ Claude's own --agent is untouched and passes through to claude.`)
|
|
|
218
202
|
const parsed = parseCodexArgs(i >= 0 ? raw.slice(i + 1) : []);
|
|
219
203
|
process.exitCode = runCodex(parsed, over);
|
|
220
204
|
});
|
|
221
|
-
program
|
|
222
|
-
.command('
|
|
223
|
-
.description('Run Gemini CLI (same arguments as gemini) in a tmux session that survives sleep and drops')
|
|
224
|
-
.argument('[args...]', 'passed to gemini exactly as given (quox adds only --no-tmux)')
|
|
225
|
-
.helpOption(false)
|
|
226
|
-
.allowUnknownOption()
|
|
227
|
-
.allowExcessArguments()
|
|
228
|
-
.action((_a, _o, cmd) => {
|
|
229
|
-
const raw = (cmd.parent && cmd.parent.rawArgs) || process.argv;
|
|
230
|
-
const i = raw.indexOf('gemini', 2);
|
|
231
|
-
process.exitCode = runGemini(parseGeminiArgs(i >= 0 ? raw.slice(i + 1) : []), over);
|
|
232
|
-
});
|
|
233
|
-
program
|
|
234
|
-
.command('wrap')
|
|
205
|
+
markForwardsRawArgs(program
|
|
206
|
+
.command('wrap'), 'forwards argv after the verb verbatim')
|
|
235
207
|
.description('Run any program in a governed tmux session: quox wrap <program> [args...]')
|
|
236
208
|
.argument('<program>', 'command name on PATH')
|
|
237
209
|
.argument('[args...]', 'passed to the program exactly as given')
|
|
@@ -270,10 +242,35 @@ Claude's own --agent is untouched and passes through to claude.`)
|
|
|
270
242
|
const json = !!o.json || program.opts().output === 'json';
|
|
271
243
|
process.exitCode = await runSessionsOverview({ json, watch: !!o.watch, all: !!o.all }, { ...so, fetchIndex });
|
|
272
244
|
});
|
|
245
|
+
// `quox active [sessions|slots|agents]`: one short summary of who is doing what (QUOXON-1208).
|
|
246
|
+
program.command('active [filter]')
|
|
247
|
+
.description('Who is doing what: one line per active worker, grouped working / waiting on you / idle; filter: sessions, slots, agents')
|
|
248
|
+
.option('--json', 'print the summary as JSON')
|
|
249
|
+
.option('--all', 'show every worker (default: top 10 per group)')
|
|
250
|
+
.option('--lines', 'the older one-line-per-worker layout with state and age columns')
|
|
251
|
+
.action(async (filter, o) => {
|
|
252
|
+
const so = (over && over.sessionsOverview) || {};
|
|
253
|
+
const fetchIndex = so.fetchIndex || (async () => {
|
|
254
|
+
const { client, auth } = ctx;
|
|
255
|
+
auth.requireAuth();
|
|
256
|
+
const ep = resolveEndpoint('v1.sessions.index');
|
|
257
|
+
const org = orgOrNull(auth);
|
|
258
|
+
return client.get(ep.path, { service: ep.service, retries: 0, ...(org ? { query: { org_id: org } } : {}) });
|
|
259
|
+
});
|
|
260
|
+
const json = !!o.json || program.opts().output === 'json';
|
|
261
|
+
const { runActive } = await import('../core/activeSummary.js');
|
|
262
|
+
const fetchSlots = so.fetchSlots || (async () => {
|
|
263
|
+
const { client, auth } = ctx;
|
|
264
|
+
const ep = resolveEndpoint('v1.slots.list');
|
|
265
|
+
const r = await client.get(ep.path, { service: ep.service, retries: 0 });
|
|
266
|
+
return Array.isArray(r) ? r : (r?.slots || r?.data || []);
|
|
267
|
+
});
|
|
268
|
+
process.exitCode = await runActive({ filter: filter || null, json, all: !!o.all, lines: !!o.lines }, { ...so, fetchIndex, fetchSlots });
|
|
269
|
+
});
|
|
273
270
|
sessions.command('heartbeat <tmuxSession>', { hidden: true })
|
|
274
271
|
.description('Heartbeat one wrapped tmux session to the org session index (started by quox claude)')
|
|
275
272
|
.option('--loop', 'keep beating until the tmux session ends')
|
|
276
|
-
.option('--backend <program>', 'claude, codex,
|
|
273
|
+
.option('--backend <program>', 'claude, codex, ...')
|
|
277
274
|
.option('--agent <agent>', 'agent the session started attached to (--as)')
|
|
278
275
|
.option('--slot <slot>', 'slot the session started holding (--slot)')
|
|
279
276
|
.action(async (name, o) => {
|
package/lib/commands/contacts.js
CHANGED
|
@@ -14,6 +14,7 @@ import { line as glyphLine, printEvent } from '../core/quoxglyph.js';
|
|
|
14
14
|
import { resolveEndpoint } from '../api/endpoints.js';
|
|
15
15
|
import { runSelftest } from './contactsSelftest.js';
|
|
16
16
|
import { resolveOrgId } from './helpers.js';
|
|
17
|
+
import { atToSendAt } from './contactsSendAt.js';
|
|
17
18
|
|
|
18
19
|
const KINDS = ['person', 'organisation'];
|
|
19
20
|
const REASONS = ['dnc', 'do_not_market', 'bounced'];
|
|
@@ -38,6 +39,8 @@ async function call(fn, { forbidden } = {}) {
|
|
|
38
39
|
return await fn();
|
|
39
40
|
} catch (e) {
|
|
40
41
|
if (e instanceof QuoxApiError) {
|
|
42
|
+
// A refusal that says why (scheduled/immediate send) is shown as said, never swapped for a generic line.
|
|
43
|
+
if ((e.status === 403 || e.status === 409) && e.body && typeof e.body.reason === 'string' && e.body.reason) throw new QuoxError(e.body.reason, 1);
|
|
41
44
|
if (e.status === 403 && forbidden) throw new QuoxError(forbidden, 1);
|
|
42
45
|
if (e.status === 404 && e.body && e.body.error === 'tombstone_not_found') {
|
|
43
46
|
throw new QuoxError('No erasure tombstone exists for that identifier in this org.', 1);
|
|
@@ -107,13 +110,17 @@ export function registerContactsCommands(program, ctx) {
|
|
|
107
110
|
const q = textQuery ?? options.q;
|
|
108
111
|
if (q) query.q = q;
|
|
109
112
|
if (options.status) query.status = options.status;
|
|
113
|
+
if (options.relationship) query.relationship = options.relationship;
|
|
110
114
|
if (options.limit !== undefined) query.limit = Number(options.limit);
|
|
115
|
+
if (options.offset !== undefined) query.offset = Number(options.offset);
|
|
111
116
|
const ep = resolveEndpoint('contactsq.contacts.list');
|
|
112
117
|
const data = await call(() => client.get(ep.path, { service: ep.service, query }));
|
|
113
118
|
if (isJson(program)) return printJson(data);
|
|
114
119
|
const rows = (data.contacts || []).map(cardRow);
|
|
115
120
|
formatOutput({ columns: CARD_COLS, headers: CARD_HEADS, rows }, { format: program.opts().output });
|
|
116
|
-
|
|
121
|
+
const total = Number.isInteger(data.total) ? data.total : rows.length;
|
|
122
|
+
const next = (data.offset || 0) + rows.length;
|
|
123
|
+
announce('contacts list', next < total ? `${rows.length} of ${total} contacts, next: --offset ${next}` : `${rows.length} of ${total} contacts`);
|
|
117
124
|
};
|
|
118
125
|
|
|
119
126
|
addNameOptions(contacts
|
|
@@ -185,8 +192,10 @@ export function registerContactsCommands(program, ctx) {
|
|
|
185
192
|
.command('list')
|
|
186
193
|
.description('List contacts')
|
|
187
194
|
.option('--q <text>', 'name filter')
|
|
188
|
-
.option('--status <s>', 'active | candidate | archived')
|
|
189
|
-
.option('--
|
|
195
|
+
.option('--status <s>', 'active | candidate | archived | all (default: everyone except archived)')
|
|
196
|
+
.option('--relationship <r>', 'primary relationship, e.g. customer | supplier | partner')
|
|
197
|
+
.option('--limit <n>', 'page size (default 25, max 100)')
|
|
198
|
+
.option('--offset <n>', 'skip this many (paging: the footer prints the next --offset)')
|
|
190
199
|
.action((options) => list(options));
|
|
191
200
|
|
|
192
201
|
contacts
|
|
@@ -373,6 +382,53 @@ export function registerContactsCommands(program, ctx) {
|
|
|
373
382
|
announce('contacts brief', `${d.candidates_to_review} candidates, ${d.duplicates} duplicates`);
|
|
374
383
|
});
|
|
375
384
|
|
|
385
|
+
contacts
|
|
386
|
+
.command('timeline <id>')
|
|
387
|
+
.description('Interaction history of a contact, newest first: surface, direction, outcome and time (refs only, no message bodies)')
|
|
388
|
+
.option('--limit <n>', 'max entries', '20')
|
|
389
|
+
.action(async (id, options) => {
|
|
390
|
+
auth.requireAuth();
|
|
391
|
+
const ep = resolveEndpoint('contactsq.timeline', { id });
|
|
392
|
+
const d = await call(() => client.get(ep.path, { service: ep.service, query: { limit: options.limit } }));
|
|
393
|
+
if (isJson(program)) return printJson(d);
|
|
394
|
+
const rows = d.timeline || [];
|
|
395
|
+
if (!rows.length) console.log('No interactions recorded.');
|
|
396
|
+
for (const t of rows) console.log(`${t.occurred_at || ''} ${t.surface || ''} ${t.direction || ''} ${t.outcome || ''}${t.agent_id ? ` (agent ${t.agent_id})` : ''}`);
|
|
397
|
+
announce('contacts timeline', `${rows.length} entries`);
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
contacts
|
|
401
|
+
.command('best-channel <id>')
|
|
402
|
+
.description('Which channel (voice, email or sms) may be used to reach a contact for a purpose, with every alternative and its reason codes. Read-only; never shows a number or address')
|
|
403
|
+
.option('--purpose <purpose>', 'service, transactional, marketing or research', 'service')
|
|
404
|
+
.action(async (id, options) => {
|
|
405
|
+
auth.requireAuth();
|
|
406
|
+
const ep = resolveEndpoint('contactsq.best_channel', { id });
|
|
407
|
+
const d = await call(() => client.get(ep.path, { service: ep.service, query: { purpose: options.purpose } }));
|
|
408
|
+
if (isJson(program)) return printJson(d);
|
|
409
|
+
console.log(`channel: ${d.channel || 'none allowed'}`);
|
|
410
|
+
for (const a of d.alternatives || []) console.log(` ${a.channel}: ${a.decision}${(a.reasons || []).length ? ` (${a.reasons.join(', ')})` : ''}`);
|
|
411
|
+
announce('contacts best-channel', d.channel || 'none allowed');
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
contacts
|
|
415
|
+
.command('count')
|
|
416
|
+
.description('How many contacts: the total (people and organisations, not archived or erased) with counts by kind, status and relationship. --kind narrows to person or organisation')
|
|
417
|
+
.option('--kind <kind>', 'person or organisation')
|
|
418
|
+
.action(async (options) => {
|
|
419
|
+
auth.requireAuth();
|
|
420
|
+
const ep = resolveEndpoint('contactsq.count');
|
|
421
|
+
const query = options.kind ? { kind: options.kind } : undefined;
|
|
422
|
+
const d = await call(() => client.get(ep.path, { service: ep.service, query }));
|
|
423
|
+
if (isJson(program)) return printJson(d);
|
|
424
|
+
const fmt = (o) => Object.entries(o || {}).map(([k, n]) => `${k} ${n}`).join(', ') || 'none';
|
|
425
|
+
formatDetail({
|
|
426
|
+
total: d.total, ...(d.kind ? { kind: d.kind } : {}), by_kind: fmt(d.by_kind), by_status: fmt(d.by_status),
|
|
427
|
+
by_relationship: fmt(d.by_relationship), candidates_to_review: d.candidates_to_review, generated_at: d.generated_at,
|
|
428
|
+
});
|
|
429
|
+
announce('contacts count', `${d.total} contacts${d.kind ? ` (${d.kind})` : ''}`);
|
|
430
|
+
});
|
|
431
|
+
|
|
376
432
|
contacts
|
|
377
433
|
.command('selftest')
|
|
378
434
|
.description('Drive the read path (search, find, resolve, best channel, timeline, touchpoint, brief) against a demo contact. Never dials or sends. --agent runs it through the attached agent tool scope')
|
|
@@ -431,7 +487,7 @@ export function registerContactsCommands(program, ctx) {
|
|
|
431
487
|
.description('Dial a contact by id through the phone bridge; you never see or type the number')
|
|
432
488
|
.argument('<id>', 'contact id (find it with `quox contacts search`)')
|
|
433
489
|
.option('--reason <text>', 'what the call is about (the agent opens with it)')
|
|
434
|
-
.option('--purpose <p>', 'service | transactional | marketing | research', 'service')
|
|
490
|
+
.option('--purpose <p>', 'service | transactional | marketing | research | personal', 'service')
|
|
435
491
|
.action(async (id, options) => {
|
|
436
492
|
auth.requireAuth();
|
|
437
493
|
const body = { contact_id: id, projection: 'dial', channel: 'voice', purpose: options.purpose };
|
|
@@ -446,6 +502,80 @@ export function registerContactsCommands(program, ctx) {
|
|
|
446
502
|
announce('contacts call', `${l.id || '?'} queued for the phone bridge`);
|
|
447
503
|
});
|
|
448
504
|
|
|
505
|
+
// ---- send: SMS or email now or at a time (the server delivers; you approve by running it) ----
|
|
506
|
+
const DEFAULT_TZ = 'Europe/London';
|
|
507
|
+
async function resolveContactArg(arg) {
|
|
508
|
+
if (/^ct_[a-z0-9_]+$/i.test(arg)) return arg;
|
|
509
|
+
const ep = resolveEndpoint('contactsq.search');
|
|
510
|
+
const data = await call(() => client.get(ep.path, { service: ep.service, query: { q: arg } }));
|
|
511
|
+
const c = data.candidates || [];
|
|
512
|
+
if (!c.length) throw new QuoxError(`No contact matches "${arg}". Try \`quox contacts search\`.`, 1);
|
|
513
|
+
if (data.ambiguous || c.length > 1) {
|
|
514
|
+
throw new QuoxError(`"${arg}" matches more than one contact (${c.slice(0, 5).map((x) => `${x.display_name} ${x.id}`).join('; ')}). Pass the contact id.`, 1);
|
|
515
|
+
}
|
|
516
|
+
return c[0].id;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
contacts
|
|
520
|
+
.command('send')
|
|
521
|
+
.description('Send an SMS or email to a contact now, or at a time with --at (the server delivers it; the answer says exactly when)')
|
|
522
|
+
.argument('<contact>', 'contact id (ct_...) or an unambiguous name')
|
|
523
|
+
.argument('<text>', 'the message')
|
|
524
|
+
.option('--channel <c>', 'sms | email', 'sms')
|
|
525
|
+
.option('--at <time>', 'send later: ISO time with offset, or a clock time like 10am / 14:30 (next occurrence, in --tz)')
|
|
526
|
+
.option('--tz <zone>', 'IANA zone for --at', DEFAULT_TZ)
|
|
527
|
+
.option('--title <t>', 'subject (email)')
|
|
528
|
+
.option('--purpose <p>', 'service | transactional | marketing | research | personal', 'service')
|
|
529
|
+
.option('--reason <text>', 'why (recorded as a hash only)')
|
|
530
|
+
.action(async (contactArg, text, options) => {
|
|
531
|
+
auth.requireAuth();
|
|
532
|
+
checkEnum(options.channel, ['sms', 'email'], '--channel');
|
|
533
|
+
checkEnum(options.purpose, ['service', 'transactional', 'marketing', 'research', 'personal'], '--purpose');
|
|
534
|
+
const contactId = await resolveContactArg(contactArg);
|
|
535
|
+
const body = { contact_id: contactId, channel: options.channel, message: text, purpose: options.purpose };
|
|
536
|
+
if (options.title) body.title = options.title;
|
|
537
|
+
if (options.reason) body.reason = options.reason;
|
|
538
|
+
if (options.at) { body.send_at = atToSendAt(options.at, options.tz); body.timezone = options.tz; }
|
|
539
|
+
const ep = resolveEndpoint('contactsq.sends.create');
|
|
540
|
+
const data = await call(() => client.post(ep.path, { service: ep.service, body, retries: 0 }), {
|
|
541
|
+
forbidden: 'Blocked by contact policy, or you are not a human user. Run `quox contacts best-channel <id>` to see why.',
|
|
542
|
+
});
|
|
543
|
+
if (isJson(program)) return printJson(data);
|
|
544
|
+
if (data.job) {
|
|
545
|
+
formatDetail({ job: data.job.id, status: data.job.status, channel: data.job.channel, when: data.job.when, send_at: data.job.send_at });
|
|
546
|
+
return announce('contacts send', `scheduled ${data.job.id}: ${data.job.when}`);
|
|
547
|
+
}
|
|
548
|
+
if (data.status === 'sent') { formatDetail({ lease: data.lease_id, status: 'sent' }); return announce('contacts send', `${options.channel} sent`); }
|
|
549
|
+
formatDetail({ lease: data.lease_id, status: data.status, error: data.error, reason: data.reason });
|
|
550
|
+
announce('contacts send', `FAILED: ${data.reason || data.error}`);
|
|
551
|
+
process.exitCode = 1;
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
const scheduled = contacts.command('scheduled').description('Scheduled sends: what is queued, and cancel one before it goes out');
|
|
555
|
+
scheduled
|
|
556
|
+
.command('list')
|
|
557
|
+
.option('--status <s>', 'comma list of pending,sending,sent,failed,cancelled (default pending,sending)')
|
|
558
|
+
.action(async (options) => {
|
|
559
|
+
auth.requireAuth();
|
|
560
|
+
const ep = resolveEndpoint('contactsq.sends.scheduled');
|
|
561
|
+
const data = await call(() => client.get(ep.path, { service: ep.service, query: options.status ? { status: options.status } : {} }), { forbidden: 'Scheduled sends are for human users.' });
|
|
562
|
+
if (isJson(program)) return printJson(data);
|
|
563
|
+
const rows = (data.jobs || []).map((j) => ({ id: j.id, contact: j.contact_id, channel: j.channel, status: j.status, when: j.when || j.finished_at || '', outcome: j.outcome_reason || j.outcome_code || '' }));
|
|
564
|
+
formatOutput({ columns: ['id', 'contact', 'channel', 'status', 'when', 'outcome'], rows }, { format: program.opts().output });
|
|
565
|
+
announce('contacts scheduled list', `${rows.length} jobs`);
|
|
566
|
+
});
|
|
567
|
+
scheduled
|
|
568
|
+
.command('cancel')
|
|
569
|
+
.argument('<id>', 'scheduled send id (ss_...)')
|
|
570
|
+
.action(async (id) => {
|
|
571
|
+
auth.requireAuth();
|
|
572
|
+
const ep = resolveEndpoint('contactsq.sends.cancel', { id });
|
|
573
|
+
const data = await call(() => client.post(ep.path, { service: ep.service, body: {} }), { forbidden: 'Cancelling a scheduled send is for human users.' });
|
|
574
|
+
if (isJson(program)) return printJson(data);
|
|
575
|
+
formatDetail({ job: data.job.id, status: data.job.status });
|
|
576
|
+
announce('contacts scheduled cancel', data.job.id);
|
|
577
|
+
});
|
|
578
|
+
|
|
449
579
|
contacts
|
|
450
580
|
.command('leases')
|
|
451
581
|
.description('Lease + use ledger for a contact (who was given access, when, why; never the values)')
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { QuoxError } from '../core/errors.js';
|
|
2
|
+
|
|
3
|
+
/** "10am", "10:30", "22:15" -> the next such wall-clock time in tz as a bare local ISO the server reads in that zone. ISO input passes through. */
|
|
4
|
+
export function atToSendAt(at, tz, nowMs = Date.now()) {
|
|
5
|
+
const raw = String(at).trim();
|
|
6
|
+
if (/^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}/.test(raw) || /(?:Z|[+-]\d{2}:?\d{2})$/i.test(raw)) return raw;
|
|
7
|
+
const m = /^(\d{1,2})(?::(\d{2}))?\s*(am|pm)?$/i.exec(raw);
|
|
8
|
+
if (!m) throw new QuoxError(`--at must be an ISO time (2026-10-25T10:00:00+00:00) or a clock time such as 10am or 14:30, got "${raw}"`, 2);
|
|
9
|
+
let h = Number(m[1]);
|
|
10
|
+
const mi = Number(m[2] || 0);
|
|
11
|
+
if (m[3]) { if (h < 1 || h > 12) throw new QuoxError('--at hour must be 1 to 12 with am/pm', 2); h = (h % 12) + (m[3].toLowerCase() === 'pm' ? 12 : 0); }
|
|
12
|
+
if (h > 23 || mi > 59) throw new QuoxError('--at is not a valid clock time', 2);
|
|
13
|
+
const parts = (ms) => Object.fromEntries(new Intl.DateTimeFormat('en-GB', { timeZone: tz, year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', hourCycle: 'h23' }).formatToParts(new Date(ms)).map((p) => [p.type, p.value]));
|
|
14
|
+
const n = parts(nowMs);
|
|
15
|
+
const pad = (v) => String(v).padStart(2, '0');
|
|
16
|
+
let day = Date.UTC(Number(n.year), Number(n.month) - 1, Number(n.day));
|
|
17
|
+
if (h * 60 + mi <= Number(n.hour) * 60 + Number(n.minute)) day += 86400000; // already passed today: tomorrow
|
|
18
|
+
const d = new Date(day);
|
|
19
|
+
return `${d.getUTCFullYear()}-${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())}T${pad(h)}:${pad(mi)}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
@@ -16,11 +16,29 @@ export function registerConversationCommands(program, ctx) {
|
|
|
16
16
|
|
|
17
17
|
conv
|
|
18
18
|
.command('list')
|
|
19
|
-
.description('List conversations')
|
|
19
|
+
.description('List your chat conversations (newest first); --archive lists the imported conversation archive')
|
|
20
20
|
.option('--limit <n>', 'Max results', '20')
|
|
21
|
-
.option('--offset <n>', 'Skip results', '0')
|
|
21
|
+
.option('--offset <n>', 'Skip results (archive only)', '0')
|
|
22
|
+
.option('--archive', 'list the imported conversation archive instead of chat conversations')
|
|
22
23
|
.action(async (opts) => {
|
|
23
24
|
auth.requireAuth();
|
|
25
|
+
if (!opts.archive) {
|
|
26
|
+
const ep = resolveEndpoint('conversations.chat.list');
|
|
27
|
+
const data = await client.get(ep.path, { service: ep.service, query: { limit: opts.limit } });
|
|
28
|
+
const rows = data.conversations || [];
|
|
29
|
+
formatOutput({
|
|
30
|
+
columns: ['id', 'title', 'turns', 'agent', 'updated_at'],
|
|
31
|
+
headers: ['ID', 'Title', 'Turns', 'Agent', 'Updated'],
|
|
32
|
+
rows: rows.map((c) => ({
|
|
33
|
+
id: c.id,
|
|
34
|
+
title: (c.title || 'Untitled').slice(0, 50),
|
|
35
|
+
turns: c.turn_count ?? 0,
|
|
36
|
+
agent: c.agent || '-',
|
|
37
|
+
updated_at: c.updated_at ? new Date(c.updated_at).toLocaleString() : '-',
|
|
38
|
+
})),
|
|
39
|
+
}, { format: program.opts().output });
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
24
42
|
const query = { limit: opts.limit, offset: opts.offset };
|
|
25
43
|
const data = await client.get(resolveEndpoint('conversations.list').path, { service: resolveEndpoint('conversations.list').service, query });
|
|
26
44
|
const conversations = data.conversations || data || [];
|
|
@@ -39,9 +57,22 @@ export function registerConversationCommands(program, ctx) {
|
|
|
39
57
|
conv
|
|
40
58
|
.command('get')
|
|
41
59
|
.argument('<id>', 'conversation ID')
|
|
42
|
-
.description('
|
|
43
|
-
.
|
|
60
|
+
.description('Show a chat conversation\'s turns; --archive reads the imported conversation archive')
|
|
61
|
+
.option('--limit <n>', 'Max turns (chat only)', '50')
|
|
62
|
+
.option('--archive', 'read the imported conversation archive instead of chat conversations')
|
|
63
|
+
.action(async (id, opts) => {
|
|
44
64
|
auth.requireAuth();
|
|
65
|
+
if (!opts.archive) {
|
|
66
|
+
const ep = resolveEndpoint('conversations.chat.turns', { id });
|
|
67
|
+
const data = await client.get(ep.path, { service: ep.service, query: { limit: opts.limit } });
|
|
68
|
+
const turns = data.turns || [];
|
|
69
|
+
formatOutput({
|
|
70
|
+
columns: ['ts', 'role', 'content'],
|
|
71
|
+
headers: ['Time', 'Role', 'Content'],
|
|
72
|
+
rows: turns.map((t) => ({ ts: t.ts ? new Date(t.ts).toLocaleString() : '-', role: t.role, content: t.content })),
|
|
73
|
+
}, { format: program.opts().output });
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
45
76
|
const data = await client.get(resolveEndpoint('conversations.get', { id }).path, { service: resolveEndpoint('conversations.get', { id }).service });
|
|
46
77
|
formatDetail(data.conversation || data, { format: program.opts().output, title: 'Conversation' });
|
|
47
78
|
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `quox dashboard [page]`: open the web dashboard already signed in, via a one-time link
|
|
3
|
+
* (10 minutes, single use). Same engine as /dashboard in the Quox client (lib/quoxon/slash/dashboard.js).
|
|
4
|
+
* Over SSH it prints the link and does not open a browser. The link is a credential: it is printed
|
|
5
|
+
* to the terminal only, never logged.
|
|
6
|
+
*/
|
|
7
|
+
import { resolveEndpoint } from '../api/endpoints.js';
|
|
8
|
+
import { runDashboard, dashboardLines, PAGES } from '../quoxon/slash/dashboard.js';
|
|
9
|
+
import { supportsHyperlinks } from '../quoxon/markdown.js';
|
|
10
|
+
import { resolveDashboardBase, detectLanIp } from '../quoxon/hostUrls.js';
|
|
11
|
+
import { loadJourney } from '../quoxon/onboarding/journey.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Z2H-R4: the address to print, resolved exactly like /dashboard in the Quox client (api.js
|
|
15
|
+
* dashboardBase): server app URL, else a non-loopback profile URL, else the install finale's URL,
|
|
16
|
+
* else this machine's LAN IP. Over SSH a localhost link only works on the Quox box itself.
|
|
17
|
+
*/
|
|
18
|
+
export async function commandDashboardBase({ client, config, journey = loadJourney, lanIp = detectLanIp() } = {}) {
|
|
19
|
+
const profileUrl = config.getActiveProfile?.()?.url || config.getBaseUrl();
|
|
20
|
+
let serverApp = null;
|
|
21
|
+
try { const ep = resolveEndpoint('admin.instance.basics.get'); const r = await client.get(ep.path, { service: ep.service, retries: 0 }); serverApp = r?.effective?.appBaseUrl || null; } catch { serverApp = null; }
|
|
22
|
+
let journeyUrl = null; try { journeyUrl = journey()?.dashboardUrl || null; } catch { journeyUrl = null; }
|
|
23
|
+
return resolveDashboardBase({ serverApp, profileUrl, journeyUrl, lanIp }).url;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function registerDashboardCommands(program, ctx) {
|
|
27
|
+
const { client, auth, config, lanIp = detectLanIp, journey = loadJourney } = ctx;
|
|
28
|
+
program
|
|
29
|
+
.command('dashboard')
|
|
30
|
+
.description(`Open the web dashboard signed in (one-time link, 10 minutes). Pages: ${Object.keys(PAGES).join(', ')}`)
|
|
31
|
+
.argument('[page]', 'dashboard page to land on')
|
|
32
|
+
.action(async (page) => {
|
|
33
|
+
auth.requireAuth();
|
|
34
|
+
const baseUrl = await commandDashboardBase({ client, config, journey, lanIp: lanIp() });
|
|
35
|
+
const r = await runDashboard({ page, baseUrl, mint: () => { const ep = resolveEndpoint('auth.login-link'); return client.post(ep.path, { service: ep.service, body: {}, retries: 0 }); } });
|
|
36
|
+
if (program.opts().output === 'json') {
|
|
37
|
+
console.log(JSON.stringify({ kind: r.kind, url: r.url ?? null, opened: Boolean(r.opened), remote: Boolean(r.remote), error: r.error ?? null }, null, 2));
|
|
38
|
+
} else {
|
|
39
|
+
for (const l of dashboardLines(r, { hyperlinks: supportsHyperlinks() })) console.log(l.text);
|
|
40
|
+
}
|
|
41
|
+
if (r.kind === 'error') process.exitCode = 1;
|
|
42
|
+
});
|
|
43
|
+
}
|
|
@@ -15,17 +15,38 @@ export function registerDepartmentCommands(program, ctx) {
|
|
|
15
15
|
|
|
16
16
|
dept
|
|
17
17
|
.command('list')
|
|
18
|
-
.description('List departments')
|
|
19
|
-
.
|
|
18
|
+
.description('List departments (all divisions unless --division; division ids: quox org divisions list)')
|
|
19
|
+
.option('--division <divId>', 'only this division')
|
|
20
20
|
.action(async (options) => {
|
|
21
21
|
auth.requireAuth();
|
|
22
22
|
const orgId = resolveOrgId(auth, program);
|
|
23
|
-
const
|
|
23
|
+
const listFor = async (division) => {
|
|
24
|
+
const ep = resolveEndpoint('divisions.departments.list', { orgId, division });
|
|
25
|
+
const data = await client.get(ep.path, { service: ep.service });
|
|
26
|
+
return data.departments || data || [];
|
|
27
|
+
};
|
|
28
|
+
if (options.division) {
|
|
29
|
+
formatOutput({
|
|
30
|
+
columns: ['id', 'name', 'description', 'created_at'],
|
|
31
|
+
headers: ['ID', 'Name', 'Description', 'Created'],
|
|
32
|
+
rows: await listFor(options.division),
|
|
33
|
+
}, { format: program.opts().output });
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
// quox#922: no --division lists every division's departments, so the read
|
|
37
|
+
// needs no id the department group cannot show.
|
|
38
|
+
const divEp = resolveEndpoint('divisions.list', { orgId });
|
|
39
|
+
const divData = await client.get(divEp.path, { service: divEp.service });
|
|
40
|
+
const divisions = divData.divisions || divData || [];
|
|
41
|
+
const rows = [];
|
|
42
|
+
for (const d of divisions) {
|
|
43
|
+
for (const dept of await listFor(d.id)) rows.push({ ...dept, division: d.name || d.id });
|
|
44
|
+
}
|
|
24
45
|
formatOutput({
|
|
25
|
-
columns: ['id', 'name', 'description', 'created_at'],
|
|
26
|
-
headers: ['ID', 'Name', 'Description', 'Created'],
|
|
27
|
-
rows
|
|
28
|
-
}, { format: program.opts().output });
|
|
46
|
+
columns: ['id', 'name', 'division', 'description', 'created_at'],
|
|
47
|
+
headers: ['ID', 'Name', 'Division', 'Description', 'Created'],
|
|
48
|
+
rows,
|
|
49
|
+
}, { format: program.opts().output, emptyMessage: 'No departments.' });
|
|
29
50
|
});
|
|
30
51
|
|
|
31
52
|
dept
|
|
@@ -164,10 +164,13 @@ export function registerDevResidentCommands(dev, ctx) {
|
|
|
164
164
|
const slotName = resolveSlotName(options, hostId);
|
|
165
165
|
const sessionName = options.sessionName || defaultSessionName(slotName);
|
|
166
166
|
|
|
167
|
+
let released = true;
|
|
168
|
+
let killed = true;
|
|
167
169
|
try {
|
|
168
170
|
const releaseEp = resolveEndpoint('v1.slots.release', { name: slotName });
|
|
169
171
|
await client.post(releaseEp.path, { service: releaseEp.service, body: { org_id: orgId, force: true } });
|
|
170
172
|
} catch (err) {
|
|
173
|
+
released = false;
|
|
171
174
|
printWarning(`Releasing ${slotName} failed (continuing): ${err.message}`);
|
|
172
175
|
}
|
|
173
176
|
|
|
@@ -175,10 +178,11 @@ export function registerDevResidentCommands(dev, ctx) {
|
|
|
175
178
|
const killEp = resolveEndpoint('v1.dev.sessions.kill', { hostId, sessionId: sessionName, byName: true });
|
|
176
179
|
await client.delete(killEp.path, { service: killEp.service });
|
|
177
180
|
} catch (err) {
|
|
181
|
+
killed = false;
|
|
178
182
|
printWarning(`Killing tmux session ${sessionName} on ${hostId} failed (continuing): ${err.message}`);
|
|
179
183
|
}
|
|
180
184
|
|
|
181
|
-
return { host_id: hostId, slot_name: slotName, session_name: sessionName };
|
|
185
|
+
return { host_id: hostId, slot_name: slotName, session_name: sessionName, released, killed };
|
|
182
186
|
}
|
|
183
187
|
|
|
184
188
|
resident
|
|
@@ -290,8 +294,16 @@ export function registerDevResidentCommands(dev, ctx) {
|
|
|
290
294
|
.action(async (options) => {
|
|
291
295
|
auth.requireAuth();
|
|
292
296
|
const report = await releaseAndKill(options);
|
|
293
|
-
|
|
294
|
-
|
|
297
|
+
// #1018: only claim what happened, in the card title too; a retire that left the slot held or the session running failed.
|
|
298
|
+
const outcome = report.released && report.killed ? 'Resident retired'
|
|
299
|
+
: report.released || report.killed ? 'Resident retire incomplete' : 'Resident retire failed';
|
|
300
|
+
formatDetail(report, { format: program.opts().output, title: `${outcome} on ${report.host_id}` });
|
|
301
|
+
if (report.released && report.killed) {
|
|
302
|
+
printSuccess(`Resident retired on ${report.host_id}: slot ${report.slot_name} released, session ${report.session_name} killed.`);
|
|
303
|
+
} else {
|
|
304
|
+
printWarning(`Resident NOT fully retired on ${report.host_id}: slot ${report.slot_name} ${report.released ? 'released' : 'NOT released'}, session ${report.session_name} ${report.killed ? 'killed' : 'NOT killed'}.`);
|
|
305
|
+
process.exitCode = 1;
|
|
306
|
+
}
|
|
295
307
|
});
|
|
296
308
|
|
|
297
309
|
resident
|
|
@@ -29,7 +29,7 @@ import { resolveEndpoint } from '../api/endpoints.js';
|
|
|
29
29
|
import { resolveOrgId } from './helpers.js';
|
|
30
30
|
import { loadResidentProfile, expandHome, deriveSupervisorEnv } from '../core/residentAuth.js';
|
|
31
31
|
import { createInitialPumpState, runPumpOnce } from '../core/residentPump.js';
|
|
32
|
-
import { createInitialAlarmState, runWardenPass } from '../core/residentAlarm.js';
|
|
32
|
+
import { createInitialAlarmState, runWardenPass, sendAlarm } from '../core/residentAlarm.js';
|
|
33
33
|
import {
|
|
34
34
|
DEFAULT_SETTINGS,
|
|
35
35
|
DEFAULT_IDLE_RECHECK_SECONDS,
|
|
@@ -344,7 +344,17 @@ export function registerDevSupervisorCommands(dev, ctx) {
|
|
|
344
344
|
await client.post(ep.path, { service: ep.service, body: { ...fields, slot_name: slotName, org_id: orgId } });
|
|
345
345
|
};
|
|
346
346
|
|
|
347
|
+
// GRIPE-797: one owner alert per auth-expiry episode, same transport
|
|
348
|
+
// order as the warden alarm (tg-send.sh first, slot say fallback).
|
|
349
|
+
const alertOwner = async (text) => {
|
|
350
|
+
const script = ctx.tgSendScriptPath || join(homedir(), '.claude', 'skills', 'telegram-owner', 'tg-send.sh');
|
|
351
|
+
await sendAlarm({
|
|
352
|
+
tgSend: async (session, t) => { await (ctx.execFileAsync || promisify(execFile))('bash', [script, session, t]); },
|
|
353
|
+
postSlotNotice: postReply, text, sessionName: `SUPERVISOR-${hostId}`, log,
|
|
354
|
+
});
|
|
355
|
+
};
|
|
347
356
|
const commonPollArgs = {
|
|
357
|
+
alertOwner,
|
|
348
358
|
hostId,
|
|
349
359
|
slot: slotName,
|
|
350
360
|
selfIdentity,
|
|
@@ -633,6 +643,8 @@ export function registerDevSupervisorCommands(dev, ctx) {
|
|
|
633
643
|
}
|
|
634
644
|
} catch (err) {
|
|
635
645
|
log('error', `poll failed: ${err.message}`);
|
|
646
|
+
// #1018: a single pass that failed is a failed run, not a quiet success.
|
|
647
|
+
if (options.once) process.exitCode = 1;
|
|
636
648
|
if (continuous && waitMode) {
|
|
637
649
|
waitFailures += 1;
|
|
638
650
|
if (waitFailures >= DEFAULT_WAIT_FAILURE_THRESHOLD) {
|