@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.
Files changed (53) hide show
  1. package/README.md +61 -0
  2. package/dist/cli.d.ts +14 -0
  3. package/dist/cli.js +385 -0
  4. package/dist/cli.js.map +1 -0
  5. package/dist/cloud.d.ts +33 -0
  6. package/dist/cloud.js +72 -0
  7. package/dist/cloud.js.map +1 -0
  8. package/dist/config.d.ts +10 -0
  9. package/dist/config.js +36 -0
  10. package/dist/config.js.map +1 -0
  11. package/dist/discover.d.ts +14 -0
  12. package/dist/discover.js +44 -0
  13. package/dist/discover.js.map +1 -0
  14. package/dist/distill/smart.d.ts +30 -0
  15. package/dist/distill/smart.js +108 -0
  16. package/dist/distill/smart.js.map +1 -0
  17. package/dist/distill/template.d.ts +58 -0
  18. package/dist/distill/template.js +127 -0
  19. package/dist/distill/template.js.map +1 -0
  20. package/dist/engine.d.ts +24 -0
  21. package/dist/engine.js +42 -0
  22. package/dist/engine.js.map +1 -0
  23. package/dist/health.d.ts +35 -0
  24. package/dist/health.js +133 -0
  25. package/dist/health.js.map +1 -0
  26. package/dist/import.d.ts +27 -0
  27. package/dist/import.js +129 -0
  28. package/dist/import.js.map +1 -0
  29. package/dist/llm.d.ts +23 -0
  30. package/dist/llm.js +90 -0
  31. package/dist/llm.js.map +1 -0
  32. package/dist/mcp.d.ts +13 -0
  33. package/dist/mcp.js +115 -0
  34. package/dist/mcp.js.map +1 -0
  35. package/dist/nsf.d.ts +75 -0
  36. package/dist/nsf.js +14 -0
  37. package/dist/nsf.js.map +1 -0
  38. package/dist/parsers/chat-export.d.ts +21 -0
  39. package/dist/parsers/chat-export.js +107 -0
  40. package/dist/parsers/chat-export.js.map +1 -0
  41. package/dist/parsers/claude-code.d.ts +24 -0
  42. package/dist/parsers/claude-code.js +193 -0
  43. package/dist/parsers/claude-code.js.map +1 -0
  44. package/dist/redact.d.ts +24 -0
  45. package/dist/redact.js +65 -0
  46. package/dist/redact.js.map +1 -0
  47. package/dist/tools.d.ts +37 -0
  48. package/dist/tools.js +67 -0
  49. package/dist/tools.js.map +1 -0
  50. package/dist/writers/brief-markdown.d.ts +12 -0
  51. package/dist/writers/brief-markdown.js +83 -0
  52. package/dist/writers/brief-markdown.js.map +1 -0
  53. package/package.json +45 -0
