@supersession/sup 0.1.1
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 +61 -0
- package/dist/cli.d.ts +14 -0
- package/dist/cli.js +385 -0
- package/dist/cli.js.map +1 -0
- package/dist/cloud.d.ts +33 -0
- package/dist/cloud.js +72 -0
- package/dist/cloud.js.map +1 -0
- package/dist/config.d.ts +10 -0
- package/dist/config.js +36 -0
- package/dist/config.js.map +1 -0
- package/dist/discover.d.ts +14 -0
- package/dist/discover.js +44 -0
- package/dist/discover.js.map +1 -0
- package/dist/distill/smart.d.ts +30 -0
- package/dist/distill/smart.js +108 -0
- package/dist/distill/smart.js.map +1 -0
- package/dist/distill/template.d.ts +58 -0
- package/dist/distill/template.js +127 -0
- package/dist/distill/template.js.map +1 -0
- package/dist/engine.d.ts +24 -0
- package/dist/engine.js +42 -0
- package/dist/engine.js.map +1 -0
- package/dist/health.d.ts +35 -0
- package/dist/health.js +133 -0
- package/dist/health.js.map +1 -0
- package/dist/import.d.ts +27 -0
- package/dist/import.js +129 -0
- package/dist/import.js.map +1 -0
- package/dist/llm.d.ts +23 -0
- package/dist/llm.js +90 -0
- package/dist/llm.js.map +1 -0
- package/dist/mcp.d.ts +13 -0
- package/dist/mcp.js +115 -0
- package/dist/mcp.js.map +1 -0
- package/dist/nsf.d.ts +75 -0
- package/dist/nsf.js +14 -0
- package/dist/nsf.js.map +1 -0
- package/dist/parsers/chat-export.d.ts +21 -0
- package/dist/parsers/chat-export.js +107 -0
- package/dist/parsers/chat-export.js.map +1 -0
- package/dist/parsers/claude-code.d.ts +24 -0
- package/dist/parsers/claude-code.js +193 -0
- package/dist/parsers/claude-code.js.map +1 -0
- package/dist/redact.d.ts +24 -0
- package/dist/redact.js +65 -0
- package/dist/redact.js.map +1 -0
- package/dist/tools.d.ts +37 -0
- package/dist/tools.js +67 -0
- package/dist/tools.js.map +1 -0
- package/dist/writers/brief-markdown.d.ts +12 -0
- package/dist/writers/brief-markdown.js +83 -0
- package/dist/writers/brief-markdown.js.map +1 -0
- package/package.json +45 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session discovery: given the current working directory, find the source
|
|
3
|
+
* session to hand off. v1 supports Claude Code (the one verified format).
|
|
4
|
+
*/
|
|
5
|
+
export interface FoundSession {
|
|
6
|
+
tool: string;
|
|
7
|
+
filePath: string;
|
|
8
|
+
mtimeMs: number;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Find the latest Claude Code session for a given working directory.
|
|
12
|
+
* @param cwd absolute working directory of the project
|
|
13
|
+
*/
|
|
14
|
+
export declare function findClaudeCodeSession(cwd: string): FoundSession | undefined;
|
package/dist/discover.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session discovery: given the current working directory, find the source
|
|
3
|
+
* session to hand off. v1 supports Claude Code (the one verified format).
|
|
4
|
+
*/
|
|
5
|
+
import { readdirSync, statSync } from "node:fs";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { claudeCodeProjectDir } from "./tools.js";
|
|
8
|
+
/** Most-recently-modified `.jsonl` in a directory, or undefined. */
|
|
9
|
+
function latestJsonl(dir) {
|
|
10
|
+
let best;
|
|
11
|
+
let entries;
|
|
12
|
+
try {
|
|
13
|
+
entries = readdirSync(dir);
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
for (const name of entries) {
|
|
19
|
+
if (!name.endsWith(".jsonl"))
|
|
20
|
+
continue;
|
|
21
|
+
const path = join(dir, name);
|
|
22
|
+
try {
|
|
23
|
+
const m = statSync(path).mtimeMs;
|
|
24
|
+
if (!best || m > best.mtimeMs)
|
|
25
|
+
best = { path, mtimeMs: m };
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
// skip unreadable
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return best;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Find the latest Claude Code session for a given working directory.
|
|
35
|
+
* @param cwd absolute working directory of the project
|
|
36
|
+
*/
|
|
37
|
+
export function findClaudeCodeSession(cwd) {
|
|
38
|
+
const dir = claudeCodeProjectDir(cwd);
|
|
39
|
+
const latest = latestJsonl(dir);
|
|
40
|
+
if (!latest)
|
|
41
|
+
return undefined;
|
|
42
|
+
return { tool: "claude-code", filePath: latest.path, mtimeMs: latest.mtimeMs };
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=discover.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"discover.js","sourceRoot":"","sources":["../src/discover.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAChD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAQlD,oEAAoE;AACpE,SAAS,WAAW,CAAC,GAAW;IAC9B,IAAI,IAAmD,CAAC;IACxD,IAAI,OAAiB,CAAC;IACtB,IAAI,CAAC;QACH,OAAO,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC;IAC7B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,KAAK,MAAM,IAAI,IAAI,OAAO,EAAE,CAAC;QAC3B,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC;YAAE,SAAS;QACvC,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;QAC7B,IAAI,CAAC;YACH,MAAM,CAAC,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC;YACjC,IAAI,CAAC,IAAI,IAAI,CAAC,GAAG,IAAI,CAAC,OAAO;gBAAE,IAAI,GAAG,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC;QAC7D,CAAC;QAAC,MAAM,CAAC;YACP,kBAAkB;QACpB,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,qBAAqB,CAAC,GAAW;IAC/C,MAAM,GAAG,GAAG,oBAAoB,CAAC,GAAG,CAAC,CAAC;IACtC,MAAM,MAAM,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC;IAChC,IAAI,CAAC,MAAM;QAAE,OAAO,SAAS,CAAC;IAC9B,OAAO,EAAE,IAAI,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,CAAC,IAAI,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,CAAC;AACjF,CAAC"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Smart distill: template distill + an LLM-extracted interpretation layer.
|
|
3
|
+
*
|
|
4
|
+
* The split is the whole honesty story. Grounded facts (files, commands, recent
|
|
5
|
+
* turns, git state) come from the template distiller and are PROVEN. The LLM
|
|
6
|
+
* only adds what it can genuinely infer from the conversation: the decisions
|
|
7
|
+
* made and why, the dead-ends tried and abandoned, a sharpened goal and next
|
|
8
|
+
* step. It never touches the grounded facts — so a model hiccup can soften the
|
|
9
|
+
* interpretation but can never fabricate a file or a command.
|
|
10
|
+
*
|
|
11
|
+
* Fail-safe: any error (no key, network, bad JSON) falls back to the template
|
|
12
|
+
* brief. The product never bricks; it degrades to the honest floor.
|
|
13
|
+
*/
|
|
14
|
+
import type { NsfSession } from "../nsf.js";
|
|
15
|
+
import { type Brief, type DistillOptions } from "./template.js";
|
|
16
|
+
import { type LlmComplete } from "../llm.js";
|
|
17
|
+
export interface SmartDistillOptions extends DistillOptions {
|
|
18
|
+
/** Injectable LLM caller (for tests). Defaults to env-configured BYO-key. */
|
|
19
|
+
llm?: LlmComplete;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Produce a smart brief. Returns a template brief unchanged if no LLM is
|
|
23
|
+
* available or the call fails. Result.usedLlm tells the caller which happened
|
|
24
|
+
* so the CLI can report honestly (and, in the hosted product, meter a credit).
|
|
25
|
+
*/
|
|
26
|
+
export declare function distillSmart(session: NsfSession, opts?: SmartDistillOptions): Promise<{
|
|
27
|
+
brief: Brief;
|
|
28
|
+
usedLlm: boolean;
|
|
29
|
+
error?: string;
|
|
30
|
+
}>;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Smart distill: template distill + an LLM-extracted interpretation layer.
|
|
3
|
+
*
|
|
4
|
+
* The split is the whole honesty story. Grounded facts (files, commands, recent
|
|
5
|
+
* turns, git state) come from the template distiller and are PROVEN. The LLM
|
|
6
|
+
* only adds what it can genuinely infer from the conversation: the decisions
|
|
7
|
+
* made and why, the dead-ends tried and abandoned, a sharpened goal and next
|
|
8
|
+
* step. It never touches the grounded facts — so a model hiccup can soften the
|
|
9
|
+
* interpretation but can never fabricate a file or a command.
|
|
10
|
+
*
|
|
11
|
+
* Fail-safe: any error (no key, network, bad JSON) falls back to the template
|
|
12
|
+
* brief. The product never bricks; it degrades to the honest floor.
|
|
13
|
+
*/
|
|
14
|
+
import { distillTemplate } from "./template.js";
|
|
15
|
+
import { makeLlm } from "../llm.js";
|
|
16
|
+
/** Rough char budget for the transcript we send the model. ~1 char ≈ 0.25 tok. */
|
|
17
|
+
const TRANSCRIPT_CHAR_BUDGET = 24_000;
|
|
18
|
+
function turnToText(turn) {
|
|
19
|
+
const who = turn.role === "user" ? "USER" : "ASSISTANT";
|
|
20
|
+
const acts = turn.actions
|
|
21
|
+
.filter((a) => a.path || a.command)
|
|
22
|
+
.map((a) => (a.command ? `[ran: ${a.command.split("\n")[0]}]` : `[${a.kind}: ${a.path}]`));
|
|
23
|
+
const body = [turn.text, acts.join(" ")].filter(Boolean).join("\n");
|
|
24
|
+
return `${who}: ${body}`;
|
|
25
|
+
}
|
|
26
|
+
/** Head+tail compression to fit the char budget without losing the ends. */
|
|
27
|
+
function buildTranscript(turns) {
|
|
28
|
+
const full = turns.map(turnToText).join("\n\n");
|
|
29
|
+
if (full.length <= TRANSCRIPT_CHAR_BUDGET)
|
|
30
|
+
return full;
|
|
31
|
+
const half = Math.floor(TRANSCRIPT_CHAR_BUDGET / 2);
|
|
32
|
+
const head = full.slice(0, half);
|
|
33
|
+
const tail = full.slice(-half);
|
|
34
|
+
return `${head}\n\n[... middle of session omitted for length ...]\n\n${tail}`;
|
|
35
|
+
}
|
|
36
|
+
function buildPrompt(session, transcript) {
|
|
37
|
+
return `You are analyzing an AI coding session so another AI agent can continue the work seamlessly. Extract ONLY what is genuinely supported by the conversation below. Do not invent anything.
|
|
38
|
+
|
|
39
|
+
Return STRICT JSON (no prose, no markdown fences) matching exactly this shape:
|
|
40
|
+
{
|
|
41
|
+
"goal": "one sentence: what the user is ultimately trying to accomplish",
|
|
42
|
+
"decisions": ["concrete choices that were made and the reason, e.g. 'used RRF fusion instead of raw concatenation because it handled the score-scale mismatch'"],
|
|
43
|
+
"deadEnds": ["approaches that were tried and abandoned, with why, so the next agent does not retry them"],
|
|
44
|
+
"nextStep": "the single most important thing to do next, specific and actionable"
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
Rules:
|
|
48
|
+
- decisions and deadEnds are arrays of short strings; use [] if there are genuinely none.
|
|
49
|
+
- Prefer 2-5 items each; omit anything you are not confident actually happened.
|
|
50
|
+
- Be concrete and technical. No filler.
|
|
51
|
+
|
|
52
|
+
Working directory: ${session.provenance.cwd ?? "unknown"}
|
|
53
|
+
Git branch: ${session.provenance.gitBranch ?? "unknown"}
|
|
54
|
+
|
|
55
|
+
SESSION TRANSCRIPT:
|
|
56
|
+
${transcript}`;
|
|
57
|
+
}
|
|
58
|
+
/** Pull the first balanced JSON object out of a model response. */
|
|
59
|
+
function extractJson(raw) {
|
|
60
|
+
const start = raw.indexOf("{");
|
|
61
|
+
const end = raw.lastIndexOf("}");
|
|
62
|
+
if (start === -1 || end <= start)
|
|
63
|
+
return undefined;
|
|
64
|
+
try {
|
|
65
|
+
const obj = JSON.parse(raw.slice(start, end + 1));
|
|
66
|
+
return obj && typeof obj === "object" ? obj : undefined;
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function asStringArray(v) {
|
|
73
|
+
if (!Array.isArray(v))
|
|
74
|
+
return undefined;
|
|
75
|
+
const arr = v.filter((x) => typeof x === "string" && x.trim().length > 0);
|
|
76
|
+
return arr.length ? arr : undefined;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Produce a smart brief. Returns a template brief unchanged if no LLM is
|
|
80
|
+
* available or the call fails. Result.usedLlm tells the caller which happened
|
|
81
|
+
* so the CLI can report honestly (and, in the hosted product, meter a credit).
|
|
82
|
+
*/
|
|
83
|
+
export async function distillSmart(session, opts = {}) {
|
|
84
|
+
const base = distillTemplate(session, opts);
|
|
85
|
+
const llm = opts.llm ?? makeLlm();
|
|
86
|
+
if (!llm)
|
|
87
|
+
return { brief: base, usedLlm: false };
|
|
88
|
+
try {
|
|
89
|
+
const transcript = buildTranscript(session.turns);
|
|
90
|
+
const raw = await llm(buildPrompt(session, transcript));
|
|
91
|
+
const layer = extractJson(raw);
|
|
92
|
+
if (!layer)
|
|
93
|
+
throw new Error("model did not return valid JSON");
|
|
94
|
+
const brief = {
|
|
95
|
+
...base,
|
|
96
|
+
mode: "smart",
|
|
97
|
+
goal: layer.goal?.trim() || base.goal,
|
|
98
|
+
decisions: asStringArray(layer.decisions),
|
|
99
|
+
deadEnds: asStringArray(layer.deadEnds),
|
|
100
|
+
nextStep: layer.nextStep?.trim() || base.nextStep,
|
|
101
|
+
};
|
|
102
|
+
return { brief, usedLlm: true };
|
|
103
|
+
}
|
|
104
|
+
catch (e) {
|
|
105
|
+
return { brief: base, usedLlm: false, error: e instanceof Error ? e.message : String(e) };
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
//# sourceMappingURL=smart.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"smart.js","sourceRoot":"","sources":["../../src/distill/smart.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Template distill: NSF -> continuation Brief, with ZERO LLM calls.
|
|
3
|
+
*
|
|
4
|
+
* This is the free-tier / offline distiller and the honest floor of quality.
|
|
5
|
+
* It only states things it can PROVE from the transcript + the live repo —
|
|
6
|
+
* never inferred "decisions" or "dead-ends" (that's the paid smart-distill's
|
|
7
|
+
* job, and inventing them here would be hallucination). What it can prove is
|
|
8
|
+
* already valuable: the goal, the exact files touched (ground truth from
|
|
9
|
+
* tool inputs), the commands run, the recent verbatim turns, and where things
|
|
10
|
+
* stand right now.
|
|
11
|
+
*
|
|
12
|
+
* Grounding: the files an agent touched are re-checked against the actual repo
|
|
13
|
+
* at distill time, so the brief describes current reality, not stale history.
|
|
14
|
+
*/
|
|
15
|
+
import type { NsfSession, NsfTurn } from "../nsf.js";
|
|
16
|
+
export interface FileState {
|
|
17
|
+
path: string;
|
|
18
|
+
exists: boolean;
|
|
19
|
+
/** git porcelain status if inside a repo: "M", "A", "??", "" (clean), or undefined. */
|
|
20
|
+
git?: string;
|
|
21
|
+
}
|
|
22
|
+
export interface Brief {
|
|
23
|
+
/** Which distiller produced this brief. */
|
|
24
|
+
mode: "template" | "smart";
|
|
25
|
+
goal: string;
|
|
26
|
+
files: FileState[];
|
|
27
|
+
commands: string[];
|
|
28
|
+
/**
|
|
29
|
+
* LLM-extracted decisions ("chose X because Y"). Only present in smart mode —
|
|
30
|
+
* template mode never invents these (that would be hallucination).
|
|
31
|
+
*/
|
|
32
|
+
decisions?: string[];
|
|
33
|
+
/** LLM-extracted dead-ends ("tried Z, abandoned because W"). Smart mode only. */
|
|
34
|
+
deadEnds?: string[];
|
|
35
|
+
recentTurns: NsfTurn[];
|
|
36
|
+
/** Plain-language "where we are" derived from the tail of the session. */
|
|
37
|
+
currentState: string;
|
|
38
|
+
nextStep: string;
|
|
39
|
+
fidelity: FidelityReport;
|
|
40
|
+
}
|
|
41
|
+
export interface FidelityReport {
|
|
42
|
+
totalTurns: number;
|
|
43
|
+
verbatimTurns: number;
|
|
44
|
+
compressedTurns: number;
|
|
45
|
+
droppedNotes: {
|
|
46
|
+
reason: string;
|
|
47
|
+
count: number;
|
|
48
|
+
}[];
|
|
49
|
+
secretsRedacted: number;
|
|
50
|
+
}
|
|
51
|
+
export interface DistillOptions {
|
|
52
|
+
/** How many trailing turns to carry verbatim. */
|
|
53
|
+
recentTurns?: number;
|
|
54
|
+
/** Secrets redacted upstream, passed through for the fidelity report. */
|
|
55
|
+
secretsRedacted?: number;
|
|
56
|
+
}
|
|
57
|
+
export declare function groundFiles(session: NsfSession): FileState[];
|
|
58
|
+
export declare function distillTemplate(session: NsfSession, opts?: DistillOptions): Brief;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Template distill: NSF -> continuation Brief, with ZERO LLM calls.
|
|
3
|
+
*
|
|
4
|
+
* This is the free-tier / offline distiller and the honest floor of quality.
|
|
5
|
+
* It only states things it can PROVE from the transcript + the live repo —
|
|
6
|
+
* never inferred "decisions" or "dead-ends" (that's the paid smart-distill's
|
|
7
|
+
* job, and inventing them here would be hallucination). What it can prove is
|
|
8
|
+
* already valuable: the goal, the exact files touched (ground truth from
|
|
9
|
+
* tool inputs), the commands run, the recent verbatim turns, and where things
|
|
10
|
+
* stand right now.
|
|
11
|
+
*
|
|
12
|
+
* Grounding: the files an agent touched are re-checked against the actual repo
|
|
13
|
+
* at distill time, so the brief describes current reality, not stale history.
|
|
14
|
+
*/
|
|
15
|
+
import { existsSync } from "node:fs";
|
|
16
|
+
import { execFileSync } from "node:child_process";
|
|
17
|
+
import { isAbsolute, relative } from "node:path";
|
|
18
|
+
const DEFAULT_RECENT = 8;
|
|
19
|
+
/** First real user prompt = the goal. Skips meta/command-caveat noise. */
|
|
20
|
+
function inferGoal(turns) {
|
|
21
|
+
for (const turn of turns) {
|
|
22
|
+
if (turn.role !== "user")
|
|
23
|
+
continue;
|
|
24
|
+
const t = turn.text;
|
|
25
|
+
if (!t)
|
|
26
|
+
continue;
|
|
27
|
+
// Skip harness-injected caveats and slash-command echoes.
|
|
28
|
+
if (t.startsWith("<") || t.startsWith("Caveat:"))
|
|
29
|
+
continue;
|
|
30
|
+
return firstLines(t, 4);
|
|
31
|
+
}
|
|
32
|
+
return "(no explicit goal found in session)";
|
|
33
|
+
}
|
|
34
|
+
/** git porcelain status for a set of paths, keyed by absolute path. Best-effort. */
|
|
35
|
+
function gitStatus(cwd, paths) {
|
|
36
|
+
const out = new Map();
|
|
37
|
+
if (!cwd || paths.length === 0)
|
|
38
|
+
return out;
|
|
39
|
+
try {
|
|
40
|
+
const raw = execFileSync("git", ["status", "--porcelain"], {
|
|
41
|
+
cwd,
|
|
42
|
+
encoding: "utf8",
|
|
43
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
44
|
+
});
|
|
45
|
+
// Porcelain lines: "XY path". Build a rel-path -> code map.
|
|
46
|
+
const byRel = new Map();
|
|
47
|
+
for (const line of raw.split("\n")) {
|
|
48
|
+
if (!line.trim())
|
|
49
|
+
continue;
|
|
50
|
+
const code = line.slice(0, 2).trim();
|
|
51
|
+
const p = line.slice(3).trim();
|
|
52
|
+
byRel.set(p, code);
|
|
53
|
+
}
|
|
54
|
+
for (const abs of paths) {
|
|
55
|
+
const rel = isAbsolute(abs) ? relative(cwd, abs) : abs;
|
|
56
|
+
if (byRel.has(rel))
|
|
57
|
+
out.set(abs, byRel.get(rel));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
// Not a git repo, or git absent — grounding degrades gracefully.
|
|
62
|
+
}
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
export function groundFiles(session) {
|
|
66
|
+
const git = gitStatus(session.provenance.cwd, session.filesTouched);
|
|
67
|
+
return session.filesTouched.map((path) => ({
|
|
68
|
+
path,
|
|
69
|
+
exists: existsSync(path),
|
|
70
|
+
git: git.get(path),
|
|
71
|
+
}));
|
|
72
|
+
}
|
|
73
|
+
/** Notable, deduped shell commands (skip trivial navigation/inspection). */
|
|
74
|
+
function keyCommands(session) {
|
|
75
|
+
const seen = new Set();
|
|
76
|
+
const out = [];
|
|
77
|
+
const trivial = /^(cd|ls|pwd|cat|echo|clear|which|whoami)\b/;
|
|
78
|
+
for (const turn of session.turns) {
|
|
79
|
+
for (const a of turn.actions) {
|
|
80
|
+
if (a.kind !== "run" || !a.command)
|
|
81
|
+
continue;
|
|
82
|
+
const cmd = a.command.split("\n")[0].trim();
|
|
83
|
+
if (!cmd || trivial.test(cmd) || seen.has(cmd))
|
|
84
|
+
continue;
|
|
85
|
+
seen.add(cmd);
|
|
86
|
+
out.push(cmd);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return out.slice(0, 20);
|
|
90
|
+
}
|
|
91
|
+
function firstLines(text, n) {
|
|
92
|
+
return text.split("\n").slice(0, n).join("\n").trim();
|
|
93
|
+
}
|
|
94
|
+
function summarizeState(turns) {
|
|
95
|
+
const lastAssistant = [...turns].reverse().find((t) => t.role === "assistant" && t.text);
|
|
96
|
+
if (lastAssistant)
|
|
97
|
+
return firstLines(lastAssistant.text, 6);
|
|
98
|
+
return "(session has no closing assistant message)";
|
|
99
|
+
}
|
|
100
|
+
function inferNextStep(turns) {
|
|
101
|
+
const lastUser = [...turns].reverse().find((t) => t.role === "user" && t.text);
|
|
102
|
+
if (lastUser)
|
|
103
|
+
return firstLines(lastUser.text, 3);
|
|
104
|
+
return "(no pending user request — continue the last thread)";
|
|
105
|
+
}
|
|
106
|
+
export function distillTemplate(session, opts = {}) {
|
|
107
|
+
const recentN = opts.recentTurns ?? DEFAULT_RECENT;
|
|
108
|
+
const recentTurns = session.turns.slice(-recentN);
|
|
109
|
+
const compressedTurns = Math.max(0, session.turns.length - recentTurns.length);
|
|
110
|
+
return {
|
|
111
|
+
mode: "template",
|
|
112
|
+
goal: inferGoal(session.turns),
|
|
113
|
+
files: groundFiles(session),
|
|
114
|
+
commands: keyCommands(session),
|
|
115
|
+
recentTurns,
|
|
116
|
+
currentState: summarizeState(session.turns),
|
|
117
|
+
nextStep: inferNextStep(session.turns),
|
|
118
|
+
fidelity: {
|
|
119
|
+
totalTurns: session.turns.length,
|
|
120
|
+
verbatimTurns: recentTurns.length,
|
|
121
|
+
compressedTurns,
|
|
122
|
+
droppedNotes: session.gaps.map((g) => ({ reason: g.reason, count: g.count })),
|
|
123
|
+
secretsRedacted: opts.secretsRedacted ?? 0,
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
//# sourceMappingURL=template.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"template.js","sourceRoot":"","sources":["../../src/distill/template.ts"],"names":[],"mappings":"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"}
|
package/dist/engine.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared handoff pipeline, used by BOTH the CLI and the MCP server.
|
|
3
|
+
*
|
|
4
|
+
* Hard rule: this module NEVER writes to stdout. The MCP server speaks JSON-RPC
|
|
5
|
+
* over stdout, so a stray console.log would corrupt the protocol. Callers get
|
|
6
|
+
* structured results and decide how (or whether) to print them.
|
|
7
|
+
*/
|
|
8
|
+
import { type Brief } from "./distill/template.js";
|
|
9
|
+
import type { NsfSession } from "./nsf.js";
|
|
10
|
+
export declare class NoSessionError extends Error {
|
|
11
|
+
}
|
|
12
|
+
/** Parse the latest Claude Code session for a directory, or throw NoSessionError. */
|
|
13
|
+
export declare function requireSession(cwd: string): NsfSession;
|
|
14
|
+
export interface DistillResult {
|
|
15
|
+
brief: Brief;
|
|
16
|
+
usedLlm: boolean;
|
|
17
|
+
/** A human note about which distiller ran / why (caller prints if it wants). */
|
|
18
|
+
note?: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Redact then distill an NSF. Smart (LLM) when a key is configured unless
|
|
22
|
+
* forceTemplate. Pure: returns everything, prints nothing.
|
|
23
|
+
*/
|
|
24
|
+
export declare function distillSession(nsf: NsfSession, forceTemplate: boolean): Promise<DistillResult>;
|
package/dist/engine.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared handoff pipeline, used by BOTH the CLI and the MCP server.
|
|
3
|
+
*
|
|
4
|
+
* Hard rule: this module NEVER writes to stdout. The MCP server speaks JSON-RPC
|
|
5
|
+
* over stdout, so a stray console.log would corrupt the protocol. Callers get
|
|
6
|
+
* structured results and decide how (or whether) to print them.
|
|
7
|
+
*/
|
|
8
|
+
import { findClaudeCodeSession } from "./discover.js";
|
|
9
|
+
import { parseClaudeCode } from "./parsers/claude-code.js";
|
|
10
|
+
import { redactSession } from "./redact.js";
|
|
11
|
+
import { distillTemplate } from "./distill/template.js";
|
|
12
|
+
import { distillSmart } from "./distill/smart.js";
|
|
13
|
+
export class NoSessionError extends Error {
|
|
14
|
+
}
|
|
15
|
+
/** Parse the latest Claude Code session for a directory, or throw NoSessionError. */
|
|
16
|
+
export function requireSession(cwd) {
|
|
17
|
+
const found = findClaudeCodeSession(cwd);
|
|
18
|
+
if (!found)
|
|
19
|
+
throw new NoSessionError(`No Claude Code session found for ${cwd} (looked under ~/.claude/projects).`);
|
|
20
|
+
const parsed = parseClaudeCode(found.filePath);
|
|
21
|
+
if (parsed.turns.length === 0)
|
|
22
|
+
throw new NoSessionError("Session file had no usable turns.");
|
|
23
|
+
return parsed;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Redact then distill an NSF. Smart (LLM) when a key is configured unless
|
|
27
|
+
* forceTemplate. Pure: returns everything, prints nothing.
|
|
28
|
+
*/
|
|
29
|
+
export async function distillSession(nsf, forceTemplate) {
|
|
30
|
+
const { session, hits } = redactSession(nsf);
|
|
31
|
+
if (forceTemplate) {
|
|
32
|
+
return { brief: distillTemplate(session, { secretsRedacted: hits }), usedLlm: false };
|
|
33
|
+
}
|
|
34
|
+
const { brief, usedLlm, error } = await distillSmart(session, { secretsRedacted: hits });
|
|
35
|
+
const note = usedLlm
|
|
36
|
+
? undefined
|
|
37
|
+
: error
|
|
38
|
+
? `smart distill unavailable (${error}); used template distill`
|
|
39
|
+
: "no API key set; used template distill (set ANTHROPIC_API_KEY/OPENAI_API_KEY for decisions + dead-ends)";
|
|
40
|
+
return { brief, usedLlm, note };
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=engine.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"engine.js","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC;AACtD,OAAO,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAC3D,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,eAAe,EAAc,MAAM,uBAAuB,CAAC;AACpE,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAGlD,MAAM,OAAO,cAAe,SAAQ,KAAK;CAAG;AAE5C,qFAAqF;AACrF,MAAM,UAAU,cAAc,CAAC,GAAW;IACxC,MAAM,KAAK,GAAG,qBAAqB,CAAC,GAAG,CAAC,CAAC;IACzC,IAAI,CAAC,KAAK;QACR,MAAM,IAAI,cAAc,CAAC,oCAAoC,GAAG,qCAAqC,CAAC,CAAC;IACzG,MAAM,MAAM,GAAG,eAAe,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;IAC/C,IAAI,MAAM,CAAC,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,MAAM,IAAI,cAAc,CAAC,mCAAmC,CAAC,CAAC;IAC7F,OAAO,MAAM,CAAC;AAChB,CAAC;AASD;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAClC,GAAe,EACf,aAAsB;IAEtB,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,GAAG,aAAa,CAAC,GAAG,CAAC,CAAC;IAC7C,IAAI,aAAa,EAAE,CAAC;QAClB,OAAO,EAAE,KAAK,EAAE,eAAe,CAAC,OAAO,EAAE,EAAE,eAAe,EAAE,IAAI,EAAE,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;IACxF,CAAC;IACD,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,GAAG,MAAM,YAAY,CAAC,OAAO,EAAE,EAAE,eAAe,EAAE,IAAI,EAAE,CAAC,CAAC;IACzF,MAAM,IAAI,GAAG,OAAO;QAClB,CAAC,CAAC,SAAS;QACX,CAAC,CAAC,KAAK;YACL,CAAC,CAAC,8BAA8B,KAAK,0BAA0B;YAC/D,CAAC,CAAC,wGAAwG,CAAC;IAC/G,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;AAClC,CAAC"}
|
package/dist/health.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session health: honest, proxy-based context-rot signals.
|
|
3
|
+
*
|
|
4
|
+
* We CANNOT measure "the model got dumber" from outside — nobody can. What we
|
|
5
|
+
* can measure are the conditions research associates with degraded attention
|
|
6
|
+
* (context rot): window pressure, recent tool failures, and the agent looping
|
|
7
|
+
* on the same commands. These are labeled as signals, never as a verdict, and
|
|
8
|
+
* refresh stays user-triggered. No fake quality detector.
|
|
9
|
+
*/
|
|
10
|
+
import type { NsfSession } from "./nsf.js";
|
|
11
|
+
export interface HealthSignal {
|
|
12
|
+
/** Short machine name, e.g. "window-pressure". */
|
|
13
|
+
name: string;
|
|
14
|
+
/** Human sentence, e.g. "≈142k tokens in context (~71% of a 200k window)". */
|
|
15
|
+
detail: string;
|
|
16
|
+
/** How loud to be about it. */
|
|
17
|
+
level: "info" | "warn";
|
|
18
|
+
}
|
|
19
|
+
export interface SessionHealth {
|
|
20
|
+
turns: number;
|
|
21
|
+
/** Rough token estimate of the full transcript (~4 chars/token). */
|
|
22
|
+
estTokens: number;
|
|
23
|
+
/** Tokens of actual reasoning/prose (NSF turn text), the "signal". */
|
|
24
|
+
signalTokens: number;
|
|
25
|
+
/** Fraction of the transcript that is tool-output/noise, 0..1. */
|
|
26
|
+
noiseRatio: number;
|
|
27
|
+
/** Lossy in-place summarizations the tool already applied ("scars"). */
|
|
28
|
+
compactions: number;
|
|
29
|
+
/** Files the session references that no longer exist or changed since. */
|
|
30
|
+
staleRefs: string[];
|
|
31
|
+
signals: HealthSignal[];
|
|
32
|
+
/** True when at least one warn-level signal fired. */
|
|
33
|
+
suggestRefresh: boolean;
|
|
34
|
+
}
|
|
35
|
+
export declare function assessSession(session: NsfSession): SessionHealth;
|
package/dist/health.js
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session health: honest, proxy-based context-rot signals.
|
|
3
|
+
*
|
|
4
|
+
* We CANNOT measure "the model got dumber" from outside — nobody can. What we
|
|
5
|
+
* can measure are the conditions research associates with degraded attention
|
|
6
|
+
* (context rot): window pressure, recent tool failures, and the agent looping
|
|
7
|
+
* on the same commands. These are labeled as signals, never as a verdict, and
|
|
8
|
+
* refresh stays user-triggered. No fake quality detector.
|
|
9
|
+
*/
|
|
10
|
+
import { groundFiles } from "./distill/template.js";
|
|
11
|
+
/** Default reference window; most coding tools sit at 200k today. */
|
|
12
|
+
const REF_WINDOW_TOKENS = 200_000;
|
|
13
|
+
/** Research heuristic: quality risk grows past ~60% utilization. */
|
|
14
|
+
const PRESSURE_WARN_RATIO = 0.6;
|
|
15
|
+
const RECENT_WINDOW = 20; // turns considered "recent" for failure/loop checks
|
|
16
|
+
const FAILURE_WARN_RATIO = 0.25;
|
|
17
|
+
const LOOP_WARN_REPEATS = 3;
|
|
18
|
+
export function assessSession(session) {
|
|
19
|
+
const turns = session.turns.length;
|
|
20
|
+
// Signal = the reasoning/prose that survives into NSF. Raw = the full
|
|
21
|
+
// transcript the live window actually carried (tool dumps, thinking, etc.).
|
|
22
|
+
let signalChars = 0;
|
|
23
|
+
for (const t of session.turns) {
|
|
24
|
+
signalChars += t.text.length;
|
|
25
|
+
for (const a of t.actions)
|
|
26
|
+
signalChars += (a.command?.length ?? 0) + (a.path?.length ?? 0);
|
|
27
|
+
}
|
|
28
|
+
const rawBytes = session.provenance.rawBytes ?? signalChars;
|
|
29
|
+
const estTokens = Math.ceil(Math.max(signalChars, rawBytes) / 4);
|
|
30
|
+
const signalTokens = Math.ceil(signalChars / 4);
|
|
31
|
+
const noiseRatio = rawBytes > 0 ? Math.max(0, 1 - signalChars / rawBytes) : 0;
|
|
32
|
+
const compactions = session.provenance.compactions ?? 0;
|
|
33
|
+
// Stale references: touched files that vanished or changed since discussed.
|
|
34
|
+
const staleRefs = groundFiles(session)
|
|
35
|
+
.filter((f) => !f.exists || f.git === "M")
|
|
36
|
+
.map((f) => f.path);
|
|
37
|
+
const signals = [];
|
|
38
|
+
// Noise: a rotted session is mostly tool output wrapped around a small core.
|
|
39
|
+
if (rawBytes > 40_000 && noiseRatio >= 0.85) {
|
|
40
|
+
signals.push({
|
|
41
|
+
name: "noise-ratio",
|
|
42
|
+
detail: `${Math.round(noiseRatio * 100)}% of the window is tool output/noise — only ~${Math.round(signalTokens / 1000)}k tokens of actual reasoning`,
|
|
43
|
+
level: "warn",
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
// Compaction scars: each one is a lossy summary between you and your arc.
|
|
47
|
+
if (compactions >= 2) {
|
|
48
|
+
signals.push({
|
|
49
|
+
name: "compaction-scars",
|
|
50
|
+
detail: `compacted ${compactions}× — ${compactions} layers of lossy summary between you and your original arc`,
|
|
51
|
+
level: "warn",
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
else if (compactions === 1) {
|
|
55
|
+
signals.push({
|
|
56
|
+
name: "compaction-scars",
|
|
57
|
+
detail: `compacted once — one lossy summary already applied in-place`,
|
|
58
|
+
level: "info",
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
// Stale references: the session "remembers" files that no longer match reality.
|
|
62
|
+
if (staleRefs.length >= 2) {
|
|
63
|
+
signals.push({
|
|
64
|
+
name: "stale-refs",
|
|
65
|
+
detail: `${staleRefs.length} referenced files changed or vanished since discussed — the session's memory is out of date`,
|
|
66
|
+
level: "warn",
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
const ratio = estTokens / REF_WINDOW_TOKENS;
|
|
70
|
+
if (ratio >= PRESSURE_WARN_RATIO) {
|
|
71
|
+
signals.push({
|
|
72
|
+
name: "window-pressure",
|
|
73
|
+
detail: `≈${Math.round(estTokens / 1000)}k tokens of transcript (~${Math.round(ratio * 100)}% of a 200k window) — attention degrades well before the window is full`,
|
|
74
|
+
level: "warn",
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
else if (ratio >= 0.3) {
|
|
78
|
+
signals.push({
|
|
79
|
+
name: "window-pressure",
|
|
80
|
+
detail: `≈${Math.round(estTokens / 1000)}k tokens of transcript (~${Math.round(ratio * 100)}% of a 200k window)`,
|
|
81
|
+
level: "info",
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
// Recent failure rate: failed actions / actions, over the last N turns.
|
|
85
|
+
const recent = session.turns.slice(-RECENT_WINDOW);
|
|
86
|
+
let acts = 0;
|
|
87
|
+
let failed = 0;
|
|
88
|
+
for (const t of recent) {
|
|
89
|
+
for (const a of t.actions) {
|
|
90
|
+
acts++;
|
|
91
|
+
if (a.failed)
|
|
92
|
+
failed++;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (acts >= 4 && failed / acts >= FAILURE_WARN_RATIO) {
|
|
96
|
+
signals.push({
|
|
97
|
+
name: "failure-rate",
|
|
98
|
+
detail: `${failed}/${acts} recent tool actions failed — a common looping/rot symptom`,
|
|
99
|
+
level: "warn",
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
// Command looping: the same command run 3+ times in the recent window.
|
|
103
|
+
const counts = new Map();
|
|
104
|
+
for (const t of recent) {
|
|
105
|
+
for (const a of t.actions) {
|
|
106
|
+
if (!a.command)
|
|
107
|
+
continue;
|
|
108
|
+
const key = a.command.split("\n")[0].trim();
|
|
109
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
for (const [cmd, n] of counts) {
|
|
113
|
+
if (n >= LOOP_WARN_REPEATS) {
|
|
114
|
+
signals.push({
|
|
115
|
+
name: "command-loop",
|
|
116
|
+
detail: `\`${cmd}\` run ${n}× recently — the agent may be retrying in circles`,
|
|
117
|
+
level: "warn",
|
|
118
|
+
});
|
|
119
|
+
break; // one loop signal is enough
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
turns,
|
|
124
|
+
estTokens,
|
|
125
|
+
signalTokens,
|
|
126
|
+
noiseRatio,
|
|
127
|
+
compactions,
|
|
128
|
+
staleRefs,
|
|
129
|
+
signals,
|
|
130
|
+
suggestRefresh: signals.some((s) => s.level === "warn"),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
//# sourceMappingURL=health.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"health.js","sourceRoot":"","sources":["../src/health.ts"],"names":[],"mappings":"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"}
|