Skip to content
This repository was archived by the owner on Jun 4, 2026. It is now read-only.

Commit ae4865f

Browse files
Chrisclaude
authored andcommitted
Implement PR review feedback and add /track skill
Code improvements from Copilot review: - history.ts: Scan workspace for changes when creating manual checkpoint - track.ts: Use consistent ISO timestamp format, add mutex for race conditions, add Linux fs.watch warning - stop.ts: Use more specific grep pattern to avoid false positives - profile.ts: Remove unused variable - profile-manager.ts: Add REALM_SERVER_URL derivation from MATRIX_URL - index.ts: Add password security warning, clarify env var requirements, add track/stop examples Documentation updates: - Add new /track skill for Claude Code - Update CLAUDE.md with /track skill and track→sync workflow - Update README.md to emphasize that track requires sync to push changes Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent e8ba8ae commit ae4865f

9 files changed

Lines changed: 211 additions & 19 deletions

File tree

.claude/CLAUDE.md

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,12 @@ npx boxel profile switch username # Switch by partial match
9999

100100
## Available Skills
101101

102+
### `/track` - Track Local Edits
103+
Starts `boxel track` to auto-checkpoint local file changes:
104+
- Creates checkpoints as you save files in IDE
105+
- **IMPORTANT:** Track creates LOCAL checkpoints only
106+
- **After editing, run `boxel sync . --prefer-local` to push to server**
107+
102108
### `/watch` - Smart Watch
103109
Starts `boxel watch` with intelligent interval based on context:
104110
- **Active development** (5s interval, 3s debounce): When editing files
@@ -114,7 +120,7 @@ Complete restore workflow:
114120

115121
### `/sync` - Smart Sync
116122
Context-aware bidirectional sync:
117-
- After local edits → `--prefer-local`
123+
- After local edits or track `--prefer-local`
118124
- After server changes → `--prefer-remote`
119125
- After restore → `--prefer-local` (essential for syncing deletions)
120126

@@ -268,7 +274,19 @@ boxel skills --export . # Re-export to .claude/commands/
268274

269275
## Key Workflows
270276

