@rkat/sdk 0.3.4 → 0.4.1

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,640 @@
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";
10
- import { CONTRACT_VERSION } from "./generated/types.js";
32
+ import { MeerkatError, CapabilityUnavailableError } from "./generated/errors.js";
33
+ import { CONTRACT_VERSION, } from "./generated/types.js";
34
+ import { Session } from "./session.js";
35
+ import { CommsEventStream, 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
+ commsStreamQueues = new Map();
70
+ pendingStreamQueue = null;
71
+ pendingStreamRequestId = null;
72
+ unmatchedStreamBuffer = new Map();
73
+ rl = null;
20
74
  constructor(rkatPath = "rkat-rpc") {
21
75
  this.rkatPath = rkatPath;
22
76
  }
77
+ // -- Connection ---------------------------------------------------------
78
+ async connect(options) {
79
+ if (options?.realmId && options?.isolated) {
80
+ throw new MeerkatError("INVALID_ARGS", "realmId and isolated cannot both be set");
81
+ }
82
+ const resolved = await MeerkatClient.resolveBinaryPath(this.rkatPath);
83
+ this.rkatPath = resolved.command;
84
+ const args = MeerkatClient.buildArgs(resolved.useLegacySubcommand, options);
85
+ if (resolved.useLegacySubcommand && args.length > 1) {
86
+ throw new MeerkatError("LEGACY_BINARY_UNSUPPORTED", "Realm/context options require the standalone rkat-rpc binary. Install rkat-rpc and retry.");
87
+ }
88
+ this.process = spawn(this.rkatPath, args, {
89
+ stdio: ["pipe", "pipe", "pipe"],
90
+ });
91
+ this.rl = createInterface({ input: this.process.stdout });
92
+ this.rl.on("line", (line) => this.handleLine(line));
93
+ // Handshake
94
+ const initResult = await this.request("initialize", {});
95
+ const serverVersion = String(initResult.contract_version ?? "");
96
+ if (!MeerkatClient.checkVersionCompatible(serverVersion, CONTRACT_VERSION)) {
97
+ throw new MeerkatError("VERSION_MISMATCH", `Server version ${serverVersion} incompatible with SDK ${CONTRACT_VERSION}`);
98
+ }
99
+ // Fetch capabilities
100
+ const capsResult = await this.request("capabilities/get", {});
101
+ const rawCaps = capsResult.capabilities ?? [];
102
+ this._capabilities = rawCaps.map((cap) => ({
103
+ id: String(cap.id ?? ""),
104
+ description: String(cap.description ?? ""),
105
+ status: MeerkatClient.normalizeStatus(cap.status),
106
+ }));
107
+ return this;
108
+ }
109
+ async close() {
110
+ if (this.rl) {
111
+ this.rl.close();
112
+ this.rl = null;
113
+ }
114
+ if (this.process) {
115
+ this.process.kill();
116
+ this.process = null;
117
+ }
118
+ for (const [, pending] of this.pendingRequests) {
119
+ pending.reject(new MeerkatError("CLIENT_CLOSED", "Client closed"));
120
+ }
121
+ this.pendingRequests.clear();
122
+ for (const [, queue] of this.eventQueues) {
123
+ queue.put(null);
124
+ }
125
+ this.eventQueues.clear();
126
+ for (const [, queue] of this.commsStreamQueues) {
127
+ queue.put(null);
128
+ }
129
+ this.commsStreamQueues.clear();
130
+ if (this.pendingStreamQueue) {
131
+ this.pendingStreamQueue.put(null);
132
+ this.pendingStreamQueue = null;
133
+ this.pendingStreamRequestId = null;
134
+ this.unmatchedStreamBuffer.clear();
135
+ }
136
+ }
137
+ // -- Session lifecycle --------------------------------------------------
138
+ /**
139
+ * Create a new session, run the first turn, and return a {@link Session}.
140
+ *
141
+ * @example
142
+ * ```ts
143
+ * const session = await client.createSession("Summarise this project", {
144
+ * model: "claude-sonnet-4-5",
145
+ * });
146
+ * console.log(session.text);
147
+ * ```
148
+ */
149
+ async createSession(prompt, options) {
150
+ const params = MeerkatClient.buildCreateParams(prompt, options);
151
+ const raw = await this.request("session/create", params);
152
+ const result = MeerkatClient.parseRunResult(raw);
153
+ return new Session(this, result);
154
+ }
155
+ /**
156
+ * Create a new session and stream typed events from the first turn.
157
+ *
158
+ * Returns an {@link EventStream} `AsyncIterable<AgentEvent>`.
159
+ */
160
+ createSessionStreaming(prompt, options) {
161
+ if (!this.process?.stdin) {
162
+ throw new MeerkatError("NOT_CONNECTED", "Client not connected");
163
+ }
164
+ const params = MeerkatClient.buildCreateParams(prompt, options);
165
+ this.requestId++;
166
+ const requestId = this.requestId;
167
+ if (this.pendingStreamQueue) {
168
+ throw new MeerkatError("INVALID_STATE", "Only one createSessionStreaming request can be pending at a time");
169
+ }
170
+ const queue = new AsyncQueue();
171
+ this.pendingStreamQueue = queue;
172
+ this.pendingStreamRequestId = requestId;
173
+ const responsePromise = this.registerRequest(requestId);
174
+ // When response arrives, bind the queue to the session_id
175
+ const wrappedPromise = responsePromise.then((result) => {
176
+ const sid = String(result.session_id ?? "");
177
+ if (sid) {
178
+ const buffered = this.unmatchedStreamBuffer.get(sid) ?? [];
179
+ for (const evt of buffered) {
180
+ queue.put(evt);
181
+ }
182
+ this.unmatchedStreamBuffer.delete(sid);
183
+ this.eventQueues.set(sid, queue);
184
+ }
185
+ this.pendingStreamQueue = null;
186
+ this.pendingStreamRequestId = null;
187
+ this.unmatchedStreamBuffer.clear();
188
+ return result;
189
+ });
190
+ const rpcRequest = { jsonrpc: "2.0", id: requestId, method: "session/create", params };
191
+ this.process.stdin.write(JSON.stringify(rpcRequest) + "\n");
192
+ return new EventStream({
193
+ sessionId: "",
194
+ eventQueue: queue,
195
+ responsePromise: wrappedPromise,
196
+ parseResult: MeerkatClient.parseRunResult,
197
+ });
198
+ }
199
+ // -- Session queries ----------------------------------------------------
200
+ async listSessions() {
201
+ const result = await this.request("session/list", {});
202
+ const sessions = result.sessions ?? [];
203
+ return sessions.map((s) => ({
204
+ sessionId: String(s.session_id ?? ""),
205
+ sessionRef: s.session_ref != null ? String(s.session_ref) : undefined,
206
+ createdAt: String(s.created_at ?? ""),
207
+ updatedAt: String(s.updated_at ?? ""),
208
+ messageCount: Number(s.message_count ?? 0),
209
+ totalTokens: Number(s.total_tokens ?? 0),
210
+ isActive: Boolean(s.is_active),
211
+ }));
212
+ }
213
+ async readSession(sessionId) {
214
+ return this.request("session/read", { session_id: sessionId });
215
+ }
216
+ // -- Capabilities -------------------------------------------------------
217
+ get capabilities() {
218
+ return this._capabilities;
219
+ }
220
+ hasCapability(capabilityId) {
221
+ return this._capabilities.some((c) => c.id === capabilityId && c.status === "Available");
222
+ }
223
+ requireCapability(capabilityId) {
224
+ if (!this.hasCapability(capabilityId)) {
225
+ throw new CapabilityUnavailableError("CAPABILITY_UNAVAILABLE", `Capability '${capabilityId}' is not available`);
226
+ }
227
+ }
228
+ // -- Config -------------------------------------------------------------
229
+ async getConfig() {
230
+ return this.request("config/get", {});
231
+ }
232
+ async setConfig(config, options) {
233
+ const params = { config };
234
+ if (options?.expectedGeneration !== undefined) {
235
+ params.expected_generation = options.expectedGeneration;
236
+ }
237
+ return this.request("config/set", params);
238
+ }
239
+ async patchConfig(patch, options) {
240
+ const params = { patch };
241
+ if (options?.expectedGeneration !== undefined) {
242
+ params.expected_generation = options.expectedGeneration;
243
+ }
244
+ return this.request("config/patch", params);
245
+ }
246
+ async mcpAdd(params) {
247
+ const raw = await this.request("mcp/add", params);
248
+ return MeerkatClient.parseMcpLiveOpResponse(raw);
249
+ }
250
+ async mcpRemove(params) {
251
+ const raw = await this.request("mcp/remove", params);
252
+ return MeerkatClient.parseMcpLiveOpResponse(raw);
253
+ }
254
+ async mcpReload(params) {
255
+ const raw = await this.request("mcp/reload", params);
256
+ return MeerkatClient.parseMcpLiveOpResponse(raw);
257
+ }
258
+ // snake_case aliases for parity with other SDKs/surfaces
259
+ async mcp_add(params) {
260
+ return this.mcpAdd(params);
261
+ }
262
+ async mcp_remove(params) {
263
+ return this.mcpRemove(params);
264
+ }
265
+ async mcp_reload(params) {
266
+ return this.mcpReload(params);
267
+ }
268
+ // -- Skills ---------------------------------------------------------------
269
+ async listSkills() {
270
+ const result = await this.request("skills/list", {});
271
+ return result.skills ?? [];
272
+ }
273
+ async inspectSkill(id, options) {
274
+ const params = { id };
275
+ if (options?.source !== undefined) {
276
+ params.source = options.source;
277
+ }
278
+ return this.request("skills/inspect", params);
279
+ }
280
+ async listMobPrefabs() {
281
+ const result = await this.request("mob/prefabs", {});
282
+ return result.prefabs ?? [];
283
+ }
284
+ async list_mob_prefabs() {
285
+ return this.listMobPrefabs();
286
+ }
287
+ async listMobTools() {
288
+ const result = await this.request("mob/tools", {});
289
+ return result.tools ?? [];
290
+ }
291
+ async callMobTool(name, argumentsPayload) {
292
+ return this.request("mob/call", {
293
+ name,
294
+ arguments: argumentsPayload ?? {},
295
+ });
296
+ }
297
+ // -- Internal methods used by Session -----------------------------------
298
+ /** @internal */
299
+ async _startTurn(sessionId, prompt, options) {
300
+ const params = { session_id: sessionId, prompt };
301
+ const wireRefs = skillRefsToWire(options?.skillRefs);
302
+ if (wireRefs) {
303
+ params.skill_refs = wireRefs;
304
+ }
305
+ if (options?.skillReferences) {
306
+ params.skill_references = options.skillReferences;
307
+ }
308
+ if (options?.flowToolOverlay) {
309
+ params.flow_tool_overlay = {
310
+ allowed_tools: options.flowToolOverlay.allowedTools,
311
+ blocked_tools: options.flowToolOverlay.blockedTools,
312
+ };
313
+ }
314
+ const raw = await this.request("turn/start", params);
315
+ return MeerkatClient.parseRunResult(raw);
316
+ }
317
+ /** @internal */
318
+ _startTurnStreaming(sessionId, prompt, options, session) {
319
+ if (!this.process?.stdin) {
320
+ throw new MeerkatError("NOT_CONNECTED", "Client not connected");
321
+ }
322
+ this.requestId++;
323
+ const requestId = this.requestId;
324
+ const queue = new AsyncQueue();
325
+ this.eventQueues.set(sessionId, queue);
326
+ const responsePromise = this.registerRequest(requestId);
327
+ const params = { session_id: sessionId, prompt };
328
+ const wireRefs = skillRefsToWire(options?.skillRefs);
329
+ if (wireRefs) {
330
+ params.skill_refs = wireRefs;
331
+ }
332
+ if (options?.skillReferences) {
333
+ params.skill_references = options.skillReferences;
334
+ }
335
+ if (options?.flowToolOverlay) {
336
+ params.flow_tool_overlay = {
337
+ allowed_tools: options.flowToolOverlay.allowedTools,
338
+ blocked_tools: options.flowToolOverlay.blockedTools,
339
+ };
340
+ }
341
+ const rpcRequest = { jsonrpc: "2.0", id: requestId, method: "turn/start", params };
342
+ this.process.stdin.write(JSON.stringify(rpcRequest) + "\n");
343
+ return new EventStream({
344
+ sessionId,
345
+ eventQueue: queue,
346
+ responsePromise,
347
+ parseResult: MeerkatClient.parseRunResult,
348
+ session,
349
+ });
350
+ }
351
+ /** @internal */
352
+ async _interrupt(sessionId) {
353
+ await this.request("turn/interrupt", { session_id: sessionId });
354
+ }
355
+ /** @internal */
356
+ async _archive(sessionId) {
357
+ await this.request("session/archive", { session_id: sessionId });
358
+ }
359
+ /** @internal */
360
+ async _send(sessionId, command) {
361
+ return this.send(sessionId, command);
362
+ }
363
+ /** @internal */
364
+ async _peers(sessionId) {
365
+ return this.peers(sessionId);
366
+ }
367
+ async send(sessionId, command) {
368
+ return this.request("comms/send", { session_id: sessionId, ...command });
369
+ }
370
+ async peers(sessionId) {
371
+ return this.request("comms/peers", { session_id: sessionId });
372
+ }
373
+ async openCommsStream(sessionId, options) {
374
+ const scope = options?.scope ?? "session";
375
+ const params = { session_id: sessionId, scope };
376
+ if (options?.interactionId) {
377
+ params.interaction_id = options.interactionId;
378
+ }
379
+ const opened = await this.request("comms/stream_open", params);
380
+ const streamId = String(opened.stream_id ?? "");
381
+ if (!streamId) {
382
+ throw new MeerkatError("INVALID_RESPONSE", "Missing stream_id in comms/stream_open response");
383
+ }
384
+ const queue = new AsyncQueue();
385
+ this.commsStreamQueues.set(streamId, queue);
386
+ return new CommsEventStream({
387
+ streamId,
388
+ eventQueue: queue,
389
+ onClose: async (id) => {
390
+ await this.request("comms/stream_close", { stream_id: id });
391
+ const q = this.commsStreamQueues.get(id);
392
+ if (q) {
393
+ q.put(null);
394
+ }
395
+ this.commsStreamQueues.delete(id);
396
+ },
397
+ });
398
+ }
399
+ async open_comms_stream(sessionId, options) {
400
+ return this.openCommsStream(sessionId, options);
401
+ }
402
+ async sendAndStream(sessionId, command) {
403
+ const receipt = await this.send(sessionId, command);
404
+ const interactionId = String(receipt.interaction_id ?? "");
405
+ const streamReserved = Boolean(receipt.stream_reserved ?? false);
406
+ if (!interactionId) {
407
+ throw new MeerkatError("INVALID_RESPONSE", "comms/send response missing interaction_id for sendAndStream");
408
+ }
409
+ if (!streamReserved) {
410
+ throw new MeerkatError("INVALID_RESPONSE", "comms/send response did not reserve stream for sendAndStream");
411
+ }
412
+ const stream = await this.openCommsStream(sessionId, {
413
+ scope: "interaction",
414
+ interactionId,
415
+ });
416
+ return { receipt, stream };
417
+ }
418
+ async send_and_stream(sessionId, command) {
419
+ return this.sendAndStream(sessionId, command);
420
+ }
421
+ // -- Transport ----------------------------------------------------------
422
+ handleLine(line) {
423
+ let data;
424
+ try {
425
+ data = JSON.parse(line);
426
+ }
427
+ catch {
428
+ return;
429
+ }
430
+ if ("id" in data && typeof data.id === "number") {
431
+ const pending = this.pendingRequests.get(data.id);
432
+ if (pending) {
433
+ this.pendingRequests.delete(data.id);
434
+ const error = data.error;
435
+ if (error) {
436
+ pending.reject(new MeerkatError(String(error.code ?? "UNKNOWN"), String(error.message ?? "Unknown error")));
437
+ }
438
+ else {
439
+ pending.resolve((data.result ?? {}));
440
+ }
441
+ }
442
+ }
443
+ else if ("method" in data) {
444
+ if (data.method === "comms/stream_event") {
445
+ const params = (data.params ?? {});
446
+ const streamId = String(params.stream_id ?? "");
447
+ const queue = this.commsStreamQueues.get(streamId);
448
+ if (queue) {
449
+ queue.put(params);
450
+ }
451
+ return;
452
+ }
453
+ const params = (data.params ?? {});
454
+ const sessionId = String(params.session_id ?? "");
455
+ const event = params.event;
456
+ if (event) {
457
+ const queue = this.eventQueues.get(sessionId);
458
+ if (queue) {
459
+ queue.put(event);
460
+ }
461
+ else if (this.pendingStreamQueue) {
462
+ const buffered = this.unmatchedStreamBuffer.get(sessionId) ?? [];
463
+ buffered.push(event);
464
+ this.unmatchedStreamBuffer.set(sessionId, buffered);
465
+ }
466
+ }
467
+ }
468
+ }
469
+ request(method, params) {
470
+ if (!this.process?.stdin) {
471
+ throw new MeerkatError("NOT_CONNECTED", "Client not connected");
472
+ }
473
+ this.requestId++;
474
+ const id = this.requestId;
475
+ const rpcRequest = { jsonrpc: "2.0", id, method, params };
476
+ const promise = this.registerRequest(id);
477
+ this.process.stdin.write(JSON.stringify(rpcRequest) + "\n");
478
+ return promise;
479
+ }
480
+ registerRequest(id) {
481
+ return new Promise((resolve, reject) => {
482
+ this.pendingRequests.set(id, { resolve, reject });
483
+ });
484
+ }
485
+ // -- Static helpers -----------------------------------------------------
486
+ static normalizeStatus(raw) {
487
+ if (typeof raw === "string")
488
+ return raw;
489
+ if (typeof raw === "object" && raw !== null) {
490
+ // Rust can emit externally-tagged enums for status:
491
+ // { DisabledByPolicy: { description: "..." } }
492
+ return Object.keys(raw)[0] ?? "Unknown";
493
+ }
494
+ return String(raw);
495
+ }
496
+ static parseSkillDiagnostics(raw) {
497
+ if (!raw || typeof raw !== "object")
498
+ return undefined;
499
+ const data = raw;
500
+ const sourceHealthRaw = data.source_health;
501
+ const rawQuarantined = Array.isArray(data.quarantined)
502
+ ? data.quarantined
503
+ : [];
504
+ const quarantined = rawQuarantined
505
+ .filter((item) => typeof item === "object" && item !== null)
506
+ .map((item) => ({
507
+ sourceUuid: String(item.source_uuid ?? ""),
508
+ skillId: String(item.skill_id ?? ""),
509
+ location: String(item.location ?? ""),
510
+ errorCode: String(item.error_code ?? ""),
511
+ errorClass: String(item.error_class ?? ""),
512
+ message: String(item.message ?? ""),
513
+ firstSeenUnixSecs: Number(item.first_seen_unix_secs ?? 0),
514
+ lastSeenUnixSecs: Number(item.last_seen_unix_secs ?? 0),
515
+ }));
516
+ return {
517
+ sourceHealth: {
518
+ state: String(sourceHealthRaw?.state ?? ""),
519
+ invalidRatio: Number(sourceHealthRaw?.invalid_ratio ?? 0),
520
+ invalidCount: Number(sourceHealthRaw?.invalid_count ?? 0),
521
+ totalCount: Number(sourceHealthRaw?.total_count ?? 0),
522
+ failureStreak: Number(sourceHealthRaw?.failure_streak ?? 0),
523
+ handshakeFailed: Boolean(sourceHealthRaw?.handshake_failed ?? false),
524
+ },
525
+ quarantined,
526
+ };
527
+ }
528
+ static checkVersionCompatible(server, client) {
529
+ try {
530
+ const s = server.split(".").map(Number);
531
+ const c = client.split(".").map(Number);
532
+ if (s[0] === 0 && c[0] === 0)
533
+ return s[1] === c[1];
534
+ return s[0] === c[0];
535
+ }
536
+ catch {
537
+ return false;
538
+ }
539
+ }
540
+ static parseRunResult(data) {
541
+ const usageRaw = data.usage;
542
+ const usage = {
543
+ inputTokens: Number(usageRaw?.input_tokens ?? 0),
544
+ outputTokens: Number(usageRaw?.output_tokens ?? 0),
545
+ cacheCreationTokens: usageRaw?.cache_creation_tokens != null
546
+ ? Number(usageRaw.cache_creation_tokens)
547
+ : undefined,
548
+ cacheReadTokens: usageRaw?.cache_read_tokens != null
549
+ ? Number(usageRaw.cache_read_tokens)
550
+ : undefined,
551
+ };
552
+ const rawWarnings = data.schema_warnings;
553
+ const schemaWarnings = rawWarnings?.map((w) => ({
554
+ provider: String(w.provider ?? ""),
555
+ path: String(w.path ?? ""),
556
+ message: String(w.message ?? ""),
557
+ }));
558
+ return {
559
+ sessionId: String(data.session_id ?? ""),
560
+ sessionRef: data.session_ref != null ? String(data.session_ref) : undefined,
561
+ text: String(data.text ?? ""),
562
+ turns: Number(data.turns ?? 0),
563
+ toolCalls: Number(data.tool_calls ?? 0),
564
+ usage,
565
+ structuredOutput: data.structured_output,
566
+ schemaWarnings,
567
+ skillDiagnostics: MeerkatClient.parseSkillDiagnostics(data.skill_diagnostics),
568
+ };
569
+ }
570
+ static parseMcpLiveOpResponse(raw) {
571
+ const sessionId = raw.session_id;
572
+ const operation = raw.operation;
573
+ const status = raw.status;
574
+ const persisted = raw.persisted;
575
+ if (typeof sessionId !== "string" || sessionId.length === 0) {
576
+ throw new MeerkatError("INVALID_RESPONSE", "Invalid mcp response: missing session_id");
577
+ }
578
+ if (operation !== "add" && operation !== "remove" && operation !== "reload") {
579
+ throw new MeerkatError("INVALID_RESPONSE", "Invalid mcp response: invalid operation");
580
+ }
581
+ if (typeof status !== "string" || status.length === 0) {
582
+ throw new MeerkatError("INVALID_RESPONSE", "Invalid mcp response: missing status");
583
+ }
584
+ if (typeof persisted !== "boolean") {
585
+ throw new MeerkatError("INVALID_RESPONSE", "Invalid mcp response: persisted must be boolean");
586
+ }
587
+ return raw;
588
+ }
589
+ static buildCreateParams(prompt, options) {
590
+ const params = { prompt };
591
+ if (!options)
592
+ return params;
593
+ if (options.model)
594
+ params.model = options.model;
595
+ if (options.provider)
596
+ params.provider = options.provider;
597
+ if (options.systemPrompt)
598
+ params.system_prompt = options.systemPrompt;
599
+ if (options.maxTokens)
600
+ params.max_tokens = options.maxTokens;
601
+ if (options.outputSchema)
602
+ params.output_schema = options.outputSchema;
603
+ if (options.structuredOutputRetries != null && options.structuredOutputRetries !== 2) {
604
+ params.structured_output_retries = options.structuredOutputRetries;
605
+ }
606
+ if (options.hooksOverride)
607
+ params.hooks_override = options.hooksOverride;
608
+ if (options.enableBuiltins)
609
+ params.enable_builtins = true;
610
+ if (options.enableShell)
611
+ params.enable_shell = true;
612
+ if (options.enableSubagents)
613
+ params.enable_subagents = true;
614
+ if (options.enableMemory)
615
+ params.enable_memory = true;
616
+ if (options.enableMob)
617
+ params.enable_mob = true;
618
+ if (options.hostMode)
619
+ params.host_mode = true;
620
+ if (options.commsName)
621
+ params.comms_name = options.commsName;
622
+ if (options.peerMeta != null)
623
+ params.peer_meta = options.peerMeta;
624
+ if (options.budgetLimits != null)
625
+ params.budget_limits = options.budgetLimits;
626
+ if (options.providerParams)
627
+ params.provider_params = options.providerParams;
628
+ if (options.preloadSkills != null)
629
+ params.preload_skills = options.preloadSkills;
630
+ const wireRefs = skillRefsToWire(options.skillRefs);
631
+ if (wireRefs)
632
+ params.skill_refs = wireRefs;
633
+ if (options.skillReferences != null)
634
+ params.skill_references = options.skillReferences;
635
+ return params;
636
+ }
637
+ // -- Binary resolution --------------------------------------------------
23
638
  static commandPath(command) {
24
639
  if (path.isAbsolute(command)) {
25
640
  return existsSync(command) ? command : null;
@@ -59,39 +674,20 @@ export class MeerkatClient {
59
674
  const architecture = os.arch();
60
675
  if (process.platform === "darwin") {
61
676
  if (architecture === "arm64") {
62
- return {
63
- target: "aarch64-apple-darwin",
64
- archiveExt: "tar.gz",
65
- binaryName: "rkat-rpc",
66
- };
677
+ return { target: "aarch64-apple-darwin", archiveExt: "tar.gz", binaryName: "rkat-rpc" };
67
678
  }
68
- throw new MeerkatError("UNSUPPORTED_PLATFORM", `Unsupported macOS architecture '${architecture}'. Only Apple Silicon (arm64) is supported.`);
679
+ throw new MeerkatError("UNSUPPORTED_PLATFORM", `Unsupported macOS architecture '${architecture}'.`);
69
680
  }
70
681
  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
- }
682
+ if (architecture === "x64")
683
+ return { target: "x86_64-unknown-linux-gnu", archiveExt: "tar.gz", binaryName: "rkat-rpc" };
684
+ if (architecture === "arm64")
685
+ return { target: "aarch64-unknown-linux-gnu", archiveExt: "tar.gz", binaryName: "rkat-rpc" };
85
686
  throw new MeerkatError("UNSUPPORTED_PLATFORM", `Unsupported Linux architecture '${architecture}'.`);
86
687
  }
87
688
  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
- }
689
+ if (architecture === "x64")
690
+ return { target: "x86_64-pc-windows-msvc", archiveExt: "zip", binaryName: "rkat-rpc.exe" };
95
691
  throw new MeerkatError("UNSUPPORTED_PLATFORM", `Unsupported Windows architecture '${architecture}'.`);
96
692
  }
