quoxcli 0.2.0 → 0.3.1
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 +1 -1
- package/README.md +2 -0
- package/bin/quox.js +99 -126
- package/lib/api/endpoints/admin.js +26 -0
- package/lib/api/endpoints/agent-schedules.js +26 -0
- package/lib/api/endpoints/agents.js +7 -0
- package/lib/api/endpoints/assistants.js +12 -0
- package/lib/api/endpoints/away.js +15 -0
- package/lib/api/endpoints/chat.js +7 -0
- package/lib/api/endpoints/contactsq.js +2 -0
- package/lib/api/endpoints/conversations.js +14 -0
- package/lib/api/endpoints/executions.js +13 -0
- package/lib/api/endpoints/fetchAllowlist.js +27 -0
- package/lib/api/endpoints/files.js +14 -0
- package/lib/api/endpoints/hat.js +30 -0
- package/lib/api/endpoints/inbox.js +20 -0
- package/lib/api/endpoints/index.js +14 -0
- package/lib/api/endpoints/memory.js +19 -0
- package/lib/api/endpoints/mind.js +13 -0
- package/lib/api/endpoints/mon.js +6 -0
- package/lib/api/endpoints/objectives.js +6 -0
- package/lib/api/endpoints/plugins.js +18 -0
- package/lib/api/endpoints/presence.js +33 -0
- package/lib/api/endpoints/quoxplan.js +7 -0
- package/lib/api/endpoints/quoxseo.js +6 -0
- package/lib/api/endpoints/router.js +17 -0
- package/lib/api/endpoints/safety.js +7 -0
- package/lib/api/endpoints/skills.js +20 -0
- package/lib/api/endpoints/stepup.js +20 -0
- package/lib/api/endpoints/teams.js +2 -2
- package/lib/api/endpoints/triggers.js +26 -0
- package/lib/api/endpoints/v1.js +86 -0
- package/lib/api/endpoints/ward.js +7 -0
- package/lib/api/endpoints/workflows.js +7 -0
- package/lib/api/endpoints/x.js +8 -0
- package/lib/commands/actions.js +61 -0
- package/lib/commands/admin.js +16 -0
- package/lib/commands/agent.js +163 -27
- package/lib/commands/agentAttach.js +94 -6
- package/lib/commands/approvals.js +73 -1
- package/lib/commands/aside.js +24 -0
- package/lib/commands/ask.js +144 -0
- package/lib/commands/assistant.js +59 -4
- package/lib/commands/auth.js +8 -2
- package/lib/commands/backup.js +8 -4
- package/lib/commands/brain.js +105 -14
- package/lib/commands/build.js +19 -5
- package/lib/commands/chat.js +57 -2
- package/lib/commands/claude.js +34 -8
- package/lib/commands/contacts.js +49 -0
- package/lib/commands/conversation.js +35 -4
- package/lib/commands/dashboard.js +43 -0
- package/lib/commands/department.js +28 -7
- package/lib/commands/devResident.js +15 -3
- package/lib/commands/devSupervisor.js +13 -1
- package/lib/commands/discord.js +75 -39
- package/lib/commands/done.js +98 -28
- package/lib/commands/engagement.js +17 -6
- package/lib/commands/fetchAllowlist.js +71 -0
- package/lib/commands/file.js +38 -1
- package/lib/commands/fleet.js +311 -4
- package/lib/commands/governance.js +12 -3
- package/lib/commands/gripe.js +44 -13
- package/lib/commands/hat.js +178 -9
- package/lib/commands/helpers.js +18 -1
- package/lib/commands/hyperion.js +58 -0
- package/lib/commands/inbox.js +74 -23
- package/lib/commands/licence.js +14 -155
- package/lib/commands/lucid.js +10 -4
- package/lib/commands/memory.js +85 -8
- package/lib/commands/mind.js +71 -0
- package/lib/commands/mon.js +69 -2
- package/lib/commands/monitoring.js +1 -1
- package/lib/commands/motion.js +2 -2
- package/lib/commands/nucleus.js +48 -17
- package/lib/commands/objective.js +11 -0
- package/lib/commands/offload.js +89 -0
- package/lib/commands/onboard.js +2 -1
- package/lib/commands/org.js +11 -1
- package/lib/commands/peek.js +28 -0
- package/lib/commands/plan.js +38 -0
- package/lib/commands/plugin.js +110 -0
- package/lib/commands/presence.js +89 -0
- package/lib/commands/provider.js +159 -0
- package/lib/commands/qlarity.js +19 -3
- package/lib/commands/quoxchat.js +13 -4
- package/lib/commands/quoxseo.js +38 -0
- package/lib/commands/reach.js +2 -1
- package/lib/commands/router.js +77 -0
- package/lib/commands/rows.js +358 -0
- package/lib/commands/run.js +64 -9
- package/lib/commands/server.js +292 -0
- package/lib/commands/setup.js +33 -7
- package/lib/commands/skills.js +48 -5
- package/lib/commands/slot.js +260 -7
- package/lib/commands/status.js +1 -1
- package/lib/commands/team.js +3 -1
- package/lib/commands/tips.js +23 -0
- package/lib/commands/tool.js +133 -45
- package/lib/commands/trace.js +12 -7
- package/lib/commands/trigger.js +95 -0
- package/lib/commands/update.js +46 -0
- package/lib/commands/vault.js +31 -8
- package/lib/commands/vm.js +237 -0
- package/lib/commands/voice.js +34 -3
- package/lib/commands/volt.js +45 -3
- package/lib/commands/walkthrough.js +32 -0
- package/lib/commands/watch.js +73 -21
- package/lib/commands/welcome.js +14 -0
- package/lib/commands/workflow.js +189 -32
- package/lib/commands/x.js +13 -4
- package/lib/core/actingAs.js +2 -2
- package/lib/core/activeSummary.js +186 -0
- package/lib/core/aside.js +54 -0
- package/lib/core/askDecision.js +60 -0
- package/lib/core/auth.js +164 -13
- package/lib/core/callerSource.js +1 -1
- package/lib/core/cliBuild.js +62 -0
- package/lib/core/client.js +144 -35
- package/lib/core/commandRegistrars.js +121 -0
- package/lib/core/config.js +142 -24
- package/lib/core/customHats.js +168 -0
- package/lib/core/doneEngineFetch.js +8 -0
- package/lib/core/errors.js +329 -20
- package/lib/core/global-flags.js +105 -5
- package/lib/core/hyperion.js +102 -0
- package/lib/core/inboxResolution.js +65 -0
- package/lib/core/lazyCommands.js +4 -1
- package/lib/core/offload.js +504 -0
- package/lib/core/org-override.js +1 -1
- package/lib/core/orgResolve.js +70 -0
- package/lib/core/output.js +4 -4
- package/lib/core/peek.js +126 -0
- package/lib/core/profileUsers.js +51 -0
- package/lib/core/promptCancel.js +18 -0
- package/lib/core/repoPath.js +11 -0
- package/lib/core/residentSeed.js +3 -1
- package/lib/core/rootOptions.js +39 -0
- package/lib/core/router.js +168 -0
- package/lib/core/selfUpdate.js +188 -0
- package/lib/core/sessionsOverview.js +64 -0
- package/lib/core/slotKeeper.js +248 -3
- package/lib/core/slotWake.js +120 -0
- package/lib/core/supervisorEngine.js +71 -2
- package/lib/core/version.js +1 -1
- package/lib/core/vm.js +361 -0
- package/lib/core/vmTui.js +86 -0
- package/lib/core/walkthrough.js +35 -0
- package/lib/hyperion/deck.js +29 -0
- package/lib/lucid/LucidCard.jsx +68 -0
- package/lib/lucid/LucidIntro.jsx +45 -58
- package/lib/lucid/cards.js +86 -0
- package/lib/lucid/markdown.js +81 -0
- package/lib/lucid/motion.js +56 -0
- package/lib/quoxon/activity.js +14 -2
- package/lib/quoxon/aocl.js +117 -0
- package/lib/quoxon/api.js +504 -25
- package/lib/quoxon/app.js +3000 -230
- package/lib/quoxon/approvalFacts.js +94 -0
- package/lib/quoxon/approvalsView.js +325 -0
- package/lib/quoxon/asideView.js +40 -0
- package/lib/quoxon/ask.js +121 -0
- package/lib/quoxon/away.js +52 -0
- package/lib/quoxon/bang.js +224 -0
- package/lib/quoxon/bulkRisk.js +17 -0
- package/lib/quoxon/capabilities.js +13 -6
- package/lib/quoxon/cells.js +53 -0
- package/lib/quoxon/commandShort.js +112 -0
- package/lib/quoxon/commands.js +363 -38
- package/lib/quoxon/connect.js +210 -23
- package/lib/quoxon/connectCard.js +19 -2
- package/lib/quoxon/consent.js +86 -0
- package/lib/quoxon/copy.js +4 -3
- package/lib/quoxon/countdownLine.js +11 -0
- package/lib/quoxon/demo.autoplay.js +29 -0
- package/lib/quoxon/demo.fixtures.js +1 -1
- package/lib/quoxon/demo.js +44 -3
- package/lib/quoxon/devObserver.js +20 -0
- package/lib/quoxon/dividerAocl.js +191 -0
- package/lib/quoxon/dividerRule.js +57 -0
- package/lib/quoxon/dragonModes.js +150 -0
- package/lib/quoxon/dragonView.js +41 -0
- package/lib/quoxon/earlyInput.js +40 -0
- package/lib/quoxon/echo.js +15 -0
- package/lib/quoxon/egg.js +2 -2
- package/lib/quoxon/events.js +244 -25
- package/lib/quoxon/feedback.js +50 -0
- package/lib/quoxon/gallery.js +21 -7
- package/lib/quoxon/handoffOffer.js +78 -0
- package/lib/quoxon/hatSwitch.js +37 -0
- package/lib/quoxon/headless.js +161 -0
- package/lib/quoxon/hostUrls.js +41 -0
- package/lib/quoxon/indicator.js +6 -2
- package/lib/quoxon/intent.js +12 -1
- package/lib/quoxon/keys.js +49 -6
- package/lib/quoxon/launcher.js +103 -0
- package/lib/quoxon/main.js +92 -160
- package/lib/quoxon/markdown.js +45 -21
- package/lib/quoxon/markdownSettle.js +19 -0
- package/lib/quoxon/matrixDomains.json +46 -0
- package/lib/quoxon/mentions.js +90 -0
- package/lib/quoxon/mind.js +103 -0
- package/lib/quoxon/mode.js +46 -0
- package/lib/quoxon/model.js +132 -26
- package/lib/quoxon/modelPrefs.js +345 -0
- package/lib/quoxon/motion/clock.js +3 -2
- package/lib/quoxon/motion/level.js +91 -0
- package/lib/quoxon/mouse.js +211 -0
- package/lib/quoxon/noai.js +57 -0
- package/lib/quoxon/objects/away.js +72 -0
- package/lib/quoxon/objects/fetch.js +17 -5
- package/lib/quoxon/objects/fleet.js +149 -0
- package/lib/quoxon/objects/health.js +186 -0
- package/lib/quoxon/objects/index.js +24 -3
- package/lib/quoxon/objects/mirror.js +137 -0
- package/lib/quoxon/objects/peek.js +17 -0
- package/lib/quoxon/objects/prove.js +130 -0
- package/lib/quoxon/objects/replay.js +125 -0
- package/lib/quoxon/objects/sitrep.js +58 -0
- package/lib/quoxon/objects/view.js +11 -0
- package/lib/quoxon/onboarding/copy.js +32 -4
- package/lib/quoxon/onboarding/credential.js +17 -4
- package/lib/quoxon/onboarding/dockerOffer.js +45 -0
- package/lib/quoxon/onboarding/failureCause.js +33 -0
- package/lib/quoxon/onboarding/finish.js +15 -0
- package/lib/quoxon/onboarding/index.js +34 -7
- package/lib/quoxon/onboarding/installRunner.js +64 -5
- package/lib/quoxon/onboarding/preflight.js +17 -3
- package/lib/quoxon/onboarding/probe.js +65 -0
- package/lib/quoxon/onboarding/view.js +76 -19
- package/lib/quoxon/orgLabel.js +10 -0
- package/lib/quoxon/orgSwitch.js +33 -0
- package/lib/quoxon/ownBroadcast.js +22 -0
- package/lib/quoxon/palette.js +43 -0
- package/lib/quoxon/print.js +9 -5
- package/lib/quoxon/profileSwitch.js +53 -0
- package/lib/quoxon/promptHistory.js +41 -0
- package/lib/quoxon/providers.js +48 -0
- package/lib/quoxon/qart.js +20 -51
- package/lib/quoxon/qdragon.js +410 -0
- package/lib/quoxon/qdragonFly.js +61 -0
- package/lib/quoxon/qdragonScenes.js +282 -0
- package/lib/quoxon/qeyes.js +213 -0
- package/lib/quoxon/qmotion.js +58 -37
- package/lib/quoxon/render.js +638 -119
- package/lib/quoxon/resend.js +72 -0
- package/lib/quoxon/roster.js +95 -9
- package/lib/quoxon/route.js +6 -5
- package/lib/quoxon/rows.js +711 -0
- package/lib/quoxon/rowsStore.js +80 -0
- package/lib/quoxon/screenContext.js +150 -0
- package/lib/quoxon/screenFrames.js +254 -0
- package/lib/quoxon/session.js +172 -2
- package/lib/quoxon/sessionDead.js +17 -0
- package/lib/quoxon/signin.js +125 -0
- package/lib/quoxon/slash/agents.js +112 -0
- package/lib/quoxon/slash/argSources.js +65 -0
- package/lib/quoxon/slash/args.js +65 -0
- package/lib/quoxon/slash/automate.js +89 -0
- package/lib/quoxon/slash/broadcast.js +140 -0
- package/lib/quoxon/slash/broadcastRun.js +108 -0
- package/lib/quoxon/slash/card.js +30 -0
- package/lib/quoxon/slash/challenge.js +114 -0
- package/lib/quoxon/slash/cliCommand.js +53 -0
- package/lib/quoxon/slash/cliLeaves.json +948 -0
- package/lib/quoxon/slash/compact.js +1 -1
- package/lib/quoxon/slash/confirm.js +30 -0
- package/lib/quoxon/slash/crystallize.js +69 -0
- package/lib/quoxon/slash/dashboard.js +136 -0
- package/lib/quoxon/slash/draft.js +54 -0
- package/lib/quoxon/slash/hatWalk.js +206 -0
- package/lib/quoxon/slash/inverses.js +77 -0
- package/lib/quoxon/slash/killer.js +24 -0
- package/lib/quoxon/slash/killerApi.js +16 -0
- package/lib/quoxon/slash/ledger.js +13 -8
- package/lib/quoxon/slash/listView.js +3 -1
- package/lib/quoxon/slash/modelCmd.js +261 -0
- package/lib/quoxon/slash/palette.js +21 -7
- package/lib/quoxon/slash/paletteController.js +117 -7
- package/lib/quoxon/slash/panels.js +21 -6
- package/lib/quoxon/slash/picker.js +20 -2
- package/lib/quoxon/slash/restart.js +71 -13
- package/lib/quoxon/slash/rowCmd.js +157 -0
- package/lib/quoxon/slash/rowWalk.js +175 -0
- package/lib/quoxon/slash/settings.js +16 -6
- package/lib/quoxon/slash/spawn.js +177 -0
- package/lib/quoxon/slash/stepUp.js +274 -0
- package/lib/quoxon/slash/stepUpCard.js +60 -0
- package/lib/quoxon/slash/stepUpCmds.js +78 -0
- package/lib/quoxon/slash/undoApi.js +38 -0
- package/lib/quoxon/slash/undoCmd.js +49 -0
- package/lib/quoxon/slash/undoWish.js +15 -0
- package/lib/quoxon/slash/watch.js +164 -0
- package/lib/quoxon/slash/wish.js +23 -0
- package/lib/quoxon/slash/wishCmd.js +32 -0
- package/lib/quoxon/slotChat.js +300 -0
- package/lib/quoxon/slotFormat.js +195 -0
- package/lib/quoxon/slotRun.js +131 -0
- package/lib/quoxon/sound/mapping.js +2 -1
- package/lib/quoxon/stray.js +22 -0
- package/lib/quoxon/suggestCommand.js +59 -0
- package/lib/quoxon/term.js +46 -11
- package/lib/quoxon/textBuffer.js +176 -2
- package/lib/quoxon/theme.js +58 -25
- package/lib/quoxon/tips.js +433 -0
- package/lib/quoxon/toolError.js +41 -0
- package/lib/quoxon/transcript.js +42 -4
- package/lib/quoxon/web/qface.js +18 -10
- package/lib/quoxon/welcome.js +1 -1
- package/lib/quoxon/whatsnew.js +136 -0
- package/lib/quoxon/whatsnew.jsonl +89 -0
- package/lib/quoxon/windowOrg.js +113 -0
- package/lib/server/diag.js +129 -0
- package/lib/server/doctor.js +71 -0
- package/lib/server/install.js +149 -0
- package/lib/server/pluginEnable.js +161 -0
- package/lib/server/skew.js +56 -0
- package/lib/server/snapshot.js +131 -0
- package/lib/server/status.js +128 -0
- package/lib/server/sys.js +65 -0
- package/lib/server/uninstall.js +63 -0
- package/lib/server/update.js +234 -0
- package/lib/toolOutputShape.js +315 -0
- package/lib/tui/views/HyperionDeck.jsx +79 -0
- package/lib/tui/views/LucidDeck.jsx +209 -217
- package/package.json +4 -1
- package/lib/core/websiteSession.js +0 -148
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `quox ask`: a session puts a fork in front of a human through the Approvals inbox (one inbox item, kind decision).
|
|
3
|
+
* Who is asking, what, why, the options, the recommendation and the expiry travel with it, so ONE HITL session
|
|
4
|
+
* can answer for twenty terminals. Not an approval of an action: no step-up PIN unless --high-risk.
|
|
5
|
+
*/
|
|
6
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
7
|
+
import { execFileSync } from 'node:child_process';
|
|
8
|
+
import { homedir } from 'node:os';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
import { resolveEndpoint } from '../api/endpoints.js';
|
|
11
|
+
import { resolveOrgId } from './helpers.js';
|
|
12
|
+
import { readPresence, approvalPresenceFields } from './presence.js';
|
|
13
|
+
import { toRepoRelative, LOOSENING_HINT } from '../core/repoPath.js';
|
|
14
|
+
import { parseOptions, parseDuration, YN_OPTIONS, waitForDecision, EXIT_NO_ANSWER } from '../core/askDecision.js';
|
|
15
|
+
|
|
16
|
+
function heldSlotFor(session) {
|
|
17
|
+
try {
|
|
18
|
+
const all = JSON.parse(readFileSync(join(homedir(), '.quox', 'held-slots.json'), 'utf8'));
|
|
19
|
+
const want = String(session || '').toLowerCase();
|
|
20
|
+
return Object.keys(all).find((k) => String(all[k]?.session || '').toLowerCase() === want) || null;
|
|
21
|
+
} catch { return null; }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function registerAskCommands(program, { client, auth }) {
|
|
25
|
+
const err = (s) => process.stderr.write(`${s}\n`);
|
|
26
|
+
|
|
27
|
+
async function waitAndPrint(orgId, id, { timeoutSec = null } = {}) {
|
|
28
|
+
const r = await waitForDecision({
|
|
29
|
+
poll: (w) => { const ep = resolveEndpoint('inbox.decision.get', { orgId, id }); return client.get(ep.path, { service: ep.service, query: { wait: w }, timeout: (w + 15) * 1000, retries: 0 }); },
|
|
30
|
+
log: err,
|
|
31
|
+
maxWaitMs: timeoutSec != null ? timeoutSec * 1000 : null,
|
|
32
|
+
});
|
|
33
|
+
if (r.stdout) console.log(r.stdout);
|
|
34
|
+
process.exitCode = r.code;
|
|
35
|
+
return r;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const ask = program
|
|
39
|
+
.command('ask')
|
|
40
|
+
.description('Ask a human a question through the Approvals inbox: who asks, what, why, the options. Use this instead of asking in your own terminal')
|
|
41
|
+
.option('--session <name>', 'which session is asking (default: $QUOX_SESSION)')
|
|
42
|
+
.option('--title <text>', 'what you are asking, one line')
|
|
43
|
+
.option('--why <text>', 'a simple why: what the human needs to know to decide')
|
|
44
|
+
.option('--justify <text>', 'for a loosening (a check made easier to pass): why it is safe, in plain words; the card shows it on its JUSTIFY line and defaults to No without one')
|
|
45
|
+
.option('--options <list>', 'the choices: "a: Ship now|b: Wait for review|c: Drop it"')
|
|
46
|
+
.option('--yn', 'shorthand for a yes/no question (options y and n)')
|
|
47
|
+
.option('--default <key>', 'the option you would take if nobody answers (shown, never applied for you)')
|
|
48
|
+
.option('--recommend <key>', 'the option you recommend')
|
|
49
|
+
.option('--expires <dur>', 'how long it stays open: 30m, 2h, 1d (default 24h, max 30d)')
|
|
50
|
+
.option('--slot <name>', 'your slot, so the answer is also posted there (default: found from held slots)')
|
|
51
|
+
.option('--agent <name>', 'the agent asking, when it is an agent rather than a session')
|
|
52
|
+
.option('--high-risk', 'the answer needs the human step-up PIN')
|
|
53
|
+
.option('--wait', 'block until answered; prints the chosen option key (and any note) on stdout, exits 2 on expiry or deny')
|
|
54
|
+
.option('--timeout <dur>', 'with --wait: stop waiting after this long (the question stays open)')
|
|
55
|
+
.action((options) => fileAsk(options));
|
|
56
|
+
|
|
57
|
+
async function fileAsk(options) {
|
|
58
|
+
auth.requireAuth();
|
|
59
|
+
const orgId = resolveOrgId(auth, program);
|
|
60
|
+
const session = options.session || process.env.QUOX_SESSION;
|
|
61
|
+
if (!session) throw new Error('say which session is asking: --session <NAME>');
|
|
62
|
+
if (!options.title) throw new Error('say what you are asking: --title "<what>"');
|
|
63
|
+
if (options.yn && options.options) throw new Error('use --yn or --options, not both');
|
|
64
|
+
if (!options.yn && !options.options) throw new Error('give the choices: --options "a: Ship now|b: Wait" (or --yn)');
|
|
65
|
+
// QUOXON 2026-10-17: a high-risk ask (a loosening, anything the owner must PIN) carries a plain justification
|
|
66
|
+
// 2026-10-19: three PIN approvals were wasted on loosening asks filed without --high-risk or with unusable paths
|
|
67
|
+
if (!options.highRisk && /^\s*approve\s+(?:loosening|test)/i.test(options.title)) { err(`A test-edit approval must go through the loosening verb, which sets --high-risk and repo-relative paths for you:\n ${LOOSENING_HINT}`); process.exitCode = 2; return; }
|
|
68
|
+
if (options.highRisk && !String(options.justify || '').trim()) { err('High-risk asks need --justify "<2-3 plain lines the owner can judge>"'); process.exitCode = 2; return; }
|
|
69
|
+
const opts = options.yn ? YN_OPTIONS : parseOptions(options.options);
|
|
70
|
+
const body = {
|
|
71
|
+
session, title: options.title, why: options.why, options: opts,
|
|
72
|
+
...(options.justify ? { justification: options.justify } : {}),
|
|
73
|
+
default: options.default, recommend: options.recommend,
|
|
74
|
+
...(options.expires ? { expires_in_seconds: parseDuration(options.expires) } : {}),
|
|
75
|
+
high_risk: Boolean(options.highRisk),
|
|
76
|
+
slot: options.slot || heldSlotFor(session) || undefined,
|
|
77
|
+
profile: program.opts().profile || process.env.QUOX_PROFILE || undefined,
|
|
78
|
+
agent: options.agent,
|
|
79
|
+
};
|
|
80
|
+
const ep = resolveEndpoint('inbox.decision.create', { orgId });
|
|
81
|
+
const data = await client.post(ep.path, { service: ep.service, body });
|
|
82
|
+
// PRESENCE (owner 2026-10-16): say now whether the human is away, so this session can work on
|
|
83
|
+
// something else instead of waiting. The question is filed and queued exactly as before.
|
|
84
|
+
const presence = approvalPresenceFields(await readPresence(client, orgId));
|
|
85
|
+
const d = { ...data.decision, ...presence };
|
|
86
|
+
if (program.opts().output === 'json' && !options.wait) { console.log(JSON.stringify(d, null, 2)); return; }
|
|
87
|
+
err(`asked: ${d.id} (${session}: ${d.title}); it shows in /approvals${d.expires_at ? `, open until ${d.expires_at}` : ''}`);
|
|
88
|
+
if (presence.owner_away) err(`${presence.owner_away_notice}. Expect a wait: work on something else meanwhile.`);
|
|
89
|
+
if (!options.wait) { console.log(d.id); return; }
|
|
90
|
+
await waitAndPrint(orgId, d.id, { timeoutSec: options.timeout ? parseDuration(options.timeout) : null });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
ask
|
|
94
|
+
.command('loosening')
|
|
95
|
+
.description('Ask a human to approve edits to test files (a check made easier to pass). Files a yes/no, high-risk (PIN) ask with repo-relative paths')
|
|
96
|
+
.option('--paths <files...>', 'every test file the commit edits (absolute, worktree or repo-relative: all are normalised)')
|
|
97
|
+
.option('--justify <text>', 'why it is safe, in plain words (required)')
|
|
98
|
+
.option('--why <text>', 'detail: the findings, before and after lines')
|
|
99
|
+
.option('--session <name>', 'which session is asking (default: $QUOX_SESSION)')
|
|
100
|
+
.option('--slot <name>', 'your slot')
|
|
101
|
+
.option('--expires <dur>', 'how long it stays open')
|
|
102
|
+
.option('--wait', 'block until answered')
|
|
103
|
+
.option('--timeout <dur>', 'with --wait: stop waiting after this long')
|
|
104
|
+
.action(async (subOptions) => {
|
|
105
|
+
// commander lets the parent `ask` swallow flags it shares with this verb (--justify, --why, --session...), so merge both
|
|
106
|
+
const options = { ...ask.opts(), ...Object.fromEntries(Object.entries(subOptions).filter(([, v]) => v !== undefined)) };
|
|
107
|
+
if (!String(options.justify || '').trim()) { err('say why it is safe: --justify "<plain words>"'); process.exitCode = 2; return; }
|
|
108
|
+
const raw = [].concat(options.paths || []).flatMap((x) => String(x).split(',')).map((x) => x.trim()).filter(Boolean);
|
|
109
|
+
let root;
|
|
110
|
+
try { root = execFileSync('git', ['rev-parse', '--show-toplevel'], { encoding: 'utf8' }).trim(); } catch { root = process.cwd(); }
|
|
111
|
+
const paths = [...new Set(raw.map(toRepoRelative))];
|
|
112
|
+
const missing = paths.filter((p) => !p || !existsSync(join(root, p)));
|
|
113
|
+
if (!paths.length || missing.length) { err(`not found in this repo (${root}): ${missing.join(', ') || '(no paths)'}. Run from the worktree and name files that exist.`); process.exitCode = 2; return; }
|
|
114
|
+
// 2026-10-19: the server truncates long titles, and the attestation names only paths found in title+body,
|
|
115
|
+
// so a 7-file ask covered 3. Every path also goes in the body.
|
|
116
|
+
const files = `Files (${paths.length}):\n${paths.map((p) => `- ${p}`).join('\n')}`;
|
|
117
|
+
await fileAsk({ ...options, title: `Approve test edits: ${paths.join(' ')}`, why: options.why ? `${files}\n\n${options.why}` : files, yn: true, highRisk: true, justify: options.justify });
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
ask
|
|
121
|
+
.command('status')
|
|
122
|
+
.description('Show where a question stands: pending, answered, denied or expired, with the answer')
|
|
123
|
+
.argument('<id>', 'the id `quox ask` printed')
|
|
124
|
+
.action(async (id) => {
|
|
125
|
+
auth.requireAuth();
|
|
126
|
+
const orgId = resolveOrgId(auth, program);
|
|
127
|
+
const ep = resolveEndpoint('inbox.decision.get', { orgId, id });
|
|
128
|
+
const d = (await client.get(ep.path, { service: ep.service })).decision;
|
|
129
|
+
if (program.opts().output === 'json') { console.log(JSON.stringify(d, null, 2)); return; }
|
|
130
|
+
console.log(`${d.state}${d.answer?.option ? ` ${d.answer.option}` : ''}${d.answer?.note ? ` (${d.answer.note})` : ''}`);
|
|
131
|
+
if (d.state === 'expired' || d.state === 'denied') process.exitCode = EXIT_NO_ANSWER;
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
ask
|
|
135
|
+
.command('wait')
|
|
136
|
+
.description('Block until a question is answered; prints the chosen option key (and note), exits 2 on expiry or deny')
|
|
137
|
+
.argument('<id>', 'the id `quox ask` printed')
|
|
138
|
+
.option('--timeout <dur>', 'stop waiting after this long (the question stays open)')
|
|
139
|
+
.action(async (id, options) => {
|
|
140
|
+
auth.requireAuth();
|
|
141
|
+
const orgId = resolveOrgId(auth, program);
|
|
142
|
+
await waitAndPrint(orgId, id, { timeoutSec: options.timeout ? parseDuration(options.timeout) : null });
|
|
143
|
+
});
|
|
144
|
+
}
|
|
@@ -5,6 +5,10 @@
|
|
|
5
5
|
import { formatOutput, formatDetail, printSuccess } from '../core/output.js';
|
|
6
6
|
import { resolveOrgId } from './helpers.js';
|
|
7
7
|
import { resolveEndpoint } from '../api/endpoints.js';
|
|
8
|
+
import { QuoxError } from '../core/errors.js';
|
|
9
|
+
|
|
10
|
+
// #1049: auth routes/assistants.js DEPLOYMENT_MODES (the server is the authority).
|
|
11
|
+
const DEPLOY_MODES = ['private', 'shared', 'public'];
|
|
8
12
|
|
|
9
13
|
export function registerAssistantCommands(program, ctx) {
|
|
10
14
|
const { client, auth } = ctx;
|
|
@@ -84,20 +88,71 @@ export function registerAssistantCommands(program, ctx) {
|
|
|
84
88
|
printSuccess('Assistant deleted.');
|
|
85
89
|
});
|
|
86
90
|
|
|
91
|
+
assistant
|
|
92
|
+
.command('publish')
|
|
93
|
+
.description('Publish a draft assistant (draft to active), so it can be deployed')
|
|
94
|
+
.argument('<assistantId>', 'assistant ID')
|
|
95
|
+
.action(async (assistantId) => {
|
|
96
|
+
auth.requireAuth();
|
|
97
|
+
const orgId = resolveOrgId(auth, program);
|
|
98
|
+
// #1028: create makes a draft and deploy refuses anything not active; this is the step between.
|
|
99
|
+
const ep = resolveEndpoint('assistants.publish', { orgId, assistantId });
|
|
100
|
+
await client.post(ep.path, { service: ep.service });
|
|
101
|
+
printSuccess('Assistant published.');
|
|
102
|
+
});
|
|
103
|
+
|
|
87
104
|
assistant
|
|
88
105
|
.command('deploy')
|
|
89
|
-
.description('Deploy an assistant')
|
|
106
|
+
.description('Deploy an active assistant to a scope (scope ids: quox org divisions list, quox department list, quox team list)')
|
|
90
107
|
.argument('<assistantId>', 'assistant ID')
|
|
91
|
-
.option('--
|
|
108
|
+
.option('--scope <scopeId>', 'scope ID to deploy to (required)')
|
|
109
|
+
.option('--target <scopeId>', 'alias for --scope')
|
|
110
|
+
.option('--mode <mode>', `${DEPLOY_MODES.join(', ')} (default shared)`)
|
|
92
111
|
.action(async (assistantId, options) => {
|
|
93
112
|
auth.requireAuth();
|
|
94
113
|
const orgId = resolveOrgId(auth, program);
|
|
95
|
-
|
|
96
|
-
|
|
114
|
+
// #998: the auth route requires scope_id; the CLI used to send { target }.
|
|
115
|
+
const scopeId = options.scope || options.target;
|
|
116
|
+
if (!scopeId) throw new QuoxError('assistant deploy needs --scope <scopeId>.', 1);
|
|
117
|
+
// #1049: the server's modes (auth routes/assistants.js DEPLOYMENT_MODES); refuse anything else here.
|
|
118
|
+
if (options.mode && !DEPLOY_MODES.includes(options.mode)) {
|
|
119
|
+
throw new QuoxError(`--mode must be one of: ${DEPLOY_MODES.join(', ')} (got "${options.mode}").`, 1);
|
|
120
|
+
}
|
|
121
|
+
const body = { scope_id: scopeId };
|
|
122
|
+
if (options.mode) body.mode = options.mode;
|
|
97
123
|
await client.post(resolveEndpoint('assistants.deploy', { orgId, assistantId }).path, { service: resolveEndpoint('assistants.deploy', { orgId, assistantId }).service, body });
|
|
98
124
|
printSuccess('Assistant deployed.');
|
|
99
125
|
});
|
|
100
126
|
|
|
127
|
+
assistant
|
|
128
|
+
.command('undeploy')
|
|
129
|
+
.description('Remove a deployment of an assistant (by --deployment id, or the active one at --scope)')
|
|
130
|
+
.argument('<assistantId>', 'assistant ID')
|
|
131
|
+
.option('--deployment <deploymentId>', 'deployment ID to remove (see quox assistant get)')
|
|
132
|
+
.option('--scope <scopeId>', 'remove the active deployment at this scope')
|
|
133
|
+
.action(async (assistantId, options) => {
|
|
134
|
+
auth.requireAuth();
|
|
135
|
+
const orgId = resolveOrgId(auth, program);
|
|
136
|
+
// #1049: a deployed assistant could not be removed from the CLI.
|
|
137
|
+
let deploymentId = options.deployment;
|
|
138
|
+
if (!deploymentId) {
|
|
139
|
+
if (!options.scope) throw new QuoxError('assistant undeploy needs --deployment <id> or --scope <scopeId>.', 1);
|
|
140
|
+
const g = resolveEndpoint('assistants.get', { orgId, assistantId });
|
|
141
|
+
const data = await client.get(g.path, { service: g.service });
|
|
142
|
+
const deps = (data && data.assistant && data.assistant.deployments) || (data && data.deployments) || [];
|
|
143
|
+
const live = deps.filter((d) => d.status === 'active');
|
|
144
|
+
const hit = live.find((d) => d.scope_id === options.scope);
|
|
145
|
+
if (!hit) {
|
|
146
|
+
const where = live.length ? live.map((d) => `${d.scope_id} (${d.id})`).join(', ') : 'nowhere';
|
|
147
|
+
throw new QuoxError(`No active deployment of ${assistantId} at scope ${options.scope}. It is deployed at: ${where}.`, 1);
|
|
148
|
+
}
|
|
149
|
+
deploymentId = hit.id;
|
|
150
|
+
}
|
|
151
|
+
const ep = resolveEndpoint('assistants.undeploy', { orgId, assistantId, deploymentId });
|
|
152
|
+
await client.delete(ep.path, { service: ep.service });
|
|
153
|
+
printSuccess(`Deployment ${deploymentId} removed.`);
|
|
154
|
+
});
|
|
155
|
+
|
|
101
156
|
assistant
|
|
102
157
|
.command('fork')
|
|
103
158
|
.description('Fork an assistant')
|
package/lib/commands/auth.js
CHANGED
|
@@ -199,7 +199,9 @@ export function registerAuthCommands(program, ctx) {
|
|
|
199
199
|
.action(async () => {
|
|
200
200
|
auth.requireAuth();
|
|
201
201
|
const data = await client.get(resolveEndpoint('auth.me.list').path, { service: resolveEndpoint('auth.me.list').service });
|
|
202
|
-
|
|
202
|
+
// A QUOX_TOKEN_FILE principal (the W4 builder uid) has no stored profile:
|
|
203
|
+
// render from the server's /me answer rather than crash on null.
|
|
204
|
+
const authData = config.getAuth() || {};
|
|
203
205
|
|
|
204
206
|
// quox#463: resident logins carry org_id but no org_name (the
|
|
205
207
|
// bootstrap has only the id), and this rendered a real membership as
|
|
@@ -245,8 +247,12 @@ export function registerAuthCommands(program, ctx) {
|
|
|
245
247
|
org: orgDisplay,
|
|
246
248
|
role: authData.org_role || 'n/a',
|
|
247
249
|
mfa: data.user?.mfa_enabled ? 'enabled' : 'disabled',
|
|
250
|
+
// Z2H-3c: expires_at is the ACCESS token. With a refresh token the sign-in outlives it, so do not
|
|
251
|
+
// present it as the session ending (a fresh owner read "expires in 60m" right after logging in).
|
|
248
252
|
session: authData.expires_at
|
|
249
|
-
?
|
|
253
|
+
? (authData.refresh_token
|
|
254
|
+
? `active, auto-renews (token refresh in ${Math.max(0, Math.round((authData.expires_at - Date.now()) / 60000))}m)`
|
|
255
|
+
: `expires in ${Math.max(0, Math.round((authData.expires_at - Date.now()) / 60000))}m`)
|
|
250
256
|
: 'unknown',
|
|
251
257
|
profile: config.getActiveProfileName(),
|
|
252
258
|
};
|
package/lib/commands/backup.js
CHANGED
|
@@ -59,14 +59,18 @@ export function registerBackupCommands(program, ctx) {
|
|
|
59
59
|
backup
|
|
60
60
|
.command('create')
|
|
61
61
|
.description('Create a backup')
|
|
62
|
-
.option('--label <label>', 'backup label')
|
|
63
|
-
.option('--type <type>', 'backup type: full,
|
|
62
|
+
.option('--label <label>', 'backup label (stored as the backup name)')
|
|
63
|
+
.option('--type <type>', 'backup type: full, quick, selective', 'full')
|
|
64
64
|
.option('--components <list>', 'comma-separated components to include')
|
|
65
65
|
.action(async (options) => {
|
|
66
66
|
auth.requireAuth();
|
|
67
67
|
const orgId = resolveOrgId(auth, program);
|
|
68
|
-
|
|
69
|
-
|
|
68
|
+
// #1017: the server reads backup_type and name (services/auth/routes/backups.js); type/label were dropped,
|
|
69
|
+
// and a type outside the backups.backup_type CHECK would only fail later inside the server.
|
|
70
|
+
const TYPES = ['full', 'quick', 'selective'];
|
|
71
|
+
if (!TYPES.includes(options.type)) throw new Error(`--type must be one of: ${TYPES.join(', ')}`);
|
|
72
|
+
const body = { backup_type: options.type };
|
|
73
|
+
if (options.label) body.name = options.label;
|
|
70
74
|
if (options.components) body.components = options.components.split(',').map(c => c.trim());
|
|
71
75
|
|
|
72
76
|
const ora = (await import('ora')).default;
|
package/lib/commands/brain.js
CHANGED
|
@@ -51,6 +51,18 @@ export function titleOverlap(a, b) {
|
|
|
51
51
|
for (const w of x) if (y.has(w)) hit++;
|
|
52
52
|
return hit / Math.min(x.size, y.size);
|
|
53
53
|
}
|
|
54
|
+
const NEGATIVE_WORDS = new Set(['disable', 'disabled', 'disabling', 'off', 'deny', 'denied', 'block', 'blocked', 'stop', 'stopped', 'remove', 'removed', 'drop', 'dropped', 'abandon', 'abandoned', 'reject', 'rejected', 'exclude', 'excluded', 'not', 'never', 'no', 'dont', 'cannot', 'cant', 'wont', 'without', 'revoke', 'revoked']);
|
|
55
|
+
/** Odd count of negating words = negative polarity ("not", "stop", "disable", "reject", ...). */
|
|
56
|
+
function negativeParity(t) {
|
|
57
|
+
const toks = String(t || '').toLowerCase().replace(/['\u2019]/g, '').split(/[^a-z0-9]+/).filter(Boolean);
|
|
58
|
+
let n = 0;
|
|
59
|
+
for (const w of toks) if (NEGATIVE_WORDS.has(w)) n++;
|
|
60
|
+
return n % 2;
|
|
61
|
+
}
|
|
62
|
+
/** True when two decision titles read as opposites (enable/disable, on/off, stop X, no longer X...). */
|
|
63
|
+
export function oppositePolarity(a, b) {
|
|
64
|
+
return negativeParity(a) !== negativeParity(b);
|
|
65
|
+
}
|
|
54
66
|
export const DECIDE_DUPE_THRESHOLD = 0.6;
|
|
55
67
|
export const DISTILL_CAP = 50;
|
|
56
68
|
|
|
@@ -136,6 +148,24 @@ export function scoreRetrievalEval(judgements, searches, k = 8) {
|
|
|
136
148
|
export function registerBrainCommands(program, ctx) {
|
|
137
149
|
const { client, auth } = ctx;
|
|
138
150
|
|
|
151
|
+
// quox#934: read/history/related go through the NotesQ API, which only knows
|
|
152
|
+
// editable NotesQ notes. A feed note (git_commit, email, ...) that `brain note`
|
|
153
|
+
// resolves came back as a bare 404. On a NotesQ 404, ask the brain itself;
|
|
154
|
+
// rethrow the original error when the brain does not know the note either.
|
|
155
|
+
const brainNoteOn404 = async (noteId, err) => {
|
|
156
|
+
if (!(err instanceof QuoxApiError) || err.status !== 404) throw err;
|
|
157
|
+
let note;
|
|
158
|
+
try {
|
|
159
|
+
const ep = resolveEndpoint('brain.note.get', { id: noteId });
|
|
160
|
+
note = await client.get(ep.path, { service: ep.service });
|
|
161
|
+
} catch {
|
|
162
|
+
throw err;
|
|
163
|
+
}
|
|
164
|
+
if (!note || !note.id) throw err;
|
|
165
|
+
return note;
|
|
166
|
+
};
|
|
167
|
+
const feedReason = (n, what) => `${n.id} is a ${n.kind || 'feed'} note${n.source_type ? ` from the ${n.source_type} source` : ''}, not an editable NotesQ note, so it has no ${what}.`;
|
|
168
|
+
|
|
139
169
|
const brain = program
|
|
140
170
|
.command('brain')
|
|
141
171
|
.description('Query Quox Qortex (vaults, notes, semantic search)')
|
|
@@ -164,10 +194,16 @@ export function registerBrainCommands(program, ctx) {
|
|
|
164
194
|
console.log(JSON.stringify(data, null, 2));
|
|
165
195
|
return;
|
|
166
196
|
}
|
|
197
|
+
// quox#853: a keyword-only fallback fooled both the product and a
|
|
198
|
+
// verifier ("0 hits" read as "does not exist"). Say it in words.
|
|
199
|
+
const degradedNote = data.retrieval === 'fts' && data.degraded
|
|
200
|
+
? '⚠ Semantic search was unavailable for this query, so this is keyword-only: a missing or zero result does not prove the note is absent. Retry shortly.'
|
|
201
|
+
: '';
|
|
167
202
|
if (insufficient) {
|
|
168
203
|
const legLabel = data.retrieval ? ` · ${data.retrieval}` : '';
|
|
169
204
|
console.log(glyphLine('brain2', `search "${query.slice(0, 40)}"`, { result: `0 relevant hits${legLabel}`, ms: data.tookMs ?? '?' }));
|
|
170
205
|
console.log('⚠ Qortex judged the matches for this query insufficient (no relevant notes). The weak hits are withheld; use -o json to see them with the relevance details.');
|
|
206
|
+
if (degradedNote) console.log(degradedNote);
|
|
171
207
|
return;
|
|
172
208
|
}
|
|
173
209
|
// BRAIN2-AUDIT: sessions must SEE what Qortex recalled, not just a hit
|
|
@@ -176,12 +212,14 @@ export function registerBrainCommands(program, ctx) {
|
|
|
176
212
|
const results = data.results || [];
|
|
177
213
|
const legLabel = data.retrieval ? ` · ${data.retrieval}` : '';
|
|
178
214
|
console.log(glyphLine('brain2', `search "${query.slice(0, 40)}"`, { result: `${results.length} hits${legLabel}`, ms: data.tookMs ?? '?' }));
|
|
215
|
+
if (degradedNote) console.log(degradedNote);
|
|
179
216
|
if (results.length === 0) {
|
|
180
217
|
console.log(glyphLine('brain2', 'recall', { result: 'nothing on record for this query' }));
|
|
181
218
|
return;
|
|
182
219
|
}
|
|
183
220
|
for (const r of results) {
|
|
184
|
-
|
|
221
|
+
const miss = Array.isArray(r.missingEntities) && r.missingEntities.length ? ` · does not mention ${r.missingEntities.join(', ')}` : '';
|
|
222
|
+
console.log(`${brandName('🌌 QORTEX')} recall │ "${recallSnippet(r)}" · ${recallSource(r)} · ${r.title || 'untitled'} · note ${r.noteId || '?'}${miss}`);
|
|
185
223
|
}
|
|
186
224
|
return;
|
|
187
225
|
}
|
|
@@ -191,6 +229,7 @@ export function registerBrainCommands(program, ctx) {
|
|
|
191
229
|
if (program.opts().output !== 'json') {
|
|
192
230
|
const legLabel = data.retrieval ? ` · ${data.retrieval}` : '';
|
|
193
231
|
console.log(glyphLine('brain2', `search "${query.slice(0, 40)}"`, { result: `${(data.results || []).length} hits${legLabel}`, ms: data.tookMs ?? '?' }));
|
|
232
|
+
if (degradedNote) console.log(degradedNote);
|
|
194
233
|
}
|
|
195
234
|
formatOutput({
|
|
196
235
|
columns: ['title', 'kind', 'snippet', 'score'],
|
|
@@ -300,7 +339,7 @@ export function registerBrainCommands(program, ctx) {
|
|
|
300
339
|
if (!['private', 'org'].includes(options.scope)) throw new QuoxError('--scope must be private or org.', 2);
|
|
301
340
|
const prov = decisionProvenance();
|
|
302
341
|
const title = `Decision: ${what}`.slice(0, 200);
|
|
303
|
-
|
|
342
|
+
let markdown = decisionMarkdown({ what, why: options.why, rejected: options.rejected, prov });
|
|
304
343
|
const tags = ['decision', ...String(options.tags || '').split(',').map((t) => t.trim()).filter(Boolean)];
|
|
305
344
|
const json = program.opts().output === 'json';
|
|
306
345
|
if (options.dryRun) {
|
|
@@ -311,7 +350,15 @@ export function registerBrainCommands(program, ctx) {
|
|
|
311
350
|
const sp = resolveEndpoint('brain.search.list');
|
|
312
351
|
const found = await client.post(sp.path, { service: sp.service, body: { query: what, limit: 5 } });
|
|
313
352
|
const insufficient = found.insufficient === true || found.relevance?.insufficient === true;
|
|
314
|
-
const
|
|
353
|
+
const bare = (r) => String(r.title || '').replace(/^Decision:\s*/i, '');
|
|
354
|
+
const matches = insufficient ? [] : (found.results || []).filter((r) => titleOverlap(what, bare(r)) >= DECIDE_DUPE_THRESHOLD);
|
|
355
|
+
// An opposite-polarity match is a reversal of the earlier decision, never a duplicate of it.
|
|
356
|
+
const dupe = matches.find((r) => !oppositePolarity(what, bare(r)));
|
|
357
|
+
const reversed = dupe ? null : matches[0];
|
|
358
|
+
if (reversed) {
|
|
359
|
+
const ref = reversed.noteId || reversed.title;
|
|
360
|
+
if (ref) markdown = decisionMarkdown({ what, why: options.why, rejected: options.rejected, prov: { ...prov, supersedes: ref } });
|
|
361
|
+
}
|
|
315
362
|
if (dupe && !options.force) {
|
|
316
363
|
if (json) console.log(JSON.stringify({ saved: false, reason: 'existing_match', match: { noteId: dupe.noteId, title: dupe.title, kind: dupe.kind } }, null, 2));
|
|
317
364
|
else {
|
|
@@ -324,9 +371,10 @@ export function registerBrainCommands(program, ctx) {
|
|
|
324
371
|
}
|
|
325
372
|
const ep = resolveEndpoint('brain.capture.create');
|
|
326
373
|
const data = await client.post(ep.path, { service: ep.service, body: { title, text: markdown.replace(/^# .*\n\n/, ''), tags, scope: options.scope === 'org' ? 'org' : 'user' } });
|
|
327
|
-
if (json) { console.log(JSON.stringify(data, null, 2)); return; }
|
|
374
|
+
if (json) { console.log(JSON.stringify(reversed ? { ...data, reverses: { noteId: reversed.noteId, title: reversed.title } } : data, null, 2)); return; }
|
|
328
375
|
const verb = data.status === 'created' ? 'saved' : data.status === 'unchanged' ? 'already saved' : 'updated';
|
|
329
376
|
console.log(glyphLine('brain2', 'decide', { result: `${verb} · note ${String(data.noteId || '?').slice(0, 8)}` }));
|
|
377
|
+
if (reversed) console.log(` reverses ${reversed.title || 'untitled'} (note ${reversed.noteId || '?'})`);
|
|
330
378
|
});
|
|
331
379
|
|
|
332
380
|
brain
|
|
@@ -443,14 +491,14 @@ export function registerBrainCommands(program, ctx) {
|
|
|
443
491
|
formatDetail(data, { format });
|
|
444
492
|
return;
|
|
445
493
|
}
|
|
446
|
-
const verdict = (r) => (r.contradicted ? 'contradicted' : r.supported === true ? 'supported' : r.supported === false ? 'unsupported' : 'not measured');
|
|
494
|
+
const verdict = (r) => (r.fabricated || r.support_method === 'fabricated' ? 'fabricated' : r.support_method === 'unchecked' ? 'unchecked' : r.contradicted ? 'contradicted' : r.supported === true ? 'supported' : r.support_method === 'overlap' ? 'overlap' : r.supported === false ? 'unsupported' : 'not measured');
|
|
447
495
|
formatOutput({
|
|
448
496
|
columns: ['session', 'handle', 'note', 'span', 'rev', 'verdict', 'at'],
|
|
449
497
|
headers: ['Session', 'Handle', 'Note', 'Span', 'Rev', 'Verdict', 'At'],
|
|
450
498
|
rows: (data.records || []).map((r) => ({
|
|
451
499
|
session: r.session || '-',
|
|
452
500
|
handle: r.handle || '-',
|
|
453
|
-
note: r.note_id,
|
|
501
|
+
note: r.note_id || '-',
|
|
454
502
|
span: r.char_start != null && r.char_end != null ? `${r.char_start}-${r.char_end}` : '-',
|
|
455
503
|
rev: r.note_hash ? String(r.note_hash).slice(0, 8) : '-',
|
|
456
504
|
verdict: verdict(r),
|
|
@@ -509,7 +557,8 @@ export function registerBrainCommands(program, ctx) {
|
|
|
509
557
|
{ metric: 'answers after brain search', week: cw.answers ?? 0, total: ct.answers ?? 0 },
|
|
510
558
|
{ metric: 'answers citing a note', week: `${cw.grounded ?? 0} (${pct(cw.grounded ?? 0, cw.answers ?? 0)})`, total: `${ct.grounded ?? 0} (${pct(ct.grounded ?? 0, ct.answers ?? 0)})` },
|
|
511
559
|
...((cw.supportMeasured ?? 0) + (ct.supportMeasured ?? 0) > 0
|
|
512
|
-
? [{ metric: 'answers with supported citations (
|
|
560
|
+
? [{ metric: 'answers with supported citations (entailment)', week: `${cw.supportedAnswers ?? 0} (${pct(cw.supportedAnswers ?? 0, cw.supportMeasured ?? 0)})`, total: `${ct.supportedAnswers ?? 0} (${pct(ct.supportedAnswers ?? 0, ct.supportMeasured ?? 0)})` },
|
|
561
|
+
{ metric: 'answers with overlap only (not checked for meaning)', week: `${cw.overlapAnswers ?? 0}`, total: `${ct.overlapAnswers ?? 0}` }]
|
|
513
562
|
: []),
|
|
514
563
|
{ metric: 'notes cited / retrieved', week: `${cw.citedNotes ?? 0}/${cw.retrievedNotes ?? 0}`, total: `${ct.citedNotes ?? 0}/${ct.retrievedNotes ?? 0}` },
|
|
515
564
|
],
|
|
@@ -559,7 +608,13 @@ export function registerBrainCommands(program, ctx) {
|
|
|
559
608
|
.action(async (noteId, options) => {
|
|
560
609
|
auth.requireAuth();
|
|
561
610
|
const ep = resolveEndpoint('notesq.notes.get', { id: noteId });
|
|
562
|
-
|
|
611
|
+
let data;
|
|
612
|
+
try {
|
|
613
|
+
data = await client.get(ep.path, { service: ep.service });
|
|
614
|
+
} catch (e) {
|
|
615
|
+
const bn = await brainNoteOn404(noteId, e);
|
|
616
|
+
data = { note: bn, source: 'brain', editable: false };
|
|
617
|
+
}
|
|
563
618
|
const n = data.note || data;
|
|
564
619
|
if (program.opts().output === 'json') { console.log(JSON.stringify(data, null, 2)); return; }
|
|
565
620
|
if (options.raw) { console.log(n.body ?? n.markdown ?? ''); return; }
|
|
@@ -602,11 +657,22 @@ export function registerBrainCommands(program, ctx) {
|
|
|
602
657
|
.description('Delete a note (recorded as a revision, restorable in NotesQ). Requires --yes.')
|
|
603
658
|
.argument('<noteId>', 'note id')
|
|
604
659
|
.option('--yes', 'confirm deletion')
|
|
660
|
+
.option('--expected-hash <hash>', "the note's content_hash you read (see `quox brain read`); a stale value is a conflict (409) and nothing is deleted")
|
|
605
661
|
.action(async (noteId, options) => {
|
|
606
662
|
auth.requireAuth();
|
|
607
663
|
if (!options.yes) throw new QuoxError(`Refusing to delete ${noteId} without --yes.`, 2);
|
|
664
|
+
if (!options.expectedHash) throw new QuoxError(`Refusing to delete ${noteId} without --expected-hash (the content_hash from \`quox brain read ${noteId}\`), so a concurrent edit is never deleted.`, 2);
|
|
608
665
|
const ep = resolveEndpoint('notesq.notes.delete', { id: noteId });
|
|
609
|
-
|
|
666
|
+
let data;
|
|
667
|
+
try {
|
|
668
|
+
data = await client.delete(ep.path, { service: ep.service, query: { expected_hash: options.expectedHash } });
|
|
669
|
+
} catch (e) {
|
|
670
|
+
if (e instanceof QuoxApiError && e.status === 409) {
|
|
671
|
+
const h = e.body && (e.body.currentHash || e.body.current_hash);
|
|
672
|
+
throw new QuoxError(`Conflict (409): note ${noteId} changed since hash ${options.expectedHash} (current_hash ${h || 'unknown'}). Nothing was deleted. Re-run \`quox brain read ${noteId}\` and retry.`, 1);
|
|
673
|
+
}
|
|
674
|
+
throw e;
|
|
675
|
+
}
|
|
610
676
|
if (program.opts().output === 'json') { console.log(JSON.stringify(data, null, 2)); return; }
|
|
611
677
|
printSuccess(`Deleted note ${noteId}.`);
|
|
612
678
|
});
|
|
@@ -621,7 +687,14 @@ export function registerBrainCommands(program, ctx) {
|
|
|
621
687
|
.action(async (noteId, options) => {
|
|
622
688
|
auth.requireAuth();
|
|
623
689
|
const ep = resolveEndpoint('notesq.notes.related', { id: noteId });
|
|
624
|
-
|
|
690
|
+
let data;
|
|
691
|
+
try {
|
|
692
|
+
data = await client.get(ep.path, { service: ep.service, query: { limit: Math.min(Math.max(parseInt(options.limit, 10) || 8, 1), 20) } });
|
|
693
|
+
} catch (e) {
|
|
694
|
+
const bn = await brainNoteOn404(noteId, e);
|
|
695
|
+
data = { related: [], editable: false, reason: `${feedReason(bn, 'similarity links')} Use \`quox brain neighbours ${noteId}\` for its graph links.` };
|
|
696
|
+
if (program.opts().output !== 'json') console.log(data.reason);
|
|
697
|
+
}
|
|
625
698
|
const related = data.related || [];
|
|
626
699
|
if (program.opts().output === 'json') { console.log(JSON.stringify(data, null, 2)); return; }
|
|
627
700
|
console.log(glyphLine('brain2', `related ${noteId.slice(0, 8)}`, { result: `${related.length} notes` }));
|
|
@@ -657,11 +730,13 @@ export function registerBrainCommands(program, ctx) {
|
|
|
657
730
|
.description('Token-budgeted pack of the most relevant cited passages on a topic (what the Cortex brain_context tool returns)')
|
|
658
731
|
.argument('<query>', 'topic or task')
|
|
659
732
|
.option('--budget <n>', 'approximate token budget (200-4000)', '1200')
|
|
733
|
+
.option('--since <date>', 'only notes dated on or after this ISO date/datetime (e.g. 2026-10-05)')
|
|
734
|
+
.option('--until <date>', 'only notes dated on or before this ISO date/datetime (a date alone means the whole day)')
|
|
660
735
|
.action(async (query, options) => {
|
|
661
736
|
auth.requireAuth();
|
|
662
737
|
const tokenBudget = Math.min(Math.max(parseInt(options.budget, 10) || 1200, 200), 4000);
|
|
663
738
|
const ep = resolveEndpoint('brain.context.pack');
|
|
664
|
-
const data = await client.post(ep.path, { service: ep.service, body: { query, tokenBudget } });
|
|
739
|
+
const data = await client.post(ep.path, { service: ep.service, body: { query, tokenBudget, ...(options.since ? { since: options.since } : {}), ...(options.until ? { until: options.until } : {}) } });
|
|
665
740
|
if (program.opts().output === 'json') { console.log(JSON.stringify(data, null, 2)); return; }
|
|
666
741
|
const passages = data.passages || [];
|
|
667
742
|
const insufficient = data.insufficient === true || data.relevance?.insufficient === true || passages.length === 0;
|
|
@@ -693,7 +768,7 @@ export function registerBrainCommands(program, ctx) {
|
|
|
693
768
|
if (!data || data.success === false) {
|
|
694
769
|
throw new QuoxError(`brain_check failed: ${(data && (data.error || data.output)) || 'unknown error'}`, 1);
|
|
695
770
|
}
|
|
696
|
-
const mark = { supported: '✔', contradicted: '✖', unknown: '◐' }[data.verdict] || '';
|
|
771
|
+
const mark = { supported: '✔', contradicted: '✖', overlap: '◐', unknown: '◐' }[data.verdict] || '';
|
|
697
772
|
console.log(glyphLine('brain2', `check "${claim.slice(0, 40)}"`, { result: `${data.verdict} ${mark}`.trim() }));
|
|
698
773
|
const ev = data.evidence || [];
|
|
699
774
|
for (const e of ev) {
|
|
@@ -749,7 +824,14 @@ export function registerBrainCommands(program, ctx) {
|
|
|
749
824
|
return;
|
|
750
825
|
}
|
|
751
826
|
const ep = resolveEndpoint('notesq.notes.revisions', { id: noteId });
|
|
752
|
-
|
|
827
|
+
let data;
|
|
828
|
+
try {
|
|
829
|
+
data = await client.get(ep.path, { service: ep.service });
|
|
830
|
+
} catch (e) {
|
|
831
|
+
const bn = await brainNoteOn404(noteId, e);
|
|
832
|
+
data = { revisions: [], editable: false, reason: feedReason(bn, 'revision history') };
|
|
833
|
+
if (program.opts().output !== 'json') { console.log(data.reason); return; }
|
|
834
|
+
}
|
|
753
835
|
if (program.opts().output === 'json') { console.log(JSON.stringify(data, null, 2)); return; }
|
|
754
836
|
formatOutput({ columns: ['rev', 'action', 'author_kind', 'created_at', 'content_hash'], headers: ['Rev', 'Action', 'Author', 'When', 'Hash'], rows: data.revisions || [] }, { format: program.opts().output });
|
|
755
837
|
});
|
|
@@ -770,7 +852,16 @@ export function registerBrainCommands(program, ctx) {
|
|
|
770
852
|
throw new QuoxError(`Conflict: note ${noteId} changed since hash ${options.expectedHash} (current_hash ${current || 'unknown'}). Nothing was restored. Re-run \`quox brain read ${noteId}\` and try again.`, 1);
|
|
771
853
|
}
|
|
772
854
|
const ep = resolveEndpoint('notesq.notes.restore', { id: noteId });
|
|
773
|
-
|
|
855
|
+
let data;
|
|
856
|
+
try {
|
|
857
|
+
data = await client.post(ep.path, { service: ep.service, body: { rev, expected_hash: options.expectedHash } });
|
|
858
|
+
} catch (e) {
|
|
859
|
+
if (e instanceof QuoxApiError && e.status === 409) {
|
|
860
|
+
const h = e.body && (e.body.currentHash || e.body.current_hash);
|
|
861
|
+
throw new QuoxError(`Conflict (409): note ${noteId} changed since hash ${options.expectedHash} (current_hash ${h || 'unknown'}). Nothing was restored. Re-run \`quox brain read ${noteId}\` and try again.`, 1);
|
|
862
|
+
}
|
|
863
|
+
throw e;
|
|
864
|
+
}
|
|
774
865
|
if (program.opts().output === 'json') { console.log(JSON.stringify(data, null, 2)); return; }
|
|
775
866
|
printSuccess(data.unchanged ? `Note ${noteId} already matches revision ${rev}.` : `Restored note ${noteId} to revision ${rev} (new rev ${data.revision?.rev ?? '?'}).`);
|
|
776
867
|
});
|
package/lib/commands/build.js
CHANGED
|
@@ -33,10 +33,26 @@ export function registerBuildCommands(program, ctx) {
|
|
|
33
33
|
// own parsed opts.
|
|
34
34
|
const groupOpt = (options, key) => options[key] ?? build.opts()[key];
|
|
35
35
|
|
|
36
|
+
// An org with exactly one dev server needs no --host: use it and say so. With several, name them;
|
|
37
|
+
// with none, say so (proof cases code/reach-build-status / -targets exited 1 on a one-server org).
|
|
38
|
+
async function resolveHost(options) {
|
|
39
|
+
const given = groupOpt(options, 'host');
|
|
40
|
+
if (given) return given;
|
|
41
|
+
const listEp = resolveEndpoint('v1.dev.servers.list');
|
|
42
|
+
const list = await client.get(listEp.path, { service: listEp.service });
|
|
43
|
+
const ids = (Array.isArray(list?.servers) ? list.servers : []).map((s) => s.host_id).filter(Boolean);
|
|
44
|
+
if (ids.length === 1) {
|
|
45
|
+
printWarning(`No --host given; using the only dev server: ${ids[0]}`);
|
|
46
|
+
return ids[0];
|
|
47
|
+
}
|
|
48
|
+
if (ids.length === 0) throw new Error('No dev server is registered for this org (register one with `quox dev enable <hostId>`).');
|
|
49
|
+
throw new Error(`--host <hostId> is required: this org has ${ids.length} dev servers (${ids.join(', ')}).`);
|
|
50
|
+
}
|
|
51
|
+
|
|
36
52
|
async function dispatch(target, options) {
|
|
37
53
|
auth.requireAuth();
|
|
38
54
|
options = {
|
|
39
|
-
host:
|
|
55
|
+
host: await resolveHost(options),
|
|
40
56
|
stream: groupOpt(options, 'stream'),
|
|
41
57
|
phase: groupOpt(options, 'phase'),
|
|
42
58
|
msg: groupOpt(options, 'msg'),
|
|
@@ -76,8 +92,7 @@ export function registerBuildCommands(program, ctx) {
|
|
|
76
92
|
.option('--host <hostId>', 'dev server host_id')
|
|
77
93
|
.action(async (options) => {
|
|
78
94
|
auth.requireAuth();
|
|
79
|
-
const host =
|
|
80
|
-
if (!host) throw new Error('--host <hostId> is required');
|
|
95
|
+
const host = await resolveHost(options);
|
|
81
96
|
const endpoint = resolveEndpoint('v1.dev.build.status', { hostId: host });
|
|
82
97
|
const data = await client.get(endpoint.path, { service: endpoint.service });
|
|
83
98
|
formatDetail(data, { format: program.opts().output, title: `Build status: ${host}` });
|
|
@@ -89,8 +104,7 @@ export function registerBuildCommands(program, ctx) {
|
|
|
89
104
|
.option('--host <hostId>', 'dev server host_id')
|
|
90
105
|
.action(async (options) => {
|
|
91
106
|
auth.requireAuth();
|
|
92
|
-
const host =
|
|
93
|
-
if (!host) throw new Error('--host <hostId> is required');
|
|
107
|
+
const host = await resolveHost(options);
|
|
94
108
|
const endpoint = resolveEndpoint('v1.dev.build.targets', { hostId: host });
|
|
95
109
|
const data = await client.get(endpoint.path, { service: endpoint.service });
|
|
96
110
|
formatOutput(data.targets || [], {
|