decision-memory 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/dist/chunk-43MFRJ7K.js +389 -0
- package/dist/index.js +145 -0
- package/dist/mcp.js +209 -0
- package/package.json +21 -0
|
@@ -0,0 +1,389 @@
|
|
|
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
|
+
};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
appendDecision,
|
|
4
|
+
formatContextSummaryAsToon,
|
|
5
|
+
initDecisionFile,
|
|
6
|
+
parseDecisionFile,
|
|
7
|
+
resolveDecisionFilePath,
|
|
8
|
+
searchDecisions,
|
|
9
|
+
serializeDecisionRow
|
|
10
|
+
} from "./chunk-43MFRJ7K.js";
|
|
11
|
+
|
|
12
|
+
// packages/cli/src/index.ts
|
|
13
|
+
import { Command } from "commander";
|
|
14
|
+
|
|
15
|
+
// packages/cli/src/commands/init.ts
|
|
16
|
+
import * as fs from "fs";
|
|
17
|
+
import * as path from "path";
|
|
18
|
+
function initCommand(options) {
|
|
19
|
+
const dir = options.dir ?? process.cwd();
|
|
20
|
+
const filePath = path.join(dir, "DECISIONS.toon");
|
|
21
|
+
if (fs.existsSync(filePath)) {
|
|
22
|
+
console.error(`Hata: ${filePath} zaten mevcut.`);
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
const projectName = options.project ?? path.basename(dir);
|
|
26
|
+
initDecisionFile(filePath, projectName);
|
|
27
|
+
const gitattributes = path.join(dir, ".gitattributes");
|
|
28
|
+
const entry = "DECISIONS.toon text eol=lf\n";
|
|
29
|
+
if (fs.existsSync(gitattributes)) {
|
|
30
|
+
const content = fs.readFileSync(gitattributes, "utf-8");
|
|
31
|
+
if (!content.includes("DECISIONS.toon")) {
|
|
32
|
+
fs.appendFileSync(gitattributes, entry);
|
|
33
|
+
}
|
|
34
|
+
} else {
|
|
35
|
+
fs.writeFileSync(gitattributes, entry);
|
|
36
|
+
}
|
|
37
|
+
console.log(`\u2713 ${filePath} olu\u015Fturuldu.`);
|
|
38
|
+
console.log(`\u2713 .gitattributes g\xFCncellendi.`);
|
|
39
|
+
console.log(`
|
|
40
|
+
Ba\u015Flamak i\xE7in:`);
|
|
41
|
+
console.log(` decision-memory log # yeni karar ekle`);
|
|
42
|
+
console.log(` decision-memory summary # \xF6zet g\xF6r\xFCnt\xFCle`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// packages/cli/src/commands/log.ts
|
|
46
|
+
import * as readline from "readline/promises";
|
|
47
|
+
import { stdin as input, stdout as output } from "process";
|
|
48
|
+
async function logCommand(options) {
|
|
49
|
+
if (options.topic && options.decision && options.rationale && options.impact) {
|
|
50
|
+
const inp = buildInput(options);
|
|
51
|
+
const filePath = options.file ?? resolveDecisionFilePath();
|
|
52
|
+
const entry = appendDecision(filePath, inp);
|
|
53
|
+
console.log(`\u2713 Karar kaydedildi: ${entry.id} \u2014 ${entry.decision}`);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const rl = readline.createInterface({ input, output });
|
|
57
|
+
try {
|
|
58
|
+
const topic = options.topic ?? await rl.question("Konu (\xF6rn: auth, database): ");
|
|
59
|
+
const decision = options.decision ?? await rl.question("Al\u0131nan karar: ");
|
|
60
|
+
const rationale = options.rationale ?? await rl.question("Gerek\xE7e: ");
|
|
61
|
+
const impactRaw = options.impact ?? await rl.question("Etki (low/medium/high/critical) [medium]: ");
|
|
62
|
+
const tagsRaw = options.tags ?? await rl.question("Etiketler (virg\xFClle, \xF6rn: auth,jwt) []: ");
|
|
63
|
+
const filled = {
|
|
64
|
+
topic: topic.trim() || void 0,
|
|
65
|
+
decision: decision.trim() || void 0,
|
|
66
|
+
rationale: rationale.trim() || void 0,
|
|
67
|
+
impact: impactRaw.trim() || "medium",
|
|
68
|
+
tags: tagsRaw.trim()
|
|
69
|
+
};
|
|
70
|
+
if (!filled.topic || !filled.decision || !filled.rationale) {
|
|
71
|
+
console.error("Hata: Konu, karar ve gerek\xE7e zorunludur.");
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
const decisionInput = buildInput(filled);
|
|
75
|
+
const filePath = options.file ?? resolveDecisionFilePath();
|
|
76
|
+
const entry = appendDecision(filePath, decisionInput);
|
|
77
|
+
console.log(`
|
|
78
|
+
\u2713 Karar kaydedildi: ${entry.id} \u2014 ${entry.decision}`);
|
|
79
|
+
} finally {
|
|
80
|
+
rl.close();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function buildInput(options) {
|
|
84
|
+
const validImpacts = ["low", "medium", "high", "critical"];
|
|
85
|
+
const impact = validImpacts.includes(options.impact) ? options.impact : "medium";
|
|
86
|
+
const tags = options.tags ? options.tags.split(",").map((t) => t.trim()).filter(Boolean) : [];
|
|
87
|
+
return {
|
|
88
|
+
topic: options.topic.trim(),
|
|
89
|
+
decision: options.decision.trim(),
|
|
90
|
+
rationale: options.rationale.trim(),
|
|
91
|
+
impact,
|
|
92
|
+
tags
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// packages/cli/src/commands/search.ts
|
|
97
|
+
import * as fs2 from "fs";
|
|
98
|
+
function searchCommand(options) {
|
|
99
|
+
const filePath = options.file ?? resolveDecisionFilePath();
|
|
100
|
+
if (!fs2.existsSync(filePath)) {
|
|
101
|
+
console.error(`Hata: ${filePath} bulunamad\u0131. \xD6nce 'decision-memory init' \xE7al\u0131\u015Ft\u0131r\u0131n.`);
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
104
|
+
const content = fs2.readFileSync(filePath, "utf-8");
|
|
105
|
+
const file = parseDecisionFile(content);
|
|
106
|
+
const keywords = options.keywords ? options.keywords.split(",").map((k) => k.trim()).filter(Boolean) : [];
|
|
107
|
+
const tags = options.tags ? options.tags.split(",").map((t) => t.trim()).filter(Boolean) : [];
|
|
108
|
+
const validImpacts = ["low", "medium", "high", "critical"];
|
|
109
|
+
const impact = validImpacts.includes(options.impact) ? options.impact : void 0;
|
|
110
|
+
const limit = options.limit ? parseInt(options.limit, 10) : 5;
|
|
111
|
+
const results = searchDecisions(file, { keywords, tags, impact, limit });
|
|
112
|
+
if (results.length === 0) {
|
|
113
|
+
console.log("E\u015Fle\u015Fen karar bulunamad\u0131.");
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
console.log(`decisions[${results.length}]{id,ts,topic,decision,rationale,impact,tags}:`);
|
|
117
|
+
for (const r of results) {
|
|
118
|
+
console.log(serializeDecisionRow(r.decision));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// packages/cli/src/commands/summary.ts
|
|
123
|
+
import * as fs3 from "fs";
|
|
124
|
+
function summaryCommand(options) {
|
|
125
|
+
const filePath = options.file ?? resolveDecisionFilePath();
|
|
126
|
+
if (!fs3.existsSync(filePath)) {
|
|
127
|
+
console.error(`Hata: ${filePath} bulunamad\u0131. \xD6nce 'decision-memory init' \xE7al\u0131\u015Ft\u0131r\u0131n.`);
|
|
128
|
+
process.exit(1);
|
|
129
|
+
}
|
|
130
|
+
const content = fs3.readFileSync(filePath, "utf-8");
|
|
131
|
+
const file = parseDecisionFile(content);
|
|
132
|
+
const output2 = formatContextSummaryAsToon(file, !options.noRecent);
|
|
133
|
+
console.log(output2);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// packages/cli/src/index.ts
|
|
137
|
+
var program = new Command();
|
|
138
|
+
program.name("decision-memory").description("Claude Code i\xE7in karar haf\u0131zas\u0131 arac\u0131").version("0.1.0");
|
|
139
|
+
program.command("init").description("Proje k\xF6k\xFCnde DECISIONS.toon dosyas\u0131 olu\u015Fturur").option("-p, --project <name>", "Proje ad\u0131").option("-d, --dir <path>", "Dizin (varsay\u0131lan: \xE7al\u0131\u015Fma dizini)").action((options) => initCommand(options));
|
|
140
|
+
program.command("log").description("Yeni bir karar ekler (interaktif veya flag'lerle)").option("-t, --topic <topic>", "Konu (\xF6rn: auth, database)").option("-d, --decision <text>", "Al\u0131nan karar").option("-r, --rationale <text>", "Gerek\xE7e").option("-i, --impact <level>", "Etki: low|medium|high|critical").option("--tags <tags>", "Etiketler (virg\xFClle ayr\u0131lm\u0131\u015F)").option("-f, --file <path>", "DECISIONS.toon dosya yolu").action(async (options) => {
|
|
141
|
+
await logCommand(options);
|
|
142
|
+
});
|
|
143
|
+
program.command("search").description("Ge\xE7mi\u015F kararlarda arama yapar").option("-k, --keywords <words>", "Anahtar kelimeler (virg\xFClle ayr\u0131lm\u0131\u015F)").option("--tags <tags>", "Etiketler (virg\xFClle ayr\u0131lm\u0131\u015F)").option("--impact <level>", "Etki filtresi: low|medium|high|critical").option("-n, --limit <n>", "Maksimum sonu\xE7 say\u0131s\u0131", "5").option("-f, --file <path>", "DECISIONS.toon dosya yolu").action((options) => searchCommand(options));
|
|
144
|
+
program.command("summary").description("Kararlar\u0131n \xF6zetini g\xF6r\xFCnt\xFCler").option("--no-recent", "Son kararlar\u0131 dahil etme").option("-f, --file <path>", "DECISIONS.toon dosya yolu").action((options) => summaryCommand(options));
|
|
145
|
+
program.parse();
|
package/dist/mcp.js
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
appendDecision,
|
|
4
|
+
formatContextSummaryAsToon,
|
|
5
|
+
parseDecisionFile,
|
|
6
|
+
searchDecisions,
|
|
7
|
+
serializeDecisionRow
|
|
8
|
+
} from "./chunk-43MFRJ7K.js";
|
|
9
|
+
|
|
10
|
+
// packages/mcp-server/src/index.ts
|
|
11
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
12
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
13
|
+
|
|
14
|
+
// packages/mcp-server/src/tools/log_decision.ts
|
|
15
|
+
import { z } from "zod";
|
|
16
|
+
|
|
17
|
+
// packages/mcp-server/src/config.ts
|
|
18
|
+
import * as fs from "fs";
|
|
19
|
+
import * as path from "path";
|
|
20
|
+
import * as os from "os";
|
|
21
|
+
function resolveFilePath() {
|
|
22
|
+
if (process.env.DECISION_MEMORY_FILE) {
|
|
23
|
+
return process.env.DECISION_MEMORY_FILE;
|
|
24
|
+
}
|
|
25
|
+
const local = path.join(process.cwd(), "DECISIONS.toon");
|
|
26
|
+
if (fs.existsSync(local)) return local;
|
|
27
|
+
const globalDir = path.join(os.homedir(), ".decision-memory");
|
|
28
|
+
fs.mkdirSync(globalDir, { recursive: true });
|
|
29
|
+
return path.join(globalDir, "global.toon");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// packages/mcp-server/src/tools/log_decision.ts
|
|
33
|
+
var logDecisionSchema = {
|
|
34
|
+
topic: z.string().describe(
|
|
35
|
+
"Karar\u0131n konusu (kebab-case). \xD6rn: auth, database, api-design, testing, deployment"
|
|
36
|
+
),
|
|
37
|
+
decision: z.string().describe(
|
|
38
|
+
"Al\u0131nan somut karar. Net ve spesifik olun: 'JWT RS256 kullan' gibi, 'token kullan' gibi de\u011Fil"
|
|
39
|
+
),
|
|
40
|
+
rationale: z.string().describe(
|
|
41
|
+
"Neden bu tercih yap\u0131ld\u0131? Alternatifler neden elendi?"
|
|
42
|
+
),
|
|
43
|
+
impact: z.enum(["low", "medium", "high", "critical"]).describe(
|
|
44
|
+
"low=stil/ara\xE7 se\xE7imi, medium=\xF6zellik tasar\u0131m\u0131, high=mimari, critical=g\xFCvenlik/veri"
|
|
45
|
+
),
|
|
46
|
+
tags: z.array(z.string()).describe("Aranabilir etiketler. \xD6rn: ['auth', 'jwt', 'security']")
|
|
47
|
+
};
|
|
48
|
+
async function logDecisionHandler(input) {
|
|
49
|
+
const filePath = resolveFilePath();
|
|
50
|
+
const entry = appendDecision(filePath, input);
|
|
51
|
+
return {
|
|
52
|
+
content: [
|
|
53
|
+
{
|
|
54
|
+
type: "text",
|
|
55
|
+
text: `Karar kaydedildi: ${entry.id}
|
|
56
|
+
Konu: ${entry.topic}
|
|
57
|
+
Karar: ${entry.decision}`
|
|
58
|
+
}
|
|
59
|
+
]
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// packages/mcp-server/src/tools/search_decisions.ts
|
|
64
|
+
import * as fs2 from "fs";
|
|
65
|
+
import { z as z2 } from "zod";
|
|
66
|
+
var searchDecisionsSchema = {
|
|
67
|
+
keywords: z2.array(z2.string()).optional().describe("Karar metni ve gerek\xE7ede aranacak kelimeler"),
|
|
68
|
+
tags: z2.array(z2.string()).optional().describe("Filtrelenecek etiketler. \xD6rn: ['auth', 'security']"),
|
|
69
|
+
impact: z2.enum(["low", "medium", "high", "critical"]).optional().describe("Etki seviyesi filtresi"),
|
|
70
|
+
limit: z2.number().min(1).max(20).default(5).describe("Maksimum sonu\xE7 say\u0131s\u0131")
|
|
71
|
+
};
|
|
72
|
+
async function searchDecisionsHandler(input) {
|
|
73
|
+
const filePath = resolveFilePath();
|
|
74
|
+
if (!fs2.existsSync(filePath)) {
|
|
75
|
+
return {
|
|
76
|
+
content: [
|
|
77
|
+
{
|
|
78
|
+
type: "text",
|
|
79
|
+
text: "DECISIONS.toon dosyas\u0131 bulunamad\u0131. Proje k\xF6k\xFCnde 'npx decision-memory init' \xE7al\u0131\u015Ft\u0131r\u0131n."
|
|
80
|
+
}
|
|
81
|
+
]
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
const content = fs2.readFileSync(filePath, "utf-8");
|
|
85
|
+
const file = parseDecisionFile(content);
|
|
86
|
+
const results = searchDecisions(file, {
|
|
87
|
+
keywords: input.keywords,
|
|
88
|
+
tags: input.tags,
|
|
89
|
+
impact: input.impact,
|
|
90
|
+
limit: input.limit
|
|
91
|
+
});
|
|
92
|
+
if (results.length === 0) {
|
|
93
|
+
return {
|
|
94
|
+
content: [{ type: "text", text: "E\u015Fle\u015Fen karar bulunamad\u0131." }]
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
const header = `decisions[${results.length}]{id,ts,topic,decision,rationale,impact,tags}:`;
|
|
98
|
+
const rows = results.map((r) => serializeDecisionRow(r.decision)).join("\n");
|
|
99
|
+
return {
|
|
100
|
+
content: [{ type: "text", text: `${header}
|
|
101
|
+
${rows}` }]
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// packages/mcp-server/src/tools/get_context_summary.ts
|
|
106
|
+
import * as fs3 from "fs";
|
|
107
|
+
import { z as z3 } from "zod";
|
|
108
|
+
var getContextSummarySchema = {
|
|
109
|
+
include_recent: z3.boolean().default(true).describe(
|
|
110
|
+
"Son 5 high/critical karar\u0131 dahil et. Oturum ba\u015F\u0131nda true kullan."
|
|
111
|
+
)
|
|
112
|
+
};
|
|
113
|
+
async function getContextSummaryHandler(input) {
|
|
114
|
+
const filePath = resolveFilePath();
|
|
115
|
+
if (!fs3.existsSync(filePath)) {
|
|
116
|
+
return {
|
|
117
|
+
content: [
|
|
118
|
+
{
|
|
119
|
+
type: "text",
|
|
120
|
+
text: "DECISIONS.toon bulunamad\u0131. Bu projede hen\xFCz karar kaydedilmemi\u015F."
|
|
121
|
+
}
|
|
122
|
+
]
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
const content = fs3.readFileSync(filePath, "utf-8");
|
|
126
|
+
const file = parseDecisionFile(content);
|
|
127
|
+
const summary = formatContextSummaryAsToon(file, input.include_recent);
|
|
128
|
+
return {
|
|
129
|
+
content: [{ type: "text", text: summary }]
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// packages/mcp-server/src/tools/update_decision.ts
|
|
134
|
+
import { z as z4 } from "zod";
|
|
135
|
+
var updateDecisionSchema = {
|
|
136
|
+
supersedes_id: z4.string().describe("Ge\xE7ersiz k\u0131l\u0131nan karar\u0131n ID'si. \xD6rn: 'D003'"),
|
|
137
|
+
topic: z4.string().describe("Yeni karar\u0131n konusu"),
|
|
138
|
+
decision: z4.string().describe("Yeni al\u0131nan karar"),
|
|
139
|
+
rationale: z4.string().describe("Neden \xF6nceki karar de\u011Fi\u015Fti? Yeni gerek\xE7e nedir?"),
|
|
140
|
+
impact: z4.enum(["low", "medium", "high", "critical"]),
|
|
141
|
+
tags: z4.array(z4.string())
|
|
142
|
+
};
|
|
143
|
+
async function updateDecisionHandler(input) {
|
|
144
|
+
const filePath = resolveFilePath();
|
|
145
|
+
const enrichedRationale = `[supersedes:${input.supersedes_id}] ${input.rationale}`;
|
|
146
|
+
const entry = appendDecision(filePath, {
|
|
147
|
+
topic: input.topic,
|
|
148
|
+
decision: input.decision,
|
|
149
|
+
rationale: enrichedRationale,
|
|
150
|
+
impact: input.impact,
|
|
151
|
+
tags: [...input.tags, `supersedes:${input.supersedes_id}`]
|
|
152
|
+
});
|
|
153
|
+
return {
|
|
154
|
+
content: [
|
|
155
|
+
{
|
|
156
|
+
type: "text",
|
|
157
|
+
text: `G\xFCncelleme kaydedildi: ${entry.id} \u2192 ${input.supersedes_id} karar\u0131n\u0131n yerini ald\u0131.
|
|
158
|
+
Yeni karar: ${entry.decision}`
|
|
159
|
+
}
|
|
160
|
+
]
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// packages/mcp-server/src/index.ts
|
|
165
|
+
var server = new McpServer({
|
|
166
|
+
name: "decision-memory",
|
|
167
|
+
version: "0.1.0"
|
|
168
|
+
});
|
|
169
|
+
server.registerTool(
|
|
170
|
+
"log_decision",
|
|
171
|
+
{
|
|
172
|
+
description: "\xD6nemli bir mimari veya uygulama karar\u0131n\u0131 kal\u0131c\u0131 haf\u0131zaya kaydeder. Teknoloji se\xE7imi, mimari yakla\u015F\u0131m, g\xFCvenlik karar\u0131 veya API tasar\u0131m\u0131 gibi \xF6nemli tercihler yapt\u0131\u011F\u0131n\u0131zda bu arac\u0131 \xE7a\u011F\u0131r\u0131n.",
|
|
173
|
+
inputSchema: logDecisionSchema
|
|
174
|
+
},
|
|
175
|
+
logDecisionHandler
|
|
176
|
+
);
|
|
177
|
+
server.registerTool(
|
|
178
|
+
"search_decisions",
|
|
179
|
+
{
|
|
180
|
+
description: "Ge\xE7mi\u015F kararlar\u0131 keyword veya tag ile arar. Mimari bir karar vermeden \xF6nce tutarl\u0131l\u0131\u011F\u0131 korumak i\xE7in veya daha \xF6nce ayn\u0131 konuda karar al\u0131n\u0131p al\u0131nmad\u0131\u011F\u0131n\u0131 kontrol etmek i\xE7in kullan\u0131n.",
|
|
181
|
+
inputSchema: searchDecisionsSchema
|
|
182
|
+
},
|
|
183
|
+
searchDecisionsHandler
|
|
184
|
+
);
|
|
185
|
+
server.registerTool(
|
|
186
|
+
"get_context_summary",
|
|
187
|
+
{
|
|
188
|
+
description: "T\xFCm ge\xE7mi\u015F kararlar\u0131n kompakt \xF6zetini d\xF6nd\xFCr\xFCr. Oturum ba\u015F\u0131nda bir kez \xE7a\u011F\u0131rarak projenin karar ge\xE7mi\u015Fine hakim olun. \xC7\u0131kt\u0131 ~200 token ile s\u0131n\u0131rl\u0131d\u0131r.",
|
|
189
|
+
inputSchema: getContextSummarySchema
|
|
190
|
+
},
|
|
191
|
+
getContextSummaryHandler
|
|
192
|
+
);
|
|
193
|
+
server.registerTool(
|
|
194
|
+
"update_decision",
|
|
195
|
+
{
|
|
196
|
+
description: "\xD6nceki bir karar\u0131 ge\xE7ersiz k\u0131larak yeni bir karar kaydeder. Bir mimari tercih de\u011Fi\u015Fti\u011Finde veya \xF6nceki karar revize edildi\u011Finde kullan\u0131n.",
|
|
197
|
+
inputSchema: updateDecisionSchema
|
|
198
|
+
},
|
|
199
|
+
updateDecisionHandler
|
|
200
|
+
);
|
|
201
|
+
async function main() {
|
|
202
|
+
const transport = new StdioServerTransport();
|
|
203
|
+
await server.connect(transport);
|
|
204
|
+
console.error("decision-memory MCP server ba\u015Flat\u0131ld\u0131");
|
|
205
|
+
}
|
|
206
|
+
main().catch((err) => {
|
|
207
|
+
console.error("MCP server hatas\u0131:", err);
|
|
208
|
+
process.exit(1);
|
|
209
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "decision-memory",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Claude Code için karar hafızası — CLI ve MCP server",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"decision-memory": "./dist/index.js",
|
|
8
|
+
"decision-memory-mcp": "./dist/mcp.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=20.0.0"
|
|
15
|
+
},
|
|
16
|
+
"author": "baadir",
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"publishConfig": {
|
|
19
|
+
"access": "public"
|
|
20
|
+
}
|
|
21
|
+
}
|