qualia-framework 4.1.0 → 4.3.0

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.
@@ -0,0 +1,317 @@
1
+ #!/usr/bin/env node
2
+ // ~/.claude/bin/knowledge.js — unified loader for the memory layer.
3
+ //
4
+ // Replaces ad-hoc `cat ~/.claude/knowledge/X.md` calls scattered across
5
+ // skills. One entry point, deterministic output, every command exits 0 even
6
+ // when the requested file is missing (prints "(no entries)" to stdout) so
7
+ // skills can pipe the output into prompts without breaking on a fresh install.
8
+ //
9
+ // Why this exists (v4.1.0 audit finding #3):
10
+ // Skills hardcode `cat ~/.claude/knowledge/common-fixes.md`. New knowledge
11
+ // files are dead weight — the agent never sees them because the skill never
12
+ // references them by name. This loader gives skills ONE call (`knowledge.js`
13
+ // with no args prints index.md, the entry point) and lets the agent navigate
14
+ // from there. New files reachable from the index get used automatically.
15
+ //
16
+ // Subcommands:
17
+ // knowledge.js → prints index.md (default)
18
+ // knowledge.js load <file> → prints knowledge/<file>
19
+ // knowledge.js list → lists all knowledge files
20
+ // knowledge.js search <query> → grep across all files
21
+ // knowledge.js append --type <pattern|fix|client> --title <T> --body <B>
22
+ // appends a formatted entry
23
+ // knowledge.js path [<file>] → prints absolute path
24
+ //
25
+ // Cross-platform (Windows/macOS/Linux). No shell dependencies.
26
+
27
+ const fs = require("fs");
28
+ const path = require("path");
29
+ const os = require("os");
30
+
31
+ const KNOWLEDGE_DIR = path.join(os.homedir(), ".claude", "knowledge");
32
+ const INDEX_FILE = path.join(KNOWLEDGE_DIR, "index.md");
33
+
34
+ // Type → filename mapping for `append` and convenience aliases used by the
35
+ // existing `/qualia-learn` taxonomy. Keep this list short — every additional
36
+ // type needs a corresponding section in index.md.
37
+ const TYPE_TO_FILE = {
38
+ pattern: "learned-patterns.md",
39
+ patterns: "learned-patterns.md",
40
+ fix: "common-fixes.md",
41
+ fixes: "common-fixes.md",
42
+ client: "client-prefs.md",
43
+ "client-pref": "client-prefs.md",
44
+ "client-prefs": "client-prefs.md",
45
+ "client-preference": "client-prefs.md",
46
+ };
47
+
48
+ function ensureDir() {
49
+ if (!fs.existsSync(KNOWLEDGE_DIR)) fs.mkdirSync(KNOWLEDGE_DIR, { recursive: true });
50
+ }
51
+
52
+ function readSafe(p) {
53
+ try {
54
+ return fs.readFileSync(p, "utf8");
55
+ } catch {
56
+ return "";
57
+ }
58
+ }
59
+
60
+ // Look up a knowledge file by friendly name. Accepts: "index", "index.md",
61
+ // "patterns" (alias), "fixes" (alias), a bare filename, a subdirectory-
62
+ // qualified path like "concepts/stripe-checkout", or a name that exists
63
+ // inside a known subdirectory. Returns the resolved absolute path (may
64
+ // not exist on disk).
65
+ //
66
+ // Resolution order:
67
+ // 1. "index" / "index.md" → top-level index.md
68
+ // 2. Known type alias (pattern|fix|client) → mapped top-level filename
69
+ // 3. Path with "/" → treat as relative to knowledge dir (concepts/foo)
70
+ // 4. Bare name → look in top-level first; if missing, search known
71
+ // subdirectories (concepts/, daily-log/) for an exact match. This
72
+ // means /qualia-flush can write to concepts/voice-agent-call-state.md
73
+ // and skills can later run `knowledge.js load voice-agent-call-state`
74
+ // without knowing it lives in a subdirectory.
75
+ function resolveFile(name) {
76
+ if (!name || name === "index" || name === "index.md") return INDEX_FILE;
77
+ const lower = name.toLowerCase();
78
+ if (TYPE_TO_FILE[lower]) {
79
+ return path.join(KNOWLEDGE_DIR, TYPE_TO_FILE[lower]);
80
+ }
81
+ const withExt = name.endsWith(".md") ? name : `${name}.md`;
82
+ // Subdirectory-qualified path: concepts/foo, daily-log/2026-04-26
83
+ if (withExt.includes("/") || withExt.includes(path.sep)) {
84
+ return path.join(KNOWLEDGE_DIR, withExt);
85
+ }
86
+ // Top-level wins if it exists.
87
+ const topLevel = path.join(KNOWLEDGE_DIR, withExt);
88
+ if (fs.existsSync(topLevel)) return topLevel;
89
+ // Otherwise search known subdirectories. Stop at the first match — if
90
+ // multiple subdirs have the same filename, the user should qualify.
91
+ const KNOWN_SUBDIRS = ["concepts", "connections", "daily-log"];
92
+ for (const sub of KNOWN_SUBDIRS) {
93
+ const candidate = path.join(KNOWLEDGE_DIR, sub, withExt);
94
+ if (fs.existsSync(candidate)) return candidate;
95
+ }
96
+ // Fall back to top-level (will trigger the "no entries" stub on read).
97
+ return topLevel;
98
+ }
99
+
100
+ function cmdLoad(arg) {
101
+ ensureDir();
102
+ const target = resolveFile(arg);
103
+ if (!fs.existsSync(target)) {
104
+ // Missing file → print a stub message, never fail. Skills can pipe this
105
+ // safely. The instruction line tells the agent what to do next.
106
+ const rel = path.relative(KNOWLEDGE_DIR, target) || "index.md";
107
+ console.log(`(no entries in ${rel} — use /qualia-learn to add one)`);
108
+ process.exit(0);
109
+ }
110
+ process.stdout.write(readSafe(target));
111
+ process.exit(0);
112
+ }
113
+
114
+ function cmdList() {
115
+ ensureDir();
116
+ if (!fs.existsSync(KNOWLEDGE_DIR)) {
117
+ console.log("(knowledge layer not initialized — run: npx qualia-framework@latest install)");
118
+ process.exit(0);
119
+ }
120
+ const entries = [];
121
+ for (const f of fs.readdirSync(KNOWLEDGE_DIR)) {
122
+ const p = path.join(KNOWLEDGE_DIR, f);
123
+ let stat;
124
+ try { stat = fs.statSync(p); } catch { continue; }
125
+ if (stat.isDirectory()) {
126
+ // For daily-log/, count entries.
127
+ let count = 0;
128
+ try { count = fs.readdirSync(p).filter((x) => x.endsWith(".md")).length; } catch {}
129
+ entries.push({ name: `${f}/`, size: count, mtime: stat.mtimeMs, kind: "dir" });
130
+ } else if (f.endsWith(".md")) {
131
+ entries.push({ name: f, size: stat.size, mtime: stat.mtimeMs, kind: "file" });
132
+ }
133
+ }
134
+ entries.sort((a, b) => a.name.localeCompare(b.name));
135
+ for (const e of entries) {
136
+ const sizeStr = e.kind === "dir" ? `${e.size} entries` : `${e.size}B`;
137
+ const date = new Date(e.mtime).toISOString().split("T")[0];
138
+ console.log(`${e.name.padEnd(28)} ${sizeStr.padEnd(14)} ${date}`);
139
+ }
140
+ process.exit(0);
141
+ }
142
+
143
+ function cmdSearch(query) {
144
+ ensureDir();
145
+ if (!query) {
146
+ console.error("Usage: knowledge.js search <query>");
147
+ process.exit(1);
148
+ }
149
+ if (!fs.existsSync(KNOWLEDGE_DIR)) {
150
+ console.log("(knowledge layer not initialized)");
151
+ process.exit(0);
152
+ }
153
+ const needle = query.toLowerCase();
154
+ const matches = [];
155
+ function walk(dir) {
156
+ for (const f of fs.readdirSync(dir, { withFileTypes: true })) {
157
+ const p = path.join(dir, f.name);
158
+ if (f.isDirectory()) { walk(p); continue; }
159
+ if (!f.name.endsWith(".md")) continue;
160
+ const lines = readSafe(p).split("\n");
161
+ lines.forEach((line, idx) => {
162
+ if (line.toLowerCase().includes(needle)) {
163
+ const rel = path.relative(KNOWLEDGE_DIR, p);
164
+ matches.push(`${rel}:${idx + 1}: ${line.trim()}`);
165
+ }
166
+ });
167
+ }
168
+ }
169
+ walk(KNOWLEDGE_DIR);
170
+ if (matches.length === 0) {
171
+ console.log(`(no matches for "${query}")`);
172
+ } else {
173
+ for (const m of matches) console.log(m);
174
+ }
175
+ process.exit(0);
176
+ }
177
+
178
+ // Parse minimal --flag value pairs from argv. Stops at the first positional
179
+ // argument, returns { flags: { type: "pattern", title: "X" }, rest: [...] }.
180
+ function parseFlags(argv) {
181
+ const flags = {};
182
+ const rest = [];
183
+ for (let i = 0; i < argv.length; i++) {
184
+ const a = argv[i];
185
+ if (a.startsWith("--")) {
186
+ const key = a.slice(2);
187
+ const next = argv[i + 1];
188
+ if (next && !next.startsWith("--")) {
189
+ flags[key] = next;
190
+ i++;
191
+ } else {
192
+ flags[key] = true;
193
+ }
194
+ } else {
195
+ rest.push(a);
196
+ }
197
+ }
198
+ return { flags, rest };
199
+ }
200
+
201
+ function cmdAppend(rawArgs) {
202
+ ensureDir();
203
+ const { flags } = parseFlags(rawArgs);
204
+ const type = String(flags.type || "").toLowerCase();
205
+ const title = String(flags.title || "").trim();
206
+ const body = String(flags.body || "").trim();
207
+ const project = String(flags.project || "general").trim();
208
+ const context = String(flags.context || "").trim();
209
+
210
+ if (!type || !TYPE_TO_FILE[type]) {
211
+ console.error(`append: --type must be one of: ${Object.keys(TYPE_TO_FILE).filter((k) => !k.includes("-")).join(", ")}`);
212
+ process.exit(1);
213
+ }
214
+ if (!title) {
215
+ console.error("append: --title is required");
216
+ process.exit(1);
217
+ }
218
+ if (!body) {
219
+ console.error("append: --body is required");
220
+ process.exit(1);
221
+ }
222
+
223
+ const dest = path.join(KNOWLEDGE_DIR, TYPE_TO_FILE[type]);
224
+
225
+ // 8-char hex id, ISO date.
226
+ const id = Math.floor(Math.random() * 0xffffffff).toString(16).padStart(8, "0");
227
+ const date = new Date().toISOString().split("T")[0];
228
+ const entry = [
229
+ "",
230
+ "---",
231
+ "",
232
+ `### ${title}`,
233
+ `**ID:** ${id}`,
234
+ `**Date:** ${date}`,
235
+ `**Project:** ${project}`,
236
+ context ? `**Context:** ${context}` : null,
237
+ "",
238
+ body,
239
+ "",
240
+ ].filter((l) => l !== null).join("\n");
241
+
242
+ if (!fs.existsSync(dest)) {
243
+ const header = `# ${title.split(":")[0] || type} entries\n\nAuto-maintained by /qualia-learn (via bin/knowledge.js).\n`;
244
+ fs.writeFileSync(dest, header + entry + "\n");
245
+ } else {
246
+ fs.appendFileSync(dest, entry + "\n");
247
+ }
248
+
249
+ console.log(`appended ${id} to ${path.basename(dest)}`);
250
+ process.exit(0);
251
+ }
252
+
253
+ function cmdPath(arg) {
254
+ console.log(resolveFile(arg));
255
+ process.exit(0);
256
+ }
257
+
258
+ function cmdHelp() {
259
+ process.stdout.write(`knowledge.js — Qualia Framework memory-layer loader
260
+
261
+ Usage:
262
+ knowledge.js # print index.md (entry point)
263
+ knowledge.js load <file> # print a specific knowledge file
264
+ # accepts: index, patterns, fixes,
265
+ # client, supabase-patterns, etc.
266
+ knowledge.js list # list all files with size + mtime
267
+ knowledge.js search <query> # grep across all files
268
+ knowledge.js append --type <type> --title <T> --body <B> [--project <P>] [--context <C>]
269
+ # type: pattern | fix | client
270
+ knowledge.js path [<file>] # print absolute path (no read)
271
+ knowledge.js help # this message
272
+
273
+ Skills should ALWAYS go through this loader. New knowledge files reachable
274
+ from index.md become usable to every agent automatically. Hardcoded
275
+ \`cat ~/.claude/knowledge/X.md\` calls in skills are an anti-pattern (audit
276
+ finding #3 from the v4.1.0 review) — they make new files invisible.
277
+ `);
278
+ process.exit(0);
279
+ }
280
+
281
+ const cmd = process.argv[2];
282
+ const rest = process.argv.slice(3);
283
+
284
+ switch (cmd) {
285
+ case undefined:
286
+ case null:
287
+ cmdLoad(null);
288
+ break;
289
+ case "load":
290
+ cmdLoad(rest[0]);
291
+ break;
292
+ case "list":
293
+ case "ls":
294
+ cmdList();
295
+ break;
296
+ case "search":
297
+ case "grep":
298
+ cmdSearch(rest[0]);
299
+ break;
300
+ case "append":
301
+ case "add":
302
+ cmdAppend(rest);
303
+ break;
304
+ case "path":
305
+ case "which":
306
+ cmdPath(rest[0]);
307
+ break;
308
+ case "help":
309
+ case "-h":
310
+ case "--help":
311
+ cmdHelp();
312
+ break;
313
+ default:
314
+ // Unknown command → fall through to load with that arg, so
315
+ // `knowledge.js patterns` works as shorthand for `knowledge.js load patterns`.
316
+ cmdLoad(cmd);
317
+ }