decision-memory 0.1.0 → 0.1.2
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/README.md +174 -0
- package/dist/chunk-5VFZ3YOK.js +428 -0
- package/dist/index.js +3024 -10
- package/dist/mcp.js +20898 -25
- package/package.json +6 -2
package/README.md
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# decision-memory
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/decision-memory)
|
|
4
|
+
|
|
5
|
+
**Automatic decision logging for Claude Code.** Every architectural choice your AI assistant makes gets stored in a compact, token-efficient [TOON](https://github.com/toon-format/toon) file — and retrieved automatically when context is lost.
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
decisions[3]{id,ts,topic,decision,rationale,impact,tags}:
|
|
9
|
+
D001,2026-02-10T14:32Z,auth,"Use JWT RS256","HS256 needs shared secret across services",high,auth|security|jwt
|
|
10
|
+
D002,2026-02-11T09:15Z,database,"Use Postgres","Need concurrent writes; SQLite WAL insufficient",high,database|postgres
|
|
11
|
+
D003,2026-02-12T16:44Z,testing,"Use Vitest","ESM-native project; Jest ESM support is experimental",low,testing|vitest
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Why decision-memory?
|
|
15
|
+
|
|
16
|
+
- **Context loss is real.** Long sessions and session restarts mean Claude forgets why `JWT RS256` was chosen over `HS256` three days ago.
|
|
17
|
+
- **Re-discussing decided things wastes time.** Every re-debate costs tokens and slows development.
|
|
18
|
+
- **Decisions deserve a home.** Like `CHANGELOG.md` for architecture.
|
|
19
|
+
|
|
20
|
+
## How it works
|
|
21
|
+
|
|
22
|
+
1. **Auto-trigger**: A Claude Code hook fires after every `Write`/`Edit` operation, nudging Claude to log decisions automatically.
|
|
23
|
+
2. **Session start**: Claude calls `get_context_summary` to orient itself (~200 tokens).
|
|
24
|
+
3. **Before deciding**: Claude calls `search_decisions` to check prior decisions (~50-80 tokens/result).
|
|
25
|
+
4. **After deciding**: Claude calls `log_decision` to record the choice.
|
|
26
|
+
|
|
27
|
+
No manual intervention needed.
|
|
28
|
+
|
|
29
|
+
## Quickstart (Claude Code)
|
|
30
|
+
|
|
31
|
+
### 1. Copy integration files to your project
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npx degit baadir/decisionmemo/integrations/claude-code . --force
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
This pulls `.mcp.json`, `CLAUDE.md` and `.claude/` directly into your project root — no cloning, no manual copying.
|
|
38
|
+
|
|
39
|
+
### 2. Initialize DECISIONS.toon
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
npx decision-memory init
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### 3. Start Claude Code
|
|
46
|
+
|
|
47
|
+
Claude will automatically:
|
|
48
|
+
- Call `get_context_summary` at session start
|
|
49
|
+
- Be nudged to call `log_decision` after file modifications
|
|
50
|
+
- Call `search_decisions` before making architectural choices
|
|
51
|
+
|
|
52
|
+
## MCP Server
|
|
53
|
+
|
|
54
|
+
The MCP server is the primary integration method. It exposes 4 tools:
|
|
55
|
+
|
|
56
|
+
| Tool | Description |
|
|
57
|
+
|---|---|
|
|
58
|
+
| `log_decision` | Log an architectural decision |
|
|
59
|
+
| `search_decisions` | Search past decisions by keyword/tag |
|
|
60
|
+
| `get_context_summary` | Get compact session-start summary |
|
|
61
|
+
| `update_decision` | Mark a prior decision as superseded |
|
|
62
|
+
|
|
63
|
+
## CLI
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
# Initialize
|
|
67
|
+
npx decision-memory init
|
|
68
|
+
|
|
69
|
+
# Log a decision
|
|
70
|
+
npx decision-memory log \
|
|
71
|
+
--topic auth \
|
|
72
|
+
--decision "Use JWT RS256" \
|
|
73
|
+
--rationale "HS256 requires shared secret across services" \
|
|
74
|
+
--impact high \
|
|
75
|
+
--tags auth,security,jwt
|
|
76
|
+
|
|
77
|
+
# Search
|
|
78
|
+
npx decision-memory search --keywords jwt
|
|
79
|
+
npx decision-memory search --tags auth,security
|
|
80
|
+
npx decision-memory search --impact high
|
|
81
|
+
|
|
82
|
+
# Summary
|
|
83
|
+
npx decision-memory summary
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Why TOON format?
|
|
87
|
+
|
|
88
|
+
TOON (Token-Oriented Object Notation) uses **39% fewer tokens than JSON** for the same structured data. For a project with 50 decisions:
|
|
89
|
+
|
|
90
|
+
- JSON: ~4,250 tokens
|
|
91
|
+
- TOON: ~2,600 tokens
|
|
92
|
+
- Savings: **1,650 tokens per search** that's not consumed
|
|
93
|
+
|
|
94
|
+
## DECISIONS.toon schema
|
|
95
|
+
|
|
96
|
+
```
|
|
97
|
+
# decision-memory v1
|
|
98
|
+
project: <name>
|
|
99
|
+
created: YYYY-MM-DD
|
|
100
|
+
updated: YYYY-MM-DD
|
|
101
|
+
|
|
102
|
+
decisions[N]{id,ts,topic,decision,rationale,impact,tags}:
|
|
103
|
+
D001,YYYY-MM-DDTHH:MMZ,topic,"Decision text","Rationale text",impact,tag1|tag2
|
|
104
|
+
|
|
105
|
+
summary{total,high_impact,last_updated,top_topics}:
|
|
106
|
+
N,N,YYYY-MM-DD,topic1|topic2
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
**Fields:**
|
|
110
|
+
- `impact`: `low` | `medium` | `high` | `critical`
|
|
111
|
+
- `tags`: pipe-delimited (`auth|jwt|security`)
|
|
112
|
+
- Fields with commas are quoted: `"Use JWT, not sessions"`
|
|
113
|
+
|
|
114
|
+
## File location
|
|
115
|
+
|
|
116
|
+
Priority order:
|
|
117
|
+
1. `DECISION_MEMORY_FILE` environment variable
|
|
118
|
+
2. `./DECISIONS.toon` in current working directory ← **recommended**
|
|
119
|
+
3. `~/.decision-memory/global.toon` (fallback)
|
|
120
|
+
|
|
121
|
+
Commit `DECISIONS.toon` to git — it's a project artifact like `README.md`.
|
|
122
|
+
|
|
123
|
+
## Package
|
|
124
|
+
|
|
125
|
+
Single npm package — CLI and MCP server in one:
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
npm install -g decision-memory # CLI
|
|
129
|
+
npx decision-memory init # run without installing
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
The MCP server binary (`decision-memory-mcp`) is included in the same package and started automatically via `.mcp.json`.
|
|
133
|
+
|
|
134
|
+
## How does this compare to Claude Code's MEMORY.md?
|
|
135
|
+
|
|
136
|
+
Claude Code has three places where context lives. They don't overlap — they complement each other:
|
|
137
|
+
|
|
138
|
+
| | `MEMORY.md` | `CLAUDE.md` | `DECISIONS.toon` |
|
|
139
|
+
|---|---|---|---|
|
|
140
|
+
| **Written by** | Claude (auto-generated) | You (human) | Claude (via MCP tool) |
|
|
141
|
+
| **Contains** | Discoveries, patterns, debugging notes | Behavioral rules & instructions | Architectural decisions with rationale |
|
|
142
|
+
| **Searchable** | No | No | Yes — keyword + tag search |
|
|
143
|
+
| **Location** | `~/.claude/projects/.../memory/` | Project root | Project root |
|
|
144
|
+
| **Format** | Markdown | Markdown | TOON (39% fewer tokens) |
|
|
145
|
+
|
|
146
|
+
**In plain terms:**
|
|
147
|
+
- MEMORY.md is Claude's personal notebook ("I noticed this codebase uses X pattern")
|
|
148
|
+
- CLAUDE.md is your instruction manual for Claude ("always call get_context_summary at start")
|
|
149
|
+
- DECISIONS.toon is the project's architecture log ("we chose Postgres over SQLite because...")
|
|
150
|
+
|
|
151
|
+
All three can be active at once. decision-memory adds the one thing neither MEMORY.md nor CLAUDE.md provides: **searchable, structured, rationale-rich decision history**.
|
|
152
|
+
|
|
153
|
+
## Compatibility
|
|
154
|
+
|
|
155
|
+
decision-memory is compatible with any other MCP server or Claude Code tool. It only reads and writes `DECISIONS.toon` — it does not interfere with other tools.
|
|
156
|
+
|
|
157
|
+
**Execution order when Claude Code starts:**
|
|
158
|
+
1. `.mcp.json` is read → all MCP servers start (including decision-memory)
|
|
159
|
+
2. `CLAUDE.md` is added to the system prompt
|
|
160
|
+
3. Session begins — Claude calls `get_context_summary` per instructions
|
|
161
|
+
|
|
162
|
+
Other MCP tools (context-mode, etc.) run in parallel with decision-memory. There is no conflict because each MCP server handles its own tools independently.
|
|
163
|
+
|
|
164
|
+
## Roadmap
|
|
165
|
+
|
|
166
|
+
- [x] Claude Code (MCP + hooks)
|
|
167
|
+
- [ ] Cursor (`.cursor/mcp.json`)
|
|
168
|
+
- [ ] VS Code + Cline (`.vscode/mcp.json`)
|
|
169
|
+
- [ ] Opencode (`opencode.json`)
|
|
170
|
+
- [ ] Semantic search (embeddings, opt-in)
|
|
171
|
+
|
|
172
|
+
## License
|
|
173
|
+
|
|
174
|
+
MIT
|
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
9
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
10
|
+
}) : x)(function(x) {
|
|
11
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
12
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
13
|
+
});
|
|
14
|
+
var __commonJS = (cb, mod) => function __require2() {
|
|
15
|
+
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
16
|
+
};
|
|
17
|
+
var __export = (target, all) => {
|
|
18
|
+
for (var name in all)
|
|
19
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
20
|
+
};
|
|
21
|
+
var __copyProps = (to, from, except, desc) => {
|
|
22
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
23
|
+
for (let key of __getOwnPropNames(from))
|
|
24
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
25
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
26
|
+
}
|
|
27
|
+
return to;
|
|
28
|
+
};
|
|
29
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
30
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
31
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
32
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
33
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
34
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
35
|
+
mod
|
|
36
|
+
));
|
|
37
|
+
|
|
38
|
+
// packages/core/src/parser.ts
|
|
39
|
+
function splitCsvRow(line) {
|
|
40
|
+
const fields = [];
|
|
41
|
+
let current = "";
|
|
42
|
+
let inQuote = false;
|
|
43
|
+
let i = 0;
|
|
44
|
+
while (i < line.length) {
|
|
45
|
+
const ch = line[i];
|
|
46
|
+
if (inQuote) {
|
|
47
|
+
if (ch === "\\") {
|
|
48
|
+
const next = line[i + 1];
|
|
49
|
+
if (next === '"') {
|
|
50
|
+
current += '"';
|
|
51
|
+
i += 2;
|
|
52
|
+
continue;
|
|
53
|
+
} else if (next === "n") {
|
|
54
|
+
current += "\n";
|
|
55
|
+
i += 2;
|
|
56
|
+
continue;
|
|
57
|
+
} else if (next === "\\") {
|
|
58
|
+
current += "\\";
|
|
59
|
+
i += 2;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
current += ch;
|
|
63
|
+
} else if (ch === '"') {
|
|
64
|
+
inQuote = false;
|
|
65
|
+
} else {
|
|
66
|
+
current += ch;
|
|
67
|
+
}
|
|
68
|
+
} else {
|
|
69
|
+
if (ch === '"') {
|
|
70
|
+
inQuote = true;
|
|
71
|
+
} else if (ch === ",") {
|
|
72
|
+
fields.push(current);
|
|
73
|
+
current = "";
|
|
74
|
+
} else {
|
|
75
|
+
current += ch;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
i++;
|
|
79
|
+
}
|
|
80
|
+
fields.push(current);
|
|
81
|
+
return fields;
|
|
82
|
+
}
|
|
83
|
+
function parseTableHeader(line) {
|
|
84
|
+
const match = line.match(/^decisions\[(\d+)\]\{([^}]+)\}:$/);
|
|
85
|
+
if (!match) return null;
|
|
86
|
+
return {
|
|
87
|
+
count: parseInt(match[1], 10),
|
|
88
|
+
fields: match[2].split(",")
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function isSummaryLine(line) {
|
|
92
|
+
return line.startsWith("summary{");
|
|
93
|
+
}
|
|
94
|
+
function parseDecisionRow(fields, values) {
|
|
95
|
+
const get = (name) => {
|
|
96
|
+
const idx = fields.indexOf(name);
|
|
97
|
+
return idx >= 0 ? values[idx] ?? "" : "";
|
|
98
|
+
};
|
|
99
|
+
const impactRaw = get("impact");
|
|
100
|
+
const validImpacts = ["low", "medium", "high", "critical"];
|
|
101
|
+
const impact = validImpacts.includes(impactRaw) ? impactRaw : "medium";
|
|
102
|
+
const tagsRaw = get("tags");
|
|
103
|
+
const tags = tagsRaw ? tagsRaw.split("|").filter(Boolean) : [];
|
|
104
|
+
return {
|
|
105
|
+
id: get("id"),
|
|
106
|
+
ts: get("ts"),
|
|
107
|
+
topic: get("topic"),
|
|
108
|
+
decision: get("decision"),
|
|
109
|
+
rationale: get("rationale"),
|
|
110
|
+
impact,
|
|
111
|
+
tags
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
function parseDecisionFile(content) {
|
|
115
|
+
const lines = content.split("\n");
|
|
116
|
+
const meta = {
|
|
117
|
+
project: "unknown",
|
|
118
|
+
version: "1",
|
|
119
|
+
created: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
|
|
120
|
+
updated: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)
|
|
121
|
+
};
|
|
122
|
+
const decisions = [];
|
|
123
|
+
let tableFields = [];
|
|
124
|
+
let inTable = false;
|
|
125
|
+
let inSummary = false;
|
|
126
|
+
for (const rawLine of lines) {
|
|
127
|
+
const line = rawLine.trimEnd();
|
|
128
|
+
if (!line || line.startsWith("#")) continue;
|
|
129
|
+
if (isSummaryLine(line)) {
|
|
130
|
+
inTable = false;
|
|
131
|
+
inSummary = true;
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (inSummary) continue;
|
|
135
|
+
const tableHeader = parseTableHeader(line);
|
|
136
|
+
if (tableHeader) {
|
|
137
|
+
tableFields = tableHeader.fields;
|
|
138
|
+
inTable = true;
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (inTable && tableFields.length > 0) {
|
|
142
|
+
const values = splitCsvRow(line);
|
|
143
|
+
if (values.length >= tableFields.length) {
|
|
144
|
+
decisions.push(parseDecisionRow(tableFields, values));
|
|
145
|
+
}
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
const colonIdx = line.indexOf(":");
|
|
149
|
+
if (colonIdx > 0) {
|
|
150
|
+
const key = line.slice(0, colonIdx).trim();
|
|
151
|
+
const value = line.slice(colonIdx + 1).trim();
|
|
152
|
+
if (key === "project") meta.project = value;
|
|
153
|
+
else if (key === "version") meta.version = value;
|
|
154
|
+
else if (key === "created") meta.created = value;
|
|
155
|
+
else if (key === "updated") meta.updated = value;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return { meta, decisions };
|
|
159
|
+
}
|
|
160
|
+
function serializeDecisionRow(d) {
|
|
161
|
+
const escapeField = (s) => {
|
|
162
|
+
if (s.includes(",") || s.includes('"') || s.includes("\n")) {
|
|
163
|
+
return '"' + s.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n") + '"';
|
|
164
|
+
}
|
|
165
|
+
return s;
|
|
166
|
+
};
|
|
167
|
+
const tags = d.tags.join("|");
|
|
168
|
+
return [
|
|
169
|
+
d.id,
|
|
170
|
+
d.ts,
|
|
171
|
+
d.topic,
|
|
172
|
+
escapeField(d.decision),
|
|
173
|
+
escapeField(d.rationale),
|
|
174
|
+
d.impact,
|
|
175
|
+
tags
|
|
176
|
+
].join(",");
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// packages/core/src/searcher.ts
|
|
180
|
+
var IMPACT_WEIGHT = {
|
|
181
|
+
critical: 4,
|
|
182
|
+
high: 3,
|
|
183
|
+
medium: 2,
|
|
184
|
+
low: 1
|
|
185
|
+
};
|
|
186
|
+
function computeMatchedFields(d, keywords, tags) {
|
|
187
|
+
const matched = [];
|
|
188
|
+
const kws = keywords.map((k) => k.toLowerCase());
|
|
189
|
+
if (tags.length > 0 && tags.some((t) => d.tags.map((dt) => dt.toLowerCase()).includes(t))) {
|
|
190
|
+
matched.push("tags");
|
|
191
|
+
}
|
|
192
|
+
if (kws.some((k) => d.topic.toLowerCase().includes(k))) matched.push("topic");
|
|
193
|
+
if (kws.some((k) => d.decision.toLowerCase().includes(k))) matched.push("decision");
|
|
194
|
+
if (kws.some((k) => d.rationale.toLowerCase().includes(k))) matched.push("rationale");
|
|
195
|
+
return matched;
|
|
196
|
+
}
|
|
197
|
+
function searchDecisions(file, query) {
|
|
198
|
+
const keywords = (query.keywords ?? []).map((k) => k.toLowerCase());
|
|
199
|
+
const tags = (query.tags ?? []).map((t) => t.toLowerCase());
|
|
200
|
+
const impactFilter = query.impact;
|
|
201
|
+
const limit = query.limit ?? 5;
|
|
202
|
+
if (keywords.length === 0 && tags.length === 0 && !impactFilter) {
|
|
203
|
+
return file.decisions.slice(-limit).reverse().map((d) => ({ decision: d, matchedFields: [] }));
|
|
204
|
+
}
|
|
205
|
+
const results = [];
|
|
206
|
+
for (const d of file.decisions) {
|
|
207
|
+
if (impactFilter && d.impact !== impactFilter) continue;
|
|
208
|
+
const decisionLower = d.decision.toLowerCase();
|
|
209
|
+
const rationaleLower = d.rationale.toLowerCase();
|
|
210
|
+
const topicLower = d.topic.toLowerCase();
|
|
211
|
+
const tagsLower = d.tags.map((t) => t.toLowerCase());
|
|
212
|
+
const matchesTag = tags.length === 0 || tags.some((t) => tagsLower.includes(t));
|
|
213
|
+
const matchesKeyword = keywords.length === 0 || keywords.some(
|
|
214
|
+
(k) => topicLower.includes(k) || decisionLower.includes(k) || rationaleLower.includes(k) || tagsLower.some((t) => t.includes(k))
|
|
215
|
+
);
|
|
216
|
+
if (matchesTag && matchesKeyword) {
|
|
217
|
+
results.push({
|
|
218
|
+
decision: d,
|
|
219
|
+
matchedFields: computeMatchedFields(d, keywords, tags)
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
results.sort((a, b) => {
|
|
224
|
+
const weightDiff = IMPACT_WEIGHT[b.decision.impact] - IMPACT_WEIGHT[a.decision.impact];
|
|
225
|
+
if (weightDiff !== 0) return weightDiff;
|
|
226
|
+
return b.decision.ts.localeCompare(a.decision.ts);
|
|
227
|
+
});
|
|
228
|
+
return results.slice(0, limit);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// packages/core/src/summary.ts
|
|
232
|
+
var HIGH_IMPACT = ["high", "critical"];
|
|
233
|
+
function topTopics(decisions, n = 5) {
|
|
234
|
+
const counts = /* @__PURE__ */ new Map();
|
|
235
|
+
for (const d of decisions) {
|
|
236
|
+
counts.set(d.topic, (counts.get(d.topic) ?? 0) + 1);
|
|
237
|
+
}
|
|
238
|
+
return [...counts.entries()].sort((a, b) => b[1] - a[1]).slice(0, n).map(([topic]) => topic);
|
|
239
|
+
}
|
|
240
|
+
function buildContextSummary(file, options = {}) {
|
|
241
|
+
const { includeRecent = true } = options;
|
|
242
|
+
const { decisions } = file;
|
|
243
|
+
const highImpactCount = decisions.filter(
|
|
244
|
+
(d) => HIGH_IMPACT.includes(d.impact)
|
|
245
|
+
).length;
|
|
246
|
+
const lastUpdated = decisions.length > 0 ? decisions[decisions.length - 1].ts.slice(0, 10) : file.meta.updated;
|
|
247
|
+
const recentDecisions = includeRecent ? decisions.filter((d) => HIGH_IMPACT.includes(d.impact)).slice(-5).reverse() : [];
|
|
248
|
+
return {
|
|
249
|
+
total: decisions.length,
|
|
250
|
+
highImpactCount,
|
|
251
|
+
lastUpdated,
|
|
252
|
+
topTopics: topTopics(decisions),
|
|
253
|
+
recentDecisions
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
function buildSummaryBlock(file) {
|
|
257
|
+
const summary = buildContextSummary(file, { includeRecent: false });
|
|
258
|
+
const topics = summary.topTopics.join("|");
|
|
259
|
+
return `summary{total,high_impact,last_updated,top_topics}:
|
|
260
|
+
${summary.total},${summary.highImpactCount},${summary.lastUpdated},${topics}`;
|
|
261
|
+
}
|
|
262
|
+
function formatContextSummaryAsToon(file, includeRecent = true) {
|
|
263
|
+
const summary = buildContextSummary(file, { includeRecent });
|
|
264
|
+
const topics = summary.topTopics.join("|");
|
|
265
|
+
let output = `summary{total,high_impact,last_updated,top_topics}:
|
|
266
|
+
`;
|
|
267
|
+
output += `${summary.total},${summary.highImpactCount},${summary.lastUpdated},${topics}`;
|
|
268
|
+
if (includeRecent && summary.recentDecisions.length > 0) {
|
|
269
|
+
output += `
|
|
270
|
+
|
|
271
|
+
recent_high_impact[${summary.recentDecisions.length}]{id,ts,topic,decision,impact}:
|
|
272
|
+
`;
|
|
273
|
+
for (const d of summary.recentDecisions) {
|
|
274
|
+
const decisionField = d.decision.includes(",") ? `"${d.decision}"` : d.decision;
|
|
275
|
+
output += `${d.id},${d.ts},${d.topic},${decisionField},${d.impact}
|
|
276
|
+
`;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return output.trimEnd();
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// packages/core/src/writer.ts
|
|
283
|
+
import * as fs from "fs";
|
|
284
|
+
import * as path from "path";
|
|
285
|
+
import * as os from "os";
|
|
286
|
+
|
|
287
|
+
// packages/core/src/idgen.ts
|
|
288
|
+
var ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
|
289
|
+
function encodeBase36(n) {
|
|
290
|
+
if (n === 0) return "0";
|
|
291
|
+
let result = "";
|
|
292
|
+
while (n > 0) {
|
|
293
|
+
result = ALPHABET[n % 36] + result;
|
|
294
|
+
n = Math.floor(n / 36);
|
|
295
|
+
}
|
|
296
|
+
return result;
|
|
297
|
+
}
|
|
298
|
+
function generateNextId(currentCount) {
|
|
299
|
+
const next = currentCount + 1;
|
|
300
|
+
if (next <= 999) {
|
|
301
|
+
return `D${String(next).padStart(3, "0")}`;
|
|
302
|
+
}
|
|
303
|
+
const encoded = encodeBase36(next).padStart(4, "0");
|
|
304
|
+
return `D${encoded}`;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// packages/core/src/writer.ts
|
|
308
|
+
function initDecisionFile(filePath, projectName) {
|
|
309
|
+
if (fs.existsSync(filePath)) {
|
|
310
|
+
throw new Error(`Dosya zaten var: ${filePath}`);
|
|
311
|
+
}
|
|
312
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
313
|
+
const content = `# decision-memory v1
|
|
314
|
+
project: ${projectName}
|
|
315
|
+
version: 1
|
|
316
|
+
created: ${today}
|
|
317
|
+
updated: ${today}
|
|
318
|
+
|
|
319
|
+
decisions[0]{id,ts,topic,decision,rationale,impact,tags}:
|
|
320
|
+
|
|
321
|
+
summary{total,high_impact,last_updated,top_topics}:
|
|
322
|
+
0,0,${today},
|
|
323
|
+
`;
|
|
324
|
+
fs.writeFileSync(filePath, content, "utf-8");
|
|
325
|
+
}
|
|
326
|
+
function updateDecisionCount(filePath, newCount) {
|
|
327
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
328
|
+
const updated = content.replace(
|
|
329
|
+
/^decisions\[(\d+)\]\{/m,
|
|
330
|
+
`decisions[${newCount}]{`
|
|
331
|
+
);
|
|
332
|
+
const tmpPath = filePath + ".tmp";
|
|
333
|
+
fs.writeFileSync(tmpPath, updated, "utf-8");
|
|
334
|
+
fs.renameSync(tmpPath, filePath);
|
|
335
|
+
}
|
|
336
|
+
function updateSummaryBlock(filePath) {
|
|
337
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
338
|
+
const file = parseDecisionFile(content);
|
|
339
|
+
const summaryBlock = buildSummaryBlock(file);
|
|
340
|
+
const summaryStart = content.indexOf("\nsummary{");
|
|
341
|
+
let base;
|
|
342
|
+
if (summaryStart >= 0) {
|
|
343
|
+
base = content.slice(0, summaryStart);
|
|
344
|
+
} else {
|
|
345
|
+
base = content.trimEnd();
|
|
346
|
+
}
|
|
347
|
+
const newContent = base + "\n" + summaryBlock + "\n";
|
|
348
|
+
const tmpPath = filePath + ".tmp";
|
|
349
|
+
fs.writeFileSync(tmpPath, newContent, "utf-8");
|
|
350
|
+
fs.renameSync(tmpPath, filePath);
|
|
351
|
+
}
|
|
352
|
+
function appendDecision(filePath, input) {
|
|
353
|
+
if (!fs.existsSync(filePath)) {
|
|
354
|
+
throw new Error(`DECISIONS.toon dosyas\u0131 bulunamad\u0131: ${filePath}
|
|
355
|
+
\xD6nce 'decision-memory init' komutunu \xE7al\u0131\u015Ft\u0131r\u0131n.`);
|
|
356
|
+
}
|
|
357
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
358
|
+
const countMatch = content.match(/^decisions\[(\d+)\]/m);
|
|
359
|
+
const currentCount = countMatch ? parseInt(countMatch[1], 10) : 0;
|
|
360
|
+
const id = generateNextId(currentCount);
|
|
361
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z").replace(/:\d{2}Z$/, "Z");
|
|
362
|
+
const decision = {
|
|
363
|
+
id,
|
|
364
|
+
ts,
|
|
365
|
+
topic: input.topic,
|
|
366
|
+
decision: input.decision,
|
|
367
|
+
rationale: input.rationale,
|
|
368
|
+
impact: input.impact,
|
|
369
|
+
tags: input.tags
|
|
370
|
+
};
|
|
371
|
+
const row = serializeDecisionRow(decision);
|
|
372
|
+
const lines = content.split("\n");
|
|
373
|
+
let tableEnd = -1;
|
|
374
|
+
let inTable = false;
|
|
375
|
+
for (let i = 0; i < lines.length; i++) {
|
|
376
|
+
if (/^decisions\[\d+\]\{/.test(lines[i])) {
|
|
377
|
+
inTable = true;
|
|
378
|
+
tableEnd = i;
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
if (inTable) {
|
|
382
|
+
if (lines[i].startsWith("summary{") || lines[i] === "" && tableEnd >= 0) {
|
|
383
|
+
if (lines[i].startsWith("summary{")) {
|
|
384
|
+
tableEnd = i - 1;
|
|
385
|
+
} else {
|
|
386
|
+
tableEnd = i - 1;
|
|
387
|
+
}
|
|
388
|
+
break;
|
|
389
|
+
}
|
|
390
|
+
if (lines[i] !== "") {
|
|
391
|
+
tableEnd = i;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
lines.splice(tableEnd + 1, 0, row);
|
|
396
|
+
const newContent = lines.join("\n");
|
|
397
|
+
const tmpPath = filePath + ".tmp";
|
|
398
|
+
fs.writeFileSync(tmpPath, newContent, "utf-8");
|
|
399
|
+
fs.renameSync(tmpPath, filePath);
|
|
400
|
+
updateDecisionCount(filePath, currentCount + 1);
|
|
401
|
+
updateSummaryBlock(filePath);
|
|
402
|
+
return decision;
|
|
403
|
+
}
|
|
404
|
+
function resolveDecisionFilePath(cwd) {
|
|
405
|
+
if (process.env.DECISION_MEMORY_FILE) {
|
|
406
|
+
return process.env.DECISION_MEMORY_FILE;
|
|
407
|
+
}
|
|
408
|
+
const dir = cwd ?? process.cwd();
|
|
409
|
+
const local = path.join(dir, "DECISIONS.toon");
|
|
410
|
+
if (fs.existsSync(local)) return local;
|
|
411
|
+
const globalDir = path.join(os.homedir(), ".decision-memory");
|
|
412
|
+
fs.mkdirSync(globalDir, { recursive: true });
|
|
413
|
+
return path.join(globalDir, "global.toon");
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
export {
|
|
417
|
+
__require,
|
|
418
|
+
__commonJS,
|
|
419
|
+
__export,
|
|
420
|
+
__toESM,
|
|
421
|
+
parseDecisionFile,
|
|
422
|
+
serializeDecisionRow,
|
|
423
|
+
searchDecisions,
|
|
424
|
+
formatContextSummaryAsToon,
|
|
425
|
+
initDecisionFile,
|
|
426
|
+
appendDecision,
|
|
427
|
+
resolveDecisionFilePath
|
|
428
|
+
};
|