@v1nvn/md 0.43.0 → 0.45.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 CHANGED
@@ -23,9 +23,9 @@ claude plugin install md@agentic
23
23
  In a terminal, last reply or a named file:
24
24
 
25
25
  ```sh
26
- npx -y @v1nvn/md@0.43.0 # editable — both panes
27
- npx -y @v1nvn/md@0.43.0 --view # read-only — preview pane only
28
- npx -y @v1nvn/md@0.43.0 reply.md
26
+ npx -y @v1nvn/md@0.45.0 # editable — both panes
27
+ npx -y @v1nvn/md@0.45.0 --view # read-only — preview pane only
28
+ npx -y @v1nvn/md@0.45.0 reply.md
29
29
  ```
30
30
 
31
31
  ## Usage
@@ -34,10 +34,10 @@ npx -y @v1nvn/md@0.43.0 reply.md
34
34
  |---|---|
35
35
  | `/md-edit` | the previous assistant reply → viewer, edit pane enabled, one dim status row |
36
36
  | `/md-view` | the same share read-only — preview pane only |
37
- | `npx -y @v1nvn/md@0.43.0` | the same share, printed bare |
38
- | `npx -y @v1nvn/md@0.43.0 --view` | read-only share |
39
- | `npx -y @v1nvn/md@0.43.0 reply.md` | that file instead of the last reply |
40
- | `npx -y @v1nvn/md@0.43.0 -` | Markdown from stdin |
37
+ | `npx -y @v1nvn/md@0.45.0` | the same share, printed bare |
38
+ | `npx -y @v1nvn/md@0.45.0 --view` | read-only share |
39
+ | `npx -y @v1nvn/md@0.45.0 reply.md` | that file instead of the last reply |
40
+ | `npx -y @v1nvn/md@0.45.0 -` | Markdown from stdin |
41
41
 
42
42
  `MD_VIEWER_URL` sets the viewer (default `https://md.v1n.space`); any set
43
43
  `MD_NO_OPEN` value (not just `1`) skips opening the browser.
@@ -64,15 +64,15 @@ and re-vendor `types/claude-code.d.ts` (repo root) from the engine-laid
64
64
  | `src/index.ts` | bin entry (`md-send`) — dispatch, exit codes |
65
65
  | `src/share.ts` | the `#share=` URL, browser open, clipboard, status line |
66
66
  | `hooks/register.ts` | the mod: registers `/md-edit` and `/md-view`, execs the CLI, shows its line |
67
- | `bin/send.mjs` | the standalone build the mod execs — committed, synced by `yarn build` |
67
+ | `src/index.ts` | the CLI source the mod execs — node runs it directly, nothing built |
68
68
 
69
69
  ## Contracts
70
70
 
71
71
  - One status line of output (e.g. `Opened in Markdown-Viewer (link copied).`).
72
72
  - The document reaches only the configured viewer — no third-party service.
