khala-cli 0.4.6 → 0.4.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/README.md +18 -1
  2. package/dist/{chunk-2U4E7AM7.mjs → chunk-2DHIAFEV.mjs} +294 -101
  3. package/dist/{chunk-CWHGNVZV.mjs → chunk-2VCAW4GH.mjs} +1 -1
  4. package/dist/{chunk-P5CP32X2.mjs → chunk-54XFVSEC.mjs} +1 -1
  5. package/dist/chunk-5BQTYU66.mjs +886 -0
  6. package/dist/{chunk-RRBIVI77.mjs → chunk-5LIE5GUU.mjs} +1 -1
  7. package/dist/{chunk-6E3UCO22.mjs → chunk-5PUQT3YS.mjs} +200 -20
  8. package/dist/{chunk-55KBLEX5.mjs → chunk-7HBRFY3Z.mjs} +1 -1
  9. package/dist/{chunk-PFDUXY3N.mjs → chunk-APMWDF6B.mjs} +1 -1
  10. package/dist/{chunk-PY2ZNVNY.mjs → chunk-BKLW2SPG.mjs} +55 -8
  11. package/dist/{chunk-OW3DSTEZ.mjs → chunk-CBPLCCRZ.mjs} +5 -5
  12. package/dist/{chunk-JG5J3SHW.mjs → chunk-CJTCU755.mjs} +4 -4
  13. package/dist/{chunk-AWBKVTLV.mjs → chunk-FH34B3I3.mjs} +14 -17
  14. package/dist/chunk-FV32AQTC.mjs +40 -0
  15. package/dist/{chunk-FH2CZX5T.mjs → chunk-H3K4GA2P.mjs} +83 -36
  16. package/dist/{chunk-AUJEUKVM.mjs → chunk-IQJQ4RF5.mjs} +1 -1
  17. package/dist/{chunk-SN3STAO2.mjs → chunk-NKX3SLS7.mjs} +40 -3
  18. package/dist/{chunk-Z4EDYB6Y.mjs → chunk-RNDUFTBF.mjs} +97 -57
  19. package/dist/{chunk-OECJ45NM.mjs → chunk-VAI7IUI4.mjs} +24 -8
  20. package/dist/{chunk-XUJIJ4KB.mjs → chunk-VKQ3KUYO.mjs} +16 -2
  21. package/dist/{chunk-4X35OUAN.mjs → chunk-VUSV2ADL.mjs} +92 -97
  22. package/dist/{chunk-2XTYRBYX.mjs → chunk-W6R4UJUQ.mjs} +12 -5
  23. package/dist/{chunk-B2MOLCJM.mjs → chunk-Z4JF2M5C.mjs} +7 -9
  24. package/dist/{chunk-SZIEG3RS.mjs → chunk-ZJ4W7P33.mjs} +12 -14
  25. package/dist/khala.mjs +11 -11
  26. package/dist/web/assets/local-BQl2KrE1.js +9 -0
  27. package/dist/web/assets/local-OwDq47O4.css +1 -0
  28. package/dist/web/index.html +2 -2
  29. package/package.json +1 -1
  30. package/dist/chunk-DPW43YNC.mjs +0 -36
  31. package/dist/chunk-IULQYQJZ.mjs +0 -144
  32. package/dist/chunk-WHONG7UF.mjs +0 -267
  33. package/dist/chunk-ZHMGB3PP.mjs +0 -82
  34. package/dist/web/assets/local-K8EcTVxW.js +0 -9
  35. package/dist/web/assets/local-R2y0Gl4T.css +0 -1
@@ -1,7 +1,7 @@
1
1
  import { createRequire as __khalaCreateRequire } from 'node:module'; const require = __khalaCreateRequire(import.meta.url);
2
2
  import {
3
3
  bundle
4
- } from "./chunk-PFDUXY3N.mjs";
4
+ } from "./chunk-APMWDF6B.mjs";
5
5
 
6
6
  // src/version.ts
7
7
  var KHALA_AGENT_VERSION = bundle?.version ?? "0.0.0";
@@ -1,23 +1,22 @@
1
1
  import { createRequire as __khalaCreateRequire } from 'node:module'; const require = __khalaCreateRequire(import.meta.url);
