@hanhnd/agent-kit 1.0.31 → 1.0.32

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 (75) hide show
  1. package/dist/__tests__/temp-dir.d.ts +4 -0
  2. package/dist/__tests__/temp-dir.js +20 -0
  3. package/dist/cli/index.d.ts +1 -0
  4. package/dist/cli/index.js +9 -0
  5. package/dist/cli/memory.d.ts +1 -0
  6. package/dist/cli/memory.js +77 -0
  7. package/dist/kit-cli.d.ts +2 -0
  8. package/dist/kit-cli.js +16205 -0
  9. package/dist/kit-server.js +679 -193
  10. package/dist/memory/__tests__/digest-llama-local.test.d.ts +1 -0
  11. package/dist/memory/__tests__/digest-llama-local.test.js +33 -0
  12. package/dist/memory/__tests__/digest-pending.test.d.ts +1 -0
  13. package/dist/memory/__tests__/digest-pending.test.js +164 -0
  14. package/dist/memory/__tests__/digest-processor.test.d.ts +1 -0
  15. package/dist/memory/__tests__/digest-processor.test.js +34 -0
  16. package/dist/memory/__tests__/digest-worker.test.d.ts +1 -0
  17. package/dist/memory/__tests__/digest-worker.test.js +46 -0
  18. package/dist/memory/__tests__/indexer.test.js +7 -12
  19. package/dist/memory/__tests__/lockfile.test.d.ts +1 -0
  20. package/dist/memory/__tests__/lockfile.test.js +81 -0
  21. package/dist/memory/__tests__/store.test.js +15 -3
  22. package/dist/memory/chunker.js +3 -5
  23. package/dist/memory/constants.d.ts +5 -0
  24. package/dist/memory/constants.js +12 -0
  25. package/dist/memory/digest/constants.d.ts +14 -0
  26. package/dist/memory/digest/constants.js +15 -0
  27. package/dist/memory/digest/files.d.ts +7 -0
  28. package/dist/memory/digest/files.js +45 -0
  29. package/dist/memory/digest/model-registry.d.ts +6 -0
  30. package/dist/memory/digest/model-registry.js +48 -0
  31. package/dist/memory/digest/processor.d.ts +7 -0
  32. package/dist/memory/digest/processor.js +171 -0
  33. package/dist/memory/digest/providers/llama-local.d.ts +7 -0
  34. package/dist/memory/digest/providers/llama-local.js +136 -0
  35. package/dist/memory/digest/types.d.ts +85 -0
  36. package/dist/memory/digest/types.js +1 -0
  37. package/dist/memory/digest/worker.d.ts +3 -0
  38. package/dist/memory/digest/worker.js +51 -0
  39. package/dist/memory/embedder.d.ts +1 -0
  40. package/dist/memory/embedder.js +10 -1
  41. package/dist/memory/index.d.ts +1 -1
  42. package/dist/memory/index.js +1 -1
  43. package/dist/memory/indexer.d.ts +5 -2
  44. package/dist/memory/indexer.js +15 -37
  45. package/dist/memory/store.js +6 -18
  46. package/dist/memory/types.d.ts +2 -1
  47. package/dist/memory/types.js +1 -9
  48. package/dist/tools/__tests__/cli-digest-pending.test.d.ts +1 -0
  49. package/dist/tools/__tests__/cli-digest-pending.test.js +51 -0
  50. package/dist/tools/__tests__/cli-memory.test.d.ts +1 -0
  51. package/dist/tools/__tests__/cli-memory.test.js +22 -0
  52. package/dist/tools/__tests__/digest-init.test.d.ts +1 -0
  53. package/dist/tools/__tests__/digest-init.test.js +32 -0
  54. package/dist/tools/__tests__/memory.test.js +28 -6
  55. package/dist/tools/agent.js +2 -13
  56. package/dist/tools/config.d.ts +4 -4
  57. package/dist/tools/config.js +20 -12
  58. package/dist/tools/core.js +1 -3
  59. package/dist/tools/handoffs.js +1 -2
  60. package/dist/tools/integration.js +2 -8
  61. package/dist/tools/memory.d.ts +1 -1
  62. package/dist/tools/memory.js +29 -3
  63. package/dist/tools/security.js +2 -6
  64. package/dist/utils/args.d.ts +7 -0
  65. package/dist/utils/args.js +31 -0
  66. package/dist/utils/async.d.ts +1 -0
  67. package/dist/utils/async.js +15 -0
  68. package/dist/utils/files.d.ts +8 -0
  69. package/dist/utils/files.js +77 -0
  70. package/dist/utils/hash.d.ts +1 -0
  71. package/dist/utils/hash.js +4 -0
  72. package/dist/utils/json.d.ts +2 -0
  73. package/dist/utils/json.js +13 -0
  74. package/dist/utils/parser.js +2 -6
  75. package/package.json +7 -5
