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.
- package/README.md +239 -0
- package/assets/ctxcarry-ascii.svg +39 -0
- package/dist/archive.js +19 -0
- package/dist/capture.js +250 -0
- package/dist/cli.js +213 -0
- package/dist/compile.js +152 -0
- package/dist/content-router.js +93 -0
- package/dist/distill.js +249 -0
- package/dist/git.js +65 -0
- package/dist/learn.js +30 -0
- package/dist/mcp-server.js +94 -0
- package/dist/paths.js +8 -0
- package/dist/redact.js +26 -0
- package/dist/store.js +175 -0
- package/dist/summarizers/openai.js +41 -0
- package/dist/tokens.js +14 -0
- package/dist/types.js +1 -0
- package/package.json +53 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { captureSnapshot, runAgent } from "./capture.js";
|
|
3
|
+
import { compileAgent, handoffTokenEstimate, rawEventText, renderCurrentHandoff } from "./compile.js";
|
|
4
|
+
import { compactState } from "./distill.js";
|
|
5
|
+
import { learnFromSessions } from "./learn.js";
|
|
6
|
+
import { serveMcp } from "./mcp-server.js";
|
|
7
|
+
import { estimateSavings, estimateTokens } from "./tokens.js";
|
|
8
|
+
import { appendEvent, ctxcarryDirName, ensureInitialized, initStore, readState } from "./store.js";
|
|
9
|
+
async function main() {
|
|
10
|
+
const args = parseArgs(process.argv.slice(2));
|
|
11
|
+
try {
|
|
12
|
+
switch (args.command) {
|
|
13
|
+
case undefined:
|
|
14
|
+
case "--help":
|
|
15
|
+
case "-h":
|
|
16
|
+
case "help":
|
|
17
|
+
printHelp();
|
|
18
|
+
return;
|
|
19
|
+
case "init":
|
|
20
|
+
initStore();
|
|
21
|
+
console.log(`Initialized ${ctxcarryDirName()}/ and ctxcarry.config.json`);
|
|
22
|
+
return;
|
|
23
|
+
case "capture":
|
|
24
|
+
ensureInitialized();
|
|
25
|
+
printSnapshot(captureSnapshot(stringFlag(args, "agent")));
|
|
26
|
+
return;
|
|
27
|
+
case "note":
|
|
28
|
+
ensureInitialized();
|
|
29
|
+
addNote(args);
|
|
30
|
+
return;
|
|
31
|
+
case "run":
|
|
32
|
+
ensureInitialized();
|
|
33
|
+
{
|
|
34
|
+
const result = await runAgent(requiredPositional(args, 0, "agent"));
|
|
35
|
+
console.log(`Recorded session ${result.sessionId}.`);
|
|
36
|
+
console.log(`Session files: ${result.sessionDir}`);
|
|
37
|
+
process.exitCode = result.exitCode;
|
|
38
|
+
}
|
|
39
|
+
return;
|
|
40
|
+
case "compact":
|
|
41
|
+
ensureInitialized();
|
|
42
|
+
compactState();
|
|
43
|
+
console.log("Compacted .ctxcarry/state.json and .ctxcarry/state.md");
|
|
44
|
+
return;
|
|
45
|
+
case "compile":
|
|
46
|
+
ensureInitialized();
|
|
47
|
+
compileCommand(args);
|
|
48
|
+
return;
|
|
49
|
+
case "switch":
|
|
50
|
+
ensureInitialized();
|
|
51
|
+
switchCommand(args);
|
|
52
|
+
return;
|
|
53
|
+
case "status":
|
|
54
|
+
ensureInitialized();
|
|
55
|
+
statusCommand();
|
|
56
|
+
return;
|
|
57
|
+
case "tokens":
|
|
58
|
+
ensureInitialized();
|
|
59
|
+
tokensCommand(args);
|
|
60
|
+
return;
|
|
61
|
+
case "learn":
|
|
62
|
+
ensureInitialized();
|
|
63
|
+
learnCommand(args);
|
|
64
|
+
return;
|
|
65
|
+
case "mcp":
|
|
66
|
+
ensureInitialized();
|
|
67
|
+
await mcpCommand(args);
|
|
68
|
+
return;
|
|
69
|
+
default:
|
|
70
|
+
throw new Error(`Unknown command "${args.command}". Run \`ctxcarry --help\`.`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
75
|
+
console.error(`ctxcarry: ${message}`);
|
|
76
|
+
process.exitCode = 1;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function addNote(args) {
|
|
80
|
+
const type = stringFlag(args, "type");
|
|
81
|
+
const text = stringFlag(args, "text");
|
|
82
|
+
const agent = stringFlag(args, "agent");
|
|
83
|
+
if (!type || !["decision", "failure", "todo", "constraint", "task", "next", "resolved"].includes(type)) {
|
|
84
|
+
throw new Error("`ctxcarry note` requires --type decision|failure|todo|constraint|task|next|resolved.");
|
|
85
|
+
}
|
|
86
|
+
if (!text) {
|
|
87
|
+
throw new Error("`ctxcarry note` requires --text.");
|
|
88
|
+
}
|
|
89
|
+
appendEvent({
|
|
90
|
+
type: "note",
|
|
91
|
+
noteType: type,
|
|
92
|
+
content: text,
|
|
93
|
+
agent
|
|
94
|
+
});
|
|
95
|
+
console.log(`Recorded ${type} note.`);
|
|
96
|
+
}
|
|
97
|
+
function compileCommand(args) {
|
|
98
|
+
const agent = stringFlag(args, "agent") ?? requiredPositional(args, 0, "agent");
|
|
99
|
+
const output = compileAgent(agent, numberFlag(args, "budget"));
|
|
100
|
+
console.log(`Compiled ${agent} ctxcarry to ${output}`);
|
|
101
|
+
}
|
|
102
|
+
function switchCommand(args) {
|
|
103
|
+
const agent = requiredPositional(args, 0, "agent");
|
|
104
|
+
compactState();
|
|
105
|
+
const output = compileAgent(agent, numberFlag(args, "budget"));
|
|
106
|
+
console.log(`Prepared ${agent} ctxcarry in ${output}.`);
|
|
107
|
+
if (agent === "codex") {
|
|
108
|
+
console.log("Next: run `codex` in this repo.");
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function statusCommand() {
|
|
112
|
+
const state = readState();
|
|
113
|
+
const failures = state.working.failures.length;
|
|
114
|
+
const estimate = handoffTokenEstimate("codex");
|
|
115
|
+
console.log(`Current task: ${state.working.currentTask}`);
|
|
116
|
+
console.log(`Status: ${state.working.status}`);
|
|
117
|
+
console.log(`Current branch: ${state.working.currentBranch ?? "Unknown"}`);
|
|
118
|
+
console.log(`Files touched: ${state.working.touchedFiles.length}`);
|
|
119
|
+
console.log(`Open failures: ${failures}`);
|
|
120
|
+
console.log(`Decisions: ${state.episodic.decisions.length}`);
|
|
121
|
+
console.log(`Token estimate for Codex Handoff: ${estimate}`);
|
|
122
|
+
}
|
|
123
|
+
function tokensCommand(args) {
|
|
124
|
+
const agent = stringFlag(args, "agent") ?? "codex";
|
|
125
|
+
const packed = renderCurrentHandoff(agent, numberFlag(args, "budget"));
|
|
126
|
+
const raw = rawEventText();
|
|
127
|
+
console.log(`Agent: ${agent}`);
|
|
128
|
+
console.log(`Packed estimate: ${estimateTokens(packed)} tokens`);
|
|
129
|
+
console.log(`Raw event estimate: ${estimateTokens(raw)} tokens`);
|
|
130
|
+
console.log(`Estimated savings: ${estimateSavings(raw, packed)}%`);
|
|
131
|
+
}
|
|
132
|
+
function printSnapshot(snapshot) {
|
|
133
|
+
console.log(snapshot.isRepo ? "Captured git snapshot." : "Captured snapshot outside a git repository.");
|
|
134
|
+
console.log(`Branch: ${snapshot.branch ?? "Unknown"}`);
|
|
135
|
+
console.log(`Changed files: ${snapshot.changedFiles.length}`);
|
|
136
|
+
}
|
|
137
|
+
function printHelp() {
|
|
138
|
+
console.log(`ctxcarry local agent ctxcarry CLI
|
|
139
|
+
|
|
140
|
+
Usage:
|
|
141
|
+
ctxcarry init
|
|
142
|
+
ctxcarry capture [--agent claude]
|
|
143
|
+
ctxcarry note --type decision|failure|todo|constraint|task|next|resolved --text "..."
|
|
144
|
+
ctxcarry run <agent>
|
|
145
|
+
ctxcarry compact
|
|
146
|
+
ctxcarry compile --agent codex|claude [--budget 4000]
|
|
147
|
+
ctxcarry switch <agent> [--budget 4000]
|
|
148
|
+
ctxcarry status
|
|
149
|
+
ctxcarry tokens [--agent codex] [--budget 4000]
|
|
150
|
+
ctxcarry learn [--apply]
|
|
151
|
+
ctxcarry mcp serve
|
|
152
|
+
`);
|
|
153
|
+
}
|
|
154
|
+
function parseArgs(argv) {
|
|
155
|
+
const [command, ...rest] = argv;
|
|
156
|
+
const positional = [];
|
|
157
|
+
const flags = {};
|
|
158
|
+
for (let index = 0; index < rest.length; index += 1) {
|
|
159
|
+
const arg = rest[index];
|
|
160
|
+
if (arg.startsWith("--")) {
|
|
161
|
+
const name = arg.slice(2);
|
|
162
|
+
const next = rest[index + 1];
|
|
163
|
+
if (next && !next.startsWith("--")) {
|
|
164
|
+
flags[name] = next;
|
|
165
|
+
index += 1;
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
flags[name] = true;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
positional.push(arg);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return { command, positional, flags };
|
|
176
|
+
}
|
|
177
|
+
function stringFlag(args, name) {
|
|
178
|
+
const value = args.flags[name];
|
|
179
|
+
return typeof value === "string" ? value : undefined;
|
|
180
|
+
}
|
|
181
|
+
function requiredPositional(args, index, name) {
|
|
182
|
+
const value = args.positional[index];
|
|
183
|
+
if (!value) {
|
|
184
|
+
throw new Error(`Missing required ${name}.`);
|
|
185
|
+
}
|
|
186
|
+
return value;
|
|
187
|
+
}
|
|
188
|
+
function numberFlag(args, name) {
|
|
189
|
+
const value = stringFlag(args, name);
|
|
190
|
+
if (!value) {
|
|
191
|
+
return undefined;
|
|
192
|
+
}
|
|
193
|
+
const parsed = Number(value);
|
|
194
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
195
|
+
throw new Error(`--${name} must be a positive number.`);
|
|
196
|
+
}
|
|
197
|
+
return parsed;
|
|
198
|
+
}
|
|
199
|
+
function learnCommand(args) {
|
|
200
|
+
const markdown = learnFromSessions(Boolean(args.flags.apply));
|
|
201
|
+
console.log(markdown);
|
|
202
|
+
if (args.flags.apply) {
|
|
203
|
+
console.log("\nApplied learned guidance to .ctxcarry/learned.md, AGENTS.md, and CLAUDE.md");
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
async function mcpCommand(args) {
|
|
207
|
+
const subcommand = requiredPositional(args, 0, "mcp subcommand");
|
|
208
|
+
if (subcommand !== "serve") {
|
|
209
|
+
throw new Error("Only `ctxcarry mcp serve` is supported.");
|
|
210
|
+
}
|
|
211
|
+
await serveMcp();
|
|
212
|
+
}
|
|
213
|
+
await main();
|
package/dist/compile.js
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { ctxcarryPath } from "./paths.js";
|
|
3
|
+
import { estimateTokens } from "./tokens.js";
|
|
4
|
+
import { redactText } from "./redact.js";
|
|
5
|
+
import { readConfig, readState, writeHandoff, writeManagedFile } from "./store.js";
|
|
6
|
+
export function compileAgent(agent, budgetTokens) {
|
|
7
|
+
const config = readConfig();
|
|
8
|
+
const state = readState();
|
|
9
|
+
const agentConfig = config.agents[agent];
|
|
10
|
+
if (!agentConfig?.enabled) {
|
|
11
|
+
throw new Error(`Agent "${agent}" is not enabled in ctxcarry.config.json.`);
|
|
12
|
+
}
|
|
13
|
+
const markdown = agent === "claude" ? renderClaude(state, budgetTokens) : renderCodex(state, budgetTokens);
|
|
14
|
+
writeHandoff(agent, markdown);
|
|
15
|
+
writeManagedFile(agentConfig.output, markdown);
|
|
16
|
+
return agentConfig.output;
|
|
17
|
+
}
|
|
18
|
+
export function renderCodex(state, budgetTokens) {
|
|
19
|
+
return packToBudget(() => {
|
|
20
|
+
const body = renderHandoff("Codex Handoff", state);
|
|
21
|
+
return [
|
|
22
|
+
"# ctxcarry Project Context",
|
|
23
|
+
"",
|
|
24
|
+
"Use this compact project state before continuing work. Treat `.ctxcarry/` as the source of truth for generated context.",
|
|
25
|
+
"",
|
|
26
|
+
body
|
|
27
|
+
].join("\n");
|
|
28
|
+
}, state, "codex", budgetTokens);
|
|
29
|
+
}
|
|
30
|
+
export function renderClaude(state, budgetTokens) {
|
|
31
|
+
return packToBudget(() => {
|
|
32
|
+
const body = renderHandoff("Claude Code Handoff", state);
|
|
33
|
+
return [
|
|
34
|
+
"# ctxcarry Memory",
|
|
35
|
+
"",
|
|
36
|
+
"This file is generated from local ctxcarry project memory.",
|
|
37
|
+
"",
|
|
38
|
+
body
|
|
39
|
+
].join("\n");
|
|
40
|
+
}, state, "claude", budgetTokens);
|
|
41
|
+
}
|
|
42
|
+
export function renderCurrentHandoff(agent = "codex", budgetTokens) {
|
|
43
|
+
const state = readState();
|
|
44
|
+
return agent === "claude" ? renderClaude(state, budgetTokens) : renderCodex(state, budgetTokens);
|
|
45
|
+
}
|
|
46
|
+
export function handoffTokenEstimate(agent = "codex") {
|
|
47
|
+
const ctxcarry = renderCurrentHandoff(agent);
|
|
48
|
+
return estimateTokens(ctxcarry);
|
|
49
|
+
}
|
|
50
|
+
export function rawEventText() {
|
|
51
|
+
const eventsPath = ctxcarryPath("events.jsonl");
|
|
52
|
+
return fs.existsSync(eventsPath) ? fs.readFileSync(eventsPath, "utf8") : "";
|
|
53
|
+
}
|
|
54
|
+
function renderHandoff(title, state) {
|
|
55
|
+
const sections = [
|
|
56
|
+
`## ${title}`,
|
|
57
|
+
"",
|
|
58
|
+
"### Current Task",
|
|
59
|
+
state.working.currentTask,
|
|
60
|
+
"",
|
|
61
|
+
"### Current Status",
|
|
62
|
+
state.working.status,
|
|
63
|
+
"",
|
|
64
|
+
"### Current Branch",
|
|
65
|
+
state.working.currentBranch ?? "Unknown",
|
|
66
|
+
"",
|
|
67
|
+
"### Recommended Next Step",
|
|
68
|
+
renderNextStep(state)
|
|
69
|
+
];
|
|
70
|
+
appendSection(sections, "Files Touched", state.working.touchedFiles);
|
|
71
|
+
appendSection(sections, "Decisions Made", state.episodic.decisions.map((item) => item.content));
|
|
72
|
+
appendSection(sections, "Constraints", state.working.constraints.map((item) => item.content));
|
|
73
|
+
appendSection(sections, "Known Failures", state.working.failures.map((item) => item.content));
|
|
74
|
+
appendSection(sections, "Last Commands", state.working.lastCommands);
|
|
75
|
+
return redactText(sections.join("\n"));
|
|
76
|
+
}
|
|
77
|
+
function renderNextStep(state) {
|
|
78
|
+
const explicit = state.working.nextSteps.at(-1)?.content;
|
|
79
|
+
if (explicit) {
|
|
80
|
+
return explicit;
|
|
81
|
+
}
|
|
82
|
+
const failure = state.working.failures.at(-1)?.content;
|
|
83
|
+
if (failure) {
|
|
84
|
+
return `Investigate current failure: ${failure}`;
|
|
85
|
+
}
|
|
86
|
+
if (state.working.touchedFiles.length > 0) {
|
|
87
|
+
return "Review the touched files and run the relevant tests.";
|
|
88
|
+
}
|
|
89
|
+
return "Record the current task with `ctxcarry note --type task --text \"...\"`.";
|
|
90
|
+
}
|
|
91
|
+
function renderList(items) {
|
|
92
|
+
return items.map((item) => `- ${item}`).join("\n");
|
|
93
|
+
}
|
|
94
|
+
function appendSection(sections, title, items) {
|
|
95
|
+
if (items.length === 0) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
sections.push("", `### ${title}`, renderList(items));
|
|
99
|
+
}
|
|
100
|
+
function packToBudget(render, state, agent, budgetTokens) {
|
|
101
|
+
const full = redactText(render());
|
|
102
|
+
if (!budgetTokens || estimateTokens(full) <= budgetTokens) {
|
|
103
|
+
return full;
|
|
104
|
+
}
|
|
105
|
+
const compactState = {
|
|
106
|
+
...state,
|
|
107
|
+
working: {
|
|
108
|
+
...state.working,
|
|
109
|
+
touchedFiles: state.working.touchedFiles.slice(0, 20),
|
|
110
|
+
lastCommands: state.working.lastCommands.slice(-3),
|
|
111
|
+
constraints: state.working.constraints.slice(-10),
|
|
112
|
+
failures: state.working.failures.slice(-10),
|
|
113
|
+
todos: [],
|
|
114
|
+
nextSteps: state.working.nextSteps.slice(-3)
|
|
115
|
+
},
|
|
116
|
+
episodic: {
|
|
117
|
+
...state.episodic,
|
|
118
|
+
sessions: state.episodic.sessions.slice(-2),
|
|
119
|
+
decisions: state.episodic.decisions.slice(-10),
|
|
120
|
+
attempts: [],
|
|
121
|
+
resolved: []
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
const compact = agent === "claude" ? renderClaude(compactState) : renderCodex(compactState);
|
|
125
|
+
if (estimateTokens(compact) <= budgetTokens) {
|
|
126
|
+
return compact;
|
|
127
|
+
}
|
|
128
|
+
const minimum = [
|
|
129
|
+
agent === "claude" ? "# ctxcarry Memory" : "# ctxcarry Project Context",
|
|
130
|
+
"",
|
|
131
|
+
`## ${agent === "claude" ? "Claude Code" : "Codex"} ctxcarry`,
|
|
132
|
+
"",
|
|
133
|
+
"### Current Task",
|
|
134
|
+
truncate(state.working.currentTask, 600),
|
|
135
|
+
"",
|
|
136
|
+
"### Current Status",
|
|
137
|
+
state.working.status,
|
|
138
|
+
"",
|
|
139
|
+
"### Recommended Next Step",
|
|
140
|
+
truncate(renderNextStep(state), 600)
|
|
141
|
+
];
|
|
142
|
+
appendSection(minimum, "Constraints", state.working.constraints.slice(-5).map((item) => truncate(item.content, 240)));
|
|
143
|
+
appendSection(minimum, "Known Failures", state.working.failures.slice(-5).map((item) => truncate(item.content, 240)));
|
|
144
|
+
appendSection(minimum, "Files Touched", state.working.touchedFiles.slice(0, 10));
|
|
145
|
+
return redactText(minimum.join("\n"));
|
|
146
|
+
}
|
|
147
|
+
function truncate(value, maxChars) {
|
|
148
|
+
if (value.length <= maxChars) {
|
|
149
|
+
return value;
|
|
150
|
+
}
|
|
151
|
+
return `${value.slice(0, Math.max(0, maxChars - 3))}...`;
|
|
152
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
export function routeContent(label, content) {
|
|
2
|
+
const kind = detectKind(label, content);
|
|
3
|
+
if (kind === "log" || kind === "command") {
|
|
4
|
+
return summarizeLog(content, kind);
|
|
5
|
+
}
|
|
6
|
+
if (kind === "diff") {
|
|
7
|
+
return summarizeDiff(content);
|
|
8
|
+
}
|
|
9
|
+
if (kind === "transcript" || kind === "summary") {
|
|
10
|
+
return summarizeTranscript(content, kind);
|
|
11
|
+
}
|
|
12
|
+
return {
|
|
13
|
+
kind,
|
|
14
|
+
summary: firstLines(content, 8),
|
|
15
|
+
signals: []
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
function detectKind(label, content) {
|
|
19
|
+
const lower = `${label}\n${content.slice(0, 2000)}`.toLowerCase();
|
|
20
|
+
if (lower.includes("diff --git") || lower.includes(" files changed") || lower.includes("insertions(+)")) {
|
|
21
|
+
return "diff";
|
|
22
|
+
}
|
|
23
|
+
if (/(\bfail\b|\berror\b|exception|stack trace|at .*:\d+:\d+)/i.test(content)) {
|
|
24
|
+
return label.includes("command") ? "command" : "log";
|
|
25
|
+
}
|
|
26
|
+
if (/^##\s+(current task|files changed|decisions|constraints|failures|commands run|next step)/im.test(content)) {
|
|
27
|
+
return "summary";
|
|
28
|
+
}
|
|
29
|
+
if (/\b(user|assistant|tool):/i.test(content)) {
|
|
30
|
+
return "transcript";
|
|
31
|
+
}
|
|
32
|
+
return "unknown";
|
|
33
|
+
}
|
|
34
|
+
function summarizeLog(content, kind) {
|
|
35
|
+
const lines = uniqueLines(content);
|
|
36
|
+
const error = lines.find((line) => /(?:error:|expected|received|exception|timeout|fatal)/i.test(line));
|
|
37
|
+
const failingTest = lines.find((line) => /\.(?:test|spec)\.[tj]sx?/i.test(line));
|
|
38
|
+
const appFrame = lines.find((line) => /\bat .*?(?:src|app|lib|components|tests)\//.test(line));
|
|
39
|
+
const command = lines.find((line) => /^(?:pnpm|npm|yarn|bun|cargo|pytest|go test|swift test)\b/.test(line));
|
|
40
|
+
const signals = [error, failingTest, appFrame, command].filter((line) => Boolean(line));
|
|
41
|
+
return {
|
|
42
|
+
kind,
|
|
43
|
+
summary: signals.length > 0 ? signals.join("; ") : firstLines(lines.join("\n"), 6),
|
|
44
|
+
signals
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function summarizeDiff(content) {
|
|
48
|
+
const files = uniqueLines(content)
|
|
49
|
+
.map((line) => {
|
|
50
|
+
const git = line.match(/^diff --git a\/(.+?) b\//);
|
|
51
|
+
if (git)
|
|
52
|
+
return git[1];
|
|
53
|
+
const stat = line.match(/^(.+?)\s+\|\s+\d+/);
|
|
54
|
+
if (stat)
|
|
55
|
+
return stat[1].trim();
|
|
56
|
+
return "";
|
|
57
|
+
})
|
|
58
|
+
.filter(Boolean);
|
|
59
|
+
return {
|
|
60
|
+
kind: "diff",
|
|
61
|
+
summary: files.length > 0 ? `Changed files: ${files.slice(0, 25).join(", ")}` : firstLines(content, 8),
|
|
62
|
+
signals: files
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function summarizeTranscript(content, kind) {
|
|
66
|
+
const lines = uniqueLines(content);
|
|
67
|
+
const signals = lines.filter((line) => /(?:decision|constraint|failure|next step|current task|do not|failed|fix|todo)/i.test(line)).slice(0, 20);
|
|
68
|
+
return {
|
|
69
|
+
kind,
|
|
70
|
+
summary: signals.length > 0 ? signals.join("\n") : firstLines(lines.join("\n"), 10),
|
|
71
|
+
signals
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
function firstLines(content, count) {
|
|
75
|
+
return content
|
|
76
|
+
.split("\n")
|
|
77
|
+
.map((line) => line.trim())
|
|
78
|
+
.filter(Boolean)
|
|
79
|
+
.slice(0, count)
|
|
80
|
+
.join("\n");
|
|
81
|
+
}
|
|
82
|
+
function uniqueLines(content) {
|
|
83
|
+
const seen = new Set();
|
|
84
|
+
const result = [];
|
|
85
|
+
for (const line of content.split("\n").map((item) => item.trim()).filter(Boolean)) {
|
|
86
|
+
const normalized = line.toLowerCase().replace(/\s+/g, " ");
|
|
87
|
+
if (seen.has(normalized))
|
|
88
|
+
continue;
|
|
89
|
+
seen.add(normalized);
|
|
90
|
+
result.push(line);
|
|
91
|
+
}
|
|
92
|
+
return result;
|
|
93
|
+
}
|