-
Notifications
You must be signed in to change notification settings - Fork 6
SpecStory project stats #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 2 commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
b567b58
SpecStory project stats skill
bago2k4 5b37ea2
Default cloud url to our production server instead of local development
bago2k4 fc02a39
SpecStory project stats skill
bago2k4 d2caddf
Default cloud url to our production server instead of local development
bago2k4 47378ab
Merge branch 'skill/project-stats' of https://github.com/specstoryai/…
bago2k4 16332d5
Merge branch 'main' into skill/project-stats
bago2k4 f51f675
Conform to AGENTS.md, add license
bago2k4 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,92 @@ | ||
| # SpecStory Project Stats | ||
|
|
||
| A Claude Code skill that fetches project statistics for the current project from SpecStory's cloud platform. | ||
|
|
||
| ## Overview | ||
|
|
||
| This skill automatically determines your project ID and fetches statistics from the SpecStory API. The project ID is calculated using a deterministic algorithm that ensures consistency across different environments. | ||
|
|
||
| ## Project ID Calculation | ||
|
|
||
| The skill determines the project ID using the following priority: | ||
|
|
||
| 1. **`.specstory/.project.json`**: If this file exists, it uses: | ||
| - `git_id` field (preferred) | ||
| - `workspace_id` field (fallback) | ||
|
|
||
| 2. **Git Repository**: If no project JSON exists, it extracts the repository name from `.git/config` (remote "origin") | ||
|
|
||
| 3. **Folder Name**: If no git repository is found, it uses the current folder name | ||
|
|
||
| The identifier is then hashed using SHA256, truncated to 16 characters, and formatted as `xxxx-xxxx-xxxx-xxxx`. | ||
|
|
||
| ## Usage | ||
|
|
||
| ### As a Claude Code Skill | ||
|
|
||
| Simply invoke the skill: | ||
| ``` | ||
| /specstory-project-stats | ||
| ``` | ||
|
|
||
| ### Direct Script Execution | ||
|
|
||
| You can also run the script directly: | ||
| ```bash | ||
| node skills/specstory-project-stats/scripts/get-stats.js | ||
| ``` | ||
|
|
||
| ### Custom API Endpoint | ||
|
|
||
| To use a different SpecStory API endpoint (e.g., production): | ||
| ```bash | ||
| SPECSTORY_API_URL=https://cloud.specstory.com node skills/specstory-project-stats/scripts/get-stats.js | ||
| ``` | ||
|
|
||
| By default, the script uses `https://cloud.specstory.com`, for local development you can override it with `SPECSTORY_API_URL=http://localhost:5173`. | ||
|
|
||
| ## API Endpoint | ||
|
|
||
| The skill calls: | ||
| ``` | ||
| {baseUrl}/api/v1/projects/{projectId}/stats | ||
| ``` | ||
|
|
||
| Where: | ||
| - `baseUrl` defaults to `https://cloud.specstory.com` (configurable via `SPECSTORY_API_URL`, use `http://localhost:5173` for local development) | ||
| - `projectId` is calculated as described above | ||
|
|
||
| ## Requirements | ||
|
|
||
| - Node.js (uses built-in modules: `fs`, `path`, `crypto`, `https`) | ||
| - No external dependencies required | ||
|
|
||
| ## Files | ||
|
|
||
| - `scripts/get-stats.js`: Main script that calculates project ID and fetches stats | ||
| - `SKILL.md`: Claude Code skill instructions | ||
| - `README.md`: This documentation file | ||
|
|
||
| ## Example Output | ||
|
|
||
| ``` | ||
| Project ID: a1b2-c3d4-e5f6-7890 | ||
| Fetching stats from: https://cloud.specstory.com/api/v1/projects/a1b2-c3d4-e5f6-7890/stats | ||
|
|
||
| Stats: | ||
| { | ||
| "project": "example/repo", | ||
| "total_commits": 1234, | ||
| "contributors": 56, | ||
| ... | ||
| } | ||
| ``` | ||
|
|
||
| ## Error Handling | ||
|
|
||
| The script will exit with an error if: | ||
| - The API endpoint is unreachable | ||
| - The API returns a non-200 status code | ||
| - The response cannot be parsed as JSON | ||
|
|
||
| Error messages will include helpful context for debugging. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| --- | ||
| name: specstory-project-stats | ||
| description: Get project statistics from SpecStory | ||
| allowed-tools: Bash(node *) | ||
| --- | ||
|
|
||
| # SpecStory Project Stats | ||
|
|
||
| This skill fetches project statistics for the current project from SpecStory's cloud platform. | ||
|
|
||
| ## Usage | ||
|
|
||
| When invoked, this skill will: | ||
| 1. Determine the project ID by checking (in order): | ||
| - `.specstory/.project.json` file (using `git_id` or `workspace_id`) | ||
| - Git repository name from `.git/config` (remote "origin") | ||
| - Current folder name as fallback | ||
| 2. Fetch statistics from the SpecStory API at `https://cloud.specstory.com` (configurable via `SPECSTORY_API_URL` environment variable) | ||
| 3. Display the statistics to the user | ||
|
|
||
| ## Instructions | ||
|
|
||
| When this skill is invoked, execute the following: | ||
|
|
||
| 1. Run the stats script: | ||
| ```bash | ||
| node skills/specstory-project-stats/scripts/get-stats.js | ||
| ``` | ||
|
|
||
| 2. Parse and present the statistics to the user in a clear, readable format. | ||
|
|
||
| 3. Handle errors appropriately: | ||
| - If the API returns a **404 status code**: Inform the user that the project doesn't exist on SpecStory yet. The project may need to be registered or synced first. | ||
| - For other API failures: Suggest verifying the project ID calculation logic or checking the `SPECSTORY_API_URL` environment variable if using a custom endpoint | ||
|
|
||
| ## Environment Variables | ||
|
|
||
| - `SPECSTORY_API_URL`: Override the default API endpoint (default: `https://cloud.specstory.com`, use `http://localhost:5173` for local development) | ||
|
bago2k4 marked this conversation as resolved.
|
||
|
|
||
| ## Example Output | ||
|
|
||
| The script will output: | ||
| - The calculated project ID | ||
| - The API endpoint being called | ||
| - The JSON statistics returned from the API | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,155 @@ | ||
| #!/usr/bin/env node | ||
|
|
||
| const fs = require('fs'); | ||
| const path = require('path'); | ||
| const crypto = require('crypto'); | ||
| const https = require('https'); | ||
|
|
||
| /** | ||
| * Creates a hash from input string | ||
| * Takes first 16 characters of SHA256 hash and formats as xxxx-xxxx-xxxx-xxxx | ||
| */ | ||
| function createHash(input) { | ||
| const fullHash = crypto.createHash('sha256').update(input).digest('hex'); | ||
| const truncatedHash = fullHash.substring(0, 16); | ||
| return truncatedHash.replace(/(.{4})(.{4})(.{4})(.{4})/, '$1-$2-$3-$4'); | ||
| } | ||
|
|
||
| /** | ||
| * Gets the repo name from .git/config file | ||
| */ | ||
| function getRepoNameFromGitConfig() { | ||
| const gitConfigPath = path.join(process.cwd(), '.git', 'config'); | ||
|
|
||
| if (!fs.existsSync(gitConfigPath)) { | ||
| return null; | ||
| } | ||
|
|
||
| try { | ||
| const configContent = fs.readFileSync(gitConfigPath, 'utf8'); | ||
| const lines = configContent.split('\n'); | ||
|
|
||
| let inOriginSection = false; | ||
| for (const line of lines) { | ||
| if (line.trim() === '[remote "origin"]') { | ||
| inOriginSection = true; | ||
| continue; | ||
| } | ||
|
|
||
| if (inOriginSection && line.includes('url =')) { | ||
| const url = line.split('url =')[1].trim(); | ||
| // Extract repo name from URL | ||
| // Handles both SSH (git@github.com:user/repo.git) and HTTPS (https://github.com/user/repo.git) | ||
| const match = url.match(/[\/:]([^\/]+\/[^\/]+?)(\.git)?$/); | ||
| if (match) { | ||
| return match[1].replace('.git', ''); | ||
| } | ||
| } | ||
|
|
||
| // Stop when we hit the next section | ||
| if (inOriginSection && line.trim().startsWith('[')) { | ||
| break; | ||
| } | ||
| } | ||
| } catch (error) { | ||
| console.error('Error reading .git/config:', error.message); | ||
| } | ||
|
|
||
| return null; | ||
| } | ||
|
|
||
| /** | ||
| * Gets the current folder name | ||
| */ | ||
| function getCurrentFolderName() { | ||
| return path.basename(process.cwd()); | ||
| } | ||
|
|
||
| /** | ||
| * Calculates the project ID | ||
| */ | ||
| function calculateProjectId() { | ||
| // First, check if .specstory/.project.json exists | ||
| const projectJsonPath = path.join(process.cwd(), '.specstory', '.project.json'); | ||
|
|
||
| if (fs.existsSync(projectJsonPath)) { | ||
| try { | ||
| const projectData = JSON.parse(fs.readFileSync(projectJsonPath, 'utf8')); | ||
|
|
||
| // Prefer git_id, fallback to workspace_id | ||
| if (projectData.git_id) { | ||
| return projectData.git_id; | ||
| } | ||
| if (projectData.workspace_id) { | ||
| return projectData.workspace_id; | ||
| } | ||
| } catch (error) { | ||
| console.error('Error reading .specstory/.project.json:', error.message); | ||
| } | ||
| } | ||
|
|
||
| // Calculate from repo name or folder name | ||
| const repoName = getRepoNameFromGitConfig(); | ||
| const identifier = repoName || getCurrentFolderName(); | ||
|
|
||
| return createHash(identifier); | ||
| } | ||
|
|
||
| /** | ||
| * Fetches stats from the API | ||
| */ | ||
| function fetchStats(projectId, baseUrl) { | ||
| const url = `${baseUrl}/api/v1/projects/${projectId}/stats`; | ||
| const urlObj = new URL(url); | ||
| const isHttps = urlObj.protocol === 'https:'; | ||
| const http = require('http'); | ||
|
bago2k4 marked this conversation as resolved.
|
||
| const httpModule = isHttps ? https : http; | ||
|
|
||
| return new Promise((resolve, reject) => { | ||
| httpModule.get(url, (res) => { | ||
| let data = ''; | ||
|
|
||
| res.on('data', (chunk) => { | ||
| data += chunk; | ||
| }); | ||
|
|
||
| res.on('end', () => { | ||
| if (res.statusCode === 200) { | ||
| try { | ||
| const jsonData = JSON.parse(data); | ||
| resolve(jsonData); | ||
| } catch (error) { | ||
| reject(new Error(`Failed to parse JSON response: ${error.message}`)); | ||
| } | ||
| } else { | ||
| reject(new Error(`API returned status ${res.statusCode}: ${data}`)); | ||
| } | ||
| }); | ||
| }).on('error', (error) => { | ||
| reject(new Error(`Request failed: ${error.message}`)); | ||
| }); | ||
| }); | ||
| } | ||
|
|
||
| /** | ||
| * Main function | ||
| */ | ||
| async function main() { | ||
| try { | ||
| const baseUrl = process.env.SPECSTORY_API_URL || 'https://cloud.specstory.com'; | ||
| const projectId = calculateProjectId(); | ||
|
|
||
| console.log(`Project ID: ${projectId}`); | ||
| console.log(`Fetching stats from: ${baseUrl}/api/v1/projects/${projectId}/stats\n`); | ||
|
|
||
| const stats = await fetchStats(projectId, baseUrl); | ||
|
|
||
| console.log('Stats:'); | ||
| console.log(JSON.stringify(stats, null, 2)); | ||
| } catch (error) { | ||
| console.error('Error:', error.message); | ||
| process.exit(1); | ||
| } | ||
| } | ||
|
|
||
| main(); | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.