@first-to-fly/orchestrator-mcp 0.1.0

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/tools.js ADDED
@@ -0,0 +1,1237 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.tools = void 0;
4
+ const TEXT_OUTPUT_SCHEMA = {
5
+ type: 'object',
6
+ properties: {
7
+ type: { const: 'text' },
8
+ text: { type: 'string' },
9
+ },
10
+ };
11
+ function textResult(text, isError = false) {
12
+ return { content: [{ type: 'text', text }], isError };
13
+ }
14
+ function smartTruncate(text, maxChars) {
15
+ if (text.length <= maxChars)
16
+ return text;
17
+ let cutPoint = text.lastIndexOf('\n', maxChars);
18
+ if (cutPoint < maxChars * 0.5)
19
+ cutPoint = maxChars;
20
+ let truncated = text.substring(0, cutPoint);
21
+ const fences = (truncated.match(/^```/gm) || []).length;
22
+ if (fences % 2 !== 0)
23
+ truncated += '\n```';
24
+ return truncated + `\n\n_(truncated to fit ${maxChars} char budget)_`;
25
+ }
26
+ function applyCharBudget(text, maxChars) {
27
+ if (!maxChars || text.length <= maxChars)
28
+ return text;
29
+ return smartTruncate(text, maxChars);
30
+ }
31
+ const ENTITY_TYPES_ENUM = ['module', 'decision', 'pattern', 'requirement', 'error', 'task', 'observation', 'session'];
32
+ /**
33
+ * Build structured markdown content from base content + type-specific fields.
34
+ * Appends structured sections (## Rationale, ## Fix, etc.) to the base content.
35
+ */
36
+ function buildStructuredContent(args) {
37
+ const sections = [args.content];
38
+ // Decision fields
39
+ if (args.rationale)
40
+ sections.push(`\n## Rationale\n${args.rationale}`);
41
+ if (args.alternatives?.length) {
42
+ sections.push(`\n## Alternatives Considered\n${args.alternatives.map((a) => `- ${a}`).join('\n')}`);
43
+ }
44
+ if (args.tradeoffs)
45
+ sections.push(`\n## Tradeoffs\n${args.tradeoffs}`);
46
+ // Error fields
47
+ if (args.root_cause)
48
+ sections.push(`\n## Root Cause\n${args.root_cause}`);
49
+ if (args.fix)
50
+ sections.push(`\n## Fix\n${args.fix}`);
51
+ if (args.file_path)
52
+ sections.push(`\n## File\n${args.file_path}`);
53
+ if (args.stack_trace)
54
+ sections.push(`\n## Stack Trace\n\`\`\`\n${args.stack_trace}\n\`\`\``);
55
+ // Pattern fields
56
+ if (args.example_files?.length) {
57
+ sections.push(`\n## Example Files\n${args.example_files.map((f) => `- ${f}`).join('\n')}`);
58
+ }
59
+ if (args.anti_pattern)
60
+ sections.push(`\n## Anti-Pattern\n${args.anti_pattern}`);
61
+ // Task fields
62
+ if (args.files_modified?.length) {
63
+ sections.push(`\n## Files Modified\n${args.files_modified.map((f) => `- ${f}`).join('\n')}`);
64
+ }
65
+ if (args.outcome)
66
+ sections.push(`\n## Outcome\n${args.outcome}`);
67
+ return sections.join('\n');
68
+ }
69
+ exports.tools = [
70
+ {
71
+ name: 'recall_context',
72
+ description: 'Recall relevant memory for a task. Returns summaries + last session + matching recipes. Use get_entity for full content.',
73
+ inputSchema: {
74
+ type: 'object',
75
+ properties: {
76
+ context: {
77
+ type: 'string',
78
+ description: 'Task description or context',
79
+ },
80
+ types: {
81
+ type: 'array',
82
+ items: { type: 'string', enum: ENTITY_TYPES_ENUM },
83
+ description: 'Filter by types',
84
+ },
85
+ limit: {
86
+ type: 'number',
87
+ description: 'Max results (default: 10)',
88
+ },
89
+ pinnedEntityIds: {
90
+ type: 'array',
91
+ items: { type: 'string' },
92
+ description: 'Entity IDs to always include regardless of score',
93
+ },
94
+ preview_chars: {
95
+ type: 'number',
96
+ description: 'Characters per entity preview (default: 200, max: 2000)',
97
+ },
98
+ max_chars: {
99
+ type: 'number',
100
+ description: 'Maximum output characters. Truncates output to fit context budget.',
101
+ },
102
+ scope: {
103
+ type: 'string',
104
+ description: 'Filter to entities relevant to a specific module/area (e.g., "payments", "pdf"). Modules and decisions always included.',
105
+ },
106
+ },
107
+ required: ['context'],
108
+ },
109
+ handler: async (client, args) => {
110
+ try {
111
+ const requestedLimit = args.limit || 10;
112
+ // Fetch recall, last session, and matching recipes in parallel
113
+ const [result, lastSession, recipes] = await Promise.all([
114
+ client.recall(args.context, {
115
+ types: args.types,
116
+ limit: requestedLimit,
117
+ mode: 'summary',
118
+ pinnedEntityIds: args.pinnedEntityIds,
119
+ previewChars: args.preview_chars,
120
+ scope: args.scope,
121
+ }),
122
+ client.getLastSession().catch(() => null),
123
+ client.matchRecipes(args.context).catch(() => []),
124
+ ]);
125
+ const lines = [];
126
+ // Prepend last session if available
127
+ if (lastSession) {
128
+ lines.push('# Last Session\n');
129
+ lines.push(lastSession.content);
130
+ lines.push(`\n_Saved: ${lastSession.createdAt}_\n`);
131
+ }
132
+ if (result.entities.length === 0 && !lastSession) {
133
+ return textResult('No relevant memory found for this context.');
134
+ }
135
+ if (result.entities.length > 0) {
136
+ const totalAvailable = result.totalCount || result.entities.length;
137
+ const truncated = totalAvailable > result.entities.length;
138
+ lines.push('# Recalled Memory (summaries)\n');
139
+ lines.push('_Use `get_entity(type, id)` to fetch full content._\n');
140
+ const staleCount = result.entities.filter((e) => e.isStale).length;
141
+ const previewLen = args.preview_chars || 200;
142
+ // --- 6.3: Incremental budget — stop adding entities when budget is reached ---
143
+ let charCount = lines.join('\n').length;
144
+ const includedEntities = [];
145
+ for (const entity of result.entities) {
146
+ const staleFlag = entity.isStale ? ' **[STALE]**' : '';
147
+ const preview = entity.summary || (entity.content?.length > previewLen ? entity.content.substring(0, previewLen - 3) + '...' : entity.content) || '';
148
+ const entityLines = [`- **[${entity.type}] ${entity.name}**${staleFlag} (id: ${entity.id})`, ` ${preview}`];
149
+ if (entity.tags?.length)
150
+ entityLines.push(` Tags: ${entity.tags.join(', ')}`);
151
+ const entityText = entityLines.join('\n');
152
+ if (args.max_chars && charCount + entityText.length + 1 > args.max_chars)
153
+ break;
154
+ lines.push(entityText);
155
+ charCount += entityText.length + 1;
156
+ includedEntities.push(entity);
157
+ }
158
+ if (staleCount > 0) {
159
+ lines.push(`\n> **Warning**: ${staleCount} stale entit${staleCount === 1 ? 'y' : 'ies'} included (scores demoted). Use check_staleness for details.`);
160
+ }
161
+ if (truncated || includedEntities.length < result.entities.length) {
162
+ lines.push(`\n_(showing ${includedEntities.length} of ${totalAvailable})_`);
163
+ }
164
+ }
165
+ // --- 6.2 + 6.7: Relationship names using recalled + peripheral entities ---
166
+ if (result.relationships.length > 0) {
167
+ const nameMap = new Map();
168
+ for (const entity of result.entities) {
169
+ nameMap.set(entity.id, { name: entity.name, type: entity.type });
170
+ }
171
+ if (result.peripheralEntities) {
172
+ for (const pe of result.peripheralEntities) {
173
+ nameMap.set(pe.id, { name: pe.name, type: pe.type });
174
+ }
175
+ }
176
+ lines.push('\n## Relationships');
177
+ for (const rel of result.relationships) {
178
+ const from = nameMap.get(rel.fromEntityId);
179
+ const to = nameMap.get(rel.toEntityId);
180
+ const fromLabel = from ? `${from.name}` : rel.fromEntityId;
181
+ const toLabel = to ? `${to.name}` : rel.toEntityId;
182
+ lines.push(`- ${fromLabel} --${rel.type}--> ${toLabel}`);
183
+ }
184
+ }
185
+ // Include matching recipes
186
+ if (recipes && recipes.length > 0) {
187
+ lines.push('\n## Matching Recipes');
188
+ lines.push('_Use `get_recipe(name)` to get a rendered recipe._\n');
189
+ for (const recipe of recipes.slice(0, 3)) {
190
+ lines.push(`- **${recipe.name}**: ${recipe.description || 'No description'}`);
191
+ }
192
+ }
193
+ return textResult(applyCharBudget(lines.join('\n'), args.max_chars));
194
+ }
195
+ catch (err) {
196
+ return textResult(`Warning: Could not recall context: ${err.message}`);
197
+ }
198
+ },
199
+ },
200
+ {
201
+ name: 'search_memory',
202
+ description: 'Search memory by keyword. Returns summaries; use get_entity for full content.',
203
+ inputSchema: {
204
+ type: 'object',
205
+ properties: {
206
+ query: { type: 'string', description: 'Search keywords' },
207
+ types: {
208
+ type: 'array',
209
+ items: { type: 'string', enum: ENTITY_TYPES_ENUM },
210
+ description: 'Filter by types',
211
+ },
212
+ limit: { type: 'number', description: 'Max results (default: 20)' },
213
+ mode: {
214
+ type: 'string',
215
+ enum: ['full', 'summary'],
216
+ description: '"summary" (default) or "full"',
217
+ },
218
+ preview_chars: {
219
+ type: 'number',
220
+ description: 'Characters per entity preview (default: 200, max: 2000)',
221
+ },
222
+ max_chars: {
223
+ type: 'number',
224
+ description: 'Maximum output characters. Truncates output to fit context budget.',
225
+ },
226
+ },
227
+ required: ['query'],
228
+ },
229
+ handler: async (client, args) => {
230
+ try {
231
+ const requestedLimit = args.limit || 20;
232
+ const results = await client.search(args.query, {
233
+ types: args.types,
234
+ limit: requestedLimit,
235
+ mode: args.mode || 'summary',
236
+ previewChars: args.preview_chars,
237
+ });
238
+ if (!results || (Array.isArray(results) && results.length === 0)) {
239
+ return textResult(`No results found for "${args.query}"`);
240
+ }
241
+ const items = Array.isArray(results) ? results : [];
242
+ const totalCount = results.totalCount || items.length;
243
+ const previewLen = args.preview_chars || 200;
244
+ const lines = items.map((e) => {
245
+ const content = e.summary || (e.content?.length > previewLen ? e.content.substring(0, previewLen - 3) + '...' : e.content) || '';
246
+ return `[${e.type}] **${e.name}** (id: ${e.id}): ${content}`;
247
+ });
248
+ if (totalCount > items.length) {
249
+ lines.push(`\n_(truncated, showing ${items.length} of ${totalCount})_`);
250
+ }
251
+ return textResult(applyCharBudget(lines.join('\n\n'), args.max_chars));
252
+ }
253
+ catch (err) {
254
+ return textResult(`Warning: Search failed: ${err.message}`);
255
+ }
256
+ },
257
+ },
258
+ {
259
+ name: 'list_recent',
260
+ description: 'List recent entities, optionally filtered by type.',
261
+ inputSchema: {
262
+ type: 'object',
263
+ properties: {
264
+ type: {
265
+ type: 'string',
266
+ enum: ENTITY_TYPES_ENUM,
267
+ description: 'Filter by type',
268
+ },
269
+ limit: { type: 'number', description: 'Max results (default: 20)' },
270
+ max_chars: {
271
+ type: 'number',
272
+ description: 'Maximum output characters. Truncates output to fit context budget.',
273
+ },
274
+ },
275
+ },
276
+ handler: async (client, args) => {
277
+ try {
278
+ const requestedLimit = args.limit || 20;
279
+ const result = await client.listEntities({
280
+ type: args.type,
281
+ limit: requestedLimit,
282
+ });
283
+ const items = result?.items || [];
284
+ if (items.length === 0) {
285
+ return textResult('No entities found.');
286
+ }
287
+ const lines = items.map((e) => `[${e.type}] **${e.name}** (${e.status}) - ${e.createdAt}`);
288
+ if (result?.cursor) {
289
+ lines.push(`\n_(truncated, showing ${items.length} — more available, increase limit to see more)_`);
290
+ }
291
+ return textResult(applyCharBudget(lines.join('\n'), args.max_chars));
292
+ }
293
+ catch (err) {
294
+ return textResult(`Warning: Could not list entities: ${err.message}`);
295
+ }
296
+ },
297
+ },
298
+ {
299
+ name: 'get_entity',
300
+ description: 'Fetch full entity content by type and ID. Use after recall/search returns summaries.',
301
+ inputSchema: {
302
+ type: 'object',
303
+ properties: {
304
+ type: {
305
+ type: 'string',
306
+ enum: ENTITY_TYPES_ENUM,
307
+ description: 'Entity type',
308
+ },
309
+ id: { type: 'string', description: 'Entity ID' },
310
+ },
311
+ required: ['type', 'id'],
312
+ },
313
+ handler: async (client, args) => {
314
+ try {
315
+ const entity = await client.getEntity(args.type, args.id);
316
+ const MAX_CONTENT_LENGTH = 5000;
317
+ const content = entity.content || '';
318
+ const contentTruncated = content.length > MAX_CONTENT_LENGTH;
319
+ const displayContent = contentTruncated
320
+ ? content.substring(0, MAX_CONTENT_LENGTH)
321
+ : content;
322
+ const lines = [
323
+ `## [${entity.type}] ${entity.name}`,
324
+ '',
325
+ displayContent,
326
+ ];
327
+ if (contentTruncated) {
328
+ lines.push(`\n_(content truncated at ${MAX_CONTENT_LENGTH} chars, full length: ${content.length})_`);
329
+ }
330
+ lines.push('', `**Status**: ${entity.status}`, `**Tags**: ${entity.tags?.join(', ') || 'none'}`, `**Source**: ${entity.source || 'unknown'}`, `**Created**: ${entity.createdAt}`, `**Updated**: ${entity.updatedAt}`);
331
+ if (entity.version) {
332
+ lines.push(`**Version**: ${entity.version}`);
333
+ }
334
+ if (entity.evidenceRefs?.length) {
335
+ lines.push(`**Evidence**: ${entity.evidenceRefs.join(', ')}`);
336
+ }
337
+ return textResult(lines.join('\n'));
338
+ }
339
+ catch (err) {
340
+ return textResult(`Warning: Could not fetch entity: ${err.message}`);
341
+ }
342
+ },
343
+ },
344
+ {
345
+ name: 'store_entity',
346
+ description: 'Store a knowledge entity. Deduplicates by type+name. Supports type-specific structured fields.',
347
+ inputSchema: {
348
+ type: 'object',
349
+ properties: {
350
+ name: { type: 'string', description: 'Short name' },
351
+ type: {
352
+ type: 'string',
353
+ enum: ENTITY_TYPES_ENUM,
354
+ description: 'Entity type',
355
+ },
356
+ content: { type: 'string', description: 'Detailed content' },
357
+ tags: {
358
+ type: 'array',
359
+ items: { type: 'string' },
360
+ description: 'Tags',
361
+ },
362
+ evidenceRefs: {
363
+ type: 'array',
364
+ items: { type: 'string' },
365
+ description: 'Evidence references (URLs, file paths, commit SHAs)',
366
+ },
367
+ // Decision-specific fields
368
+ alternatives: {
369
+ type: 'array',
370
+ items: { type: 'string' },
371
+ description: 'Alternatives considered (for decisions)',
372
+ },
373
+ tradeoffs: { type: 'string', description: 'Tradeoffs analysis (for decisions)' },
374
+ rationale: { type: 'string', description: 'Decision rationale (for decisions)' },
375
+ // Error-specific fields
376
+ file_path: { type: 'string', description: 'File where error occurred (for errors)' },
377
+ root_cause: { type: 'string', description: 'Root cause analysis (for errors)' },
378
+ fix: { type: 'string', description: 'How the error was fixed (for errors)' },
379
+ stack_trace: { type: 'string', description: 'Stack trace (for errors)' },
380
+ // Pattern-specific fields
381
+ example_files: {
382
+ type: 'array',
383
+ items: { type: 'string' },
384
+ description: 'Example files demonstrating the pattern (for patterns)',
385
+ },
386
+ anti_pattern: { type: 'string', description: 'What NOT to do (for patterns)' },
387
+ // Task-specific fields
388
+ files_modified: {
389
+ type: 'array',
390
+ items: { type: 'string' },
391
+ description: 'Files modified (for tasks)',
392
+ },
393
+ outcome: { type: 'string', description: 'Task outcome (for tasks)' },
394
+ },
395
+ required: ['name', 'type', 'content'],
396
+ },
397
+ handler: async (client, args) => {
398
+ const entity = await client.createEntity({
399
+ name: args.name,
400
+ type: args.type,
401
+ content: buildStructuredContent(args),
402
+ tags: args.tags,
403
+ source: 'claude-code',
404
+ evidenceRefs: args.evidenceRefs,
405
+ });
406
+ const result = entity;
407
+ if (result.deduplicated) {
408
+ return textResult(`Updated existing entity: [${args.type}] ${args.name} (id: ${result.id}) — deduplicated`);
409
+ }
410
+ return textResult(`Stored entity: [${args.type}] ${args.name} (id: ${result.id})`);
411
+ },
412
+ },
413
+ {
414
+ name: 'store_entities',
415
+ description: 'Bulk store entities (max 50). Deduplicates by type+name. Supports type-specific structured fields.',
416
+ inputSchema: {
417
+ type: 'object',
418
+ properties: {
419
+ entities: {
420
+ type: 'array',
421
+ items: {
422
+ type: 'object',
423
+ properties: {
424
+ name: { type: 'string', description: 'Short name' },
425
+ type: {
426
+ type: 'string',
427
+ enum: ENTITY_TYPES_ENUM,
428
+ description: 'Entity type',
429
+ },
430
+ content: { type: 'string', description: 'Detailed content' },
431
+ tags: { type: 'array', items: { type: 'string' }, description: 'Tags (optional)' },
432
+ evidenceRefs: { type: 'array', items: { type: 'string' }, description: 'Evidence references (optional)' },
433
+ alternatives: { type: 'array', items: { type: 'string' }, description: 'Alternatives (decisions)' },
434
+ tradeoffs: { type: 'string', description: 'Tradeoffs (decisions)' },
435
+ rationale: { type: 'string', description: 'Rationale (decisions)' },
436
+ file_path: { type: 'string', description: 'File path (errors)' },
437
+ root_cause: { type: 'string', description: 'Root cause (errors)' },
438
+ fix: { type: 'string', description: 'Fix description (errors)' },
439
+ stack_trace: { type: 'string', description: 'Stack trace (errors)' },
440
+ example_files: { type: 'array', items: { type: 'string' }, description: 'Example files (patterns)' },
441
+ anti_pattern: { type: 'string', description: 'Anti-pattern (patterns)' },
442
+ files_modified: { type: 'array', items: { type: 'string' }, description: 'Files modified (tasks)' },
443
+ outcome: { type: 'string', description: 'Outcome (tasks)' },
444
+ },
445
+ required: ['name', 'type', 'content'],
446
+ },
447
+ description: 'Array of entities to store (max 50)',
448
+ },
449
+ },
450
+ required: ['entities'],
451
+ },
452
+ handler: async (client, args) => {
453
+ const result = await client.bulkCreateEntities(args.entities.map((e) => ({
454
+ name: e.name,
455
+ type: e.type,
456
+ content: buildStructuredContent(e),
457
+ tags: e.tags,
458
+ evidenceRefs: e.evidenceRefs,
459
+ source: 'claude-code',
460
+ })));
461
+ const items = result?.items || [];
462
+ const created = items.filter((i) => !i.deduplicated).length;
463
+ const deduped = items.filter((i) => i.deduplicated).length;
464
+ const lines = [`Stored ${items.length} entities (${created} new, ${deduped} deduplicated)`];
465
+ for (const item of items) {
466
+ const flag = item.deduplicated ? ' (deduplicated)' : '';
467
+ lines.push(` - [${item.type}] ${item.name}${flag}`);
468
+ }
469
+ return textResult(lines.join('\n'));
470
+ },
471
+ },
472
+ {
473
+ name: 'update_entity',
474
+ description: 'Update an existing entity (name, content, tags, or status). Supports optimistic concurrency via expected_version.',
475
+ inputSchema: {
476
+ type: 'object',
477
+ properties: {
478
+ type: { type: 'string', description: 'Entity type' },
479
+ id: { type: 'string', description: 'Entity ID' },
480
+ name: { type: 'string', description: 'New name' },
481
+ content: { type: 'string', description: 'New content' },
482
+ tags: { type: 'array', items: { type: 'string' }, description: 'New tags' },
483
+ status: {
484
+ type: 'string',
485
+ enum: ['active', 'superseded', 'resolved'],
486
+ description: 'New status',
487
+ },
488
+ expected_version: {
489
+ type: 'number',
490
+ description: 'Expected version for optimistic concurrency. If provided and does not match, returns 409 Conflict.',
491
+ },
492
+ },
493
+ required: ['type', 'id'],
494
+ },
495
+ handler: async (client, args) => {
496
+ const updates = {};
497
+ if (args.name)
498
+ updates.name = args.name;
499
+ if (args.content)
500
+ updates.content = args.content;
501
+ if (args.tags)
502
+ updates.tags = args.tags;
503
+ if (args.status)
504
+ updates.status = args.status;
505
+ if (args.expected_version !== undefined)
506
+ updates.expectedVersion = args.expected_version;
507
+ const result = await client.updateEntity(args.type, args.id, updates);
508
+ const version = result?.version;
509
+ return textResult(`Updated entity ${args.id}${version ? ` (version: ${version})` : ''}`);
510
+ },
511
+ },
512
+ {
513
+ name: 'delete_entity',
514
+ description: 'Delete an entity by type and ID. Defaults to dry_run=true (preview without deleting). Set dry_run=false to request deletion (requires human approval). Pass approvalId after approval is granted.',
515
+ inputSchema: {
516
+ type: 'object',
517
+ properties: {
518
+ type: { type: 'string', description: 'Entity type' },
519
+ id: { type: 'string', description: 'Entity ID' },
520
+ dry_run: {
521
+ type: 'boolean',
522
+ description: 'Preview without deleting (default: true)',
523
+ },
524
+ approvalId: {
525
+ type: 'string',
526
+ description: 'Approval ID from a previously approved request. Required to complete deletion.',
527
+ },
528
+ },
529
+ required: ['type', 'id'],
530
+ },
531
+ handler: async (client, args) => {
532
+ const dryRun = args.dry_run !== false && !args.approvalId; // default true unless approvalId provided
533
+ if (dryRun) {
534
+ const result = await client.deleteEntity(args.type, args.id, { dryRun: true });
535
+ if (result?.entity) {
536
+ const e = result.entity;
537
+ return textResult(`**Dry run** — would delete:\n` +
538
+ `[${e.type}] ${e.name} (id: ${e.id})\n` +
539
+ `Content: ${e.content?.substring(0, 200) || ''}${e.content?.length > 200 ? '...' : ''}\n` +
540
+ `Tags: ${e.tags?.join(', ') || 'none'}\n\n` +
541
+ `Set dry_run=false to request deletion (requires human approval).`);
542
+ }
543
+ return textResult('Entity not found.');
544
+ }
545
+ const result = await client.deleteEntity(args.type, args.id, { approvalId: args.approvalId });
546
+ // Check if approval was required
547
+ if (result?.requiresApproval) {
548
+ return textResult(`**Approval required** to delete [${result.entity?.type}] ${result.entity?.name}\n` +
549
+ `Approval ID: ${result.approvalId}\n\n` +
550
+ `A human must approve this in the Orchestrator UI.\n` +
551
+ `Use check_approval(id: "${result.approvalId}") to poll for a decision.\n` +
552
+ `Once approved, call delete_entity(type: "${args.type}", id: "${args.id}", approvalId: "${result.approvalId}") to complete.`);
553
+ }
554
+ return textResult(`Deleted entity ${args.id}`);
555
+ },
556
+ },
557
+ {
558
+ name: 'store_relationship',
559
+ description: 'Link two entities with a typed relationship.',
560
+ inputSchema: {
561
+ type: 'object',
562
+ properties: {
563
+ fromEntityId: { type: 'string', description: 'Source entity ID' },
564
+ fromEntityType: { type: 'string', description: 'Source entity type' },
565
+ toEntityId: { type: 'string', description: 'Target entity ID' },
566
+ toEntityType: { type: 'string', description: 'Target entity type' },
567
+ type: {
568
+ type: 'string',
569
+ enum: ['depends_on', 'relates_to', 'supersedes', 'caused_by', 'part_of', 'implements'],
570
+ description: 'Relationship type',
571
+ },
572
+ },
573
+ required: ['fromEntityId', 'fromEntityType', 'toEntityId', 'toEntityType', 'type'],
574
+ },
575
+ handler: async (client, args) => {
576
+ await client.createRelationship(args);
577
+ return textResult(`Created relationship: ${args.fromEntityId} --${args.type}--> ${args.toEntityId}`);
578
+ },
579
+ },
580
+ {
581
+ name: 'add_observation',
582
+ description: 'Quick shortcut to store an observation entity.',
583
+ inputSchema: {
584
+ type: 'object',
585
+ properties: {
586
+ name: { type: 'string', description: 'Short title' },
587
+ content: { type: 'string', description: 'Observation content' },
588
+ tags: { type: 'array', items: { type: 'string' }, description: 'Tags' },
589
+ },
590
+ required: ['name', 'content'],
591
+ },
592
+ handler: async (client, args) => {
593
+ const entity = await client.createEntity({
594
+ name: args.name,
595
+ type: 'observation',
596
+ content: args.content,
597
+ tags: args.tags,
598
+ source: 'claude-code',
599
+ });
600
+ const result = entity;
601
+ if (result.deduplicated) {
602
+ return textResult(`Updated existing observation: ${args.name} (id: ${result.id}) — deduplicated`);
603
+ }
604
+ return textResult(`Added observation: ${args.name} (id: ${result.id})`);
605
+ },
606
+ },
607
+ {
608
+ name: 'save_session_summary',
609
+ description: 'Save session handoff summary. Next session auto-receives this via recall_context.',
610
+ inputSchema: {
611
+ type: 'object',
612
+ properties: {
613
+ summary: {
614
+ type: 'string',
615
+ description: 'What was accomplished and current state',
616
+ },
617
+ tasksCompleted: {
618
+ type: 'array',
619
+ items: { type: 'string' },
620
+ description: 'Completed tasks',
621
+ },
622
+ tasksInProgress: {
623
+ type: 'array',
624
+ items: { type: 'string' },
625
+ description: 'In-progress tasks',
626
+ },
627
+ blockers: {
628
+ type: 'array',
629
+ items: { type: 'string' },
630
+ description: 'Blockers',
631
+ },
632
+ filesModified: {
633
+ type: 'array',
634
+ items: { type: 'string' },
635
+ description: 'Modified files',
636
+ },
637
+ decisionsMade: {
638
+ type: 'array',
639
+ items: { type: 'string' },
640
+ description: 'Decisions made',
641
+ },
642
+ nextSteps: {
643
+ type: 'array',
644
+ items: { type: 'string' },
645
+ description: 'Next steps',
646
+ },
647
+ },
648
+ required: ['summary'],
649
+ },
650
+ handler: async (client, args) => {
651
+ const result = await client.saveSession(args);
652
+ return textResult(`Session summary saved (id: ${result.id}). Next session will receive this via recall_context.`);
653
+ },
654
+ },
655
+ {
656
+ name: 'get_last_session',
657
+ description: 'Get the last session summary for cross-session continuity.',
658
+ inputSchema: {
659
+ type: 'object',
660
+ properties: {},
661
+ },
662
+ handler: async (client) => {
663
+ try {
664
+ const session = await client.getLastSession();
665
+ if (!session) {
666
+ return textResult('No previous session found.');
667
+ }
668
+ const lines = [
669
+ `# Last Session: ${session.name}`,
670
+ `_Saved: ${session.createdAt}_\n`,
671
+ session.content,
672
+ ];
673
+ return textResult(lines.join('\n'));
674
+ }
675
+ catch (err) {
676
+ return textResult(`Warning: Could not retrieve last session: ${err.message}`);
677
+ }
678
+ },
679
+ },
680
+ {
681
+ name: 'search_sessions',
682
+ description: 'Search across past session summaries by keyword. Find what was done in previous sessions.',
683
+ inputSchema: {
684
+ type: 'object',
685
+ properties: {
686
+ query: { type: 'string', description: 'Search keywords' },
687
+ limit: { type: 'number', description: 'Max results (default: 5)' },
688
+ preview_chars: {
689
+ type: 'number',
690
+ description: 'Characters per session preview (default: 300)',
691
+ },
692
+ max_chars: {
693
+ type: 'number',
694
+ description: 'Maximum total output characters. Truncates output to fit context budget.',
695
+ },
696
+ },
697
+ required: ['query'],
698
+ },
699
+ handler: async (client, args) => {
700
+ try {
701
+ const results = await client.searchSessions(args.query, {
702
+ limit: args.limit,
703
+ previewChars: args.preview_chars,
704
+ });
705
+ if (!results || results.length === 0) {
706
+ return textResult(`No sessions found matching "${args.query}"`);
707
+ }
708
+ const lines = [`# Session Search: "${args.query}" (${results.length} matches)\n`];
709
+ for (const session of results) {
710
+ lines.push(`## ${session.name} (match: ${session.matchScore.toFixed(1)})`);
711
+ lines.push(`_Created: ${session.createdAt}_\n`);
712
+ lines.push(session.preview);
713
+ lines.push('');
714
+ }
715
+ return textResult(applyCharBudget(lines.join('\n'), args.max_chars));
716
+ }
717
+ catch (err) {
718
+ return textResult(`Warning: Session search failed: ${err.message}`);
719
+ }
720
+ },
721
+ },
722
+ {
723
+ name: 'check_error',
724
+ description: 'Check if a similar error was seen/resolved before. Call before debugging from scratch.',
725
+ inputSchema: {
726
+ type: 'object',
727
+ properties: {
728
+ errorMessage: {
729
+ type: 'string',
730
+ description: 'Error message to look up',
731
+ },
732
+ stackTrace: {
733
+ type: 'string',
734
+ description: 'Stack trace (improves matching)',
735
+ },
736
+ module: {
737
+ type: 'string',
738
+ description: 'Module where error occurred',
739
+ },
740
+ limit: {
741
+ type: 'number',
742
+ description: 'Max results (default: 5)',
743
+ },
744
+ },
745
+ required: ['errorMessage'],
746
+ },
747
+ handler: async (client, args) => {
748
+ try {
749
+ const results = await client.checkError({
750
+ errorMessage: args.errorMessage,
751
+ stackTrace: args.stackTrace,
752
+ module: args.module,
753
+ limit: args.limit,
754
+ });
755
+ if (!results || results.length === 0) {
756
+ return textResult('No matching past errors found. This appears to be a new error.');
757
+ }
758
+ const lines = ['# Matching Past Errors\n'];
759
+ for (const error of results) {
760
+ const statusLabel = error.status === 'resolved' ? 'RESOLVED' : 'ACTIVE';
761
+ lines.push(`## [${statusLabel}] ${error.name} (match: ${error.matchScore})`);
762
+ // Parse and display structured sections prominently
763
+ const content = error.content || '';
764
+ const fixMatch = content.match(/## Fix\n([\s\S]*?)(?=\n## |\n*$)/);
765
+ const rootCauseMatch = content.match(/## Root Cause\n([\s\S]*?)(?=\n## |\n*$)/);
766
+ const fileMatch = content.match(/## File\n([\s\S]*?)(?=\n## |\n*$)/);
767
+ if (fixMatch) {
768
+ lines.push(`**Fix**: ${fixMatch[1].trim()}`);
769
+ }
770
+ if (rootCauseMatch) {
771
+ lines.push(`**Root Cause**: ${rootCauseMatch[1].trim()}`);
772
+ }
773
+ if (fileMatch) {
774
+ lines.push(`**File**: ${fileMatch[1].trim()}`);
775
+ }
776
+ // Show remaining content (without already-extracted sections)
777
+ let remainingContent = content;
778
+ if (fixMatch)
779
+ remainingContent = remainingContent.replace(fixMatch[0], '');
780
+ if (rootCauseMatch)
781
+ remainingContent = remainingContent.replace(rootCauseMatch[0], '');
782
+ if (fileMatch)
783
+ remainingContent = remainingContent.replace(fileMatch[0], '');
784
+ remainingContent = remainingContent.trim();
785
+ if (remainingContent)
786
+ lines.push(remainingContent);
787
+ if (error.tags?.length)
788
+ lines.push(`Tags: ${error.tags.join(', ')}`);
789
+ if (error.status === 'resolved') {
790
+ lines.push(`_Use resolve_error to update the fix if needed._`);
791
+ }
792
+ lines.push('');
793
+ }
794
+ return textResult(lines.join('\n'));
795
+ }
796
+ catch (err) {
797
+ return textResult(`Warning: Error check failed: ${err.message}`);
798
+ }
799
+ },
800
+ },
801
+ {
802
+ name: 'resolve_error',
803
+ description: 'Mark an error entity as resolved with a fix description. Completes the check→fix→resolve lifecycle.',
804
+ inputSchema: {
805
+ type: 'object',
806
+ properties: {
807
+ id: { type: 'string', description: 'Error entity ID' },
808
+ fix: { type: 'string', description: 'How the error was fixed' },
809
+ root_cause: { type: 'string', description: 'Root cause of the error' },
810
+ },
811
+ required: ['id', 'fix'],
812
+ },
813
+ handler: async (client, args) => {
814
+ try {
815
+ // Fetch current entity
816
+ const entity = await client.getEntity('error', args.id);
817
+ // Build updated content with fix/root_cause sections
818
+ let content = entity.content || '';
819
+ // Replace or append ## Fix section
820
+ const fixSection = `\n\n## Fix\n${args.fix}`;
821
+ if (content.includes('## Fix\n')) {
822
+ content = content.replace(/## Fix\n[\s\S]*?(?=\n## |\n*$)/, `## Fix\n${args.fix}`);
823
+ }
824
+ else {
825
+ content += fixSection;
826
+ }
827
+ // Replace or append ## Root Cause section
828
+ if (args.root_cause) {
829
+ const rcSection = `\n\n## Root Cause\n${args.root_cause}`;
830
+ if (content.includes('## Root Cause\n')) {
831
+ content = content.replace(/## Root Cause\n[\s\S]*?(?=\n## |\n*$)/, `## Root Cause\n${args.root_cause}`);
832
+ }
833
+ else {
834
+ content += rcSection;
835
+ }
836
+ }
837
+ // Update entity: set content and status to resolved
838
+ await client.updateEntity('error', args.id, {
839
+ content: content.trim(),
840
+ status: 'resolved',
841
+ });
842
+ return textResult(`Error ${args.id} resolved with fix. Status set to resolved.`);
843
+ }
844
+ catch (err) {
845
+ return textResult(`Warning: Could not resolve error: ${err.message}`);
846
+ }
847
+ },
848
+ },
849
+ {
850
+ name: 'prune_stale',
851
+ description: 'Remove stale entities that have never been accessed and are older than a threshold. Dry run by default — set confirm=true to delete. Protected types (session, decision, pattern) are never pruned.',
852
+ inputSchema: {
853
+ type: 'object',
854
+ properties: {
855
+ olderThanDays: {
856
+ type: 'number',
857
+ description: 'Age threshold in days (default: 90, min: 7, max: 365)',
858
+ },
859
+ confirm: {
860
+ type: 'boolean',
861
+ description: 'Set to true to actually delete (default: false = dry run)',
862
+ },
863
+ },
864
+ },
865
+ handler: async (client, args) => {
866
+ const result = await client.pruneStale({
867
+ olderThanDays: args.olderThanDays,
868
+ confirm: args.confirm,
869
+ });
870
+ if (result.dryRun) {
871
+ if (result.count === 0) {
872
+ return textResult('No stale entities found.');
873
+ }
874
+ const lines = [`Found ${result.count} stale entities (dry run — set confirm=true to delete):\n`];
875
+ for (const item of result.candidates) {
876
+ const connInfo = item.connections > 0 ? `, ${item.connections} connection(s)` : '';
877
+ const multiplierInfo = item.typeMultiplier !== 1 ? ` (×${item.typeMultiplier})` : '';
878
+ lines.push(`- [${item.type}] ${item.name}` +
879
+ `\n Age: ${item.ageDays}d, threshold: ${item.effectiveThreshold}d${multiplierInfo}${connInfo}` +
880
+ `\n Reason: ${item.reason}`);
881
+ }
882
+ return textResult(lines.join('\n'));
883
+ }
884
+ return textResult(`Pruned ${result.deletedCount} stale entities.`);
885
+ },
886
+ },
887
+ {
888
+ name: 'track_files',
889
+ description: 'Record file modifications for queryability across sessions. Stores as a tagged observation entity.',
890
+ inputSchema: {
891
+ type: 'object',
892
+ properties: {
893
+ files: {
894
+ type: 'array',
895
+ items: {
896
+ type: 'object',
897
+ properties: {
898
+ path: { type: 'string', description: 'File path' },
899
+ action: {
900
+ type: 'string',
901
+ enum: ['created', 'modified', 'deleted', 'renamed'],
902
+ description: 'What was done to the file',
903
+ },
904
+ reason: { type: 'string', description: 'Why the file was changed' },
905
+ },
906
+ required: ['path', 'action'],
907
+ },
908
+ description: 'Files that were changed',
909
+ },
910
+ context: { type: 'string', description: 'Brief context for the changes (e.g., task name or feature)' },
911
+ relatedEntityId: { type: 'string', description: 'Link to a related task/error entity via relationship' },
912
+ relatedEntityType: { type: 'string', description: 'Type of the related entity' },
913
+ },
914
+ required: ['files', 'context'],
915
+ },
916
+ handler: async (client, args) => {
917
+ // Build structured markdown content
918
+ const lines = [`File changes for: ${args.context}\n`];
919
+ for (const file of args.files) {
920
+ const reason = file.reason ? ` — ${file.reason}` : '';
921
+ lines.push(`- **${file.action}**: \`${file.path}\`${reason}`);
922
+ }
923
+ // Build tags: file-tracking + file:{filename} + module scope for each file
924
+ const tags = ['file-tracking'];
925
+ for (const file of args.files) {
926
+ const parts = file.path.split('/');
927
+ const filename = parts.pop() || file.path;
928
+ tags.push(`file:${filename}`);
929
+ const moduleIdx = parts.indexOf('modules');
930
+ if (moduleIdx >= 0 && parts[moduleIdx + 1]) {
931
+ tags.push(parts[moduleIdx + 1]);
932
+ }
933
+ }
934
+ const entity = await client.createEntity({
935
+ name: `File changes: ${args.context}`,
936
+ type: 'observation',
937
+ content: lines.join('\n'),
938
+ tags,
939
+ source: 'claude-code',
940
+ });
941
+ // Create relationship to related entity if specified
942
+ if (args.relatedEntityId && args.relatedEntityType) {
943
+ await client.createRelationship({
944
+ fromEntityId: entity.id,
945
+ fromEntityType: 'observation',
946
+ toEntityId: args.relatedEntityId,
947
+ toEntityType: args.relatedEntityType,
948
+ type: 'relates_to',
949
+ }).catch(() => { }); // fire-and-forget
950
+ }
951
+ const action = entity.deduplicated ? 'Updated' : 'Tracked';
952
+ return textResult(`${action} ${args.files.length} file change(s) for "${args.context}" (id: ${entity.id})`);
953
+ },
954
+ },
955
+ {
956
+ name: 'check_staleness',
957
+ description: 'List all stale entities that are past their type\'s staleness window. Helps identify outdated knowledge that may need updating or removal.',
958
+ inputSchema: {
959
+ type: 'object',
960
+ properties: {},
961
+ },
962
+ handler: async (client) => {
963
+ try {
964
+ const staleEntities = await client.checkStaleness();
965
+ if (!staleEntities || staleEntities.length === 0) {
966
+ return textResult('No stale entities found. All knowledge is within freshness windows.');
967
+ }
968
+ const lines = [`# Stale Entities (${staleEntities.length} found)\n`];
969
+ lines.push('_Entities past their type\'s staleness window. Consider updating or removing._\n');
970
+ for (const entity of staleEntities) {
971
+ const overdue = entity.daysSinceActive - entity.staleAfterDays;
972
+ lines.push(`- **[${entity.type}] ${entity.name}** (id: ${entity.id})` +
973
+ `\n ${entity.daysSinceActive}d since active (threshold: ${entity.staleAfterDays}d, overdue by ${overdue}d)` +
974
+ `\n Status: ${entity.status} | Last active: ${entity.lastActive}`);
975
+ }
976
+ return textResult(lines.join('\n'));
977
+ }
978
+ catch (err) {
979
+ return textResult(`Warning: Could not check staleness: ${err.message}`);
980
+ }
981
+ },
982
+ },
983
+ // --- Recipes ---
984
+ {
985
+ name: 'get_recipe',
986
+ description: 'Get a rendered recipe by name. Recipes are reusable prompt templates with entity references.',
987
+ inputSchema: {
988
+ type: 'object',
989
+ properties: {
990
+ name: { type: 'string', description: 'Recipe name' },
991
+ variables: {
992
+ type: 'object',
993
+ description: 'Variables to substitute (key-value pairs for {{$VARIABLE}} placeholders)',
994
+ },
995
+ },
996
+ required: ['name'],
997
+ },
998
+ handler: async (client, args) => {
999
+ try {
1000
+ const recipe = await client.getRecipeByName(args.name);
1001
+ if (!recipe) {
1002
+ return textResult(`Recipe "${args.name}" not found. Use list_recipes to see available recipes.`);
1003
+ }
1004
+ const { rendered } = await client.renderRecipe(recipe.id, args.variables);
1005
+ return textResult(`# Recipe: ${recipe.name}\n\n${rendered}`);
1006
+ }
1007
+ catch (err) {
1008
+ return textResult(`Warning: Could not get recipe: ${err.message}`);
1009
+ }
1010
+ },
1011
+ },
1012
+ {
1013
+ name: 'list_recipes',
1014
+ description: 'List available recipes (reusable prompt templates).',
1015
+ inputSchema: {
1016
+ type: 'object',
1017
+ properties: {},
1018
+ },
1019
+ handler: async (client) => {
1020
+ try {
1021
+ const recipes = await client.listRecipes();
1022
+ if (!recipes || recipes.length === 0) {
1023
+ return textResult('No recipes found.');
1024
+ }
1025
+ const lines = ['# Available Recipes\n'];
1026
+ for (const recipe of recipes) {
1027
+ lines.push(`- **${recipe.name}**: ${recipe.description || 'No description'}`);
1028
+ if (recipe.triggers?.length)
1029
+ lines.push(` Triggers: ${recipe.triggers.join(', ')}`);
1030
+ if (recipe.variables?.length)
1031
+ lines.push(` Variables: ${recipe.variables.map((v) => `{{$${v}}}`).join(', ')}`);
1032
+ }
1033
+ return textResult(lines.join('\n'));
1034
+ }
1035
+ catch (err) {
1036
+ return textResult(`Warning: Could not list recipes: ${err.message}`);
1037
+ }
1038
+ },
1039
+ },
1040
+ // --- Work Queue ---
1041
+ {
1042
+ name: 'claim_next_task',
1043
+ description: 'Claim the highest-priority unclaimed work item from the queue. Returns the task with its context and a claimToken for completing/failing.',
1044
+ inputSchema: {
1045
+ type: 'object',
1046
+ properties: {
1047
+ agentId: {
1048
+ type: 'string',
1049
+ description: 'Identifier for this agent (default: session ID)',
1050
+ },
1051
+ },
1052
+ },
1053
+ handler: async (client, args) => {
1054
+ try {
1055
+ const task = await client.claimNextTask(args.agentId || 'claude-code');
1056
+ if (!task) {
1057
+ return textResult('No work items available in the queue.');
1058
+ }
1059
+ const lines = [
1060
+ `# Claimed Task: ${task.title}`,
1061
+ `**ID**: ${task.id}`,
1062
+ `**Priority**: ${task.priority}/5`,
1063
+ `**Claim Token**: ${task.claimToken}`,
1064
+ `**Lease Expires**: ${task.leaseExpiresAt}`,
1065
+ `**Retry**: ${task.retryCount}/${task.maxRetries}`,
1066
+ '',
1067
+ task.description,
1068
+ ];
1069
+ if (task.entityRefs?.length) {
1070
+ lines.push('', '**Context entities**: ' + task.entityRefs.join(', '));
1071
+ }
1072
+ return textResult(lines.join('\n'));
1073
+ }
1074
+ catch (err) {
1075
+ return textResult(`Error: Could not claim task: ${err.message}`, true);
1076
+ }
1077
+ },
1078
+ },
1079
+ {
1080
+ name: 'complete_task',
1081
+ description: 'Mark a claimed work item as completed with a result summary and optional validation artifacts.',
1082
+ inputSchema: {
1083
+ type: 'object',
1084
+ properties: {
1085
+ id: { type: 'string', description: 'Work item ID' },
1086
+ resultSummary: { type: 'string', description: 'Summary of what was accomplished' },
1087
+ claimToken: { type: 'string', description: 'Claim token received when claiming the task' },
1088
+ artifacts: {
1089
+ type: 'array',
1090
+ items: {
1091
+ type: 'object',
1092
+ properties: {
1093
+ type: {
1094
+ type: 'string',
1095
+ enum: ['test_result', 'diff', 'log', 'url'],
1096
+ description: 'Artifact type',
1097
+ },
1098
+ value: { type: 'string', description: 'Artifact content or reference' },
1099
+ },
1100
+ required: ['type', 'value'],
1101
+ },
1102
+ description: 'Validation artifacts providing evidence of correctness',
1103
+ },
1104
+ },
1105
+ required: ['id', 'resultSummary'],
1106
+ },
1107
+ handler: async (client, args) => {
1108
+ await client.completeTask(args.id, args.resultSummary, args.claimToken, args.artifacts);
1109
+ const artifactNote = args.artifacts?.length ? ` with ${args.artifacts.length} artifact(s)` : '';
1110
+ return textResult(`Task ${args.id} marked as completed${artifactNote}.`);
1111
+ },
1112
+ },
1113
+ {
1114
+ name: 'fail_task',
1115
+ description: 'Mark a claimed work item as failed with error details. If under maxRetries, it will be requeued automatically.',
1116
+ inputSchema: {
1117
+ type: 'object',
1118
+ properties: {
1119
+ id: { type: 'string', description: 'Work item ID' },
1120
+ errorDetails: { type: 'string', description: 'Description of what went wrong' },
1121
+ claimToken: { type: 'string', description: 'Claim token received when claiming the task' },
1122
+ },
1123
+ required: ['id', 'errorDetails'],
1124
+ },
1125
+ handler: async (client, args) => {
1126
+ const result = await client.failTask(args.id, args.errorDetails, args.claimToken);
1127
+ if (result?.status === 'queued') {
1128
+ return textResult(`Task ${args.id} requeued for retry (attempt ${result.retryCount}/${result.maxRetries}).`);
1129
+ }
1130
+ return textResult(`Task ${args.id} marked as failed.`);
1131
+ },
1132
+ },
1133
+ // --- Approvals ---
1134
+ {
1135
+ name: 'request_approval',
1136
+ description: 'Request human approval for a destructive or risky action. Returns an approval ID to poll with check_approval.',
1137
+ inputSchema: {
1138
+ type: 'object',
1139
+ properties: {
1140
+ scope: {
1141
+ type: 'string',
1142
+ enum: ['delete_entity', 'prune_stale', 'bulk_delete'],
1143
+ description: 'Type of action requiring approval',
1144
+ },
1145
+ riskLevel: {
1146
+ type: 'string',
1147
+ enum: ['low', 'medium', 'high'],
1148
+ description: 'Risk level (default: medium)',
1149
+ },
1150
+ context: {
1151
+ type: 'object',
1152
+ description: 'Context about the action (entityId, entityType, entityName, details)',
1153
+ },
1154
+ },
1155
+ required: ['scope'],
1156
+ },
1157
+ handler: async (client, args) => {
1158
+ try {
1159
+ const approval = await client.requestApproval({
1160
+ scope: args.scope,
1161
+ riskLevel: args.riskLevel,
1162
+ context: args.context,
1163
+ });
1164
+ return textResult(`Approval requested (id: ${approval.id}).\n` +
1165
+ `Status: ${approval.status}\n` +
1166
+ `Expires: ${approval.expiresAt}\n\n` +
1167
+ `Use check_approval(id: "${approval.id}") to poll for a decision.`);
1168
+ }
1169
+ catch (err) {
1170
+ return textResult(`Error: Could not request approval: ${err.message}`, true);
1171
+ }
1172
+ },
1173
+ },
1174
+ {
1175
+ name: 'check_approval',
1176
+ description: 'Check the status of a pending approval request.',
1177
+ inputSchema: {
1178
+ type: 'object',
1179
+ properties: {
1180
+ id: { type: 'string', description: 'Approval ID' },
1181
+ },
1182
+ required: ['id'],
1183
+ },
1184
+ handler: async (client, args) => {
1185
+ try {
1186
+ const approval = await client.checkApproval(args.id);
1187
+ const lines = [
1188
+ `**Approval ${approval.id}**`,
1189
+ `Status: ${approval.status}`,
1190
+ `Scope: ${approval.scope}`,
1191
+ `Risk: ${approval.riskLevel}`,
1192
+ ];
1193
+ if (approval.approver)
1194
+ lines.push(`Approved by: ${approval.approver}`);
1195
+ if (approval.decisionReason)
1196
+ lines.push(`Reason: ${approval.decisionReason}`);
1197
+ if (approval.decidedAt)
1198
+ lines.push(`Decided: ${approval.decidedAt}`);
1199
+ if (approval.status === 'pending')
1200
+ lines.push(`Expires: ${approval.expiresAt}`);
1201
+ return textResult(lines.join('\n'));
1202
+ }
1203
+ catch (err) {
1204
+ return textResult(`Error: Could not check approval: ${err.message}`, true);
1205
+ }
1206
+ },
1207
+ },
1208
+ // --- Cost ---
1209
+ {
1210
+ name: 'report_session_cost',
1211
+ description: 'Report token usage and cost for the current session. Call at session end.',
1212
+ inputSchema: {
1213
+ type: 'object',
1214
+ properties: {
1215
+ inputTokens: { type: 'number', description: 'Input tokens consumed' },
1216
+ outputTokens: { type: 'number', description: 'Output tokens consumed' },
1217
+ cacheReadTokens: { type: 'number', description: 'Cache read tokens (optional)' },
1218
+ estimatedCost: { type: 'number', description: 'Estimated cost in USD' },
1219
+ model: { type: 'string', description: 'Model used (e.g., claude-sonnet-4-20250514)' },
1220
+ durationMinutes: { type: 'number', description: 'Session duration in minutes' },
1221
+ },
1222
+ required: ['inputTokens', 'outputTokens', 'estimatedCost', 'model'],
1223
+ },
1224
+ handler: async (client, args) => {
1225
+ await client.reportSessionCost({
1226
+ inputTokens: args.inputTokens,
1227
+ outputTokens: args.outputTokens,
1228
+ cacheReadTokens: args.cacheReadTokens || 0,
1229
+ estimatedCost: args.estimatedCost,
1230
+ model: args.model,
1231
+ durationMinutes: args.durationMinutes || 0,
1232
+ });
1233
+ return textResult(`Session cost recorded: $${args.estimatedCost.toFixed(4)} (${args.inputTokens + args.outputTokens} tokens)`);
1234
+ },
1235
+ },
1236
+ ];
1237
+ //# sourceMappingURL=tools.js.map