@bytespell/amux 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,401 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __export = (target, all) => {
3
+ for (var name in all)
4
+ __defProp(target, name, { get: all[name], enumerable: true });
5
+ };
6
+
7
+ // src/trpc/files.ts
8
+ import { z } from "zod";
9
+
10
+ // src/trpc/trpc.ts
11
+ import { initTRPC } from "@trpc/server";
12
+ var t = initTRPC.create();
13
+ var router = t.router;
14
+ var publicProcedure = t.procedure;
15
+
16
+ // src/db/index.ts
17
+ import { randomUUID as randomUUID2 } from "crypto";
18
+ import Database from "better-sqlite3";
19
+ import { drizzle } from "drizzle-orm/better-sqlite3";
20
+
21
+ // src/lib/paths.ts
22
+ import path from "path";
23
+ import os from "os";
24
+ import fs from "fs";
25
+ var STARTUP_CWD = process.cwd();
26
+ function getDataDir() {
27
+ const home = os.homedir();
28
+ let dataDir;
29
+ switch (process.platform) {
30
+ case "darwin":
31
+ dataDir = path.join(home, "Library", "Application Support", "shella");
32
+ break;
33
+ case "win32":
34
+ dataDir = path.join(process.env.APPDATA || path.join(home, "AppData", "Roaming"), "shella");
35
+ break;
36
+ default:
37
+ dataDir = path.join(process.env.XDG_DATA_HOME || path.join(home, ".local", "share"), "shella");
38
+ }
39
+ return dataDir;
40
+ }
41
+ function isMockMode() {
42
+ return process.env.SHELLA_MOCK_MODE === "true";
43
+ }
44
+ function getDbPath() {
45
+ const filename = isMockMode() ? "shella.mock.db" : "shella.db";
46
+ return path.join(getDataDir(), filename);
47
+ }
48
+ function ensureDir(dir) {
49
+ if (!fs.existsSync(dir)) {
50
+ fs.mkdirSync(dir, { recursive: true });
51
+ }
52
+ }
53
+
54
+ // src/db/schema.ts
55
+ var schema_exports = {};
56
+ __export(schema_exports, {
57
+ agentConfigs: () => agentConfigs,
58
+ appState: () => appState,
59
+ sessionEvents: () => sessionEvents,
60
+ sessions: () => sessions
61
+ });
62
+ import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
63
+ import { sql } from "drizzle-orm";
64
+ var agentConfigs = sqliteTable("agent_configs", {
65
+ id: text("id").primaryKey(),
66
+ name: text("name").notNull(),
67
+ command: text("command").notNull(),
68
+ args: text("args", { mode: "json" }).$type().default([]),
69
+ env: text("env", { mode: "json" }).$type().default({}),
70
+ createdAt: integer("created_at", { mode: "timestamp_ms" }).notNull().default(sql`(unixepoch() * 1000)`)
71
+ });
72
+ var sessions = sqliteTable("sessions", {
73
+ id: text("id").primaryKey(),
74
+ directory: text("directory").notNull(),
75
+ agentConfigId: text("agent_config_id").notNull().references(() => agentConfigs.id),
76
+ // ACP protocol session ID for resuming (internal to agent protocol)
77
+ acpSessionId: text("acp_session_id"),
78
+ // Title from agent (via session_info_update)
79
+ title: text("title"),
80
+ model: text("model"),
81
+ mode: text("mode"),
82
+ createdAt: integer("created_at", { mode: "timestamp_ms" }).notNull().default(sql`(unixepoch() * 1000)`)
83
+ });
84
+ var appState = sqliteTable("app_state", {
85
+ key: text("key").primaryKey(),
86
+ value: text("value").notNull()
87
+ });
88
+ var sessionEvents = sqliteTable("session_events", {
89
+ id: text("id").primaryKey(),
90
+ sessionId: text("session_id").notNull().references(() => sessions.id, { onDelete: "cascade" }),
91
+ turnId: text("turn_id").notNull(),
92
+ sequenceNum: integer("sequence_num").notNull(),
93
+ eventKind: text("event_kind").notNull(),
94
+ payload: text("payload", { mode: "json" }).notNull(),
95
+ createdAt: integer("created_at", { mode: "timestamp_ms" }).notNull().default(sql`(unixepoch() * 1000)`)
96
+ });
97
+
98
+ // src/db/seed.ts
99
+ import { randomUUID } from "crypto";
100
+ import { execSync } from "child_process";
101
+ var MOCK_AGENT_ID = "mock-agent";
102
+ function commandExists(cmd) {
103
+ try {
104
+ execSync(`which ${cmd}`, { stdio: "ignore" });
105
+ return true;
106
+ } catch {
107
+ return false;
108
+ }
109
+ }
110
+ function detectAgents() {
111
+ const agents = [];
112
+ agents.push({
113
+ id: randomUUID(),
114
+ name: "Claude",
115
+ command: "npx",
116
+ args: ["@zed-industries/claude-code-acp"],
117
+ env: {}
118
+ });
119
+ if (commandExists("opencode")) {
120
+ agents.push({
121
+ id: randomUUID(),
122
+ name: "OpenCode",
123
+ command: "opencode",
124
+ args: ["acp"],
125
+ env: {}
126
+ });
127
+ }
128
+ return agents;
129
+ }
130
+ var MOCK_CONFIGS = [
131
+ {
132
+ id: MOCK_AGENT_ID,
133
+ name: "Test Agent",
134
+ command: "__stress__",
135
+ args: [],
136
+ env: {}
137
+ }
138
+ ];
139
+ function seedAgentConfigs() {
140
+ if (isMockMode()) {
141
+ const existing2 = db.select().from(agentConfigs).all();
142
+ if (existing2.length === 0) {
143
+ for (const config of MOCK_CONFIGS) {
144
+ db.insert(agentConfigs).values(config).run();
145
+ }
146
+ console.log("[db] Seeded mock agent configs");
147
+ }
148
+ seedMockSessions();
149
+ return;
150
+ }
151
+ const detected = detectAgents();
152
+ const existing = db.select().from(agentConfigs).all();
153
+ let added = 0;
154
+ for (const agent of detected) {
155
+ const alreadyExists = existing.some(
156
+ (e) => e.command === agent.command && JSON.stringify(e.args) === JSON.stringify(agent.args)
157
+ );
158
+ if (!alreadyExists) {
159
+ db.insert(agentConfigs).values(agent).run();
160
+ console.log(`[db] Detected new agent: ${agent.name}`);
161
+ added++;
162
+ }
163
+ }
164
+ if (added === 0 && detected.length > 0) {
165
+ console.log(`[db] ${detected.length} known agent(s) already configured`);
166
+ }
167
+ }
168
+ function seedMockSessions() {
169
+ const existing = db.select().from(sessions).all();
170
+ if (existing.length > 0) return;
171
+ const count = getSessionCount(10);
172
+ for (let i = 1; i <= count; i++) {
173
+ db.insert(sessions).values({
174
+ id: randomUUID(),
175
+ directory: "~",
176
+ agentConfigId: MOCK_AGENT_ID
177
+ }).run();
178
+ }
179
+ console.log(`[db] Seeded ${count} test sessions`);
180
+ }
181
+ function getSessionCount(defaultCount) {
182
+ const envCount = process.env.SHELLA_MOCK_SESSIONS ?? process.env.SHELLA_MOCK_WINDOWS;
183
+ if (envCount) {
184
+ const parsed = parseInt(envCount, 10);
185
+ if (!isNaN(parsed) && parsed > 0) {
186
+ return parsed;
187
+ }
188
+ }
189
+ return defaultCount;
190
+ }
191
+
192
+ // src/db/index.ts
193
+ ensureDir(getDataDir());
194
+ var sqlite = new Database(getDbPath());
195
+ sqlite.pragma("journal_mode = WAL");
196
+ sqlite.pragma("foreign_keys = ON");
197
+ var db = drizzle(sqlite, { schema: schema_exports });
198
+ sqlite.exec(`
199
+ CREATE TABLE IF NOT EXISTS agent_configs (
200
+ id TEXT PRIMARY KEY,
201
+ name TEXT NOT NULL,
202
+ command TEXT NOT NULL,
203
+ args TEXT DEFAULT '[]',
204
+ env TEXT DEFAULT '{}',
205
+ created_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000)
206
+ );
207
+
208
+ CREATE TABLE IF NOT EXISTS sessions (
209
+ id TEXT PRIMARY KEY,
210
+ directory TEXT NOT NULL,
211
+ agent_config_id TEXT NOT NULL REFERENCES agent_configs(id),
212
+ acp_session_id TEXT,
213
+ title TEXT,
214
+ model TEXT,
215
+ mode TEXT,
216
+ created_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000)
217
+ );
218
+
219
+ CREATE TABLE IF NOT EXISTS app_state (
220
+ key TEXT PRIMARY KEY,
221
+ value TEXT NOT NULL
222
+ );
223
+
224
+ CREATE TABLE IF NOT EXISTS session_events (
225
+ id TEXT PRIMARY KEY,
226
+ session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
227
+ turn_id TEXT NOT NULL,
228
+ sequence_num INTEGER NOT NULL,
229
+ event_kind TEXT NOT NULL,
230
+ payload TEXT NOT NULL,
231
+ created_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000)
232
+ );
233
+ `);
234
+ try {
235
+ const oldExists = sqlite.prepare(`SELECT name FROM sqlite_master WHERE type='table' AND name='client_windows'`).get();
236
+ const newCount = sqlite.prepare(`SELECT COUNT(*) as count FROM sessions`).get();
237
+ if (oldExists && newCount.count === 0) {
238
+ console.log("[db] Migrating from client_windows/window_events to sessions/session_events...");
239
+ sqlite.exec(`
240
+ INSERT INTO sessions (id, directory, agent_config_id, acp_session_id, model, mode, created_at)
241
+ SELECT id, directory, agent_config_id, session_id, model, mode, created_at
242
+ FROM client_windows;
243
+
244
+ INSERT INTO session_events (id, session_id, turn_id, sequence_num, event_kind, payload, created_at)
245
+ SELECT id, window_id, turn_id, sequence_num, event_kind, payload, created_at
246
+ FROM window_events;
247
+
248
+ -- Migrate custom titles to app_state
249
+ INSERT OR IGNORE INTO app_state (key, value)
250
+ SELECT 'window_title_' || id, title
251
+ FROM client_windows
252
+ WHERE has_custom_title = 1 AND title != '';
253
+
254
+ -- Migrate active window
255
+ INSERT OR REPLACE INTO app_state (key, value)
256
+ SELECT 'active_window_id', value
257
+ FROM app_state
258
+ WHERE key = 'active_window_id';
259
+ `);
260
+ sqlite.exec(`
261
+ DROP TABLE IF EXISTS window_events;
262
+ DROP TABLE IF EXISTS client_windows;
263
+ `);
264
+ console.log("[db] Migration complete");
265
+ }
266
+ } catch (e) {
267
+ }
268
+ try {
269
+ sqlite.exec(`ALTER TABLE sessions ADD COLUMN title TEXT`);
270
+ } catch (e) {
271
+ }
272
+ var orphanedTurns = sqlite.prepare(`
273
+ SELECT DISTINCT turn_id, session_id
274
+ FROM session_events
275
+ WHERE event_kind = 'turn_start'
276
+ AND turn_id NOT IN (
277
+ SELECT turn_id FROM session_events WHERE event_kind = 'turn_end'
278
+ )
279
+ `).all();
280
+ if (orphanedTurns.length > 0) {
281
+ const getMaxSeq = sqlite.prepare(`
282
+ SELECT COALESCE(MAX(sequence_num), 0) + 1 as next_seq
283
+ FROM session_events WHERE turn_id = ?
284
+ `);
285
+ const insert = sqlite.prepare(`
286
+ INSERT INTO session_events (id, session_id, turn_id, sequence_num, event_kind, payload)
287
+ VALUES (?, ?, ?, ?, 'turn_end', '{"amuxEvent":"turn_end"}')
288
+ `);
289
+ for (const turn of orphanedTurns) {
290
+ const { next_seq } = getMaxSeq.get(turn.turn_id);
291
+ insert.run(randomUUID2(), turn.session_id, turn.turn_id, next_seq);
292
+ }
293
+ console.log(`[db] Fixed ${orphanedTurns.length} orphaned turn(s) from previous session`);
294
+ }
295
+ seedAgentConfigs();
296
+
297
+ // src/trpc/files.ts
298
+ import { eq } from "drizzle-orm";
299
+ import * as fs2 from "fs/promises";
300
+ import * as path2 from "path";
301
+ import { glob } from "glob";
302
+ async function listFilesForAutocomplete(workingDir, partialPath, limit) {
303
+ const normalized = partialPath.startsWith("./") ? partialPath.slice(2) : partialPath;
304
+ if (normalized.includes("/")) {
305
+ return listFilesPrefix(workingDir, normalized, limit);
306
+ }
307
+ return listFilesFuzzy(workingDir, normalized, limit);
308
+ }
309
+ async function listFilesPrefix(workingDir, partialPath, limit) {
310
+ const lastSlash = partialPath.lastIndexOf("/");
311
+ const dirPart = lastSlash >= 0 ? partialPath.slice(0, lastSlash) : "";
312
+ const prefix = lastSlash >= 0 ? partialPath.slice(lastSlash + 1) : partialPath;
313
+ const targetDir = path2.resolve(workingDir, dirPart);
314
+ const normalizedWorkingDir = path2.resolve(workingDir);
315
+ const normalizedTargetDir = path2.resolve(targetDir);
316
+ if (!normalizedTargetDir.startsWith(normalizedWorkingDir)) {
317
+ return [];
318
+ }
319
+ try {
320
+ const entries = await fs2.readdir(targetDir, { withFileTypes: true });
321
+ const matching = entries.filter((e) => !e.name.startsWith(".")).filter((e) => prefix === "" || e.name.toLowerCase().startsWith(prefix.toLowerCase())).slice(0, limit * 2).map((e) => ({
322
+ name: e.name,
323
+ path: dirPart ? `${dirPart}/${e.name}` : e.name,
324
+ isDirectory: e.isDirectory()
325
+ }));
326
+ matching.sort((a, b) => {
327
+ if (a.isDirectory !== b.isDirectory) {
328
+ return a.isDirectory ? -1 : 1;
329
+ }
330
+ return a.name.localeCompare(b.name);
331
+ });
332
+ return matching.slice(0, limit);
333
+ } catch {
334
+ return [];
335
+ }
336
+ }
337
+ async function listFilesFuzzy(workingDir, query, limit) {
338
+ if (!query.trim()) return [];
339
+ try {
340
+ const pattern = `{*${query}*,**/*${query}*}`;
341
+ const matches = await glob(pattern, {
342
+ cwd: workingDir,
343
+ nodir: false,
344
+ dot: false,
345
+ // Skip hidden files
346
+ ignore: ["**/node_modules/**", "**/.git/**"],
347
+ maxDepth: 10
348
+ });
349
+ const results = [];
350
+ for (const match of matches.slice(0, limit * 2)) {
351
+ try {
352
+ const fullPath = path2.join(workingDir, match);
353
+ const stat2 = await fs2.stat(fullPath);
354
+ results.push({
355
+ name: path2.basename(match),
356
+ path: match,
357
+ isDirectory: stat2.isDirectory()
358
+ });
359
+ } catch {
360
+ }
361
+ }
362
+ results.sort((a, b) => {
363
+ if (a.isDirectory !== b.isDirectory) {
364
+ return a.isDirectory ? -1 : 1;
365
+ }
366
+ const aDepth = a.path.split("/").length;
367
+ const bDepth = b.path.split("/").length;
368
+ if (aDepth !== bDepth) {
369
+ return aDepth - bDepth;
370
+ }
371
+ return a.path.localeCompare(b.path);
372
+ });
373
+ return results.slice(0, limit);
374
+ } catch {
375
+ return [];
376
+ }
377
+ }
378
+ var filesRouter = router({
379
+ /**
380
+ * List files/directories matching a partial path for autocomplete.
381
+ * Returns files in the session's working directory.
382
+ */
383
+ listForAutocomplete: publicProcedure.input(z.object({
384
+ sessionId: z.string(),
385
+ partialPath: z.string(),
386
+ limit: z.number().default(20)
387
+ })).query(async ({ input }) => {
388
+ const session = db.select().from(sessions).where(eq(sessions.id, input.sessionId)).get();
389
+ if (!session) throw new Error(`Session ${input.sessionId} not found`);
390
+ return listFilesForAutocomplete(
391
+ session.directory,
392
+ input.partialPath,
393
+ input.limit
394
+ );
395
+ })
396
+ });
397
+ export {
398
+ filesRouter,
399
+ listFilesForAutocomplete
400
+ };
401
+ //# sourceMappingURL=files.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/trpc/files.ts","../../../src/trpc/trpc.ts","../../../src/db/index.ts","../../../src/lib/paths.ts","../../../src/db/schema.ts","../../../src/db/seed.ts"],"sourcesContent":["import { z } from 'zod';\nimport { router, publicProcedure } from './trpc.js';\nimport { db } from '../db/index.js';\nimport { sessions } from '../db/schema.js';\nimport { eq } from 'drizzle-orm';\nimport * as fs from 'fs/promises';\nimport * as path from 'path';\nimport { glob } from 'glob';\n\nexport interface FileEntry {\n name: string; // File/directory name\n path: string; // Full relative path for insertion\n isDirectory: boolean;\n}\n\n/**\n * List files/directories matching a partial path for autocomplete.\n * Uses glob for fuzzy full-text search across the entire project.\n * Exported for direct use by AmuxBridge.\n */\nexport async function listFilesForAutocomplete(\n workingDir: string,\n partialPath: string,\n limit: number\n): Promise<FileEntry[]> {\n // Normalize partial path (strip leading ./)\n const normalized = partialPath.startsWith('./')\n ? partialPath.slice(2)\n : partialPath;\n\n // If query contains a slash, user is navigating into a directory - use prefix match\n if (normalized.includes('/')) {\n return listFilesPrefix(workingDir, normalized, limit);\n }\n\n // Otherwise, do a fuzzy search across the entire project\n return listFilesFuzzy(workingDir, normalized, limit);\n}\n\n/**\n * Prefix-based listing for directory navigation (e.g., \"src/comp\")\n */\nasync function listFilesPrefix(\n workingDir: string,\n partialPath: string,\n limit: number\n): Promise<FileEntry[]> {\n const lastSlash = partialPath.lastIndexOf('/');\n const dirPart = lastSlash >= 0 ? partialPath.slice(0, lastSlash) : '';\n const prefix = lastSlash >= 0 ? partialPath.slice(lastSlash + 1) : partialPath;\n\n const targetDir = path.resolve(workingDir, dirPart);\n\n // Security: ensure we're within working directory\n const normalizedWorkingDir = path.resolve(workingDir);\n const normalizedTargetDir = path.resolve(targetDir);\n if (!normalizedTargetDir.startsWith(normalizedWorkingDir)) {\n return [];\n }\n\n try {\n const entries = await fs.readdir(targetDir, { withFileTypes: true });\n\n const matching = entries\n .filter(e => !e.name.startsWith('.'))\n .filter(e => prefix === '' || e.name.toLowerCase().startsWith(prefix.toLowerCase()))\n .slice(0, limit * 2)\n .map(e => ({\n name: e.name,\n path: dirPart ? `${dirPart}/${e.name}` : e.name,\n isDirectory: e.isDirectory(),\n }));\n\n matching.sort((a, b) => {\n if (a.isDirectory !== b.isDirectory) {\n return a.isDirectory ? -1 : 1;\n }\n return a.name.localeCompare(b.name);\n });\n\n return matching.slice(0, limit);\n } catch {\n return [];\n }\n}\n\n/**\n * Fuzzy search across entire project using glob\n */\nasync function listFilesFuzzy(\n workingDir: string,\n query: string,\n limit: number\n): Promise<FileEntry[]> {\n if (!query.trim()) return [];\n\n try {\n // Use glob to find files matching *query* pattern anywhere in the project\n // Use brace expansion to match both root-level files and nested files\n const pattern = `{*${query}*,**/*${query}*}`;\n const matches = await glob(pattern, {\n cwd: workingDir,\n nodir: false,\n dot: false, // Skip hidden files\n ignore: ['**/node_modules/**', '**/.git/**'],\n maxDepth: 10,\n });\n\n // Get file stats to determine if directory\n const results: FileEntry[] = [];\n for (const match of matches.slice(0, limit * 2)) {\n try {\n const fullPath = path.join(workingDir, match);\n const stat = await fs.stat(fullPath);\n results.push({\n name: path.basename(match),\n path: match,\n isDirectory: stat.isDirectory(),\n });\n } catch {\n // Skip files we can't stat\n }\n }\n\n // Sort: directories first, shorter paths first, then alphabetically\n results.sort((a, b) => {\n if (a.isDirectory !== b.isDirectory) {\n return a.isDirectory ? -1 : 1;\n }\n // Prefer shorter paths (closer to root)\n const aDepth = a.path.split('/').length;\n const bDepth = b.path.split('/').length;\n if (aDepth !== bDepth) {\n return aDepth - bDepth;\n }\n return a.path.localeCompare(b.path);\n });\n\n return results.slice(0, limit);\n } catch {\n return [];\n }\n}\n\nexport const filesRouter = router({\n /**\n * List files/directories matching a partial path for autocomplete.\n * Returns files in the session's working directory.\n */\n listForAutocomplete: publicProcedure\n .input(z.object({\n sessionId: z.string(),\n partialPath: z.string(),\n limit: z.number().default(20),\n }))\n .query(async ({ input }) => {\n const session = db.select().from(sessions).where(eq(sessions.id, input.sessionId)).get();\n if (!session) throw new Error(`Session ${input.sessionId} not found`);\n\n return listFilesForAutocomplete(\n session.directory,\n input.partialPath,\n input.limit\n );\n }),\n});\n","import { initTRPC } from '@trpc/server';\n\nconst t = initTRPC.create();\n\nexport const router = t.router;\nexport const publicProcedure = t.procedure;\n","import { randomUUID } from 'node:crypto';\nimport Database from 'better-sqlite3';\nimport { drizzle } from 'drizzle-orm/better-sqlite3';\nimport { getDbPath, ensureDir, getDataDir } from '../lib/paths.js';\nimport * as schema from './schema.js';\nimport { seedAgentConfigs } from './seed.js';\n\nensureDir(getDataDir());\n\nconst sqlite = new Database(getDbPath());\nsqlite.pragma('journal_mode = WAL');\nsqlite.pragma('foreign_keys = ON');\n\nexport const db = drizzle(sqlite, { schema });\n\n// Create tables if they don't exist\nsqlite.exec(`\n CREATE TABLE IF NOT EXISTS agent_configs (\n id TEXT PRIMARY KEY,\n name TEXT NOT NULL,\n command TEXT NOT NULL,\n args TEXT DEFAULT '[]',\n env TEXT DEFAULT '{}',\n created_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000)\n );\n\n CREATE TABLE IF NOT EXISTS sessions (\n id TEXT PRIMARY KEY,\n directory TEXT NOT NULL,\n agent_config_id TEXT NOT NULL REFERENCES agent_configs(id),\n acp_session_id TEXT,\n title TEXT,\n model TEXT,\n mode TEXT,\n created_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000)\n );\n\n CREATE TABLE IF NOT EXISTS app_state (\n key TEXT PRIMARY KEY,\n value TEXT NOT NULL\n );\n\n CREATE TABLE IF NOT EXISTS session_events (\n id TEXT PRIMARY KEY,\n session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,\n turn_id TEXT NOT NULL,\n sequence_num INTEGER NOT NULL,\n event_kind TEXT NOT NULL,\n payload TEXT NOT NULL,\n created_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000)\n );\n`);\n\n// Migration: rename old tables if they exist\ntry {\n // Check if old tables exist and new ones don't have data\n const oldExists = sqlite.prepare(`SELECT name FROM sqlite_master WHERE type='table' AND name='client_windows'`).get();\n const newCount = sqlite.prepare(`SELECT COUNT(*) as count FROM sessions`).get() as { count: number };\n\n if (oldExists && newCount.count === 0) {\n console.log('[db] Migrating from client_windows/window_events to sessions/session_events...');\n\n // Migrate data from old tables to new tables\n sqlite.exec(`\n INSERT INTO sessions (id, directory, agent_config_id, acp_session_id, model, mode, created_at)\n SELECT id, directory, agent_config_id, session_id, model, mode, created_at\n FROM client_windows;\n\n INSERT INTO session_events (id, session_id, turn_id, sequence_num, event_kind, payload, created_at)\n SELECT id, window_id, turn_id, sequence_num, event_kind, payload, created_at\n FROM window_events;\n\n -- Migrate custom titles to app_state\n INSERT OR IGNORE INTO app_state (key, value)\n SELECT 'window_title_' || id, title\n FROM client_windows\n WHERE has_custom_title = 1 AND title != '';\n\n -- Migrate active window\n INSERT OR REPLACE INTO app_state (key, value)\n SELECT 'active_window_id', value\n FROM app_state\n WHERE key = 'active_window_id';\n `);\n\n // Drop old tables\n sqlite.exec(`\n DROP TABLE IF EXISTS window_events;\n DROP TABLE IF EXISTS client_windows;\n `);\n\n console.log('[db] Migration complete');\n }\n} catch (e) {\n // Migration not needed or already done\n}\n\n// Migration: add title column to sessions if missing\ntry {\n sqlite.exec(`ALTER TABLE sessions ADD COLUMN title TEXT`);\n} catch (e) {\n // Column already exists\n}\n\n// Fix orphaned turns from previous crash (turn_start without turn_end)\n// Using raw SQL to avoid circular dependency with eventStore\nconst orphanedTurns = sqlite.prepare(`\n SELECT DISTINCT turn_id, session_id\n FROM session_events\n WHERE event_kind = 'turn_start'\n AND turn_id NOT IN (\n SELECT turn_id FROM session_events WHERE event_kind = 'turn_end'\n )\n`).all() as { turn_id: string; session_id: string }[];\n\nif (orphanedTurns.length > 0) {\n const getMaxSeq = sqlite.prepare(`\n SELECT COALESCE(MAX(sequence_num), 0) + 1 as next_seq\n FROM session_events WHERE turn_id = ?\n `);\n const insert = sqlite.prepare(`\n INSERT INTO session_events (id, session_id, turn_id, sequence_num, event_kind, payload)\n VALUES (?, ?, ?, ?, 'turn_end', '{\"amuxEvent\":\"turn_end\"}')\n `);\n\n for (const turn of orphanedTurns) {\n const { next_seq } = getMaxSeq.get(turn.turn_id) as { next_seq: number };\n insert.run(randomUUID(), turn.session_id, turn.turn_id, next_seq);\n }\n\n console.log(`[db] Fixed ${orphanedTurns.length} orphaned turn(s) from previous session`);\n}\n\n// Seed defaults\nseedAgentConfigs();\n\nexport { schema };\n","import path from 'path';\nimport os from 'os';\nimport fs from 'fs';\n\n// Capture at module load time (before any chdir)\nconst STARTUP_CWD = process.cwd();\n\n/**\n * Get the working directory from when the server started\n */\nexport function getStartupCwd(): string {\n return STARTUP_CWD;\n}\n\n/**\n * Get the data directory following XDG Base Directory spec\n * - Linux: ~/.local/share/shella\n * - macOS: ~/Library/Application Support/shella\n * - Windows: %APPDATA%/shella\n */\nexport function getDataDir(): string {\n const home = os.homedir();\n\n let dataDir: string;\n\n switch (process.platform) {\n case 'darwin':\n dataDir = path.join(home, 'Library', 'Application Support', 'shella');\n break;\n case 'win32':\n dataDir = path.join(process.env.APPDATA || path.join(home, 'AppData', 'Roaming'), 'shella');\n break;\n default:\n // Linux and others - follow XDG spec\n dataDir = path.join(process.env.XDG_DATA_HOME || path.join(home, '.local', 'share'), 'shella');\n }\n\n return dataDir;\n}\n\n/**\n * Get the config directory following XDG Base Directory spec\n * - Linux: ~/.config/shella\n * - macOS: ~/Library/Application Support/shella\n * - Windows: %APPDATA%/shella\n */\nexport function getConfigDir(): string {\n const home = os.homedir();\n\n let configDir: string;\n\n switch (process.platform) {\n case 'darwin':\n configDir = path.join(home, 'Library', 'Application Support', 'shella');\n break;\n case 'win32':\n configDir = path.join(process.env.APPDATA || path.join(home, 'AppData', 'Roaming'), 'shella');\n break;\n default:\n // Linux and others - follow XDG spec\n configDir = path.join(process.env.XDG_CONFIG_HOME || path.join(home, '.config'), 'shella');\n }\n\n return configDir;\n}\n\n/**\n * Get the cache directory\n * - Linux: ~/.cache/shella\n * - macOS: ~/Library/Caches/shella\n * - Windows: %LOCALAPPDATA%/shella/cache\n */\nexport function getCacheDir(): string {\n const home = os.homedir();\n\n let cacheDir: string;\n\n switch (process.platform) {\n case 'darwin':\n cacheDir = path.join(home, 'Library', 'Caches', 'shella');\n break;\n case 'win32':\n cacheDir = path.join(process.env.LOCALAPPDATA || path.join(home, 'AppData', 'Local'), 'shella', 'cache');\n break;\n default:\n // Linux and others - follow XDG spec\n cacheDir = path.join(process.env.XDG_CACHE_HOME || path.join(home, '.cache'), 'shella');\n }\n\n return cacheDir;\n}\n\n/**\n * Check if running in mock mode\n */\nexport function isMockMode(): boolean {\n return process.env.SHELLA_MOCK_MODE === 'true';\n}\n\n/**\n * Get the path to the database file\n */\nexport function getDbPath(): string {\n const filename = isMockMode() ? 'shella.mock.db' : 'shella.db';\n return path.join(getDataDir(), filename);\n}\n\n/**\n * Ensure a directory exists, creating it if necessary\n */\nexport function ensureDir(dir: string): void {\n if (!fs.existsSync(dir)) {\n fs.mkdirSync(dir, { recursive: true });\n }\n}\n\n/**\n * Ensure all shella directories exist\n */\nexport function ensureAllDirs(): void {\n ensureDir(getDataDir());\n ensureDir(getConfigDir());\n ensureDir(getCacheDir());\n}\n","import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core';\nimport { sql } from 'drizzle-orm';\n\n// User-configured agent types (e.g. claude-code-acp, opencode, etc.)\nexport const agentConfigs = sqliteTable('agent_configs', {\n id: text('id').primaryKey(),\n name: text('name').notNull(),\n command: text('command').notNull(),\n args: text('args', { mode: 'json' }).$type<string[]>().default([]),\n env: text('env', { mode: 'json' }).$type<Record<string, string>>().default({}),\n createdAt: integer('created_at', { mode: 'timestamp_ms' })\n .notNull()\n .default(sql`(unixepoch() * 1000)`),\n});\n\n// Agent sessions - each session is an independent agent instance.\nexport const sessions = sqliteTable('sessions', {\n id: text('id').primaryKey(),\n directory: text('directory').notNull(),\n agentConfigId: text('agent_config_id').notNull().references(() => agentConfigs.id),\n // ACP protocol session ID for resuming (internal to agent protocol)\n acpSessionId: text('acp_session_id'),\n // Title from agent (via session_info_update)\n title: text('title'),\n model: text('model'),\n mode: text('mode'),\n createdAt: integer('created_at', { mode: 'timestamp_ms' })\n .notNull()\n .default(sql`(unixepoch() * 1000)`),\n});\n\nexport const appState = sqliteTable('app_state', {\n key: text('key').primaryKey(),\n value: text('value').notNull(),\n});\n\n// Persisted session events for replay on reconnect\nexport const sessionEvents = sqliteTable('session_events', {\n id: text('id').primaryKey(),\n sessionId: text('session_id')\n .notNull()\n .references(() => sessions.id, { onDelete: 'cascade' }),\n turnId: text('turn_id').notNull(),\n sequenceNum: integer('sequence_num').notNull(),\n eventKind: text('event_kind').notNull(),\n payload: text('payload', { mode: 'json' }).notNull(),\n createdAt: integer('created_at', { mode: 'timestamp_ms' })\n .notNull()\n .default(sql`(unixepoch() * 1000)`),\n});\n\nexport type AgentConfig = typeof agentConfigs.$inferSelect;\nexport type NewAgentConfig = typeof agentConfigs.$inferInsert;\nexport type Session = typeof sessions.$inferSelect;\nexport type NewSession = typeof sessions.$inferInsert;\nexport type StoredSessionEvent = typeof sessionEvents.$inferSelect;\nexport type NewStoredSessionEvent = typeof sessionEvents.$inferInsert;\n\n// Legacy aliases for compatibility during migration\n/** @deprecated Use Session instead */\nexport type ClientWindow = Session;\n/** @deprecated Use NewSession instead */\nexport type NewClientWindow = NewSession;\n","import { db } from './index.js';\nimport { agentConfigs, sessions } from './schema.js';\nimport { randomUUID } from 'crypto';\nimport { execSync } from 'child_process';\nimport { isMockMode } from '../lib/paths.js';\n\nconst MOCK_AGENT_ID = 'mock-agent';\n\n// Check if a command exists on the system\nfunction commandExists(cmd: string): boolean {\n try {\n execSync(`which ${cmd}`, { stdio: 'ignore' });\n return true;\n } catch {\n return false;\n }\n}\n\n// Detect available ACP-compatible agents\nfunction detectAgents(): Array<{ id: string; name: string; command: string; args: string[]; env: Record<string, string> }> {\n const agents: Array<{ id: string; name: string; command: string; args: string[]; env: Record<string, string> }> = [];\n\n // Claude - always available via npx\n agents.push({\n id: randomUUID(),\n name: 'Claude',\n command: 'npx',\n args: ['@zed-industries/claude-code-acp'],\n env: {},\n });\n\n // OpenCode - check if installed\n if (commandExists('opencode')) {\n agents.push({\n id: randomUUID(),\n name: 'OpenCode',\n command: 'opencode',\n args: ['acp'],\n env: {},\n });\n }\n\n return agents;\n}\n\n// Mock mode now uses the stress backend for full testing capabilities\nconst MOCK_CONFIGS = [\n {\n id: MOCK_AGENT_ID,\n name: 'Test Agent',\n command: '__stress__',\n args: [],\n env: {},\n },\n];\n\nexport function seedAgentConfigs() {\n if (isMockMode()) {\n // Mock mode: seed once as before\n const existing = db.select().from(agentConfigs).all();\n if (existing.length === 0) {\n for (const config of MOCK_CONFIGS) {\n db.insert(agentConfigs).values(config).run();\n }\n console.log('[db] Seeded mock agent configs');\n }\n seedMockSessions();\n return;\n }\n\n // Production: detect and add new agents on every launch\n const detected = detectAgents();\n const existing = db.select().from(agentConfigs).all();\n\n let added = 0;\n for (const agent of detected) {\n // Check if this agent already exists (by command + args)\n const alreadyExists = existing.some(\n (e) => e.command === agent.command &&\n JSON.stringify(e.args) === JSON.stringify(agent.args)\n );\n if (!alreadyExists) {\n db.insert(agentConfigs).values(agent).run();\n console.log(`[db] Detected new agent: ${agent.name}`);\n added++;\n }\n }\n\n if (added === 0 && detected.length > 0) {\n console.log(`[db] ${detected.length} known agent(s) already configured`);\n }\n}\n\nfunction seedMockSessions() {\n const existing = db.select().from(sessions).all();\n if (existing.length > 0) return;\n\n const count = getSessionCount(10);\n for (let i = 1; i <= count; i++) {\n db.insert(sessions).values({\n id: randomUUID(),\n directory: '~',\n agentConfigId: MOCK_AGENT_ID,\n }).run();\n }\n console.log(`[db] Seeded ${count} test sessions`);\n}\n\nfunction getSessionCount(defaultCount: number): number {\n // Support both new and legacy env var names\n const envCount = process.env.SHELLA_MOCK_SESSIONS ?? process.env.SHELLA_MOCK_WINDOWS;\n if (envCount) {\n const parsed = parseInt(envCount, 10);\n if (!isNaN(parsed) && parsed > 0) {\n return parsed;\n }\n }\n return defaultCount;\n}\n"],"mappings":"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","names":["randomUUID","existing","randomUUID","fs","path","stat"]}
@@ -0,0 +1,25 @@
1
+ // src/types.ts
2
+ function normalizePlanEntry(entry) {
3
+ const meta = entry._meta;
4
+ const claudeCode = meta?.claudeCode;
5
+ const activeForm = claudeCode?.activeForm;
6
+ const directActiveForm = entry.activeForm;
7
+ return {
8
+ content: entry.content,
9
+ status: entry.status,
10
+ priority: entry.priority,
11
+ activeForm: activeForm ?? directActiveForm
12
+ };
13
+ }
14
+ function isSessionUpdate(update) {
15
+ return "sessionUpdate" in update;
16
+ }
17
+ function isAmuxEvent(update) {
18
+ return "amuxEvent" in update;
19
+ }
20
+ export {
21
+ isAmuxEvent,
22
+ isSessionUpdate,
23
+ normalizePlanEntry
24
+ };
25
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/types.ts"],"sourcesContent":["// Re-export ACP types from SDK\nexport type {\n SessionUpdate,\n ContentBlock,\n ContentChunk,\n TextContent,\n ImageContent,\n AudioContent,\n EmbeddedResource,\n ResourceLink,\n PlanEntry as AcpPlanEntry,\n PlanEntryPriority,\n PlanEntryStatus,\n Plan,\n AvailableCommand,\n AvailableCommandsUpdate,\n CurrentModeUpdate,\n ToolCall,\n ToolCallUpdate,\n ToolCallStatus,\n PermissionOption,\n RequestPermissionRequest,\n RequestPermissionResponse,\n} from '@agentclientprotocol/sdk';\n\nimport type { PlanEntry as AcpPlanEntry, PlanEntryPriority, PlanEntryStatus, SessionUpdate } from '@agentclientprotocol/sdk';\n\n/**\n * Normalized PlanEntry that handles agent-specific quirks.\n *\n * Standard ACP: content, priority, status\n * Claude-style: content, status, activeForm (priority always \"medium\")\n *\n * The `activeForm` field provides a present-tense description (e.g., \"Analyzing tests\")\n * vs the imperative `content` (e.g., \"Analyze tests\"). When `activeForm` is present\n * and the status is \"in_progress\", UI should prefer displaying `activeForm`.\n */\nexport interface NormalizedPlanEntry {\n content: string;\n status: PlanEntryStatus;\n priority: PlanEntryPriority;\n /** Present-tense form for in_progress display (Claude-style). */\n activeForm?: string;\n}\n\n/**\n * Normalize an incoming ACP plan entry.\n * Extracts `activeForm` from Claude's `_meta.claudeCode` extension if present.\n */\nexport function normalizePlanEntry(entry: AcpPlanEntry): NormalizedPlanEntry {\n // Try to extract activeForm from _meta.claudeCode (if claude-acp sends it there)\n const meta = entry._meta as Record<string, unknown> | undefined;\n const claudeCode = meta?.claudeCode as Record<string, unknown> | undefined;\n const activeForm = claudeCode?.activeForm as string | undefined;\n\n // Also check if activeForm is directly on the entry (non-standard but possible)\n const directActiveForm = (entry as Record<string, unknown>).activeForm as string | undefined;\n\n return {\n content: entry.content,\n status: entry.status,\n priority: entry.priority,\n activeForm: activeForm ?? directActiveForm,\n };\n}\n\n// Model and mode info returned from agent session\nexport interface ModelInfo {\n modelId: string;\n name: string;\n}\n\nexport interface ModeInfo {\n id: string;\n name: string;\n description?: string;\n}\n\n// Permission types for WebSocket push\nexport interface PendingPermission {\n requestId: string;\n toolCallId?: string;\n title: string;\n options: Array<{ optionId: string; name: string; kind: string }>;\n}\n\n// amux events pushed via WebSocket subscription.\n// ACP blocking RPCs (requestPermission, prompt) need WebSocket notifications\n// so the UI can track state independently of RPC callbacks (e.g. after page reload).\n// Pattern: RPC start → notification, RPC complete → notification\nexport type AmuxEvent =\n | { amuxEvent: 'error'; message: string }\n | { amuxEvent: 'permission_request'; permission: PendingPermission }\n | { amuxEvent: 'permission_cleared' }\n | { amuxEvent: 'turn_start' }\n | { amuxEvent: 'turn_end' }\n | { amuxEvent: 'turn_cancelled' };\n\n// Combined type for session subscriptions\nexport type WindowUpdate = SessionUpdate | AmuxEvent;\n\n// Event emitted by amux when a session update occurs\nexport interface SessionEvent {\n sessionId: string;\n update: WindowUpdate;\n}\n\n// Type guard helpers\nexport function isSessionUpdate(update: WindowUpdate): update is SessionUpdate {\n return 'sessionUpdate' in update;\n}\n\nexport function isAmuxEvent(update: WindowUpdate): update is AmuxEvent {\n return 'amuxEvent' in update;\n}\n"],"mappings":";AAiDO,SAAS,mBAAmB,OAA0C;AAE3E,QAAM,OAAO,MAAM;AACnB,QAAM,aAAa,MAAM;AACzB,QAAM,aAAa,YAAY;AAG/B,QAAM,mBAAoB,MAAkC;AAE5D,SAAO;AAAA,IACL,SAAS,MAAM;AAAA,IACf,QAAQ,MAAM;AAAA,IACd,UAAU,MAAM;AAAA,IAChB,YAAY,cAAc;AAAA,EAC5B;AACF;AA4CO,SAAS,gBAAgB,QAA+C;AAC7E,SAAO,mBAAmB;AAC5B;AAEO,SAAS,YAAY,QAA2C;AACrE,SAAO,eAAe;AACxB;","names":[]}
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@bytespell/amux",
3
+ "version": "0.0.1",
4
+ "description": "Agent Multiplexer - headless server for agents",
5
+ "type": "module",
6
+ "bin": {
7
+ "amux": "dist/bin/cli.js"
8
+ },
9
+ "files": [
10
+ "dist/"
11
+ ],
12
+ "exports": {
13
+ ".": "./dist/src/server.js",
14
+ "./server": "./dist/src/server.js",
15
+ "./types": "./dist/src/types.js",
16
+ "./db": "./dist/src/db/index.js",
17
+ "./agents/manager": "./dist/src/agents/manager.js",
18
+ "./agents/eventStore": "./dist/src/agents/eventStore.js",
19
+ "./trpc/files": "./dist/src/trpc/files.js"
20
+ },
21
+ "scripts": {
22
+ "build": "tsup",
23
+ "dev": "tsup --watch",
24
+ "start": "node dist/index.js",
25
+ "db:generate": "drizzle-kit generate",
26
+ "db:migrate": "drizzle-kit migrate",
27
+ "db:push": "drizzle-kit push",
28
+ "db:studio": "drizzle-kit studio"
29
+ },
30
+ "dependencies": {
31
+ "@agentclientprotocol/sdk": "^0.12.0",
32
+ "@trpc/server": "^11.8.1",
33
+ "better-sqlite3": "^11.10.0",
34
+ "chokidar": "^5.0.0",
35
+ "commander": "^12.0.0",
36
+ "cors": "^2.8.5",
37
+ "drizzle-orm": "^0.45.1",
38
+ "execa": "^9.6.1",
39
+ "express": "^5.2.1",
40
+ "glob": "^13.0.0",
41
+ "tree-kill": "^1.2.2",
42
+ "ws": "^8.19.0",
43
+ "zod": "^3.25.76"
44
+ },
45
+ "devDependencies": {
46
+ "@types/better-sqlite3": "^7.6.13",
47
+ "@types/cors": "^2.8.19",
48
+ "@types/express": "^5.0.6",
49
+ "@types/node": "^24.10.1",
50
+ "@types/ws": "^8.18.1",
51
+ "drizzle-kit": "^0.31.4"
52
+ }
53
+ }