@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,715 @@
|
|
|
1
|
+
// ../../shared/src/crypto.ts
|
|
2
|
+
var MASTER_KEY_BYTES = 32;
|
|
3
|
+
var VERSION = "v1";
|
|
4
|
+
var PAD_BLOCK = 32;
|
|
5
|
+
var TAG_BYTES = 16;
|
|
6
|
+
var subtle = () => globalThis.crypto.subtle;
|
|
7
|
+
var utf8 = new TextEncoder();
|
|
8
|
+
var fromUtf8 = new TextDecoder("utf-8", { fatal: true });
|
|
9
|
+
function toBase64Url(bytes) {
|
|
10
|
+
let bin = "";
|
|
11
|
+
for (const b of bytes) bin += String.fromCharCode(b);
|
|
12
|
+
return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
13
|
+
}
|
|
14
|
+
function fromBase64Url(s) {
|
|
15
|
+
if (!/^[A-Za-z0-9_-]*$/.test(s)) throw new Error("invalid base64url");
|
|
16
|
+
const bin = atob(s.replace(/-/g, "+").replace(/_/g, "/") + "=".repeat((4 - s.length % 4) % 4));
|
|
17
|
+
return Uint8Array.from(bin, (c) => c.charCodeAt(0));
|
|
18
|
+
}
|
|
19
|
+
var generateMasterKey = () => globalThis.crypto.getRandomValues(new Uint8Array(MASTER_KEY_BYTES));
|
|
20
|
+
var info = (name) => ({ name: "HKDF", hash: "SHA-256", salt: new Uint8Array(0), info: utf8.encode(`bitl/v1/${name}`) });
|
|
21
|
+
async function deriveKeys(master) {
|
|
22
|
+
if (master.length !== MASTER_KEY_BYTES) throw new Error(`\uD0A4 \uAE38\uC774\uAC00 ${MASTER_KEY_BYTES}\uBC14\uC774\uD2B8\uAC00 \uC544\uB2D9\uB2C8\uB2E4`);
|
|
23
|
+
const base = await subtle().importKey("raw", new Uint8Array(master), "HKDF", false, ["deriveKey", "deriveBits"]);
|
|
24
|
+
const [enc, tag2, kidBits] = await Promise.all([
|
|
25
|
+
subtle().deriveKey(info("enc"), base, { name: "AES-GCM", length: 256 }, false, ["encrypt", "decrypt"]),
|
|
26
|
+
subtle().deriveKey(info("tag"), base, { name: "HMAC", hash: "SHA-256", length: 256 }, false, ["sign"]),
|
|
27
|
+
subtle().deriveBits(info("kid"), base, 48)
|
|
28
|
+
]);
|
|
29
|
+
return { enc, tag: tag2, kid: toBase64Url(new Uint8Array(kidBits)), master: new Uint8Array(master) };
|
|
30
|
+
}
|
|
31
|
+
function pad(bytes) {
|
|
32
|
+
const out = new Uint8Array(Math.ceil((bytes.length + 1) / PAD_BLOCK) * PAD_BLOCK);
|
|
33
|
+
out.set(bytes);
|
|
34
|
+
out[bytes.length] = 128;
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
async function seal(keys, plaintext, aad2) {
|
|
38
|
+
const iv = globalThis.crypto.getRandomValues(new Uint8Array(12));
|
|
39
|
+
const ct = await subtle().encrypt({ name: "AES-GCM", iv, additionalData: utf8.encode(aad2) }, keys.enc, pad(utf8.encode(plaintext)));
|
|
40
|
+
return `${VERSION}.${keys.kid}.${toBase64Url(iv)}.${toBase64Url(new Uint8Array(ct))}`;
|
|
41
|
+
}
|
|
42
|
+
async function tag(keys, value) {
|
|
43
|
+
const mac = await subtle().sign("HMAC", keys.tag, utf8.encode(value));
|
|
44
|
+
return toBase64Url(new Uint8Array(mac).subarray(0, TAG_BYTES));
|
|
45
|
+
}
|
|
46
|
+
var sealerFor = (keys) => ({
|
|
47
|
+
seal: (plaintext, aad2) => seal(keys, plaintext, aad2),
|
|
48
|
+
tag: (value) => tag(keys, value)
|
|
49
|
+
});
|
|
50
|
+
async function keysFromBase64Url(master) {
|
|
51
|
+
return deriveKeys(fromBase64Url(master.trim()));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ../../shared/src/kinds.ts
|
|
55
|
+
var AGENT_SOURCES = ["claude-code", "codex", "antigravity", "claude-desktop", "claude-cloud", "manual"];
|
|
56
|
+
|
|
57
|
+
// ../../shared/src/normalize.ts
|
|
58
|
+
var str = (v) => typeof v === "string" && v.length > 0 ? v : void 0;
|
|
59
|
+
var oneLine = (s, max = 120) => {
|
|
60
|
+
const flat = s.replace(/\s+/g, " ").trim();
|
|
61
|
+
return flat.length > max ? `${flat.slice(0, max - 1)}\u2026` : flat;
|
|
62
|
+
};
|
|
63
|
+
function commandText(v) {
|
|
64
|
+
if (!Array.isArray(v)) return str(v);
|
|
65
|
+
const parts = v.filter((x) => typeof x === "string");
|
|
66
|
+
const shell = parts.length === 3 && /^-l?c$/.test(parts[1] ?? "");
|
|
67
|
+
return str(shell ? parts[2] : parts.join(" "));
|
|
68
|
+
}
|
|
69
|
+
function summarizeToolInput(input) {
|
|
70
|
+
if (!input || typeof input !== "object") return void 0;
|
|
71
|
+
const i = input;
|
|
72
|
+
const first = (list, key) => Array.isArray(list) ? str(list[0]?.[key]) : void 0;
|
|
73
|
+
const pick = commandText(i.command) ?? str(i.file_path) ?? str(i.path) ?? str(i.pattern) ?? str(i.url) ?? str(i.query) ?? str(i.description) ?? str(i.prompt) ?? first(i.questions, "question") ?? str(i.plan) ?? // ExitPlanMode: { plan }
|
|
74
|
+
// Codex webrun: { search_query: [{ q }] } 또는 { weather: [{ location }] }
|
|
75
|
+
first(i.search_query, "q") ?? first(i.weather, "location");
|
|
76
|
+
return pick ? oneLine(pick) : void 0;
|
|
77
|
+
}
|
|
78
|
+
var USER_BLOCKING_TOOLS = {
|
|
79
|
+
AskUserQuestion: "question",
|
|
80
|
+
ExitPlanMode: "plan"
|
|
81
|
+
};
|
|
82
|
+
function waitingFromNotification(kind) {
|
|
83
|
+
switch (kind) {
|
|
84
|
+
case "permission_prompt":
|
|
85
|
+
return "permission";
|
|
86
|
+
case "idle_prompt":
|
|
87
|
+
return "idle";
|
|
88
|
+
case "elicitation_dialog":
|
|
89
|
+
case "elicitation_url_dialog":
|
|
90
|
+
return "elicitation";
|
|
91
|
+
case "agent_needs_input":
|
|
92
|
+
return "input";
|
|
93
|
+
default:
|
|
94
|
+
return void 0;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function normalizeHook(source, raw, receivedAt) {
|
|
98
|
+
const nativeType = str(raw.hook_event_name);
|
|
99
|
+
const sessionId = str(raw.session_id);
|
|
100
|
+
if (!nativeType || !sessionId) return null;
|
|
101
|
+
let type = "notice";
|
|
102
|
+
let text;
|
|
103
|
+
let waitingReason;
|
|
104
|
+
let tool;
|
|
105
|
+
const toolInfo = (failed) => {
|
|
106
|
+
const name = str(raw.tool_name);
|
|
107
|
+
if (!name) return void 0;
|
|
108
|
+
return {
|
|
109
|
+
name,
|
|
110
|
+
useId: str(raw.tool_use_id),
|
|
111
|
+
summary: summarizeToolInput(raw.tool_input),
|
|
112
|
+
...failed ? { failed } : {}
|
|
113
|
+
};
|
|
114
|
+
};
|
|
115
|
+
switch (nativeType) {
|
|
116
|
+
case "SessionStart":
|
|
117
|
+
type = "session_start";
|
|
118
|
+
break;
|
|
119
|
+
case "SessionEnd":
|
|
120
|
+
type = "session_end";
|
|
121
|
+
break;
|
|
122
|
+
case "UserPromptSubmit":
|
|
123
|
+
type = "prompt";
|
|
124
|
+
text = str(raw.user_prompt) ?? str(raw.prompt);
|
|
125
|
+
break;
|
|
126
|
+
case "PreToolUse": {
|
|
127
|
+
tool = toolInfo();
|
|
128
|
+
const reason = tool && USER_BLOCKING_TOOLS[tool.name];
|
|
129
|
+
type = reason ? "waiting" : "tool_start";
|
|
130
|
+
waitingReason = reason;
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
case "PostToolUse":
|
|
134
|
+
type = "tool_end";
|
|
135
|
+
tool = toolInfo();
|
|
136
|
+
break;
|
|
137
|
+
case "PostToolUseFailure":
|
|
138
|
+
type = "tool_end";
|
|
139
|
+
tool = toolInfo(true);
|
|
140
|
+
text = str(raw.error_message);
|
|
141
|
+
break;
|
|
142
|
+
case "PermissionRequest":
|
|
143
|
+
type = "waiting";
|
|
144
|
+
waitingReason = "permission";
|
|
145
|
+
tool = toolInfo();
|
|
146
|
+
break;
|
|
147
|
+
case "Notification": {
|
|
148
|
+
const kind = str(raw.notification_type);
|
|
149
|
+
waitingReason = waitingFromNotification(kind);
|
|
150
|
+
type = waitingReason ? "waiting" : "notice";
|
|
151
|
+
text = str(raw.message);
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
case "Stop":
|
|
155
|
+
case "Interrupt":
|
|
156
|
+
type = "turn_end";
|
|
157
|
+
text = str(raw.last_assistant_message);
|
|
158
|
+
break;
|
|
159
|
+
case "StopFailure":
|
|
160
|
+
type = "error";
|
|
161
|
+
text = str(raw.error_message) ?? str(raw.error_type);
|
|
162
|
+
break;
|
|
163
|
+
case "SubagentStart":
|
|
164
|
+
type = "subagent_start";
|
|
165
|
+
text = str(raw.task_description);
|
|
166
|
+
break;
|
|
167
|
+
case "SubagentStop":
|
|
168
|
+
type = "subagent_end";
|
|
169
|
+
text = str(raw.last_assistant_message);
|
|
170
|
+
break;
|
|
171
|
+
default:
|
|
172
|
+
type = "notice";
|
|
173
|
+
}
|
|
174
|
+
return {
|
|
175
|
+
source,
|
|
176
|
+
sessionId,
|
|
177
|
+
type,
|
|
178
|
+
nativeType,
|
|
179
|
+
at: receivedAt,
|
|
180
|
+
cwd: str(raw.cwd),
|
|
181
|
+
turnId: str(raw.turn_id) ?? str(raw.prompt_id),
|
|
182
|
+
agentId: str(raw.agent_id),
|
|
183
|
+
agentType: str(raw.agent_type),
|
|
184
|
+
tool,
|
|
185
|
+
text,
|
|
186
|
+
waitingReason,
|
|
187
|
+
transcriptPath: str(raw.transcript_path),
|
|
188
|
+
model: str(raw.model),
|
|
189
|
+
raw
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
function normalizeCodexNotify(raw, receivedAt) {
|
|
193
|
+
const nativeType = str(raw.type);
|
|
194
|
+
const sessionId = str(raw["thread-id"]);
|
|
195
|
+
if (!nativeType || !sessionId) return null;
|
|
196
|
+
return {
|
|
197
|
+
source: "codex",
|
|
198
|
+
sessionId,
|
|
199
|
+
type: nativeType === "agent-turn-complete" ? "turn_end" : "notice",
|
|
200
|
+
nativeType,
|
|
201
|
+
at: receivedAt,
|
|
202
|
+
cwd: str(raw.cwd),
|
|
203
|
+
turnId: str(raw["turn-id"]),
|
|
204
|
+
text: str(raw["last-assistant-message"]),
|
|
205
|
+
raw
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
function agyTool(call, failed) {
|
|
209
|
+
const name = str(call?.name);
|
|
210
|
+
if (!name) return void 0;
|
|
211
|
+
const args = call?.args ?? {};
|
|
212
|
+
const summary = str(args.CommandLine) ?? summarizeToolInput(args) ?? str(args.toolSummary);
|
|
213
|
+
return { name, summary: summary && oneLine(summary), ...failed ? { failed } : {} };
|
|
214
|
+
}
|
|
215
|
+
function normalizeAntigravity(raw, receivedAt) {
|
|
216
|
+
const nativeType = str(raw.hook_event_name);
|
|
217
|
+
const sessionId = str(raw.conversationId);
|
|
218
|
+
if (!nativeType || !sessionId) return null;
|
|
219
|
+
let type;
|
|
220
|
+
let text;
|
|
221
|
+
let tool;
|
|
222
|
+
let waitingReason;
|
|
223
|
+
const call = raw.toolCall;
|
|
224
|
+
const error = str(raw.error);
|
|
225
|
+
switch (nativeType) {
|
|
226
|
+
case "PreInvocation":
|
|
227
|
+
if (raw.invocationNum !== 0) return null;
|
|
228
|
+
type = "prompt";
|
|
229
|
+
text = str(raw.prompt);
|
|
230
|
+
break;
|
|
231
|
+
case "PreToolUse":
|
|
232
|
+
tool = agyTool(call);
|
|
233
|
+
if (!tool) return null;
|
|
234
|
+
type = "tool_start";
|
|
235
|
+
break;
|
|
236
|
+
case "PostToolUse":
|
|
237
|
+
tool = agyTool(call, !!error);
|
|
238
|
+
if (!tool) return null;
|
|
239
|
+
type = "tool_end";
|
|
240
|
+
text = error;
|
|
241
|
+
break;
|
|
242
|
+
case "Stop":
|
|
243
|
+
type = error ? "error" : "turn_end";
|
|
244
|
+
text = error ?? str(raw.last_assistant_message);
|
|
245
|
+
break;
|
|
246
|
+
// 아래 둘은 agy 가 아니라 bitl-agy-watch.mjs 가 대화 DB 의 단계 상태를 보고 만든다
|
|
247
|
+
case "bitl:PermissionWaiting":
|
|
248
|
+
type = "waiting";
|
|
249
|
+
waitingReason = "permission";
|
|
250
|
+
tool = agyTool(call);
|
|
251
|
+
break;
|
|
252
|
+
case "bitl:PermissionResolved":
|
|
253
|
+
type = "progress";
|
|
254
|
+
break;
|
|
255
|
+
default:
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
const workspaces = Array.isArray(raw.workspacePaths) ? raw.workspacePaths : [];
|
|
259
|
+
return {
|
|
260
|
+
source: "antigravity",
|
|
261
|
+
sessionId,
|
|
262
|
+
type,
|
|
263
|
+
nativeType,
|
|
264
|
+
at: receivedAt,
|
|
265
|
+
cwd: str(workspaces[0]),
|
|
266
|
+
tool,
|
|
267
|
+
text,
|
|
268
|
+
waitingReason,
|
|
269
|
+
transcriptPath: str(raw.transcriptPath),
|
|
270
|
+
model: str(raw.modelName),
|
|
271
|
+
raw
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
var REPORT_TYPE = {
|
|
275
|
+
start: "prompt",
|
|
276
|
+
progress: "progress",
|
|
277
|
+
waiting: "waiting",
|
|
278
|
+
done: "turn_end",
|
|
279
|
+
error: "error",
|
|
280
|
+
end: "session_end"
|
|
281
|
+
};
|
|
282
|
+
function normalizeReport(source, raw, receivedAt) {
|
|
283
|
+
const event = str(raw.event);
|
|
284
|
+
const sessionId = str(raw.session_id);
|
|
285
|
+
if (!event || !sessionId || !(event in REPORT_TYPE)) return null;
|
|
286
|
+
const title = str(raw.title);
|
|
287
|
+
const detail = str(raw.detail);
|
|
288
|
+
return {
|
|
289
|
+
source,
|
|
290
|
+
sessionId,
|
|
291
|
+
type: REPORT_TYPE[event],
|
|
292
|
+
nativeType: `report:${event}`,
|
|
293
|
+
at: receivedAt,
|
|
294
|
+
cwd: str(raw.cwd),
|
|
295
|
+
// start 는 제목이 곧 지시 내용, 나머지는 상세 설명
|
|
296
|
+
text: event === "start" ? title ?? detail : detail,
|
|
297
|
+
request: str(raw.request),
|
|
298
|
+
waitingReason: event === "waiting" ? "input" : void 0,
|
|
299
|
+
raw
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
function normalizeRecord(rec) {
|
|
303
|
+
const at = rec.receivedAt && !Number.isNaN(Date.parse(rec.receivedAt)) ? new Date(rec.receivedAt).toISOString() : (/* @__PURE__ */ new Date()).toISOString();
|
|
304
|
+
if (rec.source === "codex-notify") return normalizeCodexNotify(rec.payload, at);
|
|
305
|
+
if (rec.source === "antigravity") return normalizeAntigravity(rec.payload, at);
|
|
306
|
+
if (!AGENT_SOURCES.includes(rec.source)) return null;
|
|
307
|
+
const source = rec.source;
|
|
308
|
+
const isReport = typeof rec.payload.event === "string" && rec.payload.hook_event_name === void 0;
|
|
309
|
+
return isReport ? normalizeReport(source, rec.payload, at) : normalizeHook(source, rec.payload, at);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// ../../shared/src/session.ts
|
|
313
|
+
var sessionKey = (source, sessionId) => `${source}:${sessionId}`;
|
|
314
|
+
|
|
315
|
+
// ../../shared/src/prepare.ts
|
|
316
|
+
var WIRE_VERSION = 2;
|
|
317
|
+
var TEXT_LIMIT = 500;
|
|
318
|
+
var TITLE_LIMIT = 80;
|
|
319
|
+
var clip = (s, max = TEXT_LIMIT) => {
|
|
320
|
+
const t = s.trim();
|
|
321
|
+
return t.length > max ? `${t.slice(0, max - 1)}\u2026` : t;
|
|
322
|
+
};
|
|
323
|
+
var carriesContext = (e) => e.type === "session_start" || e.type === "prompt" || e.type === "turn_end" || e.nativeType === "CwdChanged";
|
|
324
|
+
function activityOf(e) {
|
|
325
|
+
if (!e.tool) return void 0;
|
|
326
|
+
const base = e.tool.summary ? `${e.tool.name}: ${e.tool.summary}` : e.tool.name;
|
|
327
|
+
return e.agentId && e.agentType ? `[${e.agentType}] ${base}` : base;
|
|
328
|
+
}
|
|
329
|
+
function splitEvent(e, extra = {}) {
|
|
330
|
+
const newCwd = e.nativeType === "CwdChanged" && typeof e.raw.new_cwd === "string" ? e.raw.new_cwd : void 0;
|
|
331
|
+
const signal = {
|
|
332
|
+
source: e.source,
|
|
333
|
+
sessionId: e.sessionId,
|
|
334
|
+
type: e.type,
|
|
335
|
+
nativeType: e.nativeType,
|
|
336
|
+
at: e.at,
|
|
337
|
+
turnId: e.turnId,
|
|
338
|
+
agentId: e.agentId,
|
|
339
|
+
agentNamed: e.agentId || e.agentType !== void 0 ? Boolean(e.agentType) : void 0,
|
|
340
|
+
waitingReason: e.waitingReason,
|
|
341
|
+
toolFailed: e.tool?.failed || void 0
|
|
342
|
+
};
|
|
343
|
+
const content = {
|
|
344
|
+
title: e.type === "prompt" && e.text ? oneLine(e.text, TITLE_LIMIT) : void 0,
|
|
345
|
+
text: e.text ? clip(e.text) : void 0,
|
|
346
|
+
request: e.request ? clip(e.request) : void 0,
|
|
347
|
+
activity: activityOf(e),
|
|
348
|
+
tool: e.tool?.name,
|
|
349
|
+
toolSummary: e.tool?.summary,
|
|
350
|
+
agentType: e.agentType || void 0,
|
|
351
|
+
cwd: newCwd ?? (carriesContext(e) ? e.cwd : void 0),
|
|
352
|
+
model: e.model,
|
|
353
|
+
...extra
|
|
354
|
+
};
|
|
355
|
+
const tagSource = e.type === "prompt" ? e.request ?? e.text : e.request;
|
|
356
|
+
return { signal: dropUndefined(signal), content: dropUndefined(content), tagSource };
|
|
357
|
+
}
|
|
358
|
+
function dropUndefined(o) {
|
|
359
|
+
return Object.fromEntries(Object.entries(o).filter(([, v]) => v !== void 0 && v !== ""));
|
|
360
|
+
}
|
|
361
|
+
async function prepareRecord(e, sealer, extra = {}) {
|
|
362
|
+
const { signal, content, tagSource } = splitEvent(e, extra);
|
|
363
|
+
const aad2 = sessionKey(e.source, e.sessionId);
|
|
364
|
+
const sealed = {};
|
|
365
|
+
await Promise.all(
|
|
366
|
+
Object.entries(content).map(async ([k, v]) => {
|
|
367
|
+
sealed[k] = await sealer.seal(v, aad2);
|
|
368
|
+
})
|
|
369
|
+
);
|
|
370
|
+
if (tagSource) signal.requestTag = await sealer.tag(tagSource);
|
|
371
|
+
return { v: WIRE_VERSION, signal, content: sealed };
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// ../../shared/src/node/antigravity.ts
|
|
375
|
+
import { readFile, stat } from "node:fs/promises";
|
|
376
|
+
var MAX_TRANSCRIPT_BYTES = 20 * 1024 * 1024;
|
|
377
|
+
var RETRY_MS = 150;
|
|
378
|
+
var RETRIES = 6;
|
|
379
|
+
async function readSteps(path) {
|
|
380
|
+
try {
|
|
381
|
+
if ((await stat(path)).size > MAX_TRANSCRIPT_BYTES) return [];
|
|
382
|
+
const steps = [];
|
|
383
|
+
for (const line of (await readFile(path, "utf8")).split("\n")) {
|
|
384
|
+
if (!line.trim()) continue;
|
|
385
|
+
try {
|
|
386
|
+
steps.push(JSON.parse(line));
|
|
387
|
+
} catch {
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
return steps;
|
|
391
|
+
} catch {
|
|
392
|
+
return [];
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
function userRequestText(content) {
|
|
396
|
+
const m = /<USER_REQUEST>\s*([\s\S]*?)\s*<\/USER_REQUEST>/.exec(content);
|
|
397
|
+
return (m?.[1] ?? content).trim();
|
|
398
|
+
}
|
|
399
|
+
async function fillAntigravityText(payload) {
|
|
400
|
+
const path = typeof payload.transcriptPath === "string" ? payload.transcriptPath : void 0;
|
|
401
|
+
const event = payload.hook_event_name;
|
|
402
|
+
if (!path) return payload;
|
|
403
|
+
if (event === "PreInvocation" && payload.invocationNum === 0 && payload.prompt === void 0) {
|
|
404
|
+
const index = typeof payload.initialNumSteps === "number" ? payload.initialNumSteps - 1 : void 0;
|
|
405
|
+
for (let i = 0; i <= RETRIES; i++) {
|
|
406
|
+
const steps = await readSteps(path);
|
|
407
|
+
const input = steps.filter((s) => s.type === "USER_INPUT" && (index === void 0 || s.step_index === index)).at(-1);
|
|
408
|
+
if (typeof input?.content === "string") return { ...payload, prompt: userRequestText(input.content) };
|
|
409
|
+
await new Promise((r) => setTimeout(r, RETRY_MS));
|
|
410
|
+
}
|
|
411
|
+
return payload;
|
|
412
|
+
}
|
|
413
|
+
if (event === "Stop" && payload.last_assistant_message === void 0) {
|
|
414
|
+
const steps = await readSteps(path);
|
|
415
|
+
const reply = steps.filter((s) => s.type === "PLANNER_RESPONSE" && typeof s.content === "string" && s.content.trim()).at(-1);
|
|
416
|
+
return reply ? { ...payload, last_assistant_message: reply.content.trim() } : payload;
|
|
417
|
+
}
|
|
418
|
+
return payload;
|
|
419
|
+
}
|
|
420
|
+
var AGY_STEP = { RUNNING: 2, DONE: 3, CANCELLED: 6, REJECTED: 7, WAITING: 9 };
|
|
421
|
+
function agyWatchStep(status, waited) {
|
|
422
|
+
if (status === AGY_STEP.WAITING) return waited ? "none" : "waiting";
|
|
423
|
+
if (status === AGY_STEP.DONE || status === AGY_STEP.CANCELLED || status === AGY_STEP.REJECTED) return waited ? "resolved" : "stop";
|
|
424
|
+
if (status === AGY_STEP.RUNNING && waited) return "resolved";
|
|
425
|
+
return "none";
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// ../../shared/src/node/cloud.ts
|
|
429
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
430
|
+
import { dirname as dirname2, join as join2 } from "node:path";
|
|
431
|
+
|
|
432
|
+
// ../../shared/src/node/keys.ts
|
|
433
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
434
|
+
import { homedir } from "node:os";
|
|
435
|
+
import { dirname, join } from "node:path";
|
|
436
|
+
var bitlHome = (env = process.env.BITL_HOME) => env?.trim() ? env.trim() : join(homedir(), ".bitl");
|
|
437
|
+
var keyPath = (home = bitlHome()) => join(home, "key");
|
|
438
|
+
function loadOrCreateMasterKey(home = bitlHome(), env = process.env.BITL_KEY) {
|
|
439
|
+
if (env?.trim()) return env.trim();
|
|
440
|
+
const file = keyPath(home);
|
|
441
|
+
try {
|
|
442
|
+
const existing = readFileSync(file, "utf8").trim();
|
|
443
|
+
if (existing) return existing;
|
|
444
|
+
} catch {
|
|
445
|
+
}
|
|
446
|
+
const key = toBase64Url(generateMasterKey());
|
|
447
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
448
|
+
try {
|
|
449
|
+
writeFileSync(file, key + "\n", { encoding: "utf8", mode: 384, flag: "wx" });
|
|
450
|
+
return key;
|
|
451
|
+
} catch (err) {
|
|
452
|
+
if (err.code === "EEXIST") return readFileSync(file, "utf8").trim();
|
|
453
|
+
throw err;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
var loadKeys = (home = bitlHome()) => keysFromBase64Url(loadOrCreateMasterKey(home));
|
|
457
|
+
|
|
458
|
+
// ../../shared/src/node/cloud.ts
|
|
459
|
+
var cloudConfigPath = (home = bitlHome()) => join2(home, "cloud.json");
|
|
460
|
+
var cloudKeyPath = (home = bitlHome()) => join2(home, "cloud-key");
|
|
461
|
+
function readCloudConfig(home = bitlHome()) {
|
|
462
|
+
try {
|
|
463
|
+
const c = JSON.parse(readFileSync2(cloudConfigPath(home), "utf8"));
|
|
464
|
+
return c.url && c.deviceId && c.apiKey ? c : null;
|
|
465
|
+
} catch {
|
|
466
|
+
return null;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
function resolveTarget(home = bitlHome()) {
|
|
470
|
+
const cloud = readCloudConfig(home);
|
|
471
|
+
if (cloud) {
|
|
472
|
+
return {
|
|
473
|
+
kind: "cloud",
|
|
474
|
+
url: cloud.url.replace(/\/+$/, ""),
|
|
475
|
+
headers: { authorization: `Bearer ${cloud.apiKey}` },
|
|
476
|
+
keys: () => keysFromBase64Url(readFileSync2(cloudKeyPath(home), "utf8"))
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
return {
|
|
480
|
+
kind: "local",
|
|
481
|
+
url: (process.env.BITL_URL?.trim() || `http://127.0.0.1:${process.env.BITL_PORT ?? 7777}`).replace(/\/+$/, ""),
|
|
482
|
+
headers: {},
|
|
483
|
+
keys: () => loadKeys(home)
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// ../../shared/src/node/enrich.ts
|
|
488
|
+
import { execFile } from "node:child_process";
|
|
489
|
+
import { readFile as readFile2, stat as stat2 } from "node:fs/promises";
|
|
490
|
+
import { basename } from "node:path";
|
|
491
|
+
var BRANCH_TTL_MS = 6e4;
|
|
492
|
+
var MAX_TRANSCRIPT_BYTES2 = 20 * 1024 * 1024;
|
|
493
|
+
var branchCache = /* @__PURE__ */ new Map();
|
|
494
|
+
function gitBranch(cwd) {
|
|
495
|
+
const hit = branchCache.get(cwd);
|
|
496
|
+
if (hit && Date.now() - hit.at < BRANCH_TTL_MS) return Promise.resolve(hit.branch);
|
|
497
|
+
return new Promise((resolve) => {
|
|
498
|
+
execFile("git", ["-C", cwd, "branch", "--show-current"], { timeout: 2e3, windowsHide: true }, (err, stdout) => {
|
|
499
|
+
const branch = err ? void 0 : stdout.trim() || void 0;
|
|
500
|
+
branchCache.set(cwd, { at: Date.now(), branch });
|
|
501
|
+
resolve(branch);
|
|
502
|
+
});
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
async function aiTitleFromTranscript(path) {
|
|
506
|
+
try {
|
|
507
|
+
if ((await stat2(path)).size > MAX_TRANSCRIPT_BYTES2) return void 0;
|
|
508
|
+
const text = await readFile2(path, "utf8");
|
|
509
|
+
let title;
|
|
510
|
+
for (const line of text.split("\n")) {
|
|
511
|
+
if (!line.includes('"ai-title"')) continue;
|
|
512
|
+
try {
|
|
513
|
+
const rec = JSON.parse(line);
|
|
514
|
+
if (rec.type === "ai-title" && rec.aiTitle) title = rec.aiTitle;
|
|
515
|
+
} catch {
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
return title;
|
|
519
|
+
} catch {
|
|
520
|
+
return void 0;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
function gitRoot(cwd) {
|
|
524
|
+
const hit = rootCache.get(cwd);
|
|
525
|
+
if (hit && Date.now() - hit.at < BRANCH_TTL_MS) return Promise.resolve(hit.root);
|
|
526
|
+
return new Promise((resolve) => {
|
|
527
|
+
execFile("git", ["-C", cwd, "rev-parse", "--show-toplevel"], { timeout: 2e3, windowsHide: true }, (err, stdout) => {
|
|
528
|
+
const root = err ? void 0 : stdout.trim() || void 0;
|
|
529
|
+
rootCache.set(cwd, { at: Date.now(), root });
|
|
530
|
+
resolve(root);
|
|
531
|
+
});
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
var rootCache = /* @__PURE__ */ new Map();
|
|
535
|
+
async function projectName(cwd) {
|
|
536
|
+
const root = await gitRoot(cwd);
|
|
537
|
+
return basename((root ?? cwd).replace(/[\\/]+$/, ""));
|
|
538
|
+
}
|
|
539
|
+
async function enrichEvent(e) {
|
|
540
|
+
const extra = {};
|
|
541
|
+
const newCwd = e.nativeType === "CwdChanged" && typeof e.raw.new_cwd === "string" ? e.raw.new_cwd : void 0;
|
|
542
|
+
const cwd = newCwd ?? e.cwd;
|
|
543
|
+
const tasks = [];
|
|
544
|
+
if (cwd && carriesContext(e)) {
|
|
545
|
+
tasks.push(
|
|
546
|
+
Promise.all([gitBranch(cwd), projectName(cwd)]).then(([branch, project]) => {
|
|
547
|
+
if (branch) extra.gitBranch = branch;
|
|
548
|
+
extra.project = project;
|
|
549
|
+
})
|
|
550
|
+
);
|
|
551
|
+
}
|
|
552
|
+
if (e.source === "claude-code" && e.transcriptPath && (e.type === "prompt" || e.type === "turn_end")) {
|
|
553
|
+
tasks.push(
|
|
554
|
+
aiTitleFromTranscript(e.transcriptPath).then((title) => {
|
|
555
|
+
if (title) extra.aiTitle = title;
|
|
556
|
+
})
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
await Promise.all(tasks.map((t) => t.catch(() => {
|
|
560
|
+
})));
|
|
561
|
+
return extra;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// ../../shared/src/approval.ts
|
|
565
|
+
var APPROVAL_SIG_PATTERN = /^[A-Za-z0-9_-]{43}$/;
|
|
566
|
+
var MAX_DETAIL = 4e3;
|
|
567
|
+
var clip2 = (s, n) => s.length > n ? `${s.slice(0, n - 1)}\u2026` : s;
|
|
568
|
+
var str2 = (v) => typeof v === "string" && v.length > 0 ? v : void 0;
|
|
569
|
+
function newApprovalId() {
|
|
570
|
+
return toBase64Url(globalThis.crypto.getRandomValues(new Uint8Array(16)));
|
|
571
|
+
}
|
|
572
|
+
function describeToolRequest(toolName, input, extra = {}) {
|
|
573
|
+
const i = input && typeof input === "object" ? input : {};
|
|
574
|
+
const short = summarizeToolInput(i);
|
|
575
|
+
let detail;
|
|
576
|
+
if (typeof i.command === "string") detail = i.command;
|
|
577
|
+
else if (typeof i.file_path === "string") {
|
|
578
|
+
const body = str2(i.content) ?? (typeof i.new_string === "string" ? `- ${oneLine(String(i.old_string ?? ""), 300)}
|
|
579
|
+
+ ${i.new_string}` : void 0) ?? (Array.isArray(i.edits) ? `${i.edits.length}\uACF3 \uC218\uC815` : void 0);
|
|
580
|
+
detail = body ? `${i.file_path}
|
|
581
|
+
|
|
582
|
+
${body}` : i.file_path;
|
|
583
|
+
} else if (typeof i.plan === "string") detail = i.plan;
|
|
584
|
+
else if (Object.keys(i).length) detail = JSON.stringify(i, null, 2);
|
|
585
|
+
return {
|
|
586
|
+
tool: toolName,
|
|
587
|
+
summary: oneLine(short ? `${toolName}: ${short}` : toolName, 200),
|
|
588
|
+
detail: detail ? clip2(detail, MAX_DETAIL) : void 0,
|
|
589
|
+
description: str2(i.description) ? oneLine(String(i.description), 300) : void 0,
|
|
590
|
+
...extra
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
var aad = (id) => `approval:${id}`;
|
|
594
|
+
var sealApprovalRequest = (keys, id, req) => seal(keys, JSON.stringify(req), aad(id));
|
|
595
|
+
var approvalKeys = /* @__PURE__ */ new WeakMap();
|
|
596
|
+
function approvalKey(keys) {
|
|
597
|
+
let k = approvalKeys.get(keys);
|
|
598
|
+
if (!k) {
|
|
599
|
+
k = globalThis.crypto.subtle.importKey("raw", keys.master, "HKDF", false, ["deriveKey"]).then(
|
|
600
|
+
(base) => globalThis.crypto.subtle.deriveKey(
|
|
601
|
+
{ name: "HKDF", hash: "SHA-256", salt: new Uint8Array(0), info: new TextEncoder().encode("bitl/v1/approve") },
|
|
602
|
+
base,
|
|
603
|
+
{ name: "HMAC", hash: "SHA-256", length: 256 },
|
|
604
|
+
false,
|
|
605
|
+
["sign", "verify"]
|
|
606
|
+
)
|
|
607
|
+
);
|
|
608
|
+
approvalKeys.set(keys, k);
|
|
609
|
+
}
|
|
610
|
+
return k;
|
|
611
|
+
}
|
|
612
|
+
var message = (id, decision) => new TextEncoder().encode(`bitl/approval/v1
|
|
613
|
+
${id}
|
|
614
|
+
${decision}`);
|
|
615
|
+
async function verifyDecision(keys, id, decision, sig) {
|
|
616
|
+
if (decision !== "allow" && decision !== "deny" || !APPROVAL_SIG_PATTERN.test(sig)) return false;
|
|
617
|
+
return globalThis.crypto.subtle.verify("HMAC", await approvalKey(keys), fromBase64Url(sig), message(id, decision));
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// ../../shared/src/node/approve.ts
|
|
621
|
+
var DEFAULT_APPROVAL_WAIT_MS = 10 * 6e4;
|
|
622
|
+
var SKIP_TOOLS = /* @__PURE__ */ new Set(["AskUserQuestion"]);
|
|
623
|
+
var POLL_MS = 25e3;
|
|
624
|
+
var DENY_MESSAGE = "\uC0AC\uC6A9\uC790\uAC00 \uD734\uB300\uD3F0(BITL \uC6D0\uACA9 \uC2B9\uC778)\uC5D0\uC11C \uAC70\uBD80\uD588\uC2B5\uB2C8\uB2E4.";
|
|
625
|
+
var sleep = (ms, signal) => new Promise((resolve) => {
|
|
626
|
+
const t = setTimeout(resolve, ms);
|
|
627
|
+
signal?.addEventListener("abort", () => (clearTimeout(t), resolve()), { once: true });
|
|
628
|
+
});
|
|
629
|
+
async function requestRemoteApproval(payload, opts = {}) {
|
|
630
|
+
const log = opts.log ?? (() => {
|
|
631
|
+
});
|
|
632
|
+
const tool = typeof payload.tool_name === "string" ? payload.tool_name : void 0;
|
|
633
|
+
const sessionId = typeof payload.session_id === "string" ? payload.session_id : void 0;
|
|
634
|
+
if (!tool || !sessionId || SKIP_TOOLS.has(tool)) return null;
|
|
635
|
+
const target = resolveTarget(opts.home ?? bitlHome());
|
|
636
|
+
const api = async (path, init) => {
|
|
637
|
+
const signals = [AbortSignal.timeout(init.timeoutMs), ...opts.signal && !init.keepOnAbort ? [opts.signal] : []];
|
|
638
|
+
const res = await fetch(`${target.url}${path}`, {
|
|
639
|
+
...init,
|
|
640
|
+
headers: { "content-type": "application/json", ...target.headers },
|
|
641
|
+
signal: AbortSignal.any(signals)
|
|
642
|
+
});
|
|
643
|
+
if (!res.ok) throw new Error(`${path}: HTTP ${res.status}`);
|
|
644
|
+
return await res.json();
|
|
645
|
+
};
|
|
646
|
+
try {
|
|
647
|
+
if ((await api("/api/approvals/mode", { timeoutMs: 2e3 })).away !== true) return null;
|
|
648
|
+
} catch (err) {
|
|
649
|
+
log(`\uC6D0\uACA9 \uC2B9\uC778: \uC11C\uBC84\uC5D0 \uBB3C\uC744 \uC218 \uC5C6\uC5B4 \uD130\uBBF8\uB110\uC5D0\uC11C \uC2B9\uC778\uD569\uB2C8\uB2E4 (${err.message})`);
|
|
650
|
+
return null;
|
|
651
|
+
}
|
|
652
|
+
const keys = await target.keys();
|
|
653
|
+
const id = newApprovalId();
|
|
654
|
+
const cwd = typeof payload.cwd === "string" ? payload.cwd : void 0;
|
|
655
|
+
const project = cwd ? await projectName(cwd).catch(() => void 0) : void 0;
|
|
656
|
+
const request = await sealApprovalRequest(keys, id, describeToolRequest(tool, payload.tool_input, { project, cwd }));
|
|
657
|
+
const waitMs = opts.waitMs ?? DEFAULT_APPROVAL_WAIT_MS;
|
|
658
|
+
const source = typeof payload.source === "string" ? payload.source : "claude-code";
|
|
659
|
+
try {
|
|
660
|
+
await api("/api/approvals", {
|
|
661
|
+
method: "POST",
|
|
662
|
+
body: JSON.stringify({ id, sessionKey: `${source}:${sessionId}`, request, ttlSec: Math.ceil(waitMs / 1e3) }),
|
|
663
|
+
timeoutMs: 5e3
|
|
664
|
+
});
|
|
665
|
+
} catch (err) {
|
|
666
|
+
log(`\uC6D0\uACA9 \uC2B9\uC778: \uC694\uCCAD\uC744 \uC62C\uB9AC\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4 (${err.message})`);
|
|
667
|
+
return null;
|
|
668
|
+
}
|
|
669
|
+
const deadline = Date.now() + waitMs;
|
|
670
|
+
const cancel = () => api(`/api/approvals/${id}/cancel`, { method: "POST", timeoutMs: 3e3, keepOnAbort: true }).catch(() => void 0);
|
|
671
|
+
while (Date.now() < deadline && !opts.signal?.aborted) {
|
|
672
|
+
let a;
|
|
673
|
+
try {
|
|
674
|
+
const ms = Math.min(POLL_MS, Math.max(deadline - Date.now(), 0));
|
|
675
|
+
a = await api(`/api/approvals/${id}/wait?ms=${ms}`, { timeoutMs: ms + 1e4 });
|
|
676
|
+
} catch {
|
|
677
|
+
await sleep(2e3, opts.signal);
|
|
678
|
+
continue;
|
|
679
|
+
}
|
|
680
|
+
const status = a.status;
|
|
681
|
+
if (status === "pending") continue;
|
|
682
|
+
if (status === "allow" || status === "deny") {
|
|
683
|
+
if (!await verifyDecision(keys, id, status, typeof a.sig === "string" ? a.sig : "")) {
|
|
684
|
+
log(`\uC6D0\uACA9 \uC2B9\uC778: \uC11C\uBA85\uC774 \uB9DE\uC9C0 \uC54A\uB294 \uACB0\uC815\uC744 \uBC1B\uC558\uC2B5\uB2C8\uB2E4 (${id}) \u2014 \uB530\uB974\uC9C0 \uC54A\uACE0 \uD130\uBBF8\uB110\uC5D0\uC11C \uC2B9\uC778\uD569\uB2C8\uB2E4`);
|
|
685
|
+
return null;
|
|
686
|
+
}
|
|
687
|
+
return status === "allow" ? { behavior: "allow" } : { behavior: "deny", message: DENY_MESSAGE };
|
|
688
|
+
}
|
|
689
|
+
return null;
|
|
690
|
+
}
|
|
691
|
+
await cancel();
|
|
692
|
+
return null;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// ../../shared/src/node/index.ts
|
|
696
|
+
async function prepareRaw(rec, opts = {}) {
|
|
697
|
+
const filled = rec.source === "antigravity" && opts.enrich !== false ? { ...rec, payload: await fillAntigravityText(rec.payload) } : rec;
|
|
698
|
+
const e = normalizeRecord(filled);
|
|
699
|
+
if (!e) return null;
|
|
700
|
+
const extra = opts.enrich === false ? {} : await enrichEvent(e);
|
|
701
|
+
const keys = opts.keys ?? await loadKeys(opts.home ?? bitlHome());
|
|
702
|
+
return prepareRecord(e, sealerFor(keys), extra);
|
|
703
|
+
}
|
|
704
|
+
async function prepareForTarget(rec, home = bitlHome()) {
|
|
705
|
+
const target = resolveTarget(home);
|
|
706
|
+
const keys = await target.keys();
|
|
707
|
+
const record = await prepareRaw(rec, { keys, home });
|
|
708
|
+
return { kind: target.kind, url: target.url, headers: target.headers, kid: keys.kid, record };
|
|
709
|
+
}
|
|
710
|
+
export {
|
|
711
|
+
agyWatchStep,
|
|
712
|
+
prepareForTarget,
|
|
713
|
+
prepareRaw,
|
|
714
|
+
requestRemoteApproval
|
|
715
|
+
};
|