niahere 0.2.6 → 0.2.9

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
@@ -19,9 +19,7 @@ if [ -z "$BUN_CMD" ]; then
19
19
  echo "Bun install failed. Install manually: curl -fsSL https://bun.sh/install | bash"
20
20
  exit 1
21
21
  fi
22
- echo ""
23
22
  echo "Bun installed. Continuing..."
24
- echo "(Open a new terminal for bun to be in PATH permanently)"
25
23
  echo ""
26
24
  else
27
25
  echo ""
@@ -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.6",
3
+ "version": "0.2.9",
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": {
@@ -11,7 +11,8 @@
11
11
  "test": "tsc --noEmit && LOG_LEVEL=silent bun test --reporter=dots",
12
12
  "test:bun": "bun test",
13
13
  "typecheck": "tsc --noEmit",
14
- "seed": "bun run src/db/seed.ts"
14
+ "seed": "bun run src/db/seed.ts",
15
+ "release": "npm version patch && npm publish && git push"
15
16
  },
16
17
  "keywords": [
17
18
  "ai",
@@ -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/index.ts CHANGED
@@ -238,6 +238,12 @@ switch (command) {
238
238
  break;
239
239
  }
240
240
 
241
+ case "db": {
242
+ const { dbCommand } = await import("../commands/db");
243
+ await dbCommand();
244
+ break;
245
+ }
246
+
241
247
  case "test": {
242
248
  const verbose = process.argv.includes("-v") || process.argv.includes("--verbose");
243
249
  const extraArgs = process.argv.slice(3).filter((a) => a !== "-v" && a !== "--verbose");
@@ -285,6 +291,7 @@ switch (command) {
285
291
  console.log(" history [room] — recent messages");
286
292
  console.log(" logs [-f] — daemon logs");
287
293
  console.log(" job <sub> — manage jobs");
294
+ console.log(" db <sub> — database setup/status/migrate");
288
295
  console.log(" skills — list available skills");
289
296
  console.log(" send [-c ch] <msg> — send a message via channel");
290
297
  console.log(" telegram <token> — configure telegram");
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
 
@@ -0,0 +1,130 @@
1
+ import { getConfig } from "../utils/config";
2
+ import { runMigrations } from "../db/migrate";
3
+ import { closeDb, getSql } from "../db/connection";
4
+ import { errMsg } from "../utils/errors";
5
+
6
+ export async function dbSetup(): Promise<void> {
7
+ console.log("Setting up PostgreSQL...\n");
8
+
9
+ const pgCheck = Bun.spawnSync(["which", "psql"]);
10
+ const hasPostgres = pgCheck.exitCode === 0;
11
+
12
+ if (!hasPostgres) {
13
+ if (process.platform === "darwin") {
14
+ console.log(" PostgreSQL not found. Installing via Homebrew...");
15
+ const brew = Bun.spawn(["brew", "install", "postgresql@17"], { stdout: "inherit", stderr: "inherit" });
16
+ if (await brew.exited !== 0) {
17
+ console.log(" \u2717 brew install failed. Install manually: brew install postgresql@17");
18
+ return;
19
+ }
20
+ console.log(" \u2713 PostgreSQL installed");
21
+
22
+ console.log(" Starting PostgreSQL...");
23
+ const start = Bun.spawn(["brew", "services", "start", "postgresql@17"], { stdout: "pipe", stderr: "pipe" });
24
+ if (await start.exited !== 0) {
25
+ console.log(" \u2717 could not start. Try: brew services start postgresql@17");
26
+ return;
27
+ }
28
+ console.log(" \u2713 PostgreSQL started");
29
+ await new Promise((r) => setTimeout(r, 2000));
30
+ } else {
31
+ console.log(" PostgreSQL not found.");
32
+ console.log(" Install it:");
33
+ console.log(" Ubuntu/Debian: sudo apt install postgresql");
34
+ console.log(" Fedora: sudo dnf install postgresql-server");
35
+ console.log(" Arch: sudo pacman -S postgresql");
36
+ return;
37
+ }
38
+ } else {
39
+ const ready = Bun.spawnSync(["pg_isready"]);
40
+ if (ready.exitCode !== 0) {
41
+ console.log(" PostgreSQL installed but not running.");
42
+ if (process.platform === "darwin") {
43
+ console.log(" Starting...");
44
+ const start = Bun.spawn(["brew", "services", "start", "postgresql@17"], { stdout: "pipe", stderr: "pipe" });
45
+ await start.exited;
46
+ await new Promise((r) => setTimeout(r, 2000));
47
+ if (Bun.spawnSync(["pg_isready"]).exitCode === 0) {
48
+ console.log(" \u2713 PostgreSQL started");
49
+ } else {
50
+ console.log(" \u2717 could not start. Check: brew services list");
51
+ return;
52
+ }
53
+ } else {
54
+ console.log(" Start it: sudo systemctl start postgresql");
55
+ return;
56
+ }
57
+ } else {
58
+ console.log(" \u2713 PostgreSQL running");
59
+ }
60
+ }
61
+
62
+ // Create database
63
+ const config = getConfig();
64
+ const dbName = config.database_url.split("/").pop() || "niahere";
65
+ const createDb = Bun.spawnSync(["createdb", dbName]);
66
+ if (createDb.exitCode === 0) {
67
+ console.log(` \u2713 database "${dbName}" created`);
68
+ } else {
69
+ const stderr = new TextDecoder().decode(createDb.stderr);
70
+ if (stderr.includes("already exists")) {
71
+ console.log(` \u2713 database "${dbName}" already exists`);
72
+ } else {
73
+ console.log(` \u2717 createdb failed: ${stderr.trim()}`);
74
+ return;
75
+ }
76
+ }
77
+
78
+ // Run migrations
79
+ try {
80
+ await runMigrations();
81
+ console.log(" \u2713 migrations done");
82
+ await closeDb();
83
+ } catch (err) {
84
+ console.log(` \u2717 migrations failed: ${errMsg(err)}`);
85
+ }
86
+
87
+ console.log("\nDatabase ready.");
88
+ }
89
+
90
+ export async function dbCommand(): Promise<void> {
91
+ const sub = process.argv[3];
92
+
93
+ switch (sub) {
94
+ case "setup":
95
+ await dbSetup();
96
+ break;
97
+
98
+ case "migrate": {
99
+ try {
100
+ await runMigrations();
101
+ console.log("Migrations done.");
102
+ await closeDb();
103
+ } catch (err) {
104
+ console.log(`Failed: ${errMsg(err)}`);
105
+ process.exit(1);
106
+ }
107
+ break;
108
+ }
109
+
110
+ case "status": {
111
+ try {
112
+ const sql = getSql();
113
+ await sql`SELECT 1`;
114
+ console.log("Database: connected");
115
+ await closeDb();
116
+ } catch (err) {
117
+ console.log(`Database: unavailable (${errMsg(err)})`);
118
+ process.exit(1);
119
+ }
120
+ break;
121
+ }
122
+
123
+ default:
124
+ console.log("Usage: nia db <command>\n");
125
+ console.log(" setup — install PostgreSQL + create database + migrate");
126
+ console.log(" migrate — run database migrations");
127
+ console.log(" status — check database connection");
128
+ break;
129
+ }
130
+ }
@@ -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 {
@@ -110,7 +114,13 @@ export async function runInit(): Promise<void> {
110
114
  } catch (err) {
111
115
  const msg = errMsg(err);
112
116
  console.log(` \u2717 could not connect: ${msg}`);
113
- console.log(` (you can fix this later in ${paths.config})\n`);
117
+ const setupDb = await ask(rl, " Set up PostgreSQL now? (y/n)", "y");
118
+ if (setupDb.toLowerCase() === "y") {
119
+ const { dbSetup } = await import("./db");
120
+ await dbSetup();
121
+ } else {
122
+ console.log(` (run 'nia db setup' later)\n`);
123
+ }
114
124
  }
115
125
  delete process.env.DATABASE_URL;
116
126
 
@@ -144,6 +154,7 @@ export async function runInit(): Promise<void> {
144
154
  if (telegramToken) {
145
155
  const openInput = await ask(rl, "Allow anyone to message? (y/n)", "n");
146
156
  telegramOpen = openInput.toLowerCase() === "y";
157
+ console.log(" Tip: Send a message to your bot to activate outbound messaging.");
147
158
  }
148
159
  }
149
160
  }
@@ -181,7 +192,8 @@ export async function runInit(): Promise<void> {
181
192
  const createUrl = `https://api.slack.com/apps?new_app=1&manifest_json=${encodeURIComponent(manifest)}`;
182
193
 
183
194
  console.log("\n Opening Slack app creation page...");
184
- Bun.spawn(["open", createUrl], { stdio: ["ignore", "ignore", "ignore"] });
195
+ const openCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
196
+ Bun.spawn([openCmd, createUrl], { stdio: ["ignore", "ignore", "ignore"] });
185
197
  console.log(" 1. Click 'Create' to create the app");
186
198
  console.log(" 2. Go to 'OAuth & Permissions' → Install to workspace → copy Bot Token (xoxb-...)");
187
199
  console.log(" 3. Go to 'Basic Information' → 'App-Level Tokens' → create one with connections:write → copy (xapp-...)\n");
@@ -299,7 +311,7 @@ export async function runInit(): Promise<void> {
299
311
  // Visual identity
300
312
  const imagesDir = `${home}/images`;
301
313
  mkdirSync(imagesDir, { recursive: true });
302
- const hasUserReference = existsSync(`${imagesDir}/reference.png`);
314
+ const hasUserReference = existsSync(`${imagesDir}/reference.webp`);
303
315
  const hasDefaultReference = existsSync(`${SKILL_ASSETS_DIR}/nia-reference.webp`);
304
316
 
305
317
  if (geminiApiKey && !hasUserReference) {
@@ -317,23 +329,23 @@ export async function runInit(): Promise<void> {
317
329
  "--api-key", geminiApiKey,
318
330
  "--aspect-ratio", "9:16",
319
331
  "--prompt", prompt,
320
- "--output", `${imagesDir}/reference.png`,
332
+ "--output", `${imagesDir}/reference.webp`,
321
333
  ], { stdout: "pipe", stderr: "pipe" });
322
334
  const exitCode = await proc.exited;
323
335
  if (exitCode === 0) {
324
- console.log(` \u2713 generated reference image at ${imagesDir}/reference.png`);
336
+ console.log(` \u2713 generated reference image at ${imagesDir}/reference.webp`);
325
337
  // Also generate a profile picture
326
338
  console.log(" Generating profile picture...");
327
339
  const profileProc = Bun.spawn([
328
340
  "python3", GENERATE_SCRIPT,
329
- "--reference", `${imagesDir}/reference.png`,
341
+ "--reference", `${imagesDir}/reference.webp`,
330
342
  "--api-key", geminiApiKey,
331
343
  "--aspect-ratio", "1:1",
332
344
  "--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`,
345
+ "--output", `${imagesDir}/profile.webp`,
334
346
  ], { stdout: "pipe", stderr: "pipe" });
335
347
  if (await profileProc.exited === 0) {
336
- console.log(` \u2713 generated profile picture at ${imagesDir}/profile.png`);
348
+ console.log(` \u2713 generated profile picture at ${imagesDir}/profile.webp`);
337
349
  }
338
350
  } else {
339
351
  const stderr = await new Response(proc.stderr).text();
@@ -342,10 +354,10 @@ export async function runInit(): Promise<void> {
342
354
  } else if (hasDefaultReference) {
343
355
  // No description — copy defaults
344
356
  const { copyFileSync } = await import("fs");
345
- copyFileSync(`${SKILL_ASSETS_DIR}/nia-reference.webp`, `${imagesDir}/reference.png`);
357
+ copyFileSync(`${SKILL_ASSETS_DIR}/nia-reference.webp`, `${imagesDir}/reference.webp`);
346
358
  console.log(` \u2713 copied default reference image`);
347
359
  if (existsSync(`${SKILL_ASSETS_DIR}/nia-profile.webp`)) {
348
- copyFileSync(`${SKILL_ASSETS_DIR}/nia-profile.webp`, `${imagesDir}/profile.png`);
360
+ copyFileSync(`${SKILL_ASSETS_DIR}/nia-profile.webp`, `${imagesDir}/profile.webp`);
349
361
  console.log(` \u2713 copied default profile picture`);
350
362
  }
351
363
  }
@@ -400,6 +412,7 @@ export async function runInit(): Promise<void> {
400
412
  const vars = { agentName, ownerName, ownerRole, ownerLocation, ownerInterests };
401
413
  const selfFile = (name: string) => `${paths.selfDir}/${name}`;
402
414
 
415
+ // Identity and owner — always write (template-driven, not user-customized)
403
416
  writeFileSync(selfFile("identity.md"), loadTemplate("identity.md", vars));
404
417
  console.log(` \u2713 wrote ${selfFile("identity.md")}`);
405
418
 
@@ -411,8 +424,8 @@ export async function runInit(): Promise<void> {
411
424
  }
412
425
 
413
426
  // 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"));
427
+ writeIfMissing(selfFile("soul.md"), loadTemplate("soul.md", vars), selfFile("soul.md"));
428
+ writeIfMissing(selfFile("memory.md"), loadTemplate("memory.md", vars), selfFile("memory.md"));
416
429
 
417
430
  resetConfig();
418
431
 
@@ -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