@wrongstack/acp 0.287.0 → 0.289.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/dist/index.js CHANGED
@@ -130,7 +130,7 @@ var ClientTransport = class {
130
130
  }
131
131
  async start() {
132
132
  if (this.child) return;
133
- const [{ spawn: spawn3 }, { buildChildEnv }, os] = await Promise.all([
133
+ const [{ spawn: spawn3 }, { buildChildEnv: buildChildEnv2 }, os] = await Promise.all([
134
134
  import("node:child_process"),
135
135
  import("@wrongstack/core"),
136
136
  import("node:os")
@@ -147,7 +147,7 @@ var ClientTransport = class {
147
147
  const childArgs = this.opts.args ?? [];
148
148
  const shim = process.platform === "win32" ? buildWin32CmdShimInvocation(this.opts.command, childArgs) : null;
149
149
  this.child = spawn3(shim?.command ?? this.opts.command, shim?.args ?? childArgs, {
150
- env: { ...buildChildEnv(), ...this.opts.env },
150
+ env: { ...buildChildEnv2(), ...this.opts.env },
151
151
  cwd: spawnCwd,
152
152
  stdio: ["pipe", "pipe", "pipe"],
153
153
  windowsHide: true,
@@ -1132,7 +1132,21 @@ var WrongStackACPServer = class {
1132
1132
  async startHttp(port) {
1133
1133
  const host = this.options.host ?? "127.0.0.1";
1134
1134
  const handler = this.handler;
1135
+ const authToken = this.options.authToken;
1136
+ let httpChain = Promise.resolve();
1135
1137
  this.httpServer = createServer(async (req, res) => {
1138
+ if (authToken) {
1139
+ const url = new URL(req.url ?? "/", `http://${host}:${port}`);
1140
+ const queryToken = url.searchParams.get("token");
1141
+ const authHeader = req.headers["authorization"];
1142
+ const bearerToken = Array.isArray(authHeader) ? authHeader[0]?.replace(/^Bearer\s+/i, "") : authHeader?.replace(/^Bearer\s+/i, "");
1143
+ const supplied = queryToken ?? bearerToken ?? "";
1144
+ if (supplied !== authToken) {
1145
+ res.writeHead(401, { "Content-Type": "application/json" });
1146
+ res.end(JSON.stringify({ error: { code: -32001, message: "Unauthorized" } }));
1147
+ return;
1148
+ }
1149
+ }
1136
1150
  const selfOrigin = `http://${host}:${port}`;
1137
1151
  const reqOrigin = Array.isArray(req.headers.origin) ? req.headers.origin[0] : req.headers.origin;
1138
1152
  if (reqOrigin && reqOrigin !== selfOrigin) {
@@ -1142,7 +1156,7 @@ var WrongStackACPServer = class {
1142
1156
  }
1143
1157
  if (reqOrigin) res.setHeader("Access-Control-Allow-Origin", reqOrigin);
1144
1158
  res.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
1145
- res.setHeader("Access-Control-Allow-Headers", "Content-Type, Mcp-Session-Id");
1159
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type, Mcp-Session-Id, Authorization");
1146
1160
  if (req.method === "OPTIONS") {
1147
1161
  res.writeHead(204);
1148
1162
  res.end();
@@ -1153,10 +1167,23 @@ var WrongStackACPServer = class {
1153
1167
  res.end(JSON.stringify({ error: "method not allowed" }));
1154
1168
  return;
1155
1169
  }
1170
+ const MAX_HTTP_BODY = 10 * 1024 * 1024;
1156
1171
  let body = "";
1172
+ let bodyBytes = 0;
1173
+ let tooLarge = false;
1157
1174
  for await (const chunk of req) {
1175
+ bodyBytes += chunk.length;
1176
+ if (bodyBytes > MAX_HTTP_BODY) {
1177
+ tooLarge = true;
1178
+ break;
1179
+ }
1158
1180
  body += chunk;
1159
1181
  }
1182
+ if (tooLarge) {
1183
+ res.writeHead(413, { "Content-Type": "application/json" });
1184
+ res.end(JSON.stringify({ error: { code: -32700, message: "Request body too large" } }));
1185
+ return;
1186
+ }
1160
1187
  let msg;
1161
1188
  try {
1162
1189
  msg = JSON.parse(body);
@@ -1165,26 +1192,43 @@ var WrongStackACPServer = class {
1165
1192
  res.end(JSON.stringify({ error: { code: -32700, message: "Parse error" } }));
1166
1193
  return;
1167
1194
  }
1168
- const notifications = [];
1169
- let response = null;
1170
- const originalSend = this.transport.send.bind(this.transport);
1171
- this.transport.send = async (m) => {
1172
- if (m.id !== void 0 && (m.result !== void 0 || m.error !== void 0)) {
1173
- response = m;
1174
- } else if (m.method === "session/update") {
1175
- notifications.push(m.params);
1176
- } else {
1177
- notifications.push(m);
1195
+ const isNotification = typeof msg === "object" && msg !== null && msg.id === void 0 && typeof msg.method === "string";
1196
+ if (isNotification) {
1197
+ try {
1198
+ await handler.handleMessage(msg);
1199
+ } catch {
1178
1200
  }
1179
- };
1201
+ res.writeHead(200, { "Content-Type": "application/json" });
1202
+ res.end(JSON.stringify({ notifications: [] }));
1203
+ return;
1204
+ }
1205
+ const requestPromise = httpChain.then(async () => {
1206
+ const notifications = [];
1207
+ let response = null;
1208
+ const originalSend = this.transport.send.bind(this.transport);
1209
+ this.transport.send = async (m) => {
1210
+ if (m.id !== void 0 && (m.result !== void 0 || m.error !== void 0)) {
1211
+ response = m;
1212
+ } else if (m.method === "session/update") {
1213
+ notifications.push(m.params);
1214
+ } else {
1215
+ notifications.push(m);
1216
+ }
1217
+ };
1218
+ try {
1219
+ await handler.handleMessage(msg);
1220
+ } finally {
1221
+ this.transport.send = originalSend;
1222
+ }
1223
+ res.writeHead(200, { "Content-Type": "application/json" });
1224
+ const responseBody = response !== null ? { ...response, notifications } : { notifications };
1225
+ res.end(JSON.stringify(responseBody));
1226
+ });
1227
+ httpChain = requestPromise.catch(() => void 0);
1180
1228
  try {
1181
- await handler.handleMessage(msg);
1182
- } finally {
1183
- this.transport.send = originalSend;
1229
+ await requestPromise;
1230
+ } catch {
1184
1231
  }
1185
- res.writeHead(200, { "Content-Type": "application/json" });
1186
- const responseBody = response !== null ? { ...response, notifications } : { notifications };
1187
- res.end(JSON.stringify(responseBody));
1188
1232
  });
1189
1233
  return new Promise((resolve3) => {
1190
1234
  this.httpServer.listen(port, host, () => {
@@ -1396,8 +1440,12 @@ var ToolTranslator = class {
1396
1440
  };
1397
1441
 
1398
1442
  // src/client/file-server.ts
1443
+ import { randomBytes } from "node:crypto";
1444
+ import { realpathSync } from "node:fs";
1399
1445
  import * as fsp from "node:fs/promises";
1400
1446
  import * as path from "node:path";
1447
+ var DEFAULT_MAX_READ_BYTES = 5 * 1024 * 1024;
1448
+ var DEFAULT_MAX_WRITE_BYTES = 5 * 1024 * 1024;
1401
1449
  var FsError = class extends Error {
1402
1450
  code;
1403
1451
  path;
@@ -1410,23 +1458,40 @@ var FsError = class extends Error {
1410
1458
  };
1411
1459
  var FileServer = class {
1412
1460
  root;
1461
+ realRoot;
1413
1462
  timeoutMs;
1463
+ maxReadBytes;
1464
+ maxWriteBytes;
1414
1465
  constructor(opts) {
1415
1466
  this.root = path.resolve(opts.projectRoot);
1467
+ this.realRoot = safeRealpathSync(this.root);
1416
1468
  this.timeoutMs = opts.timeoutMs ?? 3e4;
1469
+ this.maxReadBytes = opts.maxReadBytes ?? DEFAULT_MAX_READ_BYTES;
1470
+ this.maxWriteBytes = opts.maxWriteBytes ?? DEFAULT_MAX_WRITE_BYTES;
1417
1471
  }
1418
1472
  /** Read a text file. Returns the content as a string. */
1419
1473
  async readTextFile(params) {
1420
- const safe = this.resolveInside(params.path);
1474
+ const safe = await this.resolveInside(params.path);
1421
1475
  const controller = new AbortController();
1422
1476
  const timer = setTimeout(() => controller.abort(), this.timeoutMs);
1423
1477
  try {
1478
+ const stat2 = await fsp.stat(safe).catch((err) => {
1479
+ throw mapFsError(err, safe);
1480
+ });
1481
+ if (stat2.size > this.maxReadBytes) {
1482
+ throw new FsError(
1483
+ "TOO_LARGE",
1484
+ safe,
1485
+ `file is ${stat2.size} bytes, max read is ${this.maxReadBytes} bytes`
1486
+ );
1487
+ }
1424
1488
  const content = await fsp.readFile(safe, {
1425
1489
  encoding: "utf8",
1426
1490
  signal: controller.signal
1427
1491
  });
1428
1492
  return { content };
1429
1493
  } catch (err) {
1494
+ if (err instanceof FsError) throw err;
1430
1495
  if (controller.signal.aborted) {
1431
1496
  throw new FsError("TIMEOUT", safe, `readTextFile timed out after ${this.timeoutMs}ms`);
1432
1497
  }
@@ -1437,17 +1502,31 @@ var FileServer = class {
1437
1502
  }
1438
1503
  /** Write a text file. Atomic via write-then-rename. */
1439
1504
  async writeTextFile(params) {
1440
- const safe = this.resolveInside(params.path);
1505
+ const byteLength = Buffer.byteLength(params.content, "utf8");
1506
+ if (byteLength > this.maxWriteBytes) {
1507
+ throw new FsError(
1508
+ "TOO_LARGE",
1509
+ params.path,
1510
+ `content is ${byteLength} bytes, max write is ${this.maxWriteBytes} bytes`
1511
+ );
1512
+ }
1513
+ const safe = await this.resolveInside(params.path);
1441
1514
  const controller = new AbortController();
1442
1515
  const timer = setTimeout(() => controller.abort(), this.timeoutMs);
1443
- const tmp = `${safe}.${randomHex(4)}.tmp`;
1516
+ const tmp = `${safe}.${randomBytes(6).toString("hex")}.tmp`;
1444
1517
  try {
1445
1518
  await fsp.writeFile(tmp, params.content, {
1446
1519
  encoding: "utf8",
1447
1520
  signal: controller.signal
1448
1521
  });
1522
+ await this.assertRealInside(tmp);
1523
+ await this.assertRealInside(path.dirname(safe));
1449
1524
  await fsp.rename(tmp, safe);
1450
1525
  } catch (err) {
1526
+ if (err instanceof FsError) {
1527
+ await fsp.unlink(tmp).catch(() => void 0);
1528
+ throw err;
1529
+ }
1451
1530
  try {
1452
1531
  await fsp.unlink(tmp);
1453
1532
  } catch {
@@ -1461,12 +1540,14 @@ var FileServer = class {
1461
1540
  }
1462
1541
  }
1463
1542
  /**
1464
- * Resolve a path; throw `FsError('OUTSIDE_ROOT')` if the result is
1465
- * not under the project root. Symlinks are not followed here we
1466
- * operate on the textual path. A future hardening pass can
1467
- * `fs.realpath` each access to catch symlink escapes.
1543
+ * Resolve a path and verify it is inside the project root by realpath.
1544
+ * Rejects with `FsError` if the textual path, the resolved path, or the
1545
+ * real (symlink-resolved) path escapes the project root.
1546
+ *
1547
+ * For files that don't exist yet (e.g. a write to a new file), the
1548
+ * nearest existing ancestor directory is realpath-checked instead.
1468
1549
  */
1469
- resolveInside(p) {
1550
+ async resolveInside(p) {
1470
1551
  if (typeof p !== "string" || p.length === 0) {
1471
1552
  throw new FsError("INVALID_PATH", p, "path is empty or not a string");
1472
1553
  }
@@ -1478,8 +1559,38 @@ var FileServer = class {
1478
1559
  if (resolved !== this.root && !resolved.startsWith(rootWithSep)) {
1479
1560
  throw new FsError("OUTSIDE_ROOT", resolved, "path is outside the project root");
1480
1561
  }
1562
+ await this.assertRealInside(resolved);
1481
1563
  return resolved;
1482
1564
  }
1565
+ /**
1566
+ * Resolve `resolvedPath` through `fs.realpath` and verify the result is
1567
+ * inside `realRoot`. For non-existent paths (new files), walk up to the
1568
+ * nearest existing ancestor and check that instead.
1569
+ */
1570
+ async assertRealInside(resolvedPath) {
1571
+ let probe = resolvedPath;
1572
+ for (; ; ) {
1573
+ let real;
1574
+ try {
1575
+ real = await fsp.realpath(probe);
1576
+ } catch (err) {
1577
+ const code = err.code;
1578
+ if (code === "ENOENT") {
1579
+ const parent = path.dirname(probe);
1580
+ if (parent === probe) return;
1581
+ probe = parent;
1582
+ continue;
1583
+ }
1584
+ throw mapFsError(err, resolvedPath);
1585
+ }
1586
+ if (real === this.realRoot || real.startsWith(this.realRoot + path.sep)) return;
1587
+ throw new FsError(
1588
+ "OUTSIDE_ROOT",
1589
+ resolvedPath,
1590
+ "path resolves through a symlink outside the project root"
1591
+ );
1592
+ }
1593
+ }
1483
1594
  };
1484
1595
  function mapFsError(err, p) {
1485
1596
  const code = err?.code;
@@ -1490,12 +1601,12 @@ function mapFsError(err, p) {
1490
1601
  const msg = err instanceof Error ? err.message : String(err);
1491
1602
  return new FsError("INVALID_PATH", p, msg);
1492
1603
  }
1493
- function randomHex(bytes) {
1494
- let out = "";
1495
- for (let i = 0; i < bytes * 2; i++) {
1496
- out += Math.floor(Math.random() * 16).toString(16);
1604
+ function safeRealpathSync(p) {
1605
+ try {
1606
+ return realpathSync(p);
1607
+ } catch {
1608
+ return p;
1497
1609
  }
1498
- return out;
1499
1610
  }
1500
1611
 
1501
1612
  // src/client/permission.ts
@@ -1544,17 +1655,21 @@ function makePermissionPolicy(decide) {
1544
1655
 
1545
1656
  // src/client/terminal-server.ts
1546
1657
  import { spawn } from "node:child_process";
1658
+ import { realpathSync as realpathSync2 } from "node:fs";
1547
1659
  import * as path2 from "node:path";
1660
+ import { buildChildEnv } from "@wrongstack/core/utils";
1548
1661
  var TerminalServer = class {
1549
1662
  terminals = /* @__PURE__ */ new Map();
1550
1663
  projectRoot;
1551
1664
  commandTimeoutMs;
1552
1665
  outputByteLimit;
1666
+ maxOutputByteLimit;
1553
1667
  nextId = 1;
1554
1668
  constructor(opts) {
1555
1669
  this.projectRoot = path2.resolve(opts.projectRoot);
1556
1670
  this.commandTimeoutMs = opts.commandTimeoutMs ?? 5 * 6e4;
1557
1671
  this.outputByteLimit = opts.outputByteLimit ?? 1024 * 1024;
1672
+ this.maxOutputByteLimit = opts.maxOutputByteLimit ?? 16 * 1024 * 1024;
1558
1673
  if (opts.signal) {
1559
1674
  opts.signal.addEventListener("abort", () => this.releaseAll());
1560
1675
  }
@@ -1608,12 +1723,15 @@ var TerminalServer = class {
1608
1723
  state.exitStatus = exitStatus;
1609
1724
  state.output += `[spawn error] ${err.message}
1610
1725
  `;
1611
- state.retainedBytes += Buffer.byteLength(state.output, "utf8");
1726
+ state.retainedBytes = Buffer.byteLength(state.output, "utf8");
1612
1727
  resolve3(exitStatus);
1613
1728
  });
1614
1729
  })
1615
1730
  };
1616
- const perCallByteLimit = params.outputByteLimit ?? this.outputByteLimit;
1731
+ const perCallByteLimit = Math.min(
1732
+ Math.max(1, this.clampFiniteInt(params.outputByteLimit, this.outputByteLimit)),
1733
+ this.maxOutputByteLimit
1734
+ );
1617
1735
  proc.stdout?.setEncoding("utf8");
1618
1736
  proc.stderr?.setEncoding("utf8");
1619
1737
  const onData = (chunk) => {
@@ -1693,24 +1811,57 @@ var TerminalServer = class {
1693
1811
  if (resolved !== this.projectRoot && !resolved.startsWith(rootWithSep)) {
1694
1812
  return this.projectRoot;
1695
1813
  }
1696
- return resolved;
1697
- }
1698
- buildEnv(agentEnv) {
1699
- const env = { ...process.env };
1700
- if (process.platform === "win32") {
1701
- if (env.Path !== void 0 && env.PATH === void 0) env.PATH = env.Path;
1702
- if (env.PATHEXT !== void 0 && env.PATHEXT_CASE === void 0) {
1703
- env.PATHEXT_CASE = env.PATHEXT;
1814
+ try {
1815
+ const realRoot = realpathSync2(this.projectRoot);
1816
+ const realCwd = realpathSync2(resolved);
1817
+ const realRootWithSep = realRoot.endsWith(path2.sep) ? realRoot : realRoot + path2.sep;
1818
+ if (realCwd !== realRoot && !realCwd.startsWith(realRootWithSep)) {
1819
+ return realRoot;
1704
1820
  }
1821
+ return realCwd;
1822
+ } catch {
1823
+ return this.projectRoot;
1705
1824
  }
1825
+ }
1826
+ buildEnv(agentEnv) {
1827
+ const env = buildChildEnv();
1706
1828
  if (agentEnv) {
1707
1829
  for (const { name, value } of agentEnv) {
1830
+ const upper = name.toUpperCase();
1831
+ if (DENIED_AGENT_ENV_KEYS.has(upper)) continue;
1708
1832
  env[name] = value;
1709
1833
  }
1710
1834
  }
1711
1835
  return env;
1712
1836
  }
1837
+ /**
1838
+ * Clamp an agent-supplied numeric to a finite positive safe integer, falling
1839
+ * back to `defaultValue` for undefined/NaN/non-finite values. Prevents
1840
+ * negative, NaN, or Infinity values from disabling output caps or causing
1841
+ * unbounded memory growth.
1842
+ */
1843
+ clampFiniteInt(value, defaultValue) {
1844
+ if (value === void 0 || !Number.isFinite(value) || value < 1) {
1845
+ return defaultValue;
1846
+ }
1847
+ return Math.trunc(value);
1848
+ }
1713
1849
  };
1850
+ var DENIED_AGENT_ENV_KEYS = /* @__PURE__ */ new Set([
1851
+ "NODE_OPTIONS",
1852
+ "LD_PRELOAD",
1853
+ "LD_LIBRARY_PATH",
1854
+ "DYLD_INSERT_LIBRARIES",
1855
+ "DYLD_LIBRARY_PATH",
1856
+ "DYLD_FALLBACK_LIBRARY_PATH",
1857
+ "PATH",
1858
+ "PYTHONPATH",
1859
+ "PYTHONSTARTUP",
1860
+ "PERL5OPT",
1861
+ "PERLLIB",
1862
+ "RUBYOPT",
1863
+ "RUBYLIB"
1864
+ ]);
1714
1865
 
1715
1866
  // src/client/acp-session.ts
1716
1867
  var ACPSessionError = class extends Error {
@@ -2572,6 +2723,43 @@ var ACPSession = class _ACPSession {
2572
2723
  await this.sendErrorResponse(id, -32603, `permission policy failed: ${message}`);
2573
2724
  }
2574
2725
  }
2726
+ /**
2727
+ * Enforce authorization at privileged callback sinks (fs/write,
2728
+ * terminal/create). Unlike `handlePermissionRequest` which responds to
2729
+ * agent-initiated `session/request_permission` messages, this method is
2730
+ * called by the handler BEFORE dispatching to FileServer/TerminalServer,
2731
+ * closing the gap where the agent simply skips the voluntary permission
2732
+ * request and sends the privileged callback directly.
2733
+ *
2734
+ * Uses the session's permission policy. The default policy
2735
+ * (`defaultPermissionPolicy`) auto-approves everything — this is correct
2736
+ * for trusted local agents (CLI `acp spawn`, Director fan-out). For
2737
+ * untrusted/remote agents, the host should inject
2738
+ * `readOnlyPermissionPolicy` or an interactive policy.
2739
+ *
2740
+ * Returns true if the callback is authorized, false if denied.
2741
+ */
2742
+ async authorizeCallback(partial) {
2743
+ try {
2744
+ const outcome = await this.permissionPolicy({
2745
+ toolCall: {
2746
+ sessionUpdate: "tool_call_update",
2747
+ toolCallId: partial.toolCallId,
2748
+ title: partial.title,
2749
+ kind: partial.kind,
2750
+ status: "pending"
2751
+ },
2752
+ options: [
2753
+ { optionId: "allow", name: "Allow", kind: "allow_once" },
2754
+ { optionId: "reject", name: "Reject", kind: "reject_once" }
2755
+ ],
2756
+ signal: new AbortController().signal
2757
+ });
2758
+ return outcome.outcome === "selected" && outcome.optionId !== "reject" && outcome.optionId !== "reject_once" && outcome.optionId !== "reject_always";
2759
+ } catch {
2760
+ return false;
2761
+ }
2762
+ }
2575
2763
  async handleFsRequest(msg) {
2576
2764
  const id = msg.id;
2577
2765
  if (id === void 0) return;
@@ -2580,6 +2768,17 @@ var ACPSession = class _ACPSession {
2580
2768
  await this.sendErrorResponse(id, -32602, "path is required");
2581
2769
  return;
2582
2770
  }
2771
+ if (msg.method === "fs/write_text_file") {
2772
+ const allowed = await this.authorizeCallback({
2773
+ toolCallId: `acp-fs-write-${id}`,
2774
+ title: `Write file: ${params.path}`,
2775
+ kind: "edit"
2776
+ });
2777
+ if (!allowed) {
2778
+ await this.sendErrorResponse(id, -32602, "filesystem write denied by permission policy");
2779
+ return;
2780
+ }
2781
+ }
2583
2782
  try {
2584
2783
  if (msg.method === "fs/read_text_file") {
2585
2784
  const result = await this.fileServer.readTextFile({
@@ -2608,6 +2807,15 @@ var ACPSession = class _ACPSession {
2608
2807
  try {
2609
2808
  switch (msg.method) {
2610
2809
  case "terminal/create": {
2810
+ const allowed = await this.authorizeCallback({
2811
+ toolCallId: `acp-terminal-create-${id}`,
2812
+ title: `Run command: ${String(params.command ?? "")} ${(Array.isArray(params.args) ? params.args : []).join(" ")}`.trim(),
2813
+ kind: "execute"
2814
+ });
2815
+ if (!allowed) {
2816
+ await this.sendErrorResponse(id, -32602, "terminal create denied by permission policy");
2817
+ return;
2818
+ }
2611
2819
  const createOpts = {
2612
2820
  sessionId: String(params.sessionId ?? ""),
2613
2821
  command: String(params.command ?? ""),
@@ -2899,6 +3107,28 @@ var AGENTS_CATALOG = [
2899
3107
  },
2900
3108
  integration: "experimental",
2901
3109
  docs: "https://cursor.com"
3110
+ },
3111
+ // ── Moonshot AI (Kimi) ─────────────────────────────────────────────
3112
+ {
3113
+ id: "kimi",
3114
+ displayName: "Kimi Code CLI",
3115
+ vendor: "moonshot",
3116
+ probe: { command: "kimi", args: ["--version"] },
3117
+ // Kimi Code CLI speaks ACP behind `kimi acp`. The user must complete
3118
+ // terminal login (`kimi` → `/login`) before launching `kimi acp`;
3119
+ // otherwise session creation fails with `Authentication required`.
3120
+ // The adapter reuses the CLI's existing auth state — WrongStack does
3121
+ // NOT capture or replay the Kimi OAuth tokens.
3122
+ // Docs: https://www.kimi.com/code/docs/en/kimi-code-cli/reference/kimi-acp.html
3123
+ acp: { command: "kimi", args: ["acp"] },
3124
+ supports: {
3125
+ loadSession: true,
3126
+ promptImages: true,
3127
+ terminal: true,
3128
+ fs: true
3129
+ },
3130
+ integration: "native",
3131
+ docs: "https://www.kimi.com/code/docs/en/kimi-code-cli/guides/ides.html"
2902
3132
  }
2903
3133
  ];
2904
3134
  function findAgentDescriptor(id) {
@@ -3115,7 +3345,11 @@ var REGISTRY_ID_ALIASES = {
3115
3345
  "claude-code": "claude-acp",
3116
3346
  "gemini-cli": "gemini",
3117
3347
  "codex-cli": "codex-acp",
3118
- copilot: "github-copilot-cli"
3348
+ copilot: "github-copilot-cli",
3349
+ // Kimi's live registry id is `kimi` — same as our catalog id, so the
3350
+ // alias is identity. Listed explicitly so `resolveAcpAgentCommand`
3351
+ // finds the live entry when the registry is synced.
3352
+ kimi: "kimi"
3119
3353
  };
3120
3354
  function resolveAcpAgentCommand(id, overrides, live) {
3121
3355
  const ov = overrides?.[id];
@@ -3461,6 +3695,7 @@ function inferVendor(entry) {
3461
3695
  if (hay.includes("google") || hay.includes("gemini")) return "google";
3462
3696
  if (hay.includes("openai") || hay.includes("codex")) return "openai";
3463
3697
  if (hay.includes("github") || hay.includes("copilot")) return "github";
3698
+ if (hay.includes("moonshot") || hay.includes("kimi")) return "moonshot";
3464
3699
  return "community";
3465
3700
  }
3466
3701
  async function fetchAcpRegistry(opts = {}) {