@yagr/agent 0.2.8 → 0.2.10

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 (150) hide show
  1. package/README.md +8 -8
  2. package/dist/cli.js +206 -9
  3. package/dist/cli.js.map +1 -1
  4. package/dist/config/yagr-config-service.d.ts +1 -1
  5. package/dist/config/yagr-config-service.d.ts.map +1 -1
  6. package/dist/config/yagr-home.d.ts +7 -0
  7. package/dist/config/yagr-home.d.ts.map +1 -1
  8. package/dist/config/yagr-home.js +19 -0
  9. package/dist/config/yagr-home.js.map +1 -1
  10. package/dist/gateway/format-message.d.ts +3 -0
  11. package/dist/gateway/format-message.d.ts.map +1 -1
  12. package/dist/gateway/format-message.js +63 -10
  13. package/dist/gateway/format-message.js.map +1 -1
  14. package/dist/gateway/interactive-ui.d.ts.map +1 -1
  15. package/dist/gateway/interactive-ui.js +49 -4
  16. package/dist/gateway/interactive-ui.js.map +1 -1
  17. package/dist/gateway/local-open-bridge.d.ts +3 -0
  18. package/dist/gateway/local-open-bridge.d.ts.map +1 -0
  19. package/dist/gateway/local-open-bridge.js +54 -0
  20. package/dist/gateway/local-open-bridge.js.map +1 -0
  21. package/dist/gateway/webui.d.ts.map +1 -1
  22. package/dist/gateway/webui.js +50 -53
  23. package/dist/gateway/webui.js.map +1 -1
  24. package/dist/gateway/workflow-links.d.ts +12 -0
  25. package/dist/gateway/workflow-links.d.ts.map +1 -0
  26. package/dist/gateway/workflow-links.js +53 -0
  27. package/dist/gateway/workflow-links.js.map +1 -0
  28. package/dist/llm/anthropic-account.d.ts +20 -0
  29. package/dist/llm/anthropic-account.d.ts.map +1 -0
  30. package/dist/llm/anthropic-account.js +111 -0
  31. package/dist/llm/anthropic-account.js.map +1 -0
  32. package/dist/llm/copilot-account.d.ts +36 -0
  33. package/dist/llm/copilot-account.d.ts.map +1 -0
  34. package/dist/llm/copilot-account.js +573 -0
  35. package/dist/llm/copilot-account.js.map +1 -0
  36. package/dist/llm/create-language-model.d.ts +2 -2
  37. package/dist/llm/create-language-model.d.ts.map +1 -1
  38. package/dist/llm/create-language-model.js +187 -38
  39. package/dist/llm/create-language-model.js.map +1 -1
  40. package/dist/llm/google-account.d.ts +31 -0
  41. package/dist/llm/google-account.d.ts.map +1 -0
  42. package/dist/llm/google-account.js +851 -0
  43. package/dist/llm/google-account.js.map +1 -0
  44. package/dist/llm/model-catalog-cache.d.ts +4 -0
  45. package/dist/llm/model-catalog-cache.d.ts.map +1 -0
  46. package/dist/llm/model-catalog-cache.js +46 -0
  47. package/dist/llm/model-catalog-cache.js.map +1 -0
  48. package/dist/llm/openai-account.d.ts +33 -0
  49. package/dist/llm/openai-account.d.ts.map +1 -0
  50. package/dist/llm/openai-account.js +430 -0
  51. package/dist/llm/openai-account.js.map +1 -0
  52. package/dist/llm/provider-discovery.d.ts +3 -0
  53. package/dist/llm/provider-discovery.d.ts.map +1 -0
  54. package/dist/llm/provider-discovery.js +40 -0
  55. package/dist/llm/provider-discovery.js.map +1 -0
  56. package/dist/llm/provider-registry.d.ts +37 -0
  57. package/dist/llm/provider-registry.d.ts.map +1 -0
  58. package/dist/llm/provider-registry.js +186 -0
  59. package/dist/llm/provider-registry.js.map +1 -0
  60. package/dist/llm/proxy-runtime.d.ts +37 -0
  61. package/dist/llm/proxy-runtime.d.ts.map +1 -0
  62. package/dist/llm/proxy-runtime.js +462 -0
  63. package/dist/llm/proxy-runtime.js.map +1 -0
  64. package/dist/llm/test-model-policy.d.ts +3 -0
  65. package/dist/llm/test-model-policy.d.ts.map +1 -0
  66. package/dist/llm/test-model-policy.js +16 -0
  67. package/dist/llm/test-model-policy.js.map +1 -0
  68. package/dist/n8n-local/bootstrap.d.ts +12 -0
  69. package/dist/n8n-local/bootstrap.d.ts.map +1 -0
  70. package/dist/n8n-local/bootstrap.js +281 -0
  71. package/dist/n8n-local/bootstrap.js.map +1 -0
  72. package/dist/n8n-local/browser-auth.d.ts +12 -0
  73. package/dist/n8n-local/browser-auth.d.ts.map +1 -0
  74. package/dist/n8n-local/browser-auth.js +159 -0
  75. package/dist/n8n-local/browser-auth.js.map +1 -0
  76. package/dist/n8n-local/detect.d.ts +50 -0
  77. package/dist/n8n-local/detect.d.ts.map +1 -0
  78. package/dist/n8n-local/detect.js +202 -0
  79. package/dist/n8n-local/detect.js.map +1 -0
  80. package/dist/n8n-local/direct-manager.d.ts +15 -0
  81. package/dist/n8n-local/direct-manager.d.ts.map +1 -0
  82. package/dist/n8n-local/direct-manager.js +231 -0
  83. package/dist/n8n-local/direct-manager.js.map +1 -0
  84. package/dist/n8n-local/docker-manager.d.ts +18 -0
  85. package/dist/n8n-local/docker-manager.d.ts.map +1 -0
  86. package/dist/n8n-local/docker-manager.js +216 -0
  87. package/dist/n8n-local/docker-manager.js.map +1 -0
  88. package/dist/n8n-local/owner-credentials.d.ts +15 -0
  89. package/dist/n8n-local/owner-credentials.d.ts.map +1 -0
  90. package/dist/n8n-local/owner-credentials.js +50 -0
  91. package/dist/n8n-local/owner-credentials.js.map +1 -0
  92. package/dist/n8n-local/plan.d.ts +16 -0
  93. package/dist/n8n-local/plan.d.ts.map +1 -0
  94. package/dist/n8n-local/plan.js +48 -0
  95. package/dist/n8n-local/plan.js.map +1 -0
  96. package/dist/n8n-local/state.d.ts +41 -0
  97. package/dist/n8n-local/state.d.ts.map +1 -0
  98. package/dist/n8n-local/state.js +80 -0
  99. package/dist/n8n-local/state.js.map +1 -0
  100. package/dist/n8n-local/workflow-open.d.ts +21 -0
  101. package/dist/n8n-local/workflow-open.d.ts.map +1 -0
  102. package/dist/n8n-local/workflow-open.js +50 -0
  103. package/dist/n8n-local/workflow-open.js.map +1 -0
  104. package/dist/runtime/context-compaction.d.ts.map +1 -1
  105. package/dist/runtime/context-compaction.js +7 -0
  106. package/dist/runtime/context-compaction.js.map +1 -1
  107. package/dist/runtime/run-engine.d.ts.map +1 -1
  108. package/dist/runtime/run-engine.js +52 -1
  109. package/dist/runtime/run-engine.js.map +1 -1
  110. package/dist/setup/setup-wizard.d.ts +35 -3
  111. package/dist/setup/setup-wizard.d.ts.map +1 -1
  112. package/dist/setup/setup-wizard.js +461 -54
  113. package/dist/setup/setup-wizard.js.map +1 -1
  114. package/dist/setup.d.ts +2 -1
  115. package/dist/setup.d.ts.map +1 -1
  116. package/dist/setup.js +201 -87
  117. package/dist/setup.js.map +1 -1
  118. package/dist/system/open-external.d.ts +2 -0
  119. package/dist/system/open-external.d.ts.map +1 -0
  120. package/dist/system/open-external.js +25 -0
  121. package/dist/system/open-external.js.map +1 -0
  122. package/dist/system/package-manager.d.ts +6 -0
  123. package/dist/system/package-manager.d.ts.map +1 -0
  124. package/dist/system/package-manager.js +13 -0
  125. package/dist/system/package-manager.js.map +1 -0
  126. package/dist/tools/build-tools.d.ts +110 -65
  127. package/dist/tools/build-tools.d.ts.map +1 -1
  128. package/dist/tools/deploy.d.ts +14 -14
  129. package/dist/tools/generate-workflow.d.ts +5 -5
  130. package/dist/tools/n8nac.d.ts +96 -53
  131. package/dist/tools/n8nac.d.ts.map +1 -1
  132. package/dist/tools/n8nac.js +52 -20
  133. package/dist/tools/n8nac.js.map +1 -1
  134. package/dist/tools/observer.d.ts +1 -0
  135. package/dist/tools/observer.d.ts.map +1 -1
  136. package/dist/tools/observer.js.map +1 -1
  137. package/dist/tools/present-workflow-result.d.ts +2 -0
  138. package/dist/tools/present-workflow-result.d.ts.map +1 -1
  139. package/dist/tools/present-workflow-result.js +11 -2
  140. package/dist/tools/present-workflow-result.js.map +1 -1
  141. package/dist/tools/request-required-action.d.ts +7 -7
  142. package/dist/tools/request-required-action.js +4 -4
  143. package/dist/tools/request-required-action.js.map +1 -1
  144. package/dist/tools/validate.d.ts +14 -14
  145. package/dist/tools/write-workspace-file.d.ts +5 -5
  146. package/dist/types.d.ts +1 -0
  147. package/dist/types.d.ts.map +1 -1
  148. package/dist/webui/app.js +14 -5
  149. package/dist/webui/app.js.map +2 -2
  150. package/package.json +14 -2
