koneck 2.71.2 → 2.73.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Reading the system clipboard, for when the terminal's own paste will not do.
3
+ *
4
+ * Pasting into a terminal is not one mechanism, it is several: bracketed paste if the terminal
5
+ * offers it, a plain burst of characters if not, a middle-click that KONECK may have taken over for
6
+ * its own mouse handling, and on Windows a Ctrl+V that some hosts deliver as a control code rather
7
+ * than as text. Somebody trying to put an API key into a prompt does not care which of those their
8
+ * terminal does, and telling them to type a sixty-character secret by hand is not an answer.
9
+ *
10
+ * So the prompt can go and fetch it. Every desktop already has a command for this, and KONECK
11
+ * already shells out to their write-side counterparts for /copy; this is the same list read the
12
+ * other way round.
13
+ */
14
+ export interface ClipboardRead {
15
+ ok: boolean;
16
+ text: string;
17
+ /** Which command answered, so it can be named. */
18
+ by?: string;
19
+ /** Why nothing came back, when nothing did. */
20
+ reason?: string;
21
+ }
22
+ /**
23
+ * What is on the clipboard, or why it could not be read.
24
+ *
25
+ * Each reader gets a short deadline: a clipboard tool that hangs — xclip waiting on an X server
26
+ * that is not there is the usual way — must not hold a keystroke open. Failure is ordinary and is
27
+ * reported rather than thrown: a machine reached over SSH has no clipboard, and that is a fact
28
+ * about the machine rather than an error in the program.
29
+ */
30
+ export declare function readClipboard(readers?: ReadonlyArray<{
31
+ file: string;
32
+ args: string[];
33
+ }>, timeoutMs?: number): Promise<ClipboardRead>;
34
+ /**
35
+ * A pasted secret, made safe to use as one.
36
+ *
37
+ * Three things go, each for its own reason. Surrounding whitespace, because a key copied out of a
38
+ * file or a password manager carries a trailing newline that is not part of it. Bracketed paste
39
+ * markers, because a terminal that announces a paste mid-chunk can leave the literal text "[200~"
40
+ * in the field, and a key with that in front of it fails at the provider with an authentication
41
+ * error that says nothing about where it came from. And control characters, which are invalid in an
42
+ * HTTP header — a key carrying one is rejected by the transport before the provider ever sees it,
43
+ * which is the least informative way for this to go wrong.
44
+ */
45
+ export declare function cleanPastedSecret(text: string): string;
46
+ //# sourceMappingURL=clipboard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"clipboard.d.ts","sourceRoot":"","sources":["../src/clipboard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAaH,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,OAAO,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,kDAAkD;IAClD,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,+CAA+C;IAC/C,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;GAOG;AACH,wBAAsB,aAAa,CACjC,OAAO,GAAE,aAAa,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,EAAE,CAAA;CAAE,CAAW,EAClE,SAAS,SAAQ,GAChB,OAAO,CAAC,aAAa,CAAC,CAmBxB;AAeD;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAKtD"}
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Reading the system clipboard, for when the terminal's own paste will not do.
3
+ *
4
+ * Pasting into a terminal is not one mechanism, it is several: bracketed paste if the terminal
5
+ * offers it, a plain burst of characters if not, a middle-click that KONECK may have taken over for
6
+ * its own mouse handling, and on Windows a Ctrl+V that some hosts deliver as a control code rather
7
+ * than as text. Somebody trying to put an API key into a prompt does not care which of those their
8
+ * terminal does, and telling them to type a sixty-character secret by hand is not an answer.
9
+ *
10
+ * So the prompt can go and fetch it. Every desktop already has a command for this, and KONECK
11
+ * already shells out to their write-side counterparts for /copy; this is the same list read the
12
+ * other way round.
13
+ */
14
+ /** The readers worth trying, in order. The first that exists and answers wins. */
15
+ const READERS = [
16
+ // Wayland, then X11's two common tools, then macOS.
17
+ { file: 'wl-paste', args: ['--no-newline'] },
18
+ { file: 'xclip', args: ['-selection', 'clipboard', '-o'] },
19
+ { file: 'xsel', args: ['--clipboard', '--output'] },
20
+ { file: 'pbpaste', args: [] },
21
+ // Windows, and WSL — where the Windows clipboard is the one holding what was copied.
22
+ { file: 'powershell.exe', args: ['-NoProfile', '-Command', 'Get-Clipboard'] },
23
+ ];
24
+ /**
25
+ * What is on the clipboard, or why it could not be read.
26
+ *
27
+ * Each reader gets a short deadline: a clipboard tool that hangs — xclip waiting on an X server
28
+ * that is not there is the usual way — must not hold a keystroke open. Failure is ordinary and is
29
+ * reported rather than thrown: a machine reached over SSH has no clipboard, and that is a fact
30
+ * about the machine rather than an error in the program.
31
+ */
32
+ export async function readClipboard(readers = READERS, timeoutMs = 1_500) {
33
+ const { execa } = await import('execa');
34
+ for (const reader of readers) {
35
+ try {
36
+ const done = await execa(reader.file, [...reader.args], {
37
+ reject: false, timeout: timeoutMs, stripFinalNewline: true,
38
+ });
39
+ if (done.exitCode !== 0)
40
+ continue;
41
+ // An empty clipboard is a successful read of nothing, and worth reporting as its own thing:
42
+ // it means the copy did not happen, not that KONECK cannot see the clipboard.
43
+ return { ok: true, text: String(done.stdout ?? ''), by: reader.file };
44
+ }
45
+ catch {
46
+ // ENOENT for a tool that is not installed, which is the ordinary case for all but one of
47
+ // these on any given machine.
48
+ }
49
+ }
50
+ return { ok: false, text: '', reason: 'No clipboard tool answered. On Linux that is usually the wl-clipboard package (Wayland) or '
51
+ + 'xclip (X11); over SSH there is no clipboard to read at all.' };
52
+ }
53
+ /**
54
+ * Bracketed-paste markers, as a terminal writes them and as Ink sometimes forwards them.
55
+ *
56
+ * Built from escapes rather than written literally, so this file stays free of raw control
57
+ * characters — a source file carrying one reads as binary to some tools and is invisible to a
58
+ * reviewer in all of them. The escape is optional in the pattern because Ink strips it from the
59
+ * opening marker before handing input over, so the literal that reaches a field is "[200~".
60
+ */
61
+ const PASTE_MARKERS = new RegExp('\\u001b?\\[2(?:00|01)~', 'g');
62
+ /** Anything below space, plus DEL. No credential holds one, and an HTTP header cannot carry one. */
63
+ const CONTROL_CHARS = new RegExp('[\\u0000-\\u001f\\u007f]', 'g');
64
+ /**
65
+ * A pasted secret, made safe to use as one.
66
+ *
67
+ * Three things go, each for its own reason. Surrounding whitespace, because a key copied out of a
68
+ * file or a password manager carries a trailing newline that is not part of it. Bracketed paste
69
+ * markers, because a terminal that announces a paste mid-chunk can leave the literal text "[200~"
70
+ * in the field, and a key with that in front of it fails at the provider with an authentication
71
+ * error that says nothing about where it came from. And control characters, which are invalid in an
72
+ * HTTP header — a key carrying one is rejected by the transport before the provider ever sees it,
73
+ * which is the least informative way for this to go wrong.
74
+ */
75
+ export function cleanPastedSecret(text) {
76
+ return String(text ?? '')
77
+ .replace(PASTE_MARKERS, '')
78
+ .replace(CONTROL_CHARS, '')
79
+ .trim();
80
+ }
81
+ //# sourceMappingURL=clipboard.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"clipboard.js","sourceRoot":"","sources":["../src/clipboard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,kFAAkF;AAClF,MAAM,OAAO,GAAoD;IAC/D,oDAAoD;IACpD,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC,cAAc,CAAC,EAAE;IAC5C,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,YAAY,EAAE,WAAW,EAAE,IAAI,CAAC,EAAE;IAC1D,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,aAAa,EAAE,UAAU,CAAC,EAAE;IACnD,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,EAAE,EAAE;IAC7B,qFAAqF;IACrF,EAAE,IAAI,EAAE,gBAAgB,EAAE,IAAI,EAAE,CAAC,YAAY,EAAE,UAAU,EAAE,eAAe,CAAC,EAAE;CAC9E,CAAC;AAWF;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,UAA2D,OAAO,EAClE,SAAS,GAAG,KAAK;IAEjB,MAAM,EAAE,KAAK,EAAE,GAAG,MAAM,MAAM,CAAC,OAAO,CAAC,CAAC;IACxC,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;QAC7B,IAAI,CAAC;YACH,MAAM,IAAI,GAAG,MAAM,KAAK,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,EAAE;gBACtD,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,iBAAiB,EAAE,IAAI;aAC3D,CAAC,CAAC;YACH,IAAI,IAAI,CAAC,QAAQ,KAAK,CAAC;gBAAE,SAAS;YAClC,4FAA4F;YAC5F,8EAA8E;YAC9E,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,MAAM,IAAI,EAAE,CAAC,EAAE,EAAE,EAAE,MAAM,CAAC,IAAI,EAAE,CAAC;QACxE,CAAC;QAAC,MAAM,CAAC;YACP,yFAAyF;YACzF,8BAA8B;QAChC,CAAC;IACH,CAAC;IACD,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE,EAAE,MAAM,EAClC,6FAA6F;cAC3F,6DAA6D,EAAE,CAAC;AACtE,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,aAAa,GAAG,IAAI,MAAM,CAAC,wBAAwB,EAAE,GAAG,CAAC,CAAC;AAEhE,oGAAoG;AACpG,MAAM,aAAa,GAAG,IAAI,MAAM,CAAC,0BAA0B,EAAE,GAAG,CAAC,CAAC;AAElE;;;;;;;;;;GAUG;AACH,MAAM,UAAU,iBAAiB,CAAC,IAAY;IAC5C,OAAO,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC;SACtB,OAAO,CAAC,aAAa,EAAE,EAAE,CAAC;SAC1B,OAAO,CAAC,aAAa,EAAE,EAAE,CAAC;SAC1B,IAAI,EAAE,CAAC;AACZ,CAAC"}
@@ -19,6 +19,32 @@ export declare function pickerLabel(text: string, width: number): string;
19
19
  */
