niahere 0.2.31 → 0.2.33

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": "niahere",
3
- "version": "0.2.31",
3
+ "version": "0.2.33",
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": {
@@ -1,14 +1,12 @@
1
- import { existsSync, readFileSync, readdirSync } from "fs";
2
- import { join, resolve } from "path";
3
- import { homedir } from "os";
4
- import yaml from "js-yaml";
5
- import { getNiaHome, getPaths } from "../utils/paths";
1
+ import { existsSync, readFileSync } from "fs";
2
+ import { join } from "path";
3
+ import { getPaths } from "../utils/paths";
6
4
  import { getEnvironmentPrompt, getModePrompt, getChannelPrompt } from "../prompts";
7
- import { log } from "../utils/log";
5
+ import { getSkillsSummary } from "../core/skills";
8
6
  import type { Mode } from "../types";
9
7
 
10
- // niahere project root (resolved from this file's location)
11
- const PROJECT_ROOT = resolve(import.meta.dir, "../..");
8
+ // Re-export for backwards compat
9
+ export { scanSkills as loadSkills, getSkillNames as loadSkillNames, type SkillInfo } from "../core/skills";
12
10
 
13
11
  function loadFile(dir: string, name: string): string {
14
12
  const filePath = join(dir, name);
@@ -22,71 +20,6 @@ export function loadIdentity(): string {
22
20
  return files.map((f) => loadFile(selfDir, f)).filter(Boolean).join("\n\n");
23
21
  }
24
22
 
25
- function scanSkills(): { name: string; description: string }[] {
26
- const home = homedir();
27
- const cwd = process.cwd();
28
- const niaHome = getNiaHome();
29
- const skillDirs = [
30
- join(cwd, "skills"),
31
- join(PROJECT_ROOT, "skills"),
32
- join(niaHome, "skills"),
33
- join(home, ".shared", "skills"),
34
- join(home, ".claude", "skills"),
35
- join(home, ".codex", "skills"),
36
- ];
37
-
38
- const skills: { name: string; description: string }[] = [];
39
- const seen = new Set<string>();
40
-
41
- for (const dir of skillDirs) {
42
- if (!existsSync(dir)) continue;
43
-
44
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
45
- if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
46
-
47
- const skillFile = join(dir, entry.name, "SKILL.md");
48
- if (!existsSync(skillFile)) continue;
49
-
50
- const content = readFileSync(skillFile, "utf8");
51
- const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
52
- if (!fmMatch) continue;
53
-
54
- let meta: Record<string, unknown> = {};
55
- try {
56
- meta = (yaml.load(fmMatch[1]) as Record<string, unknown>) || {};
57
- } catch (err) {
58
- log.warn({ err, skill: entry.name, path: skillFile }, "failed to parse skill metadata, skipping");
59
- continue;
60
- }
61
- const name = (typeof meta.name === "string" ? meta.name : "") || entry.name;
62
-
63
- if (seen.has(name)) continue;
64
- seen.add(name);
65
-
66
- skills.push({
67
- name,
68
- description: typeof meta.description === "string" ? meta.description : "",
69
- });
70
- }
71
- }
72
-
73
- return skills;
74
- }
75
-
76
- export function loadSkillNames(): string[] {
77
- return scanSkills().map((s) => s.name);
78
- }
79
-
80
- export function loadSkillsSummary(): string {
81
- const skills = scanSkills();
82
- if (skills.length === 0) return "";
83
-
84
- const lines = skills.map((s) =>
85
- s.description ? `- /${s.name}: ${s.description}` : `- /${s.name}`,
86
- );
87
- return `Available skills:\n${lines.join("\n")}`;
88
- }
89
-
90
23
  export function buildSystemPrompt(mode: Mode = "chat", channel: string = "terminal"): string {
91
24
  const parts: string[] = [];
92
25
 
@@ -101,7 +34,7 @@ export function buildSystemPrompt(mode: Mode = "chat", channel: string = "termin
101
34
  const channelPrompt = getChannelPrompt(channel);
102
35
  if (channelPrompt) parts.push(channelPrompt);
103
36
 
104
- const skills = loadSkillsSummary();
37
+ const skills = getSkillsSummary();
105
38
  if (skills) parts.push(skills);
106
39
 
107
40
  return parts.join("\n\n");
package/src/cli/index.ts CHANGED
@@ -287,12 +287,19 @@ switch (command) {
287
287
  }
288
288
 
289
289
  case "skills": {
290
- const { loadSkillNames } = await import("../chat/identity");
291
- const names = loadSkillNames();
292
- if (names.length === 0) {
293
- console.log("No skills found.");
290
+ const { scanSkills: loadSkills } = await import("../core/skills");
291
+ const filter = process.argv[3]; // e.g. "project", "nia", "shared", "claude"
292
+ let skills = loadSkills();
293
+ if (filter) {
294
+ skills = skills.filter((s) => s.source === filter);
295
+ }
296
+ if (skills.length === 0) {
297
+ console.log(filter ? `No skills found in "${filter}".` : "No skills found.");
294
298
  } else {
295
- for (const name of names) console.log(` ${name}`);
299
+ for (const s of skills) {
300
+ const tag = filter ? "" : ` [${s.source}]`;
301
+ console.log(` ${s.name}${tag}`);
302
+ }
296
303
  }
297
304
  break;
298
305
  }
@@ -0,0 +1,67 @@
1
+ import { existsSync, readFileSync, readdirSync } from "fs";
2
+ import { join, resolve } from "path";
3
+ import { homedir } from "os";
4
+ import yaml from "js-yaml";
5
+ import { getNiaHome } from "../utils/paths";
6
+ import { log } from "../utils/log";
7
+
8
+ // niahere project root (resolved from this file's location)
9
+ const PROJECT_ROOT = resolve(import.meta.dir, "../..");
10
+
11
+ export type SkillInfo = { name: string; description: string; source: string };
12
+
13
+ const SKILL_DIRS: { dir: string; source: string }[] = [
14
+ { dir: join(process.cwd(), "skills"), source: "cwd" },
15
+ { dir: join(PROJECT_ROOT, "skills"), source: "project" },
16
+ { dir: join(getNiaHome(), "skills"), source: "nia" },
17
+ { dir: join(homedir(), ".shared", "skills"), source: "shared" },
18
+ { dir: join(homedir(), ".claude", "skills"), source: "claude" },
19
+ { dir: join(homedir(), ".codex", "skills"), source: "codex" },
20
+ ];
21
+
22
+ export function scanSkills(): SkillInfo[] {
23
+ const skills: SkillInfo[] = [];
24
+ const seen = new Set<string>();
25
+
26
+ for (const { dir, source } of SKILL_DIRS) {
27
+ if (!existsSync(dir)) continue;
28
+
29
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
30
+ if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
31
+
32
+ const skillFile = join(dir, entry.name, "SKILL.md");
33
+ if (!existsSync(skillFile)) continue;
34
+
35
+ const content = readFileSync(skillFile, "utf8");
36
+ const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
37
+ if (!fmMatch) continue;
38
+
39
+ let meta: Record<string, unknown> = {};
40
+ try {
41
+ meta = (yaml.load(fmMatch[1]) as Record<string, unknown>) || {};
42
+ } catch (err) {
43
+ log.warn({ err, skill: entry.name, path: skillFile }, "failed to parse skill metadata, skipping");
44
+ continue;
45
+ }
46
+ const name = (typeof meta.name === "string" ? meta.name : "") || entry.name;
47
+
48
+ if (seen.has(name)) continue;
49
+ seen.add(name);
50
+
51
+ skills.push({ name, description: typeof meta.description === "string" ? meta.description : "", source });
52
+ }
53
+ }
54
+
55
+ return skills;
56
+ }
57
+
58
+ export function getSkillNames(): string[] {
59
+ return scanSkills().map((s) => s.name);
60
+ }
61
+
62
+ export function getSkillsSummary(): string {
63
+ const skills = scanSkills();
64
+ if (skills.length === 0) return "";
65
+ const lines = skills.map((s) => s.description ? `- /${s.name}: ${s.description}` : `- /${s.name}`);
66
+ return `Available skills:\n${lines.join("\n")}`;
67
+ }
package/src/mcp/server.ts CHANGED
@@ -135,9 +135,17 @@ export function createNiaMcpServer() {
135
135
  content: [{ type: "text" as const, text: handlers.addRule(args.rule) }],
136
136
  }),
137
137
  ),
