@hanhnd/agent-kit 1.0.32 → 1.0.34

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.
Files changed (111) hide show
  1. package/dist/cli/commands/cli-digest-pending.test.js +111 -0
  2. package/dist/cli/commands/cli-memory.test.js +34 -0
  3. package/dist/cli/{memory.js → commands/memory.js} +24 -11
  4. package/dist/cli/index.js +2 -2
  5. package/dist/{utils → cli/utils}/args.d.ts +2 -1
  6. package/dist/{kit-cli.js → cli.js} +342 -58
  7. package/dist/{tools/config.d.ts → core/config/index.d.ts} +2 -7
  8. package/dist/{tools/config.js → core/config/index.js} +2 -13
  9. package/dist/core/handoffs/index.d.ts +17 -0
  10. package/dist/{tools/handoffs.js → core/handoffs/index.js} +15 -7
  11. package/dist/core/handoffs/index.test.js +88 -0
  12. package/dist/{tools/security.d.ts → core/security/index.d.ts} +0 -19
  13. package/dist/{tools/security.js → core/security/index.js} +3 -76
  14. package/dist/entrypoints/cli.js +11 -0
  15. package/dist/entrypoints/server.js +37 -0
  16. package/dist/{tools → mcp}/agent.d.ts +1 -1
  17. package/dist/{tools → mcp}/agent.js +4 -4
  18. package/dist/mcp/core.d.ts +17 -0
  19. package/dist/mcp/core.js +60 -0
  20. package/dist/mcp/core.test.js +98 -0
  21. package/dist/{tools → mcp}/integration.js +3 -3
  22. package/dist/mcp/memory.d.ts +15 -0
  23. package/dist/{tools → mcp}/memory.js +15 -13
  24. package/dist/{kit-server.js → server.js} +129 -98
  25. package/dist/services/digest/background.d.ts +11 -0
  26. package/dist/services/digest/background.js +258 -0
  27. package/dist/{memory → services}/digest/constants.d.ts +4 -1
  28. package/dist/{memory → services}/digest/constants.js +6 -2
  29. package/dist/services/digest/digest-background.test.js +206 -0
  30. package/dist/{tools/__tests__ → services/digest}/digest-init.test.js +4 -3
  31. package/dist/{memory/__tests__ → services/digest}/digest-llama-local.test.js +1 -13
  32. package/dist/{memory/__tests__ → services/digest}/digest-pending.test.js +39 -11
  33. package/dist/{memory/__tests__ → services/digest}/digest-processor.test.js +5 -7
  34. package/dist/{memory → services}/digest/files.d.ts +0 -1
  35. package/dist/{memory → services}/digest/files.js +2 -2
  36. package/dist/services/digest/model-registry.d.ts +2 -0
  37. package/dist/{memory → services}/digest/model-registry.js +9 -8
  38. package/dist/services/digest/processor.d.ts +13 -0
  39. package/dist/{memory → services}/digest/processor.js +48 -31
  40. package/dist/services/digest/providers/llama-local.d.ts +2 -0
  41. package/dist/{memory → services}/digest/providers/llama-local.js +3 -4
  42. package/dist/{memory → services}/digest/types.d.ts +34 -2
  43. package/dist/services/digest/types.js +6 -0
  44. package/dist/{memory → services}/digest/worker.d.ts +0 -1
  45. package/dist/{memory → services}/digest/worker.js +2 -2
  46. package/dist/{utils → services/integration}/credentials.js +1 -1
  47. package/dist/{utils/parser.js → services/integration/parser/adf.js} +1 -0
  48. package/dist/{memory → services/memory}/chunker.d.ts +0 -2
  49. package/dist/{memory → services/memory}/chunker.js +3 -3
  50. package/dist/{memory/__tests__ → services/memory}/chunker.test.js +1 -34
  51. package/dist/{memory → services/memory}/constants.d.ts +1 -1
  52. package/dist/{memory → services/memory}/constants.js +2 -1
  53. package/dist/{memory → services/memory}/embedder.d.ts +2 -2
  54. package/dist/{memory → services/memory}/embedder.js +5 -5
  55. package/dist/services/memory/embedder.test.js +13 -0
  56. package/dist/{memory → services/memory}/indexer.d.ts +2 -1
  57. package/dist/{memory → services/memory}/indexer.js +1 -1
  58. package/dist/{memory/__tests__ → services/memory}/indexer.test.js +4 -3
  59. package/dist/{tools/__tests__ → services/memory}/memory.test.js +54 -13
  60. package/dist/{memory/__tests__ → services/memory}/store.test.js +3 -2
  61. package/dist/{memory → services/memory}/types.d.ts +7 -2
  62. package/dist/services/memory/types.js +6 -0
  63. package/dist/utils/files.d.ts +0 -1
  64. package/dist/utils/files.js +1 -1
  65. package/dist/{memory/__tests__/lockfile.test.js → utils/files.test.js} +14 -14
  66. package/dist/utils/index.d.ts +6 -0
  67. package/dist/utils/index.js +6 -0
  68. package/dist/utils/json.d.ts +0 -1
  69. package/dist/utils/json.js +0 -10
  70. package/dist/utils/paths.d.ts +0 -1
  71. package/dist/utils/paths.js +1 -1
  72. package/package.json +6 -6
  73. package/dist/memory/__tests__/digest-worker.test.js +0 -46
  74. package/dist/memory/__tests__/embedder.test.js +0 -14
  75. package/dist/memory/digest/model-registry.d.ts +0 -6
  76. package/dist/memory/digest/processor.d.ts +0 -7
  77. package/dist/memory/digest/providers/llama-local.d.ts +0 -7
  78. package/dist/tools/__tests__/cli-digest-pending.test.js +0 -51
  79. package/dist/tools/__tests__/cli-memory.test.js +0 -22
  80. package/dist/tools/core.d.ts +0 -10
  81. package/dist/tools/core.js +0 -46
  82. package/dist/tools/handoffs.d.ts +0 -22
  83. package/dist/tools/memory.d.ts +0 -16
  84. /package/dist/{tools/__tests__ → cli/commands}/cli-digest-pending.test.d.ts +0 -0
  85. /package/dist/{tools/__tests__ → cli/commands}/cli-memory.test.d.ts +0 -0
  86. /package/dist/cli/{memory.d.ts → commands/memory.d.ts} +0 -0
  87. /package/dist/{utils → cli/utils}/args.js +0 -0
  88. /package/dist/{memory/__tests__/chunker.test.d.ts → core/handoffs/index.test.d.ts} +0 -0
  89. /package/dist/{kit-cli.d.ts → entrypoints/cli.d.ts} +0 -0
  90. /package/dist/{kit-server.d.ts → entrypoints/server.d.ts} +0 -0
  91. /package/dist/{memory/__tests__/digest-llama-local.test.d.ts → mcp/core.test.d.ts} +0 -0
  92. /package/dist/{tools → mcp}/integration.d.ts +0 -0
  93. /package/dist/{memory/__tests__/digest-pending.test.d.ts → services/digest/digest-background.test.d.ts} +0 -0
  94. /package/dist/{tools/__tests__ → services/digest}/digest-init.test.d.ts +0 -0
  95. /package/dist/{memory/__tests__/digest-processor.test.d.ts → services/digest/digest-llama-local.test.d.ts} +0 -0
  96. /package/dist/{memory/__tests__/digest-worker.test.d.ts → services/digest/digest-pending.test.d.ts} +0 -0
  97. /package/dist/{memory/__tests__/embedder.test.d.ts → services/digest/digest-processor.test.d.ts} +0 -0
  98. /package/dist/{utils → services/integration}/credentials.d.ts +0 -0
  99. /package/dist/{utils/parser.d.ts → services/integration/parser/adf.d.ts} +0 -0
  100. /package/dist/{memory/__tests__/indexer.test.d.ts → services/memory/chunker.test.d.ts} +0 -0
  101. /package/dist/{memory/__tests__/lockfile.test.d.ts → services/memory/embedder.test.d.ts} +0 -0
  102. /package/dist/{memory → services/memory}/index.d.ts +0 -0
  103. /package/dist/{memory → services/memory}/index.js +0 -0
  104. /package/dist/{memory/__tests__/store.test.d.ts → services/memory/indexer.test.d.ts} +0 -0
  105. /package/dist/{tools/__tests__ → services/memory}/memory.test.d.ts +0 -0
  106. /package/dist/{memory → services/memory}/store.d.ts +0 -0
  107. /package/dist/{memory → services/memory}/store.js +0 -0
  108. /package/dist/{memory/digest/types.js → services/memory/store.test.d.ts} +0 -0
  109. /package/dist/{memory/types.js → utils/files.test.d.ts} +0 -0
  110. /package/dist/{__tests__/temp-dir.d.ts → utils/temp-dir.test.d.ts} +0 -0
  111. /package/dist/{__tests__/temp-dir.js → utils/temp-dir.test.js} +0 -0
