saycoder 0.1.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/README.md
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# saycoder
|
|
2
|
+
|
|
3
|
+
A small CLI that configures OpenCode to use SayCoder as the model layer, plus practical third-party MCP servers and lightweight coding skills.
|
|
4
|
+
|
|
5
|
+
## Setup flow
|
|
6
|
+
|
|
7
|
+
1. Checks whether `opencode` is installed.
|
|
8
|
+
2. If missing, asks whether to install it with `npm install -g opencode-ai`.
|
|
9
|
+
3. Prompts the user for a SayCoder API key.
|
|
10
|
+
4. Validates the key with `GET https://jstapi.xinbai.icu/v1/models`.
|
|
11
|
+
5. Uses the returned model list to populate OpenCode provider models.
|
|
12
|
+
6. Writes OpenCode config and installs lightweight SayCoder skills.
|
|
13
|
+
7. Tells the user to choose a model and start coding.
|
|
14
|
+
|
|
15
|
+
## MCP defaults
|
|
16
|
+
|
|
17
|
+
- `context7`: official remote MCP at `https://mcp.context7.com/mcp` for current docs.
|
|
18
|
+
- `time`: optional local MCP via `--time-mcp` and `uvx mcp-server-time` for current time and timezone conversion.
|
|
19
|
+
- `tavily`: optional remote web search MCP when `--tavily-api-key` is provided.
|
|
20
|
+
- `exa`: optional remote web/code search MCP when `--exa-api-key` is provided and Tavily is not set.
|
|
21
|
+
|
|
22
|
+
SayCoder's base URL is only used for the OpenAI-compatible model channel, not for MCP hosting.
|
|
23
|
+
|
|
24
|
+
## China mirror
|
|
25
|
+
|
|
26
|
+
When OpenCode is missing, setup asks whether to install through the China npm mirror.
|
|
27
|
+
You can also force it with:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
node bin/saycoder.js setup --npm-registry https://registry.npmmirror.com
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Installed skills
|
|
34
|
+
|
|
35
|
+
- `saycoder-lite-superpowers`: clarify user intent, split logical goals, implement production code, and report completed/not completed work.
|
|
36
|
+
- `saycoder-code-review`: lightweight production-readiness review before handoff.
|
|
37
|
+
- `saycoder-test-check`: choose and report the smallest meaningful verification.
|
|
38
|
+
|
|
39
|
+
## Local test
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
node bin/saycoder.js setup --dry-run --skip-install-check --skip-validate --api-key test-key
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Intended usage
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npx saycoder
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
You can also pass options directly:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
npx saycoder --npm-registry https://registry.npmmirror.com
|
|
55
|
+
```
|
package/bin/saycoder.js
ADDED
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, copyFileSync } from "node:fs";
|
|
4
|
+
import { dirname, join, resolve } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
import { createInterface } from "node:readline/promises";
|
|
8
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
9
|
+
import { spawnSync } from "node:child_process";
|
|
10
|
+
|
|
11
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
const packageRoot = resolve(__dirname, "..");
|
|
13
|
+
|
|
14
|
+
const DEFAULTS = {
|
|
15
|
+
providerId: "saycoder",
|
|
16
|
+
providerName: "SayCoder",
|
|
17
|
+
baseURL: "https://jstapi.xinbai.icu/",
|
|
18
|
+
apiKeyEnv: "SAYCODER_API_KEY",
|
|
19
|
+
configPath: join(homedir(), ".config", "opencode", "opencode.json"),
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const useColor = output.isTTY;
|
|
23
|
+
const paint = (code, value) => useColor ? `\x1b[${code}m${value}\x1b[0m` : value;
|
|
24
|
+
const color = {
|
|
25
|
+
blue: (value) => paint(34, value),
|
|
26
|
+
cyan: (value) => paint(36, value),
|
|
27
|
+
dim: (value) => paint(2, value),
|
|
28
|
+
green: (value) => paint(32, value),
|
|
29
|
+
red: (value) => paint(31, value),
|
|
30
|
+
yellow: (value) => paint(33, value),
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
function parseArgs(argv) {
|
|
34
|
+
const [rawCommand = "setup", ...rest] = argv;
|
|
35
|
+
const command = rawCommand.startsWith("--") ? "setup" : rawCommand;
|
|
36
|
+
const normalizedRest = rawCommand.startsWith("--") ? [rawCommand, ...rest] : rest;
|
|
37
|
+
const flags = { command };
|
|
38
|
+
|
|
39
|
+
for (let index = 0; index < normalizedRest.length; index += 1) {
|
|
40
|
+
const arg = normalizedRest[index];
|
|
41
|
+
if (!arg.startsWith("--")) continue;
|
|
42
|
+
|
|
43
|
+
const [key, inlineValue] = arg.slice(2).split("=", 2);
|
|
44
|
+
const next = normalizedRest[index + 1];
|
|
45
|
+
if (inlineValue !== undefined) {
|
|
46
|
+
flags[key] = inlineValue;
|
|
47
|
+
} else if (next && !next.startsWith("--")) {
|
|
48
|
+
flags[key] = next;
|
|
49
|
+
index += 1;
|
|
50
|
+
} else {
|
|
51
|
+
flags[key] = true;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
flags["enable-time-mcp"] = Boolean(flags["time-mcp"]);
|
|
56
|
+
if (flags["no-time-mcp"]) flags["enable-time-mcp"] = false;
|
|
57
|
+
|
|
58
|
+
return flags;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function usage() {
|
|
62
|
+
console.log(`SayCoder OpenCode setup
|
|
63
|
+
|
|
64
|
+
Usage:
|
|
65
|
+
saycoder setup [options]
|
|
66
|
+
|
|
67
|
+
Options:
|
|
68
|
+
--api-key <key> SayCoder API key. If omitted, prompts interactively.
|
|
69
|
+
--base-url <url> OpenAI-compatible API base URL. Default: ${DEFAULTS.baseURL}
|
|
70
|
+
--model <id> Main model ID. Default: first model from /models.
|
|
71
|
+
--small-model <id> Small model ID. Default: same as --model.
|
|
72
|
+
--tavily-api-key <key> Optional Tavily key for web search MCP.
|
|
73
|
+
--exa-api-key <key> Optional Exa key for web/code search MCP.
|
|
74
|
+
--time-mcp Enable local Time MCP via uvx mcp-server-time.
|
|
75
|
+
--no-time-mcp Disable local Time MCP. Default behavior.
|
|
76
|
+
--config <path> OpenCode config path. Default: ${DEFAULTS.configPath}
|
|
77
|
+
--dry-run Print config changes without writing files.
|
|
78
|
+
--yes Non-interactive confirmation for future prompts.
|
|
79
|
+
--npm-registry <url> Registry for installing OpenCode, eg https://registry.npmmirror.com.
|
|
80
|
+
--skip-install-check Skip OpenCode installation check.
|
|
81
|
+
--skip-validate Skip /models validation for local dry-run tests only.
|
|
82
|
+
`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function readJson(path) {
|
|
86
|
+
if (!existsSync(path)) return {};
|
|
87
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function sortObject(value) {
|
|
91
|
+
if (Array.isArray(value)) return value.map(sortObject);
|
|
92
|
+
if (!value || typeof value !== "object") return value;
|
|
93
|
+
return Object.fromEntries(Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, nested]) => [key, sortObject(nested)]));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function unique(values) {
|
|
97
|
+
return [...new Set(values.filter(Boolean))];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function commandExists(command) {
|
|
101
|
+
const result = spawnSync(process.platform === "win32" ? "where" : "command", process.platform === "win32" ? [command] : ["-v", command], {
|
|
102
|
+
shell: process.platform !== "win32",
|
|
103
|
+
stdio: "ignore",
|
|
104
|
+
});
|
|
105
|
+
return result.status === 0;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function installOpenCode(registry) {
|
|
109
|
+
const args = ["install", "-g", "opencode-ai"];
|
|
110
|
+
if (registry) args.push("--registry", registry);
|
|
111
|
+
step(`Installing OpenCode with npm${registry ? ` using ${registry}` : ""}...`);
|
|
112
|
+
const result = spawnSync("npm", args, { stdio: "inherit" });
|
|
113
|
+
if (result.status !== 0) throw new Error("OpenCode installation failed. Please install it manually and retry.");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function step(message) {
|
|
117
|
+
console.log(`${color.cyan("→")} ${message}`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function success(message) {
|
|
121
|
+
console.log(`${color.green("✓")} ${message}`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function warn(message) {
|
|
125
|
+
console.log(`${color.yellow("!")} ${message}`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function prompt(question) {
|
|
129
|
+
if (!input.isTTY) return "";
|
|
130
|
+
const rl = createInterface({ input, output });
|
|
131
|
+
try {
|
|
132
|
+
return (await rl.question(question)).trim();
|
|
133
|
+
} finally {
|
|
134
|
+
rl.close();
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function confirm(question, assumeYes) {
|
|
139
|
+
if (assumeYes) return true;
|
|
140
|
+
const answer = (await prompt(`${question} [y/N] `)).toLowerCase();
|
|
141
|
+
return answer === "y" || answer === "yes";
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function optionalSecret(flags, key, question) {
|
|
145
|
+
if (flags[key] !== undefined) return flags[key];
|
|
146
|
+
if (flags.yes || !input.isTTY) return "";
|
|
147
|
+
return await prompt(`${question} ${color.dim("(optional, press Enter to skip):")} `);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function normalizeBaseURL(baseURL) {
|
|
151
|
+
const trimmed = baseURL.replace(/\/+$/, "");
|
|
152
|
+
return trimmed.endsWith("/v1") ? trimmed : `${trimmed}/v1`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function modelsURL(baseURL) {
|
|
156
|
+
return `${normalizeBaseURL(baseURL)}/models`;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function fetchModels(baseURL, apiKey) {
|
|
160
|
+
const url = modelsURL(baseURL);
|
|
161
|
+
const response = await fetch(url, {
|
|
162
|
+
headers: {
|
|
163
|
+
Authorization: `Bearer ${apiKey}`,
|
|
164
|
+
Accept: "application/json",
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
const contentType = response.headers.get("content-type") || "";
|
|
169
|
+
const body = await response.text();
|
|
170
|
+
|
|
171
|
+
if (!response.ok) {
|
|
172
|
+
let detail = body.slice(0, 160).replace(/\s+/g, " ").trim();
|
|
173
|
+
try {
|
|
174
|
+
detail = JSON.parse(body).error?.message || detail;
|
|
175
|
+
} catch {}
|
|
176
|
+
throw new Error(`API key validation failed: GET ${url} returned HTTP ${response.status}${detail ? ` (${detail})` : ""}`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (!contentType.includes("application/json")) {
|
|
180
|
+
const preview = body.slice(0, 80).replace(/\s+/g, " ").trim();
|
|
181
|
+
throw new Error(`API key validation failed: GET ${url} did not return JSON. Check that the base URL is an OpenAI-compatible /v1 endpoint. Response starts with: ${preview}`);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
let payload;
|
|
185
|
+
try {
|
|
186
|
+
payload = JSON.parse(body);
|
|
187
|
+
} catch (error) {
|
|
188
|
+
throw new Error(`API key validation failed: GET ${url} returned invalid JSON (${error.message}).`);
|
|
189
|
+
}
|
|
190
|
+
const models = Array.isArray(payload.data) ? payload.data.map((item) => item.id).filter(Boolean) : [];
|
|
191
|
+
if (models.length === 0) throw new Error(`API key validation failed: GET ${url} returned no model ids.`);
|
|
192
|
+
return models;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function buildMcpConfig(flags) {
|
|
196
|
+
const mcp = {
|
|
197
|
+
context7: {
|
|
198
|
+
type: "remote",
|
|
199
|
+
url: "https://mcp.context7.com/mcp",
|
|
200
|
+
enabled: true,
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
if (flags["enable-time-mcp"] !== false) {
|
|
205
|
+
mcp.time = {
|
|
206
|
+
type: "local",
|
|
207
|
+
command: ["uvx", "mcp-server-time"],
|
|
208
|
+
enabled: true,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (flags["tavily-api-key"]) {
|
|
213
|
+
mcp.tavily = {
|
|
214
|
+
type: "remote",
|
|
215
|
+
url: "https://mcp.tavily.com/mcp/",
|
|
216
|
+
headers: {
|
|
217
|
+
Authorization: `Bearer ${flags["tavily-api-key"]}`,
|
|
218
|
+
},
|
|
219
|
+
enabled: true,
|
|
220
|
+
};
|
|
221
|
+
} else if (flags["exa-api-key"]) {
|
|
222
|
+
mcp.exa = {
|
|
223
|
+
type: "remote",
|
|
224
|
+
url: "https://mcp.exa.ai/mcp",
|
|
225
|
+
headers: {
|
|
226
|
+
Authorization: `Bearer ${flags["exa-api-key"]}`,
|
|
227
|
+
},
|
|
228
|
+
enabled: true,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return mcp;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function modelConfig(name) {
|
|
236
|
+
return {
|
|
237
|
+
name,
|
|
238
|
+
tool_call: true,
|
|
239
|
+
reasoning: true,
|
|
240
|
+
attachment: true,
|
|
241
|
+
limit: {
|
|
242
|
+
context: 128000,
|
|
243
|
+
output: 8192,
|
|
244
|
+
},
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function buildSayCoderConfig(flags, models) {
|
|
249
|
+
const providerId = DEFAULTS.providerId;
|
|
250
|
+
const apiKey = flags["api-key"];
|
|
251
|
+
const baseURL = normalizeBaseURL(flags["base-url"] || DEFAULTS.baseURL);
|
|
252
|
+
const model = flags.model || models[0];
|
|
253
|
+
const smallModel = flags["small-model"] || model;
|
|
254
|
+
const modelMap = Object.fromEntries(models.map((name) => [name, modelConfig(name)]));
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
model: `${providerId}/${model}`,
|
|
258
|
+
small_model: `${providerId}/${smallModel}`,
|
|
259
|
+
default_agent: "saycoder",
|
|
260
|
+
provider: {
|
|
261
|
+
[providerId]: {
|
|
262
|
+
npm: "@ai-sdk/openai-compatible",
|
|
263
|
+
name: DEFAULTS.providerName,
|
|
264
|
+
options: {
|
|
265
|
+
baseURL,
|
|
266
|
+
apiKey,
|
|
267
|
+
timeout: 300000,
|
|
268
|
+
chunkTimeout: 60000,
|
|
269
|
+
},
|
|
270
|
+
models: modelMap,
|
|
271
|
+
},
|
|
272
|
+
},
|
|
273
|
+
mcp: buildMcpConfig(flags),
|
|
274
|
+
agent: {
|
|
275
|
+
saycoder: {
|
|
276
|
+
mode: "primary",
|
|
277
|
+
description: "Production coding agent powered by SayCoder models, MCP, and lightweight skills.",
|
|
278
|
+
model: `${providerId}/${model}`,
|
|
279
|
+
permission: {
|
|
280
|
+
bash: "ask",
|
|
281
|
+
edit: "ask",
|
|
282
|
+
webfetch: "allow",
|
|
283
|
+
websearch: "allow",
|
|
284
|
+
},
|
|
285
|
+
prompt: [
|
|
286
|
+
"Use the saycoder-lite-superpowers skill for coding tasks.",
|
|
287
|
+
"Use saycoder-code-review before claiming production readiness when code was changed.",
|
|
288
|
+
"Use saycoder-test-check when selecting the smallest meaningful verification.",
|
|
289
|
+
"Clarify the user's real requirement when ambiguous.",
|
|
290
|
+
"Split work into logical goals, implement production-grade code, verify narrowly, and report completed/not completed items.",
|
|
291
|
+
].join("\n"),
|
|
292
|
+
},
|
|
293
|
+
},
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function mergeConfig(existing, incoming) {
|
|
298
|
+
const merged = {
|
|
299
|
+
...existing,
|
|
300
|
+
...incoming,
|
|
301
|
+
provider: {
|
|
302
|
+
...(existing.provider || {}),
|
|
303
|
+
...incoming.provider,
|
|
304
|
+
},
|
|
305
|
+
mcp: {
|
|
306
|
+
...(existing.mcp || {}),
|
|
307
|
+
...incoming.mcp,
|
|
308
|
+
},
|
|
309
|
+
agent: {
|
|
310
|
+
...(existing.agent || {}),
|
|
311
|
+
...incoming.agent,
|
|
312
|
+
},
|
|
313
|
+
plugin: unique([...(existing.plugin || []), ...(incoming.plugin || [])]),
|
|
314
|
+
};
|
|
315
|
+
if (merged.plugin.length === 0) delete merged.plugin;
|
|
316
|
+
return merged;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function installSkills(configDir, dryRun) {
|
|
320
|
+
const skillNames = ["saycoder-lite-superpowers", "saycoder-code-review", "saycoder-test-check"];
|
|
321
|
+
const targets = [];
|
|
322
|
+
|
|
323
|
+
for (const skillName of skillNames) {
|
|
324
|
+
const source = join(packageRoot, "templates", "skills", skillName, "SKILL.md");
|
|
325
|
+
const targetDir = join(configDir, "skills", skillName);
|
|
326
|
+
const target = join(targetDir, "SKILL.md");
|
|
327
|
+
targets.push(target);
|
|
328
|
+
|
|
329
|
+
if (!dryRun) {
|
|
330
|
+
mkdirSync(targetDir, { recursive: true });
|
|
331
|
+
copyFileSync(source, target);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return targets;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
async function setup(flags) {
|
|
339
|
+
const configPath = resolve(flags.config || DEFAULTS.configPath);
|
|
340
|
+
const configDir = dirname(configPath);
|
|
341
|
+
|
|
342
|
+
console.log(color.blue("\nSayCoder OpenCode Setup\n"));
|
|
343
|
+
|
|
344
|
+
if (!flags["skip-install-check"] && !commandExists("opencode")) {
|
|
345
|
+
const shouldInstall = await confirm("OpenCode is not installed. Install it now with npm?", Boolean(flags.yes));
|
|
346
|
+
if (!shouldInstall) {
|
|
347
|
+
console.log("Setup cancelled. Install OpenCode first, then rerun `saycoder setup`.");
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (!flags["npm-registry"] && input.isTTY) {
|
|
352
|
+
const useMirror = await confirm("Use China npm mirror for faster download?", false);
|
|
353
|
+
if (useMirror) flags["npm-registry"] = "https://registry.npmmirror.com";
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
installOpenCode(flags["npm-registry"]);
|
|
357
|
+
success("OpenCode installed.");
|
|
358
|
+
} else if (!flags["skip-install-check"]) {
|
|
359
|
+
success("OpenCode is installed.");
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (!flags["api-key"]) {
|
|
363
|
+
flags["api-key"] = await prompt("Enter your SayCoder API key: ");
|
|
364
|
+
}
|
|
365
|
+
if (!flags["api-key"]) throw new Error("API key is required.");
|
|
366
|
+
|
|
367
|
+
if (flags["enable-time-mcp"] && !commandExists("uvx")) {
|
|
368
|
+
const keepTime = await confirm("uvx is not installed. Keep Time MCP config anyway?", Boolean(flags.yes));
|
|
369
|
+
if (!keepTime) {
|
|
370
|
+
flags["enable-time-mcp"] = false;
|
|
371
|
+
warn("Time MCP skipped. Install uv later or rerun setup without --no-time-mcp.");
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const baseURL = flags["base-url"] || DEFAULTS.baseURL;
|
|
376
|
+
const models = flags["skip-validate"] ? [flags.model || "example-model"] : null;
|
|
377
|
+
if (models) {
|
|
378
|
+
warn(`Skipping API key validation. Using test model(s): ${models.join(", ")}`);
|
|
379
|
+
} else {
|
|
380
|
+
step(`Validating API key with ${modelsURL(baseURL)} ...`);
|
|
381
|
+
}
|
|
382
|
+
const validatedModels = models || await fetchModels(baseURL, flags["api-key"]);
|
|
383
|
+
if (!models) success(`API key is valid. Found ${validatedModels.length} model(s): ${validatedModels.join(", ")}`);
|
|
384
|
+
|
|
385
|
+
step("Writing OpenCode provider, MCP, agent, and skills config...");
|
|
386
|
+
const incoming = buildSayCoderConfig(flags, validatedModels);
|
|
387
|
+
const existing = readJson(configPath);
|
|
388
|
+
const merged = sortObject(mergeConfig(existing, incoming));
|
|
389
|
+
const skillPaths = installSkills(configDir, Boolean(flags["dry-run"]));
|
|
390
|
+
|
|
391
|
+
if (flags["dry-run"]) {
|
|
392
|
+
console.log(JSON.stringify(merged, null, 2));
|
|
393
|
+
console.log(`\nDry run only. Skills would be installed to:\n${skillPaths.map((item) => `- ${item}`).join("\n")}`);
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
mkdirSync(configDir, { recursive: true });
|
|
398
|
+
writeFileSync(configPath, `${JSON.stringify(merged, null, 2)}\n`);
|
|
399
|
+
|
|
400
|
+
success(`Updated OpenCode config: ${configPath}`);
|
|
401
|
+
success(`Installed ${skillPaths.length} skills.`);
|
|
402
|
+
console.log(color.green("\n模型选择中意模型,即可开始编程了!\n"));
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const flags = parseArgs(process.argv.slice(2));
|
|
406
|
+
|
|
407
|
+
try {
|
|
408
|
+
if (flags.command === "setup") await setup(flags);
|
|
409
|
+
else usage();
|
|
410
|
+
} catch (error) {
|
|
411
|
+
console.error(color.red(`saycoder: ${error.message}`));
|
|
412
|
+
process.exitCode = 1;
|
|
413
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "saycoder",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "SayCoder setup helper for OpenCode models, MCP servers, and skills.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"saycoder": "bin/saycoder.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"check": "node --check bin/saycoder.js",
|
|
11
|
+
"setup:local": "node bin/saycoder.js setup --dry-run --skip-install-check --skip-validate --api-key=test-key"
|
|
12
|
+
},
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=18"
|
|
15
|
+
},
|
|
16
|
+
"license": "UNLICENSED",
|
|
17
|
+
"private": false,
|
|
18
|
+
"files": [
|
|
19
|
+
"bin",
|
|
20
|
+
"templates"
|
|
21
|
+
]
|
|
22
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: saycoder-code-review
|
|
3
|
+
description: Lightweight production-readiness review for changed code before claiming work is complete.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# SayCoder Code Review
|
|
7
|
+
|
|
8
|
+
Use this skill after making code changes and before saying the work is production-ready.
|
|
9
|
+
|
|
10
|
+
## Review Checklist
|
|
11
|
+
|
|
12
|
+
1. Confirm the change directly satisfies the user's requested outcome.
|
|
13
|
+
2. Check for unnecessary scope expansion, hidden rewrites, or unrelated fixes.
|
|
14
|
+
3. Inspect error handling, edge cases, security-sensitive inputs, and config merging.
|
|
15
|
+
4. Check that names, structure, and style match the surrounding project.
|
|
16
|
+
5. Confirm the final response can clearly separate completed work from unfinished work.
|
|
17
|
+
|
|
18
|
+
## Output
|
|
19
|
+
|
|
20
|
+
Keep the review short:
|
|
21
|
+
|
|
22
|
+
- Pass: what looks ready
|
|
23
|
+
- Fix: what must change before handoff
|
|
24
|
+
- Risk: what remains uncertain after available verification
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: saycoder-lite-superpowers
|
|
3
|
+
description: Lightweight production coding workflow for OpenCode: clarify intent, split logical goals, implement safely, and report done/not done.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# SayCoder Lite Superpowers
|
|
7
|
+
|
|
8
|
+
Use this skill for coding tasks that should become production-quality changes without the overhead of a heavy process.
|
|
9
|
+
|
|
10
|
+
## Workflow
|
|
11
|
+
|
|
12
|
+
1. Confirm the user's actual outcome in one sentence when the request is ambiguous.
|
|
13
|
+
2. Split the task into logical goals that can be verified independently.
|
|
14
|
+
3. Inspect the existing project before editing; follow local conventions.
|
|
15
|
+
4. Implement the smallest production-grade change that satisfies the goals.
|
|
16
|
+
5. Verify with the narrowest meaningful check available.
|
|
17
|
+
6. Finish by stating exactly:
|
|
18
|
+
- What I completed
|
|
19
|
+
- What I did not complete
|
|
20
|
+
- Any verification run or blocker
|
|
21
|
+
|
|
22
|
+
## Guardrails
|
|
23
|
+
|
|
24
|
+
- Do not claim completion without verification or a named blocker.
|
|
25
|
+
- Do not hide partial work; call it out clearly.
|
|
26
|
+
- Prefer root-cause fixes over surface patches.
|
|
27
|
+
- Avoid broad rewrites unless explicitly requested.
|
|
28
|
+
- Ask only for decisions that block safe progress.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: saycoder-test-check
|
|
3
|
+
description: Choose the smallest meaningful verification for production coding tasks and report the exact result.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# SayCoder Test Check
|
|
7
|
+
|
|
8
|
+
Use this skill when deciding how to verify a coding change.
|
|
9
|
+
|
|
10
|
+
## Verification Ladder
|
|
11
|
+
|
|
12
|
+
Pick the narrowest check that proves the changed behavior:
|
|
13
|
+
|
|
14
|
+
1. Syntax check for parser/runtime errors.
|
|
15
|
+
2. Targeted unit or integration test for changed logic.
|
|
16
|
+
3. Focused command or dry-run for generated config/CLI behavior.
|
|
17
|
+
4. Broader test/lint/build only when the change touches shared behavior.
|
|
18
|
+
5. Manual inspection only when no executable check is available.
|
|
19
|
+
|
|
20
|
+
## Reporting
|
|
21
|
+
|
|
22
|
+
Always report:
|
|
23
|
+
|
|
24
|
+
- Command or inspection performed
|
|
25
|
+
- Result observed
|
|
26
|
+
- What was not verified and why
|