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/commands/slot.js
CHANGED
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
* /messages), never a direct Matrix client in the CLI.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
+
import { presenceText } from './presence.js';
|
|
14
|
+
import { readInstalledBuild, CLI_DIR } from '../core/cliBuild.js';
|
|
13
15
|
import { existsSync, readFileSync, writeFileSync, mkdirSync, renameSync, openSync, closeSync } from 'node:fs';
|
|
14
16
|
import { spawn } from 'node:child_process';
|
|
15
17
|
import { homedir } from 'node:os';
|
|
@@ -20,8 +22,12 @@ import { events, printEvent } from '../core/quoxglyph.js';
|
|
|
20
22
|
import { resolveEndpoint } from '../api/endpoints.js';
|
|
21
23
|
import { QuoxApiError, QuoxNetworkError } from '../core/errors.js';
|
|
22
24
|
import { resolveOrgId } from './helpers.js';
|
|
25
|
+
import { parseBroadcastArgs, broadcastSummary, deliveryLine, parseAckArgs } from '../quoxon/slash/broadcast.js';
|
|
23
26
|
import { claudeSessionInfo, procStartTime, resolveMyAttach, readSlotState } from './agentAttach.js';
|
|
24
27
|
import { runKeeper, keeperDir, makeProcessProbe, sameProcess } from '../core/slotKeeper.js';
|
|
28
|
+
import { loadConfig } from '../core/config.js';
|
|
29
|
+
import { createAuth } from '../core/auth.js';
|
|
30
|
+
import { createClient } from '../core/client.js';
|
|
25
31
|
|
|
26
32
|
// Local record of the slots checked out from this box, written at checkout
|
|
27
33
|
// and cleared at release. Consumers (the slot-inbox hook) re-verify each
|
|
@@ -53,7 +59,9 @@ function writeHeldSlots(all, path) {
|
|
|
53
59
|
// `info` is claudeSessionInfo() ({ sessionKey, pid, starttime }) or null.
|
|
54
60
|
// Entries written before quox#662 are { session, recorded_at } only, and
|
|
55
61
|
// still read fine: they just carry no process binding.
|
|
56
|
-
|
|
62
|
+
// HOLDER-SAY (2026-10-17): `profile` is the CLI profile whose identity took the hold. The server binds the hold
|
|
63
|
+
// to that identity (holder_identity), so `say` must speak through the same profile, not whatever the default is now.
|
|
64
|
+
export function recordHeldSlot(name, session, path = heldSlotsPath(), info = null, member = null, profile = null) {
|
|
57
65
|
try {
|
|
58
66
|
const all = readHeldSlots(path);
|
|
59
67
|
// P10: a joining member must not clobber a sibling session's HOLDER entry
|
|
@@ -65,6 +73,7 @@ export function recordHeldSlot(name, session, path = heldSlotsPath(), info = nul
|
|
|
65
73
|
...(info ? { sessionKey: info.sessionKey, pid: info.pid, starttime: info.starttime } : {}),
|
|
66
74
|
// P10: joined an already-held slot as a member (not the holder).
|
|
67
75
|
...(member ? { member: true, splinter: member.splinter ?? null, member_key: member.member_key ?? null } : {}),
|
|
76
|
+
...(profile ? { profile } : {}),
|
|
68
77
|
recorded_at: new Date().toISOString(),
|
|
69
78
|
};
|
|
70
79
|
writeHeldSlots(all, path);
|
|
@@ -316,6 +325,24 @@ export function registerSlotCommands(program, ctx) {
|
|
|
316
325
|
...(ctx.deps || {}),
|
|
317
326
|
};
|
|
318
327
|
|
|
328
|
+
// HOLDER-SAY: the profile this invocation runs as (null when unknown, e.g. in tests without a config).
|
|
329
|
+
const profileName = () => { try { return ctx.config?.getActiveProfileName?.() || null; } catch { return null; } };
|
|
330
|
+
const explicitProfile = () => Boolean(ctx.config?._explicitProfile) || Boolean(String(process.env.QUOX_PROFILE || '').trim());
|
|
331
|
+
// HOLDER-SAY: the client a say into `name` as session `from` goes through. The hold is bound server-side to the
|
|
332
|
+
// identity that checked it out; when this box recorded that a DIFFERENT local profile took it, speak through that
|
|
333
|
+
// profile (the same identity, its own credentials). The server's holder check is unchanged: another identity
|
|
334
|
+
// still cannot speak as the holder. An explicit -p / QUOX_PROFILE always wins (the caller said who they are).
|
|
335
|
+
function sayClientFor(name, from) {
|
|
336
|
+
if (!from || explicitProfile()) return { client, profile: null };
|
|
337
|
+
const e = readHeldSlots()[name];
|
|
338
|
+
const cur = profileName();
|
|
339
|
+
if (!e || e.member || e.session !== from || !e.profile || !cur || e.profile === cur) return { client, profile: null };
|
|
340
|
+
try {
|
|
341
|
+
const c = deps.clientForProfile ? deps.clientForProfile(e.profile) : (() => { const cfg = loadConfig(e.profile); return createClient(cfg, createAuth(cfg)); })();
|
|
342
|
+
return c ? { client: c, profile: e.profile } : { client, profile: null };
|
|
343
|
+
} catch { return { client, profile: null }; }
|
|
344
|
+
}
|
|
345
|
+
|
|
319
346
|
// Session identity (quox#662). Holder names are free text and every verb
|
|
320
347
|
// used to default differently, so EMAIL-AGENTS vs email-agents 409'd. One
|
|
321
348
|
// rule now: --session wins; else the binding this very Claude session
|
|
@@ -348,6 +375,31 @@ export function registerSlotCommands(program, ctx) {
|
|
|
348
375
|
}
|
|
349
376
|
}
|
|
350
377
|
|
|
378
|
+
// SLOTS-REPLY: the slot THIS session holds, sent as from_slot so a reply can reach
|
|
379
|
+
// it (a free-text --session label is not an address). Only this Claude session's
|
|
380
|
+
// own live bindings count; outside Claude, an entry recorded under the same
|
|
381
|
+
// session name. The server re-checks the hold (same_user) and drops a bad claim.
|
|
382
|
+
function senderSlot(from) {
|
|
383
|
+
try {
|
|
384
|
+
const cur = deps.claudeSessionInfo();
|
|
385
|
+
const all = readHeldSlots();
|
|
386
|
+
let mine = Object.entries(all).filter(([, e]) => e && !e.member && (cur
|
|
387
|
+
? e.sessionKey && e.sessionKey === cur.sessionKey && sameProcess(deps.probe, e.pid, e.starttime)
|
|
388
|
+
: from && String(e.session || '').toLowerCase() === String(from).toLowerCase() && (!e.pid || sameProcess(deps.probe, e.pid, e.starttime))));
|
|
389
|
+
if (!mine.length) {
|
|
390
|
+
const mineAtt = resolveMyAttach();
|
|
391
|
+
const st = mineAtt?.agentId ? readSlotState(mineAtt.agentId) : null;
|
|
392
|
+
return st && st.name ? st.name : null;
|
|
393
|
+
}
|
|
394
|
+
const named = mine.filter(([, e]) => from && e.session === from);
|
|
395
|
+
if (named.length) mine = named;
|
|
396
|
+
mine.sort((a, b) => String(b[1].recorded_at || '').localeCompare(String(a[1].recorded_at || '')));
|
|
397
|
+
return mine[0][0];
|
|
398
|
+
} catch {
|
|
399
|
+
return null;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
351
403
|
// P10: the collector recognises splinter members only via the ATTACH token.
|
|
352
404
|
// When this Claude session is attached to an agent whose recorded slot is
|
|
353
405
|
// `name`, send that token (same noAuth + Authorization pattern as
|
|
@@ -385,6 +437,8 @@ export function registerSlotCommands(program, ctx) {
|
|
|
385
437
|
// --json gets the API records (data keys), never the table cells.
|
|
386
438
|
raw: data.slots || [],
|
|
387
439
|
});
|
|
440
|
+
// PRESENCE: who is away (approvals to them queue). Table output only; --json has humans in the records.
|
|
441
|
+
if (program.opts().output !== 'json') for (const h of (data.humans || []).filter((x) => x && x.away)) printInfo(` ⏸ ${presenceText(h)}`);
|
|
388
442
|
});
|
|
389
443
|
|
|
390
444
|
slot
|
|
@@ -515,7 +569,7 @@ export function registerSlotCommands(program, ctx) {
|
|
|
515
569
|
// moment the binding is known for certain, so it gets written down
|
|
516
570
|
// here rather than re-derived later.
|
|
517
571
|
const me = deps.claudeSessionInfo();
|
|
518
|
-
recordHeldSlot(name, holderSession, heldSlotsPath(), me, joined);
|
|
572
|
+
recordHeldSlot(name, holderSession, heldSlotsPath(), me, joined, profileName());
|
|
519
573
|
// Owner incident, 2026-08-22/23: "you cant bind to a slot and then
|
|
520
574
|
// immediately ignore the channel it creates". Checkout hands back a
|
|
521
575
|
// live address (a Matrix/Discord room), not just a lock; say so once,
|
|
@@ -553,6 +607,24 @@ export function registerSlotCommands(program, ctx) {
|
|
|
553
607
|
throw new Error(`slot ${name} is assigned to ${err.detail.assigned_to}; pass --force to take it anyway`);
|
|
554
608
|
}
|
|
555
609
|
const holder = err.detail?.holder_session || 'another session';
|
|
610
|
+
// POKE-WAKE: the server refuses a same-session re-checkout under a different identity (a hold
|
|
611
|
+
// taken as a human, re-checked-out as the resident). When this box's binding proves the hold
|
|
612
|
+
// belongs to THIS claude process, the slot is already ours: refresh the keeper (a newer build
|
|
613
|
+
// replaces an older keeper) instead of failing. Nothing server-side changes.
|
|
614
|
+
const me = deps.claudeSessionInfo();
|
|
615
|
+
const rec = readHeldSlots()[name];
|
|
616
|
+
if (me && holder === holderSession && rec && rec.session === holderSession && !rec.member
|
|
617
|
+
&& Number(rec.pid) === Number(me.pid) && String(rec.starttime) === String(me.starttime)) {
|
|
618
|
+
printInfo(`slot ${name} is already held by this session (${holderSession}); refreshing its hold keeper.`);
|
|
619
|
+
if (options.keep !== false) {
|
|
620
|
+
try {
|
|
621
|
+
deps.spawnKeeper(name, holderSession, { pid: me.pid, starttime: me.starttime });
|
|
622
|
+
} catch (kerr) {
|
|
623
|
+
printWarning(`could not start the hold keeper (${kerr.message}).`);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
556
628
|
throw new Error(`slot ${name} is held by ${holder}`);
|
|
557
629
|
}
|
|
558
630
|
// fleet #449: a bare "404 Not Found: unknown" against an instance
|
|
@@ -586,6 +658,12 @@ export function registerSlotCommands(program, ctx) {
|
|
|
586
658
|
...attachAuthFor(name),
|
|
587
659
|
body: { holder_session: relSession || undefined, force: !!options.force, org_id: resolveOrgId(auth, program) },
|
|
588
660
|
});
|
|
661
|
+
// Never print a tick the server did not earn: the returned slot must actually be free (or handed to
|
|
662
|
+
// someone else), not still held by the session we just asked to release.
|
|
663
|
+
const after = data?.slot;
|
|
664
|
+
if (after && after.state !== 'free' && !data.promoted && (options.force || (relSession && after.holder_session === relSession))) {
|
|
665
|
+
throw new Error(`slot ${name} was NOT released: the server still shows it ${after.state} by ${after.holder_session || 'unknown'}`);
|
|
666
|
+
}
|
|
589
667
|
formatDetail(data.slot || data, { format: program.opts().output, title: `Slot released: ${name}` });
|
|
590
668
|
printEvent(events.released('quox', `slot ${name}`, relSession || 'this box'));
|
|
591
669
|
forgetHeldSlot(name);
|
|
@@ -596,7 +674,7 @@ export function registerSlotCommands(program, ctx) {
|
|
|
596
674
|
.command('control')
|
|
597
675
|
.description('Send a control command to the session holding a slot (owner/admin only). Commands: compact, status')
|
|
598
676
|
.argument('<name>', 'slot name')
|
|
599
|
-
.argument('<command>', 'control command (compact | status)')
|
|
677
|
+
.argument('<command>', 'control command (compact | status | wake)')
|
|
600
678
|
.option('--wait', 'wait up to 40s for the keeper to ack and print its answer (always on for status)')
|
|
601
679
|
.action(async (name, command, options) => {
|
|
602
680
|
auth.requireAuth();
|
|
@@ -733,14 +811,70 @@ export function registerSlotCommands(program, ctx) {
|
|
|
733
811
|
if (program.opts().output === 'json') formatDetail(data, { format: 'json' });
|
|
734
812
|
});
|
|
735
813
|
|
|
814
|
+
// QUOXON-ROOMS: a slot is a group room. join = subscribe + post without holding it (checkout stays the
|
|
815
|
+
// exclusive task-ownership concept). Membership is server-side, org-scoped.
|
|
816
|
+
async function joinRoom(name, session, person) {
|
|
817
|
+
const ep = resolveEndpoint('v1.slots.join', { name });
|
|
818
|
+
return client.post(ep.path, { service: ep.service, ...attachAuthFor(name), body: { session, ...(person ? { person } : {}), org_id: resolveOrgId(auth, program) } });
|
|
819
|
+
}
|
|
820
|
+
slot
|
|
821
|
+
.command('join')
|
|
822
|
+
.description('Join a slot as a room: read its live stream and post to it without holding it')
|
|
823
|
+
.argument('<name>', 'slot name')
|
|
824
|
+
.option('--session <s>', 'session name to join as (default: $QUOX_STREAM)')
|
|
825
|
+
.option('--person <p>', 'who you act for, shown as "session (person)"')
|
|
826
|
+
.action(async (name, options) => {
|
|
827
|
+
auth.requireAuth();
|
|
828
|
+
const session = options.session || process.env.QUOX_STREAM;
|
|
829
|
+
if (!session) throw new Error('no session name: pass --session or set QUOX_STREAM');
|
|
830
|
+
const data = await joinRoom(name, session, options.person);
|
|
831
|
+
printSuccess(data.already ? `${session} is already in ${name}.` : `${session} joined ${name}. Read: quox slot wait ${name} --session ${session}; post: quox slot say ${name} --room --session ${session} <text>`);
|
|
832
|
+
});
|
|
833
|
+
slot
|
|
834
|
+
.command('leave')
|
|
835
|
+
.description('Leave a slot room you joined (does not release a hold)')
|
|
836
|
+
.argument('<name>', 'slot name')
|
|
837
|
+
.option('--session <s>', 'session name (default: $QUOX_STREAM)')
|
|
838
|
+
.action(async (name, options) => {
|
|
839
|
+
auth.requireAuth();
|
|
840
|
+
const session = options.session || process.env.QUOX_STREAM;
|
|
841
|
+
if (!session) throw new Error('no session name: pass --session or set QUOX_STREAM');
|
|
842
|
+
const ep = resolveEndpoint('v1.slots.leave', { name });
|
|
843
|
+
const data = await client.post(ep.path, { service: ep.service, ...attachAuthFor(name), body: { session, org_id: resolveOrgId(auth, program) } });
|
|
844
|
+
printSuccess(data.removed ? `${session} left ${name}.` : `${session} was not in ${name}.`);
|
|
845
|
+
});
|
|
846
|
+
slot
|
|
847
|
+
.command('rooms')
|
|
848
|
+
.description('List the slot rooms a session has joined')
|
|
849
|
+
.option('--session <s>', 'session name (default: $QUOX_STREAM)')
|
|
850
|
+
.action(async (options) => {
|
|
851
|
+
auth.requireAuth();
|
|
852
|
+
const session = options.session || process.env.QUOX_STREAM;
|
|
853
|
+
if (!session) throw new Error('no session name: pass --session or set QUOX_STREAM');
|
|
854
|
+
const ep = resolveEndpoint('v1.slots.rooms');
|
|
855
|
+
const data = await client.get(ep.path, { service: ep.service, query: { session, org_id: resolveOrgId(auth, program) } });
|
|
856
|
+
formatOutput((data.rooms || []).map((r) => ({ ROOM: r.slot_name, JOINED: r.joined_at })), {
|
|
857
|
+
format: program.opts().output, columns: ['ROOM', 'JOINED'], emptyMessage: `${session} has joined no rooms.`, raw: data.rooms || [],
|
|
858
|
+
});
|
|
859
|
+
});
|
|
860
|
+
|
|
736
861
|
slot
|
|
737
862
|
.command('say')
|
|
738
863
|
.description("Say something into a slot's Matrix room (use - to read a multi-line, Markdown message from stdin)")
|
|
739
864
|
.argument('<name>', 'slot name')
|
|
740
865
|
.argument('<text...>', 'message text')
|
|
741
866
|
.option('--session <s>', 'session name to speak as (default: $QUOX_STREAM)')
|
|
867
|
+
.option('--room', 'post as a joined room member (no hold needed); signed "session (person)" by the server')
|
|
868
|
+
.option('--run <command>', 'ask the human to run ONE command in their own terminal (the text is why); the quox TUI shows it in full and runs it only on their y. Never run server-side')
|
|
742
869
|
.action(async (name, textParts, options) => {
|
|
743
870
|
auth.requireAuth();
|
|
871
|
+
// SLOT-RUN: same rule as the collector (one line, max 2000, no control/bidi characters), checked before sending.
|
|
872
|
+
if (options.run !== undefined) {
|
|
873
|
+
const run = String(options.run);
|
|
874
|
+
if (!run.trim()) throw new Error('--run is empty');
|
|
875
|
+
if (run.length > 2000) throw new Error('--run too long (max 2000 characters)');
|
|
876
|
+
if (/[\u0000-\u001f\u007f\u0080-\u009f\u200b-\u200f\u2028\u2029\u202a-\u202e\u2066-\u2069\ufeff]/.test(run)) throw new Error('--run must be one command on one line, without control, bidi or zero-width characters');
|
|
877
|
+
}
|
|
744
878
|
// `say <name> -` reads the message from stdin (owner 2026-10-05: slot
|
|
745
879
|
// channels were "enormous single paragraphs" because multi-line text is
|
|
746
880
|
// awkward as a shell argument). Line breaks and Markdown survive as-is.
|
|
@@ -759,16 +893,24 @@ export function registerSlotCommands(program, ctx) {
|
|
|
759
893
|
// authenticated identity.
|
|
760
894
|
const from = resolveSession(options, name, null);
|
|
761
895
|
const ep = resolveEndpoint('v1.slots.say', { name });
|
|
762
|
-
|
|
896
|
+
if (options.room && !from) throw new Error('--room needs a session: pass --session or set QUOX_STREAM');
|
|
897
|
+
const body = { text, ...(options.room ? { room_session: from } : from ? { from } : {}), org_id: resolveOrgId(auth, program) };
|
|
898
|
+
if (options.run !== undefined) body.run = String(options.run);
|
|
899
|
+
// SLOTS-REPLY: carry the sender's own slot so the recipient's reply reaches it.
|
|
900
|
+
// A room post already has its address (the room), so it carries none.
|
|
901
|
+
const fromSlot = options.room ? null : senderSlot(from);
|
|
902
|
+
if (fromSlot) body.from_slot = fromSlot;
|
|
763
903
|
// A freshly created slot gets its Matrix room a few seconds AFTER
|
|
764
904
|
// checkout returns (provisioning is async), so a say straight after
|
|
765
905
|
// checkout used to fail with 409 "no Matrix room yet" (slots audit
|
|
766
906
|
// 2026-10-02, measured ~5s). Ride out exactly that race; any other
|
|
767
907
|
// 409 is real and surfaces immediately.
|
|
768
908
|
let data;
|
|
909
|
+
const sayVia = options.room ? { client, profile: null } : sayClientFor(name, from);
|
|
910
|
+
if (sayVia.profile) printInfo(`Speaking as profile ${sayVia.profile}: it checked out ${name} for ${from}, and the hold is bound to its identity.`);
|
|
769
911
|
for (let attempt = 1; ; attempt += 1) {
|
|
770
912
|
try {
|
|
771
|
-
data = await client.post(ep.path, { service: ep.service, ...attachAuthFor(name), body });
|
|
913
|
+
data = await sayVia.client.post(ep.path, { service: ep.service, ...attachAuthFor(name), body });
|
|
772
914
|
break;
|
|
773
915
|
} catch (err) {
|
|
774
916
|
// The collector sends {error: '<text>'}, which the client stores
|
|
@@ -790,14 +932,95 @@ export function registerSlotCommands(program, ctx) {
|
|
|
790
932
|
`running in the background so replies are heard and the hold stays fresh.`
|
|
791
933
|
);
|
|
792
934
|
}
|
|
935
|
+
// A non-holder who has joined the room: post as a room member instead of failing.
|
|
936
|
+
if (err instanceof QuoxApiError && err.status === 403 && !options.room && from && /not (the current )?slot holder/i.test(said)) {
|
|
937
|
+
try {
|
|
938
|
+
data = await client.post(ep.path, { service: ep.service, ...attachAuthFor(name), body: { ...body, from: undefined, room_session: from } });
|
|
939
|
+
break;
|
|
940
|
+
} catch { /* not a member either: surface the original error with its join hint */ }
|
|
941
|
+
}
|
|
942
|
+
// HOLDER-SAY: the session name matches the hold, but this CLI speaks as a different identity than the one
|
|
943
|
+
// that took it (e.g. the default profile changed since checkout). Never bypassed: say exactly what to do.
|
|
944
|
+
if (err instanceof QuoxApiError && err.status === 403 && from && err.detail?.holder_session === from && /not (the current )?slot holder|not the current holder/i.test(said)) {
|
|
945
|
+
throw new Error(
|
|
946
|
+
`${name} is held by ${from}, but that hold is bound to the identity that checked it out, and this CLI ` +
|
|
947
|
+
`(profile ${profileName() || 'default'}) speaks as a different one. Say it as the profile that checked it out ` +
|
|
948
|
+
`(\`quox -p <that profile> slot say ${name} --session ${from} ...\`), or re-take the hold as this identity: ` +
|
|
949
|
+
`\`quox slot release ${name} --session ${from} && quox slot checkout ${name} --session ${from}\`.`
|
|
950
|
+
);
|
|
951
|
+
}
|
|
793
952
|
throw err;
|
|
794
953
|
}
|
|
795
954
|
}
|
|
796
955
|
printSuccess(`Said into ${name}.`);
|
|
956
|
+
if (!options.room && !fromSlot) {
|
|
957
|
+
printWarning(`${from || 'This session'} holds no slot, so replies cannot reach it. Claim one: quox slot checkout <name> --create --session ${from || '<session>'}`);
|
|
958
|
+
} else if (fromSlot && data?.from_slot_dropped) {
|
|
959
|
+
printWarning(`${fromSlot} was not accepted as your reply address (${data.from_slot_dropped}), so replies cannot reach you; re-check it out: quox slot checkout ${fromSlot} --session ${from || '<session>'}`);
|
|
960
|
+
}
|
|
961
|
+
reportPresence(name, data?.presence);
|
|
962
|
+
reportHumanPresence(data?.human_presence);
|
|
797
963
|
reportBridgeDelivery(data?.bridges);
|
|
798
964
|
if (program.opts().output === 'json') formatDetail(data, { format: 'json' });
|
|
799
965
|
});
|
|
800
966
|
|
|
967
|
+
// QUOXON-BROADCAST: one call fans a message out to every held slot in the org (or --slots).
|
|
968
|
+
slot
|
|
969
|
+
.command('broadcast')
|
|
970
|
+
.description('Broadcast a message to every held slot in this org (--slots to pick, --priority urgent, --ack to track acknowledgements)')
|
|
971
|
+
.argument('<text...>', 'message text')
|
|
972
|
+
.option('--slots <list>', 'comma-separated slot names instead of every held slot')
|
|
973
|
+
.option('--priority <p>', 'normal or urgent', 'normal')
|
|
974
|
+
.option('--ack', 'ask recipients to acknowledge with /ack <id>')
|
|
975
|
+
.option('--session <s>', 'session name to sign as (default: $QUOX_STREAM)')
|
|
976
|
+
.action(async (textParts, options) => {
|
|
977
|
+
auth.requireAuth();
|
|
978
|
+
const parsed = parseBroadcastArgs([
|
|
979
|
+
options.slots ? `--slots ${options.slots}` : '', `--priority ${options.priority}`, options.ack ? '--ack' : '', textParts.join(' '),
|
|
980
|
+
].filter(Boolean).join(' '));
|
|
981
|
+
if (!parsed.ok) throw new Error(parsed.message);
|
|
982
|
+
const from = options.session || process.env.QUOX_STREAM || null;
|
|
983
|
+
const ep = resolveEndpoint('v1.slots.broadcast');
|
|
984
|
+
const data = await client.post(ep.path, {
|
|
985
|
+
service: ep.service, retries: 0,
|
|
986
|
+
body: { text: parsed.text, priority: parsed.priority, ack: parsed.ack, ...(parsed.slots ? { slots: parsed.slots } : {}), ...(from ? { from } : {}), org_id: resolveOrgId(auth, program) },
|
|
987
|
+
});
|
|
988
|
+
if (program.opts().output === 'json') return formatDetail(data, { format: 'json' });
|
|
989
|
+
for (const [line, kind] of broadcastSummary(data)) (kind === 'warn' ? printWarning : printInfo)(line);
|
|
990
|
+
reportHumanPresence(data?.human_presence);
|
|
991
|
+
return undefined;
|
|
992
|
+
});
|
|
993
|
+
|
|
994
|
+
slot
|
|
995
|
+
.command('broadcast-status')
|
|
996
|
+
.description('Delivered / acknowledged / awaiting counts for a broadcast')
|
|
997
|
+
.argument('<id>', 'broadcast id (bc_...)')
|
|
998
|
+
.action(async (id) => {
|
|
999
|
+
auth.requireAuth();
|
|
1000
|
+
const ep = resolveEndpoint('v1.slots.broadcast.get', { id });
|
|
1001
|
+
const data = await client.get(ep.path, { service: ep.service, retries: 0, query: { org_id: resolveOrgId(auth, program) } });
|
|
1002
|
+
if (program.opts().output === 'json') return formatDetail(data, { format: 'json' });
|
|
1003
|
+
printInfo(deliveryLine(data));
|
|
1004
|
+
for (const d of data.deliveries || []) printInfo(` ${d.slot}: ${d.status}${d.room ? `, room ${d.room}` : ''}${d.acked ? ', acknowledged' : ''}`);
|
|
1005
|
+
return undefined;
|
|
1006
|
+
});
|
|
1007
|
+
|
|
1008
|
+
slot
|
|
1009
|
+
.command('ack')
|
|
1010
|
+
.description('Acknowledge a broadcast on behalf of a slot you hold')
|
|
1011
|
+
.argument('<id>', 'broadcast id (bc_...)')
|
|
1012
|
+
.requiredOption('--slot <name>', 'the slot acknowledging')
|
|
1013
|
+
.action(async (id, options) => {
|
|
1014
|
+
auth.requireAuth();
|
|
1015
|
+
const a = parseAckArgs(id);
|
|
1016
|
+
if (!a.ok) throw new Error(a.message);
|
|
1017
|
+
const ep = resolveEndpoint('v1.slots.broadcast.ack', { id });
|
|
1018
|
+
const data = await client.post(ep.path, { service: ep.service, retries: 0, body: { slot: options.slot, org_id: resolveOrgId(auth, program) } });
|
|
1019
|
+
if (program.opts().output === 'json') return formatDetail(data, { format: 'json' });
|
|
1020
|
+
printSuccess(`Acknowledged ${id} as ${options.slot}. ${deliveryLine(data)}`);
|
|
1021
|
+
return undefined;
|
|
1022
|
+
});
|
|
1023
|
+
|
|
801
1024
|
/**
|
|
802
1025
|
* Print what actually reached each bridge.
|
|
803
1026
|
*
|
|
@@ -812,6 +1035,21 @@ export function registerSlotCommands(program, ctx) {
|
|
|
812
1035
|
* Silent when no bridges are configured: an org that has not set one up does
|
|
813
1036
|
* not need a line about it on every say.
|
|
814
1037
|
*/
|
|
1038
|
+
function agoText(iso) {
|
|
1039
|
+
const m = iso ? Math.max(0, Math.round((Date.now() - Date.parse(iso)) / 60000)) : null;
|
|
1040
|
+
return m == null || Number.isNaN(m) ? 'a while ago' : m < 1 ? 'just now' : m < 60 ? `${m}m ago` : `${Math.round(m / 60)}h ago`;
|
|
1041
|
+
}
|
|
1042
|
+
// PRESENCE (owner 2026-10-16): the human this slot belongs to is away; approvals will queue. Silent otherwise.
|
|
1043
|
+
function reportHumanPresence(hp) {
|
|
1044
|
+
if (hp && hp.away && hp.notice) printInfo(` ⏸ ${hp.notice}`);
|
|
1045
|
+
}
|
|
1046
|
+
// Same wording as the TUI (quoxon/slotChat presenceLine). Silent when an older server sends no presence.
|
|
1047
|
+
function reportPresence(name, presence) {
|
|
1048
|
+
if (!presence || !presence.state) return;
|
|
1049
|
+
if (presence.state === 'listening') printInfo(` ✔ delivered to ${name} (listening now)`);
|
|
1050
|
+
else if (presence.state === 'nobody_holds') printInfo(` · nobody holds this slot`);
|
|
1051
|
+
else printInfo(` · not listening right now (${presence.last_checked_in_at ? `last checked in ${agoText(presence.last_checked_in_at)}` : 'no check-in on record'})`);
|
|
1052
|
+
}
|
|
815
1053
|
function reportBridgeDelivery(bridges) {
|
|
816
1054
|
if (!Array.isArray(bridges) || bridges.length === 0) return;
|
|
817
1055
|
for (const b of bridges) {
|
|
@@ -872,6 +1110,7 @@ export function registerSlotCommands(program, ctx) {
|
|
|
872
1110
|
.option('--since <ts>', 'resume from this ISO-8601 cursor (from a previous wait timeout)')
|
|
873
1111
|
.option('--timeout <mins>', 'give up after this many minutes of silence (default: wait forever)', (v) => parseFloat(v))
|
|
874
1112
|
.option('--once', 'single server round-trip (~50s max), exit even if silent')
|
|
1113
|
+
.option('--join', 'join the slot as a room first (read without holding it; your own posts are not echoed)')
|
|
875
1114
|
.option('--also <slots>', 'also listen on these peer slots (comma-separated, read-only): hear replies another session posts in ITS slot')
|
|
876
1115
|
.action(async (name, options) => {
|
|
877
1116
|
if (options.also) return waitMany(name, options);
|
|
@@ -884,6 +1123,10 @@ export function registerSlotCommands(program, ctx) {
|
|
|
884
1123
|
// separate heartbeat loop too.
|
|
885
1124
|
auth.requireAuth();
|
|
886
1125
|
const session = resolveSession(options, name, null);
|
|
1126
|
+
if (options.join) {
|
|
1127
|
+
if (!session) throw new Error('--join needs a session: pass --session or set QUOX_STREAM');
|
|
1128
|
+
await joinRoom(name, session, null);
|
|
1129
|
+
}
|
|
887
1130
|
const ep = resolveEndpoint('v1.slots.wait', { name });
|
|
888
1131
|
const deadline = Number.isFinite(options.timeout) && options.timeout > 0
|
|
889
1132
|
? Date.now() + options.timeout * 60 * 1000
|
|
@@ -896,7 +1139,7 @@ export function registerSlotCommands(program, ctx) {
|
|
|
896
1139
|
const query = { org_id: resolveOrgId(auth, program), timeout_ms: 50000 };
|
|
897
1140
|
if (cursor) query.since = cursor;
|
|
898
1141
|
if (session) {
|
|
899
|
-
query.holder_session = session;
|
|
1142
|
+
if (!options.join) query.holder_session = session; // a room member does not hold
|
|
900
1143
|
query.exclude_from = session;
|
|
901
1144
|
}
|
|
902
1145
|
let data;
|
|
@@ -956,7 +1199,7 @@ export function registerSlotCommands(program, ctx) {
|
|
|
956
1199
|
// quox#662/C: a wait whose hold is gone used to warn once and keep
|
|
957
1200
|
// polling forever: deaf AND unheld. Exit non-zero so whatever
|
|
958
1201
|
// supervises this process (the Claude background-task wake) notices.
|
|
959
|
-
const holdLost = !!session && data.held === false;
|
|
1202
|
+
const holdLost = !!session && !options.join && data.held === false;
|
|
960
1203
|
const holdLostLine = `HOLD LOST: ${name} is no longer held by ${session}; re-run quox slot checkout ${name} --session ${session}`;
|
|
961
1204
|
|
|
962
1205
|
if ((data.messages || []).length > 0) {
|
|
@@ -1030,6 +1273,16 @@ export function registerSlotCommands(program, ctx) {
|
|
|
1030
1273
|
const tmuxPane = validTmuxPane(options.tmuxPane) ? options.tmuxPane : (validTmuxPane(process.env.TMUX_PANE) ? process.env.TMUX_PANE : null);
|
|
1031
1274
|
const res = await runKeeper({
|
|
1032
1275
|
name, session, claude: me, probe: deps.probe,
|
|
1276
|
+
// KEEPER-REEXEC: track the installed build; on a change start a successor with the SAME
|
|
1277
|
+
// args (this keeper has already dropped its lock) and exit, keeping the hold.
|
|
1278
|
+
buildId: () => readInstalledBuild(CLI_DIR),
|
|
1279
|
+
reexec: () => {
|
|
1280
|
+
if (!isQuoxEntry(process.argv[1])) throw new Error('not running as the quox CLI');
|
|
1281
|
+
const fd = openSync(join(keeperDir(), `${name}.log`), 'a');
|
|
1282
|
+
try {
|
|
1283
|
+
spawn(process.execPath, process.argv.slice(1), { detached: true, stdio: ['ignore', fd, fd], env: process.env }).unref();
|
|
1284
|
+
} finally { closeSync(fd); }
|
|
1285
|
+
},
|
|
1033
1286
|
sleep: ctx.sleep,
|
|
1034
1287
|
tmuxPane,
|
|
1035
1288
|
pollControl: async () => {
|
package/lib/commands/status.js
CHANGED
|
@@ -100,7 +100,7 @@ export function registerStatusCommand(program, ctx) {
|
|
|
100
100
|
// actually reaches the collector instead of being withheld.
|
|
101
101
|
const data = await client.get(resolveEndpoint('v1.health.aggregate.list').path, {
|
|
102
102
|
service: resolveEndpoint('v1.health.aggregate.list').service,
|
|
103
|
-
timeout:
|
|
103
|
+
timeout: 15000, // #930: the aggregate fans out to several services; 5s was too tight under load
|
|
104
104
|
});
|
|
105
105
|
|
|
106
106
|
if (isJson) {
|
package/lib/commands/team.js
CHANGED
|
@@ -99,7 +99,9 @@ export function registerTeamCommands(program, ctx) {
|
|
|
99
99
|
.argument('<teamId>', 'team ID')
|
|
100
100
|
.action(async (teamId) => {
|
|
101
101
|
auth.requireAuth();
|
|
102
|
-
const
|
|
102
|
+
const orgId = resolveOrgId(auth, program);
|
|
103
|
+
const ep = resolveEndpoint('teams.provisionRoom', { orgId, teamId });
|
|
104
|
+
const data = await client.post(ep.path, { service: ep.service, body: {} });
|
|
103
105
|
printSuccess('Team Matrix room provisioned.');
|
|
104
106
|
const room = data.room || data;
|
|
105
107
|
formatDetail(room, { format: program.opts().output, title: 'Matrix Room' });
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* quox tips [--json]
|
|
3
|
+
*
|
|
4
|
+
* The canned "did you know" tips behind the terminal's /tip (QUOXON-TIPS). Offline: this prints the registry
|
|
5
|
+
* (cli/lib/quoxon/tips.js), not what is ready on your install; the terminal applies the honesty gate
|
|
6
|
+
* (shipped maturity, capabilities contract, /tip-settings) when it shows one. --json (or -o json) is the
|
|
7
|
+
* shape the dashboard and cli.quox.ai reuse.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { TIPS, tipsJson, tipsReply } from '../quoxon/tips.js';
|
|
11
|
+
|
|
12
|
+
export function registerTipsCommands(program) {
|
|
13
|
+
program
|
|
14
|
+
.command('tips')
|
|
15
|
+
.description('List the "did you know" tips the terminal shows (/tip); --json for the registry as data')
|
|
16
|
+
.argument('[query...]', 'search the tips by topic or words (like /tips); bare lists every tip')
|
|
17
|
+
.option('--json', 'print the registry as JSON')
|
|
18
|
+
.action((query, opts) => {
|
|
19
|
+
if (opts.json || program.opts().output === 'json') { console.log(JSON.stringify(tipsJson(), null, 2)); return; }
|
|
20
|
+
if (query?.length) { for (const l of tipsReply(query.join(' ')).lines) console.log(l); return; }
|
|
21
|
+
for (const t of TIPS) console.log(`${t.id} [${t.product} · ${t.tier} · ${t.maturity}] ${t.text} Try: ${t.try}`);
|
|
22
|
+
});
|
|
23
|
+
}
|