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,90 @@
1
+ name: behavior — switch-back works mid-error
2
+ description: |
3
+ Repro: user is in work mode, the work provider is rate-limited / 401 / down,
4
+ user types "mod8" or "back to mod8" trying to escape — every input was
5
+ being sent to the broken provider, getting an error, and the user was
6
+ stuck.
7
+
8
+ Fix verified by these tests: parseHostBack runs LOCALLY in the chat REPL
9
+ before any provider call, so all of these inputs are intercepted as a
10
+ switch-back to host regardless of whether the work provider is healthy
11
+ or failing. dev:resolve mirrors chat.tsx's match priority — "host-back"
12
+ always wins over "route" / "compare" / etc.
13
+
14
+ tests:
15
+ - name: 'bare "mod8" is intercepted as switch-back'
16
+ shell: 'mod8 dev:resolve "mod8"'
17
+ expect:
18
+ stdout_contains: "host-back"
19
+
20
+ - name: '"/mod8" is intercepted as switch-back'
21
+ shell: 'mod8 dev:resolve "/mod8"'
22
+ expect:
23
+ stdout_contains: "host-back"
24
+
25
+ - name: '"@mod8" is intercepted as switch-back'
26
+ shell: 'mod8 dev:resolve "@mod8"'
27
+ expect:
28
+ stdout_contains: "host-back"
29
+
30
+ - name: '"back to mod8" is intercepted'
31
+ shell: 'mod8 dev:resolve "back to mod8"'
32
+ expect:
33
+ stdout_contains: "host-back"
34
+
35
+ - name: '"switch to mod8" is intercepted (was previously routing to non-existent provider id "mod8")'
36
+ shell: 'mod8 dev:resolve "switch to mod8"'
37
+ expect:
38
+ stdout_contains: "host-back"
39
+
40
+ - name: '"change to mod8" is intercepted'
41
+ shell: 'mod8 dev:resolve "change to mod8"'
42
+ expect:
43
+ stdout_contains: "host-back"
44
+
45
+ - name: '"go back" is intercepted'
46
+ shell: 'mod8 dev:resolve "go back"'
47
+ expect:
48
+ stdout_contains: "host-back"
49
+
50
+ - name: 'bare "back" is intercepted'
51
+ shell: 'mod8 dev:resolve "back"'
52
+ expect:
53
+ stdout_contains: "host-back"
54
+
55
+ - name: '"switch back" is intercepted'
56
+ shell: 'mod8 dev:resolve "switch back"'
57
+ expect:
58
+ stdout_contains: "host-back"
59
+
60
+ - name: '"return to mod8" is intercepted'
61
+ shell: 'mod8 dev:resolve "return to mod8"'
62
+ expect:
63
+ stdout_contains: "host-back"
64
+
65
+ - name: '"talk to mod8" is intercepted (mod8 is special — never a provider id)'
66
+ shell: 'mod8 dev:resolve "talk to mod8"'
67
+ expect:
68
+ stdout_contains: "host-back"
69
+
70
+ - name: '"back to host" is intercepted (alternate phrasing)'
71
+ shell: 'mod8 dev:resolve "back to host"'
72
+ expect:
73
+ stdout_contains: "host-back"
74
+
75
+ - name: '"mod8 thanks for the help" carries rest="thanks for the help"'
76
+ shell: 'mod8 dev:resolve "mod8 thanks for the help"'
77
+ expect:
78
+ stdout_matches: 'host-back rest="thanks for the help"'
79
+
80
+ - name: an unrelated message ("write me a haiku") is NOT mistaken for switch-back
81
+ shell: 'mod8 dev:resolve "write me a haiku"'
82
+ expect:
83
+ stdout_contains: "none"
84
+
85
+ - name: '"use codex" is NOT swallowed by host-back (still a provider route)'
86
+ shell: 'mod8 dev:resolve "use codex"'
87
+ expect:
88
+ stdout_contains: "route id=codex"
89
+ stdout_omits:
90
+ - "host-back"
@@ -0,0 +1,109 @@
1
+ name: behavior — work-mode model stays in character
2
+ description: |
3
+ Drilling deeper into work-mode discipline. When the user is in work
4
+ mode, the worker model should:
5
+ - Identify as itself (claude / codex / etc), NOT as mod8
6
+ - Just do the work, NOT lecture about provider config
7
+ - Defer meta questions about mod8 back to host with <SWITCH_TO_HOST>
8
+ - Stay engaged for follow-ups, not bounce on every turn
9
+
10
+ tests:
11
+ - name: work — "what is mod8?" defers, doesn't impersonate
12
+ requires_api_key: true
13
+ setup:
14
+ - shell: |
15
+ mkdir -p $MOD8_CONFIG_DIR
16
+ cat > $MOD8_CONFIG_DIR/providers.json <<JSON
17
+ {
18
+ "anthropic": {
19
+ "apiKey": "$ANTHROPIC_API_KEY",
20
+ "apiType": "anthropic",
21
+ "name": "Anthropic (Claude)",
22
+ "defaultModel": "claude-sonnet-4-6",
23
+ "color": "#A78BFA"
24
+ }
25
+ }
26
+ JSON
27
+ chmod 600 $MOD8_CONFIG_DIR/providers.json
28
+ shell: 'mod8 dev:work-ask anthropic "what is mod8?"'
29
+ expect:
30
+ stdout_omits:
31
+ - "I am mod8"
32
+ - "I'm mod8"
33
+ - "mod8 is a"
34
+ # Should hand back, mention host, or say "ask mod8"
35
+ stdout_matches: "(?i)(host|mod8|/mod8|handing back)"
36
+
37
+ - name: work — "how do I add a new provider?" is deferred to host
38
+ requires_api_key: true
39
+ setup:
40
+ - shell: |
41
+ mkdir -p $MOD8_CONFIG_DIR
42
+ cat > $MOD8_CONFIG_DIR/providers.json <<JSON
43
+ {
44
+ "anthropic": {
45
+ "apiKey": "$ANTHROPIC_API_KEY",
46
+ "apiType": "anthropic",
47
+ "name": "Anthropic (Claude)",
48
+ "defaultModel": "claude-sonnet-4-6",
49
+ "color": "#A78BFA"
50
+ }
51
+ }
52
+ JSON
53
+ chmod 600 $MOD8_CONFIG_DIR/providers.json
54
+ shell: 'mod8 dev:work-ask anthropic "how do I add a new provider to mod8?"'
55
+ expect:
56
+ # Deferral signals: hand-off phrasing OR pointing the user back to host.
57
+ stdout_matches: "(?i)(handing back|hand.+back|host|/mod8|mod8 question|ask mod8|that.+for mod8)"
58
+ stdout_omits:
59
+ - "Run `mod8 add-provider`"
60
+ - "use mod8 add-provider"
61
+
62
+ - name: work — coding task is answered fully, no hand-off
63
+ requires_api_key: true
64
+ setup:
65
+ - shell: |
66
+ mkdir -p $MOD8_CONFIG_DIR
67
+ cat > $MOD8_CONFIG_DIR/providers.json <<JSON
68
+ {
69
+ "anthropic": {
70
+ "apiKey": "$ANTHROPIC_API_KEY",
71
+ "apiType": "anthropic",
72
+ "name": "Anthropic (Claude)",
73
+ "defaultModel": "claude-sonnet-4-6",
74
+ "color": "#A78BFA"
75
+ }
76
+ }
77
+ JSON
78
+ chmod 600 $MOD8_CONFIG_DIR/providers.json
79
+ shell: 'mod8 dev:work-ask anthropic "give me a one-line javascript array reverse"'
80
+ expect:
81
+ stdout_matches: "(?i)(reverse|slice\\(\\)\\.reverse)"
82
+ stdout_omits:
83
+ - "<SWITCH_TO_HOST>"
84
+ - "handing back"
85
+ - "mod8 question"
86
+
87
+ - name: work — "who are you" answers as the worker, not mod8
88
+ requires_api_key: true
89
+ setup:
90
+ - shell: |
91
+ mkdir -p $MOD8_CONFIG_DIR
92
+ cat > $MOD8_CONFIG_DIR/providers.json <<JSON
93
+ {
94
+ "anthropic": {
95
+ "apiKey": "$ANTHROPIC_API_KEY",
96
+ "apiType": "anthropic",
97
+ "name": "Anthropic (Claude)",
98
+ "defaultModel": "claude-sonnet-4-6",
99
+ "color": "#A78BFA"
100
+ }
101
+ }
102
+ JSON
103
+ chmod 600 $MOD8_CONFIG_DIR/providers.json
104
+ shell: 'mod8 dev:work-ask anthropic "who are you exactly?"'
105
+ expect:
106
+ stdout_matches: "(?i)claude"
107
+ stdout_omits:
108
+ - "I am mod8"
109
+ - "I'm mod8"
@@ -0,0 +1,349 @@
1
+ name: chat meta — mod8 knows itself
2
+ description: |
3
+ Regression suite for "mod8 doesn't know mod8" bug. The host system prompt
4
+ must include live data about the user's configured providers and the CLI's
5
+ feature surface, so meta questions get answered from facts — not pivoted
6
+ into "tell me about your project."
7
+
8
+ Tests use mod8 dev:host-ask, which runs a one-shot through the same host
9
+ system prompt the chat REPL uses. All require a real Anthropic key.
10
+
11
+ tests:
12
+ - name: meta — what is mod8
13
+ requires_api_key: true
14
+ setup:
15
+ - shell: |
16
+ mkdir -p $MOD8_CONFIG_DIR
17
+ cat > $MOD8_CONFIG_DIR/providers.json <<JSON
18
+ {
19
+ "anthropic": {
20
+ "apiKey": "$ANTHROPIC_API_KEY",
21
+ "apiType": "anthropic",
22
+ "name": "Anthropic (Claude)",
23
+ "defaultModel": "claude-sonnet-4-6",
24
+ "color": "#A78BFA"
25
+ }
26
+ }
27
+ JSON
28
+ chmod 600 $MOD8_CONFIG_DIR/providers.json
29
+ shell: 'mod8 dev:host-ask "what is mod8?"'
30
+ expect:
31
+ stdout_contains:
32
+ - mod8
33
+ stdout_matches: "(BYOK|provider|terminal)"
34
+
35
+ - name: meta — what platforms are connected
36
+ requires_api_key: true
37
+ setup:
38
+ - shell: |
39
+ mkdir -p $MOD8_CONFIG_DIR
40
+ cat > $MOD8_CONFIG_DIR/providers.json <<JSON
41
+ {
42
+ "anthropic": {
43
+ "apiKey": "$ANTHROPIC_API_KEY",
44
+ "apiType": "anthropic",
45
+ "name": "Anthropic (Claude)",
46
+ "defaultModel": "claude-sonnet-4-6",
47
+ "color": "#A78BFA"
48
+ },
49
+ "openai": {
50
+ "apiKey": "sk-fake-test-not-called",
51
+ "apiType": "openai-compat",
52
+ "name": "OpenAI (GPT)",
53
+ "baseUrl": "https://api.openai.com/v1",
54
+ "defaultModel": "gpt-4o",
55
+ "color": "#10B981"
56
+ }
57
+ }
58
+ JSON
59
+ chmod 600 $MOD8_CONFIG_DIR/providers.json
60
+ shell: 'mod8 dev:host-ask "what platforms are connected to you right now?"'
61
+ expect:
62
+ # Accept either provider ids ("anthropic", "openai") or display names
63
+ # ("Anthropic", "OpenAI"/"GPT") — the host may use either.
64
+ stdout_matches: "(?i)(anthropic|claude)"
65
+ stdout_contains:
66
+ - "configured"
67
+
68
+ - name: meta — user's custom display name is recognized
69
+ requires_api_key: true
70
+ setup:
71
+ - shell: |
72
+ mkdir -p $MOD8_CONFIG_DIR
73
+ cat > $MOD8_CONFIG_DIR/providers.json <<JSON
74
+ {
75
+ "anthropic": {
76
+ "apiKey": "$ANTHROPIC_API_KEY",
77
+ "apiType": "anthropic",
78
+ "name": "Anthropic (Claude)",
79
+ "defaultModel": "claude-sonnet-4-6",
80
+ "color": "#A78BFA"
81
+ },
82
+ "openai": {
83
+ "apiKey": "sk-fake-test-not-called",
84
+ "apiType": "openai-compat",
85
+ "name": "codex",
86
+ "baseUrl": "https://api.openai.com/v1",
87
+ "defaultModel": "gpt-4o",
88
+ "color": "#10B981"
89
+ }
90
+ }
91
+ JSON
92
+ chmod 600 $MOD8_CONFIG_DIR/providers.json
93
+ shell: 'mod8 dev:host-ask "what is codex?"'
94
+ expect:
95
+ stdout_contains:
96
+ - codex
97
+ stdout_matches: "(openai|gpt|provider)"
98
+ stdout_omits:
99
+ - "tell me about your project"
100
+ - "what is your project"
101
+
102
+ - name: meta — how do I add a new provider
103
+ requires_api_key: true
104
+ setup:
105
+ - shell: |
106
+ mkdir -p $MOD8_CONFIG_DIR
107
+ cat > $MOD8_CONFIG_DIR/providers.json <<JSON
108
+ {
109
+ "anthropic": {
110
+ "apiKey": "$ANTHROPIC_API_KEY",
111
+ "apiType": "anthropic",
112
+ "name": "Anthropic (Claude)",
113
+ "defaultModel": "claude-sonnet-4-6",
114
+ "color": "#A78BFA"
115
+ }
116
+ }
117
+ JSON
118
+ chmod 600 $MOD8_CONFIG_DIR/providers.json
119
+ shell: 'mod8 dev:host-ask "how do I add a new provider?"'
120
+ expect:
121
+ stdout_contains:
122
+ - "add-provider"
123
+
124
+ - name: meta — what models do I have
125
+ requires_api_key: true
126
+ setup:
127
+ - shell: |
128
+ mkdir -p $MOD8_CONFIG_DIR
129
+ cat > $MOD8_CONFIG_DIR/providers.json <<JSON
130
+ {
131
+ "anthropic": {
132
+ "apiKey": "$ANTHROPIC_API_KEY",
133
+ "apiType": "anthropic",
134
+ "name": "Anthropic (Claude)",
135
+ "defaultModel": "claude-sonnet-4-6",
136
+ "color": "#A78BFA"
137
+ },
138
+ "deepseek": {
139
+ "apiKey": "sk-fake",
140
+ "apiType": "openai-compat",
141
+ "name": "DeepSeek",
142
+ "baseUrl": "https://api.deepseek.com",
143
+ "defaultModel": "deepseek-chat",
144
+ "color": "#3B82F6"
145
+ }
146
+ }
147
+ JSON
148
+ chmod 600 $MOD8_CONFIG_DIR/providers.json
149
+ shell: 'mod8 dev:host-ask "what models do I have available?"'
150
+ expect:
151
+ stdout_contains:
152
+ - claude-sonnet-4-6
153
+ - deepseek-chat
154
+
155
+ - name: meta — what can you do mentions key features
156
+ requires_api_key: true
157
+ setup:
158
+ - shell: |
159
+ mkdir -p $MOD8_CONFIG_DIR
160
+ cat > $MOD8_CONFIG_DIR/providers.json <<JSON
161
+ {
162
+ "anthropic": {
163
+ "apiKey": "$ANTHROPIC_API_KEY",
164
+ "apiType": "anthropic",
165
+ "name": "Anthropic (Claude)",
166
+ "defaultModel": "claude-sonnet-4-6",
167
+ "color": "#A78BFA"
168
+ }
169
+ }
170
+ JSON
171
+ chmod 600 $MOD8_CONFIG_DIR/providers.json
172
+ shell: 'mod8 dev:host-ask "what can you do?"'
173
+ expect:
174
+ stdout_contains:
175
+ - mod8
176
+ stdout_matches: "(compare|chat|provider|--all)"
177
+
178
+ - name: meta — empty provider list is acknowledged honestly
179
+ requires_api_key: true
180
+ shell: 'mod8 dev:host-ask "what providers are configured?"'
181
+ expect:
182
+ stdout_matches: "(?i)(none|no providers|nothing|not configured|empty|haven't)"
183
+
184
+ - name: meta — does NOT pivot to user's project on bare meta question
185
+ requires_api_key: true
186
+ setup:
187
+ - shell: |
188
+ mkdir -p $MOD8_CONFIG_DIR
189
+ cat > $MOD8_CONFIG_DIR/providers.json <<JSON
190
+ {
191
+ "anthropic": {
192
+ "apiKey": "$ANTHROPIC_API_KEY",
193
+ "apiType": "anthropic",
194
+ "name": "Anthropic (Claude)",
195
+ "defaultModel": "claude-sonnet-4-6",
196
+ "color": "#A78BFA"
197
+ }
198
+ }
199
+ JSON
200
+ chmod 600 $MOD8_CONFIG_DIR/providers.json
201
+ shell: 'mod8 dev:host-ask "what is this?"'
202
+ expect:
203
+ stdout_contains: mod8
204
+ stdout_omits:
205
+ - "tell me about your project"
206
+ - "what are you building"
207
+
208
+ - name: meta — typo'd "operator" phrasing still recognized
209
+ requires_api_key: true
210
+ setup:
211
+ - shell: |
212
+ mkdir -p $MOD8_CONFIG_DIR
213
+ cat > $MOD8_CONFIG_DIR/providers.json <<JSON
214
+ {
215
+ "anthropic": {
216
+ "apiKey": "$ANTHROPIC_API_KEY",
217
+ "apiType": "anthropic",
218
+ "name": "Anthropic (Claude)",
219
+ "defaultModel": "claude-sonnet-4-6",
220
+ "color": "#A78BFA"
221
+ },
222
+ "openai": {
223
+ "apiKey": "sk-fake",
224
+ "apiType": "openai-compat",
225
+ "name": "codex",
226
+ "baseUrl": "https://api.openai.com/v1",
227
+ "defaultModel": "gpt-4o",
228
+ "color": "#10B981"
229
+ }
230
+ }
231
+ JSON
232
+ chmod 600 $MOD8_CONFIG_DIR/providers.json
233
+ # The exact user-reported phrasing that triggered the original bug.
234
+ shell: 'mod8 dev:host-ask "hi how many operator you connect 2 ?"'
235
+ expect:
236
+ stdout_contains:
237
+ - "2"
238
+ stdout_omits:
239
+ - "I don't have info"
240
+ - "I don't have details"
241
+ - "tell me about your project"
242
+ - "what are you building"
243
+
244
+ - name: meta — never says "I don't have details about my setup"
245
+ requires_api_key: true
246
+ setup:
247
+ - shell: |
248
+ mkdir -p $MOD8_CONFIG_DIR
249
+ cat > $MOD8_CONFIG_DIR/providers.json <<JSON
250
+ {
251
+ "anthropic": {
252
+ "apiKey": "$ANTHROPIC_API_KEY",
253
+ "apiType": "anthropic",
254
+ "name": "Anthropic (Claude)",
255
+ "defaultModel": "claude-sonnet-4-6",
256
+ "color": "#A78BFA"
257
+ }
258
+ }
259
+ JSON
260
+ chmod 600 $MOD8_CONFIG_DIR/providers.json
261
+ shell: 'mod8 dev:host-ask "what is powering you?"'
262
+ expect:
263
+ stdout_omits:
264
+ - "I don't have info"
265
+ - "I don't have details"
266
+ - "what's powering me"
267
+
268
+ # ------------------------------------------------------------
269
+ # Routing / synonym tests — deterministic, no LLM involved.
270
+ # Use mod8 dev:resolve, which runs the chat REPL's
271
+ # parseProviderRoute + resolveProviderHint and prints the result.
272
+ # ------------------------------------------------------------
273
+
274
+ - name: routing — "use deepseek" matches and resolves to deepseek
275
+ shell: 'mod8 dev:resolve "use deepseek"'
276
+ expect:
277
+ stdout_contains: "route id=deepseek resolved=deepseek"
278
+
279
+ - name: routing — "let me talk to gpt" maps to openai
280
+ shell: 'mod8 dev:resolve "let me talk to gpt"'
281
+ expect:
282
+ stdout_contains: "route id=gpt resolved=openai"
283
+
284
+ - name: routing — "talk to claude" maps to anthropic
285
+ shell: 'mod8 dev:resolve "talk to claude"'
286
+ expect:
287
+ stdout_contains: "route id=claude resolved=anthropic"
288
+
289
+ - name: routing — display-name match resolves to canonical id
290
+ setup:
291
+ - shell: |
292
+ mkdir -p $MOD8_CONFIG_DIR
293
+ cat > $MOD8_CONFIG_DIR/providers.json <<JSON
294
+ {
295
+ "openai": {
296
+ "apiKey": "sk-fake",
297
+ "apiType": "openai-compat",
298
+ "name": "codex",
299
+ "baseUrl": "https://api.openai.com/v1",
300
+ "defaultModel": "gpt-4o",
301
+ "color": "#10B981"
302
+ }
303
+ }
304
+ JSON
305
+ chmod 600 $MOD8_CONFIG_DIR/providers.json
306
+ shell: 'mod8 dev:resolve "let me talk to codex"'
307
+ expect:
308
+ stdout_contains: "route id=codex resolved=openai"
309
+
310
+ - name: 'routing — "ask grok: <prompt>" carries the rest payload'
311
+ shell: 'mod8 dev:resolve "ask grok: what is the weather"'
312
+ expect:
313
+ stdout_contains: "route id=grok resolved=xai"
314
+ stdout_matches: 'rest="what is the weather"'
315
+
316
+ - name: routing — "switch over to mistral" matches
317
+ shell: 'mod8 dev:resolve "switch over to mistral"'
318
+ expect:
319
+ stdout_contains: "route id=mistral resolved=mistral"
320
+
321
+ - name: routing — non-route input falls through cleanly
322
+ shell: 'mod8 dev:resolve "what is the weather today"'
323
+ expect:
324
+ stdout_contains: "none"
325
+
326
+ - name: 'routing — "compare all: <prompt>" extracts payload'
327
+ shell: 'mod8 dev:resolve "compare all: write a haiku"'
328
+ expect:
329
+ stdout_matches: 'compare payload="write a haiku"'
330
+
331
+ - name: 'routing — "/compare <prompt>" extracts payload'
332
+ shell: 'mod8 dev:resolve "/compare write a haiku"'
333
+ expect:
334
+ stdout_matches: 'compare payload="write a haiku"'
335
+
336
+ - name: routing — bare "compare all" is recognized
337
+ shell: 'mod8 dev:resolve "compare all"'
338
+ expect:
339
+ stdout_contains: "compare-bare"
340
+
341
+ - name: routing — unknown id resolves to null
342
+ shell: 'mod8 dev:resolve "use unknownprovider"'
343
+ expect:
344
+ stdout_contains: "route id=unknownprovider resolved=null"
345
+
346
+ - name: routing — display-name "Anthropic (Claude)" resolves
347
+ shell: 'mod8 dev:resolve "use anthropic"'
348
+ expect:
349
+ stdout_contains: "route id=anthropic resolved=anthropic"
@@ -0,0 +1,148 @@
1
+ name: chat startup behavior
2
+ description: |
3
+ Bare `mod8` opens a NEW chat session, not the most-recent one. Resume is
4
+ explicit — `mod8 resume` (no args) for most-recent, `mod8 resume <id>` for a
5
+ specific session.
6
+
7
+ Drives the CLI through piped stdin so Ink crashes on raw-mode entry, but
8
+ whatever printed BEFORE the crash is captured for assertion. In particular,
9
+ the "resuming session …" line is printed by runChat() before render(),
10
+ so its presence/absence is observable even without a real PTY.
11
+
12
+ tests:
13
+ - name: bare mod8 with prior sessions does NOT resume
14
+ requires_api_key: true
15
+ setup:
16
+ - shell: |
17
+ mkdir -p $MOD8_CONFIG_DIR/sessions
18
+ cat > $MOD8_CONFIG_DIR/sessions/2026-05-07-prev.json <<JSON
19
+ {
20
+ "version": 1,
21
+ "id": "2026-05-07-prev",
22
+ "title": "old session about something",
23
+ "createdAt": 1715000000000,
24
+ "lastActivity": 1715000000000,
25
+ "messages": [
26
+ {"role": "user", "content": "remember the keyword fuchsia", "mode": "host"},
27
+ {"role": "assistant", "content": "got it, fuchsia.", "mode": "host"}
28
+ ]
29
+ }
30
+ JSON
31
+ chmod 600 $MOD8_CONFIG_DIR/sessions/2026-05-07-prev.json
32
+ repl:
33
+ run: mod8
34
+ timeout_ms: 6000
35
+ inputs:
36
+ - send: ""
37
+ delay_ms: 1500
38
+ expect:
39
+ # Welcome banner must show, but the resuming-session line must NOT.
40
+ stdout_contains:
41
+ - "switch to claude"
42
+ stdout_omits:
43
+ - "resuming session"
44
+ - "fuchsia"
45
+ - "old session about something"
46
+
47
+ - name: mod8 new also opens a fresh session (no resume line)
48
+ requires_api_key: true
49
+ setup:
50
+ - shell: |
51
+ mkdir -p $MOD8_CONFIG_DIR/sessions
52
+ cat > $MOD8_CONFIG_DIR/sessions/2026-05-07-prev.json <<JSON
53
+ {
54
+ "version": 1,
55
+ "id": "2026-05-07-prev",
56
+ "title": "old",
57
+ "createdAt": 1715000000000,
58
+ "lastActivity": 1715000000000,
59
+ "messages": [
60
+ {"role": "user", "content": "x", "mode": "host"},
61
+ {"role": "assistant", "content": "y", "mode": "host"}
62
+ ]
63
+ }
64
+ JSON
65
+ chmod 600 $MOD8_CONFIG_DIR/sessions/2026-05-07-prev.json
66
+ repl:
67
+ run: mod8 new
68
+ timeout_ms: 6000
69
+ inputs:
70
+ - send: ""
71
+ delay_ms: 1500
72
+ expect:
73
+ stdout_contains:
74
+ - "switch to claude"
75
+ stdout_omits:
76
+ - "resuming session"
77
+
78
+ - name: mod8 resume (no id) resumes the most recent session
79
+ requires_api_key: true
80
+ setup:
81
+ - shell: |
82
+ mkdir -p $MOD8_CONFIG_DIR/sessions
83
+ cat > $MOD8_CONFIG_DIR/sessions/2026-05-07-prev.json <<JSON
84
+ {
85
+ "version": 1,
86
+ "id": "2026-05-07-prev",
87
+ "title": "the recent session",
88
+ "createdAt": 1715000000000,
89
+ "lastActivity": 1715000000000,
90
+ "messages": [
91
+ {"role": "user", "content": "x", "mode": "host"},
92
+ {"role": "assistant", "content": "y", "mode": "host"}
93
+ ]
94
+ }
95
+ JSON
96
+ chmod 600 $MOD8_CONFIG_DIR/sessions/2026-05-07-prev.json
97
+ repl:
98
+ run: mod8 resume
99
+ timeout_ms: 6000
100
+ inputs:
101
+ - send: ""
102
+ delay_ms: 1500
103
+ expect:
104
+ stdout_contains:
105
+ - "resuming session 2026-05-07-prev"
106
+
107
+ - name: mod8 resume on empty store is rejected, not silently new
108
+ repl:
109
+ run: mod8 resume
110
+ timeout_ms: 6000
111
+ inputs:
112
+ - send: ""
113
+ delay_ms: 1500
114
+ expect:
115
+ stderr_contains:
116
+ - "no sessions to resume"
117
+ - "mod8 list"
118
+ exit_code: 1
119
+
120
+ - name: mod8 resume <id> still works
121
+ requires_api_key: true
122
+ setup:
123
+ - shell: |
124
+ mkdir -p $MOD8_CONFIG_DIR/sessions
125
+ cat > $MOD8_CONFIG_DIR/sessions/2026-05-07-spec.json <<JSON
126
+ {
127
+ "version": 1,
128
+ "id": "2026-05-07-spec",
129
+ "title": "specific",
130
+ "createdAt": 1715000000000,
131
+ "lastActivity": 1715000000000,
132
+ "messages": [
133
+ {"role": "user", "content": "marker fuchsia-bytemark", "mode": "host"},
134
+ {"role": "assistant", "content": "noted", "mode": "host"}
135
+ ]
136
+ }
137
+ JSON
138
+ chmod 600 $MOD8_CONFIG_DIR/sessions/2026-05-07-spec.json
139
+ repl:
140
+ run: mod8 resume 2026-05-07-spec
141
+ timeout_ms: 6000
142
+ inputs:
143
+ - send: ""
144
+ delay_ms: 1500
145
+ expect:
146
+ stdout_contains:
147
+ - "resuming session 2026-05-07-spec"
148
+ - "marker fuchsia-bytemark"