opencode-architect 0.2.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/index.ts ADDED
@@ -0,0 +1,120 @@
1
+ import type { Plugin, PluginInput } from "@opencode-ai/plugin";
2
+ import { readFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { parse as parseYaml } from "yaml";
5
+ import type { AgentConfig } from "@opencode-ai/sdk";
6
+ import { command as syncDocsCommand } from "./commands/sync-docs";
7
+ import { OpenCodeDocsFetcher } from "./scripts/fetch-opencode-docs";
8
+ import { SilentLogger } from "./scripts/logger";
9
+ import { createSyncDocsTool } from "./tools/sync-docs";
10
+
11
+ const AGENTS_DIR = path.join(import.meta.dirname, "assets", "agents");
12
+
13
+ const FRONTMATTER_REGEX = /^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/;
14
+
15
+ interface AgentFrontmatter {
16
+ description: string;
17
+ mode: "primary" | "subagent" | "all";
18
+ tools?: Record<string, boolean>;
19
+ permission?: {
20
+ edit?: "ask" | "allow" | "deny";
21
+ bash?: ("ask" | "allow" | "deny") | Record<string, "ask" | "allow" | "deny">;
22
+ webfetch?: "ask" | "allow" | "deny";
23
+ doom_loop?: "ask" | "allow" | "deny";
24
+ external_directory?: "ask" | "allow" | "deny";
25
+ };
26
+ }
27
+
28
+ async function parseAgentMarkdown(content: string, agentName: string): Promise<AgentConfig> {
29
+ const match = content.match(FRONTMATTER_REGEX);
30
+
31
+ if (!match || match.length < 3) {
32
+ throw new Error(`Agent ${agentName} must have YAML frontmatter`);
33
+ }
34
+
35
+ const frontmatterYaml = match[1] as string;
36
+ const rawPrompt = match[2] as string;
37
+ const frontmatter = parseYaml(frontmatterYaml) as AgentFrontmatter;
38
+ const prompt = rawPrompt.replace(/^\r?\n/, "");
39
+
40
+ const config: AgentConfig = {
41
+ description: frontmatter.description,
42
+ mode: frontmatter.mode,
43
+ prompt,
44
+ };
45
+
46
+ if (frontmatter.tools) {
47
+ config.tools = frontmatter.tools;
48
+ }
49
+
50
+ if (frontmatter.permission) {
51
+ config.permission = frontmatter.permission;
52
+ }
53
+
54
+ return config;
55
+ }
56
+
57
+ const OpencodeArchitect: Plugin = async (input) => {
58
+ const syncDocsTool = createSyncDocsTool();
59
+
60
+ syncDocsOnStartup(input.client);
61
+
62
+ const agents = await loadAgents();
63
+
64
+ return {
65
+ config: async (config) => {
66
+ config.agent = config.agent || {};
67
+ config.command = config.command || {};
68
+
69
+ for (const [name, agentConfig] of Object.entries(agents)) {
70
+ config.agent[name] = agentConfig;
71
+ }
72
+
73
+ config.command["sync-docs"] = syncDocsCommand;
74
+ },
75
+ tool: {
76
+ "sync-docs": syncDocsTool,
77
+ },
78
+ };
79
+ };
80
+
81
+ export default OpencodeArchitect;
82
+
83
+ async function loadAgents(): Promise<Record<string, AgentConfig>> {
84
+ const agentFiles = [
85
+ "opencode-agent-designer.md",
86
+ "opencode-architect.md",
87
+ "opencode-command-crafter.md",
88
+ "opencode-extension-auditor.md",
89
+ "opencode-packager.md",
90
+ "opencode-publisher.md",
91
+ "opencode-mcp-integrator.md",
92
+ "opencode-plugin-engineer.md",
93
+ "opencode-skill-creator.md",
94
+ "opencode-tool-builder.md",
95
+ ];
96
+
97
+ const agents: Record<string, AgentConfig> = {};
98
+
99
+ for (const filename of agentFiles) {
100
+ const agentPath = path.join(AGENTS_DIR, filename);
101
+ const agentContent = await readFile(agentPath, "utf-8");
102
+ const agentName = path.basename(filename, ".md");
103
+ agents[agentName] = await parseAgentMarkdown(agentContent, agentName);
104
+ }
105
+
106
+ return agents;
107
+ }
108
+
109
+ function syncDocsOnStartup(client: PluginInput["client"]): void {
110
+ const fetcher = new OpenCodeDocsFetcher(new SilentLogger());
111
+ fetcher.run().catch((error: unknown) => {
112
+ const message = error instanceof Error ? error.message : String(error);
113
+ client.tui.showToast({
114
+ body: {
115
+ message: `Failed to sync OpenCode docs: ${message}`,
116
+ variant: "error",
117
+ },
118
+ });
119
+ });
120
+ }
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "opencode-architect",
3
+ "version": "0.2.0",
4
+ "private": false,
5
+ "type": "module",
6
+ "module": "index.ts",
7
+ "scripts": {
8
+ "check": "tsc --noEmit",
9
+ "release": "npm version patch && npm publish && git push --follow-tags"
10
+ },
11
+ "exports": {
12
+ ".": {
13
+ "import": "./index.ts"
14
+ }
15
+ },
16
+ "files": [
17
+ "index.ts",
18
+ "tools",
19
+ "commands",
20
+ "scripts",
21
+ "assets"
22
+ ],
23
+
24
+ "dependencies": {
25
+ "yaml": "^2.7.1"
26
+ },
27
+ "devDependencies": {
28
+ "@opencode-ai/plugin": "latest",
29
+ "@opencode-ai/sdk": "latest",
30
+ "@types/bun": "latest"
31
+ },
32
+ "peerDependencies": {
33
+ "typescript": "^5"
34
+ }
35
+ }
@@ -0,0 +1,193 @@
1
+ import { mkdir, writeFile } from "node:fs/promises";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+
5
+ import type { Logger } from "./logger";
6
+ import { ConsoleLogger } from "./logger";
7
+
8
+ interface ExternalDoc {
9
+ url: string;
10
+ filename: string;
11
+ }
12
+
13
+ export class OpenCodeDocsFetcher {
14
+ private readonly sitemapUrl: string;
15
+ private readonly docsDir: string;
16
+ private readonly externalDocs: ExternalDoc[];
17
+ private readonly logger: Logger;
18
+
19
+ public constructor(logger: Logger | null = null) {
20
+ this.sitemapUrl = "https://opencode.ai/sitemap.xml";
21
+ this.docsDir = path.join(
22
+ os.homedir(),
23
+ ".cache",
24
+ "opencode",
25
+ "opencode-architect",
26
+ "docs",
27
+ );
28
+ this.logger = logger ?? new ConsoleLogger();
29
+ this.externalDocs = [
30
+ {
31
+ url: "https://platform.claude.com/docs/en/agents-and-tools/agent-skills/best-practices.md",
32
+ filename: "claude-skill-best-practices.md",
33
+ },
34
+ {
35
+ url: "https://platform.claude.com/docs/en/build-with-claude/prompt-engineering/claude-4-best-practices.md",
36
+ filename: "claude-4-best-practices.md",
37
+ },
38
+ ];
39
+ }
40
+
41
+ public async run(): Promise<void> {
42
+ try {
43
+ await this.ensureDocsDir();
44
+ const sitemap = await this.fetchText(this.sitemapUrl);
45
+ const docUrls = this.extractDocUrls(sitemap);
46
+ const markdownUrls = this.buildMarkdownUrls(docUrls);
47
+ await this.downloadDocs(markdownUrls);
48
+ await this.downloadExternalDocs();
49
+ } catch (error) {
50
+ const message = error instanceof Error ? error.message : String(error);
51
+ this.logger.error(`Failed to fetch OpenCode docs: ${message}`);
52
+ process.exitCode = 1;
53
+ }
54
+ }
55
+
56
+ private async ensureDocsDir(): Promise<void> {
57
+ await mkdir(this.docsDir, { recursive: true });
58
+ }
59
+
60
+ private async fetchText(url: string): Promise<string> {
61
+ const response = await fetch(url, {
62
+ headers: {
63
+ "User-Agent": "opencode-docs-fetcher",
64
+ },
65
+ });
66
+ if (!response.ok) {
67
+ throw new Error(`Request failed (${response.status}) for ${url}`);
68
+ }
69
+ return await response.text();
70
+ }
71
+
72
+ private extractDocUrls(sitemap: string): string[] {
73
+ const urls: string[] = [];
74
+ const regex = /<loc>([^<]+)<\/loc>/g;
75
+ let match: RegExpExecArray | null = regex.exec(sitemap);
76
+ while (match) {
77
+ const url = match[1];
78
+ if (url !== undefined) {
79
+ try {
80
+ const parsed = new URL(url);
81
+ if (parsed.pathname.startsWith("/docs/")) {
82
+ urls.push(url);
83
+ }
84
+ } catch {
85
+ continue;
86
+ }
87
+ }
88
+ match = regex.exec(sitemap);
89
+ }
90
+ return Array.from(new Set(urls));
91
+ }
92
+
93
+ private buildMarkdownUrls(urls: string[]): string[] {
94
+ const markdownUrls: string[] = [];
95
+ for (const url of urls) {
96
+ const parsed = new URL(url);
97
+ if (!parsed.pathname.startsWith("/docs/")) {
98
+ continue;
99
+ }
100
+ const normalizedPath = this.normalizeDocPath(parsed.pathname);
101
+ if (normalizedPath === "/docs") {
102
+ continue;
103
+ }
104
+ const markdownUrl = `${parsed.origin}${normalizedPath}.md`;
105
+ markdownUrls.push(markdownUrl);
106
+ }
107
+ return Array.from(new Set(markdownUrls));
108
+ }
109
+
110
+ private normalizeDocPath(pathname: string): string {
111
+ if (pathname.endsWith("/")) {
112
+ return pathname.slice(0, -1);
113
+ }
114
+ return pathname;
115
+ }
116
+
117
+ private async downloadDocs(urls: string[]): Promise<void> {
118
+ let successCount = 0;
119
+ let failureCount = 0;
120
+ for (const url of urls) {
121
+ try {
122
+ const content = await this.fetchText(url);
123
+ const filename = this.buildFilename(url);
124
+ const filePath = path.join(this.docsDir, filename);
125
+ await writeFile(filePath, content);
126
+ successCount += 1;
127
+ } catch (error) {
128
+ const message = error instanceof Error ? error.message : String(error);
129
+ this.logger.error(`Failed to fetch ${url}: ${message}`);
130
+ failureCount += 1;
131
+ }
132
+ }
133
+
134
+ this.logger.info(
135
+ `OpenCode docs fetch complete. Success: ${successCount}, Failed: ${failureCount}.`,
136
+ );
137
+ }
138
+
139
+ private async downloadExternalDocs(): Promise<void> {
140
+ let successCount = 0;
141
+ let failureCount = 0;
142
+ for (const doc of this.externalDocs) {
143
+ try {
144
+ const content = await this.fetchText(doc.url);
145
+ const filePath = path.join(this.docsDir, doc.filename);
146
+ await writeFile(filePath, content);
147
+ successCount += 1;
148
+ } catch (error) {
149
+ const message = error instanceof Error ? error.message : String(error);
150
+ this.logger.error(`Failed to fetch ${doc.url}: ${message}`);
151
+ failureCount += 1;
152
+ }
153
+ }
154
+
155
+ this.logger.info(
156
+ `External docs fetch complete. Success: ${successCount}, Failed: ${failureCount}.`,
157
+ );
158
+ }
159
+
160
+ private buildFilename(url: string): string {
161
+ const parsed = new URL(url);
162
+ const pathname = parsed.pathname;
163
+ if (pathname === "/docs.md" || pathname === "/docs/.md") {
164
+ return "index.md";
165
+ }
166
+
167
+ let relative = pathname;
168
+ if (relative.startsWith("/docs/")) {
169
+ relative = relative.slice("/docs/".length);
170
+ } else if (relative.startsWith("/docs")) {
171
+ relative = relative.slice("/docs".length);
172
+ }
173
+
174
+ if (relative.startsWith("/")) {
175
+ relative = relative.slice(1);
176
+ }
177
+
178
+ if (relative.length === 0) {
179
+ return "index.md";
180
+ }
181
+
182
+ const normalized = relative.replace(/\//g, "-");
183
+ if (normalized.endsWith(".md")) {
184
+ return normalized;
185
+ }
186
+ return `${normalized}.md`;
187
+ }
188
+
189
+ }
190
+
191
+ if (import.meta.main) {
192
+ void new OpenCodeDocsFetcher().run();
193
+ }
@@ -0,0 +1,20 @@
1
+ export interface Logger {
2
+ info(message: string): void;
3
+ error(message: string): void;
4
+ }
5
+
6
+ export class ConsoleLogger implements Logger {
7
+ public info(message: string): void {
8
+ console.log(message);
9
+ }
10
+
11
+ public error(message: string): void {
12
+ console.error(message);
13
+ }
14
+ }
15
+
16
+ export class SilentLogger implements Logger {
17
+ public info(_message: string): void {}
18
+
19
+ public error(_message: string): void {}
20
+ }
@@ -0,0 +1,24 @@
1
+ import { tool } from "@opencode-ai/plugin";
2
+
3
+ import { OpenCodeDocsFetcher } from "../scripts/fetch-opencode-docs";
4
+
5
+ type ToolDefinition = ReturnType<typeof tool>;
6
+
7
+ function createSyncDocsTool(): ToolDefinition {
8
+ return tool({
9
+ description: "Sync OpenCode documentation by fetching the latest docs from opencode.ai",
10
+ args: {},
11
+ async execute(_args, _context) {
12
+ try {
13
+ const fetcher = new OpenCodeDocsFetcher();
14
+ await fetcher.run();
15
+ return "Successfully synced OpenCode documentation.";
16
+ } catch (error) {
17
+ const message = error instanceof Error ? error.message : String(error);
18
+ return `Failed to sync docs: ${message}`;
19
+ }
20
+ },
21
+ });
22
+ }
23
+
24
+ export { createSyncDocsTool };