@oh-my-pi/pi-coding-agent 12.8.2 → 12.9.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/CHANGELOG.md +17 -0
- package/package.json +7 -7
- package/src/discovery/helpers.ts +5 -0
- package/src/discovery/index.ts +1 -0
- package/src/discovery/opencode.ts +394 -0
- package/src/extensibility/custom-tools/wrapper.ts +1 -11
- package/src/modes/components/status-line/segments.ts +3 -2
- package/src/sdk.ts +2 -4
- package/src/session/session-manager.ts +79 -36
- package/src/tools/fetch.ts +6 -2
- package/src/tools/index.ts +2 -2
- package/src/tools/output-meta.ts +49 -42
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [12.9.0] - 2026-02-17
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added OpenCode discovery provider to load configuration from ~/.config/opencode/ and .opencode/ directories
|
|
10
|
+
- Added support for loading MCP servers from opencode.json mcp key
|
|
11
|
+
- Added support for loading skills from ~/.config/opencode/skills/ and .opencode/skills/
|
|
12
|
+
- Added support for loading slash commands from ~/.config/opencode/commands/ and .opencode/commands/
|
|
13
|
+
- Added support for loading extension modules (plugins) from ~/.config/opencode/plugins/ and .opencode/plugins/
|
|
14
|
+
- Added support for loading context files (AGENTS.md) from ~/.config/opencode/
|
|
15
|
+
- Added support for loading settings from opencode.json configuration files
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- Improved path display in status line to strip both `/work/` and `~/Projects/` prefixes when abbreviating paths
|
|
20
|
+
- Refactored session directory naming to use single-dash format for home-relative paths and double-dash format for absolute paths, with automatic migration of legacy session directories on first access
|
|
21
|
+
|
|
5
22
|
## [12.8.2] - 2026-02-17
|
|
6
23
|
### Changed
|
|
7
24
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
3
|
-
"version": "12.
|
|
3
|
+
"version": "12.9.0",
|
|
4
4
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -84,12 +84,12 @@
|
|
|
84
84
|
},
|
|
85
85
|
"dependencies": {
|
|
86
86
|
"@mozilla/readability": "0.6.0",
|
|
87
|
-
"@oh-my-pi/omp-stats": "12.
|
|
88
|
-
"@oh-my-pi/pi-agent-core": "12.
|
|
89
|
-
"@oh-my-pi/pi-ai": "12.
|
|
90
|
-
"@oh-my-pi/pi-natives": "12.
|
|
91
|
-
"@oh-my-pi/pi-tui": "12.
|
|
92
|
-
"@oh-my-pi/pi-utils": "12.
|
|
87
|
+
"@oh-my-pi/omp-stats": "12.9.0",
|
|
88
|
+
"@oh-my-pi/pi-agent-core": "12.9.0",
|
|
89
|
+
"@oh-my-pi/pi-ai": "12.9.0",
|
|
90
|
+
"@oh-my-pi/pi-natives": "12.9.0",
|
|
91
|
+
"@oh-my-pi/pi-tui": "12.9.0",
|
|
92
|
+
"@oh-my-pi/pi-utils": "12.9.0",
|
|
93
93
|
"@sinclair/typebox": "^0.34.48",
|
|
94
94
|
"@xterm/headless": "^6.0.0",
|
|
95
95
|
"ajv": "^8.18.0",
|
package/src/discovery/helpers.ts
CHANGED
|
@@ -60,6 +60,11 @@ export const SOURCE_PATHS = {
|
|
|
60
60
|
userAgent: ".gemini",
|
|
61
61
|
projectDir: ".gemini",
|
|
62
62
|
},
|
|
63
|
+
opencode: {
|
|
64
|
+
userBase: ".config/opencode",
|
|
65
|
+
userAgent: ".config/opencode",
|
|
66
|
+
projectDir: ".opencode",
|
|
67
|
+
},
|
|
63
68
|
cursor: {
|
|
64
69
|
userBase: ".cursor",
|
|
65
70
|
userAgent: ".cursor",
|
package/src/discovery/index.ts
CHANGED
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenCode Discovery Provider
|
|
3
|
+
*
|
|
4
|
+
* Loads configuration from OpenCode's config directories:
|
|
5
|
+
* - User: ~/.config/opencode/
|
|
6
|
+
* - Project: .opencode/ (cwd) and opencode.json (project root)
|
|
7
|
+
*
|
|
8
|
+
* Capabilities:
|
|
9
|
+
* - context-files: AGENTS.md (user-level only at ~/.config/opencode/AGENTS.md)
|
|
10
|
+
* - mcps: From opencode.json "mcp" key
|
|
11
|
+
* - settings: From opencode.json
|
|
12
|
+
* - skills: From skills/ subdirectories
|
|
13
|
+
* - slash-commands: From commands/ subdirectories
|
|
14
|
+
* - extension-modules: From plugins/ subdirectories
|
|
15
|
+
*
|
|
16
|
+
* Priority: 55 (tool-specific provider)
|
|
17
|
+
*/
|
|
18
|
+
import * as path from "node:path";
|
|
19
|
+
import { logger } from "@oh-my-pi/pi-utils";
|
|
20
|
+
import { registerProvider } from "../capability";
|
|
21
|
+
import { type ContextFile, contextFileCapability } from "../capability/context-file";
|
|
22
|
+
import { type ExtensionModule, extensionModuleCapability } from "../capability/extension-module";
|
|
23
|
+
import { readFile } from "../capability/fs";
|
|
24
|
+
import { type MCPServer, mcpCapability } from "../capability/mcp";
|
|
25
|
+
import { type Settings, settingsCapability } from "../capability/settings";
|
|
26
|
+
import { type Skill, skillCapability } from "../capability/skill";
|
|
27
|
+
import { type SlashCommand, slashCommandCapability } from "../capability/slash-command";
|
|
28
|
+
import type { LoadContext, LoadResult, SourceMeta } from "../capability/types";
|
|
29
|
+
import { parseFrontmatter } from "../utils/frontmatter";
|
|
30
|
+
import {
|
|
31
|
+
createSourceMeta,
|
|
32
|
+
discoverExtensionModulePaths,
|
|
33
|
+
expandEnvVarsDeep,
|
|
34
|
+
getExtensionNameFromPath,
|
|
35
|
+
getProjectPath,
|
|
36
|
+
getUserPath,
|
|
37
|
+
loadFilesFromDir,
|
|
38
|
+
loadSkillsFromDir,
|
|
39
|
+
parseJSON,
|
|
40
|
+
} from "./helpers";
|
|
41
|
+
|
|
42
|
+
const PROVIDER_ID = "opencode";
|
|
43
|
+
const DISPLAY_NAME = "OpenCode";
|
|
44
|
+
const PRIORITY = 55;
|
|
45
|
+
|
|
46
|
+
// =============================================================================
|
|
47
|
+
// JSON Config Loading
|
|
48
|
+
// =============================================================================
|
|
49
|
+
|
|
50
|
+
async function loadJsonConfig(configPath: string): Promise<Record<string, unknown> | null> {
|
|
51
|
+
const content = await readFile(configPath);
|
|
52
|
+
if (!content) return null;
|
|
53
|
+
|
|
54
|
+
const parsed = parseJSON<Record<string, unknown>>(content);
|
|
55
|
+
if (!parsed) {
|
|
56
|
+
logger.warn("Failed to parse OpenCode JSON config", { path: configPath });
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
return parsed;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// =============================================================================
|
|
63
|
+
// Context Files (AGENTS.md)
|
|
64
|
+
// =============================================================================
|
|
65
|
+
|
|
66
|
+
async function loadContextFiles(ctx: LoadContext): Promise<LoadResult<ContextFile>> {
|
|
67
|
+
const items: ContextFile[] = [];
|
|
68
|
+
const warnings: string[] = [];
|
|
69
|
+
|
|
70
|
+
// User-level only: ~/.config/opencode/AGENTS.md
|
|
71
|
+
const userAgentsMd = getUserPath(ctx, "opencode", "AGENTS.md");
|
|
72
|
+
if (userAgentsMd) {
|
|
73
|
+
const content = await readFile(userAgentsMd);
|
|
74
|
+
if (content) {
|
|
75
|
+
items.push({
|
|
76
|
+
path: userAgentsMd,
|
|
77
|
+
content,
|
|
78
|
+
level: "user",
|
|
79
|
+
_source: createSourceMeta(PROVIDER_ID, userAgentsMd, "user"),
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return { items, warnings };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// =============================================================================
|
|
88
|
+
// MCP Servers (opencode.json → mcp)
|
|
89
|
+
// =============================================================================
|
|
90
|
+
|
|
91
|
+
/** OpenCode MCP server config (from opencode.json "mcp" key) */
|
|
92
|
+
interface OpenCodeMCPConfig {
|
|
93
|
+
type?: "local" | "remote";
|
|
94
|
+
command?: string;
|
|
95
|
+
args?: string[];
|
|
96
|
+
env?: Record<string, string>;
|
|
97
|
+
url?: string;
|
|
98
|
+
headers?: Record<string, string>;
|
|
99
|
+
enabled?: boolean;
|
|
100
|
+
timeout?: number;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function loadMCPServers(ctx: LoadContext): Promise<LoadResult<MCPServer>> {
|
|
104
|
+
const items: MCPServer[] = [];
|
|
105
|
+
const warnings: string[] = [];
|
|
106
|
+
|
|
107
|
+
// User-level: ~/.config/opencode/opencode.json
|
|
108
|
+
const userConfigPath = getUserPath(ctx, "opencode", "opencode.json");
|
|
109
|
+
if (userConfigPath) {
|
|
110
|
+
const config = await loadJsonConfig(userConfigPath);
|
|
111
|
+
if (config) {
|
|
112
|
+
const result = extractMCPServers(config, userConfigPath, "user");
|
|
113
|
+
items.push(...result.items);
|
|
114
|
+
if (result.warnings) warnings.push(...result.warnings);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Project-level: opencode.json in project root
|
|
119
|
+
const projectConfigPath = path.join(ctx.cwd, "opencode.json");
|
|
120
|
+
const projectConfig = await loadJsonConfig(projectConfigPath);
|
|
121
|
+
if (projectConfig) {
|
|
122
|
+
const result = extractMCPServers(projectConfig, projectConfigPath, "project");
|
|
123
|
+
items.push(...result.items);
|
|
124
|
+
if (result.warnings) warnings.push(...result.warnings);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return { items, warnings };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function extractMCPServers(
|
|
131
|
+
config: Record<string, unknown>,
|
|
132
|
+
configPath: string,
|
|
133
|
+
level: "user" | "project",
|
|
134
|
+
): LoadResult<MCPServer> {
|
|
135
|
+
const items: MCPServer[] = [];
|
|
136
|
+
const warnings: string[] = [];
|
|
137
|
+
|
|
138
|
+
if (!config.mcp || typeof config.mcp !== "object") {
|
|
139
|
+
return { items, warnings };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const servers = expandEnvVarsDeep(config.mcp as Record<string, unknown>);
|
|
143
|
+
|
|
144
|
+
for (const [name, raw] of Object.entries(servers)) {
|
|
145
|
+
if (!raw || typeof raw !== "object") {
|
|
146
|
+
warnings.push(`Invalid MCP config for "${name}" in ${configPath}`);
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const serverConfig = raw as OpenCodeMCPConfig;
|
|
151
|
+
|
|
152
|
+
// Determine transport from OpenCode's "type" field
|
|
153
|
+
let transport: "stdio" | "sse" | "http" | undefined;
|
|
154
|
+
if (serverConfig.type === "local") {
|
|
155
|
+
transport = "stdio";
|
|
156
|
+
} else if (serverConfig.type === "remote") {
|
|
157
|
+
transport = "http";
|
|
158
|
+
} else if (serverConfig.url) {
|
|
159
|
+
transport = "http";
|
|
160
|
+
} else if (serverConfig.command) {
|
|
161
|
+
transport = "stdio";
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
items.push({
|
|
165
|
+
name,
|
|
166
|
+
command: serverConfig.command,
|
|
167
|
+
args: Array.isArray(serverConfig.args) ? (serverConfig.args as string[]) : undefined,
|
|
168
|
+
env: serverConfig.env && typeof serverConfig.env === "object" ? serverConfig.env : undefined,
|
|
169
|
+
url: typeof serverConfig.url === "string" ? serverConfig.url : undefined,
|
|
170
|
+
headers: serverConfig.headers && typeof serverConfig.headers === "object" ? serverConfig.headers : undefined,
|
|
171
|
+
enabled: serverConfig.enabled,
|
|
172
|
+
timeout: typeof serverConfig.timeout === "number" ? serverConfig.timeout : undefined,
|
|
173
|
+
transport,
|
|
174
|
+
_source: createSourceMeta(PROVIDER_ID, configPath, level),
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return { items, warnings };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// =============================================================================
|
|
182
|
+
// Skills (skills/)
|
|
183
|
+
// =============================================================================
|
|
184
|
+
|
|
185
|
+
async function loadSkills(ctx: LoadContext): Promise<LoadResult<Skill>> {
|
|
186
|
+
const userSkillsDir = getUserPath(ctx, "opencode", "skills");
|
|
187
|
+
const projectSkillsDir = getProjectPath(ctx, "opencode", "skills");
|
|
188
|
+
|
|
189
|
+
const promises: Promise<LoadResult<Skill>>[] = [];
|
|
190
|
+
|
|
191
|
+
if (userSkillsDir) {
|
|
192
|
+
promises.push(
|
|
193
|
+
loadSkillsFromDir(ctx, {
|
|
194
|
+
dir: userSkillsDir,
|
|
195
|
+
providerId: PROVIDER_ID,
|
|
196
|
+
level: "user",
|
|
197
|
+
}),
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (projectSkillsDir) {
|
|
202
|
+
promises.push(
|
|
203
|
+
loadSkillsFromDir(ctx, {
|
|
204
|
+
dir: projectSkillsDir,
|
|
205
|
+
providerId: PROVIDER_ID,
|
|
206
|
+
level: "project",
|
|
207
|
+
}),
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const results = await Promise.all(promises);
|
|
212
|
+
const items = results.flatMap(r => r.items);
|
|
213
|
+
const warnings = results.flatMap(r => r.warnings || []);
|
|
214
|
+
|
|
215
|
+
return { items, warnings };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// =============================================================================
|
|
219
|
+
// Extension Modules (plugins/)
|
|
220
|
+
// =============================================================================
|
|
221
|
+
|
|
222
|
+
async function loadExtensionModules(ctx: LoadContext): Promise<LoadResult<ExtensionModule>> {
|
|
223
|
+
const userPluginsDir = getUserPath(ctx, "opencode", "plugins");
|
|
224
|
+
const projectPluginsDir = getProjectPath(ctx, "opencode", "plugins");
|
|
225
|
+
|
|
226
|
+
const [userPaths, projectPaths] = await Promise.all([
|
|
227
|
+
userPluginsDir ? discoverExtensionModulePaths(ctx, userPluginsDir) : Promise.resolve([]),
|
|
228
|
+
projectPluginsDir ? discoverExtensionModulePaths(ctx, projectPluginsDir) : Promise.resolve([]),
|
|
229
|
+
]);
|
|
230
|
+
|
|
231
|
+
const items: ExtensionModule[] = [
|
|
232
|
+
...userPaths.map(extPath => ({
|
|
233
|
+
name: getExtensionNameFromPath(extPath),
|
|
234
|
+
path: extPath,
|
|
235
|
+
level: "user" as const,
|
|
236
|
+
_source: createSourceMeta(PROVIDER_ID, extPath, "user"),
|
|
237
|
+
})),
|
|
238
|
+
...projectPaths.map(extPath => ({
|
|
239
|
+
name: getExtensionNameFromPath(extPath),
|
|
240
|
+
path: extPath,
|
|
241
|
+
level: "project" as const,
|
|
242
|
+
_source: createSourceMeta(PROVIDER_ID, extPath, "project"),
|
|
243
|
+
})),
|
|
244
|
+
];
|
|
245
|
+
|
|
246
|
+
return { items, warnings: [] };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// =============================================================================
|
|
250
|
+
// Slash Commands (commands/)
|
|
251
|
+
// =============================================================================
|
|
252
|
+
|
|
253
|
+
async function loadSlashCommands(ctx: LoadContext): Promise<LoadResult<SlashCommand>> {
|
|
254
|
+
const userCommandsDir = getUserPath(ctx, "opencode", "commands");
|
|
255
|
+
const projectCommandsDir = getProjectPath(ctx, "opencode", "commands");
|
|
256
|
+
|
|
257
|
+
const transformCommand =
|
|
258
|
+
(level: "user" | "project") => (name: string, content: string, filePath: string, source: SourceMeta) => {
|
|
259
|
+
const { frontmatter, body } = parseFrontmatter(content, { source: filePath });
|
|
260
|
+
const commandName = frontmatter.name || name.replace(/\.md$/, "");
|
|
261
|
+
return {
|
|
262
|
+
name: String(commandName),
|
|
263
|
+
path: filePath,
|
|
264
|
+
content: body,
|
|
265
|
+
level,
|
|
266
|
+
_source: source,
|
|
267
|
+
};
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
const promises: Promise<LoadResult<SlashCommand>>[] = [];
|
|
271
|
+
|
|
272
|
+
if (userCommandsDir) {
|
|
273
|
+
promises.push(
|
|
274
|
+
loadFilesFromDir(ctx, userCommandsDir, PROVIDER_ID, "user", {
|
|
275
|
+
extensions: ["md"],
|
|
276
|
+
transform: transformCommand("user"),
|
|
277
|
+
}),
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (projectCommandsDir) {
|
|
282
|
+
promises.push(
|
|
283
|
+
loadFilesFromDir(ctx, projectCommandsDir, PROVIDER_ID, "project", {
|
|
284
|
+
extensions: ["md"],
|
|
285
|
+
transform: transformCommand("project"),
|
|
286
|
+
}),
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const results = await Promise.all(promises);
|
|
291
|
+
const items = results.flatMap(r => r.items);
|
|
292
|
+
const warnings = results.flatMap(r => r.warnings || []);
|
|
293
|
+
|
|
294
|
+
return { items, warnings };
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// =============================================================================
|
|
298
|
+
// Settings (opencode.json)
|
|
299
|
+
// =============================================================================
|
|
300
|
+
|
|
301
|
+
async function loadSettings(ctx: LoadContext): Promise<LoadResult<Settings>> {
|
|
302
|
+
const items: Settings[] = [];
|
|
303
|
+
const warnings: string[] = [];
|
|
304
|
+
|
|
305
|
+
// User-level: ~/.config/opencode/opencode.json
|
|
306
|
+
const userConfigPath = getUserPath(ctx, "opencode", "opencode.json");
|
|
307
|
+
if (userConfigPath) {
|
|
308
|
+
const content = await readFile(userConfigPath);
|
|
309
|
+
if (content) {
|
|
310
|
+
const parsed = parseJSON<Record<string, unknown>>(content);
|
|
311
|
+
if (parsed) {
|
|
312
|
+
items.push({
|
|
313
|
+
path: userConfigPath,
|
|
314
|
+
data: parsed,
|
|
315
|
+
level: "user",
|
|
316
|
+
_source: createSourceMeta(PROVIDER_ID, userConfigPath, "user"),
|
|
317
|
+
});
|
|
318
|
+
} else {
|
|
319
|
+
warnings.push(`Invalid JSON in ${userConfigPath}`);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Project-level: opencode.json in project root
|
|
325
|
+
const projectConfigPath = path.join(ctx.cwd, "opencode.json");
|
|
326
|
+
const content = await readFile(projectConfigPath);
|
|
327
|
+
if (content) {
|
|
328
|
+
const parsed = parseJSON<Record<string, unknown>>(content);
|
|
329
|
+
if (parsed) {
|
|
330
|
+
items.push({
|
|
331
|
+
path: projectConfigPath,
|
|
332
|
+
data: parsed,
|
|
333
|
+
level: "project",
|
|
334
|
+
_source: createSourceMeta(PROVIDER_ID, projectConfigPath, "project"),
|
|
335
|
+
});
|
|
336
|
+
} else {
|
|
337
|
+
warnings.push(`Invalid JSON in ${projectConfigPath}`);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return { items, warnings };
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// =============================================================================
|
|
345
|
+
// Provider Registration
|
|
346
|
+
// =============================================================================
|
|
347
|
+
|
|
348
|
+
registerProvider(contextFileCapability.id, {
|
|
349
|
+
id: PROVIDER_ID,
|
|
350
|
+
displayName: DISPLAY_NAME,
|
|
351
|
+
description: "Load AGENTS.md from ~/.config/opencode/",
|
|
352
|
+
priority: PRIORITY,
|
|
353
|
+
load: loadContextFiles,
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
registerProvider(mcpCapability.id, {
|
|
357
|
+
id: PROVIDER_ID,
|
|
358
|
+
displayName: DISPLAY_NAME,
|
|
359
|
+
description: "Load MCP servers from opencode.json mcp key",
|
|
360
|
+
priority: PRIORITY,
|
|
361
|
+
load: loadMCPServers,
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
registerProvider(skillCapability.id, {
|
|
365
|
+
id: PROVIDER_ID,
|
|
366
|
+
displayName: DISPLAY_NAME,
|
|
367
|
+
description: "Load skills from ~/.config/opencode/skills/ and .opencode/skills/",
|
|
368
|
+
priority: PRIORITY,
|
|
369
|
+
load: loadSkills,
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
registerProvider(extensionModuleCapability.id, {
|
|
373
|
+
id: PROVIDER_ID,
|
|
374
|
+
displayName: DISPLAY_NAME,
|
|
375
|
+
description: "Load extension modules from ~/.config/opencode/plugins/ and .opencode/plugins/",
|
|
376
|
+
priority: PRIORITY,
|
|
377
|
+
load: loadExtensionModules,
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
registerProvider(slashCommandCapability.id, {
|
|
381
|
+
id: PROVIDER_ID,
|
|
382
|
+
displayName: DISPLAY_NAME,
|
|
383
|
+
description: "Load slash commands from ~/.config/opencode/commands/ and .opencode/commands/",
|
|
384
|
+
priority: PRIORITY,
|
|
385
|
+
load: loadSlashCommands,
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
registerProvider(settingsCapability.id, {
|
|
389
|
+
id: PROVIDER_ID,
|
|
390
|
+
displayName: DISPLAY_NAME,
|
|
391
|
+
description: "Load settings from opencode.json",
|
|
392
|
+
priority: PRIORITY,
|
|
393
|
+
load: loadSettings,
|
|
394
|
+
});
|
|
@@ -5,7 +5,7 @@ import type { AgentTool, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core
|
|
|
5
5
|
import type { Static, TSchema } from "@sinclair/typebox";
|
|
6
6
|
import type { Theme } from "../../modes/theme/theme";
|
|
7
7
|
import { applyToolProxy } from "../tool-proxy";
|
|
8
|
-
import type { CustomTool, CustomToolContext
|
|
8
|
+
import type { CustomTool, CustomToolContext } from "./types";
|
|
9
9
|
|
|
10
10
|
export class CustomToolAdapter<TParams extends TSchema = TSchema, TDetails = any, TTheme extends Theme = Theme>
|
|
11
11
|
implements AgentTool<TParams, TDetails, TTheme>
|
|
@@ -42,14 +42,4 @@ export class CustomToolAdapter<TParams extends TSchema = TSchema, TDetails = any
|
|
|
42
42
|
): AgentTool<TParams, TDetails, TTheme> {
|
|
43
43
|
return new CustomToolAdapter(tool, getContext);
|
|
44
44
|
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Wrap all loaded custom tools into AgentTools.
|
|
48
|
-
*/
|
|
49
|
-
static wrapTools<TParams extends TSchema = TSchema, TDetails = any, TTheme extends Theme = Theme>(
|
|
50
|
-
loadedTools: LoadedCustomTool<TParams, TDetails>[],
|
|
51
|
-
getContext: () => CustomToolContext,
|
|
52
|
-
): AgentTool<TParams, TDetails, TTheme>[] {
|
|
53
|
-
return loadedTools.map(lt => CustomToolAdapter.wrap(lt.tool, getContext));
|
|
54
|
-
}
|
|
55
45
|
}
|
|
@@ -97,8 +97,9 @@ const pathSegment: StatusLineSegment = {
|
|
|
97
97
|
if (opts.abbreviate !== false) {
|
|
98
98
|
pwd = shortenPath(pwd);
|
|
99
99
|
}
|
|
100
|
-
if (opts.stripWorkPrefix !== false
|
|
101
|
-
pwd = pwd.slice(6);
|
|
100
|
+
if (opts.stripWorkPrefix !== false) {
|
|
101
|
+
if (pwd.startsWith("/work/")) pwd = pwd.slice(6);
|
|
102
|
+
else if (pwd.startsWith("~/Projects/")) pwd = pwd.slice(11);
|
|
102
103
|
}
|
|
103
104
|
|
|
104
105
|
const maxLen = opts.maxLength ?? 40;
|
package/src/sdk.ts
CHANGED
|
@@ -79,7 +79,6 @@ import {
|
|
|
79
79
|
} from "./tools";
|
|
80
80
|
import { ToolContextStore } from "./tools/context";
|
|
81
81
|
import { getGeminiImageTools } from "./tools/gemini-image";
|
|
82
|
-
import { wrapToolsWithMetaNotice } from "./tools/output-meta";
|
|
83
82
|
import { EventBus } from "./utils/event-bus";
|
|
84
83
|
import { time } from "./utils/timings";
|
|
85
84
|
|
|
@@ -762,9 +761,8 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
762
761
|
);
|
|
763
762
|
|
|
764
763
|
debugStartup("sdk:createTools:start");
|
|
765
|
-
// Create
|
|
766
|
-
const
|
|
767
|
-
const builtinTools = wrapToolsWithMetaNotice(rawBuiltinTools);
|
|
764
|
+
// Create built-in tools (already wrapped with meta notice formatting)
|
|
765
|
+
const builtinTools = await createTools(toolSession, options.toolNames);
|
|
768
766
|
debugStartup("sdk:createTools");
|
|
769
767
|
time("createAllTools");
|
|
770
768
|
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
|
+
import * as os from "node:os";
|
|
2
3
|
import * as path from "node:path";
|
|
3
4
|
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
|
4
5
|
import type { ImageContent, Message, TextContent, Usage } from "@oh-my-pi/pi-ai";
|
|
6
|
+
import { getTerminalId } from "@oh-my-pi/pi-tui";
|
|
5
7
|
import { isEnoent, logger, parseJsonlLenient, Snowflake } from "@oh-my-pi/pi-utils";
|
|
6
8
|
import { getBlobsDir, getAgentDir as getDefaultAgentDir, getProjectDir } from "@oh-my-pi/pi-utils/dirs";
|
|
7
9
|
import { type BlobPutResult, BlobStore, externalizeImageData, isBlobRef, resolveImageData } from "./blob-store";
|
|
@@ -302,6 +304,67 @@ export function migrateSessionEntries(entries: FileEntry[]): void {
|
|
|
302
304
|
migrateToCurrentVersion(entries);
|
|
303
305
|
}
|
|
304
306
|
|
|
307
|
+
let sessionDirsMigrated = false;
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Migrate old `--<home-encoded>-*--` session dirs to the new `-*` format.
|
|
311
|
+
* Runs once on first access, best-effort.
|
|
312
|
+
*/
|
|
313
|
+
function migrateHomeSessionDirs(): void {
|
|
314
|
+
if (sessionDirsMigrated) return;
|
|
315
|
+
sessionDirsMigrated = true;
|
|
316
|
+
|
|
317
|
+
const home = os.homedir();
|
|
318
|
+
const homeEncoded = home.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-");
|
|
319
|
+
const oldPrefix = `--${homeEncoded}-`;
|
|
320
|
+
const oldExact = `--${homeEncoded}--`;
|
|
321
|
+
const sessionsRoot = path.join(getDefaultAgentDir(), "sessions");
|
|
322
|
+
|
|
323
|
+
let entries: string[];
|
|
324
|
+
try {
|
|
325
|
+
entries = fs.readdirSync(sessionsRoot);
|
|
326
|
+
} catch {
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
for (const entry of entries) {
|
|
331
|
+
let remainder: string;
|
|
332
|
+
if (entry === oldExact) {
|
|
333
|
+
remainder = "";
|
|
334
|
+
} else if (entry.startsWith(oldPrefix) && entry.endsWith("--")) {
|
|
335
|
+
remainder = entry.slice(oldPrefix.length, -2);
|
|
336
|
+
} else {
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const newName = `-${remainder}`;
|
|
341
|
+
const oldPath = path.join(sessionsRoot, entry);
|
|
342
|
+
const newPath = path.join(sessionsRoot, newName);
|
|
343
|
+
|
|
344
|
+
try {
|
|
345
|
+
const existing = fs.statSync(newPath, { throwIfNoEntry: false });
|
|
346
|
+
if (existing?.isDirectory()) {
|
|
347
|
+
// Merge files from old dir into existing new dir
|
|
348
|
+
for (const file of fs.readdirSync(oldPath)) {
|
|
349
|
+
const src = path.join(oldPath, file);
|
|
350
|
+
const dst = path.join(newPath, file);
|
|
351
|
+
if (!fs.existsSync(dst)) {
|
|
352
|
+
fs.renameSync(src, dst);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
fs.rmSync(oldPath, { recursive: true, force: true });
|
|
356
|
+
} else {
|
|
357
|
+
if (existing) {
|
|
358
|
+
fs.rmSync(newPath, { recursive: true, force: true });
|
|
359
|
+
}
|
|
360
|
+
fs.renameSync(oldPath, newPath);
|
|
361
|
+
}
|
|
362
|
+
} catch {
|
|
363
|
+
// Best effort
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
305
368
|
/** Exported for compaction.test.ts */
|
|
306
369
|
export function parseSessionEntries(content: string): FileEntry[] {
|
|
307
370
|
return parseJsonlLenient<FileEntry>(content);
|
|
@@ -459,13 +522,27 @@ export function buildSessionContext(
|
|
|
459
522
|
return { messages, thinkingLevel, models, injectedTtsrRules, mode, modeData };
|
|
460
523
|
}
|
|
461
524
|
|
|
525
|
+
/**
|
|
526
|
+
* Encode a cwd into a safe directory name for session storage.
|
|
527
|
+
* Home-relative paths use single-dash format: `/Users/x/Projects/pi` → `-Projects-pi`
|
|
528
|
+
* Absolute paths use double-dash format: `/tmp/foo` → `--tmp-foo--`
|
|
529
|
+
*/
|
|
530
|
+
function encodeSessionDirName(cwd: string): string {
|
|
531
|
+
const home = os.homedir();
|
|
532
|
+
if (cwd === home || cwd.startsWith(`${home}/`) || cwd.startsWith(`${home}\\`)) {
|
|
533
|
+
const relative = cwd.slice(home.length).replace(/^[/\\]/, "");
|
|
534
|
+
return `-${relative.replace(/[/\\:]/g, "-")}`;
|
|
535
|
+
}
|
|
536
|
+
return `--${cwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`;
|
|
537
|
+
}
|
|
462
538
|
/**
|
|
463
539
|
* Compute the default session directory for a cwd.
|
|
464
540
|
* Encodes cwd into a safe directory name under ~/.omp/agent/sessions/.
|
|
465
541
|
*/
|
|
466
542
|
function getDefaultSessionDir(cwd: string, storage: SessionStorage): string {
|
|
467
|
-
|
|
468
|
-
const
|
|
543
|
+
migrateHomeSessionDirs();
|
|
544
|
+
const dirName = encodeSessionDirName(cwd);
|
|
545
|
+
const sessionDir = path.join(getDefaultAgentDir(), "sessions", dirName);
|
|
469
546
|
storage.ensureDirSync(sessionDir);
|
|
470
547
|
return sessionDir;
|
|
471
548
|
}
|
|
@@ -476,40 +553,6 @@ function getDefaultSessionDir(cwd: string, storage: SessionStorage): string {
|
|
|
476
553
|
|
|
477
554
|
const TERMINAL_SESSIONS_DIR = "terminal-sessions";
|
|
478
555
|
|
|
479
|
-
/**
|
|
480
|
-
* Get a stable identifier for the current terminal.
|
|
481
|
-
* Uses the TTY device path (e.g., /dev/pts/3), falling back to environment
|
|
482
|
-
* variables for terminal multiplexers or terminal emulators.
|
|
483
|
-
* Returns null if no terminal can be identified (e.g., piped input).
|
|
484
|
-
*/
|
|
485
|
-
function getTerminalId(): string | null {
|
|
486
|
-
// TTY device path — most reliable, unique per terminal tab
|
|
487
|
-
if (process.stdin.isTTY) {
|
|
488
|
-
try {
|
|
489
|
-
// On Linux/macOS, /proc/self/fd/0 -> /dev/pts/N
|
|
490
|
-
const ttyPath = fs.readlinkSync("/proc/self/fd/0");
|
|
491
|
-
if (ttyPath.startsWith("/dev/")) {
|
|
492
|
-
return ttyPath.slice(5).replace(/\//g, "-"); // /dev/pts/3 -> pts-3
|
|
493
|
-
}
|
|
494
|
-
} catch {}
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
// Fallback to terminal-specific env vars
|
|
498
|
-
const kittyId = process.env.KITTY_WINDOW_ID;
|
|
499
|
-
if (kittyId) return `kitty-${kittyId}`;
|
|
500
|
-
|
|
501
|
-
const tmuxPane = process.env.TMUX_PANE;
|
|
502
|
-
if (tmuxPane) return `tmux-${tmuxPane}`;
|
|
503
|
-
|
|
504
|
-
const terminalSessionId = process.env.TERM_SESSION_ID; // macOS Terminal.app
|
|
505
|
-
if (terminalSessionId) return `apple-${terminalSessionId}`;
|
|
506
|
-
|
|
507
|
-
const wtSession = process.env.WT_SESSION; // Windows Terminal
|
|
508
|
-
if (wtSession) return `wt-${wtSession}`;
|
|
509
|
-
|
|
510
|
-
return null;
|
|
511
|
-
}
|
|
512
|
-
|
|
513
556
|
/**
|
|
514
557
|
* Write a breadcrumb linking the current terminal to a session file.
|
|
515
558
|
* The breadcrumb contains the cwd and session path so --continue can
|
package/src/tools/fetch.ts
CHANGED
|
@@ -24,12 +24,13 @@ import { allocateOutputArtifact } from "./output-utils";
|
|
|
24
24
|
import { formatExpandHint } from "./render-utils";
|
|
25
25
|
import { ToolAbortError } from "./tool-errors";
|
|
26
26
|
import { toolResult } from "./tool-result";
|
|
27
|
-
import { DEFAULT_MAX_BYTES,
|
|
27
|
+
import { DEFAULT_MAX_BYTES, truncateHead } from "./truncate";
|
|
28
28
|
|
|
29
29
|
// =============================================================================
|
|
30
30
|
// Types and Constants
|
|
31
31
|
// =============================================================================
|
|
32
32
|
|
|
33
|
+
const FETCH_DEFAULT_MAX_LINES = 300;
|
|
33
34
|
// Convertible document types (markitdown supported)
|
|
34
35
|
const CONVERTIBLE_MIMES = new Set([
|
|
35
36
|
"application/pdf",
|
|
@@ -878,7 +879,10 @@ export class FetchTool implements AgentTool<typeof fetchSchema, FetchToolDetails
|
|
|
878
879
|
}
|
|
879
880
|
|
|
880
881
|
const result = await renderUrl(url, effectiveTimeout, raw, signal);
|
|
881
|
-
const truncation = truncateHead(result.content, {
|
|
882
|
+
const truncation = truncateHead(result.content, {
|
|
883
|
+
maxBytes: DEFAULT_MAX_BYTES,
|
|
884
|
+
maxLines: FETCH_DEFAULT_MAX_LINES,
|
|
885
|
+
});
|
|
882
886
|
const needsArtifact = truncation.truncated;
|
|
883
887
|
let artifactId: string | undefined;
|
|
884
888
|
|
package/src/tools/index.ts
CHANGED
|
@@ -24,7 +24,7 @@ import { FetchTool } from "./fetch";
|
|
|
24
24
|
import { FindTool } from "./find";
|
|
25
25
|
import { GrepTool } from "./grep";
|
|
26
26
|
import { NotebookTool } from "./notebook";
|
|
27
|
-
import {
|
|
27
|
+
import { wrapToolWithMetaNotice } from "./output-meta";
|
|
28
28
|
import { PythonTool } from "./python";
|
|
29
29
|
import { ReadTool } from "./read";
|
|
30
30
|
import { reportFindingTool } from "./review";
|
|
@@ -328,7 +328,7 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
|
|
|
328
328
|
logger.debug("Tool factory timings", { slowTools });
|
|
329
329
|
}
|
|
330
330
|
const tools = results.filter(r => r.tool !== null).map(r => r.tool as Tool);
|
|
331
|
-
const wrappedTools =
|
|
331
|
+
const wrappedTools = tools.map(wrapToolWithMetaNotice);
|
|
332
332
|
|
|
333
333
|
if (filteredRequestedTools !== undefined) {
|
|
334
334
|
const allowed = new Set(filteredRequestedTools);
|
package/src/tools/output-meta.ts
CHANGED
|
@@ -4,7 +4,13 @@
|
|
|
4
4
|
* Tools populate details.meta using the fluent OutputMetaBuilder.
|
|
5
5
|
* The tool wrapper automatically formats and appends notices at message boundary.
|
|
6
6
|
*/
|
|
7
|
-
import type {
|
|
7
|
+
import type {
|
|
8
|
+
AgentTool,
|
|
9
|
+
AgentToolContext,
|
|
10
|
+
AgentToolExecFn,
|
|
11
|
+
AgentToolResult,
|
|
12
|
+
AgentToolUpdateCallback,
|
|
13
|
+
} from "@oh-my-pi/pi-agent-core";
|
|
8
14
|
import type { ImageContent, TextContent } from "@oh-my-pi/pi-ai";
|
|
9
15
|
import type { OutputSummary } from "../session/streaming-output";
|
|
10
16
|
import { renderError } from "./tool-errors";
|
|
@@ -335,7 +341,8 @@ export function formatOutputNotice(meta: OutputMeta | undefined): string {
|
|
|
335
341
|
|
|
336
342
|
if (t.nextOffset != null) {
|
|
337
343
|
notice += `. Use offset=${t.nextOffset} to continue`;
|
|
338
|
-
}
|
|
344
|
+
}
|
|
345
|
+
if (t.artifactId != null) {
|
|
339
346
|
notice += `. Full: artifact://${t.artifactId}`;
|
|
340
347
|
}
|
|
341
348
|
|
|
@@ -396,31 +403,21 @@ function appendOutputNotice(
|
|
|
396
403
|
return result;
|
|
397
404
|
}
|
|
398
405
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
signal,
|
|
411
|
-
onUpdate,
|
|
412
|
-
context,
|
|
413
|
-
): Promise<AgentToolResult<any, any>> => {
|
|
414
|
-
let result: AgentToolResult<any, any>;
|
|
415
|
-
|
|
416
|
-
try {
|
|
417
|
-
result = await originalExecute(toolCallId, params, signal, onUpdate, context);
|
|
418
|
-
} catch (e) {
|
|
419
|
-
// Re-throw with formatted message so agent-loop sets isError flag
|
|
420
|
-
throw new Error(renderError(e));
|
|
421
|
-
}
|
|
406
|
+
const kUnwrappedExecute = Symbol("OutputMeta.UnwrappedExecute");
|
|
407
|
+
|
|
408
|
+
async function wrappedExecute(
|
|
409
|
+
this: AgentTool & { [kUnwrappedExecute]: AgentToolExecFn },
|
|
410
|
+
toolCallId: string,
|
|
411
|
+
params: any,
|
|
412
|
+
signal?: AbortSignal,
|
|
413
|
+
onUpdate?: AgentToolUpdateCallback,
|
|
414
|
+
context?: AgentToolContext,
|
|
415
|
+
): Promise<AgentToolResult> {
|
|
416
|
+
const originalExecute = this[kUnwrappedExecute];
|
|
422
417
|
|
|
418
|
+
try {
|
|
423
419
|
// Append notices from meta
|
|
420
|
+
const result = await originalExecute.call(this, toolCallId, params, signal, onUpdate, context);
|
|
424
421
|
const meta = (result.details as { meta?: OutputMeta } | undefined)?.meta;
|
|
425
422
|
if (meta) {
|
|
426
423
|
return {
|
|
@@ -428,26 +425,36 @@ export function wrapToolWithMetaNotice<T extends AgentTool<any, any, any>>(tool:
|
|
|
428
425
|
content: appendOutputNotice(result.content, meta) as (TextContent | ImageContent)[],
|
|
429
426
|
};
|
|
430
427
|
}
|
|
431
|
-
|
|
432
428
|
return result;
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
// private fields on tools like EditTool.
|
|
438
|
-
return new Proxy(tool, {
|
|
439
|
-
get(target, prop) {
|
|
440
|
-
if (prop === "execute") return wrappedExecute;
|
|
441
|
-
const value = (target as Record<string | symbol, unknown>)[prop];
|
|
442
|
-
if (typeof value === "function") return value.bind(target);
|
|
443
|
-
return value;
|
|
444
|
-
},
|
|
445
|
-
}) as T;
|
|
429
|
+
} catch (e) {
|
|
430
|
+
// Re-throw with formatted message so agent-loop sets isError flag
|
|
431
|
+
throw new Error(renderError(e));
|
|
432
|
+
}
|
|
446
433
|
}
|
|
447
434
|
|
|
448
435
|
/**
|
|
449
|
-
* Wrap
|
|
436
|
+
* Wrap a tool to:
|
|
437
|
+
* 1. Automatically append output notices based on details.meta
|
|
438
|
+
* 2. Handle ToolError rendering
|
|
450
439
|
*/
|
|
451
|
-
export function
|
|
452
|
-
|
|
440
|
+
export function wrapToolWithMetaNotice<T extends AgentTool<any, any, any>>(tool: T): T {
|
|
441
|
+
if (kUnwrappedExecute in tool) {
|
|
442
|
+
return tool;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const originalExecute = tool.execute;
|
|
446
|
+
|
|
447
|
+
return Object.defineProperties(tool, {
|
|
448
|
+
[kUnwrappedExecute]: {
|
|
449
|
+
value: originalExecute,
|
|
450
|
+
enumerable: false,
|
|
451
|
+
configurable: true,
|
|
452
|
+
},
|
|
453
|
+
execute: {
|
|
454
|
+
value: wrappedExecute,
|
|
455
|
+
enumerable: false,
|
|
456
|
+
configurable: true,
|
|
457
|
+
writable: true,
|
|
458
|
+
},
|
|
459
|
+
});
|
|
453
460
|
}
|