@vibegrid/mcp 0.1.2 → 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 +986 -919
  2. package/package.json +5 -6
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
+ import "module";
4
5
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
6
 
6
7
  // ../server/src/config-manager.ts
@@ -55,72 +56,6 @@ var DEFAULT_WORKSPACE = {
55
56
  iconColor: "#6b7280",
56
57
  order: 0
57
58
  };
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
59
 
125
60
  // ../server/src/database.ts
126
61
  var CONFIG_DIR = path.join(os.homedir(), ".vibegrid");
@@ -800,6 +735,50 @@ function dbUpdateWorkflow(id, updates) {
800
735
  function dbDeleteWorkflow(id) {
801
736
  getDb().prepare("DELETE FROM workflows WHERE id = ?").run(id);
802
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
+ }
803
782
  function rowToTask(r) {
804
783
  return {
805
784
  id: r.id,
@@ -853,6 +832,67 @@ function rowToWorkspace(r) {
853
832
  order: r.order
854
833
  };
855
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
+ }
856
896
 
857
897
  // ../server/src/config-manager.ts
858
898
  var DB_DIR = path2.join(os2.homedir(), ".vibegrid");
@@ -940,792 +980,99 @@ var ConfigManager = class {
940
980
  };
941
981
  var configManager = new ConfigManager();
942
982
 
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";
983
+ // src/server.ts
984
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
948
985
 
949
- // ../server/src/git-utils.ts
950
- import { execFileSync } from "child_process";
951
- import path3 from "path";
952
- import fs3 from "fs";
986
+ // src/tools/tasks.ts
953
987
  import crypto from "crypto";
954
- var EXEC_OPTS = {
955
- encoding: "utf-8",
956
- 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
957
1012
  };
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;
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));
1042
1041
  }
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
- }
1042
+ if (args.assigned_agent) {
1043
+ tasks = tasks.filter((t) => t.assignedAgent === args.assigned_agent);
1080
1044
  }
1045
+ return { content: [{ type: "text", text: JSON.stringify(tasks, null, 2) }] };
1081
1046
  }
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";
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
+ };
1096
1067
  }
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 ?? "",
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 ?? "",
1729
1076
  status,
1730
1077
  order: maxOrder + 1,
1731
1078
  createdAt: now,
@@ -1736,7 +1083,6 @@ function registerTaskTools(server, deps) {
1736
1083
  ...(status === "done" || status === "cancelled") && { completedAt: now }
1737
1084
  };
1738
1085
  dbInsertTask(task);
1739
- configManager2.notifyChanged();
1740
1086
  return { content: [{ type: "text", text: JSON.stringify(task, null, 2) }] };
1741
1087
  }
1742
1088
  );
@@ -1788,7 +1134,6 @@ function registerTaskTools(server, deps) {
1788
1134
  if (!isDone && wasDone) updates.completedAt = void 0;
1789
1135
  }
1790
1136
  dbUpdateTask(args.id, updates);
1791
- configManager2.notifyChanged();
1792
1137
  const updated = dbGetTask(args.id);
1793
1138
  return { content: [{ type: "text", text: JSON.stringify(updated, null, 2) }] };
1794
1139
  }
@@ -1806,7 +1151,6 @@ function registerTaskTools(server, deps) {
1806
1151
  };
1807
1152
  }
1808
1153
  dbDeleteTask(args.id);
1809
- configManager2.notifyChanged();
1810
1154
  return { content: [{ type: "text", text: `Deleted task: ${task.title}` }] };
1811
1155
  }
1812
1156
  );
@@ -1853,12 +1197,12 @@ function registerTaskTools(server, deps) {
1853
1197
  };
1854
1198
  }
1855
1199
  const cwd = args.cwd || process.cwd();
1856
- const normalizedCwd = path4.resolve(cwd);
1200
+ const normalizedCwd = path3.resolve(cwd);
1857
1201
  const projects = dbListProjects();
1858
1202
  let matchedProject = null;
1859
1203
  let matchLen = 0;
