@agents-ensemble/core 0.6.5 → 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.
- package/dist/conductor/conductor-agent.d.ts +50 -14
- package/dist/conductor/conductor-agent.d.ts.map +1 -1
- package/dist/conductor/conductor-agent.js +1 -96
- package/dist/conductor/conductor-agent.js.map +1 -1
- package/dist/conductor/conductor-send-reconnect.d.ts +10 -6
- package/dist/conductor/conductor-send-reconnect.d.ts.map +1 -1
- package/dist/conductor/conductor-send-reconnect.js +6 -4
- package/dist/conductor/conductor-send-reconnect.js.map +1 -1
- package/dist/conductor/conductor-session-driver.d.ts +2 -4
- package/dist/conductor/conductor-session-driver.d.ts.map +1 -1
- package/dist/conductor/conductor-session-driver.js +29 -28
- package/dist/conductor/conductor-session-driver.js.map +1 -1
- package/dist/conductor/conductor-session.d.ts +5 -1
- package/dist/conductor/conductor-session.d.ts.map +1 -1
- package/dist/conductor/conductor-session.js +77 -28
- package/dist/conductor/conductor-session.js.map +1 -1
- package/dist/conductor/conductor-tool-pi-adapter.d.ts +13 -0
- package/dist/conductor/conductor-tool-pi-adapter.d.ts.map +1 -0
- package/dist/conductor/conductor-tool-pi-adapter.js +30 -0
- package/dist/conductor/conductor-tool-pi-adapter.js.map +1 -0
- package/dist/conductor/conductor-tool-sdk-adapter.d.ts +6 -0
- package/dist/conductor/conductor-tool-sdk-adapter.d.ts.map +1 -0
- package/dist/conductor/conductor-tool-sdk-adapter.js +16 -0
- package/dist/conductor/conductor-tool-sdk-adapter.js.map +1 -0
- package/dist/conductor/conductor-tool.d.ts +35 -0
- package/dist/conductor/conductor-tool.d.ts.map +1 -0
- package/dist/conductor/conductor-tool.js +41 -0
- package/dist/conductor/conductor-tool.js.map +1 -0
- package/dist/conductor/cursor-sdk-conductor-agent.d.ts +18 -0
- package/dist/conductor/cursor-sdk-conductor-agent.d.ts.map +1 -0
- package/dist/conductor/cursor-sdk-conductor-agent.js +134 -0
- package/dist/conductor/cursor-sdk-conductor-agent.js.map +1 -0
- package/dist/conductor/index.d.ts +15 -3
- package/dist/conductor/index.d.ts.map +1 -1
- package/dist/conductor/index.js +8 -2
- package/dist/conductor/index.js.map +1 -1
- package/dist/conductor/pi-conductor-agent.d.ts +46 -0
- package/dist/conductor/pi-conductor-agent.d.ts.map +1 -0
- package/dist/conductor/pi-conductor-agent.js +487 -0
- package/dist/conductor/pi-conductor-agent.js.map +1 -0
- package/dist/conductor/pi-conductor-session.d.ts +20 -0
- package/dist/conductor/pi-conductor-session.d.ts.map +1 -0
- package/dist/conductor/pi-conductor-session.js +60 -0
- package/dist/conductor/pi-conductor-session.js.map +1 -0
- package/dist/conductor/pi-mcp-bridge.d.ts +65 -0
- package/dist/conductor/pi-mcp-bridge.d.ts.map +1 -0
- package/dist/conductor/pi-mcp-bridge.js +259 -0
- package/dist/conductor/pi-mcp-bridge.js.map +1 -0
- package/dist/conductor/pi-resource-loader.d.ts +145 -0
- package/dist/conductor/pi-resource-loader.d.ts.map +1 -0
- package/dist/conductor/pi-resource-loader.js +673 -0
- package/dist/conductor/pi-resource-loader.js.map +1 -0
- package/dist/config/defaults.d.ts +2 -1
- package/dist/config/defaults.d.ts.map +1 -1
- package/dist/config/defaults.js +2 -0
- package/dist/config/defaults.js.map +1 -1
- package/dist/config/index.d.ts +3 -3
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +2 -2
- package/dist/config/index.js.map +1 -1
- package/dist/config/parse-config.d.ts.map +1 -1
- package/dist/config/parse-config.js +29 -1
- package/dist/config/parse-config.js.map +1 -1
- package/dist/config/resolve-settings.d.ts +25 -1
- package/dist/config/resolve-settings.d.ts.map +1 -1
- package/dist/config/resolve-settings.js +38 -1
- package/dist/config/resolve-settings.js.map +1 -1
- package/dist/config/types.d.ts +11 -0
- package/dist/config/types.d.ts.map +1 -1
- package/dist/dispatch/prompt-worker-tool.d.ts +2 -2
- package/dist/dispatch/prompt-worker-tool.d.ts.map +1 -1
- package/dist/dispatch/prompt-worker-tool.js +1 -0
- package/dist/dispatch/prompt-worker-tool.js.map +1 -1
- package/dist/dispatch/session-usage-tool.d.ts +2 -2
- package/dist/dispatch/session-usage-tool.d.ts.map +1 -1
- package/dist/dispatch/session-usage-tool.js +2 -0
- package/dist/dispatch/session-usage-tool.js.map +1 -1
- package/dist/dispatch/set-dispatch-hold-tool.d.ts +2 -2
- package/dist/dispatch/set-dispatch-hold-tool.d.ts.map +1 -1
- package/dist/dispatch/set-dispatch-hold-tool.js +1 -0
- package/dist/dispatch/set-dispatch-hold-tool.js.map +1 -1
- package/dist/dispatch/worker-status-tool.d.ts +2 -2
- package/dist/dispatch/worker-status-tool.d.ts.map +1 -1
- package/dist/dispatch/worker-status-tool.js +2 -0
- package/dist/dispatch/worker-status-tool.js.map +1 -1
- package/dist/dispatch/yaml-tool-result.d.ts +3 -9
- package/dist/dispatch/yaml-tool-result.d.ts.map +1 -1
- package/dist/dispatch/yaml-tool-result.js.map +1 -1
- package/dist/escalation/answer-open-question-tool.d.ts +2 -2
- package/dist/escalation/answer-open-question-tool.d.ts.map +1 -1
- package/dist/escalation/answer-open-question-tool.js +1 -0
- package/dist/escalation/answer-open-question-tool.js.map +1 -1
- package/dist/escalation/ask-human-tool.d.ts +2 -2
- package/dist/escalation/ask-human-tool.d.ts.map +1 -1
- package/dist/escalation/ask-human-tool.js +1 -0
- package/dist/escalation/ask-human-tool.js.map +1 -1
- package/dist/escalation/open-question-list-tools.d.ts +2 -2
- package/dist/escalation/open-question-list-tools.d.ts.map +1 -1
- package/dist/escalation/open-question-list-tools.js +2 -0
- package/dist/escalation/open-question-list-tools.js.map +1 -1
- package/dist/github/fetch-github-updates.d.ts +0 -6
- package/dist/github/fetch-github-updates.d.ts.map +1 -1
- package/dist/github/fetch-github-updates.js +74 -119
- package/dist/github/fetch-github-updates.js.map +1 -1
- package/dist/github/github-monitor-cursor.d.ts +24 -8
- package/dist/github/github-monitor-cursor.d.ts.map +1 -1
- package/dist/github/github-monitor-cursor.js +51 -17
- package/dist/github/github-monitor-cursor.js.map +1 -1
- package/dist/github/github-update-types.d.ts +1 -1
- package/dist/github/github-update-types.d.ts.map +1 -1
- package/dist/github/index.d.ts +1 -1
- package/dist/github/index.d.ts.map +1 -1
- package/dist/github/index.js.map +1 -1
- package/dist/github/register-github-watch-tool.d.ts +2 -2
- package/dist/github/register-github-watch-tool.d.ts.map +1 -1
- package/dist/github/register-github-watch-tool.js +1 -0
- package/dist/github/register-github-watch-tool.js.map +1 -1
- package/dist/mcp/index.d.ts +2 -3
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +1 -1
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/load-mcp-config.d.ts +29 -5
- package/dist/mcp/load-mcp-config.d.ts.map +1 -1
- package/dist/mcp/load-mcp-config.js +9 -2
- package/dist/mcp/load-mcp-config.js.map +1 -1
- package/dist/permission/resolve-permission-tool.d.ts +2 -2
- package/dist/permission/resolve-permission-tool.d.ts.map +1 -1
- package/dist/permission/resolve-permission-tool.js +1 -0
- package/dist/permission/resolve-permission-tool.js.map +1 -1
- package/dist/profile/index.d.ts +1 -1
- package/dist/profile/index.d.ts.map +1 -1
- package/dist/profile/index.js.map +1 -1
- package/dist/profile/load-profile.d.ts.map +1 -1
- package/dist/profile/load-profile.js +19 -0
- package/dist/profile/load-profile.js.map +1 -1
- package/dist/profile/types.d.ts +7 -0
- package/dist/profile/types.d.ts.map +1 -1
- package/dist/profile/types.js.map +1 -1
- package/dist/session/session-sidecar.d.ts +4 -0
- package/dist/session/session-sidecar.d.ts.map +1 -1
- package/dist/session/session-sidecar.js +16 -0
- package/dist/session/session-sidecar.js.map +1 -1
- package/dist/testing/mock-conductor-get-usage.d.ts +2 -1
- package/dist/testing/mock-conductor-get-usage.d.ts.map +1 -1
- package/dist/testing/mock-conductor-get-usage.js +1 -10
- package/dist/testing/mock-conductor-get-usage.js.map +1 -1
- package/dist/usage/enrich-session-usage-cost.d.ts +2 -2
- package/dist/usage/enrich-session-usage-cost.d.ts.map +1 -1
- package/dist/usage/session-usage-tracker.d.ts +2 -2
- package/dist/usage/session-usage-tracker.d.ts.map +1 -1
- package/dist/usage/session-usage-tracker.js +2 -2
- package/dist/usage/session-usage-tracker.js.map +1 -1
- package/dist/usage/types.d.ts +2 -2
- package/dist/usage/types.d.ts.map +1 -1
- package/dist/worktree/worktree.d.ts.map +1 -1
- package/dist/worktree/worktree.js +33 -16
- package/dist/worktree/worktree.js.map +1 -1
- package/package.json +4 -1
|
@@ -0,0 +1,673 @@
|
|
|
1
|
+
import { readdir, readFile } from 'node:fs/promises';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { basename, dirname, extname, isAbsolute, join, resolve } from 'node:path';
|
|
4
|
+
import { pathToFileURL } from 'node:url';
|
|
5
|
+
const EXTENSION_FILE_EXTENSIONS = new Set(['.cjs', '.js', '.mjs', '.ts']);
|
|
6
|
+
const RESOURCE_FILE_EXTENSIONS = new Set([
|
|
7
|
+
'.css',
|
|
8
|
+
'.json',
|
|
9
|
+
'.md',
|
|
10
|
+
'.mjs',
|
|
11
|
+
'.js',
|
|
12
|
+
'.ts',
|
|
13
|
+
]);
|
|
14
|
+
/**
|
|
15
|
+
* Resolve the two Pi resource roots used by the conductor.
|
|
16
|
+
*
|
|
17
|
+
* `agentDir` is the user root and defaults to `~/.ensemble/pi`; `projectDir`
|
|
18
|
+
* is the repository root and defaults to `<cwd>/.ensemble/pi`. The optional
|
|
19
|
+
* environment variable is retained for compatibility with Pi's launcher.
|
|
20
|
+
*/
|
|
21
|
+
export function resolvePiResourceRoots(options) {
|
|
22
|
+
const env = options.env ?? process.env;
|
|
23
|
+
const home = options.home ?? homedir();
|
|
24
|
+
return {
|
|
25
|
+
agentDir: resolveRootPath(options.pi?.agentDir ?? env.PI_CODING_AGENT_DIR, options.cwd, home, join(home, '.ensemble', 'pi')),
|
|
26
|
+
projectDir: resolveRootPath(options.pi?.projectDir, options.cwd, home, join(options.cwd, '.ensemble', 'pi')),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function resolveRootPath(configured, cwd, home, fallback) {
|
|
30
|
+
if (!configured)
|
|
31
|
+
return fallback;
|
|
32
|
+
if (configured === '~')
|
|
33
|
+
return home;
|
|
34
|
+
if (configured.startsWith('~/'))
|
|
35
|
+
return join(home, configured.slice(2));
|
|
36
|
+
return isAbsolute(configured) ? configured : resolve(cwd, configured);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Load Pi's standard file resources for a conductor.
|
|
40
|
+
*
|
|
41
|
+
* User resources are read first and project resources second. Settings/auth
|
|
42
|
+
* objects are deep-merged, project extension tools replace same-named user
|
|
43
|
+
* tools, and extension loading failures are isolated so a broken optional
|
|
44
|
+
* local extension cannot prevent the fixed harness tools from starting.
|
|
45
|
+
*
|
|
46
|
+
* The conductor intentionally depends on low-level `pi-agent-core` rather
|
|
47
|
+
* than the interactive `pi-coding-agent` package. This loader mirrors the
|
|
48
|
+
* coding-agent resource roots and extension registration boundary without
|
|
49
|
+
* adding Pi's built-in coding tools to the conductor.
|
|
50
|
+
*/
|
|
51
|
+
export async function loadPiResources(options) {
|
|
52
|
+
const roots = resolvePiResourceRoots(options);
|
|
53
|
+
const layers = [
|
|
54
|
+
await readResourceLayer(roots.agentDir),
|
|
55
|
+
await readResourceLayer(roots.projectDir),
|
|
56
|
+
];
|
|
57
|
+
let settings = {};
|
|
58
|
+
let auth = {};
|
|
59
|
+
for (const layer of layers) {
|
|
60
|
+
settings = mergeRecords(settings, layer.settings);
|
|
61
|
+
auth = mergeRecords(auth, layer.auth);
|
|
62
|
+
}
|
|
63
|
+
const extensionPaths = uniquePaths([
|
|
64
|
+
...layers.flatMap((layer) => layer.extensionPaths),
|
|
65
|
+
]);
|
|
66
|
+
const skillPaths = uniquePaths([
|
|
67
|
+
...layers.flatMap((layer) => layer.skillPaths),
|
|
68
|
+
]);
|
|
69
|
+
const promptPaths = uniquePaths([
|
|
70
|
+
...layers.flatMap((layer) => layer.promptPaths),
|
|
71
|
+
]);
|
|
72
|
+
const themePaths = uniquePaths([
|
|
73
|
+
...layers.flatMap((layer) => layer.themePaths),
|
|
74
|
+
]);
|
|
75
|
+
return {
|
|
76
|
+
roots,
|
|
77
|
+
settings,
|
|
78
|
+
auth,
|
|
79
|
+
authLayers: layers.map((layer) => layer.auth),
|
|
80
|
+
models: mergeModels(layers.map((layer) => layer.models)),
|
|
81
|
+
extensionTools: await loadExtensionTools(extensionPaths, options.cwd),
|
|
82
|
+
extensionPaths,
|
|
83
|
+
skillPaths,
|
|
84
|
+
promptPaths,
|
|
85
|
+
themePaths,
|
|
86
|
+
skills: await loadSkills(skillPaths),
|
|
87
|
+
prompts: await loadPromptTemplates(promptPaths),
|
|
88
|
+
themes: await loadThemes(themePaths),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
async function readResourceLayer(root) {
|
|
92
|
+
const settings = (await readOptionalJson(join(root, 'settings.json'))) ?? {};
|
|
93
|
+
return {
|
|
94
|
+
settings,
|
|
95
|
+
auth: (await readOptionalJson(join(root, 'auth.json'))) ?? {},
|
|
96
|
+
models: normalizeModelsFile((await readOptionalJson(join(root, 'models.json'))) ?? {}, join(root, 'models.json')),
|
|
97
|
+
extensionPaths: uniquePaths([
|
|
98
|
+
...(await discoverDirectoryResources(join(root, 'extensions'), true)),
|
|
99
|
+
...configuredResourcePaths(settings.extensions, root),
|
|
100
|
+
]),
|
|
101
|
+
skillPaths: uniquePaths([
|
|
102
|
+
...(await discoverDirectoryResources(join(root, 'skills'), false)),
|
|
103
|
+
...configuredResourcePaths(settings.skills, root),
|
|
104
|
+
]),
|
|
105
|
+
promptPaths: uniquePaths([
|
|
106
|
+
...(await discoverDirectoryResources(join(root, 'prompts'), false)),
|
|
107
|
+
...configuredResourcePaths(settings.prompts, root),
|
|
108
|
+
]),
|
|
109
|
+
themePaths: uniquePaths([
|
|
110
|
+
...(await discoverDirectoryResources(join(root, 'themes'), false)),
|
|
111
|
+
...configuredResourcePaths(settings.themes, root),
|
|
112
|
+
]),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
async function discoverDirectoryResources(directory, extensionsOnly) {
|
|
116
|
+
const entries = await readDirectory(directory);
|
|
117
|
+
const resources = [];
|
|
118
|
+
for (const entry of entries) {
|
|
119
|
+
const path = join(directory, entry.name);
|
|
120
|
+
if (entry.isDirectory()) {
|
|
121
|
+
if (extensionsOnly) {
|
|
122
|
+
const declared = await readPackageExtensionEntries(path);
|
|
123
|
+
if (declared.length > 0) {
|
|
124
|
+
resources.push(...declared);
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
const index = await firstExistingPath([
|
|
128
|
+
join(path, 'index.ts'),
|
|
129
|
+
join(path, 'index.js'),
|
|
130
|
+
]);
|
|
131
|
+
if (index)
|
|
132
|
+
resources.push(index);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
resources.push(path);
|
|
137
|
+
}
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
const extension = extname(entry.name).toLowerCase();
|
|
141
|
+
if (extension &&
|
|
142
|
+
(extensionsOnly
|
|
143
|
+
? EXTENSION_FILE_EXTENSIONS.has(extension)
|
|
144
|
+
: RESOURCE_FILE_EXTENSIONS.has(extension))) {
|
|
145
|
+
resources.push(path);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return resources.sort();
|
|
149
|
+
}
|
|
150
|
+
async function readPackageExtensionEntries(directory) {
|
|
151
|
+
const packageJson = await readOptionalJson(join(directory, 'package.json'));
|
|
152
|
+
const pi = packageJson?.pi;
|
|
153
|
+
if (!isRecord(pi) || !Array.isArray(pi.extensions))
|
|
154
|
+
return [];
|
|
155
|
+
return pi.extensions.flatMap((value) => {
|
|
156
|
+
if (typeof value !== 'string' || value.trim() === '')
|
|
157
|
+
return [];
|
|
158
|
+
return [resolve(directory, value.trim())];
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
async function firstExistingPath(paths) {
|
|
162
|
+
for (const path of paths) {
|
|
163
|
+
try {
|
|
164
|
+
await readFile(path);
|
|
165
|
+
return path;
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
if (!isFileNotFound(error))
|
|
169
|
+
throw error;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return undefined;
|
|
173
|
+
}
|
|
174
|
+
async function readDirectory(directory) {
|
|
175
|
+
try {
|
|
176
|
+
return await readdir(directory, { withFileTypes: true });
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
if (isPathUnavailable(error))
|
|
180
|
+
return [];
|
|
181
|
+
throw error;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
async function readOptionalJson(path) {
|
|
185
|
+
try {
|
|
186
|
+
const source = await readFile(path, 'utf8');
|
|
187
|
+
const value = JSON.parse(stripJsonComments(source));
|
|
188
|
+
if (!isRecord(value)) {
|
|
189
|
+
throw new Error(`Expected a JSON object in ${path}.`);
|
|
190
|
+
}
|
|
191
|
+
return value;
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
if (isFileNotFound(error))
|
|
195
|
+
return undefined;
|
|
196
|
+
if (error instanceof SyntaxError) {
|
|
197
|
+
throw new Error(`Invalid JSON in Pi configuration file ${path}.`);
|
|
198
|
+
}
|
|
199
|
+
throw error;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
function normalizeModelsFile(value, path) {
|
|
203
|
+
const providers = value.providers;
|
|
204
|
+
if (providers === undefined)
|
|
205
|
+
return { providers: {} };
|
|
206
|
+
if (!isRecord(providers)) {
|
|
207
|
+
throw new Error(`Expected a providers object in Pi models file ${path}.`);
|
|
208
|
+
}
|
|
209
|
+
for (const [provider, config] of Object.entries(providers)) {
|
|
210
|
+
if (!isRecord(config)) {
|
|
211
|
+
throw new Error(`Expected provider "${provider}" to be an object in Pi models file ${path}.`);
|
|
212
|
+
}
|
|
213
|
+
for (const key of ['baseUrl', 'apiKey', 'api']) {
|
|
214
|
+
if (config[key] !== undefined && typeof config[key] !== 'string') {
|
|
215
|
+
throw new Error(`Expected provider "${provider}" ${key} to be a string in Pi models file ${path}.`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
if (config.authHeader !== undefined && typeof config.authHeader !== 'boolean') {
|
|
219
|
+
throw new Error(`Expected provider "${provider}" authHeader to be a boolean in Pi models file ${path}.`);
|
|
220
|
+
}
|
|
221
|
+
if (config.models !== undefined && !Array.isArray(config.models)) {
|
|
222
|
+
throw new Error(`Expected provider "${provider}" models to be an array in Pi models file ${path}.`);
|
|
223
|
+
}
|
|
224
|
+
for (const model of config.models ?? []) {
|
|
225
|
+
if (!isRecord(model) || typeof model.id !== 'string' || model.id.trim() === '') {
|
|
226
|
+
throw new Error(`Expected provider "${provider}" models to contain an id in Pi models file ${path}.`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
if (config.modelOverrides !== undefined && !isRecord(config.modelOverrides)) {
|
|
230
|
+
throw new Error(`Expected provider "${provider}" modelOverrides to be an object in Pi models file ${path}.`);
|
|
231
|
+
}
|
|
232
|
+
for (const [modelId, override] of Object.entries(config.modelOverrides ?? {})) {
|
|
233
|
+
if (!isRecord(override)) {
|
|
234
|
+
throw new Error(`Expected provider "${provider}" model override "${modelId}" to be an object in Pi models file ${path}.`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return { providers: providers };
|
|
239
|
+
}
|
|
240
|
+
function mergeModels(layers) {
|
|
241
|
+
let merged = { providers: {} };
|
|
242
|
+
for (const layer of layers) {
|
|
243
|
+
merged = mergeRecords(merged, layer);
|
|
244
|
+
}
|
|
245
|
+
return normalizeModelsFile(merged, 'merged Pi models');
|
|
246
|
+
}
|
|
247
|
+
async function loadSkills(paths) {
|
|
248
|
+
const skills = new Map();
|
|
249
|
+
for (const path of paths) {
|
|
250
|
+
for (const filePath of await discoverSkillFiles(path)) {
|
|
251
|
+
const resource = await readSkill(filePath);
|
|
252
|
+
if (resource)
|
|
253
|
+
skills.set(resource.name, resource);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return [...skills.values()];
|
|
257
|
+
}
|
|
258
|
+
async function discoverSkillFiles(path) {
|
|
259
|
+
const directFile = await readableFile(path);
|
|
260
|
+
if (directFile && extname(path).toLowerCase() === '.md')
|
|
261
|
+
return [path];
|
|
262
|
+
const entries = await readDirectory(path);
|
|
263
|
+
if (entries.length === 0)
|
|
264
|
+
return [];
|
|
265
|
+
const skillFile = join(path, 'SKILL.md');
|
|
266
|
+
if (await readableFile(skillFile))
|
|
267
|
+
return [skillFile];
|
|
268
|
+
const files = [];
|
|
269
|
+
for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
|
|
270
|
+
if (entry.name.startsWith('.'))
|
|
271
|
+
continue;
|
|
272
|
+
const child = join(path, entry.name);
|
|
273
|
+
if (entry.isDirectory()) {
|
|
274
|
+
files.push(...(await discoverSkillFiles(child)));
|
|
275
|
+
}
|
|
276
|
+
else if (extname(entry.name).toLowerCase() === '.md') {
|
|
277
|
+
files.push(child);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return files;
|
|
281
|
+
}
|
|
282
|
+
async function readSkill(path) {
|
|
283
|
+
const source = await readOptionalText(path);
|
|
284
|
+
if (source === undefined)
|
|
285
|
+
return undefined;
|
|
286
|
+
const parsed = parseMarkdownResource(source);
|
|
287
|
+
const name = stringValue(parsed.frontmatter.name) ?? basename(dirname(path));
|
|
288
|
+
const description = stringValue(parsed.frontmatter.description) ?? firstNonEmptyLine(parsed.body) ?? name;
|
|
289
|
+
return {
|
|
290
|
+
name,
|
|
291
|
+
description,
|
|
292
|
+
filePath: path,
|
|
293
|
+
content: parsed.body,
|
|
294
|
+
disableModelInvocation: parsed.frontmatter['disable-model-invocation'] === true,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
async function loadPromptTemplates(paths) {
|
|
298
|
+
const prompts = new Map();
|
|
299
|
+
for (const filePath of await discoverFiles(paths, '.md', false)) {
|
|
300
|
+
const source = await readOptionalText(filePath);
|
|
301
|
+
if (source === undefined)
|
|
302
|
+
continue;
|
|
303
|
+
const parsed = parseMarkdownResource(source);
|
|
304
|
+
const name = basename(filePath, '.md');
|
|
305
|
+
const description = stringValue(parsed.frontmatter.description) ?? firstNonEmptyLine(parsed.body) ?? name;
|
|
306
|
+
const argumentHint = stringValue(parsed.frontmatter['argument-hint']);
|
|
307
|
+
prompts.set(name, {
|
|
308
|
+
name,
|
|
309
|
+
description,
|
|
310
|
+
...(argumentHint ? { argumentHint } : {}),
|
|
311
|
+
filePath,
|
|
312
|
+
content: parsed.body,
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
return [...prompts.values()];
|
|
316
|
+
}
|
|
317
|
+
async function loadThemes(paths) {
|
|
318
|
+
const themes = new Map();
|
|
319
|
+
for (const filePath of await discoverFiles(paths, '.json', false)) {
|
|
320
|
+
const value = await readOptionalJson(filePath);
|
|
321
|
+
if (!value)
|
|
322
|
+
continue;
|
|
323
|
+
const name = typeof value.name === 'string' ? value.name : basename(filePath, '.json');
|
|
324
|
+
themes.set(name, { name, filePath, value });
|
|
325
|
+
}
|
|
326
|
+
return [...themes.values()];
|
|
327
|
+
}
|
|
328
|
+
async function discoverFiles(paths, extension, recursive) {
|
|
329
|
+
const files = [];
|
|
330
|
+
for (const path of paths) {
|
|
331
|
+
const directFile = await readableFile(path);
|
|
332
|
+
if (directFile && extname(path).toLowerCase() === extension) {
|
|
333
|
+
files.push(path);
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
const entries = await readDirectory(path);
|
|
337
|
+
for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
|
|
338
|
+
if (entry.name.startsWith('.'))
|
|
339
|
+
continue;
|
|
340
|
+
const child = join(path, entry.name);
|
|
341
|
+
if (entry.isDirectory()) {
|
|
342
|
+
if (recursive)
|
|
343
|
+
files.push(...(await discoverFiles([child], extension, true)));
|
|
344
|
+
}
|
|
345
|
+
else if (extname(entry.name).toLowerCase() === extension) {
|
|
346
|
+
files.push(child);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
return files;
|
|
351
|
+
}
|
|
352
|
+
async function readableFile(path) {
|
|
353
|
+
try {
|
|
354
|
+
await readFile(path);
|
|
355
|
+
return true;
|
|
356
|
+
}
|
|
357
|
+
catch {
|
|
358
|
+
return false;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
async function readOptionalText(path) {
|
|
362
|
+
try {
|
|
363
|
+
return await readFile(path, 'utf8');
|
|
364
|
+
}
|
|
365
|
+
catch (error) {
|
|
366
|
+
if (isPathUnavailable(error))
|
|
367
|
+
return undefined;
|
|
368
|
+
throw error;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
function parseMarkdownResource(source) {
|
|
372
|
+
const lines = source.split(/\r?\n/);
|
|
373
|
+
if (lines[0]?.trim() !== '---')
|
|
374
|
+
return { frontmatter: {}, body: source.trim() };
|
|
375
|
+
const end = lines.findIndex((line, index) => index > 0 && line.trim() === '---');
|
|
376
|
+
if (end < 0)
|
|
377
|
+
return { frontmatter: {}, body: source.trim() };
|
|
378
|
+
const frontmatter = {};
|
|
379
|
+
for (const line of lines.slice(1, end)) {
|
|
380
|
+
const separator = line.indexOf(':');
|
|
381
|
+
if (separator < 0)
|
|
382
|
+
continue;
|
|
383
|
+
const key = line.slice(0, separator).trim();
|
|
384
|
+
const rawValue = line.slice(separator + 1).trim();
|
|
385
|
+
if (!key)
|
|
386
|
+
continue;
|
|
387
|
+
if (rawValue === 'true' || rawValue === 'false') {
|
|
388
|
+
frontmatter[key] = rawValue === 'true';
|
|
389
|
+
}
|
|
390
|
+
else {
|
|
391
|
+
frontmatter[key] = unquote(rawValue);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
return {
|
|
395
|
+
frontmatter,
|
|
396
|
+
body: lines.slice(end + 1).join('\n').trim(),
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
function unquote(value) {
|
|
400
|
+
if (value.length >= 2 &&
|
|
401
|
+
((value.startsWith('"') && value.endsWith('"')) ||
|
|
402
|
+
(value.startsWith("'") && value.endsWith("'")))) {
|
|
403
|
+
return value.slice(1, -1);
|
|
404
|
+
}
|
|
405
|
+
return value;
|
|
406
|
+
}
|
|
407
|
+
function stringValue(value) {
|
|
408
|
+
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
|
409
|
+
}
|
|
410
|
+
function firstNonEmptyLine(value) {
|
|
411
|
+
return value.split(/\r?\n/).find((line) => line.trim())?.trim();
|
|
412
|
+
}
|
|
413
|
+
/** Add usable Pi skills to the compiled conductor prompt. */
|
|
414
|
+
export function formatPiSkillsForPrompt(skills) {
|
|
415
|
+
const visible = skills.filter((skill) => !skill.disableModelInvocation);
|
|
416
|
+
if (visible.length === 0)
|
|
417
|
+
return '';
|
|
418
|
+
return [
|
|
419
|
+
'',
|
|
420
|
+
'',
|
|
421
|
+
'<pi_skills>',
|
|
422
|
+
...visible.flatMap((skill) => [
|
|
423
|
+
` <skill name="${escapeXml(skill.name)}" description="${escapeXml(skill.description)}" location="${escapeXml(skill.filePath)}">`,
|
|
424
|
+
skill.content,
|
|
425
|
+
' </skill>',
|
|
426
|
+
]),
|
|
427
|
+
'</pi_skills>',
|
|
428
|
+
].join('\n');
|
|
429
|
+
}
|
|
430
|
+
function escapeXml(value) {
|
|
431
|
+
return value
|
|
432
|
+
.replace(/&/g, '&')
|
|
433
|
+
.replace(/</g, '<')
|
|
434
|
+
.replace(/>/g, '>')
|
|
435
|
+
.replace(/"/g, '"')
|
|
436
|
+
.replace(/'/g, ''');
|
|
437
|
+
}
|
|
438
|
+
/** Expand Pi prompt templates and explicit skill invocations for headless sends. */
|
|
439
|
+
export function expandPiResourcePrompt(prompt, resources) {
|
|
440
|
+
const match = /^\/([^\s]+)(?:\s+([\s\S]*))?$/.exec(prompt);
|
|
441
|
+
if (!match)
|
|
442
|
+
return prompt;
|
|
443
|
+
const name = match[1];
|
|
444
|
+
const args = splitPromptArguments(match[2] ?? '');
|
|
445
|
+
if (name.startsWith('skill:')) {
|
|
446
|
+
const skill = resources.skills.find((candidate) => candidate.name === name.slice('skill:'.length));
|
|
447
|
+
if (!skill)
|
|
448
|
+
return prompt;
|
|
449
|
+
return `${substitutePromptArguments(skill.content, args)}${args.length > 0 ? `\n\n${args.join(' ')}` : ''}`;
|
|
450
|
+
}
|
|
451
|
+
const template = resources.prompts.find((candidate) => candidate.name === name);
|
|
452
|
+
return template ? substitutePromptArguments(template.content, args) : prompt;
|
|
453
|
+
}
|
|
454
|
+
function splitPromptArguments(value) {
|
|
455
|
+
const args = [];
|
|
456
|
+
let current = '';
|
|
457
|
+
let quote;
|
|
458
|
+
for (const character of value) {
|
|
459
|
+
if (quote) {
|
|
460
|
+
if (character === quote)
|
|
461
|
+
quote = undefined;
|
|
462
|
+
else
|
|
463
|
+
current += character;
|
|
464
|
+
}
|
|
465
|
+
else if (character === '"' || character === "'") {
|
|
466
|
+
quote = character;
|
|
467
|
+
}
|
|
468
|
+
else if (/\s/.test(character)) {
|
|
469
|
+
if (current) {
|
|
470
|
+
args.push(current);
|
|
471
|
+
current = '';
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
else {
|
|
475
|
+
current += character;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
if (current)
|
|
479
|
+
args.push(current);
|
|
480
|
+
return args;
|
|
481
|
+
}
|
|
482
|
+
function substitutePromptArguments(content, args) {
|
|
483
|
+
const allArgs = args.join(' ');
|
|
484
|
+
return content.replace(/\$\{(\d+):-([^}]*)\}|\$\{@:(\d+)(?::(\d+))?\}|\$(ARGUMENTS|@|\d+)/g, (_match, defaultNumber, defaultValue, sliceStart, sliceLength, simple) => {
|
|
485
|
+
if (defaultNumber)
|
|
486
|
+
return args[Number(defaultNumber) - 1] || defaultValue;
|
|
487
|
+
if (sliceStart) {
|
|
488
|
+
const start = Math.max(0, Number(sliceStart) - 1);
|
|
489
|
+
return args.slice(start, sliceLength ? start + Number(sliceLength) : undefined).join(' ');
|
|
490
|
+
}
|
|
491
|
+
if (simple === 'ARGUMENTS' || simple === '@')
|
|
492
|
+
return allArgs;
|
|
493
|
+
return args[Number(simple) - 1] ?? '';
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
function stripJsonComments(source) {
|
|
497
|
+
let output = '';
|
|
498
|
+
let inString = false;
|
|
499
|
+
let escaped = false;
|
|
500
|
+
let lineComment = false;
|
|
501
|
+
let blockComment = false;
|
|
502
|
+
for (let index = 0; index < source.length; index += 1) {
|
|
503
|
+
const character = source[index];
|
|
504
|
+
const next = source[index + 1];
|
|
505
|
+
if (lineComment) {
|
|
506
|
+
if (character === '\n') {
|
|
507
|
+
lineComment = false;
|
|
508
|
+
output += character;
|
|
509
|
+
}
|
|
510
|
+
continue;
|
|
511
|
+
}
|
|
512
|
+
if (blockComment) {
|
|
513
|
+
if (character === '*' && next === '/') {
|
|
514
|
+
blockComment = false;
|
|
515
|
+
index += 1;
|
|
516
|
+
}
|
|
517
|
+
else if (character === '\n') {
|
|
518
|
+
output += character;
|
|
519
|
+
}
|
|
520
|
+
continue;
|
|
521
|
+
}
|
|
522
|
+
if (inString) {
|
|
523
|
+
output += character;
|
|
524
|
+
if (escaped)
|
|
525
|
+
escaped = false;
|
|
526
|
+
else if (character === '\\')
|
|
527
|
+
escaped = true;
|
|
528
|
+
else if (character === '"')
|
|
529
|
+
inString = false;
|
|
530
|
+
continue;
|
|
531
|
+
}
|
|
532
|
+
if (character === '"') {
|
|
533
|
+
inString = true;
|
|
534
|
+
output += character;
|
|
535
|
+
}
|
|
536
|
+
else if (character === '/' && next === '/') {
|
|
537
|
+
lineComment = true;
|
|
538
|
+
index += 1;
|
|
539
|
+
}
|
|
540
|
+
else if (character === '/' && next === '*') {
|
|
541
|
+
blockComment = true;
|
|
542
|
+
index += 1;
|
|
543
|
+
}
|
|
544
|
+
else {
|
|
545
|
+
output += character;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
return output;
|
|
549
|
+
}
|
|
550
|
+
function configuredResourcePaths(configured, root) {
|
|
551
|
+
const values = Array.isArray(configured) ? configured : [configured];
|
|
552
|
+
return values.flatMap((value) => {
|
|
553
|
+
if (typeof value !== 'string' || value.trim() === '')
|
|
554
|
+
return [];
|
|
555
|
+
const path = value.trim();
|
|
556
|
+
return [
|
|
557
|
+
isAbsolute(path)
|
|
558
|
+
? path
|
|
559
|
+
: resolve(root, path),
|
|
560
|
+
];
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
async function loadExtensionTools(extensionPaths, cwd) {
|
|
564
|
+
const tools = new Map();
|
|
565
|
+
for (const extensionPath of extensionPaths) {
|
|
566
|
+
const discovered = await discoverDirectoryResources(extensionPath, true);
|
|
567
|
+
const candidates = discovered.length > 0 ? discovered : [extensionPath];
|
|
568
|
+
for (const candidate of candidates) {
|
|
569
|
+
if (!EXTENSION_FILE_EXTENSIONS.has(extname(candidate).toLowerCase())) {
|
|
570
|
+
continue;
|
|
571
|
+
}
|
|
572
|
+
try {
|
|
573
|
+
const extensionTools = await loadExtension(candidate, cwd);
|
|
574
|
+
for (const tool of extensionTools) {
|
|
575
|
+
tools.set(tool.name, tool);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
catch {
|
|
579
|
+
// Extension resources are optional. The fixed harness/MCP loadout must
|
|
580
|
+
// remain available even when a local extension has a missing dependency.
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
return [...tools.values()];
|
|
585
|
+
}
|
|
586
|
+
async function loadExtension(extensionPath, cwd) {
|
|
587
|
+
const module = (await import(`${pathToFileURL(extensionPath).href}?ensemble=${encodeURIComponent(String(Date.now()))}`));
|
|
588
|
+
const factory = module.default;
|
|
589
|
+
if (typeof factory !== 'function')
|
|
590
|
+
return [];
|
|
591
|
+
const registered = [];
|
|
592
|
+
const extensionFactory = factory;
|
|
593
|
+
await extensionFactory({
|
|
594
|
+
registerTool: (definition) => {
|
|
595
|
+
if (isRecord(definition) && typeof definition.name === 'string' && typeof definition.execute === 'function') {
|
|
596
|
+
registered.push(definition);
|
|
597
|
+
}
|
|
598
|
+
},
|
|
599
|
+
on: () => { },
|
|
600
|
+
registerCommand: () => { },
|
|
601
|
+
registerShortcut: () => { },
|
|
602
|
+
registerFlag: () => { },
|
|
603
|
+
registerProvider: () => { },
|
|
604
|
+
cwd,
|
|
605
|
+
});
|
|
606
|
+
return registered.map((definition) => toPiAgentTool(definition, cwd));
|
|
607
|
+
}
|
|
608
|
+
function toPiAgentTool(definition, cwd) {
|
|
609
|
+
return {
|
|
610
|
+
name: definition.name,
|
|
611
|
+
label: definition.label ?? definition.name,
|
|
612
|
+
description: definition.description ?? '',
|
|
613
|
+
parameters: definition.parameters,
|
|
614
|
+
execute: async (toolCallId, params, signal, onUpdate) => {
|
|
615
|
+
const result = await definition.execute(toolCallId, params, signal, onUpdate, createExtensionContext(cwd, signal));
|
|
616
|
+
return normalizeToolResult(result);
|
|
617
|
+
},
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
function createExtensionContext(cwd, signal) {
|
|
621
|
+
return {
|
|
622
|
+
cwd,
|
|
623
|
+
signal,
|
|
624
|
+
hasUI: false,
|
|
625
|
+
isIdle: () => true,
|
|
626
|
+
isProjectTrusted: () => true,
|
|
627
|
+
ui: {
|
|
628
|
+
notify: () => { },
|
|
629
|
+
setStatus: () => { },
|
|
630
|
+
setWidget: () => { },
|
|
631
|
+
setTitle: () => { },
|
|
632
|
+
confirm: async () => false,
|
|
633
|
+
select: async () => undefined,
|
|
634
|
+
input: async () => undefined,
|
|
635
|
+
},
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
function normalizeToolResult(result) {
|
|
639
|
+
if (isRecord(result) && Array.isArray(result.content)) {
|
|
640
|
+
return {
|
|
641
|
+
...result,
|
|
642
|
+
details: result.details ?? {},
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
return {
|
|
646
|
+
content: [{ type: 'text', text: String(result ?? '') }],
|
|
647
|
+
details: {},
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
function mergeRecords(base, override) {
|
|
651
|
+
const merged = { ...base };
|
|
652
|
+
for (const [key, value] of Object.entries(override)) {
|
|
653
|
+
const previous = merged[key];
|
|
654
|
+
merged[key] =
|
|
655
|
+
isRecord(previous) && isRecord(value)
|
|
656
|
+
? mergeRecords(previous, value)
|
|
657
|
+
: value;
|
|
658
|
+
}
|
|
659
|
+
return merged;
|
|
660
|
+
}
|
|
661
|
+
function uniquePaths(paths) {
|
|
662
|
+
return [...new Set(paths)];
|
|
663
|
+
}
|
|
664
|
+
function isRecord(value) {
|
|
665
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
666
|
+
}
|
|
667
|
+
function isFileNotFound(error) {
|
|
668
|
+
return isRecord(error) && error.code === 'ENOENT';
|
|
669
|
+
}
|
|
670
|
+
function isPathUnavailable(error) {
|
|
671
|
+
return isRecord(error) && (error.code === 'ENOENT' || error.code === 'ENOTDIR');
|
|
672
|
+
}
|
|
673
|
+
//# sourceMappingURL=pi-resource-loader.js.map
|