@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.
- package/dist/__tests__/temp-dir.d.ts +4 -0
- package/dist/__tests__/temp-dir.js +20 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +9 -0
- package/dist/cli/memory.d.ts +1 -0
- package/dist/cli/memory.js +77 -0
- package/dist/kit-cli.d.ts +2 -0
- package/dist/kit-cli.js +16205 -0
- package/dist/kit-server.js +677 -187
- package/dist/memory/__tests__/digest-llama-local.test.d.ts +1 -0
- package/dist/memory/__tests__/digest-llama-local.test.js +33 -0
- package/dist/memory/__tests__/digest-pending.test.d.ts +1 -0
- package/dist/memory/__tests__/digest-pending.test.js +164 -0
- package/dist/memory/__tests__/digest-processor.test.d.ts +1 -0
- package/dist/memory/__tests__/digest-processor.test.js +34 -0
- package/dist/memory/__tests__/digest-worker.test.d.ts +1 -0
- package/dist/memory/__tests__/digest-worker.test.js +46 -0
- package/dist/memory/__tests__/embedder.test.d.ts +1 -0
- package/dist/memory/__tests__/embedder.test.js +14 -0
- package/dist/memory/__tests__/indexer.test.js +7 -12
- package/dist/memory/__tests__/lockfile.test.d.ts +1 -0
- package/dist/memory/__tests__/lockfile.test.js +81 -0
- package/dist/memory/__tests__/store.test.js +15 -3
- package/dist/memory/chunker.js +3 -5
- package/dist/memory/constants.d.ts +5 -0
- package/dist/memory/constants.js +12 -0
- package/dist/memory/digest/constants.d.ts +14 -0
- package/dist/memory/digest/constants.js +15 -0
- package/dist/memory/digest/files.d.ts +7 -0
- package/dist/memory/digest/files.js +45 -0
- package/dist/memory/digest/model-registry.d.ts +6 -0
- package/dist/memory/digest/model-registry.js +48 -0
- package/dist/memory/digest/processor.d.ts +7 -0
- package/dist/memory/digest/processor.js +171 -0
- package/dist/memory/digest/providers/llama-local.d.ts +7 -0
- package/dist/memory/digest/providers/llama-local.js +136 -0
- package/dist/memory/digest/types.d.ts +85 -0
- package/dist/memory/digest/types.js +1 -0
- package/dist/memory/digest/worker.d.ts +3 -0
- package/dist/memory/digest/worker.js +51 -0
- package/dist/memory/embedder.d.ts +2 -0
- package/dist/memory/embedder.js +12 -4
- package/dist/memory/index.d.ts +1 -1
- package/dist/memory/index.js +1 -1
- package/dist/memory/indexer.d.ts +5 -2
- package/dist/memory/indexer.js +15 -37
- package/dist/memory/store.js +6 -18
- package/dist/memory/types.d.ts +2 -1
- package/dist/memory/types.js +1 -9
- package/dist/tools/__tests__/cli-digest-pending.test.d.ts +1 -0
- package/dist/tools/__tests__/cli-digest-pending.test.js +51 -0
- package/dist/tools/__tests__/cli-memory.test.d.ts +1 -0
- package/dist/tools/__tests__/cli-memory.test.js +22 -0
- package/dist/tools/__tests__/digest-init.test.d.ts +1 -0
- package/dist/tools/__tests__/digest-init.test.js +32 -0
- package/dist/tools/__tests__/memory.test.js +28 -6
- package/dist/tools/agent.js +2 -13
- package/dist/tools/config.d.ts +4 -4
- package/dist/tools/config.js +21 -13
- package/dist/tools/core.js +1 -3
- package/dist/tools/handoffs.js +1 -2
- package/dist/tools/integration.js +2 -8
- package/dist/tools/memory.d.ts +1 -1
- package/dist/tools/memory.js +29 -3
- package/dist/tools/security.js +2 -6
- package/dist/utils/args.d.ts +7 -0
- package/dist/utils/args.js +31 -0
- package/dist/utils/async.d.ts +1 -0
- package/dist/utils/async.js +15 -0
- package/dist/utils/credentials.d.ts +3 -3
- package/dist/utils/credentials.js +5 -7
- package/dist/utils/files.d.ts +8 -0
- package/dist/utils/files.js +77 -0
- package/dist/utils/hash.d.ts +1 -0
- package/dist/utils/hash.js +4 -0
- package/dist/utils/json.d.ts +2 -0
- package/dist/utils/json.js +13 -0
- package/dist/utils/parser.js +2 -6
- package/dist/utils/paths.d.ts +4 -0
- package/dist/utils/paths.js +6 -0
- package/package.json +7 -5
package/dist/memory/indexer.js
CHANGED
|
@@ -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
|
-
|
|
5
|
-
|
|
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
|
|
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:
|
|
28
|
+
return { indexed: 0, deleted: 0, skipped: existingChunkIds.size };
|
|
50
29
|
}
|
|
51
30
|
const allChunks = chunkMarkdown(text, source, this.config);
|
|
52
|
-
const
|
|
53
|
-
const toIndex = allChunks.filter((
|
|
54
|
-
const toDelete = [...
|
|
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:
|
|
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 / (
|
|
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 / (
|
|
124
|
+
existing.bm25 = 1 / (RRF_K + rank + 1);
|
|
148
125
|
}
|
|
149
126
|
else {
|
|
150
|
-
scoreMap.set(r.id, { dense: 0, bm25: 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 /
|
|
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'],
|
package/dist/memory/store.js
CHANGED
|
@@ -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
|
});
|
package/dist/memory/types.d.ts
CHANGED
|
@@ -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;
|
package/dist/memory/types.js
CHANGED
|
@@ -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({
|
|
52
|
-
|
|
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 = {
|
|
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 = {
|
|
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 () => {
|
|
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 () => {
|
|
184
|
+
save: async () => {
|
|
185
|
+
throw new Error('disk full');
|
|
186
|
+
},
|
|
165
187
|
});
|
|
166
188
|
const store = makeStoreStub(true);
|
|
167
189
|
const { server, tools } = makeMockServer();
|
package/dist/tools/agent.js
CHANGED
|
@@ -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")'),
|
package/dist/tools/config.d.ts
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* Configuration utilities
|
|
3
3
|
* Shared configuration constants and loaders
|
|
4
4
|
*/
|
|
5
|
-
import {
|
|
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
|
*/
|
package/dist/tools/config.js
CHANGED
|
@@ -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/
|
|
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', // ~/.
|
|
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'];
|
package/dist/tools/core.js
CHANGED
|
@@ -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()
|
package/dist/tools/handoffs.js
CHANGED
|
@@ -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
|
|
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'}`;
|
package/dist/tools/memory.d.ts
CHANGED
|
@@ -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().
|
package/dist/tools/memory.js
CHANGED
|
@@ -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
|
}
|
package/dist/tools/security.js
CHANGED
|
@@ -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;
|