@@ -141,27 +141,19 @@ export class MemoryStore {
141
141
  }
142
142
  }
143
143
  hashesBySource(source) {
144
- const rows = this.db
145
- .prepare(`SELECT id FROM memory_chunks WHERE source = ?`)
146
- .all(source);
144
+ const rows = this.db.prepare(`SELECT id FROM memory_chunks WHERE source = ?`).all(source);
147
145
  return new Set(rows.map((r) => r.id));
148
146
  }
149
147
  indexedSources() {
150
- const rows = this.db
151
- .prepare(`SELECT DISTINCT source FROM memory_chunks`)
152
- .all();
148
+ const rows = this.db.prepare(`SELECT DISTINCT source FROM memory_chunks`).all();
153
149
  return rows.map((r) => r.source);
154
150
  }
155
151
  deleteBySource(source) {
156
- const rows = this.db
157
- .prepare(`SELECT rowid FROM memory_chunks WHERE source = ?`)
158
- .all(source);
152
+ const rows = this.db.prepare(`SELECT rowid FROM memory_chunks WHERE source = ?`).all(source);
159
153
  const deleteAll = this.db.transaction(() => {
160
154
  if (this._vecAvailable && rows.length > 0) {
161
155
  const placeholders = rows.map(() => '?').join(', ');
162
- this.db
163
- .prepare(`DELETE FROM memory_vec WHERE rowid IN (${placeholders})`)
164
- .run(...rows.map((r) => r.rowid));
156
+ this.db.prepare(`DELETE FROM memory_vec WHERE rowid IN (${placeholders})`).run(...rows.map((r) => r.rowid));
165
157
  }
166
158
  this.db.prepare(`DELETE FROM memory_chunks WHERE source = ?`).run(source);
167
159
  });
@@ -171,15 +163,11 @@ export class MemoryStore {
171
163
  if (ids.length === 0)
172
164
  return;
173
165
  const placeholders = ids.map(() => '?').join(', ');
174
- const rows = this.db
175
- .prepare(`SELECT rowid FROM memory_chunks WHERE id IN (${placeholders})`)
176
- .all(...ids);
166
+ const rows = this.db.prepare(`SELECT rowid FROM memory_chunks WHERE id IN (${placeholders})`).all(...ids);
177
167
  const deleteAll = this.db.transaction(() => {
178
168
  if (this._vecAvailable && rows.length > 0) {
179
169
  const rPlaceholders = rows.map(() => '?').join(', ');
180
- this.db
181
- .prepare(`DELETE FROM memory_vec WHERE rowid IN (${rPlaceholders})`)
182
- .run(...rows.map((r) => r.rowid));
170
+ this.db.prepare(`DELETE FROM memory_vec WHERE rowid IN (${rPlaceholders})`).run(...rows.map((r) => r.rowid));
183
171
  }
184
172
  this.db.prepare(`DELETE FROM memory_chunks WHERE id IN (${placeholders})`).run(...ids);
185
173
  });
@@ -1,3 +1,4 @@
1
+ import type { ConversationDigestSettings } from './digest/types.js';
1
2
  export interface MemoryChunk {
2
3
  id: string;
3
4
  source: string;
@@ -21,10 +22,10 @@ export interface MemoryConfig {
21
22
  overlapLines: number;
22
23
  embeddingModel: string;
23
24
  vectorDimension: number;
25
+ conversationDigest?: ConversationDigestSettings;
24
26
  }
25
27
  export interface IndexStats {
26
28
  indexed: number;
27
29
  deleted: number;
28
30
  skipped: number;
29
31
  }
30
- export declare const DEFAULT_MEMORY_CONFIG: MemoryConfig;
@@ -1,9 +1 @@
1
- export const DEFAULT_MEMORY_CONFIG = {
2
- enabled: false,
3
- wikiDir: '',
4
- topK: 5,
5
- chunkSize: 1500,
6
- overlapLines: 2,
7
- embeddingModel: 'Xenova/bge-small-en-v1.5',
8
- vectorDimension: 384,
9
- };
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,51 @@
1
+ import * as assert from 'node:assert/strict';
2
+ import { afterEach, describe, test } from 'node:test';
3
+ import { createTempDirTracker } from '../../__tests__/temp-dir.js';
4
+ import { runMemoryCli } from '../../cli/memory.js';
5
+ const tempDirs = createTempDirTracker();
6
+ afterEach(() => {
7
+ tempDirs.cleanup();
8
+ });
9
+ describe('cmdDigestPending CLI', () => {
10
+ test('C22: no --hook flag → writes usage to stderr and returns 1', async () => {
11
+ const stderrChunks = [];
12
+ const origWrite = process.stderr.write.bind(process.stderr);
13
+ process.stderr.write = (chunk) => {
14
+ stderrChunks.push(String(chunk));
15
+ return true;
16
+ };
17
+ let code;
18
+ try {
19
+ code = await runMemoryCli(['digest-pending'], process.env);
20
+ }
21
+ finally {
22
+ process.stderr.write = origWrite;
23
+ }
24
+ assert.equal(code, 1);
25
+ assert.ok(stderrChunks.some((s) => s.includes('Usage')));
26
+ });
27
+ test('C23: --hook returns 0 and emits one JSON line matching DigestPendingResult', async () => {
28
+ const workspace = tempDirs.makeTempDir('cli-pending-c23-');
29
+ const stdoutChunks = [];
30
+ const origWrite = process.stdout.write.bind(process.stdout);
31
+ process.stdout.write = (chunk) => {
32
+ stdoutChunks.push(String(chunk));
33
+ return true;
34
+ };
35
+ let code;
36
+ try {
37
+ code = await runMemoryCli(['digest-pending', '--hook'], {
38
+ ...process.env,
39
+ WORKSPACE_DIR: workspace,
40
+ });
41
+ }
42
+ finally {
43
+ process.stdout.write = origWrite;
44
+ }
45
+ assert.equal(code, 0);
46
+ const output = stdoutChunks.join('');
47
+ const parsed = JSON.parse(output.trim());
48
+ assert.ok('ok' in parsed, 'result must have ok field');
49
+ assert.ok('action' in parsed, 'result must have action field');
50
+ });
51
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,22 @@
1
+ import * as assert from 'node:assert/strict';
2
+ import { afterEach, describe, test } from 'node:test';
3
+ import { createTempDirTracker } from '../../__tests__/temp-dir.js';
4
+ import { runMemoryCli } from '../../cli/memory.js';
5
+ const tempDirs = createTempDirTracker();
6
+ afterEach(() => {
7
+ tempDirs.cleanup();
8
+ });
9
+ describe('memory CLI', () => {
10
+ test('digest-pending --hook returns 0 when settings are missing', async () => {
11
+ const workspace = tempDirs.makeTempDir('digest-pending-');
12
+ const code = await runMemoryCli(['digest-pending', '--hook'], {
13
+ ...process.env,
14
+ WORKSPACE_DIR: workspace,
15
+ });
16
+ assert.equal(code, 0);
17
+ });
18
+ test('unknown command returns 1', async () => {
19
+ const code = await runMemoryCli(['unknown'], process.env);
20
+ assert.equal(code, 1);
21
+ });
22
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,32 @@
1
+ import * as assert from 'node:assert/strict';
2
+ import * as fs from 'node:fs';
3
+ import * as path from 'node:path';
4
+ import { afterEach, describe, test } from 'node:test';
5
+ import { createTempDirTracker } from '../../__tests__/temp-dir.js';
6
+ import { initializeConversationDigestModel } from '../../memory/digest/processor.js';
7
+ const tempDirs = createTempDirTracker();
8
+ afterEach(() => {
9
+ tempDirs.cleanup();
10
+ });
11
+ describe('initializeConversationDigestModel', () => {
12
+ test('rejects unknown model and leaves settings absent', async () => {
13
+ const workspace = tempDirs.makeTempDir('digest-init-');
14
+ const result = await initializeConversationDigestModel({
15
+ workspaceRoot: workspace,
16
+ modelId: 'unknown-model',
17
+ allowDownload: true,
18
+ });
19
+ assert.equal(result.initialized, false);
20
+ assert.equal(fs.existsSync(path.join(workspace, '.agent-kit', 'settings.json')), false);
21
+ });
22
+ test('requires explicit download permission and leaves settings absent', async () => {
23
+ const workspace = tempDirs.makeTempDir('digest-init-');
24
+ const result = await initializeConversationDigestModel({
25
+ workspaceRoot: workspace,
26
+ modelId: 'qwen2.5-1.5b-instruct-q4',
27
+ allowDownload: false,
28
+ });
29
+ assert.equal(result.initialized, false);
30
+ assert.equal(fs.existsSync(path.join(workspace, '.agent-kit', 'settings.json')), false);
31
+ });
32
+ });
@@ -48,8 +48,16 @@ function extractText(result) {
48
48
  }
49
49
  describe('kit_memory_search', () => {
50
50
  test('returns formatted full content and display source when results exist', async () => {
51
- const chunk = makeChunk({ content: 'Important memory content', source: 'compiled/entities/foo.md' });
52
- const searchResult = { chunk, score: 0.85, retriever: 'bm25', contentSource: 'file' };
51
+ const chunk = makeChunk({
52
+ content: 'Important memory content',
53
+ source: 'compiled/entities/foo.md',
54
+ });
55
+ const searchResult = {
56
+ chunk,
57
+ score: 0.85,
58
+ retriever: 'bm25',
59
+ contentSource: 'file',
60
+ };
53
61
  const indexer = makeIndexerStub({ search: async () => [searchResult] });
54
62
  const store = makeStoreStub(true);
55
63
  const { server, tools } = makeMockServer();
@@ -81,7 +89,12 @@ describe('kit_memory_search', () => {
81
89
  });
82
90
  test('prepends degraded warning when vecAvailable is false with results', async () => {
83
91
  const chunk = makeChunk({ content: 'Found content', source: 'test.md' });
84
- const searchResult = { chunk, score: 0.5, retriever: 'bm25', contentSource: 'file' };
92
+ const searchResult = {
93
+ chunk,
94
+ score: 0.5,
95
+ retriever: 'bm25',
96
+ contentSource: 'file',
97
+ };
85
98
  const indexer = makeIndexerStub({ search: async () => [searchResult] });
86
99
  const store = makeStoreStub(false);
87
100
  const { server, tools } = makeMockServer();
@@ -93,7 +106,12 @@ describe('kit_memory_search', () => {
93
106
  });
94
107
  test('prepends source-unavailable warning for fallback results', async () => {
95
108
  const chunk = makeChunk({ content: 'Stored chunk only', source: 'compiled/entities/foo.md' });
96
- const searchResult = { chunk, score: 0.5, retriever: 'bm25', contentSource: 'fallback' };
109
+ const searchResult = {
110
+ chunk,
111
+ score: 0.5,
112
+ retriever: 'bm25',
113
+ contentSource: 'fallback',
114
+ };
97
115
  const indexer = makeIndexerStub({ search: async () => [searchResult] });
98
116
  const store = makeStoreStub(true);
99
117
  const { server, tools } = makeMockServer();
@@ -133,7 +151,9 @@ describe('kit_memory_search', () => {
133
151
  });
134
152
  test('returns error message when indexer.search throws', async () => {
135
153
  const indexer = makeIndexerStub({
136
- search: async () => { throw new Error('search exploded'); },
154
+ search: async () => {
155
+ throw new Error('search exploded');
156
+ },
137
157
  });
138
158
  const store = makeStoreStub(true);
139
159
  const { server, tools } = makeMockServer();
@@ -161,7 +181,9 @@ describe('kit_memory_save', () => {
161
181
  });
162
182
  test('returns saved:false and error message when indexer.save throws', async () => {
163
183
  const indexer = makeIndexerStub({
164
- save: async () => { throw new Error('disk full'); },
184
+ save: async () => {
185
+ throw new Error('disk full');
186
+ },
165
187
  });
166
188
  const store = makeStoreStub(true);
167
189
  const { server, tools } = makeMockServer();
@@ -24,16 +24,7 @@ function agentArgs(agent, prompt, workspaceRoot) {
24
24
  case 'claude':
25
25
  return ['--dangerously-skip-permissions', '-p', prompt];
26
26
  case 'codex':
27
- return [
28
- '--ask-for-approval',
29
- 'never',
30
- '--sandbox',
31
- 'workspace-write',
32
- '--cd',
33
- workspaceRoot,
34
- 'exec',
35
- prompt,
36
- ];
27
+ return ['--ask-for-approval', 'never', '--sandbox', 'workspace-write', '--cd', workspaceRoot, 'exec', prompt];
37
28
  }
38
29
  }
39
30
  function taskSlug(task) {
@@ -59,9 +50,7 @@ function initAgentLog(workspaceRoot, slug, agent) {
59
50
  }
60
51
  export function registerAgentTools(server) {
61
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).', {
62
- agent: z
63
- .enum(['gemini', 'claude', 'codex'])
64
- .describe('The agent CLI to invoke: "gemini", "claude", or "codex"'),
53
+ agent: z.enum(['gemini', 'claude', 'codex']).describe('The agent CLI to invoke: "gemini", "claude", or "codex"'),
65
54
  task: z
66
55
  .string()
67
56
  .describe('Task message or path to a handoff file (e.g., ".agent-kit/handoffs/feature-slug/plan.md")'),
@@ -2,7 +2,8 @@
2
2
  * Configuration utilities
3
3
  * Shared configuration constants and loaders
4
4
  */
5
- import { type MemoryConfig } from '../memory/types.js';
5
+ import type { MemoryConfig } from '../memory/types.js';
6
+ import type { ConversationDigestSettings } from '../memory/digest/types.js';
6
7
  /**
7
8
  * Default file extensions - expanded to support more languages
8
9
  * Used by kit_get_project_context and other tools
@@ -26,10 +27,9 @@ export declare function resolveMemoryConfig(settings: ProjectSettings, workspace
26
27
  * Allows project-specific customization
27
28
  */
28
29
  export declare function getFileExtensions(projectDir: string): string[];
29
- /**
30
- * Load project settings from .gemini/settings.json
31
- */
32
30
  export declare function loadProjectSettings(projectDir: string): ProjectSettings;
31
+ export declare function writeProjectSettings(workspaceRoot: string, patch: Partial<ProjectSettings>): void;
32
+ export declare function resolveConversationDigestConfig(settings: ProjectSettings): ConversationDigestSettings | undefined;
33
33
  /**
34
34
  * Sensitive files that agents are FORBIDDEN from reading/writing.
35
35
  */
@@ -4,7 +4,8 @@
4
4
  */
5
5
  import * as fs from 'fs';
6
6
  import * as path from 'path';
7
- import { DEFAULT_MEMORY_CONFIG } from '../memory/types.js';
7
+ import { DEFAULT_MEMORY_CONFIG } from '../memory/constants.js';
8
+ import { atomicWriteJsonFile } from '../utils/files.js';
8
9
  /**
9
10
  * Default file extensions - expanded to support more languages
10
11
  * Used by kit_get_project_context and other tools
@@ -58,9 +59,6 @@ export function getFileExtensions(projectDir) {
58
59
  }
59
60
  return DEFAULT_EXTENSIONS;
60
61
  }
61
- /**
62
- * Load project settings from .gemini/settings.json
63
- */
64
62
  export function loadProjectSettings(projectDir) {
65
63
  const settingsPath = path.join(projectDir, '.agent-kit', 'settings.json');
66
64
  if (fs.existsSync(settingsPath)) {
@@ -73,6 +71,23 @@ export function loadProjectSettings(projectDir) {
73
71
  }
74
72
  return {};
75
73
  }
74
+ export function writeProjectSettings(workspaceRoot, patch) {
75
+ const settingsPath = path.join(workspaceRoot, '.agent-kit', 'settings.json');
76
+ const current = loadProjectSettings(workspaceRoot);
77
+ atomicWriteJsonFile(settingsPath, { ...current, ...patch });
78
+ }
79
+ export function resolveConversationDigestConfig(settings) {
80
+ const digest = settings.memory?.conversationDigest;
81
+ if (!digest?.initialized || typeof digest.modelId !== 'string' || typeof digest.initializedAt !== 'string') {
82
+ return undefined;
83
+ }
84
+ return {
85
+ enabled: digest.enabled !== false,
86
+ initialized: true,
87
+ modelId: digest.modelId,
88
+ initializedAt: digest.initializedAt,
89
+ };
90
+ }
76
91
  /**
77
92
  * Sensitive files that agents are FORBIDDEN from reading/writing.
78
93
  */
@@ -108,11 +123,4 @@ export const FORBIDDEN_PATTERNS = [
108
123
  /**
109
124
  * Directory name segments that are always blocked.
110
125
  */
111
- export const FORBIDDEN_DIRS = [
112
- '.git',
113
- '.ssh',
114
- '.aws',
115
- '.kube',
116
- '.gnupg',
117
- '.docker',
118
- ];
126
+ export const FORBIDDEN_DIRS = ['.git', '.ssh', '.aws', '.kube', '.gnupg', '.docker'];
@@ -17,9 +17,7 @@ export function registerCoreTools(server) {
17
17
  // Returns the saved file path to use in next-step instructions
18
18
  // ═══════════════════════════════════════════════════════════════
19
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.`, {
20
- type: z
21
- .enum(HANDOFF_TYPES)
22
- .describe('Handoff type'),
20
+ type: z.enum(HANDOFF_TYPES).describe('Handoff type'),
23
21
  content: z.string().describe('Full markdown content to save'),
24
22
  slug: z
25
23
  .string()
@@ -42,8 +42,7 @@ export function deriveFeatureSlug(input) {
42
42
  const contentTicketSlug = findTicketId(input.content);
43
43
  if (contentTicketSlug)
44
44
  return contentTicketSlug;
45
- return (sanitizeFeatureSlug(contentSlugCandidate(input.content)) ||
46
- 'untitled-handoff');
45
+ return sanitizeFeatureSlug(contentSlugCandidate(input.content)) || 'untitled-handoff';
47
46
  }
48
47
  export function resolveHandoffPath(input) {
49
48
  const canonicalType = normalizeHandoffType(input.type);
@@ -81,11 +81,7 @@ export function registerIntegrationTools(server) {
81
81
  .optional()
82
82
  .describe('Bitbucket workspace slug (required for numeric ID if BITBUCKET_DEFAULT_WORKSPACE not set)'),
83
83
  repoSlug: z.string().optional().describe('Bitbucket repo slug (required for numeric ID)'),
84
- includeDiff: z
85
- .boolean()
86
- .optional()
87
- .default(true)
88
- .describe('Include unified diff in response'),
84
+ includeDiff: z.boolean().optional().default(true).describe('Include unified diff in response'),
89
85
  }, async ({ input, workspace, repoSlug, includeDiff }) => {
90
86
  try {
91
87
  let ws;
@@ -180,9 +176,7 @@ ${pr.description || 'No description'}`;
180
176
  **Type:** ${ticket.fields.issuetype?.name || 'Unknown'}
181
177
 
182
178
  ### Description
183
- ${typeof ticket.fields.description === 'string'
184
- ? ticket.fields.description
185
- : adfToMarkdown(ticket.fields.description)}
179
+ ${typeof ticket.fields.description === 'string' ? ticket.fields.description : adfToMarkdown(ticket.fields.description)}
186
180
 
187
181
  ### Labels
188
182
  ${ticket.fields.labels?.join(', ') || 'None'}`;
@@ -7,7 +7,7 @@ import { type ProjectSettings } from './config.js';
7
7
  * Registers tool handlers onto an already-constructed indexer/store pair.
8
8
  * Exported for unit testing with mocks.
9
9
  */
10
- export declare function registerMemoryToolHandlers(server: McpServer, indexer: MemoryIndexer, store: MemoryStore, config: MemoryConfig): void;
10
+ export declare function registerMemoryToolHandlers(server: McpServer, indexer: MemoryIndexer, store: MemoryStore, config: MemoryConfig, workspaceRoot?: string): void;
11
11
  /**
12
12
  * Initializes the memory subsystem and registers all memory tools.
13
13
  * Returns the MemoryIndexer so the caller can fire startupIndex() after server.connect().
@@ -3,13 +3,15 @@ import { z } from 'zod';
3
3
  import { Embedder } from '../memory/embedder.js';
4
4
  import { MemoryIndexer } from '../memory/indexer.js';
5
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';
6
8
  import { mcpJson, mcpText } from '../utils/utils.js';
7
- import { resolveMemoryConfig } from './config.js';
9
+ import { loadProjectSettings, resolveConversationDigestConfig, resolveMemoryConfig, } from './config.js';
8
10
  /**
9
11
  * Registers tool handlers onto an already-constructed indexer/store pair.
10
12
  * Exported for unit testing with mocks.
11
13
  */
12
- export function registerMemoryToolHandlers(server, indexer, store, config) {
14
+ export function registerMemoryToolHandlers(server, indexer, store, config, workspaceRoot = process.cwd()) {
13
15
  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.', {
14
16
  query: z.string().min(1).describe('Search query'),
15
17
  top_k: z.number().int().positive().optional().describe('Number of results to return'),
@@ -57,6 +59,30 @@ export function registerMemoryToolHandlers(server, indexer, store, config) {
57
59
  return mcpJson({ saved: false, error: message });
58
60
  }
59
61
  });
62
+ server.tool('kit_memory_digest_init', 'Initialize or toggle local conversation digesting. Downloads/cache-loads the pinned local model only when explicitly invoked.', {
63
+ model_id: z.string().optional().describe('Digest model id'),
64
+ enabled: z.boolean().optional().describe('Enable or disable conversation digesting'),
65
+ }, async ({ model_id, enabled }) => {
66
+ try {
67
+ const existingConfig = resolveConversationDigestConfig(loadProjectSettings(workspaceRoot));
68
+ const result = await initializeConversationDigestModel({
69
+ workspaceRoot,
70
+ modelId: model_id ?? existingConfig?.modelId ?? DEFAULT_DIGEST_MODEL_ID,
71
+ allowDownload: true,
72
+ enabled,
73
+ });
74
+ return mcpJson({
75
+ initialized: result.initialized,
76
+ model_id: result.modelId,
77
+ initialized_at: result.initializedAt,
78
+ error: result.error,
79
+ });
80
+ }
81
+ catch (err) {
82
+ const message = err instanceof Error ? err.message : String(err);
83
+ return mcpJson({ initialized: false, error: message });
84
+ }
85
+ });
60
86
  }
61
87
  /**
62
88
  * Initializes the memory subsystem and registers all memory tools.
@@ -70,6 +96,6 @@ export function registerMemoryTools(server, settings, workspaceRoot) {
70
96
  const store = new MemoryStore(path.join(config.wikiDir, 'index.db'), config);
71
97
  const embedder = new Embedder(config.embeddingModel);
72
98
  const indexer = new MemoryIndexer(store, embedder, config);
73
- registerMemoryToolHandlers(server, indexer, store, config);
99
+ registerMemoryToolHandlers(server, indexer, store, config, workspaceRoot);
74
100
  return indexer;
75
101
  }
@@ -110,9 +110,7 @@ export function commandExists(cmd) {
110
110
  export async function findFilesAsync(dir, extensions, maxFiles, excludeDirs = ['node_modules', '.git', 'dist', 'build', 'coverage']) {
111
111
  const results = [];
112
112
  // Use queue instead of recursion to prevent stack overflow
113
- const queue = [
114
- { fullPath: dir, relativePath: '' },
115
- ];
113
+ const queue = [{ fullPath: dir, relativePath: '' }];
116
114
  while (queue.length > 0 && results.length < maxFiles) {
117
115
  const current = queue.shift();
118
116
  let entries;
@@ -126,9 +124,7 @@ export async function findFilesAsync(dir, extensions, maxFiles, excludeDirs = ['
126
124
  if (results.length >= maxFiles)
127
125
  break;
128
126
  const entryFullPath = path.join(current.fullPath, entry.name);
129
- const entryRelPath = current.relativePath
130
- ? path.join(current.relativePath, entry.name)
131
- : entry.name;
127
+ const entryRelPath = current.relativePath ? path.join(current.relativePath, entry.name) : entry.name;
132
128
  if (entry.isDirectory()) {
133
129
  if (!excludeDirs.includes(entry.name)) {
134
130
  queue.push({ fullPath: entryFullPath, relativePath: entryRelPath });
@@ -0,0 +1,7 @@
1
+ export interface ParsedArgs {
2
+ flags: Map<string, string | boolean>;
3
+ positionals: string[];
4
+ }
5
+ export declare function parseArgs(args: string[]): ParsedArgs;
6
+ export declare function stringFlag(flags: Map<string, string | boolean>, key: string): string | undefined;
7
+ export declare function numberFlag(flags: Map<string, string | boolean>, key: string, defaultValue: number | string): number;
@@ -0,0 +1,31 @@
1
+ export function parseArgs(args) {
2
+ const flags = new Map();
3
+ const positionals = [];
4
+ for (let index = 0; index < args.length; index += 1) {
5
+ const arg = args[index];
6
+ if (!arg.startsWith('--')) {
7
+ positionals.push(arg);
8
+ continue;
9
+ }
10
+ const key = arg.slice(2);
11
+ const next = args[index + 1];
12
+ if (!next || next.startsWith('--')) {
13
+ flags.set(key, true);
14
+ continue;
15
+ }
16
+ flags.set(key, next);
17
+ index += 1;
18
+ }
19
+ return { flags, positionals };
20
+ }
21
+ export function stringFlag(flags, key) {
22
+ const value = flags.get(key);
23
+ return typeof value === 'string' ? value : undefined;
24
+ }
25
+ export function numberFlag(flags, key, defaultValue) {
26
+ const value = Number(stringFlag(flags, key) ?? defaultValue);
27
+ if (!Number.isFinite(value)) {
28
+ throw new Error(`--${key} must be a finite number`);
29
+ }
30
+ return value;
31
+ }
@@ -0,0 +1 @@
1
+ export declare function withTimeout<T>(promise: Promise<T>, timeoutMs: number, createError: () => Error): Promise<T>;
@@ -0,0 +1,15 @@
1
+ export async function withTimeout(promise, timeoutMs, createError) {
2
+ let timer;
3
+ const timeout = new Promise((_resolve, reject) => {
4
+ timer = setTimeout(() => {
5
+ reject(createError());
6
+ }, timeoutMs);
7
+ });
8
+ try {
9
+ return await Promise.race([promise, timeout]);
10
+ }
11
+ finally {
12
+ if (timer)
13
+ clearTimeout(timer);
14
+ }
15
+ }
@@ -0,0 +1,8 @@
1
+ export declare function atomicWriteTextFile(filePath: string, content: string): void;
2
+ export declare function atomicWriteJsonFile(filePath: string, value: unknown): void;
3
+ export declare function tryWriteFileExclusive(filePath: string, content: string): boolean;
4
+ export declare function acquireLock(lockPath: string, timeoutMs: number, retryMs: number): Promise<boolean>;
5
+ export declare function releaseLock(lockPath: string, opts?: {
6
+ ignoreErrors?: boolean;
7
+ }): void;
8
+ export declare function tryAcquireProcessLock(lockPath: string): boolean;
@@ -0,0 +1,77 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ export function atomicWriteTextFile(filePath, content) {
4
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
5
+ const tempPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
6
+ fs.writeFileSync(tempPath, content, 'utf8');
7
+ fs.renameSync(tempPath, filePath);
8
+ }
9
+ export function atomicWriteJsonFile(filePath, value) {
10
+ atomicWriteTextFile(filePath, `${JSON.stringify(value, null, 2)}\n`);
11
+ }
12
+ export function tryWriteFileExclusive(filePath, content) {
13
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
14
+ try {
15
+ fs.writeFileSync(filePath, content, { flag: 'wx' });
16
+ return true;
17
+ }
18
+ catch (err) {
19
+ if (err.code === 'EEXIST')
20
+ return false;
21
+ throw err;
22
+ }
23
+ }
24
+ export async function acquireLock(lockPath, timeoutMs, retryMs) {
25
+ const deadline = Date.now() + timeoutMs;
26
+ while (Date.now() < deadline) {
27
+ try {
28
+ if (tryWriteFileExclusive(lockPath, String(process.pid)))
29
+ return true;
30
+ }
31
+ catch {
32
+ // Preserve fail-open retry behavior for transient lock-file write errors.
33
+ }
34
+ await new Promise((r) => setTimeout(r, retryMs));
35
+ }
36
+ return false;
37
+ }
38
+ export function releaseLock(lockPath, opts = {}) {
39
+ try {
40
+ fs.unlinkSync(lockPath);
41
+ }
42
+ catch (err) {
43
+ if (opts.ignoreErrors || err.code === 'ENOENT')
44
+ return;
45
+ throw err;
46
+ }
47
+ }
48
+ export function tryAcquireProcessLock(lockPath) {
49
+ const lockContent = JSON.stringify({ pid: process.pid });
50
+ if (tryWriteFileExclusive(lockPath, lockContent))
51
+ return true;
52
+ let pid;
53
+ try {
54
+ const raw = fs.readFileSync(lockPath, 'utf8');
55
+ pid = JSON.parse(raw).pid;
56
+ }
57
+ catch {
58
+ // Unparseable lockfiles are treated as stale.
59
+ }
60
+ if (pid !== undefined) {
61
+ try {
62
+ process.kill(pid, 0);
63
+ return false;
64
+ }
65
+ catch (err) {
66
+ if (err.code !== 'ESRCH')
67
+ return false;
68
+ }
69
+ }
70
+ try {
71
+ fs.unlinkSync(lockPath);
72
+ }
73
+ catch {
74
+ // Ignore if another process already removed the stale lock.
75
+ }
76
+ return tryWriteFileExclusive(lockPath, lockContent);
77
+ }