ctxcarry 0.3.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.
@@ -0,0 +1,249 @@
1
+ import { freshState, readEvents, writeMarkdownState, writeState } from "./store.js";
2
+ export function compactState() {
3
+ const events = readEvents();
4
+ const state = freshState();
5
+ for (const event of events) {
6
+ applyEvent(state, event);
7
+ }
8
+ state.updatedAt = new Date().toISOString();
9
+ postProcessState(state);
10
+ writeState(state);
11
+ writeMarkdownState(renderStateMarkdown(state));
12
+ return state;
13
+ }
14
+ export function renderStateMarkdown(state) {
15
+ return [
16
+ "# ctxcarry State",
17
+ "",
18
+ "## Current Task",
19
+ state.working.currentTask,
20
+ "",
21
+ "## Current Status",
22
+ state.working.status,
23
+ "",
24
+ "## Files Touched",
25
+ renderList(state.working.touchedFiles),
26
+ "",
27
+ "## Decisions Made",
28
+ renderMemoryList(state.episodic.decisions),
29
+ "",
30
+ "## Constraints",
31
+ renderMemoryList(state.working.constraints),
32
+ "",
33
+ "## Known Failures",
34
+ renderMemoryList(state.working.failures),
35
+ "",
36
+ "## Last Commands",
37
+ renderList(state.working.lastCommands),
38
+ "",
39
+ "## Next Steps",
40
+ renderMemoryList(state.working.nextSteps)
41
+ ].join("\n");
42
+ }
43
+ function applyEvent(state, event) {
44
+ switch (event.type) {
45
+ case "note":
46
+ applyNote(state, event);
47
+ break;
48
+ case "repo_snapshot":
49
+ applySnapshot(state, event);
50
+ break;
51
+ case "agent_session":
52
+ applySession(state, event);
53
+ break;
54
+ case "command_run":
55
+ applyCommand(state, event);
56
+ break;
57
+ case "file_changed":
58
+ applyFileChanged(state, event);
59
+ break;
60
+ case "session_fallback":
61
+ applyFallback(state, event);
62
+ break;
63
+ }
64
+ }
65
+ function applyNote(state, event) {
66
+ const noteType = String(event.noteType ?? "");
67
+ const content = String(event.content ?? "").trim();
68
+ if (!content) {
69
+ return;
70
+ }
71
+ const item = toMemoryItem(event, content);
72
+ if (noteType === "decision") {
73
+ state.episodic.decisions.push(item);
74
+ }
75
+ else if (noteType === "failure") {
76
+ state.working.failures.push(item);
77
+ state.working.status = "in_progress";
78
+ }
79
+ else if (noteType === "todo") {
80
+ state.working.todos.push(item);
81
+ }
82
+ else if (noteType === "constraint") {
83
+ state.working.constraints.push(item);
84
+ }
85
+ else if (noteType === "task") {
86
+ state.working.currentTask = content;
87
+ state.working.status = "in_progress";
88
+ }
89
+ else if (noteType === "next") {
90
+ state.working.nextSteps.push(item);
91
+ }
92
+ else if (noteType === "resolved") {
93
+ state.episodic.resolved.push(item);
94
+ }
95
+ }
96
+ function applySnapshot(state, event) {
97
+ const branch = typeof event.branch === "string" ? event.branch : null;
98
+ const files = Array.isArray(event.changedFiles) ? event.changedFiles.filter((file) => typeof file === "string") : [];
99
+ state.working.currentBranch = branch;
100
+ state.working.touchedFiles.push(...files);
101
+ if (files.length > 0 && state.working.status === "not_started") {
102
+ state.working.status = "in_progress";
103
+ }
104
+ }
105
+ function applySession(state, event) {
106
+ const agent = typeof event.agent === "string" ? event.agent : "unknown";
107
+ const exitCode = typeof event.exitCode === "number" ? event.exitCode : null;
108
+ const summary = exitCode === 0 ? `${agent} session completed.` : `${agent} session exited with code ${exitCode ?? "unknown"}.`;
109
+ state.episodic.sessions.push(toMemoryItem(event, summary));
110
+ if (typeof event.branch === "string") {
111
+ state.working.currentBranch = event.branch;
112
+ }
113
+ const files = Array.isArray(event.changedFiles) ? event.changedFiles.filter((file) => typeof file === "string") : [];
114
+ state.working.touchedFiles.push(...files);
115
+ if (exitCode !== 0) {
116
+ state.working.failures.push(toMemoryItem(event, summary));
117
+ }
118
+ }
119
+ function applyFileChanged(state, event) {
120
+ const file = typeof event.path === "string" ? event.path : "";
121
+ if (file) {
122
+ state.working.touchedFiles.push(file);
123
+ state.working.status = "in_progress";
124
+ }
125
+ }
126
+ function applyFallback(state, event) {
127
+ if (typeof event.branch === "string") {
128
+ state.working.currentBranch = event.branch;
129
+ }
130
+ const files = Array.isArray(event.changedFiles) ? event.changedFiles.filter((file) => typeof file === "string") : [];
131
+ state.working.touchedFiles.push(...files);
132
+ if (files.length > 0 && state.working.currentTask === "Unspecified") {
133
+ state.working.currentTask = "Continue work from latest agent session";
134
+ }
135
+ if (files.length > 0) {
136
+ state.working.status = "in_progress";
137
+ }
138
+ }
139
+ function applyCommand(state, event) {
140
+ const command = String(event.command ?? "").trim();
141
+ if (!command) {
142
+ return;
143
+ }
144
+ state.working.lastCommands.push(command);
145
+ if (event.status === "failed") {
146
+ const summary = summarizeFailure(String(event.summary ?? `${command} failed.`), command);
147
+ state.working.failures.push(toMemoryItem(event, summary));
148
+ }
149
+ }
150
+ function toMemoryItem(event, content) {
151
+ return {
152
+ content,
153
+ timestamp: event.timestamp,
154
+ agent: typeof event.agent === "string" ? event.agent : undefined
155
+ };
156
+ }
157
+ function renderMemoryList(items) {
158
+ return renderList(items.map((item) => item.content));
159
+ }
160
+ function renderList(items) {
161
+ if (items.length === 0) {
162
+ return "- None recorded";
163
+ }
164
+ return items.map((item) => `- ${item}`).join("\n");
165
+ }
166
+ function unique(values) {
167
+ return [...new Set(values.filter(Boolean))].sort();
168
+ }
169
+ function postProcessState(state) {
170
+ state.working.touchedFiles = unique(state.working.touchedFiles.map(summarizeDiffPath));
171
+ state.working.lastCommands = dedupeStrings(state.working.lastCommands).slice(-10);
172
+ state.working.constraints = dedupeMemoryItems(state.working.constraints);
173
+ state.working.failures = removeResolvedFailures(dedupeMemoryItems(state.working.failures), state.episodic.resolved);
174
+ state.working.todos = dedupeMemoryItems(state.working.todos);
175
+ state.working.nextSteps = dedupeMemoryItems(state.working.nextSteps);
176
+ state.episodic.sessions = dedupeMemoryItems(state.episodic.sessions);
177
+ state.episodic.decisions = dedupeMemoryItems(state.episodic.decisions);
178
+ state.episodic.attempts = dedupeMemoryItems(state.episodic.attempts);
179
+ state.episodic.resolved = dedupeMemoryItems(state.episodic.resolved);
180
+ }
181
+ function dedupeStrings(items) {
182
+ const seen = new Set();
183
+ const result = [];
184
+ for (const item of items) {
185
+ const normalized = normalize(item);
186
+ if (!normalized || seen.has(normalized)) {
187
+ continue;
188
+ }
189
+ seen.add(normalized);
190
+ result.push(item);
191
+ }
192
+ return result;
193
+ }
194
+ function dedupeMemoryItems(items) {
195
+ const seen = new Set();
196
+ const result = [];
197
+ for (const item of items) {
198
+ const content = summarizeFailure(item.content);
199
+ const normalized = normalize(content);
200
+ if (!normalized || seen.has(normalized)) {
201
+ continue;
202
+ }
203
+ seen.add(normalized);
204
+ result.push({ ...item, content });
205
+ }
206
+ return result;
207
+ }
208
+ function removeResolvedFailures(failures, resolved) {
209
+ if (resolved.length === 0) {
210
+ return failures;
211
+ }
212
+ const resolvedText = resolved.map((item) => normalize(item.content));
213
+ return failures.filter((failure) => {
214
+ const text = normalize(failure.content);
215
+ return !resolvedText.some((resolvedItem) => text.includes(resolvedItem) || resolvedItem.includes(text));
216
+ });
217
+ }
218
+ function summarizeFailure(content, command) {
219
+ const lines = dedupeStrings(content
220
+ .split("\n")
221
+ .map((line) => line.trim())
222
+ .filter(Boolean));
223
+ if (lines.length <= 3 && !content.includes(" at ")) {
224
+ return content.trim();
225
+ }
226
+ const error = lines.find((line) => /(?:error:|expected|received|exception|timeout)/i.test(line)) ??
227
+ lines.find((line) => /(?:fail|failed)/i.test(line)) ??
228
+ lines[0] ??
229
+ "Command failed";
230
+ const failingTest = lines.find((line) => /\.(?:test|spec)\.[tj]sx?/i.test(line));
231
+ const appFrame = lines.find((line) => /\bat .*?(?:src|app|lib|components|tests)\//.test(line));
232
+ const parts = [`Error summary: ${error}`];
233
+ if (appFrame) {
234
+ parts.push(`Top app frame: ${appFrame}`);
235
+ }
236
+ if (failingTest && failingTest !== error) {
237
+ parts.push(`Failing test: ${failingTest}`);
238
+ }
239
+ if (command) {
240
+ parts.push(`Command: ${command}`);
241
+ }
242
+ return parts.join("; ");
243
+ }
244
+ function summarizeDiffPath(value) {
245
+ return value.replace(/\s+\|.*$/, "").trim();
246
+ }
247
+ function normalize(value) {
248
+ return value.toLowerCase().replace(/\s+/g, " ").trim();
249
+ }
package/dist/git.js ADDED
@@ -0,0 +1,65 @@
1
+ import { execFileSync } from "node:child_process";
2
+ export function getGitSnapshot() {
3
+ if (!isGitRepo()) {
4
+ return {
5
+ isRepo: false,
6
+ branch: null,
7
+ status: [],
8
+ changedFiles: [],
9
+ diffStat: ""
10
+ };
11
+ }
12
+ const branch = runGit(["branch", "--show-current"]).trim() || null;
13
+ const status = lines(runGit(["status", "--short", "--untracked-files=all"]));
14
+ const changedFiles = unique([
15
+ ...lines(runGit(["diff", "--name-only"])),
16
+ ...lines(runGit(["diff", "--cached", "--name-only"])),
17
+ ...status.map((line) => line.slice(3).trim()).filter(Boolean)
18
+ ].filter((file) => !file.startsWith(".ctxcarry/")));
19
+ const diffStat = runGit(["diff", "--stat"]);
20
+ return {
21
+ isRepo: true,
22
+ branch,
23
+ status,
24
+ changedFiles,
25
+ diffStat
26
+ };
27
+ }
28
+ export function summarizeSnapshot(snapshot) {
29
+ if (!snapshot.isRepo) {
30
+ return "Not a git repository.";
31
+ }
32
+ const changed = snapshot.changedFiles.length === 0 ? "No changed files." : `${snapshot.changedFiles.length} changed file(s).`;
33
+ const branch = snapshot.branch ? `Branch ${snapshot.branch}.` : "Detached or unknown branch.";
34
+ return `${branch} ${changed}`;
35
+ }
36
+ function isGitRepo() {
37
+ try {
38
+ runGit(["rev-parse", "--is-inside-work-tree"]);
39
+ return true;
40
+ }
41
+ catch {
42
+ return false;
43
+ }
44
+ }
45
+ function runGit(args) {
46
+ try {
47
+ return execFileSync("git", args, {
48
+ cwd: process.cwd(),
49
+ encoding: "utf8",
50
+ stdio: ["ignore", "pipe", "ignore"]
51
+ });
52
+ }
53
+ catch {
54
+ return "";
55
+ }
56
+ }
57
+ function lines(value) {
58
+ return value
59
+ .split("\n")
60
+ .map((line) => line.trimEnd())
61
+ .filter(Boolean);
62
+ }
63
+ function unique(values) {
64
+ return [...new Set(values.filter(Boolean))].sort();
65
+ }
package/dist/learn.js ADDED
@@ -0,0 +1,30 @@
1
+ import fs from "node:fs";
2
+ import { ctxcarryPath } from "./paths.js";
3
+ import { readState, writeManagedFile } from "./store.js";
4
+ export function learnFromSessions(apply) {
5
+ const state = readState();
6
+ const lines = [
7
+ "# ctxcarry Learned Guidance",
8
+ "",
9
+ "These rules were mined from local ctxcarry memory. Review before relying on them.",
10
+ "",
11
+ "## Decisions To Preserve",
12
+ ...list(state.episodic.decisions.map((item) => item.content)),
13
+ "",
14
+ "## Resolved Failure Lessons",
15
+ ...list(state.episodic.resolved.map((item) => item.content)),
16
+ "",
17
+ "## Active Failure Warnings",
18
+ ...list(state.working.failures.map((item) => item.content))
19
+ ];
20
+ const markdown = lines.join("\n");
21
+ if (apply) {
22
+ fs.writeFileSync(ctxcarryPath("learned.md"), `${markdown}\n`);
23
+ writeManagedFile("AGENTS.md", markdown);
24
+ writeManagedFile("CLAUDE.md", markdown);
25
+ }
26
+ return markdown;
27
+ }
28
+ function list(items) {
29
+ return items.length > 0 ? items.map((item) => `- ${item}`) : ["- None recorded"];
30
+ }
@@ -0,0 +1,94 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { ctxcarryPath } from "./paths.js";
4
+ import { readState } from "./store.js";
5
+ export async function serveMcp() {
6
+ process.stdin.setEncoding("utf8");
7
+ let buffer = "";
8
+ process.stdin.on("data", (chunk) => {
9
+ buffer += chunk;
10
+ let index;
11
+ while ((index = buffer.indexOf("\n")) >= 0) {
12
+ const line = buffer.slice(0, index).trim();
13
+ buffer = buffer.slice(index + 1);
14
+ if (line)
15
+ handleLine(line);
16
+ }
17
+ });
18
+ process.stdin.on("end", () => {
19
+ process.exit(0);
20
+ });
21
+ }
22
+ function handleLine(line) {
23
+ try {
24
+ const request = JSON.parse(line);
25
+ respond(request.id, dispatch(request));
26
+ }
27
+ catch (error) {
28
+ respond(null, null, error instanceof Error ? error.message : String(error));
29
+ }
30
+ }
31
+ function dispatch(request) {
32
+ if (request.method === "initialize") {
33
+ return {
34
+ protocolVersion: "2024-11-05",
35
+ serverInfo: { name: "ctxcarry", version: "0.3.0" },
36
+ capabilities: { tools: {} }
37
+ };
38
+ }
39
+ if (request.method === "tools/list") {
40
+ return {
41
+ tools: [
42
+ { name: "get_current_task", description: "Return current ctxcarry task state.", inputSchema: { type: "object", properties: {} } },
43
+ { name: "get_latest_ctxcarry", description: "Return latest ctxcarry markdown.", inputSchema: { type: "object", properties: { agent: { type: "string" } } } },
44
+ { name: "get_relevant_session_events", description: "Return recent session events.", inputSchema: { type: "object", properties: { limit: { type: "number" } } } },
45
+ { name: "expand_session_artifact", description: "Read a raw archived session artifact.", inputSchema: { type: "object", properties: { path: { type: "string" } }, required: ["path"] } },
46
+ { name: "summarize_latest_failure", description: "Return the latest active failure.", inputSchema: { type: "object", properties: {} } }
47
+ ]
48
+ };
49
+ }
50
+ if (request.method === "tools/call") {
51
+ return callTool(request.params?.name, request.params?.arguments ?? {});
52
+ }
53
+ return {};
54
+ }
55
+ function callTool(name, args) {
56
+ const state = readState();
57
+ if (name === "get_current_task") {
58
+ return toolText(JSON.stringify(state.working, null, 2));
59
+ }
60
+ if (name === "get_latest_ctxcarry") {
61
+ const agent = typeof args.agent === "string" ? args.agent : "codex";
62
+ const file = ctxcarryPath("ctxcarrys", `${agent}.md`);
63
+ return toolText(fs.existsSync(file) ? fs.readFileSync(file, "utf8") : "");
64
+ }
65
+ if (name === "get_relevant_session_events") {
66
+ const limit = typeof args.limit === "number" ? args.limit : 20;
67
+ const events = readJsonl(ctxcarryPath("events.jsonl")).slice(-limit);
68
+ return toolText(JSON.stringify(events, null, 2));
69
+ }
70
+ if (name === "expand_session_artifact") {
71
+ const requestedPath = String(args.path ?? "");
72
+ const safeRoot = ctxcarryPath("sessions");
73
+ const absolute = path.resolve(requestedPath);
74
+ if (!absolute.startsWith(path.resolve(safeRoot))) {
75
+ throw new Error("Artifact path must be inside .ctxcarry/sessions.");
76
+ }
77
+ return toolText(fs.existsSync(absolute) ? fs.readFileSync(absolute, "utf8") : "");
78
+ }
79
+ if (name === "summarize_latest_failure") {
80
+ return toolText(state.working.failures.at(-1)?.content ?? "No active failure recorded.");
81
+ }
82
+ throw new Error(`Unknown tool: ${name}`);
83
+ }
84
+ function readJsonl(file) {
85
+ if (!fs.existsSync(file))
86
+ return [];
87
+ return fs.readFileSync(file, "utf8").split("\n").filter(Boolean).map((line) => JSON.parse(line));
88
+ }
89
+ function toolText(text) {
90
+ return { content: [{ type: "text", text }] };
91
+ }
92
+ function respond(id, result, error) {
93
+ process.stdout.write(`${JSON.stringify(error ? { jsonrpc: "2.0", id, error: { code: -32000, message: error } } : { jsonrpc: "2.0", id, result })}\n`);
94
+ }
package/dist/paths.js ADDED
@@ -0,0 +1,8 @@
1
+ import path from "node:path";
2
+ export const CTXCARRY_DIR = ".ctxcarry";
3
+ export function rootPath(...parts) {
4
+ return path.join(process.cwd(), ...parts);
5
+ }
6
+ export function ctxcarryPath(...parts) {
7
+ return rootPath(CTXCARRY_DIR, ...parts);
8
+ }
package/dist/redact.js ADDED
@@ -0,0 +1,26 @@
1
+ const SECRET_PATTERNS = [
2
+ [/\b(OPENAI_API_KEY|ANTHROPIC_API_KEY|JWT_SECRET)\s*=\s*[^\s"'`]+/gi, "$1=[REDACTED]"],
3
+ [/\bDATABASE_URL\s*=\s*postgres:\/\/[^\s"'`]+/gi, "DATABASE_URL=[REDACTED]"],
4
+ [/postgres:\/\/[^:\s"'`]+:[^@\s"'`]+@[^\s"'`]+/gi, "postgres://[REDACTED]"],
5
+ [/sk-[A-Za-z0-9_-]+/g, "sk-[REDACTED]"],
6
+ [/-----BEGIN PRIVATE KEY-----[\s\S]*?-----END PRIVATE KEY-----/g, "[REDACTED PRIVATE KEY]"]
7
+ ];
8
+ export function redactText(value) {
9
+ let redacted = value;
10
+ for (const [pattern, replacement] of SECRET_PATTERNS) {
11
+ redacted = redacted.replace(pattern, replacement);
12
+ }
13
+ return redacted;
14
+ }
15
+ export function redactValue(value) {
16
+ if (typeof value === "string") {
17
+ return redactText(value);
18
+ }
19
+ if (Array.isArray(value)) {
20
+ return value.map((item) => redactValue(item));
21
+ }
22
+ if (value && typeof value === "object") {
23
+ return Object.fromEntries(Object.entries(value).map(([key, nested]) => [key, redactValue(nested)]));
24
+ }
25
+ return value;
26
+ }
package/dist/store.js ADDED
@@ -0,0 +1,175 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { CTXCARRY_DIR, ctxcarryPath, rootPath } from "./paths.js";
4
+ import { redactText, redactValue } from "./redact.js";
5
+ const DEFAULT_STATE = {
6
+ version: 1,
7
+ updatedAt: new Date(0).toISOString(),
8
+ persistent: {
9
+ architecture: [],
10
+ installCommands: [],
11
+ testCommands: [],
12
+ conventions: [],
13
+ deploymentNotes: []
14
+ },
15
+ working: {
16
+ currentTask: "Unspecified",
17
+ status: "not_started",
18
+ currentBranch: null,
19
+ touchedFiles: [],
20
+ constraints: [],
21
+ failures: [],
22
+ todos: [],
23
+ nextSteps: [],
24
+ lastCommands: []
25
+ },
26
+ episodic: {
27
+ sessions: [],
28
+ decisions: [],
29
+ attempts: [],
30
+ resolved: []
31
+ }
32
+ };
33
+ export function defaultConfig(project = path.basename(process.cwd())) {
34
+ return {
35
+ project,
36
+ default_budget_tokens: 12000,
37
+ agents: {
38
+ codex: {
39
+ enabled: true,
40
+ output: "AGENTS.md"
41
+ },
42
+ claude: {
43
+ enabled: true,
44
+ output: "CLAUDE.md",
45
+ command: "claude"
46
+ }
47
+ },
48
+ ignore: ["node_modules/**", ".next/**", "dist/**", "build/**", "pnpm-lock.yaml"],
49
+ capture: {
50
+ git_diff: true,
51
+ commands: true,
52
+ test_outputs: true,
53
+ file_changes: true
54
+ },
55
+ summarizer: {
56
+ provider: "deterministic"
57
+ }
58
+ };
59
+ }
60
+ export function ensureInitialized() {
61
+ if (!fs.existsSync(ctxcarryPath())) {
62
+ throw new Error("ctxcarry is not initialized. Run `ctxcarry init` first.");
63
+ }
64
+ }
65
+ export function initStore() {
66
+ fs.mkdirSync(ctxcarryPath("summaries"), { recursive: true });
67
+ fs.mkdirSync(ctxcarryPath("ctxcarrys"), { recursive: true });
68
+ fs.mkdirSync(ctxcarryPath("index"), { recursive: true });
69
+ fs.mkdirSync(ctxcarryPath("sessions"), { recursive: true });
70
+ writeFileIfMissing(rootPath("ctxcarry.config.json"), JSON.stringify(defaultConfig(), null, 2) + "\n");
71
+ writeFileIfMissing(ctxcarryPath("events.jsonl"), "");
72
+ writeFileIfMissing(ctxcarryPath("commands.jsonl"), "");
73
+ writeFileIfMissing(ctxcarryPath("state.json"), JSON.stringify(freshState(), null, 2) + "\n");
74
+ writeFileIfMissing(ctxcarryPath("state.md"), "# ctxcarry State\n\nNo compacted state yet.\n");
75
+ }
76
+ export function readConfig() {
77
+ const configPath = rootPath("ctxcarry.config.json");
78
+ if (!fs.existsSync(configPath)) {
79
+ return defaultConfig();
80
+ }
81
+ return JSON.parse(fs.readFileSync(configPath, "utf8"));
82
+ }
83
+ export function readState() {
84
+ const statePath = ctxcarryPath("state.json");
85
+ if (!fs.existsSync(statePath)) {
86
+ return freshState();
87
+ }
88
+ return JSON.parse(fs.readFileSync(statePath, "utf8"));
89
+ }
90
+ export function writeState(state) {
91
+ fs.writeFileSync(ctxcarryPath("state.json"), JSON.stringify(redactValue(state), null, 2) + "\n");
92
+ }
93
+ export function appendEvent(event) {
94
+ ensureInitialized();
95
+ const fullEvent = redactValue({
96
+ ...event,
97
+ timestamp: event.timestamp ?? new Date().toISOString(),
98
+ });
99
+ fs.appendFileSync(ctxcarryPath("events.jsonl"), JSON.stringify(fullEvent) + "\n");
100
+ if (fullEvent.type === "command_run") {
101
+ fs.appendFileSync(ctxcarryPath("commands.jsonl"), JSON.stringify(fullEvent) + "\n");
102
+ }
103
+ return fullEvent;
104
+ }
105
+ export function readEvents() {
106
+ ensureInitialized();
107
+ const file = ctxcarryPath("events.jsonl");
108
+ if (!fs.existsSync(file)) {
109
+ return [];
110
+ }
111
+ return fs
112
+ .readFileSync(file, "utf8")
113
+ .split("\n")
114
+ .map((line) => line.trim())
115
+ .filter(Boolean)
116
+ .map((line) => JSON.parse(line));
117
+ }
118
+ export function writeMarkdownState(markdown) {
119
+ const redacted = redactText(markdown);
120
+ fs.writeFileSync(ctxcarryPath("state.md"), redacted.endsWith("\n") ? redacted : redacted + "\n");
121
+ }
122
+ export function writeHandoff(agent, markdown) {
123
+ const output = ctxcarryPath("ctxcarrys", `${agent}.md`);
124
+ const redacted = redactText(markdown);
125
+ fs.writeFileSync(output, redacted.endsWith("\n") ? redacted : redacted + "\n");
126
+ return output;
127
+ }
128
+ export function writeManagedFile(filePath, markdown) {
129
+ const absolutePath = rootPath(filePath);
130
+ fs.mkdirSync(path.dirname(absolutePath), { recursive: true });
131
+ const start = "<!-- ctxcarry:start -->";
132
+ const end = "<!-- ctxcarry:end -->";
133
+ const block = `${start}\n${redactText(markdown).trim()}\n${end}`;
134
+ if (!fs.existsSync(absolutePath)) {
135
+ fs.writeFileSync(absolutePath, `${block}\n`);
136
+ return;
137
+ }
138
+ const existing = fs.readFileSync(absolutePath, "utf8");
139
+ const pattern = new RegExp(`${escapeRegExp(start)}[\\s\\S]*?${escapeRegExp(end)}`);
140
+ const next = pattern.test(existing) ? existing.replace(pattern, block) : `${existing.trimEnd()}\n\n${block}\n`;
141
+ fs.writeFileSync(absolutePath, next.endsWith("\n") ? next : next + "\n");
142
+ }
143
+ export function freshState() {
144
+ return {
145
+ ...DEFAULT_STATE,
146
+ updatedAt: new Date().toISOString(),
147
+ persistent: { ...DEFAULT_STATE.persistent },
148
+ working: {
149
+ ...DEFAULT_STATE.working,
150
+ touchedFiles: [],
151
+ constraints: [],
152
+ failures: [],
153
+ todos: [],
154
+ nextSteps: [],
155
+ lastCommands: []
156
+ },
157
+ episodic: {
158
+ sessions: [],
159
+ decisions: [],
160
+ attempts: [],
161
+ resolved: []
162
+ }
163
+ };
164
+ }
165
+ function writeFileIfMissing(filePath, content) {
166
+ if (!fs.existsSync(filePath)) {
167
+ fs.writeFileSync(filePath, content);
168
+ }
169
+ }
170
+ function escapeRegExp(value) {
171
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
172
+ }
173
+ export function ctxcarryDirName() {
174
+ return CTXCARRY_DIR;
175
+ }
@@ -0,0 +1,41 @@
1
+ export class OpenAISummarizer {
2
+ apiKey;
3
+ model;
4
+ constructor(apiKey = process.env.OPENAI_API_KEY, model = process.env.CTXCARRY_OPENAI_MODEL) {
5
+ this.apiKey = apiKey;
6
+ this.model = model;
7
+ }
8
+ async summarize(input) {
9
+ if (!this.apiKey) {
10
+ throw new Error("OPENAI_API_KEY is required for OpenAI summarization.");
11
+ }
12
+ if (!this.model) {
13
+ throw new Error("CTXCARRY_OPENAI_MODEL is required for OpenAI summarization.");
14
+ }
15
+ const response = await fetch("https://api.openai.com/v1/responses", {
16
+ method: "POST",
17
+ headers: {
18
+ Authorization: `Bearer ${this.apiKey}`,
19
+ "Content-Type": "application/json"
20
+ },
21
+ body: JSON.stringify({
22
+ model: this.model,
23
+ input: [
24
+ {
25
+ role: "system",
26
+ content: "Summarize coding-agent session history into compact project ctxcarry state. Preserve decisions, failures, files, constraints, and next steps."
27
+ },
28
+ {
29
+ role: "user",
30
+ content: `Budget: ${input.budgetTokens} tokens\n\n${input.text}`
31
+ }
32
+ ]
33
+ })
34
+ });
35
+ if (!response.ok) {
36
+ throw new Error(`OpenAI summarization failed with HTTP ${response.status}.`);
37
+ }
38
+ const json = (await response.json());
39
+ return json.output_text?.trim() ?? "";
40
+ }
41
+ }