@hanhnd/agent-kit 1.0.30 → 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 (81) 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 +677 -187
  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__/embedder.test.d.ts +1 -0
  19. package/dist/memory/__tests__/embedder.test.js +14 -0
  20. package/dist/memory/__tests__/indexer.test.js +7 -12
  21. package/dist/memory/__tests__/lockfile.test.d.ts +1 -0
  22. package/dist/memory/__tests__/lockfile.test.js +81 -0
  23. package/dist/memory/__tests__/store.test.js +15 -3
  24. package/dist/memory/chunker.js +3 -5
  25. package/dist/memory/constants.d.ts +5 -0
  26. package/dist/memory/constants.js +12 -0
  27. package/dist/memory/digest/constants.d.ts +14 -0
  28. package/dist/memory/digest/constants.js +15 -0
  29. package/dist/memory/digest/files.d.ts +7 -0
  30. package/dist/memory/digest/files.js +45 -0
  31. package/dist/memory/digest/model-registry.d.ts +6 -0
  32. package/dist/memory/digest/model-registry.js +48 -0
  33. package/dist/memory/digest/processor.d.ts +7 -0
  34. package/dist/memory/digest/processor.js +171 -0
  35. package/dist/memory/digest/providers/llama-local.d.ts +7 -0
  36. package/dist/memory/digest/providers/llama-local.js +136 -0
  37. package/dist/memory/digest/types.d.ts +85 -0
  38. package/dist/memory/digest/types.js +1 -0
  39. package/dist/memory/digest/worker.d.ts +3 -0
  40. package/dist/memory/digest/worker.js +51 -0
  41. package/dist/memory/embedder.d.ts +2 -0
  42. package/dist/memory/embedder.js +12 -4
  43. package/dist/memory/index.d.ts +1 -1
  44. package/dist/memory/index.js +1 -1
  45. package/dist/memory/indexer.d.ts +5 -2
  46. package/dist/memory/indexer.js +15 -37
  47. package/dist/memory/store.js +6 -18
  48. package/dist/memory/types.d.ts +2 -1
  49. package/dist/memory/types.js +1 -9
  50. package/dist/tools/__tests__/cli-digest-pending.test.d.ts +1 -0
  51. package/dist/tools/__tests__/cli-digest-pending.test.js +51 -0
  52. package/dist/tools/__tests__/cli-memory.test.d.ts +1 -0
  53. package/dist/tools/__tests__/cli-memory.test.js +22 -0
  54. package/dist/tools/__tests__/digest-init.test.d.ts +1 -0
  55. package/dist/tools/__tests__/digest-init.test.js +32 -0
  56. package/dist/tools/__tests__/memory.test.js +28 -6
  57. package/dist/tools/agent.js +2 -13
  58. package/dist/tools/config.d.ts +4 -4
  59. package/dist/tools/config.js +21 -13
  60. package/dist/tools/core.js +1 -3
  61. package/dist/tools/handoffs.js +1 -2
  62. package/dist/tools/integration.js +2 -8
  63. package/dist/tools/memory.d.ts +1 -1
  64. package/dist/tools/memory.js +29 -3
  65. package/dist/tools/security.js +2 -6
  66. package/dist/utils/args.d.ts +7 -0
  67. package/dist/utils/args.js +31 -0
  68. package/dist/utils/async.d.ts +1 -0
  69. package/dist/utils/async.js +15 -0
  70. package/dist/utils/credentials.d.ts +3 -3
  71. package/dist/utils/credentials.js +5 -7
  72. package/dist/utils/files.d.ts +8 -0
  73. package/dist/utils/files.js +77 -0
  74. package/dist/utils/hash.d.ts +1 -0
  75. package/dist/utils/hash.js +4 -0
  76. package/dist/utils/json.d.ts +2 -0
  77. package/dist/utils/json.js +13 -0
  78. package/dist/utils/parser.js +2 -6
  79. package/dist/utils/paths.d.ts +4 -0
  80. package/dist/utils/paths.js +6 -0
  81. package/package.json +7 -5
@@ -1,29 +1,8 @@
1
1
  import * as fs from 'fs';
2
2
  import * as path from 'path';
3
3
  import { chunkMarkdown } from './chunker.js';
