shennian 0.2.6 → 0.2.7

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 (85) hide show
  1. package/dist/bin/shennian.d.ts +2 -0
  2. package/dist/bin/shennian.js +2 -0
  3. package/dist/src/agents/adapter.d.ts +35 -0
  4. package/dist/src/agents/adapter.js +19 -0
  5. package/dist/src/agents/claude.d.ts +25 -0
  6. package/dist/src/agents/claude.js +234 -0
  7. package/dist/src/agents/codex.d.ts +52 -0
  8. package/dist/src/agents/codex.js +829 -0
  9. package/dist/src/agents/command-spec.d.ts +36 -0
  10. package/dist/src/agents/command-spec.js +303 -0
  11. package/dist/src/agents/cursor.d.ts +22 -0
  12. package/dist/src/agents/cursor.js +248 -0
  13. package/dist/src/agents/custom.d.ts +30 -0
  14. package/dist/src/agents/custom.js +210 -0
  15. package/dist/src/agents/detect.d.ts +9 -0
  16. package/dist/src/agents/detect.js +46 -0
  17. package/dist/src/agents/gemini.d.ts +17 -0
  18. package/dist/src/agents/gemini.js +172 -0
  19. package/dist/src/agents/model-registry/cache.d.ts +4 -0
  20. package/dist/src/agents/model-registry/cache.js +37 -0
  21. package/dist/src/agents/model-registry/discovery.d.ts +4 -0
  22. package/dist/src/agents/model-registry/discovery.js +166 -0
  23. package/dist/src/agents/model-registry/parsers.d.ts +11 -0
  24. package/dist/src/agents/model-registry/parsers.js +349 -0
  25. package/dist/src/agents/model-registry/runner.d.ts +6 -0
  26. package/dist/src/agents/model-registry/runner.js +29 -0
  27. package/dist/src/agents/model-registry/service.d.ts +6 -0
  28. package/dist/src/agents/model-registry/service.js +71 -0
  29. package/dist/src/agents/model-registry/types.d.ts +30 -0
  30. package/dist/src/agents/model-registry/types.js +8 -0
  31. package/dist/src/agents/model-registry.d.ts +13 -0
  32. package/dist/src/agents/model-registry.js +15 -0
  33. package/dist/src/agents/openclaw.d.ts +20 -0
  34. package/dist/src/agents/openclaw.js +273 -0
  35. package/dist/src/agents/pi.d.ts +51 -0
  36. package/dist/src/agents/pi.js +793 -0
  37. package/dist/src/commands/agent.d.ts +2 -0
  38. package/dist/src/commands/agent.js +131 -0
  39. package/dist/src/commands/daemon.d.ts +35 -0
  40. package/dist/src/commands/daemon.js +612 -0
  41. package/dist/src/commands/pair-qr.d.ts +4 -0
  42. package/dist/src/commands/pair-qr.js +6 -0
  43. package/dist/src/commands/pair.d.ts +8 -0
  44. package/dist/src/commands/pair.js +162 -0
  45. package/dist/src/commands/upgrade.d.ts +5 -0
  46. package/dist/src/commands/upgrade.js +145 -0
  47. package/dist/src/config/index.d.ts +32 -0
  48. package/dist/src/config/index.js +31 -0
  49. package/dist/src/fs/handler.d.ts +26 -0
  50. package/dist/src/fs/handler.js +130 -0
  51. package/dist/src/fs/security.d.ts +2 -0
  52. package/dist/src/fs/security.js +32 -0
  53. package/dist/src/index.d.ts +1 -0
  54. package/dist/src/index.js +277 -0
  55. package/dist/src/log-reporter.d.ts +18 -0
  56. package/dist/src/log-reporter.js +16 -0
  57. package/dist/src/native-fusion/parsers.d.ts +12 -0
  58. package/dist/src/native-fusion/parsers.js +687 -0
  59. package/dist/src/native-fusion/service.d.ts +25 -0
  60. package/dist/src/native-fusion/service.js +176 -0
  61. package/dist/src/native-fusion/state.d.ts +3 -0
  62. package/dist/src/native-fusion/state.js +22 -0
  63. package/dist/src/native-fusion/types.d.ts +24 -0
  64. package/dist/src/native-fusion/types.js +1 -0
  65. package/dist/src/region.d.ts +15 -0
  66. package/dist/src/region.js +99 -0
  67. package/dist/src/relay/client.d.ts +62 -0
  68. package/dist/src/relay/client.js +311 -0
  69. package/dist/src/session/handlers/agents.d.ts +4 -0
  70. package/dist/src/session/handlers/agents.js +55 -0
  71. package/dist/src/session/handlers/chat.d.ts +4 -0
  72. package/dist/src/session/handlers/chat.js +326 -0
  73. package/dist/src/session/handlers/control.d.ts +5 -0
  74. package/dist/src/session/handlers/control.js +57 -0
  75. package/dist/src/session/handlers/fs.d.ts +10 -0
  76. package/dist/src/session/handlers/fs.js +256 -0
  77. package/dist/src/session/manager.d.ts +28 -0
  78. package/dist/src/session/manager.js +174 -0
  79. package/dist/src/session/store.d.ts +7 -0
  80. package/dist/src/session/store.js +46 -0
  81. package/dist/src/session/types.d.ts +35 -0
  82. package/dist/src/session/types.js +4 -0
  83. package/dist/src/upgrade/engine.d.ts +64 -0
  84. package/dist/src/upgrade/engine.js +238 -0
  85. package/package.json +2 -2
