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,531 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* InboxQ commands: connected mail accounts, triaged messages, zones,
|
|
3
|
+
* sync, reports, watches and proposals. Governed proxy to the InboxQ
|
|
4
|
+
* plugin (collector: /api/v1/inboxq/*, org resolved server-side from auth).
|
|
5
|
+
*
|
|
6
|
+
* `quox inbox` is the separate HITL approvals command group; this is
|
|
7
|
+
* `quox mail`, InboxQ's own command group.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { readFileSync, statSync } from 'fs';
|
|
11
|
+
import { formatOutput, formatDetail, printSuccess } from '../core/output.js';
|
|
12
|
+
import { parseJSON, resolveOrgId } from './helpers.js';
|
|
13
|
+
import { resolveEndpoint } from '../api/endpoints.js';
|
|
14
|
+
|
|
15
|
+
// --- minimal CSV parsing (quoted fields + escaped quotes, no deps) ----------
|
|
16
|
+
function splitCsvLine(line) {
|
|
17
|
+
const out = [];
|
|
18
|
+
let cur = '';
|
|
19
|
+
let inQuotes = false;
|
|
20
|
+
for (let i = 0; i < line.length; i++) {
|
|
21
|
+
const c = line[i];
|
|
22
|
+
if (inQuotes) {
|
|
23
|
+
if (c === '"') {
|
|
24
|
+
if (line[i + 1] === '"') { cur += '"'; i++; } else { inQuotes = false; }
|
|
25
|
+
} else { cur += c; }
|
|
26
|
+
} else if (c === '"') { inQuotes = true; }
|
|
27
|
+
else if (c === ',') { out.push(cur); cur = ''; }
|
|
28
|
+
else { cur += c; }
|
|
29
|
+
}
|
|
30
|
+
out.push(cur);
|
|
31
|
+
return out;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function parseCsvFile(path) {
|
|
35
|
+
const text = readFileSync(path, 'utf8');
|
|
36
|
+
const lines = text.split(/\r?\n/).filter((l) => l.trim().length);
|
|
37
|
+
if (!lines.length) return { header: [], rows: [] };
|
|
38
|
+
const header = splitCsvLine(lines[0]).map((h) => h.trim().toLowerCase());
|
|
39
|
+
const rows = lines.slice(1).map((line) => {
|
|
40
|
+
const cells = splitCsvLine(line);
|
|
41
|
+
const row = {};
|
|
42
|
+
header.forEach((h, i) => { row[h] = (cells[i] ?? '').trim(); });
|
|
43
|
+
return row;
|
|
44
|
+
});
|
|
45
|
+
return { header, rows };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const VALID_ZONES = ['focus', 'fyi', 'auto', 'screened'];
|
|
49
|
+
const VALID_KINDS = ['file', 'block', 'draft'];
|
|
50
|
+
const VALID_DECISIONS = ['approve', 'deny'];
|
|
51
|
+
|
|
52
|
+
// Small CLI-local copy of src/config/emailProviders.js's EMAIL_PROVIDERS.
|
|
53
|
+
// The cli/ package can't import from src/, so the host/port defaults for
|
|
54
|
+
// each named provider are duplicated here; keep the two in sync by hand
|
|
55
|
+
// when a provider's IMAP/SMTP settings change.
|
|
56
|
+
const PROVIDER_PRESETS = {
|
|
57
|
+
gmail: { imapHost: 'imap.gmail.com', imapPort: 993, smtpHost: 'smtp.gmail.com', smtpPort: 465, smtpSecure: true },
|
|
58
|
+
icloud: { imapHost: 'imap.mail.me.com', imapPort: 993, smtpHost: 'smtp.mail.me.com', smtpPort: 587, smtpSecure: true },
|
|
59
|
+
outlook: { imapHost: 'outlook.office365.com', imapPort: 993, smtpHost: 'smtp.office365.com', smtpPort: 587, smtpSecure: true },
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
function isJsonOutput(program) {
|
|
63
|
+
return program.opts().output === 'json';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function printJson(data) {
|
|
67
|
+
console.log(JSON.stringify(data, null, 2));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function registerMailCommands(program, ctx) {
|
|
71
|
+
const { client, auth } = ctx;
|
|
72
|
+
|
|
73
|
+
const mail = program
|
|
74
|
+
.command('mail')
|
|
75
|
+
.description('InboxQ: connected mail accounts, triaged messages, zones, sync, reports, watches, proposals');
|
|
76
|
+
|
|
77
|
+
// ── accounts ─────────────────────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
mail
|
|
80
|
+
.command('accounts')
|
|
81
|
+
.description('List connected mail accounts')
|
|
82
|
+
.action(async () => {
|
|
83
|
+
auth.requireAuth();
|
|
84
|
+
const ep = resolveEndpoint('inboxq.accounts.list');
|
|
85
|
+
const data = await client.get(ep.path, { service: ep.service });
|
|
86
|
+
if (isJsonOutput(program)) return printJson(data);
|
|
87
|
+
formatOutput({
|
|
88
|
+
columns: ['id', 'emailAddress', 'displayName', 'pollEnabled', 'createdAt'],
|
|
89
|
+
headers: ['ID', 'Email', 'Name', 'Polling', 'Connected'],
|
|
90
|
+
rows: data.accounts || [],
|
|
91
|
+
}, { format: program.opts().output });
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
mail
|
|
95
|
+
.command('connect')
|
|
96
|
+
.description('Connect a mailbox. Reuse a stored credential (--credential), or create one inline from IMAP settings (--imap-host with --password-env, or --provider gmail|icloud|outlook to pre-fill host/port). Creating a credential needs a human/admin profile, e.g. quox -p moo mail connect ...')
|
|
97
|
+
.option('--credential <id>', 'reuse a stored email_account credential ID')
|
|
98
|
+
.requiredOption('--email <address>', 'mailbox address (also the IMAP username unless --username is given)')
|
|
99
|
+
.option('--username <user>', 'IMAP username, if different from --email')
|
|
100
|
+
.option('--provider <name>', 'named email provider (gmail, icloud, outlook): pre-fills IMAP/SMTP host and port')
|
|
101
|
+
.option('--imap-host <host>', 'IMAP host (creating a new credential; not required with --provider)')
|
|
102
|
+
.option('--imap-port <n>', 'IMAP port', '993')
|
|
103
|
+
.option('--smtp-host <host>', 'SMTP host (defaults to --imap-host)')
|
|
104
|
+
.option('--smtp-port <n>', 'SMTP port', '465')
|
|
105
|
+
.option('--password-env <VAR>', 'name of an env var holding the IMAP password (the password is read from it, never passed on the command line)')
|
|
106
|
+
.option('--label <label>', 'vault label for the new credential')
|
|
107
|
+
.option('--name <name>', 'display name for the account')
|
|
108
|
+
.option('--folders <csv>', 'comma-separated folder list to watch')
|
|
109
|
+
.action(async (options) => {
|
|
110
|
+
auth.requireAuth();
|
|
111
|
+
|
|
112
|
+
// Wrong-tenant guard. The InboxQ account is created in the caller's ACTIVE
|
|
113
|
+
// org (the collector resolves org server-side from the token, ignoring
|
|
114
|
+
// flags), while a --org would only retarget the vault credential path, so
|
|
115
|
+
// a divergent --org splits the credential and account across tenants.
|
|
116
|
+
// Refuse it and point at the one mechanism that moves both: org switch.
|
|
117
|
+
const requestedOrg = program.opts && program.opts().org;
|
|
118
|
+
const sessionOrg = typeof auth.getOrgId === 'function' ? auth.getOrgId() : null;
|
|
119
|
+
if (requestedOrg && sessionOrg && requestedOrg !== sessionOrg) {
|
|
120
|
+
throw new Error(
|
|
121
|
+
`--org ${requestedOrg} differs from your active org ${sessionOrg}. `
|
|
122
|
+
+ `The mailbox account is created in your ACTIVE org (server-resolved from your token), `
|
|
123
|
+
+ `so --org would put the credential and the account in different tenants. `
|
|
124
|
+
+ `Run: quox org switch ${requestedOrg} then re-run connect.`,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
let credentialId = options.credential;
|
|
129
|
+
|
|
130
|
+
// Inline credential creation: turns the ugly `vault create --credentials-stdin`
|
|
131
|
+
// JSON one-liner into `quox mail connect --imap-host ... --password-env ...`.
|
|
132
|
+
// Requires a profile with SECRETS_WRITE (a human/admin, e.g. -p moo); the
|
|
133
|
+
// resident agent is denied by design.
|
|
134
|
+
if (!credentialId) {
|
|
135
|
+
let preset = null;
|
|
136
|
+
if (options.provider) {
|
|
137
|
+
preset = PROVIDER_PRESETS[options.provider];
|
|
138
|
+
if (!preset) {
|
|
139
|
+
throw new Error(`Unknown --provider "${options.provider}". Supported: ${Object.keys(PROVIDER_PRESETS).join(', ')}`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (!options.imapHost && !preset) {
|
|
143
|
+
throw new Error('Provide --credential <id> to reuse a stored credential, or --imap-host (with --password-env) or --provider gmail|icloud|outlook to create one.');
|
|
144
|
+
}
|
|
145
|
+
if (!options.passwordEnv) {
|
|
146
|
+
throw new Error('--password-env <VAR> is required when creating a credential (the password is read from that env var, never from the command line).');
|
|
147
|
+
}
|
|
148
|
+
const password = process.env[options.passwordEnv];
|
|
149
|
+
if (!password) {
|
|
150
|
+
throw new Error(`Env var ${options.passwordEnv} is empty or unset.`);
|
|
151
|
+
}
|
|
152
|
+
const imapHost = options.imapHost || preset?.imapHost;
|
|
153
|
+
const imapPort = options.imapHost ? (Number(options.imapPort) || 993) : (preset?.imapPort ?? 993);
|
|
154
|
+
const smtpHost = options.smtpHost || (options.imapHost ? options.imapHost : preset?.smtpHost);
|
|
155
|
+
const smtpPort = options.smtpHost ? (Number(options.smtpPort) || 465) : (preset?.smtpPort ?? 465);
|
|
156
|
+
const smtpSecure = preset ? preset.smtpSecure !== false : true;
|
|
157
|
+
// When a named provider is given, label the credential's integration_type
|
|
158
|
+
// by provider (e.g. 'gmail') so it shows up as that provider in the vault
|
|
159
|
+
// UI; credential_kind stays 'email_account' either way.
|
|
160
|
+
const orgId = resolveOrgId(auth, program);
|
|
161
|
+
const vep = resolveEndpoint('vault.create', { orgId });
|
|
162
|
+
const created = await client.post(vep.path, {
|
|
163
|
+
service: vep.service,
|
|
164
|
+
body: {
|
|
165
|
+
integration_type: options.provider || 'smtp',
|
|
166
|
+
credential_kind: 'email_account',
|
|
167
|
+
label: options.label || `Mailbox ${options.email}`,
|
|
168
|
+
credentials: {
|
|
169
|
+
imap_host: imapHost,
|
|
170
|
+
imap_port: imapPort,
|
|
171
|
+
username: options.username || options.email,
|
|
172
|
+
password,
|
|
173
|
+
smtp_host: smtpHost,
|
|
174
|
+
smtp_port: smtpPort,
|
|
175
|
+
smtp_secure: smtpSecure,
|
|
176
|
+
from: options.email,
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
credentialId = created.id || created.credential?.id || created.credentialId;
|
|
181
|
+
if (!credentialId) throw new Error('Credential created but no id was returned; cannot connect the account.');
|
|
182
|
+
if (!isJsonOutput(program)) printSuccess(`Vault credential created (${credentialId}).`);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const body = { credentialId, emailAddress: options.email };
|
|
186
|
+
if (options.name) body.displayName = options.name;
|
|
187
|
+
if (options.folders) body.folders = options.folders.split(',').map((f) => f.trim()).filter(Boolean);
|
|
188
|
+
const ep = resolveEndpoint('inboxq.accounts.create');
|
|
189
|
+
const data = await client.post(ep.path, { service: ep.service, body });
|
|
190
|
+
if (isJsonOutput(program)) return printJson(data);
|
|
191
|
+
const account = data.account || data;
|
|
192
|
+
printSuccess(`Connected ${account.emailAddress || options.email} (${account.id}) in org ${sessionOrg || '(active)'}`);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
mail
|
|
196
|
+
.command('import')
|
|
197
|
+
.description('Batch-import mailboxes from a CSV. Header columns (case-insensitive): email (required), imap_host (required), imap_port, smtp_host, smtp_port, username, password_env OR password, label, name, folders (semicolon-separated). One credential + account is created per row. Needs a SECRETS_WRITE profile, e.g. quox -p moo mail import boxes.csv. A file with a password column is a secret: chmod 600 it.')
|
|
198
|
+
.argument('<csvFile>', 'path to the CSV file')
|
|
199
|
+
.option('--dry-run', 'parse and validate every row without creating anything')
|
|
200
|
+
.option('--continue-on-error', 'keep going after a failed row (default: stop on first failure)')
|
|
201
|
+
.action(async (csvFile, options) => {
|
|
202
|
+
auth.requireAuth();
|
|
203
|
+
const { header, rows } = parseCsvFile(csvFile);
|
|
204
|
+
if (!rows.length) throw new Error('CSV has a header but no data rows.');
|
|
205
|
+
if (!header.includes('email') || !header.includes('imap_host')) {
|
|
206
|
+
throw new Error('CSV must have at least "email" and "imap_host" columns.');
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Secret hygiene: an inline password column makes the file a secret. Warn
|
|
210
|
+
// loudly if it is group/other-readable. Never print the password itself.
|
|
211
|
+
if (header.includes('password')) {
|
|
212
|
+
try {
|
|
213
|
+
const mode = statSync(csvFile).mode & 0o077;
|
|
214
|
+
if (mode !== 0) console.error(`⚠ ${csvFile} has a plaintext password column and is group/other-readable. chmod 600 it, or use a password_env column instead.`);
|
|
215
|
+
} catch { /* stat failure is non-fatal */ }
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const orgId = resolveOrgId(auth, program);
|
|
219
|
+
const results = [];
|
|
220
|
+
for (let i = 0; i < rows.length; i++) {
|
|
221
|
+
const row = rows[i];
|
|
222
|
+
const where = `row ${i + 2} <${row.email || '?'}>`;
|
|
223
|
+
try {
|
|
224
|
+
if (!row.email) throw new Error('missing email');
|
|
225
|
+
if (!row.imap_host) throw new Error('missing imap_host');
|
|
226
|
+
const password = row.password_env ? process.env[row.password_env] : row.password;
|
|
227
|
+
if (!password) throw new Error(row.password_env ? `env ${row.password_env} is empty/unset` : 'missing password or password_env');
|
|
228
|
+
if (options.dryRun) { results.push({ row: where, status: 'ok (dry-run)' }); continue; }
|
|
229
|
+
|
|
230
|
+
const vep = resolveEndpoint('vault.create', { orgId });
|
|
231
|
+
const created = await client.post(vep.path, {
|
|
232
|
+
service: vep.service,
|
|
233
|
+
body: {
|
|
234
|
+
integration_type: 'smtp',
|
|
235
|
+
credential_kind: 'email_account',
|
|
236
|
+
label: row.label || `Mailbox ${row.email}`,
|
|
237
|
+
credentials: {
|
|
238
|
+
imap_host: row.imap_host,
|
|
239
|
+
imap_port: Number(row.imap_port) || 993,
|
|
240
|
+
username: row.username || row.email,
|
|
241
|
+
password,
|
|
242
|
+
smtp_host: row.smtp_host || row.imap_host,
|
|
243
|
+
smtp_port: Number(row.smtp_port) || 465,
|
|
244
|
+
smtp_secure: true,
|
|
245
|
+
from: row.email,
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
});
|
|
249
|
+
const credentialId = created.id || created.credential?.id || created.credentialId;
|
|
250
|
+
if (!credentialId) throw new Error('credential created but no id returned');
|
|
251
|
+
|
|
252
|
+
const body = { credentialId, emailAddress: row.email };
|
|
253
|
+
if (row.name) body.displayName = row.name;
|
|
254
|
+
// folders use ';' inside a cell so they do not collide with the CSV comma.
|
|
255
|
+
if (row.folders) body.folders = row.folders.split(';').map((f) => f.trim()).filter(Boolean);
|
|
256
|
+
const ep = resolveEndpoint('inboxq.accounts.create');
|
|
257
|
+
await client.post(ep.path, { service: ep.service, body });
|
|
258
|
+
results.push({ row: where, status: 'connected', credential: credentialId });
|
|
259
|
+
} catch (e) {
|
|
260
|
+
results.push({ row: where, status: `FAILED: ${e.message}` });
|
|
261
|
+
if (!options.continueOnError && !options.dryRun) {
|
|
262
|
+
if (isJsonOutput(program)) return printJson({ results, stoppedAt: where });
|
|
263
|
+
formatOutput({ columns: ['row', 'status'], headers: ['Row', 'Status'], rows: results }, { format: program.opts().output });
|
|
264
|
+
throw new Error(`Stopped at ${where}: ${e.message}. Re-run with --continue-on-error to skip failing rows.`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (isJsonOutput(program)) return printJson({ results });
|
|
270
|
+
formatOutput({ columns: ['row', 'status'], headers: ['Row', 'Status'], rows: results }, { format: program.opts().output });
|
|
271
|
+
const ok = results.filter((r) => r.status === 'connected' || r.status === 'ok (dry-run)').length;
|
|
272
|
+
printSuccess(`${ok}/${results.length} rows ${options.dryRun ? 'valid' : 'imported'}.`);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
mail
|
|
276
|
+
.command('disconnect')
|
|
277
|
+
.description('Disconnect a mail account')
|
|
278
|
+
.argument('<accountId>', 'account ID')
|
|
279
|
+
.action(async (accountId) => {
|
|
280
|
+
auth.requireAuth();
|
|
281
|
+
const ep = resolveEndpoint('inboxq.accounts.delete', { accountId });
|
|
282
|
+
const data = await client.delete(ep.path, { service: ep.service });
|
|
283
|
+
if (isJsonOutput(program)) return printJson(data);
|
|
284
|
+
printSuccess(`Disconnected account ${accountId}.`);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
// ── sync ─────────────────────────────────────────────────────────────
|
|
288
|
+
|
|
289
|
+
mail
|
|
290
|
+
.command('sync')
|
|
291
|
+
.description('Sync a mail account now (fetch new messages, run triage)')
|
|
292
|
+
.argument('<accountId>', 'account ID')
|
|
293
|
+
.action(async (accountId) => {
|
|
294
|
+
auth.requireAuth();
|
|
295
|
+
const ep = resolveEndpoint('inboxq.sync');
|
|
296
|
+
// A first full-mailbox pull runs the whole IMAP fetch + classify in one
|
|
297
|
+
// synchronous request and routinely exceeds the default 30s client timeout
|
|
298
|
+
// (the sync still completes server-side, but the CLI showed a false
|
|
299
|
+
// "timed out"). Give sync a generous 4-minute ceiling (nginx allows 300s).
|
|
300
|
+
const data = await client.post(ep.path, { service: ep.service, body: { accountId }, timeout: 240000 });
|
|
301
|
+
if (isJsonOutput(program)) return printJson(data);
|
|
302
|
+
const summary = data.summary || {};
|
|
303
|
+
const folders = summary.folders || [];
|
|
304
|
+
if (folders.length === 0) {
|
|
305
|
+
printSuccess(`Synced ${accountId}. No folders reported.`);
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
formatOutput({
|
|
309
|
+
columns: ['folder', 'new', 'failed'],
|
|
310
|
+
headers: ['Folder', 'New', 'Failed'],
|
|
311
|
+
rows: folders,
|
|
312
|
+
}, { format: program.opts().output });
|
|
313
|
+
printSuccess(`Synced ${accountId}. LLM calls: ${summary.llmCalls ?? 0}`);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
// ── messages ─────────────────────────────────────────────────────────
|
|
317
|
+
|
|
318
|
+
mail
|
|
319
|
+
.command('list')
|
|
320
|
+
.description('List triaged messages')
|
|
321
|
+
.option('--account <id>', 'filter by account ID')
|
|
322
|
+
.option('--zone <zone>', 'filter by zone: focus, fyi, auto, screened')
|
|
323
|
+
.option('--limit <n>', 'max results', '50')
|
|
324
|
+
.option('--offset <n>', 'result offset', '0')
|
|
325
|
+
.action(async (options) => {
|
|
326
|
+
auth.requireAuth();
|
|
327
|
+
if (options.zone && !VALID_ZONES.includes(options.zone)) {
|
|
328
|
+
throw new Error(`invalid --zone "${options.zone}" (expected one of ${VALID_ZONES.join(', ')})`);
|
|
329
|
+
}
|
|
330
|
+
const query = { limit: options.limit, offset: options.offset };
|
|
331
|
+
if (options.account) query.accountId = options.account;
|
|
332
|
+
if (options.zone) query.zone = options.zone;
|
|
333
|
+
const ep = resolveEndpoint('inboxq.messages.list');
|
|
334
|
+
const data = await client.get(ep.path, { service: ep.service, query });
|
|
335
|
+
if (isJsonOutput(program)) return printJson(data);
|
|
336
|
+
formatOutput({
|
|
337
|
+
columns: ['from', 'subject', 'zone', 'flags'],
|
|
338
|
+
headers: ['From', 'Subject', 'Zone', 'Flags'],
|
|
339
|
+
rows: data.messages || [],
|
|
340
|
+
}, { format: program.opts().output });
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
// ── zones ────────────────────────────────────────────────────────────
|
|
344
|
+
|
|
345
|
+
mail
|
|
346
|
+
.command('zones')
|
|
347
|
+
.description('Show zone counts (focus, fyi, auto, screened)')
|
|
348
|
+
.option('--account <id>', 'filter by account ID')
|
|
349
|
+
.action(async (options) => {
|
|
350
|
+
auth.requireAuth();
|
|
351
|
+
const query = {};
|
|
352
|
+
if (options.account) query.accountId = options.account;
|
|
353
|
+
const ep = resolveEndpoint('inboxq.zones.get');
|
|
354
|
+
const data = await client.get(ep.path, { service: ep.service, query });
|
|
355
|
+
if (isJsonOutput(program)) return printJson(data);
|
|
356
|
+
formatDetail(data.zones || {}, { format: program.opts().output, title: 'InboxQ Zones' });
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
// ── report ───────────────────────────────────────────────────────────
|
|
360
|
+
|
|
361
|
+
mail
|
|
362
|
+
.command('report')
|
|
363
|
+
.description('Show the latest triage report, or generate a fresh one')
|
|
364
|
+
.argument('<accountId>', 'account ID')
|
|
365
|
+
.option('--generate', 'generate a new report instead of reading the latest')
|
|
366
|
+
.action(async (accountId, options) => {
|
|
367
|
+
auth.requireAuth();
|
|
368
|
+
let data;
|
|
369
|
+
if (options.generate) {
|
|
370
|
+
const ep = resolveEndpoint('inboxq.report.generate');
|
|
371
|
+
data = await client.post(ep.path, { service: ep.service, body: { accountId } });
|
|
372
|
+
} else {
|
|
373
|
+
const ep = resolveEndpoint('inboxq.report.latest');
|
|
374
|
+
data = await client.get(ep.path, { service: ep.service, query: { accountId } });
|
|
375
|
+
}
|
|
376
|
+
if (isJsonOutput(program)) return printJson(data);
|
|
377
|
+
const report = data.report;
|
|
378
|
+
if (!report) {
|
|
379
|
+
console.log('No report available yet. Use --generate to create one.');
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
console.log(report.bodyMd || '(empty report)');
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
// ── watches ──────────────────────────────────────────────────────────
|
|
386
|
+
|
|
387
|
+
const watch = mail
|
|
388
|
+
.command('watch')
|
|
389
|
+
.description('Manage standing watches (label + match rules)');
|
|
390
|
+
|
|
391
|
+
watch
|
|
392
|
+
.command('list')
|
|
393
|
+
.description('List watches')
|
|
394
|
+
.option('--active-only', 'only show active watches')
|
|
395
|
+
.action(async (options) => {
|
|
396
|
+
auth.requireAuth();
|
|
397
|
+
const query = {};
|
|
398
|
+
if (options.activeOnly) query.activeOnly = 1;
|
|
399
|
+
const ep = resolveEndpoint('inboxq.watches.list');
|
|
400
|
+
const data = await client.get(ep.path, { service: ep.service, query });
|
|
401
|
+
if (isJsonOutput(program)) return printJson(data);
|
|
402
|
+
formatOutput({
|
|
403
|
+
columns: ['id', 'label', 'accountId', 'active'],
|
|
404
|
+
headers: ['ID', 'Label', 'Account', 'Active'],
|
|
405
|
+
rows: data.watches || [],
|
|
406
|
+
}, { format: program.opts().output });
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
watch
|
|
410
|
+
.command('add')
|
|
411
|
+
.description('Add a watch')
|
|
412
|
+
.requiredOption('--label <label>', 'watch label')
|
|
413
|
+
.requiredOption('--match <json>', 'match rule as a JSON object')
|
|
414
|
+
.option('--account <id>', 'restrict the watch to one account')
|
|
415
|
+
.action(async (options) => {
|
|
416
|
+
auth.requireAuth();
|
|
417
|
+
const match = parseJSON(options.match, '--match');
|
|
418
|
+
const body = { label: options.label, match };
|
|
419
|
+
if (options.account) body.accountId = options.account;
|
|
420
|
+
const ep = resolveEndpoint('inboxq.watches.create');
|
|
421
|
+
const data = await client.post(ep.path, { service: ep.service, body });
|
|
422
|
+
if (isJsonOutput(program)) return printJson(data);
|
|
423
|
+
const created = data.watch || data;
|
|
424
|
+
printSuccess(`Added watch "${options.label}" (${created.id})`);
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
watch
|
|
428
|
+
.command('active')
|
|
429
|
+
.description('Enable or disable a watch')
|
|
430
|
+
.argument('<watchId>', 'watch ID')
|
|
431
|
+
.requiredOption('--set <bool>', 'true or false')
|
|
432
|
+
.action(async (watchId, options) => {
|
|
433
|
+
auth.requireAuth();
|
|
434
|
+
const active = String(options.set).toLowerCase() === 'true';
|
|
435
|
+
const ep = resolveEndpoint('inboxq.watches.active', { watchId });
|
|
436
|
+
const data = await client.post(ep.path, { service: ep.service, body: { active } });
|
|
437
|
+
if (isJsonOutput(program)) return printJson(data);
|
|
438
|
+
printSuccess(`Watch ${watchId} is now ${active ? 'active' : 'inactive'}.`);
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
// ── proposals ────────────────────────────────────────────────────────
|
|
442
|
+
|
|
443
|
+
mail
|
|
444
|
+
.command('propose')
|
|
445
|
+
.description('Create a proposal (file, block or draft) for an account')
|
|
446
|
+
.requiredOption('--account <id>', 'account ID')
|
|
447
|
+
.requiredOption('--kind <kind>', 'proposal kind: file, block, draft')
|
|
448
|
+
.requiredOption('--payload <json>', 'proposal payload as a JSON object')
|
|
449
|
+
.action(async (options) => {
|
|
450
|
+
auth.requireAuth();
|
|
451
|
+
if (!VALID_KINDS.includes(options.kind)) {
|
|
452
|
+
throw new Error(`invalid --kind "${options.kind}" (expected one of ${VALID_KINDS.join(', ')})`);
|
|
453
|
+
}
|
|
454
|
+
const payload = parseJSON(options.payload, '--payload');
|
|
455
|
+
const body = { accountId: options.account, kind: options.kind, payload };
|
|
456
|
+
const ep = resolveEndpoint('inboxq.proposals.create');
|
|
457
|
+
const data = await client.post(ep.path, { service: ep.service, body });
|
|
458
|
+
if (isJsonOutput(program)) return printJson(data);
|
|
459
|
+
const proposal = data.proposal || data;
|
|
460
|
+
printSuccess(`Created ${options.kind} proposal ${proposal.id} for account ${options.account}`);
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
mail
|
|
464
|
+
.command('proposals')
|
|
465
|
+
.description('List proposals')
|
|
466
|
+
.option('--status <status>', 'filter by status')
|
|
467
|
+
.option('--account <id>', 'filter by account ID')
|
|
468
|
+
.action(async (options) => {
|
|
469
|
+
auth.requireAuth();
|
|
470
|
+
const query = {};
|
|
471
|
+
if (options.status) query.status = options.status;
|
|
472
|
+
if (options.account) query.accountId = options.account;
|
|
473
|
+
const ep = resolveEndpoint('inboxq.proposals.list');
|
|
474
|
+
const data = await client.get(ep.path, { service: ep.service, query });
|
|
475
|
+
if (isJsonOutput(program)) return printJson(data);
|
|
476
|
+
formatOutput({
|
|
477
|
+
columns: ['id', 'kind', 'accountId', 'status', 'createdAt'],
|
|
478
|
+
headers: ['ID', 'Kind', 'Account', 'Status', 'Created'],
|
|
479
|
+
rows: data.proposals || [],
|
|
480
|
+
}, { format: program.opts().output });
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
mail
|
|
484
|
+
.command('resolve')
|
|
485
|
+
.description('Approve or deny a proposal')
|
|
486
|
+
.argument('<proposalId>', 'proposal ID')
|
|
487
|
+
.requiredOption('--decision <decision>', 'approve or deny')
|
|
488
|
+
.action(async (proposalId, options) => {
|
|
489
|
+
auth.requireAuth();
|
|
490
|
+
if (!VALID_DECISIONS.includes(options.decision)) {
|
|
491
|
+
throw new Error(`invalid --decision "${options.decision}" (expected approve or deny)`);
|
|
492
|
+
}
|
|
493
|
+
const ep = resolveEndpoint('inboxq.proposals.resolve', { proposalId });
|
|
494
|
+
const data = await client.post(ep.path, { service: ep.service, body: { decision: options.decision } });
|
|
495
|
+
if (isJsonOutput(program)) return printJson(data);
|
|
496
|
+
printSuccess(`Proposal ${proposalId} ${options.decision}d.`);
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
// ── moves (AQ-9E U2: reversible moves) ─────────────────────────────────
|
|
500
|
+
|
|
501
|
+
mail
|
|
502
|
+
.command('moves')
|
|
503
|
+
.description('List recent undoable moves (most recent first)')
|
|
504
|
+
.option('--account <id>', 'filter by account ID')
|
|
505
|
+
.option('--limit <n>', 'max results', '20')
|
|
506
|
+
.action(async (options) => {
|
|
507
|
+
auth.requireAuth();
|
|
508
|
+
const query = { limit: options.limit };
|
|
509
|
+
if (options.account) query.accountId = options.account;
|
|
510
|
+
const ep = resolveEndpoint('inboxq.moves.recent');
|
|
511
|
+
const data = await client.get(ep.path, { service: ep.service, query });
|
|
512
|
+
if (isJsonOutput(program)) return printJson(data);
|
|
513
|
+
formatOutput({
|
|
514
|
+
columns: ['id', 'fromFolder', 'toFolder', 'kind'],
|
|
515
|
+
headers: ['Move ID', 'From', 'To', 'Kind'],
|
|
516
|
+
rows: data.moves || [],
|
|
517
|
+
}, { format: program.opts().output });
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
mail
|
|
521
|
+
.command('undo')
|
|
522
|
+
.description('Undo a recorded move: reverse the IMAP move back to its source folder')
|
|
523
|
+
.argument('<moveId>', 'move ID (from `quox mail moves`)')
|
|
524
|
+
.action(async (moveId) => {
|
|
525
|
+
auth.requireAuth();
|
|
526
|
+
const ep = resolveEndpoint('inboxq.moves.undo');
|
|
527
|
+
const data = await client.post(ep.path, { service: ep.service, body: { moveId } });
|
|
528
|
+
if (isJsonOutput(program)) return printJson(data);
|
|
529
|
+
printSuccess(`Move ${moveId} undone; message restored to ${data.restoredTo || 'its source folder'}.`);
|
|
530
|
+
});
|
|
531
|
+
}
|