feat(#26): implement mosaic-plugin-gantt skill

This commit is contained in:
Jason Woltje
2026-01-29 21:18:14 -06:00
parent 9de0b2f92f
commit 18c7b8c723
12 changed files with 1129 additions and 0 deletions

39
packages/skills/README.md Normal file
View 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

View 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
View File

@@ -0,0 +1,5 @@
node_modules/
dist/
*.log
.env
.DS_Store

View 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.

View 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

View 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`

View 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);
});

View 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);
});

View 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 "$@"

View 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 });
}

View 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';

View 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"
}
}