quoxcli 0.3.1 → 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/actions.js +15 -0
- package/lib/api/endpoints/admin.js +27 -0
- package/lib/api/endpoints/agent-attach.js +7 -0
- package/lib/api/endpoints/blog.js +16 -0
- package/lib/api/endpoints/chat.js +7 -0
- package/lib/api/endpoints/compliance.js +25 -0
- package/lib/api/endpoints/contactsq.js +4 -0
- package/lib/api/endpoints/devServerRegistry.js +25 -0
- package/lib/api/endpoints/engagement.js +26 -0
- package/lib/api/endpoints/evidence.js +20 -0
- package/lib/api/endpoints/hat.js +14 -0
- package/lib/api/endpoints/inbox.js +6 -0
- package/lib/api/endpoints/inboxq.js +19 -0
- package/lib/api/endpoints/index.js +14 -0
- package/lib/api/endpoints/loops.js +26 -0
- package/lib/api/endpoints/mon.js +295 -0
- package/lib/api/endpoints/objectives.js +6 -0
- package/lib/api/endpoints/quoxseo.js +6 -0
- package/lib/api/endpoints/server.js +21 -0
- package/lib/api/endpoints/setup.js +6 -0
- package/lib/api/endpoints/tools.js +13 -0
- package/lib/api/endpoints/v1.js +47 -0
- package/lib/commands/actions.js +81 -11
- package/lib/commands/admin.js +35 -0
- package/lib/commands/agent.js +11 -4
- package/lib/commands/agentAttach.js +61 -7
- package/lib/commands/approvals.js +198 -3
- package/lib/commands/ask.js +4 -2
- package/lib/commands/auth.js +64 -11
- package/lib/commands/backup.js +11 -4
- package/lib/commands/blog.js +110 -0
- package/lib/commands/capabilities.js +3 -2
- package/lib/commands/chat.js +142 -28
- package/lib/commands/chatCompanion.js +27 -0
- package/lib/commands/chatState.js +45 -0
- package/lib/commands/claude.js +7 -33
- package/lib/commands/compliance.js +109 -0
- package/lib/commands/connect.js +69 -0
- package/lib/commands/contacts.js +85 -4
- package/lib/commands/contactsSendAt.js +21 -0
- package/lib/commands/dev.js +6 -8
- package/lib/commands/doctor.js +22 -3
- package/lib/commands/engagement.js +90 -0
- package/lib/commands/evidence.js +155 -0
- package/lib/commands/ext.js +23 -0
- package/lib/commands/fleet.js +25 -0
- package/lib/commands/gripe.js +24 -13
- package/lib/commands/hat.js +76 -0
- package/lib/commands/image.js +68 -0
- package/lib/commands/inbox.js +37 -3
- package/lib/commands/inboxq.js +147 -0
- package/lib/commands/kuma.js +1123 -0
- package/lib/commands/kumaSla.js +174 -0
- package/lib/commands/licence.js +39 -1
- package/lib/commands/licenceLinkView.js +311 -0
- package/lib/commands/linkedin.js +19 -0
- package/lib/commands/loop.js +318 -0
- 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 +53 -3
- package/lib/commands/offload.js +2 -3
- package/lib/commands/org.js +21 -4
- package/lib/commands/plan.js +17 -0
- package/lib/commands/provider.js +87 -8
- package/lib/commands/qlarity.js +32 -1
- package/lib/commands/quoxseo.js +31 -0
- package/lib/commands/research.js +79 -0
- package/lib/commands/rows.js +9 -0
- package/lib/commands/run.js +20 -2
- package/lib/commands/scope.js +2 -2
- package/lib/commands/server.js +150 -0
- package/lib/commands/setup.js +127 -75
- package/lib/commands/shortcut.js +41 -0
- package/lib/commands/skills.js +24 -5
- 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/vm.js +2 -2
- 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/commands/youtube.js +172 -0
- package/lib/core/actionItems.js +115 -0
- package/lib/core/agentAlias.js +2 -0
- package/lib/core/agentFamilies.js +7 -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 +37 -3
- package/lib/core/commandRegistrars.js +17 -0
- package/lib/core/config.js +29 -3
- package/lib/core/confirm.js +68 -7
- package/lib/core/crossOrgApprovals.js +69 -0
- package/lib/core/errors.js +52 -1
- package/lib/core/findingReconcile.js +90 -0
- package/lib/core/global-flags.js +1 -1
- package/lib/core/governedSession.js +136 -8
- package/lib/core/inboxResolution.js +3 -2
- package/lib/core/liveSessionsReport.js +1 -1
- package/lib/core/loginGuard.js +73 -0
- package/lib/core/loopGlyph.js +97 -0
- package/lib/core/offload.js +73 -7
- package/lib/core/org-override.js +5 -1
- package/lib/core/output.js +8 -8
- package/lib/core/profileTrust.js +182 -0
- package/lib/core/quoxglyph.js +22 -1
- package/lib/core/router.js +14 -0
- package/lib/core/sessionHeartbeat.js +12 -8
- package/lib/core/shortcut.js +83 -0
- package/lib/core/stepUpPrompt.js +99 -0
- package/lib/core/version.js +1 -1
- package/lib/core/vm.js +5 -0
- package/lib/core/walkthrough.js +9 -5
- package/lib/dragon/companionView.js +34 -0
- package/lib/dragon/frames.js +118 -0
- package/lib/dragon/jobs.js +123 -0
- package/lib/dragon/renderWorker.js +15 -0
- package/lib/dragon/select.js +72 -0
- package/lib/evidence/bundleExport.js +47 -0
- package/lib/evidence/bundleVerify.js +77 -0
- package/lib/quoxon/EXTENSIONS.md +146 -0
- package/lib/quoxon/api.js +364 -27
- package/lib/quoxon/app.js +1629 -183
- package/lib/quoxon/approvalBatch.js +117 -0
- package/lib/quoxon/approvalFacts.js +64 -8
- package/lib/quoxon/approvalSentence.js +48 -0
- package/lib/quoxon/approvalsView.js +124 -7
- 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 +22 -3
- package/lib/quoxon/commands.js +110 -15
- package/lib/quoxon/connect.js +5 -3
- package/lib/quoxon/countdownLine.js +10 -1
- package/lib/quoxon/deck.js +30 -0
- package/lib/quoxon/deckLazy.js +58 -0
- package/lib/quoxon/demo.autoplay.js +1 -1
- package/lib/quoxon/demo.js +3 -1
- package/lib/quoxon/destructive.js +35 -0
- package/lib/quoxon/dividerAocl.js +4 -3
- package/lib/quoxon/dividerRule.js +3 -3
- package/lib/quoxon/dnd.js +71 -0
- package/lib/quoxon/dragonCompanion.js +285 -0
- package/lib/quoxon/dragonModes.js +1 -0
- package/lib/quoxon/dragonMoments.js +160 -0
- package/lib/quoxon/dragonReads.js +68 -0
- package/lib/quoxon/dragonView.js +2 -2
- package/lib/quoxon/echo.js +1 -0
- package/lib/quoxon/events.js +67 -10
- package/lib/quoxon/extensions.js +636 -0
- package/lib/quoxon/gripeQueue.js +135 -0
- package/lib/quoxon/inboxView.js +182 -0
- package/lib/quoxon/installCli.js +79 -0
- package/lib/quoxon/keys.js +4 -4
- package/lib/quoxon/loopCard.js +460 -0
- package/lib/quoxon/loopCards.js +189 -0
- package/lib/quoxon/loopStream.js +98 -0
- package/lib/quoxon/mailView.js +71 -0
- package/lib/quoxon/main.js +29 -5
- package/lib/quoxon/markdown.js +1 -1
- package/lib/quoxon/model.js +15 -0
- package/lib/quoxon/mouse.js +12 -1
- package/lib/quoxon/neverAnswered.js +20 -0
- package/lib/quoxon/objectiveOffer.js +82 -0
- package/lib/quoxon/objects/away.js +43 -5
- package/lib/quoxon/objects/index.js +22 -4
- package/lib/quoxon/objects/model.js +33 -0
- package/lib/quoxon/objects/proof.js +12 -4
- 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/shortcutOffer.js +28 -0
- package/lib/quoxon/onboarding/view.js +16 -3
- package/lib/quoxon/orgSwitch.js +49 -1
- package/lib/quoxon/planSteps.js +51 -0
- package/lib/quoxon/print.js +8 -3
- package/lib/quoxon/profileSwitch.js +7 -0
- package/lib/quoxon/promptHistory.js +3 -1
- package/lib/quoxon/qdragonFly.js +7 -6
- package/lib/quoxon/qmotion.js +17 -0
- package/lib/quoxon/render.js +187 -41
- package/lib/quoxon/roster.js +36 -17
- package/lib/quoxon/rosterSessions.js +44 -0
- package/lib/quoxon/rows.js +244 -1
- package/lib/quoxon/screenContext.js +1 -1
- package/lib/quoxon/secretShape.js +86 -0
- package/lib/quoxon/serverChip.js +42 -0
- package/lib/quoxon/session.js +10 -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/cliCommand.js +42 -1
- package/lib/quoxon/slash/cliLeaves.json +139 -2
- package/lib/quoxon/slash/evidenceCmd.js +34 -0
- package/lib/quoxon/slash/hatWalk.js +1 -1
- package/lib/quoxon/slash/palette.js +23 -2
- package/lib/quoxon/slash/paletteController.js +10 -4
- package/lib/quoxon/slash/qpeCmd.js +35 -0
- package/lib/quoxon/slash/restart.js +31 -4
- package/lib/quoxon/slash/resume.js +18 -1
- package/lib/quoxon/slash/stepUp.js +5 -5
- package/lib/quoxon/slash/stepUpCmds.js +74 -1
- package/lib/quoxon/slash/todoCmd.js +122 -0
- 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 +34 -3
- package/lib/quoxon/term.js +25 -1
- package/lib/quoxon/toolCards.js +30 -0
- 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 +81 -0
- package/lib/quoxon/windowOrg.js +11 -1
- package/lib/server/devServers.js +137 -0
- package/lib/server/installFlow.js +168 -0
- package/lib/server/status.js +15 -1
- package/lib/toolOutputShape.js +55 -0
- package/package.json +4 -2
- package/vendor/dragonlab/SOURCE +4 -0
- package/vendor/dragonlab/design/cipher/v1/cipher-master.svg +46 -0
- package/vendor/dragonlab/design/cipher/v2/character.mjs +49 -0
- package/vendor/dragonlab/design/cipher/v2/review.mjs +33 -0
- package/vendor/dragonlab/design/cipher/v2/watch.mjs +56 -0
- package/vendor/dragonlab/design/drago/v1/character.mjs +19 -0
- package/vendor/dragonlab/design/drago/v1/drago-master.svg +56 -0
- package/vendor/dragonlab/design/drago/v2/character.mjs +87 -0
- package/vendor/dragonlab/design/drago/v4/character.mjs +67 -0
- package/vendor/dragonlab/design/drago/v4/profile.mjs +35 -0
- package/vendor/dragonlab/design/drago/v4/review.mjs +33 -0
- package/vendor/dragonlab/design/drago/v4/watch.mjs +56 -0
- package/vendor/dragonlab/design/hyperion/v1/hyperion-master.svg +56 -0
- package/vendor/dragonlab/design/hyperion/v2/character.mjs +79 -0
- package/vendor/dragonlab/design/hyperion/v2/review.mjs +33 -0
- package/vendor/dragonlab/design/hyperion/v2/watch.mjs +56 -0
- package/vendor/dragonlab/design/lucid/recovery/arrival.mjs +22 -0
- package/vendor/dragonlab/design/lucid/recovery/character.mjs +106 -0
- package/vendor/dragonlab/design/lucid/recovery/layout.mjs +50 -0
- package/vendor/dragonlab/design/lucid/recovery/lucid-master.svg +78 -0
- package/vendor/dragonlab/design/lucid/recovery/motion.mjs +42 -0
- package/vendor/dragonlab/design/lucid/recovery/render.mjs +121 -0
- package/vendor/dragonlab/design/lucid/recovery/review.mjs +68 -0
- package/vendor/dragonlab/design/lucid/recovery/verify.mjs +84 -0
- package/vendor/dragonlab/design/lucid/recovery/watch.mjs +56 -0
- package/vendor/dragonlab/design/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/seer/v1/seer-master.svg +53 -0
- package/vendor/dragonlab/design/seer/v2/character.mjs +88 -0
- package/vendor/dragonlab/design/seer/v3/character.mjs +33 -0
- package/vendor/dragonlab/design/seer/v4/character.mjs +66 -0
- package/vendor/dragonlab/design/seer/v4/review.mjs +33 -0
- package/vendor/dragonlab/design/seer/v4/watch.mjs +56 -0
- package/vendor/dragonlab/design/stage3/companion-review.mjs +28 -0
- package/vendor/dragonlab/design/stage3/sentinel-art.js +39 -0
- package/vendor/dragonlab/design/stage3/sentinel-braille.js +216 -0
- package/vendor/dragonlab/design/stage3/sentinel-character.js +257 -0
- package/vendor/dragonlab/design/stage3/sentinel-icon.js +103 -0
- package/vendor/dragonlab/design/stage3/sentinel-warden-eyes.js +95 -0
- package/vendor/dragonlab/design/stage3/svg-coverage.js +148 -0
- package/vendor/dragonlab/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/factory/svg-rig.mjs +63 -0
- package/vendor/dragonlab/integration/companions-v1/characters.mjs +108 -0
- package/vendor/dragonlab/integration/companions-v1/cipher.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/companion.mjs +63 -0
- package/vendor/dragonlab/integration/companions-v1/drago.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/hyperion.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/lucid.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/quoxperticus.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/seer.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/sentinel.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/view.mjs +28 -0
- package/vendor/dragonlab/integration/companions-v1/vulcan.mjs +2 -0
- package/vendor/dragonlab/integration/hyperion-v3/activity.mjs +76 -0
- package/vendor/dragonlab/lib/power-effects.js +294 -0
- package/vendor/dragonlab/lib/power-registry.js +74 -0
- package/vendor/dragonlab/vendor/quox-dragon/qart.js +348 -0
- package/vendor/dragonlab/vendor/quox-dragon/qeyes.js +213 -0
package/lib/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,23 +10,28 @@ 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';
|
|
16
18
|
import { normPrefs as normModelPrefs } from './modelPrefs.js';
|
|
17
19
|
import { createRowsStore } from './rowsStore.js';
|
|
18
20
|
import { createObjectsApi } from './objects/fetch.js';
|
|
21
|
+
import { createDragonReads } from './dragonReads.js';
|
|
19
22
|
import { createKillerApi } from './slash/killerApi.js';
|
|
20
23
|
import { createUndoOps } from './slash/undoApi.js';
|
|
21
24
|
import { resolveDashboardBase, resolveCollectorBase, detectLanIp } from './hostUrls.js';
|
|
22
25
|
import { grantBody } from './consent.js';
|
|
26
|
+
import { runCallMe } from '../core/callMe.js';
|
|
23
27
|
import { loadJourney } from './onboarding/journey.js';
|
|
24
28
|
import { joinServiceUrl } from '../core/config.js';
|
|
25
29
|
import { runVmCommand } from '../core/vmTui.js';
|
|
26
30
|
import { fetchActivity, heldLines } from '../core/hyperion.js';
|
|
27
31
|
import { createPeekFetchers, peekId } from '../core/peek.js';
|
|
28
32
|
import { askAside } from '../core/aside.js';
|
|
33
|
+
import { exportConversationBundle } from '../evidence/bundleExport.js';
|
|
34
|
+
import { addActionItem, listBoard, moveActionItem } from '../core/actionItems.js';
|
|
29
35
|
|
|
30
36
|
const missing = (e) => e?.status === 404 || e?.status === 405 || e?.status === 501;
|
|
31
37
|
/** Server steer refusal label -> client reason. Unknown labels are 'ambiguous', never "finished". */
|
|
@@ -71,13 +77,39 @@ const vaultFailure = (e) => ({ ok: false, status: e?.status, message: e?.message
|
|
|
71
77
|
const slotsUnlicensed = (e) => e?.status === 403 && /not licen[cs]ed/i.test(`${e?.code || ''} ${e?.body?.error || ''} ${e?.message || ''}`);
|
|
72
78
|
|
|
73
79
|
/** PRESENCE: only a person at a real terminal is "back". --print, pipes, proof runs and agents are not. */
|
|
80
|
+
const NOTES_OFF = 'NotesQ is not available for this org. Ask an org admin to enable the NotesQ plugin (dashboard, Plugins), or check `quox notes ls` in a terminal.';
|
|
81
|
+
export function noteParts(text) {
|
|
82
|
+
const t = String(text || '').trim();
|
|
83
|
+
const first = t.split('\n')[0].trim();
|
|
84
|
+
const title = first.length > 60 ? `${first.slice(0, 60).trimEnd()}…` : first;
|
|
85
|
+
return { title, markdown: `# ${title}\n\n${t}\n` };
|
|
86
|
+
}
|
|
87
|
+
async function createNoteFrom(client, text) {
|
|
88
|
+
const { title, markdown } = noteParts(text);
|
|
89
|
+
try {
|
|
90
|
+
const ep = resolveEndpoint('notesq.notes.create');
|
|
91
|
+
const d = await client.post(ep.path, { service: ep.service, retries: 0, body: { scope: 'org', markdown, properties: {} } });
|
|
92
|
+
const n = d?.note || {};
|
|
93
|
+
if (!n.id) return { ok: false, message: 'NotesQ answered without a note id, so nothing is confirmed saved. Check quox notes ls.' };
|
|
94
|
+
return { ok: true, id: n.id, title: n.title || title };
|
|
95
|
+
} catch (e) {
|
|
96
|
+
if (e?.status === 402) return { ok: false, message: 'NotesQ is not licensed for this org. ' + NOTES_OFF };
|
|
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
|
|
99
|
+
return { ok: false, message: `Could not save the note: ${e?.message || 'request failed'}${e?.status ? ` (HTTP ${e.status})` : ''}.` };
|
|
100
|
+
}
|
|
101
|
+
}
|
|
74
102
|
export function isInteractiveHuman({ stdin = process.stdin, stdout = process.stdout, env = process.env } = {}) {
|
|
75
103
|
return Boolean(stdin?.isTTY && stdout?.isTTY) && !env.CI;
|
|
76
104
|
}
|
|
77
105
|
|
|
78
|
-
|
|
106
|
+
/** A one-line title within the tasks service's 200 character cap. */
|
|
107
|
+
const clipTitle = (g) => { const t = String(g ?? '').replace(/\s+/g, ' ').trim(); return t.length > 200 ? `${t.slice(0, 199)}…` : t; };
|
|
108
|
+
|
|
109
|
+
export function createApi({ client, config, auth, windowOrg = null, clientForOrg = null, reattachBackoff = REATTACH_BACKOFF_MS, gripeQueuePath = undefined, listSessions = null, awayRetryMs = 3000 }) {
|
|
79
110
|
let holderSessionFn = null;
|
|
80
111
|
const orgId = () => { try { return auth.getOrgId(); } catch { return null; } };
|
|
112
|
+
const gripeStore = gripeQueue(gripeQueuePath);
|
|
81
113
|
const rowsStore = createRowsStore({ client, config, orgId });
|
|
82
114
|
let credsSeen = null; // credentials-file mtime last seen (credentialsChanged)
|
|
83
115
|
const userId = () => { try { return config.getAuth?.()?.user_id || null; } catch { return null; } };
|
|
@@ -134,12 +166,13 @@ export function createApi({ client, config, auth, windowOrg = null, reattachBack
|
|
|
134
166
|
err.code = 'reattach_failed';
|
|
135
167
|
throw err;
|
|
136
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
|
|
137
170
|
/** PRESENCE auto-clear: the human's own first real action. -> { ok, cleared, welcome } (welcome once: { away_text, via }). Never throws. */
|
|
138
171
|
async function noteActivity(action, { quiet = false, interactive = isInteractiveHuman() } = {}) {
|
|
139
172
|
if (!interactive) return { ok: false, cleared: false, welcome: null, skipped: 'not interactive' };
|
|
140
173
|
try {
|
|
141
174
|
const ep = resolveEndpoint('presence.activity');
|
|
142
|
-
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 } : {}) } });
|
|
143
176
|
return { ok: true, cleared: d?.cleared === true, welcome: d?.welcome || null };
|
|
144
177
|
} catch (e) { return { ok: false, cleared: false, welcome: null, status: e?.status || null }; }
|
|
145
178
|
}
|
|
@@ -147,9 +180,13 @@ export function createApi({ client, config, auth, windowOrg = null, reattachBack
|
|
|
147
180
|
...createKillerApi({ client }),
|
|
148
181
|
orgId,
|
|
149
182
|
undoOps: createUndoOps({ client, orgId }), // /undo
|
|
183
|
+
/** /note: same call as `quox notes new` (org scope). -> { ok, id, title } | { ok:false, message } */
|
|
184
|
+
createNote: (text) => createNoteFrom(client, text),
|
|
150
185
|
userId,
|
|
151
186
|
/** /peek <id>: the same lookup as `quox peek`, in this window's org. -> peekId result */
|
|
152
187
|
peek: (id) => peekId(id, createPeekFetchers({ client, orgId })),
|
|
188
|
+
/** /evidence, /ward: the signed bundle of one conversation, written to ~/quox-evidence (same export as `quox evidence bundle`). */
|
|
189
|
+
evidenceExport: (corr) => exportConversationBundle({ client, corr }),
|
|
153
190
|
/** /aside: a no-tools, no-conversation question (core/aside.js). turns = recent exchanges, read-only context. */
|
|
154
191
|
aside: (question, turns = []) => askAside(client, { question, turns }),
|
|
155
192
|
/** @mention hosts: the Bastion inventory (same read as `quox fleet hosts`). -> [{ id, group, health }] */
|
|
@@ -158,6 +195,25 @@ export function createApi({ client, config, auth, windowOrg = null, reattachBack
|
|
|
158
195
|
const d = await client.get(ep.path, { service: ep.service, retries: 0, timeout: 8000 });
|
|
159
196
|
return (listOf(d, 'hosts') || []).map(h => ({ id: h.id, group: h.group || '', health: h.status?.health || '' }));
|
|
160
197
|
},
|
|
198
|
+
/** QUOXON objective offer: `quox objective create <goal> --checkpoint` + `start` (same endpoints). -> { id, status } */
|
|
199
|
+
async startObjective(goal, loop = null) {
|
|
200
|
+
const c = resolveEndpoint('objectives.create');
|
|
201
|
+
// QLOOP: a loop offer creates a LOOP (server-derived scope + goal), never a plan under the loop's name.
|
|
202
|
+
const body = loop ? { title: String(goal).slice(0, 200), description: String(goal), execution_mode: 'loop', capability_scope: loop.capability_scope, goal_condition: loop.goal_condition,
|
|
203
|
+
...(loop.until ? { until: loop.until } : {}), ...(loop.every_seconds ? { every_seconds: loop.every_seconds } : {}), ...(loop.max_iterations ? { max_iterations: loop.max_iterations } : {}), ...(loop.max_duration_seconds ? { max_duration_ms: loop.max_duration_seconds * 1000 } : {}), ...(Array.isArray(loop.done_check) && loop.done_check.length ? { done_check: loop.done_check } : {}) }
|
|
204
|
+
: { title: clipTitle(goal), description: String(goal), checkpoint_after_each_task: true }; // the tasks service caps a title at 200; the whole ask rides in the description
|
|
205
|
+
const made = await client.post(c.path, { service: c.service, body });
|
|
206
|
+
if (!made?.id) throw new Error('the server answered without an objective id');
|
|
207
|
+
const st = resolveEndpoint('objectives.start', { id: made.id });
|
|
208
|
+
const started = await client.post(st.path, { service: st.service, body: {} });
|
|
209
|
+
return { id: made.id, status: started?.status || 'started' };
|
|
210
|
+
},
|
|
211
|
+
/** An objective with its tasks (the same read as `quox objective get`), for the chat's report-back. */
|
|
212
|
+
async objectiveGet(id) {
|
|
213
|
+
const ep = resolveEndpoint('objectives.get', { id: encodeURIComponent(id) });
|
|
214
|
+
const d = await client.get(ep.path, { service: ep.service, retries: 0, timeout: 8000 });
|
|
215
|
+
return d?.objective || d;
|
|
216
|
+
},
|
|
161
217
|
/** @mention tasks: the newest objectives (one read, cached by the composer for the session). -> [{ id, title, status }] */
|
|
162
218
|
async mentionTasks() {
|
|
163
219
|
const ep = resolveEndpoint('objectives.list');
|
|
@@ -203,6 +259,7 @@ export function createApi({ client, config, auth, windowOrg = null, reattachBack
|
|
|
203
259
|
},
|
|
204
260
|
/** Inline object views (objects/): skill, workflow, crystal, agent, memory, rules, proof, reconcile. */
|
|
205
261
|
objects: createObjectsApi({ client, orgId, profile: config?._effectiveProfile || null }),
|
|
262
|
+
dragonReads: createDragonReads({ client }), // the companion dragons' jobs: bounded read-only reads (dragonReads.js)
|
|
206
263
|
/** /model choice persisted on the active profile ({ provider, model? } only, never a key); null = auto. */
|
|
207
264
|
modelChoice() { try { return normChoice(config.getActiveProfile?.()?.chat_model); } catch { return null; } },
|
|
208
265
|
setModelChoice(choice) {
|
|
@@ -455,6 +512,27 @@ export function createApi({ client, config, auth, windowOrg = null, reattachBack
|
|
|
455
512
|
try { const r = await fetch(`${String(base).replace(/\/+$/, '')}/install`, { method: 'GET', signal: ctl.signal }); try { await r.body?.cancel?.(); } catch { /* drained */ } return { ok: r.ok, status: r.status }; }
|
|
456
513
|
catch { return null; } finally { clearTimeout(t); }
|
|
457
514
|
},
|
|
515
|
+
/** TUI-STATUS: the registered server this quox runs on -> { name, role, remote } | null (unregistered or any failure). */
|
|
516
|
+
async serverChip() {
|
|
517
|
+
const { resolveServerChip } = await import('./serverChip.js');
|
|
518
|
+
const { loadRegistry } = await import('../server/devServers.js');
|
|
519
|
+
let registry = { servers: {} };
|
|
520
|
+
try { registry = { servers: (await loadRegistry({ client })).servers }; } catch { /* unreadable registry: no purpose shown */ }
|
|
521
|
+
return resolveServerChip({
|
|
522
|
+
registry,
|
|
523
|
+
url: config.getActiveProfile?.()?.url || config.getBaseUrl?.() || null,
|
|
524
|
+
listHosts: async () => { const ep = resolveEndpoint('v1.fleet.hosts.list'); const d = await client.get(ep.path, { service: ep.service, retries: 0, timeout: 5000 }); return d?.hosts || d; }
|
|
525
|
+
});
|
|
526
|
+
},
|
|
527
|
+
/** DEVFLEET: the registered dev servers (same rows as `quox server list`) -> rows[]; throws on a Bastion read failure. */
|
|
528
|
+
async devServers() {
|
|
529
|
+
const [{ loadRegistry, buildRows, hereIdentity }] = [await import('../server/devServers.js')];
|
|
530
|
+
const ep = resolveEndpoint('v1.fleet.hosts.list');
|
|
531
|
+
const d = await client.get(ep.path, { service: ep.service, retries: 0, timeout: 5000 });
|
|
532
|
+
const hosts = d?.hosts || d;
|
|
533
|
+
if (!Array.isArray(hosts)) throw new Error('unexpected response');
|
|
534
|
+
{ const reg = await loadRegistry({ client }); return buildRows(hosts, { servers: reg.servers, defaults: reg.defaults }, hereIdentity()); }
|
|
535
|
+
},
|
|
458
536
|
serverUrl() { try { return new URL(config.getServiceUrl('collector')).origin; } catch { return null; } },
|
|
459
537
|
/** /config settings on the active profile. */
|
|
460
538
|
settings() { try { const q = config.getActiveProfile?.()?.quoxon; return q && typeof q === 'object' ? { ...q } : {}; } catch { return {}; } },
|
|
@@ -478,16 +556,23 @@ export function createApi({ client, config, auth, windowOrg = null, reattachBack
|
|
|
478
556
|
* file when the agent did not). The client asks the user first, so the agent is told to file without asking.
|
|
479
557
|
* -> { ok:true, response, url, superseded, notes } | { ok:false, message }.
|
|
480
558
|
*/
|
|
481
|
-
async gripe(text, { yes = true } = {}) {
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
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
|
+
});
|
|
490
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 });
|
|
573
|
+
},
|
|
574
|
+
/** GRIPE-QUEUE: queued plus recently filed gripes, oldest first. */
|
|
575
|
+
gripes() { return gripeStore.list(); },
|
|
491
576
|
/**
|
|
492
577
|
* /feedback: POST the previewed payload to the public quox.ai feedback API (not the org's server, not GitHub).
|
|
493
578
|
* Anonymous by design: no token, cookie, org or profile goes with it. -> { ok:true, id, url } | { ok:false, message }.
|
|
@@ -503,6 +588,31 @@ export function createApi({ client, config, auth, windowOrg = null, reattachBack
|
|
|
503
588
|
if (!rows) throw new Error('unexpected response');
|
|
504
589
|
return rows.filter(r => r.is_enabled !== false && r.is_enabled !== 0).map(r => r.plugin_id).filter(Boolean);
|
|
505
590
|
},
|
|
591
|
+
/** /todo: add an Action Tracker item as the logged-in user (the same core as `quox actions add`). Throws on failure. -> the item. */
|
|
592
|
+
async todoAdd(text) { return addActionItem(client, { title: text }); },
|
|
593
|
+
/** /todos: the whole board { backlog, doing, done }, every page. Throws on failure so it never reads as "none". */
|
|
594
|
+
async todoList() { return listBoard(client); },
|
|
595
|
+
/** /todo move: move one item (real id) to todo, doing or done. -> the item. */
|
|
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); },
|
|
599
|
+
/** -> [{ plugin_id, version, is_enabled, ... }] every plugin installed for the org (GET plugins.list, as `quox plugin list`). Throws on failure. */
|
|
600
|
+
async pluginList() {
|
|
601
|
+
const id = orgId();
|
|
602
|
+
if (!id) throw new Error('no org');
|
|
603
|
+
const ep = resolveEndpoint('plugins.list', { orgId: id });
|
|
604
|
+
const rows = listOf(await client.get(ep.path, { service: ep.service, retries: 0 }), 'plugins', 'data', 'items');
|
|
605
|
+
if (!rows) throw new Error('unexpected response');
|
|
606
|
+
return rows;
|
|
607
|
+
},
|
|
608
|
+
/** One installed plugin's record (GET plugins.get, as `quox plugin get`), or null when unknown (404). */
|
|
609
|
+
async pluginGet(pluginId) {
|
|
610
|
+
const id = orgId();
|
|
611
|
+
if (!id) throw new Error('no org');
|
|
612
|
+
const ep = resolveEndpoint('plugins.get', { orgId: id, pluginId });
|
|
613
|
+
try { const d = await client.get(ep.path, { service: ep.service, retries: 0 }); return d?.plugin || d; }
|
|
614
|
+
catch (e) { if (e?.status === 404) return null; throw e; }
|
|
615
|
+
},
|
|
506
616
|
/** Fleet hosts already connected (tips skip the QuoxAgent tip when > 0). GET agents list; throws on failure so a failure never reads as "no hosts". */
|
|
507
617
|
async hostCount() {
|
|
508
618
|
const ep = resolveEndpoint('v1.agents.list');
|
|
@@ -522,24 +632,108 @@ export function createApi({ client, config, auth, windowOrg = null, reattachBack
|
|
|
522
632
|
* ("quox1 (Adam)"); `fast` answers once the line is in the slot, without waiting on the chat-app bridges.
|
|
523
633
|
* -> { ok:true, message } | { ok:false, message, status }.
|
|
524
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
|
+
},
|
|
525
664
|
/** SLOTS-REPLY2: the window registers how to name its checkout session; slotSay then sends it as holder_session. */
|
|
526
665
|
bindHolderSession(fn) { holderSessionFn = typeof fn === 'function' ? fn : null; },
|
|
527
|
-
async slotSay(name, text, { from = null, fast = false, roomSession = null, fromSlot = null, holderSession = holderSessionFn ? holderSessionFn() : null } = {}) {
|
|
528
|
-
const
|
|
529
|
-
|
|
666
|
+
async slotSay(name, text, { from = null, fast = false, roomSession = null, fromSlot = null, holderSession = holderSessionFn ? holderSessionFn() : null, anyOrg = true } = {}) {
|
|
667
|
+
const send = async (cl, oid) => {
|
|
668
|
+
const ep = resolveEndpoint('v1.slots.say', { name });
|
|
530
669
|
// SLOTS-REPLY: fromSlot is this window's held slot, its reply address (the server verifies the hold).
|
|
531
|
-
const d = await
|
|
670
|
+
const d = await cl.post(ep.path, { service: ep.service, retries: 0, body: { text, org_id: oid, ...(roomSession ? { room_session: roomSession } : from ? { from } : {}), ...(!roomSession && fromSlot ? { from_slot: fromSlot } : {}), ...(!roomSession && holderSession ? { holder_session: holderSession } : {}), ...(fast ? { bridge_report: false } : {}) } });
|
|
532
671
|
return { ok: true, message: d?.message || null, roomPending: d?.room_pending === true, presence: d?.presence || null, humanPresence: d?.human_presence || null };
|
|
533
|
-
}
|
|
672
|
+
};
|
|
673
|
+
const fail = (e) => {
|
|
534
674
|
if (slotsUnlicensed(e)) return { ok: false, unlicensed: true, status: 403, message: SLOTS_UNLICENSED };
|
|
535
675
|
return { ok: false, message: e?.status === 404 ? `no slot named ${name}` : (e?.message || 'failed'), status: e?.status || null };
|
|
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 }; }
|
|
536
694
|
}
|
|
695
|
+
let first;
|
|
696
|
+
try { return await send(client, orgId()); } catch (e) { first = fail(e); }
|
|
697
|
+
// HITL-INBOX-BAR: a window in another org cannot see a slot that lives in the profile's home org. Same user, same
|
|
698
|
+
// sessions (not a cross-tenant leak): look in the user's other orgs only, each on a token for THAT org.
|
|
699
|
+
if (!anyOrg || first.status !== 404 || !clientForOrg) return first;
|
|
700
|
+
const tried = new Set([orgId()]);
|
|
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
|
+
}
|
|
722
|
+
for (const oid of order) {
|
|
723
|
+
if (tried.has(oid)) continue; tried.add(oid);
|
|
724
|
+
try {
|
|
725
|
+
const cl = await clientFor(oid);
|
|
726
|
+
const r = await send(cl, oid);
|
|
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
|
|
729
|
+
}
|
|
730
|
+
return first;
|
|
537
731
|
},
|
|
538
732
|
/** QUOXON-BROADCAST: fan a message out to the org's held slots. -> { ok:true, data } | { ok:false, message, status }. */
|
|
539
|
-
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 } = {}) {
|
|
540
734
|
const ep = resolveEndpoint('v1.slots.broadcast');
|
|
541
735
|
try {
|
|
542
|
-
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 } : {}) } });
|
|
543
737
|
return { ok: true, data };
|
|
544
738
|
} catch (e) {
|
|
545
739
|
if (slotsUnlicensed(e)) return { ok: false, unlicensed: true, status: 403, message: SLOTS_UNLICENSED };
|
|
@@ -563,6 +757,18 @@ export function createApi({ client, config, auth, windowOrg = null, reattachBack
|
|
|
563
757
|
const ep = resolveEndpoint('v1.slots.broadcast.ack', { id });
|
|
564
758
|
try { return { ok: true, data: await client.post(ep.path, { service: ep.service, retries: 0, body: { slot, org_id: orgId() } }) }; } catch (e) { return { ok: false, message: e?.message || 'failed', status: e?.status || null }; }
|
|
565
759
|
},
|
|
760
|
+
/** QLOOP-LIVE: the org's loops, newest first (GET /api/v1/loops) -> { loops:[{ objective_id, title, state, created_at }] }. */
|
|
761
|
+
async loopList() {
|
|
762
|
+
return client.get('/api/v1/loops', { service: 'collector', retries: 0, timeout: 8000, query: { limit: 20 } });
|
|
763
|
+
},
|
|
764
|
+
/** QLOOP-LIVE: one loop's snapshot (GET /api/v1/loops/:id). Throws on failure (the card decides what a failure means). */
|
|
765
|
+
async loopSnapshot(id) {
|
|
766
|
+
return client.get(`/api/v1/loops/${encodeURIComponent(id)}`, { service: 'collector', retries: 0, timeout: 8000 });
|
|
767
|
+
},
|
|
768
|
+
/** QLOOP-LIVE: one loop's events (SSE; ?since resumes). Yields { event_seq, type, ... }; ends after loop.finished or a drop. */
|
|
769
|
+
async *loopEvents(id, { since = null } = {}, signal) {
|
|
770
|
+
yield* client.stream(`/api/v1/loops/${encodeURIComponent(id)}/events`, { service: 'collector', query: { since }, signal, untilClose: true });
|
|
771
|
+
},
|
|
566
772
|
/** QUOXON-MESH: the slot's live messages (SSE). Yields { type: 'connected' | 'slot.message' | 'reconnect' }. */
|
|
567
773
|
async *slotStream(name, { since = null, excludeFrom = null, session = null } = {}, signal) {
|
|
568
774
|
const ep = resolveEndpoint('v1.slots.stream', { name });
|
|
@@ -678,6 +884,20 @@ export function createApi({ client, config, auth, windowOrg = null, reattachBack
|
|
|
678
884
|
if (!windowOrg) { const e = new Error('Org switching is not available in this mode.'); e.code = 'no_window_org'; throw e; }
|
|
679
885
|
return windowOrg.switchTo(targetId);
|
|
680
886
|
},
|
|
887
|
+
/**
|
|
888
|
+
* "also make default" from the /org picker: the same profile-wide change as `quox org switch <org>` in a shell
|
|
889
|
+
* (moves the shared session and rewrites the profile's stored org + token). This window's own org is untouched.
|
|
890
|
+
*/
|
|
891
|
+
async makeOrgDefault(targetId) {
|
|
892
|
+
const ep = resolveEndpoint('auth.switch-org');
|
|
893
|
+
const data = await client.post(ep.path, { service: ep.service, body: { org_id: targetId } });
|
|
894
|
+
if (!data?.org?.id) { const e = new Error('The server did not confirm the switch.'); e.code = 'switch_failed'; throw e; }
|
|
895
|
+
const a = config.getAuth() || {};
|
|
896
|
+
a.org_id = data.org.id; a.org_name = data.org.name; a.org_slug = data.org.slug; a.org_role = data.role;
|
|
897
|
+
if (data.access_token) a.access_token = data.access_token;
|
|
898
|
+
config.setAuth(a);
|
|
899
|
+
return { id: data.org.id, name: data.org.name };
|
|
900
|
+
},
|
|
681
901
|
/** The window's own org id when it differs from the profile's (carried across /restart), else null. */
|
|
682
902
|
windowOrgId: () => windowOrg?.current()?.id || null,
|
|
683
903
|
/** -> [{ id, name, description, edition, version, decayState }] ; the org's QSKL procedural skills (read-only list; Crystals are a separate store). */
|
|
@@ -716,8 +936,26 @@ export function createApi({ client, config, auth, windowOrg = null, reattachBack
|
|
|
716
936
|
return l.map(e => fromExecution(e, now, { userId: userId() })).filter(Boolean);
|
|
717
937
|
})
|
|
718
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
|
+
]);
|
|
719
957
|
const unlisted = [
|
|
720
|
-
{
|
|
958
|
+
...extra.map(({ rows, ...rest }) => rest),
|
|
721
959
|
{ id: 'devsessions', label: 'dev sessions', state: 'unavailable', count: 0, note: 'listed per host only (quox dev sessions <host>)' }
|
|
722
960
|
];
|
|
723
961
|
return { rows: sortRows(parts.flatMap(p => p.rows)), sources: [...parts.map(({ rows, ...rest }) => rest), ...unlisted] };
|
|
@@ -753,18 +991,31 @@ export function createApi({ client, config, auth, windowOrg = null, reattachBack
|
|
|
753
991
|
return { situation, approvals };
|
|
754
992
|
},
|
|
755
993
|
/** PRESENCE: PUT /api/v1/presence (/afk, /sleep, /back, a typed turn). -> { ok:true, presence, notice } | { ok:false, message, status }. */
|
|
756
|
-
async setPresence(state, { note, slots } = {}) {
|
|
994
|
+
async setPresence(state, { note, slots, dnd = false, human = false } = {}) {
|
|
757
995
|
const ep = resolveEndpoint('presence.set');
|
|
758
996
|
try {
|
|
759
|
-
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 } : {}) } });
|
|
760
998
|
return { ok: true, presence: d?.presence || null, notice: d?.notice || null };
|
|
761
999
|
} catch (e) { return { ok: false, message: e?.message || 'failed', status: e?.status || null }; }
|
|
762
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
|
+
},
|
|
763
1009
|
noteActivity,
|
|
764
1010
|
/** E5 /away: GET /api/v1/away?since=<iso> -> { since, until, items, activity, blind }. Throws on non-2xx. */
|
|
765
1011
|
async away(since) {
|
|
766
1012
|
const ep = resolveEndpoint('away.digest');
|
|
767
|
-
|
|
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
|
|
768
1019
|
},
|
|
769
1020
|
/** Facts for /observers, each part on its own: { lucid, learning:[{ id, label, state, text }] }. */
|
|
770
1021
|
async observers() {
|
|
@@ -823,6 +1074,31 @@ export function createApi({ client, config, auth, windowOrg = null, reattachBack
|
|
|
823
1074
|
}
|
|
824
1075
|
},
|
|
825
1076
|
/** /connect status: the org vault list (the only source a user can read). -> { ok:true, rows } | { ok:false, error }. */
|
|
1077
|
+
/** LAZY-AI-CLI: which vendor CLIs (claude-code, codex) this server has. Never throws. */
|
|
1078
|
+
async providerCliStatus() {
|
|
1079
|
+
const ep = resolveEndpoint('v1.providers.cli.status');
|
|
1080
|
+
try {
|
|
1081
|
+
const d = await client.get(ep.path, { service: ep.service, retries: 0 });
|
|
1082
|
+
return Array.isArray(d?.tools) ? { ok: true, tools: d.tools } : { ok: false, error: 'unexpected response' };
|
|
1083
|
+
} catch (e) { return { ok: false, status: e?.status, missing: missing(e), error: e?.status ? `HTTP ${e.status} ${e.message || ''}`.trim() : (e?.message || 'failed') }; }
|
|
1084
|
+
},
|
|
1085
|
+
/** The install plan: source, exact package + version, download size. Instance admin only (403 otherwise). Never throws. */
|
|
1086
|
+
async providerCliPlan(tool) {
|
|
1087
|
+
const ep = resolveEndpoint('v1.providers.cli.plan', { tool });
|
|
1088
|
+
try { const d = await client.get(ep.path, { service: ep.service, retries: 0 }); return d?.ok ? d : { ok: false, error: d?.message || 'unexpected response' }; }
|
|
1089
|
+
catch (e) { return { ok: false, status: e?.status, missing: missing(e), error: e?.body?.message || e?.message || 'failed' }; }
|
|
1090
|
+
},
|
|
1091
|
+
/** Start an install job (the server audits it). Never throws. */
|
|
1092
|
+
async providerCliInstall(tool, { approvalId = null } = {}) {
|
|
1093
|
+
const ep = resolveEndpoint('v1.providers.cli.install', { tool });
|
|
1094
|
+
try { return await client.post(ep.path, { service: ep.service, body: approvalId ? { approval_id: approvalId } : {}, retries: 0 }); }
|
|
1095
|
+
catch (e) { return { ok: false, status: e?.status, error: e?.body?.message || e?.message || 'failed' }; }
|
|
1096
|
+
},
|
|
1097
|
+
async providerCliJob(id) {
|
|
1098
|
+
const ep = resolveEndpoint('v1.providers.cli.job', { id });
|
|
1099
|
+
try { return await client.get(ep.path, { service: ep.service, retries: 0 }); }
|
|
1100
|
+
catch (e) { return { state: 'unknown', error: e?.message || 'failed', status: e?.status }; }
|
|
1101
|
+
},
|
|
826
1102
|
async connections() {
|
|
827
1103
|
try {
|
|
828
1104
|
const ep = resolveEndpoint('vault.list', { orgId: orgId() });
|
|
@@ -905,12 +1181,68 @@ export function createApi({ client, config, auth, windowOrg = null, reattachBack
|
|
|
905
1181
|
/** /hyperion: held VMs and who holds them, one line each (read only, GET /api/v1/proxmox/activity). */
|
|
906
1182
|
async hyperion() { const data = await fetchActivity(client); return { lines: heldLines(data), tone: data.nodes_error || data.pool_error ? 'warn' : undefined }; },
|
|
907
1183
|
approvals: listApprovals,
|
|
1184
|
+
/** HITL-INBOX-BAR: the owner's latest inbox items (any kind, active), newest first. Throws on failure, never reads as empty. */
|
|
1185
|
+
async inboxItems({ limit = 30, page = 1 } = {}) {
|
|
1186
|
+
const ep = resolveEndpoint('inbox.list', { orgId: orgId() });
|
|
1187
|
+
const data = await client.get(ep.path, { service: ep.service, query: page > 1 ? { limit, page } : { limit } });
|
|
1188
|
+
const items = data?.items || data?.data || data || [];
|
|
1189
|
+
return Array.isArray(items) ? items : [];
|
|
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
|
+
},
|
|
1221
|
+
/** One inbox item in full (the server marks it read for this user as a side effect). */
|
|
1222
|
+
async inboxItem(itemId) {
|
|
1223
|
+
const ep = resolveEndpoint('inbox.get', { orgId: orgId(), itemId: encodeURIComponent(itemId) });
|
|
1224
|
+
const data = await client.get(ep.path, { service: ep.service });
|
|
1225
|
+
return data?.item || data;
|
|
1226
|
+
},
|
|
1227
|
+
/** Mark one inbox item read (PATCH .../inbox/:id/read). */
|
|
1228
|
+
async inboxMarkRead(itemId, isRead = true) {
|
|
1229
|
+
const ep = resolveEndpoint('inbox.get', { orgId: orgId(), itemId: encodeURIComponent(itemId) });
|
|
1230
|
+
return client.patch(`${ep.path}/read`, { service: ep.service, body: { is_read: Boolean(isRead) } });
|
|
1231
|
+
},
|
|
908
1232
|
/** Org-wide approvals awaiting a human (the dashboard header's server-side count, inbox stats awaiting_human). */
|
|
909
1233
|
async approvalStats() {
|
|
910
1234
|
const ep = resolveEndpoint('inbox.stats.list', { orgId: orgId() });
|
|
911
1235
|
const d = await client.get(ep.path, { service: ep.service });
|
|
912
1236
|
return { pending: Number(d?.pending_approvals ?? 0), awaiting: Number(d?.awaiting_human ?? 0) };
|
|
913
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
|
+
},
|
|
914
1246
|
/** GET /api/v1/approvals/:id/result -> { status, result, executed_at } ; null when unknown (404) */
|
|
915
1247
|
async approvalResult(approvalId) {
|
|
916
1248
|
const ep = resolveEndpoint('v1.approvals.result', { id: approvalId });
|
|
@@ -918,14 +1250,14 @@ export function createApi({ client, config, auth, windowOrg = null, reattachBack
|
|
|
918
1250
|
catch (e) { if (e?.status === 404) return null; throw e; }
|
|
919
1251
|
},
|
|
920
1252
|
/** quox ask: answer a decision with one of its option keys (and an optional note). */
|
|
921
|
-
async answerDecision(itemId, option, { note = null, stepUp = null, bulk = false } = {}) {
|
|
922
|
-
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)
|
|
923
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) } : {}) } });
|
|
924
1256
|
if (stepUp && Object.keys(stepUp).length) noteActivity('answer', { quiet: true }); // PRESENCE: answering is the owner being back
|
|
925
1257
|
return r;
|
|
926
1258
|
},
|
|
927
|
-
async respond(itemId, action, { via = null, stepUp = null, reason = null, bulk = false } = {}) {
|
|
928
|
-
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 });
|
|
929
1261
|
const tag = via ? `via quoxon ${via}` : 'via quoxon';
|
|
930
1262
|
const why = reason ? String(reason).trim().slice(0, 4000) : '';
|
|
931
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 } : {}) } });
|
|
@@ -943,6 +1275,11 @@ export function createApi({ client, config, auth, windowOrg = null, reattachBack
|
|
|
943
1275
|
const ep = resolveEndpoint('stepup.status');
|
|
944
1276
|
return client.get(ep.path, { service: ep.service });
|
|
945
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
|
+
},
|
|
946
1283
|
/** QUOXON-STEPUP part B: override ONE blocked action. Needs a live override-scoped step-up token. */
|
|
947
1284
|
async override({ blockId, blockType, reason, agentId = null, action = null, stepUp = {} }) {
|
|
948
1285
|
const ep = resolveEndpoint('safety.override');
|