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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # khala-cli
2
2
 
3
- The `khala` command connects your existing Claude Code or Codex session to a
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-6GOETMWV.mjs";
7
+ } from "./chunk-AAX35R7C.mjs";
8
8
  import {
9
9
  StateError,
10
10
  ensureStateDir,
11
11
  readJson,
12
12
  stateRoot
13
- } from "./chunk-S6XASSNU.mjs";
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-GO5DD2TR.mjs";
35
+ } from "./chunk-SMCTSZYU.mjs";
36
36
  import {
37
37
  bundle
38
- } from "./chunk-HMGN52QY.mjs";
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-HMGN52QY.mjs";
5
- import "./chunk-6FNXMYOM.mjs";
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 fs from "node:fs/promises";
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 fs.readFile(file, "utf8"), mode: (await fs.stat(file)).mode & 511 };
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 fs.mkdir(codexHome, { recursive: true });
339
+ await fs2.mkdir(codexHome, { recursive: true });
186
340
  if (!uninstall) {
187
341
  try {
188
- await fs.writeFile(hooksFile + ".khala-bak", hooks.text, { flag: "wx", mode: hooks.mode });
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 fs.writeFile(hooksFile, JSON.stringify(merged.config, null, 2) + "\n", { mode: hooks.mode });
195
- await fs.writeFile(tomlFile, nextToml.text, { mode: toml.mode });
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-ZRUF3P53.mjs";
16
+ } from "./chunk-3WTRQEVS.mjs";
17
17
  import {
18
18
  KhalaClientError
19
- } from "./chunk-6GOETMWV.mjs";
20
- import "./chunk-S6XASSNU.mjs";
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-GO5DD2TR.mjs";
25
- import "./chunk-HMGN52QY.mjs";
26
- import "./chunk-6FNXMYOM.mjs";
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-GO5DD2TR.mjs";
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/state");
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 (!["claude", "codex"].includes(harness) || !SESSION_ID_PATTERN.test(sessionId)) throw new StateError("invalid_session_id");
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 fs.rename(temporary, file);
59
- const directory = await fs.open(dir, constants.O_RDONLY);
60
- try {
61
- await directory.sync();
62
- } finally {
63
- await directory.close();
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-H2JCKPFR.mjs";
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-UYTGV6TW.mjs";
19
+ } from "./chunk-THNGKHZD.mjs";
20
20
  import {
21
21
  KHALA_AGENT_VERSION
22
- } from "./chunk-Y3TDVOGT.mjs";
22
+ } from "./chunk-ENNXMNKZ.mjs";
23
23
  import {
24
- KhalaClientError
25
- } from "./chunk-6GOETMWV.mjs";
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-S6XASSNU.mjs";
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-GO5DD2TR.mjs";
50
- import "./chunk-HMGN52QY.mjs";
51
- import "./chunk-6FNXMYOM.mjs";
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 === "claude" || value === "codex" ? value : "invalid";
278
+ return isHarness(value) ? value : "invalid";
272
279
  }
273
- if (env.KHALA_MCP_HARNESS === "claude" || env.KHALA_MCP_HARNESS === "codex") return 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 ?? (input.harness === "claude" ? "Claude" : "Codex")),
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-CTXFYQIX.mjs")).createLocalSession(creds) : (await import("./chunk-4C2TBZVA.mjs")).createAgentMatrixSession(creds);
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-HMGN52QY.mjs";
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-H2JCKPFR.mjs";
8
+ } from "./chunk-K2SZLOBE.mjs";
9
9
  import {
10
10
  StateError,
11
11
  readStatus,
12
12
  sessionFiles
13
- } from "./chunk-S6XASSNU.mjs";
14
- import "./chunk-GO5DD2TR.mjs";
15
- import "./chunk-6FNXMYOM.mjs";
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-ZRUF3P53.mjs";
23
+ } from "./chunk-3WTRQEVS.mjs";
24
24
  import {
25
25
  parseChannelLink,
26
26
  resolveLocalOwnerName
27
- } from "./chunk-UYTGV6TW.mjs";
27
+ } from "./chunk-THNGKHZD.mjs";
28
28
  import {
29
29
  KHALA_AGENT_VERSION
30
- } from "./chunk-Y3TDVOGT.mjs";
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-6GOETMWV.mjs";
38
+ } from "./chunk-AAX35R7C.mjs";
39
39
  import {
40
40
  StateError,
41
41
  ensureStateDir,
42
42
  readJson,
43
43
  writeJsonAtomic
44
- } from "./chunk-S6XASSNU.mjs";
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-GO5DD2TR.mjs";
60
+ } from "./chunk-SMCTSZYU.mjs";
60
61
  import {
61
62
  bundle
62
- } from "./chunk-HMGN52QY.mjs";
63
- import "./chunk-6FNXMYOM.mjs";
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 !== "claude" && harness !== "codex") return fail(400, "invalid_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);