/**
* Content Plan Builder - Asana Client
*
* Minimal Asana API client for creating projects and tasks.
*/
import type { ContentProjectPlan, AsanaCreationResult } from './types.js';
const ASANA_API_BASE = 'https://app.asana.com/api/1.0';
interface AsanaRequestOptions {
method: 'GET' | 'POST' | 'PUT' | 'DELETE';
path: string;
body?: Record<string, unknown>;
accessToken: string;
}
/**
* Make a request to the Asana API
*/
async function asanaRequest<T>(options: AsanaRequestOptions): Promise<T> {
const url = `${ASANA_API_BASE}${options.path}`;
const response = await fetch(url, {
method: options.method,
headers: {
'Authorization': `Bearer ${options.accessToken}`,
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: options.body ? JSON.stringify(options.body) : undefined
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Asana API error (${response.status}): ${errorText}`);
}
const data = await response.json() as { data: T };
return data.data;
}
interface AsanaProject {
gid: string;
name: string;
permalink_url?: string;
}
interface AsanaTask {
gid: string;
name: string;
}
/**
* Create a project in Asana
*/
async function createProject(
accessToken: string,
teamGid: string,
name: string
): Promise<AsanaProject> {
return asanaRequest<AsanaProject>({
method: 'POST',
path: '/projects',
accessToken,
body: {
data: {
name,
team: teamGid,
color: 'light-blue'
}
}
});
}
/**
* Create a task in Asana
*/
async function createTask(
accessToken: string,
projectGid: string,
name: string,
description: string,
parentGid?: string
): Promise<AsanaTask> {
const body: Record<string, unknown> = {
data: {
name,
notes: description,
assignee_status: 'upcoming'
}
};
if (parentGid) {
// Create as subtask
(body.data as Record<string, unknown>).parent = parentGid;
} else {
// Create as top-level task in project
(body.data as Record<string, unknown>).projects = [projectGid];
}
return asanaRequest<AsanaTask>({
method: 'POST',
path: '/tasks',
accessToken,
body
});
}
/**
* Add a task to a project
*/
async function addTaskToProject(
accessToken: string,
taskGid: string,
projectGid: string
): Promise<void> {
await asanaRequest({
method: 'POST',
path: `/tasks/${taskGid}/addProject`,
accessToken,
body: {
data: {
project: projectGid
}
}
});
}
/**
* Set task dependency
*/
async function setTaskDependency(
accessToken: string,
taskGid: string,
dependsOnGid: string
): Promise<void> {
try {
await asanaRequest({
method: 'POST',
path: `/tasks/${taskGid}/addDependencies`,
accessToken,
body: {
data: {
dependencies: [dependsOnGid]
}
}
});
} catch (error) {
// Dependencies require Asana Premium - log and continue
console.error(`Could not set dependency (may require Premium): ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Create a complete project in Asana from a generated plan
*/
export async function createAsanaProject(
plan: ContentProjectPlan,
accessToken: string,
teamGid: string,
projectName?: string,
existingProjectGid?: string
): Promise<AsanaCreationResult> {
try {
// Step 1: Create project or use existing
let projectGid: string;
let projectUrl: string;
if (existingProjectGid) {
projectGid = existingProjectGid;
projectUrl = `https://app.asana.com/0/${projectGid}/list`;
} else {
const name = projectName || 'Generated Project';
const project = await createProject(accessToken, teamGid, name);
projectGid = project.gid;
projectUrl = project.permalink_url || `https://app.asana.com/0/${projectGid}/list`;
}
// Step 2: Create all tasks and track mapping
const taskMapping: Record<string, string> = {};
let totalTasksCreated = 0;
for (const topTask of plan.topLevelTasks) {
// Create top-level task
const createdTopTask = await createTask(
accessToken,
projectGid,
topTask.name,
formatTaskDescription(topTask.description, topTask.estimate, topTask.assignedTo)
);
taskMapping[topTask.name] = createdTopTask.gid;
totalTasksCreated++;
console.error(`Created top-level task: ${topTask.name}`);
// Create subtasks
for (const subtask of topTask.subtasks) {
const createdSubtask = await createTask(
accessToken,
projectGid,
subtask.name,
formatTaskDescription(subtask.description, subtask.estimate, subtask.assignedTo),
createdTopTask.gid
);
taskMapping[subtask.name] = createdSubtask.gid;
totalTasksCreated++;
console.error(`Created subtask: ${subtask.name}`);
}
}
// Step 3: Set dependencies
for (const topTask of plan.topLevelTasks) {
for (const subtask of topTask.subtasks) {
if (subtask.blockedBy && subtask.blockedBy.toLowerCase() !== 'n/a') {
// Parse dependencies (could be comma-separated)
const dependencies = subtask.blockedBy.split(',').map(d => d.trim());
for (const dep of dependencies) {
if (taskMapping[dep] && taskMapping[subtask.name]) {
await setTaskDependency(
accessToken,
taskMapping[subtask.name],
taskMapping[dep]
);
console.error(`Set dependency: ${subtask.name} depends on ${dep}`);
}
}
}
}
}
return {
status: 'success',
projectGid,
projectUrl,
totalTasksCreated,
taskMapping
};
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error';
console.error(`Asana project creation failed: ${message}`);
return {
status: 'error',
error: message
};
}
}
/**
* Format a task description with metadata
*/
function formatTaskDescription(
description: string,
estimate: string,
assignedTo: string
): string {
let notes = description;
if (estimate && estimate !== 'TBD') {
notes += `\n\nEstimate: ${estimate}`;
}
if (assignedTo && assignedTo !== 'Unassigned') {
notes += `\nSuggested Role: ${assignedTo}`;
}
return notes;
}
/**
* Validate Asana credentials
*/
export async function validateAsanaCredentials(accessToken: string): Promise<boolean> {
try {
await asanaRequest({
method: 'GET',
path: '/users/me',
accessToken
});
return true;
} catch {
return false;
}
}