ctxsaver 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.
Files changed (48) hide show
  1. package/README.md +231 -0
  2. package/dist/commands/compress.d.ts +3 -0
  3. package/dist/commands/compress.js +97 -0
  4. package/dist/commands/config-cmd.d.ts +1 -0
  5. package/dist/commands/config-cmd.js +85 -0
  6. package/dist/commands/diff.d.ts +1 -0
  7. package/dist/commands/diff.js +97 -0
  8. package/dist/commands/handoff.d.ts +1 -0
  9. package/dist/commands/handoff.js +151 -0
  10. package/dist/commands/hook.d.ts +1 -0
  11. package/dist/commands/hook.js +60 -0
  12. package/dist/commands/init.d.ts +1 -0
  13. package/dist/commands/init.js +49 -0
  14. package/dist/commands/log.d.ts +4 -0
  15. package/dist/commands/log.js +53 -0
  16. package/dist/commands/resume.d.ts +4 -0
  17. package/dist/commands/resume.js +44 -0
  18. package/dist/commands/save.d.ts +13 -0
  19. package/dist/commands/save.js +160 -0
  20. package/dist/commands/share.d.ts +3 -0
  21. package/dist/commands/share.js +58 -0
  22. package/dist/commands/suggest.d.ts +1 -0
  23. package/dist/commands/suggest.js +67 -0
  24. package/dist/commands/summarize.d.ts +3 -0
  25. package/dist/commands/summarize.js +203 -0
  26. package/dist/commands/watch.d.ts +3 -0
  27. package/dist/commands/watch.js +99 -0
  28. package/dist/core/ai.d.ts +17 -0
  29. package/dist/core/ai.js +55 -0
  30. package/dist/core/context.d.ts +16 -0
  31. package/dist/core/context.js +88 -0
  32. package/dist/core/git.d.ts +7 -0
  33. package/dist/core/git.js +47 -0
  34. package/dist/core/parser.d.ts +15 -0
  35. package/dist/core/parser.js +666 -0
  36. package/dist/core/parser.test.d.ts +1 -0
  37. package/dist/core/parser.test.js +99 -0
  38. package/dist/core/prompt.d.ts +2 -0
  39. package/dist/core/prompt.js +75 -0
  40. package/dist/core/types.d.ts +24 -0
  41. package/dist/core/types.js +2 -0
  42. package/dist/index.d.ts +2 -0
  43. package/dist/index.js +92 -0
  44. package/dist/utils/clipboard.d.ts +5 -0
  45. package/dist/utils/clipboard.js +23 -0
  46. package/dist/utils/config.d.ts +32 -0
  47. package/dist/utils/config.js +49 -0
  48. package/package.json +54 -0
