@relentlessbuild/decs-mcp 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/README.md +54 -0
- package/codex/AGENTS.md +7 -0
- package/codex/prompts/decs-context.md +5 -0
- package/codex/prompts/decs-init.md +9 -0
- package/codex/prompts/decs-log.md +6 -0
- package/codex/prompts/decs-update.md +7 -0
- package/dist/cli/args.js +47 -0
- package/dist/cli/output.js +12 -0
- package/dist/cli/prompt.js +43 -0
- package/dist/commands/doctor.js +58 -0
- package/dist/commands/init.js +35 -0
- package/dist/commands/setup-claude-desktop.js +64 -0
- package/dist/commands/setup-codex.js +92 -0
- package/dist/commands/shared.js +35 -0
- package/dist/config.js +93 -0
- package/dist/decision-context.js +71 -0
- package/dist/errors.js +19 -0
- package/dist/index.js +85 -0
- package/dist/install/codex-assets.js +98 -0
- package/dist/install/fs-utils.js +29 -0
- package/dist/install/json-merge.js +28 -0
- package/dist/install/paths.js +27 -0
- package/dist/install/toml-merge.js +31 -0
- package/dist/relentless-api.js +107 -0
- package/dist/repo-config.js +71 -0
- package/dist/server.js +16 -0
- package/dist/tools.js +304 -0
- package/dist/types.js +1 -0
- package/docs/codex-quickstart.md +59 -0
- package/docs/legacy-claude-code-hooks.md +37 -0
- package/docs/mcp-clients.md +121 -0
- package/package.json +42 -0
- package/templates/claude_desktop_config.example.json +17 -0
- package/templates/codex-config.example.toml +10 -0
- package/templates/decs-config.example.json +5 -0
package/README.md
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Relentless DECS MCP
|
|
2
|
+
|
|
3
|
+
DECS (Decision-Embedded Context System) as an MCP server for Codex, Claude Desktop, and other MCP clients.
|
|
4
|
+
|
|
5
|
+
## Fast Path: Codex Setup
|
|
6
|
+
|
|
7
|
+
Use one command and follow prompts:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx @relentless/decs-mcp setup codex --repo /path/to/your/repo
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Local clone flow:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
git clone https://github.com/RelentlessToph/relentless-decs.git
|
|
17
|
+
cd relentless-decs
|
|
18
|
+
./install-codex.sh /path/to/your/repo
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
This local clone flow configures MCP using your local `dist/index.js` path (no npm publish required).
|
|
22
|
+
|
|
23
|
+
Then in Codex:
|
|
24
|
+
|
|
25
|
+
```text
|
|
26
|
+
npx @relentless/decs-mcp init <project-node-id> [project-name] --repo /path/to/your/repo
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Command Summary
|
|
30
|
+
|
|
31
|
+
- `relentless-decs-mcp serve`
|
|
32
|
+
- `relentless-decs-mcp setup codex [--repo <path>] [--yes] [--dry-run] [--no-prompts] [--use-local-server]`
|
|
33
|
+
- `relentless-decs-mcp setup claude-desktop [--platform macos|windows] [--yes] [--dry-run] [--use-local-server]`
|
|
34
|
+
- `relentless-decs-mcp doctor [--repo <path>]`
|
|
35
|
+
- `relentless-decs-mcp init <project-node-id> [project-name] [--repo <path>] [--bootstrap]`
|
|
36
|
+
|
|
37
|
+
## Docs
|
|
38
|
+
|
|
39
|
+
- Codex quickstart: `docs/codex-quickstart.md`
|
|
40
|
+
- MCP clients (Codex + Claude Desktop): `docs/mcp-clients.md`
|
|
41
|
+
- Legacy Claude Code hook install: `docs/legacy-claude-code-hooks.md`
|
|
42
|
+
|
|
43
|
+
## Development
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
npm install
|
|
47
|
+
npm run typecheck
|
|
48
|
+
npm test
|
|
49
|
+
npm run build
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## License
|
|
53
|
+
|
|
54
|
+
MIT
|
package/codex/AGENTS.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Relentless DECS Workflow
|
|
2
|
+
|
|
3
|
+
1. Before architecture-affecting work, call `decs_get_context`.
|
|
4
|
+
2. Draft decisions with: title, what, why, purpose, constraints.
|
|
5
|
+
3. Ask explicit confirmation before `decs_create` or `decs_update`.
|
|
6
|
+
4. If a key decision is contradicted, explain it and ask whether to supersede.
|
|
7
|
+
5. If `.decs.json` is missing, run DECS init flow first.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
Initialize Relentless DECS for this repository.
|
|
2
|
+
|
|
3
|
+
Arguments: `<project-node-id> [project-name]`
|
|
4
|
+
|
|
5
|
+
1. Validate project node id.
|
|
6
|
+
2. Call `decs_init_space`.
|
|
7
|
+
3. Ask for confirmation to write `.decs.json`.
|
|
8
|
+
4. Call `decs_write_repo_config`.
|
|
9
|
+
5. Offer bootstrap key decision via `decs_create`.
|
package/dist/cli/args.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export function parseArgs(argv) {
|
|
2
|
+
const positional = [];
|
|
3
|
+
const flags = {};
|
|
4
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
5
|
+
const token = argv[i];
|
|
6
|
+
if (!token) {
|
|
7
|
+
continue;
|
|
8
|
+
}
|
|
9
|
+
if (!token.startsWith("--")) {
|
|
10
|
+
positional.push(token);
|
|
11
|
+
continue;
|
|
12
|
+
}
|
|
13
|
+
const [rawKey, rawValue] = token.slice(2).split("=", 2);
|
|
14
|
+
if (!rawKey) {
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
if (rawValue !== undefined) {
|
|
18
|
+
flags[rawKey] = rawValue;
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
const next = argv[i + 1];
|
|
22
|
+
if (next && !next.startsWith("--")) {
|
|
23
|
+
flags[rawKey] = next;
|
|
24
|
+
i += 1;
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
flags[rawKey] = true;
|
|
28
|
+
}
|
|
29
|
+
return { positional, flags };
|
|
30
|
+
}
|
|
31
|
+
export function flagBoolean(flags, name, defaultValue = false) {
|
|
32
|
+
const value = flags[name];
|
|
33
|
+
if (value === undefined) {
|
|
34
|
+
return defaultValue;
|
|
35
|
+
}
|
|
36
|
+
if (typeof value === "boolean") {
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
return ["1", "true", "yes", "on"].includes(value.toLowerCase());
|
|
40
|
+
}
|
|
41
|
+
export function flagString(flags, name) {
|
|
42
|
+
const value = flags[name];
|
|
43
|
+
if (typeof value === "string") {
|
|
44
|
+
return value;
|
|
45
|
+
}
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export function logStep(message) {
|
|
2
|
+
process.stdout.write(`${message}\n`);
|
|
3
|
+
}
|
|
4
|
+
export function logWarn(message) {
|
|
5
|
+
process.stdout.write(`WARN: ${message}\n`);
|
|
6
|
+
}
|
|
7
|
+
export function logError(message) {
|
|
8
|
+
process.stderr.write(`ERROR: ${message}\n`);
|
|
9
|
+
}
|
|
10
|
+
export function logJson(data) {
|
|
11
|
+
process.stdout.write(`${JSON.stringify(data, null, 2)}\n`);
|
|
12
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { createInterface } from "node:readline/promises";
|
|
2
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
3
|
+
function maskSecret(value) {
|
|
4
|
+
if (value.length <= 4) {
|
|
5
|
+
return "****";
|
|
6
|
+
}
|
|
7
|
+
return `${value.slice(0, 4)}***`;
|
|
8
|
+
}
|
|
9
|
+
export async function promptText(label, options) {
|
|
10
|
+
const rl = createInterface({ input, output });
|
|
11
|
+
try {
|
|
12
|
+
const suffix = options?.defaultValue ? ` [${options.defaultValue}]` : "";
|
|
13
|
+
const raw = await rl.question(`${label}${suffix}: `);
|
|
14
|
+
const value = raw.trim();
|
|
15
|
+
if (value.length > 0) {
|
|
16
|
+
if (options?.secret) {
|
|
17
|
+
output.write(`Saved ${label}: ${maskSecret(value)}\n`);
|
|
18
|
+
}
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
if (options?.defaultValue) {
|
|
22
|
+
return options.defaultValue;
|
|
23
|
+
}
|
|
24
|
+
return "";
|
|
25
|
+
}
|
|
26
|
+
finally {
|
|
27
|
+
rl.close();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export async function promptYesNo(label, defaultYes = true) {
|
|
31
|
+
const rl = createInterface({ input, output });
|
|
32
|
+
try {
|
|
33
|
+
const suffix = defaultYes ? " [Y/n]" : " [y/N]";
|
|
34
|
+
const raw = (await rl.question(`${label}${suffix}: `)).trim().toLowerCase();
|
|
35
|
+
if (!raw) {
|
|
36
|
+
return defaultYes;
|
|
37
|
+
}
|
|
38
|
+
return raw === "y" || raw === "yes";
|
|
39
|
+
}
|
|
40
|
+
finally {
|
|
41
|
+
rl.close();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import TOML from "@iarna/toml";
|
|
5
|
+
import { getConfigPaths, resolveCredentials } from "../config.js";
|
|
6
|
+
import { logStep } from "../cli/output.js";
|
|
7
|
+
import { readRepoDecsConfig } from "../repo-config.js";
|
|
8
|
+
import { getCodexUserConfigPath, resolveClaudeDesktopConfigPath } from "../install/paths.js";
|
|
9
|
+
function printStatus(label, ok, detail) {
|
|
10
|
+
const status = ok ? "OK" : "MISSING";
|
|
11
|
+
logStep(`${status.padEnd(7)} ${label} - ${detail}`);
|
|
12
|
+
}
|
|
13
|
+
export function runDoctor(repoPath) {
|
|
14
|
+
logStep("Relentless DECS doctor");
|
|
15
|
+
logStep("");
|
|
16
|
+
const configPaths = getConfigPaths();
|
|
17
|
+
printStatus("Credentials file (~/.relentless/decs-config.json)", fs.existsSync(configPaths.defaultPath), configPaths.defaultPath);
|
|
18
|
+
printStatus("Legacy credentials file (~/.claude/decs-config.json)", fs.existsSync(configPaths.legacyPath), configPaths.legacyPath);
|
|
19
|
+
try {
|
|
20
|
+
const credentials = resolveCredentials();
|
|
21
|
+
printStatus("Resolved credentials", true, `URL=${credentials.baseUrl}, Buildspace=${credentials.buildspaceId}`);
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
printStatus("Resolved credentials", false, error instanceof Error ? error.message : String(error));
|
|
25
|
+
}
|
|
26
|
+
const codexConfigPath = getCodexUserConfigPath();
|
|
27
|
+
if (fs.existsSync(codexConfigPath)) {
|
|
28
|
+
try {
|
|
29
|
+
const parsed = TOML.parse(fs.readFileSync(codexConfigPath, "utf8"));
|
|
30
|
+
const mcpServers = (parsed.mcp_servers ?? {});
|
|
31
|
+
const hasServer = Object.hasOwn(mcpServers, "relentless_decs");
|
|
32
|
+
printStatus("Codex MCP config", hasServer, codexConfigPath);
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
printStatus("Codex MCP config", false, `Invalid TOML: ${codexConfigPath}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
printStatus("Codex MCP config", false, codexConfigPath);
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
const claudePath = resolveClaudeDesktopConfigPath();
|
|
43
|
+
printStatus("Claude Desktop MCP config", fs.existsSync(claudePath), claudePath);
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
printStatus("Claude Desktop MCP config (macOS)", false, "~/Library/Application Support/Claude/claude_desktop_config.json");
|
|
47
|
+
printStatus("Claude Desktop MCP config (Windows)", false, "%APPDATA%\\Claude\\claude_desktop_config.json");
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
const repo = readRepoDecsConfig(repoPath);
|
|
51
|
+
printStatus("Repo .decs.json", true, `${repo.path} (space=${repo.config.relentlessSpaceId})`);
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
printStatus("Repo .decs.json", false, error instanceof Error ? error.message : String(error));
|
|
55
|
+
}
|
|
56
|
+
const promptsPath = path.join(os.homedir(), ".codex", "prompts");
|
|
57
|
+
printStatus("Codex prompts directory", fs.existsSync(promptsPath), promptsPath);
|
|
58
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { resolveCredentials } from "../config.js";
|
|
3
|
+
import { logStep } from "../cli/output.js";
|
|
4
|
+
import { writeRepoDecsConfig } from "../repo-config.js";
|
|
5
|
+
import { RelentlessApiClient } from "../relentless-api.js";
|
|
6
|
+
export async function runInit(options) {
|
|
7
|
+
const repoPath = path.resolve(options.repoPath);
|
|
8
|
+
const projectName = options.projectName ?? path.basename(repoPath);
|
|
9
|
+
const client = new RelentlessApiClient(resolveCredentials());
|
|
10
|
+
await client.getNode(options.projectNodeId);
|
|
11
|
+
const space = await client.createNode({
|
|
12
|
+
kind: "collection",
|
|
13
|
+
title: `${projectName} - Decisions`,
|
|
14
|
+
parentId: options.projectNodeId
|
|
15
|
+
});
|
|
16
|
+
const writeResult = writeRepoDecsConfig(space.id, repoPath);
|
|
17
|
+
logStep(`Created decisions space: ${space.title} (${space.id})`);
|
|
18
|
+
logStep(`Wrote ${writeResult.path}`);
|
|
19
|
+
if (options.createBootstrapDecision) {
|
|
20
|
+
await client.createNode({
|
|
21
|
+
kind: "decision",
|
|
22
|
+
title: "Use DECS for architectural decision tracking",
|
|
23
|
+
parentId: space.id,
|
|
24
|
+
content: {
|
|
25
|
+
what: "We adopted Relentless DECS for architectural decision tracking.",
|
|
26
|
+
why: "AI sessions are stateless and need explicit architectural memory.",
|
|
27
|
+
purpose: "Maintain architecture consistency and preserve rationale over time.",
|
|
28
|
+
constraints: "Significant architecture decisions must be captured as DECS entries.",
|
|
29
|
+
isKeyDecision: true
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
logStep("Created bootstrap key decision.");
|
|
33
|
+
}
|
|
34
|
+
logStep("Initialization complete.");
|
|
35
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { ensureParentDirectory, maybeBackupFile, readTextFileIfExists, writeTextFile } from "../install/fs-utils.js";
|
|
4
|
+
import { resolveClaudeDesktopConfigPath } from "../install/paths.js";
|
|
5
|
+
import { upsertClaudeDesktopServer } from "../install/json-merge.js";
|
|
6
|
+
import { logStep } from "../cli/output.js";
|
|
7
|
+
import { resolveCredentialsForSetup } from "./shared.js";
|
|
8
|
+
const CLAUDE_SERVER_NAME = "relentless-decs";
|
|
9
|
+
export async function runSetupClaudeDesktop(options) {
|
|
10
|
+
logStep("Configuring Relentless DECS for Claude Desktop...");
|
|
11
|
+
const { credentials, configPath, wroteConfig } = await resolveCredentialsForSetup({
|
|
12
|
+
yes: options.yes,
|
|
13
|
+
dryRun: options.dryRun
|
|
14
|
+
});
|
|
15
|
+
if (wroteConfig && configPath) {
|
|
16
|
+
logStep(`Saved credentials to ${configPath}`);
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
logStep("Using credentials from environment or existing config.");
|
|
20
|
+
}
|
|
21
|
+
let command = "npx";
|
|
22
|
+
let args = ["-y", "@relentless/decs-mcp", "serve"];
|
|
23
|
+
if (options.useLocalServer) {
|
|
24
|
+
const localServerPath = path.resolve(process.cwd(), "dist", "index.js");
|
|
25
|
+
if (!fs.existsSync(localServerPath)) {
|
|
26
|
+
throw new Error(`Local server mode requested but ${localServerPath} does not exist. Run npm run build first.`);
|
|
27
|
+
}
|
|
28
|
+
command = "node";
|
|
29
|
+
args = [localServerPath, "serve"];
|
|
30
|
+
}
|
|
31
|
+
const claudeConfigPath = resolveClaudeDesktopConfigPath(options.platform);
|
|
32
|
+
const current = readTextFileIfExists(claudeConfigPath);
|
|
33
|
+
const { nextJson, changed } = upsertClaudeDesktopServer({
|
|
34
|
+
currentJson: current,
|
|
35
|
+
serverName: CLAUDE_SERVER_NAME,
|
|
36
|
+
serverConfig: {
|
|
37
|
+
command,
|
|
38
|
+
args,
|
|
39
|
+
env: {
|
|
40
|
+
RELENTLESS_API_KEY: credentials.relentlessApiKey,
|
|
41
|
+
RELENTLESS_URL: credentials.relentlessUrl,
|
|
42
|
+
RELENTLESS_BUILDSPACE_ID: credentials.buildspaceId
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
if (changed) {
|
|
47
|
+
if (options.dryRun) {
|
|
48
|
+
logStep(`[dry-run] Would update ${claudeConfigPath}`);
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
ensureParentDirectory(claudeConfigPath);
|
|
52
|
+
maybeBackupFile(claudeConfigPath);
|
|
53
|
+
writeTextFile(claudeConfigPath, nextJson);
|
|
54
|
+
logStep(`Updated ${claudeConfigPath}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
logStep(`${claudeConfigPath} already has the DECS MCP server config.`);
|
|
59
|
+
}
|
|
60
|
+
logStep("");
|
|
61
|
+
logStep("Next steps:");
|
|
62
|
+
logStep("1) Fully restart Claude Desktop.");
|
|
63
|
+
logStep("2) Open Claude Desktop and verify relentless-decs appears in MCP tools.");
|
|
64
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import { ensureParentDirectory, maybeBackupFile, readTextFileIfExists, writeTextFile } from "../install/fs-utils.js";
|
|
4
|
+
import { getCodexPromptDirPath, getCodexUserConfigPath } from "../install/paths.js";
|
|
5
|
+
import { upsertCodexMcpServer } from "../install/toml-merge.js";
|
|
6
|
+
import { installPrompts, mergeAgentsBlock } from "../install/codex-assets.js";
|
|
7
|
+
import { logStep, logWarn } from "../cli/output.js";
|
|
8
|
+
import { resolveCredentialsForSetup } from "./shared.js";
|
|
9
|
+
const CODEX_SERVER_NAME = "relentless_decs";
|
|
10
|
+
export async function runSetupCodex(options) {
|
|
11
|
+
const repoPath = path.resolve(options.repoPath);
|
|
12
|
+
logStep("Configuring Relentless DECS for Codex...");
|
|
13
|
+
const { credentials, configPath, wroteConfig } = await resolveCredentialsForSetup({
|
|
14
|
+
yes: options.yes,
|
|
15
|
+
dryRun: options.dryRun
|
|
16
|
+
});
|
|
17
|
+
if (wroteConfig && configPath) {
|
|
18
|
+
logStep(`Saved credentials to ${configPath}`);
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
logStep("Using credentials from environment or existing config.");
|
|
22
|
+
}
|
|
23
|
+
let command = "npx";
|
|
24
|
+
let args = ["-y", "@relentless/decs-mcp", "serve"];
|
|
25
|
+
if (options.useLocalServer) {
|
|
26
|
+
const localServerPath = path.resolve(process.cwd(), "dist", "index.js");
|
|
27
|
+
if (!fs.existsSync(localServerPath)) {
|
|
28
|
+
throw new Error(`Local server mode requested but ${localServerPath} does not exist. Run npm run build first.`);
|
|
29
|
+
}
|
|
30
|
+
command = "node";
|
|
31
|
+
args = [localServerPath, "serve"];
|
|
32
|
+
}
|
|
33
|
+
const codexConfigPath = getCodexUserConfigPath();
|
|
34
|
+
const currentToml = readTextFileIfExists(codexConfigPath);
|
|
35
|
+
const { nextToml, changed } = upsertCodexMcpServer({
|
|
36
|
+
currentToml,
|
|
37
|
+
serverName: CODEX_SERVER_NAME,
|
|
38
|
+
serverConfig: {
|
|
39
|
+
command,
|
|
40
|
+
args,
|
|
41
|
+
env: {
|
|
42
|
+
RELENTLESS_API_KEY: credentials.relentlessApiKey,
|
|
43
|
+
RELENTLESS_URL: credentials.relentlessUrl,
|
|
44
|
+
RELENTLESS_BUILDSPACE_ID: credentials.buildspaceId
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
if (changed) {
|
|
49
|
+
if (options.dryRun) {
|
|
50
|
+
logStep(`[dry-run] Would update ${codexConfigPath}`);
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
ensureParentDirectory(codexConfigPath);
|
|
54
|
+
maybeBackupFile(codexConfigPath);
|
|
55
|
+
writeTextFile(codexConfigPath, nextToml);
|
|
56
|
+
logStep(`Updated ${codexConfigPath}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
logStep(`${codexConfigPath} already has the DECS MCP server config.`);
|
|
61
|
+
}
|
|
62
|
+
if (!options.noPrompts) {
|
|
63
|
+
const promptDir = getCodexPromptDirPath();
|
|
64
|
+
if (options.dryRun) {
|
|
65
|
+
logStep(`[dry-run] Would install DECS prompts into ${promptDir}`);
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
const installed = installPrompts(promptDir);
|
|
69
|
+
logStep(`Installed ${installed.files.length} prompt files in ${promptDir}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
logWarn("Skipped prompt installation (--no-prompts).");
|
|
74
|
+
}
|
|
75
|
+
if (options.dryRun) {
|
|
76
|
+
logStep(`[dry-run] Would merge DECS guidance into ${path.join(repoPath, "AGENTS.md")}`);
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
const agents = mergeAgentsBlock(repoPath);
|
|
80
|
+
if (agents.changed) {
|
|
81
|
+
logStep(`Updated ${agents.path}`);
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
logStep(`${agents.path} already contains current DECS guidance.`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
logStep("");
|
|
88
|
+
logStep("Next steps:");
|
|
89
|
+
logStep("1) Restart Codex.");
|
|
90
|
+
logStep("2) In your repo, run: npx @relentless/decs-mcp init <project-node-id> [project-name] --repo .");
|
|
91
|
+
logStep("3) Optional in-chat command: /prompts:decs-init <project-node-id> [project-name]");
|
|
92
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { readUserConfigFromDisk, writeUserConfigToDisk } from "../config.js";
|
|
2
|
+
import { promptText } from "../cli/prompt.js";
|
|
3
|
+
function normalizeUrl(url) {
|
|
4
|
+
return url.replace(/\/+$/, "");
|
|
5
|
+
}
|
|
6
|
+
export async function resolveCredentialsForSetup(options) {
|
|
7
|
+
const existing = readUserConfigFromDisk();
|
|
8
|
+
let apiKey = process.env.RELENTLESS_API_KEY ?? existing.relentlessApiKey ?? "";
|
|
9
|
+
let relentlessUrl = process.env.RELENTLESS_URL ?? existing.relentlessUrl ?? "https://relentless.build";
|
|
10
|
+
let buildspaceId = process.env.RELENTLESS_BUILDSPACE_ID ?? existing.buildspaceId ?? "";
|
|
11
|
+
if (!options.yes) {
|
|
12
|
+
if (!apiKey) {
|
|
13
|
+
apiKey = await promptText("Relentless API key", { secret: true });
|
|
14
|
+
}
|
|
15
|
+
relentlessUrl = await promptText("Relentless URL", {
|
|
16
|
+
defaultValue: relentlessUrl
|
|
17
|
+
});
|
|
18
|
+
if (!buildspaceId) {
|
|
19
|
+
buildspaceId = await promptText("Relentless buildspace id");
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
if (!apiKey || !relentlessUrl || !buildspaceId) {
|
|
23
|
+
throw new Error("Missing credentials. Provide RELENTLESS_API_KEY, RELENTLESS_URL, RELENTLESS_BUILDSPACE_ID or run setup without --yes for prompts.");
|
|
24
|
+
}
|
|
25
|
+
const credentials = {
|
|
26
|
+
relentlessApiKey: apiKey,
|
|
27
|
+
relentlessUrl: normalizeUrl(relentlessUrl),
|
|
28
|
+
buildspaceId
|
|
29
|
+
};
|
|
30
|
+
if (options.dryRun) {
|
|
31
|
+
return { credentials, configPath: null, wroteConfig: false };
|
|
32
|
+
}
|
|
33
|
+
const wroteConfigPath = writeUserConfigToDisk(credentials);
|
|
34
|
+
return { credentials, configPath: wroteConfigPath, wroteConfig: true };
|
|
35
|
+
}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { DecsError } from "./errors.js";
|
|
6
|
+
const CONFIG_SCHEMA = z.object({
|
|
7
|
+
relentlessApiKey: z.string().trim().min(1).optional(),
|
|
8
|
+
relentlessUrl: z.string().trim().min(1).optional(),
|
|
9
|
+
buildspaceId: z.string().trim().min(1).optional()
|
|
10
|
+
});
|
|
11
|
+
const REQUIRED_SCHEMA = z.object({
|
|
12
|
+
relentlessApiKey: z.string().trim().min(1),
|
|
13
|
+
relentlessUrl: z.string().trim().min(1),
|
|
14
|
+
buildspaceId: z.string().trim().min(1)
|
|
15
|
+
});
|
|
16
|
+
function resolvePathConfig() {
|
|
17
|
+
return {
|
|
18
|
+
defaultPath: path.join(os.homedir(), ".relentless", "decs-config.json"),
|
|
19
|
+
legacyPath: path.join(os.homedir(), ".claude", "decs-config.json")
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
function readConfigFile(filePath) {
|
|
23
|
+
if (!fs.existsSync(filePath)) {
|
|
24
|
+
return {};
|
|
25
|
+
}
|
|
26
|
+
let parsed;
|
|
27
|
+
try {
|
|
28
|
+
parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
throw new DecsError("CONFIG_ERROR", `Invalid JSON in config file: ${filePath}`, error);
|
|
32
|
+
}
|
|
33
|
+
const result = CONFIG_SCHEMA.safeParse(parsed);
|
|
34
|
+
if (!result.success) {
|
|
35
|
+
throw new DecsError("CONFIG_ERROR", `Invalid config shape in file: ${filePath}`, result.error.flatten());
|
|
36
|
+
}
|
|
37
|
+
return result.data;
|
|
38
|
+
}
|
|
39
|
+
function ensureConfigDirectory(filePath) {
|
|
40
|
+
const directory = path.dirname(filePath);
|
|
41
|
+
fs.mkdirSync(directory, { recursive: true });
|
|
42
|
+
}
|
|
43
|
+
function trimTrailingSlash(url) {
|
|
44
|
+
return url.replace(/\/+$/, "");
|
|
45
|
+
}
|
|
46
|
+
export function resolveCredentials() {
|
|
47
|
+
const { defaultPath, legacyPath } = resolvePathConfig();
|
|
48
|
+
const legacyConfig = readConfigFile(legacyPath);
|
|
49
|
+
const defaultConfig = readConfigFile(defaultPath);
|
|
50
|
+
const fileConfig = {
|
|
51
|
+
...legacyConfig,
|
|
52
|
+
...defaultConfig
|
|
53
|
+
};
|
|
54
|
+
const mergedConfig = {
|
|
55
|
+
relentlessApiKey: process.env.RELENTLESS_API_KEY ?? fileConfig.relentlessApiKey,
|
|
56
|
+
relentlessUrl: process.env.RELENTLESS_URL ?? fileConfig.relentlessUrl,
|
|
57
|
+
buildspaceId: process.env.RELENTLESS_BUILDSPACE_ID ?? fileConfig.buildspaceId
|
|
58
|
+
};
|
|
59
|
+
const parsed = REQUIRED_SCHEMA.safeParse(mergedConfig);
|
|
60
|
+
if (!parsed.success) {
|
|
61
|
+
throw new DecsError("CONFIG_ERROR", "Missing Relentless credentials. Set RELENTLESS_API_KEY, RELENTLESS_URL, RELENTLESS_BUILDSPACE_ID or configure ~/.relentless/decs-config.json", parsed.error.flatten());
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
apiKey: parsed.data.relentlessApiKey,
|
|
65
|
+
baseUrl: trimTrailingSlash(parsed.data.relentlessUrl),
|
|
66
|
+
buildspaceId: parsed.data.buildspaceId
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
export function getConfigPaths() {
|
|
70
|
+
return resolvePathConfig();
|
|
71
|
+
}
|
|
72
|
+
export function readUserConfigFromDisk() {
|
|
73
|
+
const { defaultPath, legacyPath } = resolvePathConfig();
|
|
74
|
+
const legacyConfig = readConfigFile(legacyPath);
|
|
75
|
+
const defaultConfig = readConfigFile(defaultPath);
|
|
76
|
+
return {
|
|
77
|
+
...legacyConfig,
|
|
78
|
+
...defaultConfig
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
export function writeUserConfigToDisk(config) {
|
|
82
|
+
const { defaultPath } = resolvePathConfig();
|
|
83
|
+
ensureConfigDirectory(defaultPath);
|
|
84
|
+
fs.writeFileSync(defaultPath, `${JSON.stringify({
|
|
85
|
+
relentlessApiKey: config.relentlessApiKey,
|
|
86
|
+
relentlessUrl: trimTrailingSlash(config.relentlessUrl),
|
|
87
|
+
buildspaceId: config.buildspaceId
|
|
88
|
+
}, null, 2)}\n`, {
|
|
89
|
+
encoding: "utf8",
|
|
90
|
+
mode: 0o600
|
|
91
|
+
});
|
|
92
|
+
return defaultPath;
|
|
93
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
function asString(value) {
|
|
2
|
+
if (typeof value === "string") {
|
|
3
|
+
return value;
|
|
4
|
+
}
|
|
5
|
+
return "—";
|
|
6
|
+
}
|
|
7
|
+
function asBoolean(value) {
|
|
8
|
+
return value === true;
|
|
9
|
+
}
|
|
10
|
+
export function toDecisionSummary(node) {
|
|
11
|
+
const content = (node.content ?? {});
|
|
12
|
+
return {
|
|
13
|
+
id: node.id,
|
|
14
|
+
title: node.title,
|
|
15
|
+
updatedAt: node.updatedAt,
|
|
16
|
+
what: asString(content.what),
|
|
17
|
+
why: asString(content.why),
|
|
18
|
+
purpose: asString(content.purpose),
|
|
19
|
+
constraints: asString(content.constraints),
|
|
20
|
+
isKeyDecision: asBoolean(content.isKeyDecision)
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function sortNewestFirst(items) {
|
|
24
|
+
return [...items].sort((a, b) => {
|
|
25
|
+
const aTime = a.updatedAt ? Date.parse(a.updatedAt) : 0;
|
|
26
|
+
const bTime = b.updatedAt ? Date.parse(b.updatedAt) : 0;
|
|
27
|
+
return bTime - aTime;
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
export function partitionDecisions(decisions, recentLimit = 10) {
|
|
31
|
+
const sorted = sortNewestFirst(decisions);
|
|
32
|
+
const keyDecisions = sorted.filter((decision) => decision.isKeyDecision);
|
|
33
|
+
const recentDecisions = sorted
|
|
34
|
+
.filter((decision) => !decision.isKeyDecision)
|
|
35
|
+
.slice(0, recentLimit);
|
|
36
|
+
return { keyDecisions, recentDecisions };
|
|
37
|
+
}
|
|
38
|
+
function renderDecision(decision) {
|
|
39
|
+
const updated = decision.updatedAt
|
|
40
|
+
? `**Updated:** ${decision.updatedAt.split("T")[0]}`
|
|
41
|
+
: "**Updated:** —";
|
|
42
|
+
return [
|
|
43
|
+
`## ${decision.title}`,
|
|
44
|
+
updated,
|
|
45
|
+
"",
|
|
46
|
+
`**What:** ${decision.what}`,
|
|
47
|
+
`**Why:** ${decision.why}`,
|
|
48
|
+
`**Purpose:** ${decision.purpose}`,
|
|
49
|
+
`**Constraints:** ${decision.constraints}`,
|
|
50
|
+
"",
|
|
51
|
+
"---",
|
|
52
|
+
""
|
|
53
|
+
].join("\n");
|
|
54
|
+
}
|
|
55
|
+
export function renderDecisionContextMarkdown(input) {
|
|
56
|
+
const sections = ["=== DECS: Prior Architectural Decisions ===", ""];
|
|
57
|
+
if (input.keyDecisions.length > 0) {
|
|
58
|
+
sections.push("### Key Decisions (always active)", "");
|
|
59
|
+
for (const decision of input.keyDecisions) {
|
|
60
|
+
sections.push(renderDecision(decision));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (input.recentDecisions.length > 0) {
|
|
64
|
+
sections.push("### Recent Decisions", "");
|
|
65
|
+
for (const decision of input.recentDecisions) {
|
|
66
|
+
sections.push(renderDecision(decision));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
sections.push("When making architectural decisions, consider how they relate to the above.", "Key decisions marked above are foundational and should only be changed deliberately.");
|
|
70
|
+
return sections.join("\n");
|
|
71
|
+
}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export class DecsError extends Error {
|
|
2
|
+
code;
|
|
3
|
+
details;
|
|
4
|
+
constructor(code, message, details) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = "DecsError";
|
|
7
|
+
this.code = code;
|
|
8
|
+
this.details = details;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export function toDecsError(error) {
|
|
12
|
+
if (error instanceof DecsError) {
|
|
13
|
+
return error;
|
|
14
|
+
}
|
|
15
|
+
if (error instanceof Error) {
|
|
16
|
+
return new DecsError("UPSTREAM_ERROR", error.message);
|
|
17
|
+
}
|
|
18
|
+
return new DecsError("UPSTREAM_ERROR", "Unknown error");
|
|
19
|
+
}
|