@brainjar/cli 0.6.2 → 0.6.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brainjar/cli",
3
- "version": "0.6.2",
3
+ "version": "0.6.4",
4
4
  "description": "Shape how your AI thinks — composable soul, persona, and rules for AI agents",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/client.ts CHANGED
@@ -153,6 +153,7 @@ export async function createClient(options?: ClientOptions): Promise<BrainjarCli
153
153
  throw new IncurError({ code, message, hint })
154
154
  }
155
155
 
156
+ if (response.status === 204) return undefined as T
156
157
  return response.json() as Promise<T>
157
158
  }
158
159
 
@@ -167,7 +167,7 @@ export const persona = Cli.create('persona', {
167
167
  options: z.object({
168
168
  project: z.boolean().default(false).describe('Show project persona override (if any)'),
169
169
  short: z.boolean().default(false).describe('Print only the active persona name'),
170
- version: z.number().optional().describe('Show a specific version from history'),
170
+ rev: z.number().optional().describe('Show a specific version from history'),
171
171
  }),
172
172
  async run(c) {
173
173
  const api = await getApi()
@@ -178,11 +178,11 @@ export const persona = Cli.create('persona', {
178
178
  return state.persona ?? 'none'
179
179
  }
180
180
 
181
- if (c.options.version) {
181
+ if (c.options.rev) {
182
182
  const name = c.args.name
183
- if (!name) throw createError(ErrorCode.MISSING_ARG, { message: 'Name is required when using --version' })
183
+ if (!name) throw createError(ErrorCode.MISSING_ARG, { message: 'Name is required when using --rev' })
184
184
  const slug = normalizeSlug(name, 'persona name')
185
- const v = await api.get<ApiContentVersion>(`/api/v1/personas/${slug}/versions/${c.options.version}`)
185
+ const v = await api.get<ApiContentVersion>(`/api/v1/personas/${slug}/versions/${c.options.rev}`)
186
186
  return { name: slug, version: v.version, content: v.content, metadata: v.metadata, created_at: v.created_at }
187
187
  }
188
188
 
@@ -307,8 +307,9 @@ export const persona = Cli.create('persona', {
307
307
  const name = normalizeSlug(c.args.name, 'persona name')
308
308
  const api = await getApi()
309
309
 
310
+ let result: { affected_brains?: string[] } | undefined
310
311
  try {
311
- await api.delete(`/api/v1/personas/${name}`)
312
+ result = await api.delete<{ affected_brains?: string[] }>(`/api/v1/personas/${name}`)
312
313
  } catch (e) {
313
314
  if (e instanceof IncurError && e.code === ErrorCode.NOT_FOUND) {
314
315
  throw createError(ErrorCode.PERSONA_NOT_FOUND, { params: [name] })
@@ -320,7 +321,9 @@ export const persona = Cli.create('persona', {
320
321
  const state = await getEffectiveState(api)
321
322
  if (state.persona === name) await sync({ api })
322
323
 
323
- return { deleted: name }
324
+ const out: Record<string, unknown> = { deleted: name }
325
+ if (result?.affected_brains?.length) out.affected_brains = result.affected_brains
326
+ return out
324
327
  },
325
328
  })
326
329
  .command('drop', {
@@ -146,14 +146,14 @@ export const rules = Cli.create('rules', {
146
146
  name: z.string().describe('Rule name to show'),
147
147
  }),
148
148
  options: z.object({
149
- version: z.number().optional().describe('Show a specific version from history'),
149
+ rev: z.number().optional().describe('Show a specific version from history'),
150
150
  }),
151
151
  async run(c) {
152
152
  const name = normalizeSlug(c.args.name, 'rule name')
153
153
  const api = await getApi()
154
154
 
155
- if (c.options.version) {
156
- const v = await api.get<ApiContentVersion>(`/api/v1/rules/${name}/versions/${c.options.version}`)
155
+ if (c.options.rev) {
156
+ const v = await api.get<ApiContentVersion>(`/api/v1/rules/${name}/versions/${c.options.rev}`)
157
157
  const entries = (v.metadata as { entries?: Array<{ sort_key: number; content: string }> })?.entries ?? []
158
158
  const content = entries.map(e => e.content.trim()).join('\n\n')
159
159
  return { name, version: v.version, content, created_at: v.created_at }
@@ -247,8 +247,9 @@ export const rules = Cli.create('rules', {
247
247
  const name = normalizeSlug(c.args.name, 'rule name')
248
248
  const api = await getApi()
249
249
 
250
+ let result: { affected_brains?: string[] } | undefined
250
251
  try {
251
- await api.delete(`/api/v1/rules/${name}`)
252
+ result = await api.delete<{ affected_brains?: string[] }>(`/api/v1/rules/${name}`)
252
253
  } catch (e) {
253
254
  if (e instanceof IncurError && e.code === ErrorCode.NOT_FOUND) {
254
255
  throw createError(ErrorCode.RULE_NOT_FOUND, { params: [name] })
@@ -260,7 +261,9 @@ export const rules = Cli.create('rules', {
260
261
  const state = await getEffectiveState(api)
261
262
  if (state.rules.includes(name)) await sync({ api })
262
263
 
263
- return { deleted: name }
264
+ const out: Record<string, unknown> = { deleted: name }
265
+ if (result?.affected_brains?.length) out.affected_brains = result.affected_brains
266
+ return out
264
267
  },
265
268
  })
266
269
  .command('drop', {
@@ -133,7 +133,7 @@ export const soul = Cli.create('soul', {
133
133
  options: z.object({
134
134
  project: z.boolean().default(false).describe('Show project soul override (if any)'),
135
135
  short: z.boolean().default(false).describe('Print only the active soul name'),
136
- version: z.number().optional().describe('Show a specific version from history'),
136
+ rev: z.number().optional().describe('Show a specific version from history'),
137
137
  }),
138
138
  async run(c) {
139
139
  const api = await getApi()
@@ -144,11 +144,11 @@ export const soul = Cli.create('soul', {
144
144
  return state.soul ?? 'none'
145
145
  }
146
146
 
147
- if (c.options.version) {
147
+ if (c.options.rev) {
148
148
  const name = c.args.name
149
- if (!name) throw createError(ErrorCode.MISSING_ARG, { message: 'Name is required when using --version' })
149
+ if (!name) throw createError(ErrorCode.MISSING_ARG, { message: 'Name is required when using --rev' })
150
150
  const slug = normalizeSlug(name, 'soul name')
151
- const v = await api.get<ApiContentVersion>(`/api/v1/souls/${slug}/versions/${c.options.version}`)
151
+ const v = await api.get<ApiContentVersion>(`/api/v1/souls/${slug}/versions/${c.options.rev}`)
152
152
  return { name: slug, version: v.version, content: v.content, created_at: v.created_at }
153
153
  }
154
154
 
@@ -264,8 +264,9 @@ export const soul = Cli.create('soul', {
264
264
  const name = normalizeSlug(c.args.name, 'soul name')
265
265
  const api = await getApi()
266
266
 
267
+ let result: { affected_brains?: string[] } | undefined
267
268
  try {
268
- await api.delete(`/api/v1/souls/${name}`)
269
+ result = await api.delete<{ affected_brains?: string[] }>(`/api/v1/souls/${name}`)
269
270
  } catch (e) {
270
271
  if (e instanceof IncurError && e.code === ErrorCode.NOT_FOUND) {
271
272
  throw createError(ErrorCode.SOUL_NOT_FOUND, { params: [name] })
@@ -277,7 +278,9 @@ export const soul = Cli.create('soul', {
277
278
  const state = await getEffectiveState(api)
278
279
  if (state.soul === name) await sync({ api })
279
280
 
280
- return { deleted: name }
281
+ const out: Record<string, unknown> = { deleted: name }
282
+ if (result?.affected_brains?.length) out.affected_brains = result.affected_brains
283
+ return out
281
284
  },
282
285
  })
283
286
  .command('drop', {
package/src/daemon.ts CHANGED
@@ -279,6 +279,18 @@ export async function upgradeServer(): Promise<{ version: string; alreadyLatest:
279
279
  const pid = await readPid(localContext(config).pid_file)
280
280
  if (pid !== null && isAlive(pid)) {
281
281
  await stop()
282
+
283
+ // Verify process is actually dead before replacing binary
284
+ const deadline = Date.now() + 3000
285
+ while (Date.now() < deadline && isAlive(pid)) {
286
+ await new Promise(r => setTimeout(r, 100))
287
+ }
288
+ if (isAlive(pid)) {
289
+ throw createError(ErrorCode.SERVER_START_FAILED, {
290
+ message: `Server process (PID ${pid}) is still running. Cannot replace binary.`,
291
+ hint: `Kill it manually: kill -9 ${pid}`,
292
+ })
293
+ }
282
294
  }
283
295
 
284
296
  const versionBase = `${DIST_BASE}/${version}`
@@ -351,7 +363,14 @@ export async function stop(): Promise<{ stopped: boolean }> {
351
363
  return { stopped: false }
352
364
  }
353
365
 
354
- process.kill(pid, 'SIGTERM')
366
+ // Kill entire process group (negative pid) so child processes
367
+ // like embedded postgres are also terminated.
368
+ try {
369
+ process.kill(-pid, 'SIGTERM')
370
+ } catch {
371
+ // Process group doesn't exist — try single process
372
+ try { process.kill(pid, 'SIGTERM') } catch {}
373
+ }
355
374
 
356
375
  // Poll for exit, up to 5s
357
376
  const deadline = Date.now() + 5000
@@ -363,10 +382,15 @@ export async function stop(): Promise<{ stopped: boolean }> {
363
382
  }
364
383
  }
365
384
 
366
- // Force kill
385
+ // Force kill entire process group
367
386
  try {
368
- process.kill(pid, 'SIGKILL')
369
- } catch {}
387
+ process.kill(-pid, 'SIGKILL')
388
+ } catch {
389
+ try { process.kill(pid, 'SIGKILL') } catch {}
390
+ }
391
+
392
+ // Wait briefly for SIGKILL to take effect
393
+ await new Promise(r => setTimeout(r, 500))
370
394
  await rm(pid_file, { force: true })
371
395
  return { stopped: true }
372
396
  }