@distributionos/cli 0.1.17 → 0.1.19

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 CHANGED
@@ -22,8 +22,12 @@ distributionos setup --app <appId> --apply
22
22
  distributionos login --app <appId>
23
23
  distributionos verify --app <appId> --url <liveUrl> [--content-id <id>]
24
24
  distributionos report-implementation --app <appId> --artifact <artifactId> [--url <liveUrl>]
25
+ distributionos start-work --app <appId> [--task-type <type>] [--artifact <artifactId>] [--dedup-key <key>]
26
+ distributionos complete-work --app <appId> --work-session <id> --status <status> --summary <text> [--url <liveUrl>] [--file <path>] [--pr-url <url>]
25
27
  ```
26
28
 
29
+ `start-work` and `complete-work` are compact fallbacks for agents or terminals that cannot call the MCP `start_agent_work` / `complete_agent_work` tools directly. Use one work session per meaningful task, not per prompt.
30
+
27
31
  ## Safety
28
32
 
29
33
  - Uses DistributionOS MCP OAuth as the happy path.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@distributionos/cli",
3
- "version": "0.1.17",
3
+ "version": "0.1.19",
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/api.js CHANGED
@@ -252,6 +252,65 @@ export async function reportImplementation(input) {
252
252
  return body?.data ?? body;
253
253
  }
254
254
 
255
+ export async function startAgentWork(input) {
256
+ const token = await resolveAuthToken({
257
+ env: input.env,
258
+ apiBase: input.apiBase ?? DEFAULT_API_BASE,
259
+ appId: input.appId,
260
+ fetchImpl: input.fetch,
261
+ });
262
+ if (!token) {
263
+ throw new Error('No stored OAuth token or env token was available.');
264
+ }
265
+
266
+ const apiBase = normalizeApiBase(input.apiBase ?? DEFAULT_API_BASE);
267
+ const response = await input.fetch(`${apiBase}/api/v1/apps/${encodeURIComponent(input.appId)}/agent-work`, {
268
+ method: 'POST',
269
+ headers: {
270
+ Authorization: `Bearer ${token.value}`,
271
+ 'Content-Type': 'application/json',
272
+ Accept: 'application/json',
273
+ },
274
+ body: JSON.stringify(input.payload),
275
+ });
276
+ const body = await response.json().catch(() => null);
277
+ if (!response.ok || body?.success === false) {
278
+ throw new Error(body?.error || `Agent work start failed with HTTP ${response.status}`);
279
+ }
280
+ return body?.data ?? body;
281
+ }
282
+
283
+ export async function completeAgentWork(input) {
284
+ const token = await resolveAuthToken({
285
+ env: input.env,
286
+ apiBase: input.apiBase ?? DEFAULT_API_BASE,
287
+ appId: input.appId,
288
+ fetchImpl: input.fetch,
289
+ });
290
+ if (!token) {
291
+ throw new Error('No stored OAuth token or env token was available.');
292
+ }
293
+ if (!input.workSessionId) {
294
+ throw new Error('workSessionId is required.');
295
+ }
296
+
297
+ const apiBase = normalizeApiBase(input.apiBase ?? DEFAULT_API_BASE);
298
+ const response = await input.fetch(`${apiBase}/api/v1/apps/${encodeURIComponent(input.appId)}/agent-work/${encodeURIComponent(input.workSessionId)}/complete`, {
299
+ method: 'POST',
300
+ headers: {
301
+ Authorization: `Bearer ${token.value}`,
302
+ 'Content-Type': 'application/json',
303
+ Accept: 'application/json',
304
+ },
305
+ body: JSON.stringify(input.payload),
306
+ });
307
+ const body = await response.json().catch(() => null);
308
+ if (!response.ok || body?.success === false) {
309
+ throw new Error(body?.error || `Agent work closeout failed with HTTP ${response.status}`);
310
+ }
311
+ return body?.data ?? body;
312
+ }
313
+
255
314
  async function fetchJson({ fetchImpl, apiBase, token, path }) {
256
315
  if (typeof fetchImpl !== 'function') {
257
316
  throw new Error('fetch is unavailable in this Node runtime');
package/src/bootstrap.js CHANGED
@@ -22,9 +22,11 @@ export function buildLocalBootstrapBlock(version = CURRENT_BOOTSTRAP_VERSION, ap
22
22
  `<!-- DISTRIBUTIONOS:START version=${version}${appIdAttribute} -->`,
23
23
  '## DistributionOS Agent Sync',
24
24
  '',
25
- 'This repo is linked to DistributionOS.',
26
- '',
27
- 'Hard preflight: before editing files, creating assets, generating images, creating HyperFrames launch/demo videos, or changing copy for SEO, blog, launch, content, visual asset, competitor, social, email, or growth work, fetch the current instructions from DistributionOS:',
25
+ 'This repo is linked to DistributionOS.',
26
+ '',
27
+ 'Session preflight: when starting a fresh agent chat in this repo, call check_distributionos_connection first, then fetch the current instructions below. If DistributionOS returns setupUpdate or setupUpdateGuidance with a status other than current or not_installed, tell the user the repo has a setup update available, show the review command, and run the apply command only after the user approves.',
28
+ '',
29
+ 'Hard preflight: before editing files, creating assets, generating images, creating HyperFrames launch/demo videos, or changing copy for SEO, blog, launch, content, visual asset, competitor, social, email, or growth work, fetch the current instructions from DistributionOS:',
28
30
  '',
29
31
  instructionCall,
30
32
  '',
package/src/cli.js CHANGED
@@ -1,10 +1,15 @@
1
- import { spawn } from 'node:child_process';
2
- 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';
3
6
  import {
4
7
  ensureAnalyticsSite,
8
+ completeAgentWork,
5
9
  reportSetupInstallation,
6
10
  reportImplementation,
7
11
  resolveAuthToken,
12
+ startAgentWork,
8
13
  verifyAnalyticsInstall,
9
14
  } from './api.js';
10
15
  import { loginWithOAuth } from './oauth.js';
@@ -12,11 +17,11 @@ import { createSetupPlan, formatPlanText } from './plan.js';
12
17
  import { applySetupPlan } from './mutate.js';
13
18
  import { submitInitialization } from './initialization.js';
14
19
  import { compareValidationResults, inspectInstallArtifacts, runValidationPlan } from './validation.js';
15
- import { buildSetupReport } from './setup-report.js';
16
-
17
- const DISTRIBUTIONOS_MCP_URL = 'https://distributionos.dev/api/mcp';
18
- const DISTRIBUTIONOS_MCP_NAME = 'distributionos';
19
- const AGENT_CHOICES = new Set(['codex', 'claude', 'terminal', 'other']);
20
+ import { buildSetupReport } from './setup-report.js';
21
+
22
+ const DISTRIBUTIONOS_MCP_URL = 'https://distributionos.dev/api/mcp';
23
+ const DISTRIBUTIONOS_MCP_NAME = 'distributionos';
24
+ const AGENT_CHOICES = new Set(['codex', 'claude', 'terminal', 'other']);
20
25
 
21
26
  export async function runCli(argv, runtime) {
22
27
  const stdout = runtime.stdout;
@@ -129,24 +134,92 @@ export async function runCli(argv, runtime) {
129
134
  }
130
135
  }
131
136
 
137
+ if (args.command === 'start-work') {
138
+ if (!args.appId) {
139
+ stderr.write('Missing required --app <appId>.\n\n');
140
+ stderr.write(helpText());
141
+ return 1;
142
+ }
143
+
144
+ const authReady = await ensureCliAuth({ args, runtime, stderr });
145
+ if (!authReady) return 1;
146
+
147
+ try {
148
+ const result = await startAgentWork({
149
+ appId: args.appId,
150
+ apiBase: args.apiBase ?? DEFAULT_API_BASE,
151
+ env: runtime.env,
152
+ fetch: runtime.fetch,
153
+ payload: buildStartWorkPayload(args),
154
+ });
155
+ stdout.write(args.json ? `${JSON.stringify(result, null, 2)}\n` : formatStartWorkResult(result));
156
+ return 0;
157
+ } catch (error) {
158
+ stderr.write(`${error instanceof Error ? error.message : 'Agent work start failed'}\n`);
159
+ return 1;
160
+ }
161
+ }
162
+
163
+ if (args.command === 'complete-work') {
164
+ if (!args.appId) {
165
+ stderr.write('Missing required --app <appId>.\n\n');
166
+ stderr.write(helpText());
167
+ return 1;
168
+ }
169
+ if (!args.workSessionId) {
170
+ stderr.write('Missing required --work-session <workSessionId>.\n\n');
171
+ stderr.write(helpText());
172
+ return 1;
173
+ }
174
+ if (!args.status) {
175
+ stderr.write('Missing required --status <completed|shipped|reported|blocked|skipped|no_op|failed>.\n\n');
176
+ stderr.write(helpText());
177
+ return 1;
178
+ }
179
+ if (args.status !== 'no_op' && !args.summary) {
180
+ stderr.write('Missing required --summary <text> unless --status no_op is used.\n\n');
181
+ stderr.write(helpText());
182
+ return 1;
183
+ }
184
+
185
+ const authReady = await ensureCliAuth({ args, runtime, stderr });
186
+ if (!authReady) return 1;
187
+
188
+ try {
189
+ const result = await completeAgentWork({
190
+ appId: args.appId,
191
+ workSessionId: args.workSessionId,
192
+ apiBase: args.apiBase ?? DEFAULT_API_BASE,
193
+ env: runtime.env,
194
+ fetch: runtime.fetch,
195
+ payload: buildCompleteWorkPayload(args),
196
+ });
197
+ stdout.write(args.json ? `${JSON.stringify(result, null, 2)}\n` : formatCompleteWorkResult(result));
198
+ return 0;
199
+ } catch (error) {
200
+ stderr.write(`${error instanceof Error ? error.message : 'Agent work closeout failed'}\n`);
201
+ return 1;
202
+ }
203
+ }
204
+
132
205
  if (args.command !== 'setup') {
133
206
  stderr.write(`Unknown command: ${args.command}\n\n`);
134
207
  stderr.write(helpText());
135
208
  return 1;
136
209
  }
137
210
 
138
- if (!args.appId) {
139
- stderr.write('Missing required --app <appId>.\n\n');
140
- stderr.write(helpText());
141
- return 1;
142
- }
143
-
144
- const agentSetup = await ensureRequestedAgentSetup({ args, runtime, stdout, stderr });
145
- if (!agentSetup.ok) return 1;
146
-
147
- if (!args.noFetch) {
148
- const authReady = await ensureCliAuth({ args, runtime, stderr });
149
- if (!authReady) return 1;
211
+ if (!args.appId) {
212
+ stderr.write('Missing required --app <appId>.\n\n');
213
+ stderr.write(helpText());
214
+ return 1;
215
+ }
216
+
217
+ const agentSetup = await ensureRequestedAgentSetup({ args, runtime, stdout, stderr });
218
+ if (!agentSetup.ok) return 1;
219
+
220
+ if (!args.noFetch) {
221
+ const authReady = await ensureCliAuth({ args, runtime, stderr });
222
+ if (!authReady) return 1;
150
223
  }
151
224
 
152
225
  let plan = await createSetupPlan({
@@ -247,12 +320,12 @@ export async function runCli(argv, runtime) {
247
320
  phase: 'apply_completed',
248
321
  details: {
249
322
  changes,
250
- validation,
251
- inspection,
252
- requiresAgentRestart: Boolean(agentSetup.requiresAgentRestart) ||
253
- changes.some((change) => (change.type === 'mcp' || change.type === 'codex-mcp') && change.changed),
254
- },
255
- });
323
+ validation,
324
+ inspection,
325
+ requiresAgentRestart: Boolean(agentSetup.requiresAgentRestart) ||
326
+ changes.some((change) => (change.type === 'mcp' || change.type === 'codex-mcp') && change.changed),
327
+ },
328
+ });
256
329
  const token = await resolveAuthToken({
257
330
  env: runtime.env,
258
331
  apiBase: args.apiBase ?? DEFAULT_API_BASE,
@@ -354,17 +427,32 @@ export function parseArgs(rawArgs) {
354
427
  skipAnalytics: false,
355
428
  skipValidate: false,
356
429
  noOpen: false,
357
- noLogin: false,
358
- skipSetupReport: false,
359
- skipAgentSetup: false,
360
- yes: false,
361
- agent: null,
362
- url: null,
430
+ noLogin: false,
431
+ skipSetupReport: false,
432
+ skipAgentSetup: false,
433
+ yes: false,
434
+ agent: null,
435
+ url: null,
363
436
  contentId: null,
364
437
  contractVersion: null,
365
438
  artifactId: null,
439
+ sourceArtifactId: null,
440
+ workSessionId: null,
441
+ taskType: null,
442
+ dedupKey: null,
366
443
  status: null,
367
444
  summary: null,
445
+ commitSha: null,
446
+ prUrl: null,
447
+ deploymentUrl: null,
448
+ deploymentId: null,
449
+ implementedType: null,
450
+ changedFiles: [],
451
+ changedRoutes: [],
452
+ liveUrls: [],
453
+ unknowns: [],
454
+ risks: [],
455
+ nextSteps: [],
368
456
  analyticsOptOutReason: null,
369
457
  help: false,
370
458
  version: false,
@@ -391,13 +479,13 @@ export function parseArgs(rawArgs) {
391
479
  else if (arg === '--allow-dirty') parsed.allowDirty = true;
392
480
  else if (arg === '--skip-analytics') parsed.skipAnalytics = true;
393
481
  else if (arg === '--skip-validate') parsed.skipValidate = true;
394
- else if (arg === '--no-open') parsed.noOpen = true;
395
- else if (arg === '--no-login') parsed.noLogin = true;
396
- else if (arg === '--skip-setup-report') parsed.skipSetupReport = true;
397
- else if (arg === '--skip-agent-setup') parsed.skipAgentSetup = true;
398
- else if (arg === '--yes' || arg === '-y') parsed.yes = true;
399
- else if (arg === '--agent') parsed.agent = parseAgent(args[++index] ?? null);
400
- else if (arg.startsWith('--agent=')) parsed.agent = parseAgent(arg.slice('--agent='.length));
482
+ else if (arg === '--no-open') parsed.noOpen = true;
483
+ else if (arg === '--no-login') parsed.noLogin = true;
484
+ else if (arg === '--skip-setup-report') parsed.skipSetupReport = true;
485
+ else if (arg === '--skip-agent-setup') parsed.skipAgentSetup = true;
486
+ else if (arg === '--yes' || arg === '-y') parsed.yes = true;
487
+ else if (arg === '--agent') parsed.agent = parseAgent(args[++index] ?? null);
488
+ else if (arg.startsWith('--agent=')) parsed.agent = parseAgent(arg.slice('--agent='.length));
401
489
  else if (arg === '--app') parsed.appId = args[++index] ?? null;
402
490
  else if (arg.startsWith('--app=')) parsed.appId = arg.slice('--app='.length);
403
491
  else if (arg === '--cwd') parsed.cwd = args[++index] ?? null;
@@ -414,10 +502,41 @@ export function parseArgs(rawArgs) {
414
502
  else if (arg.startsWith('--contract-version=')) parsed.contractVersion = arg.slice('--contract-version='.length);
415
503
  else if (arg === '--artifact') parsed.artifactId = args[++index] ?? null;
416
504
  else if (arg.startsWith('--artifact=')) parsed.artifactId = arg.slice('--artifact='.length);
505
+ else if (arg === '--source-artifact') parsed.sourceArtifactId = args[++index] ?? null;
506
+ else if (arg.startsWith('--source-artifact=')) parsed.sourceArtifactId = arg.slice('--source-artifact='.length);
507
+ else if (arg === '--work-session' || arg === '--session') parsed.workSessionId = args[++index] ?? null;
508
+ else if (arg.startsWith('--work-session=')) parsed.workSessionId = arg.slice('--work-session='.length);
509
+ else if (arg.startsWith('--session=')) parsed.workSessionId = arg.slice('--session='.length);
510
+ else if (arg === '--task-type') parsed.taskType = args[++index] ?? null;
511
+ else if (arg.startsWith('--task-type=')) parsed.taskType = arg.slice('--task-type='.length);
512
+ else if (arg === '--dedup-key') parsed.dedupKey = args[++index] ?? null;
513
+ else if (arg.startsWith('--dedup-key=')) parsed.dedupKey = arg.slice('--dedup-key='.length);
417
514
  else if (arg === '--status') parsed.status = args[++index] ?? null;
418
515
  else if (arg.startsWith('--status=')) parsed.status = arg.slice('--status='.length);
419
516
  else if (arg === '--summary') parsed.summary = args[++index] ?? null;
420
517
  else if (arg.startsWith('--summary=')) parsed.summary = arg.slice('--summary='.length);
518
+ else if (arg === '--commit') parsed.commitSha = args[++index] ?? null;
519
+ else if (arg.startsWith('--commit=')) parsed.commitSha = arg.slice('--commit='.length);
520
+ else if (arg === '--pr-url') parsed.prUrl = args[++index] ?? null;
521
+ else if (arg.startsWith('--pr-url=')) parsed.prUrl = arg.slice('--pr-url='.length);
522
+ else if (arg === '--deployment-url') parsed.deploymentUrl = args[++index] ?? null;
523
+ else if (arg.startsWith('--deployment-url=')) parsed.deploymentUrl = arg.slice('--deployment-url='.length);
524
+ else if (arg === '--deployment-id') parsed.deploymentId = args[++index] ?? null;
525
+ else if (arg.startsWith('--deployment-id=')) parsed.deploymentId = arg.slice('--deployment-id='.length);
526
+ else if (arg === '--implemented-type') parsed.implementedType = args[++index] ?? null;
527
+ else if (arg.startsWith('--implemented-type=')) parsed.implementedType = arg.slice('--implemented-type='.length);
528
+ else if (arg === '--file') parsed.changedFiles.push(args[++index] ?? '');
529
+ else if (arg.startsWith('--file=')) parsed.changedFiles.push(arg.slice('--file='.length));
530
+ else if (arg === '--route') parsed.changedRoutes.push(args[++index] ?? '');
531
+ else if (arg.startsWith('--route=')) parsed.changedRoutes.push(arg.slice('--route='.length));
532
+ else if (arg === '--live-url') parsed.liveUrls.push(args[++index] ?? '');
533
+ else if (arg.startsWith('--live-url=')) parsed.liveUrls.push(arg.slice('--live-url='.length));
534
+ else if (arg === '--unknown') parsed.unknowns.push(args[++index] ?? '');
535
+ else if (arg.startsWith('--unknown=')) parsed.unknowns.push(arg.slice('--unknown='.length));
536
+ else if (arg === '--risk') parsed.risks.push(args[++index] ?? '');
537
+ else if (arg.startsWith('--risk=')) parsed.risks.push(arg.slice('--risk='.length));
538
+ else if (arg === '--next-step') parsed.nextSteps.push(args[++index] ?? '');
539
+ else if (arg.startsWith('--next-step=')) parsed.nextSteps.push(arg.slice('--next-step='.length));
421
540
  else if (arg === '--analytics-opt-out') parsed.analyticsOptOutReason = args[++index] ?? null;
422
541
  else if (arg.startsWith('--analytics-opt-out=')) parsed.analyticsOptOutReason = arg.slice('--analytics-opt-out='.length);
423
542
  else throw new Error(`Unknown argument: ${arg}`);
@@ -426,178 +545,286 @@ export function parseArgs(rawArgs) {
426
545
  return parsed;
427
546
  }
428
547
 
429
- function helpText() {
430
- return [
431
- 'DistributionOS CLI',
432
- '',
433
- 'Usage:',
434
- ' distributionos setup --app <appId> [--agent codex|claude|terminal|other] [--api-base <url>] [--cwd <path>] [--json] [--no-fetch]',
435
- ' distributionos setup --app <appId> --apply [--agent codex|claude|terminal|other] [--domain <domain>] [--allow-dirty] [--skip-agent-setup] [--skip-setup-report]',
548
+ function helpText() {
549
+ return [
550
+ 'DistributionOS CLI',
551
+ '',
552
+ 'Usage:',
553
+ ' distributionos setup --app <appId> [--agent codex|claude|terminal|other] [--api-base <url>] [--cwd <path>] [--json] [--no-fetch]',
554
+ ' distributionos setup --app <appId> --apply [--agent codex|claude|terminal|other] [--domain <domain>] [--allow-dirty] [--skip-agent-setup] [--skip-setup-report]',
436
555
  ' distributionos login --app <appId> [--api-base <url>]',
437
556
  ' distributionos verify --app <appId> --url <liveUrl> [--content-id <id>]',
438
557
  ' distributionos report-implementation --app <appId> --artifact <artifactId> [--url <liveUrl>] [--summary <text>]',
558
+ ' distributionos start-work --app <appId> [--task-type <type>] [--artifact <artifactId>] [--dedup-key <key>] [--summary <text>]',
559
+ ' distributionos complete-work --app <appId> --work-session <id> --status <status> --summary <text> [--url <liveUrl>] [--file <path>] [--pr-url <url>]',
439
560
  '',
440
- 'Default mode reviews the setup plan without changing files. Run again with --apply after the plan looks right.',
441
- 'For Codex, --agent codex registers the DistributionOS MCP server and runs codex mcp login before repo setup.',
442
- 'If the repo has existing uncommitted changes, commit/stash first or use --allow-dirty when you recognize them.',
561
+ 'Default mode reviews the setup plan without changing files. Run again with --apply after the plan looks right.',
562
+ 'For Codex, --agent codex registers the DistributionOS MCP server, uses the Codex Desktop CLI when available, and runs MCP login before repo setup.',
563
+ 'If the repo has existing uncommitted changes, commit/stash first or use --allow-dirty when you recognize them.',
443
564
  '',
444
565
  'Authentication:',
445
566
  ' Happy path uses DistributionOS MCP OAuth and stores app-scoped credentials outside the repo.',
446
567
  ' DISTRIBUTIONOS_API_KEY, DOS_API_KEY, or DISTRIBUTIONOS_TOKEN are advanced fallbacks.',
447
568
  ' Tokens are never printed or written to committed files.',
448
569
  '',
449
- ].join('\n');
450
- }
451
-
452
- function parseAgent(value) {
453
- const normalized = typeof value === 'string' ? value.trim().toLowerCase() : '';
454
- if (!AGENT_CHOICES.has(normalized)) {
455
- throw new Error('Invalid --agent value. Use codex, claude, terminal, or other.');
456
- }
457
- return normalized;
458
- }
459
-
460
- async function ensureRequestedAgentSetup({ args, runtime, stdout, stderr }) {
461
- if (args.skipAgentSetup || args.agent !== 'codex') {
462
- return { ok: true, requiresAgentRestart: false };
463
- }
464
-
465
- stdout.write('Codex MCP setup\n');
466
- const getResult = await runCodexCommand(runtime, ['mcp', 'get', DISTRIBUTIONOS_MCP_NAME]);
467
- if (getResult.ok) {
468
- if (!getResult.stdout.includes(DISTRIBUTIONOS_MCP_URL)) {
469
- stderr.write([
470
- 'Codex already has a distributionos MCP server, but it does not point to the expected DistributionOS URL.',
471
- `Expected: ${DISTRIBUTIONOS_MCP_URL}`,
472
- 'Run `codex mcp remove distributionos`, then rerun this setup command.',
473
- '',
474
- ].join('\n'));
475
- return { ok: false, requiresAgentRestart: false };
476
- }
477
- stdout.write('- DistributionOS MCP server is already registered in Codex.\n');
478
- } else if (isCommandMissing(getResult.error)) {
479
- stderr.write([
480
- 'Codex CLI was not found, so DistributionOS cannot connect Codex automatically.',
481
- 'Open this repo in Codex Desktop or install the Codex CLI, then rerun setup.',
482
- '',
483
- ].join('\n'));
484
- return { ok: false, requiresAgentRestart: false };
485
- } else {
486
- stdout.write('- Adding DistributionOS MCP server to Codex.\n');
487
- const addResult = await runCodexCommand(runtime, ['mcp', 'add', DISTRIBUTIONOS_MCP_NAME, '--url', DISTRIBUTIONOS_MCP_URL], { stream: true });
488
- if (!addResult.ok) {
489
- stderr.write(formatCodexCommandFailure('codex mcp add distributionos', addResult));
490
- return { ok: false, requiresAgentRestart: false };
491
- }
492
- }
493
-
494
- stdout.write('- Starting Codex MCP OAuth. Approve the browser sign-in if Codex asks.\n');
495
- const loginResult = await runCodexCommand(runtime, ['mcp', 'login', DISTRIBUTIONOS_MCP_NAME], { stream: true });
496
- if (!loginResult.ok) {
497
- stderr.write(formatCodexCommandFailure('codex mcp login distributionos', loginResult));
498
- return { ok: false, requiresAgentRestart: true };
499
- }
500
- stdout.write('- Codex MCP OAuth step completed.\n\n');
501
- return { ok: true, requiresAgentRestart: true };
502
- }
503
-
504
- function formatCodexCommandFailure(label, result) {
505
- const output = oneLine(result.stderr || result.stdout || result.error?.message || '');
506
- return [
507
- `${label} did not complete, so Codex cannot use DistributionOS tools yet.`,
508
- output ? `Last output: ${output}` : '',
509
- 'Run the command above manually, approve the browser sign-in, then rerun DistributionOS setup.',
510
- '',
511
- ].filter(Boolean).join('\n');
512
- }
513
-
514
- function isCommandMissing(error) {
515
- const message = error?.message ?? '';
516
- const stderr = error?.stderr ?? '';
517
- return (
518
- error?.code === 'ENOENT' ||
519
- error?.code === 'EPERM' ||
520
- /spawn\s+(ENOENT|EPERM)/i.test(message) ||
521
- /not recognized as an internal/i.test(message) ||
522
- /not recognized as an internal/i.test(stderr)
523
- );
524
- }
525
-
526
- async function runCodexCommand(runtime, args, options = {}) {
527
- return runExternalCommand(runtime, 'codex', args, options);
528
- }
529
-
530
- async function runExternalCommand(runtime, command, args, options = {}) {
531
- if (typeof runtime.execFile === 'function') {
532
- try {
533
- const result = await runtime.execFile(command, args, options);
534
- return {
535
- ok: true,
536
- stdout: result?.stdout ?? '',
537
- stderr: result?.stderr ?? '',
538
- };
539
- } catch (error) {
540
- return {
541
- ok: false,
542
- stdout: error?.stdout ?? '',
543
- stderr: error?.stderr ?? '',
544
- error,
545
- };
546
- }
547
- }
548
-
549
- const resolved = resolveCommand(command, args, runtime);
550
- return new Promise((resolve) => {
551
- let stdoutText = '';
552
- let stderrText = '';
553
- let child;
554
- try {
555
- child = spawn(resolved.command, resolved.args, {
556
- cwd: runtime.cwd,
557
- env: runtime.env,
558
- windowsHide: true,
559
- stdio: ['inherit', 'pipe', 'pipe'],
560
- });
561
- } catch (error) {
562
- resolve({ ok: false, stdout: '', stderr: '', error });
563
- return;
564
- }
565
-
566
- child.stdout?.on('data', (chunk) => {
567
- const text = String(chunk);
568
- stdoutText += text;
569
- if (options.stream) runtime.stdout?.write(text);
570
- });
571
- child.stderr?.on('data', (chunk) => {
572
- const text = String(chunk);
573
- stderrText += text;
574
- if (options.stream) runtime.stderr?.write(text);
575
- });
576
- child.on('error', (error) => {
577
- resolve({ ok: false, stdout: stdoutText, stderr: stderrText, error });
578
- });
579
- child.on('close', (code) => {
580
- const ok = code === 0;
581
- resolve({
582
- ok,
583
- stdout: stdoutText,
584
- stderr: stderrText,
585
- error: ok ? null : Object.assign(new Error(`${command} exited with code ${code}`), { code }),
586
- });
587
- });
588
- });
589
- }
590
-
591
- function resolveCommand(command, args, runtime) {
592
- const platform = runtime.platform ?? process.platform;
593
- if (platform === 'win32') {
594
- return {
595
- command: runtime.env?.ComSpec ?? process.env.ComSpec ?? 'cmd.exe',
596
- args: ['/d', '/s', '/c', command, ...args],
597
- };
598
- }
599
- return { command, args };
600
- }
570
+ ].join('\n');
571
+ }
572
+
573
+ function parseAgent(value) {
574
+ const normalized = typeof value === 'string' ? value.trim().toLowerCase() : '';
575
+ if (!AGENT_CHOICES.has(normalized)) {
576
+ throw new Error('Invalid --agent value. Use codex, claude, terminal, or other.');
577
+ }
578
+ return normalized;
579
+ }
580
+
581
+ async function ensureRequestedAgentSetup({ args, runtime, stdout, stderr }) {
582
+ if (args.skipAgentSetup || args.agent !== 'codex') {
583
+ return { ok: true, requiresAgentRestart: false };
584
+ }
585
+
586
+ stdout.write('Codex MCP setup\n');
587
+ const codexCommand = resolveCodexCommand(runtime);
588
+ stdout.write(`- Using ${codexCommand.label}.\n`);
589
+ const getResult = await runCodexCommand(runtime, codexCommand, ['mcp', 'get', DISTRIBUTIONOS_MCP_NAME]);
590
+ if (getResult.ok) {
591
+ if (!getResult.stdout.includes(DISTRIBUTIONOS_MCP_URL)) {
592
+ stderr.write([
593
+ 'Codex already has a distributionos MCP server, but it does not point to the expected DistributionOS URL.',
594
+ `Expected: ${DISTRIBUTIONOS_MCP_URL}`,
595
+ `Codex CLI used: ${codexCommand.command}`,
596
+ 'Remove the old distributionos MCP server from Codex, then rerun this setup command.',
597
+ '',
598
+ ].join('\n'));
599
+ return { ok: false, requiresAgentRestart: false };
600
+ }
601
+ stdout.write('- DistributionOS MCP server is already registered in Codex.\n');
602
+ } else if (isCommandMissing(getResult.error)) {
603
+ stderr.write([
604
+ 'Codex CLI was not found, so DistributionOS cannot connect Codex automatically.',
605
+ 'Open this repo in Codex Desktop or install the Codex CLI, then rerun setup.',
606
+ '',
607
+ ].join('\n'));
608
+ return { ok: false, requiresAgentRestart: false };
609
+ } else {
610
+ stdout.write('- Adding DistributionOS MCP server to Codex.\n');
611
+ const addResult = await runCodexCommand(runtime, codexCommand, ['mcp', 'add', DISTRIBUTIONOS_MCP_NAME, '--url', DISTRIBUTIONOS_MCP_URL], { stream: true });
612
+ if (!addResult.ok) {
613
+ stderr.write(formatCodexCommandFailure('Codex MCP add', addResult, codexCommand));
614
+ return { ok: false, requiresAgentRestart: false };
615
+ }
616
+ }
617
+
618
+ stdout.write('- Starting Codex MCP OAuth. Approve the browser sign-in if Codex asks.\n');
619
+ const loginResult = await runCodexCommand(runtime, codexCommand, ['mcp', 'login', DISTRIBUTIONOS_MCP_NAME], { stream: true });
620
+ if (!loginResult.ok) {
621
+ stderr.write(formatCodexCommandFailure('Codex MCP login', loginResult, codexCommand));
622
+ return { ok: false, requiresAgentRestart: true };
623
+ }
624
+ const listResult = await runCodexCommand(runtime, codexCommand, ['mcp', 'list']);
625
+ if (!listResult.ok) {
626
+ stderr.write(formatCodexCommandFailure('Codex MCP verification', listResult, codexCommand));
627
+ return { ok: false, requiresAgentRestart: true };
628
+ }
629
+ if (!codexMcpListShowsDistributionOs(listResult.stdout)) {
630
+ stderr.write([
631
+ 'Codex MCP login finished, but this Codex runtime does not list DistributionOS yet.',
632
+ `Codex CLI used: ${codexCommand.command}`,
633
+ 'Fully quit and reopen Codex Desktop, then rerun the Codex setup message from the app repo.',
634
+ '',
635
+ ].join('\n'));
636
+ return { ok: false, requiresAgentRestart: true };
637
+ }
638
+ if (codexMcpListShowsDistributionOsNotLoggedIn(listResult.stdout)) {
639
+ stderr.write([
640
+ 'Codex MCP login finished, but this Codex runtime still reports DistributionOS as Not logged in.',
641
+ `Codex CLI used: ${codexCommand.command}`,
642
+ 'Rerun the Codex setup message and approve the current browser sign-in while the command is waiting.',
643
+ '',
644
+ ].join('\n'));
645
+ return { ok: false, requiresAgentRestart: true };
646
+ }
647
+ stdout.write('- Codex MCP OAuth step completed and verified.\n\n');
648
+ return { ok: true, requiresAgentRestart: true };
649
+ }
650
+
651
+ function formatCodexCommandFailure(label, result, codexCommand) {
652
+ const output = oneLine(result.stderr || result.stdout || result.error?.message || '');
653
+ return [
654
+ `${label} did not complete, so Codex cannot use DistributionOS tools yet.`,
655
+ output ? `Last output: ${output}` : '',
656
+ codexCommand?.command ? `Codex CLI used: ${codexCommand.command}` : '',
657
+ '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.',
658
+ '',
659
+ ].filter(Boolean).join('\n');
660
+ }
661
+
662
+ function isCommandMissing(error) {
663
+ const message = error?.message ?? '';
664
+ const stderr = error?.stderr ?? '';
665
+ return (
666
+ error?.code === 'ENOENT' ||
667
+ error?.code === 'EPERM' ||
668
+ /spawn\s+(ENOENT|EPERM)/i.test(message) ||
669
+ /not recognized as an internal/i.test(message) ||
670
+ /not recognized as an internal/i.test(stderr)
671
+ );
672
+ }
673
+
674
+ function resolveCodexCommand(runtime) {
675
+ const env = runtime?.env ?? process.env;
676
+ const configured = typeof env.CODEX_CLI_PATH === 'string' ? env.CODEX_CLI_PATH.trim() : '';
677
+ if (configured) {
678
+ return { command: configured, label: `Codex CLI at ${configured}` };
679
+ }
680
+
681
+ const desktopCommand = findCodexDesktopCommand(runtime);
682
+ if (desktopCommand) {
683
+ return { command: desktopCommand, label: `Codex Desktop CLI at ${desktopCommand}` };
684
+ }
685
+
686
+ return { command: 'codex', label: 'Codex CLI from PATH' };
687
+ }
688
+
689
+ function findCodexDesktopCommand(runtime) {
690
+ if (Array.isArray(runtime?.codexCommandCandidates)) {
691
+ return runtime.codexCommandCandidates.find((candidate) => typeof candidate === 'string' && candidate.trim()) ?? null;
692
+ }
693
+
694
+ const env = runtime?.env ?? process.env;
695
+ const homeDir = typeof env.USERPROFILE === 'string' && env.USERPROFILE
696
+ ? env.USERPROFILE
697
+ : os.homedir();
698
+ const localAppData = typeof env.LOCALAPPDATA === 'string' && env.LOCALAPPDATA
699
+ ? env.LOCALAPPDATA
700
+ : path.join(homeDir, 'AppData', 'Local');
701
+ const codexBinRoot = path.join(localAppData, 'OpenAI', 'Codex', 'bin');
702
+ const executableName = process.platform === 'win32' ? 'codex.exe' : 'codex';
703
+ const candidates = [];
704
+
705
+ const directCandidate = path.join(codexBinRoot, executableName);
706
+ if (existsSync(directCandidate)) {
707
+ candidates.push(directCandidate);
708
+ }
709
+
710
+ try {
711
+ for (const entry of readdirSync(codexBinRoot, { withFileTypes: true })) {
712
+ if (!entry.isDirectory()) continue;
713
+ const candidate = path.join(codexBinRoot, entry.name, executableName);
714
+ if (existsSync(candidate)) {
715
+ candidates.push(candidate);
716
+ }
717
+ }
718
+ } catch {
719
+ return candidates[0] ?? null;
720
+ }
721
+
722
+ candidates.sort((left, right) => fileModifiedAt(right) - fileModifiedAt(left));
723
+ return candidates[0] ?? null;
724
+ }
725
+
726
+ function fileModifiedAt(filePath) {
727
+ try {
728
+ return statSync(filePath).mtimeMs;
729
+ } catch {
730
+ return 0;
731
+ }
732
+ }
733
+
734
+ function codexMcpListShowsDistributionOs(output) {
735
+ return Boolean(findCodexMcpListDistributionOsLine(output));
736
+ }
737
+
738
+ function codexMcpListShowsDistributionOsNotLoggedIn(output) {
739
+ const line = findCodexMcpListDistributionOsLine(output);
740
+ return Boolean(line && /not\s+logged\s+in/i.test(line));
741
+ }
742
+
743
+ function findCodexMcpListDistributionOsLine(output) {
744
+ return String(output)
745
+ .split(/\r?\n/)
746
+ .map((line) => line.trim())
747
+ .find((line) => /\bdistributionos\b/i.test(line)) ?? null;
748
+ }
749
+
750
+ async function runCodexCommand(runtime, codexCommand, args, options = {}) {
751
+ return runExternalCommand(runtime, codexCommand.command, args, options);
752
+ }
753
+
754
+ async function runExternalCommand(runtime, command, args, options = {}) {
755
+ if (typeof runtime.execFile === 'function') {
756
+ try {
757
+ const result = await runtime.execFile(command, args, options);
758
+ return {
759
+ ok: true,
760
+ stdout: result?.stdout ?? '',
761
+ stderr: result?.stderr ?? '',
762
+ };
763
+ } catch (error) {
764
+ return {
765
+ ok: false,
766
+ stdout: error?.stdout ?? '',
767
+ stderr: error?.stderr ?? '',
768
+ error,
769
+ };
770
+ }
771
+ }
772
+
773
+ const resolved = resolveCommand(command, args, runtime);
774
+ return new Promise((resolve) => {
775
+ let stdoutText = '';
776
+ let stderrText = '';
777
+ let child;
778
+ try {
779
+ child = spawn(resolved.command, resolved.args, {
780
+ cwd: runtime.cwd,
781
+ env: runtime.env,
782
+ windowsHide: true,
783
+ stdio: ['inherit', 'pipe', 'pipe'],
784
+ });
785
+ } catch (error) {
786
+ resolve({ ok: false, stdout: '', stderr: '', error });
787
+ return;
788
+ }
789
+
790
+ child.stdout?.on('data', (chunk) => {
791
+ const text = String(chunk);
792
+ stdoutText += text;
793
+ if (options.stream) runtime.stdout?.write(text);
794
+ });
795
+ child.stderr?.on('data', (chunk) => {
796
+ const text = String(chunk);
797
+ stderrText += text;
798
+ if (options.stream) runtime.stderr?.write(text);
799
+ });
800
+ child.on('error', (error) => {
801
+ resolve({ ok: false, stdout: stdoutText, stderr: stderrText, error });
802
+ });
803
+ child.on('close', (code) => {
804
+ const ok = code === 0;
805
+ resolve({
806
+ ok,
807
+ stdout: stdoutText,
808
+ stderr: stderrText,
809
+ error: ok ? null : Object.assign(new Error(`${command} exited with code ${code}`), { code }),
810
+ });
811
+ });
812
+ });
813
+ }
814
+
815
+ function resolveCommand(command, args, runtime) {
816
+ const platform = runtime.platform ?? process.platform;
817
+ if (platform === 'win32') {
818
+ if (path.isAbsolute(command) && /\.(exe|com)$/i.test(command)) {
819
+ return { command, args };
820
+ }
821
+ return {
822
+ command: runtime.env?.ComSpec ?? process.env.ComSpec ?? 'cmd.exe',
823
+ args: ['/d', '/s', '/c', command, ...args],
824
+ };
825
+ }
826
+ return { command, args };
827
+ }
601
828
 
602
829
  async function ensureCliAuth({ args, runtime, stderr }) {
603
830
  const existingToken = await resolveAuthToken({
@@ -655,6 +882,54 @@ function buildImplementationReportPayload(args) {
655
882
  };
656
883
  }
657
884
 
885
+ function buildStartWorkPayload(args) {
886
+ return {
887
+ taskType: args.taskType ?? 'general_distribution',
888
+ source: 'cli',
889
+ sourceTool: 'distributionos-cli',
890
+ ...(args.summary ? { userRequest: args.summary } : {}),
891
+ ...(args.artifactId ? { sourceArtifactId: args.artifactId } : {}),
892
+ ...(args.sourceArtifactId ? { sourceArtifactId: args.sourceArtifactId } : {}),
893
+ ...(args.dedupKey ? { dedupKey: args.dedupKey } : {}),
894
+ metadata: {
895
+ startedBy: 'distributionos-cli',
896
+ },
897
+ };
898
+ }
899
+
900
+ function buildCompleteWorkPayload(args) {
901
+ const liveUrls = [...args.liveUrls];
902
+ if (args.url) liveUrls.unshift(args.url);
903
+ return {
904
+ workSessionId: args.workSessionId,
905
+ status: args.status,
906
+ ...(args.summary ? { summary: args.summary } : {}),
907
+ changedFiles: compactArray(args.changedFiles),
908
+ changedRoutes: compactArray(args.changedRoutes),
909
+ liveUrls: compactArray(liveUrls),
910
+ ...(args.commitSha ? { commitSha: args.commitSha } : {}),
911
+ ...(args.prUrl ? { prUrl: args.prUrl } : {}),
912
+ ...(args.deploymentUrl ? { deploymentUrl: args.deploymentUrl } : {}),
913
+ ...(args.deploymentId ? { deploymentId: args.deploymentId } : {}),
914
+ ...(args.sourceArtifactId ? { sourceArtifactId: args.sourceArtifactId } : {}),
915
+ ...(args.artifactId ? { artifactId: args.artifactId } : {}),
916
+ ...(args.implementedType ? { implementedType: args.implementedType } : {}),
917
+ ...(args.url ? { publishedUrl: args.url } : {}),
918
+ ...(args.contentId ? { analyticsIds: { dosContentId: args.contentId } } : {}),
919
+ ...(args.analyticsOptOutReason ? { analyticsOptOutReason: args.analyticsOptOutReason } : {}),
920
+ unknowns: compactArray(args.unknowns),
921
+ risks: compactArray(args.risks),
922
+ nextSteps: compactArray(args.nextSteps),
923
+ metadata: {
924
+ reportedBy: 'distributionos-cli',
925
+ },
926
+ };
927
+ }
928
+
929
+ function compactArray(values) {
930
+ return values.map((value) => String(value ?? '').trim()).filter(Boolean);
931
+ }
932
+
658
933
  function formatVerifyResult(result) {
659
934
  const lines = [
660
935
  'DistributionOS analytics verification',
@@ -676,6 +951,39 @@ function formatVerifyResult(result) {
676
951
  return `${lines.join('\n')}\n`;
677
952
  }
678
953
 
954
+ function formatStartWorkResult(result) {
955
+ const session = result.session ?? result;
956
+ const closeout = result.closeout ?? {};
957
+ const lines = [
958
+ 'DistributionOS agent work started',
959
+ `- Work session: ${session.id ?? closeout.workSessionId ?? 'created'}`,
960
+ `- Task type: ${session.taskType ?? closeout.taskType ?? 'general_distribution'}`,
961
+ ];
962
+ if (result.reused) {
963
+ lines.push('- Reused existing open session for this dedup key.');
964
+ }
965
+ if (closeout.instruction) {
966
+ lines.push('- Closeout: call complete-work or complete_agent_work once when this meaningful task is done.');
967
+ }
968
+ return `${lines.join('\n')}\n`;
969
+ }
970
+
971
+ function formatCompleteWorkResult(result) {
972
+ const session = result.session ?? result;
973
+ const lines = [
974
+ 'DistributionOS agent work closeout',
975
+ `- Status: ${session.status ?? result.status ?? 'closed'}`,
976
+ `- Work session: ${session.id ?? result.workSessionId ?? 'updated'}`,
977
+ ];
978
+ if (Array.isArray(result.routed) && result.routed.length > 0) {
979
+ lines.push(`- Routed: ${result.routed.join(', ')}`);
980
+ }
981
+ if (result.idempotent) {
982
+ lines.push('- Duplicate closeout ignored; the session was already closed.');
983
+ }
984
+ return `${lines.join('\n')}\n`;
985
+ }
986
+
679
987
  function formatReportResult(result) {
680
988
  const artifact = result.artifact ?? result;
681
989
  const lines = [
@@ -751,7 +1059,8 @@ function formatApplyResult(plan, changes, initialization, validation, inspection
751
1059
  lines.push(
752
1060
  '- Open this DistributionOS review page now:',
753
1061
  ` ${reviewUrl}`,
754
- '- 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.',
1062
+ '- 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.',
1063
+ '- Then fully quit and reopen Codex Desktop before checking the connection.',
755
1064
  '- Claude Code users: if .mcp.json was created or updated, restart or reconnect Claude Code and approve the project MCP server if asked.',
756
1065
  '- Other MCP-aware agents: reconnect the agent from this repo so it can load the DistributionOS MCP server.',
757
1066
  '- On the review page, copy the connection check prompt into the fresh/reconnected agent chat.',
package/src/constants.js CHANGED
@@ -6,9 +6,9 @@ export const CLI_PACKAGE_NAME = '@distributionos/cli';
6
6
  export const CLI_LOCAL_VERSION = typeof cliPackageJson.version === 'string' ? cliPackageJson.version : '0.0.0-local';
7
7
  export const DEFAULT_API_BASE = 'https://distributionos.dev';
8
8
  export const MCP_SERVER_URL = 'https://distributionos.dev/api/mcp';
9
- export const CURRENT_BOOTSTRAP_VERSION = '2026.06.27';
10
- export const CURRENT_ANALYTICS_CONTRACT_VERSION = '2026.06.20';
11
- export const CURRENT_SETUP_CONTRACT_VERSION = '2026.07.01';
9
+ export const CURRENT_BOOTSTRAP_VERSION = '2026.07.04.1';
10
+ export const CURRENT_ANALYTICS_CONTRACT_VERSION = '2026.06.20';
11
+ export const CURRENT_SETUP_CONTRACT_VERSION = '2026.07.04.1';
12
12
 
13
13
  export const TOKEN_ENV_NAMES = [
14
14
  'DISTRIBUTIONOS_API_KEY',
@@ -16,6 +16,7 @@ const ANALYTICS_BLOCK_RE =
16
16
 
17
17
  export async function buildSetupReport(plan, phase = 'plan', details = {}) {
18
18
  const detectedCapabilities = new Set([
19
+ 'agent.workSessionCloseout',
19
20
  'analytics.verificationReady',
20
21
  'setup.reporting',
21
22
  ]);
@@ -143,10 +144,16 @@ export function formatSetupReportSummary(setupReport) {
143
144
  if (!localSetup) {
144
145
  lines.push('- Server report: pending');
145
146
  return lines;
146
- }
147
-
148
- lines.push(`- Server status: ${localSetup.status}`);
149
- lines.push(`- Missing capabilities: ${formatList(localSetup.missingCapabilities)}`);
147
+ }
148
+
149
+ lines.push(`- Server status: ${localSetup.status}`);
150
+ if (localSetup.recommendedCliVersion) {
151
+ lines.push(`- Recommended CLI: ${localSetup.recommendedCliVersion}`);
152
+ }
153
+ if (localSetup.latestContractVersion) {
154
+ lines.push(`- Current setup contract: ${localSetup.latestContractVersion}`);
155
+ }
156
+ lines.push(`- Missing capabilities: ${formatList(localSetup.missingCapabilities)}`);
150
157
  if (localSetup.status !== 'current') {
151
158
  lines.push(`- Review command: ${localSetup.updateCommand}`);
152
159
  lines.push(`- Apply command: ${localSetup.applyCommand}`);