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,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Approvals commands: result (quox#791)
|
|
3
|
+
*
|
|
4
|
+
* After a human approves a held tool call (e.g. a gated ssh_exec) the collector
|
|
5
|
+
* runs it and stores the outcome. `quox approvals result <id>` reads it back.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { resolveEndpoint } from '../api/endpoints.js';
|
|
9
|
+
|
|
10
|
+
export function registerApprovalsCommands(program, ctx) {
|
|
11
|
+
const { client, auth } = ctx;
|
|
12
|
+
|
|
13
|
+
const approvals = program
|
|
14
|
+
.command('approvals')
|
|
15
|
+
.description('Tool approvals (held actions awaiting or past a human decision)');
|
|
16
|
+
|
|
17
|
+
approvals
|
|
18
|
+
.command('result <approvalId>')
|
|
19
|
+
.description('Show the status and stored output of an approved tool call')
|
|
20
|
+
.option('--org <id>', 'org ID (service-key automation callers; defaults to active org / QUOX_ORG_ID)')
|
|
21
|
+
.action(async (approvalId, options) => {
|
|
22
|
+
auth.requireAuth();
|
|
23
|
+
const ep = resolveEndpoint('v1.approvals.result', { id: approvalId });
|
|
24
|
+
const orgId = auth.getOrgId(options.org || process.env.QUOX_ORG_ID);
|
|
25
|
+
const query = orgId ? { org_id: orgId } : undefined;
|
|
26
|
+
const data = await client.get(ep.path, { service: ep.service, ...(query ? { query } : {}) });
|
|
27
|
+
|
|
28
|
+
if (program.opts().output === 'json') {
|
|
29
|
+
console.log(JSON.stringify(data, null, 2));
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
console.log(`Approval ${data.approval_id} tool: ${data.tool} status: ${data.status}`
|
|
33
|
+
+ (data.executed_at ? ` executed: ${data.executed_at}` : ''));
|
|
34
|
+
if (data.result === null || data.result === undefined) {
|
|
35
|
+
if (data.status === 'pending') console.log('Still pending: a human has not decided yet.');
|
|
36
|
+
else if (data.status === 'resuming') console.log('Approved, executing. Re-run shortly.');
|
|
37
|
+
else console.log('No stored output.');
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const r = data.result;
|
|
41
|
+
if (typeof r === 'string') { console.log(r); return; }
|
|
42
|
+
if (r.success === false) console.log(`Failed: ${r.error || 'unknown error'}`);
|
|
43
|
+
const out = r.output ?? r.stdout ?? r.result;
|
|
44
|
+
if (out !== undefined) console.log(typeof out === 'string' ? out : JSON.stringify(out, null, 2));
|
|
45
|
+
else console.log(JSON.stringify(r, null, 2));
|
|
46
|
+
});
|
|
47
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Assistant commands: CRUD, deploy, fork
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { formatOutput, formatDetail, printSuccess } from '../core/output.js';
|
|
6
|
+
import { resolveOrgId } from './helpers.js';
|
|
7
|
+
import { resolveEndpoint } from '../api/endpoints.js';
|
|
8
|
+
|
|
9
|
+
export function registerAssistantCommands(program, ctx) {
|
|
10
|
+
const { client, auth } = ctx;
|
|
11
|
+
|
|
12
|
+
const assistant = program
|
|
13
|
+
.command('assistant')
|
|
14
|
+
.description('Manage assistants');
|
|
15
|
+
|
|
16
|
+
assistant
|
|
17
|
+
.command('list')
|
|
18
|
+
.description('List assistants')
|
|
19
|
+
.action(async () => {
|
|
20
|
+
auth.requireAuth();
|
|
21
|
+
const orgId = resolveOrgId(auth, program);
|
|
22
|
+
const data = await client.get(resolveEndpoint('assistants.list', { orgId }).path, { service: resolveEndpoint('assistants.list', { orgId }).service });
|
|
23
|
+
formatOutput({
|
|
24
|
+
// fleet #392: route returns active_deployments, not deployed.
|
|
25
|
+
columns: ['id', 'name', 'status', 'active_deployments', 'created_at'],
|
|
26
|
+
headers: ['ID', 'Name', 'Status', 'Deployed', 'Created'],
|
|
27
|
+
rows: data.items || data.assistants || data,
|
|
28
|
+
}, { format: program.opts().output });
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
assistant
|
|
32
|
+
.command('get')
|
|
33
|
+
.description('Get assistant details')
|
|
34
|
+
.argument('<assistantId>', 'assistant ID')
|
|
35
|
+
.action(async (assistantId) => {
|
|
36
|
+
auth.requireAuth();
|
|
37
|
+
const orgId = resolveOrgId(auth, program);
|
|
38
|
+
const data = await client.get(resolveEndpoint('assistants.get', { orgId, assistantId }).path, { service: resolveEndpoint('assistants.get', { orgId, assistantId }).service });
|
|
39
|
+
formatDetail(data.assistant || data, { format: program.opts().output, title: data.assistant?.name || data.name });
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
assistant
|
|
43
|
+
.command('create')
|
|
44
|
+
.description('Create an assistant')
|
|
45
|
+
.requiredOption('--name <name>', 'assistant name')
|
|
46
|
+
.option('--persona <text>', 'persona/system prompt')
|
|
47
|
+
.option('--description <desc>', 'description')
|
|
48
|
+
.action(async (options) => {
|
|
49
|
+
auth.requireAuth();
|
|
50
|
+
const orgId = resolveOrgId(auth, program);
|
|
51
|
+
const body = { name: options.name };
|
|
52
|
+
if (options.persona) body.persona = options.persona;
|
|
53
|
+
if (options.description) body.description = options.description;
|
|
54
|
+
await client.post(resolveEndpoint('assistants.create', { orgId }).path, { service: resolveEndpoint('assistants.create', { orgId }).service, body });
|
|
55
|
+
printSuccess(`Assistant "${options.name}" created.`);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
assistant
|
|
59
|
+
.command('update')
|
|
60
|
+
.description('Update an assistant')
|
|
61
|
+
.argument('<assistantId>', 'assistant ID')
|
|
62
|
+
.option('--name <name>', 'new name')
|
|
63
|
+
.option('--persona <text>', 'new persona')
|
|
64
|
+
.option('--description <desc>', 'new description')
|
|
65
|
+
.action(async (assistantId, options) => {
|
|
66
|
+
auth.requireAuth();
|
|
67
|
+
const orgId = resolveOrgId(auth, program);
|
|
68
|
+
const body = {};
|
|
69
|
+
if (options.name) body.name = options.name;
|
|
70
|
+
if (options.persona) body.persona = options.persona;
|
|
71
|
+
if (options.description) body.description = options.description;
|
|
72
|
+
await client.patch(resolveEndpoint('assistants.update', { orgId, assistantId }).path, { service: resolveEndpoint('assistants.update', { orgId, assistantId }).service, body });
|
|
73
|
+
printSuccess('Assistant updated.');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
assistant
|
|
77
|
+
.command('delete')
|
|
78
|
+
.description('Delete an assistant')
|
|
79
|
+
.argument('<assistantId>', 'assistant ID')
|
|
80
|
+
.action(async (assistantId) => {
|
|
81
|
+
auth.requireAuth();
|
|
82
|
+
const orgId = resolveOrgId(auth, program);
|
|
83
|
+
await client.delete(resolveEndpoint('assistants.delete', { orgId, assistantId }).path, { service: resolveEndpoint('assistants.delete', { orgId, assistantId }).service });
|
|
84
|
+
printSuccess('Assistant deleted.');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
assistant
|
|
88
|
+
.command('deploy')
|
|
89
|
+
.description('Deploy an assistant')
|
|
90
|
+
.argument('<assistantId>', 'assistant ID')
|
|
91
|
+
.option('--target <target>', 'deployment target')
|
|
92
|
+
.action(async (assistantId, options) => {
|
|
93
|
+
auth.requireAuth();
|
|
94
|
+
const orgId = resolveOrgId(auth, program);
|
|
95
|
+
const body = {};
|
|
96
|
+
if (options.target) body.target = options.target;
|
|
97
|
+
await client.post(resolveEndpoint('assistants.deploy', { orgId, assistantId }).path, { service: resolveEndpoint('assistants.deploy', { orgId, assistantId }).service, body });
|
|
98
|
+
printSuccess('Assistant deployed.');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
assistant
|
|
102
|
+
.command('fork')
|
|
103
|
+
.description('Fork an assistant')
|
|
104
|
+
.argument('<assistantId>', 'assistant ID to fork')
|
|
105
|
+
.option('--name <name>', 'name for the fork')
|
|
106
|
+
.action(async (assistantId, options) => {
|
|
107
|
+
auth.requireAuth();
|
|
108
|
+
const orgId = resolveOrgId(auth, program);
|
|
109
|
+
const body = {};
|
|
110
|
+
if (options.name) body.name = options.name;
|
|
111
|
+
await client.post(resolveEndpoint('assistants.fork', { orgId, assistantId }).path, { service: resolveEndpoint('assistants.fork', { orgId, assistantId }).service, body });
|
|
112
|
+
printSuccess('Assistant forked.');
|
|
113
|
+
});
|
|
114
|
+
}
|
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth commands: login, logout, whoami, signup, mfa
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
import { formatDetail, printSuccess, printInfo } from '../core/output.js';
|
|
7
|
+
import { resolveEndpoint } from '../api/endpoints.js';
|
|
8
|
+
import { loadResidentProfile, residentLogin, resolveResidentBaseUrl } from '../core/residentAuth.js';
|
|
9
|
+
import { readStoredKit, resolveMyAttach, sessionKey, myAttachState } from './agentAttach.js';
|
|
10
|
+
import { resolveActing } from '../core/actingAs.js';
|
|
11
|
+
|
|
12
|
+
export function registerAuthCommands(program, ctx) {
|
|
13
|
+
const { config, auth, client } = ctx;
|
|
14
|
+
|
|
15
|
+
// quox login
|
|
16
|
+
program
|
|
17
|
+
.command('login')
|
|
18
|
+
.description('Authenticate with QuoxCORE')
|
|
19
|
+
.option('--email <email>', 'email address')
|
|
20
|
+
.option('--password <password>', 'password (prefer --password-stdin: argv is visible in process lists)')
|
|
21
|
+
.option('--password-stdin', 'read the password from stdin (one line) — no secret in argv or shell history')
|
|
22
|
+
.option('--mfa-code <code>', 'TOTP code for MFA')
|
|
23
|
+
.option('--org <orgId>', 'organization to log into')
|
|
24
|
+
.option('--resident', 'authenticate as a resident agent using the on-disk Ed25519 identity, no password, no human')
|
|
25
|
+
.option('--resident-profile <path>', 'path to the resident bootstrap profile (default: ~/.config/quox/resident-profile.json)')
|
|
26
|
+
.action(async (options) => {
|
|
27
|
+
// Resident agent path: signed-challenge exchange with an Ed25519 key
|
|
28
|
+
// on disk, entirely separate from the email/password flow below.
|
|
29
|
+
if (options.resident) {
|
|
30
|
+
const ora = (await import('ora')).default;
|
|
31
|
+
const spinner = ora('Authenticating (resident identity)...').start();
|
|
32
|
+
try {
|
|
33
|
+
const profile = loadResidentProfile(options.residentProfile);
|
|
34
|
+
const baseUrl = resolveResidentBaseUrl(config, profile);
|
|
35
|
+
const tokenData = await residentLogin({ baseUrl, profile });
|
|
36
|
+
|
|
37
|
+
const authData = {
|
|
38
|
+
access_token: tokenData.access_token,
|
|
39
|
+
refresh_token: null,
|
|
40
|
+
user_id: tokenData.user?.id || null,
|
|
41
|
+
email: tokenData.user?.email || null,
|
|
42
|
+
display_name: tokenData.user?.display_name || null,
|
|
43
|
+
org_id: profile.org_id,
|
|
44
|
+
org_name: null,
|
|
45
|
+
org_slug: null,
|
|
46
|
+
org_role: tokenData.user?.role || null,
|
|
47
|
+
actor_type: tokenData.user?.actor_type || null,
|
|
48
|
+
host_id: tokenData.user?.host_id || profile.host_id,
|
|
49
|
+
kind: 'resident',
|
|
50
|
+
resident_profile_path: profile._path,
|
|
51
|
+
expires_at: Date.now() + (tokenData.expires_in || 900) * 1000,
|
|
52
|
+
};
|
|
53
|
+
config.setAuth(authData);
|
|
54
|
+
|
|
55
|
+
// quox#455: persist the bootstrap's reach-back URLs onto this
|
|
56
|
+
// profile so every later command (collector, slots, dev) resolves
|
|
57
|
+
// services from what this host actually reaches, never a
|
|
58
|
+
// port/scheme guess. Only fill the profile URL when it is still
|
|
59
|
+
// the local default; an explicit URL a human set always wins.
|
|
60
|
+
const active = typeof config.getActiveProfile === 'function' ? config.getActiveProfile() : null;
|
|
61
|
+
// Any loopback URL counts as "not deliberately set". An exact-string
|
|
62
|
+
// check against http://localhost:3000 missed https://localhost:3000
|
|
63
|
+
// and left the profile base pointing at the host's own empty
|
|
64
|
+
// instance, which only stayed invisible because the per-service
|
|
65
|
+
// overrides below happen to cover the services in use today.
|
|
66
|
+
const urlIsDefault = (() => {
|
|
67
|
+
if (!active?.url) return true;
|
|
68
|
+
try {
|
|
69
|
+
return ['localhost', '127.0.0.1', '::1'].includes(new URL(active.url).hostname);
|
|
70
|
+
} catch {
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
})();
|
|
74
|
+
// quox#455 follow-up / quox#469: stamp AUTH as well as collector.
|
|
75
|
+
// Stamping only collector left every auth-service call (whoami, and
|
|
76
|
+
// the token refresh itself) resolving through getServiceUrl('auth'),
|
|
77
|
+
// which port-swaps the edge to loopback :3101 and is unreachable
|
|
78
|
+
// from any remote host. The acceptance owner hit exactly that:
|
|
79
|
+
// login worked zero-override while whoami and slot ops still needed
|
|
80
|
+
// manual QUOX_AUTH_URL/QUOX_COLLECTOR_URL.
|
|
81
|
+
const services = {};
|
|
82
|
+
if (profile.collector_url) services.collector = profile.collector_url;
|
|
83
|
+
if (profile.instance_url) services.auth = profile.instance_url;
|
|
84
|
+
config.setProfileConnection({
|
|
85
|
+
...(urlIsDefault && profile.instance_url ? { url: profile.instance_url } : {}),
|
|
86
|
+
...(Object.keys(services).length ? { services } : {}),
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
spinner.stop();
|
|
90
|
+
printSuccess(`Logged in as resident agent ${authData.email || authData.user_id || authData.host_id} (host ${authData.host_id})`);
|
|
91
|
+
} catch (err) {
|
|
92
|
+
spinner.fail('Resident login failed');
|
|
93
|
+
throw err;
|
|
94
|
+
}
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
let { email, password, mfaCode } = options;
|
|
99
|
+
const orgId = options.org || program.opts().org;
|
|
100
|
+
|
|
101
|
+
// SVC-AUTH #447 bridge: non-interactive automation reads the secret
|
|
102
|
+
// from stdin (or QUOX_LOGIN_PASSWORD / QUOX_LOGIN_EMAIL env), never
|
|
103
|
+
// argv, so no credential lands in process lists or shell history.
|
|
104
|
+
if (!email && process.env.QUOX_LOGIN_EMAIL) email = process.env.QUOX_LOGIN_EMAIL;
|
|
105
|
+
if (!password && options.passwordStdin) {
|
|
106
|
+
const chunks = [];
|
|
107
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
108
|
+
password = Buffer.concat(chunks).toString('utf8').split('\n')[0].trim();
|
|
109
|
+
if (!password) {
|
|
110
|
+
console.error('Error: --password-stdin given but stdin was empty');
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (!password && process.env.QUOX_LOGIN_PASSWORD) password = process.env.QUOX_LOGIN_PASSWORD;
|
|
115
|
+
|
|
116
|
+
// Interactive prompts if not provided
|
|
117
|
+
if (!email || !password) {
|
|
118
|
+
const { default: inquirer } = await import('inquirer');
|
|
119
|
+
|
|
120
|
+
if (!email) {
|
|
121
|
+
const ans = await inquirer.prompt([{
|
|
122
|
+
type: 'input',
|
|
123
|
+
name: 'email',
|
|
124
|
+
message: 'Email:',
|
|
125
|
+
validate: v => v.includes('@') || 'Please enter a valid email',
|
|
126
|
+
}]);
|
|
127
|
+
email = ans.email;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (!password) {
|
|
131
|
+
const ans = await inquirer.prompt([{
|
|
132
|
+
type: 'password',
|
|
133
|
+
name: 'password',
|
|
134
|
+
message: 'Password:',
|
|
135
|
+
mask: '*',
|
|
136
|
+
}]);
|
|
137
|
+
password = ans.password;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const baseUrl = config.getServiceUrl('auth');
|
|
142
|
+
const ora = (await import('ora')).default;
|
|
143
|
+
const spinner = ora('Authenticating...').start();
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
const result = await auth.login(baseUrl, email, password, orgId);
|
|
147
|
+
spinner.stop();
|
|
148
|
+
|
|
149
|
+
// Handle MFA
|
|
150
|
+
if (result.mfa_required) {
|
|
151
|
+
if (!mfaCode) {
|
|
152
|
+
printInfo('MFA required. Check your authenticator app.');
|
|
153
|
+
const { default: inquirer } = await import('inquirer');
|
|
154
|
+
const ans = await inquirer.prompt([{
|
|
155
|
+
type: 'input',
|
|
156
|
+
name: 'code',
|
|
157
|
+
message: 'TOTP code:',
|
|
158
|
+
}]);
|
|
159
|
+
mfaCode = ans.code;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const mfaSpinner = ora('Verifying MFA...').start();
|
|
163
|
+
await auth.verifyMfa(baseUrl, result.access_token, mfaCode);
|
|
164
|
+
mfaSpinner.succeed('MFA verified');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const authData = config.getAuth();
|
|
168
|
+
// quox#647: login resolves and SETS the active org, silently overriding
|
|
169
|
+
// any prior `quox org switch` (the root cause of wrong-org actions).
|
|
170
|
+
// Always surface the resulting active org so the reset is visible
|
|
171
|
+
// (owner: "at least print 'active org set to X'"). Fall back
|
|
172
|
+
// org_name -> org_id like whoami, since resident logins carry org_id but
|
|
173
|
+
// no org_name (quox#463) and this line used to blank out entirely.
|
|
174
|
+
const orgLabel = authData.org_name
|
|
175
|
+
? `${authData.org_name} (${authData.org_slug})`
|
|
176
|
+
: (authData.org_id || 'unknown');
|
|
177
|
+
printSuccess(`Logged in as ${authData.email}`);
|
|
178
|
+
printInfo(`Active org set to ${orgLabel}`);
|
|
179
|
+
} catch (err) {
|
|
180
|
+
spinner.fail('Login failed');
|
|
181
|
+
throw err;
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// quox logout
|
|
186
|
+
program
|
|
187
|
+
.command('logout')
|
|
188
|
+
.description('End current session')
|
|
189
|
+
.action(async () => {
|
|
190
|
+
const baseUrl = config.getServiceUrl('auth');
|
|
191
|
+
await auth.logout(baseUrl);
|
|
192
|
+
printSuccess('Logged out. Tokens cleared.');
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// quox whoami
|
|
196
|
+
program
|
|
197
|
+
.command('whoami')
|
|
198
|
+
.description('Show current user and session info')
|
|
199
|
+
.action(async () => {
|
|
200
|
+
auth.requireAuth();
|
|
201
|
+
const data = await client.get(resolveEndpoint('auth.me.list').path, { service: resolveEndpoint('auth.me.list').service });
|
|
202
|
+
const authData = config.getAuth();
|
|
203
|
+
|
|
204
|
+
// quox#463: resident logins carry org_id but no org_name (the
|
|
205
|
+
// bootstrap has only the id), and this rendered a real membership as
|
|
206
|
+
// 'none'. Fall back to the id rather than claiming no org.
|
|
207
|
+
//
|
|
208
|
+
// quox#463 follow-up: some resident sessions have no active org_id in
|
|
209
|
+
// the stored session/profile at all, even though `quox org list` on
|
|
210
|
+
// the same session shows a real membership. Whoami is a read verb, so
|
|
211
|
+
// when the active org is unset we resolve it by fetching the caller's
|
|
212
|
+
// org memberships through the exact same endpoint `quox org list`
|
|
213
|
+
// uses, and display the single membership (or the first, marked as
|
|
214
|
+
// resolved from membership) instead of the literal 'none'. Only an
|
|
215
|
+
// empty membership list still says 'none'. This never writes back to
|
|
216
|
+
// stored auth state; it only changes what is displayed.
|
|
217
|
+
let orgDisplay;
|
|
218
|
+
if (authData.org_name) {
|
|
219
|
+
orgDisplay = `${authData.org_name} (${authData.org_slug})`;
|
|
220
|
+
} else if (authData.org_id) {
|
|
221
|
+
orgDisplay = authData.org_id;
|
|
222
|
+
} else {
|
|
223
|
+
let memberships = [];
|
|
224
|
+
try {
|
|
225
|
+
const orgsEp = resolveEndpoint('orgs.list');
|
|
226
|
+
const orgsData = await client.get(orgsEp.path, { service: orgsEp.service });
|
|
227
|
+
const list = orgsData.organizations || orgsData.orgs || orgsData;
|
|
228
|
+
memberships = Array.isArray(list) ? list : [];
|
|
229
|
+
} catch {
|
|
230
|
+
memberships = [];
|
|
231
|
+
}
|
|
232
|
+
if (memberships.length > 0) {
|
|
233
|
+
const m = memberships[0];
|
|
234
|
+
const label = m.name || m.id;
|
|
235
|
+
orgDisplay = `${label}${m.slug ? ` (${m.slug})` : ''} (resolved from membership)`;
|
|
236
|
+
} else {
|
|
237
|
+
orgDisplay = 'none';
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const info = {
|
|
242
|
+
user: data.user?.display_name || data.user?.email || authData.email,
|
|
243
|
+
email: data.user?.email || authData.email,
|
|
244
|
+
user_id: data.user?.id || authData.user_id,
|
|
245
|
+
org: orgDisplay,
|
|
246
|
+
role: authData.org_role || 'n/a',
|
|
247
|
+
mfa: data.user?.mfa_enabled ? 'enabled' : 'disabled',
|
|
248
|
+
session: authData.expires_at
|
|
249
|
+
? `expires in ${Math.max(0, Math.round((authData.expires_at - Date.now()) / 60000))}m`
|
|
250
|
+
: 'unknown',
|
|
251
|
+
profile: config.getActiveProfileName(),
|
|
252
|
+
};
|
|
253
|
+
const fields = ['user', 'email', 'user_id', 'org', 'role', 'mfa', 'session', 'profile'];
|
|
254
|
+
const labels = {
|
|
255
|
+
user: 'User',
|
|
256
|
+
email: 'Email',
|
|
257
|
+
user_id: 'User ID',
|
|
258
|
+
org: 'Org',
|
|
259
|
+
role: 'Role',
|
|
260
|
+
mfa: 'MFA',
|
|
261
|
+
session: 'Session',
|
|
262
|
+
profile: 'Profile',
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
// Resident agent identity (quox#453 phase 15): only shown when the
|
|
266
|
+
// stored identity actually carries it, so a normal human login is
|
|
267
|
+
// unaffected.
|
|
268
|
+
const actorType = data.user?.actor_type || authData.actor_type;
|
|
269
|
+
const hostId = data.user?.host_id || authData.host_id;
|
|
270
|
+
if (actorType) {
|
|
271
|
+
info.actor_type = actorType;
|
|
272
|
+
labels.actor_type = 'Actor Type';
|
|
273
|
+
fields.splice(fields.indexOf('role') + 1, 0, 'actor_type');
|
|
274
|
+
}
|
|
275
|
+
if (hostId) {
|
|
276
|
+
info.host_id = hostId;
|
|
277
|
+
labels.host_id = 'Host ID';
|
|
278
|
+
fields.splice(fields.indexOf('mfa'), 0, 'host_id');
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
formatDetail(info, {
|
|
282
|
+
format: program.opts().output,
|
|
283
|
+
fields,
|
|
284
|
+
labels,
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
// F4 (QuoxIdentity CLI finding-fix): whoami answered "who is this
|
|
288
|
+
// token" but not "is this session also attached as an agent identity".
|
|
289
|
+
// QUOXIDENTITY P9: report only THIS session's agent (resolved
|
|
290
|
+
// session-scoped), not every attach on the box — the old scan listed
|
|
291
|
+
// every session's lease, which is exactly the "single resident sees all"
|
|
292
|
+
// flaw. A scan failure must never fail whoami itself.
|
|
293
|
+
try {
|
|
294
|
+
// Show what PLAIN verbs would act as (whoami itself is exempt from the default).
|
|
295
|
+
const acting = await resolveActing({ argv: process.argv.slice(2).filter((x) => x !== 'whoami') .concat(['_probe']) });
|
|
296
|
+
if (acting.source === 'flag' || acting.source === 'env' || acting.source === 'default') {
|
|
297
|
+
const askit = readStoredKit(acting.name);
|
|
298
|
+
const why = acting.source === 'flag' ? '--as' : acting.source === 'env' ? 'QUOX_AS' : 'default: attached session';
|
|
299
|
+
printInfo(
|
|
300
|
+
` Acting as ${acting.name} (${why})`
|
|
301
|
+
+ (askit?.lease?.id ? ` (attach lease ${askit.lease.id}, expires ${askit.lease.expires_at || 'unknown'})` : '')
|
|
302
|
+
);
|
|
303
|
+
} else if (acting.source === 'self') {
|
|
304
|
+
printInfo(' Acting as yourself (--as self)');
|
|
305
|
+
} else if (acting.source === 'profile') {
|
|
306
|
+
printInfo(' Acting as yourself (-p/--profile given)');
|
|
307
|
+
} else if (myAttachState().state === 'ambiguous') {
|
|
308
|
+
printInfo(' Acting as yourself: this session has more than one attach (ambiguous), so no agent default applies. Pick one with --as <agent>.');
|
|
309
|
+
}
|
|
310
|
+
} catch {
|
|
311
|
+
// Best effort only.
|
|
312
|
+
}
|
|
313
|
+
try {
|
|
314
|
+
const mine = resolveMyAttach();
|
|
315
|
+
if (mine) {
|
|
316
|
+
const kit = readStoredKit(mine.agentId);
|
|
317
|
+
const key = sessionKey();
|
|
318
|
+
printInfo(
|
|
319
|
+
` attached as ${mine.agentId}`
|
|
320
|
+
+ (kit?.lease?.id ? ` (lease ${String(kit.lease.id).slice(0, 8)}, expires ${kit.lease.expires_at || 'unknown'})` : '')
|
|
321
|
+
+ (key ? ` · session ${key}` : '')
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
} catch {
|
|
325
|
+
// Best effort only: never fail whoami on an attach-state scan error.
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
// quox signup
|
|
330
|
+
program
|
|
331
|
+
.command('signup')
|
|
332
|
+
.description('Create a new user account')
|
|
333
|
+
.requiredOption('--email <email>', 'email address')
|
|
334
|
+
.requiredOption('--password <password>', 'password')
|
|
335
|
+
.requiredOption('--name <name>', 'display name')
|
|
336
|
+
.action(async (options) => {
|
|
337
|
+
const data = await client.post(resolveEndpoint('auth.signup').path, {
|
|
338
|
+
service: resolveEndpoint('auth.signup').service,
|
|
339
|
+
body: {
|
|
340
|
+
email: options.email,
|
|
341
|
+
password: options.password,
|
|
342
|
+
display_name: options.name,
|
|
343
|
+
},
|
|
344
|
+
noAuth: true,
|
|
345
|
+
});
|
|
346
|
+
printSuccess(`Account created for ${options.email}. Check your email for verification.`);
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
// quox mfa
|
|
350
|
+
const mfa = program
|
|
351
|
+
.command('mfa')
|
|
352
|
+
.description('Manage multi-factor authentication');
|
|
353
|
+
|
|
354
|
+
mfa
|
|
355
|
+
.command('setup')
|
|
356
|
+
.description('Enable MFA for your account')
|
|
357
|
+
.action(async () => {
|
|
358
|
+
auth.requireAuth();
|
|
359
|
+
const data = await client.post(resolveEndpoint('mfa.setup').path, { service: resolveEndpoint('mfa.setup').service });
|
|
360
|
+
if (data.qr_code || data.secret) {
|
|
361
|
+
console.log(chalk.bold('MFA Setup'));
|
|
362
|
+
if (data.secret) console.log(`Secret: ${data.secret}`);
|
|
363
|
+
if (data.qr_code) console.log(`QR URL: ${data.qr_code}`);
|
|
364
|
+
printInfo('Enter the code from your authenticator app to verify.');
|
|
365
|
+
} else {
|
|
366
|
+
printSuccess('MFA setup initiated. Check the response for setup instructions.');
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
mfa
|
|
371
|
+
.command('verify')
|
|
372
|
+
.description('Verify MFA setup with a code')
|
|
373
|
+
.requiredOption('--code <code>', 'TOTP verification code')
|
|
374
|
+
.action(async (options) => {
|
|
375
|
+
auth.requireAuth();
|
|
376
|
+
await client.post(resolveEndpoint('mfa.verify-setup').path, {
|
|
377
|
+
service: resolveEndpoint('mfa.verify-setup').service,
|
|
378
|
+
body: { code: options.code },
|
|
379
|
+
});
|
|
380
|
+
printSuccess('MFA enabled successfully.');
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
mfa
|
|
384
|
+
.command('disable')
|
|
385
|
+
.description('Disable MFA for your account')
|
|
386
|
+
.requiredOption('--code <code>', 'TOTP code to confirm')
|
|
387
|
+
.action(async (options) => {
|
|
388
|
+
auth.requireAuth();
|
|
389
|
+
await client.post(resolveEndpoint('mfa.disable').path, {
|
|
390
|
+
service: resolveEndpoint('mfa.disable').service,
|
|
391
|
+
body: { code: options.code },
|
|
392
|
+
});
|
|
393
|
+
printSuccess('MFA disabled.');
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
mfa
|
|
397
|
+
.command('backup-codes')
|
|
398
|
+
.description('View MFA backup codes')
|
|
399
|
+
.action(async () => {
|
|
400
|
+
auth.requireAuth();
|
|
401
|
+
const data = await client.get(resolveEndpoint('mfa.backup-codes.list').path, { service: resolveEndpoint('mfa.backup-codes.list').service });
|
|
402
|
+
if (program.opts().output === 'json') {
|
|
403
|
+
console.log(JSON.stringify(data, null, 2));
|
|
404
|
+
} else {
|
|
405
|
+
console.log(chalk.bold('Backup Codes'));
|
|
406
|
+
console.log(chalk.dim('Save these somewhere safe:\n'));
|
|
407
|
+
for (const code of (data.codes || data.backup_codes || [])) {
|
|
408
|
+
console.log(` ${code}`);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
// ── quox account ────────────────────────────────────────────────────────
|
|
414
|
+
// fleet #390: PATCH /api/auth/me/password existed, worked and audited, but
|
|
415
|
+
// no command exposed it — so rotating your own password on a headless box
|
|
416
|
+
// meant hand-crafting a curl with a bearer token. Credential rotation is
|
|
417
|
+
// basic hygiene; making the correct action harder than doing nothing is
|
|
418
|
+
// the wrong incentive for a governance product.
|
|
419
|
+
const account = program
|
|
420
|
+
.command('account')
|
|
421
|
+
.description('Your own account (password, session)');
|
|
422
|
+
|
|
423
|
+
account
|
|
424
|
+
.command('password')
|
|
425
|
+
.description('Change your password')
|
|
426
|
+
// Values are prompted by default. --current/--new exist for automation,
|
|
427
|
+
// but they land in argv and shell history, so the help says so plainly
|
|
428
|
+
// rather than presenting them as equivalent.
|
|
429
|
+
.option('--current <password>', 'current password (WARNING: visible in argv/shell history — prefer the prompt)')
|
|
430
|
+
.option('--new <password>', 'new password (same warning)')
|
|
431
|
+
.action(async (options) => {
|
|
432
|
+
auth.requireAuth();
|
|
433
|
+
|
|
434
|
+
let current = options.current;
|
|
435
|
+
let next = options.new;
|
|
436
|
+
|
|
437
|
+
if (!current || !next) {
|
|
438
|
+
const { default: inquirer } = await import('inquirer');
|
|
439
|
+
if (!current) {
|
|
440
|
+
const a = await inquirer.prompt([{ type: 'password', name: 'v', message: 'Current password:', mask: '*' }]);
|
|
441
|
+
current = a.v;
|
|
442
|
+
}
|
|
443
|
+
if (!next) {
|
|
444
|
+
const a = await inquirer.prompt([{ type: 'password', name: 'v', message: 'New password:', mask: '*' }]);
|
|
445
|
+
const b = await inquirer.prompt([{ type: 'password', name: 'v', message: 'Confirm new password:', mask: '*' }]);
|
|
446
|
+
if (a.v !== b.v) throw new Error('New passwords do not match.');
|
|
447
|
+
next = a.v;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const ep = resolveEndpoint('auth.me.password');
|
|
452
|
+
await client.patch(ep.path, {
|
|
453
|
+
service: ep.service,
|
|
454
|
+
body: { current_password: current, new_password: next },
|
|
455
|
+
});
|
|
456
|
+
printSuccess('Password changed. Existing sessions elsewhere may need re-authentication.');
|
|
457
|
+
});
|
|
458
|
+
}
|