@aligndottech/cli 0.37.1 → 0.38.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (175) hide show
  1. package/README.md +16 -10
  2. package/dist/cli.d.ts +28 -0
  3. package/dist/cli.d.ts.map +1 -0
  4. package/dist/cli.js +98 -0
  5. package/dist/cli.js.map +1 -0
  6. package/dist/commands/check.d.ts +1 -1
  7. package/dist/commands/check.d.ts.map +1 -1
  8. package/dist/commands/check.js +44 -2
  9. package/dist/commands/check.js.map +1 -1
  10. package/dist/commands/connect-agents.d.ts +1 -0
  11. package/dist/commands/connect-agents.d.ts.map +1 -1
  12. package/dist/commands/connect-agents.js +10 -6
  13. package/dist/commands/connect-agents.js.map +1 -1
  14. package/dist/commands/connect.d.ts +31 -0
  15. package/dist/commands/connect.d.ts.map +1 -0
  16. package/dist/commands/connect.js +92 -0
  17. package/dist/commands/connect.js.map +1 -0
  18. package/dist/commands/context.d.ts.map +1 -1
  19. package/dist/commands/context.js +52 -10
  20. package/dist/commands/context.js.map +1 -1
  21. package/dist/commands/decisions/index.d.ts.map +1 -1
  22. package/dist/commands/decisions/index.js +5 -0
  23. package/dist/commands/decisions/index.js.map +1 -1
  24. package/dist/commands/default-action.d.ts +3 -1
  25. package/dist/commands/default-action.d.ts.map +1 -1
  26. package/dist/commands/default-action.js +29 -27
  27. package/dist/commands/default-action.js.map +1 -1
  28. package/dist/commands/export.d.ts.map +1 -1
  29. package/dist/commands/export.js +3 -0
  30. package/dist/commands/export.js.map +1 -1
  31. package/dist/commands/import/sessions.d.ts +10 -2
  32. package/dist/commands/import/sessions.d.ts.map +1 -1
  33. package/dist/commands/import/sessions.js +98 -3
  34. package/dist/commands/import/sessions.js.map +1 -1
  35. package/dist/commands/import.d.ts +7 -0
  36. package/dist/commands/import.d.ts.map +1 -1
  37. package/dist/commands/import.js +42 -2
  38. package/dist/commands/import.js.map +1 -1
  39. package/dist/commands/invite.d.ts +25 -0
  40. package/dist/commands/invite.d.ts.map +1 -0
  41. package/dist/commands/invite.js +68 -0
  42. package/dist/commands/invite.js.map +1 -0
  43. package/dist/commands/links.d.ts.map +1 -1
  44. package/dist/commands/links.js +5 -0
  45. package/dist/commands/links.js.map +1 -1
  46. package/dist/commands/mcp.d.ts +64 -35
  47. package/dist/commands/mcp.d.ts.map +1 -1
  48. package/dist/commands/mcp.js +85 -40
  49. package/dist/commands/mcp.js.map +1 -1
  50. package/dist/commands/ratify.d.ts.map +1 -1
  51. package/dist/commands/ratify.js +21 -0
  52. package/dist/commands/ratify.js.map +1 -1
  53. package/dist/commands/registry.d.ts +30 -0
  54. package/dist/commands/registry.d.ts.map +1 -0
  55. package/dist/commands/registry.js +70 -0
  56. package/dist/commands/registry.js.map +1 -0
  57. package/dist/commands/setup.d.ts +45 -1
  58. package/dist/commands/setup.d.ts.map +1 -1
  59. package/dist/commands/setup.js +197 -88
  60. package/dist/commands/setup.js.map +1 -1
  61. package/dist/commands/status.d.ts.map +1 -1
  62. package/dist/commands/status.js +10 -22
  63. package/dist/commands/status.js.map +1 -1
  64. package/dist/commands/telemetry.d.ts +7 -3
  65. package/dist/commands/telemetry.d.ts.map +1 -1
  66. package/dist/commands/telemetry.js +12 -7
  67. package/dist/commands/telemetry.js.map +1 -1
  68. package/dist/commands/why.d.ts.map +1 -1
  69. package/dist/commands/why.js +52 -10
  70. package/dist/commands/why.js.map +1 -1
  71. package/dist/index.js +4 -90
  72. package/dist/index.js.map +1 -1
  73. package/dist/lib/agent-rules.d.ts +1 -0
  74. package/dist/lib/agent-rules.d.ts.map +1 -1
  75. package/dist/lib/agent-rules.js +60 -0
  76. package/dist/lib/agent-rules.js.map +1 -1
  77. package/dist/lib/commands-doc.d.ts +15 -0
  78. package/dist/lib/commands-doc.d.ts.map +1 -0
  79. package/dist/lib/commands-doc.js +85 -0
  80. package/dist/lib/commands-doc.js.map +1 -0
  81. package/dist/lib/config.d.ts +7 -2
  82. package/dist/lib/config.d.ts.map +1 -1
  83. package/dist/lib/config.js.map +1 -1
  84. package/dist/lib/connect-prompt.d.ts +1 -1
  85. package/dist/lib/connect-prompt.js +2 -2
  86. package/dist/lib/connect-prompt.js.map +1 -1
  87. package/dist/lib/decision-links.d.ts +39 -0
  88. package/dist/lib/decision-links.d.ts.map +1 -1
  89. package/dist/lib/decision-links.js +51 -0
  90. package/dist/lib/decision-links.js.map +1 -1
  91. package/dist/lib/first-decision.d.ts +22 -0
  92. package/dist/lib/first-decision.d.ts.map +1 -0
  93. package/dist/lib/first-decision.js +22 -0
  94. package/dist/lib/first-decision.js.map +1 -0
  95. package/dist/lib/gateway-client.d.ts +13 -0
  96. package/dist/lib/gateway-client.d.ts.map +1 -1
  97. package/dist/lib/gateway-client.js +14 -0
  98. package/dist/lib/gateway-client.js.map +1 -1
  99. package/dist/lib/git.d.ts +28 -0
  100. package/dist/lib/git.d.ts.map +1 -1
  101. package/dist/lib/git.js +42 -0
  102. package/dist/lib/git.js.map +1 -1
  103. package/dist/lib/hook-context.d.ts +7 -0
  104. package/dist/lib/hook-context.d.ts.map +1 -0
  105. package/dist/lib/hook-context.js +37 -0
  106. package/dist/lib/hook-context.js.map +1 -0
  107. package/dist/lib/hook-payload.d.ts +7 -0
  108. package/dist/lib/hook-payload.d.ts.map +1 -1
  109. package/dist/lib/hook-payload.js +56 -8
  110. package/dist/lib/hook-payload.js.map +1 -1
  111. package/dist/lib/invite-prompt.d.ts +46 -0
  112. package/dist/lib/invite-prompt.d.ts.map +1 -0
  113. package/dist/lib/invite-prompt.js +63 -0
  114. package/dist/lib/invite-prompt.js.map +1 -0
  115. package/dist/lib/mcp-instructions.shared.d.ts +40 -0
  116. package/dist/lib/mcp-instructions.shared.d.ts.map +1 -0
  117. package/dist/lib/mcp-instructions.shared.js +56 -0
  118. package/dist/lib/mcp-instructions.shared.js.map +1 -0
  119. package/dist/lib/mcp-setup.d.ts +32 -2
  120. package/dist/lib/mcp-setup.d.ts.map +1 -1
  121. package/dist/lib/mcp-setup.js +92 -6
  122. package/dist/lib/mcp-setup.js.map +1 -1
  123. package/dist/lib/next-step.d.ts +92 -0
  124. package/dist/lib/next-step.d.ts.map +1 -0
  125. package/dist/lib/next-step.js +97 -0
  126. package/dist/lib/next-step.js.map +1 -0
  127. package/dist/lib/personal-email-domains.d.ts +8 -0
  128. package/dist/lib/personal-email-domains.d.ts.map +1 -0
  129. package/dist/lib/personal-email-domains.js +39 -0
  130. package/dist/lib/personal-email-domains.js.map +1 -0
  131. package/dist/lib/personal-import.d.ts +2 -0
  132. package/dist/lib/personal-import.d.ts.map +1 -1
  133. package/dist/lib/personal-import.js +4 -1
  134. package/dist/lib/personal-import.js.map +1 -1
  135. package/dist/lib/read-value-rollup.d.ts +18 -0
  136. package/dist/lib/read-value-rollup.d.ts.map +1 -0
  137. package/dist/lib/read-value-rollup.js +33 -0
  138. package/dist/lib/read-value-rollup.js.map +1 -0
  139. package/dist/lib/sessions/import-summary.d.ts +39 -0
  140. package/dist/lib/sessions/import-summary.d.ts.map +1 -0
  141. package/dist/lib/sessions/import-summary.js +41 -0
  142. package/dist/lib/sessions/import-summary.js.map +1 -0
  143. package/dist/lib/sessions/memory-files.d.ts +85 -0
  144. package/dist/lib/sessions/memory-files.d.ts.map +1 -0
  145. package/dist/lib/sessions/memory-files.js +235 -0
  146. package/dist/lib/sessions/memory-files.js.map +1 -0
  147. package/dist/lib/sessions/source-url.d.ts +11 -0
  148. package/dist/lib/sessions/source-url.d.ts.map +1 -1
  149. package/dist/lib/sessions/source-url.js +15 -0
  150. package/dist/lib/sessions/source-url.js.map +1 -1
  151. package/dist/lib/setup-funnel.d.ts +41 -0
  152. package/dist/lib/setup-funnel.d.ts.map +1 -0
  153. package/dist/lib/setup-funnel.js +28 -0
  154. package/dist/lib/setup-funnel.js.map +1 -0
  155. package/dist/lib/telemetry-consent.d.ts +17 -8
  156. package/dist/lib/telemetry-consent.d.ts.map +1 -1
  157. package/dist/lib/telemetry-consent.js +21 -6
  158. package/dist/lib/telemetry-consent.js.map +1 -1
  159. package/dist/lib/telemetry-env.d.ts +19 -0
  160. package/dist/lib/telemetry-env.d.ts.map +1 -0
  161. package/dist/lib/telemetry-env.js +13 -0
  162. package/dist/lib/telemetry-env.js.map +1 -0
  163. package/dist/lib/usage-telemetry.d.ts +108 -13
  164. package/dist/lib/usage-telemetry.d.ts.map +1 -1
  165. package/dist/lib/usage-telemetry.js +227 -55
  166. package/dist/lib/usage-telemetry.js.map +1 -1
  167. package/dist/lib/user-hooks.d.ts +15 -0
  168. package/dist/lib/user-hooks.d.ts.map +1 -0
  169. package/dist/lib/user-hooks.js +144 -0
  170. package/dist/lib/user-hooks.js.map +1 -0
  171. package/dist/lib/value-rollup.d.ts +10 -3
  172. package/dist/lib/value-rollup.d.ts.map +1 -1
  173. package/dist/lib/value-rollup.js +11 -4
  174. package/dist/lib/value-rollup.js.map +1 -1
  175. package/package.json +2 -1