2
2
  import {
3
3
  bundle
4
- } from "./chunk-PFDUXY3N.mjs";
4
+ } from "./chunk-APMWDF6B.mjs";
5
5
  import {
6
+ CURSOR_WORKSPACE_ENV,
6
7
  adapterFor
7
- } from "./chunk-WHONG7UF.mjs";
8
+ } from "./chunk-5BQTYU66.mjs";
9
+ import "./chunk-VKQ3KUYO.mjs";
10
+ import "./chunk-CBPLCCRZ.mjs";
11
+ import "./chunk-IQJQ4RF5.mjs";
8
12
  import {
9
- CURSOR_WORKSPACE_ENV
10
- } from "./chunk-ZHMGB3PP.mjs";
11
- import "./chunk-XUJIJ4KB.mjs";
12
- import "./chunk-DPW43YNC.mjs";
13
- import "./chunk-OW3DSTEZ.mjs";
14
- import "./chunk-AUJEUKVM.mjs";
15
- import "./chunk-SN3STAO2.mjs";
16
- import "./chunk-55KBLEX5.mjs";
13
+ isHarnessId
14
+ } from "./chunk-NKX3SLS7.mjs";
15
+ import "./chunk-7HBRFY3Z.mjs";
17
16
 
18
17
  // src/install/main.ts
19
18
  import { spawnSync } from "node:child_process";
20
- import * as fs2 from "node:fs/promises";
19
+ import * as fs3 from "node:fs/promises";
21
20
  import os from "node:os";
22
21
  import path from "node:path";
23
22
 
@@ -82,6 +81,35 @@ function mergeCodexHooks(config, action, hooksFragment = codexHooksFragment()) {
82
81
  return { config, warnings };
83
82
  }
84
83
 
84
+ // src/install/mcp.ts
85
+ var USAGE = "usage: khala install mcp --print [--harness <id>]";
86
+ async function runMcpInstall(flags, deps) {
87
+ const stdout = deps.stdout ?? ((line) => {
88
+ process.stdout.write(line + "\n");
89
+ });
90
+ const stderr = deps.stderr ?? ((line) => {
91
+ process.stderr.write(line + "\n");
92
+ });
93
+ let harness = "generic";
94
+ let print = false;
95
+ for (let i = 0; i < flags.length; i++) {
96
+ if (flags[i] === "--print" && !print) print = true;
97
+ else if (flags[i] === "--harness" && isHarnessId(flags[i + 1])) harness = flags[++i];
98
+ else {
99
+ stderr(USAGE);
100
+ return 1;
101
+ }
102
+ }
103
+ if (!print) {
104
+ stderr(USAGE);
105
+ return 1;
106
+ }
107
+ const args = ["-y", "khala-cli", "mcp", "--harness", harness];
108
+ stdout(JSON.stringify({ mcpServers: { khala: { command: "npx", args } } }, null, 2));
109
+ stdout(`npx ${args.join(" ")}`);
110
+ return 0;
111
+ }
112
+
85
113
  // src/install/cursor.ts
86
114
  import * as fs from "node:fs/promises";
87
115
  var CURSOR_HOOK_EVENTS = ["beforeSubmitPrompt", "postToolUse", "stop"];
@@ -198,9 +226,120 @@ async function installCursor(input) {
198
226
  return 0;
199
227
  }
200
228
 
