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.
- package/README.md +17 -1
- package/bin/edge-ctl.js +151 -4
- package/client.js +8 -2
- package/dist/addon-fit.min.js +0 -0
- package/dist/app.0.2.31.min.js +1308 -0
- package/dist/index.html +2 -2
- package/dist/style.0.2.31.min.css +2012 -0
- package/dist/xterm.min.css +0 -0
- package/dist/xterm.min.js +0 -0
- package/gateway.js +0 -0
- package/package.json +2 -2
- package/public/addon-fit.min.js +0 -0
- package/public/agent-panel.js +47 -20
- package/public/api.js +1 -1
- package/public/app.js +5 -5
- package/public/base.css +0 -0
- package/public/cache.js +19 -0
- package/public/chat.js +80 -19
- package/public/files.js +26 -10
- package/public/git.js +2 -2
- package/public/icons/icon-180.png +0 -0
- package/public/icons/icon-192.png +0 -0
- package/public/icons/icon-512.png +0 -0
- package/public/icons/icon-maskable-512.png +0 -0
- package/public/index.html +2 -2
- package/public/lib.js +0 -0
- package/public/manifest.webmanifest +0 -0
- package/public/markdown.js +16 -0
- package/public/math.js +145 -7
- package/public/mobile.js +0 -0
- package/public/notifications.js +0 -0
- package/public/offline.html +0 -0
- package/public/pwa.js +1 -1
- package/public/router.js +0 -0
- package/public/scroll-follow.js +78 -0
- package/public/settings.js +41 -0
- package/public/shell/boot.js +0 -0
- package/public/shell/dashboard.js +0 -0
- package/public/shell/hub.js +0 -0
- package/public/shell/layout.js +0 -0
- package/public/shell/notes.js +0 -0
- package/public/shell/session-context.js +0 -0
- package/public/shell/session-list.js +49 -16
- package/public/shell/session-nav.js +66 -18
- package/public/shell/templates.js +0 -0
- package/public/shell/util.js +0 -0
- package/public/shell.js +0 -0
- package/public/state.js +7 -7
- package/public/style.css +5 -0
- package/public/sw.js +10 -5
- package/public/terminal.js +0 -0
- package/public/wake-lock.js +0 -0
- package/public/workspace-path.js +61 -0
- package/public/xterm.min.css +0 -0
- package/public/xterm.min.js +0 -0
- package/src/agent-quota.js +228 -0
- package/src/agents/claude.js +0 -0
- package/src/agents/codex.js +2 -0
- package/src/agents/cursor.js +2 -1
- package/src/agents/gemini.js +2 -1
- package/src/agents/grok.js +6 -1
- package/src/agents/meta-agent.js +7 -4
- package/src/agents/models.js +21 -12
- package/src/agents/sessions.js +133 -69
- package/src/agents/stream-batch.js +0 -0
- package/src/app.js +80 -36
- package/src/auto-update.js +2 -0
- package/src/collab.js +3 -0
- package/src/platform.js +133 -0
- package/src/project-api-path.js +14 -0
- package/src/runtime/approvals.js +0 -0
- package/src/runtime/chat-broadcast.js +12 -0
- package/src/runtime/outbox.js +0 -0
- package/src/runtime/port-websocket.js +80 -0
- package/src/runtime/protocol.js +0 -0
- package/dist/app.0.2.28.min.js +0 -1220
- 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('
|
|
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: '
|
|
168
|
-
{ value: '
|
|
169
|
-
{ value: '
|
|
170
|
-
{ value: '
|
|
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: '
|
|
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 || '
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
-
//
|
|
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
|
}
|
package/public/terminal.js
CHANGED
|
File without changes
|
package/public/wake-lock.js
CHANGED
|
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
|
+
}
|
package/public/xterm.min.css
CHANGED
|
File without changes
|
package/public/xterm.min.js
CHANGED
|
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
|
+
}
|
package/src/agents/claude.js
CHANGED
|
File without changes
|
package/src/agents/codex.js
CHANGED
|
@@ -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' });
|
package/src/agents/cursor.js
CHANGED
|
@@ -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' });
|
package/src/agents/gemini.js
CHANGED
|
@@ -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' });
|
package/src/agents/grok.js
CHANGED
|
@@ -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, {
|
|
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();
|
package/src/agents/meta-agent.js
CHANGED
|
@@ -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)
|
|
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
|
|
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 = '';
|
package/src/agents/models.js
CHANGED
|
@@ -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: '
|
|
14
|
+
model: 'sonnet',
|
|
15
15
|
models: [
|
|
16
|
-
{ value: '
|
|
17
|
-
{ value: '
|
|
18
|
-
{ value: '
|
|
19
|
-
{ value: '
|
|
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
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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 =
|
|
238
|
-
|
|
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'];
|