niahere 0.2.5 → 0.2.8

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
@@ -67,8 +67,8 @@ All config and data lives in `~/.niahere/`:
67
67
  soul.md — how the agent works
68
68
  memory.md — persistent learnings (read/written on demand, not loaded into context)
69
69
  images/
70
- reference.png — visual identity reference image
71
- profile.png — profile picture for Telegram/Slack
70
+ reference.webp — visual identity reference image
71
+ profile.webp — profile picture for Telegram/Slack
72
72
  tmp/
73
73
  nia.pid, daemon.log, cron-state.json, cron-audit.jsonl
74
74
  ```
package/bin/nia CHANGED
@@ -1,17 +1,26 @@
1
1
  #!/bin/sh
2
- if ! command -v bun >/dev/null 2>&1; then
2
+
3
+ # Check if bun is available (including ~/.bun/bin which may not be in PATH yet)
4
+ BUN_CMD=""
5
+ if command -v bun >/dev/null 2>&1; then
6
+ BUN_CMD="bun"
7
+ elif [ -x "$HOME/.bun/bin/bun" ]; then
8
+ BUN_CMD="$HOME/.bun/bin/bun"
9
+ fi
10
+
11
+ if [ -z "$BUN_CMD" ]; then
3
12
  echo "niahere requires Bun runtime."
4
13
  printf "Install Bun now? (y/n) "
5
14
  read -r answer
6
15
  if [ "$answer" = "y" ] || [ "$answer" = "Y" ]; then
7
16
  curl -fsSL https://bun.sh/install | bash
8
- export BUN_INSTALL="$HOME/.bun"
9
- export PATH="$BUN_INSTALL/bin:$PATH"
10
- if ! command -v bun >/dev/null 2>&1; then
17
+ BUN_CMD="$HOME/.bun/bin/bun"
18
+ if [ ! -x "$BUN_CMD" ]; then
11
19
  echo "Bun install failed. Install manually: curl -fsSL https://bun.sh/install | bash"
12
20
  exit 1
13
21
  fi
14
22
  echo "Bun installed. Continuing..."
23
+ echo ""
15
24
  else
16
25
  echo ""
17
26
  echo "Install manually: curl -fsSL https://bun.sh/install | bash"
@@ -20,14 +29,12 @@ if ! command -v bun >/dev/null 2>&1; then
20
29
  fi
21
30
  fi
22
31
 
23
- # Resolve the real path of this script (follows all symlinks)
24
- REAL="$(realpath "$0" 2>/dev/null)" || REAL="$(perl -MCwd -e 'print Cwd::realpath($ARGV[0])' "$0" 2>/dev/null)" || REAL="$0"
25
- # Go up from bin/ to package root
32
+ # Resolve the real path of this script (follows symlinks)
33
+ REAL="$(realpath "$0" 2>/dev/null)" || REAL="$0"
26
34
  PACKAGE_DIR="$(dirname "$REAL")/.."
27
35
  ENTRY="$PACKAGE_DIR/src/cli/index.ts"
28
36
 
29
37
  if [ ! -f "$ENTRY" ]; then
30
- # Fallback: try npm global root
31
38
  ENTRY="$(npm root -g 2>/dev/null)/niahere/src/cli/index.ts"
32
39
  fi
33
40
 
@@ -36,4 +43,4 @@ if [ ! -f "$ENTRY" ]; then
36
43
  exit 1
37
44
  fi
38
45
 
39
- exec bun "$ENTRY" "$@"
46
+ exec "$BUN_CMD" "$ENTRY" "$@"
@@ -31,8 +31,8 @@ You're curious. You like understanding *why* things work, not just *that* they w
31
31
 
32
32
  To generate or update your visual identity, use the `nia-image` skill.
33
33
 
34
- - **Profile picture**: `~/.niahere/images/profile.png`
35
- - **Reference image**: `~/.niahere/images/reference.png`
34
+ - **Profile picture**: `~/.niahere/images/profile.webp`
35
+ - **Reference image**: `~/.niahere/images/reference.webp`
36
36
 
37
37
  ## What You Don't Do
38
38
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "niahere",
3
- "version": "0.2.5",
3
+ "version": "0.2.8",
4
4
  "description": "A personal AI assistant daemon — scheduled jobs, chat across Telegram and Slack, persona system, and visual identity.",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -21,13 +21,13 @@ Generate photorealistic images of Nia with consistent identity across different
21
21
  ## Assets
22
22
 
23
23
  The script looks for references in this order:
24
- 1. `~/.niahere/images/reference.png` — user's custom reference (takes priority)
24
+ 1. `~/.niahere/images/reference.webp` — user's custom reference (takes priority)
25
25
  2. `assets/nia-reference.webp` — default shipped with niahere
26
26
 
27
27
  | Location | Purpose |
28
28
  |----------|---------|
29
- | `~/.niahere/images/reference.png` | User's reference image |
30
- | `~/.niahere/images/profile.png` | User's profile picture (for Telegram/Slack) |
29
+ | `~/.niahere/images/reference.webp` | User's reference image |
30
+ | `~/.niahere/images/profile.webp` | User's profile picture (for Telegram/Slack) |
31
31
  | `~/.niahere/images/` | Output directory for new generations |
32
32
  | `assets/nia-reference.webp` | Default reference (fallback) |
33
33
  | `assets/nia-profile.webp` | Default profile picture (fallback) |
@@ -24,7 +24,7 @@ NIA_CONFIG = NIA_HOME / "config.yaml"
24
24
  DEFAULT_MODEL = "gemini-3.1-flash-image-preview"
25
25
  PRO_MODEL = "gemini-3-pro-image-preview"
26
26
  BASIC_MODEL = "gemini-2.5-flash-image"
27
- USER_REFERENCE = str(NIA_HOME / "images" / "reference.png")
27
+ USER_REFERENCE = str(NIA_HOME / "images" / "reference.webp")
28
28
  DEFAULT_REFERENCE = str(PROJECT_ROOT / "assets" / "nia-reference.webp")
29
29
  DEFAULT_OUTPUT = str(NIA_HOME / "images")
30
30
  DEFAULT_PROMPT = (
@@ -194,7 +194,7 @@ def main() -> None:
194
194
  f"or add gemini_api_key to {NIA_CONFIG}."
195
195
  )
196
196
 
197
- # Resolve reference image: user's ~/.niahere/images/reference.png > skill default > none
197
+ # Resolve reference image: user's ~/.niahere/images/reference.webp > skill default > none
198
198
  ref_path: str | None = None
199
199
  if not args.no_reference:
200
200
  if args.reference:
@@ -1,5 +1,4 @@
1
1
  import { getConfig, updateRawConfig } from "../utils/config";
2
- import { withDb } from "../db/connection";
3
2
  import { getPaths } from "../utils/paths";
4
3
  import { errMsg } from "../utils/errors";
5
4
  import { fail } from "../utils/cli";
@@ -21,10 +20,8 @@ export async function sendCommand(): Promise<void> {
21
20
  const { sendMessage } = await import("../mcp/tools");
22
21
 
23
22
  try {
24
- await withDb(async () => {
25
- const result = await sendMessage(message, channel);
26
- console.log(result);
27
- });
23
+ const result = await sendMessage(message, channel);
24
+ console.log(result);
28
25
  } catch (err) {
29
26
  fail(`Failed to send: ${errMsg(err)}`);
30
27
  }
package/src/cli/status.ts CHANGED
@@ -228,7 +228,10 @@ export async function statusCommand(argv: string[] = []): Promise<void> {
228
228
  a.name.localeCompare(b.name),
229
229
  );
230
230
 
231
- for (const job of sortedJobs) {
231
+ // Hide completed one-shot jobs
232
+ const visibleJobs = sortedJobs.filter((j) => !(j.scheduleType === "once" && !j.enabled && j.lastRunAt));
233
+
234
+ for (const job of visibleJobs) {
232
235
  const stateInfo = state[job.name];
233
236
  const status = stateInfo?.status ?? (job.lastRunAt ? "ok" : "never");
234
237
  const lastRun = stateInfo?.lastRun ?? job.lastRunAt ?? null;
@@ -291,8 +294,10 @@ export async function statusCommand(argv: string[] = []): Promise<void> {
291
294
  const icon = info.status === "ok" ? "\u2713" : info.status === "error" ? "\u2717" : "\u2217";
292
295
  console.log(` ${icon} ${name}: ${info.status} (last: ${last}, ${info.duration_ms}ms)`);
293
296
  }
297
+ } else if (dbError) {
298
+ console.log(`\nJobs: database unavailable (${errMsg(dbError)})`);
294
299
  } else {
295
- console.log("\nJobs: unavailable (no job state in file)");
300
+ console.log("\nJobs: none");
296
301
  }
297
302
  }
298
303
 
@@ -52,8 +52,12 @@ async function offerBeadsShellExport(rl: readline.Interface, beadsDir: string):
52
52
  if (answer.toLowerCase() !== "y") return;
53
53
 
54
54
  appendFileSync(rcFile, `\n# Beads global task DB\n${exportLine}\n`);
