@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.
Files changed (3) hide show
  1. package/README.md +48 -0
  2. package/package.json +14 -3
  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.1.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: { 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)))
@@ -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.list) {
511
+ if (args.help) {
512
+ help()
513
+ } else if (args.list) {
464
514
  list()
465
515
  } else if (args.uninstall) {
466
516
  uninstall(args)