khala-cli 0.4.6 → 0.4.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/README.md +18 -1
  2. package/dist/{chunk-2U4E7AM7.mjs → chunk-2DHIAFEV.mjs} +294 -101
  3. package/dist/{chunk-CWHGNVZV.mjs → chunk-2VCAW4GH.mjs} +1 -1
  4. package/dist/{chunk-P5CP32X2.mjs → chunk-54XFVSEC.mjs} +1 -1
  5. package/dist/chunk-5BQTYU66.mjs +886 -0
  6. package/dist/{chunk-RRBIVI77.mjs → chunk-5LIE5GUU.mjs} +1 -1
  7. package/dist/{chunk-6E3UCO22.mjs → chunk-5PUQT3YS.mjs} +200 -20
  8. package/dist/{chunk-55KBLEX5.mjs → chunk-7HBRFY3Z.mjs} +1 -1
  9. package/dist/{chunk-PFDUXY3N.mjs → chunk-APMWDF6B.mjs} +1 -1
  10. package/dist/{chunk-PY2ZNVNY.mjs → chunk-BKLW2SPG.mjs} +55 -8
  11. package/dist/{chunk-OW3DSTEZ.mjs → chunk-CBPLCCRZ.mjs} +5 -5
  12. package/dist/{chunk-JG5J3SHW.mjs → chunk-CJTCU755.mjs} +4 -4
  13. package/dist/{chunk-AWBKVTLV.mjs → chunk-FH34B3I3.mjs} +14 -17
  14. package/dist/chunk-FV32AQTC.mjs +40 -0
  15. package/dist/{chunk-FH2CZX5T.mjs → chunk-H3K4GA2P.mjs} +83 -36
  16. package/dist/{chunk-AUJEUKVM.mjs → chunk-IQJQ4RF5.mjs} +1 -1
  17. package/dist/{chunk-SN3STAO2.mjs → chunk-NKX3SLS7.mjs} +40 -3
  18. package/dist/{chunk-Z4EDYB6Y.mjs → chunk-RNDUFTBF.mjs} +97 -57
  19. package/dist/{chunk-OECJ45NM.mjs → chunk-VAI7IUI4.mjs} +24 -8
  20. package/dist/{chunk-XUJIJ4KB.mjs → chunk-VKQ3KUYO.mjs} +16 -2
  21. package/dist/{chunk-4X35OUAN.mjs → chunk-VUSV2ADL.mjs} +92 -97
  22. package/dist/{chunk-2XTYRBYX.mjs → chunk-W6R4UJUQ.mjs} +12 -5
  23. package/dist/{chunk-B2MOLCJM.mjs → chunk-Z4JF2M5C.mjs} +7 -9
  24. package/dist/{chunk-SZIEG3RS.mjs → chunk-ZJ4W7P33.mjs} +12 -14
  25. package/dist/khala.mjs +11 -11
  26. package/dist/web/assets/local-BQl2KrE1.js +9 -0
  27. package/dist/web/assets/local-OwDq47O4.css +1 -0
  28. package/dist/web/index.html +2 -2
  29. package/package.json +1 -1
  30. package/dist/chunk-DPW43YNC.mjs +0 -36
  31. package/dist/chunk-IULQYQJZ.mjs +0 -144
  32. package/dist/chunk-WHONG7UF.mjs +0 -267
  33. package/dist/chunk-ZHMGB3PP.mjs +0 -82
  34. package/dist/web/assets/local-K8EcTVxW.js +0 -9
  35. package/dist/web/assets/local-R2y0Gl4T.css +0 -1
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # khala-cli
2
2
 
3
- The `khala` command connects your existing Claude Code, Codex or Cursor session to a
3
+ The `khala` command connects your existing Claude Code, Codex, Cursor or OpenCode session to a
4
4
  [Khala](https://khala.aiur.team) channel: an MCP server (`khala_join`, `khala_status`,
5
5
  `khala_read`, `khala_send`, `khala_event`), delivery hooks that bring channel messages into
6
6
  the session, and local channels on this computer. Requires Node 22 or newer.
@@ -40,6 +40,23 @@ under Settings → MCP). Undo with `npx -y khala-cli install cursor --uninstall`
40
40
  cannot wake an idle chat; new messages arrive when a chat finishes a turn (Sync) or after a
41
41
  tool call (Steer).
42
42
 
