quoxcli 0.3.1 → 0.5.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/CHANGELOG.md +61 -0
- package/README.md +2 -2
- package/bin/quox.js +19 -1
- package/bin/quoxon.js +6 -1
- package/lib/api/endpoints/actions.js +15 -0
- package/lib/api/endpoints/admin.js +27 -0
- package/lib/api/endpoints/agent-attach.js +7 -0
- package/lib/api/endpoints/blog.js +16 -0
- package/lib/api/endpoints/chat.js +7 -0
- package/lib/api/endpoints/compliance.js +25 -0
- package/lib/api/endpoints/contactsq.js +4 -0
- package/lib/api/endpoints/devServerRegistry.js +25 -0
- package/lib/api/endpoints/engagement.js +26 -0
- package/lib/api/endpoints/evidence.js +20 -0
- package/lib/api/endpoints/hat.js +14 -0
- package/lib/api/endpoints/inbox.js +6 -0
- package/lib/api/endpoints/inboxq.js +19 -0
- package/lib/api/endpoints/index.js +14 -0
- package/lib/api/endpoints/loops.js +26 -0
- package/lib/api/endpoints/mon.js +295 -0
- package/lib/api/endpoints/objectives.js +6 -0
- package/lib/api/endpoints/quoxseo.js +6 -0
- package/lib/api/endpoints/server.js +21 -0
- package/lib/api/endpoints/setup.js +6 -0
- package/lib/api/endpoints/tools.js +13 -0
- package/lib/api/endpoints/v1.js +47 -0
- package/lib/commands/actions.js +81 -11
- package/lib/commands/admin.js +35 -0
- package/lib/commands/agent.js +11 -4
- package/lib/commands/agentAttach.js +61 -7
- package/lib/commands/approvals.js +198 -3
- package/lib/commands/ask.js +4 -2
- package/lib/commands/auth.js +64 -11
- package/lib/commands/backup.js +11 -4
- package/lib/commands/blog.js +110 -0
- package/lib/commands/capabilities.js +3 -2
- package/lib/commands/chat.js +142 -28
- package/lib/commands/chatCompanion.js +27 -0
- package/lib/commands/chatState.js +45 -0
- package/lib/commands/claude.js +7 -33
- package/lib/commands/compliance.js +109 -0
- package/lib/commands/connect.js +69 -0
- package/lib/commands/contacts.js +85 -4
- package/lib/commands/contactsSendAt.js +21 -0
- package/lib/commands/dev.js +6 -8
- package/lib/commands/doctor.js +22 -3
- package/lib/commands/engagement.js +90 -0
- package/lib/commands/evidence.js +155 -0
- package/lib/commands/ext.js +23 -0
- package/lib/commands/fleet.js +25 -0
- package/lib/commands/gripe.js +24 -13
- package/lib/commands/hat.js +76 -0
- package/lib/commands/image.js +68 -0
- package/lib/commands/inbox.js +37 -3
- package/lib/commands/inboxq.js +147 -0
- package/lib/commands/kuma.js +1123 -0
- package/lib/commands/kumaSla.js +174 -0
- package/lib/commands/licence.js +39 -1
- package/lib/commands/licenceLinkView.js +311 -0
- package/lib/commands/linkedin.js +19 -0
- package/lib/commands/loop.js +318 -0
- package/lib/commands/lucid.js +1 -0
- package/lib/commands/mail.js +161 -20
- package/lib/commands/memory.js +68 -4
- package/lib/commands/memorySync.js +158 -0
- package/lib/commands/mon.js +55 -144
- package/lib/commands/notes.js +29 -7
- package/lib/commands/notesSeal.js +259 -0
- package/lib/commands/objective.js +53 -3
- package/lib/commands/offload.js +2 -3
- package/lib/commands/org.js +21 -4
- package/lib/commands/plan.js +17 -0
- package/lib/commands/provider.js +87 -8
- package/lib/commands/qlarity.js +32 -1
- package/lib/commands/quoxseo.js +31 -0
- package/lib/commands/research.js +79 -0
- package/lib/commands/rows.js +9 -0
- package/lib/commands/run.js +20 -2
- package/lib/commands/scope.js +2 -2
- package/lib/commands/server.js +150 -0
- package/lib/commands/setup.js +127 -75
- package/lib/commands/shortcut.js +41 -0
- package/lib/commands/skills.js +24 -5
- package/lib/commands/slot.js +49 -9
- package/lib/commands/status.js +92 -44
- package/lib/commands/tool.js +30 -0
- package/lib/commands/vault.js +6 -3
- package/lib/commands/vm.js +2 -2
- package/lib/commands/walkthrough.js +14 -2
- package/lib/commands/watch.js +11 -2
- package/lib/commands/wazuh.js +824 -0
- package/lib/commands/wazuhIntake.js +317 -0
- package/lib/commands/wazuhReport.js +192 -0
- package/lib/commands/workflow.js +47 -4
- package/lib/commands/x.js +50 -0
- package/lib/commands/youtube.js +172 -0
- package/lib/core/actionItems.js +115 -0
- package/lib/core/agentAlias.js +2 -0
- package/lib/core/agentFamilies.js +7 -0
- package/lib/core/announce.js +86 -0
- package/lib/core/auth.js +20 -2
- package/lib/core/callMe.js +118 -0
- package/lib/core/client.js +37 -3
- package/lib/core/commandRegistrars.js +17 -0
- package/lib/core/config.js +29 -3
- package/lib/core/confirm.js +68 -7
- package/lib/core/crossOrgApprovals.js +69 -0
- package/lib/core/errors.js +52 -1
- package/lib/core/findingReconcile.js +90 -0
- package/lib/core/global-flags.js +1 -1
- package/lib/core/governedSession.js +136 -8
- package/lib/core/inboxResolution.js +3 -2
- package/lib/core/liveSessionsReport.js +1 -1
- package/lib/core/loginGuard.js +73 -0
- package/lib/core/loopGlyph.js +97 -0
- package/lib/core/offload.js +73 -7
- package/lib/core/org-override.js +5 -1
- package/lib/core/output.js +8 -8
- package/lib/core/profileTrust.js +182 -0
- package/lib/core/quoxglyph.js +22 -1
- package/lib/core/router.js +14 -0
- package/lib/core/sessionHeartbeat.js +12 -8
- package/lib/core/shortcut.js +83 -0
- package/lib/core/stepUpPrompt.js +99 -0
- package/lib/core/version.js +1 -1
- package/lib/core/vm.js +5 -0
- package/lib/core/walkthrough.js +9 -5
- package/lib/dragon/companionView.js +34 -0
- package/lib/dragon/frames.js +118 -0
- package/lib/dragon/jobs.js +123 -0
- package/lib/dragon/renderWorker.js +15 -0
- package/lib/dragon/select.js +72 -0
- package/lib/evidence/bundleExport.js +47 -0
- package/lib/evidence/bundleVerify.js +77 -0
- package/lib/quoxon/EXTENSIONS.md +146 -0
- package/lib/quoxon/api.js +364 -27
- package/lib/quoxon/app.js +1629 -183
- package/lib/quoxon/approvalBatch.js +117 -0
- package/lib/quoxon/approvalFacts.js +64 -8
- package/lib/quoxon/approvalSentence.js +48 -0
- package/lib/quoxon/approvalsView.js +124 -7
- package/lib/quoxon/autoOpen.js +94 -0
- package/lib/quoxon/backgroundWork.js +59 -0
- package/lib/quoxon/bang.js +1 -1
- package/lib/quoxon/capabilities.js +7 -2
- package/lib/quoxon/commandShort.js +22 -3
- package/lib/quoxon/commands.js +110 -15
- package/lib/quoxon/connect.js +5 -3
- package/lib/quoxon/countdownLine.js +10 -1
- package/lib/quoxon/deck.js +30 -0
- package/lib/quoxon/deckLazy.js +58 -0
- package/lib/quoxon/demo.autoplay.js +1 -1
- package/lib/quoxon/demo.js +3 -1
- package/lib/quoxon/destructive.js +35 -0
- package/lib/quoxon/dividerAocl.js +4 -3
- package/lib/quoxon/dividerRule.js +3 -3
- package/lib/quoxon/dnd.js +71 -0
- package/lib/quoxon/dragonCompanion.js +285 -0
- package/lib/quoxon/dragonModes.js +1 -0
- package/lib/quoxon/dragonMoments.js +160 -0
- package/lib/quoxon/dragonReads.js +68 -0
- package/lib/quoxon/dragonView.js +2 -2
- package/lib/quoxon/echo.js +1 -0
- package/lib/quoxon/events.js +67 -10
- package/lib/quoxon/extensions.js +636 -0
- package/lib/quoxon/gripeQueue.js +135 -0
- package/lib/quoxon/inboxView.js +182 -0
- package/lib/quoxon/installCli.js +79 -0
- package/lib/quoxon/keys.js +4 -4
- package/lib/quoxon/loopCard.js +460 -0
- package/lib/quoxon/loopCards.js +189 -0
- package/lib/quoxon/loopStream.js +98 -0
- package/lib/quoxon/mailView.js +71 -0
- package/lib/quoxon/main.js +29 -5
- package/lib/quoxon/markdown.js +1 -1
- package/lib/quoxon/model.js +15 -0
- package/lib/quoxon/mouse.js +12 -1
- package/lib/quoxon/neverAnswered.js +20 -0
- package/lib/quoxon/objectiveOffer.js +82 -0
- package/lib/quoxon/objects/away.js +43 -5
- package/lib/quoxon/objects/index.js +22 -4
- package/lib/quoxon/objects/model.js +33 -0
- package/lib/quoxon/objects/proof.js +12 -4
- package/lib/quoxon/onboarding/connectServer.js +162 -0
- package/lib/quoxon/onboarding/copy.js +6 -0
- package/lib/quoxon/onboarding/index.js +5 -43
- package/lib/quoxon/onboarding/probe.js +20 -1
- package/lib/quoxon/onboarding/shortcutOffer.js +28 -0
- package/lib/quoxon/onboarding/view.js +16 -3
- package/lib/quoxon/orgSwitch.js +49 -1
- package/lib/quoxon/planSteps.js +51 -0
- package/lib/quoxon/print.js +8 -3
- package/lib/quoxon/profileSwitch.js +7 -0
- package/lib/quoxon/promptHistory.js +3 -1
- package/lib/quoxon/qdragonFly.js +7 -6
- package/lib/quoxon/qmotion.js +17 -0
- package/lib/quoxon/render.js +187 -41
- package/lib/quoxon/roster.js +36 -17
- package/lib/quoxon/rosterSessions.js +44 -0
- package/lib/quoxon/rows.js +244 -1
- package/lib/quoxon/screenContext.js +1 -1
- package/lib/quoxon/secretShape.js +86 -0
- package/lib/quoxon/serverChip.js +42 -0
- package/lib/quoxon/session.js +10 -0
- package/lib/quoxon/slash/agents.js +2 -1
- package/lib/quoxon/slash/argSources.js +9 -2
- package/lib/quoxon/slash/args.js +4 -2
- package/lib/quoxon/slash/broadcast.js +18 -5
- package/lib/quoxon/slash/broadcastRun.js +13 -2
- package/lib/quoxon/slash/cliCommand.js +42 -1
- package/lib/quoxon/slash/cliLeaves.json +139 -2
- package/lib/quoxon/slash/evidenceCmd.js +34 -0
- package/lib/quoxon/slash/hatWalk.js +1 -1
- package/lib/quoxon/slash/palette.js +23 -2
- package/lib/quoxon/slash/paletteController.js +10 -4
- package/lib/quoxon/slash/qpeCmd.js +35 -0
- package/lib/quoxon/slash/restart.js +31 -4
- package/lib/quoxon/slash/resume.js +18 -1
- package/lib/quoxon/slash/stepUp.js +5 -5
- package/lib/quoxon/slash/stepUpCmds.js +74 -1
- package/lib/quoxon/slash/todoCmd.js +122 -0
- package/lib/quoxon/slashSpinner.js +35 -0
- package/lib/quoxon/slotAttach.js +8 -0
- package/lib/quoxon/slotChat.js +17 -0
- package/lib/quoxon/slotFormat.js +1 -1
- package/lib/quoxon/slotOutbox.js +113 -0
- package/lib/quoxon/suggestCommand.js +34 -3
- package/lib/quoxon/term.js +25 -1
- package/lib/quoxon/toolCards.js +30 -0
- package/lib/quoxon/transcript.js +2 -1
- package/lib/quoxon/welcome.js +1 -1
- package/lib/quoxon/whatsnew.js +1 -1
- package/lib/quoxon/whatsnew.jsonl +81 -0
- package/lib/quoxon/windowOrg.js +11 -1
- package/lib/server/devServers.js +137 -0
- package/lib/server/installFlow.js +168 -0
- package/lib/server/status.js +15 -1
- package/lib/toolOutputShape.js +55 -0
- package/package.json +4 -2
- package/vendor/dragonlab/SOURCE +4 -0
- package/vendor/dragonlab/design/cipher/v1/cipher-master.svg +46 -0
- package/vendor/dragonlab/design/cipher/v2/character.mjs +49 -0
- package/vendor/dragonlab/design/cipher/v2/review.mjs +33 -0
- package/vendor/dragonlab/design/cipher/v2/watch.mjs +56 -0
- package/vendor/dragonlab/design/drago/v1/character.mjs +19 -0
- package/vendor/dragonlab/design/drago/v1/drago-master.svg +56 -0
- package/vendor/dragonlab/design/drago/v2/character.mjs +87 -0
- package/vendor/dragonlab/design/drago/v4/character.mjs +67 -0
- package/vendor/dragonlab/design/drago/v4/profile.mjs +35 -0
- package/vendor/dragonlab/design/drago/v4/review.mjs +33 -0
- package/vendor/dragonlab/design/drago/v4/watch.mjs +56 -0
- package/vendor/dragonlab/design/hyperion/v1/hyperion-master.svg +56 -0
- package/vendor/dragonlab/design/hyperion/v2/character.mjs +79 -0
- package/vendor/dragonlab/design/hyperion/v2/review.mjs +33 -0
- package/vendor/dragonlab/design/hyperion/v2/watch.mjs +56 -0
- package/vendor/dragonlab/design/lucid/recovery/arrival.mjs +22 -0
- package/vendor/dragonlab/design/lucid/recovery/character.mjs +106 -0
- package/vendor/dragonlab/design/lucid/recovery/layout.mjs +50 -0
- package/vendor/dragonlab/design/lucid/recovery/lucid-master.svg +78 -0
- package/vendor/dragonlab/design/lucid/recovery/motion.mjs +42 -0
- package/vendor/dragonlab/design/lucid/recovery/render.mjs +121 -0
- package/vendor/dragonlab/design/lucid/recovery/review.mjs +68 -0
- package/vendor/dragonlab/design/lucid/recovery/verify.mjs +84 -0
- package/vendor/dragonlab/design/lucid/recovery/watch.mjs +56 -0
- package/vendor/dragonlab/design/quoxperticus/v2/quoxperticus-master.svg +59 -0
- package/vendor/dragonlab/design/quoxperticus/v3/character.mjs +43 -0
- package/vendor/dragonlab/design/quoxperticus/v4/character.mjs +57 -0
- package/vendor/dragonlab/design/quoxperticus/v4/review.mjs +35 -0
- package/vendor/dragonlab/design/quoxperticus/v4/watch.mjs +56 -0
- package/vendor/dragonlab/design/seer/v1/seer-master.svg +53 -0
- package/vendor/dragonlab/design/seer/v2/character.mjs +88 -0
- package/vendor/dragonlab/design/seer/v3/character.mjs +33 -0
- package/vendor/dragonlab/design/seer/v4/character.mjs +66 -0
- package/vendor/dragonlab/design/seer/v4/review.mjs +33 -0
- package/vendor/dragonlab/design/seer/v4/watch.mjs +56 -0
- package/vendor/dragonlab/design/stage3/companion-review.mjs +28 -0
- package/vendor/dragonlab/design/stage3/sentinel-art.js +39 -0
- package/vendor/dragonlab/design/stage3/sentinel-braille.js +216 -0
- package/vendor/dragonlab/design/stage3/sentinel-character.js +257 -0
- package/vendor/dragonlab/design/stage3/sentinel-icon.js +103 -0
- package/vendor/dragonlab/design/stage3/sentinel-warden-eyes.js +95 -0
- package/vendor/dragonlab/design/stage3/svg-coverage.js +148 -0
- package/vendor/dragonlab/design/vulcan/v1/character.mjs +34 -0
- package/vendor/dragonlab/design/vulcan/v1/vulcan-master.svg +57 -0
- package/vendor/dragonlab/design/vulcan/v2/character.mjs +39 -0
- package/vendor/dragonlab/design/vulcan/v3/character.mjs +41 -0
- package/vendor/dragonlab/design/vulcan/v3/review.mjs +35 -0
- package/vendor/dragonlab/design/vulcan/v3/watch.mjs +56 -0
- package/vendor/dragonlab/factory/svg-rig-contour.mjs +64 -0
- package/vendor/dragonlab/factory/svg-rig.mjs +63 -0
- package/vendor/dragonlab/integration/companions-v1/characters.mjs +108 -0
- package/vendor/dragonlab/integration/companions-v1/cipher.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/companion.mjs +63 -0
- package/vendor/dragonlab/integration/companions-v1/drago.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/hyperion.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/lucid.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/quoxperticus.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/seer.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/sentinel.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/view.mjs +28 -0
- package/vendor/dragonlab/integration/companions-v1/vulcan.mjs +2 -0
- package/vendor/dragonlab/integration/hyperion-v3/activity.mjs +76 -0
- package/vendor/dragonlab/lib/power-effects.js +294 -0
- package/vendor/dragonlab/lib/power-registry.js +74 -0
- package/vendor/dragonlab/vendor/quox-dragon/qart.js +348 -0
- package/vendor/dragonlab/vendor/quox-dragon/qeyes.js +213 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `quox blog draft submit|list|show|mark-published` (BLOG-DRAFT).
|
|
3
|
+
*
|
|
4
|
+
* Submit saves the draft as a NotesQ note in the blog-drafts collection and files a hand-off to
|
|
5
|
+
* the blog publisher (a line in the publisher's slot plus an inbox item). It NEVER publishes:
|
|
6
|
+
* publishing is the publisher's governed pipeline after the owner's yes. `mark-published` is the
|
|
7
|
+
* publisher-side verb that records the URL and tells the originating conversation/objective.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { readFileSync } from 'fs';
|
|
11
|
+
import { formatOutput, formatDetail, printSuccess, printInfo } from '../core/output.js';
|
|
12
|
+
import { line as glyphLine, printEvent } from '../core/quoxglyph.js';
|
|
13
|
+
import { resolveEndpoint } from '../api/endpoints.js';
|
|
14
|
+
|
|
15
|
+
const isJson = (program) => program.opts().output === 'json';
|
|
16
|
+
const printJson = (d) => console.log(JSON.stringify(d, null, 2));
|
|
17
|
+
|
|
18
|
+
async function readBody(options, ctx) {
|
|
19
|
+
if (options.file) return readFileSync(options.file, 'utf-8');
|
|
20
|
+
if (options.stdin) {
|
|
21
|
+
const input = ctx.stdin || process.stdin;
|
|
22
|
+
let buf = '';
|
|
23
|
+
for await (const chunk of input) buf += chunk;
|
|
24
|
+
return buf;
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function registerBlogCommands(program, ctx) {
|
|
30
|
+
const { client, auth } = ctx;
|
|
31
|
+
const blog = program.command('blog').description('Blog: hand drafts to the blog publisher (never publishes)');
|
|
32
|
+
const draft = blog.command('draft').description('Blog drafts: saved as revisioned NotesQ notes in the blog-drafts collection');
|
|
33
|
+
|
|
34
|
+
draft
|
|
35
|
+
.command('submit')
|
|
36
|
+
.description('Save a draft and hand it to the blog publisher (slot + inbox). Does NOT publish.')
|
|
37
|
+
.requiredOption('--title <title>', 'post title')
|
|
38
|
+
.option('--file <path>', 'read the post markdown from a file')
|
|
39
|
+
.option('--stdin', 'read the post markdown from stdin')
|
|
40
|
+
.option('--hero <image>', 'hero image path or URL')
|
|
41
|
+
.option('--tag <tag...>', 'tag (repeatable)')
|
|
42
|
+
.option('--conversation <id>', 'originating conversation id (so its publication is reported back)')
|
|
43
|
+
.option('--objective <id>', 'originating objective id')
|
|
44
|
+
.action(async (options) => {
|
|
45
|
+
auth.requireAuth();
|
|
46
|
+
const markdown = await readBody(options, ctx);
|
|
47
|
+
if (markdown === null) throw new Error('give the post body with --file <path> or --stdin');
|
|
48
|
+
if (!markdown.trim()) throw new Error('the draft body is empty; nothing to submit');
|
|
49
|
+
const body = { title: options.title, markdown };
|
|
50
|
+
if (options.hero) body.hero = options.hero;
|
|
51
|
+
if (options.tag && options.tag.length) body.tags = options.tag;
|
|
52
|
+
if (options.conversation) body.conversation_id = options.conversation;
|
|
53
|
+
if (options.objective) body.objective_id = options.objective;
|
|
54
|
+
const ep = resolveEndpoint('blog.drafts.submit');
|
|
55
|
+
const data = await client.post(ep.path, { service: ep.service, body });
|
|
56
|
+
if (isJson(program)) return printJson(data);
|
|
57
|
+
printEvent(glyphLine('quox', `blog draft submit "${options.title}"`, `note ${data.note_id}`));
|
|
58
|
+
printSuccess(`Draft saved as note ${data.note_id} and handed to the publisher slot ${data.slot} (hand-off ${data.handoff_id}).`);
|
|
59
|
+
if (data.link) console.log(`link: ${data.link}`);
|
|
60
|
+
printInfo('Not published: nothing goes live until the publisher publishes after the owner approves.');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
draft
|
|
64
|
+
.command('list')
|
|
65
|
+
.description('List blog drafts')
|
|
66
|
+
.option('--status <status>', 'draft | published')
|
|
67
|
+
.action(async (options) => {
|
|
68
|
+
auth.requireAuth();
|
|
69
|
+
const ep = resolveEndpoint('blog.drafts.list');
|
|
70
|
+
const data = await client.get(ep.path, { service: ep.service, query: options.status ? { status: options.status } : {} });
|
|
71
|
+
if (isJson(program)) return printJson(data);
|
|
72
|
+
const rows = (data.drafts || []).map((d) => ({ id: d.id, title: d.title, status: d.status, url: d.url || '', updated: d.updated || '' }));
|
|
73
|
+
if (!rows.length) return printInfo('No blog drafts.');
|
|
74
|
+
formatOutput({ columns: ['id', 'title', 'status', 'url', 'updated'], rows });
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
draft
|
|
78
|
+
.command('show')
|
|
79
|
+
.description('Show one blog draft (body and hand-off facts)')
|
|
80
|
+
.argument('<id>', 'draft (note) id')
|
|
81
|
+
.action(async (id) => {
|
|
82
|
+
auth.requireAuth();
|
|
83
|
+
const ep = resolveEndpoint('blog.drafts.get', { id });
|
|
84
|
+
const data = await client.get(ep.path, { service: ep.service });
|
|
85
|
+
if (isJson(program)) return printJson(data);
|
|
86
|
+
const d = data.draft || {};
|
|
87
|
+
const { markdown, ...facts } = d;
|
|
88
|
+
formatDetail(facts);
|
|
89
|
+
console.log('');
|
|
90
|
+
console.log(markdown || '');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
draft
|
|
94
|
+
.command('mark-published')
|
|
95
|
+
.description('Publisher side: record the published URL and tell the originating conversation/objective')
|
|
96
|
+
.argument('<id>', 'draft (note) id')
|
|
97
|
+
.requiredOption('--url <url>', 'the published https URL')
|
|
98
|
+
.action(async (id, options) => {
|
|
99
|
+
auth.requireAuth();
|
|
100
|
+
if (!/^https:\/\//i.test(options.url)) throw new Error('--url must be an https URL');
|
|
101
|
+
const ep = resolveEndpoint('blog.drafts.published', { id });
|
|
102
|
+
const data = await client.post(ep.path, { service: ep.service, body: { url: options.url } });
|
|
103
|
+
if (isJson(program)) return printJson(data);
|
|
104
|
+
printSuccess(data.already ? `Draft ${id} was already marked published at ${data.url}.` : `Draft ${id} marked published at ${data.url}.`);
|
|
105
|
+
if (data.notified) {
|
|
106
|
+
const n = data.notified;
|
|
107
|
+
printInfo(`Told: ${[n.conversation_id && `conversation ${n.conversation_id}`, n.objective_id && `objective ${n.objective_id}`, n.inbox_item_id && `inbox ${n.inbox_item_id}`].filter(Boolean).join(', ') || 'inbox only'}.`);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
}
|
|
@@ -8,18 +8,19 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { resolveEndpoint } from '../api/endpoints.js';
|
|
11
|
-
import { capabilityLines, findCapability, offerFor } from '../quoxon/capabilities.js';
|
|
11
|
+
import { capabilityLines, findCapability, offerFor, needsSetupNames } from '../quoxon/capabilities.js';
|
|
12
12
|
|
|
13
13
|
export function registerCapabilitiesCommands(program, ctx) {
|
|
14
14
|
const { client, auth } = ctx;
|
|
15
15
|
program
|
|
16
16
|
.command('capabilities [name]')
|
|
17
17
|
.description('What this Quox can do for your org: ready, needs setup, not available, planned')
|
|
18
|
+
.addHelpText('after', '\nAdd --json for the full list (id, name, availability, reason, setup, permissions_ok).')
|
|
18
19
|
.action(async (name) => {
|
|
19
20
|
auth.requireAuth();
|
|
20
21
|
const ep = resolveEndpoint('v1.capabilities');
|
|
21
22
|
const data = await client.get(ep.path, { service: ep.service });
|
|
22
|
-
if (program.opts().output === 'json') { console.log(JSON.stringify(data, null, 2)); return; }
|
|
23
|
+
if (program.opts().output === 'json') { console.log(JSON.stringify({ needs_setup: needsSetupNames(data?.capabilities), ...data }, null, 2)); return; }
|
|
23
24
|
const res = { ok: Array.isArray(data?.capabilities), capabilities: data?.capabilities, error: 'unexpected response' };
|
|
24
25
|
if (name) {
|
|
25
26
|
const c = findCapability(name, res);
|
package/lib/commands/chat.js
CHANGED
|
@@ -4,7 +4,12 @@
|
|
|
4
4
|
|
|
5
5
|
import chalk from 'chalk';
|
|
6
6
|
import { formatDetail } from '../core/output.js';
|
|
7
|
+
import { eventLine, announceMuted } from '../core/announce.js';
|
|
7
8
|
import { resolveEndpoint } from '../api/endpoints.js';
|
|
9
|
+
import { existsSync } from 'node:fs';
|
|
10
|
+
import { lastConversation, saveLastConversation } from './chatState.js';
|
|
11
|
+
import { loadChatCompanion } from './chatCompanion.js';
|
|
12
|
+
import { companionPlan, motionPolicy, VENDORED_DRAGONLAB } from '../dragon/select.js';
|
|
8
13
|
|
|
9
14
|
/**
|
|
10
15
|
* Build the POST body for /chat/stream (quox#782). The prompt and context
|
|
@@ -66,6 +71,14 @@ function printTrace(ev) {
|
|
|
66
71
|
if (f) process.stderr.write((TONE_PAINT[f.tone] || chalk.cyan)(f.text) + '\n');
|
|
67
72
|
}
|
|
68
73
|
|
|
74
|
+
/** A turn's quox_event frame as its QuoxGlyph line on stderr (stdout stays the answer); silent under --json/--quiet. */
|
|
75
|
+
export function printQuoxEvent(ev) {
|
|
76
|
+
if (announceMuted()) return null;
|
|
77
|
+
const q = eventLine(ev);
|
|
78
|
+
if (q) process.stderr.write(`${q}\n`);
|
|
79
|
+
return q;
|
|
80
|
+
}
|
|
81
|
+
|
|
69
82
|
function generateQueryId() {
|
|
70
83
|
return `CLI_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
71
84
|
}
|
|
@@ -74,9 +87,10 @@ function generateQueryId() {
|
|
|
74
87
|
* Consume a chat SSE stream, printing chunks as they arrive.
|
|
75
88
|
* Returns { text, sessionId, agentUsed } once the stream completes.
|
|
76
89
|
*/
|
|
77
|
-
export async function consumeStream(client, { message, agent, sessionId, context }, { onAgentLabel, trace = false } = {}) {
|
|
90
|
+
export async function consumeStream(client, { message, agent, sessionId, context }, { onAgentLabel, trace = false, companionPath, companionDisplay } = {}) {
|
|
78
91
|
const queryId = generateQueryId();
|
|
79
92
|
const body = buildStreamQuery({ queryId, message, agent, sessionId, context });
|
|
93
|
+
body.turn_id = queryId; // an explicit turn id selects the v1 event stream: tool and quox_event frames only flow with it
|
|
80
94
|
|
|
81
95
|
let text = '';
|
|
82
96
|
let resultSessionId = sessionId;
|
|
@@ -90,19 +104,25 @@ export async function consumeStream(client, { message, agent, sessionId, context
|
|
|
90
104
|
onAgentLabel?.(agentUsed);
|
|
91
105
|
};
|
|
92
106
|
|
|
93
|
-
const
|
|
107
|
+
const companion = await loadChatCompanion(companionPath, { display: companionDisplay });
|
|
108
|
+
const write = text => companion ? companion.write(text) : process.stdout.write(text);
|
|
109
|
+
// Native terminal status follows legacy `done`; observe it when opted in.
|
|
110
|
+
let iter;
|
|
111
|
+
try {
|
|
112
|
+
iter = client.stream(resolveEndpoint('chat.stream').path, { service: resolveEndpoint('chat.stream').service, body, untilClose: Boolean(trace || companion) })[Symbol.asyncIterator]();
|
|
94
113
|
while (true) {
|
|
95
114
|
let step;
|
|
96
115
|
try {
|
|
97
116
|
step = await iter.next();
|
|
98
117
|
} catch (err) {
|
|
99
118
|
// A refusal before the stream opened carries the layers it really evaluated.
|
|
100
|
-
if (trace && Array.isArray(err?.body?.aocl_layers)) for (const l of err.body.aocl_layers) printTrace({ type: 'aocl.layer', ...l });
|
|
119
|
+
if (trace && Array.isArray(err?.body?.aocl_layers)) { companion?.suspend(); for (const l of err.body.aocl_layers) printTrace({ type: 'aocl.layer', ...l }); }
|
|
101
120
|
throw err;
|
|
102
121
|
}
|
|
103
122
|
if (step.done) break;
|
|
104
123
|
const event = step.value;
|
|
105
|
-
|
|
124
|
+
companion?.event(event);
|
|
125
|
+
if (trace) { companion?.suspend(); printTrace(event); }
|
|
106
126
|
switch (event.type) {
|
|
107
127
|
case 'connected':
|
|
108
128
|
break;
|
|
@@ -110,9 +130,9 @@ export async function consumeStream(client, { message, agent, sessionId, context
|
|
|
110
130
|
case 'thinking': {
|
|
111
131
|
const stage = event.stage || '';
|
|
112
132
|
if (stage === 'tool_call' || stage.includes('tool')) {
|
|
113
|
-
|
|
133
|
+
write(chalk.dim(` ⚙ using tool: ${event.message}`) + '\n');
|
|
114
134
|
} else if (process.argv.includes('--verbose') || process.argv.includes('-v')) {
|
|
115
|
-
|
|
135
|
+
write(chalk.dim(` … ${event.message}`) + '\n');
|
|
116
136
|
}
|
|
117
137
|
break;
|
|
118
138
|
}
|
|
@@ -120,19 +140,23 @@ export async function consumeStream(client, { message, agent, sessionId, context
|
|
|
120
140
|
case 'chunk':
|
|
121
141
|
printLabel();
|
|
122
142
|
sawChunk = true;
|
|
123
|
-
|
|
143
|
+
write(event.content || '');
|
|
124
144
|
text += event.content || '';
|
|
125
145
|
break;
|
|
126
146
|
|
|
127
147
|
case 'done':
|
|
128
148
|
if (!sawChunk && event.full_response) {
|
|
129
149
|
printLabel();
|
|
130
|
-
|
|
150
|
+
write(event.full_response);
|
|
131
151
|
text = event.full_response;
|
|
132
152
|
}
|
|
133
153
|
if (event.session_id) resultSessionId = event.session_id;
|
|
134
154
|
if (event.agent_used) agentUsed = event.agent_used;
|
|
135
|
-
|
|
155
|
+
write('\n');
|
|
156
|
+
break;
|
|
157
|
+
|
|
158
|
+
case 'quox_event':
|
|
159
|
+
printQuoxEvent(event);
|
|
136
160
|
break;
|
|
137
161
|
|
|
138
162
|
case 'error':
|
|
@@ -142,28 +166,99 @@ export async function consumeStream(client, { message, agent, sessionId, context
|
|
|
142
166
|
break;
|
|
143
167
|
}
|
|
144
168
|
}
|
|
169
|
+
} catch (err) {
|
|
170
|
+
companion?.event({ type: 'error' });
|
|
171
|
+
throw err;
|
|
172
|
+
} finally {
|
|
173
|
+
await companion?.close();
|
|
174
|
+
try { await iter?.return?.(); } catch { /* cleanup must not mask a stream error */ }
|
|
175
|
+
}
|
|
145
176
|
|
|
146
177
|
return { text, sessionId: resultSessionId, agentUsed };
|
|
147
178
|
}
|
|
148
179
|
|
|
149
|
-
/**
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
if (sessionId) body.session_id = sessionId;
|
|
180
|
+
/** The words for a failed turn: the server's plain message first, the bare error code only as a fallback. */
|
|
181
|
+
export function turnFailureText(result, httpStatus) {
|
|
182
|
+
return result?.message || result?.error || `chat turn failed (HTTP ${httpStatus})`;
|
|
183
|
+
}
|
|
154
184
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
185
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Poll a server-side chat turn until it is done. The turn keeps running on the server whatever this
|
|
189
|
+
* client does, so giving up early would hide a result (and any objects it created) from the user.
|
|
190
|
+
*/
|
|
191
|
+
async function awaitTurn(client, turnId) {
|
|
192
|
+
const capMs = Number(process.env.QUOX_CHAT_TURN_TIMEOUT_MS) || 20 * 60 * 1000;
|
|
193
|
+
const fixed = Number(process.env.QUOX_CHAT_POLL_MS) || 0;
|
|
194
|
+
const started = Date.now();
|
|
195
|
+
let wait = fixed || 1500;
|
|
196
|
+
let failures = 0;
|
|
197
|
+
const ep = resolveEndpoint('chat.turn.get', { id: turnId });
|
|
198
|
+
while (true) {
|
|
199
|
+
let t;
|
|
200
|
+
try {
|
|
201
|
+
t = await client.get(ep.path, { service: ep.service });
|
|
202
|
+
failures = 0;
|
|
203
|
+
} catch (err) {
|
|
204
|
+
if (err?.status === 404 || /not.found/i.test(err?.message || '')) {
|
|
205
|
+
throw new Error(`The server no longer has chat turn ${turnId} (expired, or the collector restarted). Check \`quox conversation list\` to see whether it finished.`);
|
|
206
|
+
}
|
|
207
|
+
if (++failures >= 5) throw err;
|
|
208
|
+
t = { status: 'running' };
|
|
209
|
+
}
|
|
210
|
+
if (t.status === 'done') {
|
|
211
|
+
if (t.http_status >= 400) throw new Error(turnFailureText(t.result, t.http_status));
|
|
212
|
+
return t.result || {};
|
|
213
|
+
}
|
|
214
|
+
if (t.status === 'error') throw new Error(`chat turn ${turnId} failed on the server: ${t.error || 'unknown error'}`);
|
|
215
|
+
if (Date.now() - started > capMs) {
|
|
216
|
+
throw new Error(`Chat turn ${turnId} is still running after ${Math.round(capMs / 1000)}s. It has not been lost: fetch it with \`quox chat --resume ${turnId}\`.`);
|
|
217
|
+
}
|
|
218
|
+
await sleep(wait);
|
|
219
|
+
if (!fixed) wait = Math.min(wait + 500, 5000);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** One-shot (non-streaming) chat via POST /chat/claude, kept server side as a pollable turn. */
|
|
224
|
+
async function sendOneShot(client, { message, agent, sessionId, resumeTurn }) {
|
|
225
|
+
let data;
|
|
226
|
+
if (resumeTurn) {
|
|
227
|
+
data = await awaitTurn(client, resumeTurn);
|
|
228
|
+
} else {
|
|
229
|
+
const body = { prompt: message };
|
|
230
|
+
if (agent) body.selected_agent = agent;
|
|
231
|
+
if (sessionId) body.session_id = sessionId;
|
|
232
|
+
|
|
233
|
+
// Prefer: respond-async returns 202 {turn_id} at once, so no client timeout can orphan a
|
|
234
|
+
// multi-tool turn. An older collector ignores it and answers synchronously (handled below).
|
|
235
|
+
data = await client.post(resolveEndpoint('chat.claude').path, {
|
|
236
|
+
service: resolveEndpoint('chat.claude').service,
|
|
237
|
+
body,
|
|
238
|
+
headers: { Prefer: 'respond-async' },
|
|
239
|
+
timeout: 30000,
|
|
240
|
+
});
|
|
241
|
+
if (data && data.turn_id && data.status === 'running') data = await awaitTurn(client, data.turn_id);
|
|
242
|
+
}
|
|
160
243
|
|
|
161
244
|
const response = data.result || data.response || data.text || JSON.stringify(data);
|
|
162
245
|
return { data, response, sessionId: data.session_id || sessionId };
|
|
163
246
|
}
|
|
164
247
|
|
|
248
|
+
/** Explicit --dragon-module wins; otherwise pick the dragon for the attached agent (null path = ordinary chat). */
|
|
249
|
+
export function chooseCompanion({ options, config, env = process.env, isTTY = Boolean(process.stdout.isTTY), exists = existsSync }) {
|
|
250
|
+
const policy = motionPolicy(env);
|
|
251
|
+
if (options.dragonModule) return { companionPath: options.dragonModule, companionDisplay: { still: policy.still, mono: policy.mono } };
|
|
252
|
+
let settings = {};
|
|
253
|
+
try { settings = config?.getActiveProfile?.()?.quoxon || {}; } catch { /* no profile settings */ }
|
|
254
|
+
const e = options.dragon ? { ...env, QUOX_DRAGON_COMPANION: options.dragon } : env;
|
|
255
|
+
const plan = companionPlan({ agent: options.agent, settings, env: e, isTTY, exists, bundled: VENDORED_DRAGONLAB });
|
|
256
|
+
if (!plan.enabled) return { companionPath: undefined, companionDisplay: undefined };
|
|
257
|
+
return { companionPath: plan.file, companionDisplay: { still: plan.still, mono: plan.mono, persist: plan.persist } };
|
|
258
|
+
}
|
|
259
|
+
|
|
165
260
|
export function registerChatCommands(program, ctx) {
|
|
166
|
-
const { client, auth } = ctx;
|
|
261
|
+
const { client, auth, config } = ctx;
|
|
167
262
|
|
|
168
263
|
const chat = program
|
|
169
264
|
.command('chat')
|
|
@@ -172,13 +267,25 @@ export function registerChatCommands(program, ctx) {
|
|
|
172
267
|
.option('--agent <name>', 'route to a specific agent (e.g. sentinel, cipher, nova)')
|
|
173
268
|
.option('--no-stream', 'disable streaming output (one-shot request/response)')
|
|
174
269
|
.option('--conversation <id>', 'continue a conversation (session id)')
|
|
270
|
+
.option('--continue', 'continue the last conversation for this profile and directory')
|
|
271
|
+
.option('--resume <turn>', 'fetch a chat turn that was still running (its id is printed if a turn outlives the wait)')
|
|
175
272
|
.option('--trace', 'print the turn\'s protocol events (per-layer AOCL, traces, summary) to stderr as they arrive')
|
|
273
|
+
.option('--dragon <mode>', 'dragon companion for dragon-backed agents: on, greeting or off (default from /dragon companion; art is bundled; QUOX_DRAGONLAB overrides it)')
|
|
274
|
+
.option('--dragon-module <path>', 'opt in to a trusted local dragon companion module (streaming, single-message mode)')
|
|
176
275
|
.action(async (message, options) => {
|
|
177
276
|
auth.requireAuth();
|
|
178
277
|
const streaming = options.stream !== false;
|
|
278
|
+
const profileName = (() => { try { return config?.getActiveProfileName?.(); } catch { return undefined; } })();
|
|
279
|
+
const noteConversation = (id) => {
|
|
280
|
+
if (!id) return;
|
|
281
|
+
saveLastConversation(profileName, process.cwd(), id);
|
|
282
|
+
if (program.opts().output !== 'json') process.stderr.write(`conversation: ${id}\n`);
|
|
283
|
+
};
|
|
284
|
+
if (!options.conversation && options.continue) options.conversation = lastConversation(profileName, process.cwd());
|
|
285
|
+
if (options.dragonModule && (!message || !streaming)) throw new Error('--dragon-module requires a single message with streaming enabled.');
|
|
179
286
|
|
|
180
|
-
// Interactive mode if no message
|
|
181
|
-
if (!message) {
|
|
287
|
+
// Interactive mode if no message (a --resume fetch is not interactive)
|
|
288
|
+
if (!message && !options.resume) {
|
|
182
289
|
const { default: inquirer } = await import('inquirer');
|
|
183
290
|
let currentAgent = options.agent || 'quox';
|
|
184
291
|
let sessionId = options.conversation;
|
|
@@ -216,7 +323,7 @@ export function registerChatCommands(program, ctx) {
|
|
|
216
323
|
onAgentLabel: (agentUsed) => process.stdout.write(chalk.green(`${(agentUsed || currentAgent).toUpperCase()}: `)),
|
|
217
324
|
trace: Boolean(options.trace),
|
|
218
325
|
});
|
|
219
|
-
if (result.sessionId) sessionId = result.sessionId;
|
|
326
|
+
if (result.sessionId) { sessionId = result.sessionId; saveLastConversation(profileName, process.cwd(), sessionId); }
|
|
220
327
|
console.log();
|
|
221
328
|
} else {
|
|
222
329
|
const { response, sessionId: newSessionId } = await sendOneShot(client, {
|
|
@@ -224,7 +331,7 @@ export function registerChatCommands(program, ctx) {
|
|
|
224
331
|
agent: currentAgent,
|
|
225
332
|
sessionId,
|
|
226
333
|
});
|
|
227
|
-
if (newSessionId) sessionId = newSessionId;
|
|
334
|
+
if (newSessionId) { sessionId = newSessionId; saveLastConversation(profileName, process.cwd(), sessionId); }
|
|
228
335
|
console.log(chalk.green(`${currentAgent.toUpperCase()}:`), response);
|
|
229
336
|
console.log();
|
|
230
337
|
}
|
|
@@ -236,16 +343,19 @@ export function registerChatCommands(program, ctx) {
|
|
|
236
343
|
}
|
|
237
344
|
|
|
238
345
|
// Single message mode
|
|
239
|
-
if (streaming) {
|
|
346
|
+
if (streaming && !options.resume) {
|
|
347
|
+
const companion = chooseCompanion({ options, config });
|
|
240
348
|
try {
|
|
241
|
-
await consumeStream(client, {
|
|
349
|
+
const result = await consumeStream(client, {
|
|
242
350
|
message,
|
|
243
351
|
agent: options.agent,
|
|
244
352
|
sessionId: options.conversation,
|
|
245
353
|
}, {
|
|
246
354
|
onAgentLabel: () => {}, // no label prefix in single-shot streaming; just print tokens
|
|
247
355
|
trace: Boolean(options.trace),
|
|
356
|
+
...companion,
|
|
248
357
|
});
|
|
358
|
+
noteConversation(result?.sessionId);
|
|
249
359
|
} catch (err) {
|
|
250
360
|
console.error(chalk.red(`Error: ${err.message}`));
|
|
251
361
|
process.exitCode = 1;
|
|
@@ -257,15 +367,18 @@ export function registerChatCommands(program, ctx) {
|
|
|
257
367
|
const spinner = ora('Thinking...').start();
|
|
258
368
|
|
|
259
369
|
try {
|
|
260
|
-
const { data, response } = await sendOneShot(client, {
|
|
370
|
+
const { data, response, sessionId: convId } = await sendOneShot(client, {
|
|
261
371
|
message,
|
|
262
372
|
agent: options.agent,
|
|
263
373
|
sessionId: options.conversation,
|
|
374
|
+
resumeTurn: options.resume,
|
|
264
375
|
});
|
|
265
376
|
spinner.stop();
|
|
377
|
+
noteConversation(convId);
|
|
378
|
+
for (const qe of Array.isArray(data.quox_events) ? data.quox_events : []) printQuoxEvent(qe); // the server's per-turn Quox effects, when it returns them
|
|
266
379
|
|
|
267
380
|
if (program.opts().output === 'json') {
|
|
268
|
-
console.log(JSON.stringify(data, null, 2));
|
|
381
|
+
console.log(JSON.stringify({ ...data, conversation_id: convId || null }, null, 2));
|
|
269
382
|
} else {
|
|
270
383
|
console.log(response);
|
|
271
384
|
|
|
@@ -293,3 +406,4 @@ export function registerChatCommands(program, ctx) {
|
|
|
293
406
|
});
|
|
294
407
|
});
|
|
295
408
|
}
|
|
409
|
+
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
2
|
+
import { pathToFileURL } from 'node:url';
|
|
3
|
+
|
|
4
|
+
/** Explicit local opt-in only. A display failure must not interrupt a chat. */
|
|
5
|
+
export async function loadChatCompanion(path, { output = process.stdout, warn = console.error, display = {} } = {}) {
|
|
6
|
+
if (!path || !output.isTTY) return null;
|
|
7
|
+
try {
|
|
8
|
+
const mod = await import(pathToFileURL(resolve(path)).href);
|
|
9
|
+
const c = await mod.createCompanion({ output, ...display });
|
|
10
|
+
if (!c || !['event', 'write', 'close', 'suspend'].every(k => typeof c[k] === 'function')) {
|
|
11
|
+
try { c?.close?.(); } catch {}
|
|
12
|
+
throw new Error('Expected createCompanion() with event/write/close/suspend');
|
|
13
|
+
}
|
|
14
|
+
let active = true;
|
|
15
|
+
const stop = async () => { active = false; try { await c.close(); } catch {} };
|
|
16
|
+
return {
|
|
17
|
+
event(e) { if (active) try { c.event(e); } catch { stop(); } },
|
|
18
|
+
write(text) { if (!active) output.write(text); else c.write(text); },
|
|
19
|
+
// Trace writes to stderr: relinquish the block first to prevent overlap.
|
|
20
|
+
suspend() { if (active) try { c.suspend(); } catch { stop(); } },
|
|
21
|
+
close: stop,
|
|
22
|
+
};
|
|
23
|
+
} catch (e) {
|
|
24
|
+
warn('Dragon companion unavailable; continuing normal chat: ' + e.message);
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Last conversation per (profile, cwd), so `quox chat --continue` keeps context without the user
|
|
3
|
+
* copying a session id. Best-effort: a read/write failure never breaks a chat turn.
|
|
4
|
+
*/
|
|
5
|
+
import { homedir } from 'node:os';
|
|
6
|
+
import { join, dirname } from 'node:path';
|
|
7
|
+
import { readFileSync, writeFileSync, mkdirSync, readlinkSync } from 'node:fs';
|
|
8
|
+
|
|
9
|
+
const stateFile = () => process.env.QUOX_CHAT_STATE || join(homedir(), '.config', 'quox', 'chat-last.json');
|
|
10
|
+
/**
|
|
11
|
+
* Who is calling: $QUOX_SESSION when set, else this terminal's tty, else nothing (a headless
|
|
12
|
+
* caller with no session id falls back to profile + cwd only). Parallel terminals/sessions must
|
|
13
|
+
* never share a "last conversation".
|
|
14
|
+
*/
|
|
15
|
+
export function callerScope(env = process.env) {
|
|
16
|
+
if (env.QUOX_SESSION) return `session:${String(env.QUOX_SESSION).slice(0, 64)}`;
|
|
17
|
+
for (const fd of [0, 1, 2]) {
|
|
18
|
+
try {
|
|
19
|
+
const t = readlinkSync(`/proc/self/fd/${fd}`);
|
|
20
|
+
if (/^\/dev\/(pts\/\d+|tty\w*)$/.test(t)) return `tty:${t}`;
|
|
21
|
+
} catch { /* not linux, or fd closed */ }
|
|
22
|
+
}
|
|
23
|
+
return '';
|
|
24
|
+
}
|
|
25
|
+
const keyOf = (profile, cwd) => `${profile || 'default'}|${cwd || process.cwd()}|${callerScope()}`;
|
|
26
|
+
|
|
27
|
+
function read() {
|
|
28
|
+
try { return JSON.parse(readFileSync(stateFile(), 'utf8')) || {}; } catch { return {}; }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function lastConversation(profile, cwd) {
|
|
32
|
+
const v = read()[keyOf(profile, cwd)];
|
|
33
|
+
return typeof v === 'string' && v ? v : undefined;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function saveLastConversation(profile, cwd, id) {
|
|
37
|
+
if (!id) return;
|
|
38
|
+
try {
|
|
39
|
+
const all = read();
|
|
40
|
+
all[keyOf(profile, cwd)] = id;
|
|
41
|
+
const f = stateFile();
|
|
42
|
+
mkdirSync(dirname(f), { recursive: true });
|
|
43
|
+
writeFileSync(f, JSON.stringify(all, null, 2));
|
|
44
|
+
} catch { /* best-effort */ }
|
|
45
|
+
}
|
package/lib/commands/claude.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* quox claude / codex /
|
|
2
|
+
* quox claude / codex / run / sessions: governed tmux sessions. The runner
|
|
3
3
|
* lives in core/governedSession.js; this file holds argument parsing and the
|
|
4
4
|
* commander wiring. argv arrays only; user input never reaches a shell string.
|
|
5
5
|
*/
|
|
@@ -29,6 +29,7 @@ export {
|
|
|
29
29
|
*/
|
|
30
30
|
export function parseClaudeArgs(raw) {
|
|
31
31
|
let noTmux = false;
|
|
32
|
+
let force = false;
|
|
32
33
|
let as;
|
|
33
34
|
let slot;
|
|
34
35
|
const args = [];
|
|
@@ -38,6 +39,7 @@ export function parseClaudeArgs(raw) {
|
|
|
38
39
|
if (past) { args.push(a); continue; }
|
|
39
40
|
if (a === '--') { past = true; args.push(a); continue; }
|
|
40
41
|
if (a === '--no-tmux') noTmux = true;
|
|
42
|
+
else if (a === '--force') force = true;
|
|
41
43
|
else if ((a === '--as' || a === '--slot') && raw[i + 1] && !raw[i + 1].startsWith('-')) {
|
|
42
44
|
if (a === '--as') as = raw[i + 1]; else slot = raw[i + 1];
|
|
43
45
|
i++;
|
|
@@ -72,7 +74,7 @@ export function parseClaudeArgs(raw) {
|
|
|
72
74
|
}
|
|
73
75
|
}
|
|
74
76
|
if (name !== undefined && !validateName(name)) name = undefined;
|
|
75
|
-
return { args, name, resume, noTmux, as, slot };
|
|
77
|
+
return { args, name, resume, noTmux, ...(force ? { force } : {}), as, slot };
|
|
76
78
|
}
|
|
77
79
|
|
|
78
80
|
/**
|
|
@@ -102,22 +104,6 @@ export const runClaude = ({ as, slot, ...opts } = {}, over) => gs.runGoverned({
|
|
|
102
104
|
program: 'claude',
|
|
103
105
|
...(as || slot ? { preattach: { agent: as, slot, session: opts.name } } : {}),
|
|
104
106
|
}, over);
|
|
105
|
-
/**
|
|
106
|
-
* quox gemini <args>: verbatim to `gemini`. Gemini resumes by index or
|
|
107
|
-
* "latest" (`--resume latest`, `-r 5`), not by name, so the tmux session is
|
|
108
|
-
* auto-named; reattach with `quox sessions attach NAME`.
|
|
109
|
-
*/
|
|
110
|
-
export function parseGeminiArgs(raw) {
|
|
111
|
-
let noTmux = false;
|
|
112
|
-
const args = [];
|
|
113
|
-
for (const a of raw) {
|
|
114
|
-
if (a === '--no-tmux') noTmux = true;
|
|
115
|
-
else args.push(a);
|
|
116
|
-
}
|
|
117
|
-
const resume = args.some((a) => a === '--resume' || a === '-r' || a.startsWith('--resume='));
|
|
118
|
-
return { args, name: undefined, resume, noTmux };
|
|
119
|
-
}
|
|
120
|
-
|
|
121
107
|
/**
|
|
122
108
|
* quox wrap <program> [args...]: wrap ANY command. Known programs use their
|
|
123
109
|
* registry entry (so `quox wrap codex` == `quox codex`); unknown ones get the
|
|
@@ -135,12 +121,11 @@ export function parseRunArgs(raw) {
|
|
|
135
121
|
}
|
|
136
122
|
|
|
137
123
|
export function runRun({ bin, args = [], noTmux = false } = {}, over) {
|
|
138
|
-
const known = ['claude', 'codex'
|
|
124
|
+
const known = ['claude', 'codex'].find((k) => PROGRAMS[k].bin === bin);
|
|
139
125
|
if (known) return gs.runGoverned({ program: known, args, noTmux }, over);
|
|
140
126
|
return gs.runGoverned({ program: 'run', bin, args, noTmux }, over);
|
|
141
127
|
}
|
|
142
128
|
|
|
143
|
-
export const runGemini = (opts, over) => gs.runGoverned({ ...opts, program: 'gemini' }, over);
|
|
144
129
|
export const runCodex = (opts, over) => gs.runGoverned({ ...opts, program: 'codex' }, over);
|
|
145
130
|
export const runTui = (opts, over) => gs.runGoverned({ ...opts, program: 'tui' }, over);
|
|
146
131
|
export const runHelp = gs.runHelp;
|
|
@@ -168,6 +153,7 @@ quox's own flags (consumed here, not passed to claude):
|
|
|
168
153
|
--as <agent> start already attached to a Quox agent
|
|
169
154
|
--slot <name> start already holding a dev slot
|
|
170
155
|
--no-tmux run claude directly, without tmux
|
|
156
|
+
--force with --resume: start a second copy even if that conversation is already running
|
|
171
157
|
-n, --name <n> name the session (also names the tmux session; claude's own flag)
|
|
172
158
|
--resume <n> resume the named session (claude's own flag)
|
|
173
159
|
-h, --help show this help
|
|
@@ -219,18 +205,6 @@ Claude's own --agent is untouched and passes through to claude.`)
|
|
|
219
205
|
const parsed = parseCodexArgs(i >= 0 ? raw.slice(i + 1) : []);
|
|
220
206
|
process.exitCode = runCodex(parsed, over);
|
|
221
207
|
});
|
|
222
|
-
markForwardsRawArgs(program
|
|
223
|
-
.command('gemini'), 'forwards argv after the verb verbatim')
|
|
224
|
-
.description('Run Gemini CLI (same arguments as gemini) in a tmux session that survives sleep and drops')
|
|
225
|
-
.argument('[args...]', 'passed to gemini exactly as given (quox adds only --no-tmux)')
|
|
226
|
-
.helpOption(false)
|
|
227
|
-
.allowUnknownOption()
|
|
228
|
-
.allowExcessArguments()
|
|
229
|
-
.action((_a, _o, cmd) => {
|
|
230
|
-
const raw = (cmd.parent && cmd.parent.rawArgs) || process.argv;
|
|
231
|
-
const i = raw.indexOf('gemini', 2);
|
|
232
|
-
process.exitCode = runGemini(parseGeminiArgs(i >= 0 ? raw.slice(i + 1) : []), over);
|
|
233
|
-
});
|
|
234
208
|
markForwardsRawArgs(program
|
|
235
209
|
.command('wrap'), 'forwards argv after the verb verbatim')
|
|
236
210
|
.description('Run any program in a governed tmux session: quox wrap <program> [args...]')
|
|
@@ -299,7 +273,7 @@ Claude's own --agent is untouched and passes through to claude.`)
|
|
|
299
273
|
sessions.command('heartbeat <tmuxSession>', { hidden: true })
|
|
300
274
|
.description('Heartbeat one wrapped tmux session to the org session index (started by quox claude)')
|
|
301
275
|
.option('--loop', 'keep beating until the tmux session ends')
|
|
302
|
-
.option('--backend <program>', 'claude, codex,
|
|
276
|
+
.option('--backend <program>', 'claude, codex, ...')
|
|
303
277
|
.option('--agent <agent>', 'agent the session started attached to (--as)')
|
|
304
278
|
.option('--slot <slot>', 'slot the session started holding (--slot)')
|
|
305
279
|
.action(async (name, o) => {
|