quoxcli 0.4.0 → 0.6.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 +110 -0
- package/README.md +2 -2
- package/bin/quox.js +19 -1
- package/bin/quoxon.js +6 -1
- package/lib/api/endpoints/admin.js +7 -0
- package/lib/api/endpoints/agent-attach.js +7 -0
- package/lib/api/endpoints/compliance.js +25 -0
- package/lib/api/endpoints/governance.js +7 -0
- package/lib/api/endpoints/inbox.js +6 -0
- package/lib/api/endpoints/inboxq.js +58 -0
- package/lib/api/endpoints/index.js +4 -0
- package/lib/api/endpoints/lucid.js +17 -0
- package/lib/api/endpoints/mcp.js +35 -0
- package/lib/api/endpoints/mon.js +122 -0
- package/lib/api/endpoints/notesq.js +2 -0
- package/lib/api/endpoints/quoxplan.js +51 -0
- package/lib/api/endpoints/tools.js +13 -0
- package/lib/api/endpoints/v1.js +13 -0
- package/lib/commands/actions.js +51 -13
- package/lib/commands/admin.js +35 -0
- package/lib/commands/agentAttach.js +15 -0
- package/lib/commands/approvals.js +193 -3
- package/lib/commands/ask.js +1 -1
- package/lib/commands/auth.js +64 -11
- package/lib/commands/backup.js +11 -4
- package/lib/commands/capabilities.js +3 -2
- package/lib/commands/chat.js +15 -0
- package/lib/commands/claude.js +4 -1
- package/lib/commands/compliance.js +109 -0
- package/lib/commands/connect.js +69 -0
- package/lib/commands/contacts.js +2 -0
- package/lib/commands/dev.js +6 -8
- package/lib/commands/doctor.js +22 -3
- package/lib/commands/evidence.js +2 -1
- package/lib/commands/gripe.js +24 -13
- package/lib/commands/inbox.js +37 -3
- package/lib/commands/inboxq.js +147 -0
- package/lib/commands/kuma.js +78 -5
- package/lib/commands/licenceLinkView.js +311 -0
- package/lib/commands/linkedin.js +19 -0
- package/lib/commands/loop.js +30 -3
- package/lib/commands/lucid.js +7 -0
- package/lib/commands/lucidGoals.js +389 -0
- package/lib/commands/mail.js +427 -25
- package/lib/commands/mcp.js +190 -0
- package/lib/commands/memory.js +68 -4
- package/lib/commands/memorySync.js +158 -0
- package/lib/commands/mon.js +69 -144
- package/lib/commands/monitoring.js +68 -0
- package/lib/commands/notes.js +29 -7
- package/lib/commands/notesSeal.js +617 -0
- package/lib/commands/objective.js +5 -2
- package/lib/commands/offload.js +52 -12
- package/lib/commands/plan.js +487 -52
- package/lib/commands/provider.js +29 -8
- package/lib/commands/qlarity.js +32 -1
- package/lib/commands/run.js +2 -1
- package/lib/commands/server.js +37 -0
- package/lib/commands/setup.js +127 -75
- package/lib/commands/skills.js +4 -1
- 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/volt.js +6 -1
- package/lib/commands/walkthrough.js +14 -2
- package/lib/commands/watch.js +11 -2
- package/lib/commands/wazuh.js +880 -0
- package/lib/commands/wazuhIntake.js +358 -0
- package/lib/commands/wazuhReport.js +192 -0
- package/lib/commands/workflow.js +47 -4
- package/lib/commands/x.js +50 -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 +27 -1
- package/lib/core/commandRegistrars.js +9 -0
- package/lib/core/config.js +42 -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/governedSession.js +135 -5
- package/lib/core/inboxResolution.js +3 -2
- package/lib/core/loginGuard.js +73 -0
- package/lib/core/offload.js +120 -0
- package/lib/core/org-override.js +5 -1
- package/lib/core/output.js +30 -5
- package/lib/core/profileTrust.js +182 -0
- package/lib/core/quoxglyph.js +22 -1
- package/lib/core/router.js +14 -0
- package/lib/core/selfUpdate.js +2 -0
- package/lib/core/stepUpPrompt.js +99 -0
- package/lib/core/version.js +1 -1
- package/lib/core/walkthrough.js +9 -5
- package/lib/dragon/select.js +2 -2
- package/lib/quoxon/EXTENSIONS.md +142 -19
- package/lib/quoxon/api.js +189 -34
- package/lib/quoxon/app.js +1277 -188
- package/lib/quoxon/approvalBatch.js +117 -0
- package/lib/quoxon/approvalFacts.js +56 -2
- package/lib/quoxon/approvalSentence.js +8 -4
- package/lib/quoxon/approvalsView.js +72 -12
- package/lib/quoxon/ask.js +4 -2
- package/lib/quoxon/autoOpen.js +94 -0
- package/lib/quoxon/backgroundWork.js +59 -0
- package/lib/quoxon/bang.js +8 -2
- package/lib/quoxon/capabilities.js +7 -2
- package/lib/quoxon/commandShort.js +8 -1
- package/lib/quoxon/commands.js +42 -12
- package/lib/quoxon/connect.js +5 -3
- package/lib/quoxon/deckLazy.js +58 -0
- package/lib/quoxon/delegationDragon.js +49 -0
- package/lib/quoxon/destructive.js +35 -0
- package/lib/quoxon/dnd.js +118 -0
- package/lib/quoxon/dragonCompanion.js +37 -7
- package/lib/quoxon/dragonMoments.js +78 -0
- package/lib/quoxon/echo.js +1 -0
- package/lib/quoxon/events.js +55 -8
- package/lib/quoxon/extensions.js +400 -38
- package/lib/quoxon/foreignText.js +49 -0
- package/lib/quoxon/gripeQueue.js +135 -0
- package/lib/quoxon/inboxView.js +125 -22
- package/lib/quoxon/keys.js +2 -2
- package/lib/quoxon/launcher.js +2 -1
- package/lib/quoxon/loopCard.js +7 -2
- package/lib/quoxon/loopCards.js +17 -8
- package/lib/quoxon/loopStream.js +20 -9
- package/lib/quoxon/mailView.js +71 -0
- package/lib/quoxon/main.js +27 -6
- package/lib/quoxon/markdown.js +1 -1
- package/lib/quoxon/model.js +15 -0
- package/lib/quoxon/neverAnswered.js +20 -0
- package/lib/quoxon/objects/away.js +43 -5
- package/lib/quoxon/onboarding/connectServer.js +162 -0
- package/lib/quoxon/onboarding/copy.js +6 -0
- package/lib/quoxon/onboarding/dockerOffer.js +3 -1
- package/lib/quoxon/onboarding/index.js +5 -43
- package/lib/quoxon/onboarding/probe.js +20 -1
- package/lib/quoxon/onboarding/view.js +16 -3
- package/lib/quoxon/orgSwitch.js +22 -0
- package/lib/quoxon/print.js +2 -0
- package/lib/quoxon/profileSwitch.js +7 -0
- package/lib/quoxon/promptHistory.js +3 -1
- package/lib/quoxon/render.js +68 -16
- package/lib/quoxon/resend.js +14 -0
- package/lib/quoxon/roster.js +10 -5
- package/lib/quoxon/rosterSessions.js +44 -0
- package/lib/quoxon/rows.js +3 -1
- package/lib/quoxon/screenContext.js +1 -1
- package/lib/quoxon/secretShape.js +88 -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 +48 -11
- package/lib/quoxon/slash/broadcastRun.js +24 -7
- package/lib/quoxon/slash/cliLeaves.json +98 -1
- package/lib/quoxon/slash/dashboard.js +2 -0
- package/lib/quoxon/slash/hatWalk.js +1 -1
- package/lib/quoxon/slash/listView.js +2 -1
- package/lib/quoxon/slash/palette.js +63 -1
- package/lib/quoxon/slash/paletteController.js +15 -5
- package/lib/quoxon/slash/restart.js +9 -4
- package/lib/quoxon/slash/stepUp.js +5 -5
- package/lib/quoxon/slash/stepUpCmds.js +74 -1
- package/lib/quoxon/slashSpinner.js +35 -0
- package/lib/quoxon/slotAttach.js +8 -0
- package/lib/quoxon/slotChat.js +24 -3
- package/lib/quoxon/slotFormat.js +1 -1
- package/lib/quoxon/slotOutbox.js +113 -0
- package/lib/quoxon/slotRun.js +2 -0
- package/lib/quoxon/suggestCommand.js +1 -1
- package/lib/quoxon/term.js +25 -1
- package/lib/quoxon/transcript.js +2 -1
- package/lib/quoxon/webPty.js +15 -0
- package/lib/quoxon/welcome.js +1 -1
- package/lib/quoxon/whatsnew.js +1 -1
- package/lib/quoxon/whatsnew.jsonl +42 -0
- package/lib/render/planBrief.js +614 -0
- package/lib/server/installFlow.js +168 -0
- package/lib/server/status.js +15 -1
- package/lib/toolOutputShape.js +61 -3
- package/package.json +3 -2
- package/vendor/dragonlab/SOURCE +3 -3
- 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/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/integration/companions-v1/characters.mjs +20 -0
- package/vendor/dragonlab/integration/companions-v1/quoxperticus.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/view.mjs +1 -1
- package/vendor/dragonlab/integration/companions-v1/vulcan.mjs +2 -0
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* dragon shows itself. The controller (dragonCompanion.js) runs the engine on every stream fact and owns the routing: while a
|
|
6
6
|
* companion owns the moments the Q dragon stays quiet, so a moment is never two dragons.
|
|
7
7
|
*/
|
|
8
|
+
import { dragonForAgent } from '../dragon/select.js';
|
|
9
|
+
|
|
8
10
|
export const MOMENTS = Object.freeze({
|
|
9
11
|
approval: Object.freeze({ mood: 'waiting', companion: true }),
|
|
10
12
|
failure: Object.freeze({ mood: 'failure', companion: true }),
|
|
@@ -80,3 +82,79 @@ export function createRosterWatch() {
|
|
|
80
82
|
},
|
|
81
83
|
};
|
|
82
84
|
}
|
|
85
|
+
|
|
86
|
+
// ---- sender moments: a named session or agent speaks to the owner, so its own dragon appears (companion mode `on` only) ---------
|
|
87
|
+
/** The same sender cannot call its dragon again for this long once it has shown (a chatty session is one dragon, not a parade). */
|
|
88
|
+
export const SENDER_COOLDOWN_MS = 5 * 60 * 1000;
|
|
89
|
+
/** Only a message this recent is a moment: a backlog replayed on (re)connect is history, not news. */
|
|
90
|
+
export const SLOT_FRESH_MS = 2 * 60 * 1000;
|
|
91
|
+
|
|
92
|
+
const head = (s) => String(s).split(/[\s\-_.:/]+/)[0];
|
|
93
|
+
/** One name -> a dragon id: the whole name, its first word, or that word without trailing digits (quoxperticus-126). */
|
|
94
|
+
function dragonForName(name) {
|
|
95
|
+
const s = String(name ?? '').trim();
|
|
96
|
+
if (!s) return null;
|
|
97
|
+
return dragonForAgent(s) || dragonForAgent(head(s)) || dragonForAgent(head(s).replace(/\d+$/, ''));
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* A slot message sender -> its agent's dragon, or null. `from` is "slot (display)" or a bare name. The speaker (the display name
|
|
101
|
+
* in brackets) wins, then the bare name, then the slot it holds. Only approved dragons (select.js AGENT_DRAGON) can match.
|
|
102
|
+
*/
|
|
103
|
+
export function dragonForSender(m) {
|
|
104
|
+
if (!m) return null;
|
|
105
|
+
const from = String(m.from ?? '').trim();
|
|
106
|
+
const inner = (from.match(/\(([^)]*)\)\s*$/) || [])[1];
|
|
107
|
+
const bare = from.replace(/\s*\(.*$/, '');
|
|
108
|
+
for (const n of [inner, bare, m.from_slot]) { const d = dragonForName(n); if (d) return d; }
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
const senderKey = (m) => String(m?.from_slot || m?.from || '').replace(/\s*\(.*$/, '').trim().toLowerCase();
|
|
112
|
+
|
|
113
|
+
/** A fresh slot message from a dragon session -> { dragon, sender, caption } | null. The caption says who and what kind, never the content. */
|
|
114
|
+
export function slotMessageMoment(m, { now = Date.now(), broadcast = false, run = false } = {}) {
|
|
115
|
+
const dragon = dragonForSender(m);
|
|
116
|
+
if (!dragon) return null;
|
|
117
|
+
const t = Date.parse(m.created_at ?? m.createdAt ?? '');
|
|
118
|
+
if (!Number.isFinite(t) || now - t > SLOT_FRESH_MS) return null;
|
|
119
|
+
const who = String(m.from).trim();
|
|
120
|
+
const caption = run ? `${who} asks you to run a command.` : broadcast ? `Broadcast from ${who}.` : `Message from ${who}.`;
|
|
121
|
+
return { dragon, sender: senderKey(m), caption };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Another session's approval card newly waiting, asked by an agent with a dragon -> { dragon, sender, key, caption } | null. */
|
|
125
|
+
export function waitingCardMoment(prev, next) {
|
|
126
|
+
const before = new Set((prev?.waiting || []).map(c => c.inboxItemId));
|
|
127
|
+
for (const c of next?.waiting || []) {
|
|
128
|
+
if (!c || before.has(c.inboxItemId)) continue;
|
|
129
|
+
const who = c.origin || c.askedBy;
|
|
130
|
+
const dragon = dragonForName(who) || dragonForName(c.askedBy);
|
|
131
|
+
if (!dragon) continue;
|
|
132
|
+
return { dragon, sender: String(who).toLowerCase(), key: c.inboxItemId, caption: `${who} wants your approval${c.tool ? ` (${c.tool})` : ''}.` };
|
|
133
|
+
}
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Dedupe + per-sender cooldown for sender moments. observe() remembers the message id at once (a re-delivery is never a second
|
|
139
|
+
* moment); shown() starts the sender's cooldown, so a moment refused because the owner was typing does not silence that sender.
|
|
140
|
+
*/
|
|
141
|
+
export function createSenderWatch({ now = Date.now, cooldownMs = SENDER_COOLDOWN_MS } = {}) {
|
|
142
|
+
const seen = new Set(), last = new Map();
|
|
143
|
+
const remember = (k) => { seen.add(k); if (seen.size > 500) seen.delete(seen.values().next().value); };
|
|
144
|
+
const cool = (sender) => { const t = last.get(sender); return t != null && now() - t < cooldownMs; };
|
|
145
|
+
return {
|
|
146
|
+
observe(m, opts = {}) {
|
|
147
|
+
const id = m?.event_id || m?.id || (m?.created_at ? `${m.created_at}|${m.seq ?? ''}|${m.from ?? ''}` : null);
|
|
148
|
+
if (id != null) { if (seen.has(String(id))) return null; remember(String(id)); }
|
|
149
|
+
const mo = slotMessageMoment(m, { ...opts, now: now() });
|
|
150
|
+
return mo && !cool(mo.sender) ? mo : null;
|
|
151
|
+
},
|
|
152
|
+
card(prev, next) {
|
|
153
|
+
const mo = waitingCardMoment(prev, next);
|
|
154
|
+
if (!mo || seen.has(`card:${mo.key}`)) return null;
|
|
155
|
+
remember(`card:${mo.key}`);
|
|
156
|
+
return cool(mo.sender) ? null : mo;
|
|
157
|
+
},
|
|
158
|
+
shown(sender) { last.set(sender, now()); },
|
|
159
|
+
};
|
|
160
|
+
}
|
package/lib/quoxon/echo.js
CHANGED
|
@@ -10,6 +10,7 @@ export const ECHO_CAP = 2000;
|
|
|
10
10
|
export function echoLine(text, { cols = 100 } = {}) {
|
|
11
11
|
let t = String(text ?? '').trim();
|
|
12
12
|
if (PIN_CMD.test(t)) t = t.split(/\s+/)[0]; // a stray argument may be a PIN: never shown
|
|
13
|
+
if (/^\/verify-email(\s|$)/i.test(t) && /(^|\D)\d{6}(\D|$)/.test(t)) t = '/verify-email'; // a 6-digit run may be a PIN typed by mistake: never shown
|
|
13
14
|
const full = redactSecrets(t).text;
|
|
14
15
|
return { shown: clip(full.replace(/\s*\n\s*/g, ' '), ECHO_CAP), full };
|
|
15
16
|
}
|
package/lib/quoxon/events.js
CHANGED
|
@@ -11,6 +11,9 @@
|
|
|
11
11
|
* turn : null | { id, status: running|interrupting, startedAt, stage, activity }
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
+
import { eventLine } from '../core/announce.js';
|
|
15
|
+
import { isDestructive, isDestructiveTool, destructiveObject } from './destructive.js';
|
|
16
|
+
import { heldReason } from './bulkRisk.js';
|
|
14
17
|
import { approvalSentence } from './approvalSentence.js';
|
|
15
18
|
import { dividerStep } from './dividerAocl.js';
|
|
16
19
|
import { approvalFacts } from './approvalFacts.js';
|
|
@@ -80,6 +83,15 @@ export function textGate(st, ev) {
|
|
|
80
83
|
|
|
81
84
|
const tms = (v) => { const t = typeof v === 'number' ? v : Date.parse(v); return Number.isFinite(t) ? t : null; };
|
|
82
85
|
|
|
86
|
+
/** A machine source key (`devsuite_pack_install:<host>:<pack>:<ver>`) is never shown as a requester: it names a human-readable origin instead. */
|
|
87
|
+
const SOURCE_KEY = /^[A-Za-z][\w.-]*(:[^:\s]+){2,}$/;
|
|
88
|
+
const SOURCE_NAMES = [[/^devsuite_/i, 'DevSuite (skill-pack sync)']];
|
|
89
|
+
export function humanRequester(v) {
|
|
90
|
+
const t = String(v ?? '').trim(); if (!t || !SOURCE_KEY.test(t)) return t || null;
|
|
91
|
+
const hit = SOURCE_NAMES.find(([re]) => re.test(t)); if (hit) return hit[1];
|
|
92
|
+
const head = t.split(':')[0].replace(/[_-]+/g, ' ').trim();
|
|
93
|
+
return head ? head.charAt(0).toUpperCase() + head.slice(1) : 'plugin or workflow';
|
|
94
|
+
}
|
|
83
95
|
const short = (v, n = 120) => { const t = typeof v === 'string' ? v : (v == null ? '' : JSON.stringify(v)); return t.length > n ? t.slice(0, n - 1) + '…' : t; };
|
|
84
96
|
const argText = (a) => a && typeof a === 'object' && !Array.isArray(a) && (a.argv || a.command) ? argText(a.argv ?? a.command) : Array.isArray(a) ? a.join(' ') : (a && typeof a === 'object' ? Object.entries(a).filter(([, v]) => v != null && typeof v !== 'object').slice(0, 4).map(([k, v]) => `${k}=${v}`).join(' ') : (a == null ? '' : String(a)));
|
|
85
97
|
|
|
@@ -97,7 +109,7 @@ export function describeInboxItem(it) {
|
|
|
97
109
|
const argv = m.argv ?? m.args ?? c.argv ?? c.args ?? p.argv ?? p.args ?? m.command ?? c.command ?? p.command ?? null;
|
|
98
110
|
const cmd = short([m.leaf || c.leaf || p.leaf, argText(argv)].filter(Boolean).join(' ').trim() || null);
|
|
99
111
|
const target = w.target || m.target || c.target || p.target || cmd || w.target_none || c.target_org || c.org_name || p.org_name || c.org_id || p.org_id || null;
|
|
100
|
-
const by = ob.agent || ob.agent_id || ob.session_name || ob.session || c.requested_by?.agent_label || c.source_label || p.source || it?.source || null;
|
|
112
|
+
const by = humanRequester(ob.agent || ob.agent_id || ob.session_name || ob.session || c.requested_by?.agent_label || c.source_label || p.source || it?.source || null);
|
|
101
113
|
const what = w.summary ? String(w.summary) : (it?.title ? short(String(it.title), 160) : null);
|
|
102
114
|
return { tool, target: target ? short(target, 200) : null, title: what, by: by ? String(by) : null, at: it?.created_at ?? it?.createdAt ?? null };
|
|
103
115
|
}
|
|
@@ -107,6 +119,13 @@ export function describeInboxItem(it) {
|
|
|
107
119
|
* Qualifies when created after `since` (the turn start, else app start), or raised on behalf of this
|
|
108
120
|
* user, or in this conversation. The inbox list is already scoped to the signed-in user and org.
|
|
109
121
|
*/
|
|
122
|
+
/** The contact-consent proposal on an inbox item (proposeOptin.js), or null. It has no executor: the TUI records the grant. */
|
|
123
|
+
export function consentOf(it) {
|
|
124
|
+
const p = it?.payload || {};
|
|
125
|
+
if (p.kind_detail !== 'contactability_optin' || !p.recipient || !p.channel) return null;
|
|
126
|
+
return { recipient: String(p.recipient), channel: String(p.channel), proposed: p.proposed && typeof p.proposed === 'object' ? p.proposed : {} };
|
|
127
|
+
}
|
|
128
|
+
|
|
110
129
|
export function cardFromInboxItem(it, { since = 0, userId = null, conversationId = null, now = 0 } = {}) {
|
|
111
130
|
if (!it || !it.id) return null;
|
|
112
131
|
if (it.payload?.type === 'decision') return null; // quox ask: answered with /answer N key from /approvals, never a y/n key card
|
|
@@ -126,9 +145,13 @@ export function cardFromInboxItem(it, { since = 0, userId = null, conversationId
|
|
|
126
145
|
const tool = d.tool || String(it.title || it.id);
|
|
127
146
|
return {
|
|
128
147
|
callId: `inbox:${it.id}`, inboxItemId: it.id, approvalId,
|
|
148
|
+
consent: consentOf(it), // a contact-consent proposal: approving records nothing until the person gives the lawful basis
|
|
129
149
|
tool, target: d.target, what: d.title, askedBy: d.by, askedAt: d.at, scope: null, risk: c.risk || null,
|
|
130
150
|
url: it.dashboard_url || null, shownAt: now, undescribable: !d.tool && !d.target && !it._plan,
|
|
131
151
|
plan: it._plan ? { lines: planLines(it._plan.steps), loaded: it._plan.loaded !== false, orgId: it._plan.orgId || null, orgName: it._plan.orgName || null } : null, // the plan's steps + the item's own org
|
|
152
|
+
destructive: isDestructive(it) ? destructiveObject(it) : null, // typed-name confirm before a y approves it
|
|
153
|
+
disruptive: [it, it.payload, c, m, it.payload?.what].some(o => o && o.disruptive === true), // server-marked: never batched
|
|
154
|
+
held: heldReason(it), // a risky ask is never part of a batch card
|
|
132
155
|
facts: approvalFacts(it), // APPROVAL-WWWH: the exact command, hosts, chain, reason, time and read-only class
|
|
133
156
|
// only an item that names THIS conversation is ours; anything else came from another session and must not take the keyboard
|
|
134
157
|
foreign: !here,
|
|
@@ -140,7 +163,7 @@ export function cardFromInboxItem(it, { since = 0, userId = null, conversationId
|
|
|
140
163
|
/** Where an inbox approval came from: its source label, else the payload source, else its type. */
|
|
141
164
|
export function approvalSourceLabel(it) {
|
|
142
165
|
const c = it?.payload?.approval_context || {};
|
|
143
|
-
return
|
|
166
|
+
return humanRequester(c.source_label || it?.payload?.source || it?.source || (c.requested_by && c.requested_by.agent_label) || it?.payload?.type || 'plugin or workflow');
|
|
144
167
|
}
|
|
145
168
|
|
|
146
169
|
/**
|
|
@@ -245,7 +268,7 @@ export function delegateInfo(ev) {
|
|
|
245
268
|
if (ev.tool !== 'delegate_to_agent') return {};
|
|
246
269
|
const id = ev.agent_id || (String(ev.summary || '').match(/\bagent_id=(\S+)/) || [])[1] || null;
|
|
247
270
|
if (!id) return {};
|
|
248
|
-
return { agentId: id, agentName: ev.agent_name || id, ...(ev.task_gist ? { taskGist: ev.task_gist } : {}) };
|
|
271
|
+
return { agentId: id, agentName: ev.agent_name || id, ...(ev.task_gist ? { taskGist: ev.task_gist } : {}), ...(ev.dragon === true ? { dragon: true } : {}) };
|
|
249
272
|
}
|
|
250
273
|
|
|
251
274
|
function upsertTool(s, ev, patch) {
|
|
@@ -377,7 +400,7 @@ function reduceInner(s, a) {
|
|
|
377
400
|
case 'help':
|
|
378
401
|
return { ...s, history: [...s.history, { id: nid(), kind: 'help', model: a.model }] };
|
|
379
402
|
case 'object': // inline object view (objects/): pure data, rebuilt at render width
|
|
380
|
-
return { ...s, history: [...s.history, { id: nid(), kind: 'object', obj: a.obj }] };
|
|
403
|
+
return { ...s, notify: scopeNotify(s.notify, a.obj), history: [...s.history, { id: nid(), kind: 'object', obj: a.obj }] };
|
|
381
404
|
case 'error':
|
|
382
405
|
return flush(addBlock(s, { kind: 'error', text: a.text }), false);
|
|
383
406
|
case 'verbose':
|
|
@@ -435,11 +458,11 @@ function reduceInner(s, a) {
|
|
|
435
458
|
const card = cardFromInboxItem(a.item, { since: s.turn?.startedAt ?? a.since ?? 0, userId: a.userId, conversationId: s.ctx.conversationId, now: a.now || 0 });
|
|
436
459
|
if (card && card.approvalId) { // an event card raised without an inbox id learns it from the poll (else y answers a null id: 404)
|
|
437
460
|
const j = s.approvals.findIndex(x => !x.inboxItemId && x.approvalId === card.approvalId);
|
|
438
|
-
if (j >= 0) { const approvals = s.approvals.slice(); approvals[j] = { ...approvals[j], inboxItemId: card.inboxItemId, url: approvals[j].url || card.url, facts: approvals[j].facts || card.facts }; return { ...s, approvals, seenInbox: { ...s.seenInbox, [card.inboxItemId]: true } }; }
|
|
461
|
+
if (j >= 0) { const approvals = s.approvals.slice(); approvals[j] = { ...approvals[j], inboxItemId: card.inboxItemId, url: approvals[j].url || card.url, facts: approvals[j].facts || card.facts, destructive: approvals[j].destructive || card.destructive }; return { ...s, approvals, seenInbox: { ...s.seenInbox, [card.inboxItemId]: true } }; }
|
|
439
462
|
}
|
|
440
463
|
if (card && card.approvalId) { // an event stub that could not be described learns its description from the inbox item
|
|
441
464
|
const j = s.approvals.findIndex(x => x.undescribable && (x.inboxItemId === card.inboxItemId || x.approvalId === card.approvalId));
|
|
442
|
-
if (j >= 0 && !card.undescribable) { const approvals = s.approvals.slice(); approvals[j] = { ...approvals[j], tool: card.tool, target: card.target, what: card.what, askedBy: card.askedBy, askedAt: card.askedAt, facts: card.facts, undescribable: false }; return { ...s, approvals }; }
|
|
465
|
+
if (j >= 0 && !card.undescribable) { const approvals = s.approvals.slice(); approvals[j] = { ...approvals[j], tool: card.tool, target: card.target, what: card.what, askedBy: card.askedBy, askedAt: card.askedAt, facts: card.facts, destructive: approvals[j].destructive || card.destructive, undescribable: false }; return { ...s, approvals }; }
|
|
443
466
|
}
|
|
444
467
|
if (!card || s.seenInbox[card.inboxItemId] || s.approvals.some(x => x.inboxItemId === card.inboxItemId || (card.approvalId && x.approvalId === card.approvalId))) return s;
|
|
445
468
|
if (card.foreign) { // another conversation's card: a compact notice, never the focused card
|
|
@@ -458,9 +481,15 @@ function reduceInner(s, a) {
|
|
|
458
481
|
return { ...s, approvals: s.approvals.filter(x => x.callId !== a.callId) };
|
|
459
482
|
case 'approval.sent': { // the human answered from this terminal
|
|
460
483
|
const rest = { ...s, inbox: (s.inbox || []).filter(x => x.id !== a.inboxItemId), seenInbox: a.inboxItemId ? { ...s.seenInbox, [a.inboxItemId]: true } : s.seenInbox, approvals: s.approvals.filter(x => x.callId !== a.callId && !(a.inboxItemId && x.inboxItemId === a.inboxItemId)), waiting: s.waiting.filter(x => x.callId !== a.callId && x.inboxItemId !== a.callId) };
|
|
484
|
+
if (a.notRecorded) return note(rest, a.notRecorded, 'warn'); // the server answered 2xx but did not record the yes
|
|
485
|
+
if (a.consent && a.action === 'approve') return note(rest, `approved, but no consent is recorded yet: ${a.consent.recipient} via ${a.consent.channel} needs the lawful basis. Type it now (one line), or n to leave it unrecorded.`, 'warn');
|
|
461
486
|
if (a.batch) return rest; // /approve N prints its own one-line summary
|
|
462
487
|
if (a.quiet && a.action === 'approve') return rest;
|
|
463
488
|
if (a.action !== 'approve') return note(rest, approvalSentence(a, false), 'warn');
|
|
489
|
+
if (a.foreign && a.approvalId) { // tracked: the poll adds one follow-up line with the outcome
|
|
490
|
+
const run = { callId: a.callId, approvalId: a.approvalId, tool: a.tool, target: a.target };
|
|
491
|
+
return note({ ...rest, running: [...rest.running.filter(x => x.callId !== a.callId), run] }, approvalSentence(a, true), 'ok');
|
|
492
|
+
}
|
|
464
493
|
if (a.foreign) return note(rest, approvalSentence(a, true), 'ok');
|
|
465
494
|
if (a.tool === 'hat.switch') { // applied server-side by the inbox approve; nothing to wait for
|
|
466
495
|
const to = String(a.target || '').split('\u2192').pop().replace(/\(privileged\)/, '').trim() || 'the new hat';
|
|
@@ -475,8 +504,9 @@ function reduceInner(s, a) {
|
|
|
475
504
|
const n = { ...s, running: s.running.filter(x => x.callId !== a.callId) };
|
|
476
505
|
const what = run ? `${run.tool || 'approval'}${run.target ? ` on ${run.target}` : ''}` : 'approval';
|
|
477
506
|
if (a.outcome === 'done') return note(n, `result: ${what}: ${a.text || 'completed'}`, 'ok');
|
|
478
|
-
if (a.outcome === 'failed') return note(n, `failed: ${what}: ${a.text || 'error'}`, 'warn');
|
|
507
|
+
if (a.outcome === 'failed') return note(n, `${Number.isInteger(a.exit) && a.exit >= 1 ? `ran, exit ${a.exit}` : 'failed'}: ${what}: ${a.text || 'error'}`, 'warn');
|
|
479
508
|
if (a.outcome === 'denied') return note(n, `denied: ${what}`, 'warn');
|
|
509
|
+
if (a.outcome === 'unrecorded') return note(n, `not run: ${what}. Your yes was saved in your inbox but never reached the tool runner: the server still shows the approval pending, with nobody recorded as deciding. Nothing ran. Ask the agent again for a fresh approval. Check: quox approvals result ${run && run.approvalId ? run.approvalId : '(no id)'}`, 'warn');
|
|
480
510
|
// FW4: never silent and never a guess. Say what is unknown and where the authoritative answer is.
|
|
481
511
|
const check = run && run.approvalId ? `quox approvals result ${run.approvalId} (or /approvals)` : '/approvals (no approval id was returned, so it cannot be tracked from here)';
|
|
482
512
|
return note(n, `result not known yet: ${what}; approved, but the server has not said whether it ran. It may still be running or may have run. Check: ${check}`, 'warn');
|
|
@@ -495,6 +525,15 @@ function reduceInner(s, a) {
|
|
|
495
525
|
}
|
|
496
526
|
|
|
497
527
|
/** The first card is armed from the moment it is actually SHOWN, not from when it was received. */
|
|
528
|
+
/** An away/sitrep card that lists older items or a source it cannot see: the approvals bar must not say all-clear next to it. */
|
|
529
|
+
function scopeNotify(n, obj) {
|
|
530
|
+
const d = obj?.data;
|
|
531
|
+
if (!d || (obj.type !== 'away' && obj.type !== 'sitrep')) return n;
|
|
532
|
+
const outstanding = obj.type === 'away' ? (d.standing?.count || 0) : (d.needs || []).length;
|
|
533
|
+
const blind = Array.isArray(d.blind) ? d.blind.length : 0;
|
|
534
|
+
return { ...n, outstanding, blind };
|
|
535
|
+
}
|
|
536
|
+
|
|
498
537
|
export function reduce(s, a) {
|
|
499
538
|
let n = reduceInner(s, a);
|
|
500
539
|
// TUI-DIVIDER: the bottom-rule AOCL strip, moved only by real turn events (dividerAocl.js).
|
|
@@ -593,7 +632,8 @@ function reduceEvent(s, ev, now) {
|
|
|
593
632
|
target: ev.target, scope: ev.scope, risk: ev.risk, url: ev.dashboard_url, shownAt: now,
|
|
594
633
|
undescribable: ev.describable === false || !ev.target,
|
|
595
634
|
turnId: s.turn?.id ?? ev.turn_id ?? null, // the turn this card arrived in (a stream event is always this conversation)
|
|
596
|
-
privileged: isPrivileged({ tool: ev.tool, risk: ev.risk, flag: ev.privileged === true || ev.elevated === true })
|
|
635
|
+
privileged: isPrivileged({ tool: ev.tool, risk: ev.risk, flag: ev.privileged === true || ev.elevated === true }),
|
|
636
|
+
destructive: ev.destructive === true || ev.irreversible === true || isDestructiveTool(ev.tool) ? (() => { const v = String(ev.object_name || ev.target || ev.inbox_item_id || ev.tool || 'item').slice(0, 80); return { label: v, name: v }; })() : null
|
|
597
637
|
};
|
|
598
638
|
if (s.approvals.some(x => x.callId === card.callId)) return s;
|
|
599
639
|
const dup = s.approvals.findIndex(x => (card.inboxItemId && x.inboxItemId === card.inboxItemId) || (card.approvalId && x.approvalId === card.approvalId));
|
|
@@ -611,6 +651,13 @@ function reduceEvent(s, ev, now) {
|
|
|
611
651
|
const n = { ...s, approvals: s.approvals.filter(x => x.callId !== ev.call_id) };
|
|
612
652
|
return card || ev.decision ? note(n, `${ev.decision === 'approved' ? '✔ approved by you' : `approval ${ev.decision || 'resolved'}`}${card ? `: ${card.tool} on ${card.target}` : ''}`, ev.decision === 'approved' ? 'ok' : 'warn') : n;
|
|
613
653
|
}
|
|
654
|
+
// QUOX-ANNOUNCE A2: a Quox effect of this turn (memory saved, approval held, delegation ...), one line, same renderer as the CLI verbs.
|
|
655
|
+
case 'quox_event': {
|
|
656
|
+
const text = eventLine(ev);
|
|
657
|
+
if (!text) return s;
|
|
658
|
+
if (ev.status === 'fail') return addBlock(s, { kind: 'error', text });
|
|
659
|
+
return addBlock(s, { kind: 'note', text, tone: ev.status === 'ok' ? 'ok' : ev.status === 'busy' ? 'accent' : 'warn' });
|
|
660
|
+
}
|
|
614
661
|
case 'steer.accepted': {
|
|
615
662
|
const i = s.steering.findIndex(x => x.text === ev.text);
|
|
616
663
|
const steering = i >= 0 ? s.steering.filter((_, k) => k !== i) : s.steering.slice(1);
|