feat(#93): implement agent spawn via federation
Implements FED-010: Agent Spawn via Federation feature that enables spawning and managing Claude agents on remote federated Mosaic Stack instances via COMMAND message type. Features: - Federation agent command types (spawn, status, kill) - FederationAgentService for handling agent operations - Integration with orchestrator's agent spawner/lifecycle services - API endpoints for spawning, querying status, and killing agents - Full command routing through federation COMMAND infrastructure - Comprehensive test coverage (12/12 tests passing) Architecture: - Hub → Spoke: Spawn agents on remote instances - Command flow: FederationController → FederationAgentService → CommandService → Remote Orchestrator - Response handling: Remote orchestrator returns agent status/results - Security: Connection validation, signature verification Files created: - apps/api/src/federation/types/federation-agent.types.ts - apps/api/src/federation/federation-agent.service.ts - apps/api/src/federation/federation-agent.service.spec.ts Files modified: - apps/api/src/federation/command.service.ts (agent command routing) - apps/api/src/federation/federation.controller.ts (agent endpoints) - apps/api/src/federation/federation.module.ts (service registration) - apps/orchestrator/src/api/agents/agents.controller.ts (status endpoint) - apps/orchestrator/src/api/agents/agents.module.ts (lifecycle integration) Testing: - 12/12 tests passing for FederationAgentService - All command service tests passing - TypeScript compilation successful - Linting passed Refs #93 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -5,9 +5,11 @@ Clawdbot skills for integrating with Mosaic Stack APIs.
|
||||
## 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
|
||||
@@ -16,9 +18,11 @@ Quick capture and semantic search for ideas and brain dumps.
|
||||
**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
|
||||
@@ -27,9 +31,11 @@ Integration with Mosaic Stack's Events API for calendar management.
|
||||
**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
|
||||
@@ -38,9 +44,11 @@ Task management integration with Mosaic Stack's Tasks API.
|
||||
**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
|
||||
|
||||
@@ -13,11 +13,13 @@ A Clawdbot skill for integrating with Mosaic Stack's Ideas/Brain API.
|
||||
## Installation
|
||||
|
||||
1. Copy this skill to your Clawdbot skills directory or link it:
|
||||
|
||||
```bash
|
||||
ln -s ~/src/mosaic-stack/packages/skills/brain ~/.config/clawdbot/skills/mosaic-brain
|
||||
```
|
||||
|
||||
2. Configure your Mosaic Stack connection:
|
||||
|
||||
```bash
|
||||
mkdir -p ~/.config/mosaic
|
||||
cat > ~/.config/mosaic/brain.conf <<EOF
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
name: mosaic-brain
|
||||
description: Capture ideas, brain dumps, and semantic search across your Mosaic Stack knowledge base.
|
||||
homepage: https://mosaicstack.dev
|
||||
metadata: {"clawdbot":{"emoji":"🧠","requires":{"bins":["curl","jq"]}}}
|
||||
metadata: { "clawdbot": { "emoji": "🧠", "requires": { "bins": ["curl", "jq"] } } }
|
||||
---
|
||||
|
||||
# Mosaic Brain
|
||||
@@ -13,6 +13,7 @@ Capture and search ideas, brain dumps, and notes in your Mosaic Stack workspace.
|
||||
|
||||
1. Ensure Mosaic Stack API is running (default: `http://localhost:3001`)
|
||||
2. Set environment variables:
|
||||
|
||||
```bash
|
||||
export MOSAIC_API_URL="http://localhost:3001"
|
||||
export MOSAIC_WORKSPACE_ID="your-workspace-uuid"
|
||||
@@ -29,7 +30,9 @@ Capture and search ideas, brain dumps, and notes in your Mosaic Stack workspace.
|
||||
## Common Commands
|
||||
|
||||
### Quick Brain Dump
|
||||
|
||||
Capture an idea quickly without thinking about categorization:
|
||||
|
||||
```bash
|
||||
# Quick capture
|
||||
./brain.sh capture "Had a great idea about using AI for code reviews"
|
||||
@@ -39,7 +42,9 @@ Capture an idea quickly without thinking about categorization:
|
||||
```
|
||||
|
||||
### Create Detailed Idea
|
||||
|
||||
Create an idea with full metadata:
|
||||
|
||||
```bash
|
||||
./brain.sh create \
|
||||
--title "New Feature Idea" \
|
||||
@@ -49,7 +54,9 @@ Create an idea with full metadata:
|
||||
```
|
||||
|
||||
### Semantic Search
|
||||
|
||||
Search your brain using natural language:
|
||||
|
||||
```bash
|
||||
# Query your knowledge base
|
||||
./brain.sh query "What did I say about AI and code reviews?"
|
||||
@@ -59,6 +66,7 @@ Search your brain using natural language:
|
||||
```
|
||||
|
||||
### List Ideas
|
||||
|
||||
```bash
|
||||
# List recent ideas
|
||||
./brain.sh list
|
||||
@@ -68,11 +76,13 @@ Search your brain using natural language:
|
||||
```
|
||||
|
||||
### Get Specific Idea
|
||||
|
||||
```bash
|
||||
./brain.sh get <idea-id>
|
||||
```
|
||||
|
||||
### Update Idea
|
||||
|
||||
```bash
|
||||
./brain.sh update <idea-id> \
|
||||
--title "Updated Title" \
|
||||
@@ -81,11 +91,13 @@ Search your brain using natural language:
|
||||
```
|
||||
|
||||
### Delete Idea
|
||||
|
||||
```bash
|
||||
./brain.sh delete <idea-id>
|
||||
```
|
||||
|
||||
### Tag Management
|
||||
|
||||
```bash
|
||||
# List all tags
|
||||
./brain.sh tags
|
||||
@@ -113,11 +125,13 @@ The skill uses these Mosaic Stack API endpoints:
|
||||
## Configuration
|
||||
|
||||
Config file priority:
|
||||
|
||||
1. Environment variables
|
||||
2. `~/.config/mosaic/brain.conf`
|
||||
3. Default values
|
||||
|
||||
Required settings:
|
||||
|
||||
- `MOSAIC_API_URL` - API base URL (default: http://localhost:3001)
|
||||
- `MOSAIC_WORKSPACE_ID` - Your workspace UUID
|
||||
- `MOSAIC_API_TOKEN` - Authentication token
|
||||
@@ -125,6 +139,7 @@ Required settings:
|
||||
## Usage from Clawdbot
|
||||
|
||||
Natural language examples:
|
||||
|
||||
- "Remember this..." / "Note to self..." → Quick capture
|
||||
- "What did I say about..." → Semantic search
|
||||
- "Show me my recent ideas" → List ideas
|
||||
|
||||
@@ -23,6 +23,7 @@ node scripts/calendar.js create \
|
||||
```
|
||||
|
||||
Natural language examples:
|
||||
|
||||
- "Schedule a meeting tomorrow at 3pm"
|
||||
- "Create an event called 'dentist appointment' on Friday at 2pm"
|
||||
- "Add a recurring standup every weekday at 9am"
|
||||
@@ -43,6 +44,7 @@ node scripts/calendar.js list --project-id "uuid-here"
|
||||
```
|
||||
|
||||
Natural language examples:
|
||||
|
||||
- "What's on my calendar this week?"
|
||||
- "Show me upcoming events"
|
||||
- "What do I have scheduled for tomorrow?"
|
||||
@@ -57,6 +59,7 @@ node scripts/calendar.js update EVENT_ID \
|
||||
```
|
||||
|
||||
Natural language examples:
|
||||
|
||||
- "Move my 3pm meeting to 4pm"
|
||||
- "Reschedule tomorrow's dentist appointment to Friday"
|
||||
- "Change the location of my team meeting to Zoom"
|
||||
@@ -68,6 +71,7 @@ node scripts/calendar.js delete EVENT_ID
|
||||
```
|
||||
|
||||
Natural language examples:
|
||||
|
||||
- "Cancel my meeting with Sarah"
|
||||
- "Delete tomorrow's standup"
|
||||
- "Remove the 3pm appointment"
|
||||
@@ -133,6 +137,7 @@ List queries return paginated results with metadata:
|
||||
## Environment
|
||||
|
||||
The script reads configuration from:
|
||||
|
||||
- `MOSAIC_API_URL` (default: http://localhost:3001)
|
||||
- `MOSAIC_WORKSPACE_ID` (required)
|
||||
- `MOSAIC_API_TOKEN` (required for authentication)
|
||||
@@ -142,6 +147,7 @@ Ensure these are set in the environment or `.env` file.
|
||||
## Error Handling
|
||||
|
||||
Common errors:
|
||||
|
||||
- **401 Unauthorized**: Missing or invalid API token
|
||||
- **403 Forbidden**: Insufficient workspace permissions
|
||||
- **404 Not Found**: Event ID doesn't exist
|
||||
|
||||
@@ -5,12 +5,12 @@
|
||||
* Interacts with Mosaic Stack's Events API for calendar management
|
||||
*/
|
||||
|
||||
import https from 'https';
|
||||
import http from 'http';
|
||||
import { URL } from 'url';
|
||||
import https from "https";
|
||||
import http from "http";
|
||||
import { URL } from "url";
|
||||
|
||||
// Configuration
|
||||
const API_URL = process.env.MOSAIC_API_URL || 'http://localhost:3001';
|
||||
const API_URL = process.env.MOSAIC_API_URL || "http://localhost:3001";
|
||||
const WORKSPACE_ID = process.env.MOSAIC_WORKSPACE_ID;
|
||||
const API_TOKEN = process.env.MOSAIC_API_TOKEN;
|
||||
|
||||
@@ -20,7 +20,7 @@ const API_TOKEN = process.env.MOSAIC_API_TOKEN;
|
||||
function apiRequest(method, path, body = null) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const url = new URL(path, API_URL);
|
||||
const isHttps = url.protocol === 'https:';
|
||||
const isHttps = url.protocol === "https:";
|
||||
const client = isHttps ? https : http;
|
||||
|
||||
const options = {
|
||||
@@ -29,16 +29,16 @@ function apiRequest(method, path, body = null) {
|
||||
port: url.port || (isHttps ? 443 : 80),
|
||||
path: url.pathname + url.search,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${API_TOKEN}`,
|
||||
'X-Workspace-ID': WORKSPACE_ID
|
||||
}
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${API_TOKEN}`,
|
||||
"X-Workspace-ID": WORKSPACE_ID,
|
||||
},
|
||||
};
|
||||
|
||||
const req = client.request(options, (res) => {
|
||||
let data = '';
|
||||
res.on('data', (chunk) => data += chunk);
|
||||
res.on('end', () => {
|
||||
let data = "";
|
||||
res.on("data", (chunk) => (data += chunk));
|
||||
res.on("end", () => {
|
||||
try {
|
||||
const parsed = data ? JSON.parse(data) : {};
|
||||
if (res.statusCode >= 200 && res.statusCode < 300) {
|
||||
@@ -46,17 +46,17 @@ function apiRequest(method, path, body = null) {
|
||||
} else {
|
||||
// Provide helpful error messages based on status code
|
||||
let errorMsg = `HTTP ${res.statusCode}: ${parsed.message || data}`;
|
||||
|
||||
|
||||
if (res.statusCode === 401) {
|
||||
errorMsg += '\n → Check MOSAIC_API_TOKEN is valid';
|
||||
errorMsg += "\n → Check MOSAIC_API_TOKEN is valid";
|
||||
} else if (res.statusCode === 403) {
|
||||
errorMsg += '\n → Verify workspace permissions and MOSAIC_WORKSPACE_ID';
|
||||
errorMsg += "\n → Verify workspace permissions and MOSAIC_WORKSPACE_ID";
|
||||
} else if (res.statusCode === 404) {
|
||||
errorMsg += '\n → Resource not found. Check the event ID';
|
||||
errorMsg += "\n → Resource not found. Check the event ID";
|
||||
} else if (res.statusCode === 400) {
|
||||
errorMsg += '\n → Invalid request. Check date formats and required fields';
|
||||
errorMsg += "\n → Invalid request. Check date formats and required fields";
|
||||
}
|
||||
|
||||
|
||||
reject(new Error(errorMsg));
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -65,7 +65,7 @@ function apiRequest(method, path, body = null) {
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', reject);
|
||||
req.on("error", reject);
|
||||
|
||||
if (body) {
|
||||
req.write(JSON.stringify(body));
|
||||
@@ -82,10 +82,10 @@ function parseArgs(args) {
|
||||
const parsed = { _: [] };
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
const arg = args[i];
|
||||
if (arg.startsWith('--')) {
|
||||
if (arg.startsWith("--")) {
|
||||
const key = arg.slice(2);
|
||||
const value = args[i + 1];
|
||||
if (value && !value.startsWith('--')) {
|
||||
if (value && !value.startsWith("--")) {
|
||||
parsed[key] = value;
|
||||
i++;
|
||||
} else {
|
||||
@@ -103,7 +103,7 @@ function parseArgs(args) {
|
||||
*/
|
||||
async function createEvent(args) {
|
||||
if (!args.title || !args.start) {
|
||||
throw new Error('Required: --title and --start (ISO 8601 date)');
|
||||
throw new Error("Required: --title and --start (ISO 8601 date)");
|
||||
}
|
||||
|
||||
const body = {
|
||||
@@ -114,11 +114,11 @@ async function createEvent(args) {
|
||||
if (args.end) body.endTime = args.end;
|
||||
if (args.description) body.description = args.description;
|
||||
if (args.location) body.location = args.location;
|
||||
if (args['all-day'] !== undefined) body.allDay = args['all-day'] === 'true';
|
||||
if (args['project-id']) body.projectId = args['project-id'];
|
||||
if (args["all-day"] !== undefined) body.allDay = args["all-day"] === "true";
|
||||
if (args["project-id"]) body.projectId = args["project-id"];
|
||||
if (args.metadata) body.metadata = JSON.parse(args.metadata);
|
||||
|
||||
const result = await apiRequest('POST', '/events', body);
|
||||
const result = await apiRequest("POST", "/events", body);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -128,17 +128,17 @@ async function createEvent(args) {
|
||||
async function listEvents(args) {
|
||||
const params = new URLSearchParams();
|
||||
|
||||
if (args.from) params.append('startFrom', args.from);
|
||||
if (args.to) params.append('startTo', args.to);
|
||||
if (args['project-id']) params.append('projectId', args['project-id']);
|
||||
if (args['all-day'] !== undefined) params.append('allDay', args['all-day']);
|
||||
if (args.page) params.append('page', args.page);
|
||||
if (args.limit) params.append('limit', args.limit);
|
||||
if (args.from) params.append("startFrom", args.from);
|
||||
if (args.to) params.append("startTo", args.to);
|
||||
if (args["project-id"]) params.append("projectId", args["project-id"]);
|
||||
if (args["all-day"] !== undefined) params.append("allDay", args["all-day"]);
|
||||
if (args.page) params.append("page", args.page);
|
||||
if (args.limit) params.append("limit", args.limit);
|
||||
|
||||
const query = params.toString();
|
||||
const path = `/events${query ? '?' + query : ''}`;
|
||||
const path = `/events${query ? "?" + query : ""}`;
|
||||
|
||||
const result = await apiRequest('GET', path);
|
||||
const result = await apiRequest("GET", path);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -147,10 +147,10 @@ async function listEvents(args) {
|
||||
*/
|
||||
async function getEvent(eventId) {
|
||||
if (!eventId) {
|
||||
throw new Error('Event ID required');
|
||||
throw new Error("Event ID required");
|
||||
}
|
||||
|
||||
const result = await apiRequest('GET', `/events/${eventId}`);
|
||||
const result = await apiRequest("GET", `/events/${eventId}`);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -159,7 +159,7 @@ async function getEvent(eventId) {
|
||||
*/
|
||||
async function updateEvent(eventId, args) {
|
||||
if (!eventId) {
|
||||
throw new Error('Event ID required');
|
||||
throw new Error("Event ID required");
|
||||
}
|
||||
|
||||
const body = {};
|
||||
@@ -169,11 +169,11 @@ async function updateEvent(eventId, args) {
|
||||
if (args.end) body.endTime = args.end;
|
||||
if (args.description) body.description = args.description;
|
||||
if (args.location) body.location = args.location;
|
||||
if (args['all-day'] !== undefined) body.allDay = args['all-day'] === 'true';
|
||||
if (args['project-id']) body.projectId = args['project-id'];
|
||||
if (args["all-day"] !== undefined) body.allDay = args["all-day"] === "true";
|
||||
if (args["project-id"]) body.projectId = args["project-id"];
|
||||
if (args.metadata) body.metadata = JSON.parse(args.metadata);
|
||||
|
||||
const result = await apiRequest('PATCH', `/events/${eventId}`, body);
|
||||
const result = await apiRequest("PATCH", `/events/${eventId}`, body);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -182,10 +182,10 @@ async function updateEvent(eventId, args) {
|
||||
*/
|
||||
async function deleteEvent(eventId) {
|
||||
if (!eventId) {
|
||||
throw new Error('Event ID required');
|
||||
throw new Error("Event ID required");
|
||||
}
|
||||
|
||||
const result = await apiRequest('DELETE', `/events/${eventId}`);
|
||||
const result = await apiRequest("DELETE", `/events/${eventId}`);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -198,12 +198,12 @@ async function main() {
|
||||
|
||||
// Validate environment
|
||||
if (!WORKSPACE_ID) {
|
||||
console.error('Error: MOSAIC_WORKSPACE_ID environment variable required');
|
||||
console.error("Error: MOSAIC_WORKSPACE_ID environment variable required");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!API_TOKEN) {
|
||||
console.error('Error: MOSAIC_API_TOKEN environment variable required');
|
||||
console.error("Error: MOSAIC_API_TOKEN environment variable required");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@@ -211,32 +211,32 @@ async function main() {
|
||||
let result;
|
||||
|
||||
switch (command) {
|
||||
case 'create':
|
||||
case "create":
|
||||
result = await createEvent(args);
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
break;
|
||||
|
||||
case 'list':
|
||||
case "list":
|
||||
result = await listEvents(args);
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
break;
|
||||
|
||||
case 'get':
|
||||
case "get":
|
||||
result = await getEvent(args._[1]);
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
break;
|
||||
|
||||
case 'update':
|
||||
case "update":
|
||||
result = await updateEvent(args._[1], args);
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
break;
|
||||
|
||||
case 'delete':
|
||||
case "delete":
|
||||
result = await deleteEvent(args._[1]);
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
break;
|
||||
|
||||
case 'help':
|
||||
case "help":
|
||||
default:
|
||||
console.log(`
|
||||
Mosaic Calendar CLI
|
||||
@@ -287,7 +287,7 @@ Environment:
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error.message);
|
||||
console.error("Error:", error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
@@ -295,11 +295,4 @@ Environment:
|
||||
// Run if called directly
|
||||
main();
|
||||
|
||||
export {
|
||||
createEvent,
|
||||
listEvents,
|
||||
getEvent,
|
||||
updateEvent,
|
||||
deleteEvent,
|
||||
apiRequest
|
||||
};
|
||||
export { createEvent, listEvents, getEvent, updateEvent, deleteEvent, apiRequest };
|
||||
|
||||
@@ -6,9 +6,7 @@
|
||||
"name": "Mosaic Stack Team",
|
||||
"email": "support@mosaicstack.dev"
|
||||
},
|
||||
"skills": [
|
||||
"gantt"
|
||||
],
|
||||
"skills": ["gantt"],
|
||||
"commands": [
|
||||
{
|
||||
"name": "gantt-api",
|
||||
@@ -17,12 +15,7 @@
|
||||
}
|
||||
],
|
||||
"environment": {
|
||||
"required": [
|
||||
"MOSAIC_WORKSPACE_ID",
|
||||
"MOSAIC_API_TOKEN"
|
||||
],
|
||||
"optional": [
|
||||
"MOSAIC_API_URL"
|
||||
]
|
||||
"required": ["MOSAIC_WORKSPACE_ID", "MOSAIC_API_TOKEN"],
|
||||
"optional": ["MOSAIC_API_URL"]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,13 +13,15 @@ Clawdbot skill for querying and analyzing project timelines, task dependencies,
|
||||
## 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"
|
||||
@@ -83,6 +85,7 @@ The `gantt-api.sh` helper script can be used directly:
|
||||
### Authentication
|
||||
|
||||
All requests require headers:
|
||||
|
||||
- `X-Workspace-Id`: Workspace UUID
|
||||
- `Authorization`: Bearer {token}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ This skill enables querying project timelines, task dependencies, critical path
|
||||
## 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)
|
||||
@@ -20,43 +21,51 @@ The Mosaic Stack provides project management capabilities with support for:
|
||||
**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?"
|
||||
@@ -64,6 +73,7 @@ Requests require authentication headers:
|
||||
## Data Models
|
||||
|
||||
### Project
|
||||
|
||||
```typescript
|
||||
{
|
||||
id: string;
|
||||
@@ -79,6 +89,7 @@ Requests require authentication headers:
|
||||
```
|
||||
|
||||
### Task
|
||||
|
||||
```typescript
|
||||
{
|
||||
id: string;
|
||||
@@ -122,6 +133,7 @@ Use `gantt-api.sh` for API queries:
|
||||
## 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"
|
||||
|
||||
@@ -1,58 +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';
|
||||
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>');
|
||||
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));
|
||||
|
||||
console.log("=".repeat(50));
|
||||
|
||||
for (const item of criticalPath.path) {
|
||||
const statusIcon = item.task.status === 'COMPLETED' ? '✓' :
|
||||
item.task.status === 'IN_PROGRESS' ? '⊙' : '□';
|
||||
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('');
|
||||
|
||||
console.log("");
|
||||
}
|
||||
|
||||
|
||||
if (criticalPath.nonCriticalTasks.length > 0) {
|
||||
console.log('\nNon-Critical Tasks (can be delayed):');
|
||||
console.log('='.repeat(50));
|
||||
|
||||
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('');
|
||||
console.log("");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(error => {
|
||||
console.error('Error:', error.message);
|
||||
main().catch((error) => {
|
||||
console.error("Error:", error.message);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
@@ -1,69 +1,73 @@
|
||||
/**
|
||||
* Example: Query project timeline and display statistics
|
||||
*
|
||||
*
|
||||
* Usage:
|
||||
* npx tsx examples/query-timeline.ts <project-id>
|
||||
*/
|
||||
|
||||
import { createGanttClientFromEnv } from '../index.js';
|
||||
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>');
|
||||
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:');
|
||||
|
||||
|
||||
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 '□';
|
||||
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)'
|
||||
: '';
|
||||
|
||||
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);
|
||||
main().catch((error) => {
|
||||
console.error("Error:", error.message);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
/**
|
||||
* 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({
|
||||
@@ -11,7 +11,7 @@
|
||||
* 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');
|
||||
@@ -24,9 +24,9 @@ export interface GanttClientConfig {
|
||||
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 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;
|
||||
@@ -133,15 +133,12 @@ export class GanttClient {
|
||||
/**
|
||||
* Make an authenticated API request
|
||||
*/
|
||||
private async request<T>(
|
||||
endpoint: string,
|
||||
options: RequestInit = {}
|
||||
): Promise<T> {
|
||||
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}`,
|
||||
"Content-Type": "application/json",
|
||||
"X-Workspace-Id": this.config.workspaceId,
|
||||
Authorization: `Bearer ${this.config.apiToken}`,
|
||||
...options.headers,
|
||||
};
|
||||
|
||||
@@ -167,12 +164,12 @@ export class GanttClient {
|
||||
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);
|
||||
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}` : ''}`;
|
||||
const endpoint = `/projects${query ? `?${query}` : ""}`;
|
||||
|
||||
return this.request<PaginatedResponse<Project>>(endpoint);
|
||||
}
|
||||
@@ -196,15 +193,15 @@ export class GanttClient {
|
||||
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());
|
||||
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}` : ''}`;
|
||||
const endpoint = `/tasks${query ? `?${query}` : ""}`;
|
||||
|
||||
return this.request<PaginatedResponse<Task>>(endpoint);
|
||||
}
|
||||
@@ -227,12 +224,12 @@ export class GanttClient {
|
||||
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;
|
||||
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,
|
||||
};
|
||||
@@ -248,25 +245,21 @@ export class GanttClient {
|
||||
const dependencyIds = task.metadata.dependencies ?? [];
|
||||
|
||||
// Fetch tasks this task depends on (blocking tasks)
|
||||
const blockedBy = await Promise.all(
|
||||
dependencyIds.map(id => this.getTask(id))
|
||||
);
|
||||
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)
|
||||
);
|
||||
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> {
|
||||
@@ -274,7 +267,7 @@ export class GanttClient {
|
||||
const tasks = tasksResponse.data;
|
||||
|
||||
// Build dependency graph
|
||||
const taskMap = new Map(tasks.map(t => [t.id, t]));
|
||||
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>();
|
||||
@@ -285,7 +278,10 @@ export class GanttClient {
|
||||
? 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)));
|
||||
const duration = Math.max(
|
||||
1,
|
||||
Math.ceil((end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24))
|
||||
);
|
||||
durations.set(task.id, duration);
|
||||
}
|
||||
|
||||
@@ -332,9 +328,7 @@ export class GanttClient {
|
||||
if (!task) return projectDuration;
|
||||
|
||||
// Find all tasks that depend on this task
|
||||
const dependents = tasks.filter(t =>
|
||||
t.metadata.dependencies?.includes(taskId)
|
||||
);
|
||||
const dependents = tasks.filter((t) => t.metadata.dependencies?.includes(taskId));
|
||||
|
||||
if (dependents.length === 0) {
|
||||
// No dependents, latest start = project end - duration
|
||||
@@ -383,7 +377,7 @@ export class GanttClient {
|
||||
// Build critical path chain
|
||||
const path = criticalTasks
|
||||
.sort((a, b) => (earliestStart.get(a.id) ?? 0) - (earliestStart.get(b.id) ?? 0))
|
||||
.map(task => ({
|
||||
.map((task) => ({
|
||||
task,
|
||||
duration: durations.get(task.id) ?? 1,
|
||||
cumulativeDuration: (earliestStart.get(task.id) ?? 0) + (durations.get(task.id) ?? 1),
|
||||
@@ -399,16 +393,13 @@ export class GanttClient {
|
||||
/**
|
||||
* Find tasks approaching their due date (within specified days)
|
||||
*/
|
||||
async getTasksApproachingDueDate(
|
||||
projectId: string,
|
||||
daysThreshold: number = 7
|
||||
): Promise<Task[]> {
|
||||
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;
|
||||
return tasksResponse.data.filter((task) => {
|
||||
if (!task.dueDate || task.status === "COMPLETED") return false;
|
||||
const dueDate = new Date(task.dueDate);
|
||||
return dueDate >= now && dueDate <= threshold;
|
||||
});
|
||||
@@ -425,7 +416,7 @@ export function createGanttClientFromEnv(): GanttClient {
|
||||
|
||||
if (!apiUrl || !workspaceId || !apiToken) {
|
||||
throw new Error(
|
||||
'Missing required environment variables: MOSAIC_API_URL, MOSAIC_WORKSPACE_ID, MOSAIC_API_TOKEN'
|
||||
"Missing required environment variables: MOSAIC_API_URL, MOSAIC_WORKSPACE_ID, MOSAIC_API_TOKEN"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -17,4 +17,4 @@ export {
|
||||
type ProjectTimeline,
|
||||
type DependencyChain,
|
||||
type CriticalPath,
|
||||
} from './gantt-client.js';
|
||||
} from "./gantt-client.js";
|
||||
|
||||
@@ -2,7 +2,15 @@
|
||||
name: mosaic-plugin-tasks
|
||||
description: Integration with Mosaic Stack's Tasks API for task management. Use when the user wants to create, list, update, complete, or delete tasks, including queries like "add task", "create task", "what's in progress", "high priority tasks", "mark task as done", "overdue tasks", "show my tasks", or "delete task".
|
||||
homepage: https://git.mosaicstack.dev/mosaic/stack
|
||||
metadata: {"clawdbot":{"emoji":"✅","requires":{"bins":["node"],"env":["MOSAIC_API_TOKEN","MOSAIC_WORKSPACE_ID"]},"primaryEnv":"MOSAIC_API_TOKEN"}}
|
||||
metadata:
|
||||
{
|
||||
"clawdbot":
|
||||
{
|
||||
"emoji": "✅",
|
||||
"requires": { "bins": ["node"], "env": ["MOSAIC_API_TOKEN", "MOSAIC_WORKSPACE_ID"] },
|
||||
"primaryEnv": "MOSAIC_API_TOKEN",
|
||||
},
|
||||
}
|
||||
---
|
||||
|
||||
# Mosaic Tasks
|
||||
@@ -25,6 +33,7 @@ node scripts/tasks.cjs create \
|
||||
```
|
||||
|
||||
Natural language examples:
|
||||
|
||||
- "Add task: review PR #123"
|
||||
- "Create a high priority task to fix the login bug"
|
||||
- "Add a task to update documentation, due Friday"
|
||||
@@ -53,6 +62,7 @@ node scripts/tasks.cjs list --overdue
|
||||
```
|
||||
|
||||
Natural language examples:
|
||||
|
||||
- "What tasks are in progress?"
|
||||
- "Show me high priority tasks"
|
||||
- "What do I need to do today?"
|
||||
@@ -67,6 +77,7 @@ node scripts/tasks.cjs get TASK_ID
|
||||
```
|
||||
|
||||
Natural language examples:
|
||||
|
||||
- "Show me task details for abc-123"
|
||||
- "What's the status of the deployment task?"
|
||||
|
||||
@@ -87,6 +98,7 @@ node scripts/tasks.cjs update TASK_ID \
|
||||
```
|
||||
|
||||
Natural language examples:
|
||||
|
||||
- "Mark task abc-123 as done"
|
||||
- "Set PR review task to in progress"
|
||||
- "Change priority of deployment task to high"
|
||||
@@ -100,6 +112,7 @@ node scripts/tasks.cjs delete TASK_ID
|
||||
```
|
||||
|
||||
Natural language examples:
|
||||
|
||||
- "Delete task abc-123"
|
||||
- "Remove the duplicate deployment task"
|
||||
- "Delete all completed tasks from last month"
|
||||
@@ -150,10 +163,12 @@ node scripts/tasks.cjs list --overdue
|
||||
```
|
||||
|
||||
This filters to:
|
||||
|
||||
- `dueDate < current_datetime`
|
||||
- `status NOT IN (COMPLETED, ARCHIVED)`
|
||||
|
||||
Natural language examples:
|
||||
|
||||
- "What tasks are overdue?"
|
||||
- "Show me late tasks"
|
||||
- "Which tasks missed their deadline?"
|
||||
@@ -212,6 +227,7 @@ Tasks can transition to any status at any time based on needs.
|
||||
## Environment
|
||||
|
||||
The script reads configuration from:
|
||||
|
||||
- `MOSAIC_API_URL` (default: http://localhost:3001)
|
||||
- `MOSAIC_WORKSPACE_ID` (required)
|
||||
- `MOSAIC_API_TOKEN` (required for authentication)
|
||||
@@ -221,6 +237,7 @@ Ensure these are set in the environment or `.env` file.
|
||||
## Error Handling
|
||||
|
||||
Common errors:
|
||||
|
||||
- **401 Unauthorized**: Missing or invalid API token
|
||||
- **403 Forbidden**: Insufficient workspace permissions (tasks require MEMBER role to create/update, ADMIN to delete)
|
||||
- **404 Not Found**: Task ID doesn't exist in this workspace
|
||||
@@ -239,6 +256,7 @@ Task operations require different permission levels:
|
||||
## Integration with Projects
|
||||
|
||||
Tasks can be linked to projects via `projectId`. This enables:
|
||||
|
||||
- Filtering tasks by project
|
||||
- Project-based task organization
|
||||
- Tracking project progress through task completion
|
||||
|
||||
Reference in New Issue
Block a user