@dot-ai/core 0.9.0 → 0.11.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 (50) hide show
  1. package/dist/boot-cache.d.ts +1 -1
  2. package/dist/boot-cache.d.ts.map +1 -1
  3. package/dist/extension-api.d.ts +10 -9
  4. package/dist/extension-api.d.ts.map +1 -1
  5. package/dist/extension-loader.d.ts +9 -2
  6. package/dist/extension-loader.d.ts.map +1 -1
  7. package/dist/extension-loader.js +179 -55
  8. package/dist/extension-loader.js.map +1 -1
  9. package/dist/extension-runner.d.ts +9 -4
  10. package/dist/extension-runner.d.ts.map +1 -1
  11. package/dist/extension-runner.js +34 -13
  12. package/dist/extension-runner.js.map +1 -1
  13. package/dist/extension-types.d.ts +15 -115
  14. package/dist/extension-types.d.ts.map +1 -1
  15. package/dist/extension-types.js +1 -88
  16. package/dist/extension-types.js.map +1 -1
  17. package/dist/format.d.ts +21 -0
  18. package/dist/format.d.ts.map +1 -1
  19. package/dist/format.js +74 -0
  20. package/dist/format.js.map +1 -1
  21. package/dist/index.d.ts +6 -7
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +3 -4
  24. package/dist/index.js.map +1 -1
  25. package/dist/package-manager.d.ts +28 -5
  26. package/dist/package-manager.d.ts.map +1 -1
  27. package/dist/package-manager.js +126 -20
  28. package/dist/package-manager.js.map +1 -1
  29. package/dist/runtime.d.ts +31 -43
  30. package/dist/runtime.d.ts.map +1 -1
  31. package/dist/runtime.js +79 -177
  32. package/dist/runtime.js.map +1 -1
  33. package/package.json +1 -1
  34. package/src/__tests__/extension-loader.test.ts +154 -11
  35. package/src/__tests__/extension-runner.test.ts +245 -32
  36. package/src/__tests__/fixtures/extensions/ctx-aware.js +12 -3
  37. package/src/__tests__/fixtures/extensions/smart-context.js +12 -3
  38. package/src/__tests__/format.test.ts +178 -1
  39. package/src/__tests__/package-manager.test.ts +237 -0
  40. package/src/__tests__/runtime.test.ts +38 -10
  41. package/src/boot-cache.ts +1 -1
  42. package/src/extension-api.ts +10 -15
  43. package/src/extension-loader.ts +187 -57
  44. package/src/extension-runner.ts +44 -15
  45. package/src/extension-types.ts +26 -195
  46. package/src/format.ts +100 -0
  47. package/src/index.ts +5 -13
  48. package/src/package-manager.ts +146 -23
  49. package/src/runtime.ts +96 -222
  50. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,237 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import { mkdir, writeFile, rm, readFile } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ import { tmpdir } from 'node:os';