97
693
  throw new MeerkatError("UNSUPPORTED_PLATFORM", `Unsupported platform '${process.platform}'.`);
@@ -100,12 +696,8 @@ export class MeerkatClient {
100
696
  return new Promise((resolve, reject) => {
101
697
  const proc = spawn(command, args, { stdio: ["ignore", "inherit", "pipe"] });
102
698
  let stderr = "";
103
- proc.stderr?.on("data", (chunk) => {
104
- stderr += String(chunk);
105
- });
106
- proc.on("error", (error) => {
107
- reject(error);
108
- });
699
+ proc.stderr?.on("data", (chunk) => { stderr += String(chunk); });
700
+ proc.on("error", (error) => { reject(error); });
109
701
  proc.on("close", (code) => {
110
702
  if (code === 0) {
111
703
  resolve();
@@ -118,12 +710,10 @@ export class MeerkatClient {
118
710
  static async extractZip(archivePath, destinationDir) {
119
711
  try {
120
712
  await MeerkatClient.runCommand("tar", ["-xf", archivePath, "-C", destinationDir]);
121
- return;
122
713
  }
123
- catch (error) {
714
+ catch {
124
715
  await MeerkatClient.runCommand("powershell", [
125
- "-NoProfile",
126
- "-Command",
716
+ "-NoProfile", "-Command",
127
717
  `Expand-Archive -LiteralPath '${archivePath.replaceAll("'", "''")}' -DestinationPath '${destinationDir.replaceAll("'", "''")}' -Force`,
128
718
  ]);
129
719
  }
@@ -136,9 +726,8 @@ export class MeerkatClient {
136
726
  const baseDir = path.join(MEERKAT_BINARY_CACHE_ROOT, `v${version}`, target);
137
727
  mkdirSync(baseDir, { recursive: true });
138
728
  const cached = path.join(baseDir, binaryName);
139
- if (existsSync(cached)) {
729
+ if (existsSync(cached))
140
730
  return cached;
141
- }
142
731
  const response = await fetch(url);
143
732
  if (!response.ok) {
144
733
  throw new MeerkatError("BINARY_DOWNLOAD_FAILED", `Failed to download binary from ${url} (HTTP ${response.status})`);
@@ -163,9 +752,8 @@ export class MeerkatClient {
163
752
  }
164
753
  catch { /* best-effort cleanup */ }
165
754
  }
166
- if (process.platform !== "win32") {
755
+ if (process.platform !== "win32")
167
756
  chmodSync(cached, 0o755);
168
- }
169
757
  return cached;
170
758
  }
171
759
  static async resolveBinaryPath(requestedPath) {
@@ -173,293 +761,54 @@ export class MeerkatClient {
173
761
  if (overridden) {
174
762
  const candidate = MeerkatClient.resolveCandidatePath(overridden);
175
763
  if (!candidate) {
176
- throw new MeerkatError("BINARY_NOT_FOUND", `Binary not found at MEERKAT_BIN_PATH='${overridden}'. Set it to a valid executable.`);
764
+ throw new MeerkatError("BINARY_NOT_FOUND", `Binary not found at MEERKAT_BIN_PATH='${overridden}'.`);
177
765
  }
178
- return {
179
- command: candidate,
180
- useLegacySubcommand: path.basename(candidate) === "rkat",
181
- };
766
+ return { command: candidate, useLegacySubcommand: path.basename(candidate) === "rkat" };
182
767
  }
183
768
  if (requestedPath !== "rkat-rpc") {
184
769
  const candidate = MeerkatClient.resolveCandidatePath(requestedPath);
185
770
  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.`);
771
+ throw new MeerkatError("BINARY_NOT_FOUND", `Binary not found at '${requestedPath}'.`);
187
772
  }
188
- return {
189
- command: candidate,
190
- useLegacySubcommand: path.basename(candidate) === "rkat",
191
- };
773
+ return { command: candidate, useLegacySubcommand: path.basename(candidate) === "rkat" };
192
774
  }
193
775
  const defaultBinary = MeerkatClient.commandPath("rkat-rpc");
194
- if (defaultBinary) {
195
- return {
196
- command: defaultBinary,
197
- useLegacySubcommand: false,
198
- };
199
- }
776
+ if (defaultBinary)
777
+ return { command: defaultBinary, useLegacySubcommand: false };
200
778
  try {
201
779
  const downloaded = await MeerkatClient.ensureDownloadedBinary();
202
780
  return { command: downloaded, useLegacySubcommand: false };
203
781
  }
204
782
  catch (error) {
205
783
  const legacy = MeerkatClient.commandPath("rkat");
206
- if (legacy) {
784
+ if (legacy)
207
785
  return { command: legacy, useLegacySubcommand: true };
208
- }
209
786
  if (error instanceof MeerkatError)
210
787
  throw error;
211
788
  throw new MeerkatError("BINARY_NOT_FOUND", `Could not find '${MEERKAT_RELEASE_BINARY}' and auto-download failed.`);
212
789
  }
213
790
  }
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;
791
+ static buildArgs(legacy, options) {
792
+ if (legacy)
793
+ return ["rpc"];
794
+ if (!options)
795
+ return [];
220
796
  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
- };
797
+ if (options.isolated)
798
+ args.push("--isolated");
799
+ if (options.realmId)
800
+ args.push("--realm", options.realmId);
801
+ if (options.instanceId)
802
+ args.push("--instance", options.instanceId);
803
+ if (options.realmBackend)
804
+ args.push("--realm-backend", options.realmBackend);
805
+ if (options.stateRoot)
806
+ args.push("--state-root", options.stateRoot);
807
+ if (options.contextRoot)
808
+ args.push("--context-root", options.contextRoot);
809
+ if (options.userConfigRoot)
810
+ args.push("--user-config-root", options.userConfigRoot);
811
+ return args;
463
812
  }
464
813
  }
465
814
  //# sourceMappingURL=client.js.map