quoxcli 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +2 -0
- package/bin/quox.js +99 -126
- package/lib/api/endpoints/admin.js +26 -0
- package/lib/api/endpoints/agent-schedules.js +26 -0
- package/lib/api/endpoints/agents.js +7 -0
- package/lib/api/endpoints/assistants.js +12 -0
- package/lib/api/endpoints/away.js +15 -0
- package/lib/api/endpoints/chat.js +7 -0
- package/lib/api/endpoints/contactsq.js +2 -0
- package/lib/api/endpoints/conversations.js +14 -0
- package/lib/api/endpoints/executions.js +13 -0
- package/lib/api/endpoints/fetchAllowlist.js +27 -0
- package/lib/api/endpoints/files.js +14 -0
- package/lib/api/endpoints/hat.js +30 -0
- package/lib/api/endpoints/inbox.js +20 -0
- package/lib/api/endpoints/index.js +14 -0
- package/lib/api/endpoints/memory.js +19 -0
- package/lib/api/endpoints/mind.js +13 -0
- package/lib/api/endpoints/mon.js +6 -0
- package/lib/api/endpoints/objectives.js +6 -0
- package/lib/api/endpoints/plugins.js +18 -0
- package/lib/api/endpoints/presence.js +33 -0
- package/lib/api/endpoints/quoxplan.js +7 -0
- package/lib/api/endpoints/quoxseo.js +6 -0
- package/lib/api/endpoints/router.js +17 -0
- package/lib/api/endpoints/safety.js +7 -0
- package/lib/api/endpoints/skills.js +20 -0
- package/lib/api/endpoints/stepup.js +20 -0
- package/lib/api/endpoints/teams.js +2 -2
- package/lib/api/endpoints/triggers.js +26 -0
- package/lib/api/endpoints/v1.js +86 -0
- package/lib/api/endpoints/ward.js +7 -0
- package/lib/api/endpoints/workflows.js +7 -0
- package/lib/api/endpoints/x.js +8 -0
- package/lib/commands/actions.js +61 -0
- package/lib/commands/admin.js +16 -0
- package/lib/commands/agent.js +163 -27
- package/lib/commands/agentAttach.js +94 -6
- package/lib/commands/approvals.js +73 -1
- package/lib/commands/aside.js +24 -0
- package/lib/commands/ask.js +144 -0
- package/lib/commands/assistant.js +59 -4
- package/lib/commands/auth.js +8 -2
- package/lib/commands/backup.js +8 -4
- package/lib/commands/brain.js +105 -14
- package/lib/commands/build.js +19 -5
- package/lib/commands/chat.js +57 -2
- package/lib/commands/claude.js +34 -8
- package/lib/commands/contacts.js +49 -0
- package/lib/commands/conversation.js +35 -4
- package/lib/commands/dashboard.js +43 -0
- package/lib/commands/department.js +28 -7
- package/lib/commands/devResident.js +15 -3
- package/lib/commands/devSupervisor.js +13 -1
- package/lib/commands/discord.js +75 -39
- package/lib/commands/done.js +98 -28
- package/lib/commands/engagement.js +17 -6
- package/lib/commands/fetchAllowlist.js +71 -0
- package/lib/commands/file.js +38 -1
- package/lib/commands/fleet.js +311 -4
- package/lib/commands/governance.js +12 -3
- package/lib/commands/gripe.js +44 -13
- package/lib/commands/hat.js +178 -9
- package/lib/commands/helpers.js +18 -1
- package/lib/commands/hyperion.js +58 -0
- package/lib/commands/inbox.js +74 -23
- package/lib/commands/licence.js +14 -155
- package/lib/commands/lucid.js +10 -4
- package/lib/commands/memory.js +85 -8
- package/lib/commands/mind.js +71 -0
- package/lib/commands/mon.js +69 -2
- package/lib/commands/monitoring.js +1 -1
- package/lib/commands/motion.js +2 -2
- package/lib/commands/nucleus.js +48 -17
- package/lib/commands/objective.js +11 -0
- package/lib/commands/offload.js +89 -0
- package/lib/commands/onboard.js +2 -1
- package/lib/commands/org.js +11 -1
- package/lib/commands/peek.js +28 -0
- package/lib/commands/plan.js +38 -0
- package/lib/commands/plugin.js +110 -0
- package/lib/commands/presence.js +89 -0
- package/lib/commands/provider.js +159 -0
- package/lib/commands/qlarity.js +19 -3
- package/lib/commands/quoxchat.js +13 -4
- package/lib/commands/quoxseo.js +38 -0
- package/lib/commands/reach.js +2 -1
- package/lib/commands/router.js +77 -0
- package/lib/commands/rows.js +358 -0
- package/lib/commands/run.js +64 -9
- package/lib/commands/server.js +292 -0
- package/lib/commands/setup.js +33 -7
- package/lib/commands/skills.js +48 -5
- package/lib/commands/slot.js +260 -7
- package/lib/commands/status.js +1 -1
- package/lib/commands/team.js +3 -1
- package/lib/commands/tips.js +23 -0
- package/lib/commands/tool.js +133 -45
- package/lib/commands/trace.js +12 -7
- package/lib/commands/trigger.js +95 -0
- package/lib/commands/update.js +46 -0
- package/lib/commands/vault.js +31 -8
- package/lib/commands/vm.js +237 -0
- package/lib/commands/voice.js +34 -3
- package/lib/commands/volt.js +45 -3
- package/lib/commands/walkthrough.js +32 -0
- package/lib/commands/watch.js +73 -21
- package/lib/commands/welcome.js +14 -0
- package/lib/commands/workflow.js +189 -32
- package/lib/commands/x.js +13 -4
- package/lib/core/actingAs.js +2 -2
- package/lib/core/activeSummary.js +186 -0
- package/lib/core/aside.js +54 -0
- package/lib/core/askDecision.js +60 -0
- package/lib/core/auth.js +164 -13
- package/lib/core/callerSource.js +1 -1
- package/lib/core/cliBuild.js +62 -0
- package/lib/core/client.js +144 -35
- package/lib/core/commandRegistrars.js +121 -0
- package/lib/core/config.js +142 -24
- package/lib/core/customHats.js +168 -0
- package/lib/core/doneEngineFetch.js +8 -0
- package/lib/core/errors.js +329 -20
- package/lib/core/global-flags.js +105 -5
- package/lib/core/hyperion.js +102 -0
- package/lib/core/inboxResolution.js +65 -0
- package/lib/core/lazyCommands.js +4 -1
- package/lib/core/offload.js +504 -0
- package/lib/core/org-override.js +1 -1
- package/lib/core/orgResolve.js +70 -0
- package/lib/core/output.js +4 -4
- package/lib/core/peek.js +126 -0
- package/lib/core/profileUsers.js +51 -0
- package/lib/core/promptCancel.js +18 -0
- package/lib/core/repoPath.js +11 -0
- package/lib/core/residentSeed.js +3 -1
- package/lib/core/rootOptions.js +39 -0
- package/lib/core/router.js +168 -0
- package/lib/core/selfUpdate.js +188 -0
- package/lib/core/sessionsOverview.js +64 -0
- package/lib/core/slotKeeper.js +248 -3
- package/lib/core/slotWake.js +120 -0
- package/lib/core/supervisorEngine.js +71 -2
- package/lib/core/version.js +1 -1
- package/lib/core/vm.js +361 -0
- package/lib/core/vmTui.js +86 -0
- package/lib/core/walkthrough.js +35 -0
- package/lib/hyperion/deck.js +29 -0
- package/lib/lucid/LucidCard.jsx +68 -0
- package/lib/lucid/LucidIntro.jsx +45 -58
- package/lib/lucid/cards.js +86 -0
- package/lib/lucid/markdown.js +81 -0
- package/lib/lucid/motion.js +56 -0
- package/lib/quoxon/activity.js +14 -2
- package/lib/quoxon/aocl.js +117 -0
- package/lib/quoxon/api.js +504 -25
- package/lib/quoxon/app.js +3000 -230
- package/lib/quoxon/approvalFacts.js +94 -0
- package/lib/quoxon/approvalsView.js +325 -0
- package/lib/quoxon/asideView.js +40 -0
- package/lib/quoxon/ask.js +121 -0
- package/lib/quoxon/away.js +52 -0
- package/lib/quoxon/bang.js +224 -0
- package/lib/quoxon/bulkRisk.js +17 -0
- package/lib/quoxon/capabilities.js +13 -6
- package/lib/quoxon/cells.js +53 -0
- package/lib/quoxon/commandShort.js +112 -0
- package/lib/quoxon/commands.js +363 -38
- package/lib/quoxon/connect.js +210 -23
- package/lib/quoxon/connectCard.js +19 -2
- package/lib/quoxon/consent.js +86 -0
- package/lib/quoxon/copy.js +4 -3
- package/lib/quoxon/countdownLine.js +11 -0
- package/lib/quoxon/demo.autoplay.js +29 -0
- package/lib/quoxon/demo.fixtures.js +1 -1
- package/lib/quoxon/demo.js +44 -3
- package/lib/quoxon/devObserver.js +20 -0
- package/lib/quoxon/dividerAocl.js +191 -0
- package/lib/quoxon/dividerRule.js +57 -0
- package/lib/quoxon/dragonModes.js +150 -0
- package/lib/quoxon/dragonView.js +41 -0
- package/lib/quoxon/earlyInput.js +40 -0
- package/lib/quoxon/echo.js +15 -0
- package/lib/quoxon/egg.js +2 -2
- package/lib/quoxon/events.js +244 -25
- package/lib/quoxon/feedback.js +50 -0
- package/lib/quoxon/gallery.js +21 -7
- package/lib/quoxon/handoffOffer.js +78 -0
- package/lib/quoxon/hatSwitch.js +37 -0
- package/lib/quoxon/headless.js +161 -0
- package/lib/quoxon/hostUrls.js +41 -0
- package/lib/quoxon/indicator.js +6 -2
- package/lib/quoxon/intent.js +12 -1
- package/lib/quoxon/keys.js +49 -6
- package/lib/quoxon/launcher.js +103 -0
- package/lib/quoxon/main.js +92 -160
- package/lib/quoxon/markdown.js +45 -21
- package/lib/quoxon/markdownSettle.js +19 -0
- package/lib/quoxon/matrixDomains.json +46 -0
- package/lib/quoxon/mentions.js +90 -0
- package/lib/quoxon/mind.js +103 -0
- package/lib/quoxon/mode.js +46 -0
- package/lib/quoxon/model.js +132 -26
- package/lib/quoxon/modelPrefs.js +345 -0
- package/lib/quoxon/motion/clock.js +3 -2
- package/lib/quoxon/motion/level.js +91 -0
- package/lib/quoxon/mouse.js +211 -0
- package/lib/quoxon/noai.js +57 -0
- package/lib/quoxon/objects/away.js +72 -0
- package/lib/quoxon/objects/fetch.js +17 -5
- package/lib/quoxon/objects/fleet.js +149 -0
- package/lib/quoxon/objects/health.js +186 -0
- package/lib/quoxon/objects/index.js +24 -3
- package/lib/quoxon/objects/mirror.js +137 -0
- package/lib/quoxon/objects/peek.js +17 -0
- package/lib/quoxon/objects/prove.js +130 -0
- package/lib/quoxon/objects/replay.js +125 -0
- package/lib/quoxon/objects/sitrep.js +58 -0
- package/lib/quoxon/objects/view.js +11 -0
- package/lib/quoxon/onboarding/copy.js +32 -4
- package/lib/quoxon/onboarding/credential.js +17 -4
- package/lib/quoxon/onboarding/dockerOffer.js +45 -0
- package/lib/quoxon/onboarding/failureCause.js +33 -0
- package/lib/quoxon/onboarding/finish.js +15 -0
- package/lib/quoxon/onboarding/index.js +34 -7
- package/lib/quoxon/onboarding/installRunner.js +64 -5
- package/lib/quoxon/onboarding/preflight.js +17 -3
- package/lib/quoxon/onboarding/probe.js +65 -0
- package/lib/quoxon/onboarding/view.js +76 -19
- package/lib/quoxon/orgLabel.js +10 -0
- package/lib/quoxon/orgSwitch.js +33 -0
- package/lib/quoxon/ownBroadcast.js +22 -0
- package/lib/quoxon/palette.js +43 -0
- package/lib/quoxon/print.js +9 -5
- package/lib/quoxon/profileSwitch.js +53 -0
- package/lib/quoxon/promptHistory.js +41 -0
- package/lib/quoxon/providers.js +48 -0
- package/lib/quoxon/qart.js +20 -51
- package/lib/quoxon/qdragon.js +410 -0
- package/lib/quoxon/qdragonFly.js +61 -0
- package/lib/quoxon/qdragonScenes.js +282 -0
- package/lib/quoxon/qeyes.js +213 -0
- package/lib/quoxon/qmotion.js +58 -37
- package/lib/quoxon/render.js +638 -119
- package/lib/quoxon/resend.js +72 -0
- package/lib/quoxon/roster.js +95 -9
- package/lib/quoxon/route.js +6 -5
- package/lib/quoxon/rows.js +711 -0
- package/lib/quoxon/rowsStore.js +80 -0
- package/lib/quoxon/screenContext.js +150 -0
- package/lib/quoxon/screenFrames.js +254 -0
- package/lib/quoxon/session.js +172 -2
- package/lib/quoxon/sessionDead.js +17 -0
- package/lib/quoxon/signin.js +125 -0
- package/lib/quoxon/slash/agents.js +112 -0
- package/lib/quoxon/slash/argSources.js +65 -0
- package/lib/quoxon/slash/args.js +65 -0
- package/lib/quoxon/slash/automate.js +89 -0
- package/lib/quoxon/slash/broadcast.js +140 -0
- package/lib/quoxon/slash/broadcastRun.js +108 -0
- package/lib/quoxon/slash/card.js +30 -0
- package/lib/quoxon/slash/challenge.js +114 -0
- package/lib/quoxon/slash/cliCommand.js +53 -0
- package/lib/quoxon/slash/cliLeaves.json +948 -0
- package/lib/quoxon/slash/compact.js +1 -1
- package/lib/quoxon/slash/confirm.js +30 -0
- package/lib/quoxon/slash/crystallize.js +69 -0
- package/lib/quoxon/slash/dashboard.js +136 -0
- package/lib/quoxon/slash/draft.js +54 -0
- package/lib/quoxon/slash/hatWalk.js +206 -0
- package/lib/quoxon/slash/inverses.js +77 -0
- package/lib/quoxon/slash/killer.js +24 -0
- package/lib/quoxon/slash/killerApi.js +16 -0
- package/lib/quoxon/slash/ledger.js +13 -8
- package/lib/quoxon/slash/listView.js +3 -1
- package/lib/quoxon/slash/modelCmd.js +261 -0
- package/lib/quoxon/slash/palette.js +21 -7
- package/lib/quoxon/slash/paletteController.js +117 -7
- package/lib/quoxon/slash/panels.js +21 -6
- package/lib/quoxon/slash/picker.js +20 -2
- package/lib/quoxon/slash/restart.js +71 -13
- package/lib/quoxon/slash/rowCmd.js +157 -0
- package/lib/quoxon/slash/rowWalk.js +175 -0
- package/lib/quoxon/slash/settings.js +16 -6
- package/lib/quoxon/slash/spawn.js +177 -0
- package/lib/quoxon/slash/stepUp.js +274 -0
- package/lib/quoxon/slash/stepUpCard.js +60 -0
- package/lib/quoxon/slash/stepUpCmds.js +78 -0
- package/lib/quoxon/slash/undoApi.js +38 -0
- package/lib/quoxon/slash/undoCmd.js +49 -0
- package/lib/quoxon/slash/undoWish.js +15 -0
- package/lib/quoxon/slash/watch.js +164 -0
- package/lib/quoxon/slash/wish.js +23 -0
- package/lib/quoxon/slash/wishCmd.js +32 -0
- package/lib/quoxon/slotChat.js +300 -0
- package/lib/quoxon/slotFormat.js +195 -0
- package/lib/quoxon/slotRun.js +131 -0
- package/lib/quoxon/sound/mapping.js +2 -1
- package/lib/quoxon/stray.js +22 -0
- package/lib/quoxon/suggestCommand.js +59 -0
- package/lib/quoxon/term.js +46 -11
- package/lib/quoxon/textBuffer.js +176 -2
- package/lib/quoxon/theme.js +58 -25
- package/lib/quoxon/tips.js +433 -0
- package/lib/quoxon/toolError.js +41 -0
- package/lib/quoxon/transcript.js +42 -4
- package/lib/quoxon/web/qface.js +18 -10
- package/lib/quoxon/welcome.js +1 -1
- package/lib/quoxon/whatsnew.js +136 -0
- package/lib/quoxon/whatsnew.jsonl +89 -0
- package/lib/quoxon/windowOrg.js +113 -0
- package/lib/server/diag.js +129 -0
- package/lib/server/doctor.js +71 -0
- package/lib/server/install.js +149 -0
- package/lib/server/pluginEnable.js +161 -0
- package/lib/server/skew.js +56 -0
- package/lib/server/snapshot.js +131 -0
- package/lib/server/status.js +128 -0
- package/lib/server/sys.js +65 -0
- package/lib/server/uninstall.js +63 -0
- package/lib/server/update.js +234 -0
- package/lib/toolOutputShape.js +315 -0
- package/lib/tui/views/HyperionDeck.jsx +79 -0
- package/lib/tui/views/LucidDeck.jsx +209 -217
- package/package.json +4 -1
- package/lib/core/websiteSession.js +0 -148
package/lib/commands/plan.js
CHANGED
|
@@ -137,6 +137,23 @@ function renderRoadmapsList(roadmaps) {
|
|
|
137
137
|
return lines;
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
+
function renderSearchResults(data) {
|
|
141
|
+
const results = data.results || [];
|
|
142
|
+
if (results.length === 0) return [chalk.dim(` No streams match "${data.q}".`)];
|
|
143
|
+
const lines = [''];
|
|
144
|
+
for (const r of results) {
|
|
145
|
+
const g = statusGlyph(r.status);
|
|
146
|
+
const where = [...new Set((r.matches || []).map((m) => m.where))].join(', ');
|
|
147
|
+
const first = (r.matches || []).find((m) => m.snippet) || {};
|
|
148
|
+
lines.push(
|
|
149
|
+
` ${g.color(g.glyph)} ${pad(truncate(r.id, ID_WIDTH), ID_WIDTH)} ${pad(truncate(r.name, NAME_WIDTH), NAME_WIDTH)} `
|
|
150
|
+
+ `${pad(r.status, 15)} ${pad(where, 18)} ${chalk.dim(truncate(first.snippet || '', 60))}`
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
lines.push('', chalk.dim(` ${results.length} stream${results.length === 1 ? '' : 's'} matched "${data.q}"`));
|
|
154
|
+
return lines;
|
|
155
|
+
}
|
|
156
|
+
|
|
140
157
|
function streamLine(s, { indent = 0, showReasons = false } = {}) {
|
|
141
158
|
const prefix = indent > 0 ? ' '.repeat(indent) + '└ ' : ' ';
|
|
142
159
|
const g = statusGlyph(s.status);
|
|
@@ -546,6 +563,27 @@ export function registerPlanCommands(program, ctx) {
|
|
|
546
563
|
printSuccess(`Created roadmap ${created.id || options.id} for org ${orgId}`);
|
|
547
564
|
});
|
|
548
565
|
|
|
566
|
+
// ── search ───────────────────────────────────────────────────────────
|
|
567
|
+
|
|
568
|
+
plan
|
|
569
|
+
.command('search <text...>')
|
|
570
|
+
.description('Search streams by text (id, name, notes, plan, phase titles)')
|
|
571
|
+
.option('--roadmap <id>', 'limit to one roadmap')
|
|
572
|
+
.option('--status <status>', `limit to one status (${VALID_STATUSES.join(', ')})`)
|
|
573
|
+
.action(async (textParts, options) => {
|
|
574
|
+
auth.requireAuth();
|
|
575
|
+
if (options.status && !VALID_STATUSES.includes(options.status)) {
|
|
576
|
+
throw new Error(`invalid --status "${options.status}" (expected ${VALID_STATUSES.join(', ')})`);
|
|
577
|
+
}
|
|
578
|
+
const query = { q: textParts.join(' ') };
|
|
579
|
+
if (options.roadmap) query.roadmap = options.roadmap;
|
|
580
|
+
if (options.status) query.status = options.status;
|
|
581
|
+
const ep = resolveEndpoint('quoxplan.search');
|
|
582
|
+
const data = await client.get(ep.path, { service: ep.service, query });
|
|
583
|
+
if (isJsonOutput(program)) return printJson(data);
|
|
584
|
+
console.log(renderSearchResults(data).join('\n'));
|
|
585
|
+
});
|
|
586
|
+
|
|
549
587
|
// ── list ─────────────────────────────────────────────────────────────
|
|
550
588
|
|
|
551
589
|
plan
|
package/lib/commands/plugin.js
CHANGED
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
* Plugin commands: org-scoped plugin install registry (PD-1..PD-8)
|
|
3
3
|
*
|
|
4
4
|
* quox plugin list|get|install|update|uninstall|mine
|
|
5
|
+
* quox plugin enable <id> (PLUGIN-IMG #1012: prebuilt private image, licence first)
|
|
5
6
|
* quox plugin data list|get|set|delete
|
|
6
7
|
* quox plugin secrets list|set|reveal|delete
|
|
7
8
|
* quox plugin assign list|add|remove
|
|
8
9
|
* quox plugin trusted-keys list|add|remove
|
|
10
|
+
* quox plugin revocations list|add|remove
|
|
9
11
|
*
|
|
10
12
|
* Backend: services/auth/routes/plugins.js, mounted org-scoped at
|
|
11
13
|
* `/api/orgs/:orgId/...` (server.js). Install/update/uninstall and all
|
|
@@ -18,6 +20,9 @@ import { readFileSync } from 'node:fs';
|
|
|
18
20
|
import { formatOutput, formatDetail, printSuccess, printWarning, printInfo } from '../core/output.js';
|
|
19
21
|
import { resolveOrgId, parseJSON } from './helpers.js';
|
|
20
22
|
import { resolveEndpoint } from '../api/endpoints.js';
|
|
23
|
+
import { enablePlugin, entitlementFrom } from '../server/pluginEnable.js';
|
|
24
|
+
import { resolveInstall, readEnvKeys } from '../server/install.js';
|
|
25
|
+
import { createSys } from '../server/sys.js';
|
|
21
26
|
|
|
22
27
|
/** Read + JSON.parse a file, wrapping errors with the file path for context. */
|
|
23
28
|
function readJsonFile(filePath, label) {
|
|
@@ -113,6 +118,56 @@ export function registerPluginCommands(program, ctx) {
|
|
|
113
118
|
}, { title: `Licence: ${pluginId}` });
|
|
114
119
|
});
|
|
115
120
|
|
|
121
|
+
// ── plugin enable (PLUGIN-IMG #1012) ────────────────────────────────────────
|
|
122
|
+
// Starts a licensed plugin from its prebuilt private image on this
|
|
123
|
+
// self-hosted install. Licence first: unlicensed means nothing runs.
|
|
124
|
+
|
|
125
|
+
plugin
|
|
126
|
+
.command('enable')
|
|
127
|
+
.description('Start a licensed plugin from its prebuilt image on this self-hosted install (QuoxPlan for now). Unlicensed: refused, nothing pulled.')
|
|
128
|
+
.argument('<pluginId>', 'plugin id, e.g. quoxplan')
|
|
129
|
+
.option('--dir <path>', 'the install directory (default: the one Getting started recorded)')
|
|
130
|
+
.action(async (pluginId, opts) => {
|
|
131
|
+
auth.requireAuth();
|
|
132
|
+
const deps = ctx.serverDeps || {};
|
|
133
|
+
const env = deps.env || process.env;
|
|
134
|
+
const sys = deps.sys || createSys();
|
|
135
|
+
const checkEntitlement = async (id) => {
|
|
136
|
+
const ep = resolveEndpoint('setup.licensing.entitlements.list');
|
|
137
|
+
const data = await client.get(ep.path, { service: ep.service });
|
|
138
|
+
return entitlementFrom(data, id);
|
|
139
|
+
};
|
|
140
|
+
// Licence before anything that touches the box: an unlicensed enable must not even look for the install.
|
|
141
|
+
let ent;
|
|
142
|
+
try { ent = await checkEntitlement(pluginId); } catch (err) { ent = err; }
|
|
143
|
+
const resolveFirst = !(ent instanceof Error) && ent && ent.entitled;
|
|
144
|
+
let install = null;
|
|
145
|
+
if (resolveFirst) {
|
|
146
|
+
const i = await resolveInstall({ dir: opts.dir, env, sys, journey: deps.journey });
|
|
147
|
+
if (i.error) { const e = new Error(i.error); e.exitCode = 2; throw e; }
|
|
148
|
+
install = i;
|
|
149
|
+
}
|
|
150
|
+
const tag = install ? (env.QUOX_IMAGE_TAG || readEnvKeys(install.repoDir, ['QUOX_IMAGE_TAG']).QUOX_IMAGE_TAG || '') : '';
|
|
151
|
+
const result = await enablePlugin({
|
|
152
|
+
pluginId,
|
|
153
|
+
checkEntitlement: async () => { if (ent instanceof Error) throw ent; return ent; },
|
|
154
|
+
install: install || { repoDir: '' },
|
|
155
|
+
sys,
|
|
156
|
+
tag,
|
|
157
|
+
...(deps.getPullCredential ? { getPullCredential: deps.getPullCredential } : {}),
|
|
158
|
+
...(deps.sleep ? { sleep: deps.sleep } : {}),
|
|
159
|
+
log: (l) => printInfo(l),
|
|
160
|
+
});
|
|
161
|
+
if (program.opts().output === 'json') {
|
|
162
|
+
console.log(JSON.stringify({ plugin: pluginId, ok: result.ok, stage: result.stage, message: result.message }, null, 2));
|
|
163
|
+
} else if (result.ok) {
|
|
164
|
+
printSuccess(result.message);
|
|
165
|
+
} else {
|
|
166
|
+
printWarning(result.message);
|
|
167
|
+
}
|
|
168
|
+
if (!result.ok) process.exitCode = result.stage === 'licence' ? 3 : 1;
|
|
169
|
+
});
|
|
170
|
+
|
|
116
171
|
// ── plugin list ─────────────────────────────────────────────────────────────
|
|
117
172
|
|
|
118
173
|
plugin
|
|
@@ -536,4 +591,59 @@ export function registerPluginCommands(program, ctx) {
|
|
|
536
591
|
await client.delete(ep.path, reqOpts);
|
|
537
592
|
printSuccess('Trusted key removed.');
|
|
538
593
|
});
|
|
594
|
+
|
|
595
|
+
// ── plugin revocations (SDK-TEAM W5) ────────────────────────────────────────
|
|
596
|
+
|
|
597
|
+
const revocations = plugin
|
|
598
|
+
.command('revocations')
|
|
599
|
+
.description('Revoke a plugin signer key or one bundle digest for this org (admin only). A revoked plugin cannot be installed and stops loading on the next page load.');
|
|
600
|
+
|
|
601
|
+
revocations
|
|
602
|
+
.command('list')
|
|
603
|
+
.description('List revocations')
|
|
604
|
+
.action(async () => {
|
|
605
|
+
auth.requireAuth();
|
|
606
|
+
const orgId = resolveOrgId(auth, program);
|
|
607
|
+
const ep = resolveEndpoint('plugins.revocations.list', { orgId });
|
|
608
|
+
const result = await client.get(ep.path, { service: ep.service });
|
|
609
|
+
const rows = result.revocations || result || [];
|
|
610
|
+
formatOutput({
|
|
611
|
+
columns: ['id', 'kind', 'value', 'reason', 'revoked_by', 'revoked_at'],
|
|
612
|
+
headers: ['ID', 'Kind', 'Value', 'Reason', 'Revoked By', 'Revoked'],
|
|
613
|
+
rows,
|
|
614
|
+
}, { format: program.opts().output, emptyMessage: 'No revocations.' });
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
revocations
|
|
618
|
+
.command('add')
|
|
619
|
+
.description('Revoke a signer key or a bundle digest')
|
|
620
|
+
.option('--signer-key <hex>', 'raw Ed25519 public key (64 hex), as in signature.json public_key_hex')
|
|
621
|
+
.option('--bundle-digest <hex>', 'sha256 of plugin.esm.js (64 hex), as shown by quox plugin list')
|
|
622
|
+
.option('--reason <text>', 'why (kept in the audit trail)')
|
|
623
|
+
.action(async (options) => {
|
|
624
|
+
auth.requireAuth();
|
|
625
|
+
if (!!options.signerKey === !!options.bundleDigest) {
|
|
626
|
+
throw new Error('Specify exactly one of --signer-key or --bundle-digest.');
|
|
627
|
+
}
|
|
628
|
+
const orgId = resolveOrgId(auth, program);
|
|
629
|
+
const body = options.signerKey
|
|
630
|
+
? { kind: 'signer_key', value: options.signerKey, reason: options.reason }
|
|
631
|
+
: { kind: 'bundle_digest', value: options.bundleDigest, reason: options.reason };
|
|
632
|
+
const ep = resolveEndpoint('plugins.revocations.create', { orgId });
|
|
633
|
+
const result = await client.post(ep.path, { service: ep.service, body });
|
|
634
|
+
printSuccess('Revoked.');
|
|
635
|
+
formatDetail(result.revocation || result, { format: program.opts().output });
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
revocations
|
|
639
|
+
.command('remove')
|
|
640
|
+
.description('Lift a revocation')
|
|
641
|
+
.argument('<revocationId>', 'revocation ID')
|
|
642
|
+
.action(async (revocationId) => {
|
|
643
|
+
auth.requireAuth();
|
|
644
|
+
const orgId = resolveOrgId(auth, program);
|
|
645
|
+
const ep = resolveEndpoint('plugins.revocations.delete', { orgId, revocationId });
|
|
646
|
+
await client.delete(ep.path, { service: ep.service });
|
|
647
|
+
printSuccess('Revocation lifted.');
|
|
648
|
+
});
|
|
539
649
|
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `quox presence [who]` / `quox presence set present|away|sleeping`: PRESENCE (owner 2026-10-16).
|
|
3
|
+
* A human says they are not at their terminal (/afk, /sleep in quox do the same). Sessions and agents
|
|
4
|
+
* read it so they stop waiting on approvals, which still queue normally: nothing is ever auto-approved.
|
|
5
|
+
* Collector routes GET/PUT /api/v1/presence; the org is the caller's, only a human login sets its own.
|
|
6
|
+
*/
|
|
7
|
+
import { resolveEndpoint } from '../api/endpoints.js';
|
|
8
|
+
import { resolveOrgId } from './helpers.js';
|
|
9
|
+
|
|
10
|
+
const STATES = ['present', 'away', 'sleeping'];
|
|
11
|
+
|
|
12
|
+
function stamp(iso) {
|
|
13
|
+
const d = new Date(iso);
|
|
14
|
+
return Number.isNaN(d.getTime()) ? String(iso || '') : `${d.toISOString().slice(0, 10)} ${d.toISOString().slice(11, 16)} UTC`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** One plain line for a presence record. */
|
|
18
|
+
export function presenceText(p) {
|
|
19
|
+
if (!p || !p.away) return `${p?.who || 'they'}: present`;
|
|
20
|
+
const note = p.note ? `, "${p.note}"` : '';
|
|
21
|
+
return `${p.who}: ${p.auto ? 'away (idle)' : p.state} since ${stamp(p.since)}${note} (approvals will queue)`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** The fields an approval request reports, from GET /api/v1/presence. */
|
|
25
|
+
export function approvalPresenceFields(list) {
|
|
26
|
+
const away = (list && Array.isArray(list.presence) ? list.presence : []).filter((p) => p && p.away);
|
|
27
|
+
if (!away.length) return { owner_away: false };
|
|
28
|
+
const p = away.find((x) => x.who === 'owner') || away[0];
|
|
29
|
+
return { owner_away: true, owner_away_since: p.since, owner_presence: p.state, owner_away_notice: list.notice || presenceText(p) };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Read the org's presence; null on any failure (a presence read never fails the caller). */
|
|
33
|
+
export async function readPresence(client, orgId) {
|
|
34
|
+
try {
|
|
35
|
+
const ep = resolveEndpoint('presence.list');
|
|
36
|
+
return await client.get(ep.path, { service: ep.service, query: orgId ? { org_id: orgId } : undefined, retries: 0, timeout: 5000 });
|
|
37
|
+
} catch { return null; }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function registerPresenceCommands(program, { client, auth }) {
|
|
41
|
+
const pr = program
|
|
42
|
+
.command('presence')
|
|
43
|
+
.description('Is a person at their terminal? With no name: everyone with presence on record. When someone is away, approvals queue; work on something else')
|
|
44
|
+
.argument('[who]', 'user id, email, email name, "owner" or "me"')
|
|
45
|
+
.action(async (who) => {
|
|
46
|
+
auth.requireAuth();
|
|
47
|
+
const orgId = resolveOrgId(auth, program);
|
|
48
|
+
const json = program.opts().output === 'json';
|
|
49
|
+
if (who) {
|
|
50
|
+
const ep = resolveEndpoint('presence.get', { who });
|
|
51
|
+
const d = await client.get(ep.path, { service: ep.service, query: { org_id: orgId } });
|
|
52
|
+
if (json) { console.log(JSON.stringify(d, null, 2)); return; }
|
|
53
|
+
console.log(presenceText(d.presence));
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const ep = resolveEndpoint('presence.list');
|
|
57
|
+
const d = await client.get(ep.path, { service: ep.service, query: { org_id: orgId } });
|
|
58
|
+
if (json) { console.log(JSON.stringify(d, null, 2)); return; }
|
|
59
|
+
const list = Array.isArray(d.presence) ? d.presence : [];
|
|
60
|
+
if (!list.some((p) => p.away)) console.log('Everyone is present: nobody has marked themselves away.');
|
|
61
|
+
for (const p of list) console.log(presenceText(p));
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
pr
|
|
65
|
+
.command('set')
|
|
66
|
+
.description('Say where you are: present, away or sleeping (a human login only). Same as /afk, /sleep, /back in quox')
|
|
67
|
+
.argument('<state>', 'present | away | sleeping')
|
|
68
|
+
.option('--note <text>', 'a short note sessions will see ("school run")')
|
|
69
|
+
.option('--slots <list>', 'your own window slots, comma separated (hitl and triage-q always count)')
|
|
70
|
+
.option('--auto-away <minutes>', 'read as away after this many idle minutes; "off" to disable (default off)')
|
|
71
|
+
.action(async (state, options) => {
|
|
72
|
+
if (!STATES.includes(state)) throw new Error('state must be present, away or sleeping');
|
|
73
|
+
auth.requireAuth();
|
|
74
|
+
const orgId = resolveOrgId(auth, program);
|
|
75
|
+
const body = { state };
|
|
76
|
+
if (options.note !== undefined) body.note = options.note;
|
|
77
|
+
if (options.slots) body.slots = String(options.slots).split(',').map((s) => s.trim()).filter(Boolean);
|
|
78
|
+
if (options.autoAway !== undefined) {
|
|
79
|
+
const off = /^(off|0|no)$/i.test(String(options.autoAway));
|
|
80
|
+
const n = Number(options.autoAway);
|
|
81
|
+
if (!off && (!Number.isInteger(n) || n < 1 || n > 1440)) throw new Error('--auto-away is minutes (1 to 1440) or off');
|
|
82
|
+
body.auto_away_minutes = off ? 0 : n;
|
|
83
|
+
}
|
|
84
|
+
const ep = resolveEndpoint('presence.set');
|
|
85
|
+
const d = await client.put(ep.path, { service: ep.service, body, query: { org_id: orgId } });
|
|
86
|
+
if (program.opts().output === 'json') { console.log(JSON.stringify(d, null, 2)); return; }
|
|
87
|
+
console.log(presenceText(d.presence));
|
|
88
|
+
});
|
|
89
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* quox provider list | connect <kind> | test [<kind>] (quox#1281, CLI verb first)
|
|
3
|
+
*
|
|
4
|
+
* The SAME backend calls as the TUI's /connect: cli/lib/quoxon/connect.js flows (connectFlow, connectOllamaFlow,
|
|
5
|
+
* connectCompatFlow, connectionLines) over cli/lib/quoxon/api.js createApi (the auth vault create/update with
|
|
6
|
+
* confirm_private_origin, the server-side key test and Ollama / OpenAI-compatible probes, capabilities, consent).
|
|
7
|
+
* Nothing here talks to the backend on its own, so the TUI and the CLI cannot drift. The vault routes audit every
|
|
8
|
+
* write server side (integration.private_origin for a private address), org-scoped by the caller's session.
|
|
9
|
+
*
|
|
10
|
+
* Secrets: a key is never an argv flag or argument. It arrives on stdin (--key-from-stdin) or from a hidden prompt,
|
|
11
|
+
* is sent once to the server test and the vault, and is never printed (connectFlow scrubs it from any message).
|
|
12
|
+
*/
|
|
13
|
+
import { formatDetail, printInfo } from '../core/output.js';
|
|
14
|
+
import { resolveOrgId } from './helpers.js';
|
|
15
|
+
import { resolveEndpoint } from '../api/endpoints.js';
|
|
16
|
+
import { createApi, isInteractiveHuman } from '../quoxon/api.js';
|
|
17
|
+
import { PROVIDERS, providerFor, looksLikeKey, connectionLines, connectFlow, connectOllamaFlow, connectCompatFlow } from '../quoxon/connect.js';
|
|
18
|
+
import { needsConsent, consentCovers, connectConsentNote, providerLabel } from '../quoxon/consent.js';
|
|
19
|
+
import { readStdin } from '../quoxon/print.js';
|
|
20
|
+
|
|
21
|
+
const KINDS = Object.keys(PROVIDERS);
|
|
22
|
+
const SAFE_ROW = ['id', 'integration_type', 'label', 'is_default', 'test_status', 'last_tested_at', 'created_at'];
|
|
23
|
+
const CONTAINER_NOTE = 'Quox runs in containers: 127.0.0.1 or localhost means the Quox container, not your machine. Use the host\'s LAN IP, e.g. http://192.0.2.10:11434';
|
|
24
|
+
|
|
25
|
+
function defaultIO() {
|
|
26
|
+
return {
|
|
27
|
+
isTTY: () => isInteractiveHuman(),
|
|
28
|
+
readStdin: () => readStdin(),
|
|
29
|
+
promptSecret: async (message) => {
|
|
30
|
+
const { default: inquirer } = await import('inquirer');
|
|
31
|
+
const a = await inquirer.prompt([{ type: 'password', name: 'v', message, mask: '*' }]);
|
|
32
|
+
return a.v;
|
|
33
|
+
},
|
|
34
|
+
confirm: async (message) => {
|
|
35
|
+
const { default: inquirer } = await import('inquirer');
|
|
36
|
+
const a = await inquirer.prompt([{ type: 'confirm', name: 'v', message, default: false }]);
|
|
37
|
+
return Boolean(a.v);
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const say = (lines) => { for (const [t] of lines || []) printInfo(t); };
|
|
43
|
+
|
|
44
|
+
export function registerProviderCommands(program, ctx) {
|
|
45
|
+
const { client, auth, config } = ctx;
|
|
46
|
+
const io = ctx.providerIO || defaultIO();
|
|
47
|
+
const api = () => (ctx.makeApi ? ctx.makeApi() : createApi({ client, config, auth }));
|
|
48
|
+
|
|
49
|
+
const provider = program
|
|
50
|
+
.command('provider')
|
|
51
|
+
.description('Connect the AI providers your org uses (the same as /connect in the Quox terminal)');
|
|
52
|
+
|
|
53
|
+
// ── provider list ──────────────────────────────────────────────────────────
|
|
54
|
+
provider
|
|
55
|
+
.command('list')
|
|
56
|
+
.description('Show which AI providers this org can use, and how each is connected')
|
|
57
|
+
.action(async () => {
|
|
58
|
+
auth.requireAuth();
|
|
59
|
+
const a = api();
|
|
60
|
+
const [vault, caps] = await Promise.all([a.connections(), a.capabilities ? a.capabilities() : null]);
|
|
61
|
+
if (program.opts().output === 'json') {
|
|
62
|
+
const rows = vault?.ok ? vault.rows.map((r) => Object.fromEntries(SAFE_ROW.filter((k) => r[k] !== undefined).map((k) => [k, r[k]]))) : null;
|
|
63
|
+
formatDetail({ connections: rows, ...(vault?.ok ? {} : { error: vault?.error || 'vault not readable' }), capabilities: caps?.ok ? caps.capabilities : null }, { format: 'json' });
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
say(connectionLines(vault, caps));
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// ── provider connect ───────────────────────────────────────────────────────
|
|
70
|
+
const readKey = async (p, opts) => {
|
|
71
|
+
if (opts.keyFromStdin) return String(await io.readStdin() || '').replace(/\r?\n$/, '').trim();
|
|
72
|
+
if (io.isTTY()) return String(await io.promptSecret(`${p.name} key (hidden, never shown or stored on this machine):`) || '').trim();
|
|
73
|
+
throw new Error(`No key given. Pipe it in with --key-from-stdin (for example: quox provider connect ${p.id} --key-from-stdin < keyfile), or run this in a terminal to be asked. A key is never taken as an argument.`);
|
|
74
|
+
};
|
|
75
|
+
const consentAfter = async (type) => {
|
|
76
|
+
const a = api();
|
|
77
|
+
if (!needsConsent(type) || !a.consentStatus) return;
|
|
78
|
+
let rec = null; try { rec = await a.consentStatus(); } catch { rec = null; }
|
|
79
|
+
if (consentCovers(rec, type)) return;
|
|
80
|
+
// --yes never grants consent: it is this person's own OK to send their conversations to a third party
|
|
81
|
+
if (!io.isTTY() || !a.grantConsent) { printInfo(connectConsentNote(type)); return; }
|
|
82
|
+
const yes = await io.confirm(`Allow Quox to send your conversations to ${providerLabel(type)}? Your messages and tool results go to ${providerLabel(type)}, a third party, when it answers. Nothing is sent until you say yes.`);
|
|
83
|
+
if (!yes) { printInfo(`Left off: nothing goes to ${providerLabel(type)} until you say so. ${connectConsentNote(type)}`); return; }
|
|
84
|
+
try { await a.grantConsent(type); printInfo(`✔ Your OK is recorded: ${providerLabel(type)} can now answer. Withdraw it any time: quox consent list.`); }
|
|
85
|
+
catch (e) { printInfo(`Quox could not record your OK (${e?.message || e}). Try: quox consent grant ${type}`); }
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
provider
|
|
89
|
+
.command('connect')
|
|
90
|
+
.description('Connect an AI provider for this org: a keyed provider (openai, anthropic, groq...) takes its key on stdin or a hidden prompt; a self-hosted one (ollama, lmstudio, vllm, custom) takes a URL')
|
|
91
|
+
.argument('<kind>', `provider: ${KINDS.join(', ')}`)
|
|
92
|
+
.argument('[url]', 'server URL for ollama / lmstudio / vllm / custom (same as --url)')
|
|
93
|
+
.option('--url <url>', `server URL for a self-hosted provider. ${CONTAINER_NOTE}`)
|
|
94
|
+
.option('--model <model>', 'default model to use on that server')
|
|
95
|
+
.option('--key-from-stdin', 'read the API key from stdin (never pass a key as an argument)')
|
|
96
|
+
.option('--yes', 'confirm connecting to an address on your private network (the server still checks it and audits the decision)')
|
|
97
|
+
.addHelpText('after', `\n${CONTAINER_NOTE}\nKeys: --key-from-stdin, or run in a terminal to be asked with a hidden prompt.`)
|
|
98
|
+
.action(async (kind, urlArg, opts) => {
|
|
99
|
+
auth.requireAuth();
|
|
100
|
+
const p = providerFor(kind);
|
|
101
|
+
if (!p) throw new Error(`No provider called "${kind}". Use one of: ${KINDS.join(', ')}`);
|
|
102
|
+
if (urlArg && (looksLikeKey(urlArg) || (p.card && !p.url))) throw new Error('never pass a key as an argument (it would sit in your shell history and process list). Use --key-from-stdin, or run in a terminal to be asked.');
|
|
103
|
+
const a = api();
|
|
104
|
+
const url = opts.url || urlArg || '';
|
|
105
|
+
let r;
|
|
106
|
+
if (p.id === 'ollama') {
|
|
107
|
+
r = await connectOllamaFlow(a, [url, opts.model].filter(Boolean).join(' '));
|
|
108
|
+
say(r.lines);
|
|
109
|
+
if (r.privateAsk) {
|
|
110
|
+
const q = `Connect to ${r.privateAsk.host}? It's on your private network. Quox saves only ${r.privateAsk.origin}, after the server checks it answers as Ollama.`;
|
|
111
|
+
let yes = Boolean(opts.yes);
|
|
112
|
+
if (!yes && io.isTTY()) yes = await io.confirm(q);
|
|
113
|
+
if (!yes && !io.isTTY()) throw new Error(`${r.privateAsk.host} is on your private network. Run again with --yes to confirm this one address. Nothing was saved.`);
|
|
114
|
+
if (!yes) throw new Error(`Left blocked: ${r.privateAsk.host} was not connected. Nothing was saved.`);
|
|
115
|
+
r = await r.privateAsk.retry();
|
|
116
|
+
say(r.lines);
|
|
117
|
+
}
|
|
118
|
+
} else if (p.url) {
|
|
119
|
+
r = await connectCompatFlow(a, p.id, [url, opts.model].filter(Boolean).join(' '));
|
|
120
|
+
say(r.lines);
|
|
121
|
+
if (r.needsKey) { r = await connectFlow(a, 'custom', await readKey(PROVIDERS.custom, opts), r.extra); printInfo(r.text); if (r.disclosure) printInfo(r.disclosure); }
|
|
122
|
+
} else {
|
|
123
|
+
r = await connectFlow(a, p.id, await readKey(p, opts));
|
|
124
|
+
printInfo(r.text);
|
|
125
|
+
if (r.ok && r.disclosure) printInfo(r.disclosure);
|
|
126
|
+
}
|
|
127
|
+
if (r?.lanOffer) printInfo(' From the CLI an instance admin allows private targets with: quox server config lan-targets on then run this again.');
|
|
128
|
+
if (!r?.ok) throw new Error(r?.text || `${p.name} was not connected. Nothing was saved.`);
|
|
129
|
+
await consentAfter(p.type);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// ── provider test ──────────────────────────────────────────────────────────
|
|
133
|
+
provider
|
|
134
|
+
.command('test')
|
|
135
|
+
.description('Test the stored connection for a provider (or every default connection) through the vault test')
|
|
136
|
+
.argument('[kind]', 'provider to test (default: every connected provider)')
|
|
137
|
+
.action(async (kind) => {
|
|
138
|
+
auth.requireAuth();
|
|
139
|
+
const p = kind ? providerFor(kind) : null;
|
|
140
|
+
if (kind && !p) throw new Error(`No provider called "${kind}". Use one of: ${KINDS.join(', ')}`);
|
|
141
|
+
const vault = await api().connections();
|
|
142
|
+
if (!vault?.ok) throw new Error(`Could not read the vault: ${vault?.error || 'no answer'}`);
|
|
143
|
+
const types = new Set(p ? [p.type] : Object.values(PROVIDERS).map((x) => x.type));
|
|
144
|
+
const rows = vault.rows.filter((r) => types.has(r.integration_type) && (r.is_default === 1 || r.is_default === true));
|
|
145
|
+
if (!rows.length) throw new Error(p ? `No ${p.id} connection is stored for this org. Connect one: quox provider connect ${p.id}` : 'No provider connection is stored for this org. Connect one: quox provider connect <kind>');
|
|
146
|
+
const orgId = resolveOrgId(auth, program);
|
|
147
|
+
let failed = 0;
|
|
148
|
+
for (const row of rows) {
|
|
149
|
+
const name = (Object.values(PROVIDERS).find((x) => x.type === row.integration_type) || {}).name || row.integration_type;
|
|
150
|
+
const ep = resolveEndpoint('vault.test', { orgId, id: row.id });
|
|
151
|
+
let res;
|
|
152
|
+
try { res = await client.post(ep.path, { service: ep.service, retries: 0 }); }
|
|
153
|
+
catch (e) { res = { success: false, message: e?.message || 'failed' }; }
|
|
154
|
+
if (res?.success === false) { failed++; printInfo(`✖ ${name} ${row.label || row.id} failed: ${res?.message || 'no reason given'}`); }
|
|
155
|
+
else printInfo(`✔ ${name} ${row.label || row.id} ok${res?.message ? `: ${res.message}` : ''}`);
|
|
156
|
+
}
|
|
157
|
+
if (failed) process.exitCode = 1;
|
|
158
|
+
});
|
|
159
|
+
}
|
package/lib/commands/qlarity.js
CHANGED
|
@@ -49,9 +49,12 @@ function annotate(err) {
|
|
|
49
49
|
throw err;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
|
|
52
|
+
// quox#907: a browser login is slower than a read; well above the client's 30s default.
|
|
53
|
+
const LOGIN_TIMEOUT_MS = 120000;
|
|
54
|
+
|
|
55
|
+
async function call(client, id, params = {}, body, extra = {}) {
|
|
53
56
|
const ep = resolveEndpoint(id, params);
|
|
54
|
-
const opts = { service: ep.service };
|
|
57
|
+
const opts = { service: ep.service, ...extra };
|
|
55
58
|
if (body) opts.body = body;
|
|
56
59
|
switch (ep.method) {
|
|
57
60
|
case 'POST': return client.post(ep.path, opts);
|
|
@@ -634,15 +637,28 @@ export function registerQlarityCommands(program, ctx) {
|
|
|
634
637
|
(await call(client, 'qlarity.sessions.list')).sessions || [],
|
|
635
638
|
normalizedOrigin,
|
|
636
639
|
);
|
|
640
|
+
let fresh = false;
|
|
637
641
|
if (!sessionId) {
|
|
638
642
|
const s = await call(client, 'qlarity.session.create', {}, {});
|
|
639
643
|
sessionId = s.id;
|
|
644
|
+
fresh = true;
|
|
640
645
|
}
|
|
641
646
|
|
|
642
647
|
const body = { origin: normalizedOrigin, credential_id: credentialId };
|
|
643
648
|
if (options.loginPath) body.login_path = options.loginPath;
|
|
644
649
|
|
|
645
|
-
|
|
650
|
+
// quox#907: a real login (page load, form fill, cookie wait) can outlast the CLI's 30s default on a loaded
|
|
651
|
+
// box, and the server kept finishing it, so every timed-out retry left a headless browser behind. Give login
|
|
652
|
+
// its own budget, and close a session THIS call created when the login call fails (as openSession does, #680).
|
|
653
|
+
let r;
|
|
654
|
+
try {
|
|
655
|
+
r = await call(client, 'qlarity.session.login', { sessionId }, body, { timeout: LOGIN_TIMEOUT_MS });
|
|
656
|
+
} catch (loginErr) {
|
|
657
|
+
if (fresh) {
|
|
658
|
+
try { await call(client, 'qlarity.session.close', { sessionId }); } catch { /* the server reaper will get it */ }
|
|
659
|
+
}
|
|
660
|
+
throw loginErr;
|
|
661
|
+
}
|
|
646
662
|
|
|
647
663
|
if (r.logged_in) {
|
|
648
664
|
loggedIn[normalizedOrigin] = { sessionId, logged_in_at: new Date().toISOString() };
|
package/lib/commands/quoxchat.js
CHANGED
|
@@ -131,11 +131,19 @@ export function registerQuoxchatCommands(program, ctx) {
|
|
|
131
131
|
.action(async () => {
|
|
132
132
|
auth.requireAuth();
|
|
133
133
|
const orgId = resolveOrgId(auth, program);
|
|
134
|
-
|
|
134
|
+
// QC-BRUTALOOP-W15: the route pages at 20; listing only page 1 silently
|
|
135
|
+
// under-reported the org's widgets (20 of 34). Walk every page.
|
|
136
|
+
const ep = resolveEndpoint('quoxchat.widgets.list', { orgId });
|
|
137
|
+
const rows = [];
|
|
138
|
+
for (let page = 1; page <= 50; page++) {
|
|
139
|
+
const data = await client.get(page === 1 ? ep.path : `${ep.path}?page=${page}`, { service: ep.service });
|
|
140
|
+
rows.push(...(Array.isArray(data) ? data : (data.items || [])));
|
|
141
|
+
if (Array.isArray(data) || !data.pagination?.has_more) break;
|
|
142
|
+
}
|
|
135
143
|
formatOutput({
|
|
136
144
|
columns: ['id', 'name', 'assistant_name', 'status', 'operator_online', 'created_at'],
|
|
137
145
|
headers: ['ID', 'Name', 'Assistant', 'Status', 'Operator online', 'Created'],
|
|
138
|
-
rows
|
|
146
|
+
rows,
|
|
139
147
|
}, { format: program.opts().output, emptyMessage: 'No widgets found.' });
|
|
140
148
|
});
|
|
141
149
|
|
|
@@ -209,7 +217,8 @@ export function registerQuoxchatCommands(program, ctx) {
|
|
|
209
217
|
.description('Print a conversation transcript in chronological order')
|
|
210
218
|
.argument('<widgetId>', 'widget ID')
|
|
211
219
|
.argument('<conversationId>', 'conversation ID')
|
|
212
|
-
.
|
|
220
|
+
.option('--full', 'do not truncate long messages in table output (JSON output is never truncated)')
|
|
221
|
+
.action(async (widgetId, conversationId, cmdOpts = {}) => {
|
|
213
222
|
auth.requireAuth();
|
|
214
223
|
const orgId = resolveOrgId(auth, program);
|
|
215
224
|
|
|
@@ -236,7 +245,7 @@ export function registerQuoxchatCommands(program, ctx) {
|
|
|
236
245
|
from: m.role === 'user' ? visitorName : m.role === 'operator' ? (m.sender_name || 'Operator') : 'AI assistant',
|
|
237
246
|
content: m.content || '',
|
|
238
247
|
})),
|
|
239
|
-
}, { format: program.opts().output, emptyMessage: 'No messages in this conversation yet.' });
|
|
248
|
+
}, { format: program.opts().output, full: cmdOpts.full === true, emptyMessage: 'No messages in this conversation yet.' });
|
|
240
249
|
});
|
|
241
250
|
|
|
242
251
|
quoxchat
|
package/lib/commands/quoxseo.js
CHANGED
|
@@ -100,6 +100,44 @@ export function registerQuoxseoCommands(program, ctx) {
|
|
|
100
100
|
} catch (err) { annotate(err); }
|
|
101
101
|
});
|
|
102
102
|
|
|
103
|
+
s.command('remove-site <site>')
|
|
104
|
+
.description('Remove a registered site and its warehouse data for this org (governed, witnessed). <site> is an origin or property id')
|
|
105
|
+
.option('-y, --yes', 'skip the confirmation prompt')
|
|
106
|
+
.action(async (site, opts) => {
|
|
107
|
+
try {
|
|
108
|
+
const list = await get(client, 'quoxseo.sites');
|
|
109
|
+
const props = list.properties || [];
|
|
110
|
+
const norm = (v) => String(v).trim().replace(/\/+$/, '').toLowerCase();
|
|
111
|
+
const target = props.find((p) => String(p.id) === String(site).trim() || norm(p.origin) === norm(site));
|
|
112
|
+
if (!target) {
|
|
113
|
+
throw new Error(`Site not found: ${site} is not registered for this org. List them with: quox seo sites`);
|
|
114
|
+
}
|
|
115
|
+
const warn = `This permanently removes ${target.origin} (#${target.id}) from this org and deletes its crawl, GSC and GA4 warehouse data. This cannot be undone.`;
|
|
116
|
+
if (!opts.yes && program.opts().yes !== true) {
|
|
117
|
+
if (!process.stdin.isTTY) {
|
|
118
|
+
throw new Error(`${warn} Confirmation needed; re-run with --yes in a non-interactive shell: quox seo remove-site ${target.origin} --yes`);
|
|
119
|
+
}
|
|
120
|
+
console.log(warn);
|
|
121
|
+
const { confirmDestructive } = await import('../core/confirm.js');
|
|
122
|
+
const ok = await confirmDestructive(program, {}, `Remove ${target.origin}?`);
|
|
123
|
+
if (!ok) { console.log('Aborted. Nothing was removed.'); return; }
|
|
124
|
+
}
|
|
125
|
+
const ep = resolveEndpoint('quoxseo.sites.remove', { id: target.id });
|
|
126
|
+
let data;
|
|
127
|
+
try {
|
|
128
|
+
data = await client.delete(ep.path, { service: ep.service });
|
|
129
|
+
} catch (err) {
|
|
130
|
+
const code = err?.status ?? err?.statusCode ?? err?.response?.status;
|
|
131
|
+
if (code === 404 || code === 405 || /\b(404|405)\b|not found|read.only/i.test(err?.message || '')) {
|
|
132
|
+
err.message = `${err.message}\nThis install's QuoxSEO service does not support site removal (no DELETE route), or the site no longer exists. Nothing was removed.`;
|
|
133
|
+
}
|
|
134
|
+
throw err;
|
|
135
|
+
}
|
|
136
|
+
if (jsonOut(data)) return;
|
|
137
|
+
console.log(`removed #${target.id} ${target.origin}`);
|
|
138
|
+
} catch (err) { annotate(err); }
|
|
139
|
+
});
|
|
140
|
+
|
|
103
141
|
s.command('urls')
|
|
104
142
|
.description('Warehouse rows for the latest crawl')
|
|
105
143
|
.option('--filter <f>', 'orphans | noindex | errors | no-gsc | discovered')
|
package/lib/commands/reach.js
CHANGED
|
@@ -103,7 +103,8 @@ export function registerReachCommands(program, ctx) {
|
|
|
103
103
|
const orgId = resolveOrgId(auth, program);
|
|
104
104
|
const days = parseInt(options.days, 10) || 30;
|
|
105
105
|
const ep = resolveEndpoint('reach.agent.leastPrivilege', { orgId, agentId, days });
|
|
106
|
-
|
|
106
|
+
// #923: the report scans the usage window and outlived the 30 s default under load
|
|
107
|
+
const result = await client.get(ep.path, { service: ep.service, timeout: 120000 });
|
|
107
108
|
|
|
108
109
|
if (isJsonOutput(program)) {
|
|
109
110
|
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|