@hivefloor/cli 0.1.5 → 0.1.8
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 +6 -20
- package/README.md +29 -8
- package/dist/chunk-2ERPDCOT.js +38 -0
- package/dist/chunk-4G362W6G.js +19 -0
- package/dist/chunk-5OJUJCE3.js +817 -0
- package/dist/{chunk-U6RRUAC2.js → chunk-5XXYQQG6.js} +53 -8
- package/dist/chunk-AGIYVFDQ.js +10984 -0
- package/dist/{chunk-RYGL6XU4.js → chunk-BLLPDPTL.js} +4 -2
- package/dist/{chunk-3N5JYP5O.js → chunk-FPJNGKT5.js} +5 -3
- package/dist/chunk-HW3KRNCO.js +201 -0
- package/dist/{chunk-4DRHR7LR.js → chunk-HWBUESB4.js} +1158 -196
- package/dist/{chunk-C627OKTW.js → chunk-K2IRTZH5.js} +7 -6
- package/dist/chunk-KQMPCV57.js +122 -0
- package/dist/{chunk-J36OIJBM.js → chunk-L6EFW2SJ.js} +14 -10
- package/dist/{chunk-IJRHCDIG.js → chunk-LX5A25O6.js} +4 -2
- package/dist/{chunk-MZGGJ6ZS.js → chunk-MYJCU5JL.js} +4 -2
- package/dist/{chunk-H6NYMF2D.js → chunk-NMUH5NXN.js} +6 -4
- package/dist/{chunk-VX7SCIF2.js → chunk-NRJONTFT.js} +3 -1
- package/dist/chunk-QBBKGORP.js +327 -0
- package/dist/{chunk-SOXRITAW.js → chunk-QGULSOQ2.js} +3 -0
- package/dist/{chunk-6MDJB3BV.js → chunk-QLMLB7RB.js} +17 -9
- package/dist/{chunk-TKMIV7BG.js → chunk-QPAFN47B.js} +3 -1
- package/dist/chunk-SBOEAOHD.js +48 -0
- package/dist/{chunk-IWIDKNWC.js → chunk-TIR4CV3P.js} +21 -17
- package/dist/{chunk-6XVIEX75.js → chunk-X2KH6AR4.js} +25 -303
- package/dist/chunk-XAXKZ572.js +106 -0
- package/dist/{chunk-TPMFQUAW.js → chunk-XPGL7VYE.js} +12 -0
- package/dist/{cli-JCBZICR3.js → cli-BFOJWJNG.js} +56 -20
- package/dist/connect-ZJ4JXBAJ.js +27 -0
- package/dist/control-6GHTZQJV.js +25 -0
- package/dist/{disconnect-WQMDCUXT.js → disconnect-3T25NTKX.js} +24 -17
- package/dist/{hook-AYJNRGIY.js → hook-TPSHBCWU.js} +277 -17
- package/dist/index.js +9 -6
- package/dist/{link-ORT4LID6.js → link-EAS5F5PC.js} +14 -11
- package/dist/{machine-connect-N52CKQEW.js → machine-connect-KEI4BRWF.js} +23 -17
- package/dist/mcp-LVIYR3EN.js +1882 -0
- package/dist/orch-KVRL2YA2.js +137 -0
- package/dist/{run-ZAZ45MKQ.js → run-JL2OZWK3.js} +1920 -28
- package/dist/{status-GOCFNO7K.js → status-QXOOJGGN.js} +14 -11
- package/dist/{token-EMM4AJAU.js → token-BFWWQIDA.js} +10 -7
- package/dist/{voice-F5MIS3IZ.js → voice-QXHFXRWG.js} +5 -3
- package/dist/{where-XFXRTP7E.js → where-CMUORQIO.js} +11 -8
- package/package.json +5 -6
- package/dist/chunk-JCLWCMEM.js +0 -8
- package/dist/chunk-K3NQKI34.js +0 -10
- package/dist/chunk-X6B6R32A.js +0 -303
- package/dist/connect-2HLVPEOL.js +0 -23
- package/dist/mcp-2SF6Y73M.js +0 -118
|
@@ -0,0 +1,817 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire as __hfCreateRequire } from 'node:module';
|
|
3
|
+
const require = __hfCreateRequire(import.meta.url);
|
|
4
|
+
import {
|
|
5
|
+
ensurePrivateDir,
|
|
6
|
+
privateDirTrouble,
|
|
7
|
+
privateFileTrouble,
|
|
8
|
+
safeName
|
|
9
|
+
} from "./chunk-SBOEAOHD.js";
|
|
10
|
+
import {
|
|
11
|
+
runFile
|
|
12
|
+
} from "./chunk-XAXKZ572.js";
|
|
13
|
+
import {
|
|
14
|
+
atomicWrite,
|
|
15
|
+
configDir,
|
|
16
|
+
controlDecisionsDir,
|
|
17
|
+
controlInboxDir,
|
|
18
|
+
isRecord,
|
|
19
|
+
readText,
|
|
20
|
+
removeFile,
|
|
21
|
+
serviceLogFile,
|
|
22
|
+
sessionsFile,
|
|
23
|
+
spoolFile
|
|
24
|
+
} from "./chunk-XPGL7VYE.js";
|
|
25
|
+
import {
|
|
26
|
+
cleanControlText,
|
|
27
|
+
stripControlChars
|
|
28
|
+
} from "./chunk-HWBUESB4.js";
|
|
29
|
+
|
|
30
|
+
// src/service/control/sessions.ts
|
|
31
|
+
import { randomBytes } from "crypto";
|
|
32
|
+
import fs from "fs";
|
|
33
|
+
import path from "path";
|
|
34
|
+
var SESSION_TTL_MS = 24 * 36e5;
|
|
35
|
+
var CWD_MAX = 500;
|
|
36
|
+
function validCwd(v) {
|
|
37
|
+
return typeof v === "string" && v.length > 0 && v.length <= CWD_MAX && path.isAbsolute(v) && !/[\u0000-\u001f\u007f]/.test(v);
|
|
38
|
+
}
|
|
39
|
+
function validTranscript(v) {
|
|
40
|
+
return validCwd(v) && v.endsWith(".jsonl");
|
|
41
|
+
}
|
|
42
|
+
function parseEntry(v, now) {
|
|
43
|
+
if (!isRecord(v) || typeof v.event !== "string" || typeof v.at !== "number" || typeof v.tool !== "string") return void 0;
|
|
44
|
+
if (now - v.at > SESSION_TTL_MS) return void 0;
|
|
45
|
+
const e = { tool: v.tool, event: v.event, at: v.at };
|
|
46
|
+
if (typeof v.pid === "number" && Number.isInteger(v.pid) && v.pid > 0) e.pid = v.pid;
|
|
47
|
+
if (typeof v.start === "string") e.start = v.start;
|
|
48
|
+
if (typeof v.prompt_id === "string") e.prompt_id = v.prompt_id;
|
|
49
|
+
if (typeof v.prompt_at === "number") e.prompt_at = v.prompt_at;
|
|
50
|
+
if (typeof v.msg_at === "number") e.msg_at = v.msg_at;
|
|
51
|
+
if (validCwd(v.cwd)) e.cwd = v.cwd;
|
|
52
|
+
if (validTranscript(v.transcript)) e.transcript = v.transcript;
|
|
53
|
+
if (v.ended === true) e.ended = true;
|
|
54
|
+
return e;
|
|
55
|
+
}
|
|
56
|
+
function readSessions(now, file = sessionsFile()) {
|
|
57
|
+
if (privateFileTrouble(file)) return {};
|
|
58
|
+
try {
|
|
59
|
+
const j = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
60
|
+
if (!isRecord(j)) return {};
|
|
61
|
+
const out = {};
|
|
62
|
+
for (const [id, v] of Object.entries(j)) {
|
|
63
|
+
const e = parseEntry(v, now);
|
|
64
|
+
if (e) out[id] = e;
|
|
65
|
+
}
|
|
66
|
+
return out;
|
|
67
|
+
} catch {
|
|
68
|
+
return {};
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
var LOCK_WAIT_MS = 200;
|
|
72
|
+
var LOCK_STALE_MS = 2e3;
|
|
73
|
+
var LOCK_STEP_MS = 5;
|
|
74
|
+
var pause = (ms) => void Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
75
|
+
function takeStaleLock(lock) {
|
|
76
|
+
let st;
|
|
77
|
+
try {
|
|
78
|
+
st = fs.statSync(lock);
|
|
79
|
+
} catch {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (Date.now() - st.mtimeMs < LOCK_STALE_MS) return;
|
|
83
|
+
const aside = `${lock}.stale-${process.pid}-${randomBytes(4).toString("hex")}`;
|
|
84
|
+
try {
|
|
85
|
+
fs.renameSync(lock, aside);
|
|
86
|
+
} catch {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
if (fs.statSync(aside).ino !== st.ino) fs.linkSync(aside, lock);
|
|
91
|
+
} catch {
|
|
92
|
+
}
|
|
93
|
+
fs.rmSync(aside, { force: true });
|
|
94
|
+
}
|
|
95
|
+
function lockMap(lock) {
|
|
96
|
+
const until = Date.now() + LOCK_WAIT_MS;
|
|
97
|
+
for (; ; ) {
|
|
98
|
+
try {
|
|
99
|
+
fs.closeSync(fs.openSync(lock, "wx", 384));
|
|
100
|
+
return true;
|
|
101
|
+
} catch (e) {
|
|
102
|
+
if (e.code !== "EEXIST") return false;
|
|
103
|
+
}
|
|
104
|
+
takeStaleLock(lock);
|
|
105
|
+
if (Date.now() >= until) return false;
|
|
106
|
+
pause(LOCK_STEP_MS);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
function updateMap(now, mutate, file) {
|
|
110
|
+
let locked = false;
|
|
111
|
+
const lock = `${file}.lock`;
|
|
112
|
+
try {
|
|
113
|
+
ensurePrivateDir(path.dirname(file));
|
|
114
|
+
locked = lockMap(lock);
|
|
115
|
+
if (!locked) return false;
|
|
116
|
+
const map = readSessions(now, file);
|
|
117
|
+
mutate(map);
|
|
118
|
+
atomicWrite(file, `${JSON.stringify(map)}
|
|
119
|
+
`, 384);
|
|
120
|
+
return true;
|
|
121
|
+
} catch {
|
|
122
|
+
return false;
|
|
123
|
+
} finally {
|
|
124
|
+
if (locked) fs.rmSync(lock, { force: true });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function updateSession(id, now, mutate, file = sessionsFile()) {
|
|
128
|
+
updateMap(
|
|
129
|
+
now,
|
|
130
|
+
(map) => {
|
|
131
|
+
const next = mutate(map[id]);
|
|
132
|
+
if (next) map[id] = next;
|
|
133
|
+
else delete map[id];
|
|
134
|
+
},
|
|
135
|
+
file
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
function markSessionsEnded(picks, now, file = sessionsFile()) {
|
|
139
|
+
if (picks.length === 0) return;
|
|
140
|
+
updateMap(
|
|
141
|
+
now,
|
|
142
|
+
(map) => {
|
|
143
|
+
for (const p of picks) {
|
|
144
|
+
const e = map[p.id];
|
|
145
|
+
if (e && !e.ended && e.pid === p.pid && e.start === p.start && e.at === p.at) map[p.id] = { ...e, ended: true };
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
file
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// src/spool.ts
|
|
153
|
+
import fs2 from "fs";
|
|
154
|
+
import path2 from "path";
|
|
155
|
+
var SPOOL_MAX_BYTES = 1024 * 1024;
|
|
156
|
+
var LOCK_STALE_MS2 = 1e4;
|
|
157
|
+
var CLAIM_STALE_MS = 6e4;
|
|
158
|
+
var LOCK_WAIT_MS2 = 250;
|
|
159
|
+
var CAP_SLACK = 64 * 1024;
|
|
160
|
+
var counter = 0;
|
|
161
|
+
var uniq = () => `${Date.now()}-${process.pid}-${counter++}`;
|
|
162
|
+
function parseSpool(text) {
|
|
163
|
+
const out = [];
|
|
164
|
+
for (const line of text.split("\n")) {
|
|
165
|
+
if (!line.trim()) continue;
|
|
166
|
+
try {
|
|
167
|
+
out.push(JSON.parse(line));
|
|
168
|
+
} catch {
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return out;
|
|
172
|
+
}
|
|
173
|
+
function capped(events, maxBytes = SPOOL_MAX_BYTES) {
|
|
174
|
+
const lines = events.map((e) => JSON.stringify(e));
|
|
175
|
+
let total = lines.reduce((n, l) => n + Buffer.byteLength(l) + 1, 0);
|
|
176
|
+
let start = 0;
|
|
177
|
+
while (total > maxBytes && start < lines.length) {
|
|
178
|
+
total -= Buffer.byteLength(lines[start]) + 1;
|
|
179
|
+
start += 1;
|
|
180
|
+
}
|
|
181
|
+
return { events: events.slice(start), dropped: start };
|
|
182
|
+
}
|
|
183
|
+
function droppedEvents(file = spoolFile()) {
|
|
184
|
+
try {
|
|
185
|
+
return Number(fs2.readFileSync(`${file}.dropped`, "utf8").trim()) || 0;
|
|
186
|
+
} catch {
|
|
187
|
+
return 0;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
function noteDropped(spool, dropped) {
|
|
191
|
+
if (dropped <= 0) return;
|
|
192
|
+
try {
|
|
193
|
+
fs2.writeFileSync(`${spool}.dropped`, String(droppedEvents(spool) + dropped), { mode: 384 });
|
|
194
|
+
} catch {
|
|
195
|
+
}
|
|
196
|
+
try {
|
|
197
|
+
fs2.appendFileSync(serviceLogFile(), `${(/* @__PURE__ */ new Date()).toISOString()} spool full: dropped ${dropped} event(s), the cap is ${SPOOL_MAX_BYTES} bytes
|
|
198
|
+
`, { mode: 384 });
|
|
199
|
+
} catch {
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
function pause2(ms) {
|
|
203
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
204
|
+
}
|
|
205
|
+
var lockPath = (file) => `${file}.lock`;
|
|
206
|
+
function acquireLock(file) {
|
|
207
|
+
const lp = lockPath(file);
|
|
208
|
+
const deadline = Date.now() + LOCK_WAIT_MS2;
|
|
209
|
+
for (; ; ) {
|
|
210
|
+
try {
|
|
211
|
+
fs2.closeSync(fs2.openSync(lp, "wx", 384));
|
|
212
|
+
return true;
|
|
213
|
+
} catch (e) {
|
|
214
|
+
if (e.code !== "EEXIST") return false;
|
|
215
|
+
}
|
|
216
|
+
try {
|
|
217
|
+
if (Date.now() - fs2.statSync(lp).mtimeMs > LOCK_STALE_MS2) {
|
|
218
|
+
fs2.rmSync(lp, { force: true });
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
} catch {
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
if (Date.now() >= deadline) return false;
|
|
225
|
+
pause2(5);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
function releaseLock(file) {
|
|
229
|
+
removeFile(lockPath(file));
|
|
230
|
+
}
|
|
231
|
+
var claimPath = (file) => `${file}.claim-${uniq()}`;
|
|
232
|
+
var pendingPath = (file) => `${file}.pending-${uniq()}`;
|
|
233
|
+
function sideFiles(dir, file, kind) {
|
|
234
|
+
const prefix = `${path2.basename(file)}.${kind}-`;
|
|
235
|
+
let names;
|
|
236
|
+
try {
|
|
237
|
+
names = fs2.readdirSync(dir);
|
|
238
|
+
} catch {
|
|
239
|
+
return [];
|
|
240
|
+
}
|
|
241
|
+
return names.filter((n) => n.startsWith(prefix)).sort().map((n) => path2.join(dir, n));
|
|
242
|
+
}
|
|
243
|
+
function owner(file) {
|
|
244
|
+
const m = /\.(?:claim|pending)-(\d+)-(\d+)-\d+$/.exec(path2.basename(file));
|
|
245
|
+
if (!m) return void 0;
|
|
246
|
+
return { ts: Number(m[1]), pid: Number(m[2]) };
|
|
247
|
+
}
|
|
248
|
+
function alive(pid) {
|
|
249
|
+
if (pid === process.pid) return true;
|
|
250
|
+
try {
|
|
251
|
+
process.kill(pid, 0);
|
|
252
|
+
return true;
|
|
253
|
+
} catch (e) {
|
|
254
|
+
return e.code === "EPERM";
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
function abandoned(file) {
|
|
258
|
+
const o = owner(file);
|
|
259
|
+
if (!o) return false;
|
|
260
|
+
if (!alive(o.pid)) return true;
|
|
261
|
+
return Date.now() - o.ts > CLAIM_STALE_MS;
|
|
262
|
+
}
|
|
263
|
+
function writeEvents(file, events) {
|
|
264
|
+
const tmp = `${file}.tmp-${process.pid}-${counter++}`;
|
|
265
|
+
try {
|
|
266
|
+
fs2.writeFileSync(tmp, events.map((e) => JSON.stringify(e)).join("\n") + (events.length ? "\n" : ""), { mode: 384 });
|
|
267
|
+
fs2.renameSync(tmp, file);
|
|
268
|
+
} catch (e) {
|
|
269
|
+
removeFile(tmp);
|
|
270
|
+
throw e;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
function writeSpool(file, events, maxBytes = SPOOL_MAX_BYTES) {
|
|
274
|
+
const { events: kept, dropped } = capped(events, maxBytes);
|
|
275
|
+
if (dropped > 0) noteDropped(file, dropped);
|
|
276
|
+
writeEvents(file, kept);
|
|
277
|
+
}
|
|
278
|
+
function recover(file) {
|
|
279
|
+
const dir = path2.dirname(file);
|
|
280
|
+
const stale = [...sideFiles(dir, file, "claim").filter(abandoned), ...sideFiles(dir, file, "pending")].sort();
|
|
281
|
+
if (stale.length === 0) return;
|
|
282
|
+
const events = [];
|
|
283
|
+
for (const f of stale) events.push(...parseSpool(readText(f) ?? ""));
|
|
284
|
+
if (events.length > 0) {
|
|
285
|
+
const existing = parseSpool(readText(file) ?? "");
|
|
286
|
+
writeSpool(file, [...events, ...existing]);
|
|
287
|
+
}
|
|
288
|
+
for (const f of stale) removeFile(f);
|
|
289
|
+
}
|
|
290
|
+
function takeSpool(file = spoolFile()) {
|
|
291
|
+
fs2.mkdirSync(configDir(), { recursive: true, mode: 448 });
|
|
292
|
+
if (!acquireLock(file)) return void 0;
|
|
293
|
+
try {
|
|
294
|
+
recover(file);
|
|
295
|
+
const claim = claimPath(file);
|
|
296
|
+
try {
|
|
297
|
+
fs2.renameSync(file, claim);
|
|
298
|
+
} catch {
|
|
299
|
+
return void 0;
|
|
300
|
+
}
|
|
301
|
+
let events;
|
|
302
|
+
try {
|
|
303
|
+
events = parseSpool(fs2.readFileSync(claim, "utf8"));
|
|
304
|
+
} catch {
|
|
305
|
+
return void 0;
|
|
306
|
+
}
|
|
307
|
+
if (events.length === 0) {
|
|
308
|
+
removeFile(claim);
|
|
309
|
+
return void 0;
|
|
310
|
+
}
|
|
311
|
+
return { spool: file, file: claim, events };
|
|
312
|
+
} finally {
|
|
313
|
+
releaseLock(file);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
function releaseSpool(claim, keep, spool = spoolFile()) {
|
|
317
|
+
if (keep.length === 0) {
|
|
318
|
+
if (claim) removeFile(claim.file);
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
const target = claim?.spool ?? spool;
|
|
322
|
+
try {
|
|
323
|
+
returnToSpool(keep, target);
|
|
324
|
+
} catch {
|
|
325
|
+
if (claim) {
|
|
326
|
+
try {
|
|
327
|
+
writeEvents(claim.file, keep);
|
|
328
|
+
} catch {
|
|
329
|
+
}
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
try {
|
|
333
|
+
writeEvents(pendingPath(target), keep);
|
|
334
|
+
} catch {
|
|
335
|
+
}
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
if (claim) removeFile(claim.file);
|
|
339
|
+
}
|
|
340
|
+
function returnToSpool(events, file = spoolFile()) {
|
|
341
|
+
if (events.length === 0) return;
|
|
342
|
+
fs2.mkdirSync(configDir(), { recursive: true, mode: 448 });
|
|
343
|
+
if (!acquireLock(file)) {
|
|
344
|
+
writeEvents(pendingPath(file), events);
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
try {
|
|
348
|
+
const existing = parseSpool(readText(file) ?? "");
|
|
349
|
+
writeSpool(file, [...events, ...existing]);
|
|
350
|
+
} finally {
|
|
351
|
+
releaseLock(file);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
function appendToSpool(event, file = spoolFile()) {
|
|
355
|
+
fs2.mkdirSync(configDir(), { recursive: true, mode: 448 });
|
|
356
|
+
const line = JSON.stringify(event) + "\n";
|
|
357
|
+
if (!acquireLock(file)) {
|
|
358
|
+
fs2.appendFileSync(file, line, { mode: 384 });
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
try {
|
|
362
|
+
fs2.appendFileSync(file, line, { mode: 384 });
|
|
363
|
+
let size = 0;
|
|
364
|
+
try {
|
|
365
|
+
size = fs2.statSync(file).size;
|
|
366
|
+
} catch {
|
|
367
|
+
size = 0;
|
|
368
|
+
}
|
|
369
|
+
if (size > SPOOL_MAX_BYTES) writeSpool(file, parseSpool(readText(file) ?? ""), SPOOL_MAX_BYTES - CAP_SLACK);
|
|
370
|
+
} finally {
|
|
371
|
+
releaseLock(file);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// src/nudge.ts
|
|
376
|
+
import fs3 from "fs";
|
|
377
|
+
import path3 from "path";
|
|
378
|
+
var STASH_TTL_MS = 24 * 36e5;
|
|
379
|
+
var nudgeStashFile = () => path3.join(configDir(), "nudges.jsonl");
|
|
380
|
+
var CONTEXT_EVENTS = {
|
|
381
|
+
claude: ["UserPromptSubmit", "SessionStart", "PreToolUse", "PostToolUse"],
|
|
382
|
+
codex: ["UserPromptSubmit", "SessionStart"]
|
|
383
|
+
};
|
|
384
|
+
function contextEventName(tool, native) {
|
|
385
|
+
return CONTEXT_EVENTS[tool]?.find((e) => e.toLowerCase() === native.toLowerCase()) ?? null;
|
|
386
|
+
}
|
|
387
|
+
var isNudge = (v) => typeof v === "object" && v !== null && typeof v.session_id === "string" && typeof v.text === "string";
|
|
388
|
+
function nudgesOf(json) {
|
|
389
|
+
const list = typeof json === "object" && json !== null ? json.nudges : void 0;
|
|
390
|
+
return Array.isArray(list) ? list.filter(isNudge) : [];
|
|
391
|
+
}
|
|
392
|
+
function contextOutput(hookEventName, texts) {
|
|
393
|
+
return JSON.stringify({ hookSpecificOutput: { hookEventName, additionalContext: texts.join("\n\n") } });
|
|
394
|
+
}
|
|
395
|
+
function stashNudges(nudges, nowMs, file = nudgeStashFile()) {
|
|
396
|
+
if (nudges.length === 0) return;
|
|
397
|
+
try {
|
|
398
|
+
fs3.mkdirSync(path3.dirname(file), { recursive: true, mode: 448 });
|
|
399
|
+
fs3.appendFileSync(file, nudges.map((n) => `${JSON.stringify({ ...n, at: nowMs })}
|
|
400
|
+
`).join(""), { mode: 384 });
|
|
401
|
+
} catch {
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
function takeStashed(sessionId, nowMs, file = nudgeStashFile()) {
|
|
405
|
+
if (!fs3.existsSync(file)) return [];
|
|
406
|
+
const claim = `${file}.${process.pid}-${nowMs}`;
|
|
407
|
+
try {
|
|
408
|
+
fs3.renameSync(file, claim);
|
|
409
|
+
} catch {
|
|
410
|
+
return [];
|
|
411
|
+
}
|
|
412
|
+
const mine = [];
|
|
413
|
+
const rest = [];
|
|
414
|
+
try {
|
|
415
|
+
for (const line of fs3.readFileSync(claim, "utf8").split("\n")) {
|
|
416
|
+
if (!line.trim()) continue;
|
|
417
|
+
try {
|
|
418
|
+
const n = JSON.parse(line);
|
|
419
|
+
if (!isNudge(n) || nowMs - (n.at ?? 0) > STASH_TTL_MS) continue;
|
|
420
|
+
if (n.session_id === sessionId) mine.push(n.text);
|
|
421
|
+
else rest.push(line);
|
|
422
|
+
} catch {
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
if (rest.length > 0) fs3.appendFileSync(file, `${rest.join("\n")}
|
|
426
|
+
`, { mode: 384 });
|
|
427
|
+
} catch {
|
|
428
|
+
}
|
|
429
|
+
try {
|
|
430
|
+
fs3.rmSync(claim, { force: true });
|
|
431
|
+
} catch {
|
|
432
|
+
}
|
|
433
|
+
return mine;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// src/service/control/procinfo.ts
|
|
437
|
+
import fs4 from "fs";
|
|
438
|
+
function parseProcStat(text) {
|
|
439
|
+
const open = text.indexOf("(");
|
|
440
|
+
const close = text.lastIndexOf(")");
|
|
441
|
+
if (open < 0 || close < open) return void 0;
|
|
442
|
+
const rest = text.slice(close + 2).split(" ");
|
|
443
|
+
const ppid = Number(rest[1]);
|
|
444
|
+
const start = rest[19];
|
|
445
|
+
if (!Number.isInteger(ppid) || !start) return void 0;
|
|
446
|
+
return { ppid, comm: text.slice(open + 1, close), start };
|
|
447
|
+
}
|
|
448
|
+
function parsePs(text) {
|
|
449
|
+
const m = /^\s*(\d+)\s+(\S+\s+\S+\s+\d+\s+[\d:]+\s+\d{4})\s+(.+?)\s*$/m.exec(text);
|
|
450
|
+
if (!m) return void 0;
|
|
451
|
+
return { ppid: Number(m[1]), start: m[2], comm: m[3].split("/").pop() ?? m[3] };
|
|
452
|
+
}
|
|
453
|
+
function realProc(run = runFile) {
|
|
454
|
+
return {
|
|
455
|
+
async info(pid) {
|
|
456
|
+
if (!Number.isInteger(pid) || pid <= 0) return void 0;
|
|
457
|
+
if (process.platform === "linux") {
|
|
458
|
+
try {
|
|
459
|
+
return parseProcStat(fs4.readFileSync(`/proc/${pid}/stat`, "utf8"));
|
|
460
|
+
} catch {
|
|
461
|
+
return void 0;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
if (process.platform === "win32") return void 0;
|
|
465
|
+
const r = await run("ps", ["-o", "ppid=,lstart=,comm=", "-p", String(pid)], { env: { ...process.env, LC_ALL: "C" }, timeoutMs: 3e3 });
|
|
466
|
+
return r.code === 0 ? parsePs(r.stdout) : void 0;
|
|
467
|
+
}
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
var SHELLS = /* @__PURE__ */ new Set(["sh", "bash", "zsh", "dash", "fish", "ksh", "csh", "tcsh", "ash"]);
|
|
471
|
+
async function agentPidOf(proc, hookPid, hookParent) {
|
|
472
|
+
void hookPid;
|
|
473
|
+
let pid = hookParent;
|
|
474
|
+
for (let hop = 0; hop < 4; hop++) {
|
|
475
|
+
const i = await proc.info(pid);
|
|
476
|
+
if (!i) return void 0;
|
|
477
|
+
const name = i.comm.replace(/^-/, "").toLowerCase();
|
|
478
|
+
if (!SHELLS.has(name)) return { pid, start: i.start };
|
|
479
|
+
pid = i.ppid;
|
|
480
|
+
}
|
|
481
|
+
return void 0;
|
|
482
|
+
}
|
|
483
|
+
async function sameProcess(proc, pid, start) {
|
|
484
|
+
if (pid === void 0 || !start) return false;
|
|
485
|
+
const i = await proc.info(pid);
|
|
486
|
+
return i !== void 0 && i.start === start;
|
|
487
|
+
}
|
|
488
|
+
async function ancestors(proc, pid, max = 64) {
|
|
489
|
+
const out = [];
|
|
490
|
+
let p = pid;
|
|
491
|
+
for (let k = 0; k < max && p > 1; k++) {
|
|
492
|
+
out.push(p);
|
|
493
|
+
const i = await proc.info(p);
|
|
494
|
+
if (!i || i.ppid === p) break;
|
|
495
|
+
p = i.ppid;
|
|
496
|
+
}
|
|
497
|
+
return out;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// src/service/control/decisions.ts
|
|
501
|
+
import fs5 from "fs";
|
|
502
|
+
import path4 from "path";
|
|
503
|
+
var fileOf = (promptId, dir) => path4.join(dir, `${safeName(promptId)}.json`);
|
|
504
|
+
function writeDecision(promptId, d, dir = controlDecisionsDir()) {
|
|
505
|
+
ensurePrivateDir(dir);
|
|
506
|
+
const file = fileOf(promptId, dir);
|
|
507
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
508
|
+
fs5.writeFileSync(tmp, JSON.stringify(d), { mode: 384 });
|
|
509
|
+
fs5.renameSync(tmp, file);
|
|
510
|
+
}
|
|
511
|
+
function takeDecision(promptId, dir = controlDecisionsDir()) {
|
|
512
|
+
try {
|
|
513
|
+
const file = fileOf(promptId, dir);
|
|
514
|
+
if (!fs5.existsSync(file)) return void 0;
|
|
515
|
+
if (privateDirTrouble(dir) || privateFileTrouble(file)) {
|
|
516
|
+
fs5.rmSync(file, { force: true });
|
|
517
|
+
return void 0;
|
|
518
|
+
}
|
|
519
|
+
const text = fs5.readFileSync(file, "utf8");
|
|
520
|
+
fs5.rmSync(file, { force: true });
|
|
521
|
+
const j = JSON.parse(text);
|
|
522
|
+
if (!isRecord(j) || j.behavior !== "allow" && j.behavior !== "deny") return void 0;
|
|
523
|
+
return { behavior: j.behavior, ...typeof j.message === "string" ? { message: j.message.slice(0, 300) } : {} };
|
|
524
|
+
} catch {
|
|
525
|
+
return void 0;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// src/service/control/inbox.ts
|
|
530
|
+
import fs6 from "fs";
|
|
531
|
+
import path5 from "path";
|
|
532
|
+
var INBOX_MAX = 6;
|
|
533
|
+
var fileOf2 = (session, dir) => path5.join(dir, `${safeName(session)}.jsonl`);
|
|
534
|
+
function parseLines(text) {
|
|
535
|
+
const out = [];
|
|
536
|
+
for (const line of text.split("\n")) {
|
|
537
|
+
if (!line.trim()) continue;
|
|
538
|
+
try {
|
|
539
|
+
const j = JSON.parse(line);
|
|
540
|
+
if (isRecord(j) && typeof j.id === "string" && typeof j.from === "string" && typeof j.text === "string" && typeof j.at === "number" && typeof j.expires_at === "number") {
|
|
541
|
+
out.push({ id: j.id, from: j.from, text: j.text, at: j.at, expires_at: j.expires_at });
|
|
542
|
+
}
|
|
543
|
+
} catch {
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
return out;
|
|
547
|
+
}
|
|
548
|
+
function readEntries(file) {
|
|
549
|
+
if (privateFileTrouble(file)) return [];
|
|
550
|
+
try {
|
|
551
|
+
return parseLines(fs6.readFileSync(file, "utf8"));
|
|
552
|
+
} catch {
|
|
553
|
+
return [];
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
function rewrite(file, entries) {
|
|
557
|
+
if (entries.length === 0) {
|
|
558
|
+
fs6.rmSync(file, { force: true });
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
562
|
+
fs6.writeFileSync(tmp, entries.map((e) => `${JSON.stringify(e)}
|
|
563
|
+
`).join(""), { mode: 384 });
|
|
564
|
+
fs6.renameSync(tmp, file);
|
|
565
|
+
}
|
|
566
|
+
function addToInbox(session, entry, now, dir = controlInboxDir()) {
|
|
567
|
+
ensurePrivateDir(dir);
|
|
568
|
+
const file = fileOf2(session, dir);
|
|
569
|
+
const live = readEntries(file).filter((e) => e.expires_at > now);
|
|
570
|
+
if (live.length >= INBOX_MAX) return "full";
|
|
571
|
+
live.push(entry);
|
|
572
|
+
rewrite(file, live);
|
|
573
|
+
return "ok";
|
|
574
|
+
}
|
|
575
|
+
function takeInbox(session, now, dir = controlInboxDir()) {
|
|
576
|
+
try {
|
|
577
|
+
if (privateDirTrouble(dir)) return [];
|
|
578
|
+
const file = fileOf2(session, dir);
|
|
579
|
+
if (!fs6.existsSync(file)) return [];
|
|
580
|
+
const claim = `${file}.${process.pid}-${now}.taken`;
|
|
581
|
+
fs6.renameSync(file, claim);
|
|
582
|
+
try {
|
|
583
|
+
if (privateFileTrouble(claim)) return [];
|
|
584
|
+
return parseLines(fs6.readFileSync(claim, "utf8")).filter((e) => e.expires_at > now);
|
|
585
|
+
} finally {
|
|
586
|
+
fs6.rmSync(claim, { force: true });
|
|
587
|
+
}
|
|
588
|
+
} catch {
|
|
589
|
+
return [];
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
function sweepInbox(now, dir = controlInboxDir()) {
|
|
593
|
+
const gone = [];
|
|
594
|
+
try {
|
|
595
|
+
if (privateDirTrouble(dir)) return gone;
|
|
596
|
+
for (const name of fs6.readdirSync(dir)) {
|
|
597
|
+
if (!name.endsWith(".jsonl")) continue;
|
|
598
|
+
const file = path5.join(dir, name);
|
|
599
|
+
const all = readEntries(file);
|
|
600
|
+
const live = all.filter((e) => e.expires_at > now);
|
|
601
|
+
if (live.length === all.length) continue;
|
|
602
|
+
for (const e of all) if (e.expires_at <= now) gone.push({ session: name.slice(0, -".jsonl".length), entry: e });
|
|
603
|
+
rewrite(file, live);
|
|
604
|
+
}
|
|
605
|
+
} catch {
|
|
606
|
+
}
|
|
607
|
+
return gone;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// src/service/control/tmux.ts
|
|
611
|
+
var T = 4e3;
|
|
612
|
+
function parsePanes(out) {
|
|
613
|
+
const panes = [];
|
|
614
|
+
for (const line of out.split("\n")) {
|
|
615
|
+
const m = /^(%\d+) (\d+)$/.exec(line.trim());
|
|
616
|
+
if (m) panes.push([m[1], Number(m[2])]);
|
|
617
|
+
}
|
|
618
|
+
return panes;
|
|
619
|
+
}
|
|
620
|
+
async function findPane(run, proc, agentPid) {
|
|
621
|
+
const r = await run("tmux", ["list-panes", "-a", "-F", "#{pane_id} #{pane_pid}"], { timeoutMs: T });
|
|
622
|
+
if (r.code !== 0) return void 0;
|
|
623
|
+
const panes = parsePanes(r.stdout);
|
|
624
|
+
if (panes.length === 0) return void 0;
|
|
625
|
+
const chain = new Set(await ancestors(proc, agentPid));
|
|
626
|
+
return panes.find(([, pid]) => chain.has(pid))?.[0];
|
|
627
|
+
}
|
|
628
|
+
var fail = (what, stderr) => ({ ok: false, detail: `${what}: ${stderr.trim().slice(0, 120)}` });
|
|
629
|
+
async function typeLine(run, pane, text) {
|
|
630
|
+
const a = await run("tmux", ["send-keys", "-t", pane, "-l", "--", text], { timeoutMs: T });
|
|
631
|
+
if (a.code !== 0) return fail("send-keys", a.stderr);
|
|
632
|
+
return await sendKey(run, pane, "Enter");
|
|
633
|
+
}
|
|
634
|
+
async function pasteText(run, pane, text, id) {
|
|
635
|
+
const buf = `hf-${id.replace(/[^A-Za-z0-9_-]/g, "_").slice(0, 40)}`;
|
|
636
|
+
const l = await run("tmux", ["load-buffer", "-b", buf, "-"], { input: text, timeoutMs: T });
|
|
637
|
+
if (l.code !== 0) return fail("load-buffer", l.stderr);
|
|
638
|
+
const p = await run("tmux", ["paste-buffer", "-p", "-d", "-b", buf, "-t", pane], { timeoutMs: T });
|
|
639
|
+
if (p.code !== 0) {
|
|
640
|
+
await run("tmux", ["delete-buffer", "-b", buf], { timeoutMs: T });
|
|
641
|
+
return fail("paste-buffer", p.stderr);
|
|
642
|
+
}
|
|
643
|
+
return await sendKey(run, pane, "Enter");
|
|
644
|
+
}
|
|
645
|
+
async function sendKey(run, pane, key) {
|
|
646
|
+
const r = await run("tmux", ["send-keys", "-t", pane, key], { timeoutMs: T });
|
|
647
|
+
return r.code === 0 ? { ok: true } : fail("send-keys", r.stderr);
|
|
648
|
+
}
|
|
649
|
+
async function capture(run, pane) {
|
|
650
|
+
const r = await run("tmux", ["capture-pane", "-p", "-t", pane], { timeoutMs: T });
|
|
651
|
+
return r.code === 0 ? r.stdout : void 0;
|
|
652
|
+
}
|
|
653
|
+
function inputBoxHolds(screen, text) {
|
|
654
|
+
const snippet = text.split("\n")[0].trim().slice(0, 24);
|
|
655
|
+
if (snippet === "") return false;
|
|
656
|
+
const lines = screen.split("\n").slice(-20);
|
|
657
|
+
return lines.some((l) => {
|
|
658
|
+
const t = l.replace(/^[\s│|┃]+/, "");
|
|
659
|
+
return /^[>❯]\s/.test(t) && t.includes(snippet);
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
function parseFg(out) {
|
|
663
|
+
const m = /^\s*(-?\d+)\s+(\d+)\s+(\S+)\s*$/m.exec(out);
|
|
664
|
+
return m ? { tpgid: Number(m[1]), pgid: Number(m[2]), tty: m[3] } : void 0;
|
|
665
|
+
}
|
|
666
|
+
var ttyName = (t) => t.replace(/^\/dev\//, "");
|
|
667
|
+
async function paneFocusTrouble(run, pane, agentPid) {
|
|
668
|
+
const d = await run("tmux", ["display-message", "-p", "-t", pane, "#{pane_tty} #{pane_in_mode}"], { timeoutMs: T });
|
|
669
|
+
const m = d.code === 0 ? /^(\S+) (\d+)\s*$/.exec(d.stdout.trim()) : null;
|
|
670
|
+
if (!m) return "pane state unknown";
|
|
671
|
+
if (m[2] !== "0") return "pane in a mode";
|
|
672
|
+
const p = await run("ps", ["-o", "tpgid=,pgid=,tty=", "-p", String(agentPid)], { env: { ...process.env, LC_ALL: "C" }, timeoutMs: T });
|
|
673
|
+
const fg = p.code === 0 ? parseFg(p.stdout) : void 0;
|
|
674
|
+
if (!fg) return "agent process unknown";
|
|
675
|
+
if (ttyName(fg.tty) !== ttyName(m[1])) return "agent not on the pane terminal";
|
|
676
|
+
if (fg.tpgid !== fg.pgid) return "agent not in the foreground";
|
|
677
|
+
return void 0;
|
|
678
|
+
}
|
|
679
|
+
function promptState(screen) {
|
|
680
|
+
const lines = screen.replace(/\s+$/, "").split("\n").slice(-12);
|
|
681
|
+
for (let k = lines.length - 1; k >= 0; k--) {
|
|
682
|
+
const t = lines[k].replace(/^[\s│|┃]+/, "").replace(/[\s│|┃]+$/, "");
|
|
683
|
+
if (t.startsWith("!")) return "bash";
|
|
684
|
+
if (/^[>❯]/.test(t)) return t.length === 1 ? "empty" : "draft";
|
|
685
|
+
}
|
|
686
|
+
return "none";
|
|
687
|
+
}
|
|
688
|
+
async function clientIdleSec(run, pane, nowMs) {
|
|
689
|
+
const s = await run("tmux", ["display-message", "-p", "-t", pane, "#{session_id}"], { timeoutMs: T });
|
|
690
|
+
const sid = s.stdout.trim();
|
|
691
|
+
if (s.code !== 0 || !/^\$\d+$/.test(sid)) return void 0;
|
|
692
|
+
const c = await run("tmux", ["list-clients", "-t", sid, "-F", "#{client_activity}"], { timeoutMs: T });
|
|
693
|
+
if (c.code !== 0) return void 0;
|
|
694
|
+
const times = c.stdout.split("\n").map((l) => Number(l.trim())).filter((n) => Number.isFinite(n) && n > 0);
|
|
695
|
+
if (times.length === 0) return void 0;
|
|
696
|
+
return Math.max(0, nowMs / 1e3 - Math.max(...times));
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// src/service/control/claude.ts
|
|
700
|
+
var PASTE_VERIFIED = [2, 1, 323];
|
|
701
|
+
var PROMPT_FRESH_MS = 3e4;
|
|
702
|
+
var INPUT_RECHECK_MS = 400;
|
|
703
|
+
var INBOX_MAX_MS = 12e4;
|
|
704
|
+
function pasteVerified(version) {
|
|
705
|
+
const m = version ? /(\d+)\.(\d+)\.(\d+)/.exec(version) : null;
|
|
706
|
+
if (!m) return false;
|
|
707
|
+
const v = [Number(m[1]), Number(m[2]), Number(m[3])];
|
|
708
|
+
if (v[0] !== PASTE_VERIFIED[0]) return false;
|
|
709
|
+
if (v[1] !== PASTE_VERIFIED[1]) return v[1] > PASTE_VERIFIED[1];
|
|
710
|
+
return v[2] >= PASTE_VERIFIED[2];
|
|
711
|
+
}
|
|
712
|
+
var promptOpen = (e, now) => e.prompt_id !== void 0 && now - (e.prompt_at ?? 0) < PROMPT_FRESH_MS;
|
|
713
|
+
var isIdle = (e, now) => (e.event === "Stop" || e.event === "SessionStart") && !promptOpen(e, now);
|
|
714
|
+
var senderName = (name) => stripControlChars(name).replace(/\s+/g, " ").trim().slice(0, 60) || "someone";
|
|
715
|
+
function guardFirstChar(text) {
|
|
716
|
+
return /^\s*[!/#]/.test(text) ? `Message: ${text}` : text;
|
|
717
|
+
}
|
|
718
|
+
var joinLines = (text) => text.split("\n").map((l) => l.trim()).filter((l) => l !== "").join(" ");
|
|
719
|
+
async function typingSafe(env, pane, pid) {
|
|
720
|
+
if (await paneFocusTrouble(env.run, pane, pid) !== void 0) return false;
|
|
721
|
+
const screen = await capture(env.run, pane);
|
|
722
|
+
return screen !== void 0 && promptState(screen) === "empty";
|
|
723
|
+
}
|
|
724
|
+
var fail2 = (code, detail) => ({ status: "failed", code, ...detail ? { detail: detail.slice(0, 200) } : {} });
|
|
725
|
+
async function claudeMessage(cmd, id, entry, env) {
|
|
726
|
+
const raw = typeof cmd.args.text === "string" ? cmd.args.text : "";
|
|
727
|
+
const text = cleanControlText(raw);
|
|
728
|
+
if (text === null) return fail2("invalid", "empty message");
|
|
729
|
+
const now = env.now();
|
|
730
|
+
const pane = entry.pid !== void 0 ? await findPane(env.run, env.proc, entry.pid) : void 0;
|
|
731
|
+
if (pane && isIdle(entry, now) && await typingSafe(env, pane, entry.pid)) {
|
|
732
|
+
const typed = guardFirstChar(text);
|
|
733
|
+
let r;
|
|
734
|
+
if (typed.includes("\n") && pasteVerified(await env.claudeVersion())) {
|
|
735
|
+
r = await pasteText(env.run, pane, typed, id);
|
|
736
|
+
} else {
|
|
737
|
+
r = await typeLine(env.run, pane, joinLines(typed).replace(/\t/g, " "));
|
|
738
|
+
}
|
|
739
|
+
if (!r.ok) return fail2("tmux_error", r.detail);
|
|
740
|
+
await env.sleep(INPUT_RECHECK_MS);
|
|
741
|
+
const screen = await capture(env.run, pane);
|
|
742
|
+
if (screen !== void 0 && inputBoxHolds(screen, typed)) await sendKey(env.run, pane, "Enter");
|
|
743
|
+
updateSession(cmd.session.external_id, env.now(), (e) => e ? { ...e, msg_at: env.now() } : e);
|
|
744
|
+
return { status: "done", via: "tmux" };
|
|
745
|
+
}
|
|
746
|
+
const who = senderName(cmd.requested_by.name);
|
|
747
|
+
const at = env.now();
|
|
748
|
+
const res = addToInbox(cmd.session.external_id, { id, from: who, text, at, expires_at: at + Math.min(Math.max(0, cmd.expires_in_ms), INBOX_MAX_MS) }, at);
|
|
749
|
+
if (res === "full") return fail2("inbox_full");
|
|
750
|
+
updateSession(cmd.session.external_id, env.now(), (e) => e ? { ...e, msg_at: env.now() } : e);
|
|
751
|
+
return { status: "done", via: "hook" };
|
|
752
|
+
}
|
|
753
|
+
function claudeDecision(cmd, entry, now) {
|
|
754
|
+
const promptId = typeof cmd.args.prompt_id === "string" ? cmd.args.prompt_id : "";
|
|
755
|
+
if (!promptId || entry.prompt_id !== promptId || !promptOpen(entry, now)) return fail2("prompt_gone");
|
|
756
|
+
try {
|
|
757
|
+
if (cmd.kind === "approve") {
|
|
758
|
+
writeDecision(promptId, { behavior: "allow" });
|
|
759
|
+
} else {
|
|
760
|
+
const reason = typeof cmd.args.reason === "string" ? cleanControlText(cmd.args.reason, 300) : null;
|
|
761
|
+
writeDecision(promptId, { behavior: "deny", message: reason ? `${senderName(cmd.requested_by.name)} denied this through HiveFloor: ${reason}` : `${senderName(cmd.requested_by.name)} denied this through HiveFloor` });
|
|
762
|
+
}
|
|
763
|
+
} catch (e) {
|
|
764
|
+
return fail2("decision_error", e.message);
|
|
765
|
+
}
|
|
766
|
+
return { status: "done", via: "hook" };
|
|
767
|
+
}
|
|
768
|
+
async function stopSession(cmd, entry, env) {
|
|
769
|
+
const now = env.now();
|
|
770
|
+
if (cmd.args.mode === "end") {
|
|
771
|
+
if (!await sameProcess(env.proc, entry.pid, entry.start)) return fail2("unknown_session");
|
|
772
|
+
try {
|
|
773
|
+
env.kill(entry.pid, "SIGTERM");
|
|
774
|
+
} catch (e) {
|
|
775
|
+
return fail2("kill_failed", e.message);
|
|
776
|
+
}
|
|
777
|
+
return { status: "done" };
|
|
778
|
+
}
|
|
779
|
+
if (cmd.args.mode !== "interrupt") return fail2("invalid", "unknown stop mode");
|
|
780
|
+
if (entry.tool !== "claude") return fail2("unsupported", "interrupt is only for Claude Code");
|
|
781
|
+
if (promptOpen(entry, now)) return fail2("prompt_open");
|
|
782
|
+
if (isIdle(entry, now)) return { status: "done", detail: "idle" };
|
|
783
|
+
const pane = entry.pid !== void 0 ? await findPane(env.run, env.proc, entry.pid) : void 0;
|
|
784
|
+
if (!pane) return fail2("no_pane");
|
|
785
|
+
const trouble = await paneFocusTrouble(env.run, pane, entry.pid);
|
|
786
|
+
if (trouble) return fail2("pane_busy", trouble);
|
|
787
|
+
const r = await sendKey(env.run, pane, "Escape");
|
|
788
|
+
return r.ok ? { status: "done", via: "tmux" } : fail2("tmux_error", r.detail);
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
export {
|
|
792
|
+
validCwd,
|
|
793
|
+
validTranscript,
|
|
794
|
+
readSessions,
|
|
795
|
+
updateSession,
|
|
796
|
+
markSessionsEnded,
|
|
797
|
+
takeSpool,
|
|
798
|
+
releaseSpool,
|
|
799
|
+
appendToSpool,
|
|
800
|
+
contextEventName,
|
|
801
|
+
nudgesOf,
|
|
802
|
+
contextOutput,
|
|
803
|
+
stashNudges,
|
|
804
|
+
takeStashed,
|
|
805
|
+
takeDecision,
|
|
806
|
+
takeInbox,
|
|
807
|
+
sweepInbox,
|
|
808
|
+
realProc,
|
|
809
|
+
agentPidOf,
|
|
810
|
+
sameProcess,
|
|
811
|
+
findPane,
|
|
812
|
+
clientIdleSec,
|
|
813
|
+
senderName,
|
|
814
|
+
claudeMessage,
|
|
815
|
+
claudeDecision,
|
|
816
|
+
stopSession
|
|
817
|
+
};
|