@dot-ai/core 0.5.2 → 0.7.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.
Files changed (132) hide show
  1. package/dist/boot-cache.d.ts +40 -0
  2. package/dist/boot-cache.d.ts.map +1 -0
  3. package/dist/boot-cache.js +72 -0
  4. package/dist/boot-cache.js.map +1 -0
  5. package/dist/capabilities.d.ts +35 -0
  6. package/dist/capabilities.d.ts.map +1 -0
  7. package/dist/capabilities.js +17 -0
  8. package/dist/capabilities.js.map +1 -0
  9. package/dist/config.d.ts +7 -23
  10. package/dist/config.d.ts.map +1 -1
  11. package/dist/config.js +131 -108
  12. package/dist/config.js.map +1 -1
  13. package/dist/extension-api.d.ts +65 -0
  14. package/dist/extension-api.d.ts.map +1 -0
  15. package/dist/extension-api.js +2 -0
  16. package/dist/extension-api.js.map +1 -0
  17. package/dist/extension-loader.d.ts +19 -0
  18. package/dist/extension-loader.d.ts.map +1 -0
  19. package/dist/extension-loader.js +113 -0
  20. package/dist/extension-loader.js.map +1 -0
  21. package/dist/extension-runner.d.ts +62 -0
  22. package/dist/extension-runner.d.ts.map +1 -0
  23. package/dist/extension-runner.js +260 -0
  24. package/dist/extension-runner.js.map +1 -0
  25. package/dist/extension-types.d.ts +312 -0
  26. package/dist/extension-types.d.ts.map +1 -0
  27. package/dist/extension-types.js +89 -0
  28. package/dist/extension-types.js.map +1 -0
  29. package/dist/format.d.ts +13 -1
  30. package/dist/format.d.ts.map +1 -1
  31. package/dist/format.js +131 -15
  32. package/dist/format.js.map +1 -1
  33. package/dist/format.spec.d.ts +2 -0
  34. package/dist/format.spec.d.ts.map +1 -0
  35. package/dist/format.spec.js +140 -0
  36. package/dist/format.spec.js.map +1 -0
  37. package/dist/index.d.ts +21 -14
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +21 -14
  40. package/dist/index.js.map +1 -1
  41. package/dist/logger.d.ts +1 -1
  42. package/dist/logger.d.ts.map +1 -1
  43. package/dist/package-manager.d.ts +30 -0
  44. package/dist/package-manager.d.ts.map +1 -0
  45. package/dist/package-manager.js +91 -0
  46. package/dist/package-manager.js.map +1 -0
  47. package/dist/runtime.d.ts +119 -0
  48. package/dist/runtime.d.ts.map +1 -0
  49. package/dist/runtime.js +441 -0
  50. package/dist/runtime.js.map +1 -0
  51. package/dist/types.d.ts +29 -10
  52. package/dist/types.d.ts.map +1 -1
  53. package/package.json +4 -1
  54. package/src/__tests__/capabilities.test.ts +72 -0
  55. package/src/__tests__/config.test.ts +22 -120
  56. package/src/__tests__/extension-loader.test.ts +84 -0
  57. package/src/__tests__/extension-runner.test.ts +228 -0
  58. package/src/__tests__/fixtures/extensions/ctx-aware.js +26 -0
  59. package/src/__tests__/fixtures/extensions/security-gate.js +20 -0
  60. package/src/__tests__/fixtures/extensions/session-analytics.js +28 -0
  61. package/src/__tests__/fixtures/extensions/smart-context.js +10 -0
  62. package/src/__tests__/format.test.ts +207 -2
  63. package/src/__tests__/runtime.test.ts +141 -0
  64. package/src/boot-cache.ts +104 -0
  65. package/src/capabilities.ts +49 -0
  66. package/src/config.ts +131 -133
  67. package/src/extension-api.ts +99 -0
  68. package/src/extension-loader.ts +127 -0
  69. package/src/extension-runner.ts +297 -0
  70. package/src/extension-types.ts +416 -0
  71. package/src/format.spec.ts +175 -0
  72. package/src/format.test.ts +218 -0
  73. package/src/format.ts +140 -16
  74. package/src/index.ts +68 -30
  75. package/src/logger.ts +1 -1
  76. package/src/package-manager.ts +119 -0
  77. package/src/runtime.ts +562 -0
  78. package/src/types.ts +36 -14
  79. package/tsconfig.json +1 -1
  80. package/tsconfig.tsbuildinfo +1 -1
  81. package/.ai/memory/2026-03-04.md +0 -2
  82. package/.ai/tasks.json +0 -7
  83. package/dist/__tests__/config.test.d.ts +0 -2
  84. package/dist/__tests__/config.test.d.ts.map +0 -1
  85. package/dist/__tests__/config.test.js +0 -128
  86. package/dist/__tests__/config.test.js.map +0 -1
  87. package/dist/__tests__/e2e.test.d.ts +0 -2
  88. package/dist/__tests__/e2e.test.d.ts.map +0 -1
  89. package/dist/__tests__/e2e.test.js +0 -211
  90. package/dist/__tests__/e2e.test.js.map +0 -1
  91. package/dist/__tests__/engine.test.d.ts +0 -2
  92. package/dist/__tests__/engine.test.d.ts.map +0 -1
  93. package/dist/__tests__/engine.test.js +0 -271
  94. package/dist/__tests__/engine.test.js.map +0 -1
  95. package/dist/__tests__/format.test.d.ts +0 -2
  96. package/dist/__tests__/format.test.d.ts.map +0 -1
  97. package/dist/__tests__/format.test.js +0 -200
  98. package/dist/__tests__/format.test.js.map +0 -1
  99. package/dist/__tests__/labels.test.d.ts +0 -2
  100. package/dist/__tests__/labels.test.d.ts.map +0 -1
  101. package/dist/__tests__/labels.test.js +0 -82
  102. package/dist/__tests__/labels.test.js.map +0 -1
  103. package/dist/__tests__/loader.test.d.ts +0 -2
  104. package/dist/__tests__/loader.test.d.ts.map +0 -1
  105. package/dist/__tests__/loader.test.js +0 -161
  106. package/dist/__tests__/loader.test.js.map +0 -1
  107. package/dist/__tests__/logger.test.d.ts +0 -2
  108. package/dist/__tests__/logger.test.d.ts.map +0 -1
  109. package/dist/__tests__/logger.test.js +0 -95
  110. package/dist/__tests__/logger.test.js.map +0 -1
  111. package/dist/__tests__/nodes.test.d.ts +0 -2
  112. package/dist/__tests__/nodes.test.d.ts.map +0 -1
  113. package/dist/__tests__/nodes.test.js +0 -83
  114. package/dist/__tests__/nodes.test.js.map +0 -1
  115. package/dist/contracts.d.ts +0 -56
  116. package/dist/contracts.d.ts.map +0 -1
  117. package/dist/contracts.js +0 -2
  118. package/dist/contracts.js.map +0 -1
  119. package/dist/engine.d.ts +0 -38
  120. package/dist/engine.d.ts.map +0 -1
  121. package/dist/engine.js +0 -88
  122. package/dist/engine.js.map +0 -1
  123. package/dist/loader.d.ts +0 -26
  124. package/dist/loader.d.ts.map +0 -1
  125. package/dist/loader.js +0 -120
  126. package/dist/loader.js.map +0 -1
  127. package/src/__tests__/e2e.test.ts +0 -257
  128. package/src/__tests__/engine.test.ts +0 -305
  129. package/src/__tests__/loader.test.ts +0 -191
  130. package/src/contracts.ts +0 -71
  131. package/src/engine.ts +0 -145
  132. package/src/loader.ts +0 -152