73
- - `/md-edit` and `/md-view` are each one `node <plugin root>/bin/send.mjs`
73
+ - `/md-edit` and `/md-view` are each one `node <plugin root>/src/index.ts
74
74
  exec through the session's Bash tool (`/md-view` adds `--view`) — the same
75
75
  share `npx md-send` runs. The one allow rule both need is
76
- `Bash(node <plugin root>/bin/send.mjs:*)`; the first run asks for it once,
76
+ `Bash(node <plugin root>/src/index.ts:*)`; the first run asks for it once,
77
77
  then it is remembered. Its status line is a dim transcript row the model
78
78
  never reads.
package/package.json CHANGED
@@ -1,20 +1,17 @@
1
1
  {
2
2
  "name": "@v1nvn/md",
3
- "version": "0.43.0",
4
- "description": "Send a Markdown reply to a self-hosted Markdown-Viewer as a #share= URL — the md-send CLI behind the md plugin.",
3
+ "version": "0.45.0",
4
+ "description": "Send a Markdown reply to the Markdown-Viewer as a #share= URL — the md-send CLI behind the md plugin.",
5
5
  "type": "module",
6
6
  "bin": {
7
- "md-send": "dist/index.js"
7
+ "md-send": "./src/index.ts"
8
8
  },
9
9
  "scripts": {
10
- "build": "vite build && vite build --mode send",
11
- "test": "vitest run",
12
- "test:watch": "vitest"
10
+ "test": "yarn run -T vitest run",
11
+ "test:watch": "yarn run -T vitest"
13
12
  },
14
13
  "keywords": [
15
- "markdown",
16
- "markdown-viewer",
17
- "share",
14
+ "Markdown-Viewer",
18
15
  "claude-code"
19
16
  ],
20
17
  "author": {
@@ -28,20 +25,15 @@
28
25
  "directory": "packages/md"
29
26
  },
30
27
  "files": [
31
- "dist"
28
+ "src"
32
29
  ],
33
30
  "publishConfig": {
34
31
  "access": "public"
35
32
  },
36
33
  "engines": {
37
- "node": ">=22"
34
+ "node": ">=23.6"
38
35
  },
39
36
  "dependencies": {
40
- "@v1nvn/agentic-core": "^0.43.0",
41
- "commander": "^15.0.0"
42
- },
43
- "devDependencies": {
44
- "vite": "^8.1.4",
45
- "vitest": "^4.1.10"
37
+ "@v1nvn/agentic-core": "0.45.0"
46
38
  }
47
39
  }
package/src/index.ts ADDED
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ lastReply,
4
+ readMarkdownFile,
5
+ readStdin,
6
+ runCli,
7
+ } from '@v1nvn/agentic-core';
8
+
9
+ import { mdSend } from './share.ts';
10
+
11
+ import type { CliSpec } from '@v1nvn/agentic-core';
12
+
13
+ const spec: CliSpec = {
14
+ name: 'md-send',
15
+ description: 'Send a Markdown reply to the Markdown-Viewer as a #share= URL',
16
+ argument: {
17
+ flags: '[file]',
18
+ description: 'Markdown file, - for stdin; the last reply when omitted',
19
+ },
20
+ options: [
21
+ {
22
+ flags: '--view',
23
+ description: 'open the viewer read-only, without the edit pane',
24
+ },
25
+ ],
26
+ run: async (options, args) => {
27
+ const arg = args.at(0);
28
+ const markdown =
29
+ arg === '-'
30
+ ? await readStdin()
31
+ : arg === undefined
32
+ ? lastReply()
33
+ : readMarkdownFile(arg);
34
+ console.log(mdSend(markdown, options.view === true));
35
+ },
36
+ };
37
+
38
+ process.exitCode = await runCli(spec, process.argv.slice(2));
package/src/share.ts ADDED
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Send Markdown to the self-hosted Markdown-Viewer as a #share= URL — opens it
3
+ * in the browser and copies the URL.
4
+ *
5
+ * md-send path/to/reply.md # from a file
6
+ * md-send - # from stdin
7
+ * md-send # from the last Claude reply (core.lastReply)
8
+ * md-send --view # open read-only (no edit pane)
9
+ *
10
+ * Env knobs: MD_VIEWER_URL (default: https://md.v1n.space), MD_NO_OPEN (any value skips the browser).
11
+ */
12
+
13
+ import { spawnSync } from 'node:child_process';
14
+ import { deflateSync } from 'node:zlib';
15
+
16
+ /**
17
+ * Encode Markdown into the Markdown-Viewer share token.
18
+ *
19
+ * Pipeline: UTF-8 bytes → zlib deflate (RFC 1950, pako.deflate-compatible) →
20
+ * base64url. Matches Markdown-Viewer's encodeMarkdownForShare() in script.js
21
+ * byte-for-byte — the decoder (pako.inflate) accepts any valid zlib stream, so
22
+ * compression level is irrelevant.
23
+ */
24
+ export function encodeShare(markdown: string): string {
25
+ return deflateSync(Buffer.from(markdown, 'utf8'))
26
+ .toString('base64')
27
+ .replaceAll('+', '-')
28
+ .replaceAll('/', '_')
29
+ .replace(/=+$/, '');
30
+ }
31
+
32
+ export function shareUrl(markdown: string, view = false): string {
33
+ const base = (process.env.MD_VIEWER_URL ?? 'https://md.v1n.space').replace(
34
+ /\/+$/,
35
+ '',
36
+ );
37
+ const mode = view ? '' : '&edit=1';
38
+ // Without `edit=1` the viewer opens the snapshot read-only — preview pane
39
+ // only, with edit and split disabled (Markdown-Viewer script.js: loadFromShareHash).
40
+ return `${base}/#share=${encodeShare(markdown)}${mode}`;
41
+ }
42
+
43
+ function copyToClipboard(text: string): boolean {
44
+ try {
45
+ return (
46
+ spawnSync('pbcopy', { input: text, stdio: ['pipe', 'ignore', 'ignore'] })
47
+ .status === 0
48
+ );
49
+ } catch {
50
+ return false;
51
+ }
52
+ }
53
+
54
+ function openUrl(url: string): boolean {
55
+ try {
56
+ return spawnSync('open', [url], { stdio: 'ignore' }).status === 0;
57
+ } catch {
58
+ return false;
59
+ }
60
+ }
61
+
62
+ export function statusLine(opened: boolean, copied: boolean): string {
63
+ if (opened && copied) {
64
+ return 'Opened in Markdown-Viewer (link copied).';
65
+ }
66
+ if (opened) {
67
+ return 'Opened in Markdown-Viewer.';
68
+ }
69
+ if (copied) {
70
+ return 'Markdown-Viewer link copied to clipboard.';
71
+ }
72
+ return 'Markdown-Viewer link ready.';
73
+ }
74
+
75
+ /** Shares the markdown and returns the status line; the URL itself is never printed. */
76
+ export function mdSend(markdown: string, view = false): string {
77
+ const url = shareUrl(markdown, view);
78
+ const copied = copyToClipboard(url);
79
+ const opened = process.env.MD_NO_OPEN === undefined && openUrl(url);
80
+ return statusLine(opened, copied);
81
+ }
package/dist/index.js DELETED
@@ -1,212 +0,0 @@
1
- #!/usr/bin/env node
2
- import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
3
- import { homedir } from "node:os";
4
- import { join } from "node:path";
5
- import { Command } from "commander";
6
- import { spawnSync } from "node:child_process";
7
- import { deflateSync } from "node:zlib";
8
- //#region ../core/dist/index.js
9
- var HelpRequested = class extends Error {
10
- command;
11
- constructor(command) {
12
- super(`${command.name()} --help`);
13
- this.command = command;
14
- }
15
- };
16
- function parseQuietly(program, args) {
17
- function quiet(command) {
18
- command.configureOutput({
19
- writeOut: () => void 0,
20
- writeErr: () => void 0
21
- });
22
- command.exitOverride((err) => {
23
- const code = err.code;
24
- if (code === "commander.help" || code === "commander.helpDisplayed") throw new HelpRequested(command);
25
- throw err;
26
- });
27
- command.commands.forEach(quiet);
28
- }
29
- try {
30
- quiet(program);
31
- program.parse([...args], { from: "user" });
32
- return program;
33
- } catch (err) {
34
- return err instanceof HelpRequested ? err : void 0;
35
- }
36
- }
37
- function exitWithCode(code) {
38
- process.exit(code);
39
- }
40
- function printHelpAndExit(command) {
41
- console.log(command.helpInformation());
42
- exitWithCode(0);
43
- }
44
- function printUsageAndExit(program) {
45
- console.error(program.helpInformation());
46
- exitWithCode(1);
47
- }
48
- async function runMain(main) {
49
- try {
50
- await main();
51
- } catch (e) {
52
- console.error(e.message);
53
- exitWithCode(1);
54
- }
55
- }
56
- function readAll(stream) {
57
- return new Promise((resolve, reject) => {
58
- let data = "";
59
- stream.setEncoding("utf8");
60
- stream.on("data", (chunk) => {
61
- data += chunk;
62
- });
63
- stream.on("end", () => {
64
- resolve(data);
65
- });
66
- stream.on("error", reject);
67
- });
68
- }
69
- function readStdin(stream = process.stdin) {
70
- return readAll(stream);
71
- }
72
- /** A markdown file's contents, with the CLI's no-such-file error. */
73
- function readMarkdownFile(path) {
74
- if (!statSync(path, { throwIfNoEntry: false })?.isFile()) throw new Error(`no such file: ${path}`);
75
- return readFileSync(path, "utf8");
76
- }
77
- function isFile(path) {
78
- try {
79
- return statSync(path).isFile();
80
- } catch {
81
- return false;
82
- }
83
- }
84
- function textBlocks(entry) {
85
- const content = entry.message?.content;
86
- if (entry.type !== "assistant" || !Array.isArray(content)) return;
87
- const blocks = content.filter((b) => typeof b === "object" && b !== null && b.type === "text");
88
- return blocks.length > 0 ? blocks : void 0;
89
- }
90
- function claudeProjectsDir() {
91
- const claudeDir = process.env.CLAUDE_DIR ?? join(homedir(), ".claude");
92
- return join(claudeDir, "projects");
93
- }
94
- /**
95
- * The last assistant text reply from a Claude Code session, reproducing Claude
96
- * Code's `/copy` byte-for-byte: the last assistant entry that contains a
97
- * `text` block, only its `text` block(s) (tool_use / thinking dropped), blocks
98
- * joined with a blank line, and no trailing newline.
99
- *
100
- * @param arg a transcript file, a session UUID under the project dir, or
101
- * nothing to use the newest session for the current project.
102
- */
103
- function lastReply(arg) {
104
- const proj = (process.env.CLAUDE_PROJECT_DIR ?? process.cwd()).replaceAll("/", "-");
105
- const projDir = join(claudeProjectsDir(), proj);
106
- let file;
107
- if (arg !== void 0) file = isFile(arg) ? arg : join(projDir, `${arg}.jsonl`);
108
- else if (existsSync(projDir)) {
109
- const newest = readdirSync(projDir).filter((f) => f.endsWith(".jsonl")).map((f) => ({
110
- f,
111
- mtimeMs: statSync(join(projDir, f)).mtimeMs
112
- })).sort((a, b) => b.mtimeMs - a.mtimeMs).at(0);
113
- if (newest) file = join(projDir, newest.f);
114
- }
115
- if (file === void 0 || !isFile(file)) throw new Error(`no session transcript found in ${projDir}`);
116
- let last;
117
- for (const line of readFileSync(file, "utf8").split("\n")) {
118
- if (!line.includes("\"assistant\"")) continue;
119
- let entry;
120
- try {
121
- entry = JSON.parse(line);
122
- } catch {
123
- continue;
124
- }
125
- const blocks = textBlocks(entry);
126
- if (blocks) last = blocks;
127
- }
128
- return (last ?? []).map((b) => b.text ?? "").join("\n\n");
129
- }
130
- Number.MAX_SAFE_INTEGER;
131
- //#endregion
132
- //#region src/share.ts
133
- /**
134
- * Send Markdown to the self-hosted Markdown-Viewer as a #share= URL — opens it
135
- * in the browser and copies the URL.
136
- *
137
- * md-send path/to/reply.md # from a file
138
- * md-send - # from stdin
139
- * md-send # from the last Claude reply (core.lastReply)
140
- * md-send --view # open read-only (no edit pane)
141
- *
142
- * Env knobs: MD_VIEWER_URL (default: https://md.v1n.space), MD_NO_OPEN (any value skips the browser).
143
- */
144
- /**
145
- * Encode Markdown into the Markdown-Viewer share token.
146
- *
147
- * Pipeline: UTF-8 bytes → zlib deflate (RFC 1950, pako.deflate-compatible) →
148
- * base64url. Matches Markdown-Viewer's encodeMarkdownForShare() in script.js
149
- * byte-for-byte — the decoder (pako.inflate) accepts any valid zlib stream, so
150
- * compression level is irrelevant.
151
- */
152
- function encodeShare(markdown) {
153
- return deflateSync(Buffer.from(markdown, "utf8")).toString("base64").replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
154
- }
155
- function shareUrl(markdown, view = false) {
156
- const base = (process.env.MD_VIEWER_URL ?? "https://md.v1n.space").replace(/\/+$/, "");
157
- const mode = view ? "" : "&edit=1";
158
- return `${base}/#share=${encodeShare(markdown)}${mode}`;
159
- }
160
- function copyToClipboard(text) {
161
- try {
162
- return spawnSync("pbcopy", {
163
- input: text,
164
- stdio: [
165
- "pipe",
166
- "ignore",
167
- "ignore"
168
- ]
169
- }).status === 0;
170
- } catch {
171
- return false;
172
- }
173
- }
174
- function openUrl(url) {
175
- try {
176
- return spawnSync("open", [url], { stdio: "ignore" }).status === 0;
177
- } catch {
178
- return false;
179
- }
180
- }
181
- function statusLine(opened, copied) {
182
- if (opened && copied) return "Opened in Markdown-Viewer (link copied).";
183
- if (opened) return "Opened in Markdown-Viewer.";
184
- if (copied) return "Markdown-Viewer link copied to clipboard.";
185
- return "Markdown-Viewer link ready.";
186
- }
187
- /** Shares the markdown and returns the status line; the URL itself is never printed. */
188
- function mdSend(markdown, view = false) {
189
- const url = shareUrl(markdown, view);
190
- const copied = copyToClipboard(url);
191
- return statusLine(process.env.MD_NO_OPEN === void 0 && openUrl(url), copied);
192
- }
193
- //#endregion
194
- //#region src/index.ts
195
- var program = new Command().name("md-send").description("Send a Markdown reply to the Markdown-Viewer as a #share= URL").argument("[file]", "Markdown file, - for stdin; the last reply when omitted").option("--view", "open the viewer read-only, without the edit pane");
196
- var asked = parseQuietly(program, process.argv.slice(2));
197
- if (asked instanceof HelpRequested) printHelpAndExit(asked.command);
198
- var parsed = asked ?? printUsageAndExit(program);
199
- var arg = parsed.args.at(0);
200
- var { view } = parsed.opts();
201
- function readMarkdown() {
202
- if (arg === "-") return readStdin();
203
- if (arg === void 0) return lastReply();
204
- return readMarkdownFile(arg);
205
- }
206
- await runMain(async () => {
207
- console.log(mdSend(await readMarkdown(), view ?? false));
208
- });
209
- //#endregion
210
- export {};
211
-
212
- //# sourceMappingURL=index.js.map
package/dist/index.js.map DELETED
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../core/dist/index.js","../src/share.ts","../src/index.ts"],"sourcesContent":["import { existsSync, readFileSync, readdirSync, statSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\n//#region src/cli.ts\nvar HelpRequested = class extends Error {\n\tcommand;\n\tconstructor(command) {\n\t\tsuper(`${command.name()} --help`);\n\t\tthis.command = command;\n\t}\n};\nfunction parseQuietly(program, args) {\n\tfunction quiet(command) {\n\t\tcommand.configureOutput({\n\t\t\twriteOut: () => void 0,\n\t\t\twriteErr: () => void 0\n\t\t});\n\t\tcommand.exitOverride((err) => {\n\t\t\tconst code = err.code;\n\t\t\tif (code === \"commander.help\" || code === \"commander.helpDisplayed\") throw new HelpRequested(command);\n\t\t\tthrow err;\n\t\t});\n\t\tcommand.commands.forEach(quiet);\n\t}\n\ttry {\n\t\tquiet(program);\n\t\tprogram.parse([...args], { from: \"user\" });\n\t\treturn program;\n\t} catch (err) {\n\t\treturn err instanceof HelpRequested ? err : void 0;\n\t}\n}\nfunction exitWithCode(code) {\n\tprocess.exit(code);\n}\nfunction printHelpAndExit(command) {\n\tconsole.log(command.helpInformation());\n\texitWithCode(0);\n}\nfunction printUsageAndExit(program) {\n\tconsole.error(program.helpInformation());\n\texitWithCode(1);\n}\nasync function runMain(main) {\n\ttry {\n\t\tawait main();\n\t} catch (e) {\n\t\tconsole.error(e.message);\n\t\texitWithCode(1);\n\t}\n}\n//#endregion\n//#region src/dev.ts\nvar RELOAD_DEBOUNCE_MS = 150;\nvar IGNORED_SEGMENTS = [\n\t\"/node_modules/\",\n\t\"/dist/\",\n\t\"/.git/\",\n\t\"/coverage/\"\n];\nfunction shouldReload(file) {\n\treturn IGNORED_SEGMENTS.every((seg) => !file.includes(seg));\n}\n/**\n* Debounced file-change reloads, serialized via a promise chain: concurrent\n* saves never overlap, and a mid-reload save still produces a follow-up.\n*/\nfunction watchWithReload(vite, reload) {\n\tlet chain = Promise.resolve();\n\tlet timer;\n\tvite.watcher.on(\"change\", (file) => {\n\t\tif (!shouldReload(file)) return;\n\t\tif (timer) clearTimeout(timer);\n\t\ttimer = setTimeout(() => {\n\t\t\ttimer = void 0;\n\t\t\tchain = chain.then(reload);\n\t\t}, RELOAD_DEBOUNCE_MS);\n\t});\n}\n//#endregion\n//#region src/input.ts\nfunction readAll(stream) {\n\treturn new Promise((resolve, reject) => {\n\t\tlet data = \"\";\n\t\tstream.setEncoding(\"utf8\");\n\t\tstream.on(\"data\", (chunk) => {\n\t\t\tdata += chunk;\n\t\t});\n\t\tstream.on(\"end\", () => {\n\t\t\tresolve(data);\n\t\t});\n\t\tstream.on(\"error\", reject);\n\t});\n}\nfunction readStdin(stream = process.stdin) {\n\treturn readAll(stream);\n}\n/** A markdown file's contents, with the CLI's no-such-file error. */\nfunction readMarkdownFile(path) {\n\tif (!statSync(path, { throwIfNoEntry: false })?.isFile()) throw new Error(`no such file: ${path}`);\n\treturn readFileSync(path, \"utf8\");\n}\n//#endregion\n//#region src/last-reply.ts\nfunction isFile(path) {\n\ttry {\n\t\treturn statSync(path).isFile();\n\t} catch {\n\t\treturn false;\n\t}\n}\nfunction textBlocks(entry) {\n\tconst content = entry.message?.content;\n\tif (entry.type !== \"assistant\" || !Array.isArray(content)) return;\n\tconst blocks = content.filter((b) => typeof b === \"object\" && b !== null && b.type === \"text\");\n\treturn blocks.length > 0 ? blocks : void 0;\n}\nfunction claudeProjectsDir() {\n\tconst claudeDir = process.env.CLAUDE_DIR ?? join(homedir(), \".claude\");\n\treturn join(claudeDir, \"projects\");\n}\n/**\n* The last assistant text reply from a Claude Code session, reproducing Claude\n* Code's `/copy` byte-for-byte: the last assistant entry that contains a\n* `text` block, only its `text` block(s) (tool_use / thinking dropped), blocks\n* joined with a blank line, and no trailing newline.\n*\n* @param arg a transcript file, a session UUID under the project dir, or\n* nothing to use the newest session for the current project.\n*/\nfunction lastReply(arg) {\n\tconst proj = (process.env.CLAUDE_PROJECT_DIR ?? process.cwd()).replaceAll(\"/\", \"-\");\n\tconst projDir = join(claudeProjectsDir(), proj);\n\tlet file;\n\tif (arg !== void 0) file = isFile(arg) ? arg : join(projDir, `${arg}.jsonl`);\n\telse if (existsSync(projDir)) {\n\t\tconst newest = readdirSync(projDir).filter((f) => f.endsWith(\".jsonl\")).map((f) => ({\n\t\t\tf,\n\t\t\tmtimeMs: statSync(join(projDir, f)).mtimeMs\n\t\t})).sort((a, b) => b.mtimeMs - a.mtimeMs).at(0);\n\t\tif (newest) file = join(projDir, newest.f);\n\t}\n\tif (file === void 0 || !isFile(file)) throw new Error(`no session transcript found in ${projDir}`);\n\tlet last;\n\tfor (const line of readFileSync(file, \"utf8\").split(\"\\n\")) {\n\t\tif (!line.includes(\"\\\"assistant\\\"\")) continue;\n\t\tlet entry;\n\t\ttry {\n\t\t\tentry = JSON.parse(line);\n\t\t} catch {\n\t\t\tcontinue;\n\t\t}\n\t\tconst blocks = textBlocks(entry);\n\t\tif (blocks) last = blocks;\n\t}\n\treturn (last ?? []).map((b) => b.text ?? \"\").join(\"\\n\\n\");\n}\n//#endregion\n//#region src/logger.ts\nvar LEVEL_RANK = {\n\tdebug: 10,\n\tinfo: 20,\n\twarn: 30,\n\terror: 40,\n\tsilent: Number.MAX_SAFE_INTEGER\n};\nvar LEVEL_LABEL = {\n\tdebug: \"DEBUG\",\n\terror: \"ERROR\",\n\tinfo: \"INFO\",\n\twarn: \"WARN\"\n};\nvar Logger = class {\n\tactiveLevel;\n\tstderr;\n\tconstructor(activeLevel, stderr = (line) => process.stderr.write(`${line}\\n`)) {\n\t\tthis.activeLevel = activeLevel;\n\t\tthis.stderr = stderr;\n\t}\n\tdebug(message) {\n\t\tthis.log(\"debug\", message);\n\t}\n\terror(message) {\n\t\tthis.log(\"error\", message);\n\t}\n\tinfo(message) {\n\t\tthis.log(\"info\", message);\n\t}\n\twarn(message) {\n\t\tthis.log(\"warn\", message);\n\t}\n\tlog(level, message) {\n\t\tif (LEVEL_RANK[level] >= LEVEL_RANK[this.activeLevel]) this.stderr(`${LEVEL_LABEL[level]} ${message}`);\n\t}\n};\n//#endregion\n//#region src/shutdown.ts\n/** Close each closable in order (best-effort), then force-exit. */\nfunction shutdownOnSignals(closables, signals = [\"SIGINT\", \"SIGTERM\"]) {\n\tfunction shutdown() {\n\t\tclosables.reduce((acc, closable) => acc.then(async () => {\n\t\t\ttry {\n\t\t\t\tawait closable.close();\n\t\t\t} catch {}\n\t\t}), Promise.resolve()).finally(() => {\n\t\t\tprocess.exit(0);\n\t\t});\n\t}\n\tfor (const signal of signals) process.on(signal, shutdown);\n}\n//#endregion\nexport { HelpRequested, Logger, claudeProjectsDir, lastReply, parseQuietly, printHelpAndExit, printUsageAndExit, readMarkdownFile, readStdin, runMain, shutdownOnSignals, watchWithReload };\n\n//# sourceMappingURL=index.js.map","/**\n * Send Markdown to the self-hosted Markdown-Viewer as a #share= URL — opens it\n * in the browser and copies the URL.\n *\n * md-send path/to/reply.md # from a file\n * md-send - # from stdin\n * md-send # from the last Claude reply (core.lastReply)\n * md-send --view # open read-only (no edit pane)\n *\n * Env knobs: MD_VIEWER_URL (default: https://md.v1n.space), MD_NO_OPEN (any value skips the browser).\n */\n\nimport { spawnSync } from 'node:child_process';\nimport { deflateSync } from 'node:zlib';\n\n/**\n * Encode Markdown into the Markdown-Viewer share token.\n *\n * Pipeline: UTF-8 bytes → zlib deflate (RFC 1950, pako.deflate-compatible) →\n * base64url. Matches Markdown-Viewer's encodeMarkdownForShare() in script.js\n * byte-for-byte — the decoder (pako.inflate) accepts any valid zlib stream, so\n * compression level is irrelevant.\n */\nexport function encodeShare(markdown: string): string {\n return deflateSync(Buffer.from(markdown, 'utf8'))\n .toString('base64')\n .replaceAll('+', '-')\n .replaceAll('/', '_')\n .replace(/=+$/, '');\n}\n\nexport function shareUrl(markdown: string, view = false): string {\n const base = (process.env.MD_VIEWER_URL ?? 'https://md.v1n.space').replace(\n /\\/+$/,\n '',\n );\n const mode = view ? '' : '&edit=1';\n // Without `edit=1` the viewer opens the snapshot read-only — preview pane\n // only, with edit and split disabled (Markdown-Viewer script.js: loadFromShareHash).\n return `${base}/#share=${encodeShare(markdown)}${mode}`;\n}\n\nfunction copyToClipboard(text: string): boolean {\n try {\n return (\n spawnSync('pbcopy', { input: text, stdio: ['pipe', 'ignore', 'ignore'] })\n .status === 0\n );\n } catch {\n return false;\n }\n}\n\nfunction openUrl(url: string): boolean {\n try {\n return spawnSync('open', [url], { stdio: 'ignore' }).status === 0;\n } catch {\n return false;\n }\n}\n\nexport function statusLine(opened: boolean, copied: boolean): string {\n if (opened && copied) {\n return 'Opened in Markdown-Viewer (link copied).';\n }\n if (opened) {\n return 'Opened in Markdown-Viewer.';\n }\n if (copied) {\n return 'Markdown-Viewer link copied to clipboard.';\n }\n return 'Markdown-Viewer link ready.';\n}\n\n/** Shares the markdown and returns the status line; the URL itself is never printed. */\nexport function mdSend(markdown: string, view = false): string {\n const url = shareUrl(markdown, view);\n const copied = copyToClipboard(url);\n const opened = process.env.MD_NO_OPEN === undefined && openUrl(url);\n return statusLine(opened, copied);\n}\n","import {\n HelpRequested,\n lastReply,\n parseQuietly,\n printHelpAndExit,\n printUsageAndExit,\n readMarkdownFile,\n readStdin,\n runMain,\n} from '@v1nvn/agentic-core';\nimport { Command } from 'commander';\n\nimport { mdSend } from './share.js';\n\nconst program = new Command()\n .name('md-send')\n .description('Send a Markdown reply to the Markdown-Viewer as a #share= URL')\n .argument('[file]', 'Markdown file, - for stdin; the last reply when omitted')\n .option('--view', 'open the viewer read-only, without the edit pane');\n\nconst asked = parseQuietly(program, process.argv.slice(2));\nif (asked instanceof HelpRequested) {\n printHelpAndExit(asked.command);\n}\nconst parsed = asked ?? printUsageAndExit(program);\nconst arg = parsed.args.at(0);\nconst { view } = parsed.opts<{ view: boolean | undefined }>();\n\nfunction readMarkdown(): Promise<string> | string {\n if (arg === '-') {\n return readStdin();\n }\n if (arg === undefined) {\n return lastReply();\n }\n return readMarkdownFile(arg);\n}\n\nawait runMain(async () => {\n console.log(mdSend(await readMarkdown(), view ?? false));\n});\n"],"mappings":"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"}