@hanhnd/agent-kit 1.0.9 → 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 CHANGED
@@ -35,17 +35,24 @@ claude plugin install ak
35
35
 
36
36
  The plugin fetches from GitHub, registers the MCP server automatically, and makes all commands available immediately.
37
37
 
38
- **Add credentials** to your shell profile (`~/.zshrc` or `~/.bashrc`):
38
+ **Add credentials** to `~/.claude/credentials` (like `~/.aws/credentials`):
39
39
 
40
40
  ```bash
41
- export ATLASSIAN_CLOUD_ID="your-atlassian-cloud-id"
42
- export ATLASSIAN_USER_EMAIL="you@yourcompany.com"
43
- export ATLASSIAN_API_TOKEN="your-atlassian-api-token"
44
- export BITBUCKET_USER_EMAIL="you@yourcompany.com"
45
- export BITBUCKET_API_TOKEN="your-atlassian-api-token"
46
- export BITBUCKET_DEFAULT_WORKSPACE="your-default-workspace-slug"
41
+ touch ~/.claude/credentials && chmod 600 ~/.claude/credentials
47
42
  ```
48
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
+
49
56
  To update: `claude plugin update ak`
50
57
  To uninstall: `claude plugin uninstall ak`
51
58
 
@@ -73,15 +80,20 @@ claude plugin install ak
73
80
 
74
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.
75
82
 
76
- **3. Add credentials** to your shell profile (`~/.zshrc` or `~/.bashrc`):
83
+ **3. Add credentials** to `~/.claude/credentials`:
77
84
 
78
85
  ```bash
79
- export ATLASSIAN_CLOUD_ID="your-atlassian-cloud-id"
80
- export ATLASSIAN_USER_EMAIL="you@yourcompany.com"
81
- export ATLASSIAN_API_TOKEN="your-atlassian-api-token"
82
- export BITBUCKET_USER_EMAIL="you@yourcompany.com"
83
- export BITBUCKET_API_TOKEN="your-atlassian-api-token"
84
- export BITBUCKET_DEFAULT_WORKSPACE="your-default-workspace-slug"
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
85
97
  ```
86
98
 
87
99
  **4. Verify**
@@ -125,11 +137,13 @@ The MCP server source is in `src/`. Agent personas are in `agents/`. Skill modul
125
137
 
126
138
  ## Integrations
127
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
+
128
142
  ### Jira (via Atlassian REST API)
129
143
 
130
144
  Used by `/ak:ticket` and `/ak:review-pr`.
131
145
 
132
- | Variable | Description |
146
+ | Key | Description |
133
147
  | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
134
148
  | `ATLASSIAN_CLOUD_ID` | Your Atlassian Cloud ID — find it at [admin.atlassian.com](https://admin.atlassian.com) under your site settings |
135
149
  | `ATLASSIAN_USER_EMAIL` | Your Atlassian account email |
@@ -139,7 +153,7 @@ Used by `/ak:ticket` and `/ak:review-pr`.
139
153
 
140
154
  Used by `/ak:review-pr`.
141
155
 
142
- | Variable | Description |
156
+ | Key | Description |
143
157
  | ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
144
158
  | `BITBUCKET_USER_EMAIL` | Your Atlassian account email (same as Jira) |
145
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
+ }
@@ -3223,8 +3223,8 @@ var require_utils = __commonJS({
3223
3223
  }
3224
3224
  return ind;
3225
3225
  }
