@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.
Files changed (3) hide show
  1. package/README.md +84 -0
  2. package/package.json +14 -3
  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.1.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: { drael: { name: 'Drael' } },
93
+ models: { [model]: { name: 'Drael' } },
88
94
  },
89
95
  },
90
- model: 'drael/drael',
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: { drael: { name: 'Drael' } },
113
+ models: { [model]: { name: 'Drael' } },
108
114
  },
109
115
  },
110
- model: 'drael/drael',
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 (client.merges && !parses(file)) {
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)))