genetics-mcp 2.9.4 → 2.9.5

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.
Binary file
package/bin/manifest.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
- "aarch64-apple-darwin": "221ace8f89b6dc3f28879c02c2ca0a324ec2a9ba5ba3c295305e8b2d43d1a396",
3
- "aarch64-unknown-linux-musl": "168621db08da7e63cc00dafe8847f65cba7db61ccbef61daa9d7322fd6b717e2",
4
- "x86_64-apple-darwin": "28eb940e42721bba6d83dde101be4bbe39bfb12250e5bc45bde7db15810378e9",
5
- "x86_64-unknown-linux-musl": "f221f2087e5f29c2a485e27b41a1c1bf6ddebcd62963952cc5e066cca04da110"
2
+ "aarch64-apple-darwin": "606a0641d503fd1ae929dc5f1dac08dac1b5ef7264a03f5b31ba0c3997287750",
3
+ "aarch64-unknown-linux-musl": "90f09361d90b4aac6d44faf549e3ffa2770b29a01f582307c6c8e705a1e8796a",
4
+ "x86_64-apple-darwin": "7858dbc8d974f68c73ebaf73b2886356899e44224f389e9fab030f15dcb02c08",
5
+ "x86_64-unknown-linux-musl": "813d13fc8d61b032a61dff03a3ac2f7b09816f70624025a8ccd0aa9dbdfc0f7a"
6
6
  }
