19420cc01d
- 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
127 lines
4.2 KiB
TypeScript
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);
|
|
}
|
|
}
|