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,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegram Pro commands (QTG-PRO P0 CLI parity): onboarding status, owner
|
|
3
|
+
* binding, session leases, and webhook admin.
|
|
4
|
+
*
|
|
5
|
+
* quox tg status [--bot <botId>]
|
|
6
|
+
* quox tg bind-owner <tg_chat_id> [--bot <botId>]
|
|
7
|
+
* quox tg leases
|
|
8
|
+
* quox tg webhook-info [--bot <botId>]
|
|
9
|
+
* quox tg setup <webhook_url> [--bot <botId>]
|
|
10
|
+
*
|
|
11
|
+
* Backend: services/collector/server.js (QTG-PRO P0 sections) — every route
|
|
12
|
+
* is collector-direct (no org segment in the URL), requireServiceOrUserAuth()
|
|
13
|
+
* gated. Onboarding handler bodies live in
|
|
14
|
+
* services/collector/lib/telegramPro/onboardingService.js (getOnboardingStatus,
|
|
15
|
+
* bindOwner); session leases in services/collector/lib/telegramPro/sessionLeaseStore.js;
|
|
16
|
+
* webhook admin reuses the existing /webhook/telegram/* routes shared with
|
|
17
|
+
* the original Telegram integration setup flow.
|
|
18
|
+
*
|
|
19
|
+
* bind-owner 409s when a primary owner binding already exists for the
|
|
20
|
+
* bot/chat/thread — mirrored on the quoxchat `reply` 409 convention
|
|
21
|
+
* (cli/lib/commands/quoxchat.js): catch, surface a plain-language error, and
|
|
22
|
+
* point the caller at `quox tg status` rather than trying to unpack response
|
|
23
|
+
* detail the generic client error path doesn't forward (QuoxApiError only
|
|
24
|
+
* carries `detail`/`details`, not arbitrary body fields like `binding`).
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { formatOutput, formatDetail, printSuccess } from '../core/output.js';
|
|
28
|
+
import { resolveEndpoint } from '../api/endpoints.js';
|
|
29
|
+
|
|
30
|
+
function yesNo(value) {
|
|
31
|
+
return value ? 'yes' : 'no';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function isoOrDash(value) {
|
|
35
|
+
if (value === null || value === undefined) return '-';
|
|
36
|
+
const d = new Date(value);
|
|
37
|
+
return Number.isNaN(d.getTime()) ? String(value) : d.toISOString();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function renderStatus(data, botId) {
|
|
41
|
+
const lines = [];
|
|
42
|
+
lines.push('');
|
|
43
|
+
lines.push(`Bot: ${botId || 'default'}`);
|
|
44
|
+
if (data.bot) {
|
|
45
|
+
lines.push(` Username ${data.bot.username || '-'}`);
|
|
46
|
+
lines.push(` Name ${data.bot.name || '-'}`);
|
|
47
|
+
} else {
|
|
48
|
+
lines.push(' (no bot token configured)');
|
|
49
|
+
}
|
|
50
|
+
lines.push(` Webhook URL ${data.webhook?.url || '-'}`);
|
|
51
|
+
lines.push(` Webhook active ${yesNo(!!data.webhook?.active)}`);
|
|
52
|
+
if (data.webhook?.last_error_message) {
|
|
53
|
+
lines.push(` Last error ${data.webhook.last_error_message}`);
|
|
54
|
+
}
|
|
55
|
+
if (data.owner_binding) {
|
|
56
|
+
lines.push(` Owner chat ${data.owner_binding.tg_chat_id}${data.owner_binding.title ? ` (${data.owner_binding.title})` : ''}`);
|
|
57
|
+
} else {
|
|
58
|
+
lines.push(' Owner chat not bound');
|
|
59
|
+
}
|
|
60
|
+
return lines.join('\n');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function registerTgCommands(program, ctx) {
|
|
64
|
+
const { client, auth } = ctx;
|
|
65
|
+
|
|
66
|
+
const tg = program
|
|
67
|
+
.command('tg')
|
|
68
|
+
.description('Manage Telegram Pro: onboarding status, owner binding, session leases, webhook admin');
|
|
69
|
+
|
|
70
|
+
// ── status ───────────────────────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
tg
|
|
73
|
+
.command('status')
|
|
74
|
+
.description('Show Telegram Pro onboarding status: bot, webhook, seen chats, owner binding')
|
|
75
|
+
.option('--bot <botId>', 'target a specific bot ID instead of the default gateway bot')
|
|
76
|
+
.action(async (options) => {
|
|
77
|
+
auth.requireAuth();
|
|
78
|
+
const query = {};
|
|
79
|
+
if (options.bot) query.bot_id = options.bot;
|
|
80
|
+
|
|
81
|
+
const ep = resolveEndpoint('tg.status');
|
|
82
|
+
const data = await client.get(ep.path, { service: ep.service, query });
|
|
83
|
+
|
|
84
|
+
const format = program.opts().output;
|
|
85
|
+
if (format === 'json') {
|
|
86
|
+
console.log(JSON.stringify(data, null, 2));
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (format === 'quiet') {
|
|
90
|
+
console.log(data.bot?.username || '');
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
console.log(renderStatus(data, options.bot));
|
|
95
|
+
|
|
96
|
+
formatOutput({
|
|
97
|
+
columns: ['tg_chat_id', 'title', 'is_forum', 'seen_at'],
|
|
98
|
+
headers: ['Chat ID', 'Title', 'Forum', 'Seen'],
|
|
99
|
+
rows: (data.chats_seen || []).map((c) => ({
|
|
100
|
+
tg_chat_id: c.tg_chat_id,
|
|
101
|
+
title: c.title || '-',
|
|
102
|
+
is_forum: yesNo(c.is_forum),
|
|
103
|
+
seen_at: c.seen_at || '-',
|
|
104
|
+
})),
|
|
105
|
+
}, { format, emptyMessage: 'No chats seen yet — message the bot from a chat first.' });
|
|
106
|
+
|
|
107
|
+
if (data.bots && data.bots.length) {
|
|
108
|
+
console.log('\nBots');
|
|
109
|
+
formatOutput({
|
|
110
|
+
columns: ['bot_id', 'username', 'kind', 'webhook_active', 'chats_bound'],
|
|
111
|
+
headers: ['Bot ID', 'Username', 'Kind', 'Webhook', 'Chats'],
|
|
112
|
+
rows: data.bots.map((b) => ({
|
|
113
|
+
bot_id: b.bot_id,
|
|
114
|
+
username: b.username || '-',
|
|
115
|
+
kind: b.kind,
|
|
116
|
+
webhook_active: yesNo(b.webhook_active),
|
|
117
|
+
chats_bound: b.chats_bound,
|
|
118
|
+
})),
|
|
119
|
+
}, { format, emptyMessage: 'No bots configured.' });
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// ── bind-owner ───────────────────────────────────────────────────────────
|
|
124
|
+
|
|
125
|
+
tg
|
|
126
|
+
.command('bind-owner')
|
|
127
|
+
.description('Bind a Telegram chat as the owner target (the chat must have messaged the bot first)')
|
|
128
|
+
.argument('<tg_chat_id>', 'Telegram chat ID that has already messaged the bot')
|
|
129
|
+
.option('--bot <botId>', 'target a specific bot ID instead of the default gateway bot')
|
|
130
|
+
.action(async (tgChatId, options) => {
|
|
131
|
+
auth.requireAuth();
|
|
132
|
+
const body = { tg_chat_id: tgChatId };
|
|
133
|
+
if (options.bot) body.bot_id = options.bot;
|
|
134
|
+
|
|
135
|
+
const ep = resolveEndpoint('tg.bindOwner');
|
|
136
|
+
try {
|
|
137
|
+
const data = await client.post(ep.path, { service: ep.service, body });
|
|
138
|
+
if (program.opts().output === 'json') {
|
|
139
|
+
console.log(JSON.stringify(data, null, 2));
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
printSuccess(`Owner bound to chat ${tgChatId}.`);
|
|
143
|
+
if (data.binding) {
|
|
144
|
+
formatDetail(data.binding, {
|
|
145
|
+
format: program.opts().output,
|
|
146
|
+
title: 'Owner Binding',
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
} catch (err) {
|
|
150
|
+
if (err.status === 409) {
|
|
151
|
+
throw new Error(
|
|
152
|
+
`A primary owner binding already exists for this bot. Run \`quox tg status${options.bot ? ` --bot ${options.bot}` : ''}\` to see the current owner binding before rebinding.`
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
throw err;
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// ── leases ───────────────────────────────────────────────────────────────
|
|
160
|
+
|
|
161
|
+
tg
|
|
162
|
+
.command('leases')
|
|
163
|
+
.description('List session leases for parallel terminal identities (T1-T8)')
|
|
164
|
+
.action(async () => {
|
|
165
|
+
auth.requireAuth();
|
|
166
|
+
const ep = resolveEndpoint('tg.leases.list');
|
|
167
|
+
const data = await client.get(ep.path, { service: ep.service });
|
|
168
|
+
|
|
169
|
+
formatOutput({
|
|
170
|
+
columns: ['terminal', 'session_id', 'stream', 'alive', 'expires_at'],
|
|
171
|
+
headers: ['Terminal', 'Session ID', 'Stream', 'Alive', 'Expires'],
|
|
172
|
+
rows: (data.leases || []).map((l) => ({
|
|
173
|
+
terminal: l.terminal,
|
|
174
|
+
session_id: l.session_id,
|
|
175
|
+
stream: l.stream || '-',
|
|
176
|
+
alive: yesNo(l.alive),
|
|
177
|
+
expires_at: isoOrDash(l.expires_at),
|
|
178
|
+
})),
|
|
179
|
+
}, { format: program.opts().output, emptyMessage: 'No active session leases.' });
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// ── webhook-info ─────────────────────────────────────────────────────────
|
|
183
|
+
|
|
184
|
+
tg
|
|
185
|
+
.command('webhook-info')
|
|
186
|
+
.description('Show current Telegram webhook status (per-bot info is pending backend support; --bot is a no-op today)')
|
|
187
|
+
.option('--bot <botId>', 'target a specific bot ID (not yet honored server-side)')
|
|
188
|
+
.action(async (options) => {
|
|
189
|
+
auth.requireAuth();
|
|
190
|
+
const query = {};
|
|
191
|
+
if (options.bot) query.bot_id = options.bot;
|
|
192
|
+
|
|
193
|
+
const ep = resolveEndpoint('tg.webhook.info');
|
|
194
|
+
const data = await client.get(ep.path, { service: ep.service, query });
|
|
195
|
+
|
|
196
|
+
formatDetail({
|
|
197
|
+
active: data.active,
|
|
198
|
+
has_token: data.has_token,
|
|
199
|
+
bot_username: data.bot_username,
|
|
200
|
+
webhook_url: data.webhook_url,
|
|
201
|
+
pending_update_count: data.pending_update_count,
|
|
202
|
+
last_error_date: data.last_error_date,
|
|
203
|
+
last_error_message: data.last_error_message,
|
|
204
|
+
}, {
|
|
205
|
+
format: program.opts().output,
|
|
206
|
+
title: 'Telegram Webhook Info',
|
|
207
|
+
labels: {
|
|
208
|
+
active: 'Active',
|
|
209
|
+
has_token: 'Has token',
|
|
210
|
+
bot_username: 'Bot username',
|
|
211
|
+
webhook_url: 'Webhook URL',
|
|
212
|
+
pending_update_count: 'Pending updates',
|
|
213
|
+
last_error_date: 'Last error date',
|
|
214
|
+
last_error_message: 'Last error message',
|
|
215
|
+
},
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
// ── setup ────────────────────────────────────────────────────────────────
|
|
220
|
+
|
|
221
|
+
tg
|
|
222
|
+
.command('setup')
|
|
223
|
+
.description('Register the Telegram webhook URL for the bot')
|
|
224
|
+
.argument('<webhook_url>', 'HTTPS URL Telegram should POST updates to')
|
|
225
|
+
.option('--bot <botId>', 'target a specific bot ID instead of the default gateway bot')
|
|
226
|
+
.action(async (webhookUrl, options) => {
|
|
227
|
+
auth.requireAuth();
|
|
228
|
+
const body = { webhook_url: webhookUrl };
|
|
229
|
+
if (options.bot) body.bot_id = options.bot;
|
|
230
|
+
|
|
231
|
+
const ep = resolveEndpoint('tg.webhook.setup');
|
|
232
|
+
const data = await client.post(ep.path, { service: ep.service, body });
|
|
233
|
+
|
|
234
|
+
if (program.opts().output === 'json') {
|
|
235
|
+
console.log(JSON.stringify(data, null, 2));
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
printSuccess(`Webhook registered: ${data.webhook_url} (bot: ${data.bot_username || 'unknown'}).`);
|
|
239
|
+
});
|
|
240
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `quox tiktok` — TikTok plugin verbs, TT-4 (instagram CLI parity).
|
|
3
|
+
*
|
|
4
|
+
* quox tiktok publish "<title>" --file <path-in-media-root> [--privacy SELF_ONLY|MUTUAL_FOLLOW_FRIENDS|PUBLIC_TO_EVERYONE] [--no-duet] [--no-comment] [--no-stitch]
|
|
5
|
+
* quox tiktok draft "<title>" --file <path-in-media-root>
|
|
6
|
+
* quox tiktok profile
|
|
7
|
+
* quox tiktok videos [--limit <n>]
|
|
8
|
+
* quox tiktok stats --ids <id,id,...>
|
|
9
|
+
* quox tiktok status --publish-id <id>
|
|
10
|
+
*
|
|
11
|
+
* The write verbs (publish, draft) execute the tiktok connector tool through
|
|
12
|
+
* the governed collector endpoint (POST /api/v1/tools/execute -> connector
|
|
13
|
+
* bridge). The bridge gates both on requiresApproval: it files an owner
|
|
14
|
+
* approval card and returns immediately with { pending:true,
|
|
15
|
+
* status:'awaiting_approval', approvalId, inbox_item_id }. The actual TikTok
|
|
16
|
+
* API write (video/init -> upload -> publish, or the inbox/draft init ->
|
|
17
|
+
* upload) only happens once the owner approves via the dashboard inbox or
|
|
18
|
+
* `quox inbox respond <id> --action approve`.
|
|
19
|
+
*
|
|
20
|
+
* TikTok has its OWN dedicated vaulted credential; there is no shared
|
|
21
|
+
* credential with any other plugin. Two hard truths this CLI states
|
|
22
|
+
* honestly: an unaudited TikTok app is restricted to posting private
|
|
23
|
+
* (SELF_ONLY) content regardless of the privacy level requested, until
|
|
24
|
+
* TikTok approves the app audit; and access tokens live 24 hours with no
|
|
25
|
+
* automatic refresh built yet.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { formatDetail, printSuccess, printInfo, printWarning } from '../core/output.js';
|
|
29
|
+
import { resolveEndpoint } from '../api/endpoints.js';
|
|
30
|
+
import { resolveOrgId } from './helpers.js';
|
|
31
|
+
|
|
32
|
+
const PRIVACY_LEVELS = ['SELF_ONLY', 'MUTUAL_FOLLOW_FRIENDS', 'PUBLIC_TO_EVERYONE'];
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Execute one tiktok connector tool via the governed collector endpoint.
|
|
36
|
+
* A pending-approval response is a SUCCESS shape here (the card was filed);
|
|
37
|
+
* only a non-pending success:false is an error.
|
|
38
|
+
*/
|
|
39
|
+
async function executeTikTokTool({ client, auth, program }, toolName, input) {
|
|
40
|
+
auth.requireAuth();
|
|
41
|
+
const orgId = resolveOrgId(auth, program);
|
|
42
|
+
const ep = resolveEndpoint('v1.tools.execute');
|
|
43
|
+
const body = { tool_name: toolName, input, org_id: orgId };
|
|
44
|
+
const data = await client.post(ep.path, { service: ep.service, body });
|
|
45
|
+
if (data && data.success === false && !data.pending) {
|
|
46
|
+
throw new Error(`${toolName} via POST ${ep.path} returned success:false: ${data.error || 'unknown error'}`);
|
|
47
|
+
}
|
|
48
|
+
return data;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function printAwaitingApproval(action, data) {
|
|
52
|
+
const itemRef = data.inbox_item_id || data.approvalId || 'unknown';
|
|
53
|
+
printSuccess(`${action} filed for owner approval - inbox item ${itemRef} (awaiting approval). Nothing publishes until an approver says yes.`);
|
|
54
|
+
if (data.inbox_item_id) {
|
|
55
|
+
printInfo(`Review and approve: quox inbox get ${data.inbox_item_id} (or: quox inbox respond ${data.inbox_item_id} --action approve)`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function registerTikTokCommands(program, ctx) {
|
|
60
|
+
const { client, auth } = ctx;
|
|
61
|
+
|
|
62
|
+
const tiktok = program
|
|
63
|
+
.command('tiktok')
|
|
64
|
+
.description('TikTok video publishing and draft uploads: governed, HITL-approved via HITL Inbox, on your own dedicated TikTok credential');
|
|
65
|
+
|
|
66
|
+
// ── tiktok publish ────────────────────────────────────────────────────
|
|
67
|
+
|
|
68
|
+
tiktok
|
|
69
|
+
.command('publish')
|
|
70
|
+
.description('Publish a video from a file under the org TikTok media root. Files an owner approval card and returns immediately; never publishes directly.')
|
|
71
|
+
.argument('<title>', 'video title/caption')
|
|
72
|
+
.requiredOption('--file <path>', 'video file path, relative to the org TikTok media root')
|
|
73
|
+
.option('--privacy <level>', `privacy level: ${PRIVACY_LEVELS.join('|')} (default SELF_ONLY)`, 'SELF_ONLY')
|
|
74
|
+
.option('--no-duet', 'disable duets on this video')
|
|
75
|
+
.option('--no-comment', 'disable comments on this video')
|
|
76
|
+
.option('--no-stitch', 'disable stitching on this video')
|
|
77
|
+
.action(async (title, options) => {
|
|
78
|
+
const privacy = options.privacy || 'SELF_ONLY';
|
|
79
|
+
if (!PRIVACY_LEVELS.includes(privacy)) {
|
|
80
|
+
throw new Error(`--privacy must be one of ${PRIVACY_LEVELS.join(', ')} (got ${privacy})`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const input = {
|
|
84
|
+
title,
|
|
85
|
+
video_path: options.file,
|
|
86
|
+
privacy_level: privacy,
|
|
87
|
+
disable_duet: !options.duet,
|
|
88
|
+
disable_comment: !options.comment,
|
|
89
|
+
disable_stitch: !options.stitch,
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const data = await executeTikTokTool({ client, auth, program }, 'tiktok_publish_video', input);
|
|
93
|
+
|
|
94
|
+
if (program.opts().output === 'json') {
|
|
95
|
+
formatDetail(data, { format: 'json' });
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
printAwaitingApproval('Publish', data);
|
|
99
|
+
if (privacy !== 'SELF_ONLY') {
|
|
100
|
+
printWarning(`Requested privacy_level=${privacy} is sent to TikTok exactly as asked, but an unaudited TikTok app is restricted to private (SELF_ONLY) viewing until TikTok approves the app audit. Check the outcome with: quox tiktok status --publish-id <id>`);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// ── tiktok draft ──────────────────────────────────────────────────────
|
|
105
|
+
|
|
106
|
+
tiktok
|
|
107
|
+
.command('draft')
|
|
108
|
+
.description('Upload a video to the account\'s TikTok inbox as a draft, finished manually in the TikTok app. Files an owner approval card and returns immediately; never uploads directly.')
|
|
109
|
+
.argument('<title>', 'working title for the draft (display only)')
|
|
110
|
+
.requiredOption('--file <path>', 'video file path, relative to the org TikTok media root')
|
|
111
|
+
.action(async (title, options) => {
|
|
112
|
+
const input = { title, video_path: options.file };
|
|
113
|
+
const data = await executeTikTokTool({ client, auth, program }, 'tiktok_upload_draft', input);
|
|
114
|
+
|
|
115
|
+
if (program.opts().output === 'json') {
|
|
116
|
+
formatDetail(data, { format: 'json' });
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
printAwaitingApproval('Draft upload', data);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// ── tiktok profile (read; cheapest live credential check) ──────────────
|
|
123
|
+
|
|
124
|
+
tiktok
|
|
125
|
+
.command('profile')
|
|
126
|
+
.description('Show the connected TikTok profile (also the cheapest live credential check).')
|
|
127
|
+
.action(async () => {
|
|
128
|
+
const data = await executeTikTokTool({ client, auth, program }, 'tiktok_get_profile', {});
|
|
129
|
+
|
|
130
|
+
if (program.opts().output === 'json') {
|
|
131
|
+
formatDetail(data, { format: 'json' });
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const result = data.result || {};
|
|
135
|
+
if (!result.open_id && !result.display_name) {
|
|
136
|
+
printInfo('No profile returned.');
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
console.log(`${result.display_name || 'unknown'} (${result.open_id || 'unknown'})`);
|
|
140
|
+
console.log(` followers: ${result.followers ?? 'unknown'} following: ${result.following ?? 'unknown'} likes: ${result.likes ?? 'unknown'} videos: ${result.videos ?? 'unknown'}`);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// ── tiktok videos (read) ────────────────────────────────────────────────
|
|
144
|
+
|
|
145
|
+
tiktok
|
|
146
|
+
.command('videos')
|
|
147
|
+
.description('List the connected account\'s recent videos (read-only, no approval needed).')
|
|
148
|
+
.option('--limit <n>', 'max videos to return (max 20)', '10')
|
|
149
|
+
.action(async (options) => {
|
|
150
|
+
const maxCount = Math.min(parseInt(options.limit, 10) || 10, 20);
|
|
151
|
+
const data = await executeTikTokTool({ client, auth, program }, 'tiktok_list_videos', { max_count: maxCount });
|
|
152
|
+
|
|
153
|
+
if (program.opts().output === 'json') {
|
|
154
|
+
formatDetail(data, { format: 'json' });
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const videos = data.result?.videos || [];
|
|
158
|
+
if (videos.length === 0) {
|
|
159
|
+
printInfo('No videos returned.');
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
for (const v of videos) {
|
|
163
|
+
console.log(`${v.id} ${v.title || '(no title)'}`);
|
|
164
|
+
console.log(` views: ${v.views ?? 0} likes: ${v.likes ?? 0} comments: ${v.comments ?? 0} shares: ${v.shares ?? 0}`);
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// ── tiktok stats (read) ──────────────────────────────────────────────────
|
|
169
|
+
|
|
170
|
+
tiktok
|
|
171
|
+
.command('stats')
|
|
172
|
+
.description('Show engagement stats for specific videos by id.')
|
|
173
|
+
.requiredOption('--ids <ids>', 'comma-separated TikTok video IDs')
|
|
174
|
+
.action(async (options) => {
|
|
175
|
+
const videoIds = options.ids.split(',').map((s) => s.trim()).filter(Boolean);
|
|
176
|
+
if (videoIds.length === 0) {
|
|
177
|
+
throw new Error('tiktok stats requires at least one id in --ids');
|
|
178
|
+
}
|
|
179
|
+
const data = await executeTikTokTool({ client, auth, program }, 'tiktok_get_video_stats', { video_ids: videoIds });
|
|
180
|
+
|
|
181
|
+
if (program.opts().output === 'json') {
|
|
182
|
+
formatDetail(data, { format: 'json' });
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
const videos = data.result || [];
|
|
186
|
+
if (videos.length === 0) {
|
|
187
|
+
printInfo('No stats returned.');
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
for (const v of videos) {
|
|
191
|
+
console.log(`${v.id} ${v.title || '(no title)'}`);
|
|
192
|
+
console.log(` views: ${v.views ?? 0} likes: ${v.likes ?? 0} comments: ${v.comments ?? 0} shares: ${v.shares ?? 0}`);
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// ── tiktok status (read) ─────────────────────────────────────────────────
|
|
197
|
+
|
|
198
|
+
tiktok
|
|
199
|
+
.command('status')
|
|
200
|
+
.description('Check the status of a video publish or draft upload by publish_id.')
|
|
201
|
+
.requiredOption('--publish-id <id>', 'the publish_id returned by publish or draft')
|
|
202
|
+
.action(async (options) => {
|
|
203
|
+
const data = await executeTikTokTool({ client, auth, program }, 'tiktok_get_publish_status', { publish_id: options.publishId });
|
|
204
|
+
|
|
205
|
+
if (program.opts().output === 'json') {
|
|
206
|
+
formatDetail(data, { format: 'json' });
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
const result = data.result || {};
|
|
210
|
+
if (!result.status) {
|
|
211
|
+
printInfo('No status returned.');
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
console.log(`${result.publish_id} status: ${result.status}`);
|
|
215
|
+
if (result.fail_reason) console.log(` fail_reason: ${result.fail_reason}`);
|
|
216
|
+
if (result.publicly_available_post_ids) console.log(` publicly_available_post_ids: ${result.publicly_available_post_ids}`);
|
|
217
|
+
});
|
|
218
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool commands: CRUD, catalog, install, publish
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { missingReasonWarning } from '../core/callerSource.js';
|
|
6
|
+
import { formatOutput, formatDetail, printSuccess } from '../core/output.js';
|
|
7
|
+
import { resolveOrgId, parseJSON } from './helpers.js';
|
|
8
|
+
import { resolveEndpoint } from '../api/endpoints.js';
|
|
9
|
+
|
|
10
|
+
export function registerToolCommands(program, ctx) {
|
|
11
|
+
const { client, auth } = ctx;
|
|
12
|
+
|
|
13
|
+
const tool = program
|
|
14
|
+
.command('tool')
|
|
15
|
+
.description('Manage tools and tool catalog');
|
|
16
|
+
|
|
17
|
+
tool
|
|
18
|
+
.command('list')
|
|
19
|
+
.description('List tools in the organization')
|
|
20
|
+
.option('--type <type>', 'filter by type: mcp, function, api, script, workflow')
|
|
21
|
+
.option('--status <status>', 'filter by status: active, draft')
|
|
22
|
+
.action(async (options) => {
|
|
23
|
+
auth.requireAuth();
|
|
24
|
+
const orgId = resolveOrgId(auth, program);
|
|
25
|
+
const query = {};
|
|
26
|
+
if (options.type) query.type = options.type;
|
|
27
|
+
if (options.status) query.status = options.status;
|
|
28
|
+
const data = await client.get(resolveEndpoint('tools.list', { orgId }).path, { service: resolveEndpoint('tools.list', { orgId }).service, query });
|
|
29
|
+
formatOutput({
|
|
30
|
+
// fleet #392: column is tool_type.
|
|
31
|
+
columns: ['id', 'name', 'tool_type', 'status', 'risk_level', 'created_at'],
|
|
32
|
+
headers: ['ID', 'Name', 'Type', 'Status', 'Risk', 'Created'],
|
|
33
|
+
rows: data.items || data.tools || data,
|
|
34
|
+
}, { format: program.opts().output });
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
tool
|
|
38
|
+
.command('get')
|
|
39
|
+
.description('Get tool details')
|
|
40
|
+
.argument('<toolId>', 'tool ID')
|
|
41
|
+
.action(async (toolId) => {
|
|
42
|
+
auth.requireAuth();
|
|
43
|
+
const orgId = resolveOrgId(auth, program);
|
|
44
|
+
const data = await client.get(resolveEndpoint('tools.get', { orgId, toolId }).path, { service: resolveEndpoint('tools.get', { orgId, toolId }).service });
|
|
45
|
+
formatDetail(data.tool || data, { format: program.opts().output, title: data.tool?.name || data.name });
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
tool
|
|
49
|
+
.command('create')
|
|
50
|
+
.description('Create a new tool')
|
|
51
|
+
.requiredOption('--name <name>', 'tool name')
|
|
52
|
+
.requiredOption('--type <type>', 'type: mcp, function, api, script, workflow')
|
|
53
|
+
.option('--description <desc>', 'description')
|
|
54
|
+
.option('--risk-level <level>', 'risk level: low, medium, high', 'low')
|
|
55
|
+
.option('--spec <json>', 'tool spec as JSON string')
|
|
56
|
+
.action(async (options) => {
|
|
57
|
+
auth.requireAuth();
|
|
58
|
+
const orgId = resolveOrgId(auth, program);
|
|
59
|
+
const body = {
|
|
60
|
+
name: options.name,
|
|
61
|
+
type: options.type,
|
|
62
|
+
risk_level: options.riskLevel,
|
|
63
|
+
};
|
|
64
|
+
if (options.description) body.description = options.description;
|
|
65
|
+
if (options.spec) body.spec = parseJSON(options.spec, '--spec');
|
|
66
|
+
await client.post(resolveEndpoint('tools.create', { orgId }).path, { service: resolveEndpoint('tools.create', { orgId }).service, body });
|
|
67
|
+
printSuccess(`Tool "${options.name}" created.`);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
tool
|
|
71
|
+
.command('update')
|
|
72
|
+
.description('Update a tool')
|
|
73
|
+
.argument('<toolId>', 'tool ID')
|
|
74
|
+
.option('--name <name>', 'new name')
|
|
75
|
+
.option('--description <desc>', 'new description')
|
|
76
|
+
.option('--status <status>', 'new status')
|
|
77
|
+
.action(async (toolId, options) => {
|
|
78
|
+
auth.requireAuth();
|
|
79
|
+
const orgId = resolveOrgId(auth, program);
|
|
80
|
+
const body = {};
|
|
81
|
+
if (options.name) body.name = options.name;
|
|
82
|
+
if (options.description) body.description = options.description;
|
|
83
|
+
if (options.status) body.status = options.status;
|
|
84
|
+
await client.patch(resolveEndpoint('tools.update', { orgId, toolId }).path, { service: resolveEndpoint('tools.update', { orgId, toolId }).service, body });
|
|
85
|
+
printSuccess('Tool updated.');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
tool
|
|
89
|
+
.command('delete')
|
|
90
|
+
.description('Delete a tool')
|
|
91
|
+
.argument('<toolId>', 'tool ID')
|
|
92
|
+
.action(async (toolId) => {
|
|
93
|
+
auth.requireAuth();
|
|
94
|
+
const orgId = resolveOrgId(auth, program);
|
|
95
|
+
await client.delete(resolveEndpoint('tools.delete', { orgId, toolId }).path, { service: resolveEndpoint('tools.delete', { orgId, toolId }).service });
|
|
96
|
+
printSuccess('Tool deleted.');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
tool
|
|
100
|
+
.command('publish')
|
|
101
|
+
.description('Publish a tool')
|
|
102
|
+
.argument('<toolId>', 'tool ID')
|
|
103
|
+
.action(async (toolId) => {
|
|
104
|
+
auth.requireAuth();
|
|
105
|
+
const orgId = resolveOrgId(auth, program);
|
|
106
|
+
await client.post(resolveEndpoint('tools.publish', { orgId, toolId }).path, { service: resolveEndpoint('tools.publish', { orgId, toolId }).service });
|
|
107
|
+
printSuccess('Tool published.');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
tool
|
|
111
|
+
.command('catalog')
|
|
112
|
+
.description('Browse the tool catalog')
|
|
113
|
+
.option('--search <query>', 'search tools')
|
|
114
|
+
.option('--category <cat>', 'filter by category')
|
|
115
|
+
.action(async (options) => {
|
|
116
|
+
auth.requireAuth();
|
|
117
|
+
const query = {};
|
|
118
|
+
if (options.search) query.search = options.search;
|
|
119
|
+
if (options.category) query.category = options.category;
|
|
120
|
+
const data = await client.get(resolveEndpoint('catalog.list').path, { service: resolveEndpoint('catalog.list').service, query });
|
|
121
|
+
// NOTE: display the business key `tool_id` (not the catalog row `id`)
|
|
122
|
+
// as the identifier column — `tool install <toolId>` and the
|
|
123
|
+
// POST /api/tools/install-to-scope handler both validate against
|
|
124
|
+
// tool_definitions.tool_id, not tool_definitions.id. Showing `id`
|
|
125
|
+
// here previously led users to copy the wrong value into `install`.
|
|
126
|
+
formatOutput({
|
|
127
|
+
columns: ['tool_id', 'name', 'category', 'risk_level', 'description'],
|
|
128
|
+
headers: ['Tool ID', 'Name', 'Category', 'Risk', 'Description'],
|
|
129
|
+
rows: data.items || data.tools || data,
|
|
130
|
+
}, { format: program.opts().output });
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
tool
|
|
134
|
+
.command('exec')
|
|
135
|
+
.description('Execute a governed tool via the collector (POST /api/v1/tools/execute)')
|
|
136
|
+
.argument('<name>', 'tool name/id (the TOOL_DEFS key, e.g. "codebase_query")')
|
|
137
|
+
.option('--args <json>', "tool arguments as a JSON string, e.g. '{\"query\":\"...\"}'", '{}')
|
|
138
|
+
// #620: docs/muscle-memory reach for --input; accept it as an alias for
|
|
139
|
+
// --args so the documented form works. --args still wins if both given.
|
|
140
|
+
.option('--input <json>', 'alias for --args (tool arguments as a JSON string)')
|
|
141
|
+
.option('--reason <text>', 'why you are running this; shown on the approval card if the call is held for a human')
|
|
142
|
+
.action(async (name, options) => {
|
|
143
|
+
auth.requireAuth();
|
|
144
|
+
// Prefer whichever was explicitly given; --args carries the '{}' default
|
|
145
|
+
// so it is always defined, hence check --input first.
|
|
146
|
+
const usedInput = options.input !== undefined && (options.args === undefined || options.args === '{}');
|
|
147
|
+
const raw = usedInput ? options.input : options.args;
|
|
148
|
+
const input = parseJSON(raw, usedInput ? '--input' : '--args');
|
|
149
|
+
const body = { tool_name: name, input };
|
|
150
|
+
if (options.reason) body.reason = String(options.reason);
|
|
151
|
+
{ const w = missingReasonWarning(name, options.reason); if (w) console.error(w); }
|
|
152
|
+
|
|
153
|
+
// Same org-resolution precedence as `fleet exec` (lib/commands/fleet.js):
|
|
154
|
+
// the collector's /api/v1/tools/execute requires org_id in the body for
|
|
155
|
+
// service-key authenticated callers (isServiceKeyAuth = !req.user,
|
|
156
|
+
// services/collector/server.js ~8326) since there is no session to
|
|
157
|
+
// derive org from. A human-logged-in caller has req.user and the
|
|
158
|
+
// server derives org from the session instead — --org / QUOX_ORG_ID
|
|
159
|
+
// still wins when set explicitly.
|
|
160
|
+
const orgId = auth.getOrgId(program.opts().org || process.env.QUOX_ORG_ID);
|
|
161
|
+
if (!orgId && process.env.QUOX_SERVICE_KEY) {
|
|
162
|
+
throw new Error(
|
|
163
|
+
'QUOX_SERVICE_KEY is set but no org could be resolved. '
|
|
164
|
+
+ 'Set QUOX_ORG_ID=<org> for automation callers, or pass --org <id>.'
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
if (orgId) body.org_id = orgId;
|
|
168
|
+
|
|
169
|
+
// QUOXHAT T0 (2026-10-01): automation callers (the collector's
|
|
170
|
+
// CLI-tools chat path) carry the conversation + acting user in env,
|
|
171
|
+
// exactly like QUOX_ORG_ID above. The collector's execute route keys
|
|
172
|
+
// hat-binding resolution on body.session_id and user prefs on
|
|
173
|
+
// body.user_id; without them every CLI-tools execution ran against the
|
|
174
|
+
// org default hat and user-less. Interactive humans have neither set.
|
|
175
|
+
if (process.env.QUOX_SESSION_ID) body.session_id = process.env.QUOX_SESSION_ID;
|
|
176
|
+
if (process.env.QUOX_USER_ID) body.user_id = process.env.QUOX_USER_ID;
|
|
177
|
+
// QUOXHAT #657 P-C.2: forward the ACTING AGENT so a specialist's CLI-tools
|
|
178
|
+
// chat turn executes as that specialist, not the orchestrator. Without it
|
|
179
|
+
// the spawned CLI is a service caller with no agent_id, which the
|
|
180
|
+
// collector defaults to 'quox' — so under enforce a restricted specialist
|
|
181
|
+
// would silently run tools against the orchestrator's broad grant (the
|
|
182
|
+
// "CLI exec silently becomes quox" bypass). The collector sets this from
|
|
183
|
+
// its own selected_agent; a service-key caller's known agent_id is
|
|
184
|
+
// trusted, and the P-B gate then authorizes against the specialist's own
|
|
185
|
+
// grant ceiling ∩ the hat mode.
|
|
186
|
+
if (process.env.QUOX_AGENT_ID) body.agent_id = process.env.QUOX_AGENT_ID;
|
|
187
|
+
|
|
188
|
+
const ep = resolveEndpoint('v1.tools.execute');
|
|
189
|
+
const data = await client.post(ep.path, { service: ep.service, body, timeout: 120000 });
|
|
190
|
+
|
|
191
|
+
if (program.opts().output === 'json') {
|
|
192
|
+
console.log(JSON.stringify(data, null, 2));
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
const result = data && (data.result !== undefined ? data.result : data);
|
|
196
|
+
if (typeof result === 'string') {
|
|
197
|
+
console.log(result);
|
|
198
|
+
} else {
|
|
199
|
+
console.log(JSON.stringify(result, null, 2));
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
tool
|
|
204
|
+
.command('install')
|
|
205
|
+
.description('Install a tool from the catalog')
|
|
206
|
+
.argument('<toolId>', 'catalog tool_id (the "Tool ID" column from `tool catalog`, not its row id)')
|
|
207
|
+
.action(async (toolId) => {
|
|
208
|
+
auth.requireAuth();
|
|
209
|
+
// org_id is derived server-side from the JWT — no :orgId in the path.
|
|
210
|
+
await client.post(resolveEndpoint('tools.install-to-scope').path, {
|
|
211
|
+
service: resolveEndpoint('tools.install-to-scope').service,
|
|
212
|
+
body: { tool_id: toolId },
|
|
213
|
+
});
|
|
214
|
+
printSuccess('Tool installed.');
|
|
215
|
+
});
|
|
216
|
+
}
|