shennian 0.2.54 → 0.2.56

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.
@@ -1,5 +1,5 @@
1
1
  import { EventEmitter } from 'node:events';
2
- import type { AgentType, ExternalChannelSessionStatus } from '@shennian/wire';
2
+ import type { AgentType, ChatAttachmentMeta, ExternalChannelSessionStatus } from '@shennian/wire';
3
3
  export type AgentEvent = {
4
4
  state: string;
5
5
  runId: string;
@@ -30,7 +30,7 @@ export declare abstract class AgentAdapter extends EventEmitter<AgentAdapterEven
30
30
  env?: NodeJS.ProcessEnv;
31
31
  }): void;
32
32
  abstract start(sessionId: string, workDir: string, agentSessionId?: string | null): Promise<void>;
33
- abstract send(text: string, modelId?: string, reasoningEffort?: string): Promise<void>;
33
+ abstract send(text: string, modelId?: string, reasoningEffort?: string, attachments?: ChatAttachmentMeta[]): Promise<void>;
34
34
  abstract resume(agentSessionId: string): Promise<void>;
35
35
  abstract stop(): Promise<void>;
36
36
  }
@@ -2,6 +2,8 @@ import { AgentAdapter } from './adapter.js';
2
2
  import type { ExternalChannelSessionStatus } from '@shennian/wire';
3
3
  export declare function normalizeClaudeModelId(modelId?: string | null): string;
4
4
  export declare function normalizeClaudeReasoningEffort(reasoningEffort?: string | null): string | undefined;
