| /** | ||
| * 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.")); | ||
| } |
+18
-0
@@ -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) { |
+5
-1
@@ -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() { |
+1
-0
@@ -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 |
+1
-1
| { | ||
| "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", |
+35
-1
@@ -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 @@ |
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Minified code
QualityThis package contains minified code. This may be harmless in some cases where minified code is included in packaged libraries, however packages on npm should not minify code.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
235807
15.22%39
8.33%4572
5.81%217
18.58%3
50%40
2.56%9
12.5%