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.
Files changed (53) hide show
  1. package/README.md +4 -4
  2. package/dist/THIRD_PARTY_NOTICES.txt +809 -0
  3. package/dist/chunk-2AMWQ7RB.mjs +1127 -0
  4. package/dist/chunk-3E5DH3Y6.mjs +9864 -0
  5. package/dist/{chunk-IQJQ4RF5.mjs → chunk-4BP7V3OP.mjs} +12 -1
  6. package/dist/{chunk-FV32AQTC.mjs → chunk-4BZQ4GVW.mjs} +10 -6
  7. package/dist/chunk-5JSGA6OB.mjs +1483 -0
  8. package/dist/chunk-6AQVW5W7.mjs +60 -0
  9. package/dist/{chunk-W6R4UJUQ.mjs → chunk-6J2PUM7L.mjs} +16 -9
  10. package/dist/chunk-6SUV3EW6.mjs +27 -0
  11. package/dist/{chunk-RNDUFTBF.mjs → chunk-ARIDZOTI.mjs} +499 -115
  12. package/dist/chunk-C2WEDMAF.mjs +15 -0
  13. package/dist/{chunk-Z4JF2M5C.mjs → chunk-CHC4VZF5.mjs} +9 -9
  14. package/dist/chunk-CKLCR5G4.mjs +1369 -0
  15. package/dist/chunk-EVT2DWNZ.mjs +58 -0
  16. package/dist/chunk-FJVVSKZ3.mjs +22 -0
  17. package/dist/{chunk-APMWDF6B.mjs → chunk-G3NMB4PY.mjs} +4 -2
  18. package/dist/chunk-HHEZVPRR.mjs +7 -0
  19. package/dist/{chunk-54XFVSEC.mjs → chunk-JKOJQTFT.mjs} +103 -5
  20. package/dist/{chunk-ZJ4W7P33.mjs → chunk-NDMFD5IM.mjs} +15 -10
  21. package/dist/{chunk-VKQ3KUYO.mjs → chunk-OEV5DFLW.mjs} +77 -7
  22. package/dist/chunk-OIZ4R5H7.mjs +173 -0
  23. package/dist/{chunk-CJTCU755.mjs → chunk-PJKI7SRI.mjs} +256 -26
  24. package/dist/chunk-PN25BUQ4.mjs +599 -0
  25. package/dist/chunk-QDPJ4VT2.mjs +432 -0
  26. package/dist/{chunk-2DHIAFEV.mjs → chunk-SRZTHCXP.mjs} +24 -10
  27. package/dist/chunk-TSK3HJEG.mjs +20 -0
  28. package/dist/chunk-UX34IVN5.mjs +33 -0
  29. package/dist/{chunk-BKLW2SPG.mjs → chunk-VAC3JTAV.mjs} +34 -17
  30. package/dist/{chunk-VAI7IUI4.mjs → chunk-VDZK4LW3.mjs} +30 -12
  31. package/dist/chunk-W6HM6F2U.mjs +901 -0
  32. package/dist/{chunk-H3K4GA2P.mjs → chunk-WPNMWNCL.mjs} +33 -21
  33. package/dist/chunk-WUDXHF2Q.mjs +14 -0
  34. package/dist/chunk-WZJ6DQJV.mjs +19 -0
  35. package/dist/{chunk-5LIE5GUU.mjs → chunk-XR625T3I.mjs} +5 -1
  36. package/dist/chunk-ZSKLNZWZ.mjs +224 -0
  37. package/dist/iterm2_send.py +56 -0
  38. package/dist/khala.mjs +20 -13
  39. package/dist/web/assets/local-CSPuGnH2.css +1 -0
  40. package/dist/web/assets/local-Dtyxaacx.js +9 -0
  41. package/dist/web/index.html +2 -2
  42. package/package.json +5 -3
  43. package/dist/chunk-2VCAW4GH.mjs +0 -34
  44. package/dist/chunk-5BQTYU66.mjs +0 -886
  45. package/dist/chunk-5GLTKWJ5.mjs +0 -1
  46. package/dist/chunk-5PUQT3YS.mjs +0 -569
  47. package/dist/chunk-7HBRFY3Z.mjs +0 -8
  48. package/dist/chunk-CBPLCCRZ.mjs +0 -191
  49. package/dist/chunk-FH34B3I3.mjs +0 -235
  50. package/dist/chunk-NKX3SLS7.mjs +0 -227
  51. package/dist/chunk-VUSV2ADL.mjs +0 -267
  52. package/dist/web/assets/local-BQl2KrE1.js +0 -9
  53. package/dist/web/assets/local-OwDq47O4.css +0 -1