@@ -1,21 +1,31 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Box, Text, render, useApp, useInput, useStdout } from 'ink';
3
3
  import { useCallback, useEffect, useRef, useState } from 'react';
4
+ import { getProviderDisplayName, getProviderSetupHint, isExperimentalProvider, isOAuthAccountProvider, providerRequiresApiKey, YAGR_MODEL_PROVIDERS, } from '../llm/provider-registry.js';
4
5
  // ─── Palette ──────────────────────────────────────────────────────────────────
5
6
  const CURSOR = '▸';
6
7
  const CHECK = '✓';
7
8
  const DOT = '·';
8
9
  const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
9
- const VALID_PROVIDERS = [
10
- 'anthropic', 'openai', 'google', 'groq', 'mistral', 'openrouter',
10
+ const PROVIDER_WIZARD_ORDER = [
11
+ 'openai',
12
+ 'openai-proxy',
13
+ 'anthropic',
14
+ 'anthropic-proxy',
15
+ 'google',
16
+ 'google-proxy',
17
+ 'copilot-proxy',
18
+ 'groq',
19
+ 'mistral',
20
+ 'openrouter',
11
21
  ];
22
+ const VALID_PROVIDERS = PROVIDER_WIZARD_ORDER.filter((provider) => YAGR_MODEL_PROVIDERS.includes(provider));
12
23
  const SURFACE_OPTIONS = [
13
24
  { value: 'telegram', label: 'Telegram', hint: 'Bot-based chat gateway' },
14
- { value: 'whatsapp', label: 'WhatsApp', hint: 'Configuration only - runtime coming soon' },
15
25
  ];
16
- export function runSetupWizard(callbacks) {
26
+ export function runSetupWizard(callbacks, options = {}) {
17
27
  return new Promise((resolve) => {
18
- const { unmount } = render(_jsx(SetupWizard, { callbacks: callbacks, onDone: (result) => { unmount(); resolve(result); } }));
28
+ const { unmount } = render(_jsx(SetupWizard, { callbacks: callbacks, options: options, onDone: (result) => { unmount(); resolve(result); } }));
19
29
  });
20
30
  }
21
31
  function sectionFor(phase) {
@@ -34,15 +44,65 @@ function sectionIndex(phase) {
34
44
  return 2;
35
45
  return 3;
36
46
  }
47
+ function getProviderAuthCopy(provider) {
48
+ if (provider === 'openai-proxy') {
49
+ return {
50
+ title: 'Connect OpenAI account',
51
+ body: [
52
+ 'Yagr will open your browser to sign you in with your ChatGPT account.',
53
+ 'This uses your ChatGPT subscription — no API credits are consumed.',
54
+ ],
55
+ continueLabel: 'Sign in with ChatGPT',
56
+ };
57
+ }
58
+ if (provider === 'anthropic-proxy') {
59
+ return {
60
+ title: 'Connect Claude token',
61
+ body: [
62
+ 'Generate a setup-token on a machine where Claude CLI is installed and logged in:',
63
+ '`claude setup-token`',
64
+ 'Paste the generated setup-token below.',
65
+ ],
66
+ continueLabel: 'Paste setup-token',
67
+ };
68
+ }
69
+ if (provider === 'google-proxy') {
70
+ return {
71
+ title: 'Connect Gemini account',
72
+ body: [
73
+ 'Yagr runs a native Google OAuth flow for your Gemini account.',
74
+ 'It will show a browser URL and ask you to paste the redirect URL back here.',
75
+ ],
76
+ continueLabel: 'Continue with Gemini sign-in',
77
+ };
78
+ }
79
+ if (provider === 'copilot-proxy') {
80
+ return {
81
+ title: 'Connect GitHub Copilot account',
82
+ body: [
83
+ 'Yagr runs a native GitHub device login and exchanges it for a Copilot runtime token.',
84
+ 'It will show a verification URL and code in the terminal.',
85
+ ],
86
+ continueLabel: 'Continue with GitHub sign-in',
87
+ };
88
+ }
89
+ return {
90
+ title: `Connect ${getProviderDisplayName(provider)}`,
91
+ body: ['Yagr will verify your account session before loading models.'],
92
+ continueLabel: 'Continue',
93
+ };
94
+ }
37
95
  // ─── Primitive UI components ──────────────────────────────────────────────────
38
96
  function Rule() {
39
97
  return _jsx(Text, { dimColor: true, children: '─'.repeat(56) });
40
98
  }
41
- function Header({ phase }) {
99
+ function Header({ phase, mode }) {
42
100
  const section = sectionFor(phase);
43
101
  const idx = sectionIndex(phase);
44
102
  const isDone = phase.kind === 'done' || phase.kind === 'cancelled' || phase.kind === 'error';
45
- return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsx(Text, { color: "cyan", bold: true, children: "\u25C8 Yagr Setup" }), !isDone && (_jsx(Text, { dimColor: true, children: `${idx === 1 ? '' : '○'}${idx === 2 ? '●' : '○'}${idx === 3 ? '●' : '○'} step ${idx} / 3` }))] }), section ? (_jsx(Text, { color: "cyan", dimColor: true, children: section })) : null, _jsx(Rule, {})] }));
103
+ return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsx(Text, { color: "cyan", bold: true, children: "\u25C8 Yagr Setup" }), !isDone && (_jsx(Text, { dimColor: true, children: mode === 'llm-only'
104
+ ? '● step 1 / 1'
105
+ : `${idx === 1 ? '●' : '○'}${idx === 2 ? '●' : '○'}${idx === 3 ? '●' : '○'} step ${idx} / 3` }))] }), section ? (_jsx(Text, { color: "cyan", dimColor: true, children: section })) : null, _jsx(Rule, {})] }));
46
106
  }
