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,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inbox / HITL commands: list, get, respond
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { formatOutput, formatDetail, printSuccess } from '../core/output.js';
|
|
6
|
+
import { resolveOrgId } from './helpers.js';
|
|
7
|
+
import { resolveEndpoint } from '../api/endpoints.js';
|
|
8
|
+
import { getApprovalContext, formatApprovalContext, approvalListColumns } from '../core/approvalContext.js';
|
|
9
|
+
|
|
10
|
+
export function registerInboxCommands(program, ctx) {
|
|
11
|
+
const { client, auth } = ctx;
|
|
12
|
+
|
|
13
|
+
// An agent hands back a tool approvalId (tool_approvals.approval_id), which is
|
|
14
|
+
// stored on the inbox card as aee_correlation_id, not its item id. No server
|
|
15
|
+
// lookup-by-correlation route exists, so on a 404 we scan the org's approval
|
|
16
|
+
// cards (all statuses) client-side. Falls back to the id as given.
|
|
17
|
+
async function resolveItemId(orgId, id) {
|
|
18
|
+
const direct = resolveEndpoint('inbox.get', { orgId, itemId: id });
|
|
19
|
+
try {
|
|
20
|
+
await client.get(direct.path, { service: direct.service });
|
|
21
|
+
return id;
|
|
22
|
+
} catch (err) {
|
|
23
|
+
if (!err || err.status !== 404) throw err;
|
|
24
|
+
const list = resolveEndpoint('inbox.list', { orgId });
|
|
25
|
+
let data;
|
|
26
|
+
try {
|
|
27
|
+
data = await client.get(list.path, { service: list.service, query: { status: 'all', kind: 'approval', limit: 200 } });
|
|
28
|
+
} catch { data = null; }
|
|
29
|
+
const items = (data && (data.items || data)) || [];
|
|
30
|
+
const hit = Array.isArray(items) && items.find((it) => it.aee_correlation_id === id || (it.payload && it.payload.approval_id === id));
|
|
31
|
+
if (hit) return hit.id;
|
|
32
|
+
err.message += `\nHint: ${id} is not an inbox item id, and no approval card in your recent approvals carries it as an approvalId. `
|
|
33
|
+
+ 'Try `quox inbox list --status all` to find the card.';
|
|
34
|
+
throw err;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const inbox = program
|
|
39
|
+
.command('inbox')
|
|
40
|
+
.description('Manage inbox and approval requests');
|
|
41
|
+
|
|
42
|
+
inbox
|
|
43
|
+
.command('list')
|
|
44
|
+
.description('List inbox items')
|
|
45
|
+
.option('--status <status>', 'filter: one status, a comma list (resolved,cancelled,expired), or all (includes answered approvals). Default: active only')
|
|
46
|
+
.option('--decision <decision>', 'with resolved/all: approved or rejected')
|
|
47
|
+
.option('--page <n>', 'page number (newest first)')
|
|
48
|
+
.option('--priority <priority>', 'filter: low, normal, high, urgent')
|
|
49
|
+
.option('--awaiting-human', 'org-wide: approvals waiting for a human (see-only, cannot act)')
|
|
50
|
+
.option('--limit <n>', 'max results', '50')
|
|
51
|
+
.action(async (options) => {
|
|
52
|
+
auth.requireAuth();
|
|
53
|
+
const orgId = resolveOrgId(auth, program);
|
|
54
|
+
const query = { limit: options.limit };
|
|
55
|
+
if (options.status) query.status = options.status;
|
|
56
|
+
if (options.decision) query.decision = options.decision;
|
|
57
|
+
if (options.page) query.page = options.page;
|
|
58
|
+
if (options.priority) query.priority = options.priority;
|
|
59
|
+
if (options.awaitingHuman) query.awaiting_human = 'true';
|
|
60
|
+
const data = await client.get(resolveEndpoint('inbox.list', { orgId }).path, { service: resolveEndpoint('inbox.list', { orgId }).service, query });
|
|
61
|
+
// HITL-CONTEXT: Source + Reason columns from payload.approval_context so a
|
|
62
|
+
// stale approval still says who asked and why without opening it.
|
|
63
|
+
const items = (data.items || data || []).map((it) => ({ ...it, ...approvalListColumns(it) }));
|
|
64
|
+
formatOutput({
|
|
65
|
+
columns: ['id', 'kind', 'priority', 'title', 'source', 'reason', 'status', 'created_at'],
|
|
66
|
+
headers: ['ID', 'Kind', 'Priority', 'Title', 'Source', 'Reason', 'Status', 'Created'],
|
|
67
|
+
rows: items,
|
|
68
|
+
}, { format: program.opts().output });
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
inbox
|
|
72
|
+
.command('get')
|
|
73
|
+
.alias('show')
|
|
74
|
+
.description('Get inbox item details (alias: show), including the approval context: source, on behalf of, reason, method, why approval')
|
|
75
|
+
.argument('<itemId>', 'inbox item ID, or a tool approvalId from an agent')
|
|
76
|
+
.action(async (givenId) => {
|
|
77
|
+
auth.requireAuth();
|
|
78
|
+
const orgId = resolveOrgId(auth, program);
|
|
79
|
+
const itemId = await resolveItemId(orgId, givenId);
|
|
80
|
+
const data = await client.get(resolveEndpoint('inbox.get', { orgId, itemId }).path, { service: resolveEndpoint('inbox.get', { orgId, itemId }).service });
|
|
81
|
+
const item = data.item || data;
|
|
82
|
+
const format = program.opts().output;
|
|
83
|
+
const context = getApprovalContext(item);
|
|
84
|
+
if (context && format !== 'json') {
|
|
85
|
+
// Lead with the context block: it is what a human needs to decide.
|
|
86
|
+
console.log(formatApprovalContext(context));
|
|
87
|
+
console.log('');
|
|
88
|
+
const { payload, ...rest } = item;
|
|
89
|
+
const { approval_context: _omit, ...payloadRest } = payload || {};
|
|
90
|
+
formatDetail({ ...rest, payload: payloadRest }, { format });
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
formatDetail(item, { format });
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
inbox
|
|
97
|
+
.command('respond')
|
|
98
|
+
.description('Respond to an inbox item')
|
|
99
|
+
.argument('<itemId>', 'inbox item ID, or a tool approvalId from an agent')
|
|
100
|
+
.requiredOption('--action <action>', 'action: approve, reject, defer, resolve')
|
|
101
|
+
.option('--comment <text>', 'response comment')
|
|
102
|
+
.action(async (givenId, options) => {
|
|
103
|
+
auth.requireAuth();
|
|
104
|
+
const orgId = resolveOrgId(auth, program);
|
|
105
|
+
const itemId = await resolveItemId(orgId, givenId);
|
|
106
|
+
await client.post(resolveEndpoint('inbox.respond', { orgId, itemId }).path, {
|
|
107
|
+
service: resolveEndpoint('inbox.respond', { orgId, itemId }).service,
|
|
108
|
+
body: { action: options.action, comment: options.comment },
|
|
109
|
+
});
|
|
110
|
+
printSuccess(`Item ${options.action}ed.`);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
inbox
|
|
114
|
+
.command('stats')
|
|
115
|
+
.description('Show inbox statistics')
|
|
116
|
+
.action(async () => {
|
|
117
|
+
auth.requireAuth();
|
|
118
|
+
const orgId = resolveOrgId(auth, program);
|
|
119
|
+
const data = await client.get(resolveEndpoint('inbox.stats.list', { orgId }).path, { service: resolveEndpoint('inbox.stats.list', { orgId }).service });
|
|
120
|
+
formatDetail(data.stats || data, { format: program.opts().output, title: 'Inbox Stats' });
|
|
121
|
+
});
|
|
122
|
+
}
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `quox instagram` — Instagram plugin verbs, IG-3 (facebook CLI parity).
|
|
3
|
+
*
|
|
4
|
+
* quox instagram publish "<caption>" --account <ig_user_id> (--image <https url> | --video <https url>) [--reel]
|
|
5
|
+
* quox instagram accounts [--page <page_id>]
|
|
6
|
+
* quox instagram media --account <ig_user_id> [--limit <n>]
|
|
7
|
+
* quox instagram insights (--media <media_id> | --account <ig_user_id>) [--metrics <a,b,c>] [--period <period>]
|
|
8
|
+
* quox instagram comments --media <media_id> [--limit <n>]
|
|
9
|
+
* quox instagram reply <comment_id> "<message>"
|
|
10
|
+
* quox instagram quota --account <ig_user_id>
|
|
11
|
+
*
|
|
12
|
+
* The write verbs (publish, reply) execute the instagram connector tool
|
|
13
|
+
* through the governed collector endpoint (POST /api/v1/tools/execute ->
|
|
14
|
+
* connector bridge). The bridge gates them on requiresApproval: it files an
|
|
15
|
+
* owner approval card and returns immediately with
|
|
16
|
+
* { pending:true, status:'awaiting_approval', approvalId, inbox_item_id }.
|
|
17
|
+
* The actual Meta Graph API write (create-container -> poll -> publish, or
|
|
18
|
+
* the comment reply) only happens once the owner approves via the dashboard
|
|
19
|
+
* inbox or `quox inbox respond <id> --action approve`.
|
|
20
|
+
* Reads (accounts, media, insights, comments, quota) execute directly.
|
|
21
|
+
*
|
|
22
|
+
* Instagram is its own plugin on the SAME shared 'meta' vault credential as
|
|
23
|
+
* facebook_pages; there is no separate Instagram credential to configure.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { formatDetail, printSuccess, printInfo } from '../core/output.js';
|
|
27
|
+
import { resolveEndpoint } from '../api/endpoints.js';
|
|
28
|
+
import { resolveOrgId } from './helpers.js';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Execute one instagram connector tool via the governed collector endpoint.
|
|
32
|
+
* A pending-approval response is a SUCCESS shape here (the card was filed);
|
|
33
|
+
* only a non-pending success:false is an error.
|
|
34
|
+
*/
|
|
35
|
+
async function executeInstagramTool({ client, auth, program }, toolName, input) {
|
|
36
|
+
auth.requireAuth();
|
|
37
|
+
const orgId = resolveOrgId(auth, program);
|
|
38
|
+
const ep = resolveEndpoint('v1.tools.execute');
|
|
39
|
+
const body = { tool_name: toolName, input, org_id: orgId };
|
|
40
|
+
const data = await client.post(ep.path, { service: ep.service, body });
|
|
41
|
+
if (data && data.success === false && !data.pending) {
|
|
42
|
+
throw new Error(`${toolName} via POST ${ep.path} returned success:false: ${data.error || 'unknown error'}`);
|
|
43
|
+
}
|
|
44
|
+
return data;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function printAwaitingApproval(action, data) {
|
|
48
|
+
const itemRef = data.inbox_item_id || data.approvalId || 'unknown';
|
|
49
|
+
printSuccess(`${action} filed for owner approval - inbox item ${itemRef} (awaiting approval). Nothing publishes until an approver says yes.`);
|
|
50
|
+
if (data.inbox_item_id) {
|
|
51
|
+
printInfo(`Review and approve: quox inbox get ${data.inbox_item_id} (or: quox inbox respond ${data.inbox_item_id} --action approve)`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function registerInstagramCommands(program, ctx) {
|
|
56
|
+
const { client, auth } = ctx;
|
|
57
|
+
|
|
58
|
+
const instagram = program
|
|
59
|
+
.command('instagram')
|
|
60
|
+
.description('Instagram publishing and monitoring (organic): governed, HITL-approved via HITL Inbox, on the shared Meta credential');
|
|
61
|
+
|
|
62
|
+
// ── instagram publish ────────────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
instagram
|
|
65
|
+
.command('publish')
|
|
66
|
+
.description('Publish a single image, video, or reel to an Instagram account. Files an owner approval card and returns immediately; never publishes directly.')
|
|
67
|
+
.argument('[caption]', 'post caption (optional)')
|
|
68
|
+
.requiredOption('--account <ig_user_id>', 'Instagram professional account ID (from `quox instagram accounts`)')
|
|
69
|
+
.option('--image <url>', 'https URL of an image to publish (exactly one of --image/--video is required)')
|
|
70
|
+
.option('--video <url>', 'https URL of a video to publish (exactly one of --image/--video is required)')
|
|
71
|
+
.option('--reel', 'publish the video as a reel (only valid with --video)')
|
|
72
|
+
.action(async (caption, options) => {
|
|
73
|
+
if (!options.image && !options.video) {
|
|
74
|
+
throw new Error('instagram publish requires exactly one of --image or --video');
|
|
75
|
+
}
|
|
76
|
+
if (options.image && options.video) {
|
|
77
|
+
throw new Error('instagram publish accepts exactly one of --image or --video, not both');
|
|
78
|
+
}
|
|
79
|
+
if (options.reel && !options.video) {
|
|
80
|
+
throw new Error('--reel is only valid with --video');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const input = { ig_user_id: options.account };
|
|
84
|
+
if (caption) input.caption = caption;
|
|
85
|
+
if (options.image) {
|
|
86
|
+
input.image_url = options.image;
|
|
87
|
+
input.media_type = 'IMAGE';
|
|
88
|
+
} else {
|
|
89
|
+
input.video_url = options.video;
|
|
90
|
+
input.media_type = options.reel ? 'REELS' : 'VIDEO';
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const data = await executeInstagramTool({ client, auth, program }, 'instagram_publish_media', input);
|
|
94
|
+
|
|
95
|
+
if (program.opts().output === 'json') {
|
|
96
|
+
formatDetail(data, { format: 'json' });
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
printAwaitingApproval('Publish', data);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// ── instagram accounts (discover, read) ──────────────────────────────────
|
|
103
|
+
|
|
104
|
+
instagram
|
|
105
|
+
.command('accounts')
|
|
106
|
+
.description('Discover the Instagram professional account linked to a Facebook Page (read-only, no approval needed).')
|
|
107
|
+
.option('--page <page_id>', 'Facebook Page ID to resolve (optional; checks every Page this token can manage when omitted)')
|
|
108
|
+
.action(async (options) => {
|
|
109
|
+
const input = {};
|
|
110
|
+
if (options.page) input.page_id = options.page;
|
|
111
|
+
const data = await executeInstagramTool({ client, auth, program }, 'instagram_discover_account', input);
|
|
112
|
+
|
|
113
|
+
if (program.opts().output === 'json') {
|
|
114
|
+
formatDetail(data, { format: 'json' });
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const result = data.result || {};
|
|
118
|
+
if (!result.ig_user_id) {
|
|
119
|
+
printInfo('No Instagram account resolved.');
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
console.log(`${result.ig_user_id} @${result.username || 'unknown'} linked page: ${result.linked_page?.name || result.linked_page?.id || 'unknown'}`);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// ── instagram media (read) ───────────────────────────────────────────────
|
|
126
|
+
|
|
127
|
+
instagram
|
|
128
|
+
.command('media')
|
|
129
|
+
.description('List recent media on an Instagram account (read-only, no approval needed).')
|
|
130
|
+
.requiredOption('--account <ig_user_id>', 'Instagram professional account ID')
|
|
131
|
+
.option('--limit <n>', 'max media items to return (max 50)', '10')
|
|
132
|
+
.action(async (options) => {
|
|
133
|
+
const limit = Math.min(parseInt(options.limit, 10) || 10, 50);
|
|
134
|
+
const data = await executeInstagramTool({ client, auth, program }, 'instagram_list_media', {
|
|
135
|
+
ig_user_id: options.account,
|
|
136
|
+
limit,
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
if (program.opts().output === 'json') {
|
|
140
|
+
formatDetail(data, { format: 'json' });
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const media = data.result || [];
|
|
144
|
+
if (media.length === 0) {
|
|
145
|
+
printInfo('No media returned.');
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
for (const m of media) {
|
|
149
|
+
console.log(`${m.id} ${m.media_type || ''} ${m.timestamp || ''}`);
|
|
150
|
+
console.log(` ${String(m.caption || '').replace(/\n/g, '\n ')}`);
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// ── instagram insights (read) ────────────────────────────────────────────
|
|
155
|
+
|
|
156
|
+
instagram
|
|
157
|
+
.command('insights')
|
|
158
|
+
.description('Show media or account insights (reach, saved, likes, comments, shares, views, follower_count).')
|
|
159
|
+
.option('--media <media_id>', 'Instagram media ID (media-level insights)')
|
|
160
|
+
.option('--account <ig_user_id>', 'Instagram professional account ID (account-level insights)')
|
|
161
|
+
.option('--metrics <metrics>', 'comma-separated metric names')
|
|
162
|
+
.option('--period <period>', 'day|week|days_28|lifetime (account-level only)')
|
|
163
|
+
.action(async (options) => {
|
|
164
|
+
if (!options.media && !options.account) {
|
|
165
|
+
throw new Error('instagram insights requires exactly one of --media or --account');
|
|
166
|
+
}
|
|
167
|
+
if (options.media && options.account) {
|
|
168
|
+
throw new Error('instagram insights accepts exactly one of --media or --account, not both');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
let data;
|
|
172
|
+
if (options.media) {
|
|
173
|
+
const input = { media_id: options.media };
|
|
174
|
+
if (options.metrics) input.metrics = options.metrics;
|
|
175
|
+
data = await executeInstagramTool({ client, auth, program }, 'instagram_get_media_insights', input);
|
|
176
|
+
} else {
|
|
177
|
+
const input = { ig_user_id: options.account };
|
|
178
|
+
if (options.metrics) input.metrics = options.metrics;
|
|
179
|
+
if (options.period) input.period = options.period;
|
|
180
|
+
data = await executeInstagramTool({ client, auth, program }, 'instagram_get_account_insights', input);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (program.opts().output === 'json') {
|
|
184
|
+
formatDetail(data, { format: 'json' });
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
const series = data.result || [];
|
|
188
|
+
if (series.length === 0) {
|
|
189
|
+
printInfo('No insights returned.');
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
for (const m of series) {
|
|
193
|
+
const latest = (m.values || [])[m.values.length - 1];
|
|
194
|
+
console.log(`${m.title || m.name}: ${latest ? JSON.stringify(latest.value) : 'unknown'}`);
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// ── instagram comments (read) ────────────────────────────────────────────
|
|
199
|
+
|
|
200
|
+
instagram
|
|
201
|
+
.command('comments')
|
|
202
|
+
.description('List comments on an Instagram media item.')
|
|
203
|
+
.requiredOption('--media <media_id>', 'Instagram media ID')
|
|
204
|
+
.option('--limit <n>', 'max comments to return (max 100)', '25')
|
|
205
|
+
.action(async (options) => {
|
|
206
|
+
const limit = Math.min(parseInt(options.limit, 10) || 25, 100);
|
|
207
|
+
const data = await executeInstagramTool({ client, auth, program }, 'instagram_get_comments', {
|
|
208
|
+
media_id: options.media,
|
|
209
|
+
limit,
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
if (program.opts().output === 'json') {
|
|
213
|
+
formatDetail(data, { format: 'json' });
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
const comments = data.result || [];
|
|
217
|
+
if (comments.length === 0) {
|
|
218
|
+
printInfo('No comments returned.');
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
for (const c of comments) {
|
|
222
|
+
console.log(`${c.id} ${c.username || 'unknown'} ${c.timestamp || ''}`);
|
|
223
|
+
console.log(` ${String(c.text || '').replace(/\n/g, '\n ')}`);
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// ── instagram reply ──────────────────────────────────────────────────────
|
|
228
|
+
|
|
229
|
+
instagram
|
|
230
|
+
.command('reply')
|
|
231
|
+
.description('Reply to a comment on an Instagram media item. Files an owner approval card and returns immediately; never replies directly.')
|
|
232
|
+
.argument('<comment_id>', 'Instagram comment ID to reply to')
|
|
233
|
+
.argument('<message>', 'reply text')
|
|
234
|
+
.action(async (commentId, message) => {
|
|
235
|
+
const data = await executeInstagramTool({ client, auth, program }, 'instagram_reply_comment', { comment_id: commentId, message });
|
|
236
|
+
|
|
237
|
+
if (program.opts().output === 'json') {
|
|
238
|
+
formatDetail(data, { format: 'json' });
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
printAwaitingApproval('Reply', data);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
// ── instagram quota (read) ───────────────────────────────────────────────
|
|
245
|
+
|
|
246
|
+
instagram
|
|
247
|
+
.command('quota')
|
|
248
|
+
.description('Show the rolling 24-hour publishing quota for an Instagram account (hard cap: 100 API-published posts).')
|
|
249
|
+
.requiredOption('--account <ig_user_id>', 'Instagram professional account ID')
|
|
250
|
+
.action(async (options) => {
|
|
251
|
+
const data = await executeInstagramTool({ client, auth, program }, 'instagram_get_publishing_limit', { ig_user_id: options.account });
|
|
252
|
+
|
|
253
|
+
if (program.opts().output === 'json') {
|
|
254
|
+
formatDetail(data, { format: 'json' });
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
const result = data.result || {};
|
|
258
|
+
if (result.quota_usage == null) {
|
|
259
|
+
printInfo('No quota data returned.');
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
console.log(`${result.quota_usage} / ${result.quota_total ?? 'unknown'} posts used (rolling ${result.quota_duration ? `${Math.round(result.quota_duration / 3600)}h` : '24h'} window)`);
|
|
263
|
+
});
|
|
264
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration commands — DEPRECATED alias of `quox vault`.
|
|
3
|
+
*
|
|
4
|
+
* VAULT-INTEG P6: this used to be a second, parallel implementation of
|
|
5
|
+
* credential CRUD that could not actually supply secret field values (its
|
|
6
|
+
* `create`/`update` sent a `config` body key the real route
|
|
7
|
+
* (POST/PUT /orgs/:orgId/integrations) never reads — it expects
|
|
8
|
+
* `credentials`). `quox vault` is the one working implementation; every
|
|
9
|
+
* subcommand here just prints a one-line deprecation note to stderr and
|
|
10
|
+
* forwards into the exact same action factories `quox vault` itself wires
|
|
11
|
+
* up (see lib/commands/vault.js `make*Action` exports), so a script that
|
|
12
|
+
* already calls `quox integration ...` keeps working (same HTTP calls, same
|
|
13
|
+
* exit codes) without a second copy of the request/response handling to
|
|
14
|
+
* drift out of sync.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
makeVaultListAction,
|
|
19
|
+
makeVaultGetAction,
|
|
20
|
+
makeVaultCreateAction,
|
|
21
|
+
makeVaultUpdateAction,
|
|
22
|
+
makeVaultDeleteAction,
|
|
23
|
+
makeVaultTestAction,
|
|
24
|
+
} from './vault.js';
|
|
25
|
+
|
|
26
|
+
function warnDeprecated(subcommand, replacement) {
|
|
27
|
+
console.error(`quox integration ${subcommand} is deprecated; use \`quox vault ${replacement}\` instead.`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function registerIntegrationCommands(program, ctx) {
|
|
31
|
+
const integration = program
|
|
32
|
+
.command('integration')
|
|
33
|
+
.description('DEPRECATED: alias of `quox vault` — kept for existing scripts');
|
|
34
|
+
|
|
35
|
+
integration
|
|
36
|
+
.command('list')
|
|
37
|
+
.description('DEPRECATED — alias of `quox vault list`')
|
|
38
|
+
.option('--type <type>', 'filter by integration type')
|
|
39
|
+
.option('--status <status>', 'filter by test status (success|failed|untested)')
|
|
40
|
+
.action(async (options) => {
|
|
41
|
+
warnDeprecated('list', 'list');
|
|
42
|
+
return makeVaultListAction(ctx, program)(options);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
integration
|
|
46
|
+
.command('get')
|
|
47
|
+
.description('DEPRECATED — alias of `quox vault get`')
|
|
48
|
+
.argument('<integrationId>', 'integration ID')
|
|
49
|
+
.action(async (integrationId) => {
|
|
50
|
+
warnDeprecated('get', 'get');
|
|
51
|
+
return makeVaultGetAction(ctx, program)(integrationId);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
integration
|
|
55
|
+
.command('create')
|
|
56
|
+
.description('DEPRECATED — alias of `quox vault create`')
|
|
57
|
+
.option('--type <type>', 'integration type (skip prompt)')
|
|
58
|
+
.option('--label <label>', 'display label (skip prompt)')
|
|
59
|
+
.option('--set-default', 'set as default for this type')
|
|
60
|
+
.option('--no-test', 'skip connection test after creation')
|
|
61
|
+
.option('--credentials-file <path>', 'JSON object of {field: value} for the credential fields (use 0600; supports "env:VAR_NAME" values)')
|
|
62
|
+
.option('--credentials-stdin', 'read that same JSON object from stdin (never touches argv or disk)')
|
|
63
|
+
.option('--metadata <json>', 'JSON object of non-secret metadata tags')
|
|
64
|
+
.option('--repos <patterns>', 'repositories this credential covers: comma-separated owner/name, owner/*, or *')
|
|
65
|
+
.option('--no-input', 'never prompt; fail with a clear error if a required field is missing')
|
|
66
|
+
.action(async (options) => {
|
|
67
|
+
warnDeprecated('create', 'create');
|
|
68
|
+
return makeVaultCreateAction(ctx, program)(options);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
integration
|
|
72
|
+
.command('update')
|
|
73
|
+
.description('DEPRECATED — alias of `quox vault update`')
|
|
74
|
+
.argument('<integrationId>', 'integration ID')
|
|
75
|
+
.option('--label <label>', 'new label')
|
|
76
|
+
.option('--description <desc>', 'new description')
|
|
77
|
+
.action(async (integrationId, options) => {
|
|
78
|
+
warnDeprecated('update', 'update');
|
|
79
|
+
return makeVaultUpdateAction(ctx, program)(integrationId, options);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
integration
|
|
83
|
+
.command('delete')
|
|
84
|
+
.description('DEPRECATED — alias of `quox vault delete`')
|
|
85
|
+
.argument('<integrationId>', 'integration ID')
|
|
86
|
+
.option('--force', 'skip confirmation')
|
|
87
|
+
.action(async (integrationId, options) => {
|
|
88
|
+
warnDeprecated('delete', 'delete');
|
|
89
|
+
return makeVaultDeleteAction(ctx, program)(integrationId, options);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
integration
|
|
93
|
+
.command('test')
|
|
94
|
+
.description('DEPRECATED — alias of `quox vault test`')
|
|
95
|
+
.argument('<integrationId>', 'integration ID')
|
|
96
|
+
.action(async (integrationId) => {
|
|
97
|
+
warnDeprecated('test', 'test');
|
|
98
|
+
return makeVaultTestAction(ctx, program)(integrationId);
|
|
99
|
+
});
|
|
100
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `quox inventory` — the org's agent CMDB from the terminal (OPGOV quox#573).
|
|
3
|
+
*
|
|
4
|
+
* Org-wide rollup of the SAME grant graph `quox reach` evaluates per agent:
|
|
5
|
+
* which agents exist (core roster + org-created), their declared model
|
|
6
|
+
* routing, and per agent the granted tools / credentials / hosts / egress /
|
|
7
|
+
* delegation counts plus the least-privilege unused-grant signal. The
|
|
8
|
+
* collector computes it (GET /orgs/:orgId/inventory, witnessed as
|
|
9
|
+
* quox.reach.inventory); this command only renders.
|
|
10
|
+
*
|
|
11
|
+
* Honesty: 'unknown' cells are real values, not absences — a registry or
|
|
12
|
+
* layer that failed to enumerate says so, and `totals.complete` is only true
|
|
13
|
+
* when NOTHING was unknown anywhere. A '?' in the table is a '?'.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { resolveEndpoint } from '../api/endpoints.js';
|
|
17
|
+
import { resolveOrgId } from './helpers.js';
|
|
18
|
+
|
|
19
|
+
function isJsonOutput(program) {
|
|
20
|
+
const o = program.opts();
|
|
21
|
+
return o.json === true || o.output === 'json';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function cell(v, width) {
|
|
25
|
+
const s = v === 'unknown' ? '?' : String(v ?? '-');
|
|
26
|
+
return s.padStart(width);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function registerInventoryCommands(program, ctx) {
|
|
30
|
+
const { client, auth } = ctx;
|
|
31
|
+
|
|
32
|
+
program
|
|
33
|
+
.command('inventory')
|
|
34
|
+
.description("Org agent inventory (the agent CMDB): agents, models, tool/credential grants, delegation, unused-grant drift")
|
|
35
|
+
.option('--days <n>', 'observed-usage window for the unused-grant column (default 30)')
|
|
36
|
+
.option('--unused', 'ALSO compute the org-wide unused-grant pass (SLOW: minutes at org scale; for one agent use quox reach unused <agentId>)')
|
|
37
|
+
.option('--drift', 'diff against the last stored snapshot and store this rollup as the new baseline (witnessed as quox.reach.inventory_drift)')
|
|
38
|
+
.action(async (options) => {
|
|
39
|
+
const orgId = resolveOrgId(auth, program);
|
|
40
|
+
if (options.drift === true) {
|
|
41
|
+
const dep = resolveEndpoint('reach.org.inventory.drift', { orgId });
|
|
42
|
+
const result = await client.get(dep.path, { service: dep.service, timeout: 330000 });
|
|
43
|
+
if (!result || result.schema !== 'inventory-drift.v1') {
|
|
44
|
+
throw new Error(`unexpected response (schema ${result?.schema ?? 'missing'}, expected inventory-drift.v1): refusing to render. Check the collector URL points at a real collector.`);
|
|
45
|
+
}
|
|
46
|
+
if (isJsonOutput(program)) {
|
|
47
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (result.baseline) {
|
|
51
|
+
process.stdout.write(`\u{1F4A5} REACH │ inventory drift │ baseline stored (snapshot #${result.snapshot_id}) · nothing to diff yet · re-run after the next change\n`);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const d = result.drift || {};
|
|
55
|
+
const summary = d.changed
|
|
56
|
+
? `+${(d.agents_added || []).length} agents · −${(d.agents_removed || []).length} · ${(d.model_changes || []).length} model · ${(d.reach_changes || []).length} reach · ${(d.visibility_changes || []).length} visibility`
|
|
57
|
+
: 'no change';
|
|
58
|
+
const trust = d.prev_complete && d.curr_complete ? '' : ' · CAUTION: one side of the diff was INCOMPLETE';
|
|
59
|
+
process.stdout.write(`\u{1F4A5} REACH │ inventory drift │ ${summary} · since ${result.prev_taken_at}${trust}\n`);
|
|
60
|
+
for (const a of d.agents_added || []) process.stdout.write(` + ${a.id} (${a.source}${a.model ? `, ${a.model}` : ''})\n`);
|
|
61
|
+
for (const a of d.agents_removed || []) process.stdout.write(` - ${a.id} (${a.source}${a.model ? `, ${a.model}` : ''})\n`);
|
|
62
|
+
for (const m of d.model_changes || []) process.stdout.write(` ~ ${m.id} model ${m.from ?? 'default'} → ${m.to ?? 'default'}\n`);
|
|
63
|
+
for (const r of d.reach_changes || []) {
|
|
64
|
+
const zones = ['tools', 'credentials', 'delegates'].filter((z) => r[z]).map((z) => `${z} ${r[z].from}→${r[z].to}`).join(', ');
|
|
65
|
+
process.stdout.write(` ~ ${r.id} ${zones}\n`);
|
|
66
|
+
}
|
|
67
|
+
for (const v of d.visibility_changes || []) process.stdout.write(` ! ${v.id} ${v.from} → ${v.to}\n`);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const ep = resolveEndpoint('reach.org.inventory', {
|
|
71
|
+
orgId,
|
|
72
|
+
days: options.days,
|
|
73
|
+
unused: options.unused === true,
|
|
74
|
+
});
|
|
75
|
+
// The first cold rollup resolves reach for every agent in the org and
|
|
76
|
+
// can take minutes; the collector caches per-agent reach for 10 min so
|
|
77
|
+
// re-runs are fast. 330s ceiling, not the default 30s.
|
|
78
|
+
const result = await client.get(ep.path, { service: ep.service, timeout: 330000 });
|
|
79
|
+
// A 200 with the wrong shape once rendered a confident empty table
|
|
80
|
+
// ("? agents", "undefined unknown") from a non-collector endpoint. A
|
|
81
|
+
// response that is not inventory.v1 is an error, never a rollup.
|
|
82
|
+
if (!result || result.schema !== 'inventory.v1') {
|
|
83
|
+
throw new Error(`unexpected response (schema ${result?.schema ?? 'missing'}, expected inventory.v1): refusing to render. Check the collector URL points at a real collector.`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (isJsonOutput(program)) {
|
|
87
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const t = result.totals || {};
|
|
92
|
+
const completeness = t.complete ? 'complete' : `INCOMPLETE (${t.agents_reach_unknown} agent(s) unknown, ${(result.unknowns || []).length} registry gap(s))`;
|
|
93
|
+
process.stdout.write(
|
|
94
|
+
`\u{1F4A5} REACH │ inventory │ ${t.agents ?? '?'} agents · ${t.tool_grants ?? '?'} tool grants · ${t.credential_grants ?? '?'} cred grants · ${completeness} · ${result.duration_ms ?? '?'}ms\n\n`
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
const header = `${'agent'.padEnd(22)}${'src'.padEnd(6)}${'model'.padEnd(18)}${cell('tools', 6)}${cell('creds', 6)}${cell('hosts', 6)}${cell('deleg', 6)}${cell('unused', 7)} badge`;
|
|
98
|
+
process.stdout.write(`${header}\n${'─'.repeat(header.length)}\n`);
|
|
99
|
+
for (const a of result.agents || []) {
|
|
100
|
+
const r = a.reach;
|
|
101
|
+
const row = r === 'unknown'
|
|
102
|
+
? `${a.id.padEnd(22)}${(a.source || '?').padEnd(6)}${(a.model || 'default').padEnd(18)}${cell('unknown', 6)}${cell('?', 6)}${cell('?', 6)}${cell('?', 6)}${cell('?', 7)} reach failed: ${a.reach_error || '?'}`
|
|
103
|
+
: `${a.id.padEnd(22)}${(a.source || '?').padEnd(6)}${(a.model || 'default').padEnd(18)}${cell(r.tools, 6)}${cell(r.credentials, 6)}${cell(r.hosts, 6)}${cell(r.delegates, 6)}${cell(a.unused_grants ?? '-', 7)} ${r.badge?.level || '-'}${(r.unknown_zones || []).length ? ` (?: ${r.unknown_zones.join(',')})` : ''}`;
|
|
104
|
+
process.stdout.write(`${row}\n`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if ((result.unknowns || []).length) {
|
|
108
|
+
process.stdout.write('\nregistry gaps (first-class UNKNOWN, not zero):\n');
|
|
109
|
+
for (const u of result.unknowns) process.stdout.write(` · [${u.source}] ${u.statement}\n`);
|
|
110
|
+
}
|
|
111
|
+
process.stdout.write(`\nunused-grant window: ${result.unused_window_days ?? 'skipped'} days · '?' = source did not enumerate · witnessed as quox.reach.inventory\n`);
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export default registerInventoryCommands;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Invitation commands (GRIPE-712): list and accept the caller's own pending
|
|
3
|
+
* invitations without needing the emailed token.
|
|
4
|
+
*/
|
|
5
|
+
import { formatOutput, printSuccess } from '../core/output.js';
|
|
6
|
+
import { resolveEndpoint } from '../api/endpoints.js';
|
|
7
|
+
|
|
8
|
+
export function registerInvitesCommands(program, ctx) {
|
|
9
|
+
const { client, auth } = ctx;
|
|
10
|
+
|
|
11
|
+
const invites = program.command('invites').description('Your pending organization invitations');
|
|
12
|
+
|
|
13
|
+
invites
|
|
14
|
+
.command('list')
|
|
15
|
+
.description('List pending invitations addressed to your verified email')
|
|
16
|
+
.action(async () => {
|
|
17
|
+
auth.requireAuth();
|
|
18
|
+
const ep = resolveEndpoint('invites.mine.list');
|
|
19
|
+
const data = await client.get(ep.path, { service: ep.service });
|
|
20
|
+
const rows = (data.invites || []).map(i => ({
|
|
21
|
+
id: i.id,
|
|
22
|
+
organization: i.organization?.name || i.organization?.slug || '',
|
|
23
|
+
role: i.role,
|
|
24
|
+
invited_by: i.invited_by || '',
|
|
25
|
+
expires_at: i.expires_at || '',
|
|
26
|
+
}));
|
|
27
|
+
formatOutput({
|
|
28
|
+
columns: ['id', 'organization', 'role', 'invited_by', 'expires_at'],
|
|
29
|
+
headers: ['ID', 'Organization', 'Role', 'Invited by', 'Expires'],
|
|
30
|
+
rows,
|
|
31
|
+
}, { format: program.opts().output });
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
invites
|
|
35
|
+
.command('accept')
|
|
36
|
+
.description('Accept a pending invitation by id (no emailed token needed)')
|
|
37
|
+
.argument('<id>', 'invitation id from `quox invites list`')
|
|
38
|
+
.action(async (id) => {
|
|
39
|
+
auth.requireAuth();
|
|
40
|
+
const ep = resolveEndpoint('invites.mine.accept', { inviteId: id });
|
|
41
|
+
const data = await client.post(ep.path, { service: ep.service, body: {} });
|
|
42
|
+
const org = data.organization;
|
|
43
|
+
printSuccess(`${data.message || 'Invite accepted'}${org?.name ? `: ${org.name}` : ''}. Run \`quox org switch ${org?.id || '<orgId>'}\` to use it.`);
|
|
44
|
+
});
|
|
45
|
+
}
|