khala-cli 0.3.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -1
- package/dist/{chunk-MGXWUWWP.mjs → chunk-35KKPOKI.mjs} +47 -25
- package/dist/{chunk-ZRUF3P53.mjs → chunk-3SWCRLWX.mjs} +12 -8
- package/dist/chunk-5GLTKWJ5.mjs +1 -0
- package/dist/{chunk-6GOETMWV.mjs → chunk-AVJKIJ34.mjs} +3 -3
- package/dist/{chunk-S6XASSNU.mjs → chunk-B5BFL7OD.mjs} +28 -10
- package/dist/{chunk-525XNG6V.mjs → chunk-CSIFRJFF.mjs} +1 -1
- package/dist/{chunk-CTXFYQIX.mjs → chunk-EVLXC2WS.mjs} +25 -13
- package/dist/{chunk-HMGN52QY.mjs → chunk-HAD5ZILW.mjs} +1 -1
- package/dist/{chunk-4C2TBZVA.mjs → chunk-KEGR6VEQ.mjs} +34 -7
- package/dist/{chunk-UYTGV6TW.mjs → chunk-L2HRDTA5.mjs} +17 -8
- package/dist/{chunk-GO5DD2TR.mjs → chunk-N6LMHR7P.mjs} +16 -3
- package/dist/{chunk-FGIU3YO2.mjs → chunk-PSN7P4AV.mjs} +163 -9
- package/dist/{chunk-HWKLLI7F.mjs → chunk-RX5HGJQT.mjs} +80 -26
- package/dist/{chunk-PQO6675G.mjs → chunk-S72K3GSP.mjs} +70 -5
- package/dist/{chunk-6FNXMYOM.mjs → chunk-SR5BG7FL.mjs} +1 -1
- package/dist/{chunk-H2JCKPFR.mjs → chunk-U5ZMOPUK.mjs} +2 -2
- package/dist/chunk-VUE2DDYY.mjs +34 -0
- package/dist/{chunk-Y3TDVOGT.mjs → chunk-YMDAY234.mjs} +1 -1
- package/dist/khala.mjs +9 -9
- package/dist/web/assets/local-DGdQ2afg.js +9 -0
- package/dist/web/assets/local-DHYvcQG0.css +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +11 -3
- package/dist/web/assets/local-DuGvSGji.css +0 -1
- package/dist/web/assets/local-hWRGqoWt.js +0 -9
|
@@ -4,16 +4,17 @@ import {
|
|
|
4
4
|
MODEL_NAMES,
|
|
5
5
|
checkName,
|
|
6
6
|
isDefaultAgentName
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-AVJKIJ34.mjs";
|
|
8
8
|
import {
|
|
9
9
|
StateError,
|
|
10
10
|
readJson,
|
|
11
11
|
stateRoot,
|
|
12
12
|
writeJsonAtomic
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-B5BFL7OD.mjs";
|
|
14
14
|
import {
|
|
15
|
+
HARNESSES,
|
|
15
16
|
validateAgentName
|
|
16
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-N6LMHR7P.mjs";
|
|
17
18
|
|
|
18
19
|
// src/local/identity.ts
|
|
19
20
|
import * as fs from "node:fs/promises";
|
|
@@ -36,7 +37,7 @@ async function ensurePrivateRoot(root) {
|
|
|
36
37
|
if (error.code !== "EEXIST") throw error;
|
|
37
38
|
}
|
|
38
39
|
const stat = await fs.lstat(root);
|
|
39
|
-
if (!stat.isDirectory() || stat.isSymbolicLink() || (stat.mode & 63) !== 0 || typeof process.getuid === "function" && stat.uid !== process.getuid()) throw new StateError("unsafe_state_dir");
|
|
40
|
+
if (!stat.isDirectory() || stat.isSymbolicLink() || process.platform !== "win32" && (stat.mode & 63) !== 0 || typeof process.getuid === "function" && stat.uid !== process.getuid()) throw new StateError("unsafe_state_dir");
|
|
40
41
|
} catch (error) {
|
|
41
42
|
if (error instanceof StateError) throw error;
|
|
42
43
|
throw new StateError("storage_failed");
|
|
@@ -138,13 +139,21 @@ async function post(url, origin, body, deps, pollSecret) {
|
|
|
138
139
|
async function requestJoin(input, deps = {}) {
|
|
139
140
|
const parsed = parseChannelLink(input.link);
|
|
140
141
|
if (!parsed) throw new KhalaClientError("invalid_link", "invalid_link");
|
|
141
|
-
if (
|
|
142
|
+
if (!HARNESSES.includes(input.harness)) fail("invalid_harness");
|
|
142
143
|
const label = validateAgentName(input.label);
|
|
143
144
|
if (!label.ok || [...label.name].length > 40) fail("invalid_label");
|
|
144
145
|
const { origin } = parsed;
|
|
145
|
-
const
|
|
146
|
-
const
|
|
147
|
-
const
|
|
146
|
+
const url = new URL("/api/agent/join", origin);
|
|
147
|
+
const base = { link: input.link, harness: input.harness, label: label.name };
|
|
148
|
+
const rejoin = input.sessionId !== void 0 && input.rejoinSecret !== void 0;
|
|
149
|
+
let response = await post(url, origin, rejoin ? { ...base, sessionId: input.sessionId, rejoinSecret: input.rejoinSecret } : base, deps);
|
|
150
|
+
let body = await json(response);
|
|
151
|
+
let code = serverCode(body);
|
|
152
|
+
if (rejoin && response.status === 400 && code === "invalid_link") {
|
|
153
|
+
response = await post(url, origin, base, deps);
|
|
154
|
+
body = await json(response);
|
|
155
|
+
code = serverCode(body);
|
|
156
|
+
}
|
|
148
157
|
if (response.status !== 201) {
|
|
149
158
|
if (response.status === 400 && code === "invalid_link") throw new KhalaClientError("invalid_link", "invalid_link");
|
|
150
159
|
if (response.status === 400 && (code === "invalid_label" || code === "invalid_harness")) fail(code);
|
|
@@ -212,7 +212,7 @@ function validateAgentName(input) {
|
|
|
212
212
|
}
|
|
213
213
|
|
|
214
214
|
// ../contracts/src/m1/agent-join.ts
|
|
215
|
-
var HARNESSES = ["claude", "codex"];
|
|
215
|
+
var HARNESSES = ["claude", "codex", "cursor"];
|
|
216
216
|
var M1_LABEL_MAX_BYTES = 512;
|
|
217
217
|
var agentConfirmPagePath = (id) => `/agent/confirm?joinId=${encodeURIComponent(id)}`;
|
|
218
218
|
function readHarness(input, path) {
|
|
@@ -239,6 +239,8 @@ function readRoomId(input, path) {
|
|
|
239
239
|
if (!/^!\S+$/u.test(value)) fail2(path, "invalid_value");
|
|
240
240
|
return value;
|
|
241
241
|
}
|
|
242
|
+
var validAgentSessionId = (value) => typeof value === "string" && /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,127}$/u.test(value);
|
|
243
|
+
var validAgentRejoinSecret = (value) => typeof value === "string" && /^[A-Za-z0-9_-]{43}$/u.test(value);
|
|
242
244
|
|
|
243
245
|
// ../contracts/src/m1/listening-mode.ts
|
|
244
246
|
var LISTENING_MODE_COMMAND_TYPE = "com.khala.listening_mode.v1";
|
|
@@ -381,6 +383,13 @@ function encodeChannelEvent(input) {
|
|
|
381
383
|
return decodeChannelEvent({ ...decoded.value, body: formatChannelEventLine(decoded.value) });
|
|
382
384
|
});
|
|
383
385
|
}
|
|
386
|
+
function memberRenameContent(content, previous) {
|
|
387
|
+
const oldName = previous?.["displayname"];
|
|
388
|
+
const newName = content["displayname"];
|
|
389
|
+
if (previous?.["membership"] !== "join" || content["membership"] !== "join" || typeof oldName !== "string" || !oldName.trim() || typeof newName !== "string" || !newName.trim() || oldName === newName) return null;
|
|
390
|
+
const encoded = encodeChannelEvent({ kind: "member", summary: `${oldName} is now ${newName}`, status: "info", source: { system: "khala" } });
|
|
391
|
+
return encoded.ok ? encoded.value : null;
|
|
392
|
+
}
|
|
384
393
|
function statusFor(content) {
|
|
385
394
|
if (content.status !== void 0) return content.status;
|
|
386
395
|
switch (content.kind) {
|
|
@@ -403,8 +412,6 @@ function statusFor(content) {
|
|
|
403
412
|
}
|
|
404
413
|
|
|
405
414
|
export {
|
|
406
|
-
LISTENING_MODES,
|
|
407
|
-
decodeListeningMode,
|
|
408
415
|
fail2 as fail,
|
|
409
416
|
decodeWith2 as decodeWith,
|
|
410
417
|
object2 as object,
|
|
@@ -423,12 +430,17 @@ export {
|
|
|
423
430
|
utcTimestamp2 as utcTimestamp,
|
|
424
431
|
reserved,
|
|
425
432
|
validateAgentName,
|
|
433
|
+
HARNESSES,
|
|
426
434
|
M1_LABEL_MAX_BYTES,
|
|
427
435
|
agentConfirmPagePath,
|
|
428
436
|
readHarness,
|
|
429
437
|
readHttpUrl,
|
|
430
438
|
readMatrixUserId,
|
|
431
439
|
readRoomId,
|
|
440
|
+
validAgentSessionId,
|
|
441
|
+
validAgentRejoinSecret,
|
|
442
|
+
LISTENING_MODES,
|
|
443
|
+
decodeListeningMode,
|
|
432
444
|
LISTENING_MODE_COMMAND_TYPE,
|
|
433
445
|
LISTENING_MODE_MEMBER_KEY,
|
|
434
446
|
DEFAULT_LISTENING_MODE,
|
|
@@ -438,5 +450,6 @@ export {
|
|
|
438
450
|
decodeChannelEvent,
|
|
439
451
|
formatChannelEventLine,
|
|
440
452
|
encodeChannelEvent,
|
|
453
|
+
memberRenameContent,
|
|
441
454
|
statusFor
|
|
442
455
|
};
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { createRequire as __khalaCreateRequire } from 'node:module'; const require = __khalaCreateRequire(import.meta.url);
|
|
2
|
+
import {
|
|
3
|
+
CURSOR_WORKSPACE_ENV
|
|
4
|
+
} from "./chunk-VUE2DDYY.mjs";
|
|
2
5
|
import {
|
|
3
6
|
bundle
|
|
4
|
-
} from "./chunk-
|
|
5
|
-
import "./chunk-
|
|
7
|
+
} from "./chunk-HAD5ZILW.mjs";
|
|
8
|
+
import "./chunk-SR5BG7FL.mjs";
|
|
6
9
|
|
|
7
10
|
// src/install/main.ts
|
|
8
11
|
import { spawnSync } from "node:child_process";
|
|
9
|
-
import * as
|
|
12
|
+
import * as fs2 from "node:fs/promises";
|
|
10
13
|
import os from "node:os";
|
|
11
14
|
import path from "node:path";
|
|
12
15
|
|
|
@@ -71,9 +74,159 @@ function mergeCodexHooks(config, action, hooksFragment = codexHooksFragment()) {
|
|
|
71
74
|
return { config, warnings };
|
|
72
75
|
}
|
|
73
76
|
|
|
77
|
+
// src/install/cursor.ts
|
|
78
|
+
import * as fs from "node:fs/promises";
|
|
79
|
+
var CURSOR_HOOK_EVENTS = ["beforeSubmitPrompt", "postToolUse", "stop"];
|
|
80
|
+
var HOOK_SUFFIX = " hook deliver --harness cursor";
|
|
81
|
+
function cursorPaths({ platform, path: path2, home, env }, packageName = "khala-cli") {
|
|
82
|
+
const cursorDir = path2.join(home, ".cursor");
|
|
83
|
+
const windows = platform === "win32";
|
|
84
|
+
const dataHome = windows ? env.LOCALAPPDATA && path2.isAbsolute(env.LOCALAPPDATA) ? env.LOCALAPPDATA : path2.join(home, "AppData", "Local") : env.XDG_DATA_HOME && path2.isAbsolute(env.XDG_DATA_HOME) ? env.XDG_DATA_HOME : path2.join(home, ".local", "share");
|
|
85
|
+
const prefix = path2.join(dataHome, "khala", "npm");
|
|
86
|
+
const modules = windows ? path2.join(prefix, "node_modules") : path2.join(prefix, "lib", "node_modules");
|
|
87
|
+
return {
|
|
88
|
+
cursorDir,
|
|
89
|
+
prefix,
|
|
90
|
+
mcpFile: path2.join(cursorDir, "mcp.json"),
|
|
91
|
+
hooksFile: path2.join(cursorDir, "hooks.json"),
|
|
92
|
+
script: path2.join(modules, ...packageName.split("/"), "dist", "khala.mjs")
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
var posixQuote = (value) => /^[A-Za-z0-9_./:-]+$/u.test(value) ? value : `'${value.replaceAll("'", `'\\''`)}'`;
|
|
96
|
+
function cursorHookCommand(platform, node, script) {
|
|
97
|
+
if (platform === "win32") return `node "${script.replaceAll("\\", "/")}"${HOOK_SUFFIX}`;
|
|
98
|
+
return `${posixQuote(node)} ${posixQuote(script)}${HOOK_SUFFIX}`;
|
|
99
|
+
}
|
|
100
|
+
function cursorMcpEntry(node, script) {
|
|
101
|
+
return {
|
|
102
|
+
type: "stdio",
|
|
103
|
+
command: node,
|
|
104
|
+
args: [script, "mcp", "--harness", "cursor"],
|
|
105
|
+
env: { [CURSOR_WORKSPACE_ENV]: "${workspaceFolder}" }
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
109
|
+
function isKhalaCursorServer(entry) {
|
|
110
|
+
if (!isObject(entry) || !Array.isArray(entry.args)) return false;
|
|
111
|
+
const args = entry.args;
|
|
112
|
+
const at = args.indexOf("--harness");
|
|
113
|
+
return args.includes("mcp") && at !== -1 && args[at + 1] === "cursor";
|
|
114
|
+
}
|
|
115
|
+
function mergeCursorMcp(config, entry) {
|
|
116
|
+
if (!isObject(config)) return { error: "invalid_config" };
|
|
117
|
+
const servers = config.mcpServers ?? {};
|
|
118
|
+
if (!isObject(servers)) return { error: "invalid_config" };
|
|
119
|
+
if (Object.hasOwn(servers, "khala") && !isKhalaCursorServer(servers.khala)) return { error: "cursor_mcp_exists" };
|
|
120
|
+
const next = { ...servers };
|
|
121
|
+
delete next.khala;
|
|
122
|
+
if (entry) next.khala = entry;
|
|
123
|
+
return { config: { ...config, mcpServers: next } };
|
|
124
|
+
}
|
|
125
|
+
function mergeCursorHooks(config, command) {
|
|
126
|
+
if (!isObject(config)) return { error: "invalid_config" };
|
|
127
|
+
const hooks = config.hooks ?? {};
|
|
128
|
+
if (!isObject(hooks) || Object.values(hooks).some((list) => !Array.isArray(list) || list.some((item) => !isObject(item)))) return { error: "invalid_config" };
|
|
129
|
+
const ours = (item) => typeof item.command === "string" && item.command.endsWith(HOOK_SUFFIX);
|
|
130
|
+
const next = {};
|
|
131
|
+
for (const [event, list] of Object.entries(hooks)) {
|
|
132
|
+
const kept = list.filter((item) => !ours(item));
|
|
133
|
+
if (kept.length) next[event] = kept;
|
|
134
|
+
}
|
|
135
|
+
if (command) {
|
|
136
|
+
for (const event of CURSOR_HOOK_EVENTS) next[event] = [...next[event] ?? [], { command, timeout: 10 }];
|
|
137
|
+
}
|
|
138
|
+
return { config: { version: 1, ...config, hooks: next } };
|
|
139
|
+
}
|
|
140
|
+
async function readJsonFile(file) {
|
|
141
|
+
let text;
|
|
142
|
+
try {
|
|
143
|
+
text = await fs.readFile(file, "utf8");
|
|
144
|
+
} catch (error) {
|
|
145
|
+
if (error.code === "ENOENT") return { text: null, value: {} };
|
|
146
|
+
throw error;
|
|
147
|
+
}
|
|
148
|
+
if (!text.trim()) return { text, value: {} };
|
|
149
|
+
try {
|
|
150
|
+
return { text, value: JSON.parse(text.replace(/^/u, "")) };
|
|
151
|
+
} catch {
|
|
152
|
+
return { text, value: void 0 };
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
async function installCursor(input) {
|
|
156
|
+
const { paths, uninstall, stdout, stderr } = input;
|
|
157
|
+
const mcp = await readJsonFile(paths.mcpFile);
|
|
158
|
+
const hooks = await readJsonFile(paths.hooksFile);
|
|
159
|
+
const command = cursorHookCommand(input.platform, input.node, paths.script);
|
|
160
|
+
const nextMcp = mergeCursorMcp(mcp.value, uninstall ? null : cursorMcpEntry(input.node, paths.script));
|
|
161
|
+
if ("error" in nextMcp) {
|
|
162
|
+
stderr(nextMcp.error === "cursor_mcp_exists" ? `khala: ${paths.mcpFile} already has a "khala" server that is not this CLI; remove it and run this again` : `khala: invalid JSON in ${paths.mcpFile}`);
|
|
163
|
+
return 1;
|
|
164
|
+
}
|
|
165
|
+
const nextHooks = mergeCursorHooks(hooks.value, uninstall ? null : command);
|
|
166
|
+
if ("error" in nextHooks) {
|
|
167
|
+
stderr(`khala: invalid JSON in ${paths.hooksFile}`);
|
|
168
|
+
return 1;
|
|
169
|
+
}
|
|
170
|
+
if (!uninstall && input.install && !input.install()) return 1;
|
|
171
|
+
await fs.mkdir(paths.cursorDir, { recursive: true });
|
|
172
|
+
for (const [file, before, after] of [[paths.mcpFile, mcp.text, nextMcp.config], [paths.hooksFile, hooks.text, nextHooks.config]]) {
|
|
173
|
+
if (uninstall && before === null) continue;
|
|
174
|
+
if (!uninstall && before !== null) {
|
|
175
|
+
try {
|
|
176
|
+
await fs.writeFile(file + ".khala-bak", before, { flag: "wx" });
|
|
177
|
+
} catch (error) {
|
|
178
|
+
if (error.code !== "EEXIST") throw error;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
const text = JSON.stringify(after, null, 2) + "\n";
|
|
182
|
+
if (text !== before) await fs.writeFile(file, text);
|
|
183
|
+
}
|
|
184
|
+
if (uninstall) {
|
|
185
|
+
stdout(`removed the Khala MCP server and hooks from ${paths.cursorDir}; delete ${paths.prefix} to remove the CLI`);
|
|
186
|
+
} else {
|
|
187
|
+
stdout(`configured ${paths.mcpFile} and ${paths.hooksFile}`);
|
|
188
|
+
stdout('restart Cursor (or toggle "khala" off and on in Settings > MCP), then check that khala shows its five tools');
|
|
189
|
+
}
|
|
190
|
+
return 0;
|
|
191
|
+
}
|
|
192
|
+
|
|
74
193
|
// src/install/main.ts
|
|
75
194
|
var MCP_MARKER = "# Khala MCP server, managed by `khala install codex`";
|
|
76
|
-
var USAGE = "usage: khala install codex [--codex-home <dir>] [--uninstall]";
|
|
195
|
+
var USAGE = "usage: khala install codex [--codex-home <dir>] [--uninstall] | khala install cursor [--uninstall]";
|
|
196
|
+
async function runCursorInstall(flags, deps, env, stdout, stderr) {
|
|
197
|
+
let uninstall = false;
|
|
198
|
+
for (const flag of flags) {
|
|
199
|
+
if (flag === "--uninstall") uninstall = true;
|
|
200
|
+
else {
|
|
201
|
+
stderr(USAGE);
|
|
202
|
+
return 1;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
const pkg = "package" in deps ? deps.package : bundle;
|
|
206
|
+
if (!pkg && !uninstall) {
|
|
207
|
+
stderr("khala: install runs from the published package (npx -y khala-cli install cursor)");
|
|
208
|
+
return 1;
|
|
209
|
+
}
|
|
210
|
+
const platform = deps.platform ?? process.platform;
|
|
211
|
+
const pathApi = platform === "win32" ? path.win32 : path.posix;
|
|
212
|
+
const home = deps.home ?? (platform === "win32" ? env.USERPROFILE || os.homedir() : env.HOME || os.homedir());
|
|
213
|
+
const paths = cursorPaths({ platform, path: pathApi, home, env }, pkg?.name);
|
|
214
|
+
const spec = pkg ? env.KHALA_INSTALL_SPEC || `${pkg.name}@${pkg.version}` : "";
|
|
215
|
+
return installCursor({
|
|
216
|
+
paths,
|
|
217
|
+
platform,
|
|
218
|
+
node: deps.node ?? process.execPath,
|
|
219
|
+
uninstall,
|
|
220
|
+
stdout,
|
|
221
|
+
stderr,
|
|
222
|
+
install: () => {
|
|
223
|
+
stdout(`installing ${spec} into ${paths.prefix}`);
|
|
224
|
+
if ((deps.npmInstall ?? defaultNpmInstall)(paths.prefix, spec)) return true;
|
|
225
|
+
stderr("khala: npm install failed for " + spec);
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
}
|
|
77
230
|
function shellQuote(value) {
|
|
78
231
|
return /^[A-Za-z0-9_./-]+$/u.test(value) ? value : `'${value.replaceAll("'", `'\\''`)}'`;
|
|
79
232
|
}
|
|
@@ -113,7 +266,7 @@ function codexMcpBlock(bin, env) {
|
|
|
113
266
|
}
|
|
114
267
|
async function readOr(file, fallback) {
|
|
115
268
|
try {
|
|
116
|
-
return { text: await
|
|
269
|
+
return { text: await fs2.readFile(file, "utf8"), mode: (await fs2.stat(file)).mode & 511 };
|
|
117
270
|
} catch (error) {
|
|
118
271
|
if (error.code === "ENOENT") return { text: fallback, mode: 384 };
|
|
119
272
|
throw error;
|
|
@@ -128,6 +281,7 @@ async function runInstall(argv, deps = {}) {
|
|
|
128
281
|
process.stderr.write(line + "\n");
|
|
129
282
|
});
|
|
130
283
|
const [target, ...flags] = argv;
|
|
284
|
+
if (target === "cursor") return runCursorInstall(flags, deps, env, stdout, stderr);
|
|
131
285
|
let codexHome = env.CODEX_HOME && path.isAbsolute(env.CODEX_HOME) ? env.CODEX_HOME : path.join(env.HOME ?? os.homedir(), ".codex");
|
|
132
286
|
let uninstall = false;
|
|
133
287
|
for (let i = 0; i < flags.length; i++) {
|
|
@@ -182,17 +336,17 @@ async function runInstall(argv, deps = {}) {
|
|
|
182
336
|
return 1;
|
|
183
337
|
}
|
|
184
338
|
}
|
|
185
|
-
await
|
|
339
|
+
await fs2.mkdir(codexHome, { recursive: true });
|
|
186
340
|
if (!uninstall) {
|
|
187
341
|
try {
|
|
188
|
-
await
|
|
342
|
+
await fs2.writeFile(hooksFile + ".khala-bak", hooks.text, { flag: "wx", mode: hooks.mode });
|
|
189
343
|
} catch (error) {
|
|
190
344
|
if (error.code !== "EEXIST") throw error;
|
|
191
345
|
}
|
|
192
346
|
}
|
|
193
347
|
for (const warning of merged.warnings) stderr(warning);
|
|
194
|
-
await
|
|
195
|
-
await
|
|
348
|
+
await fs2.writeFile(hooksFile, JSON.stringify(merged.config, null, 2) + "\n", { mode: hooks.mode });
|
|
349
|
+
await fs2.writeFile(tomlFile, nextToml.text, { mode: toml.mode });
|
|
196
350
|
if (uninstall) {
|
|
197
351
|
stdout(`removed the Khala MCP server and hooks from ${codexHome}; delete ${prefix} to remove the CLI`);
|
|
198
352
|
} else {
|
|
@@ -20,14 +20,14 @@ import {
|
|
|
20
20
|
newLocalEventId,
|
|
21
21
|
newLocalRoomId,
|
|
22
22
|
readHelperFile
|
|
23
|
-
} from "./chunk-
|
|
23
|
+
} from "./chunk-3SWCRLWX.mjs";
|
|
24
24
|
import {
|
|
25
25
|
parseChannelLink,
|
|
26
26
|
resolveLocalOwnerName
|
|
27
|
-
} from "./chunk-
|
|
27
|
+
} from "./chunk-L2HRDTA5.mjs";
|
|
28
28
|
import {
|
|
29
29
|
KHALA_AGENT_VERSION
|
|
30
|
-
} from "./chunk-
|
|
30
|
+
} from "./chunk-YMDAY234.mjs";
|
|
31
31
|
import {
|
|
32
32
|
checkName,
|
|
33
33
|
defaultAgentName,
|
|
@@ -35,16 +35,18 @@ import {
|
|
|
35
35
|
isDefaultAgentName,
|
|
36
36
|
isHumanColorId,
|
|
37
37
|
normalizeInitials
|
|
38
|
-
} from "./chunk-
|
|
38
|
+
} from "./chunk-AVJKIJ34.mjs";
|
|
39
39
|
import {
|
|
40
40
|
StateError,
|
|
41
41
|
ensureStateDir,
|
|
42
42
|
readJson,
|
|
43
43
|
writeJsonAtomic
|
|
44
|
-
} from "./chunk-
|
|
44
|
+
} from "./chunk-B5BFL7OD.mjs";
|
|
45
|
+
import "./chunk-5GLTKWJ5.mjs";
|
|
45
46
|
import {
|
|
46
47
|
CHANNEL_EVENT_TYPE,
|
|
47
48
|
DEFAULT_LISTENING_MODE,
|
|
49
|
+
HARNESSES,
|
|
48
50
|
LISTENING_MODE_COMMAND_TYPE,
|
|
49
51
|
LISTENING_MODE_MEMBER_KEY,
|
|
50
52
|
agentConfirmPagePath,
|
|
@@ -54,13 +56,16 @@ import {
|
|
|
54
56
|
decodeWith,
|
|
55
57
|
encodeChannelEvent,
|
|
56
58
|
memberListeningMode,
|
|
59
|
+
memberRenameContent,
|
|
57
60
|
object,
|
|
58
|
-
utf8Length
|
|
59
|
-
|
|
61
|
+
utf8Length,
|
|
62
|
+
validAgentRejoinSecret,
|
|
63
|
+
validAgentSessionId
|
|
64
|
+
} from "./chunk-N6LMHR7P.mjs";
|
|
60
65
|
import {
|
|
61
66
|
bundle
|
|
62
|
-
} from "./chunk-
|
|
63
|
-
import "./chunk-
|
|
67
|
+
} from "./chunk-HAD5ZILW.mjs";
|
|
68
|
+
import "./chunk-SR5BG7FL.mjs";
|
|
64
69
|
|
|
65
70
|
// src/local/cli.ts
|
|
66
71
|
import { setTimeout as sleepDefault } from "node:timers/promises";
|
|
@@ -111,6 +116,7 @@ async function openLocalStore(input) {
|
|
|
111
116
|
byEventId: /* @__PURE__ */ new Map(),
|
|
112
117
|
byTxn: /* @__PURE__ */ new Map(),
|
|
113
118
|
lastMember: /* @__PURE__ */ new Map(),
|
|
119
|
+
previousMembers: /* @__PURE__ */ new Map(),
|
|
114
120
|
secrets: emptySecrets(),
|
|
115
121
|
waiters: /* @__PURE__ */ new Set()
|
|
116
122
|
};
|
|
@@ -125,6 +131,8 @@ async function openLocalStore(input) {
|
|
|
125
131
|
} else if (event.type === "m.room.name") channel.name = event.content.name;
|
|
126
132
|
else if (event.type === "m.room.member") {
|
|
127
133
|
const user = event.content.user;
|
|
134
|
+
const previous = channel.lastMember.get(user)?.event.content;
|
|
135
|
+
if (previous) channel.previousMembers.set(event.eventId, previous);
|
|
128
136
|
const firstSeq = channel.lastMember.get(user)?.firstSeq ?? event.seq;
|
|
129
137
|
channel.lastMember.set(user, { event, firstSeq });
|
|
130
138
|
}
|
|
@@ -379,7 +387,11 @@ async function openLocalStore(input) {
|
|
|
379
387
|
if (events[mid].seq <= after) lo = mid + 1;
|
|
380
388
|
else hi = mid;
|
|
381
389
|
}
|
|
382
|
-
|
|
390
|
+
const channel = channels.get(roomId);
|
|
391
|
+
return structuredClone(events.slice(lo, lo + Math.max(0, limit)).map((event) => {
|
|
392
|
+
const previousContent = channel?.previousMembers.get(event.eventId);
|
|
393
|
+
return previousContent ? { ...event, previousContent } : event;
|
|
394
|
+
}));
|
|
383
395
|
},
|
|
384
396
|
waitForEvent: (roomId, after, timeoutMs, signal) => {
|
|
385
397
|
const channel = channels.get(roomId);
|
|
@@ -389,7 +401,17 @@ async function openLocalStore(input) {
|
|
|
389
401
|
const channel = channels.get(roomId);
|
|
390
402
|
const cutoff = before !== void 0 ? channel?.byEventId.get(before)?.seq : Infinity;
|
|
391
403
|
if (!channel || cutoff === void 0 || limit <= 0) return { events: [] };
|
|
392
|
-
const
|
|
404
|
+
const previousMembers = /* @__PURE__ */ new Map();
|
|
405
|
+
const candidates = [];
|
|
406
|
+
for (const event of channel.events) {
|
|
407
|
+
if (event.seq >= cutoff) break;
|
|
408
|
+
if (event.type === "m.room.member") {
|
|
409
|
+
const user = event.content.user;
|
|
410
|
+
const content = memberRenameContent(event.content, previousMembers.get(user));
|
|
411
|
+
previousMembers.set(user, event.content);
|
|
412
|
+
if (content) candidates.push({ ...event, type: "com.khala.event.v1", content });
|
|
413
|
+
} else if (event.type === "m.room.message" || event.type === "com.khala.event.v1") candidates.push(event);
|
|
414
|
+
}
|
|
393
415
|
const events = candidates.slice(-limit);
|
|
394
416
|
return { events: structuredClone(events), ...candidates.length > events.length ? { nextBefore: events[0].eventId } : {} };
|
|
395
417
|
},
|
|
@@ -421,11 +443,20 @@ async function openLocalStore(input) {
|
|
|
421
443
|
}
|
|
422
444
|
return null;
|
|
423
445
|
}),
|
|
424
|
-
|
|
446
|
+
memberForSession: (roomId, sessionKey) => {
|
|
447
|
+
const channel = channels.get(roomId);
|
|
448
|
+
if (!channel) return void 0;
|
|
449
|
+
const userId = Object.entries(channel.secrets.members).find(([, secret]) => secret.sessionKey === sessionKey)?.[0];
|
|
450
|
+
return userId ? member(channel, userId) : void 0;
|
|
451
|
+
},
|
|
452
|
+
setMemberToken: (roomId, userId, tokenSha256, sessionKey) => enqueue(async () => {
|
|
425
453
|
const channel = requireChannel(roomId);
|
|
426
454
|
const next = { ...channel.secrets.members };
|
|
427
455
|
if (tokenSha256 === null) delete next[userId];
|
|
428
|
-
else if (userId !== LOCAL_OWNER_USER_ID)
|
|
456
|
+
else if (userId !== LOCAL_OWNER_USER_ID) {
|
|
457
|
+
const identity = sessionKey ?? channel.secrets.members[userId]?.sessionKey;
|
|
458
|
+
next[userId] = { tokenSha256, ...identity ? { sessionKey: identity } : {} };
|
|
459
|
+
}
|
|
429
460
|
await saveSecrets(channel, { ...channel.secrets, members: next });
|
|
430
461
|
}),
|
|
431
462
|
agentForToken: (token) => {
|
|
@@ -778,10 +809,12 @@ function agentJoinRoutes() {
|
|
|
778
809
|
prune(ctx);
|
|
779
810
|
if (typeof req.body !== "object" || req.body === null || Array.isArray(req.body)) return fail(400, "invalid_link");
|
|
780
811
|
const body = req.body;
|
|
781
|
-
if (!Object.hasOwn(body, "link") || Object.keys(body).some((key) => !["link", "harness", "label"].includes(key))) return fail(400, "invalid_link");
|
|
812
|
+
if (!Object.hasOwn(body, "link") || Object.keys(body).some((key) => !["link", "harness", "label", "sessionId", "rejoinSecret"].includes(key))) return fail(400, "invalid_link");
|
|
782
813
|
if (typeof body.link !== "string" || !parseChannelLink(body.link)) return fail(400, "invalid_link");
|
|
814
|
+
if (Object.hasOwn(body, "sessionId") && !validAgentSessionId(body.sessionId)) return fail(400, "invalid_link");
|
|
815
|
+
if (Object.hasOwn(body, "rejoinSecret") && !validAgentRejoinSecret(body.rejoinSecret)) return fail(400, "invalid_link");
|
|
783
816
|
const harness = body.harness;
|
|
784
|
-
if (harness
|
|
817
|
+
if (!HARNESSES.includes(harness)) return fail(400, "invalid_harness");
|
|
785
818
|
const url = new URL(body.link);
|
|
786
819
|
if (url.origin !== ctx.origin && url.origin !== ctx.origin.replace("://127.0.0.1:", "://localhost:")) return fail(404, "link_unavailable");
|
|
787
820
|
const token = url.pathname.slice("/join/".length);
|
|
@@ -792,22 +825,32 @@ function agentJoinRoutes() {
|
|
|
792
825
|
const link = await ctx.store.consumeLink(token);
|
|
793
826
|
if (!link || !ctx.store.hasChannel(link.roomId)) return fail(404, "link_unavailable");
|
|
794
827
|
const roomId = link.roomId;
|
|
828
|
+
const sessionKey = body.sessionId === void 0 || body.rejoinSecret === void 0 ? void 0 : sha256hex(JSON.stringify([harness, body.sessionId, body.rejoinSecret]));
|
|
829
|
+
const previous = sessionKey ? ctx.store.memberForSession(roomId, sessionKey) : void 0;
|
|
795
830
|
const taken = new Set(ctx.store.members(roomId).map((member) => member.displayName.toLowerCase()));
|
|
796
831
|
let n = 1;
|
|
797
832
|
while (taken.has(defaultAgentName(username, harness, n).toLowerCase())) n++;
|
|
798
833
|
const checked = checkName(defaultAgentName(username, harness, n), "agent");
|
|
799
834
|
if (!checked.ok) return fail(503, "unavailable");
|
|
800
|
-
let userId = newLocalAgentUserId(ctx.random(4));
|
|
801
|
-
for (let attempt = 0; attempt < 5 && ctx.store.channelOfMember(userId) !== void 0; attempt++) userId = newLocalAgentUserId(ctx.random(4));
|
|
802
|
-
if (ctx.store.channelOfMember(userId) !== void 0) return fail(503, "unavailable");
|
|
835
|
+
let userId = previous?.userId ?? newLocalAgentUserId(ctx.random(4));
|
|
836
|
+
for (let attempt = 0; attempt < 5 && !previous && ctx.store.channelOfMember(userId) !== void 0; attempt++) userId = newLocalAgentUserId(ctx.random(4));
|
|
837
|
+
if (!previous && ctx.store.channelOfMember(userId) !== void 0) return fail(503, "unavailable");
|
|
803
838
|
const deviceId = "KH_LOCAL_" + userId.slice("@agent-".length, -":local".length);
|
|
804
839
|
const accessToken = b64(ctx.random(LOCAL_TOKEN_BYTES));
|
|
805
840
|
await ctx.store.append(roomId, {
|
|
806
841
|
type: "m.room.member",
|
|
807
842
|
sender: LOCAL_OWNER_USER_ID,
|
|
808
|
-
content: {
|
|
843
|
+
content: {
|
|
844
|
+
user: userId,
|
|
845
|
+
membership: "invite",
|
|
846
|
+
displayname: previous?.displayName ?? checked.name,
|
|
847
|
+
kind: "agent",
|
|
848
|
+
harness,
|
|
849
|
+
invitedBy: LOCAL_OWNER_USER_ID,
|
|
850
|
+
...previous ? { "com.khala.listening_mode": previous.listeningMode ?? "sync" } : {}
|
|
851
|
+
}
|
|
809
852
|
});
|
|
810
|
-
await ctx.store.setMemberToken(roomId, userId, sha256hex(accessToken));
|
|
853
|
+
await ctx.store.setMemberToken(roomId, userId, sha256hex(accessToken), sessionKey);
|
|
811
854
|
const joinId = b64(ctx.random(16)), pollSecret = b64(ctx.random(LOCAL_TOKEN_BYTES));
|
|
812
855
|
const expiresAt = ctx.now() + LOCAL_LINK_TTL_MS;
|
|
813
856
|
const credentials = { homeserver: ctx.origin, userId, accessToken, deviceId, roomId, transport: "local" };
|
|
@@ -886,6 +929,7 @@ function memberEvents(store, roomId, userId) {
|
|
|
886
929
|
let current;
|
|
887
930
|
let joined;
|
|
888
931
|
let announced = false;
|
|
932
|
+
let joins = 0;
|
|
889
933
|
for (; ; ) {
|
|
890
934
|
const page = store.eventsAfter(roomId, after, 200);
|
|
891
935
|
for (const event of page) {
|
|
@@ -896,13 +940,14 @@ function memberEvents(store, roomId, userId) {
|
|
|
896
940
|
} else if (current?.content["membership"] !== "join") {
|
|
897
941
|
joined = event;
|
|
898
942
|
announced = false;
|
|
943
|
+
joins++;
|
|
899
944
|
}
|
|
900
945
|
current = event;
|
|
901
946
|
} else if (joined && event.type === "com.khala.event.v1" && event.sender === LOCAL_OWNER_USER_ID && event.txnId === announcementTxn(joined)) {
|
|
902
947
|
announced = true;
|
|
903
948
|
}
|
|
904
949
|
}
|
|
905
|
-
if (page.length < 200) return { current, joined, announced };
|
|
950
|
+
if (page.length < 200) return { current, joined, announced, joins };
|
|
906
951
|
after = page.at(-1).seq;
|
|
907
952
|
}
|
|
908
953
|
}
|
|
@@ -951,14 +996,18 @@ function roomRoutes() {
|
|
|
951
996
|
let event;
|
|
952
997
|
let joined;
|
|
953
998
|
let announced = false;
|
|
999
|
+
let rejoin = false;
|
|
954
1000
|
if (caller.member.membership === "join") {
|
|
955
|
-
|
|
1001
|
+
let joins;
|
|
1002
|
+
({ current: event, joined, announced, joins } = memberEvents(ctx.store, caller.roomId, caller.userId));
|
|
1003
|
+
rejoin = joins > 1;
|
|
956
1004
|
} else {
|
|
1005
|
+
rejoin = memberEvents(ctx.store, caller.roomId, caller.userId).joins > 0;
|
|
957
1006
|
event = await ctx.store.append(caller.roomId, { type: "m.room.member", sender: caller.userId, content: memberContent(caller.member) });
|
|
958
1007
|
joined = event;
|
|
959
1008
|
}
|
|
960
1009
|
if (caller.kind === "agent" && joined && !announced) {
|
|
961
|
-
const encoded = encodeChannelEvent({ kind: "member", summary: `${joined.content["displayname"]} joined`, status: "info", source: { system: "khala-local" } });
|
|
1010
|
+
const encoded = encodeChannelEvent({ kind: "member", summary: `${joined.content["displayname"]} ${rejoin ? "rejoined" : "joined"}`, status: "info", source: { system: "khala-local" } });
|
|
962
1011
|
if (encoded.ok) await ctx.store.append(caller.roomId, {
|
|
963
1012
|
type: "com.khala.event.v1",
|
|
964
1013
|
sender: LOCAL_OWNER_USER_ID,
|
|
@@ -984,7 +1033,12 @@ function roomRoutes() {
|
|
|
984
1033
|
if ("status" in current) return current;
|
|
985
1034
|
events = ctx.store.eventsAfter(caller.roomId, after, 200);
|
|
986
1035
|
}
|
|
987
|
-
|
|
1036
|
+
const delivered = req.query.get("prev") === "1" ? events : events.map((event) => {
|
|
1037
|
+
const legacy = { ...event };
|
|
1038
|
+
delete legacy.previousContent;
|
|
1039
|
+
return legacy;
|
|
1040
|
+
});
|
|
1041
|
+
return { status: 200, json: { events: delivered, next: events.at(-1)?.seq ?? after } };
|
|
988
1042
|
}),
|
|
989
1043
|
route("GET", /^\/api\/local\/rooms\/([^/]+)\/messages$/u, "joined", async (req, _params, ctx, caller) => {
|
|
990
1044
|
const before = req.query.get("before");
|
|
@@ -1231,14 +1285,14 @@ async function cascade(store, previous, next) {
|
|
|
1231
1285
|
const members = store.members(roomId);
|
|
1232
1286
|
const taken = new Set(members.map((m) => m.displayName.toLowerCase()));
|
|
1233
1287
|
const owner2 = members.find((m) => m.userId === LOCAL_OWNER_USER_ID);
|
|
1234
|
-
if (owner2 &&
|
|
1288
|
+
if (owner2 && previous !== next) {
|
|
1235
1289
|
try {
|
|
1236
1290
|
await store.append(roomId, {
|
|
1237
1291
|
type: "m.room.member",
|
|
1238
1292
|
sender: LOCAL_OWNER_USER_ID,
|
|
1239
1293
|
content: { user: LOCAL_OWNER_USER_ID, membership: owner2.membership, displayname: next, kind: "human" }
|
|
1240
1294
|
});
|
|
1241
|
-
taken.delete(
|
|
1295
|
+
taken.delete(previous.toLowerCase());
|
|
1242
1296
|
taken.add(next.toLowerCase());
|
|
1243
1297
|
} catch {
|
|
1244
1298
|
}
|