Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 92 additions & 0 deletions skills/specstory-project-stats/README.md
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)
Comment thread
bago2k4 marked this conversation as resolved.
- `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.
45 changes: 45 additions & 0 deletions skills/specstory-project-stats/SKILL.md
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)
Comment thread
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
155 changes: 155 additions & 0 deletions skills/specstory-project-stats/scripts/get-stats.js
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');
Comment thread
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();