47
107
  function HintBar({ hints }) {
48
108
  return (_jsxs(Box, { marginTop: 1, children: [_jsx(Rule, {}), _jsx(Box, { children: hints.map((hint, i) => (_jsxs(Text, { dimColor: true, children: [hint, i < hints.length - 1 ? ' ' : ''] }, i))) })] }));
@@ -166,11 +226,16 @@ function SelectList({ options, cursor, getLabel, getHint, maxVisibleRows, maxLin
166
226
  const active = i === cursor;
167
227
  const hint = getHint?.(opt);
168
228
  const prefix = active ? ` ${CURSOR} ` : ' ';
169
- const suffix = hint ? ` ${DOT} ${hint}` : '';
170
- const line = truncateTerminalLine(`${prefix}${getLabel(opt)}${suffix}`, availableWidth);
229
+ const line = truncateTerminalLine(formatOptionLineWithHint(prefix, getLabel(opt), hint, availableWidth), availableWidth);
171
230
  return (_jsx(Box, { children: _jsx(Text, { color: active ? 'cyan' : undefined, bold: active, children: line }) }, i));
172
231
  }), end < options.length ? _jsx(Text, { dimColor: true, children: truncateTerminalLine(` ↓ ${options.length - end} more`, availableWidth) }) : null] }));
173
232
  }
233
+ function formatOptionLineWithHint(prefix, label, hint, width) {
234
+ if (!hint) {
235
+ return `${prefix}${label}`;
236
+ }
237
+ return `${prefix}${label} ${DOT} ${hint}`;
238
+ }
174
239
  function MultiSelectList({ options, cursor, selected, maxVisibleRows, maxLineWidth, }) {
175
240
  const visibleRows = Math.max(1, maxVisibleRows ?? options.length);
176
241
  const { start, end } = getVisibleWindow(options.length, cursor, visibleRows);
@@ -187,21 +252,41 @@ function MultiSelectList({ options, cursor, selected, maxVisibleRows, maxLineWid
187
252
  }), end < options.length ? _jsx(Text, { dimColor: true, children: truncateTerminalLine(` ↓ ${options.length - end} more`, availableWidth) }) : null] }));
188
253
  }
189
254
  // ─── Main wizard component ────────────────────────────────────────────────────
