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,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QuoxBox commands: status, workflows, tasks, and HITL approvals for the
|
|
3
|
+
* governed Mac-mini agent gateway, proxied through the collector
|
|
4
|
+
* (services/collector/routes/quoxbox-proxy.js, mounted at /api/quoxbox).
|
|
5
|
+
*
|
|
6
|
+
* quox quoxbox <subcommand> [options]
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import chalk from 'chalk';
|
|
10
|
+
import { formatOutput, formatDetail, printSuccess, printWarning } from '../core/output.js';
|
|
11
|
+
import { resolveEndpoint } from '../api/endpoints.js';
|
|
12
|
+
|
|
13
|
+
const VALID_MODES = ['readonly', 'propose', 'actuate'];
|
|
14
|
+
|
|
15
|
+
/** Commander collector for repeatable --param key=value flags -> { key: value, ... }. */
|
|
16
|
+
function collectParam(input, previous) {
|
|
17
|
+
const idx = input.indexOf('=');
|
|
18
|
+
if (idx === -1) {
|
|
19
|
+
throw new Error(`Invalid --param value "${input}". Expected key=value.`);
|
|
20
|
+
}
|
|
21
|
+
const key = input.slice(0, idx).trim();
|
|
22
|
+
const value = input.slice(idx + 1);
|
|
23
|
+
if (!key) {
|
|
24
|
+
throw new Error(`Invalid --param value "${input}". Key cannot be empty.`);
|
|
25
|
+
}
|
|
26
|
+
return { ...previous, [key]: value };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function statusDot(ok) {
|
|
30
|
+
return ok ? chalk.green('●') : chalk.red('●');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function renderStatus(data) {
|
|
34
|
+
const pending = data.pending || [];
|
|
35
|
+
const tasks = data.tasks || [];
|
|
36
|
+
const lines = [];
|
|
37
|
+
|
|
38
|
+
lines.push('');
|
|
39
|
+
lines.push(chalk.bold(' QUOXBOX STATUS') + chalk.dim(` mode: ${data.mode}`));
|
|
40
|
+
lines.push(chalk.dim(' ' + '─'.repeat(60)));
|
|
41
|
+
lines.push(` ${statusDot(!data.halt)} Halt ${data.halt ? chalk.red('yes') : chalk.green('no')}`);
|
|
42
|
+
lines.push(` ${statusDot(data.screenLocked === false)} Screen Locked ${data.screenLocked ? chalk.yellow('yes') : chalk.green('no')}`);
|
|
43
|
+
lines.push(` ${statusDot(!data.breakerTripped)} Breaker Tripped ${data.breakerTripped ? chalk.red('yes') : chalk.green('no')}`);
|
|
44
|
+
lines.push(` ${statusDot(true)} Pending Approvals ${pending.length > 0 ? chalk.yellow(pending.length) : pending.length}`);
|
|
45
|
+
lines.push(` ${statusDot(true)} Actuations/hr ${data.actuationsLastHour ?? 0}`);
|
|
46
|
+
|
|
47
|
+
if (tasks.length > 0) {
|
|
48
|
+
lines.push('');
|
|
49
|
+
lines.push(chalk.bold(' Recent Tasks'));
|
|
50
|
+
for (const t of tasks) {
|
|
51
|
+
lines.push(` ${chalk.cyan(t.id)} ${t.workflow} ${t.status}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
lines.push('');
|
|
56
|
+
return lines.join('\n');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function renderTask(task) {
|
|
60
|
+
const lines = [];
|
|
61
|
+
lines.push('');
|
|
62
|
+
lines.push(chalk.bold(` Task: ${task.id}`));
|
|
63
|
+
lines.push(chalk.dim(' ' + '─'.repeat(60)));
|
|
64
|
+
lines.push(` Workflow ${task.workflow}`);
|
|
65
|
+
if (task.desc) lines.push(` Desc ${task.desc}`);
|
|
66
|
+
lines.push(` Status ${task.status}`);
|
|
67
|
+
lines.push(` Progress ${task.cursor}/${task.total}`);
|
|
68
|
+
lines.push(` Started ${task.startedAt || '-'}`);
|
|
69
|
+
lines.push(` Finished ${task.finishedAt || '-'}`);
|
|
70
|
+
|
|
71
|
+
const steps = task.steps || [];
|
|
72
|
+
if (steps.length > 0) {
|
|
73
|
+
lines.push('');
|
|
74
|
+
lines.push(chalk.bold(' Steps'));
|
|
75
|
+
steps.forEach((step, i) => {
|
|
76
|
+
if (!step) {
|
|
77
|
+
lines.push(` ${i + 1}. ${chalk.dim('(pending)')}`);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const label = step.label || step.tool || `step ${i + 1}`;
|
|
81
|
+
lines.push(` ${i + 1}. ${label} ${chalk.dim(step.status || '-')}`);
|
|
82
|
+
if (step.status === 'awaiting_approval' && (step.pendingId || task.pendingId)) {
|
|
83
|
+
const approvalId = step.pendingId || task.pendingId;
|
|
84
|
+
lines.push(` ${chalk.yellow.bold(`⚠ HELD — approval required: ${approvalId}`)}`);
|
|
85
|
+
lines.push(` ${chalk.dim(`Run \`quox quoxbox approve ${approvalId}\` or \`quox quoxbox deny ${approvalId}\``)}`);
|
|
86
|
+
}
|
|
87
|
+
if (step.detail) lines.push(` ${chalk.dim(step.detail)}`);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
lines.push('');
|
|
92
|
+
return lines.join('\n');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function registerQuoxboxCommands(program, ctx) {
|
|
96
|
+
const { client, auth } = ctx;
|
|
97
|
+
|
|
98
|
+
const quoxbox = program
|
|
99
|
+
.command('quoxbox')
|
|
100
|
+
.description('QuoxBox governed Mac-mini agent — status, workflows, tasks, HITL approvals');
|
|
101
|
+
|
|
102
|
+
// ── status ───────────────────────────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
quoxbox
|
|
105
|
+
.command('status')
|
|
106
|
+
.description('Show QuoxBox live status: mode, halt, breaker, pending approvals, recent tasks')
|
|
107
|
+
.action(async () => {
|
|
108
|
+
auth.requireAuth();
|
|
109
|
+
const ep = resolveEndpoint('quoxbox.live');
|
|
110
|
+
const data = await client.get(ep.path, { service: ep.service });
|
|
111
|
+
|
|
112
|
+
const format = program.opts().output;
|
|
113
|
+
if (format === 'json') {
|
|
114
|
+
console.log(JSON.stringify(data, null, 2));
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
if (format === 'quiet') {
|
|
118
|
+
console.log(data.mode || '');
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
console.log(renderStatus(data));
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// ── workflows ────────────────────────────────────────────────────────────
|
|
125
|
+
|
|
126
|
+
quoxbox
|
|
127
|
+
.command('workflows')
|
|
128
|
+
.description('List available governed workflows')
|
|
129
|
+
.action(async () => {
|
|
130
|
+
auth.requireAuth();
|
|
131
|
+
const ep = resolveEndpoint('quoxbox.workflows.list');
|
|
132
|
+
const data = await client.get(ep.path, { service: ep.service });
|
|
133
|
+
formatOutput({
|
|
134
|
+
columns: ['id', 'desc', 'params', 'steps'],
|
|
135
|
+
headers: ['ID', 'Description', 'Params', 'Steps'],
|
|
136
|
+
rows: (data.workflows || []).map((w) => ({
|
|
137
|
+
...w,
|
|
138
|
+
params: (w.params || []).map((p) => p.name || p).join(', '),
|
|
139
|
+
})),
|
|
140
|
+
}, { format: program.opts().output, emptyMessage: 'No workflows registered.' });
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// ── run ──────────────────────────────────────────────────────────────────
|
|
144
|
+
|
|
145
|
+
quoxbox
|
|
146
|
+
.command('run')
|
|
147
|
+
.description('Start a governed workflow task')
|
|
148
|
+
.argument('<workflow>', 'workflow ID')
|
|
149
|
+
.option('--param <key=value>', 'workflow param (repeatable)', collectParam, {})
|
|
150
|
+
.option('--mode <mode>', 'override gateway mode for this call (readonly|propose|actuate)')
|
|
151
|
+
.action(async (workflow, options) => {
|
|
152
|
+
auth.requireAuth();
|
|
153
|
+
if (options.mode && !VALID_MODES.includes(options.mode)) {
|
|
154
|
+
throw new Error(`--mode must be one of: ${VALID_MODES.join(', ')}`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const body = { workflow, params: options.param || {} };
|
|
158
|
+
if (options.mode) body.mode = options.mode;
|
|
159
|
+
|
|
160
|
+
const ep = resolveEndpoint('quoxbox.task.run');
|
|
161
|
+
const data = await client.post(ep.path, { service: ep.service, body });
|
|
162
|
+
const task = data.task || data;
|
|
163
|
+
|
|
164
|
+
if (program.opts().output === 'json') {
|
|
165
|
+
console.log(JSON.stringify(data, null, 2));
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
printSuccess(`Task started: ${task.id} (workflow: ${task.workflow}, status: ${task.status})`);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// ── task ─────────────────────────────────────────────────────────────────
|
|
172
|
+
|
|
173
|
+
quoxbox
|
|
174
|
+
.command('task')
|
|
175
|
+
.description('Show full task detail, including step states')
|
|
176
|
+
.argument('<taskId>', 'task ID')
|
|
177
|
+
.action(async (taskId) => {
|
|
178
|
+
auth.requireAuth();
|
|
179
|
+
const ep = resolveEndpoint('quoxbox.task.get', { taskId });
|
|
180
|
+
const data = await client.get(ep.path, { service: ep.service });
|
|
181
|
+
const task = data.task || data;
|
|
182
|
+
|
|
183
|
+
const format = program.opts().output;
|
|
184
|
+
if (format === 'json') {
|
|
185
|
+
console.log(JSON.stringify(data, null, 2));
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
if (format === 'quiet') {
|
|
189
|
+
console.log(task.id || '');
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
console.log(renderTask(task));
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// ── tasks ────────────────────────────────────────────────────────────────
|
|
196
|
+
|
|
197
|
+
quoxbox
|
|
198
|
+
.command('tasks')
|
|
199
|
+
.description('List recent tasks (from live status)')
|
|
200
|
+
.action(async () => {
|
|
201
|
+
auth.requireAuth();
|
|
202
|
+
const ep = resolveEndpoint('quoxbox.live');
|
|
203
|
+
const data = await client.get(ep.path, { service: ep.service });
|
|
204
|
+
formatOutput({
|
|
205
|
+
columns: ['id', 'workflow', 'status', 'startedAt'],
|
|
206
|
+
headers: ['Task ID', 'Workflow', 'Status', 'Started'],
|
|
207
|
+
rows: data.tasks || [],
|
|
208
|
+
}, { format: program.opts().output, emptyMessage: 'No recent tasks.' });
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
// ── approve / deny ───────────────────────────────────────────────────────
|
|
212
|
+
|
|
213
|
+
quoxbox
|
|
214
|
+
.command('approve')
|
|
215
|
+
.description('Approve a pending HITL action')
|
|
216
|
+
.argument('<approvalId>', 'pending approval ID')
|
|
217
|
+
.action(async (approvalId) => {
|
|
218
|
+
auth.requireAuth();
|
|
219
|
+
const ep = resolveEndpoint('quoxbox.approve', { approvalId });
|
|
220
|
+
const data = await client.post(ep.path, { service: ep.service, body: {} });
|
|
221
|
+
|
|
222
|
+
if (program.opts().output === 'json') {
|
|
223
|
+
console.log(JSON.stringify(data, null, 2));
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
formatDetail(data, { format: program.opts().output, title: `Approved: ${approvalId}` });
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
quoxbox
|
|
230
|
+
.command('deny')
|
|
231
|
+
.description('Deny a pending HITL action')
|
|
232
|
+
.argument('<approvalId>', 'pending approval ID')
|
|
233
|
+
.action(async (approvalId) => {
|
|
234
|
+
auth.requireAuth();
|
|
235
|
+
const ep = resolveEndpoint('quoxbox.deny', { approvalId });
|
|
236
|
+
const data = await client.post(ep.path, { service: ep.service, body: {} });
|
|
237
|
+
|
|
238
|
+
if (program.opts().output === 'json') {
|
|
239
|
+
console.log(JSON.stringify(data, null, 2));
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
formatDetail(data, { format: program.opts().output, title: `Denied: ${approvalId}` });
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
// ── mode ─────────────────────────────────────────────────────────────────
|
|
246
|
+
|
|
247
|
+
quoxbox
|
|
248
|
+
.command('mode')
|
|
249
|
+
.description('Set the QuoxBox gateway mode (readonly|propose|actuate)')
|
|
250
|
+
.argument('<mode>', 'readonly | propose | actuate')
|
|
251
|
+
.action(async (mode) => {
|
|
252
|
+
auth.requireAuth();
|
|
253
|
+
if (!VALID_MODES.includes(mode)) {
|
|
254
|
+
throw new Error(`mode must be one of: ${VALID_MODES.join(', ')}`);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (mode === 'actuate') {
|
|
258
|
+
printWarning('WARNING: actuate mode allows QuoxBox to execute governed actions on approval/policy pass, without further confirmation from this CLI.');
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const ep = resolveEndpoint('quoxbox.mode.set');
|
|
262
|
+
const data = await client.post(ep.path, { service: ep.service, body: { mode } });
|
|
263
|
+
|
|
264
|
+
if (program.opts().output === 'json') {
|
|
265
|
+
console.log(JSON.stringify(data, null, 2));
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
printSuccess(`QuoxBox mode set to: ${data.mode || mode}`);
|
|
269
|
+
});
|
|
270
|
+
}
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QuoxChat commands: website-widget list + operator availability + live-support
|
|
3
|
+
* desk parity (QuoxChat Pro).
|
|
4
|
+
*
|
|
5
|
+
* quox quoxchat widgets
|
|
6
|
+
* quox quoxchat availability <widgetId> [--online|--offline]
|
|
7
|
+
* quox quoxchat conversations <widgetId> [--status <status>] [--waiting]
|
|
8
|
+
* quox quoxchat transcript <widgetId> <conversationId>
|
|
9
|
+
* quox quoxchat reply <widgetId> <conversationId> <message...>
|
|
10
|
+
*
|
|
11
|
+
* Backend: services/auth/routes/widgets.js (mounted at plain `/api`,
|
|
12
|
+
* requireOrgMembership): GET/PATCH `/api/orgs/:orgId/widgets[/:widgetId]`,
|
|
13
|
+
* GET `/api/orgs/:orgId/widgets/:widgetId/conversations[/:conversationId/messages]`.
|
|
14
|
+
* `operator_online` and `office_hours_json` are the QuoxChat Pro availability
|
|
15
|
+
* fields; plan-gated PATCH fields return 402 `plan_upgrade_required`.
|
|
16
|
+
*
|
|
17
|
+
* Operator reply is a separate org-authed router: POST
|
|
18
|
+
* `/api/orgs/:orgId/widgets/:widgetId/conversations/:conversationId/operator-reply`
|
|
19
|
+
* in services/auth/routes/internal-widget-matrix.js (widgetConversationOrgRouter).
|
|
20
|
+
* It derives the sender name server-side and returns 409 when the
|
|
21
|
+
* conversation is not `handed_off`.
|
|
22
|
+
*
|
|
23
|
+
* Desk status label + visitor display precedence mirror
|
|
24
|
+
* src/pages/views/LiveSupportView.jsx (convDeskStatus / visitorDisplay) so the
|
|
25
|
+
* CLI shows the same picture as the dashboard live-support desk.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { formatOutput, formatDetail, printSuccess } from '../core/output.js';
|
|
29
|
+
import { resolveOrgId } from './helpers.js';
|
|
30
|
+
import { resolveEndpoint } from '../api/endpoints.js';
|
|
31
|
+
|
|
32
|
+
/** Reduce a full widget row to the availability-relevant fields. */
|
|
33
|
+
function availabilityView(widget) {
|
|
34
|
+
return {
|
|
35
|
+
id: widget.id,
|
|
36
|
+
name: widget.name,
|
|
37
|
+
status: widget.status,
|
|
38
|
+
operator_online: Boolean(widget.operator_online),
|
|
39
|
+
office_hours: widget.office_hours_json ?? null,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Desk status, mirrors convDeskStatus() in LiveSupportView.jsx. A handed-off
|
|
44
|
+
// conversation that no operator has joined yet is WAITING whoever spoke last
|
|
45
|
+
// (the AI's handoff ack is the last turn right after a handoff, so keying off
|
|
46
|
+
// the last turn alone showed every fresh handoff as "AI active"). Otherwise
|
|
47
|
+
// the last turn decides: the visitor spoke last means waiting on a human, an
|
|
48
|
+
// operator spoke last means answered, anything else means the AI holds it.
|
|
49
|
+
function deskStatus(row) {
|
|
50
|
+
if (row?.status === 'handed_off') {
|
|
51
|
+
if (!row?.operator_joined_at && row?.last_message_role !== 'operator') return 'waiting';
|
|
52
|
+
return row?.last_message_role === 'user' ? 'waiting' : 'answered';
|
|
53
|
+
}
|
|
54
|
+
if (row?.last_message_role === 'user') return 'waiting';
|
|
55
|
+
if (row?.last_message_role === 'operator') return 'answered';
|
|
56
|
+
return 'ai';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const DESK_STATUS_LABEL = { waiting: 'Waiting', answered: 'Answered', ai: 'AI active' };
|
|
60
|
+
|
|
61
|
+
// Handed-off rows say so, so the desk never reads a human-owned chat as AI.
|
|
62
|
+
function deskStatusLabel(row) {
|
|
63
|
+
const base = DESK_STATUS_LABEL[deskStatus(row)];
|
|
64
|
+
return row?.status === 'handed_off' ? `${base} (handed off)` : base;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Visitor display precedence mirrors visitorDisplay() in LiveSupportView.jsx:
|
|
68
|
+
// pre-handoff form name, then identify() attrs, then email, then a short
|
|
69
|
+
// readable visitor id. Never crops a name mid-word.
|
|
70
|
+
function visitorLabel(row) {
|
|
71
|
+
if (!row || typeof row !== 'object') return 'Visitor';
|
|
72
|
+
if (row.visitor_name && String(row.visitor_name).trim()) return String(row.visitor_name).trim();
|
|
73
|
+
let attrs = {};
|
|
74
|
+
if (row.visitor_identified_attrs_json) {
|
|
75
|
+
try {
|
|
76
|
+
const parsed = JSON.parse(row.visitor_identified_attrs_json);
|
|
77
|
+
if (parsed && typeof parsed === 'object') attrs = parsed;
|
|
78
|
+
} catch {
|
|
79
|
+
// malformed attrs, fall through to the other precedence levels
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const name = attrs.name || [attrs.first_name, attrs.last_name].filter(Boolean).join(' ');
|
|
83
|
+
if (name) return name;
|
|
84
|
+
if (row.visitor_email) return String(row.visitor_email);
|
|
85
|
+
if (attrs.email) return String(attrs.email);
|
|
86
|
+
const raw = String(row.visitor_id || '').replace(/^(visitor[-_]?|qc[-_]?)/i, '');
|
|
87
|
+
return raw ? `Visitor ${raw.slice(0, 8)}` : 'Visitor';
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Transcript rows mix SQLite "YYYY-MM-DD HH:MM:SS" (UTC, no marker) and ISO
|
|
91
|
+
// strings depending on which service wrote them; render both as one local
|
|
92
|
+
// "YYYY-MM-DD HH:MM" format.
|
|
93
|
+
function formatMessageTime(value) {
|
|
94
|
+
if (!value) return '-';
|
|
95
|
+
const raw = String(value);
|
|
96
|
+
const normalised = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(raw)
|
|
97
|
+
? `${raw.replace(' ', 'T')}Z`
|
|
98
|
+
: raw;
|
|
99
|
+
const d = new Date(normalised);
|
|
100
|
+
if (Number.isNaN(d.getTime())) return raw;
|
|
101
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
102
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Epoch ms for a created_at that may be SQLite 'YYYY-MM-DD HH:MM:SS' (UTC) or ISO. */
|
|
106
|
+
function messageEpoch(value) {
|
|
107
|
+
if (!value) return Number.POSITIVE_INFINITY;
|
|
108
|
+
const raw = String(value);
|
|
109
|
+
const t = Date.parse(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(\.\d+)?$/.test(raw) ? `${raw.replace(' ', 'T')}Z` : raw);
|
|
110
|
+
return Number.isNaN(t) ? Number.POSITIVE_INFINITY : t;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Strictly chronological by created_at; ties keep arrival order. */
|
|
114
|
+
export function sortByCreatedAt(messages) {
|
|
115
|
+
return messages
|
|
116
|
+
.map((m, i) => ({ m, i, t: messageEpoch(m.created_at) }))
|
|
117
|
+
.sort((a, b) => (a.t - b.t) || (a.i - b.i))
|
|
118
|
+
.map((x) => x.m);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function registerQuoxchatCommands(program, ctx) {
|
|
122
|
+
const { client, auth } = ctx;
|
|
123
|
+
|
|
124
|
+
const quoxchat = program
|
|
125
|
+
.command('quoxchat')
|
|
126
|
+
.description('Manage QuoxChat website widgets');
|
|
127
|
+
|
|
128
|
+
quoxchat
|
|
129
|
+
.command('widgets')
|
|
130
|
+
.description('List QuoxChat widgets for the active org')
|
|
131
|
+
.action(async () => {
|
|
132
|
+
auth.requireAuth();
|
|
133
|
+
const orgId = resolveOrgId(auth, program);
|
|
134
|
+
const data = await client.get(resolveEndpoint('quoxchat.widgets.list', { orgId }).path, { service: resolveEndpoint('quoxchat.widgets.list', { orgId }).service });
|
|
135
|
+
formatOutput({
|
|
136
|
+
columns: ['id', 'name', 'assistant_name', 'status', 'operator_online', 'created_at'],
|
|
137
|
+
headers: ['ID', 'Name', 'Assistant', 'Status', 'Operator online', 'Created'],
|
|
138
|
+
rows: data.items || data,
|
|
139
|
+
}, { format: program.opts().output, emptyMessage: 'No widgets found.' });
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
quoxchat
|
|
143
|
+
.command('availability')
|
|
144
|
+
.description('Show or set operator availability for a widget (QuoxChat Pro)')
|
|
145
|
+
.argument('<widgetId>', 'widget ID')
|
|
146
|
+
.option('--online', 'mark the human operator online')
|
|
147
|
+
.option('--offline', 'mark the human operator offline')
|
|
148
|
+
.action(async (widgetId, options) => {
|
|
149
|
+
auth.requireAuth();
|
|
150
|
+
const orgId = resolveOrgId(auth, program);
|
|
151
|
+
|
|
152
|
+
if (options.online && options.offline) {
|
|
153
|
+
throw new Error('Use either --online or --offline, not both.');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
let widget;
|
|
157
|
+
if (options.online || options.offline) {
|
|
158
|
+
const data = await client.patch(resolveEndpoint('quoxchat.widgets.update', { orgId, widgetId }).path, {
|
|
159
|
+
service: resolveEndpoint('quoxchat.widgets.update', { orgId, widgetId }).service,
|
|
160
|
+
body: { operator_online: Boolean(options.online) },
|
|
161
|
+
});
|
|
162
|
+
widget = data.widget || data;
|
|
163
|
+
printSuccess(`Operator marked ${options.online ? 'online' : 'offline'}.`);
|
|
164
|
+
} else {
|
|
165
|
+
const data = await client.get(resolveEndpoint('quoxchat.widgets.get', { orgId, widgetId }).path, { service: resolveEndpoint('quoxchat.widgets.get', { orgId, widgetId }).service });
|
|
166
|
+
widget = data.widget || data;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
formatDetail(availabilityView(widget), {
|
|
170
|
+
format: program.opts().output,
|
|
171
|
+
title: 'QuoxChat Availability',
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
quoxchat
|
|
176
|
+
.command('conversations')
|
|
177
|
+
.description('List conversations for a QuoxChat widget (live-support desk)')
|
|
178
|
+
.argument('<widgetId>', 'widget ID')
|
|
179
|
+
.option('--status <status>', 'filter by conversation status (active, handed_off, closed)')
|
|
180
|
+
.option('--waiting', 'show only conversations waiting on a human reply')
|
|
181
|
+
.option('--include-test', 'also list simulation (sim_*) and test conversations')
|
|
182
|
+
.action(async (widgetId, options) => {
|
|
183
|
+
auth.requireAuth();
|
|
184
|
+
const orgId = resolveOrgId(auth, program);
|
|
185
|
+
|
|
186
|
+
const query = options.includeTest ? {} : { exclude_test: '1' };
|
|
187
|
+
if (options.status) query.status = options.status;
|
|
188
|
+
|
|
189
|
+
const ep = resolveEndpoint('quoxchat.conversations.list', { orgId, widgetId });
|
|
190
|
+
const data = await client.get(ep.path, { service: ep.service, query });
|
|
191
|
+
let rows = data.items || data.conversations || data || [];
|
|
192
|
+
if (options.waiting) rows = rows.filter((row) => deskStatus(row) === 'waiting');
|
|
193
|
+
|
|
194
|
+
formatOutput({
|
|
195
|
+
columns: ['id', 'visitor', 'status', 'last_message_at', 'message_count'],
|
|
196
|
+
headers: ['ID', 'Visitor', 'Status', 'Last message', 'Messages'],
|
|
197
|
+
rows: rows.map((row) => ({
|
|
198
|
+
id: row.id,
|
|
199
|
+
visitor: visitorLabel(row),
|
|
200
|
+
status: deskStatusLabel(row),
|
|
201
|
+
last_message_at: row.last_message_at || row.started_at || '-',
|
|
202
|
+
message_count: row.message_count ?? 0,
|
|
203
|
+
})),
|
|
204
|
+
}, { format: program.opts().output, emptyMessage: 'No conversations found.' });
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
quoxchat
|
|
208
|
+
.command('transcript')
|
|
209
|
+
.description('Print a conversation transcript in chronological order')
|
|
210
|
+
.argument('<widgetId>', 'widget ID')
|
|
211
|
+
.argument('<conversationId>', 'conversation ID')
|
|
212
|
+
.action(async (widgetId, conversationId) => {
|
|
213
|
+
auth.requireAuth();
|
|
214
|
+
const orgId = resolveOrgId(auth, program);
|
|
215
|
+
|
|
216
|
+
// Look up the visitor's display name from the conversation list (the
|
|
217
|
+
// messages endpoint itself carries no visitor identity, only role +
|
|
218
|
+
// sender_name for operator turns), mirroring LiveSupportView.jsx.
|
|
219
|
+
const listEp = resolveEndpoint('quoxchat.conversations.list', { orgId, widgetId });
|
|
220
|
+
const listData = await client.get(listEp.path, { service: listEp.service, query: { limit: 100 } });
|
|
221
|
+
const listRows = listData.items || listData.conversations || listData || [];
|
|
222
|
+
const conv = listRows.find((row) => row.id === conversationId);
|
|
223
|
+
const visitorName = conv ? visitorLabel(conv) : 'Visitor';
|
|
224
|
+
|
|
225
|
+
const msgEp = resolveEndpoint('quoxchat.conversations.messages.list', { orgId, widgetId, conversationId });
|
|
226
|
+
const data = await client.get(msgEp.path, { service: msgEp.service });
|
|
227
|
+
const messages = sortByCreatedAt(data.messages || data || []);
|
|
228
|
+
|
|
229
|
+
formatOutput({
|
|
230
|
+
columns: ['created_at', 'from', 'content'],
|
|
231
|
+
headers: ['Time', 'From', 'Message'],
|
|
232
|
+
rows: messages.map((m) => ({
|
|
233
|
+
// Rows mix SQLite datetime('now') and ISO strings depending on the
|
|
234
|
+
// writer; normalise to one readable local format.
|
|
235
|
+
created_at: formatMessageTime(m.created_at),
|
|
236
|
+
from: m.role === 'user' ? visitorName : m.role === 'operator' ? (m.sender_name || 'Operator') : 'AI assistant',
|
|
237
|
+
content: m.content || '',
|
|
238
|
+
})),
|
|
239
|
+
}, { format: program.opts().output, emptyMessage: 'No messages in this conversation yet.' });
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
quoxchat
|
|
243
|
+
.command('reply')
|
|
244
|
+
.description('Send an operator reply in a handed-off QuoxChat conversation')
|
|
245
|
+
.argument('<widgetId>', 'widget ID')
|
|
246
|
+
.argument('<conversationId>', 'conversation ID')
|
|
247
|
+
.argument('<message...>', 'reply text')
|
|
248
|
+
.action(async (widgetId, conversationId, messageParts) => {
|
|
249
|
+
auth.requireAuth();
|
|
250
|
+
const orgId = resolveOrgId(auth, program);
|
|
251
|
+
const body = { body: messageParts.join(' ') };
|
|
252
|
+
|
|
253
|
+
const ep = resolveEndpoint('quoxchat.conversations.reply.create', { orgId, widgetId, conversationId });
|
|
254
|
+
try {
|
|
255
|
+
const data = await client.post(ep.path, { service: ep.service, body });
|
|
256
|
+
printSuccess(`Reply sent as ${data.sender_name || 'operator'}.`);
|
|
257
|
+
} catch (err) {
|
|
258
|
+
if (err.status === 409) {
|
|
259
|
+
throw new Error(
|
|
260
|
+
'This conversation is not handed off to a human operator yet. The visitor needs to request a human, or the AI needs to hand off, before a reply can be sent.'
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
throw err;
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
}
|