@undefineds.co/linx 0.2.3 → 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 (49) hide show
  1. package/dist/index.js +225 -51
  2. package/dist/index.js.map +1 -1
  3. package/dist/lib/ai-command.js +2 -2
  4. package/dist/lib/ai-command.js.map +1 -1
  5. package/dist/lib/chat-api.js +146 -15
  6. package/dist/lib/chat-api.js.map +1 -1
  7. package/dist/lib/credentials-store.js +6 -0
  8. package/dist/lib/credentials-store.js.map +1 -1
  9. package/dist/lib/default-model.js +1 -0
  10. package/dist/lib/default-model.js.map +1 -1
  11. package/dist/lib/login-command.js +16 -1
  12. package/dist/lib/login-command.js.map +1 -1
  13. package/dist/lib/models.js +2 -2
  14. package/dist/lib/models.js.map +1 -1
  15. package/dist/lib/node-warning-filter.js +34 -0
  16. package/dist/lib/node-warning-filter.js.map +1 -0
  17. package/dist/lib/oidc-auth.js +78 -13
  18. package/dist/lib/oidc-auth.js.map +1 -1
  19. package/dist/lib/pi-adapter/auth.js +12 -5
  20. package/dist/lib/pi-adapter/auth.js.map +1 -1
  21. package/dist/lib/pi-adapter/branding.js +553 -75
  22. package/dist/lib/pi-adapter/branding.js.map +1 -1
  23. package/dist/lib/pi-adapter/interactive.js +117 -4
  24. package/dist/lib/pi-adapter/interactive.js.map +1 -1
  25. package/dist/lib/pi-adapter/pod-approval.js +121 -0
  26. package/dist/lib/pi-adapter/pod-approval.js.map +1 -0
  27. package/dist/lib/pi-adapter/pod-mirror-mapping.js +189 -0
  28. package/dist/lib/pi-adapter/pod-mirror-mapping.js.map +1 -0
  29. package/dist/lib/pi-adapter/pod-mirror.js +390 -0
  30. package/dist/lib/pi-adapter/pod-mirror.js.map +1 -0
  31. package/dist/lib/pi-adapter/pod-native.js +422 -0
  32. package/dist/lib/pi-adapter/pod-native.js.map +1 -0
  33. package/dist/lib/pi-adapter/runtime.js +116 -21
  34. package/dist/lib/pi-adapter/runtime.js.map +1 -1
  35. package/dist/lib/pi-adapter/session.js +613 -0
  36. package/dist/lib/pi-adapter/session.js.map +1 -0
  37. package/dist/lib/pi-adapter/stream.js +74 -1
  38. package/dist/lib/pi-adapter/stream.js.map +1 -1
  39. package/dist/lib/profile-identity.js +5 -5
  40. package/dist/lib/profile-identity.js.map +1 -1
  41. package/dist/lib/watch/pod-ai.js +2 -1
  42. package/dist/lib/watch/pod-ai.js.map +1 -1
  43. package/dist/lib/watch/pod-approval.js +336 -84
  44. package/dist/lib/watch/pod-approval.js.map +1 -1
  45. package/dist/lib/watch/pod-persistence.js +2 -1
  46. package/dist/lib/watch/pod-persistence.js.map +1 -1
  47. package/package.json +3 -7
  48. package/dist/generated/version.js +0 -3
  49. package/dist/generated/version.js.map +0 -1
@@ -1,24 +1,45 @@
1
1
  import { spawn } from 'node:child_process';
2
2
  import { homedir } from 'node:os';
3
3
  import { basename, join } from 'node:path';
4
+ import { readFileSync } from 'node:fs';
4
5
  import { LINX_HOME_DIRNAME } from '@undefineds.co/models/client';
5
- import { keyHint, rawKeyHint } from '@mariozechner/pi-coding-agent';
6
+ import { keyHint, LoginDialogComponent, rawKeyHint } from '@mariozechner/pi-coding-agent';
6
7
  import { Text, truncateToWidth, visibleWidth, wrapTextWithAnsi } from '@mariozechner/pi-tui';
7
8
  import { loadCredentials } from '../credentials-store.js';
8
9
  import { extractUsernameFromWebId, resolveProfileDisplayName } from '../profile-identity.js';
9
- import { LINX_CLI_VERSION } from '../../generated/version.js';
10
10
  export const LINX_AGENT_DIR = join(homedir(), LINX_HOME_DIRNAME, 'agent');
11
11
  export const LINX_UPDATE_PACKAGE_NAME = '@undefineds.co/linx';
12
12
  export const LINX_CHANGELOG_URL = 'https://github.com/undefineds-co/linx-cli/releases';
13
+ export const LINX_CLI_VERSION = readLinxCliVersion();
14
+ const LINX_AUTH_LOGIN_IN_PROGRESS = Symbol.for('linx.tui.authLoginInProgress');
15
+ const LINX_AUTH_LOGIN_ON_INIT = Symbol.for('linx.tui.authLoginOnInit');
16
+ const LINX_AUTH_LOGIN_SCHEDULED = Symbol.for('linx.tui.authLoginScheduled');
17
+ const LINX_AUTH_REPORTING_ERROR = Symbol.for('linx.tui.authReportingError');
13
18
  const LINX_UPDATE_IN_PROGRESS = Symbol.for('linx.tui.updateInProgress');
