opensddrag 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/bin/opensddrag.js +22 -0
- package/package.json +30 -0
- package/src/api.js +50 -0
- package/src/commands/init.js +244 -0
- package/src/commands/status.js +130 -0
- package/src/templates/claude-md.js +69 -0
- package/src/templates/commands/index.js +837 -0
- package/src/templates/commands/opencode.js +813 -0
- package/src/templates/index.js +0 -0
- package/src/templates/skill-md.js +74 -0
- package/src/templates/skills/index.js +192 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { program } from "commander";
|
|
3
|
+
import { createRequire } from "module";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
import { dirname, join } from "path";
|
|
6
|
+
import { readFileSync } from "fs";
|
|
7
|
+
|
|
8
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, "../package.json"), "utf8"));
|
|
10
|
+
|
|
11
|
+
program
|
|
12
|
+
.name("opensddrag")
|
|
13
|
+
.description("Connect any project to the OpenSddRag SDD+Harness MCP server")
|
|
14
|
+
.version(pkg.version);
|
|
15
|
+
|
|
16
|
+
const { initCommand } = await import("../src/commands/init.js");
|
|
17
|
+
const { statusCommand } = await import("../src/commands/status.js");
|
|
18
|
+
|
|
19
|
+
program.addCommand(initCommand);
|
|
20
|
+
program.addCommand(statusCommand);
|
|
21
|
+
|
|
22
|
+
program.parse();
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "opensddrag",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "CLI client to connect any project to the OpenSddRag SDD+Harness MCP server",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"sdd",
|
|
7
|
+
"mcp",
|
|
8
|
+
"harness",
|
|
9
|
+
"spec-driven",
|
|
10
|
+
"ai"
|
|
11
|
+
],
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"type": "module",
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=18.0.0"
|
|
16
|
+
},
|
|
17
|
+
"bin": {
|
|
18
|
+
"opensddrag": "./bin/opensddrag.js"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"bin/",
|
|
22
|
+
"src/"
|
|
23
|
+
],
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@inquirer/prompts": "^7.0.0",
|
|
26
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
27
|
+
"chalk": "^5.3.0",
|
|
28
|
+
"commander": "^12.1.0"
|
|
29
|
+
}
|
|
30
|
+
}
|
package/src/api.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
2
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
3
|
+
|
|
4
|
+
const CLIENT_INFO = { name: "opensddrag-cli", version: "0.1.0" };
|
|
5
|
+
|
|
6
|
+
async function withMcp(serverUrl, fn, apiKey) {
|
|
7
|
+
const client = new Client(CLIENT_INFO);
|
|
8
|
+
const transportOpts = apiKey
|
|
9
|
+
? { requestInit: { headers: { Authorization: `Bearer ${apiKey}` } } }
|
|
10
|
+
: {};
|
|
11
|
+
const transport = new StreamableHTTPClientTransport(
|
|
12
|
+
new URL(`${serverUrl.replace(/\/+$/, "")}/mcp`),
|
|
13
|
+
transportOpts,
|
|
14
|
+
);
|
|
15
|
+
await client.connect(transport);
|
|
16
|
+
try {
|
|
17
|
+
return await fn(client);
|
|
18
|
+
} finally {
|
|
19
|
+
await client.close();
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function parseToolResult(result) {
|
|
24
|
+
const text = result.content?.[0]?.text ?? "";
|
|
25
|
+
try {
|
|
26
|
+
return JSON.parse(text);
|
|
27
|
+
} catch {
|
|
28
|
+
throw new Error(text || "Unexpected response from server");
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function checkHealth(serverUrl, apiKey) {
|
|
33
|
+
await withMcp(serverUrl, async () => {}, apiKey);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function createProject(serverUrl, { slug, name, description }, apiKey) {
|
|
37
|
+
return withMcp(serverUrl, async (client) => {
|
|
38
|
+
const args = { slug, name };
|
|
39
|
+
if (description) args.description = description;
|
|
40
|
+
const result = await client.callTool({ name: "create_project", arguments: args });
|
|
41
|
+
return parseToolResult(result);
|
|
42
|
+
}, apiKey);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function listProjects(serverUrl, apiKey) {
|
|
46
|
+
return withMcp(serverUrl, async (client) => {
|
|
47
|
+
const result = await client.callTool({ name: "list_projects", arguments: {} });
|
|
48
|
+
return parseToolResult(result);
|
|
49
|
+
}, apiKey);
|
|
50
|
+
}
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { input, confirm, checkbox, password } from "@inquirer/prompts";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
5
|
+
import { join, basename } from "path";
|
|
6
|
+
|
|
7
|
+
import { checkHealth, createProject } from "../api.js";
|
|
8
|
+
import { renderClaudeMdBlock, renderClaudeMdStandalone } from "../templates/claude-md.js";
|
|
9
|
+
import { getCommands } from "../templates/commands/index.js";
|
|
10
|
+
import { getOpenCodeCommands } from "../templates/commands/opencode.js";
|
|
11
|
+
import { getSkills } from "../templates/skills/index.js";
|
|
12
|
+
|
|
13
|
+
// ── Config writers ─────────────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
function writeClaudeCode(cwd, serverUrl, apiKey) {
|
|
16
|
+
const mcpPath = join(cwd, ".mcp.json");
|
|
17
|
+
let config = {};
|
|
18
|
+
if (existsSync(mcpPath)) {
|
|
19
|
+
try { config = JSON.parse(readFileSync(mcpPath, "utf8")); } catch {}
|
|
20
|
+
}
|
|
21
|
+
config.mcpServers = config.mcpServers || {};
|
|
22
|
+
const entry = { type: "http", url: `${serverUrl}/mcp` };
|
|
23
|
+
if (apiKey) {
|
|
24
|
+
entry.headers = { Authorization: `Bearer ${apiKey}` };
|
|
25
|
+
}
|
|
26
|
+
config.mcpServers.opensddrag = entry;
|
|
27
|
+
writeFileSync(mcpPath, JSON.stringify(config, null, 2) + "\n");
|
|
28
|
+
return ".mcp.json";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function writeOpenCode(cwd, serverUrl, _apiKey) {
|
|
32
|
+
const configPath = join(cwd, "opencode.json");
|
|
33
|
+
let config = {};
|
|
34
|
+
if (existsSync(configPath)) {
|
|
35
|
+
try { config = JSON.parse(readFileSync(configPath, "utf8")); } catch {}
|
|
36
|
+
}
|
|
37
|
+
config.mcp = config.mcp || {};
|
|
38
|
+
config.mcp.opensddrag = {
|
|
39
|
+
type: "remote",
|
|
40
|
+
url: `${serverUrl}/mcp`,
|
|
41
|
+
enabled: true,
|
|
42
|
+
};
|
|
43
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
44
|
+
return "opencode.json";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const TOOL_WRITERS = {
|
|
48
|
+
"Claude Code": writeClaudeCode,
|
|
49
|
+
"OpenCode": writeOpenCode,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// ── Command ────────────────────────────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
export const initCommand = new Command("init")
|
|
55
|
+
.description("Connect the current project to an OpenSddRag MCP server")
|
|
56
|
+
.option("--project <slug>", "Project slug (default: current directory name)")
|
|
57
|
+
.option("--name <name>", "Project display name")
|
|
58
|
+
.option("--server <url>", "OpenSddRag server URL", "http://localhost:8000")
|
|
59
|
+
.option("--api-key <key>", "API key for authenticated servers")
|
|
60
|
+
.option("--tools <list>", "Comma-separated tools to configure: claude,opencode (default: ask)")
|
|
61
|
+
.option("--yes", "Skip confirmation prompts")
|
|
62
|
+
.action(async (opts) => {
|
|
63
|
+
const cwd = process.cwd();
|
|
64
|
+
|
|
65
|
+
console.log(chalk.bold("\n OpenSddRag — Project Init\n"));
|
|
66
|
+
|
|
67
|
+
// ── Inputs ────────────────────────────────────────────────────────────────
|
|
68
|
+
const serverUrl = opts.server ||
|
|
69
|
+
await input({ message: "OpenSddRag server URL:", default: "http://localhost:8000" });
|
|
70
|
+
|
|
71
|
+
// Prompt for API key when connecting to a remote server
|
|
72
|
+
const isRemote = !serverUrl.includes("localhost") && !serverUrl.includes("127.0.0.1");
|
|
73
|
+
let apiKey = opts.apiKey || null;
|
|
74
|
+
if (!apiKey && isRemote && !opts.yes) {
|
|
75
|
+
apiKey = await password({
|
|
76
|
+
message: "API key (leave blank to skip — server must have AUTH_ENABLED=false):",
|
|
77
|
+
mask: "*",
|
|
78
|
+
}) || null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const slug = opts.project ||
|
|
82
|
+
await input({
|
|
83
|
+
message: "Project slug:",
|
|
84
|
+
default: basename(cwd).toLowerCase().replace(/[^a-z0-9-]/g, "-"),
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const name = opts.name ||
|
|
88
|
+
await input({
|
|
89
|
+
message: "Project display name:",
|
|
90
|
+
default: slug.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()),
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// ── Which tools to configure ──────────────────────────────────────────────
|
|
94
|
+
let selectedTools;
|
|
95
|
+
if (opts.tools) {
|
|
96
|
+
const map = { claude: "Claude Code", opencode: "OpenCode" };
|
|
97
|
+
selectedTools = opts.tools.split(",").map((t) => map[t.trim().toLowerCase()]).filter(Boolean);
|
|
98
|
+
} else {
|
|
99
|
+
selectedTools = await checkbox({
|
|
100
|
+
message: "Which AI tools to configure?",
|
|
101
|
+
choices: [
|
|
102
|
+
{ name: "Claude Code (.claude/settings.json)", value: "Claude Code", checked: true },
|
|
103
|
+
{ name: "OpenCode (opencode.json)", value: "OpenCode", checked: false },
|
|
104
|
+
],
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (selectedTools.length === 0) {
|
|
109
|
+
console.log(chalk.yellow("\n No tools selected. Exiting.\n"));
|
|
110
|
+
process.exit(0);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ── Preview ───────────────────────────────────────────────────────────────
|
|
114
|
+
console.log("\n" + chalk.dim(" Will create/update:"));
|
|
115
|
+
if (selectedTools.includes("Claude Code")) {
|
|
116
|
+
console.log(chalk.dim(" .mcp.json — MCP server (type: http)"));
|
|
117
|
+
}
|
|
118
|
+
console.log(chalk.dim(" .claude/skills/opensddrag-*/SKILL.md — individual skill per command"));
|
|
119
|
+
console.log(chalk.dim(" .agents/skills/opensddrag-*/SKILL.md — individual skill per command"));
|
|
120
|
+
if (selectedTools.includes("OpenCode")) {
|
|
121
|
+
console.log(chalk.dim(" opencode.json — MCP server"));
|
|
122
|
+
console.log(chalk.dim(" .opencode/skills/opensddrag-*/SKILL.md — OpenCode-native skills"));
|
|
123
|
+
console.log(chalk.dim(" .opencode/commands/opsr/ — slash commands (/opsr:propose, /opsr:apply...)"));
|
|
124
|
+
}
|
|
125
|
+
console.log(chalk.dim(" .claude/commands/opsr/ — slash commands (/opsr:propose, /opsr:apply...)"));
|
|
126
|
+
console.log(chalk.dim(" CLAUDE.md — OpenSddRag section"));
|
|
127
|
+
console.log(chalk.dim(` Remote: register '${slug}' in central database\n`));
|
|
128
|
+
|
|
129
|
+
if (!opts.yes) {
|
|
130
|
+
const ok = await confirm({ message: "Proceed?", default: true });
|
|
131
|
+
if (!ok) process.exit(0);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ── 1. Server health ──────────────────────────────────────────────────────
|
|
135
|
+
process.stdout.write(chalk.bold(" 1/4 ") + "Connecting to server... ");
|
|
136
|
+
try {
|
|
137
|
+
await checkHealth(serverUrl, apiKey);
|
|
138
|
+
console.log(chalk.green("✓"));
|
|
139
|
+
} catch {
|
|
140
|
+
console.log(chalk.red("✗"));
|
|
141
|
+
console.error(chalk.red(`\n Cannot reach ${serverUrl}`));
|
|
142
|
+
console.error(chalk.dim(" Make sure the OpenSddRag server is running:"));
|
|
143
|
+
console.error(chalk.dim(" docker compose up -d"));
|
|
144
|
+
process.exit(1);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ── 2. Register project ───────────────────────────────────────────────────
|
|
148
|
+
process.stdout.write(chalk.bold(" 2/4 ") + "Registering project in database... ");
|
|
149
|
+
let project;
|
|
150
|
+
try {
|
|
151
|
+
project = await createProject(serverUrl, { slug, name }, apiKey);
|
|
152
|
+
console.log(project.already_existed
|
|
153
|
+
? chalk.yellow("✓ (already existed)")
|
|
154
|
+
: chalk.green(`✓ (id: ${project.id})`));
|
|
155
|
+
} catch (err) {
|
|
156
|
+
console.log(chalk.red("✗"));
|
|
157
|
+
console.error(chalk.red(`\n ${err.message}`));
|
|
158
|
+
process.exit(1);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ── 3. Configure AI tools ─────────────────────────────────────────────────
|
|
162
|
+
process.stdout.write(chalk.bold(" 3/4 ") + "Configuring AI tools... ");
|
|
163
|
+
const configured = [];
|
|
164
|
+
for (const tool of selectedTools) {
|
|
165
|
+
const file = TOOL_WRITERS[tool](cwd, serverUrl, apiKey);
|
|
166
|
+
configured.push(`${tool} → ${file}`);
|
|
167
|
+
}
|
|
168
|
+
// Individual skill files per command
|
|
169
|
+
const skills = getSkills(slug, serverUrl);
|
|
170
|
+
const skillRoots = [
|
|
171
|
+
join(cwd, ".claude", "skills"),
|
|
172
|
+
join(cwd, ".agents", "skills"),
|
|
173
|
+
];
|
|
174
|
+
for (const skill of skills) {
|
|
175
|
+
for (const root of skillRoots) {
|
|
176
|
+
const skillDir = join(root, skill.name);
|
|
177
|
+
mkdirSync(skillDir, { recursive: true });
|
|
178
|
+
writeFileSync(join(skillDir, "SKILL.md"), skill.content);
|
|
179
|
+
}
|
|
180
|
+
configured.push(`skill → .claude/skills/${skill.name}/SKILL.md`);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Install OpenCode-native skills if OpenCode is selected
|
|
184
|
+
if (selectedTools.includes("OpenCode")) {
|
|
185
|
+
const opencodeSkillsRoot = join(cwd, ".opencode", "skills");
|
|
186
|
+
for (const skill of skills) {
|
|
187
|
+
const skillDir = join(opencodeSkillsRoot, skill.name);
|
|
188
|
+
mkdirSync(skillDir, { recursive: true });
|
|
189
|
+
writeFileSync(join(skillDir, "SKILL.md"), skill.content);
|
|
190
|
+
configured.push(`skill → .opencode/skills/${skill.name}/SKILL.md`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
console.log(chalk.green("✓"));
|
|
194
|
+
for (const c of configured) console.log(chalk.dim(` ${c}`));
|
|
195
|
+
|
|
196
|
+
// ── 4. Slash commands (.claude/commands/opsr/) ───────────────────────────
|
|
197
|
+
process.stdout.write(chalk.bold(" 4/5 ") + "Writing slash commands... ");
|
|
198
|
+
const commands = getCommands(slug, serverUrl);
|
|
199
|
+
for (const cmd of commands) {
|
|
200
|
+
const cmdDir = join(cwd, ".claude", "commands", cmd.folder);
|
|
201
|
+
mkdirSync(cmdDir, { recursive: true });
|
|
202
|
+
writeFileSync(join(cmdDir, `${cmd.name}.md`), cmd.content);
|
|
203
|
+
}
|
|
204
|
+
console.log(chalk.green(`✓ (${commands.length} commands)`));
|
|
205
|
+
for (const cmd of commands) {
|
|
206
|
+
console.log(chalk.dim(` /${cmd.folder}:${cmd.name}`));
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Install OpenCode-native commands if OpenCode is selected
|
|
210
|
+
if (selectedTools.includes("OpenCode")) {
|
|
211
|
+
const opencodeCommands = getOpenCodeCommands(slug, serverUrl);
|
|
212
|
+
for (const cmd of opencodeCommands) {
|
|
213
|
+
const cmdDir = join(cwd, ".opencode", "commands", cmd.folder);
|
|
214
|
+
mkdirSync(cmdDir, { recursive: true });
|
|
215
|
+
writeFileSync(join(cmdDir, `${cmd.name}.md`), cmd.content);
|
|
216
|
+
configured.push(`command → .opencode/commands/${cmd.folder}/${cmd.name}.md`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// ── 5. CLAUDE.md ──────────────────────────────────────────────────────────
|
|
221
|
+
process.stdout.write(chalk.bold(" 5/5 ") + "Updating CLAUDE.md... ");
|
|
222
|
+
const claudeMdPath = join(cwd, "CLAUDE.md");
|
|
223
|
+
if (existsSync(claudeMdPath)) {
|
|
224
|
+
const content = readFileSync(claudeMdPath, "utf8");
|
|
225
|
+
if (content.includes("OpenSddRag")) {
|
|
226
|
+
console.log(chalk.yellow("✓ (already has OpenSddRag section)"));
|
|
227
|
+
} else {
|
|
228
|
+
writeFileSync(claudeMdPath, content.trimEnd() + "\n" + renderClaudeMdBlock({ slug, serverUrl }) + "\n");
|
|
229
|
+
console.log(chalk.green("✓ (appended)"));
|
|
230
|
+
}
|
|
231
|
+
} else {
|
|
232
|
+
writeFileSync(claudeMdPath, renderClaudeMdStandalone({ projectName: name, slug, serverUrl }));
|
|
233
|
+
console.log(chalk.green("✓ (created)"));
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// opensddrag.yaml (local project marker)
|
|
237
|
+
if (!existsSync(join(cwd, "opensddrag.yaml"))) {
|
|
238
|
+
writeFileSync(join(cwd, "opensddrag.yaml"), `project: ${slug}\nserver: ${serverUrl}\n`);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// ── Done ──────────────────────────────────────────────────────────────────
|
|
242
|
+
console.log(chalk.bold.green("\n ✓ Project connected to OpenSddRag!\n"));
|
|
243
|
+
console.log(" Open the project in your AI tool — the MCP server is configured.\n");
|
|
244
|
+
});
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import { existsSync, readFileSync } from "fs";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
|
|
6
|
+
import { checkHealth, listProjects } from "../api.js";
|
|
7
|
+
|
|
8
|
+
export const statusCommand = new Command("status")
|
|
9
|
+
.description("Check OpenSddRag connection status for the current project")
|
|
10
|
+
.action(async () => {
|
|
11
|
+
const cwd = process.cwd();
|
|
12
|
+
|
|
13
|
+
console.log(chalk.bold("\n OpenSddRag — Status\n"));
|
|
14
|
+
|
|
15
|
+
let serverUrl = "http://localhost:8000";
|
|
16
|
+
let slug = null;
|
|
17
|
+
|
|
18
|
+
// ── Local config ──────────────────────────────────────────────────────────
|
|
19
|
+
const yamlPath = join(cwd, "opensddrag.yaml");
|
|
20
|
+
if (existsSync(yamlPath)) {
|
|
21
|
+
const yaml = readFileSync(yamlPath, "utf8");
|
|
22
|
+
const slugMatch = yaml.match(/^project:\s*(.+)$/m);
|
|
23
|
+
const serverMatch = yaml.match(/^server:\s*(.+)$/m);
|
|
24
|
+
if (slugMatch) slug = slugMatch[1].trim();
|
|
25
|
+
if (serverMatch) serverUrl = serverMatch[1].trim();
|
|
26
|
+
console.log(chalk.green(" ✓") + ` opensddrag.yaml → project: ${chalk.cyan(slug)}`);
|
|
27
|
+
} else {
|
|
28
|
+
console.log(chalk.red(" ✗") + " opensddrag.yaml not found — run: " + chalk.dim("opensddrag init"));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ── Skills ────────────────────────────────────────────────────────────────
|
|
32
|
+
console.log(chalk.dim("\n Skills (.claude/skills/ and .agents/skills/):"));
|
|
33
|
+
const expectedSkills = ["opensddrag-propose","opensddrag-spec","opensddrag-design","opensddrag-tasks","opensddrag-apply","opensddrag-verify","opensddrag-sync","opensddrag-archive","opensddrag-explore","opensddrag-continue","opensddrag-status","opensddrag-flow","opensddrag-search"];
|
|
34
|
+
const missingSkills = expectedSkills.filter((s) => !existsSync(join(cwd, ".claude", "skills", s, "SKILL.md")));
|
|
35
|
+
if (missingSkills.length === 0) {
|
|
36
|
+
console.log(chalk.green(" ✓") + " all " + expectedSkills.length + " skills present");
|
|
37
|
+
} else {
|
|
38
|
+
const present = expectedSkills.length - missingSkills.length;
|
|
39
|
+
console.log(chalk.yellow(` ! ${present}/${expectedSkills.length} skills present — missing: `) + missingSkills.join(", "));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ── Claude Code ───────────────────────────────────────────────────────────
|
|
43
|
+
console.log(chalk.dim("\n Claude Code:"));
|
|
44
|
+
const mcpJsonPath = join(cwd, ".mcp.json");
|
|
45
|
+
if (existsSync(mcpJsonPath)) {
|
|
46
|
+
try {
|
|
47
|
+
const cfg = JSON.parse(readFileSync(mcpJsonPath, "utf8"));
|
|
48
|
+
const mcp = cfg?.mcpServers?.opensddrag;
|
|
49
|
+
if (mcp) {
|
|
50
|
+
console.log(chalk.green(" ✓") + " .mcp.json → type: " + chalk.cyan(mcp.type) + " url: " + chalk.cyan(mcp.url));
|
|
51
|
+
} else {
|
|
52
|
+
console.log(chalk.yellow(" !") + " .mcp.json exists but 'opensddrag' not configured");
|
|
53
|
+
}
|
|
54
|
+
} catch {
|
|
55
|
+
console.log(chalk.red(" ✗") + " .mcp.json — invalid JSON");
|
|
56
|
+
}
|
|
57
|
+
} else {
|
|
58
|
+
console.log(chalk.dim(" –") + " .mcp.json not found");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
// ── Slash commands ────────────────────────────────────────────────────────
|
|
63
|
+
console.log(chalk.dim("\n Slash commands (.claude/commands/opsr/):"));
|
|
64
|
+
const expectedCommands = ["propose", "spec", "design", "tasks", "apply", "verify", "sync", "archive", "explore", "continue", "status", "flow", "search"];
|
|
65
|
+
const opsrDir = join(cwd, ".claude", "commands", "opsr");
|
|
66
|
+
const missingCmds = expectedCommands.filter((c) => !existsSync(join(opsrDir, `${c}.md`)));
|
|
67
|
+
if (missingCmds.length === 0) {
|
|
68
|
+
console.log(chalk.green(" ✓") + " all commands present: " + chalk.dim(expectedCommands.map((c) => `/opsr:${c}`).join(", ")));
|
|
69
|
+
} else {
|
|
70
|
+
const present = expectedCommands.filter((c) => existsSync(join(opsrDir, `${c}.md`)));
|
|
71
|
+
if (present.length > 0) console.log(chalk.green(" ✓") + " " + present.map((c) => `/opsr:${c}`).join(", "));
|
|
72
|
+
console.log(chalk.yellow(" !") + " missing: " + missingCmds.map((c) => `/opsr:${c}`).join(", "));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ── OpenCode ──────────────────────────────────────────────────────────────
|
|
76
|
+
console.log(chalk.dim("\n OpenCode:"));
|
|
77
|
+
const opencodePath = join(cwd, "opencode.json");
|
|
78
|
+
if (existsSync(opencodePath)) {
|
|
79
|
+
try {
|
|
80
|
+
const cfg = JSON.parse(readFileSync(opencodePath, "utf8"));
|
|
81
|
+
const mcp = cfg?.mcp?.opensddrag;
|
|
82
|
+
if (mcp) {
|
|
83
|
+
console.log(chalk.green(" ✓") + " opencode.json → " + chalk.cyan(mcp.url));
|
|
84
|
+
} else {
|
|
85
|
+
console.log(chalk.yellow(" !") + " opencode.json exists but 'opensddrag' not configured");
|
|
86
|
+
}
|
|
87
|
+
} catch {
|
|
88
|
+
console.log(chalk.red(" ✗") + " opencode.json — invalid JSON");
|
|
89
|
+
}
|
|
90
|
+
} else {
|
|
91
|
+
console.log(chalk.dim(" –") + " opencode.json not found");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ── CLAUDE.md ─────────────────────────────────────────────────────────────
|
|
95
|
+
const claudeMd = join(cwd, "CLAUDE.md");
|
|
96
|
+
console.log(chalk.dim("\n CLAUDE.md:"));
|
|
97
|
+
if (existsSync(claudeMd)) {
|
|
98
|
+
const content = readFileSync(claudeMd, "utf8");
|
|
99
|
+
console.log(
|
|
100
|
+
(content.includes("OpenSddRag") ? chalk.green(" ✓") : chalk.yellow(" !")) +
|
|
101
|
+
" CLAUDE.md" +
|
|
102
|
+
(content.includes("OpenSddRag") ? "" : " (missing OpenSddRag section)")
|
|
103
|
+
);
|
|
104
|
+
} else {
|
|
105
|
+
console.log(chalk.dim(" –") + " CLAUDE.md not found");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ── Server health ─────────────────────────────────────────────────────────
|
|
109
|
+
console.log(chalk.dim("\n Server:"));
|
|
110
|
+
process.stdout.write(` ${serverUrl} ... `);
|
|
111
|
+
try {
|
|
112
|
+
await checkHealth(serverUrl);
|
|
113
|
+
console.log(chalk.green("online ✓"));
|
|
114
|
+
|
|
115
|
+
if (slug) {
|
|
116
|
+
const projects = await listProjects(serverUrl);
|
|
117
|
+
const mine = projects.find((p) => p.slug === slug);
|
|
118
|
+
console.log(
|
|
119
|
+
(mine ? chalk.green(" ✓") : chalk.yellow(" !")) +
|
|
120
|
+
` project '${slug}' ` +
|
|
121
|
+
(mine ? `registered (id: ${mine.id})` : "NOT registered in database — run: opensddrag init")
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
} catch {
|
|
125
|
+
console.log(chalk.red("offline ✗"));
|
|
126
|
+
console.log(chalk.dim(" docker compose up -d or opensddrag server start --transport sse"));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
console.log("");
|
|
130
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
export function renderClaudeMdBlock({ slug, serverUrl }) {
|
|
2
|
+
return `
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
## OpenSddRag — SDD + Harness
|
|
6
|
+
|
|
7
|
+
This project uses **OpenSddRag** for Spec-Driven Development with persistent semantic memory.
|
|
8
|
+
|
|
9
|
+
- **MCP server name:** \`opensddrag\` (${serverUrl}) — configured in \`.mcp.json\`
|
|
10
|
+
- **Project slug:** \`${slug}\`
|
|
11
|
+
- **Skills:** \`.claude/skills/opensddrag-*/SKILL.md\`
|
|
12
|
+
- **Commands:** \`.claude/commands/opsr/\`
|
|
13
|
+
|
|
14
|
+
### MCP Tools (opensddrag server)
|
|
15
|
+
|
|
16
|
+
The \`opensddrag\` MCP server exposes these tools — they appear in your tool list under the \`opensddrag\` namespace:
|
|
17
|
+
|
|
18
|
+
| Tool | Purpose |
|
|
19
|
+
|------|---------|
|
|
20
|
+
| \`create_artifact\` | Create proposals, specs, designs, tasks |
|
|
21
|
+
| \`read_artifact\` | Read an artifact by name |
|
|
22
|
+
| \`list_artifacts\` | List artifacts with type/status filters |
|
|
23
|
+
| \`update_artifact\` | Update content or status |
|
|
24
|
+
| \`validate_artifact\` | Check spec structure |
|
|
25
|
+
| \`link_artifacts\` | Link artifacts (implements / depends_on / relates_to) |
|
|
26
|
+
| \`get_relationships\` | Get linked artifacts |
|
|
27
|
+
| \`search_semantic\` | Semantic search via pgvector |
|
|
28
|
+
| \`recall_episodes\` | Find past agent actions (episodic memory) |
|
|
29
|
+
| \`get_working_context\` | Get active session context |
|
|
30
|
+
| \`update_working_context\` | Update session context |
|
|
31
|
+
| \`record_trace\` | Log an action to episodic memory |
|
|
32
|
+
|
|
33
|
+
> If these tools are NOT in your active tool list, the server is not connected.
|
|
34
|
+
> Start it with \`docker compose up -d\` and reload the project. Do not attempt to work around a missing server.
|
|
35
|
+
|
|
36
|
+
### Before implementing any feature
|
|
37
|
+
|
|
38
|
+
Always search for existing specs first:
|
|
39
|
+
|
|
40
|
+
\`\`\`
|
|
41
|
+
search_semantic(query="<topic>", project_slug="${slug}")
|
|
42
|
+
\`\`\`
|
|
43
|
+
|
|
44
|
+
### SDD Commands
|
|
45
|
+
|
|
46
|
+
| Command | When to use |
|
|
47
|
+
|---------|-------------|
|
|
48
|
+
| \`/opsr:propose\` | Start here — capture intent and scope before any code |
|
|
49
|
+
| \`/opsr:spec\` | Formalize requirements (Purpose / SHALL / Scenarios) |
|
|
50
|
+
| \`/opsr:design\` | Document technical decisions and trade-offs |
|
|
51
|
+
| \`/opsr:tasks\` | Decompose spec into atomic tasks (< 4h each) |
|
|
52
|
+
| \`/opsr:apply\` | Implement the next pending task against spec criteria |
|
|
53
|
+
| \`/opsr:flow\` | Run the full flow end-to-end for a feature |
|
|
54
|
+
| \`/opsr:search\` | Semantic search over specs and past work |
|
|
55
|
+
| \`/opsr:status\` | Show what's in progress and what's done |
|
|
56
|
+
| \`/opsr:archive\` | Mark a completed feature as archived |
|
|
57
|
+
|
|
58
|
+
### SDD Flow
|
|
59
|
+
|
|
60
|
+
\`\`\`
|
|
61
|
+
/opsr:propose → /opsr:spec → /opsr:design → /opsr:tasks → /opsr:apply → /opsr:archive
|
|
62
|
+
\`\`\`
|
|
63
|
+
`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function renderClaudeMdStandalone({ projectName, slug, serverUrl }) {
|
|
67
|
+
return `# ${projectName}
|
|
68
|
+
${renderClaudeMdBlock({ slug, serverUrl })}`;
|
|
69
|
+
}
|