mdpush 1.3.1 → 1.4.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/assets/SKILL.md CHANGED
@@ -1,18 +1,25 @@
1
1
  ---
2
2
  name: mdpush
3
- description: Push markdown, text, and HTML artifact files to Preview Reader / sielay server. Use when user wants to upload .md/.txt files to their docs portal OR when user wants to host/share a self-contained HTML artifact (spec, review, design, report, playground).
3
+ description: Push markdown, text, HTML, and PDF files to Preview Reader / sielay server. Use when user wants to upload .md/.txt/.pdf files to their docs portal OR when user wants to host/share a self-contained HTML artifact (spec, review, design, report, playground).
4
+ metadata:
5
+ category: preview
6
+ example: "/mdpush design.html --type design · /mdpush report.pdf"
7
+ deps: "Config: ~/.mdpush.json con { server, token, defaultProject }. Token desde Preview Reader > API Tokens. Nunca commitear ese archivo. Script: scripts/push.sh."
8
+ installs: []
9
+ risk: none
4
10
  ---
5
11
 
6
- # mdpush — Push Markdown and HTML Artifacts to Preview Reader
12
+ # mdpush — Push Markdown, HTML, and PDF Artifacts to Preview Reader
7
13
 
8
- Push `.md`, `.txt`, and `.html` files from the current project to a Preview Reader server. HTML files are hosted at `/v/:sid` as sandboxed artifacts with an optional type tag.
14
+ Push `.md`, `.txt`, `.html`, and `.pdf` files from the current project to a Preview Reader server. HTML files are hosted at `/v/:sid` as sandboxed artifacts with an optional type tag; PDFs render in the native viewer at `/v/:sid` (uploaded as binary artifacts via `?contentType=pdf`).
9
15
 
10
16
  ## Usage
11
17
 
12
18
  ```
13
19
  /mdpush <files...> # Push to default project
14
20
  /mdpush -p <project-slug> <files...> # Push to specific project
15
- /mdpush -f <folder> <files...> # Push to subfolder within project (md/txt only)
21
+ /mdpush -f <folder> <files...> # Push to subfolder within project (md/txt/pdf)
22
+ /mdpush report.pdf # Upload a PDF to the native viewer
16
23
  /mdpush design.html --type design # Upload HTML artifact with type tag
17
24
  /mdpush spec.html report.md --type spec # Mix: HTML gets type, .md does not
18
25
  ```
@@ -181,13 +181,15 @@ for FILE in "${FILES[@]}"; do
181
181
  fi
182
182
 
183
183
  BASENAME=$(basename "$FILE")
184
- EXT="${BASENAME##*.}"
184
+ # Lowercase the extension so branch logic (html/pdf) and the whitelist are
185
+ # case-insensitive (.PDF, .HTML, .MD all normalize here).
186
+ EXT=$(printf '%s' "${BASENAME##*.}" | tr '[:upper:]' '[:lower:]')
185
187
 
186
- # Extension whitelist: md, txt, html
188
+ # Extension whitelist: md, txt, html, pdf
187
189
  case "$EXT" in
188
- md|txt|html) ;;
190
+ md|txt|html|pdf) ;;
189
191
  *)
190
- echo "SKIP: $BASENAME (unsupported extension — use .md, .txt, or .html)"
192
+ echo "SKIP: $BASENAME (unsupported extension — use .md, .txt, .html, or .pdf)"
191
193
  continue
192
194
  ;;
193
195
  esac
@@ -300,8 +302,18 @@ for FILE in "${FILES[@]}"; do
300
302
  FAILED=$((FAILED + 1))
301
303
  fi
302
304
 
303
- # --- Markdown / text path: original behavior ---
305
+ # --- Markdown / text / PDF path ---
304
306
  else
307
+ # PDF uploads to the same endpoint but must declare ?contentType=pdf (so the
308
+ # server tags it as a binary artifact, not markdown); md/txt keep the shared
309
+ # ?folder query. Folder, when given, is appended in the matching form.
310
+ if [ "$EXT" = "pdf" ]; then
311
+ NONHTML_QUERY="?contentType=pdf"
312
+ [ -n "$FOLDER" ] && NONHTML_QUERY="${NONHTML_QUERY}&folder=${ENCODED_FOLDER}"
313
+ else
314
+ NONHTML_QUERY="$FOLDER_QUERY"
315
+ fi
316
+
305
317
  t_pre_curl=$(_now_ms)
306
318
  RESPONSE=$(curl -s -w "\n%{http_code}" \
307
319
  --max-time 60 \
@@ -309,7 +321,7 @@ for FILE in "${FILES[@]}"; do
309
321
  -H "Authorization: Bearer $TOKEN" \
310
322
  -H "X-Requested-With: XMLHttpRequest" \
311
323
  -F "files=@$FILE" \
312
- "${SERVER}/api/projects/${PROJECT}/upload${FOLDER_QUERY}") || {
324
+ "${SERVER}/api/projects/${PROJECT}/upload${NONHTML_QUERY}") || {
313
325
  t_post_curl=$(_now_ms)
314
326
  t_end=$(_now_ms)
315
327
  TS=$(date -u +%Y-%m-%dT%H:%M:%S.000Z 2>/dev/null || date -u +%Y-%m-%dT%H:%M:%SZ)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mdpush",
3
- "version": "1.3.1",
3
+ "version": "1.4.0",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=20.0.0"
@@ -15,7 +15,7 @@ export async function setupCommand() {
15
15
  type: 'text',
16
16
  name: 'server',
17
17
  message: 'Server URL',
18
- initial: 'https://docs.sielay.cloud'
18
+ initial: 'https://sielay.cloud'
19
19
  })
