@algochad/archcoder 2.0.2
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/README.md +113 -0
- package/bin/cli-entry.js +55 -0
- package/bin/cli-output.js +145 -0
- package/bin/cli.js +5108 -0
- package/bin/cli.test.js +56 -0
- package/dist/apple-touch-icon-120x120.png +0 -0
- package/dist/apple-touch-icon-152x152.png +0 -0
- package/dist/apple-touch-icon-167x167.png +0 -0
- package/dist/apple-touch-icon-180x180.png +0 -0
- package/dist/apple-touch-icon.png +0 -0
- package/dist/apple-touch-icon.svg +67 -0
- package/dist/assets/MultiRunWindow-BZp3MjJP.js +1 -0
- package/dist/assets/SettingsWindow-DoGYXpX7.js +1 -0
- package/dist/assets/TerminalView-BN7BR5Ff.js +3 -0
- package/dist/assets/TimelineDialog-ZQ33oVQR.js +1 -0
- package/dist/assets/ToolOutputDialog-Blv3pnug.js +16 -0
- package/dist/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
- package/dist/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
- package/dist/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
- package/dist/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
- package/dist/assets/ibm-plex-mono-latin-600-normal-BgSNZQsw.woff2 +0 -0
- package/dist/assets/ibm-plex-mono-latin-600-normal-DWFSQ4vo.woff +0 -0
- package/dist/assets/ibm-plex-sans-latin-400-normal-CDDApCn2.woff2 +0 -0
- package/dist/assets/ibm-plex-sans-latin-400-normal-CYLoc0-x.woff +0 -0
- package/dist/assets/ibm-plex-sans-latin-500-normal-6ng42L7E.woff2 +0 -0
- package/dist/assets/ibm-plex-sans-latin-500-normal-BgVn5rGT.woff +0 -0
- package/dist/assets/ibm-plex-sans-latin-600-normal-Cu4Hd6ag.woff +0 -0
- package/dist/assets/ibm-plex-sans-latin-600-normal-CuJfVYMP.woff2 +0 -0
- package/dist/assets/index-CtCEGYrr.css +1 -0
- package/dist/assets/index-o_d2wtWC.js +48 -0
- package/dist/assets/main-5QGBtzdq.css +1 -0
- package/dist/assets/main-B6oiMU86.js +8033 -0
- package/dist/assets/vendor--DbVqbJpV.css +1 -0
- package/dist/assets/vendor-.bun-HTKwyaEM.js +10086 -0
- package/dist/assets/wasm-CG6Dc4jp.js +1 -0
- package/dist/assets/worker-bqd4RMrj.js +155 -0
- package/dist/favicon-16.png +0 -0
- package/dist/favicon-32.png +0 -0
- package/dist/favicon.png +0 -0
- package/dist/favicon.svg +67 -0
- package/dist/index.html +533 -0
- package/dist/logo-dark-192x192.png +0 -0
- package/dist/logo-dark-512x512.svg +16 -0
- package/dist/logo-light-192x192.png +0 -0
- package/dist/logo-light-512x512.svg +16 -0
- package/dist/pwa-192.png +0 -0
- package/dist/pwa-512.png +0 -0
- package/dist/pwa-maskable-192.png +0 -0
- package/dist/pwa-maskable-512.png +0 -0
- package/dist/site.webmanifest +22 -0
- package/dist/sw.js +1 -0
- package/package.json +107 -0
- package/public/apple-touch-icon-120x120.png +0 -0
- package/public/apple-touch-icon-152x152.png +0 -0
- package/public/apple-touch-icon-167x167.png +0 -0
- package/public/apple-touch-icon-180x180.png +0 -0
- package/public/apple-touch-icon.png +0 -0
- package/public/apple-touch-icon.svg +67 -0
- package/public/favicon-16.png +0 -0
- package/public/favicon-32.png +0 -0
- package/public/favicon.png +0 -0
- package/public/favicon.svg +67 -0
- package/public/logo-dark-192x192.png +0 -0
- package/public/logo-dark-512x512.svg +16 -0
- package/public/logo-light-192x192.png +0 -0
- package/public/logo-light-512x512.svg +16 -0
- package/public/pwa-192.png +0 -0
- package/public/pwa-512.png +0 -0
- package/public/pwa-maskable-192.png +0 -0
- package/public/pwa-maskable-512.png +0 -0
- package/public/site.webmanifest +22 -0
- package/server/TERMINAL_INPUT_WS_PROTOCOL.md +44 -0
- package/server/index.d.ts +37 -0
- package/server/index.js +14694 -0
- package/server/lib/cloudflare-tunnel.js +650 -0
- package/server/lib/git/DOCUMENTATION.md +146 -0
- package/server/lib/git/credentials.js +74 -0
- package/server/lib/git/identity-storage.js +110 -0
- package/server/lib/git/index.js +6 -0
- package/server/lib/git/service.js +3117 -0
- package/server/lib/github/DOCUMENTATION.md +170 -0
- package/server/lib/github/auth.js +307 -0
- package/server/lib/github/device-flow.js +50 -0
- package/server/lib/github/index.js +24 -0
- package/server/lib/github/octokit.js +10 -0
- package/server/lib/github/pr-status.js +478 -0
- package/server/lib/github/repo/index.js +55 -0
- package/server/lib/installer/desktop.js +289 -0
- package/server/lib/installer/download.js +208 -0
- package/server/lib/installer/index.js +45 -0
- package/server/lib/installer/platform.js +100 -0
- package/server/lib/notifications/DOCUMENTATION.md +61 -0
- package/server/lib/notifications/index.js +1 -0
- package/server/lib/notifications/message.js +49 -0
- package/server/lib/notifications/message.test.js +59 -0
- package/server/lib/opencode/DOCUMENTATION.md +59 -0
- package/server/lib/opencode/agents.js +634 -0
- package/server/lib/opencode/auth.js +81 -0
- package/server/lib/opencode/commands.js +339 -0
- package/server/lib/opencode/index.js +66 -0
- package/server/lib/opencode/mcp.js +206 -0
- package/server/lib/opencode/providers.js +96 -0
- package/server/lib/opencode/shared.js +527 -0
- package/server/lib/opencode/skills.js +480 -0
- package/server/lib/opencode/tunnel-auth.js +591 -0
- package/server/lib/opencode/ui-auth.js +510 -0
- package/server/lib/package-manager.js +505 -0
- package/server/lib/quota/DOCUMENTATION.md +55 -0
- package/server/lib/quota/index.js +24 -0
- package/server/lib/quota/providers/claude.js +107 -0
- package/server/lib/quota/providers/codex.js +113 -0
- package/server/lib/quota/providers/copilot.js +165 -0
- package/server/lib/quota/providers/google/api.js +92 -0
- package/server/lib/quota/providers/google/auth.js +108 -0
- package/server/lib/quota/providers/google/index.js +124 -0
- package/server/lib/quota/providers/google/transforms.js +109 -0
- package/server/lib/quota/providers/index.js +152 -0
- package/server/lib/quota/providers/interface.js +55 -0
- package/server/lib/quota/providers/kimi.js +108 -0
- package/server/lib/quota/providers/minimax-cn-coding-plan.js +15 -0
- package/server/lib/quota/providers/minimax-coding-plan.js +15 -0
- package/server/lib/quota/providers/minimax-shared.js +136 -0
- package/server/lib/quota/providers/nanogpt.js +124 -0
- package/server/lib/quota/providers/ollama-cloud.js +112 -0
- package/server/lib/quota/providers/openai.js +91 -0
- package/server/lib/quota/providers/openrouter.js +92 -0
- package/server/lib/quota/providers/zai.js +91 -0
- package/server/lib/quota/utils/auth.js +46 -0
- package/server/lib/quota/utils/formatters.js +76 -0
- package/server/lib/quota/utils/index.js +10 -0
- package/server/lib/quota/utils/transformers.js +55 -0
- package/server/lib/skills-catalog/DOCUMENTATION.md +178 -0
- package/server/lib/skills-catalog/cache.js +32 -0
- package/server/lib/skills-catalog/clawdhub/api.js +158 -0
- package/server/lib/skills-catalog/clawdhub/index.js +30 -0
- package/server/lib/skills-catalog/clawdhub/install.js +238 -0
- package/server/lib/skills-catalog/clawdhub/scan.js +113 -0
- package/server/lib/skills-catalog/curated-sources.js +21 -0
- package/server/lib/skills-catalog/git.js +77 -0
- package/server/lib/skills-catalog/index.js +42 -0
- package/server/lib/skills-catalog/install.js +294 -0
- package/server/lib/skills-catalog/scan.js +221 -0
- package/server/lib/skills-catalog/source.js +85 -0
- package/server/lib/terminal/DOCUMENTATION.md +114 -0
- package/server/lib/terminal/index.js +12 -0
- package/server/lib/terminal/input-ws-protocol.js +66 -0
- package/server/lib/terminal/input-ws-protocol.test.js +138 -0
- package/server/lib/tts/DOCUMENTATION.md +134 -0
- package/server/lib/tts/index.js +16 -0
- package/server/lib/tts/service.js +162 -0
- package/server/lib/tts/summarization.js +171 -0
- package/server/lib/tunnels/index.js +166 -0
- package/server/lib/tunnels/providers/cloudflare.js +260 -0
- package/server/lib/tunnels/registry.js +51 -0
- package/server/lib/tunnels/types.js +219 -0
- package/server/lib/utils/lru.js +107 -0
- package/server/lib/utils/sse.js +121 -0
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import {
|
|
4
|
+
CONFIG_FILE,
|
|
5
|
+
OPENCODE_CONFIG_DIR,
|
|
6
|
+
COMMAND_DIR,
|
|
7
|
+
COMMAND_SCOPE,
|
|
8
|
+
ensureDirs,
|
|
9
|
+
parseMdFile,
|
|
10
|
+
writeMdFile,
|
|
11
|
+
readConfigLayers,
|
|
12
|
+
writeConfig,
|
|
13
|
+
getJsonEntrySource,
|
|
14
|
+
getJsonWriteTarget,
|
|
15
|
+
isPromptFileReference,
|
|
16
|
+
resolvePromptFilePath,
|
|
17
|
+
writePromptFile,
|
|
18
|
+
} from './shared.js';
|
|
19
|
+
|
|
20
|
+
// ============== COMMAND SCOPE HELPERS ==============
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Ensure project-level command directory exists
|
|
24
|
+
*/
|
|
25
|
+
function ensureProjectCommandDir(workingDirectory) {
|
|
26
|
+
const projectCommandDir = path.join(workingDirectory, '.opencode', 'commands');
|
|
27
|
+
if (!fs.existsSync(projectCommandDir)) {
|
|
28
|
+
fs.mkdirSync(projectCommandDir, { recursive: true });
|
|
29
|
+
}
|
|
30
|
+
const legacyProjectCommandDir = path.join(workingDirectory, '.opencode', 'command');
|
|
31
|
+
if (!fs.existsSync(legacyProjectCommandDir)) {
|
|
32
|
+
fs.mkdirSync(legacyProjectCommandDir, { recursive: true });
|
|
33
|
+
}
|
|
34
|
+
return projectCommandDir;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Get project-level command path
|
|
39
|
+
*/
|
|
40
|
+
function getProjectCommandPath(workingDirectory, commandName) {
|
|
41
|
+
const pluralPath = path.join(workingDirectory, '.opencode', 'commands', `${commandName}.md`);
|
|
42
|
+
const legacyPath = path.join(workingDirectory, '.opencode', 'command', `${commandName}.md`);
|
|
43
|
+
if (fs.existsSync(legacyPath) && !fs.existsSync(pluralPath)) return legacyPath;
|
|
44
|
+
return pluralPath;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Get user-level command path
|
|
49
|
+
*/
|
|
50
|
+
function getUserCommandPath(commandName) {
|
|
51
|
+
const pluralPath = path.join(COMMAND_DIR, `${commandName}.md`);
|
|
52
|
+
const legacyPath = path.join(OPENCODE_CONFIG_DIR, 'command', `${commandName}.md`);
|
|
53
|
+
if (fs.existsSync(legacyPath) && !fs.existsSync(pluralPath)) return legacyPath;
|
|
54
|
+
return pluralPath;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Determine command scope based on where the .md file exists
|
|
59
|
+
* Priority: project level > user level > null (built-in only)
|
|
60
|
+
*/
|
|
61
|
+
function getCommandScope(commandName, workingDirectory) {
|
|
62
|
+
if (workingDirectory) {
|
|
63
|
+
const projectPath = getProjectCommandPath(workingDirectory, commandName);
|
|
64
|
+
if (fs.existsSync(projectPath)) {
|
|
65
|
+
return { scope: COMMAND_SCOPE.PROJECT, path: projectPath };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const userPath = getUserCommandPath(commandName);
|
|
70
|
+
if (fs.existsSync(userPath)) {
|
|
71
|
+
return { scope: COMMAND_SCOPE.USER, path: userPath };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return { scope: null, path: null };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Get the path where a command should be written based on scope
|
|
79
|
+
*/
|
|
80
|
+
function getCommandWritePath(commandName, workingDirectory, requestedScope) {
|
|
81
|
+
// For updates: check existing location first (project takes precedence)
|
|
82
|
+
const existing = getCommandScope(commandName, workingDirectory);
|
|
83
|
+
if (existing.path) {
|
|
84
|
+
return existing;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// For new commands or built-in overrides: use requested scope or default to user
|
|
88
|
+
const scope = requestedScope || COMMAND_SCOPE.USER;
|
|
89
|
+
if (scope === COMMAND_SCOPE.PROJECT && workingDirectory) {
|
|
90
|
+
return {
|
|
91
|
+
scope: COMMAND_SCOPE.PROJECT,
|
|
92
|
+
path: getProjectCommandPath(workingDirectory, commandName)
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
scope: COMMAND_SCOPE.USER,
|
|
98
|
+
path: getUserCommandPath(commandName)
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function getCommandSources(commandName, workingDirectory) {
|
|
103
|
+
const projectPath = workingDirectory ? getProjectCommandPath(workingDirectory, commandName) : null;
|
|
104
|
+
const projectExists = projectPath && fs.existsSync(projectPath);
|
|
105
|
+
|
|
106
|
+
const userPath = getUserCommandPath(commandName);
|
|
107
|
+
const userExists = fs.existsSync(userPath);
|
|
108
|
+
|
|
109
|
+
const mdPath = projectExists ? projectPath : (userExists ? userPath : null);
|
|
110
|
+
const mdExists = !!mdPath;
|
|
111
|
+
const mdScope = projectExists ? COMMAND_SCOPE.PROJECT : (userExists ? COMMAND_SCOPE.USER : null);
|
|
112
|
+
|
|
113
|
+
const layers = readConfigLayers(workingDirectory);
|
|
114
|
+
const jsonSource = getJsonEntrySource(layers, 'command', commandName);
|
|
115
|
+
const jsonSection = jsonSource.section;
|
|
116
|
+
const jsonPath = jsonSource.path || layers.paths.customPath || layers.paths.projectPath || layers.paths.userPath;
|
|
117
|
+
const jsonScope = jsonSource.path === layers.paths.projectPath ? COMMAND_SCOPE.PROJECT : COMMAND_SCOPE.USER;
|
|
118
|
+
|
|
119
|
+
const sources = {
|
|
120
|
+
md: {
|
|
121
|
+
exists: mdExists,
|
|
122
|
+
path: mdPath,
|
|
123
|
+
scope: mdScope,
|
|
124
|
+
fields: []
|
|
125
|
+
},
|
|
126
|
+
json: {
|
|
127
|
+
exists: jsonSource.exists,
|
|
128
|
+
path: jsonPath,
|
|
129
|
+
scope: jsonSource.exists ? jsonScope : null,
|
|
130
|
+
fields: []
|
|
131
|
+
},
|
|
132
|
+
projectMd: {
|
|
133
|
+
exists: projectExists,
|
|
134
|
+
path: projectPath
|
|
135
|
+
},
|
|
136
|
+
userMd: {
|
|
137
|
+
exists: userExists,
|
|
138
|
+
path: userPath
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
if (mdExists) {
|
|
143
|
+
const { frontmatter, body } = parseMdFile(mdPath);
|
|
144
|
+
sources.md.fields = Object.keys(frontmatter);
|
|
145
|
+
if (body) {
|
|
146
|
+
sources.md.fields.push('template');
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (jsonSection) {
|
|
151
|
+
sources.json.fields = Object.keys(jsonSection);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return sources;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function createCommand(commandName, config, workingDirectory, scope) {
|
|
158
|
+
ensureDirs();
|
|
159
|
+
|
|
160
|
+
const projectPath = workingDirectory ? getProjectCommandPath(workingDirectory, commandName) : null;
|
|
161
|
+
const userPath = getUserCommandPath(commandName);
|
|
162
|
+
|
|
163
|
+
if (projectPath && fs.existsSync(projectPath)) {
|
|
164
|
+
throw new Error(`Command ${commandName} already exists as project-level .md file`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (fs.existsSync(userPath)) {
|
|
168
|
+
throw new Error(`Command ${commandName} already exists as user-level .md file`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const layers = readConfigLayers(workingDirectory);
|
|
172
|
+
const jsonSource = getJsonEntrySource(layers, 'command', commandName);
|
|
173
|
+
if (jsonSource.exists) {
|
|
174
|
+
throw new Error(`Command ${commandName} already exists in opencode.json`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
let targetPath;
|
|
178
|
+
let targetScope;
|
|
179
|
+
|
|
180
|
+
if (scope === COMMAND_SCOPE.PROJECT && workingDirectory) {
|
|
181
|
+
ensureProjectCommandDir(workingDirectory);
|
|
182
|
+
targetPath = projectPath;
|
|
183
|
+
targetScope = COMMAND_SCOPE.PROJECT;
|
|
184
|
+
} else {
|
|
185
|
+
targetPath = userPath;
|
|
186
|
+
targetScope = COMMAND_SCOPE.USER;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const { template, scope: _scopeFromConfig, ...frontmatter } = config;
|
|
190
|
+
|
|
191
|
+
writeMdFile(targetPath, frontmatter, template || '');
|
|
192
|
+
console.log(`Created new command: ${commandName} (scope: ${targetScope}, path: ${targetPath})`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function updateCommand(commandName, updates, workingDirectory) {
|
|
196
|
+
ensureDirs();
|
|
197
|
+
|
|
198
|
+
const { scope, path: mdPath } = getCommandWritePath(commandName, workingDirectory);
|
|
199
|
+
const mdExists = mdPath && fs.existsSync(mdPath);
|
|
200
|
+
|
|
201
|
+
const layers = readConfigLayers(workingDirectory);
|
|
202
|
+
const jsonSource = getJsonEntrySource(layers, 'command', commandName);
|
|
203
|
+
const jsonSection = jsonSource.section;
|
|
204
|
+
const hasJsonFields = jsonSource.exists && jsonSection && Object.keys(jsonSection).length > 0;
|
|
205
|
+
const jsonTarget = jsonSource.exists
|
|
206
|
+
? { config: jsonSource.config, path: jsonSource.path }
|
|
207
|
+
: getJsonWriteTarget(layers, workingDirectory ? COMMAND_SCOPE.PROJECT : COMMAND_SCOPE.USER);
|
|
208
|
+
let config = jsonTarget.config || {};
|
|
209
|
+
|
|
210
|
+
const isBuiltinOverride = !mdExists && !hasJsonFields;
|
|
211
|
+
|
|
212
|
+
let targetPath = mdPath;
|
|
213
|
+
let targetScope = scope;
|
|
214
|
+
|
|
215
|
+
if (!mdExists && isBuiltinOverride) {
|
|
216
|
+
targetPath = getUserCommandPath(commandName);
|
|
217
|
+
targetScope = COMMAND_SCOPE.USER;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const mdData = mdExists ? parseMdFile(mdPath) : (isBuiltinOverride ? { frontmatter: {}, body: '' } : null);
|
|
221
|
+
|
|
222
|
+
let mdModified = false;
|
|
223
|
+
let jsonModified = false;
|
|
224
|
+
const creatingNewMd = isBuiltinOverride;
|
|
225
|
+
|
|
226
|
+
for (const [field, value] of Object.entries(updates)) {
|
|
227
|
+
if (field === 'template') {
|
|
228
|
+
const normalizedValue = typeof value === 'string' ? value : (value == null ? '' : String(value));
|
|
229
|
+
|
|
230
|
+
if (mdExists || creatingNewMd) {
|
|
231
|
+
if (mdData) {
|
|
232
|
+
mdData.body = normalizedValue;
|
|
233
|
+
mdModified = true;
|
|
234
|
+
}
|
|
235
|
+
continue;
|
|
236
|
+
} else if (isPromptFileReference(jsonSection?.template)) {
|
|
237
|
+
const templateFilePath = resolvePromptFilePath(jsonSection.template);
|
|
238
|
+
if (!templateFilePath) {
|
|
239
|
+
throw new Error(`Invalid template file reference for command ${commandName}`);
|
|
240
|
+
}
|
|
241
|
+
writePromptFile(templateFilePath, normalizedValue);
|
|
242
|
+
continue;
|
|
243
|
+
} else if (isPromptFileReference(normalizedValue)) {
|
|
244
|
+
if (!config.command) config.command = {};
|
|
245
|
+
if (!config.command[commandName]) config.command[commandName] = {};
|
|
246
|
+
config.command[commandName].template = normalizedValue;
|
|
247
|
+
jsonModified = true;
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (!config.command) config.command = {};
|
|
252
|
+
if (!config.command[commandName]) config.command[commandName] = {};
|
|
253
|
+
config.command[commandName].template = normalizedValue;
|
|
254
|
+
jsonModified = true;
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const inMd = mdData?.frontmatter?.[field] !== undefined;
|
|
259
|
+
const inJson = jsonSection?.[field] !== undefined;
|
|
260
|
+
|
|
261
|
+
if (inJson) {
|
|
262
|
+
if (!config.command) config.command = {};
|
|
263
|
+
if (!config.command[commandName]) config.command[commandName] = {};
|
|
264
|
+
config.command[commandName][field] = value;
|
|
265
|
+
jsonModified = true;
|
|
266
|
+
} else if (inMd || creatingNewMd) {
|
|
267
|
+
if (mdData) {
|
|
268
|
+
mdData.frontmatter[field] = value;
|
|
269
|
+
mdModified = true;
|
|
270
|
+
}
|
|
271
|
+
} else {
|
|
272
|
+
if ((mdExists || creatingNewMd) && mdData) {
|
|
273
|
+
mdData.frontmatter[field] = value;
|
|
274
|
+
mdModified = true;
|
|
275
|
+
} else {
|
|
276
|
+
if (!config.command) config.command = {};
|
|
277
|
+
if (!config.command[commandName]) config.command[commandName] = {};
|
|
278
|
+
config.command[commandName][field] = value;
|
|
279
|
+
jsonModified = true;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (mdModified && mdData) {
|
|
285
|
+
writeMdFile(targetPath, mdData.frontmatter, mdData.body);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (jsonModified) {
|
|
289
|
+
writeConfig(config, jsonTarget.path || CONFIG_FILE);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
console.log(`Updated command: ${commandName} (scope: ${targetScope}, md: ${mdModified}, json: ${jsonModified})`);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function deleteCommand(commandName, workingDirectory) {
|
|
296
|
+
let deleted = false;
|
|
297
|
+
|
|
298
|
+
if (workingDirectory) {
|
|
299
|
+
const projectPath = getProjectCommandPath(workingDirectory, commandName);
|
|
300
|
+
if (fs.existsSync(projectPath)) {
|
|
301
|
+
fs.unlinkSync(projectPath);
|
|
302
|
+
console.log(`Deleted project-level command .md file: ${projectPath}`);
|
|
303
|
+
deleted = true;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const userPath = getUserCommandPath(commandName);
|
|
308
|
+
if (fs.existsSync(userPath)) {
|
|
309
|
+
fs.unlinkSync(userPath);
|
|
310
|
+
console.log(`Deleted user-level command .md file: ${userPath}`);
|
|
311
|
+
deleted = true;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const layers = readConfigLayers(workingDirectory);
|
|
315
|
+
const jsonSource = getJsonEntrySource(layers, 'command', commandName);
|
|
316
|
+
if (jsonSource.exists && jsonSource.config && jsonSource.path) {
|
|
317
|
+
if (!jsonSource.config.command) jsonSource.config.command = {};
|
|
318
|
+
delete jsonSource.config.command[commandName];
|
|
319
|
+
writeConfig(jsonSource.config, jsonSource.path);
|
|
320
|
+
console.log(`Removed command from opencode.json: ${commandName}`);
|
|
321
|
+
deleted = true;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (!deleted) {
|
|
325
|
+
throw new Error(`Command "${commandName}" not found`);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export {
|
|
330
|
+
ensureProjectCommandDir,
|
|
331
|
+
getProjectCommandPath,
|
|
332
|
+
getUserCommandPath,
|
|
333
|
+
getCommandScope,
|
|
334
|
+
getCommandWritePath,
|
|
335
|
+
getCommandSources,
|
|
336
|
+
createCommand,
|
|
337
|
+
updateCommand,
|
|
338
|
+
deleteCommand,
|
|
339
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
export {
|
|
2
|
+
AGENT_DIR,
|
|
3
|
+
COMMAND_DIR,
|
|
4
|
+
SKILL_DIR,
|
|
5
|
+
CONFIG_FILE,
|
|
6
|
+
AGENT_SCOPE,
|
|
7
|
+
COMMAND_SCOPE,
|
|
8
|
+
SKILL_SCOPE,
|
|
9
|
+
readConfig,
|
|
10
|
+
writeConfig,
|
|
11
|
+
readSkillSupportingFile,
|
|
12
|
+
writeSkillSupportingFile,
|
|
13
|
+
deleteSkillSupportingFile,
|
|
14
|
+
} from './shared.js';
|
|
15
|
+
|
|
16
|
+
export {
|
|
17
|
+
getAgentScope,
|
|
18
|
+
getAgentPermissionSource,
|
|
19
|
+
getAgentSources,
|
|
20
|
+
getAgentConfig,
|
|
21
|
+
createAgent,
|
|
22
|
+
updateAgent,
|
|
23
|
+
deleteAgent,
|
|
24
|
+
} from './agents.js';
|
|
25
|
+
|
|
26
|
+
export {
|
|
27
|
+
getCommandScope,
|
|
28
|
+
getCommandSources,
|
|
29
|
+
createCommand,
|
|
30
|
+
updateCommand,
|
|
31
|
+
deleteCommand,
|
|
32
|
+
} from './commands.js';
|
|
33
|
+
|
|
34
|
+
export {
|
|
35
|
+
getSkillSources,
|
|
36
|
+
getSkillScope,
|
|
37
|
+
discoverSkills,
|
|
38
|
+
createSkill,
|
|
39
|
+
updateSkill,
|
|
40
|
+
deleteSkill,
|
|
41
|
+
} from './skills.js';
|
|
42
|
+
|
|
43
|
+
export {
|
|
44
|
+
getProviderSources,
|
|
45
|
+
removeProviderConfig,
|
|
46
|
+
} from './providers.js';
|
|
47
|
+
|
|
48
|
+
export {
|
|
49
|
+
readAuthFile,
|
|
50
|
+
writeAuthFile,
|
|
51
|
+
removeProviderAuth,
|
|
52
|
+
getProviderAuth,
|
|
53
|
+
listProviderAuths,
|
|
54
|
+
AUTH_FILE,
|
|
55
|
+
OPENCODE_DATA_DIR,
|
|
56
|
+
} from './auth.js';
|
|
57
|
+
|
|
58
|
+
export { createUiAuth } from './ui-auth.js';
|
|
59
|
+
|
|
60
|
+
export {
|
|
61
|
+
listMcpConfigs,
|
|
62
|
+
getMcpConfig,
|
|
63
|
+
createMcpConfig,
|
|
64
|
+
updateMcpConfig,
|
|
65
|
+
deleteMcpConfig,
|
|
66
|
+
} from './mcp.js';
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import {
|
|
4
|
+
CONFIG_FILE,
|
|
5
|
+
AGENT_SCOPE,
|
|
6
|
+
readConfigFile,
|
|
7
|
+
readConfigLayers,
|
|
8
|
+
getJsonEntrySource,
|
|
9
|
+
getJsonWriteTarget,
|
|
10
|
+
writeConfig,
|
|
11
|
+
} from './shared.js';
|
|
12
|
+
|
|
13
|
+
// ============== MCP CONFIG HELPERS ==============
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Validate MCP server name
|
|
17
|
+
*/
|
|
18
|
+
function validateMcpName(name) {
|
|
19
|
+
if (!name || typeof name !== 'string') {
|
|
20
|
+
throw new Error('MCP server name is required');
|
|
21
|
+
}
|
|
22
|
+
if (!/^[a-z0-9][a-z0-9_-]*[a-z0-9]$|^[a-z0-9]$/.test(name)) {
|
|
23
|
+
throw new Error('MCP server name must be lowercase alphanumeric with hyphens/underscores');
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* List all MCP server configs from user-level opencode.json
|
|
29
|
+
*/
|
|
30
|
+
function resolveMcpScopeFromPath(layers, sourcePath) {
|
|
31
|
+
if (!sourcePath) return null;
|
|
32
|
+
return sourcePath === layers.paths.projectPath ? AGENT_SCOPE.PROJECT : AGENT_SCOPE.USER;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function ensureProjectMcpConfigPath(workingDirectory) {
|
|
36
|
+
const configDir = path.join(workingDirectory, '.opencode');
|
|
37
|
+
if (!fs.existsSync(configDir)) {
|
|
38
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
39
|
+
}
|
|
40
|
+
return path.join(configDir, 'opencode.json');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function listMcpConfigs(workingDirectory) {
|
|
44
|
+
const layers = readConfigLayers(workingDirectory);
|
|
45
|
+
const mcp = layers?.mergedConfig?.mcp || {};
|
|
46
|
+
|
|
47
|
+
return Object.entries(mcp)
|
|
48
|
+
.filter(([, entry]) => entry && typeof entry === 'object' && !Array.isArray(entry))
|
|
49
|
+
.map(([name, entry]) => {
|
|
50
|
+
const source = getJsonEntrySource(layers, 'mcp', name);
|
|
51
|
+
return {
|
|
52
|
+
name,
|
|
53
|
+
...buildMcpEntry(entry),
|
|
54
|
+
scope: resolveMcpScopeFromPath(layers, source.path),
|
|
55
|
+
};
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Get a single MCP server config by name
|
|
61
|
+
*/
|
|
62
|
+
function getMcpConfig(name, workingDirectory) {
|
|
63
|
+
const layers = readConfigLayers(workingDirectory);
|
|
64
|
+
const entry = layers?.mergedConfig?.mcp?.[name];
|
|
65
|
+
|
|
66
|
+
if (!entry) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
const source = getJsonEntrySource(layers, 'mcp', name);
|
|
70
|
+
return {
|
|
71
|
+
name,
|
|
72
|
+
...buildMcpEntry(entry),
|
|
73
|
+
scope: resolveMcpScopeFromPath(layers, source.path),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Create a new MCP server config entry
|
|
79
|
+
*/
|
|
80
|
+
function createMcpConfig(name, mcpConfig, workingDirectory, scope) {
|
|
81
|
+
validateMcpName(name);
|
|
82
|
+
|
|
83
|
+
const layers = readConfigLayers(workingDirectory);
|
|
84
|
+
const source = getJsonEntrySource(layers, 'mcp', name);
|
|
85
|
+
if (source.exists) {
|
|
86
|
+
throw new Error(`MCP server "${name}" already exists`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
let targetPath = CONFIG_FILE;
|
|
90
|
+
let config = {};
|
|
91
|
+
|
|
92
|
+
if (scope === AGENT_SCOPE.PROJECT) {
|
|
93
|
+
if (!workingDirectory) {
|
|
94
|
+
throw new Error('Project scope requires working directory');
|
|
95
|
+
}
|
|
96
|
+
targetPath = ensureProjectMcpConfigPath(workingDirectory);
|
|
97
|
+
config = fs.existsSync(targetPath) ? readConfigFile(targetPath) : {};
|
|
98
|
+
} else {
|
|
99
|
+
const jsonTarget = getJsonWriteTarget(layers, AGENT_SCOPE.USER);
|
|
100
|
+
targetPath = jsonTarget.path || CONFIG_FILE;
|
|
101
|
+
config = jsonTarget.config || {};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (!config.mcp || typeof config.mcp !== 'object' || Array.isArray(config.mcp)) {
|
|
105
|
+
config.mcp = {};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const { name: _ignoredName, ...entryData } = mcpConfig;
|
|
109
|
+
config.mcp[name] = buildMcpEntry(entryData);
|
|
110
|
+
|
|
111
|
+
writeConfig(config, targetPath);
|
|
112
|
+
console.log(`Created MCP server config: ${name}`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Update an existing MCP server config entry
|
|
117
|
+
*/
|
|
118
|
+
function updateMcpConfig(name, updates, workingDirectory) {
|
|
119
|
+
const layers = readConfigLayers(workingDirectory);
|
|
120
|
+
const source = getJsonEntrySource(layers, 'mcp', name);
|
|
121
|
+
const targetPath = source.path || CONFIG_FILE;
|
|
122
|
+
const config = source.config || (fs.existsSync(targetPath) ? readConfigFile(targetPath) : {});
|
|
123
|
+
|
|
124
|
+
if (!config.mcp || typeof config.mcp !== 'object' || Array.isArray(config.mcp)) {
|
|
125
|
+
config.mcp = {};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const existing = config.mcp[name] ?? {};
|
|
129
|
+
const { name: _ignoredName, ...updateData } = updates;
|
|
130
|
+
|
|
131
|
+
config.mcp[name] = buildMcpEntry({ ...existing, ...updateData });
|
|
132
|
+
|
|
133
|
+
writeConfig(config, targetPath);
|
|
134
|
+
console.log(`Updated MCP server config: ${name}`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Delete an MCP server config entry
|
|
139
|
+
*/
|
|
140
|
+
function deleteMcpConfig(name, workingDirectory) {
|
|
141
|
+
const layers = readConfigLayers(workingDirectory);
|
|
142
|
+
const source = getJsonEntrySource(layers, 'mcp', name);
|
|
143
|
+
const targetPath = source.path || CONFIG_FILE;
|
|
144
|
+
const config = source.config || (fs.existsSync(targetPath) ? readConfigFile(targetPath) : {});
|
|
145
|
+
|
|
146
|
+
if (!config.mcp || typeof config.mcp !== 'object' || config.mcp[name] === undefined) {
|
|
147
|
+
throw new Error(`MCP server "${name}" not found`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
delete config.mcp[name];
|
|
151
|
+
|
|
152
|
+
if (Object.keys(config.mcp).length === 0) {
|
|
153
|
+
delete config.mcp;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
writeConfig(config, targetPath);
|
|
157
|
+
console.log(`Deleted MCP server config: ${name}`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Build a clean MCP entry object, omitting undefined/null values
|
|
162
|
+
*/
|
|
163
|
+
function buildMcpEntry(data) {
|
|
164
|
+
const entry = {};
|
|
165
|
+
|
|
166
|
+
// type is required
|
|
167
|
+
entry.type = data.type === 'remote' ? 'remote' : 'local';
|
|
168
|
+
|
|
169
|
+
if (entry.type === 'local') {
|
|
170
|
+
// command must be a non-empty array of strings
|
|
171
|
+
if (Array.isArray(data.command) && data.command.length > 0) {
|
|
172
|
+
entry.command = data.command.map(String);
|
|
173
|
+
}
|
|
174
|
+
} else {
|
|
175
|
+
// remote: url required
|
|
176
|
+
if (data.url && typeof data.url === 'string') {
|
|
177
|
+
entry.url = data.url.trim();
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// environment: flat Record<string, string>
|
|
182
|
+
if (data.environment && typeof data.environment === 'object' && !Array.isArray(data.environment)) {
|
|
183
|
+
const cleaned = {};
|
|
184
|
+
for (const [k, v] of Object.entries(data.environment)) {
|
|
185
|
+
if (k && v !== undefined && v !== null) {
|
|
186
|
+
cleaned[k] = String(v);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (Object.keys(cleaned).length > 0) {
|
|
190
|
+
entry.environment = cleaned;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// enabled defaults to true
|
|
195
|
+
entry.enabled = data.enabled !== false;
|
|
196
|
+
|
|
197
|
+
return entry;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export {
|
|
201
|
+
listMcpConfigs,
|
|
202
|
+
getMcpConfig,
|
|
203
|
+
createMcpConfig,
|
|
204
|
+
updateMcpConfig,
|
|
205
|
+
deleteMcpConfig,
|
|
206
|
+
};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CONFIG_FILE,
|
|
3
|
+
readConfigLayers,
|
|
4
|
+
isPlainObject,
|
|
5
|
+
getConfigForPath,
|
|
6
|
+
writeConfig,
|
|
7
|
+
} from './shared.js';
|
|
8
|
+
|
|
9
|
+
function getProviderSources(providerId, workingDirectory) {
|
|
10
|
+
const layers = readConfigLayers(workingDirectory);
|
|
11
|
+
const { userConfig, projectConfig, customConfig, paths } = layers;
|
|
12
|
+
|
|
13
|
+
const customProviders = isPlainObject(customConfig?.provider) ? customConfig.provider : {};
|
|
14
|
+
const customProvidersAlias = isPlainObject(customConfig?.providers) ? customConfig.providers : {};
|
|
15
|
+
const projectProviders = isPlainObject(projectConfig?.provider) ? projectConfig.provider : {};
|
|
16
|
+
const projectProvidersAlias = isPlainObject(projectConfig?.providers) ? projectConfig.providers : {};
|
|
17
|
+
const userProviders = isPlainObject(userConfig?.provider) ? userConfig.provider : {};
|
|
18
|
+
const userProvidersAlias = isPlainObject(userConfig?.providers) ? userConfig.providers : {};
|
|
19
|
+
|
|
20
|
+
const customExists =
|
|
21
|
+
Object.prototype.hasOwnProperty.call(customProviders, providerId) ||
|
|
22
|
+
Object.prototype.hasOwnProperty.call(customProvidersAlias, providerId);
|
|
23
|
+
const projectExists =
|
|
24
|
+
Object.prototype.hasOwnProperty.call(projectProviders, providerId) ||
|
|
25
|
+
Object.prototype.hasOwnProperty.call(projectProvidersAlias, providerId);
|
|
26
|
+
const userExists =
|
|
27
|
+
Object.prototype.hasOwnProperty.call(userProviders, providerId) ||
|
|
28
|
+
Object.prototype.hasOwnProperty.call(userProvidersAlias, providerId);
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
sources: {
|
|
32
|
+
auth: { exists: false },
|
|
33
|
+
user: { exists: userExists, path: paths.userPath },
|
|
34
|
+
project: { exists: projectExists, path: paths.projectPath || null },
|
|
35
|
+
custom: { exists: customExists, path: paths.customPath }
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function removeProviderConfig(providerId, workingDirectory, scope = 'user') {
|
|
41
|
+
if (!providerId || typeof providerId !== 'string') {
|
|
42
|
+
throw new Error('Provider ID is required');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const layers = readConfigLayers(workingDirectory);
|
|
46
|
+
let targetPath = layers.paths.userPath;
|
|
47
|
+
|
|
48
|
+
if (scope === 'project') {
|
|
49
|
+
if (!workingDirectory) {
|
|
50
|
+
throw new Error('Working directory is required for project scope');
|
|
51
|
+
}
|
|
52
|
+
targetPath = layers.paths.projectPath || targetPath;
|
|
53
|
+
} else if (scope === 'custom') {
|
|
54
|
+
if (!layers.paths.customPath) {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
targetPath = layers.paths.customPath;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const targetConfig = getConfigForPath(layers, targetPath);
|
|
61
|
+
const providerConfig = isPlainObject(targetConfig.provider) ? targetConfig.provider : {};
|
|
62
|
+
const providersConfig = isPlainObject(targetConfig.providers) ? targetConfig.providers : {};
|
|
63
|
+
const removedProvider = Object.prototype.hasOwnProperty.call(providerConfig, providerId);
|
|
64
|
+
const removedProviders = Object.prototype.hasOwnProperty.call(providersConfig, providerId);
|
|
65
|
+
|
|
66
|
+
if (!removedProvider && !removedProviders) {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (removedProvider) {
|
|
71
|
+
delete providerConfig[providerId];
|
|
72
|
+
if (Object.keys(providerConfig).length === 0) {
|
|
73
|
+
delete targetConfig.provider;
|
|
74
|
+
} else {
|
|
75
|
+
targetConfig.provider = providerConfig;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (removedProviders) {
|
|
80
|
+
delete providersConfig[providerId];
|
|
81
|
+
if (Object.keys(providersConfig).length === 0) {
|
|
82
|
+
delete targetConfig.providers;
|
|
83
|
+
} else {
|
|
84
|
+
targetConfig.providers = providersConfig;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
writeConfig(targetConfig, targetPath || CONFIG_FILE);
|
|
89
|
+
console.log(`Removed provider ${providerId} from config: ${targetPath}`);
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export {
|
|
94
|
+
getProviderSources,
|
|
95
|
+
removeProviderConfig,
|
|
96
|
+
};
|