@distributionos/cli 0.1.10 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@distributionos/cli",
3
- "version": "0.1.10",
3
+ "version": "0.1.11",
4
4
  "description": "DistributionOS repo setup CLI for agent onboarding and first-party analytics.",
5
5
  "homepage": "https://distributionos.dev",
6
6
  "repository": {
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 {
@@ -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
- return changes;
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
- buildAnalyticsRouteConfigScript,
16
- buildAnalyticsRouteConfigSnippet,
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
- return {
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
- analytics: buildAnalyticsPlan({ analyticsContract, repo: planRepo, instructionPackVersion }),
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 analytics changes only.',
58
- 'Run baseline tests before mutation when the worktree is messy.',
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 the supported analytics layout. Review git diff before committing or deploying.',
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';