@rkat/sdk 0.3.4 → 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/dist/client.js CHANGED
@@ -1,25 +1,471 @@
1
1
  /**
2
- * Meerkat client — spawns rkat-rpc subprocess and communicates via JSON-RPC.
2
+ * Meerkat client — spawns rkat-rpc and communicates via JSON-RPC.
3
+ *
4
+ * @example
5
+ * ```ts
6
+ * import { MeerkatClient } from "@rkat/sdk";
7
+ *
8
+ * const client = new MeerkatClient();
9
+ * await client.connect();
10
+ *
11
+ * const session = await client.createSession("Hello!");
12
+ * console.log(session.text);
13
+ *
14
+ * const result = await session.turn("Tell me more");
15
+ * console.log(result.text);
16
+ *
17
+ * for await (const event of session.stream("Explain in detail")) {
18
+ * if (event.type === "text_delta") {
19
+ * process.stdout.write(event.delta);
20
+ * }
21
+ * }
22
+ *
23
+ * await session.archive();
24
+ * await client.close();
25
+ * ```
3
26
  */
4
27
  import { spawn } from "node:child_process";
5
28
  import { chmodSync, existsSync, mkdirSync, unlinkSync, writeFileSync, } from "node:fs";
6
29
  import os from "node:os";
7
30
  import path from "node:path";
8
31
  import { createInterface } from "node:readline";
9
- import { MeerkatError } from "./generated/errors.js";
32
+ import { MeerkatError, CapabilityUnavailableError } from "./generated/errors.js";
10
33
  import { CONTRACT_VERSION } from "./generated/types.js";
34
+ import { Session } from "./session.js";
35
+ import { EventStream, AsyncQueue } from "./streaming.js";
11
36
  const MEERKAT_REPO = "lukacf/meerkat";
12
37
  const MEERKAT_RELEASE_BINARY = "rkat-rpc";
13
38
  const MEERKAT_BINARY_CACHE_ROOT = path.join(os.homedir(), ".cache", "meerkat", "bin", MEERKAT_RELEASE_BINARY);
