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
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* /mind: QuoxMind cognitive implants, read from the collector (same data as `quox mind`).
|
|
3
|
+
* Pure: takes api.mind() output, returns [[text, tone]]. Implants with nothing recorded
|
|
4
|
+
* say "no activity recorded"; nothing is invented. Lines are not clipped: notes wrap to the
|
|
5
|
+
* terminal width, so a long description reads in full (owner drive 2026-10-15).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const MARK = { healthy: '✔', degraded: '⚠', weak: '⚠', collecting: '◐', 'not-reported': '·', unreachable: '✖', 'license-required': '⛔' };
|
|
9
|
+
|
|
10
|
+
/** The 14 implant short ids (the collector's registry, in order). Used for help and for "did you mean". */
|
|
11
|
+
export const MIND_IMPLANTS = ['sense', 'repobrain', 'memory', 'temporal', 'focus', 'value', 'guard', 'qortex', 'insight', 'executive', 'predict', 'reflect', 'dream', 'learn'];
|
|
12
|
+
const SHORT_OF = { 'quox-nucleus': 'repobrain', 'quox-brain2': 'qortex' };
|
|
13
|
+
const shortOf = (id) => SHORT_OF[id] || String(id).replace(/^quox-/, '');
|
|
14
|
+
|
|
15
|
+
export const MIND_HELP_ARGS = new Set(['help', '-h', '--help', '?']);
|
|
16
|
+
|
|
17
|
+
/** /mind help: the forms and the implant ids. No server call. */
|
|
18
|
+
export function mindHelpLines() {
|
|
19
|
+
return [
|
|
20
|
+
['/mind lists every QuoxMind implant with its status and last activity. Read-only, no model call.'],
|
|
21
|
+
['/mind <implant> shows one implant in full: what it does, how it grows, its metrics.'],
|
|
22
|
+
[`Implants: ${MIND_IMPLANTS.join(', ')}.`],
|
|
23
|
+
];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* A failed /mind call as plain lines. A 404 "unknown implant" names the implant that does not exist,
|
|
28
|
+
* lists the real ones, and points at a TUI command of the same name when there is one (/mind doctor -> /doctor).
|
|
29
|
+
* Any other failure reports the server's own reason, not just "Request failed with status N".
|
|
30
|
+
*/
|
|
31
|
+
export function mindErrorLines(e, arg, { isCommand = () => false } = {}) {
|
|
32
|
+
const body = e?.body && typeof e.body === 'object' ? e.body : {};
|
|
33
|
+
const reason = body.error || body.message || (typeof e?.code === 'string' && !/^E[A-Z]/.test(e.code) ? e.code : '') || e?.message || 'failed';
|
|
34
|
+
if (arg && (e?.status === 404 || /unknown implant/i.test(reason))) {
|
|
35
|
+
const ids = Array.isArray(body.valid) && body.valid.length ? body.valid.map(shortOf) : MIND_IMPLANTS;
|
|
36
|
+
const out = [[`No implant called "${arg}". /mind <implant> shows one implant's status (read-only).`, 'warn'],
|
|
37
|
+
[`Implants: ${ids.join(', ')}.`]];
|
|
38
|
+
const cmd = String(arg).split(/\s+/)[0].toLowerCase();
|
|
39
|
+
if (isCommand(cmd)) out.push([`For the ${cmd} command, use /${cmd}.`]);
|
|
40
|
+
else out.push(['/mind on its own lists them all.']);
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
return [[`mind: ${e?.status ? `HTTP ${e.status}: ` : ''}${reason}`, 'error']];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Relative "fired" time for an ISO timestamp; 'never' when null. `now` is injectable for tests. */
|
|
47
|
+
export function fmtFired(iso, now = Date.now()) {
|
|
48
|
+
if (!iso) return 'never';
|
|
49
|
+
const t = Date.parse(iso);
|
|
50
|
+
if (Number.isNaN(t)) return String(iso);
|
|
51
|
+
const s = Math.max(0, Math.round((now - t) / 1000));
|
|
52
|
+
if (s < 60) return 'just now';
|
|
53
|
+
if (s < 3600) return `${Math.floor(s / 60)}m ago`;
|
|
54
|
+
if (s < 86400) return `${Math.floor(s / 3600)}h ago`;
|
|
55
|
+
return `${Math.floor(s / 86400)}d ago`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The firing evidence for one implant (QUOXMIND-PROOF): last fired + 7-day count from the server.
|
|
60
|
+
* Returns null when the server predates the evidence fields, so the old line is kept.
|
|
61
|
+
*/
|
|
62
|
+
function firingText(i, now) {
|
|
63
|
+
if (!('fired_7d' in i)) return null;
|
|
64
|
+
if (i.activity_error) return `firing unknown: ${i.activity_error}`;
|
|
65
|
+
if (typeof i.fired_7d !== 'number') return 'firing not tracked';
|
|
66
|
+
const idle = typeof i.idle_runs_7d === 'number' && i.idle_runs_7d > 0 ? ` (${i.idle_runs_7d} idle runs)` : '';
|
|
67
|
+
return `${i.last_fired ? `fired ${fmtFired(i.last_fired, now)}` : 'never fired'} · 7d ${i.fired_7d}${idle}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function mindLines(d, { now = Date.now() } = {}) {
|
|
71
|
+
const out = [];
|
|
72
|
+
const add = (t, tone) => out.push([t, tone]);
|
|
73
|
+
if (d.implant) {
|
|
74
|
+
const i = d.implant;
|
|
75
|
+
add(`${MARK[i.status] || '·'} ${i.name} (${i.short}) · ${i.status}${i.tier ? ` · ${i.tier}` : ''}`);
|
|
76
|
+
add(` ${i.description || i.purpose}`);
|
|
77
|
+
if (i.inspired_by) add(` inspired by: ${i.inspired_by}`);
|
|
78
|
+
if (i.summary) add(` now: ${i.summary}`);
|
|
79
|
+
add(` ${i.activity}`);
|
|
80
|
+
const fired = firingText(i, now);
|
|
81
|
+
if (fired && typeof i.fired_7d === 'number') add(` fired: ${fmtFired(i.last_fired, now)}${i.last_fired ? ` (${i.last_fired})` : ''} · 7d ${i.fired_7d}${i.activity_source ? ` · from ${i.activity_source}` : ''}`);
|
|
82
|
+
else if (fired) add(` ${fired}`);
|
|
83
|
+
if (i.how_to_grow) add(` how it grows: ${i.how_to_grow}`);
|
|
84
|
+
if (Array.isArray(i.connected_to) && i.connected_to.length) add(` connected to: ${i.connected_to.map(shortOf).join(', ')}`);
|
|
85
|
+
for (const [k, v] of Object.entries(i.metrics || {})) if (v !== null && typeof v !== 'object') add(` ${k}: ${typeof v === 'number' && !Number.isInteger(v) ? Math.round(v * 100) / 100 : v}`);
|
|
86
|
+
const others = MIND_IMPLANTS.filter(s => s !== i.short);
|
|
87
|
+
add(`This is ${i.name}'s status, read-only: nothing runs. /mind for all; others: ${others.map(s => `/mind ${s}`).join(', ')}.`, 'dim');
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
90
|
+
const list = d.implants || [];
|
|
91
|
+
add(`QuoxMind: ${list.length} cognitive implants`);
|
|
92
|
+
for (const i of list) {
|
|
93
|
+
const fired = firingText(i, now);
|
|
94
|
+
const tail = fired !== null
|
|
95
|
+
? `${fired}${typeof i.fired_7d === 'number' && !i.activity_error && i.summary && i.activity !== 'no activity recorded' ? ` · ${i.summary}` : ''}`
|
|
96
|
+
: (i.activity === 'no activity recorded' ? 'no activity recorded' : i.summary || i.activity);
|
|
97
|
+
add(`${MARK[i.status] || '·'} ${i.short.padEnd(10)} ${i.status.padEnd(13)} ${tail}`, i.status === 'unreachable' ? 'warn' : undefined);
|
|
98
|
+
}
|
|
99
|
+
const h = d.health;
|
|
100
|
+
if (h && typeof h.fired_7d_total === 'number') add(`${h.fired_7d_implants} of ${list.length} fired in the last 7 days (${h.fired_7d_total} firings), ${h.silent_7d} silent, ${h.activity_unknown} not tracked. /mind <id> for one.`);
|
|
101
|
+
else if (h) add(`${h.with_activity} with activity, ${h.no_activity} with none recorded${h.average_health !== null ? `, average health ${h.average_health}` : ''}. /mind <id> for one.`);
|
|
102
|
+
return out;
|
|
103
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* /mode (owner 2026-10-16 "/say quoxon a but log b"): this window's mode.
|
|
3
|
+
* ask (default) Quox asks you before it acts; what needs approval is your org's policy.
|
|
4
|
+
* plan Quox investigates (read tools) and proposes the steps and exact commands; it runs nothing that
|
|
5
|
+
* changes state. The SERVER enforces it: the turn carries mode 'plan' (lib/planMode.js in the collector).
|
|
6
|
+
* auto not built. Logged as a plan: it needs standing grants first.
|
|
7
|
+
* Window-scoped: carried across /restart (QUOX_RESTART_MODE), never written to the shared profile.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export const MODES = ['ask', 'plan'];
|
|
11
|
+
export const AUTO_NOT_YET = 'not available yet: auto mode needs standing grants (planned).';
|
|
12
|
+
export const PLAN_GO = 'go ahead with the plan';
|
|
13
|
+
|
|
14
|
+
/** -> 'plan' | 'ask' (anything else, including 'auto', is 'ask'). */
|
|
15
|
+
export function normMode(v) {
|
|
16
|
+
return String(v || '').trim().toLowerCase() === 'plan' ? 'plan' : 'ask';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const DESC = {
|
|
20
|
+
ask: 'ask first: Quox asks you before it acts. What needs your approval is your org\'s policy on the server (/approvals lists what is waiting).',
|
|
21
|
+
plan: 'plan only: Quox investigates with read tools and proposes the steps and the exact commands. It runs nothing that changes state; the server enforces it.',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/** One line per mode for the arrow-key chooser (the same one /model uses). Auto is not offered: it is not built. */
|
|
25
|
+
export const MODE_ROWS = [
|
|
26
|
+
{ id: 'ask', text: 'ask first: Quox asks before it acts' },
|
|
27
|
+
{ id: 'plan', text: 'plan only: Quox proposes the steps and runs nothing that changes state' },
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Pure: the reply to `/mode [arg]` given the current mode.
|
|
32
|
+
* -> { text, tone, mode? } where mode is set only when it changes.
|
|
33
|
+
*/
|
|
34
|
+
export function modeReply(arg, current) {
|
|
35
|
+
const cur = normMode(current);
|
|
36
|
+
const a = String(arg || '').trim().toLowerCase();
|
|
37
|
+
if (!a || /^(-h|--help|help|\?)$/.test(a)) {
|
|
38
|
+
return { tone: null, text: `mode ${DESC[cur]}\nOptions: /mode ask (the default), /mode plan (plan only). Auto mode is ${AUTO_NOT_YET}` };
|
|
39
|
+
}
|
|
40
|
+
if (a === 'auto') return { tone: 'warn', text: `/mode auto is ${AUTO_NOT_YET} Options: /mode ask, /mode plan.` };
|
|
41
|
+
if (!MODES.includes(a)) return { tone: 'warn', text: `Unknown mode '${clipWord(a)}'. Options: /mode ask, /mode plan.` };
|
|
42
|
+
if (a === cur) return { tone: 'dim', text: `Already in mode ${DESC[cur]}` };
|
|
43
|
+
return { mode: a, tone: 'ok', text: a === 'plan' ? `mode ${DESC.plan} /mode ask to go back.` : `mode ${DESC.ask}` };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function clipWord(w) { return w.length > 20 ? `${w.slice(0, 19)}…` : w; }
|
package/lib/quoxon/model.js
CHANGED
|
@@ -7,15 +7,27 @@
|
|
|
7
7
|
* differs from what was asked, the client says so once (never a silent fallback).
|
|
8
8
|
* null choice = auto (no provider in the body; the server's policy picks).
|
|
9
9
|
*
|
|
10
|
-
* Availability is the same source as /connect: the
|
|
10
|
+
* Availability is the same source as /connect, /doctor and /capabilities (providers.js): the server's
|
|
11
|
+
* capabilities, with the org vault for detail.
|
|
11
12
|
* Model ids mirror src/config/modelCatalog.js (openai + anthropic rows); an id not listed is passed
|
|
12
13
|
* through with a note, the server decides whether it exists.
|
|
13
14
|
*/
|
|
14
|
-
import { PROVIDERS as CONNECT,
|
|
15
|
+
import { PROVIDERS as CONNECT, offerText } from './connect.js';
|
|
16
|
+
import { providerState, SUBSCRIPTION_NAME } from './providers.js';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* No hardcoded model lists for the hosted providers: OpenAI and the Claude API are listed live by the server
|
|
20
|
+
* (GET /api/v1/providers/<p>/models, 1 h server cache, refreshed by /model). The Claude SUBSCRIPTION has no
|
|
21
|
+
* key to list with, so it takes the Claude CLI's family aliases, which the CLI resolves to the newest model.
|
|
22
|
+
*/
|
|
23
|
+
export const CLAUDE_FAMILIES = ['fable', 'opus', 'sonnet', 'haiku'];
|
|
15
24
|
|
|
16
25
|
export const CHOICES = [
|
|
17
|
-
{ id: 'openai', provider: 'openai', name: 'OpenAI', vault: 'openai', connect: 'openai',
|
|
18
|
-
{ id: 'anthropic', provider: 'anthropic', name: 'Claude API', vault: 'anthropic', connect: 'anthropic',
|
|
26
|
+
{ id: 'openai', provider: 'openai', name: 'OpenAI', vault: 'openai', connect: 'openai', discover: true, models: [] },
|
|
27
|
+
{ id: 'anthropic', provider: 'anthropic', name: 'Claude API', vault: 'anthropic', connect: 'anthropic', discover: true, models: [] },
|
|
28
|
+
// R3: the server's signed-in Claude subscription (dev only). The stream asks for anthropic; with no Claude API
|
|
29
|
+
// key the server runs it through the claude CLI on that sign-in. `via` only tells the client which row it is.
|
|
30
|
+
{ id: 'claude-cli', provider: 'anthropic', via: 'subscription', cap: 'claude_subscription', name: SUBSCRIPTION_NAME, aliases: CLAUDE_FAMILIES, models: [] },
|
|
19
31
|
// B3: served by the chat stream; models are whatever that Ollama has installed (listed by the server).
|
|
20
32
|
{ id: 'ollama', provider: 'ollama', name: 'Ollama', vault: 'ollama', connect: 'ollama', dynamic: true, hint: 'http://<host>:11434', models: [] },
|
|
21
33
|
// B5: OpenAI-compatible presets. Models come from the provider's own /models (api.compatModels).
|
|
@@ -27,13 +39,16 @@ export const CHOICES = [
|
|
|
27
39
|
['custom', 'Custom', 'openai_compatible', false, ' https://<host>/v1']
|
|
28
40
|
].map(([id, name, vault, hosted, hint]) => ({ id, provider: id, name, vault, connect: id, dynamic: true, compat: true, hosted, selfHosted: !hosted, hint: hint.trim(), models: [] }))
|
|
29
41
|
];
|
|
30
|
-
const ALIASES = { gpt: 'openai', chatgpt: 'openai', claude: 'anthropic', 'claude-api': 'anthropic', 'anthropic-api': 'anthropic', local: 'ollama', hf: 'huggingface', 'lm-studio': 'lmstudio', 'openai-compatible': 'custom', openai_compatible: 'custom', default: 'auto', policy: 'auto' };
|
|
42
|
+
const ALIASES = { 'claude-subscription': 'claude-cli', subscription: 'claude-cli', 'claude-code': 'claude-cli', gpt: 'openai', chatgpt: 'openai', claude: 'anthropic', 'claude-api': 'anthropic', 'anthropic-api': 'anthropic', local: 'ollama', hf: 'huggingface', 'lm-studio': 'lmstudio', 'openai-compatible': 'custom', openai_compatible: 'custom', default: 'auto', policy: 'auto' };
|
|
31
43
|
const byProvider = (p) => CHOICES.find(c => c.provider === p) || null;
|
|
44
|
+
/** The CHOICES row a stored choice is (the subscription row shares provider anthropic). */
|
|
45
|
+
const rowOf = (ch) => (ch?.via === 'subscription' ? CHOICES.find(c => c.via === 'subscription') : byProvider(ch?.provider));
|
|
32
46
|
|
|
33
47
|
/** Stored or flag value -> { provider, model? } | null. Only the two known fields survive. */
|
|
34
48
|
export function normChoice(v) {
|
|
35
49
|
if (!v || typeof v !== 'object' || typeof v.provider !== 'string' || !byProvider(v.provider)) return null;
|
|
36
50
|
const out = typeof v.model === 'string' && v.model.trim() ? { provider: v.provider, model: v.model.trim() } : { provider: v.provider };
|
|
51
|
+
if (v.provider === 'anthropic' && v.via === 'subscription') out.via = 'subscription';
|
|
37
52
|
// B3/B5: the Ollama / self-hosted host, for the status line before the first turn (turn.started.endpoint wins after).
|
|
38
53
|
if ((v.provider === 'ollama' || byProvider(v.provider).selfHosted) && typeof v.host === 'string' && v.host.trim()) out.host = v.host.trim().slice(0, 120);
|
|
39
54
|
// D3: prompt profile (compact | full); absent = the server default (compact on ollama / local presets, full elsewhere).
|
|
@@ -53,31 +68,46 @@ export function parseModelArg(arg) {
|
|
|
53
68
|
if (id === 'auto') return { ok: true, choice: null };
|
|
54
69
|
const c = CHOICES.find(x => x.id === id);
|
|
55
70
|
if (!c) return { ok: false, error: `No provider called "${rawName}". Use ${CHOICES.map(x => x.id).join(', ')} or auto (/model lists them).` };
|
|
56
|
-
const
|
|
71
|
+
const resolved = model;
|
|
72
|
+
const choice = resolved ? { provider: c.provider, model: resolved } : { provider: c.provider };
|
|
73
|
+
if (c.via) choice.via = c.via;
|
|
57
74
|
if (profile) choice.profile = profile;
|
|
58
75
|
return { ok: true, choice };
|
|
59
76
|
}
|
|
60
77
|
|
|
61
|
-
/** ->
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
78
|
+
/** -> { state, row?, reason? } for one choice, from the shared provider status (providers.js). */
|
|
79
|
+
const availability = (res, c, caps) => providerState({ cap: c.cap || c.id, vault: c.vault }, caps, res);
|
|
80
|
+
|
|
81
|
+
/** QUOXON-FW6: a typed provider name or alias -> the CHOICES id, or null. */
|
|
82
|
+
export const providerId = (name) => {
|
|
83
|
+
const n = String(name || '').trim().toLowerCase();
|
|
84
|
+
const id = ALIASES[n] || n;
|
|
85
|
+
return CHOICES.some(c => c.id === id) ? id : null;
|
|
86
|
+
};
|
|
87
|
+
/** The CHOICES id a stored choice is (the subscription row shares provider anthropic). */
|
|
88
|
+
export const choiceId = (ch) => (ch ? rowOf(ch)?.id || null : null);
|
|
89
|
+
/** QUOXON-FW6: the CHOICES rows usable on this server now. Never lists a provider not connected. */
|
|
90
|
+
export const connectedChoices = (res, caps = null) =>
|
|
91
|
+
CHOICES.filter(c => c.served !== false && availability(res, c, caps).state === 'connected');
|
|
69
92
|
|
|
70
|
-
const same = (a, b) => (a?.provider || null) === (b?.provider || null);
|
|
93
|
+
const same = (a, b) => (a?.provider || null) === (b?.provider || null) && (a?.via || null) === (b?.via || null);
|
|
71
94
|
|
|
72
|
-
/** -> [[text, tone?]] lines for /model. */
|
|
73
|
-
export function modelLines(res, current) {
|
|
95
|
+
/** -> [[text, tone?]] lines for /model. `caps` = api.capabilities() (the server's word on each provider). */
|
|
96
|
+
export function modelLines(res, current, caps = null) {
|
|
74
97
|
const out = [['Model for your next turns (the server decides; turn.started shows what ran):']];
|
|
75
98
|
const mark = (on) => (on ? '▸' : ' ');
|
|
76
99
|
out.push([`${mark(!current)} auto server policy picks the provider and model · /model auto`]);
|
|
77
100
|
for (const c of CHOICES) {
|
|
78
|
-
const a = availability(res, c);
|
|
101
|
+
const a = availability(res, c, caps);
|
|
79
102
|
const picked = same(current, c);
|
|
80
103
|
const sel = picked && current.model ? ` · using ${current.model}` : '';
|
|
104
|
+
if (c.via === 'subscription') {
|
|
105
|
+
const api = availability(res, byProvider('anthropic'), caps).state === 'connected';
|
|
106
|
+
if (a.state === 'connected') out.push([`${mark(picked)} ● ${c.name} ready on this server${sel} · /model claude-cli${api ? ' (the Claude API key is used first while it is connected)' : ''}`, 'ok']);
|
|
107
|
+
else if (a.state === 'unknown') out.push([`${mark(picked)} ? ${c.name} unknown (the server did not say)`, 'warn']);
|
|
108
|
+
else out.push([`${mark(picked)} ○ ${c.name} ${a.reason || 'not signed in on this server'} · sign in from the dashboard Terminal`]);
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
81
111
|
if (c.served === false) {
|
|
82
112
|
out.push([`${mark(picked)} ${c.name} ${a.state === 'connected' ? 'connected, but ' : ''}not served by the chat stream yet`]);
|
|
83
113
|
continue;
|
|
@@ -85,17 +115,17 @@ export function modelLines(res, current) {
|
|
|
85
115
|
if (c.dynamic) {
|
|
86
116
|
if (a.state === 'connected') out.push([`${mark(picked)} ● ${c.name} connected · ${a.row.label || a.row.id}${sel} · /model ${c.id} lists its ${c.compat ? '' : 'installed '}models`, 'ok']);
|
|
87
117
|
else if (a.state === 'stored') out.push([`${mark(picked)} ◐ ${c.name} stored but not the default · quox vault set-default ${a.row.id}`, 'warn']);
|
|
88
|
-
else if (a.state === 'missing') out.push([`${mark(picked)} ○ ${c.name} not connected · /connect ${c.connect}${c.hint ? ` ${c.hint}` : ''}`]);
|
|
118
|
+
else if (a.state === 'missing' || a.state === 'unavailable') out.push([`${mark(picked)} ○ ${c.name} not connected · /connect ${c.connect}${c.hint ? ` ${c.hint}` : ''}`]);
|
|
89
119
|
else out.push([`${mark(picked)} ? ${c.name} unknown (vault not readable)${sel}`, 'warn']);
|
|
90
120
|
continue;
|
|
91
121
|
}
|
|
92
122
|
const models = c.models.join(', ');
|
|
93
|
-
if (a.state === 'connected') out.push([`${mark(picked)} ● ${c.name} connected${sel} · /model ${c.id} [${models}]`, 'ok']);
|
|
123
|
+
if (a.state === 'connected') out.push([`${mark(picked)} ● ${c.name} connected${sel} · /model ${c.id} [${models || 'live list'}]`, 'ok']);
|
|
124
|
+
else if (a.state === 'unavailable') out.push([`${mark(picked)} ○ ${c.name} ${a.reason || 'not available on this server'}`]);
|
|
94
125
|
else if (a.state === 'stored') out.push([`${mark(picked)} ◐ ${c.name} stored but not the default · quox vault set-default ${a.row.id}`, 'warn']);
|
|
95
126
|
else if (a.state === 'missing') out.push([`${mark(picked)} ○ ${c.name} not connected · /connect ${c.connect}`]);
|
|
96
127
|
else out.push([`${mark(picked)} ? ${c.name} unknown (vault not readable)${sel}`, 'warn']);
|
|
97
128
|
}
|
|
98
|
-
out.push([' ? Claude (subscription) dev-only · reached via auto or anthropic when no Claude API key is stored; no status route']);
|
|
99
129
|
if (!res?.ok) out.push([` could not read the vault: ${res?.error || 'no answer'}`, 'warn']);
|
|
100
130
|
return out;
|
|
101
131
|
}
|
|
@@ -104,22 +134,76 @@ export function modelLines(res, current) {
|
|
|
104
134
|
* The /model <arg> decision. -> { ok, choice?, text, tone? }. A provider the vault says is not usable is
|
|
105
135
|
* refused (with the /connect offer), never quietly swapped for another.
|
|
106
136
|
*/
|
|
107
|
-
export function selectModel(res, arg) {
|
|
137
|
+
export function selectModel(res, arg, caps = null) {
|
|
108
138
|
const p = parseModelArg(arg);
|
|
109
139
|
if (!p.ok) return { ok: false, text: p.error, tone: 'warn' };
|
|
110
140
|
if (!p.choice) return { ok: true, choice: null, text: 'Model: auto. The server policy picks the provider and model.', tone: 'ok' };
|
|
111
|
-
const c =
|
|
141
|
+
const c = rowOf(p.choice);
|
|
112
142
|
if (c.served === false) return { ok: false, text: `${c.name} is not served by the chat stream yet, so I can't use it here. /model lists what can.`, tone: 'warn' };
|
|
113
|
-
const a = availability(res, c);
|
|
143
|
+
const a = availability(res, c, caps);
|
|
144
|
+
const label0 = p.choice.model ? `${c.id} ${p.choice.model}` : c.id;
|
|
145
|
+
if (c.via === 'subscription') {
|
|
146
|
+
if (a.state === 'unknown') return { ok: true, choice: p.choice, text: `Model: ${label0}. Could not confirm the ${c.name} is signed in; the server will say if it is not.`, tone: 'warn' };
|
|
147
|
+
if (a.state !== 'connected') return { ok: false, text: `${c.name} is not ready on this server (${a.reason || 'not signed in'}). Sign in from the dashboard Terminal.`, tone: 'warn' };
|
|
148
|
+
const api = availability(res, byProvider('anthropic'), caps).state === 'connected';
|
|
149
|
+
return { ok: true, choice: p.choice, text: `Model: ${c.name}${p.choice.model ? ` · ${p.choice.model}` : ''}. ${api ? 'The Claude API key is connected, so the server uses it first.' : 'Your next turns ask for it.'}`, tone: api ? 'warn' : 'ok' };
|
|
150
|
+
}
|
|
151
|
+
if (c.id === 'anthropic' && a.state === 'missing' && availability(res, rowOf({ via: 'subscription' }), caps).state === 'connected') {
|
|
152
|
+
// what --provider anthropic does: no API key, so the server runs Claude on its signed-in subscription
|
|
153
|
+
return { ok: true, choice: { ...p.choice, via: 'subscription' }, text: `Model: ${label0}. The Claude API isn't connected, so your next turns run on the ${SUBSCRIPTION_NAME}.`, tone: 'warn' };
|
|
154
|
+
}
|
|
114
155
|
if (c.dynamic && a.state === 'missing') return { ok: false, text: `${c.name} isn't connected, so I can't use it yet. Connect it: /connect ${c.connect}${c.hint ? ` ${c.hint}` : ''}`, tone: 'warn' };
|
|
115
156
|
if (a.state === 'missing') return { ok: false, text: offerText(CONNECT[c.connect]), tone: 'warn' };
|
|
157
|
+
if (a.state === 'unavailable') return { ok: false, text: `${c.name} isn't available on this server${a.reason ? `: ${a.reason}` : ''}.`, tone: 'warn' };
|
|
116
158
|
if (a.state === 'stored') return { ok: false, text: `${c.name} has a key stored, but not as the org default, so the server will not use it. Fix: quox vault set-default ${a.row.id}`, tone: 'warn' };
|
|
117
159
|
const label = p.choice.model ? `${c.id} ${p.choice.model}` : c.id;
|
|
118
|
-
const extra = p.choice.model && !c.dynamic && !c.models.includes(p.choice.model) ? ` ${p.choice.model} is not in this client's list; the server decides whether it exists.` : '';
|
|
119
|
-
if (a.state === 'unknown') return { ok: true, choice: p.choice, text: `Model: ${label}. Could not confirm ${c.name} is connected (
|
|
160
|
+
const extra = p.choice.model && !c.dynamic && !c.discover && !c.models.includes(p.choice.model) ? ` ${p.choice.model} is not in this client's list; the server decides whether it exists.` : '';
|
|
161
|
+
if (a.state === 'unknown') return { ok: true, choice: p.choice, text: `Model: ${label}. Could not confirm ${c.name} is connected (the server did not say); the server will say if it is not.${extra}`, tone: 'warn' };
|
|
120
162
|
return { ok: true, choice: p.choice, text: `Model: ${label}. Your next turns ask for it.${extra}`, tone: extra ? 'warn' : 'ok' };
|
|
121
163
|
}
|
|
122
164
|
|
|
165
|
+
/** Newest-first discovered rows -> ids. */
|
|
166
|
+
const ids = (res) => (res?.ok && Array.isArray(res.models) ? res.models.map(modelId).filter(Boolean) : []);
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* /model openai|anthropic [model] against the live list (res from api.providerModels(provider)).
|
|
170
|
+
* Match order: exact id, "latest" (newest listed), a family (opus, sonnet, mini, ...) or any prefix/substring ->
|
|
171
|
+
* the NEWEST match. No match -> a clear error with the list. Discovery failed -> any typed id is allowed with a
|
|
172
|
+
* plain note (the provider validates it on first use). -> { ok, choice?, text, tone, models?, current? }
|
|
173
|
+
*/
|
|
174
|
+
export function selectDiscoveredModel(res, choice, row) {
|
|
175
|
+
const name = row.name;
|
|
176
|
+
const model = choice.model || null;
|
|
177
|
+
const list = ids(res);
|
|
178
|
+
const rows = res?.ok ? res.models : [];
|
|
179
|
+
if (!res?.ok || !list.length) {
|
|
180
|
+
const why = res?.message || res?.error || 'no answer';
|
|
181
|
+
if (!model) return { ok: true, choice, text: `Model: ${row.id}. Could not list ${name}'s models (${why}); the server uses its default.`, tone: 'warn' };
|
|
182
|
+
return { ok: true, choice, text: `Model: ${row.id} ${model}. Could not list ${name}'s models (${why}); ${model} is sent as typed and the provider will say if it does not exist.`, tone: 'warn' };
|
|
183
|
+
}
|
|
184
|
+
const newest = res.default_model || list[0];
|
|
185
|
+
if (!model) return { ok: true, choice, text: `Model: ${name} · ${newest} (the newest listed). Models below; pick one with /model ${row.id} <model|family|latest>.`, tone: 'ok', models: rows, current: newest };
|
|
186
|
+
const q = model.toLowerCase();
|
|
187
|
+
const hit = q === 'latest' ? list[0]
|
|
188
|
+
: list.find(i => i.toLowerCase() === q)
|
|
189
|
+
|| rows.find(r => r.family && r.family === q)?.id
|
|
190
|
+
|| list.find(i => i.toLowerCase().startsWith(q))
|
|
191
|
+
|| list.find(i => i.toLowerCase().includes(q));
|
|
192
|
+
if (!hit) return { ok: false, text: `${model} is not in ${name}'s live list. Pick an id, a family (${[...new Set(rows.map(r => r.family).filter(Boolean))].slice(0, 6).join(', ') || 'see below'}) or latest.`, tone: 'warn', models: rows };
|
|
193
|
+
return { ok: true, choice: { ...choice, model: hit }, text: `Model: ${name} · ${hit}${hit !== model ? ` (from "${model}")` : ''}. Your next turns ask for it.`, tone: 'ok' };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Subscription path: a Claude family alias (the CLI resolves the newest) or a full claude-* id; anything else is an error with the aliases. */
|
|
197
|
+
export function selectSubscriptionModel(choice) {
|
|
198
|
+
const m = choice.model;
|
|
199
|
+
if (!m) return { ok: true, choice, text: 'Model: claude-cli. The server default applies (the newest Sonnet unless your org set CLAUDE_CLI_DEFAULT_MODEL).', tone: 'ok' };
|
|
200
|
+
const q = m.toLowerCase();
|
|
201
|
+
if (CLAUDE_FAMILIES.includes(q)) return { ok: true, choice: { ...choice, model: q }, text: `Model: claude-cli ${q} (the Claude CLI resolves the newest ${q}; turn.started shows what ran).`, tone: 'ok' };
|
|
202
|
+
if (q === 'latest') return { ok: true, choice: { provider: choice.provider, via: choice.via }, text: 'Model: claude-cli, the server default.', tone: 'ok' };
|
|
203
|
+
if (/^claude-[a-z0-9.\-]+$/i.test(m)) return { ok: true, choice, text: `Model: claude-cli ${m}. The CLI will say if it does not exist.`, tone: 'ok' };
|
|
204
|
+
return { ok: false, text: `Unknown Claude model "${m}". Use ${CLAUDE_FAMILIES.join(', ')} (the CLI picks the newest) or a full claude-* id.`, tone: 'warn' };
|
|
205
|
+
}
|
|
206
|
+
|
|
123
207
|
/**
|
|
124
208
|
* B3: /model ollama [model] against the server's installed-model list (res from api.ollamaModels()).
|
|
125
209
|
* -> { ok, choice?, text, tone, rows? } ; rows = the installed list to show.
|
|
@@ -179,13 +263,35 @@ export function selectCompatModel(res, presetId, model) {
|
|
|
179
263
|
return { ok: true, choice: withHost({ provider: c.provider, model }), text: `Model: ${c.name}${host ? ` · ${host}` : ''} · ${model}. Your next turns ask for it.`, tone: 'ok' };
|
|
180
264
|
}
|
|
181
265
|
|
|
266
|
+
/**
|
|
267
|
+
* Z2H-3B: after a successful /connect, should the new provider become the active choice? Yes when the current
|
|
268
|
+
* selection is not a working connected provider (nothing connected, or a saved/phantom choice that is not connected),
|
|
269
|
+
* or when auto has no OTHER connected provider to pick. No provider is a silent default: with another working one
|
|
270
|
+
* active, nothing changes and the caller keeps its "Use it: /model <id>" hint.
|
|
271
|
+
* -> { adopt:true, choice, line } | { adopt:false }. `vault`/`caps` are re-read AFTER the connect.
|
|
272
|
+
*/
|
|
273
|
+
export function adoptAfterConnect({ vault, caps, current, id, host = null, model = null }) {
|
|
274
|
+
const row = CHOICES.find(c => c.id === id);
|
|
275
|
+
if (!row) return { adopt: false };
|
|
276
|
+
const connected = (c) => availability(vault, c, caps).state === 'connected';
|
|
277
|
+
const cur = current ? rowOf(current) : null;
|
|
278
|
+
const curWorks = cur && cur.id !== row.id && connected(cur);
|
|
279
|
+
const othersConnected = CHOICES.some(c => c.id !== row.id && connected(c));
|
|
280
|
+
if (curWorks) return { adopt: false };
|
|
281
|
+
if (!current && othersConnected) return { adopt: false };
|
|
282
|
+
const choice = normChoice({ provider: row.provider, ...(model ? { model } : {}), ...(host ? { host } : {}) });
|
|
283
|
+
return { adopt: true, choice, line: `Using ${row.name}${model ? ` ${model}` : ''} now. /model to change.` };
|
|
284
|
+
}
|
|
285
|
+
|
|
182
286
|
/** Status-line text: what the server said it ran, else what is selected; null for auto before any turn (keeps the line short). */
|
|
183
287
|
export function modelLabel(ctx, choice) {
|
|
288
|
+
if (ctx?.model && ctx.provider === 'anthropic' && ctx.auth === 'subscription') return `claude-cli ${ctx.model}`;
|
|
184
289
|
if (ctx?.model && ctx.provider === 'ollama') return `Ollama · ${ctx.endpoint || choice?.host || '?'} · ${ctx.model}`;
|
|
185
290
|
const cc = ctx?.provider ? byProvider(ctx.provider) : null;
|
|
186
291
|
if (ctx?.model && cc?.compat) return cc.hosted ? `${cc.name} · ${ctx.model}` : `${cc.name} · ${ctx.endpoint || choice?.host || '?'} · ${ctx.model}`;
|
|
187
292
|
if (ctx?.model) return ctx.provider ? `${ctx.provider} ${ctx.model}` : ctx.model;
|
|
188
293
|
if (!choice) return null;
|
|
294
|
+
if (choice.via === 'subscription') return choice.model ? `claude-cli ${choice.model}` : 'claude-cli';
|
|
189
295
|
if (choice.provider === 'ollama') return `Ollama · ${choice.host || '?'} · ${choice.model || 'default model'}`;
|
|
190
296
|
const ch = byProvider(choice.provider);
|
|
191
297
|
if (ch?.compat) return ch.hosted ? `${ch.name} · ${choice.model || 'pick a model'}` : `${ch.name} · ${choice.host || '?'} · ${choice.model || 'default model'}`;
|