20
20
  export declare function wrapPreserving(text: string, width: number): string[];
21
21
  /** Beyond this a paste is summarised in the transcript; it is still sent in full. */
22
+ /**
23
+ * The picker row that declares a provider rather than choosing one.
24
+ *
25
+ * A sentinel rather than a name, so it can never collide with a real provider — somebody is entitled
26
+ * to declare one called "add".
27
+ */
28
+ export declare const ADD_PROVIDER = "\0add-provider";
29
+ /** Declaring a provider, a field at a time. */
30
+ export interface AddProviderPrompt {
31
+ step: 'name' | 'url' | 'model' | 'confirm';
32
+ name: string;
33
+ url: string;
34
+ model: string;
35
+ /** What is being typed into the current field. */
36
+ value: string;
37
+ /** Why the last entry was refused, shown above the field. */
38
+ error?: string;
39
+ }
40
+ /**
41
+ * Checks one field and says what is wrong with it, or nothing.
42
+ *
43
+ * Separate from the component so the rules can be tested without mounting an Ink app — and they are
44
+ * worth testing: the name rule is what keeps a provider addressable from a command line, and the key
45
+ * variable rule is the one that stops a pasted credential being written into a JSON file.
46
+ */
47
+ export declare function addProviderProblem(step: AddProviderPrompt['step'], value: string): string | null;
22
48
  export declare const MAX_PASTE_ROWS = 80;
