@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.
- package/dist/index.js +999 -982
- 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
|
-
//
|
|
945
|
-
import
|
|
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
|
-
//
|
|
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
|
-
|
|
956
|
-
|
|
957
|
-
|
|
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
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
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
|
-
|
|
1045
|
-
|
|
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
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
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
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
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
|
|
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
|
-
"
|
|
2090
|
-
"
|
|
1448
|
+
"list_sessions",
|
|
1449
|
+
"List all active terminal sessions. Requires the VibeGrid app to be running.",
|
|
2091
1450
|
{
|
|
2092
|
-
|
|
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
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
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 =
|
|
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 {
|
|
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
|
-
|
|
1626
|
+
await rpcCall("terminal:kill", args.id);
|
|
2142
1627
|
return { content: [{ type: "text", text: `Killed session: ${args.id}` }] };
|
|
2143
1628
|
} catch (err) {
|
|
2144
|
-
return {
|
|
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
|
-
|
|
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: [
|
|
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
|
|
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:
|
|
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 =
|
|
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:
|
|
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
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
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:
|
|
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
|
|
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 =
|
|
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(
|
|
2433
|
+
function createMcpServer(version) {
|
|
2410
2434
|
const server = new McpServer({ name: "vibegrid", version }, { capabilities: { tools: {} } });
|
|
2411
2435
|
registerGitTools(server);
|
|
2412
|
-
registerConfigTools(server
|
|
2413
|
-
registerProjectTools(server
|
|
2414
|
-
registerTaskTools(server
|
|
2415
|
-
registerSessionTools(server
|
|
2416
|
-
registerWorkflowTools(server
|
|
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
|
|
2430
|
-
|
|
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
|
};
|