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