@vibegrid/mcp 0.1.3 → 0.2.0

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.
Files changed (2) hide show
  1. package/dist/index.js +999 -982
  2. package/package.json +2 -3
package/dist/index.js CHANGED
@@ -56,72 +56,6 @@ var DEFAULT_WORKSPACE = {
56
56
  iconColor: "#6b7280",
57
57
  order: 0
58
58
  };
59
- var IPC = {
60
- TERMINAL_CREATE: "terminal:create",
61
- TERMINAL_WRITE: "terminal:write",
62
- TERMINAL_RESIZE: "terminal:resize",
63
- TERMINAL_KILL: "terminal:kill",
64
- TERMINAL_DATA: "terminal:data",
65
- TERMINAL_EXIT: "terminal:exit",
66
- CONFIG_LOAD: "config:load",
67
- CONFIG_SAVE: "config:save",
68
- CONFIG_CHANGED: "config:changed",
69
- SESSIONS_GET_PREVIOUS: "sessions:getPrevious",
70
- SESSIONS_CLEAR: "sessions:clear",
71
- SESSIONS_GET_RECENT: "sessions:getRecent",
72
- DIALOG_OPEN_DIRECTORY: "dialog:openDirectory",
73
- IDE_DETECT: "ide:detect",
74
- IDE_OPEN: "ide:open",
75
- GIT_LIST_BRANCHES: "git:listBranches",
76
- GIT_LIST_REMOTE_BRANCHES: "git:listRemoteBranches",
77
- GIT_CREATE_WORKTREE: "git:createWorktree",
78
- GIT_REMOVE_WORKTREE: "git:removeWorktree",
79
- GIT_WORKTREE_DIRTY: "git:worktreeDirty",
80
- GIT_LIST_WORKTREES: "git:listWorktrees",
81
- WORKTREE_CONFIRM_CLEANUP: "worktree:confirmCleanup",
82
- GIT_DIFF_STAT: "git:diffStat",
83
- GIT_DIFF_FULL: "git:diffFull",
84
- GIT_COMMIT: "git:commit",
85
- GIT_PUSH: "git:push",
86
- DIALOG_OPEN_FILE: "dialog:openFile",
87
- SCHEDULER_EXECUTE: "scheduler:execute",
88
- SCHEDULER_MISSED: "scheduler:missed",
89
- SCHEDULER_GET_LOG: "scheduler:getLog",
90
- SCHEDULER_GET_NEXT_RUN: "scheduler:getNextRun",
91
- WORKFLOW_EXECUTION_COMPLETE: "workflow:executionComplete",
92
- WINDOW_MINIMIZE: "window:minimize",
93
- WINDOW_MAXIMIZE: "window:maximize",
94
- WINDOW_CLOSE: "window:close",
95
- WIDGET_STATUS_UPDATE: "widget:status-update",
96
- WIDGET_FOCUS_TERMINAL: "widget:focus-terminal",
97
- WIDGET_HIDE: "widget:hide",
98
- WIDGET_TOGGLE: "widget:toggle",
99
- WIDGET_RENDERER_STATUS: "widget:renderer-status",
100
- WIDGET_SET_ENABLED: "widget:set-enabled",
101
- WIDGET_PERMISSION_REQUEST: "widget:permission-request",
102
- WIDGET_PERMISSION_RESPONSE: "widget:permission-response",
103
- WIDGET_PERMISSION_CANCELLED: "widget:permission-cancelled",
104
- SHELL_CREATE: "shell:create",
105
- UPDATE_DOWNLOADED: "update:downloaded",
106
- UPDATE_INSTALL: "update:install",
107
- TASK_IMAGE_SAVE: "task:imageSave",
108
- TASK_IMAGE_DELETE: "task:imageDelete",
109
- TASK_IMAGE_GET_PATH: "task:imageGetPath",
110
- TASK_IMAGE_CLEANUP: "task:imageCleanup",
111
- DIALOG_OPEN_IMAGE: "dialog:openImage",
112
- SESSION_ARCHIVE: "session:archive",
113
- SESSION_UNARCHIVE: "session:unarchive",
114
- SESSION_LIST_ARCHIVED: "session:listArchived",
115
- HEADLESS_CREATE: "headless:create",
116
- HEADLESS_KILL: "headless:kill",
117
- HEADLESS_DATA: "headless:data",
118
- HEADLESS_EXIT: "headless:exit",
119
- SCRIPT_EXECUTE: "script:execute",
120
- WORKFLOW_RUN_SAVE: "workflowRun:save",
121
- WORKFLOW_RUN_LIST: "workflowRun:list",
122
- WORKFLOW_RUN_LIST_BY_TASK: "workflowRun:listByTask",
123
- AGENT_DETECT_INSTALLED: "agent:detectInstalled"
124
- };
125
59
 
126
60
  // ../server/src/database.ts
127
61
  var CONFIG_DIR = path.join(os.homedir(), ".vibegrid");
