@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.
- package/dist/boot-cache.d.ts +1 -1
- package/dist/boot-cache.d.ts.map +1 -1
- package/dist/extension-api.d.ts +10 -9
- package/dist/extension-api.d.ts.map +1 -1
- package/dist/extension-loader.d.ts +9 -2
- package/dist/extension-loader.d.ts.map +1 -1
- package/dist/extension-loader.js +179 -55
- package/dist/extension-loader.js.map +1 -1
- package/dist/extension-runner.d.ts +9 -4
- package/dist/extension-runner.d.ts.map +1 -1
- package/dist/extension-runner.js +34 -13
- package/dist/extension-runner.js.map +1 -1
- package/dist/extension-types.d.ts +15 -115
- package/dist/extension-types.d.ts.map +1 -1
- package/dist/extension-types.js +1 -88
- package/dist/extension-types.js.map +1 -1
- package/dist/format.d.ts +21 -0
- package/dist/format.d.ts.map +1 -1
- package/dist/format.js +74 -0
- package/dist/format.js.map +1 -1
- package/dist/index.d.ts +6 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -4
- package/dist/index.js.map +1 -1
- package/dist/package-manager.d.ts +28 -5
- package/dist/package-manager.d.ts.map +1 -1
- package/dist/package-manager.js +126 -20
- package/dist/package-manager.js.map +1 -1
- package/dist/runtime.d.ts +31 -43
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +79 -177
- package/dist/runtime.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/extension-loader.test.ts +154 -11
- package/src/__tests__/extension-runner.test.ts +245 -32
- package/src/__tests__/fixtures/extensions/ctx-aware.js +12 -3
- package/src/__tests__/fixtures/extensions/smart-context.js +12 -3
- package/src/__tests__/format.test.ts +178 -1
- package/src/__tests__/package-manager.test.ts +237 -0
- package/src/__tests__/runtime.test.ts +38 -10
- package/src/boot-cache.ts +1 -1
- package/src/extension-api.ts +10 -15
- package/src/extension-loader.ts +187 -57
- package/src/extension-runner.ts +44 -15
- package/src/extension-types.ts +26 -195
- package/src/format.ts +100 -0
- package/src/index.ts +5 -13
- package/src/package-manager.ts +146 -23
- package/src/runtime.ts +96 -222
- package/tsconfig.tsbuildinfo +1 -1
package/src/extension-loader.ts
CHANGED
|
@@ -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
|
|
6
|
-
import {
|
|
7
|
-
|
|
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
|
-
*
|
|
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
|
|
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
|
-
//
|
|
19
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
43
|
-
|
|
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
|
-
}
|
|
179
|
+
}
|
|
57
180
|
}
|
|
58
181
|
|
|
59
|
-
//
|
|
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
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
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
|
|
199
|
+
return allPaths;
|
|
80
200
|
}
|
|
81
201
|
|
|
82
202
|
/**
|
|
83
|
-
* Create
|
|
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
|
-
|
|
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: () => {},
|
package/src/extension-runner.ts
CHANGED
|
@@ -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
|
-
// ──
|
|
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`, `
|
|
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
|
-
|
|
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 {
|