niahere 0.2.20 → 0.2.21

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.
@@ -1,14 +1,15 @@
1
1
  # Memory
2
2
 
3
- Things I've picked up that I don't want to forget. I maintain this myself.
3
+ Concise things I've picked up that I don't want to forget. I maintain this myself.
4
4
 
5
- Write here when:
6
- - Something surprised me or broke unexpectedly
7
- - {{ownerName}} corrected me or preferred a different approach
8
- - I learned a preference, habit, or pattern worth remembering
9
- - A workaround was needed that future-me should know about
5
+ Rules:
6
+ - One insight per entry, max 200 chars
7
+ - NO raw logs, transcripts, or status dumps
8
+ - NO duplicates check before adding
9
+ - Good: "curator job can hang needs timeout recovery"
10
+ - Bad: pasting nia status output or conversation logs
10
11
 
11
- Entries are grouped by date. Use `add_memory` tool to append, or edit directly.
12
+ Entries are grouped by date. Use `add_memory` tool to append.
12
13
 
13
14
  ---
14
15
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "niahere",
3
- "version": "0.2.20",
3
+ "version": "0.2.21",
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": {
package/src/cli/index.ts CHANGED
@@ -12,6 +12,7 @@ import { fail } from "../utils/cli";
12
12
  import { jobCommand } from "./job";
13
13
  import { statusCommand } from "./status";
14
14
  import { sendCommand, telegramCommand, slackCommand } from "./channels";
15
+ import { rulesCommand, memoryCommand } from "./self";
15
16
 
16
17
  // Set LOG_LEVEL from config before anything else logs
