quoxcli 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +61 -0
- package/README.md +2 -2
- package/bin/quox.js +19 -1
- package/bin/quoxon.js +6 -1
- package/lib/api/endpoints/admin.js +7 -0
- package/lib/api/endpoints/agent-attach.js +7 -0
- package/lib/api/endpoints/compliance.js +25 -0
- package/lib/api/endpoints/inbox.js +6 -0
- package/lib/api/endpoints/inboxq.js +19 -0
- package/lib/api/endpoints/index.js +2 -0
- package/lib/api/endpoints/mon.js +122 -0
- package/lib/api/endpoints/tools.js +13 -0
- package/lib/api/endpoints/v1.js +13 -0
- package/lib/commands/admin.js +35 -0
- package/lib/commands/agentAttach.js +15 -0
- package/lib/commands/approvals.js +193 -3
- package/lib/commands/ask.js +1 -1
- package/lib/commands/auth.js +64 -11
- package/lib/commands/backup.js +11 -4
- package/lib/commands/capabilities.js +3 -2
- package/lib/commands/chat.js +15 -0
- package/lib/commands/claude.js +4 -1
- package/lib/commands/compliance.js +109 -0
- package/lib/commands/connect.js +69 -0
- package/lib/commands/dev.js +6 -8
- package/lib/commands/doctor.js +22 -3
- package/lib/commands/evidence.js +2 -1
- package/lib/commands/gripe.js +24 -13
- package/lib/commands/inbox.js +37 -3
- package/lib/commands/inboxq.js +147 -0
- package/lib/commands/kuma.js +78 -5
- package/lib/commands/licenceLinkView.js +311 -0
- package/lib/commands/linkedin.js +19 -0
- package/lib/commands/loop.js +30 -3
- package/lib/commands/lucid.js +1 -0
- package/lib/commands/mail.js +161 -20
- package/lib/commands/memory.js +68 -4
- package/lib/commands/memorySync.js +158 -0
- package/lib/commands/mon.js +55 -144
- package/lib/commands/notes.js +29 -7
- package/lib/commands/notesSeal.js +259 -0
- package/lib/commands/objective.js +5 -2
- package/lib/commands/offload.js +2 -3
- package/lib/commands/plan.js +17 -0
- package/lib/commands/provider.js +29 -8
- package/lib/commands/qlarity.js +32 -1
- package/lib/commands/run.js +2 -1
- package/lib/commands/server.js +37 -0
- package/lib/commands/setup.js +127 -75
- package/lib/commands/skills.js +4 -1
- package/lib/commands/slot.js +49 -9
- package/lib/commands/status.js +92 -44
- package/lib/commands/tool.js +30 -0
- package/lib/commands/vault.js +6 -3
- package/lib/commands/walkthrough.js +14 -2
- package/lib/commands/watch.js +11 -2
- package/lib/commands/wazuh.js +824 -0
- package/lib/commands/wazuhIntake.js +317 -0
- package/lib/commands/wazuhReport.js +192 -0
- package/lib/commands/workflow.js +47 -4
- package/lib/commands/x.js +50 -0
- package/lib/core/announce.js +86 -0
- package/lib/core/auth.js +20 -2
- package/lib/core/callMe.js +118 -0
- package/lib/core/client.js +27 -1
- package/lib/core/commandRegistrars.js +8 -0
- package/lib/core/config.js +28 -2
- package/lib/core/confirm.js +68 -7
- package/lib/core/crossOrgApprovals.js +69 -0
- package/lib/core/errors.js +52 -1
- package/lib/core/governedSession.js +135 -5
- package/lib/core/inboxResolution.js +3 -2
- package/lib/core/loginGuard.js +73 -0
- package/lib/core/offload.js +37 -0
- package/lib/core/org-override.js +5 -1
- package/lib/core/output.js +1 -1
- package/lib/core/profileTrust.js +182 -0
- package/lib/core/quoxglyph.js +22 -1
- package/lib/core/router.js +14 -0
- package/lib/core/stepUpPrompt.js +99 -0
- package/lib/core/version.js +1 -1
- package/lib/core/walkthrough.js +9 -5
- package/lib/dragon/select.js +2 -2
- package/lib/quoxon/EXTENSIONS.md +142 -19
- package/lib/quoxon/api.js +185 -34
- package/lib/quoxon/app.js +1032 -152
- package/lib/quoxon/approvalBatch.js +117 -0
- package/lib/quoxon/approvalFacts.js +56 -2
- package/lib/quoxon/approvalSentence.js +8 -4
- package/lib/quoxon/approvalsView.js +69 -11
- package/lib/quoxon/autoOpen.js +94 -0
- package/lib/quoxon/backgroundWork.js +59 -0
- package/lib/quoxon/bang.js +1 -1
- package/lib/quoxon/capabilities.js +7 -2
- package/lib/quoxon/commandShort.js +7 -0
- package/lib/quoxon/commands.js +40 -10
- package/lib/quoxon/connect.js +5 -3
- package/lib/quoxon/deckLazy.js +58 -0
- package/lib/quoxon/destructive.js +35 -0
- package/lib/quoxon/dnd.js +71 -0
- package/lib/quoxon/dragonCompanion.js +37 -7
- package/lib/quoxon/dragonMoments.js +78 -0
- package/lib/quoxon/echo.js +1 -0
- package/lib/quoxon/events.js +43 -7
- package/lib/quoxon/extensions.js +398 -38
- package/lib/quoxon/gripeQueue.js +135 -0
- package/lib/quoxon/inboxView.js +125 -22
- package/lib/quoxon/keys.js +2 -2
- package/lib/quoxon/loopCard.js +5 -1
- package/lib/quoxon/loopCards.js +17 -8
- package/lib/quoxon/loopStream.js +20 -9
- package/lib/quoxon/mailView.js +71 -0
- package/lib/quoxon/main.js +20 -5
- package/lib/quoxon/markdown.js +1 -1
- package/lib/quoxon/model.js +15 -0
- package/lib/quoxon/neverAnswered.js +20 -0
- package/lib/quoxon/objects/away.js +43 -5
- package/lib/quoxon/onboarding/connectServer.js +162 -0
- package/lib/quoxon/onboarding/copy.js +6 -0
- package/lib/quoxon/onboarding/index.js +5 -43
- package/lib/quoxon/onboarding/probe.js +20 -1
- package/lib/quoxon/onboarding/view.js +16 -3
- package/lib/quoxon/orgSwitch.js +22 -0
- package/lib/quoxon/print.js +2 -0
- package/lib/quoxon/profileSwitch.js +7 -0
- package/lib/quoxon/promptHistory.js +3 -1
- package/lib/quoxon/render.js +63 -14
- package/lib/quoxon/roster.js +10 -5
- package/lib/quoxon/rosterSessions.js +44 -0
- package/lib/quoxon/screenContext.js +1 -1
- package/lib/quoxon/secretShape.js +86 -0
- package/lib/quoxon/slash/agents.js +2 -1
- package/lib/quoxon/slash/argSources.js +9 -2
- package/lib/quoxon/slash/args.js +4 -2
- package/lib/quoxon/slash/broadcast.js +18 -5
- package/lib/quoxon/slash/broadcastRun.js +13 -2
- package/lib/quoxon/slash/cliLeaves.json +63 -1
- package/lib/quoxon/slash/hatWalk.js +1 -1
- package/lib/quoxon/slash/palette.js +22 -1
- package/lib/quoxon/slash/paletteController.js +10 -4
- package/lib/quoxon/slash/restart.js +7 -4
- package/lib/quoxon/slash/stepUp.js +5 -5
- package/lib/quoxon/slash/stepUpCmds.js +74 -1
- package/lib/quoxon/slashSpinner.js +35 -0
- package/lib/quoxon/slotAttach.js +8 -0
- package/lib/quoxon/slotChat.js +17 -0
- package/lib/quoxon/slotFormat.js +1 -1
- package/lib/quoxon/slotOutbox.js +113 -0
- package/lib/quoxon/suggestCommand.js +1 -1
- package/lib/quoxon/term.js +25 -1
- package/lib/quoxon/transcript.js +2 -1
- package/lib/quoxon/welcome.js +1 -1
- package/lib/quoxon/whatsnew.js +1 -1
- package/lib/quoxon/whatsnew.jsonl +36 -0
- package/lib/server/installFlow.js +168 -0
- package/lib/server/status.js +15 -1
- package/lib/toolOutputShape.js +55 -0
- package/package.json +3 -2
- package/vendor/dragonlab/SOURCE +3 -3
- package/vendor/dragonlab/design/quoxperticus/v2/quoxperticus-master.svg +59 -0
- package/vendor/dragonlab/design/quoxperticus/v3/character.mjs +43 -0
- package/vendor/dragonlab/design/quoxperticus/v4/character.mjs +57 -0
- package/vendor/dragonlab/design/quoxperticus/v4/review.mjs +35 -0
- package/vendor/dragonlab/design/quoxperticus/v4/watch.mjs +56 -0
- package/vendor/dragonlab/design/vulcan/v1/character.mjs +34 -0
- package/vendor/dragonlab/design/vulcan/v1/vulcan-master.svg +57 -0
- package/vendor/dragonlab/design/vulcan/v2/character.mjs +39 -0
- package/vendor/dragonlab/design/vulcan/v3/character.mjs +41 -0
- package/vendor/dragonlab/design/vulcan/v3/review.mjs +35 -0
- package/vendor/dragonlab/design/vulcan/v3/watch.mjs +56 -0
- package/vendor/dragonlab/factory/svg-rig-contour.mjs +64 -0
- package/vendor/dragonlab/integration/companions-v1/characters.mjs +20 -0
- package/vendor/dragonlab/integration/companions-v1/quoxperticus.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/view.mjs +1 -1
- package/vendor/dragonlab/integration/companions-v1/vulcan.mjs +2 -0
package/lib/quoxon/api.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* QUOXON server calls. Thin wrapper over the CLI client so the app can be
|
|
3
3
|
* driven by a fake (demo + tests). All routes per docs/QUOXON_EVENTS_v1.md.
|
|
4
4
|
*/
|
|
5
|
+
import { otherOrgsWaiting } from '../core/crossOrgApprovals.js';
|
|
5
6
|
import { resolveEndpoint } from '../api/endpoints.js';
|
|
6
7
|
import { rememberEmail } from '../core/auth.js';
|
|
7
8
|
import { isSessionDead } from './sessionDead.js'; // not signin.js: that loads React + Ink (QUOXON-LATENCY)
|
|
@@ -9,7 +10,8 @@ import { buildStreamQuery } from '../commands/chat.js';
|
|
|
9
10
|
import { loadBundledAssistantsRoster } from '../core/agentAlias.js';
|
|
10
11
|
import { bundledIdentities } from './objects/fetch.js';
|
|
11
12
|
import { recordHeldSlot } from '../commands/slot.js';
|
|
12
|
-
import { askGripe, settleGripe } from '../commands/gripe.js';
|
|
13
|
+
import { askGripe, settleGripe, fileDeterministically } from '../commands/gripe.js';
|
|
14
|
+
import { gripeQueue, fileOrQueue, makeRetryFiler } from './gripeQueue.js';
|
|
13
15
|
import { sendFeedback } from './feedback.js';
|
|
14
16
|
import { fromObjective, fromExecution, fromApproval, sortRows } from './roster.js';
|
|
15
17
|
import { normChoice } from './model.js';
|
|
@@ -21,6 +23,7 @@ import { createKillerApi } from './slash/killerApi.js';
|
|
|
21
23
|
import { createUndoOps } from './slash/undoApi.js';
|
|
22
24
|
import { resolveDashboardBase, resolveCollectorBase, detectLanIp } from './hostUrls.js';
|
|
23
25
|
import { grantBody } from './consent.js';
|
|
26
|
+
import { runCallMe } from '../core/callMe.js';
|
|
24
27
|
import { loadJourney } from './onboarding/journey.js';
|
|
25
28
|
import { joinServiceUrl } from '../core/config.js';
|
|
26
29
|
import { runVmCommand } from '../core/vmTui.js';
|
|
@@ -92,6 +95,7 @@ async function createNoteFrom(client, text) {
|
|
|
92
95
|
} catch (e) {
|
|
93
96
|
if (e?.status === 402) return { ok: false, message: 'NotesQ is not licensed for this org. ' + NOTES_OFF };
|
|
94
97
|
if (e?.status === 403 || e?.status === 404 || e?.status === 503) return { ok: false, message: NOTES_OFF };
|
|
98
|
+
if (!e?.status && /timed out|timeout|ECONNRESET|socket hang up|network|fetch failed|ETIMEDOUT/i.test(e?.message || '')) return { ok: false, maybeSaved: true, message: 'The save may still have gone through (the server was slow). /notes shows your recent notes; I won\'t retry automatically so you don\'t get a duplicate.' }; // request was sent: the outcome is unknown, not a failure
|
|
95
99
|
return { ok: false, message: `Could not save the note: ${e?.message || 'request failed'}${e?.status ? ` (HTTP ${e.status})` : ''}.` };
|
|
96
100
|
}
|
|
97
101
|
}
|
|
@@ -102,9 +106,10 @@ export function isInteractiveHuman({ stdin = process.stdin, stdout = process.std
|
|
|
102
106
|
/** A one-line title within the tasks service's 200 character cap. */
|
|
103
107
|
const clipTitle = (g) => { const t = String(g ?? '').replace(/\s+/g, ' ').trim(); return t.length > 200 ? `${t.slice(0, 199)}…` : t; };
|
|
104
108
|
|
|
105
|
-
export function createApi({ client, config, auth, windowOrg = null, clientForOrg = null, reattachBackoff = REATTACH_BACKOFF_MS }) {
|
|
109
|
+
export function createApi({ client, config, auth, windowOrg = null, clientForOrg = null, reattachBackoff = REATTACH_BACKOFF_MS, gripeQueuePath = undefined, listSessions = null, awayRetryMs = 3000 }) {
|
|
106
110
|
let holderSessionFn = null;
|
|
107
111
|
const orgId = () => { try { return auth.getOrgId(); } catch { return null; } };
|
|
112
|
+
const gripeStore = gripeQueue(gripeQueuePath);
|
|
108
113
|
const rowsStore = createRowsStore({ client, config, orgId });
|
|
109
114
|
let credsSeen = null; // credentials-file mtime last seen (credentialsChanged)
|
|
110
115
|
const userId = () => { try { return config.getAuth?.()?.user_id || null; } catch { return null; } };
|
|
@@ -161,12 +166,13 @@ export function createApi({ client, config, auth, windowOrg = null, clientForOrg
|
|
|
161
166
|
err.code = 'reattach_failed';
|
|
162
167
|
throw err;
|
|
163
168
|
}
|
|
169
|
+
const HUMAN_TUI = { 'X-Quox-Human-Action': 'tui' }; // DND contract: only a human action in the TUI sends this, never a restart or internal path
|
|
164
170
|
/** PRESENCE auto-clear: the human's own first real action. -> { ok, cleared, welcome } (welcome once: { away_text, via }). Never throws. */
|
|
165
171
|
async function noteActivity(action, { quiet = false, interactive = isInteractiveHuman() } = {}) {
|
|
166
172
|
if (!interactive) return { ok: false, cleared: false, welcome: null, skipped: 'not interactive' };
|
|
167
173
|
try {
|
|
168
174
|
const ep = resolveEndpoint('presence.activity');
|
|
169
|
-
const d = await client.post(ep.path, { service: ep.service, retries: 0, timeout: 6000, body: { action, org_id: orgId(), ...(quiet ? { quiet: true } : {}) } });
|
|
175
|
+
const d = await client.post(ep.path, { service: ep.service, retries: 0, timeout: 6000, headers: HUMAN_TUI, body: { action, org_id: orgId(), ...(quiet ? { quiet: true } : {}) } });
|
|
170
176
|
return { ok: true, cleared: d?.cleared === true, welcome: d?.welcome || null };
|
|
171
177
|
} catch (e) { return { ok: false, cleared: false, welcome: null, status: e?.status || null }; }
|
|
172
178
|
}
|
|
@@ -550,16 +556,23 @@ export function createApi({ client, config, auth, windowOrg = null, clientForOrg
|
|
|
550
556
|
* file when the agent did not). The client asks the user first, so the agent is told to file without asking.
|
|
551
557
|
* -> { ok:true, response, url, superseded, notes } | { ok:false, message }.
|
|
552
558
|
*/
|
|
553
|
-
async gripe(text, { yes = true } = {}) {
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
559
|
+
async gripe(text, { yes = true, repo = null } = {}) {
|
|
560
|
+
return fileOrQueue({
|
|
561
|
+
text, repo, queue: gripeStore,
|
|
562
|
+
run: async (key) => {
|
|
563
|
+
const options = { yes, idempotencyKey: key, queueRetryable: true, ...(repo ? { repo } : {}) };
|
|
564
|
+
const data = await askGripe({ client, complaint: text, yes, orgId: orgId() });
|
|
565
|
+
const r = await settleGripe({ client, data, complaint: text, options, orgId: orgId() });
|
|
566
|
+
return { response: r.response, url: r.url, superseded: r.superseded, notes: r.notes };
|
|
567
|
+
},
|
|
568
|
+
});
|
|
569
|
+
},
|
|
570
|
+
/** GRIPE-QUEUE: file what is queued (at most once per 2 minutes). -> { ran, filed:[{text,url}], expired:[{text}], pending }. */
|
|
571
|
+
async gripeRetry({ force = false } = {}) {
|
|
572
|
+
return gripeStore.retry(makeRetryFiler({ fileDeterministically, client, orgId: orgId() }), { force });
|
|
562
573
|
},
|
|
574
|
+
/** GRIPE-QUEUE: queued plus recently filed gripes, oldest first. */
|
|
575
|
+
gripes() { return gripeStore.list(); },
|
|
563
576
|
/**
|
|
564
577
|
* /feedback: POST the previewed payload to the public quox.ai feedback API (not the org's server, not GitHub).
|
|
565
578
|
* Anonymous by design: no token, cookie, org or profile goes with it. -> { ok:true, id, url } | { ok:false, message }.
|
|
@@ -581,6 +594,8 @@ export function createApi({ client, config, auth, windowOrg = null, clientForOrg
|
|
|
581
594
|
async todoList() { return listBoard(client); },
|
|
582
595
|
/** /todo move: move one item (real id) to todo, doing or done. -> the item. */
|
|
583
596
|
async todoMove(id, status) { return moveActionItem(client, id, status); },
|
|
597
|
+
/** /callmeforapprovals: { action, after, quiet } -> { data, changed } (same core as `quox approvals call-me`). Throws on failure. */
|
|
598
|
+
async callMe(args) { return runCallMe(client, args); },
|
|
584
599
|
/** -> [{ plugin_id, version, is_enabled, ... }] every plugin installed for the org (GET plugins.list, as `quox plugin list`). Throws on failure. */
|
|
585
600
|
async pluginList() {
|
|
586
601
|
const id = orgId();
|
|
@@ -617,6 +632,35 @@ export function createApi({ client, config, auth, windowOrg = null, clientForOrg
|
|
|
617
632
|
* ("quox1 (Adam)"); `fast` answers once the line is in the slot, without waiting on the chat-app bridges.
|
|
618
633
|
* -> { ok:true, message } | { ok:false, message, status }.
|
|
619
634
|
*/
|
|
635
|
+
/**
|
|
636
|
+
* Does this slot name exist in one of the user's OWN orgs? Reads only, never sends. Same rules as slotSay: name@org
|
|
637
|
+
* targets one membership; two other orgs holding the name is { found:true, ambiguous:true } (the send path then asks).
|
|
638
|
+
* -> { found:boolean, ambiguous?:true }. Throws when the membership list itself cannot be read.
|
|
639
|
+
*/
|
|
640
|
+
async slotLocate(name) {
|
|
641
|
+
const slug = (x) => String(x || '').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|
|
642
|
+
const clientFor = async (oid) => (oid === orgId() ? client : clientForOrg(oid));
|
|
643
|
+
const listed = async (oid, nm) => {
|
|
644
|
+
const ep = resolveEndpoint('v1.slots.list');
|
|
645
|
+
const rows = listOf(await (await clientFor(oid)).get(ep.path, { service: ep.service, retries: 0 }), 'slots') || [];
|
|
646
|
+
return rows.some(r => String(r?.name || '').toLowerCase() === String(nm).toLowerCase());
|
|
647
|
+
};
|
|
648
|
+
const oe = resolveEndpoint('orgs.list'); const d = await client.get(oe.path, { service: oe.service });
|
|
649
|
+
const orgs = (Array.isArray(d) ? d : (d?.organizations || d?.orgs || d?.data || [])).filter(o => o?.id).map(o => ({ id: o.id, name: o.name || o.id }));
|
|
650
|
+
const at = String(name).indexOf('@');
|
|
651
|
+
if (at > 0) {
|
|
652
|
+
const raw = String(name).slice(at + 1); const frag = slug(raw);
|
|
653
|
+
const hit = frag ? orgs.filter(o => o.id === raw || slug(o.name) === frag) : [];
|
|
654
|
+
const pick = hit.length ? hit : orgs.filter(o => frag && slug(o.name).startsWith(frag));
|
|
655
|
+
if (pick.length !== 1 || !clientForOrg) return { found: false };
|
|
656
|
+
return { found: await listed(pick[0].id, String(name).slice(0, at)) };
|
|
657
|
+
}
|
|
658
|
+
if (await listed(orgId(), name)) return { found: true };
|
|
659
|
+
if (!clientForOrg) return { found: false };
|
|
660
|
+
const hits = [];
|
|
661
|
+
for (const o of orgs) { if (o.id === orgId()) continue; try { if (await listed(o.id, name)) hits.push(o); } catch { /* an org that cannot be listed is not a candidate */ } }
|
|
662
|
+
return hits.length > 1 ? { found: true, ambiguous: true } : { found: hits.length === 1 };
|
|
663
|
+
},
|
|
620
664
|
/** SLOTS-REPLY2: the window registers how to name its checkout session; slotSay then sends it as holder_session. */
|
|
621
665
|
bindHolderSession(fn) { holderSessionFn = typeof fn === 'function' ? fn : null; },
|
|
622
666
|
async slotSay(name, text, { from = null, fast = false, roomSession = null, fromSlot = null, holderSession = holderSessionFn ? holderSessionFn() : null, anyOrg = true } = {}) {
|
|
@@ -630,33 +674,66 @@ export function createApi({ client, config, auth, windowOrg = null, clientForOrg
|
|
|
630
674
|
if (slotsUnlicensed(e)) return { ok: false, unlicensed: true, status: 403, message: SLOTS_UNLICENSED };
|
|
631
675
|
return { ok: false, message: e?.status === 404 ? `no slot named ${name}` : (e?.message || 'failed'), status: e?.status || null };
|
|
632
676
|
};
|
|
677
|
+
// The user's own memberships (orgs.list is scoped to the caller); nothing outside this list is ever tried.
|
|
678
|
+
const memberOrgs = async () => {
|
|
679
|
+
try {
|
|
680
|
+
const oe = resolveEndpoint('orgs.list'); const d = await client.get(oe.path, { service: oe.service });
|
|
681
|
+
return (Array.isArray(d) ? d : (d?.organizations || d?.orgs || d?.data || [])).filter(o => o?.id).map(o => ({ id: o.id, name: o.name || o.id }));
|
|
682
|
+
} catch { return []; }
|
|
683
|
+
};
|
|
684
|
+
const slug = (x) => String(x || '').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|
|
685
|
+
const clientFor = async (oid) => (oid === orgId() ? client : clientForOrg(oid));
|
|
686
|
+
const at = String(name).indexOf('@');
|
|
687
|
+
if (at > 0) { // name@org: exactly that membership, never the others
|
|
688
|
+
const base = String(name).slice(0, at); const frag = slug(String(name).slice(at + 1));
|
|
689
|
+
const orgs = anyOrg && clientForOrg ? await memberOrgs() : [];
|
|
690
|
+
const hit = frag ? orgs.filter(o => o.id === String(name).slice(at + 1) || slug(o.name) === frag) : [];
|
|
691
|
+
const pick = hit.length ? hit : orgs.filter(o => frag && slug(o.name).startsWith(frag));
|
|
692
|
+
if (pick.length !== 1) return { ok: false, status: 404, message: pick.length ? `"${name}" matches several of your orgs: ${pick.map(o => `${base}@${slug(o.name)}`).join(', ')}` : `no slot named ${name}` };
|
|
693
|
+
try { const r = await send(await clientFor(pick[0].id), pick[0].id); return { ...r, viaOrgId: pick[0].id, viaOrgName: pick[0].name }; } catch (e) { return { ...fail(e), viaOrgId: pick[0].id, viaOrgName: pick[0].name }; }
|
|
694
|
+
}
|
|
633
695
|
let first;
|
|
634
696
|
try { return await send(client, orgId()); } catch (e) { first = fail(e); }
|
|
635
697
|
// HITL-INBOX-BAR: a window in another org cannot see a slot that lives in the profile's home org. Same user, same
|
|
636
|
-
// sessions (not a cross-tenant leak):
|
|
698
|
+
// sessions (not a cross-tenant leak): look in the user's other orgs only, each on a token for THAT org.
|
|
637
699
|
if (!anyOrg || first.status !== 404 || !clientForOrg) return first;
|
|
638
700
|
const tried = new Set([orgId()]);
|
|
639
|
-
const
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
701
|
+
const orgs = await memberOrgs();
|
|
702
|
+
const nameOf = (oid) => orgs.find(o => o.id === oid)?.name || null;
|
|
703
|
+
// 1. Find it without sending: list slots per other org. Two orgs hold the name: ask, never pick.
|
|
704
|
+
const hits = [];
|
|
705
|
+
for (const o of orgs) {
|
|
706
|
+
if (tried.has(o.id)) continue;
|
|
707
|
+
try {
|
|
708
|
+
const ep = resolveEndpoint('v1.slots.list');
|
|
709
|
+
const rows = listOf(await (await clientFor(o.id)).get(ep.path, { service: ep.service, retries: 0 }), 'slots') || [];
|
|
710
|
+
if (rows.some(r => String(r?.name || '').toLowerCase() === String(name).toLowerCase())) hits.push(o);
|
|
711
|
+
} catch { /* an org that cannot be listed is not a candidate here; the probe below still covers it */ }
|
|
712
|
+
}
|
|
713
|
+
if (hits.length > 1) {
|
|
714
|
+
const choices = hits.map(o => ({ orgId: o.id, orgName: o.name, ref: `${name}@${slug(o.name)}` }));
|
|
715
|
+
return { ok: false, ambiguous: true, choices, status: null, message: `"${name}" exists in more than one of your orgs: ${choices.map(c => `${c.ref} (${c.orgName})`).join(', ')}. Say which, e.g. /say ${choices[0].ref} ...` };
|
|
716
|
+
}
|
|
717
|
+
const order = hits.map(o => o.id);
|
|
718
|
+
if (!order.length) { // nothing listed it: fall back to trying the home org, then each membership
|
|
719
|
+
try { const home = config.getAuth?.()?.org_id; if (home) order.push(home); } catch { /* no home org known */ }
|
|
720
|
+
for (const o of orgs) order.push(o.id);
|
|
721
|
+
}
|
|
645
722
|
for (const oid of order) {
|
|
646
723
|
if (tried.has(oid)) continue; tried.add(oid);
|
|
647
724
|
try {
|
|
648
|
-
const cl = await
|
|
725
|
+
const cl = await clientFor(oid);
|
|
649
726
|
const r = await send(cl, oid);
|
|
650
|
-
return { ...r, viaOrgId: oid };
|
|
651
|
-
} catch (e) { const f = fail(e); if (f.status !== 404) return { ...f, viaOrgId: oid }; } // the slot is there but refused: that is the real answer
|
|
727
|
+
return { ...r, viaOrgId: oid, viaOrgName: nameOf(oid) };
|
|
728
|
+
} catch (e) { const f = fail(e); if (f.status !== 404) return { ...f, viaOrgId: oid, viaOrgName: nameOf(oid) }; } // the slot is there but refused: that is the real answer
|
|
652
729
|
}
|
|
653
730
|
return first;
|
|
654
731
|
},
|
|
655
732
|
/** QUOXON-BROADCAST: fan a message out to the org's held slots. -> { ok:true, data } | { ok:false, message, status }. */
|
|
656
|
-
async slotBroadcast({ text, slots = null, priority = 'normal', ack = false, from = null } = {}) {
|
|
733
|
+
async slotBroadcast({ text, slots = null, priority = 'normal', ack = false, active = false, dryRun = false, from = null } = {}) {
|
|
657
734
|
const ep = resolveEndpoint('v1.slots.broadcast');
|
|
658
735
|
try {
|
|
659
|
-
const data = await client.post(ep.path, { service: ep.service, retries: 0, body: { text, priority, ack, org_id: orgId(), ...(slots ? { slots } : {}), ...(from ? { from } : {}) } });
|
|
736
|
+
const data = await client.post(ep.path, { service: ep.service, retries: 0, body: { text, priority, ack, ...(active ? { active: true } : {}), ...(dryRun ? { dry_run: true } : {}), org_id: orgId(), ...(slots ? { slots } : {}), ...(from ? { from } : {}) } });
|
|
660
737
|
return { ok: true, data };
|
|
661
738
|
} catch (e) {
|
|
662
739
|
if (slotsUnlicensed(e)) return { ok: false, unlicensed: true, status: 403, message: SLOTS_UNLICENSED };
|
|
@@ -859,8 +936,26 @@ export function createApi({ client, config, auth, windowOrg = null, clientForOrg
|
|
|
859
936
|
return l.map(e => fromExecution(e, now, { userId: userId() })).filter(Boolean);
|
|
860
937
|
})
|
|
861
938
|
]);
|
|
939
|
+
// Attached agents (org list, owner/admin) and this box's live `claude --resume` processes. Neither is a work row:
|
|
940
|
+
// they ride on the source as `items` and /roster prints them (rosterSessions.js).
|
|
941
|
+
const itemsSrc = (id, label, fn) => fn().then(
|
|
942
|
+
(items) => ({ id, label, state: 'connected', rows: [], items, count: items.length }),
|
|
943
|
+
(e) => ({ id, label, rows: [], items: [], count: 0, state: missing(e) ? 'unavailable' : 'error', note: missing(e) ? 'no list endpoint on this server' : (e?.message || 'failed') }));
|
|
944
|
+
const extra = await Promise.all([
|
|
945
|
+
itemsSrc('agents', 'attached agents', async () => {
|
|
946
|
+
const ep = resolveEndpoint('agent-attach.list');
|
|
947
|
+
const d = await client.get(ep.path, { service: ep.service, retries: 0 });
|
|
948
|
+
const l = listOf(d, 'attaches'); if (!l) throw new Error('unexpected response');
|
|
949
|
+
return l;
|
|
950
|
+
}),
|
|
951
|
+
itemsSrc('boxsessions', "this box's sessions", async () => {
|
|
952
|
+
if (listSessions) return await listSessions();
|
|
953
|
+
const [{ listLiveSessions }, { spawnSync }] = await Promise.all([import('../core/governedSession.js'), import('node:child_process')]);
|
|
954
|
+
return listLiveSessions({ spawnSync, pid: process.pid });
|
|
955
|
+
})
|
|
956
|
+
]);
|
|
862
957
|
const unlisted = [
|
|
863
|
-
{
|
|
958
|
+
...extra.map(({ rows, ...rest }) => rest),
|
|
864
959
|
{ id: 'devsessions', label: 'dev sessions', state: 'unavailable', count: 0, note: 'listed per host only (quox dev sessions <host>)' }
|
|
865
960
|
];
|
|
866
961
|
return { rows: sortRows(parts.flatMap(p => p.rows)), sources: [...parts.map(({ rows, ...rest }) => rest), ...unlisted] };
|
|
@@ -896,18 +991,31 @@ export function createApi({ client, config, auth, windowOrg = null, clientForOrg
|
|
|
896
991
|
return { situation, approvals };
|
|
897
992
|
},
|
|
898
993
|
/** PRESENCE: PUT /api/v1/presence (/afk, /sleep, /back, a typed turn). -> { ok:true, presence, notice } | { ok:false, message, status }. */
|
|
899
|
-
async setPresence(state, { note, slots } = {}) {
|
|
994
|
+
async setPresence(state, { note, slots, dnd = false, human = false } = {}) {
|
|
900
995
|
const ep = resolveEndpoint('presence.set');
|
|
901
996
|
try {
|
|
902
|
-
const d = await client.put(ep.path, { service: ep.service, retries: 0, timeout: 8000, body: { state, org_id: orgId(), ...(note ? { note } : {}), ...(Array.isArray(slots) && slots.length ? { slots } : {}) } });
|
|
997
|
+
const d = await client.put(ep.path, { service: ep.service, retries: 0, timeout: 8000, ...(human ? { headers: HUMAN_TUI } : {}), body: { state, org_id: orgId(), ...(dnd ? { dnd: true } : {}), ...(note ? { note } : {}), ...(Array.isArray(slots) && slots.length ? { slots } : {}) } });
|
|
903
998
|
return { ok: true, presence: d?.presence || null, notice: d?.notice || null };
|
|
904
999
|
} catch (e) { return { ok: false, message: e?.message || 'failed', status: e?.status || null }; }
|
|
905
1000
|
},
|
|
1001
|
+
/** AFK-SURVIVES-RESTART: read-only GET /api/v1/presence/me. -> { ok:true, presence } | { ok:false, status }. Never throws, never writes. */
|
|
1002
|
+
async getPresence() {
|
|
1003
|
+
try {
|
|
1004
|
+
const ep = resolveEndpoint('presence.get', { who: 'me' });
|
|
1005
|
+
const d = await client.get(ep.path, { service: ep.service, query: { org_id: orgId() }, retries: 0, timeout: 6000 });
|
|
1006
|
+
return { ok: true, presence: d?.presence || null };
|
|
1007
|
+
} catch (e) { return { ok: false, message: e?.message || 'failed', status: e?.status || null }; }
|
|
1008
|
+
},
|
|
906
1009
|
noteActivity,
|
|
907
1010
|
/** E5 /away: GET /api/v1/away?since=<iso> -> { since, until, items, activity, blind }. Throws on non-2xx. */
|
|
908
1011
|
async away(since) {
|
|
909
1012
|
const ep = resolveEndpoint('away.digest');
|
|
910
|
-
|
|
1013
|
+
const read = () => client.get(ep.path, { service: ep.service, query: { since }, retries: 0, timeout: 15000 });
|
|
1014
|
+
const first = await read();
|
|
1015
|
+
// AWAY-RETRY: the first calls after a fresh sign-in can time out; try those sources once more before saying "Can't see".
|
|
1016
|
+
if (!(first?.blind || []).some((b) => /timed? ?out|timeout/i.test(String(b?.reason || '')))) return first;
|
|
1017
|
+
await new Promise((r) => setTimeout(r, awayRetryMs));
|
|
1018
|
+
try { return (await read()) || first; } catch { return first; } // the retry also failing leaves the honest first answer
|
|
911
1019
|
},
|
|
912
1020
|
/** Facts for /observers, each part on its own: { lucid, learning:[{ id, label, state, text }] }. */
|
|
913
1021
|
async observers() {
|
|
@@ -1074,12 +1182,42 @@ export function createApi({ client, config, auth, windowOrg = null, clientForOrg
|
|
|
1074
1182
|
async hyperion() { const data = await fetchActivity(client); return { lines: heldLines(data), tone: data.nodes_error || data.pool_error ? 'warn' : undefined }; },
|
|
1075
1183
|
approvals: listApprovals,
|
|
1076
1184
|
/** HITL-INBOX-BAR: the owner's latest inbox items (any kind, active), newest first. Throws on failure, never reads as empty. */
|
|
1077
|
-
async inboxItems({ limit = 30 } = {}) {
|
|
1185
|
+
async inboxItems({ limit = 30, page = 1 } = {}) {
|
|
1078
1186
|
const ep = resolveEndpoint('inbox.list', { orgId: orgId() });
|
|
1079
|
-
const data = await client.get(ep.path, { service: ep.service, query: { limit } });
|
|
1187
|
+
const data = await client.get(ep.path, { service: ep.service, query: page > 1 ? { limit, page } : { limit } });
|
|
1080
1188
|
const items = data?.items || data?.data || data || [];
|
|
1081
1189
|
return Array.isArray(items) ? items : [];
|
|
1082
1190
|
},
|
|
1191
|
+
/** TUI-MAIL: connected InboxQ mailboxes (GET /api/v1/inboxq/accounts, the `quox mail accounts` read). Throws on failure, never reads as none. */
|
|
1192
|
+
async mailAccounts() {
|
|
1193
|
+
const ep = resolveEndpoint('inboxq.accounts.list');
|
|
1194
|
+
const data = await client.get(ep.path, { service: ep.service, retries: 0 });
|
|
1195
|
+
return Array.isArray(data?.accounts) ? data.accounts : [];
|
|
1196
|
+
},
|
|
1197
|
+
/** TUI-MAIL: triaged messages (GET /api/v1/inboxq/messages, the `quox mail list` read). Throws on failure. */
|
|
1198
|
+
async mailMessages({ limit = 10, zone } = {}) {
|
|
1199
|
+
const ep = resolveEndpoint('inboxq.messages.list');
|
|
1200
|
+
const data = await client.get(ep.path, { service: ep.service, retries: 0, query: { limit, offset: 0, ...(zone ? { zone } : {}) } });
|
|
1201
|
+
return Array.isArray(data?.messages) ? data.messages : [];
|
|
1202
|
+
},
|
|
1203
|
+
/** TUI-MAIL: one message with its stored body (GET /api/v1/inboxq/messages/:id, read only: no body fetch from the mailbox). */
|
|
1204
|
+
async mailMessage(id) {
|
|
1205
|
+
const ep = resolveEndpoint('inboxq.messages.get', { id });
|
|
1206
|
+
const data = await client.get(ep.path, { service: ep.service, retries: 0 });
|
|
1207
|
+
return data?.message || null;
|
|
1208
|
+
},
|
|
1209
|
+
/** Every inbox item, paged (100 per page, capped at maxPages). Throws on failure, never returns a partial list as the whole. */
|
|
1210
|
+
async inboxAllItems({ pageSize = 100, maxPages = 20 } = {}) {
|
|
1211
|
+
const ep = resolveEndpoint('inbox.list', { orgId: orgId() });
|
|
1212
|
+
const all = [];
|
|
1213
|
+
for (let page = 1; page <= maxPages; page++) {
|
|
1214
|
+
const data = await client.get(ep.path, { service: ep.service, query: { limit: pageSize, page } });
|
|
1215
|
+
const items = data?.items || data?.data || (Array.isArray(data) ? data : []);
|
|
1216
|
+
all.push(...items);
|
|
1217
|
+
if (!(data?.pagination?.has_more) || !items.length) break;
|
|
1218
|
+
}
|
|
1219
|
+
return all;
|
|
1220
|
+
},
|
|
1083
1221
|
/** One inbox item in full (the server marks it read for this user as a side effect). */
|
|
1084
1222
|
async inboxItem(itemId) {
|
|
1085
1223
|
const ep = resolveEndpoint('inbox.get', { orgId: orgId(), itemId: encodeURIComponent(itemId) });
|
|
@@ -1097,6 +1235,14 @@ export function createApi({ client, config, auth, windowOrg = null, clientForOrg
|
|
|
1097
1235
|
const d = await client.get(ep.path, { service: ep.service });
|
|
1098
1236
|
return { pending: Number(d?.pending_approvals ?? 0), awaiting: Number(d?.awaiting_human ?? 0) };
|
|
1099
1237
|
},
|
|
1238
|
+
/** Approvals waiting in the user's OTHER orgs: counts only, per-org stats on that org's own token (core/crossOrgApprovals). */
|
|
1239
|
+
async otherOrgApprovals() { return otherOrgsWaiting({ client, currentOrgId: orgId() }); },
|
|
1240
|
+
/** APPROVALS-AUTO: another org's open approvals, read with the caller's OWN token (auth checks membership of the URL org, not the token's org claim). */
|
|
1241
|
+
async approvalsInOrg(org) {
|
|
1242
|
+
const ep = resolveEndpoint('inbox.approvals.open', { orgId: org });
|
|
1243
|
+
const data = await client.get(ep.path, { service: ep.service, retries: 0 });
|
|
1244
|
+
return Array.isArray(data?.approvals) ? data.approvals.map((a) => a.item).filter(Boolean) : [];
|
|
1245
|
+
},
|
|
1100
1246
|
/** GET /api/v1/approvals/:id/result -> { status, result, executed_at } ; null when unknown (404) */
|
|
1101
1247
|
async approvalResult(approvalId) {
|
|
1102
1248
|
const ep = resolveEndpoint('v1.approvals.result', { id: approvalId });
|
|
@@ -1104,14 +1250,14 @@ export function createApi({ client, config, auth, windowOrg = null, clientForOrg
|
|
|
1104
1250
|
catch (e) { if (e?.status === 404) return null; throw e; }
|
|
1105
1251
|
},
|
|
1106
1252
|
/** quox ask: answer a decision with one of its option keys (and an optional note). */
|
|
1107
|
-
async answerDecision(itemId, option, { note = null, stepUp = null, bulk = false } = {}) {
|
|
1108
|
-
const ep = resolveEndpoint('inbox.respond', { orgId: orgId(), itemId });
|
|
1253
|
+
async answerDecision(itemId, option, { note = null, stepUp = null, bulk = false, org = null } = {}) {
|
|
1254
|
+
const ep = resolveEndpoint('inbox.respond', { orgId: org || orgId(), itemId }); // org: APPROVALS-AUTO answers another org's card by its URL org (the server checks membership of THAT org)
|
|
1109
1255
|
const r = await client.post(ep.path, { service: ep.service, headers: stepUp || {}, body: { action: 'answer', option, ...(bulk ? { bulk: true } : {}), ...(note ? { comment: String(note).slice(0, 4000) } : {}) } });
|
|
1110
1256
|
if (stepUp && Object.keys(stepUp).length) noteActivity('answer', { quiet: true }); // PRESENCE: answering is the owner being back
|
|
1111
1257
|
return r;
|
|
1112
1258
|
},
|
|
1113
|
-
async respond(itemId, action, { via = null, stepUp = null, reason = null, bulk = false } = {}) {
|
|
1114
|
-
const ep = resolveEndpoint('inbox.respond', { orgId: orgId(), itemId });
|
|
1259
|
+
async respond(itemId, action, { via = null, stepUp = null, reason = null, bulk = false, org = null } = {}) {
|
|
1260
|
+
const ep = resolveEndpoint('inbox.respond', { orgId: org || orgId(), itemId });
|
|
1115
1261
|
const tag = via ? `via quoxon ${via}` : 'via quoxon';
|
|
1116
1262
|
const why = reason ? String(reason).trim().slice(0, 4000) : '';
|
|
1117
1263
|
const r = await client.post(ep.path, { service: ep.service, headers: stepUp || {}, body: { action, ...(bulk ? { bulk: true } : {}), comment: why ? `${why} (${tag})` : tag, ...(why ? { reason: why } : {}) } });
|
|
@@ -1129,6 +1275,11 @@ export function createApi({ client, config, auth, windowOrg = null, clientForOrg
|
|
|
1129
1275
|
const ep = resolveEndpoint('stepup.status');
|
|
1130
1276
|
return client.get(ep.path, { service: ep.service });
|
|
1131
1277
|
},
|
|
1278
|
+
/** /verify-email: the SAME endpoint `quox admin users verify-email` calls. The server's checks are the only gate; the caller supplies a fresh one-shot step-up token. */
|
|
1279
|
+
async verifyEmail({ email, reason, userId = null, stepUp = {} }) {
|
|
1280
|
+
const ep = resolveEndpoint('admin.users.verify_email');
|
|
1281
|
+
return client.post(ep.path, { service: ep.service, headers: stepUp, body: { email, reason, ...(userId ? { user_id: userId } : {}) } });
|
|
1282
|
+
},
|
|
1132
1283
|
/** QUOXON-STEPUP part B: override ONE blocked action. Needs a live override-scoped step-up token. */
|
|
1133
1284
|
async override({ blockId, blockType, reason, agentId = null, action = null, stepUp = {} }) {
|
|
1134
1285
|
const ep = resolveEndpoint('safety.override');
|