@getmarrow/install 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +58 -0
- package/bin/marrow-install.js +9 -0
- package/package.json +33 -0
- package/src/installer.js +502 -0
package/README.md
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# @getmarrow/install
|
|
2
|
+
|
|
3
|
+
Universal installer for Marrow passive agent setup.
|
|
4
|
+
|
|
5
|
+
Use it when you want Marrow to detect the local agent/runtime environment and wire the safest passive integration automatically.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx @getmarrow/install --dry-run
|
|
9
|
+
npx @getmarrow/install --yes
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## What It Detects
|
|
13
|
+
|
|
14
|
+
- OpenClaw-style workspaces
|
|
15
|
+
- Codex/agent instruction files such as `AGENTS.md`
|
|
16
|
+
- Claude Code settings and hooks
|
|
17
|
+
- Cursor project folders
|
|
18
|
+
- MCP config files
|
|
19
|
+
- Node projects
|
|
20
|
+
- Python projects
|
|
21
|
+
|
|
22
|
+
## Install Modes
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npx @getmarrow/install --mcp --dry-run
|
|
26
|
+
npx @getmarrow/install --sdk --dry-run
|
|
27
|
+
npx @getmarrow/install --both --dry-run
|
|
28
|
+
npx @getmarrow/install --md --dry-run
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
`--dry-run` is the default unless `--yes` is passed.
|
|
32
|
+
|
|
33
|
+
## What It Writes
|
|
34
|
+
|
|
35
|
+
- `.claude/settings.json` passive MCP hooks for tool outcomes and prompt context.
|
|
36
|
+
- `.mcp.json` with the Marrow MCP server entry.
|
|
37
|
+
- `.marrow/passive-runtime.mjs` for SDK passive runtime preload in Node processes.
|
|
38
|
+
- `.marrow/env.example` with required environment variables.
|
|
39
|
+
- `AGENTS.md` instructions for agents that rely on markdown/skills.
|
|
40
|
+
- `.cursor/rules/marrow.mdc` when a Cursor project is detected.
|
|
41
|
+
|
|
42
|
+
## Self-Test
|
|
43
|
+
|
|
44
|
+
When `MARROW_API_KEY` is present, the installer creates a harmless test decision, commits the outcome, and reads `/v1/agent/status`.
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
MARROW_API_KEY=mrw_live_xxx npx @getmarrow/install --yes
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Skip self-test:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
npx @getmarrow/install --yes --no-self-test
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Trust Model
|
|
57
|
+
|
|
58
|
+
This package is intended to be open source and auditable. It prints every file it will touch, requires `--yes` to write, does not store API keys in project files, and supports MCP-only, SDK-only, both, and markdown-only setups.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { runCli } = require('../src/installer');
|
|
4
|
+
|
|
5
|
+
runCli(process.argv.slice(2)).catch((error) => {
|
|
6
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
7
|
+
process.stderr.write(`marrow-install failed: ${message}\n`);
|
|
8
|
+
process.exit(1);
|
|
9
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@getmarrow/install",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Universal installer for Marrow passive agent setup.",
|
|
5
|
+
"bin": {
|
|
6
|
+
"marrow-install": "bin/marrow-install.js"
|
|
7
|
+
},
|
|
8
|
+
"main": "src/installer.js",
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "node --test test/*.test.js"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"ai",
|
|
14
|
+
"agents",
|
|
15
|
+
"marrow",
|
|
16
|
+
"mcp",
|
|
17
|
+
"installer",
|
|
18
|
+
"passive-runtime"
|
|
19
|
+
],
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "https://github.com/getmarrow/marrow-install.git"
|
|
23
|
+
},
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"files": [
|
|
26
|
+
"bin",
|
|
27
|
+
"src",
|
|
28
|
+
"README.md"
|
|
29
|
+
],
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=18"
|
|
32
|
+
}
|
|
33
|
+
}
|
package/src/installer.js
ADDED
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
const fs = require('node:fs');
|
|
2
|
+
const path = require('node:path');
|
|
3
|
+
const os = require('node:os');
|
|
4
|
+
|
|
5
|
+
const DEFAULT_BASE_URL = 'https://api.getmarrow.ai';
|
|
6
|
+
const MARROW_BLOCK_START = '<!-- marrow:passive-start -->';
|
|
7
|
+
const MARROW_BLOCK_END = '<!-- marrow:passive-end -->';
|
|
8
|
+
|
|
9
|
+
function parseArgs(argv) {
|
|
10
|
+
const options = {
|
|
11
|
+
cwd: process.cwd(),
|
|
12
|
+
yes: false,
|
|
13
|
+
dryRun: false,
|
|
14
|
+
mode: 'auto',
|
|
15
|
+
apiKey: process.env.MARROW_API_KEY || '',
|
|
16
|
+
baseUrl: process.env.MARROW_BASE_URL || DEFAULT_BASE_URL,
|
|
17
|
+
agentId: process.env.MARROW_FLEET_AGENT_ID || process.env.MARROW_AGENT_ID || '',
|
|
18
|
+
selfTest: true,
|
|
19
|
+
json: false,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
23
|
+
const arg = argv[i];
|
|
24
|
+
if (arg === '--yes' || arg === '-y') options.yes = true;
|
|
25
|
+
else if (arg === '--dry-run') options.dryRun = true;
|
|
26
|
+
else if (arg === '--json') options.json = true;
|
|
27
|
+
else if (arg === '--no-self-test') options.selfTest = false;
|
|
28
|
+
else if (arg === '--self-test') options.selfTest = true;
|
|
29
|
+
else if (arg === '--cwd') options.cwd = path.resolve(argv[++i] || options.cwd);
|
|
30
|
+
else if (arg === '--mode') options.mode = argv[++i] || options.mode;
|
|
31
|
+
else if (arg === '--key') {
|
|
32
|
+
options.apiKey = argv[++i] || options.apiKey;
|
|
33
|
+
options.keyFromArg = true;
|
|
34
|
+
}
|
|
35
|
+
else if (arg === '--base-url') options.baseUrl = argv[++i] || options.baseUrl;
|
|
36
|
+
else if (arg === '--agent-id') options.agentId = argv[++i] || options.agentId;
|
|
37
|
+
else if (arg === '--mcp') options.mode = 'mcp';
|
|
38
|
+
else if (arg === '--sdk') options.mode = 'sdk';
|
|
39
|
+
else if (arg === '--md' || arg === '--instructions') options.mode = 'md';
|
|
40
|
+
else if (arg === '--both') options.mode = 'both';
|
|
41
|
+
else if (arg === '--help' || arg === '-h') {
|
|
42
|
+
options.help = true;
|
|
43
|
+
} else {
|
|
44
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (!['auto', 'mcp', 'sdk', 'both', 'md'].includes(options.mode)) {
|
|
49
|
+
throw new Error('--mode must be one of auto, mcp, sdk, both, md');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return options;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function usage() {
|
|
56
|
+
return `Usage:
|
|
57
|
+
npx @getmarrow/install --dry-run
|
|
58
|
+
npx @getmarrow/install --yes
|
|
59
|
+
npx @getmarrow/install --mcp --yes
|
|
60
|
+
npx @getmarrow/install --sdk --yes
|
|
61
|
+
|
|
62
|
+
Options:
|
|
63
|
+
--dry-run Print planned changes without writing
|
|
64
|
+
--yes, -y Write detected config files
|
|
65
|
+
--mode <mode> auto, mcp, sdk, both, or md
|
|
66
|
+
--key <key> Marrow API key for self-test. Prefer MARROW_API_KEY because CLI args can appear in process listings.
|
|
67
|
+
--base-url <url> Marrow API base URL
|
|
68
|
+
--agent-id <id> Agent/fleet id for self-test headers
|
|
69
|
+
--no-self-test Skip API smoke/self-test
|
|
70
|
+
`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function exists(filePath) {
|
|
74
|
+
return fs.existsSync(filePath);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function safeRead(filePath) {
|
|
78
|
+
return exists(filePath) ? fs.readFileSync(filePath, 'utf8') : '';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function findUp(startDir, names, maxDepth = 8) {
|
|
82
|
+
let dir = path.resolve(startDir);
|
|
83
|
+
for (let depth = 0; depth <= maxDepth; depth += 1) {
|
|
84
|
+
for (const name of names) {
|
|
85
|
+
const candidate = path.join(dir, name);
|
|
86
|
+
if (exists(candidate)) return candidate;
|
|
87
|
+
}
|
|
88
|
+
const parent = path.dirname(dir);
|
|
89
|
+
if (parent === dir) break;
|
|
90
|
+
dir = parent;
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function projectRoot(startDir) {
|
|
96
|
+
const marker = findUp(startDir, ['package.json', 'pyproject.toml', 'requirements.txt', '.git', 'AGENTS.md', 'CLAUDE.md']);
|
|
97
|
+
return marker ? path.dirname(marker) : path.resolve(startDir);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function detectEnvironment(cwd = process.cwd(), env = process.env) {
|
|
101
|
+
const root = projectRoot(cwd);
|
|
102
|
+
const home = env.HOME || env.USERPROFILE || os.homedir();
|
|
103
|
+
const paths = {
|
|
104
|
+
root,
|
|
105
|
+
packageJson: path.join(root, 'package.json'),
|
|
106
|
+
pyproject: path.join(root, 'pyproject.toml'),
|
|
107
|
+
requirements: path.join(root, 'requirements.txt'),
|
|
108
|
+
setupPy: path.join(root, 'setup.py'),
|
|
109
|
+
claudeSettings: path.join(root, '.claude', 'settings.json'),
|
|
110
|
+
claudeMd: path.join(root, 'CLAUDE.md'),
|
|
111
|
+
agentsMd: path.join(root, 'AGENTS.md'),
|
|
112
|
+
cursorRules: path.join(root, '.cursor', 'rules', 'marrow.mdc'),
|
|
113
|
+
cursorMcp: path.join(root, '.cursor', 'mcp.json'),
|
|
114
|
+
mcpJson: path.join(root, '.mcp.json'),
|
|
115
|
+
marrowDir: path.join(root, '.marrow'),
|
|
116
|
+
passiveRuntime: path.join(root, '.marrow', 'passive-runtime.mjs'),
|
|
117
|
+
passiveEnv: path.join(root, '.marrow', 'env.example'),
|
|
118
|
+
openclawJson: findUp(root, ['openclaw.json'], 4) || path.join(home, '.openclaw', 'openclaw.json'),
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
root,
|
|
123
|
+
paths,
|
|
124
|
+
node: exists(paths.packageJson),
|
|
125
|
+
python: exists(paths.pyproject) || exists(paths.requirements) || exists(paths.setupPy),
|
|
126
|
+
claudeCode: exists(paths.claudeSettings) || exists(paths.claudeMd),
|
|
127
|
+
cursor: exists(path.join(root, '.cursor')),
|
|
128
|
+
codex: exists(paths.agentsMd) || exists(path.join(root, '.codex')),
|
|
129
|
+
mcpConfig: exists(paths.mcpJson) || exists(paths.cursorMcp) || exists(paths.claudeSettings),
|
|
130
|
+
openclaw: exists(paths.openclawJson) || Boolean(env.OPENCLAW_HOME || env.OPENCLAW_AGENT_ID),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function passiveInstructions() {
|
|
135
|
+
return `${MARROW_BLOCK_START}
|
|
136
|
+
## Marrow Passive Agent Memory
|
|
137
|
+
|
|
138
|
+
Marrow should run passively after install:
|
|
139
|
+
|
|
140
|
+
- Use MCP hooks when available: \`npx -y @getmarrow/mcp setup\`.
|
|
141
|
+
- Use SDK passive runtime in owned Node processes: \`createPassiveRuntime().install()\`.
|
|
142
|
+
- Before risky work, use Marrow's decision brief or passive prompt hook.
|
|
143
|
+
- After meaningful work, record the outcome so future agents learn from it.
|
|
144
|
+
- Check health with \`marrow_agent_status\` or \`GET /v1/agent/status\`.
|
|
145
|
+
|
|
146
|
+
Required environment:
|
|
147
|
+
|
|
148
|
+
- \`MARROW_API_KEY\`
|
|
149
|
+
- Optional: \`MARROW_BASE_URL\`, \`MARROW_FLEET_AGENT_ID\`
|
|
150
|
+
${MARROW_BLOCK_END}`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function passiveRuntimeSource() {
|
|
154
|
+
return `import { MarrowClient } from '@getmarrow/sdk';
|
|
155
|
+
|
|
156
|
+
const apiKey = process.env.MARROW_API_KEY;
|
|
157
|
+
if (apiKey && !globalThis.__MARROW_PASSIVE_RUNTIME__) {
|
|
158
|
+
const marrow = new MarrowClient(apiKey, {
|
|
159
|
+
baseUrl: process.env.MARROW_BASE_URL,
|
|
160
|
+
agentId: process.env.MARROW_FLEET_AGENT_ID || process.env.MARROW_AGENT_ID,
|
|
161
|
+
sessionId: process.env.MARROW_SESSION_ID,
|
|
162
|
+
mode: process.env.MARROW_ENFORCEMENT_MODE || 'auto',
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
const runtime = marrow.createPassiveRuntime({
|
|
166
|
+
includeValueReport: process.env.MARROW_PASSIVE_VALUE_REPORT === 'true',
|
|
167
|
+
valueReportPeriod: process.env.MARROW_VALUE_REPORT_PERIOD || '7d',
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
runtime.install();
|
|
171
|
+
globalThis.__MARROW_PASSIVE_RUNTIME__ = runtime;
|
|
172
|
+
}
|
|
173
|
+
`;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function envExample() {
|
|
177
|
+
return `MARROW_API_KEY=mrw_live_replace_me
|
|
178
|
+
MARROW_BASE_URL=${DEFAULT_BASE_URL}
|
|
179
|
+
MARROW_FLEET_AGENT_ID=agent-or-fleet-id
|
|
180
|
+
MARROW_ENFORCEMENT_MODE=auto
|
|
181
|
+
MARROW_PASSIVE_BRIEF=auto
|
|
182
|
+
`;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function parseJsonObject(filePath) {
|
|
186
|
+
const raw = safeRead(filePath).trim();
|
|
187
|
+
if (!raw) return {};
|
|
188
|
+
const parsed = JSON.parse(raw);
|
|
189
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
190
|
+
throw new Error(`Expected JSON object in ${filePath}`);
|
|
191
|
+
}
|
|
192
|
+
return parsed;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function upsertBlock(content, block) {
|
|
196
|
+
if (content.includes(MARROW_BLOCK_START) && content.includes(MARROW_BLOCK_END)) {
|
|
197
|
+
const start = content.indexOf(MARROW_BLOCK_START);
|
|
198
|
+
const end = content.indexOf(MARROW_BLOCK_END) + MARROW_BLOCK_END.length;
|
|
199
|
+
return `${content.slice(0, start)}${block}${content.slice(end)}`;
|
|
200
|
+
}
|
|
201
|
+
const separator = content && !content.endsWith('\n') ? '\n\n' : content ? '\n' : '';
|
|
202
|
+
return `${content}${separator}${block}\n`;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function upsertClaudeHooks(settingsPath) {
|
|
206
|
+
const settings = parseJsonObject(settingsPath);
|
|
207
|
+
const hooks = settings.hooks && typeof settings.hooks === 'object' && !Array.isArray(settings.hooks)
|
|
208
|
+
? settings.hooks
|
|
209
|
+
: {};
|
|
210
|
+
const postToolUse = Array.isArray(hooks.PostToolUse) ? [...hooks.PostToolUse] : [];
|
|
211
|
+
const userPromptSubmit = Array.isArray(hooks.UserPromptSubmit) ? [...hooks.UserPromptSubmit] : [];
|
|
212
|
+
|
|
213
|
+
const hasPost = postToolUse.some((entry) => JSON.stringify(entry).includes('npx -y @getmarrow/mcp hook'));
|
|
214
|
+
const hasPrompt = userPromptSubmit.some((entry) => JSON.stringify(entry).includes('npx -y @getmarrow/mcp context-hook'));
|
|
215
|
+
|
|
216
|
+
if (!hasPost) {
|
|
217
|
+
postToolUse.push({
|
|
218
|
+
matcher: 'Bash|Edit|Write|MultiEdit|mcp__(?!marrow_).*',
|
|
219
|
+
hooks: [{ type: 'command', command: 'npx -y @getmarrow/mcp hook' }],
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
if (!hasPrompt) {
|
|
223
|
+
userPromptSubmit.push({
|
|
224
|
+
hooks: [{ type: 'command', command: 'npx -y @getmarrow/mcp context-hook' }],
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
settings.hooks = {
|
|
229
|
+
...hooks,
|
|
230
|
+
PostToolUse: postToolUse,
|
|
231
|
+
UserPromptSubmit: userPromptSubmit,
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
return JSON.stringify(settings, null, 2) + '\n';
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function upsertMcpServerConfig(filePath) {
|
|
238
|
+
const config = parseJsonObject(filePath);
|
|
239
|
+
const servers = config.mcpServers && typeof config.mcpServers === 'object' && !Array.isArray(config.mcpServers)
|
|
240
|
+
? config.mcpServers
|
|
241
|
+
: {};
|
|
242
|
+
servers.marrow = {
|
|
243
|
+
command: 'npx',
|
|
244
|
+
args: ['-y', '@getmarrow/mcp'],
|
|
245
|
+
env: {
|
|
246
|
+
MARROW_API_KEY: '${MARROW_API_KEY}',
|
|
247
|
+
MARROW_BASE_URL: '${MARROW_BASE_URL}',
|
|
248
|
+
MARROW_FLEET_AGENT_ID: '${MARROW_FLEET_AGENT_ID}',
|
|
249
|
+
},
|
|
250
|
+
};
|
|
251
|
+
config.mcpServers = servers;
|
|
252
|
+
return JSON.stringify(config, null, 2) + '\n';
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function buildPlan(detection, options) {
|
|
256
|
+
const mode = options.mode === 'auto'
|
|
257
|
+
? detection.node && (detection.claudeCode || detection.cursor || detection.codex || detection.openclaw)
|
|
258
|
+
? 'both'
|
|
259
|
+
: detection.node
|
|
260
|
+
? 'sdk'
|
|
261
|
+
: 'mcp'
|
|
262
|
+
: options.mode;
|
|
263
|
+
const writes = [];
|
|
264
|
+
|
|
265
|
+
if (mode === 'sdk' || mode === 'both') {
|
|
266
|
+
writes.push({
|
|
267
|
+
type: 'file',
|
|
268
|
+
path: detection.paths.passiveRuntime,
|
|
269
|
+
label: 'SDK passive runtime preload',
|
|
270
|
+
content: passiveRuntimeSource(),
|
|
271
|
+
});
|
|
272
|
+
writes.push({
|
|
273
|
+
type: 'file',
|
|
274
|
+
path: detection.paths.passiveEnv,
|
|
275
|
+
label: 'Marrow passive env example',
|
|
276
|
+
content: envExample(),
|
|
277
|
+
overwrite: false,
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (mode === 'mcp' || mode === 'both') {
|
|
282
|
+
if (detection.claudeCode) {
|
|
283
|
+
writes.push({
|
|
284
|
+
type: 'json-transform',
|
|
285
|
+
path: detection.paths.claudeSettings,
|
|
286
|
+
label: 'Claude Code MCP passive hooks',
|
|
287
|
+
transform: upsertClaudeHooks,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
writes.push({
|
|
291
|
+
type: 'json-transform',
|
|
292
|
+
path: detection.paths.mcpJson,
|
|
293
|
+
label: 'Project MCP server config',
|
|
294
|
+
transform: upsertMcpServerConfig,
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (mode === 'md' || mode === 'both' || mode === 'mcp') {
|
|
299
|
+
writes.push({
|
|
300
|
+
type: 'md-block',
|
|
301
|
+
path: detection.paths.agentsMd,
|
|
302
|
+
label: 'Agent instructions',
|
|
303
|
+
block: passiveInstructions(),
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (detection.cursor && (mode === 'md' || mode === 'both' || mode === 'mcp')) {
|
|
308
|
+
writes.push({
|
|
309
|
+
type: 'file',
|
|
310
|
+
path: detection.paths.cursorRules,
|
|
311
|
+
label: 'Cursor Marrow rule',
|
|
312
|
+
content: passiveInstructions().replace(/<!--[^>]+-->/g, '').trim() + '\n',
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return { mode, writes };
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function applyPlan(plan, options) {
|
|
320
|
+
const changes = [];
|
|
321
|
+
for (const write of plan.writes) {
|
|
322
|
+
const before = safeRead(write.path);
|
|
323
|
+
let after;
|
|
324
|
+
if (write.type === 'file') {
|
|
325
|
+
if (write.overwrite === false && before) {
|
|
326
|
+
after = before;
|
|
327
|
+
} else {
|
|
328
|
+
after = write.content;
|
|
329
|
+
}
|
|
330
|
+
} else if (write.type === 'md-block') {
|
|
331
|
+
after = upsertBlock(before, write.block);
|
|
332
|
+
} else if (write.type === 'json-transform') {
|
|
333
|
+
after = write.transform(write.path);
|
|
334
|
+
} else {
|
|
335
|
+
throw new Error(`Unknown write type: ${write.type}`);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const changed = before !== after;
|
|
339
|
+
changes.push({ path: write.path, label: write.label, changed });
|
|
340
|
+
if (changed && options.yes && !options.dryRun) {
|
|
341
|
+
fs.mkdirSync(path.dirname(write.path), { recursive: true });
|
|
342
|
+
fs.writeFileSync(write.path, after);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
return changes;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
async function requestJson(url, options) {
|
|
349
|
+
const res = await fetch(url, options);
|
|
350
|
+
const text = await res.text();
|
|
351
|
+
let json = {};
|
|
352
|
+
try {
|
|
353
|
+
json = text ? JSON.parse(text) : {};
|
|
354
|
+
} catch {
|
|
355
|
+
json = { raw: text.slice(0, 500) };
|
|
356
|
+
}
|
|
357
|
+
if (!res.ok) {
|
|
358
|
+
const message = json.error || json.message || `HTTP ${res.status}`;
|
|
359
|
+
throw new Error(String(message));
|
|
360
|
+
}
|
|
361
|
+
return json.data || json;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
async function runSelfTest(options) {
|
|
365
|
+
if (!options.selfTest) return { skipped: true, reason: 'disabled' };
|
|
366
|
+
if (!options.apiKey) return { skipped: true, reason: 'missing MARROW_API_KEY' };
|
|
367
|
+
|
|
368
|
+
const headers = {
|
|
369
|
+
authorization: `Bearer ${options.apiKey}`,
|
|
370
|
+
'content-type': 'application/json',
|
|
371
|
+
'x-marrow-session-id': `install-${Date.now()}`,
|
|
372
|
+
};
|
|
373
|
+
if (options.agentId) headers['x-marrow-agent-id'] = options.agentId;
|
|
374
|
+
|
|
375
|
+
const baseUrl = options.baseUrl.replace(/\/+$/, '');
|
|
376
|
+
const think = await requestJson(`${baseUrl}/v1/agent/think`, {
|
|
377
|
+
method: 'POST',
|
|
378
|
+
headers,
|
|
379
|
+
body: JSON.stringify({
|
|
380
|
+
type: 'process',
|
|
381
|
+
action: 'Marrow passive install self-test: verify SDK/MCP hooks can record a harmless setup event',
|
|
382
|
+
}),
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
const decisionId = think.decision_id || think.decisionId;
|
|
386
|
+
if (!decisionId) throw new Error('self-test did not return decision_id');
|
|
387
|
+
|
|
388
|
+
await requestJson(`${baseUrl}/v1/agent/commit`, {
|
|
389
|
+
method: 'POST',
|
|
390
|
+
headers,
|
|
391
|
+
body: JSON.stringify({
|
|
392
|
+
decision_id: decisionId,
|
|
393
|
+
success: true,
|
|
394
|
+
outcome: 'Marrow passive installer self-test completed successfully',
|
|
395
|
+
}),
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
const status = await requestJson(`${baseUrl}/v1/agent/status`, { headers });
|
|
399
|
+
return {
|
|
400
|
+
skipped: false,
|
|
401
|
+
decision_id: decisionId,
|
|
402
|
+
active: Boolean(status.enabled ?? status.ok),
|
|
403
|
+
health: status.health || null,
|
|
404
|
+
last_event_at: status.last_event_at || null,
|
|
405
|
+
recommended_fix: status.recommended_fix || null,
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function printReport(report) {
|
|
410
|
+
process.stdout.write(`Marrow passive installer\n`);
|
|
411
|
+
process.stdout.write(`Root: ${report.root}\n`);
|
|
412
|
+
process.stdout.write(`Mode: ${report.mode}\n`);
|
|
413
|
+
process.stdout.write(`Write mode: ${report.writeMode}\n\n`);
|
|
414
|
+
|
|
415
|
+
process.stdout.write('Detected:\n');
|
|
416
|
+
for (const [key, value] of Object.entries(report.detected)) {
|
|
417
|
+
process.stdout.write(`- ${key}: ${value ? 'yes' : 'no'}\n`);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
process.stdout.write('\nPlanned changes:\n');
|
|
421
|
+
for (const change of report.changes) {
|
|
422
|
+
const marker = change.changed ? (report.writeMode === 'write' ? 'wrote' : 'would write') : 'unchanged';
|
|
423
|
+
process.stdout.write(`- ${marker}: ${change.label} (${change.path})\n`);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
process.stdout.write('\nSelf-test:\n');
|
|
427
|
+
if (report.selfTest.skipped) {
|
|
428
|
+
process.stdout.write(`- skipped: ${report.selfTest.reason}\n`);
|
|
429
|
+
} else {
|
|
430
|
+
process.stdout.write(`- active: ${report.selfTest.active ? 'yes' : 'no'}\n`);
|
|
431
|
+
process.stdout.write(`- decision_id: ${report.selfTest.decision_id}\n`);
|
|
432
|
+
process.stdout.write(`- health: ${report.selfTest.health || 'unknown'}\n`);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (report.writeMode === 'dry-run') {
|
|
436
|
+
process.stdout.write('\nRun with --yes to write these changes.\n');
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (report.warnings.length > 0) {
|
|
440
|
+
process.stdout.write('\nWarnings:\n');
|
|
441
|
+
for (const warning of report.warnings) {
|
|
442
|
+
process.stdout.write(`- ${warning}\n`);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
async function install(options) {
|
|
448
|
+
const detection = detectEnvironment(options.cwd);
|
|
449
|
+
const plan = buildPlan(detection, options);
|
|
450
|
+
const writeMode = options.yes && !options.dryRun ? 'write' : 'dry-run';
|
|
451
|
+
const changes = applyPlan(plan, options);
|
|
452
|
+
const selfTest = await runSelfTest(options).catch((error) => ({
|
|
453
|
+
skipped: false,
|
|
454
|
+
active: false,
|
|
455
|
+
error: error instanceof Error ? error.message : String(error),
|
|
456
|
+
}));
|
|
457
|
+
|
|
458
|
+
return {
|
|
459
|
+
root: detection.root,
|
|
460
|
+
mode: plan.mode,
|
|
461
|
+
writeMode,
|
|
462
|
+
detected: {
|
|
463
|
+
node: detection.node,
|
|
464
|
+
python: detection.python,
|
|
465
|
+
claudeCode: detection.claudeCode,
|
|
466
|
+
cursor: detection.cursor,
|
|
467
|
+
codex: detection.codex,
|
|
468
|
+
openclaw: detection.openclaw,
|
|
469
|
+
mcpConfig: detection.mcpConfig,
|
|
470
|
+
},
|
|
471
|
+
changes,
|
|
472
|
+
selfTest,
|
|
473
|
+
warnings: options.keyFromArg
|
|
474
|
+
? ['Avoid --key in shared shells because command-line arguments can be visible in process listings. Prefer MARROW_API_KEY in your environment or secret manager.']
|
|
475
|
+
: [],
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
async function runCli(argv) {
|
|
480
|
+
const options = parseArgs(argv);
|
|
481
|
+
if (options.help) {
|
|
482
|
+
process.stdout.write(usage());
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
const report = await install(options);
|
|
486
|
+
if (options.json) {
|
|
487
|
+
process.stdout.write(JSON.stringify(report, null, 2) + '\n');
|
|
488
|
+
} else {
|
|
489
|
+
printReport(report);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
module.exports = {
|
|
494
|
+
parseArgs,
|
|
495
|
+
detectEnvironment,
|
|
496
|
+
buildPlan,
|
|
497
|
+
applyPlan,
|
|
498
|
+
install,
|
|
499
|
+
runSelfTest,
|
|
500
|
+
runCli,
|
|
501
|
+
passiveRuntimeSource,
|
|
502
|
+
};
|