190
- function SetupWizard({ callbacks, onDone }) {
255
+ function SetupWizard({ callbacks, options, onDone }) {
191
256
  const app = useApp();
192
257
  const { stdout } = useStdout();
193
258
  const n8nDef = callbacks.getN8nDefaults();
194
259
  const llmDef = callbacks.getLlmDefaults();
195
260
  const surfDef = callbacks.getSurfaceDefaults();
196
- const [phase, setPhase] = useState({ kind: 'n8n-url', def: n8nDef.url });
197
- const [textValue, setTextValue] = useState(n8nDef.url);
261
+ const mode = options?.mode ?? 'full';
262
+ const [phase, setPhase] = useState(() => {
263
+ if (mode === 'llm-only') {
264
+ const llmProvider = llmDef.provider;
265
+ if (llmProvider) {
266
+ const existingApiKey = llmDef.getApiKey(llmProvider);
267
+ const existingModel = llmDef.getDefaultModel(llmProvider);
268
+ if (existingModel && (existingApiKey || !providerRequiresApiKey(llmProvider))) {
269
+ return { kind: 'llm-reuse-config', provider: llmProvider, apiKey: existingApiKey ?? '', model: existingModel, cursor: 0 };
270
+ }
271
+ return {
272
+ kind: 'llm-provider',
273
+ initial: llmProvider,
274
+ cursor: Math.max(0, VALID_PROVIDERS.indexOf(llmProvider)),
275
+ };
276
+ }
277
+ return { kind: 'llm-provider', cursor: 0 };
278
+ }
279
+ return { kind: 'n8n-mode', cursor: 0 };
280
+ });
281
+ const [textValue, setTextValue] = useState('');
198
282
  const [spinnerFrame, setSpinnerFrame] = useState(0);
199
283
  const asyncGuard = useRef(0);
200
284
  const llmApiKeyDraftsRef = useRef({});
285
+ const llmBaseUrlDraftsRef = useRef({});
201
286
  const terminalRows = stdout?.rows ?? process.stdout.rows ?? 24;
202
287
  const terminalColumns = stdout?.columns ?? process.stdout.columns ?? 80;
203
288
  const listLineWidth = Math.max(12, terminalColumns - 6);
204
- const isLoading = phase.kind === 'n8n-connecting' || phase.kind === 'n8n-saving'
289
+ const isLoading = phase.kind === 'n8n-connecting' || phase.kind === 'n8n-saving' || phase.kind === 'n8n-local-installing'
205
290
  || phase.kind === 'llm-models-loading' || phase.kind === 'telegram-connecting';
206
291
  useEffect(() => {
207
292
  if (!isLoading)
@@ -218,6 +303,45 @@ function SetupWizard({ callbacks, onDone }) {
218
303
  cancel('Setup cancelled.');
219
304
  }
220
305
  });
306
+ useEffect(() => {
307
+ if (phase.kind !== 'n8n-local-installing')
308
+ return;
309
+ const guard = ++asyncGuard.current;
310
+ void (async () => {
311
+ try {
312
+ const state = await callbacks.installManagedLocalN8n(phase.strategy);
313
+ if (guard !== asyncGuard.current)
314
+ return;
315
+ const bootstrap = await callbacks.bootstrapManagedLocalN8n(state.url);
316
+ if (guard !== asyncGuard.current)
317
+ return;
318
+ if (bootstrap.mode === 'silent' && bootstrap.apiKey) {
319
+ setPhase({ kind: 'n8n-connecting', url: state.url, apiKey: bootstrap.apiKey });
320
+ return;
321
+ }
322
+ const existing = callbacks.getN8nDefaults(state.url).apiKey;
323
+ if (existing) {
324
+ setPhase({ kind: 'n8n-connecting', url: state.url, apiKey: existing });
325
+ return;
326
+ }
327
+ setPhase({
328
+ kind: 'n8n-local-ready',
329
+ url: state.url,
330
+ cursor: 0,
331
+ note: bootstrap.reason ? `Silent bootstrap fallback: ${bootstrap.reason}` : undefined,
332
+ });
333
+ }
334
+ catch (err) {
335
+ if (guard !== asyncGuard.current)
336
+ return;
337
+ setPhase({
338
+ kind: 'n8n-mode',
339
+ cursor: phase.strategy === 'docker' ? 0 : 1,
340
+ err: err instanceof Error ? err.message : String(err),
341
+ });
342
+ }
343
+ })();
344
+ }, [phase.kind]); // eslint-disable-line react-hooks/exhaustive-deps
221
345
  useEffect(() => {
222
346
  if (phase.kind !== 'n8n-connecting')
223
347
  return;
@@ -261,8 +385,8 @@ function SetupWizard({ callbacks, onDone }) {
261
385
  if (llmProvider) {
262
386
  const existingApiKey = llmDef.getApiKey(llmProvider);
263
387
  const existingModel = llmDef.getDefaultModel(llmProvider);
264
- if (existingApiKey && existingModel) {
265
- setPhase({ kind: 'llm-reuse-config', provider: llmProvider, apiKey: existingApiKey, model: existingModel, cursor: 0 });
388
+ if (existingModel && (existingApiKey || !providerRequiresApiKey(llmProvider))) {
389
+ setPhase({ kind: 'llm-reuse-config', provider: llmProvider, apiKey: existingApiKey ?? '', model: existingModel, cursor: 0 });
266
390
  return;
267
391
  }
268
392
  }
@@ -282,16 +406,56 @@ function SetupWizard({ callbacks, onDone }) {
282
406
  const guard = ++asyncGuard.current;
283
407
  void (async () => {
284
408
  try {
285
- const models = await callbacks.fetchModels(phase.provider, phase.apiKey);
409
+ let models = [];
410
+ let resolvedApiKey = phase.apiKey;
411
+ let note = phase.note;
412
+ const prepared = await callbacks.prepareProvider(phase.provider, phase.apiKey || undefined);
413
+ if (guard !== asyncGuard.current)
414
+ return;
415
+ if (prepared.ready) {
416
+ if (prepared.baseUrl) {
417
+ llmBaseUrlDraftsRef.current[phase.provider] = prepared.baseUrl;
418
+ }
419
+ if (prepared.apiKey !== undefined) {
420
+ resolvedApiKey = prepared.apiKey;
421
+ llmApiKeyDraftsRef.current[phase.provider] = prepared.apiKey;
422
+ }
423
+ if (prepared.notes && prepared.notes.length > 0) {
424
+ note = prepared.notes.join(' ');
425
+ }
426
+ models = prepared.models ?? [];
427
+ }
428
+ else if (isOAuthAccountProvider(phase.provider)) {
429
+ const preparedError = prepared.error?.toLowerCase() ?? '';
430
+ if (preparedError.includes('insufficient authentication scopes') || preparedError.includes('http 403')) {
431
+ setTextValue('');
432
+ setPhase({ kind: 'llm-account-auth', provider: phase.provider, cursor: 0 });
433
+ return;
434
+ }
435
+ setPhase({
436
+ kind: 'llm-model',
437
+ provider: phase.provider,
438
+ apiKey: phase.apiKey,
439
+ models: [],
440
+ defModel: phase.defModel,
441
+ cursor: 0,
442
+ note: prepared.error || note,
443
+ });
444
+ return;
445
+ }
446
+ if (models.length === 0) {
447
+ models = await callbacks.fetchModels(phase.provider, resolvedApiKey || undefined);
448
+ }
286
449
  if (guard !== asyncGuard.current)
287
450
  return;
288
451
  const displayedOptions = getDisplayedModelOptions(models);
289
452
  const idx = phase.defModel ? displayedOptions.indexOf(phase.defModel) : -1;
290
453
  setPhase({
291
454
  kind: 'llm-model',
292
- provider: phase.provider, apiKey: phase.apiKey,
455
+ provider: phase.provider, apiKey: resolvedApiKey,
293
456
  models, defModel: phase.defModel,
294
457
  cursor: idx >= 0 ? idx : 0,
458
+ note,
295
459
  });
296
460
  }
297
461
  catch {
@@ -300,7 +464,7 @@ function SetupWizard({ callbacks, onDone }) {
300
464
  setPhase({
301
465
  kind: 'llm-model',
302
466
  provider: phase.provider, apiKey: phase.apiKey,
303
- models: [], defModel: phase.defModel, cursor: 0,
467
+ models: [], defModel: phase.defModel, cursor: 0, note: phase.note,
304
468
  });
305
469
  }
306
470
  })();
@@ -368,16 +532,6 @@ function SetupWizard({ callbacks, onDone }) {
368
532
  }
369
533
  setPhase({ kind: 'n8n-saving', url, apiKey, project, syncFolder: folder });
370
534
  }, []);
371
- const handleLlmApiKeySubmit = useCallback((provider) => (value) => {
372
- const key = value.trim();
373
- if (!key) {
374
- setPhase((p) => ({ ...p, err: 'API key is required.' }));
375
- return;
376
- }
377
- llmApiKeyDraftsRef.current[provider] = key;
378
- const defModel = llmDef.getDefaultModel(provider);
379
- setPhase({ kind: 'llm-models-loading', provider, apiKey: key, defModel });
380
- }, [llmDef]);
381
535
  const handleBaseUrlSubmit = useCallback((provider, apiKey, model) => (value) => {
382
536
  const url = value.trim();
383
537
  if (url) {
@@ -401,8 +555,118 @@ function SetupWizard({ callbacks, onDone }) {
401
555
  }
402
556
  setPhase({ kind: 'telegram-connecting', surfaces, token });
403
557
  }, []);
558
+ const transitionToLlmModelsLoading = useCallback((provider, apiKey, defModel, note) => {
559
+ setTextValue('');
560
+ setPhase({
561
+ kind: 'llm-models-loading',
562
+ provider,
563
+ apiKey,
564
+ defModel,
565
+ ...(note ? { note } : {}),
566
+ });
567
+ }, []);
568
+ const handleLlmApiKeySubmit = useCallback((provider) => (value) => {
569
+ const key = value.trim();
570
+ if (!key && providerRequiresApiKey(provider)) {
571
+ setPhase((p) => ({ ...p, err: 'API key is required.' }));
572
+ return;
573
+ }
574
+ llmApiKeyDraftsRef.current[provider] = key;
575
+ const defModel = llmDef.getDefaultModel(provider);
576
+ transitionToLlmModelsLoading(provider, key, defModel);
577
+ }, [llmDef, transitionToLlmModelsLoading]);
578
+ const handleAccountAuthSubmit = useCallback((provider, state) => (value) => {
579
+ void (async () => {
580
+ try {
581
+ const result = await callbacks.completeAccountAuth(provider, value, state);
582
+ if (!result.ok) {
583
+ setPhase((current) => current.kind === 'llm-account-input'
584
+ ? { ...current, err: result.error || 'Authentication failed.' }
585
+ : current);
586
+ return;
587
+ }
588
+ const defModel = llmDef.getDefaultModel(provider);
589
+ transitionToLlmModelsLoading(provider, result.apiKey ?? '', defModel);
590
+ }
591
+ catch (error) {
592
+ setPhase((current) => current.kind === 'llm-account-input'
593
+ ? { ...current, err: error instanceof Error ? error.message : String(error) }
594
+ : current);
595
+ }
596
+ })();
597
+ }, [callbacks, llmDef, transitionToLlmModelsLoading]);
598
+ const saveLlmAndContinue = useCallback((provider, apiKey, model, note) => {
599
+ const draftedBaseUrl = llmBaseUrlDraftsRef.current[provider];
600
+ callbacks.saveLlmConfig({ provider, apiKey, model, baseUrl: draftedBaseUrl || undefined });
601
+ setTextValue('');
602
+ if (mode === 'llm-only') {
603
+ setPhase({ kind: 'done', n8nHost: '', n8nProject: '', provider, model, surfaces: surfDef.surfaces });
604
+ setTimeout(() => { onDone({ ok: true }); app.exit(); }, 250);
605
+ return;
606
+ }
607
+ setPhase({ kind: 'surfaces', cursor: 0, selected: surfDef.surfaces });
608
+ }, [app, callbacks, mode, onDone, surfDef]);
404
609
  const handleSelectKey = useCallback((input, key) => {
405
- if (phase.kind === 'n8n-reuse-apikey') {
610
+ if (phase.kind === 'n8n-mode') {
611
+ if (key.upArrow)
612
+ setPhase({ ...phase, cursor: Math.max(0, phase.cursor - 1), err: undefined });
613
+ else if (key.downArrow)
614
+ setPhase({ ...phase, cursor: Math.min(2, phase.cursor + 1), err: undefined });
615
+ else if (key.return) {
616
+ if (phase.cursor === 0) {
617
+ setPhase({ kind: 'n8n-local-installing', startedAt: Date.now(), strategy: 'docker' });
618
+ }
619
+ else if (phase.cursor === 1) {
620
+ setPhase({ kind: 'n8n-local-installing', startedAt: Date.now(), strategy: 'direct' });
621
+ }
622
+ else {
623
+ setPhase({ kind: 'n8n-url', def: n8nDef.url });
624
+ setTextValue(n8nDef.url);
625
+ }
626
+ }
627
+ else if (key.escape)
628
+ cancel('Setup cancelled.');
629
+ }
630
+ else if (phase.kind === 'n8n-local-ready') {
631
+ if (key.upArrow)
632
+ setPhase({ ...phase, cursor: Math.max(0, phase.cursor - 1) });
633
+ else if (key.downArrow)
634
+ setPhase({ ...phase, cursor: Math.min(1, phase.cursor + 1) });
635
+ else if (key.return) {
636
+ if (phase.cursor === 0) {
637
+ void (async () => {
638
+ try {
639
+ await callbacks.openUrl(phase.url);
640
+ }
641
+ catch {
642
+ // Leave the flow available even if browser opening fails.
643
+ }
644
+ const existing = callbacks.getN8nDefaults(phase.url).apiKey;
645
+ if (existing) {
646
+ setPhase({ kind: 'n8n-connecting', url: phase.url, apiKey: existing });
647
+ return;
648
+ }
649
+ setPhase({
650
+ kind: 'n8n-local-auth',
651
+ url: phase.url,
652
+ message: 'The local n8n editor is ready. Create the owner account, then open Settings > n8n API, generate a key for Yagr, and paste it here.',
653
+ });
654
+ setTextValue('');
655
+ })();
656
+ }
657
+ else {
658
+ setPhase({
659
+ kind: 'n8n-local-auth',
660
+ url: phase.url,
661
+ message: 'Open the local n8n URL in your browser, create the owner account, then generate a key in Settings > n8n API and paste it here.',
662
+ });
663
+ setTextValue('');
664
+ }
665
+ }
666
+ else if (key.escape)
667
+ cancel('Setup cancelled.');
668
+ }
669
+ else if (phase.kind === 'n8n-reuse-apikey') {
406
670
  if (key.upArrow)
407
671
  setPhase({ ...phase, cursor: Math.max(0, phase.cursor - 1) });
408
672
  else if (key.downArrow)
@@ -440,10 +704,13 @@ function SetupWizard({ callbacks, onDone }) {
440
704
  else if (key.return) {
441
705
  if (phase.cursor === 0) {
442
706
  llmApiKeyDraftsRef.current[phase.provider] = phase.apiKey;
443
- callbacks.saveLlmConfig({ provider: phase.provider, apiKey: phase.apiKey, model: phase.model, baseUrl: llmDef.getBaseUrl(phase.provider) });
707
+ const draftedBaseUrl = llmBaseUrlDraftsRef.current[phase.provider] ?? llmDef.getBaseUrl(phase.provider);
708
+ callbacks.saveLlmConfig({ provider: phase.provider, apiKey: phase.apiKey, model: phase.model, baseUrl: draftedBaseUrl });
709
+ setTextValue('');
444
710
  setPhase({ kind: 'surfaces', cursor: 0, selected: surfDef.surfaces });
445
711
  }
446
712
  else {
713
+ setTextValue('');
447
714
  setPhase({ kind: 'llm-provider', initial: phase.provider, cursor: VALID_PROVIDERS.indexOf(phase.provider) });
448
715
  }
449
716
  }
@@ -458,7 +725,16 @@ function SetupWizard({ callbacks, onDone }) {
458
725
  else if (key.return) {
459
726
  const provider = VALID_PROVIDERS[phase.cursor];
460
727
  const existing = llmApiKeyDraftsRef.current[provider] ?? llmDef.getApiKey(provider);
461
- if (existing) {
728
+ if (isOAuthAccountProvider(provider)) {
729
+ setTextValue('');
730
+ setPhase({ kind: 'llm-account-auth', provider, cursor: 0 });
731
+ }
732
+ else if (!providerRequiresApiKey(provider)) {
733
+ const defModel = llmDef.getDefaultModel(provider);
734
+ transitionToLlmModelsLoading(provider, existing ?? '', defModel);
735
+ }
736
+ else if (existing) {
737
+ setTextValue('');
462
738
  setPhase({ kind: 'llm-reuse-apikey', provider, existing, cursor: 0 });
463
739
  }
464
740
  else {
@@ -469,6 +745,81 @@ function SetupWizard({ callbacks, onDone }) {
469
745
  else if (key.escape)
470
746
  cancel('Setup cancelled.');
471
747
  }
748
+ else if (phase.kind === 'llm-account-auth') {
749
+ const maxCursor = 1;
750
+ if (key.upArrow)
751
+ setPhase({ ...phase, cursor: Math.max(0, phase.cursor - 1) });
752
+ else if (key.downArrow)
753
+ setPhase({ ...phase, cursor: Math.min(maxCursor, phase.cursor + 1) });
754
+ else if (key.return) {
755
+ if (phase.provider === 'anthropic-proxy' && phase.cursor === 0) {
756
+ setPhase({
757
+ kind: 'llm-account-input',
758
+ provider: 'anthropic-proxy',
759
+ title: 'Claude setup-token',
760
+ instructions: [
761
+ 'On a machine where Claude CLI is installed and logged in, run:',
762
+ 'claude setup-token',
763
+ 'Copy the generated setup-token and paste it below.',
764
+ ],
765
+ placeholder: 'Paste setup-token',
766
+ submitLabel: 'Continue with setup-token',
767
+ state: 'anthropic:setup-token',
768
+ });
769
+ setTextValue('');
770
+ }
771
+ else if (phase.cursor === 0) {
772
+ void (async () => {
773
+ try {
774
+ const authStep = await callbacks.startAccountAuth(phase.provider);
775
+ const authUrl = (authStep.instructions ?? [])
776
+ .map((line) => line.match(/https?:\/\/\S+/)?.[0])
777
+ .find(Boolean);
778
+ if (authUrl) {
779
+ void callbacks.openUrl(authUrl).catch(() => {
780
+ // Browser auto-open is best effort only.
781
+ });
782
+ }
783
+ if (authStep.kind === 'input') {
784
+ setPhase({
785
+ kind: 'llm-account-input',
786
+ provider: phase.provider,
787
+ title: authStep.title ?? getProviderAuthCopy(phase.provider).title,
788
+ instructions: authStep.instructions ?? [],
789
+ placeholder: authStep.placeholder,
790
+ submitLabel: authStep.submitLabel ?? 'Continue',
791
+ state: authStep.state,
792
+ });
793
+ setTextValue('');
794
+ return;
795
+ }
796
+ const defModel = llmDef.getDefaultModel(phase.provider);
797
+ const authCopy = getProviderAuthCopy(phase.provider);
798
+ transitionToLlmModelsLoading(phase.provider, '', defModel, authCopy.body.join(' '));
799
+ }
800
+ catch (error) {
801
+ setPhase({
802
+ kind: 'llm-account-input',
803
+ provider: phase.provider,
804
+ title: getProviderAuthCopy(phase.provider).title,
805
+ instructions: getProviderAuthCopy(phase.provider).body,
806
+ submitLabel: 'Continue',
807
+ err: error instanceof Error ? error.message : String(error),
808
+ });
809
+ }
810
+ })();
811
+ }
812
+ else {
813
+ setPhase({ kind: 'llm-provider', initial: phase.provider, cursor: VALID_PROVIDERS.indexOf(phase.provider) });
814
+ }
815
+ }
816
+ else if (key.escape)
817
+ cancel('Setup cancelled.');
818
+ }
819
+ else if (phase.kind === 'llm-account-input') {
820
+ if (key.escape)
821
+ cancel('Setup cancelled.');
822
+ }
472
823
  else if (phase.kind === 'llm-reuse-apikey') {
473
824
  if (key.upArrow)
474
825
  setPhase({ ...phase, cursor: Math.max(0, phase.cursor - 1) });
@@ -478,7 +829,7 @@ function SetupWizard({ callbacks, onDone }) {
478
829
  if (phase.cursor === 0) {
479
830
  const defModel = llmDef.getDefaultModel(phase.provider);
480
831
  llmApiKeyDraftsRef.current[phase.provider] = phase.existing;
481
- setPhase({ kind: 'llm-models-loading', provider: phase.provider, apiKey: phase.existing, defModel });
832
+ transitionToLlmModelsLoading(phase.provider, phase.existing, defModel);
482
833
  }
483
834
  else {
484
835
  setPhase({ kind: 'llm-apikey', provider: phase.provider });
@@ -502,14 +853,18 @@ function SetupWizard({ callbacks, onDone }) {
502
853
  return;
503
854
  }
504
855
  const model = selected;
856
+ const draftedBaseUrl = llmBaseUrlDraftsRef.current[phase.provider];
857
+ const defaultBaseUrl = draftedBaseUrl ?? llmDef.getBaseUrl(phase.provider);
505
858
  const needsUrl = llmDef.needsBaseUrl(phase.provider);
506
- if (needsUrl || llmDef.getBaseUrl(phase.provider)) {
507
- setPhase({ kind: 'llm-baseurl', provider: phase.provider, apiKey: phase.apiKey, model, def: llmDef.getBaseUrl(phase.provider) ?? '' });
508
- setTextValue(llmDef.getBaseUrl(phase.provider) ?? '');
859
+ if (draftedBaseUrl) {
860
+ saveLlmAndContinue(phase.provider, phase.apiKey, model, phase.note);
861
+ }
862
+ else if (needsUrl || defaultBaseUrl) {
863
+ setPhase({ kind: 'llm-baseurl', provider: phase.provider, apiKey: phase.apiKey, model, def: defaultBaseUrl ?? '' });
864
+ setTextValue(defaultBaseUrl ?? '');
509
865
  }
510
866
  else {
511
- callbacks.saveLlmConfig({ provider: phase.provider, apiKey: phase.apiKey, model });
512
- setPhase({ kind: 'surfaces', cursor: 0, selected: surfDef.surfaces });
867
+ saveLlmAndContinue(phase.provider, phase.apiKey, model, phase.note);
513
868
  }
514
869
  }
515
870
  else if (key.escape)
@@ -569,7 +924,7 @@ function SetupWizard({ callbacks, onDone }) {
569
924
  cancel('Setup cancelled.');
570
925
  }
571
926
  }, [phase, cancel, callbacks, llmDef, surfDef, n8nDef.syncFolder, app, onDone]);
572
- const isSelectPhase = ['n8n-reuse-apikey', 'n8n-project', 'llm-provider', 'llm-reuse-config', 'llm-reuse-apikey', 'surfaces', 'telegram-reuse-token'].includes(phase.kind)
927
+ const isSelectPhase = ['n8n-mode', 'n8n-local-ready', 'n8n-reuse-apikey', 'n8n-project', 'llm-provider', 'llm-account-auth', 'llm-reuse-config', 'llm-reuse-apikey', 'surfaces', 'telegram-reuse-token'].includes(phase.kind)
573
928
  || (phase.kind === 'llm-model' && phase.models.length > 0);
574
929
  useInput((input, key) => {
575
930
  if (key.ctrl && input === 'c')
@@ -577,9 +932,23 @@ function SetupWizard({ callbacks, onDone }) {
577
932
  if (isSelectPhase)
578
933
  handleSelectKey(input, key);
579
934
  }, { isActive: isSelectPhase });
580
- return (_jsxs(Box, { flexDirection: "column", paddingX: 2, paddingY: 1, children: [_jsx(Header, { phase: phase }), renderPhase()] }));
935
+ return (_jsxs(Box, { flexDirection: "column", paddingX: 2, paddingY: 1, children: [_jsx(Header, { phase: phase, mode: mode }), renderPhase()] }));
581
936
  function renderPhase() {
582
937
  switch (phase.kind) {
938
+ case 'n8n-mode':
939
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(FieldLabel, { label: "n8n setup path" }), _jsx(Text, { dimColor: true, children: " Choose the lowest-friction way to connect Yagr to n8n." }), _jsx(SelectList, { options: [
940
+ 'Install a Yagr-managed n8n with Docker',
941
+ 'Install a Yagr-managed local n8n',
942
+ 'Use an existing n8n instance and API key',
943
+ ], cursor: phase.cursor, getLabel: (v) => v, getHint: (v) => {
944
+ if (v === 'Install a Yagr-managed n8n with Docker') {
945
+ return 'Recommended if Docker is installed and running';
946
+ }
947
+ if (v === 'Install a Yagr-managed local n8n') {
948
+ return 'Managed local runtime without Docker';
949
+ }
950
+ return 'Cloud or self-managed n8n';
951
+ }, maxVisibleRows: getListViewportHeight(terminalRows, 13), maxLineWidth: listLineWidth }), phase.err ? _jsx(ErrorLine, { message: phase.err }) : null, _jsx(HintBar, { hints: ['↑↓ move', 'Enter ↵ confirm', 'Ctrl+C cancel'] })] }));
583
952
  case 'n8n-url':
584
953
  return (_jsxs(Box, { flexDirection: "column", children: [_jsx(FieldLabel, { label: "Instance URL" }), _jsx(Box, { marginLeft: 2, children: _jsx(WizardTextInput, { value: textValue, onChange: setTextValue, onSubmit: handleN8nUrlSubmit, placeholder: "https://your-n8n.example.com" }) }), phase.err ? _jsx(ErrorLine, { message: phase.err }) : null, _jsx(HintBar, { hints: ['Enter ↵ confirm', 'Ctrl+C cancel'] })] }));
585
954
  case 'n8n-reuse-apikey':
@@ -588,6 +957,18 @@ function SetupWizard({ callbacks, onDone }) {
588
957
  return (_jsxs(Box, { flexDirection: "column", children: [_jsx(FieldLabel, { label: "n8n API key" }), _jsx(Box, { marginLeft: 2, children: _jsx(WizardTextInput, { value: textValue, onChange: setTextValue, onSubmit: handleN8nApiKeySubmit(phase.url), mask: "\u25CF", placeholder: "your-api-key" }) }), phase.err ? _jsx(ErrorLine, { message: phase.err }) : null, _jsx(HintBar, { hints: ['Enter ↵ confirm', 'Ctrl+C cancel'] })] }));
589
958
  case 'n8n-connecting':
590
959
  return (_jsx(Box, { flexDirection: "column", children: _jsx(SpinnerDisplay, { message: `Connecting to ${phase.url}…`, frame: spinnerFrame }) }));
960
+ case 'n8n-local-installing':
961
+ {
962
+ const elapsedSeconds = Math.max(0, Math.round((Date.now() - phase.startedAt) / 1000));
963
+ const elapsedLabel = elapsedSeconds < 60
964
+ ? `${elapsedSeconds}s`
965
+ : `${Math.floor(elapsedSeconds / 60)}m ${String(elapsedSeconds % 60).padStart(2, '0')}s`;
966
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(SpinnerDisplay, { message: "Installing and starting a Yagr-managed local n8n instance\u2026", frame: spinnerFrame }), _jsx(Text, { dimColor: true, children: " Yagr is waiting for the n8n API and editor to become ready before continuing." }), _jsx(Text, { dimColor: true, children: " First run can take 1 to 3 minutes depending on Docker, npm downloads, and machine speed." }), _jsxs(Text, { dimColor: true, children: [" Elapsed: ", elapsedLabel] })] }));
967
+ }
968
+ case 'n8n-local-ready':
969
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(FieldLabel, { label: "Local n8n is ready" }), _jsxs(Text, { dimColor: true, children: [" Instance URL: ", phase.url] }), _jsx(Text, { dimColor: true, children: " Yagr waited for the n8n editor to finish starting before showing this step." }), _jsx(Text, { dimColor: true, children: " Yagr attempted a silent owner/API bootstrap first." }), phase.note ? _jsxs(Text, { dimColor: true, children: [" ", phase.note] }) : null, _jsx(Text, { dimColor: true, children: " You will first create the owner account, then create an API key for Yagr." }), _jsx(SelectList, { options: ['Open n8n in the browser', 'I will open it myself'], cursor: phase.cursor, getLabel: (v) => v, getHint: (v) => v.startsWith('Open') ? 'recommended' : phase.url, maxVisibleRows: getListViewportHeight(terminalRows, 12), maxLineWidth: listLineWidth }), _jsx(HintBar, { hints: ['↑↓ move', 'Enter ↵ confirm', 'Ctrl+C cancel'] })] }));
970
+ case 'n8n-local-auth':
971
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(FieldLabel, { label: "Local n8n API key" }), _jsxs(Text, { dimColor: true, children: [" Local instance URL: ", phase.url] }), _jsxs(Text, { dimColor: true, children: [" ", phase.message] }), _jsx(Text, { dimColor: true, children: " If the browser is not already open, run `yagr n8n local open` or open the URL manually." }), _jsx(Box, { marginLeft: 2, marginTop: 1, children: _jsx(WizardTextInput, { value: textValue, onChange: setTextValue, onSubmit: handleN8nApiKeySubmit(phase.url), mask: "\u25CF", placeholder: "Paste the new n8n API key" }) }), _jsx(HintBar, { hints: ['Enter ↵ confirm', 'Ctrl+C cancel'] })] }));
591
972
  case 'n8n-project':
592
973
  return (_jsxs(Box, { flexDirection: "column", children: [_jsx(FieldLabel, { label: "n8n project" }), _jsxs(Text, { dimColor: true, children: [" ", phase.projects.length, " project(s) found"] }), _jsx(SelectList, { options: phase.projects, cursor: phase.cursor, getLabel: (p) => p.name ?? p.id, getHint: (p) => p.id, maxVisibleRows: getListViewportHeight(terminalRows, 11), maxLineWidth: listLineWidth }), _jsx(HintBar, { hints: ['↑↓ move', 'Enter ↵ select', 'Ctrl+C cancel'] })] }));
593
974
  case 'n8n-syncfolder':
@@ -595,37 +976,63 @@ function SetupWizard({ callbacks, onDone }) {
595
976
  case 'n8n-saving':
596
977
  return (_jsx(Box, { flexDirection: "column", children: _jsx(SpinnerDisplay, { message: "Saving n8n configuration and refreshing workspace\u2026", frame: spinnerFrame }) }));
597
978
  case 'llm-reuse-config':
598
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(FieldLabel, { label: "LLM configuration" }), _jsxs(Text, { dimColor: true, children: [" Currently configured: ", phase.provider, " / ", phase.model] }), _jsx(SelectList, { options: ['Keep current configuration', 'Change provider or model'], cursor: phase.cursor, getLabel: (v) => v, maxVisibleRows: getListViewportHeight(terminalRows, 11), maxLineWidth: listLineWidth }), _jsx(HintBar, { hints: ['↑↓ move', 'Enter ↵ confirm', 'Ctrl+C cancel'] })] }));
979
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(FieldLabel, { label: "LLM configuration" }), _jsxs(Text, { dimColor: true, children: [" Currently configured: ", getProviderDisplayName(phase.provider), " / ", phase.model] }), _jsx(SelectList, { options: ['Keep current configuration', 'Change provider or model'], cursor: phase.cursor, getLabel: (v) => v, maxVisibleRows: getListViewportHeight(terminalRows, 11), maxLineWidth: listLineWidth }), _jsx(HintBar, { hints: ['↑↓ move', 'Enter ↵ confirm', 'Ctrl+C cancel'] })] }));
599
980
  case 'llm-provider':
