khala-cli 0.4.0 → 0.4.2
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-THNGKHZD.mjs → chunk-2OO725IZ.mjs} +14 -6
- package/dist/{chunk-H2INZPFK.mjs → chunk-46PTKRZC.mjs} +92 -39
- package/dist/chunk-5GLTKWJ5.mjs +1 -0
- package/dist/{chunk-3YSVED2R.mjs → chunk-7MQAMABY.mjs} +2 -2
- package/dist/{chunk-ZXJIFFFU.mjs → chunk-A3LJU4JA.mjs} +1 -1
- package/dist/{chunk-AYPFL33Q.mjs → chunk-B5BFL7OD.mjs} +2 -2
- package/dist/{chunk-3WTRQEVS.mjs → chunk-BZ53MZEE.mjs} +176 -229
- package/dist/{chunk-IKHXRKJQ.mjs → chunk-CDHHFC6C.mjs} +1 -1
- package/dist/{chunk-IVPQJSFO.mjs → chunk-JRZZVTQ4.mjs} +34 -7
- package/dist/{chunk-G3SVNU4N.mjs → chunk-K4MDSFHU.mjs} +21 -14
- package/dist/{chunk-ENNXMNKZ.mjs → chunk-MHZWR4A3.mjs} +1 -1
- package/dist/{chunk-SMCTSZYU.mjs → chunk-N6LMHR7P.mjs} +12 -0
- package/dist/{chunk-BNT6YPFR.mjs → chunk-NUS6M27N.mjs} +58 -22
- package/dist/{chunk-NX27G3SB.mjs → chunk-RW3DXWNC.mjs} +1 -1
- package/dist/{chunk-7272YXO6.mjs → chunk-T6RZE5IQ.mjs} +28 -16
- package/dist/{chunk-K2SZLOBE.mjs → chunk-U5ZMOPUK.mjs} +2 -2
- package/dist/chunk-YP45XTOH.mjs +144 -0
- package/dist/khala.mjs +8 -8
- package/dist/web/assets/local-CVA9799w.js +9 -0
- package/dist/web/index.html +1 -1
- package/package.json +10 -2
- package/dist/chunk-AAX35R7C.mjs +0 -91
- package/dist/web/assets/local-CzaQ3d5Z.js +0 -9
|
@@ -1,16 +1,4 @@
|
|
|
1
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-AAX35R7C.mjs";
|
|
8
|
-
import {
|
|
9
|
-
StateError,
|
|
10
|
-
ensureStateDir,
|
|
11
|
-
readJson,
|
|
12
|
-
stateRoot
|
|
13
|
-
} from "./chunk-AYPFL33Q.mjs";
|
|
14
2
|
import {
|
|
15
3
|
LISTENING_MODES,
|
|
16
4
|
M1_LABEL_MAX_BYTES,
|
|
@@ -27,15 +15,86 @@ import {
|
|
|
27
15
|
readHttpUrl,
|
|
28
16
|
readMatrixUserId,
|
|
29
17
|
readRoomId,
|
|
18
|
+
reserved,
|
|
30
19
|
safeInteger,
|
|
31
20
|
text,
|
|
32
21
|
utcTimestamp,
|
|
33
22
|
utf8Length,
|
|
34
23
|
version
|
|
35
|
-
} from "./chunk-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
24
|
+
} from "./chunk-N6LMHR7P.mjs";
|
|
25
|
+
|
|
26
|
+
// src/client.ts
|
|
27
|
+
var KhalaClientError = class extends Error {
|
|
28
|
+
code;
|
|
29
|
+
constructor(code, message) {
|
|
30
|
+
super(message ?? code);
|
|
31
|
+
this.name = "KhalaClientError";
|
|
32
|
+
this.code = code;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// ../contracts/src/m1/colors.ts
|
|
37
|
+
var HUMAN_COLOR_IDS = ["red", "orange", "amber", "lime", "green", "teal", "blue", "indigo", "purple", "pink"];
|
|
38
|
+
function isHumanColorId(value) {
|
|
39
|
+
return typeof value === "string" && HUMAN_COLOR_IDS.includes(value);
|
|
40
|
+
}
|
|
41
|
+
function fnv1a(value) {
|
|
42
|
+
let hash = 2166136261;
|
|
43
|
+
for (const byte of new TextEncoder().encode(value)) {
|
|
44
|
+
hash ^= byte;
|
|
45
|
+
hash = Math.imul(hash, 16777619) >>> 0;
|
|
46
|
+
}
|
|
47
|
+
return hash >>> 0;
|
|
48
|
+
}
|
|
49
|
+
function defaultHumanColor(ownerId) {
|
|
50
|
+
return HUMAN_COLOR_IDS[fnv1a(ownerId) % HUMAN_COLOR_IDS.length];
|
|
51
|
+
}
|
|
52
|
+
function readHumanColorId(input, path) {
|
|
53
|
+
if (!isHumanColorId(input)) return fail(path, "invalid_value");
|
|
54
|
+
return input;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ../contracts/src/m1/initials.ts
|
|
58
|
+
function normalizeInitials(input) {
|
|
59
|
+
if (typeof input !== "string") return null;
|
|
60
|
+
const nfc = input.normalize("NFC");
|
|
61
|
+
if ([...nfc].length !== 2) return null;
|
|
62
|
+
const up = nfc.toLocaleUpperCase("en-US");
|
|
63
|
+
return /^[\p{L}\p{Nd}]{2}$/u.test(up) ? up : null;
|
|
64
|
+
}
|
|
65
|
+
function isCanonicalInitials(value) {
|
|
66
|
+
return typeof value === "string" && normalizeInitials(value) === value;
|
|
67
|
+
}
|
|
68
|
+
function readHumanInitials(input, path) {
|
|
69
|
+
if (!isCanonicalInitials(input)) return fail(path, "invalid_value");
|
|
70
|
+
return input;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ../contracts/src/m1/names.ts
|
|
74
|
+
var USERNAME_MIN = 2;
|
|
75
|
+
var USERNAME_MAX = 24;
|
|
76
|
+
var AGENT_NAME_MIN = 2;
|
|
77
|
+
var AGENT_NAME_MAX = 40;
|
|
78
|
+
var mentionName = /^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/u;
|
|
79
|
+
var adjacentSeparators = /[._-]{2}/u;
|
|
80
|
+
var agentSuffix = /-(?:claude|codex|cursor)(?:-\d+)?$/iu;
|
|
81
|
+
function checkName(input, kind) {
|
|
82
|
+
if (typeof input !== "string") return { ok: false, error: "invalid_characters" };
|
|
83
|
+
const name = input.trim();
|
|
84
|
+
if (name.length < (kind === "username" ? USERNAME_MIN : AGENT_NAME_MIN)) return { ok: false, error: "too_short" };
|
|
85
|
+
if (name.length > (kind === "username" ? USERNAME_MAX : AGENT_NAME_MAX)) return { ok: false, error: "too_long" };
|
|
86
|
+
if (!mentionName.test(name) || adjacentSeparators.test(name)) return { ok: false, error: "invalid_characters" };
|
|
87
|
+
if (reserved.test(name) || kind === "username" && agentSuffix.test(name)) return { ok: false, error: "reserved" };
|
|
88
|
+
return { ok: true, name };
|
|
89
|
+
}
|
|
90
|
+
var MODEL_NAMES = { claude: "Claude", codex: "Codex", cursor: "Cursor" };
|
|
91
|
+
function defaultAgentName(username, harness, n = 1) {
|
|
92
|
+
return `${username}-${MODEL_NAMES[harness]}${n === 1 ? "" : `-${n}`}`;
|
|
93
|
+
}
|
|
94
|
+
function isDefaultAgentName(name, username, harness) {
|
|
95
|
+
const escaped = `${username}-${MODEL_NAMES[harness]}`.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
96
|
+
return new RegExp(`^${escaped}(?:-\\d+)?$`, "iu").test(name);
|
|
97
|
+
}
|
|
39
98
|
|
|
40
99
|
// ../contracts/src/m1/local.ts
|
|
41
100
|
var LOCAL_OWNER_USER_ID = "@khala_owner:local";
|
|
@@ -84,90 +143,90 @@ var localRoomKey = (roomId) => {
|
|
|
84
143
|
var localRoomPath = (roomId, tail) => `/api/local/rooms/${encodeURIComponent(roomId)}/${tail}`;
|
|
85
144
|
var LOCAL_OWNER_ID = "local-owner";
|
|
86
145
|
var LOCAL_OWNER_DEVICE_ID = "KH_LOCAL_OWNER";
|
|
87
|
-
function readLocalRoomId(input,
|
|
88
|
-
const value = readRoomId(input,
|
|
89
|
-
if (!isLocalRoomId(value)) fail(
|
|
146
|
+
function readLocalRoomId(input, path) {
|
|
147
|
+
const value = readRoomId(input, path);
|
|
148
|
+
if (!isLocalRoomId(value)) fail(path, "invalid_value");
|
|
90
149
|
return value;
|
|
91
150
|
}
|
|
92
|
-
function readLocalUserId(input,
|
|
93
|
-
const value = readMatrixUserId(input,
|
|
94
|
-
if (!isLocalUserId(value)) fail(
|
|
151
|
+
function readLocalUserId(input, path) {
|
|
152
|
+
const value = readMatrixUserId(input, path);
|
|
153
|
+
if (!isLocalUserId(value)) fail(path, "invalid_value");
|
|
95
154
|
return value;
|
|
96
155
|
}
|
|
97
|
-
function readLocalEventId(input,
|
|
98
|
-
const value = identifier(input,
|
|
99
|
-
if (!isLocalEventId(value)) fail(
|
|
156
|
+
function readLocalEventId(input, path) {
|
|
157
|
+
const value = identifier(input, path);
|
|
158
|
+
if (!isLocalEventId(value)) fail(path, "invalid_value");
|
|
100
159
|
return value;
|
|
101
160
|
}
|
|
102
|
-
function readLocalTxnId(input,
|
|
103
|
-
const value = identifier(input,
|
|
104
|
-
if (!isLocalTxnId(value)) fail(
|
|
161
|
+
function readLocalTxnId(input, path) {
|
|
162
|
+
const value = identifier(input, path);
|
|
163
|
+
if (!isLocalTxnId(value)) fail(path, "invalid_value");
|
|
105
164
|
return value;
|
|
106
165
|
}
|
|
107
|
-
function readLocalUsername(input,
|
|
108
|
-
const value = identifier(input,
|
|
166
|
+
function readLocalUsername(input, path) {
|
|
167
|
+
const value = identifier(input, path);
|
|
109
168
|
const checked = checkName(value, "username");
|
|
110
|
-
if (!checked.ok || checked.name !== value) fail(
|
|
169
|
+
if (!checked.ok || checked.name !== value) fail(path, "invalid_value");
|
|
111
170
|
return value;
|
|
112
171
|
}
|
|
113
|
-
function readLocalInitials(input,
|
|
114
|
-
return readHumanInitials(input,
|
|
172
|
+
function readLocalInitials(input, path) {
|
|
173
|
+
return readHumanInitials(input, path);
|
|
115
174
|
}
|
|
116
|
-
function record(input,
|
|
175
|
+
function record(input, path, required, optional = []) {
|
|
117
176
|
const present = optional.filter((key) => typeof input === "object" && input !== null && Object.hasOwn(input, key));
|
|
118
|
-
return object(input,
|
|
177
|
+
return object(input, path, [...required, ...present]);
|
|
119
178
|
}
|
|
120
179
|
function has(input, key) {
|
|
121
180
|
return typeof input === "object" && input !== null && Object.hasOwn(input, key);
|
|
122
181
|
}
|
|
123
|
-
function plainObject(input,
|
|
124
|
-
if (typeof input !== "object" || input === null || Array.isArray(input)) fail(
|
|
182
|
+
function plainObject(input, path) {
|
|
183
|
+
if (typeof input !== "object" || input === null || Array.isArray(input)) fail(path, "not_object");
|
|
125
184
|
const prototype = Object.getPrototypeOf(input);
|
|
126
|
-
if (prototype !== Object.prototype && prototype !== null) fail(
|
|
185
|
+
if (prototype !== Object.prototype && prototype !== null) fail(path, "not_object");
|
|
127
186
|
return input;
|
|
128
187
|
}
|
|
129
|
-
function contentObject(input,
|
|
130
|
-
const value = plainObject(input,
|
|
188
|
+
function contentObject(input, path) {
|
|
189
|
+
const value = plainObject(input, path);
|
|
131
190
|
let encoded;
|
|
132
191
|
try {
|
|
133
192
|
encoded = JSON.stringify(value);
|
|
134
193
|
} catch {
|
|
135
|
-
fail(
|
|
194
|
+
fail(path, "invalid_value");
|
|
136
195
|
}
|
|
137
|
-
if (utf8Length(encoded) > LOCAL_CONTENT_MAX_BYTES) fail(
|
|
196
|
+
if (utf8Length(encoded) > LOCAL_CONTENT_MAX_BYTES) fail(path, "too_long");
|
|
138
197
|
return value;
|
|
139
198
|
}
|
|
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(
|
|
199
|
+
function channelName(input, path) {
|
|
200
|
+
const value = displayName(input, path);
|
|
201
|
+
if ([...value].length > LOCAL_CHANNEL_NAME_MAX_CHARS) fail(path, "too_long");
|
|
202
|
+
if (value !== value.trim()) fail(path, "invalid_value");
|
|
144
203
|
return value;
|
|
145
204
|
}
|
|
146
|
-
function displayName(input,
|
|
147
|
-
const value = displayText(input,
|
|
148
|
-
if (value.length === 0) fail(
|
|
205
|
+
function displayName(input, path) {
|
|
206
|
+
const value = displayText(input, path, M1_LABEL_MAX_BYTES);
|
|
207
|
+
if (value.length === 0) fail(path, "empty");
|
|
149
208
|
return value;
|
|
150
209
|
}
|
|
151
|
-
function seq(input,
|
|
152
|
-
const value = safeInteger(input,
|
|
153
|
-
if (value === 0) fail(
|
|
210
|
+
function seq(input, path) {
|
|
211
|
+
const value = safeInteger(input, path);
|
|
212
|
+
if (value === 0) fail(path, "invalid_value");
|
|
154
213
|
return value;
|
|
155
214
|
}
|
|
156
|
-
function readCreateContent(input,
|
|
157
|
-
const r = record(input,
|
|
215
|
+
function readCreateContent(input, path) {
|
|
216
|
+
const r = record(input, path, ["name", "createdBy"], ["operationId"]);
|
|
158
217
|
return {
|
|
159
218
|
name: channelName(r.field("name"), r.at("name")),
|
|
160
219
|
createdBy: readLocalUserId(r.field("createdBy"), r.at("createdBy")),
|
|
161
220
|
...has(input, "operationId") ? { operationId: readLocalTxnId(r.field("operationId"), r.at("operationId")) } : {}
|
|
162
221
|
};
|
|
163
222
|
}
|
|
164
|
-
function readNameContent(input,
|
|
165
|
-
const r = object(input,
|
|
223
|
+
function readNameContent(input, path) {
|
|
224
|
+
const r = object(input, path, ["name"]);
|
|
166
225
|
return { name: channelName(r.field("name"), r.at("name")) };
|
|
167
226
|
}
|
|
168
|
-
function readMemberContent(input,
|
|
227
|
+
function readMemberContent(input, path) {
|
|
169
228
|
const modeKey = "com.khala.listening_mode";
|
|
170
|
-
const r = record(input,
|
|
229
|
+
const r = record(input, path, ["user", "membership", "displayname", "kind"], ["harness", "invitedBy", modeKey]);
|
|
171
230
|
const kind = literal(r.field("kind"), r.at("kind"), ["human", "agent"]);
|
|
172
231
|
if (kind === "human") {
|
|
173
232
|
for (const key of ["harness", modeKey]) if (has(input, key)) fail(r.at(key), "invalid_value");
|
|
@@ -189,28 +248,28 @@ function decodeLocalMemberContent(input) {
|
|
|
189
248
|
return decodeWith(() => readMemberContent(input, ""));
|
|
190
249
|
}
|
|
191
250
|
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,
|
|
251
|
+
function readContent(input, path, type) {
|
|
193
252
|
switch (type) {
|
|
194
253
|
case "m.room.create":
|
|
195
|
-
return readCreateContent(input,
|
|
254
|
+
return readCreateContent(input, path);
|
|
196
255
|
case "m.room.name":
|
|
197
|
-
return readNameContent(input,
|
|
256
|
+
return readNameContent(input, path);
|
|
198
257
|
case "m.room.member":
|
|
199
|
-
return readMemberContent(input,
|
|
258
|
+
return readMemberContent(input, path);
|
|
200
259
|
default: {
|
|
201
|
-
const content = contentObject(input,
|
|
260
|
+
const content = contentObject(input, path);
|
|
202
261
|
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(`${
|
|
262
|
+
literal(content["msgtype"], `${path}.msgtype`, ["m.text", "m.notice"]);
|
|
263
|
+
const body = text(content["body"], `${path}.body`, LOCAL_CONTENT_MAX_BYTES);
|
|
264
|
+
if (body.length === 0) fail(`${path}.body`, "empty");
|
|
265
|
+
if (body.length > LOCAL_BODY_MAX_CHARS) fail(`${path}.body`, "too_long");
|
|
207
266
|
}
|
|
208
267
|
return content;
|
|
209
268
|
}
|
|
210
269
|
}
|
|
211
270
|
}
|
|
212
|
-
function readEvent(input,
|
|
213
|
-
const r = record(input,
|
|
271
|
+
function readEvent(input, path) {
|
|
272
|
+
const r = record(input, path, ["seq", "eventId", "roomId", "type", "sender", "ts", "content"], ["txnId", "previousContent"]);
|
|
214
273
|
const type = literal(r.field("type"), r.at("type"), EVENT_TYPES);
|
|
215
274
|
return {
|
|
216
275
|
seq: seq(r.field("seq"), r.at("seq")),
|
|
@@ -220,22 +279,23 @@ function readEvent(input, path2) {
|
|
|
220
279
|
sender: readLocalUserId(r.field("sender"), r.at("sender")),
|
|
221
280
|
ts: safeInteger(r.field("ts"), r.at("ts")),
|
|
222
281
|
...has(input, "txnId") ? { txnId: readLocalTxnId(r.field("txnId"), r.at("txnId")) } : {},
|
|
223
|
-
content: readContent(r.field("content"), r.at("content"), type)
|
|
282
|
+
content: readContent(r.field("content"), r.at("content"), type),
|
|
283
|
+
...has(input, "previousContent") ? { previousContent: readMemberContent(r.field("previousContent"), r.at("previousContent")) } : {}
|
|
224
284
|
};
|
|
225
285
|
}
|
|
226
286
|
function decodeLocalEvent(input) {
|
|
227
287
|
return decodeWith(() => readEvent(input, ""));
|
|
228
288
|
}
|
|
229
|
-
function limitedArray(input,
|
|
230
|
-
const values = array(input,
|
|
231
|
-
if (values.length > max) fail(
|
|
232
|
-
return values.map((value, i) => read(value, elementPath(
|
|
289
|
+
function limitedArray(input, path, max, read) {
|
|
290
|
+
const values = array(input, path);
|
|
291
|
+
if (values.length > max) fail(path, "too_long");
|
|
292
|
+
return values.map((value, i) => read(value, elementPath(path, i)));
|
|
233
293
|
}
|
|
234
|
-
function readEvents(input,
|
|
235
|
-
const events = limitedArray(input,
|
|
294
|
+
function readEvents(input, path, history) {
|
|
295
|
+
const events = limitedArray(input, path, history ? LOCAL_HISTORY_LIMIT_MAX : LOCAL_EVENTS_PAGE_MAX, readEvent);
|
|
236
296
|
events.forEach((event, i) => {
|
|
237
|
-
if (i > 0 && event.seq <= events[i - 1].seq) fail(`${elementPath(
|
|
238
|
-
if (history && event.type !== "m.room.message" && event.type !== "com.khala.event.v1") fail(`${elementPath(
|
|
297
|
+
if (i > 0 && event.seq <= events[i - 1].seq) fail(`${elementPath(path, i)}.seq`, "invalid_value");
|
|
298
|
+
if (history && event.type !== "m.room.message" && event.type !== "com.khala.event.v1") fail(`${elementPath(path, i)}.type`, "invalid_value");
|
|
239
299
|
});
|
|
240
300
|
return events;
|
|
241
301
|
}
|
|
@@ -257,8 +317,8 @@ function decodeLocalHistoryPage(input) {
|
|
|
257
317
|
};
|
|
258
318
|
});
|
|
259
319
|
}
|
|
260
|
-
function readLocalMe(input,
|
|
261
|
-
const r = record(input,
|
|
320
|
+
function readLocalMe(input, path) {
|
|
321
|
+
const r = record(input, path, ["userId", "roomId", "roomName", "membership", "displayName"], ["invitedBy"]);
|
|
262
322
|
return {
|
|
263
323
|
userId: readLocalUserId(r.field("userId"), r.at("userId")),
|
|
264
324
|
roomId: readLocalRoomId(r.field("roomId"), r.at("roomId")),
|
|
@@ -271,8 +331,8 @@ function readLocalMe(input, path2) {
|
|
|
271
331
|
function decodeLocalMe(input) {
|
|
272
332
|
return decodeWith(() => readLocalMe(input, ""));
|
|
273
333
|
}
|
|
274
|
-
function readLocalJoined(input,
|
|
275
|
-
const r = record(input,
|
|
334
|
+
function readLocalJoined(input, path) {
|
|
335
|
+
const r = record(input, path, ["seq", "ts"], []);
|
|
276
336
|
return {
|
|
277
337
|
seq: seq(r.field("seq"), r.at("seq")),
|
|
278
338
|
ts: safeInteger(r.field("ts"), r.at("ts"))
|
|
@@ -281,8 +341,8 @@ function readLocalJoined(input, path2) {
|
|
|
281
341
|
function decodeLocalJoined(input) {
|
|
282
342
|
return decodeWith(() => readLocalJoined(input, ""));
|
|
283
343
|
}
|
|
284
|
-
function readLocalSendResult(input,
|
|
285
|
-
const r = record(input,
|
|
344
|
+
function readLocalSendResult(input, path) {
|
|
345
|
+
const r = record(input, path, ["eventId"], []);
|
|
286
346
|
return {
|
|
287
347
|
eventId: readLocalEventId(r.field("eventId"), r.at("eventId"))
|
|
288
348
|
};
|
|
@@ -290,8 +350,8 @@ function readLocalSendResult(input, path2) {
|
|
|
290
350
|
function decodeLocalSendResult(input) {
|
|
291
351
|
return decodeWith(() => readLocalSendResult(input, ""));
|
|
292
352
|
}
|
|
293
|
-
function readLocalErrorBody(input,
|
|
294
|
-
const r = record(input,
|
|
353
|
+
function readLocalErrorBody(input, path) {
|
|
354
|
+
const r = record(input, path, ["error"], []);
|
|
295
355
|
return {
|
|
296
356
|
error: identifier(r.field("error"), r.at("error"))
|
|
297
357
|
};
|
|
@@ -299,8 +359,8 @@ function readLocalErrorBody(input, path2) {
|
|
|
299
359
|
function decodeLocalErrorBody(input) {
|
|
300
360
|
return decodeWith(() => readLocalErrorBody(input, ""));
|
|
301
361
|
}
|
|
302
|
-
function readOwnerProfile(input,
|
|
303
|
-
const r = record(input,
|
|
362
|
+
function readOwnerProfile(input, path) {
|
|
363
|
+
const r = record(input, path, ["v", "username", "color", "initials", "updatedAt"]);
|
|
304
364
|
return {
|
|
305
365
|
v: version(r.field("v"), r.at("v")),
|
|
306
366
|
username: readLocalUsername(r.field("username"), r.at("username")),
|
|
@@ -312,11 +372,11 @@ function readOwnerProfile(input, path2) {
|
|
|
312
372
|
function decodeOwnerProfile(input) {
|
|
313
373
|
return decodeWith(() => readOwnerProfile(input, ""));
|
|
314
374
|
}
|
|
315
|
-
function nullableInitials(input,
|
|
316
|
-
return nullable(input, (value) => readLocalInitials(value,
|
|
375
|
+
function nullableInitials(input, path) {
|
|
376
|
+
return nullable(input, (value) => readLocalInitials(value, path));
|
|
317
377
|
}
|
|
318
|
-
function readMember(input,
|
|
319
|
-
const r = record(input,
|
|
378
|
+
function readMember(input, path) {
|
|
379
|
+
const r = record(input, path, ["userId", "participantId", "ownerId", "deviceId", "displayName", "kind", "membership"], ["harness", "ownerLabel", "listeningMode"]);
|
|
320
380
|
const userId = readLocalUserId(r.field("userId"), r.at("userId"));
|
|
321
381
|
const participantId = readLocalUserId(r.field("participantId"), r.at("participantId"));
|
|
322
382
|
if (participantId !== userId) fail(r.at("participantId"), "invalid_value");
|
|
@@ -372,18 +432,18 @@ function decodeHelperFile(input) {
|
|
|
372
432
|
};
|
|
373
433
|
});
|
|
374
434
|
}
|
|
375
|
-
function readSha256(input,
|
|
376
|
-
const value = identifier(input,
|
|
377
|
-
if (!/^[0-9a-f]{64}$/u.test(value)) fail(
|
|
435
|
+
function readSha256(input, path) {
|
|
436
|
+
const value = identifier(input, path);
|
|
437
|
+
if (!/^[0-9a-f]{64}$/u.test(value)) fail(path, "invalid_value");
|
|
378
438
|
return value;
|
|
379
439
|
}
|
|
380
440
|
function decodeChannelSecrets(input) {
|
|
381
441
|
return decodeWith(() => {
|
|
382
442
|
const r = object(input, "", ["v", "links", "members"]);
|
|
383
443
|
const links = Object.fromEntries(Object.entries(plainObject(r.field("links"), r.at("links"))).map(([key, value]) => {
|
|
384
|
-
const
|
|
385
|
-
readSha256(key,
|
|
386
|
-
const link = record(value,
|
|
444
|
+
const path = `${r.at("links")}.${key}`;
|
|
445
|
+
readSha256(key, path);
|
|
446
|
+
const link = record(value, path, ["expiresAt", "kind"], ["consumedAt"]);
|
|
387
447
|
return [key, {
|
|
388
448
|
expiresAt: utcTimestamp(link.field("expiresAt"), link.at("expiresAt")),
|
|
389
449
|
kind: literal(link.field("kind"), link.at("kind"), ["join"]),
|
|
@@ -391,140 +451,29 @@ function decodeChannelSecrets(input) {
|
|
|
391
451
|
}];
|
|
392
452
|
}));
|
|
393
453
|
const members = Object.fromEntries(Object.entries(plainObject(r.field("members"), r.at("members"))).map(([key, value]) => {
|
|
394
|
-
const
|
|
395
|
-
if (!isLocalAgentUserId(key)) fail(
|
|
396
|
-
const member =
|
|
397
|
-
return [key, {
|
|
454
|
+
const path = `${r.at("members")}.${key}`;
|
|
455
|
+
if (!isLocalAgentUserId(key)) fail(path, "invalid_value");
|
|
456
|
+
const member = record(value, path, ["tokenSha256"], ["sessionKey"]);
|
|
457
|
+
return [key, {
|
|
458
|
+
tokenSha256: readSha256(member.field("tokenSha256"), member.at("tokenSha256")),
|
|
459
|
+
...has(value, "sessionKey") ? { sessionKey: readSha256(member.field("sessionKey"), member.at("sessionKey")) } : {}
|
|
460
|
+
}];
|
|
398
461
|
}));
|
|
399
462
|
return { v: version(r.field("v"), r.at("v")), links, members };
|
|
400
463
|
});
|
|
401
464
|
}
|
|
402
465
|
|
|
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
466
|
export {
|
|
467
|
+
KhalaClientError,
|
|
468
|
+
isHumanColorId,
|
|
469
|
+
defaultHumanColor,
|
|
470
|
+
normalizeInitials,
|
|
471
|
+
checkName,
|
|
472
|
+
MODEL_NAMES,
|
|
473
|
+
defaultAgentName,
|
|
474
|
+
isDefaultAgentName,
|
|
527
475
|
LOCAL_OWNER_USER_ID,
|
|
476
|
+
LOCAL_DEFAULT_PORT,
|
|
528
477
|
LOCAL_LINK_TTL_MS,
|
|
529
478
|
LOCAL_LONG_POLL_MAX_S,
|
|
530
479
|
LOCAL_IDLE_EXIT_MS,
|
|
@@ -550,8 +499,6 @@ export {
|
|
|
550
499
|
decodeLocalErrorBody,
|
|
551
500
|
decodeOwnerProfile,
|
|
552
501
|
decodeLocalMembersResponse,
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
readHelperFile,
|
|
556
|
-
ensureHelper
|
|
502
|
+
decodeHelperFile,
|
|
503
|
+
decodeChannelSecrets
|
|
557
504
|
};
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { createRequire as __khalaCreateRequire } from 'node:module'; const require = __khalaCreateRequire(import.meta.url);
|
|
2
|
+
import "./chunk-5GLTKWJ5.mjs";
|
|
2
3
|
import {
|
|
3
4
|
CHANNEL_EVENT_TYPE,
|
|
4
5
|
LISTENING_MODE_COMMAND_TYPE,
|
|
5
|
-
LISTENING_MODE_MEMBER_KEY
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
LISTENING_MODE_MEMBER_KEY,
|
|
7
|
+
encodeChannelEvent,
|
|
8
|
+
memberListeningMode,
|
|
9
|
+
memberRenameContent
|
|
10
|
+
} from "./chunk-N6LMHR7P.mjs";
|
|
11
|
+
import "./chunk-A3LJU4JA.mjs";
|
|
8
12
|
|
|
9
13
|
// src/matrix/session.ts
|
|
10
14
|
import { createClient, ClientEvent, RoomEvent, MatrixEventEvent, SyncState, Direction, Method, EventType } from "matrix-js-sdk";
|
|
@@ -13,9 +17,19 @@ function message(event) {
|
|
|
13
17
|
const eventId = event.getId();
|
|
14
18
|
const roomId = event.getRoomId();
|
|
15
19
|
const sender = event.getSender();
|
|
16
|
-
if (event.isDecryptionFailure() || !eventId || !roomId || !sender || type !== "m.room.message" && type !== "com.khala.event.v1") return;
|
|
20
|
+
if (event.isDecryptionFailure() || !eventId || !roomId || !sender || type !== "m.room.message" && type !== "com.khala.event.v1" && type !== "m.room.member") return;
|
|
17
21
|
const content = event.getContent();
|
|
18
|
-
|
|
22
|
+
if (type === "m.room.member" && !memberRenameContent(content, event.getPrevContent())) return;
|
|
23
|
+
return {
|
|
24
|
+
eventId,
|
|
25
|
+
roomId,
|
|
26
|
+
sender,
|
|
27
|
+
ts: event.getTs(),
|
|
28
|
+
type,
|
|
29
|
+
body: typeof content.body === "string" ? content.body : "",
|
|
30
|
+
content,
|
|
31
|
+
...type === "m.room.member" ? { previousContent: event.getPrevContent() } : {}
|
|
32
|
+
};
|
|
19
33
|
}
|
|
20
34
|
async function createAgentMatrixSession(creds, opts) {
|
|
21
35
|
const started = Date.now();
|
|
@@ -40,7 +54,7 @@ async function createAgentMatrixSession(creds, opts) {
|
|
|
40
54
|
}
|
|
41
55
|
const cutoff = joinTimes.get(m.roomId);
|
|
42
56
|
if (cutoff === void 0) return;
|
|
43
|
-
if (m.sender === creds.userId || m.ts < cutoff || client.getRoom(m.roomId)?.getMyMembership() !== "join") {
|
|
57
|
+
if (m.sender === creds.userId && event.getType() !== "m.room.member" || m.ts < cutoff || client.getRoom(m.roomId)?.getMyMembership() !== "join") {
|
|
44
58
|
liveEvents.delete(event);
|
|
45
59
|
return;
|
|
46
60
|
}
|
|
@@ -153,6 +167,7 @@ async function createAgentMatrixSession(creds, opts) {
|
|
|
153
167
|
}, ready, timeoutMs, error);
|
|
154
168
|
return {
|
|
155
169
|
userId: creds.userId,
|
|
170
|
+
listeningMode: (roomId) => memberListeningMode(client.getRoom(roomId)?.currentState.getStateEvents("m.room.member", creds.userId)?.getContent()),
|
|
156
171
|
inviter(roomId) {
|
|
157
172
|
return inviters.get(roomId);
|
|
158
173
|
},
|
|
@@ -179,6 +194,8 @@ async function createAgentMatrixSession(creds, opts) {
|
|
|
179
194
|
},
|
|
180
195
|
async join(roomId) {
|
|
181
196
|
if (stopped) throw new Error("session_stopped");
|
|
197
|
+
if (joinedRoom === roomId) return;
|
|
198
|
+
const rejoinedAt = Date.now();
|
|
182
199
|
const membership = client.getRoom(roomId)?.getMyMembership();
|
|
183
200
|
if (membership !== "join") {
|
|
184
201
|
if (membership !== "invite") throw new Error("not_invited");
|
|
@@ -189,7 +206,17 @@ async function createAgentMatrixSession(creds, opts) {
|
|
|
189
206
|
await membershipWait(roomId, () => client.getRoom(roomId)?.getMyMembership() === "join", 3e4, "join_timeout");
|
|
190
207
|
const ownJoin = client.getRoom(roomId)?.currentState.getStateEvents("m.room.member", creds.userId);
|
|
191
208
|
if (!ownJoin || ownJoin.getContent().membership !== "join") throw new Error("join_state_unavailable");
|
|
192
|
-
|
|
209
|
+
if (membership === "join") {
|
|
210
|
+
const owner = ownJoin.getContent()["com.khala.invited_by"];
|
|
211
|
+
if (typeof owner === "string") inviters.set(roomId, owner);
|
|
212
|
+
const event = encodeChannelEvent({ kind: "member", summary: `${ownJoin.getContent().displayname ?? creds.userId} rejoined`, status: "info", source: { system: "khala-agent" } });
|
|
213
|
+
if (event.ok) await client.sendEvent(roomId, CHANNEL_EVENT_TYPE, event.value, `khala.rejoin.${creds.deviceId}`);
|
|
214
|
+
}
|
|
215
|
+
if (membership !== "join" && inviters.has(roomId)) {
|
|
216
|
+
const content = { ...ownJoin.getContent(), membership: "join", "com.khala.invited_by": inviters.get(roomId) };
|
|
217
|
+
await client.sendStateEvent(roomId, EventType.RoomMember, content, creds.userId);
|
|
218
|
+
}
|
|
219
|
+
joinTimes.set(roomId, membership === "join" ? rejoinedAt : ownJoin.getTs());
|
|
193
220
|
joinedRoom = roomId;
|
|
194
221
|
for (const event of liveEvents) {
|
|
195
222
|
if (event.getRoomId() === roomId) deliver(event);
|