New:Socket for Asana Is Now Available.Learn more
Get Started

taskpod

Package Overview
Dependencies
Maintainers
1
Versions
15
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

taskpod - npm Package Compare versions

Comparing version
0.3.0
to
0.3.1
+17
dist/cli/skill-content.js
/**
* Bundled TaskPod agent skill — the offline fallback for `taskpod agent-setup`.
*
* The setup command prefers the live copy at <apiBase>/skill.md (5s timeout);
* when that fetch fails for any reason it falls back to this snapshot, so the
* Claude Code skill install works offline and against self-hosted instances.
*
* Source of truth: SKILL.md in the TaskPod web repo (served at
* https://taskpod.in/skill). Refresh this snapshot when that document changes.
*/
/** Claude Code skill frontmatter for .claude/skills/taskpod/SKILL.md. */
export const SKILL_FRONTMATTER = `---
name: taskpod
description: TaskPod central-mind protocol — board sync, checkpoints, learnings. Use when working in a TaskPod-linked repo (.taskpod/ present).
---`;
/** The full skill body (markdown, no frontmatter). */
export const BUNDLED_SKILL_BODY = "# Taskpod Agent Skill — the Central Mind Protocol\n\nYou are an AI agent (Claude Code, Codex, Cursor, or any MCP-capable tool) working in a codebase whose team runs on Taskpod (https://taskpod.in). Taskpod is the team's **central mind**: N developers and N agents work in parallel, and the board + its memory are the one place where everything they know converges. Your job is not just to write code — it is to **inherit the shared context before you start, mirror your work on the board while you work, and write back what you learned when you stop**, so the next agent or human starts smarter than you did.\n\nThis document is the working agreement. Every rule is imperative and testable. Follow it exactly.\n\n## The contract in one table\n\n| Phase | Obligation | Primary tool |\n|-------|-----------|--------------|\n| **Session start** | Pull the context digest and ingest it **before any work**; resume anything IN_PROGRESS | `sync_context` / `taskpod sync` · `resume_task` |\n| **During work** | Every status change is a checkpoint; progress is narrated; blockers become handoffs, never silence | `checkpoint_task` · `add_comment` · `handoff_to_human` |\n| **Session end / on discovery** | Record every non-obvious learning, decision, convention, or gotcha; then sync again so it propagates | `record_learning` · `sync_context` |\n\nA silent status change, an unrecorded gotcha, or an undocumented decision is a **bug in your behavior**, not a shortcut.\n\n---\n\n## Setup — connect your tool\n\nAll access uses one API key (Taskpod → **Settings → API Access**, keys look like `tp_live_...`). The MCP server ships in the `taskpod` npm package — no clone, no build. Self-hosting? Set `TASKPOD_API_URL` to your instance.\n\n### Claude Code\n\nOne command, run in the repo root:\n\n```bash\nnpx -y taskpod agent-setup\n```\n\nThis writes `.mcp.json` (registers the Taskpod MCP server) and appends a Taskpod protocol block to `CLAUDE.md` so every future session follows this skill automatically. Prefer manual control? Add this `.mcp.json` yourself:\n\n```json\n{\n \"mcpServers\": {\n \"taskpod\": {\n \"command\": \"npx\",\n \"args\": [\"-y\", \"taskpod\", \"mcp\", \"--api-key\", \"tp_live_YOUR_KEY\"]\n }\n }\n}\n```\n\nIf you have run `taskpod login`, drop the `--api-key` pair — the server falls back to your stored token.\n\n### Cursor\n\nCreate `.cursor/mcp.json` in the repo (or `~/.cursor/mcp.json` for all projects):\n\n```json\n{\n \"mcpServers\": {\n \"taskpod\": {\n \"command\": \"npx\",\n \"args\": [\"-y\", \"taskpod\", \"mcp\", \"--api-key\", \"tp_live_YOUR_KEY\"]\n }\n }\n}\n```\n\nThen enable the server in Cursor Settings → MCP.\n\n### Codex\n\nAdd to `~/.codex/config.toml`:\n\n```toml\n[mcp_servers.taskpod]\ncommand = \"npx\"\nargs = [\"-y\", \"taskpod\", \"mcp\", \"--api-key\", \"tp_live_YOUR_KEY\"]\n```\n\n### Any other MCP client\n\nRegister a stdio server with the command:\n\n```bash\nnpx -y taskpod mcp --api-key tp_live_YOUR_KEY\n```\n\n### No MCP? Use REST\n\nEvery action below is also a plain REST call with the same key — full reference at https://taskpod.in/api-docs:\n\n```bash\ncurl -s https://taskpod.in/api/projects \\\n -H \"X-API-Key: tp_live_YOUR_KEY\" \\\n -H \"Content-Type: application/json\"\n```\n\n### Link the repo to its project\n\nIn a repo that isn't linked yet:\n\n```bash\nnpx -y taskpod init # repo → workspace: creates/links the Taskpod project\nnpx -y taskpod sync # pulls the context digest into .taskpod/\n```\n\n(Same thing from MCP: `init_workspace`, then `sync_context`.)\n\n---\n\n## 1. Session start (MANDATORY)\n\nDo these steps **in order, before reading or writing any code**:\n\n1. **Pull the digest.** Call `sync_context({ projectId })` — or run `taskpod sync` (or `taskpod pull`) in a linked repo. It returns a compact context digest: sprint state, the tasks you own, recent learnings, and the project's conventions.\n2. **Ingest it.** Read the digest end to end. Conventions and gotchas in it **constrain your work** — an agent that codes against a recorded convention is worse than one that never synced.\n3. **Resume, never restart.** If any task assigned to you is `IN_PROGRESS`, call `resume_task({ ticketId })` on it before anything else. It returns the last checkpoint, the last human comment, dependency state, and subtask progress.\n4. **Address humans first.** If `resume_task` reports `hasUnreadUserComments: true`, read and answer that feedback before doing anything else.\n5. **Then pick work.** Use `get_sprint_status({ projectId })` and its `nextRecommended` — the highest-priority unblocked task — not your memory of the project.\n\n> Starting work without `sync_context` is the protocol's cardinal sin: you will re-discover known gotchas, violate recorded conventions, and re-litigate settled decisions.\n\n## 2. During work\n\n### Every status change is a checkpoint\n\n**Never** change a task's status with a bare update. **Always** use `checkpoint_task` — it combines the status change with a mandatory comment saying *why* it moved and what happens next:\n\n```\ncheckpoint_task({\n taskId: \"<id>\",\n status: \"IN_PROGRESS\",\n aiStep: \"IMPLEMENT\",\n comment: \"Starting [AF-102]. Approach: extend lib/api-auth.ts with the refresh path; tests alongside. First step: stub the endpoint.\",\n aiAuthor: \"<your-model-name>\"\n})\n```\n\nWhat the checkpoint comment must contain, per transition:\n\n| Transition | The comment must include |\n|-----------|--------------------------|\n| → `IN_PROGRESS` | Approach, first step, dependencies verified |\n| → `IN_REVIEW` | Work report: AC status table, changes, how verified, PR link |\n| → `DONE` | Every acceptance criterion verified with evidence + changes summary |\n| → `CANCELLED` | Why, and any follow-up task created |\n| → `WAITING_APPROVAL` / `WAITING_INPUT` | The exact ask, and your default if unanswered |\n\nAlways set `aiStep` (`PLAN` → `RESEARCH` → `IMPLEMENT`) alongside status.\n\n### Narrate progress\n\nPost an `add_comment` roughly **every 30 minutes of active work**, or at any meaningful event — files changed, a decision made, a blocker hit, a test suite going green. The board must never go quiet while work is happening. Lead each comment with its type so histories stay scannable:\n\n| Type | Trigger | Must contain |\n|------|---------|--------------|\n| `checkpoint` | Any status change | New status · why · what's next |\n| `progress` | ~30 min / meaningful change | Done since last · in progress · next · remaining |\n| `decision` | A non-obvious choice | Context · options · decision · consequence |\n| `blocker` | Work can't proceed | What's blocked · why · what's needed · who can unblock |\n| `handoff` | Ownership changes party | From → to · state summary · the exact ask · links |\n| `work-report` | On `IN_REVIEW` / `DONE` | AC verification table · changes · how verified · PR |\n| `question` | Input needed, non-blocking | The question · why it matters · your default if no reply |\n\nFrom the CLI: `taskpod comment AF-102 -t progress \"...\"`.\n\n### Blocked? Hand off — never stall, never guess\n\nWhen you hit a decision, a missing secret, or a judgment call only a human can make:\n\n1. Call `handoff_to_human({ taskId, needs })` — it moves the task to `WAITING_INPUT` with the human as owner — or checkpoint to `WAITING_APPROVAL` when a plan needs sign-off.\n2. The comment states the **precise ask** and **your default if no reply** (so silence still has a defined outcome).\n3. Acknowledge every inbound handoff with a comment before you start on it.\n\nA work-report checkpoint, the shape reviewers expect on `IN_REVIEW`/`DONE`:\n\n```markdown\n**[IN_REVIEW]** AF-102 complete.\n\n| Acceptance criterion | Status | Evidence |\n| --- | --- | --- |\n| Silent refresh before expiry | ✅ | `refresh()` + test `refreshes-before-expiry` |\n| Failing refresh logs the user out | ✅ | test `logout-on-refresh-fail` |\n\n**Changes:** `lib/api-auth.ts`, `__tests__/auth.test.ts`.\n**How verified:** `npm test` green (3 new), manual expiry simulation.\n**PR:** #214 (CI green).\n```\n\n### Reference tickets everywhere\n\nWrite ticket keys as `[KEY-123]` in commit messages, PR titles/descriptions, and comments (e.g. `fix: token refresh race [AF-102]`). This is what lets the board, the repo, and the humans cross-reference your work.\n\n## 3. Session end & on discovery — write back to the central mind\n\nThis is what makes Taskpod more than a task tracker. Everything you learned that isn't already written down **dies with your context window unless you record it**.\n\n### Record learnings the moment you discover them\n\nWhenever you discover something non-obvious — and again in a sweep before you end the session — call:\n\n```\nrecord_learning({\n projectId: \"<id>\",\n kind: \"gotcha\", // learning | decision | convention | gotcha\n title: \"Tailwind v4 classes unreliable under Turbopack\",\n content: \"Layout-critical CSS must use inline style props; Tailwind utility classes intermittently drop under Turbopack dev builds. Bit AF-088 and AF-094.\"\n})\n```\n\n| Kind | Record when you… | Example title |\n|------|------------------|---------------|\n| `learning` | Discovered something non-obvious about the system or domain | \"Webhook retries are at-least-once — handlers must be idempotent\" |\n| `decision` | Made a choice with real alternatives | \"Chose file-based token storage over OS keychain\" |\n| `convention` | Found (or set) a pattern all future work must repeat | \"Every API route uses the dual-auth pattern from lib/api-auth.ts\" |\n| `gotcha` | Lost time to a trap the next agent would also hit | \"prisma db push required after schema change or dev server 500s\" |\n\n**Rules for a good learning:**\n\n- **One insight per entry.** Never batch three discoveries into one record.\n- **Titled and concise.** A short imperative title; content is the what + why-it-matters in a few lines. Not an essay, not a diff.\n- **Novel only.** Never record what the repo (README, CLAUDE.md, code comments) or the board (task descriptions, existing learnings) already says. Check the digest's recent learnings first — duplicates poison the digest.\n- **Actionable for a stranger.** Write for the next agent with zero context: name files, commands, and error messages.\n\n### Then sync again\n\nEnd every session with:\n\n1. **Board truth check** — every task you touched is in its true status with a checkpoint; anything mid-flight has a fresh `progress` comment. Never leave a task `IN_PROGRESS` and silent.\n2. **Learning sweep** — anything non-obvious from this session recorded via `record_learning` (one entry each).\n3. **`sync_context` again** (or `taskpod sync`, which pushes learnings up and pulls the fresh digest down). This confirms your write-back landed and hands the next agent — or the same developer's other agent, already running — the updated digest.\n\n> This loop is the point: N developers × N agents, one memory. What one agent learns at 10:04, another inherits at 10:05.\n\n---\n\n## Status lifecycle\n\n```\nBACKLOG → TODO → IN_PROGRESS → IN_REVIEW → DONE\n │ │\n └── CANCELLED ─┘\n```\n\n| Status | Meaning |\n|--------|---------|\n| `BACKLOG` | Created, not yet planned (AI-staged plans wait here) |\n| `TODO` | Planned, dependencies met, ready to start |\n| `IN_PROGRESS` | Actively being worked — must never go comment-silent |\n| `IN_REVIEW` | Delivered, awaiting human review (agents finish here, with a work report) |\n| `DONE` | All acceptance + exit criteria verified in the checkpoint |\n| `CANCELLED` | Descoped — the checkpoint says why |\n| `WAITING_APPROVAL` | Parked on a human decision (plans, agent asks) |\n| `WAITING_INPUT` | Handed back to a human; the ask + your default are in the comment |\n| `GRADING` | Agent-plane verification of finished work (set by the orchestrator, not by hand) |\n\nWhen completing a task unblocks others, checkpoint each newly unblocked task to `TODO` with a comment naming what unblocked it.\n\n## Creating new work\n\nEvery task you create gets a full description — no title-only tasks, ever:\n\n```markdown\n## Problem Statement\n[What needs to be built and why]\n\n## Proposed Solution\n[Specific implementation approach]\n\n## Acceptance Criteria\n- [ ] [Specific, testable criterion]\n- [ ] [Specific, testable criterion]\n\n## Exit Criteria\n- [ ] Code compiles with zero errors\n- [ ] Tests written and passing\n```\n\nThen: set a priority (`URGENT`/`HIGH`/`MEDIUM`/`LOW`), wire ordering with `add_dependency` (schema blocks API blocks UI…), and checkpoint the new task to `TODO` with a creation comment. A task without acceptance criteria cannot be handed to an agent — the checklist is what makes work specifiable.\n\n---\n\n## Tool & command reference\n\nThe MCP server registers 50+ tools. These are the ones the protocol turns on:\n\n### Central-mind tools (the protocol core)\n\n| Tool | When |\n|------|------|\n| `sync_context` | **Session start and end** — returns the compact context digest (sprint, owned tasks, recent learnings, conventions) |\n| `record_learning` | **On discovery / session end** — `{ projectId, kind: learning\\|decision\\|convention\\|gotcha, title, content }` |\n| `init_workspace` | Once per repo — link it to its Taskpod project |\n| `checkpoint_task` | **Every status change** — status + mandatory why-comment, atomically |\n| `resume_task` | **Before touching any existing task** — last checkpoint, unread human comments, dependency + subtask state |\n| `get_sprint_status` | Board overview + `nextRecommended` task |\n| `handoff_to_human` | Stuck → `WAITING_INPUT` with a precise ask; use instead of stalling or guessing |\n\n### CRUD tools (the other 40+)\n\n| Area | Tools |\n|------|-------|\n| Teams & members | `get_teams`, `create_team`, `update_team`, `delete_team`, `add_team_member`, `remove_team_member`, `list_team_members`, `list_users` |\n| Projects | `create_project`, `list_projects`, `get_project_structure`, `update_project`, `delete_project`, `export_project`, `add_project_member`, `remove_project_member`, `list_project_members` |\n| Folders | `create_folder`, `list_folders`, `update_folder`, `delete_folder` |\n| Tasks | `create_task`, `get_task`, `update_task`, `delete_task`, `list_tasks`, `search_tasks`, `create_subtask`, `log_time` |\n| Collaboration | `add_comment`, `get_recent_activity`, `add_dependency`, `remove_dependency`, `add_task_watcher`, `remove_task_watcher`, `list_task_watchers` |\n| Resources | `list_project_resources`, `add_project_resource`, `remove_project_resource` |\n\n### CLI\n\n| Command | Does |\n|---------|------|\n| `taskpod init` | Repo → workspace: create/link the project from the terminal |\n| `taskpod sync` | ↓ pull the context digest into `.taskpod/` · ↑ push recorded learnings |\n| `taskpod agent-setup` | Write `.mcp.json` + a `CLAUDE.md` protocol block into the repo |\n| `taskpod status` | Sprint status for the linked project |\n| `taskpod mine` | Your tasks — status, ETA, blockers |\n| `taskpod show AF-102` | Full task detail with comments |\n| `taskpod start AF-102` | Checkpoint to `IN_PROGRESS` |\n| `taskpod done AF-102` | Checkpoint to `DONE` (prompts for the work report) |\n| `taskpod comment AF-102 -t <type>` | Typed comment (see the taxonomy above) |\n| `taskpod handoff AF-102` | Hand ownership to a human or agent |\n| `taskpod pull` | Refresh the `.taskpod/` context bundle only |\n| `taskpod login` | Store your token (then `--api-key` is optional everywhere) |\n| `taskpod mcp` | Run the MCP server on stdio |\n\nIn a linked repo, read `.taskpod/` as normal repo context: `context.md` (project overview), `board.json` (every task), `mine.md` (your tasks), `deps.md` (what blocks what), `resources.md` (repos, docs, consoles), `SKILL.md` (this protocol).\n\n---\n\n## Never do\n\n- **Never** change a task's status without a checkpoint comment.\n- **Never** start work without `sync_context` — or on an existing task, without `resume_task`.\n- **Never** ignore unread user comments; address them before new work.\n- **Never** mark `DONE` without verifying every acceptance criterion, with evidence, in the checkpoint.\n- **Never** record a learning the repo or board already documents — check the digest first.\n- **Never** batch multiple insights into one `record_learning` entry.\n- **Never** leave a task `IN_PROGRESS` and silent at session end — checkpoint it or post a `progress` comment.\n- **Never** guess through a blocker — `handoff_to_human` with a precise ask and your default.\n- **Never** create a task without a description, acceptance criteria, and a priority.\n- **Never** end a session without the learning sweep + final `sync_context`.\n\n## Loading this skill\n\nGive your AI tool this prompt (works with MCP configured, or over plain REST with https://taskpod.in/api-docs):\n\n```\nRead https://taskpod.in/skill and follow it for all work. Taskpod is the\nteam's central mind. Non-negotiable: call sync_context at session start and\ningest the digest before any work; resume_task before touching any existing\ntask; checkpoint_task for EVERY status change; record_learning for every\nnon-obvious decision, convention, or gotcha you discover (one per entry,\nnever duplicating what's already recorded); and sync_context again before\nyou stop. API key: <your tp_live_... token>.\n```\n";
import { z } from "zod";
import { get, post } from "../api-client.js";
const ApprovalStatusEnum = z.enum(["PENDING", "APPROVED", "DENIED", "EXPIRED", "ALL"]);
export const approvalTools = [
{
name: "list_approvals",
description: "List approval requests (the decision inbox) on projects you can see. Each approval carries its tool name, payload, status, requesting agent/run, and the task it belongs to. Defaults to PENDING — the ones waiting on a decision. Pass status=ALL for full history, projectId to scope to one project. Decide one with decide_approval.",
inputSchema: z.object({
projectId: z.string().optional().describe("Scope to a single project ID"),
status: ApprovalStatusEnum.optional().describe("Filter by status: PENDING (default) | APPROVED | DENIED | EXPIRED | ALL"),
}),
handler: async (input) => {
const params = new URLSearchParams();
if (input.projectId)
params.set("projectId", input.projectId);
if (input.status)
params.set("status", input.status);
const qs = params.toString();
const approvals = await get(`/api/approvals${qs ? `?${qs}` : ""}`);
return {
content: [{ type: "text", text: JSON.stringify(approvals, null, 2) }],
};
},
},
{
name: "decide_approval",
description: "Approve or deny a pending approval request. Exactly one decision wins: if another surface (web, email link, Slack) already decided it, this returns a 409 'Already decided' error naming the winner. On success the orchestrator forwards the verdict into the suspended agent run. Find pending IDs with list_approvals.",
inputSchema: z.object({
approvalId: z.string().describe("Approval ID (from list_approvals)"),
decision: z.enum(["approve", "deny"]).describe("The verdict: approve or deny"),
note: z.string().optional().describe("Optional decision note shown with the verdict"),
}),
handler: async (input) => {
const result = await post(`/api/approvals/${input.approvalId}/decide`, {
verdict: input.decision === "approve" ? "APPROVED" : "DENIED",
...(input.note ? { note: input.note } : {}),
});
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
};
},
},
];
import { z } from "zod";
import { get, patch } from "../api-client.js";
export const okrTools = [
{
name: "list_objectives",
description: "List a team's OKRs: objectives with their key results and derived progress (0..1, the average of key-result progress). Each key result reports id, title, metricType (NUMBER | PERCENT | CURRENCY | BOOLEAN), startValue, targetValue, currentValue. Optional period filter (e.g. '2026-Q3'). Use this to find the objectiveId + keyResultId before a check-in with update_key_result.",
inputSchema: z.object({
teamId: z.string().describe("Team ID that owns the objectives"),
period: z.string().optional().describe("Filter to one period, e.g. '2026-Q3'"),
}),
handler: async (input) => {
const qs = input.period ? `?period=${encodeURIComponent(input.period)}` : "";
const objectives = await get(`/api/teams/${input.teamId}/objectives${qs}`);
return {
content: [{ type: "text", text: JSON.stringify(objectives, null, 2) }],
};
},
},
{
name: "update_key_result",
description: "Check in a key result: set its currentValue (the number the OKR board tracks). Progress is derived as (current - start) / (target - start), clamped to 0..1. Returns the updated key result. Find IDs with list_objectives.",
inputSchema: z.object({
objectiveId: z.string().describe("Objective ID the key result belongs to"),
keyResultId: z.string().describe("Key result ID (from list_objectives)"),
currentValue: z.number().describe("The new current value of the metric"),
}),
handler: async (input) => {
const kr = await patch(`/api/objectives/${input.objectiveId}/key-results/${input.keyResultId}`, { currentValue: input.currentValue });
return {
content: [{ type: "text", text: JSON.stringify(kr, null, 2) }],
};
},
},
];
+102
-30

