claude-simple 0.2.24 → 0.2.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -2
- package/client.js +20 -3
- package/dist/app.0.2.26.min.js +1179 -0
- package/dist/index.html +2 -2
- package/dist/style.0.2.26.min.css +1851 -0
- package/package.json +1 -1
- package/public/api.js +10 -3
- package/public/app.js +1 -1
- package/public/chat.js +260 -12
- package/public/files.js +50 -0
- package/public/index.html +2 -2
- package/public/locks.js +139 -0
- package/public/privacy.html +111 -0
- package/public/router.js +5 -0
- package/public/settings.js +312 -63
- package/public/shell/boot.js +3 -0
- package/public/shell/notes.js +11 -3
- package/public/shell/session-list.js +50 -2
- package/public/shell/session-nav.js +49 -12
- package/public/shell/templates.js +5 -1
- package/public/shell.js +16 -4
- package/public/state.js +6 -2
- package/public/style.css +227 -3
- package/public/sw.js +3 -3
- package/public/ui.min.css +1 -1
- package/src/agent-env.js +211 -0
- package/src/agents/claude.js +10 -3
- package/src/agents/grok.js +24 -0
- package/src/agents/meta-agent.js +89 -17
- package/src/agents/models.js +30 -1
- package/src/agents/sessions.js +12 -2
- package/src/app.js +512 -25
- package/src/collab.js +267 -0
- package/src/runtime/external-session-sync.js +106 -0
- package/src/runtime/protocol.js +11 -2
- package/dist/app.0.2.24.min.js +0 -1104
- package/dist/style.0.2.24.min.css +0 -1627
package/src/agent-env.js
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Allowlisted env for Claude Code (and Anthropic-compatible gateways
|
|
3
|
+
* such as DeepSeek). LaunchAgents do not inherit shell exports, so we
|
|
4
|
+
* persist a profile and merge ~/.claude/settings.json `env` at spawn time.
|
|
5
|
+
*/
|
|
6
|
+
import { promises as fs } from 'fs';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import os from 'os';
|
|
9
|
+
|
|
10
|
+
export const CLAUDE_ENV_KEYS = [
|
|
11
|
+
'ANTHROPIC_AUTH_TOKEN',
|
|
12
|
+
'ANTHROPIC_API_KEY',
|
|
13
|
+
'ANTHROPIC_BASE_URL',
|
|
14
|
+
'ANTHROPIC_MODEL',
|
|
15
|
+
'ANTHROPIC_DEFAULT_OPUS_MODEL',
|
|
16
|
+
'ANTHROPIC_DEFAULT_SONNET_MODEL',
|
|
17
|
+
'ANTHROPIC_DEFAULT_HAIKU_MODEL',
|
|
18
|
+
'ANTHROPIC_SMALL_FAST_MODEL',
|
|
19
|
+
'CLAUDE_CODE_SUBAGENT_MODEL',
|
|
20
|
+
'CLAUDE_CODE_EFFORT_LEVEL',
|
|
21
|
+
'CLAUDE_MODEL',
|
|
22
|
+
'CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC',
|
|
23
|
+
'API_TIMEOUT_MS',
|
|
24
|
+
'ANTHROPIC_BETAS',
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
const SECRET_RE = /TOKEN|KEY|SECRET|PASSWORD/i;
|
|
28
|
+
|
|
29
|
+
export function isSecretEnvKey(key) {
|
|
30
|
+
return SECRET_RE.test(key);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function pickAllowedEnv(src) {
|
|
34
|
+
const out = {};
|
|
35
|
+
if (!src || typeof src !== 'object') return out;
|
|
36
|
+
for (const key of CLAUDE_ENV_KEYS) {
|
|
37
|
+
const v = src[key];
|
|
38
|
+
if (typeof v === 'string' && v.trim()) out[key] = v.trim();
|
|
39
|
+
}
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function mergeClaudeEnv(...layers) {
|
|
44
|
+
const out = {};
|
|
45
|
+
for (const layer of layers) {
|
|
46
|
+
if (!layer) continue;
|
|
47
|
+
Object.assign(out, pickAllowedEnv(layer));
|
|
48
|
+
}
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Apply allowlisted keys onto a full process.env clone. */
|
|
53
|
+
export function withClaudeEnv(processEnv, extra) {
|
|
54
|
+
return { ...processEnv, ...pickAllowedEnv(extra) };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const PLACEHOLDER_MODELS = new Set([
|
|
58
|
+
'default', 'auto', '', 'claude-sonnet-4-5', 'claude-sonnet-4-6',
|
|
59
|
+
]);
|
|
60
|
+
|
|
61
|
+
export function resolveClaudeModel(options, env) {
|
|
62
|
+
const requested = typeof options?.model === 'string' ? options.model.trim() : '';
|
|
63
|
+
const fromEnv = env?.ANTHROPIC_MODEL || env?.CLAUDE_MODEL || '';
|
|
64
|
+
const customGateway = !!(env?.ANTHROPIC_BASE_URL);
|
|
65
|
+
if (!requested || requested === 'default' || requested === 'auto') {
|
|
66
|
+
return fromEnv || undefined;
|
|
67
|
+
}
|
|
68
|
+
// Official fallback ids against a third-party Anthropic-compatible URL
|
|
69
|
+
// (DeepSeek etc.) would 404; prefer the env model.
|
|
70
|
+
if (customGateway && fromEnv && PLACEHOLDER_MODELS.has(requested)) {
|
|
71
|
+
return fromEnv;
|
|
72
|
+
}
|
|
73
|
+
return requested;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function resolveClaudeEffort(options, env) {
|
|
77
|
+
const requested = typeof options?.effort === 'string' ? options.effort.trim() : '';
|
|
78
|
+
if (requested) return requested;
|
|
79
|
+
const fromEnv = env?.CLAUDE_CODE_EFFORT_LEVEL;
|
|
80
|
+
return fromEnv || undefined;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function redactEnv(env) {
|
|
84
|
+
const out = {};
|
|
85
|
+
for (const [k, v] of Object.entries(env || {})) {
|
|
86
|
+
if (isSecretEnvKey(k)) out[k] = { set: true, preview: maskSecret(v) };
|
|
87
|
+
else out[k] = v;
|
|
88
|
+
}
|
|
89
|
+
return out;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function maskSecret(v) {
|
|
93
|
+
const s = String(v);
|
|
94
|
+
if (s.length <= 8) return '••••';
|
|
95
|
+
return `${s.slice(0, 3)}…${s.slice(-4)}`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export async function readSettingsJsonEnv(configDir) {
|
|
99
|
+
if (!configDir) return {};
|
|
100
|
+
const dir = String(configDir).replace(/^~/, os.homedir());
|
|
101
|
+
let acc = {};
|
|
102
|
+
for (const name of ['settings.json', 'settings.local.json']) {
|
|
103
|
+
try {
|
|
104
|
+
const raw = JSON.parse(await fs.readFile(path.join(dir, name), 'utf8'));
|
|
105
|
+
if (raw?.env && typeof raw.env === 'object') {
|
|
106
|
+
acc = { ...acc, ...pickAllowedEnv(raw.env) };
|
|
107
|
+
}
|
|
108
|
+
} catch { /* missing is fine */ }
|
|
109
|
+
}
|
|
110
|
+
return acc;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function defaultAgentEnvPath() {
|
|
114
|
+
if (process.env.AGENT_ENV_FILE) return process.env.AGENT_ENV_FILE;
|
|
115
|
+
return path.join(os.homedir(), '.claude-simple-edge', 'agent-env.json');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function normalizeProfileName(name) {
|
|
119
|
+
return String(name || '').trim();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function parseAgentEnvDoc(raw) {
|
|
123
|
+
if (!raw || typeof raw !== 'object') return { env: {}, profiles: {} };
|
|
124
|
+
const hasProfiles = raw.profiles && typeof raw.profiles === 'object';
|
|
125
|
+
const env = pickAllowedEnv(raw.env || (!hasProfiles ? raw : {}));
|
|
126
|
+
const profiles = {};
|
|
127
|
+
if (hasProfiles) {
|
|
128
|
+
for (const [k, v] of Object.entries(raw.profiles)) {
|
|
129
|
+
const n = normalizeProfileName(k);
|
|
130
|
+
if (n) profiles[n] = pickAllowedEnv(v);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return { env, profiles };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export async function loadAgentEnvDoc(filePath = defaultAgentEnvPath()) {
|
|
137
|
+
try {
|
|
138
|
+
const raw = JSON.parse(await fs.readFile(filePath, 'utf8'));
|
|
139
|
+
return parseAgentEnvDoc(raw);
|
|
140
|
+
} catch {
|
|
141
|
+
return { env: {}, profiles: {} };
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export async function saveAgentEnvDoc(doc, filePath = defaultAgentEnvPath()) {
|
|
146
|
+
const env = pickAllowedEnv(doc?.env);
|
|
147
|
+
const profiles = {};
|
|
148
|
+
for (const [k, v] of Object.entries(doc?.profiles || {})) {
|
|
149
|
+
const n = normalizeProfileName(k);
|
|
150
|
+
const picked = pickAllowedEnv(v);
|
|
151
|
+
if (n && Object.keys(picked).length) profiles[n] = picked;
|
|
152
|
+
}
|
|
153
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true, mode: 0o700 });
|
|
154
|
+
const tmp = `${filePath}.tmp`;
|
|
155
|
+
await fs.writeFile(tmp, JSON.stringify({ env, profiles }, null, 2), { mode: 0o600 });
|
|
156
|
+
await fs.rename(tmp, filePath);
|
|
157
|
+
return { env, profiles };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Default (unnamed) allowlist. Named profiles live under `profiles`. */
|
|
161
|
+
export async function loadAgentEnvFile(filePath = defaultAgentEnvPath()) {
|
|
162
|
+
return (await loadAgentEnvDoc(filePath)).env;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export async function saveAgentEnvFile(env, filePath = defaultAgentEnvPath()) {
|
|
166
|
+
const doc = await loadAgentEnvDoc(filePath);
|
|
167
|
+
doc.env = pickAllowedEnv(env);
|
|
168
|
+
await saveAgentEnvDoc(doc, filePath);
|
|
169
|
+
return doc.env;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Default layer, then workspace-named profile (same name as Settings workspace). */
|
|
173
|
+
export function envForProfile(doc, name) {
|
|
174
|
+
const n = normalizeProfileName(name);
|
|
175
|
+
const named = n && doc?.profiles?.[n] ? doc.profiles[n] : {};
|
|
176
|
+
return mergeClaudeEnv(doc?.env, named);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Merge incoming PATCH: omit key to keep, empty string to delete, string to set. */
|
|
180
|
+
export function patchEnv(current, patch) {
|
|
181
|
+
const out = { ...pickAllowedEnv(current) };
|
|
182
|
+
if (!patch || typeof patch !== 'object') return out;
|
|
183
|
+
for (const key of CLAUDE_ENV_KEYS) {
|
|
184
|
+
if (!(key in patch)) continue;
|
|
185
|
+
const v = patch[key];
|
|
186
|
+
if (v == null || v === '' || (typeof v === 'object' && v.set === true && !v.value)) {
|
|
187
|
+
if (v && typeof v === 'object' && v.set === true) continue; // keep secret
|
|
188
|
+
delete out[key];
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
if (typeof v === 'string') out[key] = v.trim();
|
|
192
|
+
else if (typeof v === 'object' && typeof v.value === 'string') out[key] = v.value.trim();
|
|
193
|
+
}
|
|
194
|
+
return out;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export async function applyAgentEnvToProcess(processEnv = process.env) {
|
|
198
|
+
const fileEnv = await loadAgentEnvFile();
|
|
199
|
+
const settingsEnv = await readSettingsJsonEnv(path.join(os.homedir(), '.claude'));
|
|
200
|
+
const merged = mergeClaudeEnv(processEnv, settingsEnv, fileEnv);
|
|
201
|
+
for (const [k, v] of Object.entries(merged)) processEnv[k] = v;
|
|
202
|
+
return merged;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export async function buildClaudeSpawnEnv({ processEnv = process.env, configDir, extra, profile } = {}) {
|
|
206
|
+
const settingsEnv = await readSettingsJsonEnv(configDir || path.join(os.homedir(), '.claude'));
|
|
207
|
+
const doc = await loadAgentEnvDoc();
|
|
208
|
+
const fileEnv = envForProfile(doc, profile);
|
|
209
|
+
const allow = mergeClaudeEnv(processEnv, settingsEnv, fileEnv, extra);
|
|
210
|
+
return { ...processEnv, ...allow };
|
|
211
|
+
}
|
package/src/agents/claude.js
CHANGED
|
@@ -2,6 +2,7 @@ import { query } from '@anthropic-ai/claude-agent-sdk';
|
|
|
2
2
|
import crypto from 'crypto';
|
|
3
3
|
import { mapAgentEmit, compactToolInput } from '../runtime/protocol.js';
|
|
4
4
|
import { waitForApproval, cancelApprovalsForSession } from '../runtime/approvals.js';
|
|
5
|
+
import { buildClaudeSpawnEnv, resolveClaudeModel, resolveClaudeEffort } from '../agent-env.js';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Run a Claude Code turn via the agent SDK.
|
|
@@ -20,7 +21,11 @@ export async function runClaude(command, options, send, { cliPath, activeSession
|
|
|
20
21
|
sessionId: options.sessionId || null,
|
|
21
22
|
});
|
|
22
23
|
|
|
23
|
-
const env = {
|
|
24
|
+
const env = await buildClaudeSpawnEnv({
|
|
25
|
+
processEnv: process.env,
|
|
26
|
+
configDir: options.configDir,
|
|
27
|
+
profile: options.workspaceName || options.profile,
|
|
28
|
+
});
|
|
24
29
|
if (options.configDir) env.CLAUDE_CONFIG_DIR = options.configDir;
|
|
25
30
|
|
|
26
31
|
const permissionMode = options.permissionMode || 'bypassPermissions';
|
|
@@ -40,12 +45,14 @@ export async function runClaude(command, options, send, { cliPath, activeSession
|
|
|
40
45
|
tools: { type: 'preset', preset: 'claude_code' },
|
|
41
46
|
systemPrompt: { type: 'preset', preset: 'claude_code' },
|
|
42
47
|
settingSources: ['project', 'user', 'local'],
|
|
43
|
-
model: options.model || 'claude-sonnet-4-5',
|
|
44
48
|
abortSignal: controller.signal,
|
|
45
49
|
};
|
|
50
|
+
const model = resolveClaudeModel(options, env);
|
|
51
|
+
if (model) sdkOptions.model = model;
|
|
52
|
+
const effort = resolveClaudeEffort(options, env);
|
|
53
|
+
if (effort) sdkOptions.effort = effort;
|
|
46
54
|
if (options.cwd) sdkOptions.cwd = options.cwd;
|
|
47
55
|
if (options.sessionId) sdkOptions.resume = options.sessionId;
|
|
48
|
-
if (options.effort) sdkOptions.effort = options.effort;
|
|
49
56
|
if (permissionMode && permissionMode !== 'default') {
|
|
50
57
|
sdkOptions.permissionMode = permissionMode;
|
|
51
58
|
}
|
package/src/agents/grok.js
CHANGED
|
@@ -10,6 +10,30 @@ const clients = new Map();
|
|
|
10
10
|
const queues = new Map();
|
|
11
11
|
const queueAliases = new Map();
|
|
12
12
|
|
|
13
|
+
/** Read-only queue snapshot used when a browser reconnects mid-turn. The ACP
|
|
14
|
+
* process owns the real queue; exposing it lets the UI restore the user's
|
|
15
|
+
* already-accepted follow-up prompts after a page refresh. */
|
|
16
|
+
export function getGrokQueueState(sessionId) {
|
|
17
|
+
const key = (sessionId && queueAliases.get(sessionId)) || sessionId;
|
|
18
|
+
const queue = key ? queues.get(key) : null;
|
|
19
|
+
if (!queue) return { running: false, items: [] };
|
|
20
|
+
return {
|
|
21
|
+
running: !!queue.running,
|
|
22
|
+
items: queue.items.map(item => String(item?.command || '')).filter(Boolean),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Drop follow-ups that have not started yet. The active ACP prompt is aborted
|
|
27
|
+
* by app.js through its active-session entry. */
|
|
28
|
+
export function cancelGrokQueue(sessionId) {
|
|
29
|
+
const key = (sessionId && queueAliases.get(sessionId)) || sessionId;
|
|
30
|
+
const queue = key ? queues.get(key) : null;
|
|
31
|
+
if (!queue) return 0;
|
|
32
|
+
const count = queue.items.length;
|
|
33
|
+
queue.items.length = 0;
|
|
34
|
+
return count;
|
|
35
|
+
}
|
|
36
|
+
|
|
13
37
|
function profileKey(options) {
|
|
14
38
|
return `${options.model || ''}\u001f${options.effort || ''}`;
|
|
15
39
|
}
|
package/src/agents/meta-agent.js
CHANGED
|
@@ -28,8 +28,8 @@ import crypto from 'crypto';
|
|
|
28
28
|
import { spawn } from 'child_process';
|
|
29
29
|
|
|
30
30
|
export const AI_DEFAULTS = {
|
|
31
|
-
url: 'https://api.
|
|
32
|
-
model: '
|
|
31
|
+
url: 'https://api.xiaomimimo.com/v1/chat/completions',
|
|
32
|
+
model: 'mimo-v2.5',
|
|
33
33
|
vlm_url: '',
|
|
34
34
|
vlm_model: '',
|
|
35
35
|
temperature: 0.3,
|
|
@@ -51,7 +51,8 @@ export const DEFAULT_SYSTEM = `你是 Claude Simple UI 的内置工作助手(M
|
|
|
51
51
|
- 同一批图的追问:传入上次返回的 thread_id + 新 prompt,可复用图片与历史问答,不必重复传路径(也可继续传 paths 以补图)。
|
|
52
52
|
- 不要假装已经看到图片;没调用工具就说「根据路径无法看见内容」。
|
|
53
53
|
6. 需要验证时:write_file 写代码 → run_command 执行 → 根据输出迭代。
|
|
54
|
-
7.
|
|
54
|
+
7. 查找项目文件时优先用 find_files;先缩小范围再 read_file,不要用 run_command 代替只读检索。
|
|
55
|
+
8. 回复简洁有结构,中文优先(用户用中文时)。`;
|
|
55
56
|
|
|
56
57
|
export const REPORT_SYSTEM = `你是工作复盘助手。根据提供的 JSON 活动快照,写一份清晰的工作报告(Markdown)。
|
|
57
58
|
结构建议:
|
|
@@ -292,6 +293,23 @@ const TOOLS = [
|
|
|
292
293
|
},
|
|
293
294
|
},
|
|
294
295
|
},
|
|
296
|
+
{
|
|
297
|
+
type: 'function',
|
|
298
|
+
function: {
|
|
299
|
+
name: 'find_files',
|
|
300
|
+
description: '按文件名在目录树中查找文件;适合先定位代码、配置或文档,再用 read_file 读取内容',
|
|
301
|
+
parameters: {
|
|
302
|
+
type: 'object',
|
|
303
|
+
required: ['path', 'query'],
|
|
304
|
+
properties: {
|
|
305
|
+
path: { type: 'string', description: '要搜索的根目录(绝对路径)' },
|
|
306
|
+
query: { type: 'string', description: '文件名关键词,不区分大小写' },
|
|
307
|
+
max: { type: 'integer', description: '最多返回条数,默认 80,上限 200' },
|
|
308
|
+
maxDepth: { type: 'integer', description: '最大目录深度,默认 5,上限 10' },
|
|
309
|
+
},
|
|
310
|
+
},
|
|
311
|
+
},
|
|
312
|
+
},
|
|
295
313
|
{
|
|
296
314
|
type: 'function',
|
|
297
315
|
function: {
|
|
@@ -504,12 +522,16 @@ export function createMetaAgent(deps) {
|
|
|
504
522
|
cursorHome,
|
|
505
523
|
} = deps;
|
|
506
524
|
|
|
507
|
-
const configFile = path.join(rootDir, '.ai_config.json');
|
|
508
|
-
const reportsFile = path.join(rootDir, '.ai_reports.json');
|
|
509
|
-
const legacySessionsFile = path.join(rootDir, '.ai_sessions.json');
|
|
510
525
|
const aiDir = process.env.AI_DATA_DIR
|
|
511
526
|
? expandPath(process.env.AI_DATA_DIR)
|
|
512
527
|
: path.join(rootDir, '.ai');
|
|
528
|
+
// Keep credentials and reports outside an npm-installed package whenever
|
|
529
|
+
// AI_DATA_DIR is configured. Legacy files are read once for compatibility.
|
|
530
|
+
const configFile = path.join(aiDir, 'config.json');
|
|
531
|
+
const reportsFile = path.join(aiDir, 'reports.json');
|
|
532
|
+
const legacyConfigFile = path.join(rootDir, '.ai_config.json');
|
|
533
|
+
const legacyReportsFile = path.join(rootDir, '.ai_reports.json');
|
|
534
|
+
const legacySessionsFile = path.join(rootDir, '.ai_sessions.json');
|
|
513
535
|
const notesFile = path.join(aiDir, 'meta-notes.json');
|
|
514
536
|
const sessionBriefsFile = path.join(aiDir, 'session-briefs.json');
|
|
515
537
|
const sessionsDir = path.join(aiDir, 'sessions');
|
|
@@ -1455,10 +1477,15 @@ export function createMetaAgent(deps) {
|
|
|
1455
1477
|
|
|
1456
1478
|
async function loadConfig() {
|
|
1457
1479
|
if (configCache) return configCache;
|
|
1480
|
+
await ensureAiDirs();
|
|
1458
1481
|
try {
|
|
1459
1482
|
configCache = { ...AI_DEFAULTS, ...JSON.parse(await fs.readFile(configFile, 'utf8')) };
|
|
1460
1483
|
} catch {
|
|
1461
|
-
|
|
1484
|
+
try {
|
|
1485
|
+
configCache = { ...AI_DEFAULTS, ...JSON.parse(await fs.readFile(legacyConfigFile, 'utf8')) };
|
|
1486
|
+
} catch {
|
|
1487
|
+
configCache = { ...AI_DEFAULTS, key: '', vlm_key: '', system: DEFAULT_SYSTEM };
|
|
1488
|
+
}
|
|
1462
1489
|
}
|
|
1463
1490
|
if (!configCache.system) configCache.system = DEFAULT_SYSTEM;
|
|
1464
1491
|
return configCache;
|
|
@@ -1492,10 +1519,15 @@ export function createMetaAgent(deps) {
|
|
|
1492
1519
|
|
|
1493
1520
|
async function loadReports() {
|
|
1494
1521
|
if (reportsCache) return reportsCache;
|
|
1522
|
+
await ensureAiDirs();
|
|
1495
1523
|
try {
|
|
1496
1524
|
reportsCache = JSON.parse(await fs.readFile(reportsFile, 'utf8'));
|
|
1497
1525
|
} catch {
|
|
1498
|
-
|
|
1526
|
+
try {
|
|
1527
|
+
reportsCache = JSON.parse(await fs.readFile(legacyReportsFile, 'utf8'));
|
|
1528
|
+
} catch {
|
|
1529
|
+
reportsCache = {};
|
|
1530
|
+
}
|
|
1499
1531
|
}
|
|
1500
1532
|
return reportsCache;
|
|
1501
1533
|
}
|
|
@@ -1622,11 +1654,26 @@ export function createMetaAgent(deps) {
|
|
|
1622
1654
|
return res;
|
|
1623
1655
|
}
|
|
1624
1656
|
|
|
1657
|
+
function isMimoModel(model) {
|
|
1658
|
+
return String(model || '').toLowerCase().includes('mimo');
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
function providerOptions(body) {
|
|
1662
|
+
if (!isMimoModel(body?.model)) return body;
|
|
1663
|
+
// `extra_body` is an SDK-only Python convenience. This server uses fetch,
|
|
1664
|
+
// so MiMo's provider option must be sent at the JSON body top level.
|
|
1665
|
+
return {
|
|
1666
|
+
...body,
|
|
1667
|
+
max_completion_tokens: body.max_completion_tokens || 2048,
|
|
1668
|
+
thinking: { type: 'disabled' },
|
|
1669
|
+
};
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1625
1672
|
async function chatSync(cfg, body) {
|
|
1626
1673
|
const res = await chatCompletions({
|
|
1627
1674
|
url: cfg.url || AI_DEFAULTS.url,
|
|
1628
1675
|
apiKey: cfg.key || '',
|
|
1629
|
-
body: { ...body, stream: false },
|
|
1676
|
+
body: providerOptions({ ...body, stream: false }),
|
|
1630
1677
|
});
|
|
1631
1678
|
return res.json();
|
|
1632
1679
|
}
|
|
@@ -1635,7 +1682,7 @@ export function createMetaAgent(deps) {
|
|
|
1635
1682
|
const res = await chatCompletions({
|
|
1636
1683
|
url: cfg.url || AI_DEFAULTS.url,
|
|
1637
1684
|
apiKey: cfg.key || '',
|
|
1638
|
-
body: { ...body, stream: true },
|
|
1685
|
+
body: providerOptions({ ...body, stream: true }),
|
|
1639
1686
|
});
|
|
1640
1687
|
const reader = res.body.getReader();
|
|
1641
1688
|
const dec = new TextDecoder();
|
|
@@ -1821,6 +1868,37 @@ export function createMetaAgent(deps) {
|
|
|
1821
1868
|
path: path.join(dir, e.name),
|
|
1822
1869
|
}));
|
|
1823
1870
|
}
|
|
1871
|
+
case 'find_files': {
|
|
1872
|
+
const root = expandPath(args.path);
|
|
1873
|
+
if (!path.isAbsolute(root)) return { error: 'path must be absolute' };
|
|
1874
|
+
const query = String(args.query || '').trim().toLowerCase();
|
|
1875
|
+
if (!query) return { error: 'query required' };
|
|
1876
|
+
const max = Math.min(Math.max(args.max || 80, 1), 200);
|
|
1877
|
+
const maxDepth = Math.min(Math.max(args.maxDepth || 5, 0), 10);
|
|
1878
|
+
const ignored = new Set(['.git', 'node_modules', 'dist', 'build', '.next', '.dart_tool']);
|
|
1879
|
+
const matches = [];
|
|
1880
|
+
async function visit(dir, depth) {
|
|
1881
|
+
if (matches.length >= max || depth > maxDepth) return;
|
|
1882
|
+
let entries;
|
|
1883
|
+
try {
|
|
1884
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
1885
|
+
} catch {
|
|
1886
|
+
return;
|
|
1887
|
+
}
|
|
1888
|
+
for (const entry of entries) {
|
|
1889
|
+
if (matches.length >= max) return;
|
|
1890
|
+
if (ignored.has(entry.name) || entry.isSymbolicLink()) continue;
|
|
1891
|
+
const full = path.join(dir, entry.name);
|
|
1892
|
+
if (entry.isDirectory()) {
|
|
1893
|
+
await visit(full, depth + 1);
|
|
1894
|
+
} else if (entry.isFile() && entry.name.toLowerCase().includes(query)) {
|
|
1895
|
+
matches.push(full);
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
await visit(root, 0);
|
|
1900
|
+
return { root, query, matches, truncated: matches.length >= max };
|
|
1901
|
+
}
|
|
1824
1902
|
case 'read_file': {
|
|
1825
1903
|
const p = expandPath(args.path);
|
|
1826
1904
|
const maxBytes = Math.min(args.maxBytes || 120000, 500000);
|
|
@@ -1989,7 +2067,7 @@ export function createMetaAgent(deps) {
|
|
|
1989
2067
|
const res = await chatCompletions({
|
|
1990
2068
|
url: vlmUrl,
|
|
1991
2069
|
apiKey: vlmKey,
|
|
1992
|
-
body: { ...body, stream: false },
|
|
2070
|
+
body: providerOptions({ ...body, stream: false }),
|
|
1993
2071
|
});
|
|
1994
2072
|
const json = await res.json();
|
|
1995
2073
|
const text = json.choices?.[0]?.message?.content || '';
|
|
@@ -2097,12 +2175,6 @@ export function createMetaAgent(deps) {
|
|
|
2097
2175
|
stream: true,
|
|
2098
2176
|
temperature: config.temperature ?? 0.3,
|
|
2099
2177
|
};
|
|
2100
|
-
// some providers
|
|
2101
|
-
if (body.model && String(body.model).includes('mimo')) {
|
|
2102
|
-
body.max_completion_tokens = 2048;
|
|
2103
|
-
body.extra_body = { thinking: { type: 'disabled' } };
|
|
2104
|
-
}
|
|
2105
|
-
|
|
2106
2178
|
await streamChat(config, body, (chunk) => {
|
|
2107
2179
|
const d = chunk.choices?.[0]?.delta;
|
|
2108
2180
|
if (d?.content) {
|
package/src/agents/models.js
CHANGED
|
@@ -6,6 +6,7 @@ import { promises as fs } from 'fs';
|
|
|
6
6
|
import path from 'path';
|
|
7
7
|
import os from 'os';
|
|
8
8
|
import { execFile } from 'child_process';
|
|
9
|
+
import { pickAllowedEnv, loadAgentEnvFile, readSettingsJsonEnv } from '../agent-env.js';
|
|
9
10
|
|
|
10
11
|
/** Static fallbacks when local cache/config is missing. */
|
|
11
12
|
export const FALLBACK_MODELS = {
|
|
@@ -168,6 +169,34 @@ async function discoverClaude(claudeConfigDirs = []) {
|
|
|
168
169
|
let defaultModel = null;
|
|
169
170
|
const sources = [];
|
|
170
171
|
|
|
172
|
+
const envLayers = [
|
|
173
|
+
pickAllowedEnv(process.env),
|
|
174
|
+
await loadAgentEnvFile(),
|
|
175
|
+
];
|
|
176
|
+
for (const dir of dirs) {
|
|
177
|
+
envLayers.push(await readSettingsJsonEnv(dir));
|
|
178
|
+
}
|
|
179
|
+
const envMerged = Object.assign({}, ...envLayers);
|
|
180
|
+
const envModelKeys = [
|
|
181
|
+
'ANTHROPIC_MODEL',
|
|
182
|
+
'ANTHROPIC_DEFAULT_SONNET_MODEL',
|
|
183
|
+
'ANTHROPIC_DEFAULT_OPUS_MODEL',
|
|
184
|
+
'ANTHROPIC_DEFAULT_HAIKU_MODEL',
|
|
185
|
+
'ANTHROPIC_SMALL_FAST_MODEL',
|
|
186
|
+
'CLAUDE_CODE_SUBAGENT_MODEL',
|
|
187
|
+
'CLAUDE_MODEL',
|
|
188
|
+
];
|
|
189
|
+
for (const k of envModelKeys) {
|
|
190
|
+
const v = envMerged[k];
|
|
191
|
+
if (typeof v === 'string' && v.trim()) {
|
|
192
|
+
found.push({ value: v.trim(), label: shortLabel(v.trim()) });
|
|
193
|
+
if ((k === 'ANTHROPIC_MODEL' || k === 'CLAUDE_MODEL') && !defaultModel) {
|
|
194
|
+
defaultModel = v.trim();
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (found.length) sources.push('env');
|
|
199
|
+
|
|
171
200
|
for (const dir of dirs) {
|
|
172
201
|
for (const name of ['settings.json', 'settings.local.json']) {
|
|
173
202
|
const data = await readJson(path.join(dir, name));
|
|
@@ -202,7 +231,7 @@ async function discoverClaude(claudeConfigDirs = []) {
|
|
|
202
231
|
let models = uniqModels(found);
|
|
203
232
|
let source = 'fallback';
|
|
204
233
|
if (models.length) {
|
|
205
|
-
source = 'settings';
|
|
234
|
+
source = envMerged.ANTHROPIC_BASE_URL ? 'anthropic-compat' : 'settings';
|
|
206
235
|
// Custom gateway / third-party models: list only what is configured
|
|
207
236
|
} else {
|
|
208
237
|
models = FALLBACK_MODELS.claude.models.map(m => ({ ...m }));
|
package/src/agents/sessions.js
CHANGED
|
@@ -1644,14 +1644,24 @@ export async function getSessionMeta(sessionId, agent, { claudeConfigDirs, codex
|
|
|
1644
1644
|
return { cwd, display };
|
|
1645
1645
|
}
|
|
1646
1646
|
|
|
1647
|
-
|
|
1647
|
+
/**
|
|
1648
|
+
* Copy a session into a new native transcript. `upTo` is an exclusive message
|
|
1649
|
+
* count, used by message-level forks to copy only the selected conversation
|
|
1650
|
+
* prefix. Keeping this here (rather than slicing in the browser) makes the
|
|
1651
|
+
* server-owned transcript the source of truth.
|
|
1652
|
+
*/
|
|
1653
|
+
export async function convertSession(sourceSessionId, sourceAgent, targetAgent, { claudeConfigDirs, codexHome, grokHome, geminiHome, cursorHome }, { upTo = null } = {}) {
|
|
1648
1654
|
// 1. Get meta and messages from source session
|
|
1649
1655
|
const meta = await getSessionMeta(sourceSessionId, sourceAgent, { claudeConfigDirs, codexHome, grokHome, geminiHome, cursorHome });
|
|
1650
1656
|
const cwd = meta.cwd || null;
|
|
1651
1657
|
const display = meta.display || 'Converted Session';
|
|
1652
1658
|
|
|
1653
1659
|
const bundle = await loadSessionMessages(sourceSessionId, sourceAgent, { claudeConfigDirs, codexHome, grokHome, geminiHome, cursorHome });
|
|
1654
|
-
|
|
1660
|
+
let messages = bundle.messages || [];
|
|
1661
|
+
if (Number.isInteger(upTo)) {
|
|
1662
|
+
messages = messages.slice(0, Math.max(0, Math.min(upTo, messages.length)));
|
|
1663
|
+
}
|
|
1664
|
+
if (!messages.length) throw new Error('No messages available at the selected fork point');
|
|
1655
1665
|
|
|
1656
1666
|
// 2. Generate target session ID
|
|
1657
1667
|
const targetSessionId = crypto.randomUUID();
|