@yagr/agent 0.2.9 → 0.2.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/dist/cli.js +251 -9
  2. package/dist/cli.js.map +1 -1
  3. package/dist/config/n8n-config-service.d.ts +2 -1
  4. package/dist/config/n8n-config-service.d.ts.map +1 -1
  5. package/dist/config/n8n-config-service.js +2 -1
  6. package/dist/config/n8n-config-service.js.map +1 -1
  7. package/dist/config/yagr-config-service.d.ts +1 -1
  8. package/dist/config/yagr-config-service.d.ts.map +1 -1
  9. package/dist/config/yagr-home.d.ts +7 -0
  10. package/dist/config/yagr-home.d.ts.map +1 -1
  11. package/dist/config/yagr-home.js +19 -0
  12. package/dist/config/yagr-home.js.map +1 -1
  13. package/dist/gateway/format-message.d.ts +3 -0
  14. package/dist/gateway/format-message.d.ts.map +1 -1
  15. package/dist/gateway/format-message.js +63 -10
  16. package/dist/gateway/format-message.js.map +1 -1
  17. package/dist/gateway/interactive-ui.d.ts.map +1 -1
  18. package/dist/gateway/interactive-ui.js +49 -4
  19. package/dist/gateway/interactive-ui.js.map +1 -1
  20. package/dist/gateway/local-open-bridge.d.ts +3 -0
  21. package/dist/gateway/local-open-bridge.d.ts.map +1 -0
  22. package/dist/gateway/local-open-bridge.js +54 -0
  23. package/dist/gateway/local-open-bridge.js.map +1 -0
  24. package/dist/gateway/webui.d.ts.map +1 -1
  25. package/dist/gateway/webui.js +50 -53
  26. package/dist/gateway/webui.js.map +1 -1
  27. package/dist/gateway/workflow-links.d.ts +12 -0
  28. package/dist/gateway/workflow-links.d.ts.map +1 -0
  29. package/dist/gateway/workflow-links.js +53 -0
  30. package/dist/gateway/workflow-links.js.map +1 -0
  31. package/dist/llm/anthropic-account.d.ts +20 -0
  32. package/dist/llm/anthropic-account.d.ts.map +1 -0
  33. package/dist/llm/anthropic-account.js +111 -0
  34. package/dist/llm/anthropic-account.js.map +1 -0
  35. package/dist/llm/copilot-account.d.ts +36 -0
  36. package/dist/llm/copilot-account.d.ts.map +1 -0
  37. package/dist/llm/copilot-account.js +573 -0
  38. package/dist/llm/copilot-account.js.map +1 -0
  39. package/dist/llm/create-language-model.d.ts +2 -2
  40. package/dist/llm/create-language-model.d.ts.map +1 -1
  41. package/dist/llm/create-language-model.js +187 -38
  42. package/dist/llm/create-language-model.js.map +1 -1
  43. package/dist/llm/google-account.d.ts +31 -0
  44. package/dist/llm/google-account.d.ts.map +1 -0
  45. package/dist/llm/google-account.js +851 -0
  46. package/dist/llm/google-account.js.map +1 -0
  47. package/dist/llm/model-catalog-cache.d.ts +4 -0
  48. package/dist/llm/model-catalog-cache.d.ts.map +1 -0
  49. package/dist/llm/model-catalog-cache.js +46 -0
  50. package/dist/llm/model-catalog-cache.js.map +1 -0
  51. package/dist/llm/openai-account.d.ts +33 -0
  52. package/dist/llm/openai-account.d.ts.map +1 -0
  53. package/dist/llm/openai-account.js +578 -0
  54. package/dist/llm/openai-account.js.map +1 -0
  55. package/dist/llm/provider-discovery.d.ts +3 -0
  56. package/dist/llm/provider-discovery.d.ts.map +1 -0
  57. package/dist/llm/provider-discovery.js +40 -0
  58. package/dist/llm/provider-discovery.js.map +1 -0
  59. package/dist/llm/provider-registry.d.ts +37 -0
  60. package/dist/llm/provider-registry.d.ts.map +1 -0
  61. package/dist/llm/provider-registry.js +186 -0
  62. package/dist/llm/provider-registry.js.map +1 -0
  63. package/dist/llm/proxy-runtime.d.ts +37 -0
  64. package/dist/llm/proxy-runtime.d.ts.map +1 -0
  65. package/dist/llm/proxy-runtime.js +462 -0
  66. package/dist/llm/proxy-runtime.js.map +1 -0
  67. package/dist/llm/test-model-policy.d.ts +3 -0
  68. package/dist/llm/test-model-policy.d.ts.map +1 -0
  69. package/dist/llm/test-model-policy.js +16 -0
  70. package/dist/llm/test-model-policy.js.map +1 -0
  71. package/dist/n8n-local/bootstrap.d.ts +12 -0
  72. package/dist/n8n-local/bootstrap.d.ts.map +1 -0
  73. package/dist/n8n-local/bootstrap.js +281 -0
  74. package/dist/n8n-local/bootstrap.js.map +1 -0
  75. package/dist/n8n-local/browser-auth.d.ts +12 -0
  76. package/dist/n8n-local/browser-auth.d.ts.map +1 -0
  77. package/dist/n8n-local/browser-auth.js +159 -0
  78. package/dist/n8n-local/browser-auth.js.map +1 -0
  79. package/dist/n8n-local/detect.d.ts +50 -0
  80. package/dist/n8n-local/detect.d.ts.map +1 -0
  81. package/dist/n8n-local/detect.js +207 -0
  82. package/dist/n8n-local/detect.js.map +1 -0
  83. package/dist/n8n-local/direct-manager.d.ts +15 -0
  84. package/dist/n8n-local/direct-manager.d.ts.map +1 -0
  85. package/dist/n8n-local/direct-manager.js +232 -0
  86. package/dist/n8n-local/direct-manager.js.map +1 -0
  87. package/dist/n8n-local/docker-manager.d.ts +18 -0
  88. package/dist/n8n-local/docker-manager.d.ts.map +1 -0
  89. package/dist/n8n-local/docker-manager.js +217 -0
  90. package/dist/n8n-local/docker-manager.js.map +1 -0
  91. package/dist/n8n-local/managed-runtime.d.ts +17 -0
  92. package/dist/n8n-local/managed-runtime.d.ts.map +1 -0
  93. package/dist/n8n-local/managed-runtime.js +122 -0
  94. package/dist/n8n-local/managed-runtime.js.map +1 -0
  95. package/dist/n8n-local/owner-credentials.d.ts +15 -0
  96. package/dist/n8n-local/owner-credentials.d.ts.map +1 -0
  97. package/dist/n8n-local/owner-credentials.js +50 -0
  98. package/dist/n8n-local/owner-credentials.js.map +1 -0
  99. package/dist/n8n-local/plan.d.ts +16 -0
  100. package/dist/n8n-local/plan.d.ts.map +1 -0
  101. package/dist/n8n-local/plan.js +48 -0
  102. package/dist/n8n-local/plan.js.map +1 -0
  103. package/dist/n8n-local/state.d.ts +42 -0
  104. package/dist/n8n-local/state.d.ts.map +1 -0
  105. package/dist/n8n-local/state.js +108 -0
  106. package/dist/n8n-local/state.js.map +1 -0
  107. package/dist/n8n-local/workflow-open.d.ts +21 -0
  108. package/dist/n8n-local/workflow-open.d.ts.map +1 -0
  109. package/dist/n8n-local/workflow-open.js +50 -0
  110. package/dist/n8n-local/workflow-open.js.map +1 -0
  111. package/dist/runtime/context-compaction.d.ts.map +1 -1
  112. package/dist/runtime/context-compaction.js +7 -0
  113. package/dist/runtime/context-compaction.js.map +1 -1
  114. package/dist/runtime/run-engine.d.ts.map +1 -1
  115. package/dist/runtime/run-engine.js +52 -1
  116. package/dist/runtime/run-engine.js.map +1 -1
  117. package/dist/setup/setup-wizard.d.ts +36 -3
  118. package/dist/setup/setup-wizard.d.ts.map +1 -1
  119. package/dist/setup/setup-wizard.js +470 -62
  120. package/dist/setup/setup-wizard.js.map +1 -1
  121. package/dist/setup.d.ts +2 -1
  122. package/dist/setup.d.ts.map +1 -1
  123. package/dist/setup.js +204 -89
  124. package/dist/setup.js.map +1 -1
  125. package/dist/system/open-external.d.ts +2 -0
  126. package/dist/system/open-external.d.ts.map +1 -0
  127. package/dist/system/open-external.js +25 -0
  128. package/dist/system/open-external.js.map +1 -0
  129. package/dist/system/package-manager.d.ts +6 -0
  130. package/dist/system/package-manager.d.ts.map +1 -0
  131. package/dist/system/package-manager.js +13 -0
  132. package/dist/system/package-manager.js.map +1 -0
  133. package/dist/tools/build-tools.d.ts +110 -65
  134. package/dist/tools/build-tools.d.ts.map +1 -1
  135. package/dist/tools/deploy.d.ts +14 -14
  136. package/dist/tools/generate-workflow.d.ts +5 -5
  137. package/dist/tools/n8nac.d.ts +96 -53
  138. package/dist/tools/n8nac.d.ts.map +1 -1
  139. package/dist/tools/n8nac.js +52 -20
  140. package/dist/tools/n8nac.js.map +1 -1
  141. package/dist/tools/observer.d.ts +1 -0
  142. package/dist/tools/observer.d.ts.map +1 -1
  143. package/dist/tools/observer.js.map +1 -1
  144. package/dist/tools/present-workflow-result.d.ts +2 -0
  145. package/dist/tools/present-workflow-result.d.ts.map +1 -1
  146. package/dist/tools/present-workflow-result.js +11 -2
  147. package/dist/tools/present-workflow-result.js.map +1 -1
  148. package/dist/tools/request-required-action.d.ts +7 -7
  149. package/dist/tools/request-required-action.js +4 -4
  150. package/dist/tools/request-required-action.js.map +1 -1
  151. package/dist/tools/validate.d.ts +14 -14
  152. package/dist/tools/write-workspace-file.d.ts +5 -5
  153. package/dist/types.d.ts +1 -0
  154. package/dist/types.d.ts.map +1 -1
  155. package/dist/webui/app.js +14 -1
  156. package/dist/webui/app.js.map +2 -2
  157. package/package.json +15 -2
