session-forge 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jacob Terrell
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,166 @@
1
+ # session-forge
2
+
3
+ **Never start from zero.** Persistent session intelligence for AI coding assistants.
4
+
5
+ Session-forge gives your AI coding assistant memory that survives across sessions. It tracks decisions, dead ends, user preferences, and session state — so every conversation builds on the last one instead of starting from scratch.
6
+
7
+ Works with **Claude Code**, **Cursor**, **Windsurf**, and any MCP-compatible client.
8
+
9
+ ---
10
+
11
+ ## What it does
12
+
13
+ - **Session crash recovery** — checkpoint your work, pick up where you left off
14
+ - **Decision logging** — record why you chose X over Y, search it later
15
+ - **Dead end tracking** — never repeat the same debugging mistake twice
16
+ - **User profile** — AI remembers your name, preferences, and projects
17
+ - **Session journal** — capture the journey, not just the task
18
+ - **Full context recall** — bootstrap a new session with everything in one call
19
+
20
+ ## Quick start
21
+
22
+ ### Claude Code
23
+
24
+ ```bash
25
+ claude mcp add session-forge -- npx session-forge
26
+ ```
27
+
28
+ ### Cursor / Windsurf
29
+
30
+ Add to your MCP settings:
31
+
32
+ ```json
33
+ {
34
+ "mcpServers": {
35
+ "session-forge": {
36
+ "command": "npx",
37
+ "args": ["-y", "session-forge"]
38
+ }
39
+ }
40
+ }
41
+ ```
42
+
43
+ That's it. No database, no Docker, no config files.
44
+
45
+ ---
46
+
47
+ ## The 12 tools
48
+
49
+ ### Sessions
50
+
51
+ | Tool | Description | Required |
52
+ |------|-------------|----------|
53
+ | `session_checkpoint` | Save work-in-progress state for crash recovery | task, intent, next_steps |
54
+ | `session_restore` | Check for interrupted work from a previous session | — |
55
+ | `session_complete` | Archive session and mark complete | — |
56
+
57
+ ### Profile
58
+
59
+ | Tool | Description | Required |
60
+ |------|-------------|----------|
61
+ | `profile_get` | Get the current user profile | — |
62
+ | `profile_update` | Update name, preferences, projects, or notes | — |
63
+
64
+ ### Journal
65
+
66
+ | Tool | Description | Required |
67
+ |------|-------------|----------|
68
+ | `journal_entry` | Record session summary with breakthroughs and frustrations | summary |
69
+ | `journal_recall` | Retrieve recent session journals | — |
70
+
71
+ ### Decisions
72
+
73
+ | Tool | Description | Required |
74
+ |------|-------------|----------|
75
+ | `decision_record` | Log a significant decision with alternatives and reasoning | choice, reasoning |
76
+ | `decision_search` | Search past decisions by keyword | query |
77
+
78
+ ### Dead Ends
79
+
80
+ | Tool | Description | Required |
81
+ |------|-------------|----------|
82
+ | `dead_end_record` | Log a failed approach and the lesson learned | attempted, why_failed |
83
+ | `dead_end_search` | Search past dead ends to avoid repeating mistakes | query |
84
+
85
+ ### Context
86
+
87
+ | Tool | Description | Required |
88
+ |------|-------------|----------|
89
+ | `full_context_recall` | Get everything — profile, journals, decisions, dead ends | — |
90
+
91
+ ---
92
+
93
+ ## Why session-forge?
94
+
95
+ | | session-forge | Basic memory MCPs | Enterprise tools |
96
+ |---|---|---|---|
97
+ | Setup | `npx session-forge` | Varies | Docker + databases |
98
+ | Dead end tracking | Yes | No | No |
99
+ | Decision logging | Yes | No | Some |
100
+ | Session crash recovery | Yes | Some | Yes |
101
+ | User profile | Yes | Some | No |
102
+ | Dependencies | 2 (SDK + zod) | Varies | 5-10+ |
103
+ | Infrastructure | Zero (plain JSON) | SQLite/ONNX/Vector DB | PostgreSQL + Redis |
104
+ | Tools | 12 focused | 4-9 | 37+ |
105
+
106
+ ---
107
+
108
+ ## Storage
109
+
110
+ All data is stored locally as plain JSON files:
111
+
112
+ | Platform | Location |
113
+ |----------|----------|
114
+ | Linux / macOS | `~/.session-forge/` |
115
+ | Windows | `%APPDATA%\session-forge\` |
116
+
117
+ Override with the `SESSION_FORGE_DIR` environment variable:
118
+
119
+ ```bash
120
+ SESSION_FORGE_DIR=/custom/path npx session-forge
121
+ ```
122
+
123
+ Files:
124
+ ```
125
+ ~/.session-forge/
126
+ profile.json # User preferences and projects
127
+ journal.json # Session summaries (last 100)
128
+ decisions.json # Decision log (last 200)
129
+ dead-ends.json # Failed approaches (last 100)
130
+ sessions/
131
+ active.json # Current checkpoint
132
+ history/ # Archived sessions
133
+ ```
134
+
135
+ ---
136
+
137
+ ## CLAUDE.md template
138
+
139
+ Add this to your project's `CLAUDE.md` to teach the AI when to call each tool:
140
+
141
+ ```markdown
142
+ ## Session Flow
143
+
144
+ ### Fresh session
145
+ 1. Call `full_context_recall` — get profile, journals, decisions, dead ends
146
+ 2. Call `session_restore` — check for interrupted work
147
+
148
+ ### During work
149
+ - `decision_record` — when making a significant architectural choice
150
+ - `dead_end_record` — when something fails and we learn why
151
+ - `session_checkpoint` — every 10-15 tool calls during long sessions
152
+
153
+ ### Session end
154
+ 1. Call `journal_entry` — record what happened
155
+ 2. Call `session_complete` — archive the checkpoint
156
+ ```
157
+
158
+ ---
159
+
160
+ ## License
161
+
162
+ MIT
163
+
164
+ ---
165
+
166
+ Built by [Jacob Terrell](https://github.com/jacobterrell)
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=index.d.ts.map
package/dist/index.js ADDED
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { registerSessionTools } from "./tools/sessions.js";
5
+ import { registerProfileTools } from "./tools/profile.js";
6
+ import { registerJournalTools } from "./tools/journal.js";
7
+ import { registerDecisionTools } from "./tools/decisions.js";
8
+ import { registerDeadEndTools } from "./tools/dead-ends.js";
9
+ import { registerContextTools } from "./tools/context.js";
10
+ const VERSION = "1.0.0";
11
+ const server = new McpServer({
12
+ name: "session-forge",
13
+ version: VERSION,
14
+ });
15
+ registerSessionTools(server);
16
+ registerProfileTools(server);
17
+ registerJournalTools(server);
18
+ registerDecisionTools(server);
19
+ registerDeadEndTools(server);
20
+ registerContextTools(server);
21
+ async function main() {
22
+ const transport = new StdioServerTransport();
23
+ await server.connect(transport);
24
+ console.error(`session-forge v${VERSION} running`);
25
+ }
26
+ main().catch((err) => {
27
+ console.error("Fatal:", err);
28
+ process.exit(1);
29
+ });
30
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,11 @@
1
+ export declare const PATHS: {
2
+ readonly base: string;
3
+ readonly profile: string;
4
+ readonly journal: string;
5
+ readonly decisions: string;
6
+ readonly deadEnds: string;
7
+ readonly sessions: string;
8
+ readonly activeSession: string;
9
+ readonly sessionHistory: string;
10
+ };
11
+ //# sourceMappingURL=paths.d.ts.map
@@ -0,0 +1,23 @@
1
+ import { homedir } from "node:os";
2
+ import { join } from "node:path";
3
+ function getBaseDir() {
4
+ if (process.env.SESSION_FORGE_DIR) {
5
+ return process.env.SESSION_FORGE_DIR;
6
+ }
7
+ if (process.platform === "win32" && process.env.APPDATA) {
8
+ return join(process.env.APPDATA, "session-forge");
9
+ }
10
+ return join(homedir(), ".session-forge");
11
+ }
12
+ const base = getBaseDir();
13
+ export const PATHS = {
14
+ base,
15
+ profile: join(base, "profile.json"),
16
+ journal: join(base, "journal.json"),
17
+ decisions: join(base, "decisions.json"),
18
+ deadEnds: join(base, "dead-ends.json"),
19
+ sessions: join(base, "sessions"),
20
+ activeSession: join(base, "sessions", "active.json"),
21
+ sessionHistory: join(base, "sessions", "history"),
22
+ };
23
+ //# sourceMappingURL=paths.js.map
@@ -0,0 +1,5 @@
1
+ export declare function readJson<T>(filePath: string, fallback: T): T;
2
+ export declare function writeJson<T>(filePath: string, data: T): void;
3
+ export declare function deleteJson(filePath: string): boolean;
4
+ export declare function searchEntries<T>(entries: T[], query: string, textExtractor: (entry: T) => string): T[];
5
+ //# sourceMappingURL=store.d.ts.map
@@ -0,0 +1,44 @@
1
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, unlinkSync } from "node:fs";
2
+ import { dirname } from "node:path";
3
+ export function readJson(filePath, fallback) {
4
+ try {
5
+ if (!existsSync(filePath))
6
+ return fallback;
7
+ return JSON.parse(readFileSync(filePath, "utf-8"));
8
+ }
9
+ catch {
10
+ return fallback;
11
+ }
12
+ }
13
+ export function writeJson(filePath, data) {
14
+ const dir = dirname(filePath);
15
+ if (!existsSync(dir))
16
+ mkdirSync(dir, { recursive: true });
17
+ writeFileSync(filePath, JSON.stringify(data, null, 2), "utf-8");
18
+ }
19
+ export function deleteJson(filePath) {
20
+ try {
21
+ if (existsSync(filePath)) {
22
+ unlinkSync(filePath);
23
+ return true;
24
+ }
25
+ return false;
26
+ }
27
+ catch {
28
+ return false;
29
+ }
30
+ }
31
+ export function searchEntries(entries, query, textExtractor) {
32
+ const words = query
33
+ .toLowerCase()
34
+ .split(/\s+/)
35
+ .filter((w) => w.length > 1);
36
+ if (words.length === 0)
37
+ return entries.slice(-20);
38
+ const filtered = entries.filter((entry) => {
39
+ const text = textExtractor(entry).toLowerCase();
40
+ return words.some((word) => text.includes(word));
41
+ });
42
+ return filtered.slice(-20);
43
+ }
44
+ //# sourceMappingURL=store.js.map
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerContextTools(server: McpServer): void;
3
+ //# sourceMappingURL=context.d.ts.map
@@ -0,0 +1,50 @@
1
+ import { PATHS } from "../storage/paths.js";
2
+ import { readJson } from "../storage/store.js";
3
+ const DEFAULT_PROFILE = {
4
+ name: null,
5
+ preferences: {
6
+ communication_style: "direct",
7
+ emoji_usage: "occasional",
8
+ technical_level: "advanced",
9
+ verbosity: "concise",
10
+ },
11
+ projects: [],
12
+ notes: [],
13
+ created_at: new Date().toISOString(),
14
+ updated_at: new Date().toISOString(),
15
+ };
16
+ export function registerContextTools(server) {
17
+ server.registerTool("full_context_recall", {
18
+ description: "Get EVERYTHING - user profile, recent sessions, decisions, dead ends. Use when starting fresh to get full context.",
19
+ inputSchema: {},
20
+ }, async () => {
21
+ const profile = readJson(PATHS.profile, {
22
+ ...DEFAULT_PROFILE,
23
+ created_at: new Date().toISOString(),
24
+ updated_at: new Date().toISOString(),
25
+ });
26
+ const journal = readJson(PATHS.journal, { sessions: [] });
27
+ const decisions = readJson(PATHS.decisions, {
28
+ decisions: [],
29
+ });
30
+ const deadEnds = readJson(PATHS.deadEnds, {
31
+ dead_ends: [],
32
+ });
33
+ const context = {
34
+ user_profile: profile,
35
+ recent_sessions: journal.sessions.slice(-3),
36
+ recent_decisions: decisions.decisions.slice(-10),
37
+ recent_dead_ends: deadEnds.dead_ends.slice(-10),
38
+ retrieved_at: new Date().toISOString(),
39
+ };
40
+ return {
41
+ content: [
42
+ {
43
+ type: "text",
44
+ text: JSON.stringify(context, null, 2),
45
+ },
46
+ ],
47
+ };
48
+ });
49
+ }
50
+ //# sourceMappingURL=context.js.map
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerDeadEndTools(server: McpServer): void;
3
+ //# sourceMappingURL=dead-ends.d.ts.map
@@ -0,0 +1,73 @@
1
+ import { z } from "zod";
2
+ import { PATHS } from "../storage/paths.js";
3
+ import { readJson, writeJson, searchEntries } from "../storage/store.js";
4
+ export function registerDeadEndTools(server) {
5
+ server.registerTool("dead_end_record", {
6
+ description: "Record a debugging dead end so we don't repeat it. Captures what was tried and why it failed.",
7
+ inputSchema: {
8
+ attempted: z.string().describe("What was tried"),
9
+ why_failed: z.string().describe("Why it didn't work"),
10
+ lesson: z
11
+ .string()
12
+ .optional()
13
+ .describe("What to remember for next time"),
14
+ project: z.string().optional().describe("Which project"),
15
+ files_involved: z
16
+ .array(z.string())
17
+ .optional()
18
+ .describe("Files involved"),
19
+ tags: z
20
+ .array(z.string())
21
+ .optional()
22
+ .describe("Tags for searching"),
23
+ },
24
+ }, async (params) => {
25
+ const data = readJson(PATHS.deadEnds, { dead_ends: [] });
26
+ const entry = {
27
+ timestamp: new Date().toISOString(),
28
+ attempted: params.attempted,
29
+ why_failed: params.why_failed,
30
+ lesson: params.lesson ?? "",
31
+ project: params.project ?? null,
32
+ files_involved: params.files_involved ?? [],
33
+ tags: params.tags ?? [],
34
+ };
35
+ data.dead_ends.push(entry);
36
+ if (data.dead_ends.length > 100) {
37
+ data.dead_ends = data.dead_ends.slice(-100);
38
+ }
39
+ writeJson(PATHS.deadEnds, data);
40
+ return {
41
+ content: [
42
+ {
43
+ type: "text",
44
+ text: JSON.stringify(entry, null, 2),
45
+ },
46
+ ],
47
+ };
48
+ });
49
+ server.registerTool("dead_end_search", {
50
+ description: "Search past dead ends to avoid repeating mistakes",
51
+ inputSchema: {
52
+ query: z.string().describe("Search term"),
53
+ },
54
+ }, async (params) => {
55
+ const data = readJson(PATHS.deadEnds, { dead_ends: [] });
56
+ const results = searchEntries(data.dead_ends, params.query, (d) => [
57
+ d.attempted,
58
+ d.why_failed,
59
+ d.lesson,
60
+ d.project ?? "",
61
+ ...d.tags,
62
+ ].join(" "));
63
+ return {
64
+ content: [
65
+ {
66
+ type: "text",
67
+ text: JSON.stringify(results, null, 2),
68
+ },
69
+ ],
70
+ };
71
+ });
72
+ }
73
+ //# sourceMappingURL=dead-ends.js.map
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerDecisionTools(server: McpServer): void;
3
+ //# sourceMappingURL=decisions.d.ts.map
@@ -0,0 +1,74 @@
1
+ import { z } from "zod";
2
+ import { PATHS } from "../storage/paths.js";
3
+ import { readJson, writeJson, searchEntries } from "../storage/store.js";
4
+ export function registerDecisionTools(server) {
5
+ server.registerTool("decision_record", {
6
+ description: "Record a significant decision made during development. Helps future sessions understand why choices were made.",
7
+ inputSchema: {
8
+ choice: z.string().describe("What was decided"),
9
+ reasoning: z.string().describe("Why this choice was made"),
10
+ alternatives: z
11
+ .array(z.string())
12
+ .optional()
13
+ .describe("What other options existed"),
14
+ outcome: z
15
+ .string()
16
+ .optional()
17
+ .describe("How it turned out (can be updated later)"),
18
+ project: z
19
+ .string()
20
+ .optional()
21
+ .describe("Which project this relates to"),
22
+ tags: z
23
+ .array(z.string())
24
+ .optional()
25
+ .describe("Tags for searching later"),
26
+ },
27
+ }, async (params) => {
28
+ const data = readJson(PATHS.decisions, {
29
+ decisions: [],
30
+ });
31
+ const entry = {
32
+ timestamp: new Date().toISOString(),
33
+ choice: params.choice,
34
+ alternatives: params.alternatives ?? [],
35
+ reasoning: params.reasoning,
36
+ outcome: params.outcome ?? null,
37
+ project: params.project ?? null,
38
+ tags: params.tags ?? [],
39
+ };
40
+ data.decisions.push(entry);
41
+ if (data.decisions.length > 200) {
42
+ data.decisions = data.decisions.slice(-200);
43
+ }
44
+ writeJson(PATHS.decisions, data);
45
+ return {
46
+ content: [
47
+ {
48
+ type: "text",
49
+ text: JSON.stringify(entry, null, 2),
50
+ },
51
+ ],
52
+ };
53
+ });
54
+ server.registerTool("decision_search", {
55
+ description: "Search past decisions to understand why things are the way they are",
56
+ inputSchema: {
57
+ query: z.string().describe("Search term"),
58
+ },
59
+ }, async (params) => {
60
+ const data = readJson(PATHS.decisions, {
61
+ decisions: [],
62
+ });
63
+ const results = searchEntries(data.decisions, params.query, (d) => [d.choice, d.reasoning, d.project ?? "", ...d.tags].join(" "));
64
+ return {
65
+ content: [
66
+ {
67
+ type: "text",
68
+ text: JSON.stringify(results, null, 2),
69
+ },
70
+ ],
71
+ };
72
+ });
73
+ }
74
+ //# sourceMappingURL=decisions.js.map
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerJournalTools(server: McpServer): void;
3
+ //# sourceMappingURL=journal.d.ts.map
@@ -0,0 +1,79 @@
1
+ import { z } from "zod";
2
+ import { PATHS } from "../storage/paths.js";
3
+ import { readJson, writeJson } from "../storage/store.js";
4
+ export function registerJournalTools(server) {
5
+ server.registerTool("journal_entry", {
6
+ description: "Record a session journal entry capturing the journey, not just the task. Call at END of meaningful sessions to preserve context.",
7
+ inputSchema: {
8
+ summary: z
9
+ .string()
10
+ .describe("What happened this session (conversational, not just technical)"),
11
+ key_moments: z
12
+ .array(z.string())
13
+ .optional()
14
+ .describe("Significant moments - breakthroughs, realizations, fun exchanges"),
15
+ emotional_context: z
16
+ .string()
17
+ .optional()
18
+ .describe("How did the session feel? User concerns, celebrations, frustrations"),
19
+ breakthroughs: z
20
+ .array(z.string())
21
+ .optional()
22
+ .describe("What clicked or worked unexpectedly well"),
23
+ frustrations: z
24
+ .array(z.string())
25
+ .optional()
26
+ .describe("What was difficult or annoying"),
27
+ collaboration_notes: z
28
+ .string()
29
+ .optional()
30
+ .describe("Notes about working together - user preferences observed, rapport"),
31
+ },
32
+ }, async (params) => {
33
+ const data = readJson(PATHS.journal, { sessions: [] });
34
+ const entry = {
35
+ timestamp: new Date().toISOString(),
36
+ session_summary: params.summary,
37
+ key_moments: params.key_moments ?? [],
38
+ emotional_context: params.emotional_context ?? null,
39
+ breakthroughs: params.breakthroughs ?? [],
40
+ frustrations: params.frustrations ?? [],
41
+ collaboration_notes: params.collaboration_notes ?? null,
42
+ };
43
+ data.sessions.push(entry);
44
+ if (data.sessions.length > 100) {
45
+ data.sessions = data.sessions.slice(-100);
46
+ }
47
+ writeJson(PATHS.journal, data);
48
+ return {
49
+ content: [
50
+ {
51
+ type: "text",
52
+ text: JSON.stringify(entry, null, 2),
53
+ },
54
+ ],
55
+ };
56
+ });
57
+ server.registerTool("journal_recall", {
58
+ description: "Retrieve recent session context and journeys. Call at START of sessions to remember the relationship.",
59
+ inputSchema: {
60
+ sessions_count: z
61
+ .number()
62
+ .optional()
63
+ .describe("How many recent sessions to retrieve (default 3)"),
64
+ },
65
+ }, async (params) => {
66
+ const data = readJson(PATHS.journal, { sessions: [] });
67
+ const count = params.sessions_count ?? 3;
68
+ const recent = data.sessions.slice(-count);
69
+ return {
70
+ content: [
71
+ {
72
+ type: "text",
73
+ text: JSON.stringify(recent, null, 2),
74
+ },
75
+ ],
76
+ };
77
+ });
78
+ }
79
+ //# sourceMappingURL=journal.js.map
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerProfileTools(server: McpServer): void;
3
+ //# sourceMappingURL=profile.d.ts.map
@@ -0,0 +1,89 @@
1
+ import { z } from "zod";
2
+ import { PATHS } from "../storage/paths.js";
3
+ import { readJson, writeJson } from "../storage/store.js";
4
+ const DEFAULT_PROFILE = {
5
+ name: null,
6
+ preferences: {
7
+ communication_style: "direct",
8
+ emoji_usage: "occasional",
9
+ technical_level: "advanced",
10
+ verbosity: "concise",
11
+ },
12
+ projects: [],
13
+ notes: [],
14
+ created_at: new Date().toISOString(),
15
+ updated_at: new Date().toISOString(),
16
+ };
17
+ export function registerProfileTools(server) {
18
+ server.registerTool("profile_get", {
19
+ description: "Get the current user profile",
20
+ inputSchema: {},
21
+ }, async () => {
22
+ const profile = readJson(PATHS.profile, {
23
+ ...DEFAULT_PROFILE,
24
+ created_at: new Date().toISOString(),
25
+ updated_at: new Date().toISOString(),
26
+ });
27
+ return {
28
+ content: [
29
+ {
30
+ type: "text",
31
+ text: JSON.stringify(profile, null, 2),
32
+ },
33
+ ],
34
+ };
35
+ });
36
+ server.registerTool("profile_update", {
37
+ description: "Update user profile with preferences, notes, or project info. Call when learning something about the user.",
38
+ inputSchema: {
39
+ name: z.string().optional().describe("User's name if learned"),
40
+ preferences: z
41
+ .record(z.string())
42
+ .optional()
43
+ .describe("Preferences like {communication_style, emoji_usage, technical_level, verbosity}"),
44
+ add_project: z
45
+ .string()
46
+ .optional()
47
+ .describe("Add a project name to user's project list"),
48
+ add_note: z
49
+ .string()
50
+ .optional()
51
+ .describe("Add a note about the user (observations, preferences)"),
52
+ },
53
+ }, async (params) => {
54
+ const profile = readJson(PATHS.profile, {
55
+ ...DEFAULT_PROFILE,
56
+ created_at: new Date().toISOString(),
57
+ updated_at: new Date().toISOString(),
58
+ });
59
+ if (params.name) {
60
+ profile.name = params.name;
61
+ }
62
+ if (params.preferences) {
63
+ profile.preferences = { ...profile.preferences, ...params.preferences };
64
+ }
65
+ if (params.add_project && !profile.projects.includes(params.add_project)) {
66
+ profile.projects.push(params.add_project);
67
+ }
68
+ if (params.add_note) {
69
+ profile.notes.push({
70
+ content: params.add_note,
71
+ timestamp: new Date().toISOString(),
72
+ });
73
+ if (profile.notes.length > 50) {
74
+ profile.notes = profile.notes.slice(-50);
75
+ }
76
+ }
77
+ profile.updated_at = new Date().toISOString();
78
+ writeJson(PATHS.profile, profile);
79
+ return {
80
+ content: [
81
+ {
82
+ type: "text",
83
+ text: JSON.stringify(profile, null, 2),
84
+ },
85
+ ],
86
+ };
87
+ });
88
+ }
89
+ //# sourceMappingURL=profile.js.map
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerSessionTools(server: McpServer): void;
3
+ //# sourceMappingURL=sessions.d.ts.map
@@ -0,0 +1,156 @@
1
+ import { z } from "zod";
2
+ import { join } from "node:path";
3
+ import { PATHS } from "../storage/paths.js";
4
+ import { readJson, writeJson, deleteJson } from "../storage/store.js";
5
+ function archiveCheckpoint(checkpoint) {
6
+ const safeName = checkpoint.timestamp.replace(/[:.]/g, "-");
7
+ const archivePath = join(PATHS.sessionHistory, `session_${safeName}.json`);
8
+ writeJson(archivePath, checkpoint);
9
+ }
10
+ export function registerSessionTools(server) {
11
+ server.registerTool("session_checkpoint", {
12
+ description: "Save current session state. Call this every 3-5 tool calls to protect against UI crashes. Required: task, intent, next_steps",
13
+ inputSchema: {
14
+ task: z.string().describe("Brief task name"),
15
+ intent: z.string().describe("What you are trying to accomplish"),
16
+ next_steps: z.array(z.string()).describe("Planned next steps"),
17
+ status: z
18
+ .enum(["IN_PROGRESS", "BLOCKED", "WAITING_USER"])
19
+ .optional()
20
+ .describe("Current status"),
21
+ files_touched: z
22
+ .array(z.string())
23
+ .optional()
24
+ .describe("File paths modified"),
25
+ recent_actions: z
26
+ .array(z.string())
27
+ .optional()
28
+ .describe("Last 3-5 actions"),
29
+ context: z
30
+ .record(z.unknown())
31
+ .optional()
32
+ .describe("Any important state/variables"),
33
+ tool_call_count: z
34
+ .number()
35
+ .optional()
36
+ .describe("Approx tool calls so far"),
37
+ },
38
+ }, async (params) => {
39
+ const checkpoint = {
40
+ timestamp: new Date().toISOString(),
41
+ task: params.task,
42
+ intent: params.intent,
43
+ status: params.status ?? "IN_PROGRESS",
44
+ files_touched: params.files_touched ?? [],
45
+ recent_actions: params.recent_actions ?? [],
46
+ next_steps: params.next_steps,
47
+ context: params.context ?? {},
48
+ tool_call_count: params.tool_call_count ?? 0,
49
+ };
50
+ writeJson(PATHS.activeSession, checkpoint);
51
+ return {
52
+ content: [
53
+ {
54
+ type: "text",
55
+ text: JSON.stringify({
56
+ success: true,
57
+ message: `Checkpoint saved at ${checkpoint.timestamp}`,
58
+ checkpoint_id: checkpoint.timestamp,
59
+ }, null, 2),
60
+ },
61
+ ],
62
+ };
63
+ });
64
+ server.registerTool("session_restore", {
65
+ description: "Check for interrupted work from a previous session. Call this FIRST at the start of any session.",
66
+ inputSchema: {},
67
+ }, async () => {
68
+ const checkpoint = readJson(PATHS.activeSession, null);
69
+ if (!checkpoint) {
70
+ return {
71
+ content: [
72
+ {
73
+ type: "text",
74
+ text: JSON.stringify({
75
+ has_active_session: false,
76
+ message: "No active session found. Starting fresh.",
77
+ }, null, 2),
78
+ },
79
+ ],
80
+ };
81
+ }
82
+ const ageHours = (Date.now() - new Date(checkpoint.timestamp).getTime()) /
83
+ (1000 * 60 * 60);
84
+ if (ageHours > 24) {
85
+ archiveCheckpoint(checkpoint);
86
+ deleteJson(PATHS.activeSession);
87
+ return {
88
+ content: [
89
+ {
90
+ type: "text",
91
+ text: JSON.stringify({
92
+ has_active_session: false,
93
+ message: `Found stale session from ${checkpoint.timestamp} (${Math.round(ageHours)}h ago). Archived it. Starting fresh.`,
94
+ archived: true,
95
+ }, null, 2),
96
+ },
97
+ ],
98
+ };
99
+ }
100
+ const ageMinutes = Math.round(ageHours * 60);
101
+ return {
102
+ content: [
103
+ {
104
+ type: "text",
105
+ text: JSON.stringify({
106
+ has_active_session: true,
107
+ ...checkpoint,
108
+ age_minutes: ageMinutes,
109
+ message: `Found active session: "${checkpoint.task}" (${ageMinutes}m ago, ${checkpoint.tool_call_count} tool calls)`,
110
+ }, null, 2),
111
+ },
112
+ ],
113
+ };
114
+ });
115
+ server.registerTool("session_complete", {
116
+ description: "Mark the current session as complete. Call this when a task is finished.",
117
+ inputSchema: {
118
+ summary: z
119
+ .string()
120
+ .optional()
121
+ .describe("Brief summary of what was accomplished"),
122
+ },
123
+ }, async (params) => {
124
+ const checkpoint = readJson(PATHS.activeSession, null);
125
+ if (!checkpoint) {
126
+ return {
127
+ content: [
128
+ {
129
+ type: "text",
130
+ text: JSON.stringify({
131
+ success: false,
132
+ message: "No active checkpoint to complete",
133
+ }, null, 2),
134
+ },
135
+ ],
136
+ };
137
+ }
138
+ checkpoint.status = "COMPLETED";
139
+ checkpoint.completed_at = new Date().toISOString();
140
+ checkpoint.summary = params.summary ?? "Task completed";
141
+ archiveCheckpoint(checkpoint);
142
+ deleteJson(PATHS.activeSession);
143
+ return {
144
+ content: [
145
+ {
146
+ type: "text",
147
+ text: JSON.stringify({
148
+ success: true,
149
+ message: `Session completed and archived: "${checkpoint.task}"`,
150
+ }, null, 2),
151
+ },
152
+ ],
153
+ };
154
+ });
155
+ }
156
+ //# sourceMappingURL=sessions.js.map
@@ -0,0 +1,76 @@
1
+ export interface UserPreferences {
2
+ communication_style: string;
3
+ emoji_usage: string;
4
+ technical_level: string;
5
+ verbosity: string;
6
+ [key: string]: string;
7
+ }
8
+ export interface UserNote {
9
+ content: string;
10
+ timestamp: string;
11
+ }
12
+ export interface UserProfile {
13
+ name: string | null;
14
+ preferences: UserPreferences;
15
+ projects: string[];
16
+ notes: UserNote[];
17
+ created_at: string;
18
+ updated_at: string;
19
+ }
20
+ export interface JournalEntry {
21
+ timestamp: string;
22
+ session_summary: string;
23
+ key_moments: string[];
24
+ emotional_context: string | null;
25
+ breakthroughs: string[];
26
+ frustrations: string[];
27
+ collaboration_notes: string | null;
28
+ }
29
+ export interface JournalData {
30
+ sessions: JournalEntry[];
31
+ }
32
+ export interface DecisionEntry {
33
+ timestamp: string;
34
+ choice: string;
35
+ alternatives: string[];
36
+ reasoning: string;
37
+ outcome: string | null;
38
+ project: string | null;
39
+ tags: string[];
40
+ }
41
+ export interface DecisionsData {
42
+ decisions: DecisionEntry[];
43
+ }
44
+ export interface DeadEndEntry {
45
+ timestamp: string;
46
+ attempted: string;
47
+ why_failed: string;
48
+ lesson: string;
49
+ project: string | null;
50
+ files_involved: string[];
51
+ tags: string[];
52
+ }
53
+ export interface DeadEndsData {
54
+ dead_ends: DeadEndEntry[];
55
+ }
56
+ export interface SessionCheckpoint {
57
+ timestamp: string;
58
+ task: string;
59
+ intent: string;
60
+ status: string;
61
+ files_touched: string[];
62
+ recent_actions: string[];
63
+ next_steps: string[];
64
+ context: Record<string, unknown>;
65
+ tool_call_count: number;
66
+ completed_at?: string;
67
+ summary?: string;
68
+ }
69
+ export interface FullContext {
70
+ user_profile: UserProfile;
71
+ recent_sessions: JournalEntry[];
72
+ recent_decisions: DecisionEntry[];
73
+ recent_dead_ends: DeadEndEntry[];
74
+ retrieved_at: string;
75
+ }
76
+ //# sourceMappingURL=types.d.ts.map
package/dist/types.js ADDED
@@ -0,0 +1,3 @@
1
+ // ---- Profile ----
2
+ export {};
3
+ //# sourceMappingURL=types.js.map
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "session-forge",
3
+ "version": "1.0.0",
4
+ "description": "Session intelligence for AI coding assistants. Persistent memory, decisions, dead ends, crash recovery. Zero infrastructure.",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "bin": {
9
+ "session-forge": "dist/index.js"
10
+ },
11
+ "scripts": {
12
+ "build": "tsc",
13
+ "dev": "tsc --watch",
14
+ "start": "node dist/index.js",
15
+ "prepublishOnly": "npm run build"
16
+ },
17
+ "keywords": [
18
+ "mcp",
19
+ "model-context-protocol",
20
+ "claude",
21
+ "cursor",
22
+ "windsurf",
23
+ "ai-memory",
24
+ "session-management",
25
+ "dead-ends",
26
+ "decisions",
27
+ "coding-assistant",
28
+ "developer-tools"
29
+ ],
30
+ "author": "Jacob Terrell",
31
+ "license": "MIT",
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "https://github.com/jacobterrell/session-forge"
35
+ },
36
+ "homepage": "https://github.com/jacobterrell/session-forge#readme",
37
+ "bugs": {
38
+ "url": "https://github.com/jacobterrell/session-forge/issues"
39
+ },
40
+ "engines": {
41
+ "node": ">=18.0.0"
42
+ },
43
+ "files": [
44
+ "dist/**/*.js",
45
+ "dist/**/*.d.ts",
46
+ "README.md",
47
+ "LICENSE"
48
+ ],
49
+ "dependencies": {
50
+ "@modelcontextprotocol/sdk": "^1.26.0",
51
+ "zod": "^3.25.0"
52
+ },
53
+ "devDependencies": {
54
+ "@types/node": "^25.2.1"
55
+ }
56
+ }