@hanhnd/agent-kit 1.0.25 → 1.0.27

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.
@@ -3223,8 +3223,8 @@ var require_utils = __commonJS({
3223
3223
  }
3224
3224
  return ind;
3225
3225
  }
3226
- function removeDotSegments(path5) {
3227
- let input = path5;
3226
+ function removeDotSegments(path6) {
3227
+ let input = path6;
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 [path5, query] = wsComponent.resourceName.split("?");
3427
- wsComponent.path = path5 && path5 !== "/" ? path5 : void 0;
3426
+ const [path6, query] = wsComponent.resourceName.split("?");
3427
+ wsComponent.path = path6 && path6 !== "/" ? path6 : void 0;
3428
3428
  wsComponent.query = query;
3429
3429
  wsComponent.resourceName = void 0;
3430
3430
  }
@@ -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: path5, errorMaps, issueData } = params;
7281
- const fullPath = [...path5, ...issueData.path || []];
7280
+ const { data, path: path6, errorMaps, issueData } = params;
7281
+ const fullPath = [...path6, ...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, path5, key) {
7397
+ constructor(parent, value, path6, key) {
7398
7398
  this._cachedPath = [];
7399
7399
  this.parent = parent;
7400
7400
  this.data = value;
7401
- this._path = path5;
7401
+ this._path = path6;
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, path5) {
11040
- if (!path5)
11039
+ function getElementAtPath(obj, path6) {
11040
+ if (!path6)
11041
11041
  return obj;
11042
- return path5.reduce((acc, key) => acc?.[key], obj);
11042
+ return path6.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(path5, issues) {
11362
+ function prefixIssues(path6, issues) {
11363
11363
  return issues.map((iss) => {
11364
11364
  var _a;
11365
11365
  (_a = iss).path ?? (_a.path = []);
11366
- iss.path.unshift(path5);
11366
+ iss.path.unshift(path6);
11367
11367
  return iss;
11368
11368
  });
11369
11369
  }
@@ -20991,7 +20991,7 @@ var StdioServerTransport = class {
20991
20991
 
20992
20992
  // src/tools/core.ts
20993
20993
  import * as fs from "fs";
20994
- import * as path from "path";
20994
+ import * as path2 from "path";
20995
20995
 
20996
20996
  // src/utils/utils.ts
20997
20997
  function getWorkspaceRoot() {
@@ -21045,13 +21045,66 @@ var FORBIDDEN_DIRS = [
21045
21045
  ".docker"
21046
21046
  ];
21047
21047
 
21048
+ // src/tools/handoffs.ts
21049
+ import * as path from "path";
21050
+ var HANDOFF_TYPES = [
21051
+ "brainstorm",
21052
+ "clarification",
21053
+ "plan",
21054
+ "ticket",
21055
+ "research",
21056
+ "scenario",
21057
+ "investigation"
21058
+ ];
21059
+ var TICKET_ID_PATTERN = /\b[A-Z][A-Z0-9]+-\d+\b/;
21060
+ function findTicketId(value) {
21061
+ return value.match(TICKET_ID_PATTERN)?.[0].toLowerCase() ?? null;
21062
+ }
21063
+ function sanitizeFeatureSlug(value) {
21064
+ return value.replace(/[^a-zA-Z0-9-_]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").toLowerCase();
21065
+ }
21066
+ function contentSlugCandidate(content) {
21067
+ const heading = content.split(/\r?\n/).map((line) => line.replace(/^#+\s*/, "").trim()).find((line) => line.length > 0);
21068
+ return heading ?? content.replace(/\s+/g, " ").trim().slice(0, 80);
21069
+ }
21070
+ function normalizeHandoffType(type) {
21071
+ if (!HANDOFF_TYPES.includes(type)) {
21072
+ throw new Error(`Unsupported handoff type: ${type}`);
21073
+ }
21074
+ return type;
21075
+ }
21076
+ function deriveFeatureSlug(input) {
21077
+ const requestedTicketSlug = findTicketId(input.requestedSlug);
21078
+ if (requestedTicketSlug) return requestedTicketSlug;
21079
+ const requestedSlug = sanitizeFeatureSlug(input.requestedSlug);
21080
+ if (requestedSlug) return requestedSlug;
21081
+ const contentTicketSlug = findTicketId(input.content);
21082
+ if (contentTicketSlug) return contentTicketSlug;
21083
+ return sanitizeFeatureSlug(contentSlugCandidate(input.content)) || "untitled-handoff";
21084
+ }
21085
+ function resolveHandoffPath(input) {
21086
+ const canonicalType = normalizeHandoffType(input.type);
21087
+ const featureSlug = deriveFeatureSlug({
21088
+ requestedSlug: input.slug,
21089
+ content: input.content,
21090
+ type: canonicalType
21091
+ });
21092
+ const relativePath = path.join(".agent-kit", "handoffs", featureSlug, `${canonicalType}.md`);
21093
+ return {
21094
+ featureSlug,
21095
+ canonicalType,
21096
+ filePath: path.join(input.workspaceRoot, relativePath),
21097
+ relativePath
21098
+ };
21099
+ }
21100
+
21048
21101
  // src/tools/core.ts
21049
21102
  function registerCoreTools(server2) {
21050
21103
  server2.tool(
21051
21104
  "kit_save_handoff",
21052
21105
  `Save a handoff artifact to .agent-kit/handoffs/. Returns the saved file path to use in next-step instructions. Do NOT append version numbers (v2, v3, etc.) to the slug.`,
21053
21106
  {
21054
- type: external_exports.enum(["brainstorm", "clarify", "plan", "ticket", "research"]).describe("Handoff type"),
21107
+ type: external_exports.enum(HANDOFF_TYPES).describe("Handoff type"),
21055
21108
  content: external_exports.string().describe("Full markdown content to save"),
21056
21109
  slug: external_exports.string().describe(
21057
21110
  'Short identifier for the filename, e.g. "user-auth" or "PROJ-123". Do NOT append version numbers (v2, v3, etc.).'
@@ -21059,24 +21112,18 @@ function registerCoreTools(server2) {
21059
21112
  },
21060
21113
  async ({ type, content, slug }) => {
21061
21114
  try {
21062
- const safeSlug = slug.replace(/[^a-zA-Z0-9-_]/g, "-").toLowerCase();
21063
- const handoffDir = path.join(getWorkspaceRoot(), ".agent-kit", "handoffs", `${type}s`);
21115
+ const location = resolveHandoffPath({
21116
+ workspaceRoot: getWorkspaceRoot(),
21117
+ type,
21118
+ slug,
21119
+ content
21120
+ });
21121
+ const handoffDir = path2.dirname(location.filePath);
21064
21122
  if (!fs.existsSync(handoffDir)) {
21065
21123
  fs.mkdirSync(handoffDir, { recursive: true });
21066
21124
  }
21067
- const files = fs.readdirSync(handoffDir);
21068
- const slugPattern = new RegExp(`^${type}-([0-9T-]{19}-)?${safeSlug}\\.md$`);
21069
- const existingFile = files.filter((f) => slugPattern.test(f)).sort().pop();
21070
- let filePath;
21071
- if (existingFile) {
21072
- filePath = path.join(handoffDir, existingFile);
21073
- } else {
21074
- const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
21075
- const filename = `${type}-${timestamp}-${safeSlug}.md`;
21076
- filePath = path.join(handoffDir, filename);
21077
- }
21078
- fs.writeFileSync(filePath, content, "utf8");
21079
- return mcpText(`\u2705 Saved to: ${filePath}`);
21125
+ fs.writeFileSync(location.filePath, content, "utf8");
21126
+ return mcpText(`\u2705 Saved to: ${location.filePath}`);
21080
21127
  } catch (error2) {
21081
21128
  return mcpText(`Error saving handoff: ${error2}`);
21082
21129
  }
@@ -21088,27 +21135,27 @@ function registerCoreTools(server2) {
21088
21135
  import { spawn } from "child_process";
21089
21136
  import { randomUUID } from "crypto";
21090
21137
  import * as fs2 from "fs";
21091
- import * as path3 from "path";
21138
+ import * as path4 from "path";
21092
21139
 
21093
21140
  // src/tools/security.ts
21094
21141
  import { execFileSync } from "child_process";
21095
- import * as path2 from "path";
21142
+ import * as path3 from "path";
21096
21143
  function sanitize(input) {
21097
21144
  return String(input).replace(/[;&|`$<>\\]/g, "").trim().slice(0, 500);
21098
21145
  }
21099
21146
  function validatePath(filePath, baseDir = process.cwd()) {
21100
- const resolved = path2.resolve(baseDir, filePath);
21101
- const root = path2.resolve(baseDir);
21102
- if (resolved !== root && !resolved.startsWith(root + path2.sep)) {
21147
+ const resolved = path3.resolve(baseDir, filePath);
21148
+ const root = path3.resolve(baseDir);
21149
+ if (resolved !== root && !resolved.startsWith(root + path3.sep)) {
21103
21150
  throw new Error(`Path traversal detected: ${filePath}`);
21104
21151
  }
21105
- const segments = resolved.split(path2.sep);
21152
+ const segments = resolved.split(path3.sep);
21106
21153
  for (const segment of segments) {
21107
21154
  if (FORBIDDEN_DIRS.some((d) => segment.toLowerCase() === d.toLowerCase())) {
21108
21155
  throw new Error(`Access to sensitive directory is FORBIDDEN: ${segment}`);
21109
21156
  }
21110
21157
  }
21111
- const fileName = path2.basename(resolved);
21158
+ const fileName = path3.basename(resolved);
21112
21159
  const isExactForbidden = FORBIDDEN_FILES.some((f) => fileName.toLowerCase() === f.toLowerCase());
21113
21160
  const isPatternForbidden = FORBIDDEN_PATTERNS.some((re) => re.test(fileName));
21114
21161
  if (isExactForbidden || isPatternForbidden) {
@@ -21182,10 +21229,10 @@ function taskSlug(task) {
21182
21229
  return task.toLowerCase().replace(/[^a-z0-9\s]/g, "").trim().split(/\s+/).slice(0, 6).join("-").slice(0, 48);
21183
21230
  }
21184
21231
  function initAgentLog(workspaceRoot, slug, agent) {
21185
- const logsDir = path3.join(workspaceRoot, ".agent-kit", "logs", "agents");
21232
+ const logsDir = path4.join(workspaceRoot, ".agent-kit", "logs", "agents");
21186
21233
  fs2.mkdirSync(logsDir, { recursive: true });
21187
21234
  const now = (/* @__PURE__ */ new Date()).toISOString();
21188
- const logFile = path3.join(logsDir, `delegate-${now}-${slug}.log`);
21235
+ const logFile = path4.join(logsDir, `delegate-${now}-${slug}.log`);
21189
21236
  const header = `Agent: ${agent}
21190
21237
  Date: ${now}
21191
21238
  ${"\u2500".repeat(60)}
@@ -21195,16 +21242,16 @@ ${"\u2500".repeat(60)}
21195
21242
  const logStream = fs2.createWriteStream(logFile, { flags: "a" });
21196
21243
  logStream.on("error", () => {
21197
21244
  });
21198
- return { logPath: path3.relative(workspaceRoot, logFile), logStream };
21245
+ return { logPath: path4.relative(workspaceRoot, logFile), logStream };
21199
21246
  }
21200
21247
  function registerAgentTools(server2) {
21201
21248
  server2.tool(
21202
21249
  "kit_trigger_agent",
21203
- "!Important: Trigger only when user explicitly asks to delegate a task to an external agent CLI (gemini, claude, or codex). The task can be a direct message or a path to a handoff file (.agent-kit/handoffs/plans/plan-xyz.md).",
21250
+ "!Important: Trigger only when user explicitly asks to delegate a task to an external agent CLI (gemini, claude, or codex). The task can be a direct message or a path to a handoff file (.agent-kit/handoffs/feature-slug/plan.md).",
21204
21251
  {
21205
21252
  agent: external_exports.enum(["gemini", "claude", "codex"]).describe('The agent CLI to invoke: "gemini", "claude", or "codex"'),
21206
21253
  task: external_exports.string().describe(
21207
- 'Task message or path to a handoff file (e.g., ".agent-kit/handoffs/plans/plan-xyz.md")'
21254
+ 'Task message or path to a handoff file (e.g., ".agent-kit/handoffs/feature-slug/plan.md")'
21208
21255
  )
21209
21256
  },
21210
21257
  async ({ agent, task }, extra) => {
@@ -21214,7 +21261,7 @@ function registerAgentTools(server2) {
21214
21261
  let killChild;
21215
21262
  try {
21216
21263
  let prompt = task;
21217
- const resolvedTask = path3.resolve(workspaceRoot, task);
21264
+ const resolvedTask = path4.resolve(workspaceRoot, task);
21218
21265
  if (fs2.existsSync(resolvedTask) && fs2.statSync(resolvedTask).isFile()) {
21219
21266
  try {
21220
21267
  validatePath(task, workspaceRoot);
@@ -21321,8 +21368,8 @@ import { writeFileSync as writeFileSync3 } from "fs";
21321
21368
  // src/utils/credentials.ts
21322
21369
  import * as fs3 from "fs";
21323
21370
  import * as os from "os";
21324
- import * as path4 from "path";
21325
- var CREDENTIALS_FILE = path4.join(os.homedir(), ".claude", "credentials");
21371
+ import * as path5 from "path";
21372
+ var CREDENTIALS_FILE = path5.join(os.homedir(), ".claude", "credentials");
21326
21373
  var cachedCredentials = null;
21327
21374
  function parseIni(content) {
21328
21375
  const result = {};
@@ -58,13 +58,13 @@ function initAgentLog(workspaceRoot, slug, agent) {
58
58
  return { logPath: path.relative(workspaceRoot, logFile), logStream };
59
59
  }
60
60
  export function registerAgentTools(server) {
61
- server.tool('kit_trigger_agent', '!Important: Trigger only when user explicitly asks to delegate a task to an external agent CLI (gemini, claude, or codex). The task can be a direct message or a path to a handoff file (.agent-kit/handoffs/plans/plan-xyz.md).', {
61
+ server.tool('kit_trigger_agent', '!Important: Trigger only when user explicitly asks to delegate a task to an external agent CLI (gemini, claude, or codex). The task can be a direct message or a path to a handoff file (.agent-kit/handoffs/feature-slug/plan.md).', {
62
62
  agent: z
63
63
  .enum(['gemini', 'claude', 'codex'])
64
64
  .describe('The agent CLI to invoke: "gemini", "claude", or "codex"'),
65
65
  task: z
66
66
  .string()
67
- .describe('Task message or path to a handoff file (e.g., ".agent-kit/handoffs/plans/plan-xyz.md")'),
67
+ .describe('Task message or path to a handoff file (e.g., ".agent-kit/handoffs/feature-slug/plan.md")'),
68
68
  }, async ({ agent, task }, extra) => {
69
69
  const workspaceRoot = getWorkspaceRoot();
70
70
  let job;
@@ -6,6 +6,7 @@ import * as path from 'path';
6
6
  import { z } from 'zod';
7
7
  import { getWorkspaceRoot, mcpText } from '../utils/utils.js';
8
8
  import { DEFAULT_EXTENSIONS } from './config.js';
9
+ import { HANDOFF_TYPES, resolveHandoffPath } from './handoffs.js';
9
10
  /**
10
11
  * Register core tools with MCP server
11
12
  */
@@ -17,7 +18,7 @@ export function registerCoreTools(server) {
17
18
  // ═══════════════════════════════════════════════════════════════
18
19
  server.tool('kit_save_handoff', `Save a handoff artifact to .agent-kit/handoffs/. Returns the saved file path to use in next-step instructions. Do NOT append version numbers (v2, v3, etc.) to the slug.`, {
19
20
  type: z
20
- .enum(['brainstorm', 'clarify', 'plan', 'ticket', 'research'])
21
+ .enum(HANDOFF_TYPES)
21
22
  .describe('Handoff type'),
22
23
  content: z.string().describe('Full markdown content to save'),
23
24
  slug: z
@@ -25,28 +26,18 @@ export function registerCoreTools(server) {
25
26
  .describe('Short identifier for the filename, e.g. "user-auth" or "PROJ-123". Do NOT append version numbers (v2, v3, etc.).'),
26
27
  }, async ({ type, content, slug }) => {
27
28
  try {
28
- const safeSlug = slug.replace(/[^a-zA-Z0-9-_]/g, '-').toLowerCase();
29
- const handoffDir = path.join(getWorkspaceRoot(), '.agent-kit', 'handoffs', `${type}s`);
29
+ const location = resolveHandoffPath({
30
+ workspaceRoot: getWorkspaceRoot(),
31
+ type,
32
+ slug,
33
+ content,
34
+ });
35
+ const handoffDir = path.dirname(location.filePath);
30
36
  if (!fs.existsSync(handoffDir)) {
31
37
  fs.mkdirSync(handoffDir, { recursive: true });
32
38
  }
33
- const files = fs.readdirSync(handoffDir);
34
- const slugPattern = new RegExp(`^${type}-([0-9T-]{19}-)?${safeSlug}\\.md$`);
35
- const existingFile = files
36
- .filter((f) => slugPattern.test(f))
37
- .sort()
38
- .pop();
39
- let filePath;
40
- if (existingFile) {
41
- filePath = path.join(handoffDir, existingFile);
42
- }
43
- else {
44
- const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
45
- const filename = `${type}-${timestamp}-${safeSlug}.md`;
46
- filePath = path.join(handoffDir, filename);
47
- }
48
- fs.writeFileSync(filePath, content, 'utf8');
49
- return mcpText(`✅ Saved to: ${filePath}`);
39
+ fs.writeFileSync(location.filePath, content, 'utf8');
40
+ return mcpText(`✅ Saved to: ${location.filePath}`);
50
41
  }
51
42
  catch (error) {
52
43
  return mcpText(`Error saving handoff: ${error}`);
@@ -0,0 +1,22 @@
1
+ export declare const HANDOFF_TYPES: readonly ["brainstorm", "clarification", "plan", "ticket", "research", "scenario", "investigation"];
2
+ export type HandoffType = (typeof HANDOFF_TYPES)[number];
3
+ export type CanonicalHandoffType = 'brainstorm' | 'clarification' | 'plan' | 'ticket' | 'research' | 'scenario' | 'investigation';
4
+ export interface SavedHandoffLocation {
5
+ featureSlug: string;
6
+ canonicalType: CanonicalHandoffType;
7
+ filePath: string;
8
+ relativePath: string;
9
+ }
10
+ export declare function sanitizeFeatureSlug(value: string): string;
11
+ export declare function normalizeHandoffType(type: HandoffType): CanonicalHandoffType;
12
+ export declare function deriveFeatureSlug(input: {
13
+ requestedSlug: string;
14
+ content: string;
15
+ type: CanonicalHandoffType;
16
+ }): string;
17
+ export declare function resolveHandoffPath(input: {
18
+ workspaceRoot: string;
19
+ type: HandoffType;
20
+ slug: string;
21
+ content: string;
22
+ }): SavedHandoffLocation;
@@ -0,0 +1,62 @@
1
+ import * as path from 'path';
2
+ export const HANDOFF_TYPES = [
3
+ 'brainstorm',
4
+ 'clarification',
5
+ 'plan',
6
+ 'ticket',
7
+ 'research',
8
+ 'scenario',
9
+ 'investigation',
10
+ ];
11
+ const TICKET_ID_PATTERN = /\b[A-Z][A-Z0-9]+-\d+\b/;
12
+ function findTicketId(value) {
13
+ return value.match(TICKET_ID_PATTERN)?.[0].toLowerCase() ?? null;
14
+ }
15
+ export function sanitizeFeatureSlug(value) {
16
+ return value
17
+ .replace(/[^a-zA-Z0-9-_]/g, '-')
18
+ .replace(/-+/g, '-')
19
+ .replace(/^-|-$/g, '')
20
+ .toLowerCase();
21
+ }
22
+ function contentSlugCandidate(content) {
23
+ const heading = content
24
+ .split(/\r?\n/)
25
+ .map((line) => line.replace(/^#+\s*/, '').trim())
26
+ .find((line) => line.length > 0);
27
+ return heading ?? content.replace(/\s+/g, ' ').trim().slice(0, 80);
28
+ }
29
+ export function normalizeHandoffType(type) {
30
+ if (!HANDOFF_TYPES.includes(type)) {
31
+ throw new Error(`Unsupported handoff type: ${type}`);
32
+ }
33
+ return type;
34
+ }
35
+ export function deriveFeatureSlug(input) {
36
+ const requestedTicketSlug = findTicketId(input.requestedSlug);
37
+ if (requestedTicketSlug)
38
+ return requestedTicketSlug;
39
+ const requestedSlug = sanitizeFeatureSlug(input.requestedSlug);
40
+ if (requestedSlug)
41
+ return requestedSlug;
42
+ const contentTicketSlug = findTicketId(input.content);
43
+ if (contentTicketSlug)
44
+ return contentTicketSlug;
45
+ return (sanitizeFeatureSlug(contentSlugCandidate(input.content)) ||
46
+ 'untitled-handoff');
47
+ }
48
+ export function resolveHandoffPath(input) {
49
+ const canonicalType = normalizeHandoffType(input.type);
50
+ const featureSlug = deriveFeatureSlug({
51
+ requestedSlug: input.slug,
52
+ content: input.content,
53
+ type: canonicalType,
54
+ });
55
+ const relativePath = path.join('.agent-kit', 'handoffs', featureSlug, `${canonicalType}.md`);
56
+ return {
57
+ featureSlug,
58
+ canonicalType,
59
+ filePath: path.join(input.workspaceRoot, relativePath),
60
+ relativePath,
61
+ };
62
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanhnd/agent-kit",
3
- "version": "1.0.25",
3
+ "version": "1.0.27",
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",