@bytespell/amux 0.0.1 → 0.0.4

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/bin/cli.js CHANGED
@@ -1,2178 +1,17 @@
1
1
  #!/usr/bin/env node
2
- var __defProp = Object.defineProperty;
3
- var __export = (target, all) => {
4
- for (var name in all)
5
- __defProp(target, name, { get: all[name], enumerable: true });
6
- };
7
-
8
- // bin/cli.ts
9
- import { program } from "commander";
10
-
11
- // src/server.ts
12
- import express from "express";
13
- import cors from "cors";
14
- import { createServer } from "http";
15
- import { WebSocketServer } from "ws";
16
- import { createExpressMiddleware } from "@trpc/server/adapters/express";
17
- import { applyWSSHandler } from "@trpc/server/adapters/ws";
18
-
19
- // src/trpc/trpc.ts
20
- import { initTRPC } from "@trpc/server";
21
- var t = initTRPC.create();
22
- var router = t.router;
23
- var publicProcedure = t.procedure;
24
-
25
- // src/trpc/sessions.ts
26
- import { z } from "zod";
27
-
28
- // src/db/index.ts
29
- import { randomUUID as randomUUID2 } from "crypto";
30
- import Database from "better-sqlite3";
31
- import { drizzle } from "drizzle-orm/better-sqlite3";
32
-
33
- // src/lib/paths.ts
34
- import path from "path";
35
- import os from "os";
36
- import fs from "fs";
37
- var STARTUP_CWD = process.cwd();
38
- function getStartupCwd() {
39
- return STARTUP_CWD;
40
- }
41
- function getDataDir() {
42
- const home = os.homedir();
43
- let dataDir;
44
- switch (process.platform) {
45
- case "darwin":
46
- dataDir = path.join(home, "Library", "Application Support", "shella");
47
- break;
48
- case "win32":
49
- dataDir = path.join(process.env.APPDATA || path.join(home, "AppData", "Roaming"), "shella");
50
- break;
51
- default:
52
- dataDir = path.join(process.env.XDG_DATA_HOME || path.join(home, ".local", "share"), "shella");
53
- }
54
- return dataDir;
55
- }
56
- function isMockMode() {
57
- return process.env.SHELLA_MOCK_MODE === "true";
58
- }
59
- function getDbPath() {
60
- const filename = isMockMode() ? "shella.mock.db" : "shella.db";
61
- return path.join(getDataDir(), filename);
62
- }
63
- function ensureDir(dir) {
64
- if (!fs.existsSync(dir)) {
65
- fs.mkdirSync(dir, { recursive: true });
66
- }
67
- }
68
-
69
- // src/db/schema.ts
70
- var schema_exports = {};
71
- __export(schema_exports, {
72
- agentConfigs: () => agentConfigs,
73
- appState: () => appState,
74
- sessionEvents: () => sessionEvents,
75
- sessions: () => sessions
76
- });
77
- import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
78
- import { sql } from "drizzle-orm";
79
- var agentConfigs = sqliteTable("agent_configs", {
80
- id: text("id").primaryKey(),
81
- name: text("name").notNull(),
82
- command: text("command").notNull(),
83
- args: text("args", { mode: "json" }).$type().default([]),
84
- env: text("env", { mode: "json" }).$type().default({}),
85
- createdAt: integer("created_at", { mode: "timestamp_ms" }).notNull().default(sql`(unixepoch() * 1000)`)
86
- });
87
- var sessions = sqliteTable("sessions", {
88
- id: text("id").primaryKey(),
89
- directory: text("directory").notNull(),
90
- agentConfigId: text("agent_config_id").notNull().references(() => agentConfigs.id),
91
- // ACP protocol session ID for resuming (internal to agent protocol)
92
- acpSessionId: text("acp_session_id"),
93
- // Title from agent (via session_info_update)
94
- title: text("title"),
95
- model: text("model"),
96
- mode: text("mode"),
97
- createdAt: integer("created_at", { mode: "timestamp_ms" }).notNull().default(sql`(unixepoch() * 1000)`)
98
- });
99
- var appState = sqliteTable("app_state", {
100
- key: text("key").primaryKey(),
101
- value: text("value").notNull()
102
- });
103
- var sessionEvents = sqliteTable("session_events", {
104
- id: text("id").primaryKey(),
105
- sessionId: text("session_id").notNull().references(() => sessions.id, { onDelete: "cascade" }),
106
- turnId: text("turn_id").notNull(),
107
- sequenceNum: integer("sequence_num").notNull(),
108
- eventKind: text("event_kind").notNull(),
109
- payload: text("payload", { mode: "json" }).notNull(),
110
- createdAt: integer("created_at", { mode: "timestamp_ms" }).notNull().default(sql`(unixepoch() * 1000)`)
111
- });
112
-
113
- // src/db/seed.ts
114
- import { randomUUID } from "crypto";
115
- import { execSync } from "child_process";
116
- var MOCK_AGENT_ID = "mock-agent";
117
- function commandExists(cmd) {
118
- try {
119
- execSync(`which ${cmd}`, { stdio: "ignore" });
120
- return true;
121
- } catch {
122
- return false;
123
- }
124
- }
125
- function detectAgents() {
126
- const agents = [];
127
- agents.push({
128
- id: randomUUID(),
129
- name: "Claude",
130
- command: "npx",
131
- args: ["@zed-industries/claude-code-acp"],
132
- env: {}
133
- });
134
- if (commandExists("opencode")) {
135
- agents.push({
136
- id: randomUUID(),
137
- name: "OpenCode",
138
- command: "opencode",
139
- args: ["acp"],
140
- env: {}
141
- });
142
- }
143
- return agents;
144
- }
145
- var MOCK_CONFIGS = [
146
- {
147
- id: MOCK_AGENT_ID,
148
- name: "Test Agent",
149
- command: "__stress__",
150
- args: [],
151
- env: {}
152
- }
153
- ];
154
- function seedAgentConfigs() {
155
- if (isMockMode()) {
156
- const existing2 = db.select().from(agentConfigs).all();
157
- if (existing2.length === 0) {
158
- for (const config2 of MOCK_CONFIGS) {
159
- db.insert(agentConfigs).values(config2).run();
160
- }
161
- console.log("[db] Seeded mock agent configs");
162
- }
163
- seedMockSessions();
164
- return;
165
- }
166
- const detected = detectAgents();
167
- const existing = db.select().from(agentConfigs).all();
168
- let added = 0;
169
- for (const agent of detected) {
170
- const alreadyExists = existing.some(
171
- (e) => e.command === agent.command && JSON.stringify(e.args) === JSON.stringify(agent.args)
172
- );
173
- if (!alreadyExists) {
174
- db.insert(agentConfigs).values(agent).run();
175
- console.log(`[db] Detected new agent: ${agent.name}`);
176
- added++;
177
- }
178
- }
179
- if (added === 0 && detected.length > 0) {
180
- console.log(`[db] ${detected.length} known agent(s) already configured`);
181
- }
182
- }
183
- function seedMockSessions() {
184
- const existing = db.select().from(sessions).all();
185
- if (existing.length > 0) return;
186
- const count = getSessionCount(10);
187
- for (let i = 1; i <= count; i++) {
188
- db.insert(sessions).values({
189
- id: randomUUID(),
190
- directory: "~",
191
- agentConfigId: MOCK_AGENT_ID
192
- }).run();
193
- }
194
- console.log(`[db] Seeded ${count} test sessions`);
195
- }
196
- function getSessionCount(defaultCount) {
197
- const envCount = process.env.SHELLA_MOCK_SESSIONS ?? process.env.SHELLA_MOCK_WINDOWS;
198
- if (envCount) {
199
- const parsed = parseInt(envCount, 10);
200
- if (!isNaN(parsed) && parsed > 0) {
201
- return parsed;
202
- }
203
- }
204
- return defaultCount;
205
- }
206
-
207
- // src/db/index.ts
208
- ensureDir(getDataDir());
209
- var sqlite = new Database(getDbPath());
210
- sqlite.pragma("journal_mode = WAL");
211
- sqlite.pragma("foreign_keys = ON");
212
- var db = drizzle(sqlite, { schema: schema_exports });
213
- sqlite.exec(`
214
- CREATE TABLE IF NOT EXISTS agent_configs (
215
- id TEXT PRIMARY KEY,
216
- name TEXT NOT NULL,
217
- command TEXT NOT NULL,
218
- args TEXT DEFAULT '[]',
219
- env TEXT DEFAULT '{}',
220
- created_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000)
221
- );
222
-
223
- CREATE TABLE IF NOT EXISTS sessions (
224
- id TEXT PRIMARY KEY,
225
- directory TEXT NOT NULL,
226
- agent_config_id TEXT NOT NULL REFERENCES agent_configs(id),
227
- acp_session_id TEXT,
228
- title TEXT,
229
- model TEXT,
230
- mode TEXT,
231
- created_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000)
232
- );
233
-
234
- CREATE TABLE IF NOT EXISTS app_state (
235
- key TEXT PRIMARY KEY,
236
- value TEXT NOT NULL
237
- );
238
-
239
- CREATE TABLE IF NOT EXISTS session_events (
240
- id TEXT PRIMARY KEY,
241
- session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
242
- turn_id TEXT NOT NULL,
243
- sequence_num INTEGER NOT NULL,
244
- event_kind TEXT NOT NULL,
245
- payload TEXT NOT NULL,
246
- created_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000)
247
- );
248
- `);
249
- try {
250
- const oldExists = sqlite.prepare(`SELECT name FROM sqlite_master WHERE type='table' AND name='client_windows'`).get();
251
- const newCount = sqlite.prepare(`SELECT COUNT(*) as count FROM sessions`).get();
252
- if (oldExists && newCount.count === 0) {
253
- console.log("[db] Migrating from client_windows/window_events to sessions/session_events...");
254
- sqlite.exec(`
255
- INSERT INTO sessions (id, directory, agent_config_id, acp_session_id, model, mode, created_at)
256
- SELECT id, directory, agent_config_id, session_id, model, mode, created_at
257
- FROM client_windows;
258
-
259
- INSERT INTO session_events (id, session_id, turn_id, sequence_num, event_kind, payload, created_at)
260
- SELECT id, window_id, turn_id, sequence_num, event_kind, payload, created_at
261
- FROM window_events;
262
-
263
- -- Migrate custom titles to app_state
264
- INSERT OR IGNORE INTO app_state (key, value)
265
- SELECT 'window_title_' || id, title
266
- FROM client_windows
267
- WHERE has_custom_title = 1 AND title != '';
268
-
269
- -- Migrate active window
270
- INSERT OR REPLACE INTO app_state (key, value)
271
- SELECT 'active_window_id', value
272
- FROM app_state
273
- WHERE key = 'active_window_id';
274
- `);
275
- sqlite.exec(`
276
- DROP TABLE IF EXISTS window_events;
277
- DROP TABLE IF EXISTS client_windows;
278
- `);
279
- console.log("[db] Migration complete");
280
- }
281
- } catch (e) {
282
- }
283
- try {
284
- sqlite.exec(`ALTER TABLE sessions ADD COLUMN title TEXT`);
285
- } catch (e) {
286
- }
287
- var orphanedTurns = sqlite.prepare(`
288
- SELECT DISTINCT turn_id, session_id
289
- FROM session_events
290
- WHERE event_kind = 'turn_start'
291
- AND turn_id NOT IN (
292
- SELECT turn_id FROM session_events WHERE event_kind = 'turn_end'
293
- )
294
- `).all();
295
- if (orphanedTurns.length > 0) {
296
- const getMaxSeq = sqlite.prepare(`
297
- SELECT COALESCE(MAX(sequence_num), 0) + 1 as next_seq
298
- FROM session_events WHERE turn_id = ?
299
- `);
300
- const insert = sqlite.prepare(`
301
- INSERT INTO session_events (id, session_id, turn_id, sequence_num, event_kind, payload)
302
- VALUES (?, ?, ?, ?, 'turn_end', '{"amuxEvent":"turn_end"}')
303
- `);
304
- for (const turn of orphanedTurns) {
305
- const { next_seq } = getMaxSeq.get(turn.turn_id);
306
- insert.run(randomUUID2(), turn.session_id, turn.turn_id, next_seq);
307
- }
308
- console.log(`[db] Fixed ${orphanedTurns.length} orphaned turn(s) from previous session`);
309
- }
310
- seedAgentConfigs();
311
-
312
- // src/trpc/sessions.ts
313
- import { eq as eq3 } from "drizzle-orm";
314
- import { randomUUID as randomUUID8 } from "crypto";
315
-
316
- // src/agents/manager.ts
317
- import { EventEmitter } from "events";
318
- import { eq as eq2 } from "drizzle-orm";
319
-
320
- // src/types.ts
321
- function isSessionUpdate(update) {
322
- return "sessionUpdate" in update;
323
- }
324
- function isAmuxEvent(update) {
325
- return "amuxEvent" in update;
326
- }
327
-
328
- // src/agents/eventStore.ts
329
- import { randomUUID as randomUUID3 } from "crypto";
330
- import { eq, asc } from "drizzle-orm";
331
- var turnState = /* @__PURE__ */ new Map();
332
- function startTurn(sessionId) {
333
- const turnId = randomUUID3();
334
- turnState.set(sessionId, { turnId, seq: 0 });
335
- return turnId;
336
- }
337
- function storeEvent(sessionId, update) {
338
- let eventKind;
339
- if (isSessionUpdate(update)) {
340
- eventKind = update.sessionUpdate;
341
- } else if (isAmuxEvent(update) && (update.amuxEvent === "turn_start" || update.amuxEvent === "turn_end")) {
342
- eventKind = update.amuxEvent;
343
- } else {
344
- return;
345
- }
346
- let state = turnState.get(sessionId);
347
- if (!state) {
348
- state = { turnId: randomUUID3(), seq: 0 };
349
- turnState.set(sessionId, state);
350
- }
351
- const seq = state.seq++;
352
- db.insert(sessionEvents).values({
353
- id: randomUUID3(),
354
- sessionId,
355
- turnId: state.turnId,
356
- sequenceNum: seq,
357
- eventKind,
358
- payload: update
359
- }).run();
360
- }
361
- function endTurn(sessionId) {
362
- turnState.delete(sessionId);
363
- }
364
- function getEventsForSession(sessionId) {
365
- const rows = db.select().from(sessionEvents).where(eq(sessionEvents.sessionId, sessionId)).orderBy(asc(sessionEvents.createdAt), asc(sessionEvents.sequenceNum)).all();
366
- return rows.map((row) => row.payload);
367
- }
368
- function clearEventsForSession(sessionId) {
369
- db.delete(sessionEvents).where(eq(sessionEvents.sessionId, sessionId)).run();
370
- }
371
-
372
- // src/agents/backends/acp.ts
373
- import { randomUUID as randomUUID4 } from "crypto";
374
- import { spawn as nodeSpawn } from "child_process";
375
- import { Readable, Writable } from "stream";
376
- import * as fs2 from "fs/promises";
377
2
  import {
378
- ClientSideConnection,
379
- ndJsonStream
380
- } from "@agentclientprotocol/sdk";
381
-
382
- // src/agents/process.ts
383
- import { execa } from "execa";
384
- import treeKill from "tree-kill";
385
- function spawn(options) {
386
- const { command, args = [], cwd, env, timeoutMs } = options;
387
- const subprocess = execa(command, args, {
388
- cwd,
389
- env: { ...process.env, ...env },
390
- stdin: "pipe",
391
- stdout: "pipe",
392
- stderr: "pipe",
393
- timeout: timeoutMs,
394
- cleanup: true,
395
- // Kill on parent exit
396
- windowsHide: true
397
- // Hide console window on Windows
398
- });
399
- const pid = subprocess.pid;
400
- if (!pid) {
401
- throw new Error(`Failed to spawn process: ${command}`);
402
- }
403
- return {
404
- pid,
405
- stdin: subprocess.stdin,
406
- stdout: subprocess.stdout,
407
- stderr: subprocess.stderr,
408
- kill: (signal = "SIGTERM") => killTree(pid, signal),
409
- wait: async () => {
410
- try {
411
- const result = await subprocess;
412
- return { exitCode: result.exitCode };
413
- } catch (err) {
414
- return { exitCode: err.exitCode ?? 1 };
415
- }
416
- }
417
- };
418
- }
419
- function killTree(pid, signal) {
420
- return new Promise((resolve2) => {
421
- treeKill(pid, signal, (err) => {
422
- if (err && !err.message.includes("No such process")) {
423
- console.error(`[process] kill error pid=${pid}:`, err.message);
424
- }
425
- resolve2();
426
- });
427
- });
428
- }
429
-
430
- // src/agents/backends/acp.ts
431
- var INIT_TIMEOUT_MS = 3e4;
432
- function normalizeSessionUpdate(update) {
433
- if (update.sessionUpdate !== "tool_call" && update.sessionUpdate !== "tool_call_update") {
434
- return update;
435
- }
436
- const content = update.content;
437
- if (!content || !Array.isArray(content)) {
438
- return update;
439
- }
440
- const normalizedContent = content.map((item) => {
441
- if (item.type !== "diff") return item;
442
- if (typeof item.content === "string") return item;
443
- const newText = item.newText;
444
- const oldText = item.oldText;
445
- const path4 = item.path;
446
- if (newText === void 0) return item;
447
- const filePath = path4 ?? "file";
448
- const oldLines = oldText ? oldText.split("\n") : [];
449
- const newLines = newText.split("\n");
450
- let unifiedDiff = `Index: ${filePath}
451
- `;
452
- unifiedDiff += "===================================================================\n";
453
- unifiedDiff += `--- ${filePath}
454
- `;
455
- unifiedDiff += `+++ ${filePath}
456
- `;
457
- unifiedDiff += `@@ -${oldLines.length > 0 ? 1 : 0},${oldLines.length} +1,${newLines.length} @@
458
- `;
459
- for (const line of oldLines) {
460
- unifiedDiff += `-${line}
461
- `;
462
- }
463
- for (const line of newLines) {
464
- unifiedDiff += `+${line}
465
- `;
466
- }
467
- return {
468
- type: "diff",
469
- content: unifiedDiff
470
- };
471
- });
472
- return {
473
- ...update,
474
- content: normalizedContent
475
- };
476
- }
477
- function withTimeout(promise, ms, operation) {
478
- return Promise.race([
479
- promise,
480
- new Promise(
481
- (_, reject) => setTimeout(() => reject(new Error(`${operation} timed out after ${ms}ms`)), ms)
482
- )
483
- ]);
484
- }
485
- var AcpBackend = class {
486
- type = "acp";
487
- instances = /* @__PURE__ */ new Map();
488
- onSessionIdChanged;
489
- matches(config2) {
490
- return config2.command !== "__mock__" && config2.command !== "__stress__";
491
- }
492
- async start(sessionId, config2, cwd, existingAcpSessionId, emit) {
493
- if (this.instances.has(sessionId)) {
494
- await this.stop(sessionId);
495
- }
496
- const args = config2.args ?? [];
497
- const env = config2.env ?? {};
498
- console.log(`[acp] Spawning: ${config2.command} ${args.join(" ")} in ${cwd}`);
499
- const proc = spawn({
500
- command: config2.command,
501
- args,
502
- cwd,
503
- env
504
- });
505
- const instance = {
506
- process: proc,
507
- connection: null,
508
- sessionId: "",
509
- pendingPermission: null,
510
- permissionCallbacks: /* @__PURE__ */ new Map(),
511
- emit,
512
- terminals: /* @__PURE__ */ new Map()
513
- };
514
- proc.wait().then(({ exitCode }) => {
515
- if (this.instances.has(sessionId)) {
516
- emit({ amuxEvent: "error", message: `Agent process exited with code ${exitCode}` });
517
- this.instances.delete(sessionId);
518
- }
519
- });
520
- try {
521
- const input = Writable.toWeb(proc.stdin);
522
- const output = Readable.toWeb(proc.stdout);
523
- const stream = ndJsonStream(input, output);
524
- const client = this.createClient(sessionId, instance);
525
- instance.connection = new ClientSideConnection(() => client, stream);
526
- const initResult = await withTimeout(
527
- instance.connection.initialize({
528
- protocolVersion: 1,
529
- clientCapabilities: {
530
- fs: { readTextFile: true, writeTextFile: true },
531
- terminal: true
532
- }
533
- }),
534
- INIT_TIMEOUT_MS,
535
- "Agent initialization"
536
- );
537
- console.log(`[acp] Initialized agent: ${initResult.agentInfo?.name} v${initResult.agentInfo?.version}`);
538
- const canResume = initResult.agentCapabilities?.sessionCapabilities?.resume !== void 0;
539
- let acpSessionId;
540
- let sessionResult;
541
- if (existingAcpSessionId && canResume) {
542
- let resumeSucceeded = false;
543
- try {
544
- console.log(`[acp] Resuming ACP session ${existingAcpSessionId}...`);
545
- sessionResult = await withTimeout(
546
- instance.connection.unstable_resumeSession({
547
- sessionId: existingAcpSessionId,
548
- cwd,
549
- mcpServers: []
550
- }),
551
- INIT_TIMEOUT_MS,
552
- "Session resume"
553
- );
554
- await new Promise((resolve2) => setTimeout(resolve2, 100));
555
- acpSessionId = existingAcpSessionId;
556
- console.log(`[acp] ACP session resumed successfully`);
557
- resumeSucceeded = true;
558
- } catch (resumeErr) {
559
- console.log(`[acp] Resume failed, creating new session:`, resumeErr);
560
- }
561
- if (!resumeSucceeded) {
562
- sessionResult = await withTimeout(
563
- instance.connection.newSession({ cwd, mcpServers: [] }),
564
- INIT_TIMEOUT_MS,
565
- "New session creation"
566
- );
567
- acpSessionId = sessionResult.sessionId;
568
- console.log(`[acp] New ACP session created: ${acpSessionId}`);
569
- this.onSessionIdChanged?.(sessionId, acpSessionId);
570
- }
571
- } else {
572
- console.log(`[acp] Creating new ACP session in ${cwd}...`);
573
- sessionResult = await withTimeout(
574
- instance.connection.newSession({ cwd, mcpServers: [] }),
575
- INIT_TIMEOUT_MS,
576
- "New session creation"
577
- );
578
- acpSessionId = sessionResult.sessionId;
579
- console.log(`[acp] ACP session created: ${acpSessionId}`);
580
- this.onSessionIdChanged?.(sessionId, acpSessionId);
581
- }
582
- instance.sessionId = acpSessionId;
583
- this.instances.set(sessionId, instance);
584
- const models = sessionResult?.models?.availableModels;
585
- const modes = sessionResult?.modes?.availableModes;
586
- if (sessionResult?.modes) {
587
- emit({
588
- sessionUpdate: "current_mode_update",
589
- currentModeId: sessionResult.modes.currentModeId
590
- });
591
- }
592
- return {
593
- sessionId: acpSessionId,
594
- models,
595
- modes
596
- };
597
- } catch (err) {
598
- console.error(`[acp] Error starting agent for session ${sessionId}:`, err);
599
- await proc.kill();
600
- throw err;
601
- }
602
- }
603
- createClient(_sessionId, instance) {
604
- return {
605
- async requestPermission(params) {
606
- const requestId = randomUUID4();
607
- const permission = {
608
- requestId,
609
- toolCallId: params.toolCall.toolCallId,
610
- title: params.toolCall.title ?? "Permission Required",
611
- options: params.options.map((o) => ({
612
- optionId: o.optionId,
613
- name: o.name,
614
- kind: o.kind
615
- }))
616
- };
617
- instance.pendingPermission = permission;
618
- instance.emit({ amuxEvent: "permission_request", permission });
619
- return new Promise((resolve2, reject) => {
620
- instance.permissionCallbacks.set(requestId, {
621
- resolve: (optionId) => {
622
- instance.pendingPermission = null;
623
- instance.emit({ amuxEvent: "permission_cleared" });
624
- resolve2({ outcome: { outcome: "selected", optionId } });
625
- },
626
- reject
627
- });
628
- });
629
- },
630
- async sessionUpdate(params) {
631
- console.log(`[acp] sessionUpdate received:`, JSON.stringify(params));
632
- const normalized = normalizeSessionUpdate(params.update);
633
- instance.emit(normalized);
634
- },
635
- async readTextFile(params) {
636
- const content = await fs2.readFile(params.path, "utf-8");
637
- return { content };
638
- },
639
- async writeTextFile(params) {
640
- await fs2.writeFile(params.path, params.content);
641
- return {};
642
- },
643
- async createTerminal(params) {
644
- console.log(`[acp] createTerminal request:`, JSON.stringify(params));
645
- const terminalId = randomUUID4();
646
- const outputByteLimit = params.outputByteLimit ?? 1024 * 1024;
647
- const termProc = nodeSpawn(params.command, params.args ?? [], {
648
- cwd: params.cwd ?? void 0,
649
- env: params.env ? { ...process.env, ...Object.fromEntries(params.env.map((e) => [e.name, e.value])) } : process.env,
650
- shell: true,
651
- stdio: ["ignore", "pipe", "pipe"]
652
- });
653
- const terminal = {
654
- process: termProc,
655
- output: "",
656
- exitCode: null,
657
- signal: null,
658
- truncated: false,
659
- outputByteLimit
660
- };
661
- const appendOutput = (data) => {
662
- terminal.output += data.toString();
663
- if (terminal.output.length > terminal.outputByteLimit) {
664
- terminal.output = terminal.output.slice(-terminal.outputByteLimit);
665
- terminal.truncated = true;
666
- }
667
- };
668
- termProc.stdout?.on("data", appendOutput);
669
- termProc.stderr?.on("data", appendOutput);
670
- termProc.on("exit", (code, signal) => {
671
- console.log(`[acp] Terminal ${terminalId} exited with code ${code}, signal ${signal}`);
672
- terminal.exitCode = code ?? null;
673
- terminal.signal = signal ?? null;
674
- });
675
- termProc.on("error", (err) => {
676
- console.error(`[acp] Terminal ${terminalId} error:`, err.message);
677
- terminal.output += `
678
- Error: ${err.message}`;
679
- terminal.exitCode = -1;
680
- });
681
- instance.terminals.set(terminalId, terminal);
682
- console.log(`[acp] Created terminal ${terminalId} for command: ${params.command}`);
683
- return { terminalId };
684
- },
685
- async terminalOutput(params) {
686
- console.log(`[acp] terminalOutput request for terminal ${params.terminalId}`);
687
- const terminal = instance.terminals.get(params.terminalId);
688
- if (!terminal) {
689
- throw new Error(`Terminal ${params.terminalId} not found`);
690
- }
691
- return {
692
- output: terminal.output,
693
- truncated: terminal.truncated,
694
- exitStatus: terminal.exitCode !== null || terminal.signal !== null ? {
695
- exitCode: terminal.exitCode,
696
- signal: terminal.signal
697
- } : void 0
698
- };
699
- },
700
- async waitForTerminalExit(params) {
701
- console.log(`[acp] waitForTerminalExit request for terminal ${params.terminalId}`);
702
- const terminal = instance.terminals.get(params.terminalId);
703
- if (!terminal) {
704
- throw new Error(`Terminal ${params.terminalId} not found`);
705
- }
706
- if (terminal.exitCode !== null || terminal.signal !== null) {
707
- return {
708
- exitCode: terminal.exitCode,
709
- signal: terminal.signal
710
- };
711
- }
712
- return new Promise((resolve2) => {
713
- terminal.process.on("exit", (code, signal) => {
714
- resolve2({
715
- exitCode: code ?? null,
716
- signal: signal ?? null
717
- });
718
- });
719
- });
720
- },
721
- // Note: killTerminalCommand not in SDK Client interface yet, but we implement handlers
722
- // for completeness when the SDK adds support
723
- async killTerminal(params) {
724
- console.log(`[acp] killTerminal request for terminal ${params.terminalId}`);
725
- const terminal = instance.terminals.get(params.terminalId);
726
- if (!terminal) {
727
- throw new Error(`Terminal ${params.terminalId} not found`);
728
- }
729
- terminal.process.kill("SIGTERM");
730
- return {};
731
- },
732
- async releaseTerminal(params) {
733
- console.log(`[acp] releaseTerminal request for terminal ${params.terminalId}`);
734
- const terminal = instance.terminals.get(params.terminalId);
735
- if (terminal) {
736
- if (terminal.exitCode === null) {
737
- terminal.process.kill("SIGKILL");
738
- }
739
- instance.terminals.delete(params.terminalId);
740
- }
741
- return {};
742
- }
743
- };
744
- }
745
- async prompt(sessionId, content, _emit) {
746
- const instance = this.instances.get(sessionId);
747
- if (!instance) throw new Error(`No ACP instance for window ${sessionId}`);
748
- console.log(`[acp] Sending prompt to session ${instance.sessionId} with ${content.length} content block(s)...`);
749
- const result = await instance.connection.prompt({
750
- sessionId: instance.sessionId,
751
- prompt: content
752
- });
753
- console.log(`[acp] Prompt complete, stopReason: ${result.stopReason}`);
754
- }
755
- async stop(sessionId) {
756
- const instance = this.instances.get(sessionId);
757
- if (!instance) return;
758
- for (const [, callback] of instance.permissionCallbacks) {
759
- callback.reject(new Error("Agent stopped"));
760
- }
761
- instance.permissionCallbacks.clear();
762
- instance.pendingPermission = null;
763
- this.instances.delete(sessionId);
764
- await instance.process.kill();
765
- }
766
- async stopAll() {
767
- const sessionIds = [...this.instances.keys()];
768
- await Promise.all(sessionIds.map((id) => this.stop(id)));
769
- }
770
- isRunning(sessionId) {
771
- return this.instances.has(sessionId);
772
- }
773
- respondToPermission(sessionId, requestId, optionId) {
774
- const instance = this.instances.get(sessionId);
775
- const callback = instance?.permissionCallbacks.get(requestId);
776
- if (!callback) {
777
- throw new Error(`No pending permission request ${requestId}`);
778
- }
779
- callback.resolve(optionId);
780
- instance.permissionCallbacks.delete(requestId);
781
- }
782
- getPendingPermission(sessionId) {
783
- const instance = this.instances.get(sessionId);
784
- return instance?.pendingPermission ?? null;
785
- }
786
- async cancel(sessionId) {
787
- const instance = this.instances.get(sessionId);
788
- if (!instance) return;
789
- await instance.connection.cancel({ sessionId: instance.sessionId });
790
- }
791
- async setMode(sessionId, modeId) {
792
- const instance = this.instances.get(sessionId);
793
- if (!instance) throw new Error(`No ACP instance for window ${sessionId}`);
794
- await instance.connection.setSessionMode({
795
- sessionId: instance.sessionId,
796
- modeId
797
- });
798
- }
799
- async setModel(sessionId, modelId) {
800
- const instance = this.instances.get(sessionId);
801
- if (!instance) throw new Error(`No ACP instance for window ${sessionId}`);
802
- await instance.connection.unstable_setSessionModel({
803
- sessionId: instance.sessionId,
804
- modelId
805
- });
806
- }
807
- };
808
-
809
- // src/agents/backends/mock.ts
810
- import { randomUUID as randomUUID5 } from "crypto";
811
- function delay(ms) {
812
- return new Promise((resolve2) => setTimeout(resolve2, ms));
813
- }
814
- var MockBackend = class {
815
- type = "mock";
816
- running = /* @__PURE__ */ new Set();
817
- matches(config2) {
818
- return config2.command === "__mock__";
819
- }
820
- async start(sessionId, _config, _cwd, _existingAcpSessionId, emit) {
821
- console.log(`[mock] Starting mock agent for session ${sessionId}`);
822
- emit({
823
- sessionUpdate: "current_mode_update",
824
- currentModeId: "mock"
825
- });
826
- console.log(`[mock] Mock agent ready for session ${sessionId}`);
827
- this.running.add(sessionId);
828
- return {
829
- sessionId: randomUUID5(),
830
- models: [{ modelId: "mock-model", name: "Mock Model" }],
831
- modes: [{ id: "mock", name: "Mock Mode" }]
832
- };
833
- }
834
- async prompt(sessionId, content, emit) {
835
- const text2 = content.filter((b) => b.type === "text").map((b) => b.text).join("");
836
- console.log(`[mock] Mock prompt for session ${sessionId}: "${text2.slice(0, 50)}..."`);
837
- const words = [
838
- "This",
839
- "is",
840
- "a",
841
- "mock",
842
- "response",
843
- "from",
844
- "the",
845
- "mock",
846
- "agent.",
847
- "It",
848
- "simulates",
849
- "streaming",
850
- "text",
851
- "chunks",
852
- "for",
853
- "performance",
854
- "testing.",
855
- "The",
856
- "response",
857
- "arrives",
858
- "in",
859
- "small",
860
- "pieces",
861
- "to",
862
- "test",
863
- "UI",
864
- "rendering."
865
- ];
866
- for (let i = 0; i < words.length; i += 3) {
867
- const chunk = words.slice(i, i + 3).join(" ") + " ";
868
- emit({
869
- sessionUpdate: "agent_message_chunk",
870
- content: { type: "text", text: chunk }
871
- });
872
- await delay(50);
873
- }
874
- }
875
- async stop(sessionId) {
876
- console.log(`[mock] Stopping mock agent for session ${sessionId}`);
877
- this.running.delete(sessionId);
878
- }
879
- async stopAll() {
880
- const sessionIds = [...this.running];
881
- await Promise.all(sessionIds.map((id) => this.stop(id)));
882
- }
883
- isRunning(sessionId) {
884
- return this.running.has(sessionId);
885
- }
886
- };
887
-
888
- // src/agents/backends/stress.ts
889
- import { randomUUID as randomUUID7 } from "crypto";
890
-
891
- // src/stress/generators.ts
892
- import { randomUUID as randomUUID6 } from "crypto";
893
- var DEFAULT_STRESS_CONFIG = {
894
- eventsPerTurn: 50,
895
- toolCallProbability: 0.4,
896
- thoughtProbability: 0.6,
897
- planProbability: 0.3,
898
- chunkSize: 50,
899
- delayMs: 0
900
- };
901
- var MARKDOWN_SAMPLES = [
902
- // Sample 1: Code block with TypeScript
903
- `Here's a React component that handles user authentication:
904
-
905
- \`\`\`typescript
906
- import { useState, useEffect } from 'react';
907
- import { useAuth } from '@/hooks/useAuth';
908
-
909
- interface AuthProviderProps {
910
- children: React.ReactNode;
911
- }
912
-
913
- export function AuthProvider({ children }: AuthProviderProps) {
914
- const [isLoading, setIsLoading] = useState(true);
915
- const { user, login, logout } = useAuth();
916
-
917
- useEffect(() => {
918
- // Check for existing session
919
- const checkSession = async () => {
920
- try {
921
- await validateToken();
922
- } finally {
923
- setIsLoading(false);
924
- }
925
- };
926
- checkSession();
927
- }, []);
928
-
929
- if (isLoading) {
930
- return <LoadingSpinner />;
931
- }
932
-
933
- return (
934
- <AuthContext.Provider value={{ user, login, logout }}>
935
- {children}
936
- </AuthContext.Provider>
937
- );
938
- }
939
- \`\`\`
940
-
941
- This component wraps your app and provides authentication context.`,
942
- // Sample 2: Multiple code blocks
943
- `Let me show you how to set up the API client:
944
-
945
- First, install the dependencies:
946
-
947
- \`\`\`bash
948
- npm install @tanstack/react-query axios
949
- \`\`\`
950
-
951
- Then create the client:
952
-
953
- \`\`\`typescript
954
- // src/lib/api.ts
955
- import axios from 'axios';
956
-
957
- export const apiClient = axios.create({
958
- baseURL: process.env.NEXT_PUBLIC_API_URL,
959
- timeout: 10000,
960
- headers: {
961
- 'Content-Type': 'application/json',
962
- },
963
- });
964
-
965
- // Add auth interceptor
966
- apiClient.interceptors.request.use((config) => {
967
- const token = localStorage.getItem('auth_token');
968
- if (token) {
969
- config.headers.Authorization = \`Bearer \${token}\`;
970
- }
971
- return config;
972
- });
973
- \`\`\`
974
-
975
- And set up React Query:
976
-
977
- \`\`\`typescript
978
- // src/providers/QueryProvider.tsx
979
- import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
980
-
981
- const queryClient = new QueryClient({
982
- defaultOptions: {
983
- queries: {
984
- staleTime: 5 * 60 * 1000,
985
- retry: 1,
986
- },
987
- },
988
- });
989
-
990
- export function QueryProvider({ children }: { children: React.ReactNode }) {
991
- return (
992
- <QueryClientProvider client={queryClient}>
993
- {children}
994
- </QueryClientProvider>
995
- );
996
- }
997
- \`\`\``,
998
- // Sample 3: Lists and inline code
999
- `Here's what I found in the codebase:
1000
-
1001
- 1. **Components** - Located in \`src/components/\`
1002
- - \`Button.tsx\` - Primary button component
1003
- - \`Input.tsx\` - Form input with validation
1004
- - \`Modal.tsx\` - Accessible modal dialog
1005
-
1006
- 2. **Hooks** - Custom React hooks in \`src/hooks/\`
1007
- - \`useDebounce\` - Debounces rapidly changing values
1008
- - \`useLocalStorage\` - Syncs state with localStorage
1009
- - \`useMediaQuery\` - Responsive breakpoint detection
1010
-
1011
- 3. **Utils** - Helper functions in \`src/lib/\`
1012
- - \`cn()\` - Class name merger (tailwind-merge + clsx)
1013
- - \`formatDate()\` - Date formatting with Intl API
1014
- - \`validateEmail()\` - Email validation regex
1015
-
1016
- The main entry point is \`src/App.tsx\` which sets up routing and providers.`,
1017
- // Sample 4: Table and code
1018
- `Here's a comparison of the state management options:
1019
-
1020
- | Feature | Zustand | Redux | Jotai |
1021
- |---------|---------|-------|-------|
1022
- | Bundle size | 1.5kb | 7kb | 2kb |
1023
- | Boilerplate | Low | High | Low |
1024
- | DevTools | Yes | Yes | Yes |
1025
- | TypeScript | Excellent | Good | Excellent |
1026
-
1027
- Based on your requirements, I recommend Zustand:
1028
-
1029
- \`\`\`typescript
1030
- import { create } from 'zustand';
1031
-
1032
- interface AppState {
1033
- count: number;
1034
- increment: () => void;
1035
- decrement: () => void;
1036
- }
1037
-
1038
- export const useAppStore = create<AppState>((set) => ({
1039
- count: 0,
1040
- increment: () => set((state) => ({ count: state.count + 1 })),
1041
- decrement: () => set((state) => ({ count: state.count - 1 })),
1042
- }));
1043
- \`\`\``,
1044
- // Sample 5: Error explanation with code
1045
- `I found the issue! The error occurs because of a race condition:
1046
-
1047
- \`\`\`typescript
1048
- // \u274C Bug: Race condition
1049
- useEffect(() => {
1050
- fetchData().then(setData);
1051
- }, [id]);
1052
-
1053
- // \u2705 Fix: Cleanup function prevents stale updates
1054
- useEffect(() => {
1055
- let cancelled = false;
1056
-
1057
- fetchData().then((result) => {
1058
- if (!cancelled) {
1059
- setData(result);
1060
- }
1061
- });
1062
-
1063
- return () => {
1064
- cancelled = true;
1065
- };
1066
- }, [id]);
1067
- \`\`\`
1068
-
1069
- The fix adds a cleanup function that sets a \`cancelled\` flag when the effect re-runs or the component unmounts.`
1070
- ];
1071
- var THOUGHT_PREFIXES = [
1072
- "Let me analyze",
1073
- "I should check",
1074
- "Looking at",
1075
- "Considering",
1076
- "The approach here is",
1077
- "Based on the code",
1078
- "I need to",
1079
- "First",
1080
- "Next",
1081
- "This suggests",
1082
- "The pattern shows",
1083
- "Examining"
1084
- ];
1085
- var LOREM_WORDS = [
1086
- "Lorem",
1087
- "ipsum",
1088
- "dolor",
1089
- "sit",
1090
- "amet",
1091
- "consectetur",
1092
- "adipiscing",
1093
- "elit",
1094
- "sed",
1095
- "do",
1096
- "eiusmod",
1097
- "tempor",
1098
- "incididunt",
1099
- "ut",
1100
- "labore",
1101
- "et",
1102
- "dolore",
1103
- "magna",
1104
- "aliqua",
1105
- "Ut",
1106
- "enim",
1107
- "ad",
1108
- "minim",
1109
- "veniam",
1110
- "quis",
1111
- "nostrud"
1112
- ];
1113
- var PLAN_TASKS = [
1114
- "Analyze the current implementation",
1115
- "Identify areas for improvement",
1116
- "Refactor the component structure",
1117
- "Update the test suite",
1118
- "Fix type errors",
1119
- "Add documentation",
1120
- "Optimize performance",
1121
- "Review dependencies"
1122
- ];
1123
- var RICH_TOOL_CALLS = [
1124
- // Bash tool with command and output
1125
- {
1126
- toolName: "Bash",
1127
- kind: "execute",
1128
- title: "List files in src directory",
1129
- input: {
1130
- command: "ls -la src/",
1131
- description: "List files in src directory"
1132
- },
1133
- output: `total 48
1134
- drwxr-xr-x 8 user user 4096 Jan 10 14:30 .
1135
- drwxr-xr-x 12 user user 4096 Jan 10 14:25 ..
1136
- drwxr-xr-x 4 user user 4096 Jan 10 14:30 components
1137
- drwxr-xr-x 2 user user 4096 Jan 10 14:28 hooks
1138
- drwxr-xr-x 2 user user 4096 Jan 10 14:27 lib
1139
- -rw-r--r-- 1 user user 1245 Jan 10 14:30 App.tsx
1140
- -rw-r--r-- 1 user user 892 Jan 10 14:25 main.tsx
1141
- -rw-r--r-- 1 user user 2341 Jan 10 14:29 index.css`
1142
- },
1143
- // Read tool with file contents
1144
- {
1145
- toolName: "Read",
1146
- kind: "read",
1147
- title: "Read ~/repos/project/src/utils.ts",
1148
- input: {
1149
- file_path: "/home/user/repos/project/src/utils.ts"
1150
- },
1151
- output: `export function cn(...classes: string[]): string {
1152
- return classes.filter(Boolean).join(' ');
1153
- }
1154
-
1155
- export function formatDate(date: Date): string {
1156
- return new Intl.DateTimeFormat('en-US', {
1157
- year: 'numeric',
1158
- month: 'long',
1159
- day: 'numeric',
1160
- }).format(date);
1161
- }
1162
-
1163
- export function debounce<T extends (...args: unknown[]) => void>(
1164
- fn: T,
1165
- delay: number
1166
- ): T {
1167
- let timeoutId: NodeJS.Timeout;
1168
- return ((...args) => {
1169
- clearTimeout(timeoutId);
1170
- timeoutId = setTimeout(() => fn(...args), delay);
1171
- }) as T;
1172
- }`
1173
- },
1174
- // Edit tool with diff
1175
- {
1176
- toolName: "Edit",
1177
- kind: "edit",
1178
- title: "Edit ~/repos/project/src/config.ts",
1179
- input: {
1180
- file_path: "/home/user/repos/project/src/config.ts",
1181
- old_string: "timeout: 5000",
1182
- new_string: "timeout: 10000"
1183
- },
1184
- diff: `--- a/src/config.ts
1185
- +++ b/src/config.ts
1186
- @@ -5,7 +5,7 @@ export const config = {
1187
- apiUrl: process.env.API_URL,
1188
- environment: process.env.NODE_ENV,
1189
- features: {
1190
- - timeout: 5000,
1191
- + timeout: 10000,
1192
- retries: 3,
1193
- cacheEnabled: true,
1194
- },`
1195
- },
1196
- // Glob tool
1197
- {
1198
- toolName: "Glob",
1199
- kind: "search",
1200
- title: 'Glob "**/*.test.ts"',
1201
- input: {
1202
- pattern: "**/*.test.ts",
1203
- path: "/home/user/repos/project"
1204
- },
1205
- output: `src/components/Button.test.ts
1206
- src/components/Input.test.ts
1207
- src/hooks/useAuth.test.ts
1208
- src/lib/utils.test.ts
1209
- src/lib/api.test.ts`
1210
- },
1211
- // Grep tool
1212
- {
1213
- toolName: "Grep",
1214
- kind: "search",
1215
- title: 'Grep "useState" in src/',
1216
- input: {
1217
- pattern: "useState",
1218
- path: "/home/user/repos/project/src"
1219
- },
1220
- output: `src/components/Counter.tsx:3:import { useState } from 'react';
1221
- src/components/Form.tsx:1:import { useState, useCallback } from 'react';
1222
- src/hooks/useToggle.ts:1:import { useState } from 'react';
1223
- src/App.tsx:2:import { useState, useEffect } from 'react';`
1224
- },
1225
- // Task/subagent tool
1226
- {
1227
- toolName: "Task",
1228
- kind: "think",
1229
- title: "Explore Task",
1230
- input: {
1231
- subagent_type: "Explore",
1232
- description: "Find authentication implementation",
1233
- prompt: "Search for authentication-related files and understand the auth flow"
1234
- },
1235
- output: `Found 3 relevant files:
1236
- - src/hooks/useAuth.ts (main auth hook)
1237
- - src/context/AuthContext.tsx (auth provider)
1238
- - src/lib/auth.ts (token management)
1239
-
1240
- The app uses JWT tokens stored in localStorage with automatic refresh.`
1241
- },
1242
- // Write tool
1243
- {
1244
- toolName: "Write",
1245
- kind: "edit",
1246
- title: "Write ~/repos/project/src/newFile.ts",
1247
- input: {
1248
- file_path: "/home/user/repos/project/src/newFile.ts",
1249
- content: 'export const VERSION = "1.0.0";'
1250
- },
1251
- output: "File created successfully"
1252
- },
1253
- // WebFetch tool
1254
- {
1255
- toolName: "WebFetch",
1256
- kind: "fetch",
1257
- title: "Fetch https://api.example.com/docs",
1258
- input: {
1259
- url: "https://api.example.com/docs",
1260
- prompt: "Get the API documentation"
1261
- },
1262
- output: `# API Documentation
1263
-
1264
- ## Authentication
1265
- All requests require a Bearer token in the Authorization header.
1266
-
1267
- ## Endpoints
1268
- - GET /users - List all users
1269
- - POST /users - Create a user
1270
- - GET /users/:id - Get user by ID`
1271
- }
1272
- ];
1273
- function randomInt(min, max) {
1274
- return Math.floor(Math.random() * (max - min + 1)) + min;
1275
- }
1276
- function randomChoice(arr) {
1277
- return arr[Math.floor(Math.random() * arr.length)];
1278
- }
1279
- function generateText(length) {
1280
- const words = [];
1281
- while (words.join(" ").length < length) {
1282
- words.push(randomChoice(LOREM_WORDS));
1283
- }
1284
- return words.join(" ").slice(0, length);
1285
- }
1286
- function generateMessageChunks(totalLength, chunkSize, kind = "agent_message_chunk") {
1287
- const text2 = generateText(totalLength);
1288
- const chunks = [];
1289
- for (let i = 0; i < text2.length; i += chunkSize) {
1290
- const chunk = text2.slice(i, i + chunkSize);
1291
- chunks.push({
1292
- sessionUpdate: kind,
1293
- content: { type: "text", text: chunk }
1294
- });
1295
- }
1296
- return chunks;
1297
- }
1298
- function generateMarkdownChunks(chunkSize = 50) {
1299
- const markdown = randomChoice(MARKDOWN_SAMPLES);
1300
- const chunks = [];
1301
- for (let i = 0; i < markdown.length; i += chunkSize) {
1302
- const chunk = markdown.slice(i, i + chunkSize);
1303
- chunks.push({
1304
- sessionUpdate: "agent_message_chunk",
1305
- content: { type: "text", text: chunk }
1306
- });
1307
- }
1308
- return chunks;
1309
- }
1310
- function generateThoughtChunks(totalLength) {
1311
- const prefix = randomChoice(THOUGHT_PREFIXES);
1312
- const rest = generateText(totalLength - prefix.length);
1313
- const text2 = `${prefix} ${rest}`;
1314
- const chunkSize = randomInt(30, 60);
1315
- const chunks = [];
1316
- for (let i = 0; i < text2.length; i += chunkSize) {
1317
- const chunk = text2.slice(i, i + chunkSize);
1318
- chunks.push({
1319
- sessionUpdate: "agent_thought_chunk",
1320
- content: { type: "text", text: chunk }
1321
- });
1322
- }
1323
- return chunks;
1324
- }
1325
- function generateRichToolCall(config2) {
1326
- const toolCallId = randomUUID6();
1327
- const toolConfig = config2 ?? randomChoice(RICH_TOOL_CALLS);
1328
- const call = {
1329
- sessionUpdate: "tool_call",
1330
- toolCallId,
1331
- title: toolConfig.title,
1332
- status: "in_progress",
1333
- kind: toolConfig.kind,
1334
- rawInput: toolConfig.input,
1335
- _meta: {
1336
- claudeCode: {
1337
- toolName: toolConfig.toolName
1338
- }
1339
- }
1340
- };
1341
- const toolResponse = [];
1342
- if (toolConfig.output) {
1343
- toolResponse.push({ type: "text", text: toolConfig.output });
1344
- }
1345
- const content = [];
1346
- if (toolConfig.diff) {
1347
- content.push({ type: "diff", content: toolConfig.diff });
1348
- }
1349
- const update = {
1350
- sessionUpdate: "tool_call_update",
1351
- toolCallId,
1352
- status: "completed",
1353
- // Title update (sometimes tools update their title)
1354
- title: toolConfig.title.replace("...", ""),
1355
- _meta: {
1356
- claudeCode: {
1357
- toolName: toolConfig.toolName,
1358
- toolResponse: toolResponse.length > 0 ? toolResponse : void 0
1359
- }
1360
- },
1361
- ...content.length > 0 && { content }
1362
- };
1363
- return { call, update, toolCallId };
1364
- }
1365
- function generatePlan(entryCount) {
1366
- const entries = [];
1367
- const completedCount = randomInt(0, Math.floor(entryCount / 2));
1368
- for (let i = 0; i < entryCount; i++) {
1369
- const status = i < completedCount ? "completed" : i === completedCount ? "in_progress" : "pending";
1370
- entries.push({
1371
- content: randomChoice(PLAN_TASKS),
1372
- priority: randomChoice(["high", "medium", "low"]),
1373
- status
1374
- });
1375
- }
1376
- return {
1377
- sessionUpdate: "plan",
1378
- entries
1379
- };
1380
- }
1381
- function generateRealisticTurn(config2 = DEFAULT_STRESS_CONFIG) {
1382
- const events = [];
1383
- if (Math.random() < config2.thoughtProbability) {
1384
- const thoughtLength = randomInt(100, 300);
1385
- events.push(...generateThoughtChunks(thoughtLength));
1386
- }
1387
- if (Math.random() < config2.planProbability) {
1388
- events.push(generatePlan(randomInt(3, 6)));
1389
- }
1390
- if (Math.random() < config2.toolCallProbability) {
1391
- const toolCount = randomInt(1, 3);
1392
- for (let i = 0; i < toolCount; i++) {
1393
- const { call, update } = generateRichToolCall();
1394
- events.push(call);
1395
- if (Math.random() < 0.3) {
1396
- events.push(...generateThoughtChunks(randomInt(50, 100)));
1397
- }
1398
- events.push(update);
1399
- }
1400
- }
1401
- if (Math.random() < 0.7) {
1402
- events.push(...generateMarkdownChunks(config2.chunkSize));
1403
- } else {
1404
- const responseLength = randomInt(100, config2.eventsPerTurn * config2.chunkSize);
1405
- events.push(...generateMessageChunks(responseLength, config2.chunkSize));
1406
- }
1407
- return events;
1408
- }
1409
-
1410
- // src/stress/config.ts
1411
- var config = {
1412
- turnDelay: 5,
1413
- eventsPerTurn: 50,
1414
- eventDelay: 30,
1415
- // 30ms between events for visible streaming
1416
- chunkSize: 20
1417
- // smaller chunks look more like streaming
1418
- };
1419
- function getStressRuntimeConfig() {
1420
- return { ...config };
1421
- }
1422
- function setStressRuntimeConfig(updates) {
1423
- if (updates.turnDelay !== void 0) {
1424
- config.turnDelay = Math.max(0, Math.min(60, updates.turnDelay));
1425
- }
1426
- if (updates.eventsPerTurn !== void 0) {
1427
- config.eventsPerTurn = Math.max(10, Math.min(500, updates.eventsPerTurn));
1428
- }
1429
- if (updates.eventDelay !== void 0) {
1430
- config.eventDelay = Math.max(0, Math.min(200, updates.eventDelay));
1431
- }
1432
- if (updates.chunkSize !== void 0) {
1433
- config.chunkSize = Math.max(5, Math.min(100, updates.chunkSize));
1434
- }
1435
- }
1436
-
1437
- // src/agents/backends/stress.ts
1438
- function delay2(ms) {
1439
- return new Promise((resolve2) => setTimeout(resolve2, ms));
1440
- }
1441
- var StressBackend = class {
1442
- type = "stress";
1443
- running = /* @__PURE__ */ new Map();
1444
- constructor() {
1445
- }
1446
- getConfig() {
1447
- const runtime = getStressRuntimeConfig();
1448
- return {
1449
- eventsPerTurn: runtime.eventsPerTurn,
1450
- toolCallProbability: 0.4,
1451
- thoughtProbability: 0.6,
1452
- planProbability: 0.3,
1453
- chunkSize: runtime.chunkSize,
1454
- delayMs: runtime.eventDelay
1455
- };
1456
- }
1457
- matches(config2) {
1458
- return config2.command === "__stress__";
1459
- }
1460
- async start(sessionId, _config, _cwd, _existingAcpSessionId, emit) {
1461
- console.log(`[stress] Starting stress agent for session ${sessionId}`);
1462
- emit({
1463
- sessionUpdate: "current_mode_update",
1464
- currentModeId: "stress"
1465
- });
1466
- const agent = { emit, stopped: false };
1467
- this.running.set(sessionId, agent);
1468
- this.runEventLoop(sessionId, agent);
1469
- console.log(`[stress] Stress agent ready for session ${sessionId}`);
1470
- return {
1471
- sessionId: randomUUID7(),
1472
- models: [{ modelId: "stress-model", name: "Stress Model" }],
1473
- modes: [{ id: "stress", name: "Stress Mode" }]
1474
- };
1475
- }
1476
- getTurnDelayMs() {
1477
- const { turnDelay } = getStressRuntimeConfig();
1478
- if (turnDelay === 0) {
1479
- return 0;
1480
- }
1481
- const variance = turnDelay * 0.5;
1482
- return (turnDelay - variance + Math.random() * variance * 2) * 1e3;
1483
- }
1484
- async runEventLoop(sessionId, agent) {
1485
- await delay2(1e3);
1486
- let turnCount = 0;
1487
- while (!agent.stopped) {
1488
- turnCount++;
1489
- agent.emit({ amuxEvent: "turn_start" });
1490
- agent.emit({
1491
- sessionUpdate: "user_message_chunk",
1492
- content: { type: "text", text: `[Stress test turn ${turnCount}]` }
1493
- });
1494
- const config2 = this.getConfig();
1495
- const events = generateRealisticTurn(config2);
1496
- for (const event of events) {
1497
- if (agent.stopped) break;
1498
- agent.emit(event);
1499
- if (config2.delayMs > 0) {
1500
- await delay2(config2.delayMs);
1501
- }
1502
- }
1503
- if (!agent.stopped) {
1504
- agent.emit({ amuxEvent: "turn_end" });
1505
- }
1506
- const turnDelay = this.getTurnDelayMs();
1507
- await delay2(turnDelay);
1508
- }
1509
- console.log(`[stress] Event loop stopped for session ${sessionId}`);
1510
- }
1511
- async prompt(sessionId, content, emit) {
1512
- const text2 = content.filter((b) => b.type === "text").map((b) => b.text).join("");
1513
- console.log(`[stress] Prompt for session ${sessionId}: "${text2.slice(0, 50)}..."`);
1514
- const config2 = this.getConfig();
1515
- const events = generateRealisticTurn(config2);
1516
- for (const event of events) {
1517
- emit(event);
1518
- if (config2.delayMs > 0) {
1519
- await delay2(config2.delayMs);
1520
- }
1521
- }
1522
- }
1523
- async stop(sessionId) {
1524
- console.log(`[stress] Stopping stress agent for session ${sessionId}`);
1525
- const agent = this.running.get(sessionId);
1526
- if (agent) {
1527
- agent.stopped = true;
1528
- this.running.delete(sessionId);
1529
- }
1530
- }
1531
- async stopAll() {
1532
- const sessionIds = [...this.running.keys()];
1533
- await Promise.all(sessionIds.map((id) => this.stop(id)));
1534
- }
1535
- isRunning(sessionId) {
1536
- return this.running.has(sessionId);
1537
- }
1538
- };
1539
-
1540
- // src/lib/mentions.ts
1541
- import path2 from "path";
1542
- function parseMessageToContentBlocks(message, workingDir) {
1543
- const blocks = [];
1544
- const mentionRegex = /@(\.{0,2}[\w\/\.\-]+)/g;
1545
- let lastIndex = 0;
1546
- for (const match of message.matchAll(mentionRegex)) {
1547
- if (match.index > lastIndex) {
1548
- const text2 = message.slice(lastIndex, match.index);
1549
- if (text2.trim()) {
1550
- blocks.push({ type: "text", text: text2 });
1551
- }
1552
- }
1553
- const relativePath = match[1];
1554
- const absolutePath = path2.resolve(workingDir, relativePath);
1555
- blocks.push({
1556
- type: "resource_link",
1557
- uri: `file://${absolutePath}`,
1558
- name: relativePath
1559
- // Required per ACP spec - use the path the user typed
1560
- });
1561
- lastIndex = match.index + match[0].length;
1562
- }
1563
- if (lastIndex < message.length) {
1564
- const text2 = message.slice(lastIndex);
1565
- if (text2.trim()) {
1566
- blocks.push({ type: "text", text: text2 });
1567
- }
1568
- }
1569
- return blocks.length > 0 ? blocks : [{ type: "text", text: message }];
1570
- }
1571
-
1572
- // src/agents/manager.ts
1573
- function generateTitleFromMessage(message) {
1574
- const cleaned = message.replace(/@[\w./~-]+/g, "").replace(/\s+/g, " ").trim();
1575
- const firstLine = cleaned.split("\n")[0] || cleaned;
1576
- if (firstLine.length <= 50) return firstLine;
1577
- const truncated = firstLine.slice(0, 50);
1578
- const lastSpace = truncated.lastIndexOf(" ");
1579
- return lastSpace > 20 ? truncated.slice(0, lastSpace) + "..." : truncated + "...";
1580
- }
1581
- var AgentManager = class extends EventEmitter {
1582
- backends;
1583
- agents = /* @__PURE__ */ new Map();
1584
- constructor() {
1585
- super();
1586
- const acpBackend = new AcpBackend();
1587
- acpBackend.onSessionIdChanged = (sessionId, acpSessionId) => {
1588
- db.update(sessions).set({ acpSessionId }).where(eq2(sessions.id, sessionId)).run();
1589
- };
1590
- this.backends = [acpBackend, new MockBackend(), new StressBackend()];
1591
- }
1592
- getBackendForConfig(config2) {
1593
- const backend = this.backends.find((b) => b.matches(config2));
1594
- if (!backend) {
1595
- throw new Error(`No backend for agent config: ${config2.command}`);
1596
- }
1597
- return backend;
1598
- }
1599
- emitUpdate(sessionId, update) {
1600
- storeEvent(sessionId, update);
1601
- if (isSessionUpdate(update) && update.sessionUpdate === "session_info_update") {
1602
- const title = update.title;
1603
- if (title !== void 0) {
1604
- db.update(sessions).set({ title }).where(eq2(sessions.id, sessionId)).run();
1605
- }
1606
- }
1607
- this.emit("update", { sessionId, update });
1608
- }
1609
- async startForSession(sessionId) {
1610
- const existing = this.agents.get(sessionId);
1611
- if (existing?.status === "ready") {
1612
- const replayEvents2 = getEventsForSession(sessionId);
1613
- return {
1614
- acpSessionId: existing.session.sessionId,
1615
- replayEvents: replayEvents2,
1616
- models: existing.session.models,
1617
- modes: existing.session.modes
1618
- };
1619
- }
1620
- if (existing?.status === "starting") {
1621
- return new Promise((resolve2, reject) => {
1622
- const checkReady = () => {
1623
- const agent = this.agents.get(sessionId);
1624
- if (agent?.status === "ready") {
1625
- const replayEvents2 = getEventsForSession(sessionId);
1626
- resolve2({
1627
- acpSessionId: agent.session.sessionId,
1628
- replayEvents: replayEvents2,
1629
- models: agent.session.models,
1630
- modes: agent.session.modes
1631
- });
1632
- } else if (agent?.status === "dead") {
1633
- reject(new Error("Agent failed to start"));
1634
- } else {
1635
- setTimeout(checkReady, 100);
1636
- }
1637
- };
1638
- checkReady();
1639
- });
1640
- }
1641
- const session = db.select().from(sessions).where(eq2(sessions.id, sessionId)).get();
1642
- if (!session) throw new Error(`Session ${sessionId} not found`);
1643
- const dbConfig = db.select().from(agentConfigs).where(eq2(agentConfigs.id, session.agentConfigId)).get();
1644
- if (!dbConfig) throw new Error(`Agent config ${session.agentConfigId} not found`);
1645
- const config2 = {
1646
- id: dbConfig.id,
1647
- name: dbConfig.name,
1648
- command: dbConfig.command,
1649
- args: dbConfig.args ?? [],
1650
- env: dbConfig.env ?? {}
1651
- };
1652
- const backend = this.getBackendForConfig(config2);
1653
- const emit = (update) => this.emitUpdate(sessionId, update);
1654
- this.agents.set(sessionId, {
1655
- backend,
1656
- session: { sessionId: "" },
1657
- status: "starting"
1658
- });
1659
- const replayEvents = getEventsForSession(sessionId);
1660
- try {
1661
- const agentSession = await backend.start(sessionId, config2, session.directory, session.acpSessionId ?? null, emit);
1662
- this.agents.set(sessionId, {
1663
- backend,
1664
- session: agentSession,
1665
- status: "ready"
1666
- });
1667
- console.log(`[agents] Agent ready for session ${sessionId}`);
1668
- return {
1669
- acpSessionId: agentSession.sessionId,
1670
- replayEvents,
1671
- models: agentSession.models,
1672
- modes: agentSession.modes
1673
- };
1674
- } catch (err) {
1675
- this.agents.set(sessionId, {
1676
- backend,
1677
- session: { sessionId: "" },
1678
- status: "dead"
1679
- });
1680
- throw err;
1681
- }
1682
- }
1683
- // Legacy alias for compatibility during migration
1684
- /** @deprecated Use startForSession instead */
1685
- startForWindow = this.startForSession.bind(this);
1686
- async prompt(sessionId, message) {
1687
- console.log(`[agents] prompt() called for session ${sessionId}: "${message.slice(0, 50)}..."`);
1688
- const agent = this.agents.get(sessionId);
1689
- if (!agent || agent.status !== "ready") {
1690
- throw new Error(`Agent not ready for session ${sessionId}`);
1691
- }
1692
- const session = db.select().from(sessions).where(eq2(sessions.id, sessionId)).get();
1693
- if (!session) throw new Error(`Session ${sessionId} not found`);
1694
- if (!session.title) {
1695
- const title = generateTitleFromMessage(message);
1696
- if (title) {
1697
- db.update(sessions).set({ title }).where(eq2(sessions.id, sessionId)).run();
1698
- this.emitUpdate(sessionId, {
1699
- sessionUpdate: "session_info_update",
1700
- title
1701
- });
1702
- }
1703
- }
1704
- startTurn(sessionId);
1705
- this.emitUpdate(sessionId, { amuxEvent: "turn_start" });
1706
- this.emitUpdate(sessionId, {
1707
- sessionUpdate: "user_message_chunk",
1708
- content: { type: "text", text: message }
1709
- });
1710
- const content = parseMessageToContentBlocks(message, session.directory);
1711
- const emit = (update) => this.emitUpdate(sessionId, update);
1712
- try {
1713
- await agent.backend.prompt(sessionId, content, emit);
1714
- } finally {
1715
- endTurn(sessionId);
1716
- this.emitUpdate(sessionId, { amuxEvent: "turn_end" });
1717
- }
1718
- }
1719
- async setMode(sessionId, modeId) {
1720
- const agent = this.agents.get(sessionId);
1721
- if (!agent || agent.status !== "ready") {
1722
- throw new Error(`Agent not ready for session ${sessionId}`);
1723
- }
1724
- if (!agent.backend.setMode) {
1725
- throw new Error(`Backend ${agent.backend.type} does not support setMode`);
1726
- }
1727
- await agent.backend.setMode(sessionId, modeId);
1728
- }
1729
- async setModel(sessionId, modelId) {
1730
- const agent = this.agents.get(sessionId);
1731
- if (!agent || agent.status !== "ready") {
1732
- throw new Error(`Agent not ready for session ${sessionId}`);
1733
- }
1734
- if (!agent.backend.setModel) {
1735
- throw new Error(`Backend ${agent.backend.type} does not support setModel`);
1736
- }
1737
- await agent.backend.setModel(sessionId, modelId);
1738
- }
1739
- async cancel(sessionId) {
1740
- const agent = this.agents.get(sessionId);
1741
- if (!agent?.backend.cancel) return;
1742
- await agent.backend.cancel(sessionId);
1743
- }
1744
- respondPermission(sessionId, requestId, optionId) {
1745
- const agent = this.agents.get(sessionId);
1746
- if (!agent) {
1747
- console.warn(`[AgentManager] No agent running for session ${sessionId}, ignoring permission response`);
1748
- return;
1749
- }
1750
- if (!agent.backend.respondToPermission) {
1751
- throw new Error(`Backend ${agent.backend.type} does not support permissions`);
1752
- }
1753
- agent.backend.respondToPermission(sessionId, requestId, optionId);
1754
- }
1755
- async stopForSession(sessionId) {
1756
- const agent = this.agents.get(sessionId);
1757
- if (agent) {
1758
- await agent.backend.stop(sessionId);
1759
- }
1760
- this.agents.delete(sessionId);
1761
- }
1762
- // Legacy alias for compatibility during migration
1763
- /** @deprecated Use stopForSession instead */
1764
- stopForWindow = this.stopForSession.bind(this);
1765
- async stopAll() {
1766
- const sessionIds = Array.from(this.agents.keys());
1767
- await Promise.all(sessionIds.map((id) => this.stopForSession(id)));
1768
- }
1769
- getForSession(sessionId) {
1770
- return this.agents.get(sessionId);
1771
- }
1772
- getPendingPermission(sessionId) {
1773
- const agent = this.agents.get(sessionId);
1774
- if (!agent?.backend.getPendingPermission) return null;
1775
- return agent.backend.getPendingPermission(sessionId);
1776
- }
1777
- // Legacy alias for compatibility during migration
1778
- /** @deprecated Use getForSession instead */
1779
- getForWindow = this.getForSession.bind(this);
1780
- registerBackend(backend) {
1781
- this.backends.unshift(backend);
1782
- }
1783
- };
1784
- var agentManager = new AgentManager();
1785
-
1786
- // src/trpc/sessions.ts
1787
- var sessionsRouter = router({
1788
- /**
1789
- * List all sessions with agent configs.
1790
- * Note: No activeSessionId - that's a UI concern (Shella).
1791
- */
1792
- list: publicProcedure.query(async () => {
1793
- const allSessions = db.select().from(sessions).orderBy(sessions.createdAt).all();
1794
- const configs = db.select().from(agentConfigs).all();
1795
- const lastUsedRow = db.select().from(appState).where(eq3(appState.key, "last_used_agent_config_id")).get();
1796
- return {
1797
- sessions: allSessions,
1798
- agentConfigs: configs,
1799
- lastUsedAgentConfigId: lastUsedRow?.value ?? configs[0]?.id ?? null
1800
- };
1801
- }),
1802
- /**
1803
- * Get a single session by ID.
1804
- */
1805
- get: publicProcedure.input(z.object({ id: z.string() })).query(async ({ input }) => {
1806
- const session = db.select().from(sessions).where(eq3(sessions.id, input.id)).get();
1807
- if (!session) {
1808
- throw new Error(`Session ${input.id} not found`);
1809
- }
1810
- return session;
1811
- }),
1812
- /**
1813
- * Create a new session.
1814
- * Note: No title - that's a UI concern (Shella).
1815
- */
1816
- create: publicProcedure.input(z.object({
1817
- id: z.string().optional(),
1818
- // Client can provide ID for optimistic updates
1819
- directory: z.string().optional(),
1820
- agentConfigId: z.string().optional()
1821
- })).mutation(async ({ input }) => {
1822
- let configId = input.agentConfigId;
1823
- if (!configId) {
1824
- const lastUsedRow = db.select().from(appState).where(eq3(appState.key, "last_used_agent_config_id")).get();
1825
- if (lastUsedRow?.value) {
1826
- const config2 = db.select().from(agentConfigs).where(eq3(agentConfigs.id, lastUsedRow.value)).get();
1827
- if (config2) configId = config2.id;
1828
- }
1829
- if (!configId) {
1830
- const firstConfig = db.select().from(agentConfigs).get();
1831
- if (!firstConfig) {
1832
- throw new Error("No agent configs available");
1833
- }
1834
- configId = firstConfig.id;
1835
- }
1836
- } else {
1837
- db.insert(appState).values({ key: "last_used_agent_config_id", value: configId }).onConflictDoUpdate({ target: appState.key, set: { value: configId } }).run();
1838
- }
1839
- const id = input.id ?? randomUUID8();
1840
- const now = /* @__PURE__ */ new Date();
1841
- db.insert(sessions).values({
1842
- id,
1843
- directory: input.directory ?? getStartupCwd(),
1844
- agentConfigId: configId,
1845
- acpSessionId: null,
1846
- model: null,
1847
- mode: null,
1848
- createdAt: now
1849
- }).run();
1850
- return db.select().from(sessions).where(eq3(sessions.id, id)).get();
1851
- }),
1852
- /**
1853
- * Update a session.
1854
- * Note: No title/hasCustomTitle/lastAccessedAt - those are UI concerns (Shella).
1855
- */
1856
- update: publicProcedure.input(z.object({
1857
- id: z.string(),
1858
- directory: z.string().optional(),
1859
- agentConfigId: z.string().optional(),
1860
- acpSessionId: z.string().nullable().optional(),
1861
- model: z.string().nullable().optional(),
1862
- mode: z.string().nullable().optional()
1863
- })).mutation(async ({ input }) => {
1864
- const { id, ...updates } = input;
1865
- db.update(sessions).set(updates).where(eq3(sessions.id, id)).run();
1866
- if (input.agentConfigId) {
1867
- db.insert(appState).values({ key: "last_used_agent_config_id", value: input.agentConfigId }).onConflictDoUpdate({ target: appState.key, set: { value: input.agentConfigId } }).run();
1868
- }
1869
- return db.select().from(sessions).where(eq3(sessions.id, id)).get();
1870
- }),
1871
- /**
1872
- * Delete a session.
1873
- * Note: No active session management - that's a UI concern (Shella).
1874
- */
1875
- delete: publicProcedure.input(z.object({ id: z.string() })).mutation(async ({ input }) => {
1876
- await agentManager.stopForSession(input.id);
1877
- clearEventsForSession(input.id);
1878
- db.delete(sessions).where(eq3(sessions.id, input.id)).run();
1879
- return { ok: true };
1880
- })
1881
- });
1882
-
1883
- // src/trpc/agents.ts
1884
- import { z as z2 } from "zod";
1885
- import { observable } from "@trpc/server/observable";
1886
- import { eq as eq4 } from "drizzle-orm";
1887
-
1888
- // src/lib/shutdown.ts
1889
- var isShuttingDown = false;
1890
- function setShuttingDown(value) {
1891
- isShuttingDown = value;
1892
- }
1893
-
1894
- // src/trpc/agents.ts
1895
- var sessionIdInput = z2.object({
1896
- sessionId: z2.string()
1897
- });
1898
- var agentsRouter = router({
1899
- start: publicProcedure.input(sessionIdInput).mutation(async ({ input }) => {
1900
- if (isShuttingDown) {
1901
- throw new Error("Server is shutting down");
1902
- }
1903
- return agentManager.startForSession(input.sessionId);
1904
- }),
1905
- stop: publicProcedure.input(sessionIdInput).mutation(async ({ input }) => {
1906
- await agentManager.stopForSession(input.sessionId);
1907
- return { ok: true };
1908
- }),
1909
- switchAgent: publicProcedure.input(z2.object({
1910
- sessionId: z2.string(),
1911
- agentConfigId: z2.string()
1912
- })).mutation(async ({ input }) => {
1913
- const { sessionId, agentConfigId } = input;
1914
- await agentManager.stopForSession(sessionId);
1915
- clearEventsForSession(sessionId);
1916
- db.update(sessions).set({
1917
- agentConfigId,
1918
- acpSessionId: null,
1919
- model: null,
1920
- mode: null
1921
- }).where(eq4(sessions.id, sessionId)).run();
1922
- db.insert(appState).values({ key: "last_used_agent_config_id", value: agentConfigId }).onConflictDoUpdate({ target: appState.key, set: { value: agentConfigId } }).run();
1923
- return { ok: true };
1924
- }),
1925
- prompt: publicProcedure.input(z2.object({
1926
- sessionId: z2.string(),
1927
- message: z2.string()
1928
- })).mutation(async ({ input }) => {
1929
- await agentManager.prompt(input.sessionId, input.message);
1930
- return { ok: true };
1931
- }),
1932
- cancel: publicProcedure.input(sessionIdInput).mutation(async ({ input }) => {
1933
- await agentManager.cancel(input.sessionId);
1934
- return { ok: true };
1935
- }),
1936
- setMode: publicProcedure.input(z2.object({
1937
- sessionId: z2.string(),
1938
- modeId: z2.string()
1939
- })).mutation(async ({ input }) => {
1940
- await agentManager.setMode(input.sessionId, input.modeId);
1941
- return { ok: true };
1942
- }),
1943
- setModel: publicProcedure.input(z2.object({
1944
- sessionId: z2.string(),
1945
- modelId: z2.string()
1946
- })).mutation(async ({ input }) => {
1947
- await agentManager.setModel(input.sessionId, input.modelId);
1948
- return { ok: true };
1949
- }),
1950
- respondPermission: publicProcedure.input(z2.object({
1951
- sessionId: z2.string(),
1952
- requestId: z2.string(),
1953
- optionId: z2.string()
1954
- })).mutation(({ input }) => {
1955
- agentManager.respondPermission(input.sessionId, input.requestId, input.optionId);
1956
- return { ok: true };
1957
- }),
1958
- subscribe: publicProcedure.input(sessionIdInput).subscription(({ input }) => {
1959
- return observable((emit) => {
1960
- const handler = (event) => {
1961
- if (event.sessionId === input.sessionId) {
1962
- emit.next(event.update);
1963
- }
1964
- };
1965
- agentManager.on("update", handler);
1966
- return () => agentManager.off("update", handler);
1967
- });
1968
- }),
1969
- subscribeAll: publicProcedure.subscription(() => {
1970
- return observable((emit) => {
1971
- const handler = (event) => emit.next(event);
1972
- agentManager.on("update", handler);
1973
- return () => agentManager.off("update", handler);
1974
- });
1975
- }),
1976
- // Stress testing config
1977
- setStressConfig: publicProcedure.input(z2.object({
1978
- turnDelay: z2.number().min(0).max(60).optional(),
1979
- eventsPerTurn: z2.number().min(10).max(500).optional(),
1980
- eventDelay: z2.number().min(0).max(200).optional(),
1981
- chunkSize: z2.number().min(5).max(100).optional()
1982
- })).mutation(({ input }) => {
1983
- setStressRuntimeConfig(input);
1984
- return { ok: true };
1985
- }),
1986
- getStressConfig: publicProcedure.query(() => {
1987
- return getStressRuntimeConfig();
1988
- })
1989
- });
1990
-
1991
- // src/trpc/agentConfigs.ts
1992
- import { z as z3 } from "zod";
1993
- import { eq as eq5 } from "drizzle-orm";
1994
- import { randomUUID as randomUUID9 } from "crypto";
1995
- var agentConfigsRouter = router({
1996
- list: publicProcedure.query(async () => {
1997
- return db.select().from(agentConfigs).all();
1998
- }),
1999
- get: publicProcedure.input(z3.object({ id: z3.string() })).query(async ({ input }) => {
2000
- return db.select().from(agentConfigs).where(eq5(agentConfigs.id, input.id)).get();
2001
- }),
2002
- create: publicProcedure.input(z3.object({
2003
- name: z3.string(),
2004
- command: z3.string(),
2005
- args: z3.array(z3.string()).default([]),
2006
- env: z3.record(z3.string()).default({})
2007
- })).mutation(async ({ input }) => {
2008
- const id = randomUUID9();
2009
- db.insert(agentConfigs).values({ id, ...input }).run();
2010
- return db.select().from(agentConfigs).where(eq5(agentConfigs.id, id)).get();
2011
- }),
2012
- update: publicProcedure.input(z3.object({
2013
- id: z3.string(),
2014
- name: z3.string().optional(),
2015
- command: z3.string().optional(),
2016
- args: z3.array(z3.string()).optional(),
2017
- env: z3.record(z3.string()).optional()
2018
- })).mutation(async ({ input }) => {
2019
- const { id, ...updates } = input;
2020
- db.update(agentConfigs).set(updates).where(eq5(agentConfigs.id, id)).run();
2021
- return db.select().from(agentConfigs).where(eq5(agentConfigs.id, id)).get();
2022
- }),
2023
- delete: publicProcedure.input(z3.object({ id: z3.string() })).mutation(async ({ input }) => {
2024
- db.delete(agentConfigs).where(eq5(agentConfigs.id, input.id)).run();
2025
- return { ok: true };
2026
- })
2027
- });
2028
-
2029
- // src/trpc/files.ts
2030
- import { z as z4 } from "zod";
2031
- import { eq as eq6 } from "drizzle-orm";
2032
- import * as fs3 from "fs/promises";
2033
- import * as path3 from "path";
2034
- import { glob } from "glob";
2035
- async function listFilesForAutocomplete(workingDir, partialPath, limit) {
2036
- const normalized = partialPath.startsWith("./") ? partialPath.slice(2) : partialPath;
2037
- if (normalized.includes("/")) {
2038
- return listFilesPrefix(workingDir, normalized, limit);
2039
- }
2040
- return listFilesFuzzy(workingDir, normalized, limit);
2041
- }
2042
- async function listFilesPrefix(workingDir, partialPath, limit) {
2043
- const lastSlash = partialPath.lastIndexOf("/");
2044
- const dirPart = lastSlash >= 0 ? partialPath.slice(0, lastSlash) : "";
2045
- const prefix = lastSlash >= 0 ? partialPath.slice(lastSlash + 1) : partialPath;
2046
- const targetDir = path3.resolve(workingDir, dirPart);
2047
- const normalizedWorkingDir = path3.resolve(workingDir);
2048
- const normalizedTargetDir = path3.resolve(targetDir);
2049
- if (!normalizedTargetDir.startsWith(normalizedWorkingDir)) {
2050
- return [];
2051
- }
2052
- try {
2053
- const entries = await fs3.readdir(targetDir, { withFileTypes: true });
2054
- const matching = entries.filter((e) => !e.name.startsWith(".")).filter((e) => prefix === "" || e.name.toLowerCase().startsWith(prefix.toLowerCase())).slice(0, limit * 2).map((e) => ({
2055
- name: e.name,
2056
- path: dirPart ? `${dirPart}/${e.name}` : e.name,
2057
- isDirectory: e.isDirectory()
2058
- }));
2059
- matching.sort((a, b) => {
2060
- if (a.isDirectory !== b.isDirectory) {
2061
- return a.isDirectory ? -1 : 1;
2062
- }
2063
- return a.name.localeCompare(b.name);
2064
- });
2065
- return matching.slice(0, limit);
2066
- } catch {
2067
- return [];
2068
- }
2069
- }
2070
- async function listFilesFuzzy(workingDir, query, limit) {
2071
- if (!query.trim()) return [];
2072
- try {
2073
- const pattern = `{*${query}*,**/*${query}*}`;
2074
- const matches = await glob(pattern, {
2075
- cwd: workingDir,
2076
- nodir: false,
2077
- dot: false,
2078
- // Skip hidden files
2079
- ignore: ["**/node_modules/**", "**/.git/**"],
2080
- maxDepth: 10
2081
- });
2082
- const results = [];
2083
- for (const match of matches.slice(0, limit * 2)) {
2084
- try {
2085
- const fullPath = path3.join(workingDir, match);
2086
- const stat2 = await fs3.stat(fullPath);
2087
- results.push({
2088
- name: path3.basename(match),
2089
- path: match,
2090
- isDirectory: stat2.isDirectory()
2091
- });
2092
- } catch {
2093
- }
2094
- }
2095
- results.sort((a, b) => {
2096
- if (a.isDirectory !== b.isDirectory) {
2097
- return a.isDirectory ? -1 : 1;
2098
- }
2099
- const aDepth = a.path.split("/").length;
2100
- const bDepth = b.path.split("/").length;
2101
- if (aDepth !== bDepth) {
2102
- return aDepth - bDepth;
2103
- }
2104
- return a.path.localeCompare(b.path);
2105
- });
2106
- return results.slice(0, limit);
2107
- } catch {
2108
- return [];
2109
- }
2110
- }
2111
- var filesRouter = router({
2112
- /**
2113
- * List files/directories matching a partial path for autocomplete.
2114
- * Returns files in the session's working directory.
2115
- */
2116
- listForAutocomplete: publicProcedure.input(z4.object({
2117
- sessionId: z4.string(),
2118
- partialPath: z4.string(),
2119
- limit: z4.number().default(20)
2120
- })).query(async ({ input }) => {
2121
- const session = db.select().from(sessions).where(eq6(sessions.id, input.sessionId)).get();
2122
- if (!session) throw new Error(`Session ${input.sessionId} not found`);
2123
- return listFilesForAutocomplete(
2124
- session.directory,
2125
- input.partialPath,
2126
- input.limit
2127
- );
2128
- })
2129
- });
2130
-
2131
- // src/trpc/router.ts
2132
- var appRouter = router({
2133
- sessions: sessionsRouter,
2134
- agents: agentsRouter,
2135
- agentConfigs: agentConfigsRouter,
2136
- files: filesRouter
2137
- });
2138
-
2139
- // src/server.ts
2140
- function createAmuxServer(options = {}) {
2141
- const app = express();
2142
- app.use(cors());
2143
- app.use(express.json());
2144
- app.use("/trpc", createExpressMiddleware({ router: appRouter }));
2145
- app.get("/health", (_req, res) => res.json({ status: "ok", dbPath: getDbPath() }));
2146
- const server = createServer(app);
2147
- const wss = new WebSocketServer({ server, path: "/trpc" });
2148
- applyWSSHandler({ wss, router: appRouter });
2149
- return {
2150
- app,
2151
- server,
2152
- wss,
2153
- start: (port) => {
2154
- return new Promise((resolve2) => {
2155
- server.listen(port, () => {
2156
- console.log(`[amux] Running on http://localhost:${port}`);
2157
- console.log(`[amux] WebSocket at ws://localhost:${port}/trpc`);
2158
- console.log(`[amux] Database at ${getDbPath()}`);
2159
- options.onReady?.(port);
2160
- resolve2();
2161
- });
2162
- });
2163
- },
2164
- stop: async () => {
2165
- setShuttingDown(true);
2166
- console.log("[amux] Shutting down...");
2167
- wss.close();
2168
- await agentManager.stopAll();
2169
- console.log("[amux] Agents stopped");
2170
- server.close();
2171
- }
2172
- };
2173
- }
3
+ createAmuxServer
4
+ } from "../chunk-YYN3GXYP.js";
5
+ import "../chunk-2NON2HR2.js";
6
+ import "../chunk-SX7NC3ZM.js";
7
+ import "../chunk-226DBKL3.js";
8
+ import "../chunk-5IPYOXBE.js";
9
+ import "../chunk-L4DBPVMA.js";
10
+ import "../chunk-OQ5K5ON2.js";
11
+ import "../chunk-PZ5AY32C.js";
2174
12
 
2175
13
  // bin/cli.ts
14
+ import { program } from "commander";
2176
15
  var VERSION = "0.1.0";
2177
16
  program.name("amux").description("Agent Multiplexer - headless server for AI coding agents").version(VERSION).option("-p, --port <port>", "Server port", "3078").action(async (options) => {
2178
17
  const port = parseInt(options.port, 10);