claude-simple 0.2.23 → 0.2.25
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 +20 -0
- package/client.js +20 -3
- package/dist/app.0.2.25.min.js +1164 -0
- package/dist/index.html +2 -2
- package/dist/style.0.2.25.min.css +1753 -0
- package/package.json +1 -1
- package/public/api.js +1 -0
- package/public/chat.js +213 -8
- package/public/router.js +5 -0
- package/public/settings.js +312 -63
- package/public/shell/notes.js +5 -2
- package/public/shell/session-list.js +50 -2
- package/public/shell/session-nav.js +45 -12
- package/public/state.js +3 -2
- package/public/style.css +126 -0
- 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 +77 -8
- package/src/agents/models.js +30 -1
- package/src/agents/sessions.js +27 -10
- package/src/app.js +450 -14
- package/src/collab.js +248 -0
- package/src/runtime/external-session-sync.js +106 -0
- package/src/runtime/protocol.js +3 -2
- package/dist/app.0.2.23.min.js +0 -1104
- package/dist/style.0.2.23.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,10 +10,64 @@ 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
|
}
|
|
16
40
|
|
|
41
|
+
// JSON-RPC 2.0 error.code must be an integer. Node fs/spawn errors use string
|
|
42
|
+
// codes like "ENOENT"; Grok's Rust client rejects those and logs
|
|
43
|
+
// `invalid type: string "ENOENT", expected i32`.
|
|
44
|
+
export function jsonRpcErrorCode(error) {
|
|
45
|
+
const code = error?.code;
|
|
46
|
+
if (typeof code === 'number' && Number.isInteger(code)) return code;
|
|
47
|
+
return -32000;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** ACP paths must be absolute; resolve relatives against the session cwd, not the edge process cwd. */
|
|
51
|
+
export function resolveFsPath(filePath, cwd) {
|
|
52
|
+
const raw = String(filePath || '').trim();
|
|
53
|
+
if (!raw) {
|
|
54
|
+
const err = new Error('path is required');
|
|
55
|
+
err.code = -32602;
|
|
56
|
+
throw err;
|
|
57
|
+
}
|
|
58
|
+
return path.resolve(cwd || process.cwd(), raw);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function selectPermissionOption(options) {
|
|
62
|
+
const list = Array.isArray(options) ? options : [];
|
|
63
|
+
const allow = list.find(o => o?.kind === 'allow_always')
|
|
64
|
+
|| list.find(o => o?.kind === 'allow_once')
|
|
65
|
+
|| list.find(o => typeof o?.kind === 'string' && String(o.kind).startsWith('allow'))
|
|
66
|
+
|| list[0];
|
|
67
|
+
if (!allow?.optionId) return { outcome: 'cancelled' };
|
|
68
|
+
return { outcome: 'selected', optionId: allow.optionId };
|
|
69
|
+
}
|
|
70
|
+
|
|
17
71
|
function envFromParams(envList) {
|
|
18
72
|
const env = { ...process.env };
|
|
19
73
|
if (!Array.isArray(envList)) return env;
|
|
@@ -164,6 +218,7 @@ class GrokACPClient {
|
|
|
164
218
|
this.child = spawn(cliPath || 'grok', args, { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
165
219
|
this.pending = new Map();
|
|
166
220
|
this.listeners = new Map();
|
|
221
|
+
this.sessionCwd = new Map();
|
|
167
222
|
// High ids avoid colliding with agent-originated request ids (often 0..N).
|
|
168
223
|
this.nextID = 100000;
|
|
169
224
|
this.terminals = new TerminalHost();
|
|
@@ -198,13 +253,19 @@ class GrokACPClient {
|
|
|
198
253
|
return init;
|
|
199
254
|
}
|
|
200
255
|
|
|
256
|
+
fsCwd(params) {
|
|
257
|
+
return (params?.sessionId && this.sessionCwd.get(params.sessionId)) || process.cwd();
|
|
258
|
+
}
|
|
259
|
+
|
|
201
260
|
async handleClientRequest(msg) {
|
|
202
261
|
const method = msg.method;
|
|
203
262
|
const params = msg.params || {};
|
|
204
263
|
try {
|
|
205
264
|
let result = {};
|
|
206
|
-
if (method === '
|
|
207
|
-
|
|
265
|
+
if (method === 'session/request_permission') {
|
|
266
|
+
result = { outcome: selectPermissionOption(params.options) };
|
|
267
|
+
} else if (method === 'fs/read_text_file') {
|
|
268
|
+
const filePath = resolveFsPath(params.path, this.fsCwd(params));
|
|
208
269
|
const content = await fs.promises.readFile(filePath, 'utf8');
|
|
209
270
|
const limit = Number.isFinite(params.limit) ? params.limit : null;
|
|
210
271
|
const line = Number.isFinite(params.line) ? params.line : null;
|
|
@@ -217,11 +278,13 @@ class GrokACPClient {
|
|
|
217
278
|
result = { content };
|
|
218
279
|
}
|
|
219
280
|
} else if (method === 'fs/write_text_file') {
|
|
220
|
-
const filePath =
|
|
281
|
+
const filePath = resolveFsPath(params.path, this.fsCwd(params));
|
|
282
|
+
const content = params.content == null ? '' : String(params.content);
|
|
221
283
|
await fs.promises.mkdir(path.dirname(filePath), { recursive: true });
|
|
222
|
-
await fs.promises.writeFile(filePath,
|
|
284
|
+
await fs.promises.writeFile(filePath, content, 'utf8');
|
|
223
285
|
result = {};
|
|
224
286
|
} else if (method === 'terminal/create') {
|
|
287
|
+
if (!params.cwd) params = { ...params, cwd: this.fsCwd(params) };
|
|
225
288
|
result = this.terminals.create(params);
|
|
226
289
|
} else if (method === 'terminal/output') {
|
|
227
290
|
result = this.terminals.output(params.terminalId);
|
|
@@ -244,7 +307,10 @@ class GrokACPClient {
|
|
|
244
307
|
this.write({
|
|
245
308
|
jsonrpc: '2.0',
|
|
246
309
|
id: msg.id,
|
|
247
|
-
error: {
|
|
310
|
+
error: {
|
|
311
|
+
code: jsonRpcErrorCode(error),
|
|
312
|
+
message: error.message || 'Client request failed',
|
|
313
|
+
},
|
|
248
314
|
});
|
|
249
315
|
}
|
|
250
316
|
}
|
|
@@ -291,20 +357,23 @@ class GrokACPClient {
|
|
|
291
357
|
|
|
292
358
|
async ensureSession(options) {
|
|
293
359
|
await this.ready;
|
|
360
|
+
const cwd = options.cwd || process.cwd();
|
|
294
361
|
if (options.sessionId) {
|
|
295
362
|
try {
|
|
296
|
-
await this.request('session/load', { sessionId: options.sessionId, cwd
|
|
363
|
+
await this.request('session/load', { sessionId: options.sessionId, cwd, mcpServers: [] });
|
|
297
364
|
} catch {
|
|
298
|
-
await this.request('session/resume', { sessionId: options.sessionId, cwd
|
|
365
|
+
await this.request('session/resume', { sessionId: options.sessionId, cwd, mcpServers: [] });
|
|
299
366
|
}
|
|
367
|
+
this.sessionCwd.set(options.sessionId, cwd);
|
|
300
368
|
return options.sessionId;
|
|
301
369
|
}
|
|
302
370
|
const result = await this.request('session/new', {
|
|
303
|
-
cwd
|
|
371
|
+
cwd,
|
|
304
372
|
mcpServers: [],
|
|
305
373
|
_meta: { yoloMode: true },
|
|
306
374
|
});
|
|
307
375
|
if (!result.sessionId) throw new Error('Grok ACP did not return a session id');
|
|
376
|
+
this.sessionCwd.set(result.sessionId, cwd);
|
|
308
377
|
return result.sessionId;
|
|
309
378
|
}
|
|
310
379
|
}
|
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
|
@@ -910,12 +910,16 @@ function geminiJournalPath(sessionId, geminiHome) {
|
|
|
910
910
|
return path.join(geminiUiSessionsRoot(geminiHome), `${sessionId}.jsonl`);
|
|
911
911
|
}
|
|
912
912
|
|
|
913
|
-
// agy keeps
|
|
914
|
-
//
|
|
915
|
-
//
|
|
916
|
-
function
|
|
913
|
+
// agy keeps both a compact transport log and a lossless transcript. The
|
|
914
|
+
// compact log replaces large fields with `<truncated N bytes>`; always prefer
|
|
915
|
+
// the full file when it is available so chat history never loses message text.
|
|
916
|
+
function geminiNativeTranscriptPaths(sessionId, geminiHome) {
|
|
917
917
|
const home = geminiHome || path.join(os.homedir(), '.gemini');
|
|
918
|
-
|
|
918
|
+
const logs = path.join(home, 'antigravity-cli', 'brain', sessionId, '.system_generated', 'logs');
|
|
919
|
+
return [
|
|
920
|
+
path.join(logs, 'transcript_full.jsonl'),
|
|
921
|
+
path.join(logs, 'transcript.jsonl'),
|
|
922
|
+
];
|
|
919
923
|
}
|
|
920
924
|
|
|
921
925
|
function geminiPathFromUri(uri) {
|
|
@@ -1039,8 +1043,10 @@ async function findGeminiJournal(sessionId, geminiHome) {
|
|
|
1039
1043
|
}
|
|
1040
1044
|
|
|
1041
1045
|
async function findGeminiNativeTranscript(sessionId, geminiHome) {
|
|
1042
|
-
const file
|
|
1043
|
-
|
|
1046
|
+
for (const file of geminiNativeTranscriptPaths(sessionId, geminiHome)) {
|
|
1047
|
+
try { await fs.access(file); return file; } catch { /* try the compact fallback */ }
|
|
1048
|
+
}
|
|
1049
|
+
return null;
|
|
1044
1050
|
}
|
|
1045
1051
|
|
|
1046
1052
|
function geminiUserText(content) {
|
|
@@ -1571,7 +1577,8 @@ export async function loadSessionJsonl(sessionId, agent, { claudeConfigDirs, cod
|
|
|
1571
1577
|
let file;
|
|
1572
1578
|
if (a === 'codex') file = await findCodexSessionFile(sessionId, codexHome);
|
|
1573
1579
|
else if (a === 'cursor') file = await findCursorJournal(sessionId, cursorHome);
|
|
1574
|
-
else if (a === 'gemini') file = await
|
|
1580
|
+
else if (a === 'gemini') file = await findGeminiNativeTranscript(sessionId, geminiHome)
|
|
1581
|
+
|| await findGeminiJournal(sessionId, geminiHome);
|
|
1575
1582
|
else if (a === 'grok') {
|
|
1576
1583
|
const dir = await findGrokSessionDir(sessionId, grokHome);
|
|
1577
1584
|
file = dir ? path.join(dir, 'chat_history.jsonl') : null;
|
|
@@ -1637,14 +1644,24 @@ export async function getSessionMeta(sessionId, agent, { claudeConfigDirs, codex
|
|
|
1637
1644
|
return { cwd, display };
|
|
1638
1645
|
}
|
|
1639
1646
|
|
|
1640
|
-
|
|
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 } = {}) {
|
|
1641
1654
|
// 1. Get meta and messages from source session
|
|
1642
1655
|
const meta = await getSessionMeta(sourceSessionId, sourceAgent, { claudeConfigDirs, codexHome, grokHome, geminiHome, cursorHome });
|
|
1643
1656
|
const cwd = meta.cwd || null;
|
|
1644
1657
|
const display = meta.display || 'Converted Session';
|
|
1645
1658
|
|
|
1646
1659
|
const bundle = await loadSessionMessages(sourceSessionId, sourceAgent, { claudeConfigDirs, codexHome, grokHome, geminiHome, cursorHome });
|
|
1647
|
-
|
|
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');
|
|
1648
1665
|
|
|
1649
1666
|
// 2. Generate target session ID
|
|
1650
1667
|
const targetSessionId = crypto.randomUUID();
|