@@ -6,3 +6,5 @@ /**

* Claude Code → `.mcp.json` (merged, never clobbered) + a marker-guarded
* TaskPod section in `CLAUDE.md`
* TaskPod section in `CLAUDE.md` + the taskpod skill at
* `.claude/skills/taskpod/SKILL.md` (fetched from
* `<apiBase>/skill.md`, bundled copy as offline fallback)
* Cursor → `.cursor/mcp.json` (same merge)

@@ -14,9 +16,10 @@ * Generic → prints the MCP command + skill URL for any other client

* embeds `env.TASKPOD_TOKEN` for CI/containers where no login exists.
* Idempotent: re-running updates the `taskpod` server entry and the CLAUDE.md
* marker block in place; it never duplicates either.
* Idempotent: re-running updates the `taskpod` server entry, the CLAUDE.md
* marker block, and the skill file in place; it never duplicates any of them.
*/
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { readRepoConfig, resolveToken } from "./config.js";
import { readRepoConfig, resolveApiBase, resolveToken } from "./config.js";
import { ask } from "./prompt.js";
import { BUNDLED_SKILL_BODY, SKILL_FRONTMATTER } from "./skill-content.js";
import { CliError, color, flagBool, info, print, success, warn, } from "./util.js";

@@ -64,2 +67,49 @@ // ─── MCP server entry ────────────────────────────────────────────────────────

}
// ─── Claude Code skill (.claude/skills/taskpod/SKILL.md) ────────────────────
const SKILL_REL_PATH = join(".claude", "skills", "taskpod", "SKILL.md");
/**
* Fetch the live skill body from `<apiBase>/skill.md` (5s timeout). Returns
* null on ANY failure — non-200, timeout, network error, empty body — so the
* caller falls back to the bundled snapshot.
*/
async function fetchSkillBody(apiBase) {
try {
const res = await fetch(`${apiBase}/skill.md`, {
signal: AbortSignal.timeout(5000),
headers: { Accept: "text/markdown, text/plain, */*" },
});
if (!res.ok)
return null;
const text = await res.text();
return text.trim().length > 0 ? text : null;
}
catch {
return null;
}
}
/** Strip a leading `--- … ---` frontmatter block, if the body ships one. */
function stripFrontmatter(body) {
const m = /^---\r?\n[\s\S]*?\r?\n---\r?\n/.exec(body);
return m ? body.slice(m[0].length) : body;
}
/**
* Write the taskpod skill for Claude Code: valid skill frontmatter + the
* protocol body (live from the server when reachable, bundled otherwise).
* Idempotent — the file is only rewritten when its content differs.
*/
async function installClaudeSkill(cwd) {
const apiBase = resolveApiBase(cwd);
const fetched = await fetchSkillBody(apiBase);
const source = fetched ? "server" : "bundled";
const body = stripFrontmatter(fetched ?? BUNDLED_SKILL_BODY).trim();
const content = `${SKILL_FRONTMATTER}\n\n${body}\n`;
const path = join(cwd, SKILL_REL_PATH);
if (existsSync(path) && readFileSync(path, "utf8") === content) {
return { result: "unchanged", source };
}
const existed = existsSync(path);
mkdirSync(join(cwd, ".claude", "skills", "taskpod"), { recursive: true });
writeFileSync(path, content, "utf8");
return { result: existed ? "updated" : "created", source };
}
// ─── CLAUDE.md marker block ──────────────────────────────────────────────────