39
+ /**
40
+ * Normalize a SkillRef to the wire format { source_uuid, skill_name }.
41
+ *
42
+ * SkillKey objects are converted from camelCase to snake_case.
43
+ * Legacy strings are parsed and emit a console warning.
44
+ */
45
+ function normalizeSkillRef(ref) {
46
+ if (typeof ref !== "string") {
47
+ return { source_uuid: ref.sourceUuid, skill_name: ref.skillName };
48
+ }
49
+ const value = ref.startsWith("/") ? ref.slice(1) : ref;
50
+ const [sourceUuid, ...rest] = value.split("/");
51
+ if (!sourceUuid || rest.length === 0) {
52
+ throw new Error(`Invalid skill reference '${ref}'. Expected '<source_uuid>/<skill_name>'.`);
53
+ }
54
+ console.warn(`[meerkat-sdk] legacy skill reference '${ref}' is deprecated; pass { sourceUuid, skillName } instead.`);
55
+ return { source_uuid: sourceUuid, skill_name: rest.join("/") };
56
+ }
57
+ function skillRefsToWire(refs) {
58
+ if (!refs)
59
+ return undefined;
60
+ return refs.map(normalizeSkillRef);
61
+ }
14
62
  export class MeerkatClient {
15
63
  process = null;
16
64
  requestId = 0;
17
- capabilities = null;
65
+ _capabilities = [];
18
66
  rkatPath;
19
67
  pendingRequests = new Map();
68
+ eventQueues = new Map();
69
+ rl = null;
20
70
  constructor(rkatPath = "rkat-rpc") {
21
71
  this.rkatPath = rkatPath;
22
72
  }
73
+ // -- Connection ---------------------------------------------------------
74
+ async connect(options) {
75
+ if (options?.realmId && options?.isolated) {
76
+ throw new MeerkatError("INVALID_ARGS", "realmId and isolated cannot both be set");
77
+ }
78
+ const resolved = await MeerkatClient.resolveBinaryPath(this.rkatPath);
79
+ this.rkatPath = resolved.command;
80
+ const args = MeerkatClient.buildArgs(resolved.useLegacySubcommand, options);
81
+ if (resolved.useLegacySubcommand && args.length > 1) {
82
+ throw new MeerkatError("LEGACY_BINARY_UNSUPPORTED", "Realm/context options require the standalone rkat-rpc binary. Install rkat-rpc and retry.");
83
+ }
84
+ this.process = spawn(this.rkatPath, args, {
85
+ stdio: ["pipe", "pipe", "pipe"],
86
+ });
87
+ this.rl = createInterface({ input: this.process.stdout });
88
+ this.rl.on("line", (line) => this.handleLine(line));
89
+ // Handshake
90
+ const initResult = await this.request("initialize", {});
91
+ const serverVersion = String(initResult.contract_version ?? "");
92
+ if (!MeerkatClient.checkVersionCompatible(serverVersion, CONTRACT_VERSION)) {
93
+ throw new MeerkatError("VERSION_MISMATCH", `Server version ${serverVersion} incompatible with SDK ${CONTRACT_VERSION}`);
94
+ }
95
+ // Fetch capabilities
96
+ const capsResult = await this.request("capabilities/get", {});
97
+ const rawCaps = capsResult.capabilities ?? [];
98
+ this._capabilities = rawCaps.map((cap) => ({
99
+ id: String(cap.id ?? ""),
100
+ description: String(cap.description ?? ""),
101
+ status: MeerkatClient.normalizeStatus(cap.status),
102
+ }));
103
+ return this;
104
+ }
105
+ async close() {
106
+ if (this.rl) {
107
+ this.rl.close();
108
+ this.rl = null;
109
+ }
110
+ if (this.process) {
111
+ this.process.kill();
112
+ this.process = null;
113
+ }
114
+ for (const [, pending] of this.pendingRequests) {
115
+ pending.reject(new MeerkatError("CLIENT_CLOSED", "Client closed"));
116
+ }
117
+ this.pendingRequests.clear();
118
+ for (const [, queue] of this.eventQueues) {
119
+ queue.put(null);
120
+ }
121
+ this.eventQueues.clear();
122
+ }
123
+ // -- Session lifecycle --------------------------------------------------
124
+ /**
125
+ * Create a new session, run the first turn, and return a {@link Session}.
126
+ *
127
+ * @example
128
+ * ```ts
129
+ * const session = await client.createSession("Summarise this project", {
130
+ * model: "claude-sonnet-4-5",
131
+ * });
132
+ * console.log(session.text);
133
+ * ```
134
+ */
135
+ async createSession(prompt, options) {
136
+ const params = MeerkatClient.buildCreateParams(prompt, options);
137
+ const raw = await this.request("session/create", params);
138
+ const result = MeerkatClient.parseRunResult(raw);
139
+ return new Session(this, result);
140
+ }
141
+ /**
142
+ * Create a new session and stream typed events from the first turn.
143
+ *
144
+ * Returns an {@link EventStream} `AsyncIterable<AgentEvent>`.
145
+ */
146
+ createSessionStreaming(prompt, options) {
147
+ if (!this.process?.stdin) {
148
+ throw new MeerkatError("NOT_CONNECTED", "Client not connected");
149
+ }
150
+ const params = MeerkatClient.buildCreateParams(prompt, options);
151
+ this.requestId++;
152
+ const requestId = this.requestId;
153
+ const queue = new AsyncQueue();
154
+ const responsePromise = this.registerRequest(requestId);
155
+ // When response arrives, bind the queue to the session_id
156
+ const wrappedPromise = responsePromise.then((result) => {
157
+ const sid = String(result.session_id ?? "");
158
+ if (sid) {
159
+ this.eventQueues.set(sid, queue);
160
+ }
161
+ return result;
162
+ });
163
+ const rpcRequest = { jsonrpc: "2.0", id: requestId, method: "session/create", params };
164
+ this.process.stdin.write(JSON.stringify(rpcRequest) + "\n");
165
+ return new EventStream({
166
+ sessionId: "",
167
+ eventQueue: queue,
168
+ responsePromise: wrappedPromise,
169
+ parseResult: MeerkatClient.parseRunResult,
170
+ });
171
+ }
172
+ // -- Session queries ----------------------------------------------------
173
+ async listSessions() {
174
+ const result = await this.request("session/list", {});
175
+ const sessions = result.sessions ?? [];
176
+ return sessions.map((s) => ({
177
+ sessionId: String(s.session_id ?? ""),
178
+ sessionRef: s.session_ref != null ? String(s.session_ref) : undefined,
179
+ createdAt: String(s.created_at ?? ""),
180
+ updatedAt: String(s.updated_at ?? ""),
181
+ messageCount: Number(s.message_count ?? 0),
182
+ totalTokens: Number(s.total_tokens ?? 0),
183
+ isActive: Boolean(s.is_active),
184
+ }));
185
+ }
186
+ async readSession(sessionId) {
187
+ return this.request("session/read", { session_id: sessionId });
188
+ }
189
+ // -- Capabilities -------------------------------------------------------
190
+ get capabilities() {
191
+ return this._capabilities;
192
+ }
193
+ hasCapability(capabilityId) {
194
+ return this._capabilities.some((c) => c.id === capabilityId && c.status === "Available");
195
+ }
196
+ requireCapability(capabilityId) {
197
+ if (!this.hasCapability(capabilityId)) {
198
+ throw new CapabilityUnavailableError("CAPABILITY_UNAVAILABLE", `Capability '${capabilityId}' is not available`);
199
+ }
200
+ }
201
+ // -- Config -------------------------------------------------------------
202
+ async getConfig() {
203
+ return this.request("config/get", {});
204
+ }
205
+ async setConfig(config, options) {
206
+ const params = { config };
207
+ if (options?.expectedGeneration !== undefined) {
208
+ params.expected_generation = options.expectedGeneration;
209
+ }
210
+ await this.request("config/set", params);
211
+ }
212
+ async patchConfig(patch, options) {
213
+ const params = { patch };
214
+ if (options?.expectedGeneration !== undefined) {
215
+ params.expected_generation = options.expectedGeneration;
216
+ }
217
+ return this.request("config/patch", params);
218
+ }
219
+ // -- Skills ---------------------------------------------------------------
220
+ async listSkills() {
221
+ const result = await this.request("skills/list", {});
222
+ return result.skills ?? [];
223
+ }
224
+ async inspectSkill(id, options) {
225
+ const params = { id };
226
+ if (options?.source !== undefined) {
227
+ params.source = options.source;
228
+ }
229
+ return this.request("skills/inspect", params);
230
+ }
231
+ // -- Internal methods used by Session -----------------------------------
232
+ /** @internal */
233
+ async _startTurn(sessionId, prompt, options) {
234
+ const params = { session_id: sessionId, prompt };
235
+ const wireRefs = skillRefsToWire(options?.skillRefs);
236
+ if (wireRefs) {
237
+ params.skill_refs = wireRefs;
238
+ }
239
+ if (options?.skillReferences) {
240
+ params.skill_references = options.skillReferences;
241
+ }
242
+ const raw = await this.request("turn/start", params);
243
+ return MeerkatClient.parseRunResult(raw);
244
+ }
245
+ /** @internal */
246
+ _startTurnStreaming(sessionId, prompt, options, session) {
247
+ if (!this.process?.stdin) {
248
+ throw new MeerkatError("NOT_CONNECTED", "Client not connected");
249
+ }
250
+ this.requestId++;
251
+ const requestId = this.requestId;
252
+ const queue = new AsyncQueue();
253
+ this.eventQueues.set(sessionId, queue);
254
+ const responsePromise = this.registerRequest(requestId);
255
+ const params = { session_id: sessionId, prompt };
256
+ const wireRefs = skillRefsToWire(options?.skillRefs);
257
+ if (wireRefs) {
258
+ params.skill_refs = wireRefs;
259
+ }
260
+ if (options?.skillReferences) {
261
+ params.skill_references = options.skillReferences;
262
+ }
263
+ const rpcRequest = { jsonrpc: "2.0", id: requestId, method: "turn/start", params };
264
+ this.process.stdin.write(JSON.stringify(rpcRequest) + "\n");
265
+ return new EventStream({
266
+ sessionId,
267
+ eventQueue: queue,
268
+ responsePromise,
269
+ parseResult: MeerkatClient.parseRunResult,
270
+ session,
271
+ });
272
+ }
273
+ /** @internal */
274
+ async _interrupt(sessionId) {
275
+ await this.request("turn/interrupt", { session_id: sessionId });
276
+ }
277
+ /** @internal */
278
+ async _archive(sessionId) {
279
+ await this.request("session/archive", { session_id: sessionId });
280
+ }
281
+ /** @internal */
282
+ async _send(sessionId, command) {
283
+ return this.request("comms/send", { session_id: sessionId, ...command });
284
+ }
285
+ /** @internal */
286
+ async _peers(sessionId) {
287
+ return this.request("comms/peers", { session_id: sessionId });
288
+ }
289
+ // -- Transport ----------------------------------------------------------
290
+ handleLine(line) {
291
+ let data;
292
+ try {
293
+ data = JSON.parse(line);
294
+ }
295
+ catch {
296
+ return;
297
+ }
298
+ if ("id" in data && typeof data.id === "number") {
299
+ const pending = this.pendingRequests.get(data.id);
300
+ if (pending) {
301
+ this.pendingRequests.delete(data.id);
302
+ const error = data.error;
303
+ if (error) {
304
+ pending.reject(new MeerkatError(String(error.code ?? "UNKNOWN"), String(error.message ?? "Unknown error")));
305
+ }
306
+ else {
307
+ pending.resolve((data.result ?? {}));
308
+ }
309
+ }
310
+ }
311
+ else if ("method" in data) {
312
+ const params = (data.params ?? {});
313
+ const sessionId = String(params.session_id ?? "");
314
+ const event = params.event;
315
+ if (event) {
316
+ const queue = this.eventQueues.get(sessionId);
317
+ if (queue) {
318
+ queue.put(event);
319
+ }
320
+ }
321
+ }
322
+ }
323
+ request(method, params) {
324
+ if (!this.process?.stdin) {
325
+ throw new MeerkatError("NOT_CONNECTED", "Client not connected");
326
+ }
327
+ this.requestId++;
328
+ const id = this.requestId;
329
+ const rpcRequest = { jsonrpc: "2.0", id, method, params };
330
+ const promise = this.registerRequest(id);
331
+ this.process.stdin.write(JSON.stringify(rpcRequest) + "\n");
332
+ return promise;
333
+ }
334
+ registerRequest(id) {
335
+ return new Promise((resolve, reject) => {
336
+ this.pendingRequests.set(id, { resolve, reject });
337
+ });
338
+ }
339
+ // -- Static helpers -----------------------------------------------------
340
+ static normalizeStatus(raw) {
341
+ if (typeof raw === "string")
342
+ return raw;
343
+ if (typeof raw === "object" && raw !== null) {
344
+ // Rust can emit externally-tagged enums for status:
345
+ // { DisabledByPolicy: { description: "..." } }
346
+ return Object.keys(raw)[0] ?? "Unknown";
347
+ }
348
+ return String(raw);
349
+ }
350
+ static parseSkillDiagnostics(raw) {
351
+ if (!raw || typeof raw !== "object")
352
+ return undefined;
353
+ const data = raw;
354
+ const sourceHealthRaw = data.source_health;
355
+ const rawQuarantined = Array.isArray(data.quarantined)
356
+ ? data.quarantined
357
+ : [];
358
+ const quarantined = rawQuarantined
359
+ .filter((item) => typeof item === "object" && item !== null)
360
+ .map((item) => ({
361
+ sourceUuid: String(item.source_uuid ?? ""),
362
+ skillId: String(item.skill_id ?? ""),
363
+ location: String(item.location ?? ""),
364
+ errorCode: String(item.error_code ?? ""),
365
+ errorClass: String(item.error_class ?? ""),
366
+ message: String(item.message ?? ""),
367
+ firstSeenUnixSecs: Number(item.first_seen_unix_secs ?? 0),
368
+ lastSeenUnixSecs: Number(item.last_seen_unix_secs ?? 0),
369
+ }));
370
+ return {
371
+ sourceHealth: {
372
+ state: String(sourceHealthRaw?.state ?? ""),
373
+ invalidRatio: Number(sourceHealthRaw?.invalid_ratio ?? 0),
374
+ invalidCount: Number(sourceHealthRaw?.invalid_count ?? 0),
375
+ totalCount: Number(sourceHealthRaw?.total_count ?? 0),
376
+ failureStreak: Number(sourceHealthRaw?.failure_streak ?? 0),
377
+ handshakeFailed: Boolean(sourceHealthRaw?.handshake_failed ?? false),
378
+ },
379
+ quarantined,
380
+ };
381
+ }
382
+ static checkVersionCompatible(server, client) {
383
+ try {
384
+ const s = server.split(".").map(Number);
385
+ const c = client.split(".").map(Number);
386
+ if (s[0] === 0 && c[0] === 0)
387
+ return s[1] === c[1];
388
+ return s[0] === c[0];
389
+ }
390
+ catch {
391
+ return false;
392
+ }
393
+ }
394
+ static parseRunResult(data) {
395
+ const usageRaw = data.usage;
396
+ const usage = {
397
+ inputTokens: Number(usageRaw?.input_tokens ?? 0),
398
+ outputTokens: Number(usageRaw?.output_tokens ?? 0),
399
+ cacheCreationTokens: usageRaw?.cache_creation_tokens != null
400
+ ? Number(usageRaw.cache_creation_tokens)
401
+ : undefined,
402
+ cacheReadTokens: usageRaw?.cache_read_tokens != null
403
+ ? Number(usageRaw.cache_read_tokens)
404
+ : undefined,
405
+ };
406
+ const rawWarnings = data.schema_warnings;
407
+ const schemaWarnings = rawWarnings?.map((w) => ({
408
+ provider: String(w.provider ?? ""),
409
+ path: String(w.path ?? ""),
410
+ message: String(w.message ?? ""),
411
+ }));
412
+ return {
413
+ sessionId: String(data.session_id ?? ""),
414
+ sessionRef: data.session_ref != null ? String(data.session_ref) : undefined,
415
+ text: String(data.text ?? ""),
416
+ turns: Number(data.turns ?? 0),
417
+ toolCalls: Number(data.tool_calls ?? 0),
418
+ usage,
419
+ structuredOutput: data.structured_output,
420
+ schemaWarnings,
421
+ skillDiagnostics: MeerkatClient.parseSkillDiagnostics(data.skill_diagnostics),
422
+ };
423
+ }
424
+ static buildCreateParams(prompt, options) {
425
+ const params = { prompt };
426
+ if (!options)
427
+ return params;
428
+ if (options.model)
429
+ params.model = options.model;
430
+ if (options.provider)
431
+ params.provider = options.provider;
432
+ if (options.systemPrompt)
433
+ params.system_prompt = options.systemPrompt;
434
+ if (options.maxTokens)
435
+ params.max_tokens = options.maxTokens;
436
+ if (options.outputSchema)
437
+ params.output_schema = options.outputSchema;
438
+ if (options.structuredOutputRetries != null && options.structuredOutputRetries !== 2) {
439
+ params.structured_output_retries = options.structuredOutputRetries;
440
+ }
441
+ if (options.hooksOverride)
442
+ params.hooks_override = options.hooksOverride;
443
+ if (options.enableBuiltins)
444
+ params.enable_builtins = true;
445
+ if (options.enableShell)
446
+ params.enable_shell = true;
447
+ if (options.enableSubagents)
448
+ params.enable_subagents = true;
449
+ if (options.enableMemory)
450
+ params.enable_memory = true;
451
+ if (options.hostMode)
452
+ params.host_mode = true;
453
+ if (options.commsName)
454
+ params.comms_name = options.commsName;
455
+ if (options.peerMeta != null)
456
+ params.peer_meta = options.peerMeta;
457
+ if (options.providerParams)
458
+ params.provider_params = options.providerParams;
459
+ if (options.preloadSkills != null)
460
+ params.preload_skills = options.preloadSkills;
461
+ const wireRefs = skillRefsToWire(options.skillRefs);
462
+ if (wireRefs)
463
+ params.skill_refs = wireRefs;
464
+ if (options.skillReferences != null)
465
+ params.skill_references = options.skillReferences;
466
+ return params;
467
+ }
468
+ // -- Binary resolution --------------------------------------------------
23
469
  static commandPath(command) {
24
470
  if (path.isAbsolute(command)) {
25
471
  return existsSync(command) ? command : null;
@@ -59,39 +505,20 @@ export class MeerkatClient {
59
505
  const architecture = os.arch();
60
506
  if (process.platform === "darwin") {
61
507
  if (architecture === "arm64") {
62
- return {
63
- target: "aarch64-apple-darwin",
64
- archiveExt: "tar.gz",
65
- binaryName: "rkat-rpc",
66
- };
508
+ return { target: "aarch64-apple-darwin", archiveExt: "tar.gz", binaryName: "rkat-rpc" };
67
509
  }
68
- throw new MeerkatError("UNSUPPORTED_PLATFORM", `Unsupported macOS architecture '${architecture}'. Only Apple Silicon (arm64) is supported.`);
510
+ throw new MeerkatError("UNSUPPORTED_PLATFORM", `Unsupported macOS architecture '${architecture}'.`);
69
511
  }
70
512
  if (process.platform === "linux") {
71
- if (architecture === "x64") {
72
- return {
73
- target: "x86_64-unknown-linux-gnu",
74
- archiveExt: "tar.gz",
75
- binaryName: "rkat-rpc",
76
- };
77
- }
78
- if (architecture === "arm64") {
79
- return {
80
- target: "aarch64-unknown-linux-gnu",
81
- archiveExt: "tar.gz",
82
- binaryName: "rkat-rpc",
83
- };
84
- }
513
+ if (architecture === "x64")
514
+ return { target: "x86_64-unknown-linux-gnu", archiveExt: "tar.gz", binaryName: "rkat-rpc" };
515
+ if (architecture === "arm64")
516
+ return { target: "aarch64-unknown-linux-gnu", archiveExt: "tar.gz", binaryName: "rkat-rpc" };
85
517
  throw new MeerkatError("UNSUPPORTED_PLATFORM", `Unsupported Linux architecture '${architecture}'.`);
86
518
  }
87
519
  if (process.platform === "win32") {
88
- if (architecture === "x64") {
89
- return {
90
- target: "x86_64-pc-windows-msvc",
91
- archiveExt: "zip",
92
- binaryName: "rkat-rpc.exe",
93
- };
94
- }
520
+ if (architecture === "x64")
521
+ return { target: "x86_64-pc-windows-msvc", archiveExt: "zip", binaryName: "rkat-rpc.exe" };
95
522
  throw new MeerkatError("UNSUPPORTED_PLATFORM", `Unsupported Windows architecture '${architecture}'.`);
96
523
  }
97
524
  throw new MeerkatError("UNSUPPORTED_PLATFORM", `Unsupported platform '${process.platform}'.`);
@@ -100,12 +527,8 @@ export class MeerkatClient {
100
527
  return new Promise((resolve, reject) => {
101
528
  const proc = spawn(command, args, { stdio: ["ignore", "inherit", "pipe"] });
102
529
  let stderr = "";
103
- proc.stderr?.on("data", (chunk) => {
104
- stderr += String(chunk);
105
- });
106
- proc.on("error", (error) => {
107
- reject(error);
108
- });
530
+ proc.stderr?.on("data", (chunk) => { stderr += String(chunk); });
531
+ proc.on("error", (error) => { reject(error); });
109
532
  proc.on("close", (code) => {
110
533
  if (code === 0) {
111
534
  resolve();
@@ -118,12 +541,10 @@ export class MeerkatClient {
118
541
  static async extractZip(archivePath, destinationDir) {
119
542
  try {
120
543
  await MeerkatClient.runCommand("tar", ["-xf", archivePath, "-C", destinationDir]);
121
- return;
122
544
  }
123
- catch (error) {
545
+ catch {
124
546
  await MeerkatClient.runCommand("powershell", [
125
- "-NoProfile",
126
- "-Command",
547
+ "-NoProfile", "-Command",
127
548
  `Expand-Archive -LiteralPath '${archivePath.replaceAll("'", "''")}' -DestinationPath '${destinationDir.replaceAll("'", "''")}' -Force`,
128
549
  ]);
129
550
  }
@@ -136,9 +557,8 @@ export class MeerkatClient {
136
557
  const baseDir = path.join(MEERKAT_BINARY_CACHE_ROOT, `v${version}`, target);
137
558
  mkdirSync(baseDir, { recursive: true });
138
559
  const cached = path.join(baseDir, binaryName);
139
- if (existsSync(cached)) {
560
+ if (existsSync(cached))
140
561
  return cached;
141
- }
142
562
  const response = await fetch(url);
143
563
  if (!response.ok) {
144
564
  throw new MeerkatError("BINARY_DOWNLOAD_FAILED", `Failed to download binary from ${url} (HTTP ${response.status})`);
@@ -163,9 +583,8 @@ export class MeerkatClient {
163
583
  }
164
584
  catch { /* best-effort cleanup */ }
165
585
  }
166
- if (process.platform !== "win32") {
586
+ if (process.platform !== "win32")
167
587
  chmodSync(cached, 0o755);
168
- }
169
588
  return cached;
170
589
  }
171
590
  static async resolveBinaryPath(requestedPath) {
@@ -173,293 +592,54 @@ export class MeerkatClient {
173
592
  if (overridden) {
174
593
  const candidate = MeerkatClient.resolveCandidatePath(overridden);
175
594
  if (!candidate) {
176
- throw new MeerkatError("BINARY_NOT_FOUND", `Binary not found at MEERKAT_BIN_PATH='${overridden}'. Set it to a valid executable.`);
595
+ throw new MeerkatError("BINARY_NOT_FOUND", `Binary not found at MEERKAT_BIN_PATH='${overridden}'.`);
177
596
  }
178
- return {
179
- command: candidate,
180
- useLegacySubcommand: path.basename(candidate) === "rkat",
181
- };
597
+ return { command: candidate, useLegacySubcommand: path.basename(candidate) === "rkat" };
182
598
  }
183
599
  if (requestedPath !== "rkat-rpc") {
184
600
  const candidate = MeerkatClient.resolveCandidatePath(requestedPath);
185
601
  if (!candidate) {
186
- throw new MeerkatError("BINARY_NOT_FOUND", `Binary not found at '${requestedPath}'. Set rkatPath to a valid path or use MEERKAT_BIN_PATH.`);
602
+ throw new MeerkatError("BINARY_NOT_FOUND", `Binary not found at '${requestedPath}'.`);
187
603
  }
188
- return {
189
- command: candidate,
190
- useLegacySubcommand: path.basename(candidate) === "rkat",
191
- };
604
+ return { command: candidate, useLegacySubcommand: path.basename(candidate) === "rkat" };
192
605
  }
193
606
  const defaultBinary = MeerkatClient.commandPath("rkat-rpc");
194
- if (defaultBinary) {
195
- return {
196
- command: defaultBinary,
197
- useLegacySubcommand: false,
198
- };
199
- }
607
+ if (defaultBinary)
608
+ return { command: defaultBinary, useLegacySubcommand: false };
200
609
  try {
201
610
  const downloaded = await MeerkatClient.ensureDownloadedBinary();
202
611
  return { command: downloaded, useLegacySubcommand: false };
203
612
  }
204
613
  catch (error) {
205
614
  const legacy = MeerkatClient.commandPath("rkat");
206
- if (legacy) {
615
+ if (legacy)
207
616
  return { command: legacy, useLegacySubcommand: true };
208
- }
209
617
  if (error instanceof MeerkatError)
210
618
  throw error;
211
619
  throw new MeerkatError("BINARY_NOT_FOUND", `Could not find '${MEERKAT_RELEASE_BINARY}' and auto-download failed.`);
212
620
  }
213
621
  }
214
- async connect(options) {
215
- if (options?.realmId && options?.isolated) {
216
- throw new MeerkatError("INVALID_ARGS", "realmId and isolated cannot both be set");
217
- }
218
- const resolved = await MeerkatClient.resolveBinaryPath(this.rkatPath);
219
- this.rkatPath = resolved.command;
622
+ static buildArgs(legacy, options) {
623
+ if (legacy)
624
+ return ["rpc"];
625
+ if (!options)
626
+ return [];
220
627
  const args = [];
221
- const legacyRequiresNewBinary = Boolean(options?.isolated ||
222
- options?.realmId ||
223
- options?.instanceId ||
224
- options?.realmBackend ||
225
- options?.stateRoot ||
226
- options?.contextRoot ||
227
- options?.userConfigRoot);
228
- if (resolved.useLegacySubcommand) {
229
- if (legacyRequiresNewBinary) {
230
- throw new MeerkatError("LEGACY_BINARY_UNSUPPORTED", "Realm/context options require the standalone rkat-rpc binary. Install rkat-rpc and retry.");
231
- }
232
- args.push("rpc");
233
- }
234
- else {
235
- if (options?.isolated) {
236
- args.push("--isolated");
237
- }
238
- if (options?.realmId) {
239
- args.push("--realm", options.realmId);
240
- }
241
- if (options?.instanceId) {
242
- args.push("--instance", options.instanceId);
243
- }
244
- if (options?.realmBackend) {
245
- args.push("--realm-backend", options.realmBackend);
246
- }
247
- if (options?.stateRoot) {
248
- args.push("--state-root", options.stateRoot);
249
- }
250
- if (options?.contextRoot) {
251
- args.push("--context-root", options.contextRoot);
252
- }
253
- if (options?.userConfigRoot) {
254
- args.push("--user-config-root", options.userConfigRoot);
255
- }
256
- }
257
- this.process = spawn(this.rkatPath, args, {
258
- stdio: ["pipe", "pipe", "pipe"],
259
- });
260
- const rl = createInterface({ input: this.process.stdout });
261
- rl.on("line", (line) => {
262
- try {
263
- const response = JSON.parse(line);
264
- if ("id" in response &&
265
- typeof response.id === "number" &&
266
- this.pendingRequests.has(response.id)) {
267
- const pending = this.pendingRequests.get(response.id);
268
- this.pendingRequests.delete(response.id);
269
- const error = response.error;
270
- if (error) {
271
- pending.reject(new MeerkatError(String(error.code ?? "UNKNOWN"), String(error.message ?? "Unknown error")));
272
- }
273
- else {
274
- pending.resolve(response.result ?? {});
275
- }
276
- }
277
- }
278
- catch {
279
- // Ignore non-JSON lines
280
- }
281
- });
282
- // Initialize
283
- const initResult = (await this.request("initialize", {}));
284
- const serverVersion = String(initResult.contract_version ?? "");
285
- if (!this.checkVersionCompatible(serverVersion, CONTRACT_VERSION)) {
286
- throw new MeerkatError("VERSION_MISMATCH", `Server version ${serverVersion} incompatible with SDK ${CONTRACT_VERSION}`);
287
- }
288
- // Fetch capabilities
289
- const capsResult = (await this.request("capabilities/get", {}));
290
- this.capabilities = {
291
- contract_version: String(capsResult.contract_version ?? ""),
292
- capabilities: (capsResult.capabilities ?? []).map((cap) => ({
293
- id: String(cap.id ?? ""),
294
- description: String(cap.description ?? ""),
295
- status: MeerkatClient.normalizeStatus(cap.status),
296
- })),
297
- };
298
- return this;
299
- }
300
- async close() {
301
- if (this.process) {
302
- this.process.kill();
303
- this.process = null;
304
- }
305
- }
306
- async createSession(params) {
307
- const result = (await this.request("session/create", params));
308
- return this.parseRunResult(result);
309
- }
310
- async startTurn(sessionId, prompt, options) {
311
- const result = (await this.request("turn/start", {
312
- session_id: sessionId,
313
- prompt,
314
- ...options,
315
- }));
316
- return this.parseRunResult(result);
317
- }
318
- async interrupt(sessionId) {
319
- await this.request("turn/interrupt", { session_id: sessionId });
320
- }
321
- async listSessions() {
322
- const result = (await this.request("session/list", {}));
323
- return result.sessions ?? [];
324
- }
325
- async readSession(sessionId) {
326
- return (await this.request("session/read", {
327
- session_id: sessionId,
328
- }));
329
- }
330
- async archiveSession(sessionId) {
331
- await this.request("session/archive", { session_id: sessionId });
332
- }
333
- async getCapabilities() {
334
- if (this.capabilities)
335
- return this.capabilities;
336
- const result = (await this.request("capabilities/get", {}));
337
- return {
338
- contract_version: String(result.contract_version ?? ""),
339
- capabilities: [],
340
- };
341
- }
342
- hasCapability(capabilityId) {
343
- if (!this.capabilities)
344
- return false;
345
- return this.capabilities.capabilities.some((c) => c.id === capabilityId && c.status === "Available");
346
- }
347
- requireCapability(capabilityId) {
348
- if (!this.hasCapability(capabilityId)) {
349
- throw new MeerkatError("CAPABILITY_UNAVAILABLE", `Capability '${capabilityId}' is not available`);
350
- }
351
- }
352
- // --- Config ---
353
- async getConfig() {
354
- return (await this.request("config/get", {}));
355
- }
356
- async setConfig(config) {
357
- await this.request("config/set", config);
358
- }
359
- async patchConfig(patch) {
360
- return (await this.request("config/patch", patch));
361
- }
362
- /**
363
- * Send a canonical comms command to a session.
364
- *
365
- * @param sessionId - Target session ID
366
- * @param command - Command fields (kind, to, body, intent, params, etc.)
367
- * @returns Receipt info on success
368
- */
369
- async send(sessionId, command) {
370
- return (await this.request("comms/send", {
371
- session_id: sessionId,
372
- ...command,
373
- }));
374
- }
375
- /**
376
- * Send a command and open a stream in one call.
377
- *
378
- * @param sessionId - Target session ID
379
- * @param command - Command fields (kind, to, body, etc.)
380
- * @returns Receipt and stream info
381
- */
382
- async sendAndStream(sessionId, command) {
383
- return (await this.request("comms/send", {
384
- session_id: sessionId,
385
- stream: "reserve_interaction",
386
- ...command,
387
- }));
388
- }
389
- /**
390
- * List peers visible to a session's comms runtime.
391
- *
392
- * @param sessionId - Target session ID
393
- * @returns Object with `{ peers: [...] }`
394
- */
395
- async peers(sessionId) {
396
- return (await this.request("comms/peers", {
397
- session_id: sessionId,
398
- }));
399
- }
400
- // --- Internal ---
401
- /**
402
- * Normalize a CapabilityStatus from the wire.
403
- * Available is the string "Available", but other variants are
404
- * externally-tagged objects like {"DisabledByPolicy": {"description": "..."}}.
405
- * We normalize to the variant name string.
406
- */
407
- static normalizeStatus(raw) {
408
- if (typeof raw === "string")
409
- return raw;
410
- if (typeof raw === "object" && raw !== null) {
411
- const keys = Object.keys(raw);
412
- return keys[0] ?? "Unknown";
413
- }
414
- return String(raw);
415
- }
416
- request(method, params) {
417
- if (!this.process?.stdin) {
418
- throw new MeerkatError("NOT_CONNECTED", "Client not connected");
419
- }
420
- this.requestId++;
421
- const id = this.requestId;
422
- const request = {
423
- jsonrpc: "2.0",
424
- id,
425
- method,
426
- params,
427
- };
428
- return new Promise((resolve, reject) => {
429
- this.pendingRequests.set(id, { resolve, reject });
430
- this.process.stdin.write(JSON.stringify(request) + "\n");
431
- });
432
- }
433
- checkVersionCompatible(server, client) {
434
- try {
435
- const s = server.split(".").map(Number);
436
- const c = client.split(".").map(Number);
437
- if (s[0] === 0 && c[0] === 0)
438
- return s[1] === c[1];
439
- return s[0] === c[0];
440
- }
441
- catch {
442
- return false;
443
- }
444
- }
445
- parseRunResult(data) {
446
- const usage = data.usage;
447
- return {
448
- session_id: String(data.session_id ?? ""),
449
- session_ref: data.session_ref == null ? undefined : String(data.session_ref),
450
- text: String(data.text ?? ""),
451
- turns: Number(data.turns ?? 0),
452
- tool_calls: Number(data.tool_calls ?? 0),
453
- usage: {
454
- input_tokens: Number(usage?.input_tokens ?? 0),
455
- output_tokens: Number(usage?.output_tokens ?? 0),
456
- total_tokens: Number(usage?.total_tokens ?? 0),
457
- cache_creation_tokens: usage?.cache_creation_tokens,
458
- cache_read_tokens: usage?.cache_read_tokens,
459
- },
460
- structured_output: data.structured_output,
461
- schema_warnings: data.schema_warnings,
462
- };
628
+ if (options.isolated)
629
+ args.push("--isolated");
630
+ if (options.realmId)
631
+ args.push("--realm", options.realmId);
632
+ if (options.instanceId)
633
+ args.push("--instance", options.instanceId);
634
+ if (options.realmBackend)
635
+ args.push("--realm-backend", options.realmBackend);
636
+ if (options.stateRoot)
637
+ args.push("--state-root", options.stateRoot);
638
+ if (options.contextRoot)
639
+ args.push("--context-root", options.contextRoot);
640
+ if (options.userConfigRoot)
641
+ args.push("--user-config-root", options.userConfigRoot);
642
+ return args;
463
643
  }
464
644
  }
465
645
  //# sourceMappingURL=client.js.map