@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
@@ -2,85 +2,205 @@ import { readdir, readFile, stat } from 'node:fs/promises';
2
2
  import { join, resolve } from 'node:path';
3
3
  import { homedir } from 'node:os';
4
4
  import type { ExtensionAPI } from './extension-api.js';
5
- import type { LoadedExtension, ToolDefinition, CommandDefinition, ExtensionTier } from './extension-types.js';
6
- import { EVENT_TIERS } from './extension-types.js';
7
- import type { ExtensionsConfig } from './types.js';
5
+ import type { LoadedExtension, ToolDefinition, CommandDefinition } from './extension-types.js';
6
+ import type { ExtensionsConfig, Skill, Identity } from './types.js';
7
+
8
+ function isExtensionFile(name: string): boolean {
9
+ return name.endsWith('.ts') || name.endsWith('.js');
10
+ }
11
+
12
+ /**
13
+ * Resolve extension entry points from a directory.
14
+ *
15
+ * Checks for:
16
+ * 1. package.json with "dot-ai.extensions" field -> returns declared paths
17
+ * 2. index.ts or index.js -> returns the index file
18
+ *
19
+ * Returns resolved paths or null if no entry points found.
20
+ */
21
+ async function resolveExtensionEntries(dir: string): Promise<string[] | null> {
22
+ // Check for package.json with "dot-ai" field first
23
+ try {
24
+ const pkgPath = join(dir, 'package.json');
25
+ const pkgRaw = await readFile(pkgPath, 'utf-8');
26
+ const pkg = JSON.parse(pkgRaw) as Record<string, unknown>;
27
+ const dotAi = pkg['dot-ai'] as { extensions?: string[] } | undefined;
28
+ if (dotAi?.extensions && Array.isArray(dotAi.extensions)) {
29
+ const entries: string[] = [];
30
+ for (const ext of dotAi.extensions) {
31
+ const resolvedPath = resolve(dir, ext);
32
+ try {
33
+ await stat(resolvedPath);
34
+ entries.push(resolvedPath);
35
+ } catch { /* entry doesn't exist */ }
36
+ }
37
+ if (entries.length > 0) return entries;
38
+ }
39
+ } catch { /* no package.json or invalid */ }
40
+
41
+ // Check for index.ts or index.js
42
+ for (const indexName of ['index.ts', 'index.js']) {
43
+ const indexPath = join(dir, indexName);
44
+ try {
45
+ await stat(indexPath);
46
+ return [indexPath];
47
+ } catch { /* not found */ }
48
+ }
49
+
50
+ return null;
51
+ }
52
+
53
+ /**
54
+ * Scan a directory for extensions.
55
+ *
56
+ * Discovery rules:
57
+ * 1. Direct files: `dir/*.ts` or `*.js` -> load
58
+ * 2. Subdirectory with package.json: `dir/sub/package.json` with "dot-ai" field -> load what it declares
59
+ * 3. Subdirectory with index: `dir/sub/index.ts` or `index.js` -> load
60
+ *
61
+ * No recursion beyond one level. Complex packages must use package.json manifest.
62
+ */
63
+ async function discoverExtensionsInDir(dir: string): Promise<string[]> {
64
+ const discovered: string[] = [];
65
+
66
+ try {
67
+ const entries = await readdir(dir, { withFileTypes: true });
68
+
69
+ for (const entry of entries) {
70
+ const entryPath = join(dir, entry.name);
71
+
72
+ // 1. Direct files: *.ts or *.js
73
+ if (entry.isFile() && isExtensionFile(entry.name)) {
74
+ discovered.push(entryPath);
75
+ continue;
76
+ }
77
+
78
+ // 2 & 3. Subdirectories: package.json or index file
79
+ if (entry.isDirectory()) {
80
+ const resolved = await resolveExtensionEntries(entryPath);
81
+ if (resolved) {
82
+ discovered.push(...resolved);
83
+ }
84
+ }
85
+ }
86
+ } catch { /* directory doesn't exist — skip */ }
87
+
88
+ return discovered;
89
+ }
8
90
 