23
49
  /**
24
50
  * The pasted text as it should appear once submitted: what was copied, structure intact.
@@ -1 +1 @@
1
- {"version":3,"file":"ink-chat.d.ts","sourceRoot":"","sources":["../src/ink-chat.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ink-chat.d.ts","sourceRoot":"","sources":["../src/ink-chat.tsx"],"names":[],"mappings":"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"}
package/dist/ink-chat.js CHANGED
@@ -7,10 +7,11 @@ import { estimateCost, formatCost } from './pricing.js';
7
7
  import { generateSessionId, saveSession, listSessions, loadSession, relativeAge, renameSession, forkSession, archiveSession, findSession, } from './session.js';
8
8
  import { loadMemory } from './memory.js';
9
9
  import { loadKoneckConfig, saveKoneckConfig, setConfigKey, configKeyDescriptions, CONFIG_FILE, CONFIG_SCHEMA, stepValue, displayValue } from './config-store.js';
10
- import { PROVIDERS, resolveProvider, getApiKey, addUserProvider, userProvidersPath, servesLocally, servesOwnWeights, endpointForProvider } from './providers.js';
10
+ import { PROVIDERS, resolveProvider, getApiKey, addUserProvider, removeUserProvider, userProvidersPath, keyEnvFor, BUILT_INS as BUILT_IN_PROVIDERS, servesLocally, servesOwnWeights, endpointForProvider } from './providers.js';
11
11
  import { normaliseEndpoint, withEndpoint, savedEndpoints, shortSource, resolveModelChoice, withModel } from './endpoints.js';
12
12
  import { loadProjectConfig, loadGlobalConfig, saveGlobalConfig, withoutStoredApiKey } from './config.js';
13
13
  import { rememberKey, forgetAllKeys } from './credentials.js';
14
+ import { readClipboard, cleanPastedSecret } from './clipboard.js';
14
15
  import { paceFrom } from './pace.js';
15
16
  import { policyFrom, FAILOVER_OFF } from './failover.js';
16
17
  import { listCheckpoints, revertCheckpoint, previewCheckpoint, snapshotTree, filesTouchedBy, createWorkspaceCheckpoint, checkpointScope } from './checkpoint.js';
@@ -200,6 +201,37 @@ export function wrapPreserving(text, width) {
200
201
  return out;
201
202
  }
202
203
  /** Beyond this a paste is summarised in the transcript; it is still sent in full. */
