@@ -1,35 +1,73 @@
|
||||
# Mosaic Stack Skills
|
||||
|
||||
This directory contains Clawdbot skills for integrating with Mosaic Stack APIs.
|
||||
Clawdbot skills for integrating with Mosaic Stack APIs.
|
||||
|
||||
## Skills
|
||||
## Available Skills
|
||||
|
||||
### Brain (`brain/`)
|
||||
Quick capture and semantic search for ideas and brain dumps.
|
||||
|
||||
**Features:**
|
||||
- Rapid brain dump capture
|
||||
- Semantic search across ideas
|
||||
- Tag management
|
||||
- Full CRUD operations
|
||||
|
||||
**Usage:** See `brain/SKILL.md` for documentation.
|
||||
|
||||
### Calendar (`calendar/`)
|
||||
|
||||
Integration with Mosaic Stack's Events API for calendar management.
|
||||
|
||||
**Features:**
|
||||
- Create events with rich metadata
|
||||
- Query events with flexible filtering
|
||||
- Update and reschedule events
|
||||
- Delete/cancel events
|
||||
- Natural language interaction support
|
||||
|
||||
**Usage:** See `calendar/SKILL.md` for complete documentation.
|
||||
**Usage:** See `calendar/SKILL.md` for documentation.
|
||||
|
||||
### Tasks (`tasks/`)
|
||||
Task management integration with Mosaic Stack's Tasks API.
|
||||
|
||||
**Features:**
|
||||
- Create and manage tasks
|
||||
- Filter by status, priority, project
|
||||
- Track overdue tasks
|
||||
- Subtask support
|
||||
|
||||
**Usage:** See `tasks/SKILL.md` for documentation.
|
||||
|
||||
### Gantt (`gantt/`)
|
||||
Query and analyze project timelines from Mosaic Stack's Projects API.
|
||||
|
||||
**Features:**
|
||||
- Query project timelines and task lists
|
||||
- Check task dependencies and blocking relationships
|
||||
- Identify critical path items
|
||||
- PDA-friendly language
|
||||
|
||||
**Usage:** See `gantt/SKILL.md` for documentation.
|
||||
|
||||
## Installation
|
||||
|
||||
Skills can be installed individually to Clawdbot:
|
||||
|
||||
```bash
|
||||
# Example: Install brain skill
|
||||
cp -r packages/skills/brain ~/.clawdbot/skills/mosaic-brain
|
||||
```
|
||||
|
||||
## Skill Structure
|
||||
|
||||
Each skill follows Clawdbot skill conventions:
|
||||
Each skill follows Clawdbot conventions:
|
||||
|
||||
```
|
||||
skill-name/
|
||||
├── SKILL.md # Skill documentation and metadata
|
||||
└── scripts/ # Executable tools for skill functionality
|
||||
├── scripts/ # Executable tools (optional)
|
||||
└── README.md # User documentation (optional)
|
||||
```
|
||||
|
||||
## Adding New Skills
|
||||
## License
|
||||
|
||||
1. Create a new directory under `packages/skills/`
|
||||
2. Add `SKILL.md` with YAML frontmatter and documentation
|
||||
3. Add any necessary scripts or resources
|
||||
4. Update this README
|
||||
MIT
|
||||
|
||||
28
packages/skills/gantt/.claude-plugin/plugin.json
Normal file
28
packages/skills/gantt/.claude-plugin/plugin.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "mosaic-plugin-gantt",
|
||||
"description": "Query and analyze project timelines, task dependencies, and schedules from Mosaic Stack's Gantt/Project API",
|
||||
"version": "1.0.0",
|
||||
"author": {
|
||||
"name": "Mosaic Stack Team",
|
||||
"email": "support@mosaicstack.dev"
|
||||
},
|
||||
"skills": [
|
||||
"gantt"
|
||||
],
|
||||
"commands": [
|
||||
{
|
||||
"name": "gantt-api",
|
||||
"description": "API query helper for Mosaic Stack projects and tasks",
|
||||
"path": "../gantt-api.sh"
|
||||
}
|
||||
],
|
||||
"environment": {
|
||||
"required": [
|
||||
"MOSAIC_WORKSPACE_ID",
|
||||
"MOSAIC_API_TOKEN"
|
||||
],
|
||||
"optional": [
|
||||
"MOSAIC_API_URL"
|
||||
]
|
||||
}
|
||||
}
|
||||
5
packages/skills/gantt/.gitignore
vendored
Normal file
5
packages/skills/gantt/.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
node_modules/
|
||||
dist/
|
||||
*.log
|
||||
.env
|
||||
.DS_Store
|
||||
21
packages/skills/gantt/LICENSE
Normal file
21
packages/skills/gantt/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Mosaic Stack Team
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
91
packages/skills/gantt/README.md
Normal file
91
packages/skills/gantt/README.md
Normal file
@@ -0,0 +1,91 @@
|
||||
# Mosaic Stack Gantt Plugin
|
||||
|
||||
Clawdbot skill for querying and analyzing project timelines, task dependencies, and schedules from Mosaic Stack's Gantt/Project API.
|
||||
|
||||
## Features
|
||||
|
||||
- Query project timelines and task lists
|
||||
- Check task dependencies and blocking relationships
|
||||
- Get project status overviews with statistics
|
||||
- Identify critical path items in projects
|
||||
- PDA-friendly language (supportive, non-judgmental)
|
||||
|
||||
## Installation
|
||||
|
||||
1. **Copy skill to Clawdbot plugins directory:**
|
||||
```bash
|
||||
cp -r ~/src/mosaic-stack-worktrees/feature-26-gantt-skill/packages/skills/gantt ~/.claude/plugins/mosaic-plugin-gantt
|
||||
```
|
||||
|
||||
2. **Set up environment variables:**
|
||||
|
||||
Add to your `.env` or shell profile:
|
||||
```bash
|
||||
export MOSAIC_API_URL="http://localhost:3000"
|
||||
export MOSAIC_WORKSPACE_ID="your-workspace-uuid"
|
||||
export MOSAIC_API_TOKEN="your-api-token"
|
||||
```
|
||||
|
||||
3. **Verify installation:**
|
||||
```bash
|
||||
~/.claude/plugins/mosaic-plugin-gantt/gantt-api.sh projects
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Via Clawdbot
|
||||
|
||||
Once installed, you can ask Clawdbot:
|
||||
|
||||
- "Show me the timeline for Project Alpha"
|
||||
- "What blocks task 'Implement Auth'?"
|
||||
- "What's the critical path for Q1 release?"
|
||||
- "Show all high-priority tasks due this week"
|
||||
- "Give me a status overview of Project Beta"
|
||||
|
||||
### Via Command Line
|
||||
|
||||
The `gantt-api.sh` helper script can be used directly:
|
||||
|
||||
```bash
|
||||
# List all projects
|
||||
./gantt-api.sh projects
|
||||
|
||||
# Get project details with tasks
|
||||
./gantt-api.sh project <project-id>
|
||||
|
||||
# Get tasks for a project
|
||||
./gantt-api.sh tasks <project-id>
|
||||
|
||||
# Get task details
|
||||
./gantt-api.sh task <task-id>
|
||||
|
||||
# Get dependency chain for a task
|
||||
./gantt-api.sh dependencies <task-id>
|
||||
|
||||
# Calculate critical path for a project
|
||||
./gantt-api.sh critical-path <project-id>
|
||||
|
||||
# Find tasks by status
|
||||
./gantt-api.sh status IN_PROGRESS
|
||||
./gantt-api.sh status COMPLETED <project-id>
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### Endpoints
|
||||
|
||||
- `GET /projects` - List projects (paginated)
|
||||
- `GET /projects/:id` - Get project with tasks
|
||||
- `GET /tasks` - List tasks with filters
|
||||
- Query params: `projectId`, `status`, `priority`, `assigneeId`, `page`, `limit`
|
||||
|
||||
### Authentication
|
||||
|
||||
All requests require headers:
|
||||
- `X-Workspace-Id`: Workspace UUID
|
||||
- `Authorization`: Bearer {token}
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
143
packages/skills/gantt/SKILL.md
Normal file
143
packages/skills/gantt/SKILL.md
Normal file
@@ -0,0 +1,143 @@
|
||||
---
|
||||
name: mosaic-plugin-gantt
|
||||
description: Query and analyze project timelines, task dependencies, and schedules from Mosaic Stack's Gantt/Project API
|
||||
license: MIT
|
||||
---
|
||||
|
||||
This skill enables querying project timelines, task dependencies, critical path analysis, and project status from Mosaic Stack's API.
|
||||
|
||||
## Overview
|
||||
|
||||
The Mosaic Stack provides project management capabilities with support for:
|
||||
- Project timelines with start/end dates
|
||||
- Task dependencies and scheduling
|
||||
- Task status tracking (NOT_STARTED, IN_PROGRESS, PAUSED, COMPLETED, ARCHIVED)
|
||||
- Priority levels (LOW, MEDIUM, HIGH, URGENT)
|
||||
- Project status (PLANNING, ACTIVE, ON_HOLD, COMPLETED, ARCHIVED)
|
||||
|
||||
## API Endpoints
|
||||
|
||||
**Base URL**: Configured via `MOSAIC_API_URL` environment variable (default: `http://localhost:3000`)
|
||||
|
||||
### Projects
|
||||
- `GET /projects` - List all projects with pagination
|
||||
- `GET /projects/:id` - Get project details with tasks
|
||||
|
||||
### Tasks
|
||||
- `GET /tasks` - List all tasks with optional filters
|
||||
- Query params: `projectId`, `status`, `priority`, `assigneeId`, `page`, `limit`
|
||||
|
||||
## Authentication
|
||||
|
||||
Requests require authentication headers:
|
||||
- `X-Workspace-Id`: Workspace UUID (from `MOSAIC_WORKSPACE_ID` env var)
|
||||
- `Authorization`: Bearer token (from `MOSAIC_API_TOKEN` env var)
|
||||
|
||||
## Trigger Phrases & Examples
|
||||
|
||||
**Query project timeline:**
|
||||
- "Show me the timeline for [project name]"
|
||||
- "What's the status of [project]?"
|
||||
- "Give me an overview of Project Alpha"
|
||||
|
||||
**Check task dependencies:**
|
||||
- "What blocks task [task name]?"
|
||||
- "What are the dependencies for [task]?"
|
||||
- "Show me what's blocking [task]"
|
||||
|
||||
**Project status overview:**
|
||||
- "Project status for [project]"
|
||||
- "How is [project] doing?"
|
||||
- "Summary of [project]"
|
||||
|
||||
**Identify critical path:**
|
||||
- "Find the critical path for [project]"
|
||||
- "What's the critical path?"
|
||||
- "Show me blockers for [project]"
|
||||
- "Which tasks can't be delayed in [project]?"
|
||||
|
||||
**Find upcoming deadlines:**
|
||||
- "What tasks are due soon in [project]?"
|
||||
- "Show me tasks approaching deadline"
|
||||
- "What's due this week?"
|
||||
|
||||
## Data Models
|
||||
|
||||
### Project
|
||||
```typescript
|
||||
{
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
status: 'PLANNING' | 'ACTIVE' | 'ON_HOLD' | 'COMPLETED' | 'ARCHIVED';
|
||||
startDate?: Date;
|
||||
endDate?: Date;
|
||||
color?: string;
|
||||
tasks?: Task[];
|
||||
_count: { tasks: number; events: number };
|
||||
}
|
||||
```
|
||||
|
||||
### Task
|
||||
```typescript
|
||||
{
|
||||
id: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
status: 'NOT_STARTED' | 'IN_PROGRESS' | 'PAUSED' | 'COMPLETED' | 'ARCHIVED';
|
||||
priority: 'LOW' | 'MEDIUM' | 'HIGH' | 'URGENT';
|
||||
dueDate?: Date;
|
||||
completedAt?: Date;
|
||||
projectId?: string;
|
||||
assigneeId?: string;
|
||||
metadata: {
|
||||
startDate?: Date;
|
||||
dependencies?: string[]; // Array of task IDs
|
||||
[key: string]: any;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## Helper Scripts
|
||||
|
||||
Use `gantt-api.sh` for API queries:
|
||||
|
||||
```bash
|
||||
# List all projects
|
||||
./gantt-api.sh projects
|
||||
|
||||
# Get project by ID
|
||||
./gantt-api.sh project <project-id>
|
||||
|
||||
# Get tasks for a project
|
||||
./gantt-api.sh tasks <project-id>
|
||||
|
||||
# Find critical path
|
||||
./gantt-api.sh critical-path <project-id>
|
||||
|
||||
# Get dependency chain for a task
|
||||
./gantt-api.sh dependencies <task-id>
|
||||
```
|
||||
|
||||
## PDA-Friendly Language
|
||||
|
||||
When presenting information, use supportive, non-judgmental language:
|
||||
- **"Target passed"** instead of "OVERDUE" or "LATE"
|
||||
- **"Approaching target"** for near-deadline tasks
|
||||
- **"Paused"** not "BLOCKED" or "STUCK"
|
||||
- Focus on what's accomplished and what's next, not what's behind
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
1. **Environment Setup**: Ensure `.env` has required variables:
|
||||
- `MOSAIC_API_URL` - API base URL (e.g., `http://localhost:3000`, no `/api` suffix)
|
||||
- `MOSAIC_WORKSPACE_ID` - Workspace UUID
|
||||
- `MOSAIC_API_TOKEN` - Authentication token
|
||||
|
||||
2. **Error Handling**: API returns 401 for auth errors, 404 for not found
|
||||
|
||||
3. **Pagination**: Projects and tasks are paginated (default 50 items/page)
|
||||
|
||||
4. **Dependencies**: Task dependencies are stored in `metadata.dependencies` as array of task IDs
|
||||
|
||||
5. **Date Handling**: Start dates may be in `metadata.startDate` or fall back to `createdAt`
|
||||
58
packages/skills/gantt/examples/critical-path.ts
Normal file
58
packages/skills/gantt/examples/critical-path.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* Example: Calculate and display critical path for a project
|
||||
*
|
||||
* Usage:
|
||||
* npx tsx examples/critical-path.ts <project-id>
|
||||
*/
|
||||
|
||||
import { createGanttClientFromEnv } from '../index.js';
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const projectId = process.argv[2];
|
||||
|
||||
if (!projectId) {
|
||||
console.error('Usage: npx tsx examples/critical-path.ts <project-id>');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const client = createGanttClientFromEnv();
|
||||
|
||||
console.log(`Calculating critical path for project ${projectId}...\n`);
|
||||
|
||||
const criticalPath = await client.calculateCriticalPath(projectId);
|
||||
|
||||
console.log(`Critical Path (${criticalPath.totalDuration} days):`);
|
||||
console.log('='.repeat(50));
|
||||
|
||||
for (const item of criticalPath.path) {
|
||||
const statusIcon = item.task.status === 'COMPLETED' ? '✓' :
|
||||
item.task.status === 'IN_PROGRESS' ? '⊙' : '□';
|
||||
console.log(`${statusIcon} ${item.task.title}`);
|
||||
console.log(` Duration: ${item.duration} days`);
|
||||
console.log(` Cumulative: ${item.cumulativeDuration} days`);
|
||||
console.log(` Status: ${item.task.status}`);
|
||||
|
||||
if (item.task.metadata.dependencies && item.task.metadata.dependencies.length > 0) {
|
||||
console.log(` Depends on: ${item.task.metadata.dependencies.length} task(s)`);
|
||||
}
|
||||
|
||||
console.log('');
|
||||
}
|
||||
|
||||
if (criticalPath.nonCriticalTasks.length > 0) {
|
||||
console.log('\nNon-Critical Tasks (can be delayed):');
|
||||
console.log('='.repeat(50));
|
||||
|
||||
for (const item of criticalPath.nonCriticalTasks.sort((a, b) => a.slack - b.slack)) {
|
||||
console.log(`- ${item.task.title}`);
|
||||
console.log(` Slack: ${item.slack} days`);
|
||||
console.log(` Status: ${item.task.status}`);
|
||||
console.log('');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(error => {
|
||||
console.error('Error:', error.message);
|
||||
process.exit(1);
|
||||
});
|
||||
69
packages/skills/gantt/examples/query-timeline.ts
Normal file
69
packages/skills/gantt/examples/query-timeline.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* Example: Query project timeline and display statistics
|
||||
*
|
||||
* Usage:
|
||||
* npx tsx examples/query-timeline.ts <project-id>
|
||||
*/
|
||||
|
||||
import { createGanttClientFromEnv } from '../index.js';
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const projectId = process.argv[2];
|
||||
|
||||
if (!projectId) {
|
||||
console.error('Usage: npx tsx examples/query-timeline.ts <project-id>');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const client = createGanttClientFromEnv();
|
||||
|
||||
console.log(`Fetching timeline for project ${projectId}...\n`);
|
||||
|
||||
const timeline = await client.getProjectTimeline(projectId);
|
||||
|
||||
console.log(`Project: ${timeline.project.name}`);
|
||||
console.log(`Status: ${timeline.project.status}`);
|
||||
|
||||
if (timeline.project.startDate && timeline.project.endDate) {
|
||||
console.log(`Timeline: ${timeline.project.startDate} → ${timeline.project.endDate}`);
|
||||
}
|
||||
|
||||
console.log(`\nTasks (${timeline.stats.total} total):`);
|
||||
console.log(` ✓ Completed: ${timeline.stats.completed}`);
|
||||
console.log(` ⊙ In Progress: ${timeline.stats.inProgress}`);
|
||||
console.log(` □ Not Started: ${timeline.stats.notStarted}`);
|
||||
console.log(` ⏸ Paused: ${timeline.stats.paused}`);
|
||||
console.log(` ⚠ Target passed: ${timeline.stats.targetPassed}`);
|
||||
|
||||
console.log('\nTask List:');
|
||||
|
||||
const statusIcon = (status: string): string => {
|
||||
switch (status) {
|
||||
case 'COMPLETED': return '✓';
|
||||
case 'IN_PROGRESS': return '⊙';
|
||||
case 'PAUSED': return '⏸';
|
||||
case 'ARCHIVED': return '📦';
|
||||
default: return '□';
|
||||
}
|
||||
};
|
||||
|
||||
const now = new Date();
|
||||
|
||||
for (const task of timeline.tasks) {
|
||||
const icon = statusIcon(task.status);
|
||||
const dueInfo = task.dueDate
|
||||
? ` Due: ${task.dueDate}`
|
||||
: '';
|
||||
|
||||
const targetPassed = task.dueDate && new Date(task.dueDate) < now && task.status !== 'COMPLETED'
|
||||
? ' (target passed)'
|
||||
: '';
|
||||
|
||||
console.log(`${icon} ${task.title} [${task.status}]${dueInfo}${targetPassed}`);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(error => {
|
||||
console.error('Error:', error.message);
|
||||
process.exit(1);
|
||||
});
|
||||
205
packages/skills/gantt/gantt-api.sh
Executable file
205
packages/skills/gantt/gantt-api.sh
Executable file
@@ -0,0 +1,205 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# gantt-api.sh - Helper script for Mosaic Stack Gantt/Project API queries
|
||||
#
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Configuration from environment
|
||||
API_URL="${MOSAIC_API_URL:-http://localhost:3000}"
|
||||
WORKSPACE_ID="${MOSAIC_WORKSPACE_ID:-}"
|
||||
API_TOKEN="${MOSAIC_API_TOKEN:-}"
|
||||
|
||||
# Check required environment variables
|
||||
if [[ -z "$WORKSPACE_ID" ]]; then
|
||||
echo "Error: MOSAIC_WORKSPACE_ID environment variable not set" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z "$API_TOKEN" ]]; then
|
||||
echo "Error: MOSAIC_API_TOKEN environment variable not set" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Helper function to make API requests
|
||||
api_request() {
|
||||
local method="$1"
|
||||
local endpoint="$2"
|
||||
local data="${3:-}"
|
||||
|
||||
local url="${API_URL}${endpoint}"
|
||||
local curl_args=(
|
||||
-X "$method"
|
||||
-H "Content-Type: application/json"
|
||||
-H "X-Workspace-Id: $WORKSPACE_ID"
|
||||
-H "Authorization: Bearer $API_TOKEN"
|
||||
-s
|
||||
)
|
||||
|
||||
if [[ -n "$data" ]]; then
|
||||
curl_args+=(-d "$data")
|
||||
fi
|
||||
|
||||
curl "${curl_args[@]}" "$url"
|
||||
}
|
||||
|
||||
# List all projects
|
||||
list_projects() {
|
||||
local page="${1:-1}"
|
||||
local limit="${2:-50}"
|
||||
api_request GET "/projects?page=$page&limit=$limit" | jq .
|
||||
}
|
||||
|
||||
# Get a single project with tasks
|
||||
get_project() {
|
||||
local project_id="$1"
|
||||
api_request GET "/projects/$project_id" | jq .
|
||||
}
|
||||
|
||||
# Get tasks with optional filters
|
||||
get_tasks() {
|
||||
local project_id="${1:-}"
|
||||
local filters=""
|
||||
|
||||
if [[ -n "$project_id" ]]; then
|
||||
filters="projectId=$project_id"
|
||||
fi
|
||||
|
||||
api_request GET "/tasks?$filters" | jq .
|
||||
}
|
||||
|
||||
# Get a single task
|
||||
get_task() {
|
||||
local task_id="$1"
|
||||
api_request GET "/tasks/$task_id" | jq .
|
||||
}
|
||||
|
||||
# Get dependency chain for a task
|
||||
get_dependencies() {
|
||||
local task_id="$1"
|
||||
|
||||
# Get the task
|
||||
local task_json
|
||||
task_json=$(get_task "$task_id")
|
||||
|
||||
# Extract dependency IDs from metadata
|
||||
local dep_ids
|
||||
dep_ids=$(echo "$task_json" | jq -r '.metadata.dependencies // [] | .[]')
|
||||
|
||||
if [[ -z "$dep_ids" ]]; then
|
||||
echo "Task has no dependencies"
|
||||
return
|
||||
fi
|
||||
|
||||
echo "Dependencies for task: $(echo "$task_json" | jq -r '.title')"
|
||||
echo ""
|
||||
|
||||
# Fetch each dependency
|
||||
while IFS= read -r dep_id; do
|
||||
if [[ -n "$dep_id" ]]; then
|
||||
local dep_task
|
||||
dep_task=$(get_task "$dep_id")
|
||||
echo "- $(echo "$dep_task" | jq -r '.title') [$(echo "$dep_task" | jq -r '.status')]"
|
||||
echo " Due: $(echo "$dep_task" | jq -r '.dueDate // "No due date"')"
|
||||
fi
|
||||
done <<< "$dep_ids"
|
||||
}
|
||||
|
||||
# Calculate critical path for a project
|
||||
critical_path() {
|
||||
local project_id="$1"
|
||||
|
||||
# Get all tasks for the project
|
||||
local tasks_json
|
||||
tasks_json=$(get_tasks "$project_id")
|
||||
|
||||
# Use jq to build dependency graph and find longest path
|
||||
echo "$tasks_json" | jq -r '
|
||||
.data as $tasks |
|
||||
|
||||
# Build adjacency list
|
||||
($tasks | map({
|
||||
id: .id,
|
||||
title: .title,
|
||||
status: .status,
|
||||
dueDate: .dueDate,
|
||||
dependencies: (.metadata.dependencies // [])
|
||||
})) as $nodes |
|
||||
|
||||
# Find tasks with no dependencies (starting points)
|
||||
($nodes | map(select(.dependencies | length == 0))) as $starts |
|
||||
|
||||
# Display structure
|
||||
"Critical Path Analysis\n" +
|
||||
"======================\n\n" +
|
||||
"Starting tasks (no dependencies):\n" +
|
||||
($starts | map("- \(.title) [\(.status)]") | join("\n")) +
|
||||
"\n\nAll tasks with dependencies:\n" +
|
||||
($nodes | map(
|
||||
select(.dependencies | length > 0) |
|
||||
"- \(.title) [\(.status)]\n Depends on: \(.dependencies | join(", "))"
|
||||
) | join("\n"))
|
||||
'
|
||||
}
|
||||
|
||||
# Main command dispatcher
|
||||
main() {
|
||||
if [[ $# -lt 1 ]]; then
|
||||
echo "Usage: $0 <command> [args...]" >&2
|
||||
echo "" >&2
|
||||
echo "Commands:" >&2
|
||||
echo " projects - List all projects" >&2
|
||||
echo " project <id> - Get project details" >&2
|
||||
echo " tasks [project-id] - Get tasks (optionally filtered by project)" >&2
|
||||
echo " task <id> - Get task details" >&2
|
||||
echo " dependencies <id> - Get dependency chain for task" >&2
|
||||
echo " critical-path <id> - Calculate critical path for project" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local command="$1"
|
||||
shift
|
||||
|
||||
case "$command" in
|
||||
projects)
|
||||
list_projects "$@"
|
||||
;;
|
||||
project)
|
||||
if [[ $# -lt 1 ]]; then
|
||||
echo "Error: project command requires project ID" >&2
|
||||
exit 1
|
||||
fi
|
||||
get_project "$1"
|
||||
;;
|
||||
tasks)
|
||||
get_tasks "$@"
|
||||
;;
|
||||
task)
|
||||
if [[ $# -lt 1 ]]; then
|
||||
echo "Error: task command requires task ID" >&2
|
||||
exit 1
|
||||
fi
|
||||
get_task "$1"
|
||||
;;
|
||||
dependencies)
|
||||
if [[ $# -lt 1 ]]; then
|
||||
echo "Error: dependencies command requires task ID" >&2
|
||||
exit 1
|
||||
fi
|
||||
get_dependencies "$1"
|
||||
;;
|
||||
critical-path)
|
||||
if [[ $# -lt 1 ]]; then
|
||||
echo "Error: critical-path command requires project ID" >&2
|
||||
exit 1
|
||||
fi
|
||||
critical_path "$1"
|
||||
;;
|
||||
*)
|
||||
echo "Error: unknown command '$command'" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
main "$@"
|
||||
433
packages/skills/gantt/gantt-client.ts
Normal file
433
packages/skills/gantt/gantt-client.ts
Normal file
@@ -0,0 +1,433 @@
|
||||
/**
|
||||
* Mosaic Stack Gantt API Client
|
||||
*
|
||||
* Provides typed client for querying project timelines, tasks, and dependencies
|
||||
* from Mosaic Stack's API.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const client = new GanttClient({
|
||||
* apiUrl: process.env.MOSAIC_API_URL,
|
||||
* workspaceId: process.env.MOSAIC_WORKSPACE_ID,
|
||||
* apiToken: process.env.MOSAIC_API_TOKEN,
|
||||
* });
|
||||
*
|
||||
* const projects = await client.listProjects();
|
||||
* const timeline = await client.getProjectTimeline('project-id');
|
||||
* const criticalPath = await client.calculateCriticalPath('project-id');
|
||||
* ```
|
||||
*/
|
||||
|
||||
export interface GanttClientConfig {
|
||||
apiUrl: string;
|
||||
workspaceId: string;
|
||||
apiToken: string;
|
||||
}
|
||||
|
||||
export type ProjectStatus = 'PLANNING' | 'ACTIVE' | 'ON_HOLD' | 'COMPLETED' | 'ARCHIVED';
|
||||
export type TaskStatus = 'NOT_STARTED' | 'IN_PROGRESS' | 'PAUSED' | 'COMPLETED' | 'ARCHIVED';
|
||||
export type TaskPriority = 'LOW' | 'MEDIUM' | 'HIGH' | 'URGENT';
|
||||
|
||||
export interface ProjectMetadata {
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface Project {
|
||||
id: string;
|
||||
workspaceId: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
status: ProjectStatus;
|
||||
startDate: string | null;
|
||||
endDate: string | null;
|
||||
color: string | null;
|
||||
metadata: ProjectMetadata;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
tasks?: Task[];
|
||||
_count?: {
|
||||
tasks: number;
|
||||
events: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface TaskMetadata {
|
||||
startDate?: string;
|
||||
dependencies?: string[];
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface Task {
|
||||
id: string;
|
||||
workspaceId: string;
|
||||
title: string;
|
||||
description: string | null;
|
||||
status: TaskStatus;
|
||||
priority: TaskPriority;
|
||||
dueDate: string | null;
|
||||
completedAt: string | null;
|
||||
projectId: string | null;
|
||||
assigneeId: string | null;
|
||||
creatorId: string;
|
||||
parentId: string | null;
|
||||
sortOrder: number;
|
||||
metadata: TaskMetadata;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
project?: {
|
||||
id: string;
|
||||
name: string;
|
||||
color: string | null;
|
||||
};
|
||||
}
|
||||
|
||||
export interface PaginatedResponse<T> {
|
||||
data: T[];
|
||||
meta: {
|
||||
total: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
totalPages: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ProjectTimeline {
|
||||
project: Project;
|
||||
tasks: Task[];
|
||||
stats: {
|
||||
total: number;
|
||||
completed: number;
|
||||
inProgress: number;
|
||||
notStarted: number;
|
||||
paused: number;
|
||||
targetPassed: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface DependencyChain {
|
||||
task: Task;
|
||||
blockedBy: Task[];
|
||||
blocks: Task[];
|
||||
}
|
||||
|
||||
export interface CriticalPath {
|
||||
path: Array<{
|
||||
task: Task;
|
||||
duration: number;
|
||||
cumulativeDuration: number;
|
||||
}>;
|
||||
totalDuration: number;
|
||||
nonCriticalTasks: Array<{
|
||||
task: Task;
|
||||
slack: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
export class GanttClient {
|
||||
private readonly config: GanttClientConfig;
|
||||
|
||||
constructor(config: GanttClientConfig) {
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make an authenticated API request
|
||||
*/
|
||||
private async request<T>(
|
||||
endpoint: string,
|
||||
options: RequestInit = {}
|
||||
): Promise<T> {
|
||||
const url = `${this.config.apiUrl}${endpoint}`;
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Workspace-Id': this.config.workspaceId,
|
||||
'Authorization': `Bearer ${this.config.apiToken}`,
|
||||
...options.headers,
|
||||
};
|
||||
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
headers,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.text();
|
||||
throw new Error(`API request failed: ${response.status} ${error}`);
|
||||
}
|
||||
|
||||
return response.json() as Promise<T>;
|
||||
}
|
||||
|
||||
/**
|
||||
* List all projects with pagination
|
||||
*/
|
||||
async listProjects(params?: {
|
||||
page?: number;
|
||||
limit?: number;
|
||||
status?: ProjectStatus;
|
||||
}): Promise<PaginatedResponse<Project>> {
|
||||
const queryParams = new URLSearchParams();
|
||||
if (params?.page) queryParams.set('page', params.page.toString());
|
||||
if (params?.limit) queryParams.set('limit', params.limit.toString());
|
||||
if (params?.status) queryParams.set('status', params.status);
|
||||
|
||||
const query = queryParams.toString();
|
||||
const endpoint = `/projects${query ? `?${query}` : ''}`;
|
||||
|
||||
return this.request<PaginatedResponse<Project>>(endpoint);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single project with tasks
|
||||
*/
|
||||
async getProject(projectId: string): Promise<Project> {
|
||||
return this.request<Project>(`/projects/${projectId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tasks with optional filters
|
||||
*/
|
||||
async getTasks(params?: {
|
||||
projectId?: string;
|
||||
status?: TaskStatus;
|
||||
priority?: TaskPriority;
|
||||
assigneeId?: string;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
}): Promise<PaginatedResponse<Task>> {
|
||||
const queryParams = new URLSearchParams();
|
||||
if (params?.projectId) queryParams.set('projectId', params.projectId);
|
||||
if (params?.status) queryParams.set('status', params.status);
|
||||
if (params?.priority) queryParams.set('priority', params.priority);
|
||||
if (params?.assigneeId) queryParams.set('assigneeId', params.assigneeId);
|
||||
if (params?.page) queryParams.set('page', params.page.toString());
|
||||
if (params?.limit) queryParams.set('limit', params.limit.toString());
|
||||
|
||||
const query = queryParams.toString();
|
||||
const endpoint = `/tasks${query ? `?${query}` : ''}`;
|
||||
|
||||
return this.request<PaginatedResponse<Task>>(endpoint);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single task
|
||||
*/
|
||||
async getTask(taskId: string): Promise<Task> {
|
||||
return this.request<Task>(`/tasks/${taskId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get project timeline with statistics
|
||||
*/
|
||||
async getProjectTimeline(projectId: string): Promise<ProjectTimeline> {
|
||||
const project = await this.getProject(projectId);
|
||||
const tasksResponse = await this.getTasks({ projectId, limit: 1000 });
|
||||
const tasks = tasksResponse.data;
|
||||
|
||||
const now = new Date();
|
||||
const stats = {
|
||||
total: tasks.length,
|
||||
completed: tasks.filter(t => t.status === 'COMPLETED').length,
|
||||
inProgress: tasks.filter(t => t.status === 'IN_PROGRESS').length,
|
||||
notStarted: tasks.filter(t => t.status === 'NOT_STARTED').length,
|
||||
paused: tasks.filter(t => t.status === 'PAUSED').length,
|
||||
targetPassed: tasks.filter(t => {
|
||||
if (!t.dueDate || t.status === 'COMPLETED') return false;
|
||||
return new Date(t.dueDate) < now;
|
||||
}).length,
|
||||
};
|
||||
|
||||
return { project, tasks, stats };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get dependency chain for a task
|
||||
*/
|
||||
async getDependencyChain(taskId: string): Promise<DependencyChain> {
|
||||
const task = await this.getTask(taskId);
|
||||
const dependencyIds = task.metadata.dependencies ?? [];
|
||||
|
||||
// Fetch tasks this task depends on (blocking tasks)
|
||||
const blockedBy = await Promise.all(
|
||||
dependencyIds.map(id => this.getTask(id))
|
||||
);
|
||||
|
||||
// Find tasks that depend on this task
|
||||
const allTasksResponse = await this.getTasks({
|
||||
projectId: task.projectId ?? undefined,
|
||||
limit: 1000,
|
||||
});
|
||||
const blocks = allTasksResponse.data.filter(t =>
|
||||
t.metadata.dependencies?.includes(taskId)
|
||||
);
|
||||
|
||||
return { task, blockedBy, blocks };
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate critical path for a project
|
||||
*
|
||||
* Uses the Critical Path Method (CPM) to find the longest dependency chain
|
||||
*/
|
||||
async calculateCriticalPath(projectId: string): Promise<CriticalPath> {
|
||||
const tasksResponse = await this.getTasks({ projectId, limit: 1000 });
|
||||
const tasks = tasksResponse.data;
|
||||
|
||||
// Build dependency graph
|
||||
const taskMap = new Map(tasks.map(t => [t.id, t]));
|
||||
const durations = new Map<string, number>();
|
||||
const earliestStart = new Map<string, number>();
|
||||
const latestStart = new Map<string, number>();
|
||||
|
||||
// Calculate task durations (days between start and due date, or default to 1)
|
||||
for (const task of tasks) {
|
||||
const start = task.metadata.startDate
|
||||
? new Date(task.metadata.startDate)
|
||||
: new Date(task.createdAt);
|
||||
const end = task.dueDate ? new Date(task.dueDate) : new Date();
|
||||
const duration = Math.max(1, Math.ceil((end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24)));
|
||||
durations.set(task.id, duration);
|
||||
}
|
||||
|
||||
// Forward pass: calculate earliest start times
|
||||
const visited = new Set<string>();
|
||||
const calculateEarliestStart = (taskId: string): number => {
|
||||
if (visited.has(taskId)) {
|
||||
return earliestStart.get(taskId) ?? 0;
|
||||
}
|
||||
visited.add(taskId);
|
||||
|
||||
const task = taskMap.get(taskId);
|
||||
if (!task) return 0;
|
||||
|
||||
const deps = task.metadata.dependencies ?? [];
|
||||
let maxEnd = 0;
|
||||
|
||||
for (const depId of deps) {
|
||||
const depStart = calculateEarliestStart(depId);
|
||||
const depDuration = durations.get(depId) ?? 1;
|
||||
maxEnd = Math.max(maxEnd, depStart + depDuration);
|
||||
}
|
||||
|
||||
earliestStart.set(taskId, maxEnd);
|
||||
return maxEnd;
|
||||
};
|
||||
|
||||
// Calculate earliest start for all tasks
|
||||
for (const task of tasks) {
|
||||
calculateEarliestStart(task.id);
|
||||
}
|
||||
|
||||
// Find project completion time (max earliest finish)
|
||||
let projectDuration = 0;
|
||||
for (const task of tasks) {
|
||||
const start = earliestStart.get(task.id) ?? 0;
|
||||
const duration = durations.get(task.id) ?? 1;
|
||||
projectDuration = Math.max(projectDuration, start + duration);
|
||||
}
|
||||
|
||||
// Backward pass: calculate latest start times
|
||||
const calculateLatestStart = (taskId: string): number => {
|
||||
const task = taskMap.get(taskId);
|
||||
if (!task) return projectDuration;
|
||||
|
||||
// Find all tasks that depend on this task
|
||||
const dependents = tasks.filter(t =>
|
||||
t.metadata.dependencies?.includes(taskId)
|
||||
);
|
||||
|
||||
if (dependents.length === 0) {
|
||||
// No dependents, latest start = project end - duration
|
||||
const duration = durations.get(taskId) ?? 1;
|
||||
const latest = projectDuration - duration;
|
||||
latestStart.set(taskId, latest);
|
||||
return latest;
|
||||
}
|
||||
|
||||
// Latest start = min(dependent latest starts) - duration
|
||||
let minDependentStart = projectDuration;
|
||||
for (const dependent of dependents) {
|
||||
const depLatest = latestStart.get(dependent.id) ?? calculateLatestStart(dependent.id);
|
||||
minDependentStart = Math.min(minDependentStart, depLatest);
|
||||
}
|
||||
|
||||
const duration = durations.get(taskId) ?? 1;
|
||||
const latest = minDependentStart - duration;
|
||||
latestStart.set(taskId, latest);
|
||||
return latest;
|
||||
};
|
||||
|
||||
// Calculate latest start for all tasks
|
||||
for (const task of tasks) {
|
||||
if (!latestStart.has(task.id)) {
|
||||
calculateLatestStart(task.id);
|
||||
}
|
||||
}
|
||||
|
||||
// Identify critical path (tasks with zero slack)
|
||||
const criticalTasks: Task[] = [];
|
||||
const nonCriticalTasks: Array<{ task: Task; slack: number }> = [];
|
||||
|
||||
for (const task of tasks) {
|
||||
const early = earliestStart.get(task.id) ?? 0;
|
||||
const late = latestStart.get(task.id) ?? 0;
|
||||
const slack = late - early;
|
||||
|
||||
if (slack === 0) {
|
||||
criticalTasks.push(task);
|
||||
} else {
|
||||
nonCriticalTasks.push({ task, slack });
|
||||
}
|
||||
}
|
||||
|
||||
// Build critical path chain
|
||||
const path = criticalTasks
|
||||
.sort((a, b) => (earliestStart.get(a.id) ?? 0) - (earliestStart.get(b.id) ?? 0))
|
||||
.map(task => ({
|
||||
task,
|
||||
duration: durations.get(task.id) ?? 1,
|
||||
cumulativeDuration: (earliestStart.get(task.id) ?? 0) + (durations.get(task.id) ?? 1),
|
||||
}));
|
||||
|
||||
return {
|
||||
path,
|
||||
totalDuration: projectDuration,
|
||||
nonCriticalTasks,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Find tasks approaching their due date (within specified days)
|
||||
*/
|
||||
async getTasksApproachingDueDate(
|
||||
projectId: string,
|
||||
daysThreshold: number = 7
|
||||
): Promise<Task[]> {
|
||||
const tasksResponse = await this.getTasks({ projectId, limit: 1000 });
|
||||
const now = new Date();
|
||||
const threshold = new Date(now.getTime() + daysThreshold * 24 * 60 * 60 * 1000);
|
||||
|
||||
return tasksResponse.data.filter(task => {
|
||||
if (!task.dueDate || task.status === 'COMPLETED') return false;
|
||||
const dueDate = new Date(task.dueDate);
|
||||
return dueDate >= now && dueDate <= threshold;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a client instance from environment variables
|
||||
*/
|
||||
export function createGanttClientFromEnv(): GanttClient {
|
||||
const apiUrl = process.env.MOSAIC_API_URL;
|
||||
const workspaceId = process.env.MOSAIC_WORKSPACE_ID;
|
||||
const apiToken = process.env.MOSAIC_API_TOKEN;
|
||||
|
||||
if (!apiUrl || !workspaceId || !apiToken) {
|
||||
throw new Error(
|
||||
'Missing required environment variables: MOSAIC_API_URL, MOSAIC_WORKSPACE_ID, MOSAIC_API_TOKEN'
|
||||
);
|
||||
}
|
||||
|
||||
return new GanttClient({ apiUrl, workspaceId, apiToken });
|
||||
}
|
||||
20
packages/skills/gantt/index.ts
Normal file
20
packages/skills/gantt/index.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Mosaic Stack Gantt Plugin - Main exports
|
||||
*/
|
||||
|
||||
export {
|
||||
GanttClient,
|
||||
createGanttClientFromEnv,
|
||||
type GanttClientConfig,
|
||||
type Project,
|
||||
type ProjectMetadata,
|
||||
type Task,
|
||||
type TaskMetadata,
|
||||
type ProjectStatus,
|
||||
type TaskStatus,
|
||||
type TaskPriority,
|
||||
type PaginatedResponse,
|
||||
type ProjectTimeline,
|
||||
type DependencyChain,
|
||||
type CriticalPath,
|
||||
} from './gantt-client.js';
|
||||
25
packages/skills/gantt/package.json
Normal file
25
packages/skills/gantt/package.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "mosaic-plugin-gantt",
|
||||
"version": "1.0.0",
|
||||
"description": "Clawdbot skill for Mosaic Stack Gantt/Project timeline API",
|
||||
"type": "module",
|
||||
"main": "index.ts",
|
||||
"scripts": {
|
||||
"example:timeline": "tsx examples/query-timeline.ts",
|
||||
"example:critical": "tsx examples/critical-path.ts"
|
||||
},
|
||||
"keywords": [
|
||||
"clawdbot",
|
||||
"skill",
|
||||
"mosaic-stack",
|
||||
"gantt",
|
||||
"project-management"
|
||||
],
|
||||
"author": "Mosaic Stack Team",
|
||||
"license": "MIT",
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"tsx": "^4.7.0",
|
||||
"typescript": "^5.3.0"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user