@hanhnd/agent-kit 1.0.40 → 1.0.42

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 CHANGED
@@ -15136,26 +15136,26 @@ var DigestModelRegistryError = class extends Error {
15136
15136
  var DIGEST_MODEL_REGISTRY = {
15137
15137
  ["tiny" /* TINY */]: {
15138
15138
  id: "tiny" /* TINY */,
15139
- ggufUri: "hf:bartowski/Qwen2.5-0.5B-Instruct-GGUF/Qwen2.5-0.5B-Instruct-Q4_K_M.gguf",
15140
- approxSizeBytes: 4e8,
15139
+ ggufUri: "hf:lmstudio-community/Qwen3.5-0.8B-GGUF/Qwen3.5-0.8B-Q4_K_M.gguf",
15140
+ approxSizeBytes: 528e6,
15141
15141
  license: "Apache-2.0",
15142
- sourceUrl: "https://huggingface.co/bartowski/Qwen2.5-0.5B-Instruct-GGUF",
15142
+ sourceUrl: "https://huggingface.co/lmstudio-community/Qwen3.5-0.8B-GGUF",
15143
15143
  enabled: true
15144
15144
  },
15145
15145
  ["base" /* BASE */]: {
15146
15146
  id: "base" /* BASE */,
15147
- ggufUri: "hf:bartowski/Qwen2.5-1.5B-Instruct-GGUF/Qwen2.5-1.5B-Instruct-Q4_K_M.gguf",
15148
- approxSizeBytes: 1e9,
15147
+ ggufUri: "hf:lmstudio-community/Qwen3.5-2B-GGUF/Qwen3.5-2B-Q4_K_M.gguf",
15148
+ approxSizeBytes: 127e7,
15149
15149
  license: "Apache-2.0",
15150
- sourceUrl: "https://huggingface.co/bartowski/Qwen2.5-1.5B-Instruct-GGUF",
15150
+ sourceUrl: "https://huggingface.co/lmstudio-community/Qwen3.5-2B-GGUF",
15151
15151
  enabled: true
15152
15152
  },
15153
15153
  ["large" /* LARGE */]: {
15154
15154
  id: "large" /* LARGE */,
15155
- ggufUri: "hf:bartowski/Qwen2.5-3B-Instruct-GGUF/Qwen2.5-3B-Instruct-Q4_K_M.gguf",
15156
- approxSizeBytes: 19e8,
15155
+ ggufUri: "hf:lmstudio-community/Qwen3.5-4B-GGUF/Qwen3.5-4B-Q4_K_M.gguf",
15156
+ approxSizeBytes: 271e7,
15157
15157
  license: "Apache-2.0",
15158
- sourceUrl: "https://huggingface.co/bartowski/Qwen2.5-3B-Instruct-GGUF",
15158
+ sourceUrl: "https://huggingface.co/lmstudio-community/Qwen3.5-4B-GGUF",
15159
15159
  enabled: true
15160
15160
  }
15161
15161
  };
@@ -15315,7 +15315,8 @@ async function createLlamaLocalDigestProvider(modelId) {
15315
15315
  const response = await withTimeout(
15316
15316
  session.prompt(userContent, {
15317
15317
  maxTokens: LLAMA_MAX_GENERATED_TOKENS,
15318
- temperature: LLAMA_TEMPERATURE
15318
+ temperature: LLAMA_TEMPERATURE,
15319
+ budgets: { thoughtTokens: 0 }
15319
15320
  }),
15320
15321
  options.timeoutMs,
15321
15322
  () => new LlamaLocalDigestProviderError(`Llama provider timed out after ${options.timeoutMs}ms`)
@@ -1,6 +1,6 @@
1
- export declare const HANDOFF_TYPES: readonly ["brainstorm", "clarification", "plan", "ticket", "research", "scenario", "investigation", "code"];
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' | 'scenario' | 'investigation' | 'code';
3
+ type CanonicalHandoffType = 'brainstorm' | 'clarification' | 'plan' | 'ticket' | 'research' | 'investigation' | 'code';
4
4
  export interface SavedHandoffFolderLocation {
5
5
  featureSlug: string;
6
6
  canonicalType: CanonicalHandoffType;
@@ -5,7 +5,6 @@ export const HANDOFF_TYPES = [
5
5
  'plan',
6
6
  'ticket',
7
7
  'research',
8
- 'scenario',
9
8
  'investigation',
10
9
  'code',
11
10
  ];
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.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 folder (.agent-kit/handoffs/feature-slug/plan).', {
53
- agent: z.enum(['gemini', 'claude', 'codex']).describe('The agent CLI to invoke: "gemini", "claude", or "codex"'),
54
- task: z
55
- .string()
56
- .describe('Task message or path to a handoff folder (e.g., ".agent-kit/handoffs/feature-slug/plan")'),
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.tool('kit_save_handoff', `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.`, {
50
- type: z.enum(HANDOFF_TYPES).describe('Handoff type'),
51
- files: z
52
- .record(z.string(), z.string())
53
- .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$/.'),
54
- slug: z
55
- .string()
56
- .describe('Short identifier for the folder, e.g. "user-auth" or "PROJ-123". Do NOT append version numbers (v2, v3, etc.).'),
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
@@ -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.tool('kit_get_bitbucket_pr', 'Get Bitbucket PR details and optionally the diff. Accepts a full PR URL or a numeric PR ID with workspace + repoSlug.', {
78
- input: z.string().describe('Bitbucket PR URL or numeric PR ID'),
79
- workspace: z
80
- .string()
81
- .optional()
82
- .describe('Bitbucket workspace slug (required for numeric ID if BITBUCKET_DEFAULT_WORKSPACE not set)'),
83
- repoSlug: z.string().optional().describe('Bitbucket repo slug (required for numeric ID)'),
84
- includeDiff: z.boolean().optional().default(true).describe('Include unified diff in response'),
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.tool('kit_jira_get_ticket', 'Get ticket details from Jira using the Atlassian REST API', {
149
- ticketId: z.string().describe('Jira ticket ID (e.g., PROJ-123)'),
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];
@@ -12,9 +12,14 @@ import { loadGlobalSettings, resolveConversationDigestConfig } from '../core/con
12
12
  */
13
13
  function registerMemoryToolHandlers(server, subsystem, workspaceRoot = process.cwd()) {
14
14
  const { config } = subsystem;
15
- server.tool('kit_memory_search', '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.', {
16
- query: z.string().min(1).describe('Keyword-only search query with stopwords removed'),
17
- top_k: z.number().int().positive().optional().describe('Number of results to return'),
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 },
18
23
  }, async ({ query, top_k }) => {
19
24
  try {
20
25
  if (!subsystem.indexer || !subsystem.store) {
@@ -44,9 +49,14 @@ function registerMemoryToolHandlers(server, subsystem, workspaceRoot = process.c
44
49
  return mcpText(`kit_memory_search failed: ${formatError(err)}`);
45
50
  }
46
51
  });
47
- server.tool('kit_memory_recent', '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".', {
48
- n: z.number().int().positive().max(50).optional().describe('How many recent sources to return (default 5)'),
49
- source_type: z.enum(SOURCE_TYPES).optional().describe('Optional source type filter'),
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 },
50
60
  }, async ({ n, source_type }) => {
51
61
  try {
52
62
  if (!subsystem.store) {
@@ -75,8 +85,13 @@ function registerMemoryToolHandlers(server, subsystem, workspaceRoot = process.c
75
85
  return mcpText(`kit_memory_recent failed: ${formatError(err)}`);
76
86
  }
77
87
  });
78
- server.tool('kit_memory_save', 'Save content to wiki/raw for inclusion after the next /wiki compile.', {
79
- content: z.string().min(1).describe('Content to save to memory'),
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 },
80
95
  }, async ({ content }) => {
81
96
  try {
82
97
  if (!subsystem.indexer) {
@@ -100,9 +115,14 @@ function registerMemoryToolHandlers(server, subsystem, workspaceRoot = process.c
100
115
  });
101
116
  }
102
117
  });
103
- server.tool('kit_memory_digest_init', 'Initialize or toggle local conversation digesting. Downloads/cache-loads the pinned local model only when explicitly invoked.', {
104
- model_id: z.string().optional().describe('Digest model id'),
105
- enabled: z.boolean().optional().describe('Enable or disable conversation digesting'),
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 },
106
126
  }, async ({ model_id, enabled }) => {
107
127
  try {
108
128
  const existingConfig = resolveConversationDigestConfig(loadGlobalSettings());
@@ -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
+ });
package/dist/server.js CHANGED
@@ -36078,7 +36078,6 @@ var HANDOFF_TYPES = [
36078
36078
  "plan",
36079
36079
  "ticket",
36080
36080
  "research",
36081
- "scenario",
36082
36081
  "investigation",
36083
36082
  "code"
36084
36083
  ];
@@ -36161,17 +36160,21 @@ function handleSaveHandoff(args, workspaceRoot2) {
36161
36160
  }
36162
36161
  }
36163
36162
  function registerCoreTools(server2) {
36164
- server2.tool(
36163
+ server2.registerTool(
36165
36164
  "kit_save_handoff",
36166
- `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.`,
36167
36165
  {
36168
- type: external_exports.enum(HANDOFF_TYPES).describe("Handoff type"),
36169
- files: external_exports.record(external_exports.string(), external_exports.string()).describe(
36170
- "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$/."
36171
- ),
36172
- slug: external_exports.string().describe(
36173
- 'Short identifier for the folder, e.g. "user-auth" or "PROJ-123". Do NOT append version numbers (v2, v3, etc.).'
36174
- )
36166
+ title: "Save Handoff Artifact",
36167
+ 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.`,
36168
+ inputSchema: {
36169
+ type: external_exports.enum(HANDOFF_TYPES).describe("Handoff type"),
36170
+ files: external_exports.record(external_exports.string(), external_exports.string()).describe(
36171
+ "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$/."
36172
+ ),
36173
+ slug: external_exports.string().describe(
36174
+ 'Short identifier for the folder, e.g. "user-auth" or "PROJ-123". Do NOT append version numbers (v2, v3, etc.).'
36175
+ )
36176
+ },
36177
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }
36175
36178
  },
36176
36179
  async (args) => handleSaveHandoff(args, getWorkspaceRoot())
36177
36180
  );
@@ -36281,12 +36284,16 @@ ${"\u2500".repeat(60)}
36281
36284
  return { logPath: path6.relative(workspaceRoot2, logFile), logStream };
36282
36285
  }
36283
36286
  function registerAgentTools(server2) {
36284
- server2.tool(
36287
+ server2.registerTool(
36285
36288
  "kit_trigger_agent",
36286
- "!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).",
36287
36289
  {
36288
- agent: external_exports.enum(["gemini", "claude", "codex"]).describe('The agent CLI to invoke: "gemini", "claude", or "codex"'),
36289
- task: external_exports.string().describe('Task message or path to a handoff folder (e.g., ".agent-kit/handoffs/feature-slug/plan")')
36290
+ title: "Delegate to External Agent",
36291
+ 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).",
36292
+ inputSchema: {
36293
+ agent: external_exports.enum(["gemini", "claude", "codex"]).describe('The agent CLI to invoke: "gemini", "claude", or "codex"'),
36294
+ task: external_exports.string().describe('Task message or path to a handoff folder (e.g., ".agent-kit/handoffs/feature-slug/plan")')
36295
+ },
36296
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true }
36290
36297
  },
36291
36298
  async ({ agent, task }, extra) => {
36292
36299
  const workspaceRoot2 = getWorkspaceRoot();
@@ -36641,14 +36648,18 @@ async function callRestApi(url, auth, accept = "application/json") {
36641
36648
  return accept === "text/plain" ? resp.text() : resp.json();
36642
36649
  }
36643
36650
  function registerIntegrationTools(server2) {
36644
- server2.tool(
36651
+ server2.registerTool(
36645
36652
  "kit_get_bitbucket_pr",
36646
- "Get Bitbucket PR details and optionally the diff. Accepts a full PR URL or a numeric PR ID with workspace + repoSlug.",
36647
36653
  {
36648
- input: external_exports.string().describe("Bitbucket PR URL or numeric PR ID"),
36649
- workspace: external_exports.string().optional().describe("Bitbucket workspace slug (required for numeric ID if BITBUCKET_DEFAULT_WORKSPACE not set)"),
36650
- repoSlug: external_exports.string().optional().describe("Bitbucket repo slug (required for numeric ID)"),
36651
- includeDiff: external_exports.boolean().optional().default(true).describe("Include unified diff in response")
36654
+ title: "Get Bitbucket PR",
36655
+ description: "Get Bitbucket PR details and optionally the diff. Accepts a full PR URL or a numeric PR ID with workspace + repoSlug.",
36656
+ inputSchema: {
36657
+ input: external_exports.string().describe("Bitbucket PR URL or numeric PR ID"),
36658
+ workspace: external_exports.string().optional().describe("Bitbucket workspace slug (required for numeric ID if BITBUCKET_DEFAULT_WORKSPACE not set)"),
36659
+ repoSlug: external_exports.string().optional().describe("Bitbucket repo slug (required for numeric ID)"),
36660
+ includeDiff: external_exports.boolean().optional().default(true).describe("Include unified diff in response")
36661
+ },
36662
+ annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true }
36652
36663
  },
36653
36664
  async ({ input, workspace, repoSlug, includeDiff }) => {
36654
36665
  try {
@@ -36725,11 +36736,15 @@ ${diff}
36725
36736
  }
36726
36737
  }
36727
36738
  );
36728
- server2.tool(
36739
+ server2.registerTool(
36729
36740
  "kit_jira_get_ticket",
36730
- "Get ticket details from Jira using the Atlassian REST API",
36731
36741
  {
36732
- ticketId: external_exports.string().describe("Jira ticket ID (e.g., PROJ-123)")
36742
+ title: "Get Jira Ticket",
36743
+ description: "Get ticket details from Jira using the Atlassian REST API",
36744
+ inputSchema: {
36745
+ ticketId: external_exports.string().describe("Jira ticket ID (e.g., PROJ-123)")
36746
+ },
36747
+ annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true }
36733
36748
  },
36734
36749
  async ({ ticketId }) => {
36735
36750
  try {
@@ -37796,26 +37811,26 @@ var DigestModelRegistryError = class extends Error {
37796
37811
  var DIGEST_MODEL_REGISTRY = {
37797
37812
  ["tiny" /* TINY */]: {
37798
37813
  id: "tiny" /* TINY */,
37799
- ggufUri: "hf:bartowski/Qwen2.5-0.5B-Instruct-GGUF/Qwen2.5-0.5B-Instruct-Q4_K_M.gguf",
37800
- approxSizeBytes: 4e8,
37814
+ ggufUri: "hf:lmstudio-community/Qwen3.5-0.8B-GGUF/Qwen3.5-0.8B-Q4_K_M.gguf",
37815
+ approxSizeBytes: 528e6,
37801
37816
  license: "Apache-2.0",
37802
- sourceUrl: "https://huggingface.co/bartowski/Qwen2.5-0.5B-Instruct-GGUF",
37817
+ sourceUrl: "https://huggingface.co/lmstudio-community/Qwen3.5-0.8B-GGUF",
37803
37818
  enabled: true
37804
37819
  },
37805
37820
  ["base" /* BASE */]: {
37806
37821
  id: "base" /* BASE */,
37807
- ggufUri: "hf:bartowski/Qwen2.5-1.5B-Instruct-GGUF/Qwen2.5-1.5B-Instruct-Q4_K_M.gguf",
37808
- approxSizeBytes: 1e9,
37822
+ ggufUri: "hf:lmstudio-community/Qwen3.5-2B-GGUF/Qwen3.5-2B-Q4_K_M.gguf",
37823
+ approxSizeBytes: 127e7,
37809
37824
  license: "Apache-2.0",
37810
- sourceUrl: "https://huggingface.co/bartowski/Qwen2.5-1.5B-Instruct-GGUF",
37825
+ sourceUrl: "https://huggingface.co/lmstudio-community/Qwen3.5-2B-GGUF",
37811
37826
  enabled: true
37812
37827
  },
37813
37828
  ["large" /* LARGE */]: {
37814
37829
  id: "large" /* LARGE */,
37815
- ggufUri: "hf:bartowski/Qwen2.5-3B-Instruct-GGUF/Qwen2.5-3B-Instruct-Q4_K_M.gguf",
37816
- approxSizeBytes: 19e8,
37830
+ ggufUri: "hf:lmstudio-community/Qwen3.5-4B-GGUF/Qwen3.5-4B-Q4_K_M.gguf",
37831
+ approxSizeBytes: 271e7,
37817
37832
  license: "Apache-2.0",
37818
- sourceUrl: "https://huggingface.co/bartowski/Qwen2.5-3B-Instruct-GGUF",
37833
+ sourceUrl: "https://huggingface.co/lmstudio-community/Qwen3.5-4B-GGUF",
37819
37834
  enabled: true
37820
37835
  }
37821
37836
  };
@@ -37933,7 +37948,8 @@ async function createLlamaLocalDigestProvider(modelId) {
37933
37948
  const response = await withTimeout(
37934
37949
  session.prompt(userContent, {
37935
37950
  maxTokens: LLAMA_MAX_GENERATED_TOKENS,
37936
- temperature: LLAMA_TEMPERATURE
37951
+ temperature: LLAMA_TEMPERATURE,
37952
+ budgets: { thoughtTokens: 0 }
37937
37953
  }),
37938
37954
  options.timeoutMs,
37939
37955
  () => new LlamaLocalDigestProviderError(`Llama provider timed out after ${options.timeoutMs}ms`)
@@ -38143,12 +38159,16 @@ async function initializeConversationDigestModel(input) {
38143
38159
  // src/mcp/memory.ts
38144
38160
  function registerMemoryToolHandlers(server2, subsystem, workspaceRoot2 = process.cwd()) {
38145
38161
  const { config: config2 } = subsystem;
38146
- server2.tool(
38162
+ server2.registerTool(
38147
38163
  "kit_memory_search",
38148
- "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.",
38149
38164
  {
38150
- query: external_exports.string().min(1).describe("Keyword-only search query with stopwords removed"),
38151
- top_k: external_exports.number().int().positive().optional().describe("Number of results to return")
38165
+ title: "Search Memory",
38166
+ 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.",
38167
+ inputSchema: {
38168
+ query: external_exports.string().min(1).describe("Keyword-only search query with stopwords removed"),
38169
+ top_k: external_exports.number().int().positive().optional().describe("Number of results to return")
38170
+ },
38171
+ annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false }
38152
38172
  },
38153
38173
  async ({ query, top_k }) => {
38154
38174
  try {
@@ -38175,12 +38195,16 @@ ${content}`;
38175
38195
  }
38176
38196
  }
38177
38197
  );
38178
- server2.tool(
38198
+ server2.registerTool(
38179
38199
  "kit_memory_recent",
38180
- '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".',
38181
38200
  {
38182
- n: external_exports.number().int().positive().max(50).optional().describe("How many recent sources to return (default 5)"),
38183
- source_type: external_exports.enum(SOURCE_TYPES).optional().describe("Optional source type filter")
38201
+ title: "Recent Memory Sources",
38202
+ 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".',
38203
+ inputSchema: {
38204
+ n: external_exports.number().int().positive().max(50).optional().describe("How many recent sources to return (default 5)"),
38205
+ source_type: external_exports.enum(SOURCE_TYPES).optional().describe("Optional source type filter")
38206
+ },
38207
+ annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false }
38184
38208
  },
38185
38209
  async ({ n, source_type }) => {
38186
38210
  try {
@@ -38208,11 +38232,15 @@ ${content}`);
38208
38232
  }
38209
38233
  }
38210
38234
  );
38211
- server2.tool(
38235
+ server2.registerTool(
38212
38236
  "kit_memory_save",
38213
- "Save content to wiki/raw for inclusion after the next /wiki compile.",
38214
38237
  {
38215
- content: external_exports.string().min(1).describe("Content to save to memory")
38238
+ title: "Save to Memory",
38239
+ description: "Save content to wiki/raw for inclusion after the next /wiki compile.",
38240
+ inputSchema: {
38241
+ content: external_exports.string().min(1).describe("Content to save to memory")
38242
+ },
38243
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }
38216
38244
  },
38217
38245
  async ({ content }) => {
38218
38246
  try {
@@ -38237,12 +38265,16 @@ ${content}`);
38237
38265
  }
38238
38266
  }
38239
38267
  );
38240
- server2.tool(
38268
+ server2.registerTool(
38241
38269
  "kit_memory_digest_init",
38242
- "Initialize or toggle local conversation digesting. Downloads/cache-loads the pinned local model only when explicitly invoked.",
38243
38270
  {
38244
- model_id: external_exports.string().optional().describe("Digest model id"),
38245
- enabled: external_exports.boolean().optional().describe("Enable or disable conversation digesting")
38271
+ title: "Initialize Conversation Digest",
38272
+ description: "Initialize or toggle local conversation digesting. Downloads/cache-loads the pinned local model only when explicitly invoked.",
38273
+ inputSchema: {
38274
+ model_id: external_exports.string().optional().describe("Digest model id"),
38275
+ enabled: external_exports.boolean().optional().describe("Enable or disable conversation digesting")
38276
+ },
38277
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true }
38246
38278
  },
38247
38279
  async ({ model_id, enabled }) => {
38248
38280
  try {
@@ -1,21 +1,64 @@
1
1
  import * as assert from 'node:assert/strict';
2
2
  import { describe, test } from 'node:test';
3
- import { createLlamaLocalDigestProvider } from '../digest/providers/llama-local.js';
4
- const INPUT = {
5
- sourcePath: '.agent-kit/wiki/archive/conversations/conv_2026-05-19T00-45-17-697Z.md',
6
- contentHash: 'tiny',
7
- content: '**User:** use local digest generation for archived conversations',
8
- };
9
- describe('createLlamaLocalDigestProvider', () => {
10
- test('runs an opt-in smoke test against a real model', { skip: !process.env.AGENT_KIT_DIGEST_MODEL_ID }, async () => {
3
+ import { createLlamaLocalDigestProvider, sanitizeConversationDigestMarkdown, titleFromSource, trimConversationExport, } from '../digest/providers/llama-local.js';
4
+ describe('titleFromSource', () => {
5
+ test('strips the extension and turns separators into spaces', () => {
6
+ assert.equal(titleFromSource('/a/b/conv_claude_1bda-2b2b.md'), 'conv claude 1bda 2b2b');
7
+ });
8
+ test('handles a bare filename with no directory', () => {
9
+ assert.equal(titleFromSource('inbox.md'), 'inbox');
10
+ });
11
+ });
12
+ describe('trimConversationExport', () => {
13
+ test('returns the trimmed content unchanged when within the limit', () => {
14
+ assert.equal(trimConversationExport(' hello world ', 100), 'hello world');
15
+ });
16
+ test('keeps the tail and prepends an omission notice when over the limit', () => {
17
+ const content = 'A'.repeat(50) + 'TAIL_MARKER';
18
+ const out = trimConversationExport(content, 11);
19
+ assert.match(out, /^\[Earlier conversation content omitted/);
20
+ assert.ok(out.endsWith('TAIL_MARKER'), 'keeps the most recent content');
21
+ assert.equal(out.split('\n\n')[1], 'TAIL_MARKER');
22
+ });
23
+ });
24
+ describe('sanitizeConversationDigestMarkdown', () => {
25
+ test('unwraps a fenced markdown block', () => {
26
+ const out = sanitizeConversationDigestMarkdown('```markdown\n# Title\n\nbody\n```');
27
+ assert.equal(out, '# Title\n\nbody\n');
28
+ });
29
+ test('leaves unfenced text intact and guarantees a single trailing newline', () => {
30
+ const out = sanitizeConversationDigestMarkdown(' plain summary ');
31
+ assert.equal(out, 'plain summary\n');
32
+ });
33
+ test('does not strip text that merely mentions a code fence inline', () => {
34
+ const out = sanitizeConversationDigestMarkdown('Use ```bash``` to run it.');
35
+ assert.equal(out, 'Use ```bash``` to run it.\n');
36
+ });
37
+ });
38
+ describe('createLlamaLocalDigestProvider (opt-in real model)', () => {
39
+ const INPUT = {
40
+ sourcePath: '.agent-kit/wiki/raw/conv_2026-05-19T00-45-17-697Z.md',
41
+ contentHash: 'tiny',
42
+ content: '**User:** use local digest generation for archived conversations',
43
+ };
44
+ test('generates a digest with the documented metadata header and a non-empty body', { skip: !process.env.AGENT_KIT_DIGEST_MODEL_ID }, async () => {
11
45
  const modelId = process.env.AGENT_KIT_DIGEST_MODEL_ID;
12
46
  const provider = await createLlamaLocalDigestProvider(modelId);
13
- const output = await provider.generateDigestMarkdown(INPUT, {
14
- modelId,
15
- maxInputChars: 1000,
16
- timeoutMs: 120_000,
17
- });
18
- assert.match(output, /^# Conversation Digest:/);
19
- assert.match(output, /Status: provisional/);
47
+ try {
48
+ const output = await provider.generateDigestMarkdown(INPUT, {
49
+ modelId,
50
+ maxInputChars: 1000,
51
+ timeoutMs: 120_000,
52
+ });
53
+ assert.match(output, /^## Digest: conv 2026/);
54
+ assert.match(output, /\| \*\*Source\*\* \| .*conv_2026-05-19/);
55
+ assert.match(output, new RegExp(`\\| \\*\\*Model\\*\\* \\| ${modelId} \\|`));
56
+ assert.doesNotMatch(output, /<\/?think>/i);
57
+ const body = output.replace(/^##[\s\S]*?\n\n/, '').trim();
58
+ assert.ok(body.length > 0, 'expected a non-empty digest body');
59
+ }
60
+ finally {
61
+ await provider.dispose?.();
62
+ }
20
63
  });
21
64
  });
@@ -8,26 +8,26 @@ class DigestModelRegistryError extends Error {
8
8
  const DIGEST_MODEL_REGISTRY = {
9
9
  [DigestModelId.TINY]: {
10
10
  id: DigestModelId.TINY,
11
- ggufUri: 'hf:bartowski/Qwen2.5-0.5B-Instruct-GGUF/Qwen2.5-0.5B-Instruct-Q4_K_M.gguf',
12
- approxSizeBytes: 400_000_000,
11
+ ggufUri: 'hf:lmstudio-community/Qwen3.5-0.8B-GGUF/Qwen3.5-0.8B-Q4_K_M.gguf',
12
+ approxSizeBytes: 528_000_000,
13
13
  license: 'Apache-2.0',
14
- sourceUrl: 'https://huggingface.co/bartowski/Qwen2.5-0.5B-Instruct-GGUF',
14
+ sourceUrl: 'https://huggingface.co/lmstudio-community/Qwen3.5-0.8B-GGUF',
15
15
  enabled: true,
16
16
  },
17
17
  [DigestModelId.BASE]: {
18
18
  id: DigestModelId.BASE,
19
- ggufUri: 'hf:bartowski/Qwen2.5-1.5B-Instruct-GGUF/Qwen2.5-1.5B-Instruct-Q4_K_M.gguf',
20
- approxSizeBytes: 1_000_000_000,
19
+ ggufUri: 'hf:lmstudio-community/Qwen3.5-2B-GGUF/Qwen3.5-2B-Q4_K_M.gguf',
20
+ approxSizeBytes: 1_270_000_000,
21
21
  license: 'Apache-2.0',
22
- sourceUrl: 'https://huggingface.co/bartowski/Qwen2.5-1.5B-Instruct-GGUF',
22
+ sourceUrl: 'https://huggingface.co/lmstudio-community/Qwen3.5-2B-GGUF',
23
23
  enabled: true,
24
24
  },
25
25
  [DigestModelId.LARGE]: {
26
26
  id: DigestModelId.LARGE,
27
- ggufUri: 'hf:bartowski/Qwen2.5-3B-Instruct-GGUF/Qwen2.5-3B-Instruct-Q4_K_M.gguf',
28
- approxSizeBytes: 1_900_000_000,
27
+ ggufUri: 'hf:lmstudio-community/Qwen3.5-4B-GGUF/Qwen3.5-4B-Q4_K_M.gguf',
28
+ approxSizeBytes: 2_710_000_000,
29
29
  license: 'Apache-2.0',
30
- sourceUrl: 'https://huggingface.co/bartowski/Qwen2.5-3B-Instruct-GGUF',
30
+ sourceUrl: 'https://huggingface.co/lmstudio-community/Qwen3.5-4B-GGUF',
31
31
  enabled: true,
32
32
  },
33
33
  };
@@ -1,2 +1,5 @@
1
1
  import type { ConversationDigestProvider } from '../types.js';
2
+ export declare function titleFromSource(sourcePath: string): string;
3
+ export declare function trimConversationExport(content: string, maxInputChars: number): string;
4
+ export declare function sanitizeConversationDigestMarkdown(generatedText: string): string;
2
5
  export declare function createLlamaLocalDigestProvider(modelId: string): Promise<ConversationDigestProvider>;
@@ -11,13 +11,13 @@ class LlamaLocalDigestProviderError extends Error {
11
11
  this.name = 'LlamaLocalDigestProviderError';
12
12
  }
13
13
  }
14
- function titleFromSource(sourcePath) {
14
+ export function titleFromSource(sourcePath) {
15
15
  return path
16
16
  .basename(sourcePath)
17
17
  .replace(/\.[^.]+$/, '')
18
18
  .replace(/[_-]+/g, ' ');
19
19
  }
20
- function trimConversationExport(content, maxInputChars) {
20
+ export function trimConversationExport(content, maxInputChars) {
21
21
  const trimmed = content.trim();
22
22
  if (trimmed.length <= maxInputChars)
23
23
  return trimmed;
@@ -54,7 +54,7 @@ function stripCodeFence(text) {
54
54
  const match = trimmed.match(/^```(?:markdown|md)?\s*\n([\s\S]*?)\n```$/i);
55
55
  return match ? match[1].trim() : trimmed;
56
56
  }
57
- function sanitizeConversationDigestMarkdown(generatedText) {
57
+ export function sanitizeConversationDigestMarkdown(generatedText) {
58
58
  return stripCodeFence(generatedText).trim() + '\n';
59
59
  }
60
60
  export async function createLlamaLocalDigestProvider(modelId) {
@@ -104,6 +104,7 @@ export async function createLlamaLocalDigestProvider(modelId) {
104
104
  const response = await withTimeout(session.prompt(userContent, {
105
105
  maxTokens: LLAMA_MAX_GENERATED_TOKENS,
106
106
  temperature: LLAMA_TEMPERATURE,
107
+ budgets: { thoughtTokens: 0 },
107
108
  }), options.timeoutMs, () => new LlamaLocalDigestProviderError(`Llama provider timed out after ${options.timeoutMs}ms`));
108
109
  const metadata = `## Digest: ${titleFromSource(input.sourcePath)}
109
110
 
@@ -11,8 +11,9 @@ function makeMockServer() {
11
11
  const tools = new Map();
12
12
  const descriptions = new Map();
13
13
  const server = {
14
- tool(name, desc, _schema, handler) {
15
- descriptions.set(name, desc);
14
+ registerTool(name, config, handler) {
15
+ if (config.description)
16
+ descriptions.set(name, config.description);
16
17
  tools.set(name, handler);
17
18
  },
18
19
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanhnd/agent-kit",
3
- "version": "1.0.40",
3
+ "version": "1.0.42",
4
4
  "description": "Agent Kit MCP server for software development agent workflows",
5
5
  "type": "module",
6
6
  "main": "dist/server.js",