@@ -1,20 +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
25
  ];
15
- export function runSetupWizard(callbacks) {
26
+ export function runSetupWizard(callbacks, options = {}) {
16
27
  return new Promise((resolve) => {
17
- 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); } }));
18
29
  });
19
30
  }
20
31
  function sectionFor(phase) {
@@ -33,15 +44,65 @@ function sectionIndex(phase) {
33
44
  return 2;
34
45
  return 3;
35
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
+ }
36
95
  // ─── Primitive UI components ──────────────────────────────────────────────────
37
96
  function Rule() {
38
97
  return _jsx(Text, { dimColor: true, children: '─'.repeat(56) });
39
98
  }
40
- function Header({ phase }) {
99
+ function Header({ phase, mode }) {
41
100
  const section = sectionFor(phase);
42
101
  const idx = sectionIndex(phase);
43
102
  const isDone = phase.kind === 'done' || phase.kind === 'cancelled' || phase.kind === 'error';
44
- 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, {})] }));
45
106
  }
46
107
  function HintBar({ hints }) {
47
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))) })] }));
@@ -165,11 +226,16 @@ function SelectList({ options, cursor, getLabel, getHint, maxVisibleRows, maxLin
165
226
  const active = i === cursor;
166
227
  const hint = getHint?.(opt);
167
228
  const prefix = active ? ` ${CURSOR} ` : ' ';
168
- const suffix = hint ? ` ${DOT} ${hint}` : '';
169
- const line = truncateTerminalLine(`${prefix}${getLabel(opt)}${suffix}`, availableWidth);
229
+ const line = truncateTerminalLine(formatOptionLineWithHint(prefix, getLabel(opt), hint, availableWidth), availableWidth);
170
230
  return (_jsx(Box, { children: _jsx(Text, { color: active ? 'cyan' : undefined, bold: active, children: line }) }, i));
171
231
  }), end < options.length ? _jsx(Text, { dimColor: true, children: truncateTerminalLine(` ↓ ${options.length - end} more`, availableWidth) }) : null] }));
