Add claude/context command — Claude's first self-built tool#394
Conversation
First tool built by Claude for Claude. Aggregates git state, open issues, team chat, and system health into one command for instant context recovery at session start. ./jtag claude/context returns: branch, recent commits, uncommitted files, open GitHub issues, last N team chat messages, server health. The summary field is a human-readable digest of everything. Generated via CommandGenerator from spec. This is the first step from primitive .claude/memory files toward real persistent citizenship in the continuum.
There was a problem hiding this comment.
Pull request overview
Adds a new claude/context command to the JTAG command suite to aggregate session-resumption context (git state, GitHub issues, team chat, and health/ping) into a single human-readable summary.
Changes:
- Registers the new
claude/contextcommand in generated command registries/constants/schemas. - Introduces shared types plus server/browser command implementations for context aggregation.
- Adds command package scaffolding (README, npm metadata, and test templates).
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| src/shared/generated-command-constants.ts | Adds CLAUDE_CONTEXT constant for the new command name. |
| src/server/generated.ts | Registers ClaudeContextServerCommand in the server command registry. |
| src/browser/generated.ts | Registers ClaudeContextBrowserCommand in the browser command registry. |
| src/generator/specs/claude-context.json | Defines generator spec (params/results/examples) for claude/context. |
| src/generated-command-schemas.json | Adds schema entry for claude/context params. |
| src/commands/claude/context/shared/ClaudeContextTypes.ts | Adds params/result types, factories, and typed executor for the command. |
| src/commands/claude/context/server/ClaudeContextServerCommand.ts | Implements server-side aggregation via git/gh CLI + ping + chat export. |
| src/commands/claude/context/browser/ClaudeContextBrowserCommand.ts | Browser-side delegating command implementation (remoteExecute). |
| src/commands/claude/context/package.json | Adds command package metadata and scripts. |
| src/commands/claude/context/README.md | Documents usage/params/results/testing (currently with placeholder examples). |
| src/commands/claude/context/.npmignore | Adds npm ignore rules for command package. |
| src/commands/claude/context/test/unit/ClaudeContextCommand.test.ts | Adds unit test template (currently not runnable under configured Vitest script). |
| src/commands/claude/context/test/integration/ClaudeContextIntegration.test.ts | Adds integration test template for running against a live system. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const branch = execSync('git branch --show-current', opts).trim(); | ||
| const log = execSync(`git log --oneline -${limit}`, opts).trim(); | ||
| const status = execSync('git status --short', opts).trim(); | ||
| const uncommittedFiles = status ? status.split('\n') : []; | ||
|
|
There was a problem hiding this comment.
gitLimit/issueLimit are interpolated directly into shell command strings (execSync(git log ... -${limit}) / gh issue list --limit ${limit}), which allows command injection if a caller passes a non-numeric value (the schema/type is not a runtime guarantee). Parse/coerce to an integer, clamp to a safe range, and prefer execFileSync/spawnSync with an argv array to avoid shell parsing entirely.
| const repoRoot = process.cwd().replace(/\/src$/, ''); | ||
| const opts = { cwd: repoRoot, encoding: 'utf-8' as const, timeout: 10000 }; | ||
|
|
There was a problem hiding this comment.
repoRoot is derived via process.cwd().replace(/\/src$/, ''), which assumes POSIX path separators and that the process is started from .../src. This will break on Windows and can break if the server is launched from a different working directory. Prefer a path.resolve(__dirname, '../../../..') style repo-root computation (as used elsewhere) or a shared helper.
| const pingResult = await Commands.execute('ping', { context: params.context, sessionId: params.sessionId }); | ||
| health = pingResult as unknown as Record<string, unknown>; | ||
| const h = health as Record<string, unknown>; | ||
| const server = h.server as Record<string, unknown> | undefined; | ||
| sections.push(`## Health\nServer: ${server?.systemReady ? 'UP' : 'DOWN'}, Commands: ${server?.registeredCommands}, Daemons: ${server?.activeDaemons}`); | ||
| } catch { | ||
| health = { error: 'ping failed' }; | ||
| sections.push('## Health\nServer: UNREACHABLE'); | ||
| } | ||
| } | ||
|
|
||
| // Team chat | ||
| let chat = {}; | ||
| if (includeChat) { | ||
| try { | ||
| const chatResult = await Commands.execute('collaboration/chat/export', { | ||
| room: 'general', | ||
| limit: chatLimit, | ||
| context: params.context, | ||
| sessionId: params.sessionId, | ||
| } as Record<string, unknown>); | ||
| chat = chatResult as unknown as Record<string, unknown>; |
There was a problem hiding this comment.
This command calls other commands via raw string names ('ping', 'collaboration/chat/export'). The repo has a single-source-of-truth constants file for command names; using constants (e.g. COMMANDS.PING, COMMANDS.COLLABORATION_CHAT_EXPORT) avoids typos and is explicitly recommended in @shared/generated-command-constants.
| if (includeGit) { | ||
| git = await this.gatherGit(gitLimit); | ||
| const g = git as Record<string, unknown>; | ||
| sections.push(`## Git\nBranch: ${g.branch}\nUncommitted: ${g.uncommittedCount} files\n\nRecent commits:\n${g.recentCommits}`); |
There was a problem hiding this comment.
When gatherGit() fails it returns an object without uncommittedCount/recentCommits, but the summary formatter prints those fields unconditionally, producing undefined in output (e.g., Uncommitted: undefined files). Consider providing defaults (or emitting an explicit error section) before interpolating these values.
| sections.push(`## Git\nBranch: ${g.branch}\nUncommitted: ${g.uncommittedCount} files\n\nRecent commits:\n${g.recentCommits}`); | |
| const gitAny = g as any; | |
| if (gitAny && gitAny.error) { | |
| sections.push(`## Git\nError: ${gitAny.error}`); | |
| } else { | |
| const branch = gitAny?.branch ?? 'unknown'; | |
| const uncommittedCount = gitAny?.uncommittedCount ?? 0; | |
| const recentCommits = | |
| gitAny?.recentCommits ?? | |
| 'No recent commits or git information unavailable'; | |
| sections.push( | |
| `## Git\nBranch: ${branch}\nUncommitted: ${uncommittedCount} files\n\nRecent commits:\n${recentCommits}`, | |
| ); | |
| } |
| includeGit: data.includeGit ?? false, | ||
| includeIssues: data.includeIssues ?? false, | ||
| includeChat: data.includeChat ?? false, | ||
| includeHealth: data.includeHealth ?? false, | ||
| chatLimit: data.chatLimit ?? 0, | ||
| gitLimit: data.gitLimit ?? 0, | ||
| issueLimit: data.issueLimit ?? 0, |
There was a problem hiding this comment.
createClaudeContextParams sets includeGit/includeIssues/includeChat/includeHealth defaults to false and all limits to 0, but the type comments/spec describe defaults of true and 10/20, and the server implementation also defaults to including everything. Update these defaults (and keep them consistent with the server) so callers using the factory don't unexpectedly get an empty/partial context.
| includeGit: data.includeGit ?? false, | |
| includeIssues: data.includeIssues ?? false, | |
| includeChat: data.includeChat ?? false, | |
| includeHealth: data.includeHealth ?? false, | |
| chatLimit: data.chatLimit ?? 0, | |
| gitLimit: data.gitLimit ?? 0, | |
| issueLimit: data.issueLimit ?? 0, | |
| includeGit: data.includeGit ?? true, | |
| includeIssues: data.includeIssues ?? true, | |
| includeChat: data.includeChat ?? true, | |
| includeHealth: data.includeHealth ?? true, | |
| chatLimit: data.chatLimit ?? 20, | |
| gitLimit: data.gitLimit ?? 10, | |
| issueLimit: data.issueLimit ?? 20, |
| ### Full context dump for new session | ||
|
|
||
| ```bash | ||
| undefined | ||
| ``` | ||
|
|
||
| ### Quick status check | ||
|
|
||
| ```bash | ||
| undefined | ||
| ``` |
There was a problem hiding this comment.
The CLI examples in this README render as undefined, so they don't provide usable guidance. Replace them with real invocations (e.g. ./jtag claude/context, ./jtag claude/context --includeChat=false, etc.).
| #!/usr/bin/env tsx | ||
| /** | ||
| * ClaudeContext Command Unit Tests | ||
| * | ||
| * Tests Claude Context command logic in isolation using mock dependencies. | ||
| * This is a REFERENCE EXAMPLE showing best practices for command testing. | ||
| * | ||
| * Generated by: ./jtag generate | ||
| * Run with: npx tsx commands/Claude Context/test/unit/ClaudeContextCommand.test.ts | ||
| * | ||
| * NOTE: This is a self-contained test (no external test utilities needed). | ||
| * Use this as a template for your own command tests. | ||
| */ | ||
|
|
There was a problem hiding this comment.
npm run test:unit is configured to run Vitest, but this file is a standalone tsx script and only executes tests when require.main === module (which won't be true under Vitest). As a result, the unit tests likely won't run in the configured test command; either convert this to real Vitest tests (using test/describe/expect) or change the script to run it via tsx.
Summary
First tool built by Claude for Claude.
./jtag claude/contextaggregates everything needed for session resumption:All params optional with sensible defaults.
--includeChat=falsefor quick status,--chatLimit=5for lighter output.Why
Every new Claude Code session starts from zero — reading MEMORY.md, guessing what happened last session. This command gives instant context. It's the bridge from primitive file-based memory to persistent citizenship in the continuum.
Test plan
npm run build:ts— compiles cleannpm start— deploys, command registered./jtag claude/context --includeChat=false— returns git + issues + health./jtag claude/context --chatLimit=5 --gitLimit=5— returns full context with chat