@hanhnd/agent-kit 1.0.29 → 1.0.31
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/kit-server.js +14945 -44
- 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 +60 -0
- package/dist/memory/__tests__/store.test.js +19 -0
- package/dist/memory/embedder.d.ts +1 -0
- package/dist/memory/embedder.js +3 -4
- package/dist/memory/indexer.js +4 -0
- package/dist/memory/store.js +21 -16
- package/dist/tools/config.js +1 -1
- package/dist/utils/credentials.d.ts +3 -3
- package/dist/utils/credentials.js +5 -7
- package/dist/utils/paths.d.ts +4 -0
- package/dist/utils/paths.js +6 -0
- package/package.json +2 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import * as assert from 'node:assert/strict';
|
|
2
|
+
import * as os from 'node:os';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
import { describe, test } from 'node:test';
|
|
5
|
+
import { DEFAULT_MODEL_CACHE_DIR, FASTEMBED_CACHE_DIR } from '../embedder.js';
|
|
6
|
+
import { AGENT_KIT_HOME, CREDENTIALS_FILE } from '../../utils/paths.js';
|
|
7
|
+
describe('Embedder cache paths', () => {
|
|
8
|
+
test('uses global Agent Kit home for user-level files', () => {
|
|
9
|
+
assert.equal(AGENT_KIT_HOME, path.join(os.homedir(), '.agent-kit'));
|
|
10
|
+
assert.equal(CREDENTIALS_FILE, path.join(AGENT_KIT_HOME, 'credentials'));
|
|
11
|
+
assert.equal(DEFAULT_MODEL_CACHE_DIR, path.join(AGENT_KIT_HOME, 'cache', 'models'));
|
|
12
|
+
assert.equal(FASTEMBED_CACHE_DIR, path.join(DEFAULT_MODEL_CACHE_DIR, 'fastembed'));
|
|
13
|
+
});
|
|
14
|
+
});
|
|
@@ -268,6 +268,66 @@ describe('MemoryIndexer', () => {
|
|
|
268
268
|
fs.rmSync(testDir, { recursive: true, force: true });
|
|
269
269
|
}
|
|
270
270
|
});
|
|
271
|
+
test('search does not include dense-only results when BM25 has matches', async () => {
|
|
272
|
+
const testCfg = makeConfig('/tmp/search-dense-filter');
|
|
273
|
+
const chunks = [
|
|
274
|
+
{
|
|
275
|
+
id: 'preference-1',
|
|
276
|
+
source: 'compiled/preferences.md',
|
|
277
|
+
heading: '',
|
|
278
|
+
headingLevel: 0,
|
|
279
|
+
content: 'I like fish',
|
|
280
|
+
lineStart: 1,
|
|
281
|
+
lineEnd: 1,
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
id: 'dense-only-1',
|
|
285
|
+
source: 'compiled/entities/worktree.md',
|
|
286
|
+
heading: 'Worktree',
|
|
287
|
+
headingLevel: 1,
|
|
288
|
+
content: 'Unrelated worktree lifecycle content',
|
|
289
|
+
lineStart: 1,
|
|
290
|
+
lineEnd: 2,
|
|
291
|
+
},
|
|
292
|
+
];
|
|
293
|
+
const fakeStore = {
|
|
294
|
+
vecAvailable: true,
|
|
295
|
+
searchDense: () => [
|
|
296
|
+
{ id: 'dense-only-1', score: 0.99 },
|
|
297
|
+
{ id: 'preference-1', score: 0.98 },
|
|
298
|
+
],
|
|
299
|
+
searchBm25: () => [{ id: 'preference-1', score: 1 }],
|
|
300
|
+
getChunksByIds: (ids) => chunks.filter((chunk) => ids.includes(chunk.id)),
|
|
301
|
+
};
|
|
302
|
+
const testIndexer = new MemoryIndexer(fakeStore, new StubEmbedder(), testCfg);
|
|
303
|
+
const results = await testIndexer.search('personal likes and preferences of the user', 5);
|
|
304
|
+
assert.deepEqual(results.map((result) => result.chunk.id), ['preference-1']);
|
|
305
|
+
assert.equal(results[0].retriever, 'both');
|
|
306
|
+
});
|
|
307
|
+
test('search still returns dense-only results when BM25 has no matches', async () => {
|
|
308
|
+
const testCfg = makeConfig('/tmp/search-dense-fallback');
|
|
309
|
+
const chunks = [
|
|
310
|
+
{
|
|
311
|
+
id: 'semantic-1',
|
|
312
|
+
source: 'compiled/preferences.md',
|
|
313
|
+
heading: '',
|
|
314
|
+
headingLevel: 0,
|
|
315
|
+
content: 'I like fish',
|
|
316
|
+
lineStart: 1,
|
|
317
|
+
lineEnd: 1,
|
|
318
|
+
},
|
|
319
|
+
];
|
|
320
|
+
const fakeStore = {
|
|
321
|
+
vecAvailable: true,
|
|
322
|
+
searchDense: () => [{ id: 'semantic-1', score: 0.99 }],
|
|
323
|
+
searchBm25: () => [],
|
|
324
|
+
getChunksByIds: (ids) => chunks.filter((chunk) => ids.includes(chunk.id)),
|
|
325
|
+
};
|
|
326
|
+
const testIndexer = new MemoryIndexer(fakeStore, new StubEmbedder(), testCfg);
|
|
327
|
+
const results = await testIndexer.search('favorite dish', 5);
|
|
328
|
+
assert.deepEqual(results.map((result) => result.chunk.id), ['semantic-1']);
|
|
329
|
+
assert.equal(results[0].retriever, 'dense');
|
|
330
|
+
});
|
|
271
331
|
test('search falls back to stored chunk content when source file is missing', async () => {
|
|
272
332
|
const testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'search-fallback-'));
|
|
273
333
|
const testCfg = makeConfig(path.join(testDir, 'wiki'));
|
|
@@ -72,6 +72,25 @@ describe('MemoryStore', () => {
|
|
|
72
72
|
assert.ok(results.length > 0, 'Expected at least one BM25 result');
|
|
73
73
|
assert.ok(results.some((r) => r.id === 'bm25-search-0001'), `Expected chunk id in results, got: ${results.map((r) => r.id).join(', ')}`);
|
|
74
74
|
});
|
|
75
|
+
test('searchBm25 ignores filler words and matches preference source/content terms', () => {
|
|
76
|
+
const preference = makeChunk({
|
|
77
|
+
id: 'preference-search-0001',
|
|
78
|
+
source: 'compiled/preferences.md',
|
|
79
|
+
heading: '',
|
|
80
|
+
headingLevel: 0,
|
|
81
|
+
content: 'I like fish',
|
|
82
|
+
});
|
|
83
|
+
const unrelated = makeChunk({
|
|
84
|
+
id: 'preference-search-0002',
|
|
85
|
+
source: 'compiled/entities/worktree.md',
|
|
86
|
+
heading: 'Open Questions',
|
|
87
|
+
content: 'How should git manage worktree lifecycle decisions?',
|
|
88
|
+
});
|
|
89
|
+
store.upsert([preference, unrelated], [new Float32Array(384), new Float32Array(384)]);
|
|
90
|
+
const results = store.searchBm25('personal likes and preferences of the user', 5);
|
|
91
|
+
assert.ok(results.length > 0, 'Expected preference query to return results');
|
|
92
|
+
assert.equal(results[0].id, 'preference-search-0001');
|
|
93
|
+
});
|
|
75
94
|
test('searchBm25 returns empty array for empty query', () => {
|
|
76
95
|
const results = store.searchBm25(' ', 5);
|
|
77
96
|
assert.deepEqual(results, []);
|
package/dist/memory/embedder.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
const DEFAULT_CACHE_DIR = path.join(os.homedir(), '.cache', 'fastembed');
|
|
1
|
+
import { FASTEMBED_CACHE_DIR } from '../utils/paths.js';
|
|
2
|
+
export { FASTEMBED_CACHE_DIR, MODEL_CACHE_DIR as DEFAULT_MODEL_CACHE_DIR } from '../utils/paths.js';
|
|
4
3
|
export class EmbedderError extends Error {
|
|
5
4
|
constructor(message, cause) {
|
|
6
5
|
super(message);
|
|
@@ -50,7 +49,7 @@ export class Embedder {
|
|
|
50
49
|
'Xenova/all-MiniLM-L6-v2': EmbeddingModel.AllMiniLML6V2,
|
|
51
50
|
};
|
|
52
51
|
const model = modelMap[this.modelName] ?? EmbeddingModel.BGESmallENV15;
|
|
53
|
-
const embedder = await FlagEmbedding.init({ model: model, cacheDir:
|
|
52
|
+
const embedder = await FlagEmbedding.init({ model: model, cacheDir: FASTEMBED_CACHE_DIR, showDownloadProgress: false });
|
|
54
53
|
this.pipeline = async (texts) => {
|
|
55
54
|
const result = [];
|
|
56
55
|
for await (const batch of embedder.embed(texts, texts.length)) {
|
package/dist/memory/indexer.js
CHANGED
|
@@ -131,6 +131,10 @@ export class MemoryIndexer {
|
|
|
131
131
|
}
|
|
132
132
|
}
|
|
133
133
|
const bm25Results = this.store.searchBm25(query, fetchLimit);
|
|
134
|
+
const bm25Ids = new Set(bm25Results.map((result) => result.id));
|
|
135
|
+
if (bm25Ids.size > 0) {
|
|
136
|
+
denseResults = denseResults.filter((result) => bm25Ids.has(result.id));
|
|
137
|
+
}
|
|
134
138
|
// RRF fusion (k=60)
|
|
135
139
|
const k = 60;
|
|
136
140
|
const scoreMap = new Map();
|
package/dist/memory/store.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import Database from 'better-sqlite3';
|
|
2
2
|
import * as fs from 'fs';
|
|
3
3
|
import * as path from 'path';
|
|
4
|
+
import { eng, removeStopwords } from 'stopword';
|
|
4
5
|
export class StoreError extends Error {
|
|
5
6
|
constructor(message, cause) {
|
|
6
7
|
super(message);
|
|
@@ -58,18 +59,20 @@ export class MemoryStore {
|
|
|
58
59
|
createSchema(db) {
|
|
59
60
|
db.exec(`
|
|
60
61
|
CREATE TABLE IF NOT EXISTS memory_chunks (
|
|
61
|
-
rowid
|
|
62
|
-
id
|
|
63
|
-
source
|
|
64
|
-
heading
|
|
62
|
+
rowid INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
63
|
+
id TEXT NOT NULL UNIQUE,
|
|
64
|
+
source TEXT NOT NULL,
|
|
65
|
+
heading TEXT NOT NULL DEFAULT '',
|
|
65
66
|
heading_level INTEGER NOT NULL DEFAULT 0,
|
|
66
|
-
content
|
|
67
|
-
line_start
|
|
68
|
-
line_end
|
|
69
|
-
indexed_at
|
|
67
|
+
content TEXT NOT NULL,
|
|
68
|
+
line_start INTEGER NOT NULL,
|
|
69
|
+
line_end INTEGER NOT NULL,
|
|
70
|
+
indexed_at INTEGER NOT NULL
|
|
70
71
|
);
|
|
71
72
|
|
|
72
73
|
CREATE VIRTUAL TABLE IF NOT EXISTS memory_fts USING fts5(
|
|
74
|
+
source,
|
|
75
|
+
heading,
|
|
73
76
|
content,
|
|
74
77
|
content='memory_chunks',
|
|
75
78
|
content_rowid='rowid'
|
|
@@ -77,13 +80,14 @@ export class MemoryStore {
|
|
|
77
80
|
|
|
78
81
|
CREATE TRIGGER IF NOT EXISTS memory_chunks_ai
|
|
79
82
|
AFTER INSERT ON memory_chunks BEGIN
|
|
80
|
-
INSERT INTO memory_fts(rowid,
|
|
83
|
+
INSERT INTO memory_fts(rowid, source, heading, content)
|
|
84
|
+
VALUES (new.rowid, new.source, new.heading, new.content);
|
|
81
85
|
END;
|
|
82
86
|
|
|
83
87
|
CREATE TRIGGER IF NOT EXISTS memory_chunks_ad
|
|
84
88
|
AFTER DELETE ON memory_chunks BEGIN
|
|
85
|
-
INSERT INTO memory_fts(memory_fts, rowid, content)
|
|
86
|
-
VALUES ('delete', old.rowid, old.content);
|
|
89
|
+
INSERT INTO memory_fts(memory_fts, rowid, source, heading, content)
|
|
90
|
+
VALUES ('delete', old.rowid, old.source, old.heading, old.content);
|
|
87
91
|
END;
|
|
88
92
|
`);
|
|
89
93
|
if (this._vecAvailable) {
|
|
@@ -206,18 +210,19 @@ export class MemoryStore {
|
|
|
206
210
|
searchBm25(query, limit) {
|
|
207
211
|
if (!query.trim())
|
|
208
212
|
return [];
|
|
209
|
-
|
|
210
|
-
const ftsQuery = query
|
|
213
|
+
const tokens = query
|
|
211
214
|
.trim()
|
|
212
215
|
.split(/\s+/)
|
|
213
|
-
.map((t) => t.replace(/[
|
|
214
|
-
.filter(
|
|
216
|
+
.map((t) => t.replace(/[^a-zA-Z0-9_]/g, '').toLowerCase())
|
|
217
|
+
.filter((t) => t.length > 1);
|
|
218
|
+
const ftsQuery = removeStopwords(tokens, eng)
|
|
219
|
+
.map((t) => `${t.replace(/s$/, '')}*`)
|
|
215
220
|
.join(' OR ');
|
|
216
221
|
if (!ftsQuery)
|
|
217
222
|
return [];
|
|
218
223
|
try {
|
|
219
224
|
const rows = this.db
|
|
220
|
-
.prepare(`SELECT mc.id, rank
|
|
225
|
+
.prepare(`SELECT mc.id, bm25(memory_fts, 0.2, 2.0, 5.0) AS rank
|
|
221
226
|
FROM memory_fts
|
|
222
227
|
JOIN memory_chunks mc ON mc.rowid = memory_fts.rowid
|
|
223
228
|
WHERE memory_fts MATCH ?
|
package/dist/tools/config.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Credential loader — reads ~/.
|
|
2
|
+
* Credential loader — reads ~/.agent-kit/credentials (INI format, like ~/.aws/credentials)
|
|
3
3
|
*
|
|
4
4
|
* Resolution order per key:
|
|
5
5
|
* 1. process.env.KEY (explicit override / CI)
|
|
6
|
-
* 2. ~/.
|
|
6
|
+
* 2. ~/.agent-kit/credentials [profile] section
|
|
7
7
|
* 3. undefined
|
|
8
8
|
*
|
|
9
9
|
* File format:
|
|
@@ -15,6 +15,6 @@
|
|
|
15
15
|
*/
|
|
16
16
|
/**
|
|
17
17
|
* Resolve a credential key using the priority chain:
|
|
18
|
-
* process.env.KEY → ~/.
|
|
18
|
+
* process.env.KEY → ~/.agent-kit/credentials [profile] → undefined
|
|
19
19
|
*/
|
|
20
20
|
export declare function getCredential(key: string): string | undefined;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Credential loader — reads ~/.
|
|
2
|
+
* Credential loader — reads ~/.agent-kit/credentials (INI format, like ~/.aws/credentials)
|
|
3
3
|
*
|
|
4
4
|
* Resolution order per key:
|
|
5
5
|
* 1. process.env.KEY (explicit override / CI)
|
|
6
|
-
* 2. ~/.
|
|
6
|
+
* 2. ~/.agent-kit/credentials [profile] section
|
|
7
7
|
* 3. undefined
|
|
8
8
|
*
|
|
9
9
|
* File format:
|
|
@@ -14,9 +14,7 @@
|
|
|
14
14
|
* Active profile: KIT_PROFILE env var (defaults to "default")
|
|
15
15
|
*/
|
|
16
16
|
import * as fs from 'fs';
|
|
17
|
-
import
|
|
18
|
-
import * as path from 'path';
|
|
19
|
-
const CREDENTIALS_FILE = path.join(os.homedir(), '.claude', 'credentials');
|
|
17
|
+
import { CREDENTIALS_FILE } from './paths.js';
|
|
20
18
|
let cachedCredentials = null;
|
|
21
19
|
/**
|
|
22
20
|
* Parse a simple INI file into a map of profiles.
|
|
@@ -47,7 +45,7 @@ function parseIni(content) {
|
|
|
47
45
|
return result;
|
|
48
46
|
}
|
|
49
47
|
/**
|
|
50
|
-
* Load credentials from ~/.
|
|
48
|
+
* Load credentials from ~/.agent-kit/credentials for the active profile.
|
|
51
49
|
* Result is cached for the lifetime of the MCP server process.
|
|
52
50
|
*/
|
|
53
51
|
function loadCredentials() {
|
|
@@ -79,7 +77,7 @@ function loadCredentials() {
|
|
|
79
77
|
}
|
|
80
78
|
/**
|
|
81
79
|
* Resolve a credential key using the priority chain:
|
|
82
|
-
* process.env.KEY → ~/.
|
|
80
|
+
* process.env.KEY → ~/.agent-kit/credentials [profile] → undefined
|
|
83
81
|
*/
|
|
84
82
|
export function getCredential(key) {
|
|
85
83
|
if (process.env[key]) {
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import * as os from 'os';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
export const AGENT_KIT_HOME = path.join(os.homedir(), '.agent-kit');
|
|
4
|
+
export const CREDENTIALS_FILE = path.join(AGENT_KIT_HOME, 'credentials');
|
|
5
|
+
export const MODEL_CACHE_DIR = path.join(AGENT_KIT_HOME, 'cache', 'models');
|
|
6
|
+
export const FASTEMBED_CACHE_DIR = path.join(MODEL_CACHE_DIR, 'fastembed');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hanhnd/agent-kit",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.31",
|
|
4
4
|
"description": "Agent Kit MCP server for software development agent workflows",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/kit-server.js",
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
"dedent": "^1.5.3",
|
|
33
33
|
"fastembed": "^1.14.0",
|
|
34
34
|
"sqlite-vec": "^0.1.0",
|
|
35
|
+
"stopword": "^3.1.5",
|
|
35
36
|
"zod": "^3.24.4"
|
|
36
37
|
},
|
|
37
38
|
"devDependencies": {
|