@@ -1,6 +1,7 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { formatContext } from '../format.js';
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { formatContext, formatToolHints } from '../format.js';
3
3
  import type { EnrichedContext, Identity, MemoryEntry, Skill, Tool } from '../types.js';
4
+ import type { Capability } from '../capabilities.js';
4
5
 
5
6
  function makeContext(overrides?: Partial<EnrichedContext>): EnrichedContext {
6
7
  return {
@@ -15,6 +16,8 @@ function makeContext(overrides?: Partial<EnrichedContext>): EnrichedContext {
15
16
  };
16
17
  }
17
18
 
19
+ const makeEmptyContext = makeContext;
20
+
18
21
  function makeIdentity(overrides?: Partial<Identity>): Identity {
19
22
  return {
20
23
  type: 'agents',
@@ -244,4 +247,206 @@ describe('formatContext', () => {
244
247
  expect(result).toContain('Real content');
245
248
  expect(result).not.toContain('no-content');
246
249
  });
250
+
251
+ describe('format stability (determinism)', () => {
252
+ it('produces identical output for same inputs called twice', () => {
253
+ const ctx = makeContext({
254
+ identities: [
255
+ makeIdentity({ content: 'Identity A', priority: 80, type: 'user' }),
256
+ makeIdentity({ content: 'Identity B', priority: 100, type: 'agents' }),
257
+ ],
258
+ memories: [
259
+ makeMemory({ content: 'Memory 1', date: '2025-01-01' }),
260
+ makeMemory({ content: 'Memory 2', date: '2025-03-01' }),
261
+ ],
262
+ skills: [
263
+ makeSkill({ name: 'zebra-skill', content: 'Zebra content' }),
264
+ makeSkill({ name: 'alpha-skill', content: 'Alpha content' }),
265
+ ],
266
+ tools: [
267
+ makeTool({ name: 'z-tool', description: 'Z tool' }),
268
+ makeTool({ name: 'a-tool', description: 'A tool' }),
269
+ ],
270
+ });
271
+
272
+ const result1 = formatContext(ctx);
273
+ const result2 = formatContext(ctx);
274
+
275
+ expect(result1).toBe(result2);
276
+ });
277
+
278
+ it('produces same output regardless of input ordering (skills sorted by name)', () => {
279
+ const ctxA = makeContext({
280
+ skills: [
281
+ makeSkill({ name: 'zebra-skill', content: 'Zebra content' }),
282
+ makeSkill({ name: 'alpha-skill', content: 'Alpha content' }),
283
+ makeSkill({ name: 'middle-skill', content: 'Middle content' }),
284
+ ],
285
+ });
286
+
287
+ const ctxB = makeContext({
288
+ skills: [
289
+ makeSkill({ name: 'middle-skill', content: 'Middle content' }),
290
+ makeSkill({ name: 'alpha-skill', content: 'Alpha content' }),
291
+ makeSkill({ name: 'zebra-skill', content: 'Zebra content' }),
292
+ ],
293
+ });
294
+
295
+ expect(formatContext(ctxA)).toBe(formatContext(ctxB));
296
+ });
297
+
298
+ it('sorts skills by name alphabetically', () => {
299
+ const ctx = makeContext({
300
+ skills: [
301
+ makeSkill({ name: 'zebra-skill', content: 'Zebra content' }),
302
+ makeSkill({ name: 'alpha-skill', content: 'Alpha content' }),
303
+ makeSkill({ name: 'middle-skill', content: 'Middle content' }),
304
+ ],
305
+ });
306
+
307
+ const result = formatContext(ctx);
308
+ const alphaPos = result.indexOf('alpha-skill');
309
+ const middlePos = result.indexOf('middle-skill');
310
+ const zebraPos = result.indexOf('zebra-skill');
311
+
312
+ expect(alphaPos).toBeLessThan(middlePos);
313
+ expect(middlePos).toBeLessThan(zebraPos);
314
+ });
315
+
316
+ it('sorts memories by date DESC (most recent first)', () => {
317
+ const ctx = makeContext({
318
+ memories: [
319
+ makeMemory({ content: 'Oldest memory', date: '2024-01-01' }),
320
+ makeMemory({ content: 'Newest memory', date: '2025-12-31' }),
321
+ makeMemory({ content: 'Middle memory', date: '2025-06-15' }),
322
+ ],
323
+ });
324
+
325
+ const result = formatContext(ctx);
326
+ const newestPos = result.indexOf('Newest memory');
327
+ const middlePos = result.indexOf('Middle memory');
328
+ const oldestPos = result.indexOf('Oldest memory');
329
+
330
+ expect(newestPos).toBeLessThan(middlePos);
331
+ expect(middlePos).toBeLessThan(oldestPos);
332
+ });
333
+
334
+ it('produces same output for memories regardless of input ordering', () => {
335
+ const ctxA = makeContext({
336
+ memories: [
337
+ makeMemory({ content: 'Old memory', date: '2024-01-01' }),
338
+ makeMemory({ content: 'New memory', date: '2025-12-31' }),
339
+ ],
340
+ });
341
+
342
+ const ctxB = makeContext({
343
+ memories: [
344
+ makeMemory({ content: 'New memory', date: '2025-12-31' }),
345
+ makeMemory({ content: 'Old memory', date: '2024-01-01' }),
346
+ ],
347
+ });
348
+
349
+ expect(formatContext(ctxA)).toBe(formatContext(ctxB));
350
+ });
351
+
352
+ it('sorts identities by priority DESC then type alphabetically', () => {
353
+ const ctx = makeContext({
354
+ identities: [
355
+ makeIdentity({ content: 'Type z same priority', priority: 50, type: 'z-type' }),
356
+ makeIdentity({ content: 'Type a same priority', priority: 50, type: 'a-type' }),
357
+ makeIdentity({ content: 'High priority identity', priority: 100, type: 'agents' }),
358
+ ],
359
+ });
360
+
361
+ const result = formatContext(ctx);
362
+ const highPos = result.indexOf('High priority identity');
363
+ const typeAPos = result.indexOf('Type a same priority');
364
+ const typeZPos = result.indexOf('Type z same priority');
365
+
366
+ // High priority first
367
+ expect(highPos).toBeLessThan(typeAPos);
368
+ // Same priority: 'a-type' before 'z-type' alphabetically
369
+ expect(typeAPos).toBeLessThan(typeZPos);
370
+ });
371
+ });
372
+ });
373
+
374
+ describe('formatToolHints', () => {
375
+ it('returns empty string when no capabilities have hints', () => {
376
+ expect(formatToolHints([])).toBe('');
377
+ const cap: Capability = { name: 'test', description: 'x', parameters: {}, execute: vi.fn() };
378
+ expect(formatToolHints([cap])).toBe('');
379
+ });
380
+
381
+ it('formats promptSnippet', () => {
382
+ const caps: Capability[] = [{
383
+ name: 'memory_recall',
384
+ description: 'Search memory',
385
+ parameters: {},
386
+ execute: vi.fn(),
387
+ promptSnippet: 'Use memory_recall to search stored memories.',
388
+ }];
389
+ const result = formatToolHints(caps);
390
+ expect(result).toContain('## Tool Hints');
391
+ expect(result).toContain('### memory_recall');
392
+ expect(result).toContain('Use memory_recall to search stored memories.');
393
+ });
394
+
395
+ it('formats promptGuidelines', () => {
396
+ const caps: Capability[] = [{
397
+ name: 'task_list',
398
+ description: 'List tasks',
399
+ parameters: {},
400
+ execute: vi.fn(),
401
+ promptGuidelines: 'Always check tasks before starting work.',
402
+ }];
403
+ const result = formatToolHints(caps);
404
+ expect(result).toContain('> Always check tasks before starting work.');
405
+ });
406
+
407
+ it('formats both snippet and guidelines', () => {
408
+ const caps: Capability[] = [{
409
+ name: 'tool',
410
+ description: 'd',
411
+ parameters: {},
412
+ execute: vi.fn(),
413
+ promptSnippet: 'snippet text',
414
+ promptGuidelines: 'guideline text',
415
+ }];
416
+ const result = formatToolHints(caps);
417
+ expect(result).toContain('snippet text');
418
+ expect(result).toContain('> guideline text');
419
+ });
420
+
421
+ it('only includes capabilities with hints', () => {
422
+ const caps: Capability[] = [
423
+ { name: 'no-hints', description: 'd', parameters: {}, execute: vi.fn() },
424
+ { name: 'with-hint', description: 'd', parameters: {}, execute: vi.fn(), promptSnippet: 'hint' },
425
+ ];
426
+ const result = formatToolHints(caps);
427
+ expect(result).not.toContain('no-hints');
428
+ expect(result).toContain('with-hint');
429
+ });
430
+ });
431
+
432
+ describe('skillDisclosure: progressive', () => {
433
+ it('shows only description, not content, in progressive mode', () => {
434
+ const ctx = makeEmptyContext();
435
+ ctx.skills = [
436
+ { name: 'my-skill', description: 'Does something useful', labels: [], content: 'Full skill body here' },
437
+ ];
438
+ const result = formatContext(ctx, { skillDisclosure: 'progressive' });
439
+ expect(result).toContain('### my-skill');
440
+ expect(result).toContain('Does something useful');
441
+ expect(result).not.toContain('Full skill body here');
442
+ });
443
+
444
+ it('defaults to full disclosure (shows content)', () => {
445
+ const ctx = makeEmptyContext();
446
+ ctx.skills = [
447
+ { name: 'my-skill', description: 'Desc', labels: [], content: 'Full body' },
448
+ ];
449
+ const result = formatContext(ctx);
450
+ expect(result).toContain('Full body');
451
+ });
247
452
  });
@@ -0,0 +1,141 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { DotAiRuntime } from '../runtime.js';
3
+
4
+ describe('DotAiRuntime', () => {
5
+ describe('boot', () => {
6
+ it('boots and is idempotent', async () => {
7
+ const runtime = new DotAiRuntime({ workspaceRoot: '/tmp/nonexistent' });
8
+ await runtime.boot();
9
+ expect(runtime.isBooted).toBe(true);
10
+ // Second boot is a no-op
11
+ await runtime.boot();
12
+ expect(runtime.isBooted).toBe(true);
13
+ });
14
+
15
+ it('isBooted is false before boot', () => {
16
+ const runtime = new DotAiRuntime({ workspaceRoot: '/tmp/nonexistent' });
17
+ expect(runtime.isBooted).toBe(false);
18
+ });
19
+
20
+ it('auto-boots on processPrompt', async () => {
21
+ const runtime = new DotAiRuntime({ workspaceRoot: '/tmp/nonexistent' });
22
+ expect(runtime.isBooted).toBe(false);
23
+ await runtime.processPrompt('hello');
24
+ expect(runtime.isBooted).toBe(true);
25
+ });
26
+ });
27
+
28
+ describe('processPrompt', () => {
29
+ it('returns sections, labels, formatted, enriched, capabilities', async () => {
30
+ const runtime = new DotAiRuntime({ workspaceRoot: '/tmp/nonexistent' });
31
+ await runtime.boot();
32
+ const result = await runtime.processPrompt('hello world');
33
+ expect(result.formatted).toBeDefined();
34
+ expect(result.enriched).toBeDefined();
35
+ expect(result.capabilities).toBeDefined();
36
+ expect(result.labels).toBeDefined();
37
+ expect(result.sections).toBeDefined();
38
+ });
39
+ });
40
+
41
+ describe('learn', () => {
42
+ it('fires agent_end without throwing', async () => {
43
+ const runtime = new DotAiRuntime({ workspaceRoot: '/tmp/nonexistent' });
44
+ await runtime.boot();
45
+ await runtime.learn('test response');
46
+ });
47
+ });
48
+
49
+ describe('event firing', () => {
50
+ it('fire returns empty when not booted', async () => {
51
+ const runtime = new DotAiRuntime({ workspaceRoot: '/tmp/nonexistent' });
52
+ const results = await runtime.fire('any');
53
+ expect(results).toEqual([]);
54
+ });
55
+
56
+ it('fire returns empty with no extensions', async () => {
57
+ const runtime = new DotAiRuntime({ workspaceRoot: '/tmp/nonexistent' });
58
+ await runtime.boot();
59
+ const results = await runtime.fire('custom_event', { data: 1 });
60
+ expect(results).toEqual([]);
61
+ });
62
+
63
+ it('fireToolCall returns null when not booted', async () => {
64
+ const runtime = new DotAiRuntime({ workspaceRoot: '/tmp/nonexistent' });
65
+ const result = await runtime.fireToolCall({ tool: 'test', input: {} });
66
+ expect(result).toBeNull();
67
+ });
68
+
69
+ it('fireToolCall returns null with no extensions', async () => {
70
+ const runtime = new DotAiRuntime({ workspaceRoot: '/tmp/nonexistent' });
71
+ await runtime.boot();
72
+ const result = await runtime.fireToolCall({ tool: 'test', input: {} });
73
+ expect(result).toBeNull();
74
+ });
75
+ });
76
+
77
+ describe('shutdown', () => {
78
+ it('fires session_end and flushes', async () => {
79
+ const flushFn = vi.fn().mockResolvedValue(undefined);
80
+ const runtime = new DotAiRuntime({
81
+ workspaceRoot: '/tmp/nonexistent',
82
+ logger: { log: vi.fn(), flush: flushFn },
83
+ });
84
+ await runtime.boot();
85
+ await runtime.shutdown();
86
+ expect(flushFn).toHaveBeenCalledOnce();
87
+ });
88
+
89
+ it('works without boot', async () => {
90
+ const flushFn = vi.fn().mockResolvedValue(undefined);
91
+ const runtime = new DotAiRuntime({
92
+ workspaceRoot: '/tmp/nonexistent',
93
+ logger: { log: vi.fn(), flush: flushFn },
94
+ });
95
+ await runtime.shutdown();
96
+ expect(flushFn).toHaveBeenCalledOnce();
97
+ });
98
+ });
99
+
100
+ describe('flush', () => {
101
+ it('flushes logger', async () => {
102
+ const flushFn = vi.fn().mockResolvedValue(undefined);
103
+ const runtime = new DotAiRuntime({
104
+ workspaceRoot: '/tmp/nonexistent',
105
+ logger: { log: vi.fn(), flush: flushFn },
106
+ });
107
+ await runtime.flush();
108
+ expect(flushFn).toHaveBeenCalledOnce();
109
+ });
110
+ });
111
+
112
+ describe('accessors', () => {
113
+ it('runner is available after boot', async () => {
114
+ const runtime = new DotAiRuntime({ workspaceRoot: '/tmp/nonexistent' });
115
+ await runtime.boot();
116
+ expect(runtime.runner).not.toBeNull();
117
+ });
118
+
119
+ it('commands returns empty array when no extensions', async () => {
120
+ const runtime = new DotAiRuntime({ workspaceRoot: '/tmp/nonexistent' });
121
+ await runtime.boot();
122
+ expect(runtime.commands).toEqual([]);
123
+ });
124
+
125
+ it('diagnostics include vocabulary size', async () => {
126
+ const runtime = new DotAiRuntime({ workspaceRoot: '/tmp/nonexistent' });
127
+ await runtime.boot();
128
+ const diag = runtime.diagnostics;
129
+ expect(diag.vocabularySize).toBeDefined();
130
+ expect(diag.capabilityCount).toBe(0); // no extensions = no tools
131
+ expect(diag.extensions).toEqual([]);
132
+ });
133
+
134
+ it('diagnostics before boot', () => {
135
+ const runtime = new DotAiRuntime({ workspaceRoot: '/tmp/nonexistent' });
136
+ const diag = runtime.diagnostics;
137
+ expect(diag.extensions).toEqual([]);
138
+ expect(diag.capabilityCount).toBe(0);
139
+ });
140
+ });
141
+ });
@@ -0,0 +1,104 @@
1
+ import { readFile, writeFile, mkdir, stat } from 'node:fs/promises';
2
+ import { createHash } from 'node:crypto';
3
+ import { join } from 'node:path';
4
+
5
+ /**
6
+ * Cached boot data — vocabulary, extension paths, tool schemas.
7
+ * Stored at .ai/.cache/boot.json for fast hook startup.
8
+ */
9
+ export interface BootCacheData {
10
+ /** Cache format version */
11
+ version: 1;
12
+ /** Checksum of inputs that produced this cache */
13
+ checksum: string;
14
+ /** Label vocabulary from resources_discover */
15
+ vocabulary: string[];
16
+ /** Extension paths that were loaded */
17
+ extensionPaths: string[];
18
+ /** Tool names + descriptions from registered tools */
19
+ tools: Array<{ name: string; description: string }>;
20
+ /** Timestamp of cache creation */
21
+ createdAt: string;
22
+ }
23
+
24
+ const CACHE_DIR = '.ai/.cache';
25
+ const CACHE_FILE = 'boot.json';
26
+
27
+ /**
28
+ * Compute a checksum from file modification times.
29
+ * Used to invalidate the cache when extensions or config change.
30
+ */
31
+ export async function computeChecksum(
32
+ workspaceRoot: string,
33
+ extensionPaths: string[],
34
+ ): Promise<string> {
35
+ const hash = createHash('sha256');
36
+
37
+ // Include settings.json mtime (or dot-ai.yml)
38
+ for (const configName of ['settings.json', 'dot-ai.yml']) {
39
+ try {
40
+ const s = await stat(join(workspaceRoot, '.ai', configName));
41
+ hash.update(`config:${configName}:${s.mtimeMs}`);
42
+ } catch { /* not found */ }
43
+ }
44
+
45
+ // Include .ai/extensions/ dir mtime
46
+ try {
47
+ const s = await stat(join(workspaceRoot, '.ai', 'extensions'));
48
+ hash.update(`extdir:${s.mtimeMs}`);
49
+ } catch { /* not found */ }
50
+
51
+ // Include each extension file's mtime
52
+ for (const extPath of extensionPaths.sort()) {
53
+ try {
54
+ const s = await stat(extPath);
55
+ hash.update(`ext:${extPath}:${s.mtimeMs}`);
56
+ } catch { /* not found */ }
57
+ }
58
+
59
+ return hash.digest('hex').slice(0, 16);
60
+ }
61
+
62
+ /**
63
+ * Try to load cached boot data.
64
+ * Returns null if cache is missing, invalid, or checksum doesn't match.
65
+ */
66
+ export async function loadBootCache(
67
+ workspaceRoot: string,
68
+ currentChecksum: string,
69
+ ): Promise<BootCacheData | null> {
70
+ try {
71
+ const cachePath = join(workspaceRoot, CACHE_DIR, CACHE_FILE);
72
+ const raw = await readFile(cachePath, 'utf-8');
73
+ const data = JSON.parse(raw) as BootCacheData;
74
+
75
+ if (data.version !== 1) return null;
76
+ if (data.checksum !== currentChecksum) return null;
77
+
78
+ return data;
79
+ } catch {
80
+ return null;
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Write boot cache to disk.
86
+ */
87
+ export async function writeBootCache(
88
+ workspaceRoot: string,
89
+ data: BootCacheData,
90
+ ): Promise<void> {
91
+ const cacheDir = join(workspaceRoot, CACHE_DIR);
92
+ await mkdir(cacheDir, { recursive: true });
93
+ const cachePath = join(cacheDir, CACHE_FILE);
94
+ await writeFile(cachePath, JSON.stringify(data, null, 2), 'utf-8');
95
+ }
96
+
97
+ /**
98
+ * Clear the boot cache.
99
+ */
100
+ export async function clearBootCache(workspaceRoot: string): Promise<void> {
101
+ const { rm } = await import('node:fs/promises');
102
+ const cacheDir = join(workspaceRoot, CACHE_DIR);
103
+ await rm(cacheDir, { recursive: true, force: true });
104
+ }
@@ -0,0 +1,49 @@
1
+ import type { ToolDefinition } from './extension-types.js';
2
+
3
+ /**
4
+ * The result returned by a capability execution.
5
+ */
6
+ export interface CapabilityResult {
7
+ text: string;
8
+ details?: Record<string, unknown>;
9
+ }
10
+
11
+ /**
12
+ * An interactive tool (capability) that an extension exposes to agents.
13
+ * Adapters translate these into the agent's native tool format.
14
+ */
15
+ export interface Capability {
16
+ name: string;
17
+ description: string;
18
+ parameters: Record<string, unknown>; // JSON Schema
19
+ execute(params: Record<string, unknown>): Promise<CapabilityResult>;
20
+ /** Capability category */
21
+ category?: 'memory' | 'tasks' | string;
22
+ /** Whether this capability only reads data (no side effects) */
23
+ readOnly?: boolean;
24
+ /** Whether the adapter should ask for user confirmation before executing */
25
+ confirmationRequired?: boolean;
26
+ /** Capability version — incremented when parameter schema changes */
27
+ version?: number;
28
+ /** Injected into system prompt when tool is active */
29
+ promptSnippet?: string;
30
+ /** Guidelines for the LLM when using this tool */
31
+ promptGuidelines?: string;
32
+ }
33
+
34
+ /**
35
+ * Convert an extension ToolDefinition into a Capability.
36
+ */
37
+ export function toolDefinitionToCapability(tool: ToolDefinition): Capability {
38
+ return {
39
+ name: tool.name,
40
+ description: tool.description,
41
+ parameters: tool.parameters,
42
+ promptSnippet: tool.promptSnippet,
43
+ promptGuidelines: tool.promptGuidelines,
44
+ async execute(params: Record<string, unknown>): Promise<CapabilityResult> {
45
+ const result = await tool.execute(params);
46
+ return { text: result.content, details: result.details as Record<string, unknown> | undefined };
47
+ },
48
+ };
49
+ }