172
232
  }
233
+ function formatOptionLineWithHint(prefix, label, hint, width) {
234
+ if (!hint) {
235
+ return `${prefix}${label}`;
236
+ }
237
+ return `${prefix}${label} ${DOT} ${hint}`;
238
+ }
173
239
  function MultiSelectList({ options, cursor, selected, maxVisibleRows, maxLineWidth, }) {
174
240
  const visibleRows = Math.max(1, maxVisibleRows ?? options.length);
175
241
  const { start, end } = getVisibleWindow(options.length, cursor, visibleRows);
@@ -186,21 +252,41 @@ function MultiSelectList({ options, cursor, selected, maxVisibleRows, maxLineWid
186
252
  }), end < options.length ? _jsx(Text, { dimColor: true, children: truncateTerminalLine(` ↓ ${options.length - end} more`, availableWidth) }) : null] }));
187
253
  }
188
254
  // ─── Main wizard component ────────────────────────────────────────────────────
189
- function SetupWizard({ callbacks, onDone }) {
255
+ function SetupWizard({ callbacks, options, onDone }) {
190
256
  const app = useApp();
191
257
  const { stdout } = useStdout();
192
258
  const n8nDef = callbacks.getN8nDefaults();
193
259
  const llmDef = callbacks.getLlmDefaults();
194
260
  const surfDef = callbacks.getSurfaceDefaults();
195
- const [phase, setPhase] = useState({ kind: 'n8n-url', def: n8nDef.url });
196
- 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('');
197
282
  const [spinnerFrame, setSpinnerFrame] = useState(0);
198
283
  const asyncGuard = useRef(0);
199
284
  const llmApiKeyDraftsRef = useRef({});
285
+ const llmBaseUrlDraftsRef = useRef({});
200
286
  const terminalRows = stdout?.rows ?? process.stdout.rows ?? 24;
201
287
  const terminalColumns = stdout?.columns ?? process.stdout.columns ?? 80;
202
288
  const listLineWidth = Math.max(12, terminalColumns - 6);
203
- 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'
204
290
  || phase.kind === 'llm-models-loading' || phase.kind === 'telegram-connecting';
205
291
  useEffect(() => {
206
292
  if (!isLoading)
@@ -217,6 +303,45 @@ function SetupWizard({ callbacks, onDone }) {
217
303
  cancel('Setup cancelled.');
218
304
  }
219
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, runtimeSource: 'managed-local' });
320
+ return;
321
+ }
322
+ const existing = callbacks.getN8nDefaults(state.url).apiKey;
323
+ if (existing) {
324
+ setPhase({ kind: 'n8n-connecting', url: state.url, apiKey: existing, runtimeSource: 'managed-local' });
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
220
345
  useEffect(() => {
221
346
  if (phase.kind !== 'n8n-connecting')
222
347
  return;
@@ -229,14 +354,14 @@ function SetupWizard({ callbacks, onDone }) {
229
354
  if (projects.length === 1) {
230
355
  setPhase({
231
356
  kind: 'n8n-syncfolder',
232
- url: phase.url, apiKey: phase.apiKey,
357
+ url: phase.url, apiKey: phase.apiKey, runtimeSource: phase.runtimeSource,
233
358
  project: projects[0],
234
359
  def: n8nDef.syncFolder ?? 'workflows',
235
360
  });
236
361
  setTextValue(n8nDef.syncFolder ?? 'workflows');
237
362
  }
238
363
  else {
239
- setPhase({ kind: 'n8n-project', url: phase.url, apiKey: phase.apiKey, projects, cursor: 0 });
364
+ setPhase({ kind: 'n8n-project', url: phase.url, apiKey: phase.apiKey, runtimeSource: phase.runtimeSource, projects, cursor: 0 });
240
365
  }
241
366
  }
242
367
  catch (err) {
@@ -253,15 +378,15 @@ function SetupWizard({ callbacks, onDone }) {
253
378
  const guard = ++asyncGuard.current;
254
379
  void (async () => {
255
380
  try {
256
- await callbacks.saveN8nConfig({ url: phase.url, apiKey: phase.apiKey, project: phase.project, syncFolder: phase.syncFolder });
381
+ await callbacks.saveN8nConfig({ url: phase.url, apiKey: phase.apiKey, project: phase.project, syncFolder: phase.syncFolder, runtimeSource: phase.runtimeSource });
257
382
  if (guard !== asyncGuard.current)
258
383
  return;
259
384
  const llmProvider = llmDef.provider;
260
385
  if (llmProvider) {
261
386
  const existingApiKey = llmDef.getApiKey(llmProvider);
262
387
  const existingModel = llmDef.getDefaultModel(llmProvider);
263
- if (existingApiKey && existingModel) {
264
- 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 });
265
390
  return;
266
391
  }
267
392
  }
@@ -281,16 +406,56 @@ function SetupWizard({ callbacks, onDone }) {
281
406
  const guard = ++asyncGuard.current;
282
407
  void (async () => {
283
408
  try {
284
- 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
+ }
285
449
  if (guard !== asyncGuard.current)
286
450
  return;
287
451
  const displayedOptions = getDisplayedModelOptions(models);
288
452
  const idx = phase.defModel ? displayedOptions.indexOf(phase.defModel) : -1;
289
453
  setPhase({
290
454
  kind: 'llm-model',
291
- provider: phase.provider, apiKey: phase.apiKey,
455
+ provider: phase.provider, apiKey: resolvedApiKey,
292
456
  models, defModel: phase.defModel,
293
457
  cursor: idx >= 0 ? idx : 0,
458
+ note,
294
459
  });
295
460
  }
296
461
  catch {
@@ -299,7 +464,7 @@ function SetupWizard({ callbacks, onDone }) {
299
464
  setPhase({
300
465
  kind: 'llm-model',
301
466
  provider: phase.provider, apiKey: phase.apiKey,
302
- models: [], defModel: phase.defModel, cursor: 0,
467
+ models: [], defModel: phase.defModel, cursor: 0, note: phase.note,
303
468
  });
304
469
  }
305
470
  })();
@@ -357,26 +522,16 @@ function SetupWizard({ callbacks, onDone }) {
357
522
  setPhase((p) => ({ ...p, err: 'API key is required.' }));
358
523
  return;
359
524
  }
360
- setPhase({ kind: 'n8n-connecting', url, apiKey: key });
525
+ setPhase({ kind: 'n8n-connecting', url, apiKey: key, runtimeSource: 'external' });
361
526
  }, []);
362
- const handleSyncFolderSubmit = useCallback((url, apiKey, project) => (value) => {
527
+ const handleSyncFolderSubmit = useCallback((url, apiKey, runtimeSource, project) => (value) => {
363
528
  const folder = value.trim();
364
529
  if (!folder) {
365
530
  setPhase((p) => ({ ...p, err: 'Sync folder is required.' }));
366
531
  return;
367
532
  }
368
- setPhase({ kind: 'n8n-saving', url, apiKey, project, syncFolder: folder });
533
+ setPhase({ kind: 'n8n-saving', url, apiKey, runtimeSource, project, syncFolder: folder });
369
534
  }, []);
370
- const handleLlmApiKeySubmit = useCallback((provider) => (value) => {
371
- const key = value.trim();
372
- if (!key) {
373
- setPhase((p) => ({ ...p, err: 'API key is required.' }));
374
- return;
375
- }
376
- llmApiKeyDraftsRef.current[provider] = key;
377
- const defModel = llmDef.getDefaultModel(provider);
378
- setPhase({ kind: 'llm-models-loading', provider, apiKey: key, defModel });
379
- }, [llmDef]);
380
535
  const handleBaseUrlSubmit = useCallback((provider, apiKey, model) => (value) => {
381
536
  const url = value.trim();
382
537
  if (url) {
@@ -400,15 +555,125 @@ function SetupWizard({ callbacks, onDone }) {
400
555
  }
401
556
  setPhase({ kind: 'telegram-connecting', surfaces, token });
402
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]);
403
609
  const handleSelectKey = useCallback((input, key) => {
404
- 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, runtimeSource: 'managed-local' });
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') {
405
670
  if (key.upArrow)
406
671
  setPhase({ ...phase, cursor: Math.max(0, phase.cursor - 1) });
407
672
  else if (key.downArrow)
408
673
  setPhase({ ...phase, cursor: Math.min(1, phase.cursor + 1) });
409
674
  else if (key.return) {
410
675
  if (phase.cursor === 0) {
411
- setPhase({ kind: 'n8n-connecting', url: phase.url, apiKey: phase.existing });
676
+ setPhase({ kind: 'n8n-connecting', url: phase.url, apiKey: phase.existing, runtimeSource: 'external' });
412
677
  }
413
678
  else {
414
679
  setPhase({ kind: 'n8n-apikey', url: phase.url });
@@ -425,7 +690,7 @@ function SetupWizard({ callbacks, onDone }) {
425
690
  setPhase({ ...phase, cursor: Math.min(phase.projects.length - 1, phase.cursor + 1) });
426
691
  else if (key.return) {
427
692
  const project = phase.projects[phase.cursor];
428
- setPhase({ kind: 'n8n-syncfolder', url: phase.url, apiKey: phase.apiKey, project, def: n8nDef.syncFolder ?? 'workflows' });
693
+ setPhase({ kind: 'n8n-syncfolder', url: phase.url, apiKey: phase.apiKey, runtimeSource: phase.runtimeSource, project, def: n8nDef.syncFolder ?? 'workflows' });
429
694
  setTextValue(n8nDef.syncFolder ?? 'workflows');
430
695
  }
431
696
  else if (key.escape)
@@ -439,10 +704,13 @@ function SetupWizard({ callbacks, onDone }) {
439
704
  else if (key.return) {
440
705
  if (phase.cursor === 0) {
441
706
  llmApiKeyDraftsRef.current[phase.provider] = phase.apiKey;
442
- 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('');
443
710
  setPhase({ kind: 'surfaces', cursor: 0, selected: surfDef.surfaces });
444
711
  }
445
712
  else {
713
+ setTextValue('');
446
714
  setPhase({ kind: 'llm-provider', initial: phase.provider, cursor: VALID_PROVIDERS.indexOf(phase.provider) });
447
715
  }
448
716
  }
@@ -457,7 +725,16 @@ function SetupWizard({ callbacks, onDone }) {
457
725
  else if (key.return) {
458
726
  const provider = VALID_PROVIDERS[phase.cursor];
459
727
  const existing = llmApiKeyDraftsRef.current[provider] ?? llmDef.getApiKey(provider);
460
- 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('');
461
738
  setPhase({ kind: 'llm-reuse-apikey', provider, existing, cursor: 0 });
462
739
  }
463
740
  else {
@@ -468,6 +745,81 @@ function SetupWizard({ callbacks, onDone }) {
468
745
  else if (key.escape)
469
746
  cancel('Setup cancelled.');
470
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
+ }
471
823
  else if (phase.kind === 'llm-reuse-apikey') {
472
824
  if (key.upArrow)
473
825
  setPhase({ ...phase, cursor: Math.max(0, phase.cursor - 1) });
@@ -477,7 +829,7 @@ function SetupWizard({ callbacks, onDone }) {
477
829
  if (phase.cursor === 0) {
478
830
  const defModel = llmDef.getDefaultModel(phase.provider);
479
831
  llmApiKeyDraftsRef.current[phase.provider] = phase.existing;
480
- setPhase({ kind: 'llm-models-loading', provider: phase.provider, apiKey: phase.existing, defModel });
832
+ transitionToLlmModelsLoading(phase.provider, phase.existing, defModel);
481
833
  }
482
834
  else {
483
835
  setPhase({ kind: 'llm-apikey', provider: phase.provider });
@@ -501,14 +853,18 @@ function SetupWizard({ callbacks, onDone }) {
501
853
  return;
502
854
  }
503
855
  const model = selected;
856
+ const draftedBaseUrl = llmBaseUrlDraftsRef.current[phase.provider];
857
+ const defaultBaseUrl = draftedBaseUrl ?? llmDef.getBaseUrl(phase.provider);
504
858
  const needsUrl = llmDef.needsBaseUrl(phase.provider);
505
- if (needsUrl || llmDef.getBaseUrl(phase.provider)) {
506
- setPhase({ kind: 'llm-baseurl', provider: phase.provider, apiKey: phase.apiKey, model, def: llmDef.getBaseUrl(phase.provider) ?? '' });
507
- 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 ?? '');
508
865
  }
509
866
  else {
510
- callbacks.saveLlmConfig({ provider: phase.provider, apiKey: phase.apiKey, model });
511
- setPhase({ kind: 'surfaces', cursor: 0, selected: surfDef.surfaces });
867
+ saveLlmAndContinue(phase.provider, phase.apiKey, model, phase.note);
512
868
  }
513
869
  }
514
870
  else if (key.escape)
@@ -568,7 +924,7 @@ function SetupWizard({ callbacks, onDone }) {
568
924
  cancel('Setup cancelled.');
569
925
  }
570
926
  }, [phase, cancel, callbacks, llmDef, surfDef, n8nDef.syncFolder, app, onDone]);
571
- 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)
572
928
  || (phase.kind === 'llm-model' && phase.models.length > 0);
573
929
  useInput((input, key) => {
574
930
  if (key.ctrl && input === 'c')
@@ -576,9 +932,23 @@ function SetupWizard({ callbacks, onDone }) {
576
932
  if (isSelectPhase)
577
933
  handleSelectKey(input, key);
578
934
  }, { isActive: isSelectPhase });
579
- 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()] }));
580
936
  function renderPhase() {
581
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'] })] }));
582
952
  case 'n8n-url':
583
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'] })] }));
584
954
  case 'n8n-reuse-apikey':
@@ -587,44 +957,82 @@ function SetupWizard({ callbacks, onDone }) {
587
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'] })] }));
588
958
  case 'n8n-connecting':
589
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'] })] }));
590
972
  case 'n8n-project':
591
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'] })] }));
592
974
  case 'n8n-syncfolder':
593
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(FieldLabel, { label: "Local workflow sync folder" }), _jsx(Box, { marginLeft: 2, children: _jsx(WizardTextInput, { value: textValue, onChange: setTextValue, onSubmit: handleSyncFolderSubmit(phase.url, phase.apiKey, phase.project), placeholder: "workflows" }) }), phase.err ? _jsx(ErrorLine, { message: phase.err }) : null, _jsx(HintBar, { hints: ['Enter ↵ confirm', 'Ctrl+C cancel'] })] }));
975
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(FieldLabel, { label: "Local workflow sync folder" }), _jsx(Box, { marginLeft: 2, children: _jsx(WizardTextInput, { value: textValue, onChange: setTextValue, onSubmit: handleSyncFolderSubmit(phase.url, phase.apiKey, phase.runtimeSource, phase.project), placeholder: "workflows" }) }), phase.err ? _jsx(ErrorLine, { message: phase.err }) : null, _jsx(HintBar, { hints: ['Enter ↵ confirm', 'Ctrl+C cancel'] })] }));
594
976
  case 'n8n-saving':
595
977
  return (_jsx(Box, { flexDirection: "column", children: _jsx(SpinnerDisplay, { message: "Saving n8n configuration and refreshing workspace\u2026", frame: spinnerFrame }) }));
596
978
  case 'llm-reuse-config':
597
- 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'] })] }));
598
980
  case 'llm-provider':
599
- 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'] })] }));
600
999
  case 'llm-reuse-apikey':
601
- 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'] })] }));
602
1001
  case 'llm-apikey':
603
- 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) => {
604
1003
  setTextValue(nextValue);
605
1004
  llmApiKeyDraftsRef.current[phase.provider] = nextValue;
606
- }, 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'] })] }));
607
1006
  case 'llm-models-loading':
608
- 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] }));
609
1011
  case 'llm-model': {
610
1012
  const modelOptions = getDisplayedModelOptions(phase.models);
611
- 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) => {
612
1014
  const m = v.trim() || phase.defModel || '';
613
1015
  if (!m)
614
1016
  return;
1017
+ const draftedBaseUrl = llmBaseUrlDraftsRef.current[phase.provider];
1018
+ const defaultBaseUrl = draftedBaseUrl ?? llmDef.getBaseUrl(phase.provider);
615
1019
  const needsUrl = llmDef.needsBaseUrl(phase.provider);
616
- if (needsUrl || llmDef.getBaseUrl(phase.provider)) {
617
- setPhase({ kind: 'llm-baseurl', provider: phase.provider, apiKey: phase.apiKey, model: m, def: llmDef.getBaseUrl(phase.provider) ?? '' });
618
- 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 ?? '');
619
1026
  }
620
1027
  else {
621
- callbacks.saveLlmConfig({ provider: phase.provider, apiKey: phase.apiKey, model: m });
622
- setPhase({ kind: 'surfaces', cursor: 0, selected: surfDef.surfaces });
1028
+ saveLlmAndContinue(phase.provider, phase.apiKey, m, phase.note);
623
1029
  }
624
- }, 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'] })] }));
625
1033
  }
626
1034
  case 'llm-baseurl':
627
- 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'] })] }));
628
1036
  case 'surfaces':
629
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'] })] }));
630
1038
  case 'telegram-reuse-token':
@@ -634,7 +1042,7 @@ function SetupWizard({ callbacks, onDone }) {
634
1042
  case 'telegram-connecting':
635
1043
  return (_jsx(Box, { flexDirection: "column", children: _jsx(SpinnerDisplay, { message: "Verifying Telegram bot token\u2026", frame: spinnerFrame }) }));
636
1044
  case 'done':
637
- 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
638
1046
  ? _jsxs(Text, { dimColor: true, children: [" Gates ", DOT, " ", phase.surfaces.join(', ')] })
639
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" })] })] }));
640
1048
  case 'cancelled':