@dot-ai/core 0.5.2 → 0.8.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 +40 -0
- package/dist/boot-cache.d.ts.map +1 -0
- package/dist/boot-cache.js +72 -0
- package/dist/boot-cache.js.map +1 -0
- package/dist/capabilities.d.ts +35 -0
- package/dist/capabilities.d.ts.map +1 -0
- package/dist/capabilities.js +17 -0
- package/dist/capabilities.js.map +1 -0
- package/dist/config.d.ts +7 -23
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +131 -108
- package/dist/config.js.map +1 -1
- package/dist/extension-api.d.ts +65 -0
- package/dist/extension-api.d.ts.map +1 -0
- package/dist/extension-api.js +2 -0
- package/dist/extension-api.js.map +1 -0
- package/dist/extension-loader.d.ts +19 -0
- package/dist/extension-loader.d.ts.map +1 -0
- package/dist/extension-loader.js +113 -0
- package/dist/extension-loader.js.map +1 -0
- package/dist/extension-runner.d.ts +62 -0
- package/dist/extension-runner.d.ts.map +1 -0
- package/dist/extension-runner.js +260 -0
- package/dist/extension-runner.js.map +1 -0
- package/dist/extension-types.d.ts +312 -0
- package/dist/extension-types.d.ts.map +1 -0
- package/dist/extension-types.js +89 -0
- package/dist/extension-types.js.map +1 -0
- package/dist/format.d.ts +13 -1
- package/dist/format.d.ts.map +1 -1
- package/dist/format.js +131 -15
- package/dist/format.js.map +1 -1
- package/dist/format.spec.d.ts +2 -0
- package/dist/format.spec.d.ts.map +1 -0
- package/dist/format.spec.js +140 -0
- package/dist/format.spec.js.map +1 -0
- package/dist/index.d.ts +21 -14
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +21 -14
- package/dist/index.js.map +1 -1
- package/dist/logger.d.ts +1 -1
- package/dist/logger.d.ts.map +1 -1
- package/dist/package-manager.d.ts +30 -0
- package/dist/package-manager.d.ts.map +1 -0
- package/dist/package-manager.js +91 -0
- package/dist/package-manager.js.map +1 -0
- package/dist/runtime.d.ts +119 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +441 -0
- package/dist/runtime.js.map +1 -0
- package/dist/types.d.ts +29 -10
- package/dist/types.d.ts.map +1 -1
- package/package.json +4 -1
- package/src/__tests__/capabilities.test.ts +72 -0
- package/src/__tests__/config.test.ts +22 -120
- package/src/__tests__/extension-loader.test.ts +84 -0
- package/src/__tests__/extension-runner.test.ts +228 -0
- package/src/__tests__/fixtures/extensions/ctx-aware.js +26 -0
- package/src/__tests__/fixtures/extensions/security-gate.js +20 -0
- package/src/__tests__/fixtures/extensions/session-analytics.js +28 -0
- package/src/__tests__/fixtures/extensions/smart-context.js +10 -0
- package/src/__tests__/format.test.ts +207 -2
- package/src/__tests__/runtime.test.ts +141 -0
- package/src/boot-cache.ts +104 -0
- package/src/capabilities.ts +49 -0
- package/src/config.ts +131 -133
- package/src/extension-api.ts +99 -0
- package/src/extension-loader.ts +127 -0
- package/src/extension-runner.ts +297 -0
- package/src/extension-types.ts +416 -0
- package/src/format.spec.ts +175 -0
- package/src/format.test.ts +218 -0
- package/src/format.ts +140 -16
- package/src/index.ts +68 -30
- package/src/logger.ts +1 -1
- package/src/package-manager.ts +119 -0
- package/src/runtime.ts +562 -0
- package/src/types.ts +36 -14
- package/tsconfig.json +1 -1
- package/tsconfig.tsbuildinfo +1 -1
- package/.ai/memory/2026-03-04.md +0 -2
- package/.ai/tasks.json +0 -7
- package/dist/__tests__/config.test.d.ts +0 -2
- package/dist/__tests__/config.test.d.ts.map +0 -1
- package/dist/__tests__/config.test.js +0 -128
- package/dist/__tests__/config.test.js.map +0 -1
- package/dist/__tests__/e2e.test.d.ts +0 -2
- package/dist/__tests__/e2e.test.d.ts.map +0 -1
- package/dist/__tests__/e2e.test.js +0 -211
- package/dist/__tests__/e2e.test.js.map +0 -1
- package/dist/__tests__/engine.test.d.ts +0 -2
- package/dist/__tests__/engine.test.d.ts.map +0 -1
- package/dist/__tests__/engine.test.js +0 -271
- package/dist/__tests__/engine.test.js.map +0 -1
- package/dist/__tests__/format.test.d.ts +0 -2
- package/dist/__tests__/format.test.d.ts.map +0 -1
- package/dist/__tests__/format.test.js +0 -200
- package/dist/__tests__/format.test.js.map +0 -1
- package/dist/__tests__/labels.test.d.ts +0 -2
- package/dist/__tests__/labels.test.d.ts.map +0 -1
- package/dist/__tests__/labels.test.js +0 -82
- package/dist/__tests__/labels.test.js.map +0 -1
- package/dist/__tests__/loader.test.d.ts +0 -2
- package/dist/__tests__/loader.test.d.ts.map +0 -1
- package/dist/__tests__/loader.test.js +0 -161
- package/dist/__tests__/loader.test.js.map +0 -1
- package/dist/__tests__/logger.test.d.ts +0 -2
- package/dist/__tests__/logger.test.d.ts.map +0 -1
- package/dist/__tests__/logger.test.js +0 -95
- package/dist/__tests__/logger.test.js.map +0 -1
- package/dist/__tests__/nodes.test.d.ts +0 -2
- package/dist/__tests__/nodes.test.d.ts.map +0 -1
- package/dist/__tests__/nodes.test.js +0 -83
- package/dist/__tests__/nodes.test.js.map +0 -1
- package/dist/contracts.d.ts +0 -56
- package/dist/contracts.d.ts.map +0 -1
- package/dist/contracts.js +0 -2
- package/dist/contracts.js.map +0 -1
- package/dist/engine.d.ts +0 -38
- package/dist/engine.d.ts.map +0 -1
- package/dist/engine.js +0 -88
- package/dist/engine.js.map +0 -1
- package/dist/loader.d.ts +0 -26
- package/dist/loader.d.ts.map +0 -1
- package/dist/loader.js +0 -120
- package/dist/loader.js.map +0 -1
- package/src/__tests__/e2e.test.ts +0 -257
- package/src/__tests__/engine.test.ts +0 -305
- package/src/__tests__/loader.test.ts +0 -191
- package/src/contracts.ts +0 -71
- package/src/engine.ts +0 -145
- package/src/loader.ts +0 -152
package/src/config.ts
CHANGED
|
@@ -1,178 +1,176 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
-
import type { DotAiConfig,
|
|
4
|
-
import { discoverNodes, parseScanDirs } from './nodes.js';
|
|
3
|
+
import type { DotAiConfig, ExtensionsConfig } from './types.js';
|
|
5
4
|
|
|
6
5
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* Load settings.json (Pi-compatible format).
|
|
7
|
+
*
|
|
8
|
+
* Format:
|
|
9
|
+
* {
|
|
10
|
+
* "packages": ["npm:@dot-ai/ext-cockpit@1.0.0"],
|
|
11
|
+
* "extensions": [".ai/extensions/custom.ts"],
|
|
12
|
+
* "debug": { "logPath": "..." },
|
|
13
|
+
* "workspace": { "scanDirs": "..." }
|
|
14
|
+
* }
|
|
15
|
+
*/
|
|
16
|
+
async function loadSettingsJson(workspaceRoot: string): Promise<DotAiConfig | null> {
|
|
17
|
+
const settingsPath = join(workspaceRoot, '.ai', 'settings.json');
|
|
18
|
+
try {
|
|
19
|
+
const raw = await readFile(settingsPath, 'utf-8');
|
|
20
|
+
const json = JSON.parse(raw);
|
|
21
|
+
return settingsJsonToConfig(json);
|
|
22
|
+
} catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Convert Pi-compatible settings.json to DotAiConfig.
|
|
9
29
|
*/
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
if (
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
30
|
+
function settingsJsonToConfig(json: Record<string, unknown>): DotAiConfig {
|
|
31
|
+
const config: DotAiConfig = {};
|
|
32
|
+
|
|
33
|
+
// Extensions: string[] of local paths
|
|
34
|
+
const extensions = json['extensions'];
|
|
35
|
+
const packages = json['packages'];
|
|
36
|
+
|
|
37
|
+
if ((extensions && Array.isArray(extensions)) || (packages && Array.isArray(packages))) {
|
|
38
|
+
config.extensions = {};
|
|
39
|
+
if (extensions && Array.isArray(extensions)) {
|
|
40
|
+
config.extensions.paths = extensions.filter((e): e is string => typeof e === 'string');
|
|
41
|
+
}
|
|
42
|
+
if (packages && Array.isArray(packages)) {
|
|
43
|
+
config.extensions.packages = packages.filter((p): p is string => typeof p === 'string');
|
|
24
44
|
}
|
|
25
45
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
46
|
+
|
|
47
|
+
// Debug section
|
|
48
|
+
if (json['debug'] && typeof json['debug'] === 'object') {
|
|
49
|
+
const debug = json['debug'] as Record<string, unknown>;
|
|
50
|
+
config.debug = {};
|
|
51
|
+
if (typeof debug['logPath'] === 'string') config.debug.logPath = debug['logPath'];
|
|
29
52
|
}
|
|
30
|
-
|
|
31
|
-
|
|
53
|
+
|
|
54
|
+
// Workspace section
|
|
55
|
+
if (json['workspace'] && typeof json['workspace'] === 'object') {
|
|
56
|
+
const ws = json['workspace'] as Record<string, unknown>;
|
|
57
|
+
config.workspace = {};
|
|
58
|
+
if (typeof ws['scanDirs'] === 'string') config.workspace.scanDirs = ws['scanDirs'];
|
|
32
59
|
}
|
|
33
|
-
|
|
60
|
+
|
|
61
|
+
return config;
|
|
34
62
|
}
|
|
35
63
|
|
|
36
64
|
/**
|
|
37
|
-
* Load
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
* Uses a minimal YAML parser (key: value pairs + nested objects).
|
|
41
|
-
* No dependency on yaml package.
|
|
65
|
+
* Load config from workspace root.
|
|
66
|
+
* Tries settings.json first, falls back to empty config.
|
|
42
67
|
*/
|
|
43
68
|
export async function loadConfig(workspaceRoot: string): Promise<DotAiConfig> {
|
|
44
|
-
const
|
|
69
|
+
const settingsConfig = await loadSettingsJson(workspaceRoot);
|
|
70
|
+
if (settingsConfig) return settingsConfig;
|
|
71
|
+
return {};
|
|
72
|
+
}
|
|
45
73
|
|
|
74
|
+
/**
|
|
75
|
+
* Migrate dot-ai.yml to settings.json.
|
|
76
|
+
* Reads the existing YAML config and writes a settings.json equivalent.
|
|
77
|
+
* Returns the path of the written file, or null if no YAML config exists.
|
|
78
|
+
*/
|
|
79
|
+
export async function migrateConfig(workspaceRoot: string): Promise<string | null> {
|
|
80
|
+
const ymlPath = join(workspaceRoot, '.ai', 'dot-ai.yml');
|
|
46
81
|
let raw: string;
|
|
47
82
|
try {
|
|
48
|
-
raw = await readFile(
|
|
83
|
+
raw = await readFile(ymlPath, 'utf-8');
|
|
49
84
|
} catch {
|
|
50
|
-
// No
|
|
51
|
-
return {};
|
|
85
|
+
return null; // No YAML config to migrate
|
|
52
86
|
}
|
|
53
87
|
|
|
54
|
-
|
|
88
|
+
// Extract extensions section from YAML if present
|
|
89
|
+
const config: DotAiConfig = {};
|
|
90
|
+
const extensions = parseExtensionsFromYaml(raw);
|
|
91
|
+
if (extensions) {
|
|
92
|
+
config.extensions = extensions;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const settings = configToSettingsJson(config);
|
|
96
|
+
const settingsPath = join(workspaceRoot, '.ai', 'settings.json');
|
|
97
|
+
const { writeFile: wf } = await import('node:fs/promises');
|
|
98
|
+
await wf(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
|
|
99
|
+
return settingsPath;
|
|
55
100
|
}
|
|
56
101
|
|
|
57
102
|
/**
|
|
58
|
-
*
|
|
59
|
-
* Any missing provider gets the built-in file-based default.
|
|
103
|
+
* Convert DotAiConfig to Pi-compatible settings.json format.
|
|
60
104
|
*/
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
skills: ProviderConfig;
|
|
64
|
-
identity: ProviderConfig;
|
|
65
|
-
routing: ProviderConfig;
|
|
66
|
-
tasks: ProviderConfig;
|
|
67
|
-
tools: ProviderConfig;
|
|
68
|
-
debug?: import('./types.js').DebugConfig;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export function resolveConfig(config: DotAiConfig): ResolvedConfig {
|
|
72
|
-
return {
|
|
73
|
-
memory: config.memory ?? { use: '@dot-ai/provider-file-memory' },
|
|
74
|
-
skills: config.skills ?? { use: '@dot-ai/provider-file-skills' },
|
|
75
|
-
identity: config.identity ?? { use: '@dot-ai/provider-file-identity' },
|
|
76
|
-
routing: config.routing ?? { use: '@dot-ai/provider-rules-routing' },
|
|
77
|
-
tasks: config.tasks ?? { use: '@dot-ai/provider-file-tasks' },
|
|
78
|
-
tools: config.tools ?? { use: '@dot-ai/provider-file-tools' },
|
|
79
|
-
debug: config.debug,
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// ── Minimal YAML parser ─────────────────────────────────────────────────────
|
|
105
|
+
function configToSettingsJson(config: DotAiConfig): Record<string, unknown> {
|
|
106
|
+
const settings: Record<string, unknown> = {};
|
|
84
107
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function stripQuotes(s: string): string {
|
|
90
|
-
if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) {
|
|
91
|
-
return s.slice(1, -1);
|
|
108
|
+
if (config.extensions?.paths?.length) {
|
|
109
|
+
settings['extensions'] = config.extensions.paths;
|
|
92
110
|
}
|
|
93
|
-
|
|
111
|
+
if (config.extensions?.packages?.length) {
|
|
112
|
+
settings['packages'] = config.extensions.packages;
|
|
113
|
+
}
|
|
114
|
+
if (config.debug) settings['debug'] = config.debug;
|
|
115
|
+
if (config.workspace) settings['workspace'] = config.workspace;
|
|
116
|
+
|
|
117
|
+
return settings;
|
|
94
118
|
}
|
|
95
119
|
|
|
96
|
-
|
|
120
|
+
/**
|
|
121
|
+
* Parse extensions section from legacy YAML config.
|
|
122
|
+
*/
|
|
123
|
+
function parseExtensionsFromYaml(raw: string): ExtensionsConfig | null {
|
|
124
|
+
const extensions: ExtensionsConfig = {};
|
|
97
125
|
const lines = raw.split('\n');
|
|
98
|
-
|
|
99
|
-
let
|
|
126
|
+
let inExtensions = false;
|
|
127
|
+
let currentKey: 'paths' | 'packages' | null = null;
|
|
100
128
|
|
|
101
129
|
for (const line of lines) {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
// Top-level key (no indent)
|
|
106
|
-
const topMatch = line.match(/^(\w+):$/);
|
|
107
|
-
if (topMatch) {
|
|
108
|
-
currentSection = topMatch[1];
|
|
109
|
-
result[currentSection] = {};
|
|
130
|
+
if (line.match(/^extensions:$/)) {
|
|
131
|
+
inExtensions = true;
|
|
110
132
|
continue;
|
|
111
133
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
if (
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
134
|
+
if (inExtensions && line.match(/^\w+:/) && !line.match(/^extensions:/)) {
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
if (!inExtensions) continue;
|
|
138
|
+
|
|
139
|
+
const inlineArrayMatch = line.match(/^\s{2}(\w+):\s*\[(.+)\]$/);
|
|
140
|
+
if (inlineArrayMatch) {
|
|
141
|
+
const key = inlineArrayMatch[1] as 'paths' | 'packages';
|
|
142
|
+
const items = inlineArrayMatch[2]
|
|
143
|
+
.split(',')
|
|
144
|
+
.map(s => stripQuotes(s.trim()))
|
|
145
|
+
.filter(s => s.length > 0);
|
|
146
|
+
extensions[key] = items;
|
|
147
|
+
currentKey = null;
|
|
123
148
|
continue;
|
|
124
149
|
}
|
|
125
150
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
section['with'] = {};
|
|
132
|
-
}
|
|
133
|
-
let value = stripQuotes(deepMatch[2].trim());
|
|
134
|
-
value = value.replace(/\$\{(\w+)\}/g, (_, name: string) => process.env[name] ?? '');
|
|
135
|
-
(section['with'] as YamlNode)[deepMatch[1]] = value;
|
|
151
|
+
const keyMatch = line.match(/^\s{2}(\w+):$/);
|
|
152
|
+
if (keyMatch) {
|
|
153
|
+
currentKey = keyMatch[1] as 'paths' | 'packages';
|
|
154
|
+
extensions[currentKey] = [];
|
|
155
|
+
continue;
|
|
136
156
|
}
|
|
137
|
-
}
|
|
138
157
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
for (const key of providerKeys) {
|
|
144
|
-
const section = result[key];
|
|
145
|
-
if (section && typeof section === 'object') {
|
|
146
|
-
const node = section as YamlNode;
|
|
147
|
-
const providerConfig: ProviderConfig = {
|
|
148
|
-
use: typeof node['use'] === 'string' ? node['use'] : '',
|
|
149
|
-
};
|
|
150
|
-
if (node['with'] && typeof node['with'] === 'object') {
|
|
151
|
-
providerConfig.with = node['with'] as Record<string, unknown>;
|
|
152
|
-
}
|
|
153
|
-
config[key] = providerConfig;
|
|
158
|
+
const listItemMatch = line.match(/^\s{4}-\s*(.+)$/);
|
|
159
|
+
if (listItemMatch && currentKey) {
|
|
160
|
+
if (!extensions[currentKey]) extensions[currentKey] = [];
|
|
161
|
+
extensions[currentKey]!.push(stripQuotes(listItemMatch[1].trim()));
|
|
154
162
|
}
|
|
155
163
|
}
|
|
156
164
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
if (debugSection && typeof debugSection === 'object') {
|
|
160
|
-
const node = debugSection as YamlNode;
|
|
161
|
-
config.debug = {};
|
|
162
|
-
if (typeof node['logPath'] === 'string') {
|
|
163
|
-
config.debug.logPath = node['logPath'];
|
|
164
|
-
}
|
|
165
|
+
if (extensions.paths?.length || extensions.packages?.length) {
|
|
166
|
+
return extensions;
|
|
165
167
|
}
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
166
170
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
const node = workspaceSection as YamlNode;
|
|
171
|
-
config.workspace = {};
|
|
172
|
-
if (typeof node['scanDirs'] === 'string') {
|
|
173
|
-
config.workspace.scanDirs = node['scanDirs'];
|
|
174
|
-
}
|
|
171
|
+
function stripQuotes(s: string): string {
|
|
172
|
+
if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) {
|
|
173
|
+
return s.slice(1, -1);
|
|
175
174
|
}
|
|
176
|
-
|
|
177
|
-
return config;
|
|
175
|
+
return s;
|
|
178
176
|
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ContextInjectEvent, ContextInjectResult,
|
|
3
|
+
ContextModifyEvent, ContextModifyResult,
|
|
4
|
+
ToolCallEvent, ToolCallResult,
|
|
5
|
+
ToolResultEvent,
|
|
6
|
+
AgentEndEvent,
|
|
7
|
+
ToolDefinition,
|
|
8
|
+
ExtensionContext,
|
|
9
|
+
ResourcesDiscoverResult,
|
|
10
|
+
LabelExtractEvent,
|
|
11
|
+
ContextEnrichEvent, ContextEnrichResult,
|
|
12
|
+
RouteEvent, RouteResult,
|
|
13
|
+
InputEvent, InputResult,
|
|
14
|
+
CommandDefinition,
|
|
15
|
+
} from './extension-types.js';
|
|
16
|
+
import type { Label } from './types.js';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* v6 Extension Context — passed as second argument to every event handler.
|
|
20
|
+
* Extends the base ExtensionContext with labels and optional agent capabilities.
|
|
21
|
+
*/
|
|
22
|
+
export interface ExtensionContextV6 extends ExtensionContext {
|
|
23
|
+
/** Current prompt labels (available after label_extract) */
|
|
24
|
+
labels: Label[];
|
|
25
|
+
|
|
26
|
+
/** Agent capabilities (adapter-provided, may be undefined) */
|
|
27
|
+
agent?: {
|
|
28
|
+
abort(): void;
|
|
29
|
+
getContextUsage(): { tokens: number; percent: number } | undefined;
|
|
30
|
+
getSystemPrompt(): string;
|
|
31
|
+
[key: string]: unknown;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Extension API — passed to extension factory functions.
|
|
37
|
+
* Pi-compatible: same on(event) + registerTool() + registerCommand() pattern.
|
|
38
|
+
*/
|
|
39
|
+
export interface ExtensionAPI {
|
|
40
|
+
// ── Event subscription ──
|
|
41
|
+
|
|
42
|
+
/** Resource discovery: extensions declare resources and contribute labels */
|
|
43
|
+
on(event: 'resources_discover', handler: (e: undefined, ctx: ExtensionContextV6) => Promise<ResourcesDiscoverResult | void>): void;
|
|
44
|
+
/** Label extraction: extensions can add custom labels (chain-transform) */
|
|
45
|
+
on(event: 'label_extract', handler: (e: LabelExtractEvent, ctx: ExtensionContextV6) => Promise<Label[] | void>): void;
|
|
46
|
+
/** Context enrichment: extensions return sections for context injection */
|
|
47
|
+
on(event: 'context_enrich', handler: (e: ContextEnrichEvent, ctx: ExtensionContextV6) => Promise<ContextEnrichResult | void>): void;
|
|
48
|
+
/** Model routing: first result wins */
|
|
49
|
+
on(event: 'route', handler: (e: RouteEvent, ctx: ExtensionContextV6) => Promise<RouteResult | void>): void;
|
|
50
|
+
/** Input transformation: extensions can rewrite user input */
|
|
51
|
+
on(event: 'input', handler: (e: InputEvent, ctx: ExtensionContextV6) => Promise<InputResult | void>): void;
|
|
52
|
+
/** Tool call interception: fired before tool execution, can block */
|
|
53
|
+
on(event: 'tool_call', handler: (e: ToolCallEvent, ctx: ExtensionContextV6) => Promise<ToolCallResult | void>): void;
|
|
54
|
+
/** Tool result observation: fired after tool execution */
|
|
55
|
+
on(event: 'tool_result', handler: (e: ToolResultEvent, ctx: ExtensionContextV6) => Promise<void>): void;
|
|
56
|
+
|
|
57
|
+
// ── Lifecycle events ──
|
|
58
|
+
|
|
59
|
+
on(event: 'session_start', handler: (e: undefined, ctx: ExtensionContextV6) => Promise<void>): void;
|
|
60
|
+
on(event: 'session_end', handler: (e: undefined, ctx: ExtensionContextV6) => Promise<void>): void;
|
|
61
|
+
on(event: 'agent_start', handler: (e: undefined, ctx: ExtensionContextV6) => Promise<void>): void;
|
|
62
|
+
on(event: 'agent_end', handler: (e: AgentEndEvent, ctx: ExtensionContextV6) => Promise<void>): void;
|
|
63
|
+
on(event: 'turn_start', handler: (e: undefined, ctx: ExtensionContextV6) => Promise<void>): void;
|
|
64
|
+
on(event: 'turn_end', handler: (e: undefined, ctx: ExtensionContextV6) => Promise<void>): void;
|
|
65
|
+
|
|
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
|
+
// ── Catch-all for custom/Pi-specific events ──
|
|
74
|
+
|
|
75
|
+
on(event: string, handler: (e: any, ctx: ExtensionContextV6) => Promise<any>): void;
|
|
76
|
+
|
|
77
|
+
// ── Capability registration ──
|
|
78
|
+
|
|
79
|
+
/** Register a tool that the agent can invoke */
|
|
80
|
+
registerTool(tool: ToolDefinition): void;
|
|
81
|
+
/** Register a command (slash command, etc.) */
|
|
82
|
+
registerCommand(command: CommandDefinition): void;
|
|
83
|
+
|
|
84
|
+
// ── Inter-extension communication ──
|
|
85
|
+
|
|
86
|
+
events: {
|
|
87
|
+
on(event: string, handler: (...args: unknown[]) => void): void;
|
|
88
|
+
off(event: string, handler: (...args: unknown[]) => void): void;
|
|
89
|
+
emit(event: string, ...args: unknown[]): void;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// ── Extension config ──
|
|
93
|
+
|
|
94
|
+
/** Extension configuration (from extension-specific config files, env vars, etc.) */
|
|
95
|
+
config: Record<string, unknown>;
|
|
96
|
+
|
|
97
|
+
/** Workspace root directory (contains .ai/) */
|
|
98
|
+
workspaceRoot: string;
|
|
99
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { readdir, readFile, stat } from 'node:fs/promises';
|
|
2
|
+
import { join, resolve } from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
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';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Discover extension file paths from configured locations.
|
|
11
|
+
*/
|
|
12
|
+
export async function discoverExtensions(
|
|
13
|
+
workspaceRoot: string,
|
|
14
|
+
config?: ExtensionsConfig,
|
|
15
|
+
): Promise<string[]> {
|
|
16
|
+
const paths = new Set<string>();
|
|
17
|
+
|
|
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
|
+
];
|
|
24
|
+
|
|
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 */ }
|
|
41
|
+
}
|
|
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 */ }
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
} catch { /* directory doesn't exist — skip */ }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Also resolve npm packages from config
|
|
60
|
+
if (config?.packages) {
|
|
61
|
+
for (const pkg of config.packages) {
|
|
62
|
+
try {
|
|
63
|
+
const { createRequire } = await import('node:module');
|
|
64
|
+
const require = createRequire(join(workspaceRoot, 'package.json'));
|
|
65
|
+
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
|
+
}
|
|
74
|
+
}
|
|
75
|
+
} catch { /* package not found */ }
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return Array.from(paths);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Create a v6 ExtensionAPI instance that collects registrations into a LoadedExtension.
|
|
84
|
+
* Extensions communicate via events only — no provider access.
|
|
85
|
+
*/
|
|
86
|
+
export function createV6CollectorAPI(
|
|
87
|
+
extensionPath: string,
|
|
88
|
+
config: Record<string, unknown>,
|
|
89
|
+
eventBus?: { on: (event: string, handler: (...args: unknown[]) => void) => void; emit: (event: string, ...args: unknown[]) => void },
|
|
90
|
+
workspaceRoot?: string,
|
|
91
|
+
): { api: ExtensionAPI; extension: LoadedExtension } {
|
|
92
|
+
const extension: LoadedExtension = {
|
|
93
|
+
path: extensionPath,
|
|
94
|
+
handlers: new Map(),
|
|
95
|
+
tools: new Map(),
|
|
96
|
+
commands: new Map(),
|
|
97
|
+
tiers: new Set(),
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const api: ExtensionAPI = {
|
|
101
|
+
on(event: string, handler: Function) {
|
|
102
|
+
if (!extension.handlers.has(event)) {
|
|
103
|
+
extension.handlers.set(event, []);
|
|
104
|
+
}
|
|
105
|
+
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
|
+
},
|
|
112
|
+
registerTool(tool: ToolDefinition) {
|
|
113
|
+
extension.tools.set(tool.name, tool);
|
|
114
|
+
},
|
|
115
|
+
registerCommand(command: CommandDefinition) {
|
|
116
|
+
extension.commands.set(command.name, command);
|
|
117
|
+
},
|
|
118
|
+
events: eventBus ?? {
|
|
119
|
+
on: () => {},
|
|
120
|
+
emit: () => {},
|
|
121
|
+
},
|
|
122
|
+
config,
|
|
123
|
+
workspaceRoot: workspaceRoot ?? process.cwd(),
|
|
124
|
+
} as unknown as ExtensionAPI;
|
|
125
|
+
|
|
126
|
+
return { api, extension };
|
|
127
|
+
}
|