quoxcli 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +2 -0
- package/bin/quox.js +99 -126
- package/lib/api/endpoints/admin.js +26 -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/chat.js +7 -0
- package/lib/api/endpoints/contactsq.js +2 -0
- package/lib/api/endpoints/conversations.js +14 -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 +30 -0
- package/lib/api/endpoints/inbox.js +20 -0
- package/lib/api/endpoints/index.js +14 -0
- package/lib/api/endpoints/memory.js +19 -0
- package/lib/api/endpoints/mind.js +13 -0
- package/lib/api/endpoints/mon.js +6 -0
- package/lib/api/endpoints/objectives.js +6 -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 +6 -0
- package/lib/api/endpoints/router.js +17 -0
- package/lib/api/endpoints/safety.js +7 -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 +86 -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 +61 -0
- package/lib/commands/admin.js +16 -0
- package/lib/commands/agent.js +163 -27
- package/lib/commands/agentAttach.js +94 -6
- package/lib/commands/approvals.js +73 -1
- package/lib/commands/aside.js +24 -0
- package/lib/commands/ask.js +144 -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/brain.js +105 -14
- package/lib/commands/build.js +19 -5
- package/lib/commands/chat.js +57 -2
- package/lib/commands/claude.js +34 -8
- package/lib/commands/contacts.js +49 -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 +17 -6
- package/lib/commands/fetchAllowlist.js +71 -0
- package/lib/commands/file.js +38 -1
- package/lib/commands/fleet.js +311 -4
- package/lib/commands/governance.js +12 -3
- package/lib/commands/gripe.js +44 -13
- package/lib/commands/hat.js +178 -9
- package/lib/commands/helpers.js +18 -1
- package/lib/commands/hyperion.js +58 -0
- package/lib/commands/inbox.js +74 -23
- package/lib/commands/licence.js +14 -155
- 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 +11 -0
- package/lib/commands/offload.js +89 -0
- package/lib/commands/onboard.js +2 -1
- package/lib/commands/org.js +11 -1
- 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 +159 -0
- package/lib/commands/qlarity.js +19 -3
- package/lib/commands/quoxchat.js +13 -4
- package/lib/commands/quoxseo.js +38 -0
- package/lib/commands/reach.js +2 -1
- package/lib/commands/router.js +77 -0
- package/lib/commands/rows.js +358 -0
- package/lib/commands/run.js +64 -9
- package/lib/commands/server.js +292 -0
- package/lib/commands/setup.js +33 -7
- package/lib/commands/skills.js +48 -5
- 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/core/actingAs.js +2 -2
- package/lib/core/activeSummary.js +186 -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 +144 -35
- package/lib/core/commandRegistrars.js +121 -0
- package/lib/core/config.js +142 -24
- package/lib/core/customHats.js +168 -0
- package/lib/core/doneEngineFetch.js +8 -0
- package/lib/core/errors.js +329 -20
- package/lib/core/global-flags.js +105 -5
- package/lib/core/hyperion.js +102 -0
- package/lib/core/inboxResolution.js +65 -0
- package/lib/core/lazyCommands.js +4 -1
- package/lib/core/offload.js +504 -0
- package/lib/core/org-override.js +1 -1
- package/lib/core/orgResolve.js +70 -0
- package/lib/core/output.js +4 -4
- 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/sessionsOverview.js +64 -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 +361 -0
- package/lib/core/vmTui.js +86 -0
- package/lib/core/walkthrough.js +35 -0
- package/lib/hyperion/deck.js +29 -0
- package/lib/lucid/LucidCard.jsx +68 -0
- package/lib/lucid/LucidIntro.jsx +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/activity.js +14 -2
- package/lib/quoxon/aocl.js +117 -0
- package/lib/quoxon/api.js +504 -25
- package/lib/quoxon/app.js +3000 -230
- package/lib/quoxon/approvalFacts.js +94 -0
- package/lib/quoxon/approvalsView.js +325 -0
- package/lib/quoxon/asideView.js +40 -0
- package/lib/quoxon/ask.js +121 -0
- package/lib/quoxon/away.js +52 -0
- package/lib/quoxon/bang.js +224 -0
- package/lib/quoxon/bulkRisk.js +17 -0
- package/lib/quoxon/capabilities.js +13 -6
- package/lib/quoxon/cells.js +53 -0
- package/lib/quoxon/commandShort.js +112 -0
- package/lib/quoxon/commands.js +363 -38
- 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 +11 -0
- package/lib/quoxon/demo.autoplay.js +29 -0
- package/lib/quoxon/demo.fixtures.js +1 -1
- package/lib/quoxon/demo.js +44 -3
- package/lib/quoxon/devObserver.js +20 -0
- package/lib/quoxon/dividerAocl.js +191 -0
- package/lib/quoxon/dividerRule.js +57 -0
- package/lib/quoxon/dragonModes.js +150 -0
- package/lib/quoxon/dragonView.js +41 -0
- package/lib/quoxon/earlyInput.js +40 -0
- package/lib/quoxon/echo.js +15 -0
- package/lib/quoxon/egg.js +2 -2
- package/lib/quoxon/events.js +244 -25
- 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/indicator.js +6 -2
- package/lib/quoxon/intent.js +12 -1
- package/lib/quoxon/keys.js +49 -6
- package/lib/quoxon/launcher.js +103 -0
- package/lib/quoxon/main.js +92 -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 +211 -0
- package/lib/quoxon/noai.js +57 -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 +24 -3
- package/lib/quoxon/objects/mirror.js +137 -0
- package/lib/quoxon/objects/peek.js +17 -0
- package/lib/quoxon/objects/prove.js +130 -0
- package/lib/quoxon/objects/replay.js +125 -0
- package/lib/quoxon/objects/sitrep.js +58 -0
- package/lib/quoxon/objects/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/view.js +76 -19
- package/lib/quoxon/orgLabel.js +10 -0
- package/lib/quoxon/orgSwitch.js +33 -0
- package/lib/quoxon/ownBroadcast.js +22 -0
- package/lib/quoxon/palette.js +43 -0
- package/lib/quoxon/print.js +9 -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 +61 -0
- package/lib/quoxon/qdragonScenes.js +282 -0
- package/lib/quoxon/qeyes.js +213 -0
- package/lib/quoxon/qmotion.js +58 -37
- package/lib/quoxon/render.js +638 -119
- package/lib/quoxon/resend.js +72 -0
- package/lib/quoxon/roster.js +95 -9
- package/lib/quoxon/route.js +6 -5
- package/lib/quoxon/rows.js +711 -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/session.js +172 -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 +53 -0
- package/lib/quoxon/slash/cliLeaves.json +948 -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/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 +21 -7
- 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/restart.js +71 -13
- 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/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 +59 -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/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 +89 -0
- package/lib/quoxon/windowOrg.js +113 -0
- package/lib/server/diag.js +129 -0
- package/lib/server/doctor.js +71 -0
- package/lib/server/install.js +149 -0
- package/lib/server/pluginEnable.js +161 -0
- package/lib/server/skew.js +56 -0
- package/lib/server/snapshot.js +131 -0
- package/lib/server/status.js +128 -0
- package/lib/server/sys.js +65 -0
- package/lib/server/uninstall.js +63 -0
- package/lib/server/update.js +234 -0
- package/lib/toolOutputShape.js +315 -0
- package/lib/tui/views/HyperionDeck.jsx +79 -0
- package/lib/tui/views/LucidDeck.jsx +209 -217
- package/package.json +4 -1
- package/lib/core/websiteSession.js +0 -148
package/lib/quoxon/main.js
CHANGED
|
@@ -5,119 +5,22 @@ import { App } from './app.js';
|
|
|
5
5
|
import { inkRenderOptions, inkStdout } from './term.js';
|
|
6
6
|
import { createDemoApi } from './demo.js';
|
|
7
7
|
import { DEMO_LABEL } from './demo.fixtures.js';
|
|
8
|
-
import {
|
|
8
|
+
import { AUTOPLAY_LABEL, autoplayOn } from './demo.autoplay.js';
|
|
9
|
+
import { captureEarlyInput } from './earlyInput.js';
|
|
9
10
|
import { shouldShowWelcome, markWelcomeSeen } from './welcome.js';
|
|
10
11
|
import { loadRules, rulesContext } from './rules.js';
|
|
11
12
|
import { createTranscript } from './transcript.js';
|
|
12
|
-
import {
|
|
13
|
+
import { createScreenRecorder, screenFramesEnabled } from './screenFrames.js';
|
|
13
14
|
import { applySettings } from './slash/settings.js';
|
|
14
|
-
import { headOf, RUNNING_ROOT, execRestart, checkRestart } from './slash/restart.js';
|
|
15
|
+
import { headOf, RUNNING_ROOT, execRestart, restartPlan, checkRestart, startBuildWatch, settleMsFrom, restoreTerminal, RESTART_SLOT_ENV, RESTART_ATTACH_ENV, RESTART_NOTE_ENV, RESTART_MODE_ENV } from './slash/restart.js';
|
|
16
|
+
import { readLastSeen, writeLastSeen } from './away.js';
|
|
17
|
+
import { fileHistory } from './promptHistory.js';
|
|
18
|
+
import { takeLauncherHandoff, writeRestartHandoff, RESTART_EXIT_CODE } from './launcher.js';
|
|
15
19
|
|
|
16
|
-
|
|
17
|
-
|
|
20
|
+
export { parseArgs, USAGE, pickProfile, choosePick, chatCredentialProblem, noServerConfigured, printAuthMessage } from './headless.js';
|
|
21
|
+
import { parseArgs, USAGE, noServerConfigured, mainPrint, pickProfile, chatCredentialProblem } from './headless.js';
|
|
18
22
|
|
|
19
|
-
export
|
|
20
|
-
const o = { profile: null, verbose: 'on', demo: false, help: false, print: false, prompt: null, output: 'text', conversation: null, turnId: null, timeout: 0, color: false, noClear: false, name: null, serviceKey: false, demoOnboarding: false, provider: null, resume: null, verboseSet: false, errors: [] };
|
|
21
|
-
let rawProvider = null, rawModel = null;
|
|
22
|
-
const value = (flag, i) => { const v = argv[i + 1]; if (v === undefined || (v.startsWith('-') && isFlag(v))) { o.errors.push(`${flag} needs a value`); return undefined; } return v; };
|
|
23
|
-
for (let i = 0; i < argv.length; i++) {
|
|
24
|
-
const a = argv[i];
|
|
25
|
-
if (a === '--') { const rest = argv.slice(i + 1).join(' '); if (rest) o.prompt = rest; break; }
|
|
26
|
-
if (a === '-p' || a === '--profile') { const v = value(a, i); if (v !== undefined) { o.profile = v; i++; } }
|
|
27
|
-
else if (a.startsWith('--profile=')) o.profile = a.slice(10);
|
|
28
|
-
else if (a === '--verbose') { o.verboseSet = true; const v = argv[i + 1]; if (['off', 'on', 'full'].includes(v)) { o.verbose = v; i++; } else o.verbose = 'on'; } // no value means on
|
|
29
|
-
else if (a.startsWith('--verbose=')) { o.verboseSet = true; o.verbose = a.slice(10); }
|
|
30
|
-
else if (a === '--resume') { const v = argv[i + 1]; if (v !== undefined && !v.startsWith('-')) { o.resume = v; i++; } else o.resume = true; } // alone: the picker
|
|
31
|
-
else if (a.startsWith('--resume=')) o.resume = a.slice(9) || true;
|
|
32
|
-
else if (a === '-n' || a === '--name') { const v = value(a, i); if (v !== undefined) { o.name = v; i++; } }
|
|
33
|
-
else if (a.startsWith('--name=')) o.name = a.slice(7);
|
|
34
|
-
else if (a === '--demo') o.demo = true;
|
|
35
|
-
else if (a === '--demo-onboarding') o.demoOnboarding = true;
|
|
36
|
-
else if (a === '--print') { o.print = true; const n = argv[i + 1]; if (n !== undefined && !isFlag(n)) { o.prompt = n; i++; } }
|
|
37
|
-
else if (a.startsWith('--print=')) { o.print = true; o.prompt = a.slice(8); }
|
|
38
|
-
else if (a === '--output') { const v = value(a, i); if (v !== undefined) { o.output = v; i++; } }
|
|
39
|
-
else if (a.startsWith('--output=')) o.output = a.slice(9);
|
|
40
|
-
else if (a === '--conversation') { const v = value(a, i); if (v !== undefined) { o.conversation = v; i++; } }
|
|
41
|
-
else if (a === '--turn-id') { const v = value(a, i); if (v !== undefined) { o.turnId = v; i++; } }
|
|
42
|
-
else if (a === '--timeout') { const v = value(a, i); if (v !== undefined) { o.timeout = Number(v) || 0; i++; } }
|
|
43
|
-
else if (a === '--color') o.color = true;
|
|
44
|
-
else if (a === '--no-clear') o.noClear = true;
|
|
45
|
-
else if (a === '--service-key') o.serviceKey = true;
|
|
46
|
-
else if (a === '--provider' || a === '--model') { const v = value(a, i); if (v !== undefined) { if (a === '--provider') rawProvider = v; else rawModel = v; i++; } }
|
|
47
|
-
else if (a.startsWith('--provider=')) rawProvider = a.slice(11);
|
|
48
|
-
else if (a.startsWith('--model=')) rawModel = a.slice(8);
|
|
49
|
-
else if (a === '--prompt-profile') { const v = value(a, i); if (v !== undefined) { o.promptProfile = v; i++; } }
|
|
50
|
-
else if (a.startsWith('--prompt-profile=')) o.promptProfile = a.slice(17);
|
|
51
|
-
else if (a === '-h' || a === '--help') o.help = true;
|
|
52
|
-
else if (!a.startsWith('-') && o.prompt === null && argv.includes('--print')) o.prompt = a; // `--print` given earlier or later; this is its prompt
|
|
53
|
-
else o.errors.push(a.startsWith('-') ? `unknown option ${a}` : `unexpected argument ${a}`);
|
|
54
|
-
}
|
|
55
|
-
if (rawModel && !rawProvider) o.errors.push('--model needs --provider (e.g. --provider openai --model gpt-5.4)');
|
|
56
|
-
else if (rawProvider) {
|
|
57
|
-
const m = parseModelArg(rawModel ? `${rawProvider} ${rawModel}` : rawProvider);
|
|
58
|
-
if (!m.ok) o.errors.push(m.error); else o.provider = m.choice || 'auto'; // 'auto' = this run ignores the saved /model choice
|
|
59
|
-
}
|
|
60
|
-
if (o.promptProfile !== undefined && !['compact', 'full'].includes(o.promptProfile)) { o.errors.push(`invalid --prompt-profile "${o.promptProfile}" (use compact or full)`); delete o.promptProfile; }
|
|
61
|
-
if (!['off', 'on', 'full'].includes(o.verbose)) o.verbose = 'on';
|
|
62
|
-
if (!['text', 'json'].includes(o.output)) { o.errors.push(`invalid --output "${o.output}" (use text or json)`); o.output = 'text'; }
|
|
63
|
-
return o;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export const USAGE = 'quox [-p|--profile <name>] [-n|--name <session name>] [--conversation <id>] [--resume [id|name]] [--verbose off|on|full] [--demo] [--no-clear] [--service-key] [--provider openai|anthropic|ollama|openrouter|huggingface|groq|together|lmstudio|vllm|custom|auto [--model <id>]] (quoxon is an alias; the old menu is `quox tui`)\n quox --conversation <id> resume that conversation interactively (its saved turns load into the window)\n quox --resume [id|name] pick up a recent conversation; alone it opens a picker\n quox --print "<prompt>" [--output text|json] [--conversation <id>] [--turn-id <id>] [--timeout <s>] [--provider <p> [--model <id>]] [--prompt-profile compact|full] (prompt from stdin if absent or -; use -- before a prompt that starts with -)\n exit: 0 completed, 1 other, 2 held for approval, 3 interrupted or abandoned, 4 not signed in, 5 ended without server confirmation\n --provider asks the server for that provider this run (default: your /model choice for this profile, else auto); turn.started from the server says what ran\n --service-key lets chat use an ambient QUOX_SERVICE_KEY when the profile has no login (never used silently)\n --demo plays a simulated journey on its own (no server, nothing real runs)\n --demo-onboarding walks Getting started with a dry-run install (nothing is downloaded, built or started)';
|
|
67
|
-
|
|
68
|
-
/** Default profile: a resident (agent) login cannot chat, so use the single logged-in human profile. */
|
|
69
|
-
export function pickProfile(explicit, cfgMod) {
|
|
70
|
-
if (explicit) return { name: explicit, note: null };
|
|
71
|
-
const probe = cfgMod.loadConfig();
|
|
72
|
-
const active = probe.getActiveProfileName?.();
|
|
73
|
-
const residents = new Set((probe.listResidentProfiles?.() || []).map(r => r.name));
|
|
74
|
-
return choosePick(active, residents, probe.listProfiles?.() || []);
|
|
75
|
-
}
|
|
76
|
-
export function choosePick(active, residents, profiles) {
|
|
77
|
-
if (!residents.has(active)) return { name: undefined, note: null };
|
|
78
|
-
const humans = profiles.filter(p => !residents.has(p.name) && p.loggedIn);
|
|
79
|
-
if (humans.length === 1) return { name: humans[0].name, note: null }; // the status line shows the profile; no transcript noise
|
|
80
|
-
return { name: undefined, note: `The default profile "${active}" is an agent login and cannot chat. Pass -p <your profile>.` };
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Chat identity gate. A named profile that does not exist is an error (never created on the fly), and an
|
|
85
|
-
* ambient QUOX_SERVICE_KEY is only a credential when the user asked for it (--service-key or FORCE).
|
|
86
|
-
* -> null when chat may proceed, else { message }.
|
|
87
|
-
*/
|
|
88
|
-
export function chatCredentialProblem({ profileArg, profiles, hasStoredLogin, profileName, env = process.env, serviceKeyFlag = false }) {
|
|
89
|
-
if (profileArg && !profiles.some(p => p.name === profileArg)) return { message: `profile ${profileArg} does not exist; create it with: quox login -p ${profileArg}` };
|
|
90
|
-
if (hasStoredLogin) return null;
|
|
91
|
-
if ((serviceKeyFlag || env.QUOX_FORCE_SERVICE_KEY) && env.QUOX_SERVICE_KEY) return null;
|
|
92
|
-
return { message: `Not signed in for chat on profile ${profileName}. Run: quox login -p ${profileName}${env.QUOX_SERVICE_KEY ? ' (an ambient QUOX_SERVICE_KEY is ignored unless you pass --service-key)' : ''}` };
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Is this a machine with no Quox server configured at all? Then bare `quox` is Getting started, and --print
|
|
97
|
-
* says how to get there. Reads profiles only (no network).
|
|
98
|
-
*/
|
|
99
|
-
export async function noServerConfigured(opts, cfgMod, env = process.env) {
|
|
100
|
-
const { needsGettingStarted } = await import('./onboarding/detect.js');
|
|
101
|
-
const { loadJourney } = await import('./onboarding/journey.js');
|
|
102
|
-
const probe = cfgMod.loadConfig();
|
|
103
|
-
return needsGettingStarted({ profiles: probe.listProfiles?.() || [], residents: probe.listResidentProfiles?.() || [], profileArg: opts.profile, env, serviceKeyFlag: opts.serviceKey, journey: loadJourney() });
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/** --print without a usable login: today's message, plus the way in when no server exists yet. */
|
|
107
|
-
export function printAuthMessage(problem, gettingStarted, hint) {
|
|
108
|
-
return `quox: ${problem.message}\n${gettingStarted ? `quox: ${hint}\n` : ''}`;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/** Keys typed before the app is ready: raw mode now, so nothing is cooked-echoed, buffered for the composer. */
|
|
112
|
-
export function captureEarlyInput(stdin = process.stdin, stdout = process.stdout) {
|
|
113
|
-
if (!stdin?.isTTY) return null;
|
|
114
|
-
const chunks = [];
|
|
115
|
-
const fn = (d) => chunks.push(typeof d === 'string' ? d : d.toString('utf8'));
|
|
116
|
-
try { stdin.setRawMode?.(true); stdin.resume?.(); } catch { /* not a tty after all */ }
|
|
117
|
-
stdin.on('data', fn);
|
|
118
|
-
if (stdout?.isTTY) stdout.write(PASTE_ON);
|
|
119
|
-
return { chunks, detach: () => stdin.off('data', fn) };
|
|
120
|
-
}
|
|
23
|
+
export { captureEarlyInput } from './earlyInput.js';
|
|
121
24
|
|
|
122
25
|
/**
|
|
123
26
|
* Clear the screen before the chat client? Never right after Getting started: its finale (the one-time
|
|
@@ -129,13 +32,14 @@ export function shouldClearBeforeChat({ isTTY, noClear, fromGettingStarted }) {
|
|
|
129
32
|
|
|
130
33
|
export async function main(argv = process.argv.slice(2)) {
|
|
131
34
|
const opts = parseArgs(argv);
|
|
132
|
-
if (opts.help) { console.log(USAGE); return; }
|
|
133
|
-
if (opts.errors.length) { process.stderr.write(`quox: ${opts.errors.join('; ')}\n${USAGE}\n`); process.exitCode = 1; return; }
|
|
134
35
|
// Raw mode FIRST: the awaits below (module loads, config, health) must not leave the tty cooked.
|
|
36
|
+
// bin/quox.js already turned it on before loading anything heavy; this returns that same capture.
|
|
135
37
|
const early = !opts.print ? captureEarlyInput() : null;
|
|
38
|
+
if (opts.help) { early?.release(); console.log(USAGE); return; }
|
|
39
|
+
if (opts.errors.length) { early?.release(); process.stderr.write(`quox: ${opts.errors.join('; ')}\n${USAGE}\n`); process.exitCode = 1; return; }
|
|
136
40
|
const cwd = process.cwd().replace(process.env.HOME || '\0', '~');
|
|
137
41
|
const { resolveCliVersion } = await import('../core/version.js');
|
|
138
|
-
let version = '0.
|
|
42
|
+
let version = '0.3.1';
|
|
139
43
|
try { version = resolveCliVersion?.() || version; } catch { /* keep default */ }
|
|
140
44
|
if (opts.print) return mainPrint(opts);
|
|
141
45
|
let replay = null;
|
|
@@ -156,8 +60,10 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
156
60
|
let props;
|
|
157
61
|
let restart = null; // set by /restart: run the installed quox here once the app has let go of the terminal
|
|
158
62
|
if (opts.demo) {
|
|
63
|
+
const auto = autoplayOn(opts.autoplay) ? { exitAfterMs: opts.autoplayExit != null ? opts.autoplayExit * 1000 : (process.env.QUOX_DEMO_AUTOPLAY_EXIT ? Number(process.env.QUOX_DEMO_AUTOPLAY_EXIT) * 1000 : null) } : null;
|
|
159
64
|
// /restart works in the demo too (a fresh demo, nothing to resume): the same hand-over, testable with no server
|
|
160
|
-
|
|
65
|
+
// QUOX_DEMO_DECISIONS: demo-only pty test seam for the walkthrough proofs (a decisions state file; never a real server)
|
|
66
|
+
props = { api: createDemoApi({ objects: true, decisionsFile: process.env.QUOX_DEMO_DECISIONS || null }), profile: 'demo', org: 'demo-org', demoLabel: auto ? AUTOPLAY_LABEL : DEMO_LABEL, autoplay: auto ? null : 'Show me what you can do (demo)', demoAuto: auto, onRestart: (r) => { restart = { ...r, conversationId: null }; }, onDemo: () => { restart = { demo: true }; }, restartCheck: checkRestart };
|
|
161
67
|
} else {
|
|
162
68
|
const cfgMod = await import('../core/config.js');
|
|
163
69
|
const { createAuth } = await import('../core/auth.js');
|
|
@@ -168,77 +74,103 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
168
74
|
const missing = opts.profile && !profiles.some(p => p.name === opts.profile);
|
|
169
75
|
const pick = pickProfile(opts.profile, cfgMod);
|
|
170
76
|
const config = missing ? probe : cfgMod.loadConfig(pick.name);
|
|
171
|
-
const
|
|
172
|
-
const
|
|
173
|
-
const api = createApi({
|
|
77
|
+
const { createWindowClient, carryOrg, RESTART_ORG_ENV } = await import('./windowOrg.js');
|
|
78
|
+
const win = createWindowClient({ config, auth: createAuth(config), createClient }); // #1034: /org switch is window-scoped
|
|
79
|
+
const api = createApi({ ...win, config });
|
|
174
80
|
const pname = missing ? opts.profile : (config.getActiveProfileName?.() || 'default');
|
|
175
81
|
const problem = chatCredentialProblem({ profileArg: opts.profile, profiles, hasStoredLogin: missing ? false : Boolean(config.getAuth?.()?.access_token), profileName: pname, serviceKeyFlag: opts.serviceKey });
|
|
82
|
+
// Z2H: a dead session (refresh rejected) or a cleared login on a human profile is ONE signed-out state, answered
|
|
83
|
+
// by a single card and /login, not scattered digest/approvals/chat errors. Transient failures read as alive.
|
|
84
|
+
let signedOut = null;
|
|
85
|
+
if (!missing && !opts.serviceKey) {
|
|
86
|
+
if (problem && !pick.note) signedOut = { reason: null };
|
|
87
|
+
else if (!problem) {
|
|
88
|
+
const pr = await Promise.race([api.probeSession(), new Promise(r => setTimeout(() => r({ alive: true }), 4000))]).catch(() => ({ alive: true }));
|
|
89
|
+
if (!pr.alive) signedOut = { reason: pr.reason || null };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
176
92
|
// The health probe runs inside the app (background); the UI is up and typing works immediately.
|
|
177
93
|
const startup = async () => {
|
|
94
|
+
if (signedOut) return null;
|
|
178
95
|
if (problem) return problem.message;
|
|
179
96
|
if (pick.note) return pick.note;
|
|
180
97
|
const hl = await api.health(8000);
|
|
181
98
|
return hl.ok ? null : hl.absent ? `No QuoxCORE running at ${hl.url}` : `QuoxCORE at ${hl.url} is not answering (${hl.status}); messages may be slow`;
|
|
182
99
|
};
|
|
100
|
+
// /restart hand-over: re-enter the predecessor window's org in this window only (the profile is untouched).
|
|
101
|
+
const carried = !missing && !signedOut && !problem ? await carryOrg(api, process.env) : (delete process.env.QUOX_RESTART_ORG, null);
|
|
102
|
+
const startupCarry = async () => (carried?.error ? `Could not re-enter org ${carried.id} after the restart (${carried.error}). This window is in the profile's org.` : startup());
|
|
183
103
|
const saved = api.settings();
|
|
184
104
|
applySettings(saved);
|
|
185
|
-
if (!opts.verboseSet
|
|
186
|
-
|
|
105
|
+
if (!opts.verboseSet) opts.verbose = ['off', 'on', 'full'].includes(saved.verbose) ? saved.verbose : 'off'; // protocol lines are opt-in: /verbose on
|
|
106
|
+
// PROFILE-SWITCH: build a signed-in profile's api for an in-place window switch (the App only asks after a y or
|
|
107
|
+
// /profile). The window org rides along; the profile file's default is never moved (loadConfig(name) is a lens).
|
|
108
|
+
const residentNames = () => (cfgMod.loadConfig().listResidentProfiles?.() || []).map(r => r.name);
|
|
109
|
+
const switchProfile = async (name, { org: winOrg = null } = {}) => {
|
|
110
|
+
const row = (cfgMod.loadConfig().listProfiles?.() || []).find(p => p.name === name);
|
|
111
|
+
if (!row || !row.loggedIn) return { ok: false, reason: 'not_signed_in' };
|
|
112
|
+
const c2 = cfgMod.loadConfig(name);
|
|
113
|
+
const a2 = createApi({ ...createWindowClient({ config: c2, auth: createAuth(c2), createClient }), config: c2 });
|
|
114
|
+
const pr = await Promise.race([a2.probeSession(), new Promise(r => setTimeout(() => r({ alive: true }), 4000))]).catch(() => ({ alive: true }));
|
|
115
|
+
if (!pr.alive) return { ok: false, reason: 'not_signed_in' };
|
|
116
|
+
const c = winOrg ? await carryOrg(a2, { [RESTART_ORG_ENV]: winOrg }) : null;
|
|
117
|
+
const note = c?.error ? `Could not re-enter org ${winOrg} (${c.error}); this window is in ${name}'s org.` : null;
|
|
118
|
+
return { ok: true, api: a2, agentLogin: residentNames().includes(name), orgName: (c && !c.error && c.name) || c2.getAuth?.()?.org_name || null, note };
|
|
119
|
+
};
|
|
120
|
+
const loginCandidates = profiles.filter(p => !p.loggedIn && !residentNames().includes(p.name)).map(p => p.name);
|
|
121
|
+
props = { api, switchProfile, loginCandidates, away: { read: readLastSeen, write: writeLastSeen }, modelChoice: opts.provider, resume: opts.resume ?? null, onRestart: (r) => { restart = r; }, onDemo: () => { restart = { demo: true }; }, restartCheck: checkRestart, buildWatch: (o) => startBuildWatch({ ...o, settleMs: settleMsFrom() }), startSha: headOf(RUNNING_ROOT), rules: loadRules(), profile: pname, agentLogin: (probe.listResidentProfiles?.() || []).some(r => r.name === pname), humanProfiles: profiles.filter(p => p.loggedIn && !(probe.listResidentProfiles?.() || []).some(r => r.name === p.name)).map(p => p.name), org: missing ? null : ((carried && !carried.error && carried.name) || config.getAuth?.()?.org_name || null), startup: startupCarry, healthRecheck: () => api.health(8000), blocked: problem && !signedOut ? problem.message : null, signedOut, autoplay: replay };
|
|
187
122
|
}
|
|
188
123
|
// Interactive start: a clean screen under the intro (never when piped; NO_COLOR still clears),
|
|
189
124
|
// except straight after Getting started, whose finale must stay readable.
|
|
190
125
|
if (shouldClearBeforeChat({ isTTY: process.stdout.isTTY, noClear: opts.noClear, fromGettingStarted })) process.stdout.write('\x1b[2J\x1b[3J\x1b[H');
|
|
126
|
+
// Z2H-R5 C: do not advertise /slot where the licence check says no (the server's own capabilities answer, short wait).
|
|
127
|
+
let slotsAvailable = true;
|
|
128
|
+
if (!opts.demo && process.stdout.isTTY && props.api?.capabilities && !props.signedOut && !props.blocked) {
|
|
129
|
+
try {
|
|
130
|
+
const caps = await Promise.race([props.api.capabilities(), new Promise(res => setTimeout(() => res(null), 2000))]);
|
|
131
|
+
const c = caps?.ok ? caps.capabilities.find(x => x.id === 'slots') : null;
|
|
132
|
+
if (c && c.availability === 'unavailable' && /not in your plan/i.test(String(c.reason || ''))) slotsAvailable = false;
|
|
133
|
+
} catch { /* unknown: leave the invite as it was */ }
|
|
134
|
+
}
|
|
191
135
|
const welcome = shouldShowWelcome({ print: opts.print });
|
|
192
136
|
if (welcome) markWelcomeSeen();
|
|
193
137
|
const transcript = opts.demo ? null : createTranscript({ meta: { version, cols: process.stdout.columns, rows: process.stdout.rows, profile: props.profile, org: props.org, cwd } });
|
|
194
138
|
const onResize = () => transcript?.resize(process.stdout.columns, process.stdout.rows);
|
|
195
139
|
if (transcript) process.stdout.on('resize', onResize);
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
140
|
+
// SCREEN-FRAMES: what the screen showed, settled, into the same local transcript (QUOX_SCREEN_FRAMES=0 turns it off).
|
|
141
|
+
const screenRec = transcript && process.stdout.isTTY && screenFramesEnabled(process.env)
|
|
142
|
+
? createScreenRecorder({ write: (rec) => transcript.screen(rec), cols: () => process.stdout.columns, rows: () => process.stdout.rows })
|
|
143
|
+
: null;
|
|
144
|
+
const restartSlot = opts.demo ? null : (process.env[RESTART_SLOT_ENV] || null); delete process.env[RESTART_SLOT_ENV]; // one hand-over only
|
|
145
|
+
const restartAttach = opts.demo ? null : (process.env[RESTART_ATTACH_ENV] || null); delete process.env[RESTART_ATTACH_ENV];
|
|
146
|
+
const restartMode = opts.demo ? null : (process.env[RESTART_MODE_ENV] === 'plan' ? 'plan' : null); delete process.env[RESTART_MODE_ENV]; // PLAN MODE: window-scoped, carried once
|
|
147
|
+
const restartNote = opts.demo ? null : (process.env[RESTART_NOTE_ENV] || null); delete process.env[RESTART_NOTE_ENV]; // e.g. a failed /update, said once in the restarted client
|
|
148
|
+
if (restartNote && !props.introNote) props.introNote = restartNote;
|
|
149
|
+
const app = render(React.createElement(App, { ...props, restartSlot, restartAttach, ...(restartMode ? { mode: restartMode } : {}), welcome, tipStartup: !fromGettingStarted && !opts.demo && Boolean(process.stdout.isTTY), whatsNewStartup: !opts.demo && Boolean(process.stdout.isTTY), version, cwd, verbose: opts.verbose, earlyInput: early, sessionName: opts.name, resumeConversation: opts.conversation || null, invite: Boolean(process.stdout.isTTY) && !opts.name, slotsAvailable, transcript, historyStore: opts.demo ? null : fileHistory() }), inkRenderOptions(process.stdout, { tap: screenRec?.tap }));
|
|
150
|
+
try { await app.waitUntilExit(); screenRec?.stop(); transcript?.end(restart ? 'restart' : 'exit'); }
|
|
151
|
+
catch (e) { screenRec?.stop(); transcript?.end(`crash: ${e?.message || e}`); throw e; }
|
|
152
|
+
finally { screenRec?.stop(); process.stdout.off('resize', onResize); }
|
|
200
153
|
if (!restart) return;
|
|
154
|
+
if (restart.update) { // /update said y: the update owns the terminal (npm prompts, sudo), then the same restart either way
|
|
155
|
+
const { runUpdate } = await import('../core/selfUpdate.js');
|
|
156
|
+
const plan = restart.update;
|
|
157
|
+
restoreTerminal();
|
|
158
|
+
process.stdout.write(`\nUpdating quox: ${plan.command.display}\n`);
|
|
159
|
+
const u = runUpdate(plan);
|
|
160
|
+
if (u.ok) process.stdout.write(`\u2714 Update finished (exit 0). Restarting into this conversation\u2026\n`);
|
|
161
|
+
else {
|
|
162
|
+
const note = `Update failed: ${plan.command.display} ${u.error ? `could not start (${u.error})` : `exited ${u.code}`}. Restarting on the build that is installed now. Run it yourself to see why: ${plan.manual}`;
|
|
163
|
+
process.stdout.write(`\u2716 ${note}\n`);
|
|
164
|
+
process.env[RESTART_NOTE_ENV] = note; // the restarted client (same build) says it again in the conversation
|
|
165
|
+
}
|
|
166
|
+
}
|
|
201
167
|
// The app preflighted the installed quox before letting go, so a failure here is rare (it changed in between).
|
|
202
|
-
const
|
|
168
|
+
const how = { argv: restart.demo ? ['--demo', '--autoplay'] : argv, conversationId: restart.demo ? null : restart.conversationId, profile: opts.demo ? null : (restart.profile || props.profile), name: restart.name || opts.name || null, slot: restart.demo ? null : (restart.slot || null), attach: restart.demo ? null : (restart.attach || null), org: restart.demo ? null : (restart.org || null), mode: restart.demo ? null : (restart.mode || null), bin: restart.bin || undefined };
|
|
169
|
+
// QUOXON-RESTART-LEAK: under the launcher this process EXITS and the launcher runs the successor, so exactly one
|
|
170
|
+
// client stays alive per terminal. Without one (QUOX_NO_LAUNCHER=1) it falls back to waiting on a nested child.
|
|
171
|
+
const launched = takeLauncherHandoff();
|
|
172
|
+
if (launched && writeRestartHandoff(launched, restartPlan(how))) { restoreTerminal(); process.exitCode = RESTART_EXIT_CODE; return; }
|
|
173
|
+
const r = execRestart(how);
|
|
203
174
|
if (r.error) { process.stderr.write(`quox: could not restart (${r.bin}: ${r.error})${restart.conversationId ? `; resume with: quox --resume ${restart.conversationId}` : ''}\n`); process.exitCode = 1; return; }
|
|
204
175
|
process.exitCode = r.code;
|
|
205
176
|
}
|
|
206
|
-
|
|
207
|
-
async function mainPrint(opts) {
|
|
208
|
-
const { runPrint, resolvePrompt, EXIT } = await import('./print.js');
|
|
209
|
-
const out = (s) => process.stdout.write(s);
|
|
210
|
-
const err = (s) => process.stderr.write(s);
|
|
211
|
-
const prompt = await resolvePrompt(opts.prompt, process.stdin);
|
|
212
|
-
let api;
|
|
213
|
-
if (opts.demo) api = createDemoApi({ speed: 20 });
|
|
214
|
-
else {
|
|
215
|
-
const cfgMod = await import('../core/config.js');
|
|
216
|
-
const { createAuth } = await import('../core/auth.js');
|
|
217
|
-
const { createClient } = await import('../core/client.js');
|
|
218
|
-
const { createApi } = await import('./api.js');
|
|
219
|
-
const probe = cfgMod.loadConfig();
|
|
220
|
-
const profiles = probe.listProfiles?.() || [];
|
|
221
|
-
const missing = opts.profile && !profiles.some(p => p.name === opts.profile);
|
|
222
|
-
const pick = pickProfile(opts.profile, cfgMod);
|
|
223
|
-
if (pick.note) err(pick.note + '\n');
|
|
224
|
-
const config = missing ? probe : cfgMod.loadConfig(pick.name);
|
|
225
|
-
const auth = createAuth(config);
|
|
226
|
-
const pname = missing ? opts.profile : (config.getActiveProfileName?.() || 'default');
|
|
227
|
-
const problem = chatCredentialProblem({ profileArg: opts.profile, profiles, hasStoredLogin: missing ? false : Boolean(config.getAuth?.()?.access_token), profileName: pname, serviceKeyFlag: opts.serviceKey });
|
|
228
|
-
if (problem) {
|
|
229
|
-
const { NO_SERVER_PRINT } = await import('./onboarding/copy.js');
|
|
230
|
-
err(printAuthMessage(problem, await noServerConfigured(opts, cfgMod), NO_SERVER_PRINT));
|
|
231
|
-
process.exitCode = EXIT.auth; return;
|
|
232
|
-
}
|
|
233
|
-
api = createApi({ client: createClient(config, auth), config, auth });
|
|
234
|
-
if (opts.conversation && api.conversationExists) {
|
|
235
|
-
const c = await api.conversationExists(opts.conversation);
|
|
236
|
-
if (c.exists === false) { err(`quox: conversation not found: ${opts.conversation}\n`); process.exitCode = EXIT.error; return; }
|
|
237
|
-
if (c.exists === null) err(`quox: could not verify conversation ${opts.conversation} (${c.reason}); continuing\n`);
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
const rules = opts.demo ? null : loadRules();
|
|
241
|
-
for (const s of rules?.skipped || []) err(`quox: ignoring ${s.path}: ${s.reason}\n`);
|
|
242
|
-
const choice = opts.provider === 'auto' ? null : opts.provider || api.modelChoice?.() || null;
|
|
243
|
-
process.exitCode = await runPrint({ api, choice, promptProfile: opts.promptProfile, context: rulesContext(rules), prompt, output: opts.output, conversation: opts.conversation, turnId: opts.turnId || undefined, timeoutS: opts.timeout, color: opts.color, out, err });
|
|
244
|
-
}
|
package/lib/quoxon/markdown.js
CHANGED
|
@@ -13,23 +13,14 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import React from 'react';
|
|
15
15
|
import { Box, Text, Transform } from 'ink';
|
|
16
|
-
import { Lexer } from 'marked';
|
|
17
16
|
import { graphemes } from './textBuffer.js';
|
|
18
|
-
import { c, mono,
|
|
17
|
+
import { c, mono, gradient, bulletRole } from './theme.js';
|
|
19
18
|
|
|
20
19
|
const h = React.createElement;
|
|
21
20
|
|
|
22
|
-
|
|
21
|
+
import { lex, splitSettled } from './markdownSettle.js';
|
|
23
22
|
|
|
24
|
-
export
|
|
25
|
-
const tokens = lex(text);
|
|
26
|
-
let last = -1;
|
|
27
|
-
for (let i = tokens.length - 1; i >= 0; i--) if (tokens[i].type !== 'space') { last = i; break; }
|
|
28
|
-
if (last <= 0) return { settled: '', rest: text };
|
|
29
|
-
let off = 0;
|
|
30
|
-
for (let i = 0; i < last; i++) off += tokens[i].raw.length;
|
|
31
|
-
return { settled: text.slice(0, off), rest: text.slice(off) };
|
|
32
|
-
}
|
|
23
|
+
export { lex, splitSettled }; // QUOXON-LATENCY: defined in markdownSettle.js so --print never loads Ink
|
|
33
24
|
|
|
34
25
|
export function healInline(text) {
|
|
35
26
|
let t = text;
|
|
@@ -128,22 +119,55 @@ function inline(tokens = [], style = {}, key = 'i') {
|
|
|
128
119
|
const w = (s) => graphemes(s).length;
|
|
129
120
|
const padTo = (s, n) => s + ' '.repeat(Math.max(0, n - w(s)));
|
|
130
121
|
|
|
131
|
-
/**
|
|
122
|
+
/** Hard-wrap plain text to `width` columns at word boundaries (long words split). Never drops text. Always >= 1 line. */
|
|
123
|
+
export function wrapCell(text, width) {
|
|
124
|
+
const width_ = Math.max(1, width);
|
|
125
|
+
const out = [];
|
|
126
|
+
for (const para of String(text ?? '').split('\n')) {
|
|
127
|
+
let cur = '';
|
|
128
|
+
for (const word of para.split(/\s+/).filter(Boolean)) {
|
|
129
|
+
let g = graphemes(word).map(x => x.text);
|
|
130
|
+
if (cur && w(cur) + 1 + g.length <= width_) { cur += ' ' + word; continue; }
|
|
131
|
+
if (cur) { out.push(cur); cur = ''; }
|
|
132
|
+
while (g.length > width_) { out.push(g.slice(0, width_).join('')); g = g.slice(width_); }
|
|
133
|
+
cur = g.join('');
|
|
134
|
+
}
|
|
135
|
+
out.push(cur);
|
|
136
|
+
}
|
|
137
|
+
return out;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Plain-text table layout: { mode: 'grid'|'vertical', lines: [{ text, head?, rule? }] }. Pure, for tests.
|
|
142
|
+
* Grid when it fits, else vertical key/value blocks whose values wrap under a hanging indent. No cell is ever truncated
|
|
143
|
+
* (TUI-UX #3: the grounding warning named an id that sat in a clipped cell).
|
|
144
|
+
*/
|
|
132
145
|
export function layoutTable(header, rows, cols) {
|
|
133
146
|
const n = header.length;
|
|
134
147
|
const widths = header.map((hd, i) => Math.max(w(hd), ...rows.map(r => w(r[i] ?? '')), 1));
|
|
135
148
|
const total = widths.reduce((a, b) => a + b, 0) + 3 * (n - 1);
|
|
136
|
-
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
149
|
+
const gridLines = (ws) => {
|
|
150
|
+
const cellLines = (r) => {
|
|
151
|
+
const cells = r.map((cell, i) => wrapCell(cell ?? '', ws[i]));
|
|
152
|
+
const h_ = Math.max(...cells.map(x => x.length));
|
|
153
|
+
return Array.from({ length: h_ }, (_, k) => cells.map((x, i) => padTo(x[k] ?? '', ws[i])).join(' │ ').replace(/\s+$/, ''));
|
|
154
|
+
};
|
|
155
|
+
const rule = ws.map(x => '─'.repeat(x)).join('─┼─');
|
|
156
|
+
return [...cellLines(header).map(text => ({ text, head: true })), { text: rule, rule: true }, ...rows.flatMap(r => cellLines(r).map(text => ({ text })))];
|
|
157
|
+
};
|
|
158
|
+
if (total <= cols) return { mode: 'grid', lines: gridLines(widths) };
|
|
159
|
+
// vertical key: value, one block per row; values wrap under a hanging indent
|
|
142
160
|
const lines = [];
|
|
143
161
|
const keyW = Math.min(Math.max(...header.map(w)), Math.floor(cols / 3));
|
|
162
|
+
const valW = Math.max(8, cols - keyW - 2);
|
|
144
163
|
rows.forEach((r, ri) => {
|
|
145
164
|
if (ri) lines.push({ text: '' });
|
|
146
|
-
header.forEach((hd, i) =>
|
|
165
|
+
header.forEach((hd, i) => {
|
|
166
|
+
const keyLines = wrapCell(hd, keyW);
|
|
167
|
+
const val = wrapCell(r[i] ?? '', valW);
|
|
168
|
+
const count = Math.max(keyLines.length, val.length);
|
|
169
|
+
for (let k = 0; k < count; k++) lines.push({ text: `${padTo(keyLines[k] ?? '', keyW)} ${val[k] ?? ''}`.replace(/\s+$/, ''), key: k === 0 ? keyW : undefined });
|
|
170
|
+
});
|
|
147
171
|
});
|
|
148
172
|
return { mode: 'vertical', lines };
|
|
149
173
|
}
|
|
@@ -153,7 +177,7 @@ function Table({ t, cols }) {
|
|
|
153
177
|
const rows = t.rows.map(r => r.map(x => inlineText(x.tokens)));
|
|
154
178
|
const lay = layoutTable(header, rows, cols);
|
|
155
179
|
return h(Box, { flexDirection: 'column' }, ...lay.lines.map((l, i) => h(Text, {
|
|
156
|
-
key: i, bold: l.head, color: l.rule ? c('muted') : (l.head ? c('label') : undefined), wrap: '
|
|
180
|
+
key: i, bold: l.head, color: l.rule ? c('muted') : (l.head ? c('label') : undefined), wrap: 'wrap'
|
|
157
181
|
}, l.text || ' ')));
|
|
158
182
|
}
|
|
159
183
|
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The streaming split, without the renderer. events.js (and so `quox --print`) needs only
|
|
3
|
+
* splitSettled; importing it from markdown.js loaded React + Ink (~2 s cold) on every
|
|
4
|
+
* headless turn before the request was even sent (QUOXON-LATENCY). markdown.js re-exports
|
|
5
|
+
* both names, so the interactive client is unchanged.
|
|
6
|
+
*/
|
|
7
|
+
import { Lexer } from 'marked';
|
|
8
|
+
|
|
9
|
+
export const lex = (src) => new Lexer({ gfm: true, breaks: false }).lex(src);
|
|
10
|
+
|
|
11
|
+
export function splitSettled(text) {
|
|
12
|
+
const tokens = lex(text);
|
|
13
|
+
let last = -1;
|
|
14
|
+
for (let i = tokens.length - 1; i >= 0; i--) if (tokens[i].type !== 'space') { last = i; break; }
|
|
15
|
+
if (last <= 0) return { settled: '', rest: text };
|
|
16
|
+
let off = 0;
|
|
17
|
+
for (let i = 0; i < last; i++) off += tokens[i].raw.length;
|
|
18
|
+
return { settled: text.slice(0, off), rest: text.slice(off) };
|
|
19
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_comment": "Vendored from ~/cli.quox.ai/matrix/domains.json (the proof matrix, 40 domain ids). Source: https://github.com/quoxai/cli.quox.ai matrix/domains.json. Tips name a domain from this list so the matrix can ingest them. When the matrix becomes canonical this file is replaced by the matrix export.",
|
|
3
|
+
"domains": [
|
|
4
|
+
"chat",
|
|
5
|
+
"providers",
|
|
6
|
+
"approvals",
|
|
7
|
+
"receipts",
|
|
8
|
+
"safety",
|
|
9
|
+
"hats",
|
|
10
|
+
"fleet",
|
|
11
|
+
"proxmox",
|
|
12
|
+
"monitoring",
|
|
13
|
+
"lucid",
|
|
14
|
+
"memory",
|
|
15
|
+
"qortex",
|
|
16
|
+
"vault",
|
|
17
|
+
"agents",
|
|
18
|
+
"workflows",
|
|
19
|
+
"skills",
|
|
20
|
+
"objectives",
|
|
21
|
+
"mail",
|
|
22
|
+
"calendar",
|
|
23
|
+
"contacts",
|
|
24
|
+
"notes",
|
|
25
|
+
"actions",
|
|
26
|
+
"voice",
|
|
27
|
+
"quoxchat",
|
|
28
|
+
"seo",
|
|
29
|
+
"plan",
|
|
30
|
+
"signal",
|
|
31
|
+
"browser",
|
|
32
|
+
"lens",
|
|
33
|
+
"social",
|
|
34
|
+
"comms",
|
|
35
|
+
"sessions",
|
|
36
|
+
"code",
|
|
37
|
+
"security",
|
|
38
|
+
"spend",
|
|
39
|
+
"files",
|
|
40
|
+
"plugins",
|
|
41
|
+
"identity",
|
|
42
|
+
"install",
|
|
43
|
+
"compliance",
|
|
44
|
+
"mind"
|
|
45
|
+
]
|
|
46
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mention completion in the composer (MENTIONS). Typing `@` at the start of a word suggests agents, fleet hosts,
|
|
3
|
+
* tasks (objectives) and receipts (this session's own tool-call envelopes). The remote lists are fetched ONCE per
|
|
4
|
+
* session on the first `@`, then every keystroke filters the cache: no request per key. A failed fetch is quiet
|
|
5
|
+
* (that kind just has no rows) and is retried at most every RETRY_MS. Tab or Enter accepts, Esc closes.
|
|
6
|
+
*/
|
|
7
|
+
export const RETRY_MS = 30000;
|
|
8
|
+
export const KIND_ORDER = ['agent', 'host', 'task', 'receipt'];
|
|
9
|
+
|
|
10
|
+
/** The @word being typed at the end of the draft -> { start (index after '@'), partial } or null. */
|
|
11
|
+
export function mentionContext(draft) {
|
|
12
|
+
const t = String(draft ?? '');
|
|
13
|
+
const m = t.match(/(^|\s)@([^\s@]*)$/);
|
|
14
|
+
if (!m) return null;
|
|
15
|
+
return { start: t.length - m[2].length, partial: m[2] };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const clean = (s) => String(s ?? '').replace(/\s+/g, ' ').trim();
|
|
19
|
+
|
|
20
|
+
/** api -> [{ value, kind, label }] from the session's remote sources, never throwing; failures named per kind. */
|
|
21
|
+
export async function loadMentions(api) {
|
|
22
|
+
const kinds = {
|
|
23
|
+
agent: async () => (await api.agents()).map(a => ({ value: String(a.agent_id), kind: 'agent', label: a.name && a.name !== a.agent_id ? clean(a.name) : '' })),
|
|
24
|
+
host: async () => {
|
|
25
|
+
const none = { status: 'absent', value: [] };
|
|
26
|
+
const [inv, beat] = await Promise.all([api.mentionHosts, api.objects?.fleetNames].map((fn, i) => (fn ? Promise.allSettled([i ? api.objects.fleetNames() : api.mentionHosts()]).then(r => r[0]) : none)));
|
|
27
|
+
const tried = [inv, beat].filter(r => r.status !== 'absent');
|
|
28
|
+
if (tried.length && tried.every(r => r.status === 'rejected')) throw tried[0].reason;
|
|
29
|
+
const out = (inv.status === 'fulfilled' ? inv.value : []).map(h => ({ value: String(h.id), kind: 'host', label: clean([h.group, h.health].filter(Boolean).join(' · ')) }));
|
|
30
|
+
for (const h of beat.status === 'fulfilled' ? beat.value || [] : []) if (!out.some(o => o.value === String(h.value))) out.push({ value: String(h.value), kind: 'host', label: clean(h.desc) });
|
|
31
|
+
return out;
|
|
32
|
+
},
|
|
33
|
+
task: async () => ((await api.mentionTasks?.()) || []).map(o => ({ value: String(o.id), kind: 'task', label: clean(`${o.title || ''}${o.status ? ` (${o.status})` : ''}`) })),
|
|
34
|
+
};
|
|
35
|
+
const out = []; const failed = [];
|
|
36
|
+
await Promise.all(Object.entries(kinds).map(async ([k, fn]) => {
|
|
37
|
+
try { out.push(...(await fn()).filter(x => x.value && /^[\w.:-]+$/.test(x.value))); } catch { failed.push(k); }
|
|
38
|
+
}));
|
|
39
|
+
return { items: out, failed };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** This session's tool calls that carry an envelope id: their receipts, newest first. Local, no request. */
|
|
43
|
+
export function receiptMentions(items) {
|
|
44
|
+
const seen = new Set(); const out = [];
|
|
45
|
+
for (const b of [...(items || [])].reverse()) {
|
|
46
|
+
if (!b || b.kind !== 'tool' || !b.envelopeId || seen.has(b.envelopeId)) continue;
|
|
47
|
+
seen.add(b.envelopeId);
|
|
48
|
+
out.push({ value: String(b.envelopeId), kind: 'receipt', label: clean(`${b.tool || 'tool call'}${b.status ? ` ${b.status}` : ''}`) });
|
|
49
|
+
}
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Prefix on the value first, then a substring of the value or its label; agents, hosts, tasks, receipts in that order. */
|
|
54
|
+
export function rankMentions(items, partial, max = 50) {
|
|
55
|
+
const q = String(partial || '').toLowerCase();
|
|
56
|
+
const seen = new Set();
|
|
57
|
+
const uniq = (items || []).filter(i => i?.value && !seen.has(`${i.kind}:${i.value}`) && seen.add(`${i.kind}:${i.value}`));
|
|
58
|
+
const order = (a, b) => KIND_ORDER.indexOf(a.kind) - KIND_ORDER.indexOf(b.kind);
|
|
59
|
+
if (!q) return uniq.sort(order).slice(0, max);
|
|
60
|
+
const pre = uniq.filter(i => i.value.toLowerCase().startsWith(q)).sort(order);
|
|
61
|
+
const sub = uniq.filter(i => !i.value.toLowerCase().startsWith(q) && (i.value.toLowerCase().includes(q) || i.label.toLowerCase().includes(q))).sort(order);
|
|
62
|
+
const out = [...pre, ...sub];
|
|
63
|
+
return out.length === 1 && out[0].value.toLowerCase() === q ? [] : out.slice(0, max);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Accepting a mention: replace the partial word, add one space. */
|
|
67
|
+
export const applyMention = (draft, cx, value) => String(draft).slice(0, cx.start) + value + ' ';
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* One cache for the session. items() returns what is loaded (never blocks) and starts the single load the first
|
|
71
|
+
* time it is asked; a failed kind is retried no sooner than RETRY_MS. loads() counts fetches (tests, /status).
|
|
72
|
+
*/
|
|
73
|
+
export function createMentionCache({ load, refresh = () => {}, now = Date.now, retryMs = RETRY_MS }) {
|
|
74
|
+
let items = []; let state = 'idle'; let failedAt = 0; let loads = 0;
|
|
75
|
+
const start = () => {
|
|
76
|
+
state = 'loading'; loads++;
|
|
77
|
+
Promise.resolve().then(load).then(
|
|
78
|
+
(r) => { items = r?.items || []; state = r?.failed?.length ? 'partial' : 'ready'; if (state === 'partial') failedAt = now(); },
|
|
79
|
+
() => { state = 'failed'; failedAt = now(); }
|
|
80
|
+
).then(() => refresh());
|
|
81
|
+
};
|
|
82
|
+
return {
|
|
83
|
+
items() {
|
|
84
|
+
if (state === 'idle' || ((state === 'failed' || state === 'partial') && now() - failedAt >= retryMs)) start();
|
|
85
|
+
return items;
|
|
86
|
+
},
|
|
87
|
+
state: () => state,
|
|
88
|
+
loads: () => loads,
|
|
89
|
+
};
|
|
90
|
+
}
|