@v1nvn/md 0.44.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 +10 -10
- package/package.json +9 -17
- package/src/index.ts +38 -0
- package/src/share.ts +81 -0
- package/dist/index.js +0 -212
- package/dist/index.js.map +0 -1
- package/dist/send.mjs +0 -3169
- package/dist/send.mjs.map +0 -1
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.
|
|
27
|
-
npx -y @v1nvn/md@0.
|
|
28
|
-
npx -y @v1nvn/md@0.
|
|
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.44.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.
|
|
38
|
-
| `npx -y @v1nvn/md@0.
|
|
39
|
-
| `npx -y @v1nvn/md@0.
|
|
40
|
-
| `npx -y @v1nvn/md@0.
|
|
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
|
-
| `
|
|
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>/
|
|
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>/
|
|
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.
|
|
4
|
-
"description": "Send a Markdown reply to
|
|
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": "
|
|
7
|
+
"md-send": "./src/index.ts"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
|
-
"
|
|
11
|
-
"test": "
|
|
12
|
-
"test:watch": "vitest"
|
|
10
|
+
"test": "yarn run -T vitest run",
|
|
11
|
+
"test:watch": "yarn run -T vitest"
|
|
13
12
|
},
|
|
14
13
|
"keywords": [
|
|
15
|
-
"
|
|
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
|
-
"
|
|
28
|
+
"src"
|
|
32
29
|
],
|
|
33
30
|
"publishConfig": {
|
|
34
31
|
"access": "public"
|
|
35
32
|
},
|
|
36
33
|
"engines": {
|
|
37
|
-
"node": ">=
|
|
34
|
+
"node": ">=23.6"
|
|
38
35
|
},
|
|
39
36
|
"dependencies": {
|
|
40
|
-
"@v1nvn/agentic-core": "
|
|
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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"}
|