5
+ export declare function resetClaudeCapabilityCacheForTests(): void;
6
+ export declare function supportsClaudeReasoningEffort(): boolean;
5
7
  export declare class ClaudeAdapter extends AgentAdapter {
6
8
  private readonly options;
7
9
  readonly type: "claude";
@@ -1,7 +1,7 @@
1
1
  import { createInterface } from 'node:readline';
2
2
  import { randomUUID } from 'node:crypto';
3
3
  import { AgentAdapter, registerAgent } from './adapter.js';
4
- import { resolveBuiltinCommand, spawnResolvedCommand } from './command-spec.js';
4
+ import { resolveBuiltinCommand, spawnResolvedCommand, spawnResolvedCommandSync } from './command-spec.js';
5
5
  import { buildAgentProcessEnv } from '../agent-env.js';
6
6
  import { buildPlatformInstructions } from './platform-instructions.js';
7
7
  export function normalizeClaudeModelId(modelId) {
@@ -9,6 +9,7 @@ export function normalizeClaudeModelId(modelId) {
9
9
  return trimmed || 'default';
10
10
  }
11
11
  const CLAUDE_REASONING_EFFORTS = new Set(['low', 'medium', 'high', 'xhigh', 'max']);
12
+ const claudeEffortSupportCache = new Map();
12
13
  export function normalizeClaudeReasoningEffort(reasoningEffort) {
13
14
  const trimmed = reasoningEffort?.trim();
14
15
  if (!trimmed)
@@ -17,6 +18,34 @@ export function normalizeClaudeReasoningEffort(reasoningEffort) {
17
18
  return trimmed;
18
19
  throw new Error(`Unsupported Claude reasoning effort "${trimmed}". Supported values: low, medium, high, xhigh, max.`);
19
20
  }
21
+ export function resetClaudeCapabilityCacheForTests() {
22
+ claudeEffortSupportCache.clear();
23
+ }
24
+ export function supportsClaudeReasoningEffort() {
25
+ const spec = resolveBuiltinCommand('claude');
26
+ if (!spec)
27
+ return false;
28
+ const cacheKey = `${spec.kind}:${spec.path}:${spec.command}:${spec.args.join('\0')}`;
29
+ if (claudeEffortSupportCache.has(cacheKey)) {
30
+ return claudeEffortSupportCache.get(cacheKey) ?? false;
31
+ }
32
+ try {
33
+ const result = spawnResolvedCommandSync(spec, ['--help'], {
34
+ encoding: 'utf-8',
35
+ stdio: ['ignore', 'pipe', 'pipe'],
36
+ timeout: 3000,
37
+ env: buildAgentProcessEnv(),
38
+ });
39
+ const output = `${result.stdout ?? ''}\n${result.stderr ?? ''}`;
40
+ const supported = /(?:^|\s)--effort(?:\s|,|<)/.test(output);
41
+ claudeEffortSupportCache.set(cacheKey, supported);
42
+ return supported;
43
+ }
44
+ catch {
45
+ claudeEffortSupportCache.set(cacheKey, false);
46
+ return false;
47
+ }
48
+ }
20
49
  export class ClaudeAdapter extends AgentAdapter {
21
50
  options;
22
51
  type = 'claude';
@@ -64,7 +93,7 @@ export class ClaudeAdapter extends AgentAdapter {
64
93
  }
65
94
  args.push('--model', normalizeClaudeModelId(modelId));
66
95
  const effort = normalizeClaudeReasoningEffort(reasoningEffort);
67
- if (effort) {
96
+ if (effort && supportsClaudeReasoningEffort()) {
68
97
  args.push('--effort', effort);
69
98
  }
70
99
  if (this.agentSessionId) {
@@ -1,5 +1,5 @@
1
1
  import { AgentAdapter } from './adapter.js';
2
- import type { ExternalChannelSessionStatus } from '@shennian/wire';
2
+ import type { ChatAttachmentMeta, ExternalChannelSessionStatus } from '@shennian/wire';
3
3
  export declare class CodexAdapter extends AgentAdapter {
4
4
  private readonly options;
5
5
  readonly type: "codex";
@@ -35,7 +35,7 @@ export declare class CodexAdapter extends AgentAdapter {
35
35
  env?: NodeJS.ProcessEnv;
36
36
  }): void;
37
37
  start(_sessionId: string, workDir: string, agentSessionId?: string | null): Promise<void>;
38
- send(text: string, modelId?: string, reasoningEffort?: string): Promise<void>;
38
+ send(text: string, modelId?: string, reasoningEffort?: string, attachments?: ChatAttachmentMeta[]): Promise<void>;
39
39
  resume(agentSessionId: string): Promise<void>;
40
40
  stop(): Promise<void>;
41
41
  private spawnCodex;
@@ -6,6 +6,28 @@ import { AgentAdapter, registerAgent } from './adapter.js';
6
6
  import { resolveBuiltinCommand, spawnResolvedCommand } from './command-spec.js';
7
7
  import { buildAgentProcessEnv } from '../agent-env.js';
8
8
  import { ensurePlatformInstructionsFile } from './platform-instructions.js';
9
+ function buildCodexTextInput(text, attachments) {
10
+ const images = attachments
11
+ ?.filter((attachment) => attachment.kind === 'image')
12
+ .filter((attachment) => !/^https?:\/\//i.test(attachment.path))
13
+ .map((attachment) => {
14
+ if (attachment.previewData) {
15
+ return {
16
+ path: attachment.path,
17
+ data: attachment.previewData,
18
+ mimeType: attachment.mimeType,
19
+ };
20
+ }
21
+ return { path: attachment.path };
22
+ })
23
+ .filter((attachment) => attachment.path);
24
+ return {
25
+ type: 'text',
26
+ text,
27
+ text_elements: [],
28
+ ...(images?.length ? { local_images: images } : {}),
29
+ };
30
+ }
9
31
  export class CodexAdapter extends AgentAdapter {
10
32
  options;
11
33
  type = 'codex';
@@ -46,7 +68,7 @@ export class CodexAdapter extends AgentAdapter {
46
68
  if (agentSessionId)
47
69
  this.agentSessionId = agentSessionId;
48
70
  }
49
- async send(text, modelId, reasoningEffort) {
71
+ async send(text, modelId, reasoningEffort, attachments) {
50
72
  if (this.activeTurnId) {
51
73
  await this.interruptActiveTurn().catch(() => { });
52
74
  await this.killProcess();
@@ -68,7 +90,7 @@ export class CodexAdapter extends AgentAdapter {
68
90
  }).catch(() => { });
69
91
  this.namedThread = true;
70
92
  }
71
- const response = await this.startTurnWithRecovery(threadId, text, codexModelId, codexReasoningEffort);
93
+ const response = await this.startTurnWithRecovery(threadId, text, codexModelId, codexReasoningEffort, attachments);
72
94
  this.activeTurnId = response.turn?.id ?? null;
73
95
  }
74
96
  async resume(agentSessionId) {
@@ -236,23 +258,23 @@ export class CodexAdapter extends AgentAdapter {
236
258
  this.agentSessionId = threadId;
237
259
  this.namedThread = !!response.thread?.name;
238
260
  }
239
- async startTurnWithRecovery(threadId, text, codexModelId, reasoningEffort) {
261
+ async startTurnWithRecovery(threadId, text, codexModelId, reasoningEffort, attachments) {
240
262
  try {
241
- return await this.startTurn(threadId, text, codexModelId, reasoningEffort);
263
+ return await this.startTurn(threadId, text, codexModelId, reasoningEffort, attachments);
242
264
  }
243
265
  catch (error) {
244
266
  if (!isMissingCodexRolloutError(error))
245
267
  throw error;
246
268
  await this.killProcess();
247
269
  await this.ensureAppServer(codexModelId);
248
- return await this.startTurn(threadId, text, codexModelId, reasoningEffort);
270
+ return await this.startTurn(threadId, text, codexModelId, reasoningEffort, attachments);
249
271
  }
250
272
  }
251
- async startTurn(threadId, text, codexModelId, reasoningEffort) {
273
+ async startTurn(threadId, text, codexModelId, reasoningEffort, attachments) {
252
274
  try {
253
275
  return await this.sendRpc('turn/start', {
254
276
  threadId,
255
- input: [{ type: 'text', text, text_elements: [] }],
277
+ input: [buildCodexTextInput(text, attachments)],
256
278
  approvalPolicy: 'never',
257
279
  sandboxPolicy: { type: 'dangerFullAccess' },
258
280
  ...(codexModelId ? { model: codexModelId } : {}),
@@ -0,0 +1,17 @@
1
+ import type { AgentConfigSummary, AgentType } from '@shennian/wire';
2
+ export type AgentProviderConfigRecord = {
3
+ agent: 'codex' | 'claude';
4
+ baseUrl?: string;
5
+ token?: string;
6
+ updatedAt: string;
7
+ };
8
+ export declare function getManagedAgentProviderConfig(agent: 'codex' | 'claude'): AgentProviderConfigRecord | undefined;
9
+ export declare function upsertManagedAgentProviderConfig(input: {
10
+ agent: 'codex' | 'claude';
11
+ baseUrl?: string;
12
+ token?: string;
13
+ }): AgentProviderConfigRecord;
14
+ export declare function deleteManagedAgentProviderConfig(agent: 'codex' | 'claude'): void;
15
+ export declare function buildManagedAgentEnv(agent: AgentType): NodeJS.ProcessEnv;
16
+ export declare function getAgentConfigSummary(agent: AgentType, env?: NodeJS.ProcessEnv): AgentConfigSummary | undefined;
17
+ export declare function maskToken(token: string): string;
@@ -0,0 +1,205 @@
1
+ // @arch docs/features/agent-provider-config.md
2
+ // @test src/__tests__/agent-config-status.test.ts
3
+ import fs from 'node:fs';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import { readLatestUserEnv } from '../agent-env.js';
7
+ import { resolveShennianPath } from '../config/index.js';
8
+ const MANAGED_CONFIG_PATH = resolveShennianPath('agent-provider-config.json');
9
+ const TOKEN_SUFFIX_LENGTH = 4;
10
+ const AGENT_ENV_KEYS = {
11
+ codex: {
12
+ baseUrl: ['OPENAI_BASE_URL', 'OPENAI_API_BASE', 'OPENAI_API_URL'],
13
+ token: ['OPENAI_API_KEY', 'OPENAI_TOKEN'],
14
+ },
15
+ claude: {
16
+ baseUrl: ['ANTHROPIC_BASE_URL', 'ANTHROPIC_API_URL'],
17
+ token: ['ANTHROPIC_API_KEY', 'ANTHROPIC_AUTH_TOKEN'],
18
+ },
19
+ };
20
+ export function getManagedAgentProviderConfig(agent) {
21
+ return loadManagedConfig().configs[agent];
22
+ }
23
+ export function upsertManagedAgentProviderConfig(input) {
24
+ const file = loadManagedConfig();
25
+ const existing = file.configs[input.agent];
26
+ const record = {
27
+ agent: input.agent,
28
+ baseUrl: input.baseUrl?.trim() || existing?.baseUrl,
29
+ token: input.token?.trim() || existing?.token,
30
+ updatedAt: new Date().toISOString(),
31
+ };
32
+ file.configs[input.agent] = record;
33
+ saveManagedConfig(file);
34
+ return record;
35
+ }
36
+ export function deleteManagedAgentProviderConfig(agent) {
37
+ const file = loadManagedConfig();
38
+ delete file.configs[agent];
39
+ saveManagedConfig(file);
40
+ }
41
+ export function buildManagedAgentEnv(agent) {
42
+ if (agent !== 'codex' && agent !== 'claude')
43
+ return {};
44
+ const config = getManagedAgentProviderConfig(agent);
45
+ if (!config)
46
+ return {};
47
+ if (agent === 'codex') {
48
+ return compactEnv({
49
+ OPENAI_BASE_URL: config.baseUrl,
50
+ OPENAI_API_KEY: config.token,
51
+ });
52
+ }
53
+ return compactEnv({
54
+ ANTHROPIC_BASE_URL: config.baseUrl,
55
+ ANTHROPIC_API_KEY: config.token,
56
+ });
57
+ }
58
+ export function getAgentConfigSummary(agent, env = readLatestUserEnv()) {
59
+ if (agent !== 'codex' && agent !== 'claude')
60
+ return undefined;
61
+ const managed = getManagedAgentProviderConfig(agent);
62
+ if (managed?.token || managed?.baseUrl) {
63
+ return toSummary({
64
+ agent,
65
+ source: 'shennian',
66
+ status: managed.token ? 'managed' : 'missing',
67
+ baseUrl: managed.baseUrl,
68
+ token: managed.token,
69
+ tokenPresent: !!managed.token,
70
+ });
71
+ }
72
+ const envConfig = detectFromEnv(agent, env);
73
+ if (envConfig.tokenPresent || envConfig.baseUrl) {
74
+ return toSummary({
75
+ agent,
76
+ source: 'env',
77
+ status: envConfig.tokenPresent ? 'detected' : 'missing',
78
+ baseUrl: envConfig.baseUrl,
79
+ token: envConfig.token,
80
+ tokenPresent: envConfig.tokenPresent,
81
+ });
82
+ }
83
+ const fileConfig = detectFromKnownConfigFiles(agent);
84
+ if (fileConfig.tokenPresent || fileConfig.baseUrl) {
85
+ return toSummary({
86
+ agent,
87
+ source: 'config-file',
88
+ status: fileConfig.tokenPresent ? 'detected' : 'missing',
89
+ baseUrl: fileConfig.baseUrl,
90
+ token: fileConfig.token,
91
+ tokenPresent: fileConfig.tokenPresent,
92
+ });
93
+ }
94
+ return toSummary({
95
+ agent,
96
+ source: 'unknown',
97
+ status: 'missing',
98
+ tokenPresent: false,
99
+ });
100
+ }
101
+ function loadManagedConfig() {
102
+ try {
103
+ const parsed = JSON.parse(fs.readFileSync(MANAGED_CONFIG_PATH, 'utf-8'));
104
+ return { configs: parsed.configs ?? {} };
105
+ }
106
+ catch {
107
+ return { configs: {} };
108
+ }
109
+ }
110
+ function saveManagedConfig(file) {
111
+ fs.mkdirSync(path.dirname(MANAGED_CONFIG_PATH), { recursive: true });
112
+ fs.writeFileSync(MANAGED_CONFIG_PATH, JSON.stringify(file, null, 2), { mode: 0o600 });
113
+ try {
114
+ fs.chmodSync(MANAGED_CONFIG_PATH, 0o600);
115
+ }
116
+ catch {
117
+ // Best effort on filesystems without POSIX modes.
118
+ }
119
+ }
120
+ function compactEnv(env) {
121
+ const compacted = {};
122
+ for (const [key, value] of Object.entries(env)) {
123
+ if (value)
124
+ compacted[key] = value;
125
+ }
126
+ return compacted;
127
+ }
128
+ function detectFromEnv(agent, env) {
129
+ const keys = AGENT_ENV_KEYS[agent];
130
+ const baseUrl = firstEnv(env, keys.baseUrl);
131
+ const token = firstEnv(env, keys.token);
132
+ return { baseUrl, token, tokenPresent: !!token };
133
+ }
134
+ function firstEnv(env, keys) {
135
+ for (const key of keys) {
136
+ const value = env[key]?.trim();
137
+ if (value)
138
+ return value;
139
+ }
140
+ return undefined;
141
+ }
142
+ function detectFromKnownConfigFiles(agent) {
143
+ const home = os.homedir();
144
+ const candidates = agent === 'codex'
145
+ ? [path.join(home, '.codex', 'config.toml'), path.join(home, '.codex', 'auth.json')]
146
+ : [path.join(home, '.claude', 'settings.json'), path.join(home, '.claude.json')];
147
+ for (const file of candidates) {
148
+ const text = readSmallTextFile(file);
149
+ if (!text)
150
+ continue;
151
+ const token = extractTokenLikeValue(text, agent);
152
+ const baseUrl = extractBaseUrl(text);
153
+ if (token || baseUrl)
154
+ return { token, baseUrl, tokenPresent: !!token };
155
+ }
156
+ return { tokenPresent: false };
157
+ }
158
+ function readSmallTextFile(file) {
159
+ try {
160
+ const stat = fs.statSync(file);
161
+ if (!stat.isFile() || stat.size > 1024 * 1024)
162
+ return null;
163
+ return fs.readFileSync(file, 'utf-8');
164
+ }
165
+ catch {
166
+ return null;
167
+ }
168
+ }
169
+ function extractBaseUrl(text) {
170
+ const match = text.match(/(?:base_url|baseURL|api_url|apiUrl|ANTHROPIC_BASE_URL|OPENAI_BASE_URL)["'\s:=]+([^"'\s,}]+)/i);
171
+ return match?.[1]?.trim();
172
+ }
173
+ function extractTokenLikeValue(text, agent) {
174
+ const prefix = agent === 'claude' ? 'sk-ant-' : 'sk-';
175
+ const direct = text.match(new RegExp(`${prefix.replace(/-/g, '\\-')}[A-Za-z0-9_\\-]{8,}`));
176
+ if (direct?.[0])
177
+ return direct[0];
178
+ const keyMatch = text.match(/(?:api[_-]?key|auth[_-]?token|token)["'\s:=]+([A-Za-z0-9_\-.]{12,})/i);
179
+ return keyMatch?.[1]?.trim();
180
+ }
181
+ function toSummary(input) {
182
+ return {
183
+ status: input.status,
184
+ source: input.source,
185
+ tokenPresent: input.tokenPresent,
186
+ ...(input.baseUrl ? { baseUrlHost: formatBaseUrlHost(input.baseUrl) } : {}),
187
+ ...(input.token ? { tokenHint: maskToken(input.token) } : {}),
188
+ updatedAt: new Date().toISOString(),
189
+ };
190
+ }
191
+ export function maskToken(token) {
192
+ const trimmed = token.trim();
193
+ if (!trimmed)
194
+ return '';
195
+ const suffix = trimmed.slice(-TOKEN_SUFFIX_LENGTH);
196
+ return `••••${suffix}`;
197
+ }
198
+ function formatBaseUrlHost(baseUrl) {
199
+ try {
200
+ return new URL(baseUrl).host || baseUrl;
201
+ }
202
+ catch {
203
+ return baseUrl.replace(/^https?:\/\//i, '').replace(/\/.*$/, '') || baseUrl;
204
+ }
205
+ }
@@ -6,23 +6,28 @@ export function buildExternalChannelInstructions(channel, workDir, sessionId) {
6
6
  const channelName = channel.name?.trim() || '外部消息通道';
7
7
  const customPrompt = channel.systemPrompt?.trim();
8
8
  const sessionHint = sessionId?.trim()
9
- ? `当前对话 ID:${sessionId}。如果命令提示缺少外部通道上下文,使用:shennian external send --session-id ${sessionId} --text "<要发送的消息>"`
9
+ ? `当前对话 ID:${sessionId}。如果命令提示缺少外部通道上下文,在发送命令里补充 --session-id ${sessionId}。`
10
10
  : '';
11
11
  const workdirHint = workDir?.trim()
12
12
  ? `如果使用 shell_command,请设置 workdir 为 ${workDir}。不要用裸 /bin/zsh -lc 或 command_execution 运行此命令,因为那种环境可能拿不到当前对话的外部通道身份。`
13
13
  : '如果使用 shell_command,请设置 workdir 为当前对话的项目目录。不要用裸 /bin/zsh -lc 或 command_execution 运行此命令,因为那种环境可能拿不到当前对话的外部通道身份。';
14
14
  const sections = [
15
15
  `当前对话已接入外部消息通道:${channelName}。`,
16
- `外部消息会以如下格式进入对话:\n外部消息\n<时间> <用户昵称>: <内容>`,
16
+ `外部企业微信群消息会以如下格式进入对话:\n外部企业微信群消息\n<时间> <用户昵称>: <内容>`,
17
17
  channel.canReply === false
18
18
  ? '当前通道只允许接收消息,不要尝试向外部通道发送回复。'
19
19
  : [
20
20
  '当用户明确要求你向外部消息通道发送内容,或你需要回复一条外部消息时,调用:',
21
21
  'shennian external send --text "<要发送的消息>"',
22
+ '发送图片:shennian external send-image --path "<图片绝对路径>" --caption "<可选说明>"',
23
+ '发送视频:shennian external send-video --path "<视频绝对路径>" --caption "<可选说明>"',
24
+ '发送文件:shennian external send-file --path "<文件绝对路径>" --caption "<可选说明>"',
25
+ '如果需要转发外部消息里的图片/视频/文件链接,先把链接下载到本地临时文件,再用对应的 --path 命令发送;不要直接发送短时效链接,也不要只口头说明已发送。',
22
26
  sessionHint,
23
27
  workdirHint,
24
28
  '只发送用户可见的最终内容,不要发送内部推理、工具日志或实现细节。',
25
- '对外消息必须像真人聊天:简短、概要、单段;不要使用 Markdown、编号列表、项目符号、真实换行或字面 \\n。',
29
+ '对外消息必须像真人聊天:短回复一条发完;内容较多时按自然段拆成 2-4 条连续消息,每条只讲一个完整主题。',
30
+ '避免把超过 300-500 字的内容塞进单条消息;不要使用 Markdown、编号列表、项目符号或字面 \\n。',
26
31
  ].join('\n'),
27
32
  '如果外部消息和当前任务无关,可以忽略或简短说明无需处理;如果处理需要时间,先用一句话确认,再继续完成任务。',
28
33
  customPrompt ? `本通道附加约束:${customPrompt}` : '',
@@ -2,10 +2,10 @@
2
2
  // @test src/__tests__/model-switching.test.ts
3
3
  import { createInterface } from 'node:readline';
4
4
  import { resolveBuiltinCommand, spawnResolvedCommand } from '../command-spec.js';
5
- import { fallbackClaudeAliasModels, fallbackGeminiModels, parseCodexAppServerModels, parseCursorModels, parseOpenCodeModels, } from './parsers.js';
5
+ import { fallbackClaudeAliasModels, discoverClaudeAliasModelsFromEnv, fallbackGeminiModels, parseCodexAppServerModels, parseCursorModels, parseOpenCodeModels, } from './parsers.js';
6
6
  import { runResolvedCommand } from './runner.js';
7
7
  import { DISCOVERY_WORKDIR } from './types.js';
8
- import { buildAgentProcessEnv } from '../../agent-env.js';
8
+ import { buildAgentProcessEnv, readLatestUserEnv } from '../../agent-env.js';
9
9
  function sendAppServerRpc(proc, pending, id, method, params, timeoutMs) {
10
10
  if (!proc.stdin)
11
11
  return Promise.reject(new Error('codex app-server stdin unavailable'));
@@ -108,6 +108,9 @@ async function discoverOpenCodeModels() {
108
108
  async function discoverClaudeModels() {
109
109
  if (!resolveBuiltinCommand('claude'))
110
110
  return [];
111
+ const shellEnvModels = discoverClaudeAliasModelsFromEnv(readLatestUserEnv());
112
+ if (shellEnvModels.length > 0)
113
+ return shellEnvModels;
111
114
  return fallbackClaudeAliasModels();
112
115
  }
113
116
  async function discoverCodexModels() {
@@ -4,12 +4,14 @@ export declare function parseCursorModels(raw: string): ModelInfo[];
4
4
  export declare function parseOpenClawModels(raw: string): ModelInfo[];
5
5
  export declare function parseOpenCodeModels(raw: string): ModelInfo[];
6
6
  export declare function parseClaudeModels(raw: string): ModelInfo[];
7
- type EnvLike = Record<string, string | undefined>;
7
+ export type EnvLike = Record<string, string | undefined>;
8
+ export declare function readEnvValue(env: EnvLike, key: string): string | null;
9
+ export declare function inferClaudeOverrideProvider(modelName: string, env: EnvLike): string;
10
+ export declare function discoverClaudeAliasModelsFromEnv(env?: EnvLike): ModelInfo[];
8
11
  export declare function applyClaudeModelEnvOverrides(models: ModelInfo[], env?: EnvLike): ModelInfo[];
9
12
  export declare function parseClaudeBinaryModels(raw: string): ModelInfo[];
10
- export declare function fallbackClaudeAliasModels(): ModelInfo[];
13
+ export declare function fallbackClaudeAliasModels(env?: EnvLike): ModelInfo[];
11
14
  export declare function parseCodexModels(raw: string): ModelInfo[];
12
15
  export declare function parseCodexAppServerModels(raw: unknown): ModelInfo[];
13
16
  export declare function parseGeminiModels(raw: string): ModelInfo[];
14
17
  export declare function fallbackGeminiModels(): ModelInfo[];
15
- export {};
@@ -193,6 +193,9 @@ const CLAUDE_ALIAS_MODEL_ENV = {
193
193
  opus: 'ANTHROPIC_DEFAULT_OPUS_MODEL',
194
194
  haiku: 'ANTHROPIC_DEFAULT_HAIKU_MODEL',
195
195
  };
196
+ const CLAUDE_DEEPSEEK_REASONING_EFFORTS = [
197
+ { id: 'max', name: 'Max' },
198
+ ];
196
199
  const CLAUDE_REASONING_EFFORTS = [
197
200
  { id: 'low', name: 'Low' },
198
201
  { id: 'medium', name: 'Medium' },
@@ -207,17 +210,46 @@ function withClaudeReasoningEfforts(model) {
207
210
  defaultReasoningEffort: 'medium',
208
211
  };
209
212
  }
210
- function readEnvValue(env, key) {
213
+ export function readEnvValue(env, key) {
211
214
  const value = env[key]?.trim();
212
215
  return value || null;
213
216
  }
214
- function inferClaudeOverrideProvider(modelName, env) {
217
+ export function inferClaudeOverrideProvider(modelName, env) {
215
218
  const baseUrl = readEnvValue(env, 'ANTHROPIC_BASE_URL')?.toLowerCase() ?? '';
216
219
  if (baseUrl.includes('deepseek.com') || modelName.toLowerCase().startsWith('deepseek-')) {
217
220
  return 'deepseek';
218
221
  }
219
222
  return baseUrl ? 'custom' : 'anthropic';
220
223
  }
224
+ function configuredClaudeAliasModels(env) {
225
+ const configured = new Set();
226
+ for (const key of Object.values(CLAUDE_ALIAS_MODEL_ENV)) {
227
+ const modelName = readEnvValue(env, key);
228
+ if (modelName)
229
+ configured.add(modelName);
230
+ }
231
+ const models = Array.from(configured).map((modelName, index) => {
232
+ const provider = inferClaudeOverrideProvider(modelName, env);
233
+ const model = {
234
+ id: modelName,
235
+ name: modelName,
236
+ provider,
237
+ isDefault: index === 0,
238
+ };
239
+ if (provider === 'deepseek') {
240
+ return {
241
+ ...model,
242
+ supportedReasoningEfforts: CLAUDE_DEEPSEEK_REASONING_EFFORTS,
243
+ defaultReasoningEffort: readEnvValue(env, 'CLAUDE_CODE_EFFORT_LEVEL') ?? 'max',
244
+ };
245
+ }
246
+ return withClaudeReasoningEfforts(model);
247
+ });
248
+ return uniqueModels(models);
249
+ }
250
+ export function discoverClaudeAliasModelsFromEnv(env = process.env) {
251
+ return configuredClaudeAliasModels(env);
252
+ }
221
253
  export function applyClaudeModelEnvOverrides(models, env = process.env) {
222
254
  return models.map((model) => {
223
255
  if (!['default', 'sonnet', 'opus', 'haiku'].includes(model.id))
@@ -268,13 +300,16 @@ export function parseClaudeBinaryModels(raw) {
268
300
  }
269
301
  return uniqueModels(models);
270
302
  }
271
- export function fallbackClaudeAliasModels() {
303
+ export function fallbackClaudeAliasModels(env = process.env) {
304
+ const configuredModels = configuredClaudeAliasModels(env);
305
+ if (configuredModels.length > 0)
306
+ return configuredModels;
272
307
  return applyClaudeModelEnvOverrides([
273
308
  { id: 'default', name: 'Default (recommended)', provider: 'anthropic', isDefault: true },
274
309
  { id: 'sonnet', name: 'Sonnet', provider: 'anthropic' },
275
310
  { id: 'opus', name: 'Opus', provider: 'anthropic' },
276
311
  { id: 'haiku', name: 'Haiku', provider: 'anthropic' },
277
- ]).map(withClaudeReasoningEfforts);
312
+ ], env).map(withClaudeReasoningEfforts);
278
313
  }
279
314
  export function parseCodexModels(raw) {
280
315
  const clean = stripAnsi(raw);
@@ -1,5 +1,6 @@
1
1
  // @arch docs/architecture/cli/model-discovery.md
2
2
  // @test src/__tests__/model-switching.test.ts
3
+ import { getAgentConfigSummary } from '../config-status.js';
3
4
  import { isCacheFresh, readCache, writeCache } from './cache.js';
4
5
  import { discoverModelsForAgent } from './discovery.js';
5
6
  function uniqueModels(models) {
@@ -15,10 +16,14 @@ function uniqueModels(models) {
15
16
  }
16
17
  export function getCachedAgentInfos(detectedAgents) {
17
18
  const cache = readCache();
18
- return detectedAgents.map((agent) => ({
19
- type: agent.type,
20
- models: cache.agents[agent.type]?.models ?? agent.models ?? [],
21
- }));
19
+ return detectedAgents.map((agent) => {
20
+ const config = getAgentConfigSummary(agent.type);
21
+ return {
22
+ type: agent.type,
23
+ models: cache.agents[agent.type]?.models ?? agent.models ?? [],
24
+ ...(config ? { config } : {}),
25
+ };
26
+ });
22
27
  }
23
28
  export async function refreshAgentInfos(detectedAgents, context) {
24
29
  const cache = readCache();
@@ -59,9 +64,11 @@ export async function resolveAgentInfos(detectedAgents, context) {
59
64
  }
60
65
  }
61
66
  }
67
+ const config = getAgentConfigSummary(agent.type);
62
68
  infos.push({
63
69
  type: agent.type,
64
70
  models,
71
+ ...(config ? { config } : {}),
65
72
  });
66
73
  }
67
74
  if (changed) {
@@ -1,8 +1,10 @@
1
1
  export { getCachedAgentInfos, refreshAgentInfos, resolveAgentInfos, } from './model-registry/service.js';
2
- import { applyClaudeModelEnvOverrides, parseClaudeBinaryModels, parseClaudeModels, parseCodexModels, parseCodexAppServerModels, parseCursorModels, parseGeminiModels, parseOpenClawModels, stripAnsi } from './model-registry/parsers.js';
2
+ import { applyClaudeModelEnvOverrides, discoverClaudeAliasModelsFromEnv, fallbackClaudeAliasModels, parseClaudeBinaryModels, parseClaudeModels, parseCodexModels, parseCodexAppServerModels, parseCursorModels, parseGeminiModels, parseOpenClawModels, stripAnsi } from './model-registry/parsers.js';
3
3
  /** Used by Vitest / `model-switching-e2e.ts` — not a stable public API. */
4
4
  export declare const modelSwitchingTestExports: {
5
5
  readonly applyClaudeModelEnvOverrides: typeof applyClaudeModelEnvOverrides;
6
+ readonly discoverClaudeAliasModelsFromEnv: typeof discoverClaudeAliasModelsFromEnv;
7
+ readonly fallbackClaudeAliasModels: typeof fallbackClaudeAliasModels;
6
8
  readonly parseClaudeBinaryModels: typeof parseClaudeBinaryModels;
7
9
  readonly parseClaudeModels: typeof parseClaudeModels;
8
10
  readonly parseCodexModels: typeof parseCodexModels;
@@ -1,10 +1,12 @@
1
1
  // @arch docs/architecture/cli/model-discovery.md
2
2
  // @test src/__tests__/model-switching.test.ts
3
3
  export { getCachedAgentInfos, refreshAgentInfos, resolveAgentInfos, } from './model-registry/service.js';
4
- import { applyClaudeModelEnvOverrides, parseClaudeBinaryModels, parseClaudeModels, parseCodexModels, parseCodexAppServerModels, parseCursorModels, parseGeminiModels, parseOpenClawModels, stripAnsi, } from './model-registry/parsers.js';
4
+ import { applyClaudeModelEnvOverrides, discoverClaudeAliasModelsFromEnv, fallbackClaudeAliasModels, parseClaudeBinaryModels, parseClaudeModels, parseCodexModels, parseCodexAppServerModels, parseCursorModels, parseGeminiModels, parseOpenClawModels, stripAnsi, } from './model-registry/parsers.js';
5
5
  /** Used by Vitest / `model-switching-e2e.ts` — not a stable public API. */
6
6
  export const modelSwitchingTestExports = {
7
7
  applyClaudeModelEnvOverrides,
8
+ discoverClaudeAliasModelsFromEnv,
9
+ fallbackClaudeAliasModels,
8
10
  parseClaudeBinaryModels,
9
11
  parseClaudeModels,
10
12
  parseCodexModels,
@@ -0,0 +1 @@
1
+ export declare function splitExternalReplyText(text: string): string[];