@@ -0,0 +1,349 @@
1
+ // @arch docs/architecture/cli/model-discovery.md
2
+ // @test src/__tests__/model-switching.test.ts
3
+ export function stripAnsi(text) {
4
+ return text
5
+ .replace(/\u001b\[[0-9;?]*[a-zA-Z]/g, '') // eslint-disable-line no-control-regex
6
+ .replace(/\u001b\][^\u0007]*\u0007/g, '') // eslint-disable-line no-control-regex
7
+ .replace(/[\u0000-\u0008\u000b-\u001f\u007f]/g, ''); // eslint-disable-line no-control-regex
8
+ }
9
+ function uniqueModels(models) {
10
+ const seen = new Set();
11
+ const unique = [];
12
+ for (const model of models) {
13
+ if (!model.id || seen.has(model.id))
14
+ continue;
15
+ seen.add(model.id);
16
+ unique.push(model);
17
+ }
18
+ return unique;
19
+ }
20
+ function titleCaseSegment(segment) {
21
+ if (!segment)
22
+ return segment;
23
+ return segment[0].toUpperCase() + segment.slice(1);
24
+ }
25
+ function prettifyCodexModel(id) {
26
+ return id
27
+ .split('-')
28
+ .map((segment) => {
29
+ if (segment === 'gpt')
30
+ return 'GPT';
31
+ if (segment === 'codex')
32
+ return 'Codex';
33
+ return /^\d/.test(segment) ? segment : titleCaseSegment(segment);
34
+ })
35
+ .join('-');
36
+ }
37
+ function prettifyReasoningEffort(id) {
38
+ switch (id) {
39
+ case 'low':
40
+ return 'Low';
41
+ case 'medium':
42
+ return 'Medium';
43
+ case 'high':
44
+ return 'High';
45
+ case 'xhigh':
46
+ return 'Extra High';
47
+ default:
48
+ return titleCaseSegment(id);
49
+ }
50
+ }
51
+ function prettifyCursorModel(id) {
52
+ return id
53
+ .split('-')
54
+ .map((segment) => (/^\d/.test(segment) ? segment : titleCaseSegment(segment)))
55
+ .join(' ');
56
+ }
57
+ function buildGeminiId(version, flavor, preview = false) {
58
+ const normalizedFlavor = flavor.toLowerCase().replace(/\s+/g, '-');
59
+ return `gemini-${version}-${normalizedFlavor}${preview ? '-preview' : ''}`;
60
+ }
61
+ export function parseCursorModels(raw) {
62
+ const models = [];
63
+ const clean = stripAnsi(raw);
64
+ for (const line of clean.split('\n')) {
65
+ const match = line.trim().match(/^(\S+)\s+-\s+(.+?)(?:\s+\(([^)]+)\))?$/);
66
+ if (!match)
67
+ continue;
68
+ const [, id, name, flags] = match;
69
+ const tags = flags?.split(',').map((flag) => flag.trim().toLowerCase()) ?? [];
70
+ models.push({
71
+ id,
72
+ name: name.trim() || prettifyCursorModel(id),
73
+ description: flags?.trim(),
74
+ isDefault: tags.includes('default') || tags.includes('current'),
75
+ });
76
+ }
77
+ return uniqueModels(models);
78
+ }
79
+ export function parseOpenClawModels(raw) {
80
+ try {
81
+ const data = JSON.parse(raw);
82
+ const models = (data.models ?? [])
83
+ .filter((model) => model.available)
84
+ .map((model) => ({
85
+ id: model.key,
86
+ name: model.name?.trim() || model.key,
87
+ provider: model.key.split('/')[0],
88
+ description: model.tags?.join(', '),
89
+ isDefault: model.tags?.includes('default') ?? false,
90
+ }));
91
+ return uniqueModels(models);
92
+ }
93
+ catch {
94
+ return [];
95
+ }
96
+ }
97
+ export function parseClaudeModels(raw) {
98
+ const rawClean = stripAnsi(raw).replace(/\s+/g, ' ');
99
+ const patterns = [
100
+ [/\d+\.Default\(recommended\).*?(?:Sonnet|currently\s*Sonnet)\s*([\d.]+)/i, 'default', 'Default (recommended)'],
101
+ [/\d+\.Sonnet.*?Sonnet\s*([\d.]+)/i, 'sonnet', 'Sonnet'],
102
+ [/\d+\.Opus.*?Opus\s*([\d.]+)/i, 'opus', 'Opus'],
103
+ [/\d+\.Haiku.*?Haiku\s*([\d.]+)/i, 'haiku', 'Haiku'],
104
+ ];
105
+ const models = [];
106
+ for (const [pattern, id, name] of patterns) {
107
+ const match = rawClean.match(pattern);
108
+ if (match) {
109
+ models.push({
110
+ id,
111
+ name,
112
+ description: `v${match[1]}`,
113
+ provider: 'anthropic',
114
+ isDefault: id === 'default',
115
+ });
116
+ }
117
+ }
118
+ if (models.length > 0)
119
+ return uniqueModels(models);
120
+ const fallback = [];
121
+ const defaultMatch = rawClean.match(/currently\s*(Sonnet|Opus|Haiku)\s*([34](?:\.\d)?)/i);
122
+ const aliasMap = { sonnet: 'sonnet', opus: 'opus', haiku: 'haiku' };
123
+ for (const match of rawClean.matchAll(/\b(Sonnet|Opus|Haiku)\s*([34](?:\.\d)?)\b/gi)) {
124
+ const family = (match[1] ?? '').toLowerCase();
125
+ const alias = aliasMap[family];
126
+ if (!alias)
127
+ continue;
128
+ const version = match[2] ?? '';
129
+ const isDefault = defaultMatch &&
130
+ defaultMatch[1]?.toLowerCase() === family &&
131
+ defaultMatch[2] === version;
132
+ fallback.push({
133
+ id: alias,
134
+ name: `${titleCaseSegment(family)} ${version}`,
135
+ provider: 'anthropic',
136
+ isDefault: Boolean(isDefault),
137
+ });
138
+ }
139
+ return uniqueModels(fallback);
140
+ }
141
+ function compareVersionNumbers(a, b) {
142
+ const length = Math.max(a.length, b.length);
143
+ for (let index = 0; index < length; index++) {
144
+ const left = a[index] ?? -1;
145
+ const right = b[index] ?? -1;
146
+ if (left === right)
147
+ continue;
148
+ return left - right;
149
+ }
150
+ return 0;
151
+ }
152
+ function extractClaudeFamilyVersion(id, family) {
153
+ const match = id.match(new RegExp(`^claude-${family}-([0-9]+(?:-[0-9]+)*)$`, 'i'));
154
+ if (!match?.[1])
155
+ return null;
156
+ const parts = match[1]
157
+ .split('-')
158
+ .map((part) => Number.parseInt(part, 10))
159
+ .filter((part) => Number.isFinite(part));
160
+ if (parts.length === 0 || parts.length > 2)
161
+ return null;
162
+ return parts;
163
+ }
164
+ function formatClaudeVersion(version) {
165
+ if (!version?.length)
166
+ return undefined;
167
+ return `v${version.join('.')}`;
168
+ }
169
+ export function parseClaudeBinaryModels(raw) {
170
+ const clean = stripAnsi(raw);
171
+ const aliasMap = {
172
+ default: 'Default (recommended)',
173
+ sonnet: 'Sonnet',
174
+ opus: 'Opus',
175
+ haiku: 'Haiku',
176
+ };
177
+ const familyVersions = new Map();
178
+ for (const family of ['sonnet', 'opus', 'haiku']) {
179
+ for (const match of clean.matchAll(new RegExp(`\\bclaude-${family}-[0-9.-]+\\b`, 'gi'))) {
180
+ const normalized = match[0].trim().toLowerCase().replace(/\./g, '-').replace(/-+$/g, '');
181
+ const version = extractClaudeFamilyVersion(normalized, family);
182
+ if (!version)
183
+ continue;
184
+ const current = familyVersions.get(family);
185
+ if (!current || compareVersionNumbers(current, version) < 0) {
186
+ familyVersions.set(family, version);
187
+ }
188
+ }
189
+ }
190
+ const models = [];
191
+ for (const alias of ['default', 'sonnet', 'opus', 'haiku']) {
192
+ if (!new RegExp(`\\b${alias}\\b`, 'i').test(clean))
193
+ continue;
194
+ const version = (alias === 'default' ? familyVersions.get('sonnet') : familyVersions.get(alias)) ?? null;
195
+ models.push({
196
+ id: alias,
197
+ name: aliasMap[alias],
198
+ description: formatClaudeVersion(version),
199
+ provider: 'anthropic',
200
+ isDefault: alias === 'default',
201
+ });
202
+ }
203
+ return uniqueModels(models);
204
+ }
205
+ export function fallbackClaudeAliasModels() {
206
+ return [
207
+ { id: 'default', name: 'Default (recommended)', provider: 'anthropic', isDefault: true },
208
+ { id: 'sonnet', name: 'Sonnet', provider: 'anthropic' },
209
+ { id: 'opus', name: 'Opus', provider: 'anthropic' },
210
+ { id: 'haiku', name: 'Haiku', provider: 'anthropic' },
211
+ ];
212
+ }
213
+ export function parseCodexModels(raw) {
214
+ const clean = stripAnsi(raw);
215
+ const models = [];
216
+ for (const line of clean.split('\n')) {
217
+ const match = line.match(/\b(gpt-[a-z0-9][a-z0-9.-]*)\b/i);
218
+ if (!match)
219
+ continue;
220
+ const id = match[1].trim();
221
+ const description = line.replace(id, '').replace(/^[^\w]+/, '').trim() || undefined;
222
+ models.push({
223
+ id,
224
+ name: prettifyCodexModel(id),
225
+ description,
226
+ provider: 'openai',
227
+ isDefault: /\bcurrent\b/i.test(line) || /\bselected\b/i.test(line),
228
+ });
229
+ }
230
+ return uniqueModels(models);
231
+ }
232
+ export function parseCodexAppServerModels(raw) {
233
+ const data = typeof raw === 'object' && raw !== null
234
+ ? raw
235
+ : {};
236
+ const currentModel = typeof data.config?.model === 'string' ? data.config.model : undefined;
237
+ const currentReasoningEffort = typeof data.config?.model_reasoning_effort === 'string'
238
+ ? data.config.model_reasoning_effort
239
+ : undefined;
240
+ const models = [];
241
+ for (const entry of data.data ?? []) {
242
+ if (typeof entry !== 'object' || entry === null)
243
+ continue;
244
+ const item = entry;
245
+ const id = typeof item.id === 'string'
246
+ ? item.id
247
+ : typeof item.model === 'string'
248
+ ? item.model
249
+ : '';
250
+ if (!id)
251
+ continue;
252
+ const displayName = typeof item.displayName === 'string'
253
+ ? item.displayName
254
+ : typeof item.name === 'string'
255
+ ? item.name
256
+ : undefined;
257
+ const description = typeof item.description === 'string' ? item.description : undefined;
258
+ const defaultReasoningEffort = typeof item.defaultReasoningEffort === 'string'
259
+ ? item.defaultReasoningEffort
260
+ : undefined;
261
+ const supportedReasoningEfforts = Array.isArray(item.supportedReasoningEfforts)
262
+ ? item.supportedReasoningEfforts
263
+ .map((effort) => {
264
+ if (typeof effort !== 'object' || effort === null)
265
+ return null;
266
+ const rawEffort = effort;
267
+ const effortId = typeof rawEffort.reasoningEffort === 'string'
268
+ ? rawEffort.reasoningEffort
269
+ : typeof rawEffort.id === 'string'
270
+ ? rawEffort.id
271
+ : '';
272
+ if (!effortId)
273
+ return null;
274
+ return {
275
+ id: effortId,
276
+ name: prettifyReasoningEffort(effortId),
277
+ description: typeof rawEffort.description === 'string' ? rawEffort.description : undefined,
278
+ };
279
+ })
280
+ .filter((effort) => effort != null)
281
+ : undefined;
282
+ const additionalSpeedTiers = Array.isArray(item.additionalSpeedTiers)
283
+ ? item.additionalSpeedTiers.filter((tier) => typeof tier === 'string')
284
+ : undefined;
285
+ models.push({
286
+ id,
287
+ name: displayName || prettifyCodexModel(id),
288
+ description,
289
+ provider: 'openai',
290
+ isDefault: currentModel ? currentModel === id : item.isDefault === true,
291
+ supportedReasoningEfforts,
292
+ defaultReasoningEffort,
293
+ additionalSpeedTiers,
294
+ });
295
+ }
296
+ if (currentModel && !models.some((model) => model.id === currentModel)) {
297
+ models.unshift({
298
+ id: currentModel,
299
+ name: prettifyCodexModel(currentModel),
300
+ provider: 'openai',
301
+ isDefault: true,
302
+ defaultReasoningEffort: currentReasoningEffort,
303
+ });
304
+ }
305
+ return uniqueModels(models);
306
+ }
307
+ export function parseGeminiModels(raw) {
308
+ const clean = stripAnsi(raw);
309
+ const models = [];
310
+ for (const match of clean.matchAll(/\b(?:auto-)?gemini-[a-z0-9.-]+\b/gi)) {
311
+ const id = match[0].trim();
312
+ models.push({
313
+ id,
314
+ name: id,
315
+ provider: 'google',
316
+ isDefault: /\bcurrent\b/i.test(clean) && clean.includes(id),
317
+ });
318
+ }
319
+ for (const match of clean.matchAll(/\bGemini\s*(\d(?:\.\d)?)\s*(Pro|Flash(?:[- ]Lite)?)\s*(Preview)?\b/gi)) {
320
+ const version = match[1] ?? '';
321
+ const flavor = (match[2] ?? '').replace(/\s+/g, '-');
322
+ const preview = Boolean(match[3]);
323
+ const id = buildGeminiId(version, flavor, preview);
324
+ models.push({
325
+ id,
326
+ name: `Gemini ${version} ${flavor.replace(/-/g, ' ')}`.trim(),
327
+ provider: 'google',
328
+ isDefault: /\bcurrent\b/i.test(clean) && clean.includes(match[0] ?? ''),
329
+ });
330
+ }
331
+ for (const match of clean.matchAll(/\bAuto\s*Gemini\s*(\d(?:\.\d)?)\b/gi)) {
332
+ const version = match[1] ?? '';
333
+ const id = `auto-gemini-${version}`;
334
+ models.push({
335
+ id,
336
+ name: `Auto Gemini ${version}`,
337
+ provider: 'google',
338
+ isDefault: /\bcurrent\b/i.test(clean) && clean.includes(match[0] ?? ''),
339
+ });
340
+ }
341
+ return uniqueModels(models);
342
+ }
343
+ export function fallbackGeminiModels() {
344
+ return [
345
+ { id: 'gemini-2.5-pro', name: 'Gemini 2.5 Pro', provider: 'google', isDefault: true },
346
+ { id: 'gemini-2.5-flash', name: 'Gemini 2.5 Flash', provider: 'google' },
347
+ { id: 'auto-gemini-2.5', name: 'Auto Gemini 2.5', provider: 'google' },
348
+ ];
349
+ }
@@ -0,0 +1,6 @@
1
+ import type { ResolvedCommandSpec } from '../command-spec.js';
2
+ export declare function runResolvedCommand(spec: ResolvedCommandSpec, args: string[], timeoutMs?: number): Promise<{
3
+ stdout: string;
4
+ stderr: string;
5
+ exitCode: number | null;
6
+ }>;
@@ -0,0 +1,29 @@
1
+ // @arch docs/architecture/cli/model-discovery.md
2
+ // @test src/__tests__/model-switching.test.ts
3
+ import { spawnResolvedCommand } from '../command-spec.js';
4
+ import { DISCOVERY_WORKDIR } from './types.js';
5
+ export function runResolvedCommand(spec, args, timeoutMs = 15_000) {
6
+ return new Promise((resolve, reject) => {
7
+ const proc = spawnResolvedCommand(spec, args, {
8
+ cwd: DISCOVERY_WORKDIR,
9
+ stdio: ['ignore', 'pipe', 'pipe'],
10
+ env: { ...process.env, NO_COLOR: '1' },
11
+ });
12
+ let stdout = '';
13
+ let stderr = '';
14
+ const timer = setTimeout(() => {
15
+ proc.kill('SIGKILL');
16
+ }, timeoutMs);
17
+ proc.stdout?.on('data', (chunk) => {
18
+ stdout += chunk.toString();
19
+ });
20
+ proc.stderr?.on('data', (chunk) => {
21
+ stderr += chunk.toString();
22
+ });
23
+ proc.on('error', reject);
24
+ proc.on('close', (exitCode) => {
25
+ clearTimeout(timer);
26
+ resolve({ stdout, stderr, exitCode });
27
+ });
28
+ });
29
+ }
@@ -0,0 +1,6 @@
1
+ import type { AgentInfo } from '@shennian/wire';
2
+ import type { DetectedAgent } from '../detect.js';
3
+ import type { DiscoveryContext } from './types.js';
4
+ export declare function getCachedAgentInfos(detectedAgents: DetectedAgent[]): AgentInfo[];
5
+ export declare function refreshAgentInfos(detectedAgents: DetectedAgent[], context: DiscoveryContext): Promise<AgentInfo[]>;
6
+ export declare function resolveAgentInfos(detectedAgents: DetectedAgent[], context: DiscoveryContext): Promise<AgentInfo[]>;
@@ -0,0 +1,71 @@
1
+ // @arch docs/architecture/cli/model-discovery.md
2
+ // @test src/__tests__/model-switching.test.ts
3
+ import { isCacheFresh, readCache, writeCache } from './cache.js';
4
+ import { discoverModelsForAgent } from './discovery.js';
5
+ function uniqueModels(models) {
6
+ const seen = new Set();
7
+ const unique = [];
8
+ for (const model of models) {
9
+ if (!model.id || seen.has(model.id))
10
+ continue;
11
+ seen.add(model.id);
12
+ unique.push(model);
13
+ }
14
+ return unique;
15
+ }
16
+ export function getCachedAgentInfos(detectedAgents) {
17
+ const cache = readCache();
18
+ return detectedAgents.map((agent) => ({
19
+ type: agent.type,
20
+ models: cache.agents[agent.type]?.models ?? agent.models ?? [],
21
+ }));
22
+ }
23
+ export async function refreshAgentInfos(detectedAgents, context) {
24
+ const cache = readCache();
25
+ for (const agent of detectedAgents) {
26
+ delete cache.agents[agent.type];
27
+ }
28
+ writeCache(cache);
29
+ return resolveAgentInfos(detectedAgents, context);
30
+ }
31
+ export async function resolveAgentInfos(detectedAgents, context) {
32
+ const cache = readCache();
33
+ let changed = false;
34
+ const infos = [];
35
+ for (const agent of detectedAgents) {
36
+ const cached = cache.agents[agent.type];
37
+ let models;
38
+ if (isCacheFresh(cached, agent.version)) {
39
+ models = cached.models;
40
+ }
41
+ else {
42
+ let discovered = null;
43
+ try {
44
+ discovered = uniqueModels(await discoverModelsForAgent(agent, context));
45
+ models = discovered.length > 0 ? discovered : cached?.models ?? agent.models ?? [];
46
+ }
47
+ catch {
48
+ models = cached?.models ?? agent.models ?? [];
49
+ }
50
+ if (discovered && discovered.length > 0) {
51
+ const nextEntry = {
52
+ version: agent.version,
53
+ refreshedAt: Date.now(),
54
+ models,
55
+ };
56
+ if (JSON.stringify(cached) !== JSON.stringify(nextEntry)) {
57
+ cache.agents[agent.type] = nextEntry;
58
+ changed = true;
59
+ }
60
+ }
61
+ }
62
+ infos.push({
63
+ type: agent.type,
64
+ models,
65
+ });
66
+ }
67
+ if (changed) {
68
+ writeCache(cache);
69
+ }
70
+ return infos;
71
+ }
@@ -0,0 +1,30 @@
1
+ import type { ModelInfo } from '@shennian/wire';
2
+ export declare const CACHE_VERSION = 1;
3
+ export declare const CACHE_TTL_MS: number;
4
+ export declare const MODEL_CACHE_PATH: string;
5
+ export declare const DISCOVERY_WORKDIR: string;
6
+ export type CacheEntry = {
7
+ version?: string;
8
+ refreshedAt: number;
9
+ models: ModelInfo[];
10
+ };
11
+ export type CacheFile = {
12
+ version: number;
13
+ agents: Record<string, CacheEntry>;
14
+ };
15
+ export type DiscoveryContext = {
16
+ serverUrl?: string;
17
+ authToken?: string;
18
+ };
19
+ export type PtyOptions = {
20
+ startupArgs?: string[];
21
+ readyPatterns: RegExp[];
22
+ closePatterns: RegExp[];
23
+ preOpenPromptPatterns?: RegExp[];
24
+ preOpenPromptInput?: string;
25
+ initialDelayMs?: number;
26
+ timeoutMs?: number;
27
+ scrollAfterOpenMs?: number;
28
+ scrollCount?: number;
29
+ scrollIntervalMs?: number;
30
+ };
@@ -0,0 +1,8 @@
1
+ // @arch docs/architecture/cli/model-discovery.md
2
+ // @test src/__tests__/model-switching.test.ts
3
+ import os from 'node:os';
4
+ import { resolveShennianPath } from '../../config/index.js';
5
+ export const CACHE_VERSION = 1;
6
+ export const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
7
+ export const MODEL_CACHE_PATH = resolveShennianPath('model-cache.json');
8
+ export const DISCOVERY_WORKDIR = os.tmpdir();
@@ -0,0 +1,13 @@
1
+ export { getCachedAgentInfos, refreshAgentInfos, resolveAgentInfos } from './model-registry/service.js';
2
+ import { parseClaudeBinaryModels, parseClaudeModels, parseCodexModels, parseCodexAppServerModels, parseCursorModels, parseGeminiModels, parseOpenClawModels, stripAnsi } from './model-registry/parsers.js';
3
+ /** Used by Vitest / `model-switching-e2e.ts` — not a stable public API. */
4
+ export declare const modelSwitchingTestExports: {
5
+ readonly parseClaudeBinaryModels: typeof parseClaudeBinaryModels;
6
+ readonly parseClaudeModels: typeof parseClaudeModels;
7
+ readonly parseCodexModels: typeof parseCodexModels;
8
+ readonly parseCodexAppServerModels: typeof parseCodexAppServerModels;
9
+ readonly parseCursorModels: typeof parseCursorModels;
10
+ readonly parseOpenClawModels: typeof parseOpenClawModels;
11
+ readonly parseGeminiModels: typeof parseGeminiModels;
12
+ readonly stripAnsi: typeof stripAnsi;
13
+ };
@@ -0,0 +1,15 @@
1
+ // @arch docs/architecture/cli/model-discovery.md
2
+ // @test src/__tests__/model-switching.test.ts
3
+ export { getCachedAgentInfos, refreshAgentInfos, resolveAgentInfos } from './model-registry/service.js';
4
+ import { parseClaudeBinaryModels, parseClaudeModels, parseCodexModels, parseCodexAppServerModels, parseCursorModels, parseGeminiModels, parseOpenClawModels, stripAnsi, } from './model-registry/parsers.js';
5
+ /** Used by Vitest / `model-switching-e2e.ts` — not a stable public API. */
6
+ export const modelSwitchingTestExports = {
7
+ parseClaudeBinaryModels,
8
+ parseClaudeModels,
9
+ parseCodexModels,
10
+ parseCodexAppServerModels,
11
+ parseCursorModels,
12
+ parseOpenClawModels,
13
+ parseGeminiModels,
14
+ stripAnsi,
15
+ };
@@ -0,0 +1,20 @@
1
+ import { AgentAdapter } from './adapter.js';
2
+ export declare class OpenClawAdapter extends AgentAdapter {
3
+ readonly type: "openclaw";
4
+ private process;
5
+ private sessionId;
6
+ /** OpenClaw session id from JSON or `resume()`; falls back to app `sessionId`. */
7
+ private agentSessionId;
8
+ private currentModelId;
9
+ private workDir;
10
+ private seq;
11
+ private runId;
12
+ start(sessionId: string, workDir: string, agentSessionId?: string | null): Promise<void>;
13
+ send(text: string, modelId?: string): Promise<void>;
14
+ resume(agentSessionId: string): Promise<void>;
15
+ stop(): Promise<void>;
16
+ private spawnAgent;
17
+ private runAgentCommand;
18
+ private emitEvent;
19
+ private killProcess;
20
+ }