@hanhnd/agent-kit 1.0.25 → 1.0.26

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,62 @@ 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-Za-z][A-Za-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 ticketSlug = findTicketId(input.requestedSlug) ?? findTicketId(input.content);
21078
+ if (ticketSlug) return ticketSlug;
21079
+ return sanitizeFeatureSlug(input.requestedSlug) || sanitizeFeatureSlug(contentSlugCandidate(input.content)) || "untitled-handoff";
21080
+ }
21081
+ function resolveHandoffPath(input) {
21082
+ const canonicalType = normalizeHandoffType(input.type);
21083
+ const featureSlug = deriveFeatureSlug({
21084
+ requestedSlug: input.slug,
21085
+ content: input.content,
21086
+ type: canonicalType
21087
+ });
21088
+ const relativePath = path.join(".agent-kit", "handoffs", featureSlug, `${canonicalType}.md`);
21089
+ return {
21090
+ featureSlug,
21091
+ canonicalType,
21092
+ filePath: path.join(input.workspaceRoot, relativePath),
21093
+ relativePath
21094
+ };
21095
+ }
21096
+
21048
21097
  // src/tools/core.ts
21049
21098
  function registerCoreTools(server2) {
21050
21099
  server2.tool(
21051
21100
  "kit_save_handoff",
21052
21101
  `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
21102
  {
21054
- type: external_exports.enum(["brainstorm", "clarify", "plan", "ticket", "research"]).describe("Handoff type"),
21103
+ type: external_exports.enum(HANDOFF_TYPES).describe("Handoff type"),
21055
21104
  content: external_exports.string().describe("Full markdown content to save"),
21056
21105
  slug: external_exports.string().describe(
21057
21106
  'Short identifier for the filename, e.g. "user-auth" or "PROJ-123". Do NOT append version numbers (v2, v3, etc.).'
@@ -21059,24 +21108,18 @@ function registerCoreTools(server2) {
21059
21108
  },
21060
21109
  async ({ type, content, slug }) => {
21061
21110
  try {
21062
- const safeSlug = slug.replace(/[^a-zA-Z0-9-_]/g, "-").toLowerCase();
21063
- const handoffDir = path.join(getWorkspaceRoot(), ".agent-kit", "handoffs", `${type}s`);
21111
+ const location = resolveHandoffPath({
21112
+ workspaceRoot: getWorkspaceRoot(),
21113
+ type,
21114
+ slug,
21115
+ content
21116
+ });
21117
+ const handoffDir = path2.dirname(location.filePath);
21064
21118
  if (!fs.existsSync(handoffDir)) {
21065
21119
  fs.mkdirSync(handoffDir, { recursive: true });
21066
21120
  }
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}`);
21121
+ fs.writeFileSync(location.filePath, content, "utf8");
21122
+ return mcpText(`\u2705 Saved to: ${location.filePath}`);
21080
21123
  } catch (error2) {
21081
21124
  return mcpText(`Error saving handoff: ${error2}`);
21082
21125
  }
