@hanhnd/agent-kit 1.0.8 → 1.0.10
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 +31 -16
- package/dist/credentials.d.ts +20 -0
- package/dist/credentials.js +87 -0
- package/dist/kit-server.js +161 -41
- package/dist/tools/agent.js +3 -2
- package/dist/tools/config.d.ts +12 -0
- package/dist/tools/config.js +43 -0
- package/dist/tools/integration.js +12 -11
- package/dist/tools/security.d.ts +4 -0
- package/dist/tools/security.js +29 -16
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
| `/ak:research [topic]` | Research a topic |
|
|
16
16
|
| `/ak:review-pr [PR URL]` | Review a pull request |
|
|
17
17
|
| `/ak:review` | Review uncommitted local changes |
|
|
18
|
+
| `/ak:debate [subject]` | Run adversarial debate (Gilfoyle vs Dinesh vs Judge) |
|
|
18
19
|
| `/ak:ticket [ID]` | Fetch a Jira ticket and plan from it |
|
|
19
20
|
| `/ak:git` | Git commit, branch, and PR workflow |
|
|
20
21
|
| `/ak:init` | Create the project overview file |
|
|
@@ -34,17 +35,24 @@ claude plugin install ak
|
|
|
34
35
|
|
|
35
36
|
The plugin fetches from GitHub, registers the MCP server automatically, and makes all commands available immediately.
|
|
36
37
|
|
|
37
|
-
**Add credentials** to
|
|
38
|
+
**Add credentials** to `~/.claude/credentials` (like `~/.aws/credentials`):
|
|
38
39
|
|
|
39
40
|
```bash
|
|
40
|
-
|
|
41
|
-
export ATLASSIAN_USER_EMAIL="you@yourcompany.com"
|
|
42
|
-
export ATLASSIAN_API_TOKEN="your-atlassian-api-token"
|
|
43
|
-
export BITBUCKET_USER_EMAIL="you@yourcompany.com"
|
|
44
|
-
export BITBUCKET_API_TOKEN="your-atlassian-api-token"
|
|
45
|
-
export BITBUCKET_DEFAULT_WORKSPACE="your-default-workspace-slug"
|
|
41
|
+
touch ~/.claude/credentials && chmod 600 ~/.claude/credentials
|
|
46
42
|
```
|
|
47
43
|
|
|
44
|
+
```ini
|
|
45
|
+
[default]
|
|
46
|
+
ATLASSIAN_CLOUD_ID = your-atlassian-cloud-id
|
|
47
|
+
ATLASSIAN_USER_EMAIL = you@yourcompany.com
|
|
48
|
+
ATLASSIAN_API_TOKEN = your-atlassian-api-token
|
|
49
|
+
BITBUCKET_USER_EMAIL = you@yourcompany.com
|
|
50
|
+
BITBUCKET_API_TOKEN = your-atlassian-api-token
|
|
51
|
+
BITBUCKET_DEFAULT_WORKSPACE = your-default-workspace-slug
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
To use multiple profiles, add `[work]`, `[personal]`, etc. sections and set `KIT_PROFILE=work` in your environment.
|
|
55
|
+
|
|
48
56
|
To update: `claude plugin update ak`
|
|
49
57
|
To uninstall: `claude plugin uninstall ak`
|
|
50
58
|
|
|
@@ -72,15 +80,20 @@ claude plugin install ak
|
|
|
72
80
|
|
|
73
81
|
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.
|
|
74
82
|
|
|
75
|
-
**3. Add credentials** to
|
|
83
|
+
**3. Add credentials** to `~/.claude/credentials`:
|
|
76
84
|
|
|
77
85
|
```bash
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
86
|
+
touch ~/.claude/credentials && chmod 600 ~/.claude/credentials
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
```ini
|
|
90
|
+
[default]
|
|
91
|
+
ATLASSIAN_CLOUD_ID = your-atlassian-cloud-id
|
|
92
|
+
ATLASSIAN_USER_EMAIL = you@yourcompany.com
|
|
93
|
+
ATLASSIAN_API_TOKEN = your-atlassian-api-token
|
|
94
|
+
BITBUCKET_USER_EMAIL = you@yourcompany.com
|
|
95
|
+
BITBUCKET_API_TOKEN = your-atlassian-api-token
|
|
96
|
+
BITBUCKET_DEFAULT_WORKSPACE = your-default-workspace-slug
|
|
84
97
|
```
|
|
85
98
|
|
|
86
99
|
**4. Verify**
|
|
@@ -124,11 +137,13 @@ The MCP server source is in `src/`. Agent personas are in `agents/`. Skill modul
|
|
|
124
137
|
|
|
125
138
|
## Integrations
|
|
126
139
|
|
|
140
|
+
Credentials are stored in `~/.claude/credentials` (INI format). Keys can also be set as environment variables — env vars take priority (useful for CI/CD).
|
|
141
|
+
|
|
127
142
|
### Jira (via Atlassian REST API)
|
|
128
143
|
|
|
129
144
|
Used by `/ak:ticket` and `/ak:review-pr`.
|
|
130
145
|
|
|
131
|
-
|
|
|
146
|
+
| Key | Description |
|
|
132
147
|
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
|
|
133
148
|
| `ATLASSIAN_CLOUD_ID` | Your Atlassian Cloud ID — find it at [admin.atlassian.com](https://admin.atlassian.com) under your site settings |
|
|
134
149
|
| `ATLASSIAN_USER_EMAIL` | Your Atlassian account email |
|
|
@@ -138,7 +153,7 @@ Used by `/ak:ticket` and `/ak:review-pr`.
|
|
|
138
153
|
|
|
139
154
|
Used by `/ak:review-pr`.
|
|
140
155
|
|
|
141
|
-
|
|
|
156
|
+
| Key | Description |
|
|
142
157
|
| ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
|
|
143
158
|
| `BITBUCKET_USER_EMAIL` | Your Atlassian account email (same as Jira) |
|
|
144
159
|
| `BITBUCKET_API_TOKEN` | API token — create at [id.atlassian.com/manage-profile/security/api-tokens](https://id.atlassian.com/manage-profile/security/api-tokens) |
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credential loader — reads ~/.claude/credentials (INI format, like ~/.aws/credentials)
|
|
3
|
+
*
|
|
4
|
+
* Resolution order per key:
|
|
5
|
+
* 1. process.env.KEY (explicit override / CI)
|
|
6
|
+
* 2. ~/.claude/credentials [profile] section
|
|
7
|
+
* 3. undefined
|
|
8
|
+
*
|
|
9
|
+
* File format:
|
|
10
|
+
* [default]
|
|
11
|
+
* ATLASSIAN_USER_EMAIL = user@example.com
|
|
12
|
+
* ATLASSIAN_API_TOKEN = your_token_here
|
|
13
|
+
*
|
|
14
|
+
* Active profile: KIT_PROFILE env var (defaults to "default")
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Resolve a credential key using the priority chain:
|
|
18
|
+
* process.env.KEY → ~/.claude/credentials [profile] → undefined
|
|
19
|
+
*/
|
|
20
|
+
export declare function getCredential(key: string): string | undefined;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credential loader — reads ~/.claude/credentials (INI format, like ~/.aws/credentials)
|
|
3
|
+
*
|
|
4
|
+
* Resolution order per key:
|
|
5
|
+
* 1. process.env.KEY (explicit override / CI)
|
|
6
|
+
* 2. ~/.claude/credentials [profile] section
|
|
7
|
+
* 3. undefined
|
|
8
|
+
*
|
|
9
|
+
* File format:
|
|
10
|
+
* [default]
|
|
11
|
+
* ATLASSIAN_USER_EMAIL = user@example.com
|
|
12
|
+
* ATLASSIAN_API_TOKEN = your_token_here
|
|
13
|
+
*
|
|
14
|
+
* Active profile: KIT_PROFILE env var (defaults to "default")
|
|
15
|
+
*/
|
|
16
|
+
import * as fs from 'fs';
|
|
17
|
+
import * as os from 'os';
|
|
18
|
+
import * as path from 'path';
|
|
19
|
+
const CREDENTIALS_FILE = path.join(os.homedir(), '.claude', 'credentials');
|
|
20
|
+
let cachedCredentials = null;
|
|
21
|
+
/**
|
|
22
|
+
* Parse a simple INI file into a map of profiles.
|
|
23
|
+
* Ignores comment lines (# or ;) and blank lines.
|
|
24
|
+
*/
|
|
25
|
+
function parseIni(content) {
|
|
26
|
+
const result = {};
|
|
27
|
+
let currentSection = 'default';
|
|
28
|
+
for (const raw of content.split('\n')) {
|
|
29
|
+
const line = raw.trim();
|
|
30
|
+
if (!line || line.startsWith('#') || line.startsWith(';'))
|
|
31
|
+
continue;
|
|
32
|
+
const sectionMatch = line.match(/^\[([^\]]+)\]$/);
|
|
33
|
+
if (sectionMatch) {
|
|
34
|
+
currentSection = sectionMatch[1].trim();
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
const eqIdx = line.indexOf('=');
|
|
38
|
+
if (eqIdx === -1)
|
|
39
|
+
continue;
|
|
40
|
+
const key = line.slice(0, eqIdx).trim();
|
|
41
|
+
const value = line.slice(eqIdx + 1).trim();
|
|
42
|
+
if (!result[currentSection])
|
|
43
|
+
result[currentSection] = {};
|
|
44
|
+
result[currentSection][key] = value;
|
|
45
|
+
}
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Load credentials from ~/.claude/credentials for the active profile.
|
|
50
|
+
* Result is cached for the lifetime of the MCP server process.
|
|
51
|
+
*/
|
|
52
|
+
function loadCredentials() {
|
|
53
|
+
if (cachedCredentials !== null)
|
|
54
|
+
return cachedCredentials;
|
|
55
|
+
if (!fs.existsSync(CREDENTIALS_FILE)) {
|
|
56
|
+
cachedCredentials = {};
|
|
57
|
+
return cachedCredentials;
|
|
58
|
+
}
|
|
59
|
+
// Warn if the file is world-readable (not chmod 600)
|
|
60
|
+
try {
|
|
61
|
+
const stat = fs.statSync(CREDENTIALS_FILE);
|
|
62
|
+
const mode = stat.mode & 0o777;
|
|
63
|
+
if (mode !== 0o600) {
|
|
64
|
+
process.stderr.write(`[credentials] ⚠️ Warning: ${CREDENTIALS_FILE} has permissions ${mode.toString(8)} — expected 600. Run: chmod 600 ${CREDENTIALS_FILE}\n`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// Non-fatal
|
|
69
|
+
}
|
|
70
|
+
const content = fs.readFileSync(CREDENTIALS_FILE, 'utf-8');
|
|
71
|
+
const ini = parseIni(content);
|
|
72
|
+
const profile = process.env.KIT_PROFILE ?? 'default';
|
|
73
|
+
cachedCredentials = ini[profile] ?? {};
|
|
74
|
+
if (Object.keys(cachedCredentials).length === 0 && profile !== 'default') {
|
|
75
|
+
process.stderr.write(`[credentials] ⚠️ Profile "${profile}" not found in ${CREDENTIALS_FILE}. Falling back to empty.\n`);
|
|
76
|
+
}
|
|
77
|
+
return cachedCredentials;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Resolve a credential key using the priority chain:
|
|
81
|
+
* process.env.KEY → ~/.claude/credentials [profile] → undefined
|
|
82
|
+
*/
|
|
83
|
+
export function getCredential(key) {
|
|
84
|
+
if (process.env[key] !== undefined)
|
|
85
|
+
return process.env[key];
|
|
86
|
+
return loadCredentials()[key];
|
|
87
|
+
}
|
package/dist/kit-server.js
CHANGED
|
@@ -3223,8 +3223,8 @@ var require_utils = __commonJS({
|
|
|
3223
3223
|
}
|
|
3224
3224
|
return ind;
|
|
3225
3225
|
}
|
|
3226
|
-
function removeDotSegments(
|
|
3227
|
-
let input =
|
|
3226
|
+
function removeDotSegments(path5) {
|
|
3227
|
+
let input = path5;
|
|
3228
3228
|
const output = [];
|
|
3229
3229
|
let nextSlash = -1;
|
|
3230
3230
|
let len = 0;
|
|
@@ -3423,8 +3423,8 @@ var require_schemes = __commonJS({
|
|
|
3423
3423
|
wsComponent.secure = void 0;
|
|
3424
3424
|
}
|
|
3425
3425
|
if (wsComponent.resourceName) {
|
|
3426
|
-
const [
|
|
3427
|
-
wsComponent.path =
|
|
3426
|
+
const [path5, query] = wsComponent.resourceName.split("?");
|
|
3427
|
+
wsComponent.path = path5 && path5 !== "/" ? path5 : void 0;
|
|
3428
3428
|
wsComponent.query = query;
|
|
3429
3429
|
wsComponent.resourceName = void 0;
|
|
3430
3430
|
}
|
|
@@ -6786,12 +6786,12 @@ var require_dist = __commonJS({
|
|
|
6786
6786
|
throw new Error(`Unknown format "${name}"`);
|
|
6787
6787
|
return f;
|
|
6788
6788
|
};
|
|
6789
|
-
function addFormats(ajv, list,
|
|
6789
|
+
function addFormats(ajv, list, fs4, exportName) {
|
|
6790
6790
|
var _a;
|
|
6791
6791
|
var _b;
|
|
6792
6792
|
(_a = (_b = ajv.opts.code).formats) !== null && _a !== void 0 ? _a : _b.formats = (0, codegen_1._)`require("ajv-formats/dist/formats").${exportName}`;
|
|
6793
6793
|
for (const f of list)
|
|
6794
|
-
ajv.addFormat(f,
|
|
6794
|
+
ajv.addFormat(f, fs4[f]);
|
|
6795
6795
|
}
|
|
6796
6796
|
module.exports = exports = formatsPlugin;
|
|
6797
6797
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
@@ -7277,8 +7277,8 @@ function getErrorMap() {
|
|
|
7277
7277
|
|
|
7278
7278
|
// node_modules/zod/v3/helpers/parseUtil.js
|
|
7279
7279
|
var makeIssue = (params) => {
|
|
7280
|
-
const { data, path:
|
|
7281
|
-
const fullPath = [...
|
|
7280
|
+
const { data, path: path5, errorMaps, issueData } = params;
|
|
7281
|
+
const fullPath = [...path5, ...issueData.path || []];
|
|
7282
7282
|
const fullIssue = {
|
|
7283
7283
|
...issueData,
|
|
7284
7284
|
path: fullPath
|
|
@@ -7394,11 +7394,11 @@ var errorUtil;
|
|
|
7394
7394
|
|
|
7395
7395
|
// node_modules/zod/v3/types.js
|
|
7396
7396
|
var ParseInputLazyPath = class {
|
|
7397
|
-
constructor(parent, value,
|
|
7397
|
+
constructor(parent, value, path5, key) {
|
|
7398
7398
|
this._cachedPath = [];
|
|
7399
7399
|
this.parent = parent;
|
|
7400
7400
|
this.data = value;
|
|
7401
|
-
this._path =
|
|
7401
|
+
this._path = path5;
|
|
7402
7402
|
this._key = key;
|
|
7403
7403
|
}
|
|
7404
7404
|
get path() {
|
|
@@ -11036,10 +11036,10 @@ function assignProp(target, prop, value) {
|
|
|
11036
11036
|
configurable: true
|
|
11037
11037
|
});
|
|
11038
11038
|
}
|
|
11039
|
-
function getElementAtPath(obj,
|
|
11040
|
-
if (!
|
|
11039
|
+
function getElementAtPath(obj, path5) {
|
|
11040
|
+
if (!path5)
|
|
11041
11041
|
return obj;
|
|
11042
|
-
return
|
|
11042
|
+
return path5.reduce((acc, key) => acc?.[key], obj);
|
|
11043
11043
|
}
|
|
11044
11044
|
function promiseAllObject(promisesObj) {
|
|
11045
11045
|
const keys = Object.keys(promisesObj);
|
|
@@ -11359,11 +11359,11 @@ function aborted(x, startIndex = 0) {
|
|
|
11359
11359
|
}
|
|
11360
11360
|
return false;
|
|
11361
11361
|
}
|
|
11362
|
-
function prefixIssues(
|
|
11362
|
+
function prefixIssues(path5, issues) {
|
|
11363
11363
|
return issues.map((iss) => {
|
|
11364
11364
|
var _a;
|
|
11365
11365
|
(_a = iss).path ?? (_a.path = []);
|
|
11366
|
-
iss.path.unshift(
|
|
11366
|
+
iss.path.unshift(path5);
|
|
11367
11367
|
return iss;
|
|
11368
11368
|
});
|
|
11369
11369
|
}
|
|
@@ -20998,6 +20998,47 @@ 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
21000
|
|
|
21001
|
+
// src/tools/config.ts
|
|
21002
|
+
var FORBIDDEN_FILES = [
|
|
21003
|
+
".env",
|
|
21004
|
+
".bashrc",
|
|
21005
|
+
".zshrc",
|
|
21006
|
+
".profile",
|
|
21007
|
+
".bash_profile",
|
|
21008
|
+
".bash_history",
|
|
21009
|
+
".zsh_history",
|
|
21010
|
+
"config.mjs",
|
|
21011
|
+
".npmrc",
|
|
21012
|
+
".yarnrc",
|
|
21013
|
+
".netrc",
|
|
21014
|
+
".gitconfig",
|
|
21015
|
+
"credentials"
|
|
21016
|
+
// ~/.claude/credentials file
|
|
21017
|
+
];
|
|
21018
|
+
var FORBIDDEN_PATTERNS = [
|
|
21019
|
+
/^\.env$/i,
|
|
21020
|
+
// .env, .Env — case-insensitive exact
|
|
21021
|
+
/^\.env[^a-z]/i,
|
|
21022
|
+
// .env_bak, .env-prod, .env.local
|
|
21023
|
+
/^id_rsa/i,
|
|
21024
|
+
// SSH private keys
|
|
21025
|
+
/^id_ed25519/i,
|
|
21026
|
+
/^id_ecdsa/i,
|
|
21027
|
+
/\.pem$/i,
|
|
21028
|
+
// Certificates
|
|
21029
|
+
/credentials\.json$/i,
|
|
21030
|
+
/secrets\.json$/i,
|
|
21031
|
+
/secret\.json$/i
|
|
21032
|
+
];
|
|
21033
|
+
var FORBIDDEN_DIRS = [
|
|
21034
|
+
".git",
|
|
21035
|
+
".ssh",
|
|
21036
|
+
".aws",
|
|
21037
|
+
".kube",
|
|
21038
|
+
".gnupg",
|
|
21039
|
+
".docker"
|
|
21040
|
+
];
|
|
21041
|
+
|
|
21001
21042
|
// src/tools/core.ts
|
|
21002
21043
|
function registerCoreTools(server2) {
|
|
21003
21044
|
server2.tool(
|
|
@@ -21043,27 +21084,24 @@ import * as path2 from "path";
|
|
|
21043
21084
|
function sanitize(input) {
|
|
21044
21085
|
return String(input).replace(/[;&|`$<>\\]/g, "").trim().slice(0, 500);
|
|
21045
21086
|
}
|
|
21046
|
-
var FORBIDDEN_FILES = [
|
|
21047
|
-
".env",
|
|
21048
|
-
".bashrc",
|
|
21049
|
-
".zshrc",
|
|
21050
|
-
".profile",
|
|
21051
|
-
".bash_profile",
|
|
21052
|
-
".bash_history",
|
|
21053
|
-
".zsh_history",
|
|
21054
|
-
"config.mjs"
|
|
21055
|
-
// Some projects use this for sensitive config
|
|
21056
|
-
];
|
|
21057
21087
|
function validatePath(filePath, baseDir = process.cwd()) {
|
|
21058
21088
|
const resolved = path2.resolve(baseDir, filePath);
|
|
21059
21089
|
const root = path2.resolve(baseDir);
|
|
21060
21090
|
if (resolved !== root && !resolved.startsWith(root + path2.sep)) {
|
|
21061
21091
|
throw new Error(`Path traversal detected: ${filePath}`);
|
|
21062
21092
|
}
|
|
21093
|
+
const segments = resolved.split(path2.sep);
|
|
21094
|
+
for (const segment of segments) {
|
|
21095
|
+
if (FORBIDDEN_DIRS.some((d) => segment.toLowerCase() === d.toLowerCase())) {
|
|
21096
|
+
throw new Error(`Access to sensitive directory is FORBIDDEN: ${segment}`);
|
|
21097
|
+
}
|
|
21098
|
+
}
|
|
21063
21099
|
const fileName = path2.basename(resolved);
|
|
21064
|
-
|
|
21065
|
-
(
|
|
21066
|
-
)
|
|
21100
|
+
const isExactForbidden = FORBIDDEN_FILES.some(
|
|
21101
|
+
(f) => fileName.toLowerCase() === f.toLowerCase()
|
|
21102
|
+
);
|
|
21103
|
+
const isPatternForbidden = FORBIDDEN_PATTERNS.some((re) => re.test(fileName));
|
|
21104
|
+
if (isExactForbidden || isPatternForbidden) {
|
|
21067
21105
|
throw new Error(`Access to sensitive file is FORBIDDEN: ${fileName}`);
|
|
21068
21106
|
}
|
|
21069
21107
|
return resolved;
|
|
@@ -21083,6 +21121,25 @@ function commandExists(cmd) {
|
|
|
21083
21121
|
return false;
|
|
21084
21122
|
}
|
|
21085
21123
|
}
|
|
21124
|
+
function sanitizeOutput(text) {
|
|
21125
|
+
const SECRET_PATTERNS = [
|
|
21126
|
+
/(ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{36,}/g,
|
|
21127
|
+
// GitHub tokens
|
|
21128
|
+
/SK-[A-Za-z0-9]{40,}/g,
|
|
21129
|
+
// OpenAI keys
|
|
21130
|
+
/AKIA[0-9A-Z]{16}/g,
|
|
21131
|
+
// AWS access keys
|
|
21132
|
+
/xox[baprs]-[A-Za-z0-9-]{10,}/g,
|
|
21133
|
+
// Slack tokens
|
|
21134
|
+
/[Aa]tlassian[_\-]?[Aa][Pp][Ii][_\-]?[Tt]oken[^\s]*\s*[:=]\s*\S+/g
|
|
21135
|
+
// Atlassian token patterns
|
|
21136
|
+
];
|
|
21137
|
+
let sanitized = text;
|
|
21138
|
+
for (const pattern of SECRET_PATTERNS) {
|
|
21139
|
+
sanitized = sanitized.replace(pattern, "[REDACTED]");
|
|
21140
|
+
}
|
|
21141
|
+
return sanitized;
|
|
21142
|
+
}
|
|
21086
21143
|
|
|
21087
21144
|
// src/tools/agent.ts
|
|
21088
21145
|
var AGENT_TIMEOUT = parseInt(process.env.KIT_AGENT_TIMEOUT || "300000", 10);
|
|
@@ -21195,6 +21252,7 @@ function registerAgentTools(server2) {
|
|
|
21195
21252
|
child.on("error", reject);
|
|
21196
21253
|
});
|
|
21197
21254
|
const output = job.chunks.join("");
|
|
21255
|
+
const safeOutput = sanitizeOutput(output);
|
|
21198
21256
|
return {
|
|
21199
21257
|
content: [
|
|
21200
21258
|
{
|
|
@@ -21204,7 +21262,7 @@ function registerAgentTools(server2) {
|
|
|
21204
21262
|
agent: usedAgent,
|
|
21205
21263
|
status: fallbackReason ? "fallback" : "success",
|
|
21206
21264
|
log: logPath,
|
|
21207
|
-
output,
|
|
21265
|
+
output: safeOutput,
|
|
21208
21266
|
...fallbackReason && { fallback_reason: fallbackReason }
|
|
21209
21267
|
},
|
|
21210
21268
|
null,
|
|
@@ -21246,6 +21304,68 @@ function registerAgentTools(server2) {
|
|
|
21246
21304
|
|
|
21247
21305
|
// src/tools/integration.ts
|
|
21248
21306
|
import { writeFileSync as writeFileSync3 } from "fs";
|
|
21307
|
+
|
|
21308
|
+
// src/credentials.ts
|
|
21309
|
+
import * as fs3 from "fs";
|
|
21310
|
+
import * as os from "os";
|
|
21311
|
+
import * as path4 from "path";
|
|
21312
|
+
var CREDENTIALS_FILE = path4.join(os.homedir(), ".claude", "credentials");
|
|
21313
|
+
var cachedCredentials = null;
|
|
21314
|
+
function parseIni(content) {
|
|
21315
|
+
const result = {};
|
|
21316
|
+
let currentSection = "default";
|
|
21317
|
+
for (const raw of content.split("\n")) {
|
|
21318
|
+
const line = raw.trim();
|
|
21319
|
+
if (!line || line.startsWith("#") || line.startsWith(";")) continue;
|
|
21320
|
+
const sectionMatch = line.match(/^\[([^\]]+)\]$/);
|
|
21321
|
+
if (sectionMatch) {
|
|
21322
|
+
currentSection = sectionMatch[1].trim();
|
|
21323
|
+
continue;
|
|
21324
|
+
}
|
|
21325
|
+
const eqIdx = line.indexOf("=");
|
|
21326
|
+
if (eqIdx === -1) continue;
|
|
21327
|
+
const key = line.slice(0, eqIdx).trim();
|
|
21328
|
+
const value = line.slice(eqIdx + 1).trim();
|
|
21329
|
+
if (!result[currentSection]) result[currentSection] = {};
|
|
21330
|
+
result[currentSection][key] = value;
|
|
21331
|
+
}
|
|
21332
|
+
return result;
|
|
21333
|
+
}
|
|
21334
|
+
function loadCredentials() {
|
|
21335
|
+
if (cachedCredentials !== null) return cachedCredentials;
|
|
21336
|
+
if (!fs3.existsSync(CREDENTIALS_FILE)) {
|
|
21337
|
+
cachedCredentials = {};
|
|
21338
|
+
return cachedCredentials;
|
|
21339
|
+
}
|
|
21340
|
+
try {
|
|
21341
|
+
const stat = fs3.statSync(CREDENTIALS_FILE);
|
|
21342
|
+
const mode = stat.mode & 511;
|
|
21343
|
+
if (mode !== 384) {
|
|
21344
|
+
process.stderr.write(
|
|
21345
|
+
`[credentials] \u26A0\uFE0F Warning: ${CREDENTIALS_FILE} has permissions ${mode.toString(8)} \u2014 expected 600. Run: chmod 600 ${CREDENTIALS_FILE}
|
|
21346
|
+
`
|
|
21347
|
+
);
|
|
21348
|
+
}
|
|
21349
|
+
} catch {
|
|
21350
|
+
}
|
|
21351
|
+
const content = fs3.readFileSync(CREDENTIALS_FILE, "utf-8");
|
|
21352
|
+
const ini = parseIni(content);
|
|
21353
|
+
const profile = process.env.KIT_PROFILE ?? "default";
|
|
21354
|
+
cachedCredentials = ini[profile] ?? {};
|
|
21355
|
+
if (Object.keys(cachedCredentials).length === 0 && profile !== "default") {
|
|
21356
|
+
process.stderr.write(
|
|
21357
|
+
`[credentials] \u26A0\uFE0F Profile "${profile}" not found in ${CREDENTIALS_FILE}. Falling back to empty.
|
|
21358
|
+
`
|
|
21359
|
+
);
|
|
21360
|
+
}
|
|
21361
|
+
return cachedCredentials;
|
|
21362
|
+
}
|
|
21363
|
+
function getCredential(key) {
|
|
21364
|
+
if (process.env[key] !== void 0) return process.env[key];
|
|
21365
|
+
return loadCredentials()[key];
|
|
21366
|
+
}
|
|
21367
|
+
|
|
21368
|
+
// src/tools/integration.ts
|
|
21249
21369
|
var BitbucketPrSchema = external_exports.object({
|
|
21250
21370
|
id: external_exports.number(),
|
|
21251
21371
|
title: external_exports.string(),
|
|
@@ -21302,16 +21422,16 @@ function extractAdfText(adf) {
|
|
|
21302
21422
|
return textParts.join(" ") || "No description";
|
|
21303
21423
|
}
|
|
21304
21424
|
function buildJiraBasicAuth() {
|
|
21305
|
-
const email2 =
|
|
21306
|
-
const token =
|
|
21425
|
+
const email2 = getCredential("ATLASSIAN_USER_EMAIL");
|
|
21426
|
+
const token = getCredential("ATLASSIAN_API_TOKEN");
|
|
21307
21427
|
if (!email2 || !token) {
|
|
21308
21428
|
throw new Error("Missing ATLASSIAN_USER_EMAIL or ATLASSIAN_API_TOKEN");
|
|
21309
21429
|
}
|
|
21310
21430
|
return "Basic " + Buffer.from(`${email2}:${token}`).toString("base64");
|
|
21311
21431
|
}
|
|
21312
21432
|
function buildBitbucketBasicAuth() {
|
|
21313
|
-
const email2 =
|
|
21314
|
-
const token =
|
|
21433
|
+
const email2 = getCredential("BITBUCKET_USER_EMAIL");
|
|
21434
|
+
const token = getCredential("BITBUCKET_API_TOKEN");
|
|
21315
21435
|
if (!email2 || !token) {
|
|
21316
21436
|
throw new Error("Missing BITBUCKET_USER_EMAIL or BITBUCKET_API_TOKEN");
|
|
21317
21437
|
}
|
|
@@ -21373,14 +21493,14 @@ function registerIntegrationTools(server2) {
|
|
|
21373
21493
|
},
|
|
21374
21494
|
async ({ input, workspace, repoSlug, includeDiff }) => {
|
|
21375
21495
|
try {
|
|
21376
|
-
const bbEmail =
|
|
21377
|
-
const bbToken =
|
|
21496
|
+
const bbEmail = getCredential("BITBUCKET_USER_EMAIL");
|
|
21497
|
+
const bbToken = getCredential("BITBUCKET_API_TOKEN");
|
|
21378
21498
|
if (!bbEmail || !bbToken) {
|
|
21379
21499
|
return {
|
|
21380
21500
|
content: [
|
|
21381
21501
|
{
|
|
21382
21502
|
type: "text",
|
|
21383
|
-
text: `\u274C Missing BITBUCKET_USER_EMAIL or BITBUCKET_API_TOKEN.
|
|
21503
|
+
text: `\u274C Missing BITBUCKET_USER_EMAIL or BITBUCKET_API_TOKEN. Set them in ~/.claude/agent-kit or as environment variables.`
|
|
21384
21504
|
}
|
|
21385
21505
|
]
|
|
21386
21506
|
};
|
|
@@ -21395,7 +21515,7 @@ function registerIntegrationTools(server2) {
|
|
|
21395
21515
|
prId = parseInt(urlMatch[3], 10);
|
|
21396
21516
|
} else if (input.match(/^\d+$/)) {
|
|
21397
21517
|
prId = parseInt(input, 10);
|
|
21398
|
-
ws = workspace ||
|
|
21518
|
+
ws = workspace || getCredential("BITBUCKET_DEFAULT_WORKSPACE");
|
|
21399
21519
|
repo = repoSlug;
|
|
21400
21520
|
}
|
|
21401
21521
|
if (!ws) {
|
|
@@ -21478,9 +21598,9 @@ ${diff}
|
|
|
21478
21598
|
},
|
|
21479
21599
|
async ({ ticketId }) => {
|
|
21480
21600
|
try {
|
|
21481
|
-
const cloudId =
|
|
21482
|
-
const userEmail =
|
|
21483
|
-
const apiToken =
|
|
21601
|
+
const cloudId = getCredential("ATLASSIAN_CLOUD_ID");
|
|
21602
|
+
const userEmail = getCredential("ATLASSIAN_USER_EMAIL");
|
|
21603
|
+
const apiToken = getCredential("ATLASSIAN_API_TOKEN");
|
|
21484
21604
|
if (!cloudId || !userEmail || !apiToken) {
|
|
21485
21605
|
const missing = [
|
|
21486
21606
|
!cloudId && "ATLASSIAN_CLOUD_ID",
|
package/dist/tools/agent.js
CHANGED
|
@@ -8,7 +8,7 @@ import * as fs from 'fs';
|
|
|
8
8
|
import * as path from 'path';
|
|
9
9
|
import { z } from 'zod';
|
|
10
10
|
import { getWorkspaceRoot } from '../utils.js';
|
|
11
|
-
import { commandExists, validatePath } from './security.js';
|
|
11
|
+
import { commandExists, validatePath, sanitizeOutput } 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();
|
|
@@ -144,6 +144,7 @@ export function registerAgentTools(server) {
|
|
|
144
144
|
child.on('error', reject);
|
|
145
145
|
});
|
|
146
146
|
const output = job.chunks.join('');
|
|
147
|
+
const safeOutput = sanitizeOutput(output);
|
|
147
148
|
return {
|
|
148
149
|
content: [
|
|
149
150
|
{
|
|
@@ -152,7 +153,7 @@ export function registerAgentTools(server) {
|
|
|
152
153
|
agent: usedAgent,
|
|
153
154
|
status: fallbackReason ? 'fallback' : 'success',
|
|
154
155
|
log: logPath,
|
|
155
|
-
output,
|
|
156
|
+
output: safeOutput,
|
|
156
157
|
...(fallbackReason && { fallback_reason: fallbackReason }),
|
|
157
158
|
}, null, 2),
|
|
158
159
|
},
|
package/dist/tools/config.d.ts
CHANGED
|
@@ -23,3 +23,15 @@ export declare function getFileExtensions(projectDir: string): string[];
|
|
|
23
23
|
* Load project settings from .gemini/settings.json
|
|
24
24
|
*/
|
|
25
25
|
export declare function loadProjectSettings(projectDir: string): ProjectSettings;
|
|
26
|
+
/**
|
|
27
|
+
* Sensitive files that agents are FORBIDDEN from reading/writing.
|
|
28
|
+
*/
|
|
29
|
+
export declare const FORBIDDEN_FILES: string[];
|
|
30
|
+
/**
|
|
31
|
+
* Regex patterns (case-insensitive) matched against path.basename.
|
|
32
|
+
*/
|
|
33
|
+
export declare const FORBIDDEN_PATTERNS: RegExp[];
|
|
34
|
+
/**
|
|
35
|
+
* Directory name segments that are always blocked.
|
|
36
|
+
*/
|
|
37
|
+
export declare const FORBIDDEN_DIRS: string[];
|
package/dist/tools/config.js
CHANGED
|
@@ -58,3 +58,46 @@ export function loadProjectSettings(projectDir) {
|
|
|
58
58
|
}
|
|
59
59
|
return {};
|
|
60
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* Sensitive files that agents are FORBIDDEN from reading/writing.
|
|
63
|
+
*/
|
|
64
|
+
export const FORBIDDEN_FILES = [
|
|
65
|
+
'.env',
|
|
66
|
+
'.bashrc',
|
|
67
|
+
'.zshrc',
|
|
68
|
+
'.profile',
|
|
69
|
+
'.bash_profile',
|
|
70
|
+
'.bash_history',
|
|
71
|
+
'.zsh_history',
|
|
72
|
+
'config.mjs',
|
|
73
|
+
'.npmrc',
|
|
74
|
+
'.yarnrc',
|
|
75
|
+
'.netrc',
|
|
76
|
+
'.gitconfig',
|
|
77
|
+
'credentials', // ~/.claude/credentials file
|
|
78
|
+
];
|
|
79
|
+
/**
|
|
80
|
+
* Regex patterns (case-insensitive) matched against path.basename.
|
|
81
|
+
*/
|
|
82
|
+
export const FORBIDDEN_PATTERNS = [
|
|
83
|
+
/^\.env$/i, // .env, .Env — case-insensitive exact
|
|
84
|
+
/^\.env[^a-z]/i, // .env_bak, .env-prod, .env.local
|
|
85
|
+
/^id_rsa/i, // SSH private keys
|
|
86
|
+
/^id_ed25519/i,
|
|
87
|
+
/^id_ecdsa/i,
|
|
88
|
+
/\.pem$/i, // Certificates
|
|
89
|
+
/credentials\.json$/i,
|
|
90
|
+
/secrets\.json$/i,
|
|
91
|
+
/secret\.json$/i,
|
|
92
|
+
];
|
|
93
|
+
/**
|
|
94
|
+
* Directory name segments that are always blocked.
|
|
95
|
+
*/
|
|
96
|
+
export const FORBIDDEN_DIRS = [
|
|
97
|
+
'.git',
|
|
98
|
+
'.ssh',
|
|
99
|
+
'.aws',
|
|
100
|
+
'.kube',
|
|
101
|
+
'.gnupg',
|
|
102
|
+
'.docker',
|
|
103
|
+
];
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { writeFileSync } from 'fs';
|
|
6
6
|
import { z } from 'zod';
|
|
7
|
+
import { getCredential } from '../credentials.js';
|
|
7
8
|
import { sanitize } from './security.js';
|
|
8
9
|
// Zod schema for Bitbucket PR REST API response
|
|
9
10
|
const BitbucketPrSchema = z.object({
|
|
@@ -77,16 +78,16 @@ function extractAdfText(adf) {
|
|
|
77
78
|
return textParts.join(' ') || 'No description';
|
|
78
79
|
}
|
|
79
80
|
function buildJiraBasicAuth() {
|
|
80
|
-
const email =
|
|
81
|
-
const token =
|
|
81
|
+
const email = getCredential('ATLASSIAN_USER_EMAIL');
|
|
82
|
+
const token = getCredential('ATLASSIAN_API_TOKEN');
|
|
82
83
|
if (!email || !token) {
|
|
83
84
|
throw new Error('Missing ATLASSIAN_USER_EMAIL or ATLASSIAN_API_TOKEN');
|
|
84
85
|
}
|
|
85
86
|
return 'Basic ' + Buffer.from(`${email}:${token}`).toString('base64');
|
|
86
87
|
}
|
|
87
88
|
function buildBitbucketBasicAuth() {
|
|
88
|
-
const email =
|
|
89
|
-
const token =
|
|
89
|
+
const email = getCredential('BITBUCKET_USER_EMAIL');
|
|
90
|
+
const token = getCredential('BITBUCKET_API_TOKEN');
|
|
90
91
|
if (!email || !token) {
|
|
91
92
|
throw new Error('Missing BITBUCKET_USER_EMAIL or BITBUCKET_API_TOKEN');
|
|
92
93
|
}
|
|
@@ -150,14 +151,14 @@ export function registerIntegrationTools(server) {
|
|
|
150
151
|
.describe('Include unified diff in response'),
|
|
151
152
|
}, async ({ input, workspace, repoSlug, includeDiff }) => {
|
|
152
153
|
try {
|
|
153
|
-
const bbEmail =
|
|
154
|
-
const bbToken =
|
|
154
|
+
const bbEmail = getCredential('BITBUCKET_USER_EMAIL');
|
|
155
|
+
const bbToken = getCredential('BITBUCKET_API_TOKEN');
|
|
155
156
|
if (!bbEmail || !bbToken) {
|
|
156
157
|
return {
|
|
157
158
|
content: [
|
|
158
159
|
{
|
|
159
160
|
type: 'text',
|
|
160
|
-
text: `❌ Missing BITBUCKET_USER_EMAIL or BITBUCKET_API_TOKEN.
|
|
161
|
+
text: `❌ Missing BITBUCKET_USER_EMAIL or BITBUCKET_API_TOKEN. Set them in ~/.claude/agent-kit or as environment variables.`,
|
|
161
162
|
},
|
|
162
163
|
],
|
|
163
164
|
};
|
|
@@ -174,7 +175,7 @@ export function registerIntegrationTools(server) {
|
|
|
174
175
|
}
|
|
175
176
|
else if (input.match(/^\d+$/)) {
|
|
176
177
|
prId = parseInt(input, 10);
|
|
177
|
-
ws = workspace ||
|
|
178
|
+
ws = workspace || getCredential('BITBUCKET_DEFAULT_WORKSPACE');
|
|
178
179
|
repo = repoSlug;
|
|
179
180
|
}
|
|
180
181
|
if (!ws) {
|
|
@@ -242,9 +243,9 @@ ${pr.description || 'No description'}`;
|
|
|
242
243
|
ticketId: z.string().describe('Jira ticket ID (e.g., PROJ-123)'),
|
|
243
244
|
}, async ({ ticketId }) => {
|
|
244
245
|
try {
|
|
245
|
-
const cloudId =
|
|
246
|
-
const userEmail =
|
|
247
|
-
const apiToken =
|
|
246
|
+
const cloudId = getCredential('ATLASSIAN_CLOUD_ID');
|
|
247
|
+
const userEmail = getCredential('ATLASSIAN_USER_EMAIL');
|
|
248
|
+
const apiToken = getCredential('ATLASSIAN_API_TOKEN');
|
|
248
249
|
if (!cloudId || !userEmail || !apiToken) {
|
|
249
250
|
const missing = [
|
|
250
251
|
!cloudId && 'ATLASSIAN_CLOUD_ID',
|
package/dist/tools/security.d.ts
CHANGED
|
@@ -38,3 +38,7 @@ export declare function commandExists(cmd: string): boolean;
|
|
|
38
38
|
* Uses queue-based approach to prevent stack overflow on deep directories
|
|
39
39
|
*/
|
|
40
40
|
export declare function findFilesAsync(dir: string, extensions: string[], maxFiles: number, excludeDirs?: string[]): Promise<string[]>;
|
|
41
|
+
/**
|
|
42
|
+
* Sanitize output text by redacting sensitive tokens (API keys, etc.)
|
|
43
|
+
*/
|
|
44
|
+
export declare function sanitizeOutput(text: string): string;
|
package/dist/tools/security.js
CHANGED
|
@@ -6,6 +6,7 @@ import { execFileSync } from 'child_process';
|
|
|
6
6
|
import * as fs from 'fs';
|
|
7
7
|
import * as path from 'path';
|
|
8
8
|
import { getWorkspaceRoot } from '../utils.js';
|
|
9
|
+
import { FORBIDDEN_FILES, FORBIDDEN_PATTERNS, FORBIDDEN_DIRS } from './config.js';
|
|
9
10
|
/**
|
|
10
11
|
* Sanitize string for safe use with execFileSync
|
|
11
12
|
* Only removes dangerous shell operators - safe chars like !?#* are allowed
|
|
@@ -24,20 +25,6 @@ export function sanitize(input) {
|
|
|
24
25
|
.trim()
|
|
25
26
|
.slice(0, 500); // Limit length
|
|
26
27
|
}
|
|
27
|
-
/**
|
|
28
|
-
* Sensitive files that agents are FORBIDDEN from reading/writing.
|
|
29
|
-
* Includes environment variables, shell configs, and system profiles.
|
|
30
|
-
*/
|
|
31
|
-
const FORBIDDEN_FILES = [
|
|
32
|
-
'.env',
|
|
33
|
-
'.bashrc',
|
|
34
|
-
'.zshrc',
|
|
35
|
-
'.profile',
|
|
36
|
-
'.bash_profile',
|
|
37
|
-
'.bash_history',
|
|
38
|
-
'.zsh_history',
|
|
39
|
-
'config.mjs', // Some projects use this for sensitive config
|
|
40
|
-
];
|
|
41
28
|
/**
|
|
42
29
|
* Validate file path to prevent path traversal attacks and access to forbidden files.
|
|
43
30
|
* Uses stricter path.sep check to prevent prefix matching flaws.
|
|
@@ -49,9 +36,18 @@ export function validatePath(filePath, baseDir = process.cwd()) {
|
|
|
49
36
|
if (resolved !== root && !resolved.startsWith(root + path.sep)) {
|
|
50
37
|
throw new Error(`Path traversal detected: ${filePath}`);
|
|
51
38
|
}
|
|
52
|
-
// 2.
|
|
39
|
+
// 2. Directory segment check
|
|
40
|
+
const segments = resolved.split(path.sep);
|
|
41
|
+
for (const segment of segments) {
|
|
42
|
+
if (FORBIDDEN_DIRS.some((d) => segment.toLowerCase() === d.toLowerCase())) {
|
|
43
|
+
throw new Error(`Access to sensitive directory is FORBIDDEN: ${segment}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// 3. Forbidden files check (two-stage)
|
|
53
47
|
const fileName = path.basename(resolved);
|
|
54
|
-
|
|
48
|
+
const isExactForbidden = FORBIDDEN_FILES.some((f) => fileName.toLowerCase() === f.toLowerCase());
|
|
49
|
+
const isPatternForbidden = FORBIDDEN_PATTERNS.some((re) => re.test(fileName));
|
|
50
|
+
if (isExactForbidden || isPatternForbidden) {
|
|
55
51
|
throw new Error(`Access to sensitive file is FORBIDDEN: ${fileName}`);
|
|
56
52
|
}
|
|
57
53
|
return resolved;
|
|
@@ -147,3 +143,20 @@ export async function findFilesAsync(dir, extensions, maxFiles, excludeDirs = ['
|
|
|
147
143
|
}
|
|
148
144
|
return results;
|
|
149
145
|
}
|
|
146
|
+
/**
|
|
147
|
+
* Sanitize output text by redacting sensitive tokens (API keys, etc.)
|
|
148
|
+
*/
|
|
149
|
+
export function sanitizeOutput(text) {
|
|
150
|
+
const SECRET_PATTERNS = [
|
|
151
|
+
/(ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{36,}/g, // GitHub tokens
|
|
152
|
+
/SK-[A-Za-z0-9]{40,}/g, // OpenAI keys
|
|
153
|
+
/AKIA[0-9A-Z]{16}/g, // AWS access keys
|
|
154
|
+
/xox[baprs]-[A-Za-z0-9-]{10,}/g, // Slack tokens
|
|
155
|
+
/[Aa]tlassian[_\-]?[Aa][Pp][Ii][_\-]?[Tt]oken[^\s]*\s*[:=]\s*\S+/g, // Atlassian token patterns
|
|
156
|
+
];
|
|
157
|
+
let sanitized = text;
|
|
158
|
+
for (const pattern of SECRET_PATTERNS) {
|
|
159
|
+
sanitized = sanitized.replace(pattern, '[REDACTED]');
|
|
160
|
+
}
|
|
161
|
+
return sanitized;
|
|
162
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hanhnd/agent-kit",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.10",
|
|
4
4
|
"description": "Super Engineer - Team of AI Agents for software development (Claude Code Plugin)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/kit-server.js",
|
|
@@ -21,7 +21,8 @@
|
|
|
21
21
|
"prepublishOnly": "npm run build",
|
|
22
22
|
"build": "tsc && npm run bundle",
|
|
23
23
|
"bundle": "esbuild src/kit-server.ts --bundle --platform=node --format=esm --outfile=dist/kit-server.js --external:node:* --banner:js=\"import { createRequire } from 'module'; const require = createRequire(import.meta.url);\"",
|
|
24
|
-
"dev": "tsc --watch"
|
|
24
|
+
"dev": "tsc --watch",
|
|
25
|
+
"test": "node --test tests/*.test.js"
|
|
25
26
|
},
|
|
26
27
|
"dependencies": {
|
|
27
28
|
"@modelcontextprotocol/sdk": "^1.13.1",
|