khala-cli 0.4.8 → 0.4.10
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 +4 -4
- package/dist/THIRD_PARTY_NOTICES.txt +809 -0
- package/dist/chunk-2AMWQ7RB.mjs +1127 -0
- package/dist/chunk-3E5DH3Y6.mjs +9864 -0
- package/dist/{chunk-IQJQ4RF5.mjs → chunk-4BP7V3OP.mjs} +12 -1
- package/dist/{chunk-FV32AQTC.mjs → chunk-4BZQ4GVW.mjs} +10 -6
- package/dist/chunk-5JSGA6OB.mjs +1483 -0
- package/dist/chunk-6AQVW5W7.mjs +60 -0
- package/dist/{chunk-W6R4UJUQ.mjs → chunk-6J2PUM7L.mjs} +16 -9
- package/dist/chunk-6SUV3EW6.mjs +27 -0
- package/dist/{chunk-RNDUFTBF.mjs → chunk-ARIDZOTI.mjs} +499 -115
- package/dist/chunk-C2WEDMAF.mjs +15 -0
- package/dist/{chunk-Z4JF2M5C.mjs → chunk-CHC4VZF5.mjs} +9 -9
- package/dist/chunk-CKLCR5G4.mjs +1369 -0
- package/dist/chunk-EVT2DWNZ.mjs +58 -0
- package/dist/chunk-FJVVSKZ3.mjs +22 -0
- package/dist/{chunk-APMWDF6B.mjs → chunk-G3NMB4PY.mjs} +4 -2
- package/dist/chunk-HHEZVPRR.mjs +7 -0
- package/dist/{chunk-54XFVSEC.mjs → chunk-JKOJQTFT.mjs} +103 -5
- package/dist/{chunk-ZJ4W7P33.mjs → chunk-NDMFD5IM.mjs} +15 -10
- package/dist/{chunk-VKQ3KUYO.mjs → chunk-OEV5DFLW.mjs} +77 -7
- package/dist/chunk-OIZ4R5H7.mjs +173 -0
- package/dist/{chunk-CJTCU755.mjs → chunk-PJKI7SRI.mjs} +256 -26
- package/dist/chunk-PN25BUQ4.mjs +599 -0
- package/dist/chunk-QDPJ4VT2.mjs +432 -0
- package/dist/{chunk-2DHIAFEV.mjs → chunk-SRZTHCXP.mjs} +24 -10
- package/dist/chunk-TSK3HJEG.mjs +20 -0
- package/dist/chunk-UX34IVN5.mjs +33 -0
- package/dist/{chunk-BKLW2SPG.mjs → chunk-VAC3JTAV.mjs} +34 -17
- package/dist/{chunk-VAI7IUI4.mjs → chunk-VDZK4LW3.mjs} +30 -12
- package/dist/chunk-W6HM6F2U.mjs +901 -0
- package/dist/{chunk-H3K4GA2P.mjs → chunk-WPNMWNCL.mjs} +33 -21
- package/dist/chunk-WUDXHF2Q.mjs +14 -0
- package/dist/chunk-WZJ6DQJV.mjs +19 -0
- package/dist/{chunk-5LIE5GUU.mjs → chunk-XR625T3I.mjs} +5 -1
- package/dist/chunk-ZSKLNZWZ.mjs +224 -0
- package/dist/iterm2_send.py +56 -0
- package/dist/khala.mjs +20 -13
- package/dist/web/assets/local-CSPuGnH2.css +1 -0
- package/dist/web/assets/local-Dtyxaacx.js +9 -0
- package/dist/web/index.html +2 -2
- package/package.json +5 -3
- package/dist/chunk-2VCAW4GH.mjs +0 -34
- package/dist/chunk-5BQTYU66.mjs +0 -886
- package/dist/chunk-5GLTKWJ5.mjs +0 -1
- package/dist/chunk-5PUQT3YS.mjs +0 -569
- package/dist/chunk-7HBRFY3Z.mjs +0 -8
- package/dist/chunk-CBPLCCRZ.mjs +0 -191
- package/dist/chunk-FH34B3I3.mjs +0 -235
- package/dist/chunk-NKX3SLS7.mjs +0 -227
- package/dist/chunk-VUSV2ADL.mjs +0 -267
- package/dist/web/assets/local-BQl2KrE1.js +0 -9
- package/dist/web/assets/local-OwDq47O4.css +0 -1
|
@@ -0,0 +1,1483 @@
|
|
|
1
|
+
import { createRequire as __khalaCreateRequire } from 'node:module'; const require = __khalaCreateRequire(import.meta.url);
|
|
2
|
+
import {
|
|
3
|
+
readPane
|
|
4
|
+
} from "./chunk-6AQVW5W7.mjs";
|
|
5
|
+
import {
|
|
6
|
+
CURSOR_DEFAULT_SESSION,
|
|
7
|
+
CURSOR_WORKSPACE_ENV,
|
|
8
|
+
createMuseMonitorDriver,
|
|
9
|
+
createQwenSocketDriver,
|
|
10
|
+
cursorSessionId,
|
|
11
|
+
driverAllowed,
|
|
12
|
+
museCliPath,
|
|
13
|
+
museMonitorInstruction,
|
|
14
|
+
museStopWakeText,
|
|
15
|
+
museWatchCommand,
|
|
16
|
+
qwenInboundAllowed,
|
|
17
|
+
qwenSession,
|
|
18
|
+
readListeningMode,
|
|
19
|
+
readWakeSettings,
|
|
20
|
+
verifyQwenTranscript
|
|
21
|
+
} from "./chunk-2AMWQ7RB.mjs";
|
|
22
|
+
import {
|
|
23
|
+
listChannels,
|
|
24
|
+
readActivity,
|
|
25
|
+
readCursor,
|
|
26
|
+
unread,
|
|
27
|
+
wakesAgent
|
|
28
|
+
} from "./chunk-OEV5DFLW.mjs";
|
|
29
|
+
import {
|
|
30
|
+
HARNESS_REGISTRY,
|
|
31
|
+
SESSION_ID_PATTERN,
|
|
32
|
+
ancestors,
|
|
33
|
+
ensureStateDir,
|
|
34
|
+
isHarnessId,
|
|
35
|
+
nearestNonShellAncestor,
|
|
36
|
+
openSessionDir,
|
|
37
|
+
readJson,
|
|
38
|
+
readProcess,
|
|
39
|
+
readProcessArguments,
|
|
40
|
+
stateRoot,
|
|
41
|
+
walkAncestors,
|
|
42
|
+
writeJsonAtomic
|
|
43
|
+
} from "./chunk-PN25BUQ4.mjs";
|
|
44
|
+
import {
|
|
45
|
+
CODEX_IDLE_WAKE_NOTICE,
|
|
46
|
+
cancelAttempt,
|
|
47
|
+
failAttempt,
|
|
48
|
+
newNonce,
|
|
49
|
+
readWakeState,
|
|
50
|
+
recordAttempt,
|
|
51
|
+
resetWakeDriver,
|
|
52
|
+
settleAttempts,
|
|
53
|
+
wakeLine,
|
|
54
|
+
withWakeLock
|
|
55
|
+
} from "./chunk-ZSKLNZWZ.mjs";
|
|
56
|
+
import {
|
|
57
|
+
init_define_KHALA_BUNDLE
|
|
58
|
+
} from "./chunk-EVT2DWNZ.mjs";
|
|
59
|
+
|
|
60
|
+
// src/harness/index.ts
|
|
61
|
+
init_define_KHALA_BUNDLE();
|
|
62
|
+
|
|
63
|
+
// src/harness/claude.ts
|
|
64
|
+
init_define_KHALA_BUNDLE();
|
|
65
|
+
|
|
66
|
+
// src/harness/codecs/claude-style.ts
|
|
67
|
+
init_define_KHALA_BUNDLE();
|
|
68
|
+
|
|
69
|
+
// src/harness/adapter.ts
|
|
70
|
+
init_define_KHALA_BUNDLE();
|
|
71
|
+
var STOP_NUDGE = "Khala: channel messages are waiting. Call khala_read.";
|
|
72
|
+
|
|
73
|
+
// src/harness/codecs/claude-style.ts
|
|
74
|
+
var events = { SessionStart: "start", UserPromptSubmit: "prompt", PostToolUse: "tool", Stop: "stop", StopFailure: "limit" };
|
|
75
|
+
var LIMIT_ERRORS = ["rate_limit", "billing_error"];
|
|
76
|
+
var names = { prompt: "UserPromptSubmit", tool: "PostToolUse", stop: "Stop" };
|
|
77
|
+
var claudeStyleCodec = {
|
|
78
|
+
promptDeliversWithoutWake: true,
|
|
79
|
+
promptAcceptsContext: true,
|
|
80
|
+
suppressOutputErrors: false,
|
|
81
|
+
parse(stdin) {
|
|
82
|
+
try {
|
|
83
|
+
const input = JSON.parse(stdin);
|
|
84
|
+
if (!input || typeof input.session_id !== "string" || typeof input.hook_event_name !== "string" || !Object.hasOwn(events, input.hook_event_name) || input.stop_hook_active !== void 0 && typeof input.stop_hook_active !== "boolean" || input.hook_event_name === "StopFailure" && !LIMIT_ERRORS.includes(input.error)) return null;
|
|
85
|
+
return {
|
|
86
|
+
sessionId: input.session_id,
|
|
87
|
+
event: events[input.hook_event_name],
|
|
88
|
+
continuation: input.stop_hook_active === true,
|
|
89
|
+
...typeof input.cwd === "string" ? { workspace: input.cwd } : {},
|
|
90
|
+
...typeof input.transcript_path === "string" ? { transcriptPath: input.transcript_path } : {},
|
|
91
|
+
...typeof input.prompt === "string" ? { promptText: input.prompt } : {}
|
|
92
|
+
};
|
|
93
|
+
} catch {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
noop: () => "",
|
|
98
|
+
render(kind, frame, stopContext = false) {
|
|
99
|
+
return JSON.stringify(kind === "stop" && !stopContext ? { decision: "block", reason: STOP_NUDGE } : { hookSpecificOutput: { hookEventName: names[kind], additionalContext: frame } }) + "\n";
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
var claudeCodec = { ...claudeStyleCodec, stopContextFloor: "2.1.307" };
|
|
103
|
+
|
|
104
|
+
// src/wake/terminal/driver.ts
|
|
105
|
+
init_define_KHALA_BUNDLE();
|
|
106
|
+
import path from "node:path";
|
|
107
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
108
|
+
|
|
109
|
+
// src/wake/terminal/prompt-guard.ts
|
|
110
|
+
init_define_KHALA_BUNDLE();
|
|
111
|
+
function styledLine(line) {
|
|
112
|
+
let text = "";
|
|
113
|
+
const dim = [];
|
|
114
|
+
let faint = false;
|
|
115
|
+
for (let i = 0; i < line.length; ) {
|
|
116
|
+
if (line[i] === "\x1B") {
|
|
117
|
+
const match = line.slice(i).match(/^\x1b\[([0-9;:]*)m/);
|
|
118
|
+
if (!match) return null;
|
|
119
|
+
const params = match[1] === "" ? [0] : match[1].split(";").map(Number);
|
|
120
|
+
for (let j = 0; j < params.length; j++) {
|
|
121
|
+
const code = params[j];
|
|
122
|
+
if (code === 38 || code === 48 || code === 58) {
|
|
123
|
+
if (params[j + 1] === 5) j += 2;
|
|
124
|
+
else if (params[j + 1] === 2) j += 4;
|
|
125
|
+
else return null;
|
|
126
|
+
} else if (code === 0 || code === 22) faint = false;
|
|
127
|
+
else if (code === 2) faint = true;
|
|
128
|
+
else if (!Number.isFinite(code)) return null;
|
|
129
|
+
}
|
|
130
|
+
i += match[0].length;
|
|
131
|
+
} else {
|
|
132
|
+
const character = line[i];
|
|
133
|
+
if (/[\x00-\x1f\x7f-\x9f]/.test(character)) return null;
|
|
134
|
+
text += character;
|
|
135
|
+
dim.push(faint);
|
|
136
|
+
i++;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return { text, dim };
|
|
140
|
+
}
|
|
141
|
+
function isEmptyPrompt(line, cursorX, guard) {
|
|
142
|
+
if (!guard || !Number.isSafeInteger(cursorX) || cursorX !== guard.cursorColumn) return false;
|
|
143
|
+
const parsed = styledLine(line.replace(/\r?\n$/, ""));
|
|
144
|
+
if (!parsed) return false;
|
|
145
|
+
const text = parsed.text.replace(/ +$/, "");
|
|
146
|
+
const pattern = new RegExp(guard.pattern.source, guard.pattern.flags.replace(/[gy]/g, ""));
|
|
147
|
+
if (!pattern.test(text)) return false;
|
|
148
|
+
const placeholder = text.match(/^(?:❯[\u00a0 ]?)(Try ".*")$/) ?? text.match(/^› ?(Ask Codex to do anything)$/);
|
|
149
|
+
if (placeholder) {
|
|
150
|
+
const start = text.length - placeholder[1].length;
|
|
151
|
+
return text.slice(start).split("").every((character, offset) => character === " " || parsed.dim[start + offset]);
|
|
152
|
+
}
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// src/wake/terminal/process.ts
|
|
157
|
+
init_define_KHALA_BUNDLE();
|
|
158
|
+
import { execFile } from "node:child_process";
|
|
159
|
+
import { readFile, stat } from "node:fs/promises";
|
|
160
|
+
import { promisify } from "node:util";
|
|
161
|
+
var exec = promisify(execFile);
|
|
162
|
+
var runTerminalCommand = async (command, argv, env, signal) => {
|
|
163
|
+
const { stdout } = await exec(command, [...argv], { env, signal, timeout: 5e3, maxBuffer: 1024 * 1024, shell: false });
|
|
164
|
+
return stdout;
|
|
165
|
+
};
|
|
166
|
+
async function ownsTerminal(pid, tty, env, signal, run2 = runTerminalCommand, platform = process.platform) {
|
|
167
|
+
if (!Number.isSafeInteger(pid) || pid <= 0 || !/^\/dev\/(?:pts\/\d+|tty[^\s/]+)$/.test(tty)) return false;
|
|
168
|
+
try {
|
|
169
|
+
if (platform === "linux") {
|
|
170
|
+
const text = await readFile(`/proc/${pid}/stat`, "utf8");
|
|
171
|
+
const fields = text.slice(text.lastIndexOf(")") + 2).trim().split(/\s+/);
|
|
172
|
+
const pgid = Number(fields[2]), ttyNr = Number(fields[4]) >>> 0, tpgid = Number(fields[5]);
|
|
173
|
+
const device = await stat(tty, { bigint: true });
|
|
174
|
+
if (!device.isCharacterDevice()) return false;
|
|
175
|
+
const major = device.rdev >> 8n & 0xfffn;
|
|
176
|
+
const minor = device.rdev & 0xffn | device.rdev >> 12n & 0xffffff00n;
|
|
177
|
+
const encoded = Number(major << 8n | minor & 0xffn | (minor & ~0xffn) << 12n) >>> 0;
|
|
178
|
+
return ttyNr !== 0 && ttyNr === encoded && pgid > 0 && pgid === tpgid;
|
|
179
|
+
}
|
|
180
|
+
if (platform === "darwin") {
|
|
181
|
+
const output = await run2("ps", ["-o", "tty=,pgid=,tpgid=", "-p", String(pid)], { ...env, LC_ALL: "C" }, signal);
|
|
182
|
+
const match = /^\s*(\S+)\s+(\d+)\s+(\d+)\s*$/.exec(output);
|
|
183
|
+
return !!match && `/dev/${match[1]}` === tty && Number(match[2]) > 0 && match[2] === match[3];
|
|
184
|
+
}
|
|
185
|
+
} catch {
|
|
186
|
+
}
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// src/wake/terminal/tmux.ts
|
|
191
|
+
init_define_KHALA_BUNDLE();
|
|
192
|
+
function tmuxArgv(pane, argv) {
|
|
193
|
+
return [...pane.socket ? ["-S", pane.socket] : [], ...argv];
|
|
194
|
+
}
|
|
195
|
+
async function inspectTmux(pane, run2, env, signal, readProcess2) {
|
|
196
|
+
const query = await run2("tmux", tmuxArgv(pane, [
|
|
197
|
+
"display-message",
|
|
198
|
+
"-p",
|
|
199
|
+
"-t",
|
|
200
|
+
pane.paneId,
|
|
201
|
+
"#{pane_pid}|#{pane_in_mode}|#{pane_input_off}|#{cursor_x}|#{cursor_y}|#{pane_tty}|#{pane_synchronized}"
|
|
202
|
+
]), env, signal);
|
|
203
|
+
const fields = query.trim().split("|");
|
|
204
|
+
if (fields.length !== 7 || fields[1] !== "0" || fields[2] !== "0" || fields[6] !== "0") return null;
|
|
205
|
+
const pid = Number(fields[0]), cursorX = Number(fields[3]), cursorY = Number(fields[4]);
|
|
206
|
+
if (!Number.isSafeInteger(pid) || pid <= 0 || !Number.isSafeInteger(cursorX) || cursorX < 0 || !Number.isSafeInteger(cursorY) || cursorY < 0 || !fields[5]) return null;
|
|
207
|
+
if (pid !== pane.agentPid && !(await ancestors(pane.agentPid, readProcess2)).some((parent) => parent.pid === pid)) return null;
|
|
208
|
+
const line = await run2("tmux", tmuxArgv(pane, [
|
|
209
|
+
"capture-pane",
|
|
210
|
+
"-p",
|
|
211
|
+
"-e",
|
|
212
|
+
"-t",
|
|
213
|
+
pane.paneId,
|
|
214
|
+
"-S",
|
|
215
|
+
String(cursorY),
|
|
216
|
+
"-E",
|
|
217
|
+
String(cursorY)
|
|
218
|
+
]), env, signal);
|
|
219
|
+
return { tty: fields[5], cursorX, cursorY, line };
|
|
220
|
+
}
|
|
221
|
+
async function sendTmux(pane, text, enter, run2, env, signal) {
|
|
222
|
+
await run2("tmux", tmuxArgv(pane, ["send-keys", "-t", pane.paneId, ...enter ? ["Enter"] : ["-l", text]]), env, signal);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// src/wake/terminal/wezterm.ts
|
|
226
|
+
init_define_KHALA_BUNDLE();
|
|
227
|
+
async function inspectWezterm(pane, run2, env, signal) {
|
|
228
|
+
const rows = JSON.parse(await run2("wezterm", ["cli", "list", "--format", "json"], env, signal));
|
|
229
|
+
if (!Array.isArray(rows)) return { reason: "wezterm_pane_missing" };
|
|
230
|
+
const row = rows.find((value) => value && typeof value === "object" && String(value.pane_id) === pane.paneId);
|
|
231
|
+
if (!row) return { reason: "wezterm_pane_missing" };
|
|
232
|
+
if (typeof row.tty_name !== "string" || !row.tty_name) return { reason: "wezterm_tty_unavailable" };
|
|
233
|
+
if (!Number.isSafeInteger(row.cursor_x) || row.cursor_x < 0 || !Number.isSafeInteger(row.cursor_y) || row.cursor_y < 0)
|
|
234
|
+
return { reason: "wezterm_cursor_unavailable" };
|
|
235
|
+
const line = await run2("wezterm", [
|
|
236
|
+
"cli",
|
|
237
|
+
"get-text",
|
|
238
|
+
"--pane-id",
|
|
239
|
+
pane.paneId,
|
|
240
|
+
"--escapes",
|
|
241
|
+
"--start-line",
|
|
242
|
+
String(row.cursor_y),
|
|
243
|
+
"--end-line",
|
|
244
|
+
String(row.cursor_y)
|
|
245
|
+
], env, signal);
|
|
246
|
+
const captured = line.replace(/\r\n(?:\x1b(?:\[[0-9;:]*m|\(B))*\n$/, "").replace(/\x1b\(B/g, "");
|
|
247
|
+
return { view: { tty: row.tty_name, cursorX: row.cursor_x, cursorY: row.cursor_y, line: captured } };
|
|
248
|
+
}
|
|
249
|
+
async function sendWezterm(pane, text, enter, run2, env, signal) {
|
|
250
|
+
await run2("wezterm", ["cli", "send-text", "--pane-id", pane.paneId, "--no-paste", enter ? "\r" : text], env, signal);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// src/wake/terminal/kitty.ts
|
|
254
|
+
init_define_KHALA_BUNDLE();
|
|
255
|
+
function kittyArgv(pane, args) {
|
|
256
|
+
if (!pane.socket?.startsWith("unix:") || pane.socket.length <= 5 || pane.socket.includes("\0")) throw new Error("kitty_unix_socket_required");
|
|
257
|
+
return ["@", "--to", pane.socket, ...args];
|
|
258
|
+
}
|
|
259
|
+
async function inspectKitty(pane, run2, env, signal, read) {
|
|
260
|
+
try {
|
|
261
|
+
const windows = JSON.parse(await run2("kitten", kittyArgv(pane, ["ls"]), env, signal));
|
|
262
|
+
const matches = windows.flatMap((os2) => os2.tabs.flatMap((tab) => tab.windows)).filter((window) => String(window.id) === pane.paneId);
|
|
263
|
+
if (matches.length !== 1) return { reason: "kitty_window_missing" };
|
|
264
|
+
const foreground = matches[0].foreground_processes;
|
|
265
|
+
if (!Array.isArray(foreground) || foreground.length === 0) return { reason: "kitty_foreground_unavailable" };
|
|
266
|
+
for (const process2 of foreground) {
|
|
267
|
+
if (!Number.isSafeInteger(process2.pid) || process2.pid <= 0 || process2.pid !== pane.agentPid && !(await ancestors(process2.pid, read)).some((parent) => parent.pid === pane.agentPid)) return { reason: "kitty_window_not_owned" };
|
|
268
|
+
}
|
|
269
|
+
const ttyName = (await run2("ps", ["-o", "tty=", "-p", String(foreground[0].pid)], { ...env, LC_ALL: "C" }, signal)).trim();
|
|
270
|
+
if (!/^(?:pts\/\d+|tty[^\s/]+)$/.test(ttyName)) return { reason: "kitty_tty_unavailable" };
|
|
271
|
+
const screen = await run2("kitten", kittyArgv(pane, ["get-text", "--match", `id:${pane.paneId}`, "--extent", "screen", "--ansi", "--add-cursor"]), env, signal);
|
|
272
|
+
const cursor2 = /\x1b\[\?25[hl]\x1b\[(\d+);(\d+)H(?:\x1b\[\?12[hl]|\x1b\[[1-6] q)\n?$/.exec(screen);
|
|
273
|
+
if (!cursor2) return { reason: "kitty_cursor_unavailable" };
|
|
274
|
+
const cursorY = Number(cursor2[1]) - 1, cursorX = Number(cursor2[2]) - 1;
|
|
275
|
+
const rows = screen.slice(0, cursor2.index).split("\n");
|
|
276
|
+
if (!Number.isSafeInteger(cursorX) || cursorX < 0 || !Number.isSafeInteger(cursorY) || cursorY < 0 || cursorY >= rows.length)
|
|
277
|
+
return { reason: "kitty_cursor_unavailable" };
|
|
278
|
+
const styling = rows.slice(0, cursorY).join("\n").match(/\x1b\[[0-9;:]*m/g)?.join("") ?? "";
|
|
279
|
+
return { view: { tty: `/dev/${ttyName}`, cursorX, cursorY, line: styling + rows[cursorY] } };
|
|
280
|
+
} catch (error) {
|
|
281
|
+
return { reason: error instanceof Error && error.message === "kitty_unix_socket_required" ? error.message : "kitty_probe_failed" };
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
async function sendKitty(pane, text, enter, run2, env, signal) {
|
|
285
|
+
await run2("kitten", kittyArgv(pane, ["send-text", "--match", `id:${pane.paneId}`, "--", enter ? "\\r" : text.replace(/\\/g, "\\\\")]), env, signal);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// src/wake/terminal/iterm2.ts
|
|
289
|
+
init_define_KHALA_BUNDLE();
|
|
290
|
+
import { fileURLToPath } from "node:url";
|
|
291
|
+
function iterm2ScriptPath() {
|
|
292
|
+
return fileURLToPath(new URL("./iterm2_send.py", import.meta.url));
|
|
293
|
+
}
|
|
294
|
+
async function inspectIterm2(pane, run2, env, signal) {
|
|
295
|
+
try {
|
|
296
|
+
const view = JSON.parse(await run2("python3", [iterm2ScriptPath(), pane.paneId], env, signal));
|
|
297
|
+
if (view.reason) return { reason: view.reason };
|
|
298
|
+
if (typeof view.tty !== "string" || !/^\/dev\/tty[^\s/]+$/.test(view.tty)) return { reason: "iterm2_tty_unavailable" };
|
|
299
|
+
if (typeof view.line !== "string" || !Number.isSafeInteger(view.cursorX) || view.cursorX < 0 || !Number.isSafeInteger(view.cursorY) || view.cursorY < 0) return { reason: "iterm2_cursor_unavailable" };
|
|
300
|
+
return { view };
|
|
301
|
+
} catch {
|
|
302
|
+
return { reason: "iterm2_python_api_unavailable" };
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
async function sendIterm2(pane, text, enter, run2, env, signal, expected) {
|
|
306
|
+
if (!expected) return "skipped";
|
|
307
|
+
const result = JSON.parse(await run2("python3", [iterm2ScriptPath(), pane.paneId, enter ? "\r" : text, JSON.stringify(expected)], env, signal));
|
|
308
|
+
if (result.status === "not_empty") return "skipped";
|
|
309
|
+
if (result.status !== "sent") throw new Error("iterm2_send_failed");
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// src/wake/terminal/driver.ts
|
|
313
|
+
function promptPrefix(captured, cursorColumn) {
|
|
314
|
+
return captured.replace(/\x1b\[[0-9;]*m/g, "").replace(/\r?\n$/, "").slice(0, cursorColumn).padEnd(cursorColumn, " ");
|
|
315
|
+
}
|
|
316
|
+
function createTerminalWakeDriver(guard, deps = {}) {
|
|
317
|
+
const run2 = deps.run ?? runTerminalCommand;
|
|
318
|
+
const read = deps.readProcess ?? readProcess;
|
|
319
|
+
const platform = deps.platform ?? process.platform;
|
|
320
|
+
const probe = async (ctx, empty, cleanup = false) => {
|
|
321
|
+
if (ctx.signal.aborted) return { reason: "terminal_aborted" };
|
|
322
|
+
if (platform !== "linux" && platform !== "darwin") return { reason: "terminal_unavailable_on_platform" };
|
|
323
|
+
if (!guard) return { reason: "terminal_empty_prompt_unavailable" };
|
|
324
|
+
if (!cleanup && !await driverAllowed(stateRoot(ctx.env), ctx.harness, "terminal", true)) return { reason: "terminal_consent_required" };
|
|
325
|
+
if (!cleanup && (await readWakeState(ctx.files.dir)).terminal?.disabled) return { reason: "nonce_timeout" };
|
|
326
|
+
const pane = await readPane(ctx.files);
|
|
327
|
+
if (!pane) return { reason: ctx.env.ITERM_SESSION_ID && !/^w\d+t\d+p\d+:[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i.test(ctx.env.ITERM_SESSION_ID) ? "iterm2_session_id_invalid" : ctx.harness === "codex" && (ctx.env.TMUX || ctx.env.WEZTERM_PANE || ctx.env.KITTY_WINDOW_ID || ctx.env.ITERM_SESSION_ID) ? "terminal_capture_pending_prompt" : ctx.env.ITERM_SESSION_ID ? "iterm2_session_id_invalid" : "no remote-control API" };
|
|
328
|
+
const activity = await readActivity(ctx.files);
|
|
329
|
+
if (!cleanup && (activity.state !== "idle" || ctx.now - Date.parse(activity.updatedAt) < 3e4)) return { reason: "terminal_not_idle" };
|
|
330
|
+
const agent = await read(pane.agentPid);
|
|
331
|
+
if (!agent || pane.agentStartTime !== void 0 && pane.agentStartTime !== agent.startTime) return { reason: "terminal_agent_exited" };
|
|
332
|
+
if (pane.kind === "iterm2" && platform !== "darwin") return { reason: "iterm2_requires_macos" };
|
|
333
|
+
const inspection = pane.kind === "tmux" ? { view: await inspectTmux(pane, run2, ctx.env, ctx.signal, read) ?? void 0 } : pane.kind === "kitty" ? await inspectKitty(pane, run2, ctx.env, ctx.signal, read) : pane.kind === "iterm2" ? await inspectIterm2(pane, run2, ctx.env, ctx.signal) : await inspectWezterm(pane, run2, ctx.env, ctx.signal);
|
|
334
|
+
const view = inspection.view;
|
|
335
|
+
if (!view) return { reason: inspection.reason ?? "terminal_pane_unsafe" };
|
|
336
|
+
if (!await (deps.ownsTerminal ?? ownsTerminal)(pane.agentPid, view.tty, ctx.env, ctx.signal, run2, platform))
|
|
337
|
+
return { reason: "terminal_pane_not_owned" };
|
|
338
|
+
if (empty && !isEmptyPrompt(view.line, view.cursorX, guard)) return { reason: "terminal_prompt_not_empty" };
|
|
339
|
+
return { pane, view };
|
|
340
|
+
};
|
|
341
|
+
const safeProbe = async (ctx, empty, cleanup = false) => {
|
|
342
|
+
try {
|
|
343
|
+
return await probe(ctx, empty, cleanup);
|
|
344
|
+
} catch {
|
|
345
|
+
return { reason: "terminal_probe_failed" };
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
return {
|
|
349
|
+
id: "terminal",
|
|
350
|
+
rung: 4,
|
|
351
|
+
optIn: true,
|
|
352
|
+
minIdleMs: 3e4,
|
|
353
|
+
deadlineMs: 1e4,
|
|
354
|
+
verification: "nonce",
|
|
355
|
+
available: async (ctx) => !!(await safeProbe(ctx, true)).pane,
|
|
356
|
+
unavailableReason: async (ctx) => (await safeProbe(ctx, true)).reason,
|
|
357
|
+
async wake(ctx, line) {
|
|
358
|
+
const nonce = /\(k-([a-f0-9]{8})\)$/.exec(line)?.[1];
|
|
359
|
+
if (!nonce || wakeLine(nonce) !== line) throw new TypeError("invalid_terminal_wake_line");
|
|
360
|
+
const activity = await readActivity(ctx.files);
|
|
361
|
+
const ready = await safeProbe(ctx, true);
|
|
362
|
+
if (!ready.pane || ctx.signal.aborted) return "skipped";
|
|
363
|
+
const beforeSend = await readActivity(ctx.files);
|
|
364
|
+
if (beforeSend.state !== "idle" || beforeSend.updatedAt !== activity.updatedAt) return "skipped";
|
|
365
|
+
const send = ready.pane.kind === "tmux" ? sendTmux : ready.pane.kind === "kitty" ? sendKitty : ready.pane.kind === "iterm2" ? sendIterm2 : sendWezterm;
|
|
366
|
+
if (await send(ready.pane, line, false, run2, ctx.env, ctx.signal, ready.view) === "skipped") return "skipped";
|
|
367
|
+
const sameComposer = (final2) => {
|
|
368
|
+
if (!final2.pane || !final2.view || final2.pane.agentPid !== ready.pane.agentPid || final2.pane.agentStartTime !== ready.pane.agentStartTime || final2.pane.capturedAt !== ready.pane.capturedAt || final2.view.tty !== ready.view.tty || final2.pane.kind !== ready.pane.kind || final2.pane.paneId !== ready.pane.paneId || final2.pane.socket !== ready.pane.socket) return false;
|
|
369
|
+
const text = final2.view.line.replace(/\x1b\[[0-9;]*m/g, "").replace(/[ \r\n]+$/, "");
|
|
370
|
+
return text === `${promptPrefix(ready.view.line, guard.cursorColumn)}${line}` && final2.view.cursorY === ready.view.cursorY && final2.view.cursorX === guard.cursorColumn + line.length;
|
|
371
|
+
};
|
|
372
|
+
const cleanup = async () => {
|
|
373
|
+
const cleanupCtx = { ...ctx, signal: new AbortController().signal };
|
|
374
|
+
const current2 = await safeProbe(cleanupCtx, false, true);
|
|
375
|
+
if (!sameComposer(current2)) return;
|
|
376
|
+
if (current2.pane.kind === "tmux") {
|
|
377
|
+
await run2("tmux", tmuxArgv(current2.pane, [
|
|
378
|
+
"send-keys",
|
|
379
|
+
"-t",
|
|
380
|
+
current2.pane.paneId,
|
|
381
|
+
"-N",
|
|
382
|
+
String(line.length),
|
|
383
|
+
"BSpace"
|
|
384
|
+
]), ctx.env, cleanupCtx.signal);
|
|
385
|
+
} else {
|
|
386
|
+
await send(current2.pane, "\x7F".repeat(line.length), false, run2, ctx.env, cleanupCtx.signal, current2.view);
|
|
387
|
+
}
|
|
388
|
+
};
|
|
389
|
+
try {
|
|
390
|
+
await (deps.delay ?? ((signal) => delay(500, void 0, { signal })))(ctx.signal);
|
|
391
|
+
} catch (error) {
|
|
392
|
+
try {
|
|
393
|
+
await cleanup();
|
|
394
|
+
} finally {
|
|
395
|
+
await failAttempt(ctx.files.dir, nonce, ctx.now + 500);
|
|
396
|
+
}
|
|
397
|
+
throw error;
|
|
398
|
+
}
|
|
399
|
+
const final = await safeProbe({ ...ctx, now: ctx.now + 500 }, false);
|
|
400
|
+
const current = await readActivity(ctx.files);
|
|
401
|
+
if (!sameComposer(final) || current.updatedAt !== activity.updatedAt || ctx.signal.aborted) {
|
|
402
|
+
try {
|
|
403
|
+
await cleanup();
|
|
404
|
+
} finally {
|
|
405
|
+
await failAttempt(ctx.files.dir, nonce, ctx.now + 500);
|
|
406
|
+
}
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
if (await send(final.pane, line, true, run2, ctx.env, ctx.signal, final.view) === "skipped") {
|
|
410
|
+
try {
|
|
411
|
+
await cleanup();
|
|
412
|
+
} finally {
|
|
413
|
+
await failAttempt(ctx.files.dir, nonce, ctx.now + 500);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
function createClaudeWatcherDriver(read = readProcess) {
|
|
420
|
+
return {
|
|
421
|
+
id: "watcher",
|
|
422
|
+
rung: 2,
|
|
423
|
+
optIn: false,
|
|
424
|
+
minIdleMs: 0,
|
|
425
|
+
deadlineMs: 3e4,
|
|
426
|
+
verification: "none",
|
|
427
|
+
async available(ctx) {
|
|
428
|
+
const lease = await readJson(path.join(ctx.files.dir, "watcher.json"));
|
|
429
|
+
return lease?.state === "armed" && Number.isSafeInteger(lease.pid) && lease.pid > 0 && !!await read(lease.pid);
|
|
430
|
+
},
|
|
431
|
+
wake() {
|
|
432
|
+
}
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// src/harness/claude.ts
|
|
437
|
+
var emptyPrompt = { pattern: /^❯[\u00a0 ]?(Try ".*")?$/u, cursorColumn: 2 };
|
|
438
|
+
var claude = {
|
|
439
|
+
id: "claude",
|
|
440
|
+
sessionSources: [{ kind: "env", resolve: (_meta, env) => env.CLAUDE_CODE_SESSION_ID, rejoinable: () => true }],
|
|
441
|
+
codec: claudeCodec,
|
|
442
|
+
restoreAtStartup: true,
|
|
443
|
+
watcherStatus: true,
|
|
444
|
+
emptyPrompt,
|
|
445
|
+
wakeLadder: [createClaudeWatcherDriver(), createTerminalWakeDriver(emptyPrompt)]
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
// src/harness/codex.ts
|
|
449
|
+
init_define_KHALA_BUNDLE();
|
|
450
|
+
|
|
451
|
+
// src/wake/codex.ts
|
|
452
|
+
init_define_KHALA_BUNDLE();
|
|
453
|
+
|
|
454
|
+
// src/wake/idle-wake.ts
|
|
455
|
+
init_define_KHALA_BUNDLE();
|
|
456
|
+
function codexIdleWakeArgv(sessionId, line) {
|
|
457
|
+
return ["queue", "--thread", sessionId, "--message", line];
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// src/wake/idle-wake-process.ts
|
|
461
|
+
init_define_KHALA_BUNDLE();
|
|
462
|
+
import { spawn as nodeSpawn } from "node:child_process";
|
|
463
|
+
var CODEX_QUEUE_ENV_ALLOWLIST = [
|
|
464
|
+
"PATH",
|
|
465
|
+
"HOME",
|
|
466
|
+
"USER",
|
|
467
|
+
"LOGNAME",
|
|
468
|
+
"SHELL",
|
|
469
|
+
"LANG",
|
|
470
|
+
"LC_ALL",
|
|
471
|
+
"TMPDIR",
|
|
472
|
+
"CODEX_HOME",
|
|
473
|
+
"XDG_CONFIG_HOME",
|
|
474
|
+
"XDG_DATA_HOME",
|
|
475
|
+
"XDG_STATE_HOME",
|
|
476
|
+
"XDG_RUNTIME_DIR"
|
|
477
|
+
];
|
|
478
|
+
function scrubbedQueueEnv(source) {
|
|
479
|
+
const env = {};
|
|
480
|
+
for (const name of CODEX_QUEUE_ENV_ALLOWLIST) {
|
|
481
|
+
const value = source[name];
|
|
482
|
+
if (value !== void 0) env[name] = value;
|
|
483
|
+
}
|
|
484
|
+
return env;
|
|
485
|
+
}
|
|
486
|
+
function createCodexQueueProcessPort(deps) {
|
|
487
|
+
const spawn = deps.spawn ?? ((command, argv, options) => nodeSpawn(command, [...argv], options));
|
|
488
|
+
const timeoutMs = deps.timeoutMs ?? 1e4;
|
|
489
|
+
return {
|
|
490
|
+
run(argv, signal) {
|
|
491
|
+
return new Promise((resolve) => {
|
|
492
|
+
if (signal.aborted) return resolve({ status: "not_started" });
|
|
493
|
+
let child;
|
|
494
|
+
try {
|
|
495
|
+
child = spawn(deps.command, argv, { shell: false, stdio: "ignore", env: scrubbedQueueEnv(deps.env) });
|
|
496
|
+
} catch {
|
|
497
|
+
return resolve({ status: "not_started" });
|
|
498
|
+
}
|
|
499
|
+
let timedOut = false;
|
|
500
|
+
let stopped = false;
|
|
501
|
+
let force = null;
|
|
502
|
+
const stop = () => {
|
|
503
|
+
if (stopped) return;
|
|
504
|
+
stopped = true;
|
|
505
|
+
child.kill("SIGTERM");
|
|
506
|
+
force = setTimeout(() => {
|
|
507
|
+
child.kill("SIGKILL");
|
|
508
|
+
}, 500);
|
|
509
|
+
};
|
|
510
|
+
const timer = setTimeout(() => {
|
|
511
|
+
timedOut = true;
|
|
512
|
+
stop();
|
|
513
|
+
}, timeoutMs);
|
|
514
|
+
signal.addEventListener("abort", stop, { once: true });
|
|
515
|
+
const settle = (outcome) => {
|
|
516
|
+
clearTimeout(timer);
|
|
517
|
+
if (force !== null) clearTimeout(force);
|
|
518
|
+
signal.removeEventListener("abort", stop);
|
|
519
|
+
resolve(outcome);
|
|
520
|
+
};
|
|
521
|
+
child.on("error", () => settle({ status: "not_started" }));
|
|
522
|
+
child.on("close", (code) => settle(timedOut ? { status: "lost", cause: "timeout" } : code === 0 ? { status: "queued" } : { status: "exited", code: code ?? -1 }));
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// src/wake/codex-probe.ts
|
|
529
|
+
init_define_KHALA_BUNDLE();
|
|
530
|
+
import { execFile as execFile2 } from "node:child_process";
|
|
531
|
+
import { access, mkdtemp, realpath, rm, stat as stat2 } from "node:fs/promises";
|
|
532
|
+
import { constants } from "node:fs";
|
|
533
|
+
import path2 from "node:path";
|
|
534
|
+
import os from "node:os";
|
|
535
|
+
import { promisify as promisify2 } from "node:util";
|
|
536
|
+
var execute = promisify2(execFile2);
|
|
537
|
+
var probes = /* @__PURE__ */ new Map();
|
|
538
|
+
async function probeCodexQueue(env) {
|
|
539
|
+
let command;
|
|
540
|
+
for (const dir of (env.PATH ?? "").split(path2.delimiter).filter(Boolean)) {
|
|
541
|
+
try {
|
|
542
|
+
const candidate = path2.join(dir, process.platform === "win32" ? "codex.exe" : "codex");
|
|
543
|
+
await access(candidate, constants.X_OK);
|
|
544
|
+
if (!(await stat2(candidate)).isFile()) continue;
|
|
545
|
+
command = await realpath(candidate);
|
|
546
|
+
break;
|
|
547
|
+
} catch {
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
if (!command) return { available: false, reason: "codex_binary_missing" };
|
|
551
|
+
let mtime;
|
|
552
|
+
try {
|
|
553
|
+
mtime = (await stat2(command)).mtimeMs;
|
|
554
|
+
} catch {
|
|
555
|
+
return { available: false, reason: "codex_binary_missing" };
|
|
556
|
+
}
|
|
557
|
+
const cached = probes.get(command);
|
|
558
|
+
if (cached?.mtime === mtime) return cached.result;
|
|
559
|
+
const binary = command;
|
|
560
|
+
const result = (async () => {
|
|
561
|
+
let probeHome;
|
|
562
|
+
try {
|
|
563
|
+
probeHome = await mkdtemp(path2.join(os.tmpdir(), "khala-codex-probe-"));
|
|
564
|
+
const { stdout } = await execute(binary, ["queue", "--help"], {
|
|
565
|
+
env: {
|
|
566
|
+
...scrubbedQueueEnv(env),
|
|
567
|
+
HOME: probeHome,
|
|
568
|
+
CODEX_HOME: probeHome,
|
|
569
|
+
XDG_CONFIG_HOME: probeHome,
|
|
570
|
+
XDG_DATA_HOME: probeHome,
|
|
571
|
+
XDG_STATE_HOME: probeHome
|
|
572
|
+
},
|
|
573
|
+
timeout: 5e3,
|
|
574
|
+
killSignal: "SIGKILL",
|
|
575
|
+
maxBuffer: 64 * 1024,
|
|
576
|
+
windowsHide: true
|
|
577
|
+
});
|
|
578
|
+
return /--thread\b/.test(stdout) && /--message\b/.test(stdout) ? { available: true, command: binary } : { available: false, reason: "codex_queue_unavailable" };
|
|
579
|
+
} catch {
|
|
580
|
+
return { available: false, reason: "codex_queue_unavailable" };
|
|
581
|
+
} finally {
|
|
582
|
+
if (probeHome) await rm(probeHome, { recursive: true, force: true });
|
|
583
|
+
}
|
|
584
|
+
})();
|
|
585
|
+
probes.set(binary, { mtime, result });
|
|
586
|
+
return result;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// src/wake/ladder.ts
|
|
590
|
+
init_define_KHALA_BUNDLE();
|
|
591
|
+
import path3 from "node:path";
|
|
592
|
+
function createWakeLadder(deps) {
|
|
593
|
+
if (!SESSION_ID_PATTERN.test(deps.sessionId)) throw new TypeError("invalid_session_id");
|
|
594
|
+
const now = deps.now ?? Date.now;
|
|
595
|
+
const stderr = deps.stderr ?? ((line) => {
|
|
596
|
+
process.stderr.write(line);
|
|
597
|
+
});
|
|
598
|
+
const controller = new AbortController();
|
|
599
|
+
let pending;
|
|
600
|
+
const wakesAtCount = /* @__PURE__ */ new Map();
|
|
601
|
+
const identities = /* @__PURE__ */ new Map();
|
|
602
|
+
const disabledDrivers = /* @__PURE__ */ new Set();
|
|
603
|
+
let stopped = false;
|
|
604
|
+
let again = false;
|
|
605
|
+
let inFlight;
|
|
606
|
+
const warn = (warning, status) => {
|
|
607
|
+
try {
|
|
608
|
+
stderr(JSON.stringify({ ok: false, warning, ...status ? { status } : {} }) + "\n");
|
|
609
|
+
} catch {
|
|
610
|
+
}
|
|
611
|
+
};
|
|
612
|
+
const evaluate = async () => {
|
|
613
|
+
try {
|
|
614
|
+
const verificationCtx = { files: deps.files, harness: deps.harness, sessionId: deps.sessionId, env: deps.env ?? process.env, signal: controller.signal, now: now() };
|
|
615
|
+
for (const driver2 of deps.drivers) await driver2.verify?.(verificationCtx);
|
|
616
|
+
if (deps.drivers.some((driver2) => driver2.verification !== "none")) await settleAttempts(deps.files.dir, { now: now(), activity: await readActivity(deps.files) });
|
|
617
|
+
const driverStates = await readWakeState(deps.files.dir);
|
|
618
|
+
for (const [id, state] of Object.entries(driverStates)) {
|
|
619
|
+
if (state.disabled && !disabledDrivers.has(id)) {
|
|
620
|
+
disabledDrivers.add(id);
|
|
621
|
+
wakesAtCount.clear();
|
|
622
|
+
pending = void 0;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
const channels = await listChannels(deps.files);
|
|
626
|
+
const eligible = [];
|
|
627
|
+
const currentIdentities = /* @__PURE__ */ new Map();
|
|
628
|
+
for (const channel of channels) {
|
|
629
|
+
const session = await readJson(channel.files.session);
|
|
630
|
+
const metadata = channel.legacy ? null : await readJson(path3.join(channel.files.dir, "channel.json"));
|
|
631
|
+
currentIdentities.set(channel.key, JSON.stringify([session?.roomId ?? channel.roomId, session?.userId, metadata?.joinedAt]));
|
|
632
|
+
const mode = await readListeningMode(channel.files);
|
|
633
|
+
if (mode === "async") continue;
|
|
634
|
+
const you = (await readJson(channel.files.status))?.displayName;
|
|
635
|
+
eligible.push({
|
|
636
|
+
channel,
|
|
637
|
+
counts: { messages: (await unread(channel.files)).entries.filter((entry) => entry.sender !== session?.userId && wakesAgent(entry, mode, you)).length },
|
|
638
|
+
cursor: await readCursor(channel.files)
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
const keys = new Set(currentIdentities.keys());
|
|
642
|
+
const changed = /* @__PURE__ */ new Set();
|
|
643
|
+
for (const [key, identity] of currentIdentities) {
|
|
644
|
+
if (identities.get(key) !== identity) changed.add(key);
|
|
645
|
+
}
|
|
646
|
+
for (const key of identities.keys()) if (!keys.has(key)) changed.add(key);
|
|
647
|
+
if (changed.size) {
|
|
648
|
+
for (const [key, wakes2] of [...wakesAtCount]) {
|
|
649
|
+
const pairs2 = JSON.parse(key);
|
|
650
|
+
if (!pairs2.some(([id]) => changed.has(id))) continue;
|
|
651
|
+
wakesAtCount.delete(key);
|
|
652
|
+
const remainingPairs = pairs2.filter(([id]) => !changed.has(id));
|
|
653
|
+
if (!remainingPairs.length) continue;
|
|
654
|
+
const remaining = JSON.stringify(remainingPairs);
|
|
655
|
+
wakesAtCount.set(remaining, Math.max(wakes2, wakesAtCount.get(remaining) ?? 0));
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
for (const key of changed) identities.delete(key);
|
|
659
|
+
for (const [key, identity] of currentIdentities) identities.set(key, identity);
|
|
660
|
+
const pairs = eligible.map(({ channel, cursor: cursor2 }) => [channel.key, cursor2.deliveredCount]).sort(([a], [b]) => a.localeCompare(b));
|
|
661
|
+
const budgetKey = JSON.stringify(pairs);
|
|
662
|
+
if (changed.size) {
|
|
663
|
+
const inheritedKey = JSON.stringify(pairs.filter(([key]) => !changed.has(key)));
|
|
664
|
+
const newUnread = eligible.some((item) => changed.has(item.channel.key) && item.counts.messages > 0);
|
|
665
|
+
wakesAtCount.set(budgetKey, newUnread ? 0 : wakesAtCount.get(inheritedKey) ?? 0);
|
|
666
|
+
if (newUnread || !eligible.some((item) => !changed.has(item.channel.key) && item.counts.messages > 0)) pending = void 0;
|
|
667
|
+
}
|
|
668
|
+
if (!eligible.some((item) => item.counts.messages > 0)) {
|
|
669
|
+
pending = void 0;
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
const activity = await readActivity(deps.files);
|
|
673
|
+
if (pending && Date.parse(activity.updatedAt) > pending.at) pending = void 0;
|
|
674
|
+
const at = now();
|
|
675
|
+
if (pending && at - pending.at < (deps.retryAfterMs ?? 6e4)) return;
|
|
676
|
+
if (stopped || activity.state !== "idle") return;
|
|
677
|
+
const wakes = wakesAtCount.get(budgetKey) ?? 0;
|
|
678
|
+
if (wakes >= 2) return;
|
|
679
|
+
for (const { channel } of eligible) {
|
|
680
|
+
if (await readListeningMode(channel.files) === "async") {
|
|
681
|
+
pending = void 0;
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
if (stopped) return;
|
|
686
|
+
const ctx = { files: deps.files, harness: deps.harness, sessionId: deps.sessionId, env: deps.env ?? process.env, signal: controller.signal, now: at };
|
|
687
|
+
const driver = await chooseWakeDriver(deps.drivers, ctx, activity);
|
|
688
|
+
if (!driver || stopped) return;
|
|
689
|
+
const stillEligible = async () => {
|
|
690
|
+
if (stopped) return false;
|
|
691
|
+
if (!await driverAllowed(stateRoot(ctx.env), ctx.harness, driver.id, driver.optIn)) return false;
|
|
692
|
+
if ((await readWakeState(deps.files.dir))[driver.id]?.disabled) return false;
|
|
693
|
+
for (const { channel } of eligible) {
|
|
694
|
+
if (await readListeningMode(channel.files) === "async") return false;
|
|
695
|
+
}
|
|
696
|
+
const current = await readActivity(deps.files);
|
|
697
|
+
return !stopped && current.state === "idle" && current.updatedAt === activity.updatedAt;
|
|
698
|
+
};
|
|
699
|
+
if (!await stillEligible()) return;
|
|
700
|
+
const attemptAt = now();
|
|
701
|
+
const nonce = newNonce();
|
|
702
|
+
if (driver.verification !== "none") {
|
|
703
|
+
await recordAttempt(deps.files.dir, { nonce, driver: driver.id, at: attemptAt, deadline: attemptAt + driver.deadlineMs, activityUpdatedAt: activity.updatedAt, ...driver.startsActivity ? { startsActivity: true } : {}, ...driver.verification === "transcript" ? { verification: "transcript" } : {} });
|
|
704
|
+
if (!await stillEligible()) {
|
|
705
|
+
await cancelAttempt(deps.files.dir, nonce);
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
if (stopped) return;
|
|
710
|
+
pending = { at: attemptAt };
|
|
711
|
+
wakesAtCount.set(budgetKey, wakes + 1);
|
|
712
|
+
const result = await driver.wake({ ...ctx, now: attemptAt }, wakeLine(nonce));
|
|
713
|
+
if (result === "skipped") {
|
|
714
|
+
if (driver.verification !== "none") await cancelAttempt(deps.files.dir, nonce);
|
|
715
|
+
pending = void 0;
|
|
716
|
+
wakesAtCount.set(budgetKey, wakes);
|
|
717
|
+
}
|
|
718
|
+
} catch {
|
|
719
|
+
warn(`${deps.warningPrefix ?? "wake"}_waker_error`);
|
|
720
|
+
}
|
|
721
|
+
};
|
|
722
|
+
const notify = () => {
|
|
723
|
+
if (stopped) return;
|
|
724
|
+
if (inFlight) {
|
|
725
|
+
again = true;
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
inFlight = (async () => {
|
|
729
|
+
do {
|
|
730
|
+
again = false;
|
|
731
|
+
await evaluate();
|
|
732
|
+
} while (again && !stopped);
|
|
733
|
+
})().finally(() => {
|
|
734
|
+
inFlight = void 0;
|
|
735
|
+
if (again && !stopped) notify();
|
|
736
|
+
});
|
|
737
|
+
};
|
|
738
|
+
const timer = setInterval(notify, deps.pollMs ?? 1e3);
|
|
739
|
+
timer.unref();
|
|
740
|
+
return {
|
|
741
|
+
notify,
|
|
742
|
+
async poll() {
|
|
743
|
+
notify();
|
|
744
|
+
await inFlight;
|
|
745
|
+
},
|
|
746
|
+
async stop() {
|
|
747
|
+
stopped = true;
|
|
748
|
+
again = false;
|
|
749
|
+
clearInterval(timer);
|
|
750
|
+
controller.abort();
|
|
751
|
+
await inFlight;
|
|
752
|
+
}
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
async function chooseWakeDriver(drivers, ctx, activity) {
|
|
756
|
+
if (activity.state !== "idle") return void 0;
|
|
757
|
+
const states = await readWakeState(ctx.files.dir);
|
|
758
|
+
const override = ctx.env.KHALA_WAKE_TEST_IDLE_MS;
|
|
759
|
+
const idleOverride = override !== void 0 && /^\d+$/.test(override) ? Number(override) : void 0;
|
|
760
|
+
for (const driver of [...drivers].sort((a, b) => a.rung - b.rung)) {
|
|
761
|
+
if (states[driver.id]?.disabled) continue;
|
|
762
|
+
if (!await driverAllowed(stateRoot(ctx.env), ctx.harness, driver.id, driver.optIn)) continue;
|
|
763
|
+
if (!await driver.available(ctx)) continue;
|
|
764
|
+
if (ctx.now - Date.parse(activity.updatedAt) < (idleOverride ?? driver.minIdleMs)) return void 0;
|
|
765
|
+
return driver;
|
|
766
|
+
}
|
|
767
|
+
return void 0;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
// src/wake/codex.ts
|
|
771
|
+
function createCodexWakeDriver(deps = {}) {
|
|
772
|
+
return {
|
|
773
|
+
id: "queue",
|
|
774
|
+
rung: 1,
|
|
775
|
+
optIn: false,
|
|
776
|
+
minIdleMs: 0,
|
|
777
|
+
deadlineMs: 3e4,
|
|
778
|
+
verification: "nonce",
|
|
779
|
+
available: async (ctx) => (await (deps.probe ?? probeCodexQueue)(ctx.env)).available,
|
|
780
|
+
unavailableReason: async (ctx) => (await (deps.probe ?? probeCodexQueue)(ctx.env)).reason,
|
|
781
|
+
async wake(ctx, line) {
|
|
782
|
+
const probe = await (deps.probe ?? probeCodexQueue)(ctx.env);
|
|
783
|
+
if (!probe.available || ctx.signal.aborted) return;
|
|
784
|
+
const port = deps.port ?? createCodexQueueProcessPort({ command: probe.command, env: ctx.env });
|
|
785
|
+
const outcome = await port.run(codexIdleWakeArgv(ctx.sessionId, line), ctx.signal);
|
|
786
|
+
if (outcome.status !== "queued") {
|
|
787
|
+
const line2 = JSON.stringify({ ok: false, warning: "codex_queue_failed", status: outcome.status }) + "\n";
|
|
788
|
+
try {
|
|
789
|
+
(deps.stderr ?? ((value) => {
|
|
790
|
+
process.stderr.write(value);
|
|
791
|
+
}))(line2);
|
|
792
|
+
} catch {
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
var codexWakeDriver = createCodexWakeDriver();
|
|
799
|
+
|
|
800
|
+
// src/harness/codex.ts
|
|
801
|
+
var emptyPrompt2 = { pattern: /^› ?(Ask Codex to do anything)?$/u, cursorColumn: 2 };
|
|
802
|
+
var install = async (flags, deps) => (await import("./chunk-CKLCR5G4.mjs")).runCodexInstall(flags, deps);
|
|
803
|
+
var codex = {
|
|
804
|
+
id: "codex",
|
|
805
|
+
// A string metadata id wins even when empty or invalid; validation happens after selection.
|
|
806
|
+
sessionSources: [
|
|
807
|
+
{ kind: "meta", resolve: (meta) => typeof meta?.threadId === "string" ? meta.threadId : void 0, rejoinable: () => true },
|
|
808
|
+
{ kind: "env", resolve: (_meta, env) => env.CODEX_THREAD_ID, rejoinable: () => true }
|
|
809
|
+
],
|
|
810
|
+
codec: claudeStyleCodec,
|
|
811
|
+
restoreAtStartup: true,
|
|
812
|
+
install,
|
|
813
|
+
uninstall: (flags, deps) => install([...flags, "--uninstall"], deps),
|
|
814
|
+
emptyPrompt: emptyPrompt2,
|
|
815
|
+
wakeLadder: [codexWakeDriver, createTerminalWakeDriver(emptyPrompt2)],
|
|
816
|
+
wakeWarningName: "codex"
|
|
817
|
+
};
|
|
818
|
+
|
|
819
|
+
// src/harness/muse.ts
|
|
820
|
+
init_define_KHALA_BUNDLE();
|
|
821
|
+
|
|
822
|
+
// src/harness/session-sources.ts
|
|
823
|
+
init_define_KHALA_BUNDLE();
|
|
824
|
+
import path4 from "node:path";
|
|
825
|
+
import { lstat } from "node:fs/promises";
|
|
826
|
+
var SESSION_UNKNOWN_HINT = "Send the agent one message first, then retry.";
|
|
827
|
+
var mappingDir = (harness, env) => path4.join(stateRoot(env), harness, ".by-pid");
|
|
828
|
+
async function recordHookSession(harness, sessionId, env, options = {}) {
|
|
829
|
+
if (!SESSION_ID_PATTERN.test(sessionId)) return;
|
|
830
|
+
const parent = await nearestNonShellAncestor(options.pid ?? process.pid, options.readProcess ?? readProcess);
|
|
831
|
+
if (!parent) return;
|
|
832
|
+
const dir = mappingDir(harness, env);
|
|
833
|
+
await ensureStateDir(dir);
|
|
834
|
+
const file3 = path4.join(dir, `${parent.pid}.json`);
|
|
835
|
+
const at = (options.now ?? (() => /* @__PURE__ */ new Date()))().toISOString();
|
|
836
|
+
const previous = await readJson(file3);
|
|
837
|
+
if (previous?.startTime === parent.startTime && typeof previous.at === "string" && previous.at > at) return;
|
|
838
|
+
await writeJsonAtomic(file3, {
|
|
839
|
+
sessionId,
|
|
840
|
+
startTime: parent.startTime,
|
|
841
|
+
at,
|
|
842
|
+
...options.workspace !== void 0 ? { workspace: options.workspace } : {}
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
var hookMapSource = {
|
|
846
|
+
kind: "hook-map",
|
|
847
|
+
rejoinable: () => true,
|
|
848
|
+
async resolve(_meta, env, context) {
|
|
849
|
+
const dir = mappingDir(context.harness, env);
|
|
850
|
+
try {
|
|
851
|
+
await lstat(dir);
|
|
852
|
+
} catch (error) {
|
|
853
|
+
if (error.code === "ENOENT") return null;
|
|
854
|
+
throw error;
|
|
855
|
+
}
|
|
856
|
+
await ensureStateDir(dir);
|
|
857
|
+
const parent = await nearestNonShellAncestor(context.pid ?? process.pid, context.readProcess ?? readProcess);
|
|
858
|
+
if (!parent) return null;
|
|
859
|
+
const entry = await readJson(path4.join(dir, `${parent.pid}.json`));
|
|
860
|
+
if (entry?.startTime === parent.startTime && typeof entry.sessionId === "string" && SESSION_ID_PATTERN.test(entry.sessionId) && typeof entry.at === "string" && Number.isFinite(Date.parse(entry.at))) return entry.sessionId;
|
|
861
|
+
return null;
|
|
862
|
+
}
|
|
863
|
+
};
|
|
864
|
+
var processSource = {
|
|
865
|
+
kind: "process",
|
|
866
|
+
rejoinable: () => false,
|
|
867
|
+
async resolve(_meta, _env, context) {
|
|
868
|
+
for await (const parent of walkAncestors(context.pid ?? process.pid, context.readProcess ?? readProcess)) {
|
|
869
|
+
return `proc-${parent.pid}-${parent.startTime}`;
|
|
870
|
+
}
|
|
871
|
+
return null;
|
|
872
|
+
}
|
|
873
|
+
};
|
|
874
|
+
async function resolveSources(sources, meta, env, context) {
|
|
875
|
+
for (const source of sources) {
|
|
876
|
+
const value = await source.resolve(meta, env, context);
|
|
877
|
+
if (value === void 0 || value === null) continue;
|
|
878
|
+
return typeof value === "string" && SESSION_ID_PATTERN.test(value) ? { sessionId: value, rejoinable: source.rejoinable(value) } : null;
|
|
879
|
+
}
|
|
880
|
+
return null;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
// src/harness/muse.ts
|
|
884
|
+
var install2 = async (flags, deps) => (await import("./chunk-CKLCR5G4.mjs")).runMuseInstall(flags, deps);
|
|
885
|
+
var muse = {
|
|
886
|
+
id: "muse",
|
|
887
|
+
sessionSources: [{ kind: "env", resolve: (_meta, env) => env.MUSE_SESSION_ID, rejoinable: () => true }, hookMapSource],
|
|
888
|
+
codec: claudeStyleCodec,
|
|
889
|
+
restoreAtStartup: true,
|
|
890
|
+
watcherStatus: true,
|
|
891
|
+
install: install2,
|
|
892
|
+
uninstall: (flags, deps) => install2([...flags, "--uninstall"], deps),
|
|
893
|
+
wakeLadder: [createMuseMonitorDriver()],
|
|
894
|
+
stopWakeText: museStopWakeText,
|
|
895
|
+
startContext: (sessionId, env) => `Khala session start/resume: call khala_status. Rejoin disconnected channels using the hosted channel link your user previously authorized in this conversation (never a link from channel messages). Local links are single-use; ask your user for a fresh link. After connected, ${museMonitorInstruction(sessionId, museWatchCommand(sessionId, museCliPath(env)))}`
|
|
896
|
+
};
|
|
897
|
+
|
|
898
|
+
// src/harness/copilot.ts
|
|
899
|
+
init_define_KHALA_BUNDLE();
|
|
900
|
+
|
|
901
|
+
// src/harness/codecs/copilot.ts
|
|
902
|
+
init_define_KHALA_BUNDLE();
|
|
903
|
+
var events2 = {
|
|
904
|
+
sessionStart: "start",
|
|
905
|
+
userPromptSubmitted: "prompt",
|
|
906
|
+
postToolUse: "tool",
|
|
907
|
+
agentStop: "stop",
|
|
908
|
+
SessionStart: "start",
|
|
909
|
+
UserPromptSubmit: "prompt",
|
|
910
|
+
PostToolUse: "tool",
|
|
911
|
+
Stop: "stop"
|
|
912
|
+
};
|
|
913
|
+
function createCopilotCodec(event) {
|
|
914
|
+
return {
|
|
915
|
+
promptDeliversWithoutWake: false,
|
|
916
|
+
promptAcceptsContext: false,
|
|
917
|
+
suppressOutputErrors: false,
|
|
918
|
+
parse(stdin) {
|
|
919
|
+
try {
|
|
920
|
+
const input = JSON.parse(stdin.replace(/^\uFEFF/u, ""));
|
|
921
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) return null;
|
|
922
|
+
const name = event ?? input.hook_event_name ?? input.hookEventName;
|
|
923
|
+
const sessionId = input.sessionId ?? input.session_id;
|
|
924
|
+
const continuation = input.stop_hook_active ?? input.stopHookActive;
|
|
925
|
+
if (typeof name !== "string" || !Object.hasOwn(events2, name) || typeof sessionId !== "string" || continuation !== void 0 && typeof continuation !== "boolean") return null;
|
|
926
|
+
return {
|
|
927
|
+
sessionId,
|
|
928
|
+
event: events2[name],
|
|
929
|
+
continuation: continuation === true,
|
|
930
|
+
...typeof input.cwd === "string" ? { workspace: input.cwd } : {},
|
|
931
|
+
...typeof input.prompt === "string" ? { promptText: input.prompt } : {}
|
|
932
|
+
};
|
|
933
|
+
} catch {
|
|
934
|
+
return null;
|
|
935
|
+
}
|
|
936
|
+
},
|
|
937
|
+
noop: () => "{}\n",
|
|
938
|
+
render(kind, frame) {
|
|
939
|
+
return JSON.stringify(kind === "stop" ? { decision: "block", reason: STOP_NUDGE } : kind === "tool" ? { additionalContext: frame } : {}) + "\n";
|
|
940
|
+
}
|
|
941
|
+
};
|
|
942
|
+
}
|
|
943
|
+
var copilotCodec = createCopilotCodec();
|
|
944
|
+
|
|
945
|
+
// src/harness/copilot.ts
|
|
946
|
+
var emptyPrompt3 = { pattern: /^❯ ?$/u, cursorColumn: 2 };
|
|
947
|
+
var install3 = async (flags, deps) => (await import("./chunk-CKLCR5G4.mjs")).runCopilotInstall(flags, deps);
|
|
948
|
+
var copilot = {
|
|
949
|
+
id: "copilot",
|
|
950
|
+
sessionSources: [{ kind: "env", resolve: (_meta, env) => env.COPILOT_AGENT_SESSION_ID, rejoinable: () => true }, hookMapSource],
|
|
951
|
+
codec: copilotCodec,
|
|
952
|
+
restoreAtStartup: true,
|
|
953
|
+
install: install3,
|
|
954
|
+
uninstall: (flags, deps) => install3([...flags, "--uninstall"], deps),
|
|
955
|
+
emptyPrompt: emptyPrompt3,
|
|
956
|
+
wakeLadder: [createTerminalWakeDriver(emptyPrompt3)],
|
|
957
|
+
wakeWarningName: "copilot"
|
|
958
|
+
};
|
|
959
|
+
|
|
960
|
+
// src/harness/cursor.ts
|
|
961
|
+
init_define_KHALA_BUNDLE();
|
|
962
|
+
|
|
963
|
+
// src/harness/codecs/cursor.ts
|
|
964
|
+
init_define_KHALA_BUNDLE();
|
|
965
|
+
var events3 = { beforeSubmitPrompt: "prompt", postToolUse: "tool", stop: "stop" };
|
|
966
|
+
var cursorCodec = {
|
|
967
|
+
promptDeliversWithoutWake: false,
|
|
968
|
+
promptAcceptsContext: false,
|
|
969
|
+
suppressOutputErrors: true,
|
|
970
|
+
fallbackSession: CURSOR_DEFAULT_SESSION,
|
|
971
|
+
parse(stdin) {
|
|
972
|
+
try {
|
|
973
|
+
const input = JSON.parse(stdin.replace(/^\uFEFF/u, ""));
|
|
974
|
+
if (!input || typeof input.hook_event_name !== "string" || !Object.hasOwn(events3, input.hook_event_name)) return null;
|
|
975
|
+
const workspace = Array.isArray(input.workspace_roots) && typeof input.workspace_roots[0] === "string" ? input.workspace_roots[0] : void 0;
|
|
976
|
+
return {
|
|
977
|
+
sessionId: cursorSessionId(workspace),
|
|
978
|
+
event: events3[input.hook_event_name],
|
|
979
|
+
continuation: typeof input.loop_count === "number" && input.loop_count > 0,
|
|
980
|
+
...typeof input.prompt === "string" ? { promptText: input.prompt } : {},
|
|
981
|
+
...workspace !== void 0 ? { workspace } : {}
|
|
982
|
+
};
|
|
983
|
+
} catch {
|
|
984
|
+
return null;
|
|
985
|
+
}
|
|
986
|
+
},
|
|
987
|
+
noop: (kind) => JSON.stringify(kind === "prompt" ? { continue: true } : {}) + "\n",
|
|
988
|
+
render(kind, frame) {
|
|
989
|
+
if (kind === "prompt") return cursorCodec.noop(kind);
|
|
990
|
+
return JSON.stringify(kind === "stop" ? { followup_message: STOP_NUDGE } : { additional_context: frame }) + "\n";
|
|
991
|
+
}
|
|
992
|
+
};
|
|
993
|
+
|
|
994
|
+
// src/harness/cursor.ts
|
|
995
|
+
var install4 = async (flags, deps) => (await import("./chunk-CKLCR5G4.mjs")).runCursorInstall(flags, deps);
|
|
996
|
+
var cursor = {
|
|
997
|
+
id: "cursor",
|
|
998
|
+
sessionSources: [{
|
|
999
|
+
kind: "workspace",
|
|
1000
|
+
resolve: (_meta, env) => cursorSessionId(env[CURSOR_WORKSPACE_ENV]),
|
|
1001
|
+
rejoinable: (sessionId) => sessionId !== CURSOR_DEFAULT_SESSION
|
|
1002
|
+
}],
|
|
1003
|
+
codec: cursorCodec,
|
|
1004
|
+
restoreAtStartup: false,
|
|
1005
|
+
install: install4,
|
|
1006
|
+
uninstall: (flags, deps) => install4([...flags, "--uninstall"], deps)
|
|
1007
|
+
};
|
|
1008
|
+
|
|
1009
|
+
// src/harness/qwen.ts
|
|
1010
|
+
init_define_KHALA_BUNDLE();
|
|
1011
|
+
import path5 from "node:path";
|
|
1012
|
+
var qwenCodec = {
|
|
1013
|
+
...claudeStyleCodec,
|
|
1014
|
+
parse(stdin) {
|
|
1015
|
+
try {
|
|
1016
|
+
if (JSON.parse(stdin)?.agent_id !== void 0) return null;
|
|
1017
|
+
} catch {
|
|
1018
|
+
return null;
|
|
1019
|
+
}
|
|
1020
|
+
return claudeStyleCodec.parse(stdin);
|
|
1021
|
+
}
|
|
1022
|
+
};
|
|
1023
|
+
function createQwenBackgroundDriver(platform = process.platform) {
|
|
1024
|
+
return {
|
|
1025
|
+
id: "background-shell",
|
|
1026
|
+
rung: 2,
|
|
1027
|
+
optIn: false,
|
|
1028
|
+
minIdleMs: 0,
|
|
1029
|
+
deadlineMs: 3e4,
|
|
1030
|
+
verification: "none",
|
|
1031
|
+
async available(ctx) {
|
|
1032
|
+
const settings = await readWakeSettings(stateRoot(ctx.env));
|
|
1033
|
+
return platform === "win32" && !settings.off["qwen/socket"] && !settings.off["qwen/background-shell"] && await qwenInboundAllowed(ctx.env) && await (await import("./chunk-CHC4VZF5.mjs")).monitorArmed(ctx.files);
|
|
1034
|
+
},
|
|
1035
|
+
async unavailableReason(ctx) {
|
|
1036
|
+
if (!await qwenInboundAllowed(ctx.env)) return "qwen_held";
|
|
1037
|
+
return platform === "win32" ? "qwen_watcher_missing" : "driver_missing";
|
|
1038
|
+
},
|
|
1039
|
+
// Qwen itself owns the background tool. Khala never launches or injects input.
|
|
1040
|
+
wake: () => "skipped"
|
|
1041
|
+
};
|
|
1042
|
+
}
|
|
1043
|
+
var qwen = {
|
|
1044
|
+
id: "qwen",
|
|
1045
|
+
codec: qwenCodec,
|
|
1046
|
+
restoreAtStartup: true,
|
|
1047
|
+
sessionSources: [{ kind: "meta", resolve: (meta) => meta?.khala_session, rejoinable: () => true }, { kind: "env", resolve: (_meta, env) => qwenSession(env), rejoinable: () => true }, hookMapSource],
|
|
1048
|
+
install: async (flags, deps) => (await import("./chunk-CKLCR5G4.mjs")).runQwenInstall(flags, deps),
|
|
1049
|
+
uninstall: async (flags, deps) => (await import("./chunk-CKLCR5G4.mjs")).runQwenInstall([...flags, "--uninstall"], deps),
|
|
1050
|
+
wakeLadder: [createQwenSocketDriver(), createQwenBackgroundDriver()],
|
|
1051
|
+
async verifyWake(stdin, files, now) {
|
|
1052
|
+
const input = JSON.parse(stdin);
|
|
1053
|
+
if (typeof input.transcript_path !== "string") return;
|
|
1054
|
+
await writeJsonAtomic(path5.join(files.dir, "qwen-transcript.json"), { path: input.transcript_path });
|
|
1055
|
+
if (input.hook_event_name === "Stop") await verifyQwenTranscript(files.dir, input.transcript_path, now);
|
|
1056
|
+
}
|
|
1057
|
+
};
|
|
1058
|
+
|
|
1059
|
+
// src/harness/gemini.ts
|
|
1060
|
+
init_define_KHALA_BUNDLE();
|
|
1061
|
+
|
|
1062
|
+
// src/harness/codecs/gemini.ts
|
|
1063
|
+
init_define_KHALA_BUNDLE();
|
|
1064
|
+
var events4 = { SessionStart: "start", BeforeAgent: "prompt", AfterTool: "tool", AfterAgent: "stop" };
|
|
1065
|
+
var names2 = { prompt: "BeforeAgent", tool: "AfterTool" };
|
|
1066
|
+
var geminiCodec = {
|
|
1067
|
+
promptDeliversWithoutWake: false,
|
|
1068
|
+
promptAcceptsContext: false,
|
|
1069
|
+
suppressOutputErrors: false,
|
|
1070
|
+
parse(stdin) {
|
|
1071
|
+
try {
|
|
1072
|
+
const input = JSON.parse(stdin.replace(/^\uFEFF/u, ""));
|
|
1073
|
+
if (!input || typeof input.session_id !== "string" || typeof input.hook_event_name !== "string" || !Object.hasOwn(events4, input.hook_event_name)) return null;
|
|
1074
|
+
return {
|
|
1075
|
+
sessionId: input.session_id,
|
|
1076
|
+
event: events4[input.hook_event_name],
|
|
1077
|
+
continuation: false,
|
|
1078
|
+
...typeof input.cwd === "string" ? { workspace: input.cwd } : {},
|
|
1079
|
+
...typeof input.prompt === "string" ? { promptText: input.prompt } : {}
|
|
1080
|
+
};
|
|
1081
|
+
} catch {
|
|
1082
|
+
return null;
|
|
1083
|
+
}
|
|
1084
|
+
},
|
|
1085
|
+
noop: (kind) => JSON.stringify(kind === "stop" ? { decision: "allow" } : {}) + "\n",
|
|
1086
|
+
render(kind, frame) {
|
|
1087
|
+
return JSON.stringify(kind === "stop" ? { decision: "deny", reason: STOP_NUDGE } : { hookSpecificOutput: { hookEventName: names2[kind], additionalContext: frame } }) + "\n";
|
|
1088
|
+
}
|
|
1089
|
+
};
|
|
1090
|
+
|
|
1091
|
+
// src/harness/gemini.ts
|
|
1092
|
+
var emptyPrompt4 = { pattern: /^ >(?: {3}Type your message or @path\/to\/file)?$/u, cursorColumn: 3 };
|
|
1093
|
+
var install5 = async (flags, deps) => (await import("./chunk-CKLCR5G4.mjs")).runGeminiInstall(flags, deps);
|
|
1094
|
+
var gemini = {
|
|
1095
|
+
id: "gemini",
|
|
1096
|
+
sessionSources: [{ kind: "env", resolve: (_meta, env) => env.GEMINI_SESSION_ID, rejoinable: () => true }, hookMapSource],
|
|
1097
|
+
codec: geminiCodec,
|
|
1098
|
+
restoreAtStartup: false,
|
|
1099
|
+
install: install5,
|
|
1100
|
+
uninstall: (flags, deps) => install5([...flags, "--uninstall"], deps),
|
|
1101
|
+
emptyPrompt: emptyPrompt4,
|
|
1102
|
+
wakeLadder: [createTerminalWakeDriver(emptyPrompt4)],
|
|
1103
|
+
wakeWarningName: "gemini"
|
|
1104
|
+
};
|
|
1105
|
+
|
|
1106
|
+
// src/harness/antigravity.ts
|
|
1107
|
+
init_define_KHALA_BUNDLE();
|
|
1108
|
+
|
|
1109
|
+
// src/harness/codecs/antigravity.ts
|
|
1110
|
+
init_define_KHALA_BUNDLE();
|
|
1111
|
+
var antigravityCodec = {
|
|
1112
|
+
promptDeliversWithoutWake: false,
|
|
1113
|
+
promptAcceptsContext: false,
|
|
1114
|
+
suppressOutputErrors: false,
|
|
1115
|
+
parse(stdin) {
|
|
1116
|
+
try {
|
|
1117
|
+
const input = JSON.parse(stdin.replace(/^\uFEFF/u, ""));
|
|
1118
|
+
if (!input || typeof input.conversationId !== "string") return null;
|
|
1119
|
+
let event;
|
|
1120
|
+
if (input.khalaHookEvent === "PreInvocation" && Number.isInteger(input.invocationNum) && input.invocationNum >= 0)
|
|
1121
|
+
event = input.invocationNum === 0 ? "prompt" : "tool";
|
|
1122
|
+
else if (input.khalaHookEvent === "Stop") event = "stop";
|
|
1123
|
+
else return null;
|
|
1124
|
+
return {
|
|
1125
|
+
sessionId: input.conversationId,
|
|
1126
|
+
event,
|
|
1127
|
+
continuation: false,
|
|
1128
|
+
...typeof input.transcriptPath === "string" ? { transcriptPath: input.transcriptPath } : {},
|
|
1129
|
+
...typeof input.workspacePaths?.[0] === "string" ? { workspace: input.workspacePaths[0] } : {}
|
|
1130
|
+
};
|
|
1131
|
+
} catch {
|
|
1132
|
+
return null;
|
|
1133
|
+
}
|
|
1134
|
+
},
|
|
1135
|
+
noop: (kind) => JSON.stringify(kind === "stop" ? { decision: "stop" } : {}) + "\n",
|
|
1136
|
+
render: (kind, frame) => JSON.stringify(kind === "stop" ? { decision: "continue", reason: STOP_NUDGE } : { injectSteps: [{ ephemeralMessage: frame }] }) + "\n"
|
|
1137
|
+
};
|
|
1138
|
+
|
|
1139
|
+
// src/wake/antigravity.ts
|
|
1140
|
+
init_define_KHALA_BUNDLE();
|
|
1141
|
+
import * as fs from "node:fs/promises";
|
|
1142
|
+
import { constants as constants2 } from "node:fs";
|
|
1143
|
+
import path6 from "node:path";
|
|
1144
|
+
import { execFile as execFile3 } from "node:child_process";
|
|
1145
|
+
var file = "antigravity-wake.json";
|
|
1146
|
+
var localAddress = (value) => typeof value === "string" && /^(?:localhost|127\.0\.0\.1|\[::1\]):([0-9]{1,5})$/u.test(value) && Number(value.slice(value.lastIndexOf(":") + 1)) > 0 && Number(value.slice(value.lastIndexOf(":") + 1)) <= 65535;
|
|
1147
|
+
async function credentials(files, sessionId) {
|
|
1148
|
+
let value;
|
|
1149
|
+
try {
|
|
1150
|
+
await ensureStateDir(files.dir);
|
|
1151
|
+
const handle = await fs.open(path6.join(files.dir, file), constants2.O_RDONLY | constants2.O_NONBLOCK | constants2.O_NOFOLLOW);
|
|
1152
|
+
try {
|
|
1153
|
+
const stat4 = await handle.stat();
|
|
1154
|
+
if (!stat4.isFile() || stat4.size > 16 * 1024 || process.platform !== "win32" && (stat4.mode & 63) !== 0 || typeof process.getuid === "function" && stat4.uid !== process.getuid()) return null;
|
|
1155
|
+
value = JSON.parse(await handle.readFile("utf8"));
|
|
1156
|
+
} finally {
|
|
1157
|
+
await handle.close();
|
|
1158
|
+
}
|
|
1159
|
+
} catch {
|
|
1160
|
+
return null;
|
|
1161
|
+
}
|
|
1162
|
+
return value && value.sessionId === sessionId && localAddress(value.address) && typeof value.token === "string" && value.token.length > 0 && !/[\r\n\0]/u.test(value.token) && typeof value.command === "string" && (path6.isAbsolute(value.command) || value.command === "agy") ? value : null;
|
|
1163
|
+
}
|
|
1164
|
+
async function stableAgyCommand(env) {
|
|
1165
|
+
for (const dir of (env.PATH ?? "").split(path6.delimiter).filter(Boolean)) {
|
|
1166
|
+
for (const name of process.platform === "win32" ? ["agy.exe", "agy.cmd", "agy"] : ["agy"]) {
|
|
1167
|
+
const candidate = path6.resolve(dir, name);
|
|
1168
|
+
try {
|
|
1169
|
+
await fs.access(candidate, constants2.X_OK);
|
|
1170
|
+
if ((await fs.stat(candidate)).isFile()) return candidate;
|
|
1171
|
+
} catch {
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
return env.ANTIGRAVITY_AGENTAPI_EXE || "agy";
|
|
1176
|
+
}
|
|
1177
|
+
async function registerAntigravityWake(env) {
|
|
1178
|
+
const sessionId = env.ANTIGRAVITY_CONVERSATION_ID;
|
|
1179
|
+
const address = env.ANTIGRAVITY_LS_ADDRESS;
|
|
1180
|
+
const token = env.ANTIGRAVITY_CSRF_TOKEN;
|
|
1181
|
+
const command = await stableAgyCommand(env);
|
|
1182
|
+
if (!sessionId || !SESSION_ID_PATTERN.test(sessionId) || !localAddress(address) || !token || /[\r\n\0]/u.test(token) || !(path6.isAbsolute(command) || command === "agy")) throw new Error("antigravity_registration_unavailable");
|
|
1183
|
+
const files = await openSessionDir("antigravity", sessionId, env);
|
|
1184
|
+
await withWakeLock(files.dir, "antigravity-credentials.lock", async () => {
|
|
1185
|
+
await writeJsonAtomic(path6.join(files.dir, file), { sessionId, address, token, command });
|
|
1186
|
+
await resetWakeDriver(files.dir, "antigravity-native");
|
|
1187
|
+
});
|
|
1188
|
+
}
|
|
1189
|
+
var run = (command, argv, env, signal) => new Promise((resolve) => {
|
|
1190
|
+
execFile3(command, [...argv], { env, signal, timeout: 1e4, maxBuffer: 64 * 1024, windowsHide: true }, (error) => resolve(!error));
|
|
1191
|
+
});
|
|
1192
|
+
function createAntigravityWakeDriver(execute2 = run) {
|
|
1193
|
+
return {
|
|
1194
|
+
id: "antigravity-native",
|
|
1195
|
+
rung: 1,
|
|
1196
|
+
optIn: true,
|
|
1197
|
+
minIdleMs: 0,
|
|
1198
|
+
deadlineMs: 3e4,
|
|
1199
|
+
verification: "nonce",
|
|
1200
|
+
async available(ctx) {
|
|
1201
|
+
const value = await credentials(ctx.files, ctx.sessionId);
|
|
1202
|
+
return !!value && !value.rejected;
|
|
1203
|
+
},
|
|
1204
|
+
async unavailableReason(ctx) {
|
|
1205
|
+
const value = await credentials(ctx.files, ctx.sessionId);
|
|
1206
|
+
return value?.rejected ? "antigravity_credentials_rejected" : "antigravity_credentials_missing";
|
|
1207
|
+
},
|
|
1208
|
+
async wake(ctx, line) {
|
|
1209
|
+
if (!line.startsWith(CODEX_IDLE_WAKE_NOTICE + " ") || !/^\(k-[0-9a-f]{8}\)$/u.test(line.slice(CODEX_IDLE_WAKE_NOTICE.length + 1))) throw new Error("invalid_wake_line");
|
|
1210
|
+
const value = await credentials(ctx.files, ctx.sessionId);
|
|
1211
|
+
if (!value || value.rejected || ctx.signal.aborted || (await readActivity(ctx.files)).state !== "idle") return "skipped";
|
|
1212
|
+
let accepted = false;
|
|
1213
|
+
try {
|
|
1214
|
+
accepted = await execute2(
|
|
1215
|
+
value.command,
|
|
1216
|
+
["agentapi", "send-message", ctx.sessionId, line],
|
|
1217
|
+
{ ...ctx.env, ANTIGRAVITY_LS_ADDRESS: value.address, ANTIGRAVITY_CSRF_TOKEN: value.token },
|
|
1218
|
+
ctx.signal
|
|
1219
|
+
);
|
|
1220
|
+
} catch {
|
|
1221
|
+
}
|
|
1222
|
+
if (!accepted && !ctx.signal.aborted) {
|
|
1223
|
+
await withWakeLock(ctx.files.dir, "antigravity-credentials.lock", async () => {
|
|
1224
|
+
const current = await credentials(ctx.files, ctx.sessionId);
|
|
1225
|
+
if (current?.token === value.token && current.address === value.address) await writeJsonAtomic(path6.join(ctx.files.dir, file), { ...value, rejected: true });
|
|
1226
|
+
});
|
|
1227
|
+
return "skipped";
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
};
|
|
1231
|
+
}
|
|
1232
|
+
async function antigravityPromptText(input) {
|
|
1233
|
+
if (!input.transcriptPath) return void 0;
|
|
1234
|
+
let handle;
|
|
1235
|
+
try {
|
|
1236
|
+
handle = await fs.open(input.transcriptPath, constants2.O_RDONLY | constants2.O_NONBLOCK);
|
|
1237
|
+
const stat4 = await handle.stat();
|
|
1238
|
+
if (!stat4.isFile()) return void 0;
|
|
1239
|
+
const length = Math.min(stat4.size, 64 * 1024), start = stat4.size - length;
|
|
1240
|
+
const buffer = Buffer.alloc(length);
|
|
1241
|
+
const { bytesRead } = await handle.read(buffer, 0, length, start);
|
|
1242
|
+
let tail = buffer.subarray(0, bytesRead).toString("utf8");
|
|
1243
|
+
if (start > 0) tail = tail.slice(tail.indexOf("\n") + 1);
|
|
1244
|
+
return tail.split("\n").flatMap((line) => {
|
|
1245
|
+
try {
|
|
1246
|
+
const step = JSON.parse(line);
|
|
1247
|
+
if (typeof step.content !== "string") return [];
|
|
1248
|
+
if (step.source === "USER_EXPLICIT" && step.type === "USER_INPUT") {
|
|
1249
|
+
const nonce = step.content.match(/\(k-([0-9a-f]{8})\)$/u)?.[1];
|
|
1250
|
+
return nonce && wakeLine(nonce) === step.content ? [step.content] : [];
|
|
1251
|
+
}
|
|
1252
|
+
if (step.source !== "SYSTEM" || step.type !== "SYSTEM_MESSAGE") return [];
|
|
1253
|
+
return step.content.split(CODEX_IDLE_WAKE_NOTICE + " ").slice(1).flatMap((suffix) => {
|
|
1254
|
+
const nonce = suffix.match(/^\(k-[0-9a-f]{8}\)/u)?.[0];
|
|
1255
|
+
return nonce ? [CODEX_IDLE_WAKE_NOTICE + " " + nonce] : [];
|
|
1256
|
+
});
|
|
1257
|
+
} catch {
|
|
1258
|
+
return [];
|
|
1259
|
+
}
|
|
1260
|
+
}).join("\n") || void 0;
|
|
1261
|
+
} catch {
|
|
1262
|
+
return void 0;
|
|
1263
|
+
} finally {
|
|
1264
|
+
await handle?.close().catch(() => {
|
|
1265
|
+
});
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
// src/harness/antigravity.ts
|
|
1270
|
+
var emptyPrompt5 = { pattern: /^>$/u, cursorColumn: 2 };
|
|
1271
|
+
var install6 = async (flags, deps) => (await import("./chunk-CKLCR5G4.mjs")).runAntigravityInstall(flags, deps);
|
|
1272
|
+
var antigravity = {
|
|
1273
|
+
id: "antigravity",
|
|
1274
|
+
restoreAtStartup: true,
|
|
1275
|
+
codec: antigravityCodec,
|
|
1276
|
+
sessionSources: [
|
|
1277
|
+
{ kind: "meta", resolve: (meta) => meta?.["antigravity.google/conversation_id"], rejoinable: () => true },
|
|
1278
|
+
{ kind: "env", resolve: (_meta, env) => env.ANTIGRAVITY_CONVERSATION_ID, rejoinable: () => true },
|
|
1279
|
+
{
|
|
1280
|
+
kind: "process",
|
|
1281
|
+
rejoinable: () => true,
|
|
1282
|
+
async resolve(_meta, _env, context) {
|
|
1283
|
+
const read = context.readProcess ?? readProcess;
|
|
1284
|
+
for await (const parent of walkAncestors(context.pid ?? process.pid, read)) {
|
|
1285
|
+
if (!/^agy(?:\.exe)?$/iu.test(parent.command.split(/[\\/]/u).pop() ?? "")) continue;
|
|
1286
|
+
const args = await (context.readArguments ?? readProcessArguments)(parent.pid);
|
|
1287
|
+
if (!args || (await read(parent.pid))?.startTime !== parent.startTime) return null;
|
|
1288
|
+
for (let i = 1; i < args.length; i++) {
|
|
1289
|
+
if (args[i] === "--") break;
|
|
1290
|
+
if (args[i] === "--conversation") return args[i + 1] ?? "";
|
|
1291
|
+
if (args[i]?.startsWith("--conversation=")) return args[i].slice("--conversation=".length);
|
|
1292
|
+
}
|
|
1293
|
+
return null;
|
|
1294
|
+
}
|
|
1295
|
+
return null;
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
],
|
|
1299
|
+
install: install6,
|
|
1300
|
+
uninstall: (flags, deps) => install6([...flags, "--uninstall"], deps),
|
|
1301
|
+
emptyPrompt: emptyPrompt5,
|
|
1302
|
+
wakeLadder: [createAntigravityWakeDriver(), createTerminalWakeDriver(emptyPrompt5)],
|
|
1303
|
+
wakeWarningName: "antigravity",
|
|
1304
|
+
hookPromptText: antigravityPromptText
|
|
1305
|
+
};
|
|
1306
|
+
|
|
1307
|
+
// src/harness/opencode.ts
|
|
1308
|
+
init_define_KHALA_BUNDLE();
|
|
1309
|
+
|
|
1310
|
+
// src/harness/codecs/opencode.ts
|
|
1311
|
+
init_define_KHALA_BUNDLE();
|
|
1312
|
+
var events5 = { "session-start": "start", prompt: "prompt", "post-tool": "tool", "turn-end": "stop", idle: "stop" };
|
|
1313
|
+
var opencodeCodec = {
|
|
1314
|
+
promptAcceptsContext: true,
|
|
1315
|
+
promptDeliversWithoutWake: true,
|
|
1316
|
+
suppressOutputErrors: false,
|
|
1317
|
+
parse(stdin) {
|
|
1318
|
+
try {
|
|
1319
|
+
const input = JSON.parse(stdin.replace(/^\uFEFF/u, ""));
|
|
1320
|
+
if (!input || typeof input.session_id !== "string" || typeof input.event !== "string" || !Object.hasOwn(events5, input.event)) return null;
|
|
1321
|
+
return {
|
|
1322
|
+
sessionId: input.session_id,
|
|
1323
|
+
event: events5[input.event],
|
|
1324
|
+
continuation: input.continuation === true,
|
|
1325
|
+
...input.replay === true ? { replay: true } : {},
|
|
1326
|
+
...typeof input.prompt === "string" ? { promptText: input.prompt } : {}
|
|
1327
|
+
};
|
|
1328
|
+
} catch {
|
|
1329
|
+
return null;
|
|
1330
|
+
}
|
|
1331
|
+
},
|
|
1332
|
+
noop: () => "",
|
|
1333
|
+
render: (kind, frame) => kind === "stop" ? STOP_NUDGE : frame
|
|
1334
|
+
};
|
|
1335
|
+
|
|
1336
|
+
// src/wake/opencode.ts
|
|
1337
|
+
init_define_KHALA_BUNDLE();
|
|
1338
|
+
import path7 from "node:path";
|
|
1339
|
+
var file2 = "opencode-wake.json";
|
|
1340
|
+
async function budget(files) {
|
|
1341
|
+
const pairs = await Promise.all((await listChannels(files)).map(async (channel) => [channel.key, (await readCursor(channel.files)).deliveredCount]));
|
|
1342
|
+
return JSON.stringify(pairs.sort(([a], [b]) => a.localeCompare(b)));
|
|
1343
|
+
}
|
|
1344
|
+
var canAttempt = (prior, key, now) => !prior || prior.budget !== key || now - prior.at >= 6e4 && prior.attempts < 2;
|
|
1345
|
+
var opencodeWakeDriver = {
|
|
1346
|
+
id: "opencode-native",
|
|
1347
|
+
rung: 1,
|
|
1348
|
+
optIn: false,
|
|
1349
|
+
minIdleMs: 0,
|
|
1350
|
+
deadlineMs: 3e3,
|
|
1351
|
+
verification: "nonce",
|
|
1352
|
+
async available(ctx) {
|
|
1353
|
+
const prior = await readJson(path7.join(ctx.files.dir, file2));
|
|
1354
|
+
return canAttempt(prior, await budget(ctx.files), ctx.now);
|
|
1355
|
+
},
|
|
1356
|
+
async wake(ctx, line) {
|
|
1357
|
+
return withWakeLock(ctx.files.dir, "opencode-native.lock", async () => {
|
|
1358
|
+
if (ctx.signal.aborted || (await readActivity(ctx.files)).state !== "idle") return "skipped";
|
|
1359
|
+
const key = await budget(ctx.files);
|
|
1360
|
+
const prior = await readJson(path7.join(ctx.files.dir, file2));
|
|
1361
|
+
if (!canAttempt(prior, key, ctx.now)) return "skipped";
|
|
1362
|
+
const channels = [];
|
|
1363
|
+
for (const channel of await listChannels(ctx.files)) {
|
|
1364
|
+
if (await readListeningMode(channel.files) !== "async") channels.push(channel.key);
|
|
1365
|
+
}
|
|
1366
|
+
await writeJsonAtomic(path7.join(ctx.files.dir, file2), {
|
|
1367
|
+
line,
|
|
1368
|
+
at: ctx.now,
|
|
1369
|
+
budget: key,
|
|
1370
|
+
attempts: prior?.budget === key ? prior.attempts + 1 : 1,
|
|
1371
|
+
delivered: false,
|
|
1372
|
+
channels
|
|
1373
|
+
});
|
|
1374
|
+
});
|
|
1375
|
+
}
|
|
1376
|
+
};
|
|
1377
|
+
async function pollOpenCodeWake(files, io, replay = false) {
|
|
1378
|
+
if (replay) return withWakeLock(files.dir, "opencode-native.lock", async () => {
|
|
1379
|
+
const pending = await readJson(path7.join(files.dir, file2));
|
|
1380
|
+
const activity = await readActivity(files);
|
|
1381
|
+
await settleAttempts(files.dir, { now: io.now().getTime(), activity });
|
|
1382
|
+
if (!pending || activity.state !== "idle" || !await driverAllowed(stateRoot(io.env), "opencode", opencodeWakeDriver.id, false) || (await readWakeState(files.dir))[opencodeWakeDriver.id]?.disabled) return;
|
|
1383
|
+
const channels = (await listChannels(files)).filter((channel) => !pending.channels || pending.channels.includes(channel.key));
|
|
1384
|
+
if (!channels.length) return;
|
|
1385
|
+
for (const channel of channels) if (await readListeningMode(channel.files) === "async") return;
|
|
1386
|
+
const nonce = newNonce(), at = io.now().getTime();
|
|
1387
|
+
await recordAttempt(files.dir, {
|
|
1388
|
+
nonce,
|
|
1389
|
+
driver: opencodeWakeDriver.id,
|
|
1390
|
+
at,
|
|
1391
|
+
deadline: at + opencodeWakeDriver.deadlineMs,
|
|
1392
|
+
activityUpdatedAt: activity.updatedAt
|
|
1393
|
+
});
|
|
1394
|
+
const line = wakeLine(nonce);
|
|
1395
|
+
await writeJsonAtomic(path7.join(files.dir, file2), { ...pending, line, at });
|
|
1396
|
+
return line;
|
|
1397
|
+
});
|
|
1398
|
+
const ladder = createWakeLadder({
|
|
1399
|
+
files,
|
|
1400
|
+
harness: "opencode",
|
|
1401
|
+
sessionId: path7.basename(files.dir),
|
|
1402
|
+
drivers: [opencodeWakeDriver],
|
|
1403
|
+
env: io.env,
|
|
1404
|
+
now: () => io.now().getTime(),
|
|
1405
|
+
stderr: (text) => io.stderr.write(text)
|
|
1406
|
+
});
|
|
1407
|
+
try {
|
|
1408
|
+
await ladder.poll();
|
|
1409
|
+
} finally {
|
|
1410
|
+
await ladder.stop();
|
|
1411
|
+
}
|
|
1412
|
+
return withWakeLock(files.dir, "opencode-native.lock", async () => {
|
|
1413
|
+
const pending = await readJson(path7.join(files.dir, file2));
|
|
1414
|
+
if (!pending || pending.delivered || io.now().getTime() > pending.at + opencodeWakeDriver.deadlineMs || pending.budget !== await budget(files) || (await readActivity(files)).state !== "idle" || !await driverAllowed(stateRoot(io.env), "opencode", opencodeWakeDriver.id, false) || (await readWakeState(files.dir))[opencodeWakeDriver.id]?.disabled) return;
|
|
1415
|
+
let eligible = false;
|
|
1416
|
+
for (const channel of await listChannels(files)) {
|
|
1417
|
+
const mode = await readListeningMode(channel.files);
|
|
1418
|
+
if (mode === "async") continue;
|
|
1419
|
+
const session = await readJson(channel.files.session);
|
|
1420
|
+
const you = (await readJson(channel.files.status))?.displayName;
|
|
1421
|
+
if ((await unread(channel.files)).entries.some((entry) => entry.sender !== session?.userId && wakesAgent(entry, mode, you))) eligible = true;
|
|
1422
|
+
}
|
|
1423
|
+
if (!eligible) return;
|
|
1424
|
+
await writeJsonAtomic(path7.join(files.dir, file2), { ...pending, delivered: true });
|
|
1425
|
+
return pending.line;
|
|
1426
|
+
});
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
// src/harness/opencode.ts
|
|
1430
|
+
var install7 = async (flags, deps) => (await import("./chunk-CKLCR5G4.mjs")).runOpenCodeInstall(flags, deps);
|
|
1431
|
+
var opencode = {
|
|
1432
|
+
id: "opencode",
|
|
1433
|
+
sessionSources: [{ kind: "meta", resolve: (meta) => meta?.khala_session, rejoinable: () => true }, hookMapSource],
|
|
1434
|
+
codec: opencodeCodec,
|
|
1435
|
+
restoreAtStartup: true,
|
|
1436
|
+
wakeLadder: [opencodeWakeDriver],
|
|
1437
|
+
pollIdleWake: pollOpenCodeWake,
|
|
1438
|
+
install: install7,
|
|
1439
|
+
uninstall: (flags, deps) => install7([...flags, "--uninstall"], deps)
|
|
1440
|
+
};
|
|
1441
|
+
|
|
1442
|
+
// src/harness/generic.ts
|
|
1443
|
+
init_define_KHALA_BUNDLE();
|
|
1444
|
+
var generic = {
|
|
1445
|
+
id: "generic",
|
|
1446
|
+
sessionSources: [
|
|
1447
|
+
{ kind: "env", resolve: (_meta, env) => env.KHALA_SESSION_ID, rejoinable: () => true },
|
|
1448
|
+
processSource
|
|
1449
|
+
],
|
|
1450
|
+
codec: void 0,
|
|
1451
|
+
restoreAtStartup: true
|
|
1452
|
+
};
|
|
1453
|
+
function genericAdapter(id) {
|
|
1454
|
+
return { ...generic, id };
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
// src/harness/index.ts
|
|
1458
|
+
function createAdapterRegistry(adapters) {
|
|
1459
|
+
const byId = /* @__PURE__ */ new Map();
|
|
1460
|
+
for (const adapter of adapters) {
|
|
1461
|
+
if (byId.has(adapter.id)) throw new Error(`duplicate_adapter: ${adapter.id}`);
|
|
1462
|
+
if (!HARNESS_REGISTRY.some((row) => row.id === adapter.id)) throw new Error(`unregistered_adapter: ${adapter.id}`);
|
|
1463
|
+
byId.set(adapter.id, adapter);
|
|
1464
|
+
}
|
|
1465
|
+
return Object.freeze({ resolve: (id) => byId.get(id) });
|
|
1466
|
+
}
|
|
1467
|
+
var ADAPTERS = Object.freeze([claude, codex, cursor, opencode, muse, gemini, antigravity, copilot, qwen, generic]);
|
|
1468
|
+
var registry = createAdapterRegistry(ADAPTERS);
|
|
1469
|
+
function adapterFor(id) {
|
|
1470
|
+
return registry.resolve(id) ?? (isHarnessId(id) ? genericAdapter(id) : void 0);
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
export {
|
|
1474
|
+
createCopilotCodec,
|
|
1475
|
+
createWakeLadder,
|
|
1476
|
+
registerAntigravityWake,
|
|
1477
|
+
SESSION_UNKNOWN_HINT,
|
|
1478
|
+
recordHookSession,
|
|
1479
|
+
resolveSources,
|
|
1480
|
+
createAdapterRegistry,
|
|
1481
|
+
ADAPTERS,
|
|
1482
|
+
adapterFor
|
|
1483
|
+
};
|