khala-cli 0.4.5 → 0.4.6
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-TZKEQIHP.mjs → chunk-2U4E7AM7.mjs} +18 -3
- package/dist/{chunk-EXHCVFVH.mjs → chunk-2XTYRBYX.mjs} +10 -4
- package/dist/{chunk-MBLP7LQJ.mjs → chunk-4X35OUAN.mjs} +11 -11
- package/dist/{chunk-PXKGKVGW.mjs → chunk-55KBLEX5.mjs} +1 -1
- package/dist/{chunk-RSDOTTZQ.mjs → chunk-6E3UCO22.mjs} +32 -11
- package/dist/{chunk-7G2G4E4U.mjs → chunk-AWBKVTLV.mjs} +15 -13
- package/dist/{chunk-V7M27QLT.mjs → chunk-B2MOLCJM.mjs} +7 -5
- package/dist/{chunk-IR3VN2ID.mjs → chunk-CWHGNVZV.mjs} +0 -18
- package/dist/chunk-DPW43YNC.mjs +36 -0
- package/dist/{chunk-V4Y26O4R.mjs → chunk-FH2CZX5T.mjs} +14 -12
- package/dist/{chunk-HB3UGIZG.mjs → chunk-IULQYQJZ.mjs} +3 -3
- package/dist/{chunk-Y4WVILEJ.mjs → chunk-JG5J3SHW.mjs} +1 -1
- package/dist/{chunk-Y2DRTRBJ.mjs → chunk-OECJ45NM.mjs} +2 -2
- package/dist/{chunk-KCJFSVDN.mjs → chunk-OW3DSTEZ.mjs} +4 -2
- package/dist/{chunk-OLZ62X7S.mjs → chunk-PFDUXY3N.mjs} +1 -1
- package/dist/{chunk-H46OG22B.mjs → chunk-PY2ZNVNY.mjs} +1 -1
- package/dist/{chunk-SQXDATEC.mjs → chunk-RRBIVI77.mjs} +1 -1
- package/dist/{chunk-6LC66VHK.mjs → chunk-SZIEG3RS.mjs} +6 -5
- package/dist/chunk-WHONG7UF.mjs +267 -0
- package/dist/{chunk-T7YRVE43.mjs → chunk-XUJIJ4KB.mjs} +8 -30
- package/dist/{chunk-EW22K3JO.mjs → chunk-Z4EDYB6Y.mjs} +176 -293
- package/dist/chunk-ZHMGB3PP.mjs +82 -0
- package/dist/khala.mjs +10 -10
- package/dist/web/assets/local-K8EcTVxW.js +9 -0
- package/dist/web/assets/local-R2y0Gl4T.css +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/chunk-VUE2DDYY.mjs +0 -34
- package/dist/web/assets/local-C1vz22ps.js +0 -9
- package/dist/web/assets/local-DdjwMERJ.css +0 -1
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { createRequire as __khalaCreateRequire } from 'node:module'; const require = __khalaCreateRequire(import.meta.url);
|
|
2
|
+
import {
|
|
3
|
+
CURSOR_DEFAULT_SESSION,
|
|
4
|
+
CURSOR_WORKSPACE_ENV,
|
|
5
|
+
cursorSessionId,
|
|
6
|
+
readActivity,
|
|
7
|
+
readListeningMode
|
|
8
|
+
} from "./chunk-ZHMGB3PP.mjs";
|
|
9
|
+
import {
|
|
10
|
+
listChannels,
|
|
11
|
+
readCursor,
|
|
12
|
+
unreadCount
|
|
13
|
+
} from "./chunk-XUJIJ4KB.mjs";
|
|
14
|
+
import {
|
|
15
|
+
HARNESS_REGISTRY
|
|
16
|
+
} from "./chunk-DPW43YNC.mjs";
|
|
17
|
+
import {
|
|
18
|
+
SESSION_ID_PATTERN,
|
|
19
|
+
readJson
|
|
20
|
+
} from "./chunk-OW3DSTEZ.mjs";
|
|
21
|
+
|
|
22
|
+
// src/harness/claude.ts
|
|
23
|
+
var claude = {
|
|
24
|
+
id: "claude",
|
|
25
|
+
sessionSources: [(_meta, env) => env.CLAUDE_CODE_SESSION_ID],
|
|
26
|
+
codec: void 0,
|
|
27
|
+
rejoinable: () => true,
|
|
28
|
+
watcherStatus: true
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// src/wake/codex.ts
|
|
32
|
+
import path from "node:path";
|
|
33
|
+
|
|
34
|
+
// src/wake/idle-wake.ts
|
|
35
|
+
var CODEX_IDLE_WAKE_NOTICE = "Khala: channel messages are waiting. Continue.";
|
|
36
|
+
function codexIdleWakeArgv(sessionId) {
|
|
37
|
+
return ["queue", "--thread", sessionId, "--message", CODEX_IDLE_WAKE_NOTICE];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// src/wake/idle-wake-process.ts
|
|
41
|
+
import { spawn as nodeSpawn } from "node:child_process";
|
|
42
|
+
var CODEX_QUEUE_ENV_ALLOWLIST = [
|
|
43
|
+
"PATH",
|
|
44
|
+
"HOME",
|
|
45
|
+
"USER",
|
|
46
|
+
"LOGNAME",
|
|
47
|
+
"SHELL",
|
|
48
|
+
"LANG",
|
|
49
|
+
"LC_ALL",
|
|
50
|
+
"TMPDIR",
|
|
51
|
+
"CODEX_HOME",
|
|
52
|
+
"XDG_CONFIG_HOME",
|
|
53
|
+
"XDG_DATA_HOME",
|
|
54
|
+
"XDG_STATE_HOME",
|
|
55
|
+
"XDG_RUNTIME_DIR"
|
|
56
|
+
];
|
|
57
|
+
function scrubbedQueueEnv(source) {
|
|
58
|
+
const env = {};
|
|
59
|
+
for (const name of CODEX_QUEUE_ENV_ALLOWLIST) {
|
|
60
|
+
const value = source[name];
|
|
61
|
+
if (value !== void 0) env[name] = value;
|
|
62
|
+
}
|
|
63
|
+
return env;
|
|
64
|
+
}
|
|
65
|
+
function createCodexQueueProcessPort(deps) {
|
|
66
|
+
const spawn = deps.spawn ?? ((command, argv, options) => nodeSpawn(command, [...argv], options));
|
|
67
|
+
const timeoutMs = deps.timeoutMs ?? 1e4;
|
|
68
|
+
return {
|
|
69
|
+
run(argv, signal) {
|
|
70
|
+
return new Promise((resolve) => {
|
|
71
|
+
if (signal.aborted) return resolve({ status: "not_started" });
|
|
72
|
+
let child;
|
|
73
|
+
try {
|
|
74
|
+
child = spawn(deps.command, argv, { shell: false, stdio: "ignore", env: scrubbedQueueEnv(deps.env) });
|
|
75
|
+
} catch {
|
|
76
|
+
return resolve({ status: "not_started" });
|
|
77
|
+
}
|
|
78
|
+
let timedOut = false;
|
|
79
|
+
let stopped = false;
|
|
80
|
+
let force = null;
|
|
81
|
+
const stop = () => {
|
|
82
|
+
if (stopped) return;
|
|
83
|
+
stopped = true;
|
|
84
|
+
child.kill("SIGTERM");
|
|
85
|
+
force = setTimeout(() => {
|
|
86
|
+
child.kill("SIGKILL");
|
|
87
|
+
}, 500);
|
|
88
|
+
};
|
|
89
|
+
const timer = setTimeout(() => {
|
|
90
|
+
timedOut = true;
|
|
91
|
+
stop();
|
|
92
|
+
}, timeoutMs);
|
|
93
|
+
signal.addEventListener("abort", stop, { once: true });
|
|
94
|
+
const settle = (outcome) => {
|
|
95
|
+
clearTimeout(timer);
|
|
96
|
+
if (force !== null) clearTimeout(force);
|
|
97
|
+
signal.removeEventListener("abort", stop);
|
|
98
|
+
resolve(outcome);
|
|
99
|
+
};
|
|
100
|
+
child.on("error", () => settle({ status: "not_started" }));
|
|
101
|
+
child.on("close", (code) => settle(timedOut ? { status: "lost", cause: "timeout" } : code === 0 ? { status: "queued" } : { status: "exited", code: code ?? -1 }));
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// src/wake/codex.ts
|
|
108
|
+
function createCodexWaker(deps) {
|
|
109
|
+
if (!SESSION_ID_PATTERN.test(deps.threadId)) throw new TypeError("invalid_thread_id");
|
|
110
|
+
const port = deps.port ?? createCodexQueueProcessPort({ command: "codex", env: process.env });
|
|
111
|
+
const now = deps.now ?? Date.now;
|
|
112
|
+
const stderr = deps.stderr ?? ((line) => {
|
|
113
|
+
process.stderr.write(line);
|
|
114
|
+
});
|
|
115
|
+
const controller = new AbortController();
|
|
116
|
+
let pending;
|
|
117
|
+
const wakesAtCount = /* @__PURE__ */ new Map();
|
|
118
|
+
const identities = /* @__PURE__ */ new Map();
|
|
119
|
+
let stopped = false;
|
|
120
|
+
let again = false;
|
|
121
|
+
let inFlight;
|
|
122
|
+
const warn = (warning, status) => {
|
|
123
|
+
try {
|
|
124
|
+
stderr(JSON.stringify({ ok: false, warning, ...status ? { status } : {} }) + "\n");
|
|
125
|
+
} catch {
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
const evaluate = async () => {
|
|
129
|
+
try {
|
|
130
|
+
const channels = await listChannels(deps.files);
|
|
131
|
+
const eligible = [];
|
|
132
|
+
const currentIdentities = /* @__PURE__ */ new Map();
|
|
133
|
+
for (const channel of channels) {
|
|
134
|
+
const session = await readJson(channel.files.session);
|
|
135
|
+
const metadata = channel.legacy ? null : await readJson(path.join(channel.files.dir, "channel.json"));
|
|
136
|
+
currentIdentities.set(channel.key, JSON.stringify([session?.roomId ?? channel.roomId, session?.userId, metadata?.joinedAt]));
|
|
137
|
+
if (await readListeningMode(channel.files) === "async") continue;
|
|
138
|
+
eligible.push({
|
|
139
|
+
channel,
|
|
140
|
+
counts: await unreadCount(channel.files.dir),
|
|
141
|
+
cursor: await readCursor(channel.files)
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
const keys = new Set(currentIdentities.keys());
|
|
145
|
+
const changed = /* @__PURE__ */ new Set();
|
|
146
|
+
for (const [key, identity] of currentIdentities) {
|
|
147
|
+
if (identities.get(key) !== identity) changed.add(key);
|
|
148
|
+
}
|
|
149
|
+
for (const key of identities.keys()) if (!keys.has(key)) changed.add(key);
|
|
150
|
+
if (changed.size) {
|
|
151
|
+
for (const [key, wakes2] of [...wakesAtCount]) {
|
|
152
|
+
const pairs2 = JSON.parse(key);
|
|
153
|
+
if (!pairs2.some(([id]) => changed.has(id))) continue;
|
|
154
|
+
wakesAtCount.delete(key);
|
|
155
|
+
const remainingPairs = pairs2.filter(([id]) => !changed.has(id));
|
|
156
|
+
if (!remainingPairs.length) continue;
|
|
157
|
+
const remaining = JSON.stringify(remainingPairs);
|
|
158
|
+
wakesAtCount.set(remaining, Math.max(wakes2, wakesAtCount.get(remaining) ?? 0));
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
for (const key of changed) identities.delete(key);
|
|
162
|
+
for (const [key, identity] of currentIdentities) identities.set(key, identity);
|
|
163
|
+
const pairs = eligible.map(({ channel, cursor: cursor2 }) => [channel.key, cursor2.deliveredCount]).sort(([a], [b]) => a.localeCompare(b));
|
|
164
|
+
const budgetKey = JSON.stringify(pairs);
|
|
165
|
+
if (changed.size) {
|
|
166
|
+
const inheritedKey = JSON.stringify(pairs.filter(([key]) => !changed.has(key)));
|
|
167
|
+
const newUnread = eligible.some((item) => changed.has(item.channel.key) && item.counts.messages > 0);
|
|
168
|
+
wakesAtCount.set(budgetKey, newUnread ? 0 : wakesAtCount.get(inheritedKey) ?? 0);
|
|
169
|
+
if (newUnread || !eligible.some((item) => !changed.has(item.channel.key) && item.counts.messages > 0)) pending = void 0;
|
|
170
|
+
}
|
|
171
|
+
if (!eligible.some((item) => item.counts.messages > 0)) {
|
|
172
|
+
pending = void 0;
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
const activity = await readActivity(deps.files);
|
|
176
|
+
if (pending && Date.parse(activity.updatedAt) > pending.at) pending = void 0;
|
|
177
|
+
const at = now();
|
|
178
|
+
if (pending && at - pending.at < (deps.retryAfterMs ?? 6e4)) return;
|
|
179
|
+
if (stopped || activity.state !== "idle") return;
|
|
180
|
+
const wakes = wakesAtCount.get(budgetKey) ?? 0;
|
|
181
|
+
if (wakes >= 2) return;
|
|
182
|
+
for (const { channel } of eligible) {
|
|
183
|
+
if (await readListeningMode(channel.files) === "async") {
|
|
184
|
+
pending = void 0;
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (stopped) return;
|
|
189
|
+
pending = { at };
|
|
190
|
+
wakesAtCount.set(budgetKey, wakes + 1);
|
|
191
|
+
const outcome = await port.run(codexIdleWakeArgv(deps.threadId), controller.signal);
|
|
192
|
+
if (outcome.status !== "queued") warn("codex_queue_failed", outcome.status);
|
|
193
|
+
} catch {
|
|
194
|
+
warn("codex_waker_error");
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
const notify = () => {
|
|
198
|
+
if (stopped) return;
|
|
199
|
+
if (inFlight) {
|
|
200
|
+
again = true;
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
inFlight = (async () => {
|
|
204
|
+
do {
|
|
205
|
+
again = false;
|
|
206
|
+
await evaluate();
|
|
207
|
+
} while (again && !stopped);
|
|
208
|
+
})().finally(() => {
|
|
209
|
+
inFlight = void 0;
|
|
210
|
+
if (again && !stopped) notify();
|
|
211
|
+
});
|
|
212
|
+
};
|
|
213
|
+
const timer = setInterval(notify, deps.pollMs ?? 1e3);
|
|
214
|
+
timer.unref();
|
|
215
|
+
return {
|
|
216
|
+
notify,
|
|
217
|
+
async stop() {
|
|
218
|
+
stopped = true;
|
|
219
|
+
again = false;
|
|
220
|
+
clearInterval(timer);
|
|
221
|
+
controller.abort();
|
|
222
|
+
await inFlight;
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// src/harness/codex.ts
|
|
228
|
+
var install = async (flags, deps) => (await import("./chunk-6E3UCO22.mjs")).runCodexInstall(flags, deps);
|
|
229
|
+
var codex = {
|
|
230
|
+
id: "codex",
|
|
231
|
+
// A string metadata id wins even when empty or invalid; validation happens after selection.
|
|
232
|
+
sessionSources: [(meta, env) => typeof meta?.threadId === "string" ? meta.threadId : env.CODEX_THREAD_ID],
|
|
233
|
+
codec: void 0,
|
|
234
|
+
install,
|
|
235
|
+
uninstall: (flags, deps) => install([...flags, "--uninstall"], deps),
|
|
236
|
+
waker: createCodexWaker,
|
|
237
|
+
rejoinable: () => true
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
// src/harness/cursor.ts
|
|
241
|
+
var install2 = async (flags, deps) => (await import("./chunk-6E3UCO22.mjs")).runCursorInstall(flags, deps);
|
|
242
|
+
var cursor = {
|
|
243
|
+
id: "cursor",
|
|
244
|
+
sessionSources: [(_meta, env) => cursorSessionId(env[CURSOR_WORKSPACE_ENV])],
|
|
245
|
+
codec: void 0,
|
|
246
|
+
install: install2,
|
|
247
|
+
uninstall: (flags, deps) => install2([...flags, "--uninstall"], deps),
|
|
248
|
+
rejoinable: (source) => source !== CURSOR_DEFAULT_SESSION
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
// src/harness/index.ts
|
|
252
|
+
function createAdapterRegistry(adapters) {
|
|
253
|
+
const byId = /* @__PURE__ */ new Map();
|
|
254
|
+
for (const adapter of adapters) {
|
|
255
|
+
if (byId.has(adapter.id)) throw new Error(`duplicate_adapter: ${adapter.id}`);
|
|
256
|
+
if (!HARNESS_REGISTRY.some((row) => row.id === adapter.id)) throw new Error(`unregistered_adapter: ${adapter.id}`);
|
|
257
|
+
byId.set(adapter.id, adapter);
|
|
258
|
+
}
|
|
259
|
+
return Object.freeze({ resolve: (id) => byId.get(id) });
|
|
260
|
+
}
|
|
261
|
+
var ADAPTERS = Object.freeze([claude, codex, cursor]);
|
|
262
|
+
var registry = createAdapterRegistry(ADAPTERS);
|
|
263
|
+
var adapterFor = registry.resolve;
|
|
264
|
+
|
|
265
|
+
export {
|
|
266
|
+
adapterFor
|
|
267
|
+
};
|
|
@@ -11,11 +11,7 @@ import {
|
|
|
11
11
|
stateKey,
|
|
12
12
|
writeJsonAtomic,
|
|
13
13
|
writeStateFile
|
|
14
|
-
} from "./chunk-
|
|
15
|
-
import {
|
|
16
|
-
DEFAULT_LISTENING_MODE,
|
|
17
|
-
decodeListeningMode
|
|
18
|
-
} from "./chunk-AUJEUKVM.mjs";
|
|
14
|
+
} from "./chunk-OW3DSTEZ.mjs";
|
|
19
15
|
|
|
20
16
|
// src/inbox.ts
|
|
21
17
|
import * as fs from "node:fs/promises";
|
|
@@ -157,9 +153,12 @@ async function migrateLegacy(files) {
|
|
|
157
153
|
const nested = channelFiles(files, id);
|
|
158
154
|
await ensureStateDir(nested.dir);
|
|
159
155
|
const destinationInbox = await exists(nested.inbox);
|
|
160
|
-
if (
|
|
161
|
-
|
|
162
|
-
|
|
156
|
+
if (!destinationInbox && present[0] && await exists(nested.cursor)) {
|
|
157
|
+
const [legacyCursor, channelCursor] = await Promise.all([readCursor(files), readCursor(nested)]);
|
|
158
|
+
if (legacyCursor.deliveredCount !== channelCursor.deliveredCount || legacyCursor.lastDeliveredEventId !== channelCursor.lastDeliveredEventId) {
|
|
159
|
+
const safe = legacyCursor.deliveredCount < channelCursor.deliveredCount ? legacyCursor : channelCursor;
|
|
160
|
+
await writeStateFile(nested.dir, "cursor.json", safe);
|
|
161
|
+
console.warn(`Khala cursor migration: conflicting cursors for ${id}; using earlier position ${safe.deliveredCount} to avoid skipping messages (legacy=${legacyCursor.deliveredCount}, channel=${channelCursor.deliveredCount}).`);
|
|
163
162
|
}
|
|
164
163
|
}
|
|
165
164
|
const status = await readStatus(files);
|
|
@@ -203,25 +202,6 @@ function resolveChannel(list, ref) {
|
|
|
203
202
|
return { ok: false, code: matches.length ? "channel_ambiguous" : "channel_unknown", channels };
|
|
204
203
|
}
|
|
205
204
|
|
|
206
|
-
// src/mode.ts
|
|
207
|
-
async function readListeningMode(files) {
|
|
208
|
-
const content = await readJson(files.mode);
|
|
209
|
-
const decoded = decodeListeningMode(typeof content === "object" && content !== null ? content.mode : void 0);
|
|
210
|
-
return decoded.ok ? decoded.value : DEFAULT_LISTENING_MODE;
|
|
211
|
-
}
|
|
212
|
-
async function applyListeningMode(files, next, meta, now = () => /* @__PURE__ */ new Date()) {
|
|
213
|
-
const previous = await readListeningMode(files);
|
|
214
|
-
if (previous === "async" && next !== "async") {
|
|
215
|
-
for (let attempt = 0; attempt < 2; attempt++) {
|
|
216
|
-
const { cursor, entries } = await unread(files);
|
|
217
|
-
if (await advanceCursor(files, cursor, entries) !== "conflict") break;
|
|
218
|
-
if (attempt === 1) throw new StateError("storage_failed");
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
await writeJsonAtomic(files.mode, { mode: next, ...meta, updatedAt: now().toISOString() });
|
|
222
|
-
return { previous, next };
|
|
223
|
-
}
|
|
224
|
-
|
|
225
205
|
export {
|
|
226
206
|
readEntries,
|
|
227
207
|
readCursor,
|
|
@@ -232,7 +212,5 @@ export {
|
|
|
232
212
|
channelKey,
|
|
233
213
|
listChannels,
|
|
234
214
|
migrateLegacy,
|
|
235
|
-
resolveChannel
|
|
236
|
-
readListeningMode,
|
|
237
|
-
applyListeningMode
|
|
215
|
+
resolveChannel
|
|
238
216
|
};
|