229
+ // src/install/opencode.ts
230
+ import * as fs2 from "node:fs/promises";
231
+ function opencodePaths(input) {
232
+ const { path: path2, home, env, platform } = input;
233
+ const configHome = env.XDG_CONFIG_HOME && path2.isAbsolute(env.XDG_CONFIG_HOME) ? env.XDG_CONFIG_HOME : path2.join(home, ".config");
234
+ const configDir = path2.join(configHome, "opencode");
235
+ const { prefix } = cursorPaths(input);
236
+ return {
237
+ configDir,
238
+ configFile: path2.join(configDir, "opencode.json"),
239
+ prefix,
240
+ bin: platform === "win32" ? path2.join(prefix, "khala.cmd") : path2.join(prefix, "bin", "khala")
241
+ };
242
+ }
243
+ function opencodeMcpEntry(platform, bin) {
244
+ return {
245
+ type: "local",
246
+ command: [...platform === "win32" ? ["cmd", "/c"] : [], bin, "mcp", "--harness", "opencode"],
247
+ environment: {},
248
+ enabled: true
249
+ };
250
+ }
251
+ var isObject2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
252
+ var isPlugin = (spec) => spec === "khala-opencode" || spec.startsWith("khala-opencode@") || /^file:.*[/\\]khala-opencode-[^/\\]+\.tgz$/u.test(spec);
253
+ var isMcp = (entry) => {
254
+ if (!isObject2(entry) || !Array.isArray(entry.command)) return false;
255
+ const at = entry.command.indexOf("--harness");
256
+ return entry.command.includes("mcp") && at >= 0 && entry.command[at + 1] === "opencode";
257
+ };
258
+ function mergeOpenCodeConfig(config, plugin, previousOverride) {
259
+ if (!isObject2(config) || config.plugin !== void 0 && (!Array.isArray(config.plugin) || config.plugin.some((p) => typeof p !== "string")) || config.mcp !== void 0 && !isObject2(config.mcp)) return { error: "invalid_config" };
260
+ const plugins = (config.plugin ?? []).filter((spec) => !isPlugin(spec) && spec !== previousOverride && spec !== plugin);
261
+ if (plugin) plugins.push(plugin);
262
+ const next = { ...config };
263
+ if (plugins.length) next.plugin = plugins;
264
+ else delete next.plugin;
265
+ if (isObject2(config.mcp) && isMcp(config.mcp.khala)) {
266
+ const mcp = { ...config.mcp };
267
+ delete mcp.khala;
268
+ if (Object.keys(mcp).length) next.mcp = mcp;
269
+ else delete next.mcp;
270
+ }
271
+ return { config: next };
272
+ }
273
+ async function installOpenCode(input) {
274
+ const { paths, uninstall, stdout, stderr } = input;
275
+ try {
276
+ await fs2.stat(paths.configFile.replace(/\.json$/u, ".jsonc"));
277
+ stderr(`khala: opencode.jsonc exists in ${paths.configDir}; ${uninstall ? "remove" : "merge"} the Khala config manually or convert it to opencode.json before retrying`);
278
+ return 1;
279
+ } catch (error) {
280
+ if (error.code !== "ENOENT") throw error;
281
+ }
282
+ let text = null;
283
+ try {
284
+ text = await fs2.readFile(paths.configFile, "utf8");
285
+ } catch (error) {
286
+ if (error.code !== "ENOENT") throw error;
287
+ }
288
+ let config;
289
+ try {
290
+ config = text?.trim() ? JSON.parse(text.replace(/^\uFEFF/u, "")) : {};
291
+ } catch {
292
+ stderr(`khala: invalid JSON in ${paths.configFile}`);
293
+ return 1;
294
+ }
295
+ const marker = paths.configFile + ".khala-plugin";
296
+ let previousOverride;
297
+ try {
298
+ previousOverride = (await fs2.readFile(marker, "utf8")).trim();
299
+ } catch (error) {
300
+ if (error.code !== "ENOENT") throw error;
301
+ }
302
+ const merged = mergeOpenCodeConfig(config, uninstall ? null : input.plugin, previousOverride ?? input.plugin ?? void 0);
303
+ if ("error" in merged) {
304
+ stderr(`khala: invalid config in ${paths.configFile}`);
305
+ return 1;
306
+ }
307
+ if (!uninstall && input.plugin === null) {
308
+ if (isObject2(config) && isObject2(config.mcp) && Object.hasOwn(config.mcp, "khala") && !isMcp(config.mcp.khala)) {
309
+ stderr(`khala: refusing to overwrite existing mcp.khala in ${paths.configFile}; remove or rename it before retrying`);
310
+ return 1;
311
+ }
312
+ merged.config.mcp = { ...merged.config.mcp, khala: opencodeMcpEntry(input.platform, paths.bin) };
313
+ }
314
+ if (!uninstall && input.install && !input.install()) return 1;
315
+ if (uninstall && text === null) return 0;
316
+ await fs2.mkdir(paths.configDir, { recursive: true });
317
+ if (!uninstall && text !== null) {
318
+ try {
319
+ await fs2.writeFile(paths.configFile + ".khala-bak", text, { flag: "wx" });
320
+ } catch (error) {
321
+ if (error.code !== "EEXIST") throw error;
322
+ }
323
+ }
324
+ const next = JSON.stringify(merged.config, null, 2) + "\n";
325
+ if (next !== text) await fs2.writeFile(paths.configFile, next);
326
+ if (uninstall || input.plugin === null) await fs2.rm(marker, { force: true });
327
+ else await fs2.writeFile(marker, input.plugin + "\n");
328
+ stdout(uninstall ? `removed the Khala plugin and MCP entry from ${paths.configFile}; delete ${paths.prefix} to remove the CLI` : input.plugin ? `configured ${paths.configFile}; restart OpenCode to load the Khala plugin` : "OpenCode installed in MCP-only mode (Async, no wake); re-run khala install opencode after updating");
329
+ return 0;
330
+ }
331
+ async function opencodePluginPublished(version, fetchRegistry = fetch) {
332
+ try {
333
+ const response = await fetchRegistry(`https://registry.npmjs.org/khala-opencode/${encodeURIComponent(version)}`, { signal: AbortSignal.timeout(3e3) });
334
+ return response.ok;
335
+ } catch {
336
+ return false;
337
+ }
338
+ }
339
+
201
340
  // src/install/main.ts
202
341
  var MCP_MARKER = "# Khala MCP server, managed by `khala install codex`";
