khala-cli 0.3.0 → 0.4.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/README.md +15 -1
- package/dist/{chunk-ZRUF3P53.mjs → chunk-3WTRQEVS.mjs} +4 -4
- package/dist/{chunk-FGIU3YO2.mjs → chunk-3YSVED2R.mjs} +163 -9
- package/dist/{chunk-CTXFYQIX.mjs → chunk-7272YXO6.mjs} +6 -6
- package/dist/{chunk-6GOETMWV.mjs → chunk-AAX35R7C.mjs} +3 -3
- package/dist/{chunk-S6XASSNU.mjs → chunk-AYPFL33Q.mjs} +27 -9
- package/dist/{chunk-MGXWUWWP.mjs → chunk-BNT6YPFR.mjs} +21 -14
- package/dist/{chunk-Y3TDVOGT.mjs → chunk-ENNXMNKZ.mjs} +1 -1
- package/dist/{chunk-PQO6675G.mjs → chunk-G3SVNU4N.mjs} +70 -5
- package/dist/{chunk-HWKLLI7F.mjs → chunk-H2INZPFK.mjs} +10 -9
- package/dist/{chunk-525XNG6V.mjs → chunk-IKHXRKJQ.mjs} +1 -1
- package/dist/{chunk-4C2TBZVA.mjs → chunk-IVPQJSFO.mjs} +2 -2
- package/dist/{chunk-H2JCKPFR.mjs → chunk-K2SZLOBE.mjs} +2 -2
- package/dist/{chunk-HMGN52QY.mjs → chunk-NX27G3SB.mjs} +1 -1
- package/dist/{chunk-GO5DD2TR.mjs → chunk-SMCTSZYU.mjs} +4 -3
- package/dist/{chunk-UYTGV6TW.mjs → chunk-THNGKHZD.mjs} +6 -5
- package/dist/chunk-VUE2DDYY.mjs +34 -0
- package/dist/{chunk-6FNXMYOM.mjs → chunk-ZXJIFFFU.mjs} +1 -1
- package/dist/khala.mjs +9 -9
- package/dist/web/assets/local-CzaQ3d5Z.js +9 -0
- package/dist/web/assets/local-DHYvcQG0.css +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +3 -3
- package/dist/web/assets/local-DuGvSGji.css +0 -1
- package/dist/web/assets/local-hWRGqoWt.js +0 -9
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# khala-cli
|
|
2
2
|
|
|
3
|
-
The `khala` command connects your existing Claude Code or
|
|
3
|
+
The `khala` command connects your existing Claude Code, Codex or Cursor session to a
|
|
4
4
|
[Khala](https://khala.aiur.team) channel: an MCP server (`khala_join`, `khala_status`,
|
|
5
5
|
`khala_read`, `khala_send`, `khala_event`), delivery hooks that bring channel messages into
|
|
6
6
|
the session, and local channels on this computer. Requires Node 22 or newer.
|
|
@@ -26,6 +26,20 @@ This installs the CLI under `~/.local/share/khala/npm`, adds `[mcp_servers.khala
|
|
|
26
26
|
Codex and trust the three Khala hooks under **Hooks need review**. Undo with
|
|
27
27
|
`npx -y khala-cli install codex --uninstall`.
|
|
28
28
|
|
|
29
|
+
## Cursor (macOS, Linux, Windows)
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
npx -y khala-cli install cursor
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
On Windows, run it in PowerShell or Command Prompt, not WSL. This installs the CLI under
|
|
36
|
+
`~/.local/share/khala/npm` (`%LOCALAPPDATA%\khala\npm` on Windows), adds a `khala` server to
|
|
37
|
+
`~/.cursor/mcp.json` and three delivery hooks to `~/.cursor/hooks.json`, keeping everything
|
|
38
|
+
else and saving the originals as `*.khala-bak`. Restart Cursor (or toggle `khala` off and on
|
|
39
|
+
under Settings → MCP). Undo with `npx -y khala-cli install cursor --uninstall`. Cursor
|
|
40
|
+
cannot wake an idle chat; new messages arrive when a chat finishes a turn (Sync) or after a
|
|
41
|
+
tool call (Steer).
|
|
42
|
+
|
|
29
43
|
## Then
|
|
30
44
|
|
|
31
45
|
Tell your agent "Join this Khala channel: <link>" and open the confirmation link it returns.
|
|
@@ -4,13 +4,13 @@ import {
|
|
|
4
4
|
checkName,
|
|
5
5
|
readHumanColorId,
|
|
6
6
|
readHumanInitials
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-AAX35R7C.mjs";
|
|
8
8
|
import {
|
|
9
9
|
StateError,
|
|
10
10
|
ensureStateDir,
|
|
11
11
|
readJson,
|
|
12
12
|
stateRoot
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-AYPFL33Q.mjs";
|
|
14
14
|
import {
|
|
15
15
|
LISTENING_MODES,
|
|
16
16
|
M1_LABEL_MAX_BYTES,
|
|
@@ -32,10 +32,10 @@ import {
|
|
|
32
32
|
utcTimestamp,
|
|
33
33
|
utf8Length,
|
|
34
34
|
version
|
|
35
|
-
} from "./chunk-
|
|
35
|
+
} from "./chunk-SMCTSZYU.mjs";
|
|
36
36
|
import {
|
|
37
37
|
bundle
|
|
38
|
-
} from "./chunk-
|
|
38
|
+
} from "./chunk-NX27G3SB.mjs";
|
|
39
39
|
|
|
40
40
|
// ../contracts/src/m1/local.ts
|
|
41
41
|
var LOCAL_OWNER_USER_ID = "@khala_owner:local";
|
|
@@ -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-NX27G3SB.mjs";
|
|
8
|
+
import "./chunk-ZXJIFFFU.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 {
|
|
@@ -13,17 +13,17 @@ import {
|
|
|
13
13
|
decodeLocalSendResult,
|
|
14
14
|
ensureHelper,
|
|
15
15
|
localRoomPath
|
|
16
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-3WTRQEVS.mjs";
|
|
17
17
|
import {
|
|
18
18
|
KhalaClientError
|
|
19
|
-
} from "./chunk-
|
|
20
|
-
import "./chunk-
|
|
19
|
+
} from "./chunk-AAX35R7C.mjs";
|
|
20
|
+
import "./chunk-AYPFL33Q.mjs";
|
|
21
21
|
import {
|
|
22
22
|
CHANNEL_EVENT_TYPE,
|
|
23
23
|
LISTENING_MODE_COMMAND_TYPE
|
|
24
|
-
} from "./chunk-
|
|
25
|
-
import "./chunk-
|
|
26
|
-
import "./chunk-
|
|
24
|
+
} from "./chunk-SMCTSZYU.mjs";
|
|
25
|
+
import "./chunk-NX27G3SB.mjs";
|
|
26
|
+
import "./chunk-ZXJIFFFU.mjs";
|
|
27
27
|
|
|
28
28
|
// src/local/session.ts
|
|
29
29
|
import { randomUUID } from "node:crypto";
|
|
@@ -2,7 +2,7 @@ import { createRequire as __khalaCreateRequire } from 'node:module'; const requi
|
|
|
2
2
|
import {
|
|
3
3
|
fail,
|
|
4
4
|
reserved
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-SMCTSZYU.mjs";
|
|
6
6
|
|
|
7
7
|
// src/client.ts
|
|
8
8
|
var KhalaClientError = class extends Error {
|
|
@@ -58,7 +58,7 @@ var AGENT_NAME_MIN = 2;
|
|
|
58
58
|
var AGENT_NAME_MAX = 40;
|
|
59
59
|
var mentionName = /^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/u;
|
|
60
60
|
var adjacentSeparators = /[._-]{2}/u;
|
|
61
|
-
var agentSuffix = /-(?:claude|codex)(?:-\d+)?$/iu;
|
|
61
|
+
var agentSuffix = /-(?:claude|codex|cursor)(?:-\d+)?$/iu;
|
|
62
62
|
function checkName(input, kind) {
|
|
63
63
|
if (typeof input !== "string") return { ok: false, error: "invalid_characters" };
|
|
64
64
|
const name = input.trim();
|
|
@@ -68,7 +68,7 @@ function checkName(input, kind) {
|
|
|
68
68
|
if (reserved.test(name) || kind === "username" && agentSuffix.test(name)) return { ok: false, error: "reserved" };
|
|
69
69
|
return { ok: true, name };
|
|
70
70
|
}
|
|
71
|
-
var MODEL_NAMES = { claude: "Claude", codex: "Codex" };
|
|
71
|
+
var MODEL_NAMES = { claude: "Claude", codex: "Codex", cursor: "Cursor" };
|
|
72
72
|
function defaultAgentName(username, harness, n = 1) {
|
|
73
73
|
return `${username}-${MODEL_NAMES[harness]}${n === 1 ? "" : `-${n}`}`;
|
|
74
74
|
}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { createRequire as __khalaCreateRequire } from 'node:module'; const require = __khalaCreateRequire(import.meta.url);
|
|
2
|
+
import {
|
|
3
|
+
HARNESSES
|
|
4
|
+
} from "./chunk-SMCTSZYU.mjs";
|
|
2
5
|
|
|
3
6
|
// src/state.ts
|
|
4
7
|
import { constants } from "node:fs";
|
|
@@ -15,15 +18,28 @@ var StateError = class extends Error {
|
|
|
15
18
|
this.code = code;
|
|
16
19
|
}
|
|
17
20
|
};
|
|
21
|
+
var WINDOWS = process.platform === "win32";
|
|
22
|
+
async function renameWithRetry(from, to) {
|
|
23
|
+
for (let attempt = 0; ; attempt++) {
|
|
24
|
+
try {
|
|
25
|
+
await fs.rename(from, to);
|
|
26
|
+
return;
|
|
27
|
+
} catch (error) {
|
|
28
|
+
const code = error.code;
|
|
29
|
+
if (!WINDOWS || attempt >= 20 || !["EPERM", "EACCES", "EBUSY"].includes(code ?? "")) throw error;
|
|
30
|
+
await new Promise((resolve) => setTimeout(resolve, 10 + attempt * 5));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
18
34
|
function stateRoot(env = process.env) {
|
|
19
|
-
const base = env.XDG_STATE_HOME && path.isAbsolute(env.XDG_STATE_HOME) ? env.XDG_STATE_HOME : path.join(env.HOME ?? os.homedir(), ".local
|
|
35
|
+
const base = env.XDG_STATE_HOME && path.isAbsolute(env.XDG_STATE_HOME) ? env.XDG_STATE_HOME : path.join(WINDOWS ? os.homedir() : env.HOME ?? os.homedir(), ".local", "state");
|
|
20
36
|
return path.join(base, "khala");
|
|
21
37
|
}
|
|
22
38
|
function filesForDir(dir) {
|
|
23
39
|
return { dir, mode: path.join(dir, "mode.json"), join: path.join(dir, "join.json"), session: path.join(dir, "session.json"), inbox: path.join(dir, "inbox.jsonl"), cursor: path.join(dir, "cursor.json"), status: path.join(dir, "status.json") };
|
|
24
40
|
}
|
|
25
41
|
function sessionFiles(harness, sessionId, env) {
|
|
26
|
-
if (!
|
|
42
|
+
if (!HARNESSES.includes(harness) || !SESSION_ID_PATTERN.test(sessionId)) throw new StateError("invalid_session_id");
|
|
27
43
|
return filesForDir(path.join(stateRoot(env), harness, sessionId));
|
|
28
44
|
}
|
|
29
45
|
function resolveStateDir(harness, sessionId, env) {
|
|
@@ -38,7 +54,7 @@ async function ensureStateDir(dir) {
|
|
|
38
54
|
if (error.code !== "EEXIST") throw error;
|
|
39
55
|
}
|
|
40
56
|
const stat = await fs.lstat(directory);
|
|
41
|
-
if (!stat.isDirectory() || stat.isSymbolicLink() || (stat.mode & 63) !== 0 || typeof process.getuid === "function" && stat.uid !== process.getuid()) throw new StateError("unsafe_state_dir");
|
|
57
|
+
if (!stat.isDirectory() || stat.isSymbolicLink() || !WINDOWS && (stat.mode & 63) !== 0 || typeof process.getuid === "function" && stat.uid !== process.getuid()) throw new StateError("unsafe_state_dir");
|
|
42
58
|
}
|
|
43
59
|
} catch (error) {
|
|
44
60
|
if (error instanceof StateError) throw error;
|
|
@@ -55,12 +71,14 @@ async function writeJsonAtomic(file, value) {
|
|
|
55
71
|
await handle.sync();
|
|
56
72
|
await handle.close();
|
|
57
73
|
handle = void 0;
|
|
58
|
-
await
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
74
|
+
await renameWithRetry(temporary, file);
|
|
75
|
+
if (!WINDOWS) {
|
|
76
|
+
const directory = await fs.open(dir, constants.O_RDONLY);
|
|
77
|
+
try {
|
|
78
|
+
await directory.sync();
|
|
79
|
+
} finally {
|
|
80
|
+
await directory.close();
|
|
81
|
+
}
|
|
64
82
|
}
|
|
65
83
|
} catch {
|
|
66
84
|
throw new StateError("storage_failed");
|
|
@@ -9,20 +9,21 @@ import {
|
|
|
9
9
|
readListeningMode,
|
|
10
10
|
toEventInboxEntry,
|
|
11
11
|
unreadCount
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-K2SZLOBE.mjs";
|
|
13
13
|
import {
|
|
14
14
|
hostedUsernameFromAgentName,
|
|
15
15
|
pollJoin,
|
|
16
16
|
reportReady,
|
|
17
17
|
requestJoin,
|
|
18
18
|
saveHostedUsername
|
|
19
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-THNGKHZD.mjs";
|
|
20
20
|
import {
|
|
21
21
|
KHALA_AGENT_VERSION
|
|
22
|
-
} from "./chunk-
|
|
22
|
+
} from "./chunk-ENNXMNKZ.mjs";
|
|
23
23
|
import {
|
|
24
|
-
KhalaClientError
|
|
25
|
-
|
|
24
|
+
KhalaClientError,
|
|
25
|
+
MODEL_NAMES
|
|
26
|
+
} from "./chunk-AAX35R7C.mjs";
|
|
26
27
|
import {
|
|
27
28
|
SESSION_ID_PATTERN,
|
|
28
29
|
StateError,
|
|
@@ -35,9 +36,10 @@ import {
|
|
|
35
36
|
resolveStateDir,
|
|
36
37
|
sessionFiles,
|
|
37
38
|
writeStateFile
|
|
38
|
-
} from "./chunk-
|
|
39
|
+
} from "./chunk-AYPFL33Q.mjs";
|
|
39
40
|
import {
|
|
40
41
|
CHANNEL_EVENT_TYPE,
|
|
42
|
+
HARNESSES,
|
|
41
43
|
decodeListeningModeCommand,
|
|
42
44
|
displayText,
|
|
43
45
|
encodeChannelEvent,
|
|
@@ -46,9 +48,13 @@ import {
|
|
|
46
48
|
label,
|
|
47
49
|
statusFor,
|
|
48
50
|
utf8Length
|
|
49
|
-
} from "./chunk-
|
|
50
|
-
import
|
|
51
|
-
|
|
51
|
+
} from "./chunk-SMCTSZYU.mjs";
|
|
52
|
+
import {
|
|
53
|
+
CURSOR_WORKSPACE_ENV,
|
|
54
|
+
cursorSessionId
|
|
55
|
+
} from "./chunk-VUE2DDYY.mjs";
|
|
56
|
+
import "./chunk-NX27G3SB.mjs";
|
|
57
|
+
import "./chunk-ZXJIFFFU.mjs";
|
|
52
58
|
|
|
53
59
|
// src/mcp/main.ts
|
|
54
60
|
import { Console } from "node:console";
|
|
@@ -264,17 +270,18 @@ function nextChunk(iterator, signal) {
|
|
|
264
270
|
}
|
|
265
271
|
|
|
266
272
|
// src/mcp/session-id.ts
|
|
273
|
+
var isHarness = (value) => HARNESSES.includes(value);
|
|
267
274
|
function resolveHarness(argv, env) {
|
|
268
275
|
const index = argv.indexOf("--harness");
|
|
269
276
|
if (index !== -1) {
|
|
270
277
|
const value = argv[index + 1];
|
|
271
|
-
return value
|
|
278
|
+
return isHarness(value) ? value : "invalid";
|
|
272
279
|
}
|
|
273
|
-
if (env.KHALA_MCP_HARNESS
|
|
280
|
+
if (isHarness(env.KHALA_MCP_HARNESS)) return env.KHALA_MCP_HARNESS;
|
|
274
281
|
return env.CLAUDE_CODE_SESSION_ID !== void 0 ? "claude" : "codex";
|
|
275
282
|
}
|
|
276
283
|
function resolveSessionId(harness, meta, env) {
|
|
277
|
-
const value = harness === "claude" ? env.CLAUDE_CODE_SESSION_ID : typeof meta?.threadId === "string" ? meta.threadId : env.CODEX_THREAD_ID;
|
|
284
|
+
const value = harness === "claude" ? env.CLAUDE_CODE_SESSION_ID : harness === "cursor" ? cursorSessionId(env[CURSOR_WORKSPACE_ENV]) : typeof meta?.threadId === "string" ? meta.threadId : env.CODEX_THREAD_ID;
|
|
278
285
|
return typeof value === "string" && value.match(/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/)?.[0] === value ? value : null;
|
|
279
286
|
}
|
|
280
287
|
|
|
@@ -534,7 +541,7 @@ function createKhalaTools(input) {
|
|
|
534
541
|
} },
|
|
535
542
|
["link"],
|
|
536
543
|
(args) => typeof args.link === "string" && (!Object.hasOwn(args, "label") || typeof args.label === "string" && args.label.trim().length > 0 && [...args.label].length <= 40),
|
|
537
|
-
(client, args) => client.join(args.link, args.label ??
|
|
544
|
+
(client, args) => client.join(args.link, args.label ?? MODEL_NAMES[input.harness]),
|
|
538
545
|
(result) => {
|
|
539
546
|
const joined = result;
|
|
540
547
|
if (joined.state === "connected") return `Connected to ${joined.channelName}.`;
|
|
@@ -598,7 +605,7 @@ function createKhalaTools(input) {
|
|
|
598
605
|
import { createHash } from "node:crypto";
|
|
599
606
|
|
|
600
607
|
// src/transport.ts
|
|
601
|
-
var startChannelSession = async (creds) => creds.transport === "local" ? (await import("./chunk-
|
|
608
|
+
var startChannelSession = async (creds) => creds.transport === "local" ? (await import("./chunk-7272YXO6.mjs")).createLocalSession(creds) : (await import("./chunk-IVPQJSFO.mjs")).createAgentMatrixSession(creds);
|
|
602
609
|
|
|
603
610
|
// src/sender.ts
|
|
604
611
|
function localpart(userId) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createRequire as __khalaCreateRequire } from 'node:module'; const require = __khalaCreateRequire(import.meta.url);
|
|
2
2
|
import {
|
|
3
3
|
bundle
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-NX27G3SB.mjs";
|
|
5
5
|
|
|
6
6
|
// src/version.ts
|
|
7
7
|
var KHALA_AGENT_VERSION = bundle?.version ?? "0.0.0";
|
|
@@ -5,14 +5,18 @@ import {
|
|
|
5
5
|
readListeningMode,
|
|
6
6
|
unread,
|
|
7
7
|
writeActivity
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-K2SZLOBE.mjs";
|
|
9
9
|
import {
|
|
10
10
|
StateError,
|
|
11
11
|
readStatus,
|
|
12
12
|
sessionFiles
|
|
13
|
-
} from "./chunk-
|
|
14
|
-
import "./chunk-
|
|
15
|
-
import
|
|
13
|
+
} from "./chunk-AYPFL33Q.mjs";
|
|
14
|
+
import "./chunk-SMCTSZYU.mjs";
|
|
15
|
+
import {
|
|
16
|
+
CURSOR_DEFAULT_SESSION,
|
|
17
|
+
cursorSessionId
|
|
18
|
+
} from "./chunk-VUE2DDYY.mjs";
|
|
19
|
+
import "./chunk-ZXJIFFFU.mjs";
|
|
16
20
|
|
|
17
21
|
// hooks/deliver.ts
|
|
18
22
|
import * as fs from "node:fs/promises";
|
|
@@ -79,10 +83,11 @@ async function deliver(stdin, argv, io = {
|
|
|
79
83
|
now: () => /* @__PURE__ */ new Date()
|
|
80
84
|
}) {
|
|
81
85
|
const harness = argv[1];
|
|
82
|
-
if (argv.length !== 2 || argv[0] !== "--harness" || harness !== "claude" && harness !== "codex") {
|
|
86
|
+
if (argv.length !== 2 || argv[0] !== "--harness" || harness !== "claude" && harness !== "codex" && harness !== "cursor") {
|
|
83
87
|
diagnostic(io, "invalid_harness");
|
|
84
88
|
return 0;
|
|
85
89
|
}
|
|
90
|
+
if (harness === "cursor") return deliverCursor(stdin, io);
|
|
86
91
|
let input;
|
|
87
92
|
try {
|
|
88
93
|
input = JSON.parse(stdin);
|
|
@@ -127,9 +132,69 @@ async function deliver(stdin, argv, io = {
|
|
|
127
132
|
}
|
|
128
133
|
return 0;
|
|
129
134
|
}
|
|
135
|
+
var CURSOR_NOOP = { beforeSubmitPrompt: { continue: true }, postToolUse: {}, stop: {} };
|
|
136
|
+
async function cursorFiles(roots, env) {
|
|
137
|
+
const first = Array.isArray(roots) && typeof roots[0] === "string" ? roots[0] : void 0;
|
|
138
|
+
for (const id of /* @__PURE__ */ new Set([cursorSessionId(first), CURSOR_DEFAULT_SESSION])) {
|
|
139
|
+
const files = sessionFiles("cursor", id, env);
|
|
140
|
+
try {
|
|
141
|
+
if ((await fs.stat(files.dir)).isDirectory()) return files;
|
|
142
|
+
} catch {
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
async function deliverCursor(stdin, io) {
|
|
148
|
+
let event;
|
|
149
|
+
let input;
|
|
150
|
+
try {
|
|
151
|
+
input = JSON.parse(stdin.replace(/^/u, ""));
|
|
152
|
+
const name = input?.hook_event_name;
|
|
153
|
+
if (typeof name === "string" && Object.hasOwn(CURSOR_NOOP, name)) event = name;
|
|
154
|
+
} catch {
|
|
155
|
+
}
|
|
156
|
+
let output = event ? CURSOR_NOOP[event] : {};
|
|
157
|
+
if (event && input) {
|
|
158
|
+
try {
|
|
159
|
+
const files = await cursorFiles(input.workspace_roots, io.env);
|
|
160
|
+
if (files) output = await cursorOutput(files, event, input.loop_count, io) ?? output;
|
|
161
|
+
} catch (error) {
|
|
162
|
+
diagnostic(io, error instanceof StateError ? error.code : "internal_error");
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
try {
|
|
166
|
+
io.stdout.write(JSON.stringify(output) + "\n");
|
|
167
|
+
} catch {
|
|
168
|
+
}
|
|
169
|
+
return 0;
|
|
170
|
+
}
|
|
171
|
+
async function cursorOutput(files, event, loopCount, io) {
|
|
172
|
+
const mode = await readListeningMode(files);
|
|
173
|
+
if (event === "beforeSubmitPrompt") {
|
|
174
|
+
await writeActivity(files, "busy", io.now);
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
if (event === "postToolUse" && mode !== "steer") return null;
|
|
178
|
+
if (event === "stop" && (mode === "async" || typeof loopCount === "number" && loopCount > 0)) {
|
|
179
|
+
await writeActivity(files, "idle", io.now);
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
const status = await readStatus(files);
|
|
183
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
184
|
+
const { entries, cursor } = await unread(files);
|
|
185
|
+
const { frame, consumed } = selectFrame(status?.channelName, entries);
|
|
186
|
+
if (!frame || !consumed.some(isWakeEntry)) break;
|
|
187
|
+
if (await advanceCursor(files, cursor, consumed) === "conflict") continue;
|
|
188
|
+
await writeActivity(files, "busy", io.now);
|
|
189
|
+
return event === "stop" ? { followup_message: frame } : { additional_context: frame };
|
|
190
|
+
}
|
|
191
|
+
if (event === "stop") await writeActivity(files, "idle", io.now);
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
130
194
|
export {
|
|
131
195
|
run as default,
|
|
132
196
|
deliver,
|
|
197
|
+
deliverCursor,
|
|
133
198
|
renderFrame,
|
|
134
199
|
renderLine
|
|
135
200
|
};
|
|
@@ -20,14 +20,14 @@ import {
|
|
|
20
20
|
newLocalEventId,
|
|
21
21
|
newLocalRoomId,
|
|
22
22
|
readHelperFile
|
|
23
|
-
} from "./chunk-
|
|
23
|
+
} from "./chunk-3WTRQEVS.mjs";
|
|
24
24
|
import {
|
|
25
25
|
parseChannelLink,
|
|
26
26
|
resolveLocalOwnerName
|
|
27
|
-
} from "./chunk-
|
|
27
|
+
} from "./chunk-THNGKHZD.mjs";
|
|
28
28
|
import {
|
|
29
29
|
KHALA_AGENT_VERSION
|
|
30
|
-
} from "./chunk-
|
|
30
|
+
} from "./chunk-ENNXMNKZ.mjs";
|
|
31
31
|
import {
|
|
32
32
|
checkName,
|
|
33
33
|
defaultAgentName,
|
|
@@ -35,16 +35,17 @@ import {
|
|
|
35
35
|
isDefaultAgentName,
|
|
36
36
|
isHumanColorId,
|
|
37
37
|
normalizeInitials
|
|
38
|
-
} from "./chunk-
|
|
38
|
+
} from "./chunk-AAX35R7C.mjs";
|
|
39
39
|
import {
|
|
40
40
|
StateError,
|
|
41
41
|
ensureStateDir,
|
|
42
42
|
readJson,
|
|
43
43
|
writeJsonAtomic
|
|
44
|
-
} from "./chunk-
|
|
44
|
+
} from "./chunk-AYPFL33Q.mjs";
|
|
45
45
|
import {
|
|
46
46
|
CHANNEL_EVENT_TYPE,
|
|
47
47
|
DEFAULT_LISTENING_MODE,
|
|
48
|
+
HARNESSES,
|
|
48
49
|
LISTENING_MODE_COMMAND_TYPE,
|
|
49
50
|
LISTENING_MODE_MEMBER_KEY,
|
|
50
51
|
agentConfirmPagePath,
|
|
@@ -56,11 +57,11 @@ import {
|
|
|
56
57
|
memberListeningMode,
|
|
57
58
|
object,
|
|
58
59
|
utf8Length
|
|
59
|
-
} from "./chunk-
|
|
60
|
+
} from "./chunk-SMCTSZYU.mjs";
|
|
60
61
|
import {
|
|
61
62
|
bundle
|
|
62
|
-
} from "./chunk-
|
|
63
|
-
import "./chunk-
|
|
63
|
+
} from "./chunk-NX27G3SB.mjs";
|
|
64
|
+
import "./chunk-ZXJIFFFU.mjs";
|
|
64
65
|
|
|
65
66
|
// src/local/cli.ts
|
|
66
67
|
import { setTimeout as sleepDefault } from "node:timers/promises";
|
|
@@ -781,7 +782,7 @@ function agentJoinRoutes() {
|
|
|
781
782
|
if (!Object.hasOwn(body, "link") || Object.keys(body).some((key) => !["link", "harness", "label"].includes(key))) return fail(400, "invalid_link");
|
|
782
783
|
if (typeof body.link !== "string" || !parseChannelLink(body.link)) return fail(400, "invalid_link");
|
|
783
784
|
const harness = body.harness;
|
|
784
|
-
if (harness
|
|
785
|
+
if (!HARNESSES.includes(harness)) return fail(400, "invalid_harness");
|
|
785
786
|
const url = new URL(body.link);
|
|
786
787
|
if (url.origin !== ctx.origin && url.origin !== ctx.origin.replace("://127.0.0.1:", "://localhost:")) return fail(404, "link_unavailable");
|
|
787
788
|
const token = url.pathname.slice("/join/".length);
|