@hanhnd/agent-kit 1.0.17 → 1.0.19
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/README.md +9 -6
- package/dist/kit-server.js +112 -231
- package/dist/tools/agent.d.ts +0 -3
- package/dist/tools/agent.js +83 -101
- package/dist/tools/core.js +3 -7
- package/dist/tools/integration.js +30 -135
- package/dist/utils/utils.d.ts +9 -0
- package/dist/utils/utils.js +6 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
| `/ak:brainstorm [idea]` | Strategic architectural analysis |
|
|
12
12
|
| `/ak:plan [file or idea]` | Create an implementation blueprint |
|
|
13
13
|
| `/ak:code [file or task]` | Implement from a plan |
|
|
14
|
-
| `/ak:code-simplify` |
|
|
14
|
+
| `/ak:code-simplify` | Simplify modified code for readability |
|
|
15
|
+
| `/ak:code-refactor` | Refactor modified code for readability |
|
|
15
16
|
| `/ak:research [topic]` | Research a topic |
|
|
16
17
|
| `/ak:review-pr [PR URL]` | Review a pull request |
|
|
17
18
|
| `/ak:review` | Review uncommitted local changes |
|
|
@@ -68,7 +69,7 @@ You don't need to run the wiki manually. Five hooks keep it fed automatically:
|
|
|
68
69
|
|
|
69
70
|
```bash
|
|
70
71
|
claude plugin marketplace add https://github.com/hanh-nd/agent-kit
|
|
71
|
-
claude plugin install
|
|
72
|
+
claude plugin install agent-kit
|
|
72
73
|
```
|
|
73
74
|
|
|
74
75
|
The plugin fetches from GitHub, registers the MCP server automatically, and makes all commands available immediately.
|
|
@@ -91,8 +92,8 @@ BITBUCKET_DEFAULT_WORKSPACE = your-default-workspace-slug
|
|
|
91
92
|
|
|
92
93
|
To use multiple profiles, add `[work]`, `[personal]`, etc. sections and set `KIT_PROFILE=work` in your environment.
|
|
93
94
|
|
|
94
|
-
To update: `claude plugin update
|
|
95
|
-
To uninstall: `claude plugin uninstall
|
|
95
|
+
To update: `claude plugin update agent-kit`
|
|
96
|
+
To uninstall: `claude plugin uninstall agent-kit`
|
|
96
97
|
|
|
97
98
|
---
|
|
98
99
|
|
|
@@ -113,7 +114,7 @@ npm run build
|
|
|
113
114
|
|
|
114
115
|
```bash
|
|
115
116
|
claude plugin marketplace add /absolute/path/to/agent-kit
|
|
116
|
-
claude plugin install
|
|
117
|
+
claude plugin install agent-kit
|
|
117
118
|
```
|
|
118
119
|
|
|
119
120
|
This registers the MCP server automatically (pointing to your local build). Do **not** manually add a `kit-agents` entry to `settings.json` — the plugin handles that.
|
|
@@ -147,7 +148,9 @@ BITBUCKET_DEFAULT_WORKSPACE = your-default-workspace-slug
|
|
|
147
148
|
If you want to reuse the commands with [Gemini CLI](https://geminicli.com) (for `/ak:delegate` to Gemini), install it using:
|
|
148
149
|
|
|
149
150
|
```bash
|
|
150
|
-
|
|
151
|
+
git clone https://github.com/hanh-nd/agent-kit.git
|
|
152
|
+
cd agent-kit/plugins/agent-kit
|
|
153
|
+
gemini extension link .gemini
|
|
151
154
|
```
|
|
152
155
|
|
|
153
156
|
---
|
package/dist/kit-server.js
CHANGED
|
@@ -20997,6 +20997,12 @@ import * as path from "path";
|
|
|
20997
20997
|
function getWorkspaceRoot() {
|
|
20998
20998
|
return process.env.WORKSPACE_DIR || process.env.CLAUDE_WORKSPACE || process.env.GEMINI_WORKSPACE || process.env.INIT_CWD || process.env.PWD || process.cwd();
|
|
20999
20999
|
}
|
|
21000
|
+
function mcpText(text) {
|
|
21001
|
+
return { content: [{ type: "text", text }] };
|
|
21002
|
+
}
|
|
21003
|
+
function mcpJson(obj) {
|
|
21004
|
+
return mcpText(JSON.stringify(obj, null, 2));
|
|
21005
|
+
}
|
|
21000
21006
|
|
|
21001
21007
|
// src/tools/config.ts
|
|
21002
21008
|
var FORBIDDEN_FILES = [
|
|
@@ -21060,13 +21066,9 @@ function registerCoreTools(server2) {
|
|
|
21060
21066
|
}
|
|
21061
21067
|
const filePath = path.join(handoffDir, filename);
|
|
21062
21068
|
fs.writeFileSync(filePath, content, "utf8");
|
|
21063
|
-
return {
|
|
21064
|
-
content: [{ type: "text", text: `\u2705 Saved to: ${filePath}` }]
|
|
21065
|
-
};
|
|
21069
|
+
return mcpText(`\u2705 Saved to: ${filePath}`);
|
|
21066
21070
|
} catch (error2) {
|
|
21067
|
-
return {
|
|
21068
|
-
content: [{ type: "text", text: `Error saving handoff: ${error2}` }]
|
|
21069
|
-
};
|
|
21071
|
+
return mcpText(`Error saving handoff: ${error2}`);
|
|
21070
21072
|
}
|
|
21071
21073
|
}
|
|
21072
21074
|
);
|
|
@@ -21171,9 +21173,12 @@ function registerAgentTools(server2) {
|
|
|
21171
21173
|
'Task message or path to a handoff file (e.g., ".agent-kit/handoffs/plans/plan-xyz.md")'
|
|
21172
21174
|
)
|
|
21173
21175
|
},
|
|
21174
|
-
async ({ agent, task }) => {
|
|
21176
|
+
async ({ agent, task }, extra) => {
|
|
21177
|
+
const workspaceRoot = getWorkspaceRoot();
|
|
21178
|
+
let job;
|
|
21179
|
+
let logPath;
|
|
21180
|
+
let killChild;
|
|
21175
21181
|
try {
|
|
21176
|
-
const workspaceRoot = getWorkspaceRoot();
|
|
21177
21182
|
let prompt = task;
|
|
21178
21183
|
const resolvedTask = path3.resolve(workspaceRoot, task);
|
|
21179
21184
|
if (fs2.existsSync(resolvedTask) && fs2.statSync(resolvedTask).isFile()) {
|
|
@@ -21181,9 +21186,7 @@ function registerAgentTools(server2) {
|
|
|
21181
21186
|
validatePath(task, workspaceRoot);
|
|
21182
21187
|
prompt = fs2.readFileSync(resolvedTask, "utf-8");
|
|
21183
21188
|
} catch (err) {
|
|
21184
|
-
return {
|
|
21185
|
-
content: [{ type: "text", text: `Error reading task file: ${err}` }]
|
|
21186
|
-
};
|
|
21189
|
+
return mcpText(`Error reading task file: ${err}`);
|
|
21187
21190
|
}
|
|
21188
21191
|
}
|
|
21189
21192
|
let usedAgent = agent;
|
|
@@ -21195,107 +21198,89 @@ function registerAgentTools(server2) {
|
|
|
21195
21198
|
fallbackReason = `${agent} CLI not installed, fell back to ${fallback}`;
|
|
21196
21199
|
} else {
|
|
21197
21200
|
const installHint = agent === "gemini" ? "Install Gemini CLI: https://github.com/google-gemini/gemini-cli" : "Install Claude CLI: npm install -g @anthropic-ai/claude-code";
|
|
21198
|
-
return {
|
|
21199
|
-
|
|
21200
|
-
|
|
21201
|
-
|
|
21202
|
-
|
|
21203
|
-
|
|
21204
|
-
agent,
|
|
21205
|
-
status: "error",
|
|
21206
|
-
output: "",
|
|
21207
|
-
error: `Neither ${agent} nor the fallback agent CLI is installed. ${installHint}`
|
|
21208
|
-
},
|
|
21209
|
-
null,
|
|
21210
|
-
2
|
|
21211
|
-
)
|
|
21212
|
-
}
|
|
21213
|
-
]
|
|
21214
|
-
};
|
|
21201
|
+
return mcpJson({
|
|
21202
|
+
agent,
|
|
21203
|
+
status: "error",
|
|
21204
|
+
output: "",
|
|
21205
|
+
error: `Neither ${agent} nor the fallback agent CLI is installed. ${installHint}`
|
|
21206
|
+
});
|
|
21215
21207
|
}
|
|
21216
21208
|
}
|
|
21217
21209
|
const agentArgs = usedAgent === "gemini" ? ["-y", "-p", prompt] : ["--dangerously-skip-permissions", "-p", prompt];
|
|
21218
|
-
const
|
|
21219
|
-
|
|
21210
|
+
const { logPath: lp, logStream } = initAgentLog(workspaceRoot, taskSlug(task), usedAgent);
|
|
21211
|
+
logPath = lp;
|
|
21220
21212
|
const jobId = randomUUID();
|
|
21221
21213
|
const child = spawn(usedAgent, agentArgs, {
|
|
21222
21214
|
cwd: workspaceRoot,
|
|
21223
|
-
env: { ...process.env, GEMINI_WORKSPACE: workspaceRoot }
|
|
21215
|
+
env: { ...process.env, GEMINI_WORKSPACE: workspaceRoot },
|
|
21216
|
+
detached: process.platform !== "win32"
|
|
21224
21217
|
});
|
|
21225
|
-
const
|
|
21226
|
-
|
|
21218
|
+
const killTree = (sig) => {
|
|
21219
|
+
if (process.platform !== "win32" && child.pid) {
|
|
21220
|
+
try {
|
|
21221
|
+
process.kill(-child.pid, sig);
|
|
21222
|
+
} catch {
|
|
21223
|
+
child.kill(sig);
|
|
21224
|
+
}
|
|
21225
|
+
} else {
|
|
21226
|
+
child.kill(sig);
|
|
21227
|
+
}
|
|
21228
|
+
};
|
|
21229
|
+
job = {
|
|
21227
21230
|
agent: usedAgent,
|
|
21228
21231
|
process: child,
|
|
21229
21232
|
startedAt: /* @__PURE__ */ new Date(),
|
|
21230
21233
|
chunks: [],
|
|
21231
21234
|
logStream,
|
|
21232
|
-
timeoutHandle: setTimeout(() =>
|
|
21233
|
-
child.kill("SIGTERM");
|
|
21234
|
-
}, AGENT_TIMEOUT)
|
|
21235
|
+
timeoutHandle: setTimeout(() => killTree("SIGTERM"), AGENT_TIMEOUT)
|
|
21235
21236
|
};
|
|
21236
21237
|
jobRegistry.set(jobId, job);
|
|
21237
|
-
|
|
21238
|
-
|
|
21239
|
-
|
|
21240
|
-
|
|
21241
|
-
|
|
21242
|
-
|
|
21243
|
-
|
|
21244
|
-
|
|
21245
|
-
|
|
21246
|
-
|
|
21247
|
-
child.on("close", (code) => {
|
|
21248
|
-
if (code === 0) resolve3();
|
|
21249
|
-
else reject(new Error(`Process exited with code ${code}`));
|
|
21250
|
-
});
|
|
21251
|
-
child.on("error", reject);
|
|
21238
|
+
killChild = () => {
|
|
21239
|
+
killTree("SIGTERM");
|
|
21240
|
+
setTimeout(() => killTree("SIGKILL"), 5e3).unref();
|
|
21241
|
+
};
|
|
21242
|
+
if (extra.signal.aborted) killChild();
|
|
21243
|
+
else extra.signal.addEventListener("abort", killChild);
|
|
21244
|
+
await new Promise((resolve3, reject) => {
|
|
21245
|
+
child.stdout?.on("data", (data) => {
|
|
21246
|
+
job.chunks.push(data.toString());
|
|
21247
|
+
job.logStream.write(data);
|
|
21252
21248
|
});
|
|
21253
|
-
|
|
21254
|
-
|
|
21255
|
-
|
|
21256
|
-
|
|
21257
|
-
|
|
21258
|
-
|
|
21259
|
-
|
|
21260
|
-
|
|
21261
|
-
|
|
21262
|
-
|
|
21263
|
-
|
|
21264
|
-
|
|
21265
|
-
|
|
21266
|
-
|
|
21267
|
-
|
|
21268
|
-
|
|
21269
|
-
|
|
21270
|
-
|
|
21271
|
-
|
|
21272
|
-
|
|
21273
|
-
} catch (error2) {
|
|
21274
|
-
throw error2;
|
|
21275
|
-
} finally {
|
|
21276
|
-
job.logStream.end();
|
|
21277
|
-
clearTimeout(job.timeoutHandle);
|
|
21278
|
-
jobRegistry.delete(jobId);
|
|
21279
|
-
}
|
|
21249
|
+
child.stderr?.on("data", (data) => {
|
|
21250
|
+
job.chunks.push(data.toString());
|
|
21251
|
+
job.logStream.write(data);
|
|
21252
|
+
});
|
|
21253
|
+
child.on("close", (code, signal) => {
|
|
21254
|
+
if (code === 0) resolve3();
|
|
21255
|
+
else
|
|
21256
|
+
reject(
|
|
21257
|
+
new Error(`Process exited with code ${code}${signal ? ` (signal: ${signal})` : ""}`)
|
|
21258
|
+
);
|
|
21259
|
+
});
|
|
21260
|
+
child.on("error", reject);
|
|
21261
|
+
});
|
|
21262
|
+
return mcpJson({
|
|
21263
|
+
agent: usedAgent,
|
|
21264
|
+
status: fallbackReason ? "fallback" : "success",
|
|
21265
|
+
log: logPath,
|
|
21266
|
+
output: sanitizeOutput(job.chunks.join("")),
|
|
21267
|
+
...fallbackReason && { fallback_reason: fallbackReason }
|
|
21268
|
+
});
|
|
21280
21269
|
} catch (error2) {
|
|
21270
|
+
if (extra.signal.aborted) {
|
|
21271
|
+
return mcpJson({
|
|
21272
|
+
agent,
|
|
21273
|
+
status: "cancelled",
|
|
21274
|
+
log: logPath,
|
|
21275
|
+
output: sanitizeOutput(job?.chunks.join("") ?? "")
|
|
21276
|
+
});
|
|
21277
|
+
}
|
|
21281
21278
|
const msg = error2 instanceof Error ? error2.message : String(error2);
|
|
21282
|
-
return {
|
|
21283
|
-
|
|
21284
|
-
|
|
21285
|
-
|
|
21286
|
-
|
|
21287
|
-
{
|
|
21288
|
-
agent,
|
|
21289
|
-
status: "error",
|
|
21290
|
-
output: "",
|
|
21291
|
-
error: msg
|
|
21292
|
-
},
|
|
21293
|
-
null,
|
|
21294
|
-
2
|
|
21295
|
-
)
|
|
21296
|
-
}
|
|
21297
|
-
]
|
|
21298
|
-
};
|
|
21279
|
+
return mcpJson({ agent, status: "error", output: "", error: msg });
|
|
21280
|
+
} finally {
|
|
21281
|
+
if (killChild) extra.signal.removeEventListener("abort", killChild);
|
|
21282
|
+
job?.logStream.end();
|
|
21283
|
+
if (job) clearTimeout(job.timeoutHandle);
|
|
21299
21284
|
}
|
|
21300
21285
|
}
|
|
21301
21286
|
);
|
|
@@ -21529,63 +21514,18 @@ var JiraTicketSchema = external_exports.object({
|
|
|
21529
21514
|
errorMessages: external_exports.array(external_exports.string()).optional(),
|
|
21530
21515
|
fields: JiraFieldsSchema
|
|
21531
21516
|
});
|
|
21532
|
-
function
|
|
21533
|
-
const email2 = getCredential(
|
|
21534
|
-
const token = getCredential(
|
|
21535
|
-
if (!email2 || !token) {
|
|
21536
|
-
throw new Error("Missing ATLASSIAN_USER_EMAIL or ATLASSIAN_API_TOKEN");
|
|
21537
|
-
}
|
|
21517
|
+
function buildBasicAuth(emailVar, tokenVar) {
|
|
21518
|
+
const email2 = getCredential(emailVar);
|
|
21519
|
+
const token = getCredential(tokenVar);
|
|
21520
|
+
if (!email2 || !token) throw new Error(`Missing ${emailVar} or ${tokenVar}`);
|
|
21538
21521
|
return "Basic " + Buffer.from(`${email2}:${token}`).toString("base64");
|
|
21539
21522
|
}
|
|
21540
|
-
function
|
|
21541
|
-
const
|
|
21542
|
-
|
|
21543
|
-
if (
|
|
21544
|
-
|
|
21545
|
-
|
|
21546
|
-
return "Basic " + Buffer.from(`${email2}:${token}`).toString("base64");
|
|
21547
|
-
}
|
|
21548
|
-
async function callAtlassianRestApi(url) {
|
|
21549
|
-
const auth = buildJiraBasicAuth();
|
|
21550
|
-
const resp = await fetch(url, { headers: { Authorization: auth, Accept: "application/json" } });
|
|
21551
|
-
if (resp.status === 401) {
|
|
21552
|
-
throw new Error("\u274C Auth failed: check ATLASSIAN_USER_EMAIL and ATLASSIAN_API_TOKEN");
|
|
21553
|
-
}
|
|
21554
|
-
if (resp.status === 404) {
|
|
21555
|
-
throw new Error(`\u274C Not found: ${url}`);
|
|
21556
|
-
}
|
|
21557
|
-
if (!resp.ok) {
|
|
21558
|
-
throw new Error(`\u274C API error ${resp.status}: ${await resp.text()}`);
|
|
21559
|
-
}
|
|
21560
|
-
return await resp.json();
|
|
21561
|
-
}
|
|
21562
|
-
async function callBitbucketRestApi(url) {
|
|
21563
|
-
const auth = buildBitbucketBasicAuth();
|
|
21564
|
-
const resp = await fetch(url, { headers: { Authorization: auth, Accept: "application/json" } });
|
|
21565
|
-
if (resp.status === 401) {
|
|
21566
|
-
throw new Error("\u274C Auth failed: check BITBUCKET_USER_EMAIL and BITBUCKET_API_TOKEN");
|
|
21567
|
-
}
|
|
21568
|
-
if (resp.status === 404) {
|
|
21569
|
-
throw new Error(`\u274C Not found: ${url}`);
|
|
21570
|
-
}
|
|
21571
|
-
if (!resp.ok) {
|
|
21572
|
-
throw new Error(`\u274C API error ${resp.status}: ${await resp.text()}`);
|
|
21573
|
-
}
|
|
21574
|
-
return await resp.json();
|
|
21575
|
-
}
|
|
21576
|
-
async function callBitbucketDiffApi(url) {
|
|
21577
|
-
const auth = buildBitbucketBasicAuth();
|
|
21578
|
-
const resp = await fetch(url, { headers: { Authorization: auth, Accept: "text/plain" } });
|
|
21579
|
-
if (resp.status === 401) {
|
|
21580
|
-
throw new Error("\u274C Auth failed: check BITBUCKET_USER_EMAIL and BITBUCKET_API_TOKEN");
|
|
21581
|
-
}
|
|
21582
|
-
if (resp.status === 404) {
|
|
21583
|
-
throw new Error(`\u274C Not found: ${url}`);
|
|
21584
|
-
}
|
|
21585
|
-
if (!resp.ok) {
|
|
21586
|
-
throw new Error(`\u274C API error ${resp.status}: ${await resp.text()}`);
|
|
21587
|
-
}
|
|
21588
|
-
return await resp.text();
|
|
21523
|
+
async function callRestApi(url, auth, accept = "application/json") {
|
|
21524
|
+
const resp = await fetch(url, { headers: { Authorization: auth, Accept: accept } });
|
|
21525
|
+
if (resp.status === 401) throw new Error(`\u274C Auth failed (401): ${url}`);
|
|
21526
|
+
if (resp.status === 404) throw new Error(`\u274C Not found: ${url}`);
|
|
21527
|
+
if (!resp.ok) throw new Error(`\u274C API error ${resp.status}: ${await resp.text()}`);
|
|
21528
|
+
return accept === "text/plain" ? resp.text() : resp.json();
|
|
21589
21529
|
}
|
|
21590
21530
|
function registerIntegrationTools(server2) {
|
|
21591
21531
|
server2.tool(
|
|
@@ -21601,18 +21541,6 @@ function registerIntegrationTools(server2) {
|
|
|
21601
21541
|
},
|
|
21602
21542
|
async ({ input, workspace, repoSlug, includeDiff }) => {
|
|
21603
21543
|
try {
|
|
21604
|
-
const bbEmail = getCredential("BITBUCKET_USER_EMAIL");
|
|
21605
|
-
const bbToken = getCredential("BITBUCKET_API_TOKEN");
|
|
21606
|
-
if (!bbEmail || !bbToken) {
|
|
21607
|
-
return {
|
|
21608
|
-
content: [
|
|
21609
|
-
{
|
|
21610
|
-
type: "text",
|
|
21611
|
-
text: `\u274C Missing BITBUCKET_USER_EMAIL or BITBUCKET_API_TOKEN. Set them in ~/.claude/agent-kit or as environment variables.`
|
|
21612
|
-
}
|
|
21613
|
-
]
|
|
21614
|
-
};
|
|
21615
|
-
}
|
|
21616
21544
|
let ws;
|
|
21617
21545
|
let repo;
|
|
21618
21546
|
let prId;
|
|
@@ -21627,29 +21555,20 @@ function registerIntegrationTools(server2) {
|
|
|
21627
21555
|
repo = repoSlug;
|
|
21628
21556
|
}
|
|
21629
21557
|
if (!ws) {
|
|
21630
|
-
return
|
|
21631
|
-
|
|
21632
|
-
|
|
21633
|
-
type: "text",
|
|
21634
|
-
text: `\u274C workspace is required. Pass it as a parameter or set BITBUCKET_DEFAULT_WORKSPACE in your MCP env config.`
|
|
21635
|
-
}
|
|
21636
|
-
]
|
|
21637
|
-
};
|
|
21558
|
+
return mcpText(
|
|
21559
|
+
`\u274C workspace is required. Pass it as a parameter or set BITBUCKET_DEFAULT_WORKSPACE in your MCP env config.`
|
|
21560
|
+
);
|
|
21638
21561
|
}
|
|
21639
21562
|
if (!repo || !prId) {
|
|
21640
|
-
return
|
|
21641
|
-
|
|
21642
|
-
|
|
21643
|
-
type: "text",
|
|
21644
|
-
text: `\u274C Could not parse PR URL. Expected: bitbucket.org/{ws}/{repo}/pull-requests/{id}`
|
|
21645
|
-
}
|
|
21646
|
-
]
|
|
21647
|
-
};
|
|
21563
|
+
return mcpText(
|
|
21564
|
+
`\u274C Could not parse PR URL. Expected: bitbucket.org/{ws}/{repo}/pull-requests/{id}`
|
|
21565
|
+
);
|
|
21648
21566
|
}
|
|
21649
21567
|
const safeWs = sanitize(ws);
|
|
21650
21568
|
const safeRepo = sanitize(repo);
|
|
21569
|
+
const auth = buildBasicAuth("BITBUCKET_USER_EMAIL", "BITBUCKET_API_TOKEN");
|
|
21651
21570
|
const prUrl = `https://api.bitbucket.org/2.0/repositories/${safeWs}/${safeRepo}/pullrequests/${prId}`;
|
|
21652
|
-
const jsonData = await
|
|
21571
|
+
const jsonData = await callRestApi(prUrl, auth);
|
|
21653
21572
|
const parseResult = BitbucketPrSchema.safeParse(jsonData);
|
|
21654
21573
|
if (!parseResult.success) {
|
|
21655
21574
|
throw new Error(`Failed to parse PR response: ${parseResult.error.message}`);
|
|
@@ -21663,7 +21582,7 @@ function registerIntegrationTools(server2) {
|
|
|
21663
21582
|
${pr.description || "No description"}`;
|
|
21664
21583
|
if (includeDiff) {
|
|
21665
21584
|
const diffUrl = `https://api.bitbucket.org/2.0/repositories/${safeWs}/${safeRepo}/pullrequests/${prId}/diff`;
|
|
21666
|
-
const diff = await
|
|
21585
|
+
const diff = await callRestApi(diffUrl, auth, "text/plain");
|
|
21667
21586
|
const DIFF_FILE_THRESHOLD = 5e4;
|
|
21668
21587
|
if (diff.length < DIFF_FILE_THRESHOLD) {
|
|
21669
21588
|
output += `
|
|
@@ -21691,10 +21610,9 @@ ${diff}
|
|
|
21691
21610
|
}
|
|
21692
21611
|
}
|
|
21693
21612
|
}
|
|
21694
|
-
return
|
|
21613
|
+
return mcpText(output);
|
|
21695
21614
|
} catch (error2) {
|
|
21696
|
-
|
|
21697
|
-
return { content: [{ type: "text", text: errorMsg }] };
|
|
21615
|
+
return mcpText(error2 instanceof Error ? error2.message : String(error2));
|
|
21698
21616
|
}
|
|
21699
21617
|
}
|
|
21700
21618
|
);
|
|
@@ -21706,62 +21624,26 @@ ${diff}
|
|
|
21706
21624
|
},
|
|
21707
21625
|
async ({ ticketId }) => {
|
|
21708
21626
|
try {
|
|
21709
|
-
const cloudId = getCredential("ATLASSIAN_CLOUD_ID");
|
|
21710
|
-
const userEmail = getCredential("ATLASSIAN_USER_EMAIL");
|
|
21711
|
-
const apiToken = getCredential("ATLASSIAN_API_TOKEN");
|
|
21712
|
-
if (!cloudId || !userEmail || !apiToken) {
|
|
21713
|
-
const missing = [
|
|
21714
|
-
!cloudId && "ATLASSIAN_CLOUD_ID",
|
|
21715
|
-
!userEmail && "ATLASSIAN_USER_EMAIL",
|
|
21716
|
-
!apiToken && "ATLASSIAN_API_TOKEN"
|
|
21717
|
-
].filter(Boolean).join(", ");
|
|
21718
|
-
return {
|
|
21719
|
-
content: [
|
|
21720
|
-
{
|
|
21721
|
-
type: "text",
|
|
21722
|
-
text: `\u274C Missing ${missing}. Create an API Token at id.atlassian.com/manage-profile/security/api-tokens.`
|
|
21723
|
-
}
|
|
21724
|
-
]
|
|
21725
|
-
};
|
|
21726
|
-
}
|
|
21727
21627
|
const safeTicketId = ticketId.match(/^[A-Z]+-\d+$/)?.[0];
|
|
21728
21628
|
if (!safeTicketId) {
|
|
21729
|
-
return {
|
|
21730
|
-
content: [
|
|
21731
|
-
{
|
|
21732
|
-
type: "text",
|
|
21733
|
-
text: `\u274C Invalid ticket ID format: ${ticketId}
|
|
21629
|
+
return mcpText(`\u274C Invalid ticket ID format: ${ticketId}
|
|
21734
21630
|
|
|
21735
|
-
Expected format: PROJ-123`
|
|
21736
|
-
}
|
|
21737
|
-
]
|
|
21738
|
-
};
|
|
21631
|
+
Expected format: PROJ-123`);
|
|
21739
21632
|
}
|
|
21633
|
+
const cloudId = getCredential("ATLASSIAN_CLOUD_ID");
|
|
21634
|
+
if (!cloudId) throw new Error("Missing ATLASSIAN_CLOUD_ID");
|
|
21635
|
+
const auth = buildBasicAuth("ATLASSIAN_USER_EMAIL", "ATLASSIAN_API_TOKEN");
|
|
21740
21636
|
const url = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue/${safeTicketId}`;
|
|
21741
|
-
const jsonData = await
|
|
21637
|
+
const jsonData = await callRestApi(url, auth);
|
|
21742
21638
|
const parseResult = JiraTicketSchema.safeParse(jsonData);
|
|
21743
21639
|
if (!parseResult.success) {
|
|
21744
|
-
return {
|
|
21745
|
-
content: [
|
|
21746
|
-
{
|
|
21747
|
-
type: "text",
|
|
21748
|
-
text: `\u274C Invalid Jira response format: ${parseResult.error.message}`
|
|
21749
|
-
}
|
|
21750
|
-
]
|
|
21751
|
-
};
|
|
21640
|
+
return mcpText(`\u274C Invalid Jira response format: ${parseResult.error.message}`);
|
|
21752
21641
|
}
|
|
21753
21642
|
const ticket = parseResult.data;
|
|
21754
21643
|
if (ticket.errorMessages && ticket.errorMessages.length > 0) {
|
|
21755
|
-
return {
|
|
21756
|
-
content: [
|
|
21757
|
-
{
|
|
21758
|
-
type: "text",
|
|
21759
|
-
text: `\u274C Ticket not found: ${ticketId}
|
|
21644
|
+
return mcpText(`\u274C Ticket not found: ${ticketId}
|
|
21760
21645
|
|
|
21761
|
-
${ticket.errorMessages.join("\n")}`
|
|
21762
|
-
}
|
|
21763
|
-
]
|
|
21764
|
-
};
|
|
21646
|
+
${ticket.errorMessages.join("\n")}`);
|
|
21765
21647
|
}
|
|
21766
21648
|
const output = `## \u{1F3AB} ${ticketId}: ${ticket.fields.summary}
|
|
21767
21649
|
|
|
@@ -21776,10 +21658,9 @@ ${typeof ticket.fields.description === "string" ? ticket.fields.description : ad
|
|
|
21776
21658
|
|
|
21777
21659
|
### Labels
|
|
21778
21660
|
${ticket.fields.labels?.join(", ") || "None"}`;
|
|
21779
|
-
return
|
|
21661
|
+
return mcpText(output);
|
|
21780
21662
|
} catch (error2) {
|
|
21781
|
-
|
|
21782
|
-
return { content: [{ type: "text", text: `Error: ${errorMsg}` }] };
|
|
21663
|
+
return mcpText(`Error: ${error2 instanceof Error ? error2.message : String(error2)}`);
|
|
21783
21664
|
}
|
|
21784
21665
|
}
|
|
21785
21666
|
);
|
package/dist/tools/agent.d.ts
CHANGED
|
@@ -3,7 +3,4 @@
|
|
|
3
3
|
* Supports handoff file injection and automatic fallback
|
|
4
4
|
*/
|
|
5
5
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
6
|
-
/**
|
|
7
|
-
* Register agent delegation tools with MCP server
|
|
8
|
-
*/
|
|
9
6
|
export declare function registerAgentTools(server: McpServer): void;
|
package/dist/tools/agent.js
CHANGED
|
@@ -7,14 +7,11 @@ import { randomUUID } from 'crypto';
|
|
|
7
7
|
import * as fs from 'fs';
|
|
8
8
|
import * as path from 'path';
|
|
9
9
|
import { z } from 'zod';
|
|
10
|
-
import { getWorkspaceRoot } from '../utils/utils.js';
|
|
10
|
+
import { getWorkspaceRoot, mcpJson, mcpText } from '../utils/utils.js';
|
|
11
11
|
import { commandExists, sanitizeOutput, validatePath } from './security.js';
|
|
12
12
|
// Configurable timeout via environment variable (default: 5 minutes)
|
|
13
13
|
const AGENT_TIMEOUT = parseInt(process.env.KIT_AGENT_TIMEOUT || '300000', 10);
|
|
14
14
|
const jobRegistry = new Map();
|
|
15
|
-
/**
|
|
16
|
-
* Generate a URL-safe slug from a task string (first ~6 words, max 48 chars)
|
|
17
|
-
*/
|
|
18
15
|
function taskSlug(task) {
|
|
19
16
|
return task
|
|
20
17
|
.toLowerCase()
|
|
@@ -25,11 +22,6 @@ function taskSlug(task) {
|
|
|
25
22
|
.join('-')
|
|
26
23
|
.slice(0, 48);
|
|
27
24
|
}
|
|
28
|
-
/**
|
|
29
|
-
* Initialize a live-streaming log file for an agent job.
|
|
30
|
-
* Writes the header synchronously so the file exists immediately,
|
|
31
|
-
* then opens an append-mode WriteStream for real-time chunk writing.
|
|
32
|
-
*/
|
|
33
25
|
function initAgentLog(workspaceRoot, slug, agent) {
|
|
34
26
|
const logsDir = path.join(workspaceRoot, '.agent-kit', 'logs', 'agents');
|
|
35
27
|
fs.mkdirSync(logsDir, { recursive: true });
|
|
@@ -41,25 +33,19 @@ function initAgentLog(workspaceRoot, slug, agent) {
|
|
|
41
33
|
logStream.on('error', () => { });
|
|
42
34
|
return { logPath: path.relative(workspaceRoot, logFile), logStream };
|
|
43
35
|
}
|
|
44
|
-
/**
|
|
45
|
-
* Register agent delegation tools with MCP server
|
|
46
|
-
*/
|
|
47
36
|
export function registerAgentTools(server) {
|
|
48
|
-
// ═══════════════════════════════════════════════════════════════
|
|
49
|
-
// TOOL: TRIGGER AGENT
|
|
50
|
-
// Delegates a task to an external agent CLI (gemini or claude).
|
|
51
|
-
// Auto-detects if task is a file path and reads it as context.
|
|
52
|
-
// Falls back to the alternate agent if requested one is missing.
|
|
53
|
-
// ═══════════════════════════════════════════════════════════════
|
|
54
37
|
server.tool('kit_trigger_agent', '!Important: Trigger only when user explicitly asks to delegate a task to an external agent CLI (gemini or claude). The task can be a direct message or a path to a handoff file (.agent-kit/handoffs/plans/plan-xyz.md). Falls back to the other agent CLI if the requested one is not installed.', {
|
|
55
38
|
agent: z.enum(['gemini', 'claude']).describe('The agent CLI to invoke: "gemini" or "claude"'),
|
|
56
39
|
task: z
|
|
57
40
|
.string()
|
|
58
41
|
.describe('Task message or path to a handoff file (e.g., ".agent-kit/handoffs/plans/plan-xyz.md")'),
|
|
59
|
-
}, async ({ agent, task }) => {
|
|
42
|
+
}, async ({ agent, task }, extra) => {
|
|
43
|
+
const workspaceRoot = getWorkspaceRoot();
|
|
44
|
+
let job;
|
|
45
|
+
let logPath;
|
|
46
|
+
let killChild;
|
|
60
47
|
try {
|
|
61
|
-
|
|
62
|
-
// Detect if task is a file path — resolve and read if it exists
|
|
48
|
+
// Resolve task: read file if path, otherwise use as-is
|
|
63
49
|
let prompt = task;
|
|
64
50
|
const resolvedTask = path.resolve(workspaceRoot, task);
|
|
65
51
|
if (fs.existsSync(resolvedTask) && fs.statSync(resolvedTask).isFile()) {
|
|
@@ -68,12 +54,10 @@ export function registerAgentTools(server) {
|
|
|
68
54
|
prompt = fs.readFileSync(resolvedTask, 'utf-8');
|
|
69
55
|
}
|
|
70
56
|
catch (err) {
|
|
71
|
-
return {
|
|
72
|
-
content: [{ type: 'text', text: `Error reading task file: ${err}` }],
|
|
73
|
-
};
|
|
57
|
+
return mcpText(`Error reading task file: ${err}`);
|
|
74
58
|
}
|
|
75
59
|
}
|
|
76
|
-
//
|
|
60
|
+
// Resolve agent with fallback
|
|
77
61
|
let usedAgent = agent;
|
|
78
62
|
let fallbackReason;
|
|
79
63
|
if (!commandExists(agent)) {
|
|
@@ -86,105 +70,103 @@ export function registerAgentTools(server) {
|
|
|
86
70
|
const installHint = agent === 'gemini'
|
|
87
71
|
? 'Install Gemini CLI: https://github.com/google-gemini/gemini-cli'
|
|
88
72
|
: 'Install Claude CLI: npm install -g @anthropic-ai/claude-code';
|
|
89
|
-
return {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
status: 'error',
|
|
96
|
-
output: '',
|
|
97
|
-
error: `Neither ${agent} nor the fallback agent CLI is installed. ${installHint}`,
|
|
98
|
-
}, null, 2),
|
|
99
|
-
},
|
|
100
|
-
],
|
|
101
|
-
};
|
|
73
|
+
return mcpJson({
|
|
74
|
+
agent,
|
|
75
|
+
status: 'error',
|
|
76
|
+
output: '',
|
|
77
|
+
error: `Neither ${agent} nor the fallback agent CLI is installed. ${installHint}`,
|
|
78
|
+
});
|
|
102
79
|
}
|
|
103
80
|
}
|
|
104
|
-
// Execute the agent CLI with the prompt
|
|
105
81
|
// Gemini: -y (--yolo) for auto-accept, -p for headless prompt
|
|
106
82
|
// Claude: --dangerously-skip-permissions for auto-accept, -p/--print for headless
|
|
107
83
|
const agentArgs = usedAgent === 'gemini'
|
|
108
84
|
? ['-y', '-p', prompt]
|
|
109
85
|
: ['--dangerously-skip-permissions', '-p', prompt];
|
|
110
|
-
const
|
|
111
|
-
|
|
86
|
+
const { logPath: lp, logStream } = initAgentLog(workspaceRoot, taskSlug(task), usedAgent);
|
|
87
|
+
logPath = lp;
|
|
112
88
|
const jobId = randomUUID();
|
|
89
|
+
// detached=true on Unix creates a new process group so we can kill the
|
|
90
|
+
// entire tree (CLI + its sub-agents) with process.kill(-pid, signal).
|
|
113
91
|
const child = spawn(usedAgent, agentArgs, {
|
|
114
92
|
cwd: workspaceRoot,
|
|
115
93
|
env: { ...process.env, GEMINI_WORKSPACE: workspaceRoot },
|
|
94
|
+
detached: process.platform !== 'win32',
|
|
116
95
|
});
|
|
117
|
-
const
|
|
118
|
-
|
|
96
|
+
const killTree = (sig) => {
|
|
97
|
+
if (process.platform !== 'win32' && child.pid) {
|
|
98
|
+
try {
|
|
99
|
+
process.kill(-child.pid, sig);
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
child.kill(sig);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
child.kill(sig);
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
job = {
|
|
119
110
|
agent: usedAgent,
|
|
120
111
|
process: child,
|
|
121
112
|
startedAt: new Date(),
|
|
122
113
|
chunks: [],
|
|
123
114
|
logStream,
|
|
124
|
-
timeoutHandle: setTimeout(() =>
|
|
125
|
-
child.kill('SIGTERM');
|
|
126
|
-
}, AGENT_TIMEOUT),
|
|
115
|
+
timeoutHandle: setTimeout(() => killTree('SIGTERM'), AGENT_TIMEOUT),
|
|
127
116
|
};
|
|
128
117
|
jobRegistry.set(jobId, job);
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
else
|
|
143
|
-
reject(new Error(`Process exited with code ${code}`));
|
|
144
|
-
});
|
|
145
|
-
child.on('error', reject);
|
|
118
|
+
// Kill child if the MCP request is cancelled (e.g. user presses ESC)
|
|
119
|
+
killChild = () => {
|
|
120
|
+
killTree('SIGTERM');
|
|
121
|
+
setTimeout(() => killTree('SIGKILL'), 5000).unref();
|
|
122
|
+
};
|
|
123
|
+
if (extra.signal.aborted)
|
|
124
|
+
killChild();
|
|
125
|
+
else
|
|
126
|
+
extra.signal.addEventListener('abort', killChild);
|
|
127
|
+
await new Promise((resolve, reject) => {
|
|
128
|
+
child.stdout?.on('data', (data) => {
|
|
129
|
+
job.chunks.push(data.toString());
|
|
130
|
+
job.logStream.write(data);
|
|
146
131
|
});
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
}
|
|
167
|
-
finally {
|
|
168
|
-
job.logStream.end();
|
|
169
|
-
clearTimeout(job.timeoutHandle);
|
|
170
|
-
jobRegistry.delete(jobId);
|
|
171
|
-
}
|
|
132
|
+
child.stderr?.on('data', (data) => {
|
|
133
|
+
job.chunks.push(data.toString());
|
|
134
|
+
job.logStream.write(data);
|
|
135
|
+
});
|
|
136
|
+
child.on('close', (code, signal) => {
|
|
137
|
+
if (code === 0)
|
|
138
|
+
resolve();
|
|
139
|
+
else
|
|
140
|
+
reject(new Error(`Process exited with code ${code}${signal ? ` (signal: ${signal})` : ''}`));
|
|
141
|
+
});
|
|
142
|
+
child.on('error', reject);
|
|
143
|
+
});
|
|
144
|
+
return mcpJson({
|
|
145
|
+
agent: usedAgent,
|
|
146
|
+
status: fallbackReason ? 'fallback' : 'success',
|
|
147
|
+
log: logPath,
|
|
148
|
+
output: sanitizeOutput(job.chunks.join('')),
|
|
149
|
+
...(fallbackReason && { fallback_reason: fallbackReason }),
|
|
150
|
+
});
|
|
172
151
|
}
|
|
173
152
|
catch (error) {
|
|
153
|
+
if (extra.signal.aborted) {
|
|
154
|
+
return mcpJson({
|
|
155
|
+
agent,
|
|
156
|
+
status: 'cancelled',
|
|
157
|
+
log: logPath,
|
|
158
|
+
output: sanitizeOutput(job?.chunks.join('') ?? ''),
|
|
159
|
+
});
|
|
160
|
+
}
|
|
174
161
|
const msg = error instanceof Error ? error.message : String(error);
|
|
175
|
-
return {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
error: msg,
|
|
184
|
-
}, null, 2),
|
|
185
|
-
},
|
|
186
|
-
],
|
|
187
|
-
};
|
|
162
|
+
return mcpJson({ agent, status: 'error', output: '', error: msg });
|
|
163
|
+
}
|
|
164
|
+
finally {
|
|
165
|
+
if (killChild)
|
|
166
|
+
extra.signal.removeEventListener('abort', killChild);
|
|
167
|
+
job?.logStream.end();
|
|
168
|
+
if (job)
|
|
169
|
+
clearTimeout(job.timeoutHandle);
|
|
188
170
|
}
|
|
189
171
|
});
|
|
190
172
|
}
|
package/dist/tools/core.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import * as fs from 'fs';
|
|
5
5
|
import * as path from 'path';
|
|
6
6
|
import { z } from 'zod';
|
|
7
|
-
import { getWorkspaceRoot } from '../utils/utils.js';
|
|
7
|
+
import { getWorkspaceRoot, mcpText } from '../utils/utils.js';
|
|
8
8
|
import { DEFAULT_EXTENSIONS } from './config.js';
|
|
9
9
|
/**
|
|
10
10
|
* Register core tools with MCP server
|
|
@@ -32,14 +32,10 @@ export function registerCoreTools(server) {
|
|
|
32
32
|
}
|
|
33
33
|
const filePath = path.join(handoffDir, filename);
|
|
34
34
|
fs.writeFileSync(filePath, content, 'utf8');
|
|
35
|
-
return {
|
|
36
|
-
content: [{ type: 'text', text: `✅ Saved to: ${filePath}` }],
|
|
37
|
-
};
|
|
35
|
+
return mcpText(`✅ Saved to: ${filePath}`);
|
|
38
36
|
}
|
|
39
37
|
catch (error) {
|
|
40
|
-
return {
|
|
41
|
-
content: [{ type: 'text', text: `Error saving handoff: ${error}` }],
|
|
42
|
-
};
|
|
38
|
+
return mcpText(`Error saving handoff: ${error}`);
|
|
43
39
|
}
|
|
44
40
|
});
|
|
45
41
|
}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { writeFileSync } from 'fs';
|
|
6
6
|
import { z } from 'zod';
|
|
7
7
|
import { getCredential } from '../utils/credentials.js';
|
|
8
|
+
import { mcpText } from '../utils/utils.js';
|
|
8
9
|
import { adfToMarkdown } from '../utils/parser.js';
|
|
9
10
|
import { sanitize } from './security.js';
|
|
10
11
|
// Zod schema for Bitbucket PR REST API response
|
|
@@ -54,63 +55,22 @@ const JiraTicketSchema = z.object({
|
|
|
54
55
|
errorMessages: z.array(z.string()).optional(),
|
|
55
56
|
fields: JiraFieldsSchema,
|
|
56
57
|
});
|
|
57
|
-
function
|
|
58
|
-
const email = getCredential(
|
|
59
|
-
const token = getCredential(
|
|
60
|
-
if (!email || !token)
|
|
61
|
-
throw new Error(
|
|
62
|
-
}
|
|
58
|
+
function buildBasicAuth(emailVar, tokenVar) {
|
|
59
|
+
const email = getCredential(emailVar);
|
|
60
|
+
const token = getCredential(tokenVar);
|
|
61
|
+
if (!email || !token)
|
|
62
|
+
throw new Error(`Missing ${emailVar} or ${tokenVar}`);
|
|
63
63
|
return 'Basic ' + Buffer.from(`${email}:${token}`).toString('base64');
|
|
64
64
|
}
|
|
65
|
-
function
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
}
|
|
71
|
-
return 'Basic ' + Buffer.from(`${email}:${token}`).toString('base64');
|
|
72
|
-
}
|
|
73
|
-
async function callAtlassianRestApi(url) {
|
|
74
|
-
const auth = buildJiraBasicAuth();
|
|
75
|
-
const resp = await fetch(url, { headers: { Authorization: auth, Accept: 'application/json' } });
|
|
76
|
-
if (resp.status === 401) {
|
|
77
|
-
throw new Error('❌ Auth failed: check ATLASSIAN_USER_EMAIL and ATLASSIAN_API_TOKEN');
|
|
78
|
-
}
|
|
79
|
-
if (resp.status === 404) {
|
|
80
|
-
throw new Error(`❌ Not found: ${url}`);
|
|
81
|
-
}
|
|
82
|
-
if (!resp.ok) {
|
|
83
|
-
throw new Error(`❌ API error ${resp.status}: ${await resp.text()}`);
|
|
84
|
-
}
|
|
85
|
-
return await resp.json();
|
|
86
|
-
}
|
|
87
|
-
async function callBitbucketRestApi(url) {
|
|
88
|
-
const auth = buildBitbucketBasicAuth();
|
|
89
|
-
const resp = await fetch(url, { headers: { Authorization: auth, Accept: 'application/json' } });
|
|
90
|
-
if (resp.status === 401) {
|
|
91
|
-
throw new Error('❌ Auth failed: check BITBUCKET_USER_EMAIL and BITBUCKET_API_TOKEN');
|
|
92
|
-
}
|
|
93
|
-
if (resp.status === 404) {
|
|
94
|
-
throw new Error(`❌ Not found: ${url}`);
|
|
95
|
-
}
|
|
96
|
-
if (!resp.ok) {
|
|
97
|
-
throw new Error(`❌ API error ${resp.status}: ${await resp.text()}`);
|
|
98
|
-
}
|
|
99
|
-
return await resp.json();
|
|
100
|
-
}
|
|
101
|
-
async function callBitbucketDiffApi(url) {
|
|
102
|
-
const auth = buildBitbucketBasicAuth();
|
|
103
|
-
const resp = await fetch(url, { headers: { Authorization: auth, Accept: 'text/plain' } });
|
|
104
|
-
if (resp.status === 401) {
|
|
105
|
-
throw new Error('❌ Auth failed: check BITBUCKET_USER_EMAIL and BITBUCKET_API_TOKEN');
|
|
106
|
-
}
|
|
107
|
-
if (resp.status === 404) {
|
|
65
|
+
async function callRestApi(url, auth, accept = 'application/json') {
|
|
66
|
+
const resp = await fetch(url, { headers: { Authorization: auth, Accept: accept } });
|
|
67
|
+
if (resp.status === 401)
|
|
68
|
+
throw new Error(`❌ Auth failed (401): ${url}`);
|
|
69
|
+
if (resp.status === 404)
|
|
108
70
|
throw new Error(`❌ Not found: ${url}`);
|
|
109
|
-
|
|
110
|
-
if (!resp.ok) {
|
|
71
|
+
if (!resp.ok)
|
|
111
72
|
throw new Error(`❌ API error ${resp.status}: ${await resp.text()}`);
|
|
112
|
-
|
|
113
|
-
return await resp.text();
|
|
73
|
+
return accept === 'text/plain' ? resp.text() : resp.json();
|
|
114
74
|
}
|
|
115
75
|
export function registerIntegrationTools(server) {
|
|
116
76
|
// TOOL: GET BITBUCKET PR
|
|
@@ -128,22 +88,9 @@ export function registerIntegrationTools(server) {
|
|
|
128
88
|
.describe('Include unified diff in response'),
|
|
129
89
|
}, async ({ input, workspace, repoSlug, includeDiff }) => {
|
|
130
90
|
try {
|
|
131
|
-
const bbEmail = getCredential('BITBUCKET_USER_EMAIL');
|
|
132
|
-
const bbToken = getCredential('BITBUCKET_API_TOKEN');
|
|
133
|
-
if (!bbEmail || !bbToken) {
|
|
134
|
-
return {
|
|
135
|
-
content: [
|
|
136
|
-
{
|
|
137
|
-
type: 'text',
|
|
138
|
-
text: `❌ Missing BITBUCKET_USER_EMAIL or BITBUCKET_API_TOKEN. Set them in ~/.claude/agent-kit or as environment variables.`,
|
|
139
|
-
},
|
|
140
|
-
],
|
|
141
|
-
};
|
|
142
|
-
}
|
|
143
91
|
let ws;
|
|
144
92
|
let repo;
|
|
145
93
|
let prId;
|
|
146
|
-
// Try URL parse
|
|
147
94
|
const urlMatch = input.match(/bitbucket\.org\/([^/]+)\/([^/]+)\/pull-requests\/(\d+)/);
|
|
148
95
|
if (urlMatch) {
|
|
149
96
|
ws = urlMatch[1];
|
|
@@ -156,29 +103,16 @@ export function registerIntegrationTools(server) {
|
|
|
156
103
|
repo = repoSlug;
|
|
157
104
|
}
|
|
158
105
|
if (!ws) {
|
|
159
|
-
return
|
|
160
|
-
content: [
|
|
161
|
-
{
|
|
162
|
-
type: 'text',
|
|
163
|
-
text: `❌ workspace is required. Pass it as a parameter or set BITBUCKET_DEFAULT_WORKSPACE in your MCP env config.`,
|
|
164
|
-
},
|
|
165
|
-
],
|
|
166
|
-
};
|
|
106
|
+
return mcpText(`❌ workspace is required. Pass it as a parameter or set BITBUCKET_DEFAULT_WORKSPACE in your MCP env config.`);
|
|
167
107
|
}
|
|
168
108
|
if (!repo || !prId) {
|
|
169
|
-
return {
|
|
170
|
-
content: [
|
|
171
|
-
{
|
|
172
|
-
type: 'text',
|
|
173
|
-
text: `❌ Could not parse PR URL. Expected: bitbucket.org/{ws}/{repo}/pull-requests/{id}`,
|
|
174
|
-
},
|
|
175
|
-
],
|
|
176
|
-
};
|
|
109
|
+
return mcpText(`❌ Could not parse PR URL. Expected: bitbucket.org/{ws}/{repo}/pull-requests/{id}`);
|
|
177
110
|
}
|
|
178
111
|
const safeWs = sanitize(ws);
|
|
179
112
|
const safeRepo = sanitize(repo);
|
|
113
|
+
const auth = buildBasicAuth('BITBUCKET_USER_EMAIL', 'BITBUCKET_API_TOKEN');
|
|
180
114
|
const prUrl = `https://api.bitbucket.org/2.0/repositories/${safeWs}/${safeRepo}/pullrequests/${prId}`;
|
|
181
|
-
const jsonData = await
|
|
115
|
+
const jsonData = await callRestApi(prUrl, auth);
|
|
182
116
|
const parseResult = BitbucketPrSchema.safeParse(jsonData);
|
|
183
117
|
if (!parseResult.success) {
|
|
184
118
|
throw new Error(`Failed to parse PR response: ${parseResult.error.message}`);
|
|
@@ -192,7 +126,7 @@ export function registerIntegrationTools(server) {
|
|
|
192
126
|
${pr.description || 'No description'}`;
|
|
193
127
|
if (includeDiff) {
|
|
194
128
|
const diffUrl = `https://api.bitbucket.org/2.0/repositories/${safeWs}/${safeRepo}/pullrequests/${prId}/diff`;
|
|
195
|
-
const diff = await
|
|
129
|
+
const diff = (await callRestApi(diffUrl, auth, 'text/plain'));
|
|
196
130
|
const DIFF_FILE_THRESHOLD = 50_000;
|
|
197
131
|
if (diff.length < DIFF_FILE_THRESHOLD) {
|
|
198
132
|
output += `\n\n### Diff\n\`\`\`diff\n${diff}\n\`\`\``;
|
|
@@ -208,11 +142,10 @@ ${pr.description || 'No description'}`;
|
|
|
208
142
|
}
|
|
209
143
|
}
|
|
210
144
|
}
|
|
211
|
-
return
|
|
145
|
+
return mcpText(output);
|
|
212
146
|
}
|
|
213
147
|
catch (error) {
|
|
214
|
-
|
|
215
|
-
return { content: [{ type: 'text', text: errorMsg }] };
|
|
148
|
+
return mcpText(error instanceof Error ? error.message : String(error));
|
|
216
149
|
}
|
|
217
150
|
});
|
|
218
151
|
// TOOL: JIRA GET TICKET
|
|
@@ -220,60 +153,23 @@ ${pr.description || 'No description'}`;
|
|
|
220
153
|
ticketId: z.string().describe('Jira ticket ID (e.g., PROJ-123)'),
|
|
221
154
|
}, async ({ ticketId }) => {
|
|
222
155
|
try {
|
|
223
|
-
const cloudId = getCredential('ATLASSIAN_CLOUD_ID');
|
|
224
|
-
const userEmail = getCredential('ATLASSIAN_USER_EMAIL');
|
|
225
|
-
const apiToken = getCredential('ATLASSIAN_API_TOKEN');
|
|
226
|
-
if (!cloudId || !userEmail || !apiToken) {
|
|
227
|
-
const missing = [
|
|
228
|
-
!cloudId && 'ATLASSIAN_CLOUD_ID',
|
|
229
|
-
!userEmail && 'ATLASSIAN_USER_EMAIL',
|
|
230
|
-
!apiToken && 'ATLASSIAN_API_TOKEN',
|
|
231
|
-
]
|
|
232
|
-
.filter(Boolean)
|
|
233
|
-
.join(', ');
|
|
234
|
-
return {
|
|
235
|
-
content: [
|
|
236
|
-
{
|
|
237
|
-
type: 'text',
|
|
238
|
-
text: `❌ Missing ${missing}. Create an API Token at id.atlassian.com/manage-profile/security/api-tokens.`,
|
|
239
|
-
},
|
|
240
|
-
],
|
|
241
|
-
};
|
|
242
|
-
}
|
|
243
156
|
const safeTicketId = ticketId.match(/^[A-Z]+-\d+$/)?.[0];
|
|
244
157
|
if (!safeTicketId) {
|
|
245
|
-
return {
|
|
246
|
-
content: [
|
|
247
|
-
{
|
|
248
|
-
type: 'text',
|
|
249
|
-
text: `❌ Invalid ticket ID format: ${ticketId}\n\nExpected format: PROJ-123`,
|
|
250
|
-
},
|
|
251
|
-
],
|
|
252
|
-
};
|
|
158
|
+
return mcpText(`❌ Invalid ticket ID format: ${ticketId}\n\nExpected format: PROJ-123`);
|
|
253
159
|
}
|
|
160
|
+
const cloudId = getCredential('ATLASSIAN_CLOUD_ID');
|
|
161
|
+
if (!cloudId)
|
|
162
|
+
throw new Error('Missing ATLASSIAN_CLOUD_ID');
|
|
163
|
+
const auth = buildBasicAuth('ATLASSIAN_USER_EMAIL', 'ATLASSIAN_API_TOKEN');
|
|
254
164
|
const url = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue/${safeTicketId}`;
|
|
255
|
-
const jsonData = await
|
|
165
|
+
const jsonData = await callRestApi(url, auth);
|
|
256
166
|
const parseResult = JiraTicketSchema.safeParse(jsonData);
|
|
257
167
|
if (!parseResult.success) {
|
|
258
|
-
return {
|
|
259
|
-
content: [
|
|
260
|
-
{
|
|
261
|
-
type: 'text',
|
|
262
|
-
text: `❌ Invalid Jira response format: ${parseResult.error.message}`,
|
|
263
|
-
},
|
|
264
|
-
],
|
|
265
|
-
};
|
|
168
|
+
return mcpText(`❌ Invalid Jira response format: ${parseResult.error.message}`);
|
|
266
169
|
}
|
|
267
170
|
const ticket = parseResult.data;
|
|
268
171
|
if (ticket.errorMessages && ticket.errorMessages.length > 0) {
|
|
269
|
-
return {
|
|
270
|
-
content: [
|
|
271
|
-
{
|
|
272
|
-
type: 'text',
|
|
273
|
-
text: `❌ Ticket not found: ${ticketId}\n\n${ticket.errorMessages.join('\n')}`,
|
|
274
|
-
},
|
|
275
|
-
],
|
|
276
|
-
};
|
|
172
|
+
return mcpText(`❌ Ticket not found: ${ticketId}\n\n${ticket.errorMessages.join('\n')}`);
|
|
277
173
|
}
|
|
278
174
|
const output = `## 🎫 ${ticketId}: ${ticket.fields.summary}
|
|
279
175
|
|
|
@@ -290,11 +186,10 @@ ${typeof ticket.fields.description === 'string'
|
|
|
290
186
|
|
|
291
187
|
### Labels
|
|
292
188
|
${ticket.fields.labels?.join(', ') || 'None'}`;
|
|
293
|
-
return
|
|
189
|
+
return mcpText(output);
|
|
294
190
|
}
|
|
295
191
|
catch (error) {
|
|
296
|
-
|
|
297
|
-
return { content: [{ type: 'text', text: `Error: ${errorMsg}` }] };
|
|
192
|
+
return mcpText(`Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
298
193
|
}
|
|
299
194
|
});
|
|
300
195
|
}
|
package/dist/utils/utils.d.ts
CHANGED
|
@@ -1 +1,10 @@
|
|
|
1
1
|
export declare function getWorkspaceRoot(): string;
|
|
2
|
+
type McpTextResult = {
|
|
3
|
+
content: [{
|
|
4
|
+
type: 'text';
|
|
5
|
+
text: string;
|
|
6
|
+
}];
|
|
7
|
+
};
|
|
8
|
+
export declare function mcpText(text: string): McpTextResult;
|
|
9
|
+
export declare function mcpJson(obj: unknown): McpTextResult;
|
|
10
|
+
export {};
|
package/dist/utils/utils.js
CHANGED