@mobvibe/cli 0.0.0-dev

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.js ADDED
@@ -0,0 +1,2708 @@
1
+ // src/index.ts
2
+ import { Command } from "commander";
3
+
4
+ // src/auth/login.ts
5
+ import * as readline from "readline/promises";
6
+
7
+ // src/lib/logger.ts
8
+ import pino from "pino";
9
+ var LOG_LEVEL = process.env.LOG_LEVEL ?? "info";
10
+ var isPretty = process.env.NODE_ENV !== "production";
11
+ var redact = {
12
+ paths: [
13
+ "req.headers.authorization",
14
+ "req.headers.cookie",
15
+ "req.headers['x-api-key']",
16
+ "headers.authorization",
17
+ "headers.cookie",
18
+ "headers['x-api-key']",
19
+ "apiKey",
20
+ "token"
21
+ ],
22
+ censor: "[redacted]"
23
+ };
24
+ var transport = isPretty ? {
25
+ target: "pino-pretty",
26
+ options: {
27
+ colorize: true,
28
+ translateTime: "SYS:standard",
29
+ ignore: "pid,hostname"
30
+ }
31
+ } : void 0;
32
+ var logger = pino(
33
+ {
34
+ level: LOG_LEVEL,
35
+ redact,
36
+ base: { service: "mobvibe-cli" },
37
+ serializers: {
38
+ err: pino.stdSerializers.err,
39
+ error: pino.stdSerializers.err
40
+ }
41
+ },
42
+ transport ? pino.transport(transport) : void 0
43
+ );
44
+
45
+ // src/auth/credentials.ts
46
+ import fs from "fs/promises";
47
+ import os from "os";
48
+ import path from "path";
49
+ var MOBVIBE_DIR = process.env.MOBVIBE_HOME ?? path.join(os.homedir(), ".mobvibe");
50
+ var CREDENTIALS_FILE = path.join(MOBVIBE_DIR, "credentials.json");
51
+ async function ensureMobvibeDir() {
52
+ await fs.mkdir(MOBVIBE_DIR, { recursive: true });
53
+ }
54
+ async function loadCredentials() {
55
+ try {
56
+ const data = await fs.readFile(CREDENTIALS_FILE, "utf8");
57
+ const credentials = JSON.parse(data);
58
+ if (!credentials.apiKey) {
59
+ return null;
60
+ }
61
+ return credentials;
62
+ } catch {
63
+ return null;
64
+ }
65
+ }
66
+ async function saveCredentials(credentials) {
67
+ await ensureMobvibeDir();
68
+ await fs.writeFile(
69
+ CREDENTIALS_FILE,
70
+ JSON.stringify(credentials, null, 2),
71
+ { mode: 384 }
72
+ // Read/write only for owner
73
+ );
74
+ }
75
+ async function deleteCredentials() {
76
+ try {
77
+ await fs.unlink(CREDENTIALS_FILE);
78
+ } catch {
79
+ }
80
+ }
81
+ async function getApiKey() {
82
+ if (process.env.MOBVIBE_API_KEY) {
83
+ return process.env.MOBVIBE_API_KEY;
84
+ }
85
+ const credentials = await loadCredentials();
86
+ return credentials?.apiKey;
87
+ }
88
+ var DEFAULT_GATEWAY_URL = "https://mobvibe.zeabur.app";
89
+ async function getGatewayUrl() {
90
+ if (process.env.MOBVIBE_GATEWAY_URL) {
91
+ return process.env.MOBVIBE_GATEWAY_URL;
92
+ }
93
+ const credentials = await loadCredentials();
94
+ if (credentials?.gatewayUrl) {
95
+ return credentials.gatewayUrl;
96
+ }
97
+ return DEFAULT_GATEWAY_URL;
98
+ }
99
+
100
+ // src/auth/login.ts
101
+ async function login() {
102
+ logger.info("login_prompt_start");
103
+ console.log("To get an API key:");
104
+ console.log(" 1. Open the Mobvibe WebUI in your browser");
105
+ console.log(" 2. Go to Settings (gear icon) -> API Keys");
106
+ console.log(" 3. Click 'Create API Key' and copy it");
107
+ console.log(" 4. Paste the API key below\n");
108
+ const rl = readline.createInterface({
109
+ input: process.stdin,
110
+ output: process.stdout
111
+ });
112
+ try {
113
+ const apiKey = await rl.question("Paste your API key: ");
114
+ if (!apiKey.trim()) {
115
+ logger.warn("login_missing_api_key");
116
+ return { success: false, error: "No API key provided" };
117
+ }
118
+ if (!apiKey.trim().startsWith("mbk_")) {
119
+ logger.warn("login_invalid_api_key_format");
120
+ return {
121
+ success: false,
122
+ error: "Invalid API key format (should start with mbk_)"
123
+ };
124
+ }
125
+ const credentials = {
126
+ apiKey: apiKey.trim(),
127
+ createdAt: Date.now()
128
+ };
129
+ await saveCredentials(credentials);
130
+ logger.info("login_credentials_saved");
131
+ console.log("\nAPI key saved!");
132
+ console.log("Run 'mobvibe start' to connect to the gateway.");
133
+ return { success: true };
134
+ } finally {
135
+ rl.close();
136
+ }
137
+ }
138
+ async function logout() {
139
+ await deleteCredentials();
140
+ logger.info("logout_complete");
141
+ console.log("Logged out successfully. Credentials deleted.");
142
+ }
143
+ async function loginStatus() {
144
+ const credentials = await loadCredentials();
145
+ if (credentials) {
146
+ logger.info("login_status_logged_in");
147
+ console.log("Status: Logged in");
148
+ console.log(`API key: ${credentials.apiKey.slice(0, 12)}...`);
149
+ console.log(`Saved: ${new Date(credentials.createdAt).toLocaleString()}`);
150
+ } else {
151
+ logger.info("login_status_logged_out");
152
+ console.log("Status: Not logged in");
153
+ console.log("Run 'mobvibe login' to authenticate.");
154
+ }
155
+ }
156
+
157
+ // src/config.ts
158
+ import os2 from "os";
159
+ import path3 from "path";
160
+
161
+ // src/config-loader.ts
162
+ import fs2 from "fs/promises";
163
+ import path2 from "path";
164
+ var CONFIG_FILENAME = ".config.json";
165
+ var validateAgentConfig = (agent, index) => {
166
+ const errors = [];
167
+ const prefix = `agents[${index}]`;
168
+ if (typeof agent !== "object" || agent === null) {
169
+ errors.push(`${prefix}: must be an object`);
170
+ return { valid: null, errors };
171
+ }
172
+ const record = agent;
173
+ if (typeof record.id !== "string" || record.id.trim().length === 0) {
174
+ errors.push(`${prefix}.id: must be a non-empty string`);
175
+ return { valid: null, errors };
176
+ }
177
+ if (typeof record.command !== "string" || record.command.trim().length === 0) {
178
+ errors.push(`${prefix}.command: must be a non-empty string`);
179
+ return { valid: null, errors };
180
+ }
181
+ const validated = {
182
+ id: record.id.trim(),
183
+ command: record.command.trim()
184
+ };
185
+ if (record.label !== void 0) {
186
+ if (typeof record.label !== "string") {
187
+ errors.push(`${prefix}.label: must be a string`);
188
+ } else if (record.label.trim().length > 0) {
189
+ validated.label = record.label.trim();
190
+ }
191
+ }
192
+ if (record.args !== void 0) {
193
+ if (!Array.isArray(record.args)) {
194
+ errors.push(`${prefix}.args: must be an array of strings`);
195
+ } else {
196
+ const validArgs = record.args.filter((arg) => {
197
+ if (typeof arg !== "string") {
198
+ errors.push(`${prefix}.args: all elements must be strings`);
199
+ return false;
200
+ }
201
+ return true;
202
+ });
203
+ if (validArgs.length > 0) {
204
+ validated.args = validArgs;
205
+ }
206
+ }
207
+ }
208
+ if (record.env !== void 0) {
209
+ if (typeof record.env !== "object" || record.env === null) {
210
+ errors.push(`${prefix}.env: must be an object`);
211
+ } else {
212
+ const envRecord = record.env;
213
+ const validEnv = {};
214
+ let hasEnv = false;
215
+ for (const [key, value] of Object.entries(envRecord)) {
216
+ if (typeof value !== "string") {
217
+ errors.push(`${prefix}.env.${key}: must be a string`);
218
+ } else {
219
+ validEnv[key] = value;
220
+ hasEnv = true;
221
+ }
222
+ }
223
+ if (hasEnv) {
224
+ validated.env = validEnv;
225
+ }
226
+ }
227
+ }
228
+ if (errors.length > 0) {
229
+ return { valid: null, errors };
230
+ }
231
+ return { valid: validated, errors: [] };
232
+ };
233
+ var validateUserConfig = (data) => {
234
+ const errors = [];
235
+ if (typeof data !== "object" || data === null) {
236
+ errors.push("config: must be an object");
237
+ return { config: null, errors };
238
+ }
239
+ const record = data;
240
+ const config = {};
241
+ if (record.agents !== void 0) {
242
+ if (!Array.isArray(record.agents)) {
243
+ errors.push("agents: must be an array");
244
+ } else {
245
+ const validAgents = [];
246
+ const seenIds = /* @__PURE__ */ new Set();
247
+ for (let i = 0; i < record.agents.length; i++) {
248
+ const result = validateAgentConfig(record.agents[i], i);
249
+ errors.push(...result.errors);
250
+ if (result.valid) {
251
+ if (seenIds.has(result.valid.id)) {
252
+ errors.push(`agents[${i}].id: duplicate id "${result.valid.id}"`);
253
+ } else {
254
+ seenIds.add(result.valid.id);
255
+ validAgents.push(result.valid);
256
+ }
257
+ }
258
+ }
259
+ if (validAgents.length > 0) {
260
+ config.agents = validAgents;
261
+ }
262
+ }
263
+ }
264
+ if (record.defaultAgentId !== void 0) {
265
+ if (typeof record.defaultAgentId !== "string") {
266
+ errors.push("defaultAgentId: must be a string");
267
+ } else if (record.defaultAgentId.trim().length > 0) {
268
+ config.defaultAgentId = record.defaultAgentId.trim();
269
+ }
270
+ }
271
+ if (errors.length > 0) {
272
+ return { config: null, errors };
273
+ }
274
+ return { config, errors: [] };
275
+ };
276
+ var loadUserConfig = async (homePath) => {
277
+ const configPath = path2.join(homePath, CONFIG_FILENAME);
278
+ console.log(`[config] Loading config from: ${configPath}`);
279
+ try {
280
+ const content = await fs2.readFile(configPath, "utf-8");
281
+ let parsed;
282
+ try {
283
+ parsed = JSON.parse(content);
284
+ } catch {
285
+ console.log(`[config] Invalid JSON in config file: ${configPath}`);
286
+ return {
287
+ config: null,
288
+ errors: ["Invalid JSON in config file"],
289
+ path: configPath
290
+ };
291
+ }
292
+ const { config, errors } = validateUserConfig(parsed);
293
+ if (errors.length > 0) {
294
+ console.log("[config] Validation errors:", errors);
295
+ }
296
+ if (config) {
297
+ console.log("[config] Loaded config:", JSON.stringify(config, null, 2));
298
+ }
299
+ return { config, errors, path: configPath };
300
+ } catch (error) {
301
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
302
+ console.log(`[config] No config file found at: ${configPath}`);
303
+ return { config: null, errors: [], path: configPath };
304
+ }
305
+ const message = error instanceof Error ? error.message : "Unknown error reading config";
306
+ console.log(`[config] Error reading config: ${message}`);
307
+ return { config: null, errors: [message], path: configPath };
308
+ }
309
+ };
310
+
311
+ // src/config.ts
312
+ var DEFAULT_OPENCODE_BACKEND = {
313
+ id: "opencode",
314
+ label: "opencode",
315
+ command: "opencode",
316
+ args: ["acp"]
317
+ };
318
+ var generateMachineId = () => {
319
+ const hostname = os2.hostname();
320
+ const platform = os2.platform();
321
+ const arch = os2.arch();
322
+ const username = os2.userInfo().username;
323
+ return `${hostname}-${platform}-${arch}-${username}`;
324
+ };
325
+ var userAgentToBackendConfig = (agent) => ({
326
+ id: agent.id,
327
+ label: agent.label ?? agent.id,
328
+ command: agent.command,
329
+ args: agent.args ?? [],
330
+ envOverrides: agent.env
331
+ });
332
+ var mergeBackends = (defaultBackend, userAgents) => {
333
+ if (!userAgents || userAgents.length === 0) {
334
+ return { backends: [defaultBackend], defaultId: defaultBackend.id };
335
+ }
336
+ const userOpencode = userAgents.find((a) => a.id === "opencode");
337
+ if (userOpencode) {
338
+ return {
339
+ backends: userAgents.map(userAgentToBackendConfig),
340
+ defaultId: userAgents[0].id
341
+ };
342
+ }
343
+ return {
344
+ backends: [defaultBackend, ...userAgents.map(userAgentToBackendConfig)],
345
+ defaultId: defaultBackend.id
346
+ };
347
+ };
348
+ var getCliConfig = async () => {
349
+ const env = process.env;
350
+ const homePath = env.MOBVIBE_HOME ?? path3.join(os2.homedir(), ".mobvibe");
351
+ const userConfigResult = await loadUserConfig(homePath);
352
+ if (userConfigResult.errors.length > 0) {
353
+ for (const error of userConfigResult.errors) {
354
+ logger.warn({ configPath: userConfigResult.path, error }, "config_error");
355
+ }
356
+ }
357
+ const { backends, defaultId } = mergeBackends(
358
+ DEFAULT_OPENCODE_BACKEND,
359
+ userConfigResult.config?.agents
360
+ );
361
+ const resolvedDefaultId = userConfigResult.config?.defaultAgentId && backends.some((b) => b.id === userConfigResult.config?.defaultAgentId) ? userConfigResult.config.defaultAgentId : defaultId;
362
+ const gatewayUrl = await getGatewayUrl();
363
+ return {
364
+ gatewayUrl,
365
+ acpBackends: backends,
366
+ defaultAcpBackendId: resolvedDefaultId,
367
+ clientName: env.MOBVIBE_ACP_CLIENT_NAME ?? "mobvibe-cli",
368
+ clientVersion: env.MOBVIBE_ACP_CLIENT_VERSION ?? "0.0.0",
369
+ homePath,
370
+ logPath: path3.join(homePath, "logs"),
371
+ pidFile: path3.join(homePath, "daemon.pid"),
372
+ machineId: env.MOBVIBE_MACHINE_ID ?? generateMachineId(),
373
+ hostname: os2.hostname(),
374
+ platform: os2.platform(),
375
+ userConfigPath: userConfigResult.path,
376
+ userConfigErrors: userConfigResult.errors.length > 0 ? userConfigResult.errors : void 0
377
+ };
378
+ };
379
+
380
+ // src/daemon/daemon.ts
381
+ import { spawn as spawn2 } from "child_process";
382
+ import fs4 from "fs/promises";
383
+ import path5 from "path";
384
+
385
+ // src/acp/session-manager.ts
386
+ import { randomUUID as randomUUID2 } from "crypto";
387
+ import { EventEmitter as EventEmitter2 } from "events";
388
+
389
+ // ../../packages/shared/dist/types/errors.js
390
+ var createErrorDetail = (input) => ({
391
+ ...input
392
+ });
393
+ var isProtocolMismatch = (error) => {
394
+ if (error instanceof Error) {
395
+ return /protocol/i.test(error.message);
396
+ }
397
+ return false;
398
+ };
399
+ var AppError = class extends Error {
400
+ detail;
401
+ status;
402
+ constructor(detail, status = 500) {
403
+ super(detail.message);
404
+ this.detail = detail;
405
+ this.status = status;
406
+ }
407
+ };
408
+
409
+ // src/acp/acp-connection.ts
410
+ import { spawn } from "child_process";
411
+ import { randomUUID } from "crypto";
412
+ import { EventEmitter } from "events";
413
+ import { Readable, Writable } from "stream";
414
+ import {
415
+ ClientSideConnection,
416
+ ndJsonStream,
417
+ PROTOCOL_VERSION
418
+ } from "@agentclientprotocol/sdk";
419
+ var getErrorMessage = (error) => {
420
+ if (error instanceof Error) {
421
+ return error.message;
422
+ }
423
+ return String(error);
424
+ };
425
+ var buildClient = (handlers) => ({
426
+ async requestPermission(params) {
427
+ if (handlers.onRequestPermission) {
428
+ return handlers.onRequestPermission(params);
429
+ }
430
+ return { outcome: { outcome: "cancelled" } };
431
+ },
432
+ async sessionUpdate(params) {
433
+ handlers.onSessionUpdate(params);
434
+ },
435
+ async createTerminal(params) {
436
+ if (!handlers.onCreateTerminal) {
437
+ throw new Error("Terminal create handler not configured");
438
+ }
439
+ return handlers.onCreateTerminal(params);
440
+ },
441
+ async terminalOutput(params) {
442
+ if (!handlers.onTerminalOutput) {
443
+ return { output: "", truncated: false };
444
+ }
445
+ return handlers.onTerminalOutput(params);
446
+ },
447
+ async waitForTerminalExit(params) {
448
+ if (!handlers.onWaitForTerminalExit) {
449
+ return { exitCode: null, signal: null };
450
+ }
451
+ return handlers.onWaitForTerminalExit(params);
452
+ },
453
+ async killTerminal(params) {
454
+ if (!handlers.onKillTerminal) {
455
+ return {};
456
+ }
457
+ return handlers.onKillTerminal(params);
458
+ },
459
+ async releaseTerminal(params) {
460
+ if (!handlers.onReleaseTerminal) {
461
+ return {};
462
+ }
463
+ return handlers.onReleaseTerminal(params);
464
+ }
465
+ });
466
+ var formatExitMessage = (code, signal) => {
467
+ if (signal) {
468
+ return `ACP process received signal ${signal}`;
469
+ }
470
+ if (code !== null) {
471
+ return `ACP process exited with code ${code}`;
472
+ }
473
+ return "ACP process exited";
474
+ };
475
+ var buildConnectError = (error) => {
476
+ const detail = getErrorMessage(error);
477
+ if (isProtocolMismatch(error)) {
478
+ return createErrorDetail({
479
+ code: "ACP_PROTOCOL_MISMATCH",
480
+ message: "ACP protocol version mismatch",
481
+ retryable: false,
482
+ scope: "service",
483
+ detail
484
+ });
485
+ }
486
+ return createErrorDetail({
487
+ code: "ACP_CONNECT_FAILED",
488
+ message: "Failed to connect to ACP backend process",
489
+ retryable: true,
490
+ scope: "service",
491
+ detail
492
+ });
493
+ };
494
+ var buildProcessExitError = (detail) => createErrorDetail({
495
+ code: "ACP_PROCESS_EXITED",
496
+ message: "ACP backend process exited unexpectedly",
497
+ retryable: true,
498
+ scope: "service",
499
+ detail
500
+ });
501
+ var buildConnectionClosedError = (detail) => createErrorDetail({
502
+ code: "ACP_CONNECTION_CLOSED",
503
+ message: "ACP connection closed",
504
+ retryable: true,
505
+ scope: "service",
506
+ detail
507
+ });
508
+ var normalizeOutputText = (value) => value.normalize("NFC");
509
+ var isOutputOverLimit = (value, limit) => Buffer.byteLength(value, "utf8") > limit;
510
+ var sliceOutputToLimit = (value, limit) => {
511
+ const buffer = Buffer.from(value, "utf8");
512
+ if (buffer.byteLength <= limit) {
513
+ return value;
514
+ }
515
+ const sliced = buffer.subarray(buffer.byteLength - limit);
516
+ let start = 0;
517
+ while (start < sliced.length && (sliced[start] & 192) === 128) {
518
+ start += 1;
519
+ }
520
+ return sliced.subarray(start).toString("utf8");
521
+ };
522
+ var AcpConnection = class {
523
+ constructor(options) {
524
+ this.options = options;
525
+ }
526
+ connection;
527
+ process;
528
+ closedPromise;
529
+ state = "idle";
530
+ connectedAt;
531
+ error;
532
+ sessionId;
533
+ agentInfo;
534
+ agentCapabilities;
535
+ sessionUpdateEmitter = new EventEmitter();
536
+ statusEmitter = new EventEmitter();
537
+ terminalOutputEmitter = new EventEmitter();
538
+ permissionHandler;
539
+ terminals = /* @__PURE__ */ new Map();
540
+ getStatus() {
541
+ return {
542
+ backendId: this.options.backend.id,
543
+ backendLabel: this.options.backend.label,
544
+ state: this.state,
545
+ command: this.options.backend.command,
546
+ args: [...this.options.backend.args],
547
+ connectedAt: this.connectedAt?.toISOString(),
548
+ error: this.error,
549
+ sessionId: this.sessionId,
550
+ pid: this.process?.pid
551
+ };
552
+ }
553
+ getAgentInfo() {
554
+ return this.agentInfo;
555
+ }
556
+ /**
557
+ * Get the agent's session capabilities.
558
+ */
559
+ getSessionCapabilities() {
560
+ return {
561
+ list: this.agentCapabilities?.sessionCapabilities?.list != null,
562
+ load: this.agentCapabilities?.loadSession === true,
563
+ resume: this.agentCapabilities?.sessionCapabilities?.resume != null
564
+ };
565
+ }
566
+ /**
567
+ * Check if the agent supports session/list.
568
+ */
569
+ supportsSessionList() {
570
+ return this.agentCapabilities?.sessionCapabilities?.list != null;
571
+ }
572
+ /**
573
+ * Check if the agent supports session/load.
574
+ */
575
+ supportsSessionLoad() {
576
+ return this.agentCapabilities?.loadSession === true;
577
+ }
578
+ /**
579
+ * Check if the agent supports session/resume.
580
+ */
581
+ supportsSessionResume() {
582
+ return this.agentCapabilities?.sessionCapabilities?.resume != null;
583
+ }
584
+ /**
585
+ * List sessions from the agent (session/list).
586
+ * @param params Optional filter parameters
587
+ * @returns List of session info from the agent
588
+ */
589
+ async listSessions(params) {
590
+ if (!this.supportsSessionList()) {
591
+ return [];
592
+ }
593
+ const connection = await this.ensureReady();
594
+ const response = await connection.unstable_listSessions({
595
+ cursor: params?.cursor ?? void 0,
596
+ cwd: params?.cwd ?? void 0
597
+ });
598
+ return response.sessions;
599
+ }
600
+ /**
601
+ * Load a historical session with message history replay (session/load).
602
+ * @param sessionId The session ID to load
603
+ * @param cwd The working directory
604
+ * @returns Load session response with modes/models state
605
+ */
606
+ async loadSession(sessionId, cwd) {
607
+ if (!this.supportsSessionLoad()) {
608
+ throw new Error("Agent does not support session/load capability");
609
+ }
610
+ const connection = await this.ensureReady();
611
+ const response = await connection.loadSession({
612
+ sessionId,
613
+ cwd,
614
+ mcpServers: []
615
+ });
616
+ this.sessionId = sessionId;
617
+ return response;
618
+ }
619
+ /**
620
+ * Resume an active session without message history replay (session/resume).
621
+ * @param sessionId The session ID to resume
622
+ * @param cwd The working directory
623
+ * @returns Resume session response
624
+ */
625
+ async resumeSession(sessionId, cwd) {
626
+ if (!this.supportsSessionResume()) {
627
+ throw new Error("Agent does not support session/resume capability");
628
+ }
629
+ const connection = await this.ensureReady();
630
+ const response = await connection.unstable_resumeSession({
631
+ sessionId,
632
+ cwd,
633
+ mcpServers: []
634
+ });
635
+ this.sessionId = sessionId;
636
+ return response;
637
+ }
638
+ setPermissionHandler(handler) {
639
+ this.permissionHandler = handler;
640
+ }
641
+ onTerminalOutput(listener) {
642
+ this.terminalOutputEmitter.on("output", listener);
643
+ return () => {
644
+ this.terminalOutputEmitter.off("output", listener);
645
+ };
646
+ }
647
+ onSessionUpdate(listener) {
648
+ this.sessionUpdateEmitter.on("update", listener);
649
+ return () => {
650
+ this.sessionUpdateEmitter.off("update", listener);
651
+ };
652
+ }
653
+ onStatusChange(listener) {
654
+ this.statusEmitter.on("status", listener);
655
+ return () => {
656
+ this.statusEmitter.off("status", listener);
657
+ };
658
+ }
659
+ updateStatus(state, error) {
660
+ this.state = state;
661
+ this.error = error;
662
+ this.statusEmitter.emit("status", this.getStatus());
663
+ }
664
+ async connect() {
665
+ if (this.state === "connecting" || this.state === "ready") {
666
+ return;
667
+ }
668
+ this.updateStatus("connecting");
669
+ this.agentInfo = void 0;
670
+ try {
671
+ const env = this.options.backend.envOverrides ? { ...process.env, ...this.options.backend.envOverrides } : process.env;
672
+ const child = spawn(
673
+ this.options.backend.command,
674
+ this.options.backend.args,
675
+ {
676
+ stdio: ["pipe", "pipe", "pipe"],
677
+ env
678
+ }
679
+ );
680
+ this.process = child;
681
+ this.sessionId = void 0;
682
+ child.stderr.pipe(process.stderr);
683
+ const input = Writable.toWeb(child.stdin);
684
+ const output = Readable.toWeb(child.stdout);
685
+ const stream = ndJsonStream(input, output);
686
+ const connection = new ClientSideConnection(
687
+ () => buildClient({
688
+ onSessionUpdate: (notification) => this.emitSessionUpdate(notification),
689
+ onRequestPermission: (params) => this.handlePermissionRequest(params),
690
+ onCreateTerminal: (params) => this.createTerminal(params),
691
+ onTerminalOutput: (params) => this.getTerminalOutput(params),
692
+ onWaitForTerminalExit: (params) => this.waitForTerminalExit(params),
693
+ onKillTerminal: (params) => this.killTerminal(params),
694
+ onReleaseTerminal: (params) => this.releaseTerminal(params)
695
+ }),
696
+ stream
697
+ );
698
+ this.connection = connection;
699
+ child.once("error", (error) => {
700
+ if (this.state === "stopped") {
701
+ return;
702
+ }
703
+ this.updateStatus("error", buildConnectError(error));
704
+ });
705
+ child.once("exit", (code, signal) => {
706
+ if (this.state === "stopped") {
707
+ return;
708
+ }
709
+ this.updateStatus(
710
+ "error",
711
+ buildProcessExitError(formatExitMessage(code, signal))
712
+ );
713
+ });
714
+ this.closedPromise = connection.closed.catch((error) => {
715
+ this.updateStatus(
716
+ "error",
717
+ buildConnectionClosedError(getErrorMessage(error))
718
+ );
719
+ });
720
+ const initializeResponse = await connection.initialize({
721
+ protocolVersion: PROTOCOL_VERSION,
722
+ clientInfo: {
723
+ name: this.options.client.name,
724
+ version: this.options.client.version
725
+ },
726
+ clientCapabilities: { terminal: true }
727
+ });
728
+ this.agentInfo = initializeResponse.agentInfo ?? void 0;
729
+ this.agentCapabilities = initializeResponse.agentCapabilities ?? void 0;
730
+ this.connectedAt = /* @__PURE__ */ new Date();
731
+ this.updateStatus("ready");
732
+ } catch (error) {
733
+ this.updateStatus("error", buildConnectError(error));
734
+ await this.stopProcess();
735
+ throw error;
736
+ }
737
+ }
738
+ async createSession(options) {
739
+ const connection = await this.ensureReady();
740
+ const response = await this.createSessionInternal(
741
+ connection,
742
+ options?.cwd ?? process.cwd()
743
+ );
744
+ this.sessionId = response.sessionId;
745
+ return response;
746
+ }
747
+ async prompt(sessionId, prompt) {
748
+ const connection = await this.ensureReady();
749
+ return connection.prompt({ sessionId, prompt });
750
+ }
751
+ async cancel(sessionId) {
752
+ const connection = await this.ensureReady();
753
+ await connection.cancel({ sessionId });
754
+ }
755
+ async setSessionMode(sessionId, modeId) {
756
+ const connection = await this.ensureReady();
757
+ await connection.setSessionMode({ sessionId, modeId });
758
+ }
759
+ async setSessionModel(sessionId, modelId) {
760
+ const connection = await this.ensureReady();
761
+ await connection.unstable_setSessionModel({ sessionId, modelId });
762
+ }
763
+ async createTerminal(params) {
764
+ const outputLimit = typeof params.outputByteLimit === "number" && params.outputByteLimit > 0 ? Math.floor(params.outputByteLimit) : 1024 * 1024;
765
+ const resolvedEnv = params.env ? Object.fromEntries(
766
+ params.env.map((envVar) => [envVar.name, envVar.value])
767
+ ) : void 0;
768
+ const terminalId = randomUUID();
769
+ const record = {
770
+ sessionId: params.sessionId,
771
+ command: params.command,
772
+ args: params.args ?? [],
773
+ outputByteLimit: outputLimit,
774
+ output: {
775
+ output: "",
776
+ truncated: false,
777
+ exitStatus: null
778
+ }
779
+ };
780
+ this.terminals.set(terminalId, record);
781
+ const child = spawn(params.command, params.args ?? [], {
782
+ cwd: params.cwd ?? void 0,
783
+ env: resolvedEnv ? { ...process.env, ...resolvedEnv } : process.env
784
+ });
785
+ child.once("error", (error) => {
786
+ record.output.exitStatus = {
787
+ exitCode: null,
788
+ signal: null
789
+ };
790
+ record.resolveExit?.({ exitCode: null, signal: null });
791
+ this.terminalOutputEmitter.emit("output", {
792
+ sessionId: record.sessionId,
793
+ terminalId,
794
+ delta: `
795
+ [terminal error] ${String(error)}`,
796
+ truncated: record.output.truncated,
797
+ output: record.output.output,
798
+ exitStatus: record.output.exitStatus
799
+ });
800
+ });
801
+ record.process = child;
802
+ let resolveExit = () => {
803
+ };
804
+ record.onExit = new Promise((resolve) => {
805
+ resolveExit = resolve;
806
+ });
807
+ record.resolveExit = resolveExit;
808
+ const handleChunk = (chunk) => {
809
+ const delta = normalizeOutputText(chunk.toString("utf8"));
810
+ if (!delta) {
811
+ return;
812
+ }
813
+ const combinedOutput = record.output.output + delta;
814
+ record.output.truncated = isOutputOverLimit(
815
+ combinedOutput,
816
+ record.outputByteLimit
817
+ );
818
+ record.output.output = sliceOutputToLimit(
819
+ combinedOutput,
820
+ record.outputByteLimit
821
+ );
822
+ this.terminalOutputEmitter.emit("output", {
823
+ sessionId: record.sessionId,
824
+ terminalId,
825
+ delta,
826
+ truncated: record.output.truncated,
827
+ output: record.output.truncated ? record.output.output : void 0,
828
+ exitStatus: record.output.exitStatus
829
+ });
830
+ };
831
+ child.stdout?.on("data", handleChunk);
832
+ child.stderr?.on("data", handleChunk);
833
+ child.on("exit", (code, signal) => {
834
+ record.output.exitStatus = {
835
+ exitCode: code ?? null,
836
+ signal: signal ?? null
837
+ };
838
+ record.resolveExit?.({
839
+ exitCode: code ?? null,
840
+ signal: signal ?? null
841
+ });
842
+ this.terminalOutputEmitter.emit("output", {
843
+ sessionId: record.sessionId,
844
+ terminalId,
845
+ delta: "",
846
+ truncated: record.output.truncated,
847
+ output: record.output.output,
848
+ exitStatus: record.output.exitStatus
849
+ });
850
+ });
851
+ return { terminalId };
852
+ }
853
+ async getTerminalOutput(params) {
854
+ const record = this.terminals.get(params.terminalId);
855
+ if (!record || record.sessionId !== params.sessionId) {
856
+ return { output: "", truncated: false };
857
+ }
858
+ return record.output;
859
+ }
860
+ async waitForTerminalExit(params) {
861
+ const record = this.terminals.get(params.terminalId);
862
+ if (!record || record.sessionId !== params.sessionId) {
863
+ return Promise.resolve({ exitCode: null, signal: null });
864
+ }
865
+ return record.onExit ?? Promise.resolve({ exitCode: null, signal: null });
866
+ }
867
+ async killTerminal(params) {
868
+ const record = this.terminals.get(params.terminalId);
869
+ if (!record || record.sessionId !== params.sessionId) {
870
+ return {};
871
+ }
872
+ record.process?.kill("SIGTERM");
873
+ return {};
874
+ }
875
+ async releaseTerminal(params) {
876
+ const record = this.terminals.get(params.terminalId);
877
+ if (record?.process && record.process.exitCode === null) {
878
+ record.process.kill("SIGTERM");
879
+ }
880
+ this.terminals.delete(params.terminalId);
881
+ return {};
882
+ }
883
+ async disconnect() {
884
+ if (this.state === "stopped") {
885
+ return;
886
+ }
887
+ this.updateStatus("stopped");
888
+ this.sessionId = void 0;
889
+ this.agentInfo = void 0;
890
+ await this.stopProcess();
891
+ await this.closedPromise;
892
+ this.connection = void 0;
893
+ }
894
+ async ensureReady() {
895
+ if (this.state !== "ready" || !this.connection) {
896
+ await this.connect();
897
+ }
898
+ if (!this.connection || this.state !== "ready") {
899
+ throw new Error("ACP connection not available");
900
+ }
901
+ return this.connection;
902
+ }
903
+ async createSessionInternal(connection, cwd) {
904
+ const session = await connection.newSession({
905
+ cwd,
906
+ mcpServers: []
907
+ });
908
+ return session;
909
+ }
910
+ emitSessionUpdate(notification) {
911
+ this.sessionUpdateEmitter.emit("update", notification);
912
+ }
913
+ async handlePermissionRequest(params) {
914
+ if (this.permissionHandler) {
915
+ return this.permissionHandler(params);
916
+ }
917
+ return { outcome: { outcome: "cancelled" } };
918
+ }
919
+ async stopProcess() {
920
+ const child = this.process;
921
+ if (!child) {
922
+ return;
923
+ }
924
+ this.process = void 0;
925
+ if (child.exitCode === null && !child.killed) {
926
+ child.kill("SIGTERM");
927
+ }
928
+ }
929
+ };
930
+
931
+ // src/acp/session-manager.ts
932
+ var buildPermissionKey = (sessionId, requestId) => `${sessionId}:${requestId}`;
933
+ var resolveModelState = (models) => {
934
+ if (!models) {
935
+ return {
936
+ modelId: void 0,
937
+ modelName: void 0,
938
+ availableModels: void 0
939
+ };
940
+ }
941
+ const availableModels = models.availableModels?.map((model) => ({
942
+ id: model.modelId,
943
+ name: model.name,
944
+ description: model.description ?? void 0
945
+ }));
946
+ const modelId = models.currentModelId ?? void 0;
947
+ const modelName = availableModels?.find(
948
+ (model) => model.id === modelId
949
+ )?.name;
950
+ return { modelId, modelName, availableModels };
951
+ };
952
+ var resolveModeState = (modes) => {
953
+ if (!modes) {
954
+ return {
955
+ modeId: void 0,
956
+ modeName: void 0,
957
+ availableModes: void 0
958
+ };
959
+ }
960
+ const modeId = modes.currentModeId ?? void 0;
961
+ const modeName = modes.availableModes?.find(
962
+ (mode) => mode.id === modeId
963
+ )?.name;
964
+ return {
965
+ modeId,
966
+ modeName,
967
+ availableModes: modes.availableModes?.map((mode) => ({
968
+ id: mode.id,
969
+ name: mode.name
970
+ }))
971
+ };
972
+ };
973
+ var createCapabilityNotSupportedError = (message) => new AppError(
974
+ createErrorDetail({
975
+ code: "CAPABILITY_NOT_SUPPORTED",
976
+ message,
977
+ retryable: false,
978
+ scope: "session"
979
+ }),
980
+ 409
981
+ );
982
+ var SessionManager = class {
983
+ constructor(config) {
984
+ this.config = config;
985
+ this.backendById = new Map(
986
+ config.acpBackends.map((backend) => [backend.id, backend])
987
+ );
988
+ this.defaultBackendId = config.defaultAcpBackendId;
989
+ }
990
+ sessions = /* @__PURE__ */ new Map();
991
+ backendById;
992
+ defaultBackendId;
993
+ permissionRequests = /* @__PURE__ */ new Map();
994
+ sessionUpdateEmitter = new EventEmitter2();
995
+ sessionErrorEmitter = new EventEmitter2();
996
+ permissionRequestEmitter = new EventEmitter2();
997
+ permissionResultEmitter = new EventEmitter2();
998
+ terminalOutputEmitter = new EventEmitter2();
999
+ sessionsChangedEmitter = new EventEmitter2();
1000
+ listSessions() {
1001
+ return Array.from(this.sessions.values()).map(
1002
+ (record) => this.buildSummary(record)
1003
+ );
1004
+ }
1005
+ getSession(sessionId) {
1006
+ return this.sessions.get(sessionId);
1007
+ }
1008
+ onSessionUpdate(listener) {
1009
+ this.sessionUpdateEmitter.on("update", listener);
1010
+ return () => {
1011
+ this.sessionUpdateEmitter.off("update", listener);
1012
+ };
1013
+ }
1014
+ onSessionError(listener) {
1015
+ this.sessionErrorEmitter.on("error", listener);
1016
+ return () => {
1017
+ this.sessionErrorEmitter.off("error", listener);
1018
+ };
1019
+ }
1020
+ onPermissionRequest(listener) {
1021
+ this.permissionRequestEmitter.on("request", listener);
1022
+ return () => {
1023
+ this.permissionRequestEmitter.off("request", listener);
1024
+ };
1025
+ }
1026
+ onPermissionResult(listener) {
1027
+ this.permissionResultEmitter.on("result", listener);
1028
+ return () => {
1029
+ this.permissionResultEmitter.off("result", listener);
1030
+ };
1031
+ }
1032
+ onTerminalOutput(listener) {
1033
+ this.terminalOutputEmitter.on("output", listener);
1034
+ return () => {
1035
+ this.terminalOutputEmitter.off("output", listener);
1036
+ };
1037
+ }
1038
+ onSessionsChanged(listener) {
1039
+ this.sessionsChangedEmitter.on("changed", listener);
1040
+ return () => {
1041
+ this.sessionsChangedEmitter.off("changed", listener);
1042
+ };
1043
+ }
1044
+ emitSessionsChanged(payload) {
1045
+ this.sessionsChangedEmitter.emit("changed", payload);
1046
+ }
1047
+ listPendingPermissions(sessionId) {
1048
+ return Array.from(this.permissionRequests.values()).filter((record) => record.sessionId === sessionId).map((record) => this.buildPermissionRequestPayload(record));
1049
+ }
1050
+ resolvePermissionRequest(sessionId, requestId, outcome) {
1051
+ const key = buildPermissionKey(sessionId, requestId);
1052
+ const record = this.permissionRequests.get(key);
1053
+ if (!record) {
1054
+ throw new AppError(
1055
+ createErrorDetail({
1056
+ code: "REQUEST_VALIDATION_FAILED",
1057
+ message: "Permission request not found",
1058
+ retryable: false,
1059
+ scope: "request"
1060
+ }),
1061
+ 404
1062
+ );
1063
+ }
1064
+ const response = { outcome };
1065
+ record.resolve(response);
1066
+ this.permissionRequests.delete(key);
1067
+ const payload = {
1068
+ sessionId,
1069
+ requestId,
1070
+ outcome
1071
+ };
1072
+ this.permissionResultEmitter.emit("result", payload);
1073
+ return payload;
1074
+ }
1075
+ resolveBackend(backendId) {
1076
+ const normalized = backendId?.trim();
1077
+ const resolvedId = normalized && normalized.length > 0 ? normalized : this.defaultBackendId;
1078
+ const backend = this.backendById.get(resolvedId);
1079
+ if (!backend) {
1080
+ throw new AppError(
1081
+ createErrorDetail({
1082
+ code: "REQUEST_VALIDATION_FAILED",
1083
+ message: "Invalid backend ID",
1084
+ retryable: false,
1085
+ scope: "request"
1086
+ }),
1087
+ 400
1088
+ );
1089
+ }
1090
+ return backend;
1091
+ }
1092
+ async createSession(options) {
1093
+ const backend = this.resolveBackend(options?.backendId);
1094
+ const connection = new AcpConnection({
1095
+ backend,
1096
+ client: {
1097
+ name: this.config.clientName,
1098
+ version: this.config.clientVersion
1099
+ }
1100
+ });
1101
+ try {
1102
+ await connection.connect();
1103
+ const session = await connection.createSession({ cwd: options?.cwd });
1104
+ connection.setPermissionHandler(
1105
+ (params) => this.handlePermissionRequest(session.sessionId, params)
1106
+ );
1107
+ const now = /* @__PURE__ */ new Date();
1108
+ const agentInfo = connection.getAgentInfo();
1109
+ const { modelId, modelName, availableModels } = resolveModelState(
1110
+ session.models
1111
+ );
1112
+ const { modeId, modeName, availableModes } = resolveModeState(
1113
+ session.modes
1114
+ );
1115
+ const record = {
1116
+ sessionId: session.sessionId,
1117
+ title: options?.title ?? `Session ${this.sessions.size + 1}`,
1118
+ backendId: backend.id,
1119
+ backendLabel: backend.label,
1120
+ connection,
1121
+ createdAt: now,
1122
+ updatedAt: now,
1123
+ cwd: options?.cwd,
1124
+ agentName: agentInfo?.title ?? agentInfo?.name,
1125
+ modelId,
1126
+ modelName,
1127
+ modeId,
1128
+ modeName,
1129
+ availableModes,
1130
+ availableModels,
1131
+ availableCommands: void 0
1132
+ };
1133
+ record.unsubscribe = connection.onSessionUpdate(
1134
+ (notification) => {
1135
+ this.touchSession(session.sessionId);
1136
+ this.sessionUpdateEmitter.emit("update", notification);
1137
+ const update = notification.update;
1138
+ if (update.sessionUpdate === "current_mode_update") {
1139
+ record.modeId = update.currentModeId;
1140
+ record.modeName = record.availableModes?.find(
1141
+ (mode) => mode.id === update.currentModeId
1142
+ )?.name ?? record.modeName;
1143
+ return;
1144
+ }
1145
+ if (update.sessionUpdate === "session_info_update") {
1146
+ if (typeof update.title === "string") {
1147
+ record.title = update.title;
1148
+ }
1149
+ if (typeof update.updatedAt === "string") {
1150
+ record.updatedAt = new Date(update.updatedAt);
1151
+ }
1152
+ }
1153
+ if (update.sessionUpdate === "available_commands_update") {
1154
+ if (update.availableCommands) {
1155
+ record.availableCommands = update.availableCommands;
1156
+ }
1157
+ }
1158
+ }
1159
+ );
1160
+ record.unsubscribeTerminal = connection.onTerminalOutput((event) => {
1161
+ this.terminalOutputEmitter.emit("output", event);
1162
+ });
1163
+ connection.onStatusChange((status) => {
1164
+ if (status.error) {
1165
+ this.sessionErrorEmitter.emit("error", {
1166
+ sessionId: session.sessionId,
1167
+ error: status.error
1168
+ });
1169
+ }
1170
+ });
1171
+ this.sessions.set(session.sessionId, record);
1172
+ const summary = this.buildSummary(record);
1173
+ this.emitSessionsChanged({
1174
+ added: [summary],
1175
+ updated: [],
1176
+ removed: []
1177
+ });
1178
+ return summary;
1179
+ } catch (error) {
1180
+ const status = connection.getStatus();
1181
+ await connection.disconnect();
1182
+ if (status.error) {
1183
+ throw new AppError(status.error, 500);
1184
+ }
1185
+ throw error;
1186
+ }
1187
+ }
1188
+ buildPermissionRequestPayload(record) {
1189
+ const toolCall = record.params.toolCall;
1190
+ return {
1191
+ sessionId: record.sessionId,
1192
+ requestId: record.requestId,
1193
+ options: record.params.options.map((option) => ({
1194
+ optionId: option.optionId,
1195
+ // SDK uses 'name', our shared type uses 'label'
1196
+ label: option.name,
1197
+ description: option._meta?.description ?? null
1198
+ })),
1199
+ toolCall: {
1200
+ toolCallId: toolCall.toolCallId,
1201
+ name: toolCall._meta?.name ?? null,
1202
+ title: toolCall.title,
1203
+ command: toolCall._meta?.command ?? null,
1204
+ args: toolCall._meta?.args ?? null
1205
+ }
1206
+ };
1207
+ }
1208
+ handlePermissionRequest(sessionId, params) {
1209
+ const requestId = params.toolCall?.toolCallId ?? randomUUID2();
1210
+ const key = buildPermissionKey(sessionId, requestId);
1211
+ const existing = this.permissionRequests.get(key);
1212
+ if (existing) {
1213
+ return existing.promise;
1214
+ }
1215
+ let resolver = () => {
1216
+ };
1217
+ const promise = new Promise((resolve) => {
1218
+ resolver = resolve;
1219
+ });
1220
+ const record = {
1221
+ sessionId,
1222
+ requestId,
1223
+ params,
1224
+ promise,
1225
+ resolve: resolver
1226
+ };
1227
+ this.permissionRequests.set(key, record);
1228
+ this.permissionRequestEmitter.emit(
1229
+ "request",
1230
+ this.buildPermissionRequestPayload(record)
1231
+ );
1232
+ return promise;
1233
+ }
1234
+ cancelPermissionRequests(sessionId) {
1235
+ const cancelledOutcome = {
1236
+ outcome: "cancelled"
1237
+ };
1238
+ for (const [key, record] of this.permissionRequests.entries()) {
1239
+ if (record.sessionId !== sessionId) {
1240
+ continue;
1241
+ }
1242
+ record.resolve({ outcome: cancelledOutcome });
1243
+ this.permissionRequests.delete(key);
1244
+ this.permissionResultEmitter.emit("result", {
1245
+ sessionId,
1246
+ requestId: record.requestId,
1247
+ outcome: cancelledOutcome
1248
+ });
1249
+ }
1250
+ }
1251
+ updateTitle(sessionId, title) {
1252
+ const record = this.sessions.get(sessionId);
1253
+ if (!record) {
1254
+ throw new AppError(
1255
+ createErrorDetail({
1256
+ code: "SESSION_NOT_FOUND",
1257
+ message: "Session not found",
1258
+ retryable: false,
1259
+ scope: "session"
1260
+ }),
1261
+ 404
1262
+ );
1263
+ }
1264
+ record.title = title;
1265
+ record.updatedAt = /* @__PURE__ */ new Date();
1266
+ const summary = this.buildSummary(record);
1267
+ this.emitSessionsChanged({
1268
+ added: [],
1269
+ updated: [summary],
1270
+ removed: []
1271
+ });
1272
+ return summary;
1273
+ }
1274
+ touchSession(sessionId) {
1275
+ const record = this.sessions.get(sessionId);
1276
+ if (!record) {
1277
+ return;
1278
+ }
1279
+ record.updatedAt = /* @__PURE__ */ new Date();
1280
+ }
1281
+ async setSessionMode(sessionId, modeId) {
1282
+ const record = this.sessions.get(sessionId);
1283
+ if (!record) {
1284
+ throw new AppError(
1285
+ createErrorDetail({
1286
+ code: "SESSION_NOT_FOUND",
1287
+ message: "Session not found",
1288
+ retryable: false,
1289
+ scope: "session"
1290
+ }),
1291
+ 404
1292
+ );
1293
+ }
1294
+ if (!record.availableModes || record.availableModes.length === 0) {
1295
+ throw createCapabilityNotSupportedError(
1296
+ "Current agent does not support mode switching"
1297
+ );
1298
+ }
1299
+ const selected = record.availableModes.find((mode) => mode.id === modeId);
1300
+ if (!selected) {
1301
+ throw new AppError(
1302
+ createErrorDetail({
1303
+ code: "REQUEST_VALIDATION_FAILED",
1304
+ message: "Invalid mode ID",
1305
+ retryable: false,
1306
+ scope: "request"
1307
+ }),
1308
+ 400
1309
+ );
1310
+ }
1311
+ await record.connection.setSessionMode(sessionId, modeId);
1312
+ record.modeId = selected.id;
1313
+ record.modeName = selected.name;
1314
+ record.updatedAt = /* @__PURE__ */ new Date();
1315
+ const summary = this.buildSummary(record);
1316
+ this.emitSessionsChanged({
1317
+ added: [],
1318
+ updated: [summary],
1319
+ removed: []
1320
+ });
1321
+ return summary;
1322
+ }
1323
+ async setSessionModel(sessionId, modelId) {
1324
+ const record = this.sessions.get(sessionId);
1325
+ if (!record) {
1326
+ throw new AppError(
1327
+ createErrorDetail({
1328
+ code: "SESSION_NOT_FOUND",
1329
+ message: "Session not found",
1330
+ retryable: false,
1331
+ scope: "session"
1332
+ }),
1333
+ 404
1334
+ );
1335
+ }
1336
+ if (!record.availableModels || record.availableModels.length === 0) {
1337
+ throw createCapabilityNotSupportedError(
1338
+ "Current agent does not support model switching"
1339
+ );
1340
+ }
1341
+ const selected = record.availableModels.find(
1342
+ (model) => model.id === modelId
1343
+ );
1344
+ if (!selected) {
1345
+ throw new AppError(
1346
+ createErrorDetail({
1347
+ code: "REQUEST_VALIDATION_FAILED",
1348
+ message: "Invalid model ID",
1349
+ retryable: false,
1350
+ scope: "request"
1351
+ }),
1352
+ 400
1353
+ );
1354
+ }
1355
+ await record.connection.setSessionModel(sessionId, modelId);
1356
+ record.modelId = selected.id;
1357
+ record.modelName = selected.name;
1358
+ record.updatedAt = /* @__PURE__ */ new Date();
1359
+ const summary = this.buildSummary(record);
1360
+ this.emitSessionsChanged({
1361
+ added: [],
1362
+ updated: [summary],
1363
+ removed: []
1364
+ });
1365
+ return summary;
1366
+ }
1367
+ async cancelSession(sessionId) {
1368
+ const record = this.sessions.get(sessionId);
1369
+ if (!record) {
1370
+ return false;
1371
+ }
1372
+ this.cancelPermissionRequests(sessionId);
1373
+ await record.connection.cancel(sessionId);
1374
+ this.touchSession(sessionId);
1375
+ return true;
1376
+ }
1377
+ async closeSession(sessionId) {
1378
+ const record = this.sessions.get(sessionId);
1379
+ if (!record) {
1380
+ return false;
1381
+ }
1382
+ try {
1383
+ record.unsubscribe?.();
1384
+ record.unsubscribeTerminal?.();
1385
+ } catch (error) {
1386
+ logger.error({ err: error, sessionId }, "session_unsubscribe_failed");
1387
+ }
1388
+ this.cancelPermissionRequests(sessionId);
1389
+ try {
1390
+ await record.connection.disconnect();
1391
+ } catch (error) {
1392
+ logger.error({ err: error, sessionId }, "session_disconnect_failed");
1393
+ }
1394
+ this.sessions.delete(sessionId);
1395
+ this.emitSessionsChanged({
1396
+ added: [],
1397
+ updated: [],
1398
+ removed: [sessionId]
1399
+ });
1400
+ return true;
1401
+ }
1402
+ async closeAll() {
1403
+ const sessionIds = Array.from(this.sessions.keys());
1404
+ await Promise.all(
1405
+ sessionIds.map((sessionId) => this.closeSession(sessionId))
1406
+ );
1407
+ }
1408
+ /**
1409
+ * Discover sessions persisted by the ACP agent.
1410
+ * Creates a temporary connection to query sessions.
1411
+ * @param options Optional parameters for discovery
1412
+ * @returns List of discovered sessions and agent capabilities
1413
+ */
1414
+ async discoverSessions(options) {
1415
+ const backend = this.resolveBackend(options?.backendId);
1416
+ const connection = new AcpConnection({
1417
+ backend,
1418
+ client: {
1419
+ name: this.config.clientName,
1420
+ version: this.config.clientVersion
1421
+ }
1422
+ });
1423
+ try {
1424
+ await connection.connect();
1425
+ const capabilities = connection.getSessionCapabilities();
1426
+ const sessions = [];
1427
+ if (capabilities.list) {
1428
+ const agentSessions = await connection.listSessions({
1429
+ cwd: options?.cwd
1430
+ });
1431
+ for (const session of agentSessions) {
1432
+ sessions.push({
1433
+ sessionId: session.sessionId,
1434
+ cwd: session.cwd,
1435
+ title: session.title ?? void 0,
1436
+ updatedAt: session.updatedAt ?? void 0
1437
+ });
1438
+ }
1439
+ }
1440
+ logger.info(
1441
+ {
1442
+ backendId: backend.id,
1443
+ sessionCount: sessions.length,
1444
+ capabilities
1445
+ },
1446
+ "sessions_discovered"
1447
+ );
1448
+ return { sessions, capabilities };
1449
+ } finally {
1450
+ await connection.disconnect();
1451
+ }
1452
+ }
1453
+ /**
1454
+ * Load a historical session from the ACP agent.
1455
+ * This will replay the session's message history.
1456
+ * @param sessionId The session ID to load
1457
+ * @param cwd The working directory
1458
+ * @param backendId Optional backend ID
1459
+ * @returns The loaded session summary
1460
+ */
1461
+ async loadSession(sessionId, cwd, backendId) {
1462
+ const existing = this.sessions.get(sessionId);
1463
+ if (existing) {
1464
+ return this.buildSummary(existing);
1465
+ }
1466
+ const backend = this.resolveBackend(backendId);
1467
+ const connection = new AcpConnection({
1468
+ backend,
1469
+ client: {
1470
+ name: this.config.clientName,
1471
+ version: this.config.clientVersion
1472
+ }
1473
+ });
1474
+ try {
1475
+ await connection.connect();
1476
+ if (!connection.supportsSessionLoad()) {
1477
+ throw createCapabilityNotSupportedError(
1478
+ "Agent does not support session loading"
1479
+ );
1480
+ }
1481
+ const response = await connection.loadSession(sessionId, cwd);
1482
+ connection.setPermissionHandler(
1483
+ (params) => this.handlePermissionRequest(sessionId, params)
1484
+ );
1485
+ const now = /* @__PURE__ */ new Date();
1486
+ const agentInfo = connection.getAgentInfo();
1487
+ const { modelId, modelName, availableModels } = resolveModelState(
1488
+ response.models
1489
+ );
1490
+ const { modeId, modeName, availableModes } = resolveModeState(
1491
+ response.modes
1492
+ );
1493
+ const record = {
1494
+ sessionId,
1495
+ title: `Loaded Session`,
1496
+ backendId: backend.id,
1497
+ backendLabel: backend.label,
1498
+ connection,
1499
+ createdAt: now,
1500
+ updatedAt: now,
1501
+ cwd,
1502
+ agentName: agentInfo?.title ?? agentInfo?.name,
1503
+ modelId,
1504
+ modelName,
1505
+ modeId,
1506
+ modeName,
1507
+ availableModes,
1508
+ availableModels,
1509
+ availableCommands: void 0
1510
+ };
1511
+ this.setupSessionSubscriptions(record);
1512
+ this.sessions.set(sessionId, record);
1513
+ const summary = this.buildSummary(record);
1514
+ this.emitSessionsChanged({
1515
+ added: [summary],
1516
+ updated: [],
1517
+ removed: []
1518
+ });
1519
+ logger.info({ sessionId, backendId: backend.id }, "session_loaded");
1520
+ return summary;
1521
+ } catch (error) {
1522
+ await connection.disconnect();
1523
+ throw error;
1524
+ }
1525
+ }
1526
+ /**
1527
+ * Resume an active session from the ACP agent.
1528
+ * This does not replay message history.
1529
+ * @param sessionId The session ID to resume
1530
+ * @param cwd The working directory
1531
+ * @param backendId Optional backend ID
1532
+ * @returns The resumed session summary
1533
+ */
1534
+ async resumeSession(sessionId, cwd, backendId) {
1535
+ const existing = this.sessions.get(sessionId);
1536
+ if (existing) {
1537
+ return this.buildSummary(existing);
1538
+ }
1539
+ const backend = this.resolveBackend(backendId);
1540
+ const connection = new AcpConnection({
1541
+ backend,
1542
+ client: {
1543
+ name: this.config.clientName,
1544
+ version: this.config.clientVersion
1545
+ }
1546
+ });
1547
+ try {
1548
+ await connection.connect();
1549
+ if (!connection.supportsSessionResume()) {
1550
+ throw createCapabilityNotSupportedError(
1551
+ "Agent does not support session resuming"
1552
+ );
1553
+ }
1554
+ const response = await connection.resumeSession(sessionId, cwd);
1555
+ connection.setPermissionHandler(
1556
+ (params) => this.handlePermissionRequest(sessionId, params)
1557
+ );
1558
+ const now = /* @__PURE__ */ new Date();
1559
+ const agentInfo = connection.getAgentInfo();
1560
+ const { modelId, modelName, availableModels } = resolveModelState(
1561
+ response.models
1562
+ );
1563
+ const { modeId, modeName, availableModes } = resolveModeState(
1564
+ response.modes
1565
+ );
1566
+ const record = {
1567
+ sessionId,
1568
+ title: `Resumed Session`,
1569
+ backendId: backend.id,
1570
+ backendLabel: backend.label,
1571
+ connection,
1572
+ createdAt: now,
1573
+ updatedAt: now,
1574
+ cwd,
1575
+ agentName: agentInfo?.title ?? agentInfo?.name,
1576
+ modelId,
1577
+ modelName,
1578
+ modeId,
1579
+ modeName,
1580
+ availableModes,
1581
+ availableModels,
1582
+ availableCommands: void 0
1583
+ };
1584
+ this.setupSessionSubscriptions(record);
1585
+ this.sessions.set(sessionId, record);
1586
+ const summary = this.buildSummary(record);
1587
+ this.emitSessionsChanged({
1588
+ added: [summary],
1589
+ updated: [],
1590
+ removed: []
1591
+ });
1592
+ logger.info({ sessionId, backendId: backend.id }, "session_resumed");
1593
+ return summary;
1594
+ } catch (error) {
1595
+ await connection.disconnect();
1596
+ throw error;
1597
+ }
1598
+ }
1599
+ /**
1600
+ * Set up event subscriptions for a session record.
1601
+ */
1602
+ setupSessionSubscriptions(record) {
1603
+ const { sessionId, connection } = record;
1604
+ record.unsubscribe = connection.onSessionUpdate(
1605
+ (notification) => {
1606
+ this.touchSession(sessionId);
1607
+ this.sessionUpdateEmitter.emit("update", notification);
1608
+ const update = notification.update;
1609
+ if (update.sessionUpdate === "current_mode_update") {
1610
+ record.modeId = update.currentModeId;
1611
+ record.modeName = record.availableModes?.find(
1612
+ (mode) => mode.id === update.currentModeId
1613
+ )?.name ?? record.modeName;
1614
+ return;
1615
+ }
1616
+ if (update.sessionUpdate === "session_info_update") {
1617
+ if (typeof update.title === "string") {
1618
+ record.title = update.title;
1619
+ }
1620
+ if (typeof update.updatedAt === "string") {
1621
+ record.updatedAt = new Date(update.updatedAt);
1622
+ }
1623
+ }
1624
+ if (update.sessionUpdate === "available_commands_update") {
1625
+ if (update.availableCommands) {
1626
+ record.availableCommands = update.availableCommands;
1627
+ }
1628
+ }
1629
+ }
1630
+ );
1631
+ record.unsubscribeTerminal = connection.onTerminalOutput((event) => {
1632
+ this.terminalOutputEmitter.emit("output", event);
1633
+ });
1634
+ connection.onStatusChange((status) => {
1635
+ if (status.error) {
1636
+ this.sessionErrorEmitter.emit("error", {
1637
+ sessionId,
1638
+ error: status.error
1639
+ });
1640
+ }
1641
+ });
1642
+ }
1643
+ buildSummary(record) {
1644
+ const status = record.connection.getStatus();
1645
+ return {
1646
+ sessionId: record.sessionId,
1647
+ title: record.title,
1648
+ backendId: record.backendId,
1649
+ backendLabel: record.backendLabel,
1650
+ state: status.state,
1651
+ error: status.error,
1652
+ pid: status.pid,
1653
+ createdAt: record.createdAt.toISOString(),
1654
+ updatedAt: record.updatedAt.toISOString(),
1655
+ cwd: record.cwd,
1656
+ agentName: record.agentName,
1657
+ modelId: record.modelId,
1658
+ modelName: record.modelName,
1659
+ modeId: record.modeId,
1660
+ modeName: record.modeName,
1661
+ availableModes: record.availableModes,
1662
+ availableModels: record.availableModels,
1663
+ availableCommands: record.availableCommands
1664
+ };
1665
+ }
1666
+ };
1667
+
1668
+ // src/daemon/socket-client.ts
1669
+ import { EventEmitter as EventEmitter3 } from "events";
1670
+ import fs3 from "fs/promises";
1671
+ import { homedir } from "os";
1672
+ import path4 from "path";
1673
+ import ignore from "ignore";
1674
+ import { io } from "socket.io-client";
1675
+ var SESSION_ROOT_NAME = "Working Directory";
1676
+ var MAX_RESOURCE_FILES = 2e3;
1677
+ var DEFAULT_IGNORES = [
1678
+ "node_modules",
1679
+ ".git",
1680
+ "dist",
1681
+ "build",
1682
+ ".next",
1683
+ ".nuxt",
1684
+ ".output",
1685
+ ".cache",
1686
+ "__pycache__",
1687
+ ".venv",
1688
+ "venv",
1689
+ "target"
1690
+ ];
1691
+ var loadGitignore = async (rootPath) => {
1692
+ const ig = ignore().add(DEFAULT_IGNORES);
1693
+ try {
1694
+ const gitignorePath = path4.join(rootPath, ".gitignore");
1695
+ const content = await fs3.readFile(gitignorePath, "utf8");
1696
+ ig.add(content);
1697
+ } catch {
1698
+ }
1699
+ return ig;
1700
+ };
1701
+ var resolveImageMimeType = (filePath) => {
1702
+ const extension = path4.extname(filePath).toLowerCase();
1703
+ switch (extension) {
1704
+ case ".apng":
1705
+ return "image/apng";
1706
+ case ".avif":
1707
+ return "image/avif";
1708
+ case ".gif":
1709
+ return "image/gif";
1710
+ case ".jpeg":
1711
+ return "image/jpeg";
1712
+ case ".jpg":
1713
+ return "image/jpeg";
1714
+ case ".png":
1715
+ return "image/png";
1716
+ case ".svg":
1717
+ return "image/svg+xml";
1718
+ case ".webp":
1719
+ return "image/webp";
1720
+ default:
1721
+ return void 0;
1722
+ }
1723
+ };
1724
+ var readDirectoryEntries = async (dirPath) => {
1725
+ const entries = await fs3.readdir(dirPath, { withFileTypes: true });
1726
+ const resolvedEntries = await Promise.all(
1727
+ entries.map(async (entry) => {
1728
+ const entryPath = path4.join(dirPath, entry.name);
1729
+ let isDirectory = entry.isDirectory();
1730
+ if (!isDirectory && entry.isSymbolicLink()) {
1731
+ try {
1732
+ const stats = await fs3.stat(entryPath);
1733
+ isDirectory = stats.isDirectory();
1734
+ } catch {
1735
+ }
1736
+ }
1737
+ const entryType = isDirectory ? "directory" : "file";
1738
+ return {
1739
+ name: entry.name,
1740
+ path: entryPath,
1741
+ type: entryType,
1742
+ hidden: entry.name.startsWith(".")
1743
+ };
1744
+ })
1745
+ );
1746
+ return resolvedEntries.sort((left, right) => {
1747
+ if (left.type !== right.type) {
1748
+ return left.type === "directory" ? -1 : 1;
1749
+ }
1750
+ return left.name.localeCompare(right.name);
1751
+ });
1752
+ };
1753
+ var filterVisibleEntries = (entries) => entries.filter((entry) => !entry.hidden);
1754
+ var buildHostFsRoots = async () => {
1755
+ const homePath = homedir();
1756
+ return {
1757
+ homePath,
1758
+ roots: [{ name: "Home", path: homePath }]
1759
+ };
1760
+ };
1761
+ var SocketClient = class extends EventEmitter3 {
1762
+ constructor(options) {
1763
+ super();
1764
+ this.options = options;
1765
+ this.socket = io(`${options.config.gatewayUrl}/cli`, {
1766
+ path: "/socket.io",
1767
+ reconnection: true,
1768
+ reconnectionAttempts: Number.POSITIVE_INFINITY,
1769
+ reconnectionDelay: 1e3,
1770
+ reconnectionDelayMax: 3e4,
1771
+ transports: ["websocket"],
1772
+ autoConnect: false,
1773
+ extraHeaders: {
1774
+ "x-api-key": options.apiKey
1775
+ }
1776
+ });
1777
+ this.setupEventHandlers();
1778
+ this.setupRpcHandlers();
1779
+ this.setupSessionManagerListeners();
1780
+ }
1781
+ socket;
1782
+ connected = false;
1783
+ reconnectAttempts = 0;
1784
+ heartbeatInterval;
1785
+ setupEventHandlers() {
1786
+ this.socket.on("connect", () => {
1787
+ logger.info(
1788
+ { gatewayUrl: this.options.config.gatewayUrl },
1789
+ "gateway_connected"
1790
+ );
1791
+ this.connected = true;
1792
+ this.reconnectAttempts = 0;
1793
+ logger.info("gateway_register_start");
1794
+ this.register();
1795
+ this.startHeartbeat();
1796
+ this.emit("connected");
1797
+ });
1798
+ this.socket.on("disconnect", (reason) => {
1799
+ logger.warn({ reason }, "gateway_disconnected");
1800
+ this.connected = false;
1801
+ this.stopHeartbeat();
1802
+ this.emit("disconnected", reason);
1803
+ });
1804
+ this.socket.on("connect_error", (error) => {
1805
+ this.reconnectAttempts++;
1806
+ if (this.reconnectAttempts <= 3 || this.reconnectAttempts % 10 === 0) {
1807
+ logger.error(
1808
+ { attempt: this.reconnectAttempts, err: error },
1809
+ "gateway_connect_error"
1810
+ );
1811
+ }
1812
+ });
1813
+ this.socket.on("cli:registered", async (info) => {
1814
+ logger.info({ machineId: info.machineId }, "gateway_registered");
1815
+ try {
1816
+ const { sessions, capabilities } = await this.options.sessionManager.discoverSessions();
1817
+ if (sessions.length > 0) {
1818
+ this.socket.emit("sessions:discovered", { sessions, capabilities });
1819
+ logger.info(
1820
+ { count: sessions.length, capabilities },
1821
+ "historical_sessions_discovered"
1822
+ );
1823
+ }
1824
+ } catch (error) {
1825
+ logger.warn({ err: error }, "session_discovery_failed");
1826
+ }
1827
+ });
1828
+ this.socket.on("cli:error", (error) => {
1829
+ logger.error({ err: error }, "gateway_auth_error");
1830
+ this.emit("auth_error", error);
1831
+ });
1832
+ }
1833
+ setupRpcHandlers() {
1834
+ const { sessionManager } = this.options;
1835
+ this.socket.on("rpc:session:create", async (request) => {
1836
+ try {
1837
+ logger.info({ requestId: request.requestId }, "rpc_session_create");
1838
+ const session = await sessionManager.createSession(request.params);
1839
+ this.sendRpcResponse(request.requestId, session);
1840
+ } catch (error) {
1841
+ logger.error(
1842
+ { err: error, requestId: request.requestId },
1843
+ "rpc_session_create_error"
1844
+ );
1845
+ this.sendRpcError(request.requestId, error);
1846
+ }
1847
+ });
1848
+ this.socket.on("rpc:session:close", async (request) => {
1849
+ try {
1850
+ logger.info(
1851
+ { requestId: request.requestId, sessionId: request.params.sessionId },
1852
+ "rpc_session_close"
1853
+ );
1854
+ await sessionManager.closeSession(request.params.sessionId);
1855
+ this.sendRpcResponse(request.requestId, { ok: true });
1856
+ } catch (error) {
1857
+ logger.error(
1858
+ {
1859
+ err: error,
1860
+ requestId: request.requestId,
1861
+ sessionId: request.params.sessionId
1862
+ },
1863
+ "rpc_session_close_error"
1864
+ );
1865
+ this.sendRpcError(request.requestId, error);
1866
+ }
1867
+ });
1868
+ this.socket.on("rpc:session:cancel", async (request) => {
1869
+ try {
1870
+ logger.info(
1871
+ { requestId: request.requestId, sessionId: request.params.sessionId },
1872
+ "rpc_session_cancel"
1873
+ );
1874
+ await sessionManager.cancelSession(request.params.sessionId);
1875
+ this.sendRpcResponse(request.requestId, { ok: true });
1876
+ } catch (error) {
1877
+ logger.error(
1878
+ {
1879
+ err: error,
1880
+ requestId: request.requestId,
1881
+ sessionId: request.params.sessionId
1882
+ },
1883
+ "rpc_session_cancel_error"
1884
+ );
1885
+ this.sendRpcError(request.requestId, error);
1886
+ }
1887
+ });
1888
+ this.socket.on("rpc:session:mode", async (request) => {
1889
+ try {
1890
+ logger.info(
1891
+ {
1892
+ requestId: request.requestId,
1893
+ sessionId: request.params.sessionId,
1894
+ modeId: request.params.modeId
1895
+ },
1896
+ "rpc_session_mode"
1897
+ );
1898
+ const session = await sessionManager.setSessionMode(
1899
+ request.params.sessionId,
1900
+ request.params.modeId
1901
+ );
1902
+ this.sendRpcResponse(request.requestId, session);
1903
+ } catch (error) {
1904
+ logger.error(
1905
+ {
1906
+ err: error,
1907
+ requestId: request.requestId,
1908
+ sessionId: request.params.sessionId,
1909
+ modeId: request.params.modeId
1910
+ },
1911
+ "rpc_session_mode_error"
1912
+ );
1913
+ this.sendRpcError(request.requestId, error);
1914
+ }
1915
+ });
1916
+ this.socket.on("rpc:session:model", async (request) => {
1917
+ try {
1918
+ logger.info(
1919
+ {
1920
+ requestId: request.requestId,
1921
+ sessionId: request.params.sessionId,
1922
+ modelId: request.params.modelId
1923
+ },
1924
+ "rpc_session_model"
1925
+ );
1926
+ const session = await sessionManager.setSessionModel(
1927
+ request.params.sessionId,
1928
+ request.params.modelId
1929
+ );
1930
+ this.sendRpcResponse(request.requestId, session);
1931
+ } catch (error) {
1932
+ logger.error(
1933
+ {
1934
+ err: error,
1935
+ requestId: request.requestId,
1936
+ sessionId: request.params.sessionId,
1937
+ modelId: request.params.modelId
1938
+ },
1939
+ "rpc_session_model_error"
1940
+ );
1941
+ this.sendRpcError(request.requestId, error);
1942
+ }
1943
+ });
1944
+ this.socket.on("rpc:message:send", async (request) => {
1945
+ const requestStart = process.hrtime.bigint();
1946
+ try {
1947
+ const { sessionId, prompt } = request.params;
1948
+ logger.info(
1949
+ {
1950
+ requestId: request.requestId,
1951
+ sessionId,
1952
+ promptBlocks: prompt.length
1953
+ },
1954
+ "rpc_message_send"
1955
+ );
1956
+ logger.debug(
1957
+ {
1958
+ requestId: request.requestId,
1959
+ sessionId,
1960
+ promptBlocks: prompt.length
1961
+ },
1962
+ "rpc_message_send_start"
1963
+ );
1964
+ const record = sessionManager.getSession(sessionId);
1965
+ if (!record) {
1966
+ throw new Error("Session not found");
1967
+ }
1968
+ sessionManager.touchSession(sessionId);
1969
+ const result = await record.connection.prompt(
1970
+ sessionId,
1971
+ prompt
1972
+ );
1973
+ sessionManager.touchSession(sessionId);
1974
+ this.sendRpcResponse(request.requestId, {
1975
+ stopReason: result.stopReason
1976
+ });
1977
+ const durationMs = Number(process.hrtime.bigint() - requestStart) / 1e6;
1978
+ logger.info(
1979
+ {
1980
+ requestId: request.requestId,
1981
+ sessionId,
1982
+ stopReason: result.stopReason,
1983
+ durationMs
1984
+ },
1985
+ "rpc_message_send_complete"
1986
+ );
1987
+ logger.debug(
1988
+ {
1989
+ requestId: request.requestId,
1990
+ sessionId,
1991
+ durationMs
1992
+ },
1993
+ "rpc_message_send_finish"
1994
+ );
1995
+ } catch (error) {
1996
+ const durationMs = Number(process.hrtime.bigint() - requestStart) / 1e6;
1997
+ logger.error(
1998
+ {
1999
+ err: error,
2000
+ requestId: request.requestId,
2001
+ sessionId: request.params.sessionId,
2002
+ promptBlocks: request.params.prompt.length,
2003
+ durationMs
2004
+ },
2005
+ "rpc_message_send_error"
2006
+ );
2007
+ this.sendRpcError(request.requestId, error);
2008
+ }
2009
+ });
2010
+ this.socket.on("rpc:permission:decision", async (request) => {
2011
+ try {
2012
+ const { sessionId, requestId, outcome } = request.params;
2013
+ logger.info(
2014
+ { requestId: request.requestId, sessionId, outcome },
2015
+ "rpc_permission_decision"
2016
+ );
2017
+ sessionManager.resolvePermissionRequest(sessionId, requestId, outcome);
2018
+ this.sendRpcResponse(request.requestId, { ok: true });
2019
+ } catch (error) {
2020
+ logger.error(
2021
+ {
2022
+ err: error,
2023
+ requestId: request.requestId,
2024
+ sessionId: request.params.sessionId,
2025
+ permissionRequestId: request.params.requestId,
2026
+ outcome: request.params.outcome
2027
+ },
2028
+ "rpc_permission_decision_error"
2029
+ );
2030
+ this.sendRpcError(request.requestId, error);
2031
+ }
2032
+ });
2033
+ this.socket.on("rpc:fs:roots", async (request) => {
2034
+ try {
2035
+ logger.debug(
2036
+ { requestId: request.requestId, sessionId: request.params.sessionId },
2037
+ "rpc_fs_roots"
2038
+ );
2039
+ const record = sessionManager.getSession(request.params.sessionId);
2040
+ if (!record || !record.cwd) {
2041
+ throw new Error("Session not found or no working directory");
2042
+ }
2043
+ const root = {
2044
+ name: SESSION_ROOT_NAME,
2045
+ path: record.cwd
2046
+ };
2047
+ this.sendRpcResponse(request.requestId, { root });
2048
+ } catch (error) {
2049
+ logger.error(
2050
+ {
2051
+ err: error,
2052
+ requestId: request.requestId,
2053
+ sessionId: request.params.sessionId
2054
+ },
2055
+ "rpc_fs_roots_error"
2056
+ );
2057
+ this.sendRpcError(request.requestId, error);
2058
+ }
2059
+ });
2060
+ this.socket.on("rpc:hostfs:roots", async (request) => {
2061
+ try {
2062
+ logger.debug(
2063
+ {
2064
+ requestId: request.requestId,
2065
+ machineId: request.params.machineId
2066
+ },
2067
+ "rpc_hostfs_roots"
2068
+ );
2069
+ const result = await buildHostFsRoots();
2070
+ this.sendRpcResponse(request.requestId, result);
2071
+ } catch (error) {
2072
+ logger.error(
2073
+ {
2074
+ err: error,
2075
+ requestId: request.requestId,
2076
+ machineId: request.params.machineId
2077
+ },
2078
+ "rpc_hostfs_roots_error"
2079
+ );
2080
+ this.sendRpcError(request.requestId, error);
2081
+ }
2082
+ });
2083
+ this.socket.on("rpc:hostfs:entries", async (request) => {
2084
+ try {
2085
+ const { path: requestPath, machineId } = request.params;
2086
+ logger.debug(
2087
+ { requestId: request.requestId, machineId, path: requestPath },
2088
+ "rpc_hostfs_entries"
2089
+ );
2090
+ const entries = await readDirectoryEntries(requestPath);
2091
+ this.sendRpcResponse(request.requestId, {
2092
+ path: requestPath,
2093
+ entries: filterVisibleEntries(entries)
2094
+ });
2095
+ } catch (error) {
2096
+ logger.error(
2097
+ {
2098
+ err: error,
2099
+ requestId: request.requestId,
2100
+ machineId: request.params.machineId
2101
+ },
2102
+ "rpc_hostfs_entries_error"
2103
+ );
2104
+ this.sendRpcError(request.requestId, error);
2105
+ }
2106
+ });
2107
+ this.socket.on("rpc:fs:entries", async (request) => {
2108
+ try {
2109
+ const { sessionId, path: requestPath } = request.params;
2110
+ logger.debug(
2111
+ { requestId: request.requestId, sessionId, path: requestPath },
2112
+ "rpc_fs_entries"
2113
+ );
2114
+ const record = sessionManager.getSession(sessionId);
2115
+ if (!record || !record.cwd) {
2116
+ throw new Error("Session not found or no working directory");
2117
+ }
2118
+ const resolved = requestPath ? path4.isAbsolute(requestPath) ? requestPath : path4.join(record.cwd, requestPath) : record.cwd;
2119
+ const entries = await readDirectoryEntries(resolved);
2120
+ this.sendRpcResponse(request.requestId, { path: resolved, entries });
2121
+ } catch (error) {
2122
+ logger.error(
2123
+ {
2124
+ err: error,
2125
+ requestId: request.requestId,
2126
+ sessionId: request.params.sessionId
2127
+ },
2128
+ "rpc_fs_entries_error"
2129
+ );
2130
+ this.sendRpcError(request.requestId, error);
2131
+ }
2132
+ });
2133
+ this.socket.on("rpc:fs:file", async (request) => {
2134
+ try {
2135
+ const { sessionId, path: requestPath } = request.params;
2136
+ logger.debug(
2137
+ { requestId: request.requestId, sessionId, path: requestPath },
2138
+ "rpc_fs_file"
2139
+ );
2140
+ const record = sessionManager.getSession(sessionId);
2141
+ if (!record || !record.cwd) {
2142
+ throw new Error("Session not found or no working directory");
2143
+ }
2144
+ const resolved = path4.isAbsolute(requestPath) ? requestPath : path4.join(record.cwd, requestPath);
2145
+ const mimeType = resolveImageMimeType(resolved);
2146
+ if (mimeType) {
2147
+ const buffer = await fs3.readFile(resolved);
2148
+ const preview2 = {
2149
+ path: resolved,
2150
+ previewType: "image",
2151
+ content: `data:${mimeType};base64,${buffer.toString("base64")}`,
2152
+ mimeType
2153
+ };
2154
+ this.sendRpcResponse(request.requestId, preview2);
2155
+ return;
2156
+ }
2157
+ const content = await fs3.readFile(resolved, "utf8");
2158
+ const preview = {
2159
+ path: resolved,
2160
+ previewType: "code",
2161
+ content
2162
+ };
2163
+ this.sendRpcResponse(request.requestId, preview);
2164
+ } catch (error) {
2165
+ logger.error(
2166
+ {
2167
+ err: error,
2168
+ requestId: request.requestId,
2169
+ sessionId: request.params.sessionId
2170
+ },
2171
+ "rpc_fs_file_error"
2172
+ );
2173
+ this.sendRpcError(request.requestId, error);
2174
+ }
2175
+ });
2176
+ this.socket.on("rpc:fs:resources", async (request) => {
2177
+ try {
2178
+ const { sessionId } = request.params;
2179
+ logger.debug(
2180
+ { requestId: request.requestId, sessionId },
2181
+ "rpc_fs_resources"
2182
+ );
2183
+ const record = sessionManager.getSession(sessionId);
2184
+ if (!record || !record.cwd) {
2185
+ throw new Error("Session not found or no working directory");
2186
+ }
2187
+ const entries = await this.listSessionResources(record.cwd);
2188
+ this.sendRpcResponse(request.requestId, {
2189
+ rootPath: record.cwd,
2190
+ entries
2191
+ });
2192
+ } catch (error) {
2193
+ logger.error(
2194
+ {
2195
+ err: error,
2196
+ requestId: request.requestId,
2197
+ sessionId: request.params.sessionId
2198
+ },
2199
+ "rpc_fs_resources_error"
2200
+ );
2201
+ this.sendRpcError(request.requestId, error);
2202
+ }
2203
+ });
2204
+ this.socket.on("rpc:sessions:discover", async (request) => {
2205
+ try {
2206
+ const { cwd, backendId } = request.params;
2207
+ logger.info(
2208
+ { requestId: request.requestId, cwd, backendId },
2209
+ "rpc_sessions_discover"
2210
+ );
2211
+ const result = await sessionManager.discoverSessions({
2212
+ cwd,
2213
+ backendId
2214
+ });
2215
+ this.sendRpcResponse(request.requestId, result);
2216
+ } catch (error) {
2217
+ logger.error(
2218
+ {
2219
+ err: error,
2220
+ requestId: request.requestId
2221
+ },
2222
+ "rpc_sessions_discover_error"
2223
+ );
2224
+ this.sendRpcError(request.requestId, error);
2225
+ }
2226
+ });
2227
+ this.socket.on("rpc:session:load", async (request) => {
2228
+ try {
2229
+ const { sessionId, cwd } = request.params;
2230
+ logger.info(
2231
+ { requestId: request.requestId, sessionId, cwd },
2232
+ "rpc_session_load"
2233
+ );
2234
+ const session = await sessionManager.loadSession(sessionId, cwd);
2235
+ this.sendRpcResponse(request.requestId, session);
2236
+ } catch (error) {
2237
+ logger.error(
2238
+ {
2239
+ err: error,
2240
+ requestId: request.requestId,
2241
+ sessionId: request.params.sessionId
2242
+ },
2243
+ "rpc_session_load_error"
2244
+ );
2245
+ this.sendRpcError(request.requestId, error);
2246
+ }
2247
+ });
2248
+ this.socket.on("rpc:session:resume", async (request) => {
2249
+ try {
2250
+ const { sessionId, cwd } = request.params;
2251
+ logger.info(
2252
+ { requestId: request.requestId, sessionId, cwd },
2253
+ "rpc_session_resume"
2254
+ );
2255
+ const session = await sessionManager.resumeSession(sessionId, cwd);
2256
+ this.sendRpcResponse(request.requestId, session);
2257
+ } catch (error) {
2258
+ logger.error(
2259
+ {
2260
+ err: error,
2261
+ requestId: request.requestId,
2262
+ sessionId: request.params.sessionId
2263
+ },
2264
+ "rpc_session_resume_error"
2265
+ );
2266
+ this.sendRpcError(request.requestId, error);
2267
+ }
2268
+ });
2269
+ }
2270
+ setupSessionManagerListeners() {
2271
+ const { sessionManager } = this.options;
2272
+ sessionManager.onSessionUpdate((notification) => {
2273
+ if (this.connected) {
2274
+ this.socket.emit(
2275
+ "session:update",
2276
+ notification
2277
+ );
2278
+ }
2279
+ });
2280
+ sessionManager.onSessionError((payload) => {
2281
+ if (this.connected) {
2282
+ this.socket.emit("session:error", payload);
2283
+ }
2284
+ });
2285
+ sessionManager.onPermissionRequest((payload) => {
2286
+ if (this.connected) {
2287
+ this.socket.emit("permission:request", payload);
2288
+ }
2289
+ });
2290
+ sessionManager.onPermissionResult((payload) => {
2291
+ if (this.connected) {
2292
+ this.socket.emit("permission:result", payload);
2293
+ }
2294
+ });
2295
+ sessionManager.onTerminalOutput((event) => {
2296
+ if (this.connected) {
2297
+ this.socket.emit("terminal:output", event);
2298
+ }
2299
+ });
2300
+ sessionManager.onSessionsChanged((payload) => {
2301
+ if (this.connected) {
2302
+ logger.info(
2303
+ {
2304
+ added: payload.added.length,
2305
+ updated: payload.updated.length,
2306
+ removed: payload.removed.length
2307
+ },
2308
+ "sessions_changed_emit"
2309
+ );
2310
+ this.socket.emit("sessions:changed", payload);
2311
+ }
2312
+ });
2313
+ }
2314
+ async listSessionResources(rootPath) {
2315
+ const ig = await loadGitignore(rootPath);
2316
+ const allFiles = await this.listAllFiles(rootPath, ig, rootPath, []);
2317
+ return allFiles.map((filePath) => ({
2318
+ name: path4.basename(filePath),
2319
+ relativePath: path4.relative(rootPath, filePath),
2320
+ path: filePath
2321
+ }));
2322
+ }
2323
+ async listAllFiles(rootPath, ig, baseDir, collected = []) {
2324
+ if (collected.length >= MAX_RESOURCE_FILES) {
2325
+ return collected;
2326
+ }
2327
+ const entries = await fs3.readdir(rootPath, { withFileTypes: true });
2328
+ for (const entry of entries) {
2329
+ if (collected.length >= MAX_RESOURCE_FILES) {
2330
+ break;
2331
+ }
2332
+ const entryPath = path4.join(rootPath, entry.name);
2333
+ const relativePath = path4.relative(baseDir, entryPath);
2334
+ const checkPath = entry.isDirectory() ? `${relativePath}/` : relativePath;
2335
+ if (ig.ignores(checkPath)) {
2336
+ continue;
2337
+ }
2338
+ if (entry.isDirectory()) {
2339
+ await this.listAllFiles(entryPath, ig, baseDir, collected);
2340
+ } else if (entry.isFile()) {
2341
+ collected.push(entryPath);
2342
+ }
2343
+ }
2344
+ return collected;
2345
+ }
2346
+ sendRpcResponse(requestId, result) {
2347
+ const response = { requestId, result };
2348
+ this.socket.emit("rpc:response", response);
2349
+ logger.debug({ requestId }, "rpc_response_sent");
2350
+ }
2351
+ sendRpcError(requestId, error) {
2352
+ const message = error instanceof Error ? error.message : "Unknown error";
2353
+ const detail = error instanceof Error ? error.stack : void 0;
2354
+ logger.error(
2355
+ {
2356
+ requestId,
2357
+ err: error,
2358
+ message,
2359
+ detail
2360
+ },
2361
+ "rpc_response_error_sent"
2362
+ );
2363
+ const response = {
2364
+ requestId,
2365
+ error: {
2366
+ code: "INTERNAL_ERROR",
2367
+ message,
2368
+ retryable: true,
2369
+ scope: "request",
2370
+ detail
2371
+ }
2372
+ };
2373
+ this.socket.emit("rpc:response", response);
2374
+ }
2375
+ register() {
2376
+ const { config, sessionManager } = this.options;
2377
+ logger.info({ machineId: config.machineId }, "cli_register_emit");
2378
+ this.socket.emit("cli:register", {
2379
+ machineId: config.machineId,
2380
+ hostname: config.hostname,
2381
+ version: config.clientVersion,
2382
+ backends: config.acpBackends.map((backend) => ({
2383
+ backendId: backend.id,
2384
+ backendLabel: backend.label
2385
+ })),
2386
+ defaultBackendId: config.defaultAcpBackendId
2387
+ });
2388
+ logger.info({ machineId: config.machineId }, "cli_register_sessions_list");
2389
+ this.socket.emit("sessions:list", sessionManager.listSessions());
2390
+ }
2391
+ startHeartbeat() {
2392
+ this.stopHeartbeat();
2393
+ this.heartbeatInterval = setInterval(() => {
2394
+ if (this.connected) {
2395
+ this.socket.emit("cli:heartbeat");
2396
+ this.socket.emit(
2397
+ "sessions:list",
2398
+ this.options.sessionManager.listSessions()
2399
+ );
2400
+ }
2401
+ }, 3e4);
2402
+ }
2403
+ stopHeartbeat() {
2404
+ if (this.heartbeatInterval) {
2405
+ clearInterval(this.heartbeatInterval);
2406
+ this.heartbeatInterval = void 0;
2407
+ }
2408
+ }
2409
+ connect() {
2410
+ this.socket.connect();
2411
+ }
2412
+ disconnect() {
2413
+ this.stopHeartbeat();
2414
+ this.socket.disconnect();
2415
+ }
2416
+ isConnected() {
2417
+ return this.connected;
2418
+ }
2419
+ };
2420
+
2421
+ // src/daemon/daemon.ts
2422
+ var DaemonManager = class {
2423
+ constructor(config) {
2424
+ this.config = config;
2425
+ }
2426
+ async ensureHomeDirectory() {
2427
+ await fs4.mkdir(this.config.homePath, { recursive: true });
2428
+ await fs4.mkdir(this.config.logPath, { recursive: true });
2429
+ }
2430
+ async getPid() {
2431
+ try {
2432
+ const content = await fs4.readFile(this.config.pidFile, "utf8");
2433
+ const pid = Number.parseInt(content.trim(), 10);
2434
+ if (Number.isNaN(pid)) {
2435
+ return null;
2436
+ }
2437
+ try {
2438
+ process.kill(pid, 0);
2439
+ return pid;
2440
+ } catch {
2441
+ await this.removePidFile();
2442
+ return null;
2443
+ }
2444
+ } catch {
2445
+ return null;
2446
+ }
2447
+ }
2448
+ async writePidFile(pid) {
2449
+ await fs4.writeFile(this.config.pidFile, String(pid), "utf8");
2450
+ }
2451
+ async removePidFile() {
2452
+ try {
2453
+ await fs4.unlink(this.config.pidFile);
2454
+ } catch {
2455
+ }
2456
+ }
2457
+ async status() {
2458
+ const pid = await this.getPid();
2459
+ if (!pid) {
2460
+ return { running: false };
2461
+ }
2462
+ return { running: true, pid };
2463
+ }
2464
+ async start(options) {
2465
+ const existingPid = await this.getPid();
2466
+ if (existingPid) {
2467
+ logger.info({ pid: existingPid }, "daemon_already_running");
2468
+ return;
2469
+ }
2470
+ await this.ensureHomeDirectory();
2471
+ if (options?.foreground) {
2472
+ await this.runForeground();
2473
+ } else {
2474
+ await this.spawnBackground();
2475
+ }
2476
+ }
2477
+ async stop() {
2478
+ const pid = await this.getPid();
2479
+ if (!pid) {
2480
+ logger.info("daemon_not_running");
2481
+ return;
2482
+ }
2483
+ try {
2484
+ process.kill(pid, 0);
2485
+ } catch {
2486
+ logger.warn("daemon_pid_missing_cleanup");
2487
+ await this.removePidFile();
2488
+ return;
2489
+ }
2490
+ try {
2491
+ logger.info({ pid }, "daemon_stop_sigterm");
2492
+ process.kill(pid, "SIGTERM");
2493
+ const startTime = Date.now();
2494
+ const timeout = 5e3;
2495
+ while (Date.now() - startTime < timeout) {
2496
+ await new Promise((resolve) => setTimeout(resolve, 100));
2497
+ try {
2498
+ process.kill(pid, 0);
2499
+ } catch {
2500
+ logger.info({ pid }, "daemon_stopped_gracefully");
2501
+ await this.removePidFile();
2502
+ return;
2503
+ }
2504
+ }
2505
+ logger.warn({ pid }, "daemon_force_kill_start");
2506
+ try {
2507
+ process.kill(pid, "SIGKILL");
2508
+ await new Promise((resolve) => setTimeout(resolve, 500));
2509
+ logger.warn({ pid }, "daemon_force_kill_complete");
2510
+ } catch {
2511
+ logger.info({ pid }, "daemon_already_stopped");
2512
+ }
2513
+ await this.removePidFile();
2514
+ } catch (error) {
2515
+ logger.error({ err: error }, "daemon_stop_error");
2516
+ await this.removePidFile();
2517
+ }
2518
+ }
2519
+ async spawnBackground() {
2520
+ const logFile = path5.join(
2521
+ this.config.logPath,
2522
+ `${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}-daemon.log`
2523
+ );
2524
+ const args = process.argv.slice(1).filter((arg) => arg !== "--foreground" && arg !== "-f");
2525
+ args.push("--foreground");
2526
+ const child = spawn2(process.argv[0], args, {
2527
+ detached: true,
2528
+ stdio: ["ignore", "pipe", "pipe"],
2529
+ env: {
2530
+ ...process.env,
2531
+ MOBVIBE_GATEWAY_URL: this.config.gatewayUrl
2532
+ }
2533
+ });
2534
+ if (!child.pid) {
2535
+ logger.error("daemon_spawn_failed");
2536
+ throw new Error("Failed to spawn daemon process");
2537
+ }
2538
+ const logStream = await fs4.open(logFile, "a");
2539
+ const fileHandle = logStream;
2540
+ child.stdout?.on("data", (data) => {
2541
+ fileHandle.write(`[stdout] ${data.toString()}`).catch(() => {
2542
+ });
2543
+ });
2544
+ child.stderr?.on("data", (data) => {
2545
+ fileHandle.write(`[stderr] ${data.toString()}`).catch(() => {
2546
+ });
2547
+ });
2548
+ child.on("exit", (code, signal) => {
2549
+ fileHandle.write(`[exit] Process exited with code ${code}, signal ${signal}
2550
+ `).catch(() => {
2551
+ });
2552
+ fileHandle.close().catch(() => {
2553
+ });
2554
+ });
2555
+ child.unref();
2556
+ logger.info({ pid: child.pid }, "daemon_started");
2557
+ console.log(`Logs: ${logFile}`);
2558
+ logger.info({ logFile }, "daemon_log_path");
2559
+ }
2560
+ async runForeground() {
2561
+ const pid = process.pid;
2562
+ await this.writePidFile(pid);
2563
+ logger.info({ pid }, "daemon_starting");
2564
+ logger.info({ gatewayUrl: this.config.gatewayUrl }, "daemon_gateway_url");
2565
+ logger.info({ machineId: this.config.machineId }, "daemon_machine_id");
2566
+ const apiKey = await getApiKey();
2567
+ if (!apiKey) {
2568
+ logger.error("daemon_api_key_missing");
2569
+ console.error(
2570
+ `[mobvibe-cli] No API key found. Run 'mobvibe login' to authenticate.`
2571
+ );
2572
+ logger.warn("daemon_exit_missing_api_key");
2573
+ process.exit(1);
2574
+ }
2575
+ logger.info("daemon_api_key_loaded");
2576
+ const sessionManager = new SessionManager(this.config);
2577
+ const socketClient = new SocketClient({
2578
+ config: this.config,
2579
+ sessionManager,
2580
+ apiKey
2581
+ });
2582
+ let shuttingDown = false;
2583
+ const shutdown = async (signal) => {
2584
+ if (shuttingDown) {
2585
+ logger.warn({ signal }, "daemon_shutdown_already_running");
2586
+ return;
2587
+ }
2588
+ shuttingDown = true;
2589
+ logger.info({ signal }, "daemon_shutdown_start");
2590
+ try {
2591
+ socketClient.disconnect();
2592
+ await sessionManager.closeAll();
2593
+ await this.removePidFile();
2594
+ logger.info({ signal }, "daemon_shutdown_complete");
2595
+ } catch (error) {
2596
+ logger.error({ err: error, signal }, "daemon_shutdown_error");
2597
+ }
2598
+ };
2599
+ process.on("SIGINT", () => {
2600
+ shutdown("SIGINT").catch((error) => {
2601
+ logger.error({ err: error }, "daemon_shutdown_sigint_error");
2602
+ });
2603
+ });
2604
+ process.on("SIGTERM", () => {
2605
+ shutdown("SIGTERM").catch((error) => {
2606
+ logger.error({ err: error }, "daemon_shutdown_sigterm_error");
2607
+ });
2608
+ });
2609
+ socketClient.connect();
2610
+ await new Promise(() => {
2611
+ });
2612
+ }
2613
+ async logs(options) {
2614
+ const files = await fs4.readdir(this.config.logPath);
2615
+ const logFiles = files.filter((f) => f.endsWith("-daemon.log")).sort().reverse();
2616
+ if (logFiles.length === 0) {
2617
+ logger.warn("daemon_logs_empty");
2618
+ console.log("No log files found");
2619
+ return;
2620
+ }
2621
+ const latestLog = path5.join(this.config.logPath, logFiles[0]);
2622
+ logger.info({ logFile: latestLog }, "daemon_logs_latest");
2623
+ console.log(`Log file: ${latestLog}
2624
+ `);
2625
+ if (options?.follow) {
2626
+ const tail = spawn2("tail", ["-f", latestLog], {
2627
+ stdio: "inherit"
2628
+ });
2629
+ await new Promise((resolve) => {
2630
+ tail.on("close", () => resolve());
2631
+ });
2632
+ } else {
2633
+ const content = await fs4.readFile(latestLog, "utf8");
2634
+ const lines = content.split("\n");
2635
+ const count = options?.lines ?? 50;
2636
+ console.log(lines.slice(-count).join("\n"));
2637
+ }
2638
+ }
2639
+ };
2640
+
2641
+ // src/index.ts
2642
+ var program = new Command();
2643
+ program.name("mobvibe").description("Mobvibe CLI - Connect local ACP backends to the gateway").version("0.0.0");
2644
+ program.command("start").description("Start the mobvibe daemon").option("--gateway <url>", "Gateway URL", process.env.MOBVIBE_GATEWAY_URL).option("--foreground", "Run in foreground instead of detaching").action(async (options) => {
2645
+ if (options.gateway) {
2646
+ process.env.MOBVIBE_GATEWAY_URL = options.gateway;
2647
+ }
2648
+ const config = await getCliConfig();
2649
+ const daemon = new DaemonManager(config);
2650
+ await daemon.start({ foreground: options.foreground });
2651
+ });
2652
+ program.command("stop").description("Stop the mobvibe daemon").action(async () => {
2653
+ const config = await getCliConfig();
2654
+ const daemon = new DaemonManager(config);
2655
+ await daemon.stop();
2656
+ });
2657
+ program.command("status").description("Show daemon status").action(async () => {
2658
+ const config = await getCliConfig();
2659
+ const daemon = new DaemonManager(config);
2660
+ const status = await daemon.status();
2661
+ if (status.running) {
2662
+ logger.info({ pid: status.pid }, "daemon_status_running");
2663
+ console.log(`Daemon is running (PID ${status.pid})`);
2664
+ if (status.connected !== void 0) {
2665
+ console.log(`Connected to gateway: ${status.connected ? "yes" : "no"}`);
2666
+ }
2667
+ if (status.sessionCount !== void 0) {
2668
+ console.log(`Active sessions: ${status.sessionCount}`);
2669
+ }
2670
+ } else {
2671
+ logger.info("daemon_status_not_running");
2672
+ console.log("Daemon is not running");
2673
+ }
2674
+ });
2675
+ program.command("logs").description("Show daemon logs").option("-f, --follow", "Follow log output").option("-n, --lines <number>", "Number of lines to show", "50").action(async (options) => {
2676
+ const config = await getCliConfig();
2677
+ const daemon = new DaemonManager(config);
2678
+ await daemon.logs({
2679
+ follow: options.follow,
2680
+ lines: Number.parseInt(options.lines, 10)
2681
+ });
2682
+ });
2683
+ program.command("login").description("Authenticate with an API key from the WebUI").action(async () => {
2684
+ const result = await login();
2685
+ if (!result.success) {
2686
+ logger.error({ err: result.error }, "login_failed");
2687
+ console.error(`Login failed: ${result.error}`);
2688
+ process.exit(1);
2689
+ }
2690
+ });
2691
+ program.command("logout").description("Remove stored credentials").action(async () => {
2692
+ await logout();
2693
+ });
2694
+ program.command("auth-status").description("Show authentication status").action(async () => {
2695
+ await loginStatus();
2696
+ });
2697
+ async function run() {
2698
+ await program.parseAsync(process.argv);
2699
+ }
2700
+ run().catch((error) => {
2701
+ logger.error({ err: error }, "cli_run_error");
2702
+ console.error("Error:", error.message);
2703
+ process.exit(1);
2704
+ });
2705
+ export {
2706
+ run
2707
+ };
2708
+ //# sourceMappingURL=index.js.map