43
+ ## OpenCode
44
+
45
+ ```sh
46
+ npx -y khala-cli install opencode
47
+ ```
48
+
49
+ This installs a stable CLI and, when published on npm, pins `khala-opencode` at the same version in the global
50
+ `~/.config/opencode/opencode.json` (or under `XDG_CONFIG_HOME`). The plugin registers
51
+ Khala's MCP server. If the version is unpublished or the registry check fails, it configures
52
+ MCP-only mode (Async, no wake); re-run `khala install opencode` after updating. Existing
53
+ `opencode.jsonc` files require a manual merge or conversion to JSON before retrying. Restart OpenCode after installation. Existing settings and sibling
54
+ plugins are preserved. Undo with `npx -y khala-cli install opencode --uninstall`.
55
+
56
+ For unpublished-build testing only, `KHALA_INSTALL_SPEC` selects a CLI tarball and
57
+ `KHALA_OPENCODE_PLUGIN_SPEC` forces plugin mode with a spec such as
58
+ `file:/tmp/khala-opencode-0.1.0.tgz`.
59
+
43
60
  ## Then
44
61
 
45
62
  Tell your agent "Join this Khala channel: <link>" and open the confirmation link it returns.
@@ -1,19 +1,28 @@
1
1
  import { createRequire as __khalaCreateRequire } from 'node:module'; const require = __khalaCreateRequire(import.meta.url);
2
2
  import {
3
- HARNESS_REGISTRY,
4
- LEGACY_HARNESSES,
5
- harnessInfo
6
- } from "./chunk-DPW43YNC.mjs";
3
+ KHALA_AGENT_VERSION
4
+ } from "./chunk-5LIE5GUU.mjs";
5
+ import {
6
+ bundle
7
+ } from "./chunk-APMWDF6B.mjs";
8
+ import {
9
+ StateError,
10
+ ensureStateDir,
11
+ readJson,
12
+ stateRoot
13
+ } from "./chunk-CBPLCCRZ.mjs";
7
14
  import {
8
15
  LISTENING_MODES
9
- } from "./chunk-AUJEUKVM.mjs";
16
+ } from "./chunk-IQJQ4RF5.mjs";
10
17
  import {
18
+ HARNESS_REGISTRY,
11
19
  M1_LABEL_MAX_BYTES,
12
20
  array,
13
21
  decodeWith,
14
22
  displayText,
15
23
  elementPath,
16
24
  fail,
25
+ harnessInfo,
17
26
  identifier,
18
27
  literal,
19
28
  nullable,
@@ -28,7 +37,7 @@ import {
28
37
  utcTimestamp,
29
38
  utf8Length,
30
39
  version
31
- } from "./chunk-SN3STAO2.mjs";
40
+ } from "./chunk-NKX3SLS7.mjs";
32
41
 
33
42
  // src/client.ts