600
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(FieldLabel, { label: "Default LLM provider" }), _jsx(SelectList, { options: VALID_PROVIDERS, cursor: phase.cursor, getLabel: (v) => v, getHint: (v) => v === phase.initial ? 'currently configured' : undefined, maxVisibleRows: getListViewportHeight(terminalRows, 10), maxLineWidth: listLineWidth }), _jsx(HintBar, { hints: ['↑↓ move', 'Enter ↵ select', 'Ctrl+C cancel'] })] }));
981
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(FieldLabel, { label: "Default LLM provider" }), _jsx(SelectList, { options: VALID_PROVIDERS, cursor: phase.cursor, getLabel: (v) => getProviderDisplayName(v), getHint: (v) => {
982
+ const parts = [
983
+ getProviderSetupHint(v),
984
+ isExperimentalProvider(v) ? 'experimental' : undefined,
985
+ v === phase.initial ? 'currently configured' : undefined,
986
+ ].filter(Boolean);
987
+ return parts.length > 0 ? parts.join(' · ') : undefined;
988
+ }, maxVisibleRows: getListViewportHeight(terminalRows, 10), maxLineWidth: listLineWidth }), _jsx(HintBar, { hints: ['↑↓ move', 'Enter ↵ select', 'Ctrl+C cancel'] })] }));
989
+ case 'llm-account-auth':
990
+ {
991
+ const authCopy = getProviderAuthCopy(phase.provider);
992
+ const authOptions = [authCopy.continueLabel, 'Back to providers'];
993
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(FieldLabel, { label: authCopy.title }), authCopy.body.map((line) => (_jsxs(Text, { dimColor: true, children: [" ", line] }, line))), _jsx(SelectList, { options: authOptions, cursor: phase.cursor, getLabel: (v) => v, getHint: (v) => {
994
+ return (v.startsWith('Continue') || v.startsWith('Paste')) ? 'recommended' : undefined;
995
+ }, maxVisibleRows: getListViewportHeight(terminalRows, 11), maxLineWidth: listLineWidth }), _jsx(HintBar, { hints: ['↑↓ move', 'Enter ↵ confirm', 'Ctrl+C cancel'] })] }));
996
+ }
997
+ case 'llm-account-input':
998
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(FieldLabel, { label: phase.title }), phase.instructions.map((line, index) => (_jsxs(Text, { dimColor: true, children: [" ", line] }, `${index}-${line}`))), _jsx(Box, { marginLeft: 2, marginTop: 1, children: _jsx(WizardTextInput, { value: textValue, onChange: setTextValue, onSubmit: handleAccountAuthSubmit(phase.provider, phase.state), placeholder: phase.placeholder }) }), phase.err ? _jsx(ErrorLine, { message: phase.err }) : null, _jsx(HintBar, { hints: [`${phase.submitLabel} (Enter ↵)`, 'Ctrl+C cancel'] })] }));
601
999
  case 'llm-reuse-apikey':
