@drael/code 0.4.0 → 0.5.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 (2) hide show
  1. package/package.json +1 -1
  2. package/src/code.js +114 -26
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drael/code",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Point your coding client at Drael.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/code.js CHANGED
@@ -105,12 +105,12 @@ const clients = {
105
105
  merges: true,
106
106
  file: () => KILO_CONFIG,
107
107
  contents: (host, key, model) => {
108
- const existing = readJson(KILO_CONFIG)
108
+ const kept = existing(KILO_CONFIG).value
109
109
  return json({
110
- ...existing,
110
+ ...kept,
111
111
  $schema: 'https://app.kilo.ai/config.json',
112
112
  provider: {
113
- ...existing.provider,
113
+ ...kept.provider,
114
114
  drael: {
115
115
  options: { baseURL: `${host}/v1`, apiKey: key },
116
116
  models: { [model]: { name: 'Drael' } },
@@ -121,6 +121,40 @@ const clients = {
121
121
  },
122
122
  },
123
123
 
124
+ continue: {
125
+ label: 'Continue',
126
+ file: () => join(homedir(), '.continue', 'config.yaml'),
127
+ contents: (host, key, model) =>
128
+ [
129
+ 'name: Drael',
130
+ 'version: 1.0.0',
131
+ 'schema: v1',
132
+ 'models:',
133
+ ' - name: Drael',
134
+ ' provider: openai',
135
+ ` model: ${scalar(model)}`,
136
+ ` apiBase: ${scalar(`${host}/v1`)}`,
137
+ ` apiKey: ${scalar(key)}`,
138
+ '',
139
+ ].join('\n'),
140
+ },
141
+
142
+ aider: {
143
+ label: 'Aider',
144
+ // A dotfile at the root of home, so its own presence is the only sign of it: the
145
+ // directory the others are found by would be the home directory, which is always there.
146
+ at: () => join(homedir(), '.aider.conf.yml'),
147
+ file: () => join(homedir(), '.aider.conf.yml'),
148
+ contents: (host, key, model) =>
149
+ [
150
+ '# Drael.',
151
+ `model: ${scalar(`openai/${model}`)}`,
152
+ `openai-api-base: ${scalar(`${host}/v1`)}`,
153
+ `openai-api-key: ${scalar(key)}`,
154
+ '',
155
+ ].join('\n'),
156
+ },
157
+
124
158
  env: {
125
159
  label: 'anything reading OPENAI_BASE_URL',
126
160
  file: () => join(CONFIG_HOME, 'drael', 'env.sh'),
@@ -140,30 +174,80 @@ const clients = {
140
174
  const json = (value) => `${JSON.stringify(value, null, 2)}\n`
141
175
 
142
176
  /**
143
- * Kilo's file is the whole extension's configuration and not ours, so ours is merged
144
- * into what is already there. A file that does not parse is treated as absent rather
145
- * than guessed at: JSONC allows comments, and a parser guessing at them writes back a
146
- * mangled config. The copy under BACKUPS still holds the original, and `parses` below
147
- * is what lets the frame say so before it writes.
177
+ * A YAML scalar, for the two files that are YAML. YAML 1.2 is a superset of JSON, so a
178
+ * JSON string is already a correctly quoted and escaped one, and quoting every value is
179
+ * what keeps the `https://` in a URL from reading as a key with a comment after it.
148
180
  */
149
- function readJson(file) {
150
- try {
151
- return JSON.parse(readFileSync(file, 'utf8'))
152
- } catch {
153
- return {}
154
- }
155
- }
181
+ const scalar = (value) => JSON.stringify(String(value))
156
182
 
157
- function parses(file) {
183
+ /**
184
+ * Kilo's file is the whole extension's configuration and not ours, so ours is merged into
185
+ * what is already there. It is a `.jsonc`, and a file that names itself that has comments
186
+ * in it, so reading it with `JSON.parse` alone meant every commented config was replaced
187
+ * whole: the other providers, the keys and the settings in it, gone but for the copy under
188
+ * BACKUPS.
189
+ *
190
+ * What cannot be kept is the comments themselves, because what goes back is JSON. The
191
+ * frame says so when that is what happened.
192
+ */
193
+ function existing(file) {
158
194
  if (!existsSync(file)) {
159
- return true
195
+ return { value: {} }
160
196
  }
197
+ const text = readFileSync(file, 'utf8')
161
198
  try {
162
- JSON.parse(readFileSync(file, 'utf8'))
163
- return true
199
+ return { value: JSON.parse(text) }
164
200
  } catch {
165
- return false
201
+ // Not plain JSON, so it is either JSONC or damaged, and those are different answers.
202
+ }
203
+ try {
204
+ return { value: JSON.parse(uncommented(text)), stripped: true }
205
+ } catch {
206
+ return { value: {}, replaced: true }
207
+ }
208
+ }
209
+
210
+ /**
211
+ * JSON with the comments and the trailing commas taken out, which is what JSONC is. A
212
+ * scanner rather than a pattern, because a `//` inside a string value is not a comment and
213
+ * a pattern cannot tell the difference; the same goes for the comma in `"a, }"`, which a
214
+ * pattern for trailing commas would eat.
215
+ */
216
+ function uncommented(text) {
217
+ let out = ''
218
+ let comma = -1
219
+ let i = 0
220
+ while (i < text.length) {
221
+ const here = text[i]
222
+
223
+ if (here === '"') {
224
+ let end = i + 1
225
+ while (end < text.length && text[end] !== '"') {
226
+ end += text[end] === '\\' ? 2 : 1
227
+ }
228
+ out += text.slice(i, end + 1)
229
+ i = end + 1
230
+ continue
231
+ }
232
+ if (here === '/' && text[i + 1] === '/') {
233
+ while (i < text.length && text[i] !== '\n') i++
234
+ continue
235
+ }
236
+ if (here === '/' && text[i + 1] === '*') {
237
+ const closed = text.indexOf('*/', i + 2)
238
+ i = closed === -1 ? text.length : closed + 2
239
+ continue
240
+ }
241
+ if (here === ',') {
242
+ comma = out.length
243
+ } else if ((here === '}' || here === ']') && comma >= 0 && !out.slice(comma + 1).trim()) {
244
+ out = out.slice(0, comma) + out.slice(comma + 1)
245
+ comma = -1
246
+ }
247
+ out += here
248
+ i++
166
249
  }
250
+ return out
167
251
  }
168
252
 
169
253
  /* ────────────────────────────────────────────────────────────────── the drawing ── */
@@ -309,7 +393,7 @@ async function askHost(host, key) {
309
393
  * list and detection only decides which one the cursor starts on.
310
394
  */
311
395
  function found(client) {
312
- return existsSync(dirname(client.file()))
396
+ return existsSync(client.at?.() ?? dirname(client.file()))
313
397
  }
314
398
 
315
399
  function detected() {
@@ -357,8 +441,8 @@ async function install(args) {
357
441
  return
358
442
  }
359
443
 
360
- // Asked before the write, or it inspects the valid JSON we are about to put there.
361
- const replaced = client.merges && !parses(file)
444
+ // Asked before the write, or it inspects the JSON we are about to put there.
445
+ const was = client.merges ? existing(file) : {}
362
446
 
363
447
  const kept = backup(file)
364
448
  mkdirSync(dirname(file), { recursive: true })
@@ -366,8 +450,11 @@ async function install(args) {
366
450
  chmodSync(file, 0o600)
367
451
 
368
452
  console.log(row(client.merges ? 'merged' : 'wrote', tilde(file)))
369
- if (replaced) {
370
- console.log(under(dim('it was not plain JSON, so it was replaced rather than merged')))
453
+ if (was.replaced) {
454
+ console.log(under(dim('it did not parse at all, so it was replaced rather than merged')))
455
+ }
456
+ if (was.stripped) {
457
+ console.log(under(dim('everything in it was kept; its comments were not, because JSON')))
371
458
  }
372
459
  console.log(row(kept.label, dim(kept.detail)))
373
460
  if (client.after) {
@@ -491,7 +578,8 @@ function help() {
491
578
  function list() {
492
579
  console.log()
493
580
  for (const [name, client] of Object.entries(clients)) {
494
- console.log(row(name, `${dim(tilde(client.file()).padEnd(34))} ${client.label}`))
581
+ const said = `${client.label}${found(client) ? ' · on this machine' : ''}`
582
+ console.log(row(name, `${dim(tilde(client.file()).padEnd(34))} ${said}`))
495
583
  }
496
584
  console.log()
497
585
  }