agentcage 0.1.4 → 0.1.5
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 +8 -7
- package/bin/agentcage.js +7 -3
- package/package.json +1 -1
- package/src/icon.js +95 -0
- package/src/opencode.js +3 -2
- package/src/wizard.js +1 -1
package/README.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# agentcage
|
|
2
2
|
|
|
3
3
|
```
|
|
4
|
-
▄▄▄ ▄▄▄▄ ▄▄▄▄ ▄▄▄▄ █
|
|
5
|
-
▄▄▄█ █ █ █▄▄▄ █ █ ▀█▀
|
|
6
|
-
█▄▄█ ▀▀▀█ █▄▄▄ █ █ █▄▄
|
|
7
|
-
▀▀▀▀
|
|
4
|
+
▄▄▄ ▄▄▄▄ ▄▄▄▄ ▄▄▄▄ █ ▄▄▄▄ ▄▄▄ ▄▄▄▄ ▄▄▄▄
|
|
5
|
+
▄▄▄█ █ █ █▄▄▄ █ █ ▀█▀ • █ ▄▄▄█ █ █ █▄▄▄
|
|
6
|
+
█▄▄█ ▀▀▀█ █▄▄▄ █ █ █▄▄ █▄▄▄ █▄▄█ ▀▀▀█ █▄▄▄
|
|
7
|
+
▀▀▀▀ ▀▀▀▀
|
|
8
8
|
```
|
|
9
9
|
|
|
10
10
|
Isolated dev containers for AI coding agents. Each project gets its own Linux container. **The project source lives
|
|
@@ -60,8 +60,9 @@ Run `agentcage` . The wizard will ask you for this:
|
|
|
60
60
|
API key in your terminal. Additional Providers can be added later.
|
|
61
61
|
7. **An IDE** to open the container in when it's ready (Cursor, VS Code, Antigravity).
|
|
62
62
|
|
|
63
|
-
Then it builds the container, adds it to OpenCode Desktop as server **ac-<name>** (a green project
|
|
64
|
-
`/workspaces/<name>` already
|
|
63
|
+
Then it builds the container, adds it to OpenCode Desktop as server **ac-<name>** (a green project with a generated
|
|
64
|
+
128x128 icon: `ac` in small letters over the first three letters of the name) with the project `/workspaces/<name>` already
|
|
65
|
+
open, and opens the IDE.
|
|
65
66
|
|
|
66
67
|
The same works without the dialog:
|
|
67
68
|
|
|
@@ -81,7 +82,7 @@ agentcage start <name> # start after a reboot, print the OpenCode serve
|
|
|
81
82
|
agentcage shell <name> # shell in the container (opencode, claude, git ... work here)
|
|
82
83
|
agentcage open <name> [ide] # Cursor (default), VS Code, or another VS Code-based IDE attached to the container
|
|
83
84
|
agentcage provider <name> # add model provider API keys to OpenCode in the container
|
|
84
|
-
agentcage connect <name> # (re)register the container in OpenCode Desktop
|
|
85
|
+
agentcage connect <name> # (re)register the container in OpenCode Desktop and refresh its project icon
|
|
85
86
|
agentcage upgrade <name> # rebuild with the Desktop app's OpenCode version (volumes kept)
|
|
86
87
|
agentcage stop <name>
|
|
87
88
|
agentcage remove <name> # delete container, volumes and config
|
package/bin/agentcage.js
CHANGED
|
@@ -4,7 +4,7 @@ import { parseArgs } from 'node:util';
|
|
|
4
4
|
import * as p from '@clack/prompts';
|
|
5
5
|
import { adoptContainer, listContainers, loadContainer, removeContainer, saveContainer, state, up, writeDevcontainer, openIde, ws } from '../src/container.js';
|
|
6
6
|
import { ask, connectionInfo, newWizard, providerFlow, registerFlow, settingsWizard } from '../src/wizard.js';
|
|
7
|
-
import { detectDesktop, latestVersion } from '../src/opencode.js';
|
|
7
|
+
import { detectDesktop, latestVersion, setupProject } from '../src/opencode.js';
|
|
8
8
|
import { ok, run } from '../src/run.js';
|
|
9
9
|
|
|
10
10
|
process.removeAllListeners('warning'); // hide node:sqlite ExperimentalWarning
|
|
@@ -19,7 +19,7 @@ const HELP = `agentcage: isolated dev containers for AI coding agents
|
|
|
19
19
|
agentcage stop <name>
|
|
20
20
|
agentcage shell <name> shell in the container (run opencode, claude, git ... here)
|
|
21
21
|
agentcage open <name> [ide] open Cursor (default), VS Code or another VS Code-based IDE attached to the container
|
|
22
|
-
agentcage connect <name> (re)register the container in OpenCode Desktop (app must be closed)
|
|
22
|
+
agentcage connect <name> (re)register the container in OpenCode Desktop and refresh its project icon (app must be closed)
|
|
23
23
|
agentcage provider <name> add model provider API keys to OpenCode in the container
|
|
24
24
|
agentcage upgrade <name> rebuild with the Desktop app's OpenCode version (volumes kept)
|
|
25
25
|
agentcage remove <name> delete container, volumes and config
|
|
@@ -73,7 +73,11 @@ const commands = {
|
|
|
73
73
|
const b = container(); await start(b); openIde(b, arg2 ?? f.ide ?? 'cursor');
|
|
74
74
|
if (b.git === 'token') p.log.warn('The IDE forwards your SSH agent into the container. Turn it off in the IDE settings to keep pushes limited to the repo token (see README).');
|
|
75
75
|
},
|
|
76
|
-
async connect() {
|
|
76
|
+
async connect() {
|
|
77
|
+
const b = container();
|
|
78
|
+
await start(b).then(() => setupProject(b)).catch(e => p.log.warn(`Could not set the project name and icon in OpenCode: ${e.message}`));
|
|
79
|
+
await registerFlow(b);
|
|
80
|
+
},
|
|
77
81
|
async provider() { const b = container(); await start(b); await providerFlow(b, csv(f.providers)); },
|
|
78
82
|
async upgrade() {
|
|
79
83
|
const b = container(), v = detectDesktop().version ?? await latestVersion();
|
package/package.json
CHANGED
package/src/icon.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// Generates the 128x128 project icon for OpenCode: dark green background, white text in two rows —
|
|
2
|
+
// the project prefix ("ac") in small letters on top, the first three letters of the container name below.
|
|
3
|
+
// A hand-rolled PNG encoder (zlib + CRC) with a 5x7 bitmap font keeps this dependency-free.
|
|
4
|
+
import { deflateSync } from 'node:zlib';
|
|
5
|
+
|
|
6
|
+
const SIZE = 128, SS = 4, BG = [20, 83, 45], FG = [255, 255, 255]; // SS: supersampling for smooth edges
|
|
7
|
+
|
|
8
|
+
// 5x7 pixel font ('#' = ink); lowercase letters, digits and - cover all names and prefixes
|
|
9
|
+
const FONT = {
|
|
10
|
+
a: ['.....', '.....', '.###.', '....#', '.####', '#...#', '.####'],
|
|
11
|
+
b: ['#....', '#....', '####.', '#...#', '#...#', '#...#', '####.'],
|
|
12
|
+
c: ['.....', '.....', '.####', '#....', '#....', '#....', '.####'],
|
|
13
|
+
d: ['....#', '....#', '.####', '#...#', '#...#', '#...#', '.####'],
|
|
14
|
+
e: ['.....', '.....', '.###.', '#...#', '#####', '#....', '.###.'],
|
|
15
|
+
f: ['..##.', '.#...', '####.', '.#...', '.#...', '.#...', '.#...'],
|
|
16
|
+
g: ['.....', '.....', '.###.', '#...#', '.####', '....#', '.###.'],
|
|
17
|
+
h: ['#....', '#....', '####.', '#...#', '#...#', '#...#', '#...#'],
|
|
18
|
+
i: ['..#..', '.....', '..#..', '..#..', '..#..', '..#..', '..#..'],
|
|
19
|
+
j: ['....#', '.....', '....#', '....#', '....#', '#...#', '.###.'],
|
|
20
|
+
k: ['#....', '#....', '#...#', '#..#.', '###..', '#..#.', '#...#'],
|
|
21
|
+
l: ['..#..', '..#..', '..#..', '..#..', '..#..', '..#..', '..#..'],
|
|
22
|
+
m: ['.....', '.....', '##.##', '#.#.#', '#.#.#', '#.#.#', '#.#.#'],
|
|
23
|
+
n: ['.....', '.....', '####.', '#...#', '#...#', '#...#', '#...#'],
|
|
24
|
+
o: ['.....', '.....', '.###.', '#...#', '#...#', '#...#', '.###.'],
|
|
25
|
+
p: ['.....', '.....', '####.', '#...#', '#...#', '####.', '#....'],
|
|
26
|
+
q: ['.....', '.....', '.####', '#...#', '#...#', '.####', '....#'],
|
|
27
|
+
r: ['.....', '.....', '#.##.', '##..#', '#....', '#....', '#....'],
|
|
28
|
+
s: ['.....', '.....', '.####', '#....', '.###.', '....#', '####.'],
|
|
29
|
+
t: ['.#...', '.#...', '###..', '.#...', '.#...', '.#...', '.##..'],
|
|
30
|
+
u: ['.....', '.....', '#...#', '#...#', '#...#', '#...#', '.###.'],
|
|
31
|
+
v: ['.....', '.....', '#...#', '#...#', '#...#', '.#.#.', '..#..'],
|
|
32
|
+
w: ['.....', '.....', '#...#', '#...#', '#.#.#', '#.#.#', '.#.#.'],
|
|
33
|
+
x: ['.....', '.....', '#...#', '.#.#.', '..#..', '.#.#.', '#...#'],
|
|
34
|
+
y: ['.....', '.....', '#...#', '#...#', '.#.#.', '..#..', '..#..'],
|
|
35
|
+
z: ['.....', '.....', '####.', '...#.', '..#..', '.#...', '####.'],
|
|
36
|
+
0: ['.###.', '#...#', '#..##', '#.#.#', '##..#', '#...#', '.###.'],
|
|
37
|
+
1: ['..#..', '.##..', '..#..', '..#..', '..#..', '..#..', '.###.'],
|
|
38
|
+
2: ['.###.', '#...#', '....#', '...#.', '..#..', '.#...', '#####'],
|
|
39
|
+
3: ['.###.', '#...#', '....#', '..##.', '....#', '#...#', '.###.'],
|
|
40
|
+
4: ['...#.', '..##.', '.#.#.', '#..#.', '#####', '...#.', '...#.'],
|
|
41
|
+
5: ['#####', '#....', '####.', '....#', '....#', '#...#', '.###.'],
|
|
42
|
+
6: ['.###.', '#....', '#....', '####.', '#...#', '#...#', '.###.'],
|
|
43
|
+
7: ['#####', '....#', '...#.', '..#..', '..#..', '..#..', '..#..'],
|
|
44
|
+
8: ['.###.', '#...#', '#...#', '.###.', '#...#', '#...#', '.###.'],
|
|
45
|
+
9: ['.###.', '#...#', '#...#', '.####', '....#', '....#', '.###.'],
|
|
46
|
+
'-': ['.....', '.....', '.....', '.###.', '.....', '.....', '.....'],
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const crcTable = Array.from({ length: 256 }, (_, n) => { let c = n; for (let k = 0; k < 8; k++) c = c & 1 ? 0xEDB88320 ^ (c >>> 1) : c >>> 1; return c; });
|
|
50
|
+
const crc32 = buf => { let c = -1; for (const b of buf) c = crcTable[(c ^ b) & 255] ^ (c >>> 8); return (c ^ -1) >>> 0; };
|
|
51
|
+
const chunk = (type, data) => { const t = Buffer.from(type), head = Buffer.alloc(8), crc = Buffer.alloc(4);
|
|
52
|
+
head.writeUInt32BE(data.length); t.copy(head, 4); crc.writeUInt32BE(crc32(Buffer.concat([t, data])));
|
|
53
|
+
return Buffer.concat([head, data, crc]); };
|
|
54
|
+
|
|
55
|
+
const png = rgba => {
|
|
56
|
+
const ihdr = Buffer.alloc(13), stride = SIZE * 4, raw = Buffer.alloc((stride + 1) * SIZE);
|
|
57
|
+
ihdr.writeUInt32BE(SIZE); ihdr.writeUInt32BE(SIZE, 4); ihdr[8] = 8; ihdr[9] = 6; // 8-bit RGBA
|
|
58
|
+
for (let y = 0; y < SIZE; y++) rgba.copy(raw, y * (stride + 1) + 1, y * stride, (y + 1) * stride);
|
|
59
|
+
return Buffer.concat([Buffer.from('89504e470d0a1a0a', 'hex'), chunk('IHDR', ihdr), chunk('IDAT', deflateSync(raw)), chunk('IEND', Buffer.alloc(0))]);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// Draws one text row centered horizontally; s and y are in final-image pixels (rendered at SS x)
|
|
63
|
+
const draw = (img, dim, text, s, y) => {
|
|
64
|
+
let x = Math.round((SIZE - (text.length * 6 - 1) * s) / 2) * SS;
|
|
65
|
+
for (const ch of text) {
|
|
66
|
+
const g = FONT[ch];
|
|
67
|
+
if (g) for (let r = 0; r < 7; r++) for (let c = 0; c < 5; c++) if (g[r][c] === '#')
|
|
68
|
+
for (let dy = 0; dy < s * SS; dy++) for (let dx = 0; dx < s * SS; dx++) {
|
|
69
|
+
const i = ((y * SS + r * s * SS + dy) * dim + x + c * s * SS + dx) * 3;
|
|
70
|
+
img[i] = FG[0]; img[i + 1] = FG[1]; img[i + 2] = FG[2];
|
|
71
|
+
}
|
|
72
|
+
x += 6 * s * SS;
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// The icon as a data URL for OpenCode's project icon override
|
|
77
|
+
export const projectIcon = (name, prefix = 'ac') => {
|
|
78
|
+
const dim = SIZE * SS, img = Buffer.alloc(dim * dim * 3);
|
|
79
|
+
for (let i = 0; i < dim * dim; i++) { img[i * 3] = BG[0]; img[i * 3 + 1] = BG[1]; img[i * 3 + 2] = BG[2]; }
|
|
80
|
+
const rows = [[prefix.replace(/[^a-z0-9]/g, ''), 3], [name.slice(0, 3), 6]].filter(([t]) => t);
|
|
81
|
+
const gap = 12, total = rows.reduce((n, [, s]) => n + 7 * s, 0) + gap * (rows.length - 1);
|
|
82
|
+
let y = Math.round((SIZE - total) / 2);
|
|
83
|
+
for (const [t, s] of rows) { draw(img, dim, t, s, y); y += 7 * s + gap; }
|
|
84
|
+
const out = Buffer.alloc(SIZE * SIZE * 4); // downsample SS x SS blocks for smooth edges
|
|
85
|
+
for (let py = 0; py < SIZE; py++) for (let px = 0; px < SIZE; px++) {
|
|
86
|
+
let r = 0, g = 0, b = 0;
|
|
87
|
+
for (let dy = 0; dy < SS; dy++) for (let dx = 0; dx < SS; dx++) {
|
|
88
|
+
const i = ((py * SS + dy) * dim + px * SS + dx) * 3;
|
|
89
|
+
r += img[i]; g += img[i + 1]; b += img[i + 2];
|
|
90
|
+
}
|
|
91
|
+
const o = (py * SIZE + px) * 4, n = SS * SS;
|
|
92
|
+
out[o] = Math.round(r / n); out[o + 1] = Math.round(g / n); out[o + 2] = Math.round(b / n); out[o + 3] = 255;
|
|
93
|
+
}
|
|
94
|
+
return `data:image/png;base64,${png(out).toString('base64')}`;
|
|
95
|
+
};
|
package/src/opencode.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { copyFileSync, existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
|
+
import { projectIcon } from './icon.js';
|
|
5
6
|
import { ok, run, runSync } from './run.js';
|
|
6
7
|
import { loadSettings } from './settings.js';
|
|
7
8
|
|
|
@@ -55,7 +56,7 @@ export async function connectDesktop(container) {
|
|
|
55
56
|
// Names stored at creation; containers made before the settings existed use the current settings
|
|
56
57
|
export const serverName = c => c.serverName ?? loadSettings().serverPrefix + c.name;
|
|
57
58
|
|
|
58
|
-
// Sets the project's name and icon
|
|
59
|
+
// Sets the project's name and its generated icon in the container's OpenCode server. Asking about the folder creates the project first.
|
|
59
60
|
// The server starts in the background after `devcontainer up`, so retry for ~30 s.
|
|
60
61
|
export async function setupProject(c) {
|
|
61
62
|
const s = loadSettings(), name = c.projectName ?? s.projectPrefix + c.name, color = c.color ?? s.color;
|
|
@@ -66,7 +67,7 @@ export async function setupProject(c) {
|
|
|
66
67
|
try {
|
|
67
68
|
await api(`/vcs?location[directory]=${encodeURIComponent(dir)}`);
|
|
68
69
|
const { id } = (await api('/project')).find(x => x.canonical === dir);
|
|
69
|
-
return await api(`/project/${id}`, { method: 'PATCH', body: JSON.stringify({ name, icon: { color } }) });
|
|
70
|
+
return await api(`/project/${id}`, { method: 'PATCH', body: JSON.stringify({ name, icon: { color, override: projectIcon(c.name, s.projectPrefix) } }) });
|
|
70
71
|
} catch (e) { if (i >= 15) throw e; await new Promise(r => setTimeout(r, 2000)); }
|
|
71
72
|
}
|
|
72
73
|
}
|
package/src/wizard.js
CHANGED
|
@@ -135,7 +135,7 @@ export async function newWizard(f = {}) {
|
|
|
135
135
|
process.exit(1);
|
|
136
136
|
}
|
|
137
137
|
if (providers.length) await providerFlow(b, providers);
|
|
138
|
-
if (oc) await setupProject(b).catch(e => p.log.warn(`Could not set the project name and
|
|
138
|
+
if (oc) await setupProject(b).catch(e => p.log.warn(`Could not set the project name and icon in OpenCode: ${e.message}`));
|
|
139
139
|
if (oc) await registerFlow(b, !f.yes);
|
|
140
140
|
if (ide !== 'none') { openIde(b, ide); p.log.success('Opening the IDE attached to the container'); }
|
|
141
141
|
p.outro([
|