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.
- package/defaults/self/memory.md +8 -7
- package/package.json +1 -1
- package/src/cli/index.ts +13 -0
- package/src/cli/job.ts +1 -1
- package/src/cli/self.ts +74 -0
- package/src/core/runner.ts +14 -10
- package/src/mcp/server.ts +2 -2
- package/src/mcp/tools.ts +25 -5
package/defaults/self/memory.md
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
# Memory
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Concise things I've picked up that I don't want to forget. I maintain this myself.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
-
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
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
|
|
12
|
+
Entries are grouped by date. Use `add_memory` tool to append.
|
|
12
13
|
|
|
13
14
|
---
|
|
14
15
|
|
package/package.json
CHANGED
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}
|
|
52
|
+
console.log(` ${job.enabled ? "●" : "○"} ${job.name} ${job.schedule}${type}${tag}`);
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
});
|
package/src/cli/self.ts
ADDED
|
@@ -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
|
+
}
|
package/src/core/runner.ts
CHANGED
|
@@ -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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
if (
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
|
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("
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|