package/README.md ADDED
@@ -0,0 +1,231 @@
1
+ # CtxSaver 🧠
2
+
3
+ > *"Git tracks your code history. CtxSaver tracks your intent history."*
4
+
5
+ **Persistent AI coding context for teams.** Never re-explain your codebase to an AI assistant again.
6
+
7
+ ## The Problem
8
+
9
+ You're deep in a Cursor session refactoring a payment service. You've explained the architecture, tried 3 approaches, finally found the right one. Session dies. Next morning — or worse, your teammate picks it up — and the AI has **zero memory**. You spend 15 min re-explaining everything. Every. Single. Time.
10
+
11
+ This is broken across **every** AI coding tool: Cursor, Claude Code, Copilot, Windsurf — none of them persist context across sessions, editors, or team members.
12
+
13
+ ## The Solution
14
+
15
+ **CtxSaver** is a CLI tool that automatically captures and restores AI coding context, scoped to your repo and branch.
16
+
17
+ ```bash
18
+ # Save context after a session
19
+ ctxsaver save "Refactoring payment service to use event sourcing"
20
+
21
+ # Restore context in any editor, any machine
22
+ ctxsaver resume
23
+ ```
24
+
25
+ ## Install
26
+
27
+ ### Prerequisites
28
+ - Node.js 16+ and npm
29
+
30
+ ### Setup
31
+ > **Note:** Not yet published to npm. Install locally from source:
32
+
33
+ ```bash
34
+ git clone git@github.com:nickiiitu/CtxSaver.git
35
+ cd CtxSaver
36
+ npm install
37
+ npm run build
38
+ npm link # makes `ctxsaver` available globally on your machine
39
+
40
+ # Verify installation
41
+ ctxsaver --version
42
+ ```
43
+
44
+ Now **navigate to your project repo** to use CtxSaver:
45
+
46
+ ```bash
47
+ cd /path/to/your/project
48
+ ctxsaver init
49
+ ```
50
+
51
+ ## Quick Start
52
+
53
+ ```bash
54
+ # 1. Initialize in your repo
55
+ ctxsaver init
56
+
57
+ # 2. Work on your code... then save context
58
+ ctxsaver save
59
+ # → Interactive prompts capture: Task, Approaches, Decisions, Next Steps
60
+
61
+ # 3. Resume in ANY editor
62
+ ctxsaver resume
63
+ # → Copies a perfectly formatted prompt to your clipboard
64
+ # → Paste into Cursor, Claude, or ChatGPT to restore full context
65
+ ```
66
+
67
+ ## Features & Commands
68
+
69
+ ### Core (No AI Key Required)
70
+ **These commands work locally with zero dependencies.**
71
+ | Command | Description |
72
+ |---------|-------------|
73
+ | `ctxsaver init` | Initialize CtxSaver in current repo |
74
+ | `ctxsaver save [msg]` | Save context (interactive or quick mode) |
75
+ | `ctxsaver save --auto` | Auto-save context from agent/editor logs (non-interactive) |
76
+ | `ctxsaver resume` | Generate AI prompt & copy to clipboard |
77
+ | `ctxsaver log` | View context history for current branch |
78
+ | `ctxsaver diff` | Show changes since last context save |
79
+
80
+ ### Team & Automation (No AI Key Required)
81
+ | Command | Description |
82
+ |---------|-------------|
83
+ | `ctxsaver handoff @user` | explicit handoff note to a teammate |
84
+ | `ctxsaver share` | Commit `.ctxsaver/` folder to git for team sync |
85
+ | `ctxsaver watch` | Auto-save context on file changes (using `chokidar`) |
86
+ | `ctxsaver hook install` | Install git post-commit hook for auto-capture |
87
+
88
+ ### AI-Powered (Experimental)
89
+ **Requires an LLM Provider.** Set via `CTXSAVER_AI_KEY` env var or `ctxsaver config set aiApiKey <key>`. Defaults to OpenAI-compatible API.
90
+ | Command | Description |
91
+ |---------|-------------|
92
+ | `ctxsaver summarize` | AI-generates context from git diff + recent commits |
93
+ | `ctxsaver suggest` | AI suggests next steps based on current context |
94
+ | `ctxsaver compress` | detailed history into a concise summary |
95
+
96
+ #### AI Commands Setup
97
+ To use `ctxsaver summarize`, `suggest`, or `compress`, configure your AI provider:
98
+
99
+ **Quick Setup for OpenAI (default):**
100
+ ```bash
101
+ ctxsaver config set aiApiKey "your-openai-api-key"
102
+ ctxsaver config set aiModel "gpt-4o-mini"
103
+ # Provider defaults to https://api.openai.com/v1
104
+ ```
105
+
106
+ **Quick Setup for Google Gemini (via OpenRouter):**
107
+ ```bash
108
+ ctxsaver config set aiProvider "https://openrouter.ai/api/v1"
109
+ ctxsaver config set aiApiKey "your-openrouter-api-key"
110
+ ctxsaver config set aiModel "google/gemini-flash-1.5"
111
+ ```
112
+
113
+ **Quick Setup for ChatGLM (via OpenRouter):**
114
+ ```bash
115
+ ctxsaver config set aiProvider "https://openrouter.ai/api/v1"
116
+ ctxsaver config set aiApiKey "your-openrouter-api-key"
117
+ ctxsaver config set aiModel "zhipuai/glm-4"
118
+ ```
119
+
120
+ **Configuration Commands:**
121
+ ```bash
122
+ # View all settings
123
+ ctxsaver config list
124
+
125
+ # Get a specific config value
126
+ ctxsaver config get aiModel
127
+
128
+ # Set via environment variable (temporary)
129
+ export CTXSAVER_AI_KEY="your-api-key"
130
+ ```
131
+
132
+ **Supported Providers:**
133
+ - **OpenAI** (default): `https://api.openai.com/v1`
134
+ - **Google Gemini** (via OpenRouter): `https://openrouter.ai/api/v1`
135
+ - **ChatGLM** (via OpenRouter): `https://openrouter.ai/api/v1`
136
+ - **Ollama** (local): `http://localhost:11434/v1` (no API key needed)
137
+ - **LM Studio** (local): `http://localhost:1234/v1` (no API key needed)
138
+ - **Together.ai**: `https://api.together.xyz/v1`
139
+ - Any OpenAI-compatible API endpoint
140
+
141
+ **Example: Using Ollama locally**
142
+ ```bash
143
+ ctxsaver config set aiProvider "http://localhost:11434/v1"
144
+ ctxsaver config set aiModel "llama3"
145
+ # No API key needed for local models
146
+ ```
147
+
148
+ **Example: Using Google Gemini via OpenRouter**
149
+ ```bash
150
+ ctxsaver config set aiProvider "https://openrouter.ai/api/v1"
151
+ ctxsaver config set aiApiKey "your-openrouter-api-key"
152
+
153
+ # Try these Gemini model names:
154
+ ctxsaver config set aiModel "google/gemini-flash-1.5"
155
+ # or
156
+ ctxsaver config set aiModel "google/gemini-pro-1.5"
157
+ # or
158
+ ctxsaver config set aiModel "google/gemini-2.0-flash-exp"
159
+
160
+ # Check available models at: https://openrouter.ai/models
161
+ ```
162
+
163
+ **Example: Using ChatGLM via OpenRouter**
164
+ ```bash
165
+ ctxsaver config set aiProvider "https://openrouter.ai/api/v1"
166
+ ctxsaver config set aiApiKey "your-openrouter-api-key"
167
+ ctxsaver config set aiModel "zhipuai/glm-4"
168
+ # or
169
+ ctxsaver config set aiModel "zhipuai/glm-4-plus"
170
+ ```
171
+
172
+ ### Configuration
173
+ | Command | Description |
174
+ |---------|-------------|
175
+ | `ctxsaver config list` | View all configuration settings |
176
+ | `ctxsaver config get <key>` | Get a specific config value |
177
+ | `ctxsaver config set <key> <val>` | Set a configuration preference |
178
+
179
+ **Available Configuration Keys:**
180
+ | Key | Type | Default | Description |
181
+ |-----|------|---------|-------------|
182
+ | `aiApiKey` | string | - | API key for AI provider (or use `CTXSAVER_AI_KEY` env var) |
183
+ | `aiProvider` | string | `https://api.openai.com/v1` | OpenAI-compatible API endpoint |
184
+ | `aiModel` | string | `gpt-4o-mini` | Model name to use for AI commands |
185
+ | `aiMaxTokens` | number | `16384` | Max tokens for AI responses (increase for longer summaries) |
186
+ | `defaultOutput` | string | `clipboard` | Output mode for resume: `clipboard` or `stdout` |
187
+ | `autoGitCapture` | boolean | `true` | Auto-detect and include git info |
188
+ | `recentCommitCount` | number | `5` | Number of recent commits to capture |
189
+ | `defaultLogCount` | number | `10` | Default number of log entries to show |
190
+ | `watchInterval` | number | `5` | Auto-save interval in minutes for watch mode |
191
+ | `autoHook` | boolean | `false` | Auto-install git hook on init |
192
+
193
+ **Examples:**
194
+ ```bash
195
+ # View all settings
196
+ ctxsaver config list
197
+
198
+ # Configure AI settings
199
+ ctxsaver config set aiApiKey "sk-..."
200
+ ctxsaver config set aiProvider "https://api.openai.com/v1"
201
+ ctxsaver config set aiModel "gpt-4o"
202
+ ctxsaver config set aiMaxTokens 16384
203
+
204
+ # Increase max tokens if summaries are getting truncated
205
+ ctxsaver config set aiMaxTokens 32000
206
+
207
+ # Configure behavior
208
+ ctxsaver config set defaultOutput "stdout"
209
+ ctxsaver config set recentCommitCount 10
210
+ ctxsaver config set watchInterval 3
211
+
212
+ # Get a specific value
213
+ ctxsaver config get aiModel
214
+ ```
215
+
216
+ ## Integrations
217
+
218
+ ### šŸ’» Direct CLI Usage
219
+ Any AI agent with terminal access (e.g. via `run_command` or similar tools) can directly run `ctxsaver save`, `resume`, and `log`.
220
+
221
+ ## How It Works
222
+
223
+ CtxSaver stores a `.ctxsaver/` folder in your repo. Each entry captures:
224
+ - **Task**: What you are doing
225
+ - **Goal**: Why you are doing it
226
+ - **Approaches**: What you tried (and what failed)
227
+ - **Decisions**: Key architectural choices
228
+ - **State**: Where you left off
229
+
230
+ It works with **every** AI coding tool because it simply manages the *prompt* — the universal interface for LLMs.
231
+
@@ -0,0 +1,3 @@
1
+ export declare function compressCommand(options?: {
2
+ force?: boolean;
3
+ }): Promise<void>;
@@ -0,0 +1,97 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.compressCommand = compressCommand;
7
+ const chalk_1 = __importDefault(require("chalk"));
8
+ const fs_1 = __importDefault(require("fs"));
9
+ const path_1 = __importDefault(require("path"));
10
+ const context_1 = require("../core/context");
11
+ const git_1 = require("../core/git");
12
+ const ai_1 = require("../core/ai");
13
+ async function compressCommand(options) {
14
+ if (!(await (0, context_1.isInitialized)())) {
15
+ console.log(chalk_1.default.red("āœ— CtxSaver not initialized. Run `ctxsaver init` first."));
16
+ return;
17
+ }
18
+ try {
19
+ const branch = await (0, git_1.getCurrentBranch)();
20
+ const entries = await (0, context_1.loadBranchContext)(branch);
21
+ if (entries.length <= 2) {
22
+ console.log(chalk_1.default.yellow("⚠ Not enough context to compress (need at least 3 entries)."));
23
+ return;
24
+ }
25
+ console.log(chalk_1.default.gray(` Compressing ${entries.length} entries for branch: ${branch}...`));
26
+ // Keep the most recent entry intact, compress older ones
27
+ const toCompress = entries.slice(0, -1);
28
+ const latest = entries[entries.length - 1];
29
+ const prompt = `You are a developer assistant. Compress the following ${toCompress.length} coding session entries into a single summary entry. Keep only the most important information: key decisions, approaches that worked or failed, and overall progress.
30
+
31
+ Sessions to compress:
32
+ ${toCompress
33
+ .map((e, i) => `
34
+ Session ${i + 1} (${e.timestamp}):
35
+ - Task: ${e.task}
36
+ - Approaches: ${e.approaches.join(", ") || "none"}
37
+ - Decisions: ${e.decisions.join(", ") || "none"}
38
+ - State: ${e.currentState}
39
+ - Next steps: ${e.nextSteps.join(", ") || "none"}
40
+ `)
41
+ .join("\n")}
42
+
43
+ Generate a JSON response:
44
+ {
45
+ "task": "overall task description covering all sessions",
46
+ "approaches": ["significant approaches tried"],
47
+ "decisions": ["key decisions made across all sessions"],
48
+ "currentState": "where things stood after these sessions",
49
+ "nextSteps": ["remaining next steps"]
50
+ }
51
+
52
+ Be concise but preserve important decisions and learnings.`;
53
+ const result = await (0, ai_1.callAI)([
54
+ {
55
+ role: "system",
56
+ content: "You are a helpful assistant. Always respond with valid JSON.",
57
+ },
58
+ { role: "user", content: prompt },
59
+ ]);
60
+ if (result.error) {
61
+ console.log(chalk_1.default.red(`āœ— ${result.error}`));
62
+ return;
63
+ }
64
+ let parsed;
65
+ try {
66
+ const jsonMatch = result.content.match(/```(?:json)?\s*([\s\S]*?)```/) ||
67
+ [null, result.content];
68
+ parsed = JSON.parse(jsonMatch[1].trim());
69
+ }
70
+ catch {
71
+ console.log(chalk_1.default.red("āœ— Could not parse AI compression result."));
72
+ return;
73
+ }
74
+ // Create compressed entry from the oldest entry's metadata
75
+ const compressed = {
76
+ ...toCompress[0],
77
+ task: parsed.task || toCompress[0].task,
78
+ approaches: parsed.approaches || [],
79
+ decisions: parsed.decisions || [],
80
+ currentState: parsed.currentState || "",
81
+ nextSteps: parsed.nextSteps || [],
82
+ timestamp: toCompress[0].timestamp, // keep oldest timestamp
83
+ };
84
+ // Replace branch file with compressed + latest
85
+ const dir = await (0, context_1.getCtxSaverDir)();
86
+ const branchFile = path_1.default.join(dir, "branches", `${branch.replace(/\//g, "__")}.json`);
87
+ const newEntries = [compressed, latest];
88
+ fs_1.default.writeFileSync(branchFile, JSON.stringify(newEntries, null, 2));
89
+ console.log(chalk_1.default.green(`āœ“ Compressed ${entries.length} entries → 2 entries for branch: ${chalk_1.default.bold(branch)}`));
90
+ console.log(chalk_1.default.gray(` Kept: 1 compressed summary + latest entry`));
91
+ console.log(chalk_1.default.cyan(` Summary: ${parsed.task}`));
92
+ console.log();
93
+ }
94
+ catch (err) {
95
+ console.log(chalk_1.default.red(`āœ— Error: ${err.message}`));
96
+ }
97
+ }
@@ -0,0 +1 @@
1
+ export declare function configCommand(action?: string, key?: string, value?: string): Promise<void>;
@@ -0,0 +1,85 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.configCommand = configCommand;
7
+ const chalk_1 = __importDefault(require("chalk"));
8
+ const context_1 = require("../core/context");
9
+ const config_1 = require("../utils/config");
10
+ const VALID_KEYS = [
11
+ "defaultOutput",
12
+ "autoGitCapture",
13
+ "recentCommitCount",
14
+ "defaultLogCount",
15
+ "watchInterval",
16
+ "autoHook",
17
+ "aiProvider",
18
+ "aiModel",
19
+ "aiApiKey",
20
+ "aiMaxTokens",
21
+ ];
22
+ async function configCommand(action, key, value) {
23
+ if (!(await (0, context_1.isInitialized)())) {
24
+ console.log(chalk_1.default.red("āœ— CtxSaver not initialized. Run `ctxsaver init` first."));
25
+ return;
26
+ }
27
+ try {
28
+ const config = await (0, config_1.loadConfig)();
29
+ if (!action || action === "list") {
30
+ console.log(chalk_1.default.bold("\nCtxSaver Configuration:\n"));
31
+ for (const [k, v] of Object.entries(config)) {
32
+ if (k === "aiApiKey" && v) {
33
+ console.log(` ${chalk_1.default.cyan(k)}: ${chalk_1.default.gray("****" + String(v).slice(-4))}`);
34
+ }
35
+ else {
36
+ console.log(` ${chalk_1.default.cyan(k)}: ${chalk_1.default.white(String(v))}`);
37
+ }
38
+ }
39
+ console.log();
40
+ return;
41
+ }
42
+ if (action === "get") {
43
+ if (!key) {
44
+ console.log(chalk_1.default.red("āœ— Usage: ctxsaver config get <key>"));
45
+ return;
46
+ }
47
+ if (!VALID_KEYS.includes(key)) {
48
+ console.log(chalk_1.default.red(`āœ— Unknown config key: ${key}`));
49
+ console.log(chalk_1.default.gray(` Valid keys: ${VALID_KEYS.join(", ")}`));
50
+ return;
51
+ }
52
+ const val = config[key];
53
+ console.log(`${chalk_1.default.cyan(key)}: ${chalk_1.default.white(String(val ?? "(not set)"))}`);
54
+ return;
55
+ }
56
+ if (action === "set") {
57
+ if (!key || value === undefined) {
58
+ console.log(chalk_1.default.red("āœ— Usage: ctxsaver config set <key> <value>"));
59
+ return;
60
+ }
61
+ if (!VALID_KEYS.includes(key)) {
62
+ console.log(chalk_1.default.red(`āœ— Unknown config key: ${key}`));
63
+ console.log(chalk_1.default.gray(` Valid keys: ${VALID_KEYS.join(", ")}`));
64
+ return;
65
+ }
66
+ // Type coercion
67
+ let typedValue = value;
68
+ if (value === "true")
69
+ typedValue = true;
70
+ else if (value === "false")
71
+ typedValue = false;
72
+ else if (!isNaN(Number(value)) && !["aiApiKey", "aiProvider", "aiModel", "defaultOutput"].includes(key)) {
73
+ typedValue = Number(value);
74
+ }
75
+ await (0, config_1.saveConfig)({ [key]: typedValue });
76
+ console.log(chalk_1.default.green(`āœ“ Set ${chalk_1.default.bold(key)} = ${typedValue}`));
77
+ return;
78
+ }
79
+ console.log(chalk_1.default.red(`āœ— Unknown action: ${action}`));
80
+ console.log(chalk_1.default.gray(" Usage: ctxsaver config [list|get|set] [key] [value]"));
81
+ }
82
+ catch (err) {
83
+ console.log(chalk_1.default.red(`āœ— Error: ${err.message}`));
84
+ }
85
+ }
@@ -0,0 +1 @@
1
+ export declare function diffCommand(): Promise<void>;
@@ -0,0 +1,97 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.diffCommand = diffCommand;
7
+ const chalk_1 = __importDefault(require("chalk"));
8
+ const context_1 = require("../core/context");
9
+ const git_1 = require("../core/git");
10
+ function getTimeAgo(timestamp) {
11
+ const diff = Date.now() - new Date(timestamp).getTime();
12
+ const minutes = Math.floor(diff / 60000);
13
+ if (minutes < 60)
14
+ return `${minutes} minutes ago`;
15
+ const hours = Math.floor(minutes / 60);
16
+ if (hours < 24)
17
+ return `${hours} hours ago`;
18
+ const days = Math.floor(hours / 24);
19
+ return `${days} days ago`;
20
+ }
21
+ async function diffCommand() {
22
+ if (!(await (0, context_1.isInitialized)())) {
23
+ console.log(chalk_1.default.red("āœ— CtxSaver not initialized. Run `ctxsaver init` first."));
24
+ return;
25
+ }
26
+ try {
27
+ const branch = await (0, git_1.getCurrentBranch)();
28
+ const entries = await (0, context_1.loadBranchContext)(branch);
29
+ if (entries.length === 0) {
30
+ console.log(chalk_1.default.yellow(`⚠ No context found for branch: ${branch}`));
31
+ console.log(chalk_1.default.gray(" Run `ctxsaver save` to capture context first."));
32
+ return;
33
+ }
34
+ const latest = entries[entries.length - 1];
35
+ const [currentChanged, currentStaged] = await Promise.all([
36
+ (0, git_1.getChangedFiles)(),
37
+ (0, git_1.getStagedFiles)(),
38
+ ]);
39
+ const lastSaveTime = getTimeAgo(latest.timestamp);
40
+ console.log(chalk_1.default.bold(`\nSince last save (${lastSaveTime}):\n`));
41
+ // --- Files ---
42
+ const previousFiles = new Set(latest.filesChanged);
43
+ const currentFiles = new Set([...currentChanged, ...currentStaged]);
44
+ const newFiles = [...currentFiles].filter((f) => !previousFiles.has(f));
45
+ const removedFiles = [...previousFiles].filter((f) => !currentFiles.has(f));
46
+ const stillChanged = [...currentFiles].filter((f) => previousFiles.has(f));
47
+ if (newFiles.length > 0) {
48
+ newFiles.forEach((f) => console.log(` ${chalk_1.default.green("+")} ${f} ${chalk_1.default.green("(new)")}`));
49
+ }
50
+ if (stillChanged.length > 0) {
51
+ stillChanged.forEach((f) => console.log(` ${chalk_1.default.yellow("~")} ${f} ${chalk_1.default.gray("(still modified)")}`));
52
+ }
53
+ if (removedFiles.length > 0) {
54
+ removedFiles.forEach((f) => console.log(` ${chalk_1.default.red("-")} ${f} ${chalk_1.default.gray("(resolved)")}`));
55
+ }
56
+ if (newFiles.length === 0 && removedFiles.length === 0 && stillChanged.length === 0) {
57
+ console.log(chalk_1.default.gray(" No file changes since last save."));
58
+ }
59
+ // --- Decisions ---
60
+ if (entries.length >= 2) {
61
+ const previous = entries[entries.length - 2];
62
+ const prevDecisions = new Set(previous.decisions);
63
+ const newDecisions = latest.decisions.filter((d) => !prevDecisions.has(d));
64
+ if (newDecisions.length > 0) {
65
+ console.log();
66
+ newDecisions.forEach((d) => console.log(` ${chalk_1.default.cyan("Decision added:")} "${d}"`));
67
+ }
68
+ }
69
+ else if (latest.decisions.length > 0) {
70
+ console.log();
71
+ latest.decisions.forEach((d) => console.log(` ${chalk_1.default.cyan("Decision:")} "${d}"`));
72
+ }
73
+ // --- Next Steps Progress ---
74
+ if (entries.length >= 2) {
75
+ const previous = entries[entries.length - 2];
76
+ const completedSteps = previous.nextSteps.filter((step) => !latest.nextSteps.includes(step));
77
+ if (completedSteps.length > 0) {
78
+ console.log();
79
+ completedSteps.forEach((s) => console.log(` ${chalk_1.default.green("āœ“")} Next step completed: "${s}"`));
80
+ }
81
+ const newSteps = latest.nextSteps.filter((step) => !previous.nextSteps.includes(step));
82
+ if (newSteps.length > 0) {
83
+ newSteps.forEach((s) => console.log(` ${chalk_1.default.blue("→")} New next step: "${s}"`));
84
+ }
85
+ }
86
+ // --- Summary line ---
87
+ const totalNew = newFiles.length;
88
+ const totalModified = stillChanged.length;
89
+ const totalResolved = removedFiles.length;
90
+ console.log();
91
+ console.log(chalk_1.default.gray(` Summary: +${totalNew} new, ~${totalModified} modified, -${totalResolved} resolved`));
92
+ console.log();
93
+ }
94
+ catch (err) {
95
+ console.log(chalk_1.default.red(`āœ— Error: ${err.message}`));
96
+ }
97
+ }
@@ -0,0 +1 @@
1
+ export declare function handoffCommand(assignee?: string, message?: string): Promise<void>;
@@ -0,0 +1,151 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.handoffCommand = handoffCommand;
7
+ const chalk_1 = __importDefault(require("chalk"));
8
+ const inquirer_1 = __importDefault(require("inquirer"));
9
+ const uuid_1 = require("uuid");
10
+ const context_1 = require("../core/context");
11
+ const git_1 = require("../core/git");
12
+ async function handoffCommand(assignee, message) {
13
+ if (!(await (0, context_1.isInitialized)())) {
14
+ console.log(chalk_1.default.red("āœ— CtxSaver not initialized. Run `ctxsaver init` first."));
15
+ return;
16
+ }
17
+ try {
18
+ // Clean up @ prefix if present
19
+ let targetAssignee = assignee?.replace(/^@/, "") || "";
20
+ let handoffNote = message || "";
21
+ if (!targetAssignee || !handoffNote) {
22
+ const answers = await inquirer_1.default.prompt([
23
+ ...(!targetAssignee
24
+ ? [
25
+ {
26
+ type: "input",
27
+ name: "assignee",
28
+ message: "Who are you handing off to?",
29
+ validate: (input) => input.length > 0 || "Assignee is required",
30
+ },
31
+ ]
32
+ : []),
33
+ ...(!handoffNote
34
+ ? [
35
+ {
36
+ type: "input",
37
+ name: "handoffNote",
38
+ message: "Handoff note (what they need to know):",
39
+ validate: (input) => input.length > 0 || "Handoff note is required",
40
+ },
41
+ ]
42
+ : []),
43
+ {
44
+ type: "input",
45
+ name: "task",
46
+ message: "What were you working on?",
47
+ validate: (input) => input.length > 0 || "Task description is required",
48
+ },
49
+ {
50
+ type: "input",
51
+ name: "currentState",
52
+ message: "Where did you leave off?",
53
+ validate: (input) => input.length > 0 || "Current state is required",
54
+ },
55
+ {
56
+ type: "input",
57
+ name: "nextSteps",
58
+ message: "What comes next? (comma-separated)",
59
+ default: "",
60
+ },
61
+ {
62
+ type: "input",
63
+ name: "blockers",
64
+ message: "Any blockers? (comma-separated)",
65
+ default: "",
66
+ },
67
+ ]);
68
+ targetAssignee = targetAssignee || answers.assignee;
69
+ handoffNote = handoffNote || answers.handoffNote;
70
+ const [branch, repo, filesChanged, filesStaged, recentCommits, author] = await Promise.all([
71
+ (0, git_1.getCurrentBranch)(),
72
+ (0, git_1.getRepoName)(),
73
+ (0, git_1.getChangedFiles)(),
74
+ (0, git_1.getStagedFiles)(),
75
+ (0, git_1.getRecentCommits)(),
76
+ (0, git_1.getAuthor)(),
77
+ ]);
78
+ const entry = {
79
+ id: (0, uuid_1.v4)(),
80
+ timestamp: new Date().toISOString(),
81
+ branch,
82
+ repo,
83
+ author,
84
+ task: answers.task,
85
+ approaches: [],
86
+ decisions: [],
87
+ currentState: answers.currentState,
88
+ nextSteps: answers.nextSteps
89
+ ? answers.nextSteps
90
+ .split(",")
91
+ .map((s) => s.trim())
92
+ .filter(Boolean)
93
+ : [],
94
+ blockers: answers.blockers
95
+ ? answers.blockers
96
+ .split(",")
97
+ .map((s) => s.trim())
98
+ .filter(Boolean)
99
+ : undefined,
100
+ filesChanged,
101
+ filesStaged,
102
+ recentCommits,
103
+ assignee: targetAssignee,
104
+ handoffNote,
105
+ };
106
+ await (0, context_1.saveContext)(entry);
107
+ console.log(chalk_1.default.green(`\nāœ“ Handoff created for ${chalk_1.default.bold("@" + targetAssignee)}`));
108
+ console.log(chalk_1.default.gray(` Branch: ${branch}`));
109
+ console.log(chalk_1.default.cyan(`\n šŸ“ Handoff Note:`));
110
+ console.log(chalk_1.default.white(` ${handoffNote}\n`));
111
+ console.log(chalk_1.default.gray(` They can resume with: ${chalk_1.default.white("ctxsaver resume --branch " + branch)}`));
112
+ }
113
+ else {
114
+ // Quick mode — minimal context with handoff
115
+ const [branch, repo, filesChanged, filesStaged, recentCommits, author] = await Promise.all([
116
+ (0, git_1.getCurrentBranch)(),
117
+ (0, git_1.getRepoName)(),
118
+ (0, git_1.getChangedFiles)(),
119
+ (0, git_1.getStagedFiles)(),
120
+ (0, git_1.getRecentCommits)(),
121
+ (0, git_1.getAuthor)(),
122
+ ]);
123
+ const entry = {
124
+ id: (0, uuid_1.v4)(),
125
+ timestamp: new Date().toISOString(),
126
+ branch,
127
+ repo,
128
+ author,
129
+ task: `Handoff to @${targetAssignee}`,
130
+ approaches: [],
131
+ decisions: [],
132
+ currentState: handoffNote,
133
+ nextSteps: [],
134
+ filesChanged,
135
+ filesStaged,
136
+ recentCommits,
137
+ assignee: targetAssignee,
138
+ handoffNote,
139
+ };
140
+ await (0, context_1.saveContext)(entry);
141
+ console.log(chalk_1.default.green(`\nāœ“ Handoff created for ${chalk_1.default.bold("@" + targetAssignee)}`));
142
+ console.log(chalk_1.default.gray(` Branch: ${branch}`));
143
+ console.log(chalk_1.default.cyan(`\n šŸ“ Handoff Note:`));
144
+ console.log(chalk_1.default.white(` ${handoffNote}\n`));
145
+ console.log(chalk_1.default.gray(` They can resume with: ${chalk_1.default.white("ctxsaver resume --branch " + branch)}`));
146
+ }
147
+ }
148
+ catch (err) {
149
+ console.log(chalk_1.default.red(`āœ— Error: ${err.message}`));
150
+ }
151
+ }
@@ -0,0 +1 @@
1
+ export declare function hookCommand(action?: string): Promise<void>;