khala-cli 0.3.0
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/LICENSE +201 -0
- package/README.md +33 -0
- package/dist/chunk-4C2TBZVA.mjs +245 -0
- package/dist/chunk-525XNG6V.mjs +110 -0
- package/dist/chunk-6FNXMYOM.mjs +8 -0
- package/dist/chunk-6GOETMWV.mjs +91 -0
- package/dist/chunk-CTXFYQIX.mjs +352 -0
- package/dist/chunk-FGIU3YO2.mjs +214 -0
- package/dist/chunk-GO5DD2TR.mjs +442 -0
- package/dist/chunk-H2JCKPFR.mjs +149 -0
- package/dist/chunk-HMGN52QY.mjs +11 -0
- package/dist/chunk-HWKLLI7F.mjs +1652 -0
- package/dist/chunk-MGXWUWWP.mjs +1294 -0
- package/dist/chunk-PQO6675G.mjs +135 -0
- package/dist/chunk-S6XASSNU.mjs +120 -0
- package/dist/chunk-UYTGV6TW.mjs +280 -0
- package/dist/chunk-Y3TDVOGT.mjs +11 -0
- package/dist/chunk-ZRUF3P53.mjs +557 -0
- package/dist/khala.mjs +89 -0
- package/dist/web/assets/Bungee-Regular-BC4W3tCU.woff2 +0 -0
- package/dist/web/assets/JetBrainsMono-Variable-CZ7Klmjc.woff2 +0 -0
- package/dist/web/assets/SpaceGrotesk-Variable-BWuzNAiS.woff2 +0 -0
- package/dist/web/assets/aiur-logo-Dx5KVuwr.png +0 -0
- package/dist/web/assets/local-DuGvSGji.css +1 -0
- package/dist/web/assets/local-hWRGqoWt.js +9 -0
- package/dist/web/index.html +14 -0
- package/package.json +35 -0
|
@@ -0,0 +1,557 @@
|
|
|
1
|
+
import { createRequire as __khalaCreateRequire } from 'node:module'; const require = __khalaCreateRequire(import.meta.url);
|
|
2
|
+
import {
|
|
3
|
+
KhalaClientError,
|
|
4
|
+
checkName,
|
|
5
|
+
readHumanColorId,
|
|
6
|
+
readHumanInitials
|
|
7
|
+
} from "./chunk-6GOETMWV.mjs";
|
|
8
|
+
import {
|
|
9
|
+
StateError,
|
|
10
|
+
ensureStateDir,
|
|
11
|
+
readJson,
|
|
12
|
+
stateRoot
|
|
13
|
+
} from "./chunk-S6XASSNU.mjs";
|
|
14
|
+
import {
|
|
15
|
+
LISTENING_MODES,
|
|
16
|
+
M1_LABEL_MAX_BYTES,
|
|
17
|
+
array,
|
|
18
|
+
decodeWith,
|
|
19
|
+
displayText,
|
|
20
|
+
elementPath,
|
|
21
|
+
fail,
|
|
22
|
+
identifier,
|
|
23
|
+
literal,
|
|
24
|
+
nullable,
|
|
25
|
+
object,
|
|
26
|
+
readHarness,
|
|
27
|
+
readHttpUrl,
|
|
28
|
+
readMatrixUserId,
|
|
29
|
+
readRoomId,
|
|
30
|
+
safeInteger,
|
|
31
|
+
text,
|
|
32
|
+
utcTimestamp,
|
|
33
|
+
utf8Length,
|
|
34
|
+
version
|
|
35
|
+
} from "./chunk-GO5DD2TR.mjs";
|
|
36
|
+
import {
|
|
37
|
+
bundle
|
|
38
|
+
} from "./chunk-HMGN52QY.mjs";
|
|
39
|
+
|
|
40
|
+
// ../contracts/src/m1/local.ts
|
|
41
|
+
var LOCAL_OWNER_USER_ID = "@khala_owner:local";
|
|
42
|
+
var LOCAL_DEFAULT_PORT = 47830;
|
|
43
|
+
var LOCAL_LINK_TTL_MS = 6e5;
|
|
44
|
+
var LOCAL_LONG_POLL_MAX_S = 25;
|
|
45
|
+
var LOCAL_IDLE_EXIT_MS = 6e5;
|
|
46
|
+
var LOCAL_TOKEN_BYTES = 32;
|
|
47
|
+
var LOCAL_CHANNEL_NAME_MAX_CHARS = 64;
|
|
48
|
+
var LOCAL_BODY_MAX_CHARS = 8e3;
|
|
49
|
+
var LOCAL_CONTENT_MAX_BYTES = 32768;
|
|
50
|
+
var LOCAL_EVENTS_PAGE_MAX = 200;
|
|
51
|
+
var LOCAL_HISTORY_LIMIT_MAX = 100;
|
|
52
|
+
var LOCAL_MEMBERS_MAX = 100;
|
|
53
|
+
var LOCAL_AGENT_DEVICE_PREFIX = "KH_LOCAL_";
|
|
54
|
+
function base64url(bytes) {
|
|
55
|
+
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
|
56
|
+
let result = "";
|
|
57
|
+
for (let i = 0; i < bytes.length; i += 3) {
|
|
58
|
+
const n = bytes[i] << 16 | (bytes[i + 1] ?? 0) << 8 | (bytes[i + 2] ?? 0);
|
|
59
|
+
result += alphabet[n >> 18 & 63] + alphabet[n >> 12 & 63];
|
|
60
|
+
if (i + 1 < bytes.length) result += alphabet[n >> 6 & 63];
|
|
61
|
+
if (i + 2 < bytes.length) result += alphabet[n & 63];
|
|
62
|
+
}
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
function hex(bytes) {
|
|
66
|
+
return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
67
|
+
}
|
|
68
|
+
function exact(bytes, n) {
|
|
69
|
+
if (bytes.length !== n) throw new RangeError("random_length");
|
|
70
|
+
return bytes;
|
|
71
|
+
}
|
|
72
|
+
var newLocalRoomId = (random16) => `!${base64url(exact(random16, 16))}:local`;
|
|
73
|
+
var newLocalAgentUserId = (random4) => `@agent-${hex(exact(random4, 4))}:local`;
|
|
74
|
+
var newLocalEventId = (random16) => `$${base64url(exact(random16, 16))}`;
|
|
75
|
+
var isLocalRoomId = (value) => /^![A-Za-z0-9_-]{22}:local$/u.test(value);
|
|
76
|
+
var isLocalAgentUserId = (value) => /^@agent-[0-9a-f]{8}:local$/u.test(value);
|
|
77
|
+
var isLocalUserId = (value) => value === LOCAL_OWNER_USER_ID || isLocalAgentUserId(value);
|
|
78
|
+
var isLocalEventId = (value) => /^\$[A-Za-z0-9_-]{22}$/u.test(value);
|
|
79
|
+
var isLocalTxnId = (value) => /^[A-Za-z0-9._-]{1,64}$/u.test(value);
|
|
80
|
+
var localRoomKey = (roomId) => {
|
|
81
|
+
if (!isLocalRoomId(roomId)) throw new RangeError("not_local_room");
|
|
82
|
+
return roomId.slice(1, -":local".length);
|
|
83
|
+
};
|
|
84
|
+
var localRoomPath = (roomId, tail) => `/api/local/rooms/${encodeURIComponent(roomId)}/${tail}`;
|
|
85
|
+
var LOCAL_OWNER_ID = "local-owner";
|
|
86
|
+
var LOCAL_OWNER_DEVICE_ID = "KH_LOCAL_OWNER";
|
|
87
|
+
function readLocalRoomId(input, path2) {
|
|
88
|
+
const value = readRoomId(input, path2);
|
|
89
|
+
if (!isLocalRoomId(value)) fail(path2, "invalid_value");
|
|
90
|
+
return value;
|
|
91
|
+
}
|
|
92
|
+
function readLocalUserId(input, path2) {
|
|
93
|
+
const value = readMatrixUserId(input, path2);
|
|
94
|
+
if (!isLocalUserId(value)) fail(path2, "invalid_value");
|
|
95
|
+
return value;
|
|
96
|
+
}
|
|
97
|
+
function readLocalEventId(input, path2) {
|
|
98
|
+
const value = identifier(input, path2);
|
|
99
|
+
if (!isLocalEventId(value)) fail(path2, "invalid_value");
|
|
100
|
+
return value;
|
|
101
|
+
}
|
|
102
|
+
function readLocalTxnId(input, path2) {
|
|
103
|
+
const value = identifier(input, path2);
|
|
104
|
+
if (!isLocalTxnId(value)) fail(path2, "invalid_value");
|
|
105
|
+
return value;
|
|
106
|
+
}
|
|
107
|
+
function readLocalUsername(input, path2) {
|
|
108
|
+
const value = identifier(input, path2);
|
|
109
|
+
const checked = checkName(value, "username");
|
|
110
|
+
if (!checked.ok || checked.name !== value) fail(path2, "invalid_value");
|
|
111
|
+
return value;
|
|
112
|
+
}
|
|
113
|
+
function readLocalInitials(input, path2) {
|
|
114
|
+
return readHumanInitials(input, path2);
|
|
115
|
+
}
|
|
116
|
+
function record(input, path2, required, optional = []) {
|
|
117
|
+
const present = optional.filter((key) => typeof input === "object" && input !== null && Object.hasOwn(input, key));
|
|
118
|
+
return object(input, path2, [...required, ...present]);
|
|
119
|
+
}
|
|
120
|
+
function has(input, key) {
|
|
121
|
+
return typeof input === "object" && input !== null && Object.hasOwn(input, key);
|
|
122
|
+
}
|
|
123
|
+
function plainObject(input, path2) {
|
|
124
|
+
if (typeof input !== "object" || input === null || Array.isArray(input)) fail(path2, "not_object");
|
|
125
|
+
const prototype = Object.getPrototypeOf(input);
|
|
126
|
+
if (prototype !== Object.prototype && prototype !== null) fail(path2, "not_object");
|
|
127
|
+
return input;
|
|
128
|
+
}
|
|
129
|
+
function contentObject(input, path2) {
|
|
130
|
+
const value = plainObject(input, path2);
|
|
131
|
+
let encoded;
|
|
132
|
+
try {
|
|
133
|
+
encoded = JSON.stringify(value);
|
|
134
|
+
} catch {
|
|
135
|
+
fail(path2, "invalid_value");
|
|
136
|
+
}
|
|
137
|
+
if (utf8Length(encoded) > LOCAL_CONTENT_MAX_BYTES) fail(path2, "too_long");
|
|
138
|
+
return value;
|
|
139
|
+
}
|
|
140
|
+
function channelName(input, path2) {
|
|
141
|
+
const value = displayName(input, path2);
|
|
142
|
+
if ([...value].length > LOCAL_CHANNEL_NAME_MAX_CHARS) fail(path2, "too_long");
|
|
143
|
+
if (value !== value.trim()) fail(path2, "invalid_value");
|
|
144
|
+
return value;
|
|
145
|
+
}
|
|
146
|
+
function displayName(input, path2) {
|
|
147
|
+
const value = displayText(input, path2, M1_LABEL_MAX_BYTES);
|
|
148
|
+
if (value.length === 0) fail(path2, "empty");
|
|
149
|
+
return value;
|
|
150
|
+
}
|
|
151
|
+
function seq(input, path2) {
|
|
152
|
+
const value = safeInteger(input, path2);
|
|
153
|
+
if (value === 0) fail(path2, "invalid_value");
|
|
154
|
+
return value;
|
|
155
|
+
}
|
|
156
|
+
function readCreateContent(input, path2) {
|
|
157
|
+
const r = record(input, path2, ["name", "createdBy"], ["operationId"]);
|
|
158
|
+
return {
|
|
159
|
+
name: channelName(r.field("name"), r.at("name")),
|
|
160
|
+
createdBy: readLocalUserId(r.field("createdBy"), r.at("createdBy")),
|
|
161
|
+
...has(input, "operationId") ? { operationId: readLocalTxnId(r.field("operationId"), r.at("operationId")) } : {}
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
function readNameContent(input, path2) {
|
|
165
|
+
const r = object(input, path2, ["name"]);
|
|
166
|
+
return { name: channelName(r.field("name"), r.at("name")) };
|
|
167
|
+
}
|
|
168
|
+
function readMemberContent(input, path2) {
|
|
169
|
+
const modeKey = "com.khala.listening_mode";
|
|
170
|
+
const r = record(input, path2, ["user", "membership", "displayname", "kind"], ["harness", "invitedBy", modeKey]);
|
|
171
|
+
const kind = literal(r.field("kind"), r.at("kind"), ["human", "agent"]);
|
|
172
|
+
if (kind === "human") {
|
|
173
|
+
for (const key of ["harness", modeKey]) if (has(input, key)) fail(r.at(key), "invalid_value");
|
|
174
|
+
}
|
|
175
|
+
return {
|
|
176
|
+
user: readLocalUserId(r.field("user"), r.at("user")),
|
|
177
|
+
membership: literal(r.field("membership"), r.at("membership"), ["invite", "join", "leave"]),
|
|
178
|
+
displayname: displayName(r.field("displayname"), r.at("displayname")),
|
|
179
|
+
kind,
|
|
180
|
+
...has(input, "harness") ? { harness: readHarness(r.field("harness"), r.at("harness")) } : {},
|
|
181
|
+
...has(input, "invitedBy") ? { invitedBy: readLocalUserId(r.field("invitedBy"), r.at("invitedBy")) } : {},
|
|
182
|
+
...has(input, modeKey) ? { [modeKey]: literal(r.field(modeKey), r.at(modeKey), LISTENING_MODES) } : {}
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
function decodeLocalNameContent(input) {
|
|
186
|
+
return decodeWith(() => readNameContent(input, ""));
|
|
187
|
+
}
|
|
188
|
+
function decodeLocalMemberContent(input) {
|
|
189
|
+
return decodeWith(() => readMemberContent(input, ""));
|
|
190
|
+
}
|
|
191
|
+
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, path2, type) {
|
|
193
|
+
switch (type) {
|
|
194
|
+
case "m.room.create":
|
|
195
|
+
return readCreateContent(input, path2);
|
|
196
|
+
case "m.room.name":
|
|
197
|
+
return readNameContent(input, path2);
|
|
198
|
+
case "m.room.member":
|
|
199
|
+
return readMemberContent(input, path2);
|
|
200
|
+
default: {
|
|
201
|
+
const content = contentObject(input, path2);
|
|
202
|
+
if (type === "m.room.message") {
|
|
203
|
+
literal(content["msgtype"], `${path2}.msgtype`, ["m.text", "m.notice"]);
|
|
204
|
+
const body = text(content["body"], `${path2}.body`, LOCAL_CONTENT_MAX_BYTES);
|
|
205
|
+
if (body.length === 0) fail(`${path2}.body`, "empty");
|
|
206
|
+
if (body.length > LOCAL_BODY_MAX_CHARS) fail(`${path2}.body`, "too_long");
|
|
207
|
+
}
|
|
208
|
+
return content;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
function readEvent(input, path2) {
|
|
213
|
+
const r = record(input, path2, ["seq", "eventId", "roomId", "type", "sender", "ts", "content"], ["txnId"]);
|
|
214
|
+
const type = literal(r.field("type"), r.at("type"), EVENT_TYPES);
|
|
215
|
+
return {
|
|
216
|
+
seq: seq(r.field("seq"), r.at("seq")),
|
|
217
|
+
eventId: readLocalEventId(r.field("eventId"), r.at("eventId")),
|
|
218
|
+
roomId: readLocalRoomId(r.field("roomId"), r.at("roomId")),
|
|
219
|
+
type,
|
|
220
|
+
sender: readLocalUserId(r.field("sender"), r.at("sender")),
|
|
221
|
+
ts: safeInteger(r.field("ts"), r.at("ts")),
|
|
222
|
+
...has(input, "txnId") ? { txnId: readLocalTxnId(r.field("txnId"), r.at("txnId")) } : {},
|
|
223
|
+
content: readContent(r.field("content"), r.at("content"), type)
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
function decodeLocalEvent(input) {
|
|
227
|
+
return decodeWith(() => readEvent(input, ""));
|
|
228
|
+
}
|
|
229
|
+
function limitedArray(input, path2, max, read) {
|
|
230
|
+
const values = array(input, path2);
|
|
231
|
+
if (values.length > max) fail(path2, "too_long");
|
|
232
|
+
return values.map((value, i) => read(value, elementPath(path2, i)));
|
|
233
|
+
}
|
|
234
|
+
function readEvents(input, path2, history) {
|
|
235
|
+
const events = limitedArray(input, path2, history ? LOCAL_HISTORY_LIMIT_MAX : LOCAL_EVENTS_PAGE_MAX, readEvent);
|
|
236
|
+
events.forEach((event, i) => {
|
|
237
|
+
if (i > 0 && event.seq <= events[i - 1].seq) fail(`${elementPath(path2, i)}.seq`, "invalid_value");
|
|
238
|
+
if (history && event.type !== "m.room.message" && event.type !== "com.khala.event.v1") fail(`${elementPath(path2, i)}.type`, "invalid_value");
|
|
239
|
+
});
|
|
240
|
+
return events;
|
|
241
|
+
}
|
|
242
|
+
function decodeLocalEventsPage(input) {
|
|
243
|
+
return decodeWith(() => {
|
|
244
|
+
const r = object(input, "", ["events", "next"]);
|
|
245
|
+
const events = readEvents(r.field("events"), r.at("events"), false);
|
|
246
|
+
const next = safeInteger(r.field("next"), r.at("next"));
|
|
247
|
+
if (events.length > 0 && next !== events.at(-1).seq) fail(r.at("next"), "invalid_value");
|
|
248
|
+
return { events, next };
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
function decodeLocalHistoryPage(input) {
|
|
252
|
+
return decodeWith(() => {
|
|
253
|
+
const r = record(input, "", ["events"], ["nextBefore"]);
|
|
254
|
+
return {
|
|
255
|
+
events: readEvents(r.field("events"), r.at("events"), true),
|
|
256
|
+
...has(input, "nextBefore") ? { nextBefore: readLocalEventId(r.field("nextBefore"), r.at("nextBefore")) } : {}
|
|
257
|
+
};
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
function readLocalMe(input, path2) {
|
|
261
|
+
const r = record(input, path2, ["userId", "roomId", "roomName", "membership", "displayName"], ["invitedBy"]);
|
|
262
|
+
return {
|
|
263
|
+
userId: readLocalUserId(r.field("userId"), r.at("userId")),
|
|
264
|
+
roomId: readLocalRoomId(r.field("roomId"), r.at("roomId")),
|
|
265
|
+
roomName: channelName(r.field("roomName"), r.at("roomName")),
|
|
266
|
+
membership: literal(r.field("membership"), r.at("membership"), ["invite", "join", "leave"]),
|
|
267
|
+
...has(input, "invitedBy") ? { invitedBy: readLocalUserId(r.field("invitedBy"), r.at("invitedBy")) } : {},
|
|
268
|
+
displayName: displayName(r.field("displayName"), r.at("displayName"))
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
function decodeLocalMe(input) {
|
|
272
|
+
return decodeWith(() => readLocalMe(input, ""));
|
|
273
|
+
}
|
|
274
|
+
function readLocalJoined(input, path2) {
|
|
275
|
+
const r = record(input, path2, ["seq", "ts"], []);
|
|
276
|
+
return {
|
|
277
|
+
seq: seq(r.field("seq"), r.at("seq")),
|
|
278
|
+
ts: safeInteger(r.field("ts"), r.at("ts"))
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
function decodeLocalJoined(input) {
|
|
282
|
+
return decodeWith(() => readLocalJoined(input, ""));
|
|
283
|
+
}
|
|
284
|
+
function readLocalSendResult(input, path2) {
|
|
285
|
+
const r = record(input, path2, ["eventId"], []);
|
|
286
|
+
return {
|
|
287
|
+
eventId: readLocalEventId(r.field("eventId"), r.at("eventId"))
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
function decodeLocalSendResult(input) {
|
|
291
|
+
return decodeWith(() => readLocalSendResult(input, ""));
|
|
292
|
+
}
|
|
293
|
+
function readLocalErrorBody(input, path2) {
|
|
294
|
+
const r = record(input, path2, ["error"], []);
|
|
295
|
+
return {
|
|
296
|
+
error: identifier(r.field("error"), r.at("error"))
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
function decodeLocalErrorBody(input) {
|
|
300
|
+
return decodeWith(() => readLocalErrorBody(input, ""));
|
|
301
|
+
}
|
|
302
|
+
function readOwnerProfile(input, path2) {
|
|
303
|
+
const r = record(input, path2, ["v", "username", "color", "initials", "updatedAt"]);
|
|
304
|
+
return {
|
|
305
|
+
v: version(r.field("v"), r.at("v")),
|
|
306
|
+
username: readLocalUsername(r.field("username"), r.at("username")),
|
|
307
|
+
color: readHumanColorId(r.field("color"), r.at("color")),
|
|
308
|
+
initials: nullableInitials(r.field("initials"), r.at("initials")),
|
|
309
|
+
updatedAt: utcTimestamp(r.field("updatedAt"), r.at("updatedAt"))
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
function decodeOwnerProfile(input) {
|
|
313
|
+
return decodeWith(() => readOwnerProfile(input, ""));
|
|
314
|
+
}
|
|
315
|
+
function nullableInitials(input, path2) {
|
|
316
|
+
return nullable(input, (value) => readLocalInitials(value, path2));
|
|
317
|
+
}
|
|
318
|
+
function readMember(input, path2) {
|
|
319
|
+
const r = record(input, path2, ["userId", "participantId", "ownerId", "deviceId", "displayName", "kind", "membership"], ["harness", "ownerLabel", "listeningMode"]);
|
|
320
|
+
const userId = readLocalUserId(r.field("userId"), r.at("userId"));
|
|
321
|
+
const participantId = readLocalUserId(r.field("participantId"), r.at("participantId"));
|
|
322
|
+
if (participantId !== userId) fail(r.at("participantId"), "invalid_value");
|
|
323
|
+
const deviceId = identifier(r.field("deviceId"), r.at("deviceId"));
|
|
324
|
+
if (userId === LOCAL_OWNER_USER_ID ? deviceId !== LOCAL_OWNER_DEVICE_ID : !/^KH_LOCAL_[0-9a-f]{8}$/u.test(deviceId)) fail(r.at("deviceId"), "invalid_value");
|
|
325
|
+
const kind = literal(r.field("kind"), r.at("kind"), ["human", "agent"]);
|
|
326
|
+
if (kind === "human") {
|
|
327
|
+
for (const key of ["harness", "ownerLabel", "listeningMode"]) if (has(input, key)) fail(r.at(key), "invalid_value");
|
|
328
|
+
}
|
|
329
|
+
return {
|
|
330
|
+
userId,
|
|
331
|
+
participantId,
|
|
332
|
+
ownerId: literal(r.field("ownerId"), r.at("ownerId"), [LOCAL_OWNER_ID]),
|
|
333
|
+
deviceId,
|
|
334
|
+
displayName: displayName(r.field("displayName"), r.at("displayName")),
|
|
335
|
+
kind,
|
|
336
|
+
membership: literal(r.field("membership"), r.at("membership"), ["invite", "join"]),
|
|
337
|
+
...has(input, "harness") ? { harness: readHarness(r.field("harness"), r.at("harness")) } : {},
|
|
338
|
+
...has(input, "ownerLabel") ? { ownerLabel: readLocalUsername(r.field("ownerLabel"), r.at("ownerLabel")) } : {},
|
|
339
|
+
...has(input, "listeningMode") ? { listeningMode: literal(r.field("listeningMode"), r.at("listeningMode"), LISTENING_MODES) } : {}
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
function decodeLocalMembersResponse(input) {
|
|
343
|
+
return decodeWith(() => {
|
|
344
|
+
const r = object(input, "", ["members"]);
|
|
345
|
+
const members = limitedArray(r.field("members"), r.at("members"), LOCAL_MEMBERS_MAX, readMember);
|
|
346
|
+
const seen = /* @__PURE__ */ new Set();
|
|
347
|
+
members.forEach((member, i) => {
|
|
348
|
+
if (seen.has(member.userId)) fail(`${elementPath(r.at("members"), i)}.userId`, "duplicate");
|
|
349
|
+
seen.add(member.userId);
|
|
350
|
+
});
|
|
351
|
+
return { members };
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
function decodeHelperFile(input) {
|
|
355
|
+
return decodeWith(() => {
|
|
356
|
+
const r = object(input, "", ["v", "pid", "port", "origin", "adminToken", "version", "startedAt"]);
|
|
357
|
+
const pid = seq(r.field("pid"), r.at("pid"));
|
|
358
|
+
const port = seq(r.field("port"), r.at("port"));
|
|
359
|
+
if (port > 65535) fail(r.at("port"), "invalid_value");
|
|
360
|
+
const origin = readHttpUrl(r.field("origin"), r.at("origin"));
|
|
361
|
+
if (origin !== `http://127.0.0.1:${port}`) fail(r.at("origin"), "invalid_value");
|
|
362
|
+
const adminToken = identifier(r.field("adminToken"), r.at("adminToken"));
|
|
363
|
+
if (!/^[A-Za-z0-9_-]{43}$/u.test(adminToken)) fail(r.at("adminToken"), "invalid_value");
|
|
364
|
+
return {
|
|
365
|
+
v: version(r.field("v"), r.at("v")),
|
|
366
|
+
pid,
|
|
367
|
+
port,
|
|
368
|
+
origin,
|
|
369
|
+
adminToken,
|
|
370
|
+
version: identifier(r.field("version"), r.at("version")),
|
|
371
|
+
startedAt: utcTimestamp(r.field("startedAt"), r.at("startedAt"))
|
|
372
|
+
};
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
function readSha256(input, path2) {
|
|
376
|
+
const value = identifier(input, path2);
|
|
377
|
+
if (!/^[0-9a-f]{64}$/u.test(value)) fail(path2, "invalid_value");
|
|
378
|
+
return value;
|
|
379
|
+
}
|
|
380
|
+
function decodeChannelSecrets(input) {
|
|
381
|
+
return decodeWith(() => {
|
|
382
|
+
const r = object(input, "", ["v", "links", "members"]);
|
|
383
|
+
const links = Object.fromEntries(Object.entries(plainObject(r.field("links"), r.at("links"))).map(([key, value]) => {
|
|
384
|
+
const path2 = `${r.at("links")}.${key}`;
|
|
385
|
+
readSha256(key, path2);
|
|
386
|
+
const link = record(value, path2, ["expiresAt", "kind"], ["consumedAt"]);
|
|
387
|
+
return [key, {
|
|
388
|
+
expiresAt: utcTimestamp(link.field("expiresAt"), link.at("expiresAt")),
|
|
389
|
+
kind: literal(link.field("kind"), link.at("kind"), ["join"]),
|
|
390
|
+
...has(value, "consumedAt") ? { consumedAt: utcTimestamp(link.field("consumedAt"), link.at("consumedAt")) } : {}
|
|
391
|
+
}];
|
|
392
|
+
}));
|
|
393
|
+
const members = Object.fromEntries(Object.entries(plainObject(r.field("members"), r.at("members"))).map(([key, value]) => {
|
|
394
|
+
const path2 = `${r.at("members")}.${key}`;
|
|
395
|
+
if (!isLocalAgentUserId(key)) fail(path2, "invalid_value");
|
|
396
|
+
const member = object(value, path2, ["tokenSha256"]);
|
|
397
|
+
return [key, { tokenSha256: readSha256(member.field("tokenSha256"), member.at("tokenSha256")) }];
|
|
398
|
+
}));
|
|
399
|
+
return { v: version(r.field("v"), r.at("v")), links, members };
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// src/local/lifecycle.ts
|
|
404
|
+
import * as fs from "node:fs/promises";
|
|
405
|
+
import { close as closeFd } from "node:fs";
|
|
406
|
+
import path from "node:path";
|
|
407
|
+
import { fileURLToPath } from "node:url";
|
|
408
|
+
import { setTimeout as sleepDefault } from "node:timers/promises";
|
|
409
|
+
import { spawn as nodeSpawn } from "node:child_process";
|
|
410
|
+
function helperPaths(env = process.env) {
|
|
411
|
+
const root = path.join(stateRoot(env), "local");
|
|
412
|
+
const candidate = /^\d{1,5}$/u.test(env.KHALA_LOCAL_PORT ?? "") ? Number(env.KHALA_LOCAL_PORT) : 0;
|
|
413
|
+
const port = candidate >= 1 && candidate <= 65535 ? candidate : LOCAL_DEFAULT_PORT;
|
|
414
|
+
return { root, helperFile: path.join(root, "helper.json"), logFile: path.join(root, "helper.log"), port, origin: `http://127.0.0.1:${port}` };
|
|
415
|
+
}
|
|
416
|
+
async function readHelperFile(env = process.env) {
|
|
417
|
+
const decoded = decodeHelperFile(await readJson(helperPaths(env).helperFile));
|
|
418
|
+
if (!decoded.ok) return null;
|
|
419
|
+
const file = decoded.value;
|
|
420
|
+
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;
|
|
421
|
+
return file;
|
|
422
|
+
}
|
|
423
|
+
function helperBinPath() {
|
|
424
|
+
return fileURLToPath(bundle ? new URL("./khala.mjs", import.meta.url) : new URL("../../bin/khala.mjs", import.meta.url));
|
|
425
|
+
}
|
|
426
|
+
var HELPER_ENV_ALLOWLIST = [
|
|
427
|
+
"PATH",
|
|
428
|
+
"HOME",
|
|
429
|
+
"USER",
|
|
430
|
+
"LOGNAME",
|
|
431
|
+
"LANG",
|
|
432
|
+
"LC_ALL",
|
|
433
|
+
"TMPDIR",
|
|
434
|
+
"XDG_STATE_HOME",
|
|
435
|
+
"NODE_OPTIONS",
|
|
436
|
+
"KHALA_EGRESS_LOG",
|
|
437
|
+
"KHALA_LOCAL_PORT",
|
|
438
|
+
"KHALA_LOCAL_IDLE_MS",
|
|
439
|
+
"KHALA_LOCAL_WEB_DIR"
|
|
440
|
+
];
|
|
441
|
+
function helperChildEnv(env) {
|
|
442
|
+
const childEnv = {};
|
|
443
|
+
for (const key of HELPER_ENV_ALLOWLIST) if (env[key] !== void 0) childEnv[key] = env[key];
|
|
444
|
+
return childEnv;
|
|
445
|
+
}
|
|
446
|
+
var attempts = /* @__PURE__ */ new Map();
|
|
447
|
+
async function healthy(fetchImpl, file) {
|
|
448
|
+
try {
|
|
449
|
+
const response = await fetchImpl(`${file.origin}/healthz`, { signal: AbortSignal.timeout(500) });
|
|
450
|
+
if (response.status !== 200) {
|
|
451
|
+
await response.body?.cancel();
|
|
452
|
+
return false;
|
|
453
|
+
}
|
|
454
|
+
const body = await response.json();
|
|
455
|
+
return typeof body === "object" && body !== null && "ok" in body && body.ok === true && "pid" in body && body.pid === file.pid;
|
|
456
|
+
} catch {
|
|
457
|
+
return false;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
function connection(file) {
|
|
461
|
+
return { origin: file.origin, adminToken: file.adminToken };
|
|
462
|
+
}
|
|
463
|
+
async function attempt(env, deps) {
|
|
464
|
+
const fetchImpl = deps.fetch ?? globalThis.fetch;
|
|
465
|
+
const spawn = deps.spawn ?? ((command, argv, options) => nodeSpawn(command, [...argv], options));
|
|
466
|
+
const sleep = deps.sleep ?? sleepDefault;
|
|
467
|
+
const now = deps.now ?? Date.now;
|
|
468
|
+
const existing = await readHelperFile(env);
|
|
469
|
+
if (existing && await healthy(fetchImpl, existing)) return connection(existing);
|
|
470
|
+
const paths = helperPaths(env);
|
|
471
|
+
try {
|
|
472
|
+
await ensureStateDir(path.join(paths.root, "channels"));
|
|
473
|
+
let handle;
|
|
474
|
+
let fd;
|
|
475
|
+
let spawnFailed = false;
|
|
476
|
+
try {
|
|
477
|
+
if (deps.openLog) fd = await deps.openLog(paths.logFile);
|
|
478
|
+
else {
|
|
479
|
+
handle = await fs.open(paths.logFile, "a", 384);
|
|
480
|
+
await handle.chmod(384);
|
|
481
|
+
fd = handle.fd;
|
|
482
|
+
}
|
|
483
|
+
const child = spawn(process.execPath, [helperBinPath(), "local", "serve"], {
|
|
484
|
+
detached: true,
|
|
485
|
+
stdio: ["ignore", fd, fd],
|
|
486
|
+
env: helperChildEnv(env),
|
|
487
|
+
cwd: paths.root,
|
|
488
|
+
shell: false
|
|
489
|
+
});
|
|
490
|
+
child.on("error", () => {
|
|
491
|
+
spawnFailed = true;
|
|
492
|
+
});
|
|
493
|
+
child.unref();
|
|
494
|
+
} finally {
|
|
495
|
+
if (handle) await handle.close();
|
|
496
|
+
else if (fd !== void 0) {
|
|
497
|
+
const descriptor = fd;
|
|
498
|
+
await new Promise((resolve) => {
|
|
499
|
+
closeFd(descriptor, () => resolve());
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
const deadline = now() + 5e3;
|
|
504
|
+
while (!spawnFailed && now() < deadline) {
|
|
505
|
+
const file = await readHelperFile(env);
|
|
506
|
+
if (file && await healthy(fetchImpl, file) && !spawnFailed && now() <= deadline) return connection(file);
|
|
507
|
+
if (spawnFailed || now() >= deadline) break;
|
|
508
|
+
await sleep(100);
|
|
509
|
+
}
|
|
510
|
+
} catch (error) {
|
|
511
|
+
if (error instanceof StateError) throw error;
|
|
512
|
+
}
|
|
513
|
+
throw new KhalaClientError("internal_error", "helper_unavailable");
|
|
514
|
+
}
|
|
515
|
+
function ensureHelper(env = process.env, deps = {}) {
|
|
516
|
+
const key = helperPaths(env).helperFile;
|
|
517
|
+
const pending = attempts.get(key);
|
|
518
|
+
if (pending) return pending;
|
|
519
|
+
const promise = attempt(env, deps).finally(() => {
|
|
520
|
+
attempts.delete(key);
|
|
521
|
+
});
|
|
522
|
+
attempts.set(key, promise);
|
|
523
|
+
return promise;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
export {
|
|
527
|
+
LOCAL_OWNER_USER_ID,
|
|
528
|
+
LOCAL_LINK_TTL_MS,
|
|
529
|
+
LOCAL_LONG_POLL_MAX_S,
|
|
530
|
+
LOCAL_IDLE_EXIT_MS,
|
|
531
|
+
LOCAL_TOKEN_BYTES,
|
|
532
|
+
LOCAL_AGENT_DEVICE_PREFIX,
|
|
533
|
+
base64url,
|
|
534
|
+
newLocalRoomId,
|
|
535
|
+
newLocalAgentUserId,
|
|
536
|
+
newLocalEventId,
|
|
537
|
+
isLocalRoomId,
|
|
538
|
+
localRoomKey,
|
|
539
|
+
localRoomPath,
|
|
540
|
+
LOCAL_OWNER_ID,
|
|
541
|
+
LOCAL_OWNER_DEVICE_ID,
|
|
542
|
+
decodeLocalNameContent,
|
|
543
|
+
decodeLocalMemberContent,
|
|
544
|
+
decodeLocalEvent,
|
|
545
|
+
decodeLocalEventsPage,
|
|
546
|
+
decodeLocalHistoryPage,
|
|
547
|
+
decodeLocalMe,
|
|
548
|
+
decodeLocalJoined,
|
|
549
|
+
decodeLocalSendResult,
|
|
550
|
+
decodeLocalErrorBody,
|
|
551
|
+
decodeOwnerProfile,
|
|
552
|
+
decodeLocalMembersResponse,
|
|
553
|
+
decodeChannelSecrets,
|
|
554
|
+
helperPaths,
|
|
555
|
+
readHelperFile,
|
|
556
|
+
ensureHelper
|
|
557
|
+
};
|
package/dist/khala.mjs
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire as __khalaCreateRequire } from 'node:module'; const require = __khalaCreateRequire(import.meta.url);
|
|
3
|
+
import {
|
|
4
|
+
KHALA_AGENT_VERSION
|
|
5
|
+
} from "./chunk-Y3TDVOGT.mjs";
|
|
6
|
+
import "./chunk-HMGN52QY.mjs";
|
|
7
|
+
import "./chunk-6FNXMYOM.mjs";
|
|
8
|
+
|
|
9
|
+
// src/cli.ts
|
|
10
|
+
var HOOK_NAME = /^[a-z][a-z0-9-]{0,31}$/;
|
|
11
|
+
var STDIN_CAP = 1024 * 1024;
|
|
12
|
+
var USAGE = "usage: khala mcp | khala hook <name> | khala local <command> | khala install codex | khala --version";
|
|
13
|
+
async function readStdin() {
|
|
14
|
+
if (process.stdin.isTTY) return "";
|
|
15
|
+
const chunks = [];
|
|
16
|
+
let size = 0;
|
|
17
|
+
for await (const chunk of process.stdin) {
|
|
18
|
+
size += chunk.length;
|
|
19
|
+
if (size > STDIN_CAP) return "";
|
|
20
|
+
chunks.push(chunk);
|
|
21
|
+
}
|
|
22
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
23
|
+
}
|
|
24
|
+
function exitCode(value) {
|
|
25
|
+
if (typeof value !== "number" || !Number.isInteger(value)) throw new Error("bad_exit");
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
28
|
+
async function runCli(argv, modules) {
|
|
29
|
+
const [cmd, ...rest] = argv;
|
|
30
|
+
if (cmd === "--version") {
|
|
31
|
+
console.log(KHALA_AGENT_VERSION);
|
|
32
|
+
return 0;
|
|
33
|
+
}
|
|
34
|
+
if (cmd === "mcp" || cmd === "install") {
|
|
35
|
+
const load = modules[cmd]();
|
|
36
|
+
if (!load) {
|
|
37
|
+
console.error(`khala: ${cmd} not available`);
|
|
38
|
+
return 1;
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
return exitCode(await (await load()).default(rest));
|
|
42
|
+
} catch {
|
|
43
|
+
console.error("khala: internal_error");
|
|
44
|
+
return 1;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (cmd === "local") {
|
|
48
|
+
const load = modules.local();
|
|
49
|
+
if (!load) {
|
|
50
|
+
console.log('{"error":"internal_error"}');
|
|
51
|
+
return 1;
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
return exitCode(await (await load()).default(rest));
|
|
55
|
+
} catch {
|
|
56
|
+
console.log('{"error":"internal_error"}');
|
|
57
|
+
return 1;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (cmd === "hook") {
|
|
61
|
+
const [name = "", ...args] = rest;
|
|
62
|
+
const load = HOOK_NAME.test(name) ? modules.hook(name) : void 0;
|
|
63
|
+
if (!load) {
|
|
64
|
+
console.error(`khala: unknown hook ${name}`);
|
|
65
|
+
return 1;
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
const run = (await load()).default;
|
|
69
|
+
return exitCode(await run(await readStdin(), args));
|
|
70
|
+
} catch {
|
|
71
|
+
process.stderr.write('{"ok":false,"warning":"khala_hook_suppressed","code":"internal_error"}\n');
|
|
72
|
+
return 1;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
console.error(USAGE);
|
|
76
|
+
return 1;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// src/cli-bundle.ts
|
|
80
|
+
var hooks = {
|
|
81
|
+
deliver: () => import("./chunk-PQO6675G.mjs"),
|
|
82
|
+
"claude-wake": () => import("./chunk-525XNG6V.mjs")
|
|
83
|
+
};
|
|
84
|
+
process.exitCode = await runCli(process.argv.slice(2), {
|
|
85
|
+
mcp: () => () => import("./chunk-MGXWUWWP.mjs"),
|
|
86
|
+
local: () => () => import("./chunk-HWKLLI7F.mjs"),
|
|
87
|
+
install: () => () => import("./chunk-FGIU3YO2.mjs"),
|
|
88
|
+
hook: (name) => Object.hasOwn(hooks, name) ? hooks[name] : void 0
|
|
89
|
+
});
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|