@malindar/whyline 0.1.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.
- package/.claude/settings.local.json +33 -0
- package/.github/workflows/ci.yml +35 -0
- package/.github/workflows/publish.yml +37 -0
- package/.prettierrc.json +7 -0
- package/CLAUDE.md +74 -0
- package/LICENSE +21 -0
- package/README.md +359 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +125 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/delete.d.ts +3 -0
- package/dist/commands/delete.js +42 -0
- package/dist/commands/delete.js.map +1 -0
- package/dist/commands/doctor.d.ts +1 -0
- package/dist/commands/doctor.js +111 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/edit.d.ts +1 -0
- package/dist/commands/edit.js +78 -0
- package/dist/commands/edit.js.map +1 -0
- package/dist/commands/export.d.ts +8 -0
- package/dist/commands/export.js +90 -0
- package/dist/commands/export.js.map +1 -0
- package/dist/commands/import.d.ts +1 -0
- package/dist/commands/import.js +110 -0
- package/dist/commands/import.js.map +1 -0
- package/dist/commands/init.d.ts +5 -0
- package/dist/commands/init.js +23 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/install-claude.d.ts +3 -0
- package/dist/commands/install-claude.js +180 -0
- package/dist/commands/install-claude.js.map +1 -0
- package/dist/commands/list.d.ts +4 -0
- package/dist/commands/list.js +35 -0
- package/dist/commands/list.js.map +1 -0
- package/dist/commands/mcp.d.ts +1 -0
- package/dist/commands/mcp.js +10 -0
- package/dist/commands/mcp.js.map +1 -0
- package/dist/commands/save.d.ts +4 -0
- package/dist/commands/save.js +74 -0
- package/dist/commands/save.js.map +1 -0
- package/dist/commands/search.d.ts +7 -0
- package/dist/commands/search.js +46 -0
- package/dist/commands/search.js.map +1 -0
- package/dist/commands/show.d.ts +3 -0
- package/dist/commands/show.js +30 -0
- package/dist/commands/show.js.map +1 -0
- package/dist/commands/stats.d.ts +1 -0
- package/dist/commands/stats.js +27 -0
- package/dist/commands/stats.js.map +1 -0
- package/dist/commands/summarize.d.ts +3 -0
- package/dist/commands/summarize.js +140 -0
- package/dist/commands/summarize.js.map +1 -0
- package/dist/config.d.ts +11 -0
- package/dist/config.js +17 -0
- package/dist/config.js.map +1 -0
- package/dist/db/connection.d.ts +2 -0
- package/dist/db/connection.js +8 -0
- package/dist/db/connection.js.map +1 -0
- package/dist/db/migrations.d.ts +2 -0
- package/dist/db/migrations.js +19 -0
- package/dist/db/migrations.js.map +1 -0
- package/dist/db/schema.d.ts +5 -0
- package/dist/db/schema.js +64 -0
- package/dist/db/schema.js.map +1 -0
- package/dist/git/diff.d.ts +2 -0
- package/dist/git/diff.js +45 -0
- package/dist/git/diff.js.map +1 -0
- package/dist/git/git.d.ts +3 -0
- package/dist/git/git.js +25 -0
- package/dist/git/git.js.map +1 -0
- package/dist/git/repoId.d.ts +3 -0
- package/dist/git/repoId.js +49 -0
- package/dist/git/repoId.js.map +1 -0
- package/dist/mcp/server.d.ts +1 -0
- package/dist/mcp/server.js +296 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/tools.d.ts +119 -0
- package/dist/mcp/tools.js +43 -0
- package/dist/mcp/tools.js.map +1 -0
- package/dist/memory/parseSummary.d.ts +14 -0
- package/dist/memory/parseSummary.js +53 -0
- package/dist/memory/parseSummary.js.map +1 -0
- package/dist/memory/qualityCheck.d.ts +13 -0
- package/dist/memory/qualityCheck.js +78 -0
- package/dist/memory/qualityCheck.js.map +1 -0
- package/dist/memory/redactSecrets.d.ts +7 -0
- package/dist/memory/redactSecrets.js +29 -0
- package/dist/memory/redactSecrets.js.map +1 -0
- package/dist/memory/repoContext.d.ts +2 -0
- package/dist/memory/repoContext.js +23 -0
- package/dist/memory/repoContext.js.map +1 -0
- package/dist/memory/saveMemory.d.ts +40 -0
- package/dist/memory/saveMemory.js +223 -0
- package/dist/memory/saveMemory.js.map +1 -0
- package/dist/memory/searchMemory.d.ts +17 -0
- package/dist/memory/searchMemory.js +122 -0
- package/dist/memory/searchMemory.js.map +1 -0
- package/dist/memory/types.d.ts +48 -0
- package/dist/memory/types.js +2 -0
- package/dist/memory/types.js.map +1 -0
- package/dist/output/format.d.ts +3 -0
- package/dist/output/format.js +43 -0
- package/dist/output/format.js.map +1 -0
- package/docs/architecture.md +387 -0
- package/docs/ec6ab3bf-60cf-4629-ad9e-3048e8e3c43a.png +0 -0
- package/docs/logo.png +0 -0
- package/eslint.config.js +16 -0
- package/how-to-run/01-install.md +69 -0
- package/how-to-run/02-wire-up-your-repo.md +80 -0
- package/how-to-run/03-test-it-manually.md +91 -0
- package/how-to-run/04-test-with-claude-code.md +70 -0
- package/how-to-run/CLAUDE.md.template +72 -0
- package/how-to-run/README.md +49 -0
- package/package.json +60 -0
- package/src/cli.ts +142 -0
- package/src/commands/delete.ts +47 -0
- package/src/commands/doctor.ts +128 -0
- package/src/commands/edit.ts +80 -0
- package/src/commands/export.ts +95 -0
- package/src/commands/import.ts +119 -0
- package/src/commands/init.ts +31 -0
- package/src/commands/install-claude.ts +203 -0
- package/src/commands/list.ts +41 -0
- package/src/commands/mcp.ts +12 -0
- package/src/commands/save.ts +85 -0
- package/src/commands/search.ts +56 -0
- package/src/commands/show.ts +37 -0
- package/src/commands/stats.ts +31 -0
- package/src/commands/summarize.ts +183 -0
- package/src/config.ts +26 -0
- package/src/db/connection.ts +8 -0
- package/src/db/migrations.ts +26 -0
- package/src/db/schema.ts +68 -0
- package/src/git/diff.ts +43 -0
- package/src/git/git.ts +25 -0
- package/src/git/repoId.ts +49 -0
- package/src/hooks/post-commit.sample.sh +9 -0
- package/src/mcp/server.ts +326 -0
- package/src/mcp/tools.ts +53 -0
- package/src/memory/parseSummary.ts +72 -0
- package/src/memory/qualityCheck.ts +102 -0
- package/src/memory/redactSecrets.ts +32 -0
- package/src/memory/repoContext.ts +25 -0
- package/src/memory/saveMemory.ts +369 -0
- package/src/memory/searchMemory.ts +153 -0
- package/src/memory/types.ts +57 -0
- package/src/output/format.ts +44 -0
- package/src/skill/SKILL.md +95 -0
- package/tests/cliV02.test.ts +213 -0
- package/tests/doctor.test.ts +253 -0
- package/tests/exportImport.test.ts +248 -0
- package/tests/fileRename.test.ts +156 -0
- package/tests/gitHelpers.test.ts +94 -0
- package/tests/init.test.ts +93 -0
- package/tests/installClaude.test.ts +157 -0
- package/tests/parseSummary.test.ts +111 -0
- package/tests/qualityCheck.test.ts +182 -0
- package/tests/redactSecrets.test.ts +75 -0
- package/tests/saveMemory.test.ts +196 -0
- package/tests/searchFilters.test.ts +139 -0
- package/tests/searchMemory.test.ts +273 -0
- package/tests/stale.test.ts +47 -0
- package/tsconfig.json +18 -0
- package/vitest.config.ts +8 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import { isInitialized, resolveConfig } from "../config.js";
|
|
3
|
+
import { openDb } from "../db/connection.js";
|
|
4
|
+
import { getRepoRoot } from "../git/git.js";
|
|
5
|
+
import { getRepoId } from "../git/repoId.js";
|
|
6
|
+
import { getAllMemories } from "../memory/saveMemory.js";
|
|
7
|
+
import type { CodingMemory } from "../memory/types.js";
|
|
8
|
+
|
|
9
|
+
function memoryToMarkdown(memory: CodingMemory): string {
|
|
10
|
+
const lines: string[] = [];
|
|
11
|
+
lines.push(`# ${memory.id}`);
|
|
12
|
+
lines.push(`Created: ${memory.createdAt}`);
|
|
13
|
+
if (memory.commitSha) lines.push(`Commit: ${memory.commitSha}`);
|
|
14
|
+
if (memory.repoName) lines.push(`Repo: ${memory.repoName}`);
|
|
15
|
+
if (memory.files.length) lines.push(`Files: ${memory.files.join(", ")}`);
|
|
16
|
+
if (memory.tags.length) lines.push(`Tags: ${memory.tags.join(", ")}`);
|
|
17
|
+
lines.push("");
|
|
18
|
+
if (memory.task) { lines.push("Task:", memory.task, ""); }
|
|
19
|
+
lines.push("Intent:", memory.intent, "");
|
|
20
|
+
lines.push("Summary:", memory.summary, "");
|
|
21
|
+
lines.push("Decision:", memory.decision, "");
|
|
22
|
+
lines.push("Why:", memory.why, "");
|
|
23
|
+
if (memory.alternativesRejected.length) {
|
|
24
|
+
lines.push("Alternatives rejected:");
|
|
25
|
+
for (const a of memory.alternativesRejected) lines.push(`- ${a}`);
|
|
26
|
+
lines.push("");
|
|
27
|
+
}
|
|
28
|
+
if (memory.risks.length) {
|
|
29
|
+
lines.push("Risks:");
|
|
30
|
+
for (const r of memory.risks) lines.push(`- ${r}`);
|
|
31
|
+
lines.push("");
|
|
32
|
+
}
|
|
33
|
+
if (memory.followUps.length) {
|
|
34
|
+
lines.push("Follow-ups:");
|
|
35
|
+
for (const fu of memory.followUps) lines.push(`- ${fu}`);
|
|
36
|
+
lines.push("");
|
|
37
|
+
}
|
|
38
|
+
return lines.join("\n");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function runExport(options: {
|
|
42
|
+
format: string;
|
|
43
|
+
output?: string;
|
|
44
|
+
repo: boolean;
|
|
45
|
+
since?: string;
|
|
46
|
+
before?: string;
|
|
47
|
+
tag: string[];
|
|
48
|
+
}): Promise<void> {
|
|
49
|
+
if (!isInitialized()) {
|
|
50
|
+
console.error("whyline is not initialized. Run `whyline init` first.");
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const db = openDb(resolveConfig().storage.dbPath);
|
|
55
|
+
let memories = getAllMemories(db);
|
|
56
|
+
db.close();
|
|
57
|
+
|
|
58
|
+
if (options.repo) {
|
|
59
|
+
const repoRoot = getRepoRoot(process.cwd());
|
|
60
|
+
if (!repoRoot) {
|
|
61
|
+
console.error("Not inside a git repository.");
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
const repoId = getRepoId(repoRoot);
|
|
65
|
+
memories = memories.filter((m) => m.repoId === repoId);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (options.since) {
|
|
69
|
+
const sinceMs = new Date(options.since).getTime();
|
|
70
|
+
memories = memories.filter((m) => new Date(m.createdAt).getTime() >= sinceMs);
|
|
71
|
+
}
|
|
72
|
+
if (options.before) {
|
|
73
|
+
const beforeMs = new Date(options.before).getTime();
|
|
74
|
+
memories = memories.filter((m) => new Date(m.createdAt).getTime() <= beforeMs);
|
|
75
|
+
}
|
|
76
|
+
if (options.tag.length > 0) {
|
|
77
|
+
const lowerTags = options.tag.map((t) => t.toLowerCase());
|
|
78
|
+
memories = memories.filter((m) => {
|
|
79
|
+
const mTags = m.tags.map((t) => t.toLowerCase());
|
|
80
|
+
return lowerTags.every((t) => mTags.includes(t));
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const output =
|
|
85
|
+
options.format === "md"
|
|
86
|
+
? memories.map(memoryToMarkdown).join("\n---\n\n")
|
|
87
|
+
: JSON.stringify({ schemaVersion: 1, memories }, null, 2);
|
|
88
|
+
|
|
89
|
+
if (options.output) {
|
|
90
|
+
fs.writeFileSync(options.output, output, "utf-8");
|
|
91
|
+
console.error(`Exported ${memories.length} memory(s) to ${options.output}`);
|
|
92
|
+
} else {
|
|
93
|
+
process.stdout.write(output + "\n");
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import { isInitialized, resolveConfig } from "../config.js";
|
|
3
|
+
import { openDb } from "../db/connection.js";
|
|
4
|
+
import { saveMemory, getMemoryById } from "../memory/saveMemory.js";
|
|
5
|
+
import { redactSecrets } from "../memory/redactSecrets.js";
|
|
6
|
+
import type { CodingMemory } from "../memory/types.js";
|
|
7
|
+
|
|
8
|
+
const VALID_SOURCES = new Set(["manual", "claude-code", "cli", "hook"]);
|
|
9
|
+
|
|
10
|
+
function validateMemory(obj: unknown): CodingMemory | null {
|
|
11
|
+
if (!obj || typeof obj !== "object") return null;
|
|
12
|
+
const m = obj as Record<string, unknown>;
|
|
13
|
+
if (
|
|
14
|
+
typeof m.id !== "string" ||
|
|
15
|
+
typeof m.createdAt !== "string" ||
|
|
16
|
+
typeof m.updatedAt !== "string" ||
|
|
17
|
+
typeof m.repoId !== "string" ||
|
|
18
|
+
typeof m.intent !== "string" ||
|
|
19
|
+
typeof m.summary !== "string" ||
|
|
20
|
+
typeof m.decision !== "string" ||
|
|
21
|
+
typeof m.why !== "string" ||
|
|
22
|
+
!Array.isArray(m.files) ||
|
|
23
|
+
!Array.isArray(m.tags) ||
|
|
24
|
+
!Array.isArray(m.alternativesRejected) ||
|
|
25
|
+
!Array.isArray(m.risks) ||
|
|
26
|
+
!Array.isArray(m.followUps) ||
|
|
27
|
+
typeof m.embeddingText !== "string"
|
|
28
|
+
) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
const source = VALID_SOURCES.has(m.source as string)
|
|
32
|
+
? (m.source as CodingMemory["source"])
|
|
33
|
+
: "manual";
|
|
34
|
+
return {
|
|
35
|
+
id: m.id,
|
|
36
|
+
createdAt: m.createdAt,
|
|
37
|
+
updatedAt: m.updatedAt,
|
|
38
|
+
repoId: m.repoId,
|
|
39
|
+
repoPath: typeof m.repoPath === "string" ? m.repoPath : undefined,
|
|
40
|
+
repoName: typeof m.repoName === "string" ? m.repoName : undefined,
|
|
41
|
+
branch: typeof m.branch === "string" ? m.branch : undefined,
|
|
42
|
+
commitSha: typeof m.commitSha === "string" ? m.commitSha : undefined,
|
|
43
|
+
task: typeof m.task === "string" ? m.task : undefined,
|
|
44
|
+
intent: redactSecrets(m.intent),
|
|
45
|
+
summary: redactSecrets(m.summary),
|
|
46
|
+
decision: redactSecrets(m.decision),
|
|
47
|
+
why: redactSecrets(m.why),
|
|
48
|
+
files: (m.files as unknown[]).filter((f): f is string => typeof f === "string"),
|
|
49
|
+
tags: (m.tags as unknown[]).filter((t): t is string => typeof t === "string"),
|
|
50
|
+
alternativesRejected: (m.alternativesRejected as unknown[]).filter((a): a is string => typeof a === "string").map(redactSecrets),
|
|
51
|
+
risks: (m.risks as unknown[]).filter((r): r is string => typeof r === "string").map(redactSecrets),
|
|
52
|
+
followUps: (m.followUps as unknown[]).filter((f): f is string => typeof f === "string").map(redactSecrets),
|
|
53
|
+
source,
|
|
54
|
+
embeddingText: m.embeddingText,
|
|
55
|
+
rawTranscriptPath: typeof m.rawTranscriptPath === "string" ? m.rawTranscriptPath : undefined,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function runImport(filePath: string): Promise<void> {
|
|
60
|
+
if (!isInitialized()) {
|
|
61
|
+
console.error("whyline is not initialized. Run `whyline init` first.");
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let raw: string;
|
|
66
|
+
try {
|
|
67
|
+
raw = fs.readFileSync(filePath, "utf-8");
|
|
68
|
+
} catch {
|
|
69
|
+
console.error(`Cannot read file: ${filePath}`);
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
let parsed: unknown;
|
|
74
|
+
try {
|
|
75
|
+
parsed = JSON.parse(raw);
|
|
76
|
+
} catch {
|
|
77
|
+
console.error("File is not valid JSON. Only JSON exports are supported for import.");
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Accept both v1 envelope { schemaVersion, memories } and legacy bare arrays
|
|
82
|
+
let items: unknown[];
|
|
83
|
+
if (Array.isArray(parsed)) {
|
|
84
|
+
items = parsed;
|
|
85
|
+
} else if (
|
|
86
|
+
parsed &&
|
|
87
|
+
typeof parsed === "object" &&
|
|
88
|
+
"memories" in (parsed as object) &&
|
|
89
|
+
Array.isArray((parsed as Record<string, unknown>).memories)
|
|
90
|
+
) {
|
|
91
|
+
items = (parsed as Record<string, unknown>).memories as unknown[];
|
|
92
|
+
} else {
|
|
93
|
+
items = [parsed];
|
|
94
|
+
}
|
|
95
|
+
const db = openDb(resolveConfig().storage.dbPath);
|
|
96
|
+
|
|
97
|
+
let imported = 0;
|
|
98
|
+
let skipped = 0;
|
|
99
|
+
let invalid = 0;
|
|
100
|
+
|
|
101
|
+
for (const item of items) {
|
|
102
|
+
const memory = validateMemory(item);
|
|
103
|
+
if (!memory) {
|
|
104
|
+
invalid++;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
const existing = getMemoryById(db, memory.id);
|
|
108
|
+
if (existing) {
|
|
109
|
+
skipped++;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
saveMemory(db, memory);
|
|
113
|
+
imported++;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
db.close();
|
|
117
|
+
|
|
118
|
+
console.log(`Import complete: ${imported} imported, ${skipped} skipped (already exist), ${invalid} invalid.`);
|
|
119
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { DATA_DIR, DB_PATH, CONFIG_PATH, AppConfig } from "../config.js";
|
|
4
|
+
import { openDb } from "../db/connection.js";
|
|
5
|
+
import { runMigrations } from "../db/migrations.js";
|
|
6
|
+
|
|
7
|
+
type InitOptions = {
|
|
8
|
+
dataDir?: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function runInit(options: InitOptions = {}): void {
|
|
12
|
+
const dataDir = options.dataDir ?? DATA_DIR;
|
|
13
|
+
const dbPath = options.dataDir ? path.join(options.dataDir, "memory.db") : DB_PATH;
|
|
14
|
+
const configPath = options.dataDir ? path.join(options.dataDir, "config.json") : CONFIG_PATH;
|
|
15
|
+
|
|
16
|
+
fs.mkdirSync(dataDir, { recursive: true });
|
|
17
|
+
|
|
18
|
+
if (!fs.existsSync(configPath)) {
|
|
19
|
+
const config: AppConfig = {
|
|
20
|
+
version: 1,
|
|
21
|
+
storage: { dbPath },
|
|
22
|
+
};
|
|
23
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const db = openDb(dbPath);
|
|
27
|
+
runMigrations(db);
|
|
28
|
+
db.close();
|
|
29
|
+
|
|
30
|
+
console.log(`Initialized whyline at ${dataDir}`);
|
|
31
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { getRepoRoot } from "../git/git.js";
|
|
4
|
+
import { getRepoName } from "../git/repoId.js";
|
|
5
|
+
|
|
6
|
+
const MCP_TOOL_PERMISSIONS = [
|
|
7
|
+
"mcp__whyline__save_coding_memory",
|
|
8
|
+
"mcp__whyline__search_coding_memory",
|
|
9
|
+
"mcp__whyline__get_recent_memories",
|
|
10
|
+
"mcp__whyline__get_file_memories",
|
|
11
|
+
"mcp__whyline__get_commit_memory",
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
function claudeMdSection(repoPath: string): string {
|
|
15
|
+
return `
|
|
16
|
+
## Whyline Memory
|
|
17
|
+
|
|
18
|
+
You have access to a \`whyline\` MCP server. Use it every session.
|
|
19
|
+
|
|
20
|
+
### When you start working on ANY task
|
|
21
|
+
|
|
22
|
+
**Before asking any clarifying question or touching any file**, search for past context:
|
|
23
|
+
|
|
24
|
+
- If the task is clearly described: call \`search_coding_memory\` with:
|
|
25
|
+
- \`repoPath\`: \`${repoPath}\`
|
|
26
|
+
- \`query\`: the task or feature the user just described
|
|
27
|
+
- \`files\`: any files you already know are relevant
|
|
28
|
+
|
|
29
|
+
- If the task is vague or just starting out: call \`get_recent_memories\` with:
|
|
30
|
+
- \`repoPath\`: \`${repoPath}\`
|
|
31
|
+
- \`limit\`: 5
|
|
32
|
+
|
|
33
|
+
**If memories come back**, you MUST:
|
|
34
|
+
1. Show the user what was previously decided and why: _"I found a previous memory about this: [decision + reason]. Before I proceed — what's the reason for changing it now?"_
|
|
35
|
+
2. Wait for the user to give a reason before asking implementation questions
|
|
36
|
+
3. Record the new reason when saving the updated memory
|
|
37
|
+
|
|
38
|
+
Do not skip straight to implementation questions when a past memory exists for the same area. The reason matters — it goes into the next memory.
|
|
39
|
+
|
|
40
|
+
Treat memories as historical context — they explain past decisions, not current truth.
|
|
41
|
+
|
|
42
|
+
### After you commit
|
|
43
|
+
|
|
44
|
+
After \`git commit\` succeeds:
|
|
45
|
+
|
|
46
|
+
1. Synthesize from the conversation:
|
|
47
|
+
- What was the goal? → \`intent\`
|
|
48
|
+
- What was the key decision? → \`decision\`
|
|
49
|
+
- Why that decision (not another)? → \`why\`
|
|
50
|
+
- What alternatives were rejected? → \`alternativesRejected\`
|
|
51
|
+
- What risks exist? → \`risks\`
|
|
52
|
+
- What should be done next? → \`followUps\`
|
|
53
|
+
|
|
54
|
+
2. Show the summary to the user:
|
|
55
|
+
_"Here's what I'm saving as a coding memory — let me know if you want to add or correct anything:"_
|
|
56
|
+
Then display each field clearly.
|
|
57
|
+
|
|
58
|
+
3. Wait a moment for the user to respond. If they add or correct something, apply it.
|
|
59
|
+
|
|
60
|
+
4. Call \`save_coding_memory\` with:
|
|
61
|
+
- \`repoPath\`: \`${repoPath}\`
|
|
62
|
+
- \`commitSha\`: the commit SHA (use HEAD)
|
|
63
|
+
- \`files\`: files changed in this session
|
|
64
|
+
- \`source\`: \`"claude-code"\`
|
|
65
|
+
- all synthesized fields above
|
|
66
|
+
|
|
67
|
+
5. If the response contains a non-empty \`warnings\` array, show each warning to the user and offer to update the memory with richer detail.
|
|
68
|
+
|
|
69
|
+
### Memory quality rules
|
|
70
|
+
|
|
71
|
+
Only save memories that would genuinely help a future session. Good memory:
|
|
72
|
+
- Explains a non-obvious decision
|
|
73
|
+
- Warns about a real risk
|
|
74
|
+
- Records a rejected alternative that someone will try again
|
|
75
|
+
|
|
76
|
+
Do NOT save:
|
|
77
|
+
- Routine refactors with no tradeoffs
|
|
78
|
+
- Things obvious from reading the code
|
|
79
|
+
- Secrets or credentials
|
|
80
|
+
`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function writeMcpJson(repoRoot: string): "created" | "updated" | "unchanged" {
|
|
84
|
+
const mcpPath = path.join(repoRoot, ".mcp.json");
|
|
85
|
+
const whylineEntry = { command: "whyline", args: ["mcp"] };
|
|
86
|
+
|
|
87
|
+
let existing: Record<string, unknown> = {};
|
|
88
|
+
if (fs.existsSync(mcpPath)) {
|
|
89
|
+
try {
|
|
90
|
+
existing = JSON.parse(fs.readFileSync(mcpPath, "utf-8")) as Record<string, unknown>;
|
|
91
|
+
} catch {
|
|
92
|
+
// invalid JSON — overwrite
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const servers = (existing.mcpServers ?? {}) as Record<string, unknown>;
|
|
97
|
+
const alreadyThere =
|
|
98
|
+
JSON.stringify((servers.whyline ?? null)) === JSON.stringify(whylineEntry);
|
|
99
|
+
|
|
100
|
+
if (alreadyThere) return "unchanged";
|
|
101
|
+
|
|
102
|
+
servers.whyline = whylineEntry;
|
|
103
|
+
existing.mcpServers = servers;
|
|
104
|
+
fs.writeFileSync(mcpPath, JSON.stringify(existing, null, 2) + "\n", "utf-8");
|
|
105
|
+
return fs.existsSync(mcpPath) && Object.keys(servers).length > 1 ? "updated" : "created";
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function writeClaudeMd(repoRoot: string, repoPath: string): "created" | "updated" | "unchanged" {
|
|
109
|
+
const claudePath = path.join(repoRoot, "CLAUDE.md");
|
|
110
|
+
const section = claudeMdSection(repoPath);
|
|
111
|
+
|
|
112
|
+
if (!fs.existsSync(claudePath)) {
|
|
113
|
+
const repoName = getRepoName(repoRoot);
|
|
114
|
+
const header = `# Claude Instructions for ${repoName}\n`;
|
|
115
|
+
fs.writeFileSync(claudePath, header + section, "utf-8");
|
|
116
|
+
return "created";
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const existing = fs.readFileSync(claudePath, "utf-8");
|
|
120
|
+
|
|
121
|
+
// Already has the section — update repoPath lines in place
|
|
122
|
+
if (/whyline/i.test(existing)) {
|
|
123
|
+
const updated = existing.replace(
|
|
124
|
+
/(`repoPath`:\s*`)([^`]+)(`)/g,
|
|
125
|
+
`$1${repoPath}$3`
|
|
126
|
+
);
|
|
127
|
+
if (updated === existing) return "unchanged";
|
|
128
|
+
fs.writeFileSync(claudePath, updated, "utf-8");
|
|
129
|
+
return "updated";
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Exists but no Whyline section — append
|
|
133
|
+
fs.writeFileSync(claudePath, existing.trimEnd() + "\n" + section, "utf-8");
|
|
134
|
+
return "updated";
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function writeSettingsJson(repoRoot: string): "created" | "updated" | "unchanged" {
|
|
138
|
+
const settingsDir = path.join(repoRoot, ".claude");
|
|
139
|
+
const settingsPath = path.join(settingsDir, "settings.local.json");
|
|
140
|
+
|
|
141
|
+
let existing: Record<string, unknown> = {};
|
|
142
|
+
if (fs.existsSync(settingsPath)) {
|
|
143
|
+
try {
|
|
144
|
+
existing = JSON.parse(fs.readFileSync(settingsPath, "utf-8")) as Record<string, unknown>;
|
|
145
|
+
} catch {
|
|
146
|
+
// invalid JSON — overwrite
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const perms = (existing.permissions ?? {}) as Record<string, unknown>;
|
|
151
|
+
const allowed = Array.isArray(perms.allow) ? (perms.allow as string[]) : [];
|
|
152
|
+
const enabledServers = Array.isArray(existing.enabledMcpjsonServers)
|
|
153
|
+
? (existing.enabledMcpjsonServers as string[])
|
|
154
|
+
: [];
|
|
155
|
+
|
|
156
|
+
const newAllowed = [...new Set([...allowed, ...MCP_TOOL_PERMISSIONS])];
|
|
157
|
+
const newEnabled = [...new Set([...enabledServers, "whyline"])];
|
|
158
|
+
|
|
159
|
+
const unchanged =
|
|
160
|
+
newAllowed.length === allowed.length && newEnabled.length === enabledServers.length;
|
|
161
|
+
if (unchanged) return "unchanged";
|
|
162
|
+
|
|
163
|
+
perms.allow = newAllowed;
|
|
164
|
+
existing.permissions = perms;
|
|
165
|
+
existing.enabledMcpjsonServers = newEnabled;
|
|
166
|
+
|
|
167
|
+
if (!fs.existsSync(settingsDir)) fs.mkdirSync(settingsDir, { recursive: true });
|
|
168
|
+
const wasNew = !fs.existsSync(settingsPath);
|
|
169
|
+
fs.writeFileSync(settingsPath, JSON.stringify(existing, null, 2) + "\n", "utf-8");
|
|
170
|
+
return wasNew ? "created" : "updated";
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export async function runInstallClaude(options: { repoPath?: string }): Promise<void> {
|
|
174
|
+
const cwd = options.repoPath ?? process.cwd();
|
|
175
|
+
const repoRoot = getRepoRoot(cwd);
|
|
176
|
+
|
|
177
|
+
if (!repoRoot) {
|
|
178
|
+
console.error("Not inside a git repository. Run this command from your project root.");
|
|
179
|
+
process.exit(1);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const repoPath = repoRoot;
|
|
183
|
+
|
|
184
|
+
const mcpStatus = writeMcpJson(repoRoot);
|
|
185
|
+
const claudeStatus = writeClaudeMd(repoRoot, repoPath);
|
|
186
|
+
const settingsStatus = writeSettingsJson(repoRoot);
|
|
187
|
+
|
|
188
|
+
const label = (status: string, file: string) => {
|
|
189
|
+
const icon = status === "unchanged" ? "·" : "✓";
|
|
190
|
+
return ` ${icon} ${file} (${status})`;
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
console.log(label(mcpStatus, ".mcp.json"));
|
|
194
|
+
console.log(label(claudeStatus, "CLAUDE.md"));
|
|
195
|
+
console.log(label(settingsStatus, ".claude/settings.local.json"));
|
|
196
|
+
console.log("");
|
|
197
|
+
|
|
198
|
+
if (mcpStatus === "unchanged" && claudeStatus === "unchanged" && settingsStatus === "unchanged") {
|
|
199
|
+
console.log("Whyline is already configured in this repo.");
|
|
200
|
+
} else {
|
|
201
|
+
console.log("Done. Open this repo in Claude Code and run `whyline doctor` to verify.");
|
|
202
|
+
}
|
|
203
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { isInitialized, resolveConfig } from "../config.js";
|
|
2
|
+
import { openDb } from "../db/connection.js";
|
|
3
|
+
import { getRepoRoot } from "../git/git.js";
|
|
4
|
+
import { getRepoId } from "../git/repoId.js";
|
|
5
|
+
import { listMemories } from "../memory/saveMemory.js";
|
|
6
|
+
import { formatMemory } from "../output/format.js";
|
|
7
|
+
|
|
8
|
+
export async function runList(options: { repo: boolean; limit: string }): Promise<void> {
|
|
9
|
+
if (!isInitialized()) {
|
|
10
|
+
console.error("whyline is not initialized. Run `whyline init` first.");
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const limit = parseInt(options.limit, 10) || 20;
|
|
15
|
+
let repoId: string | undefined;
|
|
16
|
+
|
|
17
|
+
if (options.repo) {
|
|
18
|
+
const repoRoot = getRepoRoot(process.cwd());
|
|
19
|
+
if (!repoRoot) {
|
|
20
|
+
console.error("Not inside a git repository.");
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
repoId = getRepoId(repoRoot);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const db = openDb(resolveConfig().storage.dbPath);
|
|
27
|
+
const memories = listMemories(db, { repoId, limit });
|
|
28
|
+
db.close();
|
|
29
|
+
|
|
30
|
+
if (memories.length === 0) {
|
|
31
|
+
console.log("No memories found.");
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
for (const memory of memories) {
|
|
36
|
+
console.log(formatMemory(memory));
|
|
37
|
+
console.log("\n---");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
console.log(`\n${memories.length} memory(s) shown.`);
|
|
41
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { isInitialized } from "../config.js";
|
|
2
|
+
import { createMcpServer } from "../mcp/server.js";
|
|
3
|
+
|
|
4
|
+
export async function runMcp(): Promise<void> {
|
|
5
|
+
if (!isInitialized()) {
|
|
6
|
+
process.stderr.write(
|
|
7
|
+
"coding-memory is not initialized. Run `coding-memory init` first.\n"
|
|
8
|
+
);
|
|
9
|
+
process.exit(1);
|
|
10
|
+
}
|
|
11
|
+
await createMcpServer();
|
|
12
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import { isInitialized, resolveConfig } from "../config.js";
|
|
3
|
+
import { openDb } from "../db/connection.js";
|
|
4
|
+
import { getRepoContext } from "../memory/repoContext.js";
|
|
5
|
+
import { parseSummary } from "../memory/parseSummary.js";
|
|
6
|
+
import { redactSecrets } from "../memory/redactSecrets.js";
|
|
7
|
+
import { saveMemory, generateMemoryId, buildEmbeddingText } from "../memory/saveMemory.js";
|
|
8
|
+
import { checkQuality, checkDuplicates } from "../memory/qualityCheck.js";
|
|
9
|
+
import type { CodingMemory } from "../memory/types.js";
|
|
10
|
+
|
|
11
|
+
export async function runSave(options: { commit: string; summaryFile: string }): Promise<void> {
|
|
12
|
+
if (!isInitialized()) {
|
|
13
|
+
console.error("whyline is not initialized. Run `whyline init` first.");
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const cwd = process.cwd();
|
|
18
|
+
|
|
19
|
+
let ctx;
|
|
20
|
+
try {
|
|
21
|
+
ctx = getRepoContext(cwd, options.commit);
|
|
22
|
+
} catch (err) {
|
|
23
|
+
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (!fs.existsSync(options.summaryFile)) {
|
|
28
|
+
console.error(`Error: summary file not found: ${options.summaryFile}`);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const rawMarkdown = fs.readFileSync(options.summaryFile, "utf-8");
|
|
33
|
+
const parsed = parseSummary(rawMarkdown);
|
|
34
|
+
|
|
35
|
+
const now = new Date().toISOString();
|
|
36
|
+
const id = generateMemoryId();
|
|
37
|
+
|
|
38
|
+
const memory: CodingMemory = {
|
|
39
|
+
id,
|
|
40
|
+
createdAt: now,
|
|
41
|
+
updatedAt: now,
|
|
42
|
+
repoId: ctx.repoId,
|
|
43
|
+
repoPath: ctx.repoPath,
|
|
44
|
+
repoName: ctx.repoName,
|
|
45
|
+
branch: ctx.branch ?? undefined,
|
|
46
|
+
commitSha: ctx.commitSha,
|
|
47
|
+
files: ctx.changedFiles,
|
|
48
|
+
tags: parsed.tags.map(redactSecrets),
|
|
49
|
+
task: parsed.task ? redactSecrets(parsed.task) : undefined,
|
|
50
|
+
intent: redactSecrets(parsed.intent),
|
|
51
|
+
summary: redactSecrets(parsed.summary),
|
|
52
|
+
decision: redactSecrets(parsed.decision),
|
|
53
|
+
why: redactSecrets(parsed.why),
|
|
54
|
+
alternativesRejected: parsed.alternativesRejected.map(redactSecrets),
|
|
55
|
+
risks: parsed.risks.map(redactSecrets),
|
|
56
|
+
followUps: parsed.followUps.map(redactSecrets),
|
|
57
|
+
source: "cli",
|
|
58
|
+
embeddingText: "",
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
memory.embeddingText = buildEmbeddingText(memory);
|
|
62
|
+
|
|
63
|
+
const db = openDb(resolveConfig().storage.dbPath);
|
|
64
|
+
|
|
65
|
+
const qualityWarnings = checkQuality(memory);
|
|
66
|
+
const duplicateWarnings = checkDuplicates(db, memory);
|
|
67
|
+
|
|
68
|
+
saveMemory(db, memory);
|
|
69
|
+
db.close();
|
|
70
|
+
|
|
71
|
+
console.log(`Saved memory ${memory.id}`);
|
|
72
|
+
console.log(`Repo: ${ctx.repoName}`);
|
|
73
|
+
console.log(`Commit: ${ctx.commitSha.slice(0, 8)}`);
|
|
74
|
+
console.log(`Files: ${ctx.changedFiles.length}`);
|
|
75
|
+
|
|
76
|
+
if (qualityWarnings.length > 0) {
|
|
77
|
+
console.log("\nQuality warnings:");
|
|
78
|
+
for (const w of qualityWarnings) console.warn(` ⚠ ${w.message}`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (duplicateWarnings.length > 0) {
|
|
82
|
+
console.log("\nDuplicate warnings:");
|
|
83
|
+
for (const w of duplicateWarnings) console.warn(` ⚠ ${w.message}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { isInitialized, resolveConfig } from "../config.js";
|
|
2
|
+
import { openDb } from "../db/connection.js";
|
|
3
|
+
import { getRepoRoot } from "../git/git.js";
|
|
4
|
+
import { getRepoId } from "../git/repoId.js";
|
|
5
|
+
import { getFileRenameHistory } from "../git/diff.js";
|
|
6
|
+
import { searchMemory } from "../memory/searchMemory.js";
|
|
7
|
+
import { formatSearchResult } from "../output/format.js";
|
|
8
|
+
|
|
9
|
+
export async function runSearch(
|
|
10
|
+
query: string,
|
|
11
|
+
options: { file?: string; tag?: string[]; since?: string; before?: string; limit: string }
|
|
12
|
+
): Promise<void> {
|
|
13
|
+
if (!isInitialized()) {
|
|
14
|
+
console.error("whyline is not initialized. Run `whyline init` first.");
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const limit = parseInt(options.limit, 10) || 10;
|
|
19
|
+
const cwd = process.cwd();
|
|
20
|
+
|
|
21
|
+
let repoId: string | undefined;
|
|
22
|
+
let repoPath: string | undefined;
|
|
23
|
+
|
|
24
|
+
const repoRoot = getRepoRoot(cwd);
|
|
25
|
+
if (repoRoot) {
|
|
26
|
+
repoId = getRepoId(repoRoot);
|
|
27
|
+
repoPath = repoRoot;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const files = options.file
|
|
31
|
+
? (repoPath ? getFileRenameHistory(repoPath, options.file) : [options.file])
|
|
32
|
+
: [];
|
|
33
|
+
|
|
34
|
+
const db = openDb(resolveConfig().storage.dbPath);
|
|
35
|
+
const results = searchMemory(db, {
|
|
36
|
+
query,
|
|
37
|
+
repoId,
|
|
38
|
+
repoPath,
|
|
39
|
+
files,
|
|
40
|
+
tags: options.tag ?? [],
|
|
41
|
+
since: options.since,
|
|
42
|
+
before: options.before,
|
|
43
|
+
limit,
|
|
44
|
+
});
|
|
45
|
+
db.close();
|
|
46
|
+
|
|
47
|
+
if (results.length === 0) {
|
|
48
|
+
console.log("No memories found.");
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
for (const result of results) {
|
|
53
|
+
console.log(formatSearchResult(result));
|
|
54
|
+
console.log("\n---");
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { isInitialized, resolveConfig } from "../config.js";
|
|
2
|
+
import { openDb } from "../db/connection.js";
|
|
3
|
+
import { getMemoryById, getMemoryByCommit } from "../memory/saveMemory.js";
|
|
4
|
+
import { formatMemory } from "../output/format.js";
|
|
5
|
+
|
|
6
|
+
export async function runShow(
|
|
7
|
+
id: string | undefined,
|
|
8
|
+
options: { commit?: string }
|
|
9
|
+
): Promise<void> {
|
|
10
|
+
if (!isInitialized()) {
|
|
11
|
+
console.error("whyline is not initialized. Run `whyline init` first.");
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const db = openDb(resolveConfig().storage.dbPath);
|
|
16
|
+
|
|
17
|
+
let memory = null;
|
|
18
|
+
|
|
19
|
+
if (options.commit) {
|
|
20
|
+
memory = getMemoryByCommit(db, options.commit);
|
|
21
|
+
} else if (id) {
|
|
22
|
+
memory = getMemoryById(db, id);
|
|
23
|
+
} else {
|
|
24
|
+
db.close();
|
|
25
|
+
console.error("Error: provide a memory ID or use --commit <sha>");
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
db.close();
|
|
30
|
+
|
|
31
|
+
if (!memory) {
|
|
32
|
+
console.error(`Memory not found: ${options.commit ?? id}`);
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
console.log(formatMemory(memory, true));
|
|
37
|
+
}
|