@mirasoth/soothe-client 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs ADDED
@@ -0,0 +1,1127 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __esm = (fn, res) => function __init() {
9
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
10
+ };
11
+ var __export = (target, all) => {
12
+ for (var name in all)
13
+ __defProp(target, name, { get: all[name], enumerable: true });
14
+ };
15
+ var __copyProps = (to, from, except, desc) => {
16
+ if (from && typeof from === "object" || typeof from === "function") {
17
+ for (let key of __getOwnPropNames(from))
18
+ if (!__hasOwnProp.call(to, key) && key !== except)
19
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
20
+ }
21
+ return to;
22
+ };
23
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
24
+ // If the importer is in node compatibility mode or this is not an ESM
25
+ // file that has been converted to a CommonJS file using a Babel-
26
+ // compatible transform (i.e. "__esModule" has not been set), then set
27
+ // "default" to the CommonJS "module.exports" for node compatibility.
28
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
29
+ mod
30
+ ));
31
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
32
+
33
+ // src/config.ts
34
+ function defaultConfig() {
35
+ return {
36
+ daemonURL: "ws://localhost:8765",
37
+ verbosityLevel: "normal",
38
+ maxRetries: 5,
39
+ reconnectDelay: 2e3,
40
+ heartbeatInterval: 3e4,
41
+ daemonReadyTimeout: 2e4,
42
+ loopStatusTimeout: 6e4,
43
+ subscriptionTimeout: 1e4
44
+ };
45
+ }
46
+ function loadConfigFromEnv() {
47
+ const config = defaultConfig();
48
+ if (typeof process === "undefined") return config;
49
+ const url = process.env.SOOTHE_DAEMON_URL;
50
+ if (url) config.daemonURL = url;
51
+ const verbosity = process.env.SOOTHE_VERBOSITY;
52
+ if (verbosity) config.verbosityLevel = verbosity;
53
+ const retries = process.env.SOOTHE_MAX_RETRIES;
54
+ if (retries) {
55
+ const val = parseInt(retries, 10);
56
+ if (!isNaN(val)) config.maxRetries = val;
57
+ }
58
+ const readyTimeout = process.env.SOOTHE_DAEMON_READY_TIMEOUT_SEC;
59
+ if (readyTimeout) {
60
+ const val = parseInt(readyTimeout, 10);
61
+ if (val > 0) config.daemonReadyTimeout = val * 1e3;
62
+ }
63
+ const statusTimeout = process.env.SOOTHE_LOOP_STATUS_TIMEOUT_SEC;
64
+ if (statusTimeout) {
65
+ const val = parseInt(statusTimeout, 10);
66
+ if (val > 0) config.loopStatusTimeout = val * 1e3;
67
+ }
68
+ const subTimeout = process.env.SOOTHE_SUBSCRIPTION_TIMEOUT_SEC;
69
+ if (subTimeout) {
70
+ const val = parseInt(subTimeout, 10);
71
+ if (val > 0) config.subscriptionTimeout = val * 1e3;
72
+ }
73
+ return config;
74
+ }
75
+ var init_config = __esm({
76
+ "src/config.ts"() {
77
+ "use strict";
78
+ }
79
+ });
80
+
81
+ // src/protocol.ts
82
+ function encodeMessage(msg) {
83
+ return JSON.stringify(msg) + "\n";
84
+ }
85
+ function decodeMessage(data) {
86
+ if (!data || data.length === 0) return null;
87
+ let parsed;
88
+ try {
89
+ parsed = JSON.parse(data);
90
+ } catch {
91
+ throw new Error(`invalid JSON: ${data}`);
92
+ }
93
+ const type = parsed.type;
94
+ if (!type) return parsed;
95
+ switch (type) {
96
+ // Client → Daemon (loop-first)
97
+ case "loop_input":
98
+ return { ...parsed };
99
+ case "command":
100
+ return { ...parsed };
101
+ case "daemon_status":
102
+ return { ...parsed };
103
+ case "daemon_shutdown":
104
+ return { ...parsed };
105
+ case "config_get":
106
+ return { ...parsed };
107
+ case "loop_new":
108
+ return { ...parsed };
109
+ case "loop_subscribe":
110
+ return { ...parsed };
111
+ case "loop_detach":
112
+ return { ...parsed };
113
+ case "loop_list":
114
+ return { ...parsed };
115
+ case "loop_get":
116
+ return { ...parsed };
117
+ case "loop_tree":
118
+ return { ...parsed };
119
+ case "loop_prune":
120
+ return { ...parsed };
121
+ case "loop_delete":
122
+ return { ...parsed };
123
+ case "loop_reattach":
124
+ return { ...parsed };
125
+ case "skills_list":
126
+ return { ...parsed };
127
+ case "models_list":
128
+ return { ...parsed };
129
+ case "invoke_skill":
130
+ return { ...parsed };
131
+ case "detach":
132
+ return { ...parsed };
133
+ // Daemon → Client
134
+ case "event":
135
+ return { ...parsed };
136
+ case "status": {
137
+ const msg = { ...parsed };
138
+ if (!msg.loop_id && parsed.loopId && typeof parsed.loopId === "string") {
139
+ msg.loop_id = parsed.loopId;
140
+ }
141
+ return msg;
142
+ }
143
+ case "subscription_confirmed":
144
+ return { ...parsed };
145
+ case "error":
146
+ return { ...parsed };
147
+ case "daemon_ready":
148
+ return { ...parsed };
149
+ case "daemon_status_response":
150
+ return { ...parsed };
151
+ case "shutdown_ack":
152
+ return { ...parsed };
153
+ case "loop_new_response":
154
+ return { ...parsed };
155
+ case "loop_subscribe_response":
156
+ return { ...parsed };
157
+ case "loop_detach_response":
158
+ return { ...parsed };
159
+ case "loop_list_response":
160
+ return { ...parsed };
161
+ case "loop_get_response":
162
+ return { ...parsed };
163
+ case "loop_tree_response":
164
+ return { ...parsed };
165
+ case "loop_prune_response":
166
+ return { ...parsed };
167
+ case "loop_delete_response":
168
+ return { ...parsed };
169
+ case "loop_reattach_response":
170
+ return { ...parsed };
171
+ case "history_replay":
172
+ return { ...parsed };
173
+ case "history_replay_complete":
174
+ case "replay_complete":
175
+ return { ...parsed };
176
+ case "loop_reattached":
177
+ return { ...parsed };
178
+ case "config_get_response":
179
+ case "invoke_skill_response":
180
+ return parsed;
181
+ case "skills_list_response":
182
+ return { ...parsed };
183
+ case "models_list_response":
184
+ return { ...parsed };
185
+ default:
186
+ return parsed;
187
+ }
188
+ }
189
+ function splitWirePayload(data) {
190
+ const trimmed = data.trim();
191
+ if (trimmed === "") return [];
192
+ const lines = trimmed.split("\n").map((l) => l.trim()).filter((l) => l !== "");
193
+ return lines.length > 0 ? lines : [data];
194
+ }
195
+ function extractSootheLoopID(msg) {
196
+ if (!msg || typeof msg !== "object") return ["", false];
197
+ const m = msg;
198
+ if (m.type === "status") {
199
+ const id = m.loop_id;
200
+ if (id && id !== "") return [id, true];
201
+ return ["", false];
202
+ }
203
+ if (m.type === "event") {
204
+ const top = m.loop_id;
205
+ if (top && top !== "") return [top, true];
206
+ const data = m.data;
207
+ if (data && typeof data === "object") {
208
+ const dataId = data["loop_id"] ?? data["loopId"];
209
+ if (dataId && dataId !== "") return [dataId, true];
210
+ }
211
+ }
212
+ const generic = m["loop_id"] ?? m["loopId"];
213
+ if (generic && generic !== "") return [generic, true];
214
+ return ["", false];
215
+ }
216
+ function newRequestID() {
217
+ return (0, import_node_crypto.randomUUID)();
218
+ }
219
+ function newLoopInputMessage(loopID, content) {
220
+ return {
221
+ request_id: newRequestID(),
222
+ type: "loop_input",
223
+ loop_id: loopID,
224
+ content,
225
+ autonomous: false
226
+ };
227
+ }
228
+ function newLoopNewMessage(opts) {
229
+ const options = typeof opts === "string" ? { client_workspace: opts } : opts ?? {};
230
+ const clientWorkspace = options.client_workspace ?? options.workspace;
231
+ const msg = {
232
+ request_id: newRequestID(),
233
+ type: "loop_new"
234
+ };
235
+ if (clientWorkspace?.trim()) {
236
+ msg.client_workspace = clientWorkspace.trim();
237
+ }
238
+ if (options.user_id?.trim()) {
239
+ msg.user_id = options.user_id.trim();
240
+ }
241
+ if (options.client_workspace_id?.trim()) {
242
+ msg.client_workspace_id = options.client_workspace_id.trim();
243
+ }
244
+ if (options.is_ephemeral) {
245
+ msg.is_ephemeral = true;
246
+ }
247
+ return msg;
248
+ }
249
+ function newLoopSubscribeMessage(loopID, verbosity, streamDelivery) {
250
+ const msg = {
251
+ request_id: newRequestID(),
252
+ type: "loop_subscribe",
253
+ loop_id: loopID,
254
+ verbosity
255
+ };
256
+ if (streamDelivery) {
257
+ msg.stream_delivery = streamDelivery;
258
+ }
259
+ return msg;
260
+ }
261
+ var import_node_crypto;
262
+ var init_protocol = __esm({
263
+ "src/protocol.ts"() {
264
+ "use strict";
265
+ import_node_crypto = require("crypto");
266
+ }
267
+ });
268
+
269
+ // src/client.ts
270
+ var client_exports = {};
271
+ __export(client_exports, {
272
+ Client: () => Client
273
+ });
274
+ var import_node_events, import_ws, Client;
275
+ var init_client = __esm({
276
+ "src/client.ts"() {
277
+ "use strict";
278
+ import_node_events = require("events");
279
+ import_ws = __toESM(require("ws"), 1);
280
+ init_config();
281
+ init_protocol();
282
+ Client = class extends import_node_events.EventEmitter {
283
+ url;
284
+ config;
285
+ ws = null;
286
+ messageBuffer = [];
287
+ resolvers = [];
288
+ constructor(url, config) {
289
+ super();
290
+ this.url = url;
291
+ this.config = config ?? defaultConfig();
292
+ }
293
+ // ---------------------------------------------------------------------------
294
+ // Connection lifecycle
295
+ // ---------------------------------------------------------------------------
296
+ /** Dials the Soothe daemon WebSocket. No WS-level ping/pong (RFC-0013). */
297
+ connect() {
298
+ return new Promise((resolve, reject) => {
299
+ const ws = new import_ws.default(this.url, {
300
+ handshakeTimeout: 1e4
301
+ });
302
+ ws.on("open", () => {
303
+ this.ws = ws;
304
+ resolve();
305
+ });
306
+ ws.on("error", (err) => {
307
+ if (!this.ws) {
308
+ reject(new Error(`soothe dial: ${err.message}`));
309
+ }
310
+ });
311
+ ws.on("message", (data) => {
312
+ const text = data.toString();
313
+ for (const frame of splitWirePayload(text)) {
314
+ try {
315
+ const msg = decodeMessage(frame);
316
+ if (msg !== null) {
317
+ this.messageBuffer.push(msg);
318
+ this.emit("message", msg);
319
+ const resolver = this.resolvers.shift();
320
+ if (resolver) resolver(msg);
321
+ }
322
+ } catch {
323
+ }
324
+ }
325
+ });
326
+ ws.on("close", () => {
327
+ this.ws = null;
328
+ this.emit("close");
329
+ for (const resolver of this.resolvers) {
330
+ resolver(null);
331
+ }
332
+ this.resolvers = [];
333
+ });
334
+ });
335
+ }
336
+ /** Shuts down the WebSocket connection. */
337
+ close() {
338
+ if (!this.ws) return;
339
+ try {
340
+ this.ws.close(1e3, "");
341
+ } catch {
342
+ }
343
+ this.ws = null;
344
+ }
345
+ /** Returns whether the client has an active WebSocket connection. */
346
+ isConnected() {
347
+ return this.ws !== null && this.ws.readyState === import_ws.default.OPEN;
348
+ }
349
+ // ---------------------------------------------------------------------------
350
+ // Core messaging
351
+ // ---------------------------------------------------------------------------
352
+ /** Serializes msg as JSON and sends it as a WebSocket text frame. */
353
+ sendMessage(msg) {
354
+ return new Promise((resolve, reject) => {
355
+ if (!this.ws) {
356
+ reject(new Error("soothe: not connected"));
357
+ return;
358
+ }
359
+ const payload = JSON.stringify(msg);
360
+ this.ws.send(payload, (err) => {
361
+ if (err) reject(err);
362
+ else resolve();
363
+ });
364
+ });
365
+ }
366
+ /** Returns an async iterable of decoded messages. Ends when connection closes. */
367
+ async *receiveMessages(signal) {
368
+ while (true) {
369
+ if (signal?.aborted) return;
370
+ while (this.messageBuffer.length > 0) {
371
+ const msg2 = this.messageBuffer.shift();
372
+ yield msg2;
373
+ }
374
+ const msg = await new Promise((resolve) => {
375
+ if (!this.ws) {
376
+ resolve(null);
377
+ return;
378
+ }
379
+ this.resolvers.push(resolve);
380
+ });
381
+ if (msg === null) return;
382
+ yield msg;
383
+ }
384
+ }
385
+ /** Reads a single event from the daemon. Returns null on connection close. */
386
+ async readEvent() {
387
+ if (this.messageBuffer.length > 0) {
388
+ const msg2 = this.messageBuffer.shift();
389
+ return msg2;
390
+ }
391
+ if (!this.ws) return null;
392
+ const msg = await new Promise((resolve) => {
393
+ this.resolvers.push(resolve);
394
+ });
395
+ if (msg === null) return null;
396
+ return msg;
397
+ }
398
+ /** Reads a single event with a timeout. Returns null on timeout or connection close. */
399
+ readEventWithTimeout(timeout) {
400
+ if (this.messageBuffer.length > 0) {
401
+ const msg = this.messageBuffer.shift();
402
+ return Promise.resolve(msg);
403
+ }
404
+ if (!this.ws) return Promise.resolve(null);
405
+ return new Promise((resolve) => {
406
+ const timer = setTimeout(() => {
407
+ const idx = this.resolvers.indexOf(resolver);
408
+ if (idx >= 0) this.resolvers.splice(idx, 1);
409
+ resolve(null);
410
+ }, timeout);
411
+ const resolver = (val) => {
412
+ clearTimeout(timer);
413
+ resolve(val);
414
+ };
415
+ this.resolvers.push(resolver);
416
+ });
417
+ }
418
+ // ---------------------------------------------------------------------------
419
+ // High-level API methods (Loop-first, RFC-503)
420
+ // ---------------------------------------------------------------------------
421
+ /** Sends user input to the daemon (loop_input; requires loopID). */
422
+ sendInput(text, options) {
423
+ const loopId = (options?.loopID ?? "").trim();
424
+ if (!loopId) {
425
+ return Promise.reject(new Error("sendInput requires options.loopID"));
426
+ }
427
+ const payload = {
428
+ type: "loop_input",
429
+ loop_id: loopId,
430
+ content: text,
431
+ autonomous: options?.autonomous ?? false
432
+ };
433
+ if (options?.maxIterations !== void 0) payload.max_iterations = options.maxIterations;
434
+ if (options?.subagent) payload.preferred_subagent = options.subagent;
435
+ if (options?.interactive) payload.interactive = true;
436
+ if (options?.model) payload.model = options.model;
437
+ if (options?.modelParams) payload.model_params = options.modelParams;
438
+ if (options?.attachments) payload.attachments = options.attachments;
439
+ return this.sendMessage(payload);
440
+ }
441
+ /** Sends a slash command to the daemon. */
442
+ sendCommand(cmd) {
443
+ return this.sendMessage({ type: "command", cmd });
444
+ }
445
+ // ---------------------------------------------------------------------------
446
+ // Loop lifecycle methods (RFC-503)
447
+ // ---------------------------------------------------------------------------
448
+ /** Requests the daemon to create a new AgentLoop. */
449
+ sendLoopNew(opts) {
450
+ return this.sendMessage(newLoopNewMessage(opts));
451
+ }
452
+ /** Subscribes to events for a loop. */
453
+ sendLoopSubscribe(loopID, verbosity, streamDelivery) {
454
+ const msg = newLoopSubscribeMessage(loopID, verbosity);
455
+ if (streamDelivery) {
456
+ msg.stream_delivery = streamDelivery;
457
+ }
458
+ return this.sendMessage(msg);
459
+ }
460
+ /** Detaches from a loop (keeps loop running). */
461
+ sendLoopDetach(loopID, requestID) {
462
+ return this.sendMessage({
463
+ type: "loop_detach",
464
+ loop_id: loopID,
465
+ request_id: requestID ?? newRequestID()
466
+ });
467
+ }
468
+ /** Notifies the daemon that this client is detaching. */
469
+ sendDetach() {
470
+ return this.sendMessage({ type: "detach" });
471
+ }
472
+ /** Sends the daemon_ready handshake message. */
473
+ sendDaemonReady() {
474
+ return this.sendMessage({ type: "daemon_ready" });
475
+ }
476
+ /** Requests daemon status check. */
477
+ sendDaemonStatus(requestID) {
478
+ return this.sendMessage({
479
+ type: "daemon_status",
480
+ request_id: requestID ?? newRequestID()
481
+ });
482
+ }
483
+ /** Requests daemon shutdown. */
484
+ sendDaemonShutdown(requestID) {
485
+ return this.sendMessage({
486
+ type: "daemon_shutdown",
487
+ request_id: requestID ?? newRequestID()
488
+ });
489
+ }
490
+ /** Requests a config section from the daemon. */
491
+ sendConfigGet(section, requestID) {
492
+ return this.sendMessage({
493
+ type: "config_get",
494
+ section,
495
+ request_id: requestID ?? newRequestID()
496
+ });
497
+ }
498
+ // ---------------------------------------------------------------------------
499
+ // Loop management RPC methods (RFC-504)
500
+ // ---------------------------------------------------------------------------
501
+ /** Requests the persisted loop list. */
502
+ sendLoopList(filter, limit, requestID) {
503
+ const msg = {
504
+ type: "loop_list",
505
+ request_id: requestID ?? newRequestID()
506
+ };
507
+ if (filter) msg.filter = filter;
508
+ if (limit !== void 0) msg.limit = limit;
509
+ return this.sendMessage(msg);
510
+ }
511
+ /** Requests detailed loop metadata. */
512
+ sendLoopGet(loopID, verbose, requestID) {
513
+ const msg = {
514
+ type: "loop_get",
515
+ loop_id: loopID,
516
+ request_id: requestID ?? newRequestID()
517
+ };
518
+ if (verbose) msg.verbose = verbose;
519
+ return this.sendMessage(msg);
520
+ }
521
+ /** Requests loop tree visualization. */
522
+ sendLoopTree(loopID, format, requestID) {
523
+ const msg = {
524
+ type: "loop_tree",
525
+ loop_id: loopID,
526
+ request_id: requestID ?? newRequestID()
527
+ };
528
+ if (format) msg.format = format;
529
+ return this.sendMessage(msg);
530
+ }
531
+ /** Requests pruning of old failed branches. */
532
+ sendLoopPrune(loopID, retentionDays, dryRun, requestID) {
533
+ const msg = {
534
+ type: "loop_prune",
535
+ loop_id: loopID,
536
+ request_id: requestID ?? newRequestID()
537
+ };
538
+ if (retentionDays !== void 0) msg.retention_days = retentionDays;
539
+ if (dryRun !== void 0) msg.dry_run = dryRun;
540
+ return this.sendMessage(msg);
541
+ }
542
+ /** Requests loop deletion. */
543
+ sendLoopDelete(loopID, requestID) {
544
+ return this.sendMessage({
545
+ type: "loop_delete",
546
+ loop_id: loopID,
547
+ request_id: requestID ?? newRequestID()
548
+ });
549
+ }
550
+ /** Requests reattachment to a loop with history replay. */
551
+ sendLoopReattach(loopID, requestID) {
552
+ return this.sendMessage({
553
+ type: "loop_reattach",
554
+ loop_id: loopID,
555
+ request_id: requestID ?? newRequestID()
556
+ });
557
+ }
558
+ // ---------------------------------------------------------------------------
559
+ // Skills and models
560
+ // ---------------------------------------------------------------------------
561
+ /** Requests the skills catalog (RFC-400). */
562
+ sendSkillsList(requestID) {
563
+ return this.sendMessage({
564
+ type: "skills_list",
565
+ request_id: requestID ?? newRequestID()
566
+ });
567
+ }
568
+ /** Requests the models catalog (RFC-400). */
569
+ sendModelsList(requestID) {
570
+ return this.sendMessage({
571
+ type: "models_list",
572
+ request_id: requestID ?? newRequestID()
573
+ });
574
+ }
575
+ /** Invokes a skill on the daemon (RFC-400). */
576
+ sendInvokeSkill(skill, args, requestID) {
577
+ const msg = {
578
+ type: "invoke_skill",
579
+ skill,
580
+ request_id: requestID ?? newRequestID()
581
+ };
582
+ if (args) msg.args = args;
583
+ return this.sendMessage(msg);
584
+ }
585
+ // ---------------------------------------------------------------------------
586
+ // Request-Response pattern
587
+ // ---------------------------------------------------------------------------
588
+ /** Sends a request with a unique request_id and waits for a matching response. */
589
+ async requestResponse(payload, responseType, timeout) {
590
+ const rid = newRequestID();
591
+ payload.request_id = rid;
592
+ await this.sendMessage(payload);
593
+ const deadline = Date.now() + timeout;
594
+ while (Date.now() < deadline) {
595
+ const remaining = deadline - Date.now();
596
+ if (remaining <= 0) break;
597
+ const ev = await this.readEventWithTimeout(remaining);
598
+ if (ev === null) {
599
+ break;
600
+ }
601
+ const evRid = ev.request_id;
602
+ if (evRid !== rid) continue;
603
+ const typ = ev.type;
604
+ if (typ === "error") {
605
+ const msg = ev.message ?? "unknown error";
606
+ throw new Error(`daemon error: ${msg}`);
607
+ }
608
+ if (typ === responseType) {
609
+ return ev;
610
+ }
611
+ }
612
+ throw new Error(`timeout after ${timeout}ms waiting for ${responseType}`);
613
+ }
614
+ // ---------------------------------------------------------------------------
615
+ // Convenience RPC methods
616
+ // ---------------------------------------------------------------------------
617
+ /** Requests the skills catalog and waits for the response. */
618
+ listSkills(timeout) {
619
+ return this.requestResponse({ type: "skills_list" }, "skills_list_response", timeout ?? 15e3);
620
+ }
621
+ /** Requests the models catalog and waits for the response. */
622
+ listModels(timeout) {
623
+ return this.requestResponse({ type: "models_list" }, "models_list_response", timeout ?? 15e3);
624
+ }
625
+ /** Invokes a skill on the daemon host and receives echo (RFC-400). */
626
+ invokeSkill(skill, args, timeout) {
627
+ return this.requestResponse({ type: "invoke_skill", skill, args }, "invoke_skill_response", timeout ?? 12e4);
628
+ }
629
+ /** Requests loop list and waits for response. */
630
+ listLoops(timeout) {
631
+ return this.requestResponse({ type: "loop_list" }, "loop_list_response", timeout ?? 15e3);
632
+ }
633
+ /** Requests loop details and waits for response. */
634
+ getLoop(loopID, timeout) {
635
+ return this.requestResponse({ type: "loop_get", loop_id: loopID }, "loop_get_response", timeout ?? 15e3);
636
+ }
637
+ /** Requests loop tree and waits for response. */
638
+ getLoopTree(loopID, timeout) {
639
+ return this.requestResponse({ type: "loop_tree", loop_id: loopID }, "loop_tree_response", timeout ?? 15e3);
640
+ }
641
+ /** Requests loop deletion and waits for response. */
642
+ deleteLoop(loopID, timeout) {
643
+ return this.requestResponse({ type: "loop_delete", loop_id: loopID }, "loop_delete_response", timeout ?? 15e3);
644
+ }
645
+ // ---------------------------------------------------------------------------
646
+ // Wait helpers
647
+ // ---------------------------------------------------------------------------
648
+ /** Reads events until a daemon_ready with state == "ready". */
649
+ async waitForDaemonReady(timeout) {
650
+ const t = timeout ?? 1e4;
651
+ const deadline = Date.now() + t;
652
+ while (Date.now() < deadline) {
653
+ const remaining = deadline - Date.now();
654
+ if (remaining <= 0) break;
655
+ const ev = await this.readEventWithTimeout(remaining);
656
+ if (ev === null) break;
657
+ if (ev.type !== "daemon_ready") continue;
658
+ if (ev.state === "ready") return ev;
659
+ const msg = ev.message ?? `daemon state is ${ev.state}`;
660
+ throw new Error(`daemon not ready: ${msg}`);
661
+ }
662
+ throw new Error(`timeout after ${t}ms waiting for daemon_ready`);
663
+ }
664
+ /** Waits for subscription confirmation matching loop id. */
665
+ async waitForSubscriptionConfirmed(loopID, _verbosity, timeout) {
666
+ const t = timeout ?? 5e3;
667
+ const deadline = Date.now() + t;
668
+ while (Date.now() < deadline) {
669
+ const remaining = deadline - Date.now();
670
+ if (remaining <= 0) break;
671
+ const ev = await this.readEventWithTimeout(remaining);
672
+ if (ev === null) break;
673
+ if (ev.type === "loop_subscribe_response" && ev.success === true) {
674
+ if (String(ev.loop_id ?? "") === loopID) return;
675
+ continue;
676
+ }
677
+ if (ev.type !== "subscription_confirmed") continue;
678
+ const lid = String(ev.loop_id ?? "");
679
+ if (lid === loopID) return;
680
+ }
681
+ throw new Error(`timeout after ${t}ms waiting for subscription_confirmed`);
682
+ }
683
+ };
684
+ }
685
+ });
686
+
687
+ // src/index.ts
688
+ var index_exports = {};
689
+ __export(index_exports, {
690
+ Client: () => Client,
691
+ ConnectionError: () => ConnectionError,
692
+ DaemonError: () => DaemonError,
693
+ ESSENTIAL_EVENT_TYPES: () => ESSENTIAL_EVENT_TYPES,
694
+ EventAgentLoopCompleted: () => EventAgentLoopCompleted,
695
+ EventAgentLoopIterated: () => EventAgentLoopIterated,
696
+ EventAgentLoopReasoned: () => EventAgentLoopReasoned,
697
+ EventAgentLoopStarted: () => EventAgentLoopStarted,
698
+ EventExploreCompleted: () => EventExploreCompleted,
699
+ EventExploreMilestone: () => EventExploreMilestone,
700
+ EventExploreStarted: () => EventExploreStarted,
701
+ EventExploreStepCompleted: () => EventExploreStepCompleted,
702
+ EventFinalReport: () => EventFinalReport,
703
+ EventGeneralFailed: () => EventGeneralFailed,
704
+ EventLoopReattachedWire: () => EventLoopReattachedWire,
705
+ EventMessageReceived: () => EventMessageReceived,
706
+ EventMessageSent: () => EventMessageSent,
707
+ EventPlanCreated: () => EventPlanCreated,
708
+ EventReplayComplete: () => EventReplayComplete,
709
+ EventStreamToolCallUpdate: () => EventStreamToolCallUpdate,
710
+ EventTacitusCompleted: () => EventTacitusCompleted,
711
+ EventTacitusGatherSummary: () => EventTacitusGatherSummary,
712
+ EventTacitusStarted: () => EventTacitusStarted,
713
+ EventToolCallUpdatesBatch: () => EventToolCallUpdatesBatch,
714
+ EventToolCompleted: () => EventToolCompleted,
715
+ EventToolError: () => EventToolError,
716
+ EventToolStarted: () => EventToolStarted,
717
+ TimeoutError: () => TimeoutError,
718
+ VerbosityTier: () => VerbosityTier,
719
+ bootstrapLoopSession: () => bootstrapLoopSession,
720
+ checkDaemonStatus: () => checkDaemonStatus,
721
+ classifyEventVerbosity: () => classifyEventVerbosity,
722
+ connectWithRetries: () => connectWithRetries,
723
+ decodeMessage: () => decodeMessage,
724
+ defaultConfig: () => defaultConfig,
725
+ encodeMessage: () => encodeMessage,
726
+ extractSootheLoopID: () => extractSootheLoopID,
727
+ fetchConfigSection: () => fetchConfigSection,
728
+ fetchSkillsCatalog: () => fetchSkillsCatalog,
729
+ isCompletionEvent: () => isCompletionEvent,
730
+ isDaemonLive: () => isDaemonLive,
731
+ isSubagentProgressEvent: () => isSubagentProgressEvent,
732
+ isValidVerbosityLevel: () => isValidVerbosityLevel,
733
+ loadConfigFromEnv: () => loadConfigFromEnv,
734
+ newLoopInputMessage: () => newLoopInputMessage,
735
+ newLoopNewMessage: () => newLoopNewMessage,
736
+ newLoopSubscribeMessage: () => newLoopSubscribeMessage,
737
+ newRequestID: () => newRequestID,
738
+ parseNamespace: () => parseNamespace,
739
+ requestDaemonShutdown: () => requestDaemonShutdown,
740
+ shouldShow: () => shouldShow,
741
+ splitWirePayload: () => splitWirePayload,
742
+ waitDaemonReady: () => waitDaemonReady,
743
+ waitLoopStatusWithID: () => waitLoopStatusWithID,
744
+ waitSubscriptionConfirmed: () => waitSubscriptionConfirmed
745
+ });
746
+ module.exports = __toCommonJS(index_exports);
747
+
748
+ // src/errors.ts
749
+ var ConnectionError = class extends Error {
750
+ url;
751
+ attempt;
752
+ cause;
753
+ constructor(url, attempt, cause) {
754
+ super(`connection error to ${url} (attempt ${attempt}): ${cause.message}`);
755
+ this.name = "ConnectionError";
756
+ this.url = url;
757
+ this.attempt = attempt;
758
+ this.cause = cause;
759
+ }
760
+ };
761
+ var DaemonError = class extends Error {
762
+ code;
763
+ /** The daemon's error message text. */
764
+ daemonMessage;
765
+ constructor(code, message) {
766
+ super(`daemon error [${code}]: ${message}`);
767
+ this.name = "DaemonError";
768
+ this.code = code;
769
+ this.daemonMessage = message;
770
+ }
771
+ };
772
+ var TimeoutError = class extends Error {
773
+ operation;
774
+ duration;
775
+ constructor(operation, duration) {
776
+ super(`timeout after ${duration} waiting for ${operation}`);
777
+ this.name = "TimeoutError";
778
+ this.operation = operation;
779
+ this.duration = duration;
780
+ }
781
+ };
782
+
783
+ // src/verbosity.ts
784
+ var VerbosityTier = /* @__PURE__ */ ((VerbosityTier2) => {
785
+ VerbosityTier2[VerbosityTier2["Quiet"] = 0] = "Quiet";
786
+ VerbosityTier2[VerbosityTier2["Normal"] = 1] = "Normal";
787
+ VerbosityTier2[VerbosityTier2["Detailed"] = 2] = "Detailed";
788
+ VerbosityTier2[VerbosityTier2["Debug"] = 3] = "Debug";
789
+ VerbosityTier2[VerbosityTier2["Internal"] = 99] = "Internal";
790
+ return VerbosityTier2;
791
+ })(VerbosityTier || {});
792
+ var verbosityLevelValues = {
793
+ quiet: 0,
794
+ normal: 1,
795
+ debug: 3
796
+ };
797
+ function shouldShow(tier, verbosity) {
798
+ if (tier === 99 /* Internal */) {
799
+ return false;
800
+ }
801
+ const level = verbosityLevelValues[verbosity] ?? 1;
802
+ return tier <= level;
803
+ }
804
+ function isValidVerbosityLevel(s) {
805
+ return s in verbosityLevelValues;
806
+ }
807
+
808
+ // src/index.ts
809
+ init_config();
810
+ init_protocol();
811
+
812
+ // src/events.ts
813
+ var EventPlanCreated = "soothe.cognition.plan.created";
814
+ var EventExploreStarted = "soothe.subagent.explore.started";
815
+ var EventExploreMilestone = "soothe.subagent.explore.milestone";
816
+ var EventExploreStepCompleted = "soothe.subagent.explore.step.completed";
817
+ var EventExploreCompleted = "soothe.subagent.explore.completed";
818
+ var EventTacitusStarted = "soothe.subagent.tacitus.started";
819
+ var EventTacitusGatherSummary = "soothe.subagent.tacitus.gather.summary";
820
+ var EventTacitusCompleted = "soothe.subagent.tacitus.completed";
821
+ var EventReplayComplete = "replay_complete";
822
+ var EventLoopReattachedWire = "loop_reattached";
823
+ var EventToolStarted = "soothe.tool.execution.started";
824
+ var EventToolCompleted = "soothe.tool.execution.completed";
825
+ var EventToolError = "soothe.tool.execution.error";
826
+ var EventStreamToolCallUpdate = "soothe.stream.tool_call.update";
827
+ var EventToolCallUpdatesBatch = "tool_call_updates_batch";
828
+ var EventAgentLoopStarted = "soothe.cognition.agent_loop.started";
829
+ var EventAgentLoopIterated = "soothe.cognition.agent_loop.iterated";
830
+ var EventAgentLoopCompleted = "soothe.cognition.agent_loop.completed";
831
+ var EventAgentLoopReasoned = "soothe.cognition.agent_loop.reasoned";
832
+ var EventMessageReceived = "soothe.protocol.message.received";
833
+ var EventMessageSent = "soothe.protocol.message.sent";
834
+ var EventFinalReport = "soothe.output.autonomous.final_report.reported";
835
+ var EventGeneralFailed = "soothe.error.general.failed";
836
+ function parseNamespace(ns) {
837
+ const parts = splitNamespace(ns);
838
+ if (parts.length < 4 || parts[0] !== "soothe") {
839
+ return null;
840
+ }
841
+ if (parts[1] === "internal") {
842
+ return null;
843
+ }
844
+ return { domain: parts[1], component: parts[2], action: parts[3] };
845
+ }
846
+ function splitNamespace(ns) {
847
+ const parts = [];
848
+ let start = 0;
849
+ for (let i = 0; i < ns.length; i++) {
850
+ if (ns[i] === ".") {
851
+ parts.push(ns.slice(start, i));
852
+ start = i + 1;
853
+ }
854
+ }
855
+ parts.push(ns.slice(start));
856
+ return parts;
857
+ }
858
+ function classifyEventVerbosity(eventTypeOrNamespace) {
859
+ const parsed = parseNamespace(eventTypeOrNamespace);
860
+ if (!parsed) {
861
+ return classifyByEventTypeString(eventTypeOrNamespace);
862
+ }
863
+ return classifyByDomainAndComponent(parsed.domain, parsed.component, eventTypeOrNamespace);
864
+ }
865
+ function classifyByDomainAndComponent(domain, _component, full) {
866
+ switch (domain) {
867
+ case "cognition":
868
+ return 1 /* Normal */;
869
+ case "protocol":
870
+ return 2 /* Detailed */;
871
+ case "tool":
872
+ return 99 /* Internal */;
873
+ case "subagent":
874
+ return classifySubagentEvent(full);
875
+ case "output":
876
+ case "error":
877
+ return 0 /* Quiet */;
878
+ default:
879
+ return 1 /* Normal */;
880
+ }
881
+ }
882
+ function classifySubagentEvent(full) {
883
+ const parsed = parseNamespace(full);
884
+ if (!parsed) return 1 /* Normal */;
885
+ switch (parsed.action) {
886
+ case "started":
887
+ case "completed":
888
+ return 1 /* Normal */;
889
+ default:
890
+ return 2 /* Detailed */;
891
+ }
892
+ }
893
+ function classifyByEventTypeString(eventType) {
894
+ if (eventType === EventFinalReport || eventType === EventGeneralFailed) {
895
+ return 0 /* Quiet */;
896
+ }
897
+ if (eventType === EventToolStarted) {
898
+ return 99 /* Internal */;
899
+ }
900
+ return 1 /* Normal */;
901
+ }
902
+ function isCompletionEvent(eventType) {
903
+ return eventType.endsWith(".completed") || eventType.endsWith(".failed") || eventType === EventGeneralFailed;
904
+ }
905
+ function isSubagentProgressEvent(eventType) {
906
+ const parsed = parseNamespace(eventType);
907
+ if (!parsed || parsed.domain !== "subagent") {
908
+ return false;
909
+ }
910
+ return parsed.action === "started" || parsed.action === "completed";
911
+ }
912
+ var ESSENTIAL_EVENT_TYPES = /* @__PURE__ */ new Set([
913
+ EventAgentLoopStarted,
914
+ EventAgentLoopCompleted,
915
+ EventAgentLoopReasoned,
916
+ EventPlanCreated,
917
+ EventExploreStarted,
918
+ EventExploreCompleted,
919
+ EventTacitusStarted,
920
+ EventTacitusCompleted,
921
+ EventGeneralFailed
922
+ ]);
923
+
924
+ // src/index.ts
925
+ init_client();
926
+
927
+ // src/helpers.ts
928
+ init_config();
929
+ async function checkDaemonStatus(client, timeout) {
930
+ return client.requestResponse({ type: "daemon_status" }, "daemon_status_response", timeout ?? 5e3);
931
+ }
932
+ async function isDaemonLive(wsURL, timeout) {
933
+ const { Client: Client2 } = await Promise.resolve().then(() => (init_client(), client_exports));
934
+ const t = timeout ?? 5e3;
935
+ const client = new Client2(wsURL, defaultConfig());
936
+ try {
937
+ await client.connect();
938
+ } catch {
939
+ return false;
940
+ }
941
+ try {
942
+ await checkDaemonStatus(client, t);
943
+ return true;
944
+ } catch {
945
+ return false;
946
+ } finally {
947
+ client.close();
948
+ }
949
+ }
950
+ async function requestDaemonShutdown(client, timeout) {
951
+ const resp = await client.requestResponse({ type: "daemon_shutdown" }, "shutdown_ack", timeout ?? 1e4);
952
+ if (resp.status !== "acknowledged") {
953
+ throw new Error(`shutdown not acknowledged: ${JSON.stringify(resp)}`);
954
+ }
955
+ }
956
+ async function fetchSkillsCatalog(client, timeout) {
957
+ const resp = await client.requestResponse({ type: "skills_list" }, "skills_list_response", timeout ?? 15e3);
958
+ const skillsRaw = resp.skills;
959
+ if (!skillsRaw || !Array.isArray(skillsRaw)) return [];
960
+ return skillsRaw.filter((s) => typeof s === "object" && s !== null);
961
+ }
962
+ async function fetchConfigSection(client, section, timeout) {
963
+ const resp = await client.requestResponse({ type: "config_get", section }, "config_get_response", timeout ?? 5e3);
964
+ const sec = resp[section];
965
+ if (sec && typeof sec === "object") {
966
+ return sec;
967
+ }
968
+ return resp;
969
+ }
970
+
971
+ // src/session.ts
972
+ init_config();
973
+ init_protocol();
974
+ async function bootstrapLoopSession(client, resumeLoopId, config, loopNew) {
975
+ const cfg = config ?? defaultConfig();
976
+ await client.sendMessage({ type: "daemon_ready" });
977
+ await waitDaemonReady(client, cfg.daemonReadyTimeout);
978
+ let loopId = (resumeLoopId ?? "").trim();
979
+ if (!loopId) {
980
+ const newResp = await client.requestResponse(
981
+ newLoopNewMessage(loopNew),
982
+ "loop_new_response",
983
+ cfg.loopStatusTimeout
984
+ );
985
+ loopId = String(newResp.loop_id ?? "").trim();
986
+ if (!loopId) {
987
+ throw new Error("loop_new_response missing loop_id");
988
+ }
989
+ }
990
+ const subResp = await client.requestResponse(
991
+ { type: "loop_subscribe", loop_id: loopId, verbosity: cfg.verbosityLevel },
992
+ "loop_subscribe_response",
993
+ cfg.subscriptionTimeout
994
+ );
995
+ if (subResp.success === false) {
996
+ throw new Error(String(subResp.message ?? "loop_subscribe failed"));
997
+ }
998
+ return loopId;
999
+ }
1000
+ async function waitDaemonReady(client, timeout) {
1001
+ const deadline = Date.now() + timeout;
1002
+ while (Date.now() < deadline) {
1003
+ const remaining = deadline - Date.now();
1004
+ if (remaining <= 0) break;
1005
+ const ev = await client.readEventWithTimeout(remaining);
1006
+ if (ev === null) break;
1007
+ if (ev.type === "daemon_ready") {
1008
+ if (ev.state === "ready") return;
1009
+ throw new Error(
1010
+ `daemon not ready: state=${JSON.stringify(ev.state)} message=${JSON.stringify(ev.message ?? "")}`
1011
+ );
1012
+ }
1013
+ }
1014
+ throw new Error(`timeout after ${timeout}ms waiting for daemon_ready (state=ready)`);
1015
+ }
1016
+ async function waitLoopStatusWithID(client, timeout) {
1017
+ const deadline = Date.now() + timeout;
1018
+ while (Date.now() < deadline) {
1019
+ const remaining = deadline - Date.now();
1020
+ if (remaining <= 0) break;
1021
+ const ev = await client.readEventWithTimeout(remaining);
1022
+ if (ev === null) break;
1023
+ if (ev.type === "error") {
1024
+ const errResp = ev;
1025
+ throw new Error(`daemon error: ${errResp.code}: ${errResp.message}`);
1026
+ }
1027
+ if (ev.type === "status") {
1028
+ const status = ev;
1029
+ const lid = status.loop_id;
1030
+ if (lid && lid !== "") {
1031
+ return status;
1032
+ }
1033
+ }
1034
+ }
1035
+ throw new Error(`timeout after ${timeout}ms waiting for status with loop_id`);
1036
+ }
1037
+ async function waitSubscriptionConfirmed(client, wantLoopID, _wantVerbosity, timeout) {
1038
+ const deadline = Date.now() + timeout;
1039
+ while (Date.now() < deadline) {
1040
+ const remaining = deadline - Date.now();
1041
+ if (remaining <= 0) break;
1042
+ const ev = await client.readEventWithTimeout(remaining);
1043
+ if (ev === null) break;
1044
+ if (ev.type === "loop_subscribe_response" && ev.success === true) {
1045
+ if (String(ev.loop_id ?? "") === wantLoopID) return;
1046
+ }
1047
+ if (ev.type === "subscription_confirmed") {
1048
+ const lid = String(ev.loop_id ?? "");
1049
+ if (lid === wantLoopID) return;
1050
+ }
1051
+ }
1052
+ throw new Error(`timeout after ${timeout}ms waiting for subscription_confirmed`);
1053
+ }
1054
+ async function connectWithRetries(client, maxRetries, retryDelay) {
1055
+ const retries = maxRetries && maxRetries > 0 ? maxRetries : 40;
1056
+ const delay = retryDelay && retryDelay > 0 ? retryDelay : 250;
1057
+ let lastErr = null;
1058
+ for (let attempt = 0; attempt < retries; attempt++) {
1059
+ try {
1060
+ await client.connect();
1061
+ return;
1062
+ } catch (err) {
1063
+ lastErr = err;
1064
+ }
1065
+ await new Promise((resolve) => setTimeout(resolve, delay));
1066
+ }
1067
+ throw new Error(`failed to connect after ${retries} attempts: ${lastErr?.message ?? "unknown error"}`);
1068
+ }
1069
+ // Annotate the CommonJS export names for ESM import in node:
1070
+ 0 && (module.exports = {
1071
+ Client,
1072
+ ConnectionError,
1073
+ DaemonError,
1074
+ ESSENTIAL_EVENT_TYPES,
1075
+ EventAgentLoopCompleted,
1076
+ EventAgentLoopIterated,
1077
+ EventAgentLoopReasoned,
1078
+ EventAgentLoopStarted,
1079
+ EventExploreCompleted,
1080
+ EventExploreMilestone,
1081
+ EventExploreStarted,
1082
+ EventExploreStepCompleted,
1083
+ EventFinalReport,
1084
+ EventGeneralFailed,
1085
+ EventLoopReattachedWire,
1086
+ EventMessageReceived,
1087
+ EventMessageSent,
1088
+ EventPlanCreated,
1089
+ EventReplayComplete,
1090
+ EventStreamToolCallUpdate,
1091
+ EventTacitusCompleted,
1092
+ EventTacitusGatherSummary,
1093
+ EventTacitusStarted,
1094
+ EventToolCallUpdatesBatch,
1095
+ EventToolCompleted,
1096
+ EventToolError,
1097
+ EventToolStarted,
1098
+ TimeoutError,
1099
+ VerbosityTier,
1100
+ bootstrapLoopSession,
1101
+ checkDaemonStatus,
1102
+ classifyEventVerbosity,
1103
+ connectWithRetries,
1104
+ decodeMessage,
1105
+ defaultConfig,
1106
+ encodeMessage,
1107
+ extractSootheLoopID,
1108
+ fetchConfigSection,
1109
+ fetchSkillsCatalog,
1110
+ isCompletionEvent,
1111
+ isDaemonLive,
1112
+ isSubagentProgressEvent,
1113
+ isValidVerbosityLevel,
1114
+ loadConfigFromEnv,
1115
+ newLoopInputMessage,
1116
+ newLoopNewMessage,
1117
+ newLoopSubscribeMessage,
1118
+ newRequestID,
1119
+ parseNamespace,
1120
+ requestDaemonShutdown,
1121
+ shouldShow,
1122
+ splitWirePayload,
1123
+ waitDaemonReady,
1124
+ waitLoopStatusWithID,
1125
+ waitSubscriptionConfirmed
1126
+ });
1127
+ //# sourceMappingURL=index.cjs.map