1860
1204
  for (const p of projects) {
1861
- const normalizedPath = path4.resolve(p.path);
1205
+ const normalizedPath = path3.resolve(p.path);
1862
1206
  if (normalizedCwd.startsWith(normalizedPath) && normalizedPath.length > matchLen) {
1863
1207
  matchedProject = p;
1864
1208
  matchLen = normalizedPath.length;
@@ -1886,7 +1230,7 @@ function registerTaskTools(server, deps) {
1886
1230
  let matchedTask = null;
1887
1231
  for (const t of projectTasks) {
1888
1232
  if (t.worktreePath) {
1889
- const normalizedWorktree = path4.resolve(t.worktreePath);
1233
+ const normalizedWorktree = path3.resolve(t.worktreePath);
1890
1234
  if (normalizedCwd.startsWith(normalizedWorktree)) {
1891
1235
  matchedTask = t;
1892
1236
  break;
@@ -1930,12 +1274,21 @@ var AGENT_TYPES2 = [
1930
1274
  "opencode",
1931
1275
  "gemini"
1932
1276
  ];
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
- });
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
+ );
1939
1292
  server.tool(
1940
1293
  "create_project",
1941
1294
  "Create a new project",
@@ -1961,7 +1314,6 @@ function registerProjectTools(server, deps) {
1961
1314
  ...args.icon_color && { iconColor: args.icon_color }
1962
1315
  };
1963
1316
  dbInsertProject(project);
1964
- configManager2.notifyChanged();
1965
1317
  return { content: [{ type: "text", text: JSON.stringify(project, null, 2) }] };
1966
1318
  }
1967
1319
  );
@@ -1989,7 +1341,6 @@ function registerProjectTools(server, deps) {
1989
1341
  if (args.icon !== void 0) updates.icon = args.icon;
1990
1342
  if (args.icon_color !== void 0) updates.iconColor = args.icon_color;
1991
1343
  dbUpdateProject(args.name, updates);
1992
- configManager2.notifyChanged();
1993
1344
  const updated = dbGetProject(args.name);
1994
1345
  return { content: [{ type: "text", text: JSON.stringify(updated, null, 2) }] };
1995
1346
  }
@@ -2006,7 +1357,6 @@ function registerProjectTools(server, deps) {
2006
1357
  };
2007
1358
  }
2008
1359
  dbDeleteProject(args.name);
2009
- configManager2.notifyChanged();
2010
1360
  return { content: [{ type: "text", text: `Deleted project: ${args.name}` }] };
2011
1361
  }
2012
1362
  );
