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,439 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `quox x` — X (Twitter) posting commands, X-PLUGIN P2 CLI verbs.
|
|
3
|
+
*
|
|
4
|
+
* quox x post "<text>" [--reply-to <tweet_id>]
|
|
5
|
+
* quox x thread <file.md>
|
|
6
|
+
* quox x delete <tweet_id>
|
|
7
|
+
* quox x like <tweet_id>
|
|
8
|
+
* quox x repost <tweet_id>
|
|
9
|
+
* quox x status
|
|
10
|
+
* quox x read <tweet_id>
|
|
11
|
+
* quox x timeline [--count <n>]
|
|
12
|
+
* quox x search <query> [--count <n>]
|
|
13
|
+
* quox x mentions [--since <tweet_id>]
|
|
14
|
+
*
|
|
15
|
+
* Every write verb here executes the collector's natively-registered
|
|
16
|
+
* governed tools (x_post, x_thread, x_delete — services/collector/lib/
|
|
17
|
+
* xTools.js) via the same governed path every other agent tool call takes:
|
|
18
|
+
* POST /api/v1/tools/execute (see cli/lib/commands/nucleus.js for the same
|
|
19
|
+
* pattern with codebase_query). None of these tools ever post directly —
|
|
20
|
+
* each files an owner approval card in HITL Inbox and returns immediately with
|
|
21
|
+
* { success:true, status:'awaiting_approval', inbox_item_id }. The actual X
|
|
22
|
+
* API write only happens once the owner approves via the dashboard inbox or
|
|
23
|
+
* `quox inbox respond <id> --action approved`.
|
|
24
|
+
*
|
|
25
|
+
* `quox x status` runs the read-only x_budget_status tool (no gating) plus
|
|
26
|
+
* the twitter connector's verify_credentials tool (exposed as the
|
|
27
|
+
* connector-prefixed tool name twitter_verify_credentials, see
|
|
28
|
+
* lib/connectors/types/twitter.js + agentTools.js's parseConnectorTool).
|
|
29
|
+
*
|
|
30
|
+
* X-PLUGIN P5 (image attachments) — `quox x post --image <path>` (repeatable,
|
|
31
|
+
* up to 4) and `!image: <path>` lines inside a `quox x thread` block attach
|
|
32
|
+
* images. Neither verb copies the file anywhere: the CLI runs on the same
|
|
33
|
+
* box as the collector, so it passes the path straight through and the
|
|
34
|
+
* SERVER validates it resolves inside the caller's org X media root
|
|
35
|
+
* (xPostingStore.resolveOrgMediaPath) — the same as every other draft-time
|
|
36
|
+
* check, fail at draft, not at approval. `quox x media-root` prints where
|
|
37
|
+
* that root is.
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
import { formatDetail, printSuccess, printWarning, printInfo } from '../core/output.js';
|
|
41
|
+
import { resolveEndpoint } from '../api/endpoints.js';
|
|
42
|
+
import { resolveOrgId } from './helpers.js';
|
|
43
|
+
|
|
44
|
+
const MAX_POST_CHARS = 280;
|
|
45
|
+
const MAX_IMAGES_PER_POST = 4;
|
|
46
|
+
// X-PLUGIN P5: mirrors the server-side default (xPostingStore.js CONFIG.mediaRoot).
|
|
47
|
+
// There is no live lookup endpoint for this constant, so if a collector
|
|
48
|
+
// operator overrides it, this printed value goes stale — a real gap, not
|
|
49
|
+
// hidden here.
|
|
50
|
+
const DEFAULT_MEDIA_ROOT = '/var/lib/quox/x-media';
|
|
51
|
+
|
|
52
|
+
/** Strip `!image: <path>` lines out of a thread block, collecting the paths
|
|
53
|
+
* (up to 4) — those lines are consumed, never posted as text. Exported for
|
|
54
|
+
* testing. */
|
|
55
|
+
export function extractImagesFromBlock(block) {
|
|
56
|
+
const imagePaths = [];
|
|
57
|
+
const text = String(block)
|
|
58
|
+
.replace(/^!image:\s*(.+)$/gm, (_match, p) => {
|
|
59
|
+
imagePaths.push(p.trim());
|
|
60
|
+
return '';
|
|
61
|
+
})
|
|
62
|
+
.replace(/\n{2,}/g, '\n')
|
|
63
|
+
.trim();
|
|
64
|
+
return { text, image_paths: imagePaths };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Split thread source text into posts on blank-line-separated blocks.
|
|
69
|
+
* Exported for testing. Trims each block; drops blocks that are empty after
|
|
70
|
+
* trimming (e.g. a trailing blank line at end of file).
|
|
71
|
+
*/
|
|
72
|
+
export function splitThreadBlocks(source) {
|
|
73
|
+
return String(source)
|
|
74
|
+
.split(/\n\s*\n+/)
|
|
75
|
+
.map((block) => block.trim())
|
|
76
|
+
.filter((block) => block.length > 0);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Client-side friendly validation before ever reaching the network — the
|
|
81
|
+
* server (xTools.js's validatePostText) still enforces this too, so this is
|
|
82
|
+
* a fail-fast convenience, not the source of truth.
|
|
83
|
+
*/
|
|
84
|
+
export function validateThreadBlocks(blocks) {
|
|
85
|
+
if (blocks.length === 0) {
|
|
86
|
+
throw new Error('No posts found. The file must contain at least one non-empty, blank-line-separated block of text.');
|
|
87
|
+
}
|
|
88
|
+
for (let i = 0; i < blocks.length; i++) {
|
|
89
|
+
if (blocks[i].length > MAX_POST_CHARS) {
|
|
90
|
+
throw new Error(
|
|
91
|
+
`Post ${i + 1} of ${blocks.length} is ${blocks[i].length} characters, over the ${MAX_POST_CHARS} character limit. `
|
|
92
|
+
+ 'Split it into shorter blank-line-separated blocks.'
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return blocks;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Execute one native/connector tool via the governed collector endpoint.
|
|
101
|
+
* Throws naming the endpoint and status on an HTTP-level failure (the
|
|
102
|
+
* client already carries status via QuoxApiError); a 200 tool-level
|
|
103
|
+
* { success:false } is surfaced as a plain Error naming the endpoint too,
|
|
104
|
+
* since that failure never carried an HTTP status of its own.
|
|
105
|
+
*/
|
|
106
|
+
async function executeXTool({ client, auth, program }, toolName, input) {
|
|
107
|
+
auth.requireAuth();
|
|
108
|
+
const orgId = resolveOrgId(auth, program);
|
|
109
|
+
const ep = resolveEndpoint('v1.tools.execute');
|
|
110
|
+
const body = { tool_name: toolName, input, org_id: orgId };
|
|
111
|
+
const data = await client.post(ep.path, { service: ep.service, body });
|
|
112
|
+
if (data && data.success === false) {
|
|
113
|
+
throw new Error(`${toolName} via POST ${ep.path} returned success:false: ${data.error || 'unknown error'}`);
|
|
114
|
+
}
|
|
115
|
+
return data;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function printAwaitingApproval(action, data) {
|
|
119
|
+
printSuccess(`${action} queued for owner approval — inbox item ${data.inbox_item_id} (awaiting approval).`);
|
|
120
|
+
printInfo(`Review and approve: quox inbox get ${data.inbox_item_id} (or: quox inbox respond ${data.inbox_item_id} --action approve)`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** One tweet as two terminal lines: id + date + metrics, then the text. */
|
|
124
|
+
function printTweetLine(t) {
|
|
125
|
+
const m = t.public_metrics || {};
|
|
126
|
+
const metrics = `♥ ${m.like_count ?? 0} · ⟲ ${m.retweet_count ?? 0} · ↩ ${m.reply_count ?? 0}`;
|
|
127
|
+
console.log(`${t.id} ${t.created_at || ''} ${metrics}`);
|
|
128
|
+
console.log(` ${String(t.text || '').replace(/\n/g, '\n ')}`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Reads cost real money (X pay-per-use, roughly $0.005 per call). Every
|
|
132
|
+
* read verb below states that in its description and makes exactly ONE
|
|
133
|
+
* call per invocation. */
|
|
134
|
+
const READ_COST_NOTE = 'One paid X API read per run (roughly $0.005).';
|
|
135
|
+
|
|
136
|
+
export function registerXCommands(program, ctx) {
|
|
137
|
+
const { client, auth } = ctx;
|
|
138
|
+
|
|
139
|
+
const x = program
|
|
140
|
+
.command('x')
|
|
141
|
+
.description('X (Twitter) posting: governed, HITL-approved via HITL Inbox');
|
|
142
|
+
|
|
143
|
+
// ── x post ──────────────────────────────────────────────────────────────
|
|
144
|
+
|
|
145
|
+
x
|
|
146
|
+
.command('post')
|
|
147
|
+
.description('Draft a post to X. Files an owner approval card and returns immediately; never posts directly.')
|
|
148
|
+
.argument('<text>', `post text, max ${MAX_POST_CHARS} characters`)
|
|
149
|
+
.option('--reply-to <tweet_id>', 'id of an existing tweet this post replies to')
|
|
150
|
+
.option(
|
|
151
|
+
'--image <path>',
|
|
152
|
+
`attach an image (repeatable, up to ${MAX_IMAGES_PER_POST}); png/jpg/webp/gif, max 5MB. The file must live under the X media root (run \`quox x media-root\`) inside your org's subdirectory — the server validates the path.`,
|
|
153
|
+
(val, prev) => prev.concat([val]),
|
|
154
|
+
[]
|
|
155
|
+
)
|
|
156
|
+
.action(async (text, options) => {
|
|
157
|
+
if (text.length > MAX_POST_CHARS) {
|
|
158
|
+
throw new Error(`Text is ${text.length} characters, over the ${MAX_POST_CHARS} character limit.`);
|
|
159
|
+
}
|
|
160
|
+
if (options.image.length > MAX_IMAGES_PER_POST) {
|
|
161
|
+
throw new Error(`${options.image.length} images given, over the ${MAX_IMAGES_PER_POST} image limit per post.`);
|
|
162
|
+
}
|
|
163
|
+
const input = { text };
|
|
164
|
+
if (options.replyTo) input.reply_to_tweet_id = options.replyTo;
|
|
165
|
+
if (options.image.length) input.image_paths = options.image;
|
|
166
|
+
|
|
167
|
+
const data = await executeXTool({ client, auth, program }, 'x_post', input);
|
|
168
|
+
|
|
169
|
+
if (program.opts().output === 'json') {
|
|
170
|
+
formatDetail(data, { format: 'json' });
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
printAwaitingApproval('Post', data);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// ── x thread ────────────────────────────────────────────────────────────
|
|
177
|
+
|
|
178
|
+
x
|
|
179
|
+
.command('thread')
|
|
180
|
+
.description(
|
|
181
|
+
'Draft a thread from a file, split on blank-line-separated blocks. Files one owner approval card for the whole thread. '
|
|
182
|
+
+ `A line of the form "!image: <path>" inside a block attaches that image (up to ${MAX_IMAGES_PER_POST} per block, consumed — never posted as text); `
|
|
183
|
+
+ 'the path must live under the X media root (run `quox x media-root`) inside your org\'s subdirectory.'
|
|
184
|
+
)
|
|
185
|
+
.argument('<file>', 'path to a markdown/text file; each blank-line-separated block becomes one post')
|
|
186
|
+
.action(async (file) => {
|
|
187
|
+
const { readFile } = await import('node:fs/promises');
|
|
188
|
+
let source;
|
|
189
|
+
try {
|
|
190
|
+
source = await readFile(file, 'utf-8');
|
|
191
|
+
} catch (err) {
|
|
192
|
+
throw new Error(`Could not read ${file}: ${err.message}`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const rawBlocks = splitThreadBlocks(source);
|
|
196
|
+
const items = rawBlocks.map((block) => extractImagesFromBlock(block));
|
|
197
|
+
validateThreadBlocks(items.map((it) => it.text));
|
|
198
|
+
for (let i = 0; i < items.length; i++) {
|
|
199
|
+
if (items[i].image_paths.length > MAX_IMAGES_PER_POST) {
|
|
200
|
+
throw new Error(`Post ${i + 1} of ${items.length} has ${items[i].image_paths.length} "!image:" lines, over the ${MAX_IMAGES_PER_POST} image limit per post.`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const hasImages = items.some((it) => it.image_paths.length > 0);
|
|
205
|
+
const input = hasImages ? { posts: items } : { texts: items.map((it) => it.text) };
|
|
206
|
+
const data = await executeXTool({ client, auth, program }, 'x_thread', input);
|
|
207
|
+
|
|
208
|
+
if (program.opts().output === 'json') {
|
|
209
|
+
formatDetail(data, { format: 'json' });
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
printInfo(`${items.length} post(s) parsed from ${file}.`);
|
|
213
|
+
printAwaitingApproval('Thread', data);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// ── x delete ────────────────────────────────────────────────────────────
|
|
217
|
+
|
|
218
|
+
x
|
|
219
|
+
.command('delete')
|
|
220
|
+
.description('Delete an existing X post. Files an owner approval card and returns immediately; never deletes directly.')
|
|
221
|
+
.argument('<tweet_id>', 'id of the tweet to delete')
|
|
222
|
+
.action(async (tweetId) => {
|
|
223
|
+
const data = await executeXTool({ client, auth, program }, 'x_delete', { tweet_id: tweetId });
|
|
224
|
+
|
|
225
|
+
if (program.opts().output === 'json') {
|
|
226
|
+
formatDetail(data, { format: 'json' });
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
printAwaitingApproval('Delete', data);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
// ── x like / x repost (P10 slice B engagement writes, HITL like all writes) ──
|
|
233
|
+
|
|
234
|
+
x
|
|
235
|
+
.command('like')
|
|
236
|
+
.description('Like a tweet as the org account. Files an owner approval card and returns immediately; never likes directly. Capped by the daily engagement budget.')
|
|
237
|
+
.argument('<tweet_id>', 'id of the tweet to like')
|
|
238
|
+
.action(async (tweetId) => {
|
|
239
|
+
const data = await executeXTool({ client, auth, program }, 'x_like', { tweet_id: tweetId });
|
|
240
|
+
if (program.opts().output === 'json') {
|
|
241
|
+
formatDetail(data, { format: 'json' });
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
printAwaitingApproval('Like', data);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
x
|
|
248
|
+
.command('repost')
|
|
249
|
+
.description('Repost (retweet) a tweet as the org account. Files an owner approval card and returns immediately; never reposts directly. Capped by the daily engagement budget.')
|
|
250
|
+
.argument('<tweet_id>', 'id of the tweet to repost')
|
|
251
|
+
.action(async (tweetId) => {
|
|
252
|
+
const data = await executeXTool({ client, auth, program }, 'x_repost', { tweet_id: tweetId });
|
|
253
|
+
if (program.opts().output === 'json') {
|
|
254
|
+
formatDetail(data, { format: 'json' });
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
printAwaitingApproval('Repost', data);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
// ── read verbs (P10 parity: the connector read tools, one paid call each) ──
|
|
261
|
+
|
|
262
|
+
x
|
|
263
|
+
.command('read')
|
|
264
|
+
.description(`Read one tweet by id, with public metrics. ${READ_COST_NOTE}`)
|
|
265
|
+
.argument('<tweet_id>', 'id of the tweet to read')
|
|
266
|
+
.action(async (tweetId) => {
|
|
267
|
+
const data = await executeXTool({ client, auth, program }, 'twitter_get_tweet', { tweet_id: tweetId });
|
|
268
|
+
const tweet = data?.result || data;
|
|
269
|
+
if (program.opts().output === 'json') {
|
|
270
|
+
formatDetail(tweet, { format: 'json' });
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
printTweetLine(tweet);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
x
|
|
277
|
+
.command('timeline')
|
|
278
|
+
.description(`Read recent posts from the connected account's timeline. ${READ_COST_NOTE}`)
|
|
279
|
+
.option('--count <n>', 'posts to return, 1-20 (default 10)')
|
|
280
|
+
.option('--user <user_id>', 'read another user\'s timeline instead of the connected account')
|
|
281
|
+
.action(async (options) => {
|
|
282
|
+
const input = {};
|
|
283
|
+
if (options.count) input.max_results = Number(options.count);
|
|
284
|
+
if (options.user) input.user_id = options.user;
|
|
285
|
+
const data = await executeXTool({ client, auth, program }, 'twitter_get_user_tweets', input);
|
|
286
|
+
const result = data?.result || data;
|
|
287
|
+
if (program.opts().output === 'json') {
|
|
288
|
+
formatDetail(result, { format: 'json' });
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
const tweets = result?.tweets || [];
|
|
292
|
+
printInfo(`${tweets.length} post(s) for user ${result?.user_id || '(connected account)'}:`);
|
|
293
|
+
tweets.forEach(printTweetLine);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
x
|
|
297
|
+
.command('search')
|
|
298
|
+
.description(`Search recent tweets (last ~7 days, X search operators supported). ${READ_COST_NOTE}`)
|
|
299
|
+
.argument('<query>', 'X search query')
|
|
300
|
+
.option('--count <n>', 'results to return, 1-20 (default 10)')
|
|
301
|
+
.action(async (query, options) => {
|
|
302
|
+
const input = { query };
|
|
303
|
+
if (options.count) input.max_results = Number(options.count);
|
|
304
|
+
const data = await executeXTool({ client, auth, program }, 'twitter_search_recent', input);
|
|
305
|
+
const result = data?.result || data;
|
|
306
|
+
if (program.opts().output === 'json') {
|
|
307
|
+
formatDetail(result, { format: 'json' });
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
const tweets = result?.tweets || [];
|
|
311
|
+
printInfo(`${tweets.length} result(s) for "${query}":`);
|
|
312
|
+
tweets.forEach(printTweetLine);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
x
|
|
316
|
+
.command('mentions')
|
|
317
|
+
.description(`Read recent mentions of the connected account. ${READ_COST_NOTE} The mentions poller already covers this on a schedule when an org opts in; this verb is the on-demand check.`)
|
|
318
|
+
.option('--since <tweet_id>', 'only mentions newer than this tweet id')
|
|
319
|
+
.action(async (options) => {
|
|
320
|
+
const input = {};
|
|
321
|
+
if (options.since) input.since_id = options.since;
|
|
322
|
+
const data = await executeXTool({ client, auth, program }, 'twitter_get_mentions', input);
|
|
323
|
+
const result = data?.result || data;
|
|
324
|
+
if (program.opts().output === 'json') {
|
|
325
|
+
formatDetail(result, { format: 'json' });
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
const mentions = result?.mentions || [];
|
|
329
|
+
printInfo(`${mentions.length} mention(s):`);
|
|
330
|
+
mentions.forEach(printTweetLine);
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
// ── x status ────────────────────────────────────────────────────────────
|
|
334
|
+
|
|
335
|
+
x
|
|
336
|
+
.command('status')
|
|
337
|
+
.description('Show current posting budget usage and X credential health')
|
|
338
|
+
.action(async () => {
|
|
339
|
+
auth.requireAuth();
|
|
340
|
+
const orgId = resolveOrgId(auth, program);
|
|
341
|
+
const ep = resolveEndpoint('v1.tools.execute');
|
|
342
|
+
|
|
343
|
+
const budgetData = await client.post(ep.path, {
|
|
344
|
+
service: ep.service,
|
|
345
|
+
body: { tool_name: 'x_budget_status', input: {}, org_id: orgId },
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
let credResult = null;
|
|
349
|
+
let credError = null;
|
|
350
|
+
try {
|
|
351
|
+
const credData = await client.post(ep.path, {
|
|
352
|
+
service: ep.service,
|
|
353
|
+
body: { tool_name: 'twitter_verify_credentials', input: {}, org_id: orgId },
|
|
354
|
+
});
|
|
355
|
+
if (credData && credData.success === false) {
|
|
356
|
+
credError = credData.error || 'unknown error';
|
|
357
|
+
} else {
|
|
358
|
+
credResult = credData && credData.result ? credData.result : credData;
|
|
359
|
+
}
|
|
360
|
+
} catch (err) {
|
|
361
|
+
credError = err.message;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (program.opts().output === 'json') {
|
|
365
|
+
formatDetail({ budget: budgetData, credentials: credResult, credentials_error: credError }, { format: 'json' });
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (budgetData && budgetData.success === false) {
|
|
370
|
+
printWarning(`Budget status unavailable: ${budgetData.error || 'unknown error'}`);
|
|
371
|
+
} else {
|
|
372
|
+
console.log(`Daily: ${budgetData.daily_used}/${budgetData.daily_limit} posts (rolling 24h)`);
|
|
373
|
+
console.log(`Monthly: ${budgetData.monthly_used}/${budgetData.monthly_limit} posts (rolling 30d)`);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if (credError) {
|
|
377
|
+
printWarning(`X credential check failed (twitter_verify_credentials via POST ${ep.path}): ${credError}`);
|
|
378
|
+
} else if (credResult) {
|
|
379
|
+
printSuccess(`X credential OK — authenticated as @${credResult.username} (${credResult.name || credResult.id})`);
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
// ── x media-root ────────────────────────────────────────────────────────
|
|
384
|
+
|
|
385
|
+
x
|
|
386
|
+
.command('media-root')
|
|
387
|
+
.description('Print the X media root directory. Image files for `x post --image` / thread `!image:` lines must live under here, in your org\'s own subdirectory.')
|
|
388
|
+
.action(async () => {
|
|
389
|
+
if (program.opts().output === 'json') {
|
|
390
|
+
formatDetail({ media_root: DEFAULT_MEDIA_ROOT }, { format: 'json' });
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
console.log(DEFAULT_MEDIA_ROOT);
|
|
394
|
+
printInfo(`Place image files under ${DEFAULT_MEDIA_ROOT}/<your-org-id>/... — the server rejects any path that doesn't resolve inside your org's own subdirectory.`);
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
// ── x media upload ────────────────────────────────────────────────────────
|
|
398
|
+
// quox#592: an agent/CLI could DRAFT a post with imagePaths but had no
|
|
399
|
+
// governed way to GET the image into the org media root (it lives inside the
|
|
400
|
+
// collector container). This uploads a LOCAL file through the SAME governed
|
|
401
|
+
// endpoint the dashboard Compose UI uses (POST /x/media: entitlement-checked,
|
|
402
|
+
// org-scoped, content-sniffed, size-capped, server writes to the org root),
|
|
403
|
+
// then prints the relative name to pass to `x post --image`.
|
|
404
|
+
const media = x.command('media').description('X media files for post attachments');
|
|
405
|
+
media
|
|
406
|
+
.command('upload <path>')
|
|
407
|
+
.description('Upload a local image/GIF/MP4 into your org\'s X media root (governed: entitlement + org-scoped + content-validated server-side). Prints the relative name for `x post --image`.')
|
|
408
|
+
.action(async (filePath) => {
|
|
409
|
+
auth.requireAuth();
|
|
410
|
+
// fleet #381: node:buffer exports Blob but NOT FormData (that is a
|
|
411
|
+
// global from undici on Node >= 18). Import only Blob; use global FormData.
|
|
412
|
+
const { Blob } = await import('node:buffer');
|
|
413
|
+
const { readFile } = await import('node:fs/promises');
|
|
414
|
+
const { basename } = await import('node:path');
|
|
415
|
+
|
|
416
|
+
const content = await readFile(filePath);
|
|
417
|
+
if (!content.length) throw new Error(`refusing to upload an empty file: ${filePath}`);
|
|
418
|
+
const formData = new FormData();
|
|
419
|
+
// The route reads `mediaUpload.single('file')` — the field name is 'file'.
|
|
420
|
+
formData.append('file', new Blob([content]), basename(filePath));
|
|
421
|
+
|
|
422
|
+
const ep = resolveEndpoint('x.media.upload');
|
|
423
|
+
const orgId = resolveOrgId(auth, program);
|
|
424
|
+
const data = await client.post(ep.path, {
|
|
425
|
+
service: ep.service,
|
|
426
|
+
body: formData,
|
|
427
|
+
...(orgId ? { headers: { 'x-org-id': orgId } } : {}),
|
|
428
|
+
timeout: 120000,
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
if (program.opts().output === 'json') { formatDetail(data, { format: 'json' }); return; }
|
|
432
|
+
const name = data?.file?.name;
|
|
433
|
+
if (!name) {
|
|
434
|
+
throw new Error(`upload failed: server accepted the request but returned no file name (got: ${JSON.stringify(data).slice(0, 160)})`);
|
|
435
|
+
}
|
|
436
|
+
printSuccess(`Uploaded ${name} (${data.file.content_type}, ${data.file.size} bytes).`);
|
|
437
|
+
printInfo(`Attach it: quox x post "<text>" --image ${name}`);
|
|
438
|
+
});
|
|
439
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* actingAs.js — `--as <agent>` / QUOX_AS (quox#772).
|
|
3
|
+
*
|
|
4
|
+
* Opt-in: when set, every platform request the CLI client makes carries the
|
|
5
|
+
* named agent's stored ATTACH token as the bearer instead of the profile /
|
|
6
|
+
* resident token, so evidence credits the agent and its grant scope governs
|
|
7
|
+
* the verb. Never falls back to the human identity when it was requested.
|
|
8
|
+
*
|
|
9
|
+
* `--as` is stripped from argv BEFORE commander parses (it is not a commander
|
|
10
|
+
* option) because linkedin/claude already own a `--as` of their own and a root
|
|
11
|
+
* declaration would shadow theirs (required options would then fail).
|
|
12
|
+
*/
|
|
13
|
+
import { readFileSync } from 'node:fs';
|
|
14
|
+
import { QuoxError } from './errors.js';
|
|
15
|
+
|
|
16
|
+
// Commands whose own `--as` means something else.
|
|
17
|
+
const OWNS_AS = new Set(['linkedin', 'claude']);
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @returns {{name: string|null, argv: string[]}} acting agent (flag beats env)
|
|
21
|
+
* and argv with the global --as removed.
|
|
22
|
+
*/
|
|
23
|
+
export function extractActingAs(argv, env = process.env) {
|
|
24
|
+
const first = argv.find((a) => !a.startsWith('-'));
|
|
25
|
+
const envName = (env.QUOX_AS || '').trim() || null;
|
|
26
|
+
if (first && OWNS_AS.has(first)) return { name: envName, argv };
|
|
27
|
+
const out = [];
|
|
28
|
+
let name = null;
|
|
29
|
+
for (let i = 0; i < argv.length; i++) {
|
|
30
|
+
const a = argv[i];
|
|
31
|
+
if (a === '--') { out.push(...argv.slice(i)); break; }
|
|
32
|
+
if (a === '--as' && argv[i + 1] && !argv[i + 1].startsWith('-')) { name = argv[++i]; continue; }
|
|
33
|
+
if (a.startsWith('--as=') && a.length > 5) { name = a.slice(5); continue; }
|
|
34
|
+
out.push(a);
|
|
35
|
+
}
|
|
36
|
+
return { name: (name || '').trim() || envName, argv: out };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The EXPLICIT acting agent (QUOX_AS; bin/quox.js folds --as into it). May be 'self'. */
|
|
40
|
+
export function actingAsName(env = process.env) {
|
|
41
|
+
return (env.QUOX_AS || '').trim() || null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Verbs that manage the attach itself (or must run as the human) never get the
|
|
45
|
+
// implicit attached-session default. Explicit --as / QUOX_AS still applies.
|
|
46
|
+
const EXEMPT_TOP = new Set(['login', 'logout', 'whoami', 'slot', 'memory']);
|
|
47
|
+
const EXEMPT_AGENT_SUB = new Set([
|
|
48
|
+
'attach', 'detach', 'keep', 'attach-status', 'attach-heartbeat', 'exec',
|
|
49
|
+
'bind-slot', 'unbind-slot', 'grant-attach', 'grant-attach-all',
|
|
50
|
+
'revoke-attach-all', 'revoke-attach', 'list-grants',
|
|
51
|
+
]);
|
|
52
|
+
const VALUE_FLAGS = new Set(['-o', '--output', '--org', '-p', '--profile']);
|
|
53
|
+
|
|
54
|
+
/** First two positional words of argv, skipping global flags (and their values). */
|
|
55
|
+
function leadingWords(argv) {
|
|
56
|
+
const words = [];
|
|
57
|
+
for (let i = 0; i < argv.length && words.length < 2; i++) {
|
|
58
|
+
const a = argv[i];
|
|
59
|
+
if (a === '--') break;
|
|
60
|
+
if (VALUE_FLAGS.has(a)) { i++; continue; }
|
|
61
|
+
if (a.startsWith('-')) continue;
|
|
62
|
+
words.push(a);
|
|
63
|
+
}
|
|
64
|
+
return words;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function isDefaultExempt(argv) {
|
|
68
|
+
const [top, sub] = leadingWords(argv);
|
|
69
|
+
if (!top) return true;
|
|
70
|
+
if (EXEMPT_TOP.has(top)) return true;
|
|
71
|
+
return top === 'agent' && EXEMPT_AGENT_SUB.has(sub);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Did the command line (or QUOX_PROFILE) name a profile explicitly? */
|
|
75
|
+
export function hasExplicitProfile(argv, env = process.env) {
|
|
76
|
+
if ((env.QUOX_PROFILE || '').trim()) return true;
|
|
77
|
+
return argv.some((a) => a === '-p' || a === '--profile' || a.startsWith('--profile=') || /^-p.+/.test(a));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The single place that decides who plain verbs act as.
|
|
82
|
+
* Order: --as, QUOX_AS ('self' = own identity), explicit -p/--profile, the
|
|
83
|
+
* session's sole attach (implicit default), else the profile identity.
|
|
84
|
+
* @returns {{name: string|null, source: 'flag'|'env'|'self'|'profile'|'default'|null}}
|
|
85
|
+
*/
|
|
86
|
+
export async function resolveActing({ argv = process.argv.slice(2), env = process.env, resolveAttach } = {}) {
|
|
87
|
+
const explicit = actingAsName(env);
|
|
88
|
+
if (explicit) {
|
|
89
|
+
const source = env.QUOX_AS_FLAG === '1' ? 'flag' : 'env';
|
|
90
|
+
if (explicit.toLowerCase() === 'self') return { name: null, source: 'self', via: source };
|
|
91
|
+
return { name: explicit, source };
|
|
92
|
+
}
|
|
93
|
+
if (hasExplicitProfile(argv, env)) return { name: null, source: 'profile' };
|
|
94
|
+
if (isDefaultExempt(argv)) return { name: null, source: null };
|
|
95
|
+
const mine = resolveAttach ? resolveAttach() : (await import('../commands/agentAttach.js')).resolveMyAttach();
|
|
96
|
+
if (mine?.agentId) return { name: mine.agentId, source: 'default' };
|
|
97
|
+
return { name: null, source: null };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Resolve the attach token for `agentId` in this session's scope, refreshing
|
|
103
|
+
* via the existing attach heartbeat when near expiry. Throws if not attached.
|
|
104
|
+
* `heartbeatClient` must NOT itself act-as (it authenticates as the holder).
|
|
105
|
+
*/
|
|
106
|
+
export async function resolveActingToken(agentId, heartbeatClient, { implicit = false } = {}) {
|
|
107
|
+
const { tokenPath, ensureFreshAttachToken } = await import('../commands/agentAttach.js');
|
|
108
|
+
let present = false;
|
|
109
|
+
try { present = Boolean(readFileSync(tokenPath(agentId), 'utf-8').trim()); } catch { /* absent */ }
|
|
110
|
+
if (!present) {
|
|
111
|
+
throw new QuoxError(
|
|
112
|
+
`--as/QUOX_AS requested agent "${agentId}" but this session has no attach token for it. `
|
|
113
|
+
+ `Run \`quox agent attach ${agentId}\` first (refusing to fall back to your own identity).`,
|
|
114
|
+
3,
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
const token = await ensureFreshAttachToken(heartbeatClient, agentId);
|
|
118
|
+
if (implicit) {
|
|
119
|
+
const exp = jwtExp(token);
|
|
120
|
+
if (exp && exp <= Math.floor(Date.now() / 1000)) {
|
|
121
|
+
throw new QuoxError(
|
|
122
|
+
`This session is attached as "${agentId}" and plain quox verbs act as it by default, but the attach lease expired and could not be renewed. `
|
|
123
|
+
+ `Re-attach with \`quox agent attach ${agentId}\`, or act as yourself with \`--as self\` (or \`-p <profile>\`). `
|
|
124
|
+
+ 'Refusing to fall back to your own identity silently.',
|
|
125
|
+
3,
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return token;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function jwtExp(token) {
|
|
133
|
+
try {
|
|
134
|
+
return JSON.parse(Buffer.from(String(token).split('.')[1], 'base64url').toString()).exp || null;
|
|
135
|
+
} catch { return null; }
|
|
136
|
+
}
|
package/lib/core/aee.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AEE envelope emitter for CLI commands
|
|
3
|
+
*
|
|
4
|
+
* Emits lightweight event envelopes so CLI actions appear in the
|
|
5
|
+
* AEE audit trail. Fire-and-forget — failures are silently ignored
|
|
6
|
+
* so they never block the command itself.
|
|
7
|
+
*
|
|
8
|
+
* CLI-3 EVIDENCED dimension (quoxai/quox#290)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { resolveEndpoint } from '../api/endpoints.js';
|
|
12
|
+
|
|
13
|
+
const PROTOCOL_VERSION = '1';
|
|
14
|
+
|
|
15
|
+
function ulid() {
|
|
16
|
+
const t = Date.now().toString(36).padStart(10, '0');
|
|
17
|
+
const r = Array.from({ length: 16 }, () => Math.random().toString(36)[2]).join('');
|
|
18
|
+
return (t + r).toUpperCase();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Emit an AEE event envelope for a CLI command invocation.
|
|
23
|
+
*
|
|
24
|
+
* @param {Object} client - The CLI HTTP client (ctx.client)
|
|
25
|
+
* @param {Object} params
|
|
26
|
+
* @param {string} params.command - CLI command name (e.g. 'status', 'doctor')
|
|
27
|
+
* @param {string} params.intent - AEE intent string (e.g. 'quox.cli.status')
|
|
28
|
+
* @param {Object} [params.payload] - Command-specific result data
|
|
29
|
+
* @param {string} [params.status] - 'success' | 'error'
|
|
30
|
+
*/
|
|
31
|
+
export async function emitCliEvent(client, { command, intent, payload = {}, status = 'success' }) {
|
|
32
|
+
try {
|
|
33
|
+
const envelope = {
|
|
34
|
+
v: PROTOCOL_VERSION,
|
|
35
|
+
id: ulid(),
|
|
36
|
+
ts: new Date().toISOString(),
|
|
37
|
+
type: 'event',
|
|
38
|
+
from: 'cli.quox',
|
|
39
|
+
to: 'system.audit',
|
|
40
|
+
intent,
|
|
41
|
+
corr: null,
|
|
42
|
+
reply_to: null,
|
|
43
|
+
payload: {
|
|
44
|
+
command,
|
|
45
|
+
status,
|
|
46
|
+
...payload,
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
await client.post(resolveEndpoint('aee.store').path, { service: resolveEndpoint('aee.store').service, body: envelope, timeout: 3000 });
|
|
50
|
+
} catch {
|
|
51
|
+
// Fire-and-forget — never block CLI output
|
|
52
|
+
}
|
|
53
|
+
}
|