20
20
  if (!server) { console.log(chalk.red('Setup cancelled.')); return }
21
21
 
@@ -4,6 +4,7 @@ import os from 'os'
4
4
  import chalk from 'chalk'
5
5
  import ora from 'ora'
6
6
  import { apiRequest } from '../lib/api-request.js'
7
+ import { publicRequest, resolvePublicServer } from '../lib/public-request.js'
7
8
 
8
9
  /**
9
10
  * `mdpush skill push <dir>` / `mdpush skill install <slug>` — publish a Claude
@@ -114,9 +115,21 @@ export async function skillInstallCommand(slugs, options) {
114
115
  }
115
116
 
116
117
  /** Install one skill. `?source=cli-install` marks a real install so the
117
- * server bumps install_count (detail-page manifest fetches don't). */
118
+ * server bumps install_count (detail-page manifest fetches don't).
119
+ *
120
+ * With `options.public`, fetch anonymously from /public/skills (no login/token)
121
+ * — the aitmpl-style flow. Otherwise use the authenticated /api/skills path. */
118
122
  async function installOneSkill(slug, options) {
119
- const manifestRes = await apiRequest(`/api/skills/${slug}/manifest?source=cli-install`)
123
+ const anon = !!options.public
124
+ const server = anon ? resolvePublicServer(options.server) : null
125
+ const fetchJson = anon
126
+ ? p => publicRequest(server, `/public/skills/${slug}${p}`)
127
+ : p => apiRequest(`/api/skills/${slug}${p}`)
128
+ const fetchFile = anon
129
+ ? encoded => publicRequest(server, `/public/skills/${slug}/download/${encoded}`)
130
+ : encoded => apiRequest(`/api/projects/${slug}/download/${encoded}`)
131
+
132
+ const manifestRes = await fetchJson('/manifest?source=cli-install')
120
133
  const manifest = await manifestRes.json()
121
134
 
122
135
  const dest = path.resolve(options.dest || path.join(os.homedir(), '.claude', 'skills', slug))
@@ -131,7 +144,7 @@ async function installOneSkill(slug, options) {
131
144
  const target = path.join(dest, f.path)
132
145
  if (!target.startsWith(dest + path.sep)) throw new Error('Unsafe path in manifest')
133
146
  const encoded = f.path.split('/').map(encodeURIComponent).join('/')
134
- const res = await apiRequest(`/api/projects/${slug}/download/${encoded}`)
147
+ const res = await fetchFile(encoded)
135
148
  fs.mkdirSync(path.dirname(target), { recursive: true })
136
149
  fs.writeFileSync(target, Buffer.from(await res.arrayBuffer()))
137
150
  if (target.endsWith('.sh')) { try { fs.chmodSync(target, 0o755) } catch {} }
package/src/index.js CHANGED
@@ -41,6 +41,8 @@ skill.command('install')
41
41
  .description('Install published skills into ~/.claude/skills/<slug>')
42
42
  .argument('<slugs...>', 'Skill slug(s) — space or comma separated (see: mdpush skill list)')
43
43
  .option('-d, --dest <path>', 'Destination directory (single skill only)')
44
+ .option('--public', 'Install anonymously from /public/skills (no login required)')
45
+ .option('-s, --server <url>', 'Server URL for --public (default: stored config or https://docs.sielay.cloud)')
44
46
  .action(skillInstallCommand)
45
47
 
46
48
  skill.command('list')
@@ -0,0 +1,29 @@
1
+ import { readConfig } from './config-store.js'
2
+
3
+ /** Default public sielay instance when neither --server nor a login config set one. */
4
+ const DEFAULT_PUBLIC_SERVER = 'https://docs.sielay.cloud'
5
+
6
+ /**
7
+ * Resolve the base server URL for anonymous (public) requests. Precedence:
8
+ * explicit --server → stored login config server → the default public host.
9
+ * No token is read or sent — these hit /public/* which needs no credential.
10
+ */
11
+ export function resolvePublicServer(optServer) {
12
+ if (optServer) return optServer.replace(/\/$/, '')
13
+ const cfg = readConfig()
14
+ return (cfg.server || DEFAULT_PUBLIC_SERVER).replace(/\/$/, '')
15
+ }
16
+
17
+ /** Unauthenticated GET against a sielay /public/* path. */
18
+ export async function publicRequest(server, path) {
19
+ const res = await fetch(`${server}${path}`)
20
+ if (!res.ok) {
21
+ // /public/* returns a uniform 404 for missing / not-public / kill-switch-off,
22
+ // so surface a single actionable message rather than guessing the cause.
23
+ if (res.status === 404) {
24
+ throw new Error('Not found (skill not public, or public skills disabled on this server)')
25
+ }
26
+ throw new Error(`HTTP ${res.status}`)
27
+ }
28
+ return res
29
+ }