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.
- package/README.md +231 -0
- package/dist/commands/compress.d.ts +3 -0
- package/dist/commands/compress.js +97 -0
- package/dist/commands/config-cmd.d.ts +1 -0
- package/dist/commands/config-cmd.js +85 -0
- package/dist/commands/diff.d.ts +1 -0
- package/dist/commands/diff.js +97 -0
- package/dist/commands/handoff.d.ts +1 -0
- package/dist/commands/handoff.js +151 -0
- package/dist/commands/hook.d.ts +1 -0
- package/dist/commands/hook.js +60 -0
- package/dist/commands/init.d.ts +1 -0
- package/dist/commands/init.js +49 -0
- package/dist/commands/log.d.ts +4 -0
- package/dist/commands/log.js +53 -0
- package/dist/commands/resume.d.ts +4 -0
- package/dist/commands/resume.js +44 -0
- package/dist/commands/save.d.ts +13 -0
- package/dist/commands/save.js +160 -0
- package/dist/commands/share.d.ts +3 -0
- package/dist/commands/share.js +58 -0
- package/dist/commands/suggest.d.ts +1 -0
- package/dist/commands/suggest.js +67 -0
- package/dist/commands/summarize.d.ts +3 -0
- package/dist/commands/summarize.js +203 -0
- package/dist/commands/watch.d.ts +3 -0
- package/dist/commands/watch.js +99 -0
- package/dist/core/ai.d.ts +17 -0
- package/dist/core/ai.js +55 -0
- package/dist/core/context.d.ts +16 -0
- package/dist/core/context.js +88 -0
- package/dist/core/git.d.ts +7 -0
- package/dist/core/git.js +47 -0
- package/dist/core/parser.d.ts +15 -0
- package/dist/core/parser.js +666 -0
- package/dist/core/parser.test.d.ts +1 -0
- package/dist/core/parser.test.js +99 -0
- package/dist/core/prompt.d.ts +2 -0
- package/dist/core/prompt.js +75 -0
- package/dist/core/types.d.ts +24 -0
- package/dist/core/types.js +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +92 -0
- package/dist/utils/clipboard.d.ts +5 -0
- package/dist/utils/clipboard.js +23 -0
- package/dist/utils/config.d.ts +32 -0
- package/dist/utils/config.js +49 -0
- 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,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>;
|