khala-cli 0.4.8 → 0.4.9

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 (48) hide show
  1. package/README.md +4 -4
  2. package/dist/THIRD_PARTY_NOTICES.txt +781 -0
  3. package/dist/{chunk-APMWDF6B.mjs → chunk-3KDOTR54.mjs} +4 -2
  4. package/dist/{chunk-VUSV2ADL.mjs → chunk-3R6HFSLL.mjs} +147 -36
  5. package/dist/{chunk-2VCAW4GH.mjs → chunk-47XNLAJG.mjs} +5 -1
  6. package/dist/chunk-6BO3JSUP.mjs +33 -0
  7. package/dist/chunk-7R3EIDGR.mjs +58 -0
  8. package/dist/{chunk-5LIE5GUU.mjs → chunk-ANSR4IAW.mjs} +5 -1
  9. package/dist/chunk-BEO7WIIM.mjs +153 -0
  10. package/dist/chunk-BLCRVRIK.mjs +1000 -0
  11. package/dist/{chunk-54XFVSEC.mjs → chunk-BV45NK7K.mjs} +5 -1
  12. package/dist/{chunk-Z4JF2M5C.mjs → chunk-DJM23SIU.mjs} +8 -7
  13. package/dist/{chunk-2DHIAFEV.mjs → chunk-FO5GXCST.mjs} +24 -10
  14. package/dist/{chunk-CJTCU755.mjs → chunk-FZEH2IXZ.mjs} +95 -15
  15. package/dist/{chunk-RNDUFTBF.mjs → chunk-GSZADJMW.mjs} +233 -77
  16. package/dist/chunk-ITFD3JZS.mjs +1137 -0
  17. package/dist/chunk-K7V7X6WU.mjs +207 -0
  18. package/dist/chunk-KMLXOPBQ.mjs +20 -0
  19. package/dist/chunk-LBT47HLZ.mjs +19 -0
  20. package/dist/{chunk-IQJQ4RF5.mjs → chunk-NGSYOVQ4.mjs} +12 -1
  21. package/dist/chunk-NIX5F4LT.mjs +863 -0
  22. package/dist/chunk-NPSQNCOY.mjs +590 -0
  23. package/dist/chunk-P3YIN7G7.mjs +26 -0
  24. package/dist/chunk-PAJ4JJPV.mjs +9827 -0
  25. package/dist/{chunk-VAI7IUI4.mjs → chunk-PDZ5Z5ZE.mjs} +9 -6
  26. package/dist/chunk-QKWEZXR3.mjs +52 -0
  27. package/dist/chunk-SSO5RFOX.mjs +7 -0
  28. package/dist/{chunk-ZJ4W7P33.mjs → chunk-T7IL7V7Q.mjs} +11 -9
  29. package/dist/{chunk-VKQ3KUYO.mjs → chunk-TANCKTDK.mjs} +9 -4
  30. package/dist/{chunk-FV32AQTC.mjs → chunk-TEFOQXBK.mjs} +10 -6
  31. package/dist/{chunk-W6R4UJUQ.mjs → chunk-UK5GJO4Y.mjs} +15 -9
  32. package/dist/{chunk-BKLW2SPG.mjs → chunk-V4F3SYKW.mjs} +19 -10
  33. package/dist/{chunk-H3K4GA2P.mjs → chunk-WDOHRV3D.mjs} +31 -19
  34. package/dist/chunk-XBQDIKB5.mjs +18 -0
  35. package/dist/chunk-XUCWSYVS.mjs +148 -0
  36. package/dist/khala.mjs +20 -13
  37. package/dist/web/assets/local-CFzRHZpB.js +9 -0
  38. package/dist/web/assets/{local-OwDq47O4.css → local-ciypUFoK.css} +1 -1
  39. package/dist/web/index.html +2 -2
  40. package/package.json +5 -3
  41. package/dist/chunk-5BQTYU66.mjs +0 -886
  42. package/dist/chunk-5GLTKWJ5.mjs +0 -1
  43. package/dist/chunk-5PUQT3YS.mjs +0 -569
  44. package/dist/chunk-7HBRFY3Z.mjs +0 -8
  45. package/dist/chunk-CBPLCCRZ.mjs +0 -191
  46. package/dist/chunk-FH34B3I3.mjs +0 -235
  47. package/dist/chunk-NKX3SLS7.mjs +0 -227
  48. package/dist/web/assets/local-BQl2KrE1.js +0 -9
