import { chmod, mkdir, readFile, unlink, writeFile } from "node:fs/promises"; import { join } from "node:path"; import { getAgentDir } from "@earendil-works/pi-coding-agent"; export type TokenSource = "env" | "file" | "none"; const TOKEN_DIR = () => join(getAgentDir(), "ticktick"); const TOKEN_FILE = () => join(TOKEN_DIR(), "token"); export function getMcpUrl(): string { return process.env.TICKTICK_MCP_URL ?? "https://mcp.ticktick.com/"; } export async function getTokenSource(): Promise { if (process.env.TICKTICK_ACCESS_TOKEN?.trim() || process.env.TICKTICK_API_KEY?.trim()) { return "env"; } if (await hasSavedToken()) return "file"; return "none"; } export async function resolveToken(): Promise { const fromEnv = process.env.TICKTICK_ACCESS_TOKEN?.trim() || process.env.TICKTICK_API_KEY?.trim(); if (fromEnv) return fromEnv; try { const fromFile = await readFile(TOKEN_FILE(), "utf8"); return fromFile.trim() || null; } catch { return null; } } export async function saveToken(token: string): Promise { const trimmed = token.trim(); if (!trimmed) { throw new Error("Token cannot be empty"); } await mkdir(TOKEN_DIR(), { recursive: true }); await writeFile(TOKEN_FILE(), trimmed, { mode: 0o600 }); await chmod(TOKEN_FILE(), 0o600); } export async function clearToken(): Promise { try { await unlink(TOKEN_FILE()); return true; } catch { return false; } } export async function hasSavedToken(): Promise { try { const content = await readFile(TOKEN_FILE(), "utf8"); return Boolean(content.trim()); } catch { return false; } } export function getTokenFilePath(): string { return TOKEN_FILE(); } export function describeTokenSource(source: TokenSource): string { switch (source) { case "env": return "environment variable (TICKTICK_ACCESS_TOKEN)"; case "file": return `saved file (${TOKEN_FILE()})`; default: return "not configured — run /ticktick-setup or set TICKTICK_ACCESS_TOKEN"; } }