axmap-cli 0.3.0 → 1.0.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/.claude/settings.local.json +32 -0
- package/README.md +5 -2
- package/bin/axmap.mjs +72 -2
- package/package.json +1 -1
- package/src/invariants.mjs +79 -7
- package/src/mcpstate.mjs +90 -0
- package/tools/mcp-register.mjs +4 -10
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"permissions": {
|
|
3
|
+
"allow": [
|
|
4
|
+
"Bash(sed -i 's|/\\\\*\\\\* 궤도 모드에서 허용하는 yaw 한계 \\(약 35°\\). cos 이 0.82 아래로 내려가지 않는다. \\\\*/\\\\nconst YAW_LIMIT = 0.62|X|' app/web/graph.js)",
|
|
5
|
+
"Bash(sed -i 's|const YAW_LIMIT = 0.62|const TILT_LIMIT = 0.62|; s|허용하는 yaw 한계|허용하는 기울기 한계 \\(yaw · pitch 공통\\)|' app/web/graph.js)",
|
|
6
|
+
"Bash(sed -i 's|인코딩은 인코딩이 아니다. cos\\(TILT_LIMIT\\)=0.82 이면 고리의 화면 반지름이\\\\n \\\\* \\\\[0.82R, R\\\\] 안에 머물러 이웃 단계와 절대 겹치지 않는다.|X|' app/web/graph.js)",
|
|
7
|
+
"Bash(sed -n '/인코딩은 인코딩이 아니다/,+2p' app/web/graph.js)",
|
|
8
|
+
"Bash(sed -i 's|import { applyEdits, draft, editRecord } from|import { applyEdits, draft } from|' app/server.mjs)",
|
|
9
|
+
"Bash(sed -i 's|\"test\": \"node --test test/protocol.test.mjs test/model.test.mjs test/analyze.test.mjs\"|\"test\": \"node --test test/protocol.test.mjs test/model.test.mjs test/analyze.test.mjs test/features.test.mjs\"|' package.json)",
|
|
10
|
+
"Bash(npm test *)",
|
|
11
|
+
"Bash(sed -i 's| .filter\\(\\(\\\\[t\\\\]\\) => t !== name \\\\&\\\\& !taken.has\\(t\\)\\)| .filter\\(\\([t]\\) => t !== name \\\\&\\\\& !taken.has\\(t\\) \\\\&\\\\& !tooCommon\\(t\\)\\)|' app/lib/features.mjs)",
|
|
12
|
+
"Bash(node -e ' *)",
|
|
13
|
+
"Bash(npm run *)",
|
|
14
|
+
"Bash(export AXMAP_AGENT=claude-fix GIT_AUTHOR_NAME=claude-fix GIT_AUTHOR_EMAIL=claude-fix@local GIT_COMMITTER_NAME=claude-fix GIT_COMMITTER_EMAIL=claude-fix@local)",
|
|
15
|
+
"Bash(node bin/axmap.mjs claim app/web/app.js app/web/graph.js app/web/style.css --task fix-click-color --intent \"이름 클릭=강조/더블클릭=이름변경, 단계 없을 때 색 회귀 수정\" --actor agent)",
|
|
16
|
+
"Bash(sed 's/\\\\$$//')",
|
|
17
|
+
"Bash(python -)",
|
|
18
|
+
"Bash(sed -n '/import 를 뽑을 수 있는 언어 이름/,/^\\\\]\\)/p' app/lib/analyze.mjs)",
|
|
19
|
+
"Bash(export AXMAP_AGENT=janghyojoon)",
|
|
20
|
+
"Bash(node bin/axmap.mjs release)",
|
|
21
|
+
"Bash(echo \"// 테스트\")",
|
|
22
|
+
"Bash(git add *)",
|
|
23
|
+
"Bash(git commit *)",
|
|
24
|
+
"Bash(git reset *)",
|
|
25
|
+
"Bash(git checkout *)"
|
|
26
|
+
]
|
|
27
|
+
},
|
|
28
|
+
"enabledMcpjsonServers": [
|
|
29
|
+
"axmap"
|
|
30
|
+
],
|
|
31
|
+
"enableAllProjectMcpServers": true
|
|
32
|
+
}
|
package/README.md
CHANGED
|
@@ -50,7 +50,9 @@ node app/server.mjs https://github.com/pallets/flask 7777
|
|
|
50
50
|
⑤ 이제 당신 차례
|
|
51
51
|
```
|
|
52
52
|
|
|
53
|
-
|
|
53
|
+
**남의 저장소에서 쓰려면** → [docs/INSTALL.md](docs/INSTALL.md) (npm 두 줄이면 끝난다)
|
|
54
|
+
|
|
55
|
+
**axMap 자체를 고치려면** → [docs/ONBOARD-TEAM.md](docs/ONBOARD-TEAM.md) (3줄이면 붙는다)
|
|
54
56
|
|
|
55
57
|
---
|
|
56
58
|
|
|
@@ -179,7 +181,8 @@ npm run smoke # 🔴 화면이 실제로 뜨는지 헤드리스 크롬
|
|
|
179
181
|
| | |
|
|
180
182
|
|---|---|
|
|
181
183
|
| [CLAUDE.md](CLAUDE.md) | 작업 규칙 (사람에게도 AI 에게도 같다) |
|
|
182
|
-
| [docs/
|
|
184
|
+
| [docs/INSTALL.md](docs/INSTALL.md) | **팀원이 읽을 것 하나** — npm 으로 깔고 쓰는 법 |
|
|
185
|
+
| [docs/ONBOARD-TEAM.md](docs/ONBOARD-TEAM.md) | axMap 자체를 고칠 사람용 5분 안내 |
|
|
183
186
|
| [docs/DECISIONS.md](docs/DECISIONS.md) | 제품 결정 D1~D15 와 **아직 못 정한 것들** |
|
|
184
187
|
| [docs/WHY-CORPUS.md](docs/WHY-CORPUS.md) | 코퍼스가 왜 필요한가 |
|
|
185
188
|
| [docs/PERSONA-LOOP.md](docs/PERSONA-LOOP.md) | 시각화가 실제로 일하는지 재는 법과 결과 |
|
package/bin/axmap.mjs
CHANGED
|
@@ -41,6 +41,7 @@ import {
|
|
|
41
41
|
noticeLine,
|
|
42
42
|
registryUrl,
|
|
43
43
|
} from '../src/update.mjs'
|
|
44
|
+
import { homeRegistrations } from '../src/mcpstate.mjs'
|
|
44
45
|
|
|
45
46
|
/** 이 CLI 가 들어 있는 axMap 폴더. 갱신 확인과 setup 이 자기 위치를 알아야 한다. */
|
|
46
47
|
const SELF_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
|
@@ -1631,6 +1632,51 @@ function cmdVerify(flags) {
|
|
|
1631
1632
|
// 명령: audit (사후 증명)
|
|
1632
1633
|
// ---------------------------------------------------------------------------
|
|
1633
1634
|
|
|
1635
|
+
/**
|
|
1636
|
+
* `--code <범위>` 가 가리키는 **코드 커밋**을 모은다 (I5 · 강제 검사의 입력).
|
|
1637
|
+
*
|
|
1638
|
+
* 🔴 **머지 커밋은 뺀다** (`--no-merges`). 서버가 만든 것이라 사람이 파일을 고른
|
|
1639
|
+
* 적이 없고, 그것을 위반으로 세면 MR 을 쓸수록 빨개진다 — 문을 세운 결과가
|
|
1640
|
+
* 문을 우회하는 것이 된다.
|
|
1641
|
+
*
|
|
1642
|
+
* 🔴 **범위를 인자로 받는다. 이력 전체를 검사하지 않는다.** 장부가 생기기 전에
|
|
1643
|
+
* 쌓인 커밋은 아무도 잡지 않은 채 고쳐진 것이 당연하고, 그것까지 세면 이 잡은
|
|
1644
|
+
* 켠 날부터 영원히 빨갛다. CI 는 이번 push 로 들어온 구간만 넘긴다.
|
|
1645
|
+
*/
|
|
1646
|
+
function collectCodeCommits(root, range) {
|
|
1647
|
+
const ZERO = '0000000000000000000000000000000000000000'
|
|
1648
|
+
if (range.includes(ZERO)) {
|
|
1649
|
+
// 새 브랜치의 첫 push 면 GitLab 이 "이전 sha" 자리에 0 을 넣는다. 비교할 앞이 없다.
|
|
1650
|
+
console.log(`구간에 빈 sha 가 있습니다 (${range}) - 첫 push 로 보고 코드 대조를 건너뜁니다.`)
|
|
1651
|
+
return null
|
|
1652
|
+
}
|
|
1653
|
+
const NUL = String.fromCharCode(0)
|
|
1654
|
+
const NL = String.fromCharCode(10)
|
|
1655
|
+
const log = git(['log', '--no-merges', '--format=%H%x00%an%x00%ct%x00%s', range], { cwd: root })
|
|
1656
|
+
if (log.code !== 0) {
|
|
1657
|
+
// 🔴 못 읽은 것을 통과로 내지 않는다. 조용히 빈 배열을 주면 "위반 0" 이 된다.
|
|
1658
|
+
die([
|
|
1659
|
+
`코드 구간을 읽지 못했습니다 (${range}).`,
|
|
1660
|
+
' 구간을 확인하세요 - 검사하지 못한 것을 통과로 내지 않습니다.',
|
|
1661
|
+
].join(NL))
|
|
1662
|
+
}
|
|
1663
|
+
const out = []
|
|
1664
|
+
for (const line of log.out ? log.out.split(NL) : []) {
|
|
1665
|
+
if (!line) continue
|
|
1666
|
+
const [sha, author, ct, subject] = line.split(NUL)
|
|
1667
|
+
const files = git(['show', '--no-renames', '--name-only', '--format=', sha], { cwd: root }).out
|
|
1668
|
+
out.push({
|
|
1669
|
+
sha,
|
|
1670
|
+
author,
|
|
1671
|
+
time: Number(ct) * 1000,
|
|
1672
|
+
subject,
|
|
1673
|
+
files: (files ? files.split(NL) : []).filter(Boolean),
|
|
1674
|
+
})
|
|
1675
|
+
}
|
|
1676
|
+
return out
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
|
|
1634
1680
|
function cmdAudit(flags) {
|
|
1635
1681
|
const root = repoRoot()
|
|
1636
1682
|
const dir = ledgerDir(root)
|
|
@@ -1702,7 +1748,11 @@ function cmdAudit(flags) {
|
|
|
1702
1748
|
snapshots.push({ commit: sha, time: Number(ct) * 1000, subject, claims })
|
|
1703
1749
|
}
|
|
1704
1750
|
|
|
1705
|
-
const
|
|
1751
|
+
const codeCommits = typeof flags.code === 'string' && flags.code
|
|
1752
|
+
? collectCodeCommits(root, flags.code)
|
|
1753
|
+
: null
|
|
1754
|
+
|
|
1755
|
+
const report = auditLedger(snapshots, codeCommits)
|
|
1706
1756
|
if (flags.json) {
|
|
1707
1757
|
console.log(JSON.stringify(report, null, 2))
|
|
1708
1758
|
} else {
|
|
@@ -1803,6 +1853,17 @@ function cmdDoctor(flags) {
|
|
|
1803
1853
|
}
|
|
1804
1854
|
|
|
1805
1855
|
// 7. MCP 설정 — AI 도구가 자동으로 붙는 경로
|
|
1856
|
+
//
|
|
1857
|
+
// 🔴 붙는 길은 **둘**이다. 저장소의 `.mcp.json` 과 각 도구의 홈 설정.
|
|
1858
|
+
// 예전에는 앞의 것만 봤다. 그래서 npm 판을 깔고 `axmap setup` 까지 끝낸 사람에게도
|
|
1859
|
+
// ".mcp.json 이 없습니다 — AI 도구가 자동으로 붙지 않습니다" 라고 말했다.
|
|
1860
|
+
// 붙어 있는데 안 붙었다고 하는 것이라, 새로 온 팀은 첫날 이 줄을 보고 설치가
|
|
1861
|
+
// 잘못된 줄 안다. 홈 등록이 기본이 된 지금은 `.mcp.json` 이 **없는 것이 정상**이다.
|
|
1862
|
+
const homeMcp = homeRegistrations()
|
|
1863
|
+
const homeWord = homeMcp.found.length
|
|
1864
|
+
? `홈에 등록돼 있습니다 (${homeMcp.found.join(' · ')})` +
|
|
1865
|
+
(homeMcp.guessed.length ? ` ※ ${homeMcp.guessed.join(' · ')} 는 설정 파일 글자만 보고 짐작한 것입니다` : '')
|
|
1866
|
+
: null
|
|
1806
1867
|
const mcpJson = path.join(root, '.mcp.json')
|
|
1807
1868
|
if (fs.existsSync(mcpJson)) {
|
|
1808
1869
|
let server = null
|
|
@@ -1810,8 +1871,17 @@ function cmdDoctor(flags) {
|
|
|
1810
1871
|
if (!server) hm('MCP 설정', '.mcp.json 은 있는데 axmap 서버를 못 찾았습니다')
|
|
1811
1872
|
else if (!fs.existsSync(path.resolve(root, server))) no('MCP 설정', `서버 파일이 없습니다: ${server}`)
|
|
1812
1873
|
else ok('MCP 설정', `${server} (AI 도구가 승인만 하면 붙습니다)`)
|
|
1874
|
+
// 둘 다 있으면 **저장소 쪽이 이긴다.** 같은 서버면 문제가 없지만, npm 판을 깔아 둔
|
|
1875
|
+
// 사람이 "내가 깐 것이 쓰이겠거니" 하고 여기서 엇갈린다. 그 자리에서 말해 준다.
|
|
1876
|
+
if (homeMcp.found.length) {
|
|
1877
|
+
hm('MCP 설정', `홈에도 등록돼 있습니다 (${homeMcp.found.join(' · ')}) — 이 저장소에서는 위의 .mcp.json 이 먼저 쓰입니다`)
|
|
1878
|
+
}
|
|
1879
|
+
} else if (homeWord) {
|
|
1880
|
+
ok('MCP 설정', `${homeWord} — 저장소에 파일이 없어도 됩니다`)
|
|
1881
|
+
} else if (homeMcp.unreadable.length) {
|
|
1882
|
+
hm('MCP 설정', `홈 설정을 읽지 못했습니다 (${homeMcp.unreadable.join(' · ')}) — 붙었는지 판단하지 않습니다`)
|
|
1813
1883
|
} else {
|
|
1814
|
-
hm('MCP 설정', '
|
|
1884
|
+
hm('MCP 설정', '아직 등록된 곳이 없습니다 — AI 도구가 자동으로 붙지 않습니다\n axmap setup')
|
|
1815
1885
|
}
|
|
1816
1886
|
|
|
1817
1887
|
// 8. 실제로 판정이 도는가 — 장부를 바꾸지 않고 읽기만 한다
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"//name": "🔴 npm 의 'axmap' 은 2022년부터 남이 쓰고 있는 다른 꾸러미다(leozin_ 의 Map 라이브러리, 1.1.2). 그래서 `npx axmap` 은 우리 것이 아니고, 누구에게도 그렇게 안내하면 안 된다. 2026-08-28 에 axmap-cli 로 정했다 — 스코프(@사용자/이름)를 쓰지 않는 이유는 스코프가 npm 계정 이름에 묶여서 나중에 팀이나 조직으로 옮길 때 이름이 통째로 바뀌기 때문이다. 사람이 치는 **명령** 이름은 그대로 axmap 이다 (아래 bin) — 꾸러미 이름과 명령 이름은 달라도 된다.",
|
|
3
3
|
"name": "axmap-cli",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "1.0.0",
|
|
5
5
|
"//private": "🔴 여기 있던 \"private\": true 를 2026-08-28 에 지웠다. 그 줄이 있는 동안 npm publish 는 거부됐다. 되돌리려면 다시 넣으면 되지만, 이미 올라간 버전은 그래도 안 사라진다 — npm 은 같은 번호를 덮어쓰지 못하고 삭제도 72시간 안에만 된다. 즉 이 줄을 되살리는 것은 '앞으로 안 올린다' 는 뜻이지 '올린 것을 없앤다' 는 뜻이 아니다.",
|
|
6
6
|
"//publishConfig": "axmap-cli 는 스코프가 없어서 기본이 이미 공개다. 즉 지금은 없어도 된다. 그래도 남겨 두는 이유: 나중에 @조직/axmap 처럼 스코프를 붙이는 날 이 줄이 없으면 publish 가 '유료 플랜이 필요하다'는 엉뚱한 말로 실패한다.",
|
|
7
7
|
"publishConfig": {
|
package/src/invariants.mjs
CHANGED
|
@@ -192,12 +192,64 @@ export function snapshotTime(snapshot) {
|
|
|
192
192
|
return Math.max(snapshot.time, ...(sinces.length ? sinces : [snapshot.time]))
|
|
193
193
|
}
|
|
194
194
|
|
|
195
|
+
// ---------------------------------------------------------------------------
|
|
196
|
+
// I5 · 강제 (전이 불변식)
|
|
197
|
+
// 커밋이 파일을 고쳤다면, 그 시각에 **누군가는** 그 파일을 잡고 있어야 한다.
|
|
198
|
+
//
|
|
199
|
+
// 🔴 이 검사가 없으면 claim 은 권고 사항이다. 지금까지 강제하는 것은 각자 PC 의
|
|
200
|
+
// 커밋 훅뿐이었고, 훅은 "설치했는가" 에 달려 있으며 `--no-verify` 로 넘어가고
|
|
201
|
+
// 웹 IDE 에는 아예 없다. 서버에서 도는 검사만이 설치 여부와 무관하다.
|
|
202
|
+
//
|
|
203
|
+
// 🔴 **"고친 사람이 곧 잡은 사람인가" 는 묻지 않는다.** 장부의 `agent` 는 세션
|
|
204
|
+
// 이름(`claude-code-mcpjson` 같은)이고 커밋의 author 는 git 이름(`janghyojoon`)
|
|
205
|
+
// 이라 서로 다른 이름 공간이다. 둘을 이름으로 맞추면 **정상 작업이 전부 위반으로
|
|
206
|
+
// 나온다.** 그래서 여기서는 "아무도 안 잡은 파일이 고쳐졌나" 하나만 본다 —
|
|
207
|
+
// 이것이 이 검사가 실제로 증명할 수 있는 문장이다. 소유자까지 맞추려면 claim
|
|
208
|
+
// 레코드에 git 신원을 함께 적어야 하고, 그건 별개의 변경이다.
|
|
209
|
+
//
|
|
210
|
+
// 🔴 장부보다 앞선 커밋은 **건너뛰되 세어서 보고한다.** 조용히 빼면 "위반 0" 이
|
|
211
|
+
// "검사했고 깨끗함" 으로 읽힌다. 검사하지 않은 것을 통과로 내지 않는다.
|
|
212
|
+
// ---------------------------------------------------------------------------
|
|
213
|
+
|
|
214
|
+
export function enforcementViolations(snapshots, commits) {
|
|
215
|
+
const snaps = [...(snapshots ?? [])].sort((a, b) => snapshotTime(a) - snapshotTime(b))
|
|
216
|
+
const violations = []
|
|
217
|
+
const skipped = []
|
|
218
|
+
|
|
219
|
+
for (const c of commits ?? []) {
|
|
220
|
+
// 그 커밋 시각에 유효했던 마지막 장부 스냅샷을 찾는다.
|
|
221
|
+
let snap = null
|
|
222
|
+
for (const s of snaps) {
|
|
223
|
+
if (snapshotTime(s) <= c.time) snap = s
|
|
224
|
+
else break
|
|
225
|
+
}
|
|
226
|
+
if (!snap) {
|
|
227
|
+
skipped.push({ commit: c.sha, subject: c.subject, reason: '장부보다 앞선 커밋' })
|
|
228
|
+
continue
|
|
229
|
+
}
|
|
230
|
+
const held = activeClaims(snap.claims, c.time).flatMap((cl) => cl.paths.map(normalizePath))
|
|
231
|
+
for (const f of (c.files ?? []).map(normalizePath)) {
|
|
232
|
+
if (held.some((p) => pathsOverlap(p, f))) continue
|
|
233
|
+
violations.push({
|
|
234
|
+
invariant: 'I5',
|
|
235
|
+
message: `${f} 를 아무도 잡지 않은 채 고쳤다`,
|
|
236
|
+
agents: c.author ? [c.author] : [],
|
|
237
|
+
paths: [f],
|
|
238
|
+
commit: c.sha,
|
|
239
|
+
subject: c.subject,
|
|
240
|
+
at: new Date(c.time).toISOString(),
|
|
241
|
+
})
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return { violations, skipped }
|
|
245
|
+
}
|
|
246
|
+
|
|
195
247
|
/**
|
|
196
248
|
* 시간이 흐르는 것만으로는 겹침이 생기지 않는다 (만료는 claim 을 없앨 뿐이다).
|
|
197
249
|
* 겹침은 오직 claim 이 추가될 때 생기고, claim 은 커밋으로만 추가된다.
|
|
198
250
|
* 따라서 각 커밋 시점만 검사하면 전체 구간을 덮는다.
|
|
199
251
|
*/
|
|
200
|
-
export function auditLedger(snapshots) {
|
|
252
|
+
export function auditLedger(snapshots, commits = null) {
|
|
201
253
|
const violations = []
|
|
202
254
|
for (const snap of snapshots) {
|
|
203
255
|
const t = snapshotTime(snap)
|
|
@@ -205,17 +257,37 @@ export function auditLedger(snapshots) {
|
|
|
205
257
|
violations.push({ ...v, commit: snap.commit, subject: snap.subject, at: new Date(t).toISOString() })
|
|
206
258
|
}
|
|
207
259
|
}
|
|
208
|
-
|
|
260
|
+
const code = commits ? enforcementViolations(snapshots, commits) : null
|
|
261
|
+
if (code) violations.push(...code.violations)
|
|
262
|
+
return {
|
|
263
|
+
ok: violations.length === 0,
|
|
264
|
+
checked: snapshots.length,
|
|
265
|
+
codeChecked: commits ? commits.length : null,
|
|
266
|
+
codeSkipped: code ? code.skipped : null,
|
|
267
|
+
violations,
|
|
268
|
+
}
|
|
209
269
|
}
|
|
210
270
|
|
|
211
271
|
export function formatAudit(report) {
|
|
272
|
+
// 코드 검사를 돌렸는지, 그중 몇 개를 못 봤는지는 통과·실패 어느 쪽에서도 적는다.
|
|
273
|
+
const codeLine =
|
|
274
|
+
report.codeChecked === null || report.codeChecked === undefined
|
|
275
|
+
? '코드 커밋 대조: 안 함 (--code <범위> 를 주면 봅니다)'
|
|
276
|
+
: `코드 커밋 대조: ${report.codeChecked}개` +
|
|
277
|
+
(report.codeSkipped?.length ? ` (장부보다 앞서 건너뛴 것 ${report.codeSkipped.length}개)` : '')
|
|
278
|
+
|
|
212
279
|
if (report.ok) {
|
|
213
|
-
return
|
|
214
|
-
`감사 통과 - 스냅샷 ${report.checked}
|
|
215
|
-
'장부 이력 전체에서 상호배제(I1)가 깨진 시점이 없습니다.'
|
|
216
|
-
|
|
280
|
+
return [
|
|
281
|
+
`감사 통과 - 스냅샷 ${report.checked}개`,
|
|
282
|
+
'장부 이력 전체에서 상호배제(I1)가 깨진 시점이 없습니다.',
|
|
283
|
+
codeLine,
|
|
284
|
+
].join('\n')
|
|
217
285
|
}
|
|
218
|
-
const lines = [
|
|
286
|
+
const lines = [
|
|
287
|
+
`감사 실패 - 스냅샷 ${report.checked}개 중 위반 ${report.violations.length}건`,
|
|
288
|
+
codeLine,
|
|
289
|
+
'',
|
|
290
|
+
]
|
|
219
291
|
for (const v of report.violations) {
|
|
220
292
|
lines.push(` x [${v.invariant}] ${v.at} commit ${v.commit.slice(0, 7)} ${v.subject}`)
|
|
221
293
|
lines.push(` ${v.message}`)
|
package/src/mcpstate.mjs
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* axmap MCP 가 **어디에 등록돼 있는가** 를 읽기만 하는 자리.
|
|
3
|
+
*
|
|
4
|
+
* 왜 따로 있나 — `tools/mcp-register.mjs` 는 맨 아래에서 `process.exit(main())` 를
|
|
5
|
+
* 부른다. 그래서 그 파일을 import 하면 **등록이 실제로 실행되고 프로세스가 죽는다.**
|
|
6
|
+
* 판정만 필요한 쪽(`doctor`)이 그것을 부를 수 없다. 그렇다고 doctor 안에 같은 판정을
|
|
7
|
+
* 한 벌 더 쓰면 두 곳이 서로 다르게 굴게 된다 — 등록기는 넣었다는데 doctor 는
|
|
8
|
+
* 없다고 하는 상태가 정확히 이 파일이 생긴 이유다.
|
|
9
|
+
*
|
|
10
|
+
* 여기에는 **쓰는 코드를 두지 않는다.** 무엇을 어디에 쓸지는 등록기가 정한다.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import fs from 'node:fs'
|
|
14
|
+
import os from 'node:os'
|
|
15
|
+
import path from 'node:path'
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* `~/.claude.json` 의 user 범위 axmap 항목.
|
|
19
|
+
*
|
|
20
|
+
* 없으면 `null`, **못 읽으면 `undefined`** 다. 이 둘을 가르는 것이 중요하다 —
|
|
21
|
+
* 파일이 아예 없는 것은 "등록 안 됨" 이고, 있는데 못 읽는 것은 "사고" 다.
|
|
22
|
+
* 사고를 "등록 안 됨" 으로 뭉개면 남의 설정을 덮어쓰자는 판단이 나온다.
|
|
23
|
+
*/
|
|
24
|
+
export function claudeUserEntry() {
|
|
25
|
+
try {
|
|
26
|
+
const cfg = JSON.parse(fs.readFileSync(path.join(os.homedir(), '.claude.json'), 'utf8'))
|
|
27
|
+
return (cfg && cfg.mcpServers && cfg.mcpServers.axmap) || null
|
|
28
|
+
} catch (e) {
|
|
29
|
+
return e && e.code === 'ENOENT' ? null : undefined
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** `~/.gemini/config/mcp_config.json` 의 axmap 항목. 규칙은 위와 같다. */
|
|
34
|
+
export function agyUserEntry() {
|
|
35
|
+
try {
|
|
36
|
+
const f = path.join(os.homedir(), '.gemini', 'config', 'mcp_config.json')
|
|
37
|
+
const raw = fs.readFileSync(f, 'utf8')
|
|
38
|
+
// agy 는 설치할 때 0바이트 파일을 만들어 둔다. 빈 파일은 사고가 아니라 "아직 없음" 이다.
|
|
39
|
+
if (!raw.trim()) return null
|
|
40
|
+
const cfg = JSON.parse(raw)
|
|
41
|
+
return (cfg && cfg.mcpServers && cfg.mcpServers.axmap) || null
|
|
42
|
+
} catch (e) {
|
|
43
|
+
return e && e.code === 'ENOENT' ? null : undefined
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* `~/.codex/config.toml` 안에 axmap 서버가 있는가.
|
|
49
|
+
*
|
|
50
|
+
* 🔴 **이것은 글자 수준의 짐작이다.** 이 저장소에는 codex 로 실제 등록한 결과물이
|
|
51
|
+
* 없어서(`tools/mcp-register.mjs` 의 codex 절 주석 참고) TOML 을 제대로 파싱해서
|
|
52
|
+
* 판정할 근거가 없다. 그래서 `[mcp_servers.axmap]` 머리표만 찾는다.
|
|
53
|
+
*
|
|
54
|
+
* 확실하지 않은 것을 확실한 척 말하지 않으려고 **반환값에 그 사실을 실어 보낸다** —
|
|
55
|
+
* 부르는 쪽이 "짐작" 이라고 사람에게 말할 수 있게. 실물로 확인되면 이 함수만 고친다.
|
|
56
|
+
*/
|
|
57
|
+
export function codexUserEntry() {
|
|
58
|
+
try {
|
|
59
|
+
const raw = fs.readFileSync(path.join(os.homedir(), '.codex', 'config.toml'), 'utf8')
|
|
60
|
+
return /^\s*\[mcp_servers\.axmap\]/m.test(raw) ? { guessed: true } : null
|
|
61
|
+
} catch (e) {
|
|
62
|
+
return e && e.code === 'ENOENT' ? null : undefined
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* 홈에 등록된 것을 한 번에 모은다.
|
|
68
|
+
*
|
|
69
|
+
* `found` 는 등록이 확인된 도구 이름, `unreadable` 은 설정이 있는데 못 읽은 도구다.
|
|
70
|
+
* 못 읽은 것을 "없음" 쪽에 넣지 않는다 — 그러면 이미 붙어 있는 사람에게
|
|
71
|
+
* "안 붙었습니다" 라고 말하게 되고, 그게 지금 고치는 바로 그 버그다.
|
|
72
|
+
*/
|
|
73
|
+
export function homeRegistrations() {
|
|
74
|
+
const probes = [
|
|
75
|
+
['claude', claudeUserEntry],
|
|
76
|
+
['agy', agyUserEntry],
|
|
77
|
+
['codex', codexUserEntry],
|
|
78
|
+
]
|
|
79
|
+
const found = []
|
|
80
|
+
const unreadable = []
|
|
81
|
+
const guessed = []
|
|
82
|
+
for (const [id, probe] of probes) {
|
|
83
|
+
const e = probe()
|
|
84
|
+
if (e === undefined) { unreadable.push(id); continue }
|
|
85
|
+
if (!e) continue
|
|
86
|
+
found.push(id)
|
|
87
|
+
if (e.guessed) guessed.push(id)
|
|
88
|
+
}
|
|
89
|
+
return { found, unreadable, guessed }
|
|
90
|
+
}
|
package/tools/mcp-register.mjs
CHANGED
|
@@ -41,6 +41,7 @@ import path from 'node:path'
|
|
|
41
41
|
import { fileURLToPath } from 'node:url'
|
|
42
42
|
|
|
43
43
|
import { AGENTS, resolveBin } from '../app/lib/agentcli.mjs'
|
|
44
|
+
import { claudeUserEntry } from '../src/mcpstate.mjs'
|
|
44
45
|
|
|
45
46
|
const HERE = path.dirname(fileURLToPath(import.meta.url))
|
|
46
47
|
const AXMAP_DIR = path.resolve(HERE, '..') // .../axmap
|
|
@@ -92,16 +93,9 @@ const same = (a, b) => JSON.stringify(a) === JSON.stringify(b)
|
|
|
92
93
|
// 있는지 **읽는 것만** 우리가 하고, 쓰는 일은 claude 자신에게 맡긴다 —
|
|
93
94
|
// doCodex 와 같은 태도다.
|
|
94
95
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
const cfg = JSON.parse(fs.readFileSync(path.join(os.homedir(), '.claude.json'), 'utf8'))
|
|
99
|
-
return (cfg && cfg.mcpServers && cfg.mcpServers.axmap) || null
|
|
100
|
-
} catch (e) {
|
|
101
|
-
// 파일이 아예 없는 것은 "등록 안 됨"(null)이고, 있는데 못 읽는 것은 사고(undefined)다.
|
|
102
|
-
return e && e.code === 'ENOENT' ? null : undefined
|
|
103
|
-
}
|
|
104
|
-
}
|
|
96
|
+
// `claudeUserEntry` 는 `src/mcpstate.mjs` 에 있다 (위 import).
|
|
97
|
+
// 여기 두었다가 doctor 가 같은 판정을 한 벌 더 쓰게 됐고, 둘이 어긋나면
|
|
98
|
+
// "등록기는 넣었다는데 doctor 는 없다고 한다" 가 된다. 판정은 한 곳에만 둔다.
|
|
105
99
|
|
|
106
100
|
/**
|
|
107
101
|
* 이미 쓸 만한 항목인가.
|