mod8-cli 0.2.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 +87 -0
- package/LICENSE +21 -0
- package/README.md +239 -0
- package/bin/mod8.js +2 -0
- package/dist/cli.js +302 -0
- package/dist/commands/addProvider.js +105 -0
- package/dist/commands/all.js +158 -0
- package/dist/commands/chat.js +855 -0
- package/dist/commands/config.js +29 -0
- package/dist/commands/devAuthStatus.js +34 -0
- package/dist/commands/devHostAsk.js +51 -0
- package/dist/commands/devHostSystem.js +15 -0
- package/dist/commands/devResolve.js +54 -0
- package/dist/commands/devSimulate.js +235 -0
- package/dist/commands/devWorkAsk.js +55 -0
- package/dist/commands/intentRouting.js +280 -0
- package/dist/commands/keys.js +55 -0
- package/dist/commands/list.js +27 -0
- package/dist/commands/login.js +147 -0
- package/dist/commands/logout.js +17 -0
- package/dist/commands/prompt.js +63 -0
- package/dist/commands/providers.js +30 -0
- package/dist/commands/verify.js +5 -0
- package/dist/input/compose.js +37 -0
- package/dist/input/files.js +49 -0
- package/dist/input/stdin.js +14 -0
- package/dist/providers/anthropic.js +115 -0
- package/dist/providers/displayName.js +25 -0
- package/dist/providers/errorHints.js +175 -0
- package/dist/providers/generic.js +331 -0
- package/dist/providers/genericChat.js +265 -0
- package/dist/providers/google.js +63 -0
- package/dist/providers/hostSystem.js +173 -0
- package/dist/providers/index.js +38 -0
- package/dist/providers/mock.js +87 -0
- package/dist/providers/modelResolution.js +42 -0
- package/dist/providers/openai.js +75 -0
- package/dist/providers/pricing.js +47 -0
- package/dist/providers/proxy.js +148 -0
- package/dist/providers/registry.js +196 -0
- package/dist/providers/types.js +1 -0
- package/dist/providers/workSystem.js +33 -0
- package/dist/storage/auth.js +65 -0
- package/dist/storage/config.js +35 -0
- package/dist/storage/keys.js +59 -0
- package/dist/storage/providers.js +337 -0
- package/dist/storage/sessions.js +150 -0
- package/dist/types.js +9 -0
- package/dist/util/debug.js +79 -0
- package/dist/util/errors.js +157 -0
- package/dist/util/prompt.js +111 -0
- package/dist/util/secrets.js +110 -0
- package/dist/util/text.js +53 -0
- package/dist/util/time.js +25 -0
- package/dist/verify/runner.js +437 -0
- package/package.json +69 -0
- package/specs/all-mode.yaml +44 -0
- package/specs/behavior/auto-fallback.yaml +49 -0
- package/specs/behavior/bare-name-routing.yaml +223 -0
- package/specs/behavior/bare-paste-confirm.yaml +125 -0
- package/specs/behavior/env-var-respected.yaml +108 -0
- package/specs/behavior/error-fidelity.yaml +92 -0
- package/specs/behavior/error-hints.yaml +160 -0
- package/specs/behavior/fresh-vs-resume.yaml +94 -0
- package/specs/behavior/fuzzy-match.yaml +208 -0
- package/specs/behavior/host-self-knowledge-fresh.yaml +66 -0
- package/specs/behavior/intent-no-mismatch.yaml +115 -0
- package/specs/behavior/login-logout.yaml +97 -0
- package/specs/behavior/no-model-allowlist.yaml +80 -0
- package/specs/behavior/paste-key.yaml +342 -0
- package/specs/behavior/provider-switching.yaml +186 -0
- package/specs/behavior/providers-json-respected.yaml +106 -0
- package/specs/behavior/self-knowledge.yaml +119 -0
- package/specs/behavior/stress-session.yaml +226 -0
- package/specs/behavior/switch-back-when-failing.yaml +90 -0
- package/specs/behavior/work-character.yaml +109 -0
- package/specs/chat-meta.yaml +349 -0
- package/specs/chat-startup.yaml +148 -0
- package/specs/chat.yaml +91 -0
- package/specs/config.yaml +42 -0
- package/specs/install.yaml +112 -0
- package/specs/keys.yaml +81 -0
- package/specs/one-shot.yaml +65 -0
- package/specs/pipe-and-files.yaml +40 -0
- package/specs/providers.yaml +172 -0
- package/specs/sessions.yaml +115 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { promises as fs } from 'fs';
|
|
2
|
+
import { resolve, isAbsolute } from 'path';
|
|
3
|
+
import { homedir } from 'os';
|
|
4
|
+
const MAX_FILE_BYTES = 100_000;
|
|
5
|
+
/**
|
|
6
|
+
* Find @<path> references in a prompt and load the matching files.
|
|
7
|
+
* - Strips trailing punctuation (.,;:!?)
|
|
8
|
+
* - Expands ~ and resolves relative paths against cwd
|
|
9
|
+
* - Refuses files larger than 100KB
|
|
10
|
+
* - Returns one entry per reference (with content or error message)
|
|
11
|
+
*/
|
|
12
|
+
export async function resolveFileRefs(prompt) {
|
|
13
|
+
const matches = [...prompt.matchAll(/@([^\s]+)/g)];
|
|
14
|
+
const seen = new Set();
|
|
15
|
+
const results = [];
|
|
16
|
+
for (const m of matches) {
|
|
17
|
+
let pathSpec = m[1].replace(/[.,;:!?]+$/, '');
|
|
18
|
+
if (!pathSpec || seen.has(pathSpec))
|
|
19
|
+
continue;
|
|
20
|
+
seen.add(pathSpec);
|
|
21
|
+
let resolved = pathSpec;
|
|
22
|
+
if (resolved.startsWith('~/')) {
|
|
23
|
+
resolved = homedir() + resolved.slice(1);
|
|
24
|
+
}
|
|
25
|
+
else if (!isAbsolute(resolved)) {
|
|
26
|
+
resolved = resolve(process.cwd(), resolved);
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
const stat = await fs.stat(resolved);
|
|
30
|
+
if (!stat.isFile()) {
|
|
31
|
+
results.push({ path: pathSpec, error: 'not a regular file' });
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (stat.size > MAX_FILE_BYTES) {
|
|
35
|
+
results.push({
|
|
36
|
+
path: pathSpec,
|
|
37
|
+
error: `too large (${stat.size} bytes; limit ${MAX_FILE_BYTES})`,
|
|
38
|
+
});
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
const content = await fs.readFile(resolved, 'utf8');
|
|
42
|
+
results.push({ path: pathSpec, content });
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
results.push({ path: pathSpec, error: 'not found' });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return results;
|
|
49
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read all stdin into a string. Returns undefined when stdin is a TTY (no piped input).
|
|
3
|
+
*/
|
|
4
|
+
export async function readStdin() {
|
|
5
|
+
if (process.stdin.isTTY)
|
|
6
|
+
return undefined;
|
|
7
|
+
const chunks = [];
|
|
8
|
+
for await (const chunk of process.stdin) {
|
|
9
|
+
chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk);
|
|
10
|
+
}
|
|
11
|
+
if (chunks.length === 0)
|
|
12
|
+
return undefined;
|
|
13
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
14
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import Anthropic from '@anthropic-ai/sdk';
|
|
2
|
+
import { getKey } from '../storage/keys.js';
|
|
3
|
+
import { priceFor } from './pricing.js';
|
|
4
|
+
// Per decision 6: use latest stable. Current Sonnet is 4.6 (was 4.5 in original spec).
|
|
5
|
+
const DEFAULT_MODEL = 'claude-sonnet-4-6';
|
|
6
|
+
async function buildClient() {
|
|
7
|
+
// Env var wins (standard CLI convention). Fall back to keys.json.
|
|
8
|
+
const apiKey = process.env.ANTHROPIC_API_KEY ?? (await getKey('anthropic'));
|
|
9
|
+
if (!apiKey) {
|
|
10
|
+
throw new Error('No Anthropic key configured. Run: mod8 keys set anthropic, or set ANTHROPIC_API_KEY.');
|
|
11
|
+
}
|
|
12
|
+
return { client: new Anthropic({ apiKey }), model: DEFAULT_MODEL };
|
|
13
|
+
}
|
|
14
|
+
export const anthropicProvider = {
|
|
15
|
+
id: 'anthropic',
|
|
16
|
+
defaultModel: DEFAULT_MODEL,
|
|
17
|
+
async call(prompt, opts = {}) {
|
|
18
|
+
const { client } = await buildClient();
|
|
19
|
+
const model = opts.model ?? process.env.MOD8_ANTHROPIC_MODEL ?? DEFAULT_MODEL;
|
|
20
|
+
const start = Date.now();
|
|
21
|
+
const res = await client.messages.create({
|
|
22
|
+
model,
|
|
23
|
+
max_tokens: opts.maxTokens ?? 1024,
|
|
24
|
+
messages: [{ role: 'user', content: prompt }],
|
|
25
|
+
});
|
|
26
|
+
const latencyMs = Date.now() - start;
|
|
27
|
+
const text = res.content
|
|
28
|
+
.filter((block) => block.type === 'text')
|
|
29
|
+
.map((block) => block.text)
|
|
30
|
+
.join('');
|
|
31
|
+
const inputTokens = res.usage?.input_tokens ?? 0;
|
|
32
|
+
const outputTokens = res.usage?.output_tokens ?? 0;
|
|
33
|
+
const actualModel = res.model ?? model;
|
|
34
|
+
return {
|
|
35
|
+
text,
|
|
36
|
+
inputTokens,
|
|
37
|
+
outputTokens,
|
|
38
|
+
costUsd: priceFor(actualModel, inputTokens, outputTokens),
|
|
39
|
+
latencyMs,
|
|
40
|
+
model: actualModel,
|
|
41
|
+
};
|
|
42
|
+
},
|
|
43
|
+
async *stream(prompt, opts = {}) {
|
|
44
|
+
const { client } = await buildClient();
|
|
45
|
+
const model = opts.model ?? process.env.MOD8_ANTHROPIC_MODEL ?? DEFAULT_MODEL;
|
|
46
|
+
const start = Date.now();
|
|
47
|
+
const ms = client.messages.stream({
|
|
48
|
+
model,
|
|
49
|
+
max_tokens: opts.maxTokens ?? 1024,
|
|
50
|
+
messages: [{ role: 'user', content: prompt }],
|
|
51
|
+
});
|
|
52
|
+
for await (const event of ms) {
|
|
53
|
+
if (event.type === 'content_block_delta' &&
|
|
54
|
+
event.delta.type === 'text_delta') {
|
|
55
|
+
yield { type: 'text', delta: event.delta.text };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const final = await ms.finalMessage();
|
|
59
|
+
const latencyMs = Date.now() - start;
|
|
60
|
+
const inputTokens = final.usage.input_tokens;
|
|
61
|
+
const outputTokens = final.usage.output_tokens;
|
|
62
|
+
const actualModel = final.model ?? model;
|
|
63
|
+
yield {
|
|
64
|
+
type: 'done',
|
|
65
|
+
usage: {
|
|
66
|
+
inputTokens,
|
|
67
|
+
outputTokens,
|
|
68
|
+
latencyMs,
|
|
69
|
+
model: actualModel,
|
|
70
|
+
costUsd: priceFor(actualModel, inputTokens, outputTokens),
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
/**
|
|
76
|
+
* Multi-turn streaming chat against Anthropic with a system prompt and full
|
|
77
|
+
* message history. Used by the chat REPL (host + work modes).
|
|
78
|
+
*
|
|
79
|
+
* Throws on abort (signal aborted) so the caller can detect cancellation.
|
|
80
|
+
*/
|
|
81
|
+
export async function* streamAnthropicChat(opts) {
|
|
82
|
+
const apiKey = process.env.ANTHROPIC_API_KEY ?? (await getKey('anthropic'));
|
|
83
|
+
if (!apiKey) {
|
|
84
|
+
throw new Error('No Anthropic key configured. Run: mod8 keys set anthropic, or set ANTHROPIC_API_KEY.');
|
|
85
|
+
}
|
|
86
|
+
const client = new Anthropic({ apiKey });
|
|
87
|
+
const start = Date.now();
|
|
88
|
+
const ms = client.messages.stream({
|
|
89
|
+
model: opts.model,
|
|
90
|
+
max_tokens: opts.maxTokens ?? 4096,
|
|
91
|
+
system: opts.system,
|
|
92
|
+
messages: opts.messages,
|
|
93
|
+
}, { signal: opts.signal });
|
|
94
|
+
for await (const event of ms) {
|
|
95
|
+
if (event.type === 'content_block_delta' &&
|
|
96
|
+
event.delta.type === 'text_delta') {
|
|
97
|
+
yield { type: 'text', delta: event.delta.text };
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const final = await ms.finalMessage();
|
|
101
|
+
const latencyMs = Date.now() - start;
|
|
102
|
+
const inputTokens = final.usage.input_tokens;
|
|
103
|
+
const outputTokens = final.usage.output_tokens;
|
|
104
|
+
const actualModel = final.model ?? opts.model;
|
|
105
|
+
yield {
|
|
106
|
+
type: 'done',
|
|
107
|
+
usage: {
|
|
108
|
+
inputTokens,
|
|
109
|
+
outputTokens,
|
|
110
|
+
latencyMs,
|
|
111
|
+
model: actualModel,
|
|
112
|
+
costUsd: priceFor(actualModel, inputTokens, outputTokens),
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve the short display name shown in chat banners and the work-mode
|
|
3
|
+
* system prompt's "you are X" line. Single source of truth so that the
|
|
4
|
+
* host system prompt, the chat speaker block, and the work system prompt
|
|
5
|
+
* all agree on what to call a provider.
|
|
6
|
+
*
|
|
7
|
+
* Rules:
|
|
8
|
+
* - Anthropic always renders as "claude" — that's the canonical brand.
|
|
9
|
+
* - For everything else: prefer the user's configured display name when
|
|
10
|
+
* it's short and clean (no parens), otherwise fall back to the id.
|
|
11
|
+
*
|
|
12
|
+
* Examples:
|
|
13
|
+
* anthropic + "Anthropic (Claude)" → "claude"
|
|
14
|
+
* openai + "OpenAI (GPT)" → "openai" (parens → id)
|
|
15
|
+
* openai + "codex" → "codex" (custom name kept)
|
|
16
|
+
* deepseek + "DeepSeek" → "DeepSeek" (no parens, kept)
|
|
17
|
+
*/
|
|
18
|
+
export function workerNameFor(id, configuredName) {
|
|
19
|
+
if (id === 'anthropic')
|
|
20
|
+
return 'claude';
|
|
21
|
+
if (configuredName && !/[()]/.test(configuredName) && configuredName.length <= 32) {
|
|
22
|
+
return configuredName;
|
|
23
|
+
}
|
|
24
|
+
return id;
|
|
25
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-error-kind diagnostic explainer.
|
|
3
|
+
*
|
|
4
|
+
* Old design: one generic provider hint regardless of error. Result was
|
|
5
|
+
* misleading guidance — a 403 "project denied" for Google was followed by
|
|
6
|
+
* the generic "free-tier quota / billing / regions" tip, which doesn't
|
|
7
|
+
* match what's actually wrong.
|
|
8
|
+
*
|
|
9
|
+
* New design: combine the structured ErrorKind from `diagnose()` with the
|
|
10
|
+
* provider's id to produce three pieces:
|
|
11
|
+
*
|
|
12
|
+
* - short — single-line summary used as the error transcript entry
|
|
13
|
+
* (always quotes the raw provider message + HTTP code)
|
|
14
|
+
* - long — explanation + provider-specific fix bullets
|
|
15
|
+
* - suggestion — short tip used by the warn / auto-fallback banners
|
|
16
|
+
*
|
|
17
|
+
* The raw provider message is ALWAYS preserved so the user can search for
|
|
18
|
+
* it if our diagnosis is wrong. We never replace a structured message
|
|
19
|
+
* with a generic blanket — generic only fires for kind='other' (truly
|
|
20
|
+
* unknown errors).
|
|
21
|
+
*/
|
|
22
|
+
import { diagnose } from '../util/errors.js';
|
|
23
|
+
const PROVIDER_URLS = {
|
|
24
|
+
anthropic: {
|
|
25
|
+
billing: 'https://console.anthropic.com/settings/billing',
|
|
26
|
+
keys: 'https://console.anthropic.com/settings/keys',
|
|
27
|
+
},
|
|
28
|
+
openai: {
|
|
29
|
+
billing: 'https://platform.openai.com/billing',
|
|
30
|
+
keys: 'https://platform.openai.com/api-keys',
|
|
31
|
+
},
|
|
32
|
+
google: {
|
|
33
|
+
billing: 'https://console.cloud.google.com/billing',
|
|
34
|
+
keys: 'https://aistudio.google.com/app/apikey',
|
|
35
|
+
},
|
|
36
|
+
groq: {
|
|
37
|
+
billing: 'https://console.groq.com/settings/billing',
|
|
38
|
+
keys: 'https://console.groq.com/keys',
|
|
39
|
+
},
|
|
40
|
+
xai: {
|
|
41
|
+
billing: 'https://console.x.ai/team/default',
|
|
42
|
+
keys: 'https://console.x.ai/team/default/api-keys',
|
|
43
|
+
},
|
|
44
|
+
deepseek: {
|
|
45
|
+
billing: 'https://platform.deepseek.com/usage',
|
|
46
|
+
keys: 'https://platform.deepseek.com/api_keys',
|
|
47
|
+
},
|
|
48
|
+
mistral: {
|
|
49
|
+
billing: 'https://console.mistral.ai/billing',
|
|
50
|
+
keys: 'https://console.mistral.ai/api-keys',
|
|
51
|
+
},
|
|
52
|
+
openrouter: {
|
|
53
|
+
billing: 'https://openrouter.ai/credits',
|
|
54
|
+
keys: 'https://openrouter.ai/keys',
|
|
55
|
+
},
|
|
56
|
+
together: {
|
|
57
|
+
billing: 'https://api.together.ai/settings/billing',
|
|
58
|
+
keys: 'https://api.together.ai/settings/api-keys',
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
function urlsFor(providerId) {
|
|
62
|
+
return PROVIDER_URLS[providerId] ?? { billing: '(provider dashboard)', keys: '(provider dashboard)' };
|
|
63
|
+
}
|
|
64
|
+
function quote(text) {
|
|
65
|
+
return text ? `'${text}'` : '';
|
|
66
|
+
}
|
|
67
|
+
function rejectionLine(providerId, diag, verb) {
|
|
68
|
+
const code = diag.code ? ` (HTTP ${diag.code})` : '';
|
|
69
|
+
const tail = diag.rawMessage ? `: ${quote(diag.rawMessage)}` : '';
|
|
70
|
+
return `${providerId} ${verb}${code}${tail}`;
|
|
71
|
+
}
|
|
72
|
+
export function explainError(err, providerId) {
|
|
73
|
+
const diag = diagnose(err);
|
|
74
|
+
const urls = urlsFor(providerId);
|
|
75
|
+
switch (diag.kind) {
|
|
76
|
+
case 'auth': {
|
|
77
|
+
const code = diag.code ?? 401;
|
|
78
|
+
const tail = diag.rawMessage ? `: ${quote(diag.rawMessage)}` : '';
|
|
79
|
+
return {
|
|
80
|
+
kind: diag.kind,
|
|
81
|
+
short: `${providerId} rejected the API key (HTTP ${code})${tail}`,
|
|
82
|
+
long: `API key rejected. Verify the key is correct, not revoked, and not for the wrong product (e.g. a ChatGPT subscription is NOT an OpenAI API key).\n` +
|
|
83
|
+
`Possible fixes:\n` +
|
|
84
|
+
`- Paste a new key right here ("add a key") and mod8 will save it inline\n` +
|
|
85
|
+
`- Or rotate at ${urls.keys}`,
|
|
86
|
+
suggestion: `Type 'mod8' to switch back, or paste a new key to replace this one.`,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
case 'forbidden': {
|
|
90
|
+
return {
|
|
91
|
+
kind: diag.kind,
|
|
92
|
+
short: rejectionLine(providerId, diag, 'rejected the request'),
|
|
93
|
+
long: `${providerId} blocked the request at the project / account level — common after rapid key creation, region restrictions, or unusual activity.\n` +
|
|
94
|
+
`Possible fixes:\n` +
|
|
95
|
+
`- Wait 24h for auto-review\n` +
|
|
96
|
+
`- Create a new project in your provider console and a new key in it (${urls.keys})\n` +
|
|
97
|
+
`- Contact ${providerId} support`,
|
|
98
|
+
suggestion: `This is a project-level block, not a key issue. Switch back to mod8 with 'mod8'.`,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
case 'rate-limit': {
|
|
102
|
+
const code = diag.code ?? 429;
|
|
103
|
+
const delay = diag.retryDelaySeconds;
|
|
104
|
+
const tail = diag.rawMessage ? `: ${quote(diag.rawMessage)}` : '';
|
|
105
|
+
const delayHint = delay ? ` (retry in ${delay}s)` : '';
|
|
106
|
+
return {
|
|
107
|
+
kind: diag.kind,
|
|
108
|
+
short: `${providerId} rate-limited the request (HTTP ${code})${delayHint}${tail}`,
|
|
109
|
+
long: delay
|
|
110
|
+
? `Rate limited. The provider asked us to wait ${delay} seconds before retrying. ` +
|
|
111
|
+
`If this persists, you may be on a low-tier limit — see ${urls.billing}.`
|
|
112
|
+
: `Rate limited. Wait a few seconds and retry. ` +
|
|
113
|
+
`If this persists, you may be on a free or low-tier plan with tight limits — see ${urls.billing}.`,
|
|
114
|
+
suggestion: delay
|
|
115
|
+
? `Type 'mod8' to switch back, or wait ${delay}s and retry.`
|
|
116
|
+
: `Type 'mod8' to switch back, or wait a few seconds and retry.`,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
case 'no-credit': {
|
|
120
|
+
return {
|
|
121
|
+
kind: diag.kind,
|
|
122
|
+
short: rejectionLine(providerId, diag, 'reports insufficient credit'),
|
|
123
|
+
long: `Out of credit / billing not active. Top up at ${urls.billing}.\n` +
|
|
124
|
+
`Note: free-tier quotas can take ~10 minutes to activate after a new key is created — if you JUST created the key, wait and retry.`,
|
|
125
|
+
suggestion: `Type 'mod8' to switch back, or top up at ${urls.billing}.`,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
case 'server': {
|
|
129
|
+
return {
|
|
130
|
+
kind: diag.kind,
|
|
131
|
+
short: rejectionLine(providerId, diag, 'returned a server error'),
|
|
132
|
+
long: `${providerId} is having issues right now. Try again in a few minutes; if it persists, check the provider's status page.`,
|
|
133
|
+
suggestion: `Type 'mod8' to switch back, or retry shortly.`,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
case 'network': {
|
|
137
|
+
const tail = diag.rawMessage ? `: ${quote(diag.rawMessage)}` : '';
|
|
138
|
+
return {
|
|
139
|
+
kind: diag.kind,
|
|
140
|
+
short: `couldn't reach ${providerId}${tail}`,
|
|
141
|
+
long: `Network error. Check your internet connection — DNS, VPN, or firewall.`,
|
|
142
|
+
suggestion: `Type 'mod8' to switch back. Check your connection and retry.`,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
case 'timeout': {
|
|
146
|
+
const tail = diag.rawMessage ? `: ${quote(diag.rawMessage)}` : '';
|
|
147
|
+
return {
|
|
148
|
+
kind: diag.kind,
|
|
149
|
+
short: `request to ${providerId} timed out${tail}`,
|
|
150
|
+
long: `The request took too long. The provider may be overloaded or your network is slow.`,
|
|
151
|
+
suggestion: `Type 'mod8' to switch back, or retry shortly.`,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
case 'model': {
|
|
155
|
+
const tail = diag.rawMessage ? `: ${quote(diag.rawMessage)}` : '';
|
|
156
|
+
return {
|
|
157
|
+
kind: diag.kind,
|
|
158
|
+
short: `${providerId} model not available${tail}`,
|
|
159
|
+
long: `The configured model isn't available for this account. Set \`MOD8_${providerId.toUpperCase()}_MODEL\` to override, or pick a different provider.`,
|
|
160
|
+
suggestion: `Type 'mod8' to switch back, or switch to a different model.`,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
default: {
|
|
164
|
+
// 'other' — generic fallback. We do NOT pretend to diagnose; just
|
|
165
|
+
// surface the raw message and give a minimal escape suggestion.
|
|
166
|
+
const tail = diag.rawMessage ? `: ${quote(diag.rawMessage)}` : '';
|
|
167
|
+
return {
|
|
168
|
+
kind: diag.kind,
|
|
169
|
+
short: `${providerId} failed${tail}`,
|
|
170
|
+
long: '',
|
|
171
|
+
suggestion: `Type 'mod8' to switch back.`,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|