3226
- function removeDotSegments(path4) {
3227
- let input = path4;
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 [path4, query] = wsComponent.resourceName.split("?");
3427
- wsComponent.path = path4 && path4 !== "/" ? path4 : void 0;
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, fs3, exportName) {
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, fs3[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: path4, errorMaps, issueData } = params;
7281
- const fullPath = [...path4, ...issueData.path || []];
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, path4, key) {
7397
+ constructor(parent, value, path5, key) {
7398
7398
  this._cachedPath = [];
7399
7399
  this.parent = parent;
7400
7400
  this.data = value;
7401
- this._path = path4;
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, path4) {
11040
- if (!path4)
11039
+ function getElementAtPath(obj, path5) {
11040
+ if (!path5)
11041
11041
  return obj;
11042
- return path4.reduce((acc, key) => acc?.[key], obj);
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(path4, issues) {
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(path4);
11366
+ iss.path.unshift(path5);
11367
11367
  return iss;
11368
11368
  });
11369
11369
  }
@@ -21011,7 +21011,9 @@ var FORBIDDEN_FILES = [
21011
21011
  ".npmrc",
21012
21012
  ".yarnrc",
21013
21013
  ".netrc",
21014
- ".gitconfig"
21014
+ ".gitconfig",
21015
+ "credentials"
21016
+ // ~/.claude/credentials file
21015
21017
  ];
21016
21018
  var FORBIDDEN_PATTERNS = [
21017
21019
  /^\.env$/i,
@@ -21036,23 +21038,6 @@ var FORBIDDEN_DIRS = [
21036
21038
  ".gnupg",
21037
21039
  ".docker"
21038
21040
  ];
21039
- var SAFE_ENV_VARS = [
21040
- "PATH",
21041
- "HOME",
21042
- "USER",
21043
- "SHELL",
21044
- "TERM",
21045
- "LANG",
21046
- "LC_ALL",
21047
- "LC_CTYPE",
21048
- "TMPDIR",
21049
- "TMP",
21050
- "TEMP",
21051
- "NODE_ENV",
21052
- "GEMINI_WORKSPACE",
21053
- "KIT_AGENT_TIMEOUT",
21054
- "GEMINI_KIT_GIT_TIMEOUT"
21055
- ];
21056
21041
 
21057
21042
  // src/tools/core.ts
21058
21043
  function registerCoreTools(server2) {
@@ -21234,16 +21219,9 @@ function registerAgentTools(server2) {
21234
21219
  const slug = taskSlug(task);
21235
21220
  const { logPath, logStream } = initAgentLog(workspaceRoot, slug, usedAgent);
21236
21221
  const jobId = randomUUID();
21237
- const sanitizedEnv = {};
21238
- for (const key of SAFE_ENV_VARS) {
21239
- if (process.env[key] !== void 0) {
21240
- sanitizedEnv[key] = process.env[key];
21241
- }
21242
- }
21243
- sanitizedEnv.GEMINI_WORKSPACE = workspaceRoot;
21244
21222
  const child = spawn(usedAgent, agentArgs, {
21245
21223
  cwd: workspaceRoot,
21246
- env: sanitizedEnv
21224
+ env: { ...process.env, GEMINI_WORKSPACE: workspaceRoot }
21247
21225
  });
21248
21226
  const job = {
21249
21227
  id: jobId,
@@ -21326,6 +21304,68 @@ function registerAgentTools(server2) {
21326
21304
 
21327
21305
  // src/tools/integration.ts
21328
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
21329
21369
  var BitbucketPrSchema = external_exports.object({
21330
21370
  id: external_exports.number(),
21331
21371
  title: external_exports.string(),
@@ -21382,16 +21422,16 @@ function extractAdfText(adf) {
21382
21422
  return textParts.join(" ") || "No description";
21383
21423
  }
21384
21424
  function buildJiraBasicAuth() {
21385
- const email2 = process.env.ATLASSIAN_USER_EMAIL;
21386
- const token = process.env.ATLASSIAN_API_TOKEN;
21425
+ const email2 = getCredential("ATLASSIAN_USER_EMAIL");
21426
+ const token = getCredential("ATLASSIAN_API_TOKEN");
21387
21427
  if (!email2 || !token) {
21388
21428
  throw new Error("Missing ATLASSIAN_USER_EMAIL or ATLASSIAN_API_TOKEN");
21389
21429
  }
21390
21430
  return "Basic " + Buffer.from(`${email2}:${token}`).toString("base64");
21391
21431
  }
21392
21432
  function buildBitbucketBasicAuth() {
21393
- const email2 = process.env.BITBUCKET_USER_EMAIL;
21394
- const token = process.env.BITBUCKET_API_TOKEN;
21433
+ const email2 = getCredential("BITBUCKET_USER_EMAIL");
21434
+ const token = getCredential("BITBUCKET_API_TOKEN");
21395
21435
  if (!email2 || !token) {
21396
21436
  throw new Error("Missing BITBUCKET_USER_EMAIL or BITBUCKET_API_TOKEN");
21397
21437
  }
@@ -21453,14 +21493,14 @@ function registerIntegrationTools(server2) {
21453
21493
  },
21454
21494
  async ({ input, workspace, repoSlug, includeDiff }) => {
21455
21495
  try {
21456
- const bbEmail = process.env.BITBUCKET_USER_EMAIL;
21457
- const bbToken = process.env.BITBUCKET_API_TOKEN;
21496
+ const bbEmail = getCredential("BITBUCKET_USER_EMAIL");
21497
+ const bbToken = getCredential("BITBUCKET_API_TOKEN");
21458
21498
  if (!bbEmail || !bbToken) {
21459
21499
  return {
21460
21500
  content: [
21461
21501
  {
21462
21502
  type: "text",
21463
- text: `\u274C Missing BITBUCKET_USER_EMAIL or BITBUCKET_API_TOKEN. Create an API token at id.atlassian.com/manage-profile/security/api-tokens.`
21503
+ text: `\u274C Missing BITBUCKET_USER_EMAIL or BITBUCKET_API_TOKEN. Set them in ~/.claude/agent-kit or as environment variables.`
21464
21504
  }
21465
21505
  ]
21466
21506
  };
@@ -21475,7 +21515,7 @@ function registerIntegrationTools(server2) {
21475
21515
  prId = parseInt(urlMatch[3], 10);
21476
21516
  } else if (input.match(/^\d+$/)) {
21477
21517
  prId = parseInt(input, 10);
21478
- ws = workspace || process.env.BITBUCKET_DEFAULT_WORKSPACE;
21518
+ ws = workspace || getCredential("BITBUCKET_DEFAULT_WORKSPACE");
21479
21519
  repo = repoSlug;
21480
21520
  }
21481
21521
  if (!ws) {
@@ -21558,9 +21598,9 @@ ${diff}
21558
21598
  },
21559
21599
  async ({ ticketId }) => {
21560
21600
  try {
21561
- const cloudId = process.env.ATLASSIAN_CLOUD_ID;
21562
- const userEmail = process.env.ATLASSIAN_USER_EMAIL;
21563
- const apiToken = process.env.ATLASSIAN_API_TOKEN;
21601
+ const cloudId = getCredential("ATLASSIAN_CLOUD_ID");
21602
+ const userEmail = getCredential("ATLASSIAN_USER_EMAIL");
21603
+ const apiToken = getCredential("ATLASSIAN_API_TOKEN");
21564
21604
  if (!cloudId || !userEmail || !apiToken) {
21565
21605
  const missing = [
21566
21606
  !cloudId && "ATLASSIAN_CLOUD_ID",
@@ -9,7 +9,6 @@ import * as path from 'path';
9
9
  import { z } from 'zod';
10
10
  import { getWorkspaceRoot } from '../utils.js';
11
11
  import { commandExists, validatePath, sanitizeOutput } from './security.js';
12
- import { SAFE_ENV_VARS } from './config.js';
13
12
  // Configurable timeout via environment variable (default: 5 minutes)
14
13
  const AGENT_TIMEOUT = parseInt(process.env.KIT_AGENT_TIMEOUT || '300000', 10);
15
14
  const jobRegistry = new Map();
@@ -110,17 +109,9 @@ export function registerAgentTools(server) {
110
109
  const slug = taskSlug(task);
111
110
  const { logPath, logStream } = initAgentLog(workspaceRoot, slug, usedAgent);
112
111
  const jobId = randomUUID();
113
- // Build sanitized environment: only SAFE_ENV_VARS + GEMINI_WORKSPACE
114
- const sanitizedEnv = {};
115
- for (const key of SAFE_ENV_VARS) {
116
- if (process.env[key] !== undefined) {
117
- sanitizedEnv[key] = process.env[key];
118
- }
119
- }
120
- sanitizedEnv.GEMINI_WORKSPACE = workspaceRoot;
121
112
  const child = spawn(usedAgent, agentArgs, {
122
113
  cwd: workspaceRoot,
123
- env: sanitizedEnv,
114
+ env: { ...process.env, GEMINI_WORKSPACE: workspaceRoot },
124
115
  });
125
116
  const job = {
126
117
  id: jobId,
@@ -35,7 +35,3 @@ export declare const FORBIDDEN_PATTERNS: RegExp[];
35
35
  * Directory name segments that are always blocked.
36
36
  */
37
37
  export declare const FORBIDDEN_DIRS: string[];
38
- /**
39
- * Environment variables safe to pass to child processes.
40
- */
41
- export declare const SAFE_ENV_VARS: string[];
@@ -74,6 +74,7 @@ export const FORBIDDEN_FILES = [
74
74
  '.yarnrc',
75
75
  '.netrc',
76
76
  '.gitconfig',
77
+ 'credentials', // ~/.claude/credentials file
77
78
  ];
78
79
  /**
79
80
  * Regex patterns (case-insensitive) matched against path.basename.
@@ -100,23 +101,3 @@ export const FORBIDDEN_DIRS = [
100
101
  '.gnupg',
101
102
  '.docker',
102
103
  ];
103
- /**
104
- * Environment variables safe to pass to child processes.
105
- */
106
- export const SAFE_ENV_VARS = [
107
- 'PATH',
108
- 'HOME',
109
- 'USER',
110
- 'SHELL',
111
- 'TERM',
112
- 'LANG',
113
- 'LC_ALL',
114
- 'LC_CTYPE',
115
- 'TMPDIR',
116
- 'TMP',
117
- 'TEMP',
118
- 'NODE_ENV',
119
- 'GEMINI_WORKSPACE',
120
- 'KIT_AGENT_TIMEOUT',
121
- 'GEMINI_KIT_GIT_TIMEOUT',
122
- ];
@@ -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 = process.env.ATLASSIAN_USER_EMAIL;
81
- const token = process.env.ATLASSIAN_API_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 = process.env.BITBUCKET_USER_EMAIL;
89
- const token = process.env.BITBUCKET_API_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 = process.env.BITBUCKET_USER_EMAIL;
154
- const bbToken = process.env.BITBUCKET_API_TOKEN;
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. Create an API token at id.atlassian.com/manage-profile/security/api-tokens.`,
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 || process.env.BITBUCKET_DEFAULT_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 = process.env.ATLASSIAN_CLOUD_ID;
246
- const userEmail = process.env.ATLASSIAN_USER_EMAIL;
247
- const apiToken = process.env.ATLASSIAN_API_TOKEN;
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanhnd/agent-kit",
3
- "version": "1.0.9",
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",