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,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NotesQ session bulk authoring (AQ-9D P8): pure helpers behind
|
|
3
|
+
* `quox notes batch` and `quox notes split`. A batch travels as a zip of
|
|
4
|
+
* markdown files (frontmatter: title, collection, tags, key) and is parsed
|
|
5
|
+
* server-side by the import pipeline's `batch` format.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { readdirSync, readFileSync, statSync } from 'fs';
|
|
9
|
+
import { basename, join, relative } from 'path';
|
|
10
|
+
import { QuoxError } from '../core/errors.js';
|
|
11
|
+
|
|
12
|
+
// Mirrors MAX_BATCH_NOTES in services/collector/lib/notesq/import/index.js.
|
|
13
|
+
export const MAX_BATCH_NOTES = 200;
|
|
14
|
+
const MAX_ARCHIVE_BYTES = 100 * 1024 * 1024;
|
|
15
|
+
|
|
16
|
+
// The server's frontmatter reader strips one pair of quotes and does no escape
|
|
17
|
+
// processing, so pick the quote the value does not contain.
|
|
18
|
+
function yamlStr(v) {
|
|
19
|
+
const s = String(v).replace(/[\r\n]+/g, ' ').trim();
|
|
20
|
+
if (!s.includes('"')) return `"${s}"`;
|
|
21
|
+
if (!s.includes("'")) return `'${s}'`;
|
|
22
|
+
return `"${s.replace(/"/g, "'")}"`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** One note as a markdown file with the frontmatter the batch parser reads. */
|
|
26
|
+
export function noteFile({ title, markdown, collection, tags, key }) {
|
|
27
|
+
const fm = [];
|
|
28
|
+
if (title) fm.push(`title: ${yamlStr(title)}`);
|
|
29
|
+
if (collection) fm.push(`collection: ${yamlStr(collection)}`);
|
|
30
|
+
if (key) fm.push(`key: ${yamlStr(key)}`);
|
|
31
|
+
const list = (tags || []).map((t) => String(t).trim()).filter(Boolean);
|
|
32
|
+
if (list.length) { fm.push('tags:'); for (const t of list) fm.push(` - ${yamlStr(t)}`); }
|
|
33
|
+
return `${fm.length ? `---\n${fm.join('\n')}\n---\n` : ''}${String(markdown || '')}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** JSONL text -> items. Each line: {title, markdown|body, collection?, tags?, key?}. Key defaults to the title, so a re-run skips. */
|
|
37
|
+
export function parseJsonl(text) {
|
|
38
|
+
const items = [];
|
|
39
|
+
String(text || '').split(/\r?\n/).forEach((line, i) => {
|
|
40
|
+
if (!line.trim()) return;
|
|
41
|
+
let o;
|
|
42
|
+
try { o = JSON.parse(line); } catch (e) { throw new QuoxError(`JSONL line ${i + 1} is not valid JSON: ${e.message}`, 2); }
|
|
43
|
+
const title = typeof o.title === 'string' ? o.title.trim() : '';
|
|
44
|
+
const markdown = typeof o.markdown === 'string' ? o.markdown : (typeof o.body === 'string' ? o.body : '');
|
|
45
|
+
if (!title) throw new QuoxError(`JSONL line ${i + 1} has no title.`, 2);
|
|
46
|
+
if (o.tags !== undefined && !Array.isArray(o.tags)) throw new QuoxError(`JSONL line ${i + 1}: tags must be an array.`, 2);
|
|
47
|
+
items.push({ title, markdown, collection: typeof o.collection === 'string' ? o.collection : undefined, tags: o.tags || [], key: typeof o.key === 'string' && o.key ? o.key : title });
|
|
48
|
+
});
|
|
49
|
+
return items;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function walk(dir, root, out) {
|
|
53
|
+
for (const name of readdirSync(dir).sort()) {
|
|
54
|
+
if (name.startsWith('.')) continue;
|
|
55
|
+
const p = join(dir, name);
|
|
56
|
+
const st = statSync(p);
|
|
57
|
+
if (st.isDirectory()) walk(p, root, out);
|
|
58
|
+
else if (st.isFile()) out.push({ path: relative(root, p).split('\\').join('/'), size: st.size, read: () => readFileSync(p) });
|
|
59
|
+
}
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Build the zip for a batch. `source` is a folder (markdown + any embedded
|
|
65
|
+
* files, sent as-is), a .jsonl file, or '-' with `stdinText` holding JSONL.
|
|
66
|
+
* `items` (already-built notes, used by split) bypasses `source`.
|
|
67
|
+
* @returns {{bytes: Buffer, filename: string, notes: number}}
|
|
68
|
+
*/
|
|
69
|
+
export async function buildBatchArchive({ source, stdinText, items, name }) {
|
|
70
|
+
const JSZip = (await import('jszip')).default;
|
|
71
|
+
const zip = new JSZip();
|
|
72
|
+
let root;
|
|
73
|
+
let notes = 0;
|
|
74
|
+
const addItems = (list, rootName) => {
|
|
75
|
+
root = rootName;
|
|
76
|
+
list.forEach((it, i) => { zip.file(`${root}/${String(i + 1).padStart(3, '0')}.md`, noteFile(it)); });
|
|
77
|
+
notes = list.length;
|
|
78
|
+
};
|
|
79
|
+
if (items) addItems(items, name || 'batch');
|
|
80
|
+
else if (source === '-') addItems(parseJsonl(stdinText), name || 'stdin');
|
|
81
|
+
else {
|
|
82
|
+
let st;
|
|
83
|
+
try { st = statSync(source); } catch (e) { throw new QuoxError(`Cannot read ${source}: ${e.message}`, 2); }
|
|
84
|
+
if (st.isDirectory()) {
|
|
85
|
+
root = name || basename(source.replace(/[\\/]+$/, '')) || 'batch';
|
|
86
|
+
const files = walk(source, source, []);
|
|
87
|
+
let total = 0;
|
|
88
|
+
for (const f of files) {
|
|
89
|
+
total += f.size;
|
|
90
|
+
if (total > MAX_ARCHIVE_BYTES) throw new QuoxError(`${source} is over the 100 MB batch limit.`, 2);
|
|
91
|
+
zip.file(`${root}/${f.path}`, f.read());
|
|
92
|
+
if (/\.(md|markdown)$/i.test(f.path)) notes++;
|
|
93
|
+
}
|
|
94
|
+
} else if (/\.jsonl$/i.test(source)) {
|
|
95
|
+
addItems(parseJsonl(readFileSync(source, 'utf8')), name || basename(source).replace(/\.jsonl$/i, ''));
|
|
96
|
+
} else {
|
|
97
|
+
throw new QuoxError(`${source} is not a folder or a .jsonl file (use - to read JSONL from stdin).`, 2);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (!notes) throw new QuoxError('Nothing to write: the batch has no markdown notes.', 2);
|
|
101
|
+
if (notes > MAX_BATCH_NOTES) throw new QuoxError(`A batch may create at most ${MAX_BATCH_NOTES} notes (this one has ${notes}). Split it into smaller batches.`, 2);
|
|
102
|
+
const bytes = await zip.generateAsync({ type: 'nodebuffer', compression: 'DEFLATE' });
|
|
103
|
+
return { bytes, filename: `${root}.zip`, notes };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const HEADING = /^(#{1,6})\s+(.+?)\s*#*\s*$/;
|
|
107
|
+
const FENCE = /^\s*(```|~~~)/;
|
|
108
|
+
// Characters that would break a [[wikilink]] to the child.
|
|
109
|
+
const cleanTitle = (t) => t.replace(/[[\]|#^]/g, '').replace(/\s+/g, ' ').trim();
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Cut a note at its headings. Level defaults to the shallowest heading below
|
|
113
|
+
* the title H1. Headings inside code fences are ignored. Text before the first
|
|
114
|
+
* cut stays with the parent; deeper headings inside a section are promoted so
|
|
115
|
+
* the child reads as a note of its own.
|
|
116
|
+
* @returns {{title:string|null, preamble:string, level:number|null, sections:{title:string, body:string}[]}}
|
|
117
|
+
*/
|
|
118
|
+
export function splitMarkdown(markdown, { level } = {}) {
|
|
119
|
+
const lines = String(markdown || '').split('\n');
|
|
120
|
+
const heads = [];
|
|
121
|
+
let inFence = false;
|
|
122
|
+
lines.forEach((line, i) => {
|
|
123
|
+
if (FENCE.test(line)) { inFence = !inFence; return; }
|
|
124
|
+
if (inFence) return;
|
|
125
|
+
const m = HEADING.exec(line);
|
|
126
|
+
if (m) heads.push({ i, level: m[1].length, text: m[2] });
|
|
127
|
+
});
|
|
128
|
+
let title = null;
|
|
129
|
+
let start = 0;
|
|
130
|
+
const first = heads[0];
|
|
131
|
+
if (first && first.level === 1 && lines.slice(0, first.i).every((l) => !l.trim())) { title = first.text.trim(); start = first.i + 1; }
|
|
132
|
+
const below = heads.filter((h) => h.i >= start && (title === null || h.level > 1));
|
|
133
|
+
const lvl = level ? Number(level) : (below.length ? Math.min(...below.map((h) => h.level)) : null);
|
|
134
|
+
const cuts = below.filter((h) => h.level === lvl);
|
|
135
|
+
if (cuts.length < 2) return { title, preamble: '', level: lvl, sections: [] };
|
|
136
|
+
const preamble = lines.slice(start, cuts[0].i).join('\n').trim();
|
|
137
|
+
const seen = new Map();
|
|
138
|
+
const sections = cuts.map((c, k) => {
|
|
139
|
+
const end = k + 1 < cuts.length ? cuts[k + 1].i : lines.length;
|
|
140
|
+
let fence = false;
|
|
141
|
+
const body = lines.slice(c.i + 1, end).map((l) => {
|
|
142
|
+
if (FENCE.test(l)) { fence = !fence; return l; }
|
|
143
|
+
if (fence) return l;
|
|
144
|
+
const m = HEADING.exec(l);
|
|
145
|
+
return m && m[1].length > lvl ? `${'#'.repeat(Math.max(m[1].length - lvl + 1, 2))} ${m[2]}` : l;
|
|
146
|
+
}).join('\n').trim();
|
|
147
|
+
let t = cleanTitle(c.text) || `Part ${k + 1}`;
|
|
148
|
+
const n = (seen.get(t) || 0) + 1;
|
|
149
|
+
seen.set(t, n);
|
|
150
|
+
if (n > 1) t = `${t} (${n})`;
|
|
151
|
+
return { title: t, body };
|
|
152
|
+
});
|
|
153
|
+
return { title, preamble, level: lvl, sections };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** The parent after a split: its own preamble, then links to every child. */
|
|
157
|
+
export function indexMarkdown({ title, preamble, children }) {
|
|
158
|
+
const head = `# ${title}\n\n${preamble ? `${preamble}\n\n` : ''}`;
|
|
159
|
+
return `${head}## Contents\n\n${children.map((c) => `- [[${c}]]`).join('\n')}\n`;
|
|
160
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NotesQ AssistantQ wires (AQ-9D P4), pure helpers. Each wire composes at the
|
|
3
|
+
* edge: the CLI reads through the other plugin's own governed route (its
|
|
4
|
+
* licence, access rules and audit) and writes through NotesQ's, so no new
|
|
5
|
+
* server path reads another plugin's data.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { createHash } from 'crypto';
|
|
9
|
+
|
|
10
|
+
const short = (s) => createHash('sha1').update(s).digest('hex').slice(0, 12);
|
|
11
|
+
|
|
12
|
+
/** An InboxQ message as a note. Body text preferred; HTML is reduced to text; snippet is the last resort. */
|
|
13
|
+
export function emailToNote(msg) {
|
|
14
|
+
const subject = String(msg.subject || '').replace(/[\r\n]+/g, ' ').trim() || '(no subject)';
|
|
15
|
+
const from = msg.fromName && msg.fromAddr ? `${msg.fromName} <${msg.fromAddr}>` : (msg.fromAddr || msg.fromName || 'unknown sender');
|
|
16
|
+
const date = msg.dateTs ? new Date(Number(msg.dateTs) < 1e12 ? Number(msg.dateTs) * 1000 : Number(msg.dateTs)).toISOString() : null;
|
|
17
|
+
let body = typeof msg.bodyText === 'string' && msg.bodyText.trim() ? msg.bodyText : null;
|
|
18
|
+
let bodySource = 'text';
|
|
19
|
+
if (!body && typeof msg.bodyHtml === 'string' && msg.bodyHtml.trim()) {
|
|
20
|
+
body = msg.bodyHtml
|
|
21
|
+
.replace(/<(script|style)[\s\S]*?<\/\1>/gi, '')
|
|
22
|
+
.replace(/<br\s*\/?>/gi, '\n').replace(/<\/(p|div|li|tr|h[1-6])>/gi, '\n')
|
|
23
|
+
.replace(/<[^>]+>/g, '')
|
|
24
|
+
.replace(/ /g, ' ').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, "'")
|
|
25
|
+
.replace(/\n{3,}/g, '\n\n');
|
|
26
|
+
bodySource = 'html';
|
|
27
|
+
}
|
|
28
|
+
if (!body) { body = msg.snippet || ''; bodySource = 'snippet'; }
|
|
29
|
+
const quoted = String(body).trim().split('\n').map((l) => (l ? `> ${l}` : '>')).join('\n');
|
|
30
|
+
const markdown = `# ${subject}\n\n**From:** ${from} \n${date ? `**Date:** ${date} \n` : ''}**Saved from InboxQ**\n\n${quoted}\n`;
|
|
31
|
+
const properties = { source: 'inboxq', inboxq_message_id: msg.id, email_from: msg.fromAddr || from, ...(date ? { email_date: date } : {}) };
|
|
32
|
+
return { title: subject, markdown, properties, bodySource };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Unchecked markdown tasks (`- [ ] text`, `* [ ]`, `1. [ ]`) outside code
|
|
37
|
+
* fences, each with a stable short hash so a re-run can skip ones already sent.
|
|
38
|
+
*/
|
|
39
|
+
export function extractTasks(markdown) {
|
|
40
|
+
const out = [];
|
|
41
|
+
let fence = false;
|
|
42
|
+
for (const line of String(markdown || '').split('\n')) {
|
|
43
|
+
if (/^\s*(```|~~~)/.test(line)) { fence = !fence; continue; }
|
|
44
|
+
if (fence) continue;
|
|
45
|
+
const m = /^\s*(?:[-*+]|\d+[.)])\s+\[ \]\s+(.+?)\s*$/.exec(line);
|
|
46
|
+
if (m && m[1]) out.push({ text: m[1].slice(0, 200), hash: short(m[1].trim().toLowerCase()) });
|
|
47
|
+
}
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** The QuoxPlan stream note for a promoted NotesQ note: title, link, and an excerpt. */
|
|
52
|
+
export function promoteBody(note, link) {
|
|
53
|
+
const body = String(note.markdown || '').replace(/^\s*#\s+.+\n+/, '').trim();
|
|
54
|
+
const excerpt = body.length > 1500 ? `${body.slice(0, 1500).trimEnd()}...` : body;
|
|
55
|
+
return `NotesQ: "${note.title || 'Untitled'}" ${link}${excerpt ? `\n\n${excerpt}` : ''}`;
|
|
56
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure NotesQ link helpers (AQ-9D P5). The frontend keeps its own copy and a
|
|
3
|
+
* parity test imports this one, so behaviour here is the contract.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Blank (same length) every region where a title must not be linked: front
|
|
7
|
+
// matter, fenced code, inline code, [[wikilinks]] and [text](url) links.
|
|
8
|
+
function maskNonProse(md) {
|
|
9
|
+
const blank = (m) => m.replace(/[^\n]/g, ' ');
|
|
10
|
+
return String(md || '')
|
|
11
|
+
.replace(/^---\r?\n[\s\S]*?\r?\n---(?=\r?\n|$)/, blank)
|
|
12
|
+
.replace(/(```|~~~)[\s\S]*?(\1|$)/g, blank)
|
|
13
|
+
.replace(/`[^`\n]*`/g, blank)
|
|
14
|
+
.replace(/!?\[\[[^\]\n]*\]\]/g, blank)
|
|
15
|
+
.replace(/!?\[[^\]\n]*\]\([^)\n]*\)/g, blank)
|
|
16
|
+
.replace(/^#[ \t]+[^\n]*/m, blank); // the note's own title heading
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const escapeRe = (s) => String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Replace the first plain-text, case-insensitive, word-bounded occurrence of
|
|
23
|
+
* `title` with `[[title]]` (the title exactly as given).
|
|
24
|
+
* @returns {{markdown: string, changed: boolean}}
|
|
25
|
+
*/
|
|
26
|
+
export function linkMention(markdown, title) {
|
|
27
|
+
const md = String(markdown ?? '');
|
|
28
|
+
const t = String(title ?? '').trim();
|
|
29
|
+
if (!t) return { markdown: md, changed: false };
|
|
30
|
+
const re = new RegExp(`(?<![\\p{L}\\p{N}_])${escapeRe(t)}(?![\\p{L}\\p{N}_])`, 'iu');
|
|
31
|
+
const m = re.exec(maskNonProse(md));
|
|
32
|
+
if (!m) return { markdown: md, changed: false };
|
|
33
|
+
return { markdown: `${md.slice(0, m.index)}[[${t}]]${md.slice(m.index + m[0].length)}`, changed: true };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Append `- [[title]]` to the `## See also` section, creating it at the end if absent. */
|
|
37
|
+
export function addSeeAlso(markdown, title) {
|
|
38
|
+
const md = String(markdown ?? '');
|
|
39
|
+
const link = `[[${title}]]`;
|
|
40
|
+
const item = `- ${link}`;
|
|
41
|
+
const lines = md.split('\n');
|
|
42
|
+
// Lines inside fenced code never count as headings.
|
|
43
|
+
const fenced = [];
|
|
44
|
+
let open = null;
|
|
45
|
+
for (const l of lines) {
|
|
46
|
+
const f = /^\s*(```|~~~)/.exec(l);
|
|
47
|
+
if (open) { fenced.push(true); if (f && f[1] === open) open = null; } else if (f) { fenced.push(true); open = f[1]; } else fenced.push(false);
|
|
48
|
+
}
|
|
49
|
+
const start = lines.findIndex((l, i) => !fenced[i] && /^## See also\s*$/.test(l));
|
|
50
|
+
if (start >= 0) {
|
|
51
|
+
let end = lines.length;
|
|
52
|
+
for (let i = start + 1; i < lines.length; i++) if (!fenced[i] && /^#{1,6}\s/.test(lines[i])) { end = i; break; }
|
|
53
|
+
if (lines.slice(start + 1, end).some((l) => l.includes(link))) return md;
|
|
54
|
+
let last = end - 1;
|
|
55
|
+
while (last > start && lines[last].trim() === '') last--;
|
|
56
|
+
lines.splice(last + 1, 0, item);
|
|
57
|
+
return lines.join('\n');
|
|
58
|
+
}
|
|
59
|
+
return `${md.replace(/\n+$/, '')}\n\n## See also\n\n${item}\n`;
|
|
60
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Notification commands
|
|
3
|
+
*
|
|
4
|
+
* quox notification channels list|set
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { formatOutput, formatDetail } from '../core/output.js';
|
|
8
|
+
import { resolveEndpoint } from '../api/endpoints.js';
|
|
9
|
+
|
|
10
|
+
export function registerNotificationCommands(program, ctx) {
|
|
11
|
+
const { client, auth } = ctx;
|
|
12
|
+
|
|
13
|
+
const notification = program
|
|
14
|
+
.command('notification')
|
|
15
|
+
.description('Manage notification settings');
|
|
16
|
+
|
|
17
|
+
const channels = notification
|
|
18
|
+
.command('channels')
|
|
19
|
+
.description('Notification channel management');
|
|
20
|
+
|
|
21
|
+
channels
|
|
22
|
+
.command('list')
|
|
23
|
+
.description('List notification channels')
|
|
24
|
+
.action(async () => {
|
|
25
|
+
auth.requireAuth();
|
|
26
|
+
const data = await client.get(resolveEndpoint('users.me.notification-channels.list').path, { service: resolveEndpoint('users.me.notification-channels.list').service });
|
|
27
|
+
const chans = data.channels || data || [];
|
|
28
|
+
formatOutput({
|
|
29
|
+
// fleet #392: rows carry channel_type and a nested config object;
|
|
30
|
+
// `type` and a flat `destination` don't exist on them.
|
|
31
|
+
columns: ['type', 'enabled', 'destination'],
|
|
32
|
+
headers: ['Type', 'Enabled', 'Destination'],
|
|
33
|
+
rows: chans.map(c => ({
|
|
34
|
+
type: c.channel_type ?? c.type,
|
|
35
|
+
enabled: c.enabled ? 'Yes' : 'No',
|
|
36
|
+
destination: c.config?.destination ?? c.destination ?? '-',
|
|
37
|
+
})),
|
|
38
|
+
}, { format: program.opts().output });
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
channels
|
|
42
|
+
.command('set')
|
|
43
|
+
.description('Configure a notification channel')
|
|
44
|
+
.requiredOption('--type <type>', 'Channel type (email|slack|telegram|...)')
|
|
45
|
+
.option('--enable', 'Enable channel')
|
|
46
|
+
.option('--disable', 'Disable channel')
|
|
47
|
+
.option('--destination <dest>', 'Channel destination (email, webhook URL, chat ID)')
|
|
48
|
+
.action(async (opts) => {
|
|
49
|
+
auth.requireAuth();
|
|
50
|
+
const body = {};
|
|
51
|
+
if (opts.enable) body.enabled = true;
|
|
52
|
+
if (opts.disable) body.enabled = false;
|
|
53
|
+
if (opts.destination) body.config = { destination: opts.destination };
|
|
54
|
+
const data = await client.put(resolveEndpoint('users.me.notification-channels.update', { type: opts.type }).path, { service: resolveEndpoint('users.me.notification-channels.update', { type: opts.type }).service, body });
|
|
55
|
+
formatDetail(data, { format: program.opts().output, title: 'Channel Updated' });
|
|
56
|
+
});
|
|
57
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `quox nucleus` — QUOXIFY PASS 2 (DEVSUITE phase 15).
|
|
3
|
+
*
|
|
4
|
+
* RepoBrain has been the cheap way to answer structural questions about the
|
|
5
|
+
* Quox repos for a while (targeted snippets instead of whole files), but it
|
|
6
|
+
* was reachable only through its own standalone binary and, for agents, a raw
|
|
7
|
+
* tool. The quoxification audit called that out twice: no unified `quox`
|
|
8
|
+
* verb (S-Q3), and an ungoverned side-path where the agent tool hit the
|
|
9
|
+
* engine directly while the CLI did not (S-Q5).
|
|
10
|
+
*
|
|
11
|
+
* This verb closes the CLI half, and closes it the RIGHT way: it goes through
|
|
12
|
+
* the collector's governed /api/v1/tools/execute, so the query is org-scoped
|
|
13
|
+
* and audited exactly like every other tool call, rather than reaching the
|
|
14
|
+
* engine behind the collector's back.
|
|
15
|
+
*
|
|
16
|
+
* Announce convention: RepoBrain surfaces announce themselves with 🧠 (see
|
|
17
|
+
* the icon registry in CLAUDE.md), so a query is visible in a wall of
|
|
18
|
+
* terminal output.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { formatDetail, printWarning } from '../core/output.js';
|
|
22
|
+
import { line as glyphLine, printEvent } from '../core/quoxglyph.js';
|
|
23
|
+
import { resolveEndpoint } from '../api/endpoints.js';
|
|
24
|
+
import { resolveOrgId } from './helpers.js';
|
|
25
|
+
|
|
26
|
+
export function registerNucleusCommands(program, { client, auth }) {
|
|
27
|
+
const nucleus = program
|
|
28
|
+
.command('nucleus')
|
|
29
|
+
.description('Ask the codebase a question (RepoBrain, governed and audited)');
|
|
30
|
+
|
|
31
|
+
nucleus
|
|
32
|
+
.command('query', { isDefault: true })
|
|
33
|
+
.description('Query the codebase and get cited file:line snippets')
|
|
34
|
+
.argument('<question...>', 'the question, e.g. "where is the slot bridge verified?"')
|
|
35
|
+
.option('-t, --target <repo>', 'repo to search (default: quox-dashboard)')
|
|
36
|
+
.option('-n, --limit <n>', 'max snippets to return', '8')
|
|
37
|
+
.action(async (questionParts, options) => {
|
|
38
|
+
auth.requireAuth();
|
|
39
|
+
const question = questionParts.join(' ').trim();
|
|
40
|
+
if (!question) throw new Error('ask something: quox nucleus "where is X?"');
|
|
41
|
+
|
|
42
|
+
printEvent(glyphLine('repobrain', `query "${question}"`));
|
|
43
|
+
const started = Date.now();
|
|
44
|
+
|
|
45
|
+
const ep = resolveEndpoint('v1.tools.execute');
|
|
46
|
+
const body = {
|
|
47
|
+
tool_name: 'codebase_query',
|
|
48
|
+
input: {
|
|
49
|
+
query: question,
|
|
50
|
+
...(options.target ? { repo: options.target } : {}),
|
|
51
|
+
...(options.limit ? { limit: parseInt(options.limit, 10) || 8 } : {}),
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
const orgId = resolveOrgId(auth, program);
|
|
55
|
+
if (orgId) body.org_id = orgId;
|
|
56
|
+
|
|
57
|
+
const data = await client.post(ep.path, { service: ep.service, body, timeout: 60000 });
|
|
58
|
+
const ms = Date.now() - started;
|
|
59
|
+
|
|
60
|
+
// The tool result shape varies by engine version, so normalise rather
|
|
61
|
+
// than assuming: an unexpected shape must still print SOMETHING true.
|
|
62
|
+
const result = data && (data.result || data.output || data);
|
|
63
|
+
const results = Array.isArray(result?.results) ? result.results
|
|
64
|
+
: Array.isArray(result?.snippets) ? result.snippets
|
|
65
|
+
: Array.isArray(result) ? result
|
|
66
|
+
: null;
|
|
67
|
+
|
|
68
|
+
if (program.opts().output === 'json') {
|
|
69
|
+
formatDetail(data, { format: 'json' });
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!results || results.length === 0) {
|
|
74
|
+
printEvent(glyphLine('repobrain', `query "${question}"`, { result: '0 hits', ms }));
|
|
75
|
+
printWarning('Nothing matched. Try naming a concrete symbol, filename or behaviour rather than a general description.');
|
|
76
|
+
// Still show the raw payload when the shape was unexpected, so a
|
|
77
|
+
// parsing gap here never looks like an empty index.
|
|
78
|
+
if (result && !results) formatDetail(result, { format: program.opts().output });
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
printEvent(glyphLine('repobrain', `query "${question}"`, { result: `${results.length} hits`, ms }));
|
|
83
|
+
for (const r of results) {
|
|
84
|
+
const loc = r.file || r.path || r.source || '(unknown file)';
|
|
85
|
+
const line = r.line || r.start_line || null;
|
|
86
|
+
console.log('');
|
|
87
|
+
console.log(` ${loc}${line ? `:${line}` : ''}`);
|
|
88
|
+
const snippet = r.snippet || r.content || r.text || '';
|
|
89
|
+
for (const l of String(snippet).split('\n').slice(0, 20)) console.log(` ${l}`);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Objective commands: create, list, get, start, approve, plus the HITL
|
|
3
|
+
* checkpoint verbs (checkpoints, approve-checkpoint, deny-checkpoint).
|
|
4
|
+
*
|
|
5
|
+
* Objectives are served by the tasks service (port 3104), not the auth
|
|
6
|
+
* service. Org/user scoping comes from the JWT (org_id/sub claims) rather
|
|
7
|
+
* than a URL path segment — see services/tasks/middleware/auth.js.
|
|
8
|
+
*
|
|
9
|
+
* Two approval gates exist and must not be confused:
|
|
10
|
+
* `approve` resolves a decomposed objective's DAG plan.
|
|
11
|
+
* `approve-checkpoint` resolves one HITL checkpoint (pre-start gate or a
|
|
12
|
+
* live loop gate: budget, risk, interval).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { formatOutput, formatDetail, printSuccess } from '../core/output.js';
|
|
16
|
+
import { parseJSON } from './helpers.js';
|
|
17
|
+
import { resolveEndpoint } from '../api/endpoints.js';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Format a timestamp as a short relative-time string ("5m ago").
|
|
21
|
+
*
|
|
22
|
+
* Checkpoint rows carry SQLite `datetime('now')` values ("2026-08-14
|
|
23
|
+
* 04:00:00"), which are UTC but have no zone marker, so Node would read them
|
|
24
|
+
* as local time and skew the age by the offset. Normalise that shape to ISO
|
|
25
|
+
* UTC first; anything already ISO passes straight through.
|
|
26
|
+
*/
|
|
27
|
+
function relativeTime(ts) {
|
|
28
|
+
if (!ts) return '—';
|
|
29
|
+
const iso = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(ts)
|
|
30
|
+
? `${ts.replace(' ', 'T')}Z`
|
|
31
|
+
: ts;
|
|
32
|
+
const then = new Date(iso).getTime();
|
|
33
|
+
if (Number.isNaN(then)) return '—';
|
|
34
|
+
const diffSec = Math.max(0, Math.floor((Date.now() - then) / 1000));
|
|
35
|
+
if (diffSec < 60) return `${diffSec}s ago`;
|
|
36
|
+
const diffMin = Math.floor(diffSec / 60);
|
|
37
|
+
if (diffMin < 60) return `${diffMin}m ago`;
|
|
38
|
+
const diffHr = Math.floor(diffMin / 60);
|
|
39
|
+
if (diffHr < 24) return `${diffHr}h ago`;
|
|
40
|
+
const diffDay = Math.floor(diffHr / 24);
|
|
41
|
+
return `${diffDay}d ago`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Accumulate repeated `--capability-scope` flags into one array of raw
|
|
46
|
+
* strings. Each value may itself be a comma-separated list; splitting and
|
|
47
|
+
* trimming happen in the action, alongside the other body fields.
|
|
48
|
+
*/
|
|
49
|
+
function collectScope(value, previous) {
|
|
50
|
+
return (previous || []).concat(value);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function registerObjectiveCommands(program, ctx) {
|
|
54
|
+
const { client, auth } = ctx;
|
|
55
|
+
|
|
56
|
+
const objective = program
|
|
57
|
+
.command('objective')
|
|
58
|
+
.description('Manage objectives (autonomous multi-step goals)');
|
|
59
|
+
|
|
60
|
+
objective
|
|
61
|
+
.command('create')
|
|
62
|
+
.description('Create a new objective (DAG plan by default, or a QLoop loop with --execution-mode loop)')
|
|
63
|
+
.argument('<goal>', 'objective title / goal statement')
|
|
64
|
+
.option('--description <text>', 'longer description')
|
|
65
|
+
.option('--team <id>', 'team ID to run the objective under')
|
|
66
|
+
.option('--max-tokens <n>', 'max token budget')
|
|
67
|
+
.option('--max-tool-calls <n>', 'max tool call budget')
|
|
68
|
+
.option('--max-duration-ms <n>', 'max wall-clock duration in ms')
|
|
69
|
+
.option('--require-approval', 'require approval before starting')
|
|
70
|
+
.option('--checkpoint', 'checkpoint after each task')
|
|
71
|
+
.option('--goal-condition <text>', 're-observe loop exit condition')
|
|
72
|
+
.option('--max-iterations <n>', 'max re-observe loop iterations')
|
|
73
|
+
.option('--metadata <json>', 'arbitrary metadata as JSON')
|
|
74
|
+
.option('--execution-mode <mode>', 'dag|loop. A loop runs a QLoop backend instead of a reviewable DAG plan, so it ALWAYS requires a before_start approval (see `quox objective checkpoints` and `approve-checkpoint`)')
|
|
75
|
+
.option('--loop-backend <backend>', 'claude|codex, loop mode only (default claude)')
|
|
76
|
+
.option('--capability-scope <tools>', 'comma-separated tool names the loop may call (repeatable); empty scope escalates every tool', collectScope)
|
|
77
|
+
.option('--system-prompt <text>', 'per-objective system prompt, carried verbatim to the run')
|
|
78
|
+
.action(async (goal, options) => {
|
|
79
|
+
auth.requireAuth();
|
|
80
|
+
const body = { title: goal };
|
|
81
|
+
if (options.description) body.description = options.description;
|
|
82
|
+
if (options.team) body.team_id = options.team;
|
|
83
|
+
if (options.maxTokens) body.max_tokens = Number(options.maxTokens);
|
|
84
|
+
if (options.maxToolCalls) body.max_tool_calls = Number(options.maxToolCalls);
|
|
85
|
+
if (options.maxDurationMs) body.max_duration_ms = Number(options.maxDurationMs);
|
|
86
|
+
if (options.requireApproval) body.require_approval_before_start = true;
|
|
87
|
+
if (options.checkpoint) body.checkpoint_after_each_task = true;
|
|
88
|
+
if (options.goalCondition) body.goal_condition = options.goalCondition;
|
|
89
|
+
if (options.maxIterations) body.max_iterations = Number(options.maxIterations);
|
|
90
|
+
if (options.metadata) body.metadata = parseJSON(options.metadata, '--metadata');
|
|
91
|
+
if (options.executionMode) body.execution_mode = options.executionMode;
|
|
92
|
+
if (options.loopBackend) body.loop_backend = options.loopBackend;
|
|
93
|
+
if (options.capabilityScope) {
|
|
94
|
+
const scope = options.capabilityScope
|
|
95
|
+
.flatMap((raw) => raw.split(','))
|
|
96
|
+
.map((t) => t.trim())
|
|
97
|
+
.filter(Boolean);
|
|
98
|
+
if (scope.length > 0) body.capability_scope = scope;
|
|
99
|
+
}
|
|
100
|
+
if (options.systemPrompt) body.system_prompt = options.systemPrompt;
|
|
101
|
+
|
|
102
|
+
const data = await client.post(resolveEndpoint('objectives.create').path, { service: resolveEndpoint('objectives.create').service, body });
|
|
103
|
+
printSuccess(`Objective created: ${data.id}`);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
objective
|
|
107
|
+
.command('list')
|
|
108
|
+
.description('List objectives')
|
|
109
|
+
.option('--status <status>', 'filter by status')
|
|
110
|
+
.option('--team <id>', 'filter by team ID')
|
|
111
|
+
.option('--limit <n>', 'max results', '50')
|
|
112
|
+
.action(async (options) => {
|
|
113
|
+
auth.requireAuth();
|
|
114
|
+
const query = { limit: options.limit };
|
|
115
|
+
if (options.status) query.status = options.status;
|
|
116
|
+
if (options.team) query.team_id = options.team;
|
|
117
|
+
const data = await client.get(resolveEndpoint('objectives.list').path, { service: resolveEndpoint('objectives.list').service, query });
|
|
118
|
+
formatOutput({
|
|
119
|
+
columns: ['id', 'title', 'status', 'team_id', 'created_at'],
|
|
120
|
+
headers: ['ID', 'Title', 'Status', 'Team', 'Created'],
|
|
121
|
+
rows: data.objectives || data,
|
|
122
|
+
}, { format: program.opts().output });
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
objective
|
|
126
|
+
.command('get')
|
|
127
|
+
.description('Get objective details (tasks, checkpoints, DAG)')
|
|
128
|
+
.argument('<id>', 'objective ID')
|
|
129
|
+
.action(async (id) => {
|
|
130
|
+
auth.requireAuth();
|
|
131
|
+
const data = await client.get(resolveEndpoint('objectives.get', { id }).path, { service: resolveEndpoint('objectives.get', { id }).service });
|
|
132
|
+
formatDetail(data, { format: program.opts().output, title: data.title });
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
objective
|
|
136
|
+
.command('start')
|
|
137
|
+
.description('Start an objective (decompose into tasks)')
|
|
138
|
+
.argument('<id>', 'objective ID')
|
|
139
|
+
.option('--skip-checkpoint', 'bypass pre-start approval checkpoint')
|
|
140
|
+
.action(async (id, options) => {
|
|
141
|
+
auth.requireAuth();
|
|
142
|
+
const body = {};
|
|
143
|
+
if (options.skipCheckpoint) body.skip_checkpoint = true;
|
|
144
|
+
const data = await client.post(resolveEndpoint('objectives.start', { id }).path, { service: resolveEndpoint('objectives.start', { id }).service, body });
|
|
145
|
+
printSuccess(`Objective ${id}: ${data.status}${data.message ? ` — ${data.message}` : ''}`);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
objective
|
|
149
|
+
.command('approve')
|
|
150
|
+
.description('Approve a decomposed objective\'s plan (starts execution)')
|
|
151
|
+
.argument('<id>', 'objective ID')
|
|
152
|
+
.action(async (id) => {
|
|
153
|
+
auth.requireAuth();
|
|
154
|
+
const data = await client.post(resolveEndpoint('objectives.approve-plan', { id }).path, { service: resolveEndpoint('objectives.approve-plan', { id }).service });
|
|
155
|
+
printSuccess(`Objective ${id}: ${data.status}${data.message ? ` — ${data.message}` : ''}`);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
objective
|
|
159
|
+
.command('checkpoints')
|
|
160
|
+
.description('List pending HITL checkpoints across the org (approval queue)')
|
|
161
|
+
.option('--include-reviewed', 'include checkpoints that have already been reviewed')
|
|
162
|
+
.option('--limit <n>', 'max results', '100')
|
|
163
|
+
.action(async (options) => {
|
|
164
|
+
auth.requireAuth();
|
|
165
|
+
const query = { limit: options.limit };
|
|
166
|
+
if (options.includeReviewed) query.include_reviewed = 'true';
|
|
167
|
+
const data = await client.get(resolveEndpoint('objectives.checkpoints-pending').path, { service: resolveEndpoint('objectives.checkpoints-pending').service, query });
|
|
168
|
+
const rows = (data.checkpoints || data || []).map((cp) => ({
|
|
169
|
+
id: cp.id,
|
|
170
|
+
objective_title: cp.objectiveTitle,
|
|
171
|
+
checkpoint_type: cp.checkpointType,
|
|
172
|
+
age: relativeTime(cp.createdAt),
|
|
173
|
+
title: cp.title,
|
|
174
|
+
}));
|
|
175
|
+
formatOutput({
|
|
176
|
+
columns: ['id', 'objective_title', 'checkpoint_type', 'age', 'title'],
|
|
177
|
+
headers: ['Checkpoint', 'Objective', 'Type', 'Age', 'Title'],
|
|
178
|
+
rows,
|
|
179
|
+
}, { format: program.opts().output, emptyMessage: 'No pending checkpoints.' });
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
objective
|
|
183
|
+
.command('approve-checkpoint')
|
|
184
|
+
.description('Approve one HITL checkpoint (not the DAG plan, see `approve`)')
|
|
185
|
+
.argument('<objectiveId>', 'objective ID')
|
|
186
|
+
.argument('<checkpointId>', 'checkpoint ID')
|
|
187
|
+
.option('--notes <text>', 'review notes recorded against the decision')
|
|
188
|
+
.action(async (objectiveId, checkpointId, options) => {
|
|
189
|
+
auth.requireAuth();
|
|
190
|
+
const body = {};
|
|
191
|
+
if (options.notes) body.notes = options.notes;
|
|
192
|
+
const params = { id: objectiveId, checkpointId };
|
|
193
|
+
const data = await client.post(resolveEndpoint('objectives.checkpoint-approve', params).path, { service: resolveEndpoint('objectives.checkpoint-approve', params).service, body });
|
|
194
|
+
printSuccess(`Checkpoint ${checkpointId}: ${data.status}${data.objective_status ? ` (objective ${data.objective_status})` : ''}`);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
objective
|
|
198
|
+
.command('deny-checkpoint')
|
|
199
|
+
.description('Reject one HITL checkpoint (not the DAG plan, see `approve`)')
|
|
200
|
+
.argument('<objectiveId>', 'objective ID')
|
|
201
|
+
.argument('<checkpointId>', 'checkpoint ID')
|
|
202
|
+
.option('--notes <text>', 'review notes recorded against the decision')
|
|
203
|
+
.action(async (objectiveId, checkpointId, options) => {
|
|
204
|
+
auth.requireAuth();
|
|
205
|
+
const body = {};
|
|
206
|
+
if (options.notes) body.notes = options.notes;
|
|
207
|
+
const params = { id: objectiveId, checkpointId };
|
|
208
|
+
const data = await client.post(resolveEndpoint('objectives.checkpoint-reject', params).path, { service: resolveEndpoint('objectives.checkpoint-reject', params).service, body });
|
|
209
|
+
printSuccess(`Checkpoint ${checkpointId}: ${data.status}${data.objective_status ? ` (objective ${data.objective_status})` : ''}`);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
objective
|
|
213
|
+
.command('halt')
|
|
214
|
+
.description('Halt a running QLoop loop ON DEMAND (unconditional human stop; no pending gate needed). Fires the same signed-kill as a gate deny.')
|
|
215
|
+
.argument('<objectiveId>', 'loop objective ID')
|
|
216
|
+
.option('--notes <text>', 'reason recorded against the halt')
|
|
217
|
+
.action(async (objectiveId, options) => {
|
|
218
|
+
auth.requireAuth();
|
|
219
|
+
const body = {};
|
|
220
|
+
if (options.notes) body.notes = options.notes;
|
|
221
|
+
const params = { id: objectiveId };
|
|
222
|
+
const data = await client.post(resolveEndpoint('objectives.halt', params).path, { service: resolveEndpoint('objectives.halt', params).service, body });
|
|
223
|
+
if (data.status === 'halted') {
|
|
224
|
+
printSuccess(`Loop ${objectiveId} halted (interrupted=${data.interrupted}, objective ${data.objective_status})`);
|
|
225
|
+
} else {
|
|
226
|
+
printSuccess(`Loop ${objectiveId}: ${data.status}${data.reason ? ` (${data.reason})` : ''}`);
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
}
|