quoxcli 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +2 -0
- package/bin/quox.js +99 -126
- package/lib/api/endpoints/admin.js +26 -0
- package/lib/api/endpoints/agent-schedules.js +26 -0
- package/lib/api/endpoints/agents.js +7 -0
- package/lib/api/endpoints/assistants.js +12 -0
- package/lib/api/endpoints/away.js +15 -0
- package/lib/api/endpoints/chat.js +7 -0
- package/lib/api/endpoints/contactsq.js +2 -0
- package/lib/api/endpoints/conversations.js +14 -0
- package/lib/api/endpoints/executions.js +13 -0
- package/lib/api/endpoints/fetchAllowlist.js +27 -0
- package/lib/api/endpoints/files.js +14 -0
- package/lib/api/endpoints/hat.js +30 -0
- package/lib/api/endpoints/inbox.js +20 -0
- package/lib/api/endpoints/index.js +14 -0
- package/lib/api/endpoints/memory.js +19 -0
- package/lib/api/endpoints/mind.js +13 -0
- package/lib/api/endpoints/mon.js +6 -0
- package/lib/api/endpoints/objectives.js +6 -0
- package/lib/api/endpoints/plugins.js +18 -0
- package/lib/api/endpoints/presence.js +33 -0
- package/lib/api/endpoints/quoxplan.js +7 -0
- package/lib/api/endpoints/quoxseo.js +6 -0
- package/lib/api/endpoints/router.js +17 -0
- package/lib/api/endpoints/safety.js +7 -0
- package/lib/api/endpoints/skills.js +20 -0
- package/lib/api/endpoints/stepup.js +20 -0
- package/lib/api/endpoints/teams.js +2 -2
- package/lib/api/endpoints/triggers.js +26 -0
- package/lib/api/endpoints/v1.js +86 -0
- package/lib/api/endpoints/ward.js +7 -0
- package/lib/api/endpoints/workflows.js +7 -0
- package/lib/api/endpoints/x.js +8 -0
- package/lib/commands/actions.js +61 -0
- package/lib/commands/admin.js +16 -0
- package/lib/commands/agent.js +163 -27
- package/lib/commands/agentAttach.js +94 -6
- package/lib/commands/approvals.js +73 -1
- package/lib/commands/aside.js +24 -0
- package/lib/commands/ask.js +144 -0
- package/lib/commands/assistant.js +59 -4
- package/lib/commands/auth.js +8 -2
- package/lib/commands/backup.js +8 -4
- package/lib/commands/brain.js +105 -14
- package/lib/commands/build.js +19 -5
- package/lib/commands/chat.js +57 -2
- package/lib/commands/claude.js +34 -8
- package/lib/commands/contacts.js +49 -0
- package/lib/commands/conversation.js +35 -4
- package/lib/commands/dashboard.js +43 -0
- package/lib/commands/department.js +28 -7
- package/lib/commands/devResident.js +15 -3
- package/lib/commands/devSupervisor.js +13 -1
- package/lib/commands/discord.js +75 -39
- package/lib/commands/done.js +98 -28
- package/lib/commands/engagement.js +17 -6
- package/lib/commands/fetchAllowlist.js +71 -0
- package/lib/commands/file.js +38 -1
- package/lib/commands/fleet.js +311 -4
- package/lib/commands/governance.js +12 -3
- package/lib/commands/gripe.js +44 -13
- package/lib/commands/hat.js +178 -9
- package/lib/commands/helpers.js +18 -1
- package/lib/commands/hyperion.js +58 -0
- package/lib/commands/inbox.js +74 -23
- package/lib/commands/licence.js +14 -155
- package/lib/commands/lucid.js +10 -4
- package/lib/commands/memory.js +85 -8
- package/lib/commands/mind.js +71 -0
- package/lib/commands/mon.js +69 -2
- package/lib/commands/monitoring.js +1 -1
- package/lib/commands/motion.js +2 -2
- package/lib/commands/nucleus.js +48 -17
- package/lib/commands/objective.js +11 -0
- package/lib/commands/offload.js +89 -0
- package/lib/commands/onboard.js +2 -1
- package/lib/commands/org.js +11 -1
- package/lib/commands/peek.js +28 -0
- package/lib/commands/plan.js +38 -0
- package/lib/commands/plugin.js +110 -0
- package/lib/commands/presence.js +89 -0
- package/lib/commands/provider.js +159 -0
- package/lib/commands/qlarity.js +19 -3
- package/lib/commands/quoxchat.js +13 -4
- package/lib/commands/quoxseo.js +38 -0
- package/lib/commands/reach.js +2 -1
- package/lib/commands/router.js +77 -0
- package/lib/commands/rows.js +358 -0
- package/lib/commands/run.js +64 -9
- package/lib/commands/server.js +292 -0
- package/lib/commands/setup.js +33 -7
- package/lib/commands/skills.js +48 -5
- package/lib/commands/slot.js +260 -7
- package/lib/commands/status.js +1 -1
- package/lib/commands/team.js +3 -1
- package/lib/commands/tips.js +23 -0
- package/lib/commands/tool.js +133 -45
- package/lib/commands/trace.js +12 -7
- package/lib/commands/trigger.js +95 -0
- package/lib/commands/update.js +46 -0
- package/lib/commands/vault.js +31 -8
- package/lib/commands/vm.js +237 -0
- package/lib/commands/voice.js +34 -3
- package/lib/commands/volt.js +45 -3
- package/lib/commands/walkthrough.js +32 -0
- package/lib/commands/watch.js +73 -21
- package/lib/commands/welcome.js +14 -0
- package/lib/commands/workflow.js +189 -32
- package/lib/commands/x.js +13 -4
- package/lib/core/actingAs.js +2 -2
- package/lib/core/activeSummary.js +186 -0
- package/lib/core/aside.js +54 -0
- package/lib/core/askDecision.js +60 -0
- package/lib/core/auth.js +164 -13
- package/lib/core/callerSource.js +1 -1
- package/lib/core/cliBuild.js +62 -0
- package/lib/core/client.js +144 -35
- package/lib/core/commandRegistrars.js +121 -0
- package/lib/core/config.js +142 -24
- package/lib/core/customHats.js +168 -0
- package/lib/core/doneEngineFetch.js +8 -0
- package/lib/core/errors.js +329 -20
- package/lib/core/global-flags.js +105 -5
- package/lib/core/hyperion.js +102 -0
- package/lib/core/inboxResolution.js +65 -0
- package/lib/core/lazyCommands.js +4 -1
- package/lib/core/offload.js +504 -0
- package/lib/core/org-override.js +1 -1
- package/lib/core/orgResolve.js +70 -0
- package/lib/core/output.js +4 -4
- package/lib/core/peek.js +126 -0
- package/lib/core/profileUsers.js +51 -0
- package/lib/core/promptCancel.js +18 -0
- package/lib/core/repoPath.js +11 -0
- package/lib/core/residentSeed.js +3 -1
- package/lib/core/rootOptions.js +39 -0
- package/lib/core/router.js +168 -0
- package/lib/core/selfUpdate.js +188 -0
- package/lib/core/sessionsOverview.js +64 -0
- package/lib/core/slotKeeper.js +248 -3
- package/lib/core/slotWake.js +120 -0
- package/lib/core/supervisorEngine.js +71 -2
- package/lib/core/version.js +1 -1
- package/lib/core/vm.js +361 -0
- package/lib/core/vmTui.js +86 -0
- package/lib/core/walkthrough.js +35 -0
- package/lib/hyperion/deck.js +29 -0
- package/lib/lucid/LucidCard.jsx +68 -0
- package/lib/lucid/LucidIntro.jsx +45 -58
- package/lib/lucid/cards.js +86 -0
- package/lib/lucid/markdown.js +81 -0
- package/lib/lucid/motion.js +56 -0
- package/lib/quoxon/activity.js +14 -2
- package/lib/quoxon/aocl.js +117 -0
- package/lib/quoxon/api.js +504 -25
- package/lib/quoxon/app.js +3000 -230
- package/lib/quoxon/approvalFacts.js +94 -0
- package/lib/quoxon/approvalsView.js +325 -0
- package/lib/quoxon/asideView.js +40 -0
- package/lib/quoxon/ask.js +121 -0
- package/lib/quoxon/away.js +52 -0
- package/lib/quoxon/bang.js +224 -0
- package/lib/quoxon/bulkRisk.js +17 -0
- package/lib/quoxon/capabilities.js +13 -6
- package/lib/quoxon/cells.js +53 -0
- package/lib/quoxon/commandShort.js +112 -0
- package/lib/quoxon/commands.js +363 -38
- package/lib/quoxon/connect.js +210 -23
- package/lib/quoxon/connectCard.js +19 -2
- package/lib/quoxon/consent.js +86 -0
- package/lib/quoxon/copy.js +4 -3
- package/lib/quoxon/countdownLine.js +11 -0
- package/lib/quoxon/demo.autoplay.js +29 -0
- package/lib/quoxon/demo.fixtures.js +1 -1
- package/lib/quoxon/demo.js +44 -3
- package/lib/quoxon/devObserver.js +20 -0
- package/lib/quoxon/dividerAocl.js +191 -0
- package/lib/quoxon/dividerRule.js +57 -0
- package/lib/quoxon/dragonModes.js +150 -0
- package/lib/quoxon/dragonView.js +41 -0
- package/lib/quoxon/earlyInput.js +40 -0
- package/lib/quoxon/echo.js +15 -0
- package/lib/quoxon/egg.js +2 -2
- package/lib/quoxon/events.js +244 -25
- package/lib/quoxon/feedback.js +50 -0
- package/lib/quoxon/gallery.js +21 -7
- package/lib/quoxon/handoffOffer.js +78 -0
- package/lib/quoxon/hatSwitch.js +37 -0
- package/lib/quoxon/headless.js +161 -0
- package/lib/quoxon/hostUrls.js +41 -0
- package/lib/quoxon/indicator.js +6 -2
- package/lib/quoxon/intent.js +12 -1
- package/lib/quoxon/keys.js +49 -6
- package/lib/quoxon/launcher.js +103 -0
- package/lib/quoxon/main.js +92 -160
- package/lib/quoxon/markdown.js +45 -21
- package/lib/quoxon/markdownSettle.js +19 -0
- package/lib/quoxon/matrixDomains.json +46 -0
- package/lib/quoxon/mentions.js +90 -0
- package/lib/quoxon/mind.js +103 -0
- package/lib/quoxon/mode.js +46 -0
- package/lib/quoxon/model.js +132 -26
- package/lib/quoxon/modelPrefs.js +345 -0
- package/lib/quoxon/motion/clock.js +3 -2
- package/lib/quoxon/motion/level.js +91 -0
- package/lib/quoxon/mouse.js +211 -0
- package/lib/quoxon/noai.js +57 -0
- package/lib/quoxon/objects/away.js +72 -0
- package/lib/quoxon/objects/fetch.js +17 -5
- package/lib/quoxon/objects/fleet.js +149 -0
- package/lib/quoxon/objects/health.js +186 -0
- package/lib/quoxon/objects/index.js +24 -3
- package/lib/quoxon/objects/mirror.js +137 -0
- package/lib/quoxon/objects/peek.js +17 -0
- package/lib/quoxon/objects/prove.js +130 -0
- package/lib/quoxon/objects/replay.js +125 -0
- package/lib/quoxon/objects/sitrep.js +58 -0
- package/lib/quoxon/objects/view.js +11 -0
- package/lib/quoxon/onboarding/copy.js +32 -4
- package/lib/quoxon/onboarding/credential.js +17 -4
- package/lib/quoxon/onboarding/dockerOffer.js +45 -0
- package/lib/quoxon/onboarding/failureCause.js +33 -0
- package/lib/quoxon/onboarding/finish.js +15 -0
- package/lib/quoxon/onboarding/index.js +34 -7
- package/lib/quoxon/onboarding/installRunner.js +64 -5
- package/lib/quoxon/onboarding/preflight.js +17 -3
- package/lib/quoxon/onboarding/probe.js +65 -0
- package/lib/quoxon/onboarding/view.js +76 -19
- package/lib/quoxon/orgLabel.js +10 -0
- package/lib/quoxon/orgSwitch.js +33 -0
- package/lib/quoxon/ownBroadcast.js +22 -0
- package/lib/quoxon/palette.js +43 -0
- package/lib/quoxon/print.js +9 -5
- package/lib/quoxon/profileSwitch.js +53 -0
- package/lib/quoxon/promptHistory.js +41 -0
- package/lib/quoxon/providers.js +48 -0
- package/lib/quoxon/qart.js +20 -51
- package/lib/quoxon/qdragon.js +410 -0
- package/lib/quoxon/qdragonFly.js +61 -0
- package/lib/quoxon/qdragonScenes.js +282 -0
- package/lib/quoxon/qeyes.js +213 -0
- package/lib/quoxon/qmotion.js +58 -37
- package/lib/quoxon/render.js +638 -119
- package/lib/quoxon/resend.js +72 -0
- package/lib/quoxon/roster.js +95 -9
- package/lib/quoxon/route.js +6 -5
- package/lib/quoxon/rows.js +711 -0
- package/lib/quoxon/rowsStore.js +80 -0
- package/lib/quoxon/screenContext.js +150 -0
- package/lib/quoxon/screenFrames.js +254 -0
- package/lib/quoxon/session.js +172 -2
- package/lib/quoxon/sessionDead.js +17 -0
- package/lib/quoxon/signin.js +125 -0
- package/lib/quoxon/slash/agents.js +112 -0
- package/lib/quoxon/slash/argSources.js +65 -0
- package/lib/quoxon/slash/args.js +65 -0
- package/lib/quoxon/slash/automate.js +89 -0
- package/lib/quoxon/slash/broadcast.js +140 -0
- package/lib/quoxon/slash/broadcastRun.js +108 -0
- package/lib/quoxon/slash/card.js +30 -0
- package/lib/quoxon/slash/challenge.js +114 -0
- package/lib/quoxon/slash/cliCommand.js +53 -0
- package/lib/quoxon/slash/cliLeaves.json +948 -0
- package/lib/quoxon/slash/compact.js +1 -1
- package/lib/quoxon/slash/confirm.js +30 -0
- package/lib/quoxon/slash/crystallize.js +69 -0
- package/lib/quoxon/slash/dashboard.js +136 -0
- package/lib/quoxon/slash/draft.js +54 -0
- package/lib/quoxon/slash/hatWalk.js +206 -0
- package/lib/quoxon/slash/inverses.js +77 -0
- package/lib/quoxon/slash/killer.js +24 -0
- package/lib/quoxon/slash/killerApi.js +16 -0
- package/lib/quoxon/slash/ledger.js +13 -8
- package/lib/quoxon/slash/listView.js +3 -1
- package/lib/quoxon/slash/modelCmd.js +261 -0
- package/lib/quoxon/slash/palette.js +21 -7
- package/lib/quoxon/slash/paletteController.js +117 -7
- package/lib/quoxon/slash/panels.js +21 -6
- package/lib/quoxon/slash/picker.js +20 -2
- package/lib/quoxon/slash/restart.js +71 -13
- package/lib/quoxon/slash/rowCmd.js +157 -0
- package/lib/quoxon/slash/rowWalk.js +175 -0
- package/lib/quoxon/slash/settings.js +16 -6
- package/lib/quoxon/slash/spawn.js +177 -0
- package/lib/quoxon/slash/stepUp.js +274 -0
- package/lib/quoxon/slash/stepUpCard.js +60 -0
- package/lib/quoxon/slash/stepUpCmds.js +78 -0
- package/lib/quoxon/slash/undoApi.js +38 -0
- package/lib/quoxon/slash/undoCmd.js +49 -0
- package/lib/quoxon/slash/undoWish.js +15 -0
- package/lib/quoxon/slash/watch.js +164 -0
- package/lib/quoxon/slash/wish.js +23 -0
- package/lib/quoxon/slash/wishCmd.js +32 -0
- package/lib/quoxon/slotChat.js +300 -0
- package/lib/quoxon/slotFormat.js +195 -0
- package/lib/quoxon/slotRun.js +131 -0
- package/lib/quoxon/sound/mapping.js +2 -1
- package/lib/quoxon/stray.js +22 -0
- package/lib/quoxon/suggestCommand.js +59 -0
- package/lib/quoxon/term.js +46 -11
- package/lib/quoxon/textBuffer.js +176 -2
- package/lib/quoxon/theme.js +58 -25
- package/lib/quoxon/tips.js +433 -0
- package/lib/quoxon/toolError.js +41 -0
- package/lib/quoxon/transcript.js +42 -4
- package/lib/quoxon/web/qface.js +18 -10
- package/lib/quoxon/welcome.js +1 -1
- package/lib/quoxon/whatsnew.js +136 -0
- package/lib/quoxon/whatsnew.jsonl +89 -0
- package/lib/quoxon/windowOrg.js +113 -0
- package/lib/server/diag.js +129 -0
- package/lib/server/doctor.js +71 -0
- package/lib/server/install.js +149 -0
- package/lib/server/pluginEnable.js +161 -0
- package/lib/server/skew.js +56 -0
- package/lib/server/snapshot.js +131 -0
- package/lib/server/status.js +128 -0
- package/lib/server/sys.js +65 -0
- package/lib/server/uninstall.js +63 -0
- package/lib/server/update.js +234 -0
- package/lib/toolOutputShape.js +315 -0
- package/lib/tui/views/HyperionDeck.jsx +79 -0
- package/lib/tui/views/LucidDeck.jsx +209 -217
- package/package.json +4 -1
- package/lib/core/websiteSession.js +0 -148
package/lib/quoxon/api.js
CHANGED
|
@@ -3,13 +3,29 @@
|
|
|
3
3
|
* driven by a fake (demo + tests). All routes per docs/QUOXON_EVENTS_v1.md.
|
|
4
4
|
*/
|
|
5
5
|
import { resolveEndpoint } from '../api/endpoints.js';
|
|
6
|
+
import { rememberEmail } from '../core/auth.js';
|
|
7
|
+
import { isSessionDead } from './sessionDead.js'; // not signin.js: that loads React + Ink (QUOXON-LATENCY)
|
|
6
8
|
import { buildStreamQuery } from '../commands/chat.js';
|
|
7
9
|
import { loadBundledAssistantsRoster } from '../core/agentAlias.js';
|
|
10
|
+
import { bundledIdentities } from './objects/fetch.js';
|
|
8
11
|
import { recordHeldSlot } from '../commands/slot.js';
|
|
9
12
|
import { askGripe, settleGripe } from '../commands/gripe.js';
|
|
13
|
+
import { sendFeedback } from './feedback.js';
|
|
10
14
|
import { fromObjective, fromExecution, fromApproval, sortRows } from './roster.js';
|
|
11
15
|
import { normChoice } from './model.js';
|
|
16
|
+
import { normPrefs as normModelPrefs } from './modelPrefs.js';
|
|
17
|
+
import { createRowsStore } from './rowsStore.js';
|
|
12
18
|
import { createObjectsApi } from './objects/fetch.js';
|
|
19
|
+
import { createKillerApi } from './slash/killerApi.js';
|
|
20
|
+
import { createUndoOps } from './slash/undoApi.js';
|
|
21
|
+
import { resolveDashboardBase, resolveCollectorBase, detectLanIp } from './hostUrls.js';
|
|
22
|
+
import { grantBody } from './consent.js';
|
|
23
|
+
import { loadJourney } from './onboarding/journey.js';
|
|
24
|
+
import { joinServiceUrl } from '../core/config.js';
|
|
25
|
+
import { runVmCommand } from '../core/vmTui.js';
|
|
26
|
+
import { fetchActivity, heldLines } from '../core/hyperion.js';
|
|
27
|
+
import { createPeekFetchers, peekId } from '../core/peek.js';
|
|
28
|
+
import { askAside } from '../core/aside.js';
|
|
13
29
|
|
|
14
30
|
const missing = (e) => e?.status === 404 || e?.status === 405 || e?.status === 501;
|
|
15
31
|
/** Server steer refusal label -> client reason. Unknown labels are 'ambiguous', never "finished". */
|
|
@@ -48,12 +64,37 @@ async function* readTurn(open, cur, signal) {
|
|
|
48
64
|
const sleepFor = (ms, signal) => new Promise((r) => { if (signal?.aborted) return r(); const t = setTimeout(done, ms); function done() { clearTimeout(t); signal?.removeEventListener?.('abort', done); r(); } signal?.addEventListener?.('abort', done, { once: true }); });
|
|
49
65
|
const listOf = (d, ...keys) => { for (const k of keys) if (Array.isArray(d?.[k])) return d[k]; return Array.isArray(d) ? d : null; };
|
|
50
66
|
|
|
51
|
-
export
|
|
67
|
+
export const SLOTS_UNLICENSED = 'Slots need a licence. This Quox does not have one, so /slot is off. See: quox setup license status';
|
|
68
|
+
// Z2H run-6: every slot write (checkout, /say, /broadcast) names the licence reason the same way.
|
|
69
|
+
/** #1288: a refused vault write, keeping the server's "confirm this private address" hint. */
|
|
70
|
+
const vaultFailure = (e) => ({ ok: false, status: e?.status, message: e?.message || 'failed', ...(e?.body?.confirmable === true ? { confirmable: true, origin: typeof e.body.origin === 'string' ? e.body.origin : null } : {}) });
|
|
71
|
+
const slotsUnlicensed = (e) => e?.status === 403 && /not licen[cs]ed/i.test(`${e?.code || ''} ${e?.body?.error || ''} ${e?.message || ''}`);
|
|
72
|
+
|
|
73
|
+
/** PRESENCE: only a person at a real terminal is "back". --print, pipes, proof runs and agents are not. */
|
|
74
|
+
export function isInteractiveHuman({ stdin = process.stdin, stdout = process.stdout, env = process.env } = {}) {
|
|
75
|
+
return Boolean(stdin?.isTTY && stdout?.isTTY) && !env.CI;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function createApi({ client, config, auth, windowOrg = null, reattachBackoff = REATTACH_BACKOFF_MS }) {
|
|
79
|
+
let holderSessionFn = null;
|
|
52
80
|
const orgId = () => { try { return auth.getOrgId(); } catch { return null; } };
|
|
81
|
+
const rowsStore = createRowsStore({ client, config, orgId });
|
|
82
|
+
let credsSeen = null; // credentials-file mtime last seen (credentialsChanged)
|
|
53
83
|
const userId = () => { try { return config.getAuth?.()?.user_id || null; } catch { return null; } };
|
|
84
|
+
/** Z2H-R3: the server's public app URL (instance_config app_base_url), or null (not admin, old server, unset). Never throws. */
|
|
85
|
+
const serverAppUrl = async () => {
|
|
86
|
+
try { const ep = resolveEndpoint('admin.instance.basics.get'); const r = await client.get(ep.path, { service: ep.service, retries: 0 }); return r?.effective?.appBaseUrl || null; } catch { return null; }
|
|
87
|
+
};
|
|
88
|
+
const journeyUrl = () => { try { return loadJourney()?.dashboardUrl || null; } catch { return null; } };
|
|
54
89
|
const listApprovals = async () => {
|
|
55
|
-
|
|
56
|
-
const
|
|
90
|
+
// quox#827: the set comes from the server's one open-approvals function (the same one behind the dashboard badge and LUCID).
|
|
91
|
+
const ep = resolveEndpoint('inbox.approvals.open', { orgId: orgId() });
|
|
92
|
+
try {
|
|
93
|
+
const data = await client.get(ep.path, { service: ep.service });
|
|
94
|
+
if (Array.isArray(data?.approvals)) return data.approvals.map((a) => a.item).filter(Boolean);
|
|
95
|
+
} catch (e) { if (!missing(e)) throw e; } // an older server without the route: fall back to the inbox list below
|
|
96
|
+
const old = resolveEndpoint('inbox.list', { orgId: orgId() });
|
|
97
|
+
const data = await client.get(old.path, { service: old.service, query: { kind: 'approval', include_snoozed: 'true', limit: 200 } }); // status omitted = every ACTIVE status
|
|
57
98
|
const items = data.items || data.data || data || [];
|
|
58
99
|
return Array.isArray(items) ? items : [];
|
|
59
100
|
};
|
|
@@ -93,11 +134,75 @@ export function createApi({ client, config, auth, reattachBackoff = REATTACH_BAC
|
|
|
93
134
|
err.code = 'reattach_failed';
|
|
94
135
|
throw err;
|
|
95
136
|
}
|
|
137
|
+
/** PRESENCE auto-clear: the human's own first real action. -> { ok, cleared, welcome } (welcome once: { away_text, via }). Never throws. */
|
|
138
|
+
async function noteActivity(action, { quiet = false, interactive = isInteractiveHuman() } = {}) {
|
|
139
|
+
if (!interactive) return { ok: false, cleared: false, welcome: null, skipped: 'not interactive' };
|
|
140
|
+
try {
|
|
141
|
+
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 } : {}) } });
|
|
143
|
+
return { ok: true, cleared: d?.cleared === true, welcome: d?.welcome || null };
|
|
144
|
+
} catch (e) { return { ok: false, cleared: false, welcome: null, status: e?.status || null }; }
|
|
145
|
+
}
|
|
96
146
|
return {
|
|
147
|
+
...createKillerApi({ client }),
|
|
97
148
|
orgId,
|
|
149
|
+
undoOps: createUndoOps({ client, orgId }), // /undo
|
|
98
150
|
userId,
|
|
151
|
+
/** /peek <id>: the same lookup as `quox peek`, in this window's org. -> peekId result */
|
|
152
|
+
peek: (id) => peekId(id, createPeekFetchers({ client, orgId })),
|
|
153
|
+
/** /aside: a no-tools, no-conversation question (core/aside.js). turns = recent exchanges, read-only context. */
|
|
154
|
+
aside: (question, turns = []) => askAside(client, { question, turns }),
|
|
155
|
+
/** @mention hosts: the Bastion inventory (same read as `quox fleet hosts`). -> [{ id, group, health }] */
|
|
156
|
+
async mentionHosts() {
|
|
157
|
+
const ep = resolveEndpoint('v1.fleet.hosts.list');
|
|
158
|
+
const d = await client.get(ep.path, { service: ep.service, retries: 0, timeout: 8000 });
|
|
159
|
+
return (listOf(d, 'hosts') || []).map(h => ({ id: h.id, group: h.group || '', health: h.status?.health || '' }));
|
|
160
|
+
},
|
|
161
|
+
/** @mention tasks: the newest objectives (one read, cached by the composer for the session). -> [{ id, title, status }] */
|
|
162
|
+
async mentionTasks() {
|
|
163
|
+
const ep = resolveEndpoint('objectives.list');
|
|
164
|
+
const d = await client.get(ep.path, { service: ep.service, query: { limit: 30 }, retries: 0, timeout: 8000 });
|
|
165
|
+
return (listOf(d, 'objectives', 'data') || []).map(o => ({ id: o.id, title: o.title || '', status: o.status || '' }));
|
|
166
|
+
},
|
|
167
|
+
/** Z2H: the same login path as `quox login` (createAuth().login). Returns only what the card needs; tokens are stored by auth, never surfaced. */
|
|
168
|
+
async signIn(email, password) {
|
|
169
|
+
const r = await auth.login(config.getServiceUrl('auth'), email, password);
|
|
170
|
+
return { mfa_required: Boolean(r.mfa_required), access_token: r.mfa_required ? r.access_token : undefined, user: r.user ? { email: r.user.email } : null };
|
|
171
|
+
},
|
|
172
|
+
verifyMfa: (token, code) => auth.verifyMfa(config.getServiceUrl('auth'), token, code),
|
|
173
|
+
/** The last email this profile signed in with (prefill for /login), or ''. */
|
|
174
|
+
lastEmail() { try { return config.getAuth?.()?.email || config.getActiveProfile?.()?.last_email || ''; } catch { return ''; } },
|
|
175
|
+
/**
|
|
176
|
+
* Startup/ongoing check: is the stored session dead? Only a locally expired token is probed, via the refresh seam.
|
|
177
|
+
* -> { alive: true } | { alive: false, reason: 'idle_timeout'|'max_session'|null }. A transient failure reads as alive (we learned nothing).
|
|
178
|
+
*/
|
|
179
|
+
async probeSession() {
|
|
180
|
+
try {
|
|
181
|
+
if (!auth.isExpired?.()) return { alive: true };
|
|
182
|
+
try { rememberEmail(config, config.getAuth?.()?.email); } catch { /* best-effort */ }
|
|
183
|
+
await auth.refresh(config.getServiceUrl('auth'));
|
|
184
|
+
return { alive: true };
|
|
185
|
+
} catch (e) {
|
|
186
|
+
return isSessionDead(e) ? { alive: false, reason: e.reason || null } : { alive: true };
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
/**
|
|
190
|
+
* Stale sign-in: true once each time the credentials file changed on disk since the last call (another window
|
|
191
|
+
* or `quox login` wrote it). A cheap stat; the first call only records the baseline.
|
|
192
|
+
*/
|
|
193
|
+
credentialsChanged() {
|
|
194
|
+
try {
|
|
195
|
+
const m = config.credentialsMtime?.() || 0;
|
|
196
|
+
const was = credsSeen; credsSeen = m;
|
|
197
|
+
return was !== null && m !== was;
|
|
198
|
+
} catch { return false; }
|
|
199
|
+
},
|
|
200
|
+
/** Re-read this profile's credentials from disk. -> true when a signed-in profile is there to retry with. */
|
|
201
|
+
reloadCredentials() {
|
|
202
|
+
try { config.reloadAuth?.(); return Boolean(config.getAuth?.()?.access_token); } catch { return false; }
|
|
203
|
+
},
|
|
99
204
|
/** Inline object views (objects/): skill, workflow, crystal, agent, memory, rules, proof, reconcile. */
|
|
100
|
-
objects: createObjectsApi({ client, orgId }),
|
|
205
|
+
objects: createObjectsApi({ client, orgId, profile: config?._effectiveProfile || null }),
|
|
101
206
|
/** /model choice persisted on the active profile ({ provider, model? } only, never a key); null = auto. */
|
|
102
207
|
modelChoice() { try { return normChoice(config.getActiveProfile?.()?.chat_model); } catch { return null; } },
|
|
103
208
|
setModelChoice(choice) {
|
|
@@ -119,6 +224,46 @@ export function createApi({ client, config, auth, reattachBackoff = REATTACH_BAC
|
|
|
119
224
|
await client.patch(ep.path, { service: ep.service, body: { preferences: { soundProfile: value } } });
|
|
120
225
|
return true;
|
|
121
226
|
},
|
|
227
|
+
/**
|
|
228
|
+
* QUOXON-FW6: model favourites + default. The server is the source of truth: the user's
|
|
229
|
+
* `quoxonModels` preference (PATCH /api/auth/me merges top-level namespaces, the same store as
|
|
230
|
+
* soundProfile). The active CLI profile keeps a copy (model_prefs) for offline starts.
|
|
231
|
+
* -> { prefs, source: 'server' | 'local', error? }.
|
|
232
|
+
*/
|
|
233
|
+
async modelPrefs() {
|
|
234
|
+
const local = () => normModelPrefs(config.getActiveProfile?.()?.model_prefs);
|
|
235
|
+
try {
|
|
236
|
+
const ep = resolveEndpoint('auth.me.list');
|
|
237
|
+
const d = await client.get(ep.path, { service: ep.service, retries: 0 });
|
|
238
|
+
const raw = (d?.user?.preferences ?? d?.preferences)?.quoxonModels;
|
|
239
|
+
const prefs = normModelPrefs(raw);
|
|
240
|
+
const prof = config.getActiveProfile?.();
|
|
241
|
+
if (prof && raw !== undefined) { prof.model_prefs = prefs; config.save?.(); }
|
|
242
|
+
const useLocal = raw === undefined && local().favourites.length;
|
|
243
|
+
return { prefs: useLocal ? local() : prefs, source: 'server' };
|
|
244
|
+
} catch (e) { return { prefs: local(), source: 'local', error: e?.message || 'failed' }; }
|
|
245
|
+
},
|
|
246
|
+
/** -> { ok, synced, error? }: the local copy is always written; synced=false: refused. */
|
|
247
|
+
async setModelPrefs(value) {
|
|
248
|
+
const prefs = normModelPrefs(value);
|
|
249
|
+
const prof = config.getActiveProfile?.();
|
|
250
|
+
if (prof) { prof.model_prefs = prefs; config.save?.(); }
|
|
251
|
+
try {
|
|
252
|
+
const ep = resolveEndpoint('auth.me.update');
|
|
253
|
+
const body = { preferences: { quoxonModels: prefs } };
|
|
254
|
+
await client.patch(ep.path, { service: ep.service, body, retries: 0 });
|
|
255
|
+
return { ok: true, synced: true };
|
|
256
|
+
} catch (e) { return { ok: Boolean(prof), synced: false, error: e?.message || 'failed' }; }
|
|
257
|
+
},
|
|
258
|
+
localModelPrefs() {
|
|
259
|
+
try { return normModelPrefs(config.getActiveProfile?.()?.model_prefs); }
|
|
260
|
+
catch { return normModelPrefs(null); }
|
|
261
|
+
},
|
|
262
|
+
/**
|
|
263
|
+
* QUOXON-FW17 / ROWS: custom rows. url, quox and plugin rows sync per user (preference namespace `quoxonRows`);
|
|
264
|
+
* cmd rows stay in a local file. One implementation, shared with `quox rows` (rowsStore.js).
|
|
265
|
+
*/
|
|
266
|
+
...rowsStore,
|
|
122
267
|
/** Local copy on the active CLI profile: offline start + fallback store. */
|
|
123
268
|
localSoundProfile() { try { return config.getActiveProfile?.()?.sound_profile || null; } catch { return null; } },
|
|
124
269
|
setLocalSoundProfile(p) { const prof = config.getActiveProfile?.(); if (!prof) return false; prof.sound_profile = p; config.save?.(); return true; },
|
|
@@ -129,17 +274,21 @@ export function createApi({ client, config, auth, reattachBackoff = REATTACH_BAC
|
|
|
129
274
|
* by seq. While reconnecting it yields { type:'reconnecting', attempt } and, once back, { type:'resumed', after }.
|
|
130
275
|
* A server without v1.1 (no seq on its events) gets the v1 behaviour: no reattach.
|
|
131
276
|
*/
|
|
132
|
-
async *stream({ turnId, message, agent, conversationId, context, provider, model, promptProfile, durable = true }, signal) {
|
|
277
|
+
async *stream({ turnId, message, agent, conversationId, context, provider, model, promptProfile, heldSlot = null, durable = true, overrideId = undefined, turnBudgetMs = undefined, mode = undefined, handoffFrom = undefined }, signal) {
|
|
133
278
|
const body = buildStreamQuery({ queryId: turnId, message, agent: agent || 'quox', sessionId: conversationId, context });
|
|
134
279
|
body.turn_id = turnId;
|
|
135
280
|
if (durable) body.durable = true; // the server keeps the turn running when we disconnect (v1.1)
|
|
281
|
+
if (Number(turnBudgetMs) > 0) body.turn_budget_ms = Math.floor(Number(turnBudgetMs)); // quox#1090: a delegation hop may use the turn's budget
|
|
136
282
|
if (provider) body.provider = provider; // a request: the server policy decides, turn.started says what ran
|
|
137
283
|
if (provider && model) body.model = model;
|
|
138
284
|
if (promptProfile === 'compact' || promptProfile === 'full') body.prompt_profile = promptProfile; // D3: server default when absent
|
|
139
285
|
if (conversationId) body.conversation_id = conversationId;
|
|
286
|
+
if (mode === 'plan') body.mode = 'plan'; // PLAN MODE: the server makes no state-changing tool eligible and refuses writes
|
|
287
|
+
if (heldSlot?.name && heldSlot?.session) body.held_slot = heldSlot; // QUOXON-MESH: slot_say signs as this window's slot
|
|
288
|
+
if (handoffFrom) body.handoff_from = String(handoffFrom).slice(0, 80); // a handed-off turn: this agent stays attached, the server labels the exchange for it
|
|
140
289
|
const ep = resolveEndpoint('chat.stream');
|
|
141
290
|
const cur = { lastSeq: 0, finished: false, conversationId: conversationId || null };
|
|
142
|
-
const drop = yield* readTurn((sig) => client.stream(ep.path, { service: ep.service, body, signal: sig, untilClose: true }), cur, signal);
|
|
291
|
+
const drop = yield* readTurn((sig) => client.stream(ep.path, { service: ep.service, body, signal: sig, untilClose: true, ...(overrideId ? { headers: { 'x-override-id': overrideId } } : {}) }), cur, signal);
|
|
143
292
|
if (cur.finished || !drop || !durable || signal?.aborted) return;
|
|
144
293
|
if (!cur.lastSeq || !cur.conversationId) { if (drop.error) throw drop.error; return; } // v1 server: nothing to reattach to
|
|
145
294
|
yield* reattachLoop({ turnId, cur, signal, first: drop.error, backoff: reattachBackoff });
|
|
@@ -208,20 +357,24 @@ export function createApi({ client, config, auth, reattachBackoff = REATTACH_BAC
|
|
|
208
357
|
const roster = loadBundledAssistantsRoster();
|
|
209
358
|
const nameOf = new Map(roster.map(r => [r.id, r.name || r.shortName || r.id]));
|
|
210
359
|
const out = new Map(); let ok = 0; let lastErr = null;
|
|
360
|
+
let ids = null; const roleOf = (id) => { ids ??= bundledIdentities(); const i = ids.get(id); return i?.tagline || i?.description || null; };
|
|
211
361
|
try {
|
|
212
362
|
const ep = resolveEndpoint('v1.tools.agents.list');
|
|
213
|
-
const d = await client.get(ep.path, { service: ep.service,
|
|
214
|
-
for (const id of Object.keys(d?.agentTools || {})) out.set(id, { agent_id: id, name: nameOf.get(id) || id });
|
|
363
|
+
const d = await client.get(ep.path, { service: ep.service, retries: 0 }); // authenticated: the collector's default-deny gate 401s a noAuth read
|
|
364
|
+
for (const id of Object.keys(d?.agentTools || {})) out.set(id, { agent_id: id, name: nameOf.get(id) || id, role: roleOf(id), status: 'registered' });
|
|
215
365
|
ok++;
|
|
216
|
-
} catch (e) { lastErr = e; for (const r of roster) out.set(r.id, { agent_id: r.id, name: nameOf.get(r.id) }); if (roster.length) ok++; }
|
|
366
|
+
} catch (e) { lastErr = e; for (const r of roster) out.set(r.id, { agent_id: r.id, name: nameOf.get(r.id), role: roleOf(r.id) }); if (roster.length) ok++; }
|
|
217
367
|
try {
|
|
218
368
|
const ep = resolveEndpoint('agents.list', { orgId: orgId() });
|
|
219
369
|
const rows = listOf(await client.get(ep.path, { service: ep.service, query: { includeTools: 'false' }, retries: 0 }), 'agents', 'data', 'items');
|
|
220
|
-
for (const r of rows || []) if (r.agent_id && !out.has(r.agent_id)) out.set(r.agent_id, { agent_id: r.agent_id, name: r.name || r.agent_id });
|
|
370
|
+
for (const r of rows || []) if (r.agent_id && !out.has(r.agent_id)) out.set(r.agent_id, { agent_id: r.agent_id, name: r.name || r.agent_id, role: r.description || null, status: r.status || null });
|
|
221
371
|
if (rows) ok++;
|
|
222
372
|
} catch (e) { lastErr = lastErr || e; }
|
|
223
373
|
if (!ok) throw lastErr || new Error('no agent list available');
|
|
224
|
-
|
|
374
|
+
const list = [...out.values()];
|
|
375
|
+
// The live registry failed and the bundled roster stood in: say so, so a miss is never read as "no such agent".
|
|
376
|
+
if (lastErr) Object.defineProperty(list, 'listError', { value: lastErr });
|
|
377
|
+
return list;
|
|
225
378
|
},
|
|
226
379
|
/** -> saved turns of one of YOUR conversations, oldest first [{ id, role, content }]; [] when none or not yours. */
|
|
227
380
|
async conversationTurns(conversationId) {
|
|
@@ -236,6 +389,24 @@ export function createApi({ client, config, auth, reattachBackoff = REATTACH_BAC
|
|
|
236
389
|
if (!d || !Array.isArray(d.groups)) throw new Error('unexpected response');
|
|
237
390
|
return d;
|
|
238
391
|
},
|
|
392
|
+
/** GET /api/consent: this user's AI data sharing record ({ ai_data_sharing_consent, provider_consent }). */
|
|
393
|
+
async consentStatus() {
|
|
394
|
+
const ep = resolveEndpoint('consent.list');
|
|
395
|
+
return client.get(ep.path, { service: ep.service, retries: 0 });
|
|
396
|
+
},
|
|
397
|
+
/** Grant AI data sharing consent for one provider (the user's own act; keeps the other providers' settings). */
|
|
398
|
+
async grantConsent(provider) {
|
|
399
|
+
const lst = resolveEndpoint('consent.list');
|
|
400
|
+
let cur = null; try { cur = await client.get(lst.path, { service: lst.service, retries: 0 }); } catch { cur = null; }
|
|
401
|
+
const ep = resolveEndpoint('consent.create');
|
|
402
|
+
await client.post(ep.path, { service: ep.service, body: grantBody(cur, provider), retries: 0 });
|
|
403
|
+
return true;
|
|
404
|
+
},
|
|
405
|
+
/** The dashboard address to print (never localhost when a better one is known). */
|
|
406
|
+
async dashboardBase() {
|
|
407
|
+
const profileUrl = config.getActiveProfile?.()?.url || config.getBaseUrl();
|
|
408
|
+
return resolveDashboardBase({ serverApp: await serverAppUrl(), profileUrl, journeyUrl: journeyUrl(), lanIp: detectLanIp() }).url;
|
|
409
|
+
},
|
|
239
410
|
/** POST /api/v1/conversations/:id/compact (server-side summary; a model call, so a long timeout and never retried). */
|
|
240
411
|
async compactConversation(conversationId) {
|
|
241
412
|
return client.post(`/api/v1/conversations/${encodeURIComponent(conversationId)}/compact`, { service: 'collector', body: {}, retries: 0, timeout: 180000 });
|
|
@@ -254,6 +425,36 @@ export function createApi({ client, config, auth, reattachBackoff = REATTACH_BAC
|
|
|
254
425
|
},
|
|
255
426
|
/** The `quox doctor` checks, run in this process. */
|
|
256
427
|
async doctorChecks() { const { runDoctorChecks } = await import('../commands/doctor.js'); return runDoctorChecks({ config }); },
|
|
428
|
+
/** C4: the `quox server status` panel rows for the local install. -> { ok, rows } | { ok:false, error }. */
|
|
429
|
+
async serverStatus() {
|
|
430
|
+
const { createSys } = await import('../server/sys.js');
|
|
431
|
+
const { resolveInstall, backupsRoot } = await import('../server/install.js');
|
|
432
|
+
const { gatherStatus, statusRows, defaultUrls } = await import('../server/status.js');
|
|
433
|
+
const sys = createSys();
|
|
434
|
+
const install = await resolveInstall({ sys });
|
|
435
|
+
if (install.error) return { ok: false, error: install.error };
|
|
436
|
+
const st = await gatherStatus({ install, sys, urls: defaultUrls(config), backups: backupsRoot() });
|
|
437
|
+
return { ok: true, rows: statusRows(st) };
|
|
438
|
+
},
|
|
439
|
+
/** /dashboard: mint a one-time sign-in link for this profile (see slash/dashboard.js). Never throws; kind 'link' | 'plain' | 'error'. */
|
|
440
|
+
async dashboardLink(page) {
|
|
441
|
+
const { runDashboard } = await import('./slash/dashboard.js');
|
|
442
|
+
const profileUrl = config.getActiveProfile?.()?.url || config.getBaseUrl();
|
|
443
|
+
const baseUrl = resolveDashboardBase({ serverApp: await serverAppUrl(), profileUrl, journeyUrl: journeyUrl(), lanIp: detectLanIp() }).url;
|
|
444
|
+
return runDashboard({ page, baseUrl, mint: () => { const ep = resolveEndpoint('auth.login-link'); return client.post(ep.path, { service: ep.service, body: {}, retries: 0 }); } });
|
|
445
|
+
},
|
|
446
|
+
/** Z2H-R3: the address to print for the host installer (never localhost when a better one is known). */
|
|
447
|
+
async hostsBaseUrl() {
|
|
448
|
+
let collectorUrl = null; try { collectorUrl = new URL(config.getServiceUrl('collector')).origin; } catch { /* none */ }
|
|
449
|
+
return resolveCollectorBase({ collectorUrl, serverApp: await serverAppUrl(), journeyUrl: journeyUrl(), lanIp: detectLanIp() }).url;
|
|
450
|
+
},
|
|
451
|
+
/** Z2H-3c: can `${base}/install` be served? -> { ok, status } or null when unreachable (never throws, 4 s cap). */
|
|
452
|
+
async hostInstallCheck(base) {
|
|
453
|
+
if (!base || /^<|example/.test(String(base))) return null;
|
|
454
|
+
const ctl = new AbortController(); const t = setTimeout(() => ctl.abort(), 4000);
|
|
455
|
+
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
|
+
catch { return null; } finally { clearTimeout(t); }
|
|
457
|
+
},
|
|
257
458
|
serverUrl() { try { return new URL(config.getServiceUrl('collector')).origin; } catch { return null; } },
|
|
258
459
|
/** /config settings on the active profile. */
|
|
259
460
|
settings() { try { const q = config.getActiveProfile?.()?.quoxon; return q && typeof q === 'object' ? { ...q } : {}; } catch { return {}; } },
|
|
@@ -287,17 +488,198 @@ export function createApi({ client, config, auth, reattachBackoff = REATTACH_BAC
|
|
|
287
488
|
return { ok: false, message: e?.status ? `HTTP ${e.status}: ${e.message}` : (e?.message || 'failed') };
|
|
288
489
|
}
|
|
289
490
|
},
|
|
290
|
-
|
|
491
|
+
/**
|
|
492
|
+
* /feedback: POST the previewed payload to the public quox.ai feedback API (not the org's server, not GitHub).
|
|
493
|
+
* Anonymous by design: no token, cookie, org or profile goes with it. -> { ok:true, id, url } | { ok:false, message }.
|
|
494
|
+
*/
|
|
495
|
+
async feedback(payload, { version = null } = {}) { return sendFeedback(payload, { version }); },
|
|
496
|
+
/** Installed, enabled plugin ids for the org (GET plugins.list, as `quox plugin list`). Throws on failure so it never reads as "none". */
|
|
497
|
+
async installedPlugins() {
|
|
498
|
+
const id = orgId();
|
|
499
|
+
if (!id) throw new Error('no org');
|
|
500
|
+
const ep = resolveEndpoint('plugins.list', { orgId: id });
|
|
501
|
+
const data = await client.get(ep.path, { service: ep.service, retries: 0 });
|
|
502
|
+
const rows = listOf(data, 'plugins', 'data', 'items');
|
|
503
|
+
if (!rows) throw new Error('unexpected response');
|
|
504
|
+
return rows.filter(r => r.is_enabled !== false && r.is_enabled !== 0).map(r => r.plugin_id).filter(Boolean);
|
|
505
|
+
},
|
|
506
|
+
/** 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
|
+
async hostCount() {
|
|
508
|
+
const ep = resolveEndpoint('v1.agents.list');
|
|
509
|
+
const rows = listOf(await client.get(ep.path, { service: ep.service, retries: 0 }), 'agents', 'data', 'items');
|
|
510
|
+
if (!rows) throw new Error('unexpected response');
|
|
511
|
+
return rows.length;
|
|
512
|
+
},
|
|
513
|
+
/** -> [{ name, host_name, assigned_to, ... }] ; GET /api/v1/slots (throws on failure, never reads as empty). */
|
|
514
|
+
async slots() {
|
|
515
|
+
const ep = resolveEndpoint('v1.slots.list');
|
|
516
|
+
const rows = listOf(await client.get(ep.path, { service: ep.service, retries: 0 }), 'slots');
|
|
517
|
+
if (!rows) throw new Error('unexpected response');
|
|
518
|
+
return rows;
|
|
519
|
+
},
|
|
520
|
+
/**
|
|
521
|
+
* Post a message into a slot room (same call as `quox slot say`). QUOXON-MESH: `from` signs it as this window
|
|
522
|
+
* ("quox1 (Adam)"); `fast` answers once the line is in the slot, without waiting on the chat-app bridges.
|
|
523
|
+
* -> { ok:true, message } | { ok:false, message, status }.
|
|
524
|
+
*/
|
|
525
|
+
/** SLOTS-REPLY2: the window registers how to name its checkout session; slotSay then sends it as holder_session. */
|
|
526
|
+
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 ep = resolveEndpoint('v1.slots.say', { name });
|
|
529
|
+
try {
|
|
530
|
+
// SLOTS-REPLY: fromSlot is this window's held slot, its reply address (the server verifies the hold).
|
|
531
|
+
const d = await client.post(ep.path, { service: ep.service, retries: 0, body: { text, org_id: orgId(), ...(roomSession ? { room_session: roomSession } : from ? { from } : {}), ...(!roomSession && fromSlot ? { from_slot: fromSlot } : {}), ...(!roomSession && holderSession ? { holder_session: holderSession } : {}), ...(fast ? { bridge_report: false } : {}) } });
|
|
532
|
+
return { ok: true, message: d?.message || null, roomPending: d?.room_pending === true, presence: d?.presence || null, humanPresence: d?.human_presence || null };
|
|
533
|
+
} catch (e) {
|
|
534
|
+
if (slotsUnlicensed(e)) return { ok: false, unlicensed: true, status: 403, message: SLOTS_UNLICENSED };
|
|
535
|
+
return { ok: false, message: e?.status === 404 ? `no slot named ${name}` : (e?.message || 'failed'), status: e?.status || null };
|
|
536
|
+
}
|
|
537
|
+
},
|
|
538
|
+
/** 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 } = {}) {
|
|
540
|
+
const ep = resolveEndpoint('v1.slots.broadcast');
|
|
541
|
+
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 } : {}) } });
|
|
543
|
+
return { ok: true, data };
|
|
544
|
+
} catch (e) {
|
|
545
|
+
if (slotsUnlicensed(e)) return { ok: false, unlicensed: true, status: 403, message: SLOTS_UNLICENSED };
|
|
546
|
+
return { ok: false, message: e?.message || 'failed', status: e?.status || null };
|
|
547
|
+
}
|
|
548
|
+
},
|
|
549
|
+
async slotBroadcastStatus(id) {
|
|
550
|
+
const ep = resolveEndpoint('v1.slots.broadcast.get', { id });
|
|
551
|
+
try { return { ok: true, data: await client.get(ep.path, { service: ep.service, retries: 0, query: { org_id: orgId() } }) }; } catch (e) { return { ok: false, message: e?.message || 'failed', status: e?.status || null }; }
|
|
552
|
+
},
|
|
553
|
+
// POKE-WAKE: slot control (server allows a human org owner/admin only; agents are refused).
|
|
554
|
+
async slotControl(slot, command) {
|
|
555
|
+
const ep = resolveEndpoint('v1.slots.control', { name: slot });
|
|
556
|
+
try { return { ok: true, data: await client.post(ep.path, { service: ep.service, retries: 0, body: { command, org_id: orgId() } }) }; } catch (e) { return { ok: false, message: e?.message || 'failed', status: e?.status || null }; }
|
|
557
|
+
},
|
|
558
|
+
async slotControlGet(slot, id) {
|
|
559
|
+
const ep = resolveEndpoint('v1.slots.control.get', { name: slot, id });
|
|
560
|
+
try { return { ok: true, data: await client.get(ep.path, { service: ep.service, retries: 0, query: { org_id: orgId() } }) }; } catch (e) { return { ok: false, message: e?.message || 'failed', status: e?.status || null }; }
|
|
561
|
+
},
|
|
562
|
+
async slotAck(id, slot) {
|
|
563
|
+
const ep = resolveEndpoint('v1.slots.broadcast.ack', { id });
|
|
564
|
+
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
|
+
},
|
|
566
|
+
/** QUOXON-MESH: the slot's live messages (SSE). Yields { type: 'connected' | 'slot.message' | 'reconnect' }. */
|
|
567
|
+
async *slotStream(name, { since = null, excludeFrom = null, session = null } = {}, signal) {
|
|
568
|
+
const ep = resolveEndpoint('v1.slots.stream', { name });
|
|
569
|
+
yield* client.stream(ep.path, { service: ep.service, query: { org_id: orgId(), since, exclude_from: excludeFrom, ...(session ? { session } : {}) }, signal, untilClose: true });
|
|
570
|
+
},
|
|
571
|
+
/** QUOXON-ROOMS: join / leave a slot as a room (no hold). -> { ok, already? | removed? } | { ok:false, message, status }. */
|
|
572
|
+
async slotJoin(name, session, person = null) {
|
|
573
|
+
const ep = resolveEndpoint('v1.slots.join', { name });
|
|
574
|
+
try { const d = await client.post(ep.path, { service: ep.service, retries: 0, body: { session, ...(person ? { person } : {}), org_id: orgId() } }); return { ok: true, already: d?.already === true }; }
|
|
575
|
+
catch (e) { return { ok: false, message: e?.status === 404 ? `no slot named ${name}` : (e?.message || 'failed'), status: e?.status || null }; }
|
|
576
|
+
},
|
|
577
|
+
async slotLeave(name, session) {
|
|
578
|
+
const ep = resolveEndpoint('v1.slots.leave', { name });
|
|
579
|
+
try { const d = await client.post(ep.path, { service: ep.service, retries: 0, body: { session, org_id: orgId() } }); return { ok: true, removed: d?.removed === true }; }
|
|
580
|
+
catch (e) { return { ok: false, message: e?.message || 'failed', status: e?.status || null }; }
|
|
581
|
+
},
|
|
582
|
+
/** The rooms this session has joined (server truth, survives a restart). -> [{ slot_name, joined_at }]; throws on failure. */
|
|
583
|
+
async slotRooms(session) {
|
|
584
|
+
const ep = resolveEndpoint('v1.slots.rooms');
|
|
585
|
+
const d = await client.get(ep.path, { service: ep.service, retries: 0, query: { session, org_id: orgId() } });
|
|
586
|
+
return Array.isArray(d?.rooms) ? d.rooms : [];
|
|
587
|
+
},
|
|
588
|
+
/** Long-poll fallback for a server without the stream route (same call as `quox slot wait`). */
|
|
589
|
+
async slotWait(name, { since = null, excludeFrom = null } = {}) {
|
|
590
|
+
const ep = resolveEndpoint('v1.slots.wait', { name });
|
|
591
|
+
return client.get(ep.path, { service: ep.service, retries: 0, timeout: 40000, query: { org_id: orgId(), since, exclude_from: excludeFrom, timeout_ms: 25000 } });
|
|
592
|
+
},
|
|
593
|
+
/** Last `limit` messages of a slot, oldest first (same call as `quox slot read`). Throws on failure. */
|
|
594
|
+
async slotMessages(name, limit = 20) {
|
|
595
|
+
const ep = resolveEndpoint('v1.slots.messages', { name });
|
|
596
|
+
const rows = listOf(await client.get(ep.path, { service: ep.service, retries: 0, query: { org_id: orgId(), limit } }), 'messages');
|
|
597
|
+
if (!rows) throw new Error('unexpected response');
|
|
598
|
+
return rows;
|
|
599
|
+
},
|
|
600
|
+
/** The signed-in human's display name for slot lines (display_name, else the email's local part). */
|
|
601
|
+
displayName() {
|
|
602
|
+
try { const a = config.getAuth?.() || {}; return a.display_name || (a.email ? String(a.email).split('@')[0] : null); } catch { return null; }
|
|
603
|
+
},
|
|
604
|
+
/** `take` = the same signed-in user's other/renamed/restarted session takes a LIVE hold (stale holds of yours are reclaimed regardless). */
|
|
605
|
+
async checkoutSlot(name, session, { take = false } = {}) {
|
|
291
606
|
const ep = resolveEndpoint('v1.slots.checkout', { name });
|
|
292
607
|
try {
|
|
293
|
-
const data = await client.post(ep.path, { service: ep.service, body: { holder_session: session, holder_kind: 'interactive', host_id: null, org_id: orgId(), force: false, create: true } });
|
|
608
|
+
const data = await client.post(ep.path, { service: ep.service, body: { holder_session: session, holder_kind: 'interactive', host_id: null, org_id: orgId(), force: false, create: true, ...(take ? { take: true } : {}) } });
|
|
294
609
|
recordHeldSlot(name, session);
|
|
295
|
-
return { ok: true, slot: data?.slot || data };
|
|
610
|
+
return { ok: true, slot: data?.slot || data, tookOver: data?.took_over || null };
|
|
296
611
|
} catch (e) {
|
|
297
|
-
if (e?.status === 409)
|
|
298
|
-
|
|
612
|
+
if (e?.status === 409) {
|
|
613
|
+
const d = e.detail || {};
|
|
614
|
+
if (d.assigned_to) return { ok: false, message: `slot ${name} is assigned to ${d.assigned_to}` };
|
|
615
|
+
return { ok: false, message: `slot ${name} is held by ${d.holder_session || 'another session'}`, held: { holder_session: d.holder_session || null, same_user: d.same_user === true, state: d.state || null, since: d.checked_out_at || null, last_seen: d.last_heartbeat_at || null } };
|
|
616
|
+
}
|
|
617
|
+
// Z2H-R5 C: a free install answers 403 "QuoxSlots is not licensed": say that, not a bare 403.
|
|
618
|
+
if (slotsUnlicensed(e)) {
|
|
619
|
+
return { ok: false, unlicensed: true, transient: false, status: 403, message: SLOTS_UNLICENSED };
|
|
620
|
+
}
|
|
621
|
+
// No answer, or the server failing (timeout, network, 5xx, e.g. a collector mid-redeploy): worth retrying.
|
|
622
|
+
const st = e?.status || null;
|
|
623
|
+
return { ok: false, message: e?.message || 'failed', status: st, transient: !st || st >= 500 || st === 429 || st === 408 };
|
|
299
624
|
}
|
|
300
625
|
},
|
|
626
|
+
/** Keep a TUI-held slot alive (the hold lapses to stale after 15m without one). -> { ok } */
|
|
627
|
+
async slotHeartbeat(name, session) {
|
|
628
|
+
const ep = resolveEndpoint('v1.slots.heartbeat', { name });
|
|
629
|
+
try { await client.post(ep.path, { service: ep.service, body: { holder_session: session, org_id: orgId() } }); return { ok: true }; }
|
|
630
|
+
catch (e) { return { ok: false, message: e?.message || 'failed' }; }
|
|
631
|
+
},
|
|
632
|
+
/** quox#1183: the org's hats -> [{ id, key, name, definition }]. Same endpoint as `quox hat list`. */
|
|
633
|
+
async hats() {
|
|
634
|
+
const ep = resolveEndpoint('hat.list', { orgId: orgId() });
|
|
635
|
+
const d = await client.get(ep.path, { service: ep.service });
|
|
636
|
+
return Array.isArray(d?.hats) ? d.hats.filter(h => h.status !== 'archived') : [];
|
|
637
|
+
},
|
|
638
|
+
/** CUSTOM-HATS: create an org hat of your own -> { hat, warnings }. Throws the server's refusal unchanged. */
|
|
639
|
+
async createHat({ key, name, definition, visibility = 'org' }) {
|
|
640
|
+
const ep = resolveEndpoint('hat.create', { orgId: orgId() });
|
|
641
|
+
return client.post(ep.path, { service: ep.service, retries: 0, body: { key, name, definition, visibility } });
|
|
642
|
+
},
|
|
643
|
+
/** CUSTOM-HATS: a new revision of a hat -> { hat, warnings }. */
|
|
644
|
+
async updateHat(hatId, { definition, name, change_reason }) {
|
|
645
|
+
const ep = resolveEndpoint('hat.update', { orgId: orgId(), hatId });
|
|
646
|
+
return client.put(ep.path, { service: ep.service, retries: 0, body: { definition, ...(name ? { name } : {}), change_reason } });
|
|
647
|
+
},
|
|
648
|
+
/** CUSTOM-HATS: delete a custom hat -> { deleted: { key, revision, bindings_cleared, user_prefs_cleared } }. */
|
|
649
|
+
async deleteHat(hatId) {
|
|
650
|
+
const ep = resolveEndpoint('hat.delete', { orgId: orgId(), hatId });
|
|
651
|
+
return client.delete(ep.path, { service: ep.service, retries: 0 });
|
|
652
|
+
},
|
|
653
|
+
/** quox#1183: the hat this conversation is on (or would start on) -> { key, name } | null. */
|
|
654
|
+
async activeHat(conversationId) {
|
|
655
|
+
const ep = resolveEndpoint('hat.resolve', { orgId: orgId() });
|
|
656
|
+
const d = await client.get(ep.path, { service: ep.service, ...(conversationId ? { query: { conversation_id: conversationId } } : {}) });
|
|
657
|
+
return d?.hat ? { key: d.hat.key, name: d.hat.name, source: d.source || null } : null;
|
|
658
|
+
},
|
|
659
|
+
/** quox#1183: the person's own hat switch for this conversation (audited on the server). Throws the refusal unchanged. */
|
|
660
|
+
async bindHat(conversationId, hatId) {
|
|
661
|
+
const ep = resolveEndpoint('hat.bind', { orgId: orgId(), conversationId });
|
|
662
|
+
const d = await client.put(ep.path, { service: ep.service, retries: 0, body: { hat_id: hatId } });
|
|
663
|
+
return d?.binding || d;
|
|
664
|
+
},
|
|
665
|
+
/** QUOXON-FW3: the orgs the signed-in user belongs to -> [{ id, name, slug, role }]. Same endpoint as `quox org list`. */
|
|
666
|
+
async orgs() {
|
|
667
|
+
const ep = resolveEndpoint('orgs.list');
|
|
668
|
+
const d = await client.get(ep.path, { service: ep.service });
|
|
669
|
+
const rows = Array.isArray(d) ? d : (d?.organizations || d?.orgs || d?.data || []);
|
|
670
|
+
return Array.isArray(rows) ? rows : [];
|
|
671
|
+
},
|
|
672
|
+
/**
|
|
673
|
+
* QUOXON-FW3, #1034: re-scope THIS WINDOW to another org. The org-scoped token is held in this window's memory
|
|
674
|
+
* (windowOrg); the shared profile, its file and every other window keep their org. `quox org switch` in a shell is
|
|
675
|
+
* the explicit way to change a profile's default. -> { id, name }. Throws the server's refusal unchanged.
|
|
676
|
+
*/
|
|
677
|
+
async switchOrg(targetId) {
|
|
678
|
+
if (!windowOrg) { const e = new Error('Org switching is not available in this mode.'); e.code = 'no_window_org'; throw e; }
|
|
679
|
+
return windowOrg.switchTo(targetId);
|
|
680
|
+
},
|
|
681
|
+
/** The window's own org id when it differs from the profile's (carried across /restart), else null. */
|
|
682
|
+
windowOrgId: () => windowOrg?.current()?.id || null,
|
|
301
683
|
/** -> [{ id, name, description, edition, version, decayState }] ; the org's QSKL procedural skills (read-only list; Crystals are a separate store). */
|
|
302
684
|
async skills() {
|
|
303
685
|
const ep = resolveEndpoint('quoxflow.skills.list');
|
|
@@ -361,6 +743,29 @@ export function createApi({ client, config, auth, reattachBackoff = REATTACH_BAC
|
|
|
361
743
|
}
|
|
362
744
|
return { ok: false, unavailable: true };
|
|
363
745
|
},
|
|
746
|
+
/** /sitrep: the LUCID situation (cursor not moved) and the approvals inbox, each part on its own. */
|
|
747
|
+
async sitrep() {
|
|
748
|
+
const part = async (fn) => { try { return { ok: true, v: await fn() }; } catch (e) { return { ok: false, error: e?.status ? `HTTP ${e.status}` : /timed out/i.test(e?.message || '') ? 'timed out' : (e?.message || 'failed') }; } };
|
|
749
|
+
const [situation, approvals] = await Promise.all([
|
|
750
|
+
part(async () => { const ep = resolveEndpoint('lucid.situation'); return client.get(ep.path, { service: ep.service, query: { since: 'last', surface: 'cli' }, retries: 0, timeout: 12000 }); }),
|
|
751
|
+
part(() => listApprovals())
|
|
752
|
+
]);
|
|
753
|
+
return { situation, approvals };
|
|
754
|
+
},
|
|
755
|
+
/** 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 } = {}) {
|
|
757
|
+
const ep = resolveEndpoint('presence.set');
|
|
758
|
+
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 } : {}) } });
|
|
760
|
+
return { ok: true, presence: d?.presence || null, notice: d?.notice || null };
|
|
761
|
+
} catch (e) { return { ok: false, message: e?.message || 'failed', status: e?.status || null }; }
|
|
762
|
+
},
|
|
763
|
+
noteActivity,
|
|
764
|
+
/** E5 /away: GET /api/v1/away?since=<iso> -> { since, until, items, activity, blind }. Throws on non-2xx. */
|
|
765
|
+
async away(since) {
|
|
766
|
+
const ep = resolveEndpoint('away.digest');
|
|
767
|
+
return client.get(ep.path, { service: ep.service, query: { since }, retries: 0, timeout: 15000 });
|
|
768
|
+
},
|
|
364
769
|
/** Facts for /observers, each part on its own: { lucid, learning:[{ id, label, state, text }] }. */
|
|
365
770
|
async observers() {
|
|
366
771
|
const part = async (fn) => { try { return { ok: true, v: await fn() }; } catch (e) { return { ok: false, unavailable: missing(e), error: e?.message || 'failed' }; } };
|
|
@@ -377,8 +782,27 @@ export function createApi({ client, config, auth, reattachBackoff = REATTACH_BAC
|
|
|
377
782
|
]);
|
|
378
783
|
return { situation: sit, lessons, learning: [reflect, learn, insight] };
|
|
379
784
|
},
|
|
785
|
+
/** /mind [id]: QuoxMind implants from the collector. -> { implants, health } | { implant }. Throws on non-2xx. */
|
|
786
|
+
async mind(id) {
|
|
787
|
+
if (id) { const ep = resolveEndpoint('mind.implant', { id }); return { implant: await client.get(ep.path, { service: ep.service, retries: 0, timeout: 15000 }) }; }
|
|
788
|
+
const [l, h] = await Promise.all([resolveEndpoint('mind.implants'), resolveEndpoint('mind.health')].map((ep) => client.get(ep.path, { service: ep.service, retries: 0, timeout: 15000 })));
|
|
789
|
+
return { implants: l.implants || [], health: h };
|
|
790
|
+
},
|
|
791
|
+
/** /mirror: the Mirror as CommanderQ sees it (compact System Status block + age). Throws on non-2xx. */
|
|
792
|
+
async mirror() {
|
|
793
|
+
const ep = resolveEndpoint('mind.mirror');
|
|
794
|
+
return client.get(ep.path, { service: ep.service, retries: 0, timeout: 10000 });
|
|
795
|
+
},
|
|
796
|
+
/** #904 part 3: providers refusing turns right now (weekly limit, no credits). -> [{ provider, state, line }] | null on error. */
|
|
797
|
+
async providerHealth() {
|
|
798
|
+
try {
|
|
799
|
+
const ep = resolveEndpoint('chat.status.list');
|
|
800
|
+
const d = await client.get(ep.path, { service: ep.service, retries: 0, timeout: 8000 });
|
|
801
|
+
return Array.isArray(d?.provider_health) ? d.provider_health : [];
|
|
802
|
+
} catch { return null; }
|
|
803
|
+
},
|
|
380
804
|
async health(timeoutMs = 12000) {
|
|
381
|
-
const url =
|
|
805
|
+
const url = joinServiceUrl(config.getServiceUrl('collector'), '/health');
|
|
382
806
|
const ctrl = new AbortController();
|
|
383
807
|
const t = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
384
808
|
try { const r = await fetch(url, { signal: ctrl.signal }); return { ok: r.ok, status: r.status, url: url.origin }; }
|
|
@@ -419,20 +843,32 @@ export function createApi({ client, config, auth, reattachBackoff = REATTACH_BAC
|
|
|
419
843
|
} catch (e) { return { ok: false, status: e?.status, message: e?.message || 'failed' }; }
|
|
420
844
|
},
|
|
421
845
|
/** The governed vault write (same route as `quox vault create`): audited, AEE envelope carries type + label only. */
|
|
422
|
-
async saveProviderKey({ type, label, credentials, isDefault }) {
|
|
846
|
+
async saveProviderKey({ type, label, credentials, isDefault, confirmPrivateOrigin }) {
|
|
423
847
|
const ep = resolveEndpoint('vault.create', { orgId: orgId() });
|
|
424
848
|
try {
|
|
425
|
-
const r = await client.post(ep.path, { service: ep.service, body: { integration_type: type, label, credentials, is_default: Boolean(isDefault) }, retries: 0, sensitive: true });
|
|
849
|
+
const r = await client.post(ep.path, { service: ep.service, body: { integration_type: type, label, credentials, is_default: Boolean(isDefault), ...(confirmPrivateOrigin ? { confirm_private_origin: confirmPrivateOrigin } : {}) }, retries: 0, sensitive: true });
|
|
426
850
|
return { ok: Boolean(r?.id), id: r?.id || null, message: r?.id ? null : 'unexpected response' };
|
|
427
|
-
} catch (e) { return
|
|
851
|
+
} catch (e) { return vaultFailure(e); }
|
|
428
852
|
},
|
|
429
853
|
/** B3: refresh an existing vault credential (same governed route as `quox vault update`). */
|
|
430
|
-
async updateProviderCredential({ id, credentials, isDefault }) {
|
|
854
|
+
async updateProviderCredential({ id, credentials, isDefault, confirmPrivateOrigin }) {
|
|
431
855
|
const ep = resolveEndpoint('vault.update', { orgId: orgId(), id });
|
|
432
856
|
try {
|
|
433
|
-
const r = await client.put(ep.path, { service: ep.service, body: { credentials, is_default: Boolean(isDefault) }, retries: 0, sensitive: true });
|
|
857
|
+
const r = await client.put(ep.path, { service: ep.service, body: { credentials, is_default: Boolean(isDefault), ...(confirmPrivateOrigin ? { confirm_private_origin: confirmPrivateOrigin } : {}) }, retries: 0, sensitive: true });
|
|
434
858
|
return { ok: Boolean(r && (r.id || r.success || r.credential)), id, message: null };
|
|
435
|
-
} catch (e) { return
|
|
859
|
+
} catch (e) { return vaultFailure(e); }
|
|
860
|
+
},
|
|
861
|
+
/** Z2H: read the instance LAN-target gate -> { ok, enabled } | { ok:false, status, message }. */
|
|
862
|
+
async lanTargets() {
|
|
863
|
+
const ep = resolveEndpoint('admin.instance.lan_targets.get');
|
|
864
|
+
try { const r = await client.get(ep.path, { service: ep.service, retries: 0 }); return { ok: true, enabled: Boolean(r?.enabled) }; }
|
|
865
|
+
catch (e) { return { ok: false, status: e?.status, message: e?.message || 'failed' }; }
|
|
866
|
+
},
|
|
867
|
+
/** Z2H: set the instance LAN-target gate (instance admin only; audited server side). */
|
|
868
|
+
async setLanTargets(on) {
|
|
869
|
+
const ep = resolveEndpoint('admin.instance.lan_targets.set');
|
|
870
|
+
try { const r = await client.put(ep.path, { service: ep.service, body: { enabled: Boolean(on) }, retries: 0 }); return { ok: true, enabled: Boolean(r?.enabled) }; }
|
|
871
|
+
catch (e) { return { ok: false, status: e?.status, message: e?.message || 'failed' }; }
|
|
436
872
|
},
|
|
437
873
|
/** B3: server-side probe of an Ollama URL (GET <url>/api/tags). Nothing stored. */
|
|
438
874
|
async ollamaProbe(baseUrl) {
|
|
@@ -446,6 +882,12 @@ export function createApi({ client, config, auth, reattachBackoff = REATTACH_BAC
|
|
|
446
882
|
try { return await client.get(ep.path, { service: ep.service, retries: 0 }); }
|
|
447
883
|
catch (e) { return { ok: false, status: e?.status, error: e?.code || null, message: e?.message || 'failed' }; }
|
|
448
884
|
},
|
|
885
|
+
/** QUOXON-MODEL: live model list for openai | anthropic -> { ok, models:[{id,family,created}], default_model } | { ok:false, error, message }. */
|
|
886
|
+
async providerModels(provider, { refresh = false } = {}) {
|
|
887
|
+
const ep = resolveEndpoint('chat.providers.models', { provider });
|
|
888
|
+
try { return await client.get(ep.path, { service: ep.service, query: refresh ? { refresh: 1 } : undefined, retries: 0 }); }
|
|
889
|
+
catch (e) { return { ok: false, status: e?.status, error: e?.code || null, message: e?.message || 'failed' }; }
|
|
890
|
+
},
|
|
449
891
|
/** B5: server-side probe of a self-hosted / custom OpenAI-compatible base URL (lmstudio|vllm|custom). Nothing stored. */
|
|
450
892
|
async compatProbe(preset, baseUrl) {
|
|
451
893
|
const ep = resolveEndpoint('chat.providers.compat.probe', { preset });
|
|
@@ -458,16 +900,53 @@ export function createApi({ client, config, auth, reattachBackoff = REATTACH_BAC
|
|
|
458
900
|
try { return await client.get(ep.path, { service: ep.service, retries: 0 }); }
|
|
459
901
|
catch (e) { return { ok: false, status: e?.status, error: e?.code || null, message: e?.message || 'failed' }; }
|
|
460
902
|
},
|
|
903
|
+
/** /vm <verb> ...: the HYPERION Proxmox tools through POST /api/v1/tools/execute. Returns { lines, tone }. */
|
|
904
|
+
async vm(argv) { return runVmCommand({ client, orgId: orgId(), argv }); },
|
|
905
|
+
/** /hyperion: held VMs and who holds them, one line each (read only, GET /api/v1/proxmox/activity). */
|
|
906
|
+
async hyperion() { const data = await fetchActivity(client); return { lines: heldLines(data), tone: data.nodes_error || data.pool_error ? 'warn' : undefined }; },
|
|
461
907
|
approvals: listApprovals,
|
|
908
|
+
/** Org-wide approvals awaiting a human (the dashboard header's server-side count, inbox stats awaiting_human). */
|
|
909
|
+
async approvalStats() {
|
|
910
|
+
const ep = resolveEndpoint('inbox.stats.list', { orgId: orgId() });
|
|
911
|
+
const d = await client.get(ep.path, { service: ep.service });
|
|
912
|
+
return { pending: Number(d?.pending_approvals ?? 0), awaiting: Number(d?.awaiting_human ?? 0) };
|
|
913
|
+
},
|
|
462
914
|
/** GET /api/v1/approvals/:id/result -> { status, result, executed_at } ; null when unknown (404) */
|
|
463
915
|
async approvalResult(approvalId) {
|
|
464
916
|
const ep = resolveEndpoint('v1.approvals.result', { id: approvalId });
|
|
465
917
|
try { return await client.get(ep.path, { service: ep.service, query: { org_id: orgId() || undefined } }); }
|
|
466
918
|
catch (e) { if (e?.status === 404) return null; throw e; }
|
|
467
919
|
},
|
|
468
|
-
|
|
920
|
+
/** 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 });
|
|
923
|
+
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
|
+
if (stepUp && Object.keys(stepUp).length) noteActivity('answer', { quiet: true }); // PRESENCE: answering is the owner being back
|
|
925
|
+
return r;
|
|
926
|
+
},
|
|
927
|
+
async respond(itemId, action, { via = null, stepUp = null, reason = null, bulk = false } = {}) {
|
|
469
928
|
const ep = resolveEndpoint('inbox.respond', { orgId: orgId(), itemId });
|
|
470
|
-
|
|
929
|
+
const tag = via ? `via quoxon ${via}` : 'via quoxon';
|
|
930
|
+
const why = reason ? String(reason).trim().slice(0, 4000) : '';
|
|
931
|
+
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 } : {}) } });
|
|
932
|
+
if (stepUp && Object.keys(stepUp).length) noteActivity('approval', { quiet: true }); // PRESENCE
|
|
933
|
+
return r;
|
|
934
|
+
},
|
|
935
|
+
/** QUOXON-STEPUP: PIN -> short-lived step-up token. The PIN goes straight into the request body; never logged, never stored. */
|
|
936
|
+
async stepUpVerify(pin, { orgId: org = null, scopes = ['approve'] } = {}) {
|
|
937
|
+
const ep = resolveEndpoint('stepup.verify');
|
|
938
|
+
const r = await client.post(ep.path, { service: ep.service, body: { pin, org_id: org || orgId() || undefined, scopes } });
|
|
939
|
+
noteActivity('pin-step-up', { quiet: true }); // PRESENCE: a PIN step-up is a live human
|
|
940
|
+
return r;
|
|
941
|
+
},
|
|
942
|
+
async stepUpStatus() {
|
|
943
|
+
const ep = resolveEndpoint('stepup.status');
|
|
944
|
+
return client.get(ep.path, { service: ep.service });
|
|
945
|
+
},
|
|
946
|
+
/** QUOXON-STEPUP part B: override ONE blocked action. Needs a live override-scoped step-up token. */
|
|
947
|
+
async override({ blockId, blockType, reason, agentId = null, action = null, stepUp = {} }) {
|
|
948
|
+
const ep = resolveEndpoint('safety.override');
|
|
949
|
+
return client.post(ep.path, { service: ep.service, headers: stepUp, body: { block_id: blockId, block_type: blockType, reason, agent_id: agentId || undefined, action: action || undefined } });
|
|
471
950
|
}
|
|
472
951
|
};
|
|
473
952
|
}
|