session-continuity 0.1.3
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 +116 -0
- package/README.md +175 -0
- package/dist/cli.js +81 -0
- package/dist/server.js +8 -0
- package/package.json +41 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
PolyForm Noncommercial License 1.0.0
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Waycraft (https://github.com/100615056)
|
|
4
|
+
|
|
5
|
+
This software is provided under the PolyForm Noncommercial License 1.0.0.
|
|
6
|
+
Full license text: https://polyformproject.org/licenses/noncommercial/1.0.0
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
Acceptance
|
|
11
|
+
|
|
12
|
+
In order to get any license under these terms, you must agree to them as both
|
|
13
|
+
strict obligations and conditions to all your licenses.
|
|
14
|
+
|
|
15
|
+
Copyright License
|
|
16
|
+
|
|
17
|
+
The licensor grants you a copyright license for the software to do everything
|
|
18
|
+
you might do with the software that would otherwise infringe the licensor's
|
|
19
|
+
copyright in it for any permitted purpose. However, you may only distribute
|
|
20
|
+
the software according to Distribution License and make changes or new works
|
|
21
|
+
based on the software according to Changes and New Works License.
|
|
22
|
+
|
|
23
|
+
Distribution License
|
|
24
|
+
|
|
25
|
+
The licensor grants you an additional copyright license to distribute copies
|
|
26
|
+
of the software. Your license to distribute covers distributing the software
|
|
27
|
+
with changes and new works permitted by Changes and New Works License.
|
|
28
|
+
|
|
29
|
+
Notices
|
|
30
|
+
|
|
31
|
+
You must ensure that anyone who gets a copy of any part of the software from
|
|
32
|
+
you also gets a copy of these terms or the URL for them above, as well as
|
|
33
|
+
copies of any plain-text lines beginning with Required Notice: that the
|
|
34
|
+
licensor provided with the software. For example:
|
|
35
|
+
|
|
36
|
+
Required Notice: Copyright Waycraft (https://github.com/100615056)
|
|
37
|
+
|
|
38
|
+
Changes and New Works License
|
|
39
|
+
|
|
40
|
+
The licensor grants you an additional copyright license to make changes and
|
|
41
|
+
new works based on the software for any permitted purpose.
|
|
42
|
+
|
|
43
|
+
Patent License
|
|
44
|
+
|
|
45
|
+
The licensor grants you a patent license for the software that covers patent
|
|
46
|
+
claims the licensor can license, or becomes able to license, that you would
|
|
47
|
+
infringe by using the software.
|
|
48
|
+
|
|
49
|
+
Noncommercial Purposes
|
|
50
|
+
|
|
51
|
+
Any noncommercial purpose is a permitted purpose.
|
|
52
|
+
|
|
53
|
+
Personal Uses
|
|
54
|
+
|
|
55
|
+
Personal use for research, experiment, and testing for the benefit of public
|
|
56
|
+
knowledge, personal study, private entertainment, hobby projects, amateur
|
|
57
|
+
pursuits, or religious observance, without any anticipated commercial
|
|
58
|
+
application, is use for a noncommercial purpose.
|
|
59
|
+
|
|
60
|
+
Noncommercial Organizations
|
|
61
|
+
|
|
62
|
+
Use by any charitable organization, educational institution, public research
|
|
63
|
+
organization, public safety or health organization, environmental protection
|
|
64
|
+
organization, or government institution is use for a noncommercial purpose
|
|
65
|
+
regardless of the source of funding or obligations resulting from the funding.
|
|
66
|
+
|
|
67
|
+
Fair Use
|
|
68
|
+
|
|
69
|
+
You may have "fair use" rights for the software under the law. These terms do
|
|
70
|
+
not limit them.
|
|
71
|
+
|
|
72
|
+
No Other Rights
|
|
73
|
+
|
|
74
|
+
These terms do not allow you to sublicense or transfer any of your licenses
|
|
75
|
+
to anyone else, or prevent the licensor from granting licenses to anyone else.
|
|
76
|
+
These terms do not imply any other licenses.
|
|
77
|
+
|
|
78
|
+
Patent Defense
|
|
79
|
+
|
|
80
|
+
If you make any written claim that the software infringes or contributes to
|
|
81
|
+
infringement of any patent, your patent license for the software granted under
|
|
82
|
+
these terms ends immediately. If your employer makes such a claim, your patent
|
|
83
|
+
license ends immediately for work on behalf of your employer.
|
|
84
|
+
|
|
85
|
+
Violations
|
|
86
|
+
|
|
87
|
+
The first time you are notified in writing that you have violated any of these
|
|
88
|
+
terms, or done anything with the software not covered by your licenses, your
|
|
89
|
+
licenses can nonetheless continue if you come into full compliance with these
|
|
90
|
+
terms, and take practical steps to correct past violations, within 32 days of
|
|
91
|
+
receiving notice. Otherwise, all your licenses end immediately.
|
|
92
|
+
|
|
93
|
+
No Liability
|
|
94
|
+
|
|
95
|
+
As far as the law allows, the software comes as is, without any warranty or
|
|
96
|
+
condition, and the licensor will not be liable to you for any damages arising
|
|
97
|
+
out of these terms or the use or nature of the software, under any kind of
|
|
98
|
+
legal claim.
|
|
99
|
+
|
|
100
|
+
Definitions
|
|
101
|
+
|
|
102
|
+
The licensor is the entity offering these terms, and the software is the
|
|
103
|
+
software the licensor makes available under these terms.
|
|
104
|
+
|
|
105
|
+
You refers to the individual or entity agreeing to these terms.
|
|
106
|
+
|
|
107
|
+
Your company is any legal entity, sole proprietorship, or other kind of
|
|
108
|
+
organization that you work for, plus all organizations that have control over,
|
|
109
|
+
are under the control of, or are under common control with that organization.
|
|
110
|
+
|
|
111
|
+
Your licenses are all the licenses granted to you for the software under
|
|
112
|
+
these terms.
|
|
113
|
+
|
|
114
|
+
Use means anything you do with the software requiring one of your licenses.
|
|
115
|
+
|
|
116
|
+
Trademark means trademarks, service marks, and similar rights.
|
package/README.md
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# sc — session continuity for Claude Code
|
|
2
|
+
|
|
3
|
+
Pick up exactly where you left off across Claude sessions. No re-explaining context, no copy-pasting summaries — just open a new session and keep working.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## How it works
|
|
8
|
+
|
|
9
|
+
When a Claude session ends, `sc` automatically writes a snapshot to `.claude/session.md`:
|
|
10
|
+
- **Git state** — branch, status, recent commits, diff stat
|
|
11
|
+
- **Narrative** — what was being worked on, decisions made, next steps, blockers (generated by `claude -p`)
|
|
12
|
+
|
|
13
|
+
When a new session starts, Claude reads `session.md` automatically via a `CLAUDE.md` import — no action needed.
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
Session ends → Stop hook → sc snapshot → .claude/session.md
|
|
17
|
+
New session starts → CLAUDE.md @import → full context loaded ✓
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Install
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm install -g session-continuity
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
> Requires Node.js ≥ 18 and the [Claude Code CLI](https://docs.anthropic.com/en/docs/claude-code).
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Quickstart
|
|
33
|
+
|
|
34
|
+
**One-time setup per project:**
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
cd your-project
|
|
38
|
+
sc init
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
That's it. `sc init` does three things:
|
|
42
|
+
1. Creates `.claude/session.md` (the snapshot file)
|
|
43
|
+
2. Adds `@.claude/session.md` to `CLAUDE.md` so Claude reads it on every session start
|
|
44
|
+
3. Registers a `Stop` hook in `.claude/settings.json` so `sc snapshot` runs automatically when a session ends
|
|
45
|
+
|
|
46
|
+
**From then on, every session:**
|
|
47
|
+
- Ends by writing a snapshot automatically
|
|
48
|
+
- Starts with full context from the last snapshot
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Commands
|
|
53
|
+
|
|
54
|
+
| Command | Description |
|
|
55
|
+
|---|---|
|
|
56
|
+
| `sc init` | Wire up a project (run once) |
|
|
57
|
+
| `sc snapshot` | Write a snapshot now (called automatically by the Stop hook) |
|
|
58
|
+
| `sc status` | Show the current session state |
|
|
59
|
+
| `sc rotate` | Manually trigger a snapshot — use this before force-quitting Claude |
|
|
60
|
+
| `sc clear` | Reset session history |
|
|
61
|
+
| `sc decide "<why>"` | Pin a permanent decision that survives the rolling session window |
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Snapshot format
|
|
66
|
+
|
|
67
|
+
Each snapshot is a dated markdown block. Up to 3 sessions are kept (newest first); older sessions roll off automatically.
|
|
68
|
+
|
|
69
|
+
```markdown
|
|
70
|
+
---
|
|
71
|
+
## Session — 2026-05-24T21:30:00Z | branch: feat/auth
|
|
72
|
+
|
|
73
|
+
**Git status:**
|
|
74
|
+
...
|
|
75
|
+
|
|
76
|
+
**Recent commits:**
|
|
77
|
+
...
|
|
78
|
+
|
|
79
|
+
**Narrative:**
|
|
80
|
+
**Status:** Implementing JWT refresh token flow — middleware done, route handler in progress.
|
|
81
|
+
|
|
82
|
+
**Decisions made:**
|
|
83
|
+
- Storing refresh tokens in httpOnly cookies (not localStorage) to prevent XSS
|
|
84
|
+
- Using a 15-minute access token TTL based on security requirements
|
|
85
|
+
|
|
86
|
+
**Next steps:**
|
|
87
|
+
- Wire up the /auth/refresh route
|
|
88
|
+
- Add token rotation on each refresh
|
|
89
|
+
|
|
90
|
+
**Blockers:** None
|
|
91
|
+
---
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## Pinning decisions
|
|
97
|
+
|
|
98
|
+
Use `sc decide` to record architectural choices, tradeoffs, or rejected ideas that should survive past the 3-session rolling window:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
sc decide "Chose Postgres over SQLite — need concurrent writes in production"
|
|
102
|
+
sc decide "Rejected Redis for session storage — adds ops complexity we don't need yet"
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Pinned decisions appear at the top of `session.md` under a `## Pinned decisions` block and are never trimmed.
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## .gitignore recommendation
|
|
110
|
+
|
|
111
|
+
Session state is personal — add this to your project's `.gitignore`:
|
|
112
|
+
|
|
113
|
+
```
|
|
114
|
+
.claude/session.md
|
|
115
|
+
.claude/session.md.tmp
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Do commit `.claude/settings.json` (it contains the Stop hook config other contributors need).
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## Troubleshooting
|
|
123
|
+
|
|
124
|
+
**Narrative says `[unavailable]`**
|
|
125
|
+
|
|
126
|
+
The `claude` CLI is not in your PATH. Check with `claude --version`. If not installed, follow the [Claude Code setup guide](https://docs.anthropic.com/en/docs/claude-code). Without it, `sc snapshot` still writes objective git state — just no AI-generated narrative.
|
|
127
|
+
|
|
128
|
+
**Context not loading in new sessions**
|
|
129
|
+
|
|
130
|
+
Check that `CLAUDE.md` contains `@.claude/session.md`. If the line is missing, re-run `sc init` — it's safe to run multiple times.
|
|
131
|
+
|
|
132
|
+
**Hook not firing**
|
|
133
|
+
|
|
134
|
+
Verify `.claude/settings.json` has a `Stop` hook pointing to `sc snapshot`:
|
|
135
|
+
|
|
136
|
+
```json
|
|
137
|
+
{
|
|
138
|
+
"hooks": {
|
|
139
|
+
"Stop": [{ "hooks": [{ "type": "command", "command": "sc snapshot" }] }]
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
If it's missing, run `sc init` again. Also confirm `sc` is in your PATH (`which sc`).
|
|
145
|
+
|
|
146
|
+
**Session ended without a snapshot (crash / force-quit)**
|
|
147
|
+
|
|
148
|
+
Run `sc rotate` manually to write a snapshot from the current git state. To avoid this in future, run `sc rotate` before closing Claude if you know you'll be force-quitting.
|
|
149
|
+
|
|
150
|
+
**`sc init` breaks my existing hooks**
|
|
151
|
+
|
|
152
|
+
It won't — `sc init` merges into `.claude/settings.json` rather than overwriting it. Your existing hooks are preserved.
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## How sessions are bounded
|
|
157
|
+
|
|
158
|
+
- **Rolling window:** 3 sessions kept, newest first. Older sessions roll off.
|
|
159
|
+
- **Per-session token cap:** ~200 tokens per session entry (~600 tokens total). Well within Claude's context budget.
|
|
160
|
+
- **Pinned decisions:** Never trimmed — use `sc decide` for anything you want to keep long-term.
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## Roadmap
|
|
165
|
+
|
|
166
|
+
- `PostToolUse` incremental checkpointing (crash safety without `sc rotate`)
|
|
167
|
+
- `sc history` — list past session headers
|
|
168
|
+
- `sc diff` — show git diff since last snapshot
|
|
169
|
+
- Configurable rolling window size
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## License
|
|
174
|
+
|
|
175
|
+
MIT
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
#!/usr/bin/env node
|
|
3
|
+
import{existsSync as R,readFileSync as z,writeFileSync as k}from"fs";import{readFileSync as B,writeFileSync as I,existsSync as D,mkdirSync as H,renameSync as G}from"fs";import{execSync as J}from"child_process";import{join as v}from"path";var p=".claude",n=v(p,"session.md"),x=v(p,"settings.json"),h="CLAUDE.md",w="@.claude/session.md",N=3,_=150,u=`<!-- sc: no previous session recorded -->
|
|
4
|
+
`;function S(e){D(e)||H(e,{recursive:!0})}function L(e,o={}){if(!D(e))return o;try{return JSON.parse(B(e,"utf8"))}catch{return o}}function O(e,o){I(e,JSON.stringify(o,null,2)+`
|
|
5
|
+
`,"utf8")}function m(e,o){let t=e+".tmp";I(t,o,"utf8"),G(t,e)}function f(e,o=""){try{return J(`git ${e}`,{encoding:"utf8",stdio:["pipe","pipe","pipe"]}).trim()}catch{return o}}function C(e,o){let t=e.split(/\s+/).filter(Boolean);return t.length<=o?e:t.slice(0,o).join(" ")+" \u2026[trimmed]"}var q="## Pinned decisions";function $(e){if(!e.includes(q))return{pinned:"",rest:e};let o=e.indexOf(`
|
|
6
|
+
---
|
|
7
|
+
`);return o===-1?{pinned:e.trim(),rest:""}:{pinned:e.slice(0,o).trim(),rest:e.slice(o)}}function b(e){return e.split(/^---$/m).map(o=>o.trim()).filter(o=>o&&!o.startsWith("<!--"))}function A(e){return e.map(o=>`---
|
|
8
|
+
${o}
|
|
9
|
+
`).join(`
|
|
10
|
+
`)+`
|
|
11
|
+
`}async function P(){let e=[];S(p),e.push("\u2713 .claude/ directory ready"),R(n)?e.push("\xB7 .claude/session.md already exists"):(k(n,u,"utf8"),e.push("\u2713 .claude/session.md created"));let o=R(h),t=o?z(h,"utf8"):"";t.includes(w)?e.push("\xB7 CLAUDE.md already has @import"):(t=`
|
|
12
|
+
<!-- sc: session continuity -->
|
|
13
|
+
${w}
|
|
14
|
+
`+t,k(h,t,"utf8"),e.push(`\u2713 CLAUDE.md ${o?"updated":"created"} with @import`));let s=L(x,{}),d={type:"command",command:"sc snapshot"};s.hooks||(s.hooks={}),s.hooks.Stop||(s.hooks.Stop=[]),s.hooks.Stop.some(r=>Array.isArray(r.hooks)&&r.hooks.some(a=>a.command&&a.command.includes("sc snapshot")))?e.push("\xB7 Stop hook already registered"):(s.hooks.Stop.push({hooks:[d]}),O(x,s),e.push("\u2713 Stop hook registered in .claude/settings.json"));let c=!1;try{let{execSync:r}=await import("child_process");r("claude --version",{stdio:"pipe"}),c=!0,e.push("\u2713 claude CLI found \u2014 narrative snapshots enabled")}catch{e.push("\u26A0 claude CLI not found \u2014 snapshots will be objective-only (git state, no narrative)")}console.log(`
|
|
15
|
+
sc init complete
|
|
16
|
+
`),e.forEach(r=>console.log(" ",r)),console.log(`
|
|
17
|
+
How it works:`),console.log(" \u2022 When a Claude session ends, sc snapshot runs automatically"),console.log(" \u2022 The snapshot is saved to .claude/session.md"),console.log(" \u2022 CLAUDE.md imports it, so every new session starts with full context"),c||(console.log(`
|
|
18
|
+
To enable narrative summaries, install the Claude CLI:`),console.log(" https://docs.anthropic.com/en/docs/claude-code")),console.log()}import{spawnSync as X}from"child_process";import{existsSync as Y,readFileSync as V}from"fs";var K=`You are summarizing a Claude Code development session for the developer who just finished it.
|
|
19
|
+
Given the git state below, write a concise session snapshot in this exact format (no other text):
|
|
20
|
+
|
|
21
|
+
**Status:** <one sentence \u2014 what was being worked on and where things stand>
|
|
22
|
+
|
|
23
|
+
**Decisions made:**
|
|
24
|
+
- <decision 1 \u2014 include the "why" if inferable from context>
|
|
25
|
+
- <decision 2>
|
|
26
|
+
|
|
27
|
+
**Next steps:**
|
|
28
|
+
- <most important thing to do next>
|
|
29
|
+
- <second thing if applicable>
|
|
30
|
+
|
|
31
|
+
**Blockers:** <"None" or a brief description>
|
|
32
|
+
|
|
33
|
+
Rules:
|
|
34
|
+
- Total response must be under 120 words
|
|
35
|
+
- Be specific \u2014 name files, functions, or commands if relevant
|
|
36
|
+
- If git state shows no meaningful changes, say "No significant changes this session"
|
|
37
|
+
- Do not add headings, preamble, or closing remarks \u2014 just the five fields above
|
|
38
|
+
|
|
39
|
+
Git state:
|
|
40
|
+
`;function Q(){let e=f("branch --show-current","unknown"),o=f("status --short","(no changes)"),t=f("log --oneline -5","(no commits)"),s=f("diff --stat HEAD","").split(`
|
|
41
|
+
`).slice(0,10).join(`
|
|
42
|
+
`);return{branch:e,status:o,log:t,diffStat:s}}function Z(e,o){let{branch:t,status:s,log:d,diffStat:i}=o;return[`## Session \u2014 ${e} | branch: ${t}`,"","**Git status:**","```",s||"(clean)","```","","**Recent commits:**","```",d||"(none)","```",i?`
|
|
43
|
+
**Diff stat:**
|
|
44
|
+
\`\`\`
|
|
45
|
+
${i}
|
|
46
|
+
\`\`\``:""].filter(c=>c!==void 0).join(`
|
|
47
|
+
`).trim()}function ee(e){let o=[`Branch: ${e.branch}`,`Status:
|
|
48
|
+
${e.status}`,`Recent commits:
|
|
49
|
+
${e.log}`,e.diffStat?`Diff stat:
|
|
50
|
+
${e.diffStat}`:""].filter(Boolean).join(`
|
|
51
|
+
|
|
52
|
+
`),t=K+o,s=X("claude",["-p",t],{encoding:"utf8",timeout:15e3,stdio:["pipe","pipe","pipe"]});return s.status!==0||s.error||!s.stdout.trim()?null:s.stdout.trim()}async function g(){S(p);let e=new Date().toISOString(),o=Q(),t=Z(e,o),s=ee(o);if(s){let E=C(s,_);t+=`
|
|
53
|
+
|
|
54
|
+
**Narrative:**
|
|
55
|
+
`+E}else t+="\n\n**Narrative:** [unavailable \u2014 run `claude --version` to enable]";let d=Y(n)?V(n,"utf8"):"",{pinned:i,rest:c}=$(d),r=b(c),a=[t,...r].slice(0,N),y=(i?i+`
|
|
56
|
+
`:"")+A(a);m(n,y),process.stderr.write(`[sc] snapshot written \u2014 ${e}
|
|
57
|
+
`)}import{existsSync as oe,readFileSync as te}from"fs";async function W(){if(!oe(n)){console.log("No session file found. Run `sc init` first.");return}let e=te(n,"utf8").trim();if(!e||e.startsWith("<!--")){console.log("No session recorded yet. End a Claude session to generate the first snapshot.");return}console.log(`
|
|
58
|
+
\u2500\u2500 Current session state (`+n+`) \u2500\u2500
|
|
59
|
+
`),console.log(e),console.log(`
|
|
60
|
+
\u2500\u2500 end \u2500\u2500
|
|
61
|
+
`)}import{existsSync as se}from"fs";import{createInterface as ne}from"readline";async function j(){if(!se(n)){console.log("Nothing to clear \u2014 .claude/session.md does not exist.");return}if(!await ie("Clear all session history? This cannot be undone. (y/N) ")){console.log("Aborted.");return}m(n,u),console.log("Session history cleared.")}function ie(e){return new Promise(o=>{let t=ne({input:process.stdin,output:process.stdout});t.question(e,s=>{t.close(),o(s.trim().toLowerCase()==="y")})})}async function F(){console.log("Writing snapshot\u2026"),await g(),console.log("Done. Run `sc status` to review.")}import{existsSync as re,readFileSync as ce}from"fs";var T="## Pinned decisions",ae="<!-- sc: pinned decisions survive the rolling session window -->";async function M([e]){(!e||!e.trim())&&(console.error('Usage: sc decide "Your decision or rationale here"'),process.exit(1));let o=e.trim(),t=`- ${o}`,s=new Date().toISOString(),d=`${t} _(${s})_`,i=re(n)?ce(n,"utf8"):u,c;if(i.includes(T))c=i.replace(/(## Pinned decisions\n(?:<!--[^>]*-->\n)?)([\s\S]*?)(\n---|\n<!-- sc:|$)/,(r,a,y,E)=>`${a}${y}
|
|
62
|
+
${d}${E}`);else{let r=`${T}
|
|
63
|
+
${ae}
|
|
64
|
+
${d}
|
|
65
|
+
|
|
66
|
+
`,a=i.indexOf(`---
|
|
67
|
+
`);a===-1||i.trim()===u.trim()?c=r+(i.startsWith("<!--")?"":i):c=i.slice(0,a)+r+i.slice(a)}m(n,c),console.log(`Decision pinned: "${o}"`)}var[,,l,...le]=process.argv,U={init:P,snapshot:g,status:W,clear:j,rotate:F,decide:M};(!l||l==="--help"||l==="-h")&&(console.log(`
|
|
68
|
+
sc \u2014 session continuity for Claude Code
|
|
69
|
+
|
|
70
|
+
Commands:
|
|
71
|
+
sc init Wire up this project (run once per project)
|
|
72
|
+
sc snapshot Write a session snapshot (called automatically by Stop hook)
|
|
73
|
+
sc status Show the current session state
|
|
74
|
+
sc clear Reset session history
|
|
75
|
+
sc rotate Manually trigger a snapshot write (use before force-quit)
|
|
76
|
+
sc decide "<why>" Pin a permanent decision that survives the rolling window
|
|
77
|
+
|
|
78
|
+
Options:
|
|
79
|
+
--version, -v Print version
|
|
80
|
+
--help, -h Show this help
|
|
81
|
+
`),process.exit(0));if(l==="--version"||l==="-v"){let{createRequire:e}=await import("module"),t=e(import.meta.url)("../package.json");console.log(t.version),process.exit(0)}U[l]||(console.error(`Unknown command: ${l}. Run 'sc --help' for usage.`),process.exit(1));try{await U[l](le)}catch(e){console.error(`sc ${l} failed:`,e.message),process.exit(1)}
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
#!/usr/bin/env node
|
|
3
|
+
import{McpServer as C}from"@modelcontextprotocol/sdk/server/mcp.js";import{StdioServerTransport as L}from"@modelcontextprotocol/sdk/server/stdio.js";import{z as r}from"zod";import{existsSync as x,readFileSync as y,readdirSync as N,mkdirSync as O}from"fs";import{join as d,basename as S}from"path";import{homedir as k}from"os";import{createHash as I}from"crypto";import{readFileSync as M,writeFileSync as j,existsSync as W,mkdirSync as F,renameSync as v}from"fs";import{join as h}from"path";var l=".claude",U=h(l,"session.md"),X=h(l,"settings.json");var m=3;function f(e,s){let t=e+".tmp";j(t,s,"utf8"),v(t,e)}var c=d(k(),".sc","sessions");function g(){x(c)||O(c,{recursive:!0})}function A(e){return I("sha1").update(e).digest("hex").slice(0,12)}function w(e){return d(c,`${A(e)}.json`)}function u(e){let s=w(e);if(!x(s))return{path:e,name:S(e),pinned:[],sessions:[]};try{return JSON.parse(y(s,"utf8"))}catch{return{path:e,name:S(e),pinned:[],sessions:[]}}}function b(e,s){g(),f(w(e),JSON.stringify(s,null,2)+`
|
|
4
|
+
`)}function $(e,s){let t=u(e);return t.sessions=[s,...t.sessions].slice(0,m),b(e,t),t}function _(e,s){let t=u(e);return t.pinned=t.pinned||[],t.pinned.push({text:s,timestamp:new Date().toISOString()}),b(e,t),t}function D(){return g(),N(c).filter(e=>e.endsWith(".json")).map(e=>{try{return JSON.parse(y(d(c,e),"utf8"))}catch{return null}}).filter(Boolean).sort((e,s)=>{let t=e.sessions[0]?.timestamp??"";return(s.sessions[0]?.timestamp??"").localeCompare(t)})}import{createRequire as P}from"module";var{version:R}=P(import.meta.url)("../package.json"),a=new C({name:"session-continuity",version:R});a.tool("load_session","Load the previous session context for a project. Call this at the start of every session to restore context without re-explanation.",{project_path:r.string().describe("Absolute path to the project root. Defaults to current working directory.").optional()},async({project_path:e})=>{let s=e||process.cwd(),t=u(s);if(t.sessions.length===0&&t.pinned.length===0)return{content:[{type:"text",text:`No previous session found for ${t.name}. This appears to be a fresh start.`}]};let n=[`# Session context \u2014 ${t.name}`,`**Project:** ${t.path}`,""];return t.pinned.length>0&&(n.push("## Pinned decisions"),t.pinned.forEach(o=>n.push(`- ${o.text} *(${o.timestamp})*`)),n.push("")),t.sessions.forEach((o,p)=>{n.push(`## ${p===0?"Last session":`Session ${p+1} ago`} \u2014 ${o.timestamp}`),n.push(`**Branch:** ${o.branch}`),n.push(`**Status:** ${o.status}`),o.decisions?.length&&(n.push("**Decisions made:**"),o.decisions.forEach(i=>n.push(`- ${i}`))),o.next_steps?.length&&(n.push("**Next steps:**"),o.next_steps.forEach(i=>n.push(`- ${i}`))),o.blockers&&n.push(`**Blockers:** ${o.blockers}`),n.push("")}),{content:[{type:"text",text:n.join(`
|
|
5
|
+
`)}]}});a.tool("save_session","Save the current session context. Call this at the end of a session or at any checkpoint. Claude should summarize what happened, decisions made, and what comes next.",{project_path:r.string().describe("Absolute path to the project root. Defaults to current working directory.").optional(),status:r.string().describe("One sentence: what was being worked on and where things stand."),decisions:r.array(r.string()).describe("Key decisions made this session, each with the reason why.").optional(),next_steps:r.array(r.string()).describe("Ordered list of what to do next session, most important first.").optional(),blockers:r.string().describe('Current blockers, or "None".').optional(),branch:r.string().describe("Current git branch.").optional()},async({project_path:e,status:s,decisions:t,next_steps:n,blockers:o,branch:p})=>{let i=e||process.cwd(),E={timestamp:new Date().toISOString(),branch:p||"unknown",status:s,decisions:t||[],next_steps:n||[],blockers:o||"None"};return $(i,E),{content:[{type:"text",text:`Session saved for ${i}.
|
|
6
|
+
Next session will start with: "${s}"`}]}});a.tool("pin_decision","Pin a permanent decision or architectural choice that should survive the rolling session window. Use for tradeoffs, rejected alternatives, and why-choices.",{project_path:r.string().describe("Absolute path to the project root. Defaults to current working directory.").optional(),decision:r.string().describe('The decision to record. Include the "why" \u2014 e.g. "Chose X over Y because Z."')},async({project_path:e,decision:s})=>{let t=e||process.cwd();return _(t,s),{content:[{type:"text",text:`Decision pinned: "${s}"`}]}});a.tool("list_projects","List all projects with tracked session history, sorted by most recently active.",{},async()=>{let e=D();if(e.length===0)return{content:[{type:"text",text:"No projects tracked yet. Save a session first."}]};let s=[`# Tracked projects
|
|
7
|
+
`];return e.forEach(t=>{let n=t.sessions[0];s.push(`**${t.name}**`),s.push(` Path: ${t.path}`),n&&(s.push(` Last session: ${n.timestamp}`),s.push(` Status: ${n.status}`)),t.pinned?.length&&s.push(` Pinned decisions: ${t.pinned.length}`),s.push("")}),{content:[{type:"text",text:s.join(`
|
|
8
|
+
`)}]}});var T=new L;await a.connect(T);
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "session-continuity",
|
|
3
|
+
"version": "0.1.3",
|
|
4
|
+
"description": "Pick up where you left off across Claude sessions — zero re-explanation.",
|
|
5
|
+
"bin": {
|
|
6
|
+
"sc": "./dist/cli.js",
|
|
7
|
+
"sc-mcp": "./dist/server.js"
|
|
8
|
+
},
|
|
9
|
+
"type": "module",
|
|
10
|
+
"files": [
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "node build.js",
|
|
15
|
+
"prepare": "node build.js",
|
|
16
|
+
"test": "node --test src/*.test.js"
|
|
17
|
+
},
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=18"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"mcp",
|
|
23
|
+
"claude",
|
|
24
|
+
"claude-code",
|
|
25
|
+
"session",
|
|
26
|
+
"context",
|
|
27
|
+
"memory",
|
|
28
|
+
"continuity",
|
|
29
|
+
"ai-native",
|
|
30
|
+
"workflow",
|
|
31
|
+
"developer-tools"
|
|
32
|
+
],
|
|
33
|
+
"license": "PolyForm-Noncommercial-1.0.0",
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
36
|
+
"zod": "^4.4.3"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"esbuild": "^0.28.0"
|
|
40
|
+
}
|
|
41
|
+
}
|