34
43
  var KhalaClientError = class extends Error {
@@ -58,8 +67,8 @@ function fnv1a(value) {
58
67
  function defaultHumanColor(ownerId) {
59
68
  return HUMAN_COLOR_IDS[fnv1a(ownerId) % HUMAN_COLOR_IDS.length];
60
69
  }
61
- function readHumanColorId(input, path) {
62
- if (!isHumanColorId(input)) return fail(path, "invalid_value");
70
+ function readHumanColorId(input, path2) {
71
+ if (!isHumanColorId(input)) return fail(path2, "invalid_value");
63
72
  return input;
64
73
  }
65
74
 
@@ -74,8 +83,8 @@ function normalizeInitials(input) {
74
83
  function isCanonicalInitials(value) {
75
84
  return typeof value === "string" && normalizeInitials(value) === value;
76
85
  }
77
- function readHumanInitials(input, path) {
78
- if (!isCanonicalInitials(input)) return fail(path, "invalid_value");
86
+ function readHumanInitials(input, path2) {
87
+ if (!isCanonicalInitials(input)) return fail(path2, "invalid_value");
79
88
  return input;
80
89
  }
81
90
 
@@ -103,9 +112,6 @@ function checkNewUsername(input) {
103
112
  if (!checked.ok) return checked;
104
113
  return newAgentSuffix.test(checked.name) ? { ok: false, error: "reserved" } : checked;
105
114
  }
106
- var MODEL_NAMES = Object.fromEntries(
107
- LEGACY_HARNESSES.map((id) => [id, harnessInfo(id).modelName])
108
- );
109
115
  function defaultAgentName(username, harness, n = 1) {
110
116
  return `${username}-${harnessInfo(harness).modelName}${n === 1 ? "" : `-${n}`}`;
111
117
  }
@@ -161,90 +167,90 @@ var localRoomKey = (roomId) => {
161
167
  var localRoomPath = (roomId, tail) => `/api/local/rooms/${encodeURIComponent(roomId)}/${tail}`;
162
168
  var LOCAL_OWNER_ID = "local-owner";
163
169
  var LOCAL_OWNER_DEVICE_ID = "KH_LOCAL_OWNER";
164
- function readLocalRoomId(input, path) {
165
- const value = readRoomId(input, path);
166
- if (!isLocalRoomId(value)) fail(path, "invalid_value");
170
+ function readLocalRoomId(input, path2) {
171
+ const value = readRoomId(input, path2);
172
+ if (!isLocalRoomId(value)) fail(path2, "invalid_value");
167
173
  return value;
168
174
  }
169
- function readLocalUserId(input, path) {
170
- const value = readMatrixUserId(input, path);
171
- if (!isLocalUserId(value)) fail(path, "invalid_value");
175
+ function readLocalUserId(input, path2) {
176
+ const value = readMatrixUserId(input, path2);
177
+ if (!isLocalUserId(value)) fail(path2, "invalid_value");
172
178
  return value;
173
179
  }
174
- function readLocalEventId(input, path) {
175
- const value = identifier(input, path);
176
- if (!isLocalEventId(value)) fail(path, "invalid_value");
180
+ function readLocalEventId(input, path2) {
181
+ const value = identifier(input, path2);
182
+ if (!isLocalEventId(value)) fail(path2, "invalid_value");
177
183
  return value;
178
184
  }
179
- function readLocalTxnId(input, path) {
180
- const value = identifier(input, path);
181
- if (!isLocalTxnId(value)) fail(path, "invalid_value");
185
+ function readLocalTxnId(input, path2) {
186
+ const value = identifier(input, path2);
187
+ if (!isLocalTxnId(value)) fail(path2, "invalid_value");
182
188
  return value;
183
189
  }
184
- function readLocalUsername(input, path) {
185
- const value = identifier(input, path);
190
+ function readLocalUsername(input, path2) {
191
+ const value = identifier(input, path2);
186
192
  const checked = checkName(value, "username");
187
- if (!checked.ok || checked.name !== value) fail(path, "invalid_value");
193
+ if (!checked.ok || checked.name !== value) fail(path2, "invalid_value");
188
194
  return value;
189
195
  }
190
- function readLocalInitials(input, path) {
191
- return readHumanInitials(input, path);
196
+ function readLocalInitials(input, path2) {
197
+ return readHumanInitials(input, path2);
192
198
  }
193
- function record(input, path, required, optional = []) {
199
+ function record(input, path2, required, optional = []) {
194
200
  const present = optional.filter((key) => typeof input === "object" && input !== null && Object.hasOwn(input, key));
195
- return object(input, path, [...required, ...present]);
201
+ return object(input, path2, [...required, ...present]);
196
202
  }
197
203
  function has(input, key) {
198
204
  return typeof input === "object" && input !== null && Object.hasOwn(input, key);
199
205
  }
200
- function plainObject(input, path) {
201
- if (typeof input !== "object" || input === null || Array.isArray(input)) fail(path, "not_object");
206
+ function plainObject(input, path2) {
207
+ if (typeof input !== "object" || input === null || Array.isArray(input)) fail(path2, "not_object");
202
208
  const prototype = Object.getPrototypeOf(input);
203
- if (prototype !== Object.prototype && prototype !== null) fail(path, "not_object");
209
+ if (prototype !== Object.prototype && prototype !== null) fail(path2, "not_object");
204
210
  return input;
205
211
  }
206
- function contentObject(input, path) {
207
- const value = plainObject(input, path);
212
+ function contentObject(input, path2) {
213
+ const value = plainObject(input, path2);
208
214
  let encoded;
209
215
  try {
210
216
  encoded = JSON.stringify(value);
211
217
  } catch {
212
- fail(path, "invalid_value");
218
+ fail(path2, "invalid_value");
213
219
  }
214
- if (utf8Length(encoded) > LOCAL_CONTENT_MAX_BYTES) fail(path, "too_long");
220
+ if (utf8Length(encoded) > LOCAL_CONTENT_MAX_BYTES) fail(path2, "too_long");
215
221
  return value;
216
222
  }
217
- function channelName(input, path) {
218
- const value = displayName(input, path);
219
- if ([...value].length > LOCAL_CHANNEL_NAME_MAX_CHARS) fail(path, "too_long");
220
- if (value !== value.trim()) fail(path, "invalid_value");
223
+ function channelName(input, path2) {
224
+ const value = displayName(input, path2);
225
+ if ([...value].length > LOCAL_CHANNEL_NAME_MAX_CHARS) fail(path2, "too_long");
226
+ if (value !== value.trim()) fail(path2, "invalid_value");
221
227
  return value;
222
228
  }
223
- function displayName(input, path) {
224
- const value = displayText(input, path, M1_LABEL_MAX_BYTES);
225
- if (value.length === 0) fail(path, "empty");
229
+ function displayName(input, path2) {
230
+ const value = displayText(input, path2, M1_LABEL_MAX_BYTES);
231
+ if (value.length === 0) fail(path2, "empty");
226
232
  return value;
227
233
  }
228
- function seq(input, path) {
229
- const value = safeInteger(input, path);
230
- if (value === 0) fail(path, "invalid_value");
234
+ function seq(input, path2) {
235
+ const value = safeInteger(input, path2);
236
+ if (value === 0) fail(path2, "invalid_value");
231
237
  return value;
232
238
  }
233
- function readCreateContent(input, path) {
234
- const r = record(input, path, ["name", "createdBy"], ["operationId"]);
239
+ function readCreateContent(input, path2) {
240
+ const r = record(input, path2, ["name", "createdBy"], ["operationId"]);
235
241
  return {
236
242
  name: channelName(r.field("name"), r.at("name")),
237
243
  createdBy: readLocalUserId(r.field("createdBy"), r.at("createdBy")),
238
244
  ...has(input, "operationId") ? { operationId: readLocalTxnId(r.field("operationId"), r.at("operationId")) } : {}
239
245
  };
240
246
  }
241
- function readNameContent(input, path) {
242
- const r = object(input, path, ["name"]);
247
+ function readNameContent(input, path2) {
248
+ const r = object(input, path2, ["name"]);
243
249
  return { name: channelName(r.field("name"), r.at("name")) };
244
250
  }
245
- function readMemberContent(input, path) {
251
+ function readMemberContent(input, path2) {
246
252
  const modeKey = "com.khala.listening_mode";
247
- const r = record(input, path, ["user", "membership", "displayname", "kind"], ["harness", "invitedBy", modeKey]);
253
+ const r = record(input, path2, ["user", "membership", "displayname", "kind"], ["harness", "invitedBy", modeKey]);
248
254
  const kind = literal(r.field("kind"), r.at("kind"), ["human", "agent"]);
249
255
  if (kind === "human") {
250
256
  for (const key of ["harness", modeKey]) if (has(input, key)) fail(r.at(key), "invalid_value");
@@ -266,28 +272,28 @@ function decodeLocalMemberContent(input) {
266
272
  return decodeWith(() => readMemberContent(input, ""));
267
273
  }
268
274
  var EVENT_TYPES = ["m.room.create", "m.room.name", "m.room.member", "m.room.message", "com.khala.event.v1", "com.khala.listening_mode.v1"];
269
- function readContent(input, path, type) {
275
+ function readContent(input, path2, type) {
270
276
  switch (type) {
271
277
  case "m.room.create":
272
- return readCreateContent(input, path);
278
+ return readCreateContent(input, path2);
273
279
  case "m.room.name":
274
- return readNameContent(input, path);
280
+ return readNameContent(input, path2);
275
281
  case "m.room.member":
276
- return readMemberContent(input, path);
282
+ return readMemberContent(input, path2);
277
283
  default: {
278
- const content = contentObject(input, path);
284
+ const content = contentObject(input, path2);
279
285
  if (type === "m.room.message") {
280
- literal(content["msgtype"], `${path}.msgtype`, ["m.text", "m.notice"]);
281
- const body = text(content["body"], `${path}.body`, LOCAL_CONTENT_MAX_BYTES);
282
- if (body.length === 0) fail(`${path}.body`, "empty");
283
- if (body.length > LOCAL_BODY_MAX_CHARS) fail(`${path}.body`, "too_long");
286
+ literal(content["msgtype"], `${path2}.msgtype`, ["m.text", "m.notice"]);
287
+ const body = text(content["body"], `${path2}.body`, LOCAL_CONTENT_MAX_BYTES);
288
+ if (body.length === 0) fail(`${path2}.body`, "empty");
289
+ if (body.length > LOCAL_BODY_MAX_CHARS) fail(`${path2}.body`, "too_long");
284
290
  }
285
291
  return content;
286
292
  }
287
293
  }
288
294
  }
289
- function readEvent(input, path) {
290
- const r = record(input, path, ["seq", "eventId", "roomId", "type", "sender", "ts", "content"], ["txnId", "previousContent"]);
295
+ function readEvent(input, path2) {
296
+ const r = record(input, path2, ["seq", "eventId", "roomId", "type", "sender", "ts", "content"], ["txnId", "previousContent"]);
291
297
  const type = literal(r.field("type"), r.at("type"), EVENT_TYPES);
292
298
  return {
293
299
  seq: seq(r.field("seq"), r.at("seq")),
@@ -304,16 +310,16 @@ function readEvent(input, path) {
304
310
  function decodeLocalEvent(input) {
305
311
  return decodeWith(() => readEvent(input, ""));
306
312
  }
307
- function limitedArray(input, path, max, read) {
308
- const values = array(input, path);
309
- if (values.length > max) fail(path, "too_long");
310
- return values.map((value, i) => read(value, elementPath(path, i)));
313
+ function limitedArray(input, path2, max, read) {
314
+ const values = array(input, path2);
315
+ if (values.length > max) fail(path2, "too_long");
316
+ return values.map((value, i) => read(value, elementPath(path2, i)));
311
317
  }
312
- function readEvents(input, path, history) {
313
- const events = limitedArray(input, path, history ? LOCAL_HISTORY_LIMIT_MAX : LOCAL_EVENTS_PAGE_MAX, readEvent);
318
+ function readEvents(input, path2, history) {
319
+ const events = limitedArray(input, path2, history ? LOCAL_HISTORY_LIMIT_MAX : LOCAL_EVENTS_PAGE_MAX, readEvent);
314
320
  events.forEach((event, i) => {
315
- if (i > 0 && event.seq <= events[i - 1].seq) fail(`${elementPath(path, i)}.seq`, "invalid_value");
316
- if (history && event.type !== "m.room.message" && event.type !== "com.khala.event.v1") fail(`${elementPath(path, i)}.type`, "invalid_value");
321
+ if (i > 0 && event.seq <= events[i - 1].seq) fail(`${elementPath(path2, i)}.seq`, "invalid_value");
322
+ if (history && event.type !== "m.room.message" && event.type !== "com.khala.event.v1") fail(`${elementPath(path2, i)}.type`, "invalid_value");
317
323
  });
318
324
  return events;
319
325
  }
@@ -335,8 +341,8 @@ function decodeLocalHistoryPage(input) {
335
341
  };
336
342
  });
337
343
  }
338
- function readLocalMe(input, path) {
339
- const r = record(input, path, ["userId", "roomId", "roomName", "membership", "displayName"], ["invitedBy"]);
344
+ function readLocalMe(input, path2) {
345
+ const r = record(input, path2, ["userId", "roomId", "roomName", "membership", "displayName"], ["invitedBy"]);
340
346
  return {
341
347
  userId: readLocalUserId(r.field("userId"), r.at("userId")),
342
348
  roomId: readLocalRoomId(r.field("roomId"), r.at("roomId")),
@@ -349,8 +355,8 @@ function readLocalMe(input, path) {
349
355
  function decodeLocalMe(input) {
350
356
  return decodeWith(() => readLocalMe(input, ""));
351
357
  }
352
- function readLocalJoined(input, path) {
353
- const r = record(input, path, ["seq", "ts"], []);
358
+ function readLocalJoined(input, path2) {
359
+ const r = record(input, path2, ["seq", "ts"], []);
354
360
  return {
355
361
  seq: seq(r.field("seq"), r.at("seq")),
356
362
  ts: safeInteger(r.field("ts"), r.at("ts"))
@@ -359,8 +365,8 @@ function readLocalJoined(input, path) {
359
365
  function decodeLocalJoined(input) {
360
366
  return decodeWith(() => readLocalJoined(input, ""));
361
367
  }
362
- function readLocalSendResult(input, path) {
363
- const r = record(input, path, ["eventId"], []);
368
+ function readLocalSendResult(input, path2) {
369
+ const r = record(input, path2, ["eventId"], []);
364
370
  return {
365
371
  eventId: readLocalEventId(r.field("eventId"), r.at("eventId"))
366
372
  };
@@ -368,8 +374,8 @@ function readLocalSendResult(input, path) {
368
374
  function decodeLocalSendResult(input) {
369
375
  return decodeWith(() => readLocalSendResult(input, ""));
370
376
  }
371
- function readLocalErrorBody(input, path) {
372
- const r = record(input, path, ["error"], []);
377
+ function readLocalErrorBody(input, path2) {
378
+ const r = record(input, path2, ["error"], []);
373
379
  return {
374
380
  error: identifier(r.field("error"), r.at("error"))
375
381
  };
@@ -377,8 +383,8 @@ function readLocalErrorBody(input, path) {
377
383
  function decodeLocalErrorBody(input) {
378
384
  return decodeWith(() => readLocalErrorBody(input, ""));
379
385
  }
380
- function readOwnerProfile(input, path) {
381
- const r = record(input, path, ["v", "username", "color", "initials", "updatedAt"]);
386
+ function readOwnerProfile(input, path2) {
387
+ const r = record(input, path2, ["v", "username", "color", "initials", "updatedAt"]);
382
388
  return {
383
389
  v: version(r.field("v"), r.at("v")),
384
390
  username: readLocalUsername(r.field("username"), r.at("username")),
@@ -390,11 +396,11 @@ function readOwnerProfile(input, path) {
390
396
  function decodeOwnerProfile(input) {
391
397
  return decodeWith(() => readOwnerProfile(input, ""));
392
398
  }
393
- function nullableInitials(input, path) {
394
- return nullable(input, (value) => readLocalInitials(value, path));
399
+ function nullableInitials(input, path2) {
400
+ return nullable(input, (value) => readLocalInitials(value, path2));
395
401
  }
396
- function readMember(input, path) {
397
- const r = record(input, path, ["userId", "participantId", "ownerId", "deviceId", "displayName", "kind", "membership"], ["harness", "ownerLabel", "listeningMode"]);
402
+ function readMember(input, path2) {
403
+ const r = record(input, path2, ["userId", "participantId", "ownerId", "deviceId", "displayName", "kind", "membership"], ["harness", "ownerLabel", "listeningMode"]);
398
404
  const userId = readLocalUserId(r.field("userId"), r.at("userId"));
399
405
  const participantId = readLocalUserId(r.field("participantId"), r.at("participantId"));
400
406
  if (participantId !== userId) fail(r.at("participantId"), "invalid_value");
@@ -450,18 +456,18 @@ function decodeHelperFile(input) {
450
456
  };
451
457
  });
452
458
  }
453
- function readSha256(input, path) {
454
- const value = identifier(input, path);
455
- if (!/^[0-9a-f]{64}$/u.test(value)) fail(path, "invalid_value");
459
+ function readSha256(input, path2) {
460
+ const value = identifier(input, path2);
461
+ if (!/^[0-9a-f]{64}$/u.test(value)) fail(path2, "invalid_value");
456
462
  return value;
457
463
  }
458
464
  function decodeChannelSecrets(input) {
459
465
  return decodeWith(() => {
460
466
  const r = object(input, "", ["v", "links", "members"]);
461
467
  const links = Object.fromEntries(Object.entries(plainObject(r.field("links"), r.at("links"))).map(([key, value]) => {
462
- const path = `${r.at("links")}.${key}`;
463
- readSha256(key, path);
464
- const link = record(value, path, ["expiresAt", "kind"], ["consumedAt"]);
468
+ const path2 = `${r.at("links")}.${key}`;
469
+ readSha256(key, path2);
470
+ const link = record(value, path2, ["expiresAt", "kind"], ["consumedAt"]);
465
471
  return [key, {
466
472
  expiresAt: utcTimestamp(link.field("expiresAt"), link.at("expiresAt")),
467
473
  kind: literal(link.field("kind"), link.at("kind"), ["join"]),
@@ -469,9 +475,9 @@ function decodeChannelSecrets(input) {
469
475
  }];
470
476
  }));
471
477
  const members = Object.fromEntries(Object.entries(plainObject(r.field("members"), r.at("members"))).map(([key, value]) => {
472
- const path = `${r.at("members")}.${key}`;
473
- if (!isLocalAgentUserId(key)) fail(path, "invalid_value");
474
- const member = record(value, path, ["tokenSha256"], ["sessionKey"]);
478
+ const path2 = `${r.at("members")}.${key}`;
479
+ if (!isLocalAgentUserId(key)) fail(path2, "invalid_value");
480
+ const member = record(value, path2, ["tokenSha256"], ["sessionKey"]);
475
481
  return [key, {
476
482
  tokenSha256: readSha256(member.field("tokenSha256"), member.at("tokenSha256")),
477
483
  ...has(value, "sessionKey") ? { sessionKey: readSha256(member.field("sessionKey"), member.at("sessionKey")) } : {}
@@ -481,6 +487,191 @@ function decodeChannelSecrets(input) {
481
487
  });
482
488
  }
483
489
 
490
+ // src/local/lifecycle.ts
491
+ import * as fs from "node:fs/promises";
492
+ import { close as closeFd } from "node:fs";
493
+ import path from "node:path";
494
+ import { fileURLToPath } from "node:url";
495
+ import { setTimeout as sleepDefault } from "node:timers/promises";
496
+ import { spawn as nodeSpawn } from "node:child_process";
497
+ function helperPaths(env = process.env) {
498
+ const root = path.join(stateRoot(env), "local");
499
+ const candidate = /^\d{1,5}$/u.test(env.KHALA_LOCAL_PORT ?? "") ? Number(env.KHALA_LOCAL_PORT) : 0;
500
+ const port = candidate >= 1 && candidate <= 65535 ? candidate : LOCAL_DEFAULT_PORT;
501
+ return { root, helperFile: path.join(root, "helper.json"), logFile: path.join(root, "helper.log"), port, origin: `http://127.0.0.1:${port}` };
502
+ }
503
+ async function readHelperFile(env = process.env) {
504
+ const decoded = decodeHelperFile(await readJson(helperPaths(env).helperFile));
505
+ if (!decoded.ok) return null;
506
+ const file = decoded.value;
507
+ if (!Number.isSafeInteger(file.pid) || file.pid <= 0 || !Number.isSafeInteger(file.port) || file.port <= 0 || file.port > 65535 || file.origin !== `http://127.0.0.1:${file.port}` || !/^[A-Za-z0-9_-]{43}$/u.test(file.adminToken)) return null;
508
+ return file;
509
+ }
510
+ function helperBinPath() {
511
+ return fileURLToPath(bundle ? new URL("./khala.mjs", import.meta.url) : new URL("../../bin/khala.mjs", import.meta.url));
512
+ }
513
+ var HELPER_ENV_ALLOWLIST = [
514
+ "PATH",
515
+ "HOME",
516
+ "USER",
517
+ "LOGNAME",
518
+ "LANG",
519
+ "LC_ALL",
520
+ "TMPDIR",
521
+ "XDG_STATE_HOME",
522
+ "NODE_OPTIONS",
523
+ "KHALA_EGRESS_LOG",
524
+ "KHALA_LOCAL_PORT",
525
+ "KHALA_LOCAL_IDLE_MS",
526
+ "KHALA_LOCAL_WEB_DIR"
527
+ ];
528
+ function helperChildEnv(env) {
529
+ const childEnv = {};
530
+ for (const key of HELPER_ENV_ALLOWLIST) if (env[key] !== void 0) childEnv[key] = env[key];
531
+ return childEnv;
532
+ }
533
+ var attempts = /* @__PURE__ */ new Map();
534
+ var restarted = false;
535
+ var recovering = /* @__PURE__ */ new Map();
536
+ function olderHelperVersion(version2) {
537
+ if (typeof version2 !== "string") return true;
538
+ const parse = (value) => /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/u.exec(value);
539
+ const helper = parse(version2), cli = parse(KHALA_AGENT_VERSION);
540
+ if (!helper || !cli) return true;
541
+ for (let i = 1; i <= 3; i++) {
542
+ if (Number(helper[i]) !== Number(cli[i])) return Number(helper[i]) < Number(cli[i]);
543
+ }
544
+ if (helper[4] === cli[4]) return false;
545
+ if (!helper[4] || !cli[4]) return Boolean(helper[4]);
546
+ const a = helper[4].split("."), b = cli[4].split(".");
547
+ for (let i = 0; i < Math.max(a.length, b.length); i++) {
548
+ if (a[i] === b[i]) continue;
549
+ if (a[i] === void 0 || b[i] === void 0) return a[i] === void 0;
550
+ const an = /^\d+$/u.test(a[i]), bn = /^\d+$/u.test(b[i]);
551
+ if (an && bn) return Number(a[i]) < Number(b[i]);
552
+ if (an !== bn) return an;
553
+ return a[i] < b[i];
554
+ }
555
+ return false;
556
+ }
557
+ async function healthy(fetchImpl, file) {
558
+ try {
559
+ const response = await fetchImpl(`${file.origin}/healthz`, { signal: AbortSignal.timeout(500) });
560
+ if (response.status !== 200) {
561
+ await response.body?.cancel();
562
+ return null;
563
+ }
564
+ const body = await response.json();
565
+ return typeof body === "object" && body !== null && "ok" in body && body.ok === true && "pid" in body && body.pid === file.pid ? { version: "version" in body ? body.version : void 0 } : null;
566
+ } catch {
567
+ return null;
568
+ }
569
+ }
570
+ function connection(file) {
571
+ return { origin: file.origin, adminToken: file.adminToken };
572
+ }
573
+ async function attempt(env, deps, restartOrigin) {
574
+ const fetchImpl = deps.fetch ?? globalThis.fetch;
575
+ const spawn = deps.spawn ?? ((command, argv, options) => nodeSpawn(command, [...argv], options));
576
+ const sleep = deps.sleep ?? sleepDefault;
577
+ const now = deps.now ?? Date.now;
578
+ const existing = await readHelperFile(env);
579
+ const status = existing ? await healthy(fetchImpl, existing) : null;
580
+ if (existing && status) {
581
+ if (!olderHelperVersion(status.version) && restartOrigin === void 0 || restarted) return connection(existing);
582
+ if (restartOrigin !== void 0 && existing.origin !== restartOrigin) return connection(existing);
583
+ restarted = true;
584
+ recovering.set(helperPaths(env).helperFile, existing.origin);
585
+ try {
586
+ const response = await fetchImpl(`${existing.origin}/api/local/shutdown`, {
587
+ method: "POST",
588
+ headers: { authorization: `Bearer ${existing.adminToken}` },
589
+ signal: AbortSignal.timeout(2e3)
590
+ });
591
+ await response.body?.cancel();
592
+ if (response.status !== 204) throw new Error("shutdown_failed");
593
+ const deadline = now() + 2e3;
594
+ for (let i = 0; i < 20; i++) {
595
+ if (!await healthy(fetchImpl, existing)) break;
596
+ if (now() >= deadline || i === 19) throw new Error("shutdown_timeout");
597
+ await sleep(100);
598
+ }
599
+ } catch {
600
+ const refreshed = await readHelperFile(env);
601
+ if (refreshed && await healthy(fetchImpl, refreshed)) return connection(refreshed);
602
+ }
603
+ }
604
+ const paths = helperPaths(env);
605
+ try {
606
+ await ensureStateDir(path.join(paths.root, "channels"));
607
+ let handle;
608
+ let fd;
609
+ let spawnFailed = false;
610
+ try {
611
+ if (deps.openLog) fd = await deps.openLog(paths.logFile);
612
+ else {
613
+ handle = await fs.open(paths.logFile, "a", 384);
614
+ await handle.chmod(384);
615
+ fd = handle.fd;
616
+ }
617
+ const child = spawn(process.execPath, [helperBinPath(), "local", "serve"], {
618
+ detached: true,
619
+ stdio: ["ignore", fd, fd],
620
+ env: helperChildEnv(existing && status ? { ...env, KHALA_LOCAL_PORT: String(existing.port) } : env),
621
+ cwd: paths.root,
622
+ shell: false
623
+ });
624
+ child.on("error", () => {
625
+ spawnFailed = true;
626
+ });
627
+ child.unref();
628
+ } finally {
629
+ if (handle) await handle.close();
630
+ else if (fd !== void 0) {
631
+ const descriptor = fd;
632
+ await new Promise((resolve) => {
633
+ closeFd(descriptor, () => resolve());
634
+ });
635
+ }
636
+ }
637
+ const deadline = now() + 5e3;
638
+ while (!spawnFailed && now() < deadline) {
639
+ const file = await readHelperFile(env);
640
+ if (file && await healthy(fetchImpl, file) && !spawnFailed && now() <= deadline) return connection(file);
641
+ if (spawnFailed || now() >= deadline) break;
642
+ await sleep(100);
643
+ }
644
+ } catch (error) {
645
+ if (error instanceof StateError) throw error;
646
+ }
647
+ throw new KhalaClientError("internal_error", "helper_unavailable");
648
+ }
649
+ function ensureHelper(env = process.env, deps = {}, restartOrigin) {
650
+ const key = helperPaths(env).helperFile;
651
+ const pending = attempts.get(key);
652
+ if (pending) return pending;
653
+ const promise = attempt(env, deps, restartOrigin).finally(() => {
654
+ attempts.delete(key);
655
+ recovering.delete(key);
656
+ });
657
+ attempts.set(key, promise);
658
+ return promise;
659
+ }
660
+ async function restartHelper(origin, env = process.env, deps = {}) {
661
+ const key = helperPaths(env).helperFile;
662
+ const pending = attempts.get(key);
663
+ if (pending) {
664
+ const shareRecovery = !restarted || recovering.get(key) === origin;
665
+ const connection2 = await pending;
666
+ if (shareRecovery && restarted && connection2.origin === origin) return true;
667
+ }
668
+ if (restarted) return false;
669
+ const file = await readHelperFile(env);
670
+ if (!file || file.origin !== origin) return false;
671
+ await ensureHelper(env, deps, origin);
672
+ return restarted;
673
+ }
674
+
484
675
  export {
485
676
  KhalaClientError,
486
677
  isHumanColorId,
@@ -488,11 +679,9 @@ export {
488
679
  normalizeInitials,
489
680
  checkName,
490
681
  checkNewUsername,
491
- MODEL_NAMES,
492
682
  defaultAgentName,
493
683
  isDefaultAgentName,
494
684
  LOCAL_OWNER_USER_ID,
495
- LOCAL_DEFAULT_PORT,
496
685
  LOCAL_LINK_TTL_MS,
497
686
  LOCAL_LONG_POLL_MAX_S,
498
687
  LOCAL_IDLE_EXIT_MS,
@@ -518,6 +707,10 @@ export {
518
707
  decodeLocalErrorBody,
519
708
  decodeOwnerProfile,
520
709
  decodeLocalMembersResponse,
521
- decodeHelperFile,
522
- decodeChannelSecrets
710
+ decodeChannelSecrets,
711
+ helperPaths,
712
+ readHelperFile,
713
+ olderHelperVersion,
714
+ ensureHelper,
715
+ restartHelper
523
716
  };
@@ -2,7 +2,7 @@ import { createRequire as __khalaCreateRequire } from 'node:module'; const requi
2
2
  import {
3
3
  decodeChannelEvent,
4
4
  formatChannelEventLine
5
- } from "./chunk-P5CP32X2.mjs";
5
+ } from "./chunk-54XFVSEC.mjs";
6
6
 
7
7
  // src/events/receive.ts
8
8
  function toEventInboxEntry(base, rawContent) {
@@ -9,7 +9,7 @@ import {
9
9
  utcTimestamp,
10
10
  utf8Length,
11
11
  version
12
- } from "./chunk-SN3STAO2.mjs";
12
+ } from "./chunk-NKX3SLS7.mjs";
13
13
 
14
14
  // ../contracts/src/m1/channel-event.ts
15
15
  var CHANNEL_EVENT_TYPE = "com.khala.event.v1";