17
18
  try {
@@ -207,6 +208,16 @@ switch (command) {
207
208
  break;
208
209
  }
209
210
 
211
+ case "rules": {
212
+ rulesCommand();
213
+ break;
214
+ }
215
+
216
+ case "memory": {
217
+ memoryCommand();
218
+ break;
219
+ }
220
+
210
221
  case "history": {
211
222
  const room = process.argv[3];
212
223
  try {
@@ -419,6 +430,8 @@ switch (command) {
419
430
  console.log(" history [room] — recent messages");
420
431
  console.log(" logs [-f] [--channel ch] — daemon logs (filter by channel)");
421
432
  console.log(" job <sub> — manage jobs");
433
+ console.log(" rules [show|reset] — view or reset rules.md");
434
+ console.log(" memory [show|reset] — view or reset memory.md");
422
435
  console.log(" db <sub> — database setup/status/migrate");
423
436
  console.log(" skills — list available skills");
424
437
  console.log(" config <sub> — get/set/list config values");
package/src/cli/job.ts CHANGED
@@ -49,7 +49,7 @@ export async function jobCommand(): Promise<void> {
49
49
  for (const job of jobs) {
50
50
  const tag = job.always ? " always" : "";
51
51
  const type = job.scheduleType !== "cron" ? ` (${job.scheduleType})` : "";
52
- console.log(` ${job.enabled ? "●" : "○"} ${job.name} ${job.schedule}${type}${tag} ${job.prompt.slice(0, 60)}${job.prompt.length > 60 ? "..." : ""}`);
52
+ console.log(` ${job.enabled ? "●" : "○"} ${job.name} ${job.schedule}${type}${tag}`);
53
53
  }
54
54
  }
55
55
  });
@@ -0,0 +1,74 @@
1
+ import { existsSync, readFileSync, copyFileSync } from "fs";
2
+ import { join } from "path";
3
+ import { getPaths } from "../utils/paths";
4
+ import { fail } from "../utils/cli";
5
+
6
+ function selfFilePath(name: "rules" | "memory"): string {
7
+ const { selfDir } = getPaths();
8
+ return join(selfDir, `${name}.md`);
9
+ }
10
+
11
+ function defaultFilePath(name: "rules" | "memory"): string {
12
+ const projectRoot = join(import.meta.dir, "../..");
13
+ return join(projectRoot, "defaults", "self", `${name}.md`);
14
+ }
15
+
16
+ function show(name: "rules" | "memory"): void {
17
+ const path = selfFilePath(name);
18
+ if (!existsSync(path)) {
19
+ console.log(`No ${name}.md found.`);
20
+ return;
21
+ }
22
+ console.log(readFileSync(path, "utf8").trim());
23
+ }
24
+
25
+ function reset(name: "rules" | "memory"): void {
26
+ const path = selfFilePath(name);
27
+ const defaultPath = defaultFilePath(name);
28
+
29
+ if (!existsSync(defaultPath)) {
30
+ fail(`Default ${name}.md template not found.`);
31
+ }
32
+
33
+ if (existsSync(path)) {
34
+ copyFileSync(path, `${path}.bak`);
35
+ console.log(` backed up → ${name}.md.bak`);
36
+ }
37
+
38
+ copyFileSync(defaultPath, path);
39
+ console.log(` ${name}.md reset to default.`);
40
+ }
41
+
42
+ export function rulesCommand(): void {
43
+ const sub = process.argv[3];
44
+ switch (sub) {
45
+ case "show":
46
+ case undefined:
47
+ show("rules");
48
+ break;
49
+ case "reset":
50
+ reset("rules");
51
+ break;
52
+ default:
53
+ console.log("Usage: nia rules <show|reset>");
54
+ console.log(" show — display current rules (default)");
55
+ console.log(" reset — reset to default template (backs up current)");
56
+ }
57
+ }
58
+
59
+ export function memoryCommand(): void {
60
+ const sub = process.argv[3];
61
+ switch (sub) {
62
+ case "show":
63
+ case undefined:
64
+ show("memory");
65
+ break;
66
+ case "reset":
67
+ reset("memory");
68
+ break;
69
+ default:
70
+ console.log("Usage: nia memory <show|reset>");
71
+ console.log(" show — display current memory (default)");
72
+ console.log(" reset — reset to default template (backs up current)");
73
+ }
74
+ }
@@ -91,18 +91,22 @@ async function runJobWithClaude(systemPrompt: string, jobPrompt: string, cwd: st
91
91
  let agentText = "";
92
92
  let actualSessionId = sessionId;
93
93
 
94
- for await (const message of handle) {
95
- if (message.type === "system" && (message as any).subtype === "init") {
96
- actualSessionId = (message as any).session_id || sessionId;
97
- }
98
- if (message.type === "result") {
99
- if (!(message as any).is_error) {
100
- agentText = (message as any).result || "";
101
- } else {
102
- const errors = (message as any).errors;
103
- return { agentText: "", sessionId: actualSessionId, error: errors?.join(", ") || "unknown error" };
94
+ try {
95
+ for await (const message of handle) {
96
+ if (message.type === "system" && (message as any).subtype === "init") {
97
+ actualSessionId = (message as any).session_id || sessionId;
98
+ }
99
+ if (message.type === "result") {
100
+ if (!(message as any).is_error) {
101
+ agentText = (message as any).result || "";
102
+ } else {
103
+ const errors = (message as any).errors;
104
+ return { agentText: "", sessionId: actualSessionId, error: errors?.join(", ") || "unknown error" };
105
+ }
104
106
  }
105
107
  }
108
+ } finally {
109
+ handle.close();
106
110
  }
107
111
 
108
112
  return { agentText, sessionId: actualSessionId };
package/src/mcp/server.ts CHANGED
@@ -96,9 +96,9 @@ export function createNiaMcpServer() {
96
96
  ),
97
97
  tool(
98
98
  "add_memory",
99
- "Save a factual memory for future reference. Memories are read on demand, not loaded automatically. Use for things learned, preferences discovered, or context worth keeping.",
99
+ "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 200 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'.",
100
100
  {
101
- entry: z.string().describe("What to remember (e.g. 'Aman prefers short Slack messages in #tech')"),
101
+ entry: z.string().max(300).describe("A single concise insight (max 200 chars, no raw logs or transcripts)"),
102
102
  },
103
103
  async (args) => ({
104
104
  content: [{ type: "text" as const, text: handlers.addMemory(args.entry) }],
package/src/mcp/tools.ts CHANGED
@@ -230,19 +230,39 @@ export function addRule(rule: string): string {
230
230
  }
231
231
 
232
232
  export function addMemory(entry: string): string {
233
+ // Guard: reject raw logs, transcripts, and overly long entries
234
+ const trimmed = entry.trim();
235
+ if (!trimmed) return "Rejected: empty entry.";
236
+ if (trimmed.length > 300) return "Rejected: too long (max 300 chars). Distill to a single concise insight.";
237
+ if (trimmed.includes("[Thread context]") || trimmed.includes("[Current messag")) return "Rejected: no raw conversation transcripts.";
238
+ if (trimmed.split("\n").length > 5) return "Rejected: too many lines. One concise insight per memory.";
239
+
233
240
  const { selfDir } = getPaths();
234
241
  const memoryPath = join(selfDir, "memory.md");
242
+ const existing = existsSync(memoryPath) ? readFileSync(memoryPath, "utf8") : "";
243
+
244
+ // Deduplicate: skip if a substantially similar entry already exists
245
+ const normalized = trimmed.toLowerCase().replace(/[^a-z0-9 ]/g, "");
246
+ const lines = existing.split("\n").filter((l) => l.startsWith("- "));
247
+ for (const line of lines) {
248
+ const norm = line.slice(2).toLowerCase().replace(/[^a-z0-9 ]/g, "");
249
+ // Check if >60% of words overlap
250
+ const newWords = new Set(normalized.split(/\s+/).filter(Boolean));
251
+ const oldWords = new Set(norm.split(/\s+/).filter(Boolean));
252
+ if (newWords.size === 0) continue;
253
+ let overlap = 0;
254
+ for (const w of newWords) { if (oldWords.has(w)) overlap++; }
255
+ if (overlap / newWords.size > 0.6) return "Rejected: similar memory already exists.";
256
+ }
257
+
235
258
  const date = new Date().toISOString().slice(0, 10);
236
259
  const header = `\n## ${date}`;
237
260
 
238
- const existing = existsSync(memoryPath) ? readFileSync(memoryPath, "utf8") : "";
239
261
  if (existing.includes(header)) {
240
- // Append under existing date header
241
- const updated = existing.replace(header, `${header}\n- ${entry}`);
262
+ const updated = existing.replace(header, `${header}\n- ${trimmed}`);
242
263
  writeFileSync(memoryPath, updated, "utf8");
243
264
  } else {
244
- // New date section
245
- appendFileSync(memoryPath, `${header}\n- ${entry}\n`, "utf8");
265
+ appendFileSync(memoryPath, `${header}\n- ${trimmed}\n`, "utf8");
246
266
  }
247
267
  return `Memory saved.`;
248
268
  }