@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
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// bitl-hook.mjs · bitl-mcp.mjs 공용: 원본을 정규화·최소화·암호화해 보내고, 실패하면 queue.jsonl 에 쌓는다.
|
|
2
|
+
// 보낼 곳: bitl login 으로 클라우드 계정에 연결했으면 그 계정(기기 API 키), 아니면 이 PC 의 로컬 서버.
|
|
3
|
+
// 서버에는 신호(상태 판단용)와 암호문만 간다.
|
|
4
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, renameSync, statSync, unlinkSync } from 'node:fs';
|
|
5
|
+
import { homedir } from 'node:os';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
|
|
8
|
+
const HOME = process.env.BITL_HOME?.trim() || join(homedir(), '.bitl');
|
|
9
|
+
const POST_TIMEOUT_MS = 1500;
|
|
10
|
+
const MAX_QUEUE_BYTES = 20 * 1024 * 1024; // 서버를 오래 못 만나도 디스크를 채우지 않도록
|
|
11
|
+
const MAX_ERROR_LOG_BYTES = 1024 * 1024;
|
|
12
|
+
const FLUSH_LIMIT = 500; // 한 번에 다시 보낼 최대 줄 수 (hook 을 오래 붙잡지 않게)
|
|
13
|
+
const QUEUE = join(HOME, 'queue.jsonl');
|
|
14
|
+
|
|
15
|
+
// 정규화·암호화 로직은 shared(TypeScript)를 묶은 번들 (pnpm install / pnpm build 가 만든다)
|
|
16
|
+
let core;
|
|
17
|
+
const loadCore = () => (core ??= import('./dist/core.mjs'));
|
|
18
|
+
|
|
19
|
+
/** 에이전트 화면에는 아무것도 쓰지 않는다 — 문제는 여기에만 남긴다 */
|
|
20
|
+
function logError(err) {
|
|
21
|
+
try {
|
|
22
|
+
const file = join(HOME, 'adapter-error.log');
|
|
23
|
+
try {
|
|
24
|
+
if (statSync(file).size > MAX_ERROR_LOG_BYTES) return;
|
|
25
|
+
} catch {
|
|
26
|
+
// 파일 없음
|
|
27
|
+
}
|
|
28
|
+
mkdirSync(HOME, { recursive: true });
|
|
29
|
+
appendFileSync(file, `${new Date().toISOString()} ${err?.stack ?? err}\n`, 'utf8');
|
|
30
|
+
} catch {
|
|
31
|
+
// 기록도 못 하면 조용히 넘어간다
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function post(target, body) {
|
|
36
|
+
try {
|
|
37
|
+
const res = await fetch(`${target.url}/api/events`, {
|
|
38
|
+
method: 'POST',
|
|
39
|
+
headers: { 'content-type': 'application/json', ...target.headers },
|
|
40
|
+
body,
|
|
41
|
+
signal: AbortSignal.timeout(POST_TIMEOUT_MS),
|
|
42
|
+
});
|
|
43
|
+
if (res.status === 401) logError(`${target.url}: 401 — 이 PC 의 연결이 끊겼습니다. bitl login 으로 다시 연결하세요`);
|
|
44
|
+
return res.ok;
|
|
45
|
+
} catch {
|
|
46
|
+
return false; // 서버 꺼짐 / 네트워크 / 타임아웃
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function enqueue(lines) {
|
|
51
|
+
try {
|
|
52
|
+
if (statSync(QUEUE).size > MAX_QUEUE_BYTES) return false;
|
|
53
|
+
} catch {
|
|
54
|
+
// 큐 파일 없음
|
|
55
|
+
}
|
|
56
|
+
mkdirSync(HOME, { recursive: true });
|
|
57
|
+
appendFileSync(QUEUE, lines.map((l) => l + '\n').join(''), 'utf8');
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** 이 키로 봉인된 레코드인지 (연결을 바꾸기 전에 쌓인 것은 새 계정으로 보내지 않는다) */
|
|
62
|
+
function sealedWith(line, kid) {
|
|
63
|
+
try {
|
|
64
|
+
return Object.values(JSON.parse(line).content ?? {}).every((v) => typeof v === 'string' && v.startsWith(`v1.${kid}.`));
|
|
65
|
+
} catch {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 클라우드로 보냈으면 쌓아 둔 이벤트도 보낸다 (로컬 모드는 로컬 서버가 시작할 때 큐를 읽는다).
|
|
72
|
+
* 다른 hook 과 겹치지 않게 파일을 먼저 옮기고, 못 보낸 줄은 다시 쌓는다.
|
|
73
|
+
*/
|
|
74
|
+
async function flushQueue(target, kid) {
|
|
75
|
+
if (!existsSync(QUEUE)) return;
|
|
76
|
+
const processing = `${QUEUE}.${process.pid}.flushing`;
|
|
77
|
+
try {
|
|
78
|
+
renameSync(QUEUE, processing);
|
|
79
|
+
} catch {
|
|
80
|
+
return; // 다른 hook 이 먼저 가져감
|
|
81
|
+
}
|
|
82
|
+
const lines = readFileSync(processing, 'utf8').split('\n').filter(Boolean);
|
|
83
|
+
unlinkSync(processing);
|
|
84
|
+
const later = [];
|
|
85
|
+
let failed = false;
|
|
86
|
+
for (const [i, line] of lines.entries()) {
|
|
87
|
+
if (!sealedWith(line, kid)) continue; // 다른 키로 봉인됐거나 옛 형식 — 버린다
|
|
88
|
+
if (failed || i >= FLUSH_LIMIT || !(await post(target, line))) {
|
|
89
|
+
failed = true;
|
|
90
|
+
later.push(line);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (later.length) enqueue(later);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** @returns {Promise<'sent' | 'queued' | 'dropped'>} */
|
|
97
|
+
export async function sendRecord({ source, receivedAt = new Date().toISOString(), payload }) {
|
|
98
|
+
let prepared;
|
|
99
|
+
try {
|
|
100
|
+
prepared = await (await loadCore()).prepareForTarget({ source, receivedAt, payload }, HOME);
|
|
101
|
+
} catch (err) {
|
|
102
|
+
logError(err); // 번들이 없거나(빌드 전) 키를 못 읽음
|
|
103
|
+
return 'dropped';
|
|
104
|
+
}
|
|
105
|
+
if (!prepared.record) return 'dropped'; // 해석할 수 없는 이벤트
|
|
106
|
+
const body = JSON.stringify(prepared.record);
|
|
107
|
+
if (await post(prepared, body)) {
|
|
108
|
+
if (prepared.kind === 'cloud') await flushQueue(prepared, prepared.kid).catch(logError);
|
|
109
|
+
return 'sent';
|
|
110
|
+
}
|
|
111
|
+
return enqueue([body]) ? 'queued' : 'dropped';
|
|
112
|
+
}
|
package/bin/bitl.mjs
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// bitl 명령 (npm 패키지 @usebitl/cli) 진입점.
|
|
3
|
+
// hook 이 실행할 어댑터를 ~/.bitl/adapters 에 복사해 두고(버전이 바뀌었을 때만) 그 경로로 CLI 를 실행한다.
|
|
4
|
+
// npm 전역 폴더는 Node 버전(nvm 등)에 따라 바뀌어서, hook 이 그 경로를 직접 가리키면 조용히 끊길 수 있다.
|
|
5
|
+
// node bin/bitl.mjs __sync — 복사만 (npm postinstall). 실패해도 설치는 막지 않는다
|
|
6
|
+
import { cpSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
7
|
+
import { homedir } from 'node:os';
|
|
8
|
+
import { join } from 'node:path';
|
|
9
|
+
|
|
10
|
+
const syncOnly = process.argv[2] === '__sync';
|
|
11
|
+
const { version } = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
12
|
+
const home = process.env.BITL_HOME?.trim() || join(homedir(), '.bitl');
|
|
13
|
+
const dir = join(home, 'adapters');
|
|
14
|
+
const stamp = join(dir, '.version');
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
if (!existsSync(stamp) || readFileSync(stamp, 'utf8').trim() !== version) {
|
|
18
|
+
mkdirSync(dir, { recursive: true });
|
|
19
|
+
cpSync(new URL('../adapters/', import.meta.url), dir, { recursive: true, force: true });
|
|
20
|
+
writeFileSync(stamp, version);
|
|
21
|
+
}
|
|
22
|
+
} catch (err) {
|
|
23
|
+
if (syncOnly) process.exit(0);
|
|
24
|
+
console.error(`bitl: ${dir} 에 어댑터를 복사하지 못했습니다 — ${err?.message ?? err}`);
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
if (syncOnly) process.exit(0);
|
|
28
|
+
|
|
29
|
+
process.env.BITL_ADAPTER_DIR = dir;
|
|
30
|
+
await import('../dist/cli.mjs');
|