khala-cli 0.4.18 → 0.4.19
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/{chunk-VK7ZSKQZ.mjs → chunk-2CM6MFLQ.mjs} +8 -7
- package/dist/chunk-5T2T2BGP.mjs +25 -0
- package/dist/{chunk-TYRNBET6.mjs → chunk-6CUBZHAI.mjs} +2 -2
- package/dist/{chunk-XXKDMHR2.mjs → chunk-7BR55Y3K.mjs} +2 -2
- package/dist/{chunk-ELI3YCFF.mjs → chunk-7JLJ2IVR.mjs} +19 -81
- package/dist/{chunk-4WHRO5ZU.mjs → chunk-A76X5SLZ.mjs} +9 -7
- package/dist/{chunk-UA6DEWBA.mjs → chunk-BUFSSWDE.mjs} +1 -1
- package/dist/{chunk-SRUPGV2G.mjs → chunk-CR2MQIB4.mjs} +11 -13
- package/dist/{chunk-FDSHLUCQ.mjs → chunk-DP24LQAW.mjs} +44 -22
- package/dist/{chunk-IESMCVXV.mjs → chunk-ERWPSB27.mjs} +1 -1
- package/dist/{chunk-YAXWRS5U.mjs → chunk-I3UIC3DD.mjs} +2 -2
- package/dist/chunk-IH5ALDEX.mjs +17 -0
- package/dist/{chunk-RJZ6FAFC.mjs → chunk-JALXEI56.mjs} +3 -3
- package/dist/{chunk-WM75XLND.mjs → chunk-K7YTGI7R.mjs} +6 -6
- package/dist/chunk-LOVF37KO.mjs +72 -0
- package/dist/{chunk-FCOL5HBH.mjs → chunk-LUQ2K5K2.mjs} +10 -12
- package/dist/chunk-LZXXWDKB.mjs +18 -0
- package/dist/{chunk-3RCRNCG3.mjs → chunk-ME3G775Q.mjs} +140 -5
- package/dist/{chunk-QYVSNP2P.mjs → chunk-NDII2VEP.mjs} +1 -1
- package/dist/{chunk-BG4JGKQG.mjs → chunk-NYMXWKOF.mjs} +2 -2
- package/dist/{chunk-LOGBYBPA.mjs → chunk-OFLK3DF2.mjs} +1 -1
- package/dist/{chunk-RWT4I2XJ.mjs → chunk-OPBCYCDJ.mjs} +23 -25
- package/dist/{chunk-OZ5C2ZMB.mjs → chunk-POSW5KU5.mjs} +10 -12
- package/dist/{chunk-43THPTMD.mjs → chunk-PQ6Y7QRL.mjs} +7 -7
- package/dist/{chunk-KCWHDC7S.mjs → chunk-PXNKY5ZM.mjs} +8 -10
- package/dist/{chunk-GVIPYERE.mjs → chunk-Q2ENNHOG.mjs} +3 -3
- package/dist/{chunk-HOKMRMNP.mjs → chunk-RYBGLMUZ.mjs} +2 -2
- package/dist/{chunk-MTDLNIUY.mjs → chunk-TJUM3DPB.mjs} +1 -1
- package/dist/{chunk-6ZL6OFW4.mjs → chunk-TZLAEDD2.mjs} +5 -5
- package/dist/{chunk-H2X64ISL.mjs → chunk-UI5OJ2TY.mjs} +3 -3
- package/dist/{chunk-5IZ3WN3S.mjs → chunk-V5SWPOLN.mjs} +53 -53
- package/dist/{chunk-JXV5S7FC.mjs → chunk-WOHXD3RS.mjs} +433 -129
- package/dist/{chunk-4IOLQB34.mjs → chunk-YN334EWZ.mjs} +1 -1
- package/dist/khala.mjs +11 -11
- package/dist/web/assets/{local-3eMi9596.js → local-SRORuW7r.js} +2 -2
- package/dist/web/index.html +1 -1
- package/package.json +1 -1
- package/dist/chunk-4FUYRD3G.mjs +0 -27
- package/dist/chunk-AU2FBBUY.mjs +0 -20
- package/dist/chunk-EBKZETYL.mjs +0 -142
- package/dist/chunk-KXLGRISD.mjs +0 -19
- package/dist/chunk-LNDZI73C.mjs +0 -323
|
@@ -1,31 +1,27 @@
|
|
|
1
1
|
import { createRequire as __khalaCreateRequire } from 'node:module'; const require = __khalaCreateRequire(import.meta.url);
|
|
2
2
|
import {
|
|
3
3
|
DEFAULT_LISTENING_MODE,
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
listChannels,
|
|
10
|
-
markNudged,
|
|
11
|
-
mentions,
|
|
12
|
-
readActivity,
|
|
13
|
-
readCursor,
|
|
14
|
-
readEntries,
|
|
15
|
-
unread,
|
|
16
|
-
wakeStart,
|
|
17
|
-
wakesAgent
|
|
18
|
-
} from "./chunk-LNDZI73C.mjs";
|
|
4
|
+
decodeChannelEvent,
|
|
5
|
+
decodeListeningMode,
|
|
6
|
+
formatChannelEventLine,
|
|
7
|
+
wakesOn
|
|
8
|
+
} from "./chunk-ME3G775Q.mjs";
|
|
19
9
|
import {
|
|
20
10
|
SESSION_ID_PATTERN,
|
|
21
11
|
StateError,
|
|
12
|
+
channelFiles,
|
|
13
|
+
channelsDir,
|
|
22
14
|
ensureStateDir,
|
|
15
|
+
filesForDir,
|
|
23
16
|
readJson,
|
|
24
17
|
readWatcherStatus,
|
|
18
|
+
removeStateFile,
|
|
25
19
|
sessionFiles,
|
|
20
|
+
stateKey,
|
|
26
21
|
stateRoot,
|
|
27
|
-
writeJsonAtomic
|
|
28
|
-
|
|
22
|
+
writeJsonAtomic,
|
|
23
|
+
writeStateFile
|
|
24
|
+
} from "./chunk-YN334EWZ.mjs";
|
|
29
25
|
import {
|
|
30
26
|
cancelAttempt,
|
|
31
27
|
confirmWakeText,
|
|
@@ -34,16 +30,16 @@ import {
|
|
|
34
30
|
settleAttempts,
|
|
35
31
|
wakeLine,
|
|
36
32
|
withWakeLock
|
|
37
|
-
} from "./chunk-
|
|
33
|
+
} from "./chunk-OFLK3DF2.mjs";
|
|
38
34
|
import {
|
|
39
35
|
init_define_KHALA_BUNDLE
|
|
40
|
-
} from "./chunk-
|
|
36
|
+
} from "./chunk-NDII2VEP.mjs";
|
|
41
37
|
|
|
42
38
|
// src/watch.ts
|
|
43
39
|
init_define_KHALA_BUNDLE();
|
|
44
|
-
import * as
|
|
40
|
+
import * as fs10 from "node:fs/promises";
|
|
45
41
|
import { watch as watchDirectory } from "node:fs";
|
|
46
|
-
import
|
|
42
|
+
import path9 from "node:path";
|
|
47
43
|
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
48
44
|
|
|
49
45
|
// src/monitor-storage.ts
|
|
@@ -76,6 +72,137 @@ async function monitorStorage(files) {
|
|
|
76
72
|
return dir;
|
|
77
73
|
}
|
|
78
74
|
|
|
75
|
+
// src/inbox.ts
|
|
76
|
+
init_define_KHALA_BUNDLE();
|
|
77
|
+
import * as fs2 from "node:fs/promises";
|
|
78
|
+
|
|
79
|
+
// src/events/receive.ts
|
|
80
|
+
init_define_KHALA_BUNDLE();
|
|
81
|
+
function toEventInboxEntry(base, rawContent, you) {
|
|
82
|
+
const decoded = decodeChannelEvent(rawContent);
|
|
83
|
+
if (!decoded.ok) return null;
|
|
84
|
+
const content = decoded.value;
|
|
85
|
+
const to = content.kind === "handoff" ? String(content.data?.to ?? "").replace(/^@/u, "").toLowerCase() : "";
|
|
86
|
+
return {
|
|
87
|
+
entry: {
|
|
88
|
+
...base,
|
|
89
|
+
kind: "event",
|
|
90
|
+
body: formatChannelEventLine(content) + (content.url ? " " + content.url : ""),
|
|
91
|
+
eventKind: content.kind,
|
|
92
|
+
...content.data ? { data: content.data } : {},
|
|
93
|
+
...to && to === you?.toLowerCase() ? { mention: true } : {}
|
|
94
|
+
},
|
|
95
|
+
...content.key !== void 0 ? { key: content.key } : {}
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
function createEventKeyFilter() {
|
|
99
|
+
const seen = /* @__PURE__ */ new Set();
|
|
100
|
+
return (key) => {
|
|
101
|
+
if (key === void 0) return true;
|
|
102
|
+
if (seen.has(key)) return false;
|
|
103
|
+
seen.add(key);
|
|
104
|
+
return true;
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
function isWakeEntry(entry) {
|
|
108
|
+
return entry.kind === "message" || entry.mention === true;
|
|
109
|
+
}
|
|
110
|
+
var agentGroupMention = /(?<![\p{L}\p{N}_@])@(?:channel|agents)(?![\p{L}\p{N}_])/iu;
|
|
111
|
+
function mentions(entry, name) {
|
|
112
|
+
if (entry.kind === "message" && entry.senderKind !== "agent" && agentGroupMention.test(entry.body)) return true;
|
|
113
|
+
if (!name) return false;
|
|
114
|
+
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
115
|
+
return new RegExp(`(?<![\\p{L}\\p{N}_])@?${escaped}(?![\\p{L}\\p{N}_])`, "iu").test(entry.body);
|
|
116
|
+
}
|
|
117
|
+
function wakesAgent(entry, mode, you) {
|
|
118
|
+
if (!isWakeEntry(entry)) return false;
|
|
119
|
+
return wakesOn(mode ?? "steer", entry.mention === true || mentions(entry, you), entry.senderKind === "agent");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// src/inbox.ts
|
|
123
|
+
async function readEntries(files) {
|
|
124
|
+
let content;
|
|
125
|
+
try {
|
|
126
|
+
content = await fs2.readFile(files.inbox, "utf8");
|
|
127
|
+
} catch (error) {
|
|
128
|
+
if (error.code === "ENOENT") return [];
|
|
129
|
+
throw new StateError("storage_failed");
|
|
130
|
+
}
|
|
131
|
+
const entries = [];
|
|
132
|
+
for (const line of content.split("\n").slice(0, -1)) {
|
|
133
|
+
try {
|
|
134
|
+
const entry = JSON.parse(line);
|
|
135
|
+
if (entry && ["eventId", "roomId", "ts", "sender", "senderLabel", "body"].every((key) => typeof entry[key] === "string") && ["human", "agent", "unknown"].includes(entry.senderKind) && ["message", "event"].includes(entry.kind)) entries.push(entry);
|
|
136
|
+
} catch {
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return entries;
|
|
140
|
+
}
|
|
141
|
+
async function appendEntries(files, entries) {
|
|
142
|
+
const seen = new Set((await readEntries(files)).map((entry) => entry.eventId));
|
|
143
|
+
const fresh = entries.filter((entry) => {
|
|
144
|
+
if (seen.has(entry.eventId)) return false;
|
|
145
|
+
seen.add(entry.eventId);
|
|
146
|
+
return true;
|
|
147
|
+
});
|
|
148
|
+
if (fresh.length) {
|
|
149
|
+
try {
|
|
150
|
+
await fs2.appendFile(files.inbox, fresh.map((entry) => JSON.stringify(entry) + "\n").join(""), { mode: 384, flag: "a" });
|
|
151
|
+
} catch {
|
|
152
|
+
throw new StateError("storage_failed");
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return fresh;
|
|
156
|
+
}
|
|
157
|
+
async function readCursor(files) {
|
|
158
|
+
const cursor = await readJson(files.cursor);
|
|
159
|
+
return cursor && (cursor.lastDeliveredEventId === null || typeof cursor.lastDeliveredEventId === "string") && Number.isSafeInteger(cursor.deliveredCount) && cursor.deliveredCount >= 0 ? cursor : { lastDeliveredEventId: null, deliveredCount: 0 };
|
|
160
|
+
}
|
|
161
|
+
async function unread(files) {
|
|
162
|
+
const cursor = await readCursor(files);
|
|
163
|
+
const entries = await readEntries(files);
|
|
164
|
+
return { entries: entries.slice(cursor.deliveredCount), cursor };
|
|
165
|
+
}
|
|
166
|
+
function wakeStart(cursor) {
|
|
167
|
+
return Math.max(cursor.deliveredCount, Number.isSafeInteger(cursor.nudgedCount) ? cursor.nudgedCount : 0);
|
|
168
|
+
}
|
|
169
|
+
async function unwoken(files) {
|
|
170
|
+
return (await readEntries(files)).slice(wakeStart(await readCursor(files)));
|
|
171
|
+
}
|
|
172
|
+
async function markNudged(files, expected, count) {
|
|
173
|
+
const current = await readCursor(files);
|
|
174
|
+
if (current.deliveredCount !== expected.deliveredCount || current.lastDeliveredEventId !== expected.lastDeliveredEventId) return;
|
|
175
|
+
await writeJsonAtomic(files.cursor, { ...current, nudgedCount: count });
|
|
176
|
+
}
|
|
177
|
+
async function unreadCounts(files) {
|
|
178
|
+
const { entries } = await unread(files);
|
|
179
|
+
return { total: entries.length, messages: entries.filter((entry) => isWakeEntry(entry)).length };
|
|
180
|
+
}
|
|
181
|
+
async function advanceCursor(files, expected, delivered, shown = false) {
|
|
182
|
+
const current = await readCursor(files);
|
|
183
|
+
if (current.deliveredCount !== expected.deliveredCount || current.lastDeliveredEventId !== expected.lastDeliveredEventId) return "conflict";
|
|
184
|
+
if (!delivered.length) return expected;
|
|
185
|
+
const entries = await readEntries(files);
|
|
186
|
+
const slice = entries.slice(expected.deliveredCount, expected.deliveredCount + delivered.length);
|
|
187
|
+
if (slice.length !== delivered.length || slice.some((entry, index) => entry.eventId !== delivered[index]?.eventId)) return "conflict";
|
|
188
|
+
const last = delivered[delivered.length - 1].eventId;
|
|
189
|
+
const shownEventId = shown ? last : current.shownEventId;
|
|
190
|
+
const next = {
|
|
191
|
+
lastDeliveredEventId: last,
|
|
192
|
+
deliveredCount: expected.deliveredCount + delivered.length,
|
|
193
|
+
...shownEventId ? { shownEventId } : {},
|
|
194
|
+
...wakeStart(current) > expected.deliveredCount + delivered.length ? { nudgedCount: current.nudgedCount } : {}
|
|
195
|
+
};
|
|
196
|
+
await writeJsonAtomic(files.cursor, next);
|
|
197
|
+
return next;
|
|
198
|
+
}
|
|
199
|
+
async function appendInbox(dir, entry) {
|
|
200
|
+
return (await appendEntries(filesForDir(dir), [entry])).length === 1;
|
|
201
|
+
}
|
|
202
|
+
function unreadCount(dir) {
|
|
203
|
+
return unreadCounts(filesForDir(dir));
|
|
204
|
+
}
|
|
205
|
+
|
|
79
206
|
// src/mode.ts
|
|
80
207
|
init_define_KHALA_BUNDLE();
|
|
81
208
|
import path2 from "node:path";
|
|
@@ -127,17 +254,164 @@ async function applyListeningMode(files, next, meta, now = () => /* @__PURE__ */
|
|
|
127
254
|
return { previous, next };
|
|
128
255
|
}
|
|
129
256
|
|
|
257
|
+
// src/channels.ts
|
|
258
|
+
init_define_KHALA_BUNDLE();
|
|
259
|
+
import * as fs3 from "node:fs/promises";
|
|
260
|
+
import path3 from "node:path";
|
|
261
|
+
function channelKey(roomId2) {
|
|
262
|
+
return stateKey(roomId2);
|
|
263
|
+
}
|
|
264
|
+
async function exists(file) {
|
|
265
|
+
try {
|
|
266
|
+
await fs3.lstat(file);
|
|
267
|
+
return true;
|
|
268
|
+
} catch (error) {
|
|
269
|
+
if (error.code === "ENOENT") return false;
|
|
270
|
+
throw new StateError("storage_failed");
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
function roomId(value) {
|
|
274
|
+
if (value && typeof value === "object" && "roomId" in value && typeof value.roomId === "string" && value.roomId.length) return value.roomId;
|
|
275
|
+
return void 0;
|
|
276
|
+
}
|
|
277
|
+
async function legacyRoomId(files) {
|
|
278
|
+
const saved = roomId(await readJson(files.session));
|
|
279
|
+
if (saved) return saved;
|
|
280
|
+
let content;
|
|
281
|
+
try {
|
|
282
|
+
content = await fs3.readFile(files.inbox, "utf8");
|
|
283
|
+
} catch (error) {
|
|
284
|
+
if (error.code === "ENOENT") return void 0;
|
|
285
|
+
throw new StateError("storage_failed");
|
|
286
|
+
}
|
|
287
|
+
try {
|
|
288
|
+
return roomId(JSON.parse(content.split("\n")[0]));
|
|
289
|
+
} catch {
|
|
290
|
+
return void 0;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
async function listChannels(files) {
|
|
294
|
+
await ensureStateDir(files.dir);
|
|
295
|
+
const channels = [];
|
|
296
|
+
const dir = channelsDir(files);
|
|
297
|
+
if (await exists(dir)) {
|
|
298
|
+
await ensureStateDir(dir);
|
|
299
|
+
for (const key of await fs3.readdir(dir)) {
|
|
300
|
+
if (!/^[a-f0-9]{24}$/.test(key)) continue;
|
|
301
|
+
const nested = filesForDir(path3.join(dir, key));
|
|
302
|
+
await ensureStateDir(nested.dir);
|
|
303
|
+
const metadata = await readJson(path3.join(nested.dir, "channel.json"));
|
|
304
|
+
const id = roomId(metadata);
|
|
305
|
+
if (!id || channelKey(id) !== key) continue;
|
|
306
|
+
channels.push({ key, roomId: id, ...typeof metadata?.channelName === "string" ? { channelName: metadata.channelName } : {}, files: nested, legacy: false });
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
if (await exists(files.inbox)) {
|
|
310
|
+
const id = await legacyRoomId(files);
|
|
311
|
+
if (id) {
|
|
312
|
+
const status = await readJson(files.status);
|
|
313
|
+
channels.push({ key: channelKey(id), roomId: id, ...typeof status?.channelName === "string" ? { channelName: status.channelName } : {}, files, legacy: true });
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return channels.sort((a, b) => (a.channelName ?? "").localeCompare(b.channelName ?? "") || a.roomId.localeCompare(b.roomId));
|
|
317
|
+
}
|
|
318
|
+
async function migrateLegacy(files) {
|
|
319
|
+
await ensureStateDir(files.dir);
|
|
320
|
+
const names = ["cursor.json", "mode.json", "inbox.jsonl"];
|
|
321
|
+
const present = await Promise.all(names.map((name) => exists(path3.join(files.dir, name))));
|
|
322
|
+
if (!present.some(Boolean)) return "none";
|
|
323
|
+
const id = await legacyRoomId(files);
|
|
324
|
+
if (!id) {
|
|
325
|
+
for (const name of names) await removeStateFile(files.dir, name);
|
|
326
|
+
return "discarded";
|
|
327
|
+
}
|
|
328
|
+
const nested = channelFiles(files, id);
|
|
329
|
+
await ensureStateDir(nested.dir);
|
|
330
|
+
const destinationInbox = await exists(nested.inbox);
|
|
331
|
+
if (!destinationInbox && present[0] && await exists(nested.cursor)) {
|
|
332
|
+
const [legacyCursor, channelCursor] = await Promise.all([readCursor(files), readCursor(nested)]);
|
|
333
|
+
if (legacyCursor.deliveredCount !== channelCursor.deliveredCount || legacyCursor.lastDeliveredEventId !== channelCursor.lastDeliveredEventId) {
|
|
334
|
+
const safe = legacyCursor.deliveredCount < channelCursor.deliveredCount ? legacyCursor : channelCursor;
|
|
335
|
+
await writeStateFile(nested.dir, "cursor.json", safe);
|
|
336
|
+
console.warn(`Khala cursor migration: conflicting cursors for ${id}; using earlier position ${safe.deliveredCount} to avoid skipping messages (legacy=${legacyCursor.deliveredCount}, channel=${channelCursor.deliveredCount}).`);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
const status = await readJson(files.status);
|
|
340
|
+
const metadata = await readJson(path3.join(nested.dir, "channel.json"));
|
|
341
|
+
if (!metadata || roomId(metadata) !== id) {
|
|
342
|
+
await writeStateFile(nested.dir, "channel.json", {
|
|
343
|
+
roomId: id,
|
|
344
|
+
...typeof status?.channelName === "string" ? { channelName: status.channelName } : {},
|
|
345
|
+
joinedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
if (destinationInbox) {
|
|
349
|
+
if (present[2]) await appendEntries(nested, (await unread(files)).entries);
|
|
350
|
+
for (const name of ["inbox.jsonl", "cursor.json", "mode.json"]) await removeStateFile(files.dir, name);
|
|
351
|
+
return "moved";
|
|
352
|
+
}
|
|
353
|
+
for (const name of names) {
|
|
354
|
+
if (await exists(path3.join(nested.dir, name))) {
|
|
355
|
+
await removeStateFile(files.dir, name);
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
try {
|
|
359
|
+
await fs3.rename(path3.join(files.dir, name), path3.join(nested.dir, name));
|
|
360
|
+
} catch (error) {
|
|
361
|
+
if (error.code !== "ENOENT") throw new StateError("storage_failed");
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
return "moved";
|
|
365
|
+
}
|
|
366
|
+
function resolveChannel(list, ref) {
|
|
367
|
+
const channels = list.map((item) => ({ channel: item.channelName ?? item.roomId, roomId: item.roomId }));
|
|
368
|
+
if (ref === void 0) {
|
|
369
|
+
if (list.length === 1) return { ok: true, channel: list[0] };
|
|
370
|
+
return { ok: false, code: list.length ? "channel_required" : "channel_unknown", channels };
|
|
371
|
+
}
|
|
372
|
+
const exact = list.find((item) => item.roomId === ref);
|
|
373
|
+
if (exact) return { ok: true, channel: exact };
|
|
374
|
+
const normalized = ref.replace(/^#/, "").toLowerCase();
|
|
375
|
+
const matches = list.filter((item) => item.channelName?.replace(/^#/, "").toLowerCase() === normalized);
|
|
376
|
+
if (matches.length === 1) return { ok: true, channel: matches[0] };
|
|
377
|
+
return { ok: false, code: matches.length ? "channel_ambiguous" : "channel_unknown", channels };
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// src/activity.ts
|
|
381
|
+
init_define_KHALA_BUNDLE();
|
|
382
|
+
import * as fs4 from "node:fs/promises";
|
|
383
|
+
import path4 from "node:path";
|
|
384
|
+
async function readActivity(files) {
|
|
385
|
+
const activity = await readJson(path4.join(files.dir, "activity.json"));
|
|
386
|
+
return activity && ["idle", "busy"].includes(activity.state) && typeof activity.updatedAt === "string" && Number.isFinite(Date.parse(activity.updatedAt)) ? activity : { state: "busy", updatedAt: (/* @__PURE__ */ new Date(0)).toISOString() };
|
|
387
|
+
}
|
|
388
|
+
var USAGE_LIMIT = "usage-limit.json";
|
|
389
|
+
async function markUsageLimit(files, now = () => /* @__PURE__ */ new Date()) {
|
|
390
|
+
await writeJsonAtomic(path4.join(files.dir, USAGE_LIMIT), { at: now().toISOString() });
|
|
391
|
+
}
|
|
392
|
+
async function clearUsageLimit(files) {
|
|
393
|
+
await fs4.rm(path4.join(files.dir, USAGE_LIMIT), { force: true });
|
|
394
|
+
}
|
|
395
|
+
async function usageLimited(dir) {
|
|
396
|
+
return await readJson(path4.join(dir, USAGE_LIMIT)) !== null;
|
|
397
|
+
}
|
|
398
|
+
async function writeActivity(files, state, now = () => /* @__PURE__ */ new Date()) {
|
|
399
|
+
const activity = { state, updatedAt: now().toISOString() };
|
|
400
|
+
await writeJsonAtomic(path4.join(files.dir, "activity.json"), activity);
|
|
401
|
+
return activity;
|
|
402
|
+
}
|
|
403
|
+
|
|
130
404
|
// src/wake/shared/index.ts
|
|
131
405
|
init_define_KHALA_BUNDLE();
|
|
132
406
|
|
|
133
407
|
// src/wake/shared/settings.ts
|
|
134
408
|
init_define_KHALA_BUNDLE();
|
|
135
|
-
import * as
|
|
136
|
-
import
|
|
409
|
+
import * as fs5 from "node:fs/promises";
|
|
410
|
+
import path5 from "node:path";
|
|
137
411
|
import { randomBytes } from "node:crypto";
|
|
138
412
|
async function readWakeSettings(root) {
|
|
139
413
|
try {
|
|
140
|
-
const value = JSON.parse(await
|
|
414
|
+
const value = JSON.parse(await fs5.readFile(path5.join(root, "wake-settings.json"), "utf8"));
|
|
141
415
|
const entries = (input) => {
|
|
142
416
|
if (!input || typeof input !== "object" || Array.isArray(input)) return {};
|
|
143
417
|
return Object.fromEntries(Object.entries(input).filter(([, entry]) => entry && typeof entry === "object" && typeof entry.at === "string"));
|
|
@@ -149,14 +423,14 @@ async function readWakeSettings(root) {
|
|
|
149
423
|
}
|
|
150
424
|
}
|
|
151
425
|
async function writeWakeSettings(root, settings) {
|
|
152
|
-
await
|
|
153
|
-
const target2 =
|
|
426
|
+
await fs5.mkdir(root, { recursive: true, mode: 448 });
|
|
427
|
+
const target2 = path5.join(root, "wake-settings.json");
|
|
154
428
|
const temp = `${target2}.${randomBytes(8).toString("hex")}.tmp`;
|
|
155
429
|
try {
|
|
156
|
-
await
|
|
157
|
-
await
|
|
430
|
+
await fs5.writeFile(temp, JSON.stringify(settings), { mode: 384 });
|
|
431
|
+
await fs5.rename(temp, target2);
|
|
158
432
|
} finally {
|
|
159
|
-
await
|
|
433
|
+
await fs5.rm(temp, { force: true });
|
|
160
434
|
}
|
|
161
435
|
}
|
|
162
436
|
async function updateWakeSettings(root, update) {
|
|
@@ -175,9 +449,9 @@ async function driverAllowed(root, harness, driver, optIn) {
|
|
|
175
449
|
// src/wake/muse-monitor.ts
|
|
176
450
|
init_define_KHALA_BUNDLE();
|
|
177
451
|
import { createInterface } from "node:readline";
|
|
178
|
-
import * as
|
|
452
|
+
import * as fs7 from "node:fs/promises";
|
|
179
453
|
import os2 from "node:os";
|
|
180
|
-
import
|
|
454
|
+
import path7 from "node:path";
|
|
181
455
|
|
|
182
456
|
// src/install/cursor.ts
|
|
183
457
|
init_define_KHALA_BUNDLE();
|
|
@@ -220,12 +494,12 @@ function nodeScriptCommand(platform, node, script) {
|
|
|
220
494
|
|
|
221
495
|
// src/install/launcher.ts
|
|
222
496
|
init_define_KHALA_BUNDLE();
|
|
223
|
-
function installedLauncher(platform,
|
|
497
|
+
function installedLauncher(platform, path10, home, env) {
|
|
224
498
|
const windows = platform === "win32";
|
|
225
499
|
const candidate = windows ? env.LOCALAPPDATA : env.XDG_DATA_HOME;
|
|
226
|
-
const dataHome = candidate &&
|
|
227
|
-
const prefix =
|
|
228
|
-
return { prefix, bin: windows ?
|
|
500
|
+
const dataHome = candidate && path10.isAbsolute(candidate) ? candidate : windows ? path10.join(home, "AppData", "Local") : path10.join(home, ".local", "share");
|
|
501
|
+
const prefix = path10.join(dataHome, "khala", "npm");
|
|
502
|
+
return { prefix, bin: windows ? path10.join(prefix, "khala.cmd") : path10.join(prefix, "bin", "khala") };
|
|
229
503
|
}
|
|
230
504
|
function launcherShellQuote(platform, bin) {
|
|
231
505
|
return platform === "win32" ? `"${bin}"` : `'${bin.replaceAll("'", `'\\''`)}'`;
|
|
@@ -233,8 +507,8 @@ function launcherShellQuote(platform, bin) {
|
|
|
233
507
|
|
|
234
508
|
// src/install/managed-file.ts
|
|
235
509
|
init_define_KHALA_BUNDLE();
|
|
236
|
-
import * as
|
|
237
|
-
import
|
|
510
|
+
import * as fs6 from "node:fs/promises";
|
|
511
|
+
import path6 from "node:path";
|
|
238
512
|
import { isDeepStrictEqual } from "node:util";
|
|
239
513
|
var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
240
514
|
var BOM = "\uFEFF";
|
|
@@ -273,7 +547,7 @@ var textFormat = {
|
|
|
273
547
|
};
|
|
274
548
|
async function readManaged(file) {
|
|
275
549
|
try {
|
|
276
|
-
return { file, text: await
|
|
550
|
+
return { file, text: await fs6.readFile(file, "utf8") };
|
|
277
551
|
} catch (error) {
|
|
278
552
|
if (error.code === "ENOENT") return { file, text: null };
|
|
279
553
|
throw error;
|
|
@@ -283,11 +557,11 @@ var LEGACY_BACKUP = ".khala-bak";
|
|
|
283
557
|
var ManagedFiles = class {
|
|
284
558
|
store;
|
|
285
559
|
constructor(stateDir) {
|
|
286
|
-
this.store =
|
|
560
|
+
this.store = path6.join(stateDir, "install-originals.json");
|
|
287
561
|
}
|
|
288
562
|
async load() {
|
|
289
563
|
try {
|
|
290
|
-
const value = JSON.parse(await
|
|
564
|
+
const value = JSON.parse(await fs6.readFile(this.store, "utf8"));
|
|
291
565
|
return isObject(value) ? value : {};
|
|
292
566
|
} catch (error) {
|
|
293
567
|
if (error.code === "ENOENT") return {};
|
|
@@ -296,17 +570,17 @@ var ManagedFiles = class {
|
|
|
296
570
|
}
|
|
297
571
|
async save(recordings) {
|
|
298
572
|
if (!Object.keys(recordings).length) {
|
|
299
|
-
await
|
|
573
|
+
await fs6.rm(this.store, { force: true });
|
|
300
574
|
return;
|
|
301
575
|
}
|
|
302
|
-
await
|
|
576
|
+
await fs6.mkdir(path6.dirname(this.store), { recursive: true, mode: 448 });
|
|
303
577
|
const temp = `${this.store}.${process.pid}.tmp`;
|
|
304
|
-
await
|
|
305
|
-
await
|
|
578
|
+
await fs6.writeFile(temp, JSON.stringify(recordings, null, 2) + "\n", { mode: 384 });
|
|
579
|
+
await fs6.rename(temp, this.store);
|
|
306
580
|
}
|
|
307
581
|
/** The recorded original: `undefined` when nothing is recorded, `null` when the file did not exist. */
|
|
308
582
|
async original(file) {
|
|
309
|
-
const recording = (await this.load())[
|
|
583
|
+
const recording = (await this.load())[path6.resolve(file)];
|
|
310
584
|
if (recording) return recording.existed ? Buffer.from(recording.bytes ?? "", "base64") : null;
|
|
311
585
|
const legacy = await readBytes(file + LEGACY_BACKUP);
|
|
312
586
|
return legacy ?? void 0;
|
|
@@ -318,14 +592,14 @@ var ManagedFiles = class {
|
|
|
318
592
|
* the override still never replaces a previously saved recording.
|
|
319
593
|
*/
|
|
320
594
|
async record(file, options) {
|
|
321
|
-
const key =
|
|
595
|
+
const key = path6.resolve(file);
|
|
322
596
|
const recordings = await this.load();
|
|
323
597
|
if (recordings[key]) return;
|
|
324
598
|
const legacy = await readBytes(file + LEGACY_BACKUP);
|
|
325
599
|
const current = options ? options.original : legacy ?? await readBytes(file);
|
|
326
|
-
recordings[key] = current === null ? { existed: false, createdDirs: await missingDirs(
|
|
600
|
+
recordings[key] = current === null ? { existed: false, createdDirs: await missingDirs(path6.dirname(key)) } : { existed: true, bytes: current.toString("base64"), createdDirs: [] };
|
|
327
601
|
await this.save(recordings);
|
|
328
|
-
if (legacy) await
|
|
602
|
+
if (legacy) await fs6.rm(file + LEGACY_BACKUP, { force: true });
|
|
329
603
|
}
|
|
330
604
|
/**
|
|
331
605
|
* Install: records every file's original first (so each knows which directories Khala
|
|
@@ -335,8 +609,8 @@ var ManagedFiles = class {
|
|
|
335
609
|
for (const { current } of entries) await this.record(current.file);
|
|
336
610
|
for (const { current, text, mode } of entries) {
|
|
337
611
|
if (text === current.text) continue;
|
|
338
|
-
await
|
|
339
|
-
await
|
|
612
|
+
await fs6.mkdir(path6.dirname(current.file), { recursive: true });
|
|
613
|
+
await fs6.writeFile(current.file, text, mode === void 0 ? void 0 : { mode });
|
|
340
614
|
}
|
|
341
615
|
}
|
|
342
616
|
/**
|
|
@@ -347,7 +621,7 @@ var ManagedFiles = class {
|
|
|
347
621
|
* stripped content. The recording is then dropped.
|
|
348
622
|
*/
|
|
349
623
|
async restore(current, format, strip) {
|
|
350
|
-
const key =
|
|
624
|
+
const key = path6.resolve(current.file);
|
|
351
625
|
const recordings = await this.load();
|
|
352
626
|
const recording = recordings[key];
|
|
353
627
|
const original = await this.original(current.file);
|
|
@@ -364,25 +638,25 @@ var ManagedFiles = class {
|
|
|
364
638
|
let value = strip(format.parse(current.text), originalValue);
|
|
365
639
|
if (comparable && format.prune) value = format.prune(value, originalValue);
|
|
366
640
|
if (comparable && original === null && format.empty(value)) {
|
|
367
|
-
await
|
|
368
|
-
for (const dir of recording?.createdDirs ?? []) await
|
|
641
|
+
await fs6.rm(current.file, { force: true });
|
|
642
|
+
for (const dir of recording?.createdDirs ?? []) await fs6.rmdir(dir).catch(() => void 0);
|
|
369
643
|
} else if (comparable && original && format.equal(value, originalValue)) {
|
|
370
|
-
if (!original.equals(Buffer.from(current.text, "utf8"))) await
|
|
644
|
+
if (!original.equals(Buffer.from(current.text, "utf8"))) await fs6.writeFile(current.file, original);
|
|
371
645
|
} else {
|
|
372
646
|
const text = format.serialize(value, current.text);
|
|
373
|
-
if (text !== current.text) await
|
|
647
|
+
if (text !== current.text) await fs6.writeFile(current.file, text);
|
|
374
648
|
}
|
|
375
649
|
}
|
|
376
650
|
if (recording) {
|
|
377
651
|
delete recordings[key];
|
|
378
652
|
await this.save(recordings);
|
|
379
653
|
}
|
|
380
|
-
await
|
|
654
|
+
await fs6.rm(current.file + LEGACY_BACKUP, { force: true });
|
|
381
655
|
}
|
|
382
656
|
};
|
|
383
657
|
async function readBytes(file) {
|
|
384
658
|
try {
|
|
385
|
-
return await
|
|
659
|
+
return await fs6.readFile(file);
|
|
386
660
|
} catch (error) {
|
|
387
661
|
if (error.code === "ENOENT") return null;
|
|
388
662
|
throw error;
|
|
@@ -390,32 +664,32 @@ async function readBytes(file) {
|
|
|
390
664
|
}
|
|
391
665
|
async function missingDirs(dir) {
|
|
392
666
|
const missing = [];
|
|
393
|
-
for (let at = dir; ; at =
|
|
667
|
+
for (let at = dir; ; at = path6.dirname(at)) {
|
|
394
668
|
try {
|
|
395
|
-
await
|
|
669
|
+
await fs6.stat(at);
|
|
396
670
|
return missing;
|
|
397
671
|
} catch (error) {
|
|
398
672
|
if (error.code !== "ENOENT") throw error;
|
|
399
673
|
}
|
|
400
674
|
missing.push(at);
|
|
401
|
-
if (
|
|
675
|
+
if (path6.dirname(at) === at) return missing;
|
|
402
676
|
}
|
|
403
677
|
}
|
|
404
678
|
|
|
405
679
|
// src/install/cursor.ts
|
|
406
680
|
var CURSOR_HOOK_EVENTS = ["beforeSubmitPrompt", "postToolUse", "stop"];
|
|
407
681
|
var HOOK_SUFFIX = " hook deliver --harness cursor";
|
|
408
|
-
function cursorPaths({ platform, path:
|
|
409
|
-
const cursorDir =
|
|
682
|
+
function cursorPaths({ platform, path: path10, home, env }, packageName = "khala-cli") {
|
|
683
|
+
const cursorDir = path10.join(home, ".cursor");
|
|
410
684
|
const windows = platform === "win32";
|
|
411
|
-
const { prefix } = installedLauncher(platform,
|
|
412
|
-
const modules = windows ?
|
|
685
|
+
const { prefix } = installedLauncher(platform, path10, home, env);
|
|
686
|
+
const modules = windows ? path10.join(prefix, "node_modules") : path10.join(prefix, "lib", "node_modules");
|
|
413
687
|
return {
|
|
414
688
|
cursorDir,
|
|
415
689
|
prefix,
|
|
416
|
-
mcpFile:
|
|
417
|
-
hooksFile:
|
|
418
|
-
script:
|
|
690
|
+
mcpFile: path10.join(cursorDir, "mcp.json"),
|
|
691
|
+
hooksFile: path10.join(cursorDir, "hooks.json"),
|
|
692
|
+
script: path10.join(modules, ...packageName.split("/"), "dist", "khala.mjs")
|
|
419
693
|
};
|
|
420
694
|
}
|
|
421
695
|
function cursorHookCommand(platform, node, script) {
|
|
@@ -514,8 +788,8 @@ var MUSE_WAKE_REQUEST = "muse-monitor-wake.json";
|
|
|
514
788
|
var shellQuote = (value) => `'${value.replaceAll("'", "'\\''")}'`;
|
|
515
789
|
function museCliPath(env = process.env) {
|
|
516
790
|
const home = env.HOME || env.USERPROFILE || os2.homedir();
|
|
517
|
-
const { prefix } = cursorPaths({ platform: process.platform, path:
|
|
518
|
-
return process.platform === "win32" ?
|
|
791
|
+
const { prefix } = cursorPaths({ platform: process.platform, path: path7, home, env });
|
|
792
|
+
return process.platform === "win32" ? path7.join(prefix, "khala.cmd") : path7.join(prefix, "bin", "khala");
|
|
519
793
|
}
|
|
520
794
|
function museWatchCommand(sessionId, bin = museCliPath(), platform = process.platform) {
|
|
521
795
|
const quote = platform === "win32" ? (value) => `'${value.replaceAll("'", "''")}'` : shellQuote;
|
|
@@ -550,7 +824,7 @@ function createMuseMonitorDriver() {
|
|
|
550
824
|
if (ctx.signal.aborted || (await readActivity(ctx.files)).state !== "idle" || !await monitorArmed(ctx.files)) return "skipped";
|
|
551
825
|
const epoch = (await readActivity(ctx.files)).updatedAt;
|
|
552
826
|
for (const dir of monitorStorageCandidates(ctx.files)) {
|
|
553
|
-
if ((await readJson(
|
|
827
|
+
if ((await readJson(path7.join(dir, "monitor-wake-observed.json")))?.epoch === epoch) return "skipped";
|
|
554
828
|
}
|
|
555
829
|
const owner = await monitorOwner(ctx.files);
|
|
556
830
|
if (!owner) return "skipped";
|
|
@@ -558,12 +832,12 @@ function createMuseMonitorDriver() {
|
|
|
558
832
|
let journalOffset = 0;
|
|
559
833
|
if (journal) {
|
|
560
834
|
try {
|
|
561
|
-
journalOffset = (await
|
|
835
|
+
journalOffset = (await fs7.stat(journal)).size;
|
|
562
836
|
} catch (error) {
|
|
563
837
|
if (error.code !== "ENOENT") throw error;
|
|
564
838
|
}
|
|
565
839
|
}
|
|
566
|
-
await writeJsonAtomic(
|
|
840
|
+
await writeJsonAtomic(path7.join(ctx.files.dir, MUSE_WAKE_REQUEST), {
|
|
567
841
|
owner: owner.nonce,
|
|
568
842
|
line,
|
|
569
843
|
at: ctx.now,
|
|
@@ -578,8 +852,8 @@ function museJournalPath(sessionId, env) {
|
|
|
578
852
|
const created = new Date(Number.parseInt(sessionId.replaceAll("-", "").slice(0, 12), 16));
|
|
579
853
|
const date = [String(created.getFullYear()), String(created.getMonth() + 1).padStart(2, "0"), String(created.getDate()).padStart(2, "0")];
|
|
580
854
|
const home = env.HOME || env.USERPROFILE || os2.homedir();
|
|
581
|
-
const data = env.XDG_DATA_HOME &&
|
|
582
|
-
return
|
|
855
|
+
const data = env.XDG_DATA_HOME && path7.isAbsolute(env.XDG_DATA_HOME) ? env.XDG_DATA_HOME : path7.join(home, ".local", "share");
|
|
856
|
+
return path7.join(data, "muse", "sessions", ...date, sessionId, "session.jsonl");
|
|
583
857
|
}
|
|
584
858
|
async function museStopWakeText(stdin, files, env) {
|
|
585
859
|
const input = JSON.parse(stdin);
|
|
@@ -587,14 +861,14 @@ async function museStopWakeText(stdin, files, env) {
|
|
|
587
861
|
return museWakeProof(files, input.session_id, env, input.turn_id);
|
|
588
862
|
}
|
|
589
863
|
async function museWakeProof(files, sessionId, env, turnId) {
|
|
590
|
-
const request = await readJson(
|
|
864
|
+
const request = await readJson(path7.join(files.dir, MUSE_WAKE_REQUEST));
|
|
591
865
|
if (!request) return void 0;
|
|
592
866
|
const journal = museJournalPath(sessionId, env);
|
|
593
867
|
if (!journal) return void 0;
|
|
594
868
|
if (!Number.isSafeInteger(request.journalOffset) || request.journalOffset < 0) return void 0;
|
|
595
869
|
let handle;
|
|
596
870
|
try {
|
|
597
|
-
handle = await
|
|
871
|
+
handle = await fs7.open(journal, "r");
|
|
598
872
|
} catch (error) {
|
|
599
873
|
if (error.code === "ENOENT") return void 0;
|
|
600
874
|
throw error;
|
|
@@ -624,17 +898,17 @@ async function museWakeProof(files, sessionId, env, turnId) {
|
|
|
624
898
|
|
|
625
899
|
// src/wake/qwen-socket.ts
|
|
626
900
|
init_define_KHALA_BUNDLE();
|
|
627
|
-
import * as
|
|
901
|
+
import * as fs9 from "node:fs/promises";
|
|
628
902
|
import net from "node:net";
|
|
629
903
|
import os3 from "node:os";
|
|
630
|
-
import
|
|
904
|
+
import path8 from "node:path";
|
|
631
905
|
import { randomBytes as randomBytes2, randomUUID as randomUUID2 } from "node:crypto";
|
|
632
906
|
|
|
633
907
|
// src/wake/qwen-transcript.ts
|
|
634
908
|
init_define_KHALA_BUNDLE();
|
|
635
|
-
import * as
|
|
909
|
+
import * as fs8 from "node:fs/promises";
|
|
636
910
|
async function qwenNotification(filePath) {
|
|
637
|
-
const file = await
|
|
911
|
+
const file = await fs8.open(filePath, "r");
|
|
638
912
|
let text;
|
|
639
913
|
try {
|
|
640
914
|
const stat3 = await file.stat();
|
|
@@ -666,23 +940,23 @@ async function verifyQwenTranscript(dir, filePath, now) {
|
|
|
666
940
|
}
|
|
667
941
|
|
|
668
942
|
// src/wake/qwen-socket.ts
|
|
669
|
-
var qwenHome = (env) => env.QWEN_HOME ?
|
|
943
|
+
var qwenHome = (env) => env.QWEN_HOME ? path8.resolve(env.QWEN_HOME) : path8.join(env.HOME ?? os3.homedir(), ".qwen");
|
|
670
944
|
async function qwenInboundAllowed(env, workspace = env.PWD ?? process.cwd()) {
|
|
671
|
-
for (const file of [
|
|
945
|
+
for (const file of [path8.join(qwenHome(env), "settings.json"), path8.join(workspace, ".qwen", "settings.json")]) {
|
|
672
946
|
const config = await readJson(file);
|
|
673
947
|
if (["hold", "refuse"].includes(config?.agents?.crossSessionInbound ?? "") || config?.agents?.crossSessionMessaging === false) return false;
|
|
674
948
|
}
|
|
675
949
|
return true;
|
|
676
950
|
}
|
|
677
|
-
var qwenControllerFile = (env) =>
|
|
951
|
+
var qwenControllerFile = (env) => path8.join(stateRoot(env), "qwen", "controller.json");
|
|
678
952
|
async function qwenSession(env) {
|
|
679
953
|
const socket = env.QWEN_CODE_MESSAGING_SOCKET;
|
|
680
954
|
if (!socket) return void 0;
|
|
681
955
|
try {
|
|
682
|
-
const dir =
|
|
683
|
-
for (const file of await
|
|
956
|
+
const dir = path8.join(qwenHome(env), "sessions");
|
|
957
|
+
for (const file of await fs9.readdir(dir)) {
|
|
684
958
|
if (!/^\d+\.json$/u.test(file)) continue;
|
|
685
|
-
const record = await readJson(
|
|
959
|
+
const record = await readJson(path8.join(dir, file));
|
|
686
960
|
if (record?.ipcPath === socket && typeof record.sessionId === "string" && SESSION_ID_PATTERN.test(record.sessionId)) return record.sessionId;
|
|
687
961
|
}
|
|
688
962
|
} catch {
|
|
@@ -694,7 +968,7 @@ async function target(env) {
|
|
|
694
968
|
if (!sessionId) return void 0;
|
|
695
969
|
try {
|
|
696
970
|
const file = qwenControllerFile(env);
|
|
697
|
-
const stat3 = await
|
|
971
|
+
const stat3 = await fs9.lstat(file);
|
|
698
972
|
if (!stat3.isFile() || (stat3.mode & 63) !== 0) return void 0;
|
|
699
973
|
const credential = await readJson(file);
|
|
700
974
|
if (!credential?.token || !/^qpc_[0-9a-f]{64}$/u.test(credential.token)) return void 0;
|
|
@@ -719,10 +993,10 @@ function qwenFrames(target2, line, from, replyToken, msgId) {
|
|
|
719
993
|
}
|
|
720
994
|
async function sendQwenWake(target2, line, signal, options = {}) {
|
|
721
995
|
if (signal.aborted) return "failed";
|
|
722
|
-
const dir = await
|
|
723
|
-
const from =
|
|
996
|
+
const dir = await fs9.mkdtemp(path8.join(options.runtimeDir ?? os3.tmpdir(), "khala-qw-"));
|
|
997
|
+
const from = path8.join(dir, "r.sock");
|
|
724
998
|
if (Buffer.byteLength(from) > 103) {
|
|
725
|
-
await
|
|
999
|
+
await fs9.rm(dir, { recursive: true, force: true });
|
|
726
1000
|
return "failed";
|
|
727
1001
|
}
|
|
728
1002
|
const replyToken = randomBytes2(32).toString("hex");
|
|
@@ -778,7 +1052,7 @@ async function sendQwenWake(target2, line, signal, options = {}) {
|
|
|
778
1052
|
server.once("error", reject);
|
|
779
1053
|
server.listen(from, resolve);
|
|
780
1054
|
});
|
|
781
|
-
await
|
|
1055
|
+
await fs9.chmod(from, 384);
|
|
782
1056
|
if (signal.aborted) return "failed";
|
|
783
1057
|
sender = net.createConnection(target2.socket);
|
|
784
1058
|
sender.on("error", () => finish("failed"));
|
|
@@ -794,20 +1068,20 @@ async function sendQwenWake(target2, line, signal, options = {}) {
|
|
|
794
1068
|
sender?.destroy();
|
|
795
1069
|
for (const socket of peers) socket.destroy();
|
|
796
1070
|
await new Promise((resolve) => server.close(() => resolve()));
|
|
797
|
-
await
|
|
1071
|
+
await fs9.rm(dir, { recursive: true, force: true });
|
|
798
1072
|
}
|
|
799
1073
|
}
|
|
800
1074
|
function createQwenSocketDriver(deps = {}) {
|
|
801
1075
|
const resolve = deps.resolve ?? target;
|
|
802
1076
|
const policy = async (env) => {
|
|
803
|
-
const config = await readJson(
|
|
1077
|
+
const config = await readJson(path8.join(qwenHome(env), "settings.json"));
|
|
804
1078
|
return config?.agents;
|
|
805
1079
|
};
|
|
806
1080
|
const reason = async (ctx) => {
|
|
807
1081
|
const config = await policy(ctx.env);
|
|
808
1082
|
if (config?.crossSessionInbound === "hold") return "qwen_held";
|
|
809
1083
|
if ((deps.platform ?? process.platform) === "win32") return "windows";
|
|
810
|
-
const receipt = await readJson(
|
|
1084
|
+
const receipt = await readJson(path8.join(ctx.files.dir, "qwen-receipt.json"));
|
|
811
1085
|
if (receipt?.status === "held" && config?.crossSessionInbound !== "accept") return "qwen_held";
|
|
812
1086
|
if (config?.crossSessionMessaging === false || config?.crossSessionInbound === "refuse") return "qwen_refused";
|
|
813
1087
|
if (!deps.resolve && !ctx.env.QWEN_CODE_MESSAGING_SOCKET) return "qwen_session_missing";
|
|
@@ -821,15 +1095,15 @@ function createQwenSocketDriver(deps = {}) {
|
|
|
821
1095
|
deadlineMs: 3e4,
|
|
822
1096
|
verification: "transcript",
|
|
823
1097
|
async verify(ctx) {
|
|
824
|
-
const journal = await readJson(
|
|
825
|
-
const receipt = await readJson(
|
|
1098
|
+
const journal = await readJson(path8.join(ctx.files.dir, "wake-journal.json"));
|
|
1099
|
+
const receipt = await readJson(path8.join(ctx.files.dir, "qwen-receipt.json"));
|
|
826
1100
|
if (!journal?.attempts?.some((attempt) => attempt.driver === "socket") && receipt?.status !== "held") return;
|
|
827
|
-
const transcript = await readJson(
|
|
1101
|
+
const transcript = await readJson(path8.join(ctx.files.dir, "qwen-transcript.json"));
|
|
828
1102
|
if (!transcript?.path) return;
|
|
829
1103
|
try {
|
|
830
1104
|
const text = await verifyQwenTranscript(ctx.files.dir, transcript.path, ctx.now);
|
|
831
1105
|
if (receipt?.status === "held" && receipt.nonce && text?.includes(`(k-${receipt.nonce})`)) {
|
|
832
|
-
await writeJsonAtomic(
|
|
1106
|
+
await writeJsonAtomic(path8.join(ctx.files.dir, "qwen-receipt.json"), { status: "delivered" });
|
|
833
1107
|
}
|
|
834
1108
|
} catch {
|
|
835
1109
|
}
|
|
@@ -843,7 +1117,7 @@ function createQwenSocketDriver(deps = {}) {
|
|
|
843
1117
|
const status = await (deps.send ?? sendQwenWake)(destination, line, ctx.signal);
|
|
844
1118
|
await ensureStateDir(ctx.files.dir);
|
|
845
1119
|
const nonce = line.match(/\(k-([0-9a-f]{8})\)$/u)?.[1];
|
|
846
|
-
await writeJsonAtomic(
|
|
1120
|
+
await writeJsonAtomic(path8.join(ctx.files.dir, "qwen-receipt.json"), { status, nonce });
|
|
847
1121
|
if (nonce && status === "held") await cancelAttempt(ctx.files.dir, nonce);
|
|
848
1122
|
else if (nonce && status !== "delivered") await failAttempt(ctx.files.dir, nonce, ctx.now);
|
|
849
1123
|
}
|
|
@@ -853,12 +1127,13 @@ function createQwenSocketDriver(deps = {}) {
|
|
|
853
1127
|
// src/watch.ts
|
|
854
1128
|
var MARKER = "monitor.json";
|
|
855
1129
|
var OBSERVATION = "monitor-cursor.json";
|
|
1130
|
+
var TICK_MS = 2e3;
|
|
856
1131
|
var NONCE_PATTERN = /^[a-f0-9-]{36}$/;
|
|
857
|
-
var leaseFile = (dir, nonce) =>
|
|
1132
|
+
var leaseFile = (dir, nonce) => path9.join(dir, `monitor-${nonce}.json`);
|
|
858
1133
|
async function monitorOwner(files) {
|
|
859
1134
|
const owners = [];
|
|
860
1135
|
for (const dir of monitorStorageCandidates(files)) {
|
|
861
|
-
const owner = await readJson(
|
|
1136
|
+
const owner = await readJson(path9.join(dir, MARKER));
|
|
862
1137
|
if (!owner || !Number.isSafeInteger(owner.pid) || owner.pid <= 0 || typeof owner.nonce !== "string" || !NONCE_PATTERN.test(owner.nonce)) continue;
|
|
863
1138
|
const lease = await readJson(leaseFile(dir, owner.nonce));
|
|
864
1139
|
if (lease?.nonce !== owner.nonce) continue;
|
|
@@ -892,7 +1167,7 @@ async function watchSession(files, io) {
|
|
|
892
1167
|
const wake = await readWakeSettings(stateRoot(env));
|
|
893
1168
|
return await qwenInboundAllowed(env) && !wake.off["qwen/socket"] && !wake.off["qwen/background-shell"];
|
|
894
1169
|
};
|
|
895
|
-
|
|
1170
|
+
const watchers = [];
|
|
896
1171
|
let timer;
|
|
897
1172
|
let running;
|
|
898
1173
|
let dirty = false;
|
|
@@ -907,7 +1182,7 @@ async function watchSession(files, io) {
|
|
|
907
1182
|
if (done) return;
|
|
908
1183
|
done = true;
|
|
909
1184
|
code = exit;
|
|
910
|
-
|
|
1185
|
+
for (const w of watchers) w.close();
|
|
911
1186
|
clearInterval(timer);
|
|
912
1187
|
if (!qwen || exit !== 0) io.stderr?.(`khala watch: stopped (${reason}). Do not re-arm.`);
|
|
913
1188
|
finish();
|
|
@@ -977,7 +1252,7 @@ async function watchSession(files, io) {
|
|
|
977
1252
|
}
|
|
978
1253
|
const mode = await readListeningMode(target2);
|
|
979
1254
|
const entries = await readEntries(target2);
|
|
980
|
-
const observed = await readJson(
|
|
1255
|
+
const observed = await readJson(path9.join(await observationDir(target2), OBSERVATION));
|
|
981
1256
|
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;
|
|
982
1257
|
const baseline = initialDelivered.get(target2.dir).cursor;
|
|
983
1258
|
const initialCount = baseline.deliveredCount === 0 || entries[baseline.deliveredCount - 1]?.eventId === baseline.lastDeliveredEventId ? baseline.deliveredCount : 0;
|
|
@@ -1003,7 +1278,7 @@ async function watchSession(files, io) {
|
|
|
1003
1278
|
}
|
|
1004
1279
|
if (position !== entries.length && !done && await ownsSession() && JSON.stringify(await readJson(target2.session)) === JSON.stringify(latestSession)) {
|
|
1005
1280
|
try {
|
|
1006
|
-
await writeJsonAtomic(
|
|
1281
|
+
await writeJsonAtomic(path9.join(await observationDir(target2), OBSERVATION), {
|
|
1007
1282
|
userId: session.userId,
|
|
1008
1283
|
roomId: session.roomId,
|
|
1009
1284
|
count: entries.length,
|
|
@@ -1015,7 +1290,7 @@ async function watchSession(files, io) {
|
|
|
1015
1290
|
}
|
|
1016
1291
|
}
|
|
1017
1292
|
if (io.harness === "muse" && !done && await ownsSession()) {
|
|
1018
|
-
const request = await readJson(
|
|
1293
|
+
const request = await readJson(path9.join(files.dir, MUSE_WAKE_REQUEST));
|
|
1019
1294
|
const now = (io.now ?? Date.now)();
|
|
1020
1295
|
const wakeNonce = typeof request?.line === "string" ? /\(k-([a-f0-9]{8})\)$/.exec(request.line)?.[1] : void 0;
|
|
1021
1296
|
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") {
|
|
@@ -1033,7 +1308,7 @@ async function watchSession(files, io) {
|
|
|
1033
1308
|
const activity = await readActivity(files);
|
|
1034
1309
|
if (eligible && await ownsSession() && activity.state === "idle") {
|
|
1035
1310
|
const epoch = activity.updatedAt;
|
|
1036
|
-
const dedupFile =
|
|
1311
|
+
const dedupFile = path9.join(storage, "monitor-wake-observed.json");
|
|
1037
1312
|
const observed = await readJson(dedupFile);
|
|
1038
1313
|
if (observed?.epoch === epoch) return;
|
|
1039
1314
|
await writeJsonAtomic(dedupFile, { epoch, line: request.line });
|
|
@@ -1058,15 +1333,16 @@ async function watchSession(files, io) {
|
|
|
1058
1333
|
});
|
|
1059
1334
|
};
|
|
1060
1335
|
let published = false;
|
|
1061
|
-
|
|
1062
|
-
if (published && (filename === null || [MARKER, MUSE_WAKE_REQUEST, "inbox.jsonl", "cursor.json", "mode.json", "session.json", "status.json"].includes(String(filename)))) notify();
|
|
1063
|
-
}
|
|
1064
|
-
|
|
1336
|
+
const onChange = (_event, filename) => {
|
|
1337
|
+
if (published && (filename === null || [MARKER, MUSE_WAKE_REQUEST, "inbox.jsonl", "cursor.json", "mode.json", "session.json", "status.json", "activity.json"].includes(path9.basename(String(filename))))) notify();
|
|
1338
|
+
};
|
|
1339
|
+
watchers.push(watchDirectory(files.dir, onChange), watchDirectory(files.dir, { recursive: true }, onChange));
|
|
1340
|
+
for (const w of watchers) w.on("error", (error) => stop(3, errorReason(error)));
|
|
1065
1341
|
lastHeartbeat = Date.now();
|
|
1066
1342
|
await writeJsonAtomic(leaseFile(storage, nonce), { nonce, pid: process.pid, heartbeatAt: new Date(lastHeartbeat).toISOString() });
|
|
1067
|
-
await writeJsonAtomic(
|
|
1343
|
+
await writeJsonAtomic(path9.join(storage, MARKER), { nonce, pid: process.pid, startedAt: Date.now() });
|
|
1068
1344
|
published = true;
|
|
1069
|
-
timer = setInterval(notify,
|
|
1345
|
+
timer = setInterval(notify, TICK_MS);
|
|
1070
1346
|
io.signal.addEventListener("abort", abort, { once: true });
|
|
1071
1347
|
if (io.signal.aborted) stop();
|
|
1072
1348
|
else notify();
|
|
@@ -1078,13 +1354,17 @@ async function watchSession(files, io) {
|
|
|
1078
1354
|
return 3;
|
|
1079
1355
|
} finally {
|
|
1080
1356
|
clearInterval(timer);
|
|
1081
|
-
|
|
1357
|
+
for (const w of watchers) w.close();
|
|
1082
1358
|
io.signal.removeEventListener("abort", abort);
|
|
1083
|
-
await
|
|
1359
|
+
await fs10.unlink(leaseFile(storage, nonce)).catch((error) => {
|
|
1084
1360
|
if (!["ENOENT", "EACCES", "EROFS"].includes(error.code ?? "")) throw error;
|
|
1085
1361
|
});
|
|
1086
1362
|
}
|
|
1087
1363
|
}
|
|
1364
|
+
function claudeWatchCommand(sessionId, env) {
|
|
1365
|
+
const root = env.CLAUDE_PLUGIN_ROOT;
|
|
1366
|
+
return `${root ? `'${path9.join(root, "bin", "khala").replaceAll("'", "'\\''")}'` : "khala"} watch --harness claude --session ${sessionId}`;
|
|
1367
|
+
}
|
|
1088
1368
|
var WATCH_USAGE = "usage: khala watch [--harness <id>] [--session <id>]";
|
|
1089
1369
|
async function run(argv) {
|
|
1090
1370
|
if (argv.length === 1 && ["--help", "-h"].includes(argv[0])) {
|
|
@@ -1097,8 +1377,8 @@ ${WATCH_USAGE}`);
|
|
|
1097
1377
|
return 1;
|
|
1098
1378
|
};
|
|
1099
1379
|
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");
|
|
1100
|
-
const { resolveHarness, resolveSessionId } = await import("./chunk-
|
|
1101
|
-
const { adapterFor } = await import("./chunk-
|
|
1380
|
+
const { resolveHarness, resolveSessionId } = await import("./chunk-LZXXWDKB.mjs");
|
|
1381
|
+
const { adapterFor } = await import("./chunk-IH5ALDEX.mjs");
|
|
1102
1382
|
const harness = resolveHarness(argv, process.env);
|
|
1103
1383
|
if (harness === "invalid" || !adapterFor(harness)?.codec) return invalid("invalid_harness");
|
|
1104
1384
|
const index = argv.indexOf("--session");
|
|
@@ -1123,9 +1403,29 @@ ${WATCH_USAGE}`);
|
|
|
1123
1403
|
}
|
|
1124
1404
|
|
|
1125
1405
|
export {
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1406
|
+
toEventInboxEntry,
|
|
1407
|
+
createEventKeyFilter,
|
|
1408
|
+
isWakeEntry,
|
|
1409
|
+
mentions,
|
|
1410
|
+
wakesAgent,
|
|
1411
|
+
readEntries,
|
|
1412
|
+
readCursor,
|
|
1413
|
+
unread,
|
|
1414
|
+
wakeStart,
|
|
1415
|
+
unwoken,
|
|
1416
|
+
markNudged,
|
|
1417
|
+
advanceCursor,
|
|
1418
|
+
appendInbox,
|
|
1419
|
+
unreadCount,
|
|
1420
|
+
channelKey,
|
|
1421
|
+
listChannels,
|
|
1422
|
+
migrateLegacy,
|
|
1423
|
+
resolveChannel,
|
|
1424
|
+
readActivity,
|
|
1425
|
+
markUsageLimit,
|
|
1426
|
+
clearUsageLimit,
|
|
1427
|
+
usageLimited,
|
|
1428
|
+
writeActivity,
|
|
1129
1429
|
readModeState,
|
|
1130
1430
|
delegateChannel,
|
|
1131
1431
|
isDelegated,
|
|
@@ -1133,9 +1433,13 @@ export {
|
|
|
1133
1433
|
applyArchived,
|
|
1134
1434
|
ATTENTION,
|
|
1135
1435
|
applyListeningMode,
|
|
1436
|
+
readWakeSettings,
|
|
1437
|
+
updateWakeSettings,
|
|
1438
|
+
driverAllowed,
|
|
1136
1439
|
CURSOR_WORKSPACE_ENV,
|
|
1137
1440
|
CURSOR_DEFAULT_SESSION,
|
|
1138
1441
|
cursorSessionId,
|
|
1442
|
+
nodeScriptCommand,
|
|
1139
1443
|
installedLauncher,
|
|
1140
1444
|
launcherShellQuote,
|
|
1141
1445
|
formatJson,
|
|
@@ -1143,23 +1447,23 @@ export {
|
|
|
1143
1447
|
textFormat,
|
|
1144
1448
|
readManaged,
|
|
1145
1449
|
ManagedFiles,
|
|
1450
|
+
cursorPaths,
|
|
1451
|
+
installCursor,
|
|
1452
|
+
museCliPath,
|
|
1453
|
+
museWatchCommand,
|
|
1454
|
+
museMonitorInstruction,
|
|
1455
|
+
createMuseMonitorDriver,
|
|
1456
|
+
museStopWakeText,
|
|
1146
1457
|
verifyQwenTranscript,
|
|
1147
1458
|
qwenHome,
|
|
1148
1459
|
qwenInboundAllowed,
|
|
1149
1460
|
qwenControllerFile,
|
|
1150
1461
|
qwenSession,
|
|
1151
1462
|
createQwenSocketDriver,
|
|
1152
|
-
nodeScriptCommand,
|
|
1153
|
-
cursorPaths,
|
|
1154
|
-
installCursor,
|
|
1155
1463
|
monitorOwner,
|
|
1156
1464
|
monitorArmed,
|
|
1157
1465
|
watchSession,
|
|
1466
|
+
claudeWatchCommand,
|
|
1158
1467
|
WATCH_USAGE,
|
|
1159
|
-
run
|
|
1160
|
-
museCliPath,
|
|
1161
|
-
museWatchCommand,
|
|
1162
|
-
museMonitorInstruction,
|
|
1163
|
-
createMuseMonitorDriver,
|
|
1164
|
-
museStopWakeText
|
|
1468
|
+
run
|
|
1165
1469
|
};
|