203
- var USAGE = "usage: khala install codex [--codex-home <dir>] [--uninstall] | khala install cursor [--uninstall]";
342
+ var USAGE2 = "usage: khala install codex [--codex-home <dir>] [--uninstall] | khala install cursor [--uninstall] | khala install opencode [--uninstall] | khala install mcp --print [--harness <id>]";
204
343
  async function runCursorInstall(flags, deps) {
205
344
  const env = deps.env ?? process.env;
206
345
  const stdout = deps.stdout ?? ((line) => {
@@ -213,7 +352,7 @@ async function runCursorInstall(flags, deps) {
213
352
  for (const flag of flags) {
214
353
  if (flag === "--uninstall") uninstall = true;
215
354
  else {
216
- stderr(USAGE);
355
+ stderr(USAGE2);
217
356
  return 1;
218
357
  }
219
358
  }
@@ -242,6 +381,45 @@ async function runCursorInstall(flags, deps) {
242
381
  }
243
382
  });
244
383
  }
384
+ async function runOpenCodeInstall(flags, deps) {
385
+ const env = deps.env ?? process.env;
386
+ const stdout = deps.stdout ?? ((line) => {
387
+ process.stdout.write(line + "\n");
388
+ });
389
+ const stderr = deps.stderr ?? ((line) => {
390
+ process.stderr.write(line + "\n");
391
+ });
392
+ if (flags.some((flag) => flag !== "--uninstall")) {
393
+ stderr(USAGE2);
394
+ return 1;
395
+ }
396
+ const uninstall = flags.includes("--uninstall");
397
+ const pkg = "package" in deps ? deps.package : bundle;
398
+ if (!pkg && !uninstall) {
399
+ stderr("khala: install runs from the published package (npx -y khala-cli install opencode)");
400
+ return 1;
401
+ }
402
+ const platform = deps.platform ?? process.platform;
403
+ const home = deps.home ?? (platform === "win32" ? env.USERPROFILE || os.homedir() : env.HOME || os.homedir());
404
+ const paths = opencodePaths({ platform, path: platform === "win32" ? path.win32 : path.posix, home, env });
405
+ const spec = pkg ? env.KHALA_INSTALL_SPEC || `${pkg.name}@${pkg.version}` : "";
406
+ const override = env.KHALA_OPENCODE_PLUGIN_SPEC;
407
+ const plugin = uninstall ? null : override || (await opencodePluginPublished(pkg.version, deps.fetchRegistry) ? `khala-opencode@${pkg.version}` : null);
408
+ return installOpenCode({
409
+ paths,
410
+ platform,
411
+ uninstall,
412
+ stdout,
413
+ stderr,
414
+ plugin,
415
+ install: () => {
416
+ stdout(`installing ${spec} into ${paths.prefix}`);
417
+ if ((deps.npmInstall ?? defaultNpmInstall)(paths.prefix, spec)) return true;
418
+ stderr("khala: npm install failed for " + spec);
419
+ return false;
420
+ }
421
+ });
422
+ }
245
423
  function shellQuote(value) {
246
424
  return /^[A-Za-z0-9_./-]+$/u.test(value) ? value : `'${value.replaceAll("'", `'\\''`)}'`;
247
425
  }
@@ -281,7 +459,7 @@ function codexMcpBlock(bin, env) {
281
459
  }
282
460
  async function readOr(file, fallback) {
283
461
  try {
284
- return { text: await fs2.readFile(file, "utf8"), mode: (await fs2.stat(file)).mode & 511 };
462
+ return { text: await fs3.readFile(file, "utf8"), mode: (await fs3.stat(file)).mode & 511 };
285
463
  } catch (error) {
286
464
  if (error.code === "ENOENT") return { text: fallback, mode: 384 };
287
465
  throw error;
@@ -301,7 +479,7 @@ async function runCodexInstall(flags, deps) {
301
479
  if (flags[i] === "--uninstall") uninstall = true;
302
480
  else if (flags[i] === "--codex-home" && flags[i + 1]) codexHome = path.resolve(flags[++i]);
303
481
  else {
304
- stderr(USAGE);
482
+ stderr(USAGE2);
305
483
  return 1;
306
484
  }
307
485
  }
@@ -345,17 +523,17 @@ async function runCodexInstall(flags, deps) {
345
523
  return 1;
346
524
  }
347
525
  }
348
- await fs2.mkdir(codexHome, { recursive: true });
526
+ await fs3.mkdir(codexHome, { recursive: true });
349
527
  if (!uninstall) {
350
528
  try {
351
- await fs2.writeFile(hooksFile + ".khala-bak", hooks.text, { flag: "wx", mode: hooks.mode });
529
+ await fs3.writeFile(hooksFile + ".khala-bak", hooks.text, { flag: "wx", mode: hooks.mode });
352
530
  } catch (error) {
353
531
  if (error.code !== "EEXIST") throw error;
354
532
  }
355
533
  }
356
534
  for (const warning of merged.warnings) stderr(warning);
357
- await fs2.writeFile(hooksFile, JSON.stringify(merged.config, null, 2) + "\n", { mode: hooks.mode });
358
- await fs2.writeFile(tomlFile, nextToml.text, { mode: toml.mode });
535
+ await fs3.writeFile(hooksFile, JSON.stringify(merged.config, null, 2) + "\n", { mode: hooks.mode });
536
+ await fs3.writeFile(tomlFile, nextToml.text, { mode: toml.mode });
359
537
  if (uninstall) {
360
538
  stdout(`removed the Khala MCP server and hooks from ${codexHome}; delete ${prefix} to remove the CLI`);
361
539
  } else {
@@ -366,12 +544,13 @@ async function runCodexInstall(flags, deps) {
366
544
  }
367
545
  async function runInstall(argv, deps = {}) {
368
546
  const [target = "", ...flags] = argv;
547
+ if (target === "mcp") return runMcpInstall(flags, deps);
369
548
  const adapter = adapterFor(target);
370
549
  const run = adapter?.install;
371
550
  if (run) return run(flags, deps);
372
551
  (deps.stderr ?? ((line) => {
373
552
  process.stderr.write(line + "\n");
374
- }))(USAGE);
553
+ }))(USAGE2);
375
554
  return 1;
376
555
  }
377
556
  async function main(argv) {
@@ -384,6 +563,7 @@ export {
384
563
  runCodexInstall,
385
564
  runCursorInstall,
386
565
  runInstall,
566
+ runOpenCodeInstall,
387
567
  shellQuote,
388
568
  updateCodexToml
389
569
  };
@@ -1,7 +1,7 @@
1
1
  import { createRequire as __khalaCreateRequire } from 'node:module'; const require = __khalaCreateRequire(import.meta.url);
2
2
 
3
3
  // <define:__KHALA_BUNDLE__>
4
- var define_KHALA_BUNDLE_default = { name: "khala-cli", version: "0.4.6" };
4
+ var define_KHALA_BUNDLE_default = { name: "khala-cli", version: "0.4.8" };
5
5
 
6
6
  export {
7
7
  define_KHALA_BUNDLE_default
@@ -1,7 +1,7 @@
1
1
  import { createRequire as __khalaCreateRequire } from 'node:module'; const require = __khalaCreateRequire(import.meta.url);
2
2
  import {
3
3
  define_KHALA_BUNDLE_default
4
- } from "./chunk-55KBLEX5.mjs";
4
+ } from "./chunk-7HBRFY3Z.mjs";
5
5
 
6
6
  // src/bundle.ts
7
7
  var bundle = typeof define_KHALA_BUNDLE_default === "undefined" ? void 0 : define_KHALA_BUNDLE_default;
@@ -1,10 +1,22 @@
1
1
  import { createRequire as __khalaCreateRequire } from 'node:module'; const require = __khalaCreateRequire(import.meta.url);
2
- import "./chunk-55KBLEX5.mjs";
2
+ import {
3
+ sessionStartWakeChannels
4
+ } from "./chunk-FV32AQTC.mjs";
5
+ import {
6
+ adapterFor
7
+ } from "./chunk-5BQTYU66.mjs";
8
+ import "./chunk-VKQ3KUYO.mjs";
9
+ import {
10
+ filesForDir,
11
+ resolveStateDir
12
+ } from "./chunk-CBPLCCRZ.mjs";
13
+ import "./chunk-IQJQ4RF5.mjs";
14
+ import "./chunk-NKX3SLS7.mjs";
15
+ import "./chunk-7HBRFY3Z.mjs";
3
16
 
4
17
  // hooks/claude-wake.ts
5
18
  import fs from "node:fs/promises";
6
19
  import path from "node:path";
7
- import os from "node:os";
8
20
  import { randomBytes } from "node:crypto";
9
21
  import { setTimeout as sleep } from "node:timers/promises";
10
22
  var NOTICE = "Khala: new channel messages. They arrive in the next hook context.\n";
@@ -73,20 +85,38 @@ function parentAlive(parent) {
73
85
  }
74
86
  async function watch(stdin, _argv, io = { stderr: process.stderr, env: process.env, now: () => /* @__PURE__ */ new Date() }) {
75
87
  let temporary;
88
+ let finish;
89
+ let state = "exited";
76
90
  try {
77
91
  const parent = process.ppid;
78
92
  const input = JSON.parse(stdin);
79
- if (input?.hook_event_name !== "Stop" || typeof input.session_id !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(input.session_id)) return 0;
80
- const root = io.env.XDG_STATE_HOME && path.isAbsolute(io.env.XDG_STATE_HOME) ? io.env.XDG_STATE_HOME : path.join(io.env.HOME ?? os.homedir(), ".local/state");
81
- const dir = path.join(root, "khala", "claude", input.session_id);
93
+ if (!["Stop", "SessionStart"].includes(input?.hook_event_name) || typeof input.session_id !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(input.session_id)) return 0;
94
+ const dir = resolveStateDir(adapterFor("claude").id, input.session_id, io.env);
82
95
  if (!(await fs.stat(dir)).isDirectory()) return 0;
96
+ if (input.hook_event_name === "SessionStart") {
97
+ if (io.env.CLAUDE_CODE_ENTRYPOINT !== "cli" || !["startup", "resume"].includes(input.source)) return 0;
98
+ const files = filesForDir(dir);
99
+ if (!(await sessionStartWakeChannels(files, io.env)).length) return 0;
100
+ }
83
101
  const nonce = randomBytes(6).toString("hex");
84
102
  const owner = path.join(dir, "watcher.json");
85
103
  temporary = path.join(dir, `.watcher-${nonce}.tmp`);
86
104
  const started = io.now();
87
- await fs.writeFile(temporary, JSON.stringify({ nonce, armedAt: started.toISOString() }) + "\n", { mode: 384, flag: "wx" });
105
+ const record = { nonce, armedAt: started.toISOString(), pid: process.pid, parentPid: parent, state: "armed" };
106
+ await fs.writeFile(temporary, JSON.stringify(record) + "\n", { mode: 384, flag: "wx" });
88
107
  await fs.rename(temporary, owner);
89
108
  temporary = void 0;
109
+ finish = async () => {
110
+ const file = await fs.open(owner, "r+");
111
+ try {
112
+ if (JSON.parse(await file.readFile("utf8")).nonce !== nonce) return;
113
+ const body = JSON.stringify({ ...record, state }) + "\n";
114
+ await file.write(body, 0, "utf8");
115
+ await file.truncate(Buffer.byteLength(body));
116
+ } finally {
117
+ await file.close();
118
+ }
119
+ };
90
120
  const deadline = started.getTime() + testDuration(io.env.KHALA_WAKE_TEST_DEADLINE_MS, DEADLINE_MS);
91
121
  const poll = testDuration(io.env.KHALA_WAKE_TEST_POLL_MS, POLL_MS);
92
122
  const owns = async () => (await readJson(owner))?.nonce === nonce;
@@ -94,18 +124,35 @@ async function watch(stdin, _argv, io = { stderr: process.stderr, env: process.e
94
124
  const activity = await readJson(path.join(dir, "activity.json"));
95
125
  return activity?.state === "idle" && typeof activity.updatedAt === "string" && Number.isFinite(Date.parse(activity.updatedAt));
96
126
  };
127
+ const unread = async () => {
128
+ if (input.hook_event_name !== "SessionStart") return unreadMessages(dir);
129
+ let messages = 0;
130
+ for (const channel of await sessionStartWakeChannels(filesForDir(dir), io.env)) {
131
+ if (channel.connected && (await readJson(channel.files.mode))?.mode !== "async") messages += await unreadChannelMessages(channel.files.dir);
132
+ }
133
+ return messages;
134
+ };
97
135
  while (io.now().getTime() < deadline) {
98
136
  if (!await owns() || !parentAlive(parent)) return 0;
99
- if (await idle() && await unreadMessages(dir) > 0) {
100
- if (await unreadMessages(dir) > 0 && await idle() && await owns() && parentAlive(parent) && io.now().getTime() < deadline) {
137
+ if (input.hook_event_name === "SessionStart" && !(await sessionStartWakeChannels(filesForDir(dir), io.env)).length) return 0;
138
+ if (await idle() && await unread() > 0) {
139
+ if (await unread() > 0 && await idle() && await owns() && parentAlive(parent) && io.now().getTime() < deadline) {
101
140
  io.stderr.write(NOTICE);
141
+ state = "woke";
102
142
  return 2;
103
143
  }
104
144
  }
105
145
  await sleep(Math.min(poll, Math.max(0, deadline - io.now().getTime())));
106
146
  }
147
+ state = "expired";
107
148
  } catch {
108
149
  } finally {
150
+ if (finish) {
151
+ try {
152
+ await finish();
153
+ } catch {
154
+ }
155
+ }
109
156
  if (temporary) {
110
157
  try {
111
158
  await fs.unlink(temporary);
@@ -1,7 +1,7 @@
1
1
  import { createRequire as __khalaCreateRequire } from 'node:module'; const require = __khalaCreateRequire(import.meta.url);
2
2
  import {
3
- HARNESSES
4
- } from "./chunk-SN3STAO2.mjs";
3
+ isHarnessId
4
+ } from "./chunk-NKX3SLS7.mjs";
5
5
 
6
6
  // src/state.ts
7
7
  import { constants } from "node:fs";
@@ -71,7 +71,7 @@ async function removeJoinFile(files, link) {
71
71
  }
72
72
  }
73
73
  function sessionFiles(harness, sessionId, env) {
74
- if (!HARNESSES.includes(harness) || !SESSION_ID_PATTERN.test(sessionId)) throw new StateError("invalid_session_id");
74
+ if (!isHarnessId(harness) || !SESSION_ID_PATTERN.test(sessionId)) throw new StateError("invalid_session_id");
75
75
  return filesForDir(path.join(stateRoot(env), harness, sessionId));
76
76
  }
77
77
  function resolveStateDir(harness, sessionId, env) {
@@ -80,9 +80,9 @@ function resolveStateDir(harness, sessionId, env) {
80
80
  async function ensureStateDir(dir) {
81
81
  try {
82
82
  let sessionDir = dir;
83
- if (path.basename(path.dirname(dir)) === "channels" && /^[a-f0-9]{24}$/.test(path.basename(dir)) && HARNESSES.includes(path.basename(path.dirname(path.dirname(path.dirname(dir)))))) {
83
+ if (path.basename(path.dirname(dir)) === "channels" && /^[a-f0-9]{24}$/.test(path.basename(dir)) && isHarnessId(path.basename(path.dirname(path.dirname(path.dirname(dir))))) && path.basename(path.dirname(path.dirname(path.dirname(path.dirname(dir))))) === "khala") {
84
84
  sessionDir = path.dirname(path.dirname(dir));
85
- } else if (["channels", "joins"].includes(path.basename(dir)) && HARNESSES.includes(path.basename(path.dirname(path.dirname(dir))))) {
85
+ } else if (["channels", "joins"].includes(path.basename(dir)) && isHarnessId(path.basename(path.dirname(path.dirname(dir)))) && path.basename(path.dirname(path.dirname(path.dirname(dir)))) === "khala") {
86
86
  sessionDir = path.dirname(dir);
87
87
  }
88
88
  const directories = [path.dirname(path.dirname(sessionDir)), path.dirname(sessionDir), sessionDir];
@@ -4,14 +4,14 @@ import {
4
4
  CHANNEL_EVENT_TYPE,
5
5
  encodeChannelEvent,
6
6
  memberRenameContent
7
- } from "./chunk-P5CP32X2.mjs";
7
+ } from "./chunk-54XFVSEC.mjs";
8
8
  import {
9
9
  LISTENING_MODE_COMMAND_TYPE,
10
10
  LISTENING_MODE_MEMBER_KEY,
11
11
  memberListeningMode
12
- } from "./chunk-AUJEUKVM.mjs";
13
- import "./chunk-SN3STAO2.mjs";
14
- import "./chunk-55KBLEX5.mjs";
12
+ } from "./chunk-IQJQ4RF5.mjs";
13
+ import "./chunk-NKX3SLS7.mjs";
14
+ import "./chunk-7HBRFY3Z.mjs";
15
15
 
16
16
  // src/matrix/session.ts
17
17
  import { createClient, ClientEvent, RoomEvent, MatrixEventEvent, SyncState, Direction, Method, EventType } from "matrix-js-sdk";
@@ -1,22 +1,21 @@
1
1
  import { createRequire as __khalaCreateRequire } from 'node:module'; const require = __khalaCreateRequire(import.meta.url);
2
2
  import {
3
- adapterFor
4
- } from "./chunk-WHONG7UF.mjs";
5
- import {
6
- readListeningMode
7
- } from "./chunk-ZHMGB3PP.mjs";
3
+ adapterFor,
4
+ readListeningMode,
5
+ resolveSources
6
+ } from "./chunk-5BQTYU66.mjs";
8
7
  import {
9
8
  listChannels,
10
9
  readCursor,
11
10
  readEntries
12
- } from "./chunk-XUJIJ4KB.mjs";
11
+ } from "./chunk-VKQ3KUYO.mjs";
13
12
  import {
14
13
  SESSION_ID_PATTERN,
15
14
  readJson,
16
15
  readStatus,
17
16
  sessionFiles,
18
17
  writeJsonAtomic
19
- } from "./chunk-OW3DSTEZ.mjs";
18
+ } from "./chunk-CBPLCCRZ.mjs";
20
19
 
21
20
  // src/watch.ts
22
21
  import * as fs from "node:fs/promises";
@@ -35,13 +34,11 @@ function resolveHarness(argv, env) {
35
34
  if (isHarness(env.KHALA_MCP_HARNESS)) return env.KHALA_MCP_HARNESS;
36
35
  return env.CLAUDE_CODE_SESSION_ID !== void 0 ? "claude" : "codex";
37
36
  }
38
- function resolveSessionId(harness, meta, env) {
39
- let value;
40
- for (const source of adapterFor(harness)?.sessionSources ?? []) {
41
- value = source(meta, env);
42
- if (value !== void 0 && value !== null) break;
43
- }
44
- return typeof value === "string" && value.match(/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/)?.[0] === value ? value : null;
37
+ async function resolveSession(harness, meta, env, context = {}) {
38
+ return resolveSources(adapterFor(harness)?.sessionSources ?? [], meta, env, { ...context, harness });
39
+ }
40
+ async function resolveSessionId(harness, meta, env) {
41
+ return (await resolveSession(harness, meta, env))?.sessionId ?? null;
45
42
  }
46
43
 
47
44
  // src/watch.ts
@@ -207,9 +204,9 @@ ${WATCH_USAGE}`);
207
204
  };
