@code2rich/jpage 1.5.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.
Files changed (143) hide show
  1. package/.claude/settings.local.json +68 -0
  2. package/.dockerignore +8 -0
  3. package/.env.example +56 -0
  4. package/.github/workflows/ci.yml +43 -0
  5. package/CLAUDE.md +280 -0
  6. package/Dockerfile +44 -0
  7. package/LICENSE +21 -0
  8. package/README.md +433 -0
  9. package/README_EN.md +399 -0
  10. package/bin/args.js +64 -0
  11. package/bin/client.js +93 -0
  12. package/bin/commands/_shared.js +54 -0
  13. package/bin/commands/cat.js +23 -0
  14. package/bin/commands/ls.js +44 -0
  15. package/bin/commands/mv.js +20 -0
  16. package/bin/commands/rm.js +22 -0
  17. package/bin/commands/skills.js +70 -0
  18. package/bin/commands/star.js +23 -0
  19. package/bin/commands/tags.js +97 -0
  20. package/bin/commands/upload.js +84 -0
  21. package/bin/commands/url.js +25 -0
  22. package/bin/commands/whoami.js +29 -0
  23. package/bin/config.js +85 -0
  24. package/bin/jpage.js +168 -0
  25. package/build.js +112 -0
  26. package/docker-compose.yml +26 -0
  27. package/docs/api.md +438 -0
  28. package/docs/design/005-custom-modal.md +296 -0
  29. package/docs/design/013-file-version-history.md +324 -0
  30. package/docs/design/billing-system.md +600 -0
  31. package/docs/design/db-index-and-healthcheck.md +176 -0
  32. package/docs/design/loading-states.md +209 -0
  33. package/docs/virtual-hosting-feasibility.md +453 -0
  34. package/eslint.config.mjs +172 -0
  35. package/lib/auth-state.js +15 -0
  36. package/lib/categories.js +20 -0
  37. package/lib/crypto.js +85 -0
  38. package/lib/csp.js +66 -0
  39. package/lib/db.js +53 -0
  40. package/lib/dispatch.js +103 -0
  41. package/lib/fts.js +81 -0
  42. package/lib/middleware/auth.js +114 -0
  43. package/lib/middleware/files.js +42 -0
  44. package/lib/paths.js +9 -0
  45. package/lib/render-cache.js +48 -0
  46. package/lib/render.js +157 -0
  47. package/lib/templates.js +149 -0
  48. package/lib/util.js +66 -0
  49. package/lib/view-counts.js +59 -0
  50. package/lib/zip.js +192 -0
  51. package/logger.js +16 -0
  52. package/mailer.js +34 -0
  53. package/mcp/constants.js +16 -0
  54. package/mcp/resources.js +74 -0
  55. package/mcp/server.js +43 -0
  56. package/mcp/tools-categories.js +56 -0
  57. package/mcp/tools-content-templates.js +59 -0
  58. package/mcp/tools-files.js +245 -0
  59. package/mcp/tools-tags.js +41 -0
  60. package/mcp/tools-versions.js +57 -0
  61. package/mcp/transport.js +183 -0
  62. package/mcp/util.js +63 -0
  63. package/mcp-server.js +20 -0
  64. package/migrations/001_init_schema.js +25 -0
  65. package/migrations/002_add_share_key.js +33 -0
  66. package/migrations/003_add_roles_and_tokens.js +28 -0
  67. package/migrations/004_add_version_history.js +32 -0
  68. package/migrations/005_tags_starred_categories.js +49 -0
  69. package/migrations/006_zip_bundle.js +17 -0
  70. package/migrations/007_add_file_type_uploaded_by_indexes.js +7 -0
  71. package/migrations/008_add_fts5.js +6 -0
  72. package/migrations/009_add_link_visits.js +20 -0
  73. package/migrations/010_add_templates_system.js +34 -0
  74. package/migrations/011_content_templates.js +233 -0
  75. package/migrations/012_add_email_and_verification.js +35 -0
  76. package/migrations/013_add_token_encrypted.js +14 -0
  77. package/migrations.js +65 -0
  78. package/package.json +63 -0
  79. package/public/css/style.css +2915 -0
  80. package/public/index.html +855 -0
  81. package/public/js/api.js +22 -0
  82. package/public/js/app.js +94 -0
  83. package/public/js/components/dialog.js +106 -0
  84. package/public/js/components/toast.js +13 -0
  85. package/public/js/pages/content-templates.js +330 -0
  86. package/public/js/pages/home.js +1903 -0
  87. package/public/js/pages/landing.js +158 -0
  88. package/public/js/pages/login.js +175 -0
  89. package/public/js/pages/preview.js +713 -0
  90. package/public/js/theme.js +44 -0
  91. package/public/js/utils.js +67 -0
  92. package/routes/admin.js +136 -0
  93. package/routes/auth.js +365 -0
  94. package/routes/categories.js +90 -0
  95. package/routes/content-templates.js +215 -0
  96. package/routes/files/_shared.js +112 -0
  97. package/routes/files/associations.js +94 -0
  98. package/routes/files/crud.js +139 -0
  99. package/routes/files/detail-serve.js +178 -0
  100. package/routes/files/index.js +38 -0
  101. package/routes/files/list.js +200 -0
  102. package/routes/files/overwrite.js +114 -0
  103. package/routes/files/upload.js +204 -0
  104. package/routes/files/versions.js +166 -0
  105. package/routes/files.js +16 -0
  106. package/routes/skills.js +93 -0
  107. package/routes/tags.js +65 -0
  108. package/routes/tokens.js +110 -0
  109. package/routes/users.js +120 -0
  110. package/server.js +372 -0
  111. package/skills/jpage-content-template/SKILL.md +98 -0
  112. package/skills/jpage-upload/SKILL.md +247 -0
  113. package/skills-registry.js +135 -0
  114. package/templates/academic.html +41 -0
  115. package/templates/dark-pro.html +41 -0
  116. package/templates/default.html +56 -0
  117. package/templates/github.html +67 -0
  118. package/test/browser-harness.js +125 -0
  119. package/test/dispatch-bench.js +74 -0
  120. package/test/helpers/setup.js +45 -0
  121. package/test/integration/admin.test.js +108 -0
  122. package/test/integration/auth.test.js +93 -0
  123. package/test/integration/categories.test.js +103 -0
  124. package/test/integration/cli.test.js +310 -0
  125. package/test/integration/content-templates.test.js +147 -0
  126. package/test/integration/files-security.test.js +248 -0
  127. package/test/integration/files.test.js +139 -0
  128. package/test/integration/share.test.js +79 -0
  129. package/test/integration/skills.test.js +104 -0
  130. package/test/integration/tags.test.js +84 -0
  131. package/test/integration/tokens.test.js +89 -0
  132. package/test/integration/users.test.js +138 -0
  133. package/test/mcp-harness.js +152 -0
  134. package/test/perf-bench.js +108 -0
  135. package/test/perf-harness.js +198 -0
  136. package/test/run-server.sh +15 -0
  137. package/test/unit/cli-args.test.js +88 -0
  138. package/test/unit/cli-config.test.js +89 -0
  139. package/test/unit/crypto.test.js +100 -0
  140. package/test/unit/fts.test.js +52 -0
  141. package/test/unit/render-cache.test.js +76 -0
  142. package/test/unit/util.test.js +81 -0
  143. package/test/unit/zip.test.js +164 -0
