@milcho0604/velog-mcp 0.4.1 → 0.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.
package/dist/me.d.ts CHANGED
@@ -16,8 +16,8 @@ export interface CurrentUser {
16
16
  short_bio?: string | null;
17
17
  } | null;
18
18
  }
19
- export declare function fetchCurrentUser(client: VelogClient): Promise<CurrentUser>;
19
+ export declare function fetchCurrentUser(client: VelogClient, signal?: AbortSignal): Promise<CurrentUser>;
20
20
  /** username 을 생략한 도구들이 쓴다. */
21
- export declare function resolveMyUsername(client: VelogClient): Promise<string>;
21
+ export declare function resolveMyUsername(client: VelogClient, signal?: AbortSignal): Promise<string>;
22
22
  /** 프로필을 바꾼 뒤 캐시를 비운다. 테스트에서도 쓴다. */
23
23
  export declare function invalidateMe(client: VelogClient): void;
package/dist/me.js CHANGED
@@ -8,11 +8,14 @@
8
8
  */
9
9
  import { QUERY_CURRENT_USER } from "./graphql.js";
10
10
  const cache = new WeakMap();
11
- export async function fetchCurrentUser(client) {
11
+ export async function fetchCurrentUser(client, signal) {
12
+ // ★ 캐시 적중도 취소를 존중한다. 여기서 빼면 '취소했는데 어떤 호출은 그냥
13
+ // 진행되는' 비일관이 생긴다 — 취소 규약은 경로마다 달라지면 안 된다.
14
+ signal?.throwIfAborted();
12
15
  const cached = cache.get(client);
13
16
  if (cached)
14
17
  return cached;
15
- const data = await client.request(QUERY_CURRENT_USER);
18
+ const data = await client.request(QUERY_CURRENT_USER, {}, { signal });
16
19
  if (!data.currentUser) {
17
20
  throw new Error('현재 로그인한 계정을 확인할 수 없습니다. 토큰이 만료됐거나 잘못됐습니다. ' +
18
21
  '(access_token 1시간 / refresh_token 30일)');
@@ -21,8 +24,8 @@ export async function fetchCurrentUser(client) {
21
24
  return data.currentUser;
22
25
  }
23
26
  /** username 을 생략한 도구들이 쓴다. */
24
- export async function resolveMyUsername(client) {
25
- const me = await fetchCurrentUser(client);
27
+ export async function resolveMyUsername(client, signal) {
28
+ const me = await fetchCurrentUser(client, signal);
26
29
  if (!me.username) {
27
30
  throw new Error('계정에 username 이 없습니다. 도구 인자로 username 을 직접 지정하세요.');
28
31
  }
package/dist/me.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"me.js","sourceRoot":"","sources":["../src/me.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AASlD,MAAM,KAAK,GAAG,IAAI,OAAO,EAA4B,CAAC;AAEtD,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,MAAmB;IACzD,MAAM,MAAM,GAAG,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IACjC,IAAI,MAAM;QAAE,OAAO,MAAM,CAAC;IAE1B,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,OAAO,CAChC,kBAAkB,CAClB,CAAC;IACF,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC;QACvB,MAAM,IAAI,KAAK,CACd,4CAA4C;YAC3C,wCAAwC,CACzC,CAAC;IACH,CAAC;IACD,KAAK,CAAC,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,WAAW,CAAC,CAAC;IACpC,OAAO,IAAI,CAAC,WAAW,CAAC;AACzB,CAAC;AAED,8BAA8B;AAC9B,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,MAAmB;IAC1D,MAAM,EAAE,GAAG,MAAM,gBAAgB,CAAC,MAAM,CAAC,CAAC;IAC1C,IAAI,CAAC,EAAE,CAAC,QAAQ,EAAE,CAAC;QAClB,MAAM,IAAI,KAAK,CACd,kDAAkD,CAClD,CAAC;IACH,CAAC;IACD,OAAO,EAAE,CAAC,QAAQ,CAAC;AACpB,CAAC;AAED,oCAAoC;AACpC,MAAM,UAAU,YAAY,CAAC,MAAmB;IAC/C,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;AACtB,CAAC"}
1
+ {"version":3,"file":"me.js","sourceRoot":"","sources":["../src/me.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AASlD,MAAM,KAAK,GAAG,IAAI,OAAO,EAA4B,CAAC;AAEtD,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACrC,MAAmB,EACnB,MAAoB;IAEpB,6CAA6C;IAC7C,4CAA4C;IAC5C,MAAM,EAAE,cAAc,EAAE,CAAC;IACzB,MAAM,MAAM,GAAG,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IACjC,IAAI,MAAM;QAAE,OAAO,MAAM,CAAC;IAE1B,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,OAAO,CAChC,kBAAkB,EAClB,EAAE,EACF,EAAE,MAAM,EAAE,CACV,CAAC;IACF,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC;QACvB,MAAM,IAAI,KAAK,CACd,4CAA4C;YAC3C,wCAAwC,CACzC,CAAC;IACH,CAAC;IACD,KAAK,CAAC,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,WAAW,CAAC,CAAC;IACpC,OAAO,IAAI,CAAC,WAAW,CAAC;AACzB,CAAC;AAED,8BAA8B;AAC9B,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACtC,MAAmB,EACnB,MAAoB;IAEpB,MAAM,EAAE,GAAG,MAAM,gBAAgB,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAClD,IAAI,CAAC,EAAE,CAAC,QAAQ,EAAE,CAAC;QAClB,MAAM,IAAI,KAAK,CACd,kDAAkD,CAClD,CAAC;IACH,CAAC;IACD,OAAO,EAAE,CAAC,QAAQ,CAAC;AACpB,CAAC;AAED,oCAAoC;AACpC,MAAM,UAAU,YAAY,CAAC,MAAmB;IAC/C,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;AACtB,CAAC"}
@@ -15,6 +15,7 @@ import { mkdtemp, readdir, rm, stat, writeFile } from 'node:fs/promises';
15
15
  import { tmpdir } from 'node:os';
16
16
  import { join } from 'node:path';
17
17
  import { pathToFileURL } from 'node:url';
18
+ import { makeSerializer } from "../serial.js";
18
19
  import { dumpDom, screenshot } from "./chrome.js";
19
20
  import { MAX_AREA, MAX_DIM, buildDiagramHtml, parseAudit, } from "./page.js";
20
21
  import { buildCoverHtml } from "./cover.js";
@@ -26,16 +27,13 @@ import { buildCoverHtml } from "./cover.js";
26
27
  * 모델이 그림 다섯 장을 한 번에 요청하면 크롬 45개·6GB 가 된다.
27
28
  * 사용자 기기에서 도는 물건이 그러면 안 된다 — 다른 작업까지 같이 죽는다.
28
29
  *
29
- * 그림 한 장은 4초면 끝나므로 줄 세워도 체감 손해가 거의 없다.
30
- * 얻는 것(메모리 상한이 1GB 로 고정된다)이 훨씬 크다.
30
+ * 그림 한 장은 4초면 끝나므로 줄 세워도 체감 손해가 거의 없다
31
+ * (실측 2026-08-07: 3.9초/장 → 60초에 15장). 얻는 것이 훨씬 크다.
32
+ *
33
+ * ★ 구현은 src/serial.ts 로 옮겼다. 업로드도 같은 이유로 줄이 필요해졌는데,
34
+ * 같은 코드를 두 벌 두면 한쪽만 고치게 된다.
31
35
  */
32
- let renderQueue = Promise.resolve();
33
- function serialize(task) {
34
- // 앞 작업이 실패해도 줄은 계속 이어져야 한다.
35
- const next = renderQueue.then(task, task);
36
- renderQueue = next.then(() => undefined, () => undefined);
37
- return next;
38
- }
36
+ const serialize = makeSerializer();
39
37
  const RENDER_PREFIX = 'velog-mcp-render-';
40
38
  const PROFILE_PREFIX = 'velog-mcp-chrome-';
41
39
  const KEEP_MS = 24 * 60 * 60 * 1000;
@@ -70,8 +68,16 @@ async function render(args, parse) {
70
68
  // 프로필만 finally 에서 지운다. 렌더 결과(dir)는 남겨야 하므로 밖에 둘 이유가 없다.
71
69
  let profileA = '';
72
70
  let profileB = '';
71
+ // ★ 결과 디렉터리는 **성공했을 때만** 남긴다. 예전엔 실패해도 남겨서,
72
+ // 감사 파싱이나 스크린샷이 반복 실패하면 HTML 과 쓰다 만 PNG 가 든 폴더가
73
+ // 매번 하나씩 쌓였다. 청소(sweepOld)는 다음 렌더가 돌 때만, 그것도 24시간이
74
+ // 지난 것만 거둬가므로 다시 안 그리면 영영 남는다.
75
+ // 실패한 산출물은 사용자가 열어볼 이유도 없다 — 경로를 돌려주지 못하니까.
76
+ let resultDir = '';
77
+ let succeeded = false;
73
78
  try {
74
79
  const dir = await mkdtemp(join(tmpdir(), RENDER_PREFIX));
80
+ resultDir = dir;
75
81
  // 크롬 두 번에 **서로 다른 프로필**을 준다. 산출물이 나오면 SIGKILL 하고 실제
76
82
  // 종료를 기다리지 않으므로, 같은 프로필을 재사용하면 앞선 크롬이 잠금을 놓기
77
83
  // 전에 다음 크롬이 같은 폴더를 열게 된다.
@@ -100,6 +106,7 @@ async function render(args, parse) {
100
106
  throw new Error('크롬이 PNG 를 만들지 못했습니다. 화면 크기가 비정상이거나(0 이하) ' +
101
107
  '크롬 실행이 중간에 끊겼을 수 있습니다.');
102
108
  }
109
+ succeeded = true;
103
110
  return {
104
111
  pngPath,
105
112
  htmlPath,
@@ -111,12 +118,15 @@ async function render(args, parse) {
111
118
  };
112
119
  }
113
120
  finally {
114
- // 크롬 프로필은 임시 산출물이라 항상 지운다. 렌더 결과(dir)는 남긴다 —
115
- // 사용자가 HTML 을 열어 손보거나 PNG 를 다시 쓸 수 있어야 한다.
121
+ // 크롬 프로필은 임시 산출물이라 항상 지운다. 렌더 결과(dir)는 **성공했을
122
+ // 때만** 남긴다 — 사용자가 HTML 을 열어 손보거나 PNG 를 다시 쓸 수 있어야 한다.
116
123
  for (const profile of [profileA, profileB]) {
117
124
  if (profile)
118
125
  await rm(profile, { recursive: true, force: true }).catch(() => { });
119
126
  }
127
+ if (!succeeded && resultDir) {
128
+ await rm(resultDir, { recursive: true, force: true }).catch(() => { });
129
+ }
120
130
  }
121
131
  }
122
132
  export function renderDiagram(spec, scale = 2) {
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/render/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AACzE,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,EAGN,QAAQ,EACR,OAAO,EACP,gBAAgB,EAChB,UAAU,GACV,MAAM,WAAW,CAAC;AACnB,OAAO,EAAmC,cAAc,EAAE,MAAM,YAAY,CAAC;AAoB7E;;;;;;;;;;GAUG;AACH,IAAI,WAAW,GAAqB,OAAO,CAAC,OAAO,EAAE,CAAC;AAEtD,SAAS,SAAS,CAAI,IAAsB;IAC3C,4BAA4B;IAC5B,MAAM,IAAI,GAAG,WAAW,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IAC1C,WAAW,GAAG,IAAI,CAAC,IAAI,CACtB,GAAG,EAAE,CAAC,SAAS,EACf,GAAG,EAAE,CAAC,SAAS,CACf,CAAC;IACF,OAAO,IAAI,CAAC;AACb,CAAC;AAED,MAAM,aAAa,GAAG,mBAAmB,CAAC;AAC1C,MAAM,cAAc,GAAG,mBAAmB,CAAC;AAC3C,MAAM,OAAO,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;AAEpC;;;;;;GAMG;AACH,KAAK,UAAU,QAAQ;IACtB,MAAM,IAAI,GAAG,MAAM,EAAE,CAAC;IACtB,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,OAAO,CAAC;IACpC,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,IAAI,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC;IAC7E,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;QAC7B,IAAI,CAAC,KAAK,CAAC,WAAW,EAAE;YAAE,SAAS;QACnC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,aAAa,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,cAAc,CAAC;YAAE,SAAS;QAC9F,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;QACpC,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC;QAChD,IAAI,IAAI,IAAI,IAAI,CAAC,OAAO,GAAG,MAAM,EAAE,CAAC;YACnC,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;QAClE,CAAC;IACF,CAAC;AACF,CAAC;AAED,KAAK,UAAU,MAAM,CACpB,IAAa,EACb,KAAyB;IAEzB,sCAAsC;IACtC,MAAM,QAAQ,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;IAEjC,mDAAmD;IACnD,gCAAgC;IAChC,wDAAwD;IACxD,IAAI,QAAQ,GAAG,EAAE,CAAC;IAClB,IAAI,QAAQ,GAAG,EAAE,CAAC;IAClB,IAAI,CAAC;QACJ,MAAM,GAAG,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,aAAa,CAAC,CAAC,CAAC;QACzD,oDAAoD;QACpD,6CAA6C;QAC7C,0BAA0B;QAC1B,QAAQ,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,cAAc,CAAC,CAAC,CAAC;QACzD,QAAQ,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,cAAc,CAAC,CAAC,CAAC;QACzD,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,QAAQ,OAAO,CAAC,CAAC;QACpD,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,QAAQ,MAAM,CAAC,CAAC;QAClD,MAAM,SAAS,CAAC,QAAQ,EAAE,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QAC7C,MAAM,OAAO,GAAG,aAAa,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC;QAE7C,MAAM,GAAG,GAAG,MAAM,OAAO,CAAC,OAAO,EAAE,EAAE,UAAU,EAAE,QAAQ,EAAE,CAAC,CAAC;QAC7D,MAAM,KAAK,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC;QAEzB,mDAAmD;QACnD,wCAAwC;QACxC,IAAI,KAAK,CAAC,CAAC,GAAG,OAAO,IAAI,KAAK,CAAC,CAAC,GAAG,OAAO,IAAI,KAAK,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,GAAG,QAAQ,EAAE,CAAC;YAC5E,MAAM,IAAI,KAAK,CACd,eAAe,KAAK,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,KAAK;gBACrC,OAAO,OAAO,WAAW,CAAC,QAAQ,GAAG,SAAS,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,QAAQ,CACnE,CAAC;QACH,CAAC;QAED,MAAM,UAAU,CAAC,OAAO,EAAE,OAAO,EAAE;YAClC,UAAU,EAAE,QAAQ;YACpB,KAAK,EAAE,KAAK,CAAC,CAAC;YACd,MAAM,EAAE,KAAK,CAAC,CAAC;YACf,KAAK,EAAE,IAAI,CAAC,KAAK;SACjB,CAAC,CAAC;QAEH,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC;QACnD,IAAI,CAAC,IAAI,EAAE,CAAC;YACX,MAAM,IAAI,KAAK,CACd,2CAA2C;gBAC1C,wBAAwB,CACzB,CAAC;QACH,CAAC;QAED,OAAO;YACN,OAAO;YACP,QAAQ;YACR,KAAK,EAAE,KAAK,CAAC,CAAC;YACd,MAAM,EAAE,KAAK,CAAC,CAAC;YACf,KAAK,EAAE,IAAI,CAAC,KAAK;YACjB,KAAK;YACL,KAAK,EAAE,IAAI,CAAC,IAAI;SAChB,CAAC;IACH,CAAC;YAAS,CAAC;QACV,6CAA6C;QAC7C,2CAA2C;QAC3C,KAAK,MAAM,OAAO,IAAI,CAAC,QAAQ,EAAE,QAAQ,CAAC,EAAE,CAAC;YAC5C,IAAI,OAAO;gBAAE,MAAM,EAAE,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;QAClF,CAAC;IACF,CAAC;AACF,CAAC;AAED,MAAM,UAAU,aAAa,CAC5B,IAAiB,EACjB,KAAK,GAAG,CAAC;IAET,OAAO,SAAS,CAAC,GAAG,EAAE,CACrB,MAAM,CAAC,EAAE,IAAI,EAAE,gBAAgB,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,SAAS,EAAE,KAAK,EAAE,EAAE,UAAU,CAAC,CAChF,CAAC;AACH,CAAC;AAED,MAAM,UAAU,WAAW,CAC1B,IAAe,EACf,KAAK,GAAG,CAAC;IAET,OAAO,SAAS,CAAC,GAAG,EAAE,CACrB,MAAM,CAAC,EAAE,IAAI,EAAE,cAAc,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,CAAC,GAAG,EAAE,EAAE;QACxE,MAAM,KAAK,GAAG,kCAAkC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAC3D,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,qBAAqB,CAAC,CAAC;QACxD,OAAO,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAe,CAAC;IAC3C,CAAC,CAAC,CACF,CAAC;AACH,CAAC;AAED,OAAO,EAAsC,WAAW,EAAE,MAAM,WAAW,CAAC;AAC5E,OAAO,EAAmC,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAC/E,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACxC,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/render/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,60 @@
1
+ /**
2
+ * 줄 세우기.
3
+ *
4
+ * MCP 클라이언트는 도구를 **병렬로** 부른다. 이 저장소에는 그게 문제가 되는
5
+ * 자리가 두 종류 있다.
6
+ *
7
+ * ① 자원을 통째로 먹는 작업 — 렌더는 이미 이 방식으로 크롬 메모리를 1GB 에
8
+ * 묶어뒀다(render/index.ts 주석). 업로드도 같은 성격이다: 파일당 10MB 를
9
+ * 최대 60초간 들고 있는데 동시성 상한이 없으면 그대로 곱해진다.
10
+ *
11
+ * ② **읽고-합쳐-쓰는** 작업 — `velog_update_post` 는 기존 글을 읽어 생략 필드를
12
+ * 채운 뒤 전체를 교체한다. 벨로그에는 버전도 ETag 도 없다. 같은 글에
13
+ * '제목=A' 와 '본문=B' 가 동시에 오면 둘 다 옛 값을 읽고, 나중에 쓴 쪽이
14
+ * 앞선 변경을 지운다. 그런데 **둘 다 성공을 보고한다** — 사후 검증도 각자
15
+ * 자기가 보낸 값과 맞춰보므로 통과한다. 사용자는 제목이 사라진 걸 모른다.
16
+ *
17
+ * ②는 같은 대상(글 id·프로필)끼리만 줄을 세우면 되므로 키를 받는다.
18
+ * 다른 글끼리는 그대로 병렬로 돈다.
19
+ *
20
+ * ⚠️ 이건 **한 프로세스 안**에서만 유효하다. 사용자가 벨로그 웹에서 동시에
21
+ * 고치는 것은 막을 수 없다 — 그건 벨로그가 버전을 주지 않는 한 방법이 없다.
22
+ * rate limiter 와 같은 한계이고, 같은 이유로 여기에 적어둔다.
23
+ */
24
+ type Task<T> = () => Promise<T>;
25
+ /** 앞 작업이 끝나야 다음이 시작한다. 앞이 실패해도 줄은 이어진다. */
26
+ export declare function makeSerializer(): <T>(task: Task<T>) => Promise<T>;
27
+ /**
28
+ * 키가 같은 작업끼리만 줄을 세운다.
29
+ *
30
+ * ★ 다 쓴 키는 지운다. 안 지우면 글을 고칠 때마다 항목이 하나씩 쌓여
31
+ * 오래 켜둔 서버에서 계속 자란다. 대기자 수를 세다가 0 이 되면 버린다.
32
+ */
33
+ export interface KeyedSerializer {
34
+ <T>(key: string, task: Task<T>): Promise<T>;
35
+ /**
36
+ * 지금 살아 있는 줄 수. **테스트 전용.**
37
+ * 정리가 되는지 밖에서 볼 방법이 없으면 그 테스트는 거짓 초록이 된다 —
38
+ * 실제로 `lanes.delete()` 를 지워도 통과하는 테스트를 썼다가 코덱스에 잡혔다.
39
+ */
40
+ laneCount(): number;
41
+ }
42
+ export declare function makeKeyedSerializer(): KeyedSerializer;
43
+ /**
44
+ * ★★ 쓰기 줄은 **저장소 전체에 하나뿐이어야 한다.**
45
+ *
46
+ * 처음엔 모듈마다 `makeKeyedSerializer()` 를 따로 만들었다. 그러면 키가 같아도
47
+ * 줄이 다르다 — `velog_update_draft` 와 `velog_publish_draft` 를 같은 글에 동시에
48
+ * 부르면 서로를 못 본다. 코덱스 교차검증에서 실제로 재현됐다: 같은 글의 사전
49
+ * 조회가 겹쳤고(2), 발행 mutation 뒤에 초안 수정이 덮어써 최종 상태가 다시
50
+ * 초안이 됐다. 도구 이름이 달라도 **대상이 같으면 같은 줄**이어야 한다.
51
+ *
52
+ * 키 규약: 글은 `post:<id>`, 새 글은 `post:new`, 프로필은 `profile`.
53
+ */
54
+ export declare const serializeWrite: KeyedSerializer;
55
+ /** 테스트에서만 쓴다 — 줄이 실제로 비워지는지 본다. */
56
+ export declare const __testing: {
57
+ makeKeyedSerializer: typeof makeKeyedSerializer;
58
+ makeSerializer: typeof makeSerializer;
59
+ };
60
+ export {};
package/dist/serial.js ADDED
@@ -0,0 +1,68 @@
1
+ /**
2
+ * 줄 세우기.
3
+ *
4
+ * MCP 클라이언트는 도구를 **병렬로** 부른다. 이 저장소에는 그게 문제가 되는
5
+ * 자리가 두 종류 있다.
6
+ *
7
+ * ① 자원을 통째로 먹는 작업 — 렌더는 이미 이 방식으로 크롬 메모리를 1GB 에
8
+ * 묶어뒀다(render/index.ts 주석). 업로드도 같은 성격이다: 파일당 10MB 를
9
+ * 최대 60초간 들고 있는데 동시성 상한이 없으면 그대로 곱해진다.
10
+ *
11
+ * ② **읽고-합쳐-쓰는** 작업 — `velog_update_post` 는 기존 글을 읽어 생략 필드를
12
+ * 채운 뒤 전체를 교체한다. 벨로그에는 버전도 ETag 도 없다. 같은 글에
13
+ * '제목=A' 와 '본문=B' 가 동시에 오면 둘 다 옛 값을 읽고, 나중에 쓴 쪽이
14
+ * 앞선 변경을 지운다. 그런데 **둘 다 성공을 보고한다** — 사후 검증도 각자
15
+ * 자기가 보낸 값과 맞춰보므로 통과한다. 사용자는 제목이 사라진 걸 모른다.
16
+ *
17
+ * ②는 같은 대상(글 id·프로필)끼리만 줄을 세우면 되므로 키를 받는다.
18
+ * 다른 글끼리는 그대로 병렬로 돈다.
19
+ *
20
+ * ⚠️ 이건 **한 프로세스 안**에서만 유효하다. 사용자가 벨로그 웹에서 동시에
21
+ * 고치는 것은 막을 수 없다 — 그건 벨로그가 버전을 주지 않는 한 방법이 없다.
22
+ * rate limiter 와 같은 한계이고, 같은 이유로 여기에 적어둔다.
23
+ */
24
+ /** 앞 작업이 끝나야 다음이 시작한다. 앞이 실패해도 줄은 이어진다. */
25
+ export function makeSerializer() {
26
+ let queue = Promise.resolve();
27
+ return (task) => {
28
+ // 성공·실패 둘 다에 task 를 걸어 앞 작업의 실패가 줄을 끊지 않게 한다.
29
+ const next = queue.then(task, task);
30
+ queue = next.then(() => undefined, () => undefined);
31
+ return next;
32
+ };
33
+ }
34
+ export function makeKeyedSerializer() {
35
+ const lanes = new Map();
36
+ const serialize = (key, task) => {
37
+ const lane = lanes.get(key) ?? { tail: Promise.resolve(), waiting: 0 };
38
+ lanes.set(key, lane);
39
+ lane.waiting++;
40
+ const next = lane.tail.then(task, task);
41
+ lane.tail = next.then(() => undefined, () => undefined);
42
+ // ★ 정리는 `next` 가 아니라 `lane.tail` 에 건다. next 는 호출자에게 그대로
43
+ // 돌려주므로, 여기에 .then 을 걸면 호출자가 잡지 않은 거절이 하나 더 생긴다.
44
+ void lane.tail.then(() => {
45
+ lane.waiting--;
46
+ if (lane.waiting === 0 && lanes.get(key) === lane)
47
+ lanes.delete(key);
48
+ });
49
+ return next;
50
+ };
51
+ serialize.laneCount = () => lanes.size;
52
+ return serialize;
53
+ }
54
+ /**
55
+ * ★★ 쓰기 줄은 **저장소 전체에 하나뿐이어야 한다.**
56
+ *
57
+ * 처음엔 모듈마다 `makeKeyedSerializer()` 를 따로 만들었다. 그러면 키가 같아도
58
+ * 줄이 다르다 — `velog_update_draft` 와 `velog_publish_draft` 를 같은 글에 동시에
59
+ * 부르면 서로를 못 본다. 코덱스 교차검증에서 실제로 재현됐다: 같은 글의 사전
60
+ * 조회가 겹쳤고(2), 발행 mutation 뒤에 초안 수정이 덮어써 최종 상태가 다시
61
+ * 초안이 됐다. 도구 이름이 달라도 **대상이 같으면 같은 줄**이어야 한다.
62
+ *
63
+ * 키 규약: 글은 `post:<id>`, 새 글은 `post:new`, 프로필은 `profile`.
64
+ */
65
+ export const serializeWrite = makeKeyedSerializer();
66
+ /** 테스트에서만 쓴다 — 줄이 실제로 비워지는지 본다. */
67
+ export const __testing = { makeKeyedSerializer, makeSerializer };
68
+ //# sourceMappingURL=serial.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"serial.js","sourceRoot":"","sources":["../src/serial.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAIH,2CAA2C;AAC3C,MAAM,UAAU,cAAc;IAC7B,IAAI,KAAK,GAAqB,OAAO,CAAC,OAAO,EAAE,CAAC;IAChD,OAAO,CAAI,IAAa,EAAc,EAAE;QACvC,8CAA8C;QAC9C,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;QACpC,KAAK,GAAG,IAAI,CAAC,IAAI,CAChB,GAAG,EAAE,CAAC,SAAS,EACf,GAAG,EAAE,CAAC,SAAS,CACf,CAAC;QACF,OAAO,IAAI,CAAC;IACb,CAAC,CAAC;AACH,CAAC;AAkBD,MAAM,UAAU,mBAAmB;IAClC,MAAM,KAAK,GAAG,IAAI,GAAG,EAAuD,CAAC;IAE7E,MAAM,SAAS,GAAG,CAAI,GAAW,EAAE,IAAa,EAAc,EAAE;QAC/D,MAAM,IAAI,GAAG,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,CAAC,OAAO,EAAE,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC;QACvE,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;QACrB,IAAI,CAAC,OAAO,EAAE,CAAC;QAEf,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;QACxC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,IAAI,CACpB,GAAG,EAAE,CAAC,SAAS,EACf,GAAG,EAAE,CAAC,SAAS,CACf,CAAC;QACF,wDAAwD;QACxD,mDAAmD;QACnD,KAAK,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE;YACxB,IAAI,CAAC,OAAO,EAAE,CAAC;YACf,IAAI,IAAI,CAAC,OAAO,KAAK,CAAC,IAAI,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,IAAI;gBAAE,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QACtE,CAAC,CAAC,CAAC;QACH,OAAO,IAAI,CAAC;IACb,CAAC,CAAC;IACF,SAAS,CAAC,SAAS,GAAG,GAAW,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC;IAC/C,OAAO,SAAS,CAAC;AAClB,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,mBAAmB,EAAE,CAAC;AAEpD,mCAAmC;AACnC,MAAM,CAAC,MAAM,SAAS,GAAG,EAAE,mBAAmB,EAAE,cAAc,EAAE,CAAC"}
@@ -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;