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,91 @@
|
|
|
1
|
+
import { createRequire as __khalaCreateRequire } from 'node:module'; const require = __khalaCreateRequire(import.meta.url);
|
|
2
|
+
import {
|
|
3
|
+
fail,
|
|
4
|
+
reserved
|
|
5
|
+
} from "./chunk-GO5DD2TR.mjs";
|
|
6
|
+
|
|
7
|
+
// src/client.ts
|
|
8
|
+
var KhalaClientError = class extends Error {
|
|
9
|
+
code;
|
|
10
|
+
constructor(code, message) {
|
|
11
|
+
super(message ?? code);
|
|
12
|
+
this.name = "KhalaClientError";
|
|
13
|
+
this.code = code;
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// ../contracts/src/m1/colors.ts
|
|
18
|
+
var HUMAN_COLOR_IDS = ["red", "orange", "amber", "lime", "green", "teal", "blue", "indigo", "purple", "pink"];
|
|
19
|
+
function isHumanColorId(value) {
|
|
20
|
+
return typeof value === "string" && HUMAN_COLOR_IDS.includes(value);
|
|
21
|
+
}
|
|
22
|
+
function fnv1a(value) {
|
|
23
|
+
let hash = 2166136261;
|
|
24
|
+
for (const byte of new TextEncoder().encode(value)) {
|
|
25
|
+
hash ^= byte;
|
|
26
|
+
hash = Math.imul(hash, 16777619) >>> 0;
|
|
27
|
+
}
|
|
28
|
+
return hash >>> 0;
|
|
29
|
+
}
|
|
30
|
+
function defaultHumanColor(ownerId) {
|
|
31
|
+
return HUMAN_COLOR_IDS[fnv1a(ownerId) % HUMAN_COLOR_IDS.length];
|
|
32
|
+
}
|
|
33
|
+
function readHumanColorId(input, path) {
|
|
34
|
+
if (!isHumanColorId(input)) return fail(path, "invalid_value");
|
|
35
|
+
return input;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ../contracts/src/m1/initials.ts
|
|
39
|
+
function normalizeInitials(input) {
|
|
40
|
+
if (typeof input !== "string") return null;
|
|
41
|
+
const nfc = input.normalize("NFC");
|
|
42
|
+
if ([...nfc].length !== 2) return null;
|
|
43
|
+
const up = nfc.toLocaleUpperCase("en-US");
|
|
44
|
+
return /^[\p{L}\p{Nd}]{2}$/u.test(up) ? up : null;
|
|
45
|
+
}
|
|
46
|
+
function isCanonicalInitials(value) {
|
|
47
|
+
return typeof value === "string" && normalizeInitials(value) === value;
|
|
48
|
+
}
|
|
49
|
+
function readHumanInitials(input, path) {
|
|
50
|
+
if (!isCanonicalInitials(input)) return fail(path, "invalid_value");
|
|
51
|
+
return input;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ../contracts/src/m1/names.ts
|
|
55
|
+
var USERNAME_MIN = 2;
|
|
56
|
+
var USERNAME_MAX = 24;
|
|
57
|
+
var AGENT_NAME_MIN = 2;
|
|
58
|
+
var AGENT_NAME_MAX = 40;
|
|
59
|
+
var mentionName = /^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/u;
|
|
60
|
+
var adjacentSeparators = /[._-]{2}/u;
|
|
61
|
+
var agentSuffix = /-(?:claude|codex)(?:-\d+)?$/iu;
|
|
62
|
+
function checkName(input, kind) {
|
|
63
|
+
if (typeof input !== "string") return { ok: false, error: "invalid_characters" };
|
|
64
|
+
const name = input.trim();
|
|
65
|
+
if (name.length < (kind === "username" ? USERNAME_MIN : AGENT_NAME_MIN)) return { ok: false, error: "too_short" };
|
|
66
|
+
if (name.length > (kind === "username" ? USERNAME_MAX : AGENT_NAME_MAX)) return { ok: false, error: "too_long" };
|
|
67
|
+
if (!mentionName.test(name) || adjacentSeparators.test(name)) return { ok: false, error: "invalid_characters" };
|
|
68
|
+
if (reserved.test(name) || kind === "username" && agentSuffix.test(name)) return { ok: false, error: "reserved" };
|
|
69
|
+
return { ok: true, name };
|
|
70
|
+
}
|
|
71
|
+
var MODEL_NAMES = { claude: "Claude", codex: "Codex" };
|
|
72
|
+
function defaultAgentName(username, harness, n = 1) {
|
|
73
|
+
return `${username}-${MODEL_NAMES[harness]}${n === 1 ? "" : `-${n}`}`;
|
|
74
|
+
}
|
|
75
|
+
function isDefaultAgentName(name, username, harness) {
|
|
76
|
+
const escaped = `${username}-${MODEL_NAMES[harness]}`.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
77
|
+
return new RegExp(`^${escaped}(?:-\\d+)?$`, "iu").test(name);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export {
|
|
81
|
+
KhalaClientError,
|
|
82
|
+
isHumanColorId,
|
|
83
|
+
defaultHumanColor,
|
|
84
|
+
readHumanColorId,
|
|
85
|
+
normalizeInitials,
|
|
86
|
+
readHumanInitials,
|
|
87
|
+
checkName,
|
|
88
|
+
MODEL_NAMES,
|
|
89
|
+
defaultAgentName,
|
|
90
|
+
isDefaultAgentName
|
|
91
|
+
};
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
import { createRequire as __khalaCreateRequire } from 'node:module'; const require = __khalaCreateRequire(import.meta.url);
|
|
2
|
+
import {
|
|
3
|
+
LOCAL_LONG_POLL_MAX_S,
|
|
4
|
+
LOCAL_OWNER_USER_ID,
|
|
5
|
+
decodeLocalErrorBody,
|
|
6
|
+
decodeLocalEventsPage,
|
|
7
|
+
decodeLocalHistoryPage,
|
|
8
|
+
decodeLocalJoined,
|
|
9
|
+
decodeLocalMe,
|
|
10
|
+
decodeLocalMemberContent,
|
|
11
|
+
decodeLocalMembersResponse,
|
|
12
|
+
decodeLocalNameContent,
|
|
13
|
+
decodeLocalSendResult,
|
|
14
|
+
ensureHelper,
|
|
15
|
+
localRoomPath
|
|
16
|
+
} from "./chunk-ZRUF3P53.mjs";
|
|
17
|
+
import {
|
|
18
|
+
KhalaClientError
|
|
19
|
+
} from "./chunk-6GOETMWV.mjs";
|
|
20
|
+
import "./chunk-S6XASSNU.mjs";
|
|
21
|
+
import {
|
|
22
|
+
CHANNEL_EVENT_TYPE,
|
|
23
|
+
LISTENING_MODE_COMMAND_TYPE
|
|
24
|
+
} from "./chunk-GO5DD2TR.mjs";
|
|
25
|
+
import "./chunk-HMGN52QY.mjs";
|
|
26
|
+
import "./chunk-6FNXMYOM.mjs";
|
|
27
|
+
|
|
28
|
+
// src/local/session.ts
|
|
29
|
+
import { randomUUID } from "node:crypto";
|
|
30
|
+
var LocalCallError = class extends Error {
|
|
31
|
+
constructor(status, code) {
|
|
32
|
+
super(code);
|
|
33
|
+
this.status = status;
|
|
34
|
+
this.code = code;
|
|
35
|
+
}
|
|
36
|
+
status;
|
|
37
|
+
code;
|
|
38
|
+
};
|
|
39
|
+
var LocalNetworkError = class extends Error {
|
|
40
|
+
constructor(refused2) {
|
|
41
|
+
super("local_network_error");
|
|
42
|
+
this.refused = refused2;
|
|
43
|
+
}
|
|
44
|
+
refused;
|
|
45
|
+
};
|
|
46
|
+
function decoded(value, decoder) {
|
|
47
|
+
const result = decoder(value);
|
|
48
|
+
if (!result.ok) throw new LocalCallError(0, "protocol");
|
|
49
|
+
return result.value;
|
|
50
|
+
}
|
|
51
|
+
function sleep(ms, signal) {
|
|
52
|
+
return new Promise((resolve, reject) => {
|
|
53
|
+
if (signal?.aborted) {
|
|
54
|
+
reject(new Error("session_stopped"));
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const abort = () => {
|
|
58
|
+
clearTimeout(timer);
|
|
59
|
+
signal?.removeEventListener("abort", abort);
|
|
60
|
+
reject(new Error("session_stopped"));
|
|
61
|
+
};
|
|
62
|
+
const timer = setTimeout(() => {
|
|
63
|
+
signal?.removeEventListener("abort", abort);
|
|
64
|
+
resolve();
|
|
65
|
+
}, ms);
|
|
66
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
function refused(error) {
|
|
70
|
+
const cause = error?.cause;
|
|
71
|
+
return cause?.code === "ECONNREFUSED" || cause?.errors?.some((e) => e.code === "ECONNREFUSED") === true;
|
|
72
|
+
}
|
|
73
|
+
function message(event) {
|
|
74
|
+
return {
|
|
75
|
+
eventId: event.eventId,
|
|
76
|
+
roomId: event.roomId,
|
|
77
|
+
sender: event.sender,
|
|
78
|
+
ts: event.ts,
|
|
79
|
+
type: event.type,
|
|
80
|
+
body: typeof event.content["body"] === "string" ? event.content["body"] : "",
|
|
81
|
+
content: event.content
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
async function createLocalSession(creds, opts = {}) {
|
|
85
|
+
let origin;
|
|
86
|
+
try {
|
|
87
|
+
if (!/^http:\/\/(?:127\.0\.0\.1|localhost|\[::1\]):[0-9]+\/?$/iu.test(creds.homeserver)) throw new Error();
|
|
88
|
+
origin = new URL(creds.homeserver).origin;
|
|
89
|
+
} catch {
|
|
90
|
+
throw new KhalaClientError("internal_error", "invalid_local_origin");
|
|
91
|
+
}
|
|
92
|
+
const fetchImpl = opts.fetch ?? globalThis.fetch;
|
|
93
|
+
const pause = opts.sleep ?? sleep;
|
|
94
|
+
const log = opts.log ?? (() => {
|
|
95
|
+
});
|
|
96
|
+
const ensure = async () => {
|
|
97
|
+
try {
|
|
98
|
+
await (opts.ensureHelper ?? (async () => {
|
|
99
|
+
await ensureHelper(process.env);
|
|
100
|
+
}))();
|
|
101
|
+
} catch (error) {
|
|
102
|
+
const code = error?.code;
|
|
103
|
+
throw new KhalaClientError("internal_error", code === "unsafe_state_dir" || code === "storage_failed" ? code : "helper_unavailable");
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
const controller = new AbortController();
|
|
107
|
+
const names = /* @__PURE__ */ new Map();
|
|
108
|
+
const messages = /* @__PURE__ */ new Set();
|
|
109
|
+
const modes = /* @__PURE__ */ new Set();
|
|
110
|
+
const ended = /* @__PURE__ */ new Set();
|
|
111
|
+
let stopped = false;
|
|
112
|
+
let terminal;
|
|
113
|
+
let joined = false;
|
|
114
|
+
let joining;
|
|
115
|
+
let loop;
|
|
116
|
+
let name;
|
|
117
|
+
let invitedBy;
|
|
118
|
+
let after = 0;
|
|
119
|
+
function check(roomId) {
|
|
120
|
+
if (stopped) throw new Error("session_stopped");
|
|
121
|
+
if (roomId !== creds.roomId) throw new Error("unknown_room");
|
|
122
|
+
if (terminal) throw terminal;
|
|
123
|
+
}
|
|
124
|
+
async function request(method, tail, body, signal, timeout = 1e4) {
|
|
125
|
+
check(creds.roomId);
|
|
126
|
+
const combined = AbortSignal.any([controller.signal, AbortSignal.timeout(timeout), ...signal ? [signal] : []]);
|
|
127
|
+
let response;
|
|
128
|
+
try {
|
|
129
|
+
response = await fetchImpl(origin + localRoomPath(creds.roomId, tail), {
|
|
130
|
+
method,
|
|
131
|
+
headers: { authorization: `Bearer ${creds.accessToken}`, ...body !== void 0 ? { "content-type": "application/json" } : {} },
|
|
132
|
+
...body !== void 0 ? { body: JSON.stringify(body) } : {},
|
|
133
|
+
signal: combined
|
|
134
|
+
});
|
|
135
|
+
} catch (error) {
|
|
136
|
+
if (stopped) throw new Error("session_stopped");
|
|
137
|
+
if (signal?.aborted) throw new Error("request_aborted");
|
|
138
|
+
throw new LocalNetworkError(refused(error));
|
|
139
|
+
}
|
|
140
|
+
if (stopped) throw new Error("session_stopped");
|
|
141
|
+
if (signal?.aborted) throw new Error("request_aborted");
|
|
142
|
+
if (response.status === 204) return void 0;
|
|
143
|
+
let value;
|
|
144
|
+
try {
|
|
145
|
+
value = await response.json();
|
|
146
|
+
} catch {
|
|
147
|
+
if (stopped) throw new Error("session_stopped");
|
|
148
|
+
if (signal?.aborted) throw new Error("request_aborted");
|
|
149
|
+
throw new LocalCallError(response.ok ? 0 : response.status, response.ok ? "protocol" : `http_${response.status}`);
|
|
150
|
+
}
|
|
151
|
+
check(creds.roomId);
|
|
152
|
+
if (signal?.aborted) throw new Error("request_aborted");
|
|
153
|
+
if (!response.ok) {
|
|
154
|
+
const error = decodeLocalErrorBody(value);
|
|
155
|
+
throw new LocalCallError(response.status, error.ok && !error.value.error.includes(creds.accessToken) ? error.value.error : `http_${response.status}`);
|
|
156
|
+
}
|
|
157
|
+
return value;
|
|
158
|
+
}
|
|
159
|
+
async function withHelper(call) {
|
|
160
|
+
try {
|
|
161
|
+
return await call();
|
|
162
|
+
} catch (error) {
|
|
163
|
+
if (!(error instanceof LocalNetworkError && error.refused) || stopped) throw error;
|
|
164
|
+
log("local_helper_restart");
|
|
165
|
+
await ensure();
|
|
166
|
+
return call();
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
function cache(me2) {
|
|
170
|
+
if (me2.userId !== creds.userId || me2.roomId !== creds.roomId) throw new LocalCallError(0, "protocol");
|
|
171
|
+
names.set(me2.userId, me2.displayName);
|
|
172
|
+
name = me2.roomName;
|
|
173
|
+
if (me2.invitedBy !== void 0) invitedBy = me2.invitedBy;
|
|
174
|
+
}
|
|
175
|
+
async function me() {
|
|
176
|
+
const value = decoded(await withHelper(() => request("GET", "me")), decodeLocalMe);
|
|
177
|
+
cache(value);
|
|
178
|
+
return value;
|
|
179
|
+
}
|
|
180
|
+
function dispatch(event) {
|
|
181
|
+
if (stopped || event.roomId !== creds.roomId) return;
|
|
182
|
+
if (event.type === "m.room.member") {
|
|
183
|
+
const content = decoded(event.content, decodeLocalMemberContent);
|
|
184
|
+
names.set(content.user, content.displayname);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
if (event.type === "m.room.name") {
|
|
188
|
+
name = decoded(event.content, decodeLocalNameContent).name;
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
if (event.type === "m.room.create" || event.sender === creds.userId) return;
|
|
192
|
+
if (event.type === LISTENING_MODE_COMMAND_TYPE) {
|
|
193
|
+
for (const handler of modes) {
|
|
194
|
+
if (stopped) break;
|
|
195
|
+
try {
|
|
196
|
+
handler({ eventId: event.eventId, roomId: event.roomId, sender: event.sender, ts: event.ts, content: event.content });
|
|
197
|
+
} catch {
|
|
198
|
+
log("mode_handler_error");
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
} else if (event.type === "m.room.message" || event.type === CHANNEL_EVENT_TYPE) {
|
|
202
|
+
for (const handler of messages) {
|
|
203
|
+
if (stopped) break;
|
|
204
|
+
try {
|
|
205
|
+
handler(message(event));
|
|
206
|
+
} catch {
|
|
207
|
+
log("message_handler_error");
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
async function run() {
|
|
213
|
+
let backoff = 500;
|
|
214
|
+
let streak = 0;
|
|
215
|
+
let lastEnsureAt = 0;
|
|
216
|
+
while (!stopped) {
|
|
217
|
+
try {
|
|
218
|
+
const page = decoded(await request(
|
|
219
|
+
"GET",
|
|
220
|
+
`events?after=${after}&wait=${LOCAL_LONG_POLL_MAX_S}`,
|
|
221
|
+
void 0,
|
|
222
|
+
controller.signal,
|
|
223
|
+
(LOCAL_LONG_POLL_MAX_S + 10) * 1e3
|
|
224
|
+
), decodeLocalEventsPage);
|
|
225
|
+
if (stopped) return;
|
|
226
|
+
for (const event of page.events) {
|
|
227
|
+
if (event.seq <= after) continue;
|
|
228
|
+
after = event.seq;
|
|
229
|
+
dispatch(event);
|
|
230
|
+
}
|
|
231
|
+
backoff = 500;
|
|
232
|
+
streak = 0;
|
|
233
|
+
lastEnsureAt = 0;
|
|
234
|
+
} catch (error) {
|
|
235
|
+
if (stopped) return;
|
|
236
|
+
if (error instanceof LocalCallError && [401, 403, 404].includes(error.status)) {
|
|
237
|
+
terminal = error;
|
|
238
|
+
log(error.status === 404 ? "local_channel_gone" : "local_session_revoked");
|
|
239
|
+
const reason = error.status === 404 ? "channel_deleted" : error.status === 403 ? "removed" : "unauthorized";
|
|
240
|
+
for (const handler of ended) {
|
|
241
|
+
try {
|
|
242
|
+
handler(reason);
|
|
243
|
+
} catch {
|
|
244
|
+
log("ended_handler_error");
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
ended.clear();
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
streak++;
|
|
251
|
+
if (error instanceof LocalNetworkError && error.refused && (lastEnsureAt === 0 || streak - lastEnsureAt >= 5)) {
|
|
252
|
+
lastEnsureAt = streak;
|
|
253
|
+
log("local_helper_restart");
|
|
254
|
+
try {
|
|
255
|
+
await ensure();
|
|
256
|
+
} catch {
|
|
257
|
+
log("local_helper_unavailable");
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
if (stopped) return;
|
|
261
|
+
try {
|
|
262
|
+
await pause(backoff, controller.signal);
|
|
263
|
+
} catch {
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
backoff = Math.min(backoff * 2, 8e3);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
await me();
|
|
271
|
+
async function send(roomId, type, content, txnId = `kls-${randomUUID()}`) {
|
|
272
|
+
check(roomId);
|
|
273
|
+
return decoded(await withHelper(() => request("POST", "send", { txnId, type, content })), decodeLocalSendResult);
|
|
274
|
+
}
|
|
275
|
+
return {
|
|
276
|
+
userId: creds.userId,
|
|
277
|
+
onEnded(handler) {
|
|
278
|
+
if (!stopped) ended.add(handler);
|
|
279
|
+
return () => {
|
|
280
|
+
ended.delete(handler);
|
|
281
|
+
};
|
|
282
|
+
},
|
|
283
|
+
inviter: (roomId) => roomId === creds.roomId ? invitedBy ?? LOCAL_OWNER_USER_ID : void 0,
|
|
284
|
+
roomName: (roomId) => roomId === creds.roomId ? name : void 0,
|
|
285
|
+
displayName: (userId) => names.get(userId),
|
|
286
|
+
onMessage(handler) {
|
|
287
|
+
if (!stopped) messages.add(handler);
|
|
288
|
+
return () => {
|
|
289
|
+
messages.delete(handler);
|
|
290
|
+
};
|
|
291
|
+
},
|
|
292
|
+
onListeningModeCommand(handler) {
|
|
293
|
+
if (!stopped) modes.add(handler);
|
|
294
|
+
return () => {
|
|
295
|
+
modes.delete(handler);
|
|
296
|
+
};
|
|
297
|
+
},
|
|
298
|
+
async waitForInvite(roomId, timeoutMs) {
|
|
299
|
+
check(roomId);
|
|
300
|
+
for (let i = 0; i < Math.max(1, Math.ceil(timeoutMs / 250)); i++) {
|
|
301
|
+
const value = await me();
|
|
302
|
+
if (value.membership === "invite" || value.membership === "join") return;
|
|
303
|
+
await pause(250, controller.signal);
|
|
304
|
+
check(roomId);
|
|
305
|
+
}
|
|
306
|
+
throw new Error("invite_timeout");
|
|
307
|
+
},
|
|
308
|
+
async join(roomId) {
|
|
309
|
+
check(roomId);
|
|
310
|
+
if (joined) return;
|
|
311
|
+
joining ??= (async () => {
|
|
312
|
+
const result = decoded(await withHelper(() => request("POST", "join", {})), decodeLocalJoined);
|
|
313
|
+
after = result.seq;
|
|
314
|
+
await me();
|
|
315
|
+
const members = decoded(await withHelper(() => request("GET", "members")), decodeLocalMembersResponse);
|
|
316
|
+
check(roomId);
|
|
317
|
+
for (const member of members.members) names.set(member.userId, member.displayName);
|
|
318
|
+
joined = true;
|
|
319
|
+
loop = run();
|
|
320
|
+
})();
|
|
321
|
+
try {
|
|
322
|
+
await joining;
|
|
323
|
+
} finally {
|
|
324
|
+
joining = void 0;
|
|
325
|
+
}
|
|
326
|
+
},
|
|
327
|
+
async history(roomId, limit, before) {
|
|
328
|
+
check(roomId);
|
|
329
|
+
const page = decoded(await withHelper(() => request("GET", `messages?limit=${limit}${before !== void 0 ? `&before=${encodeURIComponent(before)}` : ""}`)), decodeLocalHistoryPage);
|
|
330
|
+
const result = page.events.filter((e) => e.roomId === creds.roomId).map(message);
|
|
331
|
+
return { messages: result, ...page.nextBefore !== void 0 ? { nextBefore: page.nextBefore } : {} };
|
|
332
|
+
},
|
|
333
|
+
send: (roomId, text) => send(roomId, "m.room.message", { msgtype: "m.text", body: text }),
|
|
334
|
+
sendChannelEvent: (roomId, content, txnId) => send(roomId, CHANNEL_EVENT_TYPE, content, txnId),
|
|
335
|
+
async publishListeningMode(roomId, mode, signal) {
|
|
336
|
+
check(roomId);
|
|
337
|
+
const result = await withHelper(() => request("PUT", `members/${encodeURIComponent(creds.userId)}`, { listeningMode: mode }, signal));
|
|
338
|
+
if (result !== void 0) throw new LocalCallError(0, "protocol");
|
|
339
|
+
},
|
|
340
|
+
async stop() {
|
|
341
|
+
stopped = true;
|
|
342
|
+
controller.abort();
|
|
343
|
+
messages.clear();
|
|
344
|
+
modes.clear();
|
|
345
|
+
ended.clear();
|
|
346
|
+
await loop;
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
export {
|
|
351
|
+
createLocalSession
|
|
352
|
+
};
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { createRequire as __khalaCreateRequire } from 'node:module'; const require = __khalaCreateRequire(import.meta.url);
|
|
2
|
+
import {
|
|
3
|
+
bundle
|
|
4
|
+
} from "./chunk-HMGN52QY.mjs";
|
|
5
|
+
import "./chunk-6FNXMYOM.mjs";
|
|
6
|
+
|
|
7
|
+
// src/install/main.ts
|
|
8
|
+
import { spawnSync } from "node:child_process";
|
|
9
|
+
import * as fs from "node:fs/promises";
|
|
10
|
+
import os from "node:os";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
|
|
13
|
+
// hooks/hooks.codex.json
|
|
14
|
+
var hooks_codex_default = {
|
|
15
|
+
hooks: {
|
|
16
|
+
PostToolUse: [
|
|
17
|
+
{ hooks: [{ type: "command", command: "khala hook deliver --harness codex", timeout: 10 }] }
|
|
18
|
+
],
|
|
19
|
+
UserPromptSubmit: [{ hooks: [{ type: "command", command: "khala hook deliver --harness codex", timeout: 10 }] }],
|
|
20
|
+
Stop: [{ hooks: [{ type: "command", command: "khala hook deliver --harness codex", timeout: 10 }] }]
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// codex/hooks-config.mjs
|
|
25
|
+
function codexHooksFragment(command = "khala hook deliver --harness codex") {
|
|
26
|
+
const hooks = {};
|
|
27
|
+
for (const [event, groups] of Object.entries(hooks_codex_default.hooks)) {
|
|
28
|
+
hooks[event] = groups.map((group) => ({ ...group, hooks: group.hooks.map((hook) => ({ ...hook, command })) }));
|
|
29
|
+
}
|
|
30
|
+
return { hooks };
|
|
31
|
+
}
|
|
32
|
+
function mergeCodexHooks(config, action, hooksFragment = codexHooksFragment()) {
|
|
33
|
+
if (!config || typeof config !== "object" || Array.isArray(config)) throw new Error("invalid_hooks_config");
|
|
34
|
+
config.hooks ??= {};
|
|
35
|
+
if (typeof config.hooks !== "object" || Array.isArray(config.hooks)) throw new Error("invalid_hooks_config");
|
|
36
|
+
for (const groups of Object.values(config.hooks)) {
|
|
37
|
+
if (!Array.isArray(groups) || groups.some((group) => !group || !Array.isArray(group.hooks))) {
|
|
38
|
+
throw new Error("invalid_hooks_config");
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const commands = new Set(Object.values(hooksFragment.hooks).flatMap((groups) => groups.flatMap((group) => group.hooks.map((hook) => hook.command))));
|
|
42
|
+
const warnings = [];
|
|
43
|
+
if (action === "install") {
|
|
44
|
+
for (const [event, groups] of Object.entries(hooksFragment.hooks)) {
|
|
45
|
+
const existing = config.hooks[event] ??= [];
|
|
46
|
+
for (const group of groups) {
|
|
47
|
+
if (!existing.some((item) => item.hooks.some((hook) => hook.command === group.hooks[0].command))) existing.push(group);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
for (const groups of Object.values(config.hooks)) {
|
|
51
|
+
for (const group of groups) {
|
|
52
|
+
for (const hook of group.hooks) {
|
|
53
|
+
if (typeof hook.command === "string" && hook.command.endsWith(" codex-hook")) {
|
|
54
|
+
warnings.push("warning: remove old Khala handler: " + hook.command);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
} else {
|
|
60
|
+
for (const [event, groups] of Object.entries(config.hooks)) {
|
|
61
|
+
const remaining = groups.flatMap((group) => {
|
|
62
|
+
const hooks = group.hooks.filter((hook) => !commands.has(hook.command));
|
|
63
|
+
if (hooks.length === group.hooks.length) return [group];
|
|
64
|
+
return hooks.length ? [{ ...group, hooks }] : [];
|
|
65
|
+
});
|
|
66
|
+
if (remaining.length === groups.length && remaining.every((group, index) => group === groups[index])) continue;
|
|
67
|
+
if (remaining.length) config.hooks[event] = remaining;
|
|
68
|
+
else delete config.hooks[event];
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return { config, warnings };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// src/install/main.ts
|
|
75
|
+
var MCP_MARKER = "# Khala MCP server, managed by `khala install codex`";
|
|
76
|
+
var USAGE = "usage: khala install codex [--codex-home <dir>] [--uninstall]";
|
|
77
|
+
function shellQuote(value) {
|
|
78
|
+
return /^[A-Za-z0-9_./-]+$/u.test(value) ? value : `'${value.replaceAll("'", `'\\''`)}'`;
|
|
79
|
+
}
|
|
80
|
+
function defaultNpmInstall(prefix, spec) {
|
|
81
|
+
const result = spawnSync("npm", ["install", "--global", "--prefix", prefix, "--no-audit", "--no-fund", "--loglevel=error", spec], {
|
|
82
|
+
stdio: ["ignore", "ignore", "inherit"],
|
|
83
|
+
shell: process.platform === "win32"
|
|
84
|
+
});
|
|
85
|
+
return result.status === 0;
|
|
86
|
+
}
|
|
87
|
+
function updateCodexToml(text, block) {
|
|
88
|
+
const lines = text.split("\n");
|
|
89
|
+
const start = lines.indexOf(MCP_MARKER);
|
|
90
|
+
if (start >= 0) {
|
|
91
|
+
let end = start + 2;
|
|
92
|
+
while (end < lines.length && !/^\s*\[/u.test(lines[end] ?? "")) end++;
|
|
93
|
+
while (end > start + 2 && (lines[end - 1] ?? "").trim() === "") end--;
|
|
94
|
+
const from = start > 0 && (lines[start - 1] ?? "").trim() === "" ? start - 1 : start;
|
|
95
|
+
lines.splice(from, end - from);
|
|
96
|
+
} else if (lines.some((line) => /^\s*\[\s*mcp_servers\s*\.\s*"?khala"?\s*\]/u.test(line))) {
|
|
97
|
+
return { error: "codex_mcp_exists" };
|
|
98
|
+
}
|
|
99
|
+
let out = lines.join("\n").replace(/\n*$/u, "");
|
|
100
|
+
if (block) out = (out ? out + "\n\n" : "") + block;
|
|
101
|
+
return { text: out ? out + "\n" : "" };
|
|
102
|
+
}
|
|
103
|
+
function codexMcpBlock(bin, env) {
|
|
104
|
+
const home = env.HOME && path.isAbsolute(env.HOME) ? env.HOME : os.homedir();
|
|
105
|
+
const state = env.XDG_STATE_HOME && path.isAbsolute(env.XDG_STATE_HOME) ? env.XDG_STATE_HOME : path.join(home, ".local/state");
|
|
106
|
+
return [
|
|
107
|
+
MCP_MARKER,
|
|
108
|
+
"[mcp_servers.khala]",
|
|
109
|
+
`command = ${JSON.stringify(bin)}`,
|
|
110
|
+
'args = ["mcp", "--harness", "codex"]',
|
|
111
|
+
`env = { HOME = ${JSON.stringify(home)}, XDG_STATE_HOME = ${JSON.stringify(state)} }`
|
|
112
|
+
].join("\n");
|
|
113
|
+
}
|
|
114
|
+
async function readOr(file, fallback) {
|
|
115
|
+
try {
|
|
116
|
+
return { text: await fs.readFile(file, "utf8"), mode: (await fs.stat(file)).mode & 511 };
|
|
117
|
+
} catch (error) {
|
|
118
|
+
if (error.code === "ENOENT") return { text: fallback, mode: 384 };
|
|
119
|
+
throw error;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
async function runInstall(argv, deps = {}) {
|
|
123
|
+
const env = deps.env ?? process.env;
|
|
124
|
+
const stdout = deps.stdout ?? ((line) => {
|
|
125
|
+
process.stdout.write(line + "\n");
|
|
126
|
+
});
|
|
127
|
+
const stderr = deps.stderr ?? ((line) => {
|
|
128
|
+
process.stderr.write(line + "\n");
|
|
129
|
+
});
|
|
130
|
+
const [target, ...flags] = argv;
|
|
131
|
+
let codexHome = env.CODEX_HOME && path.isAbsolute(env.CODEX_HOME) ? env.CODEX_HOME : path.join(env.HOME ?? os.homedir(), ".codex");
|
|
132
|
+
let uninstall = false;
|
|
133
|
+
for (let i = 0; i < flags.length; i++) {
|
|
134
|
+
if (flags[i] === "--uninstall") uninstall = true;
|
|
135
|
+
else if (flags[i] === "--codex-home" && flags[i + 1]) codexHome = path.resolve(flags[++i]);
|
|
136
|
+
else {
|
|
137
|
+
stderr(USAGE);
|
|
138
|
+
return 1;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (target !== "codex") {
|
|
142
|
+
stderr(USAGE);
|
|
143
|
+
return 1;
|
|
144
|
+
}
|
|
145
|
+
const pkg = "package" in deps ? deps.package : bundle;
|
|
146
|
+
if (!pkg && !uninstall) {
|
|
147
|
+
stderr("khala: install runs from the published package (npx -y <package> install codex); from a checkout follow packages/agent/docs/install-codex.md");
|
|
148
|
+
return 1;
|
|
149
|
+
}
|
|
150
|
+
const dataHome = env.XDG_DATA_HOME && path.isAbsolute(env.XDG_DATA_HOME) ? env.XDG_DATA_HOME : path.join(env.HOME ?? os.homedir(), ".local/share");
|
|
151
|
+
const prefix = path.join(dataHome, "khala", "npm");
|
|
152
|
+
const bin = path.join(prefix, "bin", "khala");
|
|
153
|
+
const command = `${shellQuote(bin)} hook deliver --harness codex`;
|
|
154
|
+
const hooksFile = path.join(codexHome, "hooks.json");
|
|
155
|
+
const tomlFile = path.join(codexHome, "config.toml");
|
|
156
|
+
const hooks = await readOr(hooksFile, '{"hooks":{}}\n');
|
|
157
|
+
const toml = await readOr(tomlFile, "");
|
|
158
|
+
let parsed;
|
|
159
|
+
try {
|
|
160
|
+
parsed = JSON.parse(hooks.text);
|
|
161
|
+
} catch {
|
|
162
|
+
stderr("khala: invalid hooks.json in " + codexHome);
|
|
163
|
+
return 1;
|
|
164
|
+
}
|
|
165
|
+
let merged;
|
|
166
|
+
try {
|
|
167
|
+
merged = mergeCodexHooks(parsed, uninstall ? "uninstall" : "install", codexHooksFragment(command));
|
|
168
|
+
} catch {
|
|
169
|
+
stderr("khala: invalid hooks.json in " + codexHome);
|
|
170
|
+
return 1;
|
|
171
|
+
}
|
|
172
|
+
const nextToml = updateCodexToml(toml.text, uninstall ? null : codexMcpBlock(bin, env));
|
|
173
|
+
if ("error" in nextToml) {
|
|
174
|
+
stderr(`khala: ${tomlFile} already has an unmanaged [mcp_servers.khala] table; remove it and run this again`);
|
|
175
|
+
return 1;
|
|
176
|
+
}
|
|
177
|
+
if (!uninstall && pkg) {
|
|
178
|
+
const spec = env.KHALA_INSTALL_SPEC || `${pkg.name}@${pkg.version}`;
|
|
179
|
+
stdout(`installing ${spec} into ${prefix}`);
|
|
180
|
+
if (!(deps.npmInstall ?? defaultNpmInstall)(prefix, spec)) {
|
|
181
|
+
stderr("khala: npm install failed for " + spec);
|
|
182
|
+
return 1;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
await fs.mkdir(codexHome, { recursive: true });
|
|
186
|
+
if (!uninstall) {
|
|
187
|
+
try {
|
|
188
|
+
await fs.writeFile(hooksFile + ".khala-bak", hooks.text, { flag: "wx", mode: hooks.mode });
|
|
189
|
+
} catch (error) {
|
|
190
|
+
if (error.code !== "EEXIST") throw error;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
for (const warning of merged.warnings) stderr(warning);
|
|
194
|
+
await fs.writeFile(hooksFile, JSON.stringify(merged.config, null, 2) + "\n", { mode: hooks.mode });
|
|
195
|
+
await fs.writeFile(tomlFile, nextToml.text, { mode: toml.mode });
|
|
196
|
+
if (uninstall) {
|
|
197
|
+
stdout(`removed the Khala MCP server and hooks from ${codexHome}; delete ${prefix} to remove the CLI`);
|
|
198
|
+
} else {
|
|
199
|
+
stdout(`configured ${tomlFile} and ${hooksFile}`);
|
|
200
|
+
stdout('restart or resume Codex, then trust the three Khala hooks in "Hooks need review"');
|
|
201
|
+
}
|
|
202
|
+
return 0;
|
|
203
|
+
}
|
|
204
|
+
async function main(argv) {
|
|
205
|
+
return runInstall(argv);
|
|
206
|
+
}
|
|
207
|
+
export {
|
|
208
|
+
MCP_MARKER,
|
|
209
|
+
codexMcpBlock,
|
|
210
|
+
main as default,
|
|
211
|
+
runInstall,
|
|
212
|
+
shellQuote,
|
|
213
|
+
updateCodexToml
|
|
214
|
+
};
|