feat(#26): implement mosaic-plugin-gantt skill
This commit is contained in:
39
packages/skills/README.md
Normal file
39
packages/skills/README.md
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# Mosaic Stack Skills
|
||||||
|
|
||||||
|
Clawdbot skills for integrating with Mosaic Stack services.
|
||||||
|
|
||||||
|
## Available Skills
|
||||||
|
|
||||||
|
### gantt
|
||||||
|
Query and analyze 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)
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
See [gantt/README.md](./gantt/README.md) for detailed documentation.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Skills can be installed individually to Clawdbot:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install gantt skill
|
||||||
|
cp -r packages/skills/gantt ~/.claude/plugins/mosaic-plugin-gantt
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
Each skill follows the Clawdbot skill structure:
|
||||||
|
- `SKILL.md` - Skill definition and usage documentation
|
||||||
|
- `.claude-plugin/plugin.json` - Plugin metadata
|
||||||
|
- `README.md` - User documentation
|
||||||
|
- Implementation files (TypeScript, Bash, etc.)
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
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-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/api"
|
||||||
|
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 /api/projects` - List projects (paginated)
|
||||||
|
- `GET /api/projects/:id` - Get project with tasks
|
||||||
|
- `GET /api/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/api`)
|
||||||
|
|
||||||
|
### Projects
|
||||||
|
- `GET /api/projects` - List all projects with pagination
|
||||||
|
- `GET /api/projects/:id` - Get project details with tasks
|
||||||
|
|
||||||
|
### Tasks
|
||||||
|
- `GET /api/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)
|
||||||
|
|
||||||
|
## Usage Patterns
|
||||||
|
|
||||||
|
### Query Project Timeline
|
||||||
|
When the user asks to "show project X timeline" or "what's the status of project Y":
|
||||||
|
1. List projects to find the matching project (by name or ID)
|
||||||
|
2. Get full project details including tasks
|
||||||
|
3. Present timeline information with task dates and status
|
||||||
|
|
||||||
|
### Check Task Dependencies
|
||||||
|
When asked "what blocks task X" or "what are the dependencies for task Y":
|
||||||
|
1. Search tasks to find the target task
|
||||||
|
2. Check `metadata.dependencies` field (array of task IDs)
|
||||||
|
3. Fetch blocking tasks and present the dependency chain
|
||||||
|
|
||||||
|
### Get Project Status Overview
|
||||||
|
When asked for "project status" or "project overview":
|
||||||
|
1. Get project with task count
|
||||||
|
2. Calculate status distribution (completed/in-progress/not-started)
|
||||||
|
3. Identify tasks approaching or past due dates
|
||||||
|
4. Present summary statistics
|
||||||
|
|
||||||
|
### Identify Critical Path
|
||||||
|
When asked to "find critical path" or "identify blockers":
|
||||||
|
1. Get all project tasks with dependencies
|
||||||
|
2. Build dependency graph from `metadata.dependencies`
|
||||||
|
3. Calculate longest path through dependencies
|
||||||
|
4. Identify tasks with no slack time (critical tasks)
|
||||||
|
|
||||||
|
## 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
|
||||||
|
- `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/api}"
|
||||||
|
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 "$@"
|
||||||
427
packages/skills/gantt/gantt-client.ts
Normal file
427
packages/skills/gantt/gantt-client.ts
Normal file
@@ -0,0 +1,427 @@
|
|||||||
|
/**
|
||||||
|
* 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 Project {
|
||||||
|
id: string;
|
||||||
|
workspaceId: string;
|
||||||
|
name: string;
|
||||||
|
description: string | null;
|
||||||
|
status: ProjectStatus;
|
||||||
|
startDate: string | null;
|
||||||
|
endDate: string | null;
|
||||||
|
color: string | null;
|
||||||
|
metadata: Record<string, unknown>;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
tasks?: Task[];
|
||||||
|
_count?: {
|
||||||
|
tasks: number;
|
||||||
|
events: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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: {
|
||||||
|
startDate?: string;
|
||||||
|
dependencies?: string[];
|
||||||
|
[key: string]: unknown;
|
||||||
|
};
|
||||||
|
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 });
|
||||||
|
}
|
||||||
18
packages/skills/gantt/index.ts
Normal file
18
packages/skills/gantt/index.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
/**
|
||||||
|
* Mosaic Stack Gantt Plugin - Main exports
|
||||||
|
*/
|
||||||
|
|
||||||
|
export {
|
||||||
|
GanttClient,
|
||||||
|
createGanttClientFromEnv,
|
||||||
|
type GanttClientConfig,
|
||||||
|
type Project,
|
||||||
|
type Task,
|
||||||
|
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