208
205
  if (argv.length !== 0 && !(argv.length === 2 && argv[0] === "--session") && !(argv.length === 4 && argv[0] === "--harness" && argv[2] === "--session")) return invalid("invalid_arguments");
209
206
  const harness = resolveHarness(argv, process.env);
210
- if (harness === "invalid") return invalid("invalid_harness");
207
+ if (harness === "invalid" || !adapterFor(harness)?.codec) return invalid("invalid_harness");
211
208
  const index = argv.indexOf("--session");
212
- const id = index === -1 ? resolveSessionId(harness, void 0, process.env) : argv[index + 1];
209
+ const id = index === -1 ? await resolveSessionId(harness, void 0, process.env) : argv[index + 1];
213
210
  if (!id) return invalid("session_unknown");
214
211
  if (!SESSION_ID_PATTERN.test(id)) return invalid("invalid_session_id");
215
212
  const files = sessionFiles(harness, id, process.env);
@@ -229,7 +226,7 @@ ${WATCH_USAGE}`);
229
226
 
230
227
  export {
231
228
  resolveHarness,
232
- resolveSessionId,
229
+ resolveSession,
233
230
  monitorArmed,
234
231
  mentions,
235
232
  watchSession,
@@ -0,0 +1,40 @@
1
+ import { createRequire as __khalaCreateRequire } from 'node:module'; const require = __khalaCreateRequire(import.meta.url);
2
+ import {
3
+ listChannels
4
+ } from "./chunk-VKQ3KUYO.mjs";
5
+ import {
6
+ TERMINAL_SESSION_DETAILS,
7
+ readStateFile,
8
+ readStatus
9
+ } from "./chunk-CBPLCCRZ.mjs";
10
+ import {
11
+ validAgentRejoinSecret
12
+ } from "./chunk-NKX3SLS7.mjs";
13
+
14
+ // src/session-start-wake.ts
15
+ import { createHash } from "node:crypto";
16
+ import path from "node:path";
17
+ async function sessionStartWakeChannels(files, env) {
18
+ const channels = await listChannels(files);
19
+ const secret = (await readStateFile(files.dir, "rejoin.json"))?.secret;
20
+ const hash = validAgentRejoinSecret(secret) ? createHash("sha256").update(secret).digest("hex") : void 0;
21
+ const candidates = channels.length ? channels : [{ files, roomId: void 0 }];
22
+ const result = [];
23
+ for (const channel of candidates) {
24
+ const status = await readStatus(channel.files);
25
+ if (TERMINAL_SESSION_DETAILS.some((detail) => detail === status?.detail)) continue;
26
+ if (status?.state === "connected" || status?.state === "send_failed") {
27
+ result.push({ files: channel.files, connected: true });
28
+ continue;
29
+ }
30
+ if (!channel.roomId || !hash || !["joining", "disconnected"].includes(status?.state ?? "")) continue;
31
+ const authorization = await readStateFile(channel.files.dir, "resume.json");
32
+ if (!authorization || authorization.roomId !== channel.roomId || authorization.workspace !== path.resolve(env.PWD ?? process.cwd()) || authorization.secretHash !== hash || typeof authorization.link !== "string" || typeof authorization.label !== "string" || authorization.localCredentials && (authorization.localCredentials.transport !== "local" || authorization.localCredentials.roomId !== channel.roomId)) continue;
33
+ result.push({ files: channel.files, connected: false });
34
+ }
35
+ return result;
36
+ }
37
+
38
+ export {
39
+ sessionStartWakeChannels
40
+ };