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.
- package/README.md +18 -1
- package/dist/{chunk-2U4E7AM7.mjs → chunk-2DHIAFEV.mjs} +294 -101
- package/dist/{chunk-CWHGNVZV.mjs → chunk-2VCAW4GH.mjs} +1 -1
- package/dist/{chunk-P5CP32X2.mjs → chunk-54XFVSEC.mjs} +1 -1
- package/dist/chunk-5BQTYU66.mjs +886 -0
- package/dist/{chunk-RRBIVI77.mjs → chunk-5LIE5GUU.mjs} +1 -1
- package/dist/{chunk-6E3UCO22.mjs → chunk-5PUQT3YS.mjs} +200 -20
- package/dist/{chunk-55KBLEX5.mjs → chunk-7HBRFY3Z.mjs} +1 -1
- package/dist/{chunk-PFDUXY3N.mjs → chunk-APMWDF6B.mjs} +1 -1
- package/dist/{chunk-PY2ZNVNY.mjs → chunk-BKLW2SPG.mjs} +55 -8
- package/dist/{chunk-OW3DSTEZ.mjs → chunk-CBPLCCRZ.mjs} +5 -5
- package/dist/{chunk-JG5J3SHW.mjs → chunk-CJTCU755.mjs} +4 -4
- package/dist/{chunk-AWBKVTLV.mjs → chunk-FH34B3I3.mjs} +14 -17
- package/dist/chunk-FV32AQTC.mjs +40 -0
- package/dist/{chunk-FH2CZX5T.mjs → chunk-H3K4GA2P.mjs} +83 -36
- package/dist/{chunk-AUJEUKVM.mjs → chunk-IQJQ4RF5.mjs} +1 -1
- package/dist/{chunk-SN3STAO2.mjs → chunk-NKX3SLS7.mjs} +40 -3
- package/dist/{chunk-Z4EDYB6Y.mjs → chunk-RNDUFTBF.mjs} +97 -57
- package/dist/{chunk-OECJ45NM.mjs → chunk-VAI7IUI4.mjs} +24 -8
- package/dist/{chunk-XUJIJ4KB.mjs → chunk-VKQ3KUYO.mjs} +16 -2
- package/dist/{chunk-4X35OUAN.mjs → chunk-VUSV2ADL.mjs} +92 -97
- package/dist/{chunk-2XTYRBYX.mjs → chunk-W6R4UJUQ.mjs} +12 -5
- package/dist/{chunk-B2MOLCJM.mjs → chunk-Z4JF2M5C.mjs} +7 -9
- package/dist/{chunk-SZIEG3RS.mjs → chunk-ZJ4W7P33.mjs} +12 -14
- package/dist/khala.mjs +11 -11
- package/dist/web/assets/local-BQl2KrE1.js +9 -0
- package/dist/web/assets/local-OwDq47O4.css +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/chunk-DPW43YNC.mjs +0 -36
- package/dist/chunk-IULQYQJZ.mjs +0 -144
- package/dist/chunk-WHONG7UF.mjs +0 -267
- package/dist/chunk-ZHMGB3PP.mjs +0 -82
- package/dist/web/assets/local-K8EcTVxW.js +0 -9
- 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-
|
|
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-
|
|
4
|
+
} from "./chunk-APMWDF6B.mjs";
|
|
5
5
|
import {
|
|
6
|
+
CURSOR_WORKSPACE_ENV,
|
|
6
7
|
adapterFor
|
|
7
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-5BQTYU66.mjs";
|
|
9
|
+
import "./chunk-VKQ3KUYO.mjs";
|
|
10
|
+
import "./chunk-CBPLCCRZ.mjs";
|
|
11
|
+
import "./chunk-IQJQ4RF5.mjs";
|
|
8
12
|
import {
|
|
9
|
-
|
|
10
|
-
} from "./chunk-
|
|
11
|
-
import "./chunk-
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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(
|
|
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
|
|
526
|
+
await fs3.mkdir(codexHome, { recursive: true });
|
|
349
527
|
if (!uninstall) {
|
|
350
528
|
try {
|
|
351
|
-
await
|
|
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
|
|
358
|
-
await
|
|
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
|
-
}))(
|
|
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.
|
|
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-
|
|
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
|
|
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
|
|
80
|
-
const
|
|
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
|
-
|
|
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 (
|
|
100
|
-
|
|
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
|
-
|
|
4
|
-
} from "./chunk-
|
|
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 (!
|
|
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)) &&
|
|
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)) &&
|
|
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-
|
|
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-
|
|
13
|
-
import "./chunk-
|
|
14
|
-
import "./chunk-
|
|
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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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-
|
|
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-
|
|
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
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
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
|
+
};
|