khala-cli 0.4.1 → 0.4.4
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-S72K3GSP.mjs → chunk-6OADQWV7.mjs} +31 -20
- package/dist/{chunk-YMDAY234.mjs → chunk-7CVU5OZ7.mjs} +1 -1
- package/dist/{chunk-PSN7P4AV.mjs → chunk-AK74S7EN.mjs} +4 -4
- package/dist/chunk-AUJEUKVM.mjs +121 -0
- package/dist/chunk-DC3OX6F4.mjs +52 -0
- package/dist/chunk-ENPBK5W2.mjs +187 -0
- package/dist/chunk-FA6Z65DW.mjs +19 -0
- package/dist/{chunk-KEGR6VEQ.mjs → chunk-FUU775QM.mjs} +81 -10
- package/dist/chunk-GE3RXO2X.mjs +26 -0
- package/dist/chunk-LHAMBQK7.mjs +144 -0
- package/dist/{chunk-EVLXC2WS.mjs → chunk-LN3G2LJV.mjs} +13 -10
- package/dist/{chunk-35KKPOKI.mjs → chunk-LQ6KNUNN.mjs} +87 -53
- package/dist/{chunk-CSIFRJFF.mjs → chunk-MU4FUENP.mjs} +3 -2
- package/dist/{chunk-L2HRDTA5.mjs → chunk-MZS7XFMC.mjs} +3 -3
- package/dist/{chunk-U5ZMOPUK.mjs → chunk-N5VYKQMR.mjs} +5 -46
- package/dist/{chunk-3SWCRLWX.mjs → chunk-OS2JLDL7.mjs} +172 -227
- package/dist/chunk-P5CP32X2.mjs +170 -0
- package/dist/{chunk-RX5HGJQT.mjs → chunk-PJ3E3IVH.mjs} +184 -126
- package/dist/chunk-SN3STAO2.mjs +190 -0
- package/dist/{chunk-B5BFL7OD.mjs → chunk-XEYKJMZL.mjs} +1 -1
- package/dist/{chunk-SR5BG7FL.mjs → chunk-ZKDDKUUA.mjs} +1 -1
- package/dist/{chunk-HAD5ZILW.mjs → chunk-ZRFXZK72.mjs} +1 -1
- package/dist/khala.mjs +12 -10
- package/dist/web/assets/apple-touch-icon-Du9V6qit.png +0 -0
- package/dist/web/assets/favicon-16x16-D5waNSZD.png +0 -0
- package/dist/web/assets/favicon-32x32-B9Ubw1Dx.png +0 -0
- package/dist/web/assets/favicon-zCD0pKtT.ico +0 -0
- package/dist/web/assets/local-Bk8hMIT-.js +9 -0
- package/dist/web/assets/{local-DHYvcQG0.css → local-vdvavyq0.css} +1 -1
- package/dist/web/index.html +7 -2
- package/package.json +1 -1
- package/dist/chunk-AVJKIJ34.mjs +0 -91
- package/dist/chunk-N6LMHR7P.mjs +0 -455
- package/dist/web/assets/local-DGdQ2afg.js +0 -9
|
@@ -1,18 +1,8 @@
|
|
|
1
1
|
import { createRequire as __khalaCreateRequire } from 'node:module'; const require = __khalaCreateRequire(import.meta.url);
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
readHumanColorId,
|
|
6
|
-
readHumanInitials
|
|
7
|
-
} from "./chunk-AVJKIJ34.mjs";
|
|
8
|
-
import {
|
|
9
|
-
StateError,
|
|
10
|
-
ensureStateDir,
|
|
11
|
-
readJson,
|
|
12
|
-
stateRoot
|
|
13
|
-
} from "./chunk-B5BFL7OD.mjs";
|
|
3
|
+
LISTENING_MODES
|
|
4
|
+
} from "./chunk-AUJEUKVM.mjs";
|
|
14
5
|
import {
|
|
15
|
-
LISTENING_MODES,
|
|
16
6
|
M1_LABEL_MAX_BYTES,
|
|
17
7
|
array,
|
|
18
8
|
decodeWith,
|
|
@@ -27,15 +17,86 @@ import {
|
|
|
27
17
|
readHttpUrl,
|
|
28
18
|
readMatrixUserId,
|
|
29
19
|
readRoomId,
|
|
20
|
+
reserved,
|
|
30
21
|
safeInteger,
|
|
31
22
|
text,
|
|
32
23
|
utcTimestamp,
|
|
33
24
|
utf8Length,
|
|
34
25
|
version
|
|
35
|
-
} from "./chunk-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
26
|
+
} from "./chunk-SN3STAO2.mjs";
|
|
27
|
+
|
|
28
|
+
// src/client.ts
|
|
29
|
+
var KhalaClientError = class extends Error {
|
|
30
|
+
code;
|
|
31
|
+
constructor(code, message) {
|
|
32
|
+
super(message ?? code);
|
|
33
|
+
this.name = "KhalaClientError";
|
|
34
|
+
this.code = code;
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// ../contracts/src/m1/colors.ts
|
|
39
|
+
var HUMAN_COLOR_IDS = ["red", "orange", "amber", "lime", "green", "teal", "blue", "indigo", "purple", "pink"];
|
|
40
|
+
function isHumanColorId(value) {
|
|
41
|
+
return typeof value === "string" && HUMAN_COLOR_IDS.includes(value);
|
|
42
|
+
}
|
|
43
|
+
function fnv1a(value) {
|
|
44
|
+
let hash = 2166136261;
|
|
45
|
+
for (const byte of new TextEncoder().encode(value)) {
|
|
46
|
+
hash ^= byte;
|
|
47
|
+
hash = Math.imul(hash, 16777619) >>> 0;
|
|
48
|
+
}
|
|
49
|
+
return hash >>> 0;
|
|
50
|
+
}
|
|
51
|
+
function defaultHumanColor(ownerId) {
|
|
52
|
+
return HUMAN_COLOR_IDS[fnv1a(ownerId) % HUMAN_COLOR_IDS.length];
|
|
53
|
+
}
|
|
54
|
+
function readHumanColorId(input, path) {
|
|
55
|
+
if (!isHumanColorId(input)) return fail(path, "invalid_value");
|
|
56
|
+
return input;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ../contracts/src/m1/initials.ts
|
|
60
|
+
function normalizeInitials(input) {
|
|
61
|
+
if (typeof input !== "string") return null;
|
|
62
|
+
const nfc = input.normalize("NFC");
|
|
63
|
+
if ([...nfc].length !== 2) return null;
|
|
64
|
+
const up = nfc.toLocaleUpperCase("en-US");
|
|
65
|
+
return /^[\p{L}\p{Nd}]{2}$/u.test(up) ? up : null;
|
|
66
|
+
}
|
|
67
|
+
function isCanonicalInitials(value) {
|
|
68
|
+
return typeof value === "string" && normalizeInitials(value) === value;
|
|
69
|
+
}
|
|
70
|
+
function readHumanInitials(input, path) {
|
|
71
|
+
if (!isCanonicalInitials(input)) return fail(path, "invalid_value");
|
|
72
|
+
return input;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ../contracts/src/m1/names.ts
|
|
76
|
+
var USERNAME_MIN = 2;
|
|
77
|
+
var USERNAME_MAX = 24;
|
|
78
|
+
var AGENT_NAME_MIN = 2;
|
|
79
|
+
var AGENT_NAME_MAX = 40;
|
|
80
|
+
var mentionName = /^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/u;
|
|
81
|
+
var adjacentSeparators = /[._-]{2}/u;
|
|
82
|
+
var agentSuffix = /-(?:claude|codex|cursor)(?:-\d+)?$/iu;
|
|
83
|
+
function checkName(input, kind) {
|
|
84
|
+
if (typeof input !== "string") return { ok: false, error: "invalid_characters" };
|
|
85
|
+
const name = input.trim();
|
|
86
|
+
if (name.length < (kind === "username" ? USERNAME_MIN : AGENT_NAME_MIN)) return { ok: false, error: "too_short" };
|
|
87
|
+
if (name.length > (kind === "username" ? USERNAME_MAX : AGENT_NAME_MAX)) return { ok: false, error: "too_long" };
|
|
88
|
+
if (!mentionName.test(name) || adjacentSeparators.test(name)) return { ok: false, error: "invalid_characters" };
|
|
89
|
+
if (reserved.test(name) || kind === "username" && agentSuffix.test(name)) return { ok: false, error: "reserved" };
|
|
90
|
+
return { ok: true, name };
|
|
91
|
+
}
|
|
92
|
+
var MODEL_NAMES = { claude: "Claude", codex: "Codex", cursor: "Cursor" };
|
|
93
|
+
function defaultAgentName(username, harness, n = 1) {
|
|
94
|
+
return `${username}-${MODEL_NAMES[harness]}${n === 1 ? "" : `-${n}`}`;
|
|
95
|
+
}
|
|
96
|
+
function isDefaultAgentName(name, username, harness) {
|
|
97
|
+
const escaped = `${username}-${MODEL_NAMES[harness]}`.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
98
|
+
return new RegExp(`^${escaped}(?:-\\d+)?$`, "iu").test(name);
|
|
99
|
+
}
|
|
39
100
|
|
|
40
101
|
// ../contracts/src/m1/local.ts
|
|
41
102
|
var LOCAL_OWNER_USER_ID = "@khala_owner:local";
|
|
@@ -84,90 +145,90 @@ var localRoomKey = (roomId) => {
|
|
|
84
145
|
var localRoomPath = (roomId, tail) => `/api/local/rooms/${encodeURIComponent(roomId)}/${tail}`;
|
|
85
146
|
var LOCAL_OWNER_ID = "local-owner";
|
|
86
147
|
var LOCAL_OWNER_DEVICE_ID = "KH_LOCAL_OWNER";
|
|
87
|
-
function readLocalRoomId(input,
|
|
88
|
-
const value = readRoomId(input,
|
|
89
|
-
if (!isLocalRoomId(value)) fail(
|
|
148
|
+
function readLocalRoomId(input, path) {
|
|
149
|
+
const value = readRoomId(input, path);
|
|
150
|
+
if (!isLocalRoomId(value)) fail(path, "invalid_value");
|
|
90
151
|
return value;
|
|
91
152
|
}
|
|
92
|
-
function readLocalUserId(input,
|
|
93
|
-
const value = readMatrixUserId(input,
|
|
94
|
-
if (!isLocalUserId(value)) fail(
|
|
153
|
+
function readLocalUserId(input, path) {
|
|
154
|
+
const value = readMatrixUserId(input, path);
|
|
155
|
+
if (!isLocalUserId(value)) fail(path, "invalid_value");
|
|
95
156
|
return value;
|
|
96
157
|
}
|
|
97
|
-
function readLocalEventId(input,
|
|
98
|
-
const value = identifier(input,
|
|
99
|
-
if (!isLocalEventId(value)) fail(
|
|
158
|
+
function readLocalEventId(input, path) {
|
|
159
|
+
const value = identifier(input, path);
|
|
160
|
+
if (!isLocalEventId(value)) fail(path, "invalid_value");
|
|
100
161
|
return value;
|
|
101
162
|
}
|
|
102
|
-
function readLocalTxnId(input,
|
|
103
|
-
const value = identifier(input,
|
|
104
|
-
if (!isLocalTxnId(value)) fail(
|
|
163
|
+
function readLocalTxnId(input, path) {
|
|
164
|
+
const value = identifier(input, path);
|
|
165
|
+
if (!isLocalTxnId(value)) fail(path, "invalid_value");
|
|
105
166
|
return value;
|
|
106
167
|
}
|
|
107
|
-
function readLocalUsername(input,
|
|
108
|
-
const value = identifier(input,
|
|
168
|
+
function readLocalUsername(input, path) {
|
|
169
|
+
const value = identifier(input, path);
|
|
109
170
|
const checked = checkName(value, "username");
|
|
110
|
-
if (!checked.ok || checked.name !== value) fail(
|
|
171
|
+
if (!checked.ok || checked.name !== value) fail(path, "invalid_value");
|
|
111
172
|
return value;
|
|
112
173
|
}
|
|
113
|
-
function readLocalInitials(input,
|
|
114
|
-
return readHumanInitials(input,
|
|
174
|
+
function readLocalInitials(input, path) {
|
|
175
|
+
return readHumanInitials(input, path);
|
|
115
176
|
}
|
|
116
|
-
function record(input,
|
|
177
|
+
function record(input, path, required, optional = []) {
|
|
117
178
|
const present = optional.filter((key) => typeof input === "object" && input !== null && Object.hasOwn(input, key));
|
|
118
|
-
return object(input,
|
|
179
|
+
return object(input, path, [...required, ...present]);
|
|
119
180
|
}
|
|
120
181
|
function has(input, key) {
|
|
121
182
|
return typeof input === "object" && input !== null && Object.hasOwn(input, key);
|
|
122
183
|
}
|
|
123
|
-
function plainObject(input,
|
|
124
|
-
if (typeof input !== "object" || input === null || Array.isArray(input)) fail(
|
|
184
|
+
function plainObject(input, path) {
|
|
185
|
+
if (typeof input !== "object" || input === null || Array.isArray(input)) fail(path, "not_object");
|
|
125
186
|
const prototype = Object.getPrototypeOf(input);
|
|
126
|
-
if (prototype !== Object.prototype && prototype !== null) fail(
|
|
187
|
+
if (prototype !== Object.prototype && prototype !== null) fail(path, "not_object");
|
|
127
188
|
return input;
|
|
128
189
|
}
|
|
129
|
-
function contentObject(input,
|
|
130
|
-
const value = plainObject(input,
|
|
190
|
+
function contentObject(input, path) {
|
|
191
|
+
const value = plainObject(input, path);
|
|
131
192
|
let encoded;
|
|
132
193
|
try {
|
|
133
194
|
encoded = JSON.stringify(value);
|
|
134
195
|
} catch {
|
|
135
|
-
fail(
|
|
196
|
+
fail(path, "invalid_value");
|
|
136
197
|
}
|
|
137
|
-
if (utf8Length(encoded) > LOCAL_CONTENT_MAX_BYTES) fail(
|
|
198
|
+
if (utf8Length(encoded) > LOCAL_CONTENT_MAX_BYTES) fail(path, "too_long");
|
|
138
199
|
return value;
|
|
139
200
|
}
|
|
140
|
-
function channelName(input,
|
|
141
|
-
const value = displayName(input,
|
|
142
|
-
if ([...value].length > LOCAL_CHANNEL_NAME_MAX_CHARS) fail(
|
|
143
|
-
if (value !== value.trim()) fail(
|
|
201
|
+
function channelName(input, path) {
|
|
202
|
+
const value = displayName(input, path);
|
|
203
|
+
if ([...value].length > LOCAL_CHANNEL_NAME_MAX_CHARS) fail(path, "too_long");
|
|
204
|
+
if (value !== value.trim()) fail(path, "invalid_value");
|
|
144
205
|
return value;
|
|
145
206
|
}
|
|
146
|
-
function displayName(input,
|
|
147
|
-
const value = displayText(input,
|
|
148
|
-
if (value.length === 0) fail(
|
|
207
|
+
function displayName(input, path) {
|
|
208
|
+
const value = displayText(input, path, M1_LABEL_MAX_BYTES);
|
|
209
|
+
if (value.length === 0) fail(path, "empty");
|
|
149
210
|
return value;
|
|
150
211
|
}
|
|
151
|
-
function seq(input,
|
|
152
|
-
const value = safeInteger(input,
|
|
153
|
-
if (value === 0) fail(
|
|
212
|
+
function seq(input, path) {
|
|
213
|
+
const value = safeInteger(input, path);
|
|
214
|
+
if (value === 0) fail(path, "invalid_value");
|
|
154
215
|
return value;
|
|
155
216
|
}
|
|
156
|
-
function readCreateContent(input,
|
|
157
|
-
const r = record(input,
|
|
217
|
+
function readCreateContent(input, path) {
|
|
218
|
+
const r = record(input, path, ["name", "createdBy"], ["operationId"]);
|
|
158
219
|
return {
|
|
159
220
|
name: channelName(r.field("name"), r.at("name")),
|
|
160
221
|
createdBy: readLocalUserId(r.field("createdBy"), r.at("createdBy")),
|
|
161
222
|
...has(input, "operationId") ? { operationId: readLocalTxnId(r.field("operationId"), r.at("operationId")) } : {}
|
|
162
223
|
};
|
|
163
224
|
}
|
|
164
|
-
function readNameContent(input,
|
|
165
|
-
const r = object(input,
|
|
225
|
+
function readNameContent(input, path) {
|
|
226
|
+
const r = object(input, path, ["name"]);
|
|
166
227
|
return { name: channelName(r.field("name"), r.at("name")) };
|
|
167
228
|
}
|
|
168
|
-
function readMemberContent(input,
|
|
229
|
+
function readMemberContent(input, path) {
|
|
169
230
|
const modeKey = "com.khala.listening_mode";
|
|
170
|
-
const r = record(input,
|
|
231
|
+
const r = record(input, path, ["user", "membership", "displayname", "kind"], ["harness", "invitedBy", modeKey]);
|
|
171
232
|
const kind = literal(r.field("kind"), r.at("kind"), ["human", "agent"]);
|
|
172
233
|
if (kind === "human") {
|
|
173
234
|
for (const key of ["harness", modeKey]) if (has(input, key)) fail(r.at(key), "invalid_value");
|
|
@@ -189,28 +250,28 @@ function decodeLocalMemberContent(input) {
|
|
|
189
250
|
return decodeWith(() => readMemberContent(input, ""));
|
|
190
251
|
}
|
|
191
252
|
var EVENT_TYPES = ["m.room.create", "m.room.name", "m.room.member", "m.room.message", "com.khala.event.v1", "com.khala.listening_mode.v1"];
|
|
192
|
-
function readContent(input,
|
|
253
|
+
function readContent(input, path, type) {
|
|
193
254
|
switch (type) {
|
|
194
255
|
case "m.room.create":
|
|
195
|
-
return readCreateContent(input,
|
|
256
|
+
return readCreateContent(input, path);
|
|
196
257
|
case "m.room.name":
|
|
197
|
-
return readNameContent(input,
|
|
258
|
+
return readNameContent(input, path);
|
|
198
259
|
case "m.room.member":
|
|
199
|
-
return readMemberContent(input,
|
|
260
|
+
return readMemberContent(input, path);
|
|
200
261
|
default: {
|
|
201
|
-
const content = contentObject(input,
|
|
262
|
+
const content = contentObject(input, path);
|
|
202
263
|
if (type === "m.room.message") {
|
|
203
|
-
literal(content["msgtype"], `${
|
|
204
|
-
const body = text(content["body"], `${
|
|
205
|
-
if (body.length === 0) fail(`${
|
|
206
|
-
if (body.length > LOCAL_BODY_MAX_CHARS) fail(`${
|
|
264
|
+
literal(content["msgtype"], `${path}.msgtype`, ["m.text", "m.notice"]);
|
|
265
|
+
const body = text(content["body"], `${path}.body`, LOCAL_CONTENT_MAX_BYTES);
|
|
266
|
+
if (body.length === 0) fail(`${path}.body`, "empty");
|
|
267
|
+
if (body.length > LOCAL_BODY_MAX_CHARS) fail(`${path}.body`, "too_long");
|
|
207
268
|
}
|
|
208
269
|
return content;
|
|
209
270
|
}
|
|
210
271
|
}
|
|
211
272
|
}
|
|
212
|
-
function readEvent(input,
|
|
213
|
-
const r = record(input,
|
|
273
|
+
function readEvent(input, path) {
|
|
274
|
+
const r = record(input, path, ["seq", "eventId", "roomId", "type", "sender", "ts", "content"], ["txnId", "previousContent"]);
|
|
214
275
|
const type = literal(r.field("type"), r.at("type"), EVENT_TYPES);
|
|
215
276
|
return {
|
|
216
277
|
seq: seq(r.field("seq"), r.at("seq")),
|
|
@@ -227,16 +288,16 @@ function readEvent(input, path2) {
|
|
|
227
288
|
function decodeLocalEvent(input) {
|
|
228
289
|
return decodeWith(() => readEvent(input, ""));
|
|
229
290
|
}
|
|
230
|
-
function limitedArray(input,
|
|
231
|
-
const values = array(input,
|
|
232
|
-
if (values.length > max) fail(
|
|
233
|
-
return values.map((value, i) => read(value, elementPath(
|
|
291
|
+
function limitedArray(input, path, max, read) {
|
|
292
|
+
const values = array(input, path);
|
|
293
|
+
if (values.length > max) fail(path, "too_long");
|
|
294
|
+
return values.map((value, i) => read(value, elementPath(path, i)));
|
|
234
295
|
}
|
|
235
|
-
function readEvents(input,
|
|
236
|
-
const events = limitedArray(input,
|
|
296
|
+
function readEvents(input, path, history) {
|
|
297
|
+
const events = limitedArray(input, path, history ? LOCAL_HISTORY_LIMIT_MAX : LOCAL_EVENTS_PAGE_MAX, readEvent);
|
|
237
298
|
events.forEach((event, i) => {
|
|
238
|
-
if (i > 0 && event.seq <= events[i - 1].seq) fail(`${elementPath(
|
|
239
|
-
if (history && event.type !== "m.room.message" && event.type !== "com.khala.event.v1") fail(`${elementPath(
|
|
299
|
+
if (i > 0 && event.seq <= events[i - 1].seq) fail(`${elementPath(path, i)}.seq`, "invalid_value");
|
|
300
|
+
if (history && event.type !== "m.room.message" && event.type !== "com.khala.event.v1") fail(`${elementPath(path, i)}.type`, "invalid_value");
|
|
240
301
|
});
|
|
241
302
|
return events;
|
|
242
303
|
}
|
|
@@ -258,8 +319,8 @@ function decodeLocalHistoryPage(input) {
|
|
|
258
319
|
};
|
|
259
320
|
});
|
|
260
321
|
}
|
|
261
|
-
function readLocalMe(input,
|
|
262
|
-
const r = record(input,
|
|
322
|
+
function readLocalMe(input, path) {
|
|
323
|
+
const r = record(input, path, ["userId", "roomId", "roomName", "membership", "displayName"], ["invitedBy"]);
|
|
263
324
|
return {
|
|
264
325
|
userId: readLocalUserId(r.field("userId"), r.at("userId")),
|
|
265
326
|
roomId: readLocalRoomId(r.field("roomId"), r.at("roomId")),
|
|
@@ -272,8 +333,8 @@ function readLocalMe(input, path2) {
|
|
|
272
333
|
function decodeLocalMe(input) {
|
|
273
334
|
return decodeWith(() => readLocalMe(input, ""));
|
|
274
335
|
}
|
|
275
|
-
function readLocalJoined(input,
|
|
276
|
-
const r = record(input,
|
|
336
|
+
function readLocalJoined(input, path) {
|
|
337
|
+
const r = record(input, path, ["seq", "ts"], []);
|
|
277
338
|
return {
|
|
278
339
|
seq: seq(r.field("seq"), r.at("seq")),
|
|
279
340
|
ts: safeInteger(r.field("ts"), r.at("ts"))
|
|
@@ -282,8 +343,8 @@ function readLocalJoined(input, path2) {
|
|
|
282
343
|
function decodeLocalJoined(input) {
|
|
283
344
|
return decodeWith(() => readLocalJoined(input, ""));
|
|
284
345
|
}
|
|
285
|
-
function readLocalSendResult(input,
|
|
286
|
-
const r = record(input,
|
|
346
|
+
function readLocalSendResult(input, path) {
|
|
347
|
+
const r = record(input, path, ["eventId"], []);
|
|
287
348
|
return {
|
|
288
349
|
eventId: readLocalEventId(r.field("eventId"), r.at("eventId"))
|
|
289
350
|
};
|
|
@@ -291,8 +352,8 @@ function readLocalSendResult(input, path2) {
|
|
|
291
352
|
function decodeLocalSendResult(input) {
|
|
292
353
|
return decodeWith(() => readLocalSendResult(input, ""));
|
|
293
354
|
}
|
|
294
|
-
function readLocalErrorBody(input,
|
|
295
|
-
const r = record(input,
|
|
355
|
+
function readLocalErrorBody(input, path) {
|
|
356
|
+
const r = record(input, path, ["error"], []);
|
|
296
357
|
return {
|
|
297
358
|
error: identifier(r.field("error"), r.at("error"))
|
|
298
359
|
};
|
|
@@ -300,8 +361,8 @@ function readLocalErrorBody(input, path2) {
|
|
|
300
361
|
function decodeLocalErrorBody(input) {
|
|
301
362
|
return decodeWith(() => readLocalErrorBody(input, ""));
|
|
302
363
|
}
|
|
303
|
-
function readOwnerProfile(input,
|
|
304
|
-
const r = record(input,
|
|
364
|
+
function readOwnerProfile(input, path) {
|
|
365
|
+
const r = record(input, path, ["v", "username", "color", "initials", "updatedAt"]);
|
|
305
366
|
return {
|
|
306
367
|
v: version(r.field("v"), r.at("v")),
|
|
307
368
|
username: readLocalUsername(r.field("username"), r.at("username")),
|
|
@@ -313,11 +374,11 @@ function readOwnerProfile(input, path2) {
|
|
|
313
374
|
function decodeOwnerProfile(input) {
|
|
314
375
|
return decodeWith(() => readOwnerProfile(input, ""));
|
|
315
376
|
}
|
|
316
|
-
function nullableInitials(input,
|
|
317
|
-
return nullable(input, (value) => readLocalInitials(value,
|
|
377
|
+
function nullableInitials(input, path) {
|
|
378
|
+
return nullable(input, (value) => readLocalInitials(value, path));
|
|
318
379
|
}
|
|
319
|
-
function readMember(input,
|
|
320
|
-
const r = record(input,
|
|
380
|
+
function readMember(input, path) {
|
|
381
|
+
const r = record(input, path, ["userId", "participantId", "ownerId", "deviceId", "displayName", "kind", "membership"], ["harness", "ownerLabel", "listeningMode"]);
|
|
321
382
|
const userId = readLocalUserId(r.field("userId"), r.at("userId"));
|
|
322
383
|
const participantId = readLocalUserId(r.field("participantId"), r.at("participantId"));
|
|
323
384
|
if (participantId !== userId) fail(r.at("participantId"), "invalid_value");
|
|
@@ -373,18 +434,18 @@ function decodeHelperFile(input) {
|
|
|
373
434
|
};
|
|
374
435
|
});
|
|
375
436
|
}
|
|
376
|
-
function readSha256(input,
|
|
377
|
-
const value = identifier(input,
|
|
378
|
-
if (!/^[0-9a-f]{64}$/u.test(value)) fail(
|
|
437
|
+
function readSha256(input, path) {
|
|
438
|
+
const value = identifier(input, path);
|
|
439
|
+
if (!/^[0-9a-f]{64}$/u.test(value)) fail(path, "invalid_value");
|
|
379
440
|
return value;
|
|
380
441
|
}
|
|
381
442
|
function decodeChannelSecrets(input) {
|
|
382
443
|
return decodeWith(() => {
|
|
383
444
|
const r = object(input, "", ["v", "links", "members"]);
|
|
384
445
|
const links = Object.fromEntries(Object.entries(plainObject(r.field("links"), r.at("links"))).map(([key, value]) => {
|
|
385
|
-
const
|
|
386
|
-
readSha256(key,
|
|
387
|
-
const link = record(value,
|
|
446
|
+
const path = `${r.at("links")}.${key}`;
|
|
447
|
+
readSha256(key, path);
|
|
448
|
+
const link = record(value, path, ["expiresAt", "kind"], ["consumedAt"]);
|
|
388
449
|
return [key, {
|
|
389
450
|
expiresAt: utcTimestamp(link.field("expiresAt"), link.at("expiresAt")),
|
|
390
451
|
kind: literal(link.field("kind"), link.at("kind"), ["join"]),
|
|
@@ -392,9 +453,9 @@ function decodeChannelSecrets(input) {
|
|
|
392
453
|
}];
|
|
393
454
|
}));
|
|
394
455
|
const members = Object.fromEntries(Object.entries(plainObject(r.field("members"), r.at("members"))).map(([key, value]) => {
|
|
395
|
-
const
|
|
396
|
-
if (!isLocalAgentUserId(key)) fail(
|
|
397
|
-
const member = record(value,
|
|
456
|
+
const path = `${r.at("members")}.${key}`;
|
|
457
|
+
if (!isLocalAgentUserId(key)) fail(path, "invalid_value");
|
|
458
|
+
const member = record(value, path, ["tokenSha256"], ["sessionKey"]);
|
|
398
459
|
return [key, {
|
|
399
460
|
tokenSha256: readSha256(member.field("tokenSha256"), member.at("tokenSha256")),
|
|
400
461
|
...has(value, "sessionKey") ? { sessionKey: readSha256(member.field("sessionKey"), member.at("sessionKey")) } : {}
|
|
@@ -404,131 +465,17 @@ function decodeChannelSecrets(input) {
|
|
|
404
465
|
});
|
|
405
466
|
}
|
|
406
467
|
|
|
407
|
-
// src/local/lifecycle.ts
|
|
408
|
-
import * as fs from "node:fs/promises";
|
|
409
|
-
import { close as closeFd } from "node:fs";
|
|
410
|
-
import path from "node:path";
|
|
411
|
-
import { fileURLToPath } from "node:url";
|
|
412
|
-
import { setTimeout as sleepDefault } from "node:timers/promises";
|
|
413
|
-
import { spawn as nodeSpawn } from "node:child_process";
|
|
414
|
-
function helperPaths(env = process.env) {
|
|
415
|
-
const root = path.join(stateRoot(env), "local");
|
|
416
|
-
const candidate = /^\d{1,5}$/u.test(env.KHALA_LOCAL_PORT ?? "") ? Number(env.KHALA_LOCAL_PORT) : 0;
|
|
417
|
-
const port = candidate >= 1 && candidate <= 65535 ? candidate : LOCAL_DEFAULT_PORT;
|
|
418
|
-
return { root, helperFile: path.join(root, "helper.json"), logFile: path.join(root, "helper.log"), port, origin: `http://127.0.0.1:${port}` };
|
|
419
|
-
}
|
|
420
|
-
async function readHelperFile(env = process.env) {
|
|
421
|
-
const decoded = decodeHelperFile(await readJson(helperPaths(env).helperFile));
|
|
422
|
-
if (!decoded.ok) return null;
|
|
423
|
-
const file = decoded.value;
|
|
424
|
-
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;
|
|
425
|
-
return file;
|
|
426
|
-
}
|
|
427
|
-
function helperBinPath() {
|
|
428
|
-
return fileURLToPath(bundle ? new URL("./khala.mjs", import.meta.url) : new URL("../../bin/khala.mjs", import.meta.url));
|
|
429
|
-
}
|
|
430
|
-
var HELPER_ENV_ALLOWLIST = [
|
|
431
|
-
"PATH",
|
|
432
|
-
"HOME",
|
|
433
|
-
"USER",
|
|
434
|
-
"LOGNAME",
|
|
435
|
-
"LANG",
|
|
436
|
-
"LC_ALL",
|
|
437
|
-
"TMPDIR",
|
|
438
|
-
"XDG_STATE_HOME",
|
|
439
|
-
"NODE_OPTIONS",
|
|
440
|
-
"KHALA_EGRESS_LOG",
|
|
441
|
-
"KHALA_LOCAL_PORT",
|
|
442
|
-
"KHALA_LOCAL_IDLE_MS",
|
|
443
|
-
"KHALA_LOCAL_WEB_DIR"
|
|
444
|
-
];
|
|
445
|
-
function helperChildEnv(env) {
|
|
446
|
-
const childEnv = {};
|
|
447
|
-
for (const key of HELPER_ENV_ALLOWLIST) if (env[key] !== void 0) childEnv[key] = env[key];
|
|
448
|
-
return childEnv;
|
|
449
|
-
}
|
|
450
|
-
var attempts = /* @__PURE__ */ new Map();
|
|
451
|
-
async function healthy(fetchImpl, file) {
|
|
452
|
-
try {
|
|
453
|
-
const response = await fetchImpl(`${file.origin}/healthz`, { signal: AbortSignal.timeout(500) });
|
|
454
|
-
if (response.status !== 200) {
|
|
455
|
-
await response.body?.cancel();
|
|
456
|
-
return false;
|
|
457
|
-
}
|
|
458
|
-
const body = await response.json();
|
|
459
|
-
return typeof body === "object" && body !== null && "ok" in body && body.ok === true && "pid" in body && body.pid === file.pid;
|
|
460
|
-
} catch {
|
|
461
|
-
return false;
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
function connection(file) {
|
|
465
|
-
return { origin: file.origin, adminToken: file.adminToken };
|
|
466
|
-
}
|
|
467
|
-
async function attempt(env, deps) {
|
|
468
|
-
const fetchImpl = deps.fetch ?? globalThis.fetch;
|
|
469
|
-
const spawn = deps.spawn ?? ((command, argv, options) => nodeSpawn(command, [...argv], options));
|
|
470
|
-
const sleep = deps.sleep ?? sleepDefault;
|
|
471
|
-
const now = deps.now ?? Date.now;
|
|
472
|
-
const existing = await readHelperFile(env);
|
|
473
|
-
if (existing && await healthy(fetchImpl, existing)) return connection(existing);
|
|
474
|
-
const paths = helperPaths(env);
|
|
475
|
-
try {
|
|
476
|
-
await ensureStateDir(path.join(paths.root, "channels"));
|
|
477
|
-
let handle;
|
|
478
|
-
let fd;
|
|
479
|
-
let spawnFailed = false;
|
|
480
|
-
try {
|
|
481
|
-
if (deps.openLog) fd = await deps.openLog(paths.logFile);
|
|
482
|
-
else {
|
|
483
|
-
handle = await fs.open(paths.logFile, "a", 384);
|
|
484
|
-
await handle.chmod(384);
|
|
485
|
-
fd = handle.fd;
|
|
486
|
-
}
|
|
487
|
-
const child = spawn(process.execPath, [helperBinPath(), "local", "serve"], {
|
|
488
|
-
detached: true,
|
|
489
|
-
stdio: ["ignore", fd, fd],
|
|
490
|
-
env: helperChildEnv(env),
|
|
491
|
-
cwd: paths.root,
|
|
492
|
-
shell: false
|
|
493
|
-
});
|
|
494
|
-
child.on("error", () => {
|
|
495
|
-
spawnFailed = true;
|
|
496
|
-
});
|
|
497
|
-
child.unref();
|
|
498
|
-
} finally {
|
|
499
|
-
if (handle) await handle.close();
|
|
500
|
-
else if (fd !== void 0) {
|
|
501
|
-
const descriptor = fd;
|
|
502
|
-
await new Promise((resolve) => {
|
|
503
|
-
closeFd(descriptor, () => resolve());
|
|
504
|
-
});
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
const deadline = now() + 5e3;
|
|
508
|
-
while (!spawnFailed && now() < deadline) {
|
|
509
|
-
const file = await readHelperFile(env);
|
|
510
|
-
if (file && await healthy(fetchImpl, file) && !spawnFailed && now() <= deadline) return connection(file);
|
|
511
|
-
if (spawnFailed || now() >= deadline) break;
|
|
512
|
-
await sleep(100);
|
|
513
|
-
}
|
|
514
|
-
} catch (error) {
|
|
515
|
-
if (error instanceof StateError) throw error;
|
|
516
|
-
}
|
|
517
|
-
throw new KhalaClientError("internal_error", "helper_unavailable");
|
|
518
|
-
}
|
|
519
|
-
function ensureHelper(env = process.env, deps = {}) {
|
|
520
|
-
const key = helperPaths(env).helperFile;
|
|
521
|
-
const pending = attempts.get(key);
|
|
522
|
-
if (pending) return pending;
|
|
523
|
-
const promise = attempt(env, deps).finally(() => {
|
|
524
|
-
attempts.delete(key);
|
|
525
|
-
});
|
|
526
|
-
attempts.set(key, promise);
|
|
527
|
-
return promise;
|
|
528
|
-
}
|
|
529
|
-
|
|
530
468
|
export {
|
|
469
|
+
KhalaClientError,
|
|
470
|
+
isHumanColorId,
|
|
471
|
+
defaultHumanColor,
|
|
472
|
+
normalizeInitials,
|
|
473
|
+
checkName,
|
|
474
|
+
MODEL_NAMES,
|
|
475
|
+
defaultAgentName,
|
|
476
|
+
isDefaultAgentName,
|
|
531
477
|
LOCAL_OWNER_USER_ID,
|
|
478
|
+
LOCAL_DEFAULT_PORT,
|
|
532
479
|
LOCAL_LINK_TTL_MS,
|
|
533
480
|
LOCAL_LONG_POLL_MAX_S,
|
|
534
481
|
LOCAL_IDLE_EXIT_MS,
|
|
@@ -554,8 +501,6 @@ export {
|
|
|
554
501
|
decodeLocalErrorBody,
|
|
555
502
|
decodeOwnerProfile,
|
|
556
503
|
decodeLocalMembersResponse,
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
readHelperFile,
|
|
560
|
-
ensureHelper
|
|
504
|
+
decodeHelperFile,
|
|
505
|
+
decodeChannelSecrets
|
|
561
506
|
};
|