claude-simple 0.2.28 → 0.2.31

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 (77) hide show
  1. package/README.md +17 -1
  2. package/bin/edge-ctl.js +151 -4
  3. package/client.js +8 -2
  4. package/dist/addon-fit.min.js +0 -0
  5. package/dist/app.0.2.31.min.js +1308 -0
  6. package/dist/index.html +2 -2
  7. package/dist/style.0.2.31.min.css +2012 -0
  8. package/dist/xterm.min.css +0 -0
  9. package/dist/xterm.min.js +0 -0
  10. package/gateway.js +0 -0
  11. package/package.json +2 -2
  12. package/public/addon-fit.min.js +0 -0
  13. package/public/agent-panel.js +47 -20
  14. package/public/api.js +1 -1
  15. package/public/app.js +5 -5
  16. package/public/base.css +0 -0
  17. package/public/cache.js +19 -0
  18. package/public/chat.js +80 -19
  19. package/public/files.js +26 -10
  20. package/public/git.js +2 -2
  21. package/public/icons/icon-180.png +0 -0
  22. package/public/icons/icon-192.png +0 -0
  23. package/public/icons/icon-512.png +0 -0
  24. package/public/icons/icon-maskable-512.png +0 -0
  25. package/public/index.html +2 -2
  26. package/public/lib.js +0 -0
  27. package/public/manifest.webmanifest +0 -0
  28. package/public/markdown.js +16 -0
  29. package/public/math.js +145 -7
  30. package/public/mobile.js +0 -0
  31. package/public/notifications.js +0 -0
  32. package/public/offline.html +0 -0
  33. package/public/pwa.js +1 -1
  34. package/public/router.js +0 -0
  35. package/public/scroll-follow.js +78 -0
  36. package/public/settings.js +41 -0
  37. package/public/shell/boot.js +0 -0
  38. package/public/shell/dashboard.js +0 -0
  39. package/public/shell/hub.js +0 -0
  40. package/public/shell/layout.js +0 -0
  41. package/public/shell/notes.js +0 -0
  42. package/public/shell/session-context.js +0 -0
  43. package/public/shell/session-list.js +49 -16
  44. package/public/shell/session-nav.js +66 -18
  45. package/public/shell/templates.js +0 -0
  46. package/public/shell/util.js +0 -0
  47. package/public/shell.js +0 -0
  48. package/public/state.js +7 -7
  49. package/public/style.css +5 -0
  50. package/public/sw.js +10 -5
  51. package/public/terminal.js +0 -0
  52. package/public/wake-lock.js +0 -0
  53. package/public/workspace-path.js +61 -0
  54. package/public/xterm.min.css +0 -0
  55. package/public/xterm.min.js +0 -0
  56. package/src/agent-quota.js +228 -0
  57. package/src/agents/claude.js +0 -0
  58. package/src/agents/codex.js +2 -0
  59. package/src/agents/cursor.js +2 -1
  60. package/src/agents/gemini.js +2 -1
  61. package/src/agents/grok.js +6 -1
  62. package/src/agents/meta-agent.js +7 -4
  63. package/src/agents/models.js +21 -12
  64. package/src/agents/sessions.js +133 -69
  65. package/src/agents/stream-batch.js +0 -0
  66. package/src/app.js +80 -36
  67. package/src/auto-update.js +2 -0
  68. package/src/collab.js +3 -0
  69. package/src/platform.js +133 -0
  70. package/src/project-api-path.js +14 -0
  71. package/src/runtime/approvals.js +0 -0
  72. package/src/runtime/chat-broadcast.js +12 -0
  73. package/src/runtime/outbox.js +0 -0
  74. package/src/runtime/port-websocket.js +80 -0
  75. package/src/runtime/protocol.js +0 -0
  76. package/dist/app.0.2.28.min.js +0 -1220
  77. package/dist/style.0.2.28.min.css +0 -2007
package/public/state.js CHANGED
@@ -56,7 +56,7 @@ export function setCurrentModel(model, agent = currentAgent.peek(), sessionId =
56
56
  }
57
57
 
58
58
  export const currentAgent = signal(initialAgent); // 'claude'|'codex'|'grok'|'gemini'|'cursor'
59
- export const currentModel = signal('claude-sonnet-4-5');
59
+ export const currentModel = signal('sonnet');
60
60
  export const currentEffort = signal('');
61
61
  // All local agents are trusted by default; users can still choose another mode
62
62
  // from the expanded composer options before starting a turn.
