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.
- package/dist/clipboard.d.ts +46 -0
- package/dist/clipboard.d.ts.map +1 -0
- package/dist/clipboard.js +81 -0
- package/dist/clipboard.js.map +1 -0
- package/dist/ink-chat.d.ts +26 -0
- package/dist/ink-chat.d.ts.map +1 -1
- package/dist/ink-chat.js +299 -13
- package/dist/ink-chat.js.map +1 -1
- package/dist/web/ui-client.d.ts.map +1 -1
- package/dist/web/ui-client.js +44 -10
- package/dist/web/ui-client.js.map +1 -1
- package/dist/web/ui.d.ts.map +1 -1
- package/dist/web/ui.js +16 -1
- package/dist/web/ui.js.map +1 -1
- package/package.json +1 -1
|
@@ -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"}
|
package/dist/ink-chat.d.ts
CHANGED
|
@@ -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.
|
package/dist/ink-chat.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ink-chat.d.ts","sourceRoot":"","sources":["../src/ink-chat.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAsC,MAAM,OAAO,CAAC;
|
|
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
|
|
2434
|
-
value:
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
4102
|
-
|
|
4103
|
-
|
|
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
|
-
})(),
|
|
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);
|