602
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(FieldLabel, { label: `${phase.provider} API key` }), _jsxs(Text, { dimColor: true, children: [" A saved key exists for ", phase.provider] }), _jsx(SelectList, { options: ['Keep the saved key', 'Enter a new key'], cursor: phase.cursor, getLabel: (v) => v, maxVisibleRows: getListViewportHeight(terminalRows, 11), maxLineWidth: listLineWidth }), _jsx(HintBar, { hints: ['↑↓ move', 'Enter ↵ confirm', 'Ctrl+C cancel'] })] }));
1000
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(FieldLabel, { label: `${getProviderDisplayName(phase.provider)} API key` }), _jsxs(Text, { dimColor: true, children: [" A saved key exists for ", getProviderDisplayName(phase.provider)] }), _jsx(SelectList, { options: ['Keep the saved key', 'Enter a new key'], cursor: phase.cursor, getLabel: (v) => v, maxVisibleRows: getListViewportHeight(terminalRows, 11), maxLineWidth: listLineWidth }), _jsx(HintBar, { hints: ['↑↓ move', 'Enter ↵ confirm', 'Ctrl+C cancel'] })] }));
603
1001
  case 'llm-apikey':
604
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(FieldLabel, { label: `${phase.provider} API key` }), _jsx(Box, { marginLeft: 2, children: _jsx(WizardTextInput, { value: textValue, onChange: (nextValue) => {
1002
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(FieldLabel, { label: `${getProviderDisplayName(phase.provider)} API key` }), !providerRequiresApiKey(phase.provider) ? (_jsx(Text, { dimColor: true, children: " Optional for local proxy providers. Leave empty if your proxy handles account auth." })) : null, _jsx(Box, { marginLeft: 2, children: _jsx(WizardTextInput, { value: textValue, onChange: (nextValue) => {
605
1003
  setTextValue(nextValue);
606
1004
  llmApiKeyDraftsRef.current[phase.provider] = nextValue;
607
- }, onSubmit: handleLlmApiKeySubmit(phase.provider), mask: "\u25CF", placeholder: "your-api-key" }) }), phase.err ? _jsx(ErrorLine, { message: phase.err }) : null, _jsx(HintBar, { hints: ['Enter ↵ confirm', 'Ctrl+C cancel'] })] }));
1005
+ }, onSubmit: handleLlmApiKeySubmit(phase.provider), mask: "\u25CF", placeholder: providerRequiresApiKey(phase.provider) ? 'your-api-key' : 'optional' }) }), phase.err ? _jsx(ErrorLine, { message: phase.err }) : null, _jsx(HintBar, { hints: ['Enter ↵ confirm', 'Ctrl+C cancel'] })] }));
608
1006
  case 'llm-models-loading':