@@ -164,10 +164,10 @@ export const AGENT_LABELS = {
164
164
  /** Static fallbacks when /api/agents has not loaded yet. */
165
165
  export const AGENT_MODELS = {
166
166
  claude: [
167
- { value: 'claude-sonnet-4-5', label: 'sonnet-4-5' },
168
- { value: 'claude-sonnet-4-6', label: 'sonnet-4-6' },
169
- { value: 'claude-opus-4-5', label: 'opus-4-5' },
170
- { value: 'claude-haiku-4-5', label: 'haiku-4-5' },
167
+ { value: 'fable', label: 'fable' },
168
+ { value: 'opus', label: 'opus' },
169
+ { value: 'sonnet', label: 'sonnet' },
170
+ { value: 'haiku', label: 'haiku' },
171
171
  ],
172
172
  codex: [
173
173
  { value: 'gpt-5.4', label: 'gpt-5.4' },
@@ -204,7 +204,7 @@ export const AGENT_MODELS = {
204
204
  };
205
205
 
206
206
  export const AGENT_DEFAULT_MODEL = {
207
- claude: 'claude-sonnet-4-5',
207
+ claude: 'sonnet',
208
208
  codex: 'gpt-5.4',
209
209
  grok: 'grok-4.5',
210
210
  gemini: 'gemini-3.7-flash-high',
@@ -304,7 +304,7 @@ export function getDefaultModel(agent) {
304
304
  const a = (agent || 'claude').toLowerCase();
305
305
  const meta = agentsMeta.peek()?.defaults?.[a];
306
306
  if (meta?.model) return String(meta.model);
307
- return AGENT_DEFAULT_MODEL[a] || getModelsForAgent(a)[0]?.value || 'claude-sonnet-4-5';
307
+ return AGENT_DEFAULT_MODEL[a] || getModelsForAgent(a)[0]?.value || 'sonnet';
308
308
  }
309
309
 
310
310
  /** Effort levels for current (or given) model under agent. */
package/public/style.css CHANGED
@@ -2003,3 +2003,8 @@ html.settings-open #app { visibility: hidden; }
2003
2003
  font-family: inherit;
2004
2004
  font-size: 14px;
2005
2005
  }
2006
+
2007
+ .ma-reasoning { border: 1px solid var(--color-base-300); border-radius: 8px; padding: 8px 10px; color: var(--color-base-content); font-size: 12px; }
2008
+ .ma-reasoning summary, .ma-tool summary { cursor: pointer; }
2009
+ .ma-reasoning pre { max-height: 260px; overflow: auto; overflow-wrap: anywhere; font: inherit; opacity: .7; }
2010
+ .ma-tool[open] .ma-tool-args, .ma-tool[open] .ma-tool-result { max-height: 320px; }
package/public/sw.js CHANGED
@@ -1,12 +1,12 @@
1
1
  /* Agent UI service worker — cache shell assets only.
2
2
  * Never intercepts WebSocket or mutates API semantics beyond offline fallback.
3
3
  * Bump CACHE when shipping UI that must invalidate old shells. */
4
- const CACHE = 'agent-ui-shell-v28';
4
+ const CACHE = 'agent-ui-shell-v34';
5
5
  const PRECACHE = [
6
6
  '/',
7
7
  '/index.html',
8
8
  '/manifest.webmanifest',
9
- '/style.css?v=ui-22',
9
+ '/style.css?v=ui-25',
10
10
  '/style.css',
11
11
  '/ui.min.css',
12
12
  '/vendor/mini-react.min.js',
@@ -16,7 +16,7 @@ const PRECACHE = [
16
16
  '/vendor/mermaid.min.js',
17
17
  '/vendor/katex.min.js',
18
18
  '/vendor/katex.min.css',
19
- '/app.js?v=ui-22',
19
+ '/app.js?v=ui-25',
20
20
  '/icons/icon-192.png',
21
21
  '/icons/icon-512.png',
22
22
  '/icons/icon-maskable-512.png',
@@ -49,6 +49,8 @@ function isApiOrRealtime(url) {
49
49
  || p === '/machine-connect'
50
50
  || p.startsWith('/machine/')
51
51
  || p === '/healthz'
52
+ // Never cache the worker itself or an update can stall on the old shell.
53
+ || p === '/sw.js'
52
54
  );
53
55
  }
54
56
 
@@ -89,8 +91,10 @@ self.addEventListener('fetch', (event) => {
89
91
  return;
90
92
  }
91
93
 
92
- // Same-origin static modules: stale-while-revalidate.
94
+ // App JS/CSS must be network-first so a Hub public/ deploy is visible
95
+ // on the next reload. Vendor + icons stay stale-while-revalidate.
93
96
  if (isStaticAsset(url)) {
97
+ const vendor = url.pathname.startsWith('/vendor/') || url.pathname.startsWith('/icons/');
94
98
  event.respondWith(
95
99
  caches.open(CACHE).then(async (cache) => {
96
100
  const cached = await cache.match(req);
@@ -100,7 +104,8 @@ self.addEventListener('fetch', (event) => {
100
104
  return res;
101
105
  })
102
106
  .catch(() => cached);
103
- return cached || network;
107
+ if (vendor) return cached || network;
108
+ return network.then((res) => res || cached).catch(() => cached);
104
109
  }),
105
110
  );
106
111
  }
File without changes
File without changes
@@ -0,0 +1,61 @@
1
+ /** Resolve a markdown/chat href into a Files-tab target on the edge machine. */
2
+
3
+ function decodeHref(href) {
4
+ const raw = String(href ?? '').trim();
5
+ if (!raw) return '';
6
+ try {
7
+ return decodeURIComponent(raw);
8
+ } catch {
9
+ return raw;
10
+ }
11
+ }
12
+
13
+ function slash(p) {
14
+ return String(p || '').replace(/\\/g, '/');
15
+ }
16
+
17
+ function stripLineSuffix(p) {
18
+ return p.replace(/:[0-9]+(?::[0-9]+)?$/, '');
19
+ }
20
+
21
+ export function resolveWorkspaceFile(href, cwd) {
22
+ let raw = decodeHref(href);
23
+ if (!raw) return null;
24
+ if (/^(https?:|mailto:|tel:|javascript:|#)/i.test(raw)) return null;
25
+ raw = slash(raw).split(/[?#]/)[0];
26
+ raw = raw.replace(/^file:\/\//i, '');
27
+ if (/^\/[A-Za-z]:\//.test(raw)) raw = raw.slice(1);
28
+ if (!raw || raw === '.' || raw === '/') return null;
29
+ if (/^\/(api|ws|session)\b/.test(raw)) return null;
30
+
31
+ const isDir = raw.endsWith('/');
32
+ let path = stripLineSuffix(raw.replace(/\/+$/, ''));
33
+ const root = slash(cwd).replace(/\/+$/, '');
34
+ const isAbs = path.startsWith('/') || /^[A-Za-z]:\//.test(path);
35
+
36
+ if (isAbs && root) {
37
+ const p = path.toLowerCase();
38
+ const r = root.toLowerCase();
39
+ if (p === r) return { relative: '', isDir: true, root };
40
+ if (p.startsWith(`${r}/`)) {
41
+ return { relative: path.slice(root.length + 1), isDir, root };
42
+ }
43
+ const parts = path.split('/');
44
+ const name = parts.pop() || '';
45
+ const parent = parts.join('/') || (path.startsWith('/') ? '/' : '');
46
+ return { relative: name, isDir, root: parent };
47
+ }
48
+
49
+ path = path.replace(/^\.\//, '');
50
+ if (path.startsWith('../') && root) {
51
+ const base = root.split('/');
52
+ const segs = path.split('/');
53
+ for (const seg of segs) {
54
+ if (seg === '..') base.pop();
55
+ else if (seg && seg !== '.') base.push(seg);
56
+ }
57
+ const abs = base.join('/') || '/';
58
+ return resolveWorkspaceFile(abs, cwd);
59
+ }
60
+ return { relative: path, isDir, root: root || cwd };
61
+ }
File without changes
File without changes
@@ -0,0 +1,228 @@
1
+ // Read subscription meters without starting an inference turn. Credentials
2
+ // stay on the owning edge; only the normalized meters leave this module.
3
+ import { promises as fs } from 'node:fs';
4
+ import path from 'node:path';
5
+ import os from 'node:os';
6
+ import crypto from 'node:crypto';
7
+ import { spawn, execFile } from 'node:child_process';
8
+ import { promisify } from 'node:util';
9
+ const exec = promisify(execFile);
10
+
11
+ function percent(value) {
12
+ return typeof value === 'number' && Number.isFinite(value) ? Math.max(0, Math.min(100, value)) : null;
13
+ }
14
+ function meter(label, used, reset, minutes) {
15
+ const usedPercent = percent(used);
16
+ if (usedPercent == null) return null;
17
+ const date = typeof reset === 'number' ? reset * 1000 : Date.parse(reset);
18
+ return { label, usedPercent, remainingPercent: 100 - usedPercent,
19
+ resetsAt: Number.isFinite(date) ? new Date(date).toISOString() : null,
20
+ windowMinutes: minutes || null };
21
+ }
22
+
23
+ export function normalizeClaudeQuota(data) {
24
+ const meters = [];
25
+ for (const [key, label, minutes] of [
26
+ ['five_hour', '5 小时', 300], ['seven_day', '周额度', 10080],
27
+ ['seven_day_opus', 'Opus 周额度', 10080], ['seven_day_sonnet', 'Sonnet 周额度', 10080],
28
+ ['seven_day_oauth_apps', 'OAuth 应用周额度', 10080],
29
+ ]) {
30
+ const row = data?.[key];
31
+ const m = meter(label, row?.utilization, row?.resets_at, minutes);
32
+ if (m) meters.push(m);
33
+ }
34
+ // Newer Claude usage responses expose named meters instead of fixed keys.
35
+ for (const row of (Array.isArray(data) ? data : data?.meters || [])) {
36
+ const m = meter(row.scope?.model?.display_name || row.kind || '额度', row.percent, row.resets_at,
37
+ row.group === 'weekly' ? 10080 : null);
38
+ if (m) meters.push(m);
39
+ }
40
+ return meters;
41
+ }
42
+
43
+ export function normalizeCodexQuota(data) {
44
+ const meters = [];
45
+ const buckets = data?.rateLimitsByLimitId || { codex: data?.rateLimits };
46
+ for (const [id, bucket] of Object.entries(buckets)) {
47
+ for (const key of ['primary', 'secondary']) {
48
+ const row = bucket?.[key];
49
+ const mins = row?.windowDurationMins;
50
+ const label = mins === 10080 ? '周额度' : mins === 300 ? '5 小时' : mins ? `${mins / 60} 小时` : key;
51
+ const m = meter(`${bucket?.limitName || id} · ${label}`, row?.usedPercent, row?.resetsAt, mins);
52
+ if (m) meters.push(m);
53
+ }
54
+ }
55
+ return meters;
56
+ }
57
+
58
+ export function normalizeGrokQuota(data) {
59
+ const config = data?.config;
60
+ if (!config) return [];
61
+ const period = config.currentPeriod;
62
+ const weekly = period?.type?.includes('WEEKLY');
63
+ const used = config.creditUsagePercent ?? (config.monthlyLimit?.val > 0 && typeof config.used?.val === 'number'
64
+ ? config.used.val / config.monthlyLimit.val * 100 : null);
65
+ const m = meter(weekly ? '周额度' : '账期额度', used, period?.end || config.billingPeriodEnd, weekly ? 10080 : null);
66
+ return m ? [m] : [];
67
+ }
68
+
69
+ async function getJSON(url, headers) {
70
+ const response = await fetch(url, { headers, signal: AbortSignal.timeout(15000), redirect: 'error' });
71
+ if (!response.ok) {
72
+ const err = new Error(response.status === 429 ? '额度接口限流,请稍后刷新' : response.status === 401 || response.status === 403 ? '登录已过期或缺少额度读取权限,请在机器上重新登录' : `额度服务返回 HTTP ${response.status}`);
73
+ err.status = response.status;
74
+ throw err;
75
+ }
76
+ return response.json();
77
+ }
78
+
79
+ export function readClaudeLimits(cliPath, configDir) {
80
+ return new Promise((resolve, reject) => {
81
+ const proc = spawn(cliPath, ['-p', '--input-format', 'stream-json', '--output-format', 'stream-json', '--verbose', '--strict-mcp-config', '--mcp-config', '{"mcpServers":{}}'], {
82
+ env: { ...process.env, CLAUDE_CONFIG_DIR: configDir }, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true,
83
+ });
84
+ let buffer = '';
85
+ let settled = false;
86
+ const finish = (err, value) => {
87
+ if (settled) return;
88
+ settled = true; clearTimeout(timer); proc.stdin.end(); proc.kill();
89
+ if (err) reject(err); else resolve(value);
90
+ };
91
+ const timer = setTimeout(() => finish(new Error('Claude quota control timeout')), 20000);
92
+ const send = (id, request) => proc.stdin.write(`${JSON.stringify({ type: 'control_request', request_id: id, request })}\n`);
93
+ proc.on('error', () => finish(new Error('Claude CLI unavailable')));
94
+ proc.stdin.on('error', () => finish(new Error('Claude CLI closed')));
95
+ proc.stderr.on('data', () => {});
96
+ proc.on('exit', () => { if (!settled) finish(new Error('Claude CLI exited')); });
97
+ proc.stdout.on('data', chunk => {
98
+ buffer += chunk.toString();
99
+ if (buffer.length > 1024 * 1024) return finish(new Error('Claude quota response too large'));
100
+ let end;
101
+ while ((end = buffer.indexOf('\n')) >= 0) {
102
+ const line = buffer.slice(0, end); buffer = buffer.slice(end + 1);
103
+ let msg; try { msg = JSON.parse(line); } catch { continue; }
104
+ if (msg.type !== 'control_response') continue;
105
+ const response = msg.response;
106
+ if (response?.request_id === 'quota-init') {
107
+ if (response.subtype === 'error') return finish(new Error('Claude initialization failed'));
108
+ send('quota-read', { subtype: 'get_usage' });
109
+ } else if (response?.request_id === 'quota-read') {
110
+ if (response.subtype === 'error') return finish(new Error('Claude usage control unsupported'));
111
+ finish(null, response.response);
112
+ }
113
+ }
114
+ });
115
+ send('quota-init', { subtype: 'initialize', hooks: {} });
116
+ });
117
+ }
118
+
119
+ async function claudeQuota(configDir, cliPath) {
120
+ // Let Claude read its own Keychain entry through its native credential
121
+ // integration. No user prompt is submitted and no model turn is started.
122
+ try {
123
+ const usage = await readClaudeLimits(cliPath, configDir);
124
+ if (usage?.rate_limits_available) return { meters: normalizeClaudeQuota(usage.rate_limits), plan: usage.subscription_type };
125
+ if (usage?.rate_limits_available === false) return { status: 'unavailable', meters: [], message: '当前 Claude 登录没有可读取的订阅额度(API Key、第三方网关或缺少用量权限)' };
126
+ } catch {}
127
+ let credentials;
128
+ try { credentials = JSON.parse(await fs.readFile(path.join(configDir, '.credentials.json'), 'utf8')); } catch {}
129
+ if (!credentials && process.platform === 'darwin') {
130
+ const standard = path.join(os.homedir(), '.claude');
131
+ const suffix = `-${crypto.createHash('sha256').update(configDir).digest('hex').slice(0, 8)}`;
132
+ let locked = false;
133
+ for (const service of [`Claude Code-credentials${suffix}`, ...(configDir === standard ? ['Claude Code-credentials'] : [])]) {
134
+ try {
135
+ const result = await exec('/usr/bin/security', ['find-generic-password', '-s', service, '-w'], { timeout: 5000 });
136
+ credentials = JSON.parse(result.stdout);
137
+ if (credentials?.claudeAiOauth?.accessToken) break;
138
+ } catch (err) { if (err.code === 36 || err.killed) locked = true; }
139
+ }
140
+ if (!credentials && locked) return { status: 'unavailable', message: '这台机器的 macOS 钥匙串未解锁或不允许后台读取,请在机器上解锁并授权 security 读取 Claude 登录', meters: [] };
141
+ }
142
+ const oauth = credentials?.claudeAiOauth;
143
+ if (!oauth?.accessToken) return { status: 'unavailable', message: '未找到 Claude 订阅登录;API Key 或第三方网关没有订阅周额度', meters: [] };
144
+ const data = await getJSON('https://api.anthropic.com/api/oauth/usage', {
145
+ Authorization: `Bearer ${oauth.accessToken}`, 'anthropic-beta': 'oauth-2025-04-20', 'User-Agent': 'claude-simple-quota/1.0',
146
+ });
147
+ return { meters: normalizeClaudeQuota(data), plan: oauth.subscriptionType || null };
148
+ }
149
+
150
+ export function readCodexLimits(cliPath, codexHome) {
151
+ return new Promise((resolve, reject) => {
152
+ const proc = spawn(cliPath, ['app-server'], { env: { ...process.env, CODEX_HOME: codexHome }, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true });
153
+ let buffer = '';
154
+ let settled = false;
155
+ const finish = (err, result) => {
156
+ if (settled) return;
157
+ settled = true;
158
+ clearTimeout(timer);
159
+ proc.stdin.end();
160
+ proc.kill();
161
+ if (err) reject(err); else resolve(result);
162
+ };
163
+ const timer = setTimeout(() => finish(new Error('Codex 额度读取超时')), 20000);
164
+ const send = msg => proc.stdin.write(`${JSON.stringify(msg)}\n`);
165
+ proc.on('error', () => finish(new Error('Codex CLI 不可用')));
166
+ proc.stdin.on('error', () => finish(new Error('Codex CLI 连接失败')));
167
+ proc.stderr.on('data', () => {}); // Never expose CLI diagnostics or credentials.
168
+ proc.on('exit', () => { if (!settled) finish(new Error('Codex CLI 未返回额度,请检查登录或升级 CLI')); });
169
+ proc.stdout.on('data', chunk => {
170
+ buffer += chunk.toString();
171
+ if (buffer.length > 1024 * 1024) return finish(new Error('Codex 额度响应过大'));
172
+ let index;
173
+ while ((index = buffer.indexOf('\n')) >= 0) {
174
+ const line = buffer.slice(0, index); buffer = buffer.slice(index + 1);
175
+ let msg; try { msg = JSON.parse(line); } catch { continue; }
176
+ if (msg.id === 1) {
177
+ if (msg.error) return finish(new Error('Codex CLI 初始化失败'));
178
+ send({ method: 'initialized' });
179
+ send({ id: 2, method: 'account/rateLimits/read' });
180
+ } else if (msg.id === 2) {
181
+ if (msg.error) return finish(new Error('Codex 未提供订阅额度,请检查 ChatGPT 登录;API Key 模式不支持此额度'));
182
+ finish(null, msg.result);
183
+ }
184
+ }
185
+ });
186
+ send({ id: 1, method: 'initialize', params: { clientInfo: { name: 'claude_simple_quota', version: '1.0.0' } } });
187
+ });
188
+ }
189
+
190
+ async function grokQuota(grokHome) {
191
+ let auth;
192
+ try {
193
+ const store = JSON.parse(await fs.readFile(path.join(grokHome, 'auth.json'), 'utf8'));
194
+ auth = Object.values(store).find(row => row?.auth_mode === 'oidc' && typeof row.key === 'string');
195
+ } catch {}
196
+ if (!auth) return { status: 'unavailable', message: '未找到 Grok 订阅登录;API Key 模式没有个人周额度', meters: [] };
197
+ const data = await getJSON('https://cli-chat-proxy.grok.com/v1/billing?format=credits', {
198
+ Authorization: `Bearer ${auth.key}`, 'x-userid': auth.user_id, 'x-grok-client-version': '1.0.46', 'X-XAI-Token-Auth': 'xai-grok-cli',
199
+ });
200
+ return { meters: normalizeGrokQuota(data), plan: data.subscriptionTier || null };
201
+ }
202
+
203
+ export function createQuotaReader({ claudePath = 'claude', codexPath, codexHome, grokHome, defaultClaudeDir, allowedClaudeDirs }) {
204
+ const cache = new Map();
205
+ return async ({ agent, configDir } = {}) => {
206
+ const dir = configDir || defaultClaudeDir;
207
+ if (agent === 'claude' && !allowedClaudeDirs().includes(dir)) throw new Error('Unknown Claude config directory');
208
+ const key = `${agent}:${agent === 'claude' ? dir : ''}`;
209
+ const cached = cache.get(key);
210
+ if (cached && Date.now() < cached.expiresAt) return cached.promise;
211
+ const promise = (async () => {
212
+ let result;
213
+ try {
214
+ if (agent === 'claude') result = await claudeQuota(dir, claudePath);
215
+ else if (agent === 'codex') result = { meters: normalizeCodexQuota(await readCodexLimits(codexPath, codexHome)) };
216
+ else if (agent === 'grok') result = await grokQuota(grokHome);
217
+ else throw new Error('Unsupported agent');
218
+ return { agent, status: result.meters.length ? 'ok' : 'unavailable',
219
+ ...(result.meters.length ? {} : { message: '当前账号未返回额度数据' }), ...result, fetchedAt: Date.now() };
220
+ } catch (err) {
221
+ return { agent, status: 'error', message: err.status ? err.message : '无法读取额度,请检查机器上的 CLI 登录和网络', meters: [], fetchedAt: Date.now() };
222
+ }
223
+ })();
224
+ // Shared per account, including failures, to avoid vendor endpoint limits.
225
+ cache.set(key, { promise, expiresAt: Date.now() + 5 * 60 * 1000 });
226
+ return promise;
227
+ };
228
+ }
File without changes
@@ -1,4 +1,5 @@
1
1
  import { spawn } from 'child_process';
2
+ import { spawnAgentOpts } from '../platform.js';
2
3
  import readline from 'readline';
3
4
  import crypto from 'crypto';
4
5
 
@@ -94,6 +95,7 @@ export function runCodex(command, options, send, { cliPath, activeSessions }) {
94
95
  cwd: options.cwd || process.cwd(),
95
96
  env,
96
97
  stdio: ['pipe', 'pipe', 'pipe'],
98
+ ...spawnAgentOpts(),
97
99
  });
98
100
  } catch (e) {
99
101
  send({ type: 'error', message: `Failed to start codex: ${e.message}`, agent: 'codex' });
@@ -50,12 +50,13 @@ function toolBlocks(message) {
50
50
  export function runCursor(command, options, send, { cliPath, cursorHome, activeSessions }) {
51
51
  return new Promise(resolve => {
52
52
  const sessionKey = options.sessionId || crypto.randomUUID();
53
- const home = cursorHome || path.join(process.env.HOME || '', '.cursor');
53
+ const home = cursorHome || path.join(process.env.HOME || process.env.USERPROFILE || '', '.cursor');
54
54
  const args = buildArgs(command, options);
55
55
  let child;
56
56
  try {
57
57
  child = spawn(cliPath || 'cursor-agent', args, {
58
58
  cwd: options.cwd || process.cwd(), env: { ...process.env }, stdio: ['ignore', 'pipe', 'pipe'],
59
+ windowsHide: process.platform === 'win32',
59
60
  });
60
61
  } catch (error) {
61
62
  send({ type: 'error', message: `Failed to start cursor-agent: ${error.message}`, agent: 'cursor' });
@@ -51,7 +51,7 @@ function toolInput(update) {
51
51
  export function runGemini(command, options, send, { cliPath, geminiHome, activeSessions }) {
52
52
  return new Promise(resolve => {
53
53
  const sessionKey = options.sessionId || crypto.randomUUID();
54
- const home = geminiHome || path.join(process.env.HOME || '', '.gemini');
54
+ const home = geminiHome || path.join(process.env.HOME || process.env.USERPROFILE || '', '.gemini');
55
55
  const args = buildArgs(command, options);
56
56
  // A resumed native conversation may predate the UI journal. Start recording
57
57
  // from this turn so its subsequent history/search/usage is still complete.
@@ -68,6 +68,7 @@ export function runGemini(command, options, send, { cliPath, geminiHome, activeS
68
68
  cwd: options.cwd || process.cwd(),
69
69
  env: { ...process.env },
70
70
  stdio: ['ignore', 'pipe', 'pipe'],
71
+ windowsHide: process.platform === 'win32',
71
72
  });
72
73
  } catch (error) {
73
74
  send({ type: 'error', message: `Failed to start agy: ${error.message}`, agent: 'gemini' });
@@ -1,4 +1,5 @@
1
1
  import { spawn } from 'child_process';
2
+ import { spawnAgentOpts } from '../platform.js';
2
3
  import fs from 'fs';
3
4
  import path from 'path';
4
5
  import readline from 'readline';
@@ -99,6 +100,7 @@ class TerminalHost {
99
100
  cwd,
100
101
  env,
101
102
  stdio: ['ignore', 'pipe', 'pipe'],
103
+ ...spawnAgentOpts(),
102
104
  shell: args.length === 0,
103
105
  });
104
106
  } catch (error) {
@@ -215,7 +217,10 @@ class GrokACPClient {
215
217
  if (model) args.push('--model', model);
216
218
  if (effort) args.push('--reasoning-effort', effort);
217
219
  args.push('stdio');
218
- this.child = spawn(cliPath || 'grok', args, { stdio: ['pipe', 'pipe', 'pipe'] });
220
+ this.child = spawn(cliPath || 'grok', args, {
221
+ stdio: ['pipe', 'pipe', 'pipe'],
222
+ ...spawnAgentOpts(),
223
+ });
219
224
  this.pending = new Map();
220
225
  this.listeners = new Map();
221
226
  this.sessionCwd = new Map();
@@ -26,6 +26,7 @@ import path from 'path';
26
26
  import os from 'os';
27
27
  import crypto from 'crypto';
28
28
  import { spawn } from 'child_process';
29
+ import { expandHome, shellExecSpec, spawnCommandOpts } from '../platform.js';
29
30
 
30
31
  export const AI_DEFAULTS = {
31
32
  url: 'https://api.xiaomimimo.com/v1/chat/completions',
@@ -443,7 +444,7 @@ const TOOLS = [
443
444
 
444
445
  function expandPath(p) {
445
446
  if (!p) return p;
446
- return String(p).replace(/^~(?=$|[/\\])/, os.homedir());
447
+ return expandHome(String(p));
447
448
  }
448
449
 
449
450
  function pad(n) {
@@ -1919,7 +1920,8 @@ export function createMetaAgent(deps) {
1919
1920
  const p = expandPath(args.path);
1920
1921
  if (!path.isAbsolute(p)) return { error: 'path must be absolute' };
1921
1922
  // block writing into sensitive system dirs
1922
- if (p === '/' || p.startsWith('/etc') || p.startsWith('/usr') || p.startsWith('/bin')) {
1923
+ if (p === '/' || p.startsWith('/etc') || p.startsWith('/usr') || p.startsWith('/bin')
1924
+ || /^[a-zA-Z]:\\(Windows|Program Files)/i.test(p)) {
1923
1925
  return { error: 'refusing to write to system path' };
1924
1926
  }
1925
1927
  await fs.mkdir(path.dirname(p), { recursive: true });
@@ -1933,10 +1935,11 @@ export function createMetaAgent(deps) {
1933
1935
  const cwd = expandPath(args.cwd || os.homedir());
1934
1936
  const timeoutMs = Math.min(Math.max(args.timeoutMs || 60000, 1000), 180000);
1935
1937
  return await new Promise((resolve) => {
1936
- const child = spawn(process.env.SHELL || 'bash', ['-c', args.command], {
1938
+ const spec = shellExecSpec(args.command);
1939
+ const child = spawn(spec.bin, spec.args, {
1937
1940
  cwd: fsSync.existsSync(cwd) ? cwd : os.homedir(),
1938
1941
  env: { ...process.env },
1939
- shell: false,
1942
+ ...spawnCommandOpts({ extra: { shell: false } }),
1940
1943
  });
1941
1944
  let stdout = '';
1942
1945
  let stderr = '';
@@ -11,12 +11,12 @@ import { pickAllowedEnv, loadAgentEnvFile, readSettingsJsonEnv } from '../agent-
11
11
  /** Static fallbacks when local cache/config is missing. */
12
12
  export const FALLBACK_MODELS = {
13
13
  claude: {
14
- model: 'claude-sonnet-4-5',
14
+ model: 'sonnet',
15
15
  models: [
16
- { value: 'claude-sonnet-4-5', label: 'sonnet-4-5' },
17
- { value: 'claude-sonnet-4-6', label: 'sonnet-4-6' },
18
- { value: 'claude-opus-4-5', label: 'opus-4-5' },
19
- { value: 'claude-haiku-4-5', label: 'haiku-4-5' },
16
+ { value: 'fable', label: 'fable' },
17
+ { value: 'opus', label: 'opus' },
18
+ { value: 'sonnet', label: 'sonnet' },
19
+ { value: 'haiku', label: 'haiku' },
20
20
  ],
21
21
  },
22
22
  codex: {
@@ -228,14 +228,22 @@ async function discoverClaude(claudeConfigDirs = []) {
228
228
  }
229
229
  }
230
230
 
231
- let models = uniqModels(found);
232
- let source = 'fallback';
233
- if (models.length) {
234
- source = envMerged.ANTHROPIC_BASE_URL ? 'anthropic-compat' : 'settings';
235
- // Custom gateway / third-party models: list only what is configured
231
+ // Third-party gateways only have the models named in env/settings.
232
+ // Official Claude Code accepts session aliases (fable, opus, sonnet, haiku)
233
+ // or a full model id; settings.model stays the default, not the whole list.
234
+ const customGateway = !!envMerged.ANTHROPIC_BASE_URL;
235
+ let models;
236
+ let source;
237
+ if (customGateway && found.length) {
238
+ models = uniqModels(found);
239
+ source = 'anthropic-compat';
236
240
  } else {
237
- models = FALLBACK_MODELS.claude.models.map(m => ({ ...m }));
238
- defaultModel = FALLBACK_MODELS.claude.model;
241
+ models = uniqModels([
242
+ ...FALLBACK_MODELS.claude.models.map(m => ({ ...m })),
243
+ ...found,
244
+ ]);
245
+ source = found.length ? 'alias+settings' : 'alias';
246
+ if (!defaultModel) defaultModel = FALLBACK_MODELS.claude.model;
239
247
  }
240
248
 
241
249
  if (!defaultModel) defaultModel = models[0]?.value || FALLBACK_MODELS.claude.model;
@@ -381,6 +389,7 @@ async function listAgyModels(cliPath) {
381
389
  // agy renders its model picker through a TTY. `script` supplies a tiny
382
390
  // pseudo-terminal while keeping this request non-interactive.
383
391
  const command = cliPath || 'agy';
392
+ if (process.platform === 'win32') return [];
384
393
  const scriptArgs = process.platform === 'darwin'
385
394
  ? ['-q', '/dev/null', command, 'models']
386
395
  : ['-q', '-c', `${command} models`, '/dev/null'];