quoxcli 0.3.1 → 0.4.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/lib/api/endpoints/actions.js +15 -0
- package/lib/api/endpoints/admin.js +20 -0
- package/lib/api/endpoints/blog.js +16 -0
- package/lib/api/endpoints/chat.js +7 -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/index.js +12 -0
- package/lib/api/endpoints/loops.js +26 -0
- package/lib/api/endpoints/mon.js +173 -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/v1.js +34 -0
- package/lib/commands/actions.js +81 -11
- package/lib/commands/agent.js +11 -4
- package/lib/commands/agentAttach.js +46 -7
- package/lib/commands/approvals.js +5 -0
- package/lib/commands/ask.js +3 -1
- package/lib/commands/blog.js +110 -0
- package/lib/commands/chat.js +127 -28
- package/lib/commands/chatCompanion.js +27 -0
- package/lib/commands/chatState.js +45 -0
- package/lib/commands/claude.js +3 -32
- package/lib/commands/contacts.js +85 -4
- package/lib/commands/contactsSendAt.js +21 -0
- package/lib/commands/engagement.js +90 -0
- package/lib/commands/evidence.js +154 -0
- package/lib/commands/ext.js +23 -0
- package/lib/commands/fleet.js +25 -0
- package/lib/commands/hat.js +76 -0
- package/lib/commands/image.js +68 -0
- package/lib/commands/kuma.js +1050 -0
- package/lib/commands/kumaSla.js +174 -0
- package/lib/commands/licence.js +39 -1
- package/lib/commands/loop.js +291 -0
- package/lib/commands/objective.js +48 -1
- package/lib/commands/org.js +21 -4
- package/lib/commands/provider.js +58 -0
- 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 +18 -1
- package/lib/commands/scope.js +2 -2
- package/lib/commands/server.js +113 -0
- package/lib/commands/shortcut.js +41 -0
- package/lib/commands/skills.js +20 -4
- package/lib/commands/vm.js +2 -2
- 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/client.js +10 -2
- package/lib/core/commandRegistrars.js +9 -0
- package/lib/core/config.js +1 -1
- package/lib/core/findingReconcile.js +90 -0
- package/lib/core/global-flags.js +1 -1
- package/lib/core/governedSession.js +1 -3
- package/lib/core/liveSessionsReport.js +1 -1
- package/lib/core/loopGlyph.js +97 -0
- package/lib/core/offload.js +36 -7
- package/lib/core/output.js +7 -7
- package/lib/core/sessionHeartbeat.js +12 -8
- package/lib/core/shortcut.js +83 -0
- package/lib/core/version.js +1 -1
- package/lib/core/vm.js +5 -0
- 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 +23 -0
- package/lib/quoxon/api.js +192 -6
- package/lib/quoxon/app.js +655 -89
- package/lib/quoxon/approvalFacts.js +8 -6
- package/lib/quoxon/approvalSentence.js +44 -0
- package/lib/quoxon/approvalsView.js +63 -4
- package/lib/quoxon/commandShort.js +15 -3
- package/lib/quoxon/commands.js +75 -10
- package/lib/quoxon/countdownLine.js +10 -1
- package/lib/quoxon/deck.js +30 -0
- package/lib/quoxon/demo.autoplay.js +1 -1
- package/lib/quoxon/demo.js +3 -1
- package/lib/quoxon/dividerAocl.js +4 -3
- package/lib/quoxon/dividerRule.js +3 -3
- package/lib/quoxon/dragonCompanion.js +255 -0
- package/lib/quoxon/dragonModes.js +1 -0
- package/lib/quoxon/dragonMoments.js +82 -0
- package/lib/quoxon/dragonReads.js +68 -0
- package/lib/quoxon/dragonView.js +2 -2
- package/lib/quoxon/events.js +24 -3
- package/lib/quoxon/extensions.js +276 -0
- package/lib/quoxon/inboxView.js +79 -0
- package/lib/quoxon/installCli.js +79 -0
- package/lib/quoxon/keys.js +2 -2
- package/lib/quoxon/loopCard.js +456 -0
- package/lib/quoxon/loopCards.js +180 -0
- package/lib/quoxon/loopStream.js +87 -0
- package/lib/quoxon/main.js +12 -3
- package/lib/quoxon/mouse.js +12 -1
- package/lib/quoxon/objectiveOffer.js +82 -0
- 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/shortcutOffer.js +28 -0
- package/lib/quoxon/onboarding/view.js +1 -1
- package/lib/quoxon/orgSwitch.js +27 -1
- package/lib/quoxon/planSteps.js +51 -0
- package/lib/quoxon/print.js +6 -3
- package/lib/quoxon/qdragonFly.js +7 -6
- package/lib/quoxon/qmotion.js +17 -0
- package/lib/quoxon/render.js +130 -33
- package/lib/quoxon/roster.js +26 -12
- package/lib/quoxon/rows.js +244 -1
- package/lib/quoxon/serverChip.js +42 -0
- package/lib/quoxon/session.js +10 -0
- package/lib/quoxon/slash/cliCommand.js +42 -1
- package/lib/quoxon/slash/cliLeaves.json +76 -1
- package/lib/quoxon/slash/evidenceCmd.js +34 -0
- package/lib/quoxon/slash/palette.js +1 -1
- package/lib/quoxon/slash/qpeCmd.js +35 -0
- package/lib/quoxon/slash/restart.js +25 -1
- package/lib/quoxon/slash/resume.js +18 -1
- package/lib/quoxon/slash/todoCmd.js +122 -0
- package/lib/quoxon/suggestCommand.js +33 -2
- package/lib/quoxon/toolCards.js +30 -0
- package/lib/quoxon/whatsnew.jsonl +45 -0
- package/lib/quoxon/windowOrg.js +11 -1
- package/lib/server/devServers.js +137 -0
- package/package.json +2 -1
- 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/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/factory/svg-rig.mjs +63 -0
- package/vendor/dragonlab/integration/companions-v1/characters.mjs +88 -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/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/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
package/lib/commands/actions.js
CHANGED
|
@@ -5,39 +5,52 @@
|
|
|
5
5
|
* Tracker answers could not be checked against CLI truth. These verbs call the
|
|
6
6
|
* SAME governed tool the Action Tracker UI and the agent use (POST
|
|
7
7
|
* /api/v1/tools/execute, tool_name action_list), so chat, UI and CLI read one
|
|
8
|
-
* source.
|
|
8
|
+
* source. `add` and `done` write through the collector route the dashboard
|
|
9
|
+
* uses (POST/PATCH /api/v1/actions) as the logged-in user, via core/actionItems.js,
|
|
10
|
+
* the same core the Quox TUI /todo uses. Move/update/delete stay in chat and the UI.
|
|
9
11
|
*/
|
|
10
12
|
|
|
11
|
-
import { formatOutput, formatDetail } from '../core/output.js';
|
|
13
|
+
import { formatOutput, formatDetail, printSuccess } from '../core/output.js';
|
|
12
14
|
import { QuoxError } from '../core/errors.js';
|
|
15
|
+
import { confirmDestructive } from '../core/confirm.js';
|
|
13
16
|
import { executeGovernedTool } from './tool.js';
|
|
17
|
+
import { addActionItem, completeActionItem, moveActionItem, deleteActionItem, STATUS_COLUMNS, PRIORITIES } from '../core/actionItems.js';
|
|
14
18
|
|
|
15
19
|
const STATUSES = ['todo', 'doing', 'done', 'all'];
|
|
20
|
+
const MAX_PAGES = 50;
|
|
16
21
|
|
|
17
22
|
async function listItems(ctx, { status = 'all', limit = 100, scope } = {}) {
|
|
18
23
|
if (!STATUSES.includes(status)) {
|
|
19
24
|
throw new QuoxError(`--status must be one of ${STATUSES.join(', ')}`);
|
|
20
25
|
}
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
const pageSize = Math.min(Math.max(Number(limit) || 20, 1), 100);
|
|
27
|
+
// Page through everything: a silent cap at one page hid 4 of 24 items (owner, 2026-10-20).
|
|
28
|
+
const all = [];
|
|
29
|
+
for (let page = 1; page <= MAX_PAGES; page++) {
|
|
30
|
+
const input = { status, limit: pageSize };
|
|
31
|
+
if (scope) input.scope_id = scope;
|
|
32
|
+
if (page > 1) input.page = page;
|
|
33
|
+
const data = await executeGovernedTool(ctx, 'action_list', input);
|
|
34
|
+
const result = data && data.result !== undefined ? data.result : data;
|
|
35
|
+
if (!result || result.success === false) {
|
|
36
|
+
throw new QuoxError(`action_list failed: ${(result && (result.error || result.message)) || 'no result'}`);
|
|
37
|
+
}
|
|
38
|
+
all.push(...(Array.isArray(result.items) ? result.items : []));
|
|
39
|
+
if (!result.hasMore) return all;
|
|
27
40
|
}
|
|
28
|
-
|
|
41
|
+
throw new QuoxError(`More than ${MAX_PAGES * pageSize} items; narrow with --status or --scope.`);
|
|
29
42
|
}
|
|
30
43
|
|
|
31
44
|
export function registerActionsCommands(program, ctx) {
|
|
32
45
|
const actions = program
|
|
33
46
|
.command('actions')
|
|
34
|
-
.description('Action Tracker items
|
|
47
|
+
.description('Action Tracker items: list, get, add, done, move, delete (edit in chat or the dashboard)');
|
|
35
48
|
|
|
36
49
|
actions
|
|
37
50
|
.command('list')
|
|
38
51
|
.description('List action items')
|
|
39
52
|
.option('--status <status>', 'todo, doing, done or all', 'all')
|
|
40
|
-
.option('--limit <n>', '
|
|
53
|
+
.option('--limit <n>', 'page size (1-100); every page is listed', '100')
|
|
41
54
|
.option('--scope <scopeId>', 'only items in this team/department/division scope')
|
|
42
55
|
.action(async (options) => {
|
|
43
56
|
const items = await listItems({ ...ctx, program }, options);
|
|
@@ -58,4 +71,61 @@ export function registerActionsCommands(program, ctx) {
|
|
|
58
71
|
if (!item) throw new QuoxError(`No action item ${id} among the latest 100 items.`);
|
|
59
72
|
formatDetail(item);
|
|
60
73
|
});
|
|
74
|
+
|
|
75
|
+
actions
|
|
76
|
+
.command('add')
|
|
77
|
+
.description('Add an Action Tracker item (todo) as the logged-in user')
|
|
78
|
+
.argument('<text...>', 'what needs doing')
|
|
79
|
+
.option('--due <date>', 'YYYY-MM-DD, today or tomorrow')
|
|
80
|
+
.option('--priority <priority>', `one of ${PRIORITIES.join(', ')}`)
|
|
81
|
+
.option('--description <text>', 'longer notes')
|
|
82
|
+
.action(async (words, options) => {
|
|
83
|
+
ctx.auth.requireAuth();
|
|
84
|
+
const item = await addActionItem(ctx.client, { title: words, due: options.due, priority: options.priority, description: options.description });
|
|
85
|
+
if (program.opts().output === 'json') return formatDetail(item);
|
|
86
|
+
printSuccess(`Added action item ${item.id}: ${item.title}${item.due_date ? ` (due ${String(item.due_date).slice(0, 10)})` : ''}`);
|
|
87
|
+
return undefined;
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
actions
|
|
91
|
+
.command('done')
|
|
92
|
+
.description('Mark an Action Tracker item done')
|
|
93
|
+
.argument('<id>', 'action item id (from `quox actions list`)')
|
|
94
|
+
.action(async (id) => {
|
|
95
|
+
ctx.auth.requireAuth();
|
|
96
|
+
const item = await completeActionItem(ctx.client, id);
|
|
97
|
+
if (program.opts().output === 'json') return formatDetail(item);
|
|
98
|
+
printSuccess(`Marked done ${item.id}: ${item.title}`);
|
|
99
|
+
return undefined;
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
actions
|
|
103
|
+
.command('move')
|
|
104
|
+
.description('Move an Action Tracker item to todo, doing or done')
|
|
105
|
+
.argument('<id>', 'action item id (from `quox actions list`)')
|
|
106
|
+
.argument('<status>', STATUS_COLUMNS.join(', '))
|
|
107
|
+
.action(async (id, status) => {
|
|
108
|
+
ctx.auth.requireAuth();
|
|
109
|
+
const item = await moveActionItem(ctx.client, id, status);
|
|
110
|
+
if (program.opts().output === 'json') return formatDetail(item);
|
|
111
|
+
printSuccess(`Moved ${item.id} to ${item.status}: ${item.title}`);
|
|
112
|
+
return undefined;
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
actions
|
|
116
|
+
.command('delete')
|
|
117
|
+
.description('Delete one of your Action Tracker items (asks first; --yes skips, required without a terminal)')
|
|
118
|
+
.argument('<id>', 'action item id (from `quox actions list`)')
|
|
119
|
+
.option('-y, --yes', 'do not ask')
|
|
120
|
+
.action(async (id, options) => {
|
|
121
|
+
ctx.auth.requireAuth();
|
|
122
|
+
if (!(await confirmDestructive(program, { force: options.yes === true }, `Delete action item ${id}?`))) {
|
|
123
|
+
console.log('Not deleted.');
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
const r = await deleteActionItem(ctx.client, id);
|
|
127
|
+
if (program.opts().output === 'json') return formatDetail(r);
|
|
128
|
+
printSuccess(`Deleted ${r.id}${r.title ? `: ${r.title}` : ''}`);
|
|
129
|
+
return undefined;
|
|
130
|
+
});
|
|
61
131
|
}
|
package/lib/commands/agent.js
CHANGED
|
@@ -8,6 +8,7 @@ import { resolveOrgId } from './helpers.js';
|
|
|
8
8
|
import { resolveEndpoint } from '../api/endpoints.js';
|
|
9
9
|
import { QuoxApiError } from '../core/errors.js';
|
|
10
10
|
import { loadBundledAssistantsRoster } from '../core/agentAlias.js';
|
|
11
|
+
import { AGENT_FAMILY_IDS } from '../core/agentFamilies.js';
|
|
11
12
|
import { registerAgentAttachCommands } from './agentAttach.js';
|
|
12
13
|
|
|
13
14
|
// quox#1169: the prompt, domains and parent a user gives their own agent. Auth reads
|
|
@@ -64,6 +65,7 @@ export function registerAgentCommands(program, ctx) {
|
|
|
64
65
|
.option('--level <level>', 'filter by level (0-4)')
|
|
65
66
|
.option('--source <source>', 'registry | org | all', 'all')
|
|
66
67
|
.option('--plugin <id>', 'only agents that require this plugin')
|
|
68
|
+
.option('--family <id>', `only agents in this family (${AGENT_FAMILY_IDS.join(', ')})`)
|
|
67
69
|
.option('--search <text>', 'substring match on id, name or description')
|
|
68
70
|
.action(async (options) => {
|
|
69
71
|
auth.requireAuth();
|
|
@@ -71,6 +73,9 @@ export function registerAgentCommands(program, ctx) {
|
|
|
71
73
|
if (!['registry', 'org', 'all'].includes(source)) {
|
|
72
74
|
throw new Error(`--source must be registry, org or all (got "${options.source}")`);
|
|
73
75
|
}
|
|
76
|
+
if (options.family !== undefined && !AGENT_FAMILY_IDS.includes(options.family)) {
|
|
77
|
+
throw new Error(`unknown family "${options.family}". Valid families: ${AGENT_FAMILY_IDS.join(', ')}`);
|
|
78
|
+
}
|
|
74
79
|
const notes = [];
|
|
75
80
|
|
|
76
81
|
// Registered specialists (server roster; bundled roster if the server lacks the route).
|
|
@@ -84,7 +89,7 @@ export function registerAgentCommands(program, ctx) {
|
|
|
84
89
|
} catch (err) {
|
|
85
90
|
notes.push(`note: server registry unavailable (${err.message}); showing the bundled roster, which may be out of date.`);
|
|
86
91
|
registry = loadBundledAssistantsRoster().map(r => ({
|
|
87
|
-
agent_id: r.id, name: r.name || r.id, level: null, requires_plugin: null, tool_count: null, source: 'registry',
|
|
92
|
+
agent_id: r.id, name: r.name || r.id, level: null, requires_plugin: null, family: r.family || null, tool_count: null, source: 'registry',
|
|
88
93
|
}));
|
|
89
94
|
}
|
|
90
95
|
}
|
|
@@ -113,7 +118,7 @@ export function registerAgentCommands(program, ctx) {
|
|
|
113
118
|
const key = o.agent_id || o.id;
|
|
114
119
|
const reg = merged.get(key);
|
|
115
120
|
merged.set(key, reg
|
|
116
|
-
? { ...reg, ...o, requires_plugin: reg.requires_plugin, description: reg.description ?? o.description, tool_count: o.tool_count ?? reg.tool_count, source: 'org+registry' }
|
|
121
|
+
? { ...reg, ...o, requires_plugin: reg.requires_plugin, family: reg.family ?? null, description: reg.description ?? o.description, tool_count: o.tool_count ?? reg.tool_count, source: 'org+registry' }
|
|
117
122
|
: o);
|
|
118
123
|
}
|
|
119
124
|
|
|
@@ -121,6 +126,7 @@ export function registerAgentCommands(program, ctx) {
|
|
|
121
126
|
if (options.level !== undefined) all = all.filter(r => String(r.level) === String(options.level));
|
|
122
127
|
if (options.status) all = all.filter(r => r.status === options.status);
|
|
123
128
|
if (options.plugin) all = all.filter(r => r.requires_plugin === options.plugin);
|
|
129
|
+
if (options.family) all = all.filter(r => r.family === options.family);
|
|
124
130
|
if (options.search) {
|
|
125
131
|
const q = options.search.toLowerCase();
|
|
126
132
|
all = all.filter(r => [r.agent_id, r.name, r.description].some(v => String(v ?? '').toLowerCase().includes(q)));
|
|
@@ -130,10 +136,11 @@ export function registerAgentCommands(program, ctx) {
|
|
|
130
136
|
for (const n of notes) console.error(n);
|
|
131
137
|
const dash = (v) => (v === null || v === undefined || v === '' ? '-' : v);
|
|
132
138
|
formatOutput({
|
|
133
|
-
columns: ['agent_id', 'name', 'source', 'level', 'state', 'tool_count'],
|
|
134
|
-
headers: ['Agent ID', 'Name', 'Source', 'Level', 'Status/Plugin', 'Tools'],
|
|
139
|
+
columns: ['agent_id', 'name', 'family', 'source', 'level', 'state', 'tool_count'],
|
|
140
|
+
headers: ['Agent ID', 'Name', 'Family', 'Source', 'Level', 'Status/Plugin', 'Tools'],
|
|
135
141
|
rows: all.map(r => ({
|
|
136
142
|
...r,
|
|
143
|
+
family: dash(r.family),
|
|
137
144
|
level: dash(r.level),
|
|
138
145
|
state: r.status ? r.status : (r.requires_plugin ? `plugin:${r.requires_plugin}` : '-'),
|
|
139
146
|
tool_count: dash(r.tool_count),
|
|
@@ -435,6 +435,38 @@ export function execResultFailed(data) {
|
|
|
435
435
|
return negative(data.success) || negative(data.ok);
|
|
436
436
|
}
|
|
437
437
|
|
|
438
|
+
/**
|
|
439
|
+
* A governed tool that was held for a human returns { success:false, pending:true, approvalId }. That is not a
|
|
440
|
+
* failure: the approval is filed and waiting. Returns the approval id, or null when the result is not pending.
|
|
441
|
+
*/
|
|
442
|
+
export function execResultPending(data) {
|
|
443
|
+
if (!data || typeof data !== 'object') return null;
|
|
444
|
+
const src = data.pending === true ? data : (data.result && typeof data.result === 'object' && data.result.pending === true ? data.result : null);
|
|
445
|
+
if (!src) return null;
|
|
446
|
+
return src.approvalId || src.approval_id || (src._approval && src._approval.id) || 'unknown';
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* A governed tool held by the policy layer answers HTTP 403 with {pending:true, approvalId}: the approval IS filed.
|
|
451
|
+
* Returns that body when an API error carries one (so the caller renders awaiting approval), else null.
|
|
452
|
+
*/
|
|
453
|
+
export function pendingBodyOfError(err) {
|
|
454
|
+
const body = err && err.body && typeof err.body === 'object' ? err.body : null;
|
|
455
|
+
return body && execResultPending(body) ? body : null;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Is this 401/403 really about the attach token? 401 is an unauthenticated bearer. A 403 counts only when the server
|
|
460
|
+
* says so (attach/lease/token in its code or message); any other 403 is the server's own refusal and is shown verbatim.
|
|
461
|
+
*/
|
|
462
|
+
export function isAttachTokenError(err) {
|
|
463
|
+
const status = err && err.status;
|
|
464
|
+
if (status === 401) return true;
|
|
465
|
+
if (status !== 403) return false;
|
|
466
|
+
const text = [err && err.code, err && err.body && err.body.error, err && err.body && err.body.error_code, err && err.message].filter(Boolean).join(' ');
|
|
467
|
+
return /attach|lease|token/i.test(text);
|
|
468
|
+
}
|
|
469
|
+
|
|
438
470
|
/** The first human-readable error string a failed tool result carries, or a default. */
|
|
439
471
|
function execResultError(data) {
|
|
440
472
|
return (data && (data.error || data.error_code || data.reason || data.message)) || 'tool reported failure';
|
|
@@ -1343,13 +1375,17 @@ export function registerAgentAttachCommands(agent, ctx, program) {
|
|
|
1343
1375
|
});
|
|
1344
1376
|
} catch (err) {
|
|
1345
1377
|
const status = err instanceof QuoxApiError ? err.status : 'network error';
|
|
1346
|
-
|
|
1378
|
+
const held = err instanceof QuoxApiError ? pendingBodyOfError(err) : null;
|
|
1379
|
+
if (held) {
|
|
1380
|
+
data = held; // the policy layer filed an approval: that is a result, not an error
|
|
1381
|
+
} else if (err instanceof QuoxApiError && isAttachTokenError(err)) {
|
|
1347
1382
|
throw new Error(
|
|
1348
1383
|
`POST ${ep.path} failed (${status}): the attach token for "${agentId}" is no longer valid `
|
|
1349
1384
|
+ `(expired or revoked lease). Re-attach with \`quox agent attach ${agentId}\`.`
|
|
1350
1385
|
);
|
|
1386
|
+
} else {
|
|
1387
|
+
throw new Error(`POST ${ep.path} failed (${status}): ${err.message}`);
|
|
1351
1388
|
}
|
|
1352
|
-
throw new Error(`POST ${ep.path} failed (${status}): ${err.message}`);
|
|
1353
1389
|
}
|
|
1354
1390
|
|
|
1355
1391
|
// finding (autonomous loop): the glyph must reflect the TOOL result, not
|
|
@@ -1358,14 +1394,17 @@ export function registerAgentAttachCommands(agent, ctx, program) {
|
|
|
1358
1394
|
// agent read failures as passes ([error-body-is-not-data] at the CLI
|
|
1359
1395
|
// surface). Set a non-zero exit code too so a scripted / feedback-loop
|
|
1360
1396
|
// caller can't mistake it for a pass.
|
|
1361
|
-
const
|
|
1397
|
+
const pendingId = execResultPending(data);
|
|
1398
|
+
const toolOk = pendingId ? true : !execResultFailed(data);
|
|
1362
1399
|
// Skip the announce line under --json/-o json: a scripted caller parses
|
|
1363
1400
|
// stdout as a single JSON document, and the glyph line would break that.
|
|
1364
1401
|
if (program.opts().output !== 'json') {
|
|
1365
|
-
printEvent(line('quox', `exec ${options.tool}`,
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1402
|
+
printEvent(line('quox', `exec ${options.tool}`, pendingId
|
|
1403
|
+
? { result: `${agentId} · awaiting approval ${pendingId}`, status: 'busy' }
|
|
1404
|
+
: {
|
|
1405
|
+
result: toolOk ? agentId : `${agentId} · ${execResultError(data)}`,
|
|
1406
|
+
status: toolOk ? 'ok' : 'fail',
|
|
1407
|
+
}));
|
|
1369
1408
|
}
|
|
1370
1409
|
formatDetail(data, { format: program.opts().output, title: `Tool result: ${options.tool}` });
|
|
1371
1410
|
if (!toolOk) process.exitCode = 1;
|
|
@@ -34,6 +34,11 @@ export function registerApprovalsCommands(program, ctx) {
|
|
|
34
34
|
}
|
|
35
35
|
console.log(`Approval ${data.approval_id} tool: ${data.tool} status: ${data.status}`
|
|
36
36
|
+ (data.executed_at ? ` executed: ${data.executed_at}` : ''));
|
|
37
|
+
if (data.decided_by) {
|
|
38
|
+
const who = data.decided_by.name || data.decided_by.id;
|
|
39
|
+
const via = data.decided_via && data.decided_via.channel ? ` via ${data.decided_via.channel}` : '';
|
|
40
|
+
console.log(`decided by ${who} (${data.decided_by.actor_type || 'user'})${via} at ${data.decided_at || 'unknown time'}`);
|
|
41
|
+
}
|
|
37
42
|
if (data.result === null || data.result === undefined) {
|
|
38
43
|
if (data.status === 'pending') console.log('Still pending: a human has not decided yet.');
|
|
39
44
|
else if (data.status === 'resuming') console.log('Approved, executing. Re-run shortly.');
|
package/lib/commands/ask.js
CHANGED
|
@@ -96,6 +96,7 @@ export function registerAskCommands(program, { client, auth }) {
|
|
|
96
96
|
.option('--paths <files...>', 'every test file the commit edits (absolute, worktree or repo-relative: all are normalised)')
|
|
97
97
|
.option('--justify <text>', 'why it is safe, in plain words (required)')
|
|
98
98
|
.option('--why <text>', 'detail: the findings, before and after lines')
|
|
99
|
+
.option('--recommend <y|n>', 'your advice for the card: y or n (shown as the recommendation)')
|
|
99
100
|
.option('--session <name>', 'which session is asking (default: $QUOX_SESSION)')
|
|
100
101
|
.option('--slot <name>', 'your slot')
|
|
101
102
|
.option('--expires <dur>', 'how long it stays open')
|
|
@@ -105,6 +106,7 @@ export function registerAskCommands(program, { client, auth }) {
|
|
|
105
106
|
// commander lets the parent `ask` swallow flags it shares with this verb (--justify, --why, --session...), so merge both
|
|
106
107
|
const options = { ...ask.opts(), ...Object.fromEntries(Object.entries(subOptions).filter(([, v]) => v !== undefined)) };
|
|
107
108
|
if (!String(options.justify || '').trim()) { err('say why it is safe: --justify "<plain words>"'); process.exitCode = 2; return; }
|
|
109
|
+
if (!options.recommend) err('tip: add --recommend y|n so the card shows your advice');
|
|
108
110
|
const raw = [].concat(options.paths || []).flatMap((x) => String(x).split(',')).map((x) => x.trim()).filter(Boolean);
|
|
109
111
|
let root;
|
|
110
112
|
try { root = execFileSync('git', ['rev-parse', '--show-toplevel'], { encoding: 'utf8' }).trim(); } catch { root = process.cwd(); }
|
|
@@ -114,7 +116,7 @@ export function registerAskCommands(program, { client, auth }) {
|
|
|
114
116
|
// 2026-10-19: the server truncates long titles, and the attestation names only paths found in title+body,
|
|
115
117
|
// so a 7-file ask covered 3. Every path also goes in the body.
|
|
116
118
|
const files = `Files (${paths.length}):\n${paths.map((p) => `- ${p}`).join('\n')}`;
|
|
117
|
-
await fileAsk({ ...options, title: `Approve test edits: ${paths.join(' ')}`, why: options.why ? `${files}\n\n${options.why}` : files, yn: true, highRisk: true, justify: options.justify });
|
|
119
|
+
await fileAsk({ ...options, title: `Approve test edits: ${paths.join(' ')}`, why: options.why ? `${files}\n\n${options.why}` : files, yn: true, highRisk: true, justify: options.justify, recommend: options.recommend });
|
|
118
120
|
});
|
|
119
121
|
|
|
120
122
|
ask
|
|
@@ -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
|
+
}
|