609
- return (_jsx(Box, { flexDirection: "column", children: _jsx(SpinnerDisplay, { message: `Fetching available models for ${phase.provider}…`, frame: spinnerFrame }) }));
1007
+ const loadingMessage = isOAuthAccountProvider(phase.provider)
1008
+ ? `Finalizing ${getProviderDisplayName(phase.provider)} account and fetching models…`
1009
+ : `Fetching available models for ${getProviderDisplayName(phase.provider)}…`;
1010
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(SpinnerDisplay, { message: loadingMessage, frame: spinnerFrame }), phase.note ? _jsxs(Text, { dimColor: true, children: [" ", phase.note] }) : null] }));
610
1011
  case 'llm-model': {
611
1012
  const modelOptions = getDisplayedModelOptions(phase.models);
612
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(FieldLabel, { label: `Default model · ${phase.provider}` }), phase.models.length === 0 ? (_jsx(Box, { marginLeft: 2, children: _jsx(WizardTextInput, { value: textValue, onChange: setTextValue, onSubmit: (v) => {
1013
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(FieldLabel, { label: `Default model · ${getProviderDisplayName(phase.provider)}` }), phase.note ? _jsxs(Text, { dimColor: true, children: [" ", phase.note] }) : null, phase.models.length === 0 ? (_jsx(Box, { marginLeft: 2, children: _jsx(WizardTextInput, { value: textValue, onChange: setTextValue, onSubmit: (v) => {
613
1014
  const m = v.trim() || phase.defModel || '';
614
1015
  if (!m)
615
1016
  return;
1017
+ const draftedBaseUrl = llmBaseUrlDraftsRef.current[phase.provider];
1018
+ const defaultBaseUrl = draftedBaseUrl ?? llmDef.getBaseUrl(phase.provider);
616
1019
  const needsUrl = llmDef.needsBaseUrl(phase.provider);
617
- if (needsUrl || llmDef.getBaseUrl(phase.provider)) {
618
- setPhase({ kind: 'llm-baseurl', provider: phase.provider, apiKey: phase.apiKey, model: m, def: llmDef.getBaseUrl(phase.provider) ?? '' });
619
- setTextValue(llmDef.getBaseUrl(phase.provider) ?? '');
1020
+ if (draftedBaseUrl) {
1021
+ saveLlmAndContinue(phase.provider, phase.apiKey, m, phase.note);
1022
+ }
1023
+ else if (needsUrl || defaultBaseUrl) {
1024
+ setPhase({ kind: 'llm-baseurl', provider: phase.provider, apiKey: phase.apiKey, model: m, def: defaultBaseUrl ?? '' });
1025
+ setTextValue(defaultBaseUrl ?? '');
620
1026
  }
621
1027
  else {
622
- callbacks.saveLlmConfig({ provider: phase.provider, apiKey: phase.apiKey, model: m });
623
- setPhase({ kind: 'surfaces', cursor: 0, selected: surfDef.surfaces });
1028
+ saveLlmAndContinue(phase.provider, phase.apiKey, m, phase.note);
624
1029
  }
625
- }, placeholder: phase.defModel }) })) : (_jsx(SelectList, { options: modelOptions, cursor: phase.cursor, getLabel: (v) => v === '__custom__' ? '⌨ enter manually…' : v, getHint: (v) => (phase.defModel && v === phase.defModel) ? 'previously selected' : undefined, maxVisibleRows: getListViewportHeight(terminalRows, 10), maxLineWidth: listLineWidth })), _jsx(HintBar, { hints: phase.models.length > 0 ? ['↑↓ move', 'Enter ↵ select', 'Ctrl+C cancel'] : ['Enter ↵ confirm', 'Ctrl+C cancel'] })] }));
1030
+ }, placeholder: phase.defModel }) })) : (_jsx(SelectList, { options: modelOptions, cursor: phase.cursor, getLabel: (v) => v === '__custom__' ? '⌨ enter manually…' : v, getHint: (v) => (phase.defModel && v === phase.defModel) ? 'previously selected' : undefined, maxVisibleRows: getListViewportHeight(terminalRows, 10), maxLineWidth: listLineWidth })), _jsx(HintBar, { hints: phase.models.length > 0
1031
+ ? ['↑↓ move', 'Enter ↵ select', 'Ctrl+C cancel']
1032
+ : ['Enter ↵ confirm', 'Ctrl+C cancel'] })] }));
626
1033
  }
