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.
- package/README.md +18 -1
- package/dist/{chunk-2U4E7AM7.mjs → chunk-2DHIAFEV.mjs} +294 -101
- package/dist/{chunk-CWHGNVZV.mjs → chunk-2VCAW4GH.mjs} +1 -1
- package/dist/{chunk-P5CP32X2.mjs → chunk-54XFVSEC.mjs} +1 -1
- package/dist/chunk-5BQTYU66.mjs +886 -0
- package/dist/{chunk-RRBIVI77.mjs → chunk-5LIE5GUU.mjs} +1 -1
- package/dist/{chunk-6E3UCO22.mjs → chunk-5PUQT3YS.mjs} +200 -20
- package/dist/{chunk-55KBLEX5.mjs → chunk-7HBRFY3Z.mjs} +1 -1
- package/dist/{chunk-PFDUXY3N.mjs → chunk-APMWDF6B.mjs} +1 -1
- package/dist/{chunk-PY2ZNVNY.mjs → chunk-BKLW2SPG.mjs} +55 -8
- package/dist/{chunk-OW3DSTEZ.mjs → chunk-CBPLCCRZ.mjs} +5 -5
- package/dist/{chunk-JG5J3SHW.mjs → chunk-CJTCU755.mjs} +4 -4
- package/dist/{chunk-AWBKVTLV.mjs → chunk-FH34B3I3.mjs} +14 -17
- package/dist/chunk-FV32AQTC.mjs +40 -0
- package/dist/{chunk-FH2CZX5T.mjs → chunk-H3K4GA2P.mjs} +83 -36
- package/dist/{chunk-AUJEUKVM.mjs → chunk-IQJQ4RF5.mjs} +1 -1
- package/dist/{chunk-SN3STAO2.mjs → chunk-NKX3SLS7.mjs} +40 -3
- package/dist/{chunk-Z4EDYB6Y.mjs → chunk-RNDUFTBF.mjs} +97 -57
- package/dist/{chunk-OECJ45NM.mjs → chunk-VAI7IUI4.mjs} +24 -8
- package/dist/{chunk-XUJIJ4KB.mjs → chunk-VKQ3KUYO.mjs} +16 -2
- package/dist/{chunk-4X35OUAN.mjs → chunk-VUSV2ADL.mjs} +92 -97
- package/dist/{chunk-2XTYRBYX.mjs → chunk-W6R4UJUQ.mjs} +12 -5
- package/dist/{chunk-B2MOLCJM.mjs → chunk-Z4JF2M5C.mjs} +7 -9
- package/dist/{chunk-SZIEG3RS.mjs → chunk-ZJ4W7P33.mjs} +12 -14
- package/dist/khala.mjs +11 -11
- package/dist/web/assets/local-BQl2KrE1.js +9 -0
- package/dist/web/assets/local-OwDq47O4.css +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/chunk-DPW43YNC.mjs +0 -36
- package/dist/chunk-IULQYQJZ.mjs +0 -144
- package/dist/chunk-WHONG7UF.mjs +0 -267
- package/dist/chunk-ZHMGB3PP.mjs +0 -82
- package/dist/web/assets/local-K8EcTVxW.js +0 -9
- 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
|
|
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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-
|
|
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-
|
|
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,
|
|
62
|
-
if (!isHumanColorId(input)) return fail(
|
|
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,
|
|
78
|
-
if (!isCanonicalInitials(input)) return fail(
|
|
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,
|
|
165
|
-
const value = readRoomId(input,
|
|
166
|
-
if (!isLocalRoomId(value)) fail(
|
|
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,
|
|
170
|
-
const value = readMatrixUserId(input,
|
|
171
|
-
if (!isLocalUserId(value)) fail(
|
|
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,
|
|
175
|
-
const value = identifier(input,
|
|
176
|
-
if (!isLocalEventId(value)) fail(
|
|
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,
|
|
180
|
-
const value = identifier(input,
|
|
181
|
-
if (!isLocalTxnId(value)) fail(
|
|
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,
|
|
185
|
-
const value = identifier(input,
|
|
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(
|
|
193
|
+
if (!checked.ok || checked.name !== value) fail(path2, "invalid_value");
|
|
188
194
|
return value;
|
|
189
195
|
}
|
|
190
|
-
function readLocalInitials(input,
|
|
191
|
-
return readHumanInitials(input,
|
|
196
|
+
function readLocalInitials(input, path2) {
|
|
197
|
+
return readHumanInitials(input, path2);
|
|
192
198
|
}
|
|
193
|
-
function record(input,
|
|
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,
|
|
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,
|
|
201
|
-
if (typeof input !== "object" || input === null || Array.isArray(input)) fail(
|
|
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(
|
|
209
|
+
if (prototype !== Object.prototype && prototype !== null) fail(path2, "not_object");
|
|
204
210
|
return input;
|
|
205
211
|
}
|
|
206
|
-
function contentObject(input,
|
|
207
|
-
const value = plainObject(input,
|
|
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(
|
|
218
|
+
fail(path2, "invalid_value");
|
|
213
219
|
}
|
|
214
|
-
if (utf8Length(encoded) > LOCAL_CONTENT_MAX_BYTES) fail(
|
|
220
|
+
if (utf8Length(encoded) > LOCAL_CONTENT_MAX_BYTES) fail(path2, "too_long");
|
|
215
221
|
return value;
|
|
216
222
|
}
|
|
217
|
-
function channelName(input,
|
|
218
|
-
const value = displayName(input,
|
|
219
|
-
if ([...value].length > LOCAL_CHANNEL_NAME_MAX_CHARS) fail(
|
|
220
|
-
if (value !== value.trim()) fail(
|
|
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,
|
|
224
|
-
const value = displayText(input,
|
|
225
|
-
if (value.length === 0) fail(
|
|
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,
|
|
229
|
-
const value = safeInteger(input,
|
|
230
|
-
if (value === 0) fail(
|
|
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,
|
|
234
|
-
const r = record(input,
|
|
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,
|
|
242
|
-
const r = object(input,
|
|
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,
|
|
251
|
+
function readMemberContent(input, path2) {
|
|
246
252
|
const modeKey = "com.khala.listening_mode";
|
|
247
|
-
const r = record(input,
|
|
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,
|
|
275
|
+
function readContent(input, path2, type) {
|
|
270
276
|
switch (type) {
|
|
271
277
|
case "m.room.create":
|
|
272
|
-
return readCreateContent(input,
|
|
278
|
+
return readCreateContent(input, path2);
|
|
273
279
|
case "m.room.name":
|
|
274
|
-
return readNameContent(input,
|
|
280
|
+
return readNameContent(input, path2);
|
|
275
281
|
case "m.room.member":
|
|
276
|
-
return readMemberContent(input,
|
|
282
|
+
return readMemberContent(input, path2);
|
|
277
283
|
default: {
|
|
278
|
-
const content = contentObject(input,
|
|
284
|
+
const content = contentObject(input, path2);
|
|
279
285
|
if (type === "m.room.message") {
|
|
280
|
-
literal(content["msgtype"], `${
|
|
281
|
-
const body = text(content["body"], `${
|
|
282
|
-
if (body.length === 0) fail(`${
|
|
283
|
-
if (body.length > LOCAL_BODY_MAX_CHARS) fail(`${
|
|
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,
|
|
290
|
-
const r = record(input,
|
|
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,
|
|
308
|
-
const values = array(input,
|
|
309
|
-
if (values.length > max) fail(
|
|
310
|
-
return values.map((value, i) => read(value, elementPath(
|
|
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,
|
|
313
|
-
const events = limitedArray(input,
|
|
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(
|
|
316
|
-
if (history && event.type !== "m.room.message" && event.type !== "com.khala.event.v1") fail(`${elementPath(
|
|
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,
|
|
339
|
-
const r = record(input,
|
|
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,
|
|
353
|
-
const r = record(input,
|
|
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,
|
|
363
|
-
const r = record(input,
|
|
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,
|
|
372
|
-
const r = record(input,
|
|
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,
|
|
381
|
-
const r = record(input,
|
|
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,
|
|
394
|
-
return nullable(input, (value) => readLocalInitials(value,
|
|
399
|
+
function nullableInitials(input, path2) {
|
|
400
|
+
return nullable(input, (value) => readLocalInitials(value, path2));
|
|
395
401
|
}
|
|
396
|
-
function readMember(input,
|
|
397
|
-
const r = record(input,
|
|
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,
|
|
454
|
-
const value = identifier(input,
|
|
455
|
-
if (!/^[0-9a-f]{64}$/u.test(value)) fail(
|
|
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
|
|
463
|
-
readSha256(key,
|
|
464
|
-
const link = record(value,
|
|
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
|
|
473
|
-
if (!isLocalAgentUserId(key)) fail(
|
|
474
|
-
const member = record(value,
|
|
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
|
-
|
|
522
|
-
|
|
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-
|
|
5
|
+
} from "./chunk-54XFVSEC.mjs";
|
|
6
6
|
|
|
7
7
|
// src/events/receive.ts
|
|
8
8
|
function toEventInboxEntry(base, rawContent) {
|