@vibegrid/mcp 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +2399 -0
- package/package.json +47 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2399 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
|
+
|
|
6
|
+
// ../server/src/config-manager.ts
|
|
7
|
+
import fs2 from "fs";
|
|
8
|
+
import path2 from "path";
|
|
9
|
+
import os2 from "os";
|
|
10
|
+
|
|
11
|
+
// ../shared/src/agent-defaults.ts
|
|
12
|
+
var DEFAULT_AGENT_COMMANDS = {
|
|
13
|
+
claude: {
|
|
14
|
+
command: "claude",
|
|
15
|
+
args: [],
|
|
16
|
+
headlessArgs: ["--dangerously-skip-permissions"]
|
|
17
|
+
},
|
|
18
|
+
copilot: {
|
|
19
|
+
command: "copilot",
|
|
20
|
+
args: [],
|
|
21
|
+
headlessArgs: ["--allow-all"]
|
|
22
|
+
},
|
|
23
|
+
codex: {
|
|
24
|
+
command: "codex",
|
|
25
|
+
args: [],
|
|
26
|
+
headlessArgs: ["-a", "never"]
|
|
27
|
+
},
|
|
28
|
+
opencode: { command: "opencode", args: [] },
|
|
29
|
+
gemini: {
|
|
30
|
+
command: "gemini",
|
|
31
|
+
args: [],
|
|
32
|
+
headlessArgs: ["-y"]
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// ../server/src/database.ts
|
|
37
|
+
import Database from "better-sqlite3";
|
|
38
|
+
import path from "path";
|
|
39
|
+
import os from "os";
|
|
40
|
+
import fs from "fs";
|
|
41
|
+
|
|
42
|
+
// ../server/src/logger.ts
|
|
43
|
+
import pino from "pino";
|
|
44
|
+
var log = pino({
|
|
45
|
+
level: process.env.VITEST ? "silent" : "info",
|
|
46
|
+
transport: process.env.NODE_ENV !== "production" ? { target: "pino/file", options: { destination: 2 } } : void 0
|
|
47
|
+
});
|
|
48
|
+
var logger_default = log;
|
|
49
|
+
|
|
50
|
+
// ../shared/src/types.ts
|
|
51
|
+
var DEFAULT_WORKSPACE = {
|
|
52
|
+
id: "personal",
|
|
53
|
+
name: "Personal",
|
|
54
|
+
icon: "User",
|
|
55
|
+
iconColor: "#6b7280",
|
|
56
|
+
order: 0
|
|
57
|
+
};
|
|
58
|
+
var IPC = {
|
|
59
|
+
TERMINAL_CREATE: "terminal:create",
|
|
60
|
+
TERMINAL_WRITE: "terminal:write",
|
|
61
|
+
TERMINAL_RESIZE: "terminal:resize",
|
|
62
|
+
TERMINAL_KILL: "terminal:kill",
|
|
63
|
+
TERMINAL_DATA: "terminal:data",
|
|
64
|
+
TERMINAL_EXIT: "terminal:exit",
|
|
65
|
+
CONFIG_LOAD: "config:load",
|
|
66
|
+
CONFIG_SAVE: "config:save",
|
|
67
|
+
CONFIG_CHANGED: "config:changed",
|
|
68
|
+
SESSIONS_GET_PREVIOUS: "sessions:getPrevious",
|
|
69
|
+
SESSIONS_CLEAR: "sessions:clear",
|
|
70
|
+
SESSIONS_GET_RECENT: "sessions:getRecent",
|
|
71
|
+
DIALOG_OPEN_DIRECTORY: "dialog:openDirectory",
|
|
72
|
+
IDE_DETECT: "ide:detect",
|
|
73
|
+
IDE_OPEN: "ide:open",
|
|
74
|
+
GIT_LIST_BRANCHES: "git:listBranches",
|
|
75
|
+
GIT_LIST_REMOTE_BRANCHES: "git:listRemoteBranches",
|
|
76
|
+
GIT_CREATE_WORKTREE: "git:createWorktree",
|
|
77
|
+
GIT_REMOVE_WORKTREE: "git:removeWorktree",
|
|
78
|
+
GIT_WORKTREE_DIRTY: "git:worktreeDirty",
|
|
79
|
+
GIT_LIST_WORKTREES: "git:listWorktrees",
|
|
80
|
+
WORKTREE_CONFIRM_CLEANUP: "worktree:confirmCleanup",
|
|
81
|
+
GIT_DIFF_STAT: "git:diffStat",
|
|
82
|
+
GIT_DIFF_FULL: "git:diffFull",
|
|
83
|
+
GIT_COMMIT: "git:commit",
|
|
84
|
+
GIT_PUSH: "git:push",
|
|
85
|
+
DIALOG_OPEN_FILE: "dialog:openFile",
|
|
86
|
+
SCHEDULER_EXECUTE: "scheduler:execute",
|
|
87
|
+
SCHEDULER_MISSED: "scheduler:missed",
|
|
88
|
+
SCHEDULER_GET_LOG: "scheduler:getLog",
|
|
89
|
+
SCHEDULER_GET_NEXT_RUN: "scheduler:getNextRun",
|
|
90
|
+
WORKFLOW_EXECUTION_COMPLETE: "workflow:executionComplete",
|
|
91
|
+
WINDOW_MINIMIZE: "window:minimize",
|
|
92
|
+
WINDOW_MAXIMIZE: "window:maximize",
|
|
93
|
+
WINDOW_CLOSE: "window:close",
|
|
94
|
+
WIDGET_STATUS_UPDATE: "widget:status-update",
|
|
95
|
+
WIDGET_FOCUS_TERMINAL: "widget:focus-terminal",
|
|
96
|
+
WIDGET_HIDE: "widget:hide",
|
|
97
|
+
WIDGET_TOGGLE: "widget:toggle",
|
|
98
|
+
WIDGET_RENDERER_STATUS: "widget:renderer-status",
|
|
99
|
+
WIDGET_SET_ENABLED: "widget:set-enabled",
|
|
100
|
+
WIDGET_PERMISSION_REQUEST: "widget:permission-request",
|
|
101
|
+
WIDGET_PERMISSION_RESPONSE: "widget:permission-response",
|
|
102
|
+
WIDGET_PERMISSION_CANCELLED: "widget:permission-cancelled",
|
|
103
|
+
SHELL_CREATE: "shell:create",
|
|
104
|
+
UPDATE_DOWNLOADED: "update:downloaded",
|
|
105
|
+
UPDATE_INSTALL: "update:install",
|
|
106
|
+
TASK_IMAGE_SAVE: "task:imageSave",
|
|
107
|
+
TASK_IMAGE_DELETE: "task:imageDelete",
|
|
108
|
+
TASK_IMAGE_GET_PATH: "task:imageGetPath",
|
|
109
|
+
TASK_IMAGE_CLEANUP: "task:imageCleanup",
|
|
110
|
+
DIALOG_OPEN_IMAGE: "dialog:openImage",
|
|
111
|
+
SESSION_ARCHIVE: "session:archive",
|
|
112
|
+
SESSION_UNARCHIVE: "session:unarchive",
|
|
113
|
+
SESSION_LIST_ARCHIVED: "session:listArchived",
|
|
114
|
+
HEADLESS_CREATE: "headless:create",
|
|
115
|
+
HEADLESS_KILL: "headless:kill",
|
|
116
|
+
HEADLESS_DATA: "headless:data",
|
|
117
|
+
HEADLESS_EXIT: "headless:exit",
|
|
118
|
+
SCRIPT_EXECUTE: "script:execute",
|
|
119
|
+
WORKFLOW_RUN_SAVE: "workflowRun:save",
|
|
120
|
+
WORKFLOW_RUN_LIST: "workflowRun:list",
|
|
121
|
+
WORKFLOW_RUN_LIST_BY_TASK: "workflowRun:listByTask",
|
|
122
|
+
AGENT_DETECT_INSTALLED: "agent:detectInstalled"
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
// ../server/src/database.ts
|
|
126
|
+
var CONFIG_DIR = path.join(os.homedir(), ".vibegrid");
|
|
127
|
+
var DB_PATH = path.join(CONFIG_DIR, "vibegrid.db");
|
|
128
|
+
var db = null;
|
|
129
|
+
function getDb() {
|
|
130
|
+
if (!db) throw new Error("Database not initialized. Call initDatabase() first.");
|
|
131
|
+
return db;
|
|
132
|
+
}
|
|
133
|
+
function initDatabase() {
|
|
134
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
135
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
|
|
136
|
+
}
|
|
137
|
+
try {
|
|
138
|
+
db = new Database(DB_PATH);
|
|
139
|
+
db.pragma("journal_mode = WAL");
|
|
140
|
+
db.pragma("foreign_keys = ON");
|
|
141
|
+
createSchema();
|
|
142
|
+
} catch (err) {
|
|
143
|
+
logger_default.error("[database] Failed to open database:", err);
|
|
144
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
145
|
+
const isCorrupt = /corrupt|notadb|malformed|not a database|file is not a database/i.test(
|
|
146
|
+
message
|
|
147
|
+
);
|
|
148
|
+
if (isCorrupt) {
|
|
149
|
+
logger_default.warn("[database] Database appears corrupt, attempting recovery...");
|
|
150
|
+
recoverCorruptDatabase();
|
|
151
|
+
} else {
|
|
152
|
+
throw err;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
function recoverCorruptDatabase() {
|
|
157
|
+
try {
|
|
158
|
+
db?.close();
|
|
159
|
+
} catch {
|
|
160
|
+
}
|
|
161
|
+
db = null;
|
|
162
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
163
|
+
const backupPath = `${DB_PATH}.corrupt-${timestamp}`;
|
|
164
|
+
try {
|
|
165
|
+
if (fs.existsSync(DB_PATH)) {
|
|
166
|
+
fs.copyFileSync(DB_PATH, backupPath);
|
|
167
|
+
logger_default.info(`[database] Backed up corrupt database to ${backupPath}`);
|
|
168
|
+
}
|
|
169
|
+
for (const suffix of ["", "-wal", "-shm"]) {
|
|
170
|
+
const file = DB_PATH + suffix;
|
|
171
|
+
if (fs.existsSync(file)) fs.unlinkSync(file);
|
|
172
|
+
}
|
|
173
|
+
} catch (backupErr) {
|
|
174
|
+
logger_default.error("[database] Failed to back up corrupt database:", backupErr);
|
|
175
|
+
}
|
|
176
|
+
try {
|
|
177
|
+
db = new Database(DB_PATH);
|
|
178
|
+
db.pragma("journal_mode = WAL");
|
|
179
|
+
db.pragma("foreign_keys = ON");
|
|
180
|
+
createSchema();
|
|
181
|
+
logger_default.info("[database] Successfully created fresh database after corruption recovery");
|
|
182
|
+
} catch (freshErr) {
|
|
183
|
+
logger_default.error("[database] Failed to create fresh database after corruption:", freshErr);
|
|
184
|
+
throw freshErr;
|
|
185
|
+
}
|
|
186
|
+
logger_default.warn(`[database] Database was corrupted and has been reset. Backup saved to: ${backupPath}`);
|
|
187
|
+
}
|
|
188
|
+
function closeDatabase() {
|
|
189
|
+
if (db) {
|
|
190
|
+
db.close();
|
|
191
|
+
db = null;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
function createSchema() {
|
|
195
|
+
const d = getDb();
|
|
196
|
+
const cols = d.prepare("PRAGMA table_info(workflows)").all();
|
|
197
|
+
if (cols.some((c) => c.name === "actions")) {
|
|
198
|
+
d.exec("ALTER TABLE workflows RENAME TO workflows_backup_old_format");
|
|
199
|
+
logger_default.warn("[database] migrated old-format workflows table to workflows_backup_old_format");
|
|
200
|
+
}
|
|
201
|
+
d.exec(`
|
|
202
|
+
CREATE TABLE IF NOT EXISTS schema_meta (
|
|
203
|
+
key TEXT PRIMARY KEY,
|
|
204
|
+
value TEXT NOT NULL
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
CREATE TABLE IF NOT EXISTS defaults (
|
|
208
|
+
key TEXT PRIMARY KEY,
|
|
209
|
+
value TEXT NOT NULL
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
CREATE TABLE IF NOT EXISTS projects (
|
|
213
|
+
name TEXT PRIMARY KEY,
|
|
214
|
+
path TEXT NOT NULL,
|
|
215
|
+
preferred_agents TEXT NOT NULL DEFAULT '[]',
|
|
216
|
+
icon TEXT,
|
|
217
|
+
icon_color TEXT,
|
|
218
|
+
host_ids TEXT
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
CREATE TABLE IF NOT EXISTS workflows (
|
|
222
|
+
id TEXT PRIMARY KEY,
|
|
223
|
+
name TEXT NOT NULL,
|
|
224
|
+
icon TEXT NOT NULL,
|
|
225
|
+
icon_color TEXT NOT NULL,
|
|
226
|
+
nodes TEXT NOT NULL DEFAULT '[]',
|
|
227
|
+
edges TEXT NOT NULL DEFAULT '[]',
|
|
228
|
+
enabled INTEGER NOT NULL DEFAULT 1,
|
|
229
|
+
last_run_at TEXT,
|
|
230
|
+
last_run_status TEXT,
|
|
231
|
+
stagger_delay_ms INTEGER
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
CREATE TABLE IF NOT EXISTS agent_commands (
|
|
235
|
+
agent_type TEXT PRIMARY KEY,
|
|
236
|
+
command TEXT NOT NULL,
|
|
237
|
+
args TEXT NOT NULL DEFAULT '[]',
|
|
238
|
+
fallback_command TEXT,
|
|
239
|
+
fallback_args TEXT
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
CREATE TABLE IF NOT EXISTS remote_hosts (
|
|
243
|
+
id TEXT PRIMARY KEY,
|
|
244
|
+
label TEXT NOT NULL,
|
|
245
|
+
hostname TEXT NOT NULL,
|
|
246
|
+
user TEXT NOT NULL,
|
|
247
|
+
port INTEGER NOT NULL DEFAULT 22,
|
|
248
|
+
ssh_key_path TEXT,
|
|
249
|
+
ssh_options TEXT
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
CREATE TABLE IF NOT EXISTS tasks (
|
|
253
|
+
id TEXT PRIMARY KEY,
|
|
254
|
+
project_name TEXT NOT NULL,
|
|
255
|
+
title TEXT NOT NULL,
|
|
256
|
+
description TEXT NOT NULL DEFAULT '',
|
|
257
|
+
status TEXT NOT NULL DEFAULT 'todo',
|
|
258
|
+
"order" INTEGER NOT NULL DEFAULT 0,
|
|
259
|
+
assigned_session_id TEXT,
|
|
260
|
+
assigned_agent TEXT,
|
|
261
|
+
agent_session_id TEXT,
|
|
262
|
+
branch TEXT,
|
|
263
|
+
use_worktree INTEGER DEFAULT 0,
|
|
264
|
+
created_at TEXT NOT NULL,
|
|
265
|
+
updated_at TEXT NOT NULL,
|
|
266
|
+
completed_at TEXT
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
270
|
+
id TEXT PRIMARY KEY,
|
|
271
|
+
agent_type TEXT NOT NULL,
|
|
272
|
+
project_name TEXT NOT NULL,
|
|
273
|
+
project_path TEXT NOT NULL,
|
|
274
|
+
status TEXT NOT NULL,
|
|
275
|
+
created_at INTEGER NOT NULL,
|
|
276
|
+
pid INTEGER NOT NULL,
|
|
277
|
+
display_name TEXT,
|
|
278
|
+
branch TEXT,
|
|
279
|
+
worktree_path TEXT,
|
|
280
|
+
is_worktree INTEGER DEFAULT 0,
|
|
281
|
+
remote_host_id TEXT,
|
|
282
|
+
remote_host_label TEXT,
|
|
283
|
+
hook_session_id TEXT,
|
|
284
|
+
status_source TEXT,
|
|
285
|
+
saved_at INTEGER
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
CREATE TABLE IF NOT EXISTS schedule_log (
|
|
289
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
290
|
+
workflow_id TEXT NOT NULL,
|
|
291
|
+
workflow_name TEXT NOT NULL,
|
|
292
|
+
executed_at TEXT NOT NULL,
|
|
293
|
+
status TEXT NOT NULL,
|
|
294
|
+
sessions_launched INTEGER NOT NULL DEFAULT 0,
|
|
295
|
+
error TEXT
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
CREATE INDEX IF NOT EXISTS idx_schedule_log_workflow_id ON schedule_log(workflow_id);
|
|
299
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_project ON tasks(project_name, status);
|
|
300
|
+
|
|
301
|
+
CREATE TABLE IF NOT EXISTS archived_sessions (
|
|
302
|
+
id TEXT PRIMARY KEY,
|
|
303
|
+
agent_type TEXT NOT NULL,
|
|
304
|
+
project_name TEXT NOT NULL,
|
|
305
|
+
project_path TEXT NOT NULL,
|
|
306
|
+
display_name TEXT,
|
|
307
|
+
branch TEXT,
|
|
308
|
+
agent_session_id TEXT,
|
|
309
|
+
archived_at INTEGER NOT NULL
|
|
310
|
+
);
|
|
311
|
+
|
|
312
|
+
CREATE TABLE IF NOT EXISTS workspaces (
|
|
313
|
+
id TEXT PRIMARY KEY,
|
|
314
|
+
name TEXT NOT NULL,
|
|
315
|
+
icon TEXT,
|
|
316
|
+
icon_color TEXT,
|
|
317
|
+
"order" INTEGER NOT NULL DEFAULT 0
|
|
318
|
+
);
|
|
319
|
+
|
|
320
|
+
CREATE TABLE IF NOT EXISTS workflow_runs (
|
|
321
|
+
id TEXT PRIMARY KEY,
|
|
322
|
+
workflow_id TEXT NOT NULL,
|
|
323
|
+
started_at TEXT NOT NULL,
|
|
324
|
+
completed_at TEXT,
|
|
325
|
+
status TEXT NOT NULL DEFAULT 'running',
|
|
326
|
+
trigger_task_id TEXT
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
CREATE TABLE IF NOT EXISTS workflow_run_nodes (
|
|
330
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
331
|
+
run_id TEXT NOT NULL,
|
|
332
|
+
node_id TEXT NOT NULL,
|
|
333
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
334
|
+
started_at TEXT,
|
|
335
|
+
completed_at TEXT,
|
|
336
|
+
session_id TEXT,
|
|
337
|
+
error TEXT,
|
|
338
|
+
logs TEXT,
|
|
339
|
+
task_id TEXT,
|
|
340
|
+
agent_session_id TEXT,
|
|
341
|
+
FOREIGN KEY (run_id) REFERENCES workflow_runs(id) ON DELETE CASCADE
|
|
342
|
+
);
|
|
343
|
+
|
|
344
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_runs_workflow ON workflow_runs(workflow_id);
|
|
345
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_runs_task ON workflow_runs(trigger_task_id);
|
|
346
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_run_nodes_run ON workflow_run_nodes(run_id);
|
|
347
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_run_nodes_task ON workflow_run_nodes(task_id);
|
|
348
|
+
`);
|
|
349
|
+
migrateSchema(d);
|
|
350
|
+
}
|
|
351
|
+
function migrateSchema(d) {
|
|
352
|
+
const row = d.prepare("SELECT value FROM schema_meta WHERE key = 'schema_version'").get();
|
|
353
|
+
const version = row ? parseInt(row.value, 10) : 0;
|
|
354
|
+
if (version < 1) {
|
|
355
|
+
d.transaction(() => {
|
|
356
|
+
const projectCols = d.prepare("PRAGMA table_info(projects)").all();
|
|
357
|
+
if (!projectCols.some((c) => c.name === "workspace_id")) {
|
|
358
|
+
d.exec("ALTER TABLE projects ADD COLUMN workspace_id TEXT NOT NULL DEFAULT 'personal'");
|
|
359
|
+
}
|
|
360
|
+
const workflowCols = d.prepare("PRAGMA table_info(workflows)").all();
|
|
361
|
+
if (!workflowCols.some((c) => c.name === "workspace_id")) {
|
|
362
|
+
d.exec("ALTER TABLE workflows ADD COLUMN workspace_id TEXT NOT NULL DEFAULT 'personal'");
|
|
363
|
+
}
|
|
364
|
+
d.prepare(
|
|
365
|
+
`INSERT OR IGNORE INTO workspaces (id, name, icon, icon_color, "order") VALUES (?, ?, ?, ?, ?)`
|
|
366
|
+
).run(
|
|
367
|
+
DEFAULT_WORKSPACE.id,
|
|
368
|
+
DEFAULT_WORKSPACE.name,
|
|
369
|
+
DEFAULT_WORKSPACE.icon ?? null,
|
|
370
|
+
DEFAULT_WORKSPACE.iconColor ?? null,
|
|
371
|
+
DEFAULT_WORKSPACE.order
|
|
372
|
+
);
|
|
373
|
+
d.prepare(
|
|
374
|
+
"INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('schema_version', '1')"
|
|
375
|
+
).run();
|
|
376
|
+
})();
|
|
377
|
+
logger_default.info("[database] migrated schema to version 1 (workspaces)");
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
function loadConfig() {
|
|
381
|
+
const d = getDb();
|
|
382
|
+
const defaults = loadDefaults(d);
|
|
383
|
+
const projects = loadProjects(d);
|
|
384
|
+
const agentCommands = loadAgentCommands(d);
|
|
385
|
+
const workflows = loadWorkflows(d);
|
|
386
|
+
const remoteHosts = loadRemoteHosts(d);
|
|
387
|
+
const tasks = loadTasks(d);
|
|
388
|
+
const workspaces = loadWorkspaces(d);
|
|
389
|
+
return {
|
|
390
|
+
version: 1,
|
|
391
|
+
defaults,
|
|
392
|
+
projects,
|
|
393
|
+
agentCommands: Object.keys(agentCommands).length > 0 ? agentCommands : { ...DEFAULT_AGENT_COMMANDS },
|
|
394
|
+
workflows,
|
|
395
|
+
remoteHosts,
|
|
396
|
+
tasks,
|
|
397
|
+
workspaces
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
function loadDefaults(d) {
|
|
401
|
+
const rows = d.prepare("SELECT key, value FROM defaults").all();
|
|
402
|
+
const map = {};
|
|
403
|
+
for (const row of rows) {
|
|
404
|
+
map[row.key] = JSON.parse(row.value);
|
|
405
|
+
}
|
|
406
|
+
return {
|
|
407
|
+
shell: map.shell ?? (process.platform === "win32" ? process.env.COMSPEC || "powershell.exe" : process.env.SHELL || "/bin/zsh"),
|
|
408
|
+
fontSize: map.fontSize ?? 13,
|
|
409
|
+
theme: map.theme ?? "dark",
|
|
410
|
+
...map.rowHeight !== void 0 && { rowHeight: map.rowHeight },
|
|
411
|
+
...map.defaultAgent !== void 0 && { defaultAgent: map.defaultAgent },
|
|
412
|
+
...map.notifications !== void 0 && {
|
|
413
|
+
notifications: map.notifications
|
|
414
|
+
},
|
|
415
|
+
...map.hasSeenOnboarding !== void 0 && {
|
|
416
|
+
hasSeenOnboarding: map.hasSeenOnboarding
|
|
417
|
+
},
|
|
418
|
+
...map.reopenSessions !== void 0 && { reopenSessions: map.reopenSessions },
|
|
419
|
+
...map.widgetEnabled !== void 0 && { widgetEnabled: map.widgetEnabled },
|
|
420
|
+
...map.taskViewMode !== void 0 && {
|
|
421
|
+
taskViewMode: map.taskViewMode
|
|
422
|
+
},
|
|
423
|
+
...map.activeWorkspace !== void 0 && {
|
|
424
|
+
activeWorkspace: map.activeWorkspace
|
|
425
|
+
}
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
function loadProjects(d) {
|
|
429
|
+
const rows = d.prepare("SELECT * FROM projects").all();
|
|
430
|
+
return rows.map(rowToProject);
|
|
431
|
+
}
|
|
432
|
+
function loadWorkflows(d) {
|
|
433
|
+
const rows = d.prepare("SELECT * FROM workflows").all();
|
|
434
|
+
return rows.map(rowToWorkflow);
|
|
435
|
+
}
|
|
436
|
+
function loadAgentCommands(d) {
|
|
437
|
+
const rows = d.prepare("SELECT * FROM agent_commands").all();
|
|
438
|
+
const result = {};
|
|
439
|
+
for (const r of rows) {
|
|
440
|
+
result[r.agent_type] = {
|
|
441
|
+
command: r.command,
|
|
442
|
+
args: JSON.parse(r.args),
|
|
443
|
+
...r.fallback_command != null && { fallbackCommand: r.fallback_command },
|
|
444
|
+
...r.fallback_args != null && { fallbackArgs: JSON.parse(r.fallback_args) }
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
return result;
|
|
448
|
+
}
|
|
449
|
+
function loadRemoteHosts(d) {
|
|
450
|
+
const rows = d.prepare("SELECT * FROM remote_hosts").all();
|
|
451
|
+
return rows.map((r) => ({
|
|
452
|
+
id: r.id,
|
|
453
|
+
label: r.label,
|
|
454
|
+
hostname: r.hostname,
|
|
455
|
+
user: r.user,
|
|
456
|
+
port: r.port,
|
|
457
|
+
...r.ssh_key_path != null && { sshKeyPath: r.ssh_key_path },
|
|
458
|
+
...r.ssh_options != null && { sshOptions: r.ssh_options }
|
|
459
|
+
}));
|
|
460
|
+
}
|
|
461
|
+
function loadTasks(d) {
|
|
462
|
+
const rows = d.prepare('SELECT * FROM tasks ORDER BY "order"').all();
|
|
463
|
+
return rows.map(rowToTask);
|
|
464
|
+
}
|
|
465
|
+
function loadWorkspaces(d) {
|
|
466
|
+
const rows = d.prepare('SELECT * FROM workspaces ORDER BY "order"').all();
|
|
467
|
+
return rows.map(rowToWorkspace);
|
|
468
|
+
}
|
|
469
|
+
function saveConfig(config) {
|
|
470
|
+
const d = getDb();
|
|
471
|
+
const run = d.transaction(() => {
|
|
472
|
+
d.prepare("DELETE FROM defaults").run();
|
|
473
|
+
const insertDefault = d.prepare("INSERT INTO defaults (key, value) VALUES (?, ?)");
|
|
474
|
+
for (const [key, value] of Object.entries(config.defaults)) {
|
|
475
|
+
if (value !== void 0) {
|
|
476
|
+
insertDefault.run(key, JSON.stringify(value));
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
d.prepare("DELETE FROM projects").run();
|
|
480
|
+
const insertProject = d.prepare(
|
|
481
|
+
"INSERT INTO projects (name, path, preferred_agents, icon, icon_color, host_ids, workspace_id) VALUES (?, ?, ?, ?, ?, ?, ?)"
|
|
482
|
+
);
|
|
483
|
+
for (const p of config.projects) {
|
|
484
|
+
insertProject.run(
|
|
485
|
+
p.name,
|
|
486
|
+
p.path,
|
|
487
|
+
JSON.stringify(p.preferredAgents),
|
|
488
|
+
p.icon ?? null,
|
|
489
|
+
p.iconColor ?? null,
|
|
490
|
+
p.hostIds ? JSON.stringify(p.hostIds) : null,
|
|
491
|
+
p.workspaceId ?? "personal"
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
d.prepare("DELETE FROM workflows").run();
|
|
495
|
+
const insertWorkflow = d.prepare(
|
|
496
|
+
`INSERT INTO workflows (id, name, icon, icon_color, nodes, edges, enabled, last_run_at, last_run_status, stagger_delay_ms, workspace_id)
|
|
497
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
498
|
+
);
|
|
499
|
+
for (const w of config.workflows ?? []) {
|
|
500
|
+
insertWorkflow.run(
|
|
501
|
+
w.id,
|
|
502
|
+
w.name,
|
|
503
|
+
w.icon,
|
|
504
|
+
w.iconColor,
|
|
505
|
+
JSON.stringify(w.nodes),
|
|
506
|
+
JSON.stringify(w.edges),
|
|
507
|
+
w.enabled ? 1 : 0,
|
|
508
|
+
w.lastRunAt ?? null,
|
|
509
|
+
w.lastRunStatus ?? null,
|
|
510
|
+
w.staggerDelayMs ?? null,
|
|
511
|
+
w.workspaceId ?? "personal"
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
d.prepare("DELETE FROM agent_commands").run();
|
|
515
|
+
const insertAgent = d.prepare(
|
|
516
|
+
"INSERT INTO agent_commands (agent_type, command, args, fallback_command, fallback_args) VALUES (?, ?, ?, ?, ?)"
|
|
517
|
+
);
|
|
518
|
+
if (config.agentCommands) {
|
|
519
|
+
for (const [agentType, cmd] of Object.entries(config.agentCommands)) {
|
|
520
|
+
if (cmd) {
|
|
521
|
+
insertAgent.run(
|
|
522
|
+
agentType,
|
|
523
|
+
cmd.command,
|
|
524
|
+
JSON.stringify(cmd.args),
|
|
525
|
+
cmd.fallbackCommand ?? null,
|
|
526
|
+
cmd.fallbackArgs ? JSON.stringify(cmd.fallbackArgs) : null
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
d.prepare("DELETE FROM remote_hosts").run();
|
|
532
|
+
const insertHost = d.prepare(
|
|
533
|
+
"INSERT INTO remote_hosts (id, label, hostname, user, port, ssh_key_path, ssh_options) VALUES (?, ?, ?, ?, ?, ?, ?)"
|
|
534
|
+
);
|
|
535
|
+
for (const h of config.remoteHosts ?? []) {
|
|
536
|
+
insertHost.run(
|
|
537
|
+
h.id,
|
|
538
|
+
h.label,
|
|
539
|
+
h.hostname,
|
|
540
|
+
h.user,
|
|
541
|
+
h.port,
|
|
542
|
+
h.sshKeyPath ?? null,
|
|
543
|
+
h.sshOptions ?? null
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
d.prepare("DELETE FROM tasks").run();
|
|
547
|
+
const insertTask = d.prepare(
|
|
548
|
+
`INSERT INTO tasks (id, project_name, title, description, status, "order", assigned_session_id, assigned_agent, agent_session_id, branch, use_worktree, created_at, updated_at, completed_at)
|
|
549
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
550
|
+
);
|
|
551
|
+
for (const t of config.tasks ?? []) {
|
|
552
|
+
insertTask.run(
|
|
553
|
+
t.id,
|
|
554
|
+
t.projectName,
|
|
555
|
+
t.title,
|
|
556
|
+
t.description,
|
|
557
|
+
t.status,
|
|
558
|
+
t.order,
|
|
559
|
+
t.assignedSessionId ?? null,
|
|
560
|
+
t.assignedAgent ?? null,
|
|
561
|
+
t.agentSessionId ?? null,
|
|
562
|
+
t.branch ?? null,
|
|
563
|
+
t.useWorktree ? 1 : 0,
|
|
564
|
+
t.createdAt,
|
|
565
|
+
t.updatedAt,
|
|
566
|
+
t.completedAt ?? null
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
d.prepare("DELETE FROM workspaces").run();
|
|
570
|
+
const insertWorkspace = d.prepare(
|
|
571
|
+
`INSERT INTO workspaces (id, name, icon, icon_color, "order") VALUES (?, ?, ?, ?, ?)`
|
|
572
|
+
);
|
|
573
|
+
for (const ws of config.workspaces ?? [DEFAULT_WORKSPACE]) {
|
|
574
|
+
insertWorkspace.run(ws.id, ws.name, ws.icon ?? null, ws.iconColor ?? null, ws.order);
|
|
575
|
+
}
|
|
576
|
+
});
|
|
577
|
+
run();
|
|
578
|
+
}
|
|
579
|
+
function dbListTasks(projectName, status) {
|
|
580
|
+
const d = getDb();
|
|
581
|
+
let sql = "SELECT * FROM tasks";
|
|
582
|
+
const params = [];
|
|
583
|
+
const clauses = [];
|
|
584
|
+
if (projectName) {
|
|
585
|
+
clauses.push("project_name = ?");
|
|
586
|
+
params.push(projectName);
|
|
587
|
+
}
|
|
588
|
+
if (status) {
|
|
589
|
+
clauses.push("status = ?");
|
|
590
|
+
params.push(status);
|
|
591
|
+
}
|
|
592
|
+
if (clauses.length) sql += " WHERE " + clauses.join(" AND ");
|
|
593
|
+
sql += ' ORDER BY "order"';
|
|
594
|
+
const rows = d.prepare(sql).all(...params);
|
|
595
|
+
return rows.map(rowToTask);
|
|
596
|
+
}
|
|
597
|
+
function dbGetTask(id) {
|
|
598
|
+
const row = getDb().prepare("SELECT * FROM tasks WHERE id = ?").get(id);
|
|
599
|
+
return row ? rowToTask(row) : null;
|
|
600
|
+
}
|
|
601
|
+
function dbInsertTask(task) {
|
|
602
|
+
getDb().prepare(
|
|
603
|
+
`INSERT INTO tasks (id, project_name, title, description, status, "order", assigned_session_id, assigned_agent, agent_session_id, branch, use_worktree, created_at, updated_at, completed_at)
|
|
604
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
605
|
+
).run(
|
|
606
|
+
task.id,
|
|
607
|
+
task.projectName,
|
|
608
|
+
task.title,
|
|
609
|
+
task.description,
|
|
610
|
+
task.status,
|
|
611
|
+
task.order,
|
|
612
|
+
task.assignedSessionId ?? null,
|
|
613
|
+
task.assignedAgent ?? null,
|
|
614
|
+
task.agentSessionId ?? null,
|
|
615
|
+
task.branch ?? null,
|
|
616
|
+
task.useWorktree ? 1 : 0,
|
|
617
|
+
task.createdAt,
|
|
618
|
+
task.updatedAt,
|
|
619
|
+
task.completedAt ?? null
|
|
620
|
+
);
|
|
621
|
+
}
|
|
622
|
+
function dbUpdateTask(id, updates) {
|
|
623
|
+
const sets = [];
|
|
624
|
+
const params = [];
|
|
625
|
+
if (updates.title !== void 0) {
|
|
626
|
+
sets.push("title = ?");
|
|
627
|
+
params.push(updates.title);
|
|
628
|
+
}
|
|
629
|
+
if (updates.description !== void 0) {
|
|
630
|
+
sets.push("description = ?");
|
|
631
|
+
params.push(updates.description);
|
|
632
|
+
}
|
|
633
|
+
if (updates.status !== void 0) {
|
|
634
|
+
sets.push("status = ?");
|
|
635
|
+
params.push(updates.status);
|
|
636
|
+
}
|
|
637
|
+
if (updates.order !== void 0) {
|
|
638
|
+
sets.push('"order" = ?');
|
|
639
|
+
params.push(updates.order);
|
|
640
|
+
}
|
|
641
|
+
if (updates.branch !== void 0) {
|
|
642
|
+
sets.push("branch = ?");
|
|
643
|
+
params.push(updates.branch);
|
|
644
|
+
}
|
|
645
|
+
if (updates.useWorktree !== void 0) {
|
|
646
|
+
sets.push("use_worktree = ?");
|
|
647
|
+
params.push(updates.useWorktree ? 1 : 0);
|
|
648
|
+
}
|
|
649
|
+
if (updates.assignedAgent !== void 0) {
|
|
650
|
+
sets.push("assigned_agent = ?");
|
|
651
|
+
params.push(updates.assignedAgent);
|
|
652
|
+
}
|
|
653
|
+
if (updates.assignedSessionId !== void 0) {
|
|
654
|
+
sets.push("assigned_session_id = ?");
|
|
655
|
+
params.push(updates.assignedSessionId);
|
|
656
|
+
}
|
|
657
|
+
if (updates.agentSessionId !== void 0) {
|
|
658
|
+
sets.push("agent_session_id = ?");
|
|
659
|
+
params.push(updates.agentSessionId);
|
|
660
|
+
}
|
|
661
|
+
if (updates.updatedAt !== void 0) {
|
|
662
|
+
sets.push("updated_at = ?");
|
|
663
|
+
params.push(updates.updatedAt);
|
|
664
|
+
}
|
|
665
|
+
if ("completedAt" in updates) {
|
|
666
|
+
sets.push("completed_at = ?");
|
|
667
|
+
params.push(updates.completedAt ?? null);
|
|
668
|
+
}
|
|
669
|
+
if (sets.length === 0) return;
|
|
670
|
+
params.push(id);
|
|
671
|
+
getDb().prepare(`UPDATE tasks SET ${sets.join(", ")} WHERE id = ?`).run(...params);
|
|
672
|
+
}
|
|
673
|
+
function dbDeleteTask(id) {
|
|
674
|
+
getDb().prepare("DELETE FROM tasks WHERE id = ?").run(id);
|
|
675
|
+
}
|
|
676
|
+
function dbGetMaxTaskOrder(projectName) {
|
|
677
|
+
const row = getDb().prepare('SELECT MAX("order") as m FROM tasks WHERE project_name = ?').get(projectName);
|
|
678
|
+
return row.m ?? -1;
|
|
679
|
+
}
|
|
680
|
+
function dbListProjects() {
|
|
681
|
+
const rows = getDb().prepare("SELECT * FROM projects").all();
|
|
682
|
+
return rows.map(rowToProject);
|
|
683
|
+
}
|
|
684
|
+
function dbGetProject(name) {
|
|
685
|
+
const row = getDb().prepare("SELECT * FROM projects WHERE name = ?").get(name);
|
|
686
|
+
return row ? rowToProject(row) : null;
|
|
687
|
+
}
|
|
688
|
+
function dbInsertProject(project) {
|
|
689
|
+
getDb().prepare(
|
|
690
|
+
"INSERT INTO projects (name, path, preferred_agents, icon, icon_color, host_ids, workspace_id) VALUES (?, ?, ?, ?, ?, ?, ?)"
|
|
691
|
+
).run(
|
|
692
|
+
project.name,
|
|
693
|
+
project.path,
|
|
694
|
+
JSON.stringify(project.preferredAgents),
|
|
695
|
+
project.icon ?? null,
|
|
696
|
+
project.iconColor ?? null,
|
|
697
|
+
project.hostIds ? JSON.stringify(project.hostIds) : null,
|
|
698
|
+
project.workspaceId ?? "personal"
|
|
699
|
+
);
|
|
700
|
+
}
|
|
701
|
+
function dbUpdateProject(name, updates) {
|
|
702
|
+
const sets = [];
|
|
703
|
+
const params = [];
|
|
704
|
+
if (updates.path !== void 0) {
|
|
705
|
+
sets.push("path = ?");
|
|
706
|
+
params.push(updates.path);
|
|
707
|
+
}
|
|
708
|
+
if (updates.preferredAgents !== void 0) {
|
|
709
|
+
sets.push("preferred_agents = ?");
|
|
710
|
+
params.push(JSON.stringify(updates.preferredAgents));
|
|
711
|
+
}
|
|
712
|
+
if (updates.icon !== void 0) {
|
|
713
|
+
sets.push("icon = ?");
|
|
714
|
+
params.push(updates.icon);
|
|
715
|
+
}
|
|
716
|
+
if (updates.iconColor !== void 0) {
|
|
717
|
+
sets.push("icon_color = ?");
|
|
718
|
+
params.push(updates.iconColor);
|
|
719
|
+
}
|
|
720
|
+
if (updates.hostIds !== void 0) {
|
|
721
|
+
sets.push("host_ids = ?");
|
|
722
|
+
params.push(JSON.stringify(updates.hostIds));
|
|
723
|
+
}
|
|
724
|
+
if (updates.workspaceId !== void 0) {
|
|
725
|
+
sets.push("workspace_id = ?");
|
|
726
|
+
params.push(updates.workspaceId);
|
|
727
|
+
}
|
|
728
|
+
if (sets.length === 0) return;
|
|
729
|
+
params.push(name);
|
|
730
|
+
getDb().prepare(`UPDATE projects SET ${sets.join(", ")} WHERE name = ?`).run(...params);
|
|
731
|
+
}
|
|
732
|
+
function dbDeleteProject(name) {
|
|
733
|
+
const d = getDb();
|
|
734
|
+
d.transaction(() => {
|
|
735
|
+
d.prepare("DELETE FROM tasks WHERE project_name = ?").run(name);
|
|
736
|
+
d.prepare("DELETE FROM projects WHERE name = ?").run(name);
|
|
737
|
+
})();
|
|
738
|
+
}
|
|
739
|
+
function dbListWorkflows() {
|
|
740
|
+
const rows = getDb().prepare("SELECT * FROM workflows").all();
|
|
741
|
+
return rows.map(rowToWorkflow);
|
|
742
|
+
}
|
|
743
|
+
function dbInsertWorkflow(workflow) {
|
|
744
|
+
getDb().prepare(
|
|
745
|
+
`INSERT INTO workflows (id, name, icon, icon_color, nodes, edges, enabled, last_run_at, last_run_status, stagger_delay_ms, workspace_id)
|
|
746
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
747
|
+
).run(
|
|
748
|
+
workflow.id,
|
|
749
|
+
workflow.name,
|
|
750
|
+
workflow.icon,
|
|
751
|
+
workflow.iconColor,
|
|
752
|
+
JSON.stringify(workflow.nodes),
|
|
753
|
+
JSON.stringify(workflow.edges),
|
|
754
|
+
workflow.enabled ? 1 : 0,
|
|
755
|
+
workflow.lastRunAt ?? null,
|
|
756
|
+
workflow.lastRunStatus ?? null,
|
|
757
|
+
workflow.staggerDelayMs ?? null,
|
|
758
|
+
workflow.workspaceId ?? "personal"
|
|
759
|
+
);
|
|
760
|
+
}
|
|
761
|
+
function dbUpdateWorkflow(id, updates) {
|
|
762
|
+
const sets = [];
|
|
763
|
+
const params = [];
|
|
764
|
+
if (updates.name !== void 0) {
|
|
765
|
+
sets.push("name = ?");
|
|
766
|
+
params.push(updates.name);
|
|
767
|
+
}
|
|
768
|
+
if (updates.nodes !== void 0) {
|
|
769
|
+
sets.push("nodes = ?");
|
|
770
|
+
params.push(JSON.stringify(updates.nodes));
|
|
771
|
+
}
|
|
772
|
+
if (updates.edges !== void 0) {
|
|
773
|
+
sets.push("edges = ?");
|
|
774
|
+
params.push(JSON.stringify(updates.edges));
|
|
775
|
+
}
|
|
776
|
+
if (updates.icon !== void 0) {
|
|
777
|
+
sets.push("icon = ?");
|
|
778
|
+
params.push(updates.icon);
|
|
779
|
+
}
|
|
780
|
+
if (updates.iconColor !== void 0) {
|
|
781
|
+
sets.push("icon_color = ?");
|
|
782
|
+
params.push(updates.iconColor);
|
|
783
|
+
}
|
|
784
|
+
if (updates.enabled !== void 0) {
|
|
785
|
+
sets.push("enabled = ?");
|
|
786
|
+
params.push(updates.enabled ? 1 : 0);
|
|
787
|
+
}
|
|
788
|
+
if (updates.staggerDelayMs !== void 0) {
|
|
789
|
+
sets.push("stagger_delay_ms = ?");
|
|
790
|
+
params.push(updates.staggerDelayMs);
|
|
791
|
+
}
|
|
792
|
+
if (updates.workspaceId !== void 0) {
|
|
793
|
+
sets.push("workspace_id = ?");
|
|
794
|
+
params.push(updates.workspaceId);
|
|
795
|
+
}
|
|
796
|
+
if (sets.length === 0) return;
|
|
797
|
+
params.push(id);
|
|
798
|
+
getDb().prepare(`UPDATE workflows SET ${sets.join(", ")} WHERE id = ?`).run(...params);
|
|
799
|
+
}
|
|
800
|
+
function dbDeleteWorkflow(id) {
|
|
801
|
+
getDb().prepare("DELETE FROM workflows WHERE id = ?").run(id);
|
|
802
|
+
}
|
|
803
|
+
function rowToTask(r) {
|
|
804
|
+
return {
|
|
805
|
+
id: r.id,
|
|
806
|
+
projectName: r.project_name,
|
|
807
|
+
title: r.title,
|
|
808
|
+
description: r.description,
|
|
809
|
+
status: r.status,
|
|
810
|
+
order: r.order,
|
|
811
|
+
...r.assigned_session_id != null && { assignedSessionId: r.assigned_session_id },
|
|
812
|
+
...r.assigned_agent != null && { assignedAgent: r.assigned_agent },
|
|
813
|
+
...r.agent_session_id != null && { agentSessionId: r.agent_session_id },
|
|
814
|
+
...r.branch != null && { branch: r.branch },
|
|
815
|
+
...r.use_worktree != null && r.use_worktree !== 0 && { useWorktree: true },
|
|
816
|
+
createdAt: r.created_at,
|
|
817
|
+
updatedAt: r.updated_at,
|
|
818
|
+
...r.completed_at != null && { completedAt: r.completed_at }
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
function rowToProject(r) {
|
|
822
|
+
return {
|
|
823
|
+
name: r.name,
|
|
824
|
+
path: r.path,
|
|
825
|
+
preferredAgents: JSON.parse(r.preferred_agents),
|
|
826
|
+
...r.icon != null && { icon: r.icon },
|
|
827
|
+
...r.icon_color != null && { iconColor: r.icon_color },
|
|
828
|
+
...r.host_ids != null && { hostIds: JSON.parse(r.host_ids) },
|
|
829
|
+
workspaceId: r.workspace_id ?? "personal"
|
|
830
|
+
};
|
|
831
|
+
}
|
|
832
|
+
function rowToWorkflow(r) {
|
|
833
|
+
return {
|
|
834
|
+
id: r.id,
|
|
835
|
+
name: r.name,
|
|
836
|
+
icon: r.icon,
|
|
837
|
+
iconColor: r.icon_color,
|
|
838
|
+
nodes: JSON.parse(r.nodes),
|
|
839
|
+
edges: JSON.parse(r.edges),
|
|
840
|
+
enabled: r.enabled === 1,
|
|
841
|
+
...r.last_run_at != null && { lastRunAt: r.last_run_at },
|
|
842
|
+
...r.last_run_status != null && { lastRunStatus: r.last_run_status },
|
|
843
|
+
...r.stagger_delay_ms != null && { staggerDelayMs: r.stagger_delay_ms },
|
|
844
|
+
workspaceId: r.workspace_id ?? "personal"
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
function rowToWorkspace(r) {
|
|
848
|
+
return {
|
|
849
|
+
id: r.id,
|
|
850
|
+
name: r.name,
|
|
851
|
+
...r.icon != null && { icon: r.icon },
|
|
852
|
+
...r.icon_color != null && { iconColor: r.icon_color },
|
|
853
|
+
order: r.order
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
// ../server/src/config-manager.ts
|
|
858
|
+
var DB_DIR = path2.join(os2.homedir(), ".vibegrid");
|
|
859
|
+
var ConfigManager = class {
|
|
860
|
+
changeCallbacks = [];
|
|
861
|
+
dbWatcher = null;
|
|
862
|
+
debounceTimer = null;
|
|
863
|
+
init() {
|
|
864
|
+
initDatabase();
|
|
865
|
+
}
|
|
866
|
+
close() {
|
|
867
|
+
this.stopWatchingDb();
|
|
868
|
+
closeDatabase();
|
|
869
|
+
}
|
|
870
|
+
loadConfig() {
|
|
871
|
+
try {
|
|
872
|
+
return loadConfig();
|
|
873
|
+
} catch (err) {
|
|
874
|
+
logger_default.error("[config-manager] loadConfig failed, returning defaults:", err);
|
|
875
|
+
return {
|
|
876
|
+
version: 1,
|
|
877
|
+
defaults: {
|
|
878
|
+
shell: process.platform === "win32" ? process.env.COMSPEC || "powershell.exe" : process.env.SHELL || "/bin/zsh",
|
|
879
|
+
fontSize: 13,
|
|
880
|
+
theme: "dark"
|
|
881
|
+
},
|
|
882
|
+
projects: [],
|
|
883
|
+
agentCommands: { ...DEFAULT_AGENT_COMMANDS },
|
|
884
|
+
workflows: [],
|
|
885
|
+
tasks: []
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
saveConfig(config) {
|
|
890
|
+
try {
|
|
891
|
+
saveConfig(config);
|
|
892
|
+
} catch (err) {
|
|
893
|
+
logger_default.error("[config-manager] saveConfig failed:", err);
|
|
894
|
+
throw err;
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
/** Register a callback for when config changes from within the main process */
|
|
898
|
+
onConfigChanged(callback) {
|
|
899
|
+
this.changeCallbacks.push(callback);
|
|
900
|
+
}
|
|
901
|
+
/** Notify all registered callbacks (call after main-process config mutations) */
|
|
902
|
+
notifyChanged() {
|
|
903
|
+
const config = this.loadConfig();
|
|
904
|
+
for (const cb of this.changeCallbacks) {
|
|
905
|
+
cb(config);
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
/**
|
|
909
|
+
* Watch the SQLite WAL file for external writes (e.g. MCP stdio process).
|
|
910
|
+
* On change, debounce and notify callbacks so the GUI picks up fresh data.
|
|
911
|
+
*/
|
|
912
|
+
watchDb() {
|
|
913
|
+
if (this.dbWatcher) return;
|
|
914
|
+
try {
|
|
915
|
+
this.dbWatcher = fs2.watch(DB_DIR, (eventType, filename) => {
|
|
916
|
+
if (!filename || !filename.endsWith(".db-wal")) return;
|
|
917
|
+
if (this.debounceTimer) clearTimeout(this.debounceTimer);
|
|
918
|
+
this.debounceTimer = setTimeout(() => {
|
|
919
|
+
this.notifyChanged();
|
|
920
|
+
}, 1e3);
|
|
921
|
+
});
|
|
922
|
+
} catch {
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
stopWatchingDb() {
|
|
926
|
+
if (this.dbWatcher) {
|
|
927
|
+
this.dbWatcher.close();
|
|
928
|
+
this.dbWatcher = null;
|
|
929
|
+
}
|
|
930
|
+
if (this.debounceTimer) {
|
|
931
|
+
clearTimeout(this.debounceTimer);
|
|
932
|
+
this.debounceTimer = null;
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
// No-ops -- retained for API compatibility during transition
|
|
936
|
+
watchConfig(_callback) {
|
|
937
|
+
}
|
|
938
|
+
stopWatching() {
|
|
939
|
+
}
|
|
940
|
+
};
|
|
941
|
+
var configManager = new ConfigManager();
|
|
942
|
+
|
|
943
|
+
// ../server/src/pty-manager.ts
|
|
944
|
+
import * as pty from "node-pty";
|
|
945
|
+
import crypto2 from "crypto";
|
|
946
|
+
import os3 from "os";
|
|
947
|
+
import { EventEmitter } from "events";
|
|
948
|
+
|
|
949
|
+
// ../server/src/git-utils.ts
|
|
950
|
+
import { execFileSync } from "child_process";
|
|
951
|
+
import path3 from "path";
|
|
952
|
+
import fs3 from "fs";
|
|
953
|
+
import crypto from "crypto";
|
|
954
|
+
var EXEC_OPTS = {
|
|
955
|
+
encoding: "utf-8",
|
|
956
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
957
|
+
};
|
|
958
|
+
function getGitBranch(projectPath) {
|
|
959
|
+
try {
|
|
960
|
+
const branch = execFileSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
|
|
961
|
+
cwd: projectPath,
|
|
962
|
+
...EXEC_OPTS,
|
|
963
|
+
timeout: 3e3
|
|
964
|
+
}).trim();
|
|
965
|
+
return branch && branch !== "HEAD" ? branch : null;
|
|
966
|
+
} catch {
|
|
967
|
+
return null;
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
function listBranches(projectPath) {
|
|
971
|
+
try {
|
|
972
|
+
const output = execFileSync("git", ["branch", "--format=%(refname:short)"], {
|
|
973
|
+
cwd: projectPath,
|
|
974
|
+
...EXEC_OPTS,
|
|
975
|
+
timeout: 5e3
|
|
976
|
+
}).trim();
|
|
977
|
+
return output ? output.split("\n").map((b) => b.trim()).filter(Boolean) : [];
|
|
978
|
+
} catch {
|
|
979
|
+
return [];
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
function checkoutBranch(projectPath, branch) {
|
|
983
|
+
try {
|
|
984
|
+
execFileSync("git", ["checkout", branch], {
|
|
985
|
+
cwd: projectPath,
|
|
986
|
+
...EXEC_OPTS,
|
|
987
|
+
timeout: 1e4
|
|
988
|
+
});
|
|
989
|
+
return true;
|
|
990
|
+
} catch {
|
|
991
|
+
return false;
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
function createWorktree(projectPath, branch) {
|
|
995
|
+
const projectName = path3.basename(projectPath);
|
|
996
|
+
const shortId = crypto.randomUUID().slice(0, 8);
|
|
997
|
+
const baseDir = path3.join(path3.dirname(projectPath), ".vibegrid-worktrees", projectName);
|
|
998
|
+
const worktreeDir = path3.join(baseDir, `${branch}-${shortId}`);
|
|
999
|
+
fs3.mkdirSync(baseDir, { recursive: true });
|
|
1000
|
+
const localBranches = listBranches(projectPath);
|
|
1001
|
+
if (localBranches.includes(branch)) {
|
|
1002
|
+
try {
|
|
1003
|
+
execFileSync("git", ["worktree", "add", worktreeDir, branch], {
|
|
1004
|
+
cwd: projectPath,
|
|
1005
|
+
...EXEC_OPTS,
|
|
1006
|
+
timeout: 3e4
|
|
1007
|
+
});
|
|
1008
|
+
} catch {
|
|
1009
|
+
const newBranch = `${branch}-worktree-${shortId}`;
|
|
1010
|
+
execFileSync("git", ["worktree", "add", "-b", newBranch, worktreeDir, branch], {
|
|
1011
|
+
cwd: projectPath,
|
|
1012
|
+
...EXEC_OPTS,
|
|
1013
|
+
timeout: 3e4
|
|
1014
|
+
});
|
|
1015
|
+
return { worktreePath: worktreeDir, branch: newBranch };
|
|
1016
|
+
}
|
|
1017
|
+
} else {
|
|
1018
|
+
execFileSync("git", ["worktree", "add", "-b", branch, worktreeDir], {
|
|
1019
|
+
cwd: projectPath,
|
|
1020
|
+
...EXEC_OPTS,
|
|
1021
|
+
timeout: 3e4
|
|
1022
|
+
});
|
|
1023
|
+
}
|
|
1024
|
+
return { worktreePath: worktreeDir, branch };
|
|
1025
|
+
}
|
|
1026
|
+
function getGitDiffStat(cwd) {
|
|
1027
|
+
try {
|
|
1028
|
+
const output = execFileSync("git", ["diff", "HEAD", "--numstat"], {
|
|
1029
|
+
cwd,
|
|
1030
|
+
...EXEC_OPTS,
|
|
1031
|
+
timeout: 1e4
|
|
1032
|
+
}).trim();
|
|
1033
|
+
if (!output) return { filesChanged: 0, insertions: 0, deletions: 0 };
|
|
1034
|
+
let insertions = 0;
|
|
1035
|
+
let deletions = 0;
|
|
1036
|
+
let filesChanged = 0;
|
|
1037
|
+
for (const line of output.split("\n")) {
|
|
1038
|
+
const parts = line.split(" ");
|
|
1039
|
+
if (parts[0] === "-") {
|
|
1040
|
+
filesChanged++;
|
|
1041
|
+
continue;
|
|
1042
|
+
}
|
|
1043
|
+
insertions += parseInt(parts[0], 10) || 0;
|
|
1044
|
+
deletions += parseInt(parts[1], 10) || 0;
|
|
1045
|
+
filesChanged++;
|
|
1046
|
+
}
|
|
1047
|
+
return { filesChanged, insertions, deletions };
|
|
1048
|
+
} catch {
|
|
1049
|
+
return null;
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
function getGitDiffFull(cwd) {
|
|
1053
|
+
try {
|
|
1054
|
+
const stat = getGitDiffStat(cwd);
|
|
1055
|
+
if (!stat) return null;
|
|
1056
|
+
const MAX_DIFF_SIZE = 500 * 1024;
|
|
1057
|
+
let rawDiff = execFileSync("git", ["diff", "HEAD", "-U3"], {
|
|
1058
|
+
cwd,
|
|
1059
|
+
...EXEC_OPTS,
|
|
1060
|
+
timeout: 15e3,
|
|
1061
|
+
maxBuffer: MAX_DIFF_SIZE * 2
|
|
1062
|
+
});
|
|
1063
|
+
if (rawDiff.length > MAX_DIFF_SIZE) {
|
|
1064
|
+
rawDiff = rawDiff.slice(0, MAX_DIFF_SIZE) + "\n\n... diff truncated (too large) ...\n";
|
|
1065
|
+
}
|
|
1066
|
+
const numstatOutput = execFileSync("git", ["diff", "HEAD", "--numstat"], {
|
|
1067
|
+
cwd,
|
|
1068
|
+
...EXEC_OPTS,
|
|
1069
|
+
timeout: 1e4
|
|
1070
|
+
}).trim();
|
|
1071
|
+
const fileStats = /* @__PURE__ */ new Map();
|
|
1072
|
+
if (numstatOutput) {
|
|
1073
|
+
for (const line of numstatOutput.split("\n")) {
|
|
1074
|
+
const parts = line.split(" ");
|
|
1075
|
+
if (parts.length >= 3) {
|
|
1076
|
+
const ins = parts[0] === "-" ? 0 : parseInt(parts[0], 10) || 0;
|
|
1077
|
+
const del = parts[1] === "-" ? 0 : parseInt(parts[1], 10) || 0;
|
|
1078
|
+
fileStats.set(parts.slice(2).join(" "), { insertions: ins, deletions: del });
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
const fileDiffs = [];
|
|
1083
|
+
const diffSections = rawDiff.split(/^diff --git /m).filter(Boolean);
|
|
1084
|
+
for (const section of diffSections) {
|
|
1085
|
+
const fullSection = "diff --git " + section;
|
|
1086
|
+
const plusMatch = fullSection.match(/^\+\+\+ b\/(.+)$/m);
|
|
1087
|
+
const minusMatch = fullSection.match(/^--- a\/(.+)$/m);
|
|
1088
|
+
const filePath = plusMatch?.[1] || minusMatch?.[1]?.replace(/^\/dev\/null$/, "") || "unknown";
|
|
1089
|
+
let status = "modified";
|
|
1090
|
+
if (fullSection.includes("--- /dev/null")) {
|
|
1091
|
+
status = "added";
|
|
1092
|
+
} else if (fullSection.includes("+++ /dev/null")) {
|
|
1093
|
+
status = "deleted";
|
|
1094
|
+
} else if (fullSection.includes("rename from")) {
|
|
1095
|
+
status = "renamed";
|
|
1096
|
+
}
|
|
1097
|
+
const stats = fileStats.get(filePath) || { insertions: 0, deletions: 0 };
|
|
1098
|
+
fileDiffs.push({
|
|
1099
|
+
filePath,
|
|
1100
|
+
status,
|
|
1101
|
+
insertions: stats.insertions,
|
|
1102
|
+
deletions: stats.deletions,
|
|
1103
|
+
diff: fullSection
|
|
1104
|
+
});
|
|
1105
|
+
}
|
|
1106
|
+
return { stat, files: fileDiffs };
|
|
1107
|
+
} catch {
|
|
1108
|
+
return null;
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
// ../server/src/agent-launch.ts
|
|
1113
|
+
import { execFileSync as execFileSync3 } from "child_process";
|
|
1114
|
+
|
|
1115
|
+
// ../server/src/process-utils.ts
|
|
1116
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
1117
|
+
function getUserShellEnv() {
|
|
1118
|
+
if (process.platform === "win32") return { ...process.env };
|
|
1119
|
+
try {
|
|
1120
|
+
const shell = process.env.SHELL || "/bin/zsh";
|
|
1121
|
+
const output = execFileSync2(shell, ["-ilc", "env"], {
|
|
1122
|
+
encoding: "utf-8",
|
|
1123
|
+
timeout: 5e3,
|
|
1124
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1125
|
+
});
|
|
1126
|
+
const env = {};
|
|
1127
|
+
for (const line of output.split("\n")) {
|
|
1128
|
+
const idx = line.indexOf("=");
|
|
1129
|
+
if (idx > 0) {
|
|
1130
|
+
env[line.substring(0, idx)] = line.substring(idx + 1);
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
return env;
|
|
1134
|
+
} catch {
|
|
1135
|
+
return { ...process.env };
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
var resolvedEnv = getUserShellEnv();
|
|
1139
|
+
function getDefaultShell() {
|
|
1140
|
+
if (process.platform === "win32") {
|
|
1141
|
+
return process.env.COMSPEC || "powershell.exe";
|
|
1142
|
+
}
|
|
1143
|
+
return process.env.SHELL || "/bin/zsh";
|
|
1144
|
+
}
|
|
1145
|
+
function shellEscape(s) {
|
|
1146
|
+
if (/^[a-zA-Z0-9_./:=@%+,-]+$/.test(s)) return s;
|
|
1147
|
+
return "'" + s.replace(/'/g, "'\\''") + "'";
|
|
1148
|
+
}
|
|
1149
|
+
var SENSITIVE_ENV_PREFIXES = [
|
|
1150
|
+
"AWS_SECRET",
|
|
1151
|
+
"AWS_SESSION",
|
|
1152
|
+
"GITHUB_TOKEN",
|
|
1153
|
+
"GH_TOKEN",
|
|
1154
|
+
"OPENAI_API",
|
|
1155
|
+
"ANTHROPIC_API",
|
|
1156
|
+
"GOOGLE_API",
|
|
1157
|
+
"STRIPE_",
|
|
1158
|
+
"DATABASE_URL",
|
|
1159
|
+
"DB_PASSWORD",
|
|
1160
|
+
"SECRET_",
|
|
1161
|
+
"PRIVATE_KEY",
|
|
1162
|
+
"NPM_TOKEN",
|
|
1163
|
+
"NODE_AUTH_TOKEN"
|
|
1164
|
+
];
|
|
1165
|
+
var STRIP_ENV_KEYS = ["CLAUDECODE"];
|
|
1166
|
+
function getSafeEnv() {
|
|
1167
|
+
const env = {};
|
|
1168
|
+
for (const [key, val] of Object.entries(resolvedEnv)) {
|
|
1169
|
+
if (val === void 0) continue;
|
|
1170
|
+
if (SENSITIVE_ENV_PREFIXES.some((p) => key.toUpperCase().startsWith(p))) continue;
|
|
1171
|
+
if (STRIP_ENV_KEYS.includes(key)) continue;
|
|
1172
|
+
env[key] = val;
|
|
1173
|
+
}
|
|
1174
|
+
return env;
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
// ../server/src/agent-launch.ts
|
|
1178
|
+
function commandExists(cmd, env) {
|
|
1179
|
+
try {
|
|
1180
|
+
const bin = process.platform === "win32" ? "where" : "which";
|
|
1181
|
+
execFileSync3(bin, [cmd], { stdio: "pipe", timeout: 3e3, env });
|
|
1182
|
+
return true;
|
|
1183
|
+
} catch {
|
|
1184
|
+
return false;
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
function resolveAgentCommand(config, env) {
|
|
1188
|
+
if (commandExists(config.command, env)) {
|
|
1189
|
+
return { command: config.command, args: config.args };
|
|
1190
|
+
}
|
|
1191
|
+
if (config.fallbackCommand && commandExists(config.fallbackCommand, env)) {
|
|
1192
|
+
return { command: config.fallbackCommand, args: config.fallbackArgs ?? [] };
|
|
1193
|
+
}
|
|
1194
|
+
return { command: config.command, args: config.args };
|
|
1195
|
+
}
|
|
1196
|
+
function buildAgentLaunchLine(payload, agentCommands, env) {
|
|
1197
|
+
const cmdConfig = agentCommands[payload.agentType] || DEFAULT_AGENT_COMMANDS[payload.agentType];
|
|
1198
|
+
const cmd = resolveAgentCommand(cmdConfig, env);
|
|
1199
|
+
const effectiveArgs = payload.args !== void 0 ? payload.args : cmd.args;
|
|
1200
|
+
let launchLine = [cmd.command, ...effectiveArgs.map((a) => shellEscape(a))].join(" ");
|
|
1201
|
+
if (payload.resumeSessionId) {
|
|
1202
|
+
switch (payload.agentType) {
|
|
1203
|
+
case "claude":
|
|
1204
|
+
launchLine += ` --resume ${payload.resumeSessionId}`;
|
|
1205
|
+
break;
|
|
1206
|
+
case "copilot":
|
|
1207
|
+
launchLine += ` --resume ${payload.resumeSessionId}`;
|
|
1208
|
+
break;
|
|
1209
|
+
case "codex":
|
|
1210
|
+
launchLine = `${cmd.command} resume ${payload.resumeSessionId}`;
|
|
1211
|
+
break;
|
|
1212
|
+
case "opencode":
|
|
1213
|
+
launchLine += ` --session ${payload.resumeSessionId}`;
|
|
1214
|
+
break;
|
|
1215
|
+
case "gemini":
|
|
1216
|
+
launchLine += ` --resume latest`;
|
|
1217
|
+
break;
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
if (payload.initialPrompt) {
|
|
1221
|
+
const escaped = shellEscape(payload.initialPrompt);
|
|
1222
|
+
switch (payload.agentType) {
|
|
1223
|
+
case "copilot":
|
|
1224
|
+
launchLine += ` -i ${escaped}`;
|
|
1225
|
+
break;
|
|
1226
|
+
case "gemini":
|
|
1227
|
+
launchLine += ` -i ${escaped}`;
|
|
1228
|
+
break;
|
|
1229
|
+
case "opencode":
|
|
1230
|
+
launchLine += ` --prompt ${escaped}`;
|
|
1231
|
+
break;
|
|
1232
|
+
default:
|
|
1233
|
+
launchLine += ` ${escaped}`;
|
|
1234
|
+
break;
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
return launchLine;
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
// ../server/src/pty-manager.ts
|
|
1241
|
+
var PtyManager = class extends EventEmitter {
|
|
1242
|
+
ptys = /* @__PURE__ */ new Map();
|
|
1243
|
+
sessions = /* @__PURE__ */ new Map();
|
|
1244
|
+
agentCommands = { ...DEFAULT_AGENT_COMMANDS };
|
|
1245
|
+
remoteHosts = [];
|
|
1246
|
+
dataBuffers = /* @__PURE__ */ new Map();
|
|
1247
|
+
flushTimers = /* @__PURE__ */ new Map();
|
|
1248
|
+
setRemoteHosts(hosts) {
|
|
1249
|
+
this.remoteHosts = hosts;
|
|
1250
|
+
}
|
|
1251
|
+
setAgentCommands(overrides) {
|
|
1252
|
+
this.agentCommands = { ...DEFAULT_AGENT_COMMANDS };
|
|
1253
|
+
if (overrides) {
|
|
1254
|
+
for (const [key, val] of Object.entries(overrides)) {
|
|
1255
|
+
if (val) {
|
|
1256
|
+
this.agentCommands[key] = val;
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
buildAgentLaunchLine(payload) {
|
|
1262
|
+
return buildAgentLaunchLine(payload, this.agentCommands, getSafeEnv());
|
|
1263
|
+
}
|
|
1264
|
+
createPty(payload) {
|
|
1265
|
+
const id = crypto2.randomUUID();
|
|
1266
|
+
const shell = getDefaultShell();
|
|
1267
|
+
const remoteHost = payload.remoteHostId ? this.remoteHosts.find((h) => h.id === payload.remoteHostId) : void 0;
|
|
1268
|
+
const session = remoteHost ? this.createRemotePty(id, shell, payload, remoteHost) : this.createLocalPty(id, shell, payload);
|
|
1269
|
+
this.emit("session-created", session, payload);
|
|
1270
|
+
return session;
|
|
1271
|
+
}
|
|
1272
|
+
createLocalPty(id, shell, payload) {
|
|
1273
|
+
let effectivePath = payload.projectPath;
|
|
1274
|
+
let worktreePath;
|
|
1275
|
+
let effectiveBranch;
|
|
1276
|
+
if (payload.existingWorktreePath) {
|
|
1277
|
+
effectivePath = payload.existingWorktreePath;
|
|
1278
|
+
worktreePath = payload.existingWorktreePath;
|
|
1279
|
+
effectiveBranch = payload.branch;
|
|
1280
|
+
} else if (payload.useWorktree && payload.branch) {
|
|
1281
|
+
const result = createWorktree(payload.projectPath, payload.branch);
|
|
1282
|
+
effectivePath = result.worktreePath;
|
|
1283
|
+
worktreePath = result.worktreePath;
|
|
1284
|
+
effectiveBranch = result.branch;
|
|
1285
|
+
} else if (payload.branch) {
|
|
1286
|
+
const currentBranch = getGitBranch(payload.projectPath);
|
|
1287
|
+
if (currentBranch !== payload.branch) {
|
|
1288
|
+
checkoutBranch(payload.projectPath, payload.branch);
|
|
1289
|
+
}
|
|
1290
|
+
effectiveBranch = payload.branch;
|
|
1291
|
+
}
|
|
1292
|
+
const ptyProcess = pty.spawn(shell, ["-l"], {
|
|
1293
|
+
name: "xterm-256color",
|
|
1294
|
+
cols: 80,
|
|
1295
|
+
rows: 24,
|
|
1296
|
+
cwd: effectivePath,
|
|
1297
|
+
env: getSafeEnv()
|
|
1298
|
+
});
|
|
1299
|
+
const launchLine = this.buildAgentLaunchLine(payload);
|
|
1300
|
+
setTimeout(() => ptyProcess.write(launchLine + "\r"), 300);
|
|
1301
|
+
this.setupPtyEvents(id, ptyProcess);
|
|
1302
|
+
this.ptys.set(id, ptyProcess);
|
|
1303
|
+
const branch = effectiveBranch || getGitBranch(effectivePath);
|
|
1304
|
+
const session = {
|
|
1305
|
+
id,
|
|
1306
|
+
agentType: payload.agentType,
|
|
1307
|
+
projectName: payload.projectName,
|
|
1308
|
+
projectPath: payload.projectPath,
|
|
1309
|
+
status: "running",
|
|
1310
|
+
createdAt: Date.now(),
|
|
1311
|
+
pid: ptyProcess.pid,
|
|
1312
|
+
...payload.displayName ? { displayName: payload.displayName } : {},
|
|
1313
|
+
...branch ? { branch } : {},
|
|
1314
|
+
...worktreePath ? { worktreePath, isWorktree: true } : {}
|
|
1315
|
+
};
|
|
1316
|
+
this.sessions.set(id, session);
|
|
1317
|
+
return session;
|
|
1318
|
+
}
|
|
1319
|
+
createRemotePty(id, shell, payload, host) {
|
|
1320
|
+
const ptyProcess = pty.spawn(shell, ["-l"], {
|
|
1321
|
+
name: "xterm-256color",
|
|
1322
|
+
cols: 80,
|
|
1323
|
+
rows: 24,
|
|
1324
|
+
cwd: os3.homedir(),
|
|
1325
|
+
env: getSafeEnv()
|
|
1326
|
+
});
|
|
1327
|
+
const sshParts = ["ssh", "-t"];
|
|
1328
|
+
if (host.port !== 22) sshParts.push("-p", String(host.port));
|
|
1329
|
+
if (host.sshKeyPath) sshParts.push("-i", host.sshKeyPath);
|
|
1330
|
+
if (host.sshOptions) {
|
|
1331
|
+
const opts = host.sshOptions.split(/\s+/).filter(Boolean);
|
|
1332
|
+
sshParts.push(...opts);
|
|
1333
|
+
}
|
|
1334
|
+
sshParts.push(`${host.user}@${host.hostname}`);
|
|
1335
|
+
const agentLine = this.buildAgentLaunchLine(payload);
|
|
1336
|
+
const remoteCmd = `cd ${shellEscape(payload.projectPath)} && ${agentLine}`;
|
|
1337
|
+
setTimeout(() => {
|
|
1338
|
+
if (this.ptys.has(id)) ptyProcess.write(sshParts.join(" ") + "\r");
|
|
1339
|
+
}, 300);
|
|
1340
|
+
let connected = false;
|
|
1341
|
+
const fallbackTimer = setTimeout(() => {
|
|
1342
|
+
if (!connected) {
|
|
1343
|
+
connected = true;
|
|
1344
|
+
if (this.ptys.has(id)) ptyProcess.write(remoteCmd + "\r");
|
|
1345
|
+
}
|
|
1346
|
+
}, 5e3);
|
|
1347
|
+
const promptListener = ptyProcess.onData((data) => {
|
|
1348
|
+
if (!connected && /[$#>]\s*$/.test(data)) {
|
|
1349
|
+
connected = true;
|
|
1350
|
+
clearTimeout(fallbackTimer);
|
|
1351
|
+
setTimeout(() => {
|
|
1352
|
+
if (this.ptys.has(id)) ptyProcess.write(remoteCmd + "\r");
|
|
1353
|
+
}, 100);
|
|
1354
|
+
}
|
|
1355
|
+
});
|
|
1356
|
+
this.setupPtyEvents(id, ptyProcess);
|
|
1357
|
+
this.ptys.set(id, ptyProcess);
|
|
1358
|
+
const cleanup = () => {
|
|
1359
|
+
promptListener.dispose();
|
|
1360
|
+
};
|
|
1361
|
+
const checkConnected = setInterval(() => {
|
|
1362
|
+
if (connected) {
|
|
1363
|
+
cleanup();
|
|
1364
|
+
clearInterval(checkConnected);
|
|
1365
|
+
}
|
|
1366
|
+
}, 200);
|
|
1367
|
+
setTimeout(() => {
|
|
1368
|
+
cleanup();
|
|
1369
|
+
clearInterval(checkConnected);
|
|
1370
|
+
}, 6e3);
|
|
1371
|
+
const session = {
|
|
1372
|
+
id,
|
|
1373
|
+
agentType: payload.agentType,
|
|
1374
|
+
projectName: payload.projectName,
|
|
1375
|
+
projectPath: payload.projectPath,
|
|
1376
|
+
status: "running",
|
|
1377
|
+
createdAt: Date.now(),
|
|
1378
|
+
pid: ptyProcess.pid,
|
|
1379
|
+
remoteHostId: host.id,
|
|
1380
|
+
remoteHostLabel: host.label,
|
|
1381
|
+
...payload.displayName ? { displayName: payload.displayName } : {}
|
|
1382
|
+
};
|
|
1383
|
+
this.sessions.set(id, session);
|
|
1384
|
+
return session;
|
|
1385
|
+
}
|
|
1386
|
+
createShellPty(cwd) {
|
|
1387
|
+
const id = crypto2.randomUUID();
|
|
1388
|
+
const shell = getDefaultShell();
|
|
1389
|
+
const ptyProcess = pty.spawn(shell, ["-l"], {
|
|
1390
|
+
name: "xterm-256color",
|
|
1391
|
+
cols: 80,
|
|
1392
|
+
rows: 24,
|
|
1393
|
+
cwd: cwd || os3.homedir(),
|
|
1394
|
+
env: getSafeEnv()
|
|
1395
|
+
});
|
|
1396
|
+
this.setupPtyEvents(id, ptyProcess);
|
|
1397
|
+
this.ptys.set(id, ptyProcess);
|
|
1398
|
+
return { id, pid: ptyProcess.pid };
|
|
1399
|
+
}
|
|
1400
|
+
bufferData(id, data) {
|
|
1401
|
+
const existing = this.dataBuffers.get(id);
|
|
1402
|
+
this.dataBuffers.set(id, existing ? existing + data : data);
|
|
1403
|
+
if (!this.flushTimers.has(id)) {
|
|
1404
|
+
this.flushTimers.set(
|
|
1405
|
+
id,
|
|
1406
|
+
setTimeout(() => this.flushBuffer(id), 50)
|
|
1407
|
+
);
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
flushBuffer(id) {
|
|
1411
|
+
const data = this.dataBuffers.get(id);
|
|
1412
|
+
this.dataBuffers.delete(id);
|
|
1413
|
+
this.flushTimers.delete(id);
|
|
1414
|
+
if (data) {
|
|
1415
|
+
this.emit("client-message", IPC.TERMINAL_DATA, { id, data });
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
clearBuffer(id) {
|
|
1419
|
+
const timer = this.flushTimers.get(id);
|
|
1420
|
+
if (timer) clearTimeout(timer);
|
|
1421
|
+
this.flushTimers.delete(id);
|
|
1422
|
+
this.dataBuffers.delete(id);
|
|
1423
|
+
}
|
|
1424
|
+
setupPtyEvents(id, ptyProcess) {
|
|
1425
|
+
ptyProcess.onData((data) => {
|
|
1426
|
+
this.bufferData(id, data);
|
|
1427
|
+
});
|
|
1428
|
+
ptyProcess.onExit(({ exitCode }) => {
|
|
1429
|
+
const pendingTimer = this.flushTimers.get(id);
|
|
1430
|
+
if (pendingTimer) {
|
|
1431
|
+
clearTimeout(pendingTimer);
|
|
1432
|
+
this.flushBuffer(id);
|
|
1433
|
+
}
|
|
1434
|
+
this.clearBuffer(id);
|
|
1435
|
+
this.ptys.delete(id);
|
|
1436
|
+
const session = this.sessions.get(id);
|
|
1437
|
+
if (session) {
|
|
1438
|
+
this.emit("session-exit", session);
|
|
1439
|
+
session.status = "idle";
|
|
1440
|
+
if (session.worktreePath) {
|
|
1441
|
+
this.emit("client-message", IPC.WORKTREE_CONFIRM_CLEANUP, {
|
|
1442
|
+
id: session.id,
|
|
1443
|
+
projectPath: session.projectPath,
|
|
1444
|
+
worktreePath: session.worktreePath
|
|
1445
|
+
});
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
this.emit("client-message", IPC.TERMINAL_EXIT, { id, exitCode });
|
|
1449
|
+
});
|
|
1450
|
+
}
|
|
1451
|
+
writeToPty(id, data) {
|
|
1452
|
+
this.ptys.get(id)?.write(data);
|
|
1453
|
+
}
|
|
1454
|
+
resizePty(id, cols, rows) {
|
|
1455
|
+
this.ptys.get(id)?.resize(cols, rows);
|
|
1456
|
+
}
|
|
1457
|
+
killPty(id) {
|
|
1458
|
+
const p = this.ptys.get(id);
|
|
1459
|
+
const pendingTimer = this.flushTimers.get(id);
|
|
1460
|
+
if (pendingTimer) {
|
|
1461
|
+
clearTimeout(pendingTimer);
|
|
1462
|
+
this.flushBuffer(id);
|
|
1463
|
+
}
|
|
1464
|
+
this.clearBuffer(id);
|
|
1465
|
+
const session = this.sessions.get(id);
|
|
1466
|
+
this.sessions.delete(id);
|
|
1467
|
+
this.ptys.delete(id);
|
|
1468
|
+
if (session) {
|
|
1469
|
+
this.emit("session-exit", session);
|
|
1470
|
+
if (session.worktreePath) {
|
|
1471
|
+
this.emit("client-message", IPC.WORKTREE_CONFIRM_CLEANUP, {
|
|
1472
|
+
id: session.id,
|
|
1473
|
+
projectPath: session.projectPath,
|
|
1474
|
+
worktreePath: session.worktreePath
|
|
1475
|
+
});
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
if (p) {
|
|
1479
|
+
setImmediate(() => {
|
|
1480
|
+
try {
|
|
1481
|
+
p.kill();
|
|
1482
|
+
} catch (err) {
|
|
1483
|
+
logger_default.warn(`[pty] kill failed for ${id} (already dead?):`, err);
|
|
1484
|
+
}
|
|
1485
|
+
});
|
|
1486
|
+
} else {
|
|
1487
|
+
this.emit("client-message", IPC.TERMINAL_EXIT, { id, exitCode: 0 });
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
killAll() {
|
|
1491
|
+
for (const timer of this.flushTimers.values()) {
|
|
1492
|
+
clearTimeout(timer);
|
|
1493
|
+
}
|
|
1494
|
+
this.dataBuffers.clear();
|
|
1495
|
+
this.flushTimers.clear();
|
|
1496
|
+
for (const [id, p] of this.ptys) {
|
|
1497
|
+
p.kill();
|
|
1498
|
+
this.ptys.delete(id);
|
|
1499
|
+
}
|
|
1500
|
+
this.sessions.clear();
|
|
1501
|
+
}
|
|
1502
|
+
getActiveSessions() {
|
|
1503
|
+
return Array.from(this.sessions.values());
|
|
1504
|
+
}
|
|
1505
|
+
updateSessionStatus(id, status) {
|
|
1506
|
+
const session = this.sessions.get(id);
|
|
1507
|
+
if (session) {
|
|
1508
|
+
session.status = status;
|
|
1509
|
+
this.emit("client-message", IPC.TERMINAL_DATA, { id, data: "" });
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
/**
|
|
1513
|
+
* Finds the most-recently-created terminal matching cwd that:
|
|
1514
|
+
* - is NOT already linked to a Claude session (no hookSessionId)
|
|
1515
|
+
* - is NOT in the excludeIds set (already claimed by another session_id)
|
|
1516
|
+
*/
|
|
1517
|
+
findUnlinkedSessionByCwd(cwd, excludeIds) {
|
|
1518
|
+
const normalizedCwd = cwd.replace(/\/+$/, "");
|
|
1519
|
+
let best;
|
|
1520
|
+
let bestTime = 0;
|
|
1521
|
+
for (const session of this.sessions.values()) {
|
|
1522
|
+
if (session.hookSessionId) continue;
|
|
1523
|
+
if (excludeIds.has(session.id)) continue;
|
|
1524
|
+
const sessionPath = (session.worktreePath || session.projectPath).replace(/\/+$/, "");
|
|
1525
|
+
if (sessionPath === normalizedCwd && session.createdAt > bestTime) {
|
|
1526
|
+
best = session;
|
|
1527
|
+
bestTime = session.createdAt;
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
return best;
|
|
1531
|
+
}
|
|
1532
|
+
};
|
|
1533
|
+
var ptyManager = new PtyManager();
|
|
1534
|
+
|
|
1535
|
+
// ../server/src/scheduler.ts
|
|
1536
|
+
import cron from "node-cron";
|
|
1537
|
+
import { EventEmitter as EventEmitter2 } from "events";
|
|
1538
|
+
function getTriggerConfig(wf) {
|
|
1539
|
+
const triggerNode = wf.nodes.find((n) => n.type === "trigger");
|
|
1540
|
+
if (!triggerNode) return null;
|
|
1541
|
+
return triggerNode.config;
|
|
1542
|
+
}
|
|
1543
|
+
var Scheduler = class extends EventEmitter2 {
|
|
1544
|
+
cronJobs = /* @__PURE__ */ new Map();
|
|
1545
|
+
timeouts = /* @__PURE__ */ new Map();
|
|
1546
|
+
syncSchedules(workflows) {
|
|
1547
|
+
for (const [id] of this.cronJobs) {
|
|
1548
|
+
const wf = workflows.find((w) => w.id === id);
|
|
1549
|
+
const trigger = wf ? getTriggerConfig(wf) : null;
|
|
1550
|
+
if (!wf || !wf.enabled || trigger?.triggerType !== "recurring") {
|
|
1551
|
+
this.cronJobs.get(id)?.stop();
|
|
1552
|
+
this.cronJobs.delete(id);
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
for (const [id] of this.timeouts) {
|
|
1556
|
+
const wf = workflows.find((w) => w.id === id);
|
|
1557
|
+
const trigger = wf ? getTriggerConfig(wf) : null;
|
|
1558
|
+
if (!wf || !wf.enabled || trigger?.triggerType !== "once") {
|
|
1559
|
+
clearTimeout(this.timeouts.get(id));
|
|
1560
|
+
this.timeouts.delete(id);
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
for (const wf of workflows) {
|
|
1564
|
+
if (!wf.enabled) continue;
|
|
1565
|
+
const trigger = getTriggerConfig(wf);
|
|
1566
|
+
if (!trigger) continue;
|
|
1567
|
+
if (trigger.triggerType === "recurring" && !this.cronJobs.has(wf.id)) {
|
|
1568
|
+
if (!cron.validate(trigger.cron)) {
|
|
1569
|
+
logger_default.error(
|
|
1570
|
+
`[scheduler] invalid cron expression for workflow "${wf.name}": ${trigger.cron}`
|
|
1571
|
+
);
|
|
1572
|
+
continue;
|
|
1573
|
+
}
|
|
1574
|
+
try {
|
|
1575
|
+
const task = cron.schedule(trigger.cron, () => this.executeWorkflow(wf.id), {
|
|
1576
|
+
timezone: trigger.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone
|
|
1577
|
+
});
|
|
1578
|
+
this.cronJobs.set(wf.id, task);
|
|
1579
|
+
} catch (err) {
|
|
1580
|
+
logger_default.error(`[scheduler] failed to schedule workflow "${wf.name}":`, err);
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
if (trigger.triggerType === "once" && !this.timeouts.has(wf.id)) {
|
|
1584
|
+
const runAt = new Date(trigger.runAt).getTime();
|
|
1585
|
+
if (isNaN(runAt)) {
|
|
1586
|
+
logger_default.error(`[scheduler] invalid runAt date for workflow "${wf.name}": ${trigger.runAt}`);
|
|
1587
|
+
continue;
|
|
1588
|
+
}
|
|
1589
|
+
const delay = runAt - Date.now();
|
|
1590
|
+
if (delay > 0) {
|
|
1591
|
+
const MAX_DELAY = 24 * 60 * 60 * 1e3;
|
|
1592
|
+
const safeDelay = Math.min(delay, MAX_DELAY);
|
|
1593
|
+
const timer = setTimeout(() => {
|
|
1594
|
+
if (safeDelay < delay) {
|
|
1595
|
+
this.timeouts.delete(wf.id);
|
|
1596
|
+
this.syncSchedules(configManager.loadConfig().workflows ?? []);
|
|
1597
|
+
} else {
|
|
1598
|
+
this.executeWorkflow(wf.id);
|
|
1599
|
+
}
|
|
1600
|
+
}, safeDelay);
|
|
1601
|
+
this.timeouts.set(wf.id, timer);
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
executeWorkflow(workflowId) {
|
|
1607
|
+
this.emit("client-message", IPC.SCHEDULER_EXECUTE, { workflowId });
|
|
1608
|
+
this.timeouts.delete(workflowId);
|
|
1609
|
+
}
|
|
1610
|
+
checkMissedSchedules(workflows) {
|
|
1611
|
+
const missed = [];
|
|
1612
|
+
for (const wf of workflows) {
|
|
1613
|
+
if (!wf.enabled) continue;
|
|
1614
|
+
const trigger = getTriggerConfig(wf);
|
|
1615
|
+
if (trigger?.triggerType === "once") {
|
|
1616
|
+
const runAt = new Date(trigger.runAt).getTime();
|
|
1617
|
+
if (runAt < Date.now() && !wf.lastRunAt) {
|
|
1618
|
+
missed.push({ workflow: wf, scheduledFor: trigger.runAt });
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
return missed;
|
|
1623
|
+
}
|
|
1624
|
+
getNextRun(workflowId, workflows) {
|
|
1625
|
+
const wf = workflows.find((w) => w.id === workflowId);
|
|
1626
|
+
if (!wf || !wf.enabled) return null;
|
|
1627
|
+
const trigger = getTriggerConfig(wf);
|
|
1628
|
+
if (!trigger) return null;
|
|
1629
|
+
if (trigger.triggerType === "once") {
|
|
1630
|
+
const runAt = new Date(trigger.runAt).getTime();
|
|
1631
|
+
return runAt > Date.now() ? trigger.runAt : null;
|
|
1632
|
+
}
|
|
1633
|
+
if (trigger.triggerType === "recurring") {
|
|
1634
|
+
return trigger.cron;
|
|
1635
|
+
}
|
|
1636
|
+
return null;
|
|
1637
|
+
}
|
|
1638
|
+
stopAll() {
|
|
1639
|
+
for (const [, job] of this.cronJobs) job.stop();
|
|
1640
|
+
for (const [, timer] of this.timeouts) clearTimeout(timer);
|
|
1641
|
+
this.cronJobs.clear();
|
|
1642
|
+
this.timeouts.clear();
|
|
1643
|
+
}
|
|
1644
|
+
};
|
|
1645
|
+
var scheduler = new Scheduler();
|
|
1646
|
+
|
|
1647
|
+
// src/server.ts
|
|
1648
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
1649
|
+
|
|
1650
|
+
// src/tools/tasks.ts
|
|
1651
|
+
import crypto3 from "crypto";
|
|
1652
|
+
import path4 from "path";
|
|
1653
|
+
import { z as z2 } from "zod";
|
|
1654
|
+
|
|
1655
|
+
// src/validation.ts
|
|
1656
|
+
import { z } from "zod";
|
|
1657
|
+
var safeName = z.string().min(1, "Name must not be empty").max(200, "Name must be 200 characters or less").refine((s) => !s.includes("..") && !s.includes("/") && !s.includes("\\"), {
|
|
1658
|
+
message: "Name must not contain path traversal characters (.. / \\)"
|
|
1659
|
+
});
|
|
1660
|
+
var safeId = z.string().min(1, "ID must not be empty").max(100, "ID must be 100 characters or less");
|
|
1661
|
+
var safeTitle = z.string().min(1, "Title must not be empty").max(500, "Title must be 500 characters or less");
|
|
1662
|
+
var safeDescription = z.string().max(5e3, "Description must be 5000 characters or less");
|
|
1663
|
+
var safeShortText = z.string().max(200, "Value must be 200 characters or less");
|
|
1664
|
+
var safePrompt = z.string().max(1e4, "Prompt must be 10000 characters or less");
|
|
1665
|
+
var safeAbsolutePath = z.string().min(1, "Path must not be empty").max(1e3, "Path must be 1000 characters or less").refine((s) => s.startsWith("/"), { message: "Path must be absolute (start with /)" });
|
|
1666
|
+
var safeHexColor = z.string().regex(/^#[0-9a-fA-F]{3,8}$/, "Must be a valid hex color (e.g. #6366f1)");
|
|
1667
|
+
var V = {
|
|
1668
|
+
name: safeName,
|
|
1669
|
+
id: safeId,
|
|
1670
|
+
title: safeTitle,
|
|
1671
|
+
description: safeDescription,
|
|
1672
|
+
shortText: safeShortText,
|
|
1673
|
+
prompt: safePrompt,
|
|
1674
|
+
absolutePath: safeAbsolutePath,
|
|
1675
|
+
hexColor: safeHexColor
|
|
1676
|
+
};
|
|
1677
|
+
|
|
1678
|
+
// src/tools/tasks.ts
|
|
1679
|
+
var TASK_STATUSES = ["todo", "in_progress", "in_review", "done", "cancelled"];
|
|
1680
|
+
var AGENT_TYPES = [
|
|
1681
|
+
"claude",
|
|
1682
|
+
"copilot",
|
|
1683
|
+
"codex",
|
|
1684
|
+
"opencode",
|
|
1685
|
+
"gemini"
|
|
1686
|
+
];
|
|
1687
|
+
function registerTaskTools(server, deps) {
|
|
1688
|
+
const { configManager: configManager2 } = deps;
|
|
1689
|
+
server.tool(
|
|
1690
|
+
"list_tasks",
|
|
1691
|
+
"List tasks, optionally filtered by project and/or status",
|
|
1692
|
+
{
|
|
1693
|
+
project_name: V.name.optional().describe("Filter by project name"),
|
|
1694
|
+
status: z2.enum(TASK_STATUSES).optional().describe("Filter by status")
|
|
1695
|
+
},
|
|
1696
|
+
async (args) => {
|
|
1697
|
+
const tasks = dbListTasks(args.project_name, args.status);
|
|
1698
|
+
return { content: [{ type: "text", text: JSON.stringify(tasks, null, 2) }] };
|
|
1699
|
+
}
|
|
1700
|
+
);
|
|
1701
|
+
server.tool(
|
|
1702
|
+
"create_task",
|
|
1703
|
+
"Create a new task in a project",
|
|
1704
|
+
{
|
|
1705
|
+
project_name: V.name.describe("Project name (must match existing project)"),
|
|
1706
|
+
title: V.title.describe("Task title"),
|
|
1707
|
+
description: V.description.optional().describe("Task description (markdown)"),
|
|
1708
|
+
status: z2.enum(TASK_STATUSES).optional().describe("Task status (default: todo)"),
|
|
1709
|
+
branch: V.shortText.optional().describe("Git branch for this task"),
|
|
1710
|
+
use_worktree: z2.boolean().optional().describe("Create a git worktree for this task"),
|
|
1711
|
+
assigned_agent: z2.enum(AGENT_TYPES).optional().describe("Assign to an agent type")
|
|
1712
|
+
},
|
|
1713
|
+
async (args) => {
|
|
1714
|
+
const project = dbGetProject(args.project_name);
|
|
1715
|
+
if (!project) {
|
|
1716
|
+
return {
|
|
1717
|
+
content: [{ type: "text", text: `Error: project "${args.project_name}" not found` }],
|
|
1718
|
+
isError: true
|
|
1719
|
+
};
|
|
1720
|
+
}
|
|
1721
|
+
const maxOrder = dbGetMaxTaskOrder(args.project_name);
|
|
1722
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1723
|
+
const status = args.status ?? "todo";
|
|
1724
|
+
const task = {
|
|
1725
|
+
id: crypto3.randomUUID(),
|
|
1726
|
+
projectName: args.project_name,
|
|
1727
|
+
title: args.title,
|
|
1728
|
+
description: args.description ?? "",
|
|
1729
|
+
status,
|
|
1730
|
+
order: maxOrder + 1,
|
|
1731
|
+
createdAt: now,
|
|
1732
|
+
updatedAt: now,
|
|
1733
|
+
...args.branch && { branch: args.branch },
|
|
1734
|
+
...args.use_worktree && { useWorktree: args.use_worktree },
|
|
1735
|
+
...args.assigned_agent && { assignedAgent: args.assigned_agent },
|
|
1736
|
+
...(status === "done" || status === "cancelled") && { completedAt: now }
|
|
1737
|
+
};
|
|
1738
|
+
dbInsertTask(task);
|
|
1739
|
+
configManager2.notifyChanged();
|
|
1740
|
+
return { content: [{ type: "text", text: JSON.stringify(task, null, 2) }] };
|
|
1741
|
+
}
|
|
1742
|
+
);
|
|
1743
|
+
server.tool("get_task", "Get a task by ID", { id: V.id.describe("Task ID") }, async (args) => {
|
|
1744
|
+
const task = dbGetTask(args.id);
|
|
1745
|
+
if (!task) {
|
|
1746
|
+
return {
|
|
1747
|
+
content: [{ type: "text", text: `Error: task "${args.id}" not found` }],
|
|
1748
|
+
isError: true
|
|
1749
|
+
};
|
|
1750
|
+
}
|
|
1751
|
+
return { content: [{ type: "text", text: JSON.stringify(task, null, 2) }] };
|
|
1752
|
+
});
|
|
1753
|
+
server.tool(
|
|
1754
|
+
"update_task",
|
|
1755
|
+
"Update a task's properties",
|
|
1756
|
+
{
|
|
1757
|
+
id: V.id.describe("Task ID"),
|
|
1758
|
+
title: V.title.optional().describe("New title"),
|
|
1759
|
+
description: V.description.optional().describe("New description"),
|
|
1760
|
+
status: z2.enum(TASK_STATUSES).optional().describe("New status"),
|
|
1761
|
+
branch: V.shortText.optional().describe("Git branch"),
|
|
1762
|
+
use_worktree: z2.boolean().optional().describe("Use git worktree"),
|
|
1763
|
+
assigned_agent: z2.enum(AGENT_TYPES).optional().describe("Assigned agent type"),
|
|
1764
|
+
order: z2.number().optional().describe("Queue order")
|
|
1765
|
+
},
|
|
1766
|
+
async (args) => {
|
|
1767
|
+
const task = dbGetTask(args.id);
|
|
1768
|
+
if (!task) {
|
|
1769
|
+
return {
|
|
1770
|
+
content: [{ type: "text", text: `Error: task "${args.id}" not found` }],
|
|
1771
|
+
isError: true
|
|
1772
|
+
};
|
|
1773
|
+
}
|
|
1774
|
+
const updates = { updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
1775
|
+
if (args.title !== void 0) updates.title = args.title;
|
|
1776
|
+
if (args.description !== void 0) updates.description = args.description;
|
|
1777
|
+
if (args.branch !== void 0) updates.branch = args.branch;
|
|
1778
|
+
if (args.use_worktree !== void 0) updates.useWorktree = args.use_worktree;
|
|
1779
|
+
if (args.assigned_agent !== void 0)
|
|
1780
|
+
updates.assignedAgent = args.assigned_agent;
|
|
1781
|
+
if (args.order !== void 0) updates.order = args.order;
|
|
1782
|
+
if (args.status !== void 0) {
|
|
1783
|
+
const newStatus = args.status;
|
|
1784
|
+
const wasDone = task.status === "done" || task.status === "cancelled";
|
|
1785
|
+
const isDone = newStatus === "done" || newStatus === "cancelled";
|
|
1786
|
+
updates.status = newStatus;
|
|
1787
|
+
if (isDone && !wasDone) updates.completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1788
|
+
if (!isDone && wasDone) updates.completedAt = void 0;
|
|
1789
|
+
}
|
|
1790
|
+
dbUpdateTask(args.id, updates);
|
|
1791
|
+
configManager2.notifyChanged();
|
|
1792
|
+
const updated = dbGetTask(args.id);
|
|
1793
|
+
return { content: [{ type: "text", text: JSON.stringify(updated, null, 2) }] };
|
|
1794
|
+
}
|
|
1795
|
+
);
|
|
1796
|
+
server.tool(
|
|
1797
|
+
"delete_task",
|
|
1798
|
+
"Delete a task by ID",
|
|
1799
|
+
{ id: V.id.describe("Task ID") },
|
|
1800
|
+
async (args) => {
|
|
1801
|
+
const task = dbGetTask(args.id);
|
|
1802
|
+
if (!task) {
|
|
1803
|
+
return {
|
|
1804
|
+
content: [{ type: "text", text: `Error: task "${args.id}" not found` }],
|
|
1805
|
+
isError: true
|
|
1806
|
+
};
|
|
1807
|
+
}
|
|
1808
|
+
dbDeleteTask(args.id);
|
|
1809
|
+
configManager2.notifyChanged();
|
|
1810
|
+
return { content: [{ type: "text", text: `Deleted task: ${task.title}` }] };
|
|
1811
|
+
}
|
|
1812
|
+
);
|
|
1813
|
+
server.tool(
|
|
1814
|
+
"get_my_context",
|
|
1815
|
+
"Get your current task and project context. Auto-detects based on your working directory. Call this at the start of a session to understand what you are working on.",
|
|
1816
|
+
{
|
|
1817
|
+
cwd: V.absolutePath.optional().describe(
|
|
1818
|
+
"Your current working directory (auto-detected if omitted). Used to match against known projects and task worktrees."
|
|
1819
|
+
),
|
|
1820
|
+
task_id: V.id.optional().describe("Specific task ID to get context for (overrides auto-detection)")
|
|
1821
|
+
},
|
|
1822
|
+
async (args) => {
|
|
1823
|
+
if (args.task_id) {
|
|
1824
|
+
const task = dbGetTask(args.task_id);
|
|
1825
|
+
if (!task) {
|
|
1826
|
+
return {
|
|
1827
|
+
content: [{ type: "text", text: `Error: task "${args.task_id}" not found` }],
|
|
1828
|
+
isError: true
|
|
1829
|
+
};
|
|
1830
|
+
}
|
|
1831
|
+
const project = dbGetProject(task.projectName);
|
|
1832
|
+
const siblingTasks = dbListTasks(task.projectName);
|
|
1833
|
+
return {
|
|
1834
|
+
content: [
|
|
1835
|
+
{
|
|
1836
|
+
type: "text",
|
|
1837
|
+
text: JSON.stringify(
|
|
1838
|
+
{
|
|
1839
|
+
task,
|
|
1840
|
+
project: project ?? void 0,
|
|
1841
|
+
siblingTasks: siblingTasks.filter((t) => t.id !== task.id).map((t) => ({
|
|
1842
|
+
id: t.id,
|
|
1843
|
+
title: t.title,
|
|
1844
|
+
status: t.status,
|
|
1845
|
+
branch: t.branch
|
|
1846
|
+
}))
|
|
1847
|
+
},
|
|
1848
|
+
null,
|
|
1849
|
+
2
|
|
1850
|
+
)
|
|
1851
|
+
}
|
|
1852
|
+
]
|
|
1853
|
+
};
|
|
1854
|
+
}
|
|
1855
|
+
const cwd = args.cwd || process.cwd();
|
|
1856
|
+
const normalizedCwd = path4.resolve(cwd);
|
|
1857
|
+
const projects = dbListProjects();
|
|
1858
|
+
let matchedProject = null;
|
|
1859
|
+
let matchLen = 0;
|
|
1860
|
+
for (const p of projects) {
|
|
1861
|
+
const normalizedPath = path4.resolve(p.path);
|
|
1862
|
+
if (normalizedCwd.startsWith(normalizedPath) && normalizedPath.length > matchLen) {
|
|
1863
|
+
matchedProject = p;
|
|
1864
|
+
matchLen = normalizedPath.length;
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
if (!matchedProject) {
|
|
1868
|
+
return {
|
|
1869
|
+
content: [
|
|
1870
|
+
{
|
|
1871
|
+
type: "text",
|
|
1872
|
+
text: JSON.stringify(
|
|
1873
|
+
{
|
|
1874
|
+
message: "No matching project found for current directory.",
|
|
1875
|
+
cwd: normalizedCwd,
|
|
1876
|
+
hint: "Use list_projects to see available projects, or pass a task_id directly."
|
|
1877
|
+
},
|
|
1878
|
+
null,
|
|
1879
|
+
2
|
|
1880
|
+
)
|
|
1881
|
+
}
|
|
1882
|
+
]
|
|
1883
|
+
};
|
|
1884
|
+
}
|
|
1885
|
+
const projectTasks = dbListTasks(matchedProject.name);
|
|
1886
|
+
let matchedTask = null;
|
|
1887
|
+
for (const t of projectTasks) {
|
|
1888
|
+
if (t.worktreePath) {
|
|
1889
|
+
const normalizedWorktree = path4.resolve(t.worktreePath);
|
|
1890
|
+
if (normalizedCwd.startsWith(normalizedWorktree)) {
|
|
1891
|
+
matchedTask = t;
|
|
1892
|
+
break;
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
if (!matchedTask) {
|
|
1897
|
+
matchedTask = projectTasks.find((t) => t.status === "in_progress") ?? null;
|
|
1898
|
+
}
|
|
1899
|
+
const result = {
|
|
1900
|
+
project: {
|
|
1901
|
+
name: matchedProject.name,
|
|
1902
|
+
path: matchedProject.path,
|
|
1903
|
+
preferredAgents: matchedProject.preferredAgents
|
|
1904
|
+
},
|
|
1905
|
+
cwd: normalizedCwd
|
|
1906
|
+
};
|
|
1907
|
+
if (matchedTask) {
|
|
1908
|
+
result.task = matchedTask;
|
|
1909
|
+
result.siblingTasks = projectTasks.filter((t) => t.id !== matchedTask.id).map((t) => ({ id: t.id, title: t.title, status: t.status, branch: t.branch }));
|
|
1910
|
+
} else {
|
|
1911
|
+
result.message = "No specific task matched. Showing all project tasks.";
|
|
1912
|
+
result.tasks = projectTasks.map((t) => ({
|
|
1913
|
+
id: t.id,
|
|
1914
|
+
title: t.title,
|
|
1915
|
+
status: t.status,
|
|
1916
|
+
branch: t.branch
|
|
1917
|
+
}));
|
|
1918
|
+
}
|
|
1919
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
1920
|
+
}
|
|
1921
|
+
);
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1924
|
+
// src/tools/projects.ts
|
|
1925
|
+
import { z as z3 } from "zod";
|
|
1926
|
+
var AGENT_TYPES2 = [
|
|
1927
|
+
"claude",
|
|
1928
|
+
"copilot",
|
|
1929
|
+
"codex",
|
|
1930
|
+
"opencode",
|
|
1931
|
+
"gemini"
|
|
1932
|
+
];
|
|
1933
|
+
function registerProjectTools(server, deps) {
|
|
1934
|
+
const { configManager: configManager2 } = deps;
|
|
1935
|
+
server.tool("list_projects", "List all projects", async () => {
|
|
1936
|
+
const projects = dbListProjects();
|
|
1937
|
+
return { content: [{ type: "text", text: JSON.stringify(projects, null, 2) }] };
|
|
1938
|
+
});
|
|
1939
|
+
server.tool(
|
|
1940
|
+
"create_project",
|
|
1941
|
+
"Create a new project",
|
|
1942
|
+
{
|
|
1943
|
+
name: V.name.describe("Project name (unique identifier)"),
|
|
1944
|
+
path: V.absolutePath.describe("Absolute path to project directory"),
|
|
1945
|
+
preferred_agents: z3.array(z3.enum(AGENT_TYPES2)).optional().describe("Preferred agent types"),
|
|
1946
|
+
icon: V.shortText.optional().describe("Lucide icon name"),
|
|
1947
|
+
icon_color: V.hexColor.optional().describe("Hex color for icon")
|
|
1948
|
+
},
|
|
1949
|
+
async (args) => {
|
|
1950
|
+
if (dbGetProject(args.name)) {
|
|
1951
|
+
return {
|
|
1952
|
+
content: [{ type: "text", text: `Error: project "${args.name}" already exists` }],
|
|
1953
|
+
isError: true
|
|
1954
|
+
};
|
|
1955
|
+
}
|
|
1956
|
+
const project = {
|
|
1957
|
+
name: args.name,
|
|
1958
|
+
path: args.path,
|
|
1959
|
+
preferredAgents: args.preferred_agents ?? [],
|
|
1960
|
+
...args.icon && { icon: args.icon },
|
|
1961
|
+
...args.icon_color && { iconColor: args.icon_color }
|
|
1962
|
+
};
|
|
1963
|
+
dbInsertProject(project);
|
|
1964
|
+
configManager2.notifyChanged();
|
|
1965
|
+
return { content: [{ type: "text", text: JSON.stringify(project, null, 2) }] };
|
|
1966
|
+
}
|
|
1967
|
+
);
|
|
1968
|
+
server.tool(
|
|
1969
|
+
"update_project",
|
|
1970
|
+
"Update a project's properties",
|
|
1971
|
+
{
|
|
1972
|
+
name: V.name.describe("Project name (identifier, cannot be changed)"),
|
|
1973
|
+
path: V.absolutePath.optional().describe("New project path"),
|
|
1974
|
+
preferred_agents: z3.array(z3.enum(AGENT_TYPES2)).optional().describe("Preferred agent types"),
|
|
1975
|
+
icon: V.shortText.optional().describe("Lucide icon name"),
|
|
1976
|
+
icon_color: V.hexColor.optional().describe("Hex color for icon")
|
|
1977
|
+
},
|
|
1978
|
+
async (args) => {
|
|
1979
|
+
if (!dbGetProject(args.name)) {
|
|
1980
|
+
return {
|
|
1981
|
+
content: [{ type: "text", text: `Error: project "${args.name}" not found` }],
|
|
1982
|
+
isError: true
|
|
1983
|
+
};
|
|
1984
|
+
}
|
|
1985
|
+
const updates = {};
|
|
1986
|
+
if (args.path !== void 0) updates.path = args.path;
|
|
1987
|
+
if (args.preferred_agents !== void 0)
|
|
1988
|
+
updates.preferredAgents = args.preferred_agents;
|
|
1989
|
+
if (args.icon !== void 0) updates.icon = args.icon;
|
|
1990
|
+
if (args.icon_color !== void 0) updates.iconColor = args.icon_color;
|
|
1991
|
+
dbUpdateProject(args.name, updates);
|
|
1992
|
+
configManager2.notifyChanged();
|
|
1993
|
+
const updated = dbGetProject(args.name);
|
|
1994
|
+
return { content: [{ type: "text", text: JSON.stringify(updated, null, 2) }] };
|
|
1995
|
+
}
|
|
1996
|
+
);
|
|
1997
|
+
server.tool(
|
|
1998
|
+
"delete_project",
|
|
1999
|
+
"Delete a project and all its tasks",
|
|
2000
|
+
{ name: V.name.describe("Project name") },
|
|
2001
|
+
async (args) => {
|
|
2002
|
+
if (!dbGetProject(args.name)) {
|
|
2003
|
+
return {
|
|
2004
|
+
content: [{ type: "text", text: `Error: project "${args.name}" not found` }],
|
|
2005
|
+
isError: true
|
|
2006
|
+
};
|
|
2007
|
+
}
|
|
2008
|
+
dbDeleteProject(args.name);
|
|
2009
|
+
configManager2.notifyChanged();
|
|
2010
|
+
return { content: [{ type: "text", text: `Deleted project: ${args.name}` }] };
|
|
2011
|
+
}
|
|
2012
|
+
);
|
|
2013
|
+
}
|
|
2014
|
+
|
|
2015
|
+
// src/tools/sessions.ts
|
|
2016
|
+
import { z as z4 } from "zod";
|
|
2017
|
+
var AGENT_TYPES3 = [
|
|
2018
|
+
"claude",
|
|
2019
|
+
"copilot",
|
|
2020
|
+
"codex",
|
|
2021
|
+
"opencode",
|
|
2022
|
+
"gemini"
|
|
2023
|
+
];
|
|
2024
|
+
function registerSessionTools(server, deps) {
|
|
2025
|
+
const { ptyManager: ptyManager2 } = deps;
|
|
2026
|
+
server.tool("list_sessions", "List all active terminal sessions", async () => {
|
|
2027
|
+
const sessions = ptyManager2.getActiveSessions();
|
|
2028
|
+
const summary = sessions.map((s) => ({
|
|
2029
|
+
id: s.id,
|
|
2030
|
+
agentType: s.agentType,
|
|
2031
|
+
projectName: s.projectName,
|
|
2032
|
+
status: s.status,
|
|
2033
|
+
displayName: s.displayName,
|
|
2034
|
+
branch: s.branch,
|
|
2035
|
+
pid: s.pid
|
|
2036
|
+
}));
|
|
2037
|
+
return { content: [{ type: "text", text: JSON.stringify(summary, null, 2) }] };
|
|
2038
|
+
});
|
|
2039
|
+
server.tool(
|
|
2040
|
+
"launch_agent",
|
|
2041
|
+
"Launch an AI agent in a new terminal session",
|
|
2042
|
+
{
|
|
2043
|
+
agent_type: z4.enum(AGENT_TYPES3).describe("Agent type to launch"),
|
|
2044
|
+
project_name: V.name.describe("Project name"),
|
|
2045
|
+
project_path: V.absolutePath.describe("Absolute path to project directory"),
|
|
2046
|
+
prompt: V.prompt.optional().describe("Initial prompt to send to the agent"),
|
|
2047
|
+
branch: V.shortText.optional().describe("Git branch to checkout"),
|
|
2048
|
+
use_worktree: z4.boolean().optional().describe("Create a git worktree"),
|
|
2049
|
+
display_name: V.shortText.optional().describe("Display name for the session")
|
|
2050
|
+
},
|
|
2051
|
+
async (args) => {
|
|
2052
|
+
const payload = {
|
|
2053
|
+
agentType: args.agent_type,
|
|
2054
|
+
projectName: args.project_name,
|
|
2055
|
+
projectPath: args.project_path,
|
|
2056
|
+
...args.prompt && { initialPrompt: args.prompt },
|
|
2057
|
+
...args.branch && { branch: args.branch },
|
|
2058
|
+
...args.use_worktree && { useWorktree: args.use_worktree },
|
|
2059
|
+
...args.display_name && { displayName: args.display_name }
|
|
2060
|
+
};
|
|
2061
|
+
try {
|
|
2062
|
+
const session = ptyManager2.createPty(payload);
|
|
2063
|
+
return {
|
|
2064
|
+
content: [
|
|
2065
|
+
{
|
|
2066
|
+
type: "text",
|
|
2067
|
+
text: JSON.stringify(
|
|
2068
|
+
{
|
|
2069
|
+
id: session.id,
|
|
2070
|
+
agentType: session.agentType,
|
|
2071
|
+
projectName: session.projectName,
|
|
2072
|
+
pid: session.pid,
|
|
2073
|
+
status: session.status
|
|
2074
|
+
},
|
|
2075
|
+
null,
|
|
2076
|
+
2
|
|
2077
|
+
)
|
|
2078
|
+
}
|
|
2079
|
+
]
|
|
2080
|
+
};
|
|
2081
|
+
} catch (err) {
|
|
2082
|
+
return { content: [{ type: "text", text: `Error launching agent: ${err}` }], isError: true };
|
|
2083
|
+
}
|
|
2084
|
+
}
|
|
2085
|
+
);
|
|
2086
|
+
server.tool(
|
|
2087
|
+
"kill_session",
|
|
2088
|
+
"Kill a terminal session",
|
|
2089
|
+
{ id: V.id.describe("Session ID to kill") },
|
|
2090
|
+
async (args) => {
|
|
2091
|
+
try {
|
|
2092
|
+
ptyManager2.killPty(args.id);
|
|
2093
|
+
return { content: [{ type: "text", text: `Killed session: ${args.id}` }] };
|
|
2094
|
+
} catch (err) {
|
|
2095
|
+
return { content: [{ type: "text", text: `Error killing session: ${err}` }], isError: true };
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
2098
|
+
);
|
|
2099
|
+
server.tool(
|
|
2100
|
+
"write_to_terminal",
|
|
2101
|
+
"Send input to a running terminal session",
|
|
2102
|
+
{
|
|
2103
|
+
id: V.id.describe("Session ID"),
|
|
2104
|
+
data: z4.string().max(5e4, "Data must be 50000 characters or less").describe("Data to write (text input to send to the agent)")
|
|
2105
|
+
},
|
|
2106
|
+
async (args) => {
|
|
2107
|
+
try {
|
|
2108
|
+
ptyManager2.writeToPty(args.id, args.data);
|
|
2109
|
+
return { content: [{ type: "text", text: `Wrote to session: ${args.id}` }] };
|
|
2110
|
+
} catch (err) {
|
|
2111
|
+
return {
|
|
2112
|
+
content: [{ type: "text", text: `Error writing to terminal: ${err}` }],
|
|
2113
|
+
isError: true
|
|
2114
|
+
};
|
|
2115
|
+
}
|
|
2116
|
+
}
|
|
2117
|
+
);
|
|
2118
|
+
}
|
|
2119
|
+
|
|
2120
|
+
// src/tools/workflows.ts
|
|
2121
|
+
import crypto4 from "crypto";
|
|
2122
|
+
import { z as z5 } from "zod";
|
|
2123
|
+
var launchAgentConfigSchema = z5.object({
|
|
2124
|
+
agentType: z5.enum(["claude", "copilot", "codex", "opencode", "gemini"]),
|
|
2125
|
+
projectName: V.name,
|
|
2126
|
+
projectPath: V.absolutePath,
|
|
2127
|
+
args: z5.array(V.shortText).optional(),
|
|
2128
|
+
displayName: V.shortText.optional(),
|
|
2129
|
+
branch: V.shortText.optional(),
|
|
2130
|
+
useWorktree: z5.boolean().optional(),
|
|
2131
|
+
remoteHostId: V.id.optional(),
|
|
2132
|
+
prompt: V.prompt.optional(),
|
|
2133
|
+
promptDelayMs: z5.number().optional(),
|
|
2134
|
+
taskId: V.id.optional(),
|
|
2135
|
+
taskFromQueue: z5.boolean().optional()
|
|
2136
|
+
});
|
|
2137
|
+
var triggerConfigSchema = z5.union([
|
|
2138
|
+
z5.object({ triggerType: z5.literal("manual") }),
|
|
2139
|
+
z5.object({ triggerType: z5.literal("once"), runAt: V.shortText }),
|
|
2140
|
+
z5.object({
|
|
2141
|
+
triggerType: z5.literal("recurring"),
|
|
2142
|
+
cron: V.shortText,
|
|
2143
|
+
timezone: V.shortText.optional()
|
|
2144
|
+
}),
|
|
2145
|
+
z5.object({ triggerType: z5.literal("taskCreated"), projectFilter: V.name.optional() }),
|
|
2146
|
+
z5.object({
|
|
2147
|
+
triggerType: z5.literal("taskStatusChanged"),
|
|
2148
|
+
projectFilter: V.name.optional(),
|
|
2149
|
+
fromStatus: z5.enum(["todo", "in_progress", "in_review", "done", "cancelled"]).optional(),
|
|
2150
|
+
toStatus: z5.enum(["todo", "in_progress", "in_review", "done", "cancelled"]).optional()
|
|
2151
|
+
})
|
|
2152
|
+
]);
|
|
2153
|
+
var nodeSchema = z5.object({
|
|
2154
|
+
id: V.id,
|
|
2155
|
+
type: z5.enum(["trigger", "launchAgent"]),
|
|
2156
|
+
label: V.shortText,
|
|
2157
|
+
config: z5.record(z5.unknown()),
|
|
2158
|
+
position: z5.object({ x: z5.number(), y: z5.number() })
|
|
2159
|
+
});
|
|
2160
|
+
var edgeSchema = z5.object({
|
|
2161
|
+
id: V.id,
|
|
2162
|
+
source: V.id,
|
|
2163
|
+
target: V.id
|
|
2164
|
+
});
|
|
2165
|
+
function buildGraphFromFlat(trigger, actions) {
|
|
2166
|
+
const nodes = [];
|
|
2167
|
+
const edges = [];
|
|
2168
|
+
const triggerNode = {
|
|
2169
|
+
id: crypto4.randomUUID(),
|
|
2170
|
+
type: "trigger",
|
|
2171
|
+
label: trigger.triggerType === "manual" ? "Manual Trigger" : trigger.triggerType === "once" ? "Schedule (Once)" : trigger.triggerType === "recurring" ? "Schedule (Recurring)" : trigger.triggerType === "taskCreated" ? "When Task Created" : trigger.triggerType === "taskStatusChanged" ? "When Task Status Changes" : "Trigger",
|
|
2172
|
+
config: trigger,
|
|
2173
|
+
position: { x: 0, y: 0 }
|
|
2174
|
+
};
|
|
2175
|
+
nodes.push(triggerNode);
|
|
2176
|
+
let prevId = triggerNode.id;
|
|
2177
|
+
const NODE_GAP = 140;
|
|
2178
|
+
for (let i = 0; i < actions.length; i++) {
|
|
2179
|
+
const action = actions[i];
|
|
2180
|
+
const nodeId = crypto4.randomUUID();
|
|
2181
|
+
nodes.push({
|
|
2182
|
+
id: nodeId,
|
|
2183
|
+
type: "launchAgent",
|
|
2184
|
+
label: `Launch ${action.agentType}`,
|
|
2185
|
+
config: action,
|
|
2186
|
+
position: { x: 0, y: (i + 1) * NODE_GAP }
|
|
2187
|
+
});
|
|
2188
|
+
edges.push({
|
|
2189
|
+
id: crypto4.randomUUID(),
|
|
2190
|
+
source: prevId,
|
|
2191
|
+
target: nodeId
|
|
2192
|
+
});
|
|
2193
|
+
prevId = nodeId;
|
|
2194
|
+
}
|
|
2195
|
+
return { nodes, edges };
|
|
2196
|
+
}
|
|
2197
|
+
function registerWorkflowTools(server, deps) {
|
|
2198
|
+
const { configManager: configManager2, scheduler: scheduler2 } = deps;
|
|
2199
|
+
server.tool("list_workflows", "List all workflows", async () => {
|
|
2200
|
+
const workflows = dbListWorkflows();
|
|
2201
|
+
return { content: [{ type: "text", text: JSON.stringify(workflows, null, 2) }] };
|
|
2202
|
+
});
|
|
2203
|
+
server.tool(
|
|
2204
|
+
"create_workflow",
|
|
2205
|
+
"Create a new workflow. Accepts either full nodes/edges or a convenience flat format (trigger + actions array).",
|
|
2206
|
+
z5.object({
|
|
2207
|
+
name: V.title.describe("Workflow name"),
|
|
2208
|
+
trigger: triggerConfigSchema.optional().describe("Trigger config (convenience mode). Defaults to manual."),
|
|
2209
|
+
actions: z5.array(launchAgentConfigSchema).optional().describe("Actions to execute (convenience mode). Auto-generates graph."),
|
|
2210
|
+
nodes: z5.array(nodeSchema).optional().describe("Full graph nodes (advanced mode)"),
|
|
2211
|
+
edges: z5.array(edgeSchema).optional().describe("Full graph edges (advanced mode)"),
|
|
2212
|
+
icon: V.shortText.optional().describe("Lucide icon name (default: zap)"),
|
|
2213
|
+
icon_color: V.hexColor.optional().describe("Hex color (default: #6366f1)"),
|
|
2214
|
+
enabled: z5.boolean().optional().describe("Whether workflow is enabled (default: true)"),
|
|
2215
|
+
stagger_delay_ms: z5.number().optional().describe("Delay in ms between actions")
|
|
2216
|
+
}),
|
|
2217
|
+
async (args) => {
|
|
2218
|
+
let nodes;
|
|
2219
|
+
let edges;
|
|
2220
|
+
if (args.nodes && args.edges) {
|
|
2221
|
+
nodes = args.nodes;
|
|
2222
|
+
edges = args.edges;
|
|
2223
|
+
} else {
|
|
2224
|
+
const trigger = args.trigger ?? { triggerType: "manual" };
|
|
2225
|
+
const actions = args.actions ?? [];
|
|
2226
|
+
const graph = buildGraphFromFlat(trigger, actions);
|
|
2227
|
+
nodes = graph.nodes;
|
|
2228
|
+
edges = graph.edges;
|
|
2229
|
+
}
|
|
2230
|
+
const workflow = {
|
|
2231
|
+
id: crypto4.randomUUID(),
|
|
2232
|
+
name: args.name,
|
|
2233
|
+
icon: args.icon ?? "zap",
|
|
2234
|
+
iconColor: args.icon_color ?? "#6366f1",
|
|
2235
|
+
nodes,
|
|
2236
|
+
edges,
|
|
2237
|
+
enabled: args.enabled ?? true,
|
|
2238
|
+
...args.stagger_delay_ms && { staggerDelayMs: args.stagger_delay_ms }
|
|
2239
|
+
};
|
|
2240
|
+
dbInsertWorkflow(workflow);
|
|
2241
|
+
scheduler2.syncSchedules(dbListWorkflows());
|
|
2242
|
+
configManager2.notifyChanged();
|
|
2243
|
+
return { content: [{ type: "text", text: JSON.stringify(workflow, null, 2) }] };
|
|
2244
|
+
}
|
|
2245
|
+
);
|
|
2246
|
+
server.tool(
|
|
2247
|
+
"update_workflow",
|
|
2248
|
+
"Update a workflow's properties",
|
|
2249
|
+
z5.object({
|
|
2250
|
+
id: V.id.describe("Workflow ID"),
|
|
2251
|
+
name: V.title.optional(),
|
|
2252
|
+
nodes: z5.array(nodeSchema).optional(),
|
|
2253
|
+
edges: z5.array(edgeSchema).optional(),
|
|
2254
|
+
icon: V.shortText.optional(),
|
|
2255
|
+
icon_color: V.hexColor.optional(),
|
|
2256
|
+
enabled: z5.boolean().optional(),
|
|
2257
|
+
stagger_delay_ms: z5.number().optional()
|
|
2258
|
+
}),
|
|
2259
|
+
async (args) => {
|
|
2260
|
+
const workflows = dbListWorkflows();
|
|
2261
|
+
const workflow = workflows.find((w) => w.id === args.id);
|
|
2262
|
+
if (!workflow) {
|
|
2263
|
+
return {
|
|
2264
|
+
content: [{ type: "text", text: `Error: workflow "${args.id}" not found` }],
|
|
2265
|
+
isError: true
|
|
2266
|
+
};
|
|
2267
|
+
}
|
|
2268
|
+
const updates = {};
|
|
2269
|
+
if (args.name !== void 0) updates.name = args.name;
|
|
2270
|
+
if (args.nodes !== void 0) updates.nodes = args.nodes;
|
|
2271
|
+
if (args.edges !== void 0) updates.edges = args.edges;
|
|
2272
|
+
if (args.icon !== void 0) updates.icon = args.icon;
|
|
2273
|
+
if (args.icon_color !== void 0) updates.iconColor = args.icon_color;
|
|
2274
|
+
if (args.enabled !== void 0) updates.enabled = args.enabled;
|
|
2275
|
+
if (args.stagger_delay_ms !== void 0) updates.staggerDelayMs = args.stagger_delay_ms;
|
|
2276
|
+
dbUpdateWorkflow(args.id, updates);
|
|
2277
|
+
scheduler2.syncSchedules(dbListWorkflows());
|
|
2278
|
+
configManager2.notifyChanged();
|
|
2279
|
+
return {
|
|
2280
|
+
content: [{ type: "text", text: JSON.stringify({ ...workflow, ...updates }, null, 2) }]
|
|
2281
|
+
};
|
|
2282
|
+
}
|
|
2283
|
+
);
|
|
2284
|
+
server.tool(
|
|
2285
|
+
"delete_workflow",
|
|
2286
|
+
"Delete a workflow",
|
|
2287
|
+
{ id: V.id.describe("Workflow ID") },
|
|
2288
|
+
async (args) => {
|
|
2289
|
+
const workflows = dbListWorkflows();
|
|
2290
|
+
const workflow = workflows.find((w) => w.id === args.id);
|
|
2291
|
+
if (!workflow) {
|
|
2292
|
+
return {
|
|
2293
|
+
content: [{ type: "text", text: `Error: workflow "${args.id}" not found` }],
|
|
2294
|
+
isError: true
|
|
2295
|
+
};
|
|
2296
|
+
}
|
|
2297
|
+
dbDeleteWorkflow(args.id);
|
|
2298
|
+
scheduler2.syncSchedules(dbListWorkflows());
|
|
2299
|
+
configManager2.notifyChanged();
|
|
2300
|
+
return { content: [{ type: "text", text: `Deleted workflow: ${workflow.name}` }] };
|
|
2301
|
+
}
|
|
2302
|
+
);
|
|
2303
|
+
}
|
|
2304
|
+
|
|
2305
|
+
// src/tools/git.ts
|
|
2306
|
+
function registerGitTools(server) {
|
|
2307
|
+
server.tool(
|
|
2308
|
+
"list_branches",
|
|
2309
|
+
"List git branches for a project",
|
|
2310
|
+
{ project_path: V.absolutePath.describe("Absolute path to project directory") },
|
|
2311
|
+
async (args) => {
|
|
2312
|
+
try {
|
|
2313
|
+
const local = listBranches(args.project_path);
|
|
2314
|
+
const current = getGitBranch(args.project_path);
|
|
2315
|
+
return {
|
|
2316
|
+
content: [{ type: "text", text: JSON.stringify({ current, branches: local }, null, 2) }]
|
|
2317
|
+
};
|
|
2318
|
+
} catch (err) {
|
|
2319
|
+
return {
|
|
2320
|
+
content: [{ type: "text", text: `Error listing branches: ${err}` }],
|
|
2321
|
+
isError: true
|
|
2322
|
+
};
|
|
2323
|
+
}
|
|
2324
|
+
}
|
|
2325
|
+
);
|
|
2326
|
+
server.tool(
|
|
2327
|
+
"get_diff",
|
|
2328
|
+
"Get git diff for a project (staged and unstaged changes)",
|
|
2329
|
+
{ project_path: V.absolutePath.describe("Absolute path to project directory") },
|
|
2330
|
+
async (args) => {
|
|
2331
|
+
try {
|
|
2332
|
+
const result = getGitDiffFull(args.project_path);
|
|
2333
|
+
if (!result) {
|
|
2334
|
+
return {
|
|
2335
|
+
content: [{ type: "text", text: "No changes detected or not a git repository" }]
|
|
2336
|
+
};
|
|
2337
|
+
}
|
|
2338
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
2339
|
+
} catch (err) {
|
|
2340
|
+
return { content: [{ type: "text", text: `Error getting diff: ${err}` }], isError: true };
|
|
2341
|
+
}
|
|
2342
|
+
}
|
|
2343
|
+
);
|
|
2344
|
+
}
|
|
2345
|
+
|
|
2346
|
+
// src/tools/config.ts
|
|
2347
|
+
function registerConfigTools(server, deps) {
|
|
2348
|
+
const { configManager: configManager2 } = deps;
|
|
2349
|
+
server.tool(
|
|
2350
|
+
"get_config",
|
|
2351
|
+
"Get the full VibeGrid configuration (projects, tasks, workflows, settings)",
|
|
2352
|
+
async () => {
|
|
2353
|
+
const config = configManager2.loadConfig();
|
|
2354
|
+
return { content: [{ type: "text", text: JSON.stringify(config, null, 2) }] };
|
|
2355
|
+
}
|
|
2356
|
+
);
|
|
2357
|
+
}
|
|
2358
|
+
|
|
2359
|
+
// src/server.ts
|
|
2360
|
+
function createMcpServer(deps, version) {
|
|
2361
|
+
const server = new McpServer({ name: "vibegrid", version }, { capabilities: { tools: {} } });
|
|
2362
|
+
registerGitTools(server);
|
|
2363
|
+
registerConfigTools(server, deps);
|
|
2364
|
+
registerProjectTools(server, deps);
|
|
2365
|
+
registerTaskTools(server, deps);
|
|
2366
|
+
registerSessionTools(server, deps);
|
|
2367
|
+
registerWorkflowTools(server, deps);
|
|
2368
|
+
return server;
|
|
2369
|
+
}
|
|
2370
|
+
|
|
2371
|
+
// src/index.ts
|
|
2372
|
+
var _origError = console.error;
|
|
2373
|
+
console.log = (...args) => _origError("[mcp]", ...args);
|
|
2374
|
+
console.info = (...args) => _origError("[mcp]", ...args);
|
|
2375
|
+
console.debug = (...args) => _origError("[mcp:debug]", ...args);
|
|
2376
|
+
console.warn = (...args) => _origError("[mcp:warn]", ...args);
|
|
2377
|
+
console.error = (...args) => _origError("[mcp:error]", ...args);
|
|
2378
|
+
async function main() {
|
|
2379
|
+
configManager.init();
|
|
2380
|
+
const config = configManager.loadConfig();
|
|
2381
|
+
if (config.agentCommands) {
|
|
2382
|
+
ptyManager.setAgentCommands(config.agentCommands);
|
|
2383
|
+
}
|
|
2384
|
+
ptyManager.setRemoteHosts(config.remoteHosts ?? []);
|
|
2385
|
+
scheduler.syncSchedules(config.workflows ?? []);
|
|
2386
|
+
const server = createMcpServer({ configManager, ptyManager, scheduler }, "0.1.1");
|
|
2387
|
+
const transport = new StdioServerTransport();
|
|
2388
|
+
await server.connect(transport);
|
|
2389
|
+
transport.onclose = () => {
|
|
2390
|
+
scheduler.stopAll();
|
|
2391
|
+
ptyManager.killAll();
|
|
2392
|
+
configManager.close();
|
|
2393
|
+
process.exit(0);
|
|
2394
|
+
};
|
|
2395
|
+
}
|
|
2396
|
+
main().catch((err) => {
|
|
2397
|
+
console.error("Failed to start MCP server:", err);
|
|
2398
|
+
process.exit(1);
|
|
2399
|
+
});
|