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.
Files changed (86) hide show
  1. package/CHANGELOG.md +87 -0
  2. package/LICENSE +21 -0
  3. package/README.md +239 -0
  4. package/bin/mod8.js +2 -0
  5. package/dist/cli.js +302 -0
  6. package/dist/commands/addProvider.js +105 -0
  7. package/dist/commands/all.js +158 -0
  8. package/dist/commands/chat.js +855 -0
  9. package/dist/commands/config.js +29 -0
  10. package/dist/commands/devAuthStatus.js +34 -0
  11. package/dist/commands/devHostAsk.js +51 -0
  12. package/dist/commands/devHostSystem.js +15 -0
  13. package/dist/commands/devResolve.js +54 -0
  14. package/dist/commands/devSimulate.js +235 -0
  15. package/dist/commands/devWorkAsk.js +55 -0
  16. package/dist/commands/intentRouting.js +280 -0
  17. package/dist/commands/keys.js +55 -0
  18. package/dist/commands/list.js +27 -0
  19. package/dist/commands/login.js +147 -0
  20. package/dist/commands/logout.js +17 -0
  21. package/dist/commands/prompt.js +63 -0
  22. package/dist/commands/providers.js +30 -0
  23. package/dist/commands/verify.js +5 -0
  24. package/dist/input/compose.js +37 -0
  25. package/dist/input/files.js +49 -0
  26. package/dist/input/stdin.js +14 -0
  27. package/dist/providers/anthropic.js +115 -0
  28. package/dist/providers/displayName.js +25 -0
  29. package/dist/providers/errorHints.js +175 -0
  30. package/dist/providers/generic.js +331 -0
  31. package/dist/providers/genericChat.js +265 -0
  32. package/dist/providers/google.js +63 -0
  33. package/dist/providers/hostSystem.js +173 -0
  34. package/dist/providers/index.js +38 -0
  35. package/dist/providers/mock.js +87 -0
  36. package/dist/providers/modelResolution.js +42 -0
  37. package/dist/providers/openai.js +75 -0
  38. package/dist/providers/pricing.js +47 -0
  39. package/dist/providers/proxy.js +148 -0
  40. package/dist/providers/registry.js +196 -0
  41. package/dist/providers/types.js +1 -0
  42. package/dist/providers/workSystem.js +33 -0
  43. package/dist/storage/auth.js +65 -0
  44. package/dist/storage/config.js +35 -0
  45. package/dist/storage/keys.js +59 -0
  46. package/dist/storage/providers.js +337 -0
  47. package/dist/storage/sessions.js +150 -0
  48. package/dist/types.js +9 -0
  49. package/dist/util/debug.js +79 -0
  50. package/dist/util/errors.js +157 -0
  51. package/dist/util/prompt.js +111 -0
  52. package/dist/util/secrets.js +110 -0
  53. package/dist/util/text.js +53 -0
  54. package/dist/util/time.js +25 -0
  55. package/dist/verify/runner.js +437 -0
  56. package/package.json +69 -0
  57. package/specs/all-mode.yaml +44 -0
  58. package/specs/behavior/auto-fallback.yaml +49 -0
  59. package/specs/behavior/bare-name-routing.yaml +223 -0
  60. package/specs/behavior/bare-paste-confirm.yaml +125 -0
  61. package/specs/behavior/env-var-respected.yaml +108 -0
  62. package/specs/behavior/error-fidelity.yaml +92 -0
  63. package/specs/behavior/error-hints.yaml +160 -0
  64. package/specs/behavior/fresh-vs-resume.yaml +94 -0
  65. package/specs/behavior/fuzzy-match.yaml +208 -0
  66. package/specs/behavior/host-self-knowledge-fresh.yaml +66 -0
  67. package/specs/behavior/intent-no-mismatch.yaml +115 -0
  68. package/specs/behavior/login-logout.yaml +97 -0
  69. package/specs/behavior/no-model-allowlist.yaml +80 -0
  70. package/specs/behavior/paste-key.yaml +342 -0
  71. package/specs/behavior/provider-switching.yaml +186 -0
  72. package/specs/behavior/providers-json-respected.yaml +106 -0
  73. package/specs/behavior/self-knowledge.yaml +119 -0
  74. package/specs/behavior/stress-session.yaml +226 -0
  75. package/specs/behavior/switch-back-when-failing.yaml +90 -0
  76. package/specs/behavior/work-character.yaml +109 -0
  77. package/specs/chat-meta.yaml +349 -0
  78. package/specs/chat-startup.yaml +148 -0
  79. package/specs/chat.yaml +91 -0
  80. package/specs/config.yaml +42 -0
  81. package/specs/install.yaml +112 -0
  82. package/specs/keys.yaml +81 -0
  83. package/specs/one-shot.yaml +65 -0
  84. package/specs/pipe-and-files.yaml +40 -0
  85. package/specs/providers.yaml +172 -0
  86. package/specs/sessions.yaml +115 -0