5
+ import {
6
+ listPackages,
7
+ resolvePackages,
8
+ ensurePackagesInstalled,
9
+ } from '../package-manager.js';
10
+
11
+ // Use temp directory for test fixtures
12
+ const testDir = join(tmpdir(), 'dot-ai-pkg-test-' + Date.now());
13
+
14
+ beforeEach(async () => {
15
+ await mkdir(join(testDir, '.ai', 'packages'), { recursive: true });
16
+ });
17
+
18
+ afterEach(async () => {
19
+ await rm(testDir, { recursive: true, force: true });
20
+ });
21
+
22
+ // ── Helper: create a fake installed package ─────────────────────────────────
23
+ async function createFakePackage(
24
+ name: string,
25
+ version: string,
26
+ dotAi?: { extensions?: string[] },
27
+ ) {
28
+ const installDir = join(testDir, '.ai', 'packages');
29
+ const pkgDir = join(installDir, 'node_modules', name);
30
+ await mkdir(pkgDir, { recursive: true });
31
+
32
+ // Write package.json for installed package
33
+ const pkgJson: Record<string, unknown> = { name, version };
34
+ if (dotAi) pkgJson['dot-ai'] = dotAi;
35
+ await writeFile(join(pkgDir, 'package.json'), JSON.stringify(pkgJson));
36
+
37
+ // Create extension files if declared
38
+ if (dotAi?.extensions) {
39
+ for (const ext of dotAi.extensions) {
40
+ const extPath = join(pkgDir, ext);
41
+ await mkdir(join(extPath, '..'), { recursive: true });
42
+ await writeFile(extPath, 'export default function() {}');
43
+ }
44
+ }
45
+
46
+ // Update the packages/package.json dependencies
47
+ const depsPkgPath = join(installDir, 'package.json');
48
+ let deps: Record<string, string> = {};
49
+ try {
50
+ const raw = await readFile(depsPkgPath, 'utf-8');
51
+ const existing = JSON.parse(raw);
52
+ deps = existing.dependencies ?? {};
53
+ } catch { /* no file yet */ }
54
+
55
+ deps[name] = version;
56
+ await writeFile(depsPkgPath, JSON.stringify({ dependencies: deps }));
57
+ }
58
+
59
+ describe('listPackages', () => {
60
+ it('returns empty array when no packages installed', async () => {
61
+ const result = await listPackages(testDir);
62
+ expect(result).toEqual([]);
63
+ });
64
+
65
+ it('lists installed packages with dot-ai field', async () => {
66
+ await createFakePackage('@dot-ai/ext-memory', '1.0.0', {
67
+ extensions: ['./dist/extension.js'],
68
+ });
69
+
70
+ const result = await listPackages(testDir);
71
+ expect(result).toHaveLength(1);
72
+ expect(result[0].name).toBe('@dot-ai/ext-memory');
73
+ expect(result[0].version).toBe('1.0.0');
74
+ expect(result[0].dotAi?.extensions).toEqual(['./dist/extension.js']);
75
+ });
76
+
77
+ it('lists packages without dot-ai field', async () => {
78
+ await createFakePackage('some-lib', '2.0.0');
79
+
80
+ const result = await listPackages(testDir);
81
+ expect(result).toHaveLength(1);
82
+ expect(result[0].name).toBe('some-lib');
83
+ expect(result[0].dotAi).toBeUndefined();
84
+ });
85
+
86
+ it('lists multiple packages', async () => {
87
+ await createFakePackage('@dot-ai/ext-memory', '1.0.0', {
88
+ extensions: ['./dist/extension.js'],
89
+ });
90
+ await createFakePackage('@dot-ai/ext-identity', '1.0.0', {
91
+ extensions: ['./dist/index.js'],
92
+ });
93
+
94
+ const result = await listPackages(testDir);
95
+ expect(result).toHaveLength(2);
96
+ });
97
+ });
98
+
99
+ describe('resolvePackages', () => {
100
+ it('returns empty when no packages', async () => {
101
+ const result = await resolvePackages(testDir);
102
+ expect(result.extensions).toEqual([]);
103
+ expect(result.skills).toEqual([]);
104
+ expect(result.providers).toEqual([]);
105
+ });
106
+
107
+ it('resolves extension paths from packages with dot-ai field', async () => {
108
+ await createFakePackage('@dot-ai/ext-memory', '1.0.0', {
109
+ extensions: ['./dist/extension.js'],
110
+ });
111
+
112
+ const result = await resolvePackages(testDir);
113
+ expect(result.extensions).toHaveLength(1);
114
+ expect(result.extensions[0]).toContain('ext-memory');
115
+ expect(result.extensions[0]).toContain('dist/extension.js');
116
+ });
117
+
118
+ it('skips packages without dot-ai field', async () => {
119
+ await createFakePackage('some-lib', '2.0.0');
120
+
121
+ const result = await resolvePackages(testDir);
122
+ expect(result.extensions).toHaveLength(0);
123
+ });
124
+ });
125
+
126
+ describe('ensurePackagesInstalled', () => {
127
+ it('skips already installed packages', async () => {
128
+ await createFakePackage('@dot-ai/ext-memory', '1.0.0', {
129
+ extensions: ['./dist/extension.js'],
130
+ });
131
+
132
+ const result = await ensurePackagesInstalled(
133
+ testDir,
134
+ ['@dot-ai/ext-memory'],
135
+ );
136
+
137
+ expect(result.skipped).toContain('@dot-ai/ext-memory');
138
+ expect(result.installed).toHaveLength(0);
139
+ expect(result.errors).toHaveLength(0);
140
+ });
141
+
142
+ it('skips packages when onMissing returns skip', async () => {
143
+ const result = await ensurePackagesInstalled(
144
+ testDir,
145
+ ['@dot-ai/ext-not-there'],
146
+ async () => 'skip',
147
+ );
148
+
149
+ expect(result.skipped).toContain('@dot-ai/ext-not-there');
150
+ expect(result.installed).toHaveLength(0);
151
+ });
152
+
153
+ it('errors when onMissing returns error', async () => {
154
+ const result = await ensurePackagesInstalled(
155
+ testDir,
156
+ ['@dot-ai/ext-not-there'],
157
+ async () => 'error',
158
+ );
159
+
160
+ expect(result.errors).toHaveLength(1);
161
+ expect(result.errors[0].source).toBe('@dot-ai/ext-not-there');
162
+ expect(result.errors[0].error).toContain('Missing package');
163
+ });
164
+
165
+ it('reports errors for failed installs', async () => {
166
+ // Use an impossible package name that npm will fail to find
167
+ const result = await ensurePackagesInstalled(
168
+ testDir,
169
+ ['@dot-ai-fake-nonexistent-pkg-xyz/no-exist'],
170
+ );
171
+
172
+ expect(result.errors).toHaveLength(1);
173
+ expect(result.errors[0].source).toBe('@dot-ai-fake-nonexistent-pkg-xyz/no-exist');
174
+ });
175
+
176
+ it('handles npm: prefix in source', async () => {
177
+ await createFakePackage('@dot-ai/ext-memory', '1.0.0', {
178
+ extensions: ['./dist/extension.js'],
179
+ });
180
+
181
+ const result = await ensurePackagesInstalled(
182
+ testDir,
183
+ ['npm:@dot-ai/ext-memory@1.0.0'],
184
+ );
185
+
186
+ // npm:@dot-ai/ext-memory@1.0.0 → name = @dot-ai/ext-memory → already installed
187
+ expect(result.skipped).toContain('npm:@dot-ai/ext-memory@1.0.0');
188
+ });
189
+
190
+ it('handles multiple packages', async () => {
191
+ await createFakePackage('@dot-ai/ext-memory', '1.0.0', {
192
+ extensions: ['./dist/extension.js'],
193
+ });
194
+
195
+ const result = await ensurePackagesInstalled(
196
+ testDir,
197
+ ['@dot-ai/ext-memory', '@dot-ai/ext-not-there'],
198
+ async (source) => source.includes('not-there') ? 'skip' : 'install',
199
+ );
200
+
201
+ expect(result.skipped).toContain('@dot-ai/ext-memory');
202
+ expect(result.skipped).toContain('@dot-ai/ext-not-there');
203
+ });
204
+ });
205
+
206
+ describe('parsePackageSource (via ensurePackagesInstalled)', () => {
207
+ // We test parsePackageSource indirectly through ensurePackagesInstalled
208
+
209
+ it('handles scoped package with version', async () => {
210
+ await createFakePackage('@dot-ai/ext-memory', '1.0.0');
211
+
212
+ const result = await ensurePackagesInstalled(testDir, ['@dot-ai/ext-memory@1.0.0']);
213
+ // Should find it as already installed (strips version to get name)
214
+ expect(result.skipped).toContain('@dot-ai/ext-memory@1.0.0');
215
+ });
216
+
217
+ it('handles scoped package without version', async () => {
218
+ await createFakePackage('@dot-ai/ext-memory', '1.0.0');
219
+
220
+ const result = await ensurePackagesInstalled(testDir, ['@dot-ai/ext-memory']);
221
+ expect(result.skipped).toContain('@dot-ai/ext-memory');
222
+ });
223
+
224
+ it('handles unscoped package with version', async () => {
225
+ await createFakePackage('my-ext', '2.0.0');
226
+
227
+ const result = await ensurePackagesInstalled(testDir, ['my-ext@2.0.0']);
228
+ expect(result.skipped).toContain('my-ext@2.0.0');
229
+ });
230
+
231
+ it('handles npm: prefix with scoped package', async () => {
232
+ await createFakePackage('@dot-ai/ext-memory', '1.0.0');
233
+
234
+ const result = await ensurePackagesInstalled(testDir, ['npm:@dot-ai/ext-memory']);
235
+ expect(result.skipped).toContain('npm:@dot-ai/ext-memory');
236
+ });
237
+ });
@@ -26,23 +26,24 @@ describe('DotAiRuntime', () => {
26
26
  });