204
+ /**
205
+ * The picker row that declares a provider rather than choosing one.
206
+ *
207
+ * A sentinel rather than a name, so it can never collide with a real provider — somebody is entitled
208
+ * to declare one called "add".
209
+ */
210
+ export const ADD_PROVIDER = 'add-provider';
211
+ /**
212
+ * Checks one field and says what is wrong with it, or nothing.
213
+ *
214
+ * Separate from the component so the rules can be tested without mounting an Ink app — and they are
215
+ * worth testing: the name rule is what keeps a provider addressable from a command line, and the key
216
+ * variable rule is the one that stops a pasted credential being written into a JSON file.
217
+ */
218
+ export function addProviderProblem(step, value) {
219
+ const said = value.trim();
220
+ if (step === 'name') {
221
+ if (said === '')
222
+ return 'A name is needed — it is how you refer to this provider afterwards.';
223
+ if (!/^[a-z0-9][a-z0-9._-]{0,40}$/.test(said.toLowerCase())) {
224
+ return 'Letters, digits, dot, dash and underscore only, starting with a letter or digit.';
225
+ }
226
+ return null;
227
+ }
228
+ if (step === 'url') {
229
+ if (!/^https?:\/\/.+/i.test(said))
230
+ return 'That is not an http or https URL.';
231
+ return null;
232
+ }
233
+ return null;
234
+ }
203
235
  export const MAX_PASTE_ROWS = 80;
204
236
  /**
205
237
  * The pasted text as it should appear once submitted: what was copied, structure intact.
@@ -1776,6 +1808,16 @@ export function App({ config: initialConfig, clearFrame }) {
1776
1808
  // to the client as a runtime override — KONECK never writes it to disk, and it is never put
1777
1809
  // into a transcript row, so it cannot end up in a saved session file.
1778
1810
  const [keyPrompt, setKeyPrompt] = useState(null);
1811
+ /**
1812
+ * Declaring a provider, one field at a time.
1813
+ *
1814
+ * A step at a time rather than one long command, because the command form already existed and was
1815
+ * the problem: `/connect openrouter https://openrouter.ai/api/v1 anthropic/claude-sonnet-4.5` is
1816
+ * three things to know before you can type anything at all. Asked one at a time, each question
1817
+ * carries its own example and its own refusal, and the endpoint is checked before it is written
1818
+ * rather than failing on the first message afterwards.
1819
+ */
1820
+ const [addProv, setAddProv] = useState(null);
1779
1821
  const [picker, setPicker] = useState(null);
1780
1822
  const [pickIndex, setPickIndex] = useState(0);
1781
1823
  const [pickQuery, setPickQuery] = useState('');
@@ -2429,13 +2471,103 @@ export function App({ config: initialConfig, clearFrame }) {
2429
2471
  current: m.id === cfg.model,
2430
2472
  }));
2431
2473
  }