@@ -2014,6 +1364,78 @@ function registerProjectTools(server, deps) {
2014
1364
 
2015
1365
  // src/tools/sessions.ts
2016
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
2017
1439
  var AGENT_TYPES3 = [
2018
1440
  "claude",
2019
1441
  "copilot",
@@ -2021,24 +1443,73 @@ var AGENT_TYPES3 = [
2021
1443
  "opencode",
2022
1444
  "gemini"
2023
1445
  ];
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
- });
1446
+ function registerSessionTools(server) {
1447
+ server.tool(
1448
+ "list_sessions",
1449
+ "List all active terminal sessions. Requires the VibeGrid app to be running.",
1450
+ {
1451
+ project_name: V.name.optional().describe("Filter by project name")
1452
+ },
1453
+ async (args) => {
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
+ );
2039
1510
  server.tool(
2040
1511
  "launch_agent",
2041
- "Launch an AI agent in a new terminal session",
1512
+ "Launch an AI agent in a new terminal session. Requires the VibeGrid app to be running.",
2042
1513
  {
2043
1514
  agent_type: z4.enum(AGENT_TYPES3).describe("Agent type to launch"),
2044
1515
  project_name: V.name.describe("Project name"),
@@ -2059,7 +1530,7 @@ function registerSessionTools(server, deps) {
2059
1530
  ...args.display_name && { displayName: args.display_name }
2060
1531
  };
2061
1532
  try {
2062
- const session = ptyManager2.createPty(payload);
1533
+ const session = await rpcCall("terminal:create", payload);
2063
1534
  return {
2064
1535
  content: [
2065
1536
  {
@@ -2079,37 +1550,134 @@ function registerSessionTools(server, deps) {
2079
1550
  ]
2080
1551
  };
2081
1552
  } catch (err) {
2082
- return { content: [{ type: "text", text: `Error launching agent: ${err}` }], isError: true };
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 },
1584
+ ...args.use_worktree && { useWorktree: args.use_worktree },
1585
+ ...args.display_name && { displayName: args.display_name }
1586
+ };
1587
+ try {
1588
+ const session = await rpcCall("headless:create", payload);
1589
+ return {
1590
+ content: [
1591
+ {
1592
+ type: "text",
1593
+ text: JSON.stringify(
1594
+ {
1595
+ id: session.id,
1596
+ agentType: session.agentType,
1597
+ projectName: session.projectName,
1598
+ pid: session.pid,
1599
+ status: session.status
1600
+ },
1601
+ null,
1602
+ 2
1603
+ )
1604
+ }
1605
+ ]
1606
+ };
1607
+ } catch (err) {
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
+ };
2083
1617
  }
2084
1618
  }
2085
1619
  );
2086
1620
  server.tool(
2087
1621
  "kill_session",
2088
- "Kill a terminal session",
1622
+ "Kill a terminal session. Requires the VibeGrid app to be running.",
2089
1623
  { id: V.id.describe("Session ID to kill") },
2090
1624
  async (args) => {
2091
1625
  try {
2092
- ptyManager2.killPty(args.id);
1626
+ await rpcCall("terminal:kill", args.id);
2093
1627
  return { content: [{ type: "text", text: `Killed session: ${args.id}` }] };
2094
1628
  } catch (err) {
2095
- 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
+ };
2096
1659
  }
2097
1660
  }
2098
1661
  );
2099
1662
  server.tool(
2100
1663
  "write_to_terminal",
2101
- "Send input to a running terminal session",
1664
+ "Send input to a running terminal session. Requires the VibeGrid app to be running.",
2102
1665
  {
2103
1666
  id: V.id.describe("Session ID"),
2104
1667
  data: z4.string().max(5e4, "Data must be 50000 characters or less").describe("Data to write (text input to send to the agent)")
2105
1668
  },
2106
1669
  async (args) => {
2107
1670
  try {
2108
- ptyManager2.writeToPty(args.id, args.data);
1671
+ await rpcNotify("terminal:write", { id: args.id, data: args.data });
2109
1672
  return { content: [{ type: "text", text: `Wrote to session: ${args.id}` }] };
2110
1673
  } catch (err) {
2111
1674
  return {
2112
- 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
+ ],
2113
1681
  isError: true
2114
1682
  };
2115
1683
  }
@@ -2118,7 +1686,7 @@ function registerSessionTools(server, deps) {
2118
1686
  }
2119
1687
 
2120
1688
  // src/tools/workflows.ts
2121
- import crypto4 from "crypto";
1689
+ import crypto2 from "crypto";
2122
1690
  import { z as z5 } from "zod";
2123
1691
  var launchAgentConfigSchema = z5.object({
2124
1692
  agentType: z5.enum(["claude", "copilot", "codex", "opencode", "gemini"]),
@@ -2166,7 +1734,7 @@ function buildGraphFromFlat(trigger, actions) {
2166
1734
  const nodes = [];
2167
1735
  const edges = [];
2168
1736
  const triggerNode = {
2169
- id: crypto4.randomUUID(),
1737
+ id: crypto2.randomUUID(),
2170
1738
  type: "trigger",
2171
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",
2172
1740
  config: trigger,
@@ -2177,7 +1745,7 @@ function buildGraphFromFlat(trigger, actions) {
2177
1745
  const NODE_GAP = 140;
2178
1746
  for (let i = 0; i < actions.length; i++) {
2179
1747
  const action = actions[i];
2180
- const nodeId = crypto4.randomUUID();
1748
+ const nodeId = crypto2.randomUUID();
2181
1749
  nodes.push({
2182
1750
  id: nodeId,
2183
1751
  type: "launchAgent",
@@ -2186,7 +1754,7 @@ function buildGraphFromFlat(trigger, actions) {
2186
1754
  position: { x: 0, y: (i + 1) * NODE_GAP }
2187
1755
  });
2188
1756
  edges.push({
2189
- id: crypto4.randomUUID(),
1757
+ id: crypto2.randomUUID(),
2190
1758
  source: prevId,
2191
1759
  target: nodeId
2192
1760
  });
@@ -2194,12 +1762,21 @@ function buildGraphFromFlat(trigger, actions) {
2194
1762
  }
2195
1763
  return { nodes, edges };
2196
1764
  }
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
- });
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
+ );
2203
1780
  server.tool(
2204
1781
  "create_workflow",
2205
1782
  "Create a new workflow. Accepts either full nodes/edges or a convenience flat format (trigger + actions array).",
@@ -2228,7 +1805,7 @@ function registerWorkflowTools(server, deps) {
2228
1805
  edges = graph.edges;
2229
1806
  }
2230
1807
  const workflow = {
2231
- id: crypto4.randomUUID(),
1808
+ id: crypto2.randomUUID(),
2232
1809
  name: args.name,
2233
1810
  icon: args.icon ?? "zap",
2234
1811
  iconColor: args.icon_color ?? "#6366f1",
@@ -2238,8 +1815,6 @@ function registerWorkflowTools(server, deps) {
2238
1815
  ...args.stagger_delay_ms && { staggerDelayMs: args.stagger_delay_ms }
2239
1816
  };
2240
1817
  dbInsertWorkflow(workflow);
2241
- scheduler2.syncSchedules(dbListWorkflows());
2242
- configManager2.notifyChanged();
2243
1818
  return { content: [{ type: "text", text: JSON.stringify(workflow, null, 2) }] };
2244
1819
  }
2245
1820
  );
@@ -2274,8 +1849,6 @@ function registerWorkflowTools(server, deps) {
2274
1849
  if (args.enabled !== void 0) updates.enabled = args.enabled;
2275
1850
  if (args.stagger_delay_ms !== void 0) updates.staggerDelayMs = args.stagger_delay_ms;
2276
1851
  dbUpdateWorkflow(args.id, updates);
2277
- scheduler2.syncSchedules(dbListWorkflows());
2278
- configManager2.notifyChanged();
2279
1852
  return {
2280
1853
  content: [{ type: "text", text: JSON.stringify({ ...workflow, ...updates }, null, 2) }]
2281
1854
  };
@@ -2295,11 +1868,308 @@ function registerWorkflowTools(server, deps) {
2295
1868
  };
2296
1869
  }
2297
1870
  dbDeleteWorkflow(args.id);
2298
- scheduler2.syncSchedules(dbListWorkflows());
2299
- configManager2.notifyChanged();
2300
1871
  return { content: [{ type: "text", text: `Deleted workflow: ${workflow.name}` }] };
2301
1872
  }
2302
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
+ }
2303
2173
  }
2304
2174
 
2305
2175
  // src/tools/git.ts
@@ -2323,6 +2193,22 @@ function registerGitTools(server) {
2323
2193
  }
2324
2194
  }
2325
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
+ );
2326
2212
  server.tool(
2327
2213
  "get_diff",
2328
2214
  "Get git diff for a project (staged and unstaged changes)",
@@ -2341,30 +2227,218 @@ function registerGitTools(server) {
2341
2227
  }
2342
2228
  }
