commit 0a45a1f5fb60854f3ef6d7aa5e098cc03f16674b
Author: Peter Krzyzek <peter@chykalophia.com>
Date:   Tue Feb 25 15:03:17 2025 -0600

    first commit

diff --git a/f/app_custom/folder.meta.yaml b/f/app_custom/folder.meta.yaml
new file mode 100644
index 0000000..573f64a
--- /dev/null
+++ b/f/app_custom/folder.meta.yaml
@@ -0,0 +1,5 @@
+summary: null
+display_name: App Custom Components
+extra_perms:
+  g/all: false
+owners: []
diff --git a/f/app_groups/folder.meta.yaml b/f/app_groups/folder.meta.yaml
new file mode 100644
index 0000000..d224ebe
--- /dev/null
+++ b/f/app_groups/folder.meta.yaml
@@ -0,0 +1,5 @@
+summary: null
+display_name: App Groups
+extra_perms:
+  g/all: false
+owners: []
diff --git a/f/app_themes/folder.meta.yaml b/f/app_themes/folder.meta.yaml
new file mode 100644
index 0000000..7c672ae
--- /dev/null
+++ b/f/app_themes/folder.meta.yaml
@@ -0,0 +1,5 @@
+summary: null
+display_name: App Themes
+extra_perms:
+  g/all: false
+owners: []
diff --git a/f/clickup/create_task.deno.ts b/f/clickup/create_task.deno.ts
new file mode 100644
index 0000000..eda7a8c
--- /dev/null
+++ b/f/clickup/create_task.deno.ts
@@ -0,0 +1,155 @@
+// clickup/create_task.ts
+type TaskInput = {
+  // REQUIRED FIELDS
+  /** The ID of the list to put this task into */
+  list_id: string;
+  /** Name of the task (max 99840 chars) */
+  name: string;
+
+  // OPTIONAL CORE FIELDS  
+  /** Detailed task description (supports markdown) */
+  description?: string;
+  /** Task status must match existing status in List */
+  status?: string;
+  /** Unix timestamp in milliseconds */
+  due_date?: number;
+  /** Unix timestamp in milliseconds */
+  start_date?: number;
+  /** Time estimate in milliseconds */
+  time_estimate?: number;
+  /** Markdown-formatted description */
+  markdown_description?: string;
+  /** Priority level (1-4) */
+  priority?: 1 | 2 | 3 | 4;
+  /** Parent task ID for subtasks */
+  parent?: string;
+  /** Custom task type ID */
+  custom_item_id?: number;
+
+  // PEOPLE MANAGEMENT  
+  /** Array of user IDs */
+  assignees?: number[];
+  /** Notify all task members */
+  notify_all?: boolean;
+
+  // TAGS & RELATIONSHIPS
+  /** Array of tag names */
+  tags?: string[];
+  /** Task dependencies */
+  depends_on?: string[];
+  /** Linked task IDs */
+  links_to?: string;
+
+  // CUSTOMIZATION
+  /** Array of custom field objects */
+  custom_fields?: {
+    /** Custom field ID */
+    id: string;
+    /** Field value (type-specific) */
+    value: string | number | boolean | string[];
+  }[];
+
+  // VALIDATION FLAGS
+  /** Enforce required custom fields */
+  check_required_custom_fields?: boolean;
+
+  // ADVANCED OPTIONS
+  /** Team ID for custom task IDs */
+  team_id?: number;
+  /** Custom task ID (requires team_id) */
+  custom_task_id?: string;
+  /** Task creation source (max 6 chars) */
+  source?: string;
+};
+
+// All we need here is the access token.
+type ClickupCredentials = {
+  access_token: string;
+};
+
+export async function main(
+  params: TaskInput,
+  credentials: ClickupCredentials
+) {
+  // 1. Input Validation
+  if (!params.list_id?.trim()) {
+    throw new Error("Missing required field: list_id");
+  }
+
+  if (!params.name?.trim()) {
+    throw new Error("Missing required field: name");
+  }
+
+  // 2. API Request Configuration
+  const response = await fetch(`https://api.clickup.com/api/v2/list/${params.list_id}/task`, {
+    method: "POST",
+    headers: {
+      "Content-Type": "application/json",
+      "accept": 'application/json',
+      "Authorization": "Bearer " + credentials.access_token
+    },
+    body: JSON.stringify({
+      // Required
+      name: params.name,
+
+      // Core optional parameters
+      description: params.description,
+      status: params.status,
+      due_date: params.due_date,
+      start_date: params.start_date,
+      time_estimate: params.time_estimate,
+      markdown_description: params.markdown_description,
+      priority: params.priority,
+      parent: params.parent,
+      custom_item_id: params.custom_item_id,
+
+      // People management
+      assignees: params.assignees,
+      notify_all: params.notify_all,
+
+      // Tags & relationships
+      tags: params.tags,
+      depends_on: params.depends_on,
+      links_to: params.links_to,
+
+      // Customization
+      custom_fields: params.custom_fields?.map(field => ({
+        id: field.id,
+        value: field.value
+      })),
+
+      // Validation flags
+      check_required_custom_fields: params.check_required_custom_fields,
+
+      // Advanced configurations
+      team_id: params.team_id,
+      custom_task_id: params.custom_task_id,
+      source: params.source
+    })
+
+  });
+
+  // 3. Error Handling
+  if (!response.ok) {
+    const errorBody = await response.text();
+    throw new Error(`ClickUp API Error (${response.status}): ${errorBody}`);
+  }
+
+  // 4. Response Processing
+  const result = await response.json();
+
+  // Validate successful task creation
+  if (!result?.id) {
+    throw new Error("Failed to create task: No ID returned");
+  }
+
+  return {
+    task_id: result.id,
+    url: result.url,
+    status: result.status?.status || "created",
+    _metadata: {
+      clickup_space: result.space?.id,
+      created_at: new Date().toISOString()
+    }
+  };
+}
diff --git a/f/clickup/create_task.script.yaml b/f/clickup/create_task.script.yaml
new file mode 100644
index 0000000..40e6a7b
--- /dev/null
+++ b/f/clickup/create_task.script.yaml
@@ -0,0 +1,21 @@
+summary: Create Task
+description: ''
+lock: ''
+kind: script
+schema:
+  $schema: 'https://json-schema.org/draft/2020-12/schema'
+  type: object
+  properties:
+    credentials:
+      type: object
+      description: ''
+      default: null
+      format: resource-clickup_credentials
+    params:
+      type: object
+      description: ''
+      default: null
+      format: resource-task_input
+  required:
+    - params
+    - credentials
diff --git a/f/clickup/folder.meta.yaml b/f/clickup/folder.meta.yaml
new file mode 100644
index 0000000..39427db
--- /dev/null
+++ b/f/clickup/folder.meta.yaml
@@ -0,0 +1,7 @@
+summary: ''
+display_name: clickup
+extra_perms:
+  g/all: true
+  u/peter: true
+owners:
+  - u/peter
diff --git a/f/clickup/get_task.deno.ts b/f/clickup/get_task.deno.ts
new file mode 100644
index 0000000..24aae88
--- /dev/null
+++ b/f/clickup/get_task.deno.ts
@@ -0,0 +1,205 @@
+// clickup/get_task.ts
+interface TaskResponse {
+  id: string;
+  name: string;
+  description?: string;
+  text_content?: string;
+  status?: {
+    status: string;
+    color: string;
+    orderindex: number;
+    type: "open" | "closed" | "custom";
+  };
+  orderindex?: string;
+  date_created?: string;
+  date_updated?: string;
+  date_closed?: string | null;
+  date_done?: string | null;
+  archived?: boolean;
+  creator?: {
+    id: number;
+    username: string;
+    email: string;
+    color: string;
+    profilePicture?: string;
+  };
+  assignees?: Array<{
+    id: number;
+    username: string;
+    email: string;
+    color: string;
+    initials: string;
+    profilePicture?: string;
+  }>;
+  watchers?: Array<{
+    id: number;
+    username: string;
+    email: string;
+    color: string;
+    initials: string;
+    profilePicture?: string;
+  }>;
+  checklists?: Array<{
+    id: string;
+    name: string;
+    orderindex: number;
+    resolved: number;
+    unresolved: number;
+    items: Array<{
+      id: string;
+      name: string;
+      orderindex: number;
+      assignee?: number;
+      resolved: boolean;
+      parent?: string;
+      date_created: string;
+      children?: string[];
+    }>;
+  }>;
+  tags?: Array<{
+    name: string;
+    tag_fg: string;
+    tag_bg: string;
+    creator?: number;
+  }>;
+  parent?: string | null;
+  priority?: {
+    id: string;
+    priority: "urgent" | "high" | "normal" | "low";
+    color: string;
+  };
+  due_date?: string | null;
+  start_date?: string | null;
+  time_estimate?: number | null;
+  time_spent?: number | null;
+  custom_fields?: Array<{
+    id: string;
+    name: string;
+    type: string;
+    type_config: Record<string, any>;
+    value: any;
+  }>;
+  list?: {
+    id: string;
+    name: string;
+    access: boolean;
+  };
+  project?: {
+    id: string;
+    name: string;
+    hidden: boolean;
+    access: boolean;
+  };
+  folder?: {
+    id: string;
+    name: string;
+    hidden: boolean;
+    access: boolean;
+  };
+  space?: {
+    id: string;
+    name: string;
+  };
+  url?: string;
+  permission_level?: string;
+  custom_item_id?: number | null;
+  custom_task_ids?: Array<{
+    custom_task_id: string;
+    team_id: string;
+  }>;
+  dependencies?: Array<{
+    task_id: string;
+    depends_on: string;
+    type: number;
+    date_created: string;
+    userid: string;
+  }>;
+  linked_tasks?: Array<{
+    task_id: string;
+    link_id: string;
+    date_created: string;
+    userid: string;
+  }>;
+  team_id?: string;
+  custom_id?: string | null;
+  attachments?: Array<{
+    id: string;
+    version: string;
+    date: string;
+    title: string;
+    extension: string;
+    thumbnail_small: string;
+    thumbnail_large: string;
+    size: number;
+  }>;
+  shared?: Array<{
+    id: string;
+    name: string;
+    type: string;
+    access_level: string;
+    team_id: string;
+  }>;
+  followers?: Array<{
+    id: number;
+    username: string;
+    email: string;
+    color: string;
+    initials: string;
+    profilePicture?: string;
+  }>;
+  [key: string]: any; // For future API additions
+}
+
+type FieldSelector = Array<keyof TaskResponse | string>;
+
+// All we need here is the access token.
+type ClickupCredentials = {
+  access_token: string;
+};
+
+export async function main(
+  task_id: string,
+  fields: FieldSelector = ["id", "name", "status", "date_created"],
+  credentials: ClickupCredentials,
+) {
+  // Input validation
+  if (!task_id?.trim()) throw new Error("Task ID is required");
+
+  // API call
+  const response = await fetch(
+    `https://api.clickup.com/api/v2/task/${task_id}`,
+    {
+      headers: {
+        "Authorization": credentials.access_token,
+      },
+    },
+  );
+
+  if (!response.ok) {
+    const error = await response.text();
+    throw new Error(`ClickUp Error ${response.status}: ${error}`);
+  }
+
+  // Full response parsing
+  const fullTask = await response.json();
+
+  // Field filtering
+  return fields.length > 0 ? filterFields(fullTask, fields) : fullTask;
+}
+
+// Recursive field filtering function
+function filterFields(obj: any, fields: FieldSelector): any {
+  return fields.reduce((acc, field) => {
+    const [root, ...nested] = (field as string).split(".");
+
+    if (obj[root] !== undefined) {
+      if (nested.length > 0 && typeof obj[root] === "object") {
+        acc[root] = filterFields(obj[root], [nested.join(".")]);
+      } else {
+        acc[root] = obj[root];
+      }
+    }
+
+    return acc;
+  }, {} as Record<string, any>);
+}
diff --git a/f/clickup/get_task.script.yaml b/f/clickup/get_task.script.yaml
new file mode 100644
index 0000000..6e81a29
--- /dev/null
+++ b/f/clickup/get_task.script.yaml
@@ -0,0 +1,35 @@
+summary: Get Task
+description: ''
+lock: ''
+kind: script
+schema:
+  $schema: 'https://json-schema.org/draft/2020-12/schema'
+  type: object
+  properties:
+    credentials:
+      type: object
+      description: ''
+      default: null
+      format: resource-clickup_credentials
+      properties:
+        access_token:
+          type: string
+          description: ''
+          originalType: string
+    fields:
+      type: object
+      description: ''
+      default:
+        - id
+        - name
+        - status
+        - date_created
+      format: resource-field_selector
+    task_id:
+      type: string
+      description: ''
+      default: null
+      originalType: string
+  required:
+    - task_id
+    - credentials
diff --git a/f/clickup/rotate_tokens.deno.ts b/f/clickup/rotate_tokens.deno.ts
new file mode 100644
index 0000000..2f8156e
--- /dev/null
+++ b/f/clickup/rotate_tokens.deno.ts
@@ -0,0 +1,132 @@
+import * as wmill from "jsr:@windmill/windmill";
+
+type ClickupCredentials = {
+  client_id: string;
+  expires_at: number;
+  access_token: string;
+  client_secret: string;
+};
+
+// Function to initiate OAuth flow and get initial tokens
+export async function get_initial_token(
+  client_id: string,
+  client_secret: string,
+  redirect_uri: string,
+) {
+  const state = crypto.randomUUID();
+
+  const authUrl = `https://app.clickup.com/api?client_id=${encodeURIComponent(client_id)
+    }&redirect_uri=${encodeURIComponent(redirect_uri)}&state=${encodeURIComponent(state)
+    }&response_type=code&scope=*`;
+
+  console.log("Please visit this URL to authorize the application:", authUrl);
+
+  return {
+    auth_url: authUrl,
+    state: state,
+    next_step:
+      "After authorization, you will receive a code in the redirect URI. Use this code with exchange_code_for_token function and verify the state parameter matches.",
+  };
+}
+
+// Function to exchange authorization code for tokens
+export async function exchange_code_for_token(
+  code: string,
+  client_id: string,
+  client_secret: string,
+  expected_state?: string,
+  received_state?: string,
+) {
+  if (expected_state && received_state) {
+    if (expected_state !== received_state) {
+      throw new Error("State parameter mismatch - possible CSRF attack");
+    }
+  }
+
+  const formData = new URLSearchParams({
+    client_id: client_id,
+    client_secret: client_secret,
+    code: code,
+    grant_type: "authorization_code"
+  });
+
+  try {
+    const response = await fetch("https://api.clickup.com/api/v2/oauth/token", {
+      method: "POST",
+      headers: {
+        "Content-Type": "application/x-www-form-urlencoded",
+      },
+      body: formData.toString(),
+    });
+
+    if (!response.ok) {
+      const errorText = await response.text();
+      console.error("Token exchange error details:", {
+        status: response.status,
+        statusText: response.statusText,
+        error: errorText,
+        requestBody: formData.toString(),
+      });
+      throw new Error(
+        `Token exchange failed: ${response.status} ${errorText}`,
+      );
+    }
+
+    const tokens = await response.json();
+
+    const credentials: ClickupCredentials = {
+      client_id,
+      client_secret,
+      access_token: tokens.access_token,
+      expires_at: Date.now() + (tokens.expires_in * 1000),
+    };
+
+    // Using setResource with string path instead of object
+    await wmill.setResource("f/clickup/clickup_prod_creds", JSON.stringify(credentials));
+
+    return {
+      status: "initialized",
+      expires_at: new Date(credentials.expires_at).toISOString(),
+    };
+  } catch (error) {
+    console.error("Detailed error:", error);
+    throw error;
+  }
+}
+
+export async function main(
+  clickupResource: ClickupCredentials,
+  mode: "check" | "refresh" | "initialize" = "check",
+  code?: string,
+  redirect_uri?: string,
+  state?: string,
+  expected_state?: string,
+) {
+  if (mode === "initialize") {
+    if (!clickupResource.client_id || !clickupResource.client_secret) {
+      throw new Error(
+        "Client ID and Client Secret are required for initialization",
+      );
+    }
+    if (!redirect_uri) {
+      throw new Error("Redirect URI is required for initialization");
+    }
+    return await get_initial_token(
+      clickupResource.client_id,
+      clickupResource.client_secret,
+      redirect_uri,
+    );
+  }
+
+  if (!code) {
+    throw new Error("Clickup oAuth CODE required to get token. Use Initialize step first.");
+  }
+
+  return await exchange_code_for_token(
+    code,
+    clickupResource.client_id,
+    clickupResource.client_secret,
+    expected_state,
+    state,
+  );
+}
\ No newline at end of file
diff --git a/f/clickup/rotate_tokens.script.lock b/f/clickup/rotate_tokens.script.lock
new file mode 100644
index 0000000..65bcba6
--- /dev/null
+++ b/f/clickup/rotate_tokens.script.lock
@@ -0,0 +1,11 @@
+{
+  "version": "4",
+  "specifiers": {
+    "jsr:@windmill/windmill@*": "1.458.2"
+  },
+  "jsr": {
+    "@windmill/windmill@1.458.2": {
+      "integrity": "c3188c099b9553cd92c5f3aa2b9a4f500feef31d8222237b527b7751de49558c"
+    }
+  }
+}
diff --git a/f/clickup/rotate_tokens.script.yaml b/f/clickup/rotate_tokens.script.yaml
new file mode 100644
index 0000000..2f48709
--- /dev/null
+++ b/f/clickup/rotate_tokens.script.yaml
@@ -0,0 +1,46 @@
+summary: Rotate Tokens
+description: ''
+lock: '!inline f/clickup/rotate_tokens.script.lock'
+concurrency_time_window_s: 0
+kind: script
+schema:
+  $schema: 'https://json-schema.org/draft/2020-12/schema'
+  type: object
+  order: []
+  properties:
+    clickupResource:
+      type: object
+      description: ''
+      default: null
+      format: resource-clickup_credentials
+    code:
+      type: string
+      description: ''
+      default: null
+      originalType: string
+    expected_state:
+      type: string
+      description: ''
+      default: null
+      originalType: string
+    mode:
+      type: string
+      description: ''
+      default: check
+      enum:
+        - check
+        - refresh
+        - initialize
+      originalType: enum
+    redirect_uri:
+      type: string
+      description: ''
+      default: null
+      originalType: string
+    state:
+      type: string
+      description: ''
+      default: null
+      originalType: string
+  required:
+    - clickupResource
diff --git a/f/clickup/utils/filter-task-fields.deno.ts b/f/clickup/utils/filter-task-fields.deno.ts
new file mode 100644
index 0000000..32e8dca
--- /dev/null
+++ b/f/clickup/utils/filter-task-fields.deno.ts
@@ -0,0 +1,64 @@
+// utils/filter-task-fields.ts
+import type { TaskResponse } from '../types/clickup';
+
+type FieldSelector = Array<string | NestedField>;
+type NestedField = {
+  field: string;
+  subfields?: FieldSelector;
+};
+
+export function filterTaskFields(
+  task: TaskResponse,
+  fields: FieldSelector
+): Partial<TaskResponse> {
+  return fields.reduce((acc, selector) => {
+    if (typeof selector === 'string') {
+      handleStringSelector(acc, task, selector);
+    } else if (selector.subfields) {
+      handleNestedSelector(acc, task, selector);
+    }
+    return acc;
+  }, {} as Partial<TaskResponse>);
+}
+
+function handleStringSelector(
+  acc: Partial<TaskResponse>,
+  task: TaskResponse,
+  selector: string
+) {
+  const [root, ...nested] = selector.split('.');
+
+  if (task[root as keyof TaskResponse] !== undefined) {
+    if (nested.length > 0) {
+      acc[root as keyof TaskResponse] = processNestedField(
+        task[root as keyof TaskResponse],
+        nested.join('.')
+      );
+    } else {
+      acc[root as keyof TaskResponse] = task[root as keyof TaskResponse];
+    }
+  }
+}
+
+function handleNestedSelector(
+  acc: Partial<TaskResponse>,
+  task: TaskResponse,
+  selector: NestedField
+) {
+  const value = task[selector.field as keyof TaskResponse];
+  if (value && typeof value === 'object') {
+    acc[selector.field as keyof TaskResponse] = Array.isArray(value)
+      ? value.map(item => filterTaskFields(item as TaskResponse, selector.subfields!))
+      : filterTaskFields(value as TaskResponse, selector.subfields!);
+  }
+}
+
+function processNestedField(value: any, nestedPath: string): any {
+  const [current, ...remaining] = nestedPath.split('.');
+
+  if (value[current] === undefined) return undefined;
+
+  return remaining.length > 0
+    ? processNestedField(value[current], remaining.join('.'))
+    : value[current];
+}
\ No newline at end of file
diff --git a/f/clickup/utils/filter-task-fields.script.yaml b/f/clickup/utils/filter-task-fields.script.yaml
new file mode 100644
index 0000000..4d5bc0b
--- /dev/null
+++ b/f/clickup/utils/filter-task-fields.script.yaml
@@ -0,0 +1,10 @@
+summary: Filter Task Fields
+description: ''
+lock: ''
+kind: script
+no_main_func: true
+schema:
+  $schema: 'https://json-schema.org/draft/2020-12/schema'
+  type: object
+  properties: {}
+  required: []
diff --git a/f/clickup_utils/folder.meta.yaml b/f/clickup_utils/folder.meta.yaml
new file mode 100644
index 0000000..8cf74bd
--- /dev/null
+++ b/f/clickup_utils/folder.meta.yaml
@@ -0,0 +1,6 @@
+summary: null
+display_name: clickup_utils
+extra_perms:
+  u/peter: true
+owners:
+  - u/peter
diff --git a/f/types/clickup.deno.ts b/f/types/clickup.deno.ts
new file mode 100644
index 0000000..523ee08
--- /dev/null
+++ b/f/types/clickup.deno.ts
@@ -0,0 +1,241 @@
+// types/clickup.d.ts
+
+/**
+ * Base Task Interface - Shared mutable fields across operations
+ * Contains all fields that can be SENT in create/update requests
+ * and RECEIVED in responses (excluding read-only metadata)
+ */
+interface TaskBase {
+  // Core Content
+  name: string;
+  description?: string;
+  text_content?: string;
+
+  // Status & Priority
+  status?: string;
+  priority?: 1 | 2 | 3 | 4;
+
+  // Time Management
+  due_date?: number | null;
+  start_date?: number | null;
+  time_estimate?: number | null;
+  time_spent?: number | null;
+
+  // People & Assignments
+  assignees?: number[];
+  tags?: string[];
+  custom_fields?: Array<{
+    id: string;
+    value: any;
+  }>;
+
+  // Task Relationships
+  parent?: string | null;
+  links_to?: string;
+
+  // Advanced Features
+  markdown_description?: string;
+  notify_all?: boolean;
+  check_required_custom_fields?: boolean;
+  custom_item_id?: number | null;
+  source?: string;
+}
+
+/**
+ * Task Creation Parameters
+ * @see https://developer.clickup.com/reference/createtask
+ */
+interface TaskCreate extends TaskBase {
+  /** Required Fields */
+  list_id: string;
+  name: string;
+
+  /** Creation-Specific Fields */
+  custom_task_id?: string;
+  team_id?: number;
+}
+
+/**
+ * Task Update Parameters
+ * @see https://developer.clickup.com/reference/updatetask
+ */
+interface TaskUpdate extends Partial<TaskBase> {
+  /** Required Identifier */
+  id: string;
+
+  /** Update-Specific Fields */
+  task_revision?: number;
+  task_template_id?: string;
+  idempotency_key?: string;
+}
+
+/**
+ * Full Task Response
+ * @see https://developer.clickup.com/reference/gettask
+ */
+interface TaskResponse extends TaskBase {
+  /** Read-Only Metadata */
+  id: string;
+  url: string;
+  orderindex: string;
+  date_created: string;
+  date_updated: string;
+  date_closed?: string | null;
+  date_done?: string | null;
+  archived: boolean;
+
+  /** System Relationships */
+  list: {
+    id: string;
+    name: string;
+    access: boolean;
+  };
+  space: {
+    id: string;
+    name: string;
+  };
+  folder: {
+    id: string;
+    name: string;
+    hidden: boolean;
+    access: boolean;
+  };
+
+  /** People & Permissions */
+  creator: User;
+  watchers: User[];
+  followers: User[];
+  permission_level: string;
+  shared: SharedAccess[];
+
+  /** Complex Structures */
+  checklists: Checklist[];
+  dependencies: Dependency[];
+  linked_tasks: LinkedTask[];
+  subtasks: TaskResponse[];
+  attachments: Attachment[];
+
+  /** Status Details */
+  status: {
+    status: string;
+    color: string;
+    orderindex: number;
+    type: 'open' | 'closed' | 'custom';
+  };
+
+  /** Priority Details */
+  priority: {
+    id: string;
+    priority: 'urgent' | 'high' | 'normal' | 'low';
+    color: string;
+  } | null;
+}
+
+// Supporting Interfaces
+interface User {
+  id: number;
+  username: string;
+  email: string;
+  color: string;
+  initials: string;
+  profilePicture?: string;
+}
+
+interface Checklist {
+  id: string;
+  name: string;
+  orderindex: number;
+  resolved: number;
+  unresolved: number;
+  items: ChecklistItem[];
+}
+
+interface ChecklistItem {
+  id: string;
+  name: string;
+  orderindex: number;
+  assignee?: number;
+  resolved: boolean;
+  parent?: string;
+  date_created: string;
+  children?: string[];
+}
+
+interface Tag {
+  name: string;
+  tag_fg: string;
+  tag_bg: string;
+  creator?: number;
+}
+
+interface Dependency {
+  task_id: string;
+  depends_on: string;
+  type: number;
+  date_created: string;
+  userid: string;
+}
+
+interface LinkedTask {
+  task_id: string;
+  link_id: string;
+  date_created: string;
+  userid: string;
+}
+
+interface CustomField {
+  id: string;
+  name: string;
+  type: string;
+  type_config: {
+    [key: string]: any;
+    options?: Array<{
+      id: string;
+      name: string;
+      color?: string;
+      orderindex?: number;
+    }>;
+  };
+  value: any;
+}
+
+interface Attachment {
+  id: string;
+  version: string;
+  date: string;
+  title: string;
+  extension: string;
+  thumbnail_small: string;
+  thumbnail_large: string;
+  size: number;
+}
+
+interface SharedAccess {
+  id: string;
+  name: string;
+  type: string;
+  access_level: string;
+  team_id: string;
+}
+
+/**
+ * Full task hierarchy types
+ * Ref: https://developer.clickup.com/docs
+ */
+interface ClickUpHierarchy {
+  workspace_id: string;
+  space_id: string;
+  folder_id?: string;
+  list_id: string;
+  task_id: string;
+}
+
+/**
+ * API error response format
+ * Ref: https://developer.clickup.com/docs/errors
+ */
+interface ClickUpError {
+  err: string;
+  code: number;
+  message?: string;
+}
diff --git a/f/types/clickup.script.yaml b/f/types/clickup.script.yaml
new file mode 100644
index 0000000..f20f790
--- /dev/null
+++ b/f/types/clickup.script.yaml
@@ -0,0 +1,16 @@
+summary: clickup
+description: ''
+lock: ''
+concurrency_time_window_s: 0
+kind: script
+no_main_func: true
+schema:
+  $schema: 'https://json-schema.org/draft/2020-12/schema'
+  type: object
+  order:
+    - a
+    - b
+    - d
+    - e
+  properties: {}
+  required: []
diff --git a/f/types/folder.meta.yaml b/f/types/folder.meta.yaml
new file mode 100644
index 0000000..3f4c6d1
--- /dev/null
+++ b/f/types/folder.meta.yaml
@@ -0,0 +1,6 @@
+summary: null
+display_name: types
+extra_perms:
+  u/peter: true
+owners:
+  - u/peter
diff --git a/f/utils/folder.meta.yaml b/f/utils/folder.meta.yaml
new file mode 100644
index 0000000..26c6e13
--- /dev/null
+++ b/f/utils/folder.meta.yaml
@@ -0,0 +1,6 @@
+summary: null
+display_name: utils
+extra_perms:
+  u/peter: true
+owners:
+  - u/peter
diff --git a/wmill-lock.yaml b/wmill-lock.yaml
new file mode 100644
index 0000000..f97633f
--- /dev/null
+++ b/wmill-lock.yaml
@@ -0,0 +1,6 @@
+locks:
+  f/clickup/create_task: 43a306643623f8d42f2e9432175d2061cb05bd4ac0825972c93559163b773ad0
+  f/clickup/get_task: e1ac7c952de428bd53df01621c05ecb3995e38cf5f78de1ab03c6baeda9da760
+  f/clickup/rotate_tokens: d0a53e8f1855ab682ebf0cc89d093e8e98db761d9225831c7c9c2d0014f4f6bb
+  f/clickup/utils/filter-task-fields: a160d19c2fa752fa3828f2c87b498be1e79f383ac271678a7dc51c63a4237b4a
+  f/types/clickup: eb03485311a666b6410b7a621f063dcc84aee0e3a198a721165abdccdf690fa4
diff --git a/wmill.yaml b/wmill.yaml
new file mode 100644
index 0000000..d38463d
--- /dev/null
+++ b/wmill.yaml
@@ -0,0 +1,10 @@
+defaultTs: bun
+includes:
+  - f/**
+excludes: []
+codebases: []
+skipVariables: true
+skipResources: true
+skipSecrets: true
+includeSchedules: false
+includeTriggers: false