package/README_EN.md ADDED
@@ -0,0 +1,399 @@
1
+ # jpage
2
+
3
+ > Drop a file, get a page — instantly.
4
+
5
+ [![CI](https://github.com/code2rich/jpage/actions/workflows/ci.yml/badge.svg)](https://github.com/code2rich/jpage/actions/workflows/ci.yml)
6
+
7
+ **[>>> View Product Introduction <<<](https://jpage.cn/)**
8
+
9
+ **jpage** is a zero-config HTML / Markdown instant preview and sharing tool. Drop in a document and instantly get a clean online page — no deployment pipeline, no server knowledge required. Especially great for one-click sharing of AI-generated content.
10
+
11
+ ---
12
+
13
+ ## Features
14
+
15
+ ### Core
16
+
17
+ - **Instant Preview** — Upload HTML or Markdown files, get a rendered online page in seconds
18
+ - **Enhanced Markdown Rendering** — Syntax highlighting (highlight.js), math formulas (KaTeX), Mermaid diagrams, with automatic dark/light theme switching
19
+ - **Source View** — Toggle between rendered and source modes for easy comparison
20
+ - **Short Links** — Each file gets an auto-generated 8-character short link (`/s/xxxxxxxx`)
21
+ - **File Management** — Rename, delete, download, toggle public/private — simple and intuitive
22
+ - **Drag & Drop Upload** — Click or drag to upload, up to 50MB per file
23
+ - **Version History** — Overwrite uploads automatically preserve previous versions; rollback anytime
24
+ - **Responsive Design** — Adapts to desktop and mobile; dark mode follows system preference
25
+
26
+ ### Organization
27
+
28
+ - **Tags** — Tag files for multi-dimensional categorization and search
29
+ - **Categories** — Organize files into categories for clear hierarchy
30
+ - **Favorites** — One-click bookmark for quick access to frequently used files
31
+
32
+ ### Security & Permissions
33
+
34
+ - **Multi-user Support** — Admin can create and manage multiple users; regular users can only access their own files and public files
35
+ - **Open Registration** — Enable self-service registration via `ALLOW_REGISTRATION=true`, with optional SMTP email verification
36
+ - **Session Auth** — Cookie + bcrypt password hashing
37
+ - **API Tokens** — Each user can create multiple API tokens for scripts and AI tools
38
+ - **Public/Private Files** — Toggle visibility on upload; private files are only accessible to the owner and admin
39
+ - **Rate Limiting** — Login and upload endpoints are rate-limited to prevent brute force and abuse
40
+
41
+ ### AI Integration
42
+
43
+ - **MCP Protocol** — Built-in MCP Streamable HTTP endpoint for direct AI tool integration
44
+ - **Skills Management** — Auto-discovers Claude Code/Desktop skill packs in the `skills/` directory
45
+ - **JSON Upload API** — `/api/files/upload-json` for programmatic uploads, ideal for AI workflows
46
+
47
+ ### Deployment
48
+
49
+ - **Zero-Dependency Runtime** — Single container startup with built-in SQLite storage
50
+ - **Docker One-Click Deploy** — Multi-stage build, env var configuration, volume persistence
51
+ - **Database Migrations** — Automatic schema migration on startup; no manual upgrades needed
52
+
53
+ ## Tech Stack
54
+
55
+ - **Backend**: Node.js + Express + express-session (SQLite session store), domain-split Router architecture (routes/ + lib/ shared layer)
56
+ - **Database**: SQLite3 (zero-config, auto-migration)
57
+ - **Frontend**: Vanilla JavaScript (no framework dependencies)
58
+ - **Rendering**: marked.js + highlight.js + KaTeX + Mermaid
59
+ - **Security**: helmet + layered CSP (strict policy for admin UI, iframe sandbox isolation + content-tiered CSP for render pages)
60
+ - **Protocol**: MCP Streamable HTTP (@modelcontextprotocol/sdk)
61
+ - **Testing**: node:test + supertest (unit + integration), GitHub Actions CI
62
+ - **Container**: Docker / Docker Compose
63
+
64
+ ## Quick Start
65
+
66
+ ### Docker Deploy (Recommended)
67
+
68
+ ```bash
69
+ git clone https://github.com/code2rich/jpage.git
70
+ cd jpage
71
+ cp .env.example .env # Edit .env with ADMIN_PASSWORD and SESSION_SECRET
72
+ docker-compose up -d
73
+ ```
74
+
75
+ Visit http://localhost:8858 — you'll be redirected to the login page.
76
+
77
+ ### Local Development
78
+
79
+ ```bash
80
+ npm install
81
+ ADMIN_USER=admin ADMIN_PASSWORD=test1234 SESSION_SECRET=dev-secret npm start
82
+ ```
83
+
84
+ Development mode (hot reload):
85
+
86
+ ```bash
87
+ npm run dev
88
+ ```
89
+
90
+ ### Development & Testing
91
+
92
+ ```bash
93
+ npm test # Unit + integration tests (node:test + supertest)
94
+ npm run test:unit # Unit tests only
95
+ npm run build # Build frontend bundle (esbuild → public/dist)
96
+ ```
97
+
98
+ End-to-end / benchmarks (start the server first with `npm start`):
99
+
100
+ ```bash
101
+ node test/perf-harness.js 8858 # Core e2e (login/upload/render/short-link/tags)
102
+ node test/mcp-harness.js 8858 # MCP endpoint
103
+ node test/perf-bench.js 8858 # Render/list/cache latency benchmarks
104
+ ```
105
+
106
+ ## Auth & Security
107
+
108
+ jpage supports a multi-user system. Admin manages all users and files; regular users can only access their own files and public files. Share links (`/api/files/:id/render`, `/s/:key`, download, source) are anonymously accessible when the file is marked public. Uncheck "Public access" on upload to make the file visible only to the owner and admin.
109
+
110
+ **Content Security (CSP)**: Hardened via helmet + layered policies — the admin UI gets a strict CSP (same-origin scripts only); user-content render pages are isolated by iframe sandbox (without `allow-same-origin`, blocking access to the parent window). Markdown pages use a strict CSP (inline mermaid init script whitelisted via nonce), while HTML pages use a relaxed CSP + sandbox fallback (user HTML often contains legitimate scripts).
111
+
112
+ ### Authentication Methods
113
+
114
+ API and MCP endpoints support three authentication methods:
115
+
116
+ 1. **Session Cookie** — `jpage.sid` cookie after login, for browser access
117
+ 2. **API Token** — User-created `jp_` prefixed tokens, for script integration
118
+ 3. **MCP Token** — `MCP_TOKEN` environment variable, for AI tool connections (backward compatible)
119
+
120
+ ### Environment Variables
121
+
122
+ | Variable | Required | Description |
123
+ |---|---|---|
124
+ | `ADMIN_USER` | No | Admin username for first startup when users table is empty; defaults to `admin` |
125
+ | `ADMIN_PASSWORD` | No | Admin password for first startup (≥8 chars); if empty, a random 16-char password is generated and printed to startup logs |
126
+ | `SESSION_SECRET` | Prod | Encrypts session cookies; in dev mode a temporary key is auto-generated (lost on restart) |
127
+ | `NODE_ENV` | No | When `production`, cookies are sent only over HTTPS; missing SESSION_SECRET will refuse to start |
128
+ | `PORT` | No | Default `8858` |
129
+ | `MCP_TOKEN` | No | Global Bearer token for the `/mcp` endpoint (backward compatible); when unset, `/mcp` is still accessible via a user-level API token (`jp_` prefix) |
130
+ | `ALLOW_REGISTRATION` | No | Set to `true` to enable self-service registration; defaults to off (admin-only user creation) |
131
+ | `SMTP_HOST` | No | SMTP server address (e.g. `smtp.qq.com`); enables email verification when configured |
132
+ | `SMTP_PORT` | No | SMTP port (e.g. `465`) |
133
+ | `SMTP_SECURE` | No | Use SSL (`true`/`false`) |
134
+ | `SMTP_USER` | No | SMTP login username |
135
+ | `SMTP_PASS` | No | SMTP login password or authorization code |
136
+ | `SMTP_FROM` | No | Sender address (e.g. `"jpage <user@example.com>"`) |
137
+ | `APP_URL` | No | External app URL used to build verification links (e.g. `https://jpage.cn`) |
138
+
139
+ If both `ADMIN_USER` and `ADMIN_PASSWORD` are left empty, the startup log will output:
140
+
141
+ ```
142
+ [jpage] Created initial admin: admin
143
+ [jpage] Initial password (save this): 7Hk2mN9pq4rTv8wX
144
+ [jpage] ⚠️ Please change the password after first login
145
+ ```
146
+
147
+ Copy the password from the log to log in.
148
+
149
+ Recommended `SESSION_SECRET` generation:
150
+ ```bash
151
+ node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
152
+ ```
153
+
154
+ ### Reset or Change Password
155
+
156
+ All users can change their password in Settings after login. Admin can reset other users' passwords in User Management.
157
+
158
+ Manual method (SQLite command line):
159
+
160
+ ```bash
161
+ node -e "console.log(require('bcryptjs').hashSync('new-password', 10))"
162
+ sqlite3 data/database.sqlite "UPDATE users SET password_hash='<hash-from-above>' WHERE username='admin';"
163
+ ```
164
+
165
+ ## Project Structure
166
+
167
+ ```
168
+ jpage/
169
+ ├── server.js # Entry: app assembly + middleware + startup orchestration (logic split out)
170
+ ├── routes/ # Domain-split Express Routers
171
+ │ ├── auth.js # Login/register/email verification
172
+ │ ├── users.js # User management (admin)
173
+ │ ├── tokens.js # API tokens
174
+ │ ├── files.js # File CRUD/upload/render/versions/tags/star/stats
175
+ │ ├── tags.js # Tags
176
+ │ ├── categories.js # Categories + template metadata
177
+ │ ├── content-templates.js # Content template marketplace
178
+ │ ├── admin.js # Backup export/import/stats
179
+ │ └── skills.js # Skills + MCP config
180
+ ├── lib/ # Shared layer (reused by routes)
181
+ │ ├── db.js # SQLite access (dbRun/dbGet/dbAll + PRAGMA)
182
+ │ ├── paths.js # Data/upload dir constants
183
+ │ ├── util.js # now/shareKey/clientIp/decodeFilename pure helpers
184
+ │ ├── csp.js # Layered CSP policies + nonce
185
+ │ ├── auth-state.js # Shared adminUserId state
186
+ │ ├── templates.js # Template system + marked/hljs/KaTeX pipeline
187
+ │ ├── render.js # File → HTML rendering (with CSP headers)
188
+ │ ├── render-cache.js # Render result LRU cache
189
+ │ ├── fts.js # FTS5 full-text index
190
+ │ ├── categories.js # Category name in-memory cache
191
+ │ ├── view-counts.js # Buffered view-count batched flush
192
+ │ ├── zip.js # ZIP upload (security validation/extraction/classification)
193
+ │ ├── dispatch.js # MCP in-process request dispatch (bypass TCP self-call)
194
+ │ └── middleware/ # Auth + file-loading middleware
195
+ ├── logger.js # Structured JSON Lines logger
196
+ ├── mailer.js # SMTP mail (verification codes/links)
197
+ ├── mcp-server.js # MCP Streamable HTTP endpoint (/mcp)
198
+ ├── migrations.js # Database migration runner
199
+ ├── migrations/ # Sequential schema migrations (001-012)
200
+ ├── skills-registry.js # Scans skills/ dir, provides skill list/details/zip packaging
201
+ ├── templates/ # Markdown render style templates (default/github/academic/dark-pro)
202
+ ├── package.json
203
+ ├── build.js # esbuild bundles frontend → public/dist
204
+ ├── Dockerfile
205
+ ├── docker-compose.yml
206
+ ├── .env.example # Environment variable template
207
+ ├── .mcp.json # MCP client config example
208
+ ├── docs/
209
+ │ ├── api.md # Complete REST API reference
210
+ │ └── design/ # Design documents
211
+ ├── skills/
212
+ │ └── jpage-upload/ # Claude Code / Desktop skill
213
+ │ └── SKILL.md
214
+ ├── test/ # Unit + integration tests (node:test + supertest) + e2e harness
215
+ ├── data/ # SQLite databases, uploaded files & sessions (auto-created)
216
+ └── public/ # Frontend static assets
217
+ ├── index.html
218
+ ├── css/style.css
219
+ ├── js/ # Page-split ES modules
220
+ └── dist/ # Build output (npm run build, git-ignored)
221
+ ```
222
+
223
+ ## REST API
224
+
225
+ Port `8858` (overridable via `PORT`). All write endpoints require login or Bearer token.
226
+
227
+ ### Auth
228
+
229
+ | Endpoint | Method | Description |
230
+ |---|---|---|
231
+ | `/api/auth/me` | GET | Current login info |
232
+ | `/api/auth/login` | POST | Login (`{username, password}`) |
233
+ | `/api/auth/register` | POST | Register (requires `ALLOW_REGISTRATION=true`) |
234
+ | `/api/auth/logout` | POST | Logout |
235
+ | `/api/auth/change-password` | POST | Change password (all users) |
236
+
237
+ ### User Management (Admin only)
238
+
239
+ | Endpoint | Method | Description |
240
+ |---|---|---|
241
+ | `/api/users` | GET | List all users |
242
+ | `/api/users` | POST | Create user |
243
+ | `/api/users/:id` | PUT | Update role or reset password |
244
+ | `/api/users/:id` | DELETE | Delete user; files transfer to admin |
245
+
246
+ ### API Tokens
247
+
248
+ | Endpoint | Method | Description |
249
+ |---|---|---|
250
+ | `/api/tokens` | GET | List own tokens |
251
+ | `/api/tokens` | POST | Create token (plaintext returned once only) |
252
+ | `/api/tokens/:id` | DELETE | Delete token |
253
+
254
+ ### File Management
255
+
256
+ | Endpoint | Method | Description |
257
+ |---|---|---|
258
+ | `/api/files` | GET | List files (admin sees all; users see own + public) |
259
+ | `/api/files/upload` | POST | Multipart upload |
260
+ | `/api/files/upload-json` | POST | JSON upload (`{name, content, isPublic?}`) |
261
+ | `/api/files/:id` | PUT | Rename or toggle public/private |
262
+ | `/api/files/:id` | DELETE | Delete file |
263
+ | `/api/files/:id/content` | GET | Return raw text |
264
+ | `/api/files/:id/render` | GET | Return rendered HTML |
265
+ | `/api/files/:id/download` | GET | Stream download file |
266
+ | `/s/:key` | GET | Short link page render |
267
+
268
+ ### Tags
269
+
270
+ | Endpoint | Method | Description |
271
+ |---|---|---|
272
+ | `/api/tags` | GET | List all tags (with file_count) |
273
+ | `/api/tags` | POST | Create tag |
274
+ | `/api/tags/:id` | DELETE | Delete tag |
275
+ | `/api/files/:id/tags` | PUT | Replace file's tag list |
276
+
277
+ ### Favorites
278
+
279
+ | Endpoint | Method | Description |
280
+ |---|---|---|
281
+ | `/api/files/:id/star` | POST | Favorite file |
282
+ | `/api/files/:id/star` | DELETE | Unfavorite file |
283
+
284
+ ### Categories
285
+
286
+ | Endpoint | Method | Description |
287
+ |---|---|---|
288
+ | `/api/categories` | GET | List categories (with file_count) |
289
+ | `/api/categories` | POST | Create category |
290
+ | `/api/categories/:id` | PUT | Rename category |
291
+ | `/api/categories/:id` | DELETE | Delete category |
292
+ | `/api/files/:id/category` | PUT | Set file category |
293
+
294
+ ### Skills
295
+
296
+ | Endpoint | Method | Description |
297
+ |---|---|---|
298
+ | `/api/skills` | GET | List installed skill packs |
299
+ | `/api/skills/:name` | GET | Skill details |
300
+ | `/api/skills/:name/download` | GET | ZIP download of skill directory |
301
+
302
+ Full API documentation: [docs/api.md](docs/api.md).
303
+
304
+ ## MCP / AI Integration
305
+
306
+ jpage includes a built-in [MCP Streamable HTTP](https://modelcontextprotocol.io) endpoint, enabling AI tools like Claude Code and Claude Desktop to directly upload and manage files.
307
+
308
+ ### Enable
309
+
310
+ Set the `MCP_TOKEN` environment variable:
311
+
312
+ ```bash
313
+ MCP_TOKEN=your-secret-token
314
+ ```
315
+
316
+ ### Client Configuration
317
+
318
+ **Claude Code** — Add to your project's `.mcp.json` or merge into `~/.claude.json`:
319
+
320
+ ```json
321
+ {
322
+ "mcpServers": {
323
+ "jpage": {
324
+ "type": "http",
325
+ "url": "http://localhost:8858/mcp",
326
+ "headers": {
327
+ "Authorization": "Bearer ${env.MCP_TOKEN}"
328
+ }
329
+ }
330
+ }
331
+ }
332
+ ```
333
+
334
+ **Claude Desktop** — Merge the `mcpServers` block into `~/Library/Application Support/Claude/claude_desktop_config.json`.
335
+
336
+ ### Capabilities
337
+
338
+ **Tools** (15):
339
+
340
+ | Tool | Purpose |
341
+ |---|---|
342
+ | `upload_file` | Upload HTML or Markdown, return preview link |
343
+ | `list_files` | List all files |
344
+ | `get_file_content` | Read file content |
345
+ | `get_file_url` | Get file preview URL |
346
+ | `rename_file` | Rename file |
347
+ | `delete_file` | Delete file |
348
+ | `list_file_versions` | View file version history |
349
+ | `restore_file_version` | Rollback to specified version |
350
+ | `list_tags` | List tags |
351
+ | `add_tags_to_file` | Add tags to file |
352
+ | `star_file` | Favorite file |
353
+ | `unstar_file` | Unfavorite file |
354
+ | `list_categories` | List categories |
355
+ | `create_category` | Create category |
356
+ | `set_file_category` | Set file category |
357
+
358
+ **Resources** (2):
359
+
360
+ | URI | Description |
361
+ |---|---|
362
+ | `jpage://files` | All file metadata (JSON list) |
363
+ | `jpage://file/{id}` | Single file content (≤ 256KB) |
364
+
365
+ ### Companion Skill
366
+
367
+ The repo includes `skills/jpage-upload/SKILL.md`, a ready-to-use skill for Claude Code / Desktop. Once installed, AI-generated HTML, Markdown, reports, and visualizations are automatically uploaded to jpage with a preview link.
368
+
369
+ ```bash
370
+ ln -s "$(pwd)/skills/jpage-upload" ~/.claude/skills/jpage-upload
371
+ ```
372
+
373
+ ### Web Management
374
+
375
+ After login, the homepage bottom section shows an **AI Skills** block where you can view details and download zip packages. Add new skills by creating `skills/<name>/SKILL.md` with YAML frontmatter — the service auto-discovers them on restart.
376
+
377
+ ### Debug
378
+
379
+ ```bash
380
+ npx -y @modelcontextprotocol/inspector http://localhost:8858/mcp
381
+ ```
382
+
383
+ ## Use Cases
384
+
385
+ - **AI Content Sharing** — One-click upload of HTML reports and visualizations generated by Claude Code, Cursor, and similar tools
386
+ - **Technical Documentation** — Markdown notes, meeting minutes, project reports with auto-rendered code highlighting, math formulas, and diagrams
387
+ - **Static Page Hosting** — Single-page HTML demos, prototypes, landing pages — no server configuration needed
388
+ - **Temporary File Sharing** — Any HTML/Markdown file, drag and drop to get a link — no account required
389
+ - **Version Management** — Iterative document updates automatically preserve history; rollback anytime
390
+
391
+ ## Why
392
+
393
+ Existing solutions are either too heavy (requiring server setup, domains, CI) or too closed (locked to specific platforms).
394
+
395
+ jpage does one thing: make static content sharing simple again. Drop a file, get a link. An optional multi-user system exists, but the default is zero-friction — drop a file to get a link, share public files anonymously without registering.
396
+
397
+ ## License
398
+
399
+ MIT
package/bin/args.js ADDED
@@ -0,0 +1,64 @@
1
+ // 零依赖 argv 解析器。
2
+ //
3
+ // 约定:
4
+ // - 以 `--` 开头的 token 是选项(长选项);支持 `--key value` 和 `--key=value` 两种形式
5
+ // - 不支持单字母组合短选项(-a),避免歧义;单字母需求用长选项
6
+ // - 布尔标志:出现即为 true(如 --public)。需要显式值时用 --key=value
7
+ // - 其余 token 是位置参数(positional)
8
+ // - 第一个位置参数视作命令(cmd),第二个视作子命令(sub)
9
+ //
10
+ // 返回:{ cmd, sub, opts, positional }
11
+ // cmd 第一个位置参数(字符串,无则 null)
12
+ // sub 第二个位置参数(字符串,无则 null)
13
+ // opts 选项对象(键名去掉 -- 前缀,值默认 true,否则字符串)
14
+ // positional 全部位置参数数组(含 cmd/sub)
15
+ //
16
+ // 设计目标:够用、可预测、无运行时依赖。commander 级别的复杂场景(子命令嵌套、
17
+ // 变长参数类型校验)超出 CLI 需求,故不引入。
18
+
19
+ function parse(argv) {
20
+ const opts = {};
21
+ const positional = [];
22
+ let cmd = null;
23
+ let sub = null;
24
+
25
+ for (let i = 0; i < argv.length; i++) {
26
+ const tok = argv[i];
27
+
28
+ if (tok === '--') {
29
+ // 之后的全部当位置参数
30
+ for (let j = i + 1; j < argv.length; j++) positional.push(argv[j]);
31
+ break;
32
+ }
33
+
34
+ if (tok.startsWith('--')) {
35
+ const eq = tok.indexOf('=');
36
+ if (eq > 1) {
37
+ // --key=value
38
+ const key = tok.slice(2, eq);
39
+ const value = tok.slice(eq + 1);
40
+ opts[key] = value;
41
+ } else {
42
+ const key = tok.slice(2);
43
+ const next = argv[i + 1];
44
+ // 下一个 token 不是选项(且存在)→ 当作值;否则当布尔标志
45
+ if (next !== undefined && !next.startsWith('--')) {
46
+ opts[key] = next;
47
+ i++;
48
+ } else {
49
+ opts[key] = true;
50
+ }
51
+ }
52
+ continue;
53
+ }
54
+
55
+ positional.push(tok);
56
+ }
57
+
58
+ if (positional.length >= 1) cmd = positional[0];
59
+ if (positional.length >= 2) sub = positional[1];
60
+
61
+ return { cmd, sub, opts, positional };
62
+ }
63
+
64
+ module.exports = { parse };
package/bin/client.js ADDED
@@ -0,0 +1,93 @@
1
+ // CLI HTTP 客户端:基于全局 fetch 的薄封装,统一鉴权与错误处理。
2
+ //
3
+ // 接口形:{ get, post, postForm, put, del },path 以 /api/... 开头。
4
+ // 非预期 JSON(如下载二进制)用 raw(path, init) 拿原始 Response。
5
+ //
6
+ // 测试注入:createClient 接受可选 fetchImpl 参数。集成测试注入一个打到 supertest
7
+ // app 的 fetch shim,避免起真实 TCP 端口(项目既有测试都是 in-process 模式)。
8
+ //
9
+ // 与 lib/dispatch.js 的区别:dispatch 是「服务端进程内直调」(绕过 TCP),
10
+ // 本文件是「客户端经网络调」(真 fetch),两者职责正交。
11
+
12
+ // 请求失败的统一错误类型。带 status + 可读 message,命令层据此决定退出码与提示。
13
+ class HttpError extends Error {
14
+ constructor(status, message, body) {
15
+ super(message);
16
+ this.name = 'HttpError';
17
+ this.status = status;
18
+ this.body = body;
19
+ }
20
+ }
21
+
22
+ /**
23
+ * 创建 API 客户端。
24
+ * @param {object} opts
25
+ * @param {string} opts.base - 服务地址(如 http://localhost:8858),无尾斜杠
26
+ * @param {string} [opts.token] - Bearer token;为空时不发 Authorization 头
27
+ * @param {function} [opts.fetchImpl] - 可选 fetch 实现(测试注入)
28
+ * @returns {object}
29
+ */
30
+ function createClient({ base, token, fetchImpl } = {}) {
31
+ const f = fetchImpl || fetch;
32
+
33
+ async function raw(path, init = {}) {
34
+ const url = base + path;
35
+ const headers = { ...(init.headers || {}) };
36
+ if (token) headers.Authorization = 'Bearer ' + token;
37
+
38
+ let res;
39
+ try {
40
+ res = await f(url, { ...init, headers });
41
+ } catch (e) {
42
+ // 网络层错误(ECONNREFUSED / DNS / 超时)→ 包装成可识别错误
43
+ const err = new Error(`无法连接到 ${base}:${e.message}`);
44
+ err.name = 'NetworkError';
45
+ err.cause = e;
46
+ throw err;
47
+ }
48
+ return res;
49
+ }
50
+
51
+ // JSON 请求:发 JSON body,期望 JSON 响应。失败抛 HttpError。
52
+ async function json(method, path, body) {
53
+ const init = { method };
54
+ if (body !== undefined) {
55
+ init.headers = { 'content-type': 'application/json' };
56
+ init.body = JSON.stringify(body);
57
+ }
58
+ const res = await raw(path, init);
59
+ return parseJson(res, method, path);
60
+ }
61
+
62
+ async function parseJson(res, method, path) {
63
+ const text = await res.text();
64
+ let data = null;
65
+ if (text) {
66
+ try {
67
+ data = JSON.parse(text);
68
+ } catch {
69
+ data = { raw: text };
70
+ }
71
+ }
72
+ if (res.status < 200 || res.status >= 300) {
73
+ const msg = (data && data.error) || `HTTP ${res.status}`;
74
+ throw new HttpError(res.status, `REST ${method} ${path} -> ${res.status} ${msg}`, data);
75
+ }
76
+ return data;
77
+ }
78
+
79
+ return {
80
+ raw,
81
+ get: (p) => json('GET', p),
82
+ post: (p, body) => json('POST', p, body),
83
+ put: (p, body) => json('PUT', p, body),
84
+ del: (p) => json('DELETE', p),
85
+ // multipart:调用方传已构造好的 FormData
86
+ postForm: async (p, formData) => {
87
+ const res = await raw(p, { method: 'POST', body: formData });
88
+ return parseJson(res, 'POST', p);
89
+ },
90
+ };
91
+ }
92
+
93
+ module.exports = { createClient, HttpError };
@@ -0,0 +1,54 @@
1
+ // 命令间共享的输出辅助函数。
2
+ //
3
+ // 设计:命令模块只做「调 API + 格式化输出」,错误码由 bin/jpage.js 统一处理。
4
+ // 这里集中放格式化工具(人类可读的字节/时间、表格),避免命令文件互相重复。
5
+ //
6
+ // I/O 注入:out()/err() 默认写 process.stdout/stderr,但 run() 可通过 setIo()
7
+ // 注入自定义流(测试用),这样测试捕获输出时不必 monkeypatch process.stdout
8
+ // (那会破坏 node:test 自身的 TAP 输出)。
9
+
10
+ let _stdout = process.stdout;
11
+ let _stderr = process.stderr;
12
+
13
+ function setIo({ stdout, stderr } = {}) {
14
+ if (stdout) _stdout = stdout;
15
+ if (stderr) _stderr = stderr;
16
+ }
17
+
18
+ function resetIo() {
19
+ _stdout = process.stdout;
20
+ _stderr = process.stderr;
21
+ }
22
+
23
+ // 写一行到 stdout(自动补换行)。
24
+ function out(s) {
25
+ _stdout.write(s);
26
+ }
27
+ // 写一行到 stderr。
28
+ function err(s) {
29
+ _stderr.write(s);
30
+ }
31
+
32
+ // 字节 → 人类可读(B / KB / MB)。
33
+ function formatSize(bytes) {
34
+ if (bytes == null) return '-';
35
+ const n = Number(bytes);
36
+ if (!isFinite(n)) return '-';
37
+ if (n < 1024) return n + 'B';
38
+ if (n < 1024 * 1024) return (n / 1024).toFixed(1) + 'K';
39
+ return (n / 1024 / 1024).toFixed(1) + 'M';
40
+ }
41
+
42
+ // ISO/SQLite 时间 → 简短显示。原样返回无法解析的输入。
43
+ function formatTime(t) {
44
+ if (!t) return '-';
45
+ return String(t).replace('T', ' ').replace(/\.\d+$/, '');
46
+ }
47
+
48
+ // 从文件元数据拼预览 URL:优先 /s/:key,无私有短链时返回 null。
49
+ function shareUrl(base, file) {
50
+ if (!file || !file.share_key) return null;
51
+ return `${base}/s/${file.share_key}`;
52
+ }
53
+
54
+ module.exports = { setIo, resetIo, out, err, formatSize, formatTime, shareUrl };
@@ -0,0 +1,23 @@
1
+ // cat 命令:输出文件原始内容到 stdout。
2
+ // 后端:GET /api/files/:id/content(owner/admin 限定)。
3
+ // bundle(is_bundle=1)不支持取「整个内容」,后端会返回入口文件内容,此处原样输出。
4
+
5
+ const { out } = require('./_shared');
6
+
7
+ async function run(client, args) {
8
+ const id = args.sub;
9
+ if (!id) {
10
+ const e = new Error('用法:jpage cat <id>');
11
+ e.name = 'UsageError';
12
+ throw e;
13
+ }
14
+ const data = await client.get(`/api/files/${id}/content`);
15
+ if (typeof data.content === 'string') {
16
+ out(data.content);
17
+ if (!data.content.endsWith('\n')) out('\n');
18
+ } else {
19
+ out(JSON.stringify(data, null, 2) + '\n');
20
+ }
21
+ }
22
+
23
+ module.exports = { run };
@@ -0,0 +1,44 @@
1
+ // ls 命令:列出文件。
2
+ // 后端:GET /api/files(支持 page/limit/sort/order/keyword/category/tag)。
3
+
4
+ const { formatSize, formatTime, out } = require('./_shared');
5
+
6
+ async function run(client, args) {
7
+ const o = args.opts;
8
+ const params = new URLSearchParams();
9
+ if (o.page) params.set('page', o.page);
10
+ if (o.limit) params.set('limit', o.limit);
11
+ if (o.sort) params.set('sort', o.sort);
12
+ if (o.order) params.set('order', o.order);
13
+ if (o.kw || o.keyword) params.set('keyword', o.kw || o.keyword);
14
+ if (o.cat || o.category) params.set('category', o.cat || o.category);
15
+ if (o.tag) params.set('tag', o.tag);
16
+ const qs = params.toString();
17
+
18
+ const data = await client.get('/api/files' + (qs ? '?' + qs : ''));
19
+ const files = data.files || [];
20
+ const pg = data.pagination || {};
21
+
22
+ if (files.length === 0) {
23
+ out('(无文件)\n');
24
+ return;
25
+ }
26
+
27
+ // 对齐表格:id / 类型 / 公开 / 大小 / 更新时间 / 文件名 / 标签
28
+ for (const f of files) {
29
+ const pub = f.is_public ? 'pub' : 'pri';
30
+ const tags = (f.tags || []).map((t) => t.name).join(',');
31
+ const bundle = f.is_bundle ? ' 📦' : '';
32
+ out(
33
+ `#${f.id} [${f.file_type || '?'} ${pub}] ${formatSize(f.size).padEnd(7)} ` +
34
+ `${formatTime(f.updated_at)} ${f.original_name}${bundle}` +
35
+ (tags ? ` {${tags}}` : '') +
36
+ '\n'
37
+ );
38
+ }
39
+ out(
40
+ `\n第 ${pg.page || 1} / ${pg.totalPages || 1} 页,共 ${pg.total || files.length} 个\n`
41
+ );
42
+ }
43
+
44
+ module.exports = { run };