@distributionos/cli 0.1.16 → 0.1.18

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.16",
3
+ "version": "0.1.18",
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
@@ -1,4 +1,8 @@
1
- import { CLI_LOCAL_VERSION, DEFAULT_API_BASE } from './constants.js';
1
+ import { spawn } from 'node:child_process';
2
+ import { existsSync, readdirSync, statSync } from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { CLI_LOCAL_VERSION, DEFAULT_API_BASE } from './constants.js';
2
6
  import {
3
7
  ensureAnalyticsSite,
4
8
  reportSetupInstallation,
@@ -13,6 +17,10 @@ import { submitInitialization } from './initialization.js';
13
17
  import { compareValidationResults, inspectInstallArtifacts, runValidationPlan } from './validation.js';
14
18
  import { buildSetupReport } from './setup-report.js';
15
19
 
20
+ const DISTRIBUTIONOS_MCP_URL = 'https://distributionos.dev/api/mcp';
21
+ const DISTRIBUTIONOS_MCP_NAME = 'distributionos';
22
+ const AGENT_CHOICES = new Set(['codex', 'claude', 'terminal', 'other']);
23
+
16
24
  export async function runCli(argv, runtime) {
17
25
  const stdout = runtime.stdout;
18
26
  const stderr = runtime.stderr;
@@ -136,6 +144,9 @@ export async function runCli(argv, runtime) {
136
144
  return 1;
137
145
  }
138
146
 
147
+ const agentSetup = await ensureRequestedAgentSetup({ args, runtime, stdout, stderr });
148
+ if (!agentSetup.ok) return 1;
149
+
139
150
  if (!args.noFetch) {
140
151
  const authReady = await ensureCliAuth({ args, runtime, stderr });
141
152
  if (!authReady) return 1;
@@ -239,11 +250,12 @@ export async function runCli(argv, runtime) {
239
250
  phase: 'apply_completed',
240
251
  details: {
241
252
  changes,
242
- validation,
243
- inspection,
244
- requiresAgentRestart: changes.some((change) => (change.type === 'mcp' || change.type === 'codex-mcp') && change.changed),
245
- },
246
- });
253
+ validation,
254
+ inspection,
255
+ requiresAgentRestart: Boolean(agentSetup.requiresAgentRestart) ||
256
+ changes.some((change) => (change.type === 'mcp' || change.type === 'codex-mcp') && change.changed),
257
+ },
258
+ });
247
259
  const token = await resolveAuthToken({
248
260
  env: runtime.env,
249
261
  apiBase: args.apiBase ?? DEFAULT_API_BASE,
@@ -347,7 +359,9 @@ export function parseArgs(rawArgs) {
347
359
  noOpen: false,
348
360
  noLogin: false,
349
361
  skipSetupReport: false,
362
+ skipAgentSetup: false,
350
363
  yes: false,
364
+ agent: null,
351
365
  url: null,
352
366
  contentId: null,
353
367
  contractVersion: null,
@@ -383,7 +397,10 @@ export function parseArgs(rawArgs) {
383
397
  else if (arg === '--no-open') parsed.noOpen = true;
384
398
  else if (arg === '--no-login') parsed.noLogin = true;
385
399
  else if (arg === '--skip-setup-report') parsed.skipSetupReport = true;
400
+ else if (arg === '--skip-agent-setup') parsed.skipAgentSetup = true;
386
401
  else if (arg === '--yes' || arg === '-y') parsed.yes = true;
402
+ else if (arg === '--agent') parsed.agent = parseAgent(args[++index] ?? null);
403
+ else if (arg.startsWith('--agent=')) parsed.agent = parseAgent(arg.slice('--agent='.length));
387
404
  else if (arg === '--app') parsed.appId = args[++index] ?? null;
388
405
  else if (arg.startsWith('--app=')) parsed.appId = arg.slice('--app='.length);
389
406
  else if (arg === '--cwd') parsed.cwd = args[++index] ?? null;
@@ -417,13 +434,14 @@ function helpText() {
417
434
  'DistributionOS CLI',
418
435
  '',
419
436
  'Usage:',
420
- ' distributionos setup --app <appId> [--api-base <url>] [--cwd <path>] [--json] [--no-fetch]',
421
- ' distributionos setup --app <appId> --apply [--domain <domain>] [--allow-dirty] [--skip-setup-report]',
437
+ ' distributionos setup --app <appId> [--agent codex|claude|terminal|other] [--api-base <url>] [--cwd <path>] [--json] [--no-fetch]',
438
+ ' distributionos setup --app <appId> --apply [--agent codex|claude|terminal|other] [--domain <domain>] [--allow-dirty] [--skip-agent-setup] [--skip-setup-report]',
422
439
  ' distributionos login --app <appId> [--api-base <url>]',
423
440
  ' distributionos verify --app <appId> --url <liveUrl> [--content-id <id>]',
424
441
  ' distributionos report-implementation --app <appId> --artifact <artifactId> [--url <liveUrl>] [--summary <text>]',
425
442
  '',
426
443
  'Default mode reviews the setup plan without changing files. Run again with --apply after the plan looks right.',
444
+ 'For Codex, --agent codex registers the DistributionOS MCP server, uses the Codex Desktop CLI when available, and runs MCP login before repo setup.',
427
445
  'If the repo has existing uncommitted changes, commit/stash first or use --allow-dirty when you recognize them.',
428
446
  '',
429
447
  'Authentication:',
@@ -434,6 +452,262 @@ function helpText() {
434
452
  ].join('\n');
435
453
  }
436
454
 
455
+ function parseAgent(value) {
456
+ const normalized = typeof value === 'string' ? value.trim().toLowerCase() : '';
457
+ if (!AGENT_CHOICES.has(normalized)) {
458
+ throw new Error('Invalid --agent value. Use codex, claude, terminal, or other.');
459
+ }
460
+ return normalized;
461
+ }
462
+
463
+ async function ensureRequestedAgentSetup({ args, runtime, stdout, stderr }) {
464
+ if (args.skipAgentSetup || args.agent !== 'codex') {
465
+ return { ok: true, requiresAgentRestart: false };
466
+ }
467
+
468
+ stdout.write('Codex MCP setup\n');
469
+ const codexCommand = resolveCodexCommand(runtime);
470
+ stdout.write(`- Using ${codexCommand.label}.\n`);
471
+ const getResult = await runCodexCommand(runtime, codexCommand, ['mcp', 'get', DISTRIBUTIONOS_MCP_NAME]);
472
+ if (getResult.ok) {
473
+ if (!getResult.stdout.includes(DISTRIBUTIONOS_MCP_URL)) {
474
+ stderr.write([
475
+ 'Codex already has a distributionos MCP server, but it does not point to the expected DistributionOS URL.',
476
+ `Expected: ${DISTRIBUTIONOS_MCP_URL}`,
477
+ `Codex CLI used: ${codexCommand.command}`,
478
+ 'Remove the old distributionos MCP server from Codex, then rerun this setup command.',
479
+ '',
480
+ ].join('\n'));
481
+ return { ok: false, requiresAgentRestart: false };
482
+ }
483
+ stdout.write('- DistributionOS MCP server is already registered in Codex.\n');
484
+ } else if (isCommandMissing(getResult.error)) {
485
+ stderr.write([
486
+ 'Codex CLI was not found, so DistributionOS cannot connect Codex automatically.',
487
+ 'Open this repo in Codex Desktop or install the Codex CLI, then rerun setup.',
488
+ '',
489
+ ].join('\n'));
490
+ return { ok: false, requiresAgentRestart: false };
491
+ } else {
492
+ stdout.write('- Adding DistributionOS MCP server to Codex.\n');
493
+ const addResult = await runCodexCommand(runtime, codexCommand, ['mcp', 'add', DISTRIBUTIONOS_MCP_NAME, '--url', DISTRIBUTIONOS_MCP_URL], { stream: true });
494
+ if (!addResult.ok) {
495
+ stderr.write(formatCodexCommandFailure('Codex MCP add', addResult, codexCommand));
496
+ return { ok: false, requiresAgentRestart: false };
497
+ }
498
+ }
499
+
500
+ stdout.write('- Starting Codex MCP OAuth. Approve the browser sign-in if Codex asks.\n');
501
+ const loginResult = await runCodexCommand(runtime, codexCommand, ['mcp', 'login', DISTRIBUTIONOS_MCP_NAME], { stream: true });
502
+ if (!loginResult.ok) {
503
+ stderr.write(formatCodexCommandFailure('Codex MCP login', loginResult, codexCommand));
504
+ return { ok: false, requiresAgentRestart: true };
505
+ }
506
+ const listResult = await runCodexCommand(runtime, codexCommand, ['mcp', 'list']);
507
+ if (!listResult.ok) {
508
+ stderr.write(formatCodexCommandFailure('Codex MCP verification', listResult, codexCommand));
509
+ return { ok: false, requiresAgentRestart: true };
510
+ }
511
+ if (!codexMcpListShowsDistributionOs(listResult.stdout)) {
512
+ stderr.write([
513
+ 'Codex MCP login finished, but this Codex runtime does not list DistributionOS yet.',
514
+ `Codex CLI used: ${codexCommand.command}`,
515
+ 'Fully quit and reopen Codex Desktop, then rerun the Codex setup message from the app repo.',
516
+ '',
517
+ ].join('\n'));
518
+ return { ok: false, requiresAgentRestart: true };
519
+ }
520
+ if (codexMcpListShowsDistributionOsNotLoggedIn(listResult.stdout)) {
521
+ stderr.write([
522
+ 'Codex MCP login finished, but this Codex runtime still reports DistributionOS as Not logged in.',
523
+ `Codex CLI used: ${codexCommand.command}`,
524
+ 'Rerun the Codex setup message and approve the current browser sign-in while the command is waiting.',
525
+ '',
526
+ ].join('\n'));
527
+ return { ok: false, requiresAgentRestart: true };
528
+ }
529
+ stdout.write('- Codex MCP OAuth step completed and verified.\n\n');
530
+ return { ok: true, requiresAgentRestart: true };
531
+ }
532
+
533
+ function formatCodexCommandFailure(label, result, codexCommand) {
534
+ const output = oneLine(result.stderr || result.stdout || result.error?.message || '');
535
+ return [
536
+ `${label} did not complete, so Codex cannot use DistributionOS tools yet.`,
537
+ output ? `Last output: ${output}` : '',
538
+ codexCommand?.command ? `Codex CLI used: ${codexCommand.command}` : '',
539
+ 'Rerun the Codex setup message, approve the browser sign-in, and wait for the command to say the Codex MCP OAuth step completed and verified.',
540
+ '',
541
+ ].filter(Boolean).join('\n');
542
+ }
543
+
544
+ function isCommandMissing(error) {
545
+ const message = error?.message ?? '';
546
+ const stderr = error?.stderr ?? '';
547
+ return (
548
+ error?.code === 'ENOENT' ||
549
+ error?.code === 'EPERM' ||
550
+ /spawn\s+(ENOENT|EPERM)/i.test(message) ||
551
+ /not recognized as an internal/i.test(message) ||
552
+ /not recognized as an internal/i.test(stderr)
553
+ );
554
+ }
555
+
556
+ function resolveCodexCommand(runtime) {
557
+ const env = runtime?.env ?? process.env;
558
+ const configured = typeof env.CODEX_CLI_PATH === 'string' ? env.CODEX_CLI_PATH.trim() : '';
559
+ if (configured) {
560
+ return { command: configured, label: `Codex CLI at ${configured}` };
561
+ }
562
+
563
+ const desktopCommand = findCodexDesktopCommand(runtime);
564
+ if (desktopCommand) {
565
+ return { command: desktopCommand, label: `Codex Desktop CLI at ${desktopCommand}` };
566
+ }
567
+
568
+ return { command: 'codex', label: 'Codex CLI from PATH' };
569
+ }
570
+
571
+ function findCodexDesktopCommand(runtime) {
572
+ if (Array.isArray(runtime?.codexCommandCandidates)) {
573
+ return runtime.codexCommandCandidates.find((candidate) => typeof candidate === 'string' && candidate.trim()) ?? null;
574
+ }
575
+
576
+ const env = runtime?.env ?? process.env;
577
+ const homeDir = typeof env.USERPROFILE === 'string' && env.USERPROFILE
578
+ ? env.USERPROFILE
579
+ : os.homedir();
580
+ const localAppData = typeof env.LOCALAPPDATA === 'string' && env.LOCALAPPDATA
581
+ ? env.LOCALAPPDATA
582
+ : path.join(homeDir, 'AppData', 'Local');
583
+ const codexBinRoot = path.join(localAppData, 'OpenAI', 'Codex', 'bin');
584
+ const executableName = process.platform === 'win32' ? 'codex.exe' : 'codex';
585
+ const candidates = [];
586
+
587
+ const directCandidate = path.join(codexBinRoot, executableName);
588
+ if (existsSync(directCandidate)) {
589
+ candidates.push(directCandidate);
590
+ }
591
+
592
+ try {
593
+ for (const entry of readdirSync(codexBinRoot, { withFileTypes: true })) {
594
+ if (!entry.isDirectory()) continue;
595
+ const candidate = path.join(codexBinRoot, entry.name, executableName);
596
+ if (existsSync(candidate)) {
597
+ candidates.push(candidate);
598
+ }
599
+ }
600
+ } catch {
601
+ return candidates[0] ?? null;
602
+ }
603
+
604
+ candidates.sort((left, right) => fileModifiedAt(right) - fileModifiedAt(left));
605
+ return candidates[0] ?? null;
606
+ }
607
+
608
+ function fileModifiedAt(filePath) {
609
+ try {
610
+ return statSync(filePath).mtimeMs;
611
+ } catch {
612
+ return 0;
613
+ }
614
+ }
615
+
616
+ function codexMcpListShowsDistributionOs(output) {
617
+ return Boolean(findCodexMcpListDistributionOsLine(output));
618
+ }
619
+
620
+ function codexMcpListShowsDistributionOsNotLoggedIn(output) {
621
+ const line = findCodexMcpListDistributionOsLine(output);
622
+ return Boolean(line && /not\s+logged\s+in/i.test(line));
623
+ }
624
+
625
+ function findCodexMcpListDistributionOsLine(output) {
626
+ return String(output)
627
+ .split(/\r?\n/)
628
+ .map((line) => line.trim())
629
+ .find((line) => /\bdistributionos\b/i.test(line)) ?? null;
630
+ }
631
+
632
+ async function runCodexCommand(runtime, codexCommand, args, options = {}) {
633
+ return runExternalCommand(runtime, codexCommand.command, args, options);
634
+ }
635
+
636
+ async function runExternalCommand(runtime, command, args, options = {}) {
637
+ if (typeof runtime.execFile === 'function') {
638
+ try {
639
+ const result = await runtime.execFile(command, args, options);
640
+ return {
641
+ ok: true,
642
+ stdout: result?.stdout ?? '',
643
+ stderr: result?.stderr ?? '',
644
+ };
645
+ } catch (error) {
646
+ return {
647
+ ok: false,
648
+ stdout: error?.stdout ?? '',
649
+ stderr: error?.stderr ?? '',
650
+ error,
651
+ };
652
+ }
653
+ }
654
+
655
+ const resolved = resolveCommand(command, args, runtime);
656
+ return new Promise((resolve) => {
657
+ let stdoutText = '';
658
+ let stderrText = '';
659
+ let child;
660
+ try {
661
+ child = spawn(resolved.command, resolved.args, {
662
+ cwd: runtime.cwd,
663
+ env: runtime.env,
664
+ windowsHide: true,
665
+ stdio: ['inherit', 'pipe', 'pipe'],
666
+ });
667
+ } catch (error) {
668
+ resolve({ ok: false, stdout: '', stderr: '', error });
669
+ return;
670
+ }
671
+
672
+ child.stdout?.on('data', (chunk) => {
673
+ const text = String(chunk);
674
+ stdoutText += text;
675
+ if (options.stream) runtime.stdout?.write(text);
676
+ });
677
+ child.stderr?.on('data', (chunk) => {
678
+ const text = String(chunk);
679
+ stderrText += text;
680
+ if (options.stream) runtime.stderr?.write(text);
681
+ });
682
+ child.on('error', (error) => {
683
+ resolve({ ok: false, stdout: stdoutText, stderr: stderrText, error });
684
+ });
685
+ child.on('close', (code) => {
686
+ const ok = code === 0;
687
+ resolve({
688
+ ok,
689
+ stdout: stdoutText,
690
+ stderr: stderrText,
691
+ error: ok ? null : Object.assign(new Error(`${command} exited with code ${code}`), { code }),
692
+ });
693
+ });
694
+ });
695
+ }
696
+
697
+ function resolveCommand(command, args, runtime) {
698
+ const platform = runtime.platform ?? process.platform;
699
+ if (platform === 'win32') {
700
+ if (path.isAbsolute(command) && /\.(exe|com)$/i.test(command)) {
701
+ return { command, args };
702
+ }
703
+ return {
704
+ command: runtime.env?.ComSpec ?? process.env.ComSpec ?? 'cmd.exe',
705
+ args: ['/d', '/s', '/c', command, ...args],
706
+ };
707
+ }
708
+ return { command, args };
709
+ }
710
+
437
711
  async function ensureCliAuth({ args, runtime, stderr }) {
438
712
  const existingToken = await resolveAuthToken({
439
713
  env: runtime.env,
@@ -586,10 +860,11 @@ function formatApplyResult(plan, changes, initialization, validation, inspection
586
860
  lines.push(
587
861
  '- Open this DistributionOS review page now:',
588
862
  ` ${reviewUrl}`,
589
- '- Codex users: make sure `codex mcp add distributionos --url https://distributionos.dev/api/mcp` and `codex mcp login distributionos` have run, then fully quit and reopen Codex Desktop before checking the connection.',
863
+ '- Codex users: if you used the Codex setup option, the CLI already ran MCP add/login against the Codex runtime and verified it did not report DistributionOS as Not logged in.',
864
+ '- Then fully quit and reopen Codex Desktop before checking the connection.',
590
865
  '- Claude Code users: if .mcp.json was created or updated, restart or reconnect Claude Code and approve the project MCP server if asked.',
591
- '- Other MCP-aware agents: reconnect the agent from this repo so it can load the DistributionOS MCP server.',
592
- '- On the review page, copy the connection check prompt into the fresh/reconnected agent chat.',
866
+ '- Other MCP-aware agents: reconnect the agent from this repo so it can load the DistributionOS MCP server.',
867
+ '- On the review page, copy the connection check prompt into the fresh/reconnected agent chat.',
593
868
  '- If your agent only sees DistributionOS authentication tools, copy the auth prompt from the review page and complete OAuth before continuing.',
594
869
  '- Then copy the verification prompt from the review page and ask your agent to call check_distributionos_connection, get_agent_instructions, and get_analytics_summary.',
595
870
  '- DistributionOS will wait for a successful app-scoped MCP call before you continue to Brain Doc review.',
package/src/constants.js CHANGED
@@ -8,7 +8,7 @@ export const DEFAULT_API_BASE = 'https://distributionos.dev';
8
8
  export const MCP_SERVER_URL = 'https://distributionos.dev/api/mcp';
9
9
  export const CURRENT_BOOTSTRAP_VERSION = '2026.06.27';
10
10
  export const CURRENT_ANALYTICS_CONTRACT_VERSION = '2026.06.20';
11
- export const CURRENT_SETUP_CONTRACT_VERSION = '2026.07.01';
11
+ export const CURRENT_SETUP_CONTRACT_VERSION = '2026.07.01';
12
12
 
13
13
  export const TOKEN_ENV_NAMES = [
14
14
  'DISTRIBUTIONOS_API_KEY',
@@ -35,24 +35,24 @@ export async function buildSetupReport(plan, phase = 'plan', details = {}) {
35
35
  reason: plan.bootstrap.reason,
36
36
  });
37
37
 
38
- if (plan.mcp?.action === 'none') {
39
- detectedCapabilities.add('agent.mcp.remoteConfig');
40
- }
41
- if (plan.codexMcp?.action === 'none') {
42
- detectedCapabilities.add('agent.mcp.codexConfig');
43
- }
44
- if (plan.codexMcp?.action === 'manual') {
45
- blockedReason = plan.codexMcp.reason;
46
- } else if (plan.mcp?.action === 'manual') {
47
- blockedReason = plan.mcp.reason;
48
- }
49
- if (plan.codexMcp) {
50
- managedFiles.push({
51
- path: plan.codexMcp.path,
52
- type: 'agent.mcp.codexConfig',
53
- action: plan.codexMcp.action,
54
- status: plan.codexMcp.action === 'none' ? 'current' : plan.codexMcp.action === 'manual' ? 'blocked_manual' : 'planned',
55
- reason: plan.codexMcp.reason,
38
+ if (plan.mcp?.action === 'none') {
39
+ detectedCapabilities.add('agent.mcp.remoteConfig');
40
+ }
41
+ if (plan.codexMcp?.action === 'none') {
42
+ detectedCapabilities.add('agent.mcp.codexConfig');
43
+ }
44
+ if (plan.codexMcp?.action === 'manual') {
45
+ blockedReason = plan.codexMcp.reason;
46
+ } else if (plan.mcp?.action === 'manual') {
47
+ blockedReason = plan.mcp.reason;
48
+ }
49
+ if (plan.codexMcp) {
50
+ managedFiles.push({
51
+ path: plan.codexMcp.path,
52
+ type: 'agent.mcp.codexConfig',
53
+ action: plan.codexMcp.action,
54
+ status: plan.codexMcp.action === 'none' ? 'current' : plan.codexMcp.action === 'manual' ? 'blocked_manual' : 'planned',
55
+ reason: plan.codexMcp.reason,
56
56
  });
57
57
  }
58
58
  if (plan.mcp) {