niahere 0.2.58 → 0.2.59

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 CHANGED
@@ -1,5 +1,9 @@
1
1
  # nia
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/niahere.svg)](https://www.npmjs.com/package/niahere)
4
+ [![npm downloads](https://img.shields.io/npm/dm/niahere.svg)](https://www.npmjs.com/package/niahere)
5
+ [![license](https://img.shields.io/npm/l/niahere.svg)](https://github.com/onlyoneaman/niahere/blob/main/LICENSE)
6
+
3
7
  A personal AI agent you fork and make your own. Small enough to understand, built for one user. Powered by Claude Agent SDK.
4
8
 
5
9
  - npm package: [`niahere`](https://www.npmjs.com/package/niahere)
@@ -33,7 +37,7 @@ nia start # starts daemon + registers OS service
33
37
  - **Telegram** — message your agent from your phone, typing indicator while processing
34
38
  - **Slack** — Socket Mode bot with thread awareness, thinking emoji, watch channels for proactive monitoring
35
39
  - **Terminal chat** — REPL with session resume support
36
- - **Scheduled jobs** — recurring jobs and crons that run Claude and can message you back
40
+ - **Scheduled jobs** — recurring jobs and crons that run Claude and can message you back. Stateful by default (working memory), per-job model routing for cost savings
37
41
  - **Persona system** — customizable identity, soul, owner profile, rules, and memory (preloaded every session)
38
42
  - **Agents** — domain specialists (marketer, senior-dev) via Claude Agent SDK subagents
39
43
  - **Skills** — loads skills from multiple directories, invokable as slash commands
@@ -63,8 +67,8 @@ nia update — update to latest version (auto-backup + resta
63
67
  nia job list — list all jobs
64
68
  nia job show [name] — full details + recent runs
65
69
  nia job status [name] — quick status check
66
- nia job add <n> <s> <p> — add a job (--type, --always, --agent, --stateless, --prompt-file)
67
- nia job update <name> — update a job (--schedule, --prompt, --prompt-file, --type, --always, --agent, --stateless)
70
+ nia job add <n> <s> <p> — add a job (--type, --always, --agent, --model, --stateless, --prompt-file)
71
+ nia job update <name> — update a job (--schedule, --prompt, --prompt-file, --type, --always, --agent, --model, --stateless)
68
72
  nia job remove <name> — delete a job
69
73
  nia job enable / disable <n> — toggle a job
70
74
  nia job run <name> — run a job once
@@ -106,6 +110,8 @@ All config and data lives in `~/.niahere/`:
106
110
  soul.md — how the agent works
107
111
  rules.md — behavioral instructions (loaded every session)
108
112
  memory.md — persistent facts and context (loaded every session)
113
+ jobs/ — per-job working memory and state (auto-created)
114
+ optimizations/ — optimization loop run workspaces
109
115
  images/
110
116
  reference.webp — visual identity reference image
111
117
  profile.webp — profile picture for Telegram/Slack
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "niahere",
3
- "version": "0.2.58",
3
+ "version": "0.2.59",
4
4
  "description": "A personal AI assistant daemon — chat, scheduled jobs, persona system, extensible via skills.",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -43,7 +43,7 @@
43
43
  "license": "MIT",
44
44
  "private": false,
45
45
  "dependencies": {
46
- "@anthropic-ai/claude-agent-sdk": "^0.2.74",
46
+ "@anthropic-ai/claude-agent-sdk": "^0.2.97",
47
47
  "@modelcontextprotocol/sdk": "^1.27.1",
48
48
  "@slack/bolt": "^4.6.0",
49
49
  "cron-parser": "^5.5.0",
@@ -3,9 +3,8 @@ name: beads-tasks
3
3
  description: >
4
4
  Persistent task management via Beads CLI (bd). Use when user mentions tasks, todos, issues, or tracking work.
5
5
  Check `which bd` first — if missing, offer: `npm install -g @beads/bd`.
6
- Prefer setting `BEATS_DIR` (for example `~/.niahere/beads`) and run commands from there:
7
- `cd "$BEATS_DIR" && bd <command>`. Always label: `--label project:<project-name>`.
8
- Run `cd "$BEATS_DIR" && bd help-all` for available commands. Not for ephemeral in-conversation tracking.
6
+ All commands: run from `$BEATS_DIR` (for example `~/.niahere/beads`) and use `bd <command>`. Always label: `--label project:<project-name>`.
7
+ Run `bd help-all` for available commands. Not for ephemeral in-conversation tracking.
9
8
  ---
10
9
 
11
10
  ## Overview
@@ -19,7 +18,63 @@ Global task manager powered by [Beads](https://github.com/steveyegge/beads). Sto
19
18
  2. Ensure `~/.niahere/beads/.beads` exists — `bd init` if not.
20
19
  3. Set `BEATS_DIR` to your Beads workspace (for example `~/.niahere/beads`).
21
20
  4. All commands: `cd "$BEATS_DIR" && bd <command>`.
22
- 5. Always label with `--label project:<project-name>`.
21
+ 5. Always label with `--label project:<name>`.
22
+ 6. Run `cd "$BEATS_DIR" && bd help-all` for available commands.
23
+
24
+ ## Core Commands
25
+
26
+ ### Creating tasks
27
+
28
+ ```bash
29
+ # Basic
30
+ bd create --title "Fix auth token refresh" --priority P2 --type bug
31
+
32
+ # With parent (subtask)
33
+ bd create --title "Extract shared logic" --priority P2 --type task --parent <parent-id>
34
+
35
+ # With description — ALWAYS add context: what's broken, links, references
36
+ bd create --title "Chat fails on long docs" --type bug --description "Fails on docs >500 pages. Ref: https://..."
37
+
38
+ # Epic (container for related tasks)
39
+ bd create --title "API performance improvements" --type epic --priority P2
40
+ ```
41
+
42
+ ### Updating tasks
43
+
44
+ `bd update` is the workhorse — use it for reparenting, reprioritizing, retyping, renaming:
45
+
46
+ ```bash
47
+ bd update <id> --parent <new-parent-id> # Reparent / move under epic
48
+ bd update <id> --parent "" # Remove parent (make top-level)
49
+ bd update <id> --priority P1 # Change priority
50
+ bd update <id> --type bug # Change type
51
+ bd update <id> --title "Better title" # Rename
52
+ bd update <id> --status in_progress # Start work
53
+ bd update <id> --description "..." # Add/replace description
54
+ bd update <id> --add-label personal # Add label
55
+ bd update <id> --set-labels bug,urgent # Replace all labels
56
+ ```
57
+
58
+ Chain multiple updates: `bd update <id> --priority P1 --type bug --parent <parent-id>`
59
+
60
+ ### Viewing tasks
61
+
62
+ ```bash
63
+ bd list # Open tasks (tree view)
64
+ bd list --all # Include closed/deferred tasks
65
+ bd list --label project:<name> # Filter by project
66
+ bd show <id> # Full details of a task
67
+ bd children <id> # List children of a parent
68
+ ```
69
+
70
+ ### Closing tasks
71
+
72
+ ```bash
73
+ bd close <id> # Close a task
74
+ bd reopen <id> # Reopen if closed prematurely
75
+ ```
76
+
77
+ **Warning:** Closing a parent does NOT close or reparent its children. If a parent epic is done but children remain open, reparent them first or they become orphaned top-level items.
23
78
 
24
79
  ## Decision Points
25
80
 
@@ -29,22 +84,45 @@ Global task manager powered by [Beads](https://github.com/steveyegge/beads). Sto
29
84
  - User says "done with X" / "finished" → `bd close <id>`
30
85
  - User wants to see cross-project work → `bd list` (no project filter)
31
86
  - User wants project-specific view → `bd list --label project:<name>`
32
- - User asks for task in-progress state → use task status:
33
- - `bd update <task-id> --status in_progress`
34
- - `bd set-state ... state=...` is for operational metadata only and does not
35
- change list-visible task status.
36
87
  - bd not installed → offer install, don't silently fail
37
88
  - Ephemeral/conversation-only tracking → use conversation context, not beads
89
+ - `bd set-state ... state=...` is for operational metadata only; it does not change the task status shown in list.
90
+
91
+ ## Hierarchy & Organization
92
+
93
+ ### When to use parent-child vs labels
94
+
95
+ - **Parent-child** (`--parent`): for structural grouping — epics containing subtasks, features broken into steps.
96
+ - **Labels** (`--add-label`): for cross-cutting tags — `personal`, `urgent`, `project:<name>`. A task can have multiple labels but only one parent.
97
+
98
+ ### Epic patterns
99
+
100
+ - Use `--type epic` for containers that group related work.
101
+ - Epics can nest: epic > sub-epic > tasks.
102
+ - Keep epic titles broad ("API improvements"), subtask titles specific ("Reduce /search latency from 2s to 200ms").
103
+
104
+ ### Cleanup & auditing
105
+
106
+ Periodically review with `bd list` and look for:
107
+ - **Orphaned tasks** — top-level items that should be under an epic.
108
+ - **Similar ungrouped tasks** — multiple tasks on the same topic that should share a parent.
109
+ - **Misplaced tasks** — bugs under improvement epics or vice versa.
110
+ - **Stale tasks** — open tasks that are actually done or no longer relevant.
111
+
112
+ When reorganizing, reparent with `bd update <id> --parent <new-parent>` — don't delete and recreate.
38
113
 
39
114
  ## Conventions
40
115
 
41
- - Titles: descriptive, actionable (e.g. "Fix auth token refresh in niahere")
42
- - Labels: `project:<name>`, `bug`, `feature`, `chore`, `urgent`
43
- - Priority: default P2 unless user specifies urgency
44
- - Status flow: `open``in_progress` `closed`
116
+ - **Titles:** descriptive, actionable (e.g. "Fix auth token refresh in niahere")
117
+ - **Descriptions:** always include context what's broken, why it matters, links to references (Canny, threads, logs). Future you needs enough to start working without asking questions.
118
+ - **Types:** `epic`, `bug`, `feature`, `task`, `chore`, `decision`
119
+ - **Priority:** P0 (critical)P4 (nice-to-have). Default P2 unless user specifies.
120
+ - **Labels:** `project:<name>`, `personal`, `bug`, `feature`, `chore`, `urgent`
121
+ - **Status flow:** `open` → `in_progress` → `closed`
45
122
 
46
123
  ## Validation
47
124
 
48
- - `cd "$BEATS_DIR" && bd list` returns results after creating a task
125
+ - `bd list` returns results after creating a task
49
126
  - Labels appear correctly in list output
127
+ - Parent-child relationships show as indented tree in `bd list`
50
128
  - Dependencies show in `bd dep tree`
@@ -382,6 +382,7 @@ export async function createChatEngine(
382
382
  duration_ms: msg.duration_ms,
383
383
  duration_api_ms: msg.duration_api_ms,
384
384
  stop_reason: msg.stop_reason,
385
+ terminal_reason: msg.terminal_reason,
385
386
  session_id: msg.session_id,
386
387
  subtype: msg.subtype,
387
388
  usage: msg.usage,
package/src/cli/index.ts CHANGED
@@ -1,6 +1,12 @@
1
1
  #!/usr/bin/env bun
2
2
  import { existsSync, mkdirSync } from "fs";
3
- import { isRunning, readPid, runDaemon, startDaemon, stopDaemon } from "../core/daemon";
3
+ import {
4
+ isRunning,
5
+ readPid,
6
+ runDaemon,
7
+ startDaemon,
8
+ stopDaemon,
9
+ } from "../core/daemon";
4
10
  import { getConfig } from "../utils/config";
5
11
  import { localTime } from "../utils/time";
6
12
  import { startRepl } from "../chat/repl";
@@ -29,7 +35,12 @@ try {
29
35
  const command = process.argv[2];
30
36
 
31
37
  // Ensure ~/.niahere/ exists for commands that need it
32
- if (command && !["init", "help", "version", "-v", "--version", "-h", "--help"].includes(command)) {
38
+ if (
39
+ command &&
40
+ !["init", "help", "version", "-v", "--version", "-h", "--help"].includes(
41
+ command,
42
+ )
43
+ ) {
33
44
  mkdirSync(getNiaHome(), { recursive: true });
34
45
  }
35
46
 
@@ -45,7 +56,8 @@ async function awaitStartup(timeout = 60_000): Promise<void> {
45
56
  const expecting = new Set<string>();
46
57
  if (config.channels.enabled) {
47
58
  if (config.channels.telegram.bot_token) expecting.add("telegram");
48
- if (config.channels.slack.bot_token && config.channels.slack.app_token) expecting.add("slack");
59
+ if (config.channels.slack.bot_token && config.channels.slack.app_token)
60
+ expecting.add("slack");
49
61
  }
50
62
  expecting.add("scheduler");
51
63
 
@@ -54,13 +66,19 @@ async function awaitStartup(timeout = 60_000): Promise<void> {
54
66
  const { readFileSync } = await import("fs");
55
67
  const ready = new Set<string>();
56
68
  let logOffset = 0;
57
- try { logOffset = readFileSync(daemonLog, "utf8").length; } catch {}
69
+ try {
70
+ logOffset = readFileSync(daemonLog, "utf8").length;
71
+ } catch {}
58
72
 
59
73
  const startTime = Date.now();
60
74
  while (ready.size < expecting.size && Date.now() - startTime < timeout) {
61
75
  await new Promise((r) => setTimeout(r, 500));
62
76
  let content = "";
63
- try { content = readFileSync(daemonLog, "utf8").slice(logOffset); } catch { continue; }
77
+ try {
78
+ content = readFileSync(daemonLog, "utf8").slice(logOffset);
79
+ } catch {
80
+ continue;
81
+ }
64
82
 
65
83
  for (const name of expecting) {
66
84
  if (ready.has(name)) continue;
@@ -125,7 +143,8 @@ switch (command) {
125
143
  }
126
144
 
127
145
  case "restart": {
128
- const { isServiceInstalled, restartService } = await import("../commands/service");
146
+ const { isServiceInstalled, restartService } =
147
+ await import("../commands/service");
129
148
  if (isServiceInstalled()) {
130
149
  // Service-aware: unload (stops KeepAlive respawn), kill, then reload
131
150
  await restartService();
@@ -134,7 +153,9 @@ switch (command) {
134
153
  startDaemon();
135
154
  }
136
155
  const restartPid = readPid();
137
- console.log(`nia restarting${restartPid ? ` (pid: ${restartPid})` : ""}...`);
156
+ console.log(
157
+ `nia restarting${restartPid ? ` (pid: ${restartPid})` : ""}...`,
158
+ );
138
159
  await awaitStartup();
139
160
  console.log("nia restarted");
140
161
  break;
@@ -145,7 +166,12 @@ switch (command) {
145
166
  if (prompt) {
146
167
  const { createChatEngine } = await import("../chat/engine");
147
168
  const { getMcpServers } = await import("../mcp");
148
- const { DIM, RESET: RST, CLEAR_LINE, SPINNER: FRAMES } = await import("../utils/cli");
169
+ const {
170
+ DIM,
171
+ RESET: RST,
172
+ CLEAR_LINE,
173
+ SPINNER: FRAMES,
174
+ } = await import("../utils/cli");
149
175
  let frame = 0;
150
176
  let statusText = "thinking";
151
177
  let spinTimer: ReturnType<typeof setInterval> | null = null;
@@ -153,31 +179,47 @@ switch (command) {
153
179
  let streaming = false;
154
180
 
155
181
  const renderSpinner = () => {
156
- process.stderr.write(`${CLEAR_LINE}${DIM} ${FRAMES[frame]} ${statusText}${RST}`);
182
+ process.stderr.write(
183
+ `${CLEAR_LINE}${DIM} ${FRAMES[frame]} ${statusText}${RST}`,
184
+ );
157
185
  frame = (frame + 1) % FRAMES.length;
158
186
  };
159
187
 
160
188
  await withDb(async () => {
161
- const engine = await createChatEngine({ room: "cli-run", channel: "terminal", resume: false, mcpServers: getMcpServers() });
189
+ const engine = await createChatEngine({
190
+ room: "cli-run",
191
+ channel: "terminal",
192
+ resume: false,
193
+ mcpServers: getMcpServers(),
194
+ });
162
195
  spinTimer = setInterval(renderSpinner, 80);
163
196
  renderSpinner();
164
197
 
165
198
  const { result, costUsd, turns } = await engine.send(prompt, {
166
199
  onStream(textSoFar) {
167
200
  if (!streaming) {
168
- if (spinTimer) { clearInterval(spinTimer); spinTimer = null; }
201
+ if (spinTimer) {
202
+ clearInterval(spinTimer);
203
+ spinTimer = null;
204
+ }
169
205
  process.stderr.write("\x1b[2K\r");
170
206
  streaming = true;
171
207
  }
172
208
  const chunk = textSoFar.slice(streamedLen);
173
- if (chunk) { process.stdout.write(chunk); streamedLen = textSoFar.length; }
209
+ if (chunk) {
210
+ process.stdout.write(chunk);
211
+ streamedLen = textSoFar.length;
212
+ }
174
213
  },
175
214
  onActivity(text) {
176
215
  if (!streaming) statusText = text;
177
216
  },
178
217
  });
179
218
 
180
- if (spinTimer) { clearInterval(spinTimer); spinTimer = null; }
219
+ if (spinTimer) {
220
+ clearInterval(spinTimer);
221
+ spinTimer = null;
222
+ }
181
223
 
182
224
  if (!streaming && result.trim()) {
183
225
  process.stderr.write("\x1b[2K\r");
@@ -190,13 +232,15 @@ switch (command) {
190
232
  }
191
233
 
192
234
  const costStr = costUsd > 0 ? `$${costUsd.toFixed(4)}` : "";
193
- const turnsStr = turns > 0 ? `${turns} turn${turns !== 1 ? "s" : ""}` : "";
235
+ const turnsStr =
236
+ turns > 0 ? `${turns} turn${turns !== 1 ? "s" : ""}` : "";
194
237
  const meta = [costStr, turnsStr].filter(Boolean).join(" · ");
195
238
  if (meta) process.stderr.write(`\n${DIM}${meta}${RST}`);
196
239
  process.stdout.write("\n");
197
240
 
198
241
  engine.close();
199
242
  });
243
+ process.exit(0);
200
244
  } else {
201
245
  await runDaemon();
202
246
  }
@@ -235,8 +279,13 @@ switch (command) {
235
279
  const time = localTime(new Date(m.createdAt));
236
280
  const prefix = m.sender === "user" ? "you" : m.sender;
237
281
  const roomTag = room ? "" : `[${m.room}] `;
238
- const snippet = m.content.length > 120 ? m.content.slice(0, 120) + "..." : m.content;
239
- console.log(` ${roomTag}${time} ${prefix} > ${snippet.replace(/\n/g, " ")}`);
282
+ const snippet =
283
+ m.content.length > 120
284
+ ? m.content.slice(0, 120) + "..."
285
+ : m.content;
286
+ console.log(
287
+ ` ${roomTag}${time} ${prefix} > ${snippet.replace(/\n/g, " ")}`,
288
+ );
240
289
  }
241
290
  }
242
291
  });
@@ -253,16 +302,25 @@ switch (command) {
253
302
  const follow = logArgs.includes("-f") || logArgs.includes("--follow");
254
303
  // --channel <name> filters logs by channel/component via grep
255
304
  const chIdx = logArgs.indexOf("--channel");
256
- const channelFilter = chIdx !== -1 && logArgs[chIdx + 1] ? logArgs[chIdx + 1] : null;
305
+ const channelFilter =
306
+ chIdx !== -1 && logArgs[chIdx + 1] ? logArgs[chIdx + 1] : null;
257
307
 
258
308
  if (channelFilter) {
259
309
  // Pipe through grep to filter by channel name in structured logs
260
- const tailArgs = follow ? ["tail", "-f", daemonLog] : ["tail", "-200", daemonLog];
261
- const tail = Bun.spawn(tailArgs, { stdio: ["ignore", "pipe", "inherit"] });
262
- const grep = Bun.spawn(["grep", "-i", channelFilter], { stdio: [tail.stdout, "inherit", "inherit"] });
310
+ const tailArgs = follow
311
+ ? ["tail", "-f", daemonLog]
312
+ : ["tail", "-200", daemonLog];
313
+ const tail = Bun.spawn(tailArgs, {
314
+ stdio: ["ignore", "pipe", "inherit"],
315
+ });
316
+ const grep = Bun.spawn(["grep", "-i", channelFilter], {
317
+ stdio: [tail.stdout, "inherit", "inherit"],
318
+ });
263
319
  await grep.exited;
264
320
  } else {
265
- const args = follow ? ["tail", "-f", daemonLog] : ["tail", "-50", daemonLog];
321
+ const args = follow
322
+ ? ["tail", "-f", daemonLog]
323
+ : ["tail", "-50", daemonLog];
266
324
  const proc = Bun.spawn(args, { stdio: ["ignore", "inherit", "inherit"] });
267
325
  await proc.exited;
268
326
  }
@@ -276,13 +334,15 @@ switch (command) {
276
334
 
277
335
  case "chat": {
278
336
  const chatArgs = process.argv.slice(3);
279
- const mode = (chatArgs.includes("--continue") || chatArgs.includes("-c"))
280
- ? "continue" as const
281
- : (chatArgs.includes("--resume") || chatArgs.includes("-r"))
282
- ? "pick" as const
283
- : "new" as const;
337
+ const mode =
338
+ chatArgs.includes("--continue") || chatArgs.includes("-c")
339
+ ? ("continue" as const)
340
+ : chatArgs.includes("--resume") || chatArgs.includes("-r")
341
+ ? ("pick" as const)
342
+ : ("new" as const);
284
343
  const chIdx = chatArgs.indexOf("--channel");
285
- const simChannel = chIdx !== -1 && chatArgs[chIdx + 1] ? chatArgs[chIdx + 1] : undefined;
344
+ const simChannel =
345
+ chIdx !== -1 && chatArgs[chIdx + 1] ? chatArgs[chIdx + 1] : undefined;
286
346
  await startRepl(mode, simChannel);
287
347
  break;
288
348
  }
@@ -300,7 +360,9 @@ switch (command) {
300
360
  skills = skills.filter((s) => s.source === filter);
301
361
  }
302
362
  if (skills.length === 0) {
303
- console.log(filter ? `No skills found in "${filter}".` : "No skills found.");
363
+ console.log(
364
+ filter ? `No skills found in "${filter}".` : "No skills found.",
365
+ );
304
366
  } else {
305
367
  for (const s of skills) {
306
368
  const tag = filter ? "" : ` [${s.source}]`;
@@ -354,8 +416,12 @@ switch (command) {
354
416
  const parts = configKey.split(".");
355
417
  let val: unknown = raw;
356
418
  for (const p of parts) {
357
- if (val && typeof val === "object") val = (val as Record<string, unknown>)[p];
358
- else { val = undefined; break; }
419
+ if (val && typeof val === "object")
420
+ val = (val as Record<string, unknown>)[p];
421
+ else {
422
+ val = undefined;
423
+ break;
424
+ }
359
425
  }
360
426
  if (val === undefined) {
361
427
  console.log(`${configKey}: (not set)`);
@@ -389,7 +455,9 @@ switch (command) {
389
455
  process.kill(pid, "SIGHUP");
390
456
  console.log(`channels ${enabled ? "enabled" : "disabled"}`);
391
457
  } else {
392
- console.log(`channels ${enabled ? "enabled" : "disabled"} — start nia to apply`);
458
+ console.log(
459
+ `channels ${enabled ? "enabled" : "disabled"} — start nia to apply`,
460
+ );
393
461
  }
394
462
  } else {
395
463
  console.log(`channels: ${getConfig().channels.enabled ? "on" : "off"}`);
@@ -404,8 +472,11 @@ switch (command) {
404
472
  }
405
473
 
406
474
  case "test": {
407
- const verbose = process.argv.includes("-v") || process.argv.includes("--verbose");
408
- const extraArgs = process.argv.slice(3).filter((a) => a !== "-v" && a !== "--verbose");
475
+ const verbose =
476
+ process.argv.includes("-v") || process.argv.includes("--verbose");
477
+ const extraArgs = process.argv
478
+ .slice(3)
479
+ .filter((a) => a !== "-v" && a !== "--verbose");
409
480
  const proc = Bun.spawn(["bun", "test", ...extraArgs], {
410
481
  stdio: ["ignore", "pipe", "pipe"],
411
482
  cwd: import.meta.dir + "/../..",
@@ -423,7 +494,12 @@ switch (command) {
423
494
  process.stdout.write(output);
424
495
  } else {
425
496
  for (const line of output.split("\n")) {
426
- if (/^\s*\d+ pass/.test(line) || /^\s*\d+ fail/.test(line) || /^Ran \d+ tests/.test(line) || /expect\(\) calls/.test(line)) {
497
+ if (
498
+ /^\s*\d+ pass/.test(line) ||
499
+ /^\s*\d+ fail/.test(line) ||
500
+ /^Ran \d+ tests/.test(line) ||
501
+ /expect\(\) calls/.test(line)
502
+ ) {
427
503
  console.log(line);
428
504
  } else if (/^✗|FAIL|error:/i.test(line.trim())) {
429
505
  console.log(line);
@@ -460,13 +536,18 @@ switch (command) {
460
536
  console.log(`⚠ backup skipped: ${errMsg(err)}`);
461
537
  }
462
538
  console.log("Updating...");
463
- const install = Bun.spawn(["npm", "i", "-g", "niahere@latest"], { stdio: ["ignore", "inherit", "inherit"] });
539
+ const install = Bun.spawn(["npm", "i", "-g", "niahere@latest"], {
540
+ stdio: ["ignore", "inherit", "inherit"],
541
+ });
464
542
  const installExit = await install.exited;
465
543
  if (installExit !== 0) {
466
544
  fail("Update failed.");
467
545
  }
468
546
  // Get new version
469
- const check = Bun.spawn(["npm", "view", "niahere", "version"], { stdout: "pipe", stderr: "pipe" });
547
+ const check = Bun.spawn(["npm", "view", "niahere", "version"], {
548
+ stdout: "pipe",
549
+ stderr: "pipe",
550
+ });
470
551
  const newVersion = (await new Response(check.stdout).text()).trim();
471
552
  await check.exited;
472
553
  if (newVersion === currentVersion) {
@@ -475,7 +556,8 @@ switch (command) {
475
556
  console.log(`Updated: v${currentVersion} → v${newVersion}`);
476
557
  if (isRunning()) {
477
558
  console.log("Restarting daemon...");
478
- const { isServiceInstalled, restartService } = await import("../commands/service");
559
+ const { isServiceInstalled, restartService } =
560
+ await import("../commands/service");
479
561
  if (isServiceInstalled()) {
480
562
  await restartService();
481
563
  } else {
@@ -540,7 +622,11 @@ System:
540
622
 
541
623
  console.log(HELP);
542
624
  // Unknown command → exit 1, help/no command → exit 0
543
- const isHelp = !command || command === "help" || command === "--help" || command === "-h";
625
+ const isHelp =
626
+ !command ||
627
+ command === "help" ||
628
+ command === "--help" ||
629
+ command === "-h";
544
630
  if (!isHelp) console.error(`\nUnknown command: ${command}`);
545
631
  process.exit(isHelp ? 0 : 1);
546
632
  }