2474
+ /**
2475
+ * The providers to choose from, with declaring one as the first choice.
2476
+ *
2477
+ * Declaring has been possible from the terminal all along — `/connect <name> <url>` writes it to
2478
+ * disk and makes it a first-class choice — but nothing said so. The picker listed what existed and
2479
+ * stopped there, `/provider` with no argument printed the same list and a usage line about
2480
+ * switching, and the browser meanwhile had a visible "+ add a provider" button. So the answer to
2481
+ * "how do I add one here" was a command form you had to already know. It is an entry in the list
2482
+ * now, where somebody looking for it is looking.
2483
+ */
2432
2484
  function providerItems() {
2433
- return Object.values(PROVIDERS).map(p => ({
2434
- value: p.name,
2435
- label: p.name,
2436
- desc: `${p.displayName} - ${p.defaultModel}` + (p.userDefined ? ' (yours)' : ''),
2437
- current: p.name === cfg.provider,
2438
- }));
2485
+ return [
2486
+ { value: ADD_PROVIDER, label: '+ add a provider',
2487
+ desc: 'an OpenAI-compatible endpoint KONECK does not ship with' },
2488
+ ...Object.values(PROVIDERS).map(p => ({
2489
+ value: p.name,
2490
+ label: p.name,
2491
+ desc: `${p.displayName} - ${p.defaultModel}` + (p.userDefined ? ' (yours)' : ''),
2492
+ current: p.name === cfg.provider,
2493
+ })),
2494
+ ];
2495
+ }
2496
+ /** Opens the declare-a-provider prompt, optionally with fields already known. */
2497
+ function startAddProvider(seed = {}) {
2498
+ setAddProv({ step: 'name', name: '', url: '', model: '', value: '', ...seed });
2499
+ }
2500
+ /**
2501
+ * Takes what was typed into the current field and moves on, or refuses it.
2502
+ *
2503
+ * The built-in check sits between the endpoint and the model rather than at the start, because it
2504
+ * is not about the name being invalid: repointing a name KONECK ships with is a legitimate thing
2505
+ * to want — a company proxy in front of a vendor is exactly this — and the only requirement is
2506
+ * that it cannot happen by accident. Same reasoning as the browser, and the same wording.
2507
+ */
2508
+ async function advanceAddProvider() {
2509
+ const at = addProv;
2510
+ if (!at)
2511
+ return;
2512
+ const said = at.value.trim();
2513
+ const problem = addProviderProblem(at.step, said);
2514
+ if (problem) {
2515
+ setAddProv({ ...at, error: problem });
2516
+ return;
2517
+ }
2518
+ if (at.step === 'name') {
2519
+ const name = said.toLowerCase();
2520
+ setAddProv({ ...at, name, step: 'url', value: '', error: undefined });
2521
+ return;
2522
+ }
2523
+ if (at.step === 'url') {
2524
+ const url = said.replace(/\/+$/, '');
2525
+ setAddProv({ ...at, url, step: 'model', value: '', error: undefined });
2526
+ return;
2527
+ }
2528
+ if (at.step === 'model') {
2529
+ const next = { ...at, model: said, value: '', error: undefined };
2530
+ if (BUILT_IN_PROVIDERS[at.name]) {
2531
+ setAddProv({ ...next, step: 'confirm' });
2532
+ return;
2533
+ }
2534
+ await saveDeclaredProvider(next);
2535
+ return;
2536
+ }
2537
+ // confirm
2538
+ if (/^y(es)?$/i.test(said)) {
2539
+ await saveDeclaredProvider(at);
2540
+ return;
2541
+ }
2542
+ setAddProv(null);
2543
+ addSystem(`Left ${at.name} pointing at ${BUILT_IN_PROVIDERS[at.name]?.baseURL}.`);
2544
+ }
2545
+ /** Writes the declaration and switches to it, which is what somebody adding one meant to do. */
2546
+ async function saveDeclaredProvider(from) {
2547
+ const shadowed = BUILT_IN_PROVIDERS[from.name];
2548
+ let def;
2549
+ try {
2550
+ def = addUserProvider({
2551
+ name: from.name,
2552
+ displayName: from.name,
2553
+ baseURL: from.url,
2554
+ apiKeyEnv: keyEnvFor(from.name),
2555
+ defaultModel: from.model.trim() || 'default',
2556
+ });
2557
+ }
2558
+ catch (e) {
2559
+ setAddProv({ ...from, error: e instanceof Error ? e.message : String(e) });
2560
+ return;
2561
+ }
2562
+ setAddProv(null);
2563
+ addSystem((shadowed
2564
+ ? `⚠️ Repointed **${def.name}** → ${def.baseURL} (was ${shadowed.baseURL})\n`
2565
+ + 'Everything that uses this name now goes there, in the browser as well as here.\n'
2566
+ : `Declared **${def.name}** → ${def.baseURL}\n`)
2567
+ + `Saved to ${userProvidersPath()}. Its key is read from ${def.apiKeyEnv} `
2568
+ + '(or KONECK_API_KEY).\n'
2569
+ + `Remove it again with \`/provider remove ${def.name}\`.`);
2570
+ await choosePick({ value: def.name, label: def.name, desc: '' }, 'provider');
2439
2571
  }
2440
2572
  /**
2441
2573
  * Offers workspace paths for an `@` reference.
@@ -2575,6 +2707,10 @@ export function App({ config: initialConfig, clearFrame }) {
2575
2707
  addSystem(`Model → ${item.value} · remembered for ${cfg.provider} (session reset)`);
2576
2708
  return;
2577
2709
  }
2710
+ if (kind === 'provider' && item.value === ADD_PROVIDER) {
2711
+ startAddProvider();
2712
+ return;
2713
+ }
2578
2714
  if (kind === 'provider') {
2579
2715
  const def = PROVIDERS[item.value];
2580
2716
  // The endpoint saved for the provider being switched to, not its built-in address. Passing
@@ -2617,6 +2753,45 @@ export function App({ config: initialConfig, clearFrame }) {
2617
2753
  }
2618
2754
  }
2619
2755
  /** Applies a key typed at the prompt. Takes effect immediately — no restart. */
