genetics-mcp 2.12.0 → 2.14.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/bin/aarch64-apple-darwin/gcell +0 -0
- package/bin/aarch64-unknown-linux-musl/gcell +0 -0
- package/bin/manifest.json +4 -4
- package/bin/x86_64-apple-darwin/gcell +0 -0
- package/bin/x86_64-unknown-linux-musl/gcell +0 -0
- package/genetics.mjs +165 -54
- package/package.json +1 -1
- package/skills/genetics-cell-id/SKILL.md +3 -3
- package/skills/genetics-report/SKILL.md +1 -1
- package/skills/genetics-sync/SKILL.md +1 -1
|
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": "50e3f5feb6b5f3820063b9698bd947c5e12ac526434c0dd9373981b0e38abb58",
|
|
3
|
+
"aarch64-unknown-linux-musl": "f6d1966397744977074fbbd912850eafd6f31a3f7adc560a06aa88cf070e94ea",
|
|
4
|
+
"x86_64-apple-darwin": "29484f84cd380dc8612880e657bc8d10b3e36b37d3e5a34ce204becaec1dd378",
|
|
5
|
+
"x86_64-unknown-linux-musl": "39b43a90e274a7c8d911c5179d3ef538983968af600f0c35230d32be8bb9353b"
|
|
6
6
|
}
|
|
Binary file
|
|
Binary file
|
package/genetics.mjs
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* 이 서버는 모양을 만들지 않는다. 서버(build.py)가 규격대로 렌더하므로,
|
|
14
14
|
* 여기서 보내는 것은 **내용뿐**이다. 그래서 CSS 가 제각각일 수 없다.
|
|
15
15
|
*
|
|
16
|
-
* **키 하나가 고객사 하나다.** 여기에 더해 발행 도구는 `tenant`(대상 고객사
|
|
16
|
+
* **키 하나가 고객사 하나다.** 여기에 더해 발행 도구는 `tenant`(대상 고객사 slug — 이름도 받는다)를
|
|
17
17
|
* 반드시 받아 키와 대조한다 — 키만 믿었다가 실제로 남의 고객사에 발행한 적이 있다
|
|
18
18
|
* (2026-08-12). 키와 의도가 둘 다 맞아야 나간다.
|
|
19
19
|
* 키는 사람이 관리 화면에서 발급해 자격증명 파일(권한 600)에 둔다 — 경로는 아래 ENV_FILE.
|
|
@@ -700,6 +700,7 @@ function limMeta(h) {
|
|
|
700
700
|
head_id: h.get('x-lim-head-id'), head_ms: num('x-lim-head-ms'),
|
|
701
701
|
lines: num('x-lim-lines'), bytes: num('x-lim-bytes'),
|
|
702
702
|
redacted: num('x-lim-redacted'), tenant_id: num('x-lim-tenant-id'),
|
|
703
|
+
tenant_slug: h.get('x-lim-tenant-slug'),
|
|
703
704
|
};
|
|
704
705
|
}
|
|
705
706
|
|
|
@@ -1257,6 +1258,9 @@ function findTranscript(uuid) {
|
|
|
1257
1258
|
const here = path.join(transcriptDir(process.cwd()), `${uuid}.jsonl`);
|
|
1258
1259
|
if (fs.existsSync(here)) return here;
|
|
1259
1260
|
const root = path.join(os.homedir(), '.claude', 'projects');
|
|
1261
|
+
// Claude Code 가 없는 기계(Cursor 만 깔린 곳)에는 이 디렉터리 자체가 없다 —
|
|
1262
|
+
// 「전사 없음」이지 오류가 아니다. 던지면 mail 등 도구가 통째로 죽는다(smoke 실측).
|
|
1263
|
+
if (!fs.existsSync(root)) return null;
|
|
1260
1264
|
for (const d of fs.readdirSync(root, { withFileTypes: true })) {
|
|
1261
1265
|
if (!d.isDirectory()) continue;
|
|
1262
1266
|
const f = path.join(root, d.name, `${uuid}.jsonl`);
|
|
@@ -1265,6 +1269,81 @@ function findTranscript(uuid) {
|
|
|
1265
1269
|
return null;
|
|
1266
1270
|
}
|
|
1267
1271
|
|
|
1272
|
+
/* ── Cursor 신원 ──────────────────────────────────────────────────────────
|
|
1273
|
+
*
|
|
1274
|
+
* Cursor 는 MCP 자식에게 대화(세션) id 를 **안 준다** — 실측(2026-08-27, pid 98284 env):
|
|
1275
|
+
* `CODE_EDITOR=cursor` 와 `CURSOR_WORKSPACE_LABEL=<창 이름>` 뿐이고, MCP 프로세스는
|
|
1276
|
+
* 대화마다가 아니라 **창마다 하나**다. 대신 창의 워크스페이스 저장소가 「지금 포커스된
|
|
1277
|
+
* 대화」를 들고 있다:
|
|
1278
|
+
*
|
|
1279
|
+
* <Cursor>/User/workspaceStorage/<hash>/workspace.json → { folder: "file:///…" }
|
|
1280
|
+
* 같은 칸의 state.vscdb ItemTable 'composer.composerData'
|
|
1281
|
+
* → { lastFocusedComposerIds: ["<uuid4>"], selectedComposerIds: […] }
|
|
1282
|
+
*
|
|
1283
|
+
* composerId 는 uuid4 라 군체의 세션 열쇠와 같은 자리수다 — 그 uuid 를 「지금 이 대화」로
|
|
1284
|
+
* 쓴다. 단 **CLAUDE_CODE_SESSION_ID 가 있으면 언제나 그것이 정본**이다: Cursor 통합
|
|
1285
|
+
* 터미널 안 Claude Code 도 CURSOR_* env 를 물려받으므로(실측: pid 706 은 둘 다 가짐),
|
|
1286
|
+
* 순서를 바꾸면 Claude 세션이 Cursor 대화로 오인된다.
|
|
1287
|
+
*
|
|
1288
|
+
* 한계를 지어내지 않는다 — 이 uuid 로 되는 것은 **주소를 대는 것**까지다. 전사가
|
|
1289
|
+
* ~/.claude/projects 에 없으므로 올리기·머리 계산·자동 올리기는 각자의 전사 검사에서
|
|
1290
|
+
* 조용히 비켜난다(beatTargets:transcriptOf null → skip). 대화 본문을 올리는 길(두뇌
|
|
1291
|
+
* 파서까지)은 별도 단계다 — DESIGN-cursor-colony.md.
|
|
1292
|
+
*/
|
|
1293
|
+
const CURSOR_ROOT = process.env.GENETICS_CURSOR_ROOT
|
|
1294
|
+
|| path.join(os.homedir(), 'Library', 'Application Support', 'Cursor', 'User');
|
|
1295
|
+
|
|
1296
|
+
let cursorSeen = { at: 0, id: '', folder: '' }; // 15초 캐시 — 도구마다 sqlite 를 안 두드리게
|
|
1297
|
+
|
|
1298
|
+
function cursorActiveComposer() {
|
|
1299
|
+
if (process.env.CLAUDE_CODE_SESSION_ID) return '';
|
|
1300
|
+
const label = process.env.CURSOR_WORKSPACE_LABEL || '';
|
|
1301
|
+
if (!label) return ''; // 창을 못 고르면 추측하지 않는다
|
|
1302
|
+
const now = Date.now();
|
|
1303
|
+
if (now - cursorSeen.at < 15000) return cursorSeen.id;
|
|
1304
|
+
cursorSeen = { at: now, id: '', folder: '' };
|
|
1305
|
+
try {
|
|
1306
|
+
const wsRoot = path.join(CURSOR_ROOT, 'workspaceStorage');
|
|
1307
|
+
let best = null; // 같은 이름의 창이 여럿이면 가장 최근에 움직인 것
|
|
1308
|
+
for (const d of fs.readdirSync(wsRoot)) {
|
|
1309
|
+
const dir = path.join(wsRoot, d);
|
|
1310
|
+
let folder = '';
|
|
1311
|
+
try {
|
|
1312
|
+
folder = String(JSON.parse(fs.readFileSync(path.join(dir, 'workspace.json'), 'utf8')).folder || '');
|
|
1313
|
+
} catch { continue; }
|
|
1314
|
+
if (!folder.startsWith('file://')) continue;
|
|
1315
|
+
const wsPath = decodeURIComponent(folder.slice('file://'.length)).replace(/\/+$/, '');
|
|
1316
|
+
if (path.basename(wsPath) !== label) continue;
|
|
1317
|
+
let mt = 0;
|
|
1318
|
+
try { mt = fs.statSync(path.join(dir, 'state.vscdb')).mtimeMs; } catch { continue; }
|
|
1319
|
+
if (!best || mt > best.mt) best = { dir, wsPath, mt };
|
|
1320
|
+
}
|
|
1321
|
+
if (!best) return '';
|
|
1322
|
+
const r = spawnSync('/usr/bin/sqlite3',
|
|
1323
|
+
['-readonly', path.join(best.dir, 'state.vscdb'),
|
|
1324
|
+
"SELECT value FROM ItemTable WHERE key='composer.composerData'"],
|
|
1325
|
+
{ encoding: 'utf8', timeout: 2000 });
|
|
1326
|
+
if (r.status !== 0) return '';
|
|
1327
|
+
const j = JSON.parse((r.stdout || '').trim() || '{}');
|
|
1328
|
+
const v = j.lastFocusedComposerIds || j.selectedComposerIds;
|
|
1329
|
+
const id = Array.isArray(v) ? String(v[0] || '') : '';
|
|
1330
|
+
if (!/^[0-9a-f-]{36}$/i.test(id)) return '';
|
|
1331
|
+
cursorSeen = { at: now, id, folder: best.wsPath };
|
|
1332
|
+
return id;
|
|
1333
|
+
} catch { return ''; }
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
/** 위 해석기가 고른 창의 **저장소 폴더** — 소속처 입양(같은 폴더 규칙)이 쓴다. */
|
|
1337
|
+
function cursorWorkspaceFolder() {
|
|
1338
|
+
if (!cursorSeen.folder) cursorActiveComposer();
|
|
1339
|
+
return cursorSeen.folder;
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
/** 「지금 이 세션」 — Claude Code env 가 정본, 없으면 Cursor 활성 대화. */
|
|
1343
|
+
function mySessionId() {
|
|
1344
|
+
return process.env.CLAUDE_CODE_SESSION_ID || cursorActiveComposer();
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1268
1347
|
function gitHere() {
|
|
1269
1348
|
try {
|
|
1270
1349
|
const r = spawnSync('git', ['-C', process.cwd(), 'rev-parse', '--abbrev-ref', 'HEAD'],
|
|
@@ -1382,7 +1461,7 @@ async function pushSession(tenant, uuid, title, dryRun) {
|
|
|
1382
1461
|
const parts = splitAtLines(sc.jsonl, PUSH_PART_MAX);
|
|
1383
1462
|
if (parts.length === 1) {
|
|
1384
1463
|
const r = await api('PUT', '/api/sessions', body);
|
|
1385
|
-
if (!dryRun) indexNote(uuid, { tenant: r.tenant || tenant, tenant_id: r.tenant_id, head_id: r.head_id, head_ms: r.head_ms }, 'tx');
|
|
1464
|
+
if (!dryRun) indexNote(uuid, { tenant: r.tenant || tenant, tenant_id: r.tenant_id, head_id: r.head_id, head_ms: r.head_ms, ...(r.tenant_slug ? { tenant_slug: r.tenant_slug } : {}) }, 'tx');
|
|
1386
1465
|
return { r, before, parts: 1 };
|
|
1387
1466
|
}
|
|
1388
1467
|
|
|
@@ -1399,7 +1478,7 @@ async function pushSession(tenant, uuid, title, dryRun) {
|
|
|
1399
1478
|
process.stderr.write(`genetics: 조각 ${i + 1}/${parts.length} ${dryRun ? '시험' : '올림'} `
|
|
1400
1479
|
+ `(${(Buffer.byteLength(parts[i], 'utf8') / 1048576).toFixed(1)}MB)\n`);
|
|
1401
1480
|
}
|
|
1402
|
-
if (!dryRun) indexNote(uuid, { tenant: last.tenant || tenant, tenant_id: last.tenant_id, head_id: last.head_id, head_ms: last.head_ms }, 'tx');
|
|
1481
|
+
if (!dryRun) indexNote(uuid, { tenant: last.tenant || tenant, tenant_id: last.tenant_id, head_id: last.head_id, head_ms: last.head_ms, ...(last.tenant_slug ? { tenant_slug: last.tenant_slug } : {}) }, 'tx');
|
|
1403
1482
|
return { r: last, before, parts: parts.length };
|
|
1404
1483
|
}
|
|
1405
1484
|
|
|
@@ -1516,6 +1595,12 @@ function indexRead() {
|
|
|
1516
1595
|
* 말한 머리를 그대로 적을 뿐이고, 등록(:1676)은 아무것도 주고받지 않는다. 그 둘은 계기가
|
|
1517
1596
|
* 없다. 그리고 커서 쓰기는 **아무것도 안 왔을 때도** 도므로 부르는 쪽이 조건을 건다.
|
|
1518
1597
|
*/
|
|
1598
|
+
/** 색인 유래로 서버를 부를 때의 `tenant` 인자 — slug 가 있으면 slug(유일·불변), 없으면 옛 이름.
|
|
1599
|
+
* 색인의 `tenant` 칸은 계속 **이름**이다(표시용) — 인자로 나갈 때만 여기서 갈아탄다. */
|
|
1600
|
+
function tenantArgOf(e) {
|
|
1601
|
+
return (e && typeof e.tenant_slug === 'string' && e.tenant_slug !== '') ? e.tenant_slug : (e?.tenant || '');
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1519
1604
|
function indexNote(uuid, entry, flow = '') {
|
|
1520
1605
|
if (!uuid || !entry?.tenant_id) return;
|
|
1521
1606
|
try {
|
|
@@ -1618,7 +1703,7 @@ function transcriptOf(uuid) {
|
|
|
1618
1703
|
* 지어낸 출처 한 줄이 뒤에서 회수 보호의 근거가 되면, 걷어야 할 것을 안 걷게 된다.
|
|
1619
1704
|
*/
|
|
1620
1705
|
function myCitation() {
|
|
1621
|
-
const uuid =
|
|
1706
|
+
const uuid = mySessionId();
|
|
1622
1707
|
if (!/^[0-9a-f-]{36}$/i.test(uuid)) return {};
|
|
1623
1708
|
const f = transcriptOf(uuid);
|
|
1624
1709
|
if (!f) return {};
|
|
@@ -1660,18 +1745,29 @@ function adoptTenantOf(myDir, rows) {
|
|
|
1660
1745
|
let adoptSaid = false;
|
|
1661
1746
|
/** 위 규칙을 이 기계에 적용한다 — 색인에 한 줄을 더할 뿐, **올리지는 않는다.** */
|
|
1662
1747
|
function indexAdoptCurrent() {
|
|
1663
|
-
const mine =
|
|
1748
|
+
const mine = mySessionId();
|
|
1664
1749
|
if (!mine) return;
|
|
1665
1750
|
const idx = indexRead();
|
|
1666
1751
|
if (idx[mine]?.tenant_id) return; // 이미 등록돼 있다
|
|
1667
1752
|
const f = transcriptOf(mine);
|
|
1668
|
-
|
|
1669
|
-
|
|
1753
|
+
/*
|
|
1754
|
+
* 폴더 열쇠 — 입양 규칙(같은 폴더 = 같은 소속처)의 「폴더」는 전사 디렉터리다.
|
|
1755
|
+
* Cursor 대화는 ~/.claude/projects 에 전사가 없지만 **창의 저장소 폴더**는 아니까,
|
|
1756
|
+
* 같은 슬러그 규칙(transcriptDir)으로 열쇠를 만들면 그 저장소의 Claude 세션들과
|
|
1757
|
+
* 같은 칸에 선다 — report-portal 창의 Cursor 대화는 report-portal 세션들을 따라간다.
|
|
1758
|
+
*/
|
|
1759
|
+
let myDir = '';
|
|
1760
|
+
if (f) myDir = path.dirname(f);
|
|
1761
|
+
else if (mine && mine === cursorActiveComposer()) {
|
|
1762
|
+
const folder = cursorWorkspaceFolder();
|
|
1763
|
+
if (folder) myDir = transcriptDir(folder);
|
|
1764
|
+
}
|
|
1765
|
+
if (!myDir) return; // 이 기계에 사본도, 창 귀속도 없다
|
|
1670
1766
|
const rows = [];
|
|
1671
1767
|
for (const [u, e] of Object.entries(idx)) {
|
|
1672
1768
|
if (!e?.tenant_id) continue;
|
|
1673
1769
|
const p = transcriptOf(u);
|
|
1674
|
-
if (p) rows.push({ dir: path.dirname(p), tenant: e.tenant, tenant_id: e.tenant_id });
|
|
1770
|
+
if (p) rows.push({ dir: path.dirname(p), tenant: e.tenant, tenant_id: e.tenant_id, ...(e.tenant_slug ? { tenant_slug: e.tenant_slug } : {}) });
|
|
1675
1771
|
}
|
|
1676
1772
|
const pick = adoptTenantOf(myDir, rows);
|
|
1677
1773
|
// 이 폴더의 **첫 세션**이면 근거가 없다 — 여기서 조용히 되돌아가는 것은 의도이고,
|
|
@@ -1686,14 +1782,14 @@ function indexAdoptCurrent() {
|
|
|
1686
1782
|
}
|
|
1687
1783
|
return;
|
|
1688
1784
|
}
|
|
1689
|
-
indexNote(mine, { tenant: pick.tenant, tenant_id: pick.tenant_id, head_id: '', head_ms: 0 });
|
|
1785
|
+
indexNote(mine, { tenant: pick.tenant, tenant_id: pick.tenant_id, head_id: '', head_ms: 0, ...(pick.tenant_slug ? { tenant_slug: pick.tenant_slug } : {}) });
|
|
1690
1786
|
process.stderr.write('genetics: 이 세션을 «' + pick.tenant
|
|
1691
1787
|
+ '» 로 등록했다 — 같은 폴더의 앞 세션과 같은 곳이다. 이제 자동으로 올라간다.\n');
|
|
1692
1788
|
}
|
|
1693
1789
|
|
|
1694
1790
|
function beatTargets() {
|
|
1695
1791
|
const idx = indexRead();
|
|
1696
|
-
const mine =
|
|
1792
|
+
const mine = mySessionId();
|
|
1697
1793
|
const now = Date.now();
|
|
1698
1794
|
const out = [];
|
|
1699
1795
|
for (const [uuid, e] of Object.entries(idx)) {
|
|
@@ -1711,7 +1807,7 @@ function beatTargets() {
|
|
|
1711
1807
|
lines: head.lines, tenant: e.tenant_id, flags: 0,
|
|
1712
1808
|
// 방금 움직였으면 「말하는 중」이다 — 화면의 맥박이 여기서 뛴다.
|
|
1713
1809
|
state: now - mtime < BEAT_EVERY_MS ? BEAT_STATE.WORKING : BEAT_STATE.OPEN,
|
|
1714
|
-
|
|
1810
|
+
_tenant_arg: tenantArgOf(e),
|
|
1715
1811
|
});
|
|
1716
1812
|
if (out.length >= BEAT.MAX) break;
|
|
1717
1813
|
}
|
|
@@ -1882,11 +1978,11 @@ async function autoPushIfAhead(reply, recs) {
|
|
|
1882
1978
|
for (const v of reply) {
|
|
1883
1979
|
if (v.verdict !== BEAT_VERDICT.AHEAD || !(v.flags & BEAT_WANT_PUSH)) continue;
|
|
1884
1980
|
const rec = recs.find((r) => r.uuid === v.uuid);
|
|
1885
|
-
if (!rec?.
|
|
1981
|
+
if (!rec?._tenant_arg) continue;
|
|
1886
1982
|
if (now - (autoPushAt.get(v.uuid) || 0) < AUTO_PUSH_MIN_MS) continue;
|
|
1887
1983
|
autoPushAt.set(v.uuid, now);
|
|
1888
1984
|
try {
|
|
1889
|
-
const { r } = await pushSession(rec.
|
|
1985
|
+
const { r } = await pushSession(rec._tenant_arg, v.uuid, '');
|
|
1890
1986
|
process.stderr.write(`genetics: ${v.uuid.slice(0, 8)} 를 자동으로 올렸다 — ${r.lines}줄(서버가 낡아 있었다).\n`);
|
|
1891
1987
|
} catch (e) {
|
|
1892
1988
|
// 실패는 알리되 멈추지 않는다. 다음 심박이 다시 시도한다.
|
|
@@ -1975,10 +2071,10 @@ function observeTick() {
|
|
|
1975
2071
|
/** 이 세션의 색인 항목이 아는 소속처 이름 — 없으면 ''. `indexAdoptCurrent()` 가 같은 폴더 규칙으로 채운다. */
|
|
1976
2072
|
function observeTenant() {
|
|
1977
2073
|
try {
|
|
1978
|
-
const mine =
|
|
2074
|
+
const mine = mySessionId();
|
|
1979
2075
|
if (!mine) return '';
|
|
1980
2076
|
const e = indexRead()[mine];
|
|
1981
|
-
return (e && e.tenant_id
|
|
2077
|
+
return (e && e.tenant_id) ? tenantArgOf(e) : '';
|
|
1982
2078
|
} catch { return ''; }
|
|
1983
2079
|
}
|
|
1984
2080
|
|
|
@@ -2060,8 +2156,8 @@ const TOOLS = [
|
|
|
2060
2156
|
{
|
|
2061
2157
|
name: 'list_my_tenants',
|
|
2062
2158
|
description:
|
|
2063
|
-
'**내가 속한 곳 목록.** 다른 도구가 요구하는 `tenant`
|
|
2064
|
-
+ '
|
|
2159
|
+
'**내가 속한 곳 목록.** 다른 도구가 요구하는 `tenant` 값(slug, 이름도 받는다)을 여기서 얻는다.\n'
|
|
2160
|
+
+ '값을 모르는 채로 `tenant` 를 **지어내지 말 것** — 이것부터 부르고, 여럿이면 사용자에게 고르게 한다.\n'
|
|
2065
2161
|
+ '지금 폴더에 대해 서버가 **본 것**을 함께 준다 — 「이 자리는 지금까지 어디에서 올라왔나」. '
|
|
2066
2162
|
+ '그것은 **기록이지 근거가 아니다**: 서버가 한 곳으로 좁히지 못하거나 자리를 못 읽으면 '
|
|
2067
2163
|
+ '그렇게 말한다. 좁혀지지 않으면 **사용자에게 물어라.**',
|
|
@@ -2127,7 +2223,7 @@ const TOOLS = [
|
|
|
2127
2223
|
inputSchema: {
|
|
2128
2224
|
type: 'object',
|
|
2129
2225
|
properties: {
|
|
2130
|
-
tenant: { type: 'string', description: '문서를 받을 소속처
|
|
2226
|
+
tenant: { type: 'string', description: '문서를 받을 소속처 slug(이름도 받는다 — slug 는 list_my_tenants 가 알려 준다). 키가 가리키는 곳과 다르면 거부된다(오발행 방지).' },
|
|
2131
2227
|
slug: { type: 'string', description: '파일 이름이 될 식별자. 영문 소문자·숫자·하이픈 (예: fixes-20260812)' },
|
|
2132
2228
|
published_at: { type: 'string', description: 'YYYY-MM-DD (생략하면 오늘)' },
|
|
2133
2229
|
publish: {
|
|
@@ -2151,7 +2247,7 @@ const TOOLS = [
|
|
|
2151
2247
|
inputSchema: {
|
|
2152
2248
|
type: 'object',
|
|
2153
2249
|
properties: {
|
|
2154
|
-
tenant: { type: 'string', description: '대상 소속처
|
|
2250
|
+
tenant: { type: 'string', description: '대상 소속처 slug(이름도 받는다). 키와 다르면 거부된다.' },
|
|
2155
2251
|
id: { type: 'integer' },
|
|
2156
2252
|
published_at: { type: 'string', description: 'YYYY-MM-DD (생략하면 오늘)' },
|
|
2157
2253
|
},
|
|
@@ -2166,7 +2262,7 @@ const TOOLS = [
|
|
|
2166
2262
|
inputSchema: {
|
|
2167
2263
|
type: 'object',
|
|
2168
2264
|
properties: {
|
|
2169
|
-
tenant: { type: 'string', description: '대상 소속처
|
|
2265
|
+
tenant: { type: 'string', description: '대상 소속처 slug(이름도 받는다). 키와 다르면 거부된다.' },
|
|
2170
2266
|
id: { type: 'integer' },
|
|
2171
2267
|
reason: { type: 'string', description: '왜 내리는지. 서버 로그에 남아 다음 사람이 이유를 안다.' },
|
|
2172
2268
|
},
|
|
@@ -2195,7 +2291,7 @@ const TOOLS = [
|
|
|
2195
2291
|
inputSchema: {
|
|
2196
2292
|
type: 'object',
|
|
2197
2293
|
properties: {
|
|
2198
|
-
tenant: { type: 'string', description: '대상 소속처
|
|
2294
|
+
tenant: { type: 'string', description: '대상 소속처 slug(이름도 받는다). 키와 다르면 거부된다(오발행 방지).' },
|
|
2199
2295
|
id: { type: 'integer' },
|
|
2200
2296
|
dry_run: {
|
|
2201
2297
|
type: 'boolean',
|
|
@@ -2228,7 +2324,7 @@ const TOOLS = [
|
|
|
2228
2324
|
inputSchema: {
|
|
2229
2325
|
type: 'object',
|
|
2230
2326
|
properties: {
|
|
2231
|
-
tenant: { type: 'string', description: '대상 소속처
|
|
2327
|
+
tenant: { type: 'string', description: '대상 소속처 slug(이름도 받는다 — list_my_tenants 참조).' },
|
|
2232
2328
|
title: { type: 'string', description: '한 줄 제목. 파일 이름이 된다(같은 제목이면 덮어쓴다).' },
|
|
2233
2329
|
symptom: { type: 'string', description: '겪은 현상. 원인이 아니라 **눈에 보인 것**을 그대로.' },
|
|
2234
2330
|
cause: { type: 'string', description: '원인. 모르면 비운다 — 지어내지 않는다.' },
|
|
@@ -2260,7 +2356,7 @@ const TOOLS = [
|
|
|
2260
2356
|
inputSchema: {
|
|
2261
2357
|
type: 'object',
|
|
2262
2358
|
properties: {
|
|
2263
|
-
tenant: { type: 'string', description: '대상 소속처
|
|
2359
|
+
tenant: { type: 'string', description: '대상 소속처 slug(이름도 받는다 — list_my_tenants 참조).' },
|
|
2264
2360
|
q: { type: 'string', description: '증상 문장. 원인이 아니라 현상으로.' },
|
|
2265
2361
|
limit: { type: 'integer', minimum: 1, maximum: 20, description: '몇 건까지(기본 5).' },
|
|
2266
2362
|
fast: { type: 'boolean', description: '의미 확장을 건너뛴다(빠르지만 덜 찾는다). 기본은 확장 켬 — '
|
|
@@ -2278,7 +2374,7 @@ const TOOLS = [
|
|
|
2278
2374
|
inputSchema: {
|
|
2279
2375
|
type: 'object',
|
|
2280
2376
|
properties: {
|
|
2281
|
-
tenant: { type: 'string', description: '대상 소속처
|
|
2377
|
+
tenant: { type: 'string', description: '대상 소속처 slug(이름도 받는다 — list_my_tenants 참조).' },
|
|
2282
2378
|
path: { type: 'string', description: 'search_cases 결과의 path 그대로(예: cases/….md).' },
|
|
2283
2379
|
},
|
|
2284
2380
|
required: ['tenant', 'path'],
|
|
@@ -2294,7 +2390,7 @@ const TOOLS = [
|
|
|
2294
2390
|
inputSchema: {
|
|
2295
2391
|
type: 'object',
|
|
2296
2392
|
properties: {
|
|
2297
|
-
tenant: { type: 'string', description: '대상 소속처
|
|
2393
|
+
tenant: { type: 'string', description: '대상 소속처 slug(이름도 받는다 — list_my_tenants 참조).' },
|
|
2298
2394
|
path: { type: 'string', description: '사례 경로(cases/….md).' },
|
|
2299
2395
|
verdict: { type: 'string', enum: ['applied', 'refuted', 'irrelevant'], description: '사람이 말한 판정.' },
|
|
2300
2396
|
note: { type: 'string', description: '한 줄 메모(선택, 200자).' },
|
|
@@ -2312,7 +2408,7 @@ const TOOLS = [
|
|
|
2312
2408
|
inputSchema: {
|
|
2313
2409
|
type: 'object',
|
|
2314
2410
|
properties: {
|
|
2315
|
-
tenant: { type: 'string', description: '대상 소속처
|
|
2411
|
+
tenant: { type: 'string', description: '대상 소속처 slug(이름도 받는다 — list_my_tenants 참조).' },
|
|
2316
2412
|
title: { type: 'string', description: '검토 제목 — 메일 제목 「충돌 — <제목>」의 <제목> 그대로.' },
|
|
2317
2413
|
verdict: { type: 'string', enum: ['applied', 'refuted'], description: '이 세션의 판정.' },
|
|
2318
2414
|
type: { type: 'string', enum: ['contradiction', 'duplicate', 'missing-page'], description: '검토 종류(선택 — 충돌=contradiction·중복=duplicate·문서 필요=missing-page). 비우면 서버가 셋 중 있는 것을 고른다.' },
|
|
@@ -2335,7 +2431,7 @@ const TOOLS = [
|
|
|
2335
2431
|
inputSchema: {
|
|
2336
2432
|
type: 'object',
|
|
2337
2433
|
properties: {
|
|
2338
|
-
tenant: { type: 'string', description: '대상 소속처
|
|
2434
|
+
tenant: { type: 'string', description: '대상 소속처 slug(이름도 받는다 — list_my_tenants 참조).' },
|
|
2339
2435
|
file: { type: 'integer', minimum: 1, description: '문서 번호(목록의 #). 없으면 목록.' },
|
|
2340
2436
|
},
|
|
2341
2437
|
required: ['tenant'],
|
|
@@ -2350,7 +2446,7 @@ const TOOLS = [
|
|
|
2350
2446
|
inputSchema: {
|
|
2351
2447
|
type: 'object',
|
|
2352
2448
|
properties: {
|
|
2353
|
-
tenant: { type: 'string', description: '대상 소속처
|
|
2449
|
+
tenant: { type: 'string', description: '대상 소속처 slug(이름도 받는다 — list_my_tenants 참조).' },
|
|
2354
2450
|
page: {
|
|
2355
2451
|
type: 'integer', minimum: 1, maximum: 1000,
|
|
2356
2452
|
description: '목록 page(1부터, 최대 1000). 응답이 알려 준 next를 넣는다.',
|
|
@@ -2375,7 +2471,7 @@ const TOOLS = [
|
|
|
2375
2471
|
inputSchema: {
|
|
2376
2472
|
type: 'object',
|
|
2377
2473
|
properties: {
|
|
2378
|
-
tenant: { type: 'string', description: '대상 소속처
|
|
2474
|
+
tenant: { type: 'string', description: '대상 소속처 slug(이름도 받는다 — list_my_tenants 참조).' },
|
|
2379
2475
|
uuid: { type: 'string', description: '세션 id. `--resume` 에 쓰는 그 값.' },
|
|
2380
2476
|
title: { type: 'string', description: '목록에서 알아볼 한 줄. 생략하면 빈 값.' },
|
|
2381
2477
|
dry_run: { type: 'boolean', description: '참이면 아무것도 저장하지 않고 검열 결과만 돌려준다.' },
|
|
@@ -2399,7 +2495,7 @@ const TOOLS = [
|
|
|
2399
2495
|
inputSchema: {
|
|
2400
2496
|
type: 'object',
|
|
2401
2497
|
properties: {
|
|
2402
|
-
tenant: { type: 'string', description: '대상 소속처
|
|
2498
|
+
tenant: { type: 'string', description: '대상 소속처 slug(이름도 받는다 — list_my_tenants 참조).' },
|
|
2403
2499
|
uuid: { type: 'string', description: '이어받을 세션 id.' },
|
|
2404
2500
|
into: { type: 'string', description: '원본 파일을 놓을 자리(생략하면 지금 폴더). '
|
|
2405
2501
|
+ '`--resume` 은 그 폴더의 트랜스크립트를 찾으므로 이 값이 곧 원본을 열 자리다.' },
|
|
@@ -2419,7 +2515,7 @@ const TOOLS = [
|
|
|
2419
2515
|
inputSchema: {
|
|
2420
2516
|
type: 'object',
|
|
2421
2517
|
properties: {
|
|
2422
|
-
tenant: { type: 'string', description: '대상 소속처
|
|
2518
|
+
tenant: { type: 'string', description: '대상 소속처 slug(이름도 받는다 — list_my_tenants 참조).' },
|
|
2423
2519
|
uuid: { type: 'string', description: '받을 세션 id.' },
|
|
2424
2520
|
into: { type: 'string', description: '어느 디렉터리에서 이어 갈 것인가(생략하면 지금 폴더). '
|
|
2425
2521
|
+ '`--resume` 은 그 폴더의 트랜스크립트를 찾으므로 이 값이 곧 이어 갈 자리다.' },
|
|
@@ -2447,7 +2543,7 @@ const TOOLS = [
|
|
|
2447
2543
|
inputSchema: {
|
|
2448
2544
|
type: 'object',
|
|
2449
2545
|
properties: {
|
|
2450
|
-
tenant: { type: 'string', description: '소속처
|
|
2546
|
+
tenant: { type: 'string', description: '소속처 slug(이름도 받는다). 모르면 list_my_tenants 로 먼저 확인한다' },
|
|
2451
2547
|
to: { type: 'string', description: '받는 쪽 — 세포 이름 · 세션 uuid · 또는 brain' },
|
|
2452
2548
|
body: { type: 'string', description: '할 말' },
|
|
2453
2549
|
subject: { type: 'string', description: '한 줄 제목(선택)' },
|
|
@@ -2693,7 +2789,7 @@ let tenantSaidAt = 0;
|
|
|
2693
2789
|
* **한 번도 안 돌았다**(`world-<root>.json` 의 `cursor` 가 null 이었다).
|
|
2694
2790
|
*/
|
|
2695
2791
|
function tenantNotice() {
|
|
2696
|
-
const mine =
|
|
2792
|
+
const mine = mySessionId();
|
|
2697
2793
|
if (!mine) return '';
|
|
2698
2794
|
try {
|
|
2699
2795
|
if (indexRead()[mine]?.tenant_id) return ''; // 붙어 있다 — 조용하다
|
|
@@ -2797,7 +2893,7 @@ let brainCursor = null; // null = 아직 색인에서 안 읽었다
|
|
|
2797
2893
|
const brainQueue = []; // 아직 안 말한 모순
|
|
2798
2894
|
|
|
2799
2895
|
async function brainPull() {
|
|
2800
|
-
const mine =
|
|
2896
|
+
const mine = mySessionId();
|
|
2801
2897
|
if (!mine) return;
|
|
2802
2898
|
const e = indexRead()[mine];
|
|
2803
2899
|
if (!e?.tenant || !e?.tenant_id) return; // 아직 어느 소속처인지 모른다
|
|
@@ -2812,7 +2908,7 @@ async function brainPull() {
|
|
|
2812
2908
|
const cwd = relHome(process.cwd());
|
|
2813
2909
|
const fresh = [];
|
|
2814
2910
|
for (let page = 0; page < 10; page++) {
|
|
2815
|
-
const r = await api('GET', '/api/world?tenant=' + encodeURIComponent(e
|
|
2911
|
+
const r = await api('GET', '/api/world?tenant=' + encodeURIComponent(tenantArgOf(e))
|
|
2816
2912
|
+ '&since=' + brainCursor + '&cwd=' + encodeURIComponent(cwd) + '&limit=' + BRAIN_PAGE);
|
|
2817
2913
|
const evs = Array.isArray(r?.events) ? r.events : [];
|
|
2818
2914
|
for (const ev of evs) if (ev?.kind === 'review' && ev?.ref) fresh.push(ev);
|
|
@@ -2960,10 +3056,18 @@ function introNameOf(line) {
|
|
|
2960
3056
|
}
|
|
2961
3057
|
let nameCache = null; // { file, size, name }
|
|
2962
3058
|
function myAgentName() {
|
|
2963
|
-
const uuid =
|
|
3059
|
+
const uuid = mySessionId();
|
|
2964
3060
|
if (!/^[0-9a-f-]{36}$/i.test(uuid)) return null;
|
|
2965
3061
|
const f = transcriptOf(uuid);
|
|
2966
|
-
if (!f)
|
|
3062
|
+
if (!f) {
|
|
3063
|
+
/*
|
|
3064
|
+
* Cursor 활성 대화 — 전사도 ListAgents 도 없다. 이름은 별칭일 뿐 열쇠는 uuid 이므로
|
|
3065
|
+
* (mail 레일 계약), 출처가 읽히는 결정적 별칭을 낸다: cursor-<uuid 앞 2자>.
|
|
3066
|
+
* 하네스 이름(app-2c)과 같은 문법이라 받는 쪽 화면·회신 규칙이 그대로 통한다.
|
|
3067
|
+
*/
|
|
3068
|
+
if (uuid === cursorActiveComposer()) return 'cursor-' + uuid.slice(0, 2);
|
|
3069
|
+
return null;
|
|
3070
|
+
}
|
|
2967
3071
|
let st;
|
|
2968
3072
|
try { st = fs.statSync(f); } catch { return null; }
|
|
2969
3073
|
if (nameCache?.file === f && nameCache.size === st.size) return nameCache.name;
|
|
@@ -3013,15 +3117,15 @@ function myAgentName() {
|
|
|
3013
3117
|
*/
|
|
3014
3118
|
const mailTenants = new Map(); // tenant 이름 → tenant_id(모르면 0). mail 도구가 성공할 때 적는다
|
|
3015
3119
|
function mailReady() {
|
|
3016
|
-
const mine =
|
|
3120
|
+
const mine = mySessionId();
|
|
3017
3121
|
if (!mine) return null;
|
|
3018
3122
|
const cred = credential();
|
|
3019
3123
|
if (!cred || cred.kind !== 'user') return null; // 세계 장부와 같다 — 서버가 사람 토큰만 받는다
|
|
3020
3124
|
const e = indexRead()[mine];
|
|
3021
3125
|
const set = new Map(mailTenants);
|
|
3022
|
-
|
|
3126
|
+
{ const a = tenantArgOf(e); if (a && !set.has(a)) set.set(a, Number(e.tenant_id) || 0); }
|
|
3023
3127
|
if (!set.size) return null; // 아직 어느 소속처에도 주소를 안 댔다
|
|
3024
|
-
return { mine, tenants: set, indexTenant: e
|
|
3128
|
+
return { mine, tenants: set, indexTenant: tenantArgOf(e), indexEntry: e };
|
|
3025
3129
|
}
|
|
3026
3130
|
|
|
3027
3131
|
/**
|
|
@@ -3037,7 +3141,8 @@ async function mailRegister(tenant, mine, force = false) {
|
|
|
3037
3141
|
tenant,
|
|
3038
3142
|
session: mine,
|
|
3039
3143
|
name: name || '',
|
|
3040
|
-
|
|
3144
|
+
// 신원이 어디서 왔는지 그대로 말한다 — Cursor 대화에 claude-code 이름표를 달지 않는다.
|
|
3145
|
+
client: mine === cursorActiveComposer() ? 'cursor' : 'claude-code',
|
|
3041
3146
|
cwd: relHome(process.cwd()),
|
|
3042
3147
|
});
|
|
3043
3148
|
}
|
|
@@ -3078,7 +3183,7 @@ async function mailPull() {
|
|
|
3078
3183
|
const r = await api('GET', '/api/sessions/' + encodeURIComponent(mine)
|
|
3079
3184
|
+ '?tenant=' + encodeURIComponent(indexTenant) + '&no_body=1');
|
|
3080
3185
|
if (r?.bake) {
|
|
3081
|
-
indexNote(mine, { tenant:
|
|
3186
|
+
indexNote(mine, { tenant: indexEntry.tenant, tenant_id: indexEntry.tenant_id, bake: r.bake });
|
|
3082
3187
|
}
|
|
3083
3188
|
} catch { /* 옛 서버는 이 칸이 없다 — 없으면 상태바가 「모름」으로 둔다 */ }
|
|
3084
3189
|
}
|
|
@@ -3106,7 +3211,7 @@ async function mailPullOne(tenant, mine, indexEntry) {
|
|
|
3106
3211
|
// 색인에는 색인의 소속처 것만 적는다(다른 곳의 커서는 프로세스 안에서만 — 잃어도 ack 가 중복을 막는다).
|
|
3107
3212
|
// 빈 폴링은 수신이 아니다 — 위 백신 커서와 같은 규율.
|
|
3108
3213
|
if (indexEntry?.tenant_id) {
|
|
3109
|
-
indexNote(mine, { tenant, tenant_id: indexEntry.tenant_id, mail_cursor: cursor },
|
|
3214
|
+
indexNote(mine, { tenant: indexEntry.tenant, tenant_id: indexEntry.tenant_id, mail_cursor: cursor },
|
|
3110
3215
|
rows.length ? 'rx' : '');
|
|
3111
3216
|
}
|
|
3112
3217
|
|
|
@@ -3337,13 +3442,17 @@ async function callTool(name, args = {}) {
|
|
|
3337
3442
|
const mark = (t) => (t.has_logo === false ? ' · 로고 없음' : '');
|
|
3338
3443
|
|
|
3339
3444
|
if (list.length === 1) {
|
|
3340
|
-
|
|
3341
|
-
|
|
3445
|
+
// 옛 서버는 slug 칸이 없다 — 그때는 이름만 말한다(없는 값을 지어내지 않는다).
|
|
3446
|
+
const slug0 = typeof list[0].slug === 'string' && list[0].slug !== '' ? list[0].slug : '';
|
|
3447
|
+
return `속한 곳이 하나입니다 — **${list[0].name}**${slug0 ? ` (\`${slug0}\`)` : ''}${mark(list[0])}\n`
|
|
3448
|
+
+ (slug0 ? '다른 도구의 `tenant` 에는 slug 를 쓰세요(이름도 받는다).'
|
|
3449
|
+
: '다른 도구의 `tenant` 에 이 이름을 그대로 쓰세요.')
|
|
3342
3450
|
+ (says ? `\n\n${says}` : '');
|
|
3343
3451
|
}
|
|
3344
3452
|
const lines = list.map((t) => {
|
|
3345
3453
|
const when = t.last_used_at ? String(t.last_used_at).slice(0, 10) : '기록 없음';
|
|
3346
|
-
|
|
3454
|
+
const sl = typeof t.slug === 'string' && t.slug !== '' ? ` (\`${t.slug}\`)` : '';
|
|
3455
|
+
return ` · ${t.name}${sl}${seen(t)}${mark(t)} · 최근 ${when}`;
|
|
3347
3456
|
});
|
|
3348
3457
|
return `속한 곳 ${list.length}곳입니다. **사용자에게 어디로 할지 물어보세요** — 고르지 마세요.\n`
|
|
3349
3458
|
+ `${lines.join('\n')}\n`
|
|
@@ -3572,7 +3681,8 @@ async function callTool(name, args = {}) {
|
|
|
3572
3681
|
+ r.hits.map((h) => `· ${h.path} [${h.score}점]${h.status ? `\n ⚠ 상태: ${h.status}` : ''}`
|
|
3573
3682
|
+ `\n ${h.excerpt}`).join('\n')
|
|
3574
3683
|
+ '\n전문은 get_case(tenant, path) — 처방·상태까지 그대로 읽는다.'
|
|
3575
|
-
+ (
|
|
3684
|
+
+ (typeof r.human_url === 'string' && r.human_url !== '' ? ` 사람 화면: ${r.human_url}?p=<path>`
|
|
3685
|
+
: Number.isInteger(r.tenant_id) ? ` 사람 화면: wiki?t=${r.tenant_id}&p=<path>` : '');
|
|
3576
3686
|
}
|
|
3577
3687
|
case 'get_case': {
|
|
3578
3688
|
// 서버 창구(/api/wiki/page)는 이미 있었다 — MCP 가 안 부르고 있었을 뿐이다(2026-08-25).
|
|
@@ -3662,7 +3772,7 @@ async function callTool(name, args = {}) {
|
|
|
3662
3772
|
// **본 것을 적어 둔다.** 이 짝(uuid → 고객사 id)이 없으면 심박을 보낼 수가 없다 —
|
|
3663
3773
|
// 프레임에는 고객사 이름이 아니라 id 가 실린다.
|
|
3664
3774
|
for (const s of sessions) {
|
|
3665
|
-
if (!s.deleted) indexNote(s.uuid, { tenant: r.tenant, tenant_id: s.tenant_id, head_id: s.head_id, head_ms: s.head_ms });
|
|
3775
|
+
if (!s.deleted) indexNote(s.uuid, { tenant: r.tenant, tenant_id: s.tenant_id, head_id: s.head_id, head_ms: s.head_ms, ...(r.tenant_slug ? { tenant_slug: r.tenant_slug } : {}) });
|
|
3666
3776
|
}
|
|
3667
3777
|
const p = r.pagination && typeof r.pagination === 'object' ? r.pagination : null;
|
|
3668
3778
|
let count = `세션 ${sessions.length}건`;
|
|
@@ -3738,7 +3848,7 @@ async function callTool(name, args = {}) {
|
|
|
3738
3848
|
const q = `?tenant=${encodeURIComponent(args.tenant)}`;
|
|
3739
3849
|
const r = await api('GET',
|
|
3740
3850
|
`/api/sessions/${encodeURIComponent(args.uuid)}${q}&no_body=1&digest=1&digest_chars=${chars}`);
|
|
3741
|
-
indexNote(args.uuid, { tenant: r.tenant || args.tenant, tenant_id: r.tenant_id, head_id: r.head_id, head_ms: r.head_ms }, 'rx');
|
|
3851
|
+
indexNote(args.uuid, { tenant: r.tenant || args.tenant, tenant_id: r.tenant_id, head_id: r.head_id, head_ms: r.head_ms, ...(r.tenant_slug ? { tenant_slug: r.tenant_slug } : {}) }, 'rx');
|
|
3742
3852
|
|
|
3743
3853
|
// 원본도 제자리에 놓는다 — **덮지 않는다**(pull_session 과 같은 이유).
|
|
3744
3854
|
const into = args.into || process.cwd();
|
|
@@ -3799,7 +3909,7 @@ async function callTool(name, args = {}) {
|
|
|
3799
3909
|
`/api/sessions/${encodeURIComponent(args.uuid)}?tenant=${encodeURIComponent(args.tenant)}&raw=1`,
|
|
3800
3910
|
target);
|
|
3801
3911
|
const m = limMeta(headers);
|
|
3802
|
-
indexNote(args.uuid, { tenant: args.tenant, tenant_id: m.tenant_id, head_id: m.head_id, head_ms: m.head_ms }, 'rx');
|
|
3912
|
+
indexNote(args.uuid, { tenant: args.tenant, tenant_id: m.tenant_id, head_id: m.head_id, head_ms: m.head_ms, ...(m.tenant_slug ? { tenant_slug: m.tenant_slug } : {}) }, 'rx');
|
|
3803
3913
|
// 헤더를 안 주는 옛 서버면 **지어내지 않는다** — 받은 바이트만 말한다.
|
|
3804
3914
|
const size = `${((m.bytes ?? bytes) / 1024).toFixed(0)}KB`;
|
|
3805
3915
|
return `받았습니다 — ${args.uuid.slice(0, 8)} · ${m.lines === null ? '' : `${m.lines}줄 · `}${size}\n`
|
|
@@ -3808,10 +3918,10 @@ async function callTool(name, args = {}) {
|
|
|
3808
3918
|
+ (m.redacted ? `※ 가린 자리 ${m.redacted}곳 — 도구 실행 결과는 안 실려 옵니다. 필요하면 다시 실행하세요.` : '');
|
|
3809
3919
|
}
|
|
3810
3920
|
case 'mail': {
|
|
3811
|
-
const mine =
|
|
3921
|
+
const mine = mySessionId();
|
|
3812
3922
|
if (!mine) {
|
|
3813
3923
|
return '이 세션의 번호를 모른다 — 주소를 댈 수 없어 보낼 수 없다'
|
|
3814
|
-
+ '(CLAUDE_CODE_SESSION_ID
|
|
3924
|
+
+ '(CLAUDE_CODE_SESSION_ID 도, Cursor 활성 대화도 없는 환경이다).';
|
|
3815
3925
|
}
|
|
3816
3926
|
/*
|
|
3817
3927
|
* 주소가 없으면 먼저 댄다. **이름이 없으면 서버가 409 로 거절**하므로,
|
|
@@ -3853,7 +3963,7 @@ async function callTool(name, args = {}) {
|
|
|
3853
3963
|
// ⚠ 서버가 tenant_id 를 아직 안 주는 판(재배포 전)이어도 **받기는 이름만으로 된다** —
|
|
3854
3964
|
// GET /api/mail 은 tenant 이름으로 묻는다. 색인(indexNote)만 id 가 있어야 적힌다.
|
|
3855
3965
|
mailTenants.set(args.tenant, r?.tenant_id || 0); // 이 소속처에서도 받는다 — 합집합, 폴백 아님
|
|
3856
|
-
if (r?.tenant_id && !indexRead()[mine]?.tenant) indexNote(mine, { tenant: args.tenant, tenant_id: r.tenant_id });
|
|
3966
|
+
if (r?.tenant_id && !indexRead()[mine]?.tenant) indexNote(mine, { tenant: args.tenant, tenant_id: r.tenant_id, ...(r.tenant_slug ? { tenant_slug: r.tenant_slug } : {}) });
|
|
3857
3967
|
return heldNote + `«${r.to}» 앞으로 보냈다 — 실 ${r.thread} · ${r.turn}번째.\n`
|
|
3858
3968
|
+ `받는 쪽이 보게 될 글자는 이것이다:\n${r.preview}\n`
|
|
3859
3969
|
+ (r.says ? `${r.says}\n` : '')
|
|
@@ -3977,7 +4087,8 @@ async function handle(line) {
|
|
|
3977
4087
|
'',
|
|
3978
4088
|
'자격은 **`login` 으로 받는 SSO 하나면 된다** — 그것으로 내가 속한 소속처 전부에 닿는다.',
|
|
3979
4089
|
'키(rpk_)도 쓸 수 있지만 **한 소속처에만** 매이고, 발행할 때만 그쪽이 먼저 쓰인다.',
|
|
3980
|
-
'어디로 갈지는 tenant 에 소속처
|
|
4090
|
+
'어디로 갈지는 tenant 에 소속처 **slug** 로 적는다(이름도 받는다 — list_my_tenants 가 둘 다 알려 준다).',
|
|
4091
|
+
'값을 모르면 지어내지 말고 사용자에게 묻는다 —',
|
|
3981
4092
|
'내 소속이 아닌 곳은 존재도 안 보이므로, 404 는 "없다"가 아니라 "내 것이 아니다"일 수 있다.',
|
|
3982
4093
|
].join('\n'),
|
|
3983
4094
|
};
|
package/package.json
CHANGED
|
@@ -138,7 +138,7 @@ ls -t ~/.claude/projects/<프로젝트-슬러그>/*.jsonl | head -1
|
|
|
138
138
|
|---|---|
|
|
139
139
|
| 내 세션 uuid | 전사 파일 이름 |
|
|
140
140
|
| 내가 어느 사람·어느 권한으로 붙어 있나 | `whoami` 도구 |
|
|
141
|
-
| 내가 닿는 소속처 목록 | `list_my_tenants` 도구 |
|
|
141
|
+
| 내가 닿는 소속처 목록(slug 포함) | `list_my_tenants` 도구 |
|
|
142
142
|
| 내 세션이 서버에 올라가 있나 | `list_sessions` 도구 — 목록에 내 앞자리가 있는지 본다 |
|
|
143
143
|
|
|
144
144
|
## 말 걸기
|
|
@@ -155,13 +155,13 @@ ls -t ~/.claude/projects/<프로젝트-슬러그>/*.jsonl | head -1
|
|
|
155
155
|
|
|
156
156
|
```
|
|
157
157
|
SendMessage({to: "app-2f", message: "…"}) # 같은 기계
|
|
158
|
-
mail({tenant: "
|
|
158
|
+
mail({tenant: "<소속처 slug>", to: "brain", body: "…"}) # 기계를 넘어서
|
|
159
159
|
```
|
|
160
160
|
|
|
161
161
|
받은 것에 답할 때 `SendMessage` 는 그 메시지의 `from` 을 그대로 `to` 에 쓰고, `mail` 은 `thread` 를 씁니다.
|
|
162
162
|
|
|
163
163
|
`mail` 의 `to` 에 올 수 있는 것은 **셋뿐**입니다. **지어내지 마세요** — 셋 중 하나가 아니면
|
|
164
|
-
사용자에게 묻습니다. 소속처
|
|
164
|
+
사용자에게 묻습니다. 소속처 값(slug — 이름도 받는다)을 모르면 `list_my_tenants` 로 먼저 확인합니다.
|
|
165
165
|
|
|
166
166
|
| `to` | 언제 쓰나 |
|
|
167
167
|
|---|---|
|
|
@@ -70,7 +70,7 @@ version: "4.0.0"
|
|
|
70
70
|
**Lim 발행 규칙** (어겨도 서버가 거부하지만, 알고 시작할 것):
|
|
71
71
|
- **톤은 개조식이다** — `~함 / ~했음 / ~필요 / ~한 상황`. `~합니다 / ~해 주세요` 아님.
|
|
72
72
|
본문에 `습니다|십시오|합니다|입니다|됩니다|주세요` 가 **0건**이어야 한다.
|
|
73
|
-
- `tenant` 는 키가 가리키는 고객사와 일치해야 한다(오발행 방지 가드).
|
|
73
|
+
- `tenant` 는 키가 가리키는 고객사와 일치해야 한다(오발행 방지 가드). 값은 소속처 slug 우선·이름 폴백 — 아래 :228 의 `slug` 는 **보고서** slug 라 다른 것이다.
|
|
74
74
|
- `sections[].kind` 는 `summary|status|cases|asks|note` 다섯 가지뿐 — 그 밖의 값은 절이 통째로 사라지므로 서버가 거절한다.
|
|
75
75
|
- `cases[].grade` 는 `grades` 에 정의된 이름이어야 한다.
|
|
76
76
|
- **`update_report` 는 같은 주소를 덮어쓴다** — 이미 고객에게 나간 링크가 새 내용을 가리키므로, 발행 후 수정은 재발행이 아니라 update 로 한다. 기존 문서는 `list_reports`/`get_report` 로 받아 고쳐 되돌려 보낸다.
|
|
@@ -57,7 +57,7 @@ version: "1.0.0"
|
|
|
57
57
|
적혀 있으면(자동 올리기가 켜진 세션) 저절로 섭니다. 둘 다 아니면 사람이 한 번 칩니다:
|
|
58
58
|
```bash
|
|
59
59
|
GC=$(grep -m1 '^GENETICS_CELL_BIN=' ~/.config/genetics/api.env | cut -d= -f2-); GC=${GC:-~/.config/genetics/bin/gcell}
|
|
60
|
-
"$GC" sync --tenant <소속처
|
|
60
|
+
"$GC" sync --tenant <소속처 slug|이름>
|
|
61
61
|
```
|
|
62
62
|
2. **내 세션이 서버에 올라가 있습니다.** `list_sessions` 목록에 내 앞자리가 있는지 봅니다.
|
|
63
63
|
소속은 **올라온 세션에만** 붙습니다 — 안 올라간 세션은 어느 일에도 속할 수 없습니다.
|