55
+ // Apply to current process so it takes effect immediately
56
+ const parts = exportLine.replace("$HOME", homedir()).split("=");
57
+ if (parts.length === 2) {
58
+ process.env[parts[0].replace("export ", "")] = parts[1].replace(/"/g, "");
59
+ }
55
60
  console.log(` \u2713 added BEADS_DIR to ${rcFile.replace(homedir(), "~")}`);
56
- console.log(` Run 'source ${rcFile.replace(homedir(), "~")}' or open a new terminal.`);
57
61
  }
58
62
 
59
63
  function loadTemplate(name: string, vars: Record<string, string> = {}): string {
@@ -144,6 +148,7 @@ export async function runInit(): Promise<void> {
144
148
  if (telegramToken) {
145
149
  const openInput = await ask(rl, "Allow anyone to message? (y/n)", "n");
146
150
  telegramOpen = openInput.toLowerCase() === "y";
151
+ console.log(" Tip: Send a message to your bot to activate outbound messaging.");
147
152
  }
148
153
  }
149
154
  }
@@ -181,7 +186,8 @@ export async function runInit(): Promise<void> {
181
186
  const createUrl = `https://api.slack.com/apps?new_app=1&manifest_json=${encodeURIComponent(manifest)}`;
182
187
 
183
188
  console.log("\n Opening Slack app creation page...");
184
- Bun.spawn(["open", createUrl], { stdio: ["ignore", "ignore", "ignore"] });
189
+ const openCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
190
+ Bun.spawn([openCmd, createUrl], { stdio: ["ignore", "ignore", "ignore"] });
185
191
  console.log(" 1. Click 'Create' to create the app");
186
192
  console.log(" 2. Go to 'OAuth & Permissions' → Install to workspace → copy Bot Token (xoxb-...)");
187
193
  console.log(" 3. Go to 'Basic Information' → 'App-Level Tokens' → create one with connections:write → copy (xapp-...)\n");
@@ -299,7 +305,7 @@ export async function runInit(): Promise<void> {
299
305
  // Visual identity
300
306
  const imagesDir = `${home}/images`;
301
307
  mkdirSync(imagesDir, { recursive: true });
302
- const hasUserReference = existsSync(`${imagesDir}/reference.png`);
308
+ const hasUserReference = existsSync(`${imagesDir}/reference.webp`);
303
309
  const hasDefaultReference = existsSync(`${SKILL_ASSETS_DIR}/nia-reference.webp`);
304
310
 
305
311
  if (geminiApiKey && !hasUserReference) {
@@ -317,23 +323,23 @@ export async function runInit(): Promise<void> {
317
323
  "--api-key", geminiApiKey,
318
324
  "--aspect-ratio", "9:16",
319
325
  "--prompt", prompt,
320
- "--output", `${imagesDir}/reference.png`,
326
+ "--output", `${imagesDir}/reference.webp`,
321
327
  ], { stdout: "pipe", stderr: "pipe" });
322
328
  const exitCode = await proc.exited;
323
329
  if (exitCode === 0) {
324
- console.log(` \u2713 generated reference image at ${imagesDir}/reference.png`);
330
+ console.log(` \u2713 generated reference image at ${imagesDir}/reference.webp`);
325
331
  // Also generate a profile picture
326
332
  console.log(" Generating profile picture...");
327
333
  const profileProc = Bun.spawn([
328
334
  "python3", GENERATE_SCRIPT,
329
- "--reference", `${imagesDir}/reference.png`,
335
+ "--reference", `${imagesDir}/reference.webp`,
330
336
  "--api-key", geminiApiKey,
331
337
  "--aspect-ratio", "1:1",
332
338
  "--prompt", `Photorealistic close-up portrait of the same person from the reference. Warm slight smile, direct eye contact, soft ambient side lighting, creamy bokeh background, 85mm f/1.8, shallow depth of field. Same face, same style, natural skin texture, DSLR quality, hyper-detailed.`,
333
- "--output", `${imagesDir}/profile.png`,
339
+ "--output", `${imagesDir}/profile.webp`,
334
340
  ], { stdout: "pipe", stderr: "pipe" });
335
341
  if (await profileProc.exited === 0) {
336
- console.log(` \u2713 generated profile picture at ${imagesDir}/profile.png`);
342
+ console.log(` \u2713 generated profile picture at ${imagesDir}/profile.webp`);
337
343
  }
338
344
  } else {
339
345
  const stderr = await new Response(proc.stderr).text();
@@ -342,10 +348,10 @@ export async function runInit(): Promise<void> {
342
348
  } else if (hasDefaultReference) {
343
349
  // No description — copy defaults
344
350
  const { copyFileSync } = await import("fs");
345
- copyFileSync(`${SKILL_ASSETS_DIR}/nia-reference.webp`, `${imagesDir}/reference.png`);
351
+ copyFileSync(`${SKILL_ASSETS_DIR}/nia-reference.webp`, `${imagesDir}/reference.webp`);
346
352
  console.log(` \u2713 copied default reference image`);
347
353
  if (existsSync(`${SKILL_ASSETS_DIR}/nia-profile.webp`)) {
348
- copyFileSync(`${SKILL_ASSETS_DIR}/nia-profile.webp`, `${imagesDir}/profile.png`);
354
+ copyFileSync(`${SKILL_ASSETS_DIR}/nia-profile.webp`, `${imagesDir}/profile.webp`);
349
355
  console.log(` \u2713 copied default profile picture`);
350
356
  }
351
357
  }
@@ -400,6 +406,7 @@ export async function runInit(): Promise<void> {
400
406
  const vars = { agentName, ownerName, ownerRole, ownerLocation, ownerInterests };
401
407
  const selfFile = (name: string) => `${paths.selfDir}/${name}`;
402
408
 
409
+ // Identity and owner — always write (template-driven, not user-customized)
403
410
  writeFileSync(selfFile("identity.md"), loadTemplate("identity.md", vars));
404
411
  console.log(` \u2713 wrote ${selfFile("identity.md")}`);
405
412
 
@@ -411,8 +418,8 @@ export async function runInit(): Promise<void> {
411
418
  }
412
419
 
413
420
  // Soul and memory — only create if missing (user may have customized)
414
- writeIfMissing(selfFile("soul.md"), loadTemplate("soul.md"), selfFile("soul.md"));
415
- writeIfMissing(selfFile("memory.md"), loadTemplate("memory.md"), selfFile("memory.md"));
421
+ writeIfMissing(selfFile("soul.md"), loadTemplate("soul.md", vars), selfFile("soul.md"));
422
+ writeIfMissing(selfFile("memory.md"), loadTemplate("memory.md", vars), selfFile("memory.md"));
416
423
 
417
424
  resetConfig();
418
425
 
@@ -99,6 +99,12 @@ async function tick(): Promise<void> {
99
99
  await Job.markRun(job.name, nextRun).catch((err) => {
100
100
  log.error({ err, job: job.name }, "scheduler: failed to update next_run_at");
101
101
  });
102
+
103
+ // Auto-disable one-shot jobs after execution
104
+ if (job.scheduleType === "once") {
105
+ await Job.update(job.name, { enabled: false }).catch(() => {});
106
+ log.info({ job: job.name }, "scheduler: one-shot job completed, auto-disabled");
107
+ }
102
108
  }
103
109
  }
104
110