@usebitl/cli 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/README.md +42 -0
- package/adapters/bitl-agy-watch.mjs +86 -0
- package/adapters/bitl-approve.mjs +61 -0
- package/adapters/bitl-hook.mjs +80 -0
- package/adapters/dist/core.mjs +715 -0
- package/adapters/send.mjs +112 -0
- package/bin/bitl.mjs +30 -0
- package/dist/cli.mjs +20952 -0
- package/package.json +39 -0
package/README.md
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# @usebitl/cli
|
|
2
|
+
|
|
3
|
+
Command-line companion for [BITL](https://usebitl.com) — stay in the loop of every AI agent you run.
|
|
4
|
+
|
|
5
|
+
BITL shows what Claude Code, Codex and Antigravity are doing and where they are waiting on your call, in your PC browser and on your phone. Prompts, answers and commands are end-to-end encrypted on your PC before they leave it.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm install -g @usebitl/cli
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Requires Node.js 20.11 or later. The command is `bitl`.
|
|
14
|
+
|
|
15
|
+
## Connect
|
|
16
|
+
|
|
17
|
+
```sh
|
|
18
|
+
bitl login # once per PC — approve in the browser that opens
|
|
19
|
+
bitl install claude # Claude Code
|
|
20
|
+
bitl install codex # Codex — then trust the bitl hooks in Codex's /hooks
|
|
21
|
+
bitl install antigravity # Antigravity (agy CLI and IDE)
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
`bitl uninstall <claude|codex|antigravity>` removes only what BITL added. Run `bitl` for all commands.
|
|
25
|
+
|
|
26
|
+
The hooks run small scripts copied to `~/.bitl/adapters`. They never block or slow down your agent.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## 한국어
|
|
31
|
+
|
|
32
|
+
[BITL](https://usebitl.com) 을 쓰기 위한 명령줄 도구입니다. Claude Code, Codex, Antigravity 가 지금 무엇을 하는지, 어디서 내 결정을 기다리는지 PC 브라우저와 휴대폰에서 봅니다. 질문·답변·명령은 PC 에서 종단간 암호화되어 전송됩니다.
|
|
33
|
+
|
|
34
|
+
```sh
|
|
35
|
+
npm install -g @usebitl/cli
|
|
36
|
+
bitl login # PC 마다 한 번 — 열리는 브라우저에서 승인
|
|
37
|
+
bitl install claude # Claude Code
|
|
38
|
+
bitl install codex # Codex — 설치 뒤 Codex 의 /hooks 에서 bitl hook 을 신뢰(trust)
|
|
39
|
+
bitl install antigravity # Antigravity (agy CLI·IDE)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Node.js 20.11 이상이 필요합니다. 문의: support@usebitl.com
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Antigravity(agy) 승인 대기 감시. bitl-hook.mjs 가 PreToolUse 마다 따로 띄운다 (agy 는 기다리지 않는다).
|
|
3
|
+
//
|
|
4
|
+
// node bitl-agy-watch.mjs <PreToolUse 페이로드를 담은 임시 파일>
|
|
5
|
+
//
|
|
6
|
+
// agy hook 에는 승인 대기 이벤트가 없다 → 대화 DB 의 그 단계(stepIdx) 상태를 잠깐 지켜본다:
|
|
7
|
+
// 승인 대기가 되면 bitl:PermissionWaiting, 승인·거부·취소되면 bitl:PermissionResolved 를 보낸다.
|
|
8
|
+
// 그 전에 끝나면(대부분의 도구) 아무것도 보내지 않고 끝난다.
|
|
9
|
+
// node:sqlite 가 없는 Node(22.5 미만)·DB 가 없는 환경(IDE 등)에서는 조용히 끝난다. 항상 exit 0.
|
|
10
|
+
|
|
11
|
+
import { readFileSync, unlinkSync } from 'node:fs';
|
|
12
|
+
import { homedir } from 'node:os';
|
|
13
|
+
import { join } from 'node:path';
|
|
14
|
+
import { sendRecord } from './send.mjs';
|
|
15
|
+
|
|
16
|
+
const POLL_MS = 400;
|
|
17
|
+
const MAX_MS = 10 * 60_000; // 승인을 이보다 오래 미루면 더 지켜보지 않는다
|
|
18
|
+
const NO_STEP_MS = 15_000; // 단계가 DB 에 나타나기를 기다리는 시간
|
|
19
|
+
|
|
20
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
21
|
+
|
|
22
|
+
async function main() {
|
|
23
|
+
const file = process.argv[2];
|
|
24
|
+
if (!file) return;
|
|
25
|
+
let payload;
|
|
26
|
+
try {
|
|
27
|
+
payload = JSON.parse(readFileSync(file, 'utf8'));
|
|
28
|
+
} finally {
|
|
29
|
+
try {
|
|
30
|
+
unlinkSync(file);
|
|
31
|
+
} catch {
|
|
32
|
+
// 이미 없음
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const { conversationId, stepIdx } = payload ?? {};
|
|
36
|
+
if (typeof conversationId !== 'string' || !/^[\w-]+$/.test(conversationId) || typeof stepIdx !== 'number') return;
|
|
37
|
+
|
|
38
|
+
let DatabaseSync;
|
|
39
|
+
try {
|
|
40
|
+
({ DatabaseSync } = await import('node:sqlite'));
|
|
41
|
+
} catch {
|
|
42
|
+
return; // 오래된 Node
|
|
43
|
+
}
|
|
44
|
+
const { agyWatchStep } = await import('./dist/core.mjs');
|
|
45
|
+
const dbPath = join(homedir(), '.gemini', 'antigravity-cli', 'conversations', `${conversationId}.db`);
|
|
46
|
+
|
|
47
|
+
let db;
|
|
48
|
+
try {
|
|
49
|
+
db = new DatabaseSync(dbPath, { readOnly: true });
|
|
50
|
+
} catch {
|
|
51
|
+
return; // DB 없음 (IDE 등)
|
|
52
|
+
}
|
|
53
|
+
const query = db.prepare('SELECT status FROM steps WHERE idx = ?');
|
|
54
|
+
const send = (event) => sendRecord({ source: 'antigravity', payload: { ...payload, hook_event_name: event } });
|
|
55
|
+
|
|
56
|
+
const started = Date.now();
|
|
57
|
+
let waited = false;
|
|
58
|
+
try {
|
|
59
|
+
while (Date.now() - started < MAX_MS) {
|
|
60
|
+
let status;
|
|
61
|
+
try {
|
|
62
|
+
status = query.get(stepIdx)?.status;
|
|
63
|
+
} catch {
|
|
64
|
+
// agy 가 쓰는 중 (잠김) — 다음에 다시
|
|
65
|
+
}
|
|
66
|
+
if (status === undefined && !waited && Date.now() - started > NO_STEP_MS) return;
|
|
67
|
+
const action = agyWatchStep(status, waited);
|
|
68
|
+
if (action === 'waiting') {
|
|
69
|
+
waited = true;
|
|
70
|
+
await send('bitl:PermissionWaiting');
|
|
71
|
+
} else if (action === 'resolved') {
|
|
72
|
+
await send('bitl:PermissionResolved');
|
|
73
|
+
return;
|
|
74
|
+
} else if (action === 'stop') {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
await sleep(POLL_MS);
|
|
78
|
+
}
|
|
79
|
+
} finally {
|
|
80
|
+
db.close();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
main()
|
|
85
|
+
.catch(() => {})
|
|
86
|
+
.finally(() => process.exit(0));
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// 원격 승인 hook (Claude Code PermissionRequest, 동기). bitl install 이 bitl-hook.mjs 와 함께 설치한다.
|
|
3
|
+
//
|
|
4
|
+
// node bitl-approve.mjs [--source claude-code] (페이로드는 stdin)
|
|
5
|
+
//
|
|
6
|
+
// 원격 승인이 켜졌을 때만 (보드의 "원격 승인" 스위치 또는 bitl remote on) 폰의 허용·거부를 기다린다 (기본 10분, $BITL_APPROVAL_WAIT_SEC).
|
|
7
|
+
// 그 밖에는 아무것도 출력하지 않고 바로 끝나 → Claude Code 가 평소처럼 터미널에 승인 창을 띄운다.
|
|
8
|
+
// 폰의 결정은 계정 키 서명을 확인한 뒤에만 따른다 (서버가 승인을 만들어 낼 수 없다). 실패는 모두 "터미널에서 승인"으로.
|
|
9
|
+
import { appendFileSync, mkdirSync, statSync } from 'node:fs';
|
|
10
|
+
import { homedir } from 'node:os';
|
|
11
|
+
import { join } from 'node:path';
|
|
12
|
+
|
|
13
|
+
const HOME = process.env.BITL_HOME?.trim() || join(homedir(), '.bitl');
|
|
14
|
+
|
|
15
|
+
function log(msg) {
|
|
16
|
+
try {
|
|
17
|
+
const file = join(HOME, 'adapter-error.log');
|
|
18
|
+
try {
|
|
19
|
+
if (statSync(file).size > 1024 * 1024) return;
|
|
20
|
+
} catch {}
|
|
21
|
+
mkdirSync(HOME, { recursive: true });
|
|
22
|
+
appendFileSync(file, `${new Date().toISOString()} ${msg}\n`, 'utf8');
|
|
23
|
+
} catch {}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function readStdin() {
|
|
27
|
+
return new Promise((resolve) => {
|
|
28
|
+
let data = '';
|
|
29
|
+
const timer = setTimeout(() => resolve(data), 2000);
|
|
30
|
+
process.stdin.setEncoding('utf8');
|
|
31
|
+
process.stdin.on('data', (c) => (data += c));
|
|
32
|
+
process.stdin.on('end', () => (clearTimeout(timer), resolve(data)));
|
|
33
|
+
process.stdin.on('error', () => (clearTimeout(timer), resolve(data)));
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const stop = new AbortController();
|
|
38
|
+
for (const sig of ['SIGTERM', 'SIGINT', 'SIGHUP']) process.on(sig, () => stop.abort());
|
|
39
|
+
|
|
40
|
+
async function main() {
|
|
41
|
+
const i = process.argv.indexOf('--source');
|
|
42
|
+
const source = i >= 0 ? process.argv[i + 1] : 'claude-code';
|
|
43
|
+
let payload;
|
|
44
|
+
try {
|
|
45
|
+
payload = JSON.parse(await readStdin());
|
|
46
|
+
} catch {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (payload?.hook_event_name !== 'PermissionRequest') return;
|
|
50
|
+
const { requestRemoteApproval } = await import('./dist/core.mjs');
|
|
51
|
+
const waitSec = Number(process.env.BITL_APPROVAL_WAIT_SEC);
|
|
52
|
+
const decision = await requestRemoteApproval(
|
|
53
|
+
{ ...payload, source },
|
|
54
|
+
{ home: HOME, waitMs: waitSec > 0 ? waitSec * 1000 : undefined, signal: stop.signal, log },
|
|
55
|
+
);
|
|
56
|
+
if (decision) process.stdout.write(JSON.stringify({ hookSpecificOutput: { hookEventName: 'PermissionRequest', decision } }));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
main()
|
|
60
|
+
.catch((err) => log(`원격 승인: ${err?.stack ?? err}`))
|
|
61
|
+
.finally(() => process.exit(0));
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// 에이전트 hook/notify 페이로드를 bitl-server 로 전달한다.
|
|
3
|
+
//
|
|
4
|
+
// Claude Code / Codex hooks : node bitl-hook.mjs --source claude-code|codex (페이로드는 stdin)
|
|
5
|
+
// Codex notify : node bitl-hook.mjs --source codex-notify (페이로드는 마지막 argv)
|
|
6
|
+
// Antigravity hooks : node bitl-hook.mjs --source antigravity --event PreToolUse (이벤트 이름이 페이로드에 없다)
|
|
7
|
+
//
|
|
8
|
+
// 서버: $BITL_URL (기본 http://127.0.0.1:$BITL_PORT, 7777)
|
|
9
|
+
// 서버가 꺼져 있으면 $BITL_HOME/queue.jsonl (기본 ~/.bitl) 에 (암호화한 채로) 쌓아두고, 서버가 시작할 때 처리한다.
|
|
10
|
+
//
|
|
11
|
+
// 원칙: 에이전트를 절대 방해하지 않는다 — stdout 무출력, 항상 exit 0, 짧은 타임아웃.
|
|
12
|
+
// 해석(정규화)·최소화·암호화는 이 PC 에서 한다 (send.mjs → dist/core.mjs). 서버는 내용을 읽지 못한다.
|
|
13
|
+
// 규칙이 바뀌면 번들만 다시 빌드하면 된다 (pnpm build). 설치 경로가 그대로라 재설치는 필요 없다.
|
|
14
|
+
|
|
15
|
+
import { spawn } from 'node:child_process';
|
|
16
|
+
import { writeFileSync } from 'node:fs';
|
|
17
|
+
import { tmpdir } from 'node:os';
|
|
18
|
+
import { join } from 'node:path';
|
|
19
|
+
import { sendRecord } from './send.mjs';
|
|
20
|
+
|
|
21
|
+
const STDIN_TIMEOUT_MS = 2000;
|
|
22
|
+
|
|
23
|
+
function argValue(name) {
|
|
24
|
+
const i = process.argv.indexOf(name);
|
|
25
|
+
return i >= 0 ? process.argv[i + 1] : undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function readStdin() {
|
|
29
|
+
return new Promise((resolve) => {
|
|
30
|
+
if (process.stdin.isTTY) return resolve('');
|
|
31
|
+
let data = '';
|
|
32
|
+
const timer = setTimeout(() => resolve(data), STDIN_TIMEOUT_MS);
|
|
33
|
+
process.stdin.setEncoding('utf8');
|
|
34
|
+
process.stdin.on('data', (chunk) => (data += chunk));
|
|
35
|
+
process.stdin.on('end', () => {
|
|
36
|
+
clearTimeout(timer);
|
|
37
|
+
resolve(data);
|
|
38
|
+
});
|
|
39
|
+
process.stdin.on('error', () => {
|
|
40
|
+
clearTimeout(timer);
|
|
41
|
+
resolve(data);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** agy 는 승인 대기를 hook 으로 알리지 않는다 → 그 단계를 지켜볼 프로세스를 따로 띄운다 (bitl-agy-watch.mjs) */
|
|
47
|
+
function watchAgyPermission(payload) {
|
|
48
|
+
try {
|
|
49
|
+
if (typeof payload.stepIdx !== 'number') return;
|
|
50
|
+
const file = join(tmpdir(), `bitl-agy-${process.pid}-${Date.now()}.json`);
|
|
51
|
+
writeFileSync(file, JSON.stringify(payload), 'utf8');
|
|
52
|
+
const script = join(import.meta.dirname, 'bitl-agy-watch.mjs');
|
|
53
|
+
spawn(process.execPath, [script, file], { detached: true, stdio: 'ignore', windowsHide: true }).unref();
|
|
54
|
+
} catch {
|
|
55
|
+
// 감시를 못 띄워도 hook 은 계속
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function main() {
|
|
60
|
+
const receivedAt = new Date().toISOString(); // hook 이 실행된 시각 (서버 도착 시각이 아님)
|
|
61
|
+
const source = argValue('--source') ?? 'claude-code';
|
|
62
|
+
|
|
63
|
+
const lastArg = process.argv[process.argv.length - 1];
|
|
64
|
+
const raw = lastArg?.trimStart().startsWith('{') ? lastArg : await readStdin();
|
|
65
|
+
let payload;
|
|
66
|
+
try {
|
|
67
|
+
payload = JSON.parse(raw);
|
|
68
|
+
} catch {
|
|
69
|
+
return; // 해석할 수 없는 입력은 버린다
|
|
70
|
+
}
|
|
71
|
+
// Antigravity 는 페이로드에 이벤트 이름이 없어 --event 로 받는다
|
|
72
|
+
const event = argValue('--event');
|
|
73
|
+
if (event && payload && typeof payload === 'object' && payload.hook_event_name === undefined) payload.hook_event_name = event;
|
|
74
|
+
if (source === 'antigravity' && event === 'PreToolUse') watchAgyPermission(payload);
|
|
75
|
+
await sendRecord({ source, receivedAt, payload });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
main()
|
|
79
|
+
.catch(() => {})
|
|
80
|
+
.finally(() => process.exit(0));
|