quoxcli 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +35 -0
- package/README.md +142 -2
- package/bin/quox.js +292 -0
- package/bin/quoxon.js +8 -0
- package/lib/api/endpoints/admin.js +125 -0
- package/lib/api/endpoints/aee.js +38 -0
- package/lib/api/endpoints/agent-attach.js +126 -0
- package/lib/api/endpoints/agents.js +56 -0
- package/lib/api/endpoints/assistants.js +50 -0
- package/lib/api/endpoints/audit.js +14 -0
- package/lib/api/endpoints/auth.js +50 -0
- package/lib/api/endpoints/awareness.js +34 -0
- package/lib/api/endpoints/backup-schedules.js +20 -0
- package/lib/api/endpoints/backups.js +86 -0
- package/lib/api/endpoints/brain.js +140 -0
- package/lib/api/endpoints/budgets.js +62 -0
- package/lib/api/endpoints/calendarq.js +71 -0
- package/lib/api/endpoints/catalog.js +14 -0
- package/lib/api/endpoints/chat.js +54 -0
- package/lib/api/endpoints/consent.js +26 -0
- package/lib/api/endpoints/contactsq.js +66 -0
- package/lib/api/endpoints/conversations.js +32 -0
- package/lib/api/endpoints/custom-roles.js +92 -0
- package/lib/api/endpoints/divisions.js +56 -0
- package/lib/api/endpoints/domains.js +38 -0
- package/lib/api/endpoints/engagement.js +231 -0
- package/lib/api/endpoints/engine.js +11 -0
- package/lib/api/endpoints/entity.js +50 -0
- package/lib/api/endpoints/executions.js +32 -0
- package/lib/api/endpoints/file.js +20 -0
- package/lib/api/endpoints/files.js +26 -0
- package/lib/api/endpoints/github.js +21 -0
- package/lib/api/endpoints/governance.js +43 -0
- package/lib/api/endpoints/hat.js +36 -0
- package/lib/api/endpoints/inbox.js +32 -0
- package/lib/api/endpoints/inboxq.js +120 -0
- package/lib/api/endpoints/index.js +228 -0
- package/lib/api/endpoints/invites.js +19 -0
- package/lib/api/endpoints/llm-policy.js +43 -0
- package/lib/api/endpoints/lucid.js +89 -0
- package/lib/api/endpoints/members.js +32 -0
- package/lib/api/endpoints/memory.js +75 -0
- package/lib/api/endpoints/mfa.js +32 -0
- package/lib/api/endpoints/mon.js +178 -0
- package/lib/api/endpoints/notesq.js +43 -0
- package/lib/api/endpoints/objectives.js +62 -0
- package/lib/api/endpoints/orgs.js +38 -0
- package/lib/api/endpoints/plugins.js +134 -0
- package/lib/api/endpoints/powers.js +75 -0
- package/lib/api/endpoints/prompts.js +69 -0
- package/lib/api/endpoints/qlarity.js +122 -0
- package/lib/api/endpoints/quoxbox.js +55 -0
- package/lib/api/endpoints/quoxchat.js +54 -0
- package/lib/api/endpoints/quoxflow.js +32 -0
- package/lib/api/endpoints/quoxplan.js +114 -0
- package/lib/api/endpoints/quoxseo.js +108 -0
- package/lib/api/endpoints/reach.js +47 -0
- package/lib/api/endpoints/runs.js +38 -0
- package/lib/api/endpoints/safety.js +46 -0
- package/lib/api/endpoints/scim.js +46 -0
- package/lib/api/endpoints/scopes.js +14 -0
- package/lib/api/endpoints/security.js +69 -0
- package/lib/api/endpoints/setup.js +90 -0
- package/lib/api/endpoints/skills.js +180 -0
- package/lib/api/endpoints/sso.js +67 -0
- package/lib/api/endpoints/tags.js +32 -0
- package/lib/api/endpoints/teams.js +56 -0
- package/lib/api/endpoints/telephony.js +46 -0
- package/lib/api/endpoints/tg.js +56 -0
- package/lib/api/endpoints/tools.js +50 -0
- package/lib/api/endpoints/traces.js +157 -0
- package/lib/api/endpoints/triggers.js +70 -0
- package/lib/api/endpoints/user-library.js +41 -0
- package/lib/api/endpoints/users.js +20 -0
- package/lib/api/endpoints/v1.js +499 -0
- package/lib/api/endpoints/vault.js +192 -0
- package/lib/api/endpoints/voice.js +22 -0
- package/lib/api/endpoints/volt.js +84 -0
- package/lib/api/endpoints/ward.js +40 -0
- package/lib/api/endpoints/webhooks.js +46 -0
- package/lib/api/endpoints/workflows.js +162 -0
- package/lib/api/endpoints/x.js +24 -0
- package/lib/api/endpoints.js +15 -0
- package/lib/commands/admin.js +359 -0
- package/lib/commands/aee.js +99 -0
- package/lib/commands/agent.js +201 -0
- package/lib/commands/agentAttach.js +1440 -0
- package/lib/commands/approvals.js +47 -0
- package/lib/commands/assistant.js +114 -0
- package/lib/commands/auth.js +458 -0
- package/lib/commands/awareness.js +88 -0
- package/lib/commands/backup.js +373 -0
- package/lib/commands/brain.js +994 -0
- package/lib/commands/budget.js +241 -0
- package/lib/commands/build.js +156 -0
- package/lib/commands/calendar.js +216 -0
- package/lib/commands/capabilities.js +34 -0
- package/lib/commands/chat.js +240 -0
- package/lib/commands/claude.js +354 -0
- package/lib/commands/config-cmd.js +106 -0
- package/lib/commands/consent.js +84 -0
- package/lib/commands/contacts.js +967 -0
- package/lib/commands/contactsSelftest.js +132 -0
- package/lib/commands/conversation.js +74 -0
- package/lib/commands/department.js +75 -0
- package/lib/commands/dev.js +620 -0
- package/lib/commands/devResident.js +314 -0
- package/lib/commands/devSupervisor.js +1050 -0
- package/lib/commands/discord.js +96 -0
- package/lib/commands/doctor.js +181 -0
- package/lib/commands/domain.js +114 -0
- package/lib/commands/done.js +277 -0
- package/lib/commands/engagement.js +586 -0
- package/lib/commands/entity.js +119 -0
- package/lib/commands/facebook.js +212 -0
- package/lib/commands/file.js +128 -0
- package/lib/commands/fleet.js +882 -0
- package/lib/commands/governance.js +638 -0
- package/lib/commands/gripe.js +324 -0
- package/lib/commands/hat.js +205 -0
- package/lib/commands/helpers.js +79 -0
- package/lib/commands/inbox.js +122 -0
- package/lib/commands/instagram.js +264 -0
- package/lib/commands/integration.js +100 -0
- package/lib/commands/inventory.js +115 -0
- package/lib/commands/invites.js +45 -0
- package/lib/commands/licence.js +280 -0
- package/lib/commands/linkedin.js +178 -0
- package/lib/commands/llm-policy.js +117 -0
- package/lib/commands/llm.js +45 -0
- package/lib/commands/lucid.js +591 -0
- package/lib/commands/mail.js +531 -0
- package/lib/commands/memory.js +284 -0
- package/lib/commands/mon.js +514 -0
- package/lib/commands/monitoring.js +86 -0
- package/lib/commands/motion.js +22 -0
- package/lib/commands/notes.js +1246 -0
- package/lib/commands/notesBatch.js +160 -0
- package/lib/commands/notesBridges.js +56 -0
- package/lib/commands/notesLinks.js +60 -0
- package/lib/commands/notification.js +57 -0
- package/lib/commands/nucleus.js +92 -0
- package/lib/commands/objective.js +229 -0
- package/lib/commands/onboard.js +328 -0
- package/lib/commands/open.js +56 -0
- package/lib/commands/org.js +266 -0
- package/lib/commands/plan.js +906 -0
- package/lib/commands/plugin-doctor.js +1091 -0
- package/lib/commands/plugin.js +539 -0
- package/lib/commands/policy.js +78 -0
- package/lib/commands/powers.js +296 -0
- package/lib/commands/prompt.js +350 -0
- package/lib/commands/qlarity.js +690 -0
- package/lib/commands/quoxbox.js +270 -0
- package/lib/commands/quoxchat.js +266 -0
- package/lib/commands/quoxseo.js +340 -0
- package/lib/commands/reach.js +158 -0
- package/lib/commands/role.js +286 -0
- package/lib/commands/run.js +132 -0
- package/lib/commands/safety.js +107 -0
- package/lib/commands/scim.js +88 -0
- package/lib/commands/scope.js +58 -0
- package/lib/commands/security.js +157 -0
- package/lib/commands/service.js +89 -0
- package/lib/commands/settings.js +99 -0
- package/lib/commands/setup.js +543 -0
- package/lib/commands/skillManifest.js +113 -0
- package/lib/commands/skillRightsFormat.js +38 -0
- package/lib/commands/skills.js +644 -0
- package/lib/commands/slot.js +1198 -0
- package/lib/commands/sso.js +229 -0
- package/lib/commands/status.js +137 -0
- package/lib/commands/tag.js +91 -0
- package/lib/commands/team.js +140 -0
- package/lib/commands/tg.js +240 -0
- package/lib/commands/tiktok.js +218 -0
- package/lib/commands/tool.js +216 -0
- package/lib/commands/trace.js +525 -0
- package/lib/commands/trigger.js +128 -0
- package/lib/commands/vault.js +1424 -0
- package/lib/commands/voice.js +182 -0
- package/lib/commands/volt.js +528 -0
- package/lib/commands/watch.js +115 -0
- package/lib/commands/webhook.js +137 -0
- package/lib/commands/workflow.js +503 -0
- package/lib/commands/x.js +439 -0
- package/lib/core/actingAs.js +136 -0
- package/lib/core/aee.js +53 -0
- package/lib/core/agentAlias.js +163 -0
- package/lib/core/approvalContext.js +61 -0
- package/lib/core/auth.js +350 -0
- package/lib/core/banner.js +143 -0
- package/lib/core/brainPrime.js +90 -0
- package/lib/core/callerSource.js +129 -0
- package/lib/core/claudeLs.js +94 -0
- package/lib/core/client.js +584 -0
- package/lib/core/completions.js +163 -0
- package/lib/core/config.js +551 -0
- package/lib/core/confirm.js +37 -0
- package/lib/core/deeplinks.js +76 -0
- package/lib/core/doneEngineFetch.js +219 -0
- package/lib/core/errors.js +132 -0
- package/lib/core/global-flags.js +90 -0
- package/lib/core/governedSession.js +402 -0
- package/lib/core/lazyCommands.js +119 -0
- package/lib/core/liveSessionsReport.js +112 -0
- package/lib/core/org-override.js +52 -0
- package/lib/core/output.js +239 -0
- package/lib/core/prodEnv.js +7 -0
- package/lib/core/quoxglyph.js +158 -0
- package/lib/core/residentAlarm.js +282 -0
- package/lib/core/residentAuth.js +243 -0
- package/lib/core/residentPump.js +229 -0
- package/lib/core/residentSeed.js +104 -0
- package/lib/core/sessionHeartbeat.js +192 -0
- package/lib/core/sessionsOverview.js +160 -0
- package/lib/core/slotKeeper.js +282 -0
- package/lib/core/supervisorEngine.js +864 -0
- package/lib/core/systemdInstaller.js +256 -0
- package/lib/core/version.js +87 -0
- package/lib/core/websiteSession.js +148 -0
- package/lib/lucid/LucidIntro.jsx +105 -0
- package/lib/lucid/blind.js +71 -0
- package/lib/lucid/deck.js +32 -0
- package/lib/lucid/fmt.js +139 -0
- package/lib/lucid/sitrep.js +173 -0
- package/lib/plugins/loader.js +93 -0
- package/lib/quoxon/activity.js +104 -0
- package/lib/quoxon/api.js +473 -0
- package/lib/quoxon/app.js +1262 -0
- package/lib/quoxon/capabilities.js +75 -0
- package/lib/quoxon/commands.js +154 -0
- package/lib/quoxon/connect.js +467 -0
- package/lib/quoxon/connectCard.js +28 -0
- package/lib/quoxon/context.js +68 -0
- package/lib/quoxon/copy.js +33 -0
- package/lib/quoxon/demo.fixtures.js +24 -0
- package/lib/quoxon/demo.js +35 -0
- package/lib/quoxon/egg.js +36 -0
- package/lib/quoxon/events.js +458 -0
- package/lib/quoxon/gallery.js +223 -0
- package/lib/quoxon/indicator.js +193 -0
- package/lib/quoxon/intent.js +41 -0
- package/lib/quoxon/keys.js +121 -0
- package/lib/quoxon/main.js +244 -0
- package/lib/quoxon/markdown.js +317 -0
- package/lib/quoxon/model.js +205 -0
- package/lib/quoxon/motion/clock.js +69 -0
- package/lib/quoxon/motion/color.js +35 -0
- package/lib/quoxon/motion/frame.js +14 -0
- package/lib/quoxon/motion/reveal.js +54 -0
- package/lib/quoxon/motion/ring.js +79 -0
- package/lib/quoxon/objects/agent.js +61 -0
- package/lib/quoxon/objects/crystal.js +100 -0
- package/lib/quoxon/objects/fetch.js +170 -0
- package/lib/quoxon/objects/index.js +85 -0
- package/lib/quoxon/objects/ladder.js +71 -0
- package/lib/quoxon/objects/memory.js +44 -0
- package/lib/quoxon/objects/model.js +118 -0
- package/lib/quoxon/objects/proof.js +110 -0
- package/lib/quoxon/objects/skill.js +105 -0
- package/lib/quoxon/objects/view.js +76 -0
- package/lib/quoxon/objects/workflow.js +119 -0
- package/lib/quoxon/observers.js +42 -0
- package/lib/quoxon/onboarding/copy.js +235 -0
- package/lib/quoxon/onboarding/credential.js +95 -0
- package/lib/quoxon/onboarding/detect.js +22 -0
- package/lib/quoxon/onboarding/finish.js +119 -0
- package/lib/quoxon/onboarding/index.js +67 -0
- package/lib/quoxon/onboarding/installRunner.js +225 -0
- package/lib/quoxon/onboarding/intent.js +53 -0
- package/lib/quoxon/onboarding/journey.js +73 -0
- package/lib/quoxon/onboarding/preflight.js +142 -0
- package/lib/quoxon/onboarding/runner.js +58 -0
- package/lib/quoxon/onboarding/view.js +625 -0
- package/lib/quoxon/print.js +94 -0
- package/lib/quoxon/qart.js +379 -0
- package/lib/quoxon/qglyph.js +143 -0
- package/lib/quoxon/qhello.js +58 -0
- package/lib/quoxon/qmotion.js +111 -0
- package/lib/quoxon/render.js +648 -0
- package/lib/quoxon/roster.js +221 -0
- package/lib/quoxon/route.js +38 -0
- package/lib/quoxon/rules.js +130 -0
- package/lib/quoxon/session.js +68 -0
- package/lib/quoxon/skills.js +30 -0
- package/lib/quoxon/slash/compact.js +90 -0
- package/lib/quoxon/slash/ledger.js +83 -0
- package/lib/quoxon/slash/listView.js +28 -0
- package/lib/quoxon/slash/palette.js +104 -0
- package/lib/quoxon/slash/paletteController.js +59 -0
- package/lib/quoxon/slash/panels.js +101 -0
- package/lib/quoxon/slash/picker.js +38 -0
- package/lib/quoxon/slash/restart.js +144 -0
- package/lib/quoxon/slash/resume.js +59 -0
- package/lib/quoxon/slash/settings.js +50 -0
- package/lib/quoxon/sound/index.js +186 -0
- package/lib/quoxon/sound/mapping.js +119 -0
- package/lib/quoxon/sound/output.js +52 -0
- package/lib/quoxon/sound/synth.js +122 -0
- package/lib/quoxon/term.js +106 -0
- package/lib/quoxon/textBuffer.js +242 -0
- package/lib/quoxon/theme.js +253 -0
- package/lib/quoxon/transcript.js +150 -0
- package/lib/quoxon/web/qface.js +81 -0
- package/lib/quoxon/welcome.js +52 -0
- package/lib/tui/app.jsx +289 -0
- package/lib/tui/components/Breadcrumb.jsx +42 -0
- package/lib/tui/components/ChatInput.jsx +68 -0
- package/lib/tui/components/Confirm.jsx +32 -0
- package/lib/tui/components/DataTable.jsx +270 -0
- package/lib/tui/components/DetailPanel.jsx +241 -0
- package/lib/tui/components/HelpOverlay.jsx +67 -0
- package/lib/tui/components/Layout.jsx +129 -0
- package/lib/tui/components/Palette.jsx +141 -0
- package/lib/tui/components/SearchInput.jsx +125 -0
- package/lib/tui/components/Sparkline.jsx +55 -0
- package/lib/tui/components/StatusBar.jsx +133 -0
- package/lib/tui/components/Toast.jsx +52 -0
- package/lib/tui/hooks/useApi.js +44 -0
- package/lib/tui/hooks/useHealth.js +90 -0
- package/lib/tui/hooks/usePolledApi.js +86 -0
- package/lib/tui/hooks/useTerminalSize.js +46 -0
- package/lib/tui/index.js +56 -0
- package/lib/tui/intro/Intro.jsx +332 -0
- package/lib/tui/jsx-loader.js +32 -0
- package/lib/tui/keys.js +87 -0
- package/lib/tui/registry.js +49 -0
- package/lib/tui/theme.js +103 -0
- package/lib/tui/views/AdminView.jsx +398 -0
- package/lib/tui/views/AgentView.jsx +204 -0
- package/lib/tui/views/BackupView.jsx +225 -0
- package/lib/tui/views/BrainView.jsx +223 -0
- package/lib/tui/views/ChatView.jsx +468 -0
- package/lib/tui/views/FilesView.jsx +218 -0
- package/lib/tui/views/FleetView.jsx +234 -0
- package/lib/tui/views/GovernanceView.jsx +453 -0
- package/lib/tui/views/HealthView.jsx +216 -0
- package/lib/tui/views/InboxView.jsx +228 -0
- package/lib/tui/views/LucidDeck.jsx +727 -0
- package/lib/tui/views/MainMenu.jsx +123 -0
- package/lib/tui/views/MemoryView.jsx +377 -0
- package/lib/tui/views/ObjectivesView.jsx +230 -0
- package/lib/tui/views/OrgView.jsx +229 -0
- package/lib/tui/views/ToolView.jsx +221 -0
- package/lib/tui/views/VaultView.jsx +422 -0
- package/lib/tui/views/WorkflowView.jsx +329 -0
- package/package.json +72 -4
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `quox licence` (alias `license`), GRIPE-715: governed, audited licence
|
|
3
|
+
* operations from the terminal.
|
|
4
|
+
*
|
|
5
|
+
* Phase 1 (instance-local): status reads the lease (never the key), refresh
|
|
6
|
+
* runs the renewal, seats lists/cleans band holders (dry run default).
|
|
7
|
+
*
|
|
8
|
+
* Phase 2 (owner-only, talks to the quox.ai WEBSITE admin API with a separate
|
|
9
|
+
* website session, see core/websiteSession.js): reissue re-bands/extends a
|
|
10
|
+
* licence record, catalogue-sign re-signs the product catalogue. Both are DRY
|
|
11
|
+
* RUN unless --apply. The signing key never leaves the website VM; the CLI
|
|
12
|
+
* never sees, prints or sends key material or licence keys.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { writeFileSync } from 'node:fs';
|
|
16
|
+
import { formatOutput, formatDetail, printSuccess, printInfo, printWarning } from '../core/output.js';
|
|
17
|
+
import { resolveEndpoint } from '../api/endpoints.js';
|
|
18
|
+
import { QuoxValidationError } from '../core/errors.js';
|
|
19
|
+
import {
|
|
20
|
+
DEFAULT_WEBSITE_URL, DEFAULT_WEBSITE_PROFILE, normaliseWebsiteUrl, websiteLogin, websitePost,
|
|
21
|
+
saveWebsiteSession, clearWebsiteSession, getWebsiteSession,
|
|
22
|
+
} from '../core/websiteSession.js';
|
|
23
|
+
|
|
24
|
+
const OWNER_HELP = `
|
|
25
|
+
Owner-only (website admin session, dry run unless --apply):
|
|
26
|
+
website-login log in to the quox.ai admin API once (password is not stored)
|
|
27
|
+
reissue <licenseId> band bare paid ids / add ids, re-signed on the website VM
|
|
28
|
+
catalogue-sign re-sign the product catalogue with the live key
|
|
29
|
+
See the quox-licensing skill (~/.claude/skills/quox-licensing/SKILL.md).
|
|
30
|
+
`;
|
|
31
|
+
|
|
32
|
+
const ID_LIST_RE = /^[a-z0-9][a-z0-9._-]{0,63}$/;
|
|
33
|
+
|
|
34
|
+
export function parseIdList(value, flag) {
|
|
35
|
+
if (value === undefined) return undefined;
|
|
36
|
+
const ids = String(value).split(',').map((s) => s.trim()).filter(Boolean);
|
|
37
|
+
for (const id of ids) {
|
|
38
|
+
if (!ID_LIST_RE.test(id)) throw new QuoxValidationError(`${flag}: invalid plugin id "${id}"`);
|
|
39
|
+
}
|
|
40
|
+
return ids;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function parseOrgs(value) {
|
|
44
|
+
if (value === undefined) return undefined;
|
|
45
|
+
const n = Number(value);
|
|
46
|
+
if (!Number.isInteger(n) || n < 1 || n > 1000) throw new QuoxValidationError('--orgs must be an integer between 1 and 1000');
|
|
47
|
+
return n;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function list(arr) {
|
|
51
|
+
return arr && arr.length ? arr.join(', ') : '(none)';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function registerLicenceCommands(program, ctx) {
|
|
55
|
+
const { client, auth } = ctx;
|
|
56
|
+
const fetchImpl = ctx.websiteFetch || globalThis.fetch;
|
|
57
|
+
const json = () => program.opts().output === 'json';
|
|
58
|
+
|
|
59
|
+
const licence = program
|
|
60
|
+
.command('licence')
|
|
61
|
+
.alias('license')
|
|
62
|
+
.description('Instance licence status, lease refresh and seat bands')
|
|
63
|
+
.addHelpText('after', OWNER_HELP)
|
|
64
|
+
.action(() => { licence.help(); });
|
|
65
|
+
|
|
66
|
+
licence
|
|
67
|
+
.command('status')
|
|
68
|
+
.description('Licence id, plan, lease timestamps, governed count, entitled list with seat usage')
|
|
69
|
+
.option('--plugin <id>', 'limit to one plugin')
|
|
70
|
+
.action(async (opts) => {
|
|
71
|
+
auth.requireAuth();
|
|
72
|
+
const ep = resolveEndpoint('setup.licensing.status');
|
|
73
|
+
const d = await client.get(ep.path, { service: ep.service });
|
|
74
|
+
const bands = d.org_bands || {};
|
|
75
|
+
const pick = (id) => !opts.plugin || id === opts.plugin;
|
|
76
|
+
const entitled = (d.entitlements || []).filter(pick).map((id) => {
|
|
77
|
+
const b = bands[id];
|
|
78
|
+
return { plugin: id, seats: b ? `${b.assigned_count}/${b.allowance}` : 'whole install' };
|
|
79
|
+
});
|
|
80
|
+
const out = {
|
|
81
|
+
licenses: (d.licenses || []).map((l) => ({
|
|
82
|
+
license_id: l.license_id,
|
|
83
|
+
plans: l.plans || [],
|
|
84
|
+
lease_issued_at: l.issued_at,
|
|
85
|
+
lease_expires_at: l.lease_expires_at,
|
|
86
|
+
licence_expires_at: l.expires_at,
|
|
87
|
+
})),
|
|
88
|
+
governed_count: Array.isArray(d.governed) ? d.governed.length : 0,
|
|
89
|
+
org_id: d.org_id || null,
|
|
90
|
+
entitled,
|
|
91
|
+
entitlement_errors: d.entitlement_errors || [],
|
|
92
|
+
renewal_health: d.renewal_health || null,
|
|
93
|
+
};
|
|
94
|
+
if (opts.plugin) out.governed = (d.governed || []).includes(opts.plugin);
|
|
95
|
+
if (json()) { console.log(JSON.stringify(out, null, 2)); return; }
|
|
96
|
+
formatDetail({
|
|
97
|
+
licence_id: out.licenses.map((l) => l.license_id).join(', ') || '(none)',
|
|
98
|
+
plan: out.licenses.flatMap((l) => l.plans).join(', ') || '(none)',
|
|
99
|
+
lease_issued: out.licenses.map((l) => l.lease_issued_at || '?').join(', ') || '(none)',
|
|
100
|
+
lease_expires: out.licenses.map((l) => l.lease_expires_at).join(', ') || '(none)',
|
|
101
|
+
governed: String(out.governed_count),
|
|
102
|
+
org_id: out.org_id || '(instance scoped)',
|
|
103
|
+
entitlement_errors: out.entitlement_errors.length
|
|
104
|
+
? out.entitlement_errors.map((e) => (e.entitlement ? `${e.entitlement}: ${e.error}` : JSON.stringify(e))).join('; ')
|
|
105
|
+
: '(none)',
|
|
106
|
+
}, { title: 'Licence status' });
|
|
107
|
+
formatOutput({
|
|
108
|
+
columns: ['plugin', 'seats'],
|
|
109
|
+
headers: ['Entitled plugin', 'Seats (used/cap)'],
|
|
110
|
+
rows: entitled,
|
|
111
|
+
}, { format: program.opts().output, emptyMessage: 'No entitlements.' });
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
licence
|
|
115
|
+
.command('refresh')
|
|
116
|
+
.description('Renew the licence lease now; prints old to new lease timestamps and entitlement diff')
|
|
117
|
+
.action(async () => {
|
|
118
|
+
auth.requireAuth();
|
|
119
|
+
const ep = resolveEndpoint('setup.licensing.refresh');
|
|
120
|
+
const d = await client.post(ep.path, { service: ep.service, body: {} });
|
|
121
|
+
if (json()) { console.log(JSON.stringify(d, null, 2)); return; }
|
|
122
|
+
const byId = new Map((d.before || []).map((l) => [l.license_id, l]));
|
|
123
|
+
for (const l of d.after || []) {
|
|
124
|
+
const old = byId.get(l.license_id);
|
|
125
|
+
printInfo(`${l.license_id}: lease expires ${old ? old.lease_expires_at : '(new)'} -> ${l.lease_expires_at}`);
|
|
126
|
+
}
|
|
127
|
+
for (const r of d.results || []) printInfo(`${r.license_id}: ${r.action}${r.reason ? ` (${r.reason})` : ''}`);
|
|
128
|
+
const diff = d.entitlement_diff || { added: [], removed: [] };
|
|
129
|
+
printInfo(`entitlements added: ${diff.added.join(', ') || '(none)'}`);
|
|
130
|
+
printInfo(`entitlements removed: ${diff.removed.join(', ') || '(none)'}`);
|
|
131
|
+
printSuccess('Lease refresh complete.');
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
licence
|
|
135
|
+
.command('seats')
|
|
136
|
+
.description('List a plugin band holders; --release-dead frees non-live holders (dry run unless --yes)')
|
|
137
|
+
.argument('<pluginId>', 'plugin id, e.g. notesq')
|
|
138
|
+
.option('--release-dead', 'release holders that are not live orgs (deleted orgs, user: ids)')
|
|
139
|
+
.option('--yes', 'actually release (otherwise dry run)')
|
|
140
|
+
.action(async (pluginId, opts) => {
|
|
141
|
+
auth.requireAuth();
|
|
142
|
+
const lep = resolveEndpoint('setup.licensing.seats.list', { pluginId });
|
|
143
|
+
const list = await client.get(lep.path, { service: lep.service });
|
|
144
|
+
let rel = null;
|
|
145
|
+
if (opts.releaseDead) {
|
|
146
|
+
const rep = resolveEndpoint('setup.licensing.seats.releaseDead', { pluginId });
|
|
147
|
+
rel = await client.post(rep.path, { service: rep.service, body: { dry_run: !opts.yes } });
|
|
148
|
+
}
|
|
149
|
+
if (json()) { console.log(JSON.stringify({ ...list, release: rel }, null, 2)); return; }
|
|
150
|
+
printInfo(`${pluginId}: ${list.holders.length} holder(s), cap ${list.allowance ?? '(no band)'}`);
|
|
151
|
+
formatOutput({
|
|
152
|
+
columns: ['id', 'name', 'status'],
|
|
153
|
+
headers: ['Holder', 'Org name', 'Status'],
|
|
154
|
+
rows: list.holders.map((h) => ({ id: h.id, name: h.name || '-', status: h.status })),
|
|
155
|
+
}, { format: program.opts().output, emptyMessage: 'No holders.' });
|
|
156
|
+
if (rel) {
|
|
157
|
+
if (rel.dry_run) {
|
|
158
|
+
printInfo(`DRY RUN: would release ${rel.dead.length} non-live holder(s): ${rel.dead.map((d) => d.id).join(', ') || '(none)'}. Re-run with --yes to release.`);
|
|
159
|
+
} else {
|
|
160
|
+
printSuccess(`Released ${rel.released.length} non-live holder(s): ${rel.released.join(', ') || '(none)'}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// ---------------- Phase 2: owner-only, website admin API ----------------
|
|
166
|
+
|
|
167
|
+
licence
|
|
168
|
+
.command('website-login')
|
|
169
|
+
.description('Log in to the quox.ai website admin API for reissue/catalogue-sign (stores the session cookie only, 0600)')
|
|
170
|
+
.option('--website-profile <name>', 'website session profile', DEFAULT_WEBSITE_PROFILE)
|
|
171
|
+
.option('--url <url>', 'website base URL', DEFAULT_WEBSITE_URL)
|
|
172
|
+
.option('--username <name>', 'website admin username')
|
|
173
|
+
.option('--password-stdin', 'read the password from stdin (one line) instead of prompting')
|
|
174
|
+
.action(async (opts) => {
|
|
175
|
+
normaliseWebsiteUrl(opts.url); // refuse a bad or plain-http URL before asking for a password
|
|
176
|
+
let { username } = opts;
|
|
177
|
+
let password;
|
|
178
|
+
if (opts.passwordStdin) {
|
|
179
|
+
const chunks = [];
|
|
180
|
+
for await (const c of process.stdin) chunks.push(Buffer.from(c));
|
|
181
|
+
password = Buffer.concat(chunks).toString('utf8').split('\n')[0].trim();
|
|
182
|
+
}
|
|
183
|
+
if (!username || !password) {
|
|
184
|
+
const { default: inquirer } = await import('inquirer');
|
|
185
|
+
if (!username) username = (await inquirer.prompt([{ type: 'input', name: 'u', message: 'Website admin username:' }])).u;
|
|
186
|
+
if (!password) password = (await inquirer.prompt([{ type: 'password', name: 'p', message: 'Password:', mask: '*' }])).p;
|
|
187
|
+
}
|
|
188
|
+
if (!username || !password) throw new QuoxValidationError('username and password are required');
|
|
189
|
+
const session = await websiteLogin({ url: opts.url, username, password, fetchImpl });
|
|
190
|
+
password = null;
|
|
191
|
+
saveWebsiteSession(opts.websiteProfile, session);
|
|
192
|
+
if (json()) { console.log(JSON.stringify({ profile: opts.websiteProfile, url: session.url, username: session.username, must_change_password: session.must_change_password }, null, 2)); return; }
|
|
193
|
+
printSuccess(`Website session saved for ${session.username} at ${session.url} (profile ${opts.websiteProfile}, valid ~24h). Password not stored.`);
|
|
194
|
+
if (session.must_change_password) printWarning('This admin must change their password on the website before licence operations will be allowed (HTTP 403 until then).');
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
licence
|
|
198
|
+
.command('website-logout')
|
|
199
|
+
.description('End the website admin session and delete the stored cookie')
|
|
200
|
+
.option('--website-profile <name>', 'website session profile', DEFAULT_WEBSITE_PROFILE)
|
|
201
|
+
.action(async (opts) => {
|
|
202
|
+
if (getWebsiteSession(opts.websiteProfile)) {
|
|
203
|
+
try { await websitePost(opts.websiteProfile, '/api/admin/logout', {}, { fetchImpl }); } catch { /* session may already be dead */ }
|
|
204
|
+
}
|
|
205
|
+
const had = clearWebsiteSession(opts.websiteProfile);
|
|
206
|
+
printSuccess(had ? `Website session ${opts.websiteProfile} removed.` : `No website session ${opts.websiteProfile} was stored.`);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
licence
|
|
210
|
+
.command('reissue')
|
|
211
|
+
.description('Owner-only: band bare paid ids and add ids on a licence record, re-signed on the website (dry run unless --apply)')
|
|
212
|
+
.argument('<licenseId>', 'licence id, e.g. lic_abc123')
|
|
213
|
+
.option('--orgs <n>', 'org band for every banded/added id (default 10)')
|
|
214
|
+
.option('--add <ids>', 'comma-separated ids to ADD as <id>@orgs=N')
|
|
215
|
+
.option('--band <ids>', 'comma-separated ids already present to FORCE-band')
|
|
216
|
+
.option('--apply', 'actually reissue (otherwise dry run, nothing changes)')
|
|
217
|
+
.option('--website-profile <name>', 'website session profile', DEFAULT_WEBSITE_PROFILE)
|
|
218
|
+
.action(async (licenseId, opts) => {
|
|
219
|
+
const body = { dryRun: !opts.apply };
|
|
220
|
+
const orgs = parseOrgs(opts.orgs);
|
|
221
|
+
if (orgs !== undefined) body.orgs = orgs;
|
|
222
|
+
const add = parseIdList(opts.add, '--add');
|
|
223
|
+
if (add) body.add = add;
|
|
224
|
+
const band = parseIdList(opts.band, '--band');
|
|
225
|
+
if (band) body.band = band;
|
|
226
|
+
|
|
227
|
+
const d = await websitePost(opts.websiteProfile, `/api/admin/licenses/${encodeURIComponent(licenseId)}/reissue`, body, { fetchImpl });
|
|
228
|
+
if (json()) { console.log(JSON.stringify(d, null, 2)); return; }
|
|
229
|
+
|
|
230
|
+
printInfo(`Licence ${d.license_id}${d.customer_email ? ` (${d.customer_email})` : ''}: max_servers ${d.max_servers ?? '?'}, expires ${d.expires_at || 'never'}`);
|
|
231
|
+
const removed = new Set(d.diff?.removed || []);
|
|
232
|
+
const added = new Set(d.diff?.added || []);
|
|
233
|
+
for (const e of d.current || []) console.log(` ${removed.has(e) ? '-' : ' '} ${e}`);
|
|
234
|
+
for (const e of d.next || []) if (added.has(e)) console.log(` + ${e}`);
|
|
235
|
+
if (d.unknown_ids?.length) printWarning(`Not issuable (unknown to the signed catalogue), apply will be refused: ${d.unknown_ids.join(', ')}`);
|
|
236
|
+
|
|
237
|
+
if (!d.changed) { printSuccess('No change needed: the licence already has these entitlements.'); return; }
|
|
238
|
+
if (d.dry_run) { printInfo('DRY RUN: nothing changed. Re-run with --apply to reissue.'); return; }
|
|
239
|
+
printSuccess(`Reissued ${d.license_id}. Audit/backup id: ${d.backup_id}`);
|
|
240
|
+
printInfo('Instances pick this up on their next lease renewal (~6h). To pull it now, on the instance: quox licence refresh');
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
licence
|
|
244
|
+
.command('catalogue-sign')
|
|
245
|
+
.alias('catalog-sign')
|
|
246
|
+
.description('Owner-only: re-sign the product catalogue with the live key on the website (dry run unless --apply)')
|
|
247
|
+
.option('--apply', 'actually sign and publish to the website API (otherwise dry run)')
|
|
248
|
+
.option('--out <path>', 'after --apply, save the signed catalogue (public artifact) here for committing')
|
|
249
|
+
.option('--website-profile <name>', 'website session profile', DEFAULT_WEBSITE_PROFILE)
|
|
250
|
+
.action(async (opts) => {
|
|
251
|
+
if (opts.out && !opts.apply) throw new QuoxValidationError('--out is only meaningful with --apply');
|
|
252
|
+
const d = await websitePost(opts.websiteProfile, '/api/admin/catalog/sign', { dryRun: !opts.apply }, { fetchImpl });
|
|
253
|
+
if (opts.out && d.artifact) writeFileSync(opts.out, JSON.stringify(d.artifact, null, 2) + '\n', 'utf8');
|
|
254
|
+
if (json()) {
|
|
255
|
+
const { artifact, ...rest } = d;
|
|
256
|
+
console.log(JSON.stringify({ ...rest, artifact_saved_to: opts.out && artifact ? opts.out : null }, null, 2));
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
const p = d.published || {};
|
|
260
|
+
formatDetail({
|
|
261
|
+
published: p.exists ? `${p.verified ? 'verified' : 'NOT VERIFIED'}, sha256 ${p.sha256 ? p.sha256.slice(0, 16) : '?'}..., ${p.generated_at || '?'}` : '(none)',
|
|
262
|
+
products: `${p.product_count ?? 0} -> ${d.candidate?.product_count ?? '?'}`,
|
|
263
|
+
governed: `${p.governed_count ?? 0} -> ${d.candidate?.governed_count ?? '?'}`,
|
|
264
|
+
products_added: list(d.diff?.products_added),
|
|
265
|
+
products_removed: list(d.diff?.products_removed),
|
|
266
|
+
products_changed: list(d.diff?.products_changed),
|
|
267
|
+
governed_added: list(d.diff?.governed_added),
|
|
268
|
+
governed_removed: list(d.diff?.governed_removed),
|
|
269
|
+
signing_key: d.signing ? (d.signing.trusted ? `trusted (${d.signing.trusted_key_prefix}...)` : (d.signing.configured ? 'NOT the trusted key: apply will be refused' : 'not configured: apply will be refused')) : '?',
|
|
270
|
+
}, { title: d.dry_run ? 'Catalogue sign (DRY RUN)' : 'Catalogue sign' });
|
|
271
|
+
if (d.diff?.governed_added?.length) {
|
|
272
|
+
printWarning(`Newly governed: ${d.diff.governed_added.join(', ')}. Reissue the owner licence with these ids (quox licence reissue <id> --add ...) and refresh its lease BEFORE instances install this catalogue, or they lock out.`);
|
|
273
|
+
}
|
|
274
|
+
if (!d.changed) { printSuccess('No change: the published catalogue already matches.'); return; }
|
|
275
|
+
if (d.dry_run) { printInfo('DRY RUN: nothing signed. Re-run with --apply to sign.'); return; }
|
|
276
|
+
printSuccess(`Catalogue signed. sha256 ${d.sha256}, governed ${d.governed_count}, audit id ${d.audit_id}`);
|
|
277
|
+
if (opts.out) printInfo(`Signed catalogue saved to ${opts.out}.`);
|
|
278
|
+
printInfo('The website container copy does not survive a rebuild: commit the artifact to quox-website api/data/signed-catalog.json and quox-dashboard services/auth/data/signed-catalog.json (use --out to save it).');
|
|
279
|
+
});
|
|
280
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `quox linkedin` — LinkedIn plugin verbs, LI-OVERHAUL P9 (X CLI parity).
|
|
3
|
+
*
|
|
4
|
+
* quox linkedin post "<text>" --as <author_urn> [--visibility PUBLIC|CONNECTIONS] [--link <url>] [--image-url <https url>]
|
|
5
|
+
* quox linkedin comment <post_urn> "<text>" --as <actor_urn>
|
|
6
|
+
* quox linkedin posts <author_urn> [--count <n>]
|
|
7
|
+
* quox linkedin org <org_id>
|
|
8
|
+
*
|
|
9
|
+
* Both write verbs execute the linkedin connector tools through the governed
|
|
10
|
+
* collector endpoint (POST /api/v1/tools/execute -> connector bridge). The
|
|
11
|
+
* bridge gates them on their requiresApproval flag: each files an owner
|
|
12
|
+
* approval card and returns immediately with
|
|
13
|
+
* { pending:true, status:'awaiting_approval', approvalId, inbox_item_id }.
|
|
14
|
+
* The actual LinkedIn API write only happens once the owner approves via the
|
|
15
|
+
* dashboard inbox or `quox inbox respond <id> --action approve`. Reads
|
|
16
|
+
* (posts, org) execute directly; LinkedIn reads carry no per-call dollar
|
|
17
|
+
* cost (unlike X), only LinkedIn's own daily program quotas.
|
|
18
|
+
*
|
|
19
|
+
* URNs are explicit by design: LinkedIn addresses everything by URN
|
|
20
|
+
* (urn:li:person:xxx member, urn:li:organization:12345 org page) and the
|
|
21
|
+
* CLI has no org-scoped plugin-config lookup yet, so --as is required on
|
|
22
|
+
* writes rather than silently guessed.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { formatDetail, printSuccess, printInfo } from '../core/output.js';
|
|
26
|
+
import { resolveEndpoint } from '../api/endpoints.js';
|
|
27
|
+
import { resolveOrgId } from './helpers.js';
|
|
28
|
+
|
|
29
|
+
const MAX_POST_CHARS = 3000;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Execute one linkedin connector tool via the governed collector endpoint.
|
|
33
|
+
* A pending-approval response is a SUCCESS shape here (the card was filed);
|
|
34
|
+
* only a non-pending success:false is an error.
|
|
35
|
+
*/
|
|
36
|
+
async function executeLinkedInTool({ client, auth, program }, toolName, input) {
|
|
37
|
+
auth.requireAuth();
|
|
38
|
+
const orgId = resolveOrgId(auth, program);
|
|
39
|
+
const ep = resolveEndpoint('v1.tools.execute');
|
|
40
|
+
const body = { tool_name: toolName, input, org_id: orgId };
|
|
41
|
+
const data = await client.post(ep.path, { service: ep.service, body });
|
|
42
|
+
if (data && data.success === false && !data.pending) {
|
|
43
|
+
throw new Error(`${toolName} via POST ${ep.path} returned success:false: ${data.error || 'unknown error'}`);
|
|
44
|
+
}
|
|
45
|
+
return data;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function printAwaitingApproval(action, data) {
|
|
49
|
+
const itemRef = data.inbox_item_id || data.approvalId || 'unknown';
|
|
50
|
+
printSuccess(`${action} filed for owner approval — inbox item ${itemRef} (awaiting approval). Nothing publishes until an approver says yes.`);
|
|
51
|
+
if (data.inbox_item_id) {
|
|
52
|
+
printInfo(`Review and approve: quox inbox get ${data.inbox_item_id} (or: quox inbox respond ${data.inbox_item_id} --action approve)`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function registerLinkedInCommands(program, ctx) {
|
|
57
|
+
const { client, auth } = ctx;
|
|
58
|
+
|
|
59
|
+
const linkedin = program
|
|
60
|
+
.command('linkedin')
|
|
61
|
+
.description('LinkedIn publishing: governed, HITL-approved via HITL Inbox');
|
|
62
|
+
|
|
63
|
+
// ── linkedin post ────────────────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
linkedin
|
|
66
|
+
.command('post')
|
|
67
|
+
.description('Draft a LinkedIn post. Files an owner approval card and returns immediately; never posts directly.')
|
|
68
|
+
.argument('<text>', `post text, max ${MAX_POST_CHARS} characters`)
|
|
69
|
+
.requiredOption('--as <author_urn>', 'author URN (urn:li:person:xxx or urn:li:organization:12345)')
|
|
70
|
+
.option('--visibility <visibility>', 'PUBLIC or CONNECTIONS', 'PUBLIC')
|
|
71
|
+
.option('--link <url>', 'attach a link/article URL')
|
|
72
|
+
.option('--image-url <url>', 'attach an image from an https URL (max 10MB); uploaded to LinkedIn server-side before the card is filed, so the approver sees the finished attachment')
|
|
73
|
+
.action(async (text, options) => {
|
|
74
|
+
if (text.length > MAX_POST_CHARS) {
|
|
75
|
+
throw new Error(`Text is ${text.length} characters, over the ${MAX_POST_CHARS} character limit.`);
|
|
76
|
+
}
|
|
77
|
+
if (!['PUBLIC', 'CONNECTIONS'].includes(options.visibility)) {
|
|
78
|
+
throw new Error(`--visibility must be PUBLIC or CONNECTIONS, got ${options.visibility}`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const input = { author: options.as, text, visibility: options.visibility };
|
|
82
|
+
if (options.link) input.link_url = options.link;
|
|
83
|
+
|
|
84
|
+
if (options.imageUrl) {
|
|
85
|
+
const media = await executeLinkedInTool({ client, auth, program }, 'linkedin_upload_media', {
|
|
86
|
+
owner: options.as,
|
|
87
|
+
image_url: options.imageUrl,
|
|
88
|
+
});
|
|
89
|
+
const urn = media?.result?.image_urn;
|
|
90
|
+
if (!urn) {
|
|
91
|
+
throw new Error(`Image upload did not return an image URN: ${JSON.stringify(media).slice(0, 200)}`);
|
|
92
|
+
}
|
|
93
|
+
printInfo(`Image uploaded (${media.result.bytes} bytes) — ${urn}`);
|
|
94
|
+
input.media_id = urn;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const data = await executeLinkedInTool({ client, auth, program }, 'linkedin_publish_post', input);
|
|
98
|
+
|
|
99
|
+
if (program.opts().output === 'json') {
|
|
100
|
+
formatDetail(data, { format: 'json' });
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
printAwaitingApproval('Post', data);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// ── linkedin comment ─────────────────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
linkedin
|
|
109
|
+
.command('comment')
|
|
110
|
+
.description('Draft a comment on a LinkedIn post. Files an owner approval card and returns immediately; never comments directly.')
|
|
111
|
+
.argument('<post_urn>', 'URN of the post to comment on (e.g. urn:li:share:12345)')
|
|
112
|
+
.argument('<text>', 'comment text')
|
|
113
|
+
.requiredOption('--as <actor_urn>', 'actor URN posting the comment (person or organisation)')
|
|
114
|
+
.action(async (postUrn, text, options) => {
|
|
115
|
+
const data = await executeLinkedInTool({ client, auth, program }, 'linkedin_create_comment', {
|
|
116
|
+
post_urn: postUrn,
|
|
117
|
+
actor: options.as,
|
|
118
|
+
text,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
if (program.opts().output === 'json') {
|
|
122
|
+
formatDetail(data, { format: 'json' });
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
printAwaitingApproval('Comment', data);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// ── linkedin posts (read) ────────────────────────────────────────────────
|
|
129
|
+
|
|
130
|
+
linkedin
|
|
131
|
+
.command('posts')
|
|
132
|
+
.description('List recent posts for a member or organisation (read-only, no approval needed).')
|
|
133
|
+
.argument('<author_urn>', 'author URN (person or organisation)')
|
|
134
|
+
.option('--count <n>', 'number of posts (max 50)', '20')
|
|
135
|
+
.action(async (authorUrn, options) => {
|
|
136
|
+
const count = Math.min(parseInt(options.count, 10) || 20, 50);
|
|
137
|
+
const data = await executeLinkedInTool({ client, auth, program }, 'linkedin_get_posts', {
|
|
138
|
+
author: authorUrn,
|
|
139
|
+
count,
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
if (program.opts().output === 'json') {
|
|
143
|
+
formatDetail(data, { format: 'json' });
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
const posts = data.result || [];
|
|
147
|
+
if (posts.length === 0) {
|
|
148
|
+
printInfo('No posts returned.');
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
for (const p of posts) {
|
|
152
|
+
console.log(`${p.id} ${p.created_at || ''} ${p.lifecycle_state || ''}`);
|
|
153
|
+
console.log(` ${String(p.text || '').replace(/\n/g, '\n ')}`);
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// ── linkedin org (read) ──────────────────────────────────────────────────
|
|
158
|
+
|
|
159
|
+
linkedin
|
|
160
|
+
.command('org')
|
|
161
|
+
.description('Show a LinkedIn organisation page: name, follower count, website (read-only). Also the cheapest live credential check.')
|
|
162
|
+
.argument('<org_id>', 'numeric organisation id (e.g. 12345)')
|
|
163
|
+
.action(async (orgId) => {
|
|
164
|
+
const data = await executeLinkedInTool({ client, auth, program }, 'linkedin_get_org_info', { org_id: orgId });
|
|
165
|
+
|
|
166
|
+
if (program.opts().output === 'json') {
|
|
167
|
+
formatDetail(data, { format: 'json' });
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
const org = data.result || {};
|
|
171
|
+
printSuccess(`${org.name || 'Unknown'} (urn:li:organization:${org.id})`);
|
|
172
|
+
if (org.vanity_name) printInfo(`linkedin.com/company/${org.vanity_name}`);
|
|
173
|
+
// followers can be null when networkSizes is unavailable — say unknown,
|
|
174
|
+
// never a fake 0.
|
|
175
|
+
printInfo(`Followers: ${org.followers == null ? 'unknown' : org.followers.toLocaleString()}`);
|
|
176
|
+
if (org.website) printInfo(`Website: ${org.website}`);
|
|
177
|
+
});
|
|
178
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LLM routing policy commands: view/set/dry-run-test the org's execution
|
|
3
|
+
* policy (which providers/models agents are allowed to route to).
|
|
4
|
+
*
|
|
5
|
+
* quox llm-policy get|set|test
|
|
6
|
+
*
|
|
7
|
+
* Backend: services/auth/routes/llm-routing-policies.js, mounted at plain
|
|
8
|
+
* `/api` (org-scoped in the URL): GET/PUT `/api/orgs/:orgId/llm-routing-policy`,
|
|
9
|
+
* POST `/api/orgs/:orgId/llm-routing-policy/test`. See
|
|
10
|
+
* cli/lib/api/endpoints/llm-policy.js for the full verified response shapes.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { readFileSync } from 'node:fs';
|
|
14
|
+
import { formatDetail, printSuccess, printWarning } from '../core/output.js';
|
|
15
|
+
import { resolveOrgId, parseJSON } from './helpers.js';
|
|
16
|
+
import { resolveEndpoint } from '../api/endpoints.js';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Resolve a policy object from --file or --policy. Throws if neither given
|
|
20
|
+
* or the input isn't a JSON object.
|
|
21
|
+
*/
|
|
22
|
+
function resolvePolicy(options) {
|
|
23
|
+
let raw;
|
|
24
|
+
if (options.file) {
|
|
25
|
+
try {
|
|
26
|
+
raw = readFileSync(options.file, 'utf-8');
|
|
27
|
+
} catch (err) {
|
|
28
|
+
throw new Error(`Could not read ${options.file}: ${err.message}`);
|
|
29
|
+
}
|
|
30
|
+
} else if (options.policy !== undefined) {
|
|
31
|
+
raw = options.policy;
|
|
32
|
+
} else {
|
|
33
|
+
throw new Error('Policy input is required. Use --file <path> or --policy <json>.');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const policy = parseJSON(raw, options.file ? '--file contents' : '--policy');
|
|
37
|
+
if (!policy || typeof policy !== 'object' || Array.isArray(policy)) {
|
|
38
|
+
throw new Error('Policy must be a JSON object.');
|
|
39
|
+
}
|
|
40
|
+
return policy;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function registerLlmPolicyCommands(program, ctx) {
|
|
44
|
+
const { client, auth } = ctx;
|
|
45
|
+
|
|
46
|
+
const llmPolicy = program
|
|
47
|
+
.command('llm-policy')
|
|
48
|
+
.description('View, set, and dry-run-validate the org LLM routing policy');
|
|
49
|
+
|
|
50
|
+
// ── llm-policy get ──────────────────────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
llmPolicy
|
|
53
|
+
.command('get')
|
|
54
|
+
.description('Show the effective LLM routing policy for the active org')
|
|
55
|
+
.action(async () => {
|
|
56
|
+
auth.requireAuth();
|
|
57
|
+
const orgId = resolveOrgId(auth, program);
|
|
58
|
+
const data = await client.get(resolveEndpoint('llm-policy.get', { orgId }).path, {
|
|
59
|
+
service: resolveEndpoint('llm-policy.get', { orgId }).service,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
if (data.source === 'seed') {
|
|
63
|
+
printWarning('No org-specific policy saved — showing the built-in seed policy.');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
formatDetail(data.policy, {
|
|
67
|
+
format: program.opts().output,
|
|
68
|
+
title: `LLM Routing Policy (source: ${data.source})`,
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// ── llm-policy set ──────────────────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
llmPolicy
|
|
75
|
+
.command('set')
|
|
76
|
+
.description('Save a new LLM routing policy for the active org (requires ORG_MANAGE_SETTINGS)')
|
|
77
|
+
.option('--file <path>', 'path to a JSON file containing the policy object')
|
|
78
|
+
.option('--policy <json>', 'inline JSON policy object')
|
|
79
|
+
.action(async (options) => {
|
|
80
|
+
auth.requireAuth();
|
|
81
|
+
const orgId = resolveOrgId(auth, program);
|
|
82
|
+
const policy = resolvePolicy(options);
|
|
83
|
+
|
|
84
|
+
const data = await client.put(resolveEndpoint('llm-policy.set', { orgId }).path, {
|
|
85
|
+
service: resolveEndpoint('llm-policy.set', { orgId }).service,
|
|
86
|
+
body: { policy },
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
printSuccess('LLM routing policy saved.');
|
|
90
|
+
formatDetail(data.policy, { format: program.opts().output });
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// ── llm-policy test ─────────────────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
llmPolicy
|
|
96
|
+
.command('test')
|
|
97
|
+
.description('Dry-run validate a policy without saving it')
|
|
98
|
+
.option('--file <path>', 'path to a JSON file containing the policy object')
|
|
99
|
+
.option('--policy <json>', 'inline JSON policy object')
|
|
100
|
+
.action(async (options) => {
|
|
101
|
+
auth.requireAuth();
|
|
102
|
+
const orgId = resolveOrgId(auth, program);
|
|
103
|
+
const policy = resolvePolicy(options);
|
|
104
|
+
|
|
105
|
+
const data = await client.post(resolveEndpoint('llm-policy.test', { orgId }).path, {
|
|
106
|
+
service: resolveEndpoint('llm-policy.test', { orgId }).service,
|
|
107
|
+
body: { policy },
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
if (data.valid) {
|
|
111
|
+
printSuccess('Policy is valid.');
|
|
112
|
+
} else {
|
|
113
|
+
printWarning(`Policy is invalid (${(data.errors || []).length} error(s)).`);
|
|
114
|
+
}
|
|
115
|
+
formatDetail(data, { format: program.opts().output });
|
|
116
|
+
});
|
|
117
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LLM commands: provider status incl. degraded state and the failover target.
|
|
3
|
+
*
|
|
4
|
+
* quox llm status
|
|
5
|
+
*
|
|
6
|
+
* Backend: collector `GET /chat/status` (same endpoint as `quox chat-status`,
|
|
7
|
+
* key 'chat.status.list'). This is the curated LLM view of it: it surfaces the
|
|
8
|
+
* failover fields shipped with the LLM failover chain work (`degraded`,
|
|
9
|
+
* `degraded_reason`, `degraded_until`, `fallback_hint`) and warns loudly when
|
|
10
|
+
* the primary provider is in a failover cooldown. Routing policy itself lives
|
|
11
|
+
* under `quox llm-policy`.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { formatDetail, printWarning } from '../core/output.js';
|
|
15
|
+
import { resolveEndpoint } from '../api/endpoints.js';
|
|
16
|
+
|
|
17
|
+
export function registerLlmCommands(program, ctx) {
|
|
18
|
+
const { client } = ctx;
|
|
19
|
+
|
|
20
|
+
const llm = program
|
|
21
|
+
.command('llm')
|
|
22
|
+
.description('LLM provider status and failover');
|
|
23
|
+
|
|
24
|
+
llm
|
|
25
|
+
.command('status')
|
|
26
|
+
.description('Show LLM provider status, degraded state, and the failover target')
|
|
27
|
+
.action(async () => {
|
|
28
|
+
// No requireAuth: the route is public on collectors that allowlist it,
|
|
29
|
+
// but the token is attached when logged in (the dev gate requires a JWT).
|
|
30
|
+
const data = await client.get(resolveEndpoint('chat.status.list').path, { service: resolveEndpoint('chat.status.list').service });
|
|
31
|
+
|
|
32
|
+
if (data.degraded) {
|
|
33
|
+
const reason = data.degraded_reason ? ` (${data.degraded_reason})` : '';
|
|
34
|
+
const fallback = data.fallback_hint
|
|
35
|
+
? ` Failing over to ${data.fallback_hint.provider}:${data.fallback_hint.model}.`
|
|
36
|
+
: '';
|
|
37
|
+
printWarning(`Primary provider degraded${reason}.${fallback}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
formatDetail(data, {
|
|
41
|
+
format: program.opts().output,
|
|
42
|
+
title: 'LLM Status',
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
}
|