@@ -0,0 +1,863 @@
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-NGSYOVQ4.mjs";
6
+ import {
7
+ advanceCursor,
8
+ listChannels,
9
+ readActivity,
10
+ readCursor,
11
+ readEntries,
12
+ unread
13
+ } from "./chunk-TANCKTDK.mjs";
14
+ import {
15
+ SESSION_ID_PATTERN,
16
+ StateError,
17
+ readJson,
18
+ readWatcherStatus,
19
+ sessionFiles,
20
+ stateRoot,
21
+ writeJsonAtomic
22
+ } from "./chunk-NPSQNCOY.mjs";
23
+ import {
24
+ readWakeState,
25
+ settleAttempts,
26
+ wakeLine,
27
+ withWakeLock
28
+ } from "./chunk-K7V7X6WU.mjs";
29
+ import {
30
+ init_define_KHALA_BUNDLE
31
+ } from "./chunk-7R3EIDGR.mjs";
32
+
33
+ // src/watch.ts
34
+ init_define_KHALA_BUNDLE();
35
+ import * as fs5 from "node:fs/promises";
36
+ import { watch as watchDirectory } from "node:fs";
37
+ import path5 from "node:path";
38
+ import { randomUUID as randomUUID2 } from "node:crypto";
39
+
40
+ // src/monitor-storage.ts
41
+ init_define_KHALA_BUNDLE();
42
+ import * as fs from "node:fs/promises";
43
+ import { createHash, randomUUID } from "node:crypto";
44
+ import os from "node:os";
45
+ import path from "node:path";
46
+ function monitorStorageCandidates(files) {
47
+ const hash = createHash("sha256").update(path.resolve(files.dir)).digest("hex");
48
+ const uid = process.getuid?.() ?? os.userInfo().username;
49
+ return [files.dir, path.join(process.platform === "win32" ? os.tmpdir() : "/tmp", `khala-monitor-${uid}-${hash}`)];
50
+ }
51
+ async function monitorStorage(files) {
52
+ const probe = path.join(files.dir, `.monitor-probe-${randomUUID()}`);
53
+ try {
54
+ const handle = await fs.open(probe, "wx", 384);
55
+ await handle.close();
56
+ await fs.unlink(probe);
57
+ return files.dir;
58
+ } catch (error) {
59
+ if (!["EACCES", "EPERM", "EROFS"].includes(error.code ?? "")) throw error;
60
+ }
61
+ const dir = monitorStorageCandidates(files)[1];
62
+ await fs.mkdir(dir, { mode: 448 }).catch((error) => {
63
+ if (error.code !== "EEXIST") throw error;
64
+ });
65
+ const stat3 = await fs.lstat(dir);
66
+ if (!stat3.isDirectory() || stat3.isSymbolicLink() || process.getuid && stat3.uid !== process.getuid() || process.platform !== "win32" && (stat3.mode & 63) !== 0) throw new Error("unsafe_monitor_directory");
67
+ return dir;
68
+ }
69
+
70
+ // src/mode.ts
71
+ init_define_KHALA_BUNDLE();
72
+ async function readListeningMode(files) {
73
+ const content = await readJson(files.mode);
74
+ const decoded = decodeListeningMode(typeof content === "object" && content !== null ? content.mode : void 0);
75
+ return decoded.ok ? decoded.value : DEFAULT_LISTENING_MODE;
76
+ }
77
+ async function applyListeningMode(files, next, meta, now = () => /* @__PURE__ */ new Date()) {
78
+ const previous = await readListeningMode(files);
79
+ if (previous === "async" && next !== "async") {
80
+ for (let attempt = 0; attempt < 2; attempt++) {
81
+ const { cursor, entries } = await unread(files);
82
+ if (await advanceCursor(files, cursor, entries) !== "conflict") break;
83
+ if (attempt === 1) throw new StateError("storage_failed");
84
+ }
85
+ }
86
+ await writeJsonAtomic(files.mode, { mode: next, ...meta, updatedAt: now().toISOString() });
87
+ return { previous, next };
88
+ }
89
+
90
+ // src/wake/shared/index.ts
91
+ init_define_KHALA_BUNDLE();
92
+
93
+ // src/wake/shared/settings.ts
94
+ init_define_KHALA_BUNDLE();
95
+ import * as fs2 from "node:fs/promises";
96
+ import path2 from "node:path";
97
+ import { randomBytes } from "node:crypto";
98
+ async function readWakeSettings(root) {
99
+ try {
100
+ const value = JSON.parse(await fs2.readFile(path2.join(root, "wake-settings.json"), "utf8"));
101
+ const entries = (input) => {
102
+ if (!input || typeof input !== "object" || Array.isArray(input)) return {};
103
+ return Object.fromEntries(Object.entries(input).filter(([, entry]) => entry && typeof entry === "object" && typeof entry.at === "string"));
104
+ };
105
+ return { consent: entries(value?.consent), off: entries(value?.off) };
106
+ } catch (error) {
107
+ if (error.code === "ENOENT") return { consent: {}, off: {} };
108
+ throw error;
109
+ }
110
+ }
111
+ async function writeWakeSettings(root, settings) {
112
+ await fs2.mkdir(root, { recursive: true, mode: 448 });
113
+ const target = path2.join(root, "wake-settings.json");
114
+ const temp = `${target}.${randomBytes(8).toString("hex")}.tmp`;
115
+ try {
116
+ await fs2.writeFile(temp, JSON.stringify(settings), { mode: 384 });
117
+ await fs2.rename(temp, target);
118
+ } finally {
119
+ await fs2.rm(temp, { force: true });
120
+ }
121
+ }
122
+ async function updateWakeSettings(root, update) {
123
+ await withWakeLock(root, "wake-settings.lock", async () => {
124
+ const settings = await readWakeSettings(root);
125
+ update(settings);
126
+ await writeWakeSettings(root, settings);
127
+ });
128
+ }
129
+ async function driverAllowed(root, harness, driver, optIn) {
130
+ const settings = await readWakeSettings(root);
131
+ const key = `${harness}/${driver}`;
132
+ return !Object.hasOwn(settings.off, key) && (!optIn || Object.hasOwn(settings.consent, key));
133
+ }
134
+
135
+ // src/wake/muse-monitor.ts
136
+ init_define_KHALA_BUNDLE();
137
+ import { createInterface } from "node:readline";
138
+ import * as fs4 from "node:fs/promises";
139
+ import os2 from "node:os";
140
+ import path4 from "node:path";
141
+
142
+ // src/install/cursor.ts
143
+ init_define_KHALA_BUNDLE();
144
+
145
+ // src/cursor.ts
146
+ init_define_KHALA_BUNDLE();
147
+ import { createHash as createHash2 } from "node:crypto";
148
+ var CURSOR_WORKSPACE_ENV = "KHALA_CURSOR_WORKSPACE";
149
+ var CURSOR_DEFAULT_SESSION = "cursor-default";
150
+ function normalizeWorkspace(root) {
151
+ let value = root.trim();
152
+ if (!value || value.includes("${")) return null;
153
+ if (/^file:\/\//iu.test(value)) {
154
+ try {
155
+ value = decodeURIComponent(new URL(value).pathname);
156
+ } catch {
157
+ return null;
158
+ }
159
+ }
160
+ value = value.replaceAll("\\", "/");
161
+ if (/^\/[A-Za-z]:(?:\/|$)/u.test(value)) value = value.slice(1);
162
+ if (/^[A-Za-z]:(?:\/|$)/u.test(value)) value = value.toLowerCase();
163
+ value = value.replace(/\/+$/u, "");
164
+ if (/^[a-z]:$/u.test(value)) value += "/";
165
+ return value || null;
166
+ }
167
+ function cursorSessionId(root) {
168
+ const normalized = typeof root === "string" ? normalizeWorkspace(root) : null;
169
+ if (normalized === null) return CURSOR_DEFAULT_SESSION;
170
+ return `ws-${createHash2("sha256").update(normalized).digest("hex").slice(0, 32)}`;
171
+ }
172
+
173
+ // src/install/command.ts
174
+ init_define_KHALA_BUNDLE();
175
+ function nodeScriptCommand(platform, node, script) {
176
+ if (platform === "win32") return `node "${script.replaceAll("\\", "/")}"`;
177
+ const quote = (value) => /^[A-Za-z0-9_./:-]+$/u.test(value) ? value : `'${value.replaceAll("'", `'\\''`)}'`;
178
+ return `${quote(node)} ${quote(script)}`;
179
+ }
180
+
181
+ // src/install/managed-file.ts
182
+ init_define_KHALA_BUNDLE();
183
+ import * as fs3 from "node:fs/promises";
184
+ import path3 from "node:path";
185
+ import { isDeepStrictEqual } from "node:util";
186
+ var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
187
+ var BOM = "\uFEFF";
188
+ function formatJson(value, like) {
189
+ const body = like?.startsWith(BOM) ? like.slice(1) : like;
190
+ const indent = body?.match(/^([ \t]+)\S/mu)?.[1] ?? 2;
191
+ const newline = body?.includes("\r\n") ? "\r\n" : "\n";
192
+ const trailing = body === null || body === void 0 || /\n$/u.test(body) || body.trim() === "" ? newline : "";
193
+ return (like?.startsWith(BOM) ? BOM : "") + JSON.stringify(value, null, indent).replaceAll("\n", newline) + trailing;
194
+ }
195
+ function pruneCreated(value, original) {
196
+ if (!isObject(value)) return value;
197
+ const next = {};
198
+ for (const [key, child] of Object.entries(value)) {
199
+ const before = isObject(original) && Object.hasOwn(original, key) ? original[key] : void 0;
200
+ const pruned = pruneCreated(child, before);
201
+ const emptyContainer = Array.isArray(pruned) ? pruned.length === 0 : isObject(pruned) && Object.keys(pruned).length === 0;
202
+ if (emptyContainer && before === void 0) continue;
203
+ next[key] = pruned;
204
+ }
205
+ return next;
206
+ }
207
+ var jsonFormat = {
208
+ parse: (text) => text.trim() ? JSON.parse(text.replace(/^/u, "")) : {},
209
+ serialize: formatJson,
210
+ equal: isDeepStrictEqual,
211
+ empty: (value) => isObject(value) && Object.keys(value).length === 0,
212
+ prune: pruneCreated
213
+ };
214
+ var meaningfulLines = (text) => String(text).split(/\r?\n/u).map((line) => line.trimEnd()).filter(Boolean);
215
+ var textFormat = {
216
+ parse: (text) => text,
217
+ serialize: (value) => String(value),
218
+ equal: (a, b) => isDeepStrictEqual(meaningfulLines(a), meaningfulLines(b)),
219
+ empty: (value) => meaningfulLines(value).length === 0
220
+ };
221
+ async function readManaged(file) {
222
+ try {
223
+ return { file, text: await fs3.readFile(file, "utf8") };
224
+ } catch (error) {
225
+ if (error.code === "ENOENT") return { file, text: null };
226
+ throw error;
227
+ }
228
+ }
229
+ var LEGACY_BACKUP = ".khala-bak";
230
+ var ManagedFiles = class {
231
+ store;
232
+ constructor(stateDir) {
233
+ this.store = path3.join(stateDir, "install-originals.json");
234
+ }
235
+ async load() {
236
+ try {
237
+ const value = JSON.parse(await fs3.readFile(this.store, "utf8"));
238
+ return isObject(value) ? value : {};
239
+ } catch (error) {
240
+ if (error.code === "ENOENT") return {};
241
+ throw error;
242
+ }
243
+ }
244
+ async save(recordings) {
245
+ if (!Object.keys(recordings).length) {
246
+ await fs3.rm(this.store, { force: true });
247
+ return;
248
+ }
249
+ await fs3.mkdir(path3.dirname(this.store), { recursive: true, mode: 448 });
250
+ const temp = `${this.store}.${process.pid}.tmp`;
251
+ await fs3.writeFile(temp, JSON.stringify(recordings, null, 2) + "\n", { mode: 384 });
252
+ await fs3.rename(temp, this.store);
253
+ }
254
+ /** The recorded original: `undefined` when nothing is recorded, `null` when the file did not exist. */
255
+ async original(file) {
256
+ const recording = (await this.load())[path3.resolve(file)];
257
+ if (recording) return recording.existed ? Buffer.from(recording.bytes ?? "", "base64") : null;
258
+ const legacy = await readBytes(file + LEGACY_BACKUP);
259
+ return legacy ?? void 0;
260
+ }
261
+ /**
262
+ * Records the original once, before Khala's first change. An earlier installer's
263
+ * `.khala-bak` is adopted as the original (the file itself then holds Khala's output)
264
+ * and removed. A harness-specific legacy absence sentinel can pass `original: null`;
265
+ * the override still never replaces a previously saved recording.
266
+ */
267
+ async record(file, options) {
268
+ const key = path3.resolve(file);
269
+ const recordings = await this.load();
270
+ if (recordings[key]) return;
271
+ const legacy = await readBytes(file + LEGACY_BACKUP);
272
+ const current = options ? options.original : legacy ?? await readBytes(file);
273
+ recordings[key] = current === null ? { existed: false, createdDirs: await missingDirs(path3.dirname(key)) } : { existed: true, bytes: current.toString("base64"), createdDirs: [] };
274
+ await this.save(recordings);
275
+ if (legacy) await fs3.rm(file + LEGACY_BACKUP, { force: true });
276
+ }
277
+ /**
278
+ * Install: records every file's original first (so each knows which directories Khala
279
+ * creates), then writes each `text` that differs from the current content.
280
+ */
281
+ async write(entries) {
282
+ for (const { current } of entries) await this.record(current.file);
283
+ for (const { current, text, mode } of entries) {
284
+ if (text === current.text) continue;
285
+ await fs3.mkdir(path3.dirname(current.file), { recursive: true });
286
+ await fs3.writeFile(current.file, text, mode === void 0 ? void 0 : { mode });
287
+ }
288
+ }
289
+ /**
290
+ * Uninstall. `strip` receives the parsed current content and the parsed recorded original
291
+ * (`undefined` when the file did not exist or nothing is recorded) and returns the content
292
+ * without Khala's keys. Writes back the recorded bytes when the result is equal to them,
293
+ * deletes the file when it did not exist and nothing is left, and otherwise writes the
294
+ * stripped content. The recording is then dropped.
295
+ */
296
+ async restore(current, format, strip) {
297
+ const key = path3.resolve(current.file);
298
+ const recordings = await this.load();
299
+ const recording = recordings[key];
300
+ const original = await this.original(current.file);
301
+ if (current.text !== null) {
302
+ let originalValue;
303
+ let comparable = original !== void 0;
304
+ if (original) {
305
+ try {
306
+ originalValue = format.parse(original.toString("utf8"));
307
+ } catch {
308
+ comparable = false;
309
+ }
310
+ }
311
+ let value = strip(format.parse(current.text), originalValue);
312
+ if (comparable && format.prune) value = format.prune(value, originalValue);
313
+ if (comparable && original === null && format.empty(value)) {
314
+ await fs3.rm(current.file, { force: true });
315
+ for (const dir of recording?.createdDirs ?? []) await fs3.rmdir(dir).catch(() => void 0);
316
+ } else if (comparable && original && format.equal(value, originalValue)) {
317
+ if (!original.equals(Buffer.from(current.text, "utf8"))) await fs3.writeFile(current.file, original);
318
+ } else {
319
+ const text = format.serialize(value, current.text);
320
+ if (text !== current.text) await fs3.writeFile(current.file, text);
321
+ }
322
+ }
323
+ if (recording) {
324
+ delete recordings[key];
325
+ await this.save(recordings);
326
+ }
327
+ await fs3.rm(current.file + LEGACY_BACKUP, { force: true });
328
+ }
329
+ };
330
+ async function readBytes(file) {
331
+ try {
332
+ return await fs3.readFile(file);
333
+ } catch (error) {
334
+ if (error.code === "ENOENT") return null;
335
+ throw error;
336
+ }
337
+ }
338
+ async function missingDirs(dir) {
339
+ const missing = [];
340
+ for (let at = dir; ; at = path3.dirname(at)) {
341
+ try {
342
+ await fs3.stat(at);
343
+ return missing;
344
+ } catch (error) {
345
+ if (error.code !== "ENOENT") throw error;
346
+ }
347
+ missing.push(at);
348
+ if (path3.dirname(at) === at) return missing;
349
+ }
350
+ }
351
+
352
+ // src/install/cursor.ts
353
+ var CURSOR_HOOK_EVENTS = ["beforeSubmitPrompt", "postToolUse", "stop"];
354
+ var HOOK_SUFFIX = " hook deliver --harness cursor";
355
+ function cursorPaths({ platform, path: path6, home, env }, packageName = "khala-cli") {
356
+ const cursorDir = path6.join(home, ".cursor");
357
+ const windows = platform === "win32";
358
+ const dataHome = windows ? env.LOCALAPPDATA && path6.isAbsolute(env.LOCALAPPDATA) ? env.LOCALAPPDATA : path6.join(home, "AppData", "Local") : env.XDG_DATA_HOME && path6.isAbsolute(env.XDG_DATA_HOME) ? env.XDG_DATA_HOME : path6.join(home, ".local", "share");
359
+ const prefix = path6.join(dataHome, "khala", "npm");
360
+ const modules = windows ? path6.join(prefix, "node_modules") : path6.join(prefix, "lib", "node_modules");
361
+ return {
362
+ cursorDir,
363
+ prefix,
364
+ mcpFile: path6.join(cursorDir, "mcp.json"),
365
+ hooksFile: path6.join(cursorDir, "hooks.json"),
366
+ script: path6.join(modules, ...packageName.split("/"), "dist", "khala.mjs")
367
+ };
368
+ }
369
+ function cursorHookCommand(platform, node, script) {
370
+ return nodeScriptCommand(platform, node, script) + HOOK_SUFFIX;
371
+ }
372
+ function cursorMcpEntry(node, script) {
373
+ return {
374
+ type: "stdio",
375
+ command: node,
376
+ args: [script, "mcp", "--harness", "cursor"],
377
+ env: { [CURSOR_WORKSPACE_ENV]: "${workspaceFolder}" }
378
+ };
379
+ }
380
+ var isObject2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
381
+ function isKhalaCursorServer(entry) {
382
+ if (!isObject2(entry) || !Array.isArray(entry.args)) return false;
383
+ const args = entry.args;
384
+ const at = args.indexOf("--harness");
385
+ return args.includes("mcp") && at !== -1 && args[at + 1] === "cursor";
386
+ }
387
+ function mergeCursorMcp(config, entry) {
388
+ if (!isObject2(config)) return { error: "invalid_config" };
389
+ const servers = config.mcpServers ?? {};
390
+ if (!isObject2(servers)) return { error: "invalid_config" };
391
+ if (Object.hasOwn(servers, "khala") && !isKhalaCursorServer(servers.khala)) return { error: "cursor_mcp_exists" };
392
+ const next = { ...servers };
393
+ delete next.khala;
394
+ if (entry) next.khala = entry;
395
+ return { config: { ...config, mcpServers: next } };
396
+ }
397
+ function mergeCursorHooks(config, command) {
398
+ if (!isObject2(config)) return { error: "invalid_config" };
399
+ const hooks = config.hooks ?? {};
400
+ if (!isObject2(hooks) || Object.values(hooks).some((list) => !Array.isArray(list) || list.some((item) => !isObject2(item)))) return { error: "invalid_config" };
401
+ const ours = (item) => typeof item.command === "string" && item.command.endsWith(HOOK_SUFFIX);
402
+ const next = {};
403
+ for (const [event, list] of Object.entries(hooks)) {
404
+ const kept = list.filter((item) => !ours(item));
405
+ if (kept.length) next[event] = kept;
406
+ }
407
+ if (command) {
408
+ for (const event of CURSOR_HOOK_EVENTS) next[event] = [...next[event] ?? [], { command, timeout: 10 }];
409
+ }
410
+ return { config: { version: 1, ...config, hooks: next } };
411
+ }
412
+ async function readJsonFile(file) {
413
+ const current = await readManaged(file);
414
+ try {
415
+ return { ...current, value: jsonFormat.parse(current.text ?? "") };
416
+ } catch {
417
+ return { ...current, value: void 0 };
418
+ }
419
+ }
420
+ async function installCursor(input) {
421
+ const { paths, uninstall, stdout, stderr } = input;
422
+ const mcp = await readJsonFile(paths.mcpFile);
423
+ const hooks = await readJsonFile(paths.hooksFile);
424
+ const command = cursorHookCommand(input.platform, input.node, paths.script);
425
+ const nextMcp = mergeCursorMcp(mcp.value, uninstall ? null : cursorMcpEntry(input.node, paths.script));
426
+ if ("error" in nextMcp) {
427
+ 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}`);
428
+ return 1;
429
+ }
430
+ const nextHooks = mergeCursorHooks(hooks.value, uninstall ? null : command);
431
+ if ("error" in nextHooks) {
432
+ stderr(`khala: invalid JSON in ${paths.hooksFile}`);
433
+ return 1;
434
+ }
435
+ if (!uninstall && input.install && !input.install()) return 1;
436
+ const managed = new ManagedFiles(input.stateDir);
437
+ if (uninstall) {
438
+ await managed.restore(mcp, jsonFormat, (value) => mergeCursorMcp(value, null).config);
439
+ await managed.restore(hooks, jsonFormat, (value, original) => {
440
+ const { config } = mergeCursorHooks(value, null);
441
+ const hadVersion = isObject2(original) && Object.hasOwn(original, "version");
442
+ if (!hadVersion && isObject2(config.hooks) && !Object.keys(config.hooks).length) delete config.version;
443
+ return config;
444
+ });
445
+ } else {
446
+ await managed.write([
447
+ { current: mcp, text: formatJson(nextMcp.config, mcp.text) },
448
+ { current: hooks, text: formatJson(nextHooks.config, hooks.text) }
449
+ ]);
450
+ }
451
+ if (uninstall) {
452
+ stdout(`removed the Khala MCP server and hooks from ${paths.cursorDir}; delete ${paths.prefix} to remove the CLI`);
453
+ } else {
454
+ stdout(`configured ${paths.mcpFile} and ${paths.hooksFile}`);
455
+ stdout('restart Cursor (or toggle "khala" off and on in Settings > MCP), then check that khala shows its five tools');
456
+ }
457
+ return 0;
458
+ }
459
+
460
+ // src/wake/muse-monitor.ts
461
+ var MUSE_WAKE_REQUEST = "muse-monitor-wake.json";
462
+ var shellQuote = (value) => `'${value.replaceAll("'", "'\\''")}'`;
463
+ function museCliPath(env = process.env) {
464
+ const home = env.HOME || env.USERPROFILE || os2.homedir();
465
+ const { prefix } = cursorPaths({ platform: process.platform, path: path4, home, env });
466
+ return process.platform === "win32" ? path4.join(prefix, "khala.cmd") : path4.join(prefix, "bin", "khala");
467
+ }
468
+ function museWatchCommand(sessionId, bin = museCliPath()) {
469
+ return `${shellQuote(bin)} watch --harness muse --session ${shellQuote(sessionId ?? "<current session id from khala_status>")}`;
470
+ }
471
+ function museMonitorInstruction(sessionId, command = museWatchCommand(sessionId)) {
472
+ 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; use khala_read for channel content. Never run the watcher as a foreground shell command.`;
473
+ }
474
+ function createMuseMonitorDriver() {
475
+ return {
476
+ id: "monitor",
477
+ rung: 1,
478
+ optIn: false,
479
+ minIdleMs: 0,
480
+ deadlineMs: 3e4,
481
+ verification: "nonce",
482
+ startsActivity: true,
483
+ available: (ctx) => monitorArmed(ctx.files),
484
+ unavailableReason: () => "monitor_missing",
485
+ async verify(ctx) {
486
+ const proof = await museWakeProof(ctx.files, ctx.sessionId, ctx.env);
487
+ if (proof) await settleAttempts(ctx.files.dir, {
488
+ now: ctx.now,
489
+ activity: null,
490
+ promptText: proof.text,
491
+ verifiedAt: proof.at
492
+ });
493
+ },
494
+ async wake(ctx, line) {
495
+ const nonce = /\(k-([a-f0-9]{8})\)$/.exec(line)?.[1];
496
+ if (!nonce || wakeLine(nonce) !== line) throw new TypeError("invalid_monitor_wake_line");
497
+ if (ctx.signal.aborted || (await readActivity(ctx.files)).state !== "idle" || !await monitorArmed(ctx.files)) return "skipped";
498
+ const epoch = (await readActivity(ctx.files)).updatedAt;
499
+ for (const dir of monitorStorageCandidates(ctx.files)) {
500
+ if ((await readJson(path4.join(dir, "monitor-wake-observed.json")))?.epoch === epoch) return "skipped";
501
+ }
502
+ const owner = await monitorOwner(ctx.files);
503
+ if (!owner) return "skipped";
504
+ const journal = museJournalPath(ctx.sessionId, ctx.env);
505
+ let journalOffset = 0;
506
+ if (journal) {
507
+ try {
508
+ journalOffset = (await fs4.stat(journal)).size;
509
+ } catch (error) {
510
+ if (error.code !== "ENOENT") throw error;
511
+ }
512
+ }
513
+ await writeJsonAtomic(path4.join(ctx.files.dir, MUSE_WAKE_REQUEST), {
514
+ owner: owner.nonce,
515
+ line,
516
+ at: ctx.now,
517
+ deadline: ctx.now + 3e4,
518
+ journalOffset
519
+ });
520
+ }
521
+ };
522
+ }
523
+ function museJournalPath(sessionId, env) {
524
+ 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;
525
+ const created = new Date(Number.parseInt(sessionId.replaceAll("-", "").slice(0, 12), 16));
526
+ const date = [String(created.getFullYear()), String(created.getMonth() + 1).padStart(2, "0"), String(created.getDate()).padStart(2, "0")];
527
+ const home = env.HOME || env.USERPROFILE || os2.homedir();
528
+ const data = env.XDG_DATA_HOME && path4.isAbsolute(env.XDG_DATA_HOME) ? env.XDG_DATA_HOME : path4.join(home, ".local", "share");
529
+ return path4.join(data, "muse", "sessions", ...date, sessionId, "session.jsonl");
530
+ }
531
+ async function museStopWakeText(stdin, files, env) {
532
+ const input = JSON.parse(stdin);
533
+ if (input.hook_event_name !== "Stop" || typeof input.turn_id !== "string") return void 0;
534
+ return museWakeProof(files, input.session_id, env, input.turn_id);
535
+ }
536
+ async function museWakeProof(files, sessionId, env, turnId) {
537
+ const request = await readJson(path4.join(files.dir, MUSE_WAKE_REQUEST));
538
+ if (!request) return void 0;
539
+ const journal = museJournalPath(sessionId, env);
540
+ if (!journal) return void 0;
541
+ if (!Number.isSafeInteger(request.journalOffset) || request.journalOffset < 0) return void 0;
542
+ let handle;
543
+ try {
544
+ handle = await fs4.open(journal, "r");
545
+ } catch (error) {
546
+ if (error.code === "ENOENT") return void 0;
547
+ throw error;
548
+ }
549
+ const stream = handle.createReadStream({ start: request.journalOffset, encoding: "utf8", autoClose: false });
550
+ const reader = createInterface({ input: stream, crlfDelay: Infinity });
551
+ try {
552
+ for await (const line of reader) {
553
+ let record;
554
+ try {
555
+ record = JSON.parse(line);
556
+ } catch {
557
+ continue;
558
+ }
559
+ const event = record.payload?.event;
560
+ 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 };
561
+ }
562
+ } catch (error) {
563
+ if (error.code !== "ENOENT") throw error;
564
+ } finally {
565
+ reader.close();
566
+ stream.destroy();
567
+ await handle.close();
568
+ }
569
+ return void 0;
570
+ }
571
+
572
+ // src/watch.ts
573
+ var MARKER = "monitor.json";
574
+ var OBSERVATION = "monitor-cursor.json";
575
+ var NONCE_PATTERN = /^[a-f0-9-]{36}$/;
576
+ var leaseFile = (dir, nonce) => path5.join(dir, `monitor-${nonce}.json`);
577
+ async function monitorOwner(files) {
578
+ const owners = [];
579
+ for (const dir of monitorStorageCandidates(files)) {
580
+ const owner = await readJson(path5.join(dir, MARKER));
581
+ if (!owner || !Number.isSafeInteger(owner.pid) || owner.pid <= 0 || typeof owner.nonce !== "string" || !NONCE_PATTERN.test(owner.nonce)) continue;
582
+ const lease = await readJson(leaseFile(dir, owner.nonce));
583
+ if (lease?.nonce !== owner.nonce) continue;
584
+ if (lease.heartbeatAt !== void 0) {
585
+ const age = Date.now() - Date.parse(lease.heartbeatAt);
586
+ if (Number.isFinite(age) && age >= 0 && age < 6e4) owners.push(owner);
587
+ continue;
588
+ }
589
+ try {
590
+ process.kill(owner.pid, 0);
591
+ owners.push(owner);
592
+ } catch (error) {
593
+ if (error.code === "EPERM") owners.push(owner);
594
+ }
595
+ }
596
+ return owners.sort((a, b) => (b.startedAt ?? 0) - (a.startedAt ?? 0))[0];
597
+ }
598
+ async function monitorArmed(files) {
599
+ return !!await monitorOwner(files);
600
+ }
601
+ function mentions(entry, name) {
602
+ if (!name) return false;
603
+ const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
604
+ return new RegExp(`(?<![\\p{L}\\p{N}_])@?${escaped}(?![\\p{L}\\p{N}_])`, "iu").test(entry.body);
605
+ }
606
+ function channelLabel(name) {
607
+ return Array.from(name.replace(/[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/gu, " ").trim()).slice(0, 80).join("");
608
+ }
609
+ async function watchSession(files, io) {
610
+ const nonce = randomUUID2();
611
+ let storage = files.dir;
612
+ let watcher;
613
+ let timer;
614
+ let running;
615
+ let dirty = false;
616
+ let done = false;
617
+ let emittedRequest;
618
+ let code = 0;
619
+ let finish;
620
+ const finished = new Promise((resolve) => {
621
+ finish = resolve;
622
+ });
623
+ const stop = (exit = 0, reason = "stopped") => {
624
+ if (done) return;
625
+ done = true;
626
+ code = exit;
627
+ watcher?.close();
628
+ clearInterval(timer);
629
+ io.stderr?.(`khala watch: stopped (${reason}). Do not re-arm.`);
630
+ finish();
631
+ };
632
+ let lastHeartbeat = 0;
633
+ let exitReason = "disconnected";
634
+ const errorReason = (error) => {
635
+ const code2 = error.code;
636
+ if (code2 === "EROFS") return "monitor_storage_read_only";
637
+ if (code2 === "EACCES" || code2 === "EPERM") return "state_access_denied";
638
+ return error instanceof Error && error.message === "unsafe_monitor_directory" ? "unsafe_monitor_directory" : "state_unreadable";
639
+ };
640
+ const abort = () => stop();
641
+ try {
642
+ const status = await readWatcherStatus(files);
643
+ const initialSession = await readJson(files.session);
644
+ if (!["connected", "send_failed"].includes(status?.state ?? "") || !initialSession && !(await listChannels(files)).length) {
645
+ stop(2, status?.detail ?? status?.state ?? "session_missing");
646
+ return 2;
647
+ }
648
+ storage = await monitorStorage(files);
649
+ const storageDirs = /* @__PURE__ */ new Map([[files.dir, storage]]);
650
+ const observationDir = async (target) => {
651
+ let dir = storageDirs.get(target.dir);
652
+ if (!dir) {
653
+ dir = await monitorStorage(target);
654
+ storageDirs.set(target.dir, dir);
655
+ }
656
+ return dir;
657
+ };
658
+ const initialDelivered = /* @__PURE__ */ new Map();
659
+ const seedBaseline = async (target, session) => {
660
+ initialDelivered.set(target.dir, {
661
+ identity: JSON.stringify([session.roomId, session.userId]),
662
+ cursor: await readCursor(target)
663
+ });
664
+ };
665
+ if (initialSession) await seedBaseline(files, initialSession);
666
+ for (const channel of await listChannels(files)) {
667
+ const session = await readJson(channel.files.session);
668
+ if (session) await seedBaseline(channel.files, session);
669
+ }
670
+ const evaluate = async () => {
671
+ const ownsSession = async () => {
672
+ const owner = await monitorOwner(files);
673
+ const status2 = await readWatcherStatus(files);
674
+ exitReason = owner?.nonce !== nonce ? "superseded" : status2?.detail ?? status2?.state ?? "status_missing";
675
+ return owner?.nonce === nonce && ["connected", "send_failed"].includes(status2?.state ?? "");
676
+ };
677
+ if (!await ownsSession()) {
678
+ stop(2, exitReason);
679
+ return;
680
+ }
681
+ if (Date.now() - lastHeartbeat >= 15e3) {
682
+ lastHeartbeat = Date.now();
683
+ await writeJsonAtomic(leaseFile(storage, nonce), { nonce, pid: process.pid, heartbeatAt: new Date(lastHeartbeat).toISOString() });
684
+ }
685
+ for (const channel of await listChannels(files)) {
686
+ const target = channel.files;
687
+ const session = await readJson(target.session);
688
+ const status2 = await readWatcherStatus(target);
689
+ if (!session || session.roomId !== channel.roomId || typeof session.userId !== "string" || !["connected", "send_failed"].includes(status2?.state ?? "")) continue;
690
+ const identity = JSON.stringify([session.roomId, session.userId]);
691
+ if (initialDelivered.get(target.dir)?.identity !== identity) {
692
+ await seedBaseline(target, session);
693
+ }
694
+ const mode = await readListeningMode(target);
695
+ const entries = await readEntries(target);
696
+ const observed = await readJson(path5.join(await observationDir(target), OBSERVATION));
697
+ 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;
698
+ const baseline = initialDelivered.get(target.dir).cursor;
699
+ const initialCount = baseline.deliveredCount === 0 || entries[baseline.deliveredCount - 1]?.eventId === baseline.lastDeliveredEventId ? baseline.deliveredCount : 0;
700
+ const fresh = entries.slice(Math.max(initialCount, position));
701
+ const latestMode = await readListeningMode(target);
702
+ const latestSession = await readJson(target.session);
703
+ const latestStatus = await readWatcherStatus(target);
704
+ if (!await ownsSession()) {
705
+ stop(2, exitReason);
706
+ return;
707
+ }
708
+ if (latestSession?.userId !== session.userId || latestSession?.roomId !== session.roomId || !["connected", "send_failed"].includes(latestStatus?.state ?? "")) continue;
709
+ const messages = fresh.filter((entry) => entry.kind === "message" && entry.sender !== session.userId && entry.roomId === session.roomId);
710
+ if (io.harness !== "muse" && !done && mode !== "async" && latestMode !== "async" && messages.length) {
711
+ const count = messages.filter((entry) => mentions(entry, latestStatus?.displayName)).length;
712
+ io.write(`khala: ${messages.length} new message${messages.length === 1 ? "" : "s"} in #${channelLabel(latestStatus?.channelName ?? channel.channelName ?? "channel")} (${count} mentions you)
713
+ `);
714
+ }
715
+ if (position !== entries.length && !done && await ownsSession() && JSON.stringify(await readJson(target.session)) === JSON.stringify(latestSession)) {
716
+ try {
717
+ await writeJsonAtomic(path5.join(await observationDir(target), OBSERVATION), {
718
+ userId: session.userId,
719
+ roomId: session.roomId,
720
+ count: entries.length,
721
+ lastEventId: entries.at(-1)?.eventId ?? null
722
+ });
723
+ } catch (error) {
724
+ if (await readJson(target.session)) throw error;
725
+ }
726
+ }
727
+ }
728
+ if (io.harness === "muse" && !done && await ownsSession()) {
729
+ const request = await readJson(path5.join(files.dir, MUSE_WAKE_REQUEST));
730
+ const now = (io.now ?? Date.now)();
731
+ const wakeNonce = typeof request?.line === "string" ? /\(k-([a-f0-9]{8})\)$/.exec(request.line)?.[1] : void 0;
732
+ 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") {
733
+ const channels = await listChannels(files);
734
+ let eligible = false;
735
+ for (const channel of channels) {
736
+ if (await readListeningMode(channel.files) === "async") continue;
737
+ const session = await readJson(channel.files.session);
738
+ const status2 = await readWatcherStatus(channel.files);
739
+ if (!["connected", "send_failed"].includes(status2?.state ?? "")) continue;
740
+ const cursor = await readCursor(channel.files);
741
+ eligible ||= (await readEntries(channel.files)).slice(cursor.deliveredCount).some((entry) => entry.kind === "message" && entry.sender !== session?.userId);
742
+ }
743
+ const activity = await readActivity(files);
744
+ if (eligible && await ownsSession() && activity.state === "idle") {
745
+ const epoch = activity.updatedAt;
746
+ const dedupFile = path5.join(storage, "monitor-wake-observed.json");
747
+ const observed = await readJson(dedupFile);
748
+ if (observed?.epoch === epoch) return;
749
+ await writeJsonAtomic(dedupFile, { epoch, line: request.line });
750
+ io.write(request.line + "\n");
751
+ emittedRequest = request.line;
752
+ }
753
+ }
754
+ }
755
+ };
756
+ const notify = () => {
757
+ if (done) return;
758
+ dirty = true;
759
+ if (running) return;
760
+ running = (async () => {
761
+ do {
762
+ dirty = false;
763
+ await evaluate();
764
+ } while (dirty && !done);
765
+ })().catch((error) => stop(3, errorReason(error))).finally(() => {
766
+ running = void 0;
767
+ if (dirty && !done) notify();
768
+ });
769
+ };
770
+ let published = false;
771
+ watcher = watchDirectory(files.dir, (_event, filename) => {
772
+ if (published && (filename === null || [MARKER, MUSE_WAKE_REQUEST, "inbox.jsonl", "cursor.json", "mode.json", "session.json", "status.json"].includes(String(filename)))) notify();
773
+ });
774
+ watcher.on("error", (error) => stop(3, errorReason(error)));
775
+ lastHeartbeat = Date.now();
776
+ await writeJsonAtomic(leaseFile(storage, nonce), { nonce, pid: process.pid, heartbeatAt: new Date(lastHeartbeat).toISOString() });
777
+ await writeJsonAtomic(path5.join(storage, MARKER), { nonce, pid: process.pid, startedAt: Date.now() });
778
+ published = true;
779
+ timer = setInterval(notify, 100);
780
+ io.signal.addEventListener("abort", abort, { once: true });
781
+ if (io.signal.aborted) stop();
782
+ else notify();
783
+ await finished;
784
+ await running;
785
+ return code;
786
+ } catch (error) {
787
+ stop(3, errorReason(error));
788
+ return 3;
789
+ } finally {
790
+ clearInterval(timer);
791
+ watcher?.close();
792
+ io.signal.removeEventListener("abort", abort);
793
+ await fs5.unlink(leaseFile(storage, nonce)).catch((error) => {
794
+ if (!["ENOENT", "EACCES", "EROFS"].includes(error.code ?? "")) throw error;
795
+ });
796
+ }
797
+ }
798
+ var WATCH_USAGE = "usage: khala watch [--harness <id>] [--session <id>]";
799
+ async function run(argv) {
800
+ if (argv.length === 1 && ["--help", "-h"].includes(argv[0])) {
801
+ console.log(WATCH_USAGE);
802
+ return 0;
803
+ }
804
+ const invalid = (reason) => {
805
+ console.error(`khala: ${reason}
806
+ ${WATCH_USAGE}`);
807
+ return 1;
808
+ };
809
+ 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");
810
+ const { resolveHarness, resolveSessionId } = await import("./chunk-LBT47HLZ.mjs");
811
+ const { adapterFor } = await import("./chunk-XBQDIKB5.mjs");
812
+ const harness = resolveHarness(argv, process.env);
813
+ if (harness === "invalid" || !adapterFor(harness)?.codec) return invalid("invalid_harness");
814
+ const index = argv.indexOf("--session");
815
+ const id = index === -1 ? await resolveSessionId(harness, void 0, process.env) : argv[index + 1];
816
+ if (!id) return invalid("session_unknown");
817
+ if (!SESSION_ID_PATTERN.test(id)) return invalid("invalid_session_id");
818
+ const files = sessionFiles(harness, id, process.env);
819
+ const controller = new AbortController();
820
+ const stop = () => controller.abort();
821
+ process.once("SIGTERM", stop);
822
+ process.once("SIGINT", stop);
823
+ try {
824
+ return await watchSession(files, { write: (line) => {
825
+ process.stdout.write(line);
826
+ }, signal: controller.signal, harness, stderr: (line) => {
827
+ process.stderr.write(line + "\n");
828
+ } });
829
+ } finally {
830
+ process.removeListener("SIGTERM", stop);
831
+ process.removeListener("SIGINT", stop);
832
+ }
833
+ }
834
+
835
+ export {
836
+ readWakeSettings,
837
+ updateWakeSettings,
838
+ driverAllowed,
839
+ readListeningMode,
840
+ applyListeningMode,
841
+ CURSOR_WORKSPACE_ENV,
842
+ CURSOR_DEFAULT_SESSION,
843
+ cursorSessionId,
844
+ formatJson,
845
+ jsonFormat,
846
+ textFormat,
847
+ readManaged,
848
+ ManagedFiles,
849
+ nodeScriptCommand,
850
+ cursorPaths,
851
+ installCursor,
852
+ monitorOwner,
853
+ monitorArmed,
854
+ mentions,
855
+ watchSession,
856
+ WATCH_USAGE,
857
+ run,
858
+ museCliPath,
859
+ museWatchCommand,
860
+ museMonitorInstruction,
861
+ createMuseMonitorDriver,
862
+ museStopWakeText
863
+ };