reslop 0.3.1 → 0.3.2
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/CHANGELOG.md +16 -1
- package/README.md +21 -3
- package/lib/agents/acp.js +309 -0
- package/lib/agents/agents.js +163 -436
- package/lib/agents/allow.js +141 -52
- package/lib/agents/allow.json +16 -14
- package/lib/agents/models.js +250 -383
- package/lib/agents/registry.js +437 -0
- package/lib/agents/run.js +234 -0
- package/lib/agents/transcript.js +143 -0
- package/lib/cli.js +2 -1
- package/lib/common/process.js +26 -1
- package/lib/dashboard/git.js +11 -1
- package/lib/dashboard/reports.js +80 -0
- package/lib/dashboard/watch.js +43 -8
- package/lib/dashboard/workspace.js +19 -20
- package/lib/dashboard/worktrees.js +145 -0
- package/lib/git/dir.js +50 -0
- package/lib/git/linked.js +152 -0
- package/lib/git/local.js +179 -0
- package/lib/git/status.js +126 -0
- package/lib/git/worktree.js +113 -36
- package/lib/input/actions.js +83 -5
- package/lib/input/keys.js +30 -1
- package/lib/input/line-edit.js +5 -1
- package/lib/render/agents.js +50 -24
- package/lib/render/branches.js +55 -27
- package/lib/render/field.js +19 -5
- package/lib/render/footer.js +14 -6
- package/lib/render/frame.js +16 -2
- package/lib/render/menu.js +103 -6
- package/lib/render/name-edit.js +31 -0
- package/lib/render/repos.js +52 -8
- package/lib/render/status.js +22 -4
- package/lib/report/run.js +16 -23
- package/lib/review/agent-instructions.md +2 -2
- package/lib/runs/process.js +3 -3
- package/lib/runs/runs.js +27 -4
- package/lib/session/agents/agents.js +220 -205
- package/lib/session/agents/away.js +48 -0
- package/lib/session/agents/job.js +23 -86
- package/lib/session/agents/jobs.js +14 -27
- package/lib/session/agents/options.js +45 -101
- package/lib/session/agents/plans.js +53 -0
- package/lib/session/agents/view.js +34 -0
- package/lib/session/branches.js +18 -1
- package/lib/session/compose.js +13 -5
- package/lib/session/dispatch.js +43 -4
- package/lib/session/editor-pointer.js +98 -0
- package/lib/session/files-pane.js +3 -0
- package/lib/session/lifecycle.js +67 -10
- package/lib/session/navigation.js +5 -0
- package/lib/session/npm/history.js +2 -1
- package/lib/session/npm/npm.js +89 -12
- package/lib/session/ops.js +3 -0
- package/lib/session/pointer.js +304 -46
- package/lib/session/session.js +15 -2
- package/lib/session/terminal.js +2 -2
- package/lib/session/text-menu.js +50 -0
- package/lib/session/view.js +19 -2
- package/lib/session/workspace.js +179 -38
- package/lib/session/worktrees.js +280 -0
- package/lib/term/themes/dark.json +4 -0
- package/lib/term/themes/light.json +4 -0
- package/package.json +2 -2
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const { Readable, Transform } = require('node:stream');
|
|
6
|
+
const { pipeline } = require('node:stream/promises');
|
|
7
|
+
const zlib = require('node:zlib');
|
|
8
|
+
|
|
9
|
+
const REGISTRY_URL =
|
|
10
|
+
'https://cdn.agentclientprotocol.com/registry/v1/latest/registry.json';
|
|
11
|
+
const INSTALLERS = {
|
|
12
|
+
'antigravity-acp': {
|
|
13
|
+
host: 'dl.google.com',
|
|
14
|
+
path: '/agy-extensions/releases/',
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
const ZIP_LOCAL = 0x04034b50;
|
|
18
|
+
const ZIP_CENTRAL = 0x02014b50;
|
|
19
|
+
const ZIP_END = 0x06054b50;
|
|
20
|
+
const ZIP_TAIL = 65557;
|
|
21
|
+
const CENTRAL_LIMIT = 64 * 1024 * 1024;
|
|
22
|
+
const EXPANDED_LIMIT = 4 * 1024 * 1024 * 1024;
|
|
23
|
+
|
|
24
|
+
const platformTarget = (platform = process.platform, arch = process.arch) => {
|
|
25
|
+
const osName = { darwin: 'darwin', linux: 'linux', win32: 'windows' }[
|
|
26
|
+
platform
|
|
27
|
+
];
|
|
28
|
+
const archName = { x64: 'x86_64', arm64: 'aarch64' }[arch];
|
|
29
|
+
return osName && archName ? `${osName}-${archName}` : '';
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const dataHome = (env = process.env, platform = process.platform) => {
|
|
33
|
+
if (env.RESLOP_AGENTS_DIR) return path.resolve(env.RESLOP_AGENTS_DIR);
|
|
34
|
+
const home = env.HOME || env.USERPROFILE;
|
|
35
|
+
if (!home) return '';
|
|
36
|
+
if (platform === 'win32') {
|
|
37
|
+
const base = env.LOCALAPPDATA || path.join(home, 'AppData', 'Local');
|
|
38
|
+
return path.join(base, 'reslop', 'agents');
|
|
39
|
+
}
|
|
40
|
+
if (platform === 'darwin') {
|
|
41
|
+
return path.join(
|
|
42
|
+
home,
|
|
43
|
+
'Library',
|
|
44
|
+
'Application Support',
|
|
45
|
+
'reslop',
|
|
46
|
+
'agents',
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
const base = env.XDG_DATA_HOME || path.join(home, '.local', 'share');
|
|
50
|
+
return path.join(base, 'reslop', 'agents');
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const agentRoot = (id, options = {}) => {
|
|
54
|
+
const base = dataHome(options.env, options.platform);
|
|
55
|
+
return base ? path.join(base, id) : '';
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const executable = (file, platform = process.platform) => {
|
|
59
|
+
try {
|
|
60
|
+
if (!fs.statSync(file).isFile()) return false;
|
|
61
|
+
const unixHost = process.platform !== 'win32';
|
|
62
|
+
const unixFile = platform !== 'win32';
|
|
63
|
+
if (unixHost && unixFile) fs.accessSync(file, fs.constants.X_OK);
|
|
64
|
+
return true;
|
|
65
|
+
} catch {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const inside = (root, file) => {
|
|
71
|
+
const rel = path.relative(root, file);
|
|
72
|
+
return rel && !rel.startsWith('..') && !path.isAbsolute(rel);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const readReceipt = (id, options = {}) => {
|
|
76
|
+
const root = agentRoot(id, options);
|
|
77
|
+
if (!root) return null;
|
|
78
|
+
try {
|
|
79
|
+
const file = path.join(root, 'current.json');
|
|
80
|
+
const receipt = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
81
|
+
const bin = path.resolve(root, `${receipt.command ?? ''}`);
|
|
82
|
+
if (!inside(root, bin) || !executable(bin, options.platform)) return null;
|
|
83
|
+
return { bin, args: Array.isArray(receipt.args) ? receipt.args : [] };
|
|
84
|
+
} catch {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const zedRoot = (id, options = {}) => {
|
|
90
|
+
const env = options.env ?? process.env;
|
|
91
|
+
const platform = options.platform ?? process.platform;
|
|
92
|
+
const home = env.HOME || env.USERPROFILE;
|
|
93
|
+
if (!home) return '';
|
|
94
|
+
let base;
|
|
95
|
+
if (platform === 'win32') {
|
|
96
|
+
base = env.LOCALAPPDATA || path.join(home, 'AppData', 'Local');
|
|
97
|
+
} else if (platform === 'darwin') {
|
|
98
|
+
base = path.join(home, 'Library', 'Application Support');
|
|
99
|
+
} else {
|
|
100
|
+
base = env.XDG_DATA_HOME || path.join(home, '.local', 'share');
|
|
101
|
+
}
|
|
102
|
+
return path.join(base, 'zed', 'external_agents', 'registry', id);
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const serverName = (platform = process.platform) =>
|
|
106
|
+
platform === 'win32' ? 'agy_acp_server.exe' : 'agy_acp_server.par';
|
|
107
|
+
|
|
108
|
+
const registryArgs = (platform = process.platform) =>
|
|
109
|
+
platform === 'linux' ? ['--uid='] : [];
|
|
110
|
+
|
|
111
|
+
const findBelow = (root, name, depth = 2) => {
|
|
112
|
+
if (!root || depth < 0) return '';
|
|
113
|
+
let entries;
|
|
114
|
+
try {
|
|
115
|
+
entries = fs.readdirSync(root, { withFileTypes: true });
|
|
116
|
+
} catch {
|
|
117
|
+
return '';
|
|
118
|
+
}
|
|
119
|
+
entries.sort((left, right) => right.name.localeCompare(left.name));
|
|
120
|
+
for (const entry of entries) {
|
|
121
|
+
const full = path.join(root, entry.name);
|
|
122
|
+
if (entry.isFile() && entry.name === name && executable(full)) return full;
|
|
123
|
+
}
|
|
124
|
+
for (const entry of entries) {
|
|
125
|
+
if (!entry.isDirectory()) continue;
|
|
126
|
+
const found = findBelow(path.join(root, entry.name), name, depth - 1);
|
|
127
|
+
if (found) return found;
|
|
128
|
+
}
|
|
129
|
+
return '';
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const installedRegistryAgent = (id, options = {}) => {
|
|
133
|
+
const installed = readReceipt(id, options);
|
|
134
|
+
if (installed) return installed;
|
|
135
|
+
if (id !== 'antigravity-acp') return null;
|
|
136
|
+
const platform = options.platform ?? process.platform;
|
|
137
|
+
const bin = findBelow(zedRoot(id, options), serverName(platform));
|
|
138
|
+
return bin ? { bin, args: registryArgs(platform) } : null;
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const registryEntry = (manifest, id, options = {}) => {
|
|
142
|
+
const installer = INSTALLERS[id];
|
|
143
|
+
if (!installer) throw new Error(`unsupported registry agent ${id}`);
|
|
144
|
+
const target = platformTarget(options.platform, options.arch);
|
|
145
|
+
if (!target) throw new Error('platform is not supported');
|
|
146
|
+
const agent = (manifest.agents ?? []).find((item) => item.id === id);
|
|
147
|
+
if (!agent) throw new Error(`${id} is not in the ACP registry`);
|
|
148
|
+
const binary = agent.distribution?.binary?.[target];
|
|
149
|
+
if (!binary) throw new Error(`${id} is not available for ${target}`);
|
|
150
|
+
const url = new URL(binary.archive);
|
|
151
|
+
if (
|
|
152
|
+
url.protocol !== 'https:' ||
|
|
153
|
+
url.hostname !== installer.host ||
|
|
154
|
+
!url.pathname.startsWith(installer.path) ||
|
|
155
|
+
!url.pathname.endsWith('.zip')
|
|
156
|
+
) {
|
|
157
|
+
throw new Error(`unexpected archive for ${id}`);
|
|
158
|
+
}
|
|
159
|
+
const version = `${agent.version ?? ''}`;
|
|
160
|
+
if (!/^\d+\.\d+\.\d+(?:[-+][\w.-]+)?$/.test(version)) {
|
|
161
|
+
throw new Error(`invalid version for ${id}`);
|
|
162
|
+
}
|
|
163
|
+
const command = `${binary.cmd ?? ''}`.replace(/^\.\//, '');
|
|
164
|
+
if (!command || path.isAbsolute(command) || command.includes('..')) {
|
|
165
|
+
throw new Error(`invalid command for ${id}`);
|
|
166
|
+
}
|
|
167
|
+
return {
|
|
168
|
+
version,
|
|
169
|
+
archive: url.href,
|
|
170
|
+
command,
|
|
171
|
+
args: Array.isArray(binary.args) ? binary.args.map(String) : [],
|
|
172
|
+
};
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const readAt = async (handle, length, position) => {
|
|
176
|
+
const buffer = Buffer.alloc(length);
|
|
177
|
+
const { bytesRead } = await handle.read(buffer, 0, length, position);
|
|
178
|
+
if (bytesRead !== length) throw new Error('truncated zip archive');
|
|
179
|
+
return buffer;
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const endOffset = (tail) => {
|
|
183
|
+
for (let at = tail.length - 22; at >= 0; at -= 1) {
|
|
184
|
+
if (tail.readUInt32LE(at) === ZIP_END) return at;
|
|
185
|
+
}
|
|
186
|
+
return -1;
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const centralEntries = async (handle) => {
|
|
190
|
+
const { size } = await handle.stat();
|
|
191
|
+
const tailSize = Math.min(size, ZIP_TAIL);
|
|
192
|
+
const tail = await readAt(handle, tailSize, size - tailSize);
|
|
193
|
+
const at = endOffset(tail);
|
|
194
|
+
if (at < 0) throw new Error('zip directory not found');
|
|
195
|
+
const count = tail.readUInt16LE(at + 10);
|
|
196
|
+
const centralSize = tail.readUInt32LE(at + 12);
|
|
197
|
+
const centralAt = tail.readUInt32LE(at + 16);
|
|
198
|
+
if (count === 0xffff || centralAt === 0xffffffff) {
|
|
199
|
+
throw new Error('zip64 archives are not supported');
|
|
200
|
+
}
|
|
201
|
+
if (centralSize > CENTRAL_LIMIT || centralAt + centralSize > size) {
|
|
202
|
+
throw new Error('invalid zip directory');
|
|
203
|
+
}
|
|
204
|
+
const data = await readAt(handle, centralSize, centralAt);
|
|
205
|
+
const entries = [];
|
|
206
|
+
let cursor = 0;
|
|
207
|
+
let expanded = 0;
|
|
208
|
+
while (cursor < data.length && entries.length < count) {
|
|
209
|
+
if (data.readUInt32LE(cursor) !== ZIP_CENTRAL) {
|
|
210
|
+
throw new Error('invalid zip entry');
|
|
211
|
+
}
|
|
212
|
+
const nameLength = data.readUInt16LE(cursor + 28);
|
|
213
|
+
const extraLength = data.readUInt16LE(cursor + 30);
|
|
214
|
+
const commentLength = data.readUInt16LE(cursor + 32);
|
|
215
|
+
const end = cursor + 46 + nameLength + extraLength + commentLength;
|
|
216
|
+
if (end > data.length) throw new Error('truncated zip entry');
|
|
217
|
+
const name = data.toString('utf8', cursor + 46, cursor + 46 + nameLength);
|
|
218
|
+
const entry = {
|
|
219
|
+
flags: data.readUInt16LE(cursor + 8),
|
|
220
|
+
method: data.readUInt16LE(cursor + 10),
|
|
221
|
+
crc: data.readUInt32LE(cursor + 16),
|
|
222
|
+
compressed: data.readUInt32LE(cursor + 20),
|
|
223
|
+
size: data.readUInt32LE(cursor + 24),
|
|
224
|
+
attrs: data.readUInt32LE(cursor + 38),
|
|
225
|
+
offset: data.readUInt32LE(cursor + 42),
|
|
226
|
+
name,
|
|
227
|
+
};
|
|
228
|
+
expanded += entry.size;
|
|
229
|
+
if (expanded > EXPANDED_LIMIT) throw new Error('zip archive is too large');
|
|
230
|
+
entries.push(entry);
|
|
231
|
+
cursor = end;
|
|
232
|
+
}
|
|
233
|
+
if (entries.length !== count) throw new Error('incomplete zip directory');
|
|
234
|
+
return entries;
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const CRC_TABLE = Array.from({ length: 256 }, (_, index) => {
|
|
238
|
+
let value = index;
|
|
239
|
+
for (let bit = 0; bit < 8; bit += 1) {
|
|
240
|
+
value = value & 1 ? 0xedb88320 ^ (value >>> 1) : value >>> 1;
|
|
241
|
+
}
|
|
242
|
+
return value >>> 0;
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
const crcStream = (size, name) => {
|
|
246
|
+
let crc = 0xffffffff;
|
|
247
|
+
let written = 0;
|
|
248
|
+
const stream = new Transform({
|
|
249
|
+
transform(chunk, encoding, done) {
|
|
250
|
+
written += chunk.length;
|
|
251
|
+
if (written > size) {
|
|
252
|
+
done(new Error(`zip entry is too large ${name}`));
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
for (const byte of chunk) {
|
|
256
|
+
crc = CRC_TABLE[(crc ^ byte) & 0xff] ^ (crc >>> 8);
|
|
257
|
+
}
|
|
258
|
+
done(null, chunk);
|
|
259
|
+
},
|
|
260
|
+
});
|
|
261
|
+
stream.digest = () => (crc ^ 0xffffffff) >>> 0;
|
|
262
|
+
return stream;
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
const outputPath = (root, name) => {
|
|
266
|
+
if (!name || name.includes('\0')) throw new Error('invalid zip path');
|
|
267
|
+
const normalized = name.replaceAll('\\', '/');
|
|
268
|
+
if (normalized.startsWith('/') || /^[A-Za-z]:\//.test(normalized)) {
|
|
269
|
+
throw new Error('absolute zip path');
|
|
270
|
+
}
|
|
271
|
+
const file = path.resolve(root, normalized);
|
|
272
|
+
if (!inside(root, file)) throw new Error('zip path leaves install directory');
|
|
273
|
+
return file;
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
const extractEntry = async (archive, handle, root, entry) => {
|
|
277
|
+
const file = outputPath(root, entry.name);
|
|
278
|
+
if (entry.name.endsWith('/')) {
|
|
279
|
+
await fs.promises.mkdir(file, { recursive: true });
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
const type = (entry.attrs >>> 16) & 0o170000;
|
|
283
|
+
if (type === 0o120000) throw new Error('zip symlinks are not supported');
|
|
284
|
+
if (entry.flags & 1) {
|
|
285
|
+
throw new Error('encrypted zip entries are not supported');
|
|
286
|
+
}
|
|
287
|
+
if (entry.method !== 0 && entry.method !== 8) {
|
|
288
|
+
throw new Error(`unsupported zip compression ${entry.method}`);
|
|
289
|
+
}
|
|
290
|
+
const local = await readAt(handle, 30, entry.offset);
|
|
291
|
+
if (local.readUInt32LE(0) !== ZIP_LOCAL) {
|
|
292
|
+
throw new Error('invalid zip header');
|
|
293
|
+
}
|
|
294
|
+
const nameLength = local.readUInt16LE(26);
|
|
295
|
+
const extraLength = local.readUInt16LE(28);
|
|
296
|
+
const start = entry.offset + 30 + nameLength + extraLength;
|
|
297
|
+
await fs.promises.mkdir(path.dirname(file), { recursive: true });
|
|
298
|
+
if (entry.compressed === 0) {
|
|
299
|
+
if (entry.size !== 0 || entry.crc !== 0) {
|
|
300
|
+
throw new Error(`corrupt zip entry ${entry.name}`);
|
|
301
|
+
}
|
|
302
|
+
await fs.promises.writeFile(file, '');
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
const source = fs.createReadStream(archive, {
|
|
306
|
+
start,
|
|
307
|
+
end: start + entry.compressed - 1,
|
|
308
|
+
});
|
|
309
|
+
const crc = crcStream(entry.size, entry.name);
|
|
310
|
+
const output = fs.createWriteStream(file, { mode: 0o600 });
|
|
311
|
+
if (entry.method === 8) {
|
|
312
|
+
await pipeline(source, zlib.createInflateRaw(), crc, output);
|
|
313
|
+
} else {
|
|
314
|
+
await pipeline(source, crc, output);
|
|
315
|
+
}
|
|
316
|
+
const stat = await fs.promises.stat(file);
|
|
317
|
+
if (stat.size !== entry.size || crc.digest() !== entry.crc) {
|
|
318
|
+
throw new Error(`corrupt zip entry ${entry.name}`);
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
const extractZip = async (archive, root) => {
|
|
323
|
+
const handle = await fs.promises.open(archive, 'r');
|
|
324
|
+
try {
|
|
325
|
+
const entries = await centralEntries(handle);
|
|
326
|
+
for (const entry of entries) {
|
|
327
|
+
await extractEntry(archive, handle, root, entry);
|
|
328
|
+
}
|
|
329
|
+
} finally {
|
|
330
|
+
await handle.close();
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
const responseError = (response, label) => {
|
|
335
|
+
if (response && response.ok) return;
|
|
336
|
+
const status = response ? ` (${response.status})` : '';
|
|
337
|
+
throw new Error(`${label} failed${status}`);
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
const download = async (url, file, fetchImpl) => {
|
|
341
|
+
const response = await fetchImpl(url);
|
|
342
|
+
responseError(response, 'download');
|
|
343
|
+
if (!response.body) throw new Error('download returned no body');
|
|
344
|
+
const source = Readable.fromWeb(response.body);
|
|
345
|
+
await pipeline(source, fs.createWriteStream(file, { mode: 0o600 }));
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
const installRegistryAgent = async (id, options = {}) => {
|
|
349
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
350
|
+
if (typeof fetchImpl !== 'function') {
|
|
351
|
+
throw new Error('fetch is not available');
|
|
352
|
+
}
|
|
353
|
+
const env = options.env ?? process.env;
|
|
354
|
+
const platform = options.platform ?? process.platform;
|
|
355
|
+
const arch = options.arch ?? process.arch;
|
|
356
|
+
const log = options.log ?? (() => {});
|
|
357
|
+
log('Checking the ACP registry…');
|
|
358
|
+
const manifestResponse = await fetchImpl(REGISTRY_URL);
|
|
359
|
+
responseError(manifestResponse, 'ACP registry');
|
|
360
|
+
const entry = registryEntry(await manifestResponse.json(), id, {
|
|
361
|
+
platform,
|
|
362
|
+
arch,
|
|
363
|
+
});
|
|
364
|
+
const root = agentRoot(id, { env, platform });
|
|
365
|
+
if (!root) throw new Error('cannot find the user data directory');
|
|
366
|
+
const destination = path.join(root, entry.version);
|
|
367
|
+
const command = path.join(destination, entry.command);
|
|
368
|
+
if (!executable(command, platform)) {
|
|
369
|
+
await fs.promises.mkdir(root, { recursive: true });
|
|
370
|
+
const stage = await fs.promises.mkdtemp(path.join(root, '.install-'));
|
|
371
|
+
try {
|
|
372
|
+
const archive = path.join(stage, 'agent.zip');
|
|
373
|
+
const expanded = path.join(stage, 'expanded');
|
|
374
|
+
await fs.promises.mkdir(expanded);
|
|
375
|
+
log(`Downloading Google Antigravity ACP ${entry.version}…`);
|
|
376
|
+
await download(entry.archive, archive, fetchImpl);
|
|
377
|
+
log('Extracting Google Antigravity ACP…');
|
|
378
|
+
await extractZip(archive, expanded);
|
|
379
|
+
const stagedCommand = path.join(expanded, entry.command);
|
|
380
|
+
if (!fs.existsSync(stagedCommand)) {
|
|
381
|
+
throw new Error('agent command is missing');
|
|
382
|
+
}
|
|
383
|
+
if (platform !== 'win32') {
|
|
384
|
+
await fs.promises.chmod(stagedCommand, 0o755);
|
|
385
|
+
const helper = path.join(
|
|
386
|
+
path.dirname(stagedCommand),
|
|
387
|
+
'localharness_external',
|
|
388
|
+
);
|
|
389
|
+
if (fs.existsSync(helper)) await fs.promises.chmod(helper, 0o755);
|
|
390
|
+
}
|
|
391
|
+
if (fs.existsSync(destination)) {
|
|
392
|
+
await fs.promises.rm(destination, { recursive: true, force: true });
|
|
393
|
+
}
|
|
394
|
+
await fs.promises.rename(expanded, destination);
|
|
395
|
+
} finally {
|
|
396
|
+
await fs.promises.rm(stage, { recursive: true, force: true });
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
const receipt = {
|
|
400
|
+
version: entry.version,
|
|
401
|
+
command: path.relative(root, command),
|
|
402
|
+
args: entry.args,
|
|
403
|
+
};
|
|
404
|
+
const tempReceipt = path.join(root, `current-${process.pid}.json`);
|
|
405
|
+
const currentReceipt = path.join(root, 'current.json');
|
|
406
|
+
await fs.promises.writeFile(tempReceipt, `${JSON.stringify(receipt)}\n`);
|
|
407
|
+
if (platform === 'win32') {
|
|
408
|
+
await fs.promises.rm(currentReceipt, { force: true });
|
|
409
|
+
}
|
|
410
|
+
await fs.promises.rename(tempReceipt, currentReceipt);
|
|
411
|
+
log(`Installed Google Antigravity ACP ${entry.version}\n`);
|
|
412
|
+
return { bin: command, args: entry.args };
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
const main = async () => {
|
|
416
|
+
const id = process.argv[2];
|
|
417
|
+
try {
|
|
418
|
+
await installRegistryAgent(id, {
|
|
419
|
+
log: (line) => process.stdout.write(`${line}\n`),
|
|
420
|
+
});
|
|
421
|
+
} catch (error) {
|
|
422
|
+
process.stderr.write(`${error.message}\n`);
|
|
423
|
+
process.exitCode = 1;
|
|
424
|
+
}
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
if (require.main === module) void main();
|
|
428
|
+
|
|
429
|
+
module.exports = {
|
|
430
|
+
REGISTRY_URL,
|
|
431
|
+
platformTarget,
|
|
432
|
+
agentRoot,
|
|
433
|
+
installedRegistryAgent,
|
|
434
|
+
registryEntry,
|
|
435
|
+
extractZip,
|
|
436
|
+
installRegistryAgent,
|
|
437
|
+
};
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { IS_WIN, spawnBase } = require('../common/process.js');
|
|
4
|
+
const { commandEnv, RunProcess, killTree } = require('../runs/process.js');
|
|
5
|
+
const { AcpConnection, AcpSession, methodMissing } = require('./acp.js');
|
|
6
|
+
const { decidePermission } = require('./allow.js');
|
|
7
|
+
const models = require('./models.js');
|
|
8
|
+
const { modelChange, effortChange, contextChange, fastChange } = models;
|
|
9
|
+
const { modeChange, modelRequest, catalogOf } = models;
|
|
10
|
+
const { Transcript } = require('./transcript.js');
|
|
11
|
+
const PKG = require('../../package.json');
|
|
12
|
+
|
|
13
|
+
const PROBE_MS = 30000;
|
|
14
|
+
const REVERT_MS = 15000;
|
|
15
|
+
const STOP_MS = 3000;
|
|
16
|
+
const CLIENT = { name: PKG.name, version: PKG.version };
|
|
17
|
+
const WIN_SCRIPT = /\.(?:cmd|bat)$/i;
|
|
18
|
+
|
|
19
|
+
// The review screen forces raw output. An agent needs the reduced test report.
|
|
20
|
+
const agentEnv = (extra) => {
|
|
21
|
+
const env = commandEnv({ ...process.env, ...extra });
|
|
22
|
+
delete env.RESLOP_OUTPUT;
|
|
23
|
+
return env;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// npm installs an adapter on Windows as a script only a shell can start
|
|
27
|
+
const spawnOptions = (cmd, cwd, env) => {
|
|
28
|
+
const shell = IS_WIN && WIN_SCRIPT.test(cmd);
|
|
29
|
+
return spawnBase({ cwd, env: agentEnv(env), detached: !IS_WIN, shell });
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const connect = (cmd, args, cwd, env, handlers) => {
|
|
33
|
+
const connection = new AcpConnection(handlers);
|
|
34
|
+
const quoted = IS_WIN && WIN_SCRIPT.test(cmd) ? `"${cmd}"` : cmd;
|
|
35
|
+
return connection.start(quoted, args, spawnOptions(cmd, cwd, env));
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const tryConnect = (cmd, args, cwd, env, handlers) => {
|
|
39
|
+
try {
|
|
40
|
+
return connect(cmd, args, cwd, env, handlers);
|
|
41
|
+
} catch (error) {
|
|
42
|
+
return { child: null, error };
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const tokensOf = (result, used) => {
|
|
47
|
+
const usage = result && result.usage;
|
|
48
|
+
const total = usage ? Number(usage.totalTokens) : NaN;
|
|
49
|
+
return Number.isFinite(total) ? total : used;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// One plan handed to an agent over ACP: a session, its settings, one prompt
|
|
53
|
+
class AcpRun extends RunProcess {
|
|
54
|
+
constructor(launch, onData, onClose) {
|
|
55
|
+
super(onData, onClose);
|
|
56
|
+
this.launch = launch;
|
|
57
|
+
this.transcript = new Transcript();
|
|
58
|
+
this.session = `${launch.session ?? ''}`;
|
|
59
|
+
this.acp = null;
|
|
60
|
+
this.cwd = '';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
start(cwd, env) {
|
|
64
|
+
const { cmd, args } = this.launch;
|
|
65
|
+
this.cwd = cwd;
|
|
66
|
+
this.transcript = new Transcript(cwd);
|
|
67
|
+
const handlers = {
|
|
68
|
+
notify: (method, params) => this.onNotify(method, params),
|
|
69
|
+
request: (method, params) => this.onRequest(method, params),
|
|
70
|
+
};
|
|
71
|
+
const connection = tryConnect(cmd, args ?? [], cwd, env, handlers);
|
|
72
|
+
if (!connection.child) {
|
|
73
|
+
this.fail(connection.error);
|
|
74
|
+
return this;
|
|
75
|
+
}
|
|
76
|
+
this.child = connection.child;
|
|
77
|
+
this.acp = new AcpSession(connection, cwd, CLIENT);
|
|
78
|
+
this.acp.onAuth = (method) => {
|
|
79
|
+
this.transcript.note(`signing in: ${method.name ?? method.id}`);
|
|
80
|
+
this.push();
|
|
81
|
+
};
|
|
82
|
+
this.drive().then(
|
|
83
|
+
(status) => this.end(status),
|
|
84
|
+
(error) => this.fail(error),
|
|
85
|
+
);
|
|
86
|
+
return this;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async drive() {
|
|
90
|
+
const { launch, acp } = this;
|
|
91
|
+
const state = await acp.open(launch.session, true);
|
|
92
|
+
this.session = state.sessionId;
|
|
93
|
+
this.push();
|
|
94
|
+
const picked = launch.choice ?? {};
|
|
95
|
+
await acp.change(modelChange(acp.state, picked));
|
|
96
|
+
await acp.change(effortChange(acp.state, picked));
|
|
97
|
+
await acp.change(contextChange(acp.state, picked));
|
|
98
|
+
await acp.change(fastChange(acp.state, picked));
|
|
99
|
+
await acp.change(modeChange(acp.state, launch.mode));
|
|
100
|
+
const result = await acp.prompt(launch.prompt);
|
|
101
|
+
this.transcript.tokens = tokensOf(result, this.transcript.tokens);
|
|
102
|
+
const reason = `${result.stopReason ?? ''}`;
|
|
103
|
+
if (reason === 'end_turn') return 0;
|
|
104
|
+
this.transcript.note(`stopped: ${reason || 'no reason given'}`);
|
|
105
|
+
return 1;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
onNotify(method, params) {
|
|
109
|
+
if (method !== 'session/update' || this.acp.replaying) return;
|
|
110
|
+
this.acp.absorb(params.update ?? {});
|
|
111
|
+
this.transcript.update(params.update);
|
|
112
|
+
this.push();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
onRequest(method, params) {
|
|
116
|
+
if (method !== 'session/request_permission') throw methodMissing(method);
|
|
117
|
+
const { allowed, outcome } = decidePermission(params, this.cwd);
|
|
118
|
+
if (!allowed) {
|
|
119
|
+
this.transcript.deny(params.toolCall ?? {});
|
|
120
|
+
this.push();
|
|
121
|
+
}
|
|
122
|
+
return { outcome };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
stats() {
|
|
126
|
+
const { transcript } = this;
|
|
127
|
+
return {
|
|
128
|
+
session: this.session,
|
|
129
|
+
files: transcript.files.size || null,
|
|
130
|
+
tokens: transcript.tokens,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
push() {
|
|
135
|
+
if (!this.closed) this.show(this.transcript.text());
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
fail(error) {
|
|
139
|
+
if (this.closed) return;
|
|
140
|
+
this.transcript.fail(error);
|
|
141
|
+
this.end(1);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
end(status) {
|
|
145
|
+
if (this.closed) return;
|
|
146
|
+
this.text = this.transcript.text();
|
|
147
|
+
this.settled = true;
|
|
148
|
+
this.onClose({ status, text: this.text, stats: this.stats() });
|
|
149
|
+
this.release();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// A stopped agent is asked to end its turn, so that it can still answer
|
|
153
|
+
kill() {
|
|
154
|
+
if (this.closed) return;
|
|
155
|
+
this.stopped = true;
|
|
156
|
+
if (this.acp) this.acp.cancel();
|
|
157
|
+
this.release(STOP_MS);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// The run is over for its job; the agent stays until its settings are back
|
|
161
|
+
release(wait = REVERT_MS) {
|
|
162
|
+
const { child, acp } = this;
|
|
163
|
+
if (!child) return;
|
|
164
|
+
let over = false;
|
|
165
|
+
const onExit = () => killTree(child);
|
|
166
|
+
const timer = setTimeout(onExit, wait);
|
|
167
|
+
const done = () => {
|
|
168
|
+
if (over) return;
|
|
169
|
+
over = true;
|
|
170
|
+
clearTimeout(timer);
|
|
171
|
+
process.off('exit', onExit);
|
|
172
|
+
if (child.exitCode === null && !child.signalCode) killTree(child);
|
|
173
|
+
};
|
|
174
|
+
process.once('exit', onExit);
|
|
175
|
+
child.once('close', done);
|
|
176
|
+
if (acp) acp.revert().then(done, done);
|
|
177
|
+
else done();
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const startRun = (cwd, launch, onData, onClose, env) =>
|
|
182
|
+
new AcpRun(launch, onData, onClose).start(cwd, env);
|
|
183
|
+
|
|
184
|
+
// The settings of a model show once a session is set to it
|
|
185
|
+
const describeModels = async (session, names, found) => {
|
|
186
|
+
const base = session.state;
|
|
187
|
+
const catalog = catalogOf(base);
|
|
188
|
+
found.state = base;
|
|
189
|
+
if (!catalog.configId) return;
|
|
190
|
+
const byModel = new Map([[catalog.currentId, base.configOptions]]);
|
|
191
|
+
found.state = { ...base, byModel };
|
|
192
|
+
try {
|
|
193
|
+
for (const name of names) {
|
|
194
|
+
const { id } = catalog.resolve(name, {});
|
|
195
|
+
if (!id || byModel.has(id)) continue;
|
|
196
|
+
await session.change(modelRequest(catalog, id));
|
|
197
|
+
byModel.set(id, session.state.configOptions);
|
|
198
|
+
}
|
|
199
|
+
} finally {
|
|
200
|
+
await session.revert();
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
// What a new session of the agent offers, read without sending a prompt
|
|
205
|
+
const probeSession = async (row, options = {}) => {
|
|
206
|
+
const { signal, cwd = process.cwd(), models = [] } = options;
|
|
207
|
+
if (signal && signal.aborted) return null;
|
|
208
|
+
const connection = tryConnect(row.bin, row.args ?? [], cwd, options.env, {});
|
|
209
|
+
if (!connection.child) return null;
|
|
210
|
+
const session = new AcpSession(connection, cwd, CLIENT);
|
|
211
|
+
const found = { state: null };
|
|
212
|
+
let timer = null;
|
|
213
|
+
let onAbort = null;
|
|
214
|
+
const aborted = new Promise((resolve) => {
|
|
215
|
+
onAbort = () => resolve();
|
|
216
|
+
timer = setTimeout(onAbort, options.timeout ?? PROBE_MS);
|
|
217
|
+
if (signal) signal.addEventListener('abort', onAbort);
|
|
218
|
+
});
|
|
219
|
+
const asked = session
|
|
220
|
+
.open()
|
|
221
|
+
.then(() => describeModels(session, models, found));
|
|
222
|
+
try {
|
|
223
|
+
await Promise.race([asked, aborted]);
|
|
224
|
+
} catch {
|
|
225
|
+
// What the agent answered before it failed is still what it offers
|
|
226
|
+
} finally {
|
|
227
|
+
clearTimeout(timer);
|
|
228
|
+
if (signal) signal.removeEventListener('abort', onAbort);
|
|
229
|
+
killTree(connection.child);
|
|
230
|
+
}
|
|
231
|
+
return found.state;
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
module.exports = { AcpRun, startRun, probeSession, describeModels };
|