2756
+ /**
2757
+ * Puts whatever is on the clipboard into the key field.
2758
+ *
2759
+ * The result is reported in the panel rather than as a system message, because the panel is where
2760
+ * the person is looking and because the alternative — silence — is exactly what made pasting feel
2761
+ * broken in the first place. Nothing about the key itself is shown, only how many characters
2762
+ * arrived, which is enough to tell a successful paste from an empty clipboard.
2763
+ */
2764
+ async function pasteFromClipboard() {
2765
+ setKeyPrompt(p => (p ? { ...p, note: 'reading the clipboard…' } : p));
2766
+ const got = await readClipboard();
2767
+ if (!got.ok) {
2768
+ setKeyPrompt(p => (p ? { ...p, note: got.reason ?? 'the clipboard could not be read' } : p));
2769
+ return;
2770
+ }
2771
+ const clean = cleanPastedSecret(got.text);
2772
+ if (clean === '') {
2773
+ setKeyPrompt(p => (p ? { ...p, note: 'the clipboard is empty — copy the key first' } : p));
2774
+ return;
2775
+ }
2776
+ setKeyPrompt(p => (p ? {
2777
+ ...p, value: p.value + clean,
2778
+ note: `${clean.length} characters from the clipboard · enter to use them`,
2779
+ } : p));
2780
+ }
2781
+ /** The same, for the field being filled in while declaring a provider. */
2782
+ async function pasteIntoAddProvider() {
2783
+ const got = await readClipboard();
2784
+ if (!got.ok) {
2785
+ setAddProv(p => (p ? { ...p, error: got.reason ?? 'the clipboard could not be read' } : p));
2786
+ return;
2787
+ }
2788
+ const clean = cleanPastedSecret(got.text);
2789
+ if (clean === '') {
2790
+ setAddProv(p => (p ? { ...p, error: 'the clipboard is empty' } : p));
2791
+ return;
2792
+ }
2793
+ setAddProv(p => (p ? { ...p, value: p.value + clean, error: undefined } : p));
2794
+ }
2620
2795
  async function applyTypedKey() {
2621
2796
  if (!keyPrompt)
2622
2797
  return;
@@ -2975,8 +3150,64 @@ export function App({ config: initialConfig, clearFrame }) {
2975
3150
  return;
2976
3151
  }
2977
3152
  case '/provider': {
3153
+ const [verb, ...restArgs] = arg.trim().split(/\s+/).filter(Boolean);
3154
+ /*
3155
+ * add and remove, named as what they are.
3156
+ *
3157
+ * Declaring was only ever reachable as `/connect <name> <url>`, which is a thing you have to
3158
+ * already know — and somebody looking for how to add a provider looks at `/provider`. The
3159
+ * old form still works; this is where it can be found.
3160
+ */
3161
+ if (verb?.toLowerCase() === 'add') {
3162
+ const [name, url, ...model] = restArgs;
3163
+ // With arguments it is one step; without, it asks. Both end in the same place.
3164
+ startAddProvider({
3165
+ ...(name ? { name: name.toLowerCase(), step: 'url' } : {}),
3166
+ ...(name && url ? { url: url.replace(/\/+$/, ''), step: 'model' } : {}),
3167
+ ...(model.length ? { value: model.join(' ') } : {}),
3168
+ });
3169
+ return;
3170
+ }
3171
+ if (verb?.toLowerCase() === 'remove' || verb?.toLowerCase() === 'forget') {
3172
+ const target = (restArgs[0] ?? '').toLowerCase();
3173
+ if (!target) {
3174
+ const mine = Object.values(PROVIDERS).filter(p => p.userDefined).map(p => p.name);
3175
+ addSystem(mine.length
3176
+ ? `Usage: /provider remove <name>\nYours: ${mine.join(', ')}`
3177
+ : 'You have not declared any providers, so there is nothing to remove.');
3178
+ return;
3179
+ }
3180
+ const wasShadowing = BUILT_IN_PROVIDERS[target] !== undefined;
3181
+ if (!removeUserProvider(target)) {
3182
+ addSystem(PROVIDERS[target]
3183
+ ? `"${target}" ships with KONECK — it was not declared by you, so there is nothing to remove.`
3184
+ : `No declared provider called "${target}".`);
3185
+ return;
3186
+ }
3187
+ addSystem(wasShadowing
3188
+ ? `Removed your "${target}". The built-in one is back, pointing at ${BUILT_IN_PROVIDERS[target].baseURL}.`
3189
+ : `Removed "${target}".`);
3190
+ return;
3191
+ }
2978
3192
  if (!arg) {
2979
- addSystem(`Current provider: ${cfg.provider}\nAvailable: ${Object.keys(PROVIDERS).join(', ')}\nUsage: /provider <name>`);
3193
+ /*
3194
+ * Says where things stand, then opens the picker.
3195
+ *
3196
+ * The list scrolls into the transcript where it can be read back; the picker is for doing
3197
+ * something about it, and carries "+ add a provider" as its first row. An earlier version
3198
+ * of this printed the summary and stopped, and the summary said the picker would open —
3199
+ * which it did not, because nothing opened it. A help line that describes behaviour the
3200
+ * program does not have is worse than no help line.
3201
+ */
3202
+ const mine = Object.values(PROVIDERS).filter(p => p.userDefined).map(p => p.name);
3203
+ addSystem(`Current provider: ${cfg.provider}\n`
3204
+ + `Available: ${Object.keys(PROVIDERS).join(', ')}\n`
3205
+ + (mine.length ? `Declared by you: ${mine.join(', ')}\n` : '')
3206
+ + '\nSwitch: /provider <name>, or pick one below\n'
3207
+ + 'Add: /provider add — asks for a name, an endpoint and a default model\n'
3208
+ + ' /provider add <name> <url> [model] if you have them to hand\n'
3209
+ + 'Remove: /provider remove <name>');
3210
+ openPicker('provider', providerItems());
2980
3211
  return;
2981
3212
  }
2982
3213
  const name = arg.trim().toLowerCase();
@@ -2985,7 +3216,7 @@ export function App({ config: initialConfig, clearFrame }) {
2985
3216
  // Switching to a name that does not resolve used to reset the session anyway, leaving it
2986
3217
  // pointed at nothing and failing on the next message instead of on the command.
2987
3218
  addSystem(`Unknown provider "${arg}". Known: ${Object.keys(PROVIDERS).join(', ')}\n` +
2988
- `Declare one: /connect ${name} https://endpoint/v1`);
3219
+ `Declare it: /provider add ${name} https://endpoint/v1`);
2989
3220
  return;
2990
3221
  }
2991
3222
  // Keep the typed command and picker on one implementation path. The picker clears a
@@ -4084,6 +4315,35 @@ export function App({ config: initialConfig, clearFrame }) {
4084
4315
  answer(typed);
4085
4316
  return;
4086
4317
  }
4318
+ if (addProv) {
4319
+ if (key.escape) {
4320
+ setAddProv(null);
4321
+ addSystem('Cancelled — nothing was declared.');
4322
+ return;
4323
+ }
4324
+ if (key.return) {
4325
+ void advanceAddProvider();
4326
+ return;
4327
+ }
4328
+ if (key.backspace || key.delete) {
4329
+ setAddProv(p => (p ? { ...p, value: p.value.slice(0, -1) } : p));
4330
+ return;
4331
+ }
4332
+ // The same clipboard escape hatch the key prompt has. An endpoint is the other thing here
4333
+ // nobody wants to retype, and it is the field most likely to arrive from a browser's address
4334
+ // bar with a newline attached.
4335
+ if (key.ctrl && input.toLowerCase() === 'v') {
4336
+ void pasteIntoAddProvider();
4337
+ return;
4338
+ }
4339
+ // A pasted endpoint arrives as one chunk, so it is taken whole rather than a letter at a time.
4340
+ if (!key.meta && input) {
4341
+ const clean = cleanPastedSecret(input);
4342
+ if (clean !== '')
4343
+ setAddProv(p => (p ? { ...p, value: p.value + clean } : p));
4344
+ }
4345
+ return;
4346
+ }
4087
4347
  if (keyPrompt) {
4088
4348
  if (key.escape) {
4089
4349
  setKeyPrompt(null);
@@ -4098,9 +4358,27 @@ export function App({ config: initialConfig, clearFrame }) {
4098
4358
  setKeyPrompt(p => (p ? { ...p, value: p.value.slice(0, -1) } : p));
4099
4359
  return;
4100
4360
  }
4101
- // Paste arrives as one chunk; take it whole rather than character by character.
4102
- if (!key.ctrl && !key.meta && input) {
4103
- setKeyPrompt(p => (p ? { ...p, value: p.value + input } : p));
4361
+ /*
4362
+ * Ctrl+V fetches the clipboard itself, rather than waiting for the terminal to deliver it.
4363
+ *
4364
+ * Pasting into a terminal is several different mechanisms and not all of them arrive here:
4365
+ * bracketed paste does, a plain burst does, a middle-click may have been taken over by
4366
+ * KONECK's own mouse handling, and on Windows some hosts deliver Ctrl+V as a control code
4367
+ * with no text attached. Somebody putting an API key into this box does not care which of
4368
+ * those their terminal does, and typing a sixty-character secret by hand is not an answer. So
4369
+ * this asks the operating system instead, which works the same way everywhere it works at all.
4370
+ */
4371
+ if (key.ctrl && input.toLowerCase() === 'v') {
4372
+ void pasteFromClipboard();
4373
+ return;
4374
+ }
4375
+ // A paste arrives as one chunk, so it is taken whole rather than a character at a time — and
4376
+ // cleaned, because a key copied out of a file carries a newline and a terminal announcing a
4377
+ // paste mid-chunk can leave its marker text behind.
4378
+ if (!key.meta && input) {
4379
+ const clean = cleanPastedSecret(input);
4380
+ if (clean !== '')
4381
+ setKeyPrompt(p => (p ? { ...p, value: p.value + clean } : p));
4104
4382
  }
4105
4383
  return;
4106
4384
  }
@@ -5138,7 +5416,15 @@ export function App({ config: initialConfig, clearFrame }) {
5138
5416
  return (_jsx(Box, { flexDirection: "column", children: _jsx(Text, { backgroundColor: selected ? CYAN : undefined, color: selected ? '#10222A' : INK, bold: selected, children: fitCells(`${selected ? G.caret + ' ' : ' '}${label}` +
5139
5417
  `${item.current ? '(current) ' : ''}${header ? `${header} · ` : ''}${item.desc}`, width) }) }, item.value + absolute));
5140
5418
  }), list.length > pickerRows && (_jsxs(Text, { color: DIM, children: ["\u2191\u2193 navigate \u00B7 pgup/pgdn jump \u00B7 enter select \u00B7 showing ", Math.max(0, start) + 1, "\u2013", Math.max(0, start) + shown.length, " of ", list.length] }))] }));
