@oh-my-pi/pi-coding-agent 12.8.2 → 12.10.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 +57 -0
- package/package.json +7 -7
- package/src/capability/rule.ts +173 -5
- package/src/cli/args.ts +3 -0
- package/src/cli/update-cli.ts +1 -66
- package/src/commands/launch.ts +3 -0
- package/src/config/model-registry.ts +300 -21
- package/src/config/model-resolver.ts +4 -4
- package/src/config/settings-schema.ts +12 -0
- package/src/discovery/agents.ts +175 -12
- package/src/discovery/builtin.ts +3 -13
- package/src/discovery/cline.ts +4 -45
- package/src/discovery/cursor.ts +2 -29
- package/src/discovery/helpers.ts +43 -0
- package/src/discovery/index.ts +1 -0
- package/src/discovery/opencode.ts +394 -0
- package/src/discovery/windsurf.ts +5 -44
- package/src/export/ttsr.ts +324 -54
- package/src/extensibility/custom-tools/wrapper.ts +1 -11
- package/src/internal-urls/index.ts +4 -2
- package/src/internal-urls/memory-protocol.ts +133 -0
- package/src/internal-urls/router.ts +4 -2
- package/src/internal-urls/skill-protocol.ts +1 -1
- package/src/internal-urls/types.ts +6 -2
- package/src/main.ts +5 -0
- package/src/memories/index.ts +6 -13
- package/src/modes/components/settings-defs.ts +6 -0
- package/src/modes/components/status-line/segments.ts +3 -2
- package/src/modes/rpc/rpc-client.ts +16 -0
- package/src/prompts/memories/consolidation.md +1 -1
- package/src/prompts/memories/read_path.md +4 -4
- package/src/prompts/memories/stage_one_input.md +1 -2
- package/src/prompts/tools/bash.md +10 -23
- package/src/prompts/tools/read.md +2 -0
- package/src/sdk.ts +25 -10
- package/src/session/agent-session.ts +252 -44
- package/src/session/session-manager.ts +79 -36
- package/src/tools/bash-skill-urls.ts +177 -0
- package/src/tools/bash.ts +7 -1
- package/src/tools/fetch.ts +6 -2
- package/src/tools/index.ts +2 -2
- package/src/tools/output-meta.ts +49 -42
- package/src/tools/read.ts +2 -2
package/src/discovery/cline.ts
CHANGED
|
@@ -10,8 +10,7 @@ import { readDirEntries, readFile } from "../capability/fs";
|
|
|
10
10
|
import type { Rule } from "../capability/rule";
|
|
11
11
|
import { ruleCapability } from "../capability/rule";
|
|
12
12
|
import type { LoadContext, LoadResult } from "../capability/types";
|
|
13
|
-
import {
|
|
14
|
-
import { createSourceMeta, loadFilesFromDir } from "./helpers";
|
|
13
|
+
import { buildRuleFromMarkdown, createSourceMeta, loadFilesFromDir } from "./helpers";
|
|
15
14
|
|
|
16
15
|
const PROVIDER_ID = "cline";
|
|
17
16
|
const DISPLAY_NAME = "Cline";
|
|
@@ -53,29 +52,8 @@ async function loadRules(ctx: LoadContext): Promise<LoadResult<Rule>> {
|
|
|
53
52
|
// Directory format: load all *.md files
|
|
54
53
|
const result = await loadFilesFromDir(ctx, found.path, PROVIDER_ID, "project", {
|
|
55
54
|
extensions: ["md"],
|
|
56
|
-
transform: (name, content, path, source) =>
|
|
57
|
-
|
|
58
|
-
const ruleName = name.replace(/\.md$/, "");
|
|
59
|
-
|
|
60
|
-
// Parse globs (can be array or single string)
|
|
61
|
-
let globs: string[] | undefined;
|
|
62
|
-
if (Array.isArray(frontmatter.globs)) {
|
|
63
|
-
globs = frontmatter.globs.filter((g): g is string => typeof g === "string");
|
|
64
|
-
} else if (typeof frontmatter.globs === "string") {
|
|
65
|
-
globs = [frontmatter.globs];
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
return {
|
|
69
|
-
name: ruleName,
|
|
70
|
-
path,
|
|
71
|
-
content: body,
|
|
72
|
-
globs,
|
|
73
|
-
alwaysApply: typeof frontmatter.alwaysApply === "boolean" ? frontmatter.alwaysApply : undefined,
|
|
74
|
-
description: typeof frontmatter.description === "string" ? frontmatter.description : undefined,
|
|
75
|
-
ttsrTrigger: typeof frontmatter.ttsr_trigger === "string" ? frontmatter.ttsr_trigger : undefined,
|
|
76
|
-
_source: source,
|
|
77
|
-
};
|
|
78
|
-
},
|
|
55
|
+
transform: (name, content, path, source) =>
|
|
56
|
+
buildRuleFromMarkdown(name, content, path, source, { stripNamePattern: /\.md$/ }),
|
|
79
57
|
});
|
|
80
58
|
|
|
81
59
|
items.push(...result.items);
|
|
@@ -88,27 +66,8 @@ async function loadRules(ctx: LoadContext): Promise<LoadResult<Rule>> {
|
|
|
88
66
|
return { items, warnings };
|
|
89
67
|
}
|
|
90
68
|
|
|
91
|
-
const { frontmatter, body } = parseFrontmatter(content, { source: found.path });
|
|
92
69
|
const source = createSourceMeta(PROVIDER_ID, found.path, "project");
|
|
93
|
-
|
|
94
|
-
// Parse globs (can be array or single string)
|
|
95
|
-
let globs: string[] | undefined;
|
|
96
|
-
if (Array.isArray(frontmatter.globs)) {
|
|
97
|
-
globs = frontmatter.globs.filter((g): g is string => typeof g === "string");
|
|
98
|
-
} else if (typeof frontmatter.globs === "string") {
|
|
99
|
-
globs = [frontmatter.globs];
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
items.push({
|
|
103
|
-
name: "clinerules",
|
|
104
|
-
path: found.path,
|
|
105
|
-
content: body,
|
|
106
|
-
globs,
|
|
107
|
-
alwaysApply: typeof frontmatter.alwaysApply === "boolean" ? frontmatter.alwaysApply : undefined,
|
|
108
|
-
description: typeof frontmatter.description === "string" ? frontmatter.description : undefined,
|
|
109
|
-
ttsrTrigger: typeof frontmatter.ttsr_trigger === "string" ? frontmatter.ttsr_trigger : undefined,
|
|
110
|
-
_source: source,
|
|
111
|
-
});
|
|
70
|
+
items.push(buildRuleFromMarkdown("clinerules.md", content, found.path, source, { ruleName: "clinerules" }));
|
|
112
71
|
}
|
|
113
72
|
|
|
114
73
|
return { items, warnings };
|
package/src/discovery/cursor.ts
CHANGED
|
@@ -21,8 +21,8 @@ import { ruleCapability } from "../capability/rule";
|
|
|
21
21
|
import type { Settings } from "../capability/settings";
|
|
22
22
|
import { settingsCapability } from "../capability/settings";
|
|
23
23
|
import type { LoadContext, LoadResult, SourceMeta } from "../capability/types";
|
|
24
|
-
import { parseFrontmatter } from "../utils/frontmatter";
|
|
25
24
|
import {
|
|
25
|
+
buildRuleFromMarkdown,
|
|
26
26
|
createSourceMeta,
|
|
27
27
|
expandEnvVarsDeep,
|
|
28
28
|
getProjectPath,
|
|
@@ -137,34 +137,7 @@ async function loadRules(ctx: LoadContext): Promise<LoadResult<Rule>> {
|
|
|
137
137
|
}
|
|
138
138
|
|
|
139
139
|
function transformMDCRule(name: string, content: string, path: string, source: SourceMeta): Rule {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
// Extract frontmatter fields
|
|
143
|
-
const description = typeof frontmatter.description === "string" ? frontmatter.description : undefined;
|
|
144
|
-
const alwaysApply = frontmatter.alwaysApply === true;
|
|
145
|
-
const ttsrTrigger = typeof frontmatter.ttsr_trigger === "string" ? frontmatter.ttsr_trigger : undefined;
|
|
146
|
-
|
|
147
|
-
// Parse globs (can be array or single string)
|
|
148
|
-
let globs: string[] | undefined;
|
|
149
|
-
if (Array.isArray(frontmatter.globs)) {
|
|
150
|
-
globs = frontmatter.globs.filter((g): g is string => typeof g === "string");
|
|
151
|
-
} else if (typeof frontmatter.globs === "string") {
|
|
152
|
-
globs = [frontmatter.globs];
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// Derive name from filename (strip extension)
|
|
156
|
-
const ruleName = name.replace(/\.(mdc|md)$/, "");
|
|
157
|
-
|
|
158
|
-
return {
|
|
159
|
-
name: ruleName,
|
|
160
|
-
path,
|
|
161
|
-
content: body,
|
|
162
|
-
description,
|
|
163
|
-
alwaysApply,
|
|
164
|
-
globs,
|
|
165
|
-
ttsrTrigger,
|
|
166
|
-
_source: source,
|
|
167
|
-
};
|
|
140
|
+
return buildRuleFromMarkdown(name, content, path, source, { stripNamePattern: /\.(mdc|md)$/ });
|
|
168
141
|
}
|
|
169
142
|
|
|
170
143
|
// =============================================================================
|
package/src/discovery/helpers.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
|
7
7
|
import { FileType, glob } from "@oh-my-pi/pi-natives";
|
|
8
8
|
import { CONFIG_DIR_NAME } from "@oh-my-pi/pi-utils/dirs";
|
|
9
9
|
import { readFile } from "../capability/fs";
|
|
10
|
+
import { parseRuleConditionAndScope, type Rule, type RuleFrontmatter } from "../capability/rule";
|
|
10
11
|
import type { Skill, SkillFrontmatter } from "../capability/skill";
|
|
11
12
|
import type { LoadContext, LoadResult, SourceMeta } from "../capability/types";
|
|
12
13
|
import { parseFrontmatter } from "../utils/frontmatter";
|
|
@@ -60,6 +61,11 @@ export const SOURCE_PATHS = {
|
|
|
60
61
|
userAgent: ".gemini",
|
|
61
62
|
projectDir: ".gemini",
|
|
62
63
|
},
|
|
64
|
+
opencode: {
|
|
65
|
+
userBase: ".config/opencode",
|
|
66
|
+
userAgent: ".config/opencode",
|
|
67
|
+
projectDir: ".opencode",
|
|
68
|
+
},
|
|
63
69
|
cursor: {
|
|
64
70
|
userBase: ".cursor",
|
|
65
71
|
userAgent: ".cursor",
|
|
@@ -158,6 +164,43 @@ export function parseArrayOrCSV(value: unknown): string[] | undefined {
|
|
|
158
164
|
return undefined;
|
|
159
165
|
}
|
|
160
166
|
|
|
167
|
+
/**
|
|
168
|
+
* Build a canonical rule item from a markdown/markdown-frontmatter document.
|
|
169
|
+
*/
|
|
170
|
+
export function buildRuleFromMarkdown(
|
|
171
|
+
name: string,
|
|
172
|
+
content: string,
|
|
173
|
+
filePath: string,
|
|
174
|
+
source: SourceMeta,
|
|
175
|
+
options?: {
|
|
176
|
+
ruleName?: string;
|
|
177
|
+
stripNamePattern?: RegExp;
|
|
178
|
+
},
|
|
179
|
+
): Rule {
|
|
180
|
+
const { frontmatter, body } = parseFrontmatter(content, { source: filePath });
|
|
181
|
+
const { condition, scope } = parseRuleConditionAndScope(frontmatter as RuleFrontmatter);
|
|
182
|
+
|
|
183
|
+
let globs: string[] | undefined;
|
|
184
|
+
if (Array.isArray(frontmatter.globs)) {
|
|
185
|
+
globs = frontmatter.globs.filter((item): item is string => typeof item === "string");
|
|
186
|
+
} else if (typeof frontmatter.globs === "string") {
|
|
187
|
+
globs = [frontmatter.globs];
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const resolvedName = options?.ruleName ?? name.replace(options?.stripNamePattern ?? /\.(md|mdc)$/, "");
|
|
191
|
+
return {
|
|
192
|
+
name: resolvedName,
|
|
193
|
+
path: filePath,
|
|
194
|
+
content: body,
|
|
195
|
+
globs,
|
|
196
|
+
alwaysApply: frontmatter.alwaysApply === true,
|
|
197
|
+
description: typeof frontmatter.description === "string" ? frontmatter.description : undefined,
|
|
198
|
+
condition,
|
|
199
|
+
scope,
|
|
200
|
+
_source: source,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
161
204
|
/**
|
|
162
205
|
* Parse model field into a prioritized list.
|
|
163
206
|
*/
|
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
|
+
});
|
|
@@ -15,8 +15,8 @@ import { readFile } from "../capability/fs";
|
|
|
15
15
|
import { type MCPServer, mcpCapability } from "../capability/mcp";
|
|
16
16
|
import { type Rule, ruleCapability } from "../capability/rule";
|
|
17
17
|
import type { LoadContext, LoadResult } from "../capability/types";
|
|
18
|
-
import { parseFrontmatter } from "../utils/frontmatter";
|
|
19
18
|
import {
|
|
19
|
+
buildRuleFromMarkdown,
|
|
20
20
|
createSourceMeta,
|
|
21
21
|
expandEnvVarsDeep,
|
|
22
22
|
getProjectPath,
|
|
@@ -104,26 +104,8 @@ async function loadRules(ctx: LoadContext): Promise<LoadResult<Rule>> {
|
|
|
104
104
|
if (userPath) {
|
|
105
105
|
const content = await readFile(userPath);
|
|
106
106
|
if (content) {
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
// Validate and normalize globs
|
|
110
|
-
let globs: string[] | undefined;
|
|
111
|
-
if (Array.isArray(frontmatter.globs)) {
|
|
112
|
-
globs = frontmatter.globs.filter((g): g is string => typeof g === "string");
|
|
113
|
-
} else if (typeof frontmatter.globs === "string") {
|
|
114
|
-
globs = [frontmatter.globs];
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
items.push({
|
|
118
|
-
name: "global_rules",
|
|
119
|
-
path: userPath,
|
|
120
|
-
content: body,
|
|
121
|
-
globs,
|
|
122
|
-
alwaysApply: frontmatter.alwaysApply as boolean | undefined,
|
|
123
|
-
description: frontmatter.description as string | undefined,
|
|
124
|
-
ttsrTrigger: typeof frontmatter.ttsr_trigger === "string" ? frontmatter.ttsr_trigger : undefined,
|
|
125
|
-
_source: createSourceMeta(PROVIDER_ID, userPath, "user"),
|
|
126
|
-
});
|
|
107
|
+
const source = createSourceMeta(PROVIDER_ID, userPath, "user");
|
|
108
|
+
items.push(buildRuleFromMarkdown("global_rules.md", content, userPath, source, { ruleName: "global_rules" }));
|
|
127
109
|
}
|
|
128
110
|
}
|
|
129
111
|
|
|
@@ -132,29 +114,8 @@ async function loadRules(ctx: LoadContext): Promise<LoadResult<Rule>> {
|
|
|
132
114
|
if (projectRulesDir) {
|
|
133
115
|
const result = await loadFilesFromDir<Rule>(ctx, projectRulesDir, PROVIDER_ID, "project", {
|
|
134
116
|
extensions: ["md"],
|
|
135
|
-
transform: (name, content, path, source) =>
|
|
136
|
-
|
|
137
|
-
const ruleName = name.replace(/\.md$/, "");
|
|
138
|
-
|
|
139
|
-
// Validate and normalize globs
|
|
140
|
-
let globs: string[] | undefined;
|
|
141
|
-
if (Array.isArray(frontmatter.globs)) {
|
|
142
|
-
globs = frontmatter.globs.filter((g): g is string => typeof g === "string");
|
|
143
|
-
} else if (typeof frontmatter.globs === "string") {
|
|
144
|
-
globs = [frontmatter.globs];
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
return {
|
|
148
|
-
name: ruleName,
|
|
149
|
-
path,
|
|
150
|
-
content: body,
|
|
151
|
-
globs,
|
|
152
|
-
alwaysApply: frontmatter.alwaysApply as boolean | undefined,
|
|
153
|
-
description: frontmatter.description as string | undefined,
|
|
154
|
-
ttsrTrigger: typeof frontmatter.ttsr_trigger === "string" ? frontmatter.ttsr_trigger : undefined,
|
|
155
|
-
_source: source,
|
|
156
|
-
};
|
|
157
|
-
},
|
|
117
|
+
transform: (name, content, path, source) =>
|
|
118
|
+
buildRuleFromMarkdown(name, content, path, source, { stripNamePattern: /\.md$/ }),
|
|
158
119
|
});
|
|
159
120
|
items.push(...result.items);
|
|
160
121
|
if (result.warnings) warnings.push(...result.warnings);
|