clay-server 2.46.0 → 2.47.0-beta.2

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/lib/yoke/index.js CHANGED
@@ -5,6 +5,7 @@ var iface = require("./interface");
5
5
  var instructions = require("./instructions");
6
6
  var createClaudeAdapter = require("./adapters/claude").createClaudeAdapter;
7
7
  var createCodexAdapter = require("./adapters/codex").createCodexAdapter;
8
+ var createKiroAdapter = require("./adapters/kiro").createKiroAdapter;
8
9
 
9
10
  /**
10
11
  * Wrap adapter.createQuery to inject cross-vendor project instructions.
@@ -50,6 +51,8 @@ function createAdapter(opts) {
50
51
  adapter = createClaudeAdapter(opts);
51
52
  } else if (vendor === "codex") {
52
53
  adapter = createCodexAdapter(opts);
54
+ } else if (vendor === "kiro") {
55
+ adapter = createKiroAdapter(opts);
53
56
  } else {
54
57
  throw new Error("[YOKE] Unknown adapter vendor: " + vendor);
55
58
  }
@@ -73,7 +76,7 @@ function logAuthCheck(auth) {
73
76
  if (_lastAuthLogKey === key && now - _lastAuthLogAt < 30000) return;
74
77
  _lastAuthLogKey = key;
75
78
  _lastAuthLogAt = now;
76
- console.log("[yoke] Auth check: claude=" + auth.claude + " codex=" + auth.codex);
79
+ console.log("[yoke] Auth check: claude=" + auth.claude + " codex=" + auth.codex + " kiro=" + auth.kiro);
77
80
  }
78
81
 
79
82
  function checkAuth() {
@@ -181,7 +184,30 @@ function checkAuth() {
181
184
  }
182
185
  }
183
186
 
184
- _authCache = { claude: checkClaude(), codex: checkCodex() };
187
+ function resolveKiroBinary() {
188
+ var fs = require("fs");
189
+ try {
190
+ var findKiroPath = require("./kiro-acp-server").findKiroPath;
191
+ var kiroBin = findKiroPath();
192
+ if (kiroBin && fs.existsSync(kiroBin)) return kiroBin;
193
+ } catch (e) {}
194
+ return lookupBinary("kiro-cli");
195
+ }
196
+
197
+ function checkKiro() {
198
+ try {
199
+ var kiroBin = resolveKiroBinary();
200
+ if (!kiroBin) return false;
201
+ // `kiro-cli whoami` exits 0 and prints account details when logged in,
202
+ // and exits non-zero otherwise.
203
+ execFileSync(kiroBin, ["whoami"], { timeout: 5000, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
204
+ return true;
205
+ } catch (e) {
206
+ return false;
207
+ }
208
+ }
209
+
210
+ _authCache = { claude: checkClaude(), codex: checkCodex(), kiro: checkKiro() };
185
211
  logAuthCheck(_authCache);
186
212
  return _authCache;
187
213
  }
@@ -203,12 +229,17 @@ function checkInstalled() {
203
229
 
204
230
  var fs = require("fs");
205
231
  var execFileSync = require("child_process").execFileSync;
206
- var result = { claude: false, codex: false };
232
+ var result = { claude: false, codex: false, kiro: false };
207
233
  try {
208
234
  if (process.platform === "win32") execFileSync("where", ["claude"], { timeout: 3000, stdio: ["pipe", "pipe", "pipe"] });
209
235
  else execFileSync("which", ["claude"], { timeout: 3000, stdio: ["pipe", "pipe", "pipe"] });
210
236
  result.claude = true;
211
237
  } catch (e) {}
238
+ try {
239
+ var findKiroPath = require("./kiro-acp-server").findKiroPath;
240
+ var kiroBin = findKiroPath();
241
+ if (kiroBin && fs.existsSync(kiroBin)) result.kiro = true;
242
+ } catch (e) {}
212
243
  try {
213
244
  var codexBin = null;
214
245
  try {
@@ -250,13 +281,17 @@ function createAdapters(opts) {
250
281
  // that `claude auth status` does not always detect. Runtime auth failures are
251
282
  // handled downstream via query-level error detection.
252
283
  var installed = checkInstalled();
253
- var auth = { claude: false, codex: false };
284
+ var auth = { claude: false, codex: false, kiro: false };
254
285
  var adapters = {};
255
286
 
256
287
  if (installed.claude) {
257
288
  try {
258
289
  if (!_sharedClaudeAdapter) {
259
290
  _sharedClaudeAdapter = createAdapter({ vendor: "claude", cwd: opts.cwd });
291
+ // This adapter instance is reused by every project. Mark it so
292
+ // per-project teardown never shuts it down on behalf of one project
293
+ // (see destroy() in lib/project.js).
294
+ _sharedClaudeAdapter.shared = true;
260
295
  }
261
296
  adapters.claude = _sharedClaudeAdapter;
262
297
  auth.claude = true;
@@ -276,6 +311,18 @@ function createAdapters(opts) {
276
311
  }
277
312
  }
278
313
 
314
+ if (installed.kiro && !opts.osUsers) {
315
+ try {
316
+ adapters.kiro = createAdapter({ vendor: "kiro", cwd: opts.cwd, slug: opts.slug });
317
+ auth.kiro = true;
318
+ console.log("[yoke] Adapter created: kiro");
319
+ } catch (e) {
320
+ console.error("[yoke] Failed to create adapter for kiro:", e.message);
321
+ }
322
+ } else if (installed.kiro && opts.osUsers) {
323
+ console.log("[yoke] Kiro adapter disabled: OS-user isolation requires per-user ACP spawning");
324
+ }
325
+
279
326
  return { adapters: adapters, auth: auth };
280
327
  }
281
328
 
@@ -287,6 +334,11 @@ function createAdapters(opts) {
287
334
  async function lazyCreateAdapter(adapters, vendor, opts) {
288
335
  opts = opts || {};
289
336
 
337
+ if (vendor === "kiro" && (opts.osUsers || opts.linuxUser)) {
338
+ console.log("[yoke] Refusing lazy Kiro adapter creation for OS-isolated user " + (opts.linuxUser || "unknown"));
339
+ return null;
340
+ }
341
+
290
342
  // Force re-check since user may have logged in after server start
291
343
  invalidateAuthCache();
292
344
  var installed = checkInstalled();
@@ -0,0 +1,328 @@
1
+ // Kiro ACP Server Protocol Client
2
+ // --------------------------------
3
+ // Manages a `kiro-cli acp` child process with bidirectional JSON-RPC 2.0
4
+ // communication over stdin/stdout. Kiro CLI implements the Agent Client
5
+ // Protocol (ACP), the same standardized protocol used by editors like Zed.
6
+ //
7
+ // This is structurally the same transport as codex-app-server.js: line-delimited
8
+ // JSON-RPC where the child both answers our requests and initiates its own
9
+ // (session/update notifications and session/request_permission requests).
10
+
11
+ var { spawn } = require("child_process");
12
+ var readline = require("readline");
13
+ var path = require("path");
14
+ var fs = require("fs");
15
+
16
+ // --- Find the kiro-cli binary path ---
17
+ // Kiro CLI installs to ~/.local/bin on Linux/macOS. We also honor a
18
+ // KIRO_CLI_PATH override and fall back to a plain PATH lookup.
19
+ function findKiroPath() {
20
+ if (process.env.KIRO_CLI_PATH && fs.existsSync(process.env.KIRO_CLI_PATH)) {
21
+ return process.env.KIRO_CLI_PATH;
22
+ }
23
+
24
+ var binName = process.platform === "win32" ? "kiro-cli.exe" : "kiro-cli";
25
+ var REAL_HOME;
26
+ try { REAL_HOME = require("../config").REAL_HOME; } catch (e) { REAL_HOME = require("os").homedir(); }
27
+
28
+ var candidates = [
29
+ path.join(REAL_HOME || "", ".local", "bin", binName),
30
+ path.join(REAL_HOME || "", "bin", binName),
31
+ "/usr/local/bin/" + binName,
32
+ "/opt/homebrew/bin/" + binName,
33
+ ];
34
+ for (var i = 0; i < candidates.length; i++) {
35
+ if (candidates[i] && fs.existsSync(candidates[i])) return candidates[i];
36
+ }
37
+
38
+ // Fall back to a PATH lookup.
39
+ try {
40
+ var execFileSync = require("child_process").execFileSync;
41
+ var out = process.platform === "win32"
42
+ ? execFileSync("where", [binName], { timeout: 3000, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] })
43
+ : execFileSync("which", ["kiro-cli"], { timeout: 3000, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
44
+ var resolved = out.trim().split(/\r?\n/)[0];
45
+ if (resolved) return resolved;
46
+ } catch (e) {}
47
+
48
+ throw new Error("Could not find kiro-cli binary (looked in ~/.local/bin, PATH, KIRO_CLI_PATH)");
49
+ }
50
+
51
+ // --- KiroAcpServer ---
52
+
53
+ function KiroAcpServer(executablePath, opts) {
54
+ this.proc = null;
55
+ this.rl = null;
56
+ this.nextId = 1;
57
+ this.pendingRequests = {}; // id -> { resolve, reject, timer }
58
+ this.requestHandlers = {}; // method -> async function(params, message)
59
+ // One ACP process is shared by every session in a project, so server-initiated
60
+ // events must be routed by params.sessionId rather than handed to a single
61
+ // handler. Each entry is { sessionId, fn }; sessionId starts null and is
62
+ // filled in once session/new or session/load resolves.
63
+ this.handlers = [];
64
+ this.executablePath = executablePath || findKiroPath();
65
+ this.opts = opts || {};
66
+ this.started = false;
67
+ this._stderrBuf = "";
68
+ this._authSignalSent = false;
69
+ }
70
+
71
+ KiroAcpServer.prototype.start = function() {
72
+ var self = this;
73
+
74
+ return new Promise(function(resolve, reject) {
75
+ try {
76
+ var args = ["acp"];
77
+ // Kiro auto-approves nothing by default; Clay drives approvals through
78
+ // session/request_permission, so we do not pass --trust-all-tools here.
79
+ if (self.opts.extraArgs && self.opts.extraArgs.length) {
80
+ args = args.concat(self.opts.extraArgs);
81
+ }
82
+ var env = Object.assign({}, process.env, self.opts.env || {});
83
+
84
+ console.log("[kiro-acp-server] Spawning:", self.executablePath, args.join(" "));
85
+
86
+ self.proc = spawn(self.executablePath, args, {
87
+ stdio: ["pipe", "pipe", "pipe"],
88
+ env: env,
89
+ cwd: self.opts.cwd || process.cwd(),
90
+ });
91
+
92
+ self.proc.on("error", function(err) {
93
+ console.error("[kiro-acp-server] Process error:", err.message);
94
+ if (!self.started) reject(err);
95
+ self._rejectAllPending(err);
96
+ });
97
+
98
+ self.proc.on("exit", function(code, signal) {
99
+ console.log("[kiro-acp-server] Process exited: code=" + code + " signal=" + signal);
100
+ self.started = false;
101
+ self._rejectAllPending(new Error("Process exited: code=" + code));
102
+ });
103
+
104
+ // Collect stderr for debugging + auth-error detection.
105
+ self.proc.stderr.on("data", function(chunk) {
106
+ var text = chunk.toString();
107
+ self._stderrBuf += text;
108
+ var lines = self._stderrBuf.split("\n");
109
+ while (lines.length > 1) {
110
+ var line = lines.shift();
111
+ if (line.trim()) console.log("[kiro-acp-server stderr]", line);
112
+ self._maybeSignalAuthError(line);
113
+ }
114
+ self._stderrBuf = lines[0] || "";
115
+ });
116
+
117
+ // Line-based JSON-RPC reading from stdout.
118
+ self.rl = readline.createInterface({ input: self.proc.stdout, crlfDelay: Infinity });
119
+ self.rl.on("line", function(line) {
120
+ if (!line.trim()) return;
121
+ try {
122
+ var msg = JSON.parse(line);
123
+ self._handleMessage(msg);
124
+ } catch (e) {
125
+ console.error("[kiro-acp-server] Failed to parse line:", line.substring(0, 200));
126
+ }
127
+ });
128
+ self.rl.on("close", function() {
129
+ console.log("[kiro-acp-server] stdout closed");
130
+ });
131
+
132
+ self.started = true;
133
+ resolve();
134
+ } catch (e) {
135
+ reject(e);
136
+ }
137
+ });
138
+ };
139
+
140
+ KiroAcpServer.prototype._handleMessage = function(msg) {
141
+ // Response to a request we sent.
142
+ if (msg.id !== undefined && msg.id !== null && (msg.result !== undefined || msg.error !== undefined)) {
143
+ var pending = this.pendingRequests[msg.id];
144
+ if (pending) {
145
+ delete this.pendingRequests[msg.id];
146
+ if (pending.timer) clearTimeout(pending.timer);
147
+ if (msg.error) {
148
+ var e = new Error(msg.error.message || JSON.stringify(msg.error));
149
+ e.rpcError = msg.error;
150
+ pending.reject(e);
151
+ } else {
152
+ pending.resolve(msg.result);
153
+ }
154
+ }
155
+ return;
156
+ }
157
+
158
+ // Server-initiated request (has id + method) or notification (has method, no id).
159
+ if (msg.method) {
160
+ var isRequest = msg.id !== undefined && msg.id !== null;
161
+ var directHandler = isRequest && this.requestHandlers[msg.method];
162
+ if (directHandler) {
163
+ var self = this;
164
+ Promise.resolve().then(function() {
165
+ return directHandler(msg.params || {}, msg);
166
+ }).then(function(result) {
167
+ self.respond(msg.id, result);
168
+ }).catch(function(err) {
169
+ console.error("[kiro-acp-server] Request handler failed for " + msg.method + ":", err && err.message ? err.message : err);
170
+ self.respondError(msg.id, -32002, err && err.message ? err.message : "Request handler failed");
171
+ });
172
+ return;
173
+ }
174
+
175
+ var sessionId = msg.params && msg.params.sessionId;
176
+ var targets;
177
+ if (sessionId) {
178
+ targets = this.handlers.filter(function(h) { return h.sessionId === sessionId; });
179
+ } else {
180
+ // Process-wide events (auth failures, transport errors) have no session,
181
+ // so every active session should see them.
182
+ targets = this.handlers.slice();
183
+ }
184
+
185
+ if (!targets.length) {
186
+ // A request carries an id and MUST be answered, otherwise kiro-cli blocks
187
+ // on it until session/prompt times out. Never drop one silently.
188
+ if (msg.id !== undefined && msg.id !== null) {
189
+ console.warn("[kiro-acp-server] No handler for request " + msg.method + " (session=" + (sessionId || "none") + "), rejecting");
190
+ this.respondError(msg.id, -32001, "No active handler for session " + (sessionId || "none"));
191
+ } else {
192
+ console.log("[kiro-acp-server] Unhandled event:", msg.method);
193
+ }
194
+ return;
195
+ }
196
+
197
+ // A request must be answered exactly once, so only the first matching
198
+ // handler gets it. Notifications fan out to all matches.
199
+ if (msg.id !== undefined && msg.id !== null) {
200
+ try {
201
+ targets[0].fn(msg);
202
+ } catch (e) {
203
+ console.error("[kiro-acp-server] Handler threw for " + msg.method + ":", e && e.message ? e.message : e);
204
+ this.respondError(msg.id, -32000, "Handler error");
205
+ }
206
+ return;
207
+ }
208
+ targets.forEach(function(h) {
209
+ try { h.fn(msg); } catch (e) {
210
+ console.error("[kiro-acp-server] Handler threw for " + msg.method + ":", e && e.message ? e.message : e);
211
+ }
212
+ });
213
+ }
214
+ };
215
+
216
+ // Register a per-query handler. Returns the entry so the caller can set
217
+ // entry.sessionId once known and pass it back to removeHandler on teardown.
218
+ KiroAcpServer.prototype.addHandler = function(fn) {
219
+ var entry = { sessionId: null, fn: fn };
220
+ this.handlers.push(entry);
221
+ return entry;
222
+ };
223
+
224
+ KiroAcpServer.prototype.removeHandler = function(entry) {
225
+ var idx = this.handlers.indexOf(entry);
226
+ if (idx !== -1) this.handlers.splice(idx, 1);
227
+ };
228
+
229
+ KiroAcpServer.prototype.addRequestHandler = function(method, fn) {
230
+ this.requestHandlers[method] = fn;
231
+ };
232
+
233
+ // Detect "not logged in" signals on stderr. Kiro surfaces auth failures as
234
+ // 401/expired-token/"kiro-cli login" hints. Deduped so a burst collapses.
235
+ KiroAcpServer.prototype._maybeSignalAuthError = function(line) {
236
+ if (!this.handlers.length || !line || this._authSignalSent) return;
237
+ var isAuth = /not logged in|expired token|token has expired|please (?:sign in|log ?in) again|reauthenticate|kiro-cli login|no valid credentials|forbidden/i.test(line)
238
+ || (/\b401\b/.test(line) && /unauthorized|credential|token/i.test(line));
239
+ if (!isAuth) return;
240
+ this._authSignalSent = true;
241
+ var self = this;
242
+ var dedupeTimer = setTimeout(function() { self._authSignalSent = false; }, 15000);
243
+ // Don't hold the event loop open just to reset a dedupe flag.
244
+ if (dedupeTimer && typeof dedupeTimer.unref === "function") dedupeTimer.unref();
245
+ // No sessionId: _handleMessage fans this out to every active session.
246
+ this._handleMessage({ method: "_kiro/error", params: { error: { kiroErrorInfo: "unauthorized", message: line } } });
247
+ };
248
+
249
+ // Send a JSON-RPC request (expects a response).
250
+ KiroAcpServer.prototype.send = function(method, params, timeoutMs) {
251
+ var self = this;
252
+ var id = this.nextId++;
253
+ timeoutMs = timeoutMs || 30000;
254
+
255
+ return new Promise(function(resolve, reject) {
256
+ if (!self.proc || !self.started) {
257
+ return reject(new Error("ACP server not started"));
258
+ }
259
+ var timer = setTimeout(function() {
260
+ delete self.pendingRequests[id];
261
+ reject(new Error("Request timeout: " + method + " (id=" + id + ")"));
262
+ }, timeoutMs);
263
+ self.pendingRequests[id] = { resolve: resolve, reject: reject, timer: timer };
264
+
265
+ var msg = { jsonrpc: "2.0", id: id, method: method };
266
+ if (params !== undefined) msg.params = params;
267
+ self._write(msg);
268
+ });
269
+ };
270
+
271
+ // Send a JSON-RPC notification (no response expected).
272
+ KiroAcpServer.prototype.notify = function(method, params) {
273
+ if (!this.proc || !this.started) return;
274
+ var msg = { jsonrpc: "2.0", method: method };
275
+ if (params !== undefined) msg.params = params;
276
+ this._write(msg);
277
+ };
278
+
279
+ // Respond to a server-initiated request.
280
+ KiroAcpServer.prototype.respond = function(id, result) {
281
+ if (!this.proc || !this.started) return;
282
+ this._write({ jsonrpc: "2.0", id: id, result: result });
283
+ };
284
+
285
+ // Respond with an error to a server-initiated request.
286
+ KiroAcpServer.prototype.respondError = function(id, code, message) {
287
+ if (!this.proc || !this.started) return;
288
+ this._write({ jsonrpc: "2.0", id: id, error: { code: code || -1, message: message || "Error" } });
289
+ };
290
+
291
+ KiroAcpServer.prototype._write = function(msg) {
292
+ if (!this.proc || !this.proc.stdin || this.proc.stdin.destroyed) return;
293
+ try {
294
+ this.proc.stdin.write(JSON.stringify(msg) + "\n");
295
+ } catch (e) {
296
+ console.error("[kiro-acp-server] Write error:", e.message);
297
+ }
298
+ };
299
+
300
+ KiroAcpServer.prototype._rejectAllPending = function(err) {
301
+ var ids = Object.keys(this.pendingRequests);
302
+ for (var i = 0; i < ids.length; i++) {
303
+ var pending = this.pendingRequests[ids[i]];
304
+ if (pending.timer) clearTimeout(pending.timer);
305
+ pending.reject(err);
306
+ }
307
+ this.pendingRequests = {};
308
+ };
309
+
310
+ KiroAcpServer.prototype.stop = function() {
311
+ this.started = false;
312
+ this._rejectAllPending(new Error("Stopped"));
313
+
314
+ if (this.rl) {
315
+ this.rl.close();
316
+ this.rl = null;
317
+ }
318
+ if (this.proc) {
319
+ try { this.proc.stdin.end(); } catch (e) {}
320
+ try { this.proc.kill("SIGTERM"); } catch (e) {}
321
+ this.proc = null;
322
+ }
323
+ };
324
+
325
+ module.exports = {
326
+ KiroAcpServer: KiroAcpServer,
327
+ findKiroPath: findKiroPath,
328
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clay-server",
3
- "version": "2.46.0",
3
+ "version": "2.47.0-beta.2",
4
4
  "description": "Self-hosted team workspace for Claude Code and Codex. Multi-user, browser-based, with persistent AI mates.",
5
5
  "bin": {
6
6
  "clay-server": "./bin/cli.js",
@@ -8,6 +8,7 @@
8
8
  },
9
9
  "scripts": {
10
10
  "dev": "node bin/cli.js --dev",
11
+ "test": "node --test --test-force-exit test/*.test.js",
11
12
  "prepack": "npm pkg delete bin.clay-dev",
12
13
  "postpack": "npm pkg set bin.clay-dev=./bin/cli.js",
13
14
  "semantic-release": "semantic-release"