271-
### Active Development Session
277+
### Local Development with Track (IDE/Agent Editing)
278+
```bash
279+
boxel track . # Start tracking local edits (auto-checkpoints)
280+
# ... edit files in IDE or with Claude ...
281+
# Track creates LOCAL checkpoints as you save
282+
283+
# IMPORTANT: When ready to push changes to Boxel server:
284+
boxel sync . --prefer-local # Push your local changes to server
285+
```
286+
287+
**Remember:** Track does NOT sync to server automatically - it only creates local checkpoints. Always run `sync --prefer-local` when you want your changes live on the server.
288+
289+
### Active Development Session (Watching Server)
272290
```bash
273291
/watch # Starts with 5s interval
274292
# ... edit in Boxel UI or locally ...

.claude/commands/track.md

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# Track Skill
2+
3+
Start `boxel track` to monitor local file changes and create checkpoints automatically.
4+
5+
## When to Use Track
6+
7+
Use **track** when you're editing files locally (in IDE, with AI agent, etc.) and want automatic backups:
8+
- Working in VS Code, Cursor, or other IDE
9+
- AI agent is editing files
10+
- You want checkpoint history of your work
11+
12+
**Track vs Watch:**
13+
| Command | Symbol | Direction | Purpose |
14+
|---------|--------|-----------|---------|
15+
| `track` || Local edits → Checkpoints | Backup your work as you edit |
16+
| `watch` || Server → Local | Pull external changes from Boxel UI |
17+
18+
## Commands
19+
20+
```bash
21+
# Start tracking (default: 3s debounce, 10s min interval)
22+
boxel track .
23+
24+
# Custom timing (5s debounce, 30s between checkpoints)
25+
boxel track . -d 5 -i 30
26+
27+
# Quiet mode (only show checkpoints)
28+
boxel track . -q
29+
30+
# Stop all track/watch processes
31+
boxel stop
32+
```
33+
34+
## The Track → Sync Workflow
35+
36+
**IMPORTANT:** Track only creates local checkpoints. To push changes to the Boxel server:
37+
38+
```bash
39+
# 1. Track creates checkpoints as you edit
40+
boxel track .
41+
42+
# 2. When ready to push to server, sync with --prefer-local
43+
boxel sync . --prefer-local
44+
```
45+
46+
Track does NOT automatically sync to the server. This is intentional - it lets you:
47+
- Work offline with local backups
48+
- Batch multiple edits before pushing
49+
- Review changes before they go live
50+
51+
## Context Detection
52+
53+
When invoked, consider:
54+
55+
### Standard Development (3s debounce, 10s interval)
56+
- Normal editing workflow
57+
- Balanced between checkpoint frequency and overhead
58+
59+
### Fast Iteration (2s debounce, 5s interval)
60+
- Rapid prototyping
61+
- User says "track closely" or "capture everything"
62+
63+
### Background Tracking (5s debounce, 30s interval)
64+
- Long editing sessions
65+
- User says "just backup" or "light tracking"
66+
67+
## Response Format
68+
69+
When invoked:
70+
1. Confirm workspace directory
71+
2. Start track with appropriate settings
72+
3. **Remind user to sync when ready to push changes**
73+
74+
Example:
75+
```
76+
Starting track in the current workspace (3s debounce, 10s interval).
77+
Checkpoints will be created automatically as you save files.
78+
79+
Remember: Track creates LOCAL checkpoints only.
80+
When ready to push changes to Boxel server:
81+
boxel sync . --prefer-local
82+
83+
Use Ctrl+C to stop tracking, or `boxel stop` from another terminal.
84+
```

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,9 +301,14 @@ boxel skills --export . # Export to .claude/commands/
301301
boxel track . # Start tracking local edits
302302
# In another terminal or IDE, edit files...
303303
# Checkpoints created automatically as you save
304+
305+
# IMPORTANT: Track creates LOCAL checkpoints only!
306+
# When ready to push changes to Boxel server:
304307
boxel sync . --prefer-local # Push changes to server
305308
```
306309

