create-snipara 1.1.0 → 1.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/README.md +148 -89
- package/dist/index.js +842 -297
- package/package.json +6 -4
package/dist/index.js
CHANGED
|
@@ -1,34 +1,165 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import {
|
|
4
|
+
import { Command } from "commander";
|
|
5
5
|
import prompts from "prompts";
|
|
6
6
|
import chalk from "chalk";
|
|
7
7
|
import ora from "ora";
|
|
8
8
|
import fs from "fs";
|
|
9
9
|
import path from "path";
|
|
10
|
-
import { execSync } from "child_process";
|
|
10
|
+
import { execSync, spawn } from "child_process";
|
|
11
11
|
import { homedir } from "os";
|
|
12
|
-
var VERSION = "1.
|
|
12
|
+
var VERSION = "1.3.0";
|
|
13
13
|
var BASE_URL = "https://www.snipara.com";
|
|
14
14
|
var API_URL = "https://api.snipara.com";
|
|
15
|
+
var COMPANION_PACKAGE_NAME = "snipara-companion";
|
|
16
|
+
var MCP_PACKAGE_NAME = "snipara-mcp";
|
|
17
|
+
var RUNTIME_PACKAGE_NAME = "rlm-runtime[all]";
|
|
18
|
+
function getCompanionInitClientFlag(aiClient) {
|
|
19
|
+
if (aiClient === "cursor") {
|
|
20
|
+
return " --client cursor";
|
|
21
|
+
}
|
|
22
|
+
return aiClient === "claude-code" ? " --client claude-code" : "";
|
|
23
|
+
}
|
|
24
|
+
function buildProgram() {
|
|
25
|
+
const program = new Command();
|
|
26
|
+
program.name("create-snipara").description("Initialize and maintain Snipara MCP + local companion workflows").version(VERSION);
|
|
27
|
+
program.command("init").description("Install and configure Snipara for this project").option("--api-key <key>", "Snipara project API key").option("--team-key <key>", "Snipara team API key").option("--slug <slug>", "Project slug").option("--project-id <id>", "Project ID").option(
|
|
28
|
+
"--profile <profile>",
|
|
29
|
+
"Install profile: hosted-only, hosted-companion, full-stack, runtime-only"
|
|
30
|
+
).option("--skip-companion", "Skip local companion CLI installation").option("--skip-mcp", "Skip snipara-mcp installation").option("--skip-runtime", "Skip rlm-runtime installation").option("--skip-hooks", "Skip hook generation").option("--skip-test", "Skip hosted connection test").option("--runtime-only", "Install only RLM-Runtime").option("-y, --yes", "Accept defaults without prompting").action(async (options) => {
|
|
31
|
+
printBanner();
|
|
32
|
+
await runInit(options);
|
|
33
|
+
});
|
|
34
|
+
program.command("doctor").description("Inspect the current project and write a validation report").option("--json", "Print the report as JSON").action(async (options) => {
|
|
35
|
+
printBanner();
|
|
36
|
+
await runDoctor(options);
|
|
37
|
+
});
|
|
38
|
+
program.command("repair").description("Rebuild local Snipara config and re-run validation").option("--api-key <key>", "Snipara project API key override").option("--team-key <key>", "Snipara team API key override").option("--slug <slug>", "Project slug override").option("--project-id <id>", "Project ID override").option("--profile <profile>", "Target install profile").option("--with-hooks", "Force hook generation").option("--with-runtime", "Force runtime server/config").option("--with-mcp", "Force MCP config").option("--with-companion", "Force companion pack generation").action(async (options) => {
|
|
39
|
+
printBanner();
|
|
40
|
+
await runRepair(options);
|
|
41
|
+
});
|
|
42
|
+
program.command("upgrade").description("Upgrade installed Snipara local components").option("--with-hooks", "Refresh hooks after upgrade").option("--with-runtime", "Upgrade runtime even if not detected").option("--with-mcp", "Upgrade snipara-mcp even if not detected").option("--with-companion", "Upgrade companion even if not detected").action(async (options) => {
|
|
43
|
+
printBanner();
|
|
44
|
+
await runUpgrade(options);
|
|
45
|
+
});
|
|
46
|
+
program.command("print-config").description("Print detected local setup and inferred install profile").option("--json", "Print JSON instead of a formatted summary").action(async (options) => {
|
|
47
|
+
printBanner();
|
|
48
|
+
await runPrintConfig(options);
|
|
49
|
+
});
|
|
50
|
+
addSharedRootOptions(program);
|
|
51
|
+
program.action(async (options) => {
|
|
52
|
+
printBanner();
|
|
53
|
+
await runInit(options);
|
|
54
|
+
});
|
|
55
|
+
return program;
|
|
56
|
+
}
|
|
57
|
+
function addSharedRootOptions(program) {
|
|
58
|
+
program.option("--api-key <key>", "Snipara project API key").option("--team-key <key>", "Snipara team API key").option("--slug <slug>", "Project slug").option("--project-id <id>", "Project ID").option(
|
|
59
|
+
"--profile <profile>",
|
|
60
|
+
"Install profile: hosted-only, hosted-companion, full-stack, runtime-only"
|
|
61
|
+
).option("--skip-companion", "Skip local companion CLI installation").option("--skip-mcp", "Skip snipara-mcp installation").option("--skip-runtime", "Skip rlm-runtime installation").option("--skip-hooks", "Skip hook generation").option("--skip-test", "Skip hosted connection test").option("--runtime-only", "Install only RLM-Runtime").option("-y, --yes", "Accept defaults without prompting");
|
|
62
|
+
}
|
|
15
63
|
async function main() {
|
|
16
|
-
program
|
|
17
|
-
|
|
18
|
-
|
|
64
|
+
const program = buildProgram();
|
|
65
|
+
await program.parseAsync(process.argv);
|
|
66
|
+
}
|
|
67
|
+
async function runInit(options) {
|
|
19
68
|
const answers = await gatherConfiguration(options);
|
|
20
69
|
if (answers.installMcp || answers.installRuntime) {
|
|
21
|
-
await installPythonPackages(answers);
|
|
70
|
+
await installPythonPackages(answers, false);
|
|
71
|
+
}
|
|
72
|
+
if (answers.installCompanion) {
|
|
73
|
+
await installNodeCompanion(false);
|
|
74
|
+
}
|
|
75
|
+
if (answers.apiKeyType !== "none" || answers.installRuntime || answers.installMcp) {
|
|
76
|
+
await createMcpConfig(answers);
|
|
22
77
|
}
|
|
23
|
-
await createMcpConfig(answers);
|
|
24
78
|
if (answers.installHooks && answers.aiClient === "claude-code") {
|
|
25
79
|
await installClaudeCodeHooks(answers);
|
|
26
80
|
}
|
|
27
81
|
await updateEnvFiles(answers);
|
|
28
|
-
|
|
29
|
-
|
|
82
|
+
await createCompanionPack(answers);
|
|
83
|
+
const validationReport = await runValidation(answers, !options.skipTest);
|
|
84
|
+
writeDoctorReport(validationReport);
|
|
85
|
+
printSuccess(answers, validationReport);
|
|
86
|
+
if (answers.runRlmInit && answers.installRuntime) {
|
|
87
|
+
await runRlmInit();
|
|
30
88
|
}
|
|
31
|
-
|
|
89
|
+
}
|
|
90
|
+
async function runDoctor(options) {
|
|
91
|
+
const answers = loadExistingAnswers(options);
|
|
92
|
+
const report = await runValidation(answers, true);
|
|
93
|
+
writeDoctorReport(report);
|
|
94
|
+
if (options.json) {
|
|
95
|
+
console.log(JSON.stringify({ detection: detectEnvironment(), validation: report }, null, 2));
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
printDoctorReport(detectEnvironment(), report);
|
|
99
|
+
}
|
|
100
|
+
async function runRepair(options) {
|
|
101
|
+
const answers = loadExistingAnswers(options);
|
|
102
|
+
console.log(chalk.bold("\n\u{1F6E0}\uFE0F Repairing Local Setup\n"));
|
|
103
|
+
if (answers.installMcp || answers.installRuntime) {
|
|
104
|
+
await installPythonPackages(answers, true);
|
|
105
|
+
}
|
|
106
|
+
if (answers.installCompanion) {
|
|
107
|
+
await installNodeCompanion(true);
|
|
108
|
+
}
|
|
109
|
+
await createMcpConfig(answers);
|
|
110
|
+
await updateEnvFiles(answers);
|
|
111
|
+
await createCompanionPack(answers);
|
|
112
|
+
if (answers.installHooks && answers.aiClient === "claude-code") {
|
|
113
|
+
await installClaudeCodeHooks(answers);
|
|
114
|
+
}
|
|
115
|
+
const report = await runValidation(answers, true);
|
|
116
|
+
writeDoctorReport(report);
|
|
117
|
+
printDoctorReport(detectEnvironment(), report);
|
|
118
|
+
}
|
|
119
|
+
async function runUpgrade(options) {
|
|
120
|
+
const detection = detectEnvironment();
|
|
121
|
+
const answers = loadExistingAnswers(options);
|
|
122
|
+
console.log(chalk.bold("\n\u2B06\uFE0F Upgrading Local Components\n"));
|
|
123
|
+
if (options.withMcp || options.withRuntime || detection.existingConfig.hasRuntimeServer) {
|
|
124
|
+
await installPythonPackages(
|
|
125
|
+
{
|
|
126
|
+
...answers,
|
|
127
|
+
installMcp: options.withMcp || answers.installMcp,
|
|
128
|
+
installRuntime: options.withRuntime || answers.installRuntime
|
|
129
|
+
},
|
|
130
|
+
true
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
if (options.withCompanion || detection.tooling.companionCli || answers.installCompanion) {
|
|
134
|
+
await installNodeCompanion(true);
|
|
135
|
+
}
|
|
136
|
+
if (options.withHooks || answers.installHooks) {
|
|
137
|
+
await installClaudeCodeHooks({ ...answers, installHooks: true });
|
|
138
|
+
}
|
|
139
|
+
await createCompanionPack(answers);
|
|
140
|
+
const report = await runValidation(answers, true);
|
|
141
|
+
writeDoctorReport(report);
|
|
142
|
+
printDoctorReport(detectEnvironment(), report);
|
|
143
|
+
}
|
|
144
|
+
async function runPrintConfig(options) {
|
|
145
|
+
const detection = detectEnvironment();
|
|
146
|
+
const answers = loadExistingAnswers(options);
|
|
147
|
+
if (options.json) {
|
|
148
|
+
console.log(JSON.stringify({ detection, inferredSetup: answers }, null, 2));
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
console.log(chalk.bold("\n\u{1F4E6} Current Setup\n"));
|
|
152
|
+
console.log(` Project slug: ${answers.projectSlug}`);
|
|
153
|
+
console.log(` Project id: ${answers.projectId || "not-set"}`);
|
|
154
|
+
console.log(` Profile: ${answers.profile}`);
|
|
155
|
+
console.log(` AI client: ${answers.aiClient}`);
|
|
156
|
+
console.log(` Companion: ${answers.installCompanion ? "enabled" : "disabled"}`);
|
|
157
|
+
console.log(` Hosted MCP: ${answers.installMcp ? "enabled" : "disabled"}`);
|
|
158
|
+
console.log(` Runtime: ${answers.installRuntime ? "enabled" : "disabled"}`);
|
|
159
|
+
console.log(` Hooks: ${answers.installHooks ? "enabled" : "disabled"}`);
|
|
160
|
+
console.log(` Existing .mcp.json: ${detection.files.mcpConfig ? "yes" : "no"}`);
|
|
161
|
+
console.log(` Companion pack: ${detection.files.sniparaCompanionPack ? "yes" : "no"}`);
|
|
162
|
+
console.log("");
|
|
32
163
|
}
|
|
33
164
|
function printBanner() {
|
|
34
165
|
console.log(
|
|
@@ -37,10 +168,10 @@ function printBanner() {
|
|
|
37
168
|
\u2551 \u2551
|
|
38
169
|
\u2551 ${chalk.bold("Snipara")} - Context Optimization + RLM-Runtime \u2551
|
|
39
170
|
\u2551 \u2551
|
|
40
|
-
\u2551 \u2022
|
|
41
|
-
\u2551 \u2022
|
|
171
|
+
\u2551 \u2022 Hosted MCP, memory, and orchestration \u2551
|
|
172
|
+
\u2551 \u2022 Optional local companion workflows \u2551
|
|
42
173
|
\u2551 \u2022 Safe code execution with Docker isolation \u2551
|
|
43
|
-
\u2551 \u2022
|
|
174
|
+
\u2551 \u2022 One CLI for install, doctor, repair, and upgrade \u2551
|
|
44
175
|
\u2551 \u2551
|
|
45
176
|
\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D
|
|
46
177
|
`)
|
|
@@ -57,42 +188,35 @@ async function gatherConfiguration(options) {
|
|
|
57
188
|
detectedSlug = existingConfig.slug || detectedSlug;
|
|
58
189
|
detectedProjectId = existingConfig.projectId || "";
|
|
59
190
|
}
|
|
60
|
-
|
|
61
|
-
console.log(chalk.cyan("\n \u2139\uFE0F Runtime-only mode: No API key needed for local code execution\n"));
|
|
62
|
-
console.log(chalk.dim(" execute_python works without any API key when used in Claude Code."));
|
|
63
|
-
console.log(chalk.dim(" For rlm run/agent CLI, you'll need OPENAI_API_KEY or ANTHROPIC_API_KEY.\n"));
|
|
64
|
-
return {
|
|
65
|
-
projectSlug: options.slug || detectedSlug || "my-project",
|
|
66
|
-
projectId: "",
|
|
67
|
-
apiKey: "",
|
|
68
|
-
apiKeyType: "none",
|
|
69
|
-
installMcp: false,
|
|
70
|
-
installRuntime: true,
|
|
71
|
-
installHooks: false,
|
|
72
|
-
aiClient: detectedClient
|
|
73
|
-
};
|
|
74
|
-
}
|
|
191
|
+
const initialProfile = resolveRequestedProfile(options);
|
|
75
192
|
if (options.yes) {
|
|
76
|
-
|
|
77
|
-
const apiKeyType2 = options.teamKey ? "team" : "project";
|
|
78
|
-
return {
|
|
193
|
+
return buildAnswersFromProfile({
|
|
79
194
|
projectSlug: options.slug || detectedSlug || "my-project",
|
|
80
195
|
projectId: options.projectId || detectedProjectId,
|
|
81
|
-
apiKey:
|
|
82
|
-
apiKeyType:
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
196
|
+
apiKey: options.teamKey || options.apiKey || "YOUR_API_KEY_HERE",
|
|
197
|
+
apiKeyType: options.teamKey ? "team" : options.apiKey ? "project" : "none",
|
|
198
|
+
profile: initialProfile,
|
|
199
|
+
aiClient: detectedClient,
|
|
200
|
+
skipCompanion: !!options.skipCompanion,
|
|
201
|
+
skipMcp: !!options.skipMcp,
|
|
202
|
+
skipRuntime: !!options.skipRuntime,
|
|
203
|
+
skipHooks: !!options.skipHooks
|
|
204
|
+
});
|
|
88
205
|
}
|
|
89
206
|
const response = await prompts([
|
|
207
|
+
{
|
|
208
|
+
type: "select",
|
|
209
|
+
name: "profile",
|
|
210
|
+
message: "Choose an install profile:",
|
|
211
|
+
initial: installProfileChoices().findIndex((choice) => choice.value === initialProfile),
|
|
212
|
+
choices: installProfileChoices()
|
|
213
|
+
},
|
|
90
214
|
{
|
|
91
215
|
type: "text",
|
|
92
216
|
name: "projectSlug",
|
|
93
217
|
message: "Project slug:",
|
|
94
218
|
initial: options.slug || detectedSlug || "",
|
|
95
|
-
validate: (
|
|
219
|
+
validate: (value) => value.length > 0 ? true : "Project slug is required (for example my-project)"
|
|
96
220
|
},
|
|
97
221
|
{
|
|
98
222
|
type: "text",
|
|
@@ -101,7 +225,7 @@ async function gatherConfiguration(options) {
|
|
|
101
225
|
initial: options.projectId || detectedProjectId || ""
|
|
102
226
|
},
|
|
103
227
|
{
|
|
104
|
-
type: options.apiKey || options.teamKey ? null : "select",
|
|
228
|
+
type: (_, values) => values.profile === "runtime-only" || options.apiKey || options.teamKey ? null : "select",
|
|
105
229
|
name: "apiKeyType",
|
|
106
230
|
message: "What type of API key do you have?",
|
|
107
231
|
choices: [
|
|
@@ -126,138 +250,194 @@ async function gatherConfiguration(options) {
|
|
|
126
250
|
]
|
|
127
251
|
},
|
|
128
252
|
{
|
|
129
|
-
type: (
|
|
253
|
+
type: (_, values) => values.profile === "runtime-only" ? null : values.apiKeyType === "project" || values.apiKeyType === "team" ? "text" : null,
|
|
130
254
|
name: "apiKey",
|
|
131
|
-
message: (
|
|
132
|
-
validate: (
|
|
255
|
+
message: (_, values) => values.apiKeyType === "team" ? "Enter your Team API key:" : "Enter your Project API key:",
|
|
256
|
+
validate: (value) => value.startsWith("rlm_") ? true : "API key should start with rlm_"
|
|
133
257
|
},
|
|
134
258
|
{
|
|
135
259
|
type: "select",
|
|
136
260
|
name: "aiClient",
|
|
137
261
|
message: "Which AI client are you using?",
|
|
138
|
-
initial: ["claude-code", "cursor", "claude-desktop", "other"].indexOf(
|
|
139
|
-
detectedClient
|
|
140
|
-
),
|
|
262
|
+
initial: ["claude-code", "cursor", "claude-desktop", "other"].indexOf(detectedClient),
|
|
141
263
|
choices: [
|
|
142
264
|
{
|
|
143
265
|
title: "Claude Code (CLI/VS Code extension)",
|
|
144
266
|
value: "claude-code",
|
|
145
|
-
description: "
|
|
267
|
+
description: "Best hooks support"
|
|
146
268
|
},
|
|
147
269
|
{
|
|
148
270
|
title: "Cursor",
|
|
149
271
|
value: "cursor",
|
|
150
|
-
description: "MCP
|
|
272
|
+
description: "MCP via .mcp.json plus companion terminal workflows"
|
|
151
273
|
},
|
|
152
274
|
{
|
|
153
275
|
title: "Claude Desktop",
|
|
154
276
|
value: "claude-desktop",
|
|
155
|
-
description: "MCP
|
|
277
|
+
description: "Desktop MCP config flow"
|
|
156
278
|
},
|
|
157
279
|
{ title: "Other", value: "other" }
|
|
158
280
|
]
|
|
159
281
|
},
|
|
160
282
|
{
|
|
161
|
-
type: "
|
|
162
|
-
name: "
|
|
163
|
-
message: "
|
|
164
|
-
|
|
165
|
-
{
|
|
166
|
-
title: "snipara-mcp",
|
|
167
|
-
value: "mcp",
|
|
168
|
-
selected: !options.skipMcp,
|
|
169
|
-
description: "MCP server for context queries (requires API key)"
|
|
170
|
-
},
|
|
171
|
-
{
|
|
172
|
-
title: "rlm-runtime",
|
|
173
|
-
value: "runtime",
|
|
174
|
-
selected: !options.skipRuntime,
|
|
175
|
-
description: "Safe code execution (no API key for execute_python)"
|
|
176
|
-
},
|
|
177
|
-
{
|
|
178
|
-
title: "Claude Code hooks",
|
|
179
|
-
value: "hooks",
|
|
180
|
-
selected: !options.skipHooks,
|
|
181
|
-
description: "Session memory & automation (Claude Code only)"
|
|
182
|
-
}
|
|
183
|
-
],
|
|
184
|
-
hint: "Space to toggle, Enter to confirm"
|
|
283
|
+
type: "confirm",
|
|
284
|
+
name: "installHooks",
|
|
285
|
+
message: "Generate local hooks when supported?",
|
|
286
|
+
initial: detectedClient === "claude-code"
|
|
185
287
|
}
|
|
186
288
|
]);
|
|
187
289
|
let apiKey = options.teamKey || options.apiKey;
|
|
188
|
-
let apiKeyType = options.teamKey ? "team" : "project";
|
|
189
|
-
if (!apiKey) {
|
|
290
|
+
let apiKeyType = response.profile === "runtime-only" ? "none" : options.teamKey ? "team" : options.apiKey ? "project" : "none";
|
|
291
|
+
if (!apiKey && response.profile !== "runtime-only") {
|
|
190
292
|
if (response.apiKeyType === "signup") {
|
|
191
293
|
apiKey = await handleSignup();
|
|
192
294
|
apiKeyType = "project";
|
|
193
295
|
} else if (response.apiKeyType === "project" || response.apiKeyType === "team") {
|
|
194
296
|
apiKey = response.apiKey;
|
|
195
297
|
apiKeyType = response.apiKeyType;
|
|
196
|
-
} else
|
|
298
|
+
} else {
|
|
197
299
|
apiKey = "YOUR_API_KEY_HERE";
|
|
198
300
|
apiKeyType = "none";
|
|
199
301
|
}
|
|
200
302
|
}
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
303
|
+
const answers = buildAnswersFromProfile({
|
|
304
|
+
projectSlug: response.projectSlug,
|
|
305
|
+
projectId: response.projectId || "",
|
|
306
|
+
apiKey: apiKey || "",
|
|
307
|
+
apiKeyType,
|
|
308
|
+
profile: response.profile || initialProfile,
|
|
309
|
+
aiClient: response.aiClient || detectedClient,
|
|
310
|
+
skipCompanion: !!options.skipCompanion,
|
|
311
|
+
skipMcp: !!options.skipMcp,
|
|
312
|
+
skipRuntime: !!options.skipRuntime,
|
|
313
|
+
skipHooks: !!options.skipHooks || !response.installHooks
|
|
314
|
+
});
|
|
315
|
+
if (answers.installRuntime) {
|
|
316
|
+
const runtimeResponse = await prompts([
|
|
210
317
|
{
|
|
211
318
|
type: "select",
|
|
212
319
|
name: "llmProvider",
|
|
213
|
-
message: "Will you use rlm run/agent CLI?
|
|
320
|
+
message: "Will you use rlm run/agent CLI too?",
|
|
214
321
|
choices: [
|
|
215
322
|
{
|
|
216
|
-
title: "No -
|
|
323
|
+
title: "No - execute_python only",
|
|
217
324
|
value: "none",
|
|
218
|
-
description: "No
|
|
325
|
+
description: "No extra LLM API key needed"
|
|
219
326
|
},
|
|
220
327
|
{
|
|
221
|
-
title: "Yes - OpenAI
|
|
328
|
+
title: "Yes - OpenAI",
|
|
222
329
|
value: "openai",
|
|
223
330
|
description: "Requires OPENAI_API_KEY"
|
|
224
331
|
},
|
|
225
332
|
{
|
|
226
|
-
title: "Yes - Anthropic
|
|
333
|
+
title: "Yes - Anthropic",
|
|
227
334
|
value: "anthropic",
|
|
228
335
|
description: "Requires ANTHROPIC_API_KEY"
|
|
229
336
|
}
|
|
230
337
|
]
|
|
231
338
|
},
|
|
232
339
|
{
|
|
233
|
-
type: (prev) => prev
|
|
340
|
+
type: (prev) => prev === "none" ? null : "text",
|
|
234
341
|
name: "llmApiKey",
|
|
235
342
|
message: (prev) => prev === "openai" ? "Enter your OpenAI API key (sk-...):" : "Enter your Anthropic API key (sk-ant-...):",
|
|
236
|
-
validate: (
|
|
237
|
-
if (values.llmProvider === "openai" && !
|
|
343
|
+
validate: (value, values) => {
|
|
344
|
+
if (values.llmProvider === "openai" && !value.startsWith("sk-")) {
|
|
238
345
|
return "OpenAI key should start with sk-";
|
|
239
346
|
}
|
|
240
|
-
if (values.llmProvider === "anthropic" && !
|
|
347
|
+
if (values.llmProvider === "anthropic" && !value.startsWith("sk-ant-")) {
|
|
241
348
|
return "Anthropic key should start with sk-ant-";
|
|
242
349
|
}
|
|
243
350
|
return true;
|
|
244
351
|
}
|
|
352
|
+
},
|
|
353
|
+
{
|
|
354
|
+
type: "confirm",
|
|
355
|
+
name: "runRlmInit",
|
|
356
|
+
message: "Run 'rlm init' now to choose sandbox/docker/local?",
|
|
357
|
+
initial: false
|
|
245
358
|
}
|
|
246
359
|
]);
|
|
247
|
-
llmProvider =
|
|
248
|
-
llmApiKey =
|
|
360
|
+
answers.llmProvider = runtimeResponse.llmProvider;
|
|
361
|
+
answers.llmApiKey = runtimeResponse.llmApiKey;
|
|
362
|
+
answers.runRlmInit = runtimeResponse.runRlmInit || false;
|
|
249
363
|
}
|
|
364
|
+
return answers;
|
|
365
|
+
}
|
|
366
|
+
function resolveRequestedProfile(options) {
|
|
367
|
+
if ("runtimeOnly" in options && options.runtimeOnly) {
|
|
368
|
+
return "runtime-only";
|
|
369
|
+
}
|
|
370
|
+
if (options.profile && isInstallProfile(options.profile)) {
|
|
371
|
+
return options.profile;
|
|
372
|
+
}
|
|
373
|
+
return "hosted-companion";
|
|
374
|
+
}
|
|
375
|
+
function isInstallProfile(value) {
|
|
376
|
+
return ["hosted-only", "hosted-companion", "full-stack", "runtime-only"].includes(value);
|
|
377
|
+
}
|
|
378
|
+
function installProfileChoices() {
|
|
379
|
+
return [
|
|
380
|
+
{
|
|
381
|
+
title: "Hosted only",
|
|
382
|
+
value: "hosted-only",
|
|
383
|
+
description: "Snipara MCP only, no local companion or runtime"
|
|
384
|
+
},
|
|
385
|
+
{
|
|
386
|
+
title: "Hosted + Companion",
|
|
387
|
+
value: "hosted-companion",
|
|
388
|
+
description: "Recommended default: hosted MCP plus snipara-companion"
|
|
389
|
+
},
|
|
390
|
+
{
|
|
391
|
+
title: "Hosted + Companion + Runtime",
|
|
392
|
+
value: "full-stack",
|
|
393
|
+
description: "Add local runtime execution as well"
|
|
394
|
+
},
|
|
395
|
+
{
|
|
396
|
+
title: "Runtime only",
|
|
397
|
+
value: "runtime-only",
|
|
398
|
+
description: "Local execution only, no hosted API key required"
|
|
399
|
+
}
|
|
400
|
+
];
|
|
401
|
+
}
|
|
402
|
+
function buildAnswersFromProfile(input) {
|
|
403
|
+
const defaults = {
|
|
404
|
+
"hosted-only": {
|
|
405
|
+
installCompanion: false,
|
|
406
|
+
installMcp: true,
|
|
407
|
+
installRuntime: false,
|
|
408
|
+
installHooks: false
|
|
409
|
+
},
|
|
410
|
+
"hosted-companion": {
|
|
411
|
+
installCompanion: true,
|
|
412
|
+
installMcp: true,
|
|
413
|
+
installRuntime: false,
|
|
414
|
+
installHooks: input.aiClient === "claude-code"
|
|
415
|
+
},
|
|
416
|
+
"full-stack": {
|
|
417
|
+
installCompanion: true,
|
|
418
|
+
installMcp: true,
|
|
419
|
+
installRuntime: true,
|
|
420
|
+
installHooks: input.aiClient === "claude-code"
|
|
421
|
+
},
|
|
422
|
+
"runtime-only": {
|
|
423
|
+
installCompanion: false,
|
|
424
|
+
installMcp: false,
|
|
425
|
+
installRuntime: true,
|
|
426
|
+
installHooks: false
|
|
427
|
+
}
|
|
428
|
+
};
|
|
429
|
+
const selected = defaults[input.profile];
|
|
250
430
|
return {
|
|
251
|
-
projectSlug:
|
|
252
|
-
projectId:
|
|
253
|
-
apiKey: apiKey || "YOUR_API_KEY_HERE",
|
|
254
|
-
apiKeyType,
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
431
|
+
projectSlug: input.projectSlug,
|
|
432
|
+
projectId: input.projectId,
|
|
433
|
+
apiKey: input.apiKey || "YOUR_API_KEY_HERE",
|
|
434
|
+
apiKeyType: input.profile === "runtime-only" ? "none" : input.apiKeyType,
|
|
435
|
+
profile: input.profile,
|
|
436
|
+
installCompanion: selected.installCompanion && !input.skipCompanion,
|
|
437
|
+
installMcp: selected.installMcp && !input.skipMcp,
|
|
438
|
+
installRuntime: selected.installRuntime && !input.skipRuntime,
|
|
439
|
+
installHooks: selected.installHooks && !input.skipHooks,
|
|
440
|
+
aiClient: input.aiClient
|
|
261
441
|
};
|
|
262
442
|
}
|
|
263
443
|
async function handleSignup() {
|
|
@@ -266,67 +446,102 @@ async function handleSignup() {
|
|
|
266
446
|
Opening ${signupUrl} in your browser...
|
|
267
447
|
`));
|
|
268
448
|
try {
|
|
269
|
-
const
|
|
270
|
-
|
|
449
|
+
const platform = process.platform;
|
|
450
|
+
const openCommand = platform === "darwin" ? "open" : platform === "win32" ? "start" : "xdg-open";
|
|
451
|
+
execSync(`${openCommand} "${signupUrl}"`, { stdio: "ignore", shell: "/bin/sh" });
|
|
271
452
|
} catch {
|
|
272
|
-
console.log(
|
|
273
|
-
|
|
274
|
-
${signupUrl}
|
|
275
|
-
`)
|
|
276
|
-
);
|
|
453
|
+
console.log(chalk.dim(` Please open ${signupUrl} manually.
|
|
454
|
+
`));
|
|
277
455
|
}
|
|
278
|
-
console.log(
|
|
279
|
-
chalk.dim(
|
|
280
|
-
" After signing up, create a project and copy your API key.\n"
|
|
281
|
-
)
|
|
282
|
-
);
|
|
283
456
|
const response = await prompts({
|
|
284
457
|
type: "text",
|
|
285
|
-
name: "
|
|
286
|
-
message: "Paste your API key
|
|
287
|
-
validate: (
|
|
458
|
+
name: "apiKey",
|
|
459
|
+
message: "Paste your Project API key when ready:",
|
|
460
|
+
validate: (value) => value.startsWith("rlm_") ? true : "API key should start with rlm_"
|
|
288
461
|
});
|
|
289
|
-
return response.
|
|
462
|
+
return response.apiKey;
|
|
290
463
|
}
|
|
291
|
-
async function installPythonPackages(answers) {
|
|
292
|
-
console.log(chalk.bold(
|
|
464
|
+
async function installPythonPackages(answers, upgrade) {
|
|
465
|
+
console.log(chalk.bold(`
|
|
466
|
+
\u{1F4E6} ${upgrade ? "Upgrading" : "Installing"} Python Packages
|
|
467
|
+
`));
|
|
293
468
|
const pipCmd = detectPipCommand();
|
|
294
469
|
if (!pipCmd) {
|
|
295
|
-
console.log(
|
|
296
|
-
chalk.yellow(
|
|
297
|
-
" \u26A0\uFE0F Python pip not found. Please install packages manually:\n"
|
|
298
|
-
)
|
|
299
|
-
);
|
|
470
|
+
console.log(chalk.yellow(" \u26A0\uFE0F Python pip not found. Please install packages manually:\n"));
|
|
300
471
|
if (answers.installMcp) {
|
|
301
|
-
console.log(chalk.gray(
|
|
472
|
+
console.log(chalk.gray(` pip install ${upgrade ? "-U " : ""}${MCP_PACKAGE_NAME}`));
|
|
302
473
|
}
|
|
303
474
|
if (answers.installRuntime) {
|
|
304
|
-
console.log(chalk.gray(
|
|
475
|
+
console.log(chalk.gray(` pip install ${upgrade ? "-U " : ""}${RUNTIME_PACKAGE_NAME}`));
|
|
305
476
|
}
|
|
306
477
|
console.log("");
|
|
307
478
|
return;
|
|
308
479
|
}
|
|
309
480
|
const packages = [];
|
|
310
481
|
if (answers.installMcp) {
|
|
311
|
-
packages.push(
|
|
482
|
+
packages.push(MCP_PACKAGE_NAME);
|
|
312
483
|
}
|
|
313
484
|
if (answers.installRuntime) {
|
|
314
|
-
packages.push(
|
|
485
|
+
packages.push(RUNTIME_PACKAGE_NAME);
|
|
315
486
|
}
|
|
316
487
|
if (packages.length === 0) return;
|
|
317
|
-
const spinner = ora(
|
|
488
|
+
const spinner = ora(
|
|
489
|
+
`${upgrade ? "Upgrading" : "Installing"} ${packages.map(stripExtras).join(", ")}...`
|
|
490
|
+
).start();
|
|
318
491
|
try {
|
|
319
|
-
const installCmd = pipCmd === "uv" ? `uv pip install ${packages.join(" ")}` : `${pipCmd} install ${packages.join(" ")}`;
|
|
492
|
+
const installCmd = pipCmd === "uv" ? `uv pip install ${upgrade ? "--upgrade " : ""}${packages.join(" ")}` : `${pipCmd} install ${upgrade ? "--upgrade " : ""}${packages.join(" ")}`;
|
|
320
493
|
execSync(installCmd, {
|
|
321
494
|
stdio: "pipe",
|
|
322
495
|
encoding: "utf-8"
|
|
323
496
|
});
|
|
324
|
-
spinner.succeed(
|
|
325
|
-
|
|
326
|
-
|
|
497
|
+
spinner.succeed(
|
|
498
|
+
`${upgrade ? "Upgraded" : "Installed"} ${packages.map(stripExtras).join(", ")}`
|
|
499
|
+
);
|
|
500
|
+
} catch {
|
|
501
|
+
spinner.fail(`Failed to ${upgrade ? "upgrade" : "install"} Python packages`);
|
|
327
502
|
console.log(chalk.yellow("\n Install manually with:"));
|
|
328
|
-
console.log(chalk.gray(` pip install ${packages.join(" ")}
|
|
503
|
+
console.log(chalk.gray(` pip install ${upgrade ? "--upgrade " : ""}${packages.join(" ")}
|
|
504
|
+
`));
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
function stripExtras(name) {
|
|
508
|
+
return name.replace(/\[.*\]$/, "");
|
|
509
|
+
}
|
|
510
|
+
function detectNodePackageManager() {
|
|
511
|
+
if (commandExists("pnpm")) return "pnpm";
|
|
512
|
+
if (commandExists("npm")) return "npm";
|
|
513
|
+
return null;
|
|
514
|
+
}
|
|
515
|
+
async function installNodeCompanion(upgrade) {
|
|
516
|
+
console.log(chalk.bold(`
|
|
517
|
+
\u{1F9F0} ${upgrade ? "Upgrading" : "Installing"} Local Companion CLI
|
|
329
518
|
`));
|
|
519
|
+
const packageManager = detectNodePackageManager();
|
|
520
|
+
if (!packageManager) {
|
|
521
|
+
console.log(chalk.yellow(" \u26A0\uFE0F npm/pnpm not found. Please install manually:\n"));
|
|
522
|
+
console.log(chalk.gray(` npm install -g ${COMPANION_PACKAGE_NAME}
|
|
523
|
+
`));
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
const spinner = ora(
|
|
527
|
+
`${upgrade ? "Upgrading" : "Installing"} ${COMPANION_PACKAGE_NAME}...`
|
|
528
|
+
).start();
|
|
529
|
+
try {
|
|
530
|
+
const installCmd = packageManager === "pnpm" ? `pnpm add -g ${upgrade ? "--latest " : ""}${COMPANION_PACKAGE_NAME}` : `npm install -g ${upgrade ? COMPANION_PACKAGE_NAME + "@latest" : COMPANION_PACKAGE_NAME}`;
|
|
531
|
+
execSync(installCmd, {
|
|
532
|
+
stdio: "pipe",
|
|
533
|
+
encoding: "utf-8"
|
|
534
|
+
});
|
|
535
|
+
spinner.succeed(`${upgrade ? "Upgraded" : "Installed"} ${COMPANION_PACKAGE_NAME}`);
|
|
536
|
+
} catch {
|
|
537
|
+
spinner.fail(`Failed to ${upgrade ? "upgrade" : "install"} ${COMPANION_PACKAGE_NAME}`);
|
|
538
|
+
console.log(chalk.yellow("\n Install manually with:"));
|
|
539
|
+
console.log(
|
|
540
|
+
chalk.gray(
|
|
541
|
+
` npm install -g ${upgrade ? COMPANION_PACKAGE_NAME + "@latest" : COMPANION_PACKAGE_NAME}
|
|
542
|
+
`
|
|
543
|
+
)
|
|
544
|
+
);
|
|
330
545
|
}
|
|
331
546
|
}
|
|
332
547
|
async function createMcpConfig(answers) {
|
|
@@ -338,18 +553,18 @@ async function createMcpConfig(answers) {
|
|
|
338
553
|
mcpConfig = JSON.parse(fs.readFileSync(mcpConfigPath, "utf-8"));
|
|
339
554
|
console.log(chalk.dim(" Found existing .mcp.json, merging..."));
|
|
340
555
|
} catch {
|
|
341
|
-
console.log(
|
|
342
|
-
chalk.yellow(" Could not parse existing .mcp.json, creating new...")
|
|
343
|
-
);
|
|
556
|
+
console.log(chalk.yellow(" Could not parse existing .mcp.json, creating new..."));
|
|
344
557
|
}
|
|
345
558
|
}
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
559
|
+
if (answers.apiKeyType !== "none" || answers.installMcp) {
|
|
560
|
+
mcpConfig.mcpServers.snipara = {
|
|
561
|
+
type: "http",
|
|
562
|
+
url: `${API_URL}/mcp/${answers.projectSlug}`,
|
|
563
|
+
headers: {
|
|
564
|
+
"X-API-Key": answers.apiKey || "YOUR_API_KEY_HERE"
|
|
565
|
+
}
|
|
566
|
+
};
|
|
567
|
+
}
|
|
353
568
|
if (answers.installRuntime) {
|
|
354
569
|
mcpConfig.mcpServers["rlm-runtime"] = {
|
|
355
570
|
type: "http",
|
|
@@ -359,26 +574,28 @@ async function createMcpConfig(answers) {
|
|
|
359
574
|
}
|
|
360
575
|
fs.writeFileSync(mcpConfigPath, JSON.stringify(mcpConfig, null, 2) + "\n");
|
|
361
576
|
console.log(chalk.green(" \u2713 Created .mcp.json"));
|
|
577
|
+
ensureGitignoreEntry(".mcp.json", "# Snipara MCP config (contains API key)");
|
|
578
|
+
if (answers.aiClient === "claude-desktop") {
|
|
579
|
+
await createClaudeDesktopConfig(answers);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
function ensureGitignoreEntry(entry, comment) {
|
|
362
583
|
const gitignorePath = path.join(process.cwd(), ".gitignore");
|
|
363
584
|
if (fs.existsSync(gitignorePath)) {
|
|
364
585
|
const gitignore = fs.readFileSync(gitignorePath, "utf-8");
|
|
365
|
-
if (!gitignore.includes(
|
|
366
|
-
fs.appendFileSync(
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
console.log(chalk.green(
|
|
586
|
+
if (!gitignore.includes(entry)) {
|
|
587
|
+
fs.appendFileSync(gitignorePath, `
|
|
588
|
+
${comment}
|
|
589
|
+
${entry}
|
|
590
|
+
`);
|
|
591
|
+
console.log(chalk.green(` \u2713 Added ${entry} to .gitignore`));
|
|
371
592
|
}
|
|
372
|
-
|
|
373
|
-
fs.writeFileSync(
|
|
374
|
-
gitignorePath,
|
|
375
|
-
"# Snipara MCP config (contains API key)\n.mcp.json\n"
|
|
376
|
-
);
|
|
377
|
-
console.log(chalk.green(" \u2713 Created .gitignore with .mcp.json"));
|
|
378
|
-
}
|
|
379
|
-
if (answers.aiClient === "claude-desktop") {
|
|
380
|
-
await createClaudeDesktopConfig(answers);
|
|
593
|
+
return;
|
|
381
594
|
}
|
|
595
|
+
fs.writeFileSync(gitignorePath, `${comment}
|
|
596
|
+
${entry}
|
|
597
|
+
`);
|
|
598
|
+
console.log(chalk.green(` \u2713 Created .gitignore with ${entry}`));
|
|
382
599
|
}
|
|
383
600
|
async function createClaudeDesktopConfig(answers) {
|
|
384
601
|
const configDir = process.platform === "darwin" ? path.join(homedir(), "Library", "Application Support", "Claude") : process.platform === "win32" ? path.join(homedir(), "AppData", "Roaming", "Claude") : path.join(homedir(), ".config", "claude");
|
|
@@ -394,9 +611,9 @@ async function createClaudeDesktopConfig(answers) {
|
|
|
394
611
|
config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
395
612
|
}
|
|
396
613
|
config.mcpServers = config.mcpServers || {};
|
|
397
|
-
config.mcpServers
|
|
614
|
+
config.mcpServers.snipara = {
|
|
398
615
|
command: "uvx",
|
|
399
|
-
args: [
|
|
616
|
+
args: [MCP_PACKAGE_NAME],
|
|
400
617
|
env: {
|
|
401
618
|
SNIPARA_API_KEY: answers.apiKey,
|
|
402
619
|
SNIPARA_PROJECT_SLUG: answers.projectSlug
|
|
@@ -415,9 +632,7 @@ async function createClaudeDesktopConfig(answers) {
|
|
|
415
632
|
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
416
633
|
console.log(chalk.green(" \u2713 Updated Claude Desktop config"));
|
|
417
634
|
} catch (error) {
|
|
418
|
-
console.log(
|
|
419
|
-
chalk.yellow(` \u26A0\uFE0F Could not update Claude Desktop config: ${error}`)
|
|
420
|
-
);
|
|
635
|
+
console.log(chalk.yellow(` \u26A0\uFE0F Could not update Claude Desktop config: ${error}`));
|
|
421
636
|
}
|
|
422
637
|
}
|
|
423
638
|
async function installClaudeCodeHooks(answers) {
|
|
@@ -426,11 +641,8 @@ async function installClaudeCodeHooks(answers) {
|
|
|
426
641
|
const hooksDir = path.join(claudeDir, "hooks");
|
|
427
642
|
if (!fs.existsSync(claudeDir)) {
|
|
428
643
|
console.log(
|
|
429
|
-
chalk.yellow(
|
|
430
|
-
" \u26A0\uFE0F ~/.claude not found. Install Claude Code first, then run:\n"
|
|
431
|
-
)
|
|
644
|
+
chalk.yellow(" \u26A0\uFE0F ~/.claude not found. Install Claude Code first, then rerun repair.\n")
|
|
432
645
|
);
|
|
433
|
-
console.log(chalk.gray(" create-snipara --skip-mcp --skip-runtime\n"));
|
|
434
646
|
return;
|
|
435
647
|
}
|
|
436
648
|
if (!fs.existsSync(hooksDir)) {
|
|
@@ -439,17 +651,14 @@ async function installClaudeCodeHooks(answers) {
|
|
|
439
651
|
const hooks = [
|
|
440
652
|
{
|
|
441
653
|
name: "snipara-startup.sh",
|
|
442
|
-
event: "PreToolUse",
|
|
443
654
|
content: createStartupHook(answers)
|
|
444
655
|
},
|
|
445
656
|
{
|
|
446
657
|
name: "snipara-session.sh",
|
|
447
|
-
event: "PostToolUse",
|
|
448
658
|
content: createSessionHook(answers)
|
|
449
659
|
},
|
|
450
660
|
{
|
|
451
661
|
name: "snipara-compact.sh",
|
|
452
|
-
event: "PreCompact",
|
|
453
662
|
content: createCompactHook(answers)
|
|
454
663
|
}
|
|
455
664
|
];
|
|
@@ -465,6 +674,7 @@ async function installClaudeCodeHooks(answers) {
|
|
|
465
674
|
try {
|
|
466
675
|
settings = JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
|
|
467
676
|
} catch {
|
|
677
|
+
settings = {};
|
|
468
678
|
}
|
|
469
679
|
}
|
|
470
680
|
settings.hooks = settings.hooks || {};
|
|
@@ -488,75 +698,50 @@ async function installClaudeCodeHooks(answers) {
|
|
|
488
698
|
for (const entry of hookEntries) {
|
|
489
699
|
const eventHooks = hooksConfig[entry.event] || [];
|
|
490
700
|
const exists = eventHooks.some(
|
|
491
|
-
(
|
|
701
|
+
(hook) => typeof hook === "object" && hook !== null && "script" in hook && hook.script === entry.script
|
|
492
702
|
);
|
|
493
703
|
if (!exists) {
|
|
494
704
|
eventHooks.push(entry);
|
|
495
705
|
hooksConfig[entry.event] = eventHooks;
|
|
496
706
|
}
|
|
497
707
|
}
|
|
498
|
-
settings.hooks = hooksConfig;
|
|
499
708
|
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
500
709
|
console.log(chalk.green(" \u2713 Updated ~/.claude/settings.json"));
|
|
501
710
|
}
|
|
502
711
|
function createStartupHook(answers) {
|
|
503
|
-
return `#!/bin/bash
|
|
504
|
-
|
|
505
|
-
# Event: PreToolUse (TodoWrite)
|
|
712
|
+
return `#!/usr/bin/env bash
|
|
713
|
+
set -euo pipefail
|
|
506
714
|
|
|
507
|
-
export SNIPARA_API_KEY="${answers.apiKey}"
|
|
508
715
|
export SNIPARA_PROJECT_SLUG="${answers.projectSlug}"
|
|
716
|
+
${answers.projectId ? `export SNIPARA_PROJECT_ID="${answers.projectId}"` : ""}
|
|
509
717
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
export SNIPARA_SESSION_STARTED=1
|
|
513
|
-
|
|
514
|
-
# Use snipara-mcp to recall context
|
|
515
|
-
if command -v snipara-mcp &> /dev/null; then
|
|
516
|
-
snipara-mcp recall --query "session progress recent work" --limit 3 2>/dev/null || true
|
|
517
|
-
fi
|
|
718
|
+
if command -v rlm-hook >/dev/null 2>&1; then
|
|
719
|
+
rlm-hook session-bootstrap || true
|
|
518
720
|
fi
|
|
519
|
-
|
|
520
|
-
exit 0
|
|
521
721
|
`;
|
|
522
722
|
}
|
|
523
723
|
function createSessionHook(answers) {
|
|
524
|
-
return `#!/bin/bash
|
|
525
|
-
|
|
526
|
-
# Event: PostToolUse (Bash)
|
|
724
|
+
return `#!/usr/bin/env bash
|
|
725
|
+
set -euo pipefail
|
|
527
726
|
|
|
528
|
-
export SNIPARA_API_KEY="${answers.apiKey}"
|
|
529
727
|
export SNIPARA_PROJECT_SLUG="${answers.projectSlug}"
|
|
728
|
+
${answers.projectId ? `export SNIPARA_PROJECT_ID="${answers.projectId}"` : ""}
|
|
530
729
|
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
COMMIT_MSG=$(git log -1 --pretty=%B 2>/dev/null || echo "")
|
|
534
|
-
|
|
535
|
-
if [ -n "$COMMIT_MSG" ]; then
|
|
536
|
-
if command -v snipara-mcp &> /dev/null; then
|
|
537
|
-
snipara-mcp remember --type "context" --content "Committed: $COMMIT_MSG" --ttl 7 2>/dev/null || true
|
|
538
|
-
fi
|
|
539
|
-
fi
|
|
730
|
+
if command -v rlm-hook >/dev/null 2>&1; then
|
|
731
|
+
rlm-hook emit-event --event-type tool_result --json '{"source":"claude-code-hook"}' || true
|
|
540
732
|
fi
|
|
541
|
-
|
|
542
|
-
exit 0
|
|
543
733
|
`;
|
|
544
734
|
}
|
|
545
735
|
function createCompactHook(answers) {
|
|
546
|
-
return `#!/bin/bash
|
|
547
|
-
|
|
548
|
-
# Event: PreCompact
|
|
736
|
+
return `#!/usr/bin/env bash
|
|
737
|
+
set -euo pipefail
|
|
549
738
|
|
|
550
|
-
export SNIPARA_API_KEY="${answers.apiKey}"
|
|
551
739
|
export SNIPARA_PROJECT_SLUG="${answers.projectSlug}"
|
|
740
|
+
${answers.projectId ? `export SNIPARA_PROJECT_ID="${answers.projectId}"` : ""}
|
|
552
741
|
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
# Extract key context from conversation (simplified)
|
|
556
|
-
snipara-mcp remember --type "context" --content "Session compacted. Resume point saved." --ttl 7 2>/dev/null || true
|
|
742
|
+
if command -v rlm-hook >/dev/null 2>&1; then
|
|
743
|
+
rlm-hook task-commit --summary "Claude Code compacted local session context" --outcome partial || true
|
|
557
744
|
fi
|
|
558
|
-
|
|
559
|
-
exit 0
|
|
560
745
|
`;
|
|
561
746
|
}
|
|
562
747
|
async function updateEnvFiles(answers) {
|
|
@@ -587,7 +772,7 @@ ${llmVarName}=${answers.llmApiKey}`;
|
|
|
587
772
|
}
|
|
588
773
|
if (fs.existsSync(envExamplePath)) {
|
|
589
774
|
const content = fs.readFileSync(envExamplePath, "utf-8");
|
|
590
|
-
if (!content.includes("SNIPARA_API_KEY")) {
|
|
775
|
+
if (!content.includes("SNIPARA_API_KEY") && !content.includes("SNIPARA_PROJECT_SLUG")) {
|
|
591
776
|
fs.appendFileSync(envExamplePath, `
|
|
592
777
|
|
|
593
778
|
${envVars}
|
|
@@ -608,10 +793,262 @@ ${envVars}
|
|
|
608
793
|
`);
|
|
609
794
|
console.log(chalk.green(` \u2713 Updated ${path.basename(targetEnvPath)}`));
|
|
610
795
|
}
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
if (!answers.apiKey.includes("YOUR_") || answers.llmApiKey) {
|
|
799
|
+
fs.writeFileSync(targetEnvPath, envVars + "\n");
|
|
800
|
+
console.log(chalk.green(` \u2713 Created ${path.basename(targetEnvPath)}`));
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
function getClientGuidance(client) {
|
|
804
|
+
if (client === "claude-code") {
|
|
805
|
+
return [
|
|
806
|
+
"Claude Code can use both the hosted MCP surface and the local rlm-hook companion flows.",
|
|
807
|
+
"If hooks are enabled, the edge layer forwards lifecycle signals without owning durable memory locally."
|
|
808
|
+
];
|
|
809
|
+
}
|
|
810
|
+
if (client === "cursor") {
|
|
811
|
+
return [
|
|
812
|
+
"Cursor reads the generated .mcp.json directly, so the hosted Snipara MCP surface is available after restart.",
|
|
813
|
+
"Use the companion CLI for explicit upload, planning, chunk lookup, doctor, and task commit workflows outside the editor UI."
|
|
814
|
+
];
|
|
815
|
+
}
|
|
816
|
+
if (client === "claude-desktop") {
|
|
817
|
+
return [
|
|
818
|
+
"Claude Desktop uses the generated desktop MCP config plus the same hosted Snipara tool contract.",
|
|
819
|
+
"Keep the companion CLI nearby for bootstrap, upload, and task summaries that are easier to run from a terminal."
|
|
820
|
+
];
|
|
821
|
+
}
|
|
822
|
+
return [
|
|
823
|
+
"The generated pack assumes a hosted-first Snipara setup with optional local companion workflows.",
|
|
824
|
+
"Use the companion CLI whenever your client does not expose an equivalent UX for planning, upload, doctor, or task commit flows."
|
|
825
|
+
];
|
|
826
|
+
}
|
|
827
|
+
function getCompanionCommands(answers) {
|
|
828
|
+
const projectIdFlag = answers.projectId ? ` --project-id ${answers.projectId}` : "";
|
|
829
|
+
const apiKeyFlag = answers.apiKeyType === "none" || answers.apiKey.includes("YOUR_") ? " --api-key <rlm_key>" : ` --api-key ${answers.apiKey}`;
|
|
830
|
+
const clientFlag = getCompanionInitClientFlag(answers.aiClient);
|
|
831
|
+
return [
|
|
832
|
+
{
|
|
833
|
+
title: "Bootstrap local companion",
|
|
834
|
+
command: `rlm-hook init${clientFlag}${projectIdFlag}${apiKeyFlag}`,
|
|
835
|
+
purpose: "Write local companion config for this workstation."
|
|
836
|
+
},
|
|
837
|
+
{
|
|
838
|
+
title: "Run doctor for this project",
|
|
839
|
+
command: "create-snipara doctor",
|
|
840
|
+
purpose: "Inspect local wiring, hosted reachability, and generated assets."
|
|
841
|
+
},
|
|
842
|
+
{
|
|
843
|
+
title: "Repair local setup",
|
|
844
|
+
command: "create-snipara repair",
|
|
845
|
+
purpose: "Rebuild config files, hooks, and the companion pack."
|
|
846
|
+
},
|
|
847
|
+
{
|
|
848
|
+
title: "Run a focused hosted query",
|
|
849
|
+
command: 'rlm-hook query --query "recent auth decisions" --max-tokens 4000',
|
|
850
|
+
purpose: "Pull ranked context before editing or reviewing code."
|
|
851
|
+
},
|
|
852
|
+
{
|
|
853
|
+
title: "Generate a hosted plan",
|
|
854
|
+
command: 'rlm-hook plan --query "Implement webhook retry hardening" --max-tokens 3000',
|
|
855
|
+
purpose: "Produce a structured execution plan against indexed project knowledge."
|
|
856
|
+
},
|
|
857
|
+
{
|
|
858
|
+
title: "Upload or refresh a doc",
|
|
859
|
+
command: "rlm-hook upload --path docs/local-playbook.md --file ./docs/local-playbook.md",
|
|
860
|
+
purpose: "Push local notes or docs into hosted Snipara context."
|
|
861
|
+
},
|
|
862
|
+
{
|
|
863
|
+
title: "Inspect a specific chunk",
|
|
864
|
+
command: "rlm-hook chunk get --chunk-id <chunk_id>",
|
|
865
|
+
purpose: "Open an exact chunk returned by references-based search."
|
|
866
|
+
},
|
|
867
|
+
{
|
|
868
|
+
title: "Bootstrap the session memory view",
|
|
869
|
+
command: "rlm-hook session-bootstrap",
|
|
870
|
+
purpose: "Load critical and daily memories at session start."
|
|
871
|
+
},
|
|
872
|
+
{
|
|
873
|
+
title: "Commit durable task memory",
|
|
874
|
+
command: 'rlm-hook task-commit --summary "Hardened webhook retries and updated tests" --category engineering --outcome completed --files src/api/webhooks.ts tests/webhooks.test.ts',
|
|
875
|
+
purpose: "Persist reusable task outcomes back into hosted memory."
|
|
876
|
+
}
|
|
877
|
+
];
|
|
878
|
+
}
|
|
879
|
+
async function createCompanionPack(answers) {
|
|
880
|
+
console.log(chalk.bold("\n\u{1F5C2}\uFE0F Generating Companion Pack\n"));
|
|
881
|
+
const packDir = path.join(process.cwd(), ".snipara", "companion");
|
|
882
|
+
const commands = getCompanionCommands(answers);
|
|
883
|
+
const guidance = getClientGuidance(answers.aiClient);
|
|
884
|
+
const detection = detectEnvironment();
|
|
885
|
+
fs.mkdirSync(packDir, { recursive: true });
|
|
886
|
+
const readme = `# Snipara Companion Pack
|
|
887
|
+
|
|
888
|
+
This project uses Snipara as a hosted-first context and memory layer with an optional local companion CLI.
|
|
889
|
+
|
|
890
|
+
## Install Profile
|
|
891
|
+
|
|
892
|
+
- Profile: \`${answers.profile}\`
|
|
893
|
+
- Project slug: \`${answers.projectSlug}\`
|
|
894
|
+
- Project id: \`${answers.projectId || "not-set"}\`
|
|
895
|
+
- AI client: \`${answers.aiClient}\`
|
|
896
|
+
- Installed local companion: \`${answers.installCompanion ? "yes" : "no"}\`
|
|
897
|
+
- Installed MCP server: \`${answers.installMcp ? "yes" : "no"}\`
|
|
898
|
+
- Installed runtime: \`${answers.installRuntime ? "yes" : "no"}\`
|
|
899
|
+
- Generated hooks: \`${answers.installHooks ? "yes" : "no"}\`
|
|
900
|
+
|
|
901
|
+
## Local Detection Snapshot
|
|
902
|
+
|
|
903
|
+
- Git repo: \`${detection.inGitRepo ? "yes" : "no"}\`
|
|
904
|
+
- .mcp.json present: \`${detection.files.mcpConfig ? "yes" : "no"}\`
|
|
905
|
+
- .env.local present: \`${detection.files.envLocal ? "yes" : "no"}\`
|
|
906
|
+
- Companion CLI on PATH: \`${detection.tooling.companionCli ? "yes" : "no"}\`
|
|
907
|
+
- Runtime CLI on PATH: \`${detection.tooling.runtimeCli ? "yes" : "no"}\`
|
|
908
|
+
|
|
909
|
+
## Client Guidance
|
|
910
|
+
|
|
911
|
+
${guidance.map((line) => `- ${line}`).join("\n")}
|
|
912
|
+
|
|
913
|
+
## Suggested Commands
|
|
914
|
+
|
|
915
|
+
${commands.map(
|
|
916
|
+
(item) => `### ${item.title}
|
|
917
|
+
|
|
918
|
+
\`${item.command}\`
|
|
919
|
+
|
|
920
|
+
${item.purpose}`
|
|
921
|
+
).join("\n\n")}
|
|
922
|
+
|
|
923
|
+
## Design Rule
|
|
924
|
+
|
|
925
|
+
- Durable memory, policy, and orchestration stay hosted in Snipara.
|
|
926
|
+
- The local companion is for capture, bootstrap, upload, exploration, doctor, repair, and explicit task summaries.
|
|
927
|
+
`;
|
|
928
|
+
const pack = {
|
|
929
|
+
generated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
930
|
+
project_slug: answers.projectSlug,
|
|
931
|
+
project_id: answers.projectId || null,
|
|
932
|
+
ai_client: answers.aiClient,
|
|
933
|
+
profile: answers.profile,
|
|
934
|
+
llm_provider: answers.llmProvider || null,
|
|
935
|
+
commands,
|
|
936
|
+
guidance,
|
|
937
|
+
detection
|
|
938
|
+
};
|
|
939
|
+
fs.writeFileSync(path.join(packDir, "README.md"), readme + "\n");
|
|
940
|
+
fs.writeFileSync(path.join(packDir, "commands.json"), JSON.stringify(pack, null, 2) + "\n");
|
|
941
|
+
console.log(chalk.green(" \u2713 Created .snipara/companion/README.md"));
|
|
942
|
+
console.log(chalk.green(" \u2713 Created .snipara/companion/commands.json"));
|
|
943
|
+
}
|
|
944
|
+
async function runRlmInit() {
|
|
945
|
+
console.log(chalk.bold("\n\u{1F527} Running RLM-Runtime Configuration\n"));
|
|
946
|
+
console.log(
|
|
947
|
+
chalk.dim(" Starting 'rlm init' - follow the prompts to configure execution environment...\n")
|
|
948
|
+
);
|
|
949
|
+
return new Promise((resolve) => {
|
|
950
|
+
const child = spawn("rlm", ["init"], {
|
|
951
|
+
stdio: "inherit",
|
|
952
|
+
shell: true
|
|
953
|
+
});
|
|
954
|
+
child.on("close", (code) => {
|
|
955
|
+
if (code === 0) {
|
|
956
|
+
console.log(chalk.green("\n \u2713 RLM-Runtime configured successfully"));
|
|
957
|
+
} else if (code !== null) {
|
|
958
|
+
console.log(chalk.yellow(`
|
|
959
|
+
\u26A0\uFE0F rlm init exited with code ${code}`));
|
|
960
|
+
console.log(chalk.dim(" You can run 'rlm init' manually later."));
|
|
961
|
+
}
|
|
962
|
+
resolve();
|
|
963
|
+
});
|
|
964
|
+
child.on("error", (error) => {
|
|
965
|
+
console.log(chalk.yellow(`
|
|
966
|
+
\u26A0\uFE0F Could not run rlm init: ${error.message}`));
|
|
967
|
+
console.log(chalk.dim(" Run 'rlm init' manually after installation completes."));
|
|
968
|
+
resolve();
|
|
969
|
+
});
|
|
970
|
+
});
|
|
971
|
+
}
|
|
972
|
+
async function runValidation(answers, includeHostedCheck) {
|
|
973
|
+
console.log(chalk.bold("\n\u{1FA7A} Running Validation\n"));
|
|
974
|
+
const checks = [];
|
|
975
|
+
const detection = detectEnvironment();
|
|
976
|
+
checks.push({
|
|
977
|
+
name: "project_slug",
|
|
978
|
+
status: answers.projectSlug ? "pass" : "fail",
|
|
979
|
+
detail: answers.projectSlug || "Project slug missing"
|
|
980
|
+
});
|
|
981
|
+
checks.push({
|
|
982
|
+
name: "mcp_config",
|
|
983
|
+
status: detection.files.mcpConfig ? "pass" : "warn",
|
|
984
|
+
detail: detection.files.mcpConfig ? ".mcp.json present" : ".mcp.json missing"
|
|
985
|
+
});
|
|
986
|
+
checks.push({
|
|
987
|
+
name: "companion_cli",
|
|
988
|
+
status: answers.installCompanion ? detection.tooling.companionCli ? "pass" : "fail" : "pass",
|
|
989
|
+
detail: answers.installCompanion && !detection.tooling.companionCli ? `${COMPANION_PACKAGE_NAME} not found on PATH` : "Companion CLI ready or not requested"
|
|
990
|
+
});
|
|
991
|
+
checks.push({
|
|
992
|
+
name: "runtime_cli",
|
|
993
|
+
status: answers.installRuntime ? detection.tooling.runtimeCli ? "pass" : "warn" : "pass",
|
|
994
|
+
detail: answers.installRuntime && !detection.tooling.runtimeCli ? "rlm CLI not found on PATH" : "Runtime CLI ready or not requested"
|
|
995
|
+
});
|
|
996
|
+
checks.push({
|
|
997
|
+
name: "companion_pack",
|
|
998
|
+
status: detection.files.sniparaCompanionPack ? "pass" : "warn",
|
|
999
|
+
detail: detection.files.sniparaCompanionPack ? ".snipara/companion present" : ".snipara/companion missing"
|
|
1000
|
+
});
|
|
1001
|
+
checks.push({
|
|
1002
|
+
name: "env_files",
|
|
1003
|
+
status: detection.files.env || detection.files.envLocal || detection.files.envExample ? "pass" : "warn",
|
|
1004
|
+
detail: detection.files.env || detection.files.envLocal || detection.files.envExample ? "Environment file present" : "No environment file found"
|
|
1005
|
+
});
|
|
1006
|
+
checks.push({
|
|
1007
|
+
name: "hooks",
|
|
1008
|
+
status: answers.installHooks && answers.aiClient === "claude-code" ? detection.files.claudeDir ? "pass" : "warn" : "pass",
|
|
1009
|
+
detail: answers.installHooks && answers.aiClient === "claude-code" ? detection.files.claudeDir ? "~/.claude detected" : "~/.claude missing" : "Hooks not requested"
|
|
1010
|
+
});
|
|
1011
|
+
if (includeHostedCheck && answers.apiKeyType !== "none" && !answers.apiKey.includes("YOUR_")) {
|
|
1012
|
+
checks.push(await testConnection(answers));
|
|
1013
|
+
} else if (includeHostedCheck) {
|
|
1014
|
+
checks.push({
|
|
1015
|
+
name: "hosted_connection",
|
|
1016
|
+
status: "warn",
|
|
1017
|
+
detail: "Skipped hosted connectivity test because no live API key was available"
|
|
1018
|
+
});
|
|
1019
|
+
}
|
|
1020
|
+
for (const check of checks) {
|
|
1021
|
+
const symbol = check.status === "pass" ? chalk.green("\u2713") : check.status === "warn" ? chalk.yellow("!") : chalk.red("\u2717");
|
|
1022
|
+
console.log(` ${symbol} ${check.name}: ${check.detail}`);
|
|
1023
|
+
}
|
|
1024
|
+
return {
|
|
1025
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1026
|
+
profile: answers.profile,
|
|
1027
|
+
checks
|
|
1028
|
+
};
|
|
1029
|
+
}
|
|
1030
|
+
function writeDoctorReport(report) {
|
|
1031
|
+
const reportDir = path.join(process.cwd(), ".snipara", "companion");
|
|
1032
|
+
fs.mkdirSync(reportDir, { recursive: true });
|
|
1033
|
+
fs.writeFileSync(path.join(reportDir, "doctor.json"), JSON.stringify(report, null, 2) + "\n");
|
|
1034
|
+
console.log(chalk.green(" \u2713 Wrote .snipara/companion/doctor.json"));
|
|
1035
|
+
}
|
|
1036
|
+
function printDoctorReport(detection, report) {
|
|
1037
|
+
console.log(chalk.bold("\n\u{1FA7A} Snipara Doctor\n"));
|
|
1038
|
+
console.log(
|
|
1039
|
+
` Project slug: ${detection.existingConfig.slug || detection.projectSlug || "unknown"}`
|
|
1040
|
+
);
|
|
1041
|
+
console.log(` AI client: ${detection.aiClient}`);
|
|
1042
|
+
console.log(` In git repo: ${detection.inGitRepo ? "yes" : "no"}`);
|
|
1043
|
+
console.log(` Profile: ${report.profile}`);
|
|
1044
|
+
console.log("");
|
|
1045
|
+
for (const check of report.checks) {
|
|
1046
|
+
const symbol = check.status === "pass" ? chalk.green("\u2713") : check.status === "warn" ? chalk.yellow("!") : chalk.red("\u2717");
|
|
1047
|
+
console.log(` ${symbol} ${check.name}: ${check.detail}`);
|
|
611
1048
|
}
|
|
1049
|
+
console.log("");
|
|
612
1050
|
}
|
|
613
1051
|
async function testConnection(answers) {
|
|
614
|
-
console.log(chalk.bold("\n\u{1F50C} Testing Connection\n"));
|
|
615
1052
|
const spinner = ora("Connecting to Snipara API...").start();
|
|
616
1053
|
try {
|
|
617
1054
|
const response = await fetch(`${API_URL}/mcp/${answers.projectSlug}`, {
|
|
@@ -631,22 +1068,29 @@ async function testConnection(answers) {
|
|
|
631
1068
|
const data = await response.json();
|
|
632
1069
|
const toolCount = data.result?.tools?.length || 0;
|
|
633
1070
|
spinner.succeed(`Connected! ${toolCount} MCP tools available.`);
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
1071
|
+
return {
|
|
1072
|
+
name: "hosted_connection",
|
|
1073
|
+
status: "pass",
|
|
1074
|
+
detail: `Connected successfully (${toolCount} tools reported)`
|
|
1075
|
+
};
|
|
637
1076
|
}
|
|
1077
|
+
const errorText = await response.text();
|
|
1078
|
+
spinner.warn(`Connection returned ${response.status}`);
|
|
1079
|
+
return {
|
|
1080
|
+
name: "hosted_connection",
|
|
1081
|
+
status: "warn",
|
|
1082
|
+
detail: `HTTP ${response.status}: ${errorText.slice(0, 120)}`
|
|
1083
|
+
};
|
|
638
1084
|
} catch (error) {
|
|
639
1085
|
spinner.warn(`Could not connect: ${error}`);
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
console.log(chalk.dim(` Create your project at: ${BASE_URL}/dashboard
|
|
646
|
-
`));
|
|
1086
|
+
return {
|
|
1087
|
+
name: "hosted_connection",
|
|
1088
|
+
status: "warn",
|
|
1089
|
+
detail: "Could not reach Snipara API from this machine"
|
|
1090
|
+
};
|
|
647
1091
|
}
|
|
648
1092
|
}
|
|
649
|
-
function printSuccess(answers) {
|
|
1093
|
+
function printSuccess(answers, report) {
|
|
650
1094
|
console.log(
|
|
651
1095
|
chalk.green(`
|
|
652
1096
|
\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557
|
|
@@ -656,66 +1100,153 @@ function printSuccess(answers) {
|
|
|
656
1100
|
\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D
|
|
657
1101
|
`)
|
|
658
1102
|
);
|
|
659
|
-
console.log(chalk.bold("
|
|
1103
|
+
console.log(chalk.bold("Installed profile:\n"));
|
|
1104
|
+
console.log(` ${chalk.green("\u2713")} ${answers.profile}`);
|
|
1105
|
+
console.log(chalk.bold("\nWhat was installed:\n"));
|
|
660
1106
|
if (answers.installMcp) {
|
|
661
|
-
console.log(chalk.green(" \u2713 snipara-mcp") + chalk.dim(" - MCP server
|
|
1107
|
+
console.log(chalk.green(" \u2713 snipara-mcp") + chalk.dim(" - Hosted MCP server wiring"));
|
|
662
1108
|
}
|
|
663
1109
|
if (answers.installRuntime) {
|
|
664
|
-
console.log(chalk.green(" \u2713 rlm-runtime") + chalk.dim(" - Safe code execution
|
|
1110
|
+
console.log(chalk.green(" \u2713 rlm-runtime") + chalk.dim(" - Safe code execution"));
|
|
1111
|
+
}
|
|
1112
|
+
if (answers.installCompanion) {
|
|
1113
|
+
console.log(
|
|
1114
|
+
chalk.green(` \u2713 ${COMPANION_PACKAGE_NAME}`) + chalk.dim(" - Local companion workflows and hook bootstrap")
|
|
1115
|
+
);
|
|
665
1116
|
}
|
|
666
1117
|
if (answers.installHooks) {
|
|
667
|
-
console.log(chalk.green(" \u2713
|
|
1118
|
+
console.log(chalk.green(" \u2713 hooks") + chalk.dim(" - Local session lifecycle scripts"));
|
|
668
1119
|
}
|
|
669
|
-
|
|
670
|
-
|
|
1120
|
+
console.log(chalk.green(" \u2713 .snipara/companion") + chalk.dim(" - Local pack + doctor report"));
|
|
1121
|
+
const failedChecks = report.checks.filter((check) => check.status === "fail");
|
|
1122
|
+
const warnedChecks = report.checks.filter((check) => check.status === "warn");
|
|
1123
|
+
console.log(chalk.bold("\nNext steps:\n"));
|
|
1124
|
+
console.log(" 1. Restart your AI client if you installed or changed MCP config.");
|
|
1125
|
+
console.log(" 2. Open .snipara/companion/README.md for local commands and repair flow.");
|
|
1126
|
+
console.log(" 3. Run create-snipara doctor anytime you want a fresh local validation report.");
|
|
1127
|
+
if (answers.installCompanion) {
|
|
1128
|
+
console.log(
|
|
1129
|
+
" 4. Use rlm-hook query / plan / upload / task-commit for explicit local workflows."
|
|
1130
|
+
);
|
|
671
1131
|
}
|
|
672
|
-
if (answers.
|
|
673
|
-
console.log(
|
|
674
|
-
} else if (answers.apiKeyType === "project") {
|
|
675
|
-
console.log(chalk.dim(`
|
|
676
|
-
\u{1F511} Using Project API key for: ${answers.projectSlug}`));
|
|
1132
|
+
if (answers.installRuntime && !answers.runRlmInit) {
|
|
1133
|
+
console.log(" 5. Run rlm init when you want to choose sandbox/docker/local execution.");
|
|
677
1134
|
}
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
console.log(chalk.dim("
|
|
685
|
-
} else if (answers.apiKey.includes("YOUR_") || answers.apiKeyType === "none") {
|
|
686
|
-
console.log(` 1. Get your API key at ${chalk.cyan(`${BASE_URL}/signup`)}`);
|
|
687
|
-
console.log(" 2. Update SNIPARA_API_KEY in .mcp.json");
|
|
688
|
-
console.log(" 3. Restart your AI client");
|
|
1135
|
+
if (failedChecks.length > 0 || warnedChecks.length > 0) {
|
|
1136
|
+
console.log(chalk.bold("\nValidation summary:\n"));
|
|
1137
|
+
for (const check of [...failedChecks, ...warnedChecks]) {
|
|
1138
|
+
const symbol = check.status === "fail" ? chalk.red("\u2717") : chalk.yellow("!");
|
|
1139
|
+
console.log(` ${symbol} ${check.name}: ${check.detail}`);
|
|
1140
|
+
}
|
|
1141
|
+
console.log(chalk.dim("\n Run create-snipara repair to rebuild local wiring.\n"));
|
|
689
1142
|
} else {
|
|
690
|
-
console.log(
|
|
691
|
-
|
|
692
|
-
|
|
1143
|
+
console.log(
|
|
1144
|
+
chalk.dim(
|
|
1145
|
+
"\n Validation passed cleanly. Doctor report saved in .snipara/companion/doctor.json\n"
|
|
1146
|
+
)
|
|
1147
|
+
);
|
|
693
1148
|
}
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
console.log(chalk.dim("\n CLI commands (not configured - set LLM API key later if needed):"));
|
|
708
|
-
console.log(chalk.dim(" \u2022 rlm run - Requires OPENAI_API_KEY or ANTHROPIC_API_KEY"));
|
|
709
|
-
console.log(chalk.dim(" \u2022 rlm agent - Requires OPENAI_API_KEY or ANTHROPIC_API_KEY"));
|
|
1149
|
+
console.log(chalk.dim(`Docs: ${BASE_URL}/docs`));
|
|
1150
|
+
console.log(chalk.dim("Support: hello@starbox-group.com\n"));
|
|
1151
|
+
}
|
|
1152
|
+
function commandExists(command) {
|
|
1153
|
+
try {
|
|
1154
|
+
execSync(`${command} --version`, { stdio: "ignore", shell: "/bin/sh" });
|
|
1155
|
+
return true;
|
|
1156
|
+
} catch {
|
|
1157
|
+
try {
|
|
1158
|
+
execSync(`command -v ${command}`, { stdio: "ignore", shell: "/bin/sh" });
|
|
1159
|
+
return true;
|
|
1160
|
+
} catch {
|
|
1161
|
+
return false;
|
|
710
1162
|
}
|
|
711
1163
|
}
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
1164
|
+
}
|
|
1165
|
+
function detectEnvironment() {
|
|
1166
|
+
const existingConfig = getExistingConfig();
|
|
1167
|
+
return {
|
|
1168
|
+
cwd: process.cwd(),
|
|
1169
|
+
projectSlug: detectProjectSlug(),
|
|
1170
|
+
aiClient: detectAiClient(),
|
|
1171
|
+
inGitRepo: isGitRepo(),
|
|
1172
|
+
files: {
|
|
1173
|
+
mcpConfig: fs.existsSync(path.join(process.cwd(), ".mcp.json")),
|
|
1174
|
+
env: fs.existsSync(path.join(process.cwd(), ".env")),
|
|
1175
|
+
envLocal: fs.existsSync(path.join(process.cwd(), ".env.local")),
|
|
1176
|
+
envExample: fs.existsSync(path.join(process.cwd(), ".env.example")),
|
|
1177
|
+
claudeDir: fs.existsSync(path.join(homedir(), ".claude")),
|
|
1178
|
+
cursorDir: fs.existsSync(path.join(homedir(), ".cursor")),
|
|
1179
|
+
sniparaCompanionPack: fs.existsSync(path.join(process.cwd(), ".snipara", "companion"))
|
|
1180
|
+
},
|
|
1181
|
+
tooling: {
|
|
1182
|
+
node: commandExists("node"),
|
|
1183
|
+
npm: commandExists("npm"),
|
|
1184
|
+
pnpm: commandExists("pnpm"),
|
|
1185
|
+
python: commandExists("python3") || commandExists("python"),
|
|
1186
|
+
pip: commandExists("pip3") || commandExists("pip"),
|
|
1187
|
+
uv: commandExists("uv"),
|
|
1188
|
+
docker: commandExists("docker"),
|
|
1189
|
+
git: commandExists("git"),
|
|
1190
|
+
companionCli: commandExists("rlm-hook"),
|
|
1191
|
+
runtimeCli: commandExists("rlm")
|
|
1192
|
+
},
|
|
1193
|
+
existingConfig: {
|
|
1194
|
+
slug: existingConfig?.slug,
|
|
1195
|
+
projectId: existingConfig?.projectId,
|
|
1196
|
+
hasSniparaServer: existingConfig?.hasSniparaServer ?? false,
|
|
1197
|
+
hasRuntimeServer: existingConfig?.hasRuntimeServer ?? false
|
|
1198
|
+
}
|
|
1199
|
+
};
|
|
1200
|
+
}
|
|
1201
|
+
function loadExistingAnswers(options) {
|
|
1202
|
+
const detection = detectEnvironment();
|
|
1203
|
+
const effectiveProfile = resolveMaintenanceProfile(detection, options.profile);
|
|
1204
|
+
const apiKey = options.teamKey || options.apiKey || readApiKeyFromEnvFiles() || "YOUR_API_KEY_HERE";
|
|
1205
|
+
const apiKeyType = options.teamKey ? "team" : options.apiKey || readApiKeyFromEnvFiles() ? "project" : effectiveProfile === "runtime-only" ? "none" : "none";
|
|
1206
|
+
return buildAnswersFromProfile({
|
|
1207
|
+
projectSlug: options.slug || detection.existingConfig.slug || detection.projectSlug || "my-project",
|
|
1208
|
+
projectId: options.projectId || detection.existingConfig.projectId || "",
|
|
1209
|
+
apiKey,
|
|
1210
|
+
apiKeyType,
|
|
1211
|
+
profile: effectiveProfile,
|
|
1212
|
+
aiClient: detection.aiClient,
|
|
1213
|
+
skipCompanion: options.withCompanion === true ? false : !detection.tooling.companionCli && effectiveProfile !== "hosted-companion" && effectiveProfile !== "full-stack",
|
|
1214
|
+
skipMcp: options.withMcp === true ? false : !detection.existingConfig.hasSniparaServer && effectiveProfile === "runtime-only",
|
|
1215
|
+
skipRuntime: options.withRuntime === true ? false : !detection.existingConfig.hasRuntimeServer && effectiveProfile !== "full-stack" && effectiveProfile !== "runtime-only",
|
|
1216
|
+
skipHooks: options.withHooks ? false : detection.aiClient !== "claude-code"
|
|
1217
|
+
});
|
|
1218
|
+
}
|
|
1219
|
+
function resolveMaintenanceProfile(detection, explicitProfile) {
|
|
1220
|
+
if (explicitProfile && isInstallProfile(explicitProfile)) {
|
|
1221
|
+
return explicitProfile;
|
|
1222
|
+
}
|
|
1223
|
+
if (detection.existingConfig.hasRuntimeServer && detection.existingConfig.hasSniparaServer) {
|
|
1224
|
+
return detection.tooling.companionCli ? "full-stack" : "full-stack";
|
|
1225
|
+
}
|
|
1226
|
+
if (detection.existingConfig.hasSniparaServer && detection.tooling.companionCli) {
|
|
1227
|
+
return "hosted-companion";
|
|
1228
|
+
}
|
|
1229
|
+
if (detection.existingConfig.hasSniparaServer) {
|
|
1230
|
+
return "hosted-only";
|
|
1231
|
+
}
|
|
1232
|
+
return detection.tooling.runtimeCli ? "runtime-only" : "hosted-companion";
|
|
1233
|
+
}
|
|
1234
|
+
function readApiKeyFromEnvFiles() {
|
|
1235
|
+
const candidates = [".env.local", ".env"];
|
|
1236
|
+
for (const file of candidates) {
|
|
1237
|
+
const filePath = path.join(process.cwd(), file);
|
|
1238
|
+
if (!fs.existsSync(filePath)) continue;
|
|
1239
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
1240
|
+
const match = content.match(/^SNIPARA_API_KEY=(.+)$/m);
|
|
1241
|
+
if (match?.[1]) {
|
|
1242
|
+
return match[1].trim();
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
return null;
|
|
715
1246
|
}
|
|
716
1247
|
function detectProjectSlug() {
|
|
717
1248
|
try {
|
|
718
|
-
const remote = execSync("git remote get-url origin
|
|
1249
|
+
const remote = execSync("git remote get-url origin", {
|
|
719
1250
|
encoding: "utf-8"
|
|
720
1251
|
}).trim();
|
|
721
1252
|
const match = remote.match(/[/:]([^/]+?)(?:\.git)?$/);
|
|
@@ -726,6 +1257,16 @@ function detectProjectSlug() {
|
|
|
726
1257
|
}
|
|
727
1258
|
return path.basename(process.cwd()).toLowerCase().replace(/[^a-z0-9-]/g, "-");
|
|
728
1259
|
}
|
|
1260
|
+
function isGitRepo() {
|
|
1261
|
+
try {
|
|
1262
|
+
execSync("git rev-parse --is-inside-work-tree", {
|
|
1263
|
+
stdio: "ignore"
|
|
1264
|
+
});
|
|
1265
|
+
return true;
|
|
1266
|
+
} catch {
|
|
1267
|
+
return false;
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
729
1270
|
function detectAiClient() {
|
|
730
1271
|
if (fs.existsSync(path.join(homedir(), ".claude")) || process.env.CLAUDE_CODE) {
|
|
731
1272
|
return "claude-code";
|
|
@@ -733,7 +1274,7 @@ function detectAiClient() {
|
|
|
733
1274
|
if (fs.existsSync(path.join(homedir(), ".cursor")) || process.env.CURSOR_EDITOR) {
|
|
734
1275
|
return "cursor";
|
|
735
1276
|
}
|
|
736
|
-
const claudeDesktopPath = process.platform === "darwin" ? path.join(homedir(), "Library", "Application Support", "Claude") : path.join(homedir(), "AppData", "Roaming", "Claude");
|
|
1277
|
+
const claudeDesktopPath = process.platform === "darwin" ? path.join(homedir(), "Library", "Application Support", "Claude") : process.platform === "win32" ? path.join(homedir(), "AppData", "Roaming", "Claude") : path.join(homedir(), ".config", "claude");
|
|
737
1278
|
if (fs.existsSync(claudeDesktopPath)) {
|
|
738
1279
|
return "claude-desktop";
|
|
739
1280
|
}
|
|
@@ -741,33 +1282,37 @@ function detectAiClient() {
|
|
|
741
1282
|
}
|
|
742
1283
|
function detectPipCommand() {
|
|
743
1284
|
const commands = ["uv", "pip3", "pip"];
|
|
744
|
-
for (const
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
return cmd;
|
|
748
|
-
} catch {
|
|
1285
|
+
for (const command of commands) {
|
|
1286
|
+
if (commandExists(command)) {
|
|
1287
|
+
return command;
|
|
749
1288
|
}
|
|
750
1289
|
}
|
|
751
1290
|
return null;
|
|
752
1291
|
}
|
|
753
1292
|
function getExistingConfig() {
|
|
754
1293
|
const mcpConfigPath = path.join(process.cwd(), ".mcp.json");
|
|
755
|
-
if (fs.existsSync(mcpConfigPath)) {
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
}
|
|
1294
|
+
if (!fs.existsSync(mcpConfigPath)) {
|
|
1295
|
+
return null;
|
|
1296
|
+
}
|
|
1297
|
+
try {
|
|
1298
|
+
const config = JSON.parse(fs.readFileSync(mcpConfigPath, "utf-8"));
|
|
1299
|
+
const sniparaServer = config.mcpServers?.snipara;
|
|
1300
|
+
const runtimeServer = config.mcpServers?.["rlm-runtime"];
|
|
1301
|
+
const match = sniparaServer?.url?.match(/\/mcp\/([^/]+)$/);
|
|
1302
|
+
return {
|
|
1303
|
+
slug: match?.[1],
|
|
1304
|
+
projectId: "",
|
|
1305
|
+
hasSniparaServer: Boolean(sniparaServer),
|
|
1306
|
+
hasRuntimeServer: Boolean(runtimeServer)
|
|
1307
|
+
};
|
|
1308
|
+
} catch {
|
|
1309
|
+
return {
|
|
1310
|
+
slug: void 0,
|
|
1311
|
+
projectId: "",
|
|
1312
|
+
hasSniparaServer: false,
|
|
1313
|
+
hasRuntimeServer: false
|
|
1314
|
+
};
|
|
769
1315
|
}
|
|
770
|
-
return null;
|
|
771
1316
|
}
|
|
772
1317
|
main().catch((error) => {
|
|
773
1318
|
console.error(chalk.red("Error:"), error.message);
|