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
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* A modal list under the composer (the /resume picker, the /config card). While open it takes every key,
|
|
3
3
|
* like the /connect card: Up/Down select, Enter picks, Esc closes, Left/Right/Space go to onAdjust.
|
|
4
|
+
* Typing (any text but a Space the card uses) closes it and goes to the composer, so a slash command typed
|
|
5
|
+
* while it is open runs instead of being read as navigation (R3 D2). onEvent returns false for those keys.
|
|
4
6
|
*/
|
|
5
7
|
export function createPicker({ refresh = () => {} } = {}) {
|
|
6
8
|
let cur = null; // { title, rows, sel, hint, onPick, onAdjust, onClose, rowsFn }
|
|
@@ -13,7 +15,20 @@ export function createPicker({ refresh = () => {} } = {}) {
|
|
|
13
15
|
onEvent(e) {
|
|
14
16
|
if (!cur) return false;
|
|
15
17
|
const rows = cur.rowsFn ? cur.rowsFn() : cur.rows;
|
|
16
|
-
|
|
18
|
+
// onChar(char, sel): a card key such as f or d. true, or a new selection index = handled.
|
|
19
|
+
if (e.type === 'text' && cur.onChar) {
|
|
20
|
+
const r = cur.onChar(e.text, cur.sel);
|
|
21
|
+
if (r === true || typeof r === 'number') {
|
|
22
|
+
if (typeof r === 'number') {
|
|
23
|
+
const n = (cur.rowsFn ? cur.rowsFn() : cur.rows).length;
|
|
24
|
+
cur.sel = Math.max(0, Math.min(r, Math.max(0, n - 1)));
|
|
25
|
+
}
|
|
26
|
+
refresh();
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (e.type === 'paste' || (e.type === 'text' && !(e.text === ' ' && cur.onAdjust))) { close(); return false; } // typing goes to the composer
|
|
31
|
+
if (e.type !== 'key' && !(e.type === 'text' && e.text === ' ')) return true;
|
|
17
32
|
const name = e.type === 'text' ? 'space' : e.name;
|
|
18
33
|
if (name === 'escape' || name === 'ctrl-c') { close(); return true; }
|
|
19
34
|
if (name === 'up' && rows.length) cur.sel = (cur.sel - 1 + rows.length) % rows.length;
|
|
@@ -32,7 +47,10 @@ export function createPicker({ refresh = () => {} } = {}) {
|
|
|
32
47
|
const rows = cur.rowsFn ? cur.rowsFn() : cur.rows;
|
|
33
48
|
const max = 10;
|
|
34
49
|
const start = Math.max(0, Math.min(cur.sel - max + 1, rows.length - max));
|
|
35
|
-
|
|
50
|
+
const title = cur.titleFn ? cur.titleFn() : cur.title;
|
|
51
|
+
const more = Math.max(0, rows.length - max);
|
|
52
|
+
const view = rows.slice(start, start + max);
|
|
53
|
+
return { title, rows: view, sel: cur.sel - start, more, hint: cur.hint, empty: cur.empty };
|
|
36
54
|
}
|
|
37
55
|
};
|
|
38
56
|
}
|
|
@@ -65,14 +65,42 @@ export function newerBuild({ startSha, runningRoot = RUNNING_ROOT, env = process
|
|
|
65
65
|
return DIFFERENT_NOTICE; // unrelated or unknown history: say so, never claim "newer
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
/**
|
|
69
|
-
export function
|
|
70
|
-
|
|
68
|
+
/** The installed build's id (its CLI tree's git HEAD), read locally. null when it cannot be told. */
|
|
69
|
+
export function installedBuildId({ runningRoot = RUNNING_ROOT, env = process.env } = {}) {
|
|
70
|
+
return headOf(cliRootOf(findOnPath('quox', env)) || runningRoot);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Poll at most every `everyMs`; calls onNotice(text, buildId) once. -> stop().
|
|
75
|
+
* With `repeat`, keeps watching and calls onNotice again for each LATER installed build (a new id).
|
|
76
|
+
*/
|
|
77
|
+
// settleMs defaults to 0 here (announce at once); the TUI (main.js) passes settleMsFrom() so real sessions settle.
|
|
78
|
+
export const RESTART_SETTLE_MS = 120000; // a newer build must stay the installed one this long before it is announced
|
|
79
|
+
export const RESTART_BOOT_WINDOW_MS = 5 * 60 * 1000; // after a start, the settle window runs from the start
|
|
80
|
+
|
|
81
|
+
export function settleMsFrom(env = process.env) {
|
|
82
|
+
const n = Number(env.QUOX_RESTART_SETTLE_MS);
|
|
83
|
+
return env.QUOX_RESTART_SETTLE_MS != null && env.QUOX_RESTART_SETTLE_MS !== '' && Number.isFinite(n) && n >= 0 ? n : RESTART_SETTLE_MS;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function startBuildWatch({ onNotice, everyMs = 60000, check = newerBuild, repeat = false, buildId = installedBuildId, settleMs = 0, now = Date.now, ...opts } = {}) {
|
|
87
|
+
let last; // repeat mode: the last build announced
|
|
88
|
+
let seen; let seenAt = 0; let seenNotice = null; // repeat mode: the build waiting to settle, and when it last changed
|
|
89
|
+
const bootAt = now();
|
|
71
90
|
const id = setInterval(() => {
|
|
72
|
-
if (said) return;
|
|
73
91
|
let t = null;
|
|
74
92
|
try { t = check(opts); } catch { t = null; }
|
|
75
|
-
if (t)
|
|
93
|
+
if (!t) return;
|
|
94
|
+
let b = null;
|
|
95
|
+
try { b = buildId(opts); } catch { b = null; }
|
|
96
|
+
if (!repeat) { clearInterval(id); onNotice(t, b); return; }
|
|
97
|
+
if (b === last && last !== undefined) return;
|
|
98
|
+
if (b !== seen || seen === undefined) { seen = b; seenAt = now(); } // a new build resets the settle timer
|
|
99
|
+
seenNotice = t;
|
|
100
|
+
const t0 = now() - bootAt < RESTART_BOOT_WINDOW_MS ? Math.max(seenAt, bootAt) : seenAt;
|
|
101
|
+
if (now() - t0 < settleMs) return;
|
|
102
|
+
last = b;
|
|
103
|
+
onNotice(seenNotice, b);
|
|
76
104
|
}, everyMs);
|
|
77
105
|
id.unref?.();
|
|
78
106
|
return () => clearInterval(id);
|
|
@@ -84,15 +112,18 @@ export function startBuildWatch({ onNotice, everyMs = 60000, check = newerBuild,
|
|
|
84
112
|
*/
|
|
85
113
|
export function restartArgs(argv = [], conversationId = null, { profile = null, name = null } = {}) {
|
|
86
114
|
const out = [];
|
|
115
|
+
const pin = Boolean(profile) && !argv.includes('--demo'); // the profile the session RESOLVED wins over whatever argv said
|
|
87
116
|
for (let i = 0; i < argv.length; i++) {
|
|
88
117
|
const a = argv[i];
|
|
118
|
+
if (pin && (a === '-p' || a === '--profile')) { const n = argv[i + 1]; if (n !== undefined && !n.startsWith('-')) i++; continue; }
|
|
119
|
+
if (pin && a.startsWith('--profile=')) continue;
|
|
89
120
|
if (a === '--conversation') { i++; continue; }
|
|
90
121
|
if (a === '--resume') { const n = argv[i + 1]; if (n !== undefined && !n.startsWith('-')) i++; continue; }
|
|
91
122
|
if (/^--(resume|conversation)=/.test(a)) continue;
|
|
92
123
|
out.push(a);
|
|
93
124
|
}
|
|
94
125
|
const has = (...f) => out.some(a => f.includes(a) || f.some(x => a.startsWith(`${x}=`)));
|
|
95
|
-
if (
|
|
126
|
+
if (pin) out.push('-p', profile);
|
|
96
127
|
if (name && !has('-n', '--name')) out.push('-n', name);
|
|
97
128
|
return conversationId ? [...out, '--resume', conversationId] : out;
|
|
98
129
|
}
|
|
@@ -121,8 +152,19 @@ export function checkRestart({ env = process.env, bin = findOnPath('quox', env),
|
|
|
121
152
|
});
|
|
122
153
|
}
|
|
123
154
|
|
|
155
|
+
/** Env var carrying the held slot name from the old client to its restarted successor. */
|
|
156
|
+
export const RESTART_SLOT_ENV = 'QUOX_RESTART_SLOT';
|
|
157
|
+
/** Env var carrying the /attach target (`agent:<id>` or `slot:<name>`) to the restarted successor. */
|
|
158
|
+
export const RESTART_ATTACH_ENV = 'QUOX_RESTART_ATTACH';
|
|
159
|
+
/** Env var carrying one line for the restarted client to say first (a failed /update says why there). */
|
|
160
|
+
export const RESTART_NOTE_ENV = 'QUOX_RESTART_NOTE';
|
|
161
|
+
/** Env var carrying the window's own org (#1034: /org switch is window-scoped) to the restarted successor. */
|
|
162
|
+
export const RESTART_ORG_ENV = 'QUOX_RESTART_ORG';
|
|
163
|
+
/** Env var carrying the window's /mode (PLAN MODE: only 'plan' is carried; ask is the default) to the restarted successor. */
|
|
164
|
+
export const RESTART_MODE_ENV = 'QUOX_RESTART_MODE';
|
|
165
|
+
|
|
124
166
|
/** Terminal modes the client turns on (bracketed paste, focus reporting, hidden cursor): all off before the hand-over. */
|
|
125
|
-
export const TERMINAL_RESTORE = '\x1b[?2004l\x1b[?1004l\x1b[?25h';
|
|
167
|
+
export const TERMINAL_RESTORE = '\x1b[?2004l\x1b[?1004l\x1b[?25h\x1b[?1006l\x1b[?1002l\x1b[?1000l'; // bracketed paste, focus, cursor; mouse tracking off (QUOXON-MOUSE)
|
|
126
168
|
|
|
127
169
|
/** Give the terminal back in a plain state: cooked mode, stdin paused, modes off. */
|
|
128
170
|
export function restoreTerminal({ stdin = process.stdin, stdout = process.stdout } = {}) {
|
|
@@ -130,15 +172,31 @@ export function restoreTerminal({ stdin = process.stdin, stdout = process.stdout
|
|
|
130
172
|
try { if (stdout?.isTTY) stdout.write(TERMINAL_RESTORE); } catch { /* closed */ }
|
|
131
173
|
}
|
|
132
174
|
|
|
175
|
+
/**
|
|
176
|
+
* What a restart runs: { cmd, args, env }. The installed quox (or this tree), the same arguments with --resume
|
|
177
|
+
* for this conversation, and the one-shot hand-over env (held slot, attach, org, plan mode) for the successor.
|
|
178
|
+
*/
|
|
179
|
+
export function restartPlan({ argv, conversationId, profile = null, name = null, slot = null, attach = null, org = null, mode = null, env = process.env, bin = findOnPath('quox', env) }) {
|
|
180
|
+
const args = restartArgs(argv, conversationId, { profile, name });
|
|
181
|
+
let childEnv = profile && !argv.includes('--demo') ? { ...env, QUOX_PROFILE: profile } : env; // belt and braces: same profile even if -p were lost
|
|
182
|
+
if (slot) childEnv = { ...childEnv, [RESTART_SLOT_ENV]: slot }; // the held slot rides along; the new client takes it over before this one is gone
|
|
183
|
+
if (attach) childEnv = { ...childEnv, [RESTART_ATTACH_ENV]: attach }; // so does the attached agent or slot
|
|
184
|
+
if (org) childEnv = { ...childEnv, [RESTART_ORG_ENV]: org }; // and the window's org (never the profile's file)
|
|
185
|
+
if (mode === 'plan') childEnv = { ...childEnv, [RESTART_MODE_ENV]: 'plan' }; // and the window's /mode (never the profile)
|
|
186
|
+
const [cmd, lead] = restartTarget(env, bin);
|
|
187
|
+
return { cmd, args: [...lead, ...args], env: childEnv };
|
|
188
|
+
}
|
|
189
|
+
|
|
133
190
|
/**
|
|
134
191
|
* Run the installed quox in this terminal, wait for it, and hand back { code } (its exit code), or
|
|
135
192
|
* { error } when it could not be started (the caller stays in the current client and says so).
|
|
193
|
+
* Only used when there is no launcher (QUOX_NO_LAUNCHER=1, or no handoff dir): it NESTS the successor under
|
|
194
|
+
* this process, which stays resident until the successor exits. The launcher path (launcher.js) does not.
|
|
136
195
|
*/
|
|
137
|
-
export function execRestart({ argv, conversationId, profile = null, name = null, env = process.env, spawn = spawnSync, bin = findOnPath('quox', env), restore = restoreTerminal }) {
|
|
138
|
-
const
|
|
196
|
+
export function execRestart({ argv, conversationId, profile = null, name = null, slot = null, attach = null, org = null, mode = null, env = process.env, spawn = spawnSync, bin = findOnPath('quox', env), restore = restoreTerminal }) {
|
|
197
|
+
const plan = restartPlan({ argv, conversationId, profile, name, slot, attach, org, mode, env, bin });
|
|
139
198
|
restore(); // the child owns the terminal now
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
return { code: r.status ?? (r.signal ? 128 + (os.constants.signals[r.signal] || 0) : 1), bin: cmd };
|
|
199
|
+
const r = spawn(plan.cmd, plan.args, { stdio: 'inherit', env: plan.env });
|
|
200
|
+
if (r.error) return { error: `${r.error.code || 'failed'}: ${r.error.message}`, bin: plan.cmd };
|
|
201
|
+
return { code: r.status ?? (r.signal ? 128 + (os.constants.signals[r.signal] || 0) : 1), bin: plan.cmd };
|
|
144
202
|
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QUOXON-FW17: /row, custom rows above the status line. Kept out of app.js.
|
|
3
|
+
*
|
|
4
|
+
* url rows save to the user's server-side preferences (api.setRowPrefs, namespace quoxonRows); cmd rows save to a
|
|
5
|
+
* local file only (api.setLocalCmdRows) and are never sent anywhere. The engine (rows.js) does the polling.
|
|
6
|
+
*/
|
|
7
|
+
import { checkRow, parseRowCmd, splitRows, rowLabel, normRows, MAX_ROWS, rowDisplay, ROLES, secretInRow, syncable } from '../rows.js';
|
|
8
|
+
import { clip } from '../theme.js';
|
|
9
|
+
import { createRowWalk } from './rowWalk.js';
|
|
10
|
+
|
|
11
|
+
/** How a row reads its data, in words (for /row list). */
|
|
12
|
+
export const whereOf = (r) => {
|
|
13
|
+
const s = r.source;
|
|
14
|
+
if (s.type === 'url') return `url ${s.url}`;
|
|
15
|
+
if (s.type === 'quox') return `quox ${s.path}${s.service === 'collector' ? ' (collector)' : ''}`;
|
|
16
|
+
if (s.type === 'plugin') return `plugin ${s.plugin} ${s.path}`;
|
|
17
|
+
return `cmd ${s.cmd} (this machine only)`;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/** ROWS: rows changed elsewhere (`quox rows`, another window) reach an open window within this long. */
|
|
21
|
+
export const RELOAD_MS = 60 * 1000;
|
|
22
|
+
|
|
23
|
+
const cleanLine = (l) => String(l).replace(/[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/g, '');
|
|
24
|
+
|
|
25
|
+
export function createRowCmd({ api, say, engine, bump, cols, setT = setTimeout, clearT = clearTimeout }) {
|
|
26
|
+
let url = []; // synced (url, quox and plugin rows)
|
|
27
|
+
let cmd = []; // this machine only
|
|
28
|
+
let reloadTimer = null;
|
|
29
|
+
const all = () => [...url, ...cmd];
|
|
30
|
+
const apply = () => { engine.setRows(all()); bump(); };
|
|
31
|
+
|
|
32
|
+
/** Start up: the cached/local rows at once, then the server copy. Never throws. */
|
|
33
|
+
async function load() {
|
|
34
|
+
try { url = api.localRowPrefs?.() || []; cmd = api.localCmdRows?.() || []; } catch { url = []; cmd = []; }
|
|
35
|
+
engine.setEnabled(api.settings?.().rows !== 'off');
|
|
36
|
+
apply();
|
|
37
|
+
await refreshFromStores();
|
|
38
|
+
schedule();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Re-read both stores; the engine keeps the state of rows that did not change. -> true when anything changed. */
|
|
42
|
+
async function refreshFromStores() {
|
|
43
|
+
const before = JSON.stringify(all());
|
|
44
|
+
try { cmd = api.localCmdRows?.() || cmd; } catch { /* keep */ }
|
|
45
|
+
try {
|
|
46
|
+
const r = await api.rowPrefs?.();
|
|
47
|
+
if (r?.source === 'server') url = normRows(r.rows);
|
|
48
|
+
} catch { /* the local copy stands */ }
|
|
49
|
+
const changed = JSON.stringify(all()) !== before;
|
|
50
|
+
if (changed) apply();
|
|
51
|
+
return changed;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function schedule() {
|
|
55
|
+
clearT(reloadTimer);
|
|
56
|
+
reloadTimer = setT(async () => { await refreshFromStores(); schedule(); }, RELOAD_MS);
|
|
57
|
+
reloadTimer?.unref?.();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Save one row (new, or replacing `replace`). Shared by /row add and the walk. -> { ok, text }. */
|
|
61
|
+
async function put(row, { replace = null } = {}) {
|
|
62
|
+
const exists = replace || find(row.id);
|
|
63
|
+
if (!exists && all().length >= MAX_ROWS) return { ok: false, text: `You have ${MAX_ROWS} rows already (the most there is room for). /row rm <id> makes space.` };
|
|
64
|
+
if (exists) { url = url.filter((x) => x !== exists); cmd = cmd.filter((x) => x !== exists); }
|
|
65
|
+
if (exists && !syncable(exists) && syncable(row)) api.setLocalCmdRows?.(cmd);
|
|
66
|
+
if (syncable(row)) {
|
|
67
|
+
url = [...url, row];
|
|
68
|
+
apply();
|
|
69
|
+
const r = api.setRowPrefs ? await api.setRowPrefs(url) : { synced: true };
|
|
70
|
+
if (!r.synced) return { ok: false, text: `Saved on this machine only (${r.error || 'the server did not take it'}). Making the change again retries.` };
|
|
71
|
+
return { ok: true, text: `Row ${row.id} ${exists ? 'saved' : 'added'}. It follows your account to other machines. /row test ${row.id} shows the raw value.` };
|
|
72
|
+
}
|
|
73
|
+
cmd = [...cmd, row];
|
|
74
|
+
apply();
|
|
75
|
+
if (exists && syncable(exists) && api.setRowPrefs) await api.setRowPrefs(url);
|
|
76
|
+
const ok = api.setLocalCmdRows ? api.setLocalCmdRows(cmd) : true;
|
|
77
|
+
return ok ? { ok: true, text: `Row ${row.id} ${exists ? 'saved' : 'added'}. It runs your command on THIS machine only and is never synced.` } : { ok: false, text: 'Could not save the row to the local file; it lasts until you quit.' };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const walk = createRowWalk({ say, bump, probe: (row) => engine.probe(row), save: put, existing: () => all() });
|
|
81
|
+
|
|
82
|
+
async function saveUrl(tail = '') {
|
|
83
|
+
if (!api.setRowPrefs) return;
|
|
84
|
+
const r = await api.setRowPrefs(url);
|
|
85
|
+
if (!r.synced) say(`Saved on this machine only (${r.error || 'the server did not take it'}). Making the change again retries.`, 'warn');
|
|
86
|
+
else if (tail) say(tail);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const find = (id) => all().find((r) => r.id.toLowerCase() === String(id).toLowerCase());
|
|
90
|
+
|
|
91
|
+
async function handle(arg) {
|
|
92
|
+
const sub = parseRowCmd(arg);
|
|
93
|
+
const w = Math.max(20, cols() - 4);
|
|
94
|
+
switch (sub.kind) {
|
|
95
|
+
case 'error': return say(sub.text, 'warn');
|
|
96
|
+
case 'list': {
|
|
97
|
+
const rows = all();
|
|
98
|
+
if (!rows.length) return say('No custom rows yet. /row new walks you through one (or quox rows add from a shell).');
|
|
99
|
+
say(`Custom rows (${rows.length} of ${MAX_ROWS})${engine.isEnabled() ? '' : ', hidden: /row on shows them'}:`);
|
|
100
|
+
for (const r of rows) {
|
|
101
|
+
const s = engine.state(r.id);
|
|
102
|
+
say(clip(` ${r.id}${r.label && r.label !== r.id ? ` (${r.label})` : ''} every ${r.source.every} ${whereOf(r)}${r.template ? '' : ' [status-row contract]'}`, w));
|
|
103
|
+
if (s && engine.isEnabled()) say(clip(` now: ${rowDisplay(s).text}`, w), s.status === 'error' ? 'warn' : undefined);
|
|
104
|
+
}
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
case 'on': case 'off': {
|
|
108
|
+
engine.setEnabled(sub.kind === 'on');
|
|
109
|
+
api.saveSetting?.('rows', sub.kind);
|
|
110
|
+
bump();
|
|
111
|
+
return say(sub.kind === 'on' ? 'Custom rows are on.' : 'Custom rows hidden and paused. /row on brings them back.');
|
|
112
|
+
}
|
|
113
|
+
case 'rm': {
|
|
114
|
+
const r = find(sub.id);
|
|
115
|
+
if (!r) return say(`No row "${sub.id}". /row list shows them.`, 'warn');
|
|
116
|
+
if (syncable(r)) { url = url.filter((x) => x !== r); apply(); await saveUrl(); } else { cmd = cmd.filter((x) => x !== r); apply(); api.setLocalCmdRows?.(cmd); }
|
|
117
|
+
return say(`Removed row ${r.id}.`, 'ok');
|
|
118
|
+
}
|
|
119
|
+
case 'test': {
|
|
120
|
+
const r = find(sub.id);
|
|
121
|
+
if (!r) return say(`No row "${sub.id}". /row list shows them.`, 'warn');
|
|
122
|
+
say(`Testing ${r.id}…`);
|
|
123
|
+
const p = await engine.probe(r);
|
|
124
|
+
if (!p.ok && !p.raw) return say(clip(`⚠ ${rowLabel(r)}: ${p.error}`, w), 'warn');
|
|
125
|
+
const lines = p.raw.split('\n').filter((l) => l.trim()).slice(0, 4);
|
|
126
|
+
say('Raw value:');
|
|
127
|
+
for (const l of lines) say(clip(` ${cleanLine(l)}`, w));
|
|
128
|
+
if (p.raw.length > 400 || lines.length >= 4) say(' …');
|
|
129
|
+
if (!p.ok) return say(clip(`⚠ ${rowLabel(r)}: ${p.error}`, w), 'warn');
|
|
130
|
+
for (const wn of p.warnings || []) say(clip(`Note: ${wn}`, w), 'warn');
|
|
131
|
+
if (p.seg?.link) say(clip(`Link: ${p.seg.link}`, w));
|
|
132
|
+
return say(clip(`Row shows: ${p.text}`, w), 'ok');
|
|
133
|
+
}
|
|
134
|
+
case 'add': {
|
|
135
|
+
const v = checkRow(sub.row);
|
|
136
|
+
if (v.error) return say(v.error, 'warn');
|
|
137
|
+
const secret = secretInRow(v.row);
|
|
138
|
+
if (secret) return say(secret, 'warn');
|
|
139
|
+
const r = await put(v.row);
|
|
140
|
+
return say(r.text, r.ok ? 'ok' : 'warn');
|
|
141
|
+
}
|
|
142
|
+
case 'new': return walk.start('new');
|
|
143
|
+
case 'edit': return walk.start('edit', sub.id);
|
|
144
|
+
case 'reload': {
|
|
145
|
+
const changed = await refreshFromStores();
|
|
146
|
+
return say(changed ? `Rows reloaded: ${all().map((r) => r.id).join(', ') || 'none'}.` : 'Rows reloaded; nothing changed.');
|
|
147
|
+
}
|
|
148
|
+
default: return undefined;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
handle, load, stopEngine: () => { clearT(reloadTimer); engine.stop(); }, view: () => engine.view(), all, colours: ROLES, counts: () => splitRows(all()),
|
|
154
|
+
/** the /row new|edit walk: app.js routes typed lines, its y/N/c card and Esc here */
|
|
155
|
+
walk
|
|
156
|
+
};
|
|
157
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ROWS (owner 2026-10-17): /row new and /row edit <id> in the quox window. The same shape as the hat walk
|
|
3
|
+
* (hatWalk.js): every question is a y/N/c card (askText) or a one-line text capture. Esc, or any slash command,
|
|
4
|
+
* ends the walk and nothing is saved. Before the last card the row is fetched ONCE and the card shows what it
|
|
5
|
+
* will say, so a row is never saved blind.
|
|
6
|
+
*
|
|
7
|
+
* Command rows get their own card naming the exact command, because a command runs a shell on this machine every
|
|
8
|
+
* refresh. This file has no I/O beyond the functions it is given (probe, save, existing).
|
|
9
|
+
*/
|
|
10
|
+
import { askText } from '../ask.js';
|
|
11
|
+
import { checkRow, secretInRow, validId, parseEvery, rowLabel } from '../rows.js';
|
|
12
|
+
|
|
13
|
+
export const KINDS = [
|
|
14
|
+
{ key: 'url', name: 'a web address (URL) that returns JSON or text', ask: 'Which URL? It is fetched with no credentials.' },
|
|
15
|
+
{ key: 'quox', name: 'a path on your own Quox, read with your session', ask: 'Which path? It starts with /api/; {org} is filled in for you. Example: /api/orgs/{org}/inbox/stats' },
|
|
16
|
+
{ key: 'plugin', name: 'a status row a plugin ships', ask: 'Which plugin and row path? Example: qpe row' },
|
|
17
|
+
{ key: 'cmd', name: 'a command on THIS machine (never synced or shared)', ask: 'Which command? It runs through your shell, 5 s at most.' }
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
const slug = (s) => String(s ?? '').toLowerCase().replace(/[^a-z0-9_-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 24);
|
|
21
|
+
|
|
22
|
+
/** A kind and the typed target -> a source, or { error }. */
|
|
23
|
+
export function sourceFrom(kind, line, every = '60s') {
|
|
24
|
+
const t = String(line ?? '').trim();
|
|
25
|
+
if (!t) return { error: 'That is empty.' };
|
|
26
|
+
if (kind === 'url') return { source: { type: 'url', url: t, every } };
|
|
27
|
+
if (kind === 'cmd') return { source: { type: 'cmd', cmd: t, every } };
|
|
28
|
+
if (kind === 'quox') return { source: { type: 'quox', service: t.startsWith('/api/v1/') ? 'collector' : 'auth', path: t, every } };
|
|
29
|
+
const [plugin, ...rest] = t.split(/\s+/);
|
|
30
|
+
if (!rest.length) return { error: 'Give the plugin id and the row path, e.g. qpe row.' };
|
|
31
|
+
return { source: { type: 'plugin', plugin, path: rest.join(''), every } };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** "1", "url", "a URL" -> a KINDS key, or null. */
|
|
35
|
+
export function pickKind(line) {
|
|
36
|
+
const t = String(line ?? '').trim().toLowerCase();
|
|
37
|
+
if (/^[1-4]$/.test(t)) return KINDS[Number(t) - 1].key;
|
|
38
|
+
const hit = KINDS.find((k) => k.key === t || (t.length >= 3 && k.name.toLowerCase().includes(t)));
|
|
39
|
+
return hit ? hit.key : (t === 'command' || t === 'shell' ? 'cmd' : null);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function createRowWalk({ say, bump = () => {}, probe, save, existing }) {
|
|
43
|
+
let W = null;
|
|
44
|
+
|
|
45
|
+
const ask = (question, reason = '') => { const st = { at: Date.now() }; W.card = st; say(askText(question, { reason }), 'ask'); return st; };
|
|
46
|
+
const yn = (question, on, reason) => { W.wait = { kind: 'yn', on }; ask(question, reason); bump(); };
|
|
47
|
+
// path: the answer may itself start with / (a quox path like /api/...), so only a known slash command ends the walk
|
|
48
|
+
const text = (prompt, on, { empty = false, path = false } = {}) => { W.wait = { kind: 'text', on, empty, path }; W.card = null; say(prompt); bump(); };
|
|
49
|
+
const end = () => { W = null; bump(); };
|
|
50
|
+
|
|
51
|
+
/** Validate, preview once, then the final card. */
|
|
52
|
+
async function preview(draft, verb) {
|
|
53
|
+
const c = checkRow(draft);
|
|
54
|
+
if (c.error) { say(c.error, 'warn'); return W?.retry?.(); }
|
|
55
|
+
const secret = secretInRow(c.row);
|
|
56
|
+
if (secret) { say(secret, 'warn'); return W?.retry?.(); }
|
|
57
|
+
const row = c.row;
|
|
58
|
+
say(`Fetching ${rowLabel(row)} once to show you what it will say…`);
|
|
59
|
+
const p = await probe(row);
|
|
60
|
+
if (!W) return undefined; // cancelled while fetching
|
|
61
|
+
if (p.ok) {
|
|
62
|
+
say(`It will show: ${p.text}`, 'ok');
|
|
63
|
+
for (const w of p.warnings || []) say(`Note: ${w}`, 'warn');
|
|
64
|
+
} else say(`It could not be read just now (${p.error}). You can still save it; the row shows the problem until it works.`, 'warn');
|
|
65
|
+
yn(`${verb} the ${rowLabel(row)} row?`, async (yes) => {
|
|
66
|
+
if (!yes) { say('Nothing saved.'); return end(); }
|
|
67
|
+
const r = await save(row, { replace: W.replace || null });
|
|
68
|
+
say(r.text, r.ok ? 'ok' : 'warn');
|
|
69
|
+
end();
|
|
70
|
+
}, row.source.type === 'cmd' ? 'Runs on this machine only, never synced' : 'Follows your account to your other machines');
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Shared tail: template, interval, then preview.
|
|
75
|
+
function tail(draft, verb) {
|
|
76
|
+
const askTemplate = () => text(draft.template
|
|
77
|
+
? `Template? Enter keeps ${draft.template}; "-" uses the status-row contract ({"text": ...}).`
|
|
78
|
+
: 'Template? Enter reads the status-row contract ({"text": ..., "value": ..., "tone": ...}) or plain text. Or type one, e.g. {{pending_approvals}} waiting', (line) => {
|
|
79
|
+
const t = line.trim();
|
|
80
|
+
if (t === '-') delete draft.template; else if (t) draft.template = t;
|
|
81
|
+
askEvery();
|
|
82
|
+
}, { empty: true });
|
|
83
|
+
const askEvery = () => yn(`Refresh every ${draft.source.every}?`, (yes) => {
|
|
84
|
+
if (yes) return preview(draft, verb);
|
|
85
|
+
const again = () => text('How often? 30s, 5m or 1h (5 s to 1 day).', (line) => {
|
|
86
|
+
if (parseEvery(line) === null) { say(`"${line.trim()}" is not an interval.`, 'warn'); return again(); }
|
|
87
|
+
draft.source.every = line.trim(); preview(draft, verb);
|
|
88
|
+
});
|
|
89
|
+
again();
|
|
90
|
+
});
|
|
91
|
+
W.retry = askTemplate;
|
|
92
|
+
askTemplate();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function askSource(draft, verb) {
|
|
96
|
+
KINDS.forEach((k, i) => say(` ${i + 1}. ${k.name}`));
|
|
97
|
+
const pick = () => text('Where does it read from? A number.', (line) => {
|
|
98
|
+
const kind = pickKind(line);
|
|
99
|
+
if (!kind) { say('Pick 1, 2, 3 or 4.', 'warn'); return pick(); }
|
|
100
|
+
const target = () => text(KINDS.find((k) => k.key === kind).ask, (l2) => {
|
|
101
|
+
const s = sourceFrom(kind, l2, draft.source?.every || '60s');
|
|
102
|
+
if (s.error) { say(s.error, 'warn'); return target(); }
|
|
103
|
+
draft.source = s.source;
|
|
104
|
+
const c = checkRow({ ...draft, template: draft.template });
|
|
105
|
+
if (c.error && !/template|Template/.test(c.error)) { say(c.error, 'warn'); return target(); }
|
|
106
|
+
const secret = secretInRow(draft);
|
|
107
|
+
if (secret) { say(secret, 'warn'); return target(); }
|
|
108
|
+
if (kind === 'cmd') return yn(`Run \`${s.source.cmd}\` on this machine every ${s.source.every}?`, (yes) => {
|
|
109
|
+
if (!yes) { say('Nothing saved.'); return end(); }
|
|
110
|
+
tail(draft, verb);
|
|
111
|
+
}, 'A command row runs a shell here and is never synced or shared');
|
|
112
|
+
tail(draft, verb);
|
|
113
|
+
}, { path: kind === 'quox' });
|
|
114
|
+
target();
|
|
115
|
+
});
|
|
116
|
+
pick();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function startNew() {
|
|
120
|
+
W = { mode: 'new' };
|
|
121
|
+
const draft = { source: { every: '60s' } };
|
|
122
|
+
const askName = () => text('What should the row be called? A short label, e.g. Approvals.', (line) => {
|
|
123
|
+
const label = line.trim().slice(0, 24);
|
|
124
|
+
const id = slug(label);
|
|
125
|
+
if (!validId(id)) { say('Use letters or digits in the name.', 'warn'); return askName(); }
|
|
126
|
+
if (existing().some((r) => r.id.toLowerCase() === id)) { say(`A row called ${id} exists. /row edit ${id} changes it, or pick another name.`, 'warn'); return askName(); }
|
|
127
|
+
draft.id = id; draft.label = label;
|
|
128
|
+
askSource(draft, 'Add');
|
|
129
|
+
});
|
|
130
|
+
askName();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function startEdit(id) {
|
|
134
|
+
const cur = existing().find((r) => r.id.toLowerCase() === String(id).toLowerCase());
|
|
135
|
+
if (!cur) { say(`No row "${id}". /row list shows them.`, 'warn'); return; }
|
|
136
|
+
W = { mode: 'edit', replace: cur };
|
|
137
|
+
const draft = JSON.parse(JSON.stringify(cur));
|
|
138
|
+
yn(`Change where ${rowLabel(cur)} reads from?`, (yes) => {
|
|
139
|
+
if (yes) return askSource(draft, 'Save');
|
|
140
|
+
yn('Rename it?', (y2) => {
|
|
141
|
+
if (!y2) return tail(draft, 'Save');
|
|
142
|
+
const again = () => text('What is the new label?', (line) => { const l = line.trim().slice(0, 24); if (!l) { say('A label cannot be empty.', 'warn'); return again(); } draft.label = l; tail(draft, 'Save'); });
|
|
143
|
+
again();
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
active: () => Boolean(W),
|
|
150
|
+
card: () => (W?.wait?.kind === 'yn' ? W.card : null),
|
|
151
|
+
start(kind, arg = '') {
|
|
152
|
+
if (W) { say('A row walk is already open. Esc cancels it.', 'warn'); return; }
|
|
153
|
+
if (kind === 'new') return startNew();
|
|
154
|
+
return startEdit(arg);
|
|
155
|
+
},
|
|
156
|
+
/** One typed line. true = consumed. A slash command ends the walk and is left to run. */
|
|
157
|
+
answer(line) {
|
|
158
|
+
if (!W?.wait) return false;
|
|
159
|
+
const t = String(line ?? '').trim();
|
|
160
|
+
const isPath = W.wait.kind === 'text' && W.wait.path && t.startsWith('/api/');
|
|
161
|
+
if (t.startsWith('/') && !isPath) { W = null; say('Row walk ended. Nothing was saved.'); bump(); return false; }
|
|
162
|
+
const w = W.wait;
|
|
163
|
+
if (w.kind === 'yn') {
|
|
164
|
+
const yes = /^y(es)?$/i.test(t);
|
|
165
|
+
if (!yes && t && !/^no?$/i.test(t)) return false; // c or free text: the card's normal chat behaviour
|
|
166
|
+
W.wait = null; Promise.resolve(w.on(yes)).catch((e) => { say(`Row walk failed: ${e?.message || 'error'}.`, 'warn'); end(); });
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
if (!t && !w.empty) return true;
|
|
170
|
+
W.wait = null; Promise.resolve(w.on(t)).catch((e) => { say(`Row walk failed: ${e?.message || 'error'}.`, 'warn'); end(); });
|
|
171
|
+
return true;
|
|
172
|
+
},
|
|
173
|
+
cancel() { if (!W) return false; W = null; say('Row walk cancelled. Nothing was saved.'); bump(); return true; }
|
|
174
|
+
};
|
|
175
|
+
}
|
|
@@ -5,18 +5,24 @@
|
|
|
5
5
|
* which case the shell wins and the card says so.
|
|
6
6
|
*/
|
|
7
7
|
import { PROFILE_IDS } from '../sound/mapping.js';
|
|
8
|
+
import { LEVELS, DEFAULT_LEVEL, normaliseLevel } from '../motion/level.js';
|
|
8
9
|
|
|
9
10
|
// Captured at import, before anything here writes it: what the user's shell set.
|
|
10
|
-
const SHELL = {
|
|
11
|
+
const SHELL = {
|
|
12
|
+
theme: process.env.QUOX_THEME || null,
|
|
13
|
+
motion: process.env.QUOX_REDUCED_MOTION === '1' ? 'quiet' : (normaliseLevel(process.env.QUOX_MOTION) || null),
|
|
14
|
+
motionVar: process.env.QUOX_REDUCED_MOTION === '1' ? 'QUOX_REDUCED_MOTION' : 'QUOX_MOTION',
|
|
15
|
+
};
|
|
11
16
|
|
|
12
17
|
|
|
13
18
|
/** The settings this build can honour. Sound is the /sound controller's own profile (server-side soundProfile). */
|
|
14
19
|
export function settingsList({ sound = true } = {}) {
|
|
15
20
|
return [
|
|
16
21
|
{ id: 'theme', label: 'Theme', values: ['auto', 'dark', 'light'], def: 'auto', help: 'auto follows your terminal background' },
|
|
17
|
-
{ id: 'motion', label: 'Motion', values:
|
|
22
|
+
{ id: 'motion', label: 'Motion', values: LEVELS, def: DEFAULT_LEVEL, help: 'quiet stills it all; expressive adds glows; cinematic adds the dragon' },
|
|
18
23
|
...(sound ? [{ id: 'sound', label: 'Sound', values: PROFILE_IDS, def: 'minimal', help: 'same as /sound; saved to your account' }] : []),
|
|
19
24
|
{ id: 'verbose', label: 'Verbose', values: ['off', 'on', 'full'], def: 'on', help: 'protocol lines in turns' },
|
|
25
|
+
{ id: 'slotBell', label: 'Slot bell', values: ['off', 'on'], def: 'on', help: 'a terminal bell when a slot message arrives' },
|
|
20
26
|
{ id: 'model', label: 'Model', action: 'model', help: 'Enter opens /model' }
|
|
21
27
|
];
|
|
22
28
|
}
|
|
@@ -27,7 +33,9 @@ export function applySettings(s = {}, env = process.env, shell = SHELL) {
|
|
|
27
33
|
if (s.theme === 'dark' || s.theme === 'light') env.QUOX_THEME = s.theme; else delete env.QUOX_THEME;
|
|
28
34
|
}
|
|
29
35
|
if (!shell.motion) {
|
|
30
|
-
|
|
36
|
+
const level = normaliseLevel(s.motion) || DEFAULT_LEVEL; // pre-levels profiles saved full / reduced
|
|
37
|
+
if (level === DEFAULT_LEVEL) delete env.QUOX_MOTION; else env.QUOX_MOTION = level;
|
|
38
|
+
if (level === 'quiet') env.QUOX_REDUCED_MOTION = '1'; else delete env.QUOX_REDUCED_MOTION; // every older reader agrees
|
|
31
39
|
}
|
|
32
40
|
return { themeFromShell: Boolean(shell.theme), motionFromShell: Boolean(shell.motion) };
|
|
33
41
|
}
|
|
@@ -38,13 +46,15 @@ export const shellOverrides = () => ({ ...SHELL });
|
|
|
38
46
|
export function configRows(list, values, { shell = SHELL, model = 'auto' } = {}) {
|
|
39
47
|
return list.map(it => {
|
|
40
48
|
if (it.action === 'model') return { key: it.label, text: model, note: it.help };
|
|
41
|
-
const v = values[it.id] ?? it.def;
|
|
42
|
-
const
|
|
49
|
+
const v = it.id === 'motion' ? (normaliseLevel(values.motion) || it.def) : (values[it.id] ?? it.def);
|
|
50
|
+
const motionVar = shell.motionVar || 'QUOX_MOTION';
|
|
51
|
+
const forced = (it.id === 'theme' && shell.theme) ? `QUOX_THEME=${shell.theme} in your shell wins`
|
|
52
|
+
: (it.id === 'motion' && shell.motion) ? (motionVar === 'QUOX_MOTION' ? `QUOX_MOTION=${shell.motion} in your shell wins` : `${motionVar} in your shell wins`) : null;
|
|
43
53
|
return { key: it.label, text: it.values.map(x => (x === v ? `[${x}]` : x)).join(' '), note: forced || it.help };
|
|
44
54
|
});
|
|
45
55
|
}
|
|
46
56
|
|
|
47
57
|
export const cycle = (it, cur, dir = 1) => {
|
|
48
|
-
const i = it.values.indexOf(cur ?? it.def);
|
|
58
|
+
const i = it.values.indexOf((it.id === 'motion' ? normaliseLevel(cur) : cur) ?? it.def);
|
|
49
59
|
return it.values[(i + dir + it.values.length) % it.values.length];
|
|
50
60
|
};
|