@@ -0,0 +1,105 @@
1
+ /**
2
+ * `mod8 add-provider` — interactive flow:
3
+ * 1. Paste API key.
4
+ * 2. mod8 detects format from prefix → suggests built-in template if it knows.
5
+ * 3. User confirms / edits id, display name, base URL, default model.
6
+ * 4. Provider is saved to providers.json with an auto-assigned palette color.
7
+ *
8
+ * Mostly used for non-built-in providers, but also works as a faster path for
9
+ * registering a known one (no need to remember the exact id).
10
+ */
11
+ import chalk from 'chalk';
12
+ import { readSecret, readLine, maskKey } from '../util/prompt.js';
13
+ import { detectFromKey, isValidProviderId } from '../providers/registry.js';
14
+ import { setProvider, nextPaletteColor, PROVIDERS_FILE_PATH, } from '../storage/providers.js';
15
+ const API_TYPES = ['anthropic', 'openai-compat', 'gemini'];
16
+ function isApiType(s) {
17
+ return API_TYPES.includes(s);
18
+ }
19
+ export async function addProviderCommand() {
20
+ console.log();
21
+ console.log(chalk.bold('mod8 add-provider'));
22
+ console.log(chalk.dim(' Paste an API key. mod8 will detect the format and ask anything it can\'t guess.'));
23
+ console.log();
24
+ const key = (await readSecret('paste key: ')).trim();
25
+ if (!key) {
26
+ console.error(chalk.red('No key entered. Aborted.'));
27
+ process.exit(1);
28
+ }
29
+ const tpl = detectFromKey(key);
30
+ if (tpl) {
31
+ console.log();
32
+ console.log(chalk.green('✓') +
33
+ ` Looks like ${chalk.bold(tpl.name)} (${tpl.id}, ${tpl.apiType}).`);
34
+ }
35
+ else {
36
+ console.log();
37
+ console.log(chalk.yellow('?') + ` Haven\'t seen this key format before. Tell me about the provider.`);
38
+ }
39
+ const id = await promptId(tpl?.id);
40
+ const name = await readLine(`display name [${tpl?.name ?? id}]: `, tpl?.name ?? id);
41
+ const apiType = await promptApiType(tpl?.apiType);
42
+ const baseUrl = await promptBaseUrl(apiType, tpl?.baseUrl);
43
+ const defaultModel = await readLine(`default model [${tpl?.defaultModel ?? ''}]: `, tpl?.defaultModel ?? '');
44
+ if (!defaultModel) {
45
+ console.error(chalk.red('A default model is required (e.g. "deepseek-chat").'));
46
+ process.exit(1);
47
+ }
48
+ const color = tpl?.color ?? (await nextPaletteColor());
49
+ const entry = {
50
+ apiKey: key,
51
+ apiType,
52
+ name,
53
+ defaultModel,
54
+ color,
55
+ custom: !tpl,
56
+ };
57
+ if (baseUrl)
58
+ entry.baseUrl = baseUrl;
59
+ await setProvider(id, entry);
60
+ console.log();
61
+ console.log(chalk.green('✓') +
62
+ ` Saved ${chalk.bold(name)} (${id}) — key ${chalk.dim(maskKey(key))}, color ${chalk.hex(color)('●')}`);
63
+ console.log(chalk.dim(` Stored at ${PROVIDERS_FILE_PATH} (mode 0600)`));
64
+ console.log();
65
+ console.log(chalk.dim(` Use it: mod8 -c (or any flag) → not yet; in chat: "ask ${id}" / "use ${id}"`));
66
+ }
67
+ async function promptId(suggested) {
68
+ while (true) {
69
+ const id = await readLine(`provider id [${suggested ?? 'lowercase, e.g. "deepseek"'}]: `, suggested ?? '');
70
+ if (!id) {
71
+ console.error(chalk.red(' id required (lowercase, letters/digits/dash/underscore).'));
72
+ continue;
73
+ }
74
+ if (!isValidProviderId(id)) {
75
+ console.error(chalk.red(' invalid id — use lowercase letters, digits, dash, underscore (max 30 chars).'));
76
+ continue;
77
+ }
78
+ return id;
79
+ }
80
+ }
81
+ async function promptApiType(suggested) {
82
+ const fallback = suggested ?? 'openai-compat';
83
+ while (true) {
84
+ const v = await readLine(`api type (anthropic | openai-compat | gemini) [${fallback}]: `, fallback);
85
+ if (isApiType(v))
86
+ return v;
87
+ console.error(chalk.red(` must be one of: ${API_TYPES.join(', ')}.`));
88
+ }
89
+ }
90
+ async function promptBaseUrl(apiType, suggested) {
91
+ if (apiType === 'anthropic' || apiType === 'gemini')
92
+ return undefined;
93
+ while (true) {
94
+ const v = await readLine(`base URL [${suggested ?? 'https://api.example.com/v1'}]: `, suggested ?? '');
95
+ if (!v) {
96
+ console.error(chalk.red(' base URL is required for openai-compat providers.'));
97
+ continue;
98
+ }
99
+ if (!/^https?:\/\//.test(v)) {
100
+ console.error(chalk.red(' must be http(s):// URL.'));
101
+ continue;
102
+ }
103
+ return v;
104
+ }
105
+ }
@@ -0,0 +1,158 @@
1
+ import chalk from 'chalk';
2
+ import { getProviderClient } from '../providers/index.js';
3
+ import { formatCost } from '../providers/pricing.js';
4
+ import { getConfig, updateConfig } from '../storage/config.js';
5
+ import { confirm } from '../util/prompt.js';
6
+ import { classifyError } from '../util/errors.js';
7
+ import { configuredProviderIds, resolveConfigured, } from '../storage/providers.js';
8
+ import { templateById } from '../providers/registry.js';
9
+ /**
10
+ * Resolve which providers `--all` should fan out to. Order:
11
+ * 1. Every provider currently configured in providers.json (post-migration)
12
+ * 2. Plus any of the legacy three (anthropic/openai/google) that have an
13
+ * env-var key set, so out-of-the-box `--all` still works for users who
14
+ * only export ANTHROPIC_API_KEY etc.
15
+ * 3. In MOD8_MOCK mode with no configured providers, fall back to the
16
+ * original three so existing mock-driven tests keep working.
17
+ */
18
+ async function resolveAllProviders() {
19
+ // In MOD8_MOCK mode we always run the legacy three side-by-side, regardless
20
+ // of which env keys happen to be set, so the mock-driven --all tests are
21
+ // deterministic and so demos always show three blocks even with an empty
22
+ // sandbox.
23
+ if (process.env.MOD8_MOCK === '1') {
24
+ const ids = ['anthropic', 'openai', 'google'];
25
+ const out = [];
26
+ for (const id of ids) {
27
+ const tpl = templateById(id);
28
+ out.push({
29
+ id,
30
+ entry: {
31
+ apiKey: 'mock',
32
+ apiType: tpl?.apiType ?? 'openai-compat',
33
+ name: tpl?.name ?? id,
34
+ defaultModel: tpl?.defaultModel ?? `${id}-mock-1`,
35
+ color: tpl?.color ?? '#888888',
36
+ },
37
+ });
38
+ }
39
+ return out;
40
+ }
41
+ const ids = await configuredProviderIds();
42
+ // Add legacy ids whose env keys are set but who aren't yet stored.
43
+ for (const legacy of ['anthropic', 'openai', 'google']) {
44
+ if (!ids.includes(legacy)) {
45
+ const env = await resolveConfigured(legacy);
46
+ if (env)
47
+ ids.push(legacy);
48
+ }
49
+ }
50
+ const out = [];
51
+ for (const id of ids) {
52
+ const entry = await resolveConfigured(id);
53
+ if (entry)
54
+ out.push({ id, entry });
55
+ else if (process.env.MOD8_MOCK === '1') {
56
+ // Synthesize a placeholder so the mock dispatcher still gets called.
57
+ out.push({
58
+ id,
59
+ entry: {
60
+ apiKey: 'mock',
61
+ apiType: 'openai-compat',
62
+ name: id,
63
+ defaultModel: `${id}-mock-1`,
64
+ color: '#888888',
65
+ },
66
+ });
67
+ }
68
+ }
69
+ return out;
70
+ }
71
+ export async function runAll(prompt) {
72
+ const targets = await resolveAllProviders();
73
+ if (targets.length === 0) {
74
+ console.error(chalk.red('mod8: --all has no configured providers.\n') +
75
+ chalk.dim(' Run mod8 keys set <provider> or mod8 add-provider, then retry.'));
76
+ process.exit(1);
77
+ }
78
+ console.log(chalk.dim(`Running against ${targets.length} providers in parallel…`));
79
+ const settled = await Promise.allSettled(targets.map(async ({ id }) => {
80
+ const client = await getProviderClient(id);
81
+ return client.call(prompt);
82
+ }));
83
+ const results = targets.map(({ id, entry }, i) => {
84
+ const s = settled[i];
85
+ if (s.status === 'fulfilled') {
86
+ return { id, label: entry.name, color: entry.color, ok: true, response: s.value };
87
+ }
88
+ return {
89
+ id,
90
+ label: entry.name,
91
+ color: entry.color,
92
+ ok: false,
93
+ error: classifyError(s.reason, id),
94
+ };
95
+ });
96
+ let totalTokens = 0;
97
+ let totalCost = 0;
98
+ let totalLatencyMs = 0;
99
+ let okCount = 0;
100
+ for (const r of results) {
101
+ renderBlock(r);
102
+ if (r.ok && r.response) {
103
+ totalTokens += r.response.inputTokens + r.response.outputTokens;
104
+ totalCost += r.response.costUsd;
105
+ totalLatencyMs = Math.max(totalLatencyMs, r.response.latencyMs);
106
+ okCount++;
107
+ }
108
+ }
109
+ console.log();
110
+ console.log(chalk.dim('─'.repeat(60)));
111
+ const seconds = (totalLatencyMs / 1000).toFixed(2);
112
+ const summary = `${okCount}/${results.length} ok · ${totalTokens.toLocaleString()} tok · ${seconds}s · ${formatCost(totalCost)}`;
113
+ console.log(`${chalk.bold('Total:')} ${chalk.dim(summary)}`);
114
+ }
115
+ function renderBlock(result) {
116
+ const color = chalk.hex(result.color);
117
+ console.log();
118
+ if (result.ok && result.response) {
119
+ const r = result.response;
120
+ const seconds = (r.latencyMs / 1000).toFixed(2);
121
+ const tokens = (r.inputTokens + r.outputTokens).toLocaleString();
122
+ console.log(`${color('▎')} ${chalk.bold(result.label)} ${chalk.dim(r.model)} ${chalk.dim(`${tokens} tok · ${seconds}s · ${formatCost(r.costUsd)}`)}`);
123
+ console.log(r.text.trimEnd());
124
+ }
125
+ else {
126
+ console.log(`${color('▎')} ${chalk.bold(result.label)} ${chalk.red('✗ failed')}`);
127
+ console.log(chalk.red(` ${result.error ?? 'Unknown error'}`));
128
+ }
129
+ }
130
+ /**
131
+ * Gate first --all use behind explicit consent.
132
+ * Must be called BEFORE stdin is consumed (the prompt needs the TTY/pipe to be readable).
133
+ */
134
+ export async function ensureAllConsent({ stdinPiped }) {
135
+ if (process.env.MOD8_AUTO_CONFIRM === '1')
136
+ return;
137
+ const config = await getConfig();
138
+ if (config.allConsent)
139
+ return;
140
+ if (stdinPiped) {
141
+ console.error(chalk.red('mod8: --all needs first-run confirmation, but stdin is piped.'));
142
+ console.error(chalk.dim(' Run mod8 --all interactively once, or set MOD8_AUTO_CONFIRM=1 to accept non-interactively.'));
143
+ process.exit(1);
144
+ }
145
+ console.log();
146
+ console.log(chalk.yellow('First time using --all:'));
147
+ console.log(chalk.dim(` This sends your prompt to every configured provider in parallel.\n` +
148
+ ` Cost depends on prompt length and the providers' rates.\n` +
149
+ ' This confirmation only appears once.'));
150
+ console.log();
151
+ const ok = await confirm(chalk.bold('Continue? [y/N]: '));
152
+ if (!ok) {
153
+ console.log(chalk.dim('Cancelled.'));
154
+ process.exit(0);
155
+ }
156
+ await updateConfig({ allConsent: true });
157
+ console.log();
158
+ }