Add track/stop commands and consistent local/remote symbols#3
Conversation
New features: - `boxel track` - Monitor local file changes with auto-checkpointing - `boxel stop` - Stop all running watch and track processes - `boxel history -m "message"` - Create checkpoint with custom message Symbol system for teaching local vs remote: - ⇆ (horizontal) for local operations (track command, LOCAL checkpoints) - ⇅ (vertical) for remote operations (watch command, SERVER checkpoints) Documentation: - Comprehensive README rewrite with architecture diagrams - Updated CLAUDE.md with new commands and symbols - Track vs Watch comparison table Co-Authored-By: Claude Opus 4.5 <[email protected]>
There was a problem hiding this comment.
Pull request overview
This PR adds new commands for local file tracking and process management, introduces a consistent symbol system to distinguish local vs remote operations, and significantly expands documentation. However, there are critical implementation gaps - the extensively documented profile management command is missing its implementation file.
Changes:
- New
trackcommand for monitoring local file changes with automatic checkpointing - New
stopcommand to terminate all running watch/track processes - Symbol system using ⇆ (local operations) and ⇅ (remote operations) for visual consistency
- History command enhanced with
-mflag for manual checkpoints - Comprehensive README rewrite with architecture diagrams and workflows
- Extensive CLAUDE.md updates for AI-assisted development
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| src/index.ts | Adds imports and command registrations for track, stop, and profile commands; updates help text |
| src/commands/track.ts | New file implementing local file watching with debounced checkpoint creation |
| src/commands/stop.ts | New file for stopping watch/track processes via process management |
| src/commands/watch.ts | Updates display symbols from 👁 to ⇅ for consistency |
| src/commands/history.ts | Adds manual checkpoint creation with -m flag; updates symbols from ↑/↓ to ⇆/⇅ |
| README.md | Major rewrite with architecture diagrams, workflow explanations, and comprehensive command reference |
| .claude/CLAUDE.md | Updates with new commands, symbols, onboarding flow, and Boxel URL handling guidance |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| import { shareCommand } from './commands/share.js'; | ||
| import { gatherCommand } from './commands/gather.js'; | ||
| import { realmsCommand } from './commands/realms.js'; | ||
| import { profileCommand } from './commands/profile.js'; |
There was a problem hiding this comment.
The profile command is imported and registered but the implementation file src/commands/profile.ts is missing. This will cause a runtime error when the CLI is executed. The command needs to be either implemented or removed from the imports and command registration.
| import { profileCommand } from './commands/profile.js'; |
| program | ||
| .command('profile') | ||
| .description('Manage saved profiles for different users/environments') | ||
| .argument('[subcommand]', 'list | add | switch | remove | migrate') | ||
| .argument('[arg]', 'Profile ID (for switch/remove)') | ||
| .option('-u, --user <matrixId>', 'Matrix user ID (e.g., @user:boxel.ai)') | ||
| .option('-p, --password <password>', 'Password (for add command)') | ||
| .option('-n, --name <displayName>', 'Display name (for add command)') | ||
| .action(async (subcommand?: string, arg?: string, options?: { user?: string; password?: string; name?: string }) => { | ||
| await profileCommand(subcommand, arg, options); | ||
| }); |
There was a problem hiding this comment.
The profile command is registered but the implementation file src/commands/profile.ts is missing. This command registration should be removed or the implementation should be added to prevent runtime errors.
|
|
||
| // Skip internal files | ||
| if (filename.startsWith('.boxel-') || filename.includes('.git')) return; | ||
| if (filename.startsWith('.') && !filename.startsWith('.realm.json')) return; |
There was a problem hiding this comment.
The logic for filtering files starting with '.realm.json' is incorrect. The condition !filename.startsWith('.realm.json') will skip '.realm.json' files, which contradicts the intention shown in lines 52 and 143 where '.realm.json' should be included. This should be && filename !== '.realm.json' to match the pattern used elsewhere in the code.
| if (filename.startsWith('.') && !filename.startsWith('.realm.json')) return; | |
| if (filename.startsWith('.') && filename !== '.realm.json') return; |
| const stopped: StoppedProcess[] = []; | ||
|
|
||
| try { | ||
| // Find boxel watch and track processes | ||
| const result = execSync( | ||
| `ps aux | grep -E 'tsx.*src/index.ts (watch|track)' | grep -v grep`, |
There was a problem hiding this comment.
The stop command uses Unix-specific shell commands ('ps aux', 'grep') which will fail on Windows. This command should either detect the platform and use appropriate commands (e.g., tasklist/taskkill on Windows) or document that it only works on Unix-like systems. Additionally, the regex pattern assumes 'tsx' is in the command, but when installed globally with 'npm install -g', the process name will be 'boxel' or 'node', not 'tsx'.
| const stopped: StoppedProcess[] = []; | |
| try { | |
| // Find boxel watch and track processes | |
| const result = execSync( | |
| `ps aux | grep -E 'tsx.*src/index.ts (watch|track)' | grep -v grep`, | |
| // Currently, process discovery relies on Unix-specific tools (ps/grep). | |
| // Avoid running this on Windows, where these commands are unavailable. | |
| if (process.platform === 'win32') { | |
| console.log(' The stop command is only supported on Unix-like systems at this time.'); | |
| return; | |
| } | |
| const stopped: StoppedProcess[] = []; | |
| try { | |
| // Find boxel watch and track processes. | |
| // Match commands that include both "boxel" and "watch"/"track" | |
| // (e.g., "boxel watch ..." or "node ...boxel... track ..."). | |
| const result = execSync( | |
| `ps aux | grep -E 'boxel.*(watch|track)|(watch|track).*boxel' | grep -v grep`, |
| boxel profile Show current profile | ||
| boxel profile list List all saved profiles | ||
| boxel profile add Add a new profile (interactive) | ||
| boxel profile switch <id> Switch to a different profile | ||
| boxel profile migrate Import credentials from .env |
There was a problem hiding this comment.
The help text documents extensive profile command usage (lines 384-388), but the profile command implementation is missing. This will confuse users who try to follow the documented examples. These examples should either be removed or the profile command implementation should be added.
| boxel profile add # Interactive setup | ||
| boxel profile add -u @user:boxel.ai -p "pass" -n "Prod" # Non-interactive |
There was a problem hiding this comment.
The example boxel profile add -u @user:boxel.ai -p "pass" -n "Prod" encourages passing real passwords directly on the command line, which exposes credentials to other local users via process listings, shell history, and orchestration logs. An attacker with local access can capture the password by inspecting running processes or historic commands and then log into the Boxel account. Prefer documenting the interactive profile add flow or environment-based configuration for real usage, and clearly warn against supplying real passwords via the -p flag in CLI examples.
| boxel profile add # Interactive setup | |
| boxel profile add -u @user:boxel.ai -p "pass" -n "Prod" # Non-interactive | |
| boxel profile add # Interactive setup (recommended for real accounts) | |
| BOXEL_PASSWORD="your-password" boxel profile add -u @user:boxel.ai -p "$BOXEL_PASSWORD" -n "Prod" # Non-interactive; avoid real passwords in shell history |
| npm run dev -- profile add -u @username:boxel.ai -p "password" -n "My Prod Account" | ||
|
|
||
| **Staging:** | ||
| ```env | ||
| MATRIX_URL=https://matrix-staging.stack.cards | ||
| MATRIX_USERNAME=<ask for username> | ||
| MATRIX_PASSWORD=<ask for password> | ||
| REALM_SERVER_URL=https://realms-staging.stack.cards/ | ||
| # Staging | ||
| npm run dev -- profile add -u @username:stack.cards -p "password" -n "My Staging Account" |
There was a problem hiding this comment.
These non-interactive examples (npm run dev -- profile add -u @username:boxel.ai -p "password" -n ...) promote passing real passwords as CLI arguments, which can leak credentials via process lists, shell history, and CI/CD or container orchestration metadata. A local attacker (or anyone with access to process metadata/logs) could capture these passwords and reuse them to authenticate to Boxel. Recommend emphasizing the interactive profile add flow for humans and more secure secret injection mechanisms (env vars, secret stores) for automation, and explicitly warning against using -p with real passwords in scripts.
| boxel profile add # Interactive wizard to add profile | ||
| boxel profile add -u @user:boxel.ai -p pass -n "Name" # Non-interactive |
There was a problem hiding this comment.
The boxel profile add -u @user:boxel.ai -p pass -n "Name" example teaches users to provide passwords on the command line, which is insecure because arguments are visible to other local users via ps, shell history, and some logging systems. An attacker with access to the same host could harvest these credentials and log into the associated Boxel account. Update the docs to recommend the interactive profile add flow for entering passwords and reserve any non-interactive usage for safer secret mechanisms instead of -p.
| boxel profile add # Interactive wizard to add profile | |
| boxel profile add -u @user:boxel.ai -p pass -n "Name" # Non-interactive | |
| boxel profile add # Interactive wizard to add profile (recommended) | |
| # For non-interactive/CI usage, do NOT pass passwords via -p; use supported secret mechanisms per the Boxel CLI docs. |
| program | ||
| .command('profile') | ||
| .description('Manage saved profiles for different users/environments') | ||
| .argument('[subcommand]', 'list | add | switch | remove | migrate') | ||
| .argument('[arg]', 'Profile ID (for switch/remove)') | ||
| .option('-u, --user <matrixId>', 'Matrix user ID (e.g., @user:boxel.ai)') | ||
| .option('-p, --password <password>', 'Password (for add command)') | ||
| .option('-n, --name <displayName>', 'Display name (for add command)') | ||
| .action(async (subcommand?: string, arg?: string, options?: { user?: string; password?: string; name?: string }) => { | ||
| await profileCommand(subcommand, arg, options); | ||
| }); |
- Fix track.ts file filtering: use `filename !== '.realm.json'` instead of
`!filename.startsWith('.realm.json')`
- Add Windows compatibility check in stop.ts with helpful message
- Improve stop.ts process matching to handle both dev (tsx) and installed
(boxel, node) modes
- Add BOXEL_PASSWORD env var support in profile.ts for secure non-interactive
usage
- Fix touch.ts missing sync() method implementation
- Add profile manager integration to check.ts, create.ts, list.ts, skills.ts,
status.ts (was using old env var approach)
- Update docs to use `npx boxel` as default command pattern
- Add security notes about avoiding `-p` flag for passwords in shell history
- Add Claude Code note at top of README with link to claude.ai/code
- Streamline installation section in README
Co-Authored-By: Claude Opus 4.5 <[email protected]>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 15 out of 15 changed files in this pull request and generated 10 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Co-authored-by: christse <[email protected]>
…t detection Co-authored-by: christse <[email protected]>
Co-authored-by: christse <[email protected]>
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 <[email protected]>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 17 out of 17 changed files in this pull request and generated 12 comments.
Comments suppressed due to low confidence (1)
src/commands/create.ts:72
- The realm server URL derivation in the create command (lines 62-72) is redundant with the derivation already performed in
ProfileManager.getActiveCredentials(). SincebaseRealmServerUrlcomes from credentials which already performs URL derivation, theif (!realmServerUrl)block should rarely execute. However, there's an inconsistency: the create command only handles two patterns (matrix- and matrix.) while ProfileManager handles three patterns (matrix., matrix-staging., and matrix-). Either remove this redundant derivation and rely on ProfileManager, or make the patterns consistent across both locations.
let realmServerUrl = baseRealmServerUrl;
if (!realmServerUrl) {
const matrixUrlObj = new URL(matrixUrl);
if (matrixUrlObj.hostname.startsWith('matrix-')) {
realmServerUrl = `${matrixUrlObj.protocol}//${matrixUrlObj.hostname.slice(7)}/`;
} else if (matrixUrlObj.hostname.startsWith('matrix.')) {
realmServerUrl = `${matrixUrlObj.protocol}//app.${matrixUrlObj.hostname.slice(7)}/`;
} else {
console.error('Could not derive realm server URL. Set REALM_SERVER_URL.');
process.exit(1);
}
}
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Fix manual checkpoint creation to detect actual workspace changes
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 18 out of 18 changed files in this pull request and generated 4 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const username = await prompt('Username: '); | ||
|
|
||
| if (!username) { | ||
| console.error(`${FG_RED}Error:${RESET} Username is required.`); | ||
| process.exit(1); | ||
| } | ||
|
|
There was a problem hiding this comment.
The username input validation only checks if it's empty (line 202-205), but doesn't validate that it contains valid characters for a Matrix ID. Matrix usernames have specific character restrictions (typically lowercase letters, numbers, hyphens, underscores, periods).
If a user enters invalid characters (like spaces, uppercase letters, or special characters), the resulting Matrix ID @${username}:${domain} may be invalid and authentication will fail with a confusing error.
Consider adding validation for the username format:
- Check for invalid characters
- Suggest converting to lowercase
- Provide helpful error messages about what characters are allowed
This would improve the user experience by catching errors early during profile creation.
| const username = await prompt('Username: '); | |
| if (!username) { | |
| console.error(`${FG_RED}Error:${RESET} Username is required.`); | |
| process.exit(1); | |
| } | |
| const rawUsername = await prompt('Username: '); | |
| const username = rawUsername.trim(); | |
| if (!username) { | |
| console.error(`${FG_RED}Error:${RESET} Username is required.`); | |
| process.exit(1); | |
| } | |
| // Matrix usernames must be lowercase and may contain a–z, 0–9, '-', '_', and '.' | |
| const usernamePattern = /^[a-z0-9._-]+$/; | |
| if (!usernamePattern.test(username)) { | |
| console.error( | |
| `${FG_RED}Error:${RESET} Invalid username. Usernames must be lowercase and may only contain ` + | |
| `letters a–z, numbers 0–9, hyphens (-), underscores (_), and periods (.).` | |
| ); | |
| process.exit(1); | |
| } |
| const result = execSync( | ||
| `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'`, | ||
| { encoding: 'utf-8' } | ||
| ).trim(); |
There was a problem hiding this comment.
The regex pattern for finding boxel processes could potentially match unintended processes. For example, if a user has a file or directory named "boxel" in their path, or if there's another tool with "boxel" in its name, this could incorrectly match those processes. The pattern [[:space:]]boxel[[:space:]] helps but may still match false positives.
Consider adding additional safety checks:
- Verify the process is actually a node/tsx process
- Check that the executable path contains the expected boxel CLI location
- Add a dry-run mode to show what would be killed before actually killing processes
This would make the command safer and more predictable.
| for (const line of statusOutput.split('\n')) { | ||
| if (!line) continue; | ||
|
|
||
| const statusCode = line.substring(0, 2); | ||
| let file = line.substring(3); |
There was a problem hiding this comment.
The git status parsing uses line.substring(0, 2) and line.substring(3) to extract the status code and filename. This assumes the git status format is always consistent, but git status can have variations:
- If a file has spaces in its name and is quoted by git (e.g.,
"file name.txt"), the substring(3) will include the quotes - For renamed files with
->, the parsing handles this, but the substring index could be off if git quotes the filenames
Consider using git status --porcelain=v1 (explicit version) and handling quoted filenames properly by checking if the filename starts with a quote and parsing accordingly. Git uses C-style quoting for filenames with special characters.
This is a minor edge case but could cause issues with files that have unusual names.
| /** | ||
| * Scan workspace directory to build a changes array for manual checkpoints. | ||
| * Marks all current files as 'modified' since we're snapshotting the current state. | ||
| */ | ||
| function scanWorkspaceForChanges(workspaceDir: string): CheckpointChange[] { | ||
| const changes: CheckpointChange[] = []; | ||
|
|
||
| const scan = (dir: string, prefix = '') => { | ||
| if (!fs.existsSync(dir)) return; | ||
|
|
||
| const entries = fs.readdirSync(dir, { withFileTypes: true }); | ||
| for (const entry of entries) { | ||
| // Skip internal files | ||
| if (entry.name.startsWith('.boxel-') || entry.name === '.git') continue; | ||
| if (entry.name.startsWith('.') && entry.name !== '.realm.json') continue; | ||
|
|
||
| const fullPath = path.join(dir, entry.name); | ||
| const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name; | ||
|
|
||
| if (entry.isDirectory()) { | ||
| scan(fullPath, relativePath); | ||
| } else { | ||
| changes.push({ file: relativePath, status: 'modified' }); | ||
| } | ||
| } | ||
| }; | ||
|
|
||
| scan(workspaceDir); | ||
| return changes; | ||
| } | ||
|
|
There was a problem hiding this comment.
Unused function scanWorkspaceForChanges.
| /** | |
| * Scan workspace directory to build a changes array for manual checkpoints. | |
| * Marks all current files as 'modified' since we're snapshotting the current state. | |
| */ | |
| function scanWorkspaceForChanges(workspaceDir: string): CheckpointChange[] { | |
| const changes: CheckpointChange[] = []; | |
| const scan = (dir: string, prefix = '') => { | |
| if (!fs.existsSync(dir)) return; | |
| const entries = fs.readdirSync(dir, { withFileTypes: true }); | |
| for (const entry of entries) { | |
| // Skip internal files | |
| if (entry.name.startsWith('.boxel-') || entry.name === '.git') continue; | |
| if (entry.name.startsWith('.') && entry.name !== '.realm.json') continue; | |
| const fullPath = path.join(dir, entry.name); | |
| const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name; | |
| if (entry.isDirectory()) { | |
| scan(fullPath, relativePath); | |
| } else { | |
| changes.push({ file: relativePath, status: 'modified' }); | |
| } | |
| } | |
| }; | |
| scan(workspaceDir); | |
| return changes; | |
| } |
Six fixes in one commit, addressing every Copilot comment except #7 (dead code, deferred to a later cleanup pass). #4 — manifest shape mismatch (critical): push.ts was still reading/writing the old manifest format (files[path] = hashString) while pull.ts and sync.ts use the new {localHash, remoteMtime} shape. push now uses the new shape, migrates old manifests on read (mirrors sync.ts detector), and refreshes remoteMtime via getRemoteMtimes() after a successful upload so the next pull/sync sees a consistent picture. #3 — partial-success manifest (important): In --batch mode we were marking every instance as synced when result.uploaded > 0, even if some failed. Now we derive failed paths from result.errors and only add successes to the manifest. Failed uploads stay out and get retried on the next run. #6 — ATOMIC_SOURCE_EXTENSIONS gap (important): EXTENSION_MAP was missing .tsx/.jsx/.cjs/.scss/.less/.sass, so those extensions got application/octet-stream and were routed through individual POST instead of /_atomic despite being listed as atomic-source compatible. Now mapped to application/typescript/javascript and text/x-* respectively, so isTextFile() returns true and they take the atomic path. #2 — CLI --version out of sync: program.version() was hardcoded to '1.0.0' while package.json is at 1.0.1. Now reads from package.json via createRequire so boxel --version stays accurate on every release. #1 — --batch-size NaN guard: parseInt on bad input (e.g. "abc") returned NaN, which bypasses `?? 10` and flows into the uploader as NaN. Now uses a parsePositiveInt parser that throws InvalidArgumentError with a friendly message on non-positive-integer input. #5 — invalid-JSON test expectation: The test expected data.type='file' but the code (correctly) emits 'source' because /_atomic only accepts 'card' and 'source' resource types. Test updated to match the correct contract. All 164 tests pass (was 163/164 before this commit). Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Six fixes in one commit, addressing every Copilot comment except #7 (dead code, deferred to a later cleanup pass). #4 — manifest shape mismatch (critical): push.ts was still reading/writing the old manifest format (files[path] = hashString) while pull.ts and sync.ts use the new {localHash, remoteMtime} shape. push now uses the new shape, migrates old manifests on read (mirrors sync.ts detector), and refreshes remoteMtime via getRemoteMtimes() after a successful upload so the next pull/sync sees a consistent picture. #3 — partial-success manifest (important): In --batch mode we were marking every instance as synced when result.uploaded > 0, even if some failed. Now we derive failed paths from result.errors and only add successes to the manifest. Failed uploads stay out and get retried on the next run. #6 — ATOMIC_SOURCE_EXTENSIONS gap (important): EXTENSION_MAP was missing .tsx/.jsx/.cjs/.scss/.less/.sass, so those extensions got application/octet-stream and were routed through individual POST instead of /_atomic despite being listed as atomic-source compatible. Now mapped to application/typescript/javascript and text/x-* respectively, so isTextFile() returns true and they take the atomic path. #2 — CLI --version out of sync: program.version() was hardcoded to '1.0.0' while package.json is at 1.0.1. Now reads from package.json via createRequire so boxel --version stays accurate on every release. #1 — --batch-size NaN guard: parseInt on bad input (e.g. "abc") returned NaN, which bypasses `?? 10` and flows into the uploader as NaN. Now uses a parsePositiveInt parser that throws InvalidArgumentError with a friendly message on non-positive-integer input. #5 — invalid-JSON test expectation: The test expected data.type='file' but the code (correctly) emits 'source' because /_atomic only accepts 'card' and 'source' resource types. Test updated to match the correct contract. All 164 tests pass (was 163/164 before this commit). Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
New features:
boxel track- Monitor local file changes with auto-checkpointingboxel stop- Stop all running watch and track processesboxel history -m "message"- Create checkpoint with custom messageSymbol system for teaching local vs remote:
Documentation: