@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 +210 -0
- package/build/cli.js +129 -0
- package/build/core.js +176 -0
- package/package.json +39 -0
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
|
+
}
|