9
91
  /**
10
- * Discover extension file paths from configured locations.
92
+ * Resolve extensions from installed packages in .ai/packages/.
93
+ *
94
+ * Reads the package.json created by `npm --prefix .ai/packages/` and resolves
95
+ * extension entry points from each installed package's "dot-ai.extensions" field.
96
+ */
97
+ async function resolveInstalledPackages(workspaceRoot: string): Promise<string[]> {
98
+ const packagesDir = join(workspaceRoot, '.ai', 'packages');
99
+ const pkgJsonPath = join(packagesDir, 'package.json');
100
+
101
+ try {
102
+ const raw = await readFile(pkgJsonPath, 'utf-8');
103
+ const pkg = JSON.parse(raw) as Record<string, unknown>;
104
+ const deps = pkg.dependencies as Record<string, string> | undefined;
105
+ if (!deps) return [];
106
+
107
+ const paths: string[] = [];
108
+ for (const name of Object.keys(deps)) {
109
+ const pkgDir = join(packagesDir, 'node_modules', name);
110
+ const entries = await resolveExtensionEntries(pkgDir);
111
+ if (entries) {
112
+ paths.push(...entries);
113
+ }
114
+ }
115
+ return paths;
116
+ } catch {
117
+ return [];
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Discover extension file paths from all sources.
123
+ *
124
+ * Sources (in order):
125
+ * 1. Auto-discovery: .ai/extensions/ (project-local)
126
+ * 2. Auto-discovery: ~/.ai/extensions/ (global)
127
+ * 3. Installed packages: .ai/packages/node_modules/ (installed via dot-ai install)
128
+ * 4. Configured paths: settings.json "extensions" array (explicit paths/dirs)
129
+ * 5. Configured packages: settings.json "packages" array (npm package names resolved from workspace node_modules)
11
130
  */
12
131
  export async function discoverExtensions(
13
132
  workspaceRoot: string,
14
133
  config?: ExtensionsConfig,
15
134
  ): Promise<string[]> {
16
- const paths = new Set<string>();
135
+ const seen = new Set<string>();
136
+ const allPaths: string[] = [];
137
+
138
+ const addPaths = (paths: string[]) => {
139
+ for (const p of paths) {
140
+ const resolved = resolve(p);
141
+ if (!seen.has(resolved)) {
142
+ seen.add(resolved);
143
+ allPaths.push(resolved);
144
+ }
145
+ }
146
+ };
17
147
 
18
- // Default discovery paths
19
- const searchDirs = [
20
- join(workspaceRoot, '.ai', 'extensions'),
21
- join(homedir(), '.ai', 'extensions'),
22
- ...(config?.paths ?? []).map(p => resolve(workspaceRoot, p)),
23
- ];
148
+ // 1. Project-local extensions: .ai/extensions/
149
+ addPaths(await discoverExtensionsInDir(join(workspaceRoot, '.ai', 'extensions')));
24
150
 
25
- for (const dir of searchDirs) {
26
- try {
27
- const entries = await readdir(dir, { withFileTypes: true });
28
- for (const entry of entries) {
29
- const fullPath = join(dir, entry.name);
30
- if (entry.isFile() && (entry.name.endsWith('.ts') || entry.name.endsWith('.js'))) {
31
- paths.add(fullPath);
32
- } else if (entry.isDirectory()) {
33
- // Check for index.ts or index.js
34
- for (const indexName of ['index.ts', 'index.js']) {
35
- const indexPath = join(fullPath, indexName);
36
- try {
37
- await stat(indexPath);
38
- paths.add(indexPath);
39
- break;
40
- } catch { /* not found */ }
151
+ // 2. Global extensions: ~/.ai/extensions/
152
+ addPaths(await discoverExtensionsInDir(join(homedir(), '.ai', 'extensions')));
153
+
154
+ // 3. Installed packages: .ai/packages/node_modules/
155
+ addPaths(await resolveInstalledPackages(workspaceRoot));
156
+
157
+ // 4. Explicitly configured paths from settings.json "extensions" array
158
+ if (config?.paths) {
159
+ for (const p of config.paths) {
160
+ const resolved = resolve(workspaceRoot, p);
161
+ try {
162
+ const s = await stat(resolved);
163
+ if (s.isDirectory()) {
164
+ // Check for package.json or index file first
165
+ const entries = await resolveExtensionEntries(resolved);
166
+ if (entries) {
167
+ addPaths(entries);
168
+ } else {
169
+ // Discover individual files in directory
170
+ addPaths(await discoverExtensionsInDir(resolved));
41
171
  }
42
- // Check for package.json with dot-ai field
43
- try {
44
- const pkgPath = join(fullPath, 'package.json');
45
- const pkgRaw = await readFile(pkgPath, 'utf-8');
46
- const pkg = JSON.parse(pkgRaw) as Record<string, unknown>;
47
- const dotAi = pkg['dot-ai'] as { extensions?: string[] } | undefined;
48
- if (dotAi?.extensions && Array.isArray(dotAi.extensions)) {
49
- for (const ext of dotAi.extensions) {
50
- paths.add(resolve(fullPath, ext));
51
- }
52
- }
53
- } catch { /* no package.json or no dot-ai field */ }
172
+ } else if (s.isFile()) {
173
+ addPaths([resolved]);
54
174
  }
175
+ } catch {
176
+ // Path doesn't exist — add anyway (will fail at load time with clear error)
177
+ addPaths([resolved]);
55
178
  }
56
- } catch { /* directory doesn't exist — skip */ }
179
+ }
57
180
  }
58
181
 
59
- // Also resolve npm packages from config
182
+ // 5. Configured npm packages from settings.json "packages" array
183
+ // These are resolved from the workspace's own node_modules
60
184
  if (config?.packages) {
61
185
  for (const pkg of config.packages) {
62
186
  try {
63
187
  const { createRequire } = await import('node:module');
64
188
  const require = createRequire(join(workspaceRoot, 'package.json'));
65
189
  const pkgJsonPath = require.resolve(`${pkg}/package.json`);
66
- const pkgRaw = await readFile(pkgJsonPath, 'utf-8');
67
- const pkgJson = JSON.parse(pkgRaw) as Record<string, unknown>;
68
- const dotAi = pkgJson['dot-ai'] as { extensions?: string[] } | undefined;
69
- if (dotAi?.extensions && Array.isArray(dotAi.extensions)) {
70
- const pkgDir = join(pkgJsonPath, '..');
71
- for (const ext of dotAi.extensions) {
72
- paths.add(resolve(pkgDir, ext));
73
- }
190
+ const pkgDir = join(pkgJsonPath, '..');
191
+ const entries = await resolveExtensionEntries(pkgDir);
192
+ if (entries) {
193
+ addPaths(entries);
74
194
  }
75
195
  } catch { /* package not found */ }
76
196
  }
77
197
  }
78
198
 
79
- return Array.from(paths);
199
+ return allPaths;
80
200
  }
81
201
 
82
202
  /**
83
- * Create a v6 ExtensionAPI instance that collects registrations into a LoadedExtension.
203
+ * Create an ExtensionAPI instance that collects registrations into a LoadedExtension.
84
204
  * Extensions communicate via events only — no provider access.
85
205
  */
86
206
  export function createV6CollectorAPI(
@@ -94,7 +214,9 @@ export function createV6CollectorAPI(
94
214
  handlers: new Map(),
95
215
  tools: new Map(),
96
216
  commands: new Map(),
97
- tiers: new Set(),
217
+ skills: new Map(),
218
+ identities: new Map(),
219
+ labels: new Set(),
98
220
  };
99
221
 
100
222
  const api: ExtensionAPI = {
@@ -103,11 +225,6 @@ export function createV6CollectorAPI(
103
225
  extension.handlers.set(event, []);
104
226
  }
105
227
  extension.handlers.get(event)!.push(handler);
106
-
107
- const tier: ExtensionTier | undefined = EVENT_TIERS[event];
108
- if (tier) {
109
- extension.tiers.add(tier);
110
- }
111
228
  },
112
229
  registerTool(tool: ToolDefinition) {
113
230
  extension.tools.set(tool.name, tool);
@@ -115,6 +232,19 @@ export function createV6CollectorAPI(
115
232
  registerCommand(command: CommandDefinition) {
116
233
  extension.commands.set(command.name, command);
117
234
  },
235
+ registerSkill(skill: Skill) {
236
+ extension.skills.set(skill.name, skill);
237
+ // Skills contribute their labels and triggers to vocabulary
238
+ for (const label of skill.labels) extension.labels.add(label);
239
+ for (const trigger of skill.triggers ?? []) extension.labels.add(trigger);
240
+ },
241
+ registerIdentity(identity: Identity) {
242
+ const key = `${identity.type}:${identity.node ?? 'root'}`;
243
+ extension.identities.set(key, identity);
244
+ },
245
+ contributeLabels(labels: string[]) {
246
+ for (const label of labels) extension.labels.add(label);
247
+ },
118
248
  events: eventBus ?? {
119
249
  on: () => {},
120
250
  emit: () => {},
@@ -2,7 +2,6 @@ import type { Logger } from './logger.js';
2
2
  import type {
3
3
  LoadedExtension,
4
4
  ToolDefinition,
5
- ExtensionTier,
6
5
  ToolCallEvent,
7
6
  ToolCallResult,
8
7
  ExtensionDiagnostic,
@@ -13,6 +12,7 @@ import type {
13
12
  CommandDefinition,
14
13
  InputResult,
15
14
  } from './extension-types.js';
15
+ import type { Skill, Identity } from './types.js';
16
16
 
17
17
  /**
18
18
  * Simple event bus for inter-extension communication.
@@ -111,7 +111,7 @@ export class ExtensionRunner {
111
111
  return null;
112
112
  }
113
113
 
114
- // ── v6 Emission Patterns ──
114
+ // ── Emission Patterns ──
115
115
 
116
116
  /**
117
117
  * Fire an event and collect sections from all handlers.
@@ -184,7 +184,7 @@ export class ExtensionRunner {
184
184
 
185
185
  /**
186
186
  * Fire an event as a transform chain — each handler receives the previous handler's output.
187
- * Used for `label_extract`, `input`, `context_modify`.
187
+ * Used for `label_extract`, `input`, `tool_result`.
188
188
  *
189
189
  * - Initial value is `data`.
190
190
  * - If a handler returns undefined/null, the previous value is kept (no-op).
@@ -255,6 +255,45 @@ export class ExtensionRunner {
255
255
  return Array.from(cmdMap.values());
256
256
  }
257
257
 
258
+ /** Get all registered skills across extensions (last-wins for overrides) */
259
+ get skills(): Skill[] {
260
+ const skillMap = new Map<string, Skill>();
261
+
262
+ for (const ext of this.extensions) {
263
+ for (const [name, skill] of ext.skills) {
264
+ skillMap.set(name, skill);
265
+ }
266
+ }
267
+
268
+ return Array.from(skillMap.values());
269
+ }
270
+
271
+ /** Get all registered identities across extensions */
272
+ get identities(): Identity[] {
273
+ const identityMap = new Map<string, Identity>();
274
+
275
+ for (const ext of this.extensions) {
276
+ for (const [key, identity] of ext.identities) {
277
+ identityMap.set(key, identity);
278
+ }
279
+ }
280
+
281
+ return Array.from(identityMap.values());
282
+ }
283
+
284
+ /** Get all vocabulary labels contributed by extensions */
285
+ get vocabularyLabels(): string[] {
286
+ const labels = new Set<string>();
287
+
288
+ for (const ext of this.extensions) {
289
+ for (const label of ext.labels) {
290
+ labels.add(label);
291
+ }
292
+ }
293
+
294
+ return Array.from(labels);
295
+ }
296
+
258
297
  /** Get diagnostic info */
259
298
  get diagnostics(): ExtensionDiagnostic[] {
260
299
  return this.extensions.map(ext => ({
@@ -264,21 +303,11 @@ export class ExtensionRunner {
264
303
  ),
265
304
  toolNames: Array.from(ext.tools.keys()),
266
305
  commandNames: ext.commands ? Array.from(ext.commands.keys()) : [],
267
- tiers: Array.from(ext.tiers),
306
+ skillNames: Array.from(ext.skills.keys()),
307
+ identityNames: Array.from(ext.identities.keys()),
268
308
  }));
269
309
  }
270
310
 
271
- /** Which tiers are used by loaded extensions */
272
- get usedTiers(): Set<ExtensionTier> {
273
- const tiers = new Set<ExtensionTier>();
274
- for (const ext of this.extensions) {
275
- for (const tier of ext.tiers) {
276
- tiers.add(tier);
277
- }
278
- }
279
- return tiers;
280
- }
281
-
282
311
  // ── Private Helpers ──
283
312
 
284
313
  private logHandlerError(extPath: string, event: string, err: unknown): void {