@@ -29,6 +29,9 @@ const { version } = pkg;
29
29
  import { printBanner } from '../lib/brand.js';
30
30
  import { guardedPrompt } from '../lib/prompt-guard.js';
31
31
  import { setupSummaryLine, unresolvedGaps } from '../lib/connect-prompt.js';
32
+ import { createSetupFunnel } from '../lib/setup-funnel.js';
33
+ import { agentAskLine, agentConnectedLine, orderAgents, projectAgentsFromWritten } from '../lib/next-step.js';
34
+ import { firstDecision } from '../lib/first-decision.js';
32
35
  const TIER_ORDER = { personal: 0, site: 1, workspace: 2 };
33
36
  // How many connectors import concurrently after auth. Each import is itself
34
37
  // batch-parallel (runPersonalImport), so this bounds total gateway load.
@@ -334,23 +337,30 @@ async function collectTokens(source, seed = {}, opts = {}) {
334
337
  * unmarked default, everything else is explicit.
335
338
  */
336
339
  export function importRetryHint(sourceId, envName) {
337
- return envName === 'prod' ? `align import ${sourceId}` : `align import ${sourceId} --env ${envName}`;
340
+ return envName === 'prod' ? `align connect ${sourceId}` : `align connect ${sourceId} --env ${envName}`;
338
341
  }
342
+ /**
343
+ * Returns the repo-relative files written (empty when the write failed), so the outro can
344
+ * tell whether the project .mcp.json - the one that wires Claude Code - actually landed
345
+ * (ALI-950). Naming an agent as connected off a write that threw would be false.
346
+ */
339
347
  function writeAgentAlignment(envName) {
340
348
  try {
341
349
  const written = setupAgentAlignment({ cwd: process.cwd(), env: envName });
342
350
  p.log.success(`Auto-alignment configured: ${written.join(', ')}`);
343
351
  p.log.info(chalk.dim(' A PostToolUse hook will check edits against your decision graph. Claude Code asks ' +
344
352
  'once to approve project hooks - accept it to enable automatic alignment.'));
353
+ return written;
345
354
  }
346
355
  catch (err) {
347
356
  p.log.warn(`Could not write auto-alignment files: ${err.message}`);
357
+ return [];
348
358
  }
349
359
  }
350
360
  /** Thrown inside the docs block to leave it without starting a read; never surfaces. */
351
361
  class SkipDocs extends Error {
352
362
  }
353
- async function runLocalValuePhase(opts = {}) {
363
+ async function runLocalValuePhase(opts) {
354
364
  // Without a TTY neither prompt below can work: a piped stdin hangs forever and a closed
355
365
  // stdin crashes clack's raw-mode init (uv_tty_init EINVAL) AFTER local setup has already
356
366
  // succeeded (align-cli#118). Computed once, up front, and reused by both prompts in this
@@ -362,6 +372,10 @@ async function runLocalValuePhase(opts = {}) {
362
372
  const localEnv = config.getEnvironment('local');
363
373
  const localClient = createGatewayClient(localEnv);
364
374
  const capture = createCaptureCollector();
375
+ // ALI-949: local setup has begun. Sends now on a re-run (consent already on disk); on a
376
+ // first run it cannot, and the post-consent offer below is the one that lands. Fire-and-
377
+ // forget: a slow gateway never delays the value moment.
378
+ void opts.funnel.started(localEnv);
365
379
  // ALI-794: value before questions. Git needs no credential and no consent, so it runs
366
380
  // before anything is asked - the found-decisions summary below is what "here is what I
367
381
  // found" means, and it has to exist before the agent-wiring/consent/connector steps that
@@ -388,7 +402,7 @@ async function runLocalValuePhase(opts = {}) {
388
402
  repoKnown = gitCount > 0;
389
403
  if (repoKnown) {
390
404
  console.log('');
391
- p.log.info(chalk.dim(`Git: ${gitCount} decisions from ${repo} are already in your graph. \`align import git --env local\` refreshes them.`));
405
+ p.log.info(chalk.dim(`Git: ${gitCount} decisions from ${repo} are already in your graph. \`align connect git --env local\` refreshes them.`));
392
406
  }
393
407
  docsKnown = knownDb.hasDocsForRepo(repo);
394
408
  }
@@ -465,7 +479,7 @@ async function runLocalValuePhase(opts = {}) {
465
479
  // not after them.
466
480
  console.log('');
467
481
  if (docsKnown) {
468
- p.log.info(chalk.dim('Repo docs: already in your graph. `align import docs --env local` refreshes them.'));
482
+ p.log.info(chalk.dim('Repo docs: already in your graph. `align connect docs --env local` refreshes them.'));
469
483
  }
470
484
  const localDocsSpinner = p.spinner();
471
485
  if (!docsKnown)
@@ -485,6 +499,18 @@ async function runLocalValuePhase(opts = {}) {
485
499
  local: true,
486
500
  funnel: { env: localEnv, source: 'docs' },
487
501
  });
502
+ // ALI-950: a repo can carry decision-shaped docs and no git history worth mining, and
503
+ // the outro's question must name a decision the wizard FOUND, whichever source found
504
+ // it. Read back the same way the git summary does, only when git left nothing to name.
505
+ if (!firstFoundTitle) {
506
+ const docsDb = createLocalDb(dbPath);
507
+ try {
508
+ firstFoundTitle = buildFoundSummary(docsDb).recent[0]?.title;
509
+ }
510
+ finally {
511
+ docsDb.close();
512
+ }
513
+ }
488
514
  }
489
515
  else {
490
516
  localDocsSpinner.stop('No ADRs or CLAUDE.md/AGENTS.md content found');
@@ -494,36 +520,51 @@ async function runLocalValuePhase(opts = {}) {
494
520
  if (!(e instanceof SkipDocs))
495
521
  localDocsSpinner.stop(`Docs import skipped - ${e.message}`);
496
522
  }
497
- writeAgentAlignment('local');
523
+ const projectAgents = projectAgentsFromWritten(writeAgentAlignment('local'));
498
524
  // The agents installed on this machine, not just the ones this project configures. Local
499
525
  // setup skipped this and cloud did not, which is backwards: local mode is the one whose
500
526
  // entire pitch is an agent on your own machine reading a graph that never leaves it.
501
527
  console.log('');
502
528
  const localAgents = await connectDetectedAgents('local');
503
- // The "try it" nudge (ALI-794): only when something was actually wired, and phrased against
504
- // a REAL decision from the summary above when one exists - "why did we X" beats a generic
505
- // question because it is checkable against the repo the user is sitting in.
506
- if (localAgents.connected > 0) {
529
+ // ALI-950: whenever ANY agent was wired - the project .mcp.json counts. This used to print
530
+ // only when a GLOBAL config was written, so a Claude-Code-only user, wired through the
531
+ // project file a few lines up, never saw it. The question itself moves to the outro's
532
+ // last line, where it is the one thing left to do.
533
+ const agents = orderAgents({ project: projectAgents, global: localAgents.wired });
534
+ const connectedLine = agentConnectedLine(agents);
535
+ if (connectedLine) {
507
536
  console.log('');
508
- p.log.info(chalk.dim('Your agent is connected. Try asking:'));
509
- p.log.info(chalk.bold(firstFoundTitle ? ` "why did we ${firstFoundTitle}?"` : ' "What decisions exist in this codebase?"'));
537
+ p.log.info(chalk.dim(connectedLine));
510
538
  }
511
539
  // ALI-618: one-time, never asked again once answered. Local mode has no account, so consent
512
540
  // is the only thing that can ever turn this on - see usage-telemetry.ts's local-embedded
513
541
  // branch, which reads this same stored decision.
514
542
  await maybeRequestTelemetryConsent(config, Boolean(interactive));
515
- return { interactive, config, localEnv, localClient, dbPath, opts, capture };
543
+ // ALI-949: the first checkpoint at which a local-mode setup_started CAN send - consent is
544
+ // only now on disk (or was already, on a re-run). The wizard-start offer in runSetup could
545
+ // not send for a fresh install, so the funnel object is offered the local env again here.
546
+ void opts.funnel.started(localEnv);
547
+ return { interactive, config, localEnv, localClient, dbPath, opts, capture, funnel: opts.funnel, agents, firstFoundTitle };
548
+ }
549
+ /** The ids `align connect --source` accepts: every local paste-token source, in picker order. */
550
+ export function localConnectorIds() {
551
+ return buildSources(false)
552
+ .filter((s) => s.id !== 'git' && s.tokenLabel)
553
+ .sort((a, b) => TIER_ORDER[a.tier ?? 'personal'] - TIER_ORDER[b.tier ?? 'personal'])
554
+ .map((s) => s.id);
516
555
  }
517
556
  /**
518
- * The connector-picker tail, shared by every path that reaches local mode: `--local`
519
- * directly, the login-declined fallback in runCloudSetup, and (ALI-794) the fresh-install
520
- * flow after the upgrade question comes back "stay local". Split out of runLocalSetup as a
521
- * pure extraction - this body is byte-identical to what it replaced, just parameterised on
522
- * the value phase's result instead of closing over local variables (refactoring.md: a pure
523
- * move changes only how code is reached, never what it does).
557
+ * The picker-and-import tail of local setup: ask which read-only-token sources to connect,
558
+ * collect every credential up front, then fetch and import each one. Extracted from
559
+ * runLocalConnectorPhase for `align connect` (ALI-951), which is this same flow without the
560
+ * wizard around it - one picker, one paste path, one save rule, in one place. The body is
561
+ * the wizard's; `preselected`, `seedTokens` and `json` are the only additions, and each is
562
+ * inert when absent.
524
563
  */
525
- async function runLocalConnectorPhase(ctx) {
526
- const { interactive, config, localEnv, localClient, dbPath, opts, capture } = ctx;
564
+ export async function connectLocalSources(o) {
565
+ const { interactive, config, localEnv, localClient, capture, approve } = o;
566
+ const quiet = o.json === true;
567
+ const results = [];
527
568
  // Connectors: local mode connects by a read-only token the user mints themselves,
528
569
  // for every connector - their personal graph, their credential. OAuth belongs to
529
570
  // the personal-cloud path, where the hosted broker holds the client secrets. (This
@@ -538,13 +579,21 @@ async function runLocalConnectorPhase(ctx) {
538
579
  const localConnectors = buildSources(false)
539
580
  .filter((s) => s.id !== 'git' && s.tokenLabel)
540
581
  .sort((a, b) => TIER_ORDER[a.tier ?? 'personal'] - TIER_ORDER[b.tier ?? 'personal']);
541
- console.log('');
582
+ if (o.preselected) {
583
+ const known = new Set(localConnectors.map((s) => s.id));
584
+ const unknown = o.preselected.filter((id) => !known.has(id));
585
+ if (unknown.length) {
586
+ throw new Error(`Unknown source ${unknown.join(', ')}. --source takes one of: ${[...known].join(', ')}.`);
587
+ }
588
+ }
589
+ if (!quiet)
590
+ console.log('');
542
591
  // Say WHY, at the point of use. This reason used to live only in the comment above:
543
592
  // the user was sent to a provider page to mint a token with no explanation, which reads
544
593
  // as the tool being clumsy rather than as the privacy trade they chose. The constraint
545
594
  // is the provider's, not ours - OAuth needs a client secret, and a secret inside a
546
595
  // distributed binary is not a secret. See ALI-778.
547
- if (interactive && localConnectors.length > 0) {
596
+ if (interactive && localConnectors.length > 0 && !quiet) {
548
597
  // ONE story, stated as the design it is (Tom, 2026-08-31, superseding ALI-778's
549
598
  // local-OAuth direction): this is the user's PERSONAL graph, so the credential is
550
599
  // one they mint, scope and can revoke themselves. Earlier versions blamed the
@@ -564,7 +613,7 @@ async function runLocalConnectorPhase(ctx) {
564
613
  if (saved?.['token'])
565
614
  savedTokens.set(source.id, saved);
566
615
  }
567
- if (savedTokens.size > 0) {
616
+ if (savedTokens.size > 0 && !quiet && !o.preselected) {
568
617
  const connected = localConnectors.filter((s) => savedTokens.has(s.id));
569
618
  const names = connected.map((s) => s.label).join(', ');
570
619
  // Named out loud, and left alone: a re-run adds what you pick and touches nothing else.
@@ -575,35 +624,38 @@ async function runLocalConnectorPhase(ctx) {
575
624
  p.log.info(chalk.dim(`Connected: ${names} (saved read-only tokens). Select one to re-import it, or to replace its token.`));
576
625
  }
577
626
  else {
578
- const refresh = connected.map((s) => `align import ${s.id} --env local`).join(', ');
627
+ const refresh = connected.map((s) => `align connect --source ${s.id} --yes`).join(', ');
579
628
  p.log.info(chalk.dim(`Connected: ${names} (saved read-only tokens), left alone: no terminal to pick from. Refresh with ${refresh}.`));
580
629
  }
581
630
  }
582
- // `interactive` computed once, at the top of this function - see the comment there.
631
+ // `interactive` computed once, by the caller - see runLocalValuePhase.
583
632
  // ALI-794: the found-summary above sits between this picker and the last clean screen,
584
633
  // which is the exact condition that used to corrupt clack's in-place redraw for an
585
634
  // outside tester (2026-08-30). Clear first so the picker gets its own canvas.
586
- if (interactive)
635
+ const askPicker = interactive && !o.preselected;
636
+ if (askPicker)
587
637
  clearScreenForPicker();
588
638
  // A windowed list ends in "..." and reads as cut off; the count turns it into "scroll".
589
639
  const maxItems = pickerMaxItems(process.stdout.rows, localConnectors.length);
590
640
  const windowed = maxItems < localConnectors.length;
591
- const selected = interactive
592
- ? await p.multiselect({
593
- message: `Connect more sources with a read-only token? (skip to finish${windowed ? `; ${localConnectors.length} sources, scroll for more` : ''})`,
594
- options: localConnectors.map((s) => ({
595
- value: s.id,
596
- label: s.label,
597
- // A connected connector stays in the list so it can be re-imported, or its expired
598
- // token replaced, without a separate command. Not selecting it leaves it alone.
599
- hint: savedTokens.has(s.id) ? 'connected - select to re-import' : s.description,
600
- })),
601
- required: false,
602
- // Without maxItems clack renders all eight and its in-place redraw miscounts
603
- // once the list is taller than the viewport, painting duplicate rows.
604
- maxItems,
605
- })
606
- : [];
641
+ const selected = o.preselected
642
+ ? o.preselected
643
+ : askPicker
644
+ ? await p.multiselect({
645
+ message: `Connect more sources with a read-only token? (skip to finish${windowed ? `; ${localConnectors.length} sources, scroll for more` : ''})`,
646
+ options: localConnectors.map((s) => ({
647
+ value: s.id,
648
+ label: s.label,
649
+ // A connected connector stays in the list so it can be re-imported, or its expired
650
+ // token replaced, without a separate command. Not selecting it leaves it alone.
651
+ hint: savedTokens.has(s.id) ? 'connected - select to re-import' : s.description,
652
+ })),
653
+ required: false,
654
+ // Without maxItems clack renders all eight and its in-place redraw miscounts
655
+ // once the list is taller than the viewport, painting duplicate rows.
656
+ maxItems,
657
+ })
658
+ : [];
607
659
  // Collect every credential up front, so the automatic phase below never stops to ask.
608
660
  const localReady = [];
609
661
  if (!p.isCancel(selected)) {
@@ -612,17 +664,23 @@ async function runLocalConnectorPhase(ctx) {
612
664
  const source = localConnectors.find((s) => s.id === id);
613
665
  if (!source)
614
666
  continue;
615
- console.log('');
616
- p.log.step(chalk.bold(source.label));
667
+ if (!quiet) {
668
+ console.log('');
669
+ p.log.step(chalk.bold(source.label));
670
+ }
617
671
  // Jira and Confluence share one Atlassian account: same email, same site
618
672
  // domain, same id.atlassian.com API token. Ask once, reuse for the other, and
619
673
  // SAY so - the same disclosure rule as the gh-token reuse, because a silently
620
674
  // absorbed credential is the thing nobody can audit afterwards.
621
675
  const isAtlassian = source.id === 'jira' || source.id === 'confluence';
622
676
  let seed = isAtlassian ? { ...atlassianShared } : {};
623
- if (isAtlassian && seed['token'] !== undefined) {
677
+ if (isAtlassian && seed['token'] !== undefined && !quiet) {
624
678
  p.log.info(chalk.dim(' Using your Atlassian email, domain and API token from the previous connector.'));
625
679
  }
680
+ // ALI-951: `--token` (and any other field a flag supplied) seeds every preselected
681
+ // source the way the Atlassian reuse above does - a seeded field is never asked for.
682
+ if (o.seedTokens)
683
+ seed = { ...seed, ...o.seedTokens };
626
684
  // A connected connector that was selected: re-import with the saved token unless the
627
685
  // user wants to replace it. Seeding collectTokens with the saved fields is what skips
628
686
  // every paste; an empty seed is what asks for them. --approve never stops to ask.
@@ -633,7 +691,7 @@ async function runLocalConnectorPhase(ctx) {
633
691
  const saved = savedTokens.get(source.id);
634
692
  let reused = false;
635
693
  if (saved && seed['token'] === undefined) {
636
- const reuse = opts.approve
694
+ const reuse = approve
637
695
  ? true
638
696
  : await p.confirm({
639
697
  message: source.tokenShortLived
@@ -648,7 +706,19 @@ async function runLocalConnectorPhase(ctx) {
648
706
  reused = true;
649
707
  }
650
708
  }
651
- const tokens = await collectTokens(source, seed, { approve: opts.approve });
709
+ // ALI-951: with no terminal there is nothing to paste into. Name the flag rather than
710
+ // letting clack crash on a closed stdin, which reads as the tool breaking (align-cli#118).
711
+ if (!interactive) {
712
+ const missing = (source.extraFields ?? []).map((f) => f.key).filter((k) => seed[k] === undefined);
713
+ if (source.tokenLabel && seed['token'] === undefined)
714
+ missing.push('token');
715
+ if (missing.length) {
716
+ const fieldFlags = missing.filter((k) => k !== 'token').map((k) => `--${k} <${k}>`);
717
+ throw new Error(`${source.label} needs a ${source.tokenLabel ?? 'token'} and there is no terminal to paste one into. ` +
718
+ `Pass --token <token>${fieldFlags.length ? `, or run align connect ${source.id} ${fieldFlags.join(' ')} --token <token>` : ''}.`);
719
+ }
720
+ }
721
+ const tokens = await collectTokens(source, seed, { approve });
652
722
  if (!tokens)
653
723
  continue;
654
724
  if (isAtlassian) {
@@ -665,7 +735,7 @@ async function runLocalConnectorPhase(ctx) {
665
735
  // found" moment, not stranded behind the questions this phase exists to ask. This phase
666
736
  // is only the automatic import for the paste-token connectors just collected.
667
737
  for (const { source, tokens, reused } of localReady) {
668
- const spinner = p.spinner();
738
+ const spinner = quiet ? { start() { }, stop() { } } : p.spinner();
669
739
  spinner.start(`Fetching from ${source.label}...`);
670
740
  try {
671
741
  const fetched = await source.fetch(tokens);
@@ -677,15 +747,19 @@ async function runLocalConnectorPhase(ctx) {
677
747
  // there is one rule here rather than a branch that has to stay in step with the reuse.
678
748
  config.saveConnectorFields('local', source.id, tokens);
679
749
  spinner.stop(`Found ${items.length} items`);
750
+ let imported = 0;
680
751
  if (items.length) {
681
- await runPersonalImport(items, localClient, {
752
+ imported = await runPersonalImport(items, localClient, {
682
753
  label: source.label,
683
754
  approve: true,
684
755
  appUrl: resolveAppUrl(localEnv),
685
756
  local: true,
757
+ quiet: quiet || undefined,
758
+ silent: quiet || undefined,
686
759
  funnel: { env: localEnv, source: source.id },
687
760
  });
688
761
  }
762
+ results.push({ id: source.id, label: source.label, found: items.length, imported });
689
763
  }
690
764
  catch (e) {
691
765
  const msg = e.message;
@@ -700,8 +774,14 @@ async function runLocalConnectorPhase(ctx) {
700
774
  else {
701
775
  spinner.stop(`Skipped ${source.label} - ${msg}`);
702
776
  }
777
+ results.push({ id: source.id, label: source.label, found: 0, imported: 0, error: msg });
703
778
  }
704
779
  }
780
+ return results;
781
+ }
782
+ async function runLocalConnectorPhase(ctx) {
783
+ const { interactive, config, localEnv, localClient, dbPath, opts, capture, funnel, agents, firstFoundTitle } = ctx;
784
+ await connectLocalSources({ interactive, config, localEnv, localClient, capture, approve: opts.approve ?? false });
705
785
  // ALI-827: what each source fetched and what it could not reach - once, after every
706
786
  // import has printed its own line, so the numbers sit together. A source whose fetch
707
787
  // THREW is not in it: its spinner line above already named the error, and a row of
@@ -733,7 +813,7 @@ async function runLocalConnectorPhase(ctx) {
733
813
  ? `export ${chalk.bold('ALIGN_ENV=local')} (or unset it)`
734
814
  : `run ${chalk.bold('align env set local')} to make local the default ` +
735
815
  `(${chalk.dim(`align env set ${bareEnv}`)} switches back)`;
736
- p.log.warn(`Bare commands (align ask, align import ...) use the ${bareEnv} cloud graph, not this local one, because ${cause}.\n` +
816
+ p.log.warn(`Bare commands (align ask, align connect ...) use the ${bareEnv} cloud graph, not this local one, because ${cause}.\n` +
737
817
  `Add ${chalk.bold('--env local')} per command, or ${remedy}.`);
738
818
  }
739
819
  // ALI-796: the graph names its own gaps - a ref whose platform has no connected
@@ -744,19 +824,24 @@ async function runLocalConnectorPhase(ctx) {
744
824
  const gaps = unresolvedGaps(refsDb.getAllRefs(), isConnected);
745
825
  refsDb.close();
746
826
  const gapLine = setupSummaryLine(gaps);
827
+ // ALI-950: the last line is the next step, and it happens in the agent - named, with a
828
+ // question about a decision the wizard just found, and no CLI verb. This used to end on
829
+ // `align ask "why <a thing you decided>"`, a prompt verb on the one screen whose job is to
830
+ // send the user into their agent. The gap line, when there is one, sits above it.
747
831
  p.outro(`${chalk.green('You are set up in local mode.')}\n` +
748
832
  ` Graph: ${chalk.dim(dbPath)}\n` +
749
- ` Run ${chalk.bold('align')} any time to see your graph and what to do next.\n` +
750
- // Deliberately NOT "What decisions exist in this codebase?", which this line used to
751
- // suggest. That question is ABOUT the graph rather than IN it, so on-device search
752
- // matches nothing and a tester was told his freshly imported graph was empty (ALI-771).
753
- // It is a fine thing to ask an AGENT over MCP, and a bad first thing to type here.
754
- ` Ask it something real: ${chalk.bold('align ask "why <a thing you decided>"')}${gapLine ? `\n\n ${chalk.dim(gapLine)}` : ''}`);
833
+ ` Run ${chalk.bold('align')} any time to see your graph and what to do next.` +
834
+ `${gapLine ? `\n\n ${chalk.dim(gapLine)}` : ''}\n\n` +
835
+ ` ${chalk.bold(agentAskLine({ agents, firstTitle: firstFoundTitle, inRepo: true, envName: 'local' }))}`);
836
+ // ALI-949: after the outro, never before it - telemetry must not delay what the user is
837
+ // waiting for. Awaited (unlike the other emitters) because this is the wizard's last act
838
+ // and the one row the funnel was blind to; there is nothing left for a 2s worst case to hold up.
839
+ await funnel.completed(localEnv);
755
840
  }
756
841
  // Local-embedded onboarding (opt-in via --local): no account, no cloud, no OAuth. Composes
757
842
  // the two phases above unchanged - this is exactly what ran before the ALI-794 split, just
758
843
  // as two calls instead of one function body.
759
- async function runLocalSetup(opts = {}) {
844
+ async function runLocalSetup(opts) {
760
845
  const ctx = await runLocalValuePhase(opts);
761
846
  await runLocalConnectorPhase(ctx);
762
847
  }
@@ -787,11 +872,18 @@ export async function runSetup(opts = {}) {
787
872
  // The one place a full brand moment belongs: first run, before any questions.
788
873
  printBanner({ version });
789
874
  p.intro(commandIntro('align setup'));
875
+ // ALI-949: the wizard's setup_started / setup_completed emitter. NOT offered an env
876
+ // here, where the mode is still unknown: a `--local` run on a machine holding a cloud
877
+ // token would report a cloud setup_started for an explicitly offline session (review on
878
+ // #279). Each mode branch offers its own env at its top - runLocalValuePhase and
879
+ // runCloudSetup - and again once consent / login makes a send possible.
880
+ const funnel = createSetupFunnel();
790
881
  // ---- Step 0: Cloud (default) vs local (--local) ----
791
- // Solo defaults to a personal CLOUD tenant: telemetry, the real cloud
792
- // relationship classifier, backup, and a clean upgrade path to a team
793
- // (reuses the personal->org join flow). --local is the opt-in offline
794
- // escape hatch; --approve runs the cloud path non-interactively.
882
+ // Solo defaults to CLOUD: telemetry, the real cloud relationship classifier, backup.
883
+ // A work email lands you in the tenant already registered for that domain (or creates
884
+ // one, admin if first); a personal email gets a tenant of one, with a web-only invite
885
+ // path into a company tenant later (reuses the personal->org join flow). --local is
886
+ // the opt-in offline escape hatch; --approve runs the cloud path non-interactively.
795
887
  //
796
888
  // ALI-794: on a genuinely fresh machine (neither mode configured yet), interactively,
797
889
  // with neither flag forcing a mode, invert this - build the local graph and show what
@@ -799,7 +891,7 @@ export async function runSetup(opts = {}) {
799
891
  // not asked to sit through that again; they keep the question below, same as today.
800
892
  const interactive = process.stdin.isTTY && process.stdout.isTTY;
801
893
  if (!opts.local && !opts.approve && interactive && isFreshInstall(config)) {
802
- await runFreshSetup({ config, env, client, envName, opts });
894
+ await runFreshSetup({ config, env, client, envName, opts, funnel });
803
895
  return;
804
896
  }
805
897
  let mode;
@@ -813,7 +905,7 @@ export async function runSetup(opts = {}) {
813
905
  const choice = await p.select({
814
906
  message: 'How are you using Align?',
815
907
  options: [
816
- { value: 'cloud', label: 'Cloud (recommended) - your personal decision graph', hint: 'syncs, backed up, upgradeable to a team' },
908
+ { value: 'cloud', label: 'Cloud (recommended) - your personal decision graph', hint: "work email: your company's graph; personal email: just you" },
817
909
  { value: 'local', label: 'Local only - private, offline, no account', hint: 'stays on this machine (--local)' },
818
910
  ],
819
911
  initialValue: 'cloud',
@@ -825,10 +917,10 @@ export async function runSetup(opts = {}) {
825
917
  mode = choice;
826
918
  }
827
919
  if (mode === 'local') {
828
- await runLocalSetup({ approve: opts.approve });
920
+ await runLocalSetup({ approve: opts.approve, funnel });
829
921
  return;
830
922
  }
831
- await runCloudSetup({ opts, config, env, client, envName });
923
+ await runCloudSetup({ opts, config, env, client, envName, funnel });
832
924
  }
833
925
  /**
834
926
  * ALI-794: the value-first fresh-install flow. Builds and shows the local graph with
@@ -841,22 +933,24 @@ export async function runSetup(opts = {}) {
841
933
  * provisional state (component 1's "nothing about the cloud path changes once chosen").
842
934
  */
843
935
  async function runFreshSetup(ctx) {
844
- const phase = await runLocalValuePhase({ approve: ctx.opts.approve });
936
+ const phase = await runLocalValuePhase({ approve: ctx.opts.approve, funnel: ctx.funnel });
845
937
  // The value phase's capture report is printed by runLocalConnectorPhase, so choosing
846
938
  // cloud below drops it - deliberately: runCloudSetup re-imports git and docs into the
847
939
  // cloud tenant and prints its own report, which is the one that describes that graph.
848
940
  // Framed by what the graph is missing, not "how are you using Align" in the abstract -
849
941
  // there is already a local graph on screen, so the question is whether to extend it.
850
942
  //
851
- // "A path to team sharing", never "for team sharing": choosing cloud creates a
852
- // PERSONAL tenant (the option's hint says so) and a team is a separate join/upgrade
853
- // later. The question and the option below must keep agreeing on that - the question
943
+ // "A path to team sharing", never "for team sharing": choosing cloud on a personal
944
+ // email creates a tenant of one (the option's hint says so), and joining a team is a
945
+ // separate web invite later. A work email already lands you in the shared tenant,
946
+ // which is why the hint distinguishes them rather than promising a generic upgrade.
947
+ // The question and the option below must keep agreeing on that - the question
854
948
  // overclaimed for a while and a live tester read it as sharing starting here.
855
949
  const choice = await p.select({
856
950
  message: 'Stay local, or sync to the cloud for backup, richer detection, and a path to team sharing?',
857
951
  options: [
858
952
  { value: 'local', label: 'Stay local - keep what you just built, private and offline', hint: 'no account' },
859
- { value: 'cloud', label: 'Sync to the cloud - backup, team upgrade path, richer detection', hint: 'personal tenant' },
953
+ { value: 'cloud', label: 'Sync to the cloud - backup, team upgrade path, richer detection', hint: "work email: your company's graph; personal email: just you" },
860
954
  ],
861
955
  initialValue: 'local',
862
956
  });
@@ -874,7 +968,7 @@ async function runFreshSetup(ctx) {
874
968
  // then offer personal-scoped connectors. A personal-email login lands on an
875
969
  // isolated personal tenant server-side; connectors auto-bind to it.
876
970
  async function runCloudSetup(ctx) {
877
- const { opts, config, env, envName } = ctx;
971
+ const { opts, config, env, envName, funnel } = ctx;
878
972
  let client = ctx.client;
879
973
  // ---- Step 1: Auth check (inline login when interactive + unauthenticated) ----
880
974
  const authSpinner = p.spinner();
@@ -882,6 +976,12 @@ async function runCloudSetup(ctx) {
882
976
  try {
883
977
  const me = await client.whoami();
884
978
  authSpinner.stop(`Logged in as ${me.user.email} (${me.tenant?.name ?? envName})`);
979
+ // ALI-949: cloud setup has begun, and the token is proven good. Offered only AFTER
980
+ // whoami (Copilot on #279): a stale stored token passes the emitter's token check, so an
981
+ // earlier offer would send a request the gateway rejects, count as sent, and suppress the
982
+ // post-login offer below that would have landed. Reached from every route into cloud
983
+ // setup, including the fresh-install upgrade question.
984
+ void funnel.started(config.getEnvironment(envName));
885
985
  }
886
986
  catch {
887
987
  authSpinner.stop('Not authenticated');
@@ -899,12 +999,15 @@ async function runCloudSetup(ctx) {
899
999
  }
900
1000
  // Re-create the client so it carries the freshly stored token.
901
1001
  client = createGatewayClient(config.getEnvironment(envName));
1002
+ // ALI-949: the first checkpoint at which a fresh cloud install's setup_started CAN
1003
+ // send - the token exists now. `env` above is the pre-login snapshot, so re-read.
1004
+ void funnel.started(config.getEnvironment(envName));
902
1005
  }
903
1006
  else {
904
1007
  // Declined cloud login: offer the local escape hatch instead of failing.
905
1008
  const wantLocal = await p.confirm({ message: 'Set up local-only mode instead? (no account, stays on this machine)' });
906
1009
  if (!p.isCancel(wantLocal) && wantLocal) {
907
- await runLocalSetup({ approve: opts.approve });
1010
+ await runLocalSetup({ approve: opts.approve, funnel });
908
1011
  return;
909
1012
  }
910
1013
  p.log.warn(`Run ${chalk.bold('align login')} when ready, then ${chalk.bold('align setup')}.`);
@@ -1017,9 +1120,11 @@ async function runCloudSetup(ctx) {
1017
1120
  // `align mcp --remove` as the undo. This block used to write to a user-level config without
1018
1121
  // a word when exactly one editor was detected and prompt only at two or more, and that
1019
1122
  // multiselect was unguarded, so `align setup --approve` with two agents installed hung.
1020
- const agents = await connectDetectedAgents(envName);
1123
+ const globalAgents = await connectDetectedAgents(envName);
1021
1124
  // ---- Step 5b: Deterministic auto-alignment files (hook + nudges) ----
1022
- writeAgentAlignment(envName);
1125
+ const projectAgents = projectAgentsFromWritten(writeAgentAlignment(envName));
1126
+ // ALI-950: project config first - it is the one the user is most likely sitting in.
1127
+ const agents = orderAgents({ project: projectAgents, global: globalAgents.wired });
1023
1128
  // ---- Step 6: Git auto-import (zero-auth baseline graph seed) ----
1024
1129
  let totalDecisions = 0;
1025
1130
  const sourcesImported = [];
@@ -1200,34 +1305,38 @@ async function runCloudSetup(ctx) {
1200
1305
  // ---- Outro ----
1201
1306
  const decisionsLine = totalDecisions > 0
1202
1307
  ? ` ${totalDecisions} decisions in your graph`
1203
- : ` No decisions yet - run ${chalk.bold('align import')} to load your history`;
1308
+ : ` No decisions yet - run ${chalk.bold('align connect')} to load your history`;
1204
1309
  const sourceLine = sourcesImported.length > 0
1205
1310
  ? `\n Sources: ${sourcesImported.join(', ')}`
1206
1311
  : '';
1207
- // ---- Step 8: First-query prompt ----
1208
- // Only when something was actually WIRED, not merely detected: telling someone their agent
1209
- // is connected when they declined the prompt would be false.
1210
- //
1211
- // The question stays as it is, deliberately. ALI-771 removed it from the LOCAL outro
1212
- // because typing it into `align ask` matches nothing - it is about the graph rather than in
1213
- // it. Asked of an AGENT over MCP it is a good opening question, because the agent answers
1214
- // it by calling a list tool rather than a similarity search. Different surface, different
1215
- // advice.
1216
- if (agents.connected > 0) {
1312
+ // ---- Step 8: the agent is connected, and the next step is in it (ALI-950) ----
1313
+ // Only when something was actually WIRED, not merely detected - and the project .mcp.json
1314
+ // counts, which it did not before: this printed only for a global config, so a
1315
+ // Claude-Code-only user never saw it.
1316
+ const connectedLine = agentConnectedLine(agents);
1317
+ if (connectedLine) {
1217
1318
  console.log('');
1218
- p.log.info(chalk.dim('Your agent is connected. Try asking:'));
1219
- p.log.info(chalk.bold(' "What decisions exist in this codebase?"'));
1319
+ p.log.info(chalk.dim(connectedLine));
1220
1320
  }
1321
+ // The question names a decision the wizard just put in the graph, read straight back
1322
+ // rather than trusting the import's tally (same rule as the local found-summary). Only
1323
+ // worth a request when something was imported.
1324
+ const firstFoundTitle = totalDecisions > 0 ? (await firstDecision(client)).firstTitle : undefined;
1325
+ // The last line is the next step, in the agent, with no CLI verb. It used to be
1326
+ // `Run: align ask "any question about your codebase"`.
1221
1327
  const outroText = [
1222
1328
  chalk.bold('Setup complete.\n'),
1223
1329
  decisionsLine,
1224
1330
  sourceLine,
1225
- `\n\n Run: ${chalk.bold('align ask "any question about your codebase"')}`,
1226
1331
  chalk.dim('\n\n Want your whole team on a shared decision graph?'),
1227
1332
  chalk.dim('\n Upgrade by accepting a team invite - your decisions come with you'),
1228
1333
  chalk.dim('\n (you reconnect your connectors once in the team workspace).'),
1229
1334
  chalk.dim('\n https://app.align.tech/pricing'),
1335
+ `\n\n ${chalk.bold(agentAskLine({ agents, firstTitle: firstFoundTitle, inRepo: true, envName }))}`,
1230
1336
  ].join('');
1231
1337
  p.outro(outroText);
1338
+ // ALI-949: after the outro, and against the env as it is NOW (the inline login above may
1339
+ // have stored the token after `env` was read). See runLocalConnectorPhase for why awaited.
1340
+ await funnel.completed(config.getEnvironment(envName));
1232
1341
  }
1233
1342
  //# sourceMappingURL=setup.js.map