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,324 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `quox gripe` — QUOXIFY PASS 2 (DEVSUITE phase 15).
|
|
3
|
+
*
|
|
4
|
+
* The bundle audit (note #2840, theme S9) found Gripe had no `quox` verb at
|
|
5
|
+
* all, despite one being in its own definition of done. The customer-facing
|
|
6
|
+
* filing path was chat-agent-only, which meant a user had to know an agent
|
|
7
|
+
* existed and open a chat surface to report that something was broken. The
|
|
8
|
+
* fastest possible complaint is a one-liner in the terminal you are already
|
|
9
|
+
* in, which is exactly what the original terminal-only gripe skill was for.
|
|
10
|
+
*
|
|
11
|
+
* This wraps the WORKING path rather than building a second one: it talks to
|
|
12
|
+
* the GRIPE agent over the same governed chat route the dashboard and the
|
|
13
|
+
* Discord/Telegram bridges use, and GRIPE files the issue through the GitHub
|
|
14
|
+
* connector. No parallel issue store, no second credential model (the audit
|
|
15
|
+
* flagged three entry paths with three credential models; this adds a fourth
|
|
16
|
+
* ENTRY but reuses the third's governed path deliberately).
|
|
17
|
+
*
|
|
18
|
+
* Honest prerequisite, per the audit's "disclose the prereq" finding: filing
|
|
19
|
+
* needs a configured GitHub connector for the org. If it is missing, the
|
|
20
|
+
* agent says so and this command surfaces that plainly instead of pretending
|
|
21
|
+
* an issue was created.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { formatDetail, printInfo, printWarning } from '../core/output.js';
|
|
25
|
+
import { events, line as glyphLine, printEvent } from '../core/quoxglyph.js';
|
|
26
|
+
import { resolveEndpoint } from '../api/endpoints.js';
|
|
27
|
+
import { resolveOrgId } from './helpers.js';
|
|
28
|
+
|
|
29
|
+
export function registerGripeCommands(program, { client, auth }) {
|
|
30
|
+
program
|
|
31
|
+
.command('gripe')
|
|
32
|
+
.description('File a one-liner complaint as a GitHub issue (via the GRIPE agent)')
|
|
33
|
+
.argument('<complaint...>', 'the gripe, e.g. "sidebar tooltip clips on resize"')
|
|
34
|
+
.option('--repo <owner/name>', 'target repo (GRIPE defaults sensibly when omitted)')
|
|
35
|
+
.option('--label <label>', 'issue label, e.g. bug, idea, fleet')
|
|
36
|
+
.option('--yes', 'skip the confirmation step and let GRIPE file it directly')
|
|
37
|
+
.action(async (complaintParts, options) => {
|
|
38
|
+
auth.requireAuth();
|
|
39
|
+
const complaint = complaintParts.join(' ').trim();
|
|
40
|
+
if (!complaint) throw new Error('say what is wrong: quox gripe "the thing that is broken"');
|
|
41
|
+
|
|
42
|
+
printEvent(glyphLine('quox', 'filing gripe via GRIPE'));
|
|
43
|
+
const orgId = resolveOrgId(auth, program);
|
|
44
|
+
const data = await askGripe({ client, complaint, repo: options.repo, label: options.label, yes: !!options.yes, orgId });
|
|
45
|
+
|
|
46
|
+
if (program.opts().output === 'json') {
|
|
47
|
+
formatDetail(data, { format: 'json' });
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const r = await settleGripe({ client, data, complaint, options, orgId });
|
|
52
|
+
console.log('');
|
|
53
|
+
if (r.superseded) printInfo(SUPERSEDED_MESSAGE);
|
|
54
|
+
else console.log(r.response || '(no response)');
|
|
55
|
+
console.log('');
|
|
56
|
+
for (const n of r.notes) {
|
|
57
|
+
if (n.kind === 'created') printEvent(events.created('quox', 'issue', n.url));
|
|
58
|
+
else if (n.kind === 'info') printInfo(n.text);
|
|
59
|
+
else printWarning(n.text);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const SUPERSEDED_MESSAGE = 'GRIPE agent did not file this itself; its reply is superseded by the direct filing below.';
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Ask the GRIPE agent to file `complaint` over the governed chat route. -> the raw chat response.
|
|
68
|
+
* Shared by `quox gripe` and the quox client's /gripe, so there is one filing path.
|
|
69
|
+
*/
|
|
70
|
+
export async function askGripe({ client, complaint, repo, label, yes = false, orgId = null }) {
|
|
71
|
+
// GRIPE's whole discipline is speed: it does not investigate, it files.
|
|
72
|
+
// So the prompt states everything up front rather than inviting a
|
|
73
|
+
// back-and-forth the terminal is a bad place for.
|
|
74
|
+
const parts = [`File this gripe: ${complaint}`];
|
|
75
|
+
if (repo) parts.push(`Repo: ${repo}.`);
|
|
76
|
+
if (label) parts.push(`Label: ${label}.`);
|
|
77
|
+
if (yes) parts.push('File it now without asking me to confirm.');
|
|
78
|
+
else parts.push('Confirm the repo, label and title with me before filing.');
|
|
79
|
+
const ep = resolveEndpoint('chat.claude');
|
|
80
|
+
const body = { prompt: parts.join(' '), selected_agent: 'gripe' };
|
|
81
|
+
// The collector tracks which tools ran PER caller-supplied session_id;
|
|
82
|
+
// sending none meant tools_used always came back [] and classification
|
|
83
|
+
// fell back to parsing prose (DEVSUITE-GRIPE phase 5). One id per run.
|
|
84
|
+
body.session_id = `GRIPE_CLI_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
|
|
85
|
+
if (orgId) body.org_id = orgId;
|
|
86
|
+
return client.post(ep.path, { service: ep.service, body, timeout: 120000 });
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Decide the outcome of a GRIPE run and, with yes, run the deterministic file when the agent did not.
|
|
91
|
+
* -> { response, outcome, superseded, url, notes: [{ kind: 'created', url } | { kind: 'info'|'warn', text }] }.
|
|
92
|
+
* Prints nothing: the caller renders the notes (console for `quox gripe`, history lines for /gripe).
|
|
93
|
+
*/
|
|
94
|
+
export async function settleGripe({ client, data, complaint, options = {}, orgId = null }) {
|
|
95
|
+
const response = data?.result || data?.response || data?.text || '';
|
|
96
|
+
const yes = !!options.yes;
|
|
97
|
+
// tools_used is the collector's own record of which tools ran. Passing it
|
|
98
|
+
// is what stops the outcome from depending on how the agent phrased its
|
|
99
|
+
// reply. Absent (older collector), classification falls back to prose.
|
|
100
|
+
const toolsUsed = Array.isArray(data?.tools_used) ? data.tools_used : null;
|
|
101
|
+
const outcome = classifyGripeResponse(response, { assumedYes: yes, toolsUsed });
|
|
102
|
+
// quox#668: with --yes, a non-filing agent run is followed by the
|
|
103
|
+
// deterministic file. Printing the agent's prose first ("no tool, cannot
|
|
104
|
+
// file") right above "issue created" contradicted the real outcome. In
|
|
105
|
+
// that case the agent's reply is superseded, so say so instead of
|
|
106
|
+
// echoing a claim the next lines disprove.
|
|
107
|
+
const superseded = yes && ['no-connector', 'awaiting-confirm', 'not-filed'].some((k) => outcome.kind === k || outcome.hint === k);
|
|
108
|
+
const notes = [];
|
|
109
|
+
let url = null;
|
|
110
|
+
const det = async () => {
|
|
111
|
+
const r = await fileDeterministically({ client, complaint, options, orgId, quiet: true });
|
|
112
|
+
notes.push(...r.notes); if (r.url) url = r.url;
|
|
113
|
+
return r;
|
|
114
|
+
};
|
|
115
|
+
if (outcome.kind === 'filed') {
|
|
116
|
+
url = outcome.url; notes.push({ kind: 'created', url });
|
|
117
|
+
} else if (outcome.kind === 'filed-unknown') {
|
|
118
|
+
notes.push({ kind: 'warn', text: 'GRIPE called github_create_issue but no issue link came back. Check the repo before re-running: '
|
|
119
|
+
+ 'the issue may exist, and a re-run would file it twice.' });
|
|
120
|
+
} else if (outcome.hint === 'no-connector' || outcome.kind === 'no-connector') {
|
|
121
|
+
// quox#621: "no connector" from the AGENT is not proof the org lacks
|
|
122
|
+
// one. The agent's tool-resolution can transiently fail (the gripe
|
|
123
|
+
// filed on an immediate retry, same org) or the github_create_issue
|
|
124
|
+
// tool may not be wired to this agent (#581), while a healthy github
|
|
125
|
+
// credential sits in the vault. The deterministic path resolves that
|
|
126
|
+
// credential server-side over a DIFFERENT route (the /github/issues
|
|
127
|
+
// proxy, not the agent tool), so with --yes we try it before believing
|
|
128
|
+
// the agent: if a connector really exists this files it (turning a
|
|
129
|
+
// silent-loss-then-retry into a first-try success); only if the vault
|
|
130
|
+
// path ALSO cannot file is the connector genuinely missing, and only
|
|
131
|
+
// then do we send the user to Settings.
|
|
132
|
+
if (yes) {
|
|
133
|
+
const r = await det();
|
|
134
|
+
if (!r.filed) notes.push({ kind: 'warn', text: NO_CONNECTOR_MESSAGE });
|
|
135
|
+
} else {
|
|
136
|
+
notes.push({ kind: 'warn', text: NO_CONNECTOR_MESSAGE });
|
|
137
|
+
}
|
|
138
|
+
} else if (outcome.hint === 'auth-unavailable' || outcome.kind === 'auth-unavailable') {
|
|
139
|
+
notes.push({ kind: 'warn', text: AUTH_UNAVAILABLE_MESSAGE });
|
|
140
|
+
} else if (outcome.hint === 'awaiting-confirm' || outcome.kind === 'awaiting-confirm') {
|
|
141
|
+
if (yes) await det();
|
|
142
|
+
else notes.push({ kind: 'info', text: 'GRIPE is asking you to confirm. Re-run with --yes to file it directly.' });
|
|
143
|
+
} else if (outcome.kind === 'not-filed') {
|
|
144
|
+
if (yes) await det();
|
|
145
|
+
else notes.push({ kind: 'warn', text: 'Not filed: github_create_issue was never called on this run.' });
|
|
146
|
+
} else {
|
|
147
|
+
notes.push({ kind: 'warn', text: 'No issue URL came back, so treat this as NOT filed until you can see the issue.' });
|
|
148
|
+
}
|
|
149
|
+
return { response, outcome, superseded, url, notes };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// quox#478: --yes must GUARANTEE a headless file, not merely ask the model
|
|
153
|
+
// nicely. When the LLM run verifiably did not file (tools_used is the
|
|
154
|
+
// collector's own record), fall back to the governed GitHub proxy
|
|
155
|
+
// (POST /github/issues: user-authed, entitlement-gated, credential resolved
|
|
156
|
+
// from the org vault server-side) and file deterministically. Deliberately
|
|
157
|
+
// NOT run after 'filed-unknown' (the issue may exist; a second file would
|
|
158
|
+
// duplicate it). It IS now run after 'no-connector' with --yes (quox#621):
|
|
159
|
+
// the proxy resolves the vault credential over a DIFFERENT route than the
|
|
160
|
+
// agent tool, so it succeeds when the agent merely failed to resolve/wire the
|
|
161
|
+
// tool (#581) — and the caller falls back to NO_CONNECTOR_MESSAGE only if this
|
|
162
|
+
// also cannot file. Still NOT auto-run after auth-unavailable (a 403/429/503
|
|
163
|
+
// is a platform outage the proxy would hit too; retrying shortly is the fix).
|
|
164
|
+
// Returns { filed, url?, error? } so the caller can decide the message.
|
|
165
|
+
export async function fileDeterministically({ client, complaint, options, orgId, quiet = false }) {
|
|
166
|
+
const notes = [];
|
|
167
|
+
const say = (n) => { notes.push(n); if (quiet) return; if (n.kind === 'created') printEvent(events.created('quox', 'issue', n.url)); else if (n.kind === 'info') printInfo(n.text); else printWarning(n.text); };
|
|
168
|
+
const repoArg = options.repo || 'quoxai/quox';
|
|
169
|
+
const [owner, repo] = repoArg.includes('/') ? repoArg.split('/', 2) : ['quoxai', repoArg];
|
|
170
|
+
const firstLine = complaint.split('\n')[0];
|
|
171
|
+
const title = firstLine.length > 80 ? `${firstLine.slice(0, 77)}...` : firstLine;
|
|
172
|
+
const ep = resolveEndpoint('github.issue.create');
|
|
173
|
+
try {
|
|
174
|
+
const data = await client.post(ep.path, {
|
|
175
|
+
service: ep.service,
|
|
176
|
+
body: {
|
|
177
|
+
owner,
|
|
178
|
+
repo,
|
|
179
|
+
title,
|
|
180
|
+
body: `## Gripe\n${complaint}\n\n## Context\n- Filed deterministically by \`quox gripe --yes\` (the GRIPE agent run did not file; quox#478 guarantee path)\n- Priority: minor / off-task observation\n\n## Suggested Action\nInvestigate and fix if confirmed.`,
|
|
181
|
+
labels: [options.label || 'fleet/qa'],
|
|
182
|
+
...(orgId ? { org_id: orgId } : {}),
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
const url = data?.issue?.html_url || data?.issue?.url || null;
|
|
186
|
+
if (url) {
|
|
187
|
+
say({ kind: 'created', url });
|
|
188
|
+
say({ kind: 'info', text: 'Filed via the deterministic path (the GRIPE agent run did not file; --yes now guarantees the file).' });
|
|
189
|
+
return { filed: true, url, notes };
|
|
190
|
+
}
|
|
191
|
+
say({ kind: 'warn', text: 'Deterministic file returned no issue URL; treat as NOT filed until you can see the issue.' });
|
|
192
|
+
return { filed: false, notes };
|
|
193
|
+
} catch (err) {
|
|
194
|
+
say({ kind: 'warn', text: `Not filed: the GRIPE run did not file and the deterministic fallback failed too (${err.message}).` });
|
|
195
|
+
return { filed: false, error: err.message, notes };
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const NO_CONNECTOR_MESSAGE =
|
|
200
|
+
'No issue was filed: the org has no GitHub connector configured. Set one up in Settings > Integrations, then re-run.';
|
|
201
|
+
|
|
202
|
+
const AUTH_UNAVAILABLE_MESSAGE =
|
|
203
|
+
'No issue was filed: a platform authorization step failed (membership verify or credential resolve did not answer). '
|
|
204
|
+
+ 'This is NOT a missing GitHub connector. Retry shortly; if it persists, check auth service health.';
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Decide what actually happened from GRIPE's reply.
|
|
208
|
+
*
|
|
209
|
+
* Split out of the action handler so it can be tested against real agent
|
|
210
|
+
* wording. The missing-connector branch used to match
|
|
211
|
+
* /not configured|no github|credential|connector/i, which does not match what
|
|
212
|
+
* the agent actually says. Observed live on 2026-08-23:
|
|
213
|
+
*
|
|
214
|
+
* "GitHub isn't connected for this org, no issue-filing tool is available
|
|
215
|
+
* right now. Connect GitHub in Settings > Integrations and I'll file it
|
|
216
|
+
* immediately after. I'm not fabricating an issue number."
|
|
217
|
+
*
|
|
218
|
+
* None of the four alternatives appear in that sentence, so the specific,
|
|
219
|
+
* actionable "connect GitHub in Settings" advice was skipped and the user got
|
|
220
|
+
* the generic "treat this as NOT filed" instead. Both are honest, which is why
|
|
221
|
+
* this never showed up as a bug: it degraded to a vaguer truth rather than a
|
|
222
|
+
* falsehood. It still costs the user the one sentence that says what to do.
|
|
223
|
+
*
|
|
224
|
+
* Matching prose with a regex is fragile by nature and this will drift again
|
|
225
|
+
* when the agent's phrasing changes. It is deliberately biased toward the
|
|
226
|
+
* generic branch: a missed connector hint is a papercut, whereas wrongly
|
|
227
|
+
* claiming a connector problem would send someone to Settings for a fault
|
|
228
|
+
* that is not there. The URL check stays first and stays authoritative, since
|
|
229
|
+
* a real issue link is the only thing that proves filing.
|
|
230
|
+
*/
|
|
231
|
+
export function classifyGripeResponse(response, { assumedYes = false, toolsUsed = null } = {}) {
|
|
232
|
+
const text = String(response ?? '');
|
|
233
|
+
const url = (text.match(/https:\/\/github\.com\/\S+\/issues\/\d+/) || [])[0];
|
|
234
|
+
if (url) return { kind: 'filed', url, certain: true };
|
|
235
|
+
|
|
236
|
+
// The Claude CLI's own error for a NATIVE call to a tool that is not native.
|
|
237
|
+
// In CLI-tools mode the catalogue runs only via `quox tool exec`, so this
|
|
238
|
+
// string means github_create_issue never ran (2026-10-10: every --yes gripe
|
|
239
|
+
// fell to "unknown" on it and never reached the direct filing).
|
|
240
|
+
if (/No such tool available/i.test(text)) {
|
|
241
|
+
return { kind: 'not-filed', certain: true, hint: null };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Structured evidence beats prose. The collector returns `tools_used` on
|
|
245
|
+
// every chat response, so whether github_create_issue actually RAN is a fact,
|
|
246
|
+
// not something to infer from English. Reading the outcome out of the agent's
|
|
247
|
+
// wording is what this function got wrong twice: once when the
|
|
248
|
+
// missing-connector phrasing changed, once when the bare word "connector"
|
|
249
|
+
// matched a healthy reply. A third regex patch would be the same mistake
|
|
250
|
+
// again, so the outcome now comes from the tool list and prose is demoted to
|
|
251
|
+
// a HINT about what to advise, where being wrong costs a sentence rather than
|
|
252
|
+
// a false claim.
|
|
253
|
+
// An EMPTY list is NOT evidence. The collector tracks MCP tool calls per
|
|
254
|
+
// caller-supplied session_id, so a request that sends none gets [] back
|
|
255
|
+
// whether or not the tool ran. Reading that as "never called" would replace a
|
|
256
|
+
// hedged truth with a confident falsehood and invite a duplicate issue.
|
|
257
|
+
// Absence of evidence is not evidence of absence.
|
|
258
|
+
if (Array.isArray(toolsUsed) && toolsUsed.length > 0) {
|
|
259
|
+
const ran = toolsUsed.some((t) => String(t).endsWith('github_create_issue'));
|
|
260
|
+
if (ran) {
|
|
261
|
+
// The tool ran and returned no link. Saying "filed" would invent an
|
|
262
|
+
// issue; saying "not filed" would send the user to file a duplicate.
|
|
263
|
+
return { kind: 'filed-unknown', certain: true };
|
|
264
|
+
}
|
|
265
|
+
return { kind: 'not-filed', certain: true, hint: adviceHint(text, assumedYes) };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// No structured evidence available (an older collector, or a surface that
|
|
269
|
+
// does not return tools_used). Fall back to the prose-only classification,
|
|
270
|
+
// unchanged, rather than degrading every caller to "unknown".
|
|
271
|
+
const hint = adviceHint(text, assumedYes);
|
|
272
|
+
if (hint === 'no-connector') return { kind: 'no-connector' };
|
|
273
|
+
if (hint === 'auth-unavailable') return { kind: 'auth-unavailable' };
|
|
274
|
+
if (hint === 'awaiting-confirm') return { kind: 'awaiting-confirm' };
|
|
275
|
+
return { kind: 'unknown' };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* What to ADVISE when nothing was filed. This is a hint, never an outcome:
|
|
280
|
+
* getting it wrong costs the user one unhelpful sentence, where getting the
|
|
281
|
+
* outcome wrong costs them a duplicate issue or a phantom one.
|
|
282
|
+
*
|
|
283
|
+
* @returns {'no-connector'|'awaiting-confirm'|null}
|
|
284
|
+
*/
|
|
285
|
+
function adviceHint(text, assumedYes) {
|
|
286
|
+
// Platform-side authorization/availability failures beat the no-connector
|
|
287
|
+
// guess. On 2026-09-21 auth's rate limiter 429'd the collector's membership
|
|
288
|
+
// verify, the tool call 403'd with "no active membership", GRIPE narrated
|
|
289
|
+
// "GitHub isn't connected", and this function turned a platform outage into
|
|
290
|
+
// "the org has no GitHub connector configured" — a confident falsehood while
|
|
291
|
+
// the vault held three healthy credentials (quox#565). These shapes come
|
|
292
|
+
// from the collector's own error strings, so match them FIRST.
|
|
293
|
+
if (
|
|
294
|
+
/membership/i.test(text) ||
|
|
295
|
+
/verification .{0,20}unavailable/i.test(text) ||
|
|
296
|
+
/auth service did not answer/i.test(text) ||
|
|
297
|
+
/\b(403|503|429)\b/.test(text)
|
|
298
|
+
) {
|
|
299
|
+
return 'auth-unavailable';
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Every alternative here must carry its own NEGATION. The bare words
|
|
303
|
+
// `credential` and `connector` used to be alternatives on their own, and on
|
|
304
|
+
// 2026-08-23 that fired on a reply where GRIPE said "Yes, I can see it" and
|
|
305
|
+
// then echoed the user's word inside the proposed issue title. The CLI told
|
|
306
|
+
// the user to go configure a connector that was working. `connector` and
|
|
307
|
+
// `credential` are what this feature is ABOUT, so they appear in healthy
|
|
308
|
+
// replies constantly; only a negation makes them evidence of anything.
|
|
309
|
+
const noConnector =
|
|
310
|
+
/not configured/i.test(text) ||
|
|
311
|
+
/no github/i.test(text) ||
|
|
312
|
+
/github (is|isn't|is not|was not|wasn't)\s*(n't)?\s*connect/i.test(text) ||
|
|
313
|
+
/no issue[- ]filing tool/i.test(text) ||
|
|
314
|
+
/connect github/i.test(text) ||
|
|
315
|
+
/no (\w+ ){0,2}(credential|connector|integration)/i.test(text) ||
|
|
316
|
+
/(credential|connector|integration)s?\s+(is|are)\s+(not|un)(configured|available|connected|set)/i.test(text);
|
|
317
|
+
if (noConnector) return 'no-connector';
|
|
318
|
+
|
|
319
|
+
// Only meaningful when the caller did NOT pass --yes: with --yes the prompt
|
|
320
|
+
// already told GRIPE to file without asking, so a reply with no URL is an
|
|
321
|
+
// unknown outcome, not a confirmation prompt.
|
|
322
|
+
if (!assumedYes) return 'awaiting-confirm';
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QuoxHat commands (QUOXHAT #721 b): READ-ONLY views of the org's hats.
|
|
3
|
+
*
|
|
4
|
+
* quox hat list
|
|
5
|
+
* quox hat show <id|key> [--rev <n>]
|
|
6
|
+
* quox hat reset <id|key> --yes (audited reset-to-template)
|
|
7
|
+
*
|
|
8
|
+
* Shows what each hat presents (explicit tools, bundles, connector families),
|
|
9
|
+
* its authorization mode, revision, template status and the org's enforcement
|
|
10
|
+
* and selection mode. Editing hat definitions is deliberately NOT here: edits
|
|
11
|
+
* go through the dashboard (Settings, QuoxHat Modes) where they are validated
|
|
12
|
+
* and audited. The one write is `reset`, which returns an org-edited built-in
|
|
13
|
+
* to its current template through the same audited route the dashboard uses.
|
|
14
|
+
*
|
|
15
|
+
* Backend: services/auth/routes/quoxhat.js (see cli/lib/api/endpoints/quoxhat.js).
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import chalk from 'chalk';
|
|
19
|
+
import { formatOutput, printInfo } from '../core/output.js';
|
|
20
|
+
import { resolveOrgId } from './helpers.js';
|
|
21
|
+
import { resolveEndpoint } from '../api/endpoints.js';
|
|
22
|
+
|
|
23
|
+
/** Authorization mode a hat declares; no authz block means permissive (grant ceiling). */
|
|
24
|
+
export function authzMode(def) {
|
|
25
|
+
const a = def && typeof def.authz === 'object' ? def.authz : null;
|
|
26
|
+
if (!a) return 'permissive';
|
|
27
|
+
return a.mode === 'restrictive' ? 'restrictive' : 'permissive (declared)';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** "v9", "v8 (template v9 available)" or "custom". */
|
|
31
|
+
export function templateLabel(hat) {
|
|
32
|
+
if (!hat.template_key) return 'custom';
|
|
33
|
+
const cur = hat.template_version;
|
|
34
|
+
const latest = hat.template_latest;
|
|
35
|
+
// The latest revision's author (only on `show`, which carries history):
|
|
36
|
+
// anything not 'template:*' means the org edited this built-in, which
|
|
37
|
+
// freezes it out of automatic template upgrades until reset-to-template.
|
|
38
|
+
const latestAuthor = Array.isArray(hat.revisions) && hat.revisions[0] ? hat.revisions[0].created_by : null;
|
|
39
|
+
const customised = typeof latestAuthor === 'string' && !latestAuthor.startsWith('template:');
|
|
40
|
+
if (latest && cur && cur < latest) return `v${cur} (template v${latest} available)`;
|
|
41
|
+
return customised ? `v${cur || '?'} (edited)` : `v${cur || '?'}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function toolCounts(def) {
|
|
45
|
+
const explicit = Array.isArray(def?.tools?.explicit) ? def.tools.explicit.length : 0;
|
|
46
|
+
const bundles = Array.isArray(def?.tools?.bundles) ? def.tools.bundles : [];
|
|
47
|
+
const bundled = bundles.reduce((n, b) => n + (Array.isArray(b?.tools) ? b.tools.length : 0), 0);
|
|
48
|
+
return { explicit, bundles: bundles.length, bundled };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function fetchList(client, orgId) {
|
|
52
|
+
const ep = resolveEndpoint('hat.list', { orgId });
|
|
53
|
+
return client.get(ep.path, { service: ep.service });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Resolve a hat argument (ULID or key) to its id using the org's hat list. */
|
|
57
|
+
export function findHat(hats, ref) {
|
|
58
|
+
const r = String(ref || '').trim();
|
|
59
|
+
return (hats || []).find(h => h.id === r) || (hats || []).find(h => h.key === r) || null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function registerHatCommands(program, ctx) {
|
|
63
|
+
const { client, auth } = ctx;
|
|
64
|
+
|
|
65
|
+
const hat = program
|
|
66
|
+
.command('hat')
|
|
67
|
+
.description('QuoxHat: read the org hats (tools, mode, revision, enforcement); reset a built-in to its template');
|
|
68
|
+
|
|
69
|
+
// ── hat list ────────────────────────────────────────────────────────────────
|
|
70
|
+
hat
|
|
71
|
+
.command('list')
|
|
72
|
+
.description('List the org hats with revision, template status, mode and tool counts')
|
|
73
|
+
.action(async () => {
|
|
74
|
+
auth.requireAuth();
|
|
75
|
+
const orgId = resolveOrgId(auth, program);
|
|
76
|
+
const data = await fetchList(client, orgId);
|
|
77
|
+
const hats = Array.isArray(data?.hats) ? data.hats : [];
|
|
78
|
+
const settings = data?.settings || {};
|
|
79
|
+
const format = program.opts().output;
|
|
80
|
+
|
|
81
|
+
if (format === 'json') {
|
|
82
|
+
console.log(JSON.stringify({ hats, settings }, null, 2));
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const rows = hats.map(h => {
|
|
87
|
+
const c = toolCounts(h.definition);
|
|
88
|
+
return {
|
|
89
|
+
key: h.key + (settings.default_hat_id === h.id ? ' *' : ''),
|
|
90
|
+
name: h.name,
|
|
91
|
+
rev: String(h.current_revision),
|
|
92
|
+
status: h.status,
|
|
93
|
+
template: templateLabel(h),
|
|
94
|
+
mode: authzMode(h.definition),
|
|
95
|
+
privileged: h.definition?.transitions?.privileged ? 'yes' : 'no',
|
|
96
|
+
tools: `${c.explicit} + ${c.bundled} in ${c.bundles} bundle(s)`,
|
|
97
|
+
id: h.id,
|
|
98
|
+
};
|
|
99
|
+
});
|
|
100
|
+
formatOutput({
|
|
101
|
+
rows,
|
|
102
|
+
columns: ['key', 'name', 'rev', 'status', 'template', 'mode', 'privileged', 'tools', 'id'],
|
|
103
|
+
headers: ['Key', 'Name', 'Rev', 'Status', 'Template', 'Mode', 'Privileged', 'Tools (explicit + bundled)', 'ID'],
|
|
104
|
+
}, { format, emptyMessage: 'No hats.' });
|
|
105
|
+
printInfo(`Enforcement: ${settings.enforcement || 'preview'} · selection: ${settings.selection_mode || 'manual'}${settings.default_hat_id ? ' · * = org default' : ' · org default: built-in Default'}`);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// ── hat show ────────────────────────────────────────────────────────────────
|
|
109
|
+
hat
|
|
110
|
+
.command('show <hat>')
|
|
111
|
+
.description('Show one hat (id or key): tools, bundles, mode, revision history, enforcement')
|
|
112
|
+
.option('--rev <n>', 'show a historical revision instead of the current one')
|
|
113
|
+
.action(async (ref, options) => {
|
|
114
|
+
auth.requireAuth();
|
|
115
|
+
const orgId = resolveOrgId(auth, program);
|
|
116
|
+
let rev;
|
|
117
|
+
if (options.rev !== undefined) {
|
|
118
|
+
rev = Number(options.rev);
|
|
119
|
+
if (!Number.isInteger(rev) || rev < 1) throw new Error(`--rev must be a positive integer (got '${options.rev}')`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const list = await fetchList(client, orgId);
|
|
123
|
+
const found = findHat(list?.hats, ref);
|
|
124
|
+
if (!found) {
|
|
125
|
+
const keys = (list?.hats || []).map(h => h.key).join(', ') || 'none';
|
|
126
|
+
throw new Error(`No hat '${ref}' in this org. Available: ${keys}`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const ep = resolveEndpoint('hat.get', { orgId, hatId: found.id });
|
|
130
|
+
const data = await client.get(ep.path, {
|
|
131
|
+
service: ep.service,
|
|
132
|
+
...(rev ? { query: { revision: rev } } : {}),
|
|
133
|
+
});
|
|
134
|
+
const h = data?.hat;
|
|
135
|
+
if (!h) throw new Error(`Hat '${ref}' not found`);
|
|
136
|
+
const enforcement = data.enforcement || list?.settings?.enforcement || 'preview';
|
|
137
|
+
|
|
138
|
+
if (program.opts().output === 'json') {
|
|
139
|
+
console.log(JSON.stringify({ hat: h, enforcement, settings: list?.settings || null }, null, 2));
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const def = h.definition || {};
|
|
144
|
+
const shown = h.revision_shown || h.current_revision;
|
|
145
|
+
const out = [];
|
|
146
|
+
out.push(chalk.bold(`${h.name} (${h.key})`) + chalk.dim(` ${h.id}`));
|
|
147
|
+
if (h.purpose) out.push(h.purpose);
|
|
148
|
+
out.push('');
|
|
149
|
+
out.push(`Revision: ${shown}${shown !== h.current_revision ? chalk.yellow(` (historical; current is ${h.current_revision})`) : ' (current)'}`);
|
|
150
|
+
out.push(`Digest: ${h.digest || '-'}`);
|
|
151
|
+
out.push(`Status: ${h.status}`);
|
|
152
|
+
out.push(`Template: ${templateLabel(h)}`);
|
|
153
|
+
out.push(`Mode: ${authzMode(def)}`);
|
|
154
|
+
out.push(`Enforcement: ${enforcement} (org)`);
|
|
155
|
+
out.push(`Transitions: confirm=${def.transitions?.confirm || 'none'} privileged=${def.transitions?.privileged ? 'yes' : 'no'}`);
|
|
156
|
+
out.push(`Agents: orchestrator=${def.agents?.orchestrator || '-'} specialists=${(def.agents?.specialists || []).join(', ') || 'none'}`);
|
|
157
|
+
out.push(`Budgets: initial_tool_target=${def.budgets?.initial_tool_target ?? '-'} max_manifest=${def.budgets?.max_manifest ?? '-'}`);
|
|
158
|
+
out.push('');
|
|
159
|
+
const explicit = def.tools?.explicit || [];
|
|
160
|
+
out.push(chalk.bold(`Explicit tools (${explicit.length}):`));
|
|
161
|
+
out.push(` ${explicit.join(', ') || 'none'}`);
|
|
162
|
+
const bundles = def.tools?.bundles || [];
|
|
163
|
+
out.push(chalk.bold(`Bundles (${bundles.length}, loaded per conversation with hat_load_bundle):`));
|
|
164
|
+
for (const b of bundles) out.push(` ${b.key}: ${(b.tools || []).join(', ')}`);
|
|
165
|
+
if (!bundles.length) out.push(' none');
|
|
166
|
+
const fams = def.tools?.connector_families || [];
|
|
167
|
+
out.push(chalk.bold('Connector families:') + ` ${fams.join(', ') || 'none'}`);
|
|
168
|
+
if (def.authz && typeof def.authz === 'object') {
|
|
169
|
+
if (Array.isArray(def.authz.allow)) out.push(`Authz allow: ${def.authz.allow.join(', ')}`);
|
|
170
|
+
if (Array.isArray(def.authz.deny)) out.push(`Authz deny: ${def.authz.deny.join(', ')}`);
|
|
171
|
+
}
|
|
172
|
+
const revisions = Array.isArray(h.revisions) ? h.revisions : [];
|
|
173
|
+
if (revisions.length) {
|
|
174
|
+
out.push('');
|
|
175
|
+
out.push(chalk.bold('Revisions:'));
|
|
176
|
+
for (const r of revisions) {
|
|
177
|
+
out.push(` ${String(r.revision).padStart(3)} ${r.created_at || ''} ${r.created_by || ''} ${r.change_reason || ''}`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
console.log(out.join('\n'));
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// ── hat reset ───────────────────────────────────────────────────────────────
|
|
184
|
+
hat
|
|
185
|
+
.command('reset <hat>')
|
|
186
|
+
.description('Reset an org-edited built-in hat to its current template (drops the org edits; audited)')
|
|
187
|
+
.option('--yes', 'confirm: the org edits on this hat are replaced by the template')
|
|
188
|
+
.action(async (ref, options) => {
|
|
189
|
+
auth.requireAuth();
|
|
190
|
+
if (!options.yes) throw new Error(`Resetting '${ref}' replaces its org edits with the template. Re-run with --yes to confirm.`);
|
|
191
|
+
const orgId = resolveOrgId(auth, program);
|
|
192
|
+
const list = await fetchList(client, orgId);
|
|
193
|
+
const found = findHat(list?.hats, ref);
|
|
194
|
+
if (!found) {
|
|
195
|
+
const keys = (list?.hats || []).map(h => h.key).join(', ') || 'none';
|
|
196
|
+
throw new Error(`No hat '${ref}' in this org. Available: ${keys}`);
|
|
197
|
+
}
|
|
198
|
+
const ep = resolveEndpoint('hat.reset', { orgId, hatId: found.id });
|
|
199
|
+
const data = await client.post(ep.path, { service: ep.service, body: {} });
|
|
200
|
+
const h = data?.hat;
|
|
201
|
+
if (!h) throw new Error(`Reset of '${ref}' returned no hat`);
|
|
202
|
+
if (program.opts().output === 'json') { console.log(JSON.stringify({ hat: h }, null, 2)); return; }
|
|
203
|
+
console.log(` Ⓠ QUOX │ hat ${h.key} reset to template │ rev ${h.current_revision} · ${templateLabel(h)} ✔`);
|
|
204
|
+
});
|
|
205
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helpers for command modules
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { QuoxValidationError } from '../core/errors.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Resolve the active org ID from --org flag or stored auth.
|
|
9
|
+
* Throws if no org context is available.
|
|
10
|
+
*/
|
|
11
|
+
export function resolveOrgId(auth, program) {
|
|
12
|
+
const orgId = auth.getOrgId(program.opts().org);
|
|
13
|
+
if (!orgId) {
|
|
14
|
+
// Ask about AUTH before blaming the org, because a logged-out user has no
|
|
15
|
+
// org for the obvious reason, and telling them to switch org sends them
|
|
16
|
+
// down a path that cannot work.
|
|
17
|
+
//
|
|
18
|
+
// 2026-08-18: a new-user setup audit ran `quox slot create audit-demo` on
|
|
19
|
+
// a shell with no token. It answered "No active org. Use --org <id> or
|
|
20
|
+
// `quox org switch <id>` first." Following that instruction gave a
|
|
21
|
+
// DIFFERENT error, "No authentication token provided ... Run `quox login`".
|
|
22
|
+
// Two errors, two dead ends, and the real first step named only in the
|
|
23
|
+
// second one. The owner's standing complaint about this bundle is that it
|
|
24
|
+
// is not "easy as pie" to set up; this was the very first command in that
|
|
25
|
+
// path, and it misdirected.
|
|
26
|
+
//
|
|
27
|
+
// Every command that needs an org goes through here, so the fix lands once
|
|
28
|
+
// for all of them.
|
|
29
|
+
auth.requireAuth();
|
|
30
|
+
|
|
31
|
+
// requireAuth() passing does NOT mean a USER is logged in. isLoggedIn()
|
|
32
|
+
// also accepts an ambient QUOX_SERVICE_KEY as the automation fallback, and
|
|
33
|
+
// a service key carries no organisation — so on any box that exports one
|
|
34
|
+
// globally (every dev box here does) a logged-out user sails past the auth
|
|
35
|
+
// check and lands on "No active org", which sends them to `quox org
|
|
36
|
+
// switch` when the real first step is `quox login`.
|
|
37
|
+
//
|
|
38
|
+
// 2026-08-19: reproduced exactly that with `quox slot list --profile local`
|
|
39
|
+
// on a shell with QUOX_SERVICE_KEY exported. This is the SAME misdirection
|
|
40
|
+
// the 2026-08-18 note above set out to kill, arriving through the one door
|
|
41
|
+
// that check cannot see: the fix asked "are you authenticated?" when the
|
|
42
|
+
// question that matters is "authenticated AS WHOM?".
|
|
43
|
+
const hasUserToken = Boolean(auth.getTokens?.()?.access_token);
|
|
44
|
+
if (!hasUserToken) {
|
|
45
|
+
throw new Error(
|
|
46
|
+
'Authenticated with a service key, which has no organisation. '
|
|
47
|
+
+ 'Run `quox login` to act as a user, or pass --org <id> to name the org explicitly.'
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
throw new Error('No active org. Use --org <id>, or pick one with `quox org list` then `quox org switch <id>`.');
|
|
52
|
+
}
|
|
53
|
+
return orgId;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Safely parse a JSON string from CLI input.
|
|
58
|
+
* Throws QuoxValidationError with a clear message on invalid JSON.
|
|
59
|
+
*/
|
|
60
|
+
export function parseJSON(value, label = 'JSON') {
|
|
61
|
+
try {
|
|
62
|
+
return JSON.parse(value);
|
|
63
|
+
} catch {
|
|
64
|
+
throw new QuoxValidationError(`Invalid ${label}: not valid JSON. Check your input and try again.`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Build a query object from common list options.
|
|
70
|
+
*/
|
|
71
|
+
export function buildListQuery(options) {
|
|
72
|
+
const query = {};
|
|
73
|
+
if (options.limit) query.limit = options.limit;
|
|
74
|
+
if (options.offset) query.offset = options.offset;
|
|
75
|
+
if (options.search) query.search = options.search;
|
|
76
|
+
if (options.status) query.status = options.status;
|
|
77
|
+
if (options.sort) query.sort = options.sort;
|
|
78
|
+
return query;
|
|
79
|
+
}
|