@diagent/cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,210 @@
1
+ ## @diagent/cli
2
+
3
+ Command-line tool to encode and decode Diagent shareable flowchart URLs.
4
+ Designed for agents (Claude Code, Cursor) and humans alike.
5
+
6
+ By default, `diagent encode` produces a **short URL** like
7
+ `https://diagent.dev/d/abcdefghij` (~31 chars, constant size regardless
8
+ of diagram complexity). This works by calling the Diagent backend's
9
+ `POST /api/s` endpoint, which stores the Mermaid in Cloudflare KV and
10
+ returns a content-addressed short ID.
11
+
12
+ If the backend is unreachable (dev, offline, rate-limit, Cloudflare
13
+ outage), `diagent encode` **automatically falls back** to an inline URL
14
+ of the form `https://diagent.dev/?code=<lz-compressed>`, which carries
15
+ the full Mermaid source in the `code` query parameter. The feature
16
+ never fully breaks — worst case, you get a longer URL and a stderr
17
+ notice. Pass `--inline` to skip the backend entirely and always produce
18
+ the inline format.
19
+
20
+ ### Install
21
+
22
+ **From npm:**
23
+
24
+ ```bash
25
+ npx -y @diagent/cli --help
26
+ # or install globally
27
+ npm install -g @diagent/cli
28
+ ```
29
+
30
+ **From source (contributors):**
31
+
32
+ ```bash
33
+ cd cli
34
+ npm install
35
+ npm run build
36
+ npm link # puts `diagent` on your global PATH
37
+ ```
38
+
39
+ ### Usage
40
+
41
+ ```
42
+ diagent encode [FILE] [--base-url URL] [--inline]
43
+ Encode Mermaid from FILE or stdin -> URL on stdout
44
+ (tries backend short URL, falls back to inline)
45
+ diagent decode <URL> Decode inline ?code= URL -> Mermaid source on stdout
46
+ diagent decode - Decode URL read from stdin
47
+ diagent --help Show help
48
+ diagent --version Show version
49
+ ```
50
+
51
+ **Encode from stdin (produces a short URL):**
52
+
53
+ ```bash
54
+ cat flow.mmd | diagent encode
55
+ # https://diagent.dev/d/kwtsgx5o24
56
+ ```
57
+
58
+ **Encode from a file:**
59
+
60
+ ```bash
61
+ diagent encode flow.mmd
62
+ ```
63
+
64
+ **Force inline format (skip backend):**
65
+
66
+ ```bash
67
+ diagent encode flow.mmd --inline
68
+ # https://diagent.dev/?code=GYGw9g7gxg...
69
+ ```
70
+
71
+ **Encode against a local dev server:**
72
+
73
+ ```bash
74
+ diagent encode flow.mmd --base-url http://localhost:5173/
75
+ ```
76
+
77
+ **Decode a short URL or inline URL (both work):**
78
+
79
+ ```bash
80
+ # Short URL — diagent follows the 302 internally
81
+ diagent decode "https://diagent.dev/d/kwtsgx5o24"
82
+ # flowchart TD
83
+ # A["Hello"]
84
+ # B["World"]
85
+ # A --> B
86
+
87
+ # Inline URL
88
+ diagent decode "https://diagent.dev/?code=GYGw9g7gxgFghgJwC4AIAqARAUC3KCCA..."
89
+
90
+ # From stdin — avoids shell-quoting pain for long URLs
91
+ echo "$URL" | diagent decode -
92
+ ```
93
+
94
+ `diagent decode` transparently handles both URL formats. For `/d/:id`
95
+ short URLs it sends a HEAD request (3s timeout), reads the `Location`
96
+ header, and extracts `?code=` from the redirect target. The CLI user
97
+ never has to know which format they have.
98
+
99
+ ### Agent workflow
100
+
101
+ Once `diagent` is on your agent's `$PATH`, no per-agent configuration is
102
+ required. The agent invokes it via its shell/Bash tool:
103
+
104
+ ```bash
105
+ # Agent shares a diagram it authored — short URL by default
106
+ echo 'flowchart TD
107
+ A[Start] --> B[End]' | diagent encode
108
+ # -> https://diagent.dev/d/abcdefghij
109
+
110
+ # Agent reads a URL the user pasted — works for both short and inline
111
+ diagent decode "https://diagent.dev/d/abcdefghij"
112
+ diagent decode "https://diagent.dev/?code=..."
113
+ ```
114
+
115
+ Short URLs are legible in chat, dramatically shorter than the inline
116
+ format, and unbounded in diagram complexity. The agent never sees or
117
+ handles the `lz-string` format — the backend takes care of it.
118
+
119
+ No MCP server, no SDK, no per-user `.mcp.json` — just a CLI on PATH.
120
+ For stateless operations like these, a CLI is strictly simpler than an
121
+ MCP server and works in any environment with shell access.
122
+
123
+ ### Claude Code Skill (auto-discovery)
124
+
125
+ Without priming, Claude Code doesn't know about `diagent` — it's a new
126
+ tool that isn't in training data. To teach every Claude Code session
127
+ across every project about this CLI, install the bundled Skill:
128
+
129
+ ```bash
130
+ mkdir -p ~/.claude/skills/diagent
131
+ curl -o ~/.claude/skills/diagent/SKILL.md \
132
+ https://raw.githubusercontent.com/daniellee-ux/Diagent/main/.claude/skills/diagent/SKILL.md
133
+ ```
134
+
135
+ The skill uses `npx -y @diagent/cli` internally, so the CLI auto-downloads
136
+ on first use — no `npm link` or build step required.
137
+
138
+ Or, if you have the repo cloned and want auto-updating via symlink:
139
+
140
+ ```bash
141
+ mkdir -p ~/.claude/skills
142
+ ln -sfn /path/to/Diagent/.claude/skills/diagent ~/.claude/skills/diagent
143
+ ```
144
+
145
+ Once installed, a fresh Claude Code session in any project will
146
+ recognize phrases like "draw me a flowchart" or "diagram the login
147
+ flow" and invoke the CLI without needing to be told it exists. Test it:
148
+
149
+ ```text
150
+ You: Can you draw me a flowchart of how this login handler works?
151
+
152
+ Claude: [reads the handler, runs `npx -y @diagent/cli encode` on a generated Mermaid]
153
+ Here's the diagram: https://diagent.dev/d/...
154
+ ```
155
+
156
+ If Claude still says "I don't have a diagent CLI available," verify
157
+ the skill file: `ls -la ~/.claude/skills/diagent/SKILL.md`. A full Claude
158
+ Code restart may be needed to pick up newly-installed skills.
159
+
160
+ ### Environment
161
+
162
+ | Variable | Default | Description |
163
+ |---|---|---|
164
+ | `DIAGENT_BASE_URL` | `https://diagent.dev/` | Base URL for both `POST /api/s` and inline URL construction. Override with `--base-url` flag for per-invocation. |
165
+
166
+ ### Exit codes
167
+
168
+ | Code | Meaning |
169
+ |---|---|
170
+ | 0 | Success (includes fallback-to-inline path) |
171
+ | 1 | Runtime error (empty input, invalid URL, corrupt code param, too large, file not found) |
172
+ | 2 | Usage error (unknown subcommand, missing required argument) |
173
+
174
+ When encode falls back from short URL to inline URL due to backend
175
+ unavailability, it still returns exit 0 — the operation succeeded,
176
+ just in a degraded form. The stderr notice `backend unreachable,
177
+ using inline URL` is the signal.
178
+
179
+ ### Format parity with the web app
180
+
181
+ The CLI and the browser's **Copy Link** button produce byte-identical
182
+ output for the same Mermaid source and base URL — both short URLs
183
+ (from the Worker) and inline fallback URLs (from `lz-string`). The CLI
184
+ uses the same compression, the same `?code=` query-param convention,
185
+ and the same `/d/:id` shape as the browser.
186
+
187
+ This means you can:
188
+ - Generate a short URL via CLI, open it in the browser — the diagram loads.
189
+ - Click **Copy Link** in the browser, `diagent decode` the inline form
190
+ (after following the redirect) — the source comes back exactly as the
191
+ browser serialized it.
192
+
193
+ ### Local dev workflow
194
+
195
+ To test the CLI against a local Worker instead of production, run one
196
+ dev server that hosts both the SPA and the Worker:
197
+
198
+ ```bash
199
+ # Terminal 1 — Vite dev server runs the Worker via @cloudflare/vite-plugin
200
+ npm run dev # http://localhost:5173
201
+
202
+ # Terminal 2 (or wherever you invoke diagent)
203
+ echo 'flowchart TD\n A --> B' | diagent encode --base-url http://localhost:5173/
204
+ # -> http://localhost:5173/d/abcdefghij
205
+ ```
206
+
207
+ The `@cloudflare/vite-plugin` makes Vite's dev server execute the Worker
208
+ script natively, so `/api/*` and `/d/*` requests are handled on port 5173
209
+ without a separate `wrangler dev` process or proxy. Production
210
+ `wrangler deploy` is unaffected.
package/build/cli.js ADDED
@@ -0,0 +1,129 @@
1
+ #!/usr/bin/env node
2
+ import { parseArgs } from 'node:util';
3
+ import { readFile } from 'node:fs/promises';
4
+ import { exec } from 'node:child_process';
5
+ import { resolveMermaidFromUrl, shortenOrInline, DEFAULT_BASE_URL, } from './core.js';
6
+ const HELP = `diagent — encode and decode Diagent shareable flowchart URLs
7
+
8
+ Usage:
9
+ diagent encode [FILE] [--base-url URL] [--inline] [--open]
10
+ Encode Mermaid from FILE or stdin → URL on stdout
11
+ (tries backend short URL, falls back to inline on failure)
12
+ diagent decode <URL> Decode short /d/:id OR inline ?code= URL → Mermaid on stdout
13
+ diagent decode - Decode URL read from stdin
14
+ diagent --help Show this help
15
+ diagent --version Show version
16
+
17
+ Options:
18
+ --inline Skip the backend and always produce an inline ?code= URL
19
+ --open Open the generated URL in the default browser
20
+ --base-url URL Diagent origin for both backend calls and URL construction
21
+ (default: ${DEFAULT_BASE_URL})
22
+
23
+ Examples:
24
+ cat flow.mmd | diagent encode
25
+ diagent encode flow.mmd
26
+ diagent encode flow.mmd --base-url http://localhost:5173/
27
+ diagent encode flow.mmd --inline
28
+ diagent decode "https://diagent.dev/d/abcdefghij"
29
+ diagent decode "https://diagent.dev/?code=GYGw9g7gxg..."
30
+
31
+ Environment:
32
+ DIAGENT_BASE_URL Default base URL for encode (default: ${DEFAULT_BASE_URL})
33
+ `;
34
+ async function readStdin() {
35
+ let data = '';
36
+ process.stdin.setEncoding('utf8');
37
+ for await (const chunk of process.stdin)
38
+ data += chunk;
39
+ return data;
40
+ }
41
+ async function runEncode(args) {
42
+ const { values, positionals } = parseArgs({
43
+ args,
44
+ options: {
45
+ 'base-url': { type: 'string' },
46
+ inline: { type: 'boolean' },
47
+ open: { type: 'boolean' },
48
+ help: { type: 'boolean', short: 'h' },
49
+ },
50
+ allowPositionals: true,
51
+ });
52
+ if (values.help) {
53
+ process.stdout.write(HELP);
54
+ return 0;
55
+ }
56
+ const file = positionals[0];
57
+ const mermaid = file ? await readFile(file, 'utf8') : await readStdin();
58
+ const baseUrl = values['base-url'] ?? DEFAULT_BASE_URL;
59
+ const preferInline = values.inline === true;
60
+ const { result, usedFallback } = await shortenOrInline(mermaid, baseUrl, preferInline);
61
+ if (!result.ok) {
62
+ process.stderr.write(`diagent: ${result.error}\n`);
63
+ return 1;
64
+ }
65
+ if (usedFallback && !preferInline) {
66
+ process.stderr.write('diagent: backend unreachable, using inline URL\n');
67
+ }
68
+ process.stdout.write(result.url + '\n');
69
+ if (values.open) {
70
+ const cmd = process.platform === 'darwin' ? 'open' :
71
+ process.platform === 'win32' ? 'start' : 'xdg-open';
72
+ exec(`${cmd} ${JSON.stringify(result.url)}`);
73
+ }
74
+ return 0;
75
+ }
76
+ async function runDecode(args) {
77
+ const { values, positionals } = parseArgs({
78
+ args,
79
+ options: { help: { type: 'boolean', short: 'h' } },
80
+ allowPositionals: true,
81
+ });
82
+ if (values.help) {
83
+ process.stdout.write(HELP);
84
+ return 0;
85
+ }
86
+ let url = positionals[0];
87
+ if (!url) {
88
+ process.stderr.write('diagent decode: missing URL argument (pass URL or "-" for stdin)\n');
89
+ return 2;
90
+ }
91
+ if (url === '-')
92
+ url = (await readStdin()).trim();
93
+ const result = await resolveMermaidFromUrl(url);
94
+ if (!result.ok) {
95
+ process.stderr.write(`diagent: could not decode URL — ${result.error}\n`);
96
+ return 1;
97
+ }
98
+ const mermaid = result.mermaid;
99
+ process.stdout.write(mermaid + (mermaid.endsWith('\n') ? '' : '\n'));
100
+ return 0;
101
+ }
102
+ async function main() {
103
+ const argv = process.argv.slice(2);
104
+ if (argv.length === 0 || argv[0] === '--help' || argv[0] === '-h') {
105
+ process.stdout.write(HELP);
106
+ return 0;
107
+ }
108
+ if (argv[0] === '--version' || argv[0] === '-v') {
109
+ const pkg = await readFile(new URL('../package.json', import.meta.url), 'utf8');
110
+ process.stdout.write(JSON.parse(pkg).version + '\n');
111
+ return 0;
112
+ }
113
+ const [sub, ...rest] = argv;
114
+ switch (sub) {
115
+ case 'encode':
116
+ return runEncode(rest);
117
+ case 'decode':
118
+ return runDecode(rest);
119
+ default:
120
+ process.stderr.write(`diagent: unknown subcommand "${sub}"\n${HELP}`);
121
+ return 2;
122
+ }
123
+ }
124
+ main()
125
+ .then((code) => process.exit(code))
126
+ .catch((err) => {
127
+ process.stderr.write(`diagent: ${err instanceof Error ? err.message : err}\n`);
128
+ process.exit(1);
129
+ });
package/build/core.js ADDED
@@ -0,0 +1,176 @@
1
+ import LZString from 'lz-string';
2
+ export const URL_CODE_PARAM = 'code';
3
+ export const MAX_URL_LENGTH = 8000;
4
+ export const DEFAULT_BASE_URL = process.env.DIAGENT_BASE_URL ?? 'https://diagent.dev/';
5
+ /** Matches a `/d/<10 base32 chars>` short URL path. */
6
+ const SHORT_URL_PATH_RE = /^\/d\/[a-z2-7]{10}$/;
7
+ export function buildShareableUrl(mermaid, baseUrl = DEFAULT_BASE_URL) {
8
+ if (!mermaid || !mermaid.trim()) {
9
+ return { ok: false, error: 'Nothing to encode' };
10
+ }
11
+ const encoded = LZString.compressToEncodedURIComponent(mermaid);
12
+ const normalized = baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`;
13
+ const url = `${normalized}?${URL_CODE_PARAM}=${encoded}`;
14
+ if (url.length > MAX_URL_LENGTH) {
15
+ return {
16
+ ok: false,
17
+ error: `Diagram too large for URL (${url.length} > ${MAX_URL_LENGTH} chars)`,
18
+ };
19
+ }
20
+ return { ok: true, url };
21
+ }
22
+ export function extractMermaidFromUrl(url) {
23
+ let parsed;
24
+ try {
25
+ parsed = new URL(url);
26
+ }
27
+ catch {
28
+ return null;
29
+ }
30
+ const raw = parsed.searchParams.get(URL_CODE_PARAM);
31
+ if (!raw)
32
+ return null;
33
+ const decoded = LZString.decompressFromEncodedURIComponent(raw);
34
+ return decoded || null;
35
+ }
36
+ /**
37
+ * Try to shorten a Mermaid source via the backend (`POST {baseUrl}api/s`).
38
+ * Returns a short URL on success, or an error result on any failure
39
+ * (network, 3s timeout, non-2xx). Retries once after a 500ms delay to
40
+ * absorb cold-start hiccups and transient 5xxs from Cloudflare Workers.
41
+ * Callers should fall back to `buildShareableUrl` for the inline format.
42
+ */
43
+ export async function shortenViaBackend(mermaid, baseUrl = DEFAULT_BASE_URL) {
44
+ if (!mermaid || !mermaid.trim()) {
45
+ return { ok: false, error: 'Nothing to encode' };
46
+ }
47
+ const normalized = baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`;
48
+ const apiUrl = `${normalized}api/s`;
49
+ const attempt = async () => {
50
+ try {
51
+ const controller = new AbortController();
52
+ const timer = setTimeout(() => controller.abort(), 3000);
53
+ const res = await fetch(apiUrl, {
54
+ method: 'POST',
55
+ headers: { 'Content-Type': 'text/plain' },
56
+ body: mermaid,
57
+ signal: controller.signal,
58
+ });
59
+ clearTimeout(timer);
60
+ if (!res.ok)
61
+ return { ok: false, error: `backend ${res.status}` };
62
+ const data = (await res.json());
63
+ if (!data?.url)
64
+ return { ok: false, error: 'malformed backend response' };
65
+ return { ok: true, url: data.url };
66
+ }
67
+ catch (err) {
68
+ return {
69
+ ok: false,
70
+ error: err instanceof Error ? err.message : 'network error',
71
+ };
72
+ }
73
+ };
74
+ const first = await attempt();
75
+ if (first.ok)
76
+ return first;
77
+ // Retry once after a short delay — first-call cold starts and
78
+ // transient 5xxs are common on Cloudflare Workers free tier.
79
+ await new Promise((r) => setTimeout(r, 500));
80
+ return attempt();
81
+ }
82
+ /**
83
+ * Resolve any Diagent URL (inline `?code=` OR short `/d/<id>`) to its
84
+ * underlying Mermaid source. For inline URLs this is a synchronous
85
+ * extraction. For short URLs this HEADs the URL to follow the 302 Location
86
+ * header, then extracts `?code=` from the redirect target. 3s timeout;
87
+ * fails fast with a typed error result.
88
+ */
89
+ export async function resolveMermaidFromUrl(url) {
90
+ // Fast path: inline ?code= URLs don't need a network call.
91
+ const direct = extractMermaidFromUrl(url);
92
+ if (direct)
93
+ return { ok: true, mermaid: direct };
94
+ // Parse the URL to check for the /d/<id> short-URL shape.
95
+ let parsed;
96
+ try {
97
+ parsed = new URL(url);
98
+ }
99
+ catch {
100
+ return { ok: false, error: 'not a valid URL' };
101
+ }
102
+ if (!SHORT_URL_PATH_RE.test(parsed.pathname)) {
103
+ return {
104
+ ok: false,
105
+ error: 'URL has no ?code= param and is not a /d/<id> short URL',
106
+ };
107
+ }
108
+ // Follow the 302 to pull the underlying inline URL, then recurse.
109
+ try {
110
+ const controller = new AbortController();
111
+ const timer = setTimeout(() => controller.abort(), 3000);
112
+ const res = await fetch(url, {
113
+ method: 'HEAD',
114
+ redirect: 'manual',
115
+ signal: controller.signal,
116
+ });
117
+ clearTimeout(timer);
118
+ if (res.status !== 302) {
119
+ return {
120
+ ok: false,
121
+ error: `backend returned ${res.status}, expected 302`,
122
+ };
123
+ }
124
+ const location = res.headers.get('location');
125
+ if (!location) {
126
+ return { ok: false, error: 'backend 302 missing Location header' };
127
+ }
128
+ // Worker redirects a missing /d/:id to /?notfound=:id so both
129
+ // browser and CLI get a structured signal. Surface a specific,
130
+ // user-friendly error that the agent can relay verbatim.
131
+ try {
132
+ const locUrl = new URL(location, parsed.origin);
133
+ const nfId = locUrl.searchParams.get('notfound');
134
+ if (nfId) {
135
+ return {
136
+ ok: false,
137
+ error: `short URL /d/${nfId} was not found (may have been deleted or never existed)`,
138
+ };
139
+ }
140
+ }
141
+ catch {
142
+ // Fall through to the existing decode path; if the Location is
143
+ // malformed, extractMermaidFromUrl will return its own error.
144
+ }
145
+ const decoded = extractMermaidFromUrl(location);
146
+ if (!decoded) {
147
+ return {
148
+ ok: false,
149
+ error: 'redirect target has no valid ?code= param',
150
+ };
151
+ }
152
+ return { ok: true, mermaid: decoded };
153
+ }
154
+ catch (err) {
155
+ return {
156
+ ok: false,
157
+ error: err instanceof Error ? err.message : 'network error',
158
+ };
159
+ }
160
+ }
161
+ /**
162
+ * Primary encode entry point: tries the backend first (unless preferInline
163
+ * is set), falls back to inline URL on failure. The caller gets both the
164
+ * result and a flag indicating which path produced it.
165
+ */
166
+ export async function shortenOrInline(mermaid, baseUrl = DEFAULT_BASE_URL, preferInline = false) {
167
+ if (!preferInline) {
168
+ const short = await shortenViaBackend(mermaid, baseUrl);
169
+ if (short.ok)
170
+ return { result: short, usedFallback: false };
171
+ }
172
+ return {
173
+ result: buildShareableUrl(mermaid, baseUrl),
174
+ usedFallback: !preferInline,
175
+ };
176
+ }
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@diagent/cli",
3
+ "version": "0.1.0",
4
+ "description": "Encode and decode Diagent shareable flowchart URLs from the command line. Designed for agents (Claude Code, Cursor) and humans.",
5
+ "type": "module",
6
+ "bin": {
7
+ "diagent": "./build/cli.js"
8
+ },
9
+ "files": [
10
+ "build/cli.js",
11
+ "build/core.js",
12
+ "README.md"
13
+ ],
14
+ "publishConfig": {
15
+ "access": "public"
16
+ },
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "https://github.com/daniellee-ux/Diagent.git",
20
+ "directory": "cli"
21
+ },
22
+ "homepage": "https://github.com/daniellee-ux/Diagent/tree/main/cli#readme",
23
+ "keywords": ["mermaid", "diagram", "flowchart", "cli", "agent", "claude-code"],
24
+ "scripts": {
25
+ "build": "tsc && chmod +x build/cli.js",
26
+ "start": "node build/cli.js",
27
+ "test": "node --test build/core.test.js"
28
+ },
29
+ "engines": {
30
+ "node": ">=18.3.0"
31
+ },
32
+ "dependencies": {
33
+ "lz-string": "^1.5.0"
34
+ },
35
+ "devDependencies": {
36
+ "@types/node": "^24.12.2",
37
+ "typescript": "~6.0.2"
38
+ }
39
+ }