Repository navigation
feat: add task-sync with CUFM and directory trees - #144
colinmollenhour wants to merge 539 commits into
Conversation
…namic allowlist, bulk tag --add
…ipe escaping, dedup PRIORITY_MAP, createGoal NaN, loadConfig double-parse
…te ranges, custom fields
…kills as internal
…, add npx fallback
…n no agents detected
Show how long a task has been in each status via the ClickUp time_in_status API endpoint. Supports TTY table, markdown, and JSON output modes. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Catch the "No data for TIS" API error and display actionable guidance instead of a raw 401 error. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The shipped skill is now a directory (SKILL.md plus references/cufm.md). Delegate install to the skills CLI against the bundled copy so Universal .agents/skills and the extra files are handled by the tool that already knows 70+ agents, instead of a three-path copier.
|
@krodak Hey Krzysztof, I know this is a huge PR, but I promise it's worth it. :) All code was written by AI with multiple models and been through multiple rounds of review, refinement, testing, etc. This feature makes possible what was previously impossible in a single command so that syncing ClickUp tasks back and forth is dead simple for humans and AIs and you don't waste the precious context window space when in the middle of planning just to put your plans online. It allows the AI to write rich descriptions without losing them over and over and allows humans to make edits without those being stripped out next time you sync. I think this is really going to take my task planning to the next level. Note, this uses an undocumented API for fetching the task descriptions from ClickUp because their documented APIs don't return the Quill format which is necessary to preserve all of the data types. I've asked them months ago about making the most minor changes with no luck so don't think that "wait for it to be official" is worthwhile, we'll have AGI before then.. 🤣 If it breaks in the future, we'll fix it, that is what the "doctor" command is there for. Oh yeah, I meant to say, thank you so much for creating and maintaining this awesome CLI, I use and love it daily! ClickUp should buy it and/or hire you. You're the man! |
`::mermaid`/`::tldraw` bodies were rebuilt from markdown-it tokens by
`tokensToSource`, which kept only inline/fence/text tokens. A blank line
inside a pretty-printed .tldr body ends the paragraph and turns the next
indented line into a `code_block` token, which was dropped — so the
compiler handed the renderer mangled JSON, the cache key missed, and
`task-sync push` reported "tldraw render failed" even though the render
and upload had succeeded.
Bodies now come from the original source lines via the container token's
`map`, byte-for-byte, with the container's own indentation or `>` prefix
stripped for nested blocks.
Also:
- Add exported `collectDiagramSources()` and use it from publish.ts in
place of its own regexes, so the render and compile sides cannot drift
apart again.
- Surface render failures as "<lang> render failed: <reason>" in compile
warnings instead of swallowing the reason.
- Key the mermaid cache by theme + source; a fenced block's `{theme=...}`
was previously ignored and same-source blocks with different themes
collided.
Lossless `task-sync pull`, Synced Content updates and doctor's stored-colour audit all need ClickUp's web session JWT; a personal pk_ API token is rejected by those endpoints (JWT_008 / JWT_062). Nothing documented how to obtain the JWT, and a pull without one silently replaced authored CUFM with ClickUp's flattened markdown export. - Add `cup auth session [token]` to store the JWT on the profile (0600, beside apiToken), with --status and --clear. Accepts a positional token, piped stdin, or a masked prompt; rejects pk_ tokens and already-expired JWTs with a pointer to where the real one lives. - Add src/session-token.ts as the single resolver: --session-token, then CU_SESSION_TOKEN, then the stored value. It decodes `exp`, so an expired token now reports "expired 20m ago" instead of an opaque 401. - `task-sync pull` refuses to overwrite a file containing CUFM with flattened markdown unless --lossy is passed, and every pull line is now labelled (lossless) or (lossy) instead of only marking the lossless case. - Keep sessionToken through the profile normaliser, which previously dropped unknown fields on any config write. - Document how to get the token, its lifetime, and the --lossy escape hatch.
A pull rebuilt CUFM line by line, so anything spanning several Quill lines
came back mangled. Round-tripping a real task description through
compile/decompile corrupted 93 lines with no network involved.
Group lines into the block they belong to before rendering:
- join consecutive code-block lines into one fence, instead of emitting a
complete fence per line (this alone made diagram sources unrecoverable)
- keep task/user/doc mentions and images inline rather than breaking the
sentence onto new lines
- wrap every line of a banner, pull quote or column layout in one component;
`::columns` was previously dropped without warning
- close toggles around their indented children; the `::` that appeared to
close them was the following table's
- collapse ClickUp's rendered-image + "<lang> source" toggle pair back to the
plain fence that produced it, since push re-renders the image
- keep inline formatting in table cells and fold multi-line cells to `<br>`
- hoist each emphasis over the longest run of spans sharing it, so adjacent
spans emit one delimiter pair instead of colliding into `**`x`**** y**`
- number ordered lists and indent children past the parent marker
- separate blocks with a blank line, as a soft break would otherwise collapse
to a space on the next push
Two of these drifted without bound: whitespace before `{align="..."}` grew by
one space per cycle and ordered numbering climbed on every pull.
Also fix a leaf inline component (`:doc{...}`) swallowing the rest of its line
and losing its props: it has no closing token, so searching for one ran to the
end of the line.
The doctor document could not have caught any of this — every code block in it
was one line long, and its banners and quotes were single paragraphs. Give it
multi-line bodies, multi-paragraph banners and quotes, mentions mid-sentence,
adjacent emphasis, deeper ordered lists and formatted table cells, and audit
the decompiled result both locally and as ClickUp stored it. Verified against
the pre-fix decompiler: 11 of the 12 new assertions fail on it.
Document the traps an author cannot see from the rendered output: soft breaks
collapse to spaces, ordered items nest by three spaces, and tables, dividers,
`::button` and `::frame` fall out of a toggle because the block embeds have no
line to carry the indent.
`tldraw export --print` streams base64 to stdout and exits without waiting for the pipe to drain. Captured through execFile that yields a prefix of the image: a real diagram produced 146,176 of 2,163,597 bytes, decoding to a 109,632-byte PNG with no IEND chunk. Shell redirection to a file gets the whole stream, which is why this never showed up manually. pngDimensions() validated only the signature and IHDR, so the truncated image passed, was uploaded, and rendered as a corrupt attachment on the task. Re-pushing reproduced the identical bytes, so nothing downstream could recover. Export now writes to the temp directory that already holds the .tldr source (--output <dir> --name diagram) and reads the file back, and pngDimensions() additionally requires a well-formed IEND trailer so a short read fails loudly as a compile warning instead of silently uploading a broken image. exportTldrawPng is exported and takes an injectable runner, matching the exporter seam renderTldrawPng already had, so the argv contract can be asserted without the external CLI. All six new tests fail against the previous implementation. Note: tests/unit/index.test.ts and tests/unit/commands/profile.test.ts have six pre-existing failures under the full suite (they pass in isolation); verified identical on a clean tree before and after.
|
Hi Colin — first, thank you for this. It's one of the most substantial and carefully built contributions this project has received: the CUFM round-trip fixture testing, atomic ID recording on push, the injection-safe tldraw/git invocations, and the visual verification against a live workspace all show real care. I've spent a good amount of time reviewing it, and I want to be upfront about where I've landed and why, because the reason isn't code quality. The decision: I can't merge the parts that depend on ClickUp's private API surface, and I'd like to invite a revised PR that keeps everything else. The reasoning. This CLI is a published npm package, which changes the risk calculus for unofficial endpoints in two ways:
What I'd love to merge. The undocumented surface looks severable to me, and you've already built the graceful-degradation path (pull falls back to markdown export without a token). A revision that:
…would be most of the value with none of the liability, and I'd be glad to take it. Two bugs worth fixing in any revision (verified locally):
Upstream. I've labeled this Thanks again for the effort you put into this. I hope the split doesn't feel like a rejection of the work — it's a scoping call about what a published CLI can responsibly carry. |
|
@krodak Thanks for your thoughtful review. I understand your reasons for not accepting, although I'd push back a little.. To me this is such a major lack of functionality and after having used it, I just can't possibly go back. Without the Quill pull/push the entire thing is kinda pointless for my workflow. What if the lossy fallback was by default for I do agree the frontdoor host needs to be fixed - it probably could be with more hackery; but I have no solution for securing the session token pattern. I suppose this means I will just have to use my fork indefinitely.. I'll continue to improve but am not really interested in submitting a second PR and maintaining my private fork plus the feature for upstream that is so nerfed it's hardly worth using. You're welcome to modify and merge it as you see fit. Thanks for filing the feedback issue, I have voted on it but didn't bother to write a request there in the first place because based on other feedback I've seen it appears to be nearly pointless. Even issues with hundreds of votes that are years old and don't even require UI don't get completed.. it should be possible to do this in 2 days but I have little faith it will get done any time soon. /rant |
A mention sitting on its own line tokenizes as mdc_block_shorthand, which compile skipped, so a pull/push cycle deleted it. The dirty-file guard lived in a catch that treated every throw as "new file", including confirmClobber refusals.
The web app looks up the regional frontdoor via an unauthenticated workspace handshake rather than a hardcoded shard hostname. Create and update still send native editor ops first, then retry with markdown_content if the public API rejects that shape.
Summary
Adds
cup task-sync, a file-based workflow for keeping ClickUp task descriptions in source control as ClickUp Flavored Markdown (CUFM). CUFM compiles directly to ClickUp's native Quill editor ops, avoiding the extra paragraphs and broken nested blocks produced by ClickUp's generic Markdown importer.The same compiler is used by
cup createandcup update --description-file.This PR also changes
cup skillto install the complete bundled skill directory throughnpx skills add, ensuring the CUFM reference is available to every supported agent.Highlights
task-sync doctorfor repeatable Quill and visual regression checks.Task sync
A directory represents one task graph:
parent:is canonical.subtasks:records display order and provides a consistency check.depends_on:andblocks:represent waiting-on edges.parent: nullexplicitly unparents; omitting the field preserves the existing parent.ClickUp Flavored Markdown
CUFM supports standard headings, inline formatting, links, nested lists, task lists, fenced code, dividers, GFM tables, and images, plus native ClickUp components:
::toc::toggle{title="…"}::banner{color="blue"}::quote{size="large"}::columns/:::column::table{widths="…"}mermaid/::mermaidtldraw/::tldraw::sync-block{id="…"}::button/::frame:badge,:user,:task,:docSupported base color tokens are
grey,red,orange,yellow,green,blue,purple, andpink. Banners additionally support their-strongvariants.Unknown
::componentsare preserved as fencedcufmsource instead of being silently discarded.Full syntax: docs/cufm.md.
Synced Content
Lossless pull retrieves both the task Delta and its backing
sync_blockscollection. The first occurrence carries the shared content; later empty occurrences clone the same ClickUp block ID:Updating the component body updates the shared backing Delta and therefore every clone. That operation requires
CU_SESSION_TOKENortask-sync push --session-token <jwt>. The initial backing block is still created in ClickUp; cup then pulls and reuses its ID.cup skillcup skillnow delegates installation tonpx skills addusing the skill directory bundled with the installed CLI version. This copiesSKILL.mdandreferences/cufm.md, supports Universal.agents/skills, and avoids maintaining an incomplete agent/path matrix in cup.--printand--pathremain local operations.--yesand cannot hang on prompts.main.Visual verification
The doctor document was exercised against task
86bbhk0rkand inspected through the public ClickUp UI:https://sharing.clickup.com/2304761/t/h/86bbhk0rk/KAQ14DPGC3O0IIA
Confirmed in the stored Quill and rendered DOM:
Public viewers see ClickUp's permission placeholder for Synced Content, but the public task payload exposes and confirms the shared references.
Validation
npm run typechecknpm run lintnpm run format:checknpm test— 111 files, 1,434 testsnpm run buildThe full gate was run against clean commit
2ed3211.