quoxcli 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +2 -0
- package/bin/quox.js +99 -126
- package/lib/api/endpoints/admin.js +26 -0
- package/lib/api/endpoints/agent-schedules.js +26 -0
- package/lib/api/endpoints/agents.js +7 -0
- package/lib/api/endpoints/assistants.js +12 -0
- package/lib/api/endpoints/away.js +15 -0
- package/lib/api/endpoints/chat.js +7 -0
- package/lib/api/endpoints/contactsq.js +2 -0
- package/lib/api/endpoints/conversations.js +14 -0
- package/lib/api/endpoints/executions.js +13 -0
- package/lib/api/endpoints/fetchAllowlist.js +27 -0
- package/lib/api/endpoints/files.js +14 -0
- package/lib/api/endpoints/hat.js +30 -0
- package/lib/api/endpoints/inbox.js +20 -0
- package/lib/api/endpoints/index.js +14 -0
- package/lib/api/endpoints/memory.js +19 -0
- package/lib/api/endpoints/mind.js +13 -0
- package/lib/api/endpoints/mon.js +6 -0
- package/lib/api/endpoints/objectives.js +6 -0
- package/lib/api/endpoints/plugins.js +18 -0
- package/lib/api/endpoints/presence.js +33 -0
- package/lib/api/endpoints/quoxplan.js +7 -0
- package/lib/api/endpoints/quoxseo.js +6 -0
- package/lib/api/endpoints/router.js +17 -0
- package/lib/api/endpoints/safety.js +7 -0
- package/lib/api/endpoints/skills.js +20 -0
- package/lib/api/endpoints/stepup.js +20 -0
- package/lib/api/endpoints/teams.js +2 -2
- package/lib/api/endpoints/triggers.js +26 -0
- package/lib/api/endpoints/v1.js +86 -0
- package/lib/api/endpoints/ward.js +7 -0
- package/lib/api/endpoints/workflows.js +7 -0
- package/lib/api/endpoints/x.js +8 -0
- package/lib/commands/actions.js +61 -0
- package/lib/commands/admin.js +16 -0
- package/lib/commands/agent.js +163 -27
- package/lib/commands/agentAttach.js +94 -6
- package/lib/commands/approvals.js +73 -1
- package/lib/commands/aside.js +24 -0
- package/lib/commands/ask.js +144 -0
- package/lib/commands/assistant.js +59 -4
- package/lib/commands/auth.js +8 -2
- package/lib/commands/backup.js +8 -4
- package/lib/commands/brain.js +105 -14
- package/lib/commands/build.js +19 -5
- package/lib/commands/chat.js +57 -2
- package/lib/commands/claude.js +34 -8
- package/lib/commands/contacts.js +49 -0
- package/lib/commands/conversation.js +35 -4
- package/lib/commands/dashboard.js +43 -0
- package/lib/commands/department.js +28 -7
- package/lib/commands/devResident.js +15 -3
- package/lib/commands/devSupervisor.js +13 -1
- package/lib/commands/discord.js +75 -39
- package/lib/commands/done.js +98 -28
- package/lib/commands/engagement.js +17 -6
- package/lib/commands/fetchAllowlist.js +71 -0
- package/lib/commands/file.js +38 -1
- package/lib/commands/fleet.js +311 -4
- package/lib/commands/governance.js +12 -3
- package/lib/commands/gripe.js +44 -13
- package/lib/commands/hat.js +178 -9
- package/lib/commands/helpers.js +18 -1
- package/lib/commands/hyperion.js +58 -0
- package/lib/commands/inbox.js +74 -23
- package/lib/commands/licence.js +14 -155
- package/lib/commands/lucid.js +10 -4
- package/lib/commands/memory.js +85 -8
- package/lib/commands/mind.js +71 -0
- package/lib/commands/mon.js +69 -2
- package/lib/commands/monitoring.js +1 -1
- package/lib/commands/motion.js +2 -2
- package/lib/commands/nucleus.js +48 -17
- package/lib/commands/objective.js +11 -0
- package/lib/commands/offload.js +89 -0
- package/lib/commands/onboard.js +2 -1
- package/lib/commands/org.js +11 -1
- package/lib/commands/peek.js +28 -0
- package/lib/commands/plan.js +38 -0
- package/lib/commands/plugin.js +110 -0
- package/lib/commands/presence.js +89 -0
- package/lib/commands/provider.js +159 -0
- package/lib/commands/qlarity.js +19 -3
- package/lib/commands/quoxchat.js +13 -4
- package/lib/commands/quoxseo.js +38 -0
- package/lib/commands/reach.js +2 -1
- package/lib/commands/router.js +77 -0
- package/lib/commands/rows.js +358 -0
- package/lib/commands/run.js +64 -9
- package/lib/commands/server.js +292 -0
- package/lib/commands/setup.js +33 -7
- package/lib/commands/skills.js +48 -5
- package/lib/commands/slot.js +260 -7
- package/lib/commands/status.js +1 -1
- package/lib/commands/team.js +3 -1
- package/lib/commands/tips.js +23 -0
- package/lib/commands/tool.js +133 -45
- package/lib/commands/trace.js +12 -7
- package/lib/commands/trigger.js +95 -0
- package/lib/commands/update.js +46 -0
- package/lib/commands/vault.js +31 -8
- package/lib/commands/vm.js +237 -0
- package/lib/commands/voice.js +34 -3
- package/lib/commands/volt.js +45 -3
- package/lib/commands/walkthrough.js +32 -0
- package/lib/commands/watch.js +73 -21
- package/lib/commands/welcome.js +14 -0
- package/lib/commands/workflow.js +189 -32
- package/lib/commands/x.js +13 -4
- package/lib/core/actingAs.js +2 -2
- package/lib/core/activeSummary.js +186 -0
- package/lib/core/aside.js +54 -0
- package/lib/core/askDecision.js +60 -0
- package/lib/core/auth.js +164 -13
- package/lib/core/callerSource.js +1 -1
- package/lib/core/cliBuild.js +62 -0
- package/lib/core/client.js +144 -35
- package/lib/core/commandRegistrars.js +121 -0
- package/lib/core/config.js +142 -24
- package/lib/core/customHats.js +168 -0
- package/lib/core/doneEngineFetch.js +8 -0
- package/lib/core/errors.js +329 -20
- package/lib/core/global-flags.js +105 -5
- package/lib/core/hyperion.js +102 -0
- package/lib/core/inboxResolution.js +65 -0
- package/lib/core/lazyCommands.js +4 -1
- package/lib/core/offload.js +504 -0
- package/lib/core/org-override.js +1 -1
- package/lib/core/orgResolve.js +70 -0
- package/lib/core/output.js +4 -4
- package/lib/core/peek.js +126 -0
- package/lib/core/profileUsers.js +51 -0
- package/lib/core/promptCancel.js +18 -0
- package/lib/core/repoPath.js +11 -0
- package/lib/core/residentSeed.js +3 -1
- package/lib/core/rootOptions.js +39 -0
- package/lib/core/router.js +168 -0
- package/lib/core/selfUpdate.js +188 -0
- package/lib/core/sessionsOverview.js +64 -0
- package/lib/core/slotKeeper.js +248 -3
- package/lib/core/slotWake.js +120 -0
- package/lib/core/supervisorEngine.js +71 -2
- package/lib/core/version.js +1 -1
- package/lib/core/vm.js +361 -0
- package/lib/core/vmTui.js +86 -0
- package/lib/core/walkthrough.js +35 -0
- package/lib/hyperion/deck.js +29 -0
- package/lib/lucid/LucidCard.jsx +68 -0
- package/lib/lucid/LucidIntro.jsx +45 -58
- package/lib/lucid/cards.js +86 -0
- package/lib/lucid/markdown.js +81 -0
- package/lib/lucid/motion.js +56 -0
- package/lib/quoxon/activity.js +14 -2
- package/lib/quoxon/aocl.js +117 -0
- package/lib/quoxon/api.js +504 -25
- package/lib/quoxon/app.js +3000 -230
- package/lib/quoxon/approvalFacts.js +94 -0
- package/lib/quoxon/approvalsView.js +325 -0
- package/lib/quoxon/asideView.js +40 -0
- package/lib/quoxon/ask.js +121 -0
- package/lib/quoxon/away.js +52 -0
- package/lib/quoxon/bang.js +224 -0
- package/lib/quoxon/bulkRisk.js +17 -0
- package/lib/quoxon/capabilities.js +13 -6
- package/lib/quoxon/cells.js +53 -0
- package/lib/quoxon/commandShort.js +112 -0
- package/lib/quoxon/commands.js +363 -38
- package/lib/quoxon/connect.js +210 -23
- package/lib/quoxon/connectCard.js +19 -2
- package/lib/quoxon/consent.js +86 -0
- package/lib/quoxon/copy.js +4 -3
- package/lib/quoxon/countdownLine.js +11 -0
- package/lib/quoxon/demo.autoplay.js +29 -0
- package/lib/quoxon/demo.fixtures.js +1 -1
- package/lib/quoxon/demo.js +44 -3
- package/lib/quoxon/devObserver.js +20 -0
- package/lib/quoxon/dividerAocl.js +191 -0
- package/lib/quoxon/dividerRule.js +57 -0
- package/lib/quoxon/dragonModes.js +150 -0
- package/lib/quoxon/dragonView.js +41 -0
- package/lib/quoxon/earlyInput.js +40 -0
- package/lib/quoxon/echo.js +15 -0
- package/lib/quoxon/egg.js +2 -2
- package/lib/quoxon/events.js +244 -25
- package/lib/quoxon/feedback.js +50 -0
- package/lib/quoxon/gallery.js +21 -7
- package/lib/quoxon/handoffOffer.js +78 -0
- package/lib/quoxon/hatSwitch.js +37 -0
- package/lib/quoxon/headless.js +161 -0
- package/lib/quoxon/hostUrls.js +41 -0
- package/lib/quoxon/indicator.js +6 -2
- package/lib/quoxon/intent.js +12 -1
- package/lib/quoxon/keys.js +49 -6
- package/lib/quoxon/launcher.js +103 -0
- package/lib/quoxon/main.js +92 -160
- package/lib/quoxon/markdown.js +45 -21
- package/lib/quoxon/markdownSettle.js +19 -0
- package/lib/quoxon/matrixDomains.json +46 -0
- package/lib/quoxon/mentions.js +90 -0
- package/lib/quoxon/mind.js +103 -0
- package/lib/quoxon/mode.js +46 -0
- package/lib/quoxon/model.js +132 -26
- package/lib/quoxon/modelPrefs.js +345 -0
- package/lib/quoxon/motion/clock.js +3 -2
- package/lib/quoxon/motion/level.js +91 -0
- package/lib/quoxon/mouse.js +211 -0
- package/lib/quoxon/noai.js +57 -0
- package/lib/quoxon/objects/away.js +72 -0
- package/lib/quoxon/objects/fetch.js +17 -5
- package/lib/quoxon/objects/fleet.js +149 -0
- package/lib/quoxon/objects/health.js +186 -0
- package/lib/quoxon/objects/index.js +24 -3
- package/lib/quoxon/objects/mirror.js +137 -0
- package/lib/quoxon/objects/peek.js +17 -0
- package/lib/quoxon/objects/prove.js +130 -0
- package/lib/quoxon/objects/replay.js +125 -0
- package/lib/quoxon/objects/sitrep.js +58 -0
- package/lib/quoxon/objects/view.js +11 -0
- package/lib/quoxon/onboarding/copy.js +32 -4
- package/lib/quoxon/onboarding/credential.js +17 -4
- package/lib/quoxon/onboarding/dockerOffer.js +45 -0
- package/lib/quoxon/onboarding/failureCause.js +33 -0
- package/lib/quoxon/onboarding/finish.js +15 -0
- package/lib/quoxon/onboarding/index.js +34 -7
- package/lib/quoxon/onboarding/installRunner.js +64 -5
- package/lib/quoxon/onboarding/preflight.js +17 -3
- package/lib/quoxon/onboarding/probe.js +65 -0
- package/lib/quoxon/onboarding/view.js +76 -19
- package/lib/quoxon/orgLabel.js +10 -0
- package/lib/quoxon/orgSwitch.js +33 -0
- package/lib/quoxon/ownBroadcast.js +22 -0
- package/lib/quoxon/palette.js +43 -0
- package/lib/quoxon/print.js +9 -5
- package/lib/quoxon/profileSwitch.js +53 -0
- package/lib/quoxon/promptHistory.js +41 -0
- package/lib/quoxon/providers.js +48 -0
- package/lib/quoxon/qart.js +20 -51
- package/lib/quoxon/qdragon.js +410 -0
- package/lib/quoxon/qdragonFly.js +61 -0
- package/lib/quoxon/qdragonScenes.js +282 -0
- package/lib/quoxon/qeyes.js +213 -0
- package/lib/quoxon/qmotion.js +58 -37
- package/lib/quoxon/render.js +638 -119
- package/lib/quoxon/resend.js +72 -0
- package/lib/quoxon/roster.js +95 -9
- package/lib/quoxon/route.js +6 -5
- package/lib/quoxon/rows.js +711 -0
- package/lib/quoxon/rowsStore.js +80 -0
- package/lib/quoxon/screenContext.js +150 -0
- package/lib/quoxon/screenFrames.js +254 -0
- package/lib/quoxon/session.js +172 -2
- package/lib/quoxon/sessionDead.js +17 -0
- package/lib/quoxon/signin.js +125 -0
- package/lib/quoxon/slash/agents.js +112 -0
- package/lib/quoxon/slash/argSources.js +65 -0
- package/lib/quoxon/slash/args.js +65 -0
- package/lib/quoxon/slash/automate.js +89 -0
- package/lib/quoxon/slash/broadcast.js +140 -0
- package/lib/quoxon/slash/broadcastRun.js +108 -0
- package/lib/quoxon/slash/card.js +30 -0
- package/lib/quoxon/slash/challenge.js +114 -0
- package/lib/quoxon/slash/cliCommand.js +53 -0
- package/lib/quoxon/slash/cliLeaves.json +948 -0
- package/lib/quoxon/slash/compact.js +1 -1
- package/lib/quoxon/slash/confirm.js +30 -0
- package/lib/quoxon/slash/crystallize.js +69 -0
- package/lib/quoxon/slash/dashboard.js +136 -0
- package/lib/quoxon/slash/draft.js +54 -0
- package/lib/quoxon/slash/hatWalk.js +206 -0
- package/lib/quoxon/slash/inverses.js +77 -0
- package/lib/quoxon/slash/killer.js +24 -0
- package/lib/quoxon/slash/killerApi.js +16 -0
- package/lib/quoxon/slash/ledger.js +13 -8
- package/lib/quoxon/slash/listView.js +3 -1
- package/lib/quoxon/slash/modelCmd.js +261 -0
- package/lib/quoxon/slash/palette.js +21 -7
- package/lib/quoxon/slash/paletteController.js +117 -7
- package/lib/quoxon/slash/panels.js +21 -6
- package/lib/quoxon/slash/picker.js +20 -2
- package/lib/quoxon/slash/restart.js +71 -13
- package/lib/quoxon/slash/rowCmd.js +157 -0
- package/lib/quoxon/slash/rowWalk.js +175 -0
- package/lib/quoxon/slash/settings.js +16 -6
- package/lib/quoxon/slash/spawn.js +177 -0
- package/lib/quoxon/slash/stepUp.js +274 -0
- package/lib/quoxon/slash/stepUpCard.js +60 -0
- package/lib/quoxon/slash/stepUpCmds.js +78 -0
- package/lib/quoxon/slash/undoApi.js +38 -0
- package/lib/quoxon/slash/undoCmd.js +49 -0
- package/lib/quoxon/slash/undoWish.js +15 -0
- package/lib/quoxon/slash/watch.js +164 -0
- package/lib/quoxon/slash/wish.js +23 -0
- package/lib/quoxon/slash/wishCmd.js +32 -0
- package/lib/quoxon/slotChat.js +300 -0
- package/lib/quoxon/slotFormat.js +195 -0
- package/lib/quoxon/slotRun.js +131 -0
- package/lib/quoxon/sound/mapping.js +2 -1
- package/lib/quoxon/stray.js +22 -0
- package/lib/quoxon/suggestCommand.js +59 -0
- package/lib/quoxon/term.js +46 -11
- package/lib/quoxon/textBuffer.js +176 -2
- package/lib/quoxon/theme.js +58 -25
- package/lib/quoxon/tips.js +433 -0
- package/lib/quoxon/toolError.js +41 -0
- package/lib/quoxon/transcript.js +42 -4
- package/lib/quoxon/web/qface.js +18 -10
- package/lib/quoxon/welcome.js +1 -1
- package/lib/quoxon/whatsnew.js +136 -0
- package/lib/quoxon/whatsnew.jsonl +89 -0
- package/lib/quoxon/windowOrg.js +113 -0
- package/lib/server/diag.js +129 -0
- package/lib/server/doctor.js +71 -0
- package/lib/server/install.js +149 -0
- package/lib/server/pluginEnable.js +161 -0
- package/lib/server/skew.js +56 -0
- package/lib/server/snapshot.js +131 -0
- package/lib/server/status.js +128 -0
- package/lib/server/sys.js +65 -0
- package/lib/server/uninstall.js +63 -0
- package/lib/server/update.js +234 -0
- package/lib/toolOutputShape.js +315 -0
- package/lib/tui/views/HyperionDeck.jsx +79 -0
- package/lib/tui/views/LucidDeck.jsx +209 -217
- package/package.json +4 -1
- package/lib/core/websiteSession.js +0 -148
package/lib/core/output.js
CHANGED
|
@@ -12,7 +12,7 @@ import Table from 'cli-table3';
|
|
|
12
12
|
* Format and print tabular data
|
|
13
13
|
*
|
|
14
14
|
* @param {Object} result - { columns, headers, rows } or array of objects
|
|
15
|
-
* @param {Object} options - { format, title, emptyMessage }
|
|
15
|
+
* @param {Object} options - { format, title, emptyMessage, full } (full: no 60-char cell truncation in table mode)
|
|
16
16
|
*/
|
|
17
17
|
export function formatOutput(result, options = {}) {
|
|
18
18
|
const format = options.format || getOutputFormat();
|
|
@@ -168,7 +168,7 @@ function formatTable(rows, columns, headers, options) {
|
|
|
168
168
|
});
|
|
169
169
|
|
|
170
170
|
for (const row of rows) {
|
|
171
|
-
table.push(columns.map(col => formatValue(row[col])));
|
|
171
|
+
table.push(columns.map(col => formatValue(row[col], options.full === true)));
|
|
172
172
|
}
|
|
173
173
|
|
|
174
174
|
console.log(table.toString());
|
|
@@ -202,7 +202,7 @@ function csvEscape(value) {
|
|
|
202
202
|
return str;
|
|
203
203
|
}
|
|
204
204
|
|
|
205
|
-
function formatValue(value) {
|
|
205
|
+
function formatValue(value, full = false) {
|
|
206
206
|
if (value == null) return chalk.dim('-');
|
|
207
207
|
if (typeof value === 'boolean') return value ? chalk.green('yes') : chalk.dim('no');
|
|
208
208
|
|
|
@@ -221,7 +221,7 @@ function formatValue(value) {
|
|
|
221
221
|
}
|
|
222
222
|
|
|
223
223
|
// Truncate long values in table mode
|
|
224
|
-
if (str.length > 60) return str.slice(0, 57) + '...';
|
|
224
|
+
if (!full && str.length > 60) return str.slice(0, 57) + '...';
|
|
225
225
|
|
|
226
226
|
return str;
|
|
227
227
|
}
|
package/lib/core/peek.js
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PEEK: look up one id without knowing what it is. `quox peek <id>` and the TUI /peek share this.
|
|
3
|
+
*
|
|
4
|
+
* The id's shape picks the kinds worth asking for (an envelope, inbox item, objective and workflow run
|
|
5
|
+
* are all ULIDs, so a ULID asks all four at once; a WARD receipt is a UUID; an agent is a slug). Every
|
|
6
|
+
* lookup reuses an existing read endpoint. A 404 means "not this kind"; any other failure is kept and
|
|
7
|
+
* said, so an unknown id always names exactly what was tried and what each answered.
|
|
8
|
+
*/
|
|
9
|
+
import { resolveEndpoint } from '../api/endpoints.js';
|
|
10
|
+
|
|
11
|
+
const ULID = /^[0-9A-HJKMNP-TV-Z]{26}$/i;
|
|
12
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
13
|
+
const SLUG = /^[a-z][a-z0-9_-]{0,63}$/i;
|
|
14
|
+
|
|
15
|
+
export const KIND_LABEL = { envelope: 'envelope', inbox: 'inbox item', objective: 'objective', run: 'workflow run', receipt: 'WARD receipt', agent: 'agent' };
|
|
16
|
+
|
|
17
|
+
/** -> { id (normalised), kinds: ordered kinds to try, shape: a plain word for the id's shape }. */
|
|
18
|
+
export function idShapes(raw) {
|
|
19
|
+
const id = String(raw ?? '').trim();
|
|
20
|
+
if (!id) return { id, kinds: [], shape: 'empty' };
|
|
21
|
+
if (/^ENV_/i.test(id)) return { id, kinds: ['envelope'], shape: 'envelope id' };
|
|
22
|
+
const obj = id.match(/^OBJ_([0-9A-HJKMNP-TV-Z]{26})$/i); // an objective's corr id
|
|
23
|
+
if (obj) return { id: obj[1], kinds: ['objective'], shape: 'objective correlation id' };
|
|
24
|
+
if (UUID.test(id)) return { id, kinds: ['receipt', 'inbox'], shape: 'UUID' };
|
|
25
|
+
if (ULID.test(id)) return { id: id.toUpperCase(), kinds: ['envelope', 'inbox', 'objective', 'run'], shape: 'ULID' };
|
|
26
|
+
if (SLUG.test(id)) return { id, kinds: ['agent', 'inbox'], shape: 'name' };
|
|
27
|
+
return { id, kinds: [], shape: 'unrecognised' };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const unwrap = (d, ...keys) => { for (const k of keys) if (d && d[k] && typeof d[k] === 'object') return d[k]; return d; };
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* One fetcher per kind on a CLI client. Each resolves to the object or throws the client's error
|
|
34
|
+
* (with .status). orgId: a function returning the org the caller is acting in (the window org in the TUI).
|
|
35
|
+
*/
|
|
36
|
+
export function createPeekFetchers({ client, orgId }) {
|
|
37
|
+
const org = () => { const o = typeof orgId === 'function' ? orgId() : orgId; return typeof o === 'string' && o ? o : null; };
|
|
38
|
+
const get = (name, params, opts = {}) => { const ep = resolveEndpoint(name, params); return client.get(ep.path, { service: ep.service, retries: 0, timeout: 25000, ...opts }); };
|
|
39
|
+
const needOrg = () => { const o = org(); if (!o) { const e = new Error('no org on this login'); e.status = 0; throw e; } return o; };
|
|
40
|
+
return {
|
|
41
|
+
envelope: async (id) => unwrap(await get('aee.get', { id: encodeURIComponent(id) }), 'envelope'),
|
|
42
|
+
inbox: async (id) => unwrap(await get('inbox.get', { orgId: needOrg(), itemId: encodeURIComponent(id) }), 'item'),
|
|
43
|
+
objective: async (id) => unwrap(await get('objectives.get', { id: encodeURIComponent(id) }), 'objective', 'data'),
|
|
44
|
+
run: async (id) => {
|
|
45
|
+
try { return unwrap(await get('executions.get', { executionId: encodeURIComponent(id) }), 'data', 'execution'); }
|
|
46
|
+
catch (e) { if (e?.status !== 404) throw e; }
|
|
47
|
+
return unwrap(await get('runs.get', { orgId: needOrg(), pollRunId: encodeURIComponent(id) }), 'run', 'data');
|
|
48
|
+
},
|
|
49
|
+
receipt: async (id) => unwrap(await get('ward.entries.get', { id: encodeURIComponent(id) }), 'entry'),
|
|
50
|
+
agent: async (id) => {
|
|
51
|
+
try { return unwrap(await get('agents.get', { orgId: needOrg(), agentId: encodeURIComponent(id) }), 'agent'); }
|
|
52
|
+
catch (e) { if (e?.status !== 404) throw e; }
|
|
53
|
+
const reg = await get('v1.agents.registry', {});
|
|
54
|
+
const hit = (reg?.agents || []).find(r => r.agent_id === id || String(r.name || '').toLowerCase() === id.toLowerCase());
|
|
55
|
+
if (!hit) { const e = new Error('Agent not found'); e.status = 404; throw e; }
|
|
56
|
+
return { ...hit, source: 'registry' };
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const failText = (e) => {
|
|
62
|
+
if (e?.status === 404) return 'not found';
|
|
63
|
+
if (e?.status === 401 || e?.status === 403) return `HTTP ${e.status} (not allowed for this login)`;
|
|
64
|
+
if (e?.status) return `HTTP ${e.status}${e.message ? `: ${String(e.message).slice(0, 80)}` : ''}`;
|
|
65
|
+
return String(e?.message || 'failed').slice(0, 80);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Ask every candidate kind at once; the first hit in shape order wins, other hits are named.
|
|
70
|
+
* -> { id, shape, hit: { kind, data } | null, also: [kind], tried: [{ kind, result }] }
|
|
71
|
+
*/
|
|
72
|
+
export async function peekId(raw, fetchers) {
|
|
73
|
+
const { id, kinds, shape } = idShapes(raw);
|
|
74
|
+
const settled = await Promise.all(kinds.map(async (kind) => {
|
|
75
|
+
try {
|
|
76
|
+
const data = await fetchers[kind](id);
|
|
77
|
+
if (data == null || (typeof data === 'object' && !Object.keys(data).length)) return { kind, result: 'empty answer' };
|
|
78
|
+
return { kind, data };
|
|
79
|
+
} catch (e) { return { kind, result: failText(e) }; }
|
|
80
|
+
}));
|
|
81
|
+
const hits = settled.filter(s => 'data' in s);
|
|
82
|
+
return {
|
|
83
|
+
id, shape,
|
|
84
|
+
hit: hits[0] ? { kind: hits[0].kind, data: hits[0].data } : null,
|
|
85
|
+
also: hits.slice(1).map(s => s.kind),
|
|
86
|
+
tried: settled.map(s => ({ kind: s.kind, result: 'data' in s ? 'found' : s.result })),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const s = (v) => (v == null || v === '' ? null : String(v));
|
|
91
|
+
const short = (v, n = 16) => { const t = s(v); return t && t.length > n ? `${t.slice(0, n)}…` : t; };
|
|
92
|
+
const when = (v) => { const t = s(v); return t ? t.replace('T', ' ').replace(/\.\d+Z$/, 'Z') : null; };
|
|
93
|
+
const parse = (v) => { if (v && typeof v === 'object') return v; try { return JSON.parse(v); } catch { return null; } };
|
|
94
|
+
|
|
95
|
+
export const MORE = { envelope: (id) => `quox aee get ${id}`, inbox: (id) => `quox inbox get ${id}`, objective: (id) => `quox objective get ${id}`, run: (id) => `quox run get ${id}`, receipt: () => 'quox governance ward entries', agent: (id) => `quox agent get ${id}` };
|
|
96
|
+
|
|
97
|
+
/** kind -> data -> [[label, value|null]]: the card fields, shared by peekLines and the TUI card. */
|
|
98
|
+
export const ROWS = {
|
|
99
|
+
envelope: (d) => [['intent', s(d.intent)], ['from', s(d.from) && `${d.from} → ${d.to || '?'}`], ['type', s(d.type)], ['corr', s(d.corr)], ['at', when(d.ts || d.timestamp)]],
|
|
100
|
+
inbox: (d) => { const p = parse(d.payload_json) || {}; return [['title', s(d.title)], ['kind', s(d.kind)], ['status', [d.status, d.priority && `${d.priority} priority`].filter(Boolean).join(', ') || null], ['from', s(p.agent || p.session || p.source || d.created_by)], ['created', when(d.created_at)]]; },
|
|
101
|
+
objective: (d) => [['title', s(d.title)], ['status', s(d.status)], ['tasks', d.task_count != null ? `${d.tasks_completed ?? 0} of ${d.task_count} done${d.tasks_failed ? `, ${d.tasks_failed} failed` : ''}` : null], ['team', s(d.team_id)], ['created', when(d.created_at)]],
|
|
102
|
+
run: (d) => [['workflow', s(d.workflowName || d.workflow_name || d.workflowId || d.workflow_id)], ['status', s(d.status)], ['trigger', s(d.triggerData?.trigger || d.trigger)], ['started', when(d.startedAt || d.started_at || d.createdAt || d.created_at)], ['finished', when(d.finishedAt || d.completedAt || d.completed_at)]],
|
|
103
|
+
receipt: (d) => [['chain', d.chain_id ? `${d.chain_id} #${d.seq ?? '?'}` : null], ['witnessed', s(d.source_kind) && `${d.source_kind} ${d.source_id || ''}`.trim()], ['at', when(d.witnessed_at)], ['hash', short(d.chain_hash)]],
|
|
104
|
+
agent: (d) => [['name', s(d.name)], ['role', [d.role, d.level != null && `level ${d.level}`].filter(Boolean).join(', ') || null], ['about', s(d.description)], ['tools', d.tool_count != null ? String(d.tool_count) : null], ['source', s(d.source)]],
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Plain lines for a peek result: [{ text, tone }] (tone ok | warn | dim | null). Width-agnostic; the caller clips.
|
|
109
|
+
*/
|
|
110
|
+
export function peekLines(r) {
|
|
111
|
+
if (!r) return [];
|
|
112
|
+
if (!r.hit) {
|
|
113
|
+
if (r.shape === 'empty') return [{ text: 'Usage: peek <id>. An envelope, inbox item, objective, workflow run, WARD receipt or agent id.', tone: 'warn' }];
|
|
114
|
+
if (!r.tried.length) return [{ text: `"${r.id}" does not look like any id Quox issues (envelope, inbox item, objective, run or agent ULID, WARD receipt UUID, agent name). Nothing was looked up.`, tone: 'warn' }];
|
|
115
|
+
return [
|
|
116
|
+
{ text: `Nothing found for ${r.id}.`, tone: 'warn' },
|
|
117
|
+
{ text: `Tried (${r.shape}): ${r.tried.map(t => `${KIND_LABEL[t.kind]} ${t.result}`).join(' · ')}`, tone: 'dim' },
|
|
118
|
+
];
|
|
119
|
+
}
|
|
120
|
+
const { kind, data } = r.hit;
|
|
121
|
+
const out = [{ text: `◇ ${KIND_LABEL[kind]} ${r.id}`, tone: 'ok' }];
|
|
122
|
+
for (const [k, v] of ROWS[kind](data || {})) if (v) out.push({ text: ` ${k.padEnd(9)} ${v}`, tone: null });
|
|
123
|
+
if (r.also.length) out.push({ text: ` ${'also'.padEnd(9)} an id of the same value: ${r.also.map(k => KIND_LABEL[k]).join(', ')}`, tone: 'dim' });
|
|
124
|
+
out.push({ text: ` ${'more'.padEnd(9)} ${MORE[kind](r.id)}`, tone: 'dim' });
|
|
125
|
+
return out;
|
|
126
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #1034: which OTHER quox processes on this machine use a given CLI profile. `quox org switch` changes the shared
|
|
3
|
+
* profile's default org, so it names the sessions that will follow it (a TUI `/org switch` only moves its own window).
|
|
4
|
+
* Linux /proc only; elsewhere the answer is an empty list. Reads argv and the single QUOX_PROFILE env field, nothing else.
|
|
5
|
+
*/
|
|
6
|
+
import { readdirSync, readFileSync } from 'node:fs';
|
|
7
|
+
|
|
8
|
+
const isQuox = (a) => /(^|\/)quox(\.js|\.mjs)?$/.test(String(a || ''));
|
|
9
|
+
|
|
10
|
+
/** The profile a process selected: a name, '' for "the config default", or null when it is not quox. */
|
|
11
|
+
export function profileOf({ argv = [], env = {} }) {
|
|
12
|
+
const at = argv.slice(0, 2).findIndex(isQuox);
|
|
13
|
+
if (at < 0) return null;
|
|
14
|
+
const rest = argv.slice(at + 1);
|
|
15
|
+
for (let i = 0; i < rest.length; i++) {
|
|
16
|
+
const a = rest[i];
|
|
17
|
+
if (a === '-p' || a === '--profile') return rest[i + 1] || '';
|
|
18
|
+
if (a.startsWith('--profile=')) return a.slice('--profile='.length);
|
|
19
|
+
}
|
|
20
|
+
return env.QUOX_PROFILE || '';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function otherProfileUsers({ profile, defaultProfile, procs, selfPids = [] }) {
|
|
24
|
+
const self = new Set(selfPids);
|
|
25
|
+
return procs.filter(p => {
|
|
26
|
+
if (self.has(p.pid)) return false;
|
|
27
|
+
const sel = profileOf(p);
|
|
28
|
+
if (sel === null) return false;
|
|
29
|
+
return sel === profile || (sel === '' && profile === defaultProfile);
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Live processes of this user: [{ pid, argv, env: { QUOX_PROFILE? } }]. */
|
|
34
|
+
export function listProcs(root = '/proc') {
|
|
35
|
+
let dirs;
|
|
36
|
+
try { dirs = readdirSync(root).filter(d => /^\d+$/.test(d)); } catch { return []; }
|
|
37
|
+
const out = [];
|
|
38
|
+
for (const d of dirs) {
|
|
39
|
+
try {
|
|
40
|
+
const argv = readFileSync(`${root}/${d}/cmdline`, 'utf8').split('\0').filter(Boolean);
|
|
41
|
+
if (!argv.slice(0, 2).some(isQuox)) continue;
|
|
42
|
+
let env = {};
|
|
43
|
+
try {
|
|
44
|
+
const line = readFileSync(`${root}/${d}/environ`, 'utf8').split('\0').find(l => l.startsWith('QUOX_PROFILE='));
|
|
45
|
+
if (line) env = { QUOX_PROFILE: line.slice('QUOX_PROFILE='.length) };
|
|
46
|
+
} catch { /* not ours to read */ }
|
|
47
|
+
out.push({ pid: Number(d), argv, env });
|
|
48
|
+
} catch { /* exited */ }
|
|
49
|
+
}
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #1101: Ctrl-C (or a closed stdin) at an inquirer prompt rejects with ExitPromptError
|
|
3
|
+
* ("User force closed the prompt with SIGINT"). That is the user leaving, not a crash: say so in one word,
|
|
4
|
+
* put the cursor back, and exit 130 instead of printing a raw Error line.
|
|
5
|
+
*/
|
|
6
|
+
export const PROMPT_CANCELLED = 'Cancelled.';
|
|
7
|
+
|
|
8
|
+
export function isPromptCancel(err) {
|
|
9
|
+
if (!err) return false;
|
|
10
|
+
return err.name === 'ExitPromptError' || /User force closed the prompt/.test(String(err.message || ''));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function exitOnPromptCancel(err, { stderr = process.stderr, exit = process.exit } = {}) {
|
|
14
|
+
if (!isPromptCancel(err)) return false;
|
|
15
|
+
try { stderr.write(`\x1b[?25h\n${PROMPT_CANCELLED}\n`); } catch { /* the exit still happens */ }
|
|
16
|
+
exit(130);
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Repo-relative path normaliser for loosening asks. Keep in step with services/auth/lib/repoPath.js and
|
|
2
|
+
// scripts/guards/check-test-loosening.mjs (cli/test/core/repoPath.test.js asserts all three agree).
|
|
3
|
+
// "<anywhere>/quox-dashboard-wt-x/cli/a.test.js" -> "cli/a.test.js"; "./cli/a.test.js" -> "cli/a.test.js".
|
|
4
|
+
export function toRepoRelative(p) {
|
|
5
|
+
let s = String(p ?? '').trim().replace(/\\/g, '/');
|
|
6
|
+
s = s.replace(/^(?:.*?\/)?quox-dashboard(?:-wt-[^/]+)?\//, '');
|
|
7
|
+
s = s.replace(/^(?:\.\/)+/, '').replace(/^\/+/, '');
|
|
8
|
+
return s;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const LOOSENING_HINT = 'quox ask loosening --paths <repo-relative test files...> --justify "<why it is safe, plain words>"';
|
package/lib/core/residentSeed.js
CHANGED
|
@@ -53,10 +53,12 @@ function oneLine(text) {
|
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
function builtInTemplate({ hostId, slotName }) {
|
|
56
|
+
// #1021: Claude names a project dir by its path with separators turned into '-'.
|
|
57
|
+
const projectDirName = (process.env.QUOX_HOST_HOME || homedir()).replace(/[\\/]/g, '-');
|
|
56
58
|
const claimCmd = `quox slot checkout ${slotName} --session resident-${slotName} --kind interactive --host ${hostId} --create`;
|
|
57
59
|
return (
|
|
58
60
|
`You are the resident Claude session for host ${hostId} (slot ${slotName}). ` +
|
|
59
|
-
`Before doing anything else: read ~/.claude/projects
|
|
61
|
+
`Before doing anything else: read ~/.claude/projects/${projectDirName}/memory/MEMORY.md and every ` +
|
|
60
62
|
`memory file it indexes, then read ~/NOTES_TO_SELF.md if it exists, then read any identity or ` +
|
|
61
63
|
`onboarding docs in your home directory for this host. Read all of it before you act on anything. ` +
|
|
62
64
|
`Once you have read everything, claim your slot with: ${claimCmd}. ` +
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The root `quox` program's global options, in one place so the binary
|
|
3
|
+
* (bin/quox.js) and the safety-flag test (test/safetyFlagsReachLeaf.test.js)
|
|
4
|
+
* build the SAME root. A test that re-typed this list would pass against a
|
|
5
|
+
* root the binary does not have.
|
|
6
|
+
*/
|
|
7
|
+
import { mergeShadowedGlobals, assertDryRunHonoured } from './global-flags.js';
|
|
8
|
+
|
|
9
|
+
export function applyRootOptions(program) {
|
|
10
|
+
return program
|
|
11
|
+
.option('-o, --output <format>', 'output format: table, json, csv, quiet', 'table')
|
|
12
|
+
.option('--json', 'shorthand for --output json')
|
|
13
|
+
.option('-q, --quiet', 'suppress output, show IDs only')
|
|
14
|
+
.option('--org <slug|id>', 'override active organization (slug, name or ID; must match your session org)')
|
|
15
|
+
.option('-p, --profile <name>', 'use a specific config profile')
|
|
16
|
+
.option('--no-color', 'disable colored output')
|
|
17
|
+
.option('-v, --verbose', 'show detailed output')
|
|
18
|
+
.option('--debug', 'show HTTP request/response details')
|
|
19
|
+
.option('--dry-run', 'show what would happen without executing')
|
|
20
|
+
.option('-y, --yes', 'skip confirmation prompts')
|
|
21
|
+
.option('--insecure', 'allow self-signed SSL certificates');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The flag-delivery hooks every invocation needs, in order:
|
|
26
|
+
* 1. a value typed on the root or on a parent group reaches the leaf that
|
|
27
|
+
* declared the same option (commander binds it to the first command that
|
|
28
|
+
* knows it, so the leaf would otherwise read undefined or its default);
|
|
29
|
+
* 2. a --dry-run the leaf cannot honour is refused before anything runs.
|
|
30
|
+
*/
|
|
31
|
+
export function installFlagDeliveryHooks(program) {
|
|
32
|
+
program.hook('preAction', (thisCommand, actionCommand) => {
|
|
33
|
+
mergeShadowedGlobals(thisCommand, actionCommand);
|
|
34
|
+
});
|
|
35
|
+
program.hook('preAction', (thisCommand, actionCommand) => {
|
|
36
|
+
assertDryRunHonoured(thisCommand, actionCommand);
|
|
37
|
+
});
|
|
38
|
+
return program;
|
|
39
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ROUTEROS-WIRE: engine for `quox router`, the read-only MikroTik RouterOS views.
|
|
3
|
+
*
|
|
4
|
+
* Every verb is one NETWATCH tool run through the governed tool route
|
|
5
|
+
* (POST /api/v1/tools/execute, agent_id netwatch). The collector's mikrotikExecutor
|
|
6
|
+
* reaches the org's configured router through the allowlisted RouterOS proxy; nothing
|
|
7
|
+
* here talks to a router directly and no verb writes. Formatters are pure so the human
|
|
8
|
+
* output is tested without a router.
|
|
9
|
+
*/
|
|
10
|
+
import { resolveEndpoint } from '../api/endpoints.js';
|
|
11
|
+
|
|
12
|
+
/** verb -> the tool it runs. All read-only (aoclClassification layer 1, autonomy FULL). */
|
|
13
|
+
export const ROUTER_TOOLS = Object.freeze({
|
|
14
|
+
health: 'mikrotik_monitoring_health',
|
|
15
|
+
leases: 'mikrotik_dhcp_leases',
|
|
16
|
+
pools: 'mikrotik_dhcp_pool_status',
|
|
17
|
+
conflicts: 'mikrotik_dhcp_conflict_check',
|
|
18
|
+
'free-ip': 'mikrotik_dhcp_find_free_ip',
|
|
19
|
+
vpn: 'mikrotik_vpn_health',
|
|
20
|
+
wireguard: 'mikrotik_vpn_wireguard_peers',
|
|
21
|
+
inventory: 'mikrotik_discovery_inventory',
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
export function makeRouterExec({ client, orgId }) {
|
|
25
|
+
return async function exec(tool, input = {}) {
|
|
26
|
+
const ep = resolveEndpoint('v1.tools.execute');
|
|
27
|
+
const body = { tool_name: tool, input, agent_id: 'netwatch' };
|
|
28
|
+
if (orgId) body.org_id = orgId;
|
|
29
|
+
const data = await client.post(ep.path, { service: ep.service, body });
|
|
30
|
+
if (data && data.success === false) {
|
|
31
|
+
const detail = data.detail ? ` (${data.detail})` : '';
|
|
32
|
+
const err = new Error(`${tool} via POST ${ep.path} returned success:false: ${data.error || 'unknown error'}${detail}`);
|
|
33
|
+
err.toolError = data.error || 'unknown error';
|
|
34
|
+
err.tool = tool;
|
|
35
|
+
throw err;
|
|
36
|
+
}
|
|
37
|
+
return routerPayload(data);
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Unwrap the tool route envelope to the executor's own result object. */
|
|
42
|
+
export function routerPayload(data) {
|
|
43
|
+
let p = data;
|
|
44
|
+
for (let i = 0; i < 3 && p && typeof p === 'object' && !Array.isArray(p); i++) {
|
|
45
|
+
const inner = ['result', 'output', 'data'].map((k) => p[k]).find((v) => v !== undefined && v !== null);
|
|
46
|
+
if (inner === undefined) break;
|
|
47
|
+
p = inner;
|
|
48
|
+
}
|
|
49
|
+
if (typeof p === 'string') { try { p = JSON.parse(p); } catch { /* keep text */ } }
|
|
50
|
+
if (p && typeof p === 'object' && p.success === false) {
|
|
51
|
+
const err = new Error(`router read failed: ${p.error || 'unknown error'}${p.detail ? ` (${p.detail})` : ''}`);
|
|
52
|
+
err.toolError = p.error;
|
|
53
|
+
throw err;
|
|
54
|
+
}
|
|
55
|
+
return p;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const dash = (v) => (v === null || v === undefined || v === '' ? '-' : String(v));
|
|
59
|
+
const mib = (b) => (b === null || b === undefined ? '-' : `${Math.round(Number(b) / 1048576)} MiB`);
|
|
60
|
+
const pad = (rows) => {
|
|
61
|
+
const w = [];
|
|
62
|
+
for (const r of rows) r.forEach((c, i) => { w[i] = Math.max(w[i] || 0, String(c).length); });
|
|
63
|
+
return rows.map((r) => r.map((c, i) => (i === r.length - 1 ? String(c) : String(c).padEnd(w[i]))).join(' ').trimEnd());
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export function healthLines(p) {
|
|
67
|
+
const s = p.summary || {};
|
|
68
|
+
return [
|
|
69
|
+
`Router health score: ${dash(p.score)}/100`,
|
|
70
|
+
`CPU load: ${s.cpu_load_pct === null || s.cpu_load_pct === undefined ? '-' : `${s.cpu_load_pct}%`}${s.cpu_count ? ` (${s.cpu_count} cpu)` : ''}`,
|
|
71
|
+
`Memory: ${s.memory_used_pct === null || s.memory_used_pct === undefined ? '-' : `${s.memory_used_pct}% used`} (${mib(s.memory_free_bytes)} free of ${mib(s.memory_total_bytes)})`,
|
|
72
|
+
`Temperature: ${s.temperature_c === null || s.temperature_c === undefined ? 'unknown (no sensor reported)' : `${s.temperature_c} C`}`,
|
|
73
|
+
`Uptime: ${dash(s.uptime)}`,
|
|
74
|
+
`RouterOS: ${dash(s.version)} board ${dash(s.board_name)} ${dash(s.architecture)}`,
|
|
75
|
+
...(p.issues || []).map((i) => `! ${i}`),
|
|
76
|
+
];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function leaseLines(p) {
|
|
80
|
+
const rows = (p.leases || []).map((l) => [dash(l.address), dash(l['mac-address']), dash(l['host-name']), dash(l.status), l.dynamic === 'true' || l.dynamic === true ? 'dynamic' : 'static']);
|
|
81
|
+
return [`${p.total ?? rows.length} DHCP lease(s), ${p.bound ?? 0} bound`, ...(rows.length ? pad([['ADDRESS', 'MAC', 'HOST', 'STATUS', 'TYPE'], ...rows]) : [])];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function poolLines(p) {
|
|
85
|
+
const rows = (p.pools || []).map((x) => [dash(x.name), dash(x.ranges), x.size, x.used, x.free, x.used_pct === null || x.used_pct === undefined ? '-' : `${x.used_pct}%`]);
|
|
86
|
+
return [
|
|
87
|
+
`${p.pool_count ?? rows.length} DHCP pool(s): ${p.total_used ?? 0} of ${p.total_size ?? 0} addresses used, ${p.total_avail ?? 0} free`,
|
|
88
|
+
...(rows.length ? pad([['POOL', 'RANGES', 'SIZE', 'USED', 'FREE', 'USED%'], ...rows]) : []),
|
|
89
|
+
];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function conflictLines(p) {
|
|
93
|
+
const out = [p.plain_english || (p.healthy ? 'No IP address conflicts found.' : 'IP address conflicts found.')];
|
|
94
|
+
for (const c of p.conflicts || []) {
|
|
95
|
+
out.push(` ${c.address}: ${(c.claims || []).map((k) => `${k.mac} (${(k.seen_in || []).join(', ')})`).join(' vs ') || (c.macs || []).join(' vs ')}`);
|
|
96
|
+
}
|
|
97
|
+
if (p.limits) out.push(`Limit: ${p.limits}`);
|
|
98
|
+
for (const i of p.issues || []) out.push(`! ${i}`);
|
|
99
|
+
return out;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function freeIpLines(p) {
|
|
103
|
+
const ips = p.free_ips || [];
|
|
104
|
+
return [
|
|
105
|
+
`${ips.length} free address(es) in pool ${dash(p.pool_searched)} (checked against DHCP leases${p.arp_checked ? ' and ARP' : ', ARP unavailable'}). Nothing was reserved.`,
|
|
106
|
+
...ips.map((f) => ` ${f.address} (${dash(f.pool)})`),
|
|
107
|
+
...(p.note ? [`! ${p.note}`] : []),
|
|
108
|
+
];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function peerRow(x) {
|
|
112
|
+
return [dash(x.name || x.public_key), dash(x.interface), dash(x.endpoint), dash(x.allowed_address), dash(x.last_handshake), x.disabled ? 'disabled' : x.connected ? 'connected' : 'not connected'];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function wireguardLines(p) {
|
|
116
|
+
const rows = (p.peers || []).map(peerRow);
|
|
117
|
+
return [
|
|
118
|
+
`${p.total ?? rows.length} WireGuard peer(s), ${p.connected ?? 0} connected`,
|
|
119
|
+
...(rows.length ? pad([['PEER', 'IFACE', 'ENDPOINT', 'ALLOWED', 'HANDSHAKE', 'STATE'], ...rows]) : []),
|
|
120
|
+
...(p.connected_rule ? [`Rule: ${p.connected_rule}`] : []),
|
|
121
|
+
];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function vpnLines(p) {
|
|
125
|
+
const h = p.health || {};
|
|
126
|
+
const out = [p.plain_english || `${p.tunnels_up ?? 0} of ${p.tunnels_total ?? 0} VPN tunnel(s) up.`, `Score: ${p.score === null || p.score === undefined ? '- (no tunnels)' : `${p.score}/100`}`];
|
|
127
|
+
for (const x of h.wireguard || []) out.push(` wg peer ${peerRow(x)[0]}: ${peerRow(x)[5]} (handshake ${dash(x.last_handshake)})`);
|
|
128
|
+
for (const x of h.ipsec || []) out.push(` ipsec ${dash(x.remote_address)}: ${dash(x.state)}, up ${dash(x.uptime)}`);
|
|
129
|
+
for (const i of p.issues || []) out.push(`! ${i}`);
|
|
130
|
+
return out;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function inventoryLines(p) {
|
|
134
|
+
const routers = (p.routers || []).map((r) => [dash(r.identity || [r.platform, r.board_name].filter(Boolean).join(' ')), dash(r.role), dash(r.address), dash(r.version), dash(r.board_name)]);
|
|
135
|
+
const devices = (p.devices || []).map((d) => [dash(d.address), dash(d.mac), dash(d.host_name), dash(d.interface), d.seen_in_arp ? 'arp' : 'lease only']);
|
|
136
|
+
return [
|
|
137
|
+
`${p.router_count ?? routers.length} MikroTik router(s)`,
|
|
138
|
+
...pad([['ROUTER', 'ROLE', 'ADDRESS', 'VERSION', 'BOARD'], ...routers]),
|
|
139
|
+
`${p.total ?? devices.length} connected device(s)`,
|
|
140
|
+
...(devices.length ? pad([['ADDRESS', 'MAC', 'HOST', 'IFACE', 'SEEN'], ...devices]) : []),
|
|
141
|
+
];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export const ROUTER_FORMATTERS = Object.freeze({
|
|
145
|
+
health: healthLines, leases: leaseLines, pools: poolLines, conflicts: conflictLines,
|
|
146
|
+
'free-ip': freeIpLines, vpn: vpnLines, wireguard: wireguardLines, inventory: inventoryLines,
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
/** `quox router trust --show`: the fingerprint to compare out of band. Pure. */
|
|
150
|
+
export function formatTrust(d) {
|
|
151
|
+
if (!d || d.success === false) {
|
|
152
|
+
return [`Could not read the router certificate: ${(d && (d.error || d.detail)) || 'unknown error'}`, 'No credentials were sent.'];
|
|
153
|
+
}
|
|
154
|
+
const lines = [
|
|
155
|
+
`Router endpoint: ${d.host}:${d.port}`,
|
|
156
|
+
`Certificate SHA-256: ${d.cert_sha256_formatted}`,
|
|
157
|
+
`Public key SHA-256: ${d.spki_sha256_formatted}`,
|
|
158
|
+
`Subject: ${d.subject ? String(d.subject).replace(/\n/g, ', ') : '-'}`,
|
|
159
|
+
`Issuer: ${d.issuer ? String(d.issuer).replace(/\n/g, ', ') : '-'}${d.self_signed ? ' (self-signed)' : ''}`,
|
|
160
|
+
`Valid until: ${d.valid_to || '-'}`,
|
|
161
|
+
d.pin_configured
|
|
162
|
+
? (d.matches_configured_pin ? 'MIKROTIK_TLS_PIN is set and MATCHES this endpoint.' : 'MIKROTIK_TLS_PIN is set and does NOT match this endpoint. Do not trust it until you know why.')
|
|
163
|
+
: 'MIKROTIK_TLS_PIN is not set, so the collector sends nothing to this router yet.',
|
|
164
|
+
'No credentials were sent. Nothing was pinned.',
|
|
165
|
+
'Compare the fingerprint with the one on the router itself (System > Certificates) over a channel you trust, then set MIKROTIK_TLS_PIN to the SHA-256 value.',
|
|
166
|
+
];
|
|
167
|
+
return lines;
|
|
168
|
+
}
|