@distributionos/cli 0.1.9 → 0.1.11
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/package.json +1 -1
- package/src/cli.js +2 -0
- package/src/initialization.js +9 -1
- package/src/mcp-config.js +164 -0
- package/src/mutate.js +13 -9
- package/src/plan.js +42 -23
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -508,6 +508,8 @@ function formatApplyResult(plan, changes, initialization, validation, inspection
|
|
|
508
508
|
'- Open this DistributionOS review page now:',
|
|
509
509
|
` ${reviewUrl}`,
|
|
510
510
|
'- Review the generated Brain Doc and Brain Vault context before asking your agent to keep working.',
|
|
511
|
+
'- If .mcp.json was created or updated, restart or reconnect your agent session so it can load the DistributionOS MCP server.',
|
|
512
|
+
'- After reconnecting, ask your agent: "Check DistributionOS connection and tell me today\'s analytics summary."',
|
|
511
513
|
'- After review, come back to this repo and inspect git diff before committing, pushing, or deploying.',
|
|
512
514
|
);
|
|
513
515
|
} else {
|
package/src/initialization.js
CHANGED
|
@@ -4,7 +4,9 @@ import path from 'node:path';
|
|
|
4
4
|
export async function buildInitializationPayload(plan, options = {}) {
|
|
5
5
|
const appName = titleFromPackageName(plan.repo.packageJson?.name ?? plan.appId);
|
|
6
6
|
const docs = await readProjectDocs(plan.cwd);
|
|
7
|
-
const
|
|
7
|
+
const routes = plan.repo.routes ?? [];
|
|
8
|
+
const publicBlogRoute = routes.find(route => route === '/blog' || route.startsWith('/blog/'));
|
|
9
|
+
const routePrivacy = plan.repo.routePrivacy ?? { allowed: [], blocked: [], review: [] };
|
|
8
10
|
const appIdentity = {
|
|
9
11
|
name: appName,
|
|
10
12
|
};
|
|
@@ -19,6 +21,12 @@ export async function buildInitializationPayload(plan, options = {}) {
|
|
|
19
21
|
frameworks: [plan.repo.framework].filter(value => value && value !== 'unknown'),
|
|
20
22
|
languages: ['TypeScript', 'JavaScript'],
|
|
21
23
|
packageManagers: [plan.repo.packageManager].filter(value => value && value !== 'unknown'),
|
|
24
|
+
routes: routes.slice(0, 80),
|
|
25
|
+
routePrivacy: {
|
|
26
|
+
allowed: (routePrivacy.allowed ?? []).slice(0, 40),
|
|
27
|
+
blocked: (routePrivacy.blocked ?? []).slice(0, 40),
|
|
28
|
+
review: (routePrivacy.review ?? []).slice(0, 40),
|
|
29
|
+
},
|
|
22
30
|
notableFiles: [
|
|
23
31
|
'package.json',
|
|
24
32
|
...plan.repo.layoutCandidates,
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { MCP_SERVER_URL } from './constants.js';
|
|
4
|
+
import { pathExists } from './detect.js';
|
|
5
|
+
|
|
6
|
+
export const PROJECT_MCP_CONFIG_PATH = '.mcp.json';
|
|
7
|
+
export const DISTRIBUTIONOS_MCP_SERVER_NAME = 'distributionos';
|
|
8
|
+
|
|
9
|
+
export async function buildProjectMcpConfigPlan(cwd, serverUrl = MCP_SERVER_URL) {
|
|
10
|
+
const target = safeResolve(cwd, PROJECT_MCP_CONFIG_PATH);
|
|
11
|
+
const base = {
|
|
12
|
+
path: PROJECT_MCP_CONFIG_PATH,
|
|
13
|
+
serverName: DISTRIBUTIONOS_MCP_SERVER_NAME,
|
|
14
|
+
serverUrl,
|
|
15
|
+
authMode: 'oauth',
|
|
16
|
+
requiresRestart: true,
|
|
17
|
+
writesSecrets: false,
|
|
18
|
+
recommendedConfig: recommendedMcpConfig(serverUrl),
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
if (!(await pathExists(target))) {
|
|
22
|
+
return {
|
|
23
|
+
...base,
|
|
24
|
+
supported: true,
|
|
25
|
+
action: 'create',
|
|
26
|
+
reason: 'Create a project-local MCP config with the DistributionOS remote server URL only.',
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let parsed;
|
|
31
|
+
try {
|
|
32
|
+
parsed = JSON.parse(await fs.readFile(target, 'utf8'));
|
|
33
|
+
} catch {
|
|
34
|
+
return {
|
|
35
|
+
...base,
|
|
36
|
+
supported: false,
|
|
37
|
+
action: 'manual',
|
|
38
|
+
reason: 'Existing .mcp.json is not valid JSON, so the CLI will not edit it automatically.',
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!isRecord(parsed)) {
|
|
43
|
+
return {
|
|
44
|
+
...base,
|
|
45
|
+
supported: false,
|
|
46
|
+
action: 'manual',
|
|
47
|
+
reason: 'Existing .mcp.json is not an object, so the CLI will not edit it automatically.',
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (containsSecretLikeConfig(parsed)) {
|
|
52
|
+
return {
|
|
53
|
+
...base,
|
|
54
|
+
supported: false,
|
|
55
|
+
action: 'manual',
|
|
56
|
+
reason: 'Existing .mcp.json appears to contain secret headers, tokens, or env values. The CLI will not rewrite it.',
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const servers = isRecord(parsed.mcpServers) ? parsed.mcpServers : {};
|
|
61
|
+
const existing = servers[DISTRIBUTIONOS_MCP_SERVER_NAME];
|
|
62
|
+
const existingUrl = isRecord(existing) && typeof existing.url === 'string'
|
|
63
|
+
? existing.url
|
|
64
|
+
: null;
|
|
65
|
+
|
|
66
|
+
if (existingUrl === serverUrl) {
|
|
67
|
+
return {
|
|
68
|
+
...base,
|
|
69
|
+
supported: true,
|
|
70
|
+
action: 'none',
|
|
71
|
+
reason: 'Project-local MCP config already includes the DistributionOS server.',
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (existingUrl) {
|
|
76
|
+
return {
|
|
77
|
+
...base,
|
|
78
|
+
supported: false,
|
|
79
|
+
action: 'manual',
|
|
80
|
+
reason: 'Existing distributionos MCP server points to a different URL. The CLI will not overwrite it automatically.',
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
...base,
|
|
86
|
+
supported: true,
|
|
87
|
+
action: 'update',
|
|
88
|
+
reason: 'Add the DistributionOS remote MCP server to the existing project-local MCP config.',
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export async function applyProjectMcpConfig(cwd, plan) {
|
|
93
|
+
if (!plan || plan.action === 'none') {
|
|
94
|
+
return { type: 'mcp', changed: false, file: plan?.path ?? PROJECT_MCP_CONFIG_PATH, reason: plan?.reason ?? 'No MCP config change planned.' };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (plan.action === 'manual' || !plan.supported) {
|
|
98
|
+
return { type: 'mcp', changed: false, file: plan.path, reason: plan.reason };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const target = safeResolve(cwd, plan.path);
|
|
102
|
+
let existing = {};
|
|
103
|
+
if (await pathExists(target)) {
|
|
104
|
+
existing = JSON.parse(await fs.readFile(target, 'utf8'));
|
|
105
|
+
}
|
|
106
|
+
if (!isRecord(existing)) {
|
|
107
|
+
return { type: 'mcp', changed: false, file: plan.path, reason: 'Existing MCP config was not an object.' };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const next = {
|
|
111
|
+
...existing,
|
|
112
|
+
mcpServers: {
|
|
113
|
+
...(isRecord(existing.mcpServers) ? existing.mcpServers : {}),
|
|
114
|
+
[DISTRIBUTIONOS_MCP_SERVER_NAME]: {
|
|
115
|
+
url: plan.serverUrl ?? MCP_SERVER_URL,
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
await fs.mkdir(path.dirname(target), { recursive: true });
|
|
121
|
+
await fs.writeFile(target, `${JSON.stringify(next, null, 2)}\n`, 'utf8');
|
|
122
|
+
return {
|
|
123
|
+
type: 'mcp',
|
|
124
|
+
changed: true,
|
|
125
|
+
file: plan.path,
|
|
126
|
+
action: plan.action === 'create' ? 'created' : 'updated',
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function recommendedMcpConfig(serverUrl) {
|
|
131
|
+
return {
|
|
132
|
+
mcpServers: {
|
|
133
|
+
[DISTRIBUTIONOS_MCP_SERVER_NAME]: {
|
|
134
|
+
url: serverUrl,
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function containsSecretLikeConfig(value) {
|
|
141
|
+
if (Array.isArray(value)) return value.some(containsSecretLikeConfig);
|
|
142
|
+
if (!isRecord(value)) return false;
|
|
143
|
+
for (const [key, nested] of Object.entries(value)) {
|
|
144
|
+
if (/authorization|api[-_]?key|token|secret|password|credential|headers|env/i.test(key)) {
|
|
145
|
+
if (nested !== null && nested !== undefined && nested !== '') return true;
|
|
146
|
+
}
|
|
147
|
+
if (containsSecretLikeConfig(nested)) return true;
|
|
148
|
+
}
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function isRecord(value) {
|
|
153
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function safeResolve(cwd, relativePath) {
|
|
157
|
+
const root = path.resolve(cwd);
|
|
158
|
+
const target = path.resolve(root, relativePath);
|
|
159
|
+
const relative = path.relative(root, target);
|
|
160
|
+
if (relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
161
|
+
throw new Error(`Refusing to write outside repo root: ${relativePath}`);
|
|
162
|
+
}
|
|
163
|
+
return target;
|
|
164
|
+
}
|
package/src/mutate.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { promises as fs } from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { applyProjectMcpConfig } from './mcp-config.js';
|
|
3
4
|
|
|
4
5
|
const BOOTSTRAP_BLOCK_RE =
|
|
5
6
|
/<!--\s*DISTRIBUTIONOS:START[\s\S]*?DISTRIBUTIONOS:END\s*-->/i;
|
|
@@ -17,13 +18,16 @@ export async function applySetupPlan(plan, options = {}) {
|
|
|
17
18
|
const bootstrap = await applyBootstrap(plan.cwd, plan.bootstrap);
|
|
18
19
|
if (bootstrap.changed) changes.push(bootstrap);
|
|
19
20
|
|
|
20
|
-
if (!options.skipAnalytics) {
|
|
21
|
-
const analytics = await applyAnalytics(plan.cwd, plan.analytics);
|
|
22
|
-
if (analytics.changed || analytics.reason) changes.push(analytics);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
21
|
+
if (!options.skipAnalytics) {
|
|
22
|
+
const analytics = await applyAnalytics(plan.cwd, plan.analytics);
|
|
23
|
+
if (analytics.changed || analytics.reason) changes.push(analytics);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const mcp = await applyProjectMcpConfig(plan.cwd, plan.mcp);
|
|
27
|
+
if (mcp.changed || mcp.reason) changes.push(mcp);
|
|
28
|
+
|
|
29
|
+
return changes;
|
|
30
|
+
}
|
|
27
31
|
|
|
28
32
|
async function applyBootstrap(cwd, bootstrap) {
|
|
29
33
|
const targetPath = safeResolve(cwd, bootstrap.target);
|
package/src/plan.js
CHANGED
|
@@ -10,10 +10,11 @@ import {
|
|
|
10
10
|
buildSetupCommand,
|
|
11
11
|
getBootstrapAction,
|
|
12
12
|
} from './bootstrap.js';
|
|
13
|
-
import { scanRepo } from './detect.js';
|
|
14
|
-
import {
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
import { scanRepo } from './detect.js';
|
|
14
|
+
import { buildProjectMcpConfigPlan } from './mcp-config.js';
|
|
15
|
+
import {
|
|
16
|
+
buildAnalyticsRouteConfigScript,
|
|
17
|
+
buildAnalyticsRouteConfigSnippet,
|
|
17
18
|
} from './privacy.js';
|
|
18
19
|
|
|
19
20
|
export async function createSetupPlan(options) {
|
|
@@ -33,10 +34,11 @@ export async function createSetupPlan(options) {
|
|
|
33
34
|
remote.instructions?.recommendedBootstrapBlock ??
|
|
34
35
|
buildLocalBootstrapBlock(instructionPackVersion, appId);
|
|
35
36
|
const bootstrap = getBootstrapAction(repo.instructionFiles, recommendedBootstrapBlock);
|
|
36
|
-
const analyticsContract = remote.analyticsContract;
|
|
37
|
-
const planRepo = sanitizeRepoForPlan(repo);
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
const analyticsContract = remote.analyticsContract;
|
|
38
|
+
const planRepo = sanitizeRepoForPlan(repo);
|
|
39
|
+
const mcp = await buildProjectMcpConfigPlan(cwd);
|
|
40
|
+
|
|
41
|
+
return {
|
|
40
42
|
appId,
|
|
41
43
|
cwd,
|
|
42
44
|
mode: options.apply ? 'apply' : 'review',
|
|
@@ -47,15 +49,17 @@ export async function createSetupPlan(options) {
|
|
|
47
49
|
repo: planRepo,
|
|
48
50
|
remote,
|
|
49
51
|
instructionPackVersion,
|
|
50
|
-
bootstrap,
|
|
51
|
-
|
|
52
|
+
bootstrap,
|
|
53
|
+
mcp,
|
|
54
|
+
analytics: buildAnalyticsPlan({ analyticsContract, repo: planRepo, instructionPackVersion }),
|
|
52
55
|
fallbackInstructions: buildFallbackMcpInstructions(appId),
|
|
53
56
|
warnings: buildWarnings({ repo: planRepo, remote }),
|
|
54
57
|
validation: buildValidationPlan(planRepo),
|
|
55
58
|
deploymentChecklist: [
|
|
56
59
|
'Review this plan and any proposed file edits.',
|
|
57
|
-
'Apply reviewed bootstrap and
|
|
58
|
-
'
|
|
60
|
+
'Apply reviewed bootstrap, analytics, and non-secret project MCP config changes only.',
|
|
61
|
+
'Restart or reconnect your agent if the project-local MCP config changed.',
|
|
62
|
+
'Run baseline tests before mutation when the worktree is messy.',
|
|
59
63
|
'Run build and test commands after mutation.',
|
|
60
64
|
'Do not deploy until a human reviews the diff.',
|
|
61
65
|
'After deployment, call verify_analytics_install on representative public URLs.',
|
|
@@ -113,11 +117,12 @@ export function formatPlanText(plan) {
|
|
|
113
117
|
...plan.remote.errors.map(error => `- ${error}`),
|
|
114
118
|
'',
|
|
115
119
|
'Planned changes',
|
|
116
|
-
`1. Agent bootstrap: ${plan.bootstrap.action} ${plan.bootstrap.target}`,
|
|
117
|
-
` ${plan.bootstrap.reason}`,
|
|
118
|
-
`2. Agent instructions: ${plan.remote.instructions ? 'use fetched current instruction pack' : 'use fallback MCP/OAuth instructions until authenticated'}`,
|
|
119
|
-
`3. Analytics: ${analyticsSummary(plan.analytics)}`,
|
|
120
|
-
|
|
120
|
+
`1. Agent bootstrap: ${plan.bootstrap.action} ${plan.bootstrap.target}`,
|
|
121
|
+
` ${plan.bootstrap.reason}`,
|
|
122
|
+
`2. Agent instructions: ${plan.remote.instructions ? 'use fetched current instruction pack' : 'use fallback MCP/OAuth instructions until authenticated'}`,
|
|
123
|
+
`3. Analytics: ${analyticsSummary(plan.analytics)}`,
|
|
124
|
+
`4. Agent MCP config: ${mcpSummary(plan.mcp)}`,
|
|
125
|
+
'',
|
|
121
126
|
'Route privacy review',
|
|
122
127
|
`- Allowed public candidates: ${formatList(plan.repo.routePrivacy.allowed, 8)}`,
|
|
123
128
|
`- Blocked private candidates: ${formatList(plan.repo.routePrivacy.blocked, 8)}`,
|
|
@@ -128,9 +133,15 @@ export function formatPlanText(plan) {
|
|
|
128
133
|
`- Test: ${plan.validation.test ?? 'not detected'}`,
|
|
129
134
|
`- Lint: ${plan.validation.lint ?? 'not detected'}`,
|
|
130
135
|
'',
|
|
131
|
-
'Fallback MCP/OAuth instructions',
|
|
132
|
-
...plan.fallbackInstructions.map(item => `- ${item}`),
|
|
133
|
-
'',
|
|
136
|
+
'Fallback MCP/OAuth instructions',
|
|
137
|
+
...plan.fallbackInstructions.map(item => `- ${item}`),
|
|
138
|
+
'',
|
|
139
|
+
'Agent live-data connection',
|
|
140
|
+
`- Project config: ${plan.mcp.path}`,
|
|
141
|
+
`- Planned action: ${plan.mcp.action}`,
|
|
142
|
+
`- Auth mode: ${plan.mcp.authMode}; no API keys or OAuth tokens are written.`,
|
|
143
|
+
`- ${plan.mcp.reason}`,
|
|
144
|
+
'',
|
|
134
145
|
'Deployment checklist',
|
|
135
146
|
...plan.deploymentChecklist.map(item => `- ${item}`),
|
|
136
147
|
];
|
|
@@ -175,7 +186,7 @@ function nextStepLines(plan) {
|
|
|
175
186
|
|
|
176
187
|
lines.push(
|
|
177
188
|
'',
|
|
178
|
-
'DistributionOS expects to edit only the managed agent instruction block and
|
|
189
|
+
'DistributionOS expects to edit only the managed agent instruction block, supported analytics layout, and non-secret project MCP config. Review git diff before committing or deploying.',
|
|
179
190
|
);
|
|
180
191
|
|
|
181
192
|
return lines;
|
|
@@ -307,7 +318,7 @@ function meterText(value) {
|
|
|
307
318
|
return `${value.used ?? 0}/${limit}`;
|
|
308
319
|
}
|
|
309
320
|
|
|
310
|
-
function analyticsSummary(analytics) {
|
|
321
|
+
function analyticsSummary(analytics) {
|
|
311
322
|
if (analytics.status === 'enabled') {
|
|
312
323
|
return `contract ${analytics.contractVersion}; install script in ${analytics.layoutTarget ?? 'reviewed shared layout'}`;
|
|
313
324
|
}
|
|
@@ -315,7 +326,15 @@ function analyticsSummary(analytics) {
|
|
|
315
326
|
return 'tracker will be created or fetched during apply before analytics is installed';
|
|
316
327
|
}
|
|
317
328
|
return 'contract not fetched; print manual guidance only';
|
|
318
|
-
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function mcpSummary(mcp) {
|
|
332
|
+
if (!mcp) return 'manual instructions only';
|
|
333
|
+
if (mcp.action === 'none') return `${mcp.path} already includes DistributionOS`;
|
|
334
|
+
if (mcp.action === 'create') return `create ${mcp.path} with DistributionOS MCP server URL`;
|
|
335
|
+
if (mcp.action === 'update') return `update ${mcp.path} with DistributionOS MCP server URL`;
|
|
336
|
+
return `manual setup needed for ${mcp.path}`;
|
|
337
|
+
}
|
|
319
338
|
|
|
320
339
|
function formatList(values, limit = 5) {
|
|
321
340
|
if (!values || values.length === 0) return 'none';
|