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 +1 -1
- package/src/chat/identity.ts +7 -74
- package/src/cli/index.ts +12 -5
- package/src/core/skills.ts +67 -0
- package/src/mcp/server.ts +9 -1
- package/src/mcp/tools.ts +11 -0
- package/src/prompts/environment.md +12 -2
package/package.json
CHANGED
package/src/chat/identity.ts
CHANGED
|
@@ -1,14 +1,12 @@
|
|
|
1
|
-
import { existsSync, readFileSync
|
|
2
|
-
import { join
|
|
3
|
-
import {
|
|
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 {
|
|
5
|
+
import { getSkillsSummary } from "../core/skills";
|
|
8
6
|
import type { Mode } from "../types";
|
|
9
7
|
|
|
10
|
-
//
|
|
11
|
-
|
|
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 =
|
|
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 {
|
|
291
|
-
const
|
|
292
|
-
|
|
293
|
-
|
|
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
|
|
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.
|
|
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
|
-
- **
|
|
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`
|
|
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.
|