@drael/code 0.1.0 → 0.3.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/README.md +48 -0
- package/package.json +14 -3
- package/src/code.js +60 -10
package/README.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# @drael/code
|
|
2
|
+
|
|
3
|
+
Points a coding client at [Drael](https://drael.sh).
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
npx @drael/code
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
It asks for your key and which client, writes that client's own configuration file, and
|
|
10
|
+
nothing else. Whatever it touches it copies first.
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
npx @drael/code --list what it knows how to configure, and where
|
|
14
|
+
npx @drael/code --dry-run exactly what it would write, changing nothing
|
|
15
|
+
npx @drael/code --uninstall every file it changed, put back
|
|
16
|
+
npx @drael/code --help the flags
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Those four answer for themselves rather than being copied out here, so this page cannot
|
|
20
|
+
go stale about which clients are supported, where their files live, or what the flags are.
|
|
21
|
+
|
|
22
|
+
## Why npx rather than `curl | sh`
|
|
23
|
+
|
|
24
|
+
Piping a URL into a shell is exactly the pattern this product's own users are right to
|
|
25
|
+
refuse. npx is already on the machine of anybody running an editor extension, and it makes
|
|
26
|
+
Windows work without a second implementation to keep in step.
|
|
27
|
+
|
|
28
|
+
It asks for no privilege, and it writes nothing outside the configuration file of the
|
|
29
|
+
client you chose. The source is one file, short enough to read in a sitting: the people
|
|
30
|
+
most right to distrust an opaque installer are this product's own users, so it earns the
|
|
31
|
+
trust by being readable rather than by asking.
|
|
32
|
+
|
|
33
|
+
## Doing it by hand
|
|
34
|
+
|
|
35
|
+
There is nothing privileged in what it writes. The API is OpenAI-compatible, so any client
|
|
36
|
+
takes a base URL and a key directly:
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
export OPENAI_BASE_URL="https://drael.sh/v1"
|
|
40
|
+
export OPENAI_API_KEY="dk-YOUR-KEY"
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Everything else
|
|
44
|
+
|
|
45
|
+
[drael.sh/docs/install](https://drael.sh/docs/install) is the documentation, and it is the
|
|
46
|
+
copy that is kept current.
|
|
47
|
+
|
|
48
|
+
MIT
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@drael/code",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Point your coding client at Drael.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
"src"
|
|
11
11
|
],
|
|
12
12
|
"scripts": {
|
|
13
|
-
"ci": "node --check src/code.js"
|
|
13
|
+
"ci": "node --check src/code.js && node --test \"test/*.test.js\"",
|
|
14
|
+
"test": "node --test \"test/*.test.js\""
|
|
14
15
|
},
|
|
15
16
|
"engines": {
|
|
16
17
|
"node": ">=20"
|
|
@@ -21,5 +22,15 @@
|
|
|
21
22
|
},
|
|
22
23
|
"dependencies": {
|
|
23
24
|
"@clack/core": "^1.0.0"
|
|
24
|
-
}
|
|
25
|
+
},
|
|
26
|
+
"homepage": "https://drael.sh/docs/install",
|
|
27
|
+
"keywords": [
|
|
28
|
+
"drael",
|
|
29
|
+
"openai-compatible",
|
|
30
|
+
"installer",
|
|
31
|
+
"cli",
|
|
32
|
+
"opencode",
|
|
33
|
+
"kilo-code",
|
|
34
|
+
"llm"
|
|
35
|
+
]
|
|
25
36
|
}
|
package/src/code.js
CHANGED
|
@@ -63,6 +63,12 @@ const KILO_CONFIG = join(homedir(), '.config', 'kilo', 'kilo.jsonc')
|
|
|
63
63
|
// it. Asking for the address was the installer failing to know its own address.
|
|
64
64
|
const DRAEL = 'https://drael.sh'
|
|
65
65
|
|
|
66
|
+
// The id the backend publishes today (`chat/transport.ModelID`), and a test holds the
|
|
67
|
+
// two together. It is the fallback rather than the answer: what goes into the file is
|
|
68
|
+
// what the host says it serves, so a version bump on the box reaches a config written
|
|
69
|
+
// months earlier without anybody editing this.
|
|
70
|
+
const MODEL = 'drael-v1'
|
|
71
|
+
|
|
66
72
|
/* ────────────────────────────────────────────────────────────────── the clients ── */
|
|
67
73
|
|
|
68
74
|
/**
|
|
@@ -76,7 +82,7 @@ const clients = {
|
|
|
76
82
|
opencode: {
|
|
77
83
|
label: 'OpenCode',
|
|
78
84
|
file: () => join(CONFIG_HOME, 'opencode', 'opencode.json'),
|
|
79
|
-
contents: (host, key) =>
|
|
85
|
+
contents: (host, key, model) =>
|
|
80
86
|
json({
|
|
81
87
|
$schema: 'https://opencode.ai/config.json',
|
|
82
88
|
provider: {
|
|
@@ -84,10 +90,10 @@ const clients = {
|
|
|
84
90
|
npm: '@ai-sdk/openai-compatible',
|
|
85
91
|
name: 'Drael',
|
|
86
92
|
options: { baseURL: `${host}/v1`, apiKey: key },
|
|
87
|
-
models: {
|
|
93
|
+
models: { [model]: { name: 'Drael' } },
|
|
88
94
|
},
|
|
89
95
|
},
|
|
90
|
-
model:
|
|
96
|
+
model: `drael/${model}`,
|
|
91
97
|
}),
|
|
92
98
|
},
|
|
93
99
|
|
|
@@ -95,7 +101,7 @@ const clients = {
|
|
|
95
101
|
label: 'Kilo Code',
|
|
96
102
|
merges: true,
|
|
97
103
|
file: () => KILO_CONFIG,
|
|
98
|
-
contents: (host, key) => {
|
|
104
|
+
contents: (host, key, model) => {
|
|
99
105
|
const existing = readJson(KILO_CONFIG)
|
|
100
106
|
return json({
|
|
101
107
|
...existing,
|
|
@@ -104,10 +110,10 @@ const clients = {
|
|
|
104
110
|
...existing.provider,
|
|
105
111
|
drael: {
|
|
106
112
|
options: { baseURL: `${host}/v1`, apiKey: key },
|
|
107
|
-
models: {
|
|
113
|
+
models: { [model]: { name: 'Drael' } },
|
|
108
114
|
},
|
|
109
115
|
},
|
|
110
|
-
model:
|
|
116
|
+
model: `drael/${model}`,
|
|
111
117
|
})
|
|
112
118
|
},
|
|
113
119
|
},
|
|
@@ -268,6 +274,25 @@ const askClient = () =>
|
|
|
268
274
|
}),
|
|
269
275
|
)
|
|
270
276
|
|
|
277
|
+
/**
|
|
278
|
+
* What the host says it serves, which is the same question `/v1/models` answers for the
|
|
279
|
+
* product itself. The one call this makes, and the only one: it is to the host being
|
|
280
|
+
* configured, with the key being configured, and a host that does not answer is not a
|
|
281
|
+
* reason to refuse to write anything, so the published id stands in.
|
|
282
|
+
*/
|
|
283
|
+
async function servedModel(host, key) {
|
|
284
|
+
try {
|
|
285
|
+
const response = await fetch(`${host}/v1/models`, {
|
|
286
|
+
headers: { Authorization: `Bearer ${key}` },
|
|
287
|
+
signal: AbortSignal.timeout(4000),
|
|
288
|
+
})
|
|
289
|
+
const served = (await response.json())?.data?.[0]?.id
|
|
290
|
+
return typeof served === 'string' && served ? served : MODEL
|
|
291
|
+
} catch {
|
|
292
|
+
return MODEL
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
271
296
|
/**
|
|
272
297
|
* Whether the client's own directory is on this machine. It is a hint and never a gate:
|
|
273
298
|
* a fresh install may not have written its directory yet, so every client stays on the
|
|
@@ -303,7 +328,7 @@ async function install(args) {
|
|
|
303
328
|
}
|
|
304
329
|
|
|
305
330
|
const file = client.file()
|
|
306
|
-
const contents = client.contents(host, key)
|
|
331
|
+
const contents = client.contents(host, key, await servedModel(host, key))
|
|
307
332
|
|
|
308
333
|
// An answered prompt leaves its frame on screen and needs a gap after it. With every
|
|
309
334
|
// answer given as a flag there was no frame, and the masthead's own gap is already it.
|
|
@@ -321,13 +346,16 @@ async function install(args) {
|
|
|
321
346
|
return
|
|
322
347
|
}
|
|
323
348
|
|
|
349
|
+
// Asked before the write, or it inspects the valid JSON we are about to put there.
|
|
350
|
+
const replaced = client.merges && !parses(file)
|
|
351
|
+
|
|
324
352
|
const kept = backup(file)
|
|
325
353
|
mkdirSync(dirname(file), { recursive: true })
|
|
326
354
|
writeFileSync(file, contents, { mode: 0o600 })
|
|
327
355
|
chmodSync(file, 0o600)
|
|
328
356
|
|
|
329
357
|
console.log(row(client.merges ? 'merged' : 'wrote', tilde(file)))
|
|
330
|
-
if (
|
|
358
|
+
if (replaced) {
|
|
331
359
|
console.log(under(dim('it was not plain JSON, so it was replaced rather than merged')))
|
|
332
360
|
}
|
|
333
361
|
console.log(row(kept.label, dim(kept.detail)))
|
|
@@ -415,6 +443,26 @@ function uninstall(args) {
|
|
|
415
443
|
console.log()
|
|
416
444
|
}
|
|
417
445
|
|
|
446
|
+
/**
|
|
447
|
+
* The flags, from the tool rather than from a page about the tool. The README points here
|
|
448
|
+
* instead of repeating any of it, and a test holds this list to the flags the code reads.
|
|
449
|
+
*/
|
|
450
|
+
function help() {
|
|
451
|
+
masthead('into the editor you already use')
|
|
452
|
+
for (const [flag, said] of [
|
|
453
|
+
['--key', 'the API key. Falls back to DRAEL_KEY, then to a prompt'],
|
|
454
|
+
['--client', 'which one to configure. Without it, you are asked'],
|
|
455
|
+
['--host', `for a local build or your own box. Defaults to ${DRAEL}`],
|
|
456
|
+
['--list', 'what it knows how to configure, and where'],
|
|
457
|
+
['--dry-run', 'exactly what it would write, changing nothing'],
|
|
458
|
+
['--uninstall', 'every file it changed, put back'],
|
|
459
|
+
['--help', 'this'],
|
|
460
|
+
]) {
|
|
461
|
+
console.log(row(flag, dim(said)))
|
|
462
|
+
}
|
|
463
|
+
console.log()
|
|
464
|
+
}
|
|
465
|
+
|
|
418
466
|
function list() {
|
|
419
467
|
console.log()
|
|
420
468
|
for (const [name, client] of Object.entries(clients)) {
|
|
@@ -445,7 +493,7 @@ function parse(argv) {
|
|
|
445
493
|
const arg = argv[i]
|
|
446
494
|
if (!arg.startsWith('--')) continue
|
|
447
495
|
const name = arg.slice(2)
|
|
448
|
-
if (['list', 'uninstall', 'dry-run'].includes(name)) {
|
|
496
|
+
if (['list', 'uninstall', 'dry-run', 'help'].includes(name)) {
|
|
449
497
|
parsed[name] = true
|
|
450
498
|
continue
|
|
451
499
|
}
|
|
@@ -460,7 +508,9 @@ process.on('exit', () => process.stdout.write('\u001b[?25h'))
|
|
|
460
508
|
|
|
461
509
|
const args = parse(process.argv.slice(2))
|
|
462
510
|
|
|
463
|
-
if (args.
|
|
511
|
+
if (args.help) {
|
|
512
|
+
help()
|
|
513
|
+
} else if (args.list) {
|
|
464
514
|
list()
|
|
465
515
|
} else if (args.uninstall) {
|
|
466
516
|
uninstall(args)
|