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.
Files changed (30) hide show
  1. package/dist/{chunk-TZKEQIHP.mjs → chunk-2U4E7AM7.mjs} +18 -3
  2. package/dist/{chunk-EXHCVFVH.mjs → chunk-2XTYRBYX.mjs} +10 -4
  3. package/dist/{chunk-MBLP7LQJ.mjs → chunk-4X35OUAN.mjs} +11 -11
  4. package/dist/{chunk-PXKGKVGW.mjs → chunk-55KBLEX5.mjs} +1 -1
  5. package/dist/{chunk-RSDOTTZQ.mjs → chunk-6E3UCO22.mjs} +32 -11
  6. package/dist/{chunk-7G2G4E4U.mjs → chunk-AWBKVTLV.mjs} +15 -13
  7. package/dist/{chunk-V7M27QLT.mjs → chunk-B2MOLCJM.mjs} +7 -5
  8. package/dist/{chunk-IR3VN2ID.mjs → chunk-CWHGNVZV.mjs} +0 -18
  9. package/dist/chunk-DPW43YNC.mjs +36 -0
  10. package/dist/{chunk-V4Y26O4R.mjs → chunk-FH2CZX5T.mjs} +14 -12
  11. package/dist/{chunk-HB3UGIZG.mjs → chunk-IULQYQJZ.mjs} +3 -3
  12. package/dist/{chunk-Y4WVILEJ.mjs → chunk-JG5J3SHW.mjs} +1 -1
  13. package/dist/{chunk-Y2DRTRBJ.mjs → chunk-OECJ45NM.mjs} +2 -2
  14. package/dist/{chunk-KCJFSVDN.mjs → chunk-OW3DSTEZ.mjs} +4 -2
  15. package/dist/{chunk-OLZ62X7S.mjs → chunk-PFDUXY3N.mjs} +1 -1
  16. package/dist/{chunk-H46OG22B.mjs → chunk-PY2ZNVNY.mjs} +1 -1
  17. package/dist/{chunk-SQXDATEC.mjs → chunk-RRBIVI77.mjs} +1 -1
  18. package/dist/{chunk-6LC66VHK.mjs → chunk-SZIEG3RS.mjs} +6 -5
  19. package/dist/chunk-WHONG7UF.mjs +267 -0
  20. package/dist/{chunk-T7YRVE43.mjs → chunk-XUJIJ4KB.mjs} +8 -30
  21. package/dist/{chunk-EW22K3JO.mjs → chunk-Z4EDYB6Y.mjs} +176 -293
  22. package/dist/chunk-ZHMGB3PP.mjs +82 -0
  23. package/dist/khala.mjs +10 -10
  24. package/dist/web/assets/local-K8EcTVxW.js +9 -0
  25. package/dist/web/assets/local-R2y0Gl4T.css +1 -0
  26. package/dist/web/index.html +2 -2
  27. package/package.json +1 -1
  28. package/dist/chunk-VUE2DDYY.mjs +0 -34
  29. package/dist/web/assets/local-C1vz22ps.js +0 -9
  30. 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-KCJFSVDN.mjs";
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 (present[2] && !destinationInbox && present[0] && await exists(nested.cursor)) {
161
- if (await fs2.readFile(files.cursor, "utf8") !== await fs2.readFile(nested.cursor, "utf8")) {
162
- throw new StateError("storage_failed");
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
  };