@@ -74,3 +124,3 @@ const CLAUDE_START = "<!-- taskpod:start -->";

"",
`This repo's board lives in TaskPod${proj}. The \`taskpod\` MCP server is configured in \`.mcp.json\`.`,
`This repo's board lives in TaskPod${proj}. The \`taskpod\` MCP server is configured in \`.mcp.json\`, and the **taskpod skill** is installed at \`.claude/skills/taskpod/SKILL.md\` — follow it for all work in this repo.`,
"",

@@ -101,2 +151,12 @@ "- Read `.taskpod/context.md`, `.taskpod/mine.md`, and `.taskpod/LEARNINGS.md` before starting work.",

}
/** What's detectable in the repo; used by auto mode and the non-TTY default. */
function detectTools(cwd) {
const claude = existsSync(join(cwd, "CLAUDE.md")) ||
existsSync(join(cwd, ".mcp.json")) ||
existsSync(join(cwd, ".claude"));
const cursor = existsSync(join(cwd, ".cursor"));
// When nothing is detected, default to Claude Code — it's the config every
// MCP client can read from.
return { claude: claude || !cursor, cursor, generic: false };
}
async function selectTools(args, cwd) {

@@ -119,9 +179,4 @@ const all = flagBool(args.flags, "all");

// default to Claude Code — it's the config every MCP client can read from.
if (process.stdin.isTTY !== true) {
return {
claude: detected.claude || !detected.cursor,
cursor: detected.cursor,
generic: false,
};
}
if (process.stdin.isTTY !== true)
return detectTools(cwd);
const yn = async (q, def) => {

@@ -139,19 +194,5 @@ const a = (await ask(`${q} ${def ? "[Y/n]" : "[y/N]"} `)).toLowerCase();

}
// ─── command ─────────────────────────────────────────────────────────────────
export async function agentSetup(args) {
const cwd = process.cwd();
const cfg = readRepoConfig(cwd);
if (!cfg) {
warn("This repo isn't linked to a Taskpod project yet — run `taskpod init` (or `taskpod link <KEY>`) first. Writing tool config anyway.");
}
const token = resolveToken();
const withKey = args.flags["with-key"];
const embedToken = withKey === undefined ? null : typeof withKey === "string" ? withKey : token;
if (withKey !== undefined && !embedToken) {
throw new CliError("`--with-key` needs a token: pass one (`--with-key tp_live_xxx`) or run `taskpod login` first.");
}
if (!token && !embedToken) {
warn("Not logged in — the MCP server will have no auth. Run `taskpod login`, or re-run with `--with-key <token>`.");
}
const sel = await selectTools(args, cwd);
// ─── apply ───────────────────────────────────────────────────────────────────
/** Write the configs for a selection. Shared by interactive and auto modes. */
async function applySelection(sel, embedToken, cfg, cwd) {
const entry = mcpServerEntry(embedToken);

@@ -162,3 +203,5 @@ print("");

const mdRes = upsertClaudeMd(cwd, claudeBlock(cfg?.projectKey));
success(`Claude Code — .mcp.json ${mcpRes}, CLAUDE.md TaskPod section ${mdRes}.`);
const skill = await installClaudeSkill(cwd);
success(`Claude Code — .mcp.json ${mcpRes}, CLAUDE.md TaskPod section ${mdRes}, ` +
`skill ${SKILL_REL_PATH} ${skill.result} (${skill.source === "server" ? "fetched from server" : "bundled copy"}).`);
}

@@ -177,2 +220,21 @@ if (sel.cursor) {

print("");
}
// ─── command ─────────────────────────────────────────────────────────────────
export async function agentSetup(args) {
const cwd = process.cwd();
const cfg = readRepoConfig(cwd);
if (!cfg) {
warn("This repo isn't linked to a Taskpod project yet — run `taskpod init` (or `taskpod link <KEY>`) first. Writing tool config anyway.");
}
const token = resolveToken();
const withKey = args.flags["with-key"];
const embedToken = withKey === undefined ? null : typeof withKey === "string" ? withKey : token;
if (withKey !== undefined && !embedToken) {
throw new CliError("`--with-key` needs a token: pass one (`--with-key tp_live_xxx`) or run `taskpod login` first.");
}
if (!token && !embedToken) {
warn("Not logged in — the MCP server will have no auth. Run `taskpod login`, or re-run with `--with-key <token>`.");
}
const sel = await selectTools(args, cwd);
await applySelection(sel, embedToken, cfg, cwd);
if (embedToken) {

@@ -186,1 +248,11 @@ warn("Your token is embedded in the MCP config — do not commit it to a shared repo.");

}
/**
* Auto-detect agent setup — the code path `taskpod init` offers at the end of
* a successful run. No prompts, no secrets: configures the tools detected in
* the repo (defaulting to Claude Code) with the login-token auth model.
*/
export async function agentSetupAuto(cwd = process.cwd()) {
const cfg = readRepoConfig(cwd);
await applySelection(detectTools(cwd), null, cfg, cwd);
info(color.dim("Re-run `taskpod agent-setup` any time — it updates in place, never duplicates."));
}

@@ -11,2 +11,3 @@ /**

import { boardAge, fetchContext, loadBoard, memberLabel, myTasks, resolveMember, resolveTicket, } from "./context.js";
import { agentSetupAuto } from "./agent-setup.js";
import { materializeBundle, writeSkillFiles } from "./materialize.js";

@@ -188,2 +189,17 @@ import { learningHash, learningsMdPath, parseLearningsMd, readSyncedHashes, resetOutbox, writeSyncedHashes, } from "./learnings.js";

}
/**
* End-of-init agent-setup offer (R1): on a TTY without `--no-agents`, offer to
* run the same auto-detect code path as `taskpod agent-setup` (no prompts, no
* secrets). Non-TTY or `--no-agents` keeps the printed hint as the only nudge.
*/
async function maybeOfferAgentSetup(args) {
if (flagBool(args.flags, "no-agents") || process.stdin.isTTY !== true)
return;
const a = (await ask("Connect your AI tools (Claude Code / Cursor) now? (Y/n) ")).toLowerCase();
if (a === "" || a === "y" || a === "yes") {
await agentSetupAuto();
return;
}
info(color.dim("Skipped — run `taskpod agent-setup` any time."));
}
/** `git remote get-url origin` for the cwd, or null when not a repo / no remote. */

@@ -234,2 +250,3 @@ function gitRemoteOrigin() {

print("");
await maybeOfferAgentSetup(args);
}

@@ -283,2 +300,3 @@ export async function init(args) {

print("");
await maybeOfferAgentSetup(args);
}

@@ -285,0 +303,0 @@ export async function link(args) {

@@ -24,4 +24,6 @@ /**

import { workspaceTools } from "./tools/workspace.js";
import { approvalTools } from "./tools/approvals.js";
import { okrTools } from "./tools/okrs.js";
export const SERVER_NAME = "taskpod";
export const SERVER_VERSION = "0.3.0";
export const SERVER_VERSION = "0.3.1";
export const allTools = [

@@ -43,2 +45,4 @@ ...teamTools,

...workspaceTools,
...approvalTools,
...okrTools,
];

@@ -45,0 +49,0 @@ export function createTaskpodServer() {

@@ -40,2 +40,3 @@ #!/usr/bin/env node

init [--team T --name N] create/link the project from this repo's git remote + pull
[--no-agents] skip the end-of-init "connect your AI tools" prompt
link [KEY] connect this repo to a project (writes taskpod.json)

@@ -42,0 +43,0 @@ agent-setup connect Claude Code / Cursor / any MCP client to this project

{
"name": "taskpod",
"version": "0.3.0",
"version": "0.3.1",
"description": "The Taskpod developer CLI and MCP server — sync project context to your machine and AI, and manage teams, projects, and tasks from any MCP client.",

@@ -5,0 +5,0 @@ "type": "module",

@@ -41,5 +41,7 @@ # taskpod

At the end of a successful `init` on a terminal, it offers to finish the job — `Connect your AI tools (Claude Code / Cursor) now? (Y/n)` — and, on yes (the default), runs the same auto-detect, no-secrets code path as `taskpod agent-setup`. Pass `--no-agents` to skip the prompt; non-interactive runs (CI, agents) never prompt and just print the `agent-setup` hint.
**`taskpod agent-setup` — one command connects every AI tool.** Writes real MCP config for the tools it detects in your repo (flags for CI: `--claude`, `--cursor`, `--codex`, `--all`):
- **Claude Code** — merges a `taskpod` server entry into `.mcp.json` (never clobbers your other servers) and adds a marker-guarded **TaskPod** section to `CLAUDE.md` pointing at the context bundle and [taskpod.in/skill](https://taskpod.in/skill).
- **Claude Code** — merges a `taskpod` server entry into `.mcp.json` (never clobbers your other servers), adds a marker-guarded **TaskPod** section to `CLAUDE.md`, and installs a real Claude Code **skill** at `.claude/skills/taskpod/SKILL.md` (valid `name`/`description` frontmatter + the full agent protocol). The skill body is fetched live from `<apiBase>/skill.md` (5-second timeout, `TASKPOD_API_URL`-aware for self-hosted instances); on any failure it falls back to the copy bundled in this package, so the install works offline. Idempotent: the file is rewritten only when its content differs.
- **Cursor** — same server entry in `.cursor/mcp.json`.

@@ -68,2 +70,3 @@ - **Anything else (Codex, Windsurf, …)** — prints the command (`npx -y taskpod mcp`) + skill URL.

init [--team T --name N] create/link the project from this repo's git remote + pull
[--no-agents] skip the end-of-init "connect your AI tools" prompt
link [KEY] connect this repo to a project (writes taskpod.json)

@@ -163,2 +166,33 @@ agent-setup connect Claude Code / Cursor / any MCP client to this project

### Approvals & OKRs
Agent runs pause on approval gates; humans set the quarter's objectives — these tools let either side read and move both from any MCP client:
- **Approvals (the decision inbox)** — `list_approvals({ projectId?, status? })` lists approval requests on projects you can see (`status ∈ PENDING|APPROVED|DENIED|EXPIRED|ALL`, default `PENDING`); `decide_approval({ approvalId, decision: "approve"|"deny", note? })` records the verdict. Exactly one decision wins across every surface (web, email link, Slack, MCP) — a second decision returns a 409 naming who decided first. On success the orchestrator forwards the verdict into the suspended run.
- **OKRs** — `list_objectives({ teamId, period? })` returns the team's objectives with key results and derived progress (0..1); `update_key_result({ objectiveId, keyResultId, currentValue })` is the check-in that moves the number. Find IDs with `list_objectives` first.
## Manual ↔ agent parity
The same board, two hands: everything a developer does at the terminal, an agent can do over MCP (and vice versa).
| Capability | CLI (human at the terminal) | MCP tool (agent) |
|------------|----------------------------|------------------|
| Link a repo to its project | `taskpod init` / `taskpod link <KEY>` | `init_workspace` |
| Pull project context | `taskpod sync` / `taskpod pull` | `sync_context` |
| Share a learning | `.taskpod/learnings.md` + `taskpod sync` | `record_learning` |
| Board overview | `taskpod status` | `get_sprint_status` |
| My tasks | `taskpod mine` | `list_tasks` (filtered) |
| Task detail | `taskpod show <ticket>` | `get_task` / `resume_task` |
| Start work | `taskpod start <ticket>` | `checkpoint_task` (→ IN_PROGRESS) |
| Finish work | `taskpod done <ticket>` | `checkpoint_task` (→ IN_REVIEW) |
| Typed comment | `taskpod comment <ticket> -t …` | `add_comment` |
| Set an ETA | `taskpod eta <ticket> <when>` | `update_task` (etaAt) |
| Reassign / hand off | `taskpod assign` / `taskpod handoff` | `handoff_to_human` / `update_task` |
| Project resources | `taskpod resources` / `taskpod setup` | `list_project_resources` / `add_project_resource` |
| Approvals inbox | Taskpod web / Slack buttons | `list_approvals` |
| Decide an approval | Taskpod web / email link / Slack | `decide_approval` |
| Read the OKRs | Taskpod web (OKR board) | `list_objectives` |
| Key-result check-in | Taskpod web (OKR board) | `update_key_result` |
| Connect AI tools | `taskpod agent-setup` (offered by `init`) | — (it's what wires the agent in) |
## Development

@@ -165,0 +199,0 @@