@m64/nats-pi-bridge 0.0.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/server.js ADDED
@@ -0,0 +1,689 @@
1
+ #!/usr/bin/env node
2
+
3
+ // server.ts
4
+ import { existsSync, readFileSync, statSync } from "node:fs";
5
+ import { homedir } from "node:os";
6
+ import { join, resolve } from "node:path";
7
+ import {
8
+ connect,
9
+ credsAuthenticator,
10
+ jwtAuthenticator,
11
+ nkeyAuthenticator,
12
+ tokenAuthenticator,
13
+ usernamePasswordAuthenticator
14
+ } from "@nats-io/transport-node";
15
+ import { Svcm } from "@nats-io/services";
16
+ import {
17
+ AuthStorage,
18
+ ModelRegistry,
19
+ SessionManager,
20
+ createAgentSession
21
+ } from "@mariozechner/pi-coding-agent";
22
+ var STATE_DIR = join(homedir(), ".pi-exec");
23
+ var CONFIG_FILE = join(STATE_DIR, "config.json");
24
+ var NATS_CONTEXT_DIR = join(homedir(), ".config", "nats", "context");
25
+ var SERVICE_NAME = "pi-exec";
26
+ var SERVICE_VERSION = "0.0.1";
27
+ var DEFAULT_MAX_LIFETIME_SECONDS = 1800;
28
+ var STALE_REQUEST_CUTOFF_MS = 30 * 60 * 1e3;
29
+ var STALE_PRUNE_INTERVAL_MS = 6e4;
30
+ var LIFETIME_CHECK_INTERVAL_MS = 3e4;
31
+ var SHUTDOWN_FORCED_EXIT_MS = 2e3;
32
+ var VALID_THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh"];
33
+ var DEFAULT_CONTEXT = {
34
+ url: "demo.nats.io",
35
+ description: "NATS demo server (no auth)"
36
+ };
37
+ function intakeSubject(owner2) {
38
+ return `agents.pi-exec.${owner2}`;
39
+ }
40
+ function sessionSubject(owner2, sessionId) {
41
+ return `agents.pi-exec.${owner2}.${sessionId}`;
42
+ }
43
+ function sessionInspectSubject(owner2, sessionId) {
44
+ return `${sessionSubject(owner2, sessionId)}.inspect`;
45
+ }
46
+ function sanitizeSessionName(s) {
47
+ return s.replace(/[^a-zA-Z0-9_-]/g, "-").toLowerCase().replace(/^-+|-+$/g, "");
48
+ }
49
+ function loadNatsContext(name) {
50
+ const contextFile = join(NATS_CONTEXT_DIR, `${name}.json`);
51
+ try {
52
+ return JSON.parse(readFileSync(contextFile, "utf8"));
53
+ } catch (err) {
54
+ throw new Error(
55
+ `NATS context "${name}" not found at ${contextFile} (${err.message})`
56
+ );
57
+ }
58
+ }
59
+ function contextToConnectOpts(ctx) {
60
+ const opts = {
61
+ name: "pi-exec"
62
+ };
63
+ if (ctx.url) {
64
+ opts.servers = ctx.url;
65
+ }
66
+ if (ctx.creds) {
67
+ opts.authenticator = credsAuthenticator(readFileSync(ctx.creds));
68
+ } else if (ctx.nkey) {
69
+ opts.authenticator = nkeyAuthenticator(readFileSync(ctx.nkey));
70
+ } else if (ctx.user_jwt && ctx.user_seed) {
71
+ const seed = new TextEncoder().encode(ctx.user_seed);
72
+ opts.authenticator = jwtAuthenticator(ctx.user_jwt, seed);
73
+ } else if (ctx.token) {
74
+ opts.authenticator = tokenAuthenticator(ctx.token);
75
+ } else if (ctx.user) {
76
+ opts.authenticator = usernamePasswordAuthenticator(ctx.user, ctx.password ?? "");
77
+ }
78
+ if (ctx.cert || ctx.key || ctx.ca) {
79
+ opts.tls = {
80
+ certFile: ctx.cert || void 0,
81
+ keyFile: ctx.key || void 0,
82
+ caFile: ctx.ca || void 0,
83
+ handshakeFirst: ctx.tls_first || void 0
84
+ };
85
+ }
86
+ if (ctx.inbox_prefix) {
87
+ opts.inboxPrefix = ctx.inbox_prefix;
88
+ }
89
+ return opts;
90
+ }
91
+ function loadConfig() {
92
+ try {
93
+ return JSON.parse(readFileSync(CONFIG_FILE, "utf8"));
94
+ } catch {
95
+ return {};
96
+ }
97
+ }
98
+ function parseEnvelope(data) {
99
+ const text = typeof data === "string" ? data : new TextDecoder().decode(data);
100
+ let parsed;
101
+ try {
102
+ parsed = JSON.parse(text);
103
+ } catch {
104
+ return { from: "anonymous", body: text };
105
+ }
106
+ if (typeof parsed === "object" && parsed !== null && typeof parsed.body === "string") {
107
+ const rec = parsed;
108
+ return {
109
+ from: typeof rec.from === "string" ? rec.from : "anonymous",
110
+ body: rec.body
111
+ };
112
+ }
113
+ return { from: "anonymous", body: text };
114
+ }
115
+ function publishChunked(nc2, replySubject, text) {
116
+ const maxPayload = nc2.info?.max_payload ?? 1024 * 1024;
117
+ const encoded = new TextEncoder().encode(text);
118
+ if (encoded.byteLength <= maxPayload) {
119
+ nc2.publish(replySubject, text);
120
+ return;
121
+ }
122
+ let offset = 0;
123
+ while (offset < encoded.byteLength) {
124
+ const chunk = encoded.subarray(offset, offset + maxPayload);
125
+ nc2.publish(replySubject, chunk);
126
+ offset += maxPayload;
127
+ }
128
+ }
129
+ function buildInspectResponse(sessionName, subject, cwd) {
130
+ return {
131
+ name: sessionName,
132
+ description: `PI agent in ${cwd}`,
133
+ capabilities: [
134
+ {
135
+ name: "prompt",
136
+ endpoint: subject,
137
+ description: "Send a prompt (plain text or {from,body} JSON) and receive a streamed response. Empty payload signals end-of-stream.",
138
+ pattern: "request/reply"
139
+ }
140
+ ]
141
+ };
142
+ }
143
+ async function startStatusLoop(conn) {
144
+ try {
145
+ for await (const s of conn.status()) {
146
+ switch (s.type) {
147
+ case "disconnect":
148
+ process.stderr.write(`pi-exec: NATS disconnected from ${s.server} \u2014 retrying
149
+ `);
150
+ break;
151
+ case "reconnect":
152
+ process.stderr.write(`pi-exec: NATS reconnected to ${s.server}
153
+ `);
154
+ break;
155
+ case "error": {
156
+ const err = s.error;
157
+ const data = s.data;
158
+ process.stderr.write(
159
+ `pi-exec: NATS error: ${err?.message ?? String(data ?? "(unknown)")}
160
+ `
161
+ );
162
+ break;
163
+ }
164
+ }
165
+ }
166
+ } catch {
167
+ }
168
+ }
169
+ var shuttingDown = false;
170
+ var requestCounter = 0;
171
+ var sessions = /* @__PURE__ */ new Map();
172
+ var creating = /* @__PURE__ */ new Set();
173
+ var nc;
174
+ var intakeService;
175
+ var owner;
176
+ var config;
177
+ var authStorage;
178
+ var modelRegistry;
179
+ var lifetimeInterval;
180
+ var pruneInterval;
181
+ function resolveModel(modelSpec) {
182
+ if (!modelSpec) return void 0;
183
+ const slash = modelSpec.indexOf("/");
184
+ if (slash < 0) return void 0;
185
+ return modelRegistry.find(modelSpec.slice(0, slash), modelSpec.slice(slash + 1));
186
+ }
187
+ function validateThinkingLevel(level) {
188
+ if (level === void 0 || level === "") return void 0;
189
+ if (VALID_THINKING_LEVELS.includes(level)) {
190
+ return level;
191
+ }
192
+ throw new Error(
193
+ `invalid thinkingLevel: ${level} (must be one of ${VALID_THINKING_LEVELS.join(", ")})`
194
+ );
195
+ }
196
+ async function createPiSession(intake) {
197
+ if (!intake.cwd) throw new Error("cwd is required");
198
+ const absCwd = resolve(intake.cwd);
199
+ if (!existsSync(absCwd) || !statSync(absCwd).isDirectory()) {
200
+ throw new Error(`cwd not found or not a directory: ${absCwd}`);
201
+ }
202
+ const modelSpec = intake.model ?? config.defaultModel;
203
+ const model = resolveModel(modelSpec);
204
+ if (modelSpec && !model) throw new Error(`unknown model: ${modelSpec}`);
205
+ const thinkingLevel = validateThinkingLevel(
206
+ intake.thinkingLevel ?? config.defaultThinkingLevel
207
+ );
208
+ const { session } = await createAgentSession({
209
+ cwd: absCwd,
210
+ sessionManager: SessionManager.inMemory(),
211
+ authStorage,
212
+ modelRegistry,
213
+ model,
214
+ thinkingLevel
215
+ });
216
+ return {
217
+ session,
218
+ cwd: absCwd,
219
+ resolvedModel: model ? `${model.provider}/${model.id}` : void 0,
220
+ thinkingLevel
221
+ };
222
+ }
223
+ async function createPerSessionService(sessionId, subject, cwd) {
224
+ const svcm2 = new Svcm(nc);
225
+ const service = await svcm2.add({
226
+ name: SERVICE_NAME,
227
+ version: SERVICE_VERSION,
228
+ description: `${sessionId} \u2014 ${cwd}`,
229
+ metadata: {
230
+ type: "session",
231
+ platform: "pi",
232
+ sessionId,
233
+ cwd,
234
+ owner
235
+ },
236
+ queue: ""
237
+ });
238
+ service.addEndpoint("prompt", {
239
+ subject,
240
+ handler: (err, msg) => handlePerSessionMessage(sessionId, err, msg)
241
+ });
242
+ const inspectResp = JSON.stringify(buildInspectResponse(sessionId, subject, cwd));
243
+ service.addEndpoint("inspect", {
244
+ subject: sessionInspectSubject(owner, sessionId),
245
+ handler: (_err, msg) => {
246
+ try {
247
+ msg.respond(inspectResp);
248
+ } catch {
249
+ }
250
+ }
251
+ });
252
+ return service;
253
+ }
254
+ function respondJson(msg, payload) {
255
+ try {
256
+ msg.respond(JSON.stringify(payload));
257
+ } catch {
258
+ }
259
+ }
260
+ function handleIntakeMessage(err, msg) {
261
+ if (err) {
262
+ process.stderr.write(`pi-exec: intake error: ${err.message}
263
+ `);
264
+ return;
265
+ }
266
+ if (shuttingDown) {
267
+ respondJson(msg, { error: "shutting_down" });
268
+ return;
269
+ }
270
+ let intake;
271
+ try {
272
+ intake = JSON.parse(msg.string());
273
+ } catch (e) {
274
+ respondJson(msg, { error: "invalid_json", message: e.message });
275
+ return;
276
+ }
277
+ if (!intake || typeof intake.sessionMode !== "string") {
278
+ respondJson(msg, { error: "invalid_request", message: "sessionMode required" });
279
+ return;
280
+ }
281
+ switch (intake.sessionMode) {
282
+ case "run":
283
+ void handleRunMode(msg, intake);
284
+ break;
285
+ case "session":
286
+ void handleSessionMode(msg, intake);
287
+ break;
288
+ case "stop":
289
+ void handleStopMode(msg, intake);
290
+ break;
291
+ case "list":
292
+ handleListMode(msg);
293
+ break;
294
+ default:
295
+ respondJson(msg, {
296
+ error: "invalid_sessionMode",
297
+ sessionMode: intake.sessionMode
298
+ });
299
+ }
300
+ }
301
+ async function handleRunMode(msg, intake) {
302
+ const reply = msg.reply;
303
+ if (!reply) {
304
+ respondJson(msg, { error: "no_reply_subject" });
305
+ return;
306
+ }
307
+ if (!intake.body || !intake.cwd) {
308
+ respondJson(msg, { error: "missing_fields", required: ["body", "cwd"] });
309
+ return;
310
+ }
311
+ let session;
312
+ let unsubscribe;
313
+ try {
314
+ const created = await createPiSession(intake);
315
+ session = created.session;
316
+ unsubscribe = session.subscribe((ev) => {
317
+ if (ev.type === "message_update" && ev.assistantMessageEvent.type === "text_delta" && ev.assistantMessageEvent.delta) {
318
+ publishChunked(nc, reply, ev.assistantMessageEvent.delta);
319
+ }
320
+ });
321
+ await session.prompt(intake.body);
322
+ } catch (e) {
323
+ try {
324
+ nc.publish(reply, `error: ${e.message}`);
325
+ } catch {
326
+ }
327
+ process.stderr.write(`pi-exec: run error: ${e.message}
328
+ `);
329
+ } finally {
330
+ try {
331
+ unsubscribe?.();
332
+ } catch {
333
+ }
334
+ try {
335
+ session?.dispose();
336
+ } catch {
337
+ }
338
+ try {
339
+ nc.publish(reply, "");
340
+ } catch {
341
+ }
342
+ try {
343
+ await nc.flush();
344
+ } catch {
345
+ }
346
+ }
347
+ }
348
+ async function handleSessionMode(msg, intake) {
349
+ const reply = msg.reply;
350
+ if (!reply) {
351
+ respondJson(msg, { error: "no_reply_subject" });
352
+ return;
353
+ }
354
+ if (!intake.body || !intake.cwd || !intake.sessionId) {
355
+ respondJson(msg, {
356
+ error: "missing_fields",
357
+ required: ["body", "cwd", "sessionId"]
358
+ });
359
+ return;
360
+ }
361
+ const sessionId = sanitizeSessionName(intake.sessionId);
362
+ if (!sessionId) {
363
+ respondJson(msg, {
364
+ error: "invalid_sessionId",
365
+ message: "empty after sanitize"
366
+ });
367
+ return;
368
+ }
369
+ if (sessionId !== intake.sessionId) {
370
+ respondJson(msg, {
371
+ error: "invalid_sessionId",
372
+ sessionId: intake.sessionId,
373
+ message: `sessionId must match [a-z0-9_-]+; suggested: ${sessionId}`
374
+ });
375
+ return;
376
+ }
377
+ if (sessions.has(sessionId) || creating.has(sessionId)) {
378
+ respondJson(msg, {
379
+ error: "session_exists",
380
+ sessionId,
381
+ subject: sessionSubject(owner, sessionId),
382
+ message: "Session already exists. Send follow-up prompts to the session subject."
383
+ });
384
+ return;
385
+ }
386
+ creating.add(sessionId);
387
+ let created;
388
+ try {
389
+ created = await createPiSession(intake);
390
+ } catch (e) {
391
+ creating.delete(sessionId);
392
+ respondJson(msg, {
393
+ error: "session_create_failed",
394
+ message: e.message
395
+ });
396
+ return;
397
+ }
398
+ const subject = sessionSubject(owner, sessionId);
399
+ let service;
400
+ try {
401
+ service = await createPerSessionService(sessionId, subject, created.cwd);
402
+ } catch (e) {
403
+ creating.delete(sessionId);
404
+ try {
405
+ created.session.dispose();
406
+ } catch {
407
+ }
408
+ respondJson(msg, {
409
+ error: "service_register_failed",
410
+ message: e.message
411
+ });
412
+ return;
413
+ }
414
+ const managed = {
415
+ sessionId,
416
+ session: created.session,
417
+ service,
418
+ cwd: created.cwd,
419
+ model: created.resolvedModel,
420
+ thinkingLevel: created.thinkingLevel,
421
+ createdAt: Date.now(),
422
+ lastActivity: Date.now(),
423
+ maxLifetime: intake.maxLifetime ?? config.defaultMaxLifetime ?? DEFAULT_MAX_LIFETIME_SECONDS,
424
+ subject,
425
+ pendingRequests: /* @__PURE__ */ new Map(),
426
+ requestQueue: [],
427
+ activeRequestId: null,
428
+ disposed: false
429
+ };
430
+ sessions.set(sessionId, managed);
431
+ creating.delete(sessionId);
432
+ process.stderr.write(`pi-exec: session created ${sessionId} cwd=${created.cwd}
433
+ `);
434
+ const initialId = `init-${++requestCounter}`;
435
+ managed.pendingRequests.set(initialId, {
436
+ requestId: initialId,
437
+ replySubject: reply,
438
+ from: intake.from ?? "anonymous",
439
+ body: intake.body,
440
+ createdAt: Date.now()
441
+ });
442
+ managed.requestQueue.push(initialId);
443
+ void drainSession(
444
+ managed,
445
+ () => {
446
+ process.stderr.write(`pi-exec: initial prompt failed for ${sessionId}; disposing
447
+ `);
448
+ void disposeSession(managed);
449
+ },
450
+ initialId
451
+ );
452
+ }
453
+ async function handleStopMode(msg, intake) {
454
+ if (!intake.sessionId) {
455
+ respondJson(msg, { error: "missing_fields", required: ["sessionId"] });
456
+ return;
457
+ }
458
+ const sid = sanitizeSessionName(intake.sessionId);
459
+ const managed = sessions.get(sid);
460
+ if (!managed) {
461
+ respondJson(msg, { error: "not_found", sessionId: sid });
462
+ return;
463
+ }
464
+ await disposeSession(managed);
465
+ respondJson(msg, { ok: true, sessionId: sid });
466
+ }
467
+ function handleListMode(msg) {
468
+ const now = Date.now();
469
+ const list = Array.from(sessions.values()).map((m) => {
470
+ const elapsedSec = Math.floor((now - m.createdAt) / 1e3);
471
+ const remaining = m.maxLifetime > 0 ? Math.max(0, m.maxLifetime - elapsedSec) : 0;
472
+ return {
473
+ sessionId: m.sessionId,
474
+ subject: m.subject,
475
+ cwd: m.cwd,
476
+ model: m.model,
477
+ thinkingLevel: m.thinkingLevel,
478
+ createdAt: new Date(m.createdAt).toISOString(),
479
+ lastActivity: new Date(m.lastActivity).toISOString(),
480
+ maxLifetime: m.maxLifetime,
481
+ remainingLifetime: remaining,
482
+ activeRequest: m.activeRequestId !== null,
483
+ queuedRequests: m.requestQueue.length
484
+ };
485
+ });
486
+ respondJson(msg, { sessions: list });
487
+ }
488
+ function handlePerSessionMessage(sessionId, err, msg) {
489
+ if (err) {
490
+ process.stderr.write(`pi-exec: ${sessionId} handler err: ${err.message}
491
+ `);
492
+ return;
493
+ }
494
+ if (!msg.reply) return;
495
+ const managed = sessions.get(sessionId);
496
+ if (!managed || managed.disposed) return;
497
+ const env = parseEnvelope(msg.data);
498
+ if (!env.body) return;
499
+ const rid = `${sessionId}-${++requestCounter}`;
500
+ managed.pendingRequests.set(rid, {
501
+ requestId: rid,
502
+ replySubject: msg.reply,
503
+ from: env.from,
504
+ body: env.body,
505
+ createdAt: Date.now()
506
+ });
507
+ managed.requestQueue.push(rid);
508
+ managed.lastActivity = Date.now();
509
+ void drainSession(managed);
510
+ }
511
+ async function drainSession(m, onInitialFailure, initialRequestId) {
512
+ if (m.activeRequestId) return;
513
+ if (m.disposed) return;
514
+ const next = m.requestQueue.shift();
515
+ if (!next) return;
516
+ const pr = m.pendingRequests.get(next);
517
+ if (!pr) {
518
+ void drainSession(m);
519
+ return;
520
+ }
521
+ m.activeRequestId = next;
522
+ m.lastActivity = Date.now();
523
+ let unsubscribe;
524
+ let failed = false;
525
+ try {
526
+ unsubscribe = m.session.subscribe((ev) => {
527
+ if (ev.type === "message_update" && ev.assistantMessageEvent.type === "text_delta" && ev.assistantMessageEvent.delta) {
528
+ publishChunked(nc, pr.replySubject, ev.assistantMessageEvent.delta);
529
+ }
530
+ });
531
+ await m.session.prompt(pr.body);
532
+ } catch (e) {
533
+ failed = true;
534
+ try {
535
+ nc.publish(pr.replySubject, `error: ${e.message}`);
536
+ } catch {
537
+ }
538
+ process.stderr.write(`pi-exec: ${m.sessionId} prompt error: ${e.message}
539
+ `);
540
+ } finally {
541
+ try {
542
+ unsubscribe?.();
543
+ } catch {
544
+ }
545
+ try {
546
+ nc.publish(pr.replySubject, "");
547
+ } catch {
548
+ }
549
+ try {
550
+ await nc.flush();
551
+ } catch {
552
+ }
553
+ m.pendingRequests.delete(next);
554
+ m.activeRequestId = null;
555
+ m.lastActivity = Date.now();
556
+ if (failed && onInitialFailure && next === initialRequestId) {
557
+ onInitialFailure();
558
+ return;
559
+ }
560
+ if (!m.disposed && m.requestQueue.length > 0) {
561
+ setImmediate(() => void drainSession(m));
562
+ }
563
+ }
564
+ }
565
+ async function disposeSession(m) {
566
+ if (m.disposed) return;
567
+ m.disposed = true;
568
+ for (const pr of m.pendingRequests.values()) {
569
+ try {
570
+ nc.publish(pr.replySubject, "");
571
+ } catch {
572
+ }
573
+ }
574
+ m.pendingRequests.clear();
575
+ m.requestQueue.length = 0;
576
+ try {
577
+ await m.service.stop();
578
+ } catch {
579
+ }
580
+ try {
581
+ m.session.dispose();
582
+ } catch {
583
+ }
584
+ sessions.delete(m.sessionId);
585
+ process.stderr.write(`pi-exec: session disposed ${m.sessionId}
586
+ `);
587
+ }
588
+ function checkLifetimes() {
589
+ const now = Date.now();
590
+ for (const m of sessions.values()) {
591
+ if (m.maxLifetime <= 0) continue;
592
+ if (now - m.createdAt > m.maxLifetime * 1e3) {
593
+ process.stderr.write(`pi-exec: session ${m.sessionId} expired (${m.maxLifetime}s)
594
+ `);
595
+ void disposeSession(m);
596
+ }
597
+ }
598
+ }
599
+ function pruneStaleRequests() {
600
+ const cutoff = Date.now() - STALE_REQUEST_CUTOFF_MS;
601
+ for (const m of sessions.values()) {
602
+ for (const [id, pr] of m.pendingRequests) {
603
+ if (id === m.activeRequestId) continue;
604
+ if (pr.createdAt < cutoff) {
605
+ m.pendingRequests.delete(id);
606
+ const qi = m.requestQueue.indexOf(id);
607
+ if (qi >= 0) m.requestQueue.splice(qi, 1);
608
+ }
609
+ }
610
+ }
611
+ }
612
+ async function shutdown(signal) {
613
+ if (shuttingDown) return;
614
+ shuttingDown = true;
615
+ process.stderr.write(`pi-exec: shutdown (${signal})
616
+ `);
617
+ const forceTimer = setTimeout(() => {
618
+ process.stderr.write("pi-exec: forced exit\n");
619
+ process.exit(0);
620
+ }, SHUTDOWN_FORCED_EXIT_MS);
621
+ forceTimer.unref();
622
+ clearInterval(lifetimeInterval);
623
+ clearInterval(pruneInterval);
624
+ await Promise.allSettled(Array.from(sessions.values()).map((m) => disposeSession(m)));
625
+ try {
626
+ await intakeService.stop();
627
+ } catch {
628
+ }
629
+ try {
630
+ await nc.drain();
631
+ } catch {
632
+ }
633
+ clearTimeout(forceTimer);
634
+ process.exit(0);
635
+ }
636
+ process.on("unhandledRejection", (err) => {
637
+ process.stderr.write(`pi-exec: unhandledRejection: ${err}
638
+ `);
639
+ });
640
+ process.on("uncaughtException", (err) => {
641
+ process.stderr.write(`pi-exec: uncaughtException: ${err}
642
+ `);
643
+ });
644
+ config = loadConfig();
645
+ var contextName = process.env.NATS_CONTEXT ?? config.context;
646
+ config.defaultModel = process.env.PI_EXEC_DEFAULT_MODEL ?? config.defaultModel;
647
+ var envMaxLifetime = process.env.PI_EXEC_DEFAULT_MAX_LIFETIME;
648
+ config.defaultMaxLifetime = envMaxLifetime ? Number(envMaxLifetime) : config.defaultMaxLifetime ?? DEFAULT_MAX_LIFETIME_SECONDS;
649
+ var natsCtx;
650
+ try {
651
+ natsCtx = contextName ? loadNatsContext(contextName) : DEFAULT_CONTEXT;
652
+ } catch (e) {
653
+ process.stderr.write(`pi-exec: ${e.message}
654
+ `);
655
+ process.exit(1);
656
+ }
657
+ process.stderr.write(
658
+ `pi-exec: connecting to ${natsCtx.url ?? "default"} (context: ${contextName ?? "default"})
659
+ `
660
+ );
661
+ var connectOpts = contextToConnectOpts(natsCtx);
662
+ connectOpts.name = "pi-exec";
663
+ nc = await connect(connectOpts);
664
+ process.stderr.write(`pi-exec: connected
665
+ `);
666
+ owner = sanitizeSessionName(process.env.USER ?? "unknown") || "unknown";
667
+ authStorage = AuthStorage.create();
668
+ modelRegistry = ModelRegistry.create(authStorage);
669
+ var svcm = new Svcm(nc);
670
+ intakeService = await svcm.add({
671
+ name: SERVICE_NAME,
672
+ version: SERVICE_VERSION,
673
+ description: "intake",
674
+ metadata: { type: "intake", platform: "pi", owner },
675
+ queue: ""
676
+ });
677
+ intakeService.addEndpoint("intake", {
678
+ subject: intakeSubject(owner),
679
+ handler: handleIntakeMessage
680
+ });
681
+ process.stderr.write(`pi-exec: intake registered on ${intakeSubject(owner)}
682
+ `);
683
+ lifetimeInterval = setInterval(checkLifetimes, LIFETIME_CHECK_INTERVAL_MS);
684
+ lifetimeInterval.unref();
685
+ pruneInterval = setInterval(pruneStaleRequests, STALE_PRUNE_INTERVAL_MS);
686
+ pruneInterval.unref();
687
+ void startStatusLoop(nc);
688
+ process.on("SIGTERM", () => void shutdown("SIGTERM"));
689
+ process.on("SIGINT", () => void shutdown("SIGINT"));