@milcho0604/velog-mcp 0.4.2 → 0.6.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.ko.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # velog-mcp
2
2
 
3
3
  [![npm](https://img.shields.io/npm/v/@milcho0604/velog-mcp)](https://www.npmjs.com/package/@milcho0604/velog-mcp)
4
- [![Node](https://img.shields.io/badge/node-%3E%3D24-brightgreen)](https://nodejs.org)
4
+ [![Node](https://img.shields.io/badge/node-%3E%3D22.18-brightgreen)](https://nodejs.org)
5
5
  [![License](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
6
6
  [![Runtime deps](https://img.shields.io/badge/runtime%20deps-2-lightgrey)](package.json)
7
7
 
@@ -27,13 +27,13 @@
27
27
  있다 — 오류 없이 빈 결과를 주는 경우, 발행글을 비공개로 만드는 경우 포함.
28
28
 
29
29
  **3. 런타임 의존성 2개.** `@modelcontextprotocol/sdk` 와 `zod` 뿐이다.
30
- HTTP·테스트 러너·타입스크립트 실행은 전부 Node 24 내장을 쓴다.
30
+ HTTP 테스트 러너와 타입스크립트 실행은 전부 Node 내장을 쓴다.
31
31
 
32
32
  ---
33
33
 
34
34
  ## 설치
35
35
 
36
- **Node.js 24 이상**이 필요하다.
36
+ **Node.js 22.18 이상**이 필요하다. 실행되는 것은 컴파일된 `dist/index.js` 지만, 개발과 검증이 `.ts` 를 직접 실행하고 그게 플래그 없이 도는 첫 버전이 22.18 이다. CI 는 22.18 과 24 와 26 에서 돌린다.
37
37
 
38
38
  ### Claude Code 플러그인으로 (권장)
39
39
 
@@ -63,7 +63,7 @@ npm 에 올려뒀으니 클론할 것 없이 MCP 클라이언트가 `npx` 로
63
63
 
64
64
  ```bash
65
65
  claude mcp add velog -e VELOG_REFRESH_TOKEN=여기에_토큰 \
66
- -- npx -y @milcho0604/velog-mcp@0.4.2
66
+ -- npx -y @milcho0604/velog-mcp@0.6.0
67
67
  ```
68
68
 
69
69
  이 방식은 토큰이 클라이언트 설정 파일에 남는다. 위의 플러그인 방식은 키체인에 넣는다.
@@ -85,7 +85,7 @@ MCP 클라이언트 설정 파일(`claude_desktop_config.json`, `.mcp.json` 등)
85
85
  "mcpServers": {
86
86
  "velog": {
87
87
  "command": "npx",
88
- "args": ["-y", "@milcho0604/velog-mcp@0.4.2"],
88
+ "args": ["-y", "@milcho0604/velog-mcp@0.6.0"],
89
89
  "env": {
90
90
  "VELOG_REFRESH_TOKEN": "여기에 토큰"
91
91
  }
@@ -98,7 +98,7 @@ Claude Code CLI 라면:
98
98
 
99
99
  ```bash
100
100
  claude mcp add velog -e VELOG_REFRESH_TOKEN=여기에_토큰 \
101
- -- npx -y @milcho0604/velog-mcp@0.4.2
101
+ -- npx -y @milcho0604/velog-mcp@0.6.0
102
102
  ```
103
103
 
104
104
  로컬 체크아웃으로 돌리려면 command 를
@@ -230,6 +230,44 @@ if (count >= 10) {
230
230
  > `velog_update_draft` 는 생략하면 초기화하고, `velog_update_post` 는 유지한다.
231
231
  > 의도한 비대칭이고 이유는 [docs/tools.md](docs/tools.md) 에 있다.
232
232
 
233
+ #### 썸네일 자동 채움
234
+
235
+ `thumbnail` 을 생략하면 **본문 첫 이미지**를 썸네일로 쓴다. 목록·공유 카드가 글자만
236
+ 나오는 걸 막기 위해서다. 무엇을 넣었는지는 결과에 항상 표시하고, 후보가 여럿이면
237
+ 나머지도 함께 보여준다.
238
+
239
+ | `thumbnail` 값 | 동작 |
240
+ | --- | --- |
241
+ | 생략 | 본문 첫 이미지로 자동 설정 |
242
+ | URL | 그대로 사용 |
243
+ | `null` | **자동 채움 끄기** — 일부러 비워 두는 경우 |
244
+
245
+ 코드블록·인라인코드 안의 이미지는 후보에서 **제외**한다(예제로 적어둔 마크다운이
246
+ 썸네일이 되면 안 되므로). `velog_update_post` 는 **기존 썸네일이 있으면 덮지 않는다** —
247
+ 제목만 고쳤는데 목록 카드가 바뀌는 일이 없도록 한 것이고, 이때 `null` 은 "채우지 마라"이지
248
+ "지워라"가 아니다.
249
+
250
+ #### 시리즈 — 이름으로 한 번에
251
+
252
+ `series_name` 에 **이름**을 주면 저장 **전에** 내 시리즈에서 찾아 **같은 요청에 실어 보낸다**.
253
+ 글쓰기와 시리즈 등록이 한 번의 호출로 끝난다. id 는 사람도 AI 도 모르기 때문에 이름을 받는다.
254
+
255
+ ```
256
+ series_name: "PostgreSQL" ← 대소문자·앞뒤 공백은 무시하고 찾는다
257
+ series_id: "e53810ca-..." ← id 를 알면 이쪽이 우선
258
+ ```
259
+
260
+ ⚠️ **못 찾으면 글을 저장하지 않는다.** 조용히 시리즈 없이 저장하면 들어간 줄 알기 때문이다.
261
+ 이때 있는 시리즈 목록을 함께 알려준다.
262
+
263
+ `series_name`·`series_id` 를 둘 다 생략하면 저장 뒤 결과에 **내 시리즈 목록**을 붙여준다.
264
+ 이 조회가 실패해도 글은 이미 저장된 뒤이므로 **저장을 실패시키지 않는다**(취소도 삼킨다 —
265
+ 여기서 실패로 보고하면 재시도 때 글이 두 번 생긴다).
266
+
267
+ > ⚠️ 벨로그 API 로는 **시리즈를 만들 수 없다.** 뮤테이션에 시리즈 관련이 하나도 없고
268
+ > `WritePostInput` 도 `series_id` 만 받는다. 새 시리즈는 벨로그 웹에서 한 번 만들면
269
+ > 그 뒤부터 이 도구로 붙일 수 있다.
270
+
233
271
  `username` 을 받는 도구 중 `velog_list_drafts`·`velog_blog_stats`·
234
272
  `velog_export_posts`·`velog_search_posts` 는 생략하면 **내 계정**을 쓴다.
235
273
 
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # velog-mcp
2
2
 
3
3
  [![npm](https://img.shields.io/npm/v/@milcho0604/velog-mcp)](https://www.npmjs.com/package/@milcho0604/velog-mcp)
4
- [![Node](https://img.shields.io/badge/node-%3E%3D24-brightgreen)](https://nodejs.org)
4
+ [![Node](https://img.shields.io/badge/node-%3E%3D22.18-brightgreen)](https://nodejs.org)
5
5
  [![License](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
6
6
  [![Runtime deps](https://img.shields.io/badge/runtime%20deps-2-lightgrey)](package.json)
7
7
 
@@ -30,13 +30,13 @@ calls. Six server-side quirks are written up in
30
30
  empty list, and one that can turn your published posts private.
31
31
 
32
32
  **3. Two runtime dependencies.** `@modelcontextprotocol/sdk` and `zod`. HTTP, test
33
- runner, and TypeScript execution all come from Node 24 itself.
33
+ runner, and TypeScript execution all come from Node itself.
34
34
 
35
35
  ---
36
36
 
37
37
  ## Install
38
38
 
39
- Requires **Node.js 24 or newer**.
39
+ Requires **Node.js 22.18 or newer**. What runs is the compiled `dist/index.js`, but development and verification execute `.ts` directly, and 22.18 is the first release where that works without a flag. CI covers 22.18, 24 and 26.
40
40
 
41
41
  ### As a Claude Code plugin (recommended)
42
42
 
@@ -68,7 +68,7 @@ Published on npm, so nothing to clone — your MCP client runs it via `npx`. See
68
68
 
69
69
  ```bash
70
70
  claude mcp add velog -e VELOG_REFRESH_TOKEN=your_refresh_token \
71
- -- npx -y @milcho0604/velog-mcp@0.4.2
71
+ -- npx -y @milcho0604/velog-mcp@0.6.0
72
72
  ```
73
73
 
74
74
  The token stays in your client's config file here. The plugin route above puts it in
@@ -91,7 +91,7 @@ Add this to your MCP client config (`claude_desktop_config.json`, `.mcp.json`,
91
91
  "mcpServers": {
92
92
  "velog": {
93
93
  "command": "npx",
94
- "args": ["-y", "@milcho0604/velog-mcp@0.4.2"],
94
+ "args": ["-y", "@milcho0604/velog-mcp@0.6.0"],
95
95
  "env": {
96
96
  "VELOG_REFRESH_TOKEN": "your_refresh_token"
97
97
  }
@@ -104,7 +104,7 @@ With the Claude Code CLI:
104
104
 
105
105
  ```bash
106
106
  claude mcp add velog -e VELOG_REFRESH_TOKEN=your_refresh_token \
107
- -- npx -y @milcho0604/velog-mcp@0.4.2
107
+ -- npx -y @milcho0604/velog-mcp@0.6.0
108
108
  ```
109
109
 
110
110
  To run a local checkout instead, swap the command for
@@ -242,6 +242,40 @@ Full reasoning: [docs/security.md](docs/security.md)
242
242
  > `velog_update_draft` resets what you omit; `velog_update_post` preserves it.
243
243
  > The asymmetry is deliberate — see [docs/tools.md](docs/tools.md).
244
244
 
245
+ #### Automatic thumbnail
246
+
247
+ Omit `thumbnail` and the **first image in the body** becomes the thumbnail, so list and
248
+ share cards aren't text-only. What was chosen is always reported back, along with the
249
+ other candidates when there is more than one.
250
+
251
+ | `thumbnail` | Behaviour |
252
+ | --- | --- |
253
+ | omitted | first image in the body |
254
+ | a URL | used as given |
255
+ | `null` | **opt out** — leave it empty on purpose |
256
+
257
+ Images inside code fences and inline code are excluded, so a markdown example never
258
+ becomes your thumbnail. `velog_update_post` **never replaces an existing thumbnail** —
259
+ editing a title should not change the card. There, `null` means "don't fill it in",
260
+ not "delete it".
261
+
262
+ #### Series — by name, in one call
263
+
264
+ Pass `series_name` and the server resolves it **before** saving, then sends the id in the
265
+ **same request** — writing and filing happen in one call. Names are matched ignoring case
266
+ and surrounding whitespace; `series_id` wins if you know it.
267
+
268
+ ⚠️ **If the name isn't found, nothing is written** — saving without the series would look
269
+ like it worked. The available series are listed in the error.
270
+
271
+ Omit both and the result carries **your series list**. That lookup never fails the write
272
+ (cancellation included — reporting failure after a successful save makes retries duplicate
273
+ the post).
274
+
275
+ > ⚠️ The velog API cannot **create** a series — there is no series mutation, and
276
+ > `WritePostInput` only accepts `series_id`. Create one on velog once, then this
277
+ > server can attach posts to it.
278
+
245
279
  Tools that take a `username` — `velog_list_drafts`, `velog_blog_stats`,
246
280
  `velog_export_posts`, `velog_search_posts` — fall back to your own account when you
247
281
  omit it.
package/dist/client.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * 벨로그 GraphQL 클라이언트.
3
3
  *
4
- * Node 24 내장 fetch 만 쓴다 — HTTP 라이브러리를 추가하지 않는다.
4
+ * Node 내장 fetch 만 쓴다 — HTTP 라이브러리를 추가하지 않는다.
5
5
  * 접속하는 호스트는 v3.velog.io 하나뿐이다 (docs/security.md).
6
6
  */
7
7
  import { type AuthState } from './auth.ts';
package/dist/client.js CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * 벨로그 GraphQL 클라이언트.
3
3
  *
4
- * Node 24 내장 fetch 만 쓴다 — HTTP 라이브러리를 추가하지 않는다.
4
+ * Node 내장 fetch 만 쓴다 — HTTP 라이브러리를 추가하지 않는다.
5
5
  * 접속하는 호스트는 v3.velog.io 하나뿐이다 (docs/security.md).
6
6
  */
7
7
  import { AuthRequiredError, TokenStore, buildCookieHeader, parseSetCookie, } from "./auth.js";
package/dist/index.d.ts CHANGED
@@ -10,7 +10,7 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
10
10
  import { type Capabilities } from './capabilities.ts';
11
11
  import { VelogClient } from './client.ts';
12
12
  export declare const SERVER_NAME = "velog-mcp";
13
- export declare const SERVER_VERSION = "0.4.2";
13
+ export declare const SERVER_VERSION = "0.6.0";
14
14
  export declare function createServer(client: VelogClient, capabilities?: Capabilities): McpServer;
15
15
  /**
16
16
  * 크롬이 있어야만 되는 도구들.
package/dist/index.js CHANGED
@@ -28,7 +28,7 @@ import { registerImageTools } from "./tools/images.js";
28
28
  export const SERVER_NAME = 'velog-mcp';
29
29
  // ⚠️ 이 값은 네 곳이 함께 움직인다 — package.json / plugin.json / .mcp.json 의
30
30
  // npx 핀 / 여기. 어긋나면 P8 테스트가 잡는다. 손으로 맞추지 말고 테스트를 믿을 것.
31
- export const SERVER_VERSION = '0.4.2';
31
+ export const SERVER_VERSION = '0.6.0';
32
32
  export function createServer(client, capabilities = { publicPublish: false, editProfile: false }) {
33
33
  const server = new McpServer({ name: SERVER_NAME, version: SERVER_VERSION }, {
34
34
  instructions: [
@@ -0,0 +1,47 @@
1
+ /**
2
+ * 시리즈 힌트 — `series_id` 를 안 준 글에 "어디에 넣을 수 있는지" 알려준다.
3
+ *
4
+ * ★★ 벨로그 API 는 시리즈를 **만들 수 없다.** 2026-08-13 인트로스펙션으로 확인:
5
+ * 뮤테이션 23개 중 시리즈 관련이 0개이고, `WritePostInput` 도 `series_id` 만
6
+ * 받는다(이름으로 만들어 붙이는 입력이 없다). 조회(`seriesList`)만 열려 있다.
7
+ * 그래서 "없으면 만든다"는 불가능하고, **"없으면 알려준다"**가 최선이다.
8
+ * 새 시리즈는 벨로그 웹에서 한 번 만들면 그 뒤부터 이 도구가 붙일 수 있다.
9
+ *
10
+ * ★ 이 모듈의 실패는 **글쓰기를 막지 않는다.** 힌트를 못 만든 것뿐인데 저장이
11
+ * 실패하면 배보다 배꼽이다. 호출부는 반드시 실패를 삼켜야 한다.
12
+ */
13
+ import type { VelogClient } from './client.ts';
14
+ /**
15
+ * 시리즈 **이름**을 id 로 바꾼다. ★ 저장(mutation) **전에** 부른다.
16
+ *
17
+ * ★★ 왜 이름을 받나 — id 는 사람도 AI 도 모른다. 이름만 받을 수 있으면
18
+ * "저장하면서 시리즈에 넣기"가 **한 번의 호출로** 끝난다. 예전엔
19
+ * 저장 → 목록 받기 → 다시 붙이기로 **세 번**이었다.
20
+ *
21
+ * ★ 못 찾으면 **던진다.** 이 시점엔 아직 아무것도 안 썼으므로 던지는 게 안전하고,
22
+ * 조용히 시리즈 없이 저장하면 사용자는 들어간 줄 안다. 벨로그 API 로는 시리즈를
23
+ * 만들 수 없으므로(뮤테이션 없음) 목록을 함께 보여주고 사람이 정하게 한다.
24
+ */
25
+ export declare function resolveSeriesId(client: VelogClient, username: string, seriesName: string, toolName: string, signal?: AbortSignal): Promise<string>;
26
+ /**
27
+ * 내 시리즈 목록을 사람이 읽을 안내문으로 만든다.
28
+ *
29
+ * @param requested 호출자가 준 series_id. 있으면 힌트를 만들지 않는다.
30
+ * @returns 결과 메시지에 이어붙일 문자열. 붙일 말이 없으면 빈 문자열.
31
+ */
32
+ export declare function describeSeriesOptions(client: VelogClient, username: string, requested: string | undefined, signal?: AbortSignal): Promise<string>;
33
+ /**
34
+ * 힌트를 만들되 **어떤 경우에도 던지지 않는다.**
35
+ *
36
+ * ★★ 이 함수는 **글이 이미 저장된 뒤에만** 불린다. 그래서 취소조차 삼킨다.
37
+ * 여기서 던지면 저장이 끝난 호출이 '실패'로 보고되고, 사용자는 안 써진 줄 알고
38
+ * 다시 부른다 → **글이 두 번 생긴다.** 취소를 존중하는 것보다 중복 생성을
39
+ * 막는 게 크다. 취소는 mutation **전·중**에 이미 걸러진다.
40
+ * (코덱스 교차검증에서 잡혔다: mutation 성공 후 취소 시 wrote=true 인데
41
+ * 최종 Promise 가 rejected 였다.)
42
+ *
43
+ * ★ username 을 **함수로 받는 이유** — 예전엔 `resolveMyUsername()` 을 인자
44
+ * 위치에서 await 했는데, 그러면 **이 try 밖에서** 평가되어 그 조회가 실패하면
45
+ * 똑같이 도구 호출이 통째로 실패했다. 안에서 부른다.
46
+ */
47
+ export declare function seriesHintSafely(client: VelogClient, resolveUsername: () => string | Promise<string>, requested: string | undefined, signal?: AbortSignal): Promise<string>;
package/dist/series.js ADDED
@@ -0,0 +1,109 @@
1
+ /**
2
+ * 시리즈 힌트 — `series_id` 를 안 준 글에 "어디에 넣을 수 있는지" 알려준다.
3
+ *
4
+ * ★★ 벨로그 API 는 시리즈를 **만들 수 없다.** 2026-08-13 인트로스펙션으로 확인:
5
+ * 뮤테이션 23개 중 시리즈 관련이 0개이고, `WritePostInput` 도 `series_id` 만
6
+ * 받는다(이름으로 만들어 붙이는 입력이 없다). 조회(`seriesList`)만 열려 있다.
7
+ * 그래서 "없으면 만든다"는 불가능하고, **"없으면 알려준다"**가 최선이다.
8
+ * 새 시리즈는 벨로그 웹에서 한 번 만들면 그 뒤부터 이 도구가 붙일 수 있다.
9
+ *
10
+ * ★ 이 모듈의 실패는 **글쓰기를 막지 않는다.** 힌트를 못 만든 것뿐인데 저장이
11
+ * 실패하면 배보다 배꼽이다. 호출부는 반드시 실패를 삼켜야 한다.
12
+ */
13
+ const QUERY_SERIES_LIST = `
14
+ query SeriesListHint($input: GetSeriesListInput!) {
15
+ seriesList(input: $input) { id name posts_count }
16
+ }
17
+ `;
18
+ /** 힌트에 보여줄 최대 개수. 시리즈가 수십 개인 사람도 있다. */
19
+ const MAX_SHOWN = 12;
20
+ /** 목록을 사람이 고를 수 있게 줄로 만든다. */
21
+ function listLines(list) {
22
+ const shown = list
23
+ .slice(0, MAX_SHOWN)
24
+ .map((s) => ` - ${s.name ?? '(이름 없음)'} (${s.posts_count ?? 0}편) — \`${s.id}\``)
25
+ .join('\n');
26
+ return list.length > MAX_SHOWN ? `${shown}\n … 외 ${list.length - MAX_SHOWN}개` : shown;
27
+ }
28
+ /** 비교용 정규화 — 앞뒤 공백·대소문자·연속 공백 차이는 무시한다. */
29
+ function norm(name) {
30
+ return name.trim().toLowerCase().replace(/\s+/g, ' ');
31
+ }
32
+ /**
33
+ * 시리즈 **이름**을 id 로 바꾼다. ★ 저장(mutation) **전에** 부른다.
34
+ *
35
+ * ★★ 왜 이름을 받나 — id 는 사람도 AI 도 모른다. 이름만 받을 수 있으면
36
+ * "저장하면서 시리즈에 넣기"가 **한 번의 호출로** 끝난다. 예전엔
37
+ * 저장 → 목록 받기 → 다시 붙이기로 **세 번**이었다.
38
+ *
39
+ * ★ 못 찾으면 **던진다.** 이 시점엔 아직 아무것도 안 썼으므로 던지는 게 안전하고,
40
+ * 조용히 시리즈 없이 저장하면 사용자는 들어간 줄 안다. 벨로그 API 로는 시리즈를
41
+ * 만들 수 없으므로(뮤테이션 없음) 목록을 함께 보여주고 사람이 정하게 한다.
42
+ */
43
+ export async function resolveSeriesId(client, username, seriesName, toolName, signal) {
44
+ const data = await client.request(QUERY_SERIES_LIST, { input: { username } }, { signal });
45
+ const list = data.seriesList ?? [];
46
+ const want = norm(seriesName);
47
+ const hit = list.filter((s) => typeof s.name === 'string' && norm(s.name) === want);
48
+ if (hit.length === 1) {
49
+ const id = hit[0]?.id;
50
+ if (id)
51
+ return id;
52
+ }
53
+ if (hit.length > 1) {
54
+ throw new Error(`${toolName}: "${seriesName}" 과 이름이 같은 시리즈가 ${hit.length}개입니다. ` +
55
+ `series_id 로 직접 지정하세요.\n${listLines(hit)}`);
56
+ }
57
+ throw new Error(`${toolName}: "${seriesName}" 시리즈를 찾지 못했습니다. **글은 저장하지 않았습니다.**\n` +
58
+ (list.length === 0
59
+ ? ' 아직 만든 시리즈가 없습니다.'
60
+ : ` 있는 시리즈:\n${listLines(list)}`) +
61
+ '\n ⚠️ 벨로그 API 로는 시리즈를 **만들 수 없습니다**(조회만 열려 있습니다). ' +
62
+ '벨로그 웹에서 만든 뒤 다시 시도하거나, 위 목록의 이름을 쓰세요.');
63
+ }
64
+ /**
65
+ * 내 시리즈 목록을 사람이 읽을 안내문으로 만든다.
66
+ *
67
+ * @param requested 호출자가 준 series_id. 있으면 힌트를 만들지 않는다.
68
+ * @returns 결과 메시지에 이어붙일 문자열. 붙일 말이 없으면 빈 문자열.
69
+ */
70
+ export async function describeSeriesOptions(client, username, requested, signal) {
71
+ // 이미 지정했으면 참견하지 않는다.
72
+ if (requested !== undefined && requested !== '')
73
+ return '';
74
+ const data = await client.request(QUERY_SERIES_LIST, { input: { username } }, { signal });
75
+ const list = data.seriesList ?? [];
76
+ if (list.length === 0) {
77
+ return ('\n\n📚 시리즈에 넣지 않았습니다. 아직 만든 시리즈가 없습니다.\n' +
78
+ ' ⚠️ 벨로그 API 로는 시리즈를 **만들 수 없습니다**(조회만 열려 있습니다). ' +
79
+ '벨로그 웹에서 시리즈를 한 번 만들면 그 뒤부터 이 도구로 붙일 수 있습니다.');
80
+ }
81
+ return ('\n\n📚 시리즈에 넣지 않았습니다. 넣으려면 **series_name** 에 아래 이름 중 하나를 주세요' +
82
+ '(다음부터는 저장과 동시에 붙습니다).\n' +
83
+ `${listLines(list)}\n` +
84
+ ' 맞는 시리즈가 없으면 벨로그 웹에서 새로 만드세요 — API 로는 만들 수 없습니다.');
85
+ }
86
+ /**
87
+ * 힌트를 만들되 **어떤 경우에도 던지지 않는다.**
88
+ *
89
+ * ★★ 이 함수는 **글이 이미 저장된 뒤에만** 불린다. 그래서 취소조차 삼킨다.
90
+ * 여기서 던지면 저장이 끝난 호출이 '실패'로 보고되고, 사용자는 안 써진 줄 알고
91
+ * 다시 부른다 → **글이 두 번 생긴다.** 취소를 존중하는 것보다 중복 생성을
92
+ * 막는 게 크다. 취소는 mutation **전·중**에 이미 걸러진다.
93
+ * (코덱스 교차검증에서 잡혔다: mutation 성공 후 취소 시 wrote=true 인데
94
+ * 최종 Promise 가 rejected 였다.)
95
+ *
96
+ * ★ username 을 **함수로 받는 이유** — 예전엔 `resolveMyUsername()` 을 인자
97
+ * 위치에서 await 했는데, 그러면 **이 try 밖에서** 평가되어 그 조회가 실패하면
98
+ * 똑같이 도구 호출이 통째로 실패했다. 안에서 부른다.
99
+ */
100
+ export async function seriesHintSafely(client, resolveUsername, requested, signal) {
101
+ try {
102
+ const username = await resolveUsername();
103
+ return await describeSeriesOptions(client, username, requested, signal);
104
+ }
105
+ catch {
106
+ return '';
107
+ }
108
+ }
109
+ //# sourceMappingURL=series.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"series.js","sourceRoot":"","sources":["../src/series.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,63 @@
1
+ /**
2
+ * 썸네일 자동 채움 — 본문에 이미 있는 이미지를 목록 카드에 쓴다.
3
+ *
4
+ * ★ 왜 있는가 — 썸네일이 없으면 글 목록·공유 카드가 글자만 나온다. 그런데
5
+ * 본문에 그림을 넣은 사람은 이미 쓸 만한 이미지를 가지고 있다. 한 번 더
6
+ * 지정하게 만들 이유가 없다.
7
+ *
8
+ * ★★ 이 모듈은 **본문을 고치지 않는다.** 읽기만 한다. 반환값은 '무엇을 쓸지'와
9
+ * '왜 그렇게 정했는지'뿐이고, 실제 적용은 호출부가 한다.
10
+ */
11
+ /** 한 번의 결정 결과. `url` 이 undefined 면 채우지 않는다. */
12
+ export interface ThumbnailChoice {
13
+ /** 실제로 보낼 값. 채우지 않기로 했으면 undefined. */
14
+ readonly url: string | undefined;
15
+ /** 본문에서 찾은 후보 전부 (중복 제거, 등장 순서). */
16
+ readonly candidates: readonly string[];
17
+ /** 왜 이렇게 정했나. 결과 메시지를 만들 때 쓴다. */
18
+ readonly reason: 'explicit' | 'opted-out' | 'auto' | 'none';
19
+ }
20
+ /**
21
+ * 마크다운 본문에서 이미지 URL 을 등장 순서대로 뽑는다.
22
+ *
23
+ * 다루는 형태:
24
+ * - `![alt](url)` · `![alt](url "제목")`
25
+ * - `<img src="url">` (HTML 을 섞어 쓰는 사람이 많다)
26
+ *
27
+ * ⚠️ 코드블록 안의 이미지는 **제외한다.** 예제로 적어둔 마크다운이 썸네일이
28
+ * 되어버리면 황당하다. 펜스(``` 또는 ~~~)와 인라인 코드(`...`)를 먼저 지운다.
29
+ */
30
+ export declare function extractImageUrls(body: string): string[];
31
+ /**
32
+ * 무엇을 썸네일로 쓸지 정한다.
33
+ *
34
+ * 우선순위:
35
+ * 1. 호출자가 준 값 → 그대로 쓴다 (`explicit`)
36
+ * 2. 호출자가 **끄겠다고** 명시 → 안 쓴다 (`opted-out`)
37
+ * 3. 본문 첫 이미지 → 자동 (`auto`)
38
+ * 4. 본문에 이미지가 없음 → 안 쓴다 (`none`)
39
+ *
40
+ * ★ 끄는 방법을 둔 이유 — 이 도구는 나만 쓰는 게 아니다. 썸네일을 **일부러**
41
+ * 비워 두는 사람이 있고, 자동 채움이 그 의도를 조용히 덮으면 안 된다.
42
+ * 빈 문자열이나 null 을 주면 "비워 둬라"로 읽는다.
43
+ */
44
+ export declare function chooseThumbnail(requested: string | null | undefined, body: string): ThumbnailChoice;
45
+ /**
46
+ * 병합 수정(velog_update_post)용. 생성과 규칙이 다르다.
47
+ *
48
+ * ★★ **기존 썸네일이 최우선이다.** 이미 붙어 있는 그림을 본문 첫 이미지로
49
+ * 갈아치우면, 제목만 고치려던 사람이 목록 카드가 바뀌는 걸 당한다.
50
+ * 자동 채움은 **비어 있을 때만** 한다.
51
+ *
52
+ * ⚠️ `null` 을 줘도 **기존 썸네일을 지우지 않는다.** 여기서 null 은 "자동으로
53
+ * 채우지 마라"이지 "지워라"가 아니다. 지우는 건 되돌리기 어려운데 그 의도를
54
+ * null 하나로 단정할 수 없다 — 지우려면 벨로그에서 직접 하는 게 맞다.
55
+ */
56
+ export declare function chooseThumbnailForUpdate(requested: string | null | undefined, existing: string | null | undefined, body: string): ThumbnailChoice;
57
+ /**
58
+ * 결과 메시지에 붙일 안내. 붙일 말이 없으면 빈 문자열.
59
+ *
60
+ * ★ 조용히 하지 않는다. 자동으로 넣었으면 **넣었다고 말한다.** 내가 시키지 않은
61
+ * 변경이 결과에 안 보이면 그게 사고다.
62
+ */
63
+ export declare function describeThumbnail(choice: ThumbnailChoice): string;
@@ -0,0 +1,139 @@
1
+ /**
2
+ * 썸네일 자동 채움 — 본문에 이미 있는 이미지를 목록 카드에 쓴다.
3
+ *
4
+ * ★ 왜 있는가 — 썸네일이 없으면 글 목록·공유 카드가 글자만 나온다. 그런데
5
+ * 본문에 그림을 넣은 사람은 이미 쓸 만한 이미지를 가지고 있다. 한 번 더
6
+ * 지정하게 만들 이유가 없다.
7
+ *
8
+ * ★★ 이 모듈은 **본문을 고치지 않는다.** 읽기만 한다. 반환값은 '무엇을 쓸지'와
9
+ * '왜 그렇게 정했는지'뿐이고, 실제 적용은 호출부가 한다.
10
+ */
11
+ import { isSafeImageUrl } from "./slug.js";
12
+ /** 문자 개수. 괄호 균형 검사에만 쓴다. */
13
+ function countOf(text, ch) {
14
+ let n = 0;
15
+ for (const c of text)
16
+ if (c === ch)
17
+ n++;
18
+ return n;
19
+ }
20
+ /**
21
+ * 마크다운 본문에서 이미지 URL 을 등장 순서대로 뽑는다.
22
+ *
23
+ * 다루는 형태:
24
+ * - `![alt](url)` · `![alt](url "제목")`
25
+ * - `<img src="url">` (HTML 을 섞어 쓰는 사람이 많다)
26
+ *
27
+ * ⚠️ 코드블록 안의 이미지는 **제외한다.** 예제로 적어둔 마크다운이 썸네일이
28
+ * 되어버리면 황당하다. 펜스(``` 또는 ~~~)와 인라인 코드(`...`)를 먼저 지운다.
29
+ */
30
+ export function extractImageUrls(body) {
31
+ // ★ 지우는 순서가 중요하다. 펜스를 먼저 지워야 그 안의 백틱이 인라인 코드로
32
+ // 잘못 짝지어지지 않는다.
33
+ const withoutCode = body
34
+ .replace(/^[ \t]*(`{3,}|~{3,})[\s\S]*?^[ \t]*\1[ \t]*$/gm, '')
35
+ .replace(/`[^`\n]*`/g, '');
36
+ const found = [];
37
+ const push = (raw) => {
38
+ if (!raw)
39
+ return;
40
+ // 마크다운은 URL 뒤에 제목을 붙일 수 있다: (url "제목")
41
+ const url = raw.trim().split(/\s+/)[0]?.replace(/^<|>$/g, '');
42
+ if (!url || !isSafeImageUrl(url))
43
+ return;
44
+ // ★★ 괄호가 안 맞으면 **버린다.** `![a](https://x/y(1).png)` 는 `)` 에서 끊겨
45
+ // `https://x/y(1` 이 잡히는데, 이것도 https 라 URL 검사를 통과한다.
46
+ // 그대로 두면 **깨진 주소가 조용히 썸네일이 된다** — 아무도 모르게 목록
47
+ // 카드만 깨진다. 애매하면 넣지 않는 쪽이 맞다.
48
+ if (countOf(url, '(') !== countOf(url, ')'))
49
+ return;
50
+ if (!found.includes(url))
51
+ found.push(url);
52
+ };
53
+ // alt 안의 대괄호 한 겹까지 허용한다: `![도표[1]](url)`
54
+ for (const m of withoutCode.matchAll(/!\[(?:[^[\]]|\[[^\]]*\])*\]\(([^)]+)\)/g))
55
+ push(m[1]);
56
+ for (const m of withoutCode.matchAll(/<img\b[^>]*?\ssrc\s*=\s*["']([^"']+)["']/gi)) {
57
+ push(m[1]);
58
+ }
59
+ return found;
60
+ }
61
+ /**
62
+ * 무엇을 썸네일로 쓸지 정한다.
63
+ *
64
+ * 우선순위:
65
+ * 1. 호출자가 준 값 → 그대로 쓴다 (`explicit`)
66
+ * 2. 호출자가 **끄겠다고** 명시 → 안 쓴다 (`opted-out`)
67
+ * 3. 본문 첫 이미지 → 자동 (`auto`)
68
+ * 4. 본문에 이미지가 없음 → 안 쓴다 (`none`)
69
+ *
70
+ * ★ 끄는 방법을 둔 이유 — 이 도구는 나만 쓰는 게 아니다. 썸네일을 **일부러**
71
+ * 비워 두는 사람이 있고, 자동 채움이 그 의도를 조용히 덮으면 안 된다.
72
+ * 빈 문자열이나 null 을 주면 "비워 둬라"로 읽는다.
73
+ */
74
+ export function chooseThumbnail(requested, body) {
75
+ const candidates = extractImageUrls(body);
76
+ if (typeof requested === 'string' && requested.trim() !== '') {
77
+ return { url: requested, candidates, reason: 'explicit' };
78
+ }
79
+ // ★ null 과 빈 문자열은 "자동으로 넣지 마라"는 뜻이다. undefined(미지정)와
80
+ // 구분해야 한다 — 미지정은 "알아서 해달라"이고 이건 명시적 거부다.
81
+ if (requested !== undefined) {
82
+ return { url: undefined, candidates, reason: 'opted-out' };
83
+ }
84
+ const first = candidates[0];
85
+ if (first === undefined)
86
+ return { url: undefined, candidates, reason: 'none' };
87
+ return { url: first, candidates, reason: 'auto' };
88
+ }
89
+ /**
90
+ * 병합 수정(velog_update_post)용. 생성과 규칙이 다르다.
91
+ *
92
+ * ★★ **기존 썸네일이 최우선이다.** 이미 붙어 있는 그림을 본문 첫 이미지로
93
+ * 갈아치우면, 제목만 고치려던 사람이 목록 카드가 바뀌는 걸 당한다.
94
+ * 자동 채움은 **비어 있을 때만** 한다.
95
+ *
96
+ * ⚠️ `null` 을 줘도 **기존 썸네일을 지우지 않는다.** 여기서 null 은 "자동으로
97
+ * 채우지 마라"이지 "지워라"가 아니다. 지우는 건 되돌리기 어려운데 그 의도를
98
+ * null 하나로 단정할 수 없다 — 지우려면 벨로그에서 직접 하는 게 맞다.
99
+ */
100
+ export function chooseThumbnailForUpdate(requested, existing, body) {
101
+ if (typeof requested === 'string' && requested.trim() !== '') {
102
+ return { url: requested, candidates: extractImageUrls(body), reason: 'explicit' };
103
+ }
104
+ // ★★ 생략하면 **기존 값을 그대로 잇는다. 비어 있어도 마찬가지다.**
105
+ // 예전엔 기존이 비었을 때 본문 첫 이미지로 채웠는데, 이 도구는 문서에
106
+ // "생략한 필드는 유지된다"고 약속해 뒀다. 제목만 고치려던 호출이 **일부러
107
+ // 비워 둔 썸네일을 채워버리는** 건 그 약속을 깨는 것이다.
108
+ // 자동 채움은 새로 쓰는 경로(create/publish)에만 둔다.
109
+ // (코덱스 교차검증에서 잡혔다.)
110
+ return { url: existing ?? undefined, candidates: [], reason: 'explicit' };
111
+ }
112
+ /**
113
+ * 결과 메시지에 붙일 안내. 붙일 말이 없으면 빈 문자열.
114
+ *
115
+ * ★ 조용히 하지 않는다. 자동으로 넣었으면 **넣었다고 말한다.** 내가 시키지 않은
116
+ * 변경이 결과에 안 보이면 그게 사고다.
117
+ */
118
+ export function describeThumbnail(choice) {
119
+ if (choice.reason === 'auto') {
120
+ const others = choice.candidates.length - 1;
121
+ const head = `\n\n🖼️ 썸네일을 본문 첫 이미지로 **자동 설정**했습니다.\n ${choice.url}`;
122
+ if (others <= 0)
123
+ return head;
124
+ const list = choice.candidates
125
+ .slice(1, 6)
126
+ .map((u, i) => ` ${i + 2}. ${u}`)
127
+ .join('\n');
128
+ const more = others > 5 ? `\n … 외 ${others - 5}개` : '';
129
+ return (`${head}\n\n 본문에 이미지가 ${choice.candidates.length}개 있습니다. ` +
130
+ `다른 걸 쓰려면 thumbnail 에 아래 중 하나를 주세요.\n${list}${more}`);
131
+ }
132
+ if (choice.reason === 'none') {
133
+ return ('\n\n💡 썸네일이 없습니다. 본문에 이미지가 없어 자동으로 채우지 못했습니다 — ' +
134
+ '목록·공유 카드에 그림이 필요하면 thumbnail 에 이미지 URL 을 주거나 ' +
135
+ 'velog_render_diagram / velog_upload_image 로 하나 만들어 붙이세요.');
136
+ }
137
+ return '';
138
+ }
139
+ //# sourceMappingURL=thumbnail.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"thumbnail.js","sourceRoot":"","sources":["../src/thumbnail.ts"],"names":[],"mappings":"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"}