@hanhnd/agent-kit 1.0.39 → 1.0.41
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/cli.js +55 -15
- package/dist/core/handoffs/index.d.ts +2 -2
- package/dist/core/handoffs/index.js +0 -1
- package/dist/entrypoints/server.js +3 -3
- package/dist/mcp/agent.js +12 -5
- package/dist/mcp/core.js +13 -8
- package/dist/mcp/integration.js +20 -10
- package/dist/mcp/memory.d.ts +4 -9
- package/dist/mcp/memory.js +73 -29
- package/dist/mcp/registration.test.d.ts +1 -0
- package/dist/mcp/registration.test.js +77 -0
- package/dist/server.js +281 -123
- package/dist/services/memory/index.d.ts +3 -1
- package/dist/services/memory/index.js +1 -0
- package/dist/services/memory/indexer.d.ts +5 -6
- package/dist/services/memory/indexer.js +36 -15
- package/dist/services/memory/indexer.test.js +132 -0
- package/dist/services/memory/memory.test.js +61 -2
- package/dist/services/memory/subsystem.d.ts +19 -0
- package/dist/services/memory/subsystem.js +67 -0
- package/dist/services/memory/types.d.ts +20 -0
- package/dist/utils/async.d.ts +1 -0
- package/dist/utils/async.js +17 -0
- package/dist/utils/async.test.d.ts +1 -0
- package/dist/utils/async.test.js +48 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -15195,6 +15195,22 @@ async function withTimeout(promise, timeoutMs, createError) {
|
|
|
15195
15195
|
if (timer) clearTimeout(timer);
|
|
15196
15196
|
}
|
|
15197
15197
|
}
|
|
15198
|
+
async function mapLimit(items, limit, worker) {
|
|
15199
|
+
if (items.length === 0) return [];
|
|
15200
|
+
const concurrency = Math.max(1, Math.floor(limit));
|
|
15201
|
+
const results = new Array(items.length);
|
|
15202
|
+
let nextIndex = 0;
|
|
15203
|
+
async function runWorker() {
|
|
15204
|
+
while (nextIndex < items.length) {
|
|
15205
|
+
const currentIndex = nextIndex;
|
|
15206
|
+
nextIndex++;
|
|
15207
|
+
results[currentIndex] = await worker(items[currentIndex], currentIndex);
|
|
15208
|
+
}
|
|
15209
|
+
}
|
|
15210
|
+
const runners = Array.from({ length: Math.min(concurrency, items.length) }, () => runWorker());
|
|
15211
|
+
await Promise.all(runners);
|
|
15212
|
+
return results;
|
|
15213
|
+
}
|
|
15198
15214
|
|
|
15199
15215
|
// src/utils/paths.ts
|
|
15200
15216
|
import * as os2 from "os";
|
|
@@ -15912,9 +15928,19 @@ var MemoryIndexer = class {
|
|
|
15912
15928
|
}
|
|
15913
15929
|
_ready = false;
|
|
15914
15930
|
async indexFile(absolutePath) {
|
|
15915
|
-
|
|
15931
|
+
const mutation = await this.prepareIndexFileRelativeTo(absolutePath, this.config.wikiDir);
|
|
15932
|
+
return this.commitPreparedMutation(mutation);
|
|
15916
15933
|
}
|
|
15917
|
-
|
|
15934
|
+
makeSkippedMutation(source, skipped) {
|
|
15935
|
+
return {
|
|
15936
|
+
source,
|
|
15937
|
+
chunksToUpsert: [],
|
|
15938
|
+
embeddings: [],
|
|
15939
|
+
idsToDelete: [],
|
|
15940
|
+
stats: { indexed: 0, deleted: 0, skipped }
|
|
15941
|
+
};
|
|
15942
|
+
}
|
|
15943
|
+
async prepareIndexFileRelativeTo(absolutePath, sourceRoot) {
|
|
15918
15944
|
const source = path8.relative(sourceRoot, absolutePath);
|
|
15919
15945
|
const existingChunkIds = this.store.hashesBySource(source);
|
|
15920
15946
|
let text;
|
|
@@ -15922,7 +15948,7 @@ var MemoryIndexer = class {
|
|
|
15922
15948
|
text = await fs6.promises.readFile(absolutePath, "utf8");
|
|
15923
15949
|
} catch (err) {
|
|
15924
15950
|
console.warn(`[memory-indexer] Cannot read file ${absolutePath}:`, err);
|
|
15925
|
-
return
|
|
15951
|
+
return this.makeSkippedMutation(source, existingChunkIds.size);
|
|
15926
15952
|
}
|
|
15927
15953
|
let fileMtimeAt;
|
|
15928
15954
|
try {
|
|
@@ -15942,21 +15968,30 @@ var MemoryIndexer = class {
|
|
|
15942
15968
|
embeddings = await this.embedder.embed(toIndex.map((c) => c.content));
|
|
15943
15969
|
} catch (err) {
|
|
15944
15970
|
console.warn(`[memory-indexer] Embedding failed for ${absolutePath}:`, err);
|
|
15945
|
-
return
|
|
15971
|
+
return this.makeSkippedMutation(source, existingChunkIds.size);
|
|
15946
15972
|
}
|
|
15947
15973
|
}
|
|
15948
|
-
if (toIndex.length > 0) {
|
|
15949
|
-
this.store.upsert(toIndex, embeddings);
|
|
15950
|
-
}
|
|
15951
|
-
if (toDelete.length > 0) {
|
|
15952
|
-
this.store.deleteByIds(toDelete);
|
|
15953
|
-
}
|
|
15954
15974
|
return {
|
|
15955
|
-
|
|
15956
|
-
|
|
15957
|
-
|
|
15975
|
+
source,
|
|
15976
|
+
chunksToUpsert: toIndex,
|
|
15977
|
+
embeddings,
|
|
15978
|
+
idsToDelete: toDelete,
|
|
15979
|
+
stats: {
|
|
15980
|
+
indexed: toIndex.length,
|
|
15981
|
+
deleted: toDelete.length,
|
|
15982
|
+
skipped: allChunks.length - toIndex.length
|
|
15983
|
+
}
|
|
15958
15984
|
};
|
|
15959
15985
|
}
|
|
15986
|
+
commitPreparedMutation(mutation) {
|
|
15987
|
+
if (mutation.chunksToUpsert.length > 0) {
|
|
15988
|
+
this.store.upsert(mutation.chunksToUpsert, mutation.embeddings);
|
|
15989
|
+
}
|
|
15990
|
+
if (mutation.idsToDelete.length > 0) {
|
|
15991
|
+
this.store.deleteByIds(mutation.idsToDelete);
|
|
15992
|
+
}
|
|
15993
|
+
return mutation.stats;
|
|
15994
|
+
}
|
|
15960
15995
|
async indexDirectory(rootDir, opts = {}) {
|
|
15961
15996
|
const totals = { indexed: 0, deleted: 0, skipped: 0 };
|
|
15962
15997
|
let rootExists;
|
|
@@ -15998,8 +16033,13 @@ var MemoryIndexer = class {
|
|
|
15998
16033
|
totals.deleted += 1;
|
|
15999
16034
|
}
|
|
16000
16035
|
}
|
|
16001
|
-
|
|
16002
|
-
|
|
16036
|
+
const preparedMutations = await mapLimit(
|
|
16037
|
+
files,
|
|
16038
|
+
opts.fileConcurrency ?? 2,
|
|
16039
|
+
(file) => this.prepareIndexFileRelativeTo(file, relativeBase)
|
|
16040
|
+
);
|
|
16041
|
+
for (const mutation of preparedMutations) {
|
|
16042
|
+
const stats = this.commitPreparedMutation(mutation);
|
|
16003
16043
|
totals.indexed += stats.indexed;
|
|
16004
16044
|
totals.deleted += stats.deleted;
|
|
16005
16045
|
totals.skipped += stats.skipped;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
export declare const HANDOFF_TYPES: readonly ["brainstorm", "clarification", "plan", "ticket", "research", "
|
|
1
|
+
export declare const HANDOFF_TYPES: readonly ["brainstorm", "clarification", "plan", "ticket", "research", "investigation", "code"];
|
|
2
2
|
type HandoffType = (typeof HANDOFF_TYPES)[number];
|
|
3
|
-
type CanonicalHandoffType = 'brainstorm' | 'clarification' | 'plan' | 'ticket' | 'research' | '
|
|
3
|
+
type CanonicalHandoffType = 'brainstorm' | 'clarification' | 'plan' | 'ticket' | 'research' | 'investigation' | 'code';
|
|
4
4
|
export interface SavedHandoffFolderLocation {
|
|
5
5
|
featureSlug: string;
|
|
6
6
|
canonicalType: CanonicalHandoffType;
|
|
@@ -23,13 +23,13 @@ registerIntegrationTools(server);
|
|
|
23
23
|
registerCoreTools(server);
|
|
24
24
|
registerAgentTools(server);
|
|
25
25
|
const workspaceRoot = getWorkspaceRoot();
|
|
26
|
-
const
|
|
26
|
+
const memorySubsystem = registerMemoryTools(server, workspaceRoot);
|
|
27
27
|
// ═══════════════════════════════════════════════════════════════
|
|
28
28
|
// START SERVER
|
|
29
29
|
// ═══════════════════════════════════════════════════════════════
|
|
30
30
|
const transport = new StdioServerTransport();
|
|
31
31
|
await server.connect(transport);
|
|
32
32
|
// Fire-and-forget startup indexing — must run after server.connect
|
|
33
|
-
if (
|
|
34
|
-
void
|
|
33
|
+
if (memorySubsystem) {
|
|
34
|
+
void memorySubsystem.startWarmup();
|
|
35
35
|
}
|
package/dist/mcp/agent.js
CHANGED
|
@@ -49,11 +49,18 @@ function initAgentLog(workspaceRoot, slug, agent) {
|
|
|
49
49
|
return { logPath: path.relative(workspaceRoot, logFile), logStream };
|
|
50
50
|
}
|
|
51
51
|
export function registerAgentTools(server) {
|
|
52
|
-
server.
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
52
|
+
server.registerTool('kit_trigger_agent', {
|
|
53
|
+
title: 'Delegate to External Agent',
|
|
54
|
+
description: '!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 folder (.agent-kit/handoffs/feature-slug/plan).',
|
|
55
|
+
inputSchema: {
|
|
56
|
+
agent: z
|
|
57
|
+
.enum(['gemini', 'claude', 'codex'])
|
|
58
|
+
.describe('The agent CLI to invoke: "gemini", "claude", or "codex"'),
|
|
59
|
+
task: z
|
|
60
|
+
.string()
|
|
61
|
+
.describe('Task message or path to a handoff folder (e.g., ".agent-kit/handoffs/feature-slug/plan")'),
|
|
62
|
+
},
|
|
63
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
57
64
|
}, async ({ agent, task }, extra) => {
|
|
58
65
|
const workspaceRoot = getWorkspaceRoot();
|
|
59
66
|
let job;
|
package/dist/mcp/core.js
CHANGED
|
@@ -46,14 +46,19 @@ export function registerCoreTools(server) {
|
|
|
46
46
|
// Writes handoff artifacts to the workspace as a folder of files
|
|
47
47
|
// Returns the saved folder path to use in next-step instructions
|
|
48
48
|
// ═══════════════════════════════════════════════════════════════
|
|
49
|
-
server.
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
.describe('
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
49
|
+
server.registerTool('kit_save_handoff', {
|
|
50
|
+
title: 'Save Handoff Artifact',
|
|
51
|
+
description: `Save a handoff artifact to .agent-kit/handoffs/. Returns the saved folder path to use in next-step instructions. Do NOT append version numbers (v2, v3, etc.) to the slug.`,
|
|
52
|
+
inputSchema: {
|
|
53
|
+
type: z.enum(HANDOFF_TYPES).describe('Handoff type'),
|
|
54
|
+
files: z
|
|
55
|
+
.record(z.string(), z.string())
|
|
56
|
+
.describe('Markdown files to write into the handoff folder. May include one or many files per call; repeated calls with the same type and slug add or overwrite the provided files. Filenames must match /^[A-Za-z0-9_-]+\\.md$/.'),
|
|
57
|
+
slug: z
|
|
58
|
+
.string()
|
|
59
|
+
.describe('Short identifier for the folder, e.g. "user-auth" or "PROJ-123". Do NOT append version numbers (v2, v3, etc.).'),
|
|
60
|
+
},
|
|
61
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
57
62
|
}, async (args) => handleSaveHandoff(args, getWorkspaceRoot()));
|
|
58
63
|
}
|
|
59
64
|
// Export DEFAULT_EXTENSIONS for backward compatibility
|
package/dist/mcp/integration.js
CHANGED
|
@@ -74,14 +74,19 @@ async function callRestApi(url, auth, accept = 'application/json') {
|
|
|
74
74
|
}
|
|
75
75
|
export function registerIntegrationTools(server) {
|
|
76
76
|
// TOOL: GET BITBUCKET PR
|
|
77
|
-
server.
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
.
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
77
|
+
server.registerTool('kit_get_bitbucket_pr', {
|
|
78
|
+
title: 'Get Bitbucket PR',
|
|
79
|
+
description: 'Get Bitbucket PR details and optionally the diff. Accepts a full PR URL or a numeric PR ID with workspace + repoSlug.',
|
|
80
|
+
inputSchema: {
|
|
81
|
+
input: z.string().describe('Bitbucket PR URL or numeric PR ID'),
|
|
82
|
+
workspace: z
|
|
83
|
+
.string()
|
|
84
|
+
.optional()
|
|
85
|
+
.describe('Bitbucket workspace slug (required for numeric ID if BITBUCKET_DEFAULT_WORKSPACE not set)'),
|
|
86
|
+
repoSlug: z.string().optional().describe('Bitbucket repo slug (required for numeric ID)'),
|
|
87
|
+
includeDiff: z.boolean().optional().default(true).describe('Include unified diff in response'),
|
|
88
|
+
},
|
|
89
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
85
90
|
}, async ({ input, workspace, repoSlug, includeDiff }) => {
|
|
86
91
|
try {
|
|
87
92
|
let ws;
|
|
@@ -145,8 +150,13 @@ ${pr.description || 'No description'}`;
|
|
|
145
150
|
}
|
|
146
151
|
});
|
|
147
152
|
// TOOL: JIRA GET TICKET
|
|
148
|
-
server.
|
|
149
|
-
|
|
153
|
+
server.registerTool('kit_jira_get_ticket', {
|
|
154
|
+
title: 'Get Jira Ticket',
|
|
155
|
+
description: 'Get ticket details from Jira using the Atlassian REST API',
|
|
156
|
+
inputSchema: {
|
|
157
|
+
ticketId: z.string().describe('Jira ticket ID (e.g., PROJ-123)'),
|
|
158
|
+
},
|
|
159
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
150
160
|
}, async ({ ticketId }) => {
|
|
151
161
|
try {
|
|
152
162
|
const safeTicketId = ticketId.match(/^[A-Z]+-\d+$/)?.[0];
|
package/dist/mcp/memory.d.ts
CHANGED
|
@@ -1,16 +1,11 @@
|
|
|
1
1
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
-
import {
|
|
3
|
-
import { MemoryStore } from '../services/memory/store.js';
|
|
4
|
-
import type { MemoryConfig } from '../services/memory/types.js';
|
|
2
|
+
import { type MemorySubsystem, type MemorySubsystemOverrides } from '../services/memory/subsystem.js';
|
|
5
3
|
import { type ProjectSettings } from '../core/config/index.js';
|
|
6
4
|
/**
|
|
7
5
|
* Initializes the memory subsystem and registers all memory tools.
|
|
8
|
-
* Returns the
|
|
6
|
+
* Returns the MemorySubsystem so the caller can fire startWarmup() after server.connect().
|
|
9
7
|
* Returns null when memory is disabled (settings.memory.enabled !== true).
|
|
10
8
|
*/
|
|
11
|
-
export declare function registerMemoryTools(server: McpServer, workspaceRoot: string, overrides?: {
|
|
12
|
-
indexer?: MemoryIndexer;
|
|
13
|
-
store?: MemoryStore;
|
|
14
|
-
config?: MemoryConfig;
|
|
9
|
+
export declare function registerMemoryTools(server: McpServer, workspaceRoot: string, overrides?: MemorySubsystemOverrides & {
|
|
15
10
|
settings?: ProjectSettings;
|
|
16
|
-
}):
|
|
11
|
+
}): MemorySubsystem | null;
|
package/dist/mcp/memory.js
CHANGED
|
@@ -1,29 +1,38 @@
|
|
|
1
1
|
import * as fs from 'fs';
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
import { z } from 'zod';
|
|
4
|
-
import {
|
|
5
|
-
import { MemoryIndexer } from '../services/memory/indexer.js';
|
|
6
|
-
import { MemoryStore } from '../services/memory/store.js';
|
|
4
|
+
import { createMemorySubsystem, } from '../services/memory/subsystem.js';
|
|
7
5
|
import { SOURCE_TYPES } from '../services/memory/types.js';
|
|
8
6
|
import { initializeConversationDigestModel } from '../services/digest/processor.js';
|
|
9
7
|
import { DEFAULT_DIGEST_MODEL_ID } from '../services/digest/constants.js';
|
|
10
8
|
import { formatError, mcpJson, mcpText } from '../utils/utils.js';
|
|
11
|
-
import { loadGlobalSettings, resolveConversationDigestConfig
|
|
9
|
+
import { loadGlobalSettings, resolveConversationDigestConfig } from '../core/config/index.js';
|
|
12
10
|
/**
|
|
13
|
-
* Registers tool handlers
|
|
11
|
+
* Registers tool handlers against the current memory subsystem state.
|
|
14
12
|
*/
|
|
15
|
-
function registerMemoryToolHandlers(server,
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
13
|
+
function registerMemoryToolHandlers(server, subsystem, workspaceRoot = process.cwd()) {
|
|
14
|
+
const { config } = subsystem;
|
|
15
|
+
server.registerTool('kit_memory_search', {
|
|
16
|
+
title: 'Search Memory',
|
|
17
|
+
description: 'Search persistent memory for factual or semantic matches (decisions, concepts, entities). Extract concise keywords only, removing stopwords and filler words from the user request before querying. Use kit_memory_recent for temporal or recency questions.',
|
|
18
|
+
inputSchema: {
|
|
19
|
+
query: z.string().min(1).describe('Keyword-only search query with stopwords removed'),
|
|
20
|
+
top_k: z.number().int().positive().optional().describe('Number of results to return'),
|
|
21
|
+
},
|
|
22
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
19
23
|
}, async ({ query, top_k }) => {
|
|
20
24
|
try {
|
|
25
|
+
if (!subsystem.indexer || !subsystem.store) {
|
|
26
|
+
return mcpText(formatInitializationFailure(subsystem));
|
|
27
|
+
}
|
|
28
|
+
const { indexer, store } = subsystem;
|
|
21
29
|
const results = await indexer.search(query, top_k ?? config.topK);
|
|
30
|
+
const lifecycleNote = formatLifecycleNote(subsystem);
|
|
22
31
|
const degradedNote = !store.vecAvailable
|
|
23
32
|
? '⚠️ Vector search unavailable — showing keyword-only results.\n\n'
|
|
24
33
|
: '';
|
|
25
34
|
if (results.length === 0) {
|
|
26
|
-
return mcpText(`${degradedNote}No memories found for query: "${query}"`);
|
|
35
|
+
return mcpText(`${lifecycleNote}${degradedNote}No memories found for query: "${query}"`);
|
|
27
36
|
}
|
|
28
37
|
const formatted = results
|
|
29
38
|
.map((r) => {
|
|
@@ -34,17 +43,26 @@ function registerMemoryToolHandlers(server, indexer, store, config, workspaceRoo
|
|
|
34
43
|
return `### ${displaySource} (score: ${r.score.toFixed(3)})\n${content}`;
|
|
35
44
|
})
|
|
36
45
|
.join('\n\n---\n\n');
|
|
37
|
-
return mcpText(`${degradedNote}${formatted}`);
|
|
46
|
+
return mcpText(`${lifecycleNote}${degradedNote}${formatted}`);
|
|
38
47
|
}
|
|
39
48
|
catch (err) {
|
|
40
49
|
return mcpText(`kit_memory_search failed: ${formatError(err)}`);
|
|
41
50
|
}
|
|
42
51
|
});
|
|
43
|
-
server.
|
|
44
|
-
|
|
45
|
-
|
|
52
|
+
server.registerTool('kit_memory_recent', {
|
|
53
|
+
title: 'Recent Memory Sources',
|
|
54
|
+
description: 'Return the N most recently updated memory sources. Optionally filter by source type. Use for temporal questions like "what did we do last session" or "recent".',
|
|
55
|
+
inputSchema: {
|
|
56
|
+
n: z.number().int().positive().max(50).optional().describe('How many recent sources to return (default 5)'),
|
|
57
|
+
source_type: z.enum(SOURCE_TYPES).optional().describe('Optional source type filter'),
|
|
58
|
+
},
|
|
59
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
46
60
|
}, async ({ n, source_type }) => {
|
|
47
61
|
try {
|
|
62
|
+
if (!subsystem.store) {
|
|
63
|
+
return mcpText(formatInitializationFailure(subsystem));
|
|
64
|
+
}
|
|
65
|
+
const store = subsystem.store;
|
|
48
66
|
const rows = store.getRecentSources({ limit: n ?? 5, sourceType: source_type });
|
|
49
67
|
if (rows.length === 0)
|
|
50
68
|
return mcpText('No recent sources found.');
|
|
@@ -67,10 +85,22 @@ function registerMemoryToolHandlers(server, indexer, store, config, workspaceRoo
|
|
|
67
85
|
return mcpText(`kit_memory_recent failed: ${formatError(err)}`);
|
|
68
86
|
}
|
|
69
87
|
});
|
|
70
|
-
server.
|
|
71
|
-
|
|
88
|
+
server.registerTool('kit_memory_save', {
|
|
89
|
+
title: 'Save to Memory',
|
|
90
|
+
description: 'Save content to wiki/raw for inclusion after the next /wiki compile.',
|
|
91
|
+
inputSchema: {
|
|
92
|
+
content: z.string().min(1).describe('Content to save to memory'),
|
|
93
|
+
},
|
|
94
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
|
|
72
95
|
}, async ({ content }) => {
|
|
73
96
|
try {
|
|
97
|
+
if (!subsystem.indexer) {
|
|
98
|
+
return mcpJson({
|
|
99
|
+
saved: false,
|
|
100
|
+
error: formatInitializationFailure(subsystem),
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
const indexer = subsystem.indexer;
|
|
74
104
|
await indexer.save(content);
|
|
75
105
|
return mcpJson({
|
|
76
106
|
saved: true,
|
|
@@ -85,9 +115,14 @@ function registerMemoryToolHandlers(server, indexer, store, config, workspaceRoo
|
|
|
85
115
|
});
|
|
86
116
|
}
|
|
87
117
|
});
|
|
88
|
-
server.
|
|
89
|
-
|
|
90
|
-
|
|
118
|
+
server.registerTool('kit_memory_digest_init', {
|
|
119
|
+
title: 'Initialize Conversation Digest',
|
|
120
|
+
description: 'Initialize or toggle local conversation digesting. Downloads/cache-loads the pinned local model only when explicitly invoked.',
|
|
121
|
+
inputSchema: {
|
|
122
|
+
model_id: z.string().optional().describe('Digest model id'),
|
|
123
|
+
enabled: z.boolean().optional().describe('Enable or disable conversation digesting'),
|
|
124
|
+
},
|
|
125
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
91
126
|
}, async ({ model_id, enabled }) => {
|
|
92
127
|
try {
|
|
93
128
|
const existingConfig = resolveConversationDigestConfig(loadGlobalSettings());
|
|
@@ -109,22 +144,31 @@ function registerMemoryToolHandlers(server, indexer, store, config, workspaceRoo
|
|
|
109
144
|
}
|
|
110
145
|
});
|
|
111
146
|
}
|
|
147
|
+
function formatLifecycleNote(subsystem) {
|
|
148
|
+
if (subsystem.status.state === 'warming' || subsystem.status.state === 'initializing') {
|
|
149
|
+
return '⚠️ Memory index is warming — results may be stale until startup indexing completes.\n\n';
|
|
150
|
+
}
|
|
151
|
+
if (subsystem.status.state === 'degraded') {
|
|
152
|
+
return `⚠️ Memory index is degraded${subsystem.status.error ? `: ${subsystem.status.error}` : ''}.\n\n`;
|
|
153
|
+
}
|
|
154
|
+
return '';
|
|
155
|
+
}
|
|
156
|
+
function formatInitializationFailure(subsystem) {
|
|
157
|
+
const detail = subsystem.status.error ? `: ${subsystem.status.error}` : '';
|
|
158
|
+
return `Memory initialization failed${detail}`;
|
|
159
|
+
}
|
|
112
160
|
/**
|
|
113
161
|
* Initializes the memory subsystem and registers all memory tools.
|
|
114
|
-
* Returns the
|
|
162
|
+
* Returns the MemorySubsystem so the caller can fire startWarmup() after server.connect().
|
|
115
163
|
* Returns null when memory is disabled (settings.memory.enabled !== true).
|
|
116
164
|
*/
|
|
117
165
|
export function registerMemoryTools(server, workspaceRoot, overrides) {
|
|
118
166
|
const settings = overrides?.settings ?? loadGlobalSettings();
|
|
119
167
|
if (settings.memory?.enabled !== true)
|
|
120
168
|
return null;
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
return new MemoryIndexer(store, embedder, config);
|
|
127
|
-
})();
|
|
128
|
-
registerMemoryToolHandlers(server, indexer, store, config, workspaceRoot);
|
|
129
|
-
return indexer;
|
|
169
|
+
const subsystem = createMemorySubsystem(workspaceRoot, { ...overrides, settings });
|
|
170
|
+
if (!subsystem)
|
|
171
|
+
return null;
|
|
172
|
+
registerMemoryToolHandlers(server, subsystem, workspaceRoot);
|
|
173
|
+
return subsystem;
|
|
130
174
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import * as assert from 'node:assert/strict';
|
|
2
|
+
import { describe, test } from 'node:test';
|
|
3
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
4
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
5
|
+
import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js';
|
|
6
|
+
import { registerCoreTools } from './core.js';
|
|
7
|
+
import { registerAgentTools } from './agent.js';
|
|
8
|
+
import { registerIntegrationTools } from './integration.js';
|
|
9
|
+
const EXPECTED_NON_MEMORY_TOOLS = [
|
|
10
|
+
'kit_save_handoff',
|
|
11
|
+
'kit_trigger_agent',
|
|
12
|
+
'kit_get_bitbucket_pr',
|
|
13
|
+
'kit_jira_get_ticket',
|
|
14
|
+
];
|
|
15
|
+
async function buildClientWithTools() {
|
|
16
|
+
const server = new McpServer({ name: 'test-kit', version: '0.0.0' });
|
|
17
|
+
registerIntegrationTools(server);
|
|
18
|
+
registerCoreTools(server);
|
|
19
|
+
registerAgentTools(server);
|
|
20
|
+
const [serverTransport, clientTransport] = InMemoryTransport.createLinkedPair();
|
|
21
|
+
const client = new Client({ name: 'test-client', version: '0.0.0' });
|
|
22
|
+
await Promise.all([server.connect(serverTransport), client.connect(clientTransport)]);
|
|
23
|
+
return client;
|
|
24
|
+
}
|
|
25
|
+
describe('MCP tool registration smoke tests', () => {
|
|
26
|
+
test('R1: all 4 non-memory tool names are registered', async () => {
|
|
27
|
+
const client = await buildClientWithTools();
|
|
28
|
+
const { tools } = await client.listTools();
|
|
29
|
+
const names = tools.map((t) => t.name);
|
|
30
|
+
for (const expected of EXPECTED_NON_MEMORY_TOOLS) {
|
|
31
|
+
assert.ok(names.includes(expected), `expected tool "${expected}" to be registered, got: ${names.join(', ')}`);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
test('R2: each non-memory tool has a non-empty description and inputSchema (BC3)', async () => {
|
|
35
|
+
const client = await buildClientWithTools();
|
|
36
|
+
const { tools } = await client.listTools();
|
|
37
|
+
const byName = Object.fromEntries(tools.map((t) => [t.name, t]));
|
|
38
|
+
for (const name of EXPECTED_NON_MEMORY_TOOLS) {
|
|
39
|
+
const tool = byName[name];
|
|
40
|
+
assert.ok(tool, `tool "${name}" not found`);
|
|
41
|
+
assert.ok(tool.description && tool.description.length > 0, `tool "${name}" must have a non-empty description`);
|
|
42
|
+
assert.ok(tool.inputSchema, `tool "${name}" must have an inputSchema`);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
test('R3: kit_trigger_agent (readOnlyHint:false) has correct annotations (BC5)', async () => {
|
|
46
|
+
const client = await buildClientWithTools();
|
|
47
|
+
const { tools } = await client.listTools();
|
|
48
|
+
const tool = tools.find((t) => t.name === 'kit_trigger_agent');
|
|
49
|
+
assert.ok(tool, 'kit_trigger_agent must be registered');
|
|
50
|
+
assert.ok(tool.annotations, 'kit_trigger_agent must have annotations');
|
|
51
|
+
assert.equal(tool.annotations.readOnlyHint, false, 'readOnlyHint must be false');
|
|
52
|
+
assert.equal(tool.annotations.destructiveHint, true, 'destructiveHint must be true');
|
|
53
|
+
assert.equal(tool.annotations.idempotentHint, false, 'idempotentHint must be false');
|
|
54
|
+
assert.equal(tool.annotations.openWorldHint, true, 'openWorldHint must be true');
|
|
55
|
+
});
|
|
56
|
+
test('R4: kit_save_handoff (readOnlyHint:false) has correct annotations (BC5)', async () => {
|
|
57
|
+
const client = await buildClientWithTools();
|
|
58
|
+
const { tools } = await client.listTools();
|
|
59
|
+
const tool = tools.find((t) => t.name === 'kit_save_handoff');
|
|
60
|
+
assert.ok(tool, 'kit_save_handoff must be registered');
|
|
61
|
+
assert.ok(tool.annotations, 'kit_save_handoff must have annotations');
|
|
62
|
+
assert.equal(tool.annotations.readOnlyHint, false, 'readOnlyHint must be false');
|
|
63
|
+
assert.equal(tool.annotations.destructiveHint, false, 'destructiveHint must be false');
|
|
64
|
+
assert.equal(tool.annotations.idempotentHint, true, 'idempotentHint must be true');
|
|
65
|
+
assert.equal(tool.annotations.openWorldHint, false, 'openWorldHint must be false');
|
|
66
|
+
});
|
|
67
|
+
test('R5: kit_get_bitbucket_pr (readOnlyHint:true) has correct annotations (BC5)', async () => {
|
|
68
|
+
const client = await buildClientWithTools();
|
|
69
|
+
const { tools } = await client.listTools();
|
|
70
|
+
const tool = tools.find((t) => t.name === 'kit_get_bitbucket_pr');
|
|
71
|
+
assert.ok(tool, 'kit_get_bitbucket_pr must be registered');
|
|
72
|
+
assert.ok(tool.annotations, 'kit_get_bitbucket_pr must have annotations');
|
|
73
|
+
assert.equal(tool.annotations.readOnlyHint, true, 'readOnlyHint must be true');
|
|
74
|
+
assert.equal(tool.annotations.idempotentHint, true, 'idempotentHint must be true');
|
|
75
|
+
assert.equal(tool.annotations.openWorldHint, true, 'openWorldHint must be true');
|
|
76
|
+
});
|
|
77
|
+
});
|