genetics-mcp 2.5.0 → 2.7.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.
|
Binary file
|
|
Binary file
|
package/bin/manifest.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"aarch64-apple-darwin": "
|
|
3
|
-
"aarch64-unknown-linux-musl": "
|
|
4
|
-
"x86_64-apple-darwin": "
|
|
5
|
-
"x86_64-unknown-linux-musl": "
|
|
2
|
+
"aarch64-apple-darwin": "c0f7ca318d5e4e040b6118e835cbad3e467597d61e94d150c555ed050b5d1f57",
|
|
3
|
+
"aarch64-unknown-linux-musl": "5588db11a4aad41a65609d4cf14f389d106fb8b74023aa745ae4765738243006",
|
|
4
|
+
"x86_64-apple-darwin": "857d738a752776813e9ee52e972e93d046502aa8364ec9fe2d1ece8a0d770914",
|
|
5
|
+
"x86_64-unknown-linux-musl": "f01bf90ab7014764487773b9ac91ad8532ac6a727eedd6ff50151c3e6eab127e"
|
|
6
6
|
}
|
|
Binary file
|
|
Binary file
|
package/genetics.mjs
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* 의존성 0 — MCP 는 stdio 위의 JSON-RPC 2.0 이라 표준 라이브러리로 충분하다.
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
|
-
import { spawnSync } from 'node:child_process';
|
|
24
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
25
25
|
import crypto from 'node:crypto';
|
|
26
26
|
import fs from 'node:fs';
|
|
27
27
|
import os from 'node:os';
|
|
@@ -1302,6 +1302,26 @@ function transcriptOf(uuid) {
|
|
|
1302
1302
|
}
|
|
1303
1303
|
|
|
1304
1304
|
/** 지금 심박을 보낼 세션들. 이 창의 세션 + 최근에 움직인 사본. */
|
|
1305
|
+
/**
|
|
1306
|
+
* 지금 이 대화의 **출처** — `{session_uuid, head_id}`. 모르면 빈 객체.
|
|
1307
|
+
*
|
|
1308
|
+
* 인용 표(020)는 「이 대화의 **이 턴**이 그 사례의 근거다」를 적는다. 그런데 그 둘을
|
|
1309
|
+
* 도구 인자로 두면 **모델이 지어낸다.** 그래서 여기서 로컬 상태로 채운다 —
|
|
1310
|
+
* 세션 id 는 하네스가 준 환경변수, 턴은 전사의 마지막 발화 id 다.
|
|
1311
|
+
*
|
|
1312
|
+
* ⚠ **모르면 빈 객체를 준다.** 서버는 둘 다 있을 때만 적는다(`citation_ok`).
|
|
1313
|
+
* 지어낸 출처 한 줄이 뒤에서 회수 보호의 근거가 되면, 걷어야 할 것을 안 걷게 된다.
|
|
1314
|
+
*/
|
|
1315
|
+
function myCitation() {
|
|
1316
|
+
const uuid = process.env.CLAUDE_CODE_SESSION_ID || '';
|
|
1317
|
+
if (!/^[0-9a-f-]{36}$/i.test(uuid)) return {};
|
|
1318
|
+
const f = transcriptOf(uuid);
|
|
1319
|
+
if (!f) return {};
|
|
1320
|
+
const head = transcriptHead(f);
|
|
1321
|
+
if (!head?.id) return {};
|
|
1322
|
+
return { session_uuid: uuid, head_id: head.id };
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1305
1325
|
function beatTargets() {
|
|
1306
1326
|
const idx = indexRead();
|
|
1307
1327
|
const mine = process.env.CLAUDE_CODE_SESSION_ID || '';
|
|
@@ -1484,6 +1504,49 @@ async function beatTick() {
|
|
|
1484
1504
|
}
|
|
1485
1505
|
}
|
|
1486
1506
|
|
|
1507
|
+
/*
|
|
1508
|
+
* ── 훅 없이 관측한다 (2026-08-23) ──────────────────────────────────────
|
|
1509
|
+
*
|
|
1510
|
+
* **Codex 는 훅을 신뢰하기 전엔 안 돌린다**(`Hooks need review`), 그리고 그 화면이
|
|
1511
|
+
* ChatGPT.app 경로에서는 안 뜬다(실측). 그래서 codex 세션은 훅으로 관측을 영영 못 올린다 —
|
|
1512
|
+
* 그런데 이 프로젝트가 시작된 사고가 정확히 **옆 codex 가 배포한 것을 내가 몰랐던 일**이다.
|
|
1513
|
+
*
|
|
1514
|
+
* MCP 는 codex 밑에서도 돈다(실측: MCP PID 13409, 부모가 codex 45855). 훅도 신뢰도
|
|
1515
|
+
* 안 지나는 자리다. 그래서 여기서 세포를 띄워 이 저장소의 최근 전사를 훑게 한다.
|
|
1516
|
+
*
|
|
1517
|
+
* ⚠ **심박에 얹지 않고 따로 돈다.** `beatTick()` 은 올린 세션이 없으면 일찍 빠져나가는데
|
|
1518
|
+
* (`if (!recs.length) return`), codex 는 대개 그 상태다. 얹었으면 영영 안 돌았을 것이다.
|
|
1519
|
+
*
|
|
1520
|
+
* 비용: 저장소를 아는 걸음으로 바꿔 **1,436ms → 320ms**(실측). 그리고 세포가 저장소마다
|
|
1521
|
+
* 자물쇠를 잡으므로, MCP 가 세션 수만큼 떠 있어도 **한 번만** 일한다.
|
|
1522
|
+
*/
|
|
1523
|
+
// 시험은 20초를 못 기다린다. **갈아 끼울 수 있게** 둔다(`GCELL_STATE_DIR` 과 같은 규율) —
|
|
1524
|
+
// 다만 하한을 둬서 실수로 0.01초가 들어가 기계를 태우는 일은 막는다.
|
|
1525
|
+
const OBSERVE_EVERY_MS = Math.max(200, Number(cfg.GENETICS_OBSERVE_MS) || 20_000);
|
|
1526
|
+
let observeTimer = null;
|
|
1527
|
+
|
|
1528
|
+
function observeTick() {
|
|
1529
|
+
const { bin } = cellCheck();
|
|
1530
|
+
if (!bin) return; // 세포가 없으면 할 수 있는 것이 없다
|
|
1531
|
+
try {
|
|
1532
|
+
// 떼어낸 자식이다 — MCP 는 도구 응답의 임계 경로에 있고, 이 일은 그것을 기다릴 이유가 없다.
|
|
1533
|
+
// stdio 를 전부 막는다: MCP 의 stdout 은 **프로토콜**이라 한 글자만 흘려도 세션이 깨진다.
|
|
1534
|
+
const c = spawn(bin, ['observe', '--root', process.cwd(), '--sync', '--quiet'],
|
|
1535
|
+
{ stdio: 'ignore', detached: true });
|
|
1536
|
+
c.unref?.();
|
|
1537
|
+
c.on?.('error', () => {});
|
|
1538
|
+
} catch { /* 최선만 다한다 */ }
|
|
1539
|
+
// 세포가 방금 갱신했을 사실을 보고, 바뀌었으면 알린다(한 tick 늦는다 — 자식이 비동기다).
|
|
1540
|
+
if (SEND) notifyToolsChanged(SEND);
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
function startObserve() {
|
|
1544
|
+
if (cfg.GENETICS_NO_BEAT) return; // 심박을 끄는 사람은 이것도 안 원한다
|
|
1545
|
+
observeTick();
|
|
1546
|
+
observeTimer = setInterval(observeTick, OBSERVE_EVERY_MS);
|
|
1547
|
+
observeTimer.unref?.(); // 창을 닫으면 노드가 남지 않게
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1487
1550
|
function startBeat() {
|
|
1488
1551
|
if (cfg.GENETICS_NO_BEAT) return; // 심박을 원치 않는 사람의 탈출구
|
|
1489
1552
|
const cred = credential();
|
|
@@ -1503,6 +1566,19 @@ async function stopBeat() {
|
|
|
1503
1566
|
}
|
|
1504
1567
|
|
|
1505
1568
|
const TOOLS = [
|
|
1569
|
+
/*
|
|
1570
|
+
* ★ **이 도구는 부르지 않아도 일한다.** 설명 자체가 세계 사실을 실어 나른다(`toolsNow()`).
|
|
1571
|
+
* 도구 목록은 매 턴 모델 문맥에 있으므로, 부르기 전에 이미 눈앞에 놓인다.
|
|
1572
|
+
* 부르면 더 많이(8건) 말한다 — 설명에는 3건만 싣는다.
|
|
1573
|
+
*/
|
|
1574
|
+
{
|
|
1575
|
+
name: 'world',
|
|
1576
|
+
description:
|
|
1577
|
+
'이 저장소에서 **다른 세션·다른 기계**가 최근에 무엇을 바꿨나. '
|
|
1578
|
+
+ '「누가 언제 무엇을」만 말하고 무엇을 할지는 안 말한다. '
|
|
1579
|
+
+ '고치기 전에 「내가 알고 있는 것이 아직 맞나」가 궁금하면 이것을 본다.',
|
|
1580
|
+
inputSchema: { type: 'object', properties: {}, additionalProperties: false },
|
|
1581
|
+
},
|
|
1506
1582
|
{
|
|
1507
1583
|
name: 'login',
|
|
1508
1584
|
description:
|
|
@@ -1955,21 +2031,89 @@ function cellClean(s) {
|
|
|
1955
2031
|
.slice(0, 200);
|
|
1956
2032
|
}
|
|
1957
2033
|
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
2034
|
+
/**
|
|
2035
|
+
* 세계 사실을 **줄 단위로**. 도구 응답 꼬리와 도구 설명이 **같은 것**을 쓰게 하려고 뺐다 —
|
|
2036
|
+
* 두 곳에서 따로 지으면 같은 사실을 다르게 말하게 된다.
|
|
2037
|
+
*
|
|
2038
|
+
* ⚠ **시각을 함께 말한다.** 오늘 사고에서 결정적이었던 정보가 정확히 「20:04 에」였다.
|
|
2039
|
+
* `gcell delta` 의 글 출력은 2026-08-23 에 고쳤는데, MCP 는 `--json` 을 **자기가 포맷**하고
|
|
2040
|
+
* 있어서 여기만 여전히 시각을 버리고 있었다. 같은 결함이 두 자리에 있었던 것이다.
|
|
2041
|
+
*/
|
|
2042
|
+
function worldFacts(max = 8) {
|
|
2043
|
+
const { bin } = cellCheck();
|
|
2044
|
+
if (!bin) return null; // 세포가 없다 — 「없다」와 구별한다
|
|
1961
2045
|
try {
|
|
1962
2046
|
const r = spawnSync(bin, ['delta', '--root', process.cwd(), '--json'],
|
|
1963
2047
|
{ encoding: 'utf8', timeout: 800 });
|
|
1964
|
-
if (r.error
|
|
1965
|
-
if (r.status !== 0 || !r.stdout) return '';
|
|
2048
|
+
if (r.error || r.status !== 0 || !r.stdout) return [];
|
|
1966
2049
|
const d = JSON.parse(r.stdout);
|
|
1967
2050
|
const ch = Array.isArray(d.changes) ? d.changes : [];
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
2051
|
+
const out = ch.slice(0, max).map((c) => {
|
|
2052
|
+
const at = Number(c.at) || 0;
|
|
2053
|
+
const t = at > 0
|
|
2054
|
+
? new Date(at * 1000).toTimeString().slice(0, 5)
|
|
2055
|
+
: '시각 미상';
|
|
2056
|
+
return `${cellClean(c.path)} — ${cellClean(c.client)} 세션이 ${t}에 썼다`;
|
|
2057
|
+
});
|
|
2058
|
+
if (ch.length > max) out.push(`그 밖 ${ch.length - max}건`);
|
|
2059
|
+
return out;
|
|
2060
|
+
} catch { return null; }
|
|
2061
|
+
}
|
|
2062
|
+
|
|
2063
|
+
function worldDelta() {
|
|
2064
|
+
const { bin, why } = cellCheck();
|
|
2065
|
+
if (!bin) return cellNotice(why);
|
|
2066
|
+
const facts = worldFacts(8);
|
|
2067
|
+
if (facts === null) return cellNotice('세포의 답을 못 읽었다');
|
|
2068
|
+
if (!facts.length) return ''; // 조용한 구간은 조용하다.
|
|
2069
|
+
return `이 저장소에서 최근에 바뀐 것:\n${facts.map((f) => ` · ${f}`).join('\n')}`;
|
|
2070
|
+
}
|
|
2071
|
+
|
|
2072
|
+
/*
|
|
2073
|
+
* ★★ **도구 설명이 유일한 통로다** — 이 파일의 스모크가 2026-08-20 에 그렇게 적어 뒀고
|
|
2074
|
+
* (「initialize 의 instructions 는 선택 필드라 클라이언트가 안 실을 수 있다」),
|
|
2075
|
+
* 2026-08-23 에 Codex 로 **실증됐다**: 지침에 있는 문장을 물었더니 codex 는 문맥에서 답하지
|
|
2076
|
+
* 못하고 `rg` 로 저장소를 뒤져 `mcp/genetics.mjs` 를 읽어 찾았다. 즉 그 세션의 문맥에
|
|
2077
|
+
* instructions 가 **없었다.**
|
|
2078
|
+
*
|
|
2079
|
+
* 그런데 도구 목록은 **구조상 반드시 문맥에 있다**(없으면 모델이 도구를 못 부른다).
|
|
2080
|
+
* 그래서 세계 사실을 거기에 싣는다. 부르지 않아도 **눈앞에 놓인다** —
|
|
2081
|
+
* README 가 적어 둔 그 원칙(「검색 도구가 아니라 눈앞에 놓기」)의 유일한 이식 경로다.
|
|
2082
|
+
*
|
|
2083
|
+
* ⚠ 다른 도구의 설명은 **안 건드린다.** 발동 낱말이 곧 기능이라(스모크가 그것을 붙든다)
|
|
2084
|
+
* 변하는 문장을 섞으면 도구 선택이 흔들린다. 자리를 하나 따로 둔다.
|
|
2085
|
+
*/
|
|
2086
|
+
/*
|
|
2087
|
+
* ★ **세션 도중에도 갱신한다.** 위 `toolsNow()` 는 `tools/list` 시점의 사실을 싣는데,
|
|
2088
|
+
* 그 시점은 사실상 **세션 시작**이다. 그러면 「자리를 비운 사이」는 덮어도
|
|
2089
|
+
* 「지금 막 일어난 일」은 못 덮는다 — 그리고 오늘 사고가 정확히 후자였다
|
|
2090
|
+
* (내가 일하는 동안 옆 세션이 배포했다).
|
|
2091
|
+
*
|
|
2092
|
+
* codex 가 `notifications/tools/list_changed` 를 안다(바이너리 7회). 사실이 **바뀌었을 때만**
|
|
2093
|
+
* 보낸다 — 매 tick 보내면 클라이언트가 매번 다시 묻고, 그건 소음이다.
|
|
2094
|
+
*/
|
|
2095
|
+
let toolsSig = null;
|
|
2096
|
+
let toolsNotifyReady = false; // initialize 전에는 안 보낸다
|
|
2097
|
+
// stdout 에 쓰는 문은 **하나**다(`send`). 그 함수가 아래 범위에 있어 여기로 이어 둔다 —
|
|
2098
|
+
// 두 곳에서 각자 쓰면 응답 한가운데에 알림이 끼어 프로토콜이 깨진다.
|
|
2099
|
+
let SEND = null;
|
|
2100
|
+
|
|
2101
|
+
function notifyToolsChanged(sendFn) {
|
|
2102
|
+
const facts = worldFacts(3);
|
|
2103
|
+
const sig = facts === null ? null : facts.join('|');
|
|
2104
|
+
if (sig === null) return; // 세포를 못 읽었다 — 지어내지 않는다
|
|
2105
|
+
if (toolsSig === null) { toolsSig = sig; return; } // 첫 관측은 기준선일 뿐이다
|
|
2106
|
+
if (sig === toolsSig) return; // 조용한 구간은 조용하다
|
|
2107
|
+
toolsSig = sig;
|
|
2108
|
+
if (!toolsNotifyReady) return;
|
|
2109
|
+
try { sendFn({ jsonrpc: '2.0', method: 'notifications/tools/list_changed' }); } catch { /* 최선만 */ }
|
|
2110
|
+
}
|
|
2111
|
+
|
|
2112
|
+
function toolsNow() {
|
|
2113
|
+
const facts = worldFacts(3);
|
|
2114
|
+
if (!facts || !facts.length) return TOOLS; // 조용한 구간은 조용하다
|
|
2115
|
+
const head = `\n\n지금 이 저장소:\n${facts.map((f) => ` · ${f}`).join('\n')}`;
|
|
2116
|
+
return TOOLS.map((t) => (t.name === 'world' ? { ...t, description: t.description + head } : t));
|
|
1973
2117
|
}
|
|
1974
2118
|
|
|
1975
2119
|
/**
|
|
@@ -2250,10 +2394,19 @@ async function callTool(name, args = {}) {
|
|
|
2250
2394
|
return `교체 완료 — ${r.tenant} · 건 ${r.cases}${st}\n열람 주소: ${r.url}\n${r.note || ''}`
|
|
2251
2395
|
+ pendingText(r) + warnText(r);
|
|
2252
2396
|
}
|
|
2397
|
+
case 'world': {
|
|
2398
|
+
const facts = worldFacts(8);
|
|
2399
|
+
if (facts === null) return cellNotice('세포의 답을 못 읽었다');
|
|
2400
|
+
if (!facts.length) return '이 저장소에서 최근에 남이 바꾼 것은 없다.';
|
|
2401
|
+
return `이 저장소에서 최근에 바뀐 것:\n${facts.map((f) => ` · ${f}`).join('\n')}`;
|
|
2402
|
+
}
|
|
2253
2403
|
case 'remember_case': {
|
|
2254
2404
|
const r = await api('POST', '/api/wiki/case?tenant=' + encodeURIComponent(args.tenant), {
|
|
2255
2405
|
title: args.title, symptom: args.symptom, cause: args.cause || '', fix: args.fix,
|
|
2256
2406
|
evidence: args.evidence || '', tags: args.tags || [],
|
|
2407
|
+
// ★ **출처는 우리가 채운다 — 모델에게 안 묻는다.** 스키마에 노출하면 지어낸다
|
|
2408
|
+
// (DESIGN-session-as-vcs.md:136-141). 모르면 안 보내고, 서버는 안 적는다.
|
|
2409
|
+
...myCitation(),
|
|
2257
2410
|
});
|
|
2258
2411
|
return `${r.note} — ${r.tenant} · ${r.path}\n`
|
|
2259
2412
|
+ '다음에 같은 증상을 만나면 search_cases 로 이 장을 먼저 찾는다.';
|
|
@@ -2454,6 +2607,7 @@ process.stdout.on('error', (e) => {
|
|
|
2454
2607
|
function send(msg) {
|
|
2455
2608
|
process.stdout.write(JSON.stringify(msg) + '\n');
|
|
2456
2609
|
}
|
|
2610
|
+
SEND = send;
|
|
2457
2611
|
|
|
2458
2612
|
let buf = '';
|
|
2459
2613
|
|
|
@@ -2499,7 +2653,9 @@ async function handle(line) {
|
|
|
2499
2653
|
CLIENT = req.params?.clientInfo || null;
|
|
2500
2654
|
result = {
|
|
2501
2655
|
protocolVersion: req.params?.protocolVersion || '2024-11-05',
|
|
2502
|
-
|
|
2656
|
+
// ★ `listChanged` 를 **선언해야** 알림을 보낼 자격이 생긴다. 명세상 안 알리고
|
|
2657
|
+
// 보내면 클라이언트가 버려도 할 말이 없다.
|
|
2658
|
+
capabilities: { tools: { listChanged: true } },
|
|
2503
2659
|
/*
|
|
2504
2660
|
* 자기소개 이름. **도구 이름 접두사와 다른 물건이다** — 그 접두사는 클라이언트 설정
|
|
2505
2661
|
* 키(`claude mcp add --scope user <키>`)에서 나오고, 패키지는 그 값을 못 바꾼다.
|
|
@@ -2537,8 +2693,9 @@ async function handle(line) {
|
|
|
2537
2693
|
'내 소속이 아닌 곳은 존재도 안 보이므로, 404 는 "없다"가 아니라 "내 것이 아니다"일 수 있다.',
|
|
2538
2694
|
].join('\n'),
|
|
2539
2695
|
};
|
|
2696
|
+
toolsNotifyReady = true;
|
|
2540
2697
|
} else if (req.method === 'tools/list') {
|
|
2541
|
-
result = { tools:
|
|
2698
|
+
result = { tools: toolsNow() };
|
|
2542
2699
|
} else if (req.method === 'tools/call') {
|
|
2543
2700
|
const text = await callTool(req.params?.name, req.params?.arguments || {});
|
|
2544
2701
|
result = { content: [{ type: 'text', text }] };
|
|
@@ -2729,6 +2886,7 @@ if (IS_MAIN) {
|
|
|
2729
2886
|
* 꺼진 것이지 고장이 아니다(그림 자격이 없을 때와 같은 태도).
|
|
2730
2887
|
*/
|
|
2731
2888
|
startBeat();
|
|
2889
|
+
startObserve(); // 훅이 안 걸리는 도구(Codex)도 관측을 올린다 — 위 머리말 참조
|
|
2732
2890
|
process.stdin.setEncoding('utf8');
|
|
2733
2891
|
process.stdin.on('data', (chunk) => { buf += chunk; pump(); });
|
|
2734
2892
|
process.stdin.on('end', () => { pump(true); stopBeat().catch(() => {}); });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "genetics-mcp",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.7.0",
|
|
4
4
|
"description": "문서 포털 발행·세션 이식·사례 기억 MCP — 데이터를 주면 규격 문서가 나온다 (짝인 스킬 동봉)",
|
|
5
5
|
"author": "주식회사 LIM (LIM Inc.)",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"node": ">=18"
|
|
19
19
|
},
|
|
20
20
|
"scripts": {
|
|
21
|
-
"test": "node smoke.mjs && bash ../cell/tests/cellcycle.sh",
|
|
21
|
+
"test": "node smoke.mjs && node notify.test.mjs && bash ../cell/tests/cellcycle.sh",
|
|
22
22
|
"prepack": "find skills -name __pycache__ -type d -prune -exec rm -rf {} + 2>/dev/null; bash ../cell/build-all.sh",
|
|
23
23
|
"prepublishOnly": "npm test"
|
|
24
24
|
}
|