@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.
- 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 +679 -193
- 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__/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 +1 -0
- package/dist/memory/embedder.js +10 -1
- 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 +20 -12
- 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/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/package.json +7 -5
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
|
*/
|
|
@@ -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;
|
|
@@ -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
|
+
}
|