Files
clinera-site/pi-worker/src/asana/client.ts
T
Azreen Jamal 19420cc01d feat: add pi-worker service - autonomous Asana task executor & project improvement agent
- Asana REST API client for task polling, section management, and commenting
- Pi CLI executor for running tasks with specialist agent teams
- Task loop: polls Asana To Do/Pi Worker sections, executes tasks, reports results
- Improvement loop: rotates through 8 improvement categories (code quality, security, docs, testing, etc)
- Health server on :8787
- File-based mutex locks with stale lock detection
- Docker + docker-compose deployment config
- deploy.sh for one-command deployment to cr-server-new via SSH/Incus
2026-03-06 15:26:13 +08:00

127 lines
4.2 KiB
TypeScript

import type { AsanaTask, AsanaSection, AsanaProject, AsanaComment } from "./types.js";
import { logger } from "../logger.js";
const ASANA_BASE = "https://app.asana.com/api/1.0";
export class AsanaClient {
private token: string;
private headers: Record<string, string>;
constructor(token: string) {
this.token = token;
this.headers = {
Authorization: `Bearer ${this.token}`,
"Content-Type": "application/json",
Accept: "application/json",
};
}
private async request<T>(path: string, options: RequestInit = {}): Promise<T> {
const url = `${ASANA_BASE}${path}`;
const res = await fetch(url, {
...options,
headers: { ...this.headers, ...options.headers },
});
if (!res.ok) {
const body = await res.text();
throw new Error(`Asana API ${res.status}: ${body}`);
}
const json = (await res.json()) as { data: T };
return json.data;
}
// Get all workspaces
async getWorkspaces(): Promise<Array<{ gid: string; name: string }>> {
return this.request("/workspaces");
}
// Get all projects in a workspace
async getProjects(workspaceGid: string): Promise<AsanaProject[]> {
return this.request(`/workspaces/${workspaceGid}/projects?opt_fields=name`);
}
// Get all sections in a project
async getSections(projectGid: string): Promise<AsanaSection[]> {
return this.request(`/projects/${projectGid}/sections`);
}
// Get tasks in a project
async getProjectTasks(projectGid: string): Promise<AsanaTask[]> {
return this.request(
`/projects/${projectGid}/tasks?opt_fields=name,notes,completed,assignee.name,tags.name,custom_fields,due_on,created_at,modified_at,memberships.project.name,memberships.section.name`
);
}
// Get tasks in a section
async getSectionTasks(sectionGid: string): Promise<AsanaTask[]> {
return this.request(
`/sections/${sectionGid}/tasks?opt_fields=name,notes,completed,assignee.name,tags.name,custom_fields,due_on,created_at,modified_at,memberships.project.name,memberships.section.name`
);
}
// Get single task details
async getTask(taskGid: string): Promise<AsanaTask> {
return this.request(
`/tasks/${taskGid}?opt_fields=name,notes,completed,assignee.name,tags.name,custom_fields,due_on,created_at,modified_at,memberships.project.name,memberships.section.name`
);
}
// Move task to a section
async moveTaskToSection(taskGid: string, sectionGid: string): Promise<void> {
await this.request(`/sections/${sectionGid}/addTask`, {
method: "POST",
body: JSON.stringify({ data: { task: taskGid } }),
});
}
// Update task (mark complete, change name, etc)
async updateTask(taskGid: string, updates: Record<string, any>): Promise<AsanaTask> {
return this.request(`/tasks/${taskGid}`, {
method: "PUT",
body: JSON.stringify({ data: updates }),
});
}
// Add a comment to a task
async addComment(taskGid: string, text: string): Promise<AsanaComment> {
return this.request(`/tasks/${taskGid}/stories`, {
method: "POST",
body: JSON.stringify({ data: { text } }),
});
}
// Get comments on a task
async getComments(taskGid: string): Promise<AsanaComment[]> {
return this.request(`/tasks/${taskGid}/stories?opt_fields=text,created_at`);
}
// Create a new task
async createTask(projectGid: string, data: { name: string; notes?: string; due_on?: string }): Promise<AsanaTask> {
return this.request("/tasks", {
method: "POST",
body: JSON.stringify({
data: {
...data,
projects: [projectGid],
},
}),
});
}
// Helper: Find section by name
async findSection(projectGid: string, sectionName: string): Promise<AsanaSection | null> {
const sections = await this.getSections(projectGid);
return sections.find((s) => s.name.toLowerCase().includes(sectionName.toLowerCase())) || null;
}
// Helper: Get incomplete tasks from a section by name
async getIncompleteTasks(projectGid: string, sectionName: string): Promise<AsanaTask[]> {
const section = await this.findSection(projectGid, sectionName);
if (!section) return [];
const tasks = await this.getSectionTasks(section.gid);
return tasks.filter((t) => !t.completed);
}
}