@getreka/cli 0.2.0 → 0.3.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/dist/commands/init.d.ts +1 -0
- package/dist/commands/init.js +183 -53
- package/dist/index.js +13 -16
- package/dist/mcp-config.d.ts +49 -0
- package/dist/mcp-config.js +78 -0
- package/dist/mint.d.ts +64 -0
- package/dist/mint.js +200 -0
- package/dist/setup-files.d.ts +23 -0
- package/dist/setup-files.js +134 -0
- package/package.json +7 -3
- package/dist/commands/search.d.ts +0 -6
- package/dist/commands/search.js +0 -48
package/dist/commands/init.d.ts
CHANGED
package/dist/commands/init.js
CHANGED
|
@@ -39,44 +39,128 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
39
39
|
exports.initCommand = initCommand;
|
|
40
40
|
const fs = __importStar(require("fs"));
|
|
41
41
|
const path = __importStar(require("path"));
|
|
42
|
+
const readline = __importStar(require("readline"));
|
|
43
|
+
const child_process_1 = require("child_process");
|
|
44
|
+
const util_1 = require("util");
|
|
42
45
|
const chalk_1 = __importDefault(require("chalk"));
|
|
43
46
|
const api_1 = require("../api");
|
|
44
47
|
const config_1 = require("../config");
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
48
|
+
const mcp_config_1 = require("../mcp-config");
|
|
49
|
+
const mint_1 = require("../mint");
|
|
50
|
+
const setup_files_1 = require("../setup-files");
|
|
51
|
+
/*
|
|
52
|
+
* KEY MINTING MECHANISM (LB-3)
|
|
53
|
+
* ----------------------------
|
|
54
|
+
* Self-hosted rag-api uses deny-by-default API-key auth; keys live in
|
|
55
|
+
* data/keys.json inside the container and are loaded ONCE at startup
|
|
56
|
+
* (rag-api/src/middleware/auth.ts — no file watcher). POST /api/keys is
|
|
57
|
+
* admin-gated: it sits BEHIND authMiddleware (needs any valid key) and
|
|
58
|
+
* requireAdmin (loopback socket or X-Admin-Key). From the host, a Docker
|
|
59
|
+
* published port is never a loopback socket for the container, so the CLI
|
|
60
|
+
* cannot mint over plain HTTP.
|
|
61
|
+
*
|
|
62
|
+
* Resolution order when running `reka init`:
|
|
63
|
+
* 1. --key flag — used as-is.
|
|
64
|
+
* 2. Existing REKA_API_KEY in .mcp.json (verified via GET /api/whoami) —
|
|
65
|
+
* keeps re-runs idempotent: no duplicate keys, no restarts.
|
|
66
|
+
* Skipped with --force.
|
|
67
|
+
* 3. Docker mint: discover the rag-api container (default "reka-api",
|
|
68
|
+
* override via --container or REKA_CONTAINER) and run
|
|
69
|
+
* docker exec <container> node -e "<script>"
|
|
70
|
+
* The script first POSTs http://127.0.0.1:<API_PORT>/api/keys from
|
|
71
|
+
* INSIDE the container — that's a loopback socket, so requireAdmin
|
|
72
|
+
* passes, and when ALLOW_ANONYMOUS=true (dev) authMiddleware passes
|
|
73
|
+
* too; the key is registered in the live process immediately.
|
|
74
|
+
* If that returns 401/403 (prod: keys configured, anonymous off), it
|
|
75
|
+
* falls back to the same module the server uses:
|
|
76
|
+
* const {generateKey}=require('./dist/middleware/auth');
|
|
77
|
+
* generateKey('<project>','cli-init')
|
|
78
|
+
* generateKey PERSISTS the hash to data/keys.json (saveKeys) and
|
|
79
|
+
* returns the plaintext once — but only the exec'd process has it in
|
|
80
|
+
* memory, so the CLI then runs `docker restart <container>`, waits
|
|
81
|
+
* for /api/health, and verifies the key with an authed /api/whoami.
|
|
82
|
+
* 4. Interactive prompt, printing the docker-exec admin one-liner so an
|
|
83
|
+
* operator can mint out-of-band.
|
|
84
|
+
*/
|
|
85
|
+
const execFile = (0, util_1.promisify)(child_process_1.execFile);
|
|
86
|
+
function defaultMintDeps() {
|
|
87
|
+
return {
|
|
88
|
+
execFile: async (cmd, args) => {
|
|
89
|
+
const { stdout } = await execFile(cmd, args, { timeout: 60000 });
|
|
90
|
+
return { stdout };
|
|
91
|
+
},
|
|
92
|
+
httpGet: async (url, headers) => {
|
|
93
|
+
const axios = (await Promise.resolve().then(() => __importStar(require("axios")))).default;
|
|
94
|
+
const res = await axios.get(url, {
|
|
95
|
+
headers,
|
|
96
|
+
timeout: 10000,
|
|
97
|
+
validateStatus: () => true,
|
|
98
|
+
});
|
|
99
|
+
return { status: res.status };
|
|
100
|
+
},
|
|
101
|
+
prompt: (question) => {
|
|
102
|
+
if (!process.stdin.isTTY) {
|
|
103
|
+
return Promise.reject(new Error("No API key available and stdin is not a TTY."));
|
|
104
|
+
}
|
|
105
|
+
const rl = readline.createInterface({
|
|
106
|
+
input: process.stdin,
|
|
107
|
+
output: process.stdout,
|
|
108
|
+
});
|
|
109
|
+
return new Promise((resolve) => rl.question(question, (answer) => {
|
|
110
|
+
rl.close();
|
|
111
|
+
resolve(answer);
|
|
112
|
+
}));
|
|
113
|
+
},
|
|
114
|
+
sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
|
|
115
|
+
log: (msg) => console.log(chalk_1.default.yellow(` ${msg}`)),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
56
118
|
async function initCommand(opts) {
|
|
57
119
|
const projectPath = opts.path || process.cwd();
|
|
58
120
|
const projectName = opts.project || path.basename(projectPath);
|
|
59
|
-
// Demo mode:
|
|
121
|
+
// Demo mode: device authorization flow
|
|
60
122
|
if (opts.demo) {
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
console.log(chalk_1.default.bold(" Connecting to Reka Demo
|
|
123
|
+
const demoApiUrl = "https://rag.akeryuu.com";
|
|
124
|
+
const axios = (await Promise.resolve().then(() => __importStar(require("axios")))).default;
|
|
125
|
+
console.log(chalk_1.default.bold("\n Connecting to Reka Demo...\n"));
|
|
64
126
|
try {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
})
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
127
|
+
// Step 1: Create device session
|
|
128
|
+
const { data: device } = await axios.post(`${demoApiUrl}/api/auth/device`);
|
|
129
|
+
// Step 2: Open browser
|
|
130
|
+
console.log(` Your verification code: ${chalk_1.default.bold.cyan(device.userCode)}`);
|
|
131
|
+
console.log(` Opening browser to sign in...\n`);
|
|
132
|
+
try {
|
|
133
|
+
const open = (await Promise.resolve().then(() => __importStar(require("open")))).default;
|
|
134
|
+
await open(device.verificationUrl);
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
console.log(` Open this URL in your browser:`);
|
|
138
|
+
console.log(` ${chalk_1.default.cyan(device.verificationUrl)}\n`);
|
|
139
|
+
}
|
|
140
|
+
// Step 3: Poll for completion
|
|
141
|
+
const ora = (await Promise.resolve().then(() => __importStar(require("ora")))).default;
|
|
142
|
+
const spinner = ora(" Waiting for authentication...").start();
|
|
143
|
+
const deadline = Date.now() + device.expiresIn * 1000;
|
|
144
|
+
while (Date.now() < deadline) {
|
|
145
|
+
await new Promise((r) => setTimeout(r, device.interval * 1000));
|
|
146
|
+
const { data: poll } = await axios.get(`${demoApiUrl}/api/auth/poll?device_code=${device.deviceCode}`);
|
|
147
|
+
if (poll.status === "completed") {
|
|
148
|
+
spinner.succeed("Authenticated!");
|
|
149
|
+
writeMcpConfig(projectPath, poll.apiKey, opts.force, poll.apiUrl);
|
|
150
|
+
console.log(chalk_1.default.green(` ✓ Project: ${poll.projectName}`));
|
|
151
|
+
console.log(chalk_1.default.green(` ✓ .mcp.json written`));
|
|
152
|
+
console.log("");
|
|
153
|
+
console.log(" Your AI assistant now has memory. Open it and start asking!");
|
|
154
|
+
console.log(chalk_1.default.yellow(" Note: demo data may be reset periodically."));
|
|
155
|
+
console.log("");
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
if (poll.status === "expired") {
|
|
159
|
+
spinner.fail("Authentication expired. Run the command again.");
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
spinner.fail("Authentication timed out. Run the command again.");
|
|
80
164
|
}
|
|
81
165
|
catch (err) {
|
|
82
166
|
const msg = err.response?.data?.error || err.message;
|
|
@@ -85,45 +169,91 @@ async function initCommand(opts) {
|
|
|
85
169
|
}
|
|
86
170
|
return;
|
|
87
171
|
}
|
|
88
|
-
// Cloud mode:
|
|
172
|
+
// Cloud mode: no hosted offering — be honest about what exists
|
|
89
173
|
if (opts.cloud) {
|
|
90
174
|
console.log("");
|
|
91
|
-
console.log(chalk_1.default.bold(" Reka Cloud —
|
|
175
|
+
console.log(chalk_1.default.bold(" Reka Cloud — there is no hosted Reka today"));
|
|
176
|
+
console.log("");
|
|
177
|
+
console.log(" Reka is self-hosted: your code and memory stay on your machines.");
|
|
178
|
+
console.log(" A self-hosted Team license (multi-user, support) is planned,");
|
|
179
|
+
console.log(" gated on real adoption of the open release.");
|
|
92
180
|
console.log("");
|
|
93
|
-
console.log(
|
|
94
|
-
console.log(` Join the waitlist: ${chalk_1.default.cyan("https://getreka.dev")}`);
|
|
181
|
+
console.log(` Interested in the Team tier? Tell us: ${chalk_1.default.cyan("https://getreka.dev")}`);
|
|
95
182
|
console.log("");
|
|
96
|
-
console.log("
|
|
183
|
+
console.log(" Get started self-hosted today:");
|
|
97
184
|
console.log(` ${chalk_1.default.bold("docker-compose up -d")}`);
|
|
98
185
|
console.log(` ${chalk_1.default.bold(`npx @getreka/cli init --project ${projectName}`)}`);
|
|
99
186
|
console.log("");
|
|
100
187
|
return;
|
|
101
188
|
}
|
|
102
|
-
// Self-hosted
|
|
189
|
+
// Self-hosted
|
|
190
|
+
const apiUrl = opts.apiUrl || process.env.REKA_API_URL || "http://localhost:3100";
|
|
191
|
+
const container = opts.container || process.env.REKA_CONTAINER || "reka-api";
|
|
192
|
+
console.log(`\n Initializing Reka for project ${chalk_1.default.bold(projectName)}...\n`);
|
|
193
|
+
// 1. Resolve API key: --key → existing .mcp.json key → docker mint → prompt
|
|
194
|
+
const existingKey = (0, mcp_config_1.extractExistingKey)(opts.force ? {} : (0, setup_files_1.readMcpConfig)(projectPath), projectName);
|
|
195
|
+
let mintResult;
|
|
196
|
+
try {
|
|
197
|
+
mintResult = await (0, mint_1.resolveApiKey)({
|
|
198
|
+
projectName,
|
|
199
|
+
apiUrl,
|
|
200
|
+
container,
|
|
201
|
+
key: opts.key,
|
|
202
|
+
existingKey,
|
|
203
|
+
force: opts.force,
|
|
204
|
+
}, defaultMintDeps());
|
|
205
|
+
}
|
|
206
|
+
catch (err) {
|
|
207
|
+
console.log(chalk_1.default.red(`\n Failed to obtain an API key: ${err.message}`));
|
|
208
|
+
console.log(chalk_1.default.yellow(` Is the Reka API running? Start with: docker-compose up -d\n`));
|
|
209
|
+
process.exitCode = 1;
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
const sourceLabel = {
|
|
213
|
+
flag: "from --key",
|
|
214
|
+
existing: "reused from .mcp.json",
|
|
215
|
+
minted: `minted via container "${container}"`,
|
|
216
|
+
prompt: "entered manually",
|
|
217
|
+
}[mintResult.source];
|
|
218
|
+
console.log(chalk_1.default.green(` ✓ API key ${sourceLabel}: ${mintResult.key.slice(0, 20)}...`));
|
|
219
|
+
if (mintResult.restarted) {
|
|
220
|
+
console.log(chalk_1.default.green(` ✓ Container "${container}" restarted to load the key`));
|
|
221
|
+
}
|
|
222
|
+
// 2. Write project files (.mcp.json + CLAUDE.md + permissions)
|
|
223
|
+
const entry = (0, setup_files_1.buildRagEntry)({
|
|
224
|
+
apiUrl,
|
|
225
|
+
projectName,
|
|
226
|
+
projectPath,
|
|
227
|
+
apiKey: mintResult.key,
|
|
228
|
+
});
|
|
229
|
+
const { changes } = (0, setup_files_1.applyProjectFiles)({
|
|
230
|
+
projectPath,
|
|
231
|
+
projectName,
|
|
232
|
+
entry,
|
|
233
|
+
force: opts.force,
|
|
234
|
+
});
|
|
235
|
+
for (const change of changes) {
|
|
236
|
+
console.log(chalk_1.default.green(` ✓ ${change}`));
|
|
237
|
+
}
|
|
238
|
+
// 3. Index status check (non-fatal)
|
|
103
239
|
const config = (0, config_1.loadConfig)({
|
|
104
|
-
api: { url:
|
|
240
|
+
api: { url: apiUrl, key: mintResult.key },
|
|
105
241
|
project: { name: projectName, path: projectPath },
|
|
106
242
|
});
|
|
107
243
|
const client = (0, api_1.createClient)(config);
|
|
108
|
-
console.log(`\n Generating API key for project ${chalk_1.default.bold(projectName)}...`);
|
|
109
244
|
try {
|
|
110
|
-
const { data } = await client.
|
|
111
|
-
|
|
112
|
-
label: `init-${Date.now()}`,
|
|
113
|
-
});
|
|
114
|
-
const apiKey = data.key;
|
|
115
|
-
writeMcpConfig(projectPath, apiKey, opts.force);
|
|
116
|
-
console.log(chalk_1.default.green(` ✓ API key created: ${apiKey.slice(0, 20)}...`));
|
|
117
|
-
console.log(chalk_1.default.green(` ✓ .mcp.json written`));
|
|
118
|
-
console.log("");
|
|
119
|
-
console.log(" Your AI assistant now has memory. Try asking it about your codebase!");
|
|
120
|
-
console.log("");
|
|
245
|
+
const { data } = await client.get(`/api/index/status/${projectName}_codebase`);
|
|
246
|
+
console.log(chalk_1.default.green(` ✓ Index status: ${data.status || "unknown"} (${data.vectorCount ?? "N/A"} vectors)`));
|
|
121
247
|
}
|
|
122
|
-
catch
|
|
123
|
-
|
|
124
|
-
console.log(chalk_1.default.red(`\n Failed to generate key: ${msg}`));
|
|
125
|
-
console.log(chalk_1.default.yellow(` Is the Reka API running? Start with: docker-compose up -d\n`));
|
|
248
|
+
catch {
|
|
249
|
+
console.log(chalk_1.default.yellow(` ! Not indexed yet — run \`reka index\` (or the index_codebase tool) to enable search.`));
|
|
126
250
|
}
|
|
251
|
+
console.log("");
|
|
252
|
+
console.log(" Next steps:");
|
|
253
|
+
console.log(" 1. Restart Claude Code to load the MCP server");
|
|
254
|
+
console.log(" 2. Index the codebase if you haven't: reka index");
|
|
255
|
+
console.log(" 3. Use context_briefing before code changes");
|
|
256
|
+
console.log("");
|
|
127
257
|
}
|
|
128
258
|
function writeMcpConfig(projectPath, apiKey, force, apiUrl) {
|
|
129
259
|
const mcpPath = path.join(projectPath, ".mcp.json");
|
package/dist/index.js
CHANGED
|
@@ -10,13 +10,12 @@ const api_1 = require("./api");
|
|
|
10
10
|
const status_1 = require("./commands/status");
|
|
11
11
|
const init_1 = require("./commands/init");
|
|
12
12
|
const index_1 = require("./commands/index");
|
|
13
|
-
const search_1 = require("./commands/search");
|
|
14
13
|
const models_1 = require("./commands/models");
|
|
15
14
|
const program = new commander_1.Command();
|
|
16
15
|
program
|
|
17
16
|
.name("reka")
|
|
18
17
|
.description("Reka — Memory your AI can trust")
|
|
19
|
-
.version("0.
|
|
18
|
+
.version("0.3.0")
|
|
20
19
|
.option("--api-url <url>", "RAG API URL")
|
|
21
20
|
.option("--api-key <key>", "API key")
|
|
22
21
|
.option("--project <name>", "Project name");
|
|
@@ -28,11 +27,20 @@ program
|
|
|
28
27
|
.option("-p, --path <path>", "Project path")
|
|
29
28
|
.option("-f, --force", "Overwrite existing .mcp.json")
|
|
30
29
|
.option("--demo", "Connect to the Reka demo instance")
|
|
31
|
-
.option("--cloud", "
|
|
32
|
-
.option("--key <key>", "API key
|
|
30
|
+
.option("--cloud", "Info on hosted Reka (none today; self-hosted Team license planned)")
|
|
31
|
+
.option("--key <key>", "Use an existing API key instead of minting one")
|
|
32
|
+
.option("--container <name>", "rag-api Docker container used to mint keys (default: reka-api, env REKA_CONTAINER)")
|
|
33
33
|
.option("--api-url <url>", "RAG API URL (default: http://localhost:3100)")
|
|
34
34
|
.action(async (opts) => {
|
|
35
|
-
|
|
35
|
+
// Global options with the same name (--project, --api-url, --api-key)
|
|
36
|
+
// are captured by the program, not the subcommand — merge them in.
|
|
37
|
+
const globals = program.opts();
|
|
38
|
+
await (0, init_1.initCommand)({
|
|
39
|
+
...opts,
|
|
40
|
+
project: opts.project ?? globals.project,
|
|
41
|
+
apiUrl: opts.apiUrl ?? globals.apiUrl,
|
|
42
|
+
key: opts.key ?? globals.apiKey,
|
|
43
|
+
});
|
|
36
44
|
});
|
|
37
45
|
// reka status
|
|
38
46
|
program
|
|
@@ -53,17 +61,6 @@ program
|
|
|
53
61
|
const client = (0, api_1.createClient)(config);
|
|
54
62
|
await (0, index_1.indexCommand)(client, config, { path: indexPath, ...opts });
|
|
55
63
|
});
|
|
56
|
-
// reka search <query>
|
|
57
|
-
program
|
|
58
|
-
.command("search <query>")
|
|
59
|
-
.description("Search indexed codebase")
|
|
60
|
-
.option("-l, --limit <n>", "Number of results", "5")
|
|
61
|
-
.option("-t, --type <type>", "Collection type (codebase, docs, memory)")
|
|
62
|
-
.action(async (query, opts) => {
|
|
63
|
-
const config = (0, config_1.loadConfig)(getOverrides());
|
|
64
|
-
const client = (0, api_1.createClient)(config);
|
|
65
|
-
await (0, search_1.searchCommand)(client, config, query, opts);
|
|
66
|
-
});
|
|
67
64
|
// reka models
|
|
68
65
|
const models = program.command("models").description("Manage model providers");
|
|
69
66
|
models
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* .mcp.json server-entry normalization.
|
|
3
|
+
*
|
|
4
|
+
* Target server name is "rag" (matches what the docs and the reka plugin
|
|
5
|
+
* expect). Historic inits wrote "reka" (old CLI) or "<project>-rag"
|
|
6
|
+
* (mcp-server setup_project), so init detects those, merges them into a
|
|
7
|
+
* single "rag" entry, and never duplicates on re-run.
|
|
8
|
+
*/
|
|
9
|
+
export interface McpServerEntry {
|
|
10
|
+
command?: string;
|
|
11
|
+
args?: string[];
|
|
12
|
+
env?: Record<string, string>;
|
|
13
|
+
[key: string]: unknown;
|
|
14
|
+
}
|
|
15
|
+
export interface McpConfig {
|
|
16
|
+
mcpServers?: Record<string, McpServerEntry>;
|
|
17
|
+
[key: string]: unknown;
|
|
18
|
+
}
|
|
19
|
+
export declare const TARGET_SERVER_NAME = "rag";
|
|
20
|
+
export declare function isRekaMcpEntry(entry: McpServerEntry | undefined): boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Names that may hold a legacy Reka entry, in merge order
|
|
23
|
+
* (oldest first — later entries win env conflicts, new entry wins last).
|
|
24
|
+
*/
|
|
25
|
+
export declare function candidateNames(projectName: string): string[];
|
|
26
|
+
/**
|
|
27
|
+
* Returns the names of existing entries that are Reka MCP servers and
|
|
28
|
+
* should be folded into the single "rag" entry.
|
|
29
|
+
*/
|
|
30
|
+
export declare function findRekaCandidates(config: McpConfig, projectName: string): string[];
|
|
31
|
+
/**
|
|
32
|
+
* Extracts a previously-configured API key from candidate entries
|
|
33
|
+
* (most authoritative first: rag → <project>-rag → reka).
|
|
34
|
+
*/
|
|
35
|
+
export declare function extractExistingKey(config: McpConfig, projectName: string): string | undefined;
|
|
36
|
+
export interface MergeResult {
|
|
37
|
+
config: McpConfig;
|
|
38
|
+
/** Legacy entry names that were removed/renamed into "rag". */
|
|
39
|
+
removed: string[];
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Pure merge: folds legacy Reka entries into a single "rag" entry.
|
|
43
|
+
* - command/args always come from `newEntry` (that's the upgrade path)
|
|
44
|
+
* - env is merged: legacy values are preserved unless `newEntry.env`
|
|
45
|
+
* overwrites them
|
|
46
|
+
* - non-Reka servers are left untouched
|
|
47
|
+
* - idempotent: applying twice yields the same config
|
|
48
|
+
*/
|
|
49
|
+
export declare function mergeRagServer(config: McpConfig, projectName: string, newEntry: McpServerEntry): MergeResult;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* .mcp.json server-entry normalization.
|
|
4
|
+
*
|
|
5
|
+
* Target server name is "rag" (matches what the docs and the reka plugin
|
|
6
|
+
* expect). Historic inits wrote "reka" (old CLI) or "<project>-rag"
|
|
7
|
+
* (mcp-server setup_project), so init detects those, merges them into a
|
|
8
|
+
* single "rag" entry, and never duplicates on re-run.
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.TARGET_SERVER_NAME = void 0;
|
|
12
|
+
exports.isRekaMcpEntry = isRekaMcpEntry;
|
|
13
|
+
exports.candidateNames = candidateNames;
|
|
14
|
+
exports.findRekaCandidates = findRekaCandidates;
|
|
15
|
+
exports.extractExistingKey = extractExistingKey;
|
|
16
|
+
exports.mergeRagServer = mergeRagServer;
|
|
17
|
+
exports.TARGET_SERVER_NAME = "rag";
|
|
18
|
+
/** Packages that identify an entry as a Reka MCP server. */
|
|
19
|
+
const REKA_MCP_PACKAGES = /@getreka\/mcp|@crowley\/rag-mcp/;
|
|
20
|
+
function isRekaMcpEntry(entry) {
|
|
21
|
+
if (!entry)
|
|
22
|
+
return false;
|
|
23
|
+
const haystack = [entry.command || "", ...(entry.args || [])].join(" ");
|
|
24
|
+
return REKA_MCP_PACKAGES.test(haystack);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Names that may hold a legacy Reka entry, in merge order
|
|
28
|
+
* (oldest first — later entries win env conflicts, new entry wins last).
|
|
29
|
+
*/
|
|
30
|
+
function candidateNames(projectName) {
|
|
31
|
+
return ["reka", `${projectName}-rag`, exports.TARGET_SERVER_NAME];
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Returns the names of existing entries that are Reka MCP servers and
|
|
35
|
+
* should be folded into the single "rag" entry.
|
|
36
|
+
*/
|
|
37
|
+
function findRekaCandidates(config, projectName) {
|
|
38
|
+
const servers = config.mcpServers || {};
|
|
39
|
+
return candidateNames(projectName).filter((name) => isRekaMcpEntry(servers[name]));
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Extracts a previously-configured API key from candidate entries
|
|
43
|
+
* (most authoritative first: rag → <project>-rag → reka).
|
|
44
|
+
*/
|
|
45
|
+
function extractExistingKey(config, projectName) {
|
|
46
|
+
const servers = config.mcpServers || {};
|
|
47
|
+
for (const name of [...candidateNames(projectName)].reverse()) {
|
|
48
|
+
const entry = servers[name];
|
|
49
|
+
if (isRekaMcpEntry(entry) && entry?.env?.REKA_API_KEY) {
|
|
50
|
+
return entry.env.REKA_API_KEY;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Pure merge: folds legacy Reka entries into a single "rag" entry.
|
|
57
|
+
* - command/args always come from `newEntry` (that's the upgrade path)
|
|
58
|
+
* - env is merged: legacy values are preserved unless `newEntry.env`
|
|
59
|
+
* overwrites them
|
|
60
|
+
* - non-Reka servers are left untouched
|
|
61
|
+
* - idempotent: applying twice yields the same config
|
|
62
|
+
*/
|
|
63
|
+
function mergeRagServer(config, projectName, newEntry) {
|
|
64
|
+
const result = { ...config, mcpServers: { ...config.mcpServers } };
|
|
65
|
+
const servers = result.mcpServers;
|
|
66
|
+
const candidates = findRekaCandidates(result, projectName);
|
|
67
|
+
const mergedEnv = {};
|
|
68
|
+
for (const name of candidates) {
|
|
69
|
+
Object.assign(mergedEnv, servers[name].env || {});
|
|
70
|
+
delete servers[name];
|
|
71
|
+
}
|
|
72
|
+
Object.assign(mergedEnv, newEntry.env || {});
|
|
73
|
+
servers[exports.TARGET_SERVER_NAME] = { ...newEntry, env: mergedEnv };
|
|
74
|
+
return {
|
|
75
|
+
config: result,
|
|
76
|
+
removed: candidates.filter((n) => n !== exports.TARGET_SERVER_NAME),
|
|
77
|
+
};
|
|
78
|
+
}
|
package/dist/mint.d.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API-key resolution for `reka init` (LB-3).
|
|
3
|
+
*
|
|
4
|
+
* Resolution order:
|
|
5
|
+
* 1. --key flag
|
|
6
|
+
* 2. key already present in .mcp.json (verified against /api/whoami;
|
|
7
|
+
* skipped with --force)
|
|
8
|
+
* 3. mint via the local rag-api Docker container (see init.ts for the
|
|
9
|
+
* full mechanism notes)
|
|
10
|
+
* 4. interactive prompt, with a pointer to the docker-exec admin path
|
|
11
|
+
*
|
|
12
|
+
* All side effects (docker exec, HTTP, stdin) are injected via MintDeps so
|
|
13
|
+
* the fallback order is unit-testable.
|
|
14
|
+
*/
|
|
15
|
+
export interface MintDeps {
|
|
16
|
+
/** Run a command without a shell; rejects on non-zero exit. */
|
|
17
|
+
execFile(cmd: string, args: string[]): Promise<{
|
|
18
|
+
stdout: string;
|
|
19
|
+
}>;
|
|
20
|
+
/** GET url with headers; resolves { status } or rejects on network error. */
|
|
21
|
+
httpGet(url: string, headers?: Record<string, string>): Promise<{
|
|
22
|
+
status: number;
|
|
23
|
+
}>;
|
|
24
|
+
prompt(question: string): Promise<string>;
|
|
25
|
+
sleep(ms: number): Promise<void>;
|
|
26
|
+
log(message: string): void;
|
|
27
|
+
}
|
|
28
|
+
export interface MintOptions {
|
|
29
|
+
projectName: string;
|
|
30
|
+
apiUrl: string;
|
|
31
|
+
/** rag-api container name (--container / REKA_CONTAINER / "reka-api"). */
|
|
32
|
+
container: string;
|
|
33
|
+
/** --key flag value. */
|
|
34
|
+
key?: string;
|
|
35
|
+
/** Key found in an existing .mcp.json entry. */
|
|
36
|
+
existingKey?: string;
|
|
37
|
+
/** --force: ignore the existing key and mint a fresh one. */
|
|
38
|
+
force?: boolean;
|
|
39
|
+
}
|
|
40
|
+
export interface MintResult {
|
|
41
|
+
key: string;
|
|
42
|
+
source: "flag" | "existing" | "minted" | "prompt";
|
|
43
|
+
/** keys.json id — needed to revoke the key later. */
|
|
44
|
+
keyId?: string;
|
|
45
|
+
/** True when the container had to be restarted to load the new key. */
|
|
46
|
+
restarted?: boolean;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Script executed inside the container via `docker exec <name> node -e`.
|
|
50
|
+
* Tries the live admin endpoint first (key becomes valid immediately);
|
|
51
|
+
* falls back to the offline auth module (requires a container restart).
|
|
52
|
+
* Prints a single JSON line: {"key":"rk_...","id":"...","live":bool}.
|
|
53
|
+
*/
|
|
54
|
+
export declare function buildMintScript(projectName: string): string;
|
|
55
|
+
/** Extracts the {"key":...} JSON line from mixed stdout (logger noise). */
|
|
56
|
+
export declare function parseMintOutput(stdout: string): {
|
|
57
|
+
key: string;
|
|
58
|
+
id?: string;
|
|
59
|
+
live: boolean;
|
|
60
|
+
} | null;
|
|
61
|
+
/** true = valid, false = rejected, null = API unreachable. */
|
|
62
|
+
export declare function verifyKey(apiUrl: string, key: string, deps: Pick<MintDeps, "httpGet">): Promise<boolean | null>;
|
|
63
|
+
export declare function dockerMintHint(container: string, projectName: string): string;
|
|
64
|
+
export declare function resolveApiKey(opts: MintOptions, deps: MintDeps): Promise<MintResult>;
|
package/dist/mint.js
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* API-key resolution for `reka init` (LB-3).
|
|
4
|
+
*
|
|
5
|
+
* Resolution order:
|
|
6
|
+
* 1. --key flag
|
|
7
|
+
* 2. key already present in .mcp.json (verified against /api/whoami;
|
|
8
|
+
* skipped with --force)
|
|
9
|
+
* 3. mint via the local rag-api Docker container (see init.ts for the
|
|
10
|
+
* full mechanism notes)
|
|
11
|
+
* 4. interactive prompt, with a pointer to the docker-exec admin path
|
|
12
|
+
*
|
|
13
|
+
* All side effects (docker exec, HTTP, stdin) are injected via MintDeps so
|
|
14
|
+
* the fallback order is unit-testable.
|
|
15
|
+
*/
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.buildMintScript = buildMintScript;
|
|
18
|
+
exports.parseMintOutput = parseMintOutput;
|
|
19
|
+
exports.verifyKey = verifyKey;
|
|
20
|
+
exports.dockerMintHint = dockerMintHint;
|
|
21
|
+
exports.resolveApiKey = resolveApiKey;
|
|
22
|
+
const PROJECT_NAME_RE = /^[a-zA-Z0-9_-]+$/;
|
|
23
|
+
const KEY_LABEL = "cli-init";
|
|
24
|
+
/**
|
|
25
|
+
* Script executed inside the container via `docker exec <name> node -e`.
|
|
26
|
+
* Tries the live admin endpoint first (key becomes valid immediately);
|
|
27
|
+
* falls back to the offline auth module (requires a container restart).
|
|
28
|
+
* Prints a single JSON line: {"key":"rk_...","id":"...","live":bool}.
|
|
29
|
+
*/
|
|
30
|
+
function buildMintScript(projectName) {
|
|
31
|
+
const project = JSON.stringify(projectName);
|
|
32
|
+
const label = JSON.stringify(KEY_LABEL);
|
|
33
|
+
return [
|
|
34
|
+
"(async () => {",
|
|
35
|
+
" const port = process.env.API_PORT || process.env.PORT || 3100;",
|
|
36
|
+
" try {",
|
|
37
|
+
" const r = await fetch('http://127.0.0.1:' + port + '/api/keys', {",
|
|
38
|
+
" method: 'POST',",
|
|
39
|
+
" headers: { 'Content-Type': 'application/json' },",
|
|
40
|
+
` body: JSON.stringify({ projectName: ${project}, label: ${label} }),`,
|
|
41
|
+
" });",
|
|
42
|
+
" if (r.ok) {",
|
|
43
|
+
" const d = await r.json();",
|
|
44
|
+
" if (d.key) {",
|
|
45
|
+
" console.log(JSON.stringify({ key: d.key, id: d.id, live: true }));",
|
|
46
|
+
" return;",
|
|
47
|
+
" }",
|
|
48
|
+
" }",
|
|
49
|
+
" } catch {}",
|
|
50
|
+
" const { generateKey } = require('./dist/middleware/auth');",
|
|
51
|
+
` const e = generateKey(${project}, ${label});`,
|
|
52
|
+
" console.log(JSON.stringify({ key: e.key, id: e.id, live: false }));",
|
|
53
|
+
"})();",
|
|
54
|
+
].join("\n");
|
|
55
|
+
}
|
|
56
|
+
/** Extracts the {"key":...} JSON line from mixed stdout (logger noise). */
|
|
57
|
+
function parseMintOutput(stdout) {
|
|
58
|
+
for (const line of stdout.split("\n").reverse()) {
|
|
59
|
+
const trimmed = line.trim();
|
|
60
|
+
if (!trimmed.startsWith("{"))
|
|
61
|
+
continue;
|
|
62
|
+
try {
|
|
63
|
+
const parsed = JSON.parse(trimmed);
|
|
64
|
+
if (typeof parsed.key === "string" && parsed.key.startsWith("rk_")) {
|
|
65
|
+
return { key: parsed.key, id: parsed.id, live: !!parsed.live };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
// not our line
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
/** true = valid, false = rejected, null = API unreachable. */
|
|
75
|
+
async function verifyKey(apiUrl, key, deps) {
|
|
76
|
+
try {
|
|
77
|
+
const { status } = await deps.httpGet(`${apiUrl}/api/whoami`, {
|
|
78
|
+
"X-Api-Key": key,
|
|
79
|
+
});
|
|
80
|
+
return status === 200;
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
async function waitForHealth(apiUrl, deps, attempts = 60) {
|
|
87
|
+
for (let i = 0; i < attempts; i++) {
|
|
88
|
+
try {
|
|
89
|
+
const { status } = await deps.httpGet(`${apiUrl}/api/health`);
|
|
90
|
+
if (status === 200)
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
// still starting
|
|
95
|
+
}
|
|
96
|
+
await deps.sleep(1000);
|
|
97
|
+
}
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
async function containerRunning(container, deps) {
|
|
101
|
+
try {
|
|
102
|
+
const { stdout } = await deps.execFile("docker", [
|
|
103
|
+
"inspect",
|
|
104
|
+
"-f",
|
|
105
|
+
"{{.State.Running}}",
|
|
106
|
+
container,
|
|
107
|
+
]);
|
|
108
|
+
return stdout.trim() === "true";
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function dockerMintHint(container, projectName) {
|
|
115
|
+
return (`docker exec ${container} node -e "const {generateKey}=require('./dist/middleware/auth'); ` +
|
|
116
|
+
`console.log(generateKey('${projectName}','${KEY_LABEL}').key)" ` +
|
|
117
|
+
`&& docker restart ${container}`);
|
|
118
|
+
}
|
|
119
|
+
async function mintViaDocker(opts, deps) {
|
|
120
|
+
if (!PROJECT_NAME_RE.test(opts.projectName)) {
|
|
121
|
+
deps.log(`Project name "${opts.projectName}" contains characters unsafe for key minting.`);
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
if (!(await containerRunning(opts.container, deps))) {
|
|
125
|
+
deps.log(`Container "${opts.container}" not found or not running (override with --container or REKA_CONTAINER).`);
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
let output;
|
|
129
|
+
try {
|
|
130
|
+
const { stdout } = await deps.execFile("docker", [
|
|
131
|
+
"exec",
|
|
132
|
+
opts.container,
|
|
133
|
+
"node",
|
|
134
|
+
"-e",
|
|
135
|
+
buildMintScript(opts.projectName),
|
|
136
|
+
]);
|
|
137
|
+
output = parseMintOutput(stdout);
|
|
138
|
+
}
|
|
139
|
+
catch (err) {
|
|
140
|
+
deps.log(`Key minting failed: ${err?.message || err}`);
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
if (!output) {
|
|
144
|
+
deps.log("Key minting returned no key.");
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
let restarted = false;
|
|
148
|
+
if (!output.live) {
|
|
149
|
+
// Key persisted to data/keys.json but the running server only loads
|
|
150
|
+
// keys at startup — restart to activate it.
|
|
151
|
+
deps.log(`Restarting ${opts.container} to load the new key (keys are read at startup)...`);
|
|
152
|
+
try {
|
|
153
|
+
await deps.execFile("docker", ["restart", opts.container]);
|
|
154
|
+
restarted = true;
|
|
155
|
+
}
|
|
156
|
+
catch (err) {
|
|
157
|
+
deps.log(`Container restart failed: ${err?.message || err}`);
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
if (!(await waitForHealth(opts.apiUrl, deps))) {
|
|
161
|
+
deps.log("API did not come back healthy after restart.");
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
const valid = await verifyKey(opts.apiUrl, output.key, deps);
|
|
166
|
+
if (valid === false) {
|
|
167
|
+
deps.log("Minted key was rejected by the API — falling back.");
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
return { key: output.key, source: "minted", keyId: output.id, restarted };
|
|
171
|
+
}
|
|
172
|
+
async function resolveApiKey(opts, deps) {
|
|
173
|
+
// 1. Explicit flag always wins.
|
|
174
|
+
if (opts.key) {
|
|
175
|
+
return { key: opts.key, source: "flag" };
|
|
176
|
+
}
|
|
177
|
+
// 2. Reuse the key already in .mcp.json (idempotent re-runs must not
|
|
178
|
+
// mint a new key every time). --force skips reuse.
|
|
179
|
+
if (opts.existingKey && !opts.force) {
|
|
180
|
+
const valid = await verifyKey(opts.apiUrl, opts.existingKey, deps);
|
|
181
|
+
if (valid !== false) {
|
|
182
|
+
if (valid === null) {
|
|
183
|
+
deps.log("API unreachable — keeping the existing key unverified.");
|
|
184
|
+
}
|
|
185
|
+
return { key: opts.existingKey, source: "existing" };
|
|
186
|
+
}
|
|
187
|
+
deps.log("Existing key in .mcp.json was rejected — minting a new one.");
|
|
188
|
+
}
|
|
189
|
+
// 3. Mint via the local rag-api container.
|
|
190
|
+
const minted = await mintViaDocker(opts, deps);
|
|
191
|
+
if (minted)
|
|
192
|
+
return minted;
|
|
193
|
+
// 4. Interactive prompt with a pointer to the docker-exec admin path.
|
|
194
|
+
const answer = (await deps.prompt(`Paste an API key for project "${opts.projectName}" ` +
|
|
195
|
+
`(admins can mint one with:\n ${dockerMintHint(opts.container, opts.projectName)}\n): `)).trim();
|
|
196
|
+
if (!answer) {
|
|
197
|
+
throw new Error("No API key provided.");
|
|
198
|
+
}
|
|
199
|
+
return { key: answer, source: "prompt" };
|
|
200
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project-file writes for `reka init` — parity with the mcp-server
|
|
3
|
+
* `setup_project` tool: .mcp.json, CLAUDE.md RAG section, and
|
|
4
|
+
* .claude/settings.local.json permissions. All writes are idempotent.
|
|
5
|
+
*/
|
|
6
|
+
import { McpConfig, McpServerEntry } from "./mcp-config";
|
|
7
|
+
export interface SetupResult {
|
|
8
|
+
changes: string[];
|
|
9
|
+
}
|
|
10
|
+
export declare function readMcpConfig(projectPath: string): McpConfig;
|
|
11
|
+
export declare function buildRagEntry(opts: {
|
|
12
|
+
apiUrl: string;
|
|
13
|
+
projectName: string;
|
|
14
|
+
projectPath: string;
|
|
15
|
+
apiKey: string;
|
|
16
|
+
}): McpServerEntry;
|
|
17
|
+
export declare function applyProjectFiles(opts: {
|
|
18
|
+
projectPath: string;
|
|
19
|
+
projectName: string;
|
|
20
|
+
entry: McpServerEntry;
|
|
21
|
+
/** --force: discard the existing .mcp.json instead of merging into it. */
|
|
22
|
+
force?: boolean;
|
|
23
|
+
}): SetupResult;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Project-file writes for `reka init` — parity with the mcp-server
|
|
4
|
+
* `setup_project` tool: .mcp.json, CLAUDE.md RAG section, and
|
|
5
|
+
* .claude/settings.local.json permissions. All writes are idempotent.
|
|
6
|
+
*/
|
|
7
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
8
|
+
if (k2 === undefined) k2 = k;
|
|
9
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
10
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
11
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
12
|
+
}
|
|
13
|
+
Object.defineProperty(o, k2, desc);
|
|
14
|
+
}) : (function(o, m, k, k2) {
|
|
15
|
+
if (k2 === undefined) k2 = k;
|
|
16
|
+
o[k2] = m[k];
|
|
17
|
+
}));
|
|
18
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
19
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
20
|
+
}) : function(o, v) {
|
|
21
|
+
o["default"] = v;
|
|
22
|
+
});
|
|
23
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
24
|
+
var ownKeys = function(o) {
|
|
25
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
26
|
+
var ar = [];
|
|
27
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
28
|
+
return ar;
|
|
29
|
+
};
|
|
30
|
+
return ownKeys(o);
|
|
31
|
+
};
|
|
32
|
+
return function (mod) {
|
|
33
|
+
if (mod && mod.__esModule) return mod;
|
|
34
|
+
var result = {};
|
|
35
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
36
|
+
__setModuleDefault(result, mod);
|
|
37
|
+
return result;
|
|
38
|
+
};
|
|
39
|
+
})();
|
|
40
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
41
|
+
exports.readMcpConfig = readMcpConfig;
|
|
42
|
+
exports.buildRagEntry = buildRagEntry;
|
|
43
|
+
exports.applyProjectFiles = applyProjectFiles;
|
|
44
|
+
const fs = __importStar(require("fs"));
|
|
45
|
+
const path = __importStar(require("path"));
|
|
46
|
+
const mcp_config_1 = require("./mcp-config");
|
|
47
|
+
function readMcpConfig(projectPath) {
|
|
48
|
+
try {
|
|
49
|
+
return JSON.parse(fs.readFileSync(path.join(projectPath, ".mcp.json"), "utf-8"));
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return {};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function buildRagEntry(opts) {
|
|
56
|
+
return {
|
|
57
|
+
command: "npx",
|
|
58
|
+
args: ["-y", "@getreka/mcp@latest"],
|
|
59
|
+
env: {
|
|
60
|
+
REKA_API_URL: opts.apiUrl,
|
|
61
|
+
PROJECT_NAME: opts.projectName,
|
|
62
|
+
PROJECT_PATH: opts.projectPath,
|
|
63
|
+
REKA_API_KEY: opts.apiKey,
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/** Same content as mcp-server setup_project writes. */
|
|
68
|
+
const RAG_SECTION = `\n## RAG Integration
|
|
69
|
+
|
|
70
|
+
You MUST call \`context_briefing\` before making any code changes.
|
|
71
|
+
This single tool performs all RAG lookups in parallel (recall, search, patterns, ADRs, graph).
|
|
72
|
+
|
|
73
|
+
Example: \`context_briefing(task: "describe your change", files: ["path/to/file.ts"])\`
|
|
74
|
+
|
|
75
|
+
After completing significant changes:
|
|
76
|
+
- \`remember\` — save important context for future sessions
|
|
77
|
+
- \`record_adr\` — document architectural decisions
|
|
78
|
+
`;
|
|
79
|
+
function applyProjectFiles(opts) {
|
|
80
|
+
const { projectPath, projectName, entry, force } = opts;
|
|
81
|
+
const changes = [];
|
|
82
|
+
// 1. .mcp.json — merge/rename legacy entries into a single "rag" entry
|
|
83
|
+
const existing = force ? {} : readMcpConfig(projectPath);
|
|
84
|
+
const { config, removed } = (0, mcp_config_1.mergeRagServer)(existing, projectName, entry);
|
|
85
|
+
fs.writeFileSync(path.join(projectPath, ".mcp.json"), JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
86
|
+
changes.push(removed.length
|
|
87
|
+
? `.mcp.json — "${mcp_config_1.TARGET_SERVER_NAME}" server written (merged legacy: ${removed.join(", ")})`
|
|
88
|
+
: `.mcp.json — "${mcp_config_1.TARGET_SERVER_NAME}" server written`);
|
|
89
|
+
// 2. CLAUDE.md — add RAG section (same content/idempotency as setup_project)
|
|
90
|
+
const claudeMdPath = path.join(projectPath, "CLAUDE.md");
|
|
91
|
+
let claudeMd = "";
|
|
92
|
+
try {
|
|
93
|
+
claudeMd = fs.readFileSync(claudeMdPath, "utf-8");
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
// File doesn't exist
|
|
97
|
+
}
|
|
98
|
+
if (claudeMd.includes("## RAG")) {
|
|
99
|
+
changes.push("CLAUDE.md — RAG section already exists, skipped");
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
claudeMd = claudeMd
|
|
103
|
+
? claudeMd.trimEnd() + "\n" + RAG_SECTION
|
|
104
|
+
: `# CLAUDE.md\n${RAG_SECTION}`;
|
|
105
|
+
fs.writeFileSync(claudeMdPath, claudeMd);
|
|
106
|
+
changes.push("CLAUDE.md — added RAG Integration section");
|
|
107
|
+
}
|
|
108
|
+
// 3. .claude/settings.local.json — allow the MCP server's tools
|
|
109
|
+
const claudeDir = path.join(projectPath, ".claude");
|
|
110
|
+
const settingsPath = path.join(claudeDir, "settings.local.json");
|
|
111
|
+
let settings = {};
|
|
112
|
+
try {
|
|
113
|
+
settings = JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
// File doesn't exist or invalid JSON
|
|
117
|
+
}
|
|
118
|
+
if (!settings.permissions)
|
|
119
|
+
settings.permissions = {};
|
|
120
|
+
if (!settings.permissions.allow)
|
|
121
|
+
settings.permissions.allow = [];
|
|
122
|
+
const mcpPermission = `mcp__${mcp_config_1.TARGET_SERVER_NAME}__*`;
|
|
123
|
+
if (settings.permissions.allow.includes(mcpPermission)) {
|
|
124
|
+
changes.push(".claude/settings.local.json — permission already exists, skipped");
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
settings.permissions.allow.push(mcpPermission);
|
|
128
|
+
if (!fs.existsSync(claudeDir))
|
|
129
|
+
fs.mkdirSync(claudeDir, { recursive: true });
|
|
130
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
131
|
+
changes.push(`.claude/settings.local.json — added \`${mcpPermission}\` permission`);
|
|
132
|
+
}
|
|
133
|
+
return { changes };
|
|
134
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@getreka/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Reka CLI — manage self-hosted RAG infrastructure for AI coding agents",
|
|
5
5
|
"bin": {
|
|
6
6
|
"reka": "dist/index.js"
|
|
@@ -22,20 +22,24 @@
|
|
|
22
22
|
"scripts": {
|
|
23
23
|
"build": "tsc",
|
|
24
24
|
"dev": "ts-node src/index.ts",
|
|
25
|
-
"start": "node dist/index.js"
|
|
25
|
+
"start": "node dist/index.js",
|
|
26
|
+
"test": "vitest run",
|
|
27
|
+
"test:watch": "vitest"
|
|
26
28
|
},
|
|
27
29
|
"dependencies": {
|
|
28
30
|
"axios": "^1.7.0",
|
|
29
31
|
"chalk": "^4.1.2",
|
|
30
32
|
"commander": "^12.1.0",
|
|
31
33
|
"js-yaml": "^4.1.0",
|
|
34
|
+
"open": "^11.0.0",
|
|
32
35
|
"ora": "^5.4.1"
|
|
33
36
|
},
|
|
34
37
|
"devDependencies": {
|
|
35
38
|
"@types/js-yaml": "^4.0.9",
|
|
36
39
|
"@types/node": "^20.14.0",
|
|
40
|
+
"ts-node": "^10.9.2",
|
|
37
41
|
"typescript": "^5.5.0",
|
|
38
|
-
"
|
|
42
|
+
"vitest": "^4.0.18"
|
|
39
43
|
},
|
|
40
44
|
"engines": {
|
|
41
45
|
"node": ">=18"
|
package/dist/commands/search.js
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.searchCommand = searchCommand;
|
|
7
|
-
const chalk_1 = __importDefault(require("chalk"));
|
|
8
|
-
const ora_1 = __importDefault(require("ora"));
|
|
9
|
-
const api_1 = require("../api");
|
|
10
|
-
async function searchCommand(client, config, query, opts) {
|
|
11
|
-
const limit = parseInt(opts.limit || "5", 10);
|
|
12
|
-
const spinner = (0, ora_1.default)("Searching...").start();
|
|
13
|
-
try {
|
|
14
|
-
const { data } = await client.post("/api/search", {
|
|
15
|
-
query,
|
|
16
|
-
limit,
|
|
17
|
-
collection: opts.type || "codebase",
|
|
18
|
-
});
|
|
19
|
-
spinner.stop();
|
|
20
|
-
const results = data.results || data;
|
|
21
|
-
if (!results || results.length === 0) {
|
|
22
|
-
console.log(chalk_1.default.yellow("\n No results found.\n"));
|
|
23
|
-
return;
|
|
24
|
-
}
|
|
25
|
-
console.log(chalk_1.default.bold(`\n ${results.length} results for "${query}"\n`));
|
|
26
|
-
for (const [i, r] of results.entries()) {
|
|
27
|
-
const score = r.score
|
|
28
|
-
? chalk_1.default.gray(`(${(r.score * 100).toFixed(0)}%)`)
|
|
29
|
-
: "";
|
|
30
|
-
const filePath = r.metadata?.filePath || r.metadata?.file_path || "";
|
|
31
|
-
const lines = r.metadata?.startLine
|
|
32
|
-
? `:${r.metadata.startLine}-${r.metadata.endLine || ""}`
|
|
33
|
-
: "";
|
|
34
|
-
console.log(` ${chalk_1.default.blue(`${i + 1}.`)} ${chalk_1.default.bold(filePath)}${lines} ${score}`);
|
|
35
|
-
// Show snippet (first 3 lines)
|
|
36
|
-
if (r.content || r.text) {
|
|
37
|
-
const text = (r.content || r.text);
|
|
38
|
-
const snippet = text.split("\n").slice(0, 3).join("\n");
|
|
39
|
-
console.log(chalk_1.default.gray(` ${snippet.replace(/\n/g, "\n ")}`));
|
|
40
|
-
}
|
|
41
|
-
console.log("");
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
catch (err) {
|
|
45
|
-
spinner.fail(`Search failed: ${(0, api_1.formatError)(err)}`);
|
|
46
|
-
process.exit(1);
|
|
47
|
-
}
|
|
48
|
-
}
|