5141
- })(), keyPrompt && (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: AMBER, paddingX: 1, marginTop: 1, width: barWidth, children: [_jsxs(Text, { color: AMBER, bold: true, children: ["API key for ", keyPrompt.provider] }), _jsx(Text, { color: MUTED, children: "Paste it and press enter. Held in memory for this session only; esc to cancel." }), _jsxs(Box, { marginTop: 1, children: [_jsxs(Text, { color: MUTED, children: [keyPrompt.env, ": "] }), _jsx(Text, { color: INK, children: '*'.repeat(Math.min(keyPrompt.value.length, 48)) }), _jsx(Text, { color: CYAN, children: "\u2588" })] })] })), ask && (() => {
5419
+ })(), addProv && (() => {
5420
+ const shadowed = BUILT_IN_PROVIDERS[addProv.name];
5421
+ const ask = addProv.step === 'name' ? { label: 'name', hint: 'short and lowercase, e.g. openrouter' }
5422
+ : addProv.step === 'url' ? { label: 'endpoint', hint: 'OpenAI-compatible, e.g. https://openrouter.ai/api/v1' }
5423
+ : addProv.step === 'model' ? { label: 'default model', hint: 'optional — enter to skip' }
5424
+ : { label: 'repoint ' + addProv.name + '? (y/n)',
5425
+ hint: `it ships with KONECK, pointing at ${shadowed?.baseURL}` };
5426
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: CYAN, paddingX: 1, marginTop: 1, width: barWidth, children: [_jsx(Text, { color: CYAN, bold: true, children: "Declare a provider" }), _jsxs(Text, { color: MUTED, children: [ask.hint, " \u00B7 esc to cancel"] }), addProv.name !== '' && (_jsxs(Text, { color: DIM, children: [addProv.name, addProv.url ? ' ' + addProv.url : ''] })), addProv.error && _jsx(Text, { color: CRIMSON, children: addProv.error }), _jsxs(Box, { marginTop: 1, children: [_jsxs(Text, { color: MUTED, children: [ask.label, ": "] }), _jsx(Text, { color: INK, children: addProv.value }), _jsx(Text, { color: CYAN, children: "\u2588" })] })] }));
5427
+ })(), keyPrompt && (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: AMBER, paddingX: 1, marginTop: 1, width: barWidth, children: [_jsxs(Text, { color: AMBER, bold: true, children: ["API key for ", keyPrompt.provider] }), _jsx(Text, { color: MUTED, children: "Paste it and press enter, or ctrl+v to read the clipboard. Held in memory for this session only; esc to cancel." }), keyPrompt.note && _jsx(Text, { color: CYAN, children: keyPrompt.note }), _jsxs(Box, { marginTop: 1, children: [_jsxs(Text, { color: MUTED, children: [keyPrompt.env, ": "] }), _jsx(Text, { color: INK, children: '*'.repeat(Math.min(keyPrompt.value.length, 48)) }), _jsx(Text, { color: CYAN, children: "\u2588" }), keyPrompt.value.length > 0 && (_jsxs(Text, { color: DIM, children: [" ", keyPrompt.value.length, " characters"] }))] })] })), ask && (() => {
5142
5428
  let detail = '';
5143
5429
  try {
5144
5430
  const parsed = JSON.parse(ask.args);