138
+ tool(
139
+ "read_memory",
140
+ "Read all saved memories. Use this to check what you already know before saving duplicates, or to recall context about the owner, past incidents, preferences, etc.",
141
+ {},
142
+ async () => ({
143
+ content: [{ type: "text" as const, text: handlers.readMemory() }],
144
+ }),
145
+ ),
138
146
  tool(
139
147
  "add_memory",
140
- "Save a concise factual memory for future reference. Memories are read on demand, not loaded automatically. Use for preferences, corrections, or patterns worth keeping. RULES: Max 300 chars. One insight per entry. NO raw logs, NO conversation transcripts, NO status dumps, NO duplicate observations. Bad: pasting nia status output. Good: 'curator job can get stuck in running state — needs timeout recovery'.",
148
+ "Save a concise factual memory for future reference. Proactively save personal facts (travel, schedule), work context (decisions, deadlines), and corrections don't wait to be asked. RULES: Max 300 chars. One insight per entry. NO raw logs, NO transcripts, NO status dumps.",
141
149
  {
142
150
  entry: z.string().max(300).describe("A single concise insight (max 300 chars, no raw logs or transcripts)"),
143
151
  },
package/src/mcp/tools.ts CHANGED
@@ -273,6 +273,17 @@ export function disableWatchChannel(name: string): string {
273
273
  return `Watch channel "${name}" disabled. Takes effect on next message.`;
274
274
  }
275
275
 
276
+ export function readMemory(): string {
277
+ const { selfDir } = getPaths();
278
+ const memoryPath = join(selfDir, "memory.md");
279
+ if (!existsSync(memoryPath)) return "No memories saved yet.";
280
+ const content = readFileSync(memoryPath, "utf8").trim();
281
+ // Extract just the entries, skip the header/instructions
282
+ const lines = content.split("\n").filter((l) => l.startsWith("- ") || l.startsWith("## "));
283
+ if (lines.length === 0) return "No memories saved yet.";
284
+ return lines.join("\n");
285
+ }
286
+
276
287
  export function addMemory(entry: string): string {
277
288
  // Guard: reject raw logs, transcripts, and overly long entries
278
289
  const trimmed = entry.trim();
@@ -34,7 +34,8 @@ You have MCP tools for managing jobs directly (preferred over CLI for speed):
34
34
  - **remove_watch_channel** — stop watching a Slack channel. Hot-reloads.
35
35
  - **enable_watch_channel** / **disable_watch_channel** — toggle a watch channel on/off without removing it. Hot-reloads.
36
36
  - **add_rule** — save a behavioral rule (loaded into every session, no restart needed). Use when told "from now on", "always", "never", or "remember to always..."
37
- - **add_memory** — save a factual memory (read on demand). Use when told "remember that...", or when you learn something surprising worth keeping
37
+ - **read_memory** — recall all saved memories. Check before saving to avoid duplicates, or when you need context about the owner.
38
+ - **add_memory** — save a factual memory. Proactively save personal facts, work context, corrections — don't wait to be asked.
38
39
 
39
40
  Active hours: {{activeStart}}–{{activeEnd}} ({{timezone}}). Jobs respect this; crons (always=true) don't.
40
41
 
@@ -90,9 +91,18 @@ Your persona files live in {{selfDir}}/:
90
91
  **Memory** (`memory.md`) = facts and context. Read on demand when relevant.
91
92
  - "2026-03-13: DB was down, Telegram send failed"
92
93
  - "Aman prefers terminal over Slack for debugging"
93
- - Use `add_memory` tool to save new memories.
94
+ - Use `read_memory` to recall what you know. Use `add_memory` to save new memories.
94
95
 
95
96
  **Which to use?**
96
97
  - "From now on, do X" → rule
97
98
  - "Remember that X happened" / "I prefer X" → memory
99
+
100
+ ### When to save (proactive)
101
+ Don't wait for the user to say "remember this." Proactively save when you learn:
102
+ - Personal facts: travel plans, location, schedule, preferences
103
+ - Work context: project decisions, team changes, deadlines
104
+ - Corrections: user corrected you on something worth remembering
105
+ - Patterns: recurring requests, preferred communication style
106
+
107
+ Example: if the owner says "I'm going home on the 21st, early morning flight" — save it as a memory without being asked. These are facts future sessions need.
98
108
  - If unsure, ask.