27
27
 
28
28
  describe('processPrompt', () => {
29
- it('returns sections, labels, formatted, enriched, capabilities', async () => {
29
+ it('returns sections, labels, routing', async () => {
30
30
  const runtime = new DotAiRuntime({ workspaceRoot: '/tmp/nonexistent' });
31
31
  await runtime.boot();
32
32
  const result = await runtime.processPrompt('hello world');
33
- expect(result.formatted).toBeDefined();
34
- expect(result.enriched).toBeDefined();
35
- expect(result.capabilities).toBeDefined();
36
33
  expect(result.labels).toBeDefined();
37
34
  expect(result.sections).toBeDefined();
35
+ expect(Array.isArray(result.sections)).toBe(true);
36
+ expect(result.routing).toBeDefined();
38
37
  });
39
- });
40
38
 
41
- describe('learn', () => {
42
- it('fires agent_end without throwing', async () => {
39
+ it('includes core system section', async () => {
43
40
  const runtime = new DotAiRuntime({ workspaceRoot: '/tmp/nonexistent' });
44
41
  await runtime.boot();
45
- await runtime.learn('test response');
42
+ const result = await runtime.processPrompt('hello');
43
+ const systemSection = result.sections.find(s => s.id === 'dot-ai:system');
44
+ expect(systemSection).toBeDefined();
45
+ expect(systemSection!.priority).toBe(95);
46
+ expect(systemSection!.source).toBe('core');
46
47
  });
47
48
  });
48
49
 
@@ -74,6 +75,19 @@ describe('DotAiRuntime', () => {
74
75
  });
75
76
  });
76
77
 
78
+ describe('executeTool', () => {
79
+ it('throws when not booted', async () => {
80
+ const runtime = new DotAiRuntime({ workspaceRoot: '/tmp/nonexistent' });
81
+ await expect(runtime.executeTool('test', {})).rejects.toThrow('Runtime not booted');
82
+ });
83
+
84
+ it('throws for unknown tool', async () => {
85
+ const runtime = new DotAiRuntime({ workspaceRoot: '/tmp/nonexistent' });
86
+ await runtime.boot();
87
+ await expect(runtime.executeTool('nonexistent', {})).rejects.toThrow('Tool not found');
88
+ });
89
+ });
90
+
77
91
  describe('shutdown', () => {
78
92
  it('fires session_end and flushes', async () => {
79
93
  const flushFn = vi.fn().mockResolvedValue(undefined);
@@ -122,12 +136,26 @@ describe('DotAiRuntime', () => {
122
136
  expect(runtime.commands).toEqual([]);
123
137
  });
124
138
 
125
- it('diagnostics include vocabulary size', async () => {
139
+ it('skills returns empty array when no extensions', async () => {
140
+ const runtime = new DotAiRuntime({ workspaceRoot: '/tmp/nonexistent' });
141
+ await runtime.boot();
142
+ expect(runtime.skills).toEqual([]);
143
+ });
144
+
145
+ it('identities returns empty array when no extensions', async () => {
146
+ const runtime = new DotAiRuntime({ workspaceRoot: '/tmp/nonexistent' });
147
+ await runtime.boot();
148
+ expect(runtime.identities).toEqual([]);
149
+ });
150
+
151
+ it('diagnostics include vocabulary size and counts', async () => {
126
152
  const runtime = new DotAiRuntime({ workspaceRoot: '/tmp/nonexistent' });
127
153
  await runtime.boot();
128
154
  const diag = runtime.diagnostics;
129
155
  expect(diag.vocabularySize).toBeDefined();
130
- expect(diag.capabilityCount).toBe(0); // no extensions = no tools
156
+ expect(diag.capabilityCount).toBe(0);
157
+ expect(diag.skillCount).toBe(0);
158
+ expect(diag.identityCount).toBe(0);
131
159
  expect(diag.extensions).toEqual([]);
132
160
  });
133
161
 
package/src/boot-cache.ts CHANGED
@@ -11,7 +11,7 @@ export interface BootCacheData {
11
11
  version: 1;
12
12
  /** Checksum of inputs that produced this cache */
13
13
  checksum: string;
14
- /** Label vocabulary from resources_discover */
14
+ /** Label vocabulary from registered resources */
15
15
  vocabulary: string[];
16
16
  /** Extension paths that were loaded */
17
17
  extensionPaths: string[];
@@ -1,22 +1,19 @@
1
1
  import type {
2
- ContextInjectEvent, ContextInjectResult,
3
- ContextModifyEvent, ContextModifyResult,
4
2
  ToolCallEvent, ToolCallResult,
5
3
  ToolResultEvent,
6
4
  AgentEndEvent,
7
5
  ToolDefinition,
8
6
  ExtensionContext,
9
- ResourcesDiscoverResult,
10
7
  LabelExtractEvent,
11
8
  ContextEnrichEvent, ContextEnrichResult,
12
9
  RouteEvent, RouteResult,
13
10
  InputEvent, InputResult,
14
11
  CommandDefinition,
15
12
  } from './extension-types.js';
16
- import type { Label } from './types.js';
13
+ import type { Label, Skill, Identity } from './types.js';
17
14
 
18
15
  /**
19
- * v6 Extension Context — passed as second argument to every event handler.
16
+ * v7 Extension Context — passed as second argument to every event handler.
20
17
  * Extends the base ExtensionContext with labels and optional agent capabilities.
21
18
  */
22
19
  export interface ExtensionContextV6 extends ExtensionContext {
@@ -39,8 +36,6 @@ export interface ExtensionContextV6 extends ExtensionContext {
39
36
  export interface ExtensionAPI {
40
37
  // ── Event subscription ──
41
38
 
42
- /** Resource discovery: extensions declare resources and contribute labels */
43
- on(event: 'resources_discover', handler: (e: undefined, ctx: ExtensionContextV6) => Promise<ResourcesDiscoverResult | void>): void;
44
39
  /** Label extraction: extensions can add custom labels (chain-transform) */
45
40
  on(event: 'label_extract', handler: (e: LabelExtractEvent, ctx: ExtensionContextV6) => Promise<Label[] | void>): void;
46
41
  /** Context enrichment: extensions return sections for context injection */
@@ -58,28 +53,28 @@ export interface ExtensionAPI {
58
53
 
59
54
  on(event: 'session_start', handler: (e: undefined, ctx: ExtensionContextV6) => Promise<void>): void;
60
55
  on(event: 'session_end', handler: (e: undefined, ctx: ExtensionContextV6) => Promise<void>): void;
56
+ on(event: 'session_compact', handler: (e: undefined, ctx: ExtensionContextV6) => Promise<void>): void;
61
57
  on(event: 'agent_start', handler: (e: undefined, ctx: ExtensionContextV6) => Promise<void>): void;
62
58
  on(event: 'agent_end', handler: (e: AgentEndEvent, ctx: ExtensionContextV6) => Promise<void>): void;
63
59
  on(event: 'turn_start', handler: (e: undefined, ctx: ExtensionContextV6) => Promise<void>): void;
64
60
  on(event: 'turn_end', handler: (e: undefined, ctx: ExtensionContextV6) => Promise<void>): void;
65
61
 
66
- // ── Legacy events (kept for transition) ──
67
-
68
- /** @deprecated Use context_enrich instead */
69
- on(event: 'context_inject', handler: (e: ContextInjectEvent, ctx: ExtensionContextV6) => Promise<ContextInjectResult | void>): void;
70
- /** @deprecated Use context_enrich instead */
71
- on(event: 'context_modify', handler: (e: ContextModifyEvent, ctx: ExtensionContextV6) => Promise<ContextModifyResult | void>): void;
72
-
73
62
  // ── Catch-all for custom/Pi-specific events ──
74
63
 
75
64
  on(event: string, handler: (e: any, ctx: ExtensionContextV6) => Promise<any>): void;
76
65
 
77
- // ── Capability registration ──
66
+ // ── Resource registration ──
78
67
 
79
68
  /** Register a tool that the agent can invoke */
80
69
  registerTool(tool: ToolDefinition): void;
81
70
  /** Register a command (slash command, etc.) */
82
71
  registerCommand(command: CommandDefinition): void;
72
+ /** Register a skill for context enrichment and discovery */
73
+ registerSkill(skill: Skill): void;
74
+ /** Register an identity document */
75
+ registerIdentity(identity: Identity): void;
76
+ /** Contribute labels to the global vocabulary (for label matching) */
77
+ contributeLabels(labels: string[]): void;
83
78
 
84
79
  // ── Inter-extension communication ──
85
80