@contextline/contextline 0.1.1 → 0.2.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/README.md +154 -178
- package/dist/cli.js +324 -545
- package/dist/cli.js.map +1 -1
- package/dist/postinstall.js +1 -1
- package/dist/postinstall.js.map +1 -1
- package/docs/why-contextline.md +116 -120
- package/package.json +56 -59
- package/dist/index.d.ts +0 -2
- package/dist/index.js +0 -695
- package/dist/index.js.map +0 -1
package/dist/index.js
DELETED
|
@@ -1,695 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
// src/mcpServer.ts
|
|
4
|
-
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
|
-
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
|
-
|
|
7
|
-
// src/tools/dispatch.ts
|
|
8
|
-
import { z } from "zod";
|
|
9
|
-
|
|
10
|
-
// src/tools/appendFragment.ts
|
|
11
|
-
import fs5 from "fs/promises";
|
|
12
|
-
|
|
13
|
-
// src/config.ts
|
|
14
|
-
import fs from "fs";
|
|
15
|
-
import path from "path";
|
|
16
|
-
function getMemoryRoot() {
|
|
17
|
-
if (process.env.CONTEXTLINE_ROOT) return path.resolve(process.env.CONTEXTLINE_ROOT);
|
|
18
|
-
return path.join(process.cwd(), ".contextline");
|
|
19
|
-
}
|
|
20
|
-
function resolveRoot(folder) {
|
|
21
|
-
if (folder) return path.resolve(folder);
|
|
22
|
-
return getMemoryRoot();
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// src/paths.ts
|
|
26
|
-
import path2 from "path";
|
|
27
|
-
|
|
28
|
-
// src/core/errors.ts
|
|
29
|
-
var ContextLineError = class extends Error {
|
|
30
|
-
constructor(message) {
|
|
31
|
-
super(message);
|
|
32
|
-
this.name = "ContextLineError";
|
|
33
|
-
}
|
|
34
|
-
};
|
|
35
|
-
var PathSafetyError = class extends ContextLineError {
|
|
36
|
-
constructor(message) {
|
|
37
|
-
super(message);
|
|
38
|
-
this.name = "PathSafetyError";
|
|
39
|
-
}
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
// src/paths.ts
|
|
43
|
-
var L1_FILE = "L1_Quick.md";
|
|
44
|
-
var L2_DIR = "L2_Deep";
|
|
45
|
-
var L3_DIR = "L3_Details";
|
|
46
|
-
function assertSafeRelative(input) {
|
|
47
|
-
if (!input || input.trim() !== input) {
|
|
48
|
-
throw new PathSafetyError("Path input must be non-empty and contain no leading/trailing whitespace.");
|
|
49
|
-
}
|
|
50
|
-
if (path2.isAbsolute(input)) {
|
|
51
|
-
throw new PathSafetyError("Absolute paths are not allowed in tool inputs.");
|
|
52
|
-
}
|
|
53
|
-
const parts = input.split(/[\\/]+/);
|
|
54
|
-
if (parts.includes("..") || parts.includes("")) {
|
|
55
|
-
throw new PathSafetyError("Path traversal and empty path segments are not allowed.");
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
function assertSafeRelativeFile(input) {
|
|
59
|
-
assertSafeRelative(input);
|
|
60
|
-
if (!input.endsWith(".md")) {
|
|
61
|
-
throw new PathSafetyError("Memory file paths must end with .md.");
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
function safeJoin(root, ...segments) {
|
|
65
|
-
for (const segment of segments) {
|
|
66
|
-
assertSafeRelative(segment);
|
|
67
|
-
}
|
|
68
|
-
const resolvedRoot = path2.resolve(root);
|
|
69
|
-
const target = path2.resolve(resolvedRoot, ...segments);
|
|
70
|
-
const rel = path2.relative(resolvedRoot, target);
|
|
71
|
-
if (rel.startsWith("..") || path2.isAbsolute(rel)) {
|
|
72
|
-
throw new PathSafetyError("Resolved path escapes ContextLine root.");
|
|
73
|
-
}
|
|
74
|
-
return target;
|
|
75
|
-
}
|
|
76
|
-
function deepPath(root, file) {
|
|
77
|
-
assertSafeRelativeFile(file);
|
|
78
|
-
if (file.includes("/") || file.includes("\\")) {
|
|
79
|
-
throw new PathSafetyError("L2 deep memory files must be flat filenames inside L2_Deep for V1.");
|
|
80
|
-
}
|
|
81
|
-
return safeJoin(root, L2_DIR, file);
|
|
82
|
-
}
|
|
83
|
-
function detailPath(root, file) {
|
|
84
|
-
assertSafeRelativeFile(file);
|
|
85
|
-
return safeJoin(root, L3_DIR, file);
|
|
86
|
-
}
|
|
87
|
-
function l1Path(root) {
|
|
88
|
-
return safeJoin(root, L1_FILE);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// src/core/atomicWrite.ts
|
|
92
|
-
import fs2 from "fs/promises";
|
|
93
|
-
import path3 from "path";
|
|
94
|
-
async function atomicWriteFile(filePath, content) {
|
|
95
|
-
await fs2.mkdir(path3.dirname(filePath), { recursive: true });
|
|
96
|
-
const tmp = path3.join(path3.dirname(filePath), `.${path3.basename(filePath)}.${process.pid}.${Date.now()}.tmp`);
|
|
97
|
-
const handle = await fs2.open(tmp, "w");
|
|
98
|
-
try {
|
|
99
|
-
await handle.writeFile(content, "utf8");
|
|
100
|
-
await handle.sync();
|
|
101
|
-
} finally {
|
|
102
|
-
await handle.close();
|
|
103
|
-
}
|
|
104
|
-
await fs2.rename(tmp, filePath);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// src/core/markdown.ts
|
|
108
|
-
function ensureTrailingNewline(content) {
|
|
109
|
-
return content.endsWith("\n") ? content : `${content}
|
|
110
|
-
`;
|
|
111
|
-
}
|
|
112
|
-
function upsertLineByPrefix(content, prefix, nextLine) {
|
|
113
|
-
const lines = ensureTrailingNewline(content).split("\n");
|
|
114
|
-
const index = lines.findIndex((line) => line.trimStart().startsWith(prefix));
|
|
115
|
-
if (index >= 0) {
|
|
116
|
-
lines[index] = nextLine;
|
|
117
|
-
} else {
|
|
118
|
-
lines.splice(Math.max(0, lines.length - 1), 0, nextLine);
|
|
119
|
-
}
|
|
120
|
-
return ensureTrailingNewline(lines.join("\n").replace(/\n{3,}/g, "\n\n"));
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// src/core/details.ts
|
|
124
|
-
import { format, parseISO } from "date-fns";
|
|
125
|
-
function detailFilename(dateInput) {
|
|
126
|
-
const date = dateInput ? parseISO(dateInput) : /* @__PURE__ */ new Date();
|
|
127
|
-
if (Number.isNaN(date.getTime())) {
|
|
128
|
-
throw new Error("Invalid date. Expected YYYY-MM-DD.");
|
|
129
|
-
}
|
|
130
|
-
return `L3_${format(date, "yyyy_MM")}.md`;
|
|
131
|
-
}
|
|
132
|
-
function detailHeader(dateInput) {
|
|
133
|
-
const date = dateInput ? parseISO(dateInput) : /* @__PURE__ */ new Date();
|
|
134
|
-
if (Number.isNaN(date.getTime())) {
|
|
135
|
-
throw new Error("Invalid date. Expected YYYY-MM-DD.");
|
|
136
|
-
}
|
|
137
|
-
return `# ${format(date, "MMMM yyyy")} Detailed Memory
|
|
138
|
-
|
|
139
|
-
`;
|
|
140
|
-
}
|
|
141
|
-
function detailLine(text, dateInput) {
|
|
142
|
-
const date = dateInput ? parseISO(dateInput) : /* @__PURE__ */ new Date();
|
|
143
|
-
if (Number.isNaN(date.getTime())) {
|
|
144
|
-
throw new Error("Invalid date. Expected YYYY-MM-DD.");
|
|
145
|
-
}
|
|
146
|
-
const clean = text.replace(/\s+/g, " ").trim();
|
|
147
|
-
if (!clean) {
|
|
148
|
-
throw new Error("Detail text must be non-empty.");
|
|
149
|
-
}
|
|
150
|
-
return `* [${format(date, "yyyy-MM-dd")}]: ${clean}`;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// src/core/lock.ts
|
|
154
|
-
import fs3 from "fs/promises";
|
|
155
|
-
import path4 from "path";
|
|
156
|
-
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
157
|
-
async function withLock(root, fn) {
|
|
158
|
-
const lockPath = path4.join(root, ".lock");
|
|
159
|
-
const start = Date.now();
|
|
160
|
-
let handle;
|
|
161
|
-
while (!handle) {
|
|
162
|
-
try {
|
|
163
|
-
await fs3.mkdir(root, { recursive: true });
|
|
164
|
-
handle = await fs3.open(lockPath, "wx");
|
|
165
|
-
} catch (error) {
|
|
166
|
-
const code = error.code;
|
|
167
|
-
if (code !== "EEXIST" || Date.now() - start > 5e3) {
|
|
168
|
-
throw error;
|
|
169
|
-
}
|
|
170
|
-
await sleep(50);
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
try {
|
|
174
|
-
return await fn();
|
|
175
|
-
} finally {
|
|
176
|
-
await handle.close();
|
|
177
|
-
await fs3.rm(lockPath, { force: true });
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// src/tools/init.ts
|
|
182
|
-
import fs4 from "fs/promises";
|
|
183
|
-
import path5 from "path";
|
|
184
|
-
|
|
185
|
-
// src/core/instructions.ts
|
|
186
|
-
var CONTEXTLINE_INSTRUCTIONS_FILE = "contextline_instructions.md";
|
|
187
|
-
var DEFAULT_CONTEXTLINE_INSTRUCTIONS = `# ContextLine Instructions
|
|
188
|
-
|
|
189
|
-
CONTEXTLINE-INSTRUCTIONS-V1
|
|
190
|
-
This file contains generic operating instructions for ContextLine memory.
|
|
191
|
-
Use the single MCP tool named "contextline" for all ContextLine reads and writes.
|
|
192
|
-
Do not use shell commands to read or write .contextline files.
|
|
193
|
-
Do not mention memory saving unless the user asks.
|
|
194
|
-
|
|
195
|
-
## Operating Policy
|
|
196
|
-
|
|
197
|
-
Use the single ContextLine MCP tool "contextline" silently for durable memory. Do not use shell commands to read or write .contextline files. Do not mention memory saving unless asked.
|
|
198
|
-
|
|
199
|
-
Durable memory means stable facts likely to help future sessions: user/project facts, preferences, goals, constraints, decisions, relationships, recurring patterns, and important events. Do not save filler, acknowledgements, or one-off chatter.
|
|
200
|
-
|
|
201
|
-
Do not save every message. Periodically batch-save useful durable memory since the last ContextLine save in this session.
|
|
202
|
-
|
|
203
|
-
Use ContextLine as a human-like cache:
|
|
204
|
-
|
|
205
|
-
- L1_Quick.md stores compact facts, summaries, and active pointers.
|
|
206
|
-
- L2_Deep/ stores deeper facts per important object, person, event, project, preference area, or decision thread.
|
|
207
|
-
- L3_Details/ stores timestamped specifics, receipts, and supporting history.
|
|
208
|
-
|
|
209
|
-
For V1, use flat L2 filenames with semantic prefixes instead of folders. Prefer names like person_user.md, project_contextline.md, area_work.md, place_san_jose.md, preference_media.md, preference_language.md, decision_memory_architecture.md, event_launch.md, or org_meta.md.
|
|
210
|
-
|
|
211
|
-
When saving durable memory, complete the full chain. Do not stop after writing L3_Details:
|
|
212
|
-
|
|
213
|
-
1. Append one L3_Details receipt when supporting specifics are useful.
|
|
214
|
-
2. Cross-save extracted meaning into every relevant L2_Deep file.
|
|
215
|
-
3. Update L1_Quick only for compact high-level facts or active pointers.
|
|
216
|
-
4. Link every L1 line to relevant L2 files.
|
|
217
|
-
5. Link every L2 line to relevant L3 details when supporting detail exists.
|
|
218
|
-
|
|
219
|
-
Typical tool sequence:
|
|
220
|
-
|
|
221
|
-
Call "contextline" once with action "save_memory", including:
|
|
222
|
-
|
|
223
|
-
- detail.text for the L3_Details receipt
|
|
224
|
-
- deep[] entries for every relevant L2_Deep file, such as person_user.md, area_work.md, place_san_jose.md, or preference_media.md
|
|
225
|
-
- quick[] entries for compact L1_Quick facts linked to L2 files
|
|
226
|
-
|
|
227
|
-
If you write L3_Details but do not update L2_Deep and L1_Quick, the memory is incomplete. Prefer the bundled "save_memory" action so the full chain is saved together.
|
|
228
|
-
`;
|
|
229
|
-
|
|
230
|
-
// src/tools/init.ts
|
|
231
|
-
var DEFAULT_L1 = `# ContextLine L1 Index
|
|
232
|
-
|
|
233
|
-
This is the quick memory cache: compact facts, summaries, and active pointers.
|
|
234
|
-
|
|
235
|
-
## Quick Facts
|
|
236
|
-
|
|
237
|
-
`;
|
|
238
|
-
async function initContextLine(root) {
|
|
239
|
-
const created = [];
|
|
240
|
-
await fs4.mkdir(root, { recursive: true });
|
|
241
|
-
for (const dir of [
|
|
242
|
-
L2_DIR,
|
|
243
|
-
L3_DIR,
|
|
244
|
-
"maintenance",
|
|
245
|
-
"maintenance/proposals"
|
|
246
|
-
]) {
|
|
247
|
-
const target = safeJoin(root, ...dir.split("/"));
|
|
248
|
-
await fs4.mkdir(target, { recursive: true });
|
|
249
|
-
created.push(path5.relative(root, target));
|
|
250
|
-
}
|
|
251
|
-
const files = [
|
|
252
|
-
[L1_FILE, DEFAULT_L1],
|
|
253
|
-
[CONTEXTLINE_INSTRUCTIONS_FILE, DEFAULT_CONTEXTLINE_INSTRUCTIONS],
|
|
254
|
-
["config.json", `${JSON.stringify({ version: 1, capture: { promotion: "selective" } }, null, 2)}
|
|
255
|
-
`]
|
|
256
|
-
];
|
|
257
|
-
for (const [file, content] of files) {
|
|
258
|
-
const target = safeJoin(root, file);
|
|
259
|
-
try {
|
|
260
|
-
await fs4.writeFile(target, content, { encoding: "utf8", flag: "wx" });
|
|
261
|
-
created.push(path5.relative(root, target));
|
|
262
|
-
} catch (error) {
|
|
263
|
-
if (error.code !== "EEXIST") {
|
|
264
|
-
throw error;
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
return { ok: true, root, created };
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
// src/tools/appendFragment.ts
|
|
272
|
-
async function appendFragment(text, date, rootInput) {
|
|
273
|
-
const root = resolveRoot(rootInput);
|
|
274
|
-
await initContextLine(root);
|
|
275
|
-
return withLock(root, async () => {
|
|
276
|
-
const detail_file = detailFilename(date);
|
|
277
|
-
const filePath = detailPath(root, detail_file);
|
|
278
|
-
let content;
|
|
279
|
-
try {
|
|
280
|
-
content = await fs5.readFile(filePath, "utf8");
|
|
281
|
-
} catch (error) {
|
|
282
|
-
if (error.code !== "ENOENT") {
|
|
283
|
-
throw error;
|
|
284
|
-
}
|
|
285
|
-
content = detailHeader(date);
|
|
286
|
-
}
|
|
287
|
-
const fragment = detailLine(text, date);
|
|
288
|
-
await atomicWriteFile(filePath, `${ensureTrailingNewline(content)}${fragment}
|
|
289
|
-
`);
|
|
290
|
-
return {
|
|
291
|
-
ok: true,
|
|
292
|
-
detail_file,
|
|
293
|
-
fragment,
|
|
294
|
-
next_required_steps: [
|
|
295
|
-
"Call contextline with action save_memory to save L3, L2, and L1 together.",
|
|
296
|
-
"If manually continuing, call contextline action update_deep for every relevant L2_Deep file and action update_index for compact L1_Quick facts."
|
|
297
|
-
],
|
|
298
|
-
display: `ContextLine L3 detail appended
|
|
299
|
-
- Detail file: ${detail_file}`
|
|
300
|
-
};
|
|
301
|
-
});
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
// src/tools/hydrate.ts
|
|
305
|
-
import fs6 from "fs/promises";
|
|
306
|
-
async function hydrate(rootInput) {
|
|
307
|
-
const root = resolveRoot(rootInput);
|
|
308
|
-
await initContextLine(root);
|
|
309
|
-
const l1_path = l1Path(root);
|
|
310
|
-
const instructions_path = safeJoin(root, CONTEXTLINE_INSTRUCTIONS_FILE);
|
|
311
|
-
const content = await fs6.readFile(l1_path, "utf8");
|
|
312
|
-
const instructions = await fs6.readFile(instructions_path, "utf8");
|
|
313
|
-
return {
|
|
314
|
-
ok: true,
|
|
315
|
-
root,
|
|
316
|
-
l1_path,
|
|
317
|
-
instructions_path,
|
|
318
|
-
content,
|
|
319
|
-
instructions,
|
|
320
|
-
instruction: "Use this L1 content as quick memory. Follow the included ContextLine instructions for reading and saving memory.",
|
|
321
|
-
display: `ContextLine hydrated
|
|
322
|
-
- Quick memory: ${l1_path}
|
|
323
|
-
- Instructions: ${instructions_path}`
|
|
324
|
-
};
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
// src/tools/inspect.ts
|
|
328
|
-
import fs7 from "fs/promises";
|
|
329
|
-
import path6 from "path";
|
|
330
|
-
async function countFiles(dir) {
|
|
331
|
-
try {
|
|
332
|
-
return (await fs7.readdir(dir)).length;
|
|
333
|
-
} catch {
|
|
334
|
-
return 0;
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
async function inspect(rootInput) {
|
|
338
|
-
const root = resolveRoot(rootInput);
|
|
339
|
-
return {
|
|
340
|
-
root,
|
|
341
|
-
l1: path6.join(root, L1_FILE),
|
|
342
|
-
l2_deep_files: await countFiles(safeJoin(root, L2_DIR)),
|
|
343
|
-
l3_detail_files: await countFiles(safeJoin(root, L3_DIR))
|
|
344
|
-
};
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
// src/tools/proposeCompaction.ts
|
|
348
|
-
import { format as format2 } from "date-fns";
|
|
349
|
-
async function proposeCompaction(fileA, fileB, reason, rootInput) {
|
|
350
|
-
const root = resolveRoot(rootInput);
|
|
351
|
-
deepPath(root, fileA);
|
|
352
|
-
deepPath(root, fileB);
|
|
353
|
-
return withLock(root, async () => {
|
|
354
|
-
const stamp = format2(/* @__PURE__ */ new Date(), "yyyyMMdd_HHmmss");
|
|
355
|
-
const proposal = `proposal_${stamp}_${fileA.replace(/\W+/g, "_")}_${fileB.replace(/\W+/g, "_")}.md`;
|
|
356
|
-
const proposal_path = safeJoin(root, "maintenance", "proposals", proposal);
|
|
357
|
-
const content = `# Deep Memory Compaction Proposal
|
|
358
|
-
|
|
359
|
-
Created: ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
360
|
-
|
|
361
|
-
L2 File A: [[${fileA}]]
|
|
362
|
-
L2 File B: [[${fileB}]]
|
|
363
|
-
|
|
364
|
-
## Reason
|
|
365
|
-
|
|
366
|
-
${reason.trim()}
|
|
367
|
-
|
|
368
|
-
## Instruction
|
|
369
|
-
|
|
370
|
-
Review manually. V1 never merges or mutates deep memory files automatically.
|
|
371
|
-
`;
|
|
372
|
-
await atomicWriteFile(proposal_path, content);
|
|
373
|
-
return { ok: true, proposal_path };
|
|
374
|
-
});
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
// src/tools/readDeep.ts
|
|
378
|
-
import fs8 from "fs/promises";
|
|
379
|
-
|
|
380
|
-
// src/core/links.ts
|
|
381
|
-
var WIKI_LINK_RE = /\[\[([^\]]+)\]\]/g;
|
|
382
|
-
function extractWikiLinks(content) {
|
|
383
|
-
return [...content.matchAll(WIKI_LINK_RE)].map((match) => match[1]);
|
|
384
|
-
}
|
|
385
|
-
function hasWikiLink(line, filename) {
|
|
386
|
-
return extractWikiLinks(line).includes(filename);
|
|
387
|
-
}
|
|
388
|
-
function ensureWikiLink(line, filename) {
|
|
389
|
-
return hasWikiLink(line, filename) ? line : `${line.trim()} [[${filename}]]`;
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
// src/tools/readDeep.ts
|
|
393
|
-
async function readDeep(file, rootInput) {
|
|
394
|
-
const root = resolveRoot(rootInput);
|
|
395
|
-
await initContextLine(root);
|
|
396
|
-
const deep_memory_path = deepPath(root, file);
|
|
397
|
-
const content = await fs8.readFile(deep_memory_path, "utf8");
|
|
398
|
-
const l3_links = extractWikiLinks(content).filter((link) => link.startsWith("L3_") && link.endsWith(".md"));
|
|
399
|
-
return { l2_path: deep_memory_path, deep_memory_path, content, l3_links };
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
// src/tools/readDetails.ts
|
|
403
|
-
import fs9 from "fs/promises";
|
|
404
|
-
async function readDetails(file, rootInput) {
|
|
405
|
-
const root = resolveRoot(rootInput);
|
|
406
|
-
await initContextLine(root);
|
|
407
|
-
const detail_path = detailPath(root, file);
|
|
408
|
-
const content = await fs9.readFile(detail_path, "utf8");
|
|
409
|
-
return { l3_path: detail_path, detail_path, content };
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
// src/tools/updateDeepPointer.ts
|
|
413
|
-
import fs10 from "fs/promises";
|
|
414
|
-
import path7 from "path";
|
|
415
|
-
async function updateDeepPointer(file, factText, detailFile, rootInput) {
|
|
416
|
-
const root = resolveRoot(rootInput);
|
|
417
|
-
await initContextLine(root);
|
|
418
|
-
detailPath(root, detailFile);
|
|
419
|
-
return withLock(root, async () => {
|
|
420
|
-
const filePath = deepPath(root, file);
|
|
421
|
-
let content = "";
|
|
422
|
-
try {
|
|
423
|
-
content = await fs10.readFile(filePath, "utf8");
|
|
424
|
-
} catch (error) {
|
|
425
|
-
if (error.code !== "ENOENT") {
|
|
426
|
-
throw error;
|
|
427
|
-
}
|
|
428
|
-
content = `# ${path7.basename(file, ".md")}
|
|
429
|
-
|
|
430
|
-
`;
|
|
431
|
-
}
|
|
432
|
-
const updated_line = ensureWikiLink(factText, detailFile);
|
|
433
|
-
const next = upsertLineByPrefix(ensureTrailingNewline(content), factText.trim(), updated_line);
|
|
434
|
-
await atomicWriteFile(filePath, next);
|
|
435
|
-
return { ok: true, file, updated_line, display: `ContextLine L2 updated
|
|
436
|
-
- File: ${file}` };
|
|
437
|
-
});
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
// src/tools/updateIndexPointer.ts
|
|
441
|
-
import fs11 from "fs/promises";
|
|
442
|
-
async function updateIndexPointer(indexLine, files, rootInput) {
|
|
443
|
-
if (files.some((file) => file.startsWith("L3_"))) {
|
|
444
|
-
throw new Error("L1 quick memory pointers may only reference L2 deep memory files.");
|
|
445
|
-
}
|
|
446
|
-
const root = resolveRoot(rootInput);
|
|
447
|
-
await initContextLine(root);
|
|
448
|
-
for (const file of files) {
|
|
449
|
-
deepPath(root, file);
|
|
450
|
-
}
|
|
451
|
-
return withLock(root, async () => {
|
|
452
|
-
const filePath = l1Path(root);
|
|
453
|
-
const content = await fs11.readFile(filePath, "utf8");
|
|
454
|
-
const updated_line = files.reduce((line, file) => ensureWikiLink(line, file), indexLine);
|
|
455
|
-
const next = upsertLineByPrefix(ensureTrailingNewline(content), indexLine.trim(), updated_line);
|
|
456
|
-
await atomicWriteFile(filePath, next);
|
|
457
|
-
return { ok: true, updated_line, display: "ContextLine L1 updated" };
|
|
458
|
-
});
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
// src/core/topology.ts
|
|
462
|
-
import fs12 from "fs/promises";
|
|
463
|
-
import path8 from "path";
|
|
464
|
-
async function exists(target) {
|
|
465
|
-
try {
|
|
466
|
-
await fs12.access(target);
|
|
467
|
-
return true;
|
|
468
|
-
} catch {
|
|
469
|
-
return false;
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
async function listMarkdown(dir) {
|
|
473
|
-
const out = [];
|
|
474
|
-
try {
|
|
475
|
-
const entries = await fs12.readdir(dir, { withFileTypes: true });
|
|
476
|
-
for (const entry of entries) {
|
|
477
|
-
const full = path8.join(dir, entry.name);
|
|
478
|
-
if (entry.isDirectory()) {
|
|
479
|
-
const nested = await listMarkdown(full);
|
|
480
|
-
out.push(...nested.map((file) => path8.join(entry.name, file)));
|
|
481
|
-
} else if (entry.name.endsWith(".md")) {
|
|
482
|
-
out.push(entry.name);
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
return out;
|
|
486
|
-
} catch {
|
|
487
|
-
return [];
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
async function validateTopology(root) {
|
|
491
|
-
const errors = [];
|
|
492
|
-
const warnings = [];
|
|
493
|
-
const l1 = l1Path(root);
|
|
494
|
-
const l2 = safeJoin(root, L2_DIR);
|
|
495
|
-
const l3 = safeJoin(root, L3_DIR);
|
|
496
|
-
if (!await exists(l1)) errors.push("L1_Quick.md is missing.");
|
|
497
|
-
if (!await exists(l2)) errors.push("L2_Deep directory is missing.");
|
|
498
|
-
if (!await exists(l3)) errors.push("L3_Details directory is missing.");
|
|
499
|
-
if (errors.length) return { ok: false, errors, warnings };
|
|
500
|
-
const l1Content = await fs12.readFile(l1, "utf8");
|
|
501
|
-
for (const link of extractWikiLinks(l1Content)) {
|
|
502
|
-
if (link.startsWith("L3_")) {
|
|
503
|
-
errors.push(`L1 must not link directly to L3 detail file: ${link}`);
|
|
504
|
-
} else if (!await exists(path8.join(l2, link))) {
|
|
505
|
-
errors.push(`L1 links to missing L2 deep memory file: ${link}`);
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
for (const file of await listMarkdown(l2)) {
|
|
509
|
-
const content = await fs12.readFile(path8.join(l2, file), "utf8");
|
|
510
|
-
for (const link of extractWikiLinks(content)) {
|
|
511
|
-
if (link === L1_FILE) {
|
|
512
|
-
errors.push(`L2 deep memory file ${file} must not link upward to L1_Quick.md.`);
|
|
513
|
-
} else if (!link.startsWith("L3_")) {
|
|
514
|
-
warnings.push(`L2 deep memory file ${file} links to non-L3 file: ${link}`);
|
|
515
|
-
} else if (!await exists(path8.join(l3, link))) {
|
|
516
|
-
errors.push(`L2 deep memory file ${file} links to missing L3 detail file: ${link}`);
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
}
|
|
520
|
-
for (const file of await listMarkdown(l3)) {
|
|
521
|
-
const content = await fs12.readFile(path8.join(l3, file), "utf8");
|
|
522
|
-
const links = extractWikiLinks(content);
|
|
523
|
-
if (links.length) {
|
|
524
|
-
errors.push(`L3 detail file ${file} must not contain wiki links.`);
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
return { ok: errors.length === 0, errors, warnings };
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
// src/tools/validateTopology.ts
|
|
531
|
-
async function validateTopology2(rootInput) {
|
|
532
|
-
const root = resolveRoot(rootInput);
|
|
533
|
-
await initContextLine(root);
|
|
534
|
-
const result = await validateTopology(root);
|
|
535
|
-
return {
|
|
536
|
-
...result,
|
|
537
|
-
display: result.ok ? "ContextLine topology valid" : `ContextLine topology invalid
|
|
538
|
-
- Errors: ${result.errors.length}
|
|
539
|
-
- Warnings: ${result.warnings.length}`
|
|
540
|
-
};
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
// src/tools/dispatch.ts
|
|
544
|
-
var contextlineInputSchema = {
|
|
545
|
-
action: z.enum([
|
|
546
|
-
"init",
|
|
547
|
-
"hydrate",
|
|
548
|
-
"read_deep",
|
|
549
|
-
"read_details",
|
|
550
|
-
"append_detail",
|
|
551
|
-
"update_deep",
|
|
552
|
-
"update_index",
|
|
553
|
-
"save_memory",
|
|
554
|
-
"validate",
|
|
555
|
-
"inspect",
|
|
556
|
-
"propose_compaction"
|
|
557
|
-
]),
|
|
558
|
-
root: z.string().optional(),
|
|
559
|
-
file: z.string().optional(),
|
|
560
|
-
detail_file: z.string().optional(),
|
|
561
|
-
text: z.string().optional(),
|
|
562
|
-
date: z.string().optional(),
|
|
563
|
-
files: z.array(z.string()).optional(),
|
|
564
|
-
detail: z.object({
|
|
565
|
-
text: z.string(),
|
|
566
|
-
date: z.string().optional()
|
|
567
|
-
}).optional(),
|
|
568
|
-
deep: z.array(
|
|
569
|
-
z.object({
|
|
570
|
-
file: z.string(),
|
|
571
|
-
text: z.string()
|
|
572
|
-
})
|
|
573
|
-
).optional(),
|
|
574
|
-
quick: z.array(
|
|
575
|
-
z.object({
|
|
576
|
-
text: z.string(),
|
|
577
|
-
files: z.array(z.string())
|
|
578
|
-
})
|
|
579
|
-
).optional(),
|
|
580
|
-
file_a: z.string().optional(),
|
|
581
|
-
file_b: z.string().optional(),
|
|
582
|
-
reason: z.string().optional()
|
|
583
|
-
};
|
|
584
|
-
function requireString(value, name) {
|
|
585
|
-
if (!value) throw new Error(`Missing required field: ${name}`);
|
|
586
|
-
return value;
|
|
587
|
-
}
|
|
588
|
-
async function saveMemory(input) {
|
|
589
|
-
await initContextLine(input.root);
|
|
590
|
-
let detailFile = input.detail_file;
|
|
591
|
-
let detailResult = null;
|
|
592
|
-
if (input.detail) {
|
|
593
|
-
const result = await appendFragment(input.detail.text, input.detail.date, input.root);
|
|
594
|
-
detailFile = result.detail_file;
|
|
595
|
-
detailResult = result;
|
|
596
|
-
}
|
|
597
|
-
const deepResults = [];
|
|
598
|
-
for (const item of input.deep ?? []) {
|
|
599
|
-
if (!detailFile) throw new Error("save_memory deep updates require detail_file or detail.");
|
|
600
|
-
deepResults.push(await updateDeepPointer(item.file, item.text, detailFile, input.root));
|
|
601
|
-
}
|
|
602
|
-
const quickResults = [];
|
|
603
|
-
for (const item of input.quick ?? []) {
|
|
604
|
-
quickResults.push(await updateIndexPointer(item.text, item.files, input.root));
|
|
605
|
-
}
|
|
606
|
-
const validation = await validateTopology2(input.root);
|
|
607
|
-
const deepFiles = deepResults.map((result) => result.file);
|
|
608
|
-
return {
|
|
609
|
-
ok: validation.ok,
|
|
610
|
-
detail_file: detailFile,
|
|
611
|
-
detail: detailResult,
|
|
612
|
-
deep: deepResults,
|
|
613
|
-
quick: quickResults,
|
|
614
|
-
validation,
|
|
615
|
-
display: [
|
|
616
|
-
"ContextLine memory saved",
|
|
617
|
-
detailFile ? `- L3 detail: ${detailFile}` : void 0,
|
|
618
|
-
deepFiles.length ? `- L2 updated: ${deepFiles.join(", ")}` : void 0,
|
|
619
|
-
quickResults.length ? `- L1 updated: ${quickResults.length} line${quickResults.length === 1 ? "" : "s"}` : void 0,
|
|
620
|
-
validation.ok ? "- Topology: valid" : `- Topology errors: ${validation.errors.length}`
|
|
621
|
-
].filter(Boolean).join("\n")
|
|
622
|
-
};
|
|
623
|
-
}
|
|
624
|
-
var handlers = {
|
|
625
|
-
init: (input) => initContextLine(input.root),
|
|
626
|
-
hydrate: (input) => hydrate(input.root),
|
|
627
|
-
read_deep: (input) => readDeep(requireString(input.file, "file"), input.root),
|
|
628
|
-
read_details: (input) => readDetails(requireString(input.file ?? input.detail_file, "file"), input.root),
|
|
629
|
-
append_detail: (input) => appendFragment(requireString(input.text, "text"), input.date, input.root),
|
|
630
|
-
update_deep: (input) => updateDeepPointer(requireString(input.file, "file"), requireString(input.text, "text"), requireString(input.detail_file, "detail_file"), input.root),
|
|
631
|
-
update_index: (input) => updateIndexPointer(requireString(input.text, "text"), input.files ?? [], input.root),
|
|
632
|
-
save_memory: saveMemory,
|
|
633
|
-
validate: (input) => validateTopology2(input.root),
|
|
634
|
-
inspect: (input) => inspect(input.root),
|
|
635
|
-
propose_compaction: (input) => proposeCompaction(requireString(input.file_a, "file_a"), requireString(input.file_b, "file_b"), requireString(input.reason, "reason"), input.root)
|
|
636
|
-
};
|
|
637
|
-
async function dispatchContextline(input) {
|
|
638
|
-
return handlers[input.action](input);
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
// src/prompts/compilerPrompt.ts
|
|
642
|
-
var compilerPrompt = `Compile durable ContextLine memory from the conversation.
|
|
643
|
-
|
|
644
|
-
Rules:
|
|
645
|
-
1. Treat L1, L2, and L3 as a human-like memory cache hierarchy.
|
|
646
|
-
2. L1 Quick Memory stores compact facts, summaries, and active pointers.
|
|
647
|
-
3. L2 Deep Memory stores richer facts per important object, person, event, project, preference area, or decision thread. For V1, use flat filenames with semantic prefixes, such as person_user.md, project_contextline.md, area_work.md, place_san_jose.md, preference_media.md, decision_architecture.md, event_launch.md, or org_meta.md.
|
|
648
|
-
4. L3 Details stores timestamped specifics, receipts, and supporting history.
|
|
649
|
-
5. Deconstruct durable facts into timestamped L3 detail fragments when supporting specifics are useful.
|
|
650
|
-
6. Cross-save meaning into every relevant L2 deep memory file. If a fact involves person A and project B, update both the person file and the project file with the relevant angle for each, pointing both to the same L3 detail receipt when one exists.
|
|
651
|
-
7. Every L1 memory line should include one or more wiki links to relevant L2 files.
|
|
652
|
-
8. Every L2 memory line should include one or more wiki links to relevant L3 detail files when supporting detail exists. If no L3 detail is needed, keep the L2 line concise and link it later when detail is created.
|
|
653
|
-
9. Evolve L1 only for compact high-level facts, important summaries, or active pointers.
|
|
654
|
-
10. Do not duplicate raw wording across L1, L2, and L3. Store the right level of detail at each cache layer.
|
|
655
|
-
11. Prefer updating existing L2 lines when possible.
|
|
656
|
-
12. Do not create new L2 files unless necessary.
|
|
657
|
-
13. Prefer one call to the "contextline" MCP tool with action "save_memory" so L3, L2, and L1 updates are saved together.`;
|
|
658
|
-
|
|
659
|
-
// src/prompts/hydrationPrompt.ts
|
|
660
|
-
var hydrationPrompt = `Use ContextLine as a human-like memory cache.
|
|
661
|
-
|
|
662
|
-
At the start of work, use L1 Quick Memory as the only cold-start memory map. Follow links into L2 Deep Memory files and L3 Details only when needed for the current task. Do not load the entire memory tree.`;
|
|
663
|
-
|
|
664
|
-
// src/mcpServer.ts
|
|
665
|
-
function textResult(data) {
|
|
666
|
-
if (data && typeof data === "object" && "display" in data && typeof data.display === "string") {
|
|
667
|
-
return {
|
|
668
|
-
content: [{ type: "text", text: data.display }],
|
|
669
|
-
structuredContent: data
|
|
670
|
-
};
|
|
671
|
-
}
|
|
672
|
-
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
673
|
-
}
|
|
674
|
-
function createServer() {
|
|
675
|
-
const server = new McpServer({ name: "contextline", version: "0.1.0" });
|
|
676
|
-
server.registerTool("contextline", { inputSchema: contextlineInputSchema }, async (input) => textResult(await dispatchContextline(input)));
|
|
677
|
-
server.registerPrompt("contextline_hydration_guide", {}, () => ({
|
|
678
|
-
messages: [{ role: "user", content: { type: "text", text: hydrationPrompt } }]
|
|
679
|
-
}));
|
|
680
|
-
server.registerPrompt("contextline_compile_update", {}, () => ({
|
|
681
|
-
messages: [{ role: "user", content: { type: "text", text: compilerPrompt } }]
|
|
682
|
-
}));
|
|
683
|
-
return server;
|
|
684
|
-
}
|
|
685
|
-
async function startServer() {
|
|
686
|
-
const server = createServer();
|
|
687
|
-
await server.connect(new StdioServerTransport());
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
// src/index.ts
|
|
691
|
-
startServer().catch((error) => {
|
|
692
|
-
console.error(error);
|
|
693
|
-
process.exit(1);
|
|
694
|
-
});
|
|
695
|
-
//# sourceMappingURL=index.js.map
|