decision-memory 0.1.3 → 0.1.4

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "decision-memory",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Claude Code için karar hafızası — CLI ve MCP server",
5
5
  "bin": {
6
6
  "decision-memory": "./dist/index.cjs",
@@ -1,389 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- // packages/core/src/parser.ts
4
- function splitCsvRow(line) {
5
- const fields = [];
6
- let current = "";
7
- let inQuote = false;
8
- let i = 0;
9
- while (i < line.length) {
10
- const ch = line[i];
11
- if (inQuote) {
12
- if (ch === "\\") {
13
- const next = line[i + 1];
14
- if (next === '"') {
15
- current += '"';
16
- i += 2;
17
- continue;
18
- } else if (next === "n") {
19
- current += "\n";
20
- i += 2;
21
- continue;
22
- } else if (next === "\\") {
23
- current += "\\";
24
- i += 2;
25
- continue;
26
- }
27
- current += ch;
28
- } else if (ch === '"') {
29
- inQuote = false;
30
- } else {
31
- current += ch;
32
- }
33
- } else {
34
- if (ch === '"') {
35
- inQuote = true;
36
- } else if (ch === ",") {
37
- fields.push(current);
38
- current = "";
39
- } else {
40
- current += ch;
41
- }
42
- }
43
- i++;
44
- }
45
- fields.push(current);
46
- return fields;
47
- }
48
- function parseTableHeader(line) {
49
- const match = line.match(/^decisions\[(\d+)\]\{([^}]+)\}:$/);
50
- if (!match) return null;
51
- return {
52
- count: parseInt(match[1], 10),
53
- fields: match[2].split(",")
54
- };
55
- }
56
- function isSummaryLine(line) {
57
- return line.startsWith("summary{");
58
- }
59
- function parseDecisionRow(fields, values) {
60
- const get = (name) => {
61
- const idx = fields.indexOf(name);
62
- return idx >= 0 ? values[idx] ?? "" : "";
63
- };
64
- const impactRaw = get("impact");
65
- const validImpacts = ["low", "medium", "high", "critical"];
66
- const impact = validImpacts.includes(impactRaw) ? impactRaw : "medium";
67
- const tagsRaw = get("tags");
68
- const tags = tagsRaw ? tagsRaw.split("|").filter(Boolean) : [];
69
- return {
70
- id: get("id"),
71
- ts: get("ts"),
72
- topic: get("topic"),
73
- decision: get("decision"),
74
- rationale: get("rationale"),
75
- impact,
76
- tags
77
- };
78
- }
79
- function parseDecisionFile(content) {
80
- const lines = content.split("\n");
81
- const meta = {
82
- project: "unknown",
83
- version: "1",
84
- created: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
85
- updated: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)
86
- };
87
- const decisions = [];
88
- let tableFields = [];
89
- let inTable = false;
90
- let inSummary = false;
91
- for (const rawLine of lines) {
92
- const line = rawLine.trimEnd();
93
- if (!line || line.startsWith("#")) continue;
94
- if (isSummaryLine(line)) {
95
- inTable = false;
96
- inSummary = true;
97
- continue;
98
- }
99
- if (inSummary) continue;
100
- const tableHeader = parseTableHeader(line);
101
- if (tableHeader) {
102
- tableFields = tableHeader.fields;
103
- inTable = true;
104
- continue;
105
- }
106
- if (inTable && tableFields.length > 0) {
107
- const values = splitCsvRow(line);
108
- if (values.length >= tableFields.length) {
109
- decisions.push(parseDecisionRow(tableFields, values));
110
- }
111
- continue;
112
- }
113
- const colonIdx = line.indexOf(":");
114
- if (colonIdx > 0) {
115
- const key = line.slice(0, colonIdx).trim();
116
- const value = line.slice(colonIdx + 1).trim();
117
- if (key === "project") meta.project = value;
118
- else if (key === "version") meta.version = value;
119
- else if (key === "created") meta.created = value;
120
- else if (key === "updated") meta.updated = value;
121
- }
122
- }
123
- return { meta, decisions };
124
- }
125
- function serializeDecisionRow(d) {
126
- const escapeField = (s) => {
127
- if (s.includes(",") || s.includes('"') || s.includes("\n")) {
128
- return '"' + s.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n") + '"';
129
- }
130
- return s;
131
- };
132
- const tags = d.tags.join("|");
133
- return [
134
- d.id,
135
- d.ts,
136
- d.topic,
137
- escapeField(d.decision),
138
- escapeField(d.rationale),
139
- d.impact,
140
- tags
141
- ].join(",");
142
- }
143
-
144
- // packages/core/src/searcher.ts
145
- var IMPACT_WEIGHT = {
146
- critical: 4,
147
- high: 3,
148
- medium: 2,
149
- low: 1
150
- };
151
- function computeMatchedFields(d, keywords, tags) {
152
- const matched = [];
153
- const kws = keywords.map((k) => k.toLowerCase());
154
- if (tags.length > 0 && tags.some((t) => d.tags.map((dt) => dt.toLowerCase()).includes(t))) {
155
- matched.push("tags");
156
- }
157
- if (kws.some((k) => d.topic.toLowerCase().includes(k))) matched.push("topic");
158
- if (kws.some((k) => d.decision.toLowerCase().includes(k))) matched.push("decision");
159
- if (kws.some((k) => d.rationale.toLowerCase().includes(k))) matched.push("rationale");
160
- return matched;
161
- }
162
- function searchDecisions(file, query) {
163
- const keywords = (query.keywords ?? []).map((k) => k.toLowerCase());
164
- const tags = (query.tags ?? []).map((t) => t.toLowerCase());
165
- const impactFilter = query.impact;
166
- const limit = query.limit ?? 5;
167
- if (keywords.length === 0 && tags.length === 0 && !impactFilter) {
168
- return file.decisions.slice(-limit).reverse().map((d) => ({ decision: d, matchedFields: [] }));
169
- }
170
- const results = [];
171
- for (const d of file.decisions) {
172
- if (impactFilter && d.impact !== impactFilter) continue;
173
- const decisionLower = d.decision.toLowerCase();
174
- const rationaleLower = d.rationale.toLowerCase();
175
- const topicLower = d.topic.toLowerCase();
176
- const tagsLower = d.tags.map((t) => t.toLowerCase());
177
- const matchesTag = tags.length === 0 || tags.some((t) => tagsLower.includes(t));
178
- const matchesKeyword = keywords.length === 0 || keywords.some(
179
- (k) => topicLower.includes(k) || decisionLower.includes(k) || rationaleLower.includes(k) || tagsLower.some((t) => t.includes(k))
180
- );
181
- if (matchesTag && matchesKeyword) {
182
- results.push({
183
- decision: d,
184
- matchedFields: computeMatchedFields(d, keywords, tags)
185
- });
186
- }
187
- }
188
- results.sort((a, b) => {
189
- const weightDiff = IMPACT_WEIGHT[b.decision.impact] - IMPACT_WEIGHT[a.decision.impact];
190
- if (weightDiff !== 0) return weightDiff;
191
- return b.decision.ts.localeCompare(a.decision.ts);
192
- });
193
- return results.slice(0, limit);
194
- }
195
-
196
- // packages/core/src/summary.ts
197
- var HIGH_IMPACT = ["high", "critical"];
198
- function topTopics(decisions, n = 5) {
199
- const counts = /* @__PURE__ */ new Map();
200
- for (const d of decisions) {
201
- counts.set(d.topic, (counts.get(d.topic) ?? 0) + 1);
202
- }
203
- return [...counts.entries()].sort((a, b) => b[1] - a[1]).slice(0, n).map(([topic]) => topic);
204
- }
205
- function buildContextSummary(file, options = {}) {
206
- const { includeRecent = true } = options;
207
- const { decisions } = file;
208
- const highImpactCount = decisions.filter(
209
- (d) => HIGH_IMPACT.includes(d.impact)
210
- ).length;
211
- const lastUpdated = decisions.length > 0 ? decisions[decisions.length - 1].ts.slice(0, 10) : file.meta.updated;
212
- const recentDecisions = includeRecent ? decisions.filter((d) => HIGH_IMPACT.includes(d.impact)).slice(-5).reverse() : [];
213
- return {
214
- total: decisions.length,
215
- highImpactCount,
216
- lastUpdated,
217
- topTopics: topTopics(decisions),
218
- recentDecisions
219
- };
220
- }
221
- function buildSummaryBlock(file) {
222
- const summary = buildContextSummary(file, { includeRecent: false });
223
- const topics = summary.topTopics.join("|");
224
- return `summary{total,high_impact,last_updated,top_topics}:
225
- ${summary.total},${summary.highImpactCount},${summary.lastUpdated},${topics}`;
226
- }
227
- function formatContextSummaryAsToon(file, includeRecent = true) {
228
- const summary = buildContextSummary(file, { includeRecent });
229
- const topics = summary.topTopics.join("|");
230
- let output = `summary{total,high_impact,last_updated,top_topics}:
231
- `;
232
- output += `${summary.total},${summary.highImpactCount},${summary.lastUpdated},${topics}`;
233
- if (includeRecent && summary.recentDecisions.length > 0) {
234
- output += `
235
-
236
- recent_high_impact[${summary.recentDecisions.length}]{id,ts,topic,decision,impact}:
237
- `;
238
- for (const d of summary.recentDecisions) {
239
- const decisionField = d.decision.includes(",") ? `"${d.decision}"` : d.decision;
240
- output += `${d.id},${d.ts},${d.topic},${decisionField},${d.impact}
241
- `;
242
- }
243
- }
244
- return output.trimEnd();
245
- }
246
-
247
- // packages/core/src/writer.ts
248
- import * as fs from "fs";
249
- import * as path from "path";
250
- import * as os from "os";
251
-
252
- // packages/core/src/idgen.ts
253
- var ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
254
- function encodeBase36(n) {
255
- if (n === 0) return "0";
256
- let result = "";
257
- while (n > 0) {
258
- result = ALPHABET[n % 36] + result;
259
- n = Math.floor(n / 36);
260
- }
261
- return result;
262
- }
263
- function generateNextId(currentCount) {
264
- const next = currentCount + 1;
265
- if (next <= 999) {
266
- return `D${String(next).padStart(3, "0")}`;
267
- }
268
- const encoded = encodeBase36(next).padStart(4, "0");
269
- return `D${encoded}`;
270
- }
271
-
272
- // packages/core/src/writer.ts
273
- function initDecisionFile(filePath, projectName) {
274
- if (fs.existsSync(filePath)) {
275
- throw new Error(`Dosya zaten var: ${filePath}`);
276
- }
277
- const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
278
- const content = `# decision-memory v1
279
- project: ${projectName}
280
- version: 1
281
- created: ${today}
282
- updated: ${today}
283
-
284
- decisions[0]{id,ts,topic,decision,rationale,impact,tags}:
285
-
286
- summary{total,high_impact,last_updated,top_topics}:
287
- 0,0,${today},
288
- `;
289
- fs.writeFileSync(filePath, content, "utf-8");
290
- }
291
- function updateDecisionCount(filePath, newCount) {
292
- const content = fs.readFileSync(filePath, "utf-8");
293
- const updated = content.replace(
294
- /^decisions\[(\d+)\]\{/m,
295
- `decisions[${newCount}]{`
296
- );
297
- const tmpPath = filePath + ".tmp";
298
- fs.writeFileSync(tmpPath, updated, "utf-8");
299
- fs.renameSync(tmpPath, filePath);
300
- }
301
- function updateSummaryBlock(filePath) {
302
- const content = fs.readFileSync(filePath, "utf-8");
303
- const file = parseDecisionFile(content);
304
- const summaryBlock = buildSummaryBlock(file);
305
- const summaryStart = content.indexOf("\nsummary{");
306
- let base;
307
- if (summaryStart >= 0) {
308
- base = content.slice(0, summaryStart);
309
- } else {
310
- base = content.trimEnd();
311
- }
312
- const newContent = base + "\n" + summaryBlock + "\n";
313
- const tmpPath = filePath + ".tmp";
314
- fs.writeFileSync(tmpPath, newContent, "utf-8");
315
- fs.renameSync(tmpPath, filePath);
316
- }
317
- function appendDecision(filePath, input) {
318
- if (!fs.existsSync(filePath)) {
319
- throw new Error(`DECISIONS.toon dosyas\u0131 bulunamad\u0131: ${filePath}
320
- \xD6nce 'decision-memory init' komutunu \xE7al\u0131\u015Ft\u0131r\u0131n.`);
321
- }
322
- const content = fs.readFileSync(filePath, "utf-8");
323
- const countMatch = content.match(/^decisions\[(\d+)\]/m);
324
- const currentCount = countMatch ? parseInt(countMatch[1], 10) : 0;
325
- const id = generateNextId(currentCount);
326
- const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z").replace(/:\d{2}Z$/, "Z");
327
- const decision = {
328
- id,
329
- ts,
330
- topic: input.topic,
331
- decision: input.decision,
332
- rationale: input.rationale,
333
- impact: input.impact,
334
- tags: input.tags
335
- };
336
- const row = serializeDecisionRow(decision);
337
- const lines = content.split("\n");
338
- let tableEnd = -1;
339
- let inTable = false;
340
- for (let i = 0; i < lines.length; i++) {
341
- if (/^decisions\[\d+\]\{/.test(lines[i])) {
342
- inTable = true;
343
- tableEnd = i;
344
- continue;
345
- }
346
- if (inTable) {
347
- if (lines[i].startsWith("summary{") || lines[i] === "" && tableEnd >= 0) {
348
- if (lines[i].startsWith("summary{")) {
349
- tableEnd = i - 1;
350
- } else {
351
- tableEnd = i - 1;
352
- }
353
- break;
354
- }
355
- if (lines[i] !== "") {
356
- tableEnd = i;
357
- }
358
- }
359
- }
360
- lines.splice(tableEnd + 1, 0, row);
361
- const newContent = lines.join("\n");
362
- const tmpPath = filePath + ".tmp";
363
- fs.writeFileSync(tmpPath, newContent, "utf-8");
364
- fs.renameSync(tmpPath, filePath);
365
- updateDecisionCount(filePath, currentCount + 1);
366
- updateSummaryBlock(filePath);
367
- return decision;
368
- }
369
- function resolveDecisionFilePath(cwd) {
370
- if (process.env.DECISION_MEMORY_FILE) {
371
- return process.env.DECISION_MEMORY_FILE;
372
- }
373
- const dir = cwd ?? process.cwd();
374
- const local = path.join(dir, "DECISIONS.toon");
375
- if (fs.existsSync(local)) return local;
376
- const globalDir = path.join(os.homedir(), ".decision-memory");
377
- fs.mkdirSync(globalDir, { recursive: true });
378
- return path.join(globalDir, "global.toon");
379
- }
380
-
381
- export {
382
- parseDecisionFile,
383
- serializeDecisionRow,
384
- searchDecisions,
385
- formatContextSummaryAsToon,
386
- initDecisionFile,
387
- appendDecision,
388
- resolveDecisionFilePath
389
- };