@@ -801,6 +735,50 @@ function dbUpdateWorkflow(id, updates) {
801
735
  function dbDeleteWorkflow(id) {
802
736
  getDb().prepare("DELETE FROM workflows WHERE id = ?").run(id);
803
737
  }
738
+ function dbListWorkspaces() {
739
+ const rows = getDb().prepare('SELECT * FROM workspaces ORDER BY "order"').all();
740
+ return rows.map(rowToWorkspace);
741
+ }
742
+ function dbInsertWorkspace(workspace) {
743
+ getDb().prepare(`INSERT INTO workspaces (id, name, icon, icon_color, "order") VALUES (?, ?, ?, ?, ?)`).run(
744
+ workspace.id,
745
+ workspace.name,
746
+ workspace.icon ?? null,
747
+ workspace.iconColor ?? null,
748
+ workspace.order
749
+ );
750
+ }
751
+ function dbUpdateWorkspace(id, updates) {
752
+ const sets = [];
753
+ const params = [];
754
+ if (updates.name !== void 0) {
755
+ sets.push("name = ?");
756
+ params.push(updates.name);
757
+ }
758
+ if (updates.icon !== void 0) {
759
+ sets.push("icon = ?");
760
+ params.push(updates.icon);
761
+ }
762
+ if (updates.iconColor !== void 0) {
763
+ sets.push("icon_color = ?");
764
+ params.push(updates.iconColor);
765
+ }
766
+ if (updates.order !== void 0) {
767
+ sets.push('"order" = ?');
768
+ params.push(updates.order);
769
+ }
770
+ if (sets.length === 0) return;
771
+ params.push(id);
772
+ getDb().prepare(`UPDATE workspaces SET ${sets.join(", ")} WHERE id = ?`).run(...params);
773
+ }
774
+ function dbDeleteWorkspace(id) {
775
+ const d = getDb();
776
+ d.transaction(() => {
777
+ d.prepare("UPDATE projects SET workspace_id = 'personal' WHERE workspace_id = ?").run(id);
778
+ d.prepare("UPDATE workflows SET workspace_id = 'personal' WHERE workspace_id = ?").run(id);
779
+ d.prepare("DELETE FROM workspaces WHERE id = ?").run(id);
780
+ })();
781
+ }
804
782
  function rowToTask(r) {
805
783
  return {
806
784
  id: r.id,
@@ -854,6 +832,67 @@ function rowToWorkspace(r) {
854
832
  order: r.order
855
833
  };
856
834
  }
835
+ function listWorkflowRuns(workflowId, limit = 20) {
836
+ const d = getDb();
837
+ const rows = d.prepare("SELECT * FROM workflow_runs WHERE workflow_id = ? ORDER BY started_at DESC LIMIT ?").all(workflowId, limit);
838
+ return rows.map((r) => {
839
+ const nodeRows = d.prepare("SELECT * FROM workflow_run_nodes WHERE run_id = ?").all(r.id);
840
+ return {
841
+ workflowId: r.workflow_id,
842
+ startedAt: r.started_at,
843
+ ...r.completed_at != null && { completedAt: r.completed_at },
844
+ status: r.status,
845
+ ...r.trigger_task_id != null && { triggerTaskId: r.trigger_task_id },
846
+ nodeStates: nodeRows.map((n) => ({
847
+ nodeId: n.node_id,
848
+ status: n.status,
849
+ ...n.started_at != null && { startedAt: n.started_at },
850
+ ...n.completed_at != null && { completedAt: n.completed_at },
851
+ ...n.session_id != null && { sessionId: n.session_id },
852
+ ...n.error != null && { error: n.error },
853
+ ...n.logs != null && { logs: n.logs },
854
+ ...n.task_id != null && { taskId: n.task_id },
855
+ ...n.agent_session_id != null && { agentSessionId: n.agent_session_id }
856
+ }))
857
+ };
858
+ });
859
+ }
860
+ function listWorkflowRunsByTask(taskId, limit = 20) {
861
+ const d = getDb();
862
+ const rows = d.prepare(
863
+ `
864
+ SELECT DISTINCT wr.*, w.name as workflow_name
865
+ FROM workflow_runs wr
866
+ LEFT JOIN workflows w ON w.id = wr.workflow_id
867
+ WHERE wr.trigger_task_id = ?
868
+ OR wr.id IN (SELECT run_id FROM workflow_run_nodes WHERE task_id = ?)
869
+ ORDER BY wr.started_at DESC
870
+ LIMIT ?
871
+ `
872
+ ).all(taskId, taskId, limit);
873
+ return rows.map((r) => {
874
+ const nodeRows = d.prepare("SELECT * FROM workflow_run_nodes WHERE run_id = ?").all(r.id);
875
+ return {
876
+ workflowId: r.workflow_id,
877
+ startedAt: r.started_at,
878
+ ...r.completed_at != null && { completedAt: r.completed_at },
879
+ status: r.status,
880
+ ...r.trigger_task_id != null && { triggerTaskId: r.trigger_task_id },
881
+ ...r.workflow_name != null && { workflowName: r.workflow_name },
882
+ nodeStates: nodeRows.map((n) => ({
883
+ nodeId: n.node_id,
884
+ status: n.status,
885
+ ...n.started_at != null && { startedAt: n.started_at },
886
+ ...n.completed_at != null && { completedAt: n.completed_at },
887
+ ...n.session_id != null && { sessionId: n.session_id },
888
+ ...n.error != null && { error: n.error },
889
+ ...n.logs != null && { logs: n.logs },
890
+ ...n.task_id != null && { taskId: n.task_id },
891
+ ...n.agent_session_id != null && { agentSessionId: n.agent_session_id }
892
+ }))
893
+ };
894
+ });
895
+ }
857
896
 
858
897
  // ../server/src/config-manager.ts
859
898
  var DB_DIR = path2.join(os2.homedir(), ".vibegrid");
@@ -941,840 +980,99 @@ var ConfigManager = class {
941
980
  };
942
981
  var configManager = new ConfigManager();
943
982
 
944
- // ../server/src/pty-manager.ts
945
- import * as pty from "node-pty";
946
- import crypto2 from "crypto";
947
- import os3 from "os";
948
- import { EventEmitter } from "events";
983
+ // src/server.ts
984
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
949
985
 
950
- // ../server/src/git-utils.ts
951
- import { execFileSync } from "child_process";
952
- import path3 from "path";
953
- import fs3 from "fs";
986
+ // src/tools/tasks.ts
954
987
  import crypto from "crypto";
955
- var EXEC_OPTS = {
956
- encoding: "utf-8",
957
- stdio: ["pipe", "pipe", "pipe"]
988
+ import path3 from "path";
989
+ import { z as z2 } from "zod";
990
+
991
+ // src/validation.ts
992
+ import { z } from "zod";
993
+ 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("\\"), {
994
+ message: "Name must not contain path traversal characters (.. / \\)"
995
+ });
996
+ var safeId = z.string().min(1, "ID must not be empty").max(100, "ID must be 100 characters or less");
997
+ var safeTitle = z.string().min(1, "Title must not be empty").max(500, "Title must be 500 characters or less");
998
+ var safeDescription = z.string().max(5e3, "Description must be 5000 characters or less");
999
+ var safeShortText = z.string().max(200, "Value must be 200 characters or less");
1000
+ var safePrompt = z.string().max(1e4, "Prompt must be 10000 characters or less");
1001
+ 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 /)" });
1002
+ var safeHexColor = z.string().regex(/^#[0-9a-fA-F]{3,8}$/, "Must be a valid hex color (e.g. #6366f1)");
1003
+ var V = {
1004
+ name: safeName,
1005
+ id: safeId,
1006
+ title: safeTitle,
1007
+ description: safeDescription,
1008
+ shortText: safeShortText,
1009
+ prompt: safePrompt,
1010
+ absolutePath: safeAbsolutePath,
1011
+ hexColor: safeHexColor
958
1012
  };
959
- function getGitBranch(projectPath) {
960
- try {
961
- const branch = execFileSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
962
- cwd: projectPath,
963
- ...EXEC_OPTS,
964
- timeout: 3e3
965
- }).trim();
966
- return branch && branch !== "HEAD" ? branch : null;
967
- } catch {
968
- return null;
969
- }
970
- }
971
- function listBranches(projectPath) {
972
- try {
973
- const output = execFileSync("git", ["branch", "--format=%(refname:short)"], {
974
- cwd: projectPath,
975
- ...EXEC_OPTS,
976
- timeout: 5e3
977
- }).trim();
978
- return output ? output.split("\n").map((b) => b.trim()).filter(Boolean) : [];
979
- } catch {
980
- return [];
981
- }
982
- }
983
- function checkoutBranch(projectPath, branch) {
984
- try {
985
- execFileSync("git", ["checkout", branch], {
986
- cwd: projectPath,
987
- ...EXEC_OPTS,
988
- timeout: 1e4
989
- });
990
- return true;
991
- } catch {
992
- return false;
993
- }
994
- }
995
- function createWorktree(projectPath, branch) {
996
- const projectName = path3.basename(projectPath);
997
- const shortId = crypto.randomUUID().slice(0, 8);
998
- const baseDir = path3.join(path3.dirname(projectPath), ".vibegrid-worktrees", projectName);
999
- const worktreeDir = path3.join(baseDir, `${branch}-${shortId}`);
1000
- fs3.mkdirSync(baseDir, { recursive: true });
1001
- const localBranches = listBranches(projectPath);
1002
- if (localBranches.includes(branch)) {
1003
- try {
1004
- execFileSync("git", ["worktree", "add", worktreeDir, branch], {
1005
- cwd: projectPath,
1006
- ...EXEC_OPTS,
1007
- timeout: 3e4
1008
- });
1009
- } catch {
1010
- const newBranch = `${branch}-worktree-${shortId}`;
1011
- execFileSync("git", ["worktree", "add", "-b", newBranch, worktreeDir, branch], {
1012
- cwd: projectPath,
1013
- ...EXEC_OPTS,
1014
- timeout: 3e4
1015
- });
1016
- return { worktreePath: worktreeDir, branch: newBranch };
1017
- }
1018
- } else {
1019
- execFileSync("git", ["worktree", "add", "-b", branch, worktreeDir], {
1020
- cwd: projectPath,
1021
- ...EXEC_OPTS,
1022
- timeout: 3e4
1023
- });
1024
- }
1025
- return { worktreePath: worktreeDir, branch };
1026
- }
1027
- function getGitDiffStat(cwd) {
1028
- try {
1029
- const output = execFileSync("git", ["diff", "HEAD", "--numstat"], {
1030
- cwd,
1031
- ...EXEC_OPTS,
1032
- timeout: 1e4
1033
- }).trim();
1034
- if (!output) return { filesChanged: 0, insertions: 0, deletions: 0 };
1035
- let insertions = 0;
1036
- let deletions = 0;
1037
- let filesChanged = 0;
1038
- for (const line of output.split("\n")) {
1039
- const parts = line.split(" ");
1040
- if (parts[0] === "-") {
1041
- filesChanged++;
1042
- continue;
1013
+
1014
+ // src/tools/tasks.ts
1015
+ var TASK_STATUSES = ["todo", "in_progress", "in_review", "done", "cancelled"];
1016
+ var AGENT_TYPES = [
1017
+ "claude",
1018
+ "copilot",
1019
+ "codex",
1020
+ "opencode",
1021
+ "gemini"
1022
+ ];
1023
+ function registerTaskTools(server) {
1024
+ server.tool(
1025
+ "list_tasks",
1026
+ "List tasks, optionally filtered by project, status, assigned agent, or workspace",
1027
+ {
1028
+ project_name: V.name.optional().describe("Filter by project name"),
1029
+ status: z2.enum(TASK_STATUSES).optional().describe("Filter by status"),
1030
+ assigned_agent: z2.enum(AGENT_TYPES).optional().describe("Filter by assigned agent type"),
1031
+ workspace_id: V.id.optional().describe("Filter by workspace ID (returns tasks from all projects in that workspace)")
1032
+ },
1033
+ async (args) => {
1034
+ let tasks = dbListTasks(args.project_name, args.status);
1035
+ if (args.workspace_id) {
1036
+ const projects = dbListProjects();
1037
+ const wsProjectNames = new Set(
1038
+ projects.filter((p) => (p.workspaceId ?? "personal") === args.workspace_id).map((p) => p.name)
1039
+ );
1040
+ tasks = tasks.filter((t) => wsProjectNames.has(t.projectName));
1043
1041
  }
1044
- insertions += parseInt(parts[0], 10) || 0;
1045
- deletions += parseInt(parts[1], 10) || 0;
1046
- filesChanged++;
1047
- }
1048
- return { filesChanged, insertions, deletions };
1049
- } catch {
1050
- return null;
1051
- }
1052
- }
1053
- function getGitDiffFull(cwd) {
1054
- try {
1055
- const stat = getGitDiffStat(cwd);
1056
- if (!stat) return null;
1057
- const MAX_DIFF_SIZE = 500 * 1024;
1058
- let rawDiff = execFileSync("git", ["diff", "HEAD", "-U3"], {
1059
- cwd,
1060
- ...EXEC_OPTS,
1061
- timeout: 15e3,
1062
- maxBuffer: MAX_DIFF_SIZE * 2
1063
- });
1064
- if (rawDiff.length > MAX_DIFF_SIZE) {
1065
- rawDiff = rawDiff.slice(0, MAX_DIFF_SIZE) + "\n\n... diff truncated (too large) ...\n";
1066
- }
1067
- const numstatOutput = execFileSync("git", ["diff", "HEAD", "--numstat"], {
1068
- cwd,
1069
- ...EXEC_OPTS,
1070
- timeout: 1e4
1071
- }).trim();
1072
- const fileStats = /* @__PURE__ */ new Map();
1073
- if (numstatOutput) {
1074
- for (const line of numstatOutput.split("\n")) {
1075
- const parts = line.split(" ");
1076
- if (parts.length >= 3) {
1077
- const ins = parts[0] === "-" ? 0 : parseInt(parts[0], 10) || 0;
1078
- const del = parts[1] === "-" ? 0 : parseInt(parts[1], 10) || 0;
1079
- fileStats.set(parts.slice(2).join(" "), { insertions: ins, deletions: del });
1080
- }
1042
+ if (args.assigned_agent) {
1043
+ tasks = tasks.filter((t) => t.assignedAgent === args.assigned_agent);
1081
1044
  }
1045
+ return { content: [{ type: "text", text: JSON.stringify(tasks, null, 2) }] };
1082
1046
  }
1083
- const fileDiffs = [];
1084
- const diffSections = rawDiff.split(/^diff --git /m).filter(Boolean);
1085
- for (const section of diffSections) {
1086
- const fullSection = "diff --git " + section;
1087
- const plusMatch = fullSection.match(/^\+\+\+ b\/(.+)$/m);
1088
- const minusMatch = fullSection.match(/^--- a\/(.+)$/m);
1089
- const filePath = plusMatch?.[1] || minusMatch?.[1]?.replace(/^\/dev\/null$/, "") || "unknown";
1090
- let status = "modified";
1091
- if (fullSection.includes("--- /dev/null")) {
1092
- status = "added";
1093
- } else if (fullSection.includes("+++ /dev/null")) {
1094
- status = "deleted";
1095
- } else if (fullSection.includes("rename from")) {
1096
- status = "renamed";
1047
+ );
1048
+ server.tool(
1049
+ "create_task",
1050
+ "Create a new task in a project",
1051
+ {
1052
+ project_name: V.name.describe("Project name (must match existing project)"),
1053
+ title: V.title.describe("Task title"),
1054
+ description: V.description.optional().describe("Task description (markdown)"),
1055
+ status: z2.enum(TASK_STATUSES).optional().describe("Task status (default: todo)"),
1056
+ branch: V.shortText.optional().describe("Git branch for this task"),
1057
+ use_worktree: z2.boolean().optional().describe("Create a git worktree for this task"),
1058
+ assigned_agent: z2.enum(AGENT_TYPES).optional().describe("Assign to an agent type")
1059
+ },
1060
+ async (args) => {
1061
+ const project = dbGetProject(args.project_name);
1062
+ if (!project) {
1063
+ return {
1064
+ content: [{ type: "text", text: `Error: project "${args.project_name}" not found` }],
1065
+ isError: true
1066
+ };
1097
1067
  }
1098
- const stats = fileStats.get(filePath) || { insertions: 0, deletions: 0 };
1099
- fileDiffs.push({
1100
- filePath,
1101
- status,
1102
- insertions: stats.insertions,
1103
- deletions: stats.deletions,
1104
- diff: fullSection
1105
- });
1106
- }
1107
- return { stat, files: fileDiffs };
1108
- } catch {
1109
- return null;
1110
- }
1111
- }
1112
-
1113
- // ../server/src/agent-launch.ts
1114
- import { execFileSync as execFileSync3 } from "child_process";
1115
-
1116
- // ../server/src/process-utils.ts
1117
- import { execFileSync as execFileSync2 } from "child_process";
1118
- function getUserShellEnv() {
1119
- if (process.platform === "win32") return { ...process.env };
1120
- try {
1121
- const shell = process.env.SHELL || "/bin/zsh";
1122
- const output = execFileSync2(shell, ["-ilc", "env"], {
1123
- encoding: "utf-8",
1124
- timeout: 5e3,
1125
- stdio: ["pipe", "pipe", "pipe"]
1126
- });
1127
- const env = {};
1128
- for (const line of output.split("\n")) {
1129
- const idx = line.indexOf("=");
1130
- if (idx > 0) {
1131
- env[line.substring(0, idx)] = line.substring(idx + 1);
1132
- }
1133
- }
1134
- return env;
1135
- } catch {
1136
- return { ...process.env };
1137
- }
1138
- }
1139
- var resolvedEnv = getUserShellEnv();
1140
- function getDefaultShell() {
1141
- if (process.platform === "win32") {
1142
- return process.env.COMSPEC || "powershell.exe";
1143
- }
1144
- return process.env.SHELL || "/bin/zsh";
1145
- }
1146
- function shellEscape(s) {
1147
- if (/^[a-zA-Z0-9_./:=@%+,-]+$/.test(s)) return s;
1148
- return "'" + s.replace(/'/g, "'\\''") + "'";
1149
- }
1150
- var SENSITIVE_ENV_PREFIXES = [
1151
- "AWS_SECRET",
1152
- "AWS_SESSION",
1153
- "GITHUB_TOKEN",
1154
- "GH_TOKEN",
1155
- "OPENAI_API",
1156
- "ANTHROPIC_API",
1157
- "GOOGLE_API",
1158
- "STRIPE_",
1159
- "DATABASE_URL",
1160
- "DB_PASSWORD",
1161
- "SECRET_",
1162
- "PRIVATE_KEY",
1163
- "NPM_TOKEN",
1164
- "NODE_AUTH_TOKEN"
1165
- ];
1166
- var STRIP_ENV_KEYS = ["CLAUDECODE"];
1167
- function getSafeEnv() {
1168
- const env = {};
1169
- for (const [key, val] of Object.entries(resolvedEnv)) {
1170
- if (val === void 0) continue;
1171
- if (SENSITIVE_ENV_PREFIXES.some((p) => key.toUpperCase().startsWith(p))) continue;
1172
- if (STRIP_ENV_KEYS.includes(key)) continue;
1173
- env[key] = val;
1174
- }
1175
- return env;
1176
- }
1177
-
1178
- // ../server/src/agent-launch.ts
1179
- function commandExists(cmd, env) {
1180
- try {
1181
- const bin = process.platform === "win32" ? "where" : "which";
1182
- execFileSync3(bin, [cmd], { stdio: "pipe", timeout: 3e3, env });
1183
- return true;
1184
- } catch {
1185
- return false;
1186
- }
1187
- }
1188
- function resolveAgentCommand(config, env) {
1189
- if (commandExists(config.command, env)) {
1190
- return { command: config.command, args: config.args };
1191
- }
1192
- if (config.fallbackCommand && commandExists(config.fallbackCommand, env)) {
1193
- return { command: config.fallbackCommand, args: config.fallbackArgs ?? [] };
1194
- }
1195
- return { command: config.command, args: config.args };
1196
- }
1197
- function buildAgentLaunchLine(payload, agentCommands, env) {
1198
- const cmdConfig = agentCommands[payload.agentType] || DEFAULT_AGENT_COMMANDS[payload.agentType];
1199
- const cmd = resolveAgentCommand(cmdConfig, env);
1200
- const effectiveArgs = payload.args !== void 0 ? payload.args : cmd.args;
1201
- let launchLine = [cmd.command, ...effectiveArgs.map((a) => shellEscape(a))].join(" ");
1202
- if (payload.resumeSessionId) {
1203
- switch (payload.agentType) {
1204
- case "claude":
1205
- launchLine += ` --resume ${payload.resumeSessionId}`;
1206
- break;
1207
- case "copilot":
1208
- launchLine += ` --resume ${payload.resumeSessionId}`;
1209
- break;
1210
- case "codex":
1211
- launchLine = `${cmd.command} resume ${payload.resumeSessionId}`;
1212
- break;
1213
- case "opencode":
1214
- launchLine += ` --session ${payload.resumeSessionId}`;
1215
- break;
1216
- case "gemini":
1217
- launchLine += ` --resume latest`;
1218
- break;
1219
- }
1220
- }
1221
- if (payload.initialPrompt) {
1222
- const escaped = shellEscape(payload.initialPrompt);
1223
- switch (payload.agentType) {
1224
- case "copilot":
1225
- launchLine += ` -i ${escaped}`;
1226
- break;
1227
- case "gemini":
1228
- launchLine += ` -i ${escaped}`;
1229
- break;
1230
- case "opencode":
1231
- launchLine += ` --prompt ${escaped}`;
1232
- break;
1233
- default:
1234
- launchLine += ` ${escaped}`;
1235
- break;
1236
- }
1237
- }
1238
- return launchLine;
1239
- }
1240
-
1241
- // ../server/src/pty-manager.ts
1242
- var PtyManager = class extends EventEmitter {
1243
- ptys = /* @__PURE__ */ new Map();
1244
- sessions = /* @__PURE__ */ new Map();
1245
- agentCommands = { ...DEFAULT_AGENT_COMMANDS };
1246
- remoteHosts = [];
1247
- dataBuffers = /* @__PURE__ */ new Map();
1248
- flushTimers = /* @__PURE__ */ new Map();
1249
- setRemoteHosts(hosts) {
1250
- this.remoteHosts = hosts;
1251
- }
1252
- setAgentCommands(overrides) {
1253
- this.agentCommands = { ...DEFAULT_AGENT_COMMANDS };
1254
- if (overrides) {
1255
- for (const [key, val] of Object.entries(overrides)) {
1256
- if (val) {
1257
- this.agentCommands[key] = val;
1258
- }
1259
- }
1260
- }
1261
- }
1262
- buildAgentLaunchLine(payload) {
1263
- return buildAgentLaunchLine(payload, this.agentCommands, getSafeEnv());
1264
- }
1265
- createPty(payload) {
1266
- const id = crypto2.randomUUID();
1267
- const shell = getDefaultShell();
1268
- const remoteHost = payload.remoteHostId ? this.remoteHosts.find((h) => h.id === payload.remoteHostId) : void 0;
1269
- const session = remoteHost ? this.createRemotePty(id, shell, payload, remoteHost) : this.createLocalPty(id, shell, payload);
1270
- this.emit("session-created", session, payload);
1271
- return session;
1272
- }
1273
- createLocalPty(id, shell, payload) {
1274
- let effectivePath = payload.projectPath;
1275
- let worktreePath;
1276
- let effectiveBranch;
1277
- if (payload.existingWorktreePath) {
1278
- effectivePath = payload.existingWorktreePath;
1279
- worktreePath = payload.existingWorktreePath;
1280
- effectiveBranch = payload.branch;
1281
- } else if (payload.useWorktree && payload.branch) {
1282
- const result = createWorktree(payload.projectPath, payload.branch);
1283
- effectivePath = result.worktreePath;
1284
- worktreePath = result.worktreePath;
1285
- effectiveBranch = result.branch;
1286
- } else if (payload.branch) {
1287
- const currentBranch = getGitBranch(payload.projectPath);
1288
- if (currentBranch !== payload.branch) {
1289
- checkoutBranch(payload.projectPath, payload.branch);
1290
- }
1291
- effectiveBranch = payload.branch;
1292
- }
1293
- const ptyProcess = pty.spawn(shell, ["-l"], {
1294
- name: "xterm-256color",
1295
- cols: 80,
1296
- rows: 24,
1297
- cwd: effectivePath,
1298
- env: getSafeEnv()
1299
- });
1300
- const launchLine = this.buildAgentLaunchLine(payload);
1301
- setTimeout(() => ptyProcess.write(launchLine + "\r"), 300);
1302
- this.setupPtyEvents(id, ptyProcess);
1303
- this.ptys.set(id, ptyProcess);
1304
- const branch = effectiveBranch || getGitBranch(effectivePath);
1305
- const session = {
1306
- id,
1307
- agentType: payload.agentType,
1308
- projectName: payload.projectName,
1309
- projectPath: payload.projectPath,
1310
- status: "running",
1311
- createdAt: Date.now(),
1312
- pid: ptyProcess.pid,
1313
- ...payload.displayName ? { displayName: payload.displayName } : {},
1314
- ...branch ? { branch } : {},
1315
- ...worktreePath ? { worktreePath, isWorktree: true } : {}
1316
- };
1317
- this.sessions.set(id, session);
1318
- return session;
1319
- }
1320
- createRemotePty(id, shell, payload, host) {
1321
- const ptyProcess = pty.spawn(shell, ["-l"], {
1322
- name: "xterm-256color",
1323
- cols: 80,
1324
- rows: 24,
1325
- cwd: os3.homedir(),
1326
- env: getSafeEnv()
1327
- });
1328
- const sshParts = ["ssh", "-t"];
1329
- if (host.port !== 22) sshParts.push("-p", String(host.port));
1330
- if (host.sshKeyPath) sshParts.push("-i", host.sshKeyPath);
1331
- if (host.sshOptions) {
1332
- const opts = host.sshOptions.split(/\s+/).filter(Boolean);
1333
- sshParts.push(...opts);
1334
- }
1335
- sshParts.push(`${host.user}@${host.hostname}`);
1336
- const agentLine = this.buildAgentLaunchLine(payload);
1337
- const remoteCmd = `cd ${shellEscape(payload.projectPath)} && ${agentLine}`;
1338
- setTimeout(() => {
1339
- if (this.ptys.has(id)) ptyProcess.write(sshParts.join(" ") + "\r");
1340
- }, 300);
1341
- let connected = false;
1342
- const fallbackTimer = setTimeout(() => {
1343
- if (!connected) {
1344
- connected = true;
1345
- if (this.ptys.has(id)) ptyProcess.write(remoteCmd + "\r");
1346
- }
1347
- }, 5e3);
1348
- const promptListener = ptyProcess.onData((data) => {
1349
- if (!connected && /[$#>]\s*$/.test(data)) {
1350
- connected = true;
1351
- clearTimeout(fallbackTimer);
1352
- setTimeout(() => {
1353
- if (this.ptys.has(id)) ptyProcess.write(remoteCmd + "\r");
1354
- }, 100);
1355
- }
1356
- });
1357
- this.setupPtyEvents(id, ptyProcess);
1358
- this.ptys.set(id, ptyProcess);
1359
- const cleanup = () => {
1360
- promptListener.dispose();
1361
- };
1362
- const checkConnected = setInterval(() => {
1363
- if (connected) {
1364
- cleanup();
1365
- clearInterval(checkConnected);
1366
- }
1367
- }, 200);
1368
- setTimeout(() => {
1369
- cleanup();
1370
- clearInterval(checkConnected);
1371
- }, 6e3);
1372
- const session = {
1373
- id,
1374
- agentType: payload.agentType,
1375
- projectName: payload.projectName,
1376
- projectPath: payload.projectPath,
1377
- status: "running",
1378
- createdAt: Date.now(),
1379
- pid: ptyProcess.pid,
1380
- remoteHostId: host.id,
1381
- remoteHostLabel: host.label,
1382
- ...payload.displayName ? { displayName: payload.displayName } : {}
1383
- };
1384
- this.sessions.set(id, session);
1385
- return session;
1386
- }
1387
- createShellPty(cwd) {
1388
- const id = crypto2.randomUUID();
1389
- const shell = getDefaultShell();
1390
- const ptyProcess = pty.spawn(shell, ["-l"], {
1391
- name: "xterm-256color",
1392
- cols: 80,
1393
- rows: 24,
1394
- cwd: cwd || os3.homedir(),
1395
- env: getSafeEnv()
1396
- });
1397
- this.setupPtyEvents(id, ptyProcess);
1398
- this.ptys.set(id, ptyProcess);
1399
- return { id, pid: ptyProcess.pid };
1400
- }
1401
- bufferData(id, data) {
1402
- const existing = this.dataBuffers.get(id);
1403
- this.dataBuffers.set(id, existing ? existing + data : data);
1404
- if (!this.flushTimers.has(id)) {
1405
- this.flushTimers.set(
1406
- id,
1407
- setTimeout(() => this.flushBuffer(id), 50)
1408
- );
1409
- }
1410
- }
1411
- flushBuffer(id) {
1412
- const data = this.dataBuffers.get(id);
1413
- this.dataBuffers.delete(id);
1414
- this.flushTimers.delete(id);
1415
- if (data) {
1416
- this.emit("client-message", IPC.TERMINAL_DATA, { id, data });
1417
- }
1418
- }
1419
- clearBuffer(id) {
1420
- const timer = this.flushTimers.get(id);
1421
- if (timer) clearTimeout(timer);
1422
- this.flushTimers.delete(id);
1423
- this.dataBuffers.delete(id);
1424
- }
1425
- setupPtyEvents(id, ptyProcess) {
1426
- ptyProcess.onData((data) => {
1427
- this.bufferData(id, data);
1428
- });
1429
- ptyProcess.onExit(({ exitCode }) => {
1430
- const pendingTimer = this.flushTimers.get(id);
1431
- if (pendingTimer) {
1432
- clearTimeout(pendingTimer);
1433
- this.flushBuffer(id);
1434
- }
1435
- this.clearBuffer(id);
1436
- this.ptys.delete(id);
1437
- const session = this.sessions.get(id);
1438
- if (session) {
1439
- this.emit("session-exit", session);
1440
- session.status = "idle";
1441
- if (session.worktreePath) {
1442
- this.emit("client-message", IPC.WORKTREE_CONFIRM_CLEANUP, {
1443
- id: session.id,
1444
- projectPath: session.projectPath,
1445
- worktreePath: session.worktreePath
1446
- });
1447
- }
1448
- }
1449
- this.emit("client-message", IPC.TERMINAL_EXIT, { id, exitCode });
1450
- });
1451
- }
1452
- writeToPty(id, data) {
1453
- this.ptys.get(id)?.write(data);
1454
- }
1455
- resizePty(id, cols, rows) {
1456
- this.ptys.get(id)?.resize(cols, rows);
1457
- }
1458
- killPty(id) {
1459
- const p = this.ptys.get(id);
1460
- const pendingTimer = this.flushTimers.get(id);
1461
- if (pendingTimer) {
1462
- clearTimeout(pendingTimer);
1463
- this.flushBuffer(id);
1464
- }
1465
- this.clearBuffer(id);
1466
- const session = this.sessions.get(id);
1467
- this.sessions.delete(id);
1468
- this.ptys.delete(id);
1469
- if (session) {
1470
- this.emit("session-exit", session);
1471
- if (session.worktreePath) {
1472
- this.emit("client-message", IPC.WORKTREE_CONFIRM_CLEANUP, {
1473
- id: session.id,
1474
- projectPath: session.projectPath,
1475
- worktreePath: session.worktreePath
1476
- });
1477
- }
1478
- }
1479
- if (p) {
1480
- setImmediate(() => {
1481
- try {
1482
- p.kill();
1483
- } catch (err) {
1484
- logger_default.warn(`[pty] kill failed for ${id} (already dead?):`, err);
1485
- }
1486
- });
1487
- } else {
1488
- this.emit("client-message", IPC.TERMINAL_EXIT, { id, exitCode: 0 });
1489
- }
1490
- }
1491
- killAll() {
1492
- for (const timer of this.flushTimers.values()) {
1493
- clearTimeout(timer);
1494
- }
1495
- this.dataBuffers.clear();
1496
- this.flushTimers.clear();
1497
- for (const [id, p] of this.ptys) {
1498
- p.kill();
1499
- this.ptys.delete(id);
1500
- }
1501
- this.sessions.clear();
1502
- }
1503
- getActiveSessions() {
1504
- return Array.from(this.sessions.values());
1505
- }
1506
- updateSessionStatus(id, status) {
1507
- const session = this.sessions.get(id);
1508
- if (session) {
1509
- session.status = status;
1510
- this.emit("client-message", IPC.TERMINAL_DATA, { id, data: "" });
1511
- }
1512
- }
1513
- /**
1514
- * Finds the most-recently-created terminal matching cwd that:
1515
- * - is NOT already linked to a Claude session (no hookSessionId)
1516
- * - is NOT in the excludeIds set (already claimed by another session_id)
1517
- */
1518
- findUnlinkedSessionByCwd(cwd, excludeIds) {
1519
- const normalizedCwd = cwd.replace(/\/+$/, "");
1520
- let best;
1521
- let bestTime = 0;
1522
- for (const session of this.sessions.values()) {
1523
- if (session.hookSessionId) continue;
1524
- if (excludeIds.has(session.id)) continue;
1525
- const sessionPath = (session.worktreePath || session.projectPath).replace(/\/+$/, "");
1526
- if (sessionPath === normalizedCwd && session.createdAt > bestTime) {
1527
- best = session;
1528
- bestTime = session.createdAt;
1529
- }
1530
- }
1531
- return best;
1532
- }
1533
- };
1534
- var ptyManager = new PtyManager();
1535
-
1536
- // ../server/src/scheduler.ts
1537
- import cron from "node-cron";
1538
- import fs4 from "fs";
1539
- import path4 from "path";
1540
- import os4 from "os";
1541
- import { EventEmitter as EventEmitter2 } from "events";
1542
- var LOCK_DIR = path4.join(os4.homedir(), ".vibegrid");
1543
- function acquireExecutionLock(workflowId) {
1544
- const minuteKey = Math.floor(Date.now() / 6e4);
1545
- const lockFile = path4.join(LOCK_DIR, `scheduler-${workflowId}-${minuteKey}.lock`);
1546
- try {
1547
- fs4.writeFileSync(lockFile, String(process.pid), { flag: "wx" });
1548
- cleanStaleLocks(workflowId, minuteKey);
1549
- return true;
1550
- } catch {
1551
- return false;
1552
- }
1553
- }
1554
- function cleanStaleLocks(workflowId, currentKey) {
1555
- try {
1556
- const prefix = `scheduler-${workflowId}-`;
1557
- for (const f of fs4.readdirSync(LOCK_DIR)) {
1558
- if (f.startsWith(prefix) && f.endsWith(".lock")) {
1559
- const key = parseInt(f.slice(prefix.length, -5), 10);
1560
- if (!isNaN(key) && key < currentKey) {
1561
- fs4.unlinkSync(path4.join(LOCK_DIR, f));
1562
- }
1563
- }
1564
- }
1565
- } catch {
1566
- }
1567
- }
1568
- function getTriggerConfig(wf) {
1569
- const triggerNode = wf.nodes.find((n) => n.type === "trigger");
1570
- if (!triggerNode) return null;
1571
- return triggerNode.config;
1572
- }
1573
- var Scheduler = class extends EventEmitter2 {
1574
- cronJobs = /* @__PURE__ */ new Map();
1575
- timeouts = /* @__PURE__ */ new Map();
1576
- syncSchedules(workflows) {
1577
- logger_default.info(
1578
- `[scheduler] syncing ${workflows.length} workflows (active crons: ${this.cronJobs.size}, timeouts: ${this.timeouts.size})`
1579
- );
1580
- for (const [id] of this.cronJobs) {
1581
- const wf = workflows.find((w) => w.id === id);
1582
- const trigger = wf ? getTriggerConfig(wf) : null;
1583
- if (!wf || !wf.enabled || trigger?.triggerType !== "recurring") {
1584
- this.cronJobs.get(id)?.stop();
1585
- this.cronJobs.delete(id);
1586
- }
1587
- }
1588
- for (const [id] of this.timeouts) {
1589
- const wf = workflows.find((w) => w.id === id);
1590
- const trigger = wf ? getTriggerConfig(wf) : null;
1591
- if (!wf || !wf.enabled || trigger?.triggerType !== "once") {
1592
- clearTimeout(this.timeouts.get(id));
1593
- this.timeouts.delete(id);
1594
- }
1595
- }
1596
- for (const wf of workflows) {
1597
- if (!wf.enabled) {
1598
- logger_default.info(`[scheduler] skipping disabled workflow "${wf.name}"`);
1599
- continue;
1600
- }
1601
- const trigger = getTriggerConfig(wf);
1602
- if (!trigger) {
1603
- logger_default.info(`[scheduler] no trigger node for workflow "${wf.name}"`);
1604
- continue;
1605
- }
1606
- logger_default.info(`[scheduler] workflow "${wf.name}" trigger=${trigger.triggerType}`);
1607
- if (trigger.triggerType === "recurring" && !this.cronJobs.has(wf.id)) {
1608
- logger_default.info(
1609
- `[scheduler] registering recurring workflow "${wf.name}" cron="${trigger.cron}" enabled=${wf.enabled}`
1610
- );
1611
- if (!cron.validate(trigger.cron)) {
1612
- logger_default.error(
1613
- `[scheduler] invalid cron expression for workflow "${wf.name}": ${trigger.cron}`
1614
- );
1615
- continue;
1616
- }
1617
- try {
1618
- const task = cron.schedule(trigger.cron, () => this.executeWorkflow(wf.id), {
1619
- timezone: trigger.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone
1620
- });
1621
- this.cronJobs.set(wf.id, task);
1622
- } catch (err) {
1623
- logger_default.error(`[scheduler] failed to schedule workflow "${wf.name}":`, err);
1624
- }
1625
- }
1626
- if (trigger.triggerType === "once" && !this.timeouts.has(wf.id)) {
1627
- const runAt = new Date(trigger.runAt).getTime();
1628
- if (isNaN(runAt)) {
1629
- logger_default.error(`[scheduler] invalid runAt date for workflow "${wf.name}": ${trigger.runAt}`);
1630
- continue;
1631
- }
1632
- const delay = runAt - Date.now();
1633
- if (delay > 0) {
1634
- const MAX_DELAY = 24 * 60 * 60 * 1e3;
1635
- const safeDelay = Math.min(delay, MAX_DELAY);
1636
- const timer = setTimeout(() => {
1637
- if (safeDelay < delay) {
1638
- this.timeouts.delete(wf.id);
1639
- this.syncSchedules(configManager.loadConfig().workflows ?? []);
1640
- } else {
1641
- this.executeWorkflow(wf.id);
1642
- }
1643
- }, safeDelay);
1644
- this.timeouts.set(wf.id, timer);
1645
- }
1646
- }
1647
- }
1648
- }
1649
- executeWorkflow(workflowId) {
1650
- if (!acquireExecutionLock(workflowId)) {
1651
- logger_default.info(`[scheduler] skipping workflow ${workflowId} \u2014 already executed by another instance`);
1652
- this.timeouts.delete(workflowId);
1653
- return;
1654
- }
1655
- logger_default.info(`[scheduler] executing workflow ${workflowId}`);
1656
- this.emit("client-message", IPC.SCHEDULER_EXECUTE, { workflowId });
1657
- this.timeouts.delete(workflowId);
1658
- }
1659
- checkMissedSchedules(workflows) {
1660
- const missed = [];
1661
- for (const wf of workflows) {
1662
- if (!wf.enabled) continue;
1663
- const trigger = getTriggerConfig(wf);
1664
- if (trigger?.triggerType === "once") {
1665
- const runAt = new Date(trigger.runAt).getTime();
1666
- if (runAt < Date.now() && !wf.lastRunAt) {
1667
- missed.push({ workflow: wf, scheduledFor: trigger.runAt });
1668
- }
1669
- }
1670
- }
1671
- return missed;
1672
- }
1673
- getNextRun(workflowId, workflows) {
1674
- const wf = workflows.find((w) => w.id === workflowId);
1675
- if (!wf || !wf.enabled) return null;
1676
- const trigger = getTriggerConfig(wf);
1677
- if (!trigger) return null;
1678
- if (trigger.triggerType === "once") {
1679
- const runAt = new Date(trigger.runAt).getTime();
1680
- return runAt > Date.now() ? trigger.runAt : null;
1681
- }
1682
- if (trigger.triggerType === "recurring") {
1683
- return trigger.cron;
1684
- }
1685
- return null;
1686
- }
1687
- stopAll() {
1688
- for (const [, job] of this.cronJobs) job.stop();
1689
- for (const [, timer] of this.timeouts) clearTimeout(timer);
1690
- this.cronJobs.clear();
1691
- this.timeouts.clear();
1692
- }
1693
- };
1694
- var scheduler = new Scheduler();
1695
-
1696
- // src/server.ts
1697
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
1698
-
1699
- // src/tools/tasks.ts
1700
- import crypto3 from "crypto";
1701
- import path5 from "path";
1702
- import { z as z2 } from "zod";
1703
-
1704
- // src/validation.ts
1705
- import { z } from "zod";
1706
- 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("\\"), {
1707
- message: "Name must not contain path traversal characters (.. / \\)"
1708
- });
1709
- var safeId = z.string().min(1, "ID must not be empty").max(100, "ID must be 100 characters or less");
1710
- var safeTitle = z.string().min(1, "Title must not be empty").max(500, "Title must be 500 characters or less");
1711
- var safeDescription = z.string().max(5e3, "Description must be 5000 characters or less");
1712
- var safeShortText = z.string().max(200, "Value must be 200 characters or less");
1713
- var safePrompt = z.string().max(1e4, "Prompt must be 10000 characters or less");
1714
- 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 /)" });
1715
- var safeHexColor = z.string().regex(/^#[0-9a-fA-F]{3,8}$/, "Must be a valid hex color (e.g. #6366f1)");
1716
- var V = {
1717
- name: safeName,
1718
- id: safeId,
1719
- title: safeTitle,
1720
- description: safeDescription,
1721
- shortText: safeShortText,
1722
- prompt: safePrompt,
1723
- absolutePath: safeAbsolutePath,
1724
- hexColor: safeHexColor
1725
- };
1726
-
1727
- // src/tools/tasks.ts
1728
- var TASK_STATUSES = ["todo", "in_progress", "in_review", "done", "cancelled"];
1729
- var AGENT_TYPES = [
1730
- "claude",
1731
- "copilot",
1732
- "codex",
1733
- "opencode",
1734
- "gemini"
1735
- ];
1736
- function registerTaskTools(server, deps) {
1737
- const { configManager: configManager2 } = deps;
1738
- server.tool(
1739
- "list_tasks",
1740
- "List tasks, optionally filtered by project and/or status",
1741
- {
1742
- project_name: V.name.optional().describe("Filter by project name"),
1743
- status: z2.enum(TASK_STATUSES).optional().describe("Filter by status")
1744
- },
1745
- async (args) => {
1746
- const tasks = dbListTasks(args.project_name, args.status);
1747
- return { content: [{ type: "text", text: JSON.stringify(tasks, null, 2) }] };
1748
- }
1749
- );
1750
- server.tool(
1751
- "create_task",
1752
- "Create a new task in a project",
1753
- {
1754
- project_name: V.name.describe("Project name (must match existing project)"),
1755
- title: V.title.describe("Task title"),
1756
- description: V.description.optional().describe("Task description (markdown)"),
1757
- status: z2.enum(TASK_STATUSES).optional().describe("Task status (default: todo)"),
1758
- branch: V.shortText.optional().describe("Git branch for this task"),
1759
- use_worktree: z2.boolean().optional().describe("Create a git worktree for this task"),
1760
- assigned_agent: z2.enum(AGENT_TYPES).optional().describe("Assign to an agent type")
1761
- },
1762
- async (args) => {
1763
- const project = dbGetProject(args.project_name);
1764
- if (!project) {
1765
- return {
1766
- content: [{ type: "text", text: `Error: project "${args.project_name}" not found` }],
1767
- isError: true
1768
- };
1769
- }
1770
- const maxOrder = dbGetMaxTaskOrder(args.project_name);
1771
- const now = (/* @__PURE__ */ new Date()).toISOString();
1772
- const status = args.status ?? "todo";
1773
- const task = {
1774
- id: crypto3.randomUUID(),
1775
- projectName: args.project_name,
1776
- title: args.title,
1777
- description: args.description ?? "",
1068
+ const maxOrder = dbGetMaxTaskOrder(args.project_name);
1069
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1070
+ const status = args.status ?? "todo";
1071
+ const task = {
1072
+ id: crypto.randomUUID(),
1073
+ projectName: args.project_name,
1074
+ title: args.title,
1075
+ description: args.description ?? "",
1778
1076
  status,
1779
1077
  order: maxOrder + 1,
1780
1078
  createdAt: now,
@@ -1785,7 +1083,6 @@ function registerTaskTools(server, deps) {
1785
1083
  ...(status === "done" || status === "cancelled") && { completedAt: now }
1786
1084
  };
1787
1085
  dbInsertTask(task);
1788
- configManager2.notifyChanged();
1789
1086
  return { content: [{ type: "text", text: JSON.stringify(task, null, 2) }] };
1790
1087
  }
1791
1088
  );
@@ -1837,7 +1134,6 @@ function registerTaskTools(server, deps) {
1837
1134
  if (!isDone && wasDone) updates.completedAt = void 0;
1838
1135
  }
1839
1136
  dbUpdateTask(args.id, updates);
1840
- configManager2.notifyChanged();
1841
1137
  const updated = dbGetTask(args.id);
1842
1138
  return { content: [{ type: "text", text: JSON.stringify(updated, null, 2) }] };
1843
1139
  }
@@ -1855,7 +1151,6 @@ function registerTaskTools(server, deps) {
1855
1151
  };
1856
1152
  }
1857
1153
  dbDeleteTask(args.id);
1858
- configManager2.notifyChanged();
1859
1154
  return { content: [{ type: "text", text: `Deleted task: ${task.title}` }] };
1860
1155
  }
1861
1156
  );
@@ -1902,12 +1197,12 @@ function registerTaskTools(server, deps) {
1902
1197
  };
1903
1198
  }
1904
1199
  const cwd = args.cwd || process.cwd();
1905
- const normalizedCwd = path5.resolve(cwd);
1200
+ const normalizedCwd = path3.resolve(cwd);
1906
1201
  const projects = dbListProjects();
1907
1202
  let matchedProject = null;
1908
1203
  let matchLen = 0;
1909
1204
  for (const p of projects) {
1910
- const normalizedPath = path5.resolve(p.path);
1205
+ const normalizedPath = path3.resolve(p.path);
1911
1206
  if (normalizedCwd.startsWith(normalizedPath) && normalizedPath.length > matchLen) {
1912
1207
  matchedProject = p;
1913
1208
  matchLen = normalizedPath.length;
@@ -1935,7 +1230,7 @@ function registerTaskTools(server, deps) {
1935
1230
  let matchedTask = null;
1936
1231
  for (const t of projectTasks) {
1937
1232
  if (t.worktreePath) {
1938
- const normalizedWorktree = path5.resolve(t.worktreePath);
1233
+ const normalizedWorktree = path3.resolve(t.worktreePath);
1939
1234
  if (normalizedCwd.startsWith(normalizedWorktree)) {
1940
1235
  matchedTask = t;
1941
1236
  break;
@@ -1979,12 +1274,21 @@ var AGENT_TYPES2 = [
1979
1274
  "opencode",
1980
1275
  "gemini"
1981
1276
  ];
1982
- function registerProjectTools(server, deps) {
1983
- const { configManager: configManager2 } = deps;
1984
- server.tool("list_projects", "List all projects", async () => {
1985
- const projects = dbListProjects();
1986
- return { content: [{ type: "text", text: JSON.stringify(projects, null, 2) }] };
1987
- });
1277
+ function registerProjectTools(server) {
1278
+ server.tool(
1279
+ "list_projects",
1280
+ "List all projects, optionally filtered by workspace",
1281
+ {
1282
+ workspace_id: V.id.optional().describe('Filter by workspace ID (e.g. "personal")')
1283
+ },
1284
+ async (args) => {
1285
+ let projects = dbListProjects();
1286
+ if (args.workspace_id) {
1287
+ projects = projects.filter((p) => (p.workspaceId ?? "personal") === args.workspace_id);
1288
+ }
1289
+ return { content: [{ type: "text", text: JSON.stringify(projects, null, 2) }] };
1290
+ }
1291
+ );
1988
1292
  server.tool(
1989
1293
  "create_project",
1990
1294
  "Create a new project",
@@ -2010,7 +1314,6 @@ function registerProjectTools(server, deps) {
2010
1314
  ...args.icon_color && { iconColor: args.icon_color }
2011
1315
  };
2012
1316
  dbInsertProject(project);
2013
- configManager2.notifyChanged();
2014
1317
  return { content: [{ type: "text", text: JSON.stringify(project, null, 2) }] };
2015
1318
  }
2016
1319
  );
@@ -2038,7 +1341,6 @@ function registerProjectTools(server, deps) {
2038
1341
  if (args.icon !== void 0) updates.icon = args.icon;
2039
1342
  if (args.icon_color !== void 0) updates.iconColor = args.icon_color;
2040
1343
  dbUpdateProject(args.name, updates);
2041
- configManager2.notifyChanged();
2042
1344
  const updated = dbGetProject(args.name);
2043
1345
  return { content: [{ type: "text", text: JSON.stringify(updated, null, 2) }] };
2044
1346
  }
@@ -2055,7 +1357,6 @@ function registerProjectTools(server, deps) {
2055
1357
  };
2056
1358
  }
2057
1359
  dbDeleteProject(args.name);
2058
- configManager2.notifyChanged();
2059
1360
  return { content: [{ type: "text", text: `Deleted project: ${args.name}` }] };
2060
1361
  }
2061
1362
  );
@@ -2063,6 +1364,78 @@ function registerProjectTools(server, deps) {
2063
1364
 
2064
1365
  // src/tools/sessions.ts
2065
1366
  import { z as z4 } from "zod";
1367
+
1368
+ // src/ws-client.ts
1369
+ import fs3 from "fs";
1370
+ import path4 from "path";
1371
+ import os3 from "os";
1372
+ import { WebSocket } from "ws";
1373
+ var PORT_FILE = path4.join(os3.homedir(), ".vibegrid", "ws-port");
1374
+ var TIMEOUT_MS = 1e4;
1375
+ var rpcId = 0;
1376
+ function readPort() {
1377
+ try {
1378
+ const raw = fs3.readFileSync(PORT_FILE, "utf-8").trim();
1379
+ const port = parseInt(raw, 10);
1380
+ return Number.isFinite(port) && port > 0 ? port : null;
1381
+ } catch {
1382
+ return null;
1383
+ }
1384
+ }
1385
+ async function rpcCall(method, params) {
1386
+ const port = readPort();
1387
+ if (!port) {
1388
+ throw new Error("VibeGrid app is not running. Start VibeGrid to use session management tools.");
1389
+ }
1390
+ return new Promise((resolve, reject) => {
1391
+ const ws = new WebSocket(`ws://127.0.0.1:${port}/ws`);
1392
+ const id = ++rpcId;
1393
+ const timer = setTimeout(() => {
1394
+ ws.close();
1395
+ reject(new Error(`RPC call "${method}" timed out after ${TIMEOUT_MS}ms`));
1396
+ }, TIMEOUT_MS);
1397
+ ws.on("open", () => {
1398
+ ws.send(JSON.stringify({ jsonrpc: "2.0", id, method, params }));
1399
+ });
1400
+ ws.on("message", (raw) => {
1401
+ try {
1402
+ const msg = JSON.parse(raw.toString());
1403
+ if (msg.id !== id) return;
1404
+ clearTimeout(timer);
1405
+ ws.close();
1406
+ if (msg.error) {
1407
+ reject(new Error(msg.error.message));
1408
+ } else {
1409
+ resolve(msg.result);
1410
+ }
1411
+ } catch {
1412
+ }
1413
+ });
1414
+ ws.on("error", (err) => {
1415
+ clearTimeout(timer);
1416
+ reject(new Error(`Cannot connect to VibeGrid server: ${err.message}. Is the app running?`));
1417
+ });
1418
+ });
1419
+ }
1420
+ async function rpcNotify(method, params) {
1421
+ const port = readPort();
1422
+ if (!port) {
1423
+ throw new Error("VibeGrid app is not running. Start VibeGrid to use session management tools.");
1424
+ }
1425
+ return new Promise((resolve, reject) => {
1426
+ const ws = new WebSocket(`ws://127.0.0.1:${port}/ws`);
1427
+ ws.on("open", () => {
1428
+ ws.send(JSON.stringify({ jsonrpc: "2.0", method, params }));
1429
+ ws.close();
1430
+ resolve();
1431
+ });
1432
+ ws.on("error", (err) => {
1433
+ reject(new Error(`Cannot connect to VibeGrid server: ${err.message}. Is the app running?`));
1434
+ });
1435
+ });
1436
+ }
1437
+
1438
+ // src/tools/sessions.ts
2066
1439
  var AGENT_TYPES3 = [
2067
1440
  "claude",
2068
1441
  "copilot",
@@ -2070,45 +1443,149 @@ var AGENT_TYPES3 = [
2070
1443
  "opencode",
2071
1444
  "gemini"
2072
1445
  ];
2073
- function registerSessionTools(server, deps) {
2074
- const { ptyManager: ptyManager2 } = deps;
2075
- server.tool("list_sessions", "List all active terminal sessions", async () => {
2076
- const sessions = ptyManager2.getActiveSessions();
2077
- const summary = sessions.map((s) => ({
2078
- id: s.id,
2079
- agentType: s.agentType,
2080
- projectName: s.projectName,
2081
- status: s.status,
2082
- displayName: s.displayName,
2083
- branch: s.branch,
2084
- pid: s.pid
2085
- }));
2086
- return { content: [{ type: "text", text: JSON.stringify(summary, null, 2) }] };
2087
- });
1446
+ function registerSessionTools(server) {
2088
1447
  server.tool(
2089
- "launch_agent",
2090
- "Launch an AI agent in a new terminal session",
1448
+ "list_sessions",
1449
+ "List all active terminal sessions. Requires the VibeGrid app to be running.",
2091
1450
  {
2092
- agent_type: z4.enum(AGENT_TYPES3).describe("Agent type to launch"),
2093
- project_name: V.name.describe("Project name"),
2094
- project_path: V.absolutePath.describe("Absolute path to project directory"),
2095
- prompt: V.prompt.optional().describe("Initial prompt to send to the agent"),
2096
- branch: V.shortText.optional().describe("Git branch to checkout"),
2097
- use_worktree: z4.boolean().optional().describe("Create a git worktree"),
2098
- display_name: V.shortText.optional().describe("Display name for the session")
1451
+ project_name: V.name.optional().describe("Filter by project name")
2099
1452
  },
2100
1453
  async (args) => {
2101
- const payload = {
2102
- agentType: args.agent_type,
2103
- projectName: args.project_name,
2104
- projectPath: args.project_path,
2105
- ...args.prompt && { initialPrompt: args.prompt },
2106
- ...args.branch && { branch: args.branch },
1454
+ try {
1455
+ let sessions = await rpcCall("terminal:listActive");
1456
+ if (args.project_name) {
1457
+ sessions = sessions.filter((s) => s.projectName === args.project_name);
1458
+ }
1459
+ const summary = sessions.map((s) => ({
1460
+ id: s.id,
1461
+ agentType: s.agentType,
1462
+ projectName: s.projectName,
1463
+ status: s.status,
1464
+ displayName: s.displayName,
1465
+ branch: s.branch,
1466
+ pid: s.pid
1467
+ }));
1468
+ return { content: [{ type: "text", text: JSON.stringify(summary, null, 2) }] };
1469
+ } catch (err) {
1470
+ return {
1471
+ content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : err}` }],
1472
+ isError: true
1473
+ };
1474
+ }
1475
+ }
1476
+ );
1477
+ server.tool(
1478
+ "list_recent_sessions",
1479
+ "List recent session history for a project. Requires the VibeGrid app to be running.",
1480
+ {
1481
+ project_path: V.absolutePath.optional().describe("Filter by project path")
1482
+ },
1483
+ async (args) => {
1484
+ try {
1485
+ const sessions = await rpcCall("sessions:getRecent", args.project_path);
1486
+ return { content: [{ type: "text", text: JSON.stringify(sessions, null, 2) }] };
1487
+ } catch (err) {
1488
+ return {
1489
+ content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : err}` }],
1490
+ isError: true
1491
+ };
1492
+ }
1493
+ }
1494
+ );
1495
+ server.tool(
1496
+ "list_archived_sessions",
1497
+ "List archived sessions. Requires the VibeGrid app to be running.",
1498
+ async () => {
1499
+ try {
1500
+ const sessions = await rpcCall("session:listArchived");
1501
+ return { content: [{ type: "text", text: JSON.stringify(sessions, null, 2) }] };
1502
+ } catch (err) {
1503
+ return {
1504
+ content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : err}` }],
1505
+ isError: true
1506
+ };
1507
+ }
1508
+ }
1509
+ );
1510
+ server.tool(
1511
+ "launch_agent",
1512
+ "Launch an AI agent in a new terminal session. Requires the VibeGrid app to be running.",
1513
+ {
1514
+ agent_type: z4.enum(AGENT_TYPES3).describe("Agent type to launch"),
1515
+ project_name: V.name.describe("Project name"),
1516
+ project_path: V.absolutePath.describe("Absolute path to project directory"),
1517
+ prompt: V.prompt.optional().describe("Initial prompt to send to the agent"),
1518
+ branch: V.shortText.optional().describe("Git branch to checkout"),
1519
+ use_worktree: z4.boolean().optional().describe("Create a git worktree"),
1520
+ display_name: V.shortText.optional().describe("Display name for the session")
1521
+ },
1522
+ async (args) => {
1523
+ const payload = {
1524
+ agentType: args.agent_type,
1525
+ projectName: args.project_name,
1526
+ projectPath: args.project_path,
1527
+ ...args.prompt && { initialPrompt: args.prompt },
1528
+ ...args.branch && { branch: args.branch },
1529
+ ...args.use_worktree && { useWorktree: args.use_worktree },
1530
+ ...args.display_name && { displayName: args.display_name }
1531
+ };
1532
+ try {
1533
+ const session = await rpcCall("terminal:create", payload);
1534
+ return {
1535
+ content: [
1536
+ {
1537
+ type: "text",
1538
+ text: JSON.stringify(
1539
+ {
1540
+ id: session.id,
1541
+ agentType: session.agentType,
1542
+ projectName: session.projectName,
1543
+ pid: session.pid,
1544
+ status: session.status
1545
+ },
1546
+ null,
1547
+ 2
1548
+ )
1549
+ }
1550
+ ]
1551
+ };
1552
+ } catch (err) {
1553
+ return {
1554
+ content: [
1555
+ {
1556
+ type: "text",
1557
+ text: `Error launching agent: ${err instanceof Error ? err.message : err}`
1558
+ }
1559
+ ],
1560
+ isError: true
1561
+ };
1562
+ }
1563
+ }
1564
+ );
1565
+ server.tool(
1566
+ "launch_headless",
1567
+ "Launch a headless (no UI) agent session. Requires the VibeGrid app to be running.",
1568
+ {
1569
+ agent_type: z4.enum(AGENT_TYPES3).describe("Agent type to launch"),
1570
+ project_name: V.name.describe("Project name"),
1571
+ project_path: V.absolutePath.describe("Absolute path to project directory"),
1572
+ prompt: V.prompt.optional().describe("Initial prompt to send to the agent"),
1573
+ branch: V.shortText.optional().describe("Git branch to checkout"),
1574
+ use_worktree: z4.boolean().optional().describe("Create a git worktree"),
1575
+ display_name: V.shortText.optional().describe("Display name for the session")
1576
+ },
1577
+ async (args) => {
1578
+ const payload = {
1579
+ agentType: args.agent_type,
1580
+ projectName: args.project_name,
1581
+ projectPath: args.project_path,
1582
+ ...args.prompt && { initialPrompt: args.prompt },
1583
+ ...args.branch && { branch: args.branch },
2107
1584
  ...args.use_worktree && { useWorktree: args.use_worktree },
2108
1585
  ...args.display_name && { displayName: args.display_name }
2109
1586
  };
2110
1587
  try {
2111
- const session = ptyManager2.createPty(payload);
1588
+ const session = await rpcCall("headless:create", payload);
2112
1589
  return {
2113
1590
  content: [
2114
1591
  {
@@ -2128,37 +1605,79 @@ function registerSessionTools(server, deps) {
2128
1605
  ]
2129
1606
  };
2130
1607
  } catch (err) {
2131
- return { content: [{ type: "text", text: `Error launching agent: ${err}` }], isError: true };
1608
+ return {
1609
+ content: [
1610
+ {
1611
+ type: "text",
1612
+ text: `Error launching headless agent: ${err instanceof Error ? err.message : err}`
1613
+ }
1614
+ ],
1615
+ isError: true
1616
+ };
2132
1617
  }
2133
1618
  }
2134
1619
  );
2135
1620
  server.tool(
2136
1621
  "kill_session",
2137
- "Kill a terminal session",
1622
+ "Kill a terminal session. Requires the VibeGrid app to be running.",
2138
1623
  { id: V.id.describe("Session ID to kill") },
2139
1624
  async (args) => {
2140
1625
  try {
2141
- ptyManager2.killPty(args.id);
1626
+ await rpcCall("terminal:kill", args.id);
2142
1627
  return { content: [{ type: "text", text: `Killed session: ${args.id}` }] };
2143
1628
  } catch (err) {
2144
- return { content: [{ type: "text", text: `Error killing session: ${err}` }], isError: true };
1629
+ return {
1630
+ content: [
1631
+ {
1632
+ type: "text",
1633
+ text: `Error killing session: ${err instanceof Error ? err.message : err}`
1634
+ }
1635
+ ],
1636
+ isError: true
1637
+ };
1638
+ }
1639
+ }
1640
+ );
1641
+ server.tool(
1642
+ "kill_headless",
1643
+ "Kill a headless agent session. Requires the VibeGrid app to be running.",
1644
+ { id: V.id.describe("Headless session ID to kill") },
1645
+ async (args) => {
1646
+ try {
1647
+ await rpcCall("headless:kill", args.id);
1648
+ return { content: [{ type: "text", text: `Killed headless session: ${args.id}` }] };
1649
+ } catch (err) {
1650
+ return {
1651
+ content: [
1652
+ {
1653
+ type: "text",
1654
+ text: `Error killing headless session: ${err instanceof Error ? err.message : err}`
1655
+ }
1656
+ ],
1657
+ isError: true
1658
+ };
2145
1659
  }
2146
1660
  }
2147
1661
  );
2148
1662
  server.tool(
2149
1663
  "write_to_terminal",
2150
- "Send input to a running terminal session",
1664
+ "Send input to a running terminal session. Requires the VibeGrid app to be running.",
2151
1665
  {
2152
1666
  id: V.id.describe("Session ID"),
2153
1667
  data: z4.string().max(5e4, "Data must be 50000 characters or less").describe("Data to write (text input to send to the agent)")
2154
1668
  },
2155
1669
  async (args) => {
2156
1670
  try {
2157
- ptyManager2.writeToPty(args.id, args.data);
1671
+ await rpcNotify("terminal:write", { id: args.id, data: args.data });
2158
1672
  return { content: [{ type: "text", text: `Wrote to session: ${args.id}` }] };
2159
1673
  } catch (err) {
2160
1674
  return {
2161
- content: [{ type: "text", text: `Error writing to terminal: ${err}` }],
1675
+ content: [
1676
+ {
1677
+ type: "text",
1678
+ text: `Error writing to terminal: ${err instanceof Error ? err.message : err}`
1679
+ }
1680
+ ],
2162
1681
  isError: true
2163
1682
  };
2164
1683
  }
@@ -2167,7 +1686,7 @@ function registerSessionTools(server, deps) {
2167
1686
  }
2168
1687
 
2169
1688
  // src/tools/workflows.ts
2170
- import crypto4 from "crypto";
1689
+ import crypto2 from "crypto";
2171
1690
  import { z as z5 } from "zod";
2172
1691
  var launchAgentConfigSchema = z5.object({
2173
1692
  agentType: z5.enum(["claude", "copilot", "codex", "opencode", "gemini"]),
@@ -2215,7 +1734,7 @@ function buildGraphFromFlat(trigger, actions) {
2215
1734
  const nodes = [];
2216
1735
  const edges = [];
2217
1736
  const triggerNode = {
2218
- id: crypto4.randomUUID(),
1737
+ id: crypto2.randomUUID(),
2219
1738
  type: "trigger",
2220
1739
  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",
2221
1740
  config: trigger,
@@ -2226,7 +1745,7 @@ function buildGraphFromFlat(trigger, actions) {
2226
1745
  const NODE_GAP = 140;
2227
1746
  for (let i = 0; i < actions.length; i++) {
2228
1747
  const action = actions[i];
2229
- const nodeId = crypto4.randomUUID();
1748
+ const nodeId = crypto2.randomUUID();
2230
1749
  nodes.push({
2231
1750
  id: nodeId,
2232
1751
  type: "launchAgent",
@@ -2235,7 +1754,7 @@ function buildGraphFromFlat(trigger, actions) {
2235
1754
  position: { x: 0, y: (i + 1) * NODE_GAP }
2236
1755
  });
2237
1756
  edges.push({
2238
- id: crypto4.randomUUID(),
1757
+ id: crypto2.randomUUID(),
2239
1758
  source: prevId,
2240
1759
  target: nodeId
2241
1760
  });
@@ -2243,12 +1762,21 @@ function buildGraphFromFlat(trigger, actions) {
2243
1762
  }
2244
1763
  return { nodes, edges };
2245
1764
  }
2246
- function registerWorkflowTools(server, deps) {
2247
- const { configManager: configManager2, scheduler: scheduler2 } = deps;
2248
- server.tool("list_workflows", "List all workflows", async () => {
2249
- const workflows = dbListWorkflows();
2250
- return { content: [{ type: "text", text: JSON.stringify(workflows, null, 2) }] };
2251
- });
1765
+ function registerWorkflowTools(server) {
1766
+ server.tool(
1767
+ "list_workflows",
1768
+ "List all workflows, optionally filtered by workspace",
1769
+ {
1770
+ workspace_id: V.id.optional().describe("Filter by workspace ID")
1771
+ },
1772
+ async (args) => {
1773
+ let workflows = dbListWorkflows();
1774
+ if (args.workspace_id) {
1775
+ workflows = workflows.filter((w) => (w.workspaceId ?? "personal") === args.workspace_id);
1776
+ }
1777
+ return { content: [{ type: "text", text: JSON.stringify(workflows, null, 2) }] };
1778
+ }
1779
+ );
2252
1780
  server.tool(
2253
1781
  "create_workflow",
2254
1782
  "Create a new workflow. Accepts either full nodes/edges or a convenience flat format (trigger + actions array).",
@@ -2277,7 +1805,7 @@ function registerWorkflowTools(server, deps) {
2277
1805
  edges = graph.edges;
2278
1806
  }
2279
1807
  const workflow = {
2280
- id: crypto4.randomUUID(),
1808
+ id: crypto2.randomUUID(),
2281
1809
  name: args.name,
2282
1810
  icon: args.icon ?? "zap",
2283
1811
  iconColor: args.icon_color ?? "#6366f1",
@@ -2287,8 +1815,6 @@ function registerWorkflowTools(server, deps) {
2287
1815
  ...args.stagger_delay_ms && { staggerDelayMs: args.stagger_delay_ms }
2288
1816
  };
2289
1817
  dbInsertWorkflow(workflow);
2290
- scheduler2.syncSchedules(dbListWorkflows());
2291
- configManager2.notifyChanged();
2292
1818
  return { content: [{ type: "text", text: JSON.stringify(workflow, null, 2) }] };
2293
1819
  }
2294
1820
  );
@@ -2323,8 +1849,6 @@ function registerWorkflowTools(server, deps) {
2323
1849
  if (args.enabled !== void 0) updates.enabled = args.enabled;
2324
1850
  if (args.stagger_delay_ms !== void 0) updates.staggerDelayMs = args.stagger_delay_ms;
2325
1851
  dbUpdateWorkflow(args.id, updates);
2326
- scheduler2.syncSchedules(dbListWorkflows());
2327
- configManager2.notifyChanged();
2328
1852
  return {
2329
1853
  content: [{ type: "text", text: JSON.stringify({ ...workflow, ...updates }, null, 2) }]
2330
1854
  };
@@ -2344,11 +1868,308 @@ function registerWorkflowTools(server, deps) {
2344
1868
  };
2345
1869
  }
2346
1870
  dbDeleteWorkflow(args.id);
2347
- scheduler2.syncSchedules(dbListWorkflows());
2348
- configManager2.notifyChanged();
2349
1871
  return { content: [{ type: "text", text: `Deleted workflow: ${workflow.name}` }] };
2350
1872
  }
2351
1873
  );
1874
+ server.tool(
1875
+ "list_workflow_runs",
1876
+ "List execution history for a workflow",
1877
+ {
1878
+ workflow_id: V.id.describe("Workflow ID"),
1879
+ limit: z5.number().int().min(1).max(100).optional().describe("Max results (default: 20)")
1880
+ },
1881
+ async (args) => {
1882
+ const runs = listWorkflowRuns(args.workflow_id, args.limit ?? 20);
1883
+ return { content: [{ type: "text", text: JSON.stringify(runs, null, 2) }] };
1884
+ }
1885
+ );
1886
+ server.tool(
1887
+ "list_workflow_runs_by_task",
1888
+ "List workflow executions triggered by a specific task",
1889
+ {
1890
+ task_id: V.id.describe("Task ID"),
1891
+ limit: z5.number().int().min(1).max(100).optional().describe("Max results (default: 20)")
1892
+ },
1893
+ async (args) => {
1894
+ const runs = listWorkflowRunsByTask(args.task_id, args.limit ?? 20);
1895
+ return { content: [{ type: "text", text: JSON.stringify(runs, null, 2) }] };
1896
+ }
1897
+ );
1898
+ server.tool(
1899
+ "get_scheduler_log",
1900
+ "Get scheduler execution log for a workflow. Requires the VibeGrid app to be running.",
1901
+ {
1902
+ workflow_id: V.id.optional().describe("Workflow ID (omit for all workflows)")
1903
+ },
1904
+ async (args) => {
1905
+ try {
1906
+ const log2 = await rpcCall("scheduler:getLog", args.workflow_id);
1907
+ return { content: [{ type: "text", text: JSON.stringify(log2, null, 2) }] };
1908
+ } catch (err) {
1909
+ return {
1910
+ content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : err}` }],
1911
+ isError: true
1912
+ };
1913
+ }
1914
+ }
1915
+ );
1916
+ server.tool(
1917
+ "get_next_scheduled_run",
1918
+ "Get the next scheduled run time for a workflow. Requires the VibeGrid app to be running.",
1919
+ {
1920
+ workflow_id: V.id.describe("Workflow ID")
1921
+ },
1922
+ async (args) => {
1923
+ try {
1924
+ const nextRun = await rpcCall("scheduler:getNextRun", args.workflow_id);
1925
+ return {
1926
+ content: [
1927
+ {
1928
+ type: "text",
1929
+ text: nextRun ? JSON.stringify({ nextRun }, null, 2) : "No scheduled run (workflow may be manual or disabled)"
1930
+ }
1931
+ ]
1932
+ };
1933
+ } catch (err) {
1934
+ return {
1935
+ content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : err}` }],
1936
+ isError: true
1937
+ };
1938
+ }
1939
+ }
1940
+ );
1941
+ }
1942
+
1943
+ // src/tools/git.ts
1944
+ import { z as z6 } from "zod";
1945
+
1946
+ // ../server/src/git-utils.ts
1947
+ import { execFileSync } from "child_process";
1948
+ import path5 from "path";
1949
+ import fs4 from "fs";
1950
+ import crypto3 from "crypto";
1951
+ var EXEC_OPTS = {
1952
+ encoding: "utf-8",
1953
+ stdio: ["pipe", "pipe", "pipe"]
1954
+ };
1955
+ function getGitBranch(projectPath) {
1956
+ try {
1957
+ const branch = execFileSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
1958
+ cwd: projectPath,
1959
+ ...EXEC_OPTS,
1960
+ timeout: 3e3
1961
+ }).trim();
1962
+ return branch && branch !== "HEAD" ? branch : null;
1963
+ } catch {
1964
+ return null;
1965
+ }
1966
+ }
1967
+ function listBranches(projectPath) {
1968
+ try {
1969
+ const output = execFileSync("git", ["branch", "--format=%(refname:short)"], {
1970
+ cwd: projectPath,
1971
+ ...EXEC_OPTS,
1972
+ timeout: 5e3
1973
+ }).trim();
1974
+ return output ? output.split("\n").map((b) => b.trim()).filter(Boolean) : [];
1975
+ } catch {
1976
+ return [];
1977
+ }
1978
+ }
1979
+ function listRemoteBranches(projectPath) {
1980
+ try {
1981
+ execFileSync("git", ["fetch", "--prune"], {
1982
+ cwd: projectPath,
1983
+ ...EXEC_OPTS,
1984
+ timeout: 15e3
1985
+ });
1986
+ const output = execFileSync("git", ["branch", "-r", "--format=%(refname:short)"], {
1987
+ cwd: projectPath,
1988
+ ...EXEC_OPTS,
1989
+ timeout: 5e3
1990
+ }).trim();
1991
+ return output ? output.split("\n").map((b) => b.trim().replace(/^origin\//, "")).filter((b) => b && b !== "HEAD") : [];
1992
+ } catch {
1993
+ return [];
1994
+ }
1995
+ }
1996
+ function createWorktree(projectPath, branch) {
1997
+ const projectName = path5.basename(projectPath);
1998
+ const shortId = crypto3.randomUUID().slice(0, 8);
1999
+ const baseDir = path5.join(path5.dirname(projectPath), ".vibegrid-worktrees", projectName);
2000
+ const worktreeDir = path5.join(baseDir, `${branch}-${shortId}`);
2001
+ fs4.mkdirSync(baseDir, { recursive: true });
2002
+ const localBranches = listBranches(projectPath);
2003
+ if (localBranches.includes(branch)) {
2004
+ try {
2005
+ execFileSync("git", ["worktree", "add", worktreeDir, branch], {
2006
+ cwd: projectPath,
2007
+ ...EXEC_OPTS,
2008
+ timeout: 3e4
2009
+ });
2010
+ } catch {
2011
+ const newBranch = `${branch}-worktree-${shortId}`;
2012
+ execFileSync("git", ["worktree", "add", "-b", newBranch, worktreeDir, branch], {
2013
+ cwd: projectPath,
2014
+ ...EXEC_OPTS,
2015
+ timeout: 3e4
2016
+ });
2017
+ return { worktreePath: worktreeDir, branch: newBranch };
2018
+ }
2019
+ } else {
2020
+ execFileSync("git", ["worktree", "add", "-b", branch, worktreeDir], {
2021
+ cwd: projectPath,
2022
+ ...EXEC_OPTS,
2023
+ timeout: 3e4
2024
+ });
2025
+ }
2026
+ return { worktreePath: worktreeDir, branch };
2027
+ }
2028
+ function isWorktreeDirty(worktreePath) {
2029
+ try {
2030
+ const output = execFileSync("git", ["status", "--porcelain"], {
2031
+ cwd: worktreePath,
2032
+ ...EXEC_OPTS,
2033
+ timeout: 5e3
2034
+ }).trim();
2035
+ return output.length > 0;
2036
+ } catch {
2037
+ return true;
2038
+ }
2039
+ }
2040
+ function getGitDiffStat(cwd) {
2041
+ try {
2042
+ const output = execFileSync("git", ["diff", "HEAD", "--numstat"], {
2043
+ cwd,
2044
+ ...EXEC_OPTS,
2045
+ timeout: 1e4
2046
+ }).trim();
2047
+ if (!output) return { filesChanged: 0, insertions: 0, deletions: 0 };
2048
+ let insertions = 0;
2049
+ let deletions = 0;
2050
+ let filesChanged = 0;
2051
+ for (const line of output.split("\n")) {
2052
+ const parts = line.split(" ");
2053
+ if (parts[0] === "-") {
2054
+ filesChanged++;
2055
+ continue;
2056
+ }
2057
+ insertions += parseInt(parts[0], 10) || 0;
2058
+ deletions += parseInt(parts[1], 10) || 0;
2059
+ filesChanged++;
2060
+ }
2061
+ return { filesChanged, insertions, deletions };
2062
+ } catch {
2063
+ return null;
2064
+ }
2065
+ }
2066
+ function getGitDiffFull(cwd) {
2067
+ try {
2068
+ const stat = getGitDiffStat(cwd);
2069
+ if (!stat) return null;
2070
+ const MAX_DIFF_SIZE = 500 * 1024;
2071
+ let rawDiff = execFileSync("git", ["diff", "HEAD", "-U3"], {
2072
+ cwd,
2073
+ ...EXEC_OPTS,
2074
+ timeout: 15e3,
2075
+ maxBuffer: MAX_DIFF_SIZE * 2
2076
+ });
2077
+ if (rawDiff.length > MAX_DIFF_SIZE) {
2078
+ rawDiff = rawDiff.slice(0, MAX_DIFF_SIZE) + "\n\n... diff truncated (too large) ...\n";
2079
+ }
2080
+ const numstatOutput = execFileSync("git", ["diff", "HEAD", "--numstat"], {
2081
+ cwd,
2082
+ ...EXEC_OPTS,
2083
+ timeout: 1e4
2084
+ }).trim();
2085
+ const fileStats = /* @__PURE__ */ new Map();
2086
+ if (numstatOutput) {
2087
+ for (const line of numstatOutput.split("\n")) {
2088
+ const parts = line.split(" ");
2089
+ if (parts.length >= 3) {
2090
+ const ins = parts[0] === "-" ? 0 : parseInt(parts[0], 10) || 0;
2091
+ const del = parts[1] === "-" ? 0 : parseInt(parts[1], 10) || 0;
2092
+ fileStats.set(parts.slice(2).join(" "), { insertions: ins, deletions: del });
2093
+ }
2094
+ }
2095
+ }
2096
+ const fileDiffs = [];
2097
+ const diffSections = rawDiff.split(/^diff --git /m).filter(Boolean);
2098
+ for (const section of diffSections) {
2099
+ const fullSection = "diff --git " + section;
2100
+ const plusMatch = fullSection.match(/^\+\+\+ b\/(.+)$/m);
2101
+ const minusMatch = fullSection.match(/^--- a\/(.+)$/m);
2102
+ const filePath = plusMatch?.[1] || minusMatch?.[1]?.replace(/^\/dev\/null$/, "") || "unknown";
2103
+ let status = "modified";
2104
+ if (fullSection.includes("--- /dev/null")) {
2105
+ status = "added";
2106
+ } else if (fullSection.includes("+++ /dev/null")) {
2107
+ status = "deleted";
2108
+ } else if (fullSection.includes("rename from")) {
2109
+ status = "renamed";
2110
+ }
2111
+ const stats = fileStats.get(filePath) || { insertions: 0, deletions: 0 };
2112
+ fileDiffs.push({
2113
+ filePath,
2114
+ status,
2115
+ insertions: stats.insertions,
2116
+ deletions: stats.deletions,
2117
+ diff: fullSection
2118
+ });
2119
+ }
2120
+ return { stat, files: fileDiffs };
2121
+ } catch {
2122
+ return null;
2123
+ }
2124
+ }
2125
+ function gitCommit(cwd, message, includeUnstaged) {
2126
+ try {
2127
+ if (includeUnstaged) {
2128
+ execFileSync("git", ["add", "-A"], { cwd, ...EXEC_OPTS, timeout: 1e4 });
2129
+ }
2130
+ execFileSync("git", ["commit", "-m", message], {
2131
+ cwd,
2132
+ ...EXEC_OPTS,
2133
+ timeout: 15e3
2134
+ });
2135
+ return { success: true };
2136
+ } catch (err) {
2137
+ const msg = err instanceof Error ? err.message : String(err);
2138
+ return { success: false, error: msg };
2139
+ }
2140
+ }
2141
+ function gitPush(cwd) {
2142
+ try {
2143
+ execFileSync("git", ["push"], { cwd, ...EXEC_OPTS, timeout: 3e4 });
2144
+ return { success: true };
2145
+ } catch (err) {
2146
+ const msg = err instanceof Error ? err.message : String(err);
2147
+ return { success: false, error: msg };
2148
+ }
2149
+ }
2150
+ function listWorktrees(projectPath) {
2151
+ try {
2152
+ const output = execFileSync("git", ["worktree", "list", "--porcelain"], {
2153
+ cwd: projectPath,
2154
+ ...EXEC_OPTS,
2155
+ timeout: 5e3
2156
+ }).trim();
2157
+ if (!output) return [];
2158
+ const worktrees = [];
2159
+ const blocks = output.split("\n\n");
2160
+ for (const block of blocks) {
2161
+ const lines = block.split("\n");
2162
+ const wtPath = lines.find((l) => l.startsWith("worktree "))?.replace("worktree ", "");
2163
+ const branchLine = lines.find((l) => l.startsWith("branch "));
2164
+ const branch = branchLine?.replace("branch refs/heads/", "") || "detached";
2165
+ if (wtPath) {
2166
+ worktrees.push({ path: wtPath, branch, isMain: worktrees.length === 0 });
2167
+ }
2168
+ }
2169
+ return worktrees;
2170
+ } catch {
2171
+ return [];
2172
+ }
2352
2173
  }
2353
2174
 
2354
2175
  // src/tools/git.ts
@@ -2372,6 +2193,22 @@ function registerGitTools(server) {
2372
2193
  }
2373
2194
  }
2374
2195
  );
2196
+ server.tool(
2197
+ "list_remote_branches",
2198
+ "List remote git branches for a project",
2199
+ { project_path: V.absolutePath.describe("Absolute path to project directory") },
2200
+ async (args) => {
2201
+ try {
2202
+ const remote = listRemoteBranches(args.project_path);
2203
+ return { content: [{ type: "text", text: JSON.stringify(remote, null, 2) }] };
2204
+ } catch (err) {
2205
+ return {
2206
+ content: [{ type: "text", text: `Error listing remote branches: ${err}` }],
2207
+ isError: true
2208
+ };
2209
+ }
2210
+ }
2211
+ );
2375
2212
  server.tool(
2376
2213
  "get_diff",
2377
2214
  "Get git diff for a project (staged and unstaged changes)",
@@ -2390,30 +2227,218 @@ function registerGitTools(server) {
2390
2227
  }
2391
2228
  }
2392
2229
  );
2230
+ server.tool(
2231
+ "get_diff_stat",
2232
+ "Get a summary of git changes (files changed, insertions, deletions)",
2233
+ { project_path: V.absolutePath.describe("Absolute path to project directory") },
2234
+ async (args) => {
2235
+ try {
2236
+ const result = getGitDiffStat(args.project_path);
2237
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
2238
+ } catch (err) {
2239
+ return {
2240
+ content: [{ type: "text", text: `Error getting diff stat: ${err}` }],
2241
+ isError: true
2242
+ };
2243
+ }
2244
+ }
2245
+ );
2246
+ server.tool(
2247
+ "git_commit",
2248
+ "Create a git commit",
2249
+ {
2250
+ project_path: V.absolutePath.describe("Absolute path to project directory"),
2251
+ message: V.description.describe("Commit message"),
2252
+ include_unstaged: z6.boolean().optional().describe("Stage all changes before committing")
2253
+ },
2254
+ async (args) => {
2255
+ try {
2256
+ const result = gitCommit(args.project_path, args.message, args.include_unstaged ?? false);
2257
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
2258
+ } catch (err) {
2259
+ return {
2260
+ content: [{ type: "text", text: `Error committing: ${err}` }],
2261
+ isError: true
2262
+ };
2263
+ }
2264
+ }
2265
+ );
2266
+ server.tool(
2267
+ "git_push",
2268
+ "Push commits to the remote repository",
2269
+ { project_path: V.absolutePath.describe("Absolute path to project directory") },
2270
+ async (args) => {
2271
+ try {
2272
+ const result = gitPush(args.project_path);
2273
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
2274
+ } catch (err) {
2275
+ return {
2276
+ content: [{ type: "text", text: `Error pushing: ${err}` }],
2277
+ isError: true
2278
+ };
2279
+ }
2280
+ }
2281
+ );
2282
+ server.tool(
2283
+ "list_worktrees",
2284
+ "List git worktrees for a project",
2285
+ { project_path: V.absolutePath.describe("Absolute path to project directory") },
2286
+ async (args) => {
2287
+ try {
2288
+ const worktrees = listWorktrees(args.project_path);
2289
+ return { content: [{ type: "text", text: JSON.stringify(worktrees, null, 2) }] };
2290
+ } catch (err) {
2291
+ return {
2292
+ content: [{ type: "text", text: `Error listing worktrees: ${err}` }],
2293
+ isError: true
2294
+ };
2295
+ }
2296
+ }
2297
+ );
2298
+ server.tool(
2299
+ "create_worktree",
2300
+ "Create a git worktree for a branch",
2301
+ {
2302
+ project_path: V.absolutePath.describe("Absolute path to project directory"),
2303
+ branch: V.shortText.describe("Branch name for the worktree")
2304
+ },
2305
+ async (args) => {
2306
+ try {
2307
+ const worktreePath = createWorktree(args.project_path, args.branch);
2308
+ return {
2309
+ content: [{ type: "text", text: JSON.stringify({ path: worktreePath }, null, 2) }]
2310
+ };
2311
+ } catch (err) {
2312
+ return {
2313
+ content: [{ type: "text", text: `Error creating worktree: ${err}` }],
2314
+ isError: true
2315
+ };
2316
+ }
2317
+ }
2318
+ );
2319
+ server.tool(
2320
+ "worktree_dirty",
2321
+ "Check if a worktree has uncommitted changes",
2322
+ { worktree_path: V.absolutePath.describe("Absolute path to the worktree") },
2323
+ async (args) => {
2324
+ try {
2325
+ const dirty = isWorktreeDirty(args.worktree_path);
2326
+ return { content: [{ type: "text", text: JSON.stringify({ dirty }, null, 2) }] };
2327
+ } catch (err) {
2328
+ return {
2329
+ content: [{ type: "text", text: `Error checking worktree: ${err}` }],
2330
+ isError: true
2331
+ };
2332
+ }
2333
+ }
2334
+ );
2393
2335
  }
2394
2336
 
2395
2337
  // src/tools/config.ts
2396
- function registerConfigTools(server, deps) {
2397
- const { configManager: configManager2 } = deps;
2338
+ function registerConfigTools(server) {
2398
2339
  server.tool(
2399
2340
  "get_config",
2400
2341
  "Get the full VibeGrid configuration (projects, tasks, workflows, settings)",
2401
2342
  async () => {
2402
- const config = configManager2.loadConfig();
2343
+ const config = configManager.loadConfig();
2403
2344
  return { content: [{ type: "text", text: JSON.stringify(config, null, 2) }] };
2404
2345
  }
2405
2346
  );
2406
2347
  }
2407
2348
 
2349
+ // src/tools/workspaces.ts
2350
+ import crypto4 from "crypto";
2351
+ import { z as z7 } from "zod";
2352
+ function registerWorkspaceTools(server) {
2353
+ server.tool("list_workspaces", "List all workspaces", async () => {
2354
+ const workspaces = dbListWorkspaces();
2355
+ return { content: [{ type: "text", text: JSON.stringify(workspaces, null, 2) }] };
2356
+ });
2357
+ server.tool(
2358
+ "create_workspace",
2359
+ "Create a new workspace for organizing projects",
2360
+ {
2361
+ name: V.name.describe("Workspace name"),
2362
+ icon: V.shortText.optional().describe("Lucide icon name"),
2363
+ icon_color: V.hexColor.optional().describe("Hex color for icon")
2364
+ },
2365
+ async (args) => {
2366
+ const existing = dbListWorkspaces();
2367
+ const maxOrder = existing.reduce((max, w) => Math.max(max, w.order), 0);
2368
+ const workspace = {
2369
+ id: crypto4.randomUUID(),
2370
+ name: args.name,
2371
+ order: maxOrder + 1,
2372
+ ...args.icon && { icon: args.icon },
2373
+ ...args.icon_color && { iconColor: args.icon_color }
2374
+ };
2375
+ dbInsertWorkspace(workspace);
2376
+ return { content: [{ type: "text", text: JSON.stringify(workspace, null, 2) }] };
2377
+ }
2378
+ );
2379
+ server.tool(
2380
+ "update_workspace",
2381
+ "Update a workspace's properties",
2382
+ {
2383
+ id: V.id.describe("Workspace ID"),
2384
+ name: V.name.optional().describe("New name"),
2385
+ icon: V.shortText.optional().describe("Lucide icon name"),
2386
+ icon_color: V.hexColor.optional().describe("Hex color for icon"),
2387
+ order: z7.number().int().min(0).optional().describe("Sort order")
2388
+ },
2389
+ async (args) => {
2390
+ const existing = dbListWorkspaces();
2391
+ if (!existing.find((w) => w.id === args.id)) {
2392
+ return {
2393
+ content: [{ type: "text", text: `Error: workspace "${args.id}" not found` }],
2394
+ isError: true
2395
+ };
2396
+ }
2397
+ const updates = {};
2398
+ if (args.name !== void 0) updates.name = args.name;
2399
+ if (args.icon !== void 0) updates.icon = args.icon;
2400
+ if (args.icon_color !== void 0) updates.iconColor = args.icon_color;
2401
+ if (args.order !== void 0) updates.order = args.order;
2402
+ dbUpdateWorkspace(args.id, updates);
2403
+ const updated = dbListWorkspaces().find((w) => w.id === args.id);
2404
+ return { content: [{ type: "text", text: JSON.stringify(updated, null, 2) }] };
2405
+ }
2406
+ );
2407
+ server.tool(
2408
+ "delete_workspace",
2409
+ "Delete a workspace",
2410
+ { id: V.id.describe("Workspace ID") },
2411
+ async (args) => {
2412
+ if (args.id === "personal") {
2413
+ return {
2414
+ content: [{ type: "text", text: "Error: cannot delete the default workspace" }],
2415
+ isError: true
2416
+ };
2417
+ }
2418
+ const existing = dbListWorkspaces();
2419
+ const workspace = existing.find((w) => w.id === args.id);
2420
+ if (!workspace) {
2421
+ return {
2422
+ content: [{ type: "text", text: `Error: workspace "${args.id}" not found` }],
2423
+ isError: true
2424
+ };
2425
+ }
2426
+ dbDeleteWorkspace(args.id);
2427
+ return { content: [{ type: "text", text: `Deleted workspace: ${workspace.name}` }] };
2428
+ }
2429
+ );
2430
+ }
2431
+
2408
2432
  // src/server.ts
2409
- function createMcpServer(deps, version) {
2433
+ function createMcpServer(version) {
2410
2434
  const server = new McpServer({ name: "vibegrid", version }, { capabilities: { tools: {} } });
2411
2435
  registerGitTools(server);
2412
- registerConfigTools(server, deps);
2413
- registerProjectTools(server, deps);
2414
- registerTaskTools(server, deps);
2415
- registerSessionTools(server, deps);
2416
- registerWorkflowTools(server, deps);
2436
+ registerConfigTools(server);
2437
+ registerProjectTools(server);
2438
+ registerTaskTools(server);
2439
+ registerSessionTools(server);
2440
+ registerWorkflowTools(server);
2441
+ registerWorkspaceTools(server);
2417
2442
  return server;
2418
2443
  }
2419
2444
 
@@ -2426,19 +2451,11 @@ console.warn = (...args) => _origError("[mcp:warn]", ...args);
2426
2451
  console.error = (...args) => _origError("[mcp:error]", ...args);
2427
2452
  async function main() {
2428
2453
  configManager.init();
2429
- const config = configManager.loadConfig();
2430
- if (config.agentCommands) {
2431
- ptyManager.setAgentCommands(config.agentCommands);
2432
- }
2433
- ptyManager.setRemoteHosts(config.remoteHosts ?? []);
2434
- scheduler.syncSchedules(config.workflows ?? []);
2435
- const version = true ? "0.1.3" : createRequire(import.meta.url)("../package.json").version;
2436
- const server = createMcpServer({ configManager, ptyManager, scheduler }, version);
2454
+ const version = true ? "0.2.0" : createRequire(import.meta.url)("../package.json").version;
2455
+ const server = createMcpServer(version);
2437
2456
  const transport = new StdioServerTransport();
2438
2457
  await server.connect(transport);
2439
2458
  transport.onclose = () => {
2440
- scheduler.stopAll();
2441
- ptyManager.killAll();
2442
2459
  configManager.close();
2443
2460
  process.exit(0);
2444
2461
  };