@@ -7,12 +7,13 @@ export const HANDOFF_TYPES = [
7
7
  'research',
8
8
  'scenario',
9
9
  'investigation',
10
+ 'code',
10
11
  ];
11
12
  const TICKET_ID_PATTERN = /\b[A-Z][A-Z0-9]+-\d+\b/;
12
13
  function findTicketId(value) {
13
14
  return value.match(TICKET_ID_PATTERN)?.[0].toLowerCase() ?? null;
14
15
  }
15
- export function sanitizeFeatureSlug(value) {
16
+ function sanitizeFeatureSlug(value) {
16
17
  return value
17
18
  .replace(/[^a-zA-Z0-9-_]/g, '-')
18
19
  .replace(/-+/g, '-')
@@ -26,13 +27,13 @@ function contentSlugCandidate(content) {
26
27
  .find((line) => line.length > 0);
27
28
  return heading ?? content.replace(/\s+/g, ' ').trim().slice(0, 80);
28
29
  }
29
- export function normalizeHandoffType(type) {
30
+ function normalizeHandoffType(type) {
30
31
  if (!HANDOFF_TYPES.includes(type)) {
31
32
  throw new Error(`Unsupported handoff type: ${type}`);
32
33
  }
33
34
  return type;
34
35
  }
35
- export function deriveFeatureSlug(input) {
36
+ function deriveFeatureSlug(input) {
36
37
  const requestedTicketSlug = findTicketId(input.requestedSlug);
37
38
  if (requestedTicketSlug)
38
39
  return requestedTicketSlug;
@@ -44,18 +45,25 @@ export function deriveFeatureSlug(input) {
44
45
  return contentTicketSlug;
45
46
  return sanitizeFeatureSlug(contentSlugCandidate(input.content)) || 'untitled-handoff';
46
47
  }
47
- export function resolveHandoffPath(input) {
48
+ export function resolveHandoffFolder(input) {
48
49
  const canonicalType = normalizeHandoffType(input.type);
49
50
  const featureSlug = deriveFeatureSlug({
50
51
  requestedSlug: input.slug,
51
- content: input.content,
52
+ content: input.primaryContent,
52
53
  type: canonicalType,
53
54
  });
54
- const relativePath = path.join('.agent-kit', 'handoffs', featureSlug, `${canonicalType}.md`);
55
+ const relativePath = path.join('.agent-kit', 'handoffs', featureSlug, canonicalType);
55
56
  return {
56
57
  featureSlug,
57
58
  canonicalType,
58
- filePath: path.join(input.workspaceRoot, relativePath),
59
+ folderPath: path.join(input.workspaceRoot, relativePath),
59
60
  relativePath,
60
61
  };
61
62
  }
63
+ export function validateHandoffFilename(name) {
64
+ const normalized = name.trim().replace(/^["']|["']$/g, '');
65
+ if (!/^[A-Za-z0-9_-]+\.md$/.test(normalized)) {
66
+ throw new Error(`Unsafe handoff filename: ${name}`);
67
+ }
68
+ return normalized;
69
+ }
@@ -0,0 +1,88 @@
1
+ import * as assert from 'node:assert/strict';
2
+ import * as path from 'node:path';
3
+ import { describe, test } from 'node:test';
4
+ import { resolveHandoffFolder, validateHandoffFilename } from './index.js';
5
+ const WORKSPACE = '/tmp/test-workspace';
6
+ describe('resolveHandoffFolder', () => {
7
+ test('relativePath has no .md suffix', () => {
8
+ const result = resolveHandoffFolder({
9
+ workspaceRoot: WORKSPACE,
10
+ type: 'plan',
11
+ slug: 'my-feature',
12
+ primaryContent: '',
13
+ });
14
+ assert.ok(!result.relativePath.endsWith('.md'), `relativePath must not end with .md, got: ${result.relativePath}`);
15
+ assert.equal(result.relativePath, path.join('.agent-kit', 'handoffs', 'my-feature', 'plan'));
16
+ });
17
+ test('folderPath is absolute under workspaceRoot', () => {
18
+ const result = resolveHandoffFolder({
19
+ workspaceRoot: WORKSPACE,
20
+ type: 'brainstorm',
21
+ slug: 'auth',
22
+ primaryContent: '',
23
+ });
24
+ assert.equal(result.folderPath, path.join(WORKSPACE, '.agent-kit', 'handoffs', 'auth', 'brainstorm'));
25
+ });
26
+ test('ticket-ID extracted from primaryContent when slug is empty', () => {
27
+ const result = resolveHandoffFolder({
28
+ workspaceRoot: WORKSPACE,
29
+ type: 'plan',
30
+ slug: '',
31
+ primaryContent: '# Fix PROJ-456\n\nSome content.',
32
+ });
33
+ assert.equal(result.featureSlug, 'proj-456');
34
+ });
35
+ test('first-heading fallback when no ticket ID in slug or content', () => {
36
+ const result = resolveHandoffFolder({
37
+ workspaceRoot: WORKSPACE,
38
+ type: 'research',
39
+ slug: '',
40
+ primaryContent: '# My Feature\n\nDetail.',
41
+ });
42
+ assert.equal(result.featureSlug, 'my-feature');
43
+ });
44
+ test('"untitled-handoff" fallback when both slug and content are empty', () => {
45
+ const result = resolveHandoffFolder({ workspaceRoot: WORKSPACE, type: 'ticket', slug: '', primaryContent: '' });
46
+ assert.equal(result.featureSlug, 'untitled-handoff');
47
+ });
48
+ test('code handoff resolves under code folder', () => {
49
+ const result = resolveHandoffFolder({
50
+ workspaceRoot: WORKSPACE,
51
+ type: 'code',
52
+ slug: 'execution-decision-ledger',
53
+ primaryContent: '# Execution Report',
54
+ });
55
+ assert.equal(result.canonicalType, 'code');
56
+ assert.equal(result.relativePath, path.join('.agent-kit', 'handoffs', 'execution-decision-ledger', 'code'));
57
+ assert.equal(result.folderPath, path.join(WORKSPACE, '.agent-kit', 'handoffs', 'execution-decision-ledger', 'code'));
58
+ });
59
+ });
60
+ describe('validateHandoffFilename', () => {
61
+ test('"README.md" returns unchanged', () => {
62
+ assert.equal(validateHandoffFilename('README.md'), 'README.md');
63
+ });
64
+ test('"DETAIL.md" returns unchanged', () => {
65
+ assert.equal(validateHandoffFilename('DETAIL.md'), 'DETAIL.md');
66
+ });
67
+ test('"index.md" returns unchanged', () => {
68
+ assert.equal(validateHandoffFilename('index.md'), 'index.md');
69
+ });
70
+ test('"../etc/passwd" throws', () => {
71
+ assert.throws(() => validateHandoffFilename('../etc/passwd'), /Unsafe handoff filename/);
72
+ });
73
+ test('"sub/dir.md" throws', () => {
74
+ assert.throws(() => validateHandoffFilename('sub/dir.md'), /Unsafe handoff filename/);
75
+ });
76
+ test('"README" throws — missing .md extension', () => {
77
+ assert.throws(() => validateHandoffFilename('README'), /Unsafe handoff filename/);
78
+ });
79
+ test('empty string throws', () => {
80
+ assert.throws(() => validateHandoffFilename(''), /Unsafe handoff filename/);
81
+ });
82
+ test('"README.txt" throws — wrong extension', () => {
83
+ assert.throws(() => validateHandoffFilename('README.txt'), /Unsafe handoff filename/);
84
+ });
85
+ test('".hidden.md" throws — leading dot', () => {
86
+ assert.throws(() => validateHandoffFilename('.hidden.md'), /Unsafe handoff filename/);
87
+ });
88
+ });
@@ -18,26 +18,7 @@ export declare function sanitize(input: string): string;
18
18
  * Uses stricter path.sep check to prevent prefix matching flaws.
19
19
  */
20
20
  export declare function validatePath(filePath: string, baseDir?: string): string;
21
- /**
22
- * Safe git command execution using execFileSync
23
- * Includes stderr in error message for better debugging
24
- *
25
- * @param timeout Default from GEMINI_KIT_GIT_TIMEOUT env var or 30s
26
- */
27
- export declare function safeGit(args: string[], options?: {
28
- timeout?: number;
29
- cwd?: string;
30
- }): string;
31
- /**
32
- * Check if a command exists (cross-platform)
33
- * Uses 'where' on Windows, 'which' on macOS/Linux
34
- */
35
21
  export declare function commandExists(cmd: string): boolean;
36
- /**
37
- * Async file finder - non-blocking for large repos
38
- * Uses queue-based approach to prevent stack overflow on deep directories
39
- */
40
- export declare function findFilesAsync(dir: string, extensions: string[], maxFiles: number, excludeDirs?: string[]): Promise<string[]>;
41
22
  /**
42
23
  * Sanitize output text by redacting sensitive tokens (API keys, etc.)
43
24
  */
@@ -3,10 +3,9 @@
3
3
  * Exported utilities for safe command execution
4
4
  */
5
5
  import { execFileSync } from 'child_process';
6
- import * as fs from 'fs';
7
6
  import * as path from 'path';
8
- import { getWorkspaceRoot } from '../utils/utils.js';
9
- import { FORBIDDEN_DIRS, FORBIDDEN_FILES, FORBIDDEN_PATTERNS } from './config.js';
7
+ import { getWorkspaceRoot } from '../../utils/utils.js';
8
+ import { FORBIDDEN_DIRS, FORBIDDEN_FILES, FORBIDDEN_PATTERNS } from '../config/index.js';
10
9
  /**
11
10
  * Sanitize string for safe use with execFileSync
12
11
  * Only removes dangerous shell operators - safe chars like !?#* are allowed
@@ -52,42 +51,6 @@ export function validatePath(filePath, baseDir = process.cwd()) {
52
51
  }
53
52
  return resolved;
54
53
  }
55
- /**
56
- * Extract stderr from error for better logging
57
- */
58
- function extractStderr(error) {
59
- if (error && typeof error === 'object' && 'stderr' in error) {
60
- return String(error.stderr);
61
- }
62
- return '';
63
- }
64
- // Configurable timeouts via environment variables
65
- const GIT_TIMEOUT = parseInt(process.env.GEMINI_KIT_GIT_TIMEOUT || '30000', 10);
66
- /**
67
- * Safe git command execution using execFileSync
68
- * Includes stderr in error message for better debugging
69
- *
70
- * @param timeout Default from GEMINI_KIT_GIT_TIMEOUT env var or 30s
71
- */
72
- export function safeGit(args, options) {
73
- try {
74
- return execFileSync('git', args, {
75
- encoding: 'utf8',
76
- timeout: options?.timeout || GIT_TIMEOUT,
77
- cwd: options?.cwd || getWorkspaceRoot(),
78
- maxBuffer: 10 * 1024 * 1024, // 10MB
79
- });
80
- }
81
- catch (error) {
82
- const stderr = extractStderr(error);
83
- const baseMsg = error instanceof Error ? error.message : String(error);
84
- throw new Error(`Git command failed: ${baseMsg}${stderr ? `\nDetails: ${stderr}` : ''}`);
85
- }
86
- }
87
- /**
88
- * Check if a command exists (cross-platform)
89
- * Uses 'where' on Windows, 'which' on macOS/Linux
90
- */
91
54
  export function commandExists(cmd) {
92
55
  try {
93
56
  const checkCmd = process.platform === 'win32' ? 'where' : 'which';
@@ -103,42 +66,6 @@ export function commandExists(cmd) {
103
66
  return false;
104
67
  }
105
68
  }
106
- /**
107
- * Async file finder - non-blocking for large repos
108
- * Uses queue-based approach to prevent stack overflow on deep directories
109
- */
110
- export async function findFilesAsync(dir, extensions, maxFiles, excludeDirs = ['node_modules', '.git', 'dist', 'build', 'coverage']) {
111
- const results = [];
112
- // Use queue instead of recursion to prevent stack overflow
113
- const queue = [{ fullPath: dir, relativePath: '' }];
114
- while (queue.length > 0 && results.length < maxFiles) {
115
- const current = queue.shift();
116
- let entries;
117
- try {
118
- entries = await fs.promises.readdir(current.fullPath, { withFileTypes: true });
119
- }
120
- catch {
121
- continue; // Skip directories we can't read
122
- }
123
- for (const entry of entries) {
124
- if (results.length >= maxFiles)
125
- break;
126
- const entryFullPath = path.join(current.fullPath, entry.name);
127
- const entryRelPath = current.relativePath ? path.join(current.relativePath, entry.name) : entry.name;
128
- if (entry.isDirectory()) {
129
- if (!excludeDirs.includes(entry.name)) {
130
- queue.push({ fullPath: entryFullPath, relativePath: entryRelPath });
131
- }
132
- }
133
- else if (entry.isFile()) {
134
- if (extensions.some((ext) => entry.name.endsWith(ext))) {
135
- results.push(entryRelPath);
136
- }
137
- }
138
- }
139
- }
140
- return results;
141
- }
142
69
  /**
143
70
  * Sanitize output text by redacting sensitive tokens (API keys, etc.)
144
71
  */
@@ -148,7 +75,7 @@ export function sanitizeOutput(text) {
148
75
  /SK-[A-Za-z0-9]{40,}/g, // OpenAI keys
149
76
  /AKIA[0-9A-Z]{16}/g, // AWS access keys
150
77
  /xox[baprs]-[A-Za-z0-9-]{10,}/g, // Slack tokens
151
- /[Aa]tlassian[_\-]?[Aa][Pp][Ii][_\-]?[Tt]oken[^\s]*\s*[:=]\s*\S+/g, // Atlassian token patterns
78
+ /[Aa]tlassian[_-]?[Aa][Pp][Ii][_-]?[Tt]oken[^\s]*\s*[:=]\s*\S+/g, // Atlassian token patterns
152
79
  ];
153
80
  let sanitized = text;
154
81
  for (const pattern of SECRET_PATTERNS) {
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+ import { runAgentKitCli } from '../cli/index.js';
3
+ try {
4
+ const exitCode = await runAgentKitCli(process.argv.slice(2), process.env);
5
+ process.exit(exitCode);
6
+ }
7
+ catch (err) {
8
+ const message = err instanceof Error ? err.message : String(err);
9
+ process.stderr.write('[agent-kit] CLI error: ' + message + '\n');
10
+ process.exit(1);
11
+ }
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Agent-Kit MCP Server
4
+ * Provides custom tools for agent orchestration
5
+ *
6
+ * Modular architecture - tools split into separate modules
7
+ */
8
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
9
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
10
+ import { registerCoreTools } from '../mcp/core.js';
11
+ import { registerAgentTools } from '../mcp/agent.js';
12
+ import { registerIntegrationTools } from '../mcp/integration.js';
13
+ import { loadProjectSettings } from '../core/config/index.js';
14
+ import { registerMemoryTools } from '../mcp/memory.js';
15
+ import { getWorkspaceRoot } from '../utils/utils.js';
16
+ const server = new McpServer({
17
+ name: 'kit-agents',
18
+ version: '1.0.0',
19
+ });
20
+ // ═══════════════════════════════════════════════════════════════
21
+ // REGISTER MODULAR TOOLS
22
+ // ═══════════════════════════════════════════════════════════════
23
+ registerIntegrationTools(server);
24
+ registerCoreTools(server);
25
+ registerAgentTools(server);
26
+ const workspaceRoot = getWorkspaceRoot();
27
+ const settings = loadProjectSettings(workspaceRoot);
28
+ const memoryIndexer = registerMemoryTools(server, settings, workspaceRoot);
29
+ // ═══════════════════════════════════════════════════════════════
30
+ // START SERVER
31
+ // ═══════════════════════════════════════════════════════════════
32
+ const transport = new StdioServerTransport();
33
+ await server.connect(transport);
34
+ // Fire-and-forget startup indexing — must run after server.connect
35
+ if (memoryIndexer) {
36
+ void memoryIndexer.startupIndex();
37
+ }
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Agent Delegation Tools - Invoke external agent CLIs (Gemini, Claude, Codex)
3
- * Supports handoff file injection and automatic fallback
3
+ * Supports handoff folder references and automatic fallback
4
4
  */
5
5
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
6
6
  export declare function registerAgentTools(server: McpServer): void;
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Agent Delegation Tools - Invoke external agent CLIs (Gemini, Claude, Codex)
3
- * Supports handoff file injection and automatic fallback
3
+ * Supports handoff folder references and automatic fallback
4
4
  */
5
5
  import { spawn } from 'child_process';
6
6
  import { randomUUID } from 'crypto';
@@ -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, mcpJson, mcpText } from '../utils/utils.js';
11
- import { commandExists, sanitizeOutput, validatePath } from './security.js';
11
+ import { commandExists, sanitizeOutput, validatePath } from '../core/security/index.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();
@@ -49,11 +49,11 @@ function initAgentLog(workspaceRoot, slug, agent) {
49
49
  return { logPath: path.relative(workspaceRoot, logFile), logStream };
50
50
  }
51
51
  export function registerAgentTools(server) {
52
- 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).', {
52
+ 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 folder (.agent-kit/handoffs/feature-slug/plan).', {
53
53
  agent: z.enum(['gemini', 'claude', 'codex']).describe('The agent CLI to invoke: "gemini", "claude", or "codex"'),
54
54
  task: z
55
55
  .string()
56
- .describe('Task message or path to a handoff file (e.g., ".agent-kit/handoffs/feature-slug/plan.md")'),
56
+ .describe('Task message or path to a handoff folder (e.g., ".agent-kit/handoffs/feature-slug/plan")'),
57
57
  }, async ({ agent, task }, extra) => {
58
58
  const workspaceRoot = getWorkspaceRoot();
59
59
  let job;
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Core Tools - Extension info and handoff persistence
3
+ */
4
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
5
+ import { mcpText } from '../utils/utils.js';
6
+ import { DEFAULT_EXTENSIONS } from '../core/config/index.js';
7
+ import { HANDOFF_TYPES } from '../core/handoffs/index.js';
8
+ export declare function handleSaveHandoff(args: {
9
+ type: (typeof HANDOFF_TYPES)[number];
10
+ slug: string;
11
+ files: Record<string, string>;
12
+ }, workspaceRoot: string): ReturnType<typeof mcpText>;
13
+ /**
14
+ * Register core tools with MCP server
15
+ */
16
+ export declare function registerCoreTools(server: McpServer): void;
17
+ export { DEFAULT_EXTENSIONS };
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Core Tools - Extension info and handoff persistence
3
+ */
4
+ import * as fs from 'fs';
5
+ import * as path from 'path';
6
+ import { z } from 'zod';
7
+ import { getWorkspaceRoot, mcpText } from '../utils/utils.js';
8
+ import { DEFAULT_EXTENSIONS } from '../core/config/index.js';
9
+ import { HANDOFF_TYPES, resolveHandoffFolder, validateHandoffFilename } from '../core/handoffs/index.js';
10
+ export function handleSaveHandoff(args, workspaceRoot) {
11
+ try {
12
+ if (Object.keys(args.files).length === 0) {
13
+ return mcpText('Error saving handoff: files cannot be empty');
14
+ }
15
+ for (const name of Object.keys(args.files)) {
16
+ try {
17
+ validateHandoffFilename(name);
18
+ }
19
+ catch (err) {
20
+ return mcpText(`Error saving handoff: ${err instanceof Error ? err.message : err}`);
21
+ }
22
+ }
23
+ const primaryContent = args.files['README.md'] ?? Object.values(args.files)[0];
24
+ const location = resolveHandoffFolder({
25
+ workspaceRoot,
26
+ type: args.type,
27
+ slug: args.slug,
28
+ primaryContent,
29
+ });
30
+ fs.mkdirSync(location.folderPath, { recursive: true });
31
+ for (const [name, content] of Object.entries(args.files)) {
32
+ fs.writeFileSync(path.join(location.folderPath, name), content, 'utf8');
33
+ }
34
+ return mcpText(`✅ Saved to: ${location.folderPath}`);
35
+ }
36
+ catch (error) {
37
+ return mcpText(`Error saving handoff: ${error instanceof Error ? error.message : error}`);
38
+ }
39
+ }
40
+ /**
41
+ * Register core tools with MCP server
42
+ */
43
+ export function registerCoreTools(server) {
44
+ // ═══════════════════════════════════════════════════════════════
45
+ // TOOL: SAVE HANDOFF
46
+ // Writes handoff artifacts to the workspace as a folder of files
47
+ // Returns the saved folder path to use in next-step instructions
48
+ // ═══════════════════════════════════════════════════════════════
49
+ server.tool('kit_save_handoff', `Save a handoff artifact to .agent-kit/handoffs/. Returns the saved folder path to use in next-step instructions. Do NOT append version numbers (v2, v3, etc.) to the slug.`, {
50
+ type: z.enum(HANDOFF_TYPES).describe('Handoff type'),
51
+ files: z
52
+ .record(z.string(), z.string())
53
+ .describe('Markdown files to write into the handoff folder. May include one or many files per call; repeated calls with the same type and slug add or overwrite the provided files. Filenames must match /^[A-Za-z0-9_-]+\\.md$/.'),
54
+ slug: z
55
+ .string()
56
+ .describe('Short identifier for the folder, e.g. "user-auth" or "PROJ-123". Do NOT append version numbers (v2, v3, etc.).'),
57
+ }, async (args) => handleSaveHandoff(args, getWorkspaceRoot()));
58
+ }
59
+ // Export DEFAULT_EXTENSIONS for backward compatibility
60
+ export { DEFAULT_EXTENSIONS };
@@ -0,0 +1,98 @@
1
+ import * as assert from 'node:assert/strict';
2
+ import * as fs from 'node:fs';
3
+ import * as os from 'node:os';
4
+ import * as path from 'node:path';
5
+ import { afterEach, describe, test } from 'node:test';
6
+ import { handleSaveHandoff } from './core.js';
7
+ const tempDirs = [];
8
+ function makeTempWorkspace() {
9
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'core-test-'));
10
+ tempDirs.push(dir);
11
+ return dir;
12
+ }
13
+ afterEach(() => {
14
+ while (tempDirs.length > 0) {
15
+ const dir = tempDirs.pop();
16
+ if (dir)
17
+ fs.rmSync(dir, { recursive: true, force: true });
18
+ }
19
+ });
20
+ function resultText(result) {
21
+ return result.content[0].text;
22
+ }
23
+ describe('handleSaveHandoff', () => {
24
+ test('C1: README.md + DETAIL.md written under folder; returns folder path without .md', () => {
25
+ const workspace = makeTempWorkspace();
26
+ const result = handleSaveHandoff({ type: 'brainstorm', slug: 'auth', files: { 'README.md': '# Readme', 'DETAIL.md': '# Detail' } }, workspace);
27
+ const text = resultText(result);
28
+ assert.ok(text.startsWith('✅ Saved to:'), `expected success, got: ${text}`);
29
+ assert.ok(!text.endsWith('.md'), 'folder path must not end with .md');
30
+ const folderPath = text.replace('✅ Saved to: ', '');
31
+ assert.ok(fs.existsSync(path.join(folderPath, 'README.md')), 'README.md must exist');
32
+ assert.ok(fs.existsSync(path.join(folderPath, 'DETAIL.md')), 'DETAIL.md must exist');
33
+ assert.equal(fs.readFileSync(path.join(folderPath, 'README.md'), 'utf8'), '# Readme');
34
+ assert.equal(fs.readFileSync(path.join(folderPath, 'DETAIL.md'), 'utf8'), '# Detail');
35
+ });
36
+ test('C2: single README.md written under folder; returns folder path', () => {
37
+ const workspace = makeTempWorkspace();
38
+ const result = handleSaveHandoff({ type: 'ticket', slug: 'my-ticket', files: { 'README.md': '# Ticket content' } }, workspace);
39
+ const text = resultText(result);
40
+ assert.ok(text.startsWith('✅ Saved to:'), `expected success, got: ${text}`);
41
+ const folderPath = text.replace('✅ Saved to: ', '');
42
+ assert.ok(!text.endsWith('.md'), 'folder path must not end with .md');
43
+ assert.ok(fs.existsSync(path.join(folderPath, 'README.md')), 'README.md must exist');
44
+ assert.equal(fs.readFileSync(path.join(folderPath, 'README.md'), 'utf8'), '# Ticket content');
45
+ });
46
+ test('C3: empty files returns error containing "files cannot be empty"', () => {
47
+ const workspace = makeTempWorkspace();
48
+ const result = handleSaveHandoff({ type: 'plan', slug: 'x', files: {} }, workspace);
49
+ assert.match(resultText(result), /files cannot be empty/);
50
+ });
51
+ test('C4: path-traversal filename returns error containing "Unsafe handoff filename"', () => {
52
+ const workspace = makeTempWorkspace();
53
+ const result = handleSaveHandoff({ type: 'plan', slug: 'x', files: { '../escape.md': 'bad' } }, workspace);
54
+ assert.match(resultText(result), /Unsafe handoff filename/);
55
+ });
56
+ test('C5: filename without .md extension returns error containing "Unsafe handoff filename"', () => {
57
+ const workspace = makeTempWorkspace();
58
+ const result = handleSaveHandoff({ type: 'plan', slug: 'x', files: { README: 'bad' } }, workspace);
59
+ assert.match(resultText(result), /Unsafe handoff filename/);
60
+ });
61
+ test('C6: slug derived from README.md content ticket-ID when slug is empty', () => {
62
+ const workspace = makeTempWorkspace();
63
+ const result = handleSaveHandoff({ type: 'brainstorm', slug: '', files: { 'README.md': '# Fix PROJ-789\n\nDetails.' } }, workspace);
64
+ const text = resultText(result);
65
+ assert.ok(text.includes('proj-789'), `expected proj-789 in path, got: ${text}`);
66
+ });
67
+ test('C7: second call with same slug overwrites files without error (idempotent)', () => {
68
+ const workspace = makeTempWorkspace();
69
+ const args = { type: 'plan', slug: 'idem', files: { 'README.md': 'v1' } };
70
+ const r1 = handleSaveHandoff(args, workspace);
71
+ assert.ok(resultText(r1).startsWith('✅'), `first call failed: ${resultText(r1)}`);
72
+ const argsV2 = { type: 'plan', slug: 'idem', files: { 'README.md': 'v2' } };
73
+ const r2 = handleSaveHandoff(argsV2, workspace);
74
+ assert.ok(resultText(r2).startsWith('✅'), `second call failed: ${resultText(r2)}`);
75
+ const folderPath = resultText(r2).replace('✅ Saved to: ', '');
76
+ assert.equal(fs.readFileSync(path.join(folderPath, 'README.md'), 'utf8'), 'v2');
77
+ });
78
+ test('C8: code handoff writes REPORT.md and DECISIONS.md without README.md', () => {
79
+ const workspace = makeTempWorkspace();
80
+ const result = handleSaveHandoff({
81
+ type: 'code',
82
+ slug: 'execution-decision-ledger',
83
+ files: {
84
+ 'REPORT.md': '# Code Execution Report',
85
+ 'DECISIONS.md': '# Execution Decision Records',
86
+ },
87
+ }, workspace);
88
+ const text = resultText(result);
89
+ assert.ok(text.startsWith('✅ Saved to:'), `expected success, got: ${text}`);
90
+ const folderPath = text.replace('✅ Saved to: ', '');
91
+ assert.equal(folderPath, path.join(workspace, '.agent-kit', 'handoffs', 'execution-decision-ledger', 'code'));
92
+ assert.ok(fs.existsSync(path.join(folderPath, 'REPORT.md')), 'REPORT.md must exist');
93
+ assert.ok(fs.existsSync(path.join(folderPath, 'DECISIONS.md')), 'DECISIONS.md must exist');
94
+ assert.equal(fs.existsSync(path.join(folderPath, 'README.md')), false);
95
+ assert.equal(fs.readFileSync(path.join(folderPath, 'REPORT.md'), 'utf8'), '# Code Execution Report');
96
+ assert.equal(fs.readFileSync(path.join(folderPath, 'DECISIONS.md'), 'utf8'), '# Execution Decision Records');
97
+ });
98
+ });
@@ -4,10 +4,10 @@
4
4
  */
5
5
  import { writeFileSync } from 'fs';
6
6
  import { z } from 'zod';
7
- import { getCredential } from '../utils/credentials.js';
7
+ import { getCredential } from '../services/integration/credentials.js';
8
8
  import { mcpText } from '../utils/utils.js';
9
- import { adfToMarkdown } from '../utils/parser.js';
10
- import { sanitize } from './security.js';
9
+ import { adfToMarkdown } from '../services/integration/parser/adf.js';
10
+ import { sanitize } from '../core/security/index.js';
11
11
  // Zod schema for Bitbucket PR REST API response
12
12
  const BitbucketPrSchema = z.object({
13
13
  id: z.number(),
@@ -0,0 +1,15 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { MemoryIndexer } from '../services/memory/indexer.js';
3
+ import { MemoryStore } from '../services/memory/store.js';
4
+ import type { MemoryConfig } from '../services/memory/types.js';
5
+ import { type ProjectSettings } from '../core/config/index.js';
6
+ /**
7
+ * Initializes the memory subsystem and registers all memory tools.
8
+ * Returns the MemoryIndexer so the caller can fire startupIndex() after server.connect().
9
+ * Returns null when memory is disabled (settings.memory.enabled !== true).
10
+ */
11
+ export declare function registerMemoryTools(server: McpServer, settings: ProjectSettings, workspaceRoot: string, overrides?: {
12
+ indexer?: MemoryIndexer;
13
+ store?: MemoryStore;
14
+ config?: MemoryConfig;
15
+ }): MemoryIndexer | null;
@@ -1,17 +1,16 @@
1
1
  import * as path from 'path';
2
2
  import { z } from 'zod';
3
- import { Embedder } from '../memory/embedder.js';
4
- import { MemoryIndexer } from '../memory/indexer.js';
5
- import { MemoryStore } from '../memory/store.js';
6
- import { initializeConversationDigestModel } from '../memory/digest/processor.js';
7
- import { DEFAULT_DIGEST_MODEL_ID } from '../memory/digest/constants.js';
3
+ import { Embedder } from '../services/memory/embedder.js';
4
+ import { MemoryIndexer } from '../services/memory/indexer.js';
5
+ import { MemoryStore } from '../services/memory/store.js';
6
+ import { initializeConversationDigestModel } from '../services/digest/processor.js';
7
+ import { DEFAULT_DIGEST_MODEL_ID } from '../services/digest/constants.js';
8
8
  import { mcpJson, mcpText } from '../utils/utils.js';
9
- import { loadProjectSettings, resolveConversationDigestConfig, resolveMemoryConfig, } from './config.js';
9
+ import { loadProjectSettings, resolveConversationDigestConfig, resolveMemoryConfig, } from '../core/config/index.js';
10
10
  /**
11
11
  * Registers tool handlers onto an already-constructed indexer/store pair.
12
- * Exported for unit testing with mocks.
13
12
  */
14
- export function registerMemoryToolHandlers(server, indexer, store, config, workspaceRoot = process.cwd()) {
13
+ function registerMemoryToolHandlers(server, indexer, store, config, workspaceRoot = process.cwd()) {
15
14
  server.tool('kit_memory_search', 'Search persistent memory for context relevant to the current question. Returns semantically relevant results using hybrid dense+BM25+RRF search.', {
16
15
  query: z.string().min(1).describe('Search query'),
17
16
  top_k: z.number().int().positive().optional().describe('Number of results to return'),
@@ -89,13 +88,16 @@ export function registerMemoryToolHandlers(server, indexer, store, config, works
89
88
  * Returns the MemoryIndexer so the caller can fire startupIndex() after server.connect().
90
89
  * Returns null when memory is disabled (settings.memory.enabled !== true).
91
90
  */
92
- export function registerMemoryTools(server, settings, workspaceRoot) {
91
+ export function registerMemoryTools(server, settings, workspaceRoot, overrides) {
93
92
  if (settings.memory?.enabled !== true)
94
93
  return null;
95
- const config = resolveMemoryConfig(settings, workspaceRoot);
96
- const store = new MemoryStore(path.join(config.wikiDir, 'index.db'), config);
97
- const embedder = new Embedder(config.embeddingModel);
98
- const indexer = new MemoryIndexer(store, embedder, config);
94
+ const config = overrides?.config ?? resolveMemoryConfig(settings, workspaceRoot);
95
+ const store = overrides?.store ?? new MemoryStore(path.join(config.wikiDir, 'index.db'), config);
96
+ const indexer = overrides?.indexer ??
97
+ (() => {
98
+ const embedder = new Embedder(config.embeddingModel);
99
+ return new MemoryIndexer(store, embedder, config);
100
+ })();
99
101
  registerMemoryToolHandlers(server, indexer, store, config, workspaceRoot);
100
102
  return indexer;
101
103
  }