@elbow-dev/acp 0.0.0-stage → 0.1.0
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/LICENSE +4 -0
- package/acp.mjs +623 -0
- package/agent.mjs +875 -0
- package/bin.mjs +101 -0
- package/package.json +21 -4
- package/README.md +0 -3
package/bin.mjs
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { execFileSync, spawn } from 'node:child_process';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
|
|
7
|
+
// The package an app installs from the ACP Registry: `elbow acp` without the elbow command. It signs in, keeps the
|
|
8
|
+
// key and keeps threads where the elbow command does, so either finds what the other saved.
|
|
9
|
+
const url = process.env.ELBOW_URL ?? 'https://api.elbow.dev';
|
|
10
|
+
const dir = path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config'), 'elbow');
|
|
11
|
+
const keyFile = path.join(dir, 'key');
|
|
12
|
+
const { version } = JSON.parse(fs.readFileSync(new URL('package.json', import.meta.url), 'utf8'));
|
|
13
|
+
const mac = process.platform === 'darwin';
|
|
14
|
+
|
|
15
|
+
const keychain = (...args) => {
|
|
16
|
+
if (!mac) return null;
|
|
17
|
+
try {
|
|
18
|
+
return execFileSync('security', args, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] }).trim();
|
|
19
|
+
} catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
const keyOf = () => {
|
|
24
|
+
if (process.env.ELBOW_KEY) return process.env.ELBOW_KEY;
|
|
25
|
+
const saved = keychain('find-generic-password', '-s', 'elbow', '-a', url, '-w');
|
|
26
|
+
if (saved) return saved;
|
|
27
|
+
try {
|
|
28
|
+
return fs.readFileSync(keyFile, 'utf8').trim();
|
|
29
|
+
} catch {
|
|
30
|
+
return '';
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
const save = (key) => {
|
|
34
|
+
// Through security's own prompt, so the key never shows in the process list.
|
|
35
|
+
if (mac) {
|
|
36
|
+
try {
|
|
37
|
+
execFileSync('security', ['-i'], {
|
|
38
|
+
input: `add-generic-password -U -s elbow -l Elbow -a "${url}" -w "${key}"\n`,
|
|
39
|
+
stdio: ['pipe', 'ignore', 'ignore'],
|
|
40
|
+
});
|
|
41
|
+
if (keychain('find-generic-password', '-s', 'elbow', '-a', url, '-w') === key) return;
|
|
42
|
+
} catch {}
|
|
43
|
+
}
|
|
44
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
45
|
+
fs.writeFileSync(keyFile, key, { mode: 0o600 });
|
|
46
|
+
};
|
|
47
|
+
const deviceName = () => {
|
|
48
|
+
let name = '';
|
|
49
|
+
try {
|
|
50
|
+
if (mac) name = execFileSync('scutil', ['--get', 'ComputerName'], { encoding: 'utf8' }).trim();
|
|
51
|
+
} catch {}
|
|
52
|
+
return (name || os.hostname()).replace(/[^\w .-]/g, '').slice(0, 64);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// The device flow `elbow login` uses: a short code, confirmed in the browser where the person is signed in.
|
|
56
|
+
const login = async () => {
|
|
57
|
+
const post = (route, body) =>
|
|
58
|
+
fetch(`${url}${route}`, {
|
|
59
|
+
method: 'POST',
|
|
60
|
+
headers: { 'content-type': 'application/json' },
|
|
61
|
+
body: JSON.stringify(body),
|
|
62
|
+
});
|
|
63
|
+
const start = await (await post('/auth/device', {})).json();
|
|
64
|
+
const link = `${start.verification_uri}?code=${start.user_code}`;
|
|
65
|
+
console.error(`Your code is ${start.user_code}. Confirm it in your browser:\n ${link}`);
|
|
66
|
+
if (!process.env.ELBOW_NO_BROWSER) {
|
|
67
|
+
const opener = mac ? 'open' : process.platform === 'win32' ? 'explorer' : 'xdg-open';
|
|
68
|
+
spawn(opener, [link], { stdio: 'ignore', detached: true }).on('error', () => {}).unref();
|
|
69
|
+
}
|
|
70
|
+
const until = Date.now() + (start.expires_in ?? 600) * 1000;
|
|
71
|
+
while (Date.now() < until) {
|
|
72
|
+
await new Promise((ok) => setTimeout(ok, (start.interval ?? 5) * 1000));
|
|
73
|
+
// A dropped connection is retried: Elbow hands out the same token again for a minute.
|
|
74
|
+
const res = await post('/auth/device/token', { device_code: start.device_code, name: deviceName() }).catch(
|
|
75
|
+
() => null,
|
|
76
|
+
);
|
|
77
|
+
if (!res || res.status === 503) continue;
|
|
78
|
+
const reply = await res.json().catch(() => ({}));
|
|
79
|
+
if (res.ok && reply.status === 'pending') continue;
|
|
80
|
+
if (res.ok && reply.status === 'ok' && reply.token?.startsWith('elbow-')) {
|
|
81
|
+
save(reply.token);
|
|
82
|
+
console.error('Signed in to Elbow. Go back to your app.');
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (res.ok && reply.status !== 'expired') throw new Error("Elbow didn't send a key; sign in again.");
|
|
86
|
+
if (!res.ok) throw new Error(`Elbow answered ${res.status}: ${reply.error?.message ?? reply.message ?? ''}`);
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
throw new Error('That code expired; sign in again.');
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
if (process.argv.includes('--login')) {
|
|
93
|
+
await login().catch((e) => {
|
|
94
|
+
console.error(e.message);
|
|
95
|
+
process.exit(1);
|
|
96
|
+
});
|
|
97
|
+
} else {
|
|
98
|
+
const { start } = await import('./acp.mjs');
|
|
99
|
+
const agent = await import('./agent.mjs');
|
|
100
|
+
await start({ agent, key: keyOf(), url, version, saved: path.join(dir, 'acp-sessions') });
|
|
101
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elbow-dev/acp",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Elbow's coding agent for apps that speak the Agent Client Protocol, such as T3 Code and Zed.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"elbow-acp": "bin.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin.mjs",
|
|
11
|
+
"acp.mjs",
|
|
12
|
+
"agent.mjs",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=24"
|
|
17
|
+
},
|
|
18
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
19
|
+
"homepage": "https://elbow.dev",
|
|
20
|
+
"scripts": {
|
|
21
|
+
"prepack": "node copy.mjs"
|
|
22
|
+
}
|
|
23
|
+
}
|
package/README.md
DELETED