4
- const LOCK_RETRY_MS = 50;
5
- const LOCK_TIMEOUT_MS = 500;
6
- async function acquireLock(lockPath) {
7
- const deadline = Date.now() + LOCK_TIMEOUT_MS;
8
- while (Date.now() < deadline) {
9
- try {
10
- fs.writeFileSync(lockPath, String(process.pid), { flag: 'wx' });
11
- return true;
12
- }
13
- catch {
14
- await new Promise((r) => setTimeout(r, LOCK_RETRY_MS));
15
- }
16
- }
17
- return false;
18
- }
19
- function releaseLock(lockPath) {
20
- try {
21
- fs.unlinkSync(lockPath);
22
- }
23
- catch {
24
- // ignore
25
- }
26
- }
4
+ import { LOCK_RETRY_MS, LOCK_TIMEOUT_MS, RRF_K } from './constants.js';
5
+ import { acquireLock, releaseLock } from '../utils/files.js';
27
6
  export class MemoryIndexer {
28
7
  store;
29
8
  embedder;
@@ -39,19 +18,19 @@ export class MemoryIndexer {
39
18
  }
40
19
  async indexFileRelativeTo(absolutePath, sourceRoot) {
41
20
  const source = path.relative(sourceRoot, absolutePath);
42
- const existingHashes = this.store.hashesBySource(source);
21
+ const existingChunkIds = this.store.hashesBySource(source);
43
22
  let text;
44
23
  try {
45
24
  text = fs.readFileSync(absolutePath, 'utf8');
46
25
  }
47
26
  catch (err) {
48
27
  console.warn(`[memory-indexer] Cannot read file ${absolutePath}:`, err);
49
- return { indexed: 0, deleted: 0, skipped: existingHashes.size };
28
+ return { indexed: 0, deleted: 0, skipped: existingChunkIds.size };
50
29
  }
51
30
  const allChunks = chunkMarkdown(text, source, this.config);
52
- const newHashes = new Set(allChunks.map((c) => c.id));
53
- const toIndex = allChunks.filter((c) => !existingHashes.has(c.id));
54
- const toDelete = [...existingHashes].filter((h) => !newHashes.has(h));
31
+ const currentChunkIds = new Set(allChunks.map((chunk) => chunk.id));
32
+ const toIndex = allChunks.filter((chunk) => !existingChunkIds.has(chunk.id));
33
+ const toDelete = [...existingChunkIds].filter((id) => !currentChunkIds.has(id));
55
34
  let embeddings = [];
56
35
  if (toIndex.length > 0) {
57
36
  try {
@@ -59,7 +38,7 @@ export class MemoryIndexer {
59
38
  }
60
39
  catch (err) {
61
40
  console.warn(`[memory-indexer] Embedding failed for ${absolutePath}:`, err);
62
- return { indexed: 0, deleted: 0, skipped: existingHashes.size };
41
+ return { indexed: 0, deleted: 0, skipped: existingChunkIds.size };
63
42
  }
64
43
  }
65
44
  if (toIndex.length > 0) {
@@ -135,24 +114,22 @@ export class MemoryIndexer {
135
114
  if (bm25Ids.size > 0) {
136
115
  denseResults = denseResults.filter((result) => bm25Ids.has(result.id));
137
116
  }
138
- // RRF fusion (k=60)
139
- const k = 60;
140
117
  const scoreMap = new Map();
141
118
  denseResults.forEach((r, rank) => {
142
- scoreMap.set(r.id, { dense: 1 / (k + rank + 1), bm25: 0 });
119
+ scoreMap.set(r.id, { dense: 1 / (RRF_K + rank + 1), bm25: 0 });
143
120
  });
144
121
  bm25Results.forEach((r, rank) => {
145
122
  const existing = scoreMap.get(r.id);
146
123
  if (existing) {
147
- existing.bm25 = 1 / (k + rank + 1);
124
+ existing.bm25 = 1 / (RRF_K + rank + 1);
148
125
  }
149
126
  else {
150
- scoreMap.set(r.id, { dense: 0, bm25: 1 / (k + rank + 1) });
127
+ scoreMap.set(r.id, { dense: 0, bm25: 1 / (RRF_K + rank + 1) });
151
128
  }
152
129
  });
153
130
  const hasDense = this.store.vecAvailable && denseResults.length > 0;
154
131
  const numRetrievers = hasDense ? 2 : 1;
155
- const maxScore = numRetrievers / 61;
132
+ const maxScore = numRetrievers / (RRF_K + 1);
156
133
  const ranked = [...scoreMap.entries()]
157
134
  .map(([id, scores]) => ({
158
135
  id,
@@ -193,7 +170,7 @@ export class MemoryIndexer {
193
170
  const savePath = path.join(rawDir, `conv_save_${datePart}.md`);
194
171
  const lockPath = `${savePath}.lock`;
195
172
  fs.mkdirSync(rawDir, { recursive: true });
196
- const acquired = await acquireLock(lockPath);
173
+ const acquired = await acquireLock(lockPath, LOCK_TIMEOUT_MS, LOCK_RETRY_MS);
197
174
  if (!acquired) {
198
175
  console.warn('[memory-indexer] Could not acquire write lock, writing anyway (fail-open)');
199
176
  }
@@ -202,12 +179,13 @@ export class MemoryIndexer {
202
179
  }
203
180
  finally {
204
181
  if (acquired)
205
- releaseLock(lockPath);
182
+ releaseLock(lockPath, { ignoreErrors: true });
206
183
  }
207
184
  return { indexed: 0, deleted: 0, skipped: 0 };
208
185
  }
209
186
  async startupIndex() {
210
187
  try {
188
+ await this.embedder.initialize();
211
189
  await this.indexDirectory(path.join(this.config.wikiDir, 'compiled'), {
212
190
  relativeBase: this.config.wikiDir,
213
191
  excludeFiles: ['index.md', 'log.md'],
@@ -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
  */
@@ -89,7 +104,7 @@ export const FORBIDDEN_FILES = [
89
104
  '.yarnrc',
90
105
  '.netrc',
91
106
  '.gitconfig',
92
- 'credentials', // ~/.claude/credentials file
107
+ 'credentials', // ~/.agent-kit/credentials file
93
108
  ];
94
109
  /**
95
110
  * Regex patterns (case-insensitive) matched against path.basename.
@@ -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;