310+
**Remember:** `track` does NOT sync to server - it only creates local checkpoints for safety. Always run `sync --prefer-local` when you want your changes live.
311+
307312
### Active Development (with edit lock)
308313
```bash
309314
boxel edit . my-card.gts # Lock file (if watch is running)

src/commands/history.ts

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,38 @@
11
import * as fs from 'fs';
22
import * as path from 'path';
33
import * as readline from 'readline';
4-
import { CheckpointManager, Checkpoint } from '../lib/checkpoint-manager.js';
4+
import { CheckpointManager, Checkpoint, CheckpointChange } from '../lib/checkpoint-manager.js';
5+
6+
/**
7+
* Scan workspace directory to build a changes array for manual checkpoints.
8+
* Marks all current files as 'modified' since we're snapshotting the current state.
9+
*/
10+
function scanWorkspaceForChanges(workspaceDir: string): CheckpointChange[] {
11+
const changes: CheckpointChange[] = [];
12+
13+
const scan = (dir: string, prefix = '') => {
14+
if (!fs.existsSync(dir)) return;
15+
16+
const entries = fs.readdirSync(dir, { withFileTypes: true });
17+
for (const entry of entries) {
18+
// Skip internal files
19+
if (entry.name.startsWith('.boxel-') || entry.name === '.git') continue;
20+
if (entry.name.startsWith('.') && entry.name !== '.realm.json') continue;
21+
22+
const fullPath = path.join(dir, entry.name);
23+
const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
24+
25+
if (entry.isDirectory()) {
26+
scan(fullPath, relativePath);
27+
} else {
28+
changes.push({ file: relativePath, status: 'modified' });
29+
}
30+
}
31+
};
32+
33+
scan(workspaceDir);
34+
return changes;
35+
}
536

637
// ANSI escape codes for terminal control
738
const ESC = '\x1b';
@@ -43,8 +74,9 @@ export async function historyCommand(
4374
manager.init();
4475
}
4576

46-
// Get current files to create a checkpoint of current state
47-
const checkpoint = manager.createCheckpoint('manual', [], options.message);
77+
// Scan workspace to get current files for the checkpoint
78+
const changes = scanWorkspaceForChanges(workspaceDir);
79+
const checkpoint = manager.createCheckpoint('manual', changes, options.message);
4880

4981
if (checkpoint) {
5082
console.log(`${FG_GREEN}${RESET} ${FG_YELLOW}📍${RESET} Checkpoint created: ${FG_YELLOW}${checkpoint.shortHash}${RESET}`);

src/commands/profile.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,6 @@ async function listProfiles(manager: ProfileManager): Promise<void> {
158158
const profile = manager.getProfile(id)!;
159159
const isActive = id === activeId;
160160
const env = getEnvironmentFromMatrixId(id);
161-
const username = getUsernameFromMatrixId(id);
162161

163162
const marker = isActive ? `${FG_GREEN}${RESET} ` : ' ';
164163
const envLabel = getEnvironmentShortLabel(env);

src/commands/stop.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,9 @@ export async function stopCommand(): Promise<void> {
2121
try {
2222
// Find boxel watch and track processes
2323
// Match both development mode (tsx src/index.ts) and installed mode (boxel or node...boxel)
24+
// Use more specific pattern with word boundaries to avoid false positives
2425
const result = execSync(
25-
`ps aux | grep -E '(tsx.*src/index.ts|boxel|node.*boxel).*(watch|track)' | grep -v grep | grep -v 'stop'`,
26+
`ps aux | grep -E '(tsx[[:space:]].*src/index\\.ts[[:space:]]+(watch|track)|[[:space:]]boxel[[:space:]]+(watch|track)|node[[:space:]].*boxel[[:space:]]+(watch|track))' | grep -v grep | grep -v '[[:space:]]stop'`,
2627
{ encoding: 'utf-8' }
2728
).trim();
2829

src/commands/track.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export async function trackCommand(
4040
let debounceTimer: NodeJS.Timeout | null = null;
4141
let pendingChanges = new Map<string, 'added' | 'modified' | 'deleted'>();
4242
let lastCheckpointTime = Date.now();
43+
let isCheckingChanges = false; // Mutex to prevent concurrent checkForChanges calls
4344

4445
// Initialize file states
4546
const initializeFileStates = (dir: string, prefix = '') => {
@@ -131,6 +132,18 @@ export async function trackCommand(
131132
};
132133

133134
const checkForChanges = () => {
135+
// Prevent concurrent execution (fs.watch and setInterval can trigger simultaneously)
136+
if (isCheckingChanges) return;
137+
isCheckingChanges = true;
138+
139+
try {
140+
checkForChangesImpl();
141+
} finally {
142+
isCheckingChanges = false;
143+
}
144+
};
145+
146+
const checkForChangesImpl = () => {
134147
const currentFiles = new Map<string, { mtime: number; size: number }>();
135148

136149
const scanDir = (dir: string, prefix = '') => {
@@ -214,11 +227,18 @@ export async function trackCommand(
214227
};
215228

216229
// Use fs.watch for efficient file watching
230+
// Note: recursive option is only supported on macOS and Windows.
231+
// On Linux, we rely on the polling fallback (setInterval) below.
217232
const watchers: fs.FSWatcher[] = [];
233+
const isLinux = process.platform === 'linux';
234+
235+
if (isLinux && !options.quiet) {
236+
console.log(` Note: On Linux, file watching uses polling only (fs.watch recursive not supported)\n`);
237+
}
218238

219239
const watchDir = (dir: string) => {
220240
try {
221-
const watcher = fs.watch(dir, { recursive: true }, (eventType, filename) => {
241+
const watcher = fs.watch(dir, { recursive: !isLinux }, (eventType, filename) => {
222242
if (!filename) return;
223243

224244
// Skip internal files
@@ -276,5 +296,6 @@ export async function trackCommand(
276296
}
277297

278298
function timestamp(): string {
279-
return new Date().toLocaleTimeString();
299+
const now = new Date();
300+
return now.toISOString().substring(11, 19); // HH:MM:SS in UTC
280301
}

src/index.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -329,14 +329,21 @@ program
329329
.option('-p, --password <password>', 'Password (for add command)')
330330
.option('-n, --name <displayName>', 'Display name (for add command)')
331331
.action(async (subcommand?: string, arg?: string, options?: { user?: string; password?: string; name?: string }) => {
332+
if (options?.password) {
333+
console.warn(
334+
'Warning: Supplying a password via -p/--password may expose it in shell history and process listings. ' +
335+
'For non-interactive usage, prefer the BOXEL_PASSWORD environment variable or use "boxel profile add" interactively.',
336+
);
337+
}
332338
await profileCommand(subcommand, arg, options);
333339
});
334340

335341
// Add help text for environment variables
336342
program.addHelpText('after', `
337343
Authentication:
338344
Use 'boxel profile' to manage saved credentials (recommended)
339-
Or set environment variables: MATRIX_URL, MATRIX_USERNAME, MATRIX_PASSWORD, REALM_SERVER_URL
345+
Or set all environment variables (all required):
346+
MATRIX_URL, MATRIX_USERNAME, MATRIX_PASSWORD, REALM_SERVER_URL
340347
341348
Workspace References:
342349
. Current directory (must have .boxel-sync.json)
@@ -364,6 +371,11 @@ Examples:
364371
boxel watch . -i 10 Check every 10 seconds
365372
boxel watch . -q Quiet mode (only show changes)
366373
374+
boxel track . Track local edits, auto-checkpoint
375+
boxel track . -d 5 -i 30 5s debounce, 30s min between checkpoints
376+
377+
boxel stop Stop all running watch/track processes
378+
367379
boxel pull https://... ./local One-way pull (for read-only realms)
368380
369381
boxel touch . Touch all files to force re-indexing