2343
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
+ );
2344
2335
  }
2345
2336
 
2346
2337
  // src/tools/config.ts
2347
- function registerConfigTools(server, deps) {
2348
- const { configManager: configManager2 } = deps;
2338
+ function registerConfigTools(server) {
2349
2339
  server.tool(
2350
2340
  "get_config",
2351
2341
  "Get the full VibeGrid configuration (projects, tasks, workflows, settings)",
2352
2342
  async () => {
2353
- const config = configManager2.loadConfig();
2343
+ const config = configManager.loadConfig();
2354
2344
  return { content: [{ type: "text", text: JSON.stringify(config, null, 2) }] };
2355
2345
  }
2356
2346
  );
2357
2347
  }
2358
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
+
2359
2432
  // src/server.ts
2360
- function createMcpServer(deps, version) {
2433
+ function createMcpServer(version) {
2361
2434
  const server = new McpServer({ name: "vibegrid", version }, { capabilities: { tools: {} } });
2362
2435
  registerGitTools(server);
2363
- registerConfigTools(server, deps);
2364
- registerProjectTools(server, deps);
2365
- registerTaskTools(server, deps);
2366
- registerSessionTools(server, deps);
2367
- registerWorkflowTools(server, deps);
2436
+ registerConfigTools(server);
2437
+ registerProjectTools(server);
2438
+ registerTaskTools(server);
2439
+ registerSessionTools(server);
2440
+ registerWorkflowTools(server);
2441
+ registerWorkspaceTools(server);
2368
2442
  return server;
2369
2443
  }
2370
2444
 
@@ -2377,18 +2451,11 @@ console.warn = (...args) => _origError("[mcp:warn]", ...args);
2377
2451
  console.error = (...args) => _origError("[mcp:error]", ...args);
2378
2452
  async function main() {
2379
2453
  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.2");
2454
+ const version = true ? "0.2.0" : createRequire(import.meta.url)("../package.json").version;
2455
+ const server = createMcpServer(version);
2387
2456
  const transport = new StdioServerTransport();
2388
2457
  await server.connect(transport);
2389
2458
  transport.onclose = () => {
2390
- scheduler.stopAll();
2391
- ptyManager.killAll();
2392
2459
  configManager.close();
2393
2460
  process.exit(0);
2394
2461
  };