khala-cli 0.4.9 → 0.4.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/THIRD_PARTY_NOTICES.txt +28 -0
- package/dist/{chunk-NIX5F4LT.mjs → chunk-2AMWQ7RB.mjs} +330 -66
- package/dist/{chunk-PAJ4JJPV.mjs → chunk-3E5DH3Y6.mjs} +43 -6
- package/dist/{chunk-NGSYOVQ4.mjs → chunk-4BP7V3OP.mjs} +2 -2
- package/dist/{chunk-TEFOQXBK.mjs → chunk-4BZQ4GVW.mjs} +3 -3
- package/dist/{chunk-ITFD3JZS.mjs → chunk-5JSGA6OB.mjs} +424 -78
- package/dist/{chunk-QKWEZXR3.mjs → chunk-6AQVW5W7.mjs} +12 -4
- package/dist/{chunk-UK5GJO4Y.mjs → chunk-6J2PUM7L.mjs} +6 -5
- package/dist/{chunk-P3YIN7G7.mjs → chunk-6SUV3EW6.mjs} +11 -10
- package/dist/{chunk-GSZADJMW.mjs → chunk-ARIDZOTI.mjs} +309 -81
- package/dist/chunk-C2WEDMAF.mjs +15 -0
- package/dist/{chunk-DJM23SIU.mjs → chunk-CHC4VZF5.mjs} +7 -8
- package/dist/{chunk-BLCRVRIK.mjs → chunk-CKLCR5G4.mjs} +456 -87
- package/dist/{chunk-7R3EIDGR.mjs → chunk-EVT2DWNZ.mjs} +1 -1
- package/dist/{chunk-KMLXOPBQ.mjs → chunk-FJVVSKZ3.mjs} +4 -2
- package/dist/{chunk-3KDOTR54.mjs → chunk-G3NMB4PY.mjs} +1 -1
- package/dist/{chunk-SSO5RFOX.mjs → chunk-HHEZVPRR.mjs} +1 -1
- package/dist/{chunk-BV45NK7K.mjs → chunk-JKOJQTFT.mjs} +100 -6
- package/dist/{chunk-T7IL7V7Q.mjs → chunk-NDMFD5IM.mjs} +13 -10
- package/dist/{chunk-TANCKTDK.mjs → chunk-OEV5DFLW.mjs} +70 -5
- package/dist/{chunk-BEO7WIIM.mjs → chunk-OIZ4R5H7.mjs} +28 -8
- package/dist/{chunk-FZEH2IXZ.mjs → chunk-PJKI7SRI.mjs} +176 -26
- package/dist/{chunk-NPSQNCOY.mjs → chunk-PN25BUQ4.mjs} +13 -4
- package/dist/{chunk-3R6HFSLL.mjs → chunk-QDPJ4VT2.mjs} +92 -38
- package/dist/{chunk-FO5GXCST.mjs → chunk-SRZTHCXP.mjs} +5 -5
- package/dist/{chunk-LBT47HLZ.mjs → chunk-TSK3HJEG.mjs} +10 -9
- package/dist/{chunk-6BO3JSUP.mjs → chunk-UX34IVN5.mjs} +2 -2
- package/dist/{chunk-V4F3SYKW.mjs → chunk-VAC3JTAV.mjs} +25 -17
- package/dist/{chunk-PDZ5Z5ZE.mjs → chunk-VDZK4LW3.mjs} +24 -9
- package/dist/chunk-W6HM6F2U.mjs +901 -0
- package/dist/{chunk-WDOHRV3D.mjs → chunk-WPNMWNCL.mjs} +14 -14
- package/dist/chunk-WUDXHF2Q.mjs +14 -0
- package/dist/{chunk-XBQDIKB5.mjs → chunk-WZJ6DQJV.mjs} +9 -8
- package/dist/{chunk-ANSR4IAW.mjs → chunk-XR625T3I.mjs} +2 -2
- package/dist/{chunk-K7V7X6WU.mjs → chunk-ZSKLNZWZ.mjs} +21 -4
- package/dist/iterm2_send.py +56 -0
- package/dist/khala.mjs +11 -11
- package/dist/web/assets/local-CSPuGnH2.css +1 -0
- package/dist/web/assets/local-Dtyxaacx.js +9 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/chunk-47XNLAJG.mjs +0 -38
- package/dist/chunk-XUCWSYVS.mjs +0 -148
- package/dist/web/assets/local-CFzRHZpB.js +0 -9
- package/dist/web/assets/local-ciypUFoK.css +0 -1
|
@@ -317,6 +317,34 @@ SOFTWARE.
|
|
|
317
317
|
|
|
318
318
|
|
|
319
319
|
|
|
320
|
+
smol-toml@1.8.0 — LICENSE
|
|
321
|
+
|
|
322
|
+
Copyright (c) Squirrel Chat et al., All rights reserved.
|
|
323
|
+
|
|
324
|
+
Redistribution and use in source and binary forms, with or without
|
|
325
|
+
modification, are permitted provided that the following conditions are met:
|
|
326
|
+
|
|
327
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
328
|
+
list of conditions and the following disclaimer.
|
|
329
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
330
|
+
this list of conditions and the following disclaimer in the
|
|
331
|
+
documentation and/or other materials provided with the distribution.
|
|
332
|
+
3. Neither the name of the copyright holder nor the names of its contributors
|
|
333
|
+
may be used to endorse or promote products derived from this software without
|
|
334
|
+
specific prior written permission.
|
|
335
|
+
|
|
336
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
337
|
+
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
338
|
+
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
339
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
340
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
341
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
342
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
343
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
344
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
345
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
346
|
+
|
|
347
|
+
|
|
320
348
|
sync-promise-expanded@2.0.0 — LICENSE
|
|
321
349
|
|
|
322
350
|
This is free and unencumbered software released into the public domain.
|
|
@@ -2,40 +2,47 @@ import { createRequire as __khalaCreateRequire } from 'node:module'; const requi
|
|
|
2
2
|
import {
|
|
3
3
|
DEFAULT_LISTENING_MODE,
|
|
4
4
|
decodeListeningMode
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-4BP7V3OP.mjs";
|
|
6
6
|
import {
|
|
7
7
|
advanceCursor,
|
|
8
|
+
isWakeEntry,
|
|
8
9
|
listChannels,
|
|
10
|
+
mentions,
|
|
9
11
|
readActivity,
|
|
10
12
|
readCursor,
|
|
11
13
|
readEntries,
|
|
12
|
-
unread
|
|
13
|
-
|
|
14
|
+
unread,
|
|
15
|
+
wakesAgent
|
|
16
|
+
} from "./chunk-OEV5DFLW.mjs";
|
|
14
17
|
import {
|
|
15
18
|
SESSION_ID_PATTERN,
|
|
16
19
|
StateError,
|
|
20
|
+
ensureStateDir,
|
|
17
21
|
readJson,
|
|
18
22
|
readWatcherStatus,
|
|
19
23
|
sessionFiles,
|
|
20
24
|
stateRoot,
|
|
21
25
|
writeJsonAtomic
|
|
22
|
-
} from "./chunk-
|
|
26
|
+
} from "./chunk-PN25BUQ4.mjs";
|
|
23
27
|
import {
|
|
28
|
+
cancelAttempt,
|
|
29
|
+
confirmWakeText,
|
|
30
|
+
failAttempt,
|
|
24
31
|
readWakeState,
|
|
25
32
|
settleAttempts,
|
|
26
33
|
wakeLine,
|
|
27
34
|
withWakeLock
|
|
28
|
-
} from "./chunk-
|
|
35
|
+
} from "./chunk-ZSKLNZWZ.mjs";
|
|
29
36
|
import {
|
|
30
37
|
init_define_KHALA_BUNDLE
|
|
31
|
-
} from "./chunk-
|
|
38
|
+
} from "./chunk-EVT2DWNZ.mjs";
|
|
32
39
|
|
|
33
40
|
// src/watch.ts
|
|
34
41
|
init_define_KHALA_BUNDLE();
|
|
35
|
-
import * as
|
|
42
|
+
import * as fs7 from "node:fs/promises";
|
|
36
43
|
import { watch as watchDirectory } from "node:fs";
|
|
37
|
-
import
|
|
38
|
-
import { randomUUID as
|
|
44
|
+
import path6 from "node:path";
|
|
45
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
39
46
|
|
|
40
47
|
// src/monitor-storage.ts
|
|
41
48
|
init_define_KHALA_BUNDLE();
|
|
@@ -110,11 +117,11 @@ async function readWakeSettings(root) {
|
|
|
110
117
|
}
|
|
111
118
|
async function writeWakeSettings(root, settings) {
|
|
112
119
|
await fs2.mkdir(root, { recursive: true, mode: 448 });
|
|
113
|
-
const
|
|
114
|
-
const temp = `${
|
|
120
|
+
const target2 = path2.join(root, "wake-settings.json");
|
|
121
|
+
const temp = `${target2}.${randomBytes(8).toString("hex")}.tmp`;
|
|
115
122
|
try {
|
|
116
123
|
await fs2.writeFile(temp, JSON.stringify(settings), { mode: 384 });
|
|
117
|
-
await fs2.rename(temp,
|
|
124
|
+
await fs2.rename(temp, target2);
|
|
118
125
|
} finally {
|
|
119
126
|
await fs2.rm(temp, { force: true });
|
|
120
127
|
}
|
|
@@ -178,6 +185,19 @@ function nodeScriptCommand(platform, node, script) {
|
|
|
178
185
|
return `${quote(node)} ${quote(script)}`;
|
|
179
186
|
}
|
|
180
187
|
|
|
188
|
+
// src/install/launcher.ts
|
|
189
|
+
init_define_KHALA_BUNDLE();
|
|
190
|
+
function installedLauncher(platform, path7, home, env) {
|
|
191
|
+
const windows = platform === "win32";
|
|
192
|
+
const candidate = windows ? env.LOCALAPPDATA : env.XDG_DATA_HOME;
|
|
193
|
+
const dataHome = candidate && path7.isAbsolute(candidate) ? candidate : windows ? path7.join(home, "AppData", "Local") : path7.join(home, ".local", "share");
|
|
194
|
+
const prefix = path7.join(dataHome, "khala", "npm");
|
|
195
|
+
return { prefix, bin: windows ? path7.join(prefix, "khala.cmd") : path7.join(prefix, "bin", "khala") };
|
|
196
|
+
}
|
|
197
|
+
function launcherShellQuote(platform, bin) {
|
|
198
|
+
return platform === "win32" ? `"${bin}"` : `'${bin.replaceAll("'", `'\\''`)}'`;
|
|
199
|
+
}
|
|
200
|
+
|
|
181
201
|
// src/install/managed-file.ts
|
|
182
202
|
init_define_KHALA_BUNDLE();
|
|
183
203
|
import * as fs3 from "node:fs/promises";
|
|
@@ -352,18 +372,17 @@ async function missingDirs(dir) {
|
|
|
352
372
|
// src/install/cursor.ts
|
|
353
373
|
var CURSOR_HOOK_EVENTS = ["beforeSubmitPrompt", "postToolUse", "stop"];
|
|
354
374
|
var HOOK_SUFFIX = " hook deliver --harness cursor";
|
|
355
|
-
function cursorPaths({ platform, path:
|
|
356
|
-
const cursorDir =
|
|
375
|
+
function cursorPaths({ platform, path: path7, home, env }, packageName = "khala-cli") {
|
|
376
|
+
const cursorDir = path7.join(home, ".cursor");
|
|
357
377
|
const windows = platform === "win32";
|
|
358
|
-
const
|
|
359
|
-
const
|
|
360
|
-
const modules = windows ? path6.join(prefix, "node_modules") : path6.join(prefix, "lib", "node_modules");
|
|
378
|
+
const { prefix } = installedLauncher(platform, path7, home, env);
|
|
379
|
+
const modules = windows ? path7.join(prefix, "node_modules") : path7.join(prefix, "lib", "node_modules");
|
|
361
380
|
return {
|
|
362
381
|
cursorDir,
|
|
363
382
|
prefix,
|
|
364
|
-
mcpFile:
|
|
365
|
-
hooksFile:
|
|
366
|
-
script:
|
|
383
|
+
mcpFile: path7.join(cursorDir, "mcp.json"),
|
|
384
|
+
hooksFile: path7.join(cursorDir, "hooks.json"),
|
|
385
|
+
script: path7.join(modules, ...packageName.split("/"), "dist", "khala.mjs")
|
|
367
386
|
};
|
|
368
387
|
}
|
|
369
388
|
function cursorHookCommand(platform, node, script) {
|
|
@@ -465,11 +484,12 @@ function museCliPath(env = process.env) {
|
|
|
465
484
|
const { prefix } = cursorPaths({ platform: process.platform, path: path4, home, env });
|
|
466
485
|
return process.platform === "win32" ? path4.join(prefix, "khala.cmd") : path4.join(prefix, "bin", "khala");
|
|
467
486
|
}
|
|
468
|
-
function museWatchCommand(sessionId, bin = museCliPath()) {
|
|
469
|
-
|
|
487
|
+
function museWatchCommand(sessionId, bin = museCliPath(), platform = process.platform) {
|
|
488
|
+
const quote = platform === "win32" ? (value) => `'${value.replaceAll("'", "''")}'` : shellQuote;
|
|
489
|
+
return `${platform === "win32" ? "& " : ""}${quote(bin)} watch --harness muse --session ${quote(sessionId ?? "<current session id from khala_status>")}`;
|
|
470
490
|
}
|
|
471
491
|
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;
|
|
492
|
+
return `start Muse's monitor tool with command ${JSON.stringify(command)}, persistent: true, wake_delay_ms: 0, show_lines: true. Always pass the current session id explicitly; the monitor shell does not inherit MUSE_SESSION_ID. Keep one monitor per session; If it prints "Do not re-arm", report the reason and do not restart it. Notifications are wake notices, not channel messages. Do not call khala_read or reply to the wake notice; Khala hooks supply the <khala-channel-messages> frame. End the turn so the Stop hook can deliver the frame in sync mode; in steer mode it can arrive after a tool. Use khala_read only for history or owner-requested manual reads; if you already replied to a message from history, do not reply again when the same message appears in a hook frame. Never run the watcher as a foreground shell command.`;
|
|
473
493
|
}
|
|
474
494
|
function createMuseMonitorDriver() {
|
|
475
495
|
return {
|
|
@@ -569,15 +589,243 @@ async function museWakeProof(files, sessionId, env, turnId) {
|
|
|
569
589
|
return void 0;
|
|
570
590
|
}
|
|
571
591
|
|
|
592
|
+
// src/wake/qwen-socket.ts
|
|
593
|
+
init_define_KHALA_BUNDLE();
|
|
594
|
+
import * as fs6 from "node:fs/promises";
|
|
595
|
+
import net from "node:net";
|
|
596
|
+
import os3 from "node:os";
|
|
597
|
+
import path5 from "node:path";
|
|
598
|
+
import { randomBytes as randomBytes2, randomUUID as randomUUID2 } from "node:crypto";
|
|
599
|
+
|
|
600
|
+
// src/wake/qwen-transcript.ts
|
|
601
|
+
init_define_KHALA_BUNDLE();
|
|
602
|
+
import * as fs5 from "node:fs/promises";
|
|
603
|
+
async function qwenNotification(filePath) {
|
|
604
|
+
const file = await fs5.open(filePath, "r");
|
|
605
|
+
let text;
|
|
606
|
+
try {
|
|
607
|
+
const stat3 = await file.stat();
|
|
608
|
+
const start = Math.max(0, stat3.size - 1024 * 1024);
|
|
609
|
+
const buffer = Buffer.alloc(Math.min(stat3.size, 1024 * 1024));
|
|
610
|
+
const { bytesRead } = await file.read(buffer, 0, buffer.length, start);
|
|
611
|
+
text = buffer.subarray(0, bytesRead).toString("utf8");
|
|
612
|
+
if (start) text = text.slice(text.indexOf("\n") + 1);
|
|
613
|
+
} finally {
|
|
614
|
+
await file.close();
|
|
615
|
+
}
|
|
616
|
+
const notifications = [];
|
|
617
|
+
for (const line of text.trimEnd().split("\n").reverse()) {
|
|
618
|
+
let entry;
|
|
619
|
+
try {
|
|
620
|
+
entry = JSON.parse(line);
|
|
621
|
+
} catch {
|
|
622
|
+
continue;
|
|
623
|
+
}
|
|
624
|
+
if (entry?.type !== "user" || entry.provenance !== "system" || entry.subtype !== "notification" || entry.deliveredTurn !== true || entry.message?.role !== "user" || !Array.isArray(entry.message.parts)) continue;
|
|
625
|
+
notifications.push(entry.message.parts.filter((part) => typeof part?.text === "string").map((part) => part.text).join("\n"));
|
|
626
|
+
}
|
|
627
|
+
return notifications.length ? notifications.join("\n") : void 0;
|
|
628
|
+
}
|
|
629
|
+
async function verifyQwenTranscript(dir, filePath, now) {
|
|
630
|
+
const text = await qwenNotification(filePath);
|
|
631
|
+
if (text !== void 0) await confirmWakeText(dir, "socket", text, now);
|
|
632
|
+
return text;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// src/wake/qwen-socket.ts
|
|
636
|
+
var qwenHome = (env) => env.QWEN_HOME ? path5.resolve(env.QWEN_HOME) : path5.join(env.HOME ?? os3.homedir(), ".qwen");
|
|
637
|
+
async function qwenInboundAllowed(env, workspace = env.PWD ?? process.cwd()) {
|
|
638
|
+
for (const file of [path5.join(qwenHome(env), "settings.json"), path5.join(workspace, ".qwen", "settings.json")]) {
|
|
639
|
+
const config = await readJson(file);
|
|
640
|
+
if (["hold", "refuse"].includes(config?.agents?.crossSessionInbound ?? "") || config?.agents?.crossSessionMessaging === false) return false;
|
|
641
|
+
}
|
|
642
|
+
return true;
|
|
643
|
+
}
|
|
644
|
+
var qwenControllerFile = (env) => path5.join(stateRoot(env), "qwen", "controller.json");
|
|
645
|
+
async function qwenSession(env) {
|
|
646
|
+
const socket = env.QWEN_CODE_MESSAGING_SOCKET;
|
|
647
|
+
if (!socket) return void 0;
|
|
648
|
+
try {
|
|
649
|
+
const dir = path5.join(qwenHome(env), "sessions");
|
|
650
|
+
for (const file of await fs6.readdir(dir)) {
|
|
651
|
+
if (!/^\d+\.json$/u.test(file)) continue;
|
|
652
|
+
const record = await readJson(path5.join(dir, file));
|
|
653
|
+
if (record?.ipcPath === socket && typeof record.sessionId === "string" && SESSION_ID_PATTERN.test(record.sessionId)) return record.sessionId;
|
|
654
|
+
}
|
|
655
|
+
} catch {
|
|
656
|
+
}
|
|
657
|
+
return void 0;
|
|
658
|
+
}
|
|
659
|
+
async function target(env) {
|
|
660
|
+
const sessionId = await qwenSession(env);
|
|
661
|
+
if (!sessionId) return void 0;
|
|
662
|
+
try {
|
|
663
|
+
const file = qwenControllerFile(env);
|
|
664
|
+
const stat3 = await fs6.lstat(file);
|
|
665
|
+
if (!stat3.isFile() || (stat3.mode & 63) !== 0) return void 0;
|
|
666
|
+
const credential = await readJson(file);
|
|
667
|
+
if (!credential?.token || !/^qpc_[0-9a-f]{64}$/u.test(credential.token)) return void 0;
|
|
668
|
+
return { socket: env.QWEN_CODE_MESSAGING_SOCKET, sessionId, token: credential.token };
|
|
669
|
+
} catch {
|
|
670
|
+
return void 0;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
function qwenFrames(target2, line, from, replyToken, msgId) {
|
|
674
|
+
return JSON.stringify({ msgV: 1, type: "auth", token: target2.token }) + "\n" + JSON.stringify({
|
|
675
|
+
msgV: 1,
|
|
676
|
+
msgId,
|
|
677
|
+
type: "user",
|
|
678
|
+
from,
|
|
679
|
+
replyToken,
|
|
680
|
+
fromName: "khala",
|
|
681
|
+
fromMode: "prompting",
|
|
682
|
+
toSessionId: target2.sessionId,
|
|
683
|
+
priority: "next",
|
|
684
|
+
message: { role: "user", content: line }
|
|
685
|
+
}) + "\n";
|
|
686
|
+
}
|
|
687
|
+
async function sendQwenWake(target2, line, signal, options = {}) {
|
|
688
|
+
if (signal.aborted) return "failed";
|
|
689
|
+
const dir = await fs6.mkdtemp(path5.join(options.runtimeDir ?? os3.tmpdir(), "khala-qw-"));
|
|
690
|
+
const from = path5.join(dir, "r.sock");
|
|
691
|
+
if (Buffer.byteLength(from) > 103) {
|
|
692
|
+
await fs6.rm(dir, { recursive: true, force: true });
|
|
693
|
+
return "failed";
|
|
694
|
+
}
|
|
695
|
+
const replyToken = randomBytes2(32).toString("hex");
|
|
696
|
+
const msgId = randomUUID2();
|
|
697
|
+
const peers = /* @__PURE__ */ new Set();
|
|
698
|
+
let sender;
|
|
699
|
+
let finish;
|
|
700
|
+
const result = new Promise((resolve) => {
|
|
701
|
+
finish = resolve;
|
|
702
|
+
});
|
|
703
|
+
const aborted = () => finish("failed");
|
|
704
|
+
const server = net.createServer((socket) => {
|
|
705
|
+
peers.add(socket);
|
|
706
|
+
socket.on("close", () => peers.delete(socket));
|
|
707
|
+
socket.on("error", () => {
|
|
708
|
+
});
|
|
709
|
+
let buffer = "", authenticated = false;
|
|
710
|
+
socket.setEncoding("utf8");
|
|
711
|
+
socket.on("data", (chunk) => {
|
|
712
|
+
buffer += chunk;
|
|
713
|
+
if (buffer.length > 64 * 1024) {
|
|
714
|
+
socket.destroy();
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
let at;
|
|
718
|
+
while ((at = buffer.indexOf("\n")) >= 0) {
|
|
719
|
+
const text = buffer.slice(0, at);
|
|
720
|
+
buffer = buffer.slice(at + 1);
|
|
721
|
+
let frame;
|
|
722
|
+
try {
|
|
723
|
+
frame = JSON.parse(text);
|
|
724
|
+
} catch {
|
|
725
|
+
socket.destroy();
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
if (!authenticated) {
|
|
729
|
+
authenticated = frame?.msgV === 1 && frame.type === "auth" && frame.token === replyToken;
|
|
730
|
+
if (!authenticated) {
|
|
731
|
+
socket.destroy();
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
} else if (frame?.msgV === 1 && frame.type === "control" && frame.action === "delivery_status" && frame.origMsgId === msgId && ["delivered", "held", "refused", "denied", "expired", "misaddressed", "dropped"].includes(frame.status)) {
|
|
735
|
+
finish(frame.status);
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
});
|
|
739
|
+
});
|
|
740
|
+
server.on("error", () => finish("failed"));
|
|
741
|
+
signal.addEventListener("abort", aborted, { once: true });
|
|
742
|
+
const timer = setTimeout(() => finish("failed"), options.timeoutMs ?? 5e3);
|
|
743
|
+
try {
|
|
744
|
+
await new Promise((resolve, reject) => {
|
|
745
|
+
server.once("error", reject);
|
|
746
|
+
server.listen(from, resolve);
|
|
747
|
+
});
|
|
748
|
+
await fs6.chmod(from, 384);
|
|
749
|
+
if (signal.aborted) return "failed";
|
|
750
|
+
sender = net.createConnection(target2.socket);
|
|
751
|
+
sender.on("error", () => finish("failed"));
|
|
752
|
+
sender.on("connect", () => {
|
|
753
|
+
sender.end(qwenFrames(target2, line, from, replyToken, msgId));
|
|
754
|
+
});
|
|
755
|
+
return await result;
|
|
756
|
+
} catch {
|
|
757
|
+
return "failed";
|
|
758
|
+
} finally {
|
|
759
|
+
clearTimeout(timer);
|
|
760
|
+
signal.removeEventListener("abort", aborted);
|
|
761
|
+
sender?.destroy();
|
|
762
|
+
for (const socket of peers) socket.destroy();
|
|
763
|
+
await new Promise((resolve) => server.close(() => resolve()));
|
|
764
|
+
await fs6.rm(dir, { recursive: true, force: true });
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
function createQwenSocketDriver(deps = {}) {
|
|
768
|
+
const resolve = deps.resolve ?? target;
|
|
769
|
+
const policy = async (env) => {
|
|
770
|
+
const config = await readJson(path5.join(qwenHome(env), "settings.json"));
|
|
771
|
+
return config?.agents;
|
|
772
|
+
};
|
|
773
|
+
const reason = async (ctx) => {
|
|
774
|
+
const config = await policy(ctx.env);
|
|
775
|
+
if (config?.crossSessionInbound === "hold") return "qwen_held";
|
|
776
|
+
if ((deps.platform ?? process.platform) === "win32") return "windows";
|
|
777
|
+
const receipt = await readJson(path5.join(ctx.files.dir, "qwen-receipt.json"));
|
|
778
|
+
if (receipt?.status === "held" && config?.crossSessionInbound !== "accept") return "qwen_held";
|
|
779
|
+
if (config?.crossSessionMessaging === false || config?.crossSessionInbound === "refuse") return "qwen_refused";
|
|
780
|
+
if (!deps.resolve && !ctx.env.QWEN_CODE_MESSAGING_SOCKET) return "qwen_session_missing";
|
|
781
|
+
return await resolve(ctx.env) ? void 0 : "qwen_socket_missing";
|
|
782
|
+
};
|
|
783
|
+
return {
|
|
784
|
+
id: "socket",
|
|
785
|
+
rung: 1,
|
|
786
|
+
optIn: false,
|
|
787
|
+
minIdleMs: 0,
|
|
788
|
+
deadlineMs: 3e4,
|
|
789
|
+
verification: "transcript",
|
|
790
|
+
async verify(ctx) {
|
|
791
|
+
const journal = await readJson(path5.join(ctx.files.dir, "wake-journal.json"));
|
|
792
|
+
const receipt = await readJson(path5.join(ctx.files.dir, "qwen-receipt.json"));
|
|
793
|
+
if (!journal?.attempts?.some((attempt) => attempt.driver === "socket") && receipt?.status !== "held") return;
|
|
794
|
+
const transcript = await readJson(path5.join(ctx.files.dir, "qwen-transcript.json"));
|
|
795
|
+
if (!transcript?.path) return;
|
|
796
|
+
try {
|
|
797
|
+
const text = await verifyQwenTranscript(ctx.files.dir, transcript.path, ctx.now);
|
|
798
|
+
if (receipt?.status === "held" && receipt.nonce && text?.includes(`(k-${receipt.nonce})`)) {
|
|
799
|
+
await writeJsonAtomic(path5.join(ctx.files.dir, "qwen-receipt.json"), { status: "delivered" });
|
|
800
|
+
}
|
|
801
|
+
} catch {
|
|
802
|
+
}
|
|
803
|
+
},
|
|
804
|
+
available: async (ctx) => !await reason(ctx),
|
|
805
|
+
unavailableReason: reason,
|
|
806
|
+
async wake(ctx, line) {
|
|
807
|
+
if (ctx.signal.aborted || await reason(ctx)) return "skipped";
|
|
808
|
+
const destination = await resolve(ctx.env);
|
|
809
|
+
if (!destination || destination.sessionId !== ctx.sessionId) return "skipped";
|
|
810
|
+
const status = await (deps.send ?? sendQwenWake)(destination, line, ctx.signal);
|
|
811
|
+
await ensureStateDir(ctx.files.dir);
|
|
812
|
+
const nonce = line.match(/\(k-([0-9a-f]{8})\)$/u)?.[1];
|
|
813
|
+
await writeJsonAtomic(path5.join(ctx.files.dir, "qwen-receipt.json"), { status, nonce });
|
|
814
|
+
if (nonce && status === "held") await cancelAttempt(ctx.files.dir, nonce);
|
|
815
|
+
else if (nonce && status !== "delivered") await failAttempt(ctx.files.dir, nonce, ctx.now);
|
|
816
|
+
}
|
|
817
|
+
};
|
|
818
|
+
}
|
|
819
|
+
|
|
572
820
|
// src/watch.ts
|
|
573
821
|
var MARKER = "monitor.json";
|
|
574
822
|
var OBSERVATION = "monitor-cursor.json";
|
|
575
823
|
var NONCE_PATTERN = /^[a-f0-9-]{36}$/;
|
|
576
|
-
var leaseFile = (dir, nonce) =>
|
|
824
|
+
var leaseFile = (dir, nonce) => path6.join(dir, `monitor-${nonce}.json`);
|
|
577
825
|
async function monitorOwner(files) {
|
|
578
826
|
const owners = [];
|
|
579
827
|
for (const dir of monitorStorageCandidates(files)) {
|
|
580
|
-
const owner = await readJson(
|
|
828
|
+
const owner = await readJson(path6.join(dir, MARKER));
|
|
581
829
|
if (!owner || !Number.isSafeInteger(owner.pid) || owner.pid <= 0 || typeof owner.nonce !== "string" || !NONCE_PATTERN.test(owner.nonce)) continue;
|
|
582
830
|
const lease = await readJson(leaseFile(dir, owner.nonce));
|
|
583
831
|
if (lease?.nonce !== owner.nonce) continue;
|
|
@@ -598,17 +846,19 @@ async function monitorOwner(files) {
|
|
|
598
846
|
async function monitorArmed(files) {
|
|
599
847
|
return !!await monitorOwner(files);
|
|
600
848
|
}
|
|
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
849
|
function channelLabel(name) {
|
|
607
850
|
return Array.from(name.replace(/[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/gu, " ").trim()).slice(0, 80).join("");
|
|
608
851
|
}
|
|
609
852
|
async function watchSession(files, io) {
|
|
610
|
-
const nonce =
|
|
853
|
+
const nonce = randomUUID3();
|
|
611
854
|
let storage = files.dir;
|
|
855
|
+
const env = io.env ?? process.env;
|
|
856
|
+
const qwen = io.harness === "qwen";
|
|
857
|
+
const wakeAllowed = async () => {
|
|
858
|
+
if (!qwen) return true;
|
|
859
|
+
const wake = await readWakeSettings(stateRoot(env));
|
|
860
|
+
return await qwenInboundAllowed(env) && !wake.off["qwen/socket"] && !wake.off["qwen/background-shell"];
|
|
861
|
+
};
|
|
612
862
|
let watcher;
|
|
613
863
|
let timer;
|
|
614
864
|
let running;
|
|
@@ -626,7 +876,7 @@ async function watchSession(files, io) {
|
|
|
626
876
|
code = exit;
|
|
627
877
|
watcher?.close();
|
|
628
878
|
clearInterval(timer);
|
|
629
|
-
io.stderr?.(`khala watch: stopped (${reason}). Do not re-arm.`);
|
|
879
|
+
if (!qwen || exit !== 0) io.stderr?.(`khala watch: stopped (${reason}). Do not re-arm.`);
|
|
630
880
|
finish();
|
|
631
881
|
};
|
|
632
882
|
let lastHeartbeat = 0;
|
|
@@ -647,19 +897,19 @@ async function watchSession(files, io) {
|
|
|
647
897
|
}
|
|
648
898
|
storage = await monitorStorage(files);
|
|
649
899
|
const storageDirs = /* @__PURE__ */ new Map([[files.dir, storage]]);
|
|
650
|
-
const observationDir = async (
|
|
651
|
-
let dir = storageDirs.get(
|
|
900
|
+
const observationDir = async (target2) => {
|
|
901
|
+
let dir = storageDirs.get(target2.dir);
|
|
652
902
|
if (!dir) {
|
|
653
|
-
dir = await monitorStorage(
|
|
654
|
-
storageDirs.set(
|
|
903
|
+
dir = await monitorStorage(target2);
|
|
904
|
+
storageDirs.set(target2.dir, dir);
|
|
655
905
|
}
|
|
656
906
|
return dir;
|
|
657
907
|
};
|
|
658
908
|
const initialDelivered = /* @__PURE__ */ new Map();
|
|
659
|
-
const seedBaseline = async (
|
|
660
|
-
initialDelivered.set(
|
|
909
|
+
const seedBaseline = async (target2, session) => {
|
|
910
|
+
initialDelivered.set(target2.dir, {
|
|
661
911
|
identity: JSON.stringify([session.roomId, session.userId]),
|
|
662
|
-
cursor: await readCursor(
|
|
912
|
+
cursor: await readCursor(target2)
|
|
663
913
|
});
|
|
664
914
|
};
|
|
665
915
|
if (initialSession) await seedBaseline(files, initialSession);
|
|
@@ -682,68 +932,75 @@ async function watchSession(files, io) {
|
|
|
682
932
|
lastHeartbeat = Date.now();
|
|
683
933
|
await writeJsonAtomic(leaseFile(storage, nonce), { nonce, pid: process.pid, heartbeatAt: new Date(lastHeartbeat).toISOString() });
|
|
684
934
|
}
|
|
935
|
+
if (!await wakeAllowed()) return;
|
|
685
936
|
for (const channel of await listChannels(files)) {
|
|
686
|
-
const
|
|
687
|
-
const session = await readJson(
|
|
688
|
-
const status2 = await readWatcherStatus(
|
|
937
|
+
const target2 = channel.files;
|
|
938
|
+
const session = await readJson(target2.session);
|
|
939
|
+
const status2 = await readWatcherStatus(target2);
|
|
689
940
|
if (!session || session.roomId !== channel.roomId || typeof session.userId !== "string" || !["connected", "send_failed"].includes(status2?.state ?? "")) continue;
|
|
690
941
|
const identity = JSON.stringify([session.roomId, session.userId]);
|
|
691
|
-
if (initialDelivered.get(
|
|
692
|
-
await seedBaseline(
|
|
942
|
+
if (initialDelivered.get(target2.dir)?.identity !== identity) {
|
|
943
|
+
await seedBaseline(target2, session);
|
|
693
944
|
}
|
|
694
|
-
const mode = await readListeningMode(
|
|
695
|
-
const entries = await readEntries(
|
|
696
|
-
const observed = await readJson(
|
|
945
|
+
const mode = await readListeningMode(target2);
|
|
946
|
+
const entries = await readEntries(target2);
|
|
947
|
+
const observed = await readJson(path6.join(await observationDir(target2), OBSERVATION));
|
|
697
948
|
const position = observed?.userId === session.userId && observed?.roomId === session.roomId && Number.isSafeInteger(observed.count) && observed.count >= 0 && observed.count <= entries.length && (observed.count === 0 || entries[observed.count - 1]?.eventId === observed.lastEventId) ? observed.count : 0;
|
|
698
|
-
const baseline = initialDelivered.get(
|
|
949
|
+
const baseline = initialDelivered.get(target2.dir).cursor;
|
|
699
950
|
const initialCount = baseline.deliveredCount === 0 || entries[baseline.deliveredCount - 1]?.eventId === baseline.lastDeliveredEventId ? baseline.deliveredCount : 0;
|
|
700
951
|
const fresh = entries.slice(Math.max(initialCount, position));
|
|
701
|
-
const latestMode = await readListeningMode(
|
|
702
|
-
const latestSession = await readJson(
|
|
703
|
-
const latestStatus = await readWatcherStatus(
|
|
952
|
+
const latestMode = await readListeningMode(target2);
|
|
953
|
+
const latestSession = await readJson(target2.session);
|
|
954
|
+
const latestStatus = await readWatcherStatus(target2);
|
|
704
955
|
if (!await ownsSession()) {
|
|
705
956
|
stop(2, exitReason);
|
|
706
957
|
return;
|
|
707
958
|
}
|
|
959
|
+
if (!await wakeAllowed()) return;
|
|
708
960
|
if (latestSession?.userId !== session.userId || latestSession?.roomId !== session.roomId || !["connected", "send_failed"].includes(latestStatus?.state ?? "")) continue;
|
|
709
|
-
const messages = fresh.filter((entry) => entry
|
|
710
|
-
if (io.harness !== "muse" && !done && mode !== "async" && latestMode !== "async" && messages.
|
|
961
|
+
const messages = fresh.filter((entry) => isWakeEntry(entry) && entry.sender !== session.userId && entry.roomId === session.roomId);
|
|
962
|
+
if (io.harness !== "muse" && !done && mode !== "async" && latestMode !== "async" && messages.some((entry) => wakesAgent(entry, latestMode, latestStatus?.displayName))) {
|
|
711
963
|
const count = messages.filter((entry) => mentions(entry, latestStatus?.displayName)).length;
|
|
712
964
|
io.write(`khala: ${messages.length} new message${messages.length === 1 ? "" : "s"} in #${channelLabel(latestStatus?.channelName ?? channel.channelName ?? "channel")} (${count} mentions you)
|
|
713
965
|
`);
|
|
966
|
+
if (qwen) {
|
|
967
|
+
stop();
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
714
970
|
}
|
|
715
|
-
if (position !== entries.length && !done && await ownsSession() && JSON.stringify(await readJson(
|
|
971
|
+
if (position !== entries.length && !done && await ownsSession() && JSON.stringify(await readJson(target2.session)) === JSON.stringify(latestSession)) {
|
|
716
972
|
try {
|
|
717
|
-
await writeJsonAtomic(
|
|
973
|
+
await writeJsonAtomic(path6.join(await observationDir(target2), OBSERVATION), {
|
|
718
974
|
userId: session.userId,
|
|
719
975
|
roomId: session.roomId,
|
|
720
976
|
count: entries.length,
|
|
721
977
|
lastEventId: entries.at(-1)?.eventId ?? null
|
|
722
978
|
});
|
|
723
979
|
} catch (error) {
|
|
724
|
-
if (await readJson(
|
|
980
|
+
if (await readJson(target2.session)) throw error;
|
|
725
981
|
}
|
|
726
982
|
}
|
|
727
983
|
}
|
|
728
984
|
if (io.harness === "muse" && !done && await ownsSession()) {
|
|
729
|
-
const request = await readJson(
|
|
985
|
+
const request = await readJson(path6.join(files.dir, MUSE_WAKE_REQUEST));
|
|
730
986
|
const now = (io.now ?? Date.now)();
|
|
731
987
|
const wakeNonce = typeof request?.line === "string" ? /\(k-([a-f0-9]{8})\)$/.exec(request.line)?.[1] : void 0;
|
|
732
988
|
if (request?.owner === nonce && wakeNonce && wakeLine(wakeNonce) === request.line && request.line !== emittedRequest && now >= request.at && now < request.deadline && await driverAllowed(stateRoot(io.env ?? process.env), "muse", "monitor", false) && !(await readWakeState(files.dir)).monitor?.disabled && (await readActivity(files)).state === "idle") {
|
|
733
989
|
const channels = await listChannels(files);
|
|
734
990
|
let eligible = false;
|
|
735
991
|
for (const channel of channels) {
|
|
736
|
-
|
|
992
|
+
const channelMode = await readListeningMode(channel.files);
|
|
993
|
+
if (channelMode === "async") continue;
|
|
737
994
|
const session = await readJson(channel.files.session);
|
|
738
995
|
const status2 = await readWatcherStatus(channel.files);
|
|
739
996
|
if (!["connected", "send_failed"].includes(status2?.state ?? "")) continue;
|
|
740
997
|
const cursor = await readCursor(channel.files);
|
|
741
|
-
eligible ||= (await readEntries(channel.files)).slice(cursor.deliveredCount).some((entry) => entry.
|
|
998
|
+
eligible ||= (await readEntries(channel.files)).slice(cursor.deliveredCount).some((entry) => entry.sender !== session?.userId && wakesAgent(entry, channelMode, status2?.displayName));
|
|
742
999
|
}
|
|
743
1000
|
const activity = await readActivity(files);
|
|
744
1001
|
if (eligible && await ownsSession() && activity.state === "idle") {
|
|
745
1002
|
const epoch = activity.updatedAt;
|
|
746
|
-
const dedupFile =
|
|
1003
|
+
const dedupFile = path6.join(storage, "monitor-wake-observed.json");
|
|
747
1004
|
const observed = await readJson(dedupFile);
|
|
748
1005
|
if (observed?.epoch === epoch) return;
|
|
749
1006
|
await writeJsonAtomic(dedupFile, { epoch, line: request.line });
|
|
@@ -774,7 +1031,7 @@ async function watchSession(files, io) {
|
|
|
774
1031
|
watcher.on("error", (error) => stop(3, errorReason(error)));
|
|
775
1032
|
lastHeartbeat = Date.now();
|
|
776
1033
|
await writeJsonAtomic(leaseFile(storage, nonce), { nonce, pid: process.pid, heartbeatAt: new Date(lastHeartbeat).toISOString() });
|
|
777
|
-
await writeJsonAtomic(
|
|
1034
|
+
await writeJsonAtomic(path6.join(storage, MARKER), { nonce, pid: process.pid, startedAt: Date.now() });
|
|
778
1035
|
published = true;
|
|
779
1036
|
timer = setInterval(notify, 100);
|
|
780
1037
|
io.signal.addEventListener("abort", abort, { once: true });
|
|
@@ -790,7 +1047,7 @@ async function watchSession(files, io) {
|
|
|
790
1047
|
clearInterval(timer);
|
|
791
1048
|
watcher?.close();
|
|
792
1049
|
io.signal.removeEventListener("abort", abort);
|
|
793
|
-
await
|
|
1050
|
+
await fs7.unlink(leaseFile(storage, nonce)).catch((error) => {
|
|
794
1051
|
if (!["ENOENT", "EACCES", "EROFS"].includes(error.code ?? "")) throw error;
|
|
795
1052
|
});
|
|
796
1053
|
}
|
|
@@ -807,8 +1064,8 @@ ${WATCH_USAGE}`);
|
|
|
807
1064
|
return 1;
|
|
808
1065
|
};
|
|
809
1066
|
if (argv.length !== 0 && !(argv.length === 2 && ["--session", "--harness"].includes(argv[0])) && !(argv.length === 4 && argv[0] === "--harness" && argv[2] === "--session")) return invalid("invalid_arguments");
|
|
810
|
-
const { resolveHarness, resolveSessionId } = await import("./chunk-
|
|
811
|
-
const { adapterFor } = await import("./chunk-
|
|
1067
|
+
const { resolveHarness, resolveSessionId } = await import("./chunk-TSK3HJEG.mjs");
|
|
1068
|
+
const { adapterFor } = await import("./chunk-WZJ6DQJV.mjs");
|
|
812
1069
|
const harness = resolveHarness(argv, process.env);
|
|
813
1070
|
if (harness === "invalid" || !adapterFor(harness)?.codec) return invalid("invalid_harness");
|
|
814
1071
|
const index = argv.indexOf("--session");
|
|
@@ -823,7 +1080,7 @@ ${WATCH_USAGE}`);
|
|
|
823
1080
|
try {
|
|
824
1081
|
return await watchSession(files, { write: (line) => {
|
|
825
1082
|
process.stdout.write(line);
|
|
826
|
-
}, signal: controller.signal, harness, stderr: (line) => {
|
|
1083
|
+
}, signal: controller.signal, harness, env: process.env, stderr: (line) => {
|
|
827
1084
|
process.stderr.write(line + "\n");
|
|
828
1085
|
} });
|
|
829
1086
|
} finally {
|
|
@@ -841,17 +1098,24 @@ export {
|
|
|
841
1098
|
CURSOR_WORKSPACE_ENV,
|
|
842
1099
|
CURSOR_DEFAULT_SESSION,
|
|
843
1100
|
cursorSessionId,
|
|
1101
|
+
installedLauncher,
|
|
1102
|
+
launcherShellQuote,
|
|
844
1103
|
formatJson,
|
|
845
1104
|
jsonFormat,
|
|
846
1105
|
textFormat,
|
|
847
1106
|
readManaged,
|
|
848
1107
|
ManagedFiles,
|
|
1108
|
+
verifyQwenTranscript,
|
|
1109
|
+
qwenHome,
|
|
1110
|
+
qwenInboundAllowed,
|
|
1111
|
+
qwenControllerFile,
|
|
1112
|
+
qwenSession,
|
|
1113
|
+
createQwenSocketDriver,
|
|
849
1114
|
nodeScriptCommand,
|
|
850
1115
|
cursorPaths,
|
|
851
1116
|
installCursor,
|
|
852
1117
|
monitorOwner,
|
|
853
1118
|
monitorArmed,
|
|
854
|
-
mentions,
|
|
855
1119
|
watchSession,
|
|
856
1120
|
WATCH_USAGE,
|
|
857
1121
|
run,
|