@drael/code 0.1.0 → 0.2.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 +84 -0
- package/package.json +14 -3
- package/src/code.js +36 -8
package/README.md
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# @drael/code
|
|
2
|
+
|
|
3
|
+
Points a coding client at [Drael](https://drael.sh). One command, and it asks before it
|
|
4
|
+
writes.
|
|
5
|
+
|
|
6
|
+
```sh
|
|
7
|
+
npx @drael/code
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
It asks for your key and which client, and nothing else. Both can be flags instead:
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
npx @drael/code --key dk-YOUR-KEY --client kilo
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## What it configures
|
|
17
|
+
|
|
18
|
+
| Client | File |
|
|
19
|
+
| ---------- | ----------------------------- |
|
|
20
|
+
| `opencode` | `~/.config/opencode/opencode.json` |
|
|
21
|
+
| `kilo` | `~/.config/kilo/kilo.jsonc` |
|
|
22
|
+
| `env` | `~/.config/drael/env.sh`, for anything reading `OPENAI_BASE_URL` |
|
|
23
|
+
|
|
24
|
+
All three are written `0600`, because a file holding an API key is not a file other
|
|
25
|
+
accounts on the machine should read.
|
|
26
|
+
|
|
27
|
+
The `kilo` file is **merged** rather than replaced: it is the whole extension's
|
|
28
|
+
configuration and not ours, so the other providers and everything else in it are kept.
|
|
29
|
+
|
|
30
|
+
## Undoing it
|
|
31
|
+
|
|
32
|
+
```sh
|
|
33
|
+
npx @drael/code --uninstall
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Every file is copied before it is written, under `~/.local/state/drael/installer`.
|
|
37
|
+
Uninstall puts the originals back byte for byte and removes the files that did not exist
|
|
38
|
+
before. Installing twice keeps the **first** copy, so what an uninstall restores is the
|
|
39
|
+
state before this ever touched the file.
|
|
40
|
+
|
|
41
|
+
`--dry-run` prints exactly what would be written and changes nothing, including the copy.
|
|
42
|
+
|
|
43
|
+
## The flags
|
|
44
|
+
|
|
45
|
+
| Flag | What it does |
|
|
46
|
+
| ------------- | ---------------------------------------------------------------- |
|
|
47
|
+
| `--key` | The API key. Falls back to `DRAEL_KEY`, then to a prompt. |
|
|
48
|
+
| `--client` | `opencode`, `kilo` or `env`. Without it you are asked. |
|
|
49
|
+
| `--host` | For a local build or your own box. Defaults to `https://drael.sh`. |
|
|
50
|
+
| `--list` | Prints what it knows how to configure, and exits. |
|
|
51
|
+
| `--dry-run` | Prints what would be written, and changes nothing. |
|
|
52
|
+
| `--uninstall` | Restores every file it changed, and exits. |
|
|
53
|
+
|
|
54
|
+
## Why npx rather than `curl | sh`
|
|
55
|
+
|
|
56
|
+
Piping a URL into a shell is exactly the pattern this product's own users are right to
|
|
57
|
+
refuse. npx is already on the machine of anybody running an editor extension, and it
|
|
58
|
+
makes Windows work without a second implementation to keep in step.
|
|
59
|
+
|
|
60
|
+
It makes one network call: `GET /v1/models` against the host it is configuring, to write
|
|
61
|
+
the id that host actually serves rather than one assumed here. A host that does not
|
|
62
|
+
answer is no reason to refuse to configure anything, so the published id stands in.
|
|
63
|
+
|
|
64
|
+
That call is not a check. The last thing it prints is the `curl` that verifies the host
|
|
65
|
+
and the key, for you to run yourself.
|
|
66
|
+
|
|
67
|
+
The source is one file with one dependency, and it is short enough to read in a sitting.
|
|
68
|
+
The people most right to distrust an opaque installer are this product's own users, so it
|
|
69
|
+
earns the trust by being readable rather than by asking.
|
|
70
|
+
|
|
71
|
+
## Doing it by hand
|
|
72
|
+
|
|
73
|
+
There is nothing privileged in what it writes. Any OpenAI-compatible client takes the
|
|
74
|
+
same two values directly:
|
|
75
|
+
|
|
76
|
+
```sh
|
|
77
|
+
export OPENAI_BASE_URL="https://drael.sh/v1"
|
|
78
|
+
export OPENAI_API_KEY="dk-YOUR-KEY"
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
The model id is `drael-v1`, and `GET /v1/models` is where it comes from. Full documentation at
|
|
82
|
+
[drael.sh/docs/install](https://drael.sh/docs/install).
|
|
83
|
+
|
|
84
|
+
MIT
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@drael/code",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.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)))
|