@@ -21088,27 +21131,27 @@ function registerCoreTools(server2) {
21088
21131
  import { spawn } from "child_process";
21089
21132
  import { randomUUID } from "crypto";
21090
21133
  import * as fs2 from "fs";
21091
- import * as path3 from "path";
21134
+ import * as path4 from "path";
21092
21135
 
21093
21136
  // src/tools/security.ts
21094
21137
  import { execFileSync } from "child_process";
21095
- import * as path2 from "path";
21138
+ import * as path3 from "path";
21096
21139
  function sanitize(input) {
21097
21140
  return String(input).replace(/[;&|`$<>\\]/g, "").trim().slice(0, 500);
21098
21141
  }
21099
21142
  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)) {
21143
+ const resolved = path3.resolve(baseDir, filePath);
21144
+ const root = path3.resolve(baseDir);
21145
+ if (resolved !== root && !resolved.startsWith(root + path3.sep)) {
21103
21146
  throw new Error(`Path traversal detected: ${filePath}`);
21104
21147
  }
21105
- const segments = resolved.split(path2.sep);
21148
+ const segments = resolved.split(path3.sep);
21106
21149
  for (const segment of segments) {
21107
21150
  if (FORBIDDEN_DIRS.some((d) => segment.toLowerCase() === d.toLowerCase())) {
21108
21151
  throw new Error(`Access to sensitive directory is FORBIDDEN: ${segment}`);
21109
21152
  }
21110
21153
  }
21111
- const fileName = path2.basename(resolved);
21154
+ const fileName = path3.basename(resolved);
21112
21155
  const isExactForbidden = FORBIDDEN_FILES.some((f) => fileName.toLowerCase() === f.toLowerCase());
21113
21156
  const isPatternForbidden = FORBIDDEN_PATTERNS.some((re) => re.test(fileName));
21114
21157
  if (isExactForbidden || isPatternForbidden) {
@@ -21182,10 +21225,10 @@ function taskSlug(task) {
21182
21225
  return task.toLowerCase().replace(/[^a-z0-9\s]/g, "").trim().split(/\s+/).slice(0, 6).join("-").slice(0, 48);
21183
21226
  }
21184
21227
  function initAgentLog(workspaceRoot, slug, agent) {
21185
- const logsDir = path3.join(workspaceRoot, ".agent-kit", "logs", "agents");
21228
+ const logsDir = path4.join(workspaceRoot, ".agent-kit", "logs", "agents");
21186
21229
  fs2.mkdirSync(logsDir, { recursive: true });
21187
21230
  const now = (/* @__PURE__ */ new Date()).toISOString();
21188
- const logFile = path3.join(logsDir, `delegate-${now}-${slug}.log`);
21231
+ const logFile = path4.join(logsDir, `delegate-${now}-${slug}.log`);
21189
21232
  const header = `Agent: ${agent}
21190
21233
  Date: ${now}
21191
21234
  ${"\u2500".repeat(60)}
@@ -21195,16 +21238,16 @@ ${"\u2500".repeat(60)}
21195
21238
  const logStream = fs2.createWriteStream(logFile, { flags: "a" });
21196
21239
  logStream.on("error", () => {
21197
21240
  });
21198
- return { logPath: path3.relative(workspaceRoot, logFile), logStream };
21241
+ return { logPath: path4.relative(workspaceRoot, logFile), logStream };
21199
21242
  }
21200
21243
  function registerAgentTools(server2) {
21201
21244
  server2.tool(
21202
21245
  "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).",
21246
+ "!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
21247
  {
21205
21248
  agent: external_exports.enum(["gemini", "claude", "codex"]).describe('The agent CLI to invoke: "gemini", "claude", or "codex"'),
21206
21249
  task: external_exports.string().describe(
21207
- 'Task message or path to a handoff file (e.g., ".agent-kit/handoffs/plans/plan-xyz.md")'
21250
+ 'Task message or path to a handoff file (e.g., ".agent-kit/handoffs/feature-slug/plan.md")'
21208
21251
  )
21209
21252
  },
21210
21253
  async ({ agent, task }, extra) => {
@@ -21214,7 +21257,7 @@ function registerAgentTools(server2) {
21214
21257
  let killChild;
21215
21258
  try {
21216
21259
  let prompt = task;
21217
- const resolvedTask = path3.resolve(workspaceRoot, task);
21260
+ const resolvedTask = path4.resolve(workspaceRoot, task);
21218
21261
  if (fs2.existsSync(resolvedTask) && fs2.statSync(resolvedTask).isFile()) {
21219
21262
  try {
21220
21263
  validatePath(task, workspaceRoot);
@@ -21321,8 +21364,8 @@ import { writeFileSync as writeFileSync3 } from "fs";
21321
21364
  // src/utils/credentials.ts
21322
21365
  import * as fs3 from "fs";
21323
21366
  import * as os from "os";
21324
- import * as path4 from "path";
21325
- var CREDENTIALS_FILE = path4.join(os.homedir(), ".claude", "credentials");
21367
+ import * as path5 from "path";
21368
+ var CREDENTIALS_FILE = path5.join(os.homedir(), ".claude", "credentials");
21326
21369
  var cachedCredentials = null;
21327
21370
  function parseIni(content) {
21328
21371
  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,57 @@
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-Za-z][A-Za-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 ticketSlug = findTicketId(input.requestedSlug) ?? findTicketId(input.content);
37
+ if (ticketSlug)
38
+ return ticketSlug;
39
+ return (sanitizeFeatureSlug(input.requestedSlug) ||
40
+ sanitizeFeatureSlug(contentSlugCandidate(input.content)) ||
41
+ 'untitled-handoff');
42
+ }
43
+ export function resolveHandoffPath(input) {
44
+ const canonicalType = normalizeHandoffType(input.type);
45
+ const featureSlug = deriveFeatureSlug({
46
+ requestedSlug: input.slug,
47
+ content: input.content,
48
+ type: canonicalType,
49
+ });
50
+ const relativePath = path.join('.agent-kit', 'handoffs', featureSlug, `${canonicalType}.md`);
51
+ return {
52
+ featureSlug,
53
+ canonicalType,
54
+ filePath: path.join(input.workspaceRoot, relativePath),
55
+ relativePath,
56
+ };
57
+ }
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.26",
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",