agenthub-multiagent-mcp 1.4.1 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client.d.ts +4 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +5 -1
- package/dist/client.js.map +1 -1
- package/dist/index.js +22 -5
- package/dist/index.js.map +1 -1
- package/dist/state.d.ts +3 -0
- package/dist/state.d.ts.map +1 -1
- package/dist/state.js.map +1 -1
- package/dist/tools/index.d.ts +1 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +84 -29
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/tools.test.js +19 -25
- package/dist/tools/tools.test.js.map +1 -1
- package/package.json +1 -1
- package/src/client.ts +15 -1
- package/src/index.ts +22 -5
- package/src/state.ts +3 -0
- package/src/tools/index.ts +98 -29
- package/src/tools/tools.test.ts +34 -36
package/src/tools/index.ts
CHANGED
|
@@ -17,6 +17,7 @@ export interface ToolContext {
|
|
|
17
17
|
setCurrentAgentId: (id: string) => void;
|
|
18
18
|
stopHeartbeat: () => void;
|
|
19
19
|
getWorkingDir: () => string;
|
|
20
|
+
setWorkingDir: (dir: string) => void;
|
|
20
21
|
getPushedItems: () => PushedItems;
|
|
21
22
|
}
|
|
22
23
|
|
|
@@ -178,6 +179,14 @@ export function registerTools(): Tool[] {
|
|
|
178
179
|
inputSchema: {
|
|
179
180
|
type: "object",
|
|
180
181
|
properties: {
|
|
182
|
+
working_dir: {
|
|
183
|
+
type: "string",
|
|
184
|
+
description: "Project directory path where .agenthub state will be saved (required)",
|
|
185
|
+
},
|
|
186
|
+
project: {
|
|
187
|
+
type: "string",
|
|
188
|
+
description: "Project key to bind this agent to (e.g., 'KRISHI'). Required.",
|
|
189
|
+
},
|
|
181
190
|
id: {
|
|
182
191
|
type: "string",
|
|
183
192
|
description: "Unique agent identifier (e.g., 'backend-1', 'tester')",
|
|
@@ -191,7 +200,7 @@ export function registerTools(): Tool[] {
|
|
|
191
200
|
description: "Model name (e.g., 'claude-opus-4.5', 'gpt-4-turbo', 'o3', 'gemini-2.0-pro'). Provider is auto-detected.",
|
|
192
201
|
},
|
|
193
202
|
},
|
|
194
|
-
required: ["
|
|
203
|
+
required: ["working_dir", "project", "model"],
|
|
195
204
|
},
|
|
196
205
|
},
|
|
197
206
|
{
|
|
@@ -594,14 +603,10 @@ export function registerTools(): Tool[] {
|
|
|
594
603
|
// Ticket creation tools
|
|
595
604
|
{
|
|
596
605
|
name: "create_epic",
|
|
597
|
-
description: "Create an epic (large body of work) in
|
|
606
|
+
description: "Create an epic (large body of work) in the bound project. Automatically uploads the OpenSpec file as an attachment. (requires agent_register first)",
|
|
598
607
|
inputSchema: {
|
|
599
608
|
type: "object",
|
|
600
609
|
properties: {
|
|
601
|
-
project_id: {
|
|
602
|
-
type: "string",
|
|
603
|
-
description: "Project ID to create epic in",
|
|
604
|
-
},
|
|
605
610
|
title: {
|
|
606
611
|
type: "string",
|
|
607
612
|
description: "Epic title",
|
|
@@ -612,25 +617,21 @@ export function registerTools(): Tool[] {
|
|
|
612
617
|
},
|
|
613
618
|
source_file: {
|
|
614
619
|
type: "string",
|
|
615
|
-
description: "
|
|
620
|
+
description: "Path to OpenSpec file (e.g., 'openspec/changes/feature-name/proposal.md'). Will be uploaded as attachment.",
|
|
616
621
|
},
|
|
617
622
|
},
|
|
618
|
-
required: ["
|
|
623
|
+
required: ["title", "source_file"],
|
|
619
624
|
},
|
|
620
625
|
},
|
|
621
626
|
{
|
|
622
627
|
name: "create_story",
|
|
623
|
-
description: "Create a story
|
|
628
|
+
description: "Create a story in the bound project (requires agent_register first)",
|
|
624
629
|
inputSchema: {
|
|
625
630
|
type: "object",
|
|
626
631
|
properties: {
|
|
627
|
-
project_id: {
|
|
628
|
-
type: "string",
|
|
629
|
-
description: "Project ID",
|
|
630
|
-
},
|
|
631
632
|
epic_id: {
|
|
632
633
|
type: "string",
|
|
633
|
-
description: "Epic ID to add story to",
|
|
634
|
+
description: "Epic ID to add story to (optional)",
|
|
634
635
|
},
|
|
635
636
|
title: {
|
|
636
637
|
type: "string",
|
|
@@ -651,22 +652,18 @@ export function registerTools(): Tool[] {
|
|
|
651
652
|
description: "Additional labels",
|
|
652
653
|
},
|
|
653
654
|
},
|
|
654
|
-
required: ["
|
|
655
|
+
required: ["title"],
|
|
655
656
|
},
|
|
656
657
|
},
|
|
657
658
|
{
|
|
658
659
|
name: "create_task",
|
|
659
|
-
description: "Create a task
|
|
660
|
+
description: "Create a task in the bound project (requires agent_register first)",
|
|
660
661
|
inputSchema: {
|
|
661
662
|
type: "object",
|
|
662
663
|
properties: {
|
|
663
|
-
project_id: {
|
|
664
|
-
type: "string",
|
|
665
|
-
description: "Project ID",
|
|
666
|
-
},
|
|
667
664
|
story_id: {
|
|
668
665
|
type: "string",
|
|
669
|
-
description: "Story ID to add task to",
|
|
666
|
+
description: "Story ID to add task to (optional)",
|
|
670
667
|
},
|
|
671
668
|
title: {
|
|
672
669
|
type: "string",
|
|
@@ -682,7 +679,7 @@ export function registerTools(): Tool[] {
|
|
|
682
679
|
description: "Required skill type",
|
|
683
680
|
},
|
|
684
681
|
},
|
|
685
|
-
required: ["
|
|
682
|
+
required: ["title"],
|
|
686
683
|
},
|
|
687
684
|
},
|
|
688
685
|
{
|
|
@@ -748,7 +745,17 @@ export async function handleToolCall(
|
|
|
748
745
|
switch (name) {
|
|
749
746
|
// Registration
|
|
750
747
|
case "agent_register": {
|
|
751
|
-
|
|
748
|
+
// Get required params
|
|
749
|
+
const workingDir = args.working_dir as string;
|
|
750
|
+
if (!workingDir) {
|
|
751
|
+
throw new Error("working_dir is required");
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
const projectKey = args.project as string;
|
|
755
|
+
if (!projectKey) {
|
|
756
|
+
throw new Error("project is required");
|
|
757
|
+
}
|
|
758
|
+
|
|
752
759
|
const owner = state.getCurrentOwner();
|
|
753
760
|
const requestedId = args.id as string;
|
|
754
761
|
|
|
@@ -779,6 +786,9 @@ export async function handleToolCall(
|
|
|
779
786
|
// Set connect token on client for org operations
|
|
780
787
|
client.setConnectToken(existingState.token);
|
|
781
788
|
|
|
789
|
+
// Store working_dir in context for other tools to use
|
|
790
|
+
context.setWorkingDir(workingDir);
|
|
791
|
+
|
|
782
792
|
// Update state with returned name from server
|
|
783
793
|
state.saveState(workingDir, {
|
|
784
794
|
...existingState,
|
|
@@ -805,7 +815,8 @@ export async function handleToolCall(
|
|
|
805
815
|
// Step 1: Initialize registration session
|
|
806
816
|
const initResult = await client.initRegistration(
|
|
807
817
|
requestedId,
|
|
808
|
-
args.name as string | undefined
|
|
818
|
+
args.name as string | undefined,
|
|
819
|
+
projectKey
|
|
809
820
|
);
|
|
810
821
|
|
|
811
822
|
// Step 2: Open browser to dashboard
|
|
@@ -851,6 +862,9 @@ export async function handleToolCall(
|
|
|
851
862
|
// Set connect token on client for org operations
|
|
852
863
|
client.setConnectToken(registrationResult.connect_token!);
|
|
853
864
|
|
|
865
|
+
// Store working_dir in context for other tools to use
|
|
866
|
+
context.setWorkingDir(workingDir);
|
|
867
|
+
|
|
854
868
|
// Save state for future reconnection
|
|
855
869
|
state.saveState(workingDir, {
|
|
856
870
|
agent_id: agentId,
|
|
@@ -858,6 +872,9 @@ export async function handleToolCall(
|
|
|
858
872
|
owner,
|
|
859
873
|
token: registrationResult.connect_token!,
|
|
860
874
|
agent_type: registrationResult.agent_type,
|
|
875
|
+
project_id: registrationResult.project_id!,
|
|
876
|
+
project_key: registrationResult.project_key!,
|
|
877
|
+
org_id: registrationResult.org_id!,
|
|
861
878
|
registered_at: new Date().toISOString(),
|
|
862
879
|
});
|
|
863
880
|
|
|
@@ -1055,21 +1072,65 @@ export async function handleToolCall(
|
|
|
1055
1072
|
|
|
1056
1073
|
// Ticket creation tools
|
|
1057
1074
|
case "create_epic": {
|
|
1075
|
+
if (!agentId) throw new Error("Not registered. Call agent_register first.");
|
|
1076
|
+
|
|
1077
|
+
// Get project from state
|
|
1078
|
+
const workingDir = context.getWorkingDir();
|
|
1079
|
+
const agentState = state.loadState(workingDir);
|
|
1080
|
+
|
|
1081
|
+
if (!agentState?.project_id) {
|
|
1082
|
+
throw new Error("Not registered with a project. Call agent_register first.");
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
const sourceFile = args.source_file as string;
|
|
1086
|
+
if (!sourceFile) {
|
|
1087
|
+
throw new Error("source_file is required. Provide the path to the OpenSpec file.");
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
// Create the epic
|
|
1058
1091
|
const result = await client.createEpic({
|
|
1059
|
-
project_id:
|
|
1092
|
+
project_id: agentState.project_id,
|
|
1060
1093
|
title: args.title as string,
|
|
1061
1094
|
description: args.description as string | undefined,
|
|
1062
|
-
source_file:
|
|
1095
|
+
source_file: sourceFile,
|
|
1063
1096
|
});
|
|
1097
|
+
|
|
1098
|
+
// Auto-upload the OpenSpec file as attachment
|
|
1099
|
+
let attachmentResult = null;
|
|
1100
|
+
try {
|
|
1101
|
+
attachmentResult = await client.uploadAttachment({
|
|
1102
|
+
ticket_id: result.id,
|
|
1103
|
+
ticket_type: "epic",
|
|
1104
|
+
ticket_key: result.key,
|
|
1105
|
+
file_path: sourceFile,
|
|
1106
|
+
description: "OpenSpec source document",
|
|
1107
|
+
agent_id: agentId,
|
|
1108
|
+
});
|
|
1109
|
+
} catch (uploadError) {
|
|
1110
|
+
// Log but don't fail epic creation if upload fails
|
|
1111
|
+
console.error("Failed to upload OpenSpec attachment:", uploadError);
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1064
1114
|
return {
|
|
1065
1115
|
...result,
|
|
1066
|
-
|
|
1116
|
+
attachment: attachmentResult,
|
|
1117
|
+
message: attachmentResult
|
|
1118
|
+
? `Created epic ${result.key}: ${args.title} (OpenSpec uploaded)`
|
|
1119
|
+
: `Created epic ${result.key}: ${args.title} (OpenSpec upload failed)`,
|
|
1067
1120
|
};
|
|
1068
1121
|
}
|
|
1069
1122
|
|
|
1070
1123
|
case "create_story": {
|
|
1124
|
+
// Get project from state
|
|
1125
|
+
const workingDir = context.getWorkingDir();
|
|
1126
|
+
const agentState = state.loadState(workingDir);
|
|
1127
|
+
|
|
1128
|
+
if (!agentState?.project_id) {
|
|
1129
|
+
throw new Error("Not registered with a project. Call agent_register first.");
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1071
1132
|
const result = await client.createStory({
|
|
1072
|
-
project_id:
|
|
1133
|
+
project_id: agentState.project_id, // From state, not args
|
|
1073
1134
|
epic_id: args.epic_id as string | undefined,
|
|
1074
1135
|
title: args.title as string,
|
|
1075
1136
|
description: args.description as string | undefined,
|
|
@@ -1083,8 +1144,16 @@ export async function handleToolCall(
|
|
|
1083
1144
|
}
|
|
1084
1145
|
|
|
1085
1146
|
case "create_task": {
|
|
1147
|
+
// Get project from state
|
|
1148
|
+
const workingDir = context.getWorkingDir();
|
|
1149
|
+
const agentState = state.loadState(workingDir);
|
|
1150
|
+
|
|
1151
|
+
if (!agentState?.project_id) {
|
|
1152
|
+
throw new Error("Not registered with a project. Call agent_register first.");
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1086
1155
|
const result = await client.createTask({
|
|
1087
|
-
project_id:
|
|
1156
|
+
project_id: agentState.project_id, // From state, not args
|
|
1088
1157
|
story_id: args.story_id as string | undefined,
|
|
1089
1158
|
title: args.title as string,
|
|
1090
1159
|
description: args.description as string | undefined,
|
package/src/tools/tools.test.ts
CHANGED
|
@@ -40,21 +40,25 @@ const mockClient = {
|
|
|
40
40
|
// Mock context
|
|
41
41
|
function createMockContext(): ToolContext {
|
|
42
42
|
let currentAgentId = "";
|
|
43
|
+
let currentWorkingDir = "/tmp/test-workdir";
|
|
43
44
|
return {
|
|
44
45
|
getCurrentAgentId: () => currentAgentId,
|
|
45
46
|
setCurrentAgentId: (id: string) => {
|
|
46
47
|
currentAgentId = id;
|
|
47
48
|
},
|
|
48
49
|
stopHeartbeat: vi.fn(),
|
|
49
|
-
getWorkingDir: () =>
|
|
50
|
+
getWorkingDir: () => currentWorkingDir,
|
|
51
|
+
setWorkingDir: (dir: string) => {
|
|
52
|
+
currentWorkingDir = dir;
|
|
53
|
+
},
|
|
50
54
|
getPushedItems: () => ({ tasks: [], messages: [] }),
|
|
51
55
|
};
|
|
52
56
|
}
|
|
53
57
|
|
|
54
58
|
describe("registerTools", () => {
|
|
55
|
-
it("should return all
|
|
59
|
+
it("should return all 28 tools", () => {
|
|
56
60
|
const tools = registerTools();
|
|
57
|
-
expect(tools).toHaveLength(
|
|
61
|
+
expect(tools).toHaveLength(28);
|
|
58
62
|
});
|
|
59
63
|
|
|
60
64
|
it("should have required tool names", () => {
|
|
@@ -100,40 +104,33 @@ describe("handleToolCall", () => {
|
|
|
100
104
|
});
|
|
101
105
|
|
|
102
106
|
describe("agent_register", () => {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
model_provider: "anthropic",
|
|
110
|
-
registered_at: "2024-01-01T00:00:00Z",
|
|
111
|
-
slack_notified: false,
|
|
112
|
-
pending_tasks_count: 0,
|
|
113
|
-
unread_messages_count: 0,
|
|
114
|
-
};
|
|
115
|
-
vi.mocked(mockClient.registerAgent).mockResolvedValue(mockResult);
|
|
107
|
+
// Note: The agent_register flow now uses browser-based registration
|
|
108
|
+
// which requires mocking initRegistration, pollRegistrationCallback, and open()
|
|
109
|
+
// These are integration tests that are skipped in unit tests
|
|
110
|
+
it.skip("should register agent and set current agent ID", async () => {
|
|
111
|
+
// Browser-based flow - tested via e2e tests
|
|
112
|
+
});
|
|
116
113
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
114
|
+
it("should require working_dir parameter", async () => {
|
|
115
|
+
await expect(
|
|
116
|
+
handleToolCall(
|
|
117
|
+
"agent_register",
|
|
118
|
+
{ project: "TEST", model: "claude-opus-4.5" },
|
|
119
|
+
mockClient,
|
|
120
|
+
context
|
|
121
|
+
)
|
|
122
|
+
).rejects.toThrow("working_dir is required");
|
|
123
|
+
});
|
|
123
124
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
expect(result).toMatchObject({
|
|
134
|
-
...mockResult,
|
|
135
|
-
mode: "registered",
|
|
136
|
-
});
|
|
125
|
+
it("should require project parameter", async () => {
|
|
126
|
+
await expect(
|
|
127
|
+
handleToolCall(
|
|
128
|
+
"agent_register",
|
|
129
|
+
{ working_dir: "/tmp/test", model: "claude-opus-4.5" },
|
|
130
|
+
mockClient,
|
|
131
|
+
context
|
|
132
|
+
)
|
|
133
|
+
).rejects.toThrow("project is required");
|
|
137
134
|
});
|
|
138
135
|
});
|
|
139
136
|
|
|
@@ -164,7 +161,8 @@ describe("handleToolCall", () => {
|
|
|
164
161
|
expect(mockClient.startWork).toHaveBeenCalledWith(
|
|
165
162
|
"test-agent",
|
|
166
163
|
"Building feature X",
|
|
167
|
-
"TestProject"
|
|
164
|
+
"TestProject",
|
|
165
|
+
undefined
|
|
168
166
|
);
|
|
169
167
|
// Result is wrapped with messages
|
|
170
168
|
expect(result).toMatchObject({ result: mockResult });
|