14
- const LINX_UPDATE_RUNNER = Symbol.for('linx.tui.updateRunner');
19
+ const LINX_PROVIDER_ID = 'undefineds';
20
+ const AUTH_OPTION_BROWSER = 'Authorize in browser';
21
+ const AUTH_OPTION_API_KEY = 'Enter API key';
22
+ const AUTH_OPTION_EXIT = 'Exit';
23
+ const UPDATE_OPTION_INSTALL = 'Install update and restart';
24
+ const UPDATE_OPTION_CHANGELOG = 'Open changelog';
25
+ const UPDATE_OPTION_LATER = 'Later';
15
26
  export function applyLinxInteractiveBranding(interactive) {
16
27
  patchTerminalTitle(interactive);
17
28
  patchVersionCheck(interactive);
18
29
  patchUpdateNotification(interactive);
19
- patchUpdateCommand(interactive);
30
+ patchNativeOAuthSelectors(interactive);
31
+ patchLoginCommand(interactive);
32
+ patchAuthExpiredSessionEvents(interactive);
33
+ patchAuthExpiredLoginPrompt(interactive);
20
34
  patchHeader(interactive);
21
35
  }
36
+ export function requestLinxCloudLogin(interactive, reason = 'manual') {
37
+ if (!interactive.isInitialized) {
38
+ interactive[LINX_AUTH_LOGIN_ON_INIT] = reason;
39
+ return;
40
+ }
41
+ void startLinxCloudLogin(interactive, { reason });
42
+ }
22
43
  function patchTerminalTitle(interactive) {
23
44
  const original = interactive.updateTerminalTitle?.bind(interactive);
24
45
  interactive.updateTerminalTitle = function patchedUpdateTerminalTitle() {
@@ -43,7 +64,7 @@ function patchVersionCheck(interactive) {
43
64
  }
44
65
  const body = await response.json();
45
66
  const latest = typeof body.version === 'string' ? body.version.trim() : '';
46
- if (!latest || !isNewerVersion(latest, LINX_CLI_VERSION)) {
67
+ if (!latest || !isVersionNewer(latest, LINX_CLI_VERSION)) {
47
68
  return undefined;
48
69
  }
49
70
  return latest;
@@ -55,45 +76,109 @@ function patchVersionCheck(interactive) {
55
76
  }
56
77
  function patchUpdateNotification(interactive) {
57
78
  interactive.showNewVersionNotification = function patchedShowNewVersionNotification(newVersion) {
58
- const lines = [
59
- '\x1b[1m\x1b[33mLinX Update Available\x1b[39m\x1b[22m',
60
- `\x1b[2mNew version ${newVersion} is available. \x1b[22m\x1b[36mType /update to install now.\x1b[39m`,
61
- `\x1b[2mManual install: \x1b[22m\x1b[36mnpm install -g ${LINX_UPDATE_PACKAGE_NAME}@latest\x1b[39m`,
62
- `\x1b[2mChangelog: \x1b[22m\x1b[36m${LINX_CHANGELOG_URL}\x1b[39m`,
63
- ];
64
- this.chatContainer?.addChild?.(new Text(lines.join('\n'), 1, 0));
65
- this.ui?.requestRender?.();
79
+ void showLinxUpdateSelector(this, newVersion);
66
80
  };
67
81
  }
68
- function isNewerVersion(candidate, current) {
69
- const candidateParts = parseSemverCore(candidate);
70
- const currentParts = parseSemverCore(current);
71
- if (!candidateParts || !currentParts) {
72
- return candidate !== current;
82
+ async function showLinxUpdateSelector(interactive, newVersion) {
83
+ if (interactive[LINX_UPDATE_IN_PROGRESS]) {
84
+ return;
73
85
  }
74
- for (let index = 0; index < 3; index += 1) {
75
- if (candidateParts[index] > currentParts[index]) {
76
- return true;
86
+ interactive[LINX_UPDATE_IN_PROGRESS] = true;
87
+ try {
88
+ const title = [
89
+ 'LinX update available',
90
+ `Current ${LINX_CLI_VERSION} -> latest ${newVersion}`,
91
+ 'Choose how to handle this update.',
92
+ ].join('\n');
93
+ const options = [UPDATE_OPTION_INSTALL, UPDATE_OPTION_CHANGELOG, UPDATE_OPTION_LATER];
94
+ const selected = typeof interactive.showExtensionSelector === 'function'
95
+ ? await interactive.showExtensionSelector(title, options)
96
+ : undefined;
97
+ if (selected === UPDATE_OPTION_INSTALL) {
98
+ await installLinxUpdateAndRestart(interactive, newVersion);
99
+ return;
77
100
  }
78
- if (candidateParts[index] < currentParts[index]) {
79
- return false;
101
+ if (selected === UPDATE_OPTION_CHANGELOG) {
102
+ openExternalUrl(LINX_CHANGELOG_URL, interactive);
103
+ interactive.showStatus?.(`Opened LinX changelog for ${newVersion}.`);
104
+ return;
105
+ }
106
+ if (!selected) {
107
+ showLinxUpdateFallback(interactive, newVersion);
108
+ return;
80
109
  }
110
+ interactive.showStatus?.(`Skipped LinX ${newVersion} for now.`);
111
+ }
112
+ finally {
113
+ interactive[LINX_UPDATE_IN_PROGRESS] = false;
81
114
  }
82
- return false;
83
115
  }
84
- function parseSemverCore(version) {
85
- const match = version.trim().match(/^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/);
86
- if (!match) {
87
- return null;
116
+ async function installLinxUpdateAndRestart(interactive, newVersion) {
117
+ interactive.showStatus?.(`Installing LinX ${newVersion}...`);
118
+ interactive.ui?.requestRender?.();
119
+ try {
120
+ await runNpmInstallLatest();
121
+ }
122
+ catch (error) {
123
+ const message = error instanceof Error ? error.message : String(error);
124
+ interactive.showError?.(`LinX update failed: ${message}`);
125
+ return;
88
126
  }
89
- return [Number(match[1]), Number(match[2]), Number(match[3])];
127
+ interactive.showStatus?.(`LinX ${newVersion} installed. Restarting...`);
128
+ interactive.ui?.requestRender?.();
129
+ restartCurrentProcess(interactive);
130
+ }
131
+ function runNpmInstallLatest() {
132
+ const npmCommand = process.env.npm_execpath || 'npm';
133
+ const args = ['install', '-g', `${LINX_UPDATE_PACKAGE_NAME}@latest`];
134
+ return new Promise((resolve, reject) => {
135
+ const child = spawn(npmCommand, args, {
136
+ stdio: ['ignore', 'pipe', 'pipe'],
137
+ shell: false,
138
+ });
139
+ let stderr = '';
140
+ child.stderr?.on('data', (chunk) => {
141
+ stderr += chunk.toString();
142
+ });
143
+ child.on('error', reject);
144
+ child.on('close', (code) => {
145
+ if (code === 0) {
146
+ resolve();
147
+ return;
148
+ }
149
+ reject(new Error(stderr.trim() || `npm install exited with code ${code ?? 'unknown'}`));
150
+ });
151
+ });
152
+ }
153
+ function restartCurrentProcess(interactive) {
154
+ const child = spawn(process.execPath, process.argv.slice(1), {
155
+ cwd: process.cwd(),
156
+ env: process.env,
157
+ stdio: 'inherit',
158
+ detached: false,
159
+ });
160
+ child.on('error', (error) => {
161
+ interactive.showError?.(`LinX restart failed: ${error.message}`);
162
+ });
163
+ interactive.stop?.();
164
+ setTimeout(() => process.exit(0), 50);
165
+ }
166
+ function showLinxUpdateFallback(interactive, newVersion) {
167
+ const lines = [
168
+ '\x1b[1m\x1b[33mLinX update available\x1b[39m\x1b[22m',
169
+ `\x1b[2mCurrent ${LINX_CLI_VERSION} -> latest ${newVersion}\x1b[22m`,
170
+ `\x1b[2mRun \x1b[22m\x1b[36mnpm install -g ${LINX_UPDATE_PACKAGE_NAME}@latest\x1b[39m\x1b[2m if this terminal cannot show the update selector.\x1b[22m`,
171
+ `\x1b[2mChangelog: \x1b[22m\x1b[36m${LINX_CHANGELOG_URL}\x1b[39m`,
172
+ ];
173
+ interactive.chatContainer?.addChild?.(new Text(lines.join('\n'), 1, 0));
174
+ interactive.ui?.requestRender?.();
90
175
  }
91
- function patchUpdateCommand(interactive) {
176
+ function patchLoginCommand(interactive) {
92
177
  const originalSetup = interactive.setupEditorSubmitHandler?.bind(interactive);
93
178
  if (typeof originalSetup !== 'function') {
94
179
  return;
95
180
  }
96
- interactive.setupEditorSubmitHandler = function patchedSetupEditorSubmitHandler() {
181
+ interactive.setupEditorSubmitHandler = function patchedLinxLoginSetupEditorSubmitHandler() {
97
182
  originalSetup();
98
183
  const originalSubmit = this.defaultEditor?.onSubmit?.bind(this.defaultEditor);
99
184
  if (typeof originalSubmit !== 'function') {
@@ -101,77 +186,426 @@ function patchUpdateCommand(interactive) {
101
186
  }
102
187
  this.defaultEditor.onSubmit = async (text) => {
103
188
  const command = text.trim();
104
- if (command === '/update' || command === '/update linx') {
189
+ if (command === '/login') {
105
190
  this.editor?.setText?.('');
106
- await runLinxUpdateFromTui(this);
191
+ await startLinxCloudLogin(this);
107
192
  return;
108
193
  }
109
194
  await originalSubmit(text);
110
195
  };
111
196
  };
112
197
  }
113
- async function runLinxUpdateFromTui(interactive) {
114
- if (interactive[LINX_UPDATE_IN_PROGRESS]) {
115
- interactive.showStatus?.('LinX update is already running');
198
+ function patchNativeOAuthSelectors(interactive) {
199
+ interactive.showOAuthSelector = async function patchedLinxOAuthSelector(mode = 'login') {
200
+ if (mode === 'logout') {
201
+ const authStorage = this.session?.modelRegistry?.authStorage;
202
+ authStorage?.logout?.(LINX_PROVIDER_ID);
203
+ authStorage?.setRuntimeApiKey?.(LINX_PROVIDER_ID, '');
204
+ await refreshLinxAuthState(this);
205
+ this.showStatus?.('Logged out of LinX Cloud.');
206
+ return;
207
+ }
208
+ await startLinxCloudLogin(this, { reason: 'manual' });
209
+ };
210
+ interactive.showLoginDialog = async function patchedLinxLoginDialog(providerId) {
211
+ if (!providerId || providerId === LINX_PROVIDER_ID) {
212
+ await startLinxCloudLogin(this, { reason: 'manual' });
213
+ return;
214
+ }
215
+ this.showStatus?.('LinX only supports LinX Cloud authentication in this TUI.');
216
+ await startLinxCloudLogin(this, { reason: 'manual' });
217
+ };
218
+ }
219
+ function patchAuthExpiredSessionEvents(interactive) {
220
+ const originalHandleEvent = interactive.handleEvent?.bind(interactive);
221
+ if (typeof originalHandleEvent !== 'function') {
116
222
  return;
117
223
  }
118
- interactive[LINX_UPDATE_IN_PROGRESS] = true;
119
- const packageSpec = `${LINX_UPDATE_PACKAGE_NAME}@latest`;
120
- const npmCommand = process.env.npm_execpath && !process.env.npm_execpath.endsWith('npx-cli.js')
121
- ? process.execPath
122
- : 'npm';
123
- const args = npmCommand === process.execPath
124
- ? [process.env.npm_execpath, 'install', '-g', packageSpec]
125
- : ['install', '-g', packageSpec];
224
+ interactive.handleEvent = async function patchedHandleEvent(event) {
225
+ const result = await originalHandleEvent(event);
226
+ if (eventHasLinxAuthExpiredError(event)) {
227
+ scheduleLinxCloudLogin(this, 'expired');
228
+ }
229
+ return result;
230
+ };
231
+ }
232
+ function patchAuthExpiredLoginPrompt(interactive) {
233
+ const originalShowError = interactive.showError?.bind(interactive);
234
+ if (typeof originalShowError !== 'function') {
235
+ return;
236
+ }
237
+ interactive.showError = function patchedShowError(errorMessage) {
238
+ const text = typeof errorMessage === 'string' ? errorMessage : String(errorMessage);
239
+ if (!isLinxAuthExpiredError(text)) {
240
+ return originalShowError(errorMessage);
241
+ }
242
+ scheduleLinxCloudLogin(this, 'expired');
243
+ return undefined;
244
+ };
245
+ }
246
+ function isLinxAuthExpiredError(text) {
247
+ const normalized = stripAnsi(text).toLowerCase();
248
+ return normalized.includes('linx cloud login expired')
249
+ || normalized.includes('invalid solid token')
250
+ || (normalized.includes('chat request failed (401)') && normalized.includes('unauthorized'));
251
+ }
252
+ function eventHasLinxAuthExpiredError(event) {
253
+ if (!isRecord(event)) {
254
+ return false;
255
+ }
256
+ const message = isRecord(event.message) ? event.message : undefined;
257
+ const errorMessage = typeof message?.errorMessage === 'string' ? message.errorMessage : '';
258
+ const error = isRecord(event.error) ? event.error : undefined;
259
+ const nestedErrorMessage = typeof error?.errorMessage === 'string' ? error.errorMessage : '';
260
+ return isLinxAuthExpiredError(`${errorMessage}\n${nestedErrorMessage}`);
261
+ }
262
+ async function startLinxCloudLogin(interactive, options = {}) {
263
+ if (interactive[LINX_AUTH_LOGIN_IN_PROGRESS]) {
264
+ return;
265
+ }
266
+ interactive[LINX_AUTH_LOGIN_IN_PROGRESS] = true;
126
267
  try {
127
- interactive.showStatus?.(`Installing LinX update: ${packageSpec}`);
128
- interactive.ui?.requestRender?.();
129
- interactive.ui?.stop?.();
130
- const runner = resolveUpdateRunner(interactive);
131
- const result = await runner(npmCommand, args);
132
- interactive.ui?.start?.();
133
- if (result.exitCode === 0) {
134
- interactive.showStatus?.('LinX updated. Restart linx to use the new version.');
268
+ const authStorage = interactive.session?.modelRegistry?.authStorage;
269
+ if (!authStorage) {
270
+ prefillLoginCommand(interactive);
135
271
  return;
136
272
  }
137
- const detail = result.signal ? `signal ${result.signal}` : `exit code ${result.exitCode}`;
138
- interactive.showError?.(`LinX update failed with ${detail}`);
273
+ const reason = options.reason ?? 'manual';
274
+ const selected = await selectLinxAuthMethod(interactive, reason);
275
+ if (!selected) {
276
+ interactive.showStatus?.('LinX Cloud authorization cancelled.');
277
+ return;
278
+ }
279
+ if (selected === AUTH_OPTION_BROWSER) {
280
+ if (typeof authStorage.login !== 'function') {
281
+ prefillLoginCommand(interactive);
282
+ return;
283
+ }
284
+ await runLinxCloudBrowserLogin(interactive, authStorage, reason);
285
+ await refreshLinxAuthState(interactive);
286
+ interactive.showStatus?.(`${authStatusPrefix(reason)} Browser authorization complete. Retry your message.`);
287
+ return;
288
+ }
289
+ if (selected === AUTH_OPTION_API_KEY) {
290
+ await promptForLinxApiKey(interactive, reason);
291
+ return;
292
+ }
293
+ if (selected === AUTH_OPTION_EXIT) {
294
+ if (reason === 'startup') {
295
+ interactive.stop?.();
296
+ }
297
+ else {
298
+ interactive.showStatus?.('LinX Cloud authorization cancelled.');
299
+ }
300
+ }
139
301
  }
140
302
  catch (error) {
141
- interactive.ui?.start?.();
142
303
  const message = error instanceof Error ? error.message : String(error);
143
- interactive.showError?.(`LinX update failed: ${message}`);
304
+ reportLinxLoginError(interactive, message);
144
305
  }
145
306
  finally {
146
- interactive[LINX_UPDATE_IN_PROGRESS] = false;
147
- interactive.ui?.requestRender?.();
307
+ interactive[LINX_AUTH_LOGIN_IN_PROGRESS] = false;
308
+ }
309
+ }
310
+ function scheduleLinxCloudLogin(interactive, reason) {
311
+ if (interactive[LINX_AUTH_LOGIN_IN_PROGRESS] || interactive[LINX_AUTH_LOGIN_SCHEDULED]) {
312
+ return;
313
+ }
314
+ interactive[LINX_AUTH_LOGIN_SCHEDULED] = true;
315
+ setTimeout(() => {
316
+ interactive[LINX_AUTH_LOGIN_SCHEDULED] = false;
317
+ void startLinxCloudLogin(interactive, { reason });
318
+ }, 0);
319
+ }
320
+ function reportLinxLoginError(interactive, message) {
321
+ const rendered = normalizeLinxLoginError(message);
322
+ if (interactive[LINX_AUTH_REPORTING_ERROR]) {
323
+ interactive.showStatus?.(rendered);
324
+ return;
148
325
  }
326
+ interactive[LINX_AUTH_REPORTING_ERROR] = true;
327
+ try {
328
+ if (typeof interactive.showError === 'function') {
329
+ interactive.showError(rendered);
330
+ }
331
+ else {
332
+ interactive.showStatus?.(rendered);
333
+ }
334
+ }
335
+ finally {
336
+ interactive[LINX_AUTH_REPORTING_ERROR] = false;
337
+ }
338
+ }
339
+ function normalizeLinxLoginError(message) {
340
+ const oidcCallbackError = /^OIDC callback returned\b/i.test(message);
341
+ if (oidcCallbackError) {
342
+ return `LinX Cloud login failed: ${message}`;
343
+ }
344
+ if (/server_error/i.test(message)) {
345
+ return `LinX Cloud login failed: the identity server rejected this browser login. ${message}`;
346
+ }
347
+ return `LinX Cloud login failed: ${message}`;
348
+ }
349
+ async function selectLinxAuthMethod(interactive, reason) {
350
+ const title = buildLinxAuthPromptTitle(reason, resolveRuntimeProviderLabel(interactive));
351
+ const options = [AUTH_OPTION_BROWSER, AUTH_OPTION_API_KEY, AUTH_OPTION_EXIT];
352
+ if (typeof interactive.showExtensionSelector === 'function') {
353
+ return await interactive.showExtensionSelector(title, options);
354
+ }
355
+ showLinxAuthFallback(interactive, title, options);
356
+ return undefined;
149
357
  }
150
- function resolveUpdateRunner(interactive) {
151
- return typeof interactive[LINX_UPDATE_RUNNER] === 'function'
152
- ? interactive[LINX_UPDATE_RUNNER]
153
- : spawnInstall;
358
+ function buildLinxAuthPromptTitle(reason, providerLabel) {
359
+ if (reason === 'startup') {
360
+ return [
361
+ 'LinX Cloud login required',
362
+ `Connect to ${providerLabel} before using LinX TUI.`,
363
+ 'Choose a sign-in method.',
364
+ ].join('\n');
365
+ }
366
+ if (reason === 'expired') {
367
+ return [
368
+ 'LinX Cloud login expired',
369
+ 'Your current Solid token was rejected by LinX Cloud.',
370
+ 'Re-authorize or provide an API key, then retry your message.',
371
+ ].join('\n');
372
+ }
373
+ return [
374
+ 'LinX Cloud authorization',
375
+ `Choose how LinX should authenticate with ${providerLabel}.`,
376
+ ].join('\n');
377
+ }
378
+ function showLinxAuthFallback(interactive, title, options) {
379
+ interactive.chatContainer?.addChild?.(new Text([
380
+ `\x1b[1m${title}\x1b[22m`,
381
+ '',
382
+ ...options.map((option) => `- ${option}`),
383
+ '',
384
+ 'This terminal build cannot render the LinX auth selector. Run `linx login` in another shell.',
385
+ ].join('\n'), 1, 0));
386
+ interactive.ui?.requestRender?.();
387
+ }
388
+ async function promptForLinxApiKey(interactive, reason) {
389
+ if (typeof interactive.showExtensionInput !== 'function') {
390
+ interactive.showError?.('This terminal build cannot collect an API key inside the TUI.');
391
+ return;
392
+ }
393
+ const apiKey = await interactive.showExtensionInput([
394
+ reason === 'expired' ? 'Enter LinX Cloud API key' : 'Use LinX Cloud API key',
395
+ 'Paste a key for this TUI session. Press Escape to cancel.',
396
+ ].join('\n'), 'linx API key');
397
+ const trimmed = typeof apiKey === 'string' ? apiKey.trim() : '';
398
+ if (!trimmed) {
399
+ interactive.showStatus?.('LinX Cloud API key entry cancelled.');
400
+ return;
401
+ }
402
+ const authStorage = interactive.session?.modelRegistry?.authStorage;
403
+ authStorage?.setRuntimeApiKey?.(LINX_PROVIDER_ID, trimmed);
404
+ authStorage?.set?.(LINX_PROVIDER_ID, { type: 'api_key', key: trimmed });
405
+ await refreshLinxAuthState(interactive);
406
+ interactive.showStatus?.(`${authStatusPrefix(reason)} API key saved for this TUI session. Retry your message.`);
407
+ }
408
+ async function refreshLinxAuthState(interactive) {
409
+ interactive.session?.modelRegistry?.refresh?.();
410
+ await interactive.updateAvailableProviderCount?.();
411
+ interactive.ui?.requestRender?.();
412
+ }
413
+ function authStatusPrefix(reason) {
414
+ if (reason === 'expired') {
415
+ return 'LinX Cloud login refreshed.';
416
+ }
417
+ if (reason === 'startup') {
418
+ return 'LinX Cloud connected.';
419
+ }
420
+ return 'LinX Cloud authorization updated.';
421
+ }
422
+ async function runLinxCloudLogin(interactive, authStorage, reason) {
423
+ await authStorage.login(LINX_PROVIDER_ID, {
424
+ forceFresh: reason === 'expired',
425
+ onAuth(info) {
426
+ showLinxLoginUrl(interactive, info);
427
+ openLoginUrl(info.url, interactive);
428
+ if (info.instructions) {
429
+ interactive.showStatus?.(info.instructions);
430
+ }
431
+ },
432
+ onProgress(message) {
433
+ interactive.showStatus?.(message);
434
+ interactive.ui?.requestRender?.();
435
+ },
436
+ onManualCodeInput(signal) {
437
+ return promptForLinxManualRedirectUrl(interactive, signal);
438
+ },
439
+ });
440
+ syncRuntimeCredential(interactive);
441
+ }
442
+ async function runLinxCloudBrowserLogin(interactive, authStorage, reason) {
443
+ if (canRenderLinxLoginDialog(interactive)) {
444
+ await runLinxCloudLoginDialog(interactive, authStorage, reason);
445
+ return;
446
+ }
447
+ await runLinxCloudLogin(interactive, authStorage, reason);
448
+ }
449
+ function canRenderLinxLoginDialog(interactive) {
450
+ return Boolean(interactive.ui
451
+ && typeof interactive.editorContainer?.clear === 'function'
452
+ && typeof interactive.editorContainer?.addChild === 'function'
453
+ && interactive.editor);
454
+ }
455
+ async function runLinxCloudLoginDialog(interactive, authStorage, reason) {
456
+ const dialog = new LoginDialogComponent(interactive.ui, LINX_PROVIDER_ID, () => undefined);
457
+ const restoreEditor = () => {
458
+ interactive.editorContainer.clear();
459
+ interactive.editorContainer.addChild(interactive.editor);
460
+ interactive.ui?.setFocus?.(interactive.editor);
461
+ interactive.ui?.requestRender?.();
462
+ };
463
+ interactive.editorContainer.clear();
464
+ interactive.editorContainer.addChild(dialog);
465
+ interactive.ui?.setFocus?.(dialog);
466
+ interactive.ui?.requestRender?.();
467
+ let manualRedirectResolve;
468
+ let manualRedirectReject;
469
+ const manualRedirectPromise = new Promise((resolve, reject) => {
470
+ manualRedirectResolve = resolve;
471
+ manualRedirectReject = reject;
472
+ });
473
+ try {
474
+ await authStorage.login(LINX_PROVIDER_ID, {
475
+ forceFresh: reason === 'expired',
476
+ onAuth(info) {
477
+ dialog.showAuth(info.url, info.instructions);
478
+ dialog.showManualInput('Paste redirect URL below, or complete login in browser:')
479
+ .then((value) => {
480
+ if (value && manualRedirectResolve) {
481
+ manualRedirectResolve(value);
482
+ manualRedirectResolve = undefined;
483
+ manualRedirectReject = undefined;
484
+ }
485
+ })
486
+ .catch((error) => {
487
+ if (manualRedirectReject) {
488
+ manualRedirectReject(error);
489
+ manualRedirectResolve = undefined;
490
+ manualRedirectReject = undefined;
491
+ }
492
+ });
493
+ },
494
+ onProgress(message) {
495
+ dialog.showProgress(message);
496
+ },
497
+ onManualCodeInput(signal) {
498
+ return waitForLinxDialogManualRedirect(manualRedirectPromise, signal);
499
+ },
500
+ signal: dialog.signal,
501
+ });
502
+ syncRuntimeCredential(interactive);
503
+ }
504
+ finally {
505
+ restoreEditor();
506
+ }
154
507
  }
155
- function spawnInstall(command, args) {
508
+ function waitForLinxDialogManualRedirect(manualRedirectPromise, signal) {
509
+ if (!signal) {
510
+ return manualRedirectPromise;
511
+ }
512
+ if (signal.aborted) {
513
+ return Promise.resolve('');
514
+ }
156
515
  return new Promise((resolve, reject) => {
157
- const child = spawn(command, args, {
158
- stdio: 'inherit',
159
- shell: process.platform === 'win32',
516
+ const onAbort = () => resolve('');
517
+ signal.addEventListener('abort', onAbort, { once: true });
518
+ manualRedirectPromise
519
+ .then((value) => {
520
+ signal.removeEventListener('abort', onAbort);
521
+ resolve(value);
522
+ })
523
+ .catch((error) => {
524
+ signal.removeEventListener('abort', onAbort);
525
+ reject(error);
160
526
  });
161
- child.once('error', reject);
162
- child.once('close', (exitCode, signal) => resolve({ exitCode, signal }));
163
527
  });
164
528
  }
529
+ async function promptForLinxManualRedirectUrl(interactive, signal) {
530
+ if (typeof interactive.showExtensionInput !== 'function') {
531
+ throw new Error('Manual redirect paste is not available in this terminal. Run `linx login` in another shell if the browser callback is blocked.');
532
+ }
533
+ const redirect = await interactive.showExtensionInput([
534
+ 'Paste final redirect URL',
535
+ 'If the browser cannot return to this terminal, paste the full callback URL below.',
536
+ ].join('\n'), 'http://127.0.0.1:PORT/auth/callback?code=...&state=...&iss=...', signal ? { signal } : undefined);
537
+ const trimmed = typeof redirect === 'string' ? redirect.trim() : '';
538
+ if (!trimmed) {
539
+ throw new Error('Login cancelled');
540
+ }
541
+ return trimmed;
542
+ }
543
+ function syncRuntimeCredential(interactive) {
544
+ const authStorage = interactive.session?.modelRegistry?.authStorage;
545
+ const credential = authStorage?.get?.(LINX_PROVIDER_ID);
546
+ if (credential?.type === 'oauth' && typeof credential.access === 'string' && credential.access) {
547
+ authStorage.setRuntimeApiKey?.(LINX_PROVIDER_ID, credential.access);
548
+ return;
549
+ }
550
+ if (credential?.type === 'api_key' && typeof credential.key === 'string' && credential.key) {
551
+ authStorage.setRuntimeApiKey?.(LINX_PROVIDER_ID, credential.key);
552
+ }
553
+ }
554
+ function showLinxLoginUrl(interactive, info) {
555
+ const lines = [
556
+ '\x1b[1mLinX Cloud authorization\x1b[22m',
557
+ 'Complete consent in the browser, then return here.',
558
+ '',
559
+ `\x1b[36m${info.url}\x1b[39m`,
560
+ ];
561
+ if (info.instructions) {
562
+ lines.push('', `\x1b[2m${info.instructions}\x1b[22m`);
563
+ }
564
+ interactive.chatContainer?.addChild?.(new Text(lines.join('\n'), 1, 0));
565
+ interactive.ui?.requestRender?.();
566
+ }
567
+ function openLoginUrl(url, interactive) {
568
+ openExternalUrl(url, interactive);
569
+ }
570
+ function openExternalUrl(url, interactive) {
571
+ if (typeof interactive.openExternal === 'function') {
572
+ interactive.openExternal(url);
573
+ return;
574
+ }
575
+ const command = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'cmd' : 'xdg-open';
576
+ const args = process.platform === 'win32' ? ['/c', 'start', '', url] : [url];
577
+ const child = spawn(command, args, {
578
+ detached: true,
579
+ stdio: 'ignore',
580
+ shell: false,
581
+ });
582
+ child.unref();
583
+ }
584
+ function prefillLoginCommand(interactive) {
585
+ interactive.editor?.setText?.('/login');
586
+ interactive.ui?.setFocus?.(interactive.editor);
587
+ interactive.ui?.requestRender?.();
588
+ }
165
589
  function patchHeader(interactive) {
166
- const originalInit = interactive.init.bind(interactive);
590
+ const originalInit = interactive.init?.bind(interactive);
591
+ if (typeof originalInit !== 'function') {
592
+ return;
593
+ }
167
594
  interactive.init = async function patchedInit() {
168
595
  await originalInit();
596
+ if (this[LINX_AUTH_LOGIN_ON_INIT]) {
597
+ const reason = typeof this[LINX_AUTH_LOGIN_ON_INIT] === 'string'
598
+ ? this[LINX_AUTH_LOGIN_ON_INIT]
599
+ : 'startup';
600
+ this[LINX_AUTH_LOGIN_ON_INIT] = false;
601
+ queueMicrotask(() => startLinxCloudLogin(this, { reason }));
602
+ }
169
603
  const quietStartup = this.options?.verbose ? false : this.settingsManager?.getQuietStartup?.();
170
604
  if (quietStartup) {
171
605
  return;
172
606
  }
173
607
  let profileDisplayName = null;
174
- const replacement = new LinxWelcomeCard(() => buildHeaderState(this, profileDisplayName));
608
+ const replacement = new LinxWelcomeCard(() => buildLinxWelcomeCardState(this, profileDisplayName));
175
609
  const currentHeader = this.customHeader ?? this.builtInHeader;
176
610
  const index = this.headerContainer?.children?.indexOf?.(currentHeader) ?? -1;
177
611
  if (index >= 0) {
@@ -227,13 +661,13 @@ class LinxWelcomeCard {
227
661
  ];
228
662
  }
229
663
  }
230
- function buildHeaderState(interactive, profileDisplayName = null) {
664
+ export function buildLinxWelcomeCardState(interactive, profileDisplayName = null) {
231
665
  const credentials = loadCredentials();
232
666
  const webId = credentials?.webId ?? 'not logged in';
233
667
  const workspace = interactive?.sessionManager?.getCwd?.() || process.cwd();
234
668
  const sessionId = interactive?.sessionManager?.getSessionId?.();
235
669
  const sessionName = interactive?.sessionManager?.getSessionName?.();
236
- const session = sessionName && sessionId ? `${sessionName} (${shortSessionId(sessionId)})` : shortSessionId(sessionId);
670
+ const session = sessionName && sessionId ? `${sessionName} (${formatSessionId(sessionId)})` : formatSessionId(sessionId);
237
671
  const model = interactive?.session?.model?.id ?? 'unknown-model';
238
672
  return {
239
673
  webId,
@@ -246,7 +680,7 @@ function buildHeaderState(interactive, profileDisplayName = null) {
246
680
  keyHint('tui.input.submit', 'send'),
247
681
  keyHint('app.model.select', 'model'),
248
682
  rawKeyHint('/login', 'auth'),
249
- rawKeyHint('/help', 'help'),
683
+ rawKeyHint('/hotkeys', 'keymap'),
250
684
  ].join(' \x1b[2m·\x1b[22m '),
251
685
  };
252
686
  }
@@ -264,11 +698,11 @@ function wrapAndPad(line, width) {
264
698
  ? wrapped.map((entry) => padLine(entry, width))
265
699
  : [padLine('', width)];
266
700
  }
267
- function shortSessionId(sessionId) {
701
+ function formatSessionId(sessionId) {
268
702
  if (typeof sessionId !== 'string' || !sessionId.trim()) {
269
703
  return 'new session';
270
704
  }
271
- return sessionId.length > 12 ? sessionId.slice(0, 12) : sessionId;
705
+ return sessionId.trim();
272
706
  }
273
707
  function padLine(line, width) {
274
708
  const visible = visibleWidth(line);
@@ -277,6 +711,44 @@ function padLine(line, width) {
277
711
  }
278
712
  return `${line}${' '.repeat(width - visible)}`;
279
713
  }
714
+ function readLinxCliVersion() {
715
+ try {
716
+ const raw = readFileSync(new URL('../../../package.json', import.meta.url), 'utf-8');
717
+ const pkg = JSON.parse(raw);
718
+ return typeof pkg.version === 'string' && pkg.version.trim() ? pkg.version.trim() : '0.1.0';
719
+ }
720
+ catch {
721
+ return '0.1.0';
722
+ }
723
+ }
724
+ export function isVersionNewer(candidate, current) {
725
+ const candidateVersion = parseSemverLike(candidate);
726
+ const currentVersion = parseSemverLike(current);
727
+ if (!candidateVersion || !currentVersion) {
728
+ return candidate !== current;
729
+ }
730
+ for (const key of ['major', 'minor', 'patch']) {
731
+ if (candidateVersion[key] > currentVersion[key]) {
732
+ return true;
733
+ }
734
+ if (candidateVersion[key] < currentVersion[key]) {
735
+ return false;
736
+ }
737
+ }
738
+ return !candidateVersion.prerelease && currentVersion.prerelease;
739
+ }
740
+ function parseSemverLike(version) {
741
+ const match = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+.+)?$/.exec(version.trim());
742
+ if (!match) {
743
+ return null;
744
+ }
745
+ return {
746
+ major: Number(match[1]),
747
+ minor: Number(match[2]),
748
+ patch: Number(match[3]),
749
+ prerelease: Boolean(match[4]),
750
+ };
751
+ }
280
752
  function resolveRuntimeProviderLabel(interactive) {
281
753
  const bridge = interactive?.runtimeHost?.linxAuthBridge ?? interactive?.linxAuthBridge;
282
754
  if (bridge?.providerLabel) {
@@ -327,4 +799,10 @@ function stripPodStatusLines(input) {
327
799
  .replace(new RegExp(String.raw `Using WebID:\s*${urlPattern}[ \t]*(?:\r?\n)?`, 'g'), '')
328
800
  .replace(/Successfully connected to Solid Pod[ \t]*(?:\r?\n)?/g, '');
329
801
  }
802
+ function isRecord(value) {
803
+ return typeof value === 'object' && value !== null;
804
+ }
805
+ function stripAnsi(text) {
806
+ return text.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, '');
807
+ }
330
808
  //# sourceMappingURL=branding.js.map