@@ -0,0 +1,1127 @@
1
+ import { createRequire as __khalaCreateRequire } from 'node:module'; const require = __khalaCreateRequire(import.meta.url);
2
+ import {
3
+ DEFAULT_LISTENING_MODE,
4
+ decodeListeningMode
5
+ } from "./chunk-4BP7V3OP.mjs";
6
+ import {
7
+ advanceCursor,
8
+ isWakeEntry,
9
+ listChannels,
10
+ mentions,
11
+ readActivity,
12
+ readCursor,
13
+ readEntries,
14
+ unread,
15
+ wakesAgent
16
+ } from "./chunk-OEV5DFLW.mjs";
17
+ import {
18
+ SESSION_ID_PATTERN,
19
+ StateError,
20
+ ensureStateDir,
21
+ readJson,
22
+ readWatcherStatus,
23
+ sessionFiles,
24
+ stateRoot,
25
+ writeJsonAtomic
26
+ } from "./chunk-PN25BUQ4.mjs";
27
+ import {
28
+ cancelAttempt,
29
+ confirmWakeText,
30
+ failAttempt,
31
+ readWakeState,
32
+ settleAttempts,
33
+ wakeLine,
34
+ withWakeLock
35
+ } from "./chunk-ZSKLNZWZ.mjs";
36
+ import {
37
+ init_define_KHALA_BUNDLE
38
+ } from "./chunk-EVT2DWNZ.mjs";
39
+
40
+ // src/watch.ts
41
+ init_define_KHALA_BUNDLE();
42
+ import * as fs7 from "node:fs/promises";
43
+ import { watch as watchDirectory } from "node:fs";
44
+ import path6 from "node:path";
45
+ import { randomUUID as randomUUID3 } from "node:crypto";
46
+
47
+ // src/monitor-storage.ts
48
+ init_define_KHALA_BUNDLE();
49
+ import * as fs from "node:fs/promises";
50
+ import { createHash, randomUUID } from "node:crypto";
51
+ import os from "node:os";
52
+ import path from "node:path";
53
+ function monitorStorageCandidates(files) {
54
+ const hash = createHash("sha256").update(path.resolve(files.dir)).digest("hex");
55
+ const uid = process.getuid?.() ?? os.userInfo().username;
56
+ return [files.dir, path.join(process.platform === "win32" ? os.tmpdir() : "/tmp", `khala-monitor-${uid}-${hash}`)];
57
+ }
58
+ async function monitorStorage(files) {
59
+ const probe = path.join(files.dir, `.monitor-probe-${randomUUID()}`);
60
+ try {
61
+ const handle = await fs.open(probe, "wx", 384);
62
+ await handle.close();
63
+ await fs.unlink(probe);
64
+ return files.dir;
65
+ } catch (error) {
66
+ if (!["EACCES", "EPERM", "EROFS"].includes(error.code ?? "")) throw error;
67
+ }
68
+ const dir = monitorStorageCandidates(files)[1];
69
+ await fs.mkdir(dir, { mode: 448 }).catch((error) => {
70
+ if (error.code !== "EEXIST") throw error;
71
+ });
72
+ const stat3 = await fs.lstat(dir);
73
+ if (!stat3.isDirectory() || stat3.isSymbolicLink() || process.getuid && stat3.uid !== process.getuid() || process.platform !== "win32" && (stat3.mode & 63) !== 0) throw new Error("unsafe_monitor_directory");
74
+ return dir;
75
+ }
76
+
77
+ // src/mode.ts
78
+ init_define_KHALA_BUNDLE();
79
+ async function readListeningMode(files) {
80
+ const content = await readJson(files.mode);
81
+ const decoded = decodeListeningMode(typeof content === "object" && content !== null ? content.mode : void 0);
82
+ return decoded.ok ? decoded.value : DEFAULT_LISTENING_MODE;
83
+ }
84
+ async function applyListeningMode(files, next, meta, now = () => /* @__PURE__ */ new Date()) {
85
+ const previous = await readListeningMode(files);
86
+ if (previous === "async" && next !== "async") {
87
+ for (let attempt = 0; attempt < 2; attempt++) {
88
+ const { cursor, entries } = await unread(files);
89
+ if (await advanceCursor(files, cursor, entries) !== "conflict") break;
90
+ if (attempt === 1) throw new StateError("storage_failed");
91
+ }
92
+ }
93
+ await writeJsonAtomic(files.mode, { mode: next, ...meta, updatedAt: now().toISOString() });
94
+ return { previous, next };
95
+ }
96
+
97
+ // src/wake/shared/index.ts
98
+ init_define_KHALA_BUNDLE();
99
+
100
+ // src/wake/shared/settings.ts
101
+ init_define_KHALA_BUNDLE();
102
+ import * as fs2 from "node:fs/promises";
103
+ import path2 from "node:path";
104
+ import { randomBytes } from "node:crypto";
105
+ async function readWakeSettings(root) {
106
+ try {
107
+ const value = JSON.parse(await fs2.readFile(path2.join(root, "wake-settings.json"), "utf8"));
108
+ const entries = (input) => {
109
+ if (!input || typeof input !== "object" || Array.isArray(input)) return {};
110
+ return Object.fromEntries(Object.entries(input).filter(([, entry]) => entry && typeof entry === "object" && typeof entry.at === "string"));
111
+ };
112
+ return { consent: entries(value?.consent), off: entries(value?.off) };
113
+ } catch (error) {
114
+ if (error.code === "ENOENT") return { consent: {}, off: {} };
115
+ throw error;
116
+ }
117
+ }
118
+ async function writeWakeSettings(root, settings) {
119
+ await fs2.mkdir(root, { recursive: true, mode: 448 });
120
+ const target2 = path2.join(root, "wake-settings.json");
121
+ const temp = `${target2}.${randomBytes(8).toString("hex")}.tmp`;
122
+ try {
123
+ await fs2.writeFile(temp, JSON.stringify(settings), { mode: 384 });
124
+ await fs2.rename(temp, target2);
125
+ } finally {
126
+ await fs2.rm(temp, { force: true });
127
+ }
128
+ }
129
+ async function updateWakeSettings(root, update) {
130
+ await withWakeLock(root, "wake-settings.lock", async () => {
131
+ const settings = await readWakeSettings(root);
132
+ update(settings);
133
+ await writeWakeSettings(root, settings);
134
+ });
135
+ }
136
+ async function driverAllowed(root, harness, driver, optIn) {
137
+ const settings = await readWakeSettings(root);
138
+ const key = `${harness}/${driver}`;
139
+ return !Object.hasOwn(settings.off, key) && (!optIn || Object.hasOwn(settings.consent, key));
140
+ }
141
+
142
+ // src/wake/muse-monitor.ts
143
+ init_define_KHALA_BUNDLE();
144
+ import { createInterface } from "node:readline";
145
+ import * as fs4 from "node:fs/promises";
146
+ import os2 from "node:os";
147
+ import path4 from "node:path";
148
+
149
+ // src/install/cursor.ts
150
+ init_define_KHALA_BUNDLE();
151
+
152
+ // src/cursor.ts
153
+ init_define_KHALA_BUNDLE();
154
+ import { createHash as createHash2 } from "node:crypto";
155
+ var CURSOR_WORKSPACE_ENV = "KHALA_CURSOR_WORKSPACE";
156
+ var CURSOR_DEFAULT_SESSION = "cursor-default";
157
+ function normalizeWorkspace(root) {
158
+ let value = root.trim();
159
+ if (!value || value.includes("${")) return null;
160
+ if (/^file:\/\//iu.test(value)) {
161
+ try {
162
+ value = decodeURIComponent(new URL(value).pathname);
163
+ } catch {
164
+ return null;
165
+ }
166
+ }
167
+ value = value.replaceAll("\\", "/");
168
+ if (/^\/[A-Za-z]:(?:\/|$)/u.test(value)) value = value.slice(1);
169
+ if (/^[A-Za-z]:(?:\/|$)/u.test(value)) value = value.toLowerCase();
170
+ value = value.replace(/\/+$/u, "");
171
+ if (/^[a-z]:$/u.test(value)) value += "/";
172
+ return value || null;
173
+ }
174
+ function cursorSessionId(root) {
175
+ const normalized = typeof root === "string" ? normalizeWorkspace(root) : null;
176
+ if (normalized === null) return CURSOR_DEFAULT_SESSION;
177
+ return `ws-${createHash2("sha256").update(normalized).digest("hex").slice(0, 32)}`;
178
+ }
179
+
180
+ // src/install/command.ts
181
+ init_define_KHALA_BUNDLE();
182
+ function nodeScriptCommand(platform, node, script) {
183
+ if (platform === "win32") return `node "${script.replaceAll("\\", "/")}"`;
184
+ const quote = (value) => /^[A-Za-z0-9_./:-]+$/u.test(value) ? value : `'${value.replaceAll("'", `'\\''`)}'`;
185
+ return `${quote(node)} ${quote(script)}`;
186
+ }
187
+
188
+ // src/install/launcher.ts
189
+ init_define_KHALA_BUNDLE();
190
+ function installedLauncher(platform, path7, home, env) {
191
+ const windows = platform === "win32";
192
+ const candidate = windows ? env.LOCALAPPDATA : env.XDG_DATA_HOME;
193
+ const dataHome = candidate && path7.isAbsolute(candidate) ? candidate : windows ? path7.join(home, "AppData", "Local") : path7.join(home, ".local", "share");
194
+ const prefix = path7.join(dataHome, "khala", "npm");
195
+ return { prefix, bin: windows ? path7.join(prefix, "khala.cmd") : path7.join(prefix, "bin", "khala") };
196
+ }
197
+ function launcherShellQuote(platform, bin) {
198
+ return platform === "win32" ? `"${bin}"` : `'${bin.replaceAll("'", `'\\''`)}'`;
199
+ }
200
+
201
+ // src/install/managed-file.ts
202
+ init_define_KHALA_BUNDLE();
203
+ import * as fs3 from "node:fs/promises";
204
+ import path3 from "node:path";
205
+ import { isDeepStrictEqual } from "node:util";
206
+ var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
207
+ var BOM = "\uFEFF";
208
+ function formatJson(value, like) {
209
+ const body = like?.startsWith(BOM) ? like.slice(1) : like;
210
+ const indent = body?.match(/^([ \t]+)\S/mu)?.[1] ?? 2;
211
+ const newline = body?.includes("\r\n") ? "\r\n" : "\n";
212
+ const trailing = body === null || body === void 0 || /\n$/u.test(body) || body.trim() === "" ? newline : "";
213
+ return (like?.startsWith(BOM) ? BOM : "") + JSON.stringify(value, null, indent).replaceAll("\n", newline) + trailing;
214
+ }
215
+ function pruneCreated(value, original) {
216
+ if (!isObject(value)) return value;
217
+ const next = {};
218
+ for (const [key, child] of Object.entries(value)) {
219
+ const before = isObject(original) && Object.hasOwn(original, key) ? original[key] : void 0;
220
+ const pruned = pruneCreated(child, before);
221
+ const emptyContainer = Array.isArray(pruned) ? pruned.length === 0 : isObject(pruned) && Object.keys(pruned).length === 0;
222
+ if (emptyContainer && before === void 0) continue;
223
+ next[key] = pruned;
224
+ }
225
+ return next;
226
+ }
227
+ var jsonFormat = {
228
+ parse: (text) => text.trim() ? JSON.parse(text.replace(/^/u, "")) : {},
229
+ serialize: formatJson,
230
+ equal: isDeepStrictEqual,
231
+ empty: (value) => isObject(value) && Object.keys(value).length === 0,
232
+ prune: pruneCreated
233
+ };
234
+ var meaningfulLines = (text) => String(text).split(/\r?\n/u).map((line) => line.trimEnd()).filter(Boolean);
235
+ var textFormat = {
236
+ parse: (text) => text,
237
+ serialize: (value) => String(value),
238
+ equal: (a, b) => isDeepStrictEqual(meaningfulLines(a), meaningfulLines(b)),
239
+ empty: (value) => meaningfulLines(value).length === 0
240
+ };
241
+ async function readManaged(file) {
242
+ try {
243
+ return { file, text: await fs3.readFile(file, "utf8") };
244
+ } catch (error) {
245
+ if (error.code === "ENOENT") return { file, text: null };
246
+ throw error;
247
+ }
248
+ }
249
+ var LEGACY_BACKUP = ".khala-bak";
250
+ var ManagedFiles = class {
251
+ store;
252
+ constructor(stateDir) {
253
+ this.store = path3.join(stateDir, "install-originals.json");
254
+ }
255
+ async load() {
256
+ try {
257
+ const value = JSON.parse(await fs3.readFile(this.store, "utf8"));
258
+ return isObject(value) ? value : {};
259
+ } catch (error) {
260
+ if (error.code === "ENOENT") return {};
261
+ throw error;
262
+ }
263
+ }
264
+ async save(recordings) {
265
+ if (!Object.keys(recordings).length) {
266
+ await fs3.rm(this.store, { force: true });
267
+ return;
268
+ }
269
+ await fs3.mkdir(path3.dirname(this.store), { recursive: true, mode: 448 });
270
+ const temp = `${this.store}.${process.pid}.tmp`;
271
+ await fs3.writeFile(temp, JSON.stringify(recordings, null, 2) + "\n", { mode: 384 });
272
+ await fs3.rename(temp, this.store);
273
+ }
274
+ /** The recorded original: `undefined` when nothing is recorded, `null` when the file did not exist. */
275
+ async original(file) {
276
+ const recording = (await this.load())[path3.resolve(file)];
277
+ if (recording) return recording.existed ? Buffer.from(recording.bytes ?? "", "base64") : null;
278
+ const legacy = await readBytes(file + LEGACY_BACKUP);
279
+ return legacy ?? void 0;
280
+ }
281
+ /**
282
+ * Records the original once, before Khala's first change. An earlier installer's
283
+ * `.khala-bak` is adopted as the original (the file itself then holds Khala's output)
284
+ * and removed. A harness-specific legacy absence sentinel can pass `original: null`;
285
+ * the override still never replaces a previously saved recording.
286
+ */
287
+ async record(file, options) {
288
+ const key = path3.resolve(file);
289
+ const recordings = await this.load();
290
+ if (recordings[key]) return;
291
+ const legacy = await readBytes(file + LEGACY_BACKUP);
292
+ const current = options ? options.original : legacy ?? await readBytes(file);
293
+ recordings[key] = current === null ? { existed: false, createdDirs: await missingDirs(path3.dirname(key)) } : { existed: true, bytes: current.toString("base64"), createdDirs: [] };
294
+ await this.save(recordings);
295
+ if (legacy) await fs3.rm(file + LEGACY_BACKUP, { force: true });
296
+ }
297
+ /**
298
+ * Install: records every file's original first (so each knows which directories Khala
299
+ * creates), then writes each `text` that differs from the current content.
300
+ */
301
+ async write(entries) {
302
+ for (const { current } of entries) await this.record(current.file);
303
+ for (const { current, text, mode } of entries) {
304
+ if (text === current.text) continue;
305
+ await fs3.mkdir(path3.dirname(current.file), { recursive: true });
306
+ await fs3.writeFile(current.file, text, mode === void 0 ? void 0 : { mode });
307
+ }
308
+ }
309
+ /**
310
+ * Uninstall. `strip` receives the parsed current content and the parsed recorded original
311
+ * (`undefined` when the file did not exist or nothing is recorded) and returns the content
312
+ * without Khala's keys. Writes back the recorded bytes when the result is equal to them,
313
+ * deletes the file when it did not exist and nothing is left, and otherwise writes the
314
+ * stripped content. The recording is then dropped.
315
+ */
316
+ async restore(current, format, strip) {
317
+ const key = path3.resolve(current.file);
318
+ const recordings = await this.load();
319
+ const recording = recordings[key];
320
+ const original = await this.original(current.file);
321
+ if (current.text !== null) {
322
+ let originalValue;
323
+ let comparable = original !== void 0;
324
+ if (original) {
325
+ try {
326
+ originalValue = format.parse(original.toString("utf8"));
327
+ } catch {
328
+ comparable = false;
329
+ }
330
+ }
331
+ let value = strip(format.parse(current.text), originalValue);
332
+ if (comparable && format.prune) value = format.prune(value, originalValue);
333
+ if (comparable && original === null && format.empty(value)) {
334
+ await fs3.rm(current.file, { force: true });
335
+ for (const dir of recording?.createdDirs ?? []) await fs3.rmdir(dir).catch(() => void 0);
336
+ } else if (comparable && original && format.equal(value, originalValue)) {
337
+ if (!original.equals(Buffer.from(current.text, "utf8"))) await fs3.writeFile(current.file, original);
338
+ } else {
339
+ const text = format.serialize(value, current.text);
340
+ if (text !== current.text) await fs3.writeFile(current.file, text);
341
+ }
342
+ }
343
+ if (recording) {
344
+ delete recordings[key];
345
+ await this.save(recordings);
346
+ }
347
+ await fs3.rm(current.file + LEGACY_BACKUP, { force: true });
348
+ }
349
+ };
350
+ async function readBytes(file) {
351
+ try {
352
+ return await fs3.readFile(file);
353
+ } catch (error) {
354
+ if (error.code === "ENOENT") return null;
355
+ throw error;
356
+ }
357
+ }
358
+ async function missingDirs(dir) {
359
+ const missing = [];
360
+ for (let at = dir; ; at = path3.dirname(at)) {
361
+ try {
362
+ await fs3.stat(at);
363
+ return missing;
364
+ } catch (error) {
365
+ if (error.code !== "ENOENT") throw error;
366
+ }
367
+ missing.push(at);
368
+ if (path3.dirname(at) === at) return missing;
369
+ }
370
+ }
371
+
372
+ // src/install/cursor.ts
373
+ var CURSOR_HOOK_EVENTS = ["beforeSubmitPrompt", "postToolUse", "stop"];
374
+ var HOOK_SUFFIX = " hook deliver --harness cursor";
375
+ function cursorPaths({ platform, path: path7, home, env }, packageName = "khala-cli") {
376
+ const cursorDir = path7.join(home, ".cursor");
377
+ const windows = platform === "win32";
378
+ const { prefix } = installedLauncher(platform, path7, home, env);
379
+ const modules = windows ? path7.join(prefix, "node_modules") : path7.join(prefix, "lib", "node_modules");
380
+ return {
381
+ cursorDir,
382
+ prefix,
383
+ mcpFile: path7.join(cursorDir, "mcp.json"),
384
+ hooksFile: path7.join(cursorDir, "hooks.json"),
385
+ script: path7.join(modules, ...packageName.split("/"), "dist", "khala.mjs")
386
+ };
387
+ }
388
+ function cursorHookCommand(platform, node, script) {
389
+ return nodeScriptCommand(platform, node, script) + HOOK_SUFFIX;
390
+ }
391
+ function cursorMcpEntry(node, script) {
392
+ return {
393
+ type: "stdio",
394
+ command: node,
395
+ args: [script, "mcp", "--harness", "cursor"],
396
+ env: { [CURSOR_WORKSPACE_ENV]: "${workspaceFolder}" }
397
+ };
398
+ }
399
+ var isObject2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
400
+ function isKhalaCursorServer(entry) {
401
+ if (!isObject2(entry) || !Array.isArray(entry.args)) return false;
402
+ const args = entry.args;
403
+ const at = args.indexOf("--harness");
404
+ return args.includes("mcp") && at !== -1 && args[at + 1] === "cursor";
405
+ }
406
+ function mergeCursorMcp(config, entry) {
407
+ if (!isObject2(config)) return { error: "invalid_config" };
408
+ const servers = config.mcpServers ?? {};
409
+ if (!isObject2(servers)) return { error: "invalid_config" };
410
+ if (Object.hasOwn(servers, "khala") && !isKhalaCursorServer(servers.khala)) return { error: "cursor_mcp_exists" };
411
+ const next = { ...servers };
412
+ delete next.khala;
413
+ if (entry) next.khala = entry;
414
+ return { config: { ...config, mcpServers: next } };
415
+ }
416
+ function mergeCursorHooks(config, command) {
417
+ if (!isObject2(config)) return { error: "invalid_config" };
418
+ const hooks = config.hooks ?? {};
419
+ if (!isObject2(hooks) || Object.values(hooks).some((list) => !Array.isArray(list) || list.some((item) => !isObject2(item)))) return { error: "invalid_config" };
420
+ const ours = (item) => typeof item.command === "string" && item.command.endsWith(HOOK_SUFFIX);
421
+ const next = {};
422
+ for (const [event, list] of Object.entries(hooks)) {
423
+ const kept = list.filter((item) => !ours(item));
424
+ if (kept.length) next[event] = kept;
425
+ }
426
+ if (command) {
427
+ for (const event of CURSOR_HOOK_EVENTS) next[event] = [...next[event] ?? [], { command, timeout: 10 }];
428
+ }
429
+ return { config: { version: 1, ...config, hooks: next } };
430
+ }
431
+ async function readJsonFile(file) {
432
+ const current = await readManaged(file);
433
+ try {
434
+ return { ...current, value: jsonFormat.parse(current.text ?? "") };
435
+ } catch {
436
+ return { ...current, value: void 0 };
437
+ }
438
+ }
439
+ async function installCursor(input) {
440
+ const { paths, uninstall, stdout, stderr } = input;
441
+ const mcp = await readJsonFile(paths.mcpFile);
442
+ const hooks = await readJsonFile(paths.hooksFile);
443
+ const command = cursorHookCommand(input.platform, input.node, paths.script);
444
+ const nextMcp = mergeCursorMcp(mcp.value, uninstall ? null : cursorMcpEntry(input.node, paths.script));
445
+ if ("error" in nextMcp) {
446
+ stderr(nextMcp.error === "cursor_mcp_exists" ? `khala: ${paths.mcpFile} already has a "khala" server that is not this CLI; remove it and run this again` : `khala: invalid JSON in ${paths.mcpFile}`);
447
+ return 1;
448
+ }
449
+ const nextHooks = mergeCursorHooks(hooks.value, uninstall ? null : command);
450
+ if ("error" in nextHooks) {
451
+ stderr(`khala: invalid JSON in ${paths.hooksFile}`);
452
+ return 1;
453
+ }
454
+ if (!uninstall && input.install && !input.install()) return 1;
455
+ const managed = new ManagedFiles(input.stateDir);
456
+ if (uninstall) {
457
+ await managed.restore(mcp, jsonFormat, (value) => mergeCursorMcp(value, null).config);
458
+ await managed.restore(hooks, jsonFormat, (value, original) => {
459
+ const { config } = mergeCursorHooks(value, null);
460
+ const hadVersion = isObject2(original) && Object.hasOwn(original, "version");
461
+ if (!hadVersion && isObject2(config.hooks) && !Object.keys(config.hooks).length) delete config.version;
462
+ return config;
463
+ });
464
+ } else {
465
+ await managed.write([
466
+ { current: mcp, text: formatJson(nextMcp.config, mcp.text) },
467
+ { current: hooks, text: formatJson(nextHooks.config, hooks.text) }
468
+ ]);
469
+ }
470
+ if (uninstall) {
471
+ stdout(`removed the Khala MCP server and hooks from ${paths.cursorDir}; delete ${paths.prefix} to remove the CLI`);
472
+ } else {
473
+ stdout(`configured ${paths.mcpFile} and ${paths.hooksFile}`);
474
+ stdout('restart Cursor (or toggle "khala" off and on in Settings > MCP), then check that khala shows its five tools');
475
+ }
476
+ return 0;
477
+ }
478
+
479
+ // src/wake/muse-monitor.ts
480
+ var MUSE_WAKE_REQUEST = "muse-monitor-wake.json";
481
+ var shellQuote = (value) => `'${value.replaceAll("'", "'\\''")}'`;
482
+ function museCliPath(env = process.env) {
483
+ const home = env.HOME || env.USERPROFILE || os2.homedir();
484
+ const { prefix } = cursorPaths({ platform: process.platform, path: path4, home, env });
485
+ return process.platform === "win32" ? path4.join(prefix, "khala.cmd") : path4.join(prefix, "bin", "khala");
486
+ }
487
+ function museWatchCommand(sessionId, bin = museCliPath(), platform = process.platform) {
488
+ const quote = platform === "win32" ? (value) => `'${value.replaceAll("'", "''")}'` : shellQuote;
489
+ return `${platform === "win32" ? "& " : ""}${quote(bin)} watch --harness muse --session ${quote(sessionId ?? "<current session id from khala_status>")}`;
490
+ }
491
+ function museMonitorInstruction(sessionId, command = museWatchCommand(sessionId)) {
492
+ return `start Muse's monitor tool with command ${JSON.stringify(command)}, persistent: true, wake_delay_ms: 0, show_lines: true. Always pass the current session id explicitly; the monitor shell does not inherit MUSE_SESSION_ID. Keep one monitor per session; If it prints "Do not re-arm", report the reason and do not restart it. Notifications are wake notices, not channel messages. Do not call khala_read or reply to the wake notice; Khala hooks supply the <khala-channel-messages> frame. End the turn so the Stop hook can deliver the frame in sync mode; in steer mode it can arrive after a tool. Use khala_read only for history or owner-requested manual reads; if you already replied to a message from history, do not reply again when the same message appears in a hook frame. Never run the watcher as a foreground shell command.`;
493
+ }
494
+ function createMuseMonitorDriver() {
495
+ return {
496
+ id: "monitor",
497
+ rung: 1,
498
+ optIn: false,
499
+ minIdleMs: 0,
500
+ deadlineMs: 3e4,
501
+ verification: "nonce",
502
+ startsActivity: true,
503
+ available: (ctx) => monitorArmed(ctx.files),
504
+ unavailableReason: () => "monitor_missing",
505
+ async verify(ctx) {
506
+ const proof = await museWakeProof(ctx.files, ctx.sessionId, ctx.env);
507
+ if (proof) await settleAttempts(ctx.files.dir, {
508
+ now: ctx.now,
509
+ activity: null,
510
+ promptText: proof.text,
511
+ verifiedAt: proof.at
512
+ });
513
+ },
514
+ async wake(ctx, line) {
515
+ const nonce = /\(k-([a-f0-9]{8})\)$/.exec(line)?.[1];
516
+ if (!nonce || wakeLine(nonce) !== line) throw new TypeError("invalid_monitor_wake_line");
517
+ if (ctx.signal.aborted || (await readActivity(ctx.files)).state !== "idle" || !await monitorArmed(ctx.files)) return "skipped";
518
+ const epoch = (await readActivity(ctx.files)).updatedAt;
519
+ for (const dir of monitorStorageCandidates(ctx.files)) {
520
+ if ((await readJson(path4.join(dir, "monitor-wake-observed.json")))?.epoch === epoch) return "skipped";
521
+ }
522
+ const owner = await monitorOwner(ctx.files);
523
+ if (!owner) return "skipped";
524
+ const journal = museJournalPath(ctx.sessionId, ctx.env);
525
+ let journalOffset = 0;
526
+ if (journal) {
527
+ try {
528
+ journalOffset = (await fs4.stat(journal)).size;
529
+ } catch (error) {
530
+ if (error.code !== "ENOENT") throw error;
531
+ }
532
+ }
533
+ await writeJsonAtomic(path4.join(ctx.files.dir, MUSE_WAKE_REQUEST), {
534
+ owner: owner.nonce,
535
+ line,
536
+ at: ctx.now,
537
+ deadline: ctx.now + 3e4,
538
+ journalOffset
539
+ });
540
+ }
541
+ };
542
+ }
543
+ function museJournalPath(sessionId, env) {
544
+ if (!/^[a-f0-9]{8}-[a-f0-9]{4}-7[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/u.test(sessionId)) return void 0;
545
+ const created = new Date(Number.parseInt(sessionId.replaceAll("-", "").slice(0, 12), 16));
546
+ const date = [String(created.getFullYear()), String(created.getMonth() + 1).padStart(2, "0"), String(created.getDate()).padStart(2, "0")];
547
+ const home = env.HOME || env.USERPROFILE || os2.homedir();
548
+ const data = env.XDG_DATA_HOME && path4.isAbsolute(env.XDG_DATA_HOME) ? env.XDG_DATA_HOME : path4.join(home, ".local", "share");
549
+ return path4.join(data, "muse", "sessions", ...date, sessionId, "session.jsonl");
550
+ }
551
+ async function museStopWakeText(stdin, files, env) {
552
+ const input = JSON.parse(stdin);
553
+ if (input.hook_event_name !== "Stop" || typeof input.turn_id !== "string") return void 0;
554
+ return museWakeProof(files, input.session_id, env, input.turn_id);
555
+ }
556
+ async function museWakeProof(files, sessionId, env, turnId) {
557
+ const request = await readJson(path4.join(files.dir, MUSE_WAKE_REQUEST));
558
+ if (!request) return void 0;
559
+ const journal = museJournalPath(sessionId, env);
560
+ if (!journal) return void 0;
561
+ if (!Number.isSafeInteger(request.journalOffset) || request.journalOffset < 0) return void 0;
562
+ let handle;
563
+ try {
564
+ handle = await fs4.open(journal, "r");
565
+ } catch (error) {
566
+ if (error.code === "ENOENT") return void 0;
567
+ throw error;
568
+ }
569
+ const stream = handle.createReadStream({ start: request.journalOffset, encoding: "utf8", autoClose: false });
570
+ const reader = createInterface({ input: stream, crlfDelay: Infinity });
571
+ try {
572
+ for await (const line of reader) {
573
+ let record;
574
+ try {
575
+ record = JSON.parse(line);
576
+ } catch {
577
+ continue;
578
+ }
579
+ const event = record.payload?.event;
580
+ if (record.stream?.id === sessionId && record.payload_type === "runtime.session" && typeof record.payload?.run_id === "string" && (turnId === void 0 || record.payload.run_id === turnId) && event?.kind === "inbox_item_drained" && event.drain_target_run_stream?.id === record.payload.run_id && event.delivery_snapshot?.source?.source === "monitor_event" && event.delivery_snapshot.source.event_kind === "stdout_line" && event.delivery_snapshot.body === request.line && record.recorded_at / 1e3 >= request.at && record.recorded_at / 1e3 <= request.deadline) return { text: request.line, at: record.recorded_at / 1e3 };
581
+ }
582
+ } catch (error) {
583
+ if (error.code !== "ENOENT") throw error;
584
+ } finally {
585
+ reader.close();
586
+ stream.destroy();
587
+ await handle.close();
588
+ }
589
+ return void 0;
590
+ }
591
+
592
+ // src/wake/qwen-socket.ts
593
+ init_define_KHALA_BUNDLE();
594
+ import * as fs6 from "node:fs/promises";
595
+ import net from "node:net";
596
+ import os3 from "node:os";
597
+ import path5 from "node:path";
598
+ import { randomBytes as randomBytes2, randomUUID as randomUUID2 } from "node:crypto";
599
+
600
+ // src/wake/qwen-transcript.ts
601
+ init_define_KHALA_BUNDLE();
602
+ import * as fs5 from "node:fs/promises";
603
+ async function qwenNotification(filePath) {
604
+ const file = await fs5.open(filePath, "r");
605
+ let text;
606
+ try {
607
+ const stat3 = await file.stat();
608
+ const start = Math.max(0, stat3.size - 1024 * 1024);
609
+ const buffer = Buffer.alloc(Math.min(stat3.size, 1024 * 1024));
610
+ const { bytesRead } = await file.read(buffer, 0, buffer.length, start);
611
+ text = buffer.subarray(0, bytesRead).toString("utf8");
612
+ if (start) text = text.slice(text.indexOf("\n") + 1);
613
+ } finally {
614
+ await file.close();
615
+ }
616
+ const notifications = [];
617
+ for (const line of text.trimEnd().split("\n").reverse()) {
618
+ let entry;
619
+ try {
620
+ entry = JSON.parse(line);
621
+ } catch {
622
+ continue;
623
+ }
624
+ if (entry?.type !== "user" || entry.provenance !== "system" || entry.subtype !== "notification" || entry.deliveredTurn !== true || entry.message?.role !== "user" || !Array.isArray(entry.message.parts)) continue;
625
+ notifications.push(entry.message.parts.filter((part) => typeof part?.text === "string").map((part) => part.text).join("\n"));
626
+ }
627
+ return notifications.length ? notifications.join("\n") : void 0;
628
+ }
629
+ async function verifyQwenTranscript(dir, filePath, now) {
630
+ const text = await qwenNotification(filePath);
631
+ if (text !== void 0) await confirmWakeText(dir, "socket", text, now);
632
+ return text;
633
+ }
634
+
635
+ // src/wake/qwen-socket.ts
636
+ var qwenHome = (env) => env.QWEN_HOME ? path5.resolve(env.QWEN_HOME) : path5.join(env.HOME ?? os3.homedir(), ".qwen");
637
+ async function qwenInboundAllowed(env, workspace = env.PWD ?? process.cwd()) {
638
+ for (const file of [path5.join(qwenHome(env), "settings.json"), path5.join(workspace, ".qwen", "settings.json")]) {
639
+ const config = await readJson(file);
640
+ if (["hold", "refuse"].includes(config?.agents?.crossSessionInbound ?? "") || config?.agents?.crossSessionMessaging === false) return false;
641
+ }
642
+ return true;
643
+ }
644
+ var qwenControllerFile = (env) => path5.join(stateRoot(env), "qwen", "controller.json");
645
+ async function qwenSession(env) {
646
+ const socket = env.QWEN_CODE_MESSAGING_SOCKET;
647
+ if (!socket) return void 0;
648
+ try {
649
+ const dir = path5.join(qwenHome(env), "sessions");
650
+ for (const file of await fs6.readdir(dir)) {
651
+ if (!/^\d+\.json$/u.test(file)) continue;
652
+ const record = await readJson(path5.join(dir, file));
653
+ if (record?.ipcPath === socket && typeof record.sessionId === "string" && SESSION_ID_PATTERN.test(record.sessionId)) return record.sessionId;
654
+ }
655
+ } catch {
656
+ }
657
+ return void 0;
658
+ }
659
+ async function target(env) {
660
+ const sessionId = await qwenSession(env);
661
+ if (!sessionId) return void 0;
662
+ try {
663
+ const file = qwenControllerFile(env);
664
+ const stat3 = await fs6.lstat(file);
665
+ if (!stat3.isFile() || (stat3.mode & 63) !== 0) return void 0;
666
+ const credential = await readJson(file);
667
+ if (!credential?.token || !/^qpc_[0-9a-f]{64}$/u.test(credential.token)) return void 0;
668
+ return { socket: env.QWEN_CODE_MESSAGING_SOCKET, sessionId, token: credential.token };
669
+ } catch {
670
+ return void 0;
671
+ }
672
+ }
673
+ function qwenFrames(target2, line, from, replyToken, msgId) {
674
+ return JSON.stringify({ msgV: 1, type: "auth", token: target2.token }) + "\n" + JSON.stringify({
675
+ msgV: 1,
676
+ msgId,
677
+ type: "user",
678
+ from,
679
+ replyToken,
680
+ fromName: "khala",
681
+ fromMode: "prompting",
682
+ toSessionId: target2.sessionId,
683
+ priority: "next",
684
+ message: { role: "user", content: line }
685
+ }) + "\n";
686
+ }
687
+ async function sendQwenWake(target2, line, signal, options = {}) {
688
+ if (signal.aborted) return "failed";
689
+ const dir = await fs6.mkdtemp(path5.join(options.runtimeDir ?? os3.tmpdir(), "khala-qw-"));
690
+ const from = path5.join(dir, "r.sock");
691
+ if (Buffer.byteLength(from) > 103) {
692
+ await fs6.rm(dir, { recursive: true, force: true });
693
+ return "failed";
694
+ }
695
+ const replyToken = randomBytes2(32).toString("hex");
696
+ const msgId = randomUUID2();
697
+ const peers = /* @__PURE__ */ new Set();
698
+ let sender;
699
+ let finish;
700
+ const result = new Promise((resolve) => {
701
+ finish = resolve;
702
+ });
703
+ const aborted = () => finish("failed");
704
+ const server = net.createServer((socket) => {
705
+ peers.add(socket);
706
+ socket.on("close", () => peers.delete(socket));
707
+ socket.on("error", () => {
708
+ });
709
+ let buffer = "", authenticated = false;
710
+ socket.setEncoding("utf8");
711
+ socket.on("data", (chunk) => {
712
+ buffer += chunk;
713
+ if (buffer.length > 64 * 1024) {
714
+ socket.destroy();
715
+ return;
716
+ }
717
+ let at;
718
+ while ((at = buffer.indexOf("\n")) >= 0) {
719
+ const text = buffer.slice(0, at);
720
+ buffer = buffer.slice(at + 1);
721
+ let frame;
722
+ try {
723
+ frame = JSON.parse(text);
724
+ } catch {
725
+ socket.destroy();
726
+ return;
727
+ }
728
+ if (!authenticated) {
729
+ authenticated = frame?.msgV === 1 && frame.type === "auth" && frame.token === replyToken;
730
+ if (!authenticated) {
731
+ socket.destroy();
732
+ return;
733
+ }
734
+ } else if (frame?.msgV === 1 && frame.type === "control" && frame.action === "delivery_status" && frame.origMsgId === msgId && ["delivered", "held", "refused", "denied", "expired", "misaddressed", "dropped"].includes(frame.status)) {
735
+ finish(frame.status);
736
+ }
737
+ }
738
+ });
739
+ });
740
+ server.on("error", () => finish("failed"));
741
+ signal.addEventListener("abort", aborted, { once: true });
742
+ const timer = setTimeout(() => finish("failed"), options.timeoutMs ?? 5e3);
743
+ try {
744
+ await new Promise((resolve, reject) => {
745
+ server.once("error", reject);
746
+ server.listen(from, resolve);
747
+ });
748
+ await fs6.chmod(from, 384);
749
+ if (signal.aborted) return "failed";
750
+ sender = net.createConnection(target2.socket);
751
+ sender.on("error", () => finish("failed"));
752
+ sender.on("connect", () => {
753
+ sender.end(qwenFrames(target2, line, from, replyToken, msgId));
754
+ });
755
+ return await result;
756
+ } catch {
757
+ return "failed";
758
+ } finally {
759
+ clearTimeout(timer);
760
+ signal.removeEventListener("abort", aborted);
761
+ sender?.destroy();
762
+ for (const socket of peers) socket.destroy();
763
+ await new Promise((resolve) => server.close(() => resolve()));
764
+ await fs6.rm(dir, { recursive: true, force: true });
765
+ }
766
+ }
767
+ function createQwenSocketDriver(deps = {}) {
768
+ const resolve = deps.resolve ?? target;
769
+ const policy = async (env) => {
770
+ const config = await readJson(path5.join(qwenHome(env), "settings.json"));
771
+ return config?.agents;
772
+ };
773
+ const reason = async (ctx) => {
774
+ const config = await policy(ctx.env);
775
+ if (config?.crossSessionInbound === "hold") return "qwen_held";
776
+ if ((deps.platform ?? process.platform) === "win32") return "windows";
777
+ const receipt = await readJson(path5.join(ctx.files.dir, "qwen-receipt.json"));
778
+ if (receipt?.status === "held" && config?.crossSessionInbound !== "accept") return "qwen_held";
779
+ if (config?.crossSessionMessaging === false || config?.crossSessionInbound === "refuse") return "qwen_refused";
780
+ if (!deps.resolve && !ctx.env.QWEN_CODE_MESSAGING_SOCKET) return "qwen_session_missing";
781
+ return await resolve(ctx.env) ? void 0 : "qwen_socket_missing";
782
+ };
783
+ return {
784
+ id: "socket",
785
+ rung: 1,
786
+ optIn: false,
787
+ minIdleMs: 0,
788
+ deadlineMs: 3e4,
789
+ verification: "transcript",
790
+ async verify(ctx) {
791
+ const journal = await readJson(path5.join(ctx.files.dir, "wake-journal.json"));
792
+ const receipt = await readJson(path5.join(ctx.files.dir, "qwen-receipt.json"));
793
+ if (!journal?.attempts?.some((attempt) => attempt.driver === "socket") && receipt?.status !== "held") return;
794
+ const transcript = await readJson(path5.join(ctx.files.dir, "qwen-transcript.json"));
795
+ if (!transcript?.path) return;
796
+ try {
797
+ const text = await verifyQwenTranscript(ctx.files.dir, transcript.path, ctx.now);
798
+ if (receipt?.status === "held" && receipt.nonce && text?.includes(`(k-${receipt.nonce})`)) {
799
+ await writeJsonAtomic(path5.join(ctx.files.dir, "qwen-receipt.json"), { status: "delivered" });
800
+ }
801
+ } catch {
802
+ }
803
+ },
804
+ available: async (ctx) => !await reason(ctx),
805
+ unavailableReason: reason,
806
+ async wake(ctx, line) {
807
+ if (ctx.signal.aborted || await reason(ctx)) return "skipped";
808
+ const destination = await resolve(ctx.env);
809
+ if (!destination || destination.sessionId !== ctx.sessionId) return "skipped";
810
+ const status = await (deps.send ?? sendQwenWake)(destination, line, ctx.signal);
811
+ await ensureStateDir(ctx.files.dir);
812
+ const nonce = line.match(/\(k-([0-9a-f]{8})\)$/u)?.[1];
813
+ await writeJsonAtomic(path5.join(ctx.files.dir, "qwen-receipt.json"), { status, nonce });
814
+ if (nonce && status === "held") await cancelAttempt(ctx.files.dir, nonce);
815
+ else if (nonce && status !== "delivered") await failAttempt(ctx.files.dir, nonce, ctx.now);
816
+ }
817
+ };
818
+ }
819
+
820
+ // src/watch.ts
821
+ var MARKER = "monitor.json";
822
+ var OBSERVATION = "monitor-cursor.json";
823
+ var NONCE_PATTERN = /^[a-f0-9-]{36}$/;
824
+ var leaseFile = (dir, nonce) => path6.join(dir, `monitor-${nonce}.json`);
825
+ async function monitorOwner(files) {
826
+ const owners = [];
827
+ for (const dir of monitorStorageCandidates(files)) {
828
+ const owner = await readJson(path6.join(dir, MARKER));
829
+ if (!owner || !Number.isSafeInteger(owner.pid) || owner.pid <= 0 || typeof owner.nonce !== "string" || !NONCE_PATTERN.test(owner.nonce)) continue;
830
+ const lease = await readJson(leaseFile(dir, owner.nonce));
831
+ if (lease?.nonce !== owner.nonce) continue;
832
+ if (lease.heartbeatAt !== void 0) {
833
+ const age = Date.now() - Date.parse(lease.heartbeatAt);
834
+ if (Number.isFinite(age) && age >= 0 && age < 6e4) owners.push(owner);
835
+ continue;
836
+ }
837
+ try {
838
+ process.kill(owner.pid, 0);
839
+ owners.push(owner);
840
+ } catch (error) {
841
+ if (error.code === "EPERM") owners.push(owner);
842
+ }
843
+ }
844
+ return owners.sort((a, b) => (b.startedAt ?? 0) - (a.startedAt ?? 0))[0];
845
+ }
846
+ async function monitorArmed(files) {
847
+ return !!await monitorOwner(files);
848
+ }
849
+ function channelLabel(name) {
850
+ return Array.from(name.replace(/[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/gu, " ").trim()).slice(0, 80).join("");
851
+ }
852
+ async function watchSession(files, io) {
853
+ const nonce = randomUUID3();
854
+ let storage = files.dir;
855
+ const env = io.env ?? process.env;
856
+ const qwen = io.harness === "qwen";
857
+ const wakeAllowed = async () => {
858
+ if (!qwen) return true;
859
+ const wake = await readWakeSettings(stateRoot(env));
860
+ return await qwenInboundAllowed(env) && !wake.off["qwen/socket"] && !wake.off["qwen/background-shell"];
861
+ };
862
+ let watcher;
863
+ let timer;
864
+ let running;
865
+ let dirty = false;
866
+ let done = false;
867
+ let emittedRequest;
868
+ let code = 0;
869
+ let finish;
870
+ const finished = new Promise((resolve) => {
871
+ finish = resolve;
872
+ });
873
+ const stop = (exit = 0, reason = "stopped") => {
874
+ if (done) return;
875
+ done = true;
876
+ code = exit;
877
+ watcher?.close();
878
+ clearInterval(timer);
879
+ if (!qwen || exit !== 0) io.stderr?.(`khala watch: stopped (${reason}). Do not re-arm.`);
880
+ finish();
881
+ };
882
+ let lastHeartbeat = 0;
883
+ let exitReason = "disconnected";
884
+ const errorReason = (error) => {
885
+ const code2 = error.code;
886
+ if (code2 === "EROFS") return "monitor_storage_read_only";
887
+ if (code2 === "EACCES" || code2 === "EPERM") return "state_access_denied";
888
+ return error instanceof Error && error.message === "unsafe_monitor_directory" ? "unsafe_monitor_directory" : "state_unreadable";
889
+ };
890
+ const abort = () => stop();
891
+ try {
892
+ const status = await readWatcherStatus(files);
893
+ const initialSession = await readJson(files.session);
894
+ if (!["connected", "send_failed"].includes(status?.state ?? "") || !initialSession && !(await listChannels(files)).length) {
895
+ stop(2, status?.detail ?? status?.state ?? "session_missing");
896
+ return 2;
897
+ }
898
+ storage = await monitorStorage(files);
899
+ const storageDirs = /* @__PURE__ */ new Map([[files.dir, storage]]);
900
+ const observationDir = async (target2) => {
901
+ let dir = storageDirs.get(target2.dir);
902
+ if (!dir) {
903
+ dir = await monitorStorage(target2);
904
+ storageDirs.set(target2.dir, dir);
905
+ }
906
+ return dir;
907
+ };
908
+ const initialDelivered = /* @__PURE__ */ new Map();
909
+ const seedBaseline = async (target2, session) => {
910
+ initialDelivered.set(target2.dir, {
911
+ identity: JSON.stringify([session.roomId, session.userId]),
912
+ cursor: await readCursor(target2)
913
+ });
914
+ };
915
+ if (initialSession) await seedBaseline(files, initialSession);
916
+ for (const channel of await listChannels(files)) {
917
+ const session = await readJson(channel.files.session);
918
+ if (session) await seedBaseline(channel.files, session);
919
+ }
920
+ const evaluate = async () => {
921
+ const ownsSession = async () => {
922
+ const owner = await monitorOwner(files);
923
+ const status2 = await readWatcherStatus(files);
924
+ exitReason = owner?.nonce !== nonce ? "superseded" : status2?.detail ?? status2?.state ?? "status_missing";
925
+ return owner?.nonce === nonce && ["connected", "send_failed"].includes(status2?.state ?? "");
926
+ };
927
+ if (!await ownsSession()) {
928
+ stop(2, exitReason);
929
+ return;
930
+ }
931
+ if (Date.now() - lastHeartbeat >= 15e3) {
932
+ lastHeartbeat = Date.now();
933
+ await writeJsonAtomic(leaseFile(storage, nonce), { nonce, pid: process.pid, heartbeatAt: new Date(lastHeartbeat).toISOString() });
934
+ }
935
+ if (!await wakeAllowed()) return;
936
+ for (const channel of await listChannels(files)) {
937
+ const target2 = channel.files;
938
+ const session = await readJson(target2.session);
939
+ const status2 = await readWatcherStatus(target2);
940
+ if (!session || session.roomId !== channel.roomId || typeof session.userId !== "string" || !["connected", "send_failed"].includes(status2?.state ?? "")) continue;
941
+ const identity = JSON.stringify([session.roomId, session.userId]);
942
+ if (initialDelivered.get(target2.dir)?.identity !== identity) {
943
+ await seedBaseline(target2, session);
944
+ }
945
+ const mode = await readListeningMode(target2);
946
+ const entries = await readEntries(target2);
947
+ const observed = await readJson(path6.join(await observationDir(target2), OBSERVATION));
948
+ const position = observed?.userId === session.userId && observed?.roomId === session.roomId && Number.isSafeInteger(observed.count) && observed.count >= 0 && observed.count <= entries.length && (observed.count === 0 || entries[observed.count - 1]?.eventId === observed.lastEventId) ? observed.count : 0;
949
+ const baseline = initialDelivered.get(target2.dir).cursor;
950
+ const initialCount = baseline.deliveredCount === 0 || entries[baseline.deliveredCount - 1]?.eventId === baseline.lastDeliveredEventId ? baseline.deliveredCount : 0;
951
+ const fresh = entries.slice(Math.max(initialCount, position));
952
+ const latestMode = await readListeningMode(target2);
953
+ const latestSession = await readJson(target2.session);
954
+ const latestStatus = await readWatcherStatus(target2);
955
+ if (!await ownsSession()) {
956
+ stop(2, exitReason);
957
+ return;
958
+ }
959
+ if (!await wakeAllowed()) return;
960
+ if (latestSession?.userId !== session.userId || latestSession?.roomId !== session.roomId || !["connected", "send_failed"].includes(latestStatus?.state ?? "")) continue;
961
+ const messages = fresh.filter((entry) => isWakeEntry(entry) && entry.sender !== session.userId && entry.roomId === session.roomId);
962
+ if (io.harness !== "muse" && !done && mode !== "async" && latestMode !== "async" && messages.some((entry) => wakesAgent(entry, latestMode, latestStatus?.displayName))) {
963
+ const count = messages.filter((entry) => mentions(entry, latestStatus?.displayName)).length;
964
+ io.write(`khala: ${messages.length} new message${messages.length === 1 ? "" : "s"} in #${channelLabel(latestStatus?.channelName ?? channel.channelName ?? "channel")} (${count} mentions you)
965
+ `);
966
+ if (qwen) {
967
+ stop();
968
+ return;
969
+ }
970
+ }
971
+ if (position !== entries.length && !done && await ownsSession() && JSON.stringify(await readJson(target2.session)) === JSON.stringify(latestSession)) {
972
+ try {
973
+ await writeJsonAtomic(path6.join(await observationDir(target2), OBSERVATION), {
974
+ userId: session.userId,
975
+ roomId: session.roomId,
976
+ count: entries.length,
977
+ lastEventId: entries.at(-1)?.eventId ?? null
978
+ });
979
+ } catch (error) {
980
+ if (await readJson(target2.session)) throw error;
981
+ }
982
+ }
983
+ }
984
+ if (io.harness === "muse" && !done && await ownsSession()) {
985
+ const request = await readJson(path6.join(files.dir, MUSE_WAKE_REQUEST));
986
+ const now = (io.now ?? Date.now)();
987
+ const wakeNonce = typeof request?.line === "string" ? /\(k-([a-f0-9]{8})\)$/.exec(request.line)?.[1] : void 0;
988
+ if (request?.owner === nonce && wakeNonce && wakeLine(wakeNonce) === request.line && request.line !== emittedRequest && now >= request.at && now < request.deadline && await driverAllowed(stateRoot(io.env ?? process.env), "muse", "monitor", false) && !(await readWakeState(files.dir)).monitor?.disabled && (await readActivity(files)).state === "idle") {
989
+ const channels = await listChannels(files);
990
+ let eligible = false;
991
+ for (const channel of channels) {
992
+ const channelMode = await readListeningMode(channel.files);
993
+ if (channelMode === "async") continue;
994
+ const session = await readJson(channel.files.session);
995
+ const status2 = await readWatcherStatus(channel.files);
996
+ if (!["connected", "send_failed"].includes(status2?.state ?? "")) continue;
997
+ const cursor = await readCursor(channel.files);
998
+ eligible ||= (await readEntries(channel.files)).slice(cursor.deliveredCount).some((entry) => entry.sender !== session?.userId && wakesAgent(entry, channelMode, status2?.displayName));
999
+ }
1000
+ const activity = await readActivity(files);
1001
+ if (eligible && await ownsSession() && activity.state === "idle") {
1002
+ const epoch = activity.updatedAt;
1003
+ const dedupFile = path6.join(storage, "monitor-wake-observed.json");
1004
+ const observed = await readJson(dedupFile);
1005
+ if (observed?.epoch === epoch) return;
1006
+ await writeJsonAtomic(dedupFile, { epoch, line: request.line });
1007
+ io.write(request.line + "\n");
1008
+ emittedRequest = request.line;
1009
+ }
1010
+ }
1011
+ }
1012
+ };
1013
+ const notify = () => {
1014
+ if (done) return;
1015
+ dirty = true;
1016
+ if (running) return;
1017
+ running = (async () => {
1018
+ do {
1019
+ dirty = false;
1020
+ await evaluate();
1021
+ } while (dirty && !done);
1022
+ })().catch((error) => stop(3, errorReason(error))).finally(() => {
1023
+ running = void 0;
1024
+ if (dirty && !done) notify();
1025
+ });
1026
+ };
1027
+ let published = false;
1028
+ watcher = watchDirectory(files.dir, (_event, filename) => {
1029
+ if (published && (filename === null || [MARKER, MUSE_WAKE_REQUEST, "inbox.jsonl", "cursor.json", "mode.json", "session.json", "status.json"].includes(String(filename)))) notify();
1030
+ });
1031
+ watcher.on("error", (error) => stop(3, errorReason(error)));
1032
+ lastHeartbeat = Date.now();
1033
+ await writeJsonAtomic(leaseFile(storage, nonce), { nonce, pid: process.pid, heartbeatAt: new Date(lastHeartbeat).toISOString() });
1034
+ await writeJsonAtomic(path6.join(storage, MARKER), { nonce, pid: process.pid, startedAt: Date.now() });
1035
+ published = true;
1036
+ timer = setInterval(notify, 100);
1037
+ io.signal.addEventListener("abort", abort, { once: true });
1038
+ if (io.signal.aborted) stop();
1039
+ else notify();
1040
+ await finished;
1041
+ await running;
1042
+ return code;
1043
+ } catch (error) {
1044
+ stop(3, errorReason(error));
1045
+ return 3;
1046
+ } finally {
1047
+ clearInterval(timer);
1048
+ watcher?.close();
1049
+ io.signal.removeEventListener("abort", abort);
1050
+ await fs7.unlink(leaseFile(storage, nonce)).catch((error) => {
1051
+ if (!["ENOENT", "EACCES", "EROFS"].includes(error.code ?? "")) throw error;
1052
+ });
1053
+ }
1054
+ }
1055
+ var WATCH_USAGE = "usage: khala watch [--harness <id>] [--session <id>]";
1056
+ async function run(argv) {
1057
+ if (argv.length === 1 && ["--help", "-h"].includes(argv[0])) {
1058
+ console.log(WATCH_USAGE);
1059
+ return 0;
1060
+ }
1061
+ const invalid = (reason) => {
1062
+ console.error(`khala: ${reason}
1063
+ ${WATCH_USAGE}`);
1064
+ return 1;
1065
+ };
1066
+ if (argv.length !== 0 && !(argv.length === 2 && ["--session", "--harness"].includes(argv[0])) && !(argv.length === 4 && argv[0] === "--harness" && argv[2] === "--session")) return invalid("invalid_arguments");
1067
+ const { resolveHarness, resolveSessionId } = await import("./chunk-TSK3HJEG.mjs");
1068
+ const { adapterFor } = await import("./chunk-WZJ6DQJV.mjs");
1069
+ const harness = resolveHarness(argv, process.env);
1070
+ if (harness === "invalid" || !adapterFor(harness)?.codec) return invalid("invalid_harness");
1071
+ const index = argv.indexOf("--session");
1072
+ const id = index === -1 ? await resolveSessionId(harness, void 0, process.env) : argv[index + 1];
1073
+ if (!id) return invalid("session_unknown");
1074
+ if (!SESSION_ID_PATTERN.test(id)) return invalid("invalid_session_id");
1075
+ const files = sessionFiles(harness, id, process.env);
1076
+ const controller = new AbortController();
1077
+ const stop = () => controller.abort();
1078
+ process.once("SIGTERM", stop);
1079
+ process.once("SIGINT", stop);
1080
+ try {
1081
+ return await watchSession(files, { write: (line) => {
1082
+ process.stdout.write(line);
1083
+ }, signal: controller.signal, harness, env: process.env, stderr: (line) => {
1084
+ process.stderr.write(line + "\n");
1085
+ } });
1086
+ } finally {
1087
+ process.removeListener("SIGTERM", stop);
1088
+ process.removeListener("SIGINT", stop);
1089
+ }
1090
+ }
1091
+
1092
+ export {
1093
+ readWakeSettings,
1094
+ updateWakeSettings,
1095
+ driverAllowed,
1096
+ readListeningMode,
1097
+ applyListeningMode,
1098
+ CURSOR_WORKSPACE_ENV,
1099
+ CURSOR_DEFAULT_SESSION,
1100
+ cursorSessionId,
1101
+ installedLauncher,
1102
+ launcherShellQuote,
1103
+ formatJson,
1104
+ jsonFormat,
1105
+ textFormat,
1106
+ readManaged,
1107
+ ManagedFiles,
1108
+ verifyQwenTranscript,
1109
+ qwenHome,
1110
+ qwenInboundAllowed,
1111
+ qwenControllerFile,
1112
+ qwenSession,
1113
+ createQwenSocketDriver,
1114
+ nodeScriptCommand,
1115
+ cursorPaths,
1116
+ installCursor,
1117
+ monitorOwner,
1118
+ monitorArmed,
1119
+ watchSession,
1120
+ WATCH_USAGE,
1121
+ run,
1122
+ museCliPath,
1123
+ museWatchCommand,
1124
+ museMonitorInstruction,
1125
+ createMuseMonitorDriver,
1126
+ museStopWakeText
1127
+ };