Binary file
Binary file
package/genetics.mjs CHANGED
@@ -2047,9 +2047,10 @@ const TOOLS = [
2047
2047
  name: 'list_reports',
2048
2048
  description:
2049
2049
  '내가 닿을 수 있는 문서 목록(사람으로 붙으면 소속처 전부, 키면 그 한 곳). 각 줄의 대괄호가 **문서 상태**이고, ' +
2050
- '무엇으로 만들어졌는지·API 로 읽고 쓸 수 있는지를 서버가 판정해 준다. ' +
2051
- '편집기가 정본인 문서는 API 로 못 고친다 — 사람이 편집 화면에서 고쳐야 한다. ' +
2052
- '응답 끝의 다음 page 안내를 따라가면 100건 뒤의 문서도 볼 수 있다.',
2050
+ '무엇으로 만들어졌는지를 서버가 판정해 준다. ' +
2051
+ '**사람이 편집기로 고친 문서도 API 로 고칠 수 있다**(2026-08-25 거절 제거) — 대신 줄에 ' +
2052
+ '「덮음: …」이 붙어 **무엇을 덮게 되는지 미리** 말해 준다. 덮으면 덮기 직전 판이 원장에 남고 ' +
2053
+ '되돌릴 수 있다. 응답 끝의 다음 page 안내를 따라가면 100건 뒤의 문서도 볼 수 있다.',
2053
2054
  inputSchema: {
2054
2055
  type: 'object',
2055
2056
  properties: {
@@ -2067,9 +2068,12 @@ const TOOLS = [
2067
2068
  {
2068
2069
  name: 'get_report',
2069
2070
  description:
2070
- '발행에 쓰인 데이터를 그대로 받아온다. 이걸 고쳐 update_report 로 되돌려 보내면 수정 발행이 된다 — ' +
2071
- '**단 편집기가 정본인 문서는 읽을 수는 있어도 되돌려 보낼 수 없다.** ' +
2072
- '목록의 can_write 가 그 답이고, 거절될 때는 응답에 편집 화면 주소가 실려 온다.',
2071
+ '발행에 쓰인 데이터를 그대로 받아온다. 이걸 고쳐 update_report 로 되돌려 보내면 수정 발행이 된다. ' +
2072
+ '**발행 데이터가 없는 문서(편집기 일반 문서·업로드본)도 열린다** — 그때는 data 가 null 이고, ' +
2073
+ '왜 없는지(note)와 지금 고객이 보는 것의 요약(outline)이 대신 실린다. ' +
2074
+ '어느 경우든 응답 **머리 두 줄**에 정본이 어느 쪽인지(JSON / 발행물)와 본문 판번호(body_rev)가 ' +
2075
+ '붙는다 — 정본이 발행물이면 아래 data 는 **낡은 기록**이라 그대로 되돌려 보내면 사람이 고친 것을 덮는다. ' +
2076
+ '덮어쓰기 전에 **무엇을 덮는지 볼 수 있어야 하므로** 읽기는 막지 않는다.',
2073
2077
  inputSchema: {
2074
2078
  type: 'object',
2075
2079
  properties: {
@@ -2447,6 +2451,24 @@ function warnText(r) {
2447
2451
  : '';
2448
2452
  }
2449
2453
 
2454
+ /* 발행물의 **구조**를 사람이 읽는 줄로 옮긴다.
2455
+ * 두 곳이 쓴다 — `get_report(outline: true)` 와, **데이터가 없는 문서의 기본 조회**다.
2456
+ * 후자는 서버가 outline 을 알아서 실어 주는데(그것이 지금 고객이 보는 것의 유일한 요약),
2457
+ * 예전에는 도구가 `data` 만 돌려주느라 그 요약을 통째로 버렸다. 같은 말을 두 곳에서
2458
+ * 따로 빚으면 언젠가 갈리므로 여기 하나로 둔다. */
2459
+ function outlineText(outline) {
2460
+ const o = outline || {};
2461
+ const c = o.counts || {};
2462
+ const lines = (o.blocks || []).map((b) =>
2463
+ ` ${b.tag}.${b.class}${b.side ? ` [${b.side}]` : ''}${b.text ? ` — ${b.text}` : ''}`);
2464
+ return `발행물 구조 — 절 ${c.sections ?? '?'} · 건 ${c.cases ?? '?'} · 장 ${c.chapters ?? '?'}`
2465
+ + ` · 그림 ${c.images ?? '?'}(표지마크 ${c.cover_mark ?? 0})`
2466
+ + ` · 좌우 ${c.side_l ?? 0}/${c.side_r ?? 0}`
2467
+ + ` · 도해 ${c.logic ?? 0} · 원칙카드 ${c.principles ?? 0}\n`
2468
+ + lines.join('\n')
2469
+ + (o.empty?.length ? `\n\n⚠ 빈 블록 ${o.empty.length}건: ${o.empty.join(', ')}` : '\n\n빈 블록 없음');
2470
+ }
2471
+
2450
2472
  /* 그림이 아직 오는 중이라는 사실을 **발행 응답에 붙인다.**
2451
2473
  * 안 붙이면 에이전트는 그림 없는 문서를 받아 들고 「왜 빠졌지」에서 멈추고,
2452
2474
  * 대개 그 자리에서 다시 발행한다 — 그러면 방금 시작한 생성이 통째로 버려진다. */
@@ -3237,7 +3259,20 @@ async function callTool(name, args = {}) {
3237
3259
  const bits = [];
3238
3260
  if (x.state_label) bits.push(x.state_label);
3239
3261
  else if (x.editable === false) bits.push('데이터 없음');
3240
- if (x.can_write === false) bits.push('API 로 수정 불가');
3262
+ /*
3263
+ * ⚠ 전에는 여기서 `can_write === false` 를 「API 로 수정 불가」로 찍었다. 그런데
3264
+ * 서버는 2026-08-25 거절 제거 뒤 **그 칸을 아예 안 보낸다**(늘 참이라 죽은 칸이었다).
3265
+ * 그래서 이 가지는 **한 번도 안 찍히면서** 도구 설명만 옛 정책을 말하고 있었다.
3266
+ * 지금 서버가 주는 것은 「이 문서를 API 로 고치면 **무엇을 덮게 되는가**」다.
3267
+ */
3268
+ const OVERWRITES = {
3269
+ 'human-draft': '덮음: 사람의 편집 초안',
3270
+ 'published-html': '덮음: 사람이 발행한 본문',
3271
+ both: '덮음: 초안 + 발행 본문',
3272
+ };
3273
+ if (x.overwrites && x.overwrites !== 'none') bits.push(OVERWRITES[x.overwrites] || `덮음: ${x.overwrites}`);
3274
+ // 옛 서버와도 물린다 — 그쪽은 아직 거절하던 판이다.
3275
+ else if (x.can_write === false) bits.push('API 로 수정 불가(옛 서버)');
3241
3276
  const tag = bits.length ? ` [${bits.join(' · ')}]` : '';
3242
3277
  // slug 를 싣는다. 중복 slug 는 409 로 잘 막히지만 **미리 알 수가 없었다** —
3243
3278
  // 목록에 이름표가 없어 새 이름을 고를 때 참고할 것이 없었다(2026-08-18 검토).
@@ -3278,19 +3313,30 @@ async function callTool(name, args = {}) {
3278
3313
  case 'get_report': {
3279
3314
  const q = args.outline ? '?outline=1' : '';
3280
3315
  const r = await api('GET', `/api/reports/${Number(args.id)}${q}`);
3281
- if (!args.outline) return JSON.stringify(r.data, null, 2);
3316
+ if (!args.outline) {
3317
+ /*
3318
+ * ⚠ 전에는 여기서 **`r.data` 만** 돌려줬다. 서버가 정성껏 붙인 것이 이 한 줄에서
3319
+ * 전부 사라졌다 — 데이터가 없는 문서는 그냥 `null` 이 왔고(왜 없는지 안 말한다),
3320
+ * 정본이 발행물로 넘어간 문서는 **「아래 data 는 낡았다」는 경고**를 잃었고,
3321
+ * 낙관 잠금에 쓸 `body_rev` 도 안 왔다. 도구 설명에는 「실린다」고 적혀 있었다 —
3322
+ * **설명이 코드보다 앞서 있었다**(2026-08-26 실측).
3323
+ *
3324
+ * 응답 **모양은 안 바꾼다**(본문은 여전히 `data` 그대로다). 대신 그 앞에 머리를
3325
+ * 두 줄 붙인다 — 읽는 쪽이 첫 줄에서 못 보고 지나칠 수 없게.
3326
+ */
3327
+ const head = [];
3328
+ if (r.note) head.push(`⚠ ${r.note}`);
3329
+ const owner = r.body_owner === 'html' ? '발행물(HTML)' : 'JSON';
3330
+ head.push(`[정본 ${owner} · 본문 판 body_rev=${r.body_rev ?? '?'}`
3331
+ + (r.sha256 ? ` · 지금 발행물 ${r.bytes}B sha256:${String(r.sha256).slice(0, 12)}…` : '')
3332
+ + `] — 고칠 때는 update_report 에 body_rev 를 함께 실으면, 그 사이 누가 고쳤을 때 409 로 멈춘다.`);
3333
+ // 데이터가 없으면 서버가 outline 을 함께 준다 — 그것이 지금 고객이 보는 것의 유일한 요약이다.
3334
+ if (r.data === null && r.outline) head.push(outlineText(r.outline));
3335
+ return `${head.join('\n')}\n${JSON.stringify(r.data, null, 2)}`;
3336
+ }
3282
3337
  // 구조만 본다 — 데이터는 이미 다른 호출로 받을 수 있고, 둘을 같이 실으면
3283
- // 문맥이 두 배가 된다.
3284
- const o = r.outline || {};
3285
- const c = o.counts || {};
3286
- const lines = (o.blocks || []).map((b) =>
3287
- ` ${b.tag}.${b.class}${b.side ? ` [${b.side}]` : ''}${b.text ? ` — ${b.text}` : ''}`);
3288
- return `발행물 구조 — 절 ${c.sections ?? '?'} · 건 ${c.cases ?? '?'} · 장 ${c.chapters ?? '?'}`
3289
- + ` · 그림 ${c.images ?? '?'}(표지마크 ${c.cover_mark ?? 0})`
3290
- + ` · 좌우 ${c.side_l ?? 0}/${c.side_r ?? 0}`
3291
- + ` · 도해 ${c.logic ?? 0} · 원칙카드 ${c.principles ?? 0}\n`
3292
- + lines.join('\n')
3293
- + (o.empty?.length ? `\n\n⚠ 빈 블록 ${o.empty.length}건: ${o.empty.join(', ')}` : '\n\n빈 블록 없음');
3338
+ // 문맥이 두 배가 된다. (같은 말을 데이터 없는 문서에도 쓰므로 함수로 뺐다.)
3339
+ return outlineText(r.outline);
3294
3340
  }
3295
3341
  case 'publish_report': {
3296
3342
  const data = await materializeAssets(args.data);
@@ -3628,11 +3674,10 @@ async function callTool(name, args = {}) {
3628
3674
  * 무엇을 해야 하는지 모른다. 창구가 준 사실(held_by)을 여기서 사람 말로 앞세운다.
3629
3675
  * (하네스가 짧은 이름을 재사용한다 — 2026-08-25 운영 실측, app-4a 둘.)
3630
3676
  */
3631
- if (reg?.held_by) {
3632
- return `이 세션의 이름을 살아 있는 다른 세션(${String(reg.held_by).slice(0, 8)})이 쥐고 있어 이름 없이 등록됐다 — `
3633
- + '이름 없는 주소로는 보낼 수 없다. 그 세션이 닫히면(5분 조용) 이름이 돌아온다. '
3634
- + `그때까지 이 세션에 닿으려면 uuid(${mine})를 쓰면 된다.`;
3635
- }
3677
+ // 이름을 남이 쥐고 있어도 **보낸다** — 발신자 표시는 서버가 uuid 앞 8자로 세운다(침묵 금지).
3678
+ const heldNote = reg?.held_by
3679
+ ? `(이 세션의 이름은 살아 있는 다른 세션 ${String(reg.held_by).slice(0, 8)} 이 쥐고 있어 이 통은 «${mine.slice(0, 8)}» 이름으로 나간다 — 남이 이 세션에 닿으려면 uuid ${mine} 로.)\n`
3680
+ : '';
3636
3681
 
3637
3682
  const r = await api('POST', '/api/mail', {
3638
3683
  tenant: args.tenant,
@@ -3651,7 +3696,7 @@ async function callTool(name, args = {}) {
3651
3696
  // GET /api/mail 은 tenant 이름으로 묻는다. 색인(indexNote)만 id 가 있어야 적힌다.
3652
3697
  mailTenants.set(args.tenant, r?.tenant_id || 0); // 이 소속처에서도 받는다 — 합집합, 폴백 아님
3653
3698
  if (r?.tenant_id && !indexRead()[mine]?.tenant) indexNote(mine, { tenant: args.tenant, tenant_id: r.tenant_id });
3654
- return `«${r.to}» 앞으로 보냈다 — 실 ${r.thread} · ${r.turn}번째.\n`
3699
+ return heldNote + `«${r.to}» 앞으로 보냈다 — 실 ${r.thread} · ${r.turn}번째.\n`
3655
3700
  + `받는 쪽이 보게 될 글자는 이것이다:\n${r.preview}\n`
3656
3701
  + (r.says ? `${r.says}\n` : '')
3657
3702
  + '답이 오면 다른 도구를 부를 때 응답 꼬리에 저절로 실린다 — 따로 확인할 필요 없다.';
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "genetics-mcp",
3
- "version": "2.9.4",
3
+ "version": "2.9.5",
4
4
  "description": "문서 포털 발행·세션 이식·사례 기억 MCP — 데이터를 주면 규격 문서가 나온다 (짝인 스킬 동봉)",
5
5
  "author": "주식회사 LIM (LIM Inc.)",
6
6
  "license": "SEE LICENSE IN LICENSE",
7
7
  "type": "module",
8
8
  "bin": {
9
- "genetics-mcp": "./genetics.mjs"
9
+ "genetics-mcp": "genetics.mjs"
10
10
  },
11
11
  "files": [
12
12
  "genetics.mjs",