src/lib/profile-manager.ts

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -258,16 +258,36 @@ export class ProfileManager {
258258
const matrixUrl = process.env.MATRIX_URL;
259259
const username = process.env.MATRIX_USERNAME;
260260
const password = process.env.MATRIX_PASSWORD;
261-
const realmServerUrl = process.env.REALM_SERVER_URL;
261+
let realmServerUrl = process.env.REALM_SERVER_URL;
262+
263+
if (matrixUrl && username && password) {
264+
// Derive realm server URL from Matrix URL if not explicitly set
265+
if (!realmServerUrl) {
266+
try {
267+
const matrixUrlObj = new URL(matrixUrl);
268+
// Common pattern: matrix.X.Y -> app.X.Y or matrix-staging.X.Y -> realms-staging.X.Y
269+
if (matrixUrlObj.hostname.startsWith('matrix.')) {
270+
realmServerUrl = `${matrixUrlObj.protocol}//app.${matrixUrlObj.hostname.slice(7)}/`;
271+
} else if (matrixUrlObj.hostname.startsWith('matrix-staging.')) {
272+
realmServerUrl = `${matrixUrlObj.protocol}//realms-staging.${matrixUrlObj.hostname.slice(15)}/`;
273+
} else if (matrixUrlObj.hostname.startsWith('matrix-')) {
274+
// matrix-X.Y.Z -> X.Y.Z (generic fallback)
275+
realmServerUrl = `${matrixUrlObj.protocol}//${matrixUrlObj.hostname.slice(7)}/`;
276+
}
277+
} catch {
278+
// Invalid URL, will return null below
279+
}
280+
}
262281

263-
if (matrixUrl && username && password && realmServerUrl) {
264-
return {
265-
matrixUrl,
266-
username,
267-
password,
268-
realmServerUrl,
269-
profileId: null,
270-
};
282+
if (realmServerUrl) {
283+
return {
284+
matrixUrl,
285+
username,
286+
password,
287+
realmServerUrl,
288+
profileId: null,
289+
};
290+
}
271291
}
272292

273293
return null;

0 commit comments

Comments
 (0)