@rvry/mcp 0.1.4 → 0.1.6
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/commands/deepthink.md +56 -16
- package/commands/problem-solve.md +56 -16
- package/dist/index.d.ts +1 -1
- package/dist/index.js +17 -104
- package/dist/local-writer.d.ts +30 -0
- package/dist/local-writer.js +114 -0
- package/dist/setup.js +305 -126
- package/package.json +2 -2
package/commands/deepthink.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
description: Deep structured analysis for high-stakes questions via RVRY
|
|
3
3
|
argument-hint: [--auto] <question>
|
|
4
4
|
allowed-tools:
|
|
5
|
-
-
|
|
5
|
+
- mcp__rvry-dev__deepthink
|
|
6
6
|
- AskUserQuestion
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -17,38 +17,78 @@ allowed-tools:
|
|
|
17
17
|
|
|
18
18
|
## Phase 1: Start Session
|
|
19
19
|
|
|
20
|
-
Call `
|
|
20
|
+
Call `mcp__rvry-dev__deepthink` with `{ "input": "<cleaned input>", "skipScoping": <true if AUTO_MODE> }`.
|
|
21
21
|
|
|
22
22
|
### If status is "scoping" (and not AUTO_MODE):
|
|
23
23
|
|
|
24
24
|
The engine has returned scoping questions to help calibrate the analysis.
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
- Present the question and its options to the user via `AskUserQuestion`
|
|
28
|
-
- Format as: the question text, followed by the options (label + description)
|
|
26
|
+
**IMPORTANT: You MUST call AskUserQuestion as a tool to collect answers. Do NOT render the questions as plain text — the user's answers will be lost.**
|
|
29
27
|
|
|
30
|
-
|
|
28
|
+
Call `AskUserQuestion` with the scoping questions mapped to its format. Each `scopingQuestions` entry becomes a question:
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
AskUserQuestion({
|
|
32
|
+
questions: scopingQuestions.map(sq => ({
|
|
33
|
+
question: sq.question,
|
|
34
|
+
header: sq.question.slice(0, 12), // short label
|
|
35
|
+
options: sq.options.map(o => ({ label: o.label, description: o.description })),
|
|
36
|
+
multiSelect: false
|
|
37
|
+
}))
|
|
38
|
+
})
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
After collecting all answers from AskUserQuestion, format them as a brief context summary (e.g., "Stakes: High. Looking for: risks and failure modes.") and call `mcp__rvry-dev__deepthink` with `{ "input": "<formatted scoping answers>", "sessionId": "<sessionId from previous response>" }`.
|
|
31
42
|
|
|
32
43
|
### If status is "active":
|
|
33
44
|
|
|
34
45
|
Proceed to Phase 2.
|
|
35
46
|
|
|
47
|
+
|
|
36
48
|
## Phase 2: Analysis Loop
|
|
37
49
|
|
|
38
50
|
Repeat until `status === "complete"`:
|
|
39
51
|
|
|
40
|
-
1. Read the engine's `prompt`
|
|
41
|
-
2.
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
52
|
+
1. Read the engine's `prompt` and `instruction` — these guide YOUR analysis. **Do not show either to the user.**
|
|
53
|
+
2. Show the user a brief status line:
|
|
54
|
+
- Format: `Round {round} — {what you're doing this round}`
|
|
55
|
+
- Example: `Round 3 — stress-testing the current position`
|
|
56
|
+
- Derive the description from the engine's phase indicator or your own summary of the round's focus. One line only.
|
|
57
|
+
3. Perform your analysis in your internal reasoning. The engine's prompt contains analytical framings, self-check questions, and constraint tracking — use all of these to guide your thinking, but they are YOUR instructions, not user output.
|
|
58
|
+
4. In your internal reasoning, end your analysis with the constraint update block the engine expects:
|
|
59
|
+
```
|
|
60
|
+
Constraint Updates
|
|
61
|
+
RESOLVE: C1, C2
|
|
62
|
+
ACKNOWLEDGE: C3
|
|
63
|
+
DEFER: C4 | <reason>
|
|
64
|
+
MILESTONE: <milestone> | <evidence>
|
|
65
|
+
End Constraint Updates
|
|
66
|
+
```
|
|
67
|
+
5. Call the engine tool with your full analysis (including constraint updates) as the `input`. **The full analysis text goes ONLY in the tool call, never in visible output.**
|
|
68
|
+
|
|
69
|
+
### What NEVER appears in user-visible output:
|
|
70
|
+
- The engine's `prompt` field content
|
|
71
|
+
- Constraint tables (`C1 [ACTIVE] FORWARD: ...`)
|
|
72
|
+
- Constraint update blocks (`RESOLVE: C1`, `MILESTONE: ...`)
|
|
73
|
+
- Self-check questions or responses
|
|
74
|
+
- Gate verdicts (`SOFT_FAIL`, `HARD_FAIL`, `PASS`)
|
|
75
|
+
- Milestone markers
|
|
76
|
+
- Protocol surface elements (session awareness text, phase indicators from engine)
|
|
77
|
+
|
|
78
|
+
### What the user sees per round:
|
|
79
|
+
- The one-line status (step 2 above)
|
|
80
|
+
- Nothing else until harvest
|
|
45
81
|
|
|
46
82
|
## Phase 3: Harvest
|
|
47
83
|
|
|
48
|
-
When `status === "complete"
|
|
84
|
+
When `status === "complete"`:
|
|
85
|
+
|
|
86
|
+
Synthesize the analysis for the user based on your accumulated reasoning across all rounds. Do NOT simply echo the engine's `harvest` fields — they are structured data, not a finished presentation.
|
|
49
87
|
|
|
50
88
|
Present to the user:
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
89
|
+
1. **Default Starting Point**: What the obvious answer was and why it was insufficient
|
|
90
|
+
2. **Key Findings**: The non-obvious insights that emerged, as a bullet list. Each finding should be a substantive sentence, not a label.
|
|
91
|
+
3. **Open Questions**: What remains genuinely uncertain (if any)
|
|
92
|
+
4. **Suggested Next Steps**: Concrete follow-up actions or investigations
|
|
93
|
+
|
|
94
|
+
Use the engine's `harvest.summary`, `harvest.keyFindings`, `harvest.openQuestions`, and `harvest.followUps` as source material, but write the synthesis in your own words with the depth your analysis produced. The harvest fields are often terse — your synthesis should reflect the full depth of the multi-round analysis.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
description: Structured decision-making via RVRY
|
|
3
3
|
argument-hint: [--auto] <problem or decision>
|
|
4
4
|
allowed-tools:
|
|
5
|
-
-
|
|
5
|
+
- mcp__rvry-dev__problem_solve
|
|
6
6
|
- AskUserQuestion
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -17,38 +17,78 @@ allowed-tools:
|
|
|
17
17
|
|
|
18
18
|
## Phase 1: Start Session
|
|
19
19
|
|
|
20
|
-
Call `
|
|
20
|
+
Call `mcp__rvry-dev__problem_solve` with `{ "input": "<cleaned input>", "skipScoping": <true if AUTO_MODE> }`.
|
|
21
21
|
|
|
22
22
|
### If status is "scoping" (and not AUTO_MODE):
|
|
23
23
|
|
|
24
24
|
The engine has returned scoping questions to help calibrate the analysis.
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
- Present the question and its options to the user via `AskUserQuestion`
|
|
28
|
-
- Format as: the question text, followed by the options (label + description)
|
|
26
|
+
**IMPORTANT: You MUST call AskUserQuestion as a tool to collect answers. Do NOT render the questions as plain text — the user's answers will be lost.**
|
|
29
27
|
|
|
30
|
-
|
|
28
|
+
Call `AskUserQuestion` with the scoping questions mapped to its format. Each `scopingQuestions` entry becomes a question:
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
AskUserQuestion({
|
|
32
|
+
questions: scopingQuestions.map(sq => ({
|
|
33
|
+
question: sq.question,
|
|
34
|
+
header: sq.question.slice(0, 12), // short label
|
|
35
|
+
options: sq.options.map(o => ({ label: o.label, description: o.description })),
|
|
36
|
+
multiSelect: false
|
|
37
|
+
}))
|
|
38
|
+
})
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
After collecting all answers from AskUserQuestion, format them as a brief context summary (e.g., "Options: multiple, need to narrow. Priority: risk minimization.") and call `mcp__rvry-dev__problem_solve` with `{ "input": "<formatted scoping answers>", "sessionId": "<sessionId from previous response>" }`.
|
|
31
42
|
|
|
32
43
|
### If status is "active":
|
|
33
44
|
|
|
34
45
|
Proceed to Phase 2.
|
|
35
46
|
|
|
47
|
+
|
|
36
48
|
## Phase 2: Analysis Loop
|
|
37
49
|
|
|
38
50
|
Repeat until `status === "complete"`:
|
|
39
51
|
|
|
40
|
-
1. Read the engine's `prompt`
|
|
41
|
-
2.
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
52
|
+
1. Read the engine's `prompt` and `instruction` — these guide YOUR analysis. **Do not show either to the user.**
|
|
53
|
+
2. Show the user a brief status line:
|
|
54
|
+
- Format: `Round {round} — {what you're doing this round}`
|
|
55
|
+
- Example: `Round 3 — stress-testing the current position`
|
|
56
|
+
- Derive the description from the engine's phase indicator or your own summary of the round's focus. One line only.
|
|
57
|
+
3. Perform your analysis in your internal reasoning. The engine's prompt contains analytical framings, self-check questions, and constraint tracking — use all of these to guide your thinking, but they are YOUR instructions, not user output.
|
|
58
|
+
4. In your internal reasoning, end your analysis with the constraint update block the engine expects:
|
|
59
|
+
```
|
|
60
|
+
Constraint Updates
|
|
61
|
+
RESOLVE: C1, C2
|
|
62
|
+
ACKNOWLEDGE: C3
|
|
63
|
+
DEFER: C4 | <reason>
|
|
64
|
+
MILESTONE: <milestone> | <evidence>
|
|
65
|
+
End Constraint Updates
|
|
66
|
+
```
|
|
67
|
+
5. Call the engine tool with your full analysis (including constraint updates) as the `input`. **The full analysis text goes ONLY in the tool call, never in visible output.**
|
|
68
|
+
|
|
69
|
+
### What NEVER appears in user-visible output:
|
|
70
|
+
- The engine's `prompt` field content
|
|
71
|
+
- Constraint tables (`C1 [ACTIVE] FORWARD: ...`)
|
|
72
|
+
- Constraint update blocks (`RESOLVE: C1`, `MILESTONE: ...`)
|
|
73
|
+
- Self-check questions or responses
|
|
74
|
+
- Gate verdicts (`SOFT_FAIL`, `HARD_FAIL`, `PASS`)
|
|
75
|
+
- Milestone markers
|
|
76
|
+
- Protocol surface elements (session awareness text, phase indicators from engine)
|
|
77
|
+
|
|
78
|
+
### What the user sees per round:
|
|
79
|
+
- The one-line status (step 2 above)
|
|
80
|
+
- Nothing else until harvest
|
|
45
81
|
|
|
46
82
|
## Phase 3: Harvest
|
|
47
83
|
|
|
48
|
-
When `status === "complete"
|
|
84
|
+
When `status === "complete"`:
|
|
85
|
+
|
|
86
|
+
Synthesize the analysis for the user based on your accumulated reasoning across all rounds. Do NOT simply echo the engine's `harvest` fields — they are structured data, not a finished presentation.
|
|
49
87
|
|
|
50
88
|
Present to the user:
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
89
|
+
1. **Default Starting Point**: What the obvious answer was and why it was insufficient
|
|
90
|
+
2. **Key Findings**: The non-obvious insights that emerged, as a bullet list. Each finding should be a substantive sentence, not a label.
|
|
91
|
+
3. **Open Questions**: What remains genuinely uncertain (if any)
|
|
92
|
+
4. **Suggested Next Steps**: Concrete follow-up actions or investigations
|
|
93
|
+
|
|
94
|
+
Use the engine's `harvest.summary`, `harvest.keyFindings`, `harvest.openQuestions`, and `harvest.followUps` as source material, but write the synthesis in your own words with the depth your analysis produced. The harvest fields are often terse — your synthesis should reflect the full depth of the multi-round analysis.
|
package/dist/index.d.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Thin client that proxies tool calls to the RVRY engine HTTP API.
|
|
6
6
|
* Auth via RVRY_TOKEN env var (static rvry_ token).
|
|
7
7
|
*
|
|
8
|
-
* Exposes
|
|
8
|
+
* Exposes 2 tools (RVRY_deepthink, RVRY_problem_solve) + backward-compat aliases + 2 MCP Prompts.
|
|
9
9
|
*
|
|
10
10
|
* Also supports `npx @rvry/mcp setup` to install Claude Code commands.
|
|
11
11
|
*/
|
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Thin client that proxies tool calls to the RVRY engine HTTP API.
|
|
6
6
|
* Auth via RVRY_TOKEN env var (static rvry_ token).
|
|
7
7
|
*
|
|
8
|
-
* Exposes
|
|
8
|
+
* Exposes 2 tools (RVRY_deepthink, RVRY_problem_solve) + backward-compat aliases + 2 MCP Prompts.
|
|
9
9
|
*
|
|
10
10
|
* Also supports `npx @rvry/mcp setup` to install Claude Code commands.
|
|
11
11
|
*/
|
|
@@ -24,51 +24,18 @@ const questionCache = new Map();
|
|
|
24
24
|
*/
|
|
25
25
|
const TOOL_MAP = {
|
|
26
26
|
// Primary (rvry_ prefixed)
|
|
27
|
-
RVRY_think: 'think',
|
|
28
27
|
RVRY_deepthink: 'deepthink',
|
|
29
28
|
RVRY_problem_solve: 'problem_solve',
|
|
30
|
-
RVRY_challenge: 'challenge',
|
|
31
|
-
RVRY_meta: 'meta',
|
|
32
29
|
// Backward-compat aliases (unprefixed)
|
|
33
|
-
think: 'think',
|
|
34
30
|
deepthink: 'deepthink',
|
|
35
31
|
problem_solve: 'problem_solve',
|
|
36
|
-
challenge: 'challenge',
|
|
37
|
-
meta: 'meta',
|
|
38
32
|
};
|
|
39
33
|
const TOOL_DEFS = [
|
|
40
|
-
{
|
|
41
|
-
name: 'RVRY_think',
|
|
42
|
-
description: 'Carefully examine questions through guided rounds and self-checks. ' +
|
|
43
|
-
'Use for questions that need thorough examination but don\'t fit a specific pattern. ' +
|
|
44
|
-
'For high-stakes deep analysis, use RVRY_deepthink. ' +
|
|
45
|
-
'For choosing between options, use RVRY_problem_solve. ' +
|
|
46
|
-
'For stress-testing proposals, use RVRY_challenge. ' +
|
|
47
|
-
'For reflective examination of defaults and shifts, use RVRY_meta. ' +
|
|
48
|
-
'Call with your question to start, then send your analysis with the returned sessionId to continue.',
|
|
49
|
-
inputSchema: {
|
|
50
|
-
type: 'object',
|
|
51
|
-
properties: {
|
|
52
|
-
input: {
|
|
53
|
-
type: 'string',
|
|
54
|
-
description: 'The question to analyze (new session) or your analysis findings (continuation).',
|
|
55
|
-
},
|
|
56
|
-
sessionId: {
|
|
57
|
-
type: 'string',
|
|
58
|
-
description: 'Session ID for continuing an existing session. Omit to start a new session.',
|
|
59
|
-
},
|
|
60
|
-
skipScoping: {
|
|
61
|
-
type: 'boolean',
|
|
62
|
-
description: 'Skip the scoping questions phase and begin analysis immediately.',
|
|
63
|
-
},
|
|
64
|
-
},
|
|
65
|
-
required: ['input'],
|
|
66
|
-
},
|
|
67
|
-
},
|
|
68
34
|
{
|
|
69
35
|
name: 'RVRY_deepthink',
|
|
70
|
-
description: '
|
|
71
|
-
'
|
|
36
|
+
description: 'Deep, multi-round structured analysis for high-stakes or complex questions. ' +
|
|
37
|
+
'Runs 5-7 rounds with rigorous self-checks and pre-mortem analysis. ' +
|
|
38
|
+
'Use when the question demands thorough examination from multiple angles before reaching a conclusion. ' +
|
|
72
39
|
'Call with your question to start, then send your analysis with the returned sessionId to continue.',
|
|
73
40
|
inputSchema: {
|
|
74
41
|
type: 'object',
|
|
@@ -113,54 +80,6 @@ const TOOL_DEFS = [
|
|
|
113
80
|
required: ['input'],
|
|
114
81
|
},
|
|
115
82
|
},
|
|
116
|
-
{
|
|
117
|
-
name: 'RVRY_challenge',
|
|
118
|
-
description: 'Adversarial evaluation of a proposal using causal analysis and edge-case auditing. ' +
|
|
119
|
-
'Returns a rigorous assessment of the proposal\'s viability. ' +
|
|
120
|
-
'Call with your proposal to start, then send your analysis with the returned sessionId to continue.',
|
|
121
|
-
inputSchema: {
|
|
122
|
-
type: 'object',
|
|
123
|
-
properties: {
|
|
124
|
-
input: {
|
|
125
|
-
type: 'string',
|
|
126
|
-
description: 'The proposal to challenge (new session) or your analysis findings (continuation).',
|
|
127
|
-
},
|
|
128
|
-
sessionId: {
|
|
129
|
-
type: 'string',
|
|
130
|
-
description: 'Session ID for continuing an existing session. Omit to start a new session.',
|
|
131
|
-
},
|
|
132
|
-
skipScoping: {
|
|
133
|
-
type: 'boolean',
|
|
134
|
-
description: 'Skip the scoping questions phase and begin analysis immediately.',
|
|
135
|
-
},
|
|
136
|
-
},
|
|
137
|
-
required: ['input'],
|
|
138
|
-
},
|
|
139
|
-
},
|
|
140
|
-
{
|
|
141
|
-
name: 'RVRY_meta',
|
|
142
|
-
description: 'Reflect on defaults, shifts, and reasoning posture across a session or topic. ' +
|
|
143
|
-
'Use to inspect what changed under reflection and what remained stable. ' +
|
|
144
|
-
'Call with your topic or session context to start, then send your reflection with the returned sessionId to continue.',
|
|
145
|
-
inputSchema: {
|
|
146
|
-
type: 'object',
|
|
147
|
-
properties: {
|
|
148
|
-
input: {
|
|
149
|
-
type: 'string',
|
|
150
|
-
description: 'The topic or session context to reflect on (new session) or your reflective findings (continuation).',
|
|
151
|
-
},
|
|
152
|
-
sessionId: {
|
|
153
|
-
type: 'string',
|
|
154
|
-
description: 'Session ID for continuing an existing session. Omit to start a new session.',
|
|
155
|
-
},
|
|
156
|
-
skipScoping: {
|
|
157
|
-
type: 'boolean',
|
|
158
|
-
description: 'Skip the scoping questions phase and begin analysis immediately.',
|
|
159
|
-
},
|
|
160
|
-
},
|
|
161
|
-
required: ['input'],
|
|
162
|
-
},
|
|
163
|
-
},
|
|
164
83
|
];
|
|
165
84
|
/** Strip response fields based on status for context-efficient MCP responses */
|
|
166
85
|
function stripResponse(result, question) {
|
|
@@ -244,6 +163,18 @@ async function main() {
|
|
|
244
163
|
if (result.status === 'complete') {
|
|
245
164
|
questionCache.delete(result.sessionId);
|
|
246
165
|
}
|
|
166
|
+
// Write local session summary on completion (non-blocking, silent failure)
|
|
167
|
+
if (result.status === 'complete' && result.harvest) {
|
|
168
|
+
import('./local-writer.js').then(({ writeLocalSession }) => {
|
|
169
|
+
writeLocalSession({
|
|
170
|
+
sessionId: result.sessionId,
|
|
171
|
+
operationType: rvryTool,
|
|
172
|
+
question,
|
|
173
|
+
rounds: result.round,
|
|
174
|
+
harvest: result.harvest,
|
|
175
|
+
}).catch(() => { });
|
|
176
|
+
}).catch(() => { });
|
|
177
|
+
}
|
|
247
178
|
const stripped = stripResponse(result, question);
|
|
248
179
|
return {
|
|
249
180
|
content: [{ type: 'text', text: JSON.stringify(stripped) }],
|
|
@@ -265,36 +196,18 @@ async function main() {
|
|
|
265
196
|
description: 'Deep structured analysis for high-stakes questions',
|
|
266
197
|
arguments: [{ name: 'question', description: 'The question to analyze deeply', required: true }],
|
|
267
198
|
},
|
|
268
|
-
{
|
|
269
|
-
name: 'RVRY_think',
|
|
270
|
-
description: 'Structured analysis for questions needing careful examination',
|
|
271
|
-
arguments: [{ name: 'question', description: 'The question to think through', required: true }],
|
|
272
|
-
},
|
|
273
199
|
{
|
|
274
200
|
name: 'RVRY_problem_solve',
|
|
275
201
|
description: 'Structured decision-making for problems with multiple options',
|
|
276
202
|
arguments: [{ name: 'problem', description: 'The problem or decision to analyze', required: true }],
|
|
277
203
|
},
|
|
278
|
-
{
|
|
279
|
-
name: 'RVRY_challenge',
|
|
280
|
-
description: 'Adversarial evaluation of a proposal or plan',
|
|
281
|
-
arguments: [{ name: 'proposal', description: 'The proposal or plan to challenge', required: true }],
|
|
282
|
-
},
|
|
283
|
-
{
|
|
284
|
-
name: 'RVRY_meta',
|
|
285
|
-
description: 'Reflective examination of defaults, shifts, and reasoning posture',
|
|
286
|
-
arguments: [{ name: 'topic', description: 'The topic or session context to reflect on', required: true }],
|
|
287
|
-
},
|
|
288
204
|
],
|
|
289
205
|
}));
|
|
290
206
|
server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
291
207
|
const { name, arguments: args } = request.params;
|
|
292
208
|
const promptMap = {
|
|
293
209
|
RVRY_deepthink: `Use the RVRY_deepthink tool to analyze this question in depth: ${args?.question ?? ''}`,
|
|
294
|
-
RVRY_think: `Use the RVRY_think tool to analyze this question: ${args?.question ?? ''}`,
|
|
295
210
|
RVRY_problem_solve: `Use the RVRY_problem_solve tool to work through this decision: ${args?.problem ?? ''}`,
|
|
296
|
-
RVRY_challenge: `Use the RVRY_challenge tool to stress-test this proposal: ${args?.proposal ?? ''}`,
|
|
297
|
-
RVRY_meta: `Use the RVRY_meta tool to reflect on this topic or session context: ${args?.topic ?? ''}`,
|
|
298
211
|
};
|
|
299
212
|
const text = promptMap[name] ?? `Unknown prompt: ${name}`;
|
|
300
213
|
return {
|
|
@@ -308,7 +221,7 @@ async function main() {
|
|
|
308
221
|
});
|
|
309
222
|
const transport = new StdioServerTransport();
|
|
310
223
|
await server.connect(transport);
|
|
311
|
-
console.error('[rvry-mcp] Connected via stdio. Tools:
|
|
224
|
+
console.error('[rvry-mcp] Connected via stdio. Tools: RVRY_deepthink, RVRY_problem_solve');
|
|
312
225
|
}
|
|
313
226
|
// Export internals for testing
|
|
314
227
|
export { TOOL_MAP, TOOL_DEFS, stripResponse, questionCache };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local session writer -- writes a minimal markdown summary to .rvry/sessions/
|
|
3
|
+
* when a session completes.
|
|
4
|
+
*
|
|
5
|
+
* PROTOCOL SURFACE: This file produces user-facing output. It must NEVER contain
|
|
6
|
+
* constraint IDs, detection scores, mode names, gate verdicts, or any engine internals.
|
|
7
|
+
* Only question + harvest data.
|
|
8
|
+
*/
|
|
9
|
+
interface LocalSessionData {
|
|
10
|
+
sessionId: string;
|
|
11
|
+
operationType: string;
|
|
12
|
+
question: string;
|
|
13
|
+
rounds: number;
|
|
14
|
+
harvest: {
|
|
15
|
+
summary: string;
|
|
16
|
+
keyFindings: string[];
|
|
17
|
+
openQuestions: string[];
|
|
18
|
+
followUps: Array<{
|
|
19
|
+
question: string;
|
|
20
|
+
rationale: string;
|
|
21
|
+
}>;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Write a minimal markdown summary of a completed session to .rvry/sessions/.
|
|
26
|
+
*
|
|
27
|
+
* Disabled via RVRY_LOCAL_SESSIONS=0. Fails silently on any error.
|
|
28
|
+
*/
|
|
29
|
+
export declare function writeLocalSession(data: LocalSessionData): Promise<void>;
|
|
30
|
+
export {};
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local session writer -- writes a minimal markdown summary to .rvry/sessions/
|
|
3
|
+
* when a session completes.
|
|
4
|
+
*
|
|
5
|
+
* PROTOCOL SURFACE: This file produces user-facing output. It must NEVER contain
|
|
6
|
+
* constraint IDs, detection scores, mode names, gate verdicts, or any engine internals.
|
|
7
|
+
* Only question + harvest data.
|
|
8
|
+
*/
|
|
9
|
+
import { mkdirSync, writeFileSync } from 'fs';
|
|
10
|
+
import { join } from 'path';
|
|
11
|
+
/**
|
|
12
|
+
* Slugify a question string for use in filenames.
|
|
13
|
+
* Lowercases, replaces non-alphanumeric runs with hyphens, trims, and caps at maxLen chars.
|
|
14
|
+
*/
|
|
15
|
+
function slugify(text, maxLen = 50) {
|
|
16
|
+
return text
|
|
17
|
+
.toLowerCase()
|
|
18
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
19
|
+
.replace(/^-+|-+$/g, '')
|
|
20
|
+
.slice(0, maxLen)
|
|
21
|
+
.replace(/-+$/, '');
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Format a Date as YYYYMMDD-HHMM for filenames.
|
|
25
|
+
*/
|
|
26
|
+
function formatTimestamp(date) {
|
|
27
|
+
const y = date.getFullYear().toString();
|
|
28
|
+
const mo = (date.getMonth() + 1).toString().padStart(2, '0');
|
|
29
|
+
const d = date.getDate().toString().padStart(2, '0');
|
|
30
|
+
const h = date.getHours().toString().padStart(2, '0');
|
|
31
|
+
const mi = date.getMinutes().toString().padStart(2, '0');
|
|
32
|
+
return `${y}${mo}${d}-${h}${mi}`;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Format a Date as YYYY-MM-DD HH:MM for YAML frontmatter.
|
|
36
|
+
*/
|
|
37
|
+
function formatDate(date) {
|
|
38
|
+
const y = date.getFullYear().toString();
|
|
39
|
+
const mo = (date.getMonth() + 1).toString().padStart(2, '0');
|
|
40
|
+
const d = date.getDate().toString().padStart(2, '0');
|
|
41
|
+
const h = date.getHours().toString().padStart(2, '0');
|
|
42
|
+
const mi = date.getMinutes().toString().padStart(2, '0');
|
|
43
|
+
return `${y}-${mo}-${d} ${h}:${mi}`;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Build the markdown content for a completed session.
|
|
47
|
+
*/
|
|
48
|
+
function buildMarkdown(data, now) {
|
|
49
|
+
const lines = [];
|
|
50
|
+
// YAML frontmatter
|
|
51
|
+
lines.push('---');
|
|
52
|
+
lines.push(`session: ${data.sessionId}`);
|
|
53
|
+
lines.push(`operation: ${data.operationType}`);
|
|
54
|
+
lines.push(`date: ${formatDate(now)}`);
|
|
55
|
+
lines.push(`rounds: ${data.rounds}`);
|
|
56
|
+
lines.push('---');
|
|
57
|
+
lines.push('');
|
|
58
|
+
// Question
|
|
59
|
+
lines.push(`# ${data.question}`);
|
|
60
|
+
lines.push('');
|
|
61
|
+
// Summary
|
|
62
|
+
lines.push('## Summary');
|
|
63
|
+
lines.push(data.harvest.summary);
|
|
64
|
+
lines.push('');
|
|
65
|
+
// Key Findings
|
|
66
|
+
if (data.harvest.keyFindings.length > 0) {
|
|
67
|
+
lines.push('## Key Findings');
|
|
68
|
+
for (const finding of data.harvest.keyFindings) {
|
|
69
|
+
lines.push(`- ${finding}`);
|
|
70
|
+
}
|
|
71
|
+
lines.push('');
|
|
72
|
+
}
|
|
73
|
+
// Open Questions
|
|
74
|
+
if (data.harvest.openQuestions.length > 0) {
|
|
75
|
+
lines.push('## Open Questions');
|
|
76
|
+
for (const q of data.harvest.openQuestions) {
|
|
77
|
+
lines.push(`- ${q}`);
|
|
78
|
+
}
|
|
79
|
+
lines.push('');
|
|
80
|
+
}
|
|
81
|
+
// Follow-ups
|
|
82
|
+
if (data.harvest.followUps.length > 0) {
|
|
83
|
+
lines.push('## Follow-ups');
|
|
84
|
+
for (const f of data.harvest.followUps) {
|
|
85
|
+
lines.push(`- ${f.question} — ${f.rationale}`);
|
|
86
|
+
}
|
|
87
|
+
lines.push('');
|
|
88
|
+
}
|
|
89
|
+
return lines.join('\n');
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Write a minimal markdown summary of a completed session to .rvry/sessions/.
|
|
93
|
+
*
|
|
94
|
+
* Disabled via RVRY_LOCAL_SESSIONS=0. Fails silently on any error.
|
|
95
|
+
*/
|
|
96
|
+
export async function writeLocalSession(data) {
|
|
97
|
+
// Check opt-out
|
|
98
|
+
if (process.env.RVRY_LOCAL_SESSIONS === '0')
|
|
99
|
+
return;
|
|
100
|
+
try {
|
|
101
|
+
const now = new Date();
|
|
102
|
+
const timestamp = formatTimestamp(now);
|
|
103
|
+
const slug = slugify(data.question);
|
|
104
|
+
const filename = `${timestamp}-${data.operationType}-${slug}.md`;
|
|
105
|
+
const sessionsDir = join(process.cwd(), '.rvry', 'sessions');
|
|
106
|
+
mkdirSync(sessionsDir, { recursive: true });
|
|
107
|
+
const filePath = join(sessionsDir, filename);
|
|
108
|
+
const content = buildMarkdown(data, now);
|
|
109
|
+
writeFileSync(filePath, content, 'utf-8');
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
// Fail silently -- this is a convenience feature, not critical path
|
|
113
|
+
}
|
|
114
|
+
}
|
package/dist/setup.js
CHANGED
|
@@ -180,9 +180,6 @@ function openBrowser(url) {
|
|
|
180
180
|
return false;
|
|
181
181
|
}
|
|
182
182
|
}
|
|
183
|
-
/**
|
|
184
|
-
* Check if the `claude` CLI is available in PATH.
|
|
185
|
-
*/
|
|
186
183
|
function isClaudeCLIAvailable() {
|
|
187
184
|
try {
|
|
188
185
|
const cmd = platform() === 'win32' ? 'where claude' : 'which claude';
|
|
@@ -193,9 +190,6 @@ function isClaudeCLIAvailable() {
|
|
|
193
190
|
return false;
|
|
194
191
|
}
|
|
195
192
|
}
|
|
196
|
-
/**
|
|
197
|
-
* Get the platform-specific Claude Desktop config file path.
|
|
198
|
-
*/
|
|
199
193
|
function getDesktopConfigPath() {
|
|
200
194
|
const plat = platform();
|
|
201
195
|
if (plat === 'darwin') {
|
|
@@ -206,19 +200,10 @@ function getDesktopConfigPath() {
|
|
|
206
200
|
}
|
|
207
201
|
return join(homedir(), '.config', 'Claude', 'claude_desktop_config.json');
|
|
208
202
|
}
|
|
209
|
-
/**
|
|
210
|
-
* Check if Claude Desktop config directory exists (app is installed).
|
|
211
|
-
*/
|
|
212
203
|
function isClaudeDesktopInstalled() {
|
|
213
204
|
const configPath = getDesktopConfigPath();
|
|
214
|
-
|
|
215
|
-
return existsSync(configDir);
|
|
205
|
+
return existsSync(dirname(configPath));
|
|
216
206
|
}
|
|
217
|
-
/**
|
|
218
|
-
* Configure Claude Desktop by merging RVRY into the existing config.
|
|
219
|
-
* Creates the config file if it doesn't exist. Preserves other MCP servers.
|
|
220
|
-
* Returns 'created' | 'updated' | 'unchanged' | 'error'.
|
|
221
|
-
*/
|
|
222
207
|
function configureDesktop(token) {
|
|
223
208
|
const configPath = getDesktopConfigPath();
|
|
224
209
|
try {
|
|
@@ -235,29 +220,23 @@ function configureDesktop(token) {
|
|
|
235
220
|
config = {};
|
|
236
221
|
}
|
|
237
222
|
}
|
|
238
|
-
// Ensure mcpServers object exists
|
|
239
223
|
if (!config.mcpServers || typeof config.mcpServers !== 'object') {
|
|
240
224
|
config.mcpServers = {};
|
|
241
225
|
}
|
|
242
226
|
const servers = config.mcpServers;
|
|
243
|
-
const newEntry = RVRY_SERVER_ENTRY(token);
|
|
244
|
-
// Check if RVRY is already configured with the same token
|
|
245
227
|
const existing = servers.RVRY;
|
|
246
228
|
if (existing) {
|
|
247
229
|
const existingEnv = existing.env;
|
|
248
|
-
if (existingEnv?.RVRY_TOKEN === token)
|
|
230
|
+
if (existingEnv?.RVRY_TOKEN === token)
|
|
249
231
|
return 'unchanged';
|
|
250
|
-
}
|
|
251
232
|
}
|
|
252
233
|
const wasNew = !existing;
|
|
253
|
-
servers.RVRY =
|
|
254
|
-
// Write config with clean formatting
|
|
234
|
+
servers.RVRY = RVRY_SERVER_ENTRY(token);
|
|
255
235
|
const configDir = dirname(configPath);
|
|
256
|
-
if (!existsSync(configDir))
|
|
236
|
+
if (!existsSync(configDir))
|
|
257
237
|
mkdirSync(configDir, { recursive: true });
|
|
258
|
-
}
|
|
259
238
|
writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
|
|
260
|
-
return wasNew ? '
|
|
239
|
+
return wasNew ? 'ok' : 'updated';
|
|
261
240
|
}
|
|
262
241
|
catch (err) {
|
|
263
242
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -265,27 +244,221 @@ function configureDesktop(token) {
|
|
|
265
244
|
return 'error';
|
|
266
245
|
}
|
|
267
246
|
}
|
|
268
|
-
/**
|
|
269
|
-
* Register RVRY as an MCP server in Claude Code.
|
|
270
|
-
* Removes existing registration first for idempotency.
|
|
271
|
-
*/
|
|
272
247
|
function registerClaudeCode(token) {
|
|
273
248
|
try {
|
|
274
|
-
// Remove existing (ignore error if not registered)
|
|
275
249
|
try {
|
|
276
250
|
execSync('claude mcp remove rvry', { stdio: 'pipe' });
|
|
277
251
|
}
|
|
278
|
-
catch {
|
|
279
|
-
// Not registered yet, that's fine
|
|
280
|
-
}
|
|
281
|
-
// Add with token
|
|
252
|
+
catch { /* not registered */ }
|
|
282
253
|
execSync(`claude mcp add -e RVRY_TOKEN="${token}" -s user rvry -- npx @rvry/mcp`, { stdio: 'inherit' });
|
|
283
|
-
return
|
|
254
|
+
return 'ok';
|
|
284
255
|
}
|
|
285
256
|
catch {
|
|
286
|
-
return
|
|
257
|
+
return 'error';
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Write RVRY server entry into a generic MCP JSON config file.
|
|
262
|
+
* Used by clients that follow the Desktop-style config format (Codex, Gemini, etc.)
|
|
263
|
+
*/
|
|
264
|
+
function configureJsonMcp(configPath, token) {
|
|
265
|
+
try {
|
|
266
|
+
let config = {};
|
|
267
|
+
if (existsSync(configPath)) {
|
|
268
|
+
const raw = readFileSync(configPath, 'utf-8');
|
|
269
|
+
try {
|
|
270
|
+
config = JSON.parse(raw);
|
|
271
|
+
}
|
|
272
|
+
catch {
|
|
273
|
+
writeFileSync(configPath + '.backup', raw, 'utf-8');
|
|
274
|
+
config = {};
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
if (!config.mcpServers || typeof config.mcpServers !== 'object') {
|
|
278
|
+
config.mcpServers = {};
|
|
279
|
+
}
|
|
280
|
+
const servers = config.mcpServers;
|
|
281
|
+
const existing = servers.RVRY;
|
|
282
|
+
if (existing) {
|
|
283
|
+
const existingEnv = existing.env;
|
|
284
|
+
if (existingEnv?.RVRY_TOKEN === token)
|
|
285
|
+
return 'unchanged';
|
|
286
|
+
}
|
|
287
|
+
const wasNew = !existing;
|
|
288
|
+
servers.RVRY = RVRY_SERVER_ENTRY(token);
|
|
289
|
+
const dir = dirname(configPath);
|
|
290
|
+
if (!existsSync(dir))
|
|
291
|
+
mkdirSync(dir, { recursive: true });
|
|
292
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
|
|
293
|
+
return wasNew ? 'ok' : 'updated';
|
|
294
|
+
}
|
|
295
|
+
catch (err) {
|
|
296
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
297
|
+
console.log(` Error writing config: ${msg}`);
|
|
298
|
+
return 'error';
|
|
287
299
|
}
|
|
288
300
|
}
|
|
301
|
+
/** All supported clients. Add new clients here. */
|
|
302
|
+
const CLIENT_REGISTRY = [
|
|
303
|
+
{
|
|
304
|
+
name: 'Claude Code',
|
|
305
|
+
id: 'code',
|
|
306
|
+
detect: isClaudeCLIAvailable,
|
|
307
|
+
configure: registerClaudeCode,
|
|
308
|
+
notInstalledHint: 'CLI not found (https://docs.anthropic.com/en/docs/claude-code)',
|
|
309
|
+
},
|
|
310
|
+
{
|
|
311
|
+
name: 'Claude Desktop / Claude Co-Work',
|
|
312
|
+
id: 'desktop',
|
|
313
|
+
detect: isClaudeDesktopInstalled,
|
|
314
|
+
configure: (token) => configureDesktop(token),
|
|
315
|
+
notInstalledHint: 'Not installed',
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
name: 'Anti-Gravity',
|
|
319
|
+
id: 'antigravity',
|
|
320
|
+
detect: () => {
|
|
321
|
+
// OR-logic: binary (agy or antigravity), config dir, or macOS app bundle
|
|
322
|
+
try {
|
|
323
|
+
const cmd = platform() === 'win32' ? 'where agy' : 'which agy';
|
|
324
|
+
execSync(cmd, { stdio: 'pipe' });
|
|
325
|
+
return true;
|
|
326
|
+
}
|
|
327
|
+
catch { /* not on PATH */ }
|
|
328
|
+
if (platform() !== 'win32') {
|
|
329
|
+
try {
|
|
330
|
+
execSync('which antigravity', { stdio: 'pipe' });
|
|
331
|
+
return true;
|
|
332
|
+
}
|
|
333
|
+
catch { /* not on PATH */ }
|
|
334
|
+
}
|
|
335
|
+
if (existsSync(join(homedir(), '.gemini', 'antigravity')))
|
|
336
|
+
return true;
|
|
337
|
+
if (platform() === 'darwin' && existsSync('/Applications/Google Antigravity.app'))
|
|
338
|
+
return true;
|
|
339
|
+
return false;
|
|
340
|
+
},
|
|
341
|
+
configure: (token) => configureJsonMcp(join(homedir(), '.gemini', 'antigravity', 'mcp_config.json'), token),
|
|
342
|
+
notInstalledHint: 'Not installed (https://antigravity.google)',
|
|
343
|
+
},
|
|
344
|
+
];
|
|
345
|
+
/**
|
|
346
|
+
* Interactive multi-select picker with arrow keys + space + enter.
|
|
347
|
+
* Returns indices of selected items.
|
|
348
|
+
*/
|
|
349
|
+
function multiSelect(items) {
|
|
350
|
+
return new Promise((resolve) => {
|
|
351
|
+
const stdin = process.stdin;
|
|
352
|
+
const wasRaw = stdin.isRaw;
|
|
353
|
+
if (typeof stdin.setRawMode === 'function')
|
|
354
|
+
stdin.setRawMode(true);
|
|
355
|
+
stdin.resume();
|
|
356
|
+
stdin.setEncoding('utf8');
|
|
357
|
+
let cursor = items.findIndex((i) => i.available);
|
|
358
|
+
if (cursor === -1)
|
|
359
|
+
cursor = 0;
|
|
360
|
+
const render = () => {
|
|
361
|
+
// Move cursor up to overwrite previous render (except first render)
|
|
362
|
+
for (let i = 0; i < items.length; i++) {
|
|
363
|
+
process.stdout.write('\x1b[2K'); // clear line
|
|
364
|
+
if (i < items.length - 1)
|
|
365
|
+
process.stdout.write('\x1b[1A'); // move up
|
|
366
|
+
}
|
|
367
|
+
process.stdout.write('\r');
|
|
368
|
+
for (let i = 0; i < items.length; i++) {
|
|
369
|
+
const item = items[i];
|
|
370
|
+
const pointer = i === cursor ? '›' : ' ';
|
|
371
|
+
const checkbox = !item.available
|
|
372
|
+
? '\x1b[2m[ ]\x1b[0m'
|
|
373
|
+
: item.selected
|
|
374
|
+
? '\x1b[32m[x]\x1b[0m'
|
|
375
|
+
: '[ ]';
|
|
376
|
+
const label = !item.available
|
|
377
|
+
? `\x1b[2m${item.label}\x1b[0m`
|
|
378
|
+
: item.label;
|
|
379
|
+
const hint = !item.available && item.hint
|
|
380
|
+
? ` \x1b[2m${item.hint}\x1b[0m`
|
|
381
|
+
: '';
|
|
382
|
+
process.stdout.write(` ${pointer} ${checkbox} ${label}${hint}`);
|
|
383
|
+
if (i < items.length - 1)
|
|
384
|
+
process.stdout.write('\n');
|
|
385
|
+
}
|
|
386
|
+
};
|
|
387
|
+
// Initial render: print blank lines then render
|
|
388
|
+
for (let i = 0; i < items.length; i++) {
|
|
389
|
+
process.stdout.write(i < items.length - 1 ? '\n' : '');
|
|
390
|
+
}
|
|
391
|
+
render();
|
|
392
|
+
const cleanup = () => {
|
|
393
|
+
if (typeof stdin.setRawMode === 'function')
|
|
394
|
+
stdin.setRawMode(wasRaw ?? false);
|
|
395
|
+
stdin.removeListener('data', onData);
|
|
396
|
+
stdin.pause();
|
|
397
|
+
};
|
|
398
|
+
const onData = (key) => {
|
|
399
|
+
// Ctrl+C
|
|
400
|
+
if (key === '\x03') {
|
|
401
|
+
cleanup();
|
|
402
|
+
process.stdout.write('\n');
|
|
403
|
+
process.exit(1);
|
|
404
|
+
}
|
|
405
|
+
// Enter — confirm selection
|
|
406
|
+
if (key === '\r' || key === '\n') {
|
|
407
|
+
cleanup();
|
|
408
|
+
process.stdout.write('\n');
|
|
409
|
+
const selected = [];
|
|
410
|
+
for (let i = 0; i < items.length; i++) {
|
|
411
|
+
if (items[i].selected && items[i].available)
|
|
412
|
+
selected.push(i);
|
|
413
|
+
}
|
|
414
|
+
resolve(selected);
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
// Space — toggle current item
|
|
418
|
+
if (key === ' ') {
|
|
419
|
+
if (items[cursor].available) {
|
|
420
|
+
items[cursor].selected = !items[cursor].selected;
|
|
421
|
+
render();
|
|
422
|
+
}
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
// Arrow keys (escape sequences)
|
|
426
|
+
if (key === '\x1b[A' || key === 'k') {
|
|
427
|
+
// Up
|
|
428
|
+
let next = cursor - 1;
|
|
429
|
+
while (next >= 0 && !items[next].available)
|
|
430
|
+
next--;
|
|
431
|
+
if (next >= 0) {
|
|
432
|
+
cursor = next;
|
|
433
|
+
render();
|
|
434
|
+
}
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
if (key === '\x1b[B' || key === 'j') {
|
|
438
|
+
// Down
|
|
439
|
+
let next = cursor + 1;
|
|
440
|
+
while (next < items.length && !items[next].available)
|
|
441
|
+
next++;
|
|
442
|
+
if (next < items.length) {
|
|
443
|
+
cursor = next;
|
|
444
|
+
render();
|
|
445
|
+
}
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
// 'a' — select all available
|
|
449
|
+
if (key === 'a') {
|
|
450
|
+
const allSelected = items.filter((i) => i.available).every((i) => i.selected);
|
|
451
|
+
for (const item of items) {
|
|
452
|
+
if (item.available)
|
|
453
|
+
item.selected = !allSelected;
|
|
454
|
+
}
|
|
455
|
+
render();
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
};
|
|
459
|
+
stdin.on('data', onData);
|
|
460
|
+
});
|
|
461
|
+
}
|
|
289
462
|
/**
|
|
290
463
|
* Verify token by hitting the engine /api/usage endpoint.
|
|
291
464
|
* Returns tier info on success, null on failure.
|
|
@@ -455,6 +628,32 @@ async function installCommands() {
|
|
|
455
628
|
}
|
|
456
629
|
return installed;
|
|
457
630
|
}
|
|
631
|
+
// ── Gitignore protection ───────────────────────────────────────────
|
|
632
|
+
/**
|
|
633
|
+
* Ensure .rvry/ is in the project's .gitignore to prevent
|
|
634
|
+
* accidental commits of sensitive analysis content.
|
|
635
|
+
* Fails silently on any error.
|
|
636
|
+
*/
|
|
637
|
+
function ensureGitignore() {
|
|
638
|
+
const gitignorePath = join(process.cwd(), '.gitignore');
|
|
639
|
+
try {
|
|
640
|
+
let content = '';
|
|
641
|
+
if (existsSync(gitignorePath)) {
|
|
642
|
+
content = readFileSync(gitignorePath, 'utf-8');
|
|
643
|
+
}
|
|
644
|
+
if (!content.includes('.rvry')) {
|
|
645
|
+
const newContent = content ? `${content.trimEnd()}\n.rvry/\n` : '.rvry/\n';
|
|
646
|
+
writeFileSync(gitignorePath, newContent, 'utf-8');
|
|
647
|
+
console.log(' Added .rvry/ to .gitignore');
|
|
648
|
+
}
|
|
649
|
+
else {
|
|
650
|
+
console.log(' .rvry/ already in .gitignore');
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
catch {
|
|
654
|
+
// Non-fatal -- read-only filesystem, permissions, etc.
|
|
655
|
+
}
|
|
656
|
+
}
|
|
458
657
|
// ── Main setup flow ────────────────────────────────────────────────
|
|
459
658
|
const BANNER = `
|
|
460
659
|
8888888b. 888 888 8888888b. Y88b d88P
|
|
@@ -484,11 +683,11 @@ export async function runSetup() {
|
|
|
484
683
|
process.exit(1);
|
|
485
684
|
}
|
|
486
685
|
token = flagValue;
|
|
487
|
-
console.log('[1/
|
|
686
|
+
console.log('[1/5] Authentication (from --token flag)');
|
|
488
687
|
console.log(` Token: ${maskToken(token)}`);
|
|
489
688
|
}
|
|
490
689
|
else {
|
|
491
|
-
console.log('[1/
|
|
690
|
+
console.log('[1/5] Authentication');
|
|
492
691
|
console.log(' Opening browser for sign-in...');
|
|
493
692
|
const result = await deviceAuthFlow();
|
|
494
693
|
if (result) {
|
|
@@ -522,93 +721,68 @@ export async function runSetup() {
|
|
|
522
721
|
}
|
|
523
722
|
console.log('');
|
|
524
723
|
// ── Step 2: Detect clients ──────────────────────────────────────
|
|
525
|
-
console.log('[2/
|
|
526
|
-
|
|
527
|
-
const
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
const
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
|
|
724
|
+
console.log('[2/5] Detecting clients');
|
|
725
|
+
// Filter registry if --client was passed
|
|
726
|
+
const clients = clientFilter
|
|
727
|
+
? CLIENT_REGISTRY.filter((c) => c.id === clientFilter)
|
|
728
|
+
: CLIENT_REGISTRY;
|
|
729
|
+
const detected = clients.map((c) => ({
|
|
730
|
+
client: c,
|
|
731
|
+
available: c.detect(),
|
|
732
|
+
}));
|
|
733
|
+
const anyDetected = detected.some((d) => d.available);
|
|
734
|
+
for (const d of detected) {
|
|
735
|
+
if (d.available) {
|
|
736
|
+
console.log(` Found: ${d.client.name}`);
|
|
737
|
+
}
|
|
536
738
|
}
|
|
537
|
-
if (!
|
|
538
|
-
console.log(' No
|
|
739
|
+
if (!anyDetected) {
|
|
740
|
+
console.log(' No supported clients detected.');
|
|
539
741
|
}
|
|
540
742
|
console.log('');
|
|
541
743
|
// ── Step 3: Configure clients ───────────────────────────────────
|
|
542
|
-
console.log('[3/
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
if (hasDesktop && wantDesktop) {
|
|
571
|
-
const rl = createRL();
|
|
572
|
-
const answer = await ask(rl, ' Install for Claude Desktop / Co-Work? [Y/n] ');
|
|
573
|
-
rl.close();
|
|
574
|
-
if (answer.toLowerCase() !== 'n') {
|
|
575
|
-
desktopConfigured = configureDesktop(token);
|
|
576
|
-
switch (desktopConfigured) {
|
|
577
|
-
case 'created':
|
|
578
|
-
neitherConfigured = false;
|
|
579
|
-
console.log(' Claude Desktop: Added RVRY to config');
|
|
580
|
-
break;
|
|
581
|
-
case 'updated':
|
|
582
|
-
neitherConfigured = false;
|
|
583
|
-
console.log(' Claude Desktop: Updated RVRY token in config');
|
|
584
|
-
break;
|
|
585
|
-
case 'unchanged':
|
|
586
|
-
neitherConfigured = false;
|
|
587
|
-
console.log(' Claude Desktop: Already configured with this token');
|
|
588
|
-
break;
|
|
589
|
-
case 'error':
|
|
590
|
-
console.log(' Claude Desktop: Failed to write config (see error above)');
|
|
591
|
-
break;
|
|
592
|
-
}
|
|
593
|
-
}
|
|
594
|
-
else {
|
|
595
|
-
console.log(' Claude Desktop: Skipped');
|
|
596
|
-
}
|
|
744
|
+
console.log('[3/5] Select apps to add RVRY MCP configuration');
|
|
745
|
+
console.log(' Use \x1b[1m↑↓\x1b[0m to navigate, \x1b[1mspace\x1b[0m to toggle, \x1b[1ma\x1b[0m to toggle all, \x1b[1menter\x1b[0m to confirm');
|
|
746
|
+
console.log('');
|
|
747
|
+
const pickerItems = detected.map((d) => ({
|
|
748
|
+
label: d.client.name,
|
|
749
|
+
selected: d.available, // pre-select detected clients
|
|
750
|
+
available: d.available,
|
|
751
|
+
hint: d.available ? undefined : d.client.notInstalledHint,
|
|
752
|
+
}));
|
|
753
|
+
const selectedIndices = await multiSelect(pickerItems);
|
|
754
|
+
const configuredClients = [];
|
|
755
|
+
let anyConfigured = false;
|
|
756
|
+
console.log('');
|
|
757
|
+
for (const idx of selectedIndices) {
|
|
758
|
+
const { client } = detected[idx];
|
|
759
|
+
const result = client.configure(token);
|
|
760
|
+
const statusMap = {
|
|
761
|
+
ok: 'Configured',
|
|
762
|
+
updated: 'Updated token',
|
|
763
|
+
unchanged: 'Already configured',
|
|
764
|
+
error: 'Failed',
|
|
765
|
+
};
|
|
766
|
+
const status = statusMap[result] ?? result;
|
|
767
|
+
const icon = result === 'error' ? '✗' : '✓';
|
|
768
|
+
console.log(` ${icon} ${client.name}: ${status}`);
|
|
769
|
+
configuredClients.push({ name: client.name, status });
|
|
770
|
+
if (result !== 'error')
|
|
771
|
+
anyConfigured = true;
|
|
597
772
|
}
|
|
598
|
-
|
|
599
|
-
console.log('
|
|
773
|
+
if (selectedIndices.length === 0) {
|
|
774
|
+
console.log(' No clients selected.');
|
|
600
775
|
}
|
|
601
776
|
// Fallback: print manual config if nothing was configured
|
|
602
|
-
if (
|
|
777
|
+
if (!anyConfigured) {
|
|
603
778
|
console.log('');
|
|
604
779
|
console.log(' Manual configuration:');
|
|
605
780
|
console.log('');
|
|
606
781
|
console.log(' Option A — Claude Code (if you install it later):');
|
|
607
782
|
console.log(` claude mcp add -e RVRY_TOKEN="${token}" -s user rvry -- npx @rvry/mcp`);
|
|
608
783
|
console.log('');
|
|
609
|
-
console.log(' Option B — Claude Desktop
|
|
784
|
+
console.log(' Option B — JSON config (Claude Desktop, Codex, etc.):');
|
|
610
785
|
const manualConfig = { mcpServers: { RVRY: RVRY_SERVER_ENTRY(token) } };
|
|
611
|
-
console.log(` File: ${desktopConfigPath}`);
|
|
612
786
|
console.log('');
|
|
613
787
|
for (const line of JSON.stringify(manualConfig, null, 2).split('\n')) {
|
|
614
788
|
console.log(` ${line}`);
|
|
@@ -616,7 +790,7 @@ export async function runSetup() {
|
|
|
616
790
|
}
|
|
617
791
|
console.log('');
|
|
618
792
|
// ── Step 4: Slash commands ──────────────────────────────────────
|
|
619
|
-
console.log('[4/
|
|
793
|
+
console.log('[4/5] Slash Commands');
|
|
620
794
|
const commandCount = await installCommands();
|
|
621
795
|
if (commandCount > 0) {
|
|
622
796
|
console.log(` Installed ${commandCount} command${commandCount > 1 ? 's' : ''} to .claude/commands/`);
|
|
@@ -625,41 +799,46 @@ export async function runSetup() {
|
|
|
625
799
|
console.log(' No new commands installed (already up to date).');
|
|
626
800
|
}
|
|
627
801
|
console.log('');
|
|
802
|
+
// ── Step 5: Gitignore protection ────────────────────────────────
|
|
803
|
+
console.log('[5/5] Gitignore');
|
|
804
|
+
ensureGitignore();
|
|
805
|
+
console.log('');
|
|
628
806
|
// ── Summary ─────────────────────────────────────────────────────
|
|
629
807
|
console.log('─'.repeat(50));
|
|
630
808
|
console.log('');
|
|
631
809
|
console.log('RVRY Setup Complete');
|
|
632
810
|
console.log('');
|
|
633
811
|
console.log(` Token: ${maskToken(token)}${usage ? ` (${usage.tier})` : ''}`);
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
if (desktopConfigured !== 'skipped' && desktopConfigured !== 'error') {
|
|
638
|
-
console.log(' Claude Desktop: Configured');
|
|
812
|
+
for (const c of configuredClients) {
|
|
813
|
+
const pad = ' '.repeat(Math.max(1, 16 - c.name.length));
|
|
814
|
+
console.log(` ${c.name}:${pad}${c.status}`);
|
|
639
815
|
}
|
|
640
816
|
console.log(` Commands: ${commandCount} installed`);
|
|
641
817
|
console.log('');
|
|
642
818
|
// Client-specific next steps
|
|
819
|
+
const configuredNames = new Set(configuredClients.map((c) => c.name));
|
|
820
|
+
const hasDesktopStyle = configuredNames.has('Claude Desktop / Claude Co-Work')
|
|
821
|
+
|| configuredNames.has('Anti-Gravity');
|
|
822
|
+
const hasCodeStyle = configuredNames.has('Claude Code');
|
|
643
823
|
console.log('Next steps:');
|
|
644
|
-
const desktopOk = desktopConfigured !== 'skipped' && desktopConfigured !== 'error';
|
|
645
824
|
let step = 1;
|
|
646
|
-
if (
|
|
647
|
-
console.log(` ${step}. Restart
|
|
825
|
+
if (hasDesktopStyle) {
|
|
826
|
+
console.log(` ${step}. Restart desktop apps for the new MCP server to load`);
|
|
648
827
|
step++;
|
|
649
828
|
}
|
|
650
|
-
if (
|
|
829
|
+
if (hasCodeStyle) {
|
|
651
830
|
console.log(` ${step}. In Claude Code, try:`);
|
|
652
831
|
console.log(' /deepthink "your question"');
|
|
653
832
|
console.log(' /problem-solve "your decision"');
|
|
654
833
|
step++;
|
|
655
834
|
}
|
|
656
|
-
if (
|
|
657
|
-
console.log(` ${step}. In
|
|
835
|
+
if (hasDesktopStyle) {
|
|
836
|
+
console.log(` ${step}. In desktop clients, use natural language:`);
|
|
658
837
|
console.log(' "Use deepthink to analyze..." or "Use problem-solve for..."');
|
|
659
838
|
step++;
|
|
660
839
|
}
|
|
661
|
-
if (!
|
|
662
|
-
console.log(' 1. Configure a
|
|
840
|
+
if (!anyConfigured) {
|
|
841
|
+
console.log(' 1. Configure a client using the manual instructions above');
|
|
663
842
|
console.log(' 2. Then try: /deepthink "your question"');
|
|
664
843
|
}
|
|
665
844
|
console.log('');
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rvry/mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "RVRY reasoning depth enforcement (RDE) engine client.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"rvry-mcp": "
|
|
7
|
+
"rvry-mcp": "dist/index.js"
|
|
8
8
|
},
|
|
9
9
|
"main": "./dist/index.js",
|
|
10
10
|
"files": [
|