627
1034
  case 'llm-baseurl':
628
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(FieldLabel, { label: `${phase.provider} base URL` }), _jsx(Text, { dimColor: true, children: " Leave empty to use the default endpoint" }), _jsx(Box, { marginLeft: 2, children: _jsx(WizardTextInput, { value: textValue, onChange: setTextValue, onSubmit: handleBaseUrlSubmit(phase.provider, phase.apiKey, phase.model), placeholder: phase.def || 'https://api.example.com/v1' }) }), phase.err ? _jsx(ErrorLine, { message: phase.err }) : null, _jsx(HintBar, { hints: ['Enter ↵ confirm (empty = default)', 'Ctrl+C cancel'] })] }));
1035
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(FieldLabel, { label: `${getProviderDisplayName(phase.provider)} base URL` }), _jsx(Text, { dimColor: true, children: " Leave empty to use the default endpoint" }), _jsx(Box, { marginLeft: 2, children: _jsx(WizardTextInput, { value: textValue, onChange: setTextValue, onSubmit: handleBaseUrlSubmit(phase.provider, phase.apiKey, phase.model), placeholder: phase.def || 'https://api.example.com/v1' }) }), phase.err ? _jsx(ErrorLine, { message: phase.err }) : null, _jsx(HintBar, { hints: ['Enter ↵ confirm (empty = default)', 'Ctrl+C cancel'] })] }));
629
1036
  case 'surfaces':
630
1037
  return (_jsxs(Box, { flexDirection: "column", children: [_jsx(FieldLabel, { label: "Optional messaging gateways" }), _jsx(Text, { dimColor: true, children: " These enable Yagr to receive work from external channels" }), _jsx(Box, { marginTop: 1, children: _jsx(MultiSelectList, { options: SURFACE_OPTIONS, cursor: phase.cursor, selected: phase.selected, maxVisibleRows: getListViewportHeight(terminalRows, 11), maxLineWidth: listLineWidth }) }), _jsx(HintBar, { hints: ['↑↓ move', 'Space toggle', 'Enter ↵ confirm', 'Ctrl+C cancel'] })] }));
631
1038
  case 'telegram-reuse-token':
@@ -635,7 +1042,7 @@ function SetupWizard({ callbacks, onDone }) {
635
1042
  case 'telegram-connecting':
636
1043
  return (_jsx(Box, { flexDirection: "column", children: _jsx(SpinnerDisplay, { message: "Verifying Telegram bot token\u2026", frame: spinnerFrame }) }));
637
1044
  case 'done':
638
- return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "green", bold: true, children: [CHECK, " Setup complete"] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [phase.n8nHost ? _jsxs(Text, { dimColor: true, children: [" n8n ", DOT, " ", phase.n8nHost] }) : null, phase.provider ? _jsxs(Text, { dimColor: true, children: [" LLM ", DOT, " ", phase.provider, phase.model ? ` / ${phase.model}` : ''] }) : null, phase.surfaces.length > 0
1045
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "green", bold: true, children: [CHECK, " Setup complete"] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [phase.n8nHost ? _jsxs(Text, { dimColor: true, children: [" n8n ", DOT, " ", phase.n8nHost] }) : null, phase.provider ? _jsxs(Text, { dimColor: true, children: [" LLM ", DOT, " ", getProviderDisplayName(phase.provider), phase.model ? ` / ${phase.model}` : ''] }) : null, phase.surfaces.length > 0
639
1046
  ? _jsxs(Text, { dimColor: true, children: [" Gates ", DOT, " ", phase.surfaces.join(', ')] })
640
1047
  : _jsxs(Text, { dimColor: true, children: [" Gates ", DOT, " none"] })] }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { dimColor: true, children: "Next: " }), _jsx(Text, { color: "cyan", children: "yagr start" })] })] }));
641
1048
  case 'cancelled':