@@ -0,0 +1,27 @@
1
+ /**
2
+ * `sup import` sources: a local export file, pasted text/JSON, or a share URL.
3
+ *
4
+ * Reliability ladder (honest by design):
5
+ * - FILE / STDIN: fully reliable. The official export JSON is a stable contract.
6
+ * - URL: best-effort. Modern share pages (ChatGPT especially) render via a JS
7
+ * data loader, so a plain fetch often only sees a shell. We try to extract an
8
+ * embedded conversation payload from the server HTML; if we can't, we say so
9
+ * clearly and point at the reliable export-file path — we never pretend.
10
+ */
11
+ import type { NsfSession } from "./nsf.js";
12
+ export declare class ImportError extends Error {
13
+ }
14
+ /**
15
+ * Try to pull an embedded conversation JSON blob out of server-rendered HTML.
16
+ * Covers a few known patterns; returns undefined if none match (JS-gated page).
17
+ */
18
+ export declare function extractEmbeddedJson(html: string): unknown | undefined;
19
+ /** Import from a local export file (JSON). Always reliable. */
20
+ export declare function importFromFile(path: string, tool?: string): NsfSession;
21
+ /** Import from raw pasted JSON text. */
22
+ export declare function importFromText(text: string, tool?: string): NsfSession;
23
+ /**
24
+ * Import from a share URL (best-effort). Throws a helpful ImportError when the
25
+ * page is JS-gated so the caller can steer the user to the export-file path.
26
+ */
27
+ export declare function importFromUrl(url: string): Promise<NsfSession>;
package/dist/import.js ADDED
@@ -0,0 +1,129 @@
1
+ /**
2
+ * `sup import` sources: a local export file, pasted text/JSON, or a share URL.
3
+ *
4
+ * Reliability ladder (honest by design):
5
+ * - FILE / STDIN: fully reliable. The official export JSON is a stable contract.
6
+ * - URL: best-effort. Modern share pages (ChatGPT especially) render via a JS
7
+ * data loader, so a plain fetch often only sees a shell. We try to extract an
8
+ * embedded conversation payload from the server HTML; if we can't, we say so
9
+ * clearly and point at the reliable export-file path — we never pretend.
10
+ */
11
+ import { readFileSync } from "node:fs";
12
+ import { chatToNsf } from "./parsers/chat-export.js";
13
+ export class ImportError extends Error {
14
+ }
15
+ /** Guess a provenance tool id from a URL host. */
16
+ function toolFromUrl(url) {
17
+ const h = url.toLowerCase();
18
+ if (h.includes("chatgpt.com") || h.includes("openai.com"))
19
+ return "chatgpt";
20
+ if (h.includes("claude.ai"))
21
+ return "claude-web";
22
+ if (h.includes("gemini.google") || h.includes("g.co/gemini"))
23
+ return "gemini";
24
+ return "web-chat";
25
+ }
26
+ /**
27
+ * Try to pull an embedded conversation JSON blob out of server-rendered HTML.
28
+ * Covers a few known patterns; returns undefined if none match (JS-gated page).
29
+ */
30
+ export function extractEmbeddedJson(html) {
31
+ const candidates = [];
32
+ // Next.js style: <script id="__NEXT_DATA__" type="application/json">{...}</script>
33
+ const next = /<script[^>]+id="__NEXT_DATA__"[^>]*>([\s\S]*?)<\/script>/.exec(html);
34
+ if (next?.[1])
35
+ candidates.push(next[1]);
36
+ // React Router / streamed loader payloads sometimes inline JSON assignments.
37
+ const loader = /window\.__reactRouter[\w$]*\s*=\s*(\{[\s\S]*?\});?<\/script>/.exec(html);
38
+ if (loader?.[1])
39
+ candidates.push(loader[1]);
40
+ for (const raw of candidates) {
41
+ try {
42
+ const parsed = JSON.parse(raw);
43
+ // Walk a couple of likely nesting spots for something conversation-shaped.
44
+ const found = findConversation(parsed);
45
+ if (found)
46
+ return found;
47
+ }
48
+ catch {
49
+ // try next candidate
50
+ }
51
+ }
52
+ return undefined;
53
+ }
54
+ /** Depth-limited search for an object that looks like a conversation. */
55
+ function findConversation(node, depth = 0) {
56
+ if (!node || typeof node !== "object" || depth > 8)
57
+ return undefined;
58
+ const o = node;
59
+ if (o.mapping && typeof o.mapping === "object")
60
+ return o;
61
+ if (Array.isArray(o.messages) && o.messages.length)
62
+ return o;
63
+ for (const v of Object.values(o)) {
64
+ const hit = findConversation(v, depth + 1);
65
+ if (hit)
66
+ return hit;
67
+ }
68
+ return undefined;
69
+ }
70
+ /** Import from a local export file (JSON). Always reliable. */
71
+ export function importFromFile(path, tool = "web-chat") {
72
+ let raw;
73
+ try {
74
+ raw = readFileSync(path, "utf8");
75
+ }
76
+ catch {
77
+ throw new ImportError(`Could not read file: ${path}`);
78
+ }
79
+ let data;
80
+ try {
81
+ data = JSON.parse(raw);
82
+ }
83
+ catch {
84
+ throw new ImportError(`File is not valid JSON: ${path}`);
85
+ }
86
+ const session = chatToNsf(data, tool);
87
+ if (session.turns.length === 0)
88
+ throw new ImportError("No messages found in the export file. Is this a ChatGPT/Claude export?");
89
+ return session;
90
+ }
91
+ /** Import from raw pasted JSON text. */
92
+ export function importFromText(text, tool = "web-chat") {
93
+ const data = JSON.parse(text);
94
+ const session = chatToNsf(data, tool);
95
+ if (session.turns.length === 0)
96
+ throw new ImportError("No messages found in the pasted content.");
97
+ return session;
98
+ }
99
+ /**
100
+ * Import from a share URL (best-effort). Throws a helpful ImportError when the
101
+ * page is JS-gated so the caller can steer the user to the export-file path.
102
+ */
103
+ export async function importFromUrl(url) {
104
+ const tool = toolFromUrl(url);
105
+ let html;
106
+ try {
107
+ const res = await fetch(url, { headers: { "user-agent": "supersession-import/0.1" } });
108
+ if (!res.ok)
109
+ throw new ImportError(`Fetch failed (HTTP ${res.status}) for ${url}`);
110
+ html = await res.text();
111
+ }
112
+ catch (e) {
113
+ if (e instanceof ImportError)
114
+ throw e;
115
+ throw new ImportError(`Could not fetch ${url}: ${e instanceof Error ? e.message : e}`);
116
+ }
117
+ const data = extractEmbeddedJson(html);
118
+ if (!data) {
119
+ throw new ImportError(`Couldn't extract the conversation from the share page — it's rendered client-side.\n` +
120
+ ` Reliable path: export the chat (ChatGPT: Settings → Data controls → Export;\n` +
121
+ ` Claude: Settings → Export data), then run:\n` +
122
+ ` sup import <file.json> <tool>`);
123
+ }
124
+ const session = chatToNsf(data, tool);
125
+ if (session.turns.length === 0)
126
+ throw new ImportError("Extracted the page but found no messages in it.");
127
+ return session;
128
+ }
129
+ //# sourceMappingURL=import.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"import.js","sourceRoot":"","sources":["../src/import.ts"],"names":[],"mappings":"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"}
package/dist/llm.d.ts ADDED
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Minimal, dependency-free LLM client for the smart distiller.
3
+ *
4
+ * Two paths, one interface:
5
+ * - BYO key (local): reads ANTHROPIC_API_KEY or OPENAI_API_KEY from env and
6
+ * calls the provider directly. Zero COGS to us; maximum trust for the user.
7
+ * - Cloud (later): the hosted CLI will point SUPERSESSION_LLM_BASE at the
8
+ * Cloudflare AI Gateway proxy, which meters credits. Same call shape.
9
+ *
10
+ * We deliberately don't pull in an SDK — the messages API is one fetch call and
11
+ * an SDK would be the only heavy dependency in the whole package.
12
+ */
13
+ export type LlmComplete = (prompt: string) => Promise<string>;
14
+ export interface LlmConfig {
15
+ provider: "anthropic" | "openai";
16
+ apiKey: string;
17
+ model: string;
18
+ baseUrl?: string;
19
+ }
20
+ /** Resolve config from environment. Returns undefined if no key is available. */
21
+ export declare function resolveLlmConfig(env?: NodeJS.ProcessEnv): LlmConfig | undefined;
22
+ /** Build a caller from config, or return undefined if no key is configured. */
23
+ export declare function makeLlm(cfg?: LlmConfig | undefined): LlmComplete | undefined;
package/dist/llm.js ADDED
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Minimal, dependency-free LLM client for the smart distiller.
3
+ *
4
+ * Two paths, one interface:
5
+ * - BYO key (local): reads ANTHROPIC_API_KEY or OPENAI_API_KEY from env and
6
+ * calls the provider directly. Zero COGS to us; maximum trust for the user.
7
+ * - Cloud (later): the hosted CLI will point SUPERSESSION_LLM_BASE at the
8
+ * Cloudflare AI Gateway proxy, which meters credits. Same call shape.
9
+ *
10
+ * We deliberately don't pull in an SDK — the messages API is one fetch call and
11
+ * an SDK would be the only heavy dependency in the whole package.
12
+ */
13
+ const DEFAULT_ANTHROPIC_MODEL = "claude-haiku-4-5";
14
+ const DEFAULT_OPENAI_MODEL = "gpt-4.1-mini";
15
+ /** Resolve config from environment. Returns undefined if no key is available. */
16
+ export function resolveLlmConfig(env = process.env) {
17
+ const model = env.SUPERSESSION_MODEL;
18
+ const base = env.SUPERSESSION_LLM_BASE;
19
+ const anthropicKey = env.SUPERSESSION_ANTHROPIC_KEY ?? env.ANTHROPIC_API_KEY;
20
+ if (anthropicKey) {
21
+ return {
22
+ provider: "anthropic",
23
+ apiKey: anthropicKey,
24
+ model: model ?? DEFAULT_ANTHROPIC_MODEL,
25
+ baseUrl: base,
26
+ };
27
+ }
28
+ const openaiKey = env.SUPERSESSION_OPENAI_KEY ?? env.OPENAI_API_KEY;
29
+ if (openaiKey) {
30
+ return {
31
+ provider: "openai",
32
+ apiKey: openaiKey,
33
+ model: model ?? DEFAULT_OPENAI_MODEL,
34
+ baseUrl: base,
35
+ };
36
+ }
37
+ return undefined;
38
+ }
39
+ async function callAnthropic(cfg, prompt) {
40
+ const url = `${cfg.baseUrl ?? "https://api.anthropic.com"}/v1/messages`;
41
+ const res = await fetch(url, {
42
+ method: "POST",
43
+ headers: {
44
+ "content-type": "application/json",
45
+ "x-api-key": cfg.apiKey,
46
+ "anthropic-version": "2023-06-01",
47
+ },
48
+ body: JSON.stringify({
49
+ model: cfg.model,
50
+ max_tokens: 1500,
51
+ messages: [{ role: "user", content: prompt }],
52
+ }),
53
+ });
54
+ if (!res.ok)
55
+ throw new Error(`Anthropic ${res.status}: ${await res.text()}`);
56
+ const data = (await res.json());
57
+ const text = data.content?.find((b) => b.type === "text")?.text;
58
+ if (!text)
59
+ throw new Error("Anthropic returned no text block");
60
+ return text;
61
+ }
62
+ async function callOpenai(cfg, prompt) {
63
+ const url = `${cfg.baseUrl ?? "https://api.openai.com"}/v1/chat/completions`;
64
+ const res = await fetch(url, {
65
+ method: "POST",
66
+ headers: {
67
+ "content-type": "application/json",
68
+ authorization: `Bearer ${cfg.apiKey}`,
69
+ },
70
+ body: JSON.stringify({
71
+ model: cfg.model,
72
+ max_tokens: 1500,
73
+ messages: [{ role: "user", content: prompt }],
74
+ }),
75
+ });
76
+ if (!res.ok)
77
+ throw new Error(`OpenAI ${res.status}: ${await res.text()}`);
78
+ const data = (await res.json());
79
+ const text = data.choices?.[0]?.message?.content;
80
+ if (!text)
81
+ throw new Error("OpenAI returned no content");
82
+ return text;
83
+ }
84
+ /** Build a caller from config, or return undefined if no key is configured. */
85
+ export function makeLlm(cfg = resolveLlmConfig()) {
86
+ if (!cfg)
87
+ return undefined;
88
+ return (prompt) => cfg.provider === "anthropic" ? callAnthropic(cfg, prompt) : callOpenai(cfg, prompt);
89
+ }
90
+ //# sourceMappingURL=llm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"llm.js","sourceRoot":"","sources":["../src/llm.ts"],"names":[],"mappings":"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"}
package/dist/mcp.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Supersession MCP server — one door that lights up every MCP-speaking IDE
4
+ * (Cursor, Claude Desktop, Antigravity, VS Code). Inside any of them the user
5
+ * just says "hand this off to Codex" / "refresh this session" and the agent
6
+ * calls these tools; the engine does the same parse → distill → write it does
7
+ * for the CLI.
8
+ *
9
+ * CRITICAL: stdout is the JSON-RPC channel. Nothing here may write to stdout
10
+ * except the transport. The shared engine is stdout-silent by contract; any
11
+ * diagnostics go to stderr.
12
+ */
13
+ export {};
package/dist/mcp.js ADDED
@@ -0,0 +1,115 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Supersession MCP server — one door that lights up every MCP-speaking IDE
4
+ * (Cursor, Claude Desktop, Antigravity, VS Code). Inside any of them the user
5
+ * just says "hand this off to Codex" / "refresh this session" and the agent
6
+ * calls these tools; the engine does the same parse → distill → write it does
7
+ * for the CLI.
8
+ *
9
+ * CRITICAL: stdout is the JSON-RPC channel. Nothing here may write to stdout
10
+ * except the transport. The shared engine is stdout-silent by contract; any
11
+ * diagnostics go to stderr.
12
+ */
13
+ import { writeFileSync } from "node:fs";
14
+ import { join } from "node:path";
15
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
16
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
17
+ import { ListToolsRequestSchema, CallToolRequestSchema } from "@modelcontextprotocol/sdk/types.js";
18
+ import { requireSession, distillSession, NoSessionError } from "./engine.js";
19
+ import { resolveTool, TOOLS } from "./tools.js";
20
+ import { assessSession } from "./health.js";
21
+ const TOOL_IDS = Object.keys(TOOLS).join(" | ");
22
+ const server = new Server({ name: "supersession", version: "0.1.0" }, { capabilities: { tools: {} } });
23
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
24
+ tools: [
25
+ {
26
+ name: "handoff",
27
+ description: "Hand off the current coding session to another AI tool. Distills the session (goal, decisions, files, next step) and writes the target tool's kickoff file so it continues instead of restarting.",
28
+ inputSchema: {
29
+ type: "object",
30
+ properties: {
31
+ tool: { type: "string", description: `Target tool: ${TOOL_IDS}` },
32
+ cwd: { type: "string", description: "Project directory (defaults to the server's cwd)" },
33
+ },
34
+ required: ["tool"],
35
+ },
36
+ },
37
+ {
38
+ name: "refresh",
39
+ description: "Continue the CURRENT tool in a fresh session — beats /compact for context rot. Distills the full on-disk transcript (including turns compaction dropped) into a clean brief.",
40
+ inputSchema: {
41
+ type: "object",
42
+ properties: { cwd: { type: "string", description: "Project directory (defaults to server cwd)" } },
43
+ },
44
+ },
45
+ {
46
+ name: "status",
47
+ description: "Report session quality: size, how much is live reasoning vs tool-output noise, compaction scars, stale file references, and whether a refresh is warranted.",
48
+ inputSchema: {
49
+ type: "object",
50
+ properties: { cwd: { type: "string", description: "Project directory (defaults to server cwd)" } },
51
+ },
52
+ },
53
+ ],
54
+ }));
55
+ function text(s) {
56
+ return { content: [{ type: "text", text: s }] };
57
+ }
58
+ function fail(s) {
59
+ return { content: [{ type: "text", text: s }], isError: true };
60
+ }
61
+ server.setRequestHandler(CallToolRequestSchema, async (req) => {
62
+ const args = (req.params.arguments ?? {});
63
+ const cwd = args.cwd ?? process.cwd();
64
+ try {
65
+ if (req.params.name === "status") {
66
+ const h = assessSession(requireSession(cwd));
67
+ const lines = [
68
+ `Session quality (measured from the transcript):`,
69
+ `- ${h.turns} turns, ≈${Math.round(h.estTokens / 1000)}k tokens in window`,
70
+ `- ≈${Math.round(h.signalTokens / 1000)}k tokens live reasoning (${Math.round((1 - h.noiseRatio) * 100)}% signal)`,
71
+ h.compactions ? `- ${h.compactions} compaction scar(s)` : "",
72
+ h.staleRefs.length ? `- ${h.staleRefs.length} stale file reference(s)` : "",
73
+ ...h.signals.map((s) => `- ${s.level === "warn" ? "⚠" : "·"} ${s.detail}`),
74
+ h.suggestRefresh ? `→ Recommend: refresh (fresh window, full arc).` : `→ Session looks healthy.`,
75
+ ].filter(Boolean);
76
+ return text(lines.join("\n"));
77
+ }
78
+ // handoff + refresh share the distill→write path; refresh targets the source tool.
79
+ const nsf = requireSession(cwd);
80
+ const isRefresh = req.params.name === "refresh";
81
+ const toolId = isRefresh ? nsf.provenance.tool : args.tool;
82
+ if (!toolId)
83
+ return fail(`Missing "tool". One of: ${TOOL_IDS}`);
84
+ const tool = resolveTool(toolId);
85
+ if (!tool?.writeTarget)
86
+ return fail(`Unknown tool "${toolId}". One of: ${TOOL_IDS}`);
87
+ const { brief, note } = await distillSession(nsf, false);
88
+ const sourceLabel = isRefresh ? `${tool.label} (previous session)` : "Claude Code";
89
+ const target = tool.writeTarget(brief, sourceLabel);
90
+ writeFileSync(join(cwd, target.file), target.body, "utf8");
91
+ const summary = [
92
+ isRefresh ? `Refreshed into a clean ${tool.label} session.` : `Handed off to ${tool.label}.`,
93
+ `Wrote ${target.file} (${brief.fidelity.verbatimTurns} turns verbatim, ${brief.files.length} files).`,
94
+ note ? `Note: ${note}.` : "",
95
+ `Continue with: ${target.launchHint}`,
96
+ ].filter(Boolean);
97
+ return text(summary.join("\n"));
98
+ }
99
+ catch (e) {
100
+ if (e instanceof NoSessionError)
101
+ return fail(e.message);
102
+ return fail(`Supersession error: ${e instanceof Error ? e.message : String(e)}`);
103
+ }
104
+ });
105
+ async function main() {
106
+ const transport = new StdioServerTransport();
107
+ await server.connect(transport);
108
+ // Ready. (stderr only — stdout is the protocol channel.)
109
+ process.stderr.write("supersession MCP server ready\n");
110
+ }
111
+ main().catch((e) => {
112
+ process.stderr.write(`fatal: ${e instanceof Error ? e.message : String(e)}\n`);
113
+ process.exit(1);
114
+ });
115
+ //# sourceMappingURL=mcp.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcp.js","sourceRoot":"","sources":["../src/mcp.ts"],"names":[],"mappings":"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"}
package/dist/nsf.d.ts ADDED
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Neutral Session Format (NSF) — the portable, tool-agnostic representation of
3
+ * an AI coding session. Every parser produces NSF; every distiller and writer
4
+ * consumes it. This is the contract; keeping it small and honest is the whole
5
+ * game.
6
+ *
7
+ * Design rules:
8
+ * - Lossy on purpose but never *silently* lossy. If a parser can't map
9
+ * something, it drops it and the distiller's fidelity report says so.
10
+ * - No vendor concepts leak in. "thinking", "tool_use", Codex's "AGENTS.md" —
11
+ * none of those words appear here. Only turns and actions.
12
+ */
13
+ export declare const NSF_VERSION: "0.1";
14
+ /** A single tool invocation the agent made, normalized across tools. */
15
+ export interface NsfAction {
16
+ /** Normalized verb: what the agent did. */
17
+ kind: "edit" | "write" | "read" | "run" | "search" | "other";
18
+ /** Tool's own name for it, kept for provenance (e.g. "Bash", "MultiEdit"). */
19
+ rawName: string;
20
+ /** File path this action touched, if any (absolute or repo-relative as recorded). */
21
+ path?: string;
22
+ /** Shell command, for `run` actions. */
23
+ command?: string;
24
+ /** Whether the tool reported this action as failed. */
25
+ failed?: boolean;
26
+ }
27
+ /** One conversational turn. Tool noise is folded into `actions`, not `text`. */
28
+ export interface NsfTurn {
29
+ role: "user" | "assistant";
30
+ /** Human/assistant prose only. Never contains tool JSON or thinking blocks. */
31
+ text: string;
32
+ /** Actions the agent took during this turn (assistant turns only). */
33
+ actions: NsfAction[];
34
+ /** Epoch ms if the source recorded it. */
35
+ timestamp?: number;
36
+ }
37
+ /** Provenance of where this session came from. */
38
+ export interface NsfProvenance {
39
+ /** Source tool id, e.g. "claude-code". */
40
+ tool: string;
41
+ /** The tool's native session id. */
42
+ sessionId?: string;
43
+ /** Absolute working directory the session ran in. */
44
+ cwd?: string;
45
+ /** Git branch recorded at capture time, if any. */
46
+ gitBranch?: string;
47
+ /**
48
+ * Raw size of the source transcript in bytes. NSF drops tool results and
49
+ * thinking, so prose alone underestimates real window pressure — health
50
+ * checks use this as the honest upper-bound proxy.
51
+ */
52
+ rawBytes?: number;
53
+ /**
54
+ * Number of compaction/summary events found in the source transcript. Each
55
+ * one is a lossy summarization the tool applied in-place — a "scar" between
56
+ * you and your original arc. Best-effort per tool.
57
+ */
58
+ compactions?: number;
59
+ }
60
+ /**
61
+ * What the parser had to drop or couldn't represent. Surfaced verbatim in the
62
+ * fidelity report — this is how we keep "lossy but not silent" honest.
63
+ */
64
+ export interface NsfGap {
65
+ reason: string;
66
+ count: number;
67
+ }
68
+ export interface NsfSession {
69
+ nsfVersion: typeof NSF_VERSION;
70
+ provenance: NsfProvenance;
71
+ turns: NsfTurn[];
72
+ /** Distinct files touched across the whole session, in first-seen order. */
73
+ filesTouched: string[];
74
+ gaps: NsfGap[];
75
+ }
package/dist/nsf.js ADDED
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Neutral Session Format (NSF) — the portable, tool-agnostic representation of
3
+ * an AI coding session. Every parser produces NSF; every distiller and writer
4
+ * consumes it. This is the contract; keeping it small and honest is the whole
5
+ * game.
6
+ *
7
+ * Design rules:
8
+ * - Lossy on purpose but never *silently* lossy. If a parser can't map
9
+ * something, it drops it and the distiller's fidelity report says so.
10
+ * - No vendor concepts leak in. "thinking", "tool_use", Codex's "AGENTS.md" —
11
+ * none of those words appear here. Only turns and actions.
12
+ */
13
+ export const NSF_VERSION = "0.1";
14
+ //# sourceMappingURL=nsf.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"nsf.js","sourceRoot":"","sources":["../src/nsf.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,MAAM,CAAC,MAAM,WAAW,GAAG,KAAc,CAAC"}
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Web-chat conversation -> NSF. Handles the two shapes real ChatGPT/Claude/
3
+ * Gemini data produces:
4
+ *
5
+ * 1. Official export mapping-tree (ChatGPT `conversations.json`):
6
+ * { title, mapping: { <id>: { message: { author:{role}, content:{parts|text}, create_time }, parent, children } } }
7
+ *
8
+ * 2. Simple linear form (many exporters, share-page loader payloads):
9
+ * { messages: [ { role, content }, ... ] } OR a bare [ {role,content}, ... ]
10
+ *
11
+ * Web chats have no local tool actions or file edits, so NSF turns carry prose
12
+ * only. That's honest: an imported chat is a plan/discussion, and the receiving
13
+ * coding tool is what will actually touch files.
14
+ */
15
+ import type { NsfSession } from "../nsf.js";
16
+ /**
17
+ * Convert parsed web-chat JSON into an NSF session.
18
+ * @param data parsed conversation JSON (export or linear form)
19
+ * @param tool provenance id, e.g. "chatgpt" | "claude-web" | "gemini"
20
+ */
21
+ export declare function chatToNsf(data: unknown, tool: string): NsfSession;
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Web-chat conversation -> NSF. Handles the two shapes real ChatGPT/Claude/
3
+ * Gemini data produces:
4
+ *
5
+ * 1. Official export mapping-tree (ChatGPT `conversations.json`):
6
+ * { title, mapping: { <id>: { message: { author:{role}, content:{parts|text}, create_time }, parent, children } } }
7
+ *
8
+ * 2. Simple linear form (many exporters, share-page loader payloads):
9
+ * { messages: [ { role, content }, ... ] } OR a bare [ {role,content}, ... ]
10
+ *
11
+ * Web chats have no local tool actions or file edits, so NSF turns carry prose
12
+ * only. That's honest: an imported chat is a plan/discussion, and the receiving
13
+ * coding tool is what will actually touch files.
14
+ */
15
+ import { NSF_VERSION } from "../nsf.js";
16
+ /** Pull plain text out of the many content shapes a chat message can have. */
17
+ function contentToText(content) {
18
+ if (typeof content === "string")
19
+ return content.trim();
20
+ if (Array.isArray(content))
21
+ return content.map(contentToText).filter(Boolean).join("\n").trim();
22
+ if (content && typeof content === "object") {
23
+ const o = content;
24
+ if (Array.isArray(o.parts))
25
+ return o.parts
26
+ .map((p) => (typeof p === "string" ? p : typeof p === "object" && p ? contentToText(p) : ""))
27
+ .filter(Boolean)
28
+ .join("\n")
29
+ .trim();
30
+ if (typeof o.text === "string")
31
+ return o.text.trim();
32
+ if (typeof o.content === "string")
33
+ return o.content.trim();
34
+ }
35
+ return "";
36
+ }
37
+ /** Extract an ordered list of {role,text} from either supported shape. */
38
+ function extractMessages(data) {
39
+ if (!data || typeof data !== "object")
40
+ return { msgs: [] };
41
+ const obj = data;
42
+ // Shape 1: mapping tree. Linearize by create_time (robust vs. tree-walking).
43
+ if (obj.mapping && typeof obj.mapping === "object") {
44
+ const nodes = Object.values(obj.mapping);
45
+ const msgs = [];
46
+ for (const node of nodes) {
47
+ const m = node.message;
48
+ if (!m)
49
+ continue;
50
+ const role = m.author?.role ?? "unknown";
51
+ const text = contentToText(m.content);
52
+ if (!text)
53
+ continue;
54
+ const ts = typeof m.create_time === "number" ? m.create_time * 1000 : undefined;
55
+ msgs.push({ role, text, ts });
56
+ }
57
+ msgs.sort((a, b) => (a.ts ?? 0) - (b.ts ?? 0));
58
+ return { msgs, title: typeof obj.title === "string" ? obj.title : undefined };
59
+ }
60
+ // Shape 2: linear messages array (nested or bare).
61
+ const arr = Array.isArray(data) ? data : Array.isArray(obj.messages) ? obj.messages : undefined;
62
+ if (arr) {
63
+ const msgs = [];
64
+ for (const m of arr) {
65
+ if (!m || typeof m !== "object")
66
+ continue;
67
+ const mo = m;
68
+ const role = mo.role ?? mo.author ?? "unknown";
69
+ const text = contentToText(mo.content ?? mo.text ?? mo.message);
70
+ if (text)
71
+ msgs.push({ role, text });
72
+ }
73
+ return { msgs, title: typeof obj.title === "string" ? obj.title : undefined };
74
+ }
75
+ return { msgs: [] };
76
+ }
77
+ /**
78
+ * Convert parsed web-chat JSON into an NSF session.
79
+ * @param data parsed conversation JSON (export or linear form)
80
+ * @param tool provenance id, e.g. "chatgpt" | "claude-web" | "gemini"
81
+ */
82
+ export function chatToNsf(data, tool) {
83
+ const { msgs } = extractMessages(data);
84
+ const turns = [];
85
+ let systemDropped = 0;
86
+ for (const m of msgs) {
87
+ const role = m.role === "user" ? "user" : m.role === "assistant" ? "assistant" : null;
88
+ if (!role) {
89
+ systemDropped++;
90
+ continue;
91
+ }
92
+ turns.push({ role, text: m.text, actions: [], timestamp: m.ts });
93
+ }
94
+ const gaps = [];
95
+ if (systemDropped)
96
+ gaps.push({ reason: "non-user/assistant message", count: systemDropped });
97
+ if (turns.length === 0)
98
+ gaps.push({ reason: "no recognizable messages in source", count: 1 });
99
+ return {
100
+ nsfVersion: NSF_VERSION,
101
+ provenance: { tool },
102
+ turns,
103
+ filesTouched: [],
104
+ gaps,
105
+ };
106
+ }
107
+ //# sourceMappingURL=chat-export.js.map