@oh-my-pi/pi-coding-agent 15.5.10 → 15.5.12
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/CHANGELOG.md +34 -0
- package/dist/types/cli-commands.d.ts +19 -0
- package/dist/types/commands/install.d.ts +51 -0
- package/dist/types/discovery/index.d.ts +1 -0
- package/dist/types/discovery/omp-extension-roots.d.ts +43 -0
- package/dist/types/discovery/omp-plugins.d.ts +1 -0
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +14 -0
- package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +2 -0
- package/dist/types/extensibility/plugins/loader.d.ts +12 -2
- package/dist/types/index.d.ts +3 -0
- package/dist/types/modes/components/custom-editor.d.ts +3 -0
- package/dist/types/modes/ultrathink.d.ts +10 -0
- package/dist/types/session/redis-session-storage.d.ts +124 -0
- package/dist/types/session/sql-session-storage.d.ts +141 -0
- package/dist/types/tools/todo-write.d.ts +30 -0
- package/examples/sdk/12-redis-sessions.ts +54 -0
- package/examples/sdk/13-sql-sessions.ts +61 -0
- package/package.json +8 -8
- package/scripts/build-binary.ts +14 -9
- package/src/cli-commands.ts +44 -0
- package/src/cli.ts +2 -32
- package/src/commands/install.ts +107 -0
- package/src/discovery/index.ts +1 -0
- package/src/discovery/omp-extension-roots.ts +190 -0
- package/src/discovery/omp-plugins.ts +383 -0
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +15 -0
- package/src/extensibility/plugins/legacy-pi-compat.ts +63 -22
- package/src/extensibility/plugins/loader.ts +43 -18
- package/src/index.ts +3 -0
- package/src/internal-urls/docs-index.generated.ts +2 -2
- package/src/main.ts +12 -0
- package/src/memories/index.ts +8 -3
- package/src/modes/components/custom-editor.ts +3 -0
- package/src/modes/interactive-mode.ts +243 -12
- package/src/modes/ultrathink.ts +79 -0
- package/src/prompts/system/ultrathink-notice.md +3 -0
- package/src/session/agent-session.ts +28 -0
- package/src/session/redis-session-storage.ts +481 -0
- package/src/session/sql-session-storage.ts +565 -0
- package/src/tools/read.ts +23 -6
- package/src/tools/todo-write.ts +64 -0
- package/src/tools/write.ts +40 -6
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OMP extension-package sub-discovery provider.
|
|
3
|
+
*
|
|
4
|
+
* When a user configures an extension via `extensions:` (in settings) or
|
|
5
|
+
* `--extension`/`-e` (on the CLI), the docs promise that the package's
|
|
6
|
+
* sibling directories — `skills/`, `hooks/pre|post/`, `tools/`, `commands/`,
|
|
7
|
+
* `rules/`, `prompts/`, and `.mcp.json` — are picked up by omp's standard
|
|
8
|
+
* discovery surfaces. The native `omp` provider in `builtin.ts` only walks
|
|
9
|
+
* `.omp/` and `~/.omp/agent/`, so without this provider those sub-trees are
|
|
10
|
+
* silently ignored.
|
|
11
|
+
*
|
|
12
|
+
* Provider priority is set below the native `omp` provider (100) so an
|
|
13
|
+
* extension package never shadows the user's own `.omp/` configuration on
|
|
14
|
+
* dedup.
|
|
15
|
+
*
|
|
16
|
+
* @see ./omp-extension-roots.ts
|
|
17
|
+
* @see ../../docs/extension-loading.md
|
|
18
|
+
*/
|
|
19
|
+
import * as path from "node:path";
|
|
20
|
+
import { logger, parseFrontmatter, tryParseJson } from "@oh-my-pi/pi-utils";
|
|
21
|
+
import { registerProvider } from "../capability";
|
|
22
|
+
import { readDirEntries, readFile } from "../capability/fs";
|
|
23
|
+
import { type Hook, hookCapability } from "../capability/hook";
|
|
24
|
+
import { type MCPServer, mcpCapability } from "../capability/mcp";
|
|
25
|
+
import { type Prompt, promptCapability } from "../capability/prompt";
|
|
26
|
+
import { type Rule, ruleCapability } from "../capability/rule";
|
|
27
|
+
import { type Skill, skillCapability } from "../capability/skill";
|
|
28
|
+
import { type SlashCommand, slashCommandCapability } from "../capability/slash-command";
|
|
29
|
+
import { type CustomTool, toolCapability } from "../capability/tool";
|
|
30
|
+
import type { LoadContext, LoadResult } from "../capability/types";
|
|
31
|
+
import { buildRuleFromMarkdown, createSourceMeta, loadFilesFromDir, scanSkillsFromDir } from "./helpers";
|
|
32
|
+
import { listOmpExtensionRoots, type OmpExtensionRoot } from "./omp-extension-roots";
|
|
33
|
+
|
|
34
|
+
const PROVIDER_ID = "omp-plugins";
|
|
35
|
+
const DISPLAY_NAME = "OMP Extension Packages";
|
|
36
|
+
const DESCRIPTION =
|
|
37
|
+
"Sub-discovery (skills, hooks, tools, commands, rules, prompts, .mcp.json) inside extension packages";
|
|
38
|
+
const PRIORITY = 90;
|
|
39
|
+
|
|
40
|
+
// =============================================================================
|
|
41
|
+
// Skills
|
|
42
|
+
// =============================================================================
|
|
43
|
+
|
|
44
|
+
async function loadSkills(ctx: LoadContext): Promise<LoadResult<Skill>> {
|
|
45
|
+
const roots = await listOmpExtensionRoots(ctx);
|
|
46
|
+
const results = await Promise.all(
|
|
47
|
+
roots.map(root =>
|
|
48
|
+
scanSkillsFromDir(ctx, {
|
|
49
|
+
dir: path.join(root.path, "skills"),
|
|
50
|
+
providerId: PROVIDER_ID,
|
|
51
|
+
level: root.level,
|
|
52
|
+
requireDescription: true,
|
|
53
|
+
}),
|
|
54
|
+
),
|
|
55
|
+
);
|
|
56
|
+
return {
|
|
57
|
+
items: results.flatMap(r => r.items),
|
|
58
|
+
warnings: results.flatMap(r => r.warnings ?? []),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// =============================================================================
|
|
63
|
+
// Slash Commands
|
|
64
|
+
// =============================================================================
|
|
65
|
+
|
|
66
|
+
async function loadSlashCommands(ctx: LoadContext): Promise<LoadResult<SlashCommand>> {
|
|
67
|
+
const roots = await listOmpExtensionRoots(ctx);
|
|
68
|
+
const results = await Promise.all(
|
|
69
|
+
roots.map(root =>
|
|
70
|
+
loadFilesFromDir<SlashCommand>(ctx, path.join(root.path, "commands"), PROVIDER_ID, root.level, {
|
|
71
|
+
extensions: ["md"],
|
|
72
|
+
transform: (name, content, filePath, source) => ({
|
|
73
|
+
name: name.replace(/\.md$/, ""),
|
|
74
|
+
path: filePath,
|
|
75
|
+
content,
|
|
76
|
+
level: root.level,
|
|
77
|
+
_source: source,
|
|
78
|
+
}),
|
|
79
|
+
}),
|
|
80
|
+
),
|
|
81
|
+
);
|
|
82
|
+
return {
|
|
83
|
+
items: results.flatMap(r => r.items),
|
|
84
|
+
warnings: results.flatMap(r => r.warnings ?? []),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// =============================================================================
|
|
89
|
+
// Rules
|
|
90
|
+
// =============================================================================
|
|
91
|
+
|
|
92
|
+
async function loadRules(ctx: LoadContext): Promise<LoadResult<Rule>> {
|
|
93
|
+
const roots = await listOmpExtensionRoots(ctx);
|
|
94
|
+
const results = await Promise.all(
|
|
95
|
+
roots.map(root =>
|
|
96
|
+
loadFilesFromDir<Rule>(ctx, path.join(root.path, "rules"), PROVIDER_ID, root.level, {
|
|
97
|
+
extensions: ["md", "mdc"],
|
|
98
|
+
transform: (name, content, filePath, source) =>
|
|
99
|
+
buildRuleFromMarkdown(name, content, filePath, source, { stripNamePattern: /\.(md|mdc)$/ }),
|
|
100
|
+
}),
|
|
101
|
+
),
|
|
102
|
+
);
|
|
103
|
+
return {
|
|
104
|
+
items: results.flatMap(r => r.items),
|
|
105
|
+
warnings: results.flatMap(r => r.warnings ?? []),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// =============================================================================
|
|
110
|
+
// Prompts
|
|
111
|
+
// =============================================================================
|
|
112
|
+
|
|
113
|
+
async function loadPrompts(ctx: LoadContext): Promise<LoadResult<Prompt>> {
|
|
114
|
+
const roots = await listOmpExtensionRoots(ctx);
|
|
115
|
+
const results = await Promise.all(
|
|
116
|
+
roots.map(root =>
|
|
117
|
+
loadFilesFromDir<Prompt>(ctx, path.join(root.path, "prompts"), PROVIDER_ID, root.level, {
|
|
118
|
+
extensions: ["md"],
|
|
119
|
+
transform: (name, content, filePath, source) => ({
|
|
120
|
+
name: name.replace(/\.md$/, ""),
|
|
121
|
+
path: filePath,
|
|
122
|
+
content,
|
|
123
|
+
_source: source,
|
|
124
|
+
}),
|
|
125
|
+
}),
|
|
126
|
+
),
|
|
127
|
+
);
|
|
128
|
+
return {
|
|
129
|
+
items: results.flatMap(r => r.items),
|
|
130
|
+
warnings: results.flatMap(r => r.warnings ?? []),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// =============================================================================
|
|
135
|
+
// Hooks
|
|
136
|
+
// =============================================================================
|
|
137
|
+
|
|
138
|
+
const HOOK_TYPES: ReadonlyArray<"pre" | "post"> = ["pre", "post"];
|
|
139
|
+
|
|
140
|
+
async function loadHooks(ctx: LoadContext): Promise<LoadResult<Hook>> {
|
|
141
|
+
const roots = await listOmpExtensionRoots(ctx);
|
|
142
|
+
const tasks: Array<{ root: OmpExtensionRoot; hookType: "pre" | "post" }> = [];
|
|
143
|
+
for (const root of roots) {
|
|
144
|
+
for (const hookType of HOOK_TYPES) {
|
|
145
|
+
tasks.push({ root, hookType });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
const results = await Promise.all(
|
|
149
|
+
tasks.map(({ root, hookType }) =>
|
|
150
|
+
loadFilesFromDir<Hook>(ctx, path.join(root.path, "hooks", hookType), PROVIDER_ID, root.level, {
|
|
151
|
+
transform: (name, _content, filePath, source) => {
|
|
152
|
+
const baseName = name.includes(".") ? name.slice(0, name.lastIndexOf(".")) : name;
|
|
153
|
+
const tool = baseName === "*" ? "*" : baseName;
|
|
154
|
+
return {
|
|
155
|
+
name,
|
|
156
|
+
path: filePath,
|
|
157
|
+
type: hookType,
|
|
158
|
+
tool,
|
|
159
|
+
level: root.level,
|
|
160
|
+
_source: source,
|
|
161
|
+
};
|
|
162
|
+
},
|
|
163
|
+
}),
|
|
164
|
+
),
|
|
165
|
+
);
|
|
166
|
+
return {
|
|
167
|
+
items: results.flatMap(r => r.items),
|
|
168
|
+
warnings: results.flatMap(r => r.warnings ?? []),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// =============================================================================
|
|
173
|
+
// Custom Tools
|
|
174
|
+
// =============================================================================
|
|
175
|
+
|
|
176
|
+
const TOOL_EXTENSIONS = ["json", "md", "ts", "js", "sh", "bash", "py"];
|
|
177
|
+
|
|
178
|
+
async function loadTools(ctx: LoadContext): Promise<LoadResult<CustomTool>> {
|
|
179
|
+
const roots = await listOmpExtensionRoots(ctx);
|
|
180
|
+
const perRoot = await Promise.all(
|
|
181
|
+
roots.map(async root => {
|
|
182
|
+
const toolsDir = path.join(root.path, "tools");
|
|
183
|
+
const [filesResult, entries] = await Promise.all([
|
|
184
|
+
loadFilesFromDir<CustomTool>(ctx, toolsDir, PROVIDER_ID, root.level, {
|
|
185
|
+
extensions: TOOL_EXTENSIONS,
|
|
186
|
+
transform: (name, content, filePath, source) => {
|
|
187
|
+
if (name.endsWith(".json")) {
|
|
188
|
+
const data = tryParseJson<{ name?: string; description?: string }>(content);
|
|
189
|
+
const toolName = data?.name || name.replace(/\.json$/, "");
|
|
190
|
+
const description =
|
|
191
|
+
typeof data?.description === "string" && data.description.trim()
|
|
192
|
+
? data.description
|
|
193
|
+
: `${toolName} custom tool`;
|
|
194
|
+
return { name: toolName, path: filePath, description, level: root.level, _source: source };
|
|
195
|
+
}
|
|
196
|
+
if (name.endsWith(".md")) {
|
|
197
|
+
const { frontmatter } = parseFrontmatter(content, { source: filePath });
|
|
198
|
+
const toolName = (frontmatter.name as string) || name.replace(/\.md$/, "");
|
|
199
|
+
const description =
|
|
200
|
+
typeof frontmatter.description === "string" && frontmatter.description.trim()
|
|
201
|
+
? String(frontmatter.description)
|
|
202
|
+
: `${toolName} custom tool`;
|
|
203
|
+
return { name: toolName, path: filePath, description, level: root.level, _source: source };
|
|
204
|
+
}
|
|
205
|
+
const toolName = name.replace(/\.(ts|js|sh|bash|py)$/, "");
|
|
206
|
+
return {
|
|
207
|
+
name: toolName,
|
|
208
|
+
path: filePath,
|
|
209
|
+
description: `${toolName} custom tool`,
|
|
210
|
+
level: root.level,
|
|
211
|
+
_source: source,
|
|
212
|
+
};
|
|
213
|
+
},
|
|
214
|
+
}),
|
|
215
|
+
readDirEntries(toolsDir),
|
|
216
|
+
]);
|
|
217
|
+
|
|
218
|
+
// `<tools>/<name>/index.ts` sub-directory tools, mirroring `builtin.ts:loadTools`.
|
|
219
|
+
const indexCandidates = entries
|
|
220
|
+
.filter(e => !e.name.startsWith(".") && e.isDirectory())
|
|
221
|
+
.map(e => path.join(toolsDir, e.name, "index.ts"));
|
|
222
|
+
const indexContents = await Promise.all(indexCandidates.map(p => readFile(p)));
|
|
223
|
+
const indexItems: CustomTool[] = [];
|
|
224
|
+
for (let i = 0; i < indexCandidates.length; i++) {
|
|
225
|
+
if (indexContents[i] === null) continue;
|
|
226
|
+
const indexPath = indexCandidates[i];
|
|
227
|
+
const toolName = path.basename(path.dirname(indexPath));
|
|
228
|
+
indexItems.push({
|
|
229
|
+
name: toolName,
|
|
230
|
+
path: indexPath,
|
|
231
|
+
description: `${toolName} custom tool`,
|
|
232
|
+
level: root.level,
|
|
233
|
+
_source: createSourceMeta(PROVIDER_ID, indexPath, root.level),
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return { filesResult, indexItems };
|
|
238
|
+
}),
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
const items: CustomTool[] = [];
|
|
242
|
+
const warnings: string[] = [];
|
|
243
|
+
for (const { filesResult, indexItems } of perRoot) {
|
|
244
|
+
items.push(...filesResult.items, ...indexItems);
|
|
245
|
+
if (filesResult.warnings) warnings.push(...filesResult.warnings);
|
|
246
|
+
}
|
|
247
|
+
return { items, warnings };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// =============================================================================
|
|
251
|
+
// MCP Servers
|
|
252
|
+
// =============================================================================
|
|
253
|
+
|
|
254
|
+
const MCP_FILENAMES = [".mcp.json", "mcp.json"] as const;
|
|
255
|
+
|
|
256
|
+
interface RawMcpServer {
|
|
257
|
+
enabled?: boolean;
|
|
258
|
+
timeout?: number;
|
|
259
|
+
command?: string;
|
|
260
|
+
args?: string[];
|
|
261
|
+
env?: Record<string, string>;
|
|
262
|
+
cwd?: string;
|
|
263
|
+
url?: string;
|
|
264
|
+
headers?: Record<string, string>;
|
|
265
|
+
auth?: MCPServer["auth"];
|
|
266
|
+
oauth?: MCPServer["oauth"];
|
|
267
|
+
type?: MCPServer["transport"];
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async function loadMCPServers(ctx: LoadContext): Promise<LoadResult<MCPServer>> {
|
|
271
|
+
const roots = await listOmpExtensionRoots(ctx);
|
|
272
|
+
const items: MCPServer[] = [];
|
|
273
|
+
const warnings: string[] = [];
|
|
274
|
+
|
|
275
|
+
const tasks: Array<{ root: OmpExtensionRoot; mcpPath: string }> = [];
|
|
276
|
+
for (const root of roots) {
|
|
277
|
+
for (const filename of MCP_FILENAMES) {
|
|
278
|
+
tasks.push({ root, mcpPath: path.join(root.path, filename) });
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
const contents = await Promise.all(tasks.map(({ mcpPath }) => readFile(mcpPath)));
|
|
282
|
+
|
|
283
|
+
for (let i = 0; i < tasks.length; i++) {
|
|
284
|
+
const raw = contents[i];
|
|
285
|
+
if (raw === null) continue;
|
|
286
|
+
const { root, mcpPath } = tasks[i];
|
|
287
|
+
|
|
288
|
+
const parsed = tryParseJson<{ mcpServers?: Record<string, unknown> }>(raw);
|
|
289
|
+
if (!parsed) {
|
|
290
|
+
warnings.push(`[omp-plugins] Invalid JSON in ${mcpPath}`);
|
|
291
|
+
logger.warn(`[omp-plugins] Invalid JSON in ${mcpPath}`);
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
const servers = parsed.mcpServers;
|
|
295
|
+
if (!servers || typeof servers !== "object" || Array.isArray(servers)) continue;
|
|
296
|
+
|
|
297
|
+
for (const [serverName, serverCfg] of Object.entries(servers)) {
|
|
298
|
+
if (!serverCfg || typeof serverCfg !== "object" || Array.isArray(serverCfg)) continue;
|
|
299
|
+
const cfg = serverCfg as RawMcpServer;
|
|
300
|
+
if (typeof cfg.command !== "string" && typeof cfg.url !== "string") {
|
|
301
|
+
warnings.push(`[omp-plugins] Skipping MCP server "${serverName}" in ${mcpPath}: missing command or url`);
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
items.push({
|
|
305
|
+
name: serverName,
|
|
306
|
+
...(cfg.enabled !== undefined && { enabled: cfg.enabled }),
|
|
307
|
+
...(cfg.timeout !== undefined && { timeout: cfg.timeout }),
|
|
308
|
+
...(cfg.command !== undefined && { command: cfg.command }),
|
|
309
|
+
...(cfg.args !== undefined && { args: cfg.args }),
|
|
310
|
+
...(cfg.env !== undefined && { env: cfg.env }),
|
|
311
|
+
...(cfg.cwd !== undefined && { cwd: cfg.cwd }),
|
|
312
|
+
...(cfg.url !== undefined && { url: cfg.url }),
|
|
313
|
+
...(cfg.headers !== undefined && { headers: cfg.headers }),
|
|
314
|
+
...(cfg.auth !== undefined && { auth: cfg.auth }),
|
|
315
|
+
...(cfg.oauth !== undefined && { oauth: cfg.oauth }),
|
|
316
|
+
...(cfg.type !== undefined && { transport: cfg.type }),
|
|
317
|
+
_source: createSourceMeta(PROVIDER_ID, mcpPath, root.level),
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return { items, warnings };
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// =============================================================================
|
|
326
|
+
// Provider Registration
|
|
327
|
+
// =============================================================================
|
|
328
|
+
|
|
329
|
+
registerProvider<Skill>(skillCapability.id, {
|
|
330
|
+
id: PROVIDER_ID,
|
|
331
|
+
displayName: DISPLAY_NAME,
|
|
332
|
+
description: DESCRIPTION,
|
|
333
|
+
priority: PRIORITY,
|
|
334
|
+
load: loadSkills,
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
registerProvider<SlashCommand>(slashCommandCapability.id, {
|
|
338
|
+
id: PROVIDER_ID,
|
|
339
|
+
displayName: DISPLAY_NAME,
|
|
340
|
+
description: DESCRIPTION,
|
|
341
|
+
priority: PRIORITY,
|
|
342
|
+
load: loadSlashCommands,
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
registerProvider<Rule>(ruleCapability.id, {
|
|
346
|
+
id: PROVIDER_ID,
|
|
347
|
+
displayName: DISPLAY_NAME,
|
|
348
|
+
description: DESCRIPTION,
|
|
349
|
+
priority: PRIORITY,
|
|
350
|
+
load: loadRules,
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
registerProvider<Prompt>(promptCapability.id, {
|
|
354
|
+
id: PROVIDER_ID,
|
|
355
|
+
displayName: DISPLAY_NAME,
|
|
356
|
+
description: DESCRIPTION,
|
|
357
|
+
priority: PRIORITY,
|
|
358
|
+
load: loadPrompts,
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
registerProvider<Hook>(hookCapability.id, {
|
|
362
|
+
id: PROVIDER_ID,
|
|
363
|
+
displayName: DISPLAY_NAME,
|
|
364
|
+
description: DESCRIPTION,
|
|
365
|
+
priority: PRIORITY,
|
|
366
|
+
load: loadHooks,
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
registerProvider<CustomTool>(toolCapability.id, {
|
|
370
|
+
id: PROVIDER_ID,
|
|
371
|
+
displayName: DISPLAY_NAME,
|
|
372
|
+
description: DESCRIPTION,
|
|
373
|
+
priority: PRIORITY,
|
|
374
|
+
load: loadTools,
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
registerProvider<MCPServer>(mcpCapability.id, {
|
|
378
|
+
id: PROVIDER_ID,
|
|
379
|
+
displayName: DISPLAY_NAME,
|
|
380
|
+
description: DESCRIPTION,
|
|
381
|
+
priority: PRIORITY,
|
|
382
|
+
load: loadMCPServers,
|
|
383
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compatibility shim for legacy extensions importing the package root of
|
|
3
|
+
* `@oh-my-pi/pi-coding-agent` (or one of its aliased scopes like
|
|
4
|
+
* `@earendil-works/pi-coding-agent` or `@mariozechner/pi-coding-agent`).
|
|
5
|
+
*
|
|
6
|
+
* The coding-agent package's own barrel (`./src/index.ts`) cannot be listed
|
|
7
|
+
* as a `bun --compile` extra entrypoint alongside the CLI entry without
|
|
8
|
+
* silently breaking the main binary's startup (see issue #1474 follow-up).
|
|
9
|
+
* Routing legacy plugin imports through this sibling shim sidesteps that
|
|
10
|
+
* conflict: bun bundles a distinct entry whose path differs from the CLI
|
|
11
|
+
* entry, while still re-exporting the canonical surface so plugins observe
|
|
12
|
+
* the same module identity as a direct `@oh-my-pi/pi-coding-agent` import.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export * from "../index";
|
|
@@ -4,6 +4,8 @@ import * as path from "node:path";
|
|
|
4
4
|
import * as url from "node:url";
|
|
5
5
|
import { isCompiledBinary } from "@oh-my-pi/pi-utils";
|
|
6
6
|
|
|
7
|
+
const IS_COMPILED_BINARY = isCompiledBinary();
|
|
8
|
+
|
|
7
9
|
// Canonical scope for in-process pi packages. Plugins published against any of
|
|
8
10
|
// the aliased scopes below (mariozechner's original publish, earendil-works'
|
|
9
11
|
// fork, or the canonical @oh-my-pi scope itself) are remapped to this scope and
|
|
@@ -14,10 +16,9 @@ import { isCompiledBinary } from "@oh-my-pi/pi-utils";
|
|
|
14
16
|
const CANONICAL_PI_SCOPE = "@oh-my-pi";
|
|
15
17
|
|
|
16
18
|
// Scopes that have historically been used to publish (or alias) the same set
|
|
17
|
-
// of internal pi-* packages. `@oh-my-pi` is intentionally included so
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
// plugin's own node_modules tree at install time.
|
|
19
|
+
// of internal pi-* packages. `@oh-my-pi` is intentionally included so direct
|
|
20
|
+
// canonical imports still pass through the same host-bundled package resolution
|
|
21
|
+
// path instead of pulling a duplicate copy from plugin node_modules.
|
|
21
22
|
const PI_SCOPE_ALIASES = ["oh-my-pi", "mariozechner", "earendil-works"] as const;
|
|
22
23
|
|
|
23
24
|
// Internal pi-* package basenames bundled inside the omp binary.
|
|
@@ -58,17 +59,20 @@ const resolvedSpecifierFallbacks = new Map<string, string>();
|
|
|
58
59
|
const TYPEBOX_SPECIFIER = "@sinclair/typebox";
|
|
59
60
|
const TYPEBOX_SPECIFIER_FILTER = /^@sinclair\/typebox$/;
|
|
60
61
|
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
|
|
71
|
-
|
|
62
|
+
// Compat shim paths owned by this package. The dev branch resolves the sibling
|
|
63
|
+
// source file via `import.meta.dir` (works in monorepo, source-link, and
|
|
64
|
+
// node_modules installs alike, since each install layout ships the shim next
|
|
65
|
+
// to this file). The compiled-binary branch points at the `--root`-relative
|
|
66
|
+
// bunfs path produced by `scripts/build-binary.ts`; every shim listed below
|
|
67
|
+
// must be registered there as an explicit `--compile` entrypoint or release
|
|
68
|
+
// builds fail with missing-module errors. Non-shim bundled packages are
|
|
69
|
+
// resolved via `Bun.resolveSync` (see `resolveCanonicalPiSpecifier`), so they
|
|
70
|
+
// keep working in installed-package mode where the on-disk layout differs from
|
|
71
|
+
// the monorepo source tree.
|
|
72
|
+
const BUNFS_PACKAGE_ROOT = "/$bunfs/root/packages";
|
|
73
|
+
|
|
74
|
+
const TYPEBOX_SHIM_PATH = IS_COMPILED_BINARY
|
|
75
|
+
? `${BUNFS_PACKAGE_ROOT}/coding-agent/src/extensibility/typebox.js`
|
|
72
76
|
: path.resolve(import.meta.dir, "../typebox.ts");
|
|
73
77
|
|
|
74
78
|
// Legacy extensions historically imported `Type` (and `Static`/`TSchema`) from
|
|
@@ -79,11 +83,37 @@ const TYPEBOX_SHIM_PATH = isCompiledBinary()
|
|
|
79
83
|
// plus the borrowed `Type` runtime from the Zod-backed TypeBox shim. Subpath
|
|
80
84
|
// imports such as `@oh-my-pi/pi-ai/utils/oauth` continue to resolve directly
|
|
81
85
|
// against the bundled pi-ai package.
|
|
82
|
-
const LEGACY_PI_AI_SHIM_PATH =
|
|
83
|
-
?
|
|
86
|
+
const LEGACY_PI_AI_SHIM_PATH = IS_COMPILED_BINARY
|
|
87
|
+
? `${BUNFS_PACKAGE_ROOT}/coding-agent/src/extensibility/legacy-pi-ai-shim.js`
|
|
84
88
|
: path.resolve(import.meta.dir, "../legacy-pi-ai-shim.ts");
|
|
89
|
+
|
|
90
|
+
// The coding-agent's own `./src/index.ts` cannot be listed as an extra
|
|
91
|
+
// `bun --compile` entrypoint alongside the CLI entry without breaking binary
|
|
92
|
+
// startup (issue #1474 follow-up). Legacy `@(scope)/pi-coding-agent` root
|
|
93
|
+
// imports therefore resolve through a sibling shim whose distinct file path
|
|
94
|
+
// avoids that collision while re-exporting the canonical package surface.
|
|
95
|
+
const LEGACY_PI_CODING_AGENT_SHIM_PATH = IS_COMPILED_BINARY
|
|
96
|
+
? `${BUNFS_PACKAGE_ROOT}/coding-agent/src/extensibility/legacy-pi-coding-agent-shim.js`
|
|
97
|
+
: path.resolve(import.meta.dir, "../legacy-pi-coding-agent-shim.ts");
|
|
98
|
+
|
|
99
|
+
// Package-root overrides. Shim entries are always applied because they replace
|
|
100
|
+
// (or augment) the canonical surface even in non-compiled installs. The bunfs
|
|
101
|
+
// entries are added only in compiled-binary mode — in dev / source-link /
|
|
102
|
+
// installed-package mode the canonical specifier resolves cleanly through
|
|
103
|
+
// `Bun.resolveSync`, and hardcoding a relative source-tree path would break
|
|
104
|
+
// installs where the bundled packages live at `node_modules/@oh-my-pi/pi-*`
|
|
105
|
+
// rather than `packages/*`.
|
|
85
106
|
const LEGACY_PI_PACKAGE_ROOT_OVERRIDES: Record<string, string> = {
|
|
86
107
|
[`${CANONICAL_PI_SCOPE}/pi-ai`]: LEGACY_PI_AI_SHIM_PATH,
|
|
108
|
+
[`${CANONICAL_PI_SCOPE}/pi-coding-agent`]: LEGACY_PI_CODING_AGENT_SHIM_PATH,
|
|
109
|
+
...(IS_COMPILED_BINARY
|
|
110
|
+
? {
|
|
111
|
+
[`${CANONICAL_PI_SCOPE}/pi-agent-core`]: `${BUNFS_PACKAGE_ROOT}/agent/src/index.js`,
|
|
112
|
+
[`${CANONICAL_PI_SCOPE}/pi-natives`]: `${BUNFS_PACKAGE_ROOT}/natives/native/index.js`,
|
|
113
|
+
[`${CANONICAL_PI_SCOPE}/pi-tui`]: `${BUNFS_PACKAGE_ROOT}/tui/src/index.js`,
|
|
114
|
+
[`${CANONICAL_PI_SCOPE}/pi-utils`]: `${BUNFS_PACKAGE_ROOT}/utils/src/index.js`,
|
|
115
|
+
}
|
|
116
|
+
: {}),
|
|
87
117
|
};
|
|
88
118
|
|
|
89
119
|
let isLegacyPiSpecifierShimInstalled = false;
|
|
@@ -297,14 +327,20 @@ function resolveLegacyPiSpecifier(args: { path: string; importer: string }): { p
|
|
|
297
327
|
return { path: resolveCanonicalPiSpecifier(remappedSpecifier) };
|
|
298
328
|
} catch {
|
|
299
329
|
// Fallback for compiled binary mode: the bundled packages live inside
|
|
300
|
-
// /$bunfs/root and aren't reachable by filesystem resolution.
|
|
301
|
-
//
|
|
302
|
-
//
|
|
330
|
+
// /$bunfs/root and aren't reachable by filesystem resolution. Prefer the
|
|
331
|
+
// canonical specifier against the importing file's directory when the
|
|
332
|
+
// plugin installed @oh-my-pi peer deps, then try the original legacy
|
|
333
|
+
// specifier for plugins that still vendor only @mariozechner or
|
|
334
|
+
// @earendil-works peer deps.
|
|
303
335
|
const importerDir = path.dirname(args.importer);
|
|
304
336
|
try {
|
|
305
|
-
return { path: Bun.resolveSync(
|
|
337
|
+
return { path: Bun.resolveSync(remappedSpecifier, importerDir) };
|
|
306
338
|
} catch {
|
|
307
|
-
|
|
339
|
+
try {
|
|
340
|
+
return { path: Bun.resolveSync(args.path, importerDir) };
|
|
341
|
+
} catch {
|
|
342
|
+
return undefined;
|
|
343
|
+
}
|
|
308
344
|
}
|
|
309
345
|
}
|
|
310
346
|
}
|
|
@@ -356,3 +392,8 @@ export function installLegacyPiSpecifierShim(): void {
|
|
|
356
392
|
},
|
|
357
393
|
});
|
|
358
394
|
}
|
|
395
|
+
|
|
396
|
+
/** Test seam: clears the memoized canonical specifier resolutions. */
|
|
397
|
+
export function __resetLegacyPiResolutionCache(): void {
|
|
398
|
+
resolvedSpecifierFallbacks.clear();
|
|
399
|
+
}
|
|
@@ -19,9 +19,14 @@ installLegacyPiSpecifierShim();
|
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
21
|
* Load plugin runtime config from lock file.
|
|
22
|
+
*
|
|
23
|
+
* `home` controls which `<plugins>/omp-plugins.lock.json` is read — pass it
|
|
24
|
+
* through whenever the caller is loading plugins for a tempdir-rooted
|
|
25
|
+
* scenario (tests, discovery sub-surfaces that need to mirror an alternate
|
|
26
|
+
* `LoadContext.home`).
|
|
22
27
|
*/
|
|
23
|
-
async function loadRuntimeConfig(): Promise<PluginRuntimeConfig> {
|
|
24
|
-
const lockPath = getPluginsLockfile();
|
|
28
|
+
async function loadRuntimeConfig(home?: string): Promise<PluginRuntimeConfig> {
|
|
29
|
+
const lockPath = getPluginsLockfile(home);
|
|
25
30
|
try {
|
|
26
31
|
return await Bun.file(lockPath).json();
|
|
27
32
|
} catch (err) {
|
|
@@ -46,44 +51,64 @@ async function loadProjectOverrides(cwd: string): Promise<ProjectPluginOverrides
|
|
|
46
51
|
}
|
|
47
52
|
/**
|
|
48
53
|
* Get list of enabled plugins with their resolved configurations.
|
|
49
|
-
*
|
|
54
|
+
*
|
|
55
|
+
* Respects both global runtime config and project overrides. Iterates the
|
|
56
|
+
* union of `<plugins>/package.json#dependencies` (`bun install`-installed
|
|
57
|
+
* packages) and `<plugins>/omp-plugins.lock.json#plugins` (so locally
|
|
58
|
+
* `plugin link`-symlinked extensions, which never get a dependency entry,
|
|
59
|
+
* are still discovered). The optional `home` parameter pins the plugins
|
|
60
|
+
* root for callers that need to enumerate plugins relative to a non-default
|
|
61
|
+
* home (tests with a tempdir, discovery loaders threaded with
|
|
62
|
+
* `LoadContext.home`).
|
|
50
63
|
*/
|
|
51
|
-
export async function getEnabledPlugins(cwd: string): Promise<InstalledPlugin[]> {
|
|
52
|
-
const
|
|
53
|
-
let pkg: { dependencies?: Record<string, string> };
|
|
54
|
-
try {
|
|
55
|
-
pkg = await Bun.file(pkgJsonPath).json();
|
|
56
|
-
} catch (err) {
|
|
57
|
-
if (isEnoent(err)) return [];
|
|
58
|
-
throw err;
|
|
59
|
-
}
|
|
64
|
+
export async function getEnabledPlugins(cwd: string, opts: { home?: string } = {}): Promise<InstalledPlugin[]> {
|
|
65
|
+
const { home } = opts;
|
|
60
66
|
|
|
61
|
-
const nodeModulesPath = getPluginsNodeModules();
|
|
67
|
+
const nodeModulesPath = getPluginsNodeModules(home);
|
|
62
68
|
if (!fs.existsSync(nodeModulesPath)) {
|
|
63
69
|
return [];
|
|
64
70
|
}
|
|
65
71
|
|
|
66
|
-
|
|
67
|
-
const
|
|
72
|
+
let depsKeys: string[] = [];
|
|
73
|
+
const pkgJsonPath = getPluginsPackageJson(home);
|
|
74
|
+
try {
|
|
75
|
+
const pkg: { dependencies?: Record<string, string> } = await Bun.file(pkgJsonPath).json();
|
|
76
|
+
depsKeys = Object.keys(pkg.dependencies ?? {});
|
|
77
|
+
} catch (err) {
|
|
78
|
+
// Linked-only setups may have no `<plugins>/package.json` yet — that's
|
|
79
|
+
// fine, the lockfile still records the link.
|
|
80
|
+
if (!isEnoent(err)) throw err;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const runtimeConfig = await loadRuntimeConfig(home);
|
|
68
84
|
const projectOverrides = await loadProjectOverrides(cwd);
|
|
85
|
+
|
|
86
|
+
// Union: dependencies (npm/marketplace installs) ∪ runtime-config plugins
|
|
87
|
+
// (links + already-recorded installs). Set preserves first-seen order,
|
|
88
|
+
// putting deps before link-only entries for deterministic output.
|
|
89
|
+
const names = new Set<string>(depsKeys);
|
|
90
|
+
for (const name of Object.keys(runtimeConfig.plugins ?? {})) {
|
|
91
|
+
names.add(name);
|
|
92
|
+
}
|
|
93
|
+
|
|
69
94
|
const plugins: InstalledPlugin[] = [];
|
|
70
|
-
for (const
|
|
95
|
+
for (const name of names) {
|
|
71
96
|
const pluginPkgPath = path.join(nodeModulesPath, name, "package.json");
|
|
72
97
|
let pluginPkg: { version: string; omp?: PluginManifest; pi?: PluginManifest };
|
|
73
98
|
try {
|
|
74
99
|
pluginPkg = await Bun.file(pluginPkgPath).json();
|
|
75
100
|
} catch (err) {
|
|
101
|
+
// Lockfile entry without a corresponding node_modules tree means the
|
|
102
|
+
// link was deleted out from under us; skip silently.
|
|
76
103
|
if (isEnoent(err)) continue;
|
|
77
104
|
throw err;
|
|
78
105
|
}
|
|
79
106
|
|
|
80
107
|
const manifest: PluginManifest | undefined = pluginPkg.omp || pluginPkg.pi;
|
|
81
|
-
|
|
82
108
|
if (!manifest) {
|
|
83
109
|
// Not an omp plugin, skip
|
|
84
110
|
continue;
|
|
85
111
|
}
|
|
86
|
-
|
|
87
112
|
manifest.version = pluginPkg.version;
|
|
88
113
|
|
|
89
114
|
const runtimeState = runtimeConfig.plugins[name];
|
package/src/index.ts
CHANGED
|
@@ -40,8 +40,11 @@ export * from "./session/agent-session";
|
|
|
40
40
|
// Auth and model registry
|
|
41
41
|
export * from "./session/auth-storage";
|
|
42
42
|
export * from "./session/messages";
|
|
43
|
+
export * from "./session/redis-session-storage";
|
|
43
44
|
export * from "./session/session-dump-format";
|
|
44
45
|
export * from "./session/session-manager";
|
|
46
|
+
export * from "./session/session-storage";
|
|
47
|
+
export * from "./session/sql-session-storage";
|
|
45
48
|
export * from "./task/executor";
|
|
46
49
|
export type * from "./task/types";
|
|
47
50
|
// Tools (detail types and utilities)
|