axmap-cli 1.3.2 → 1.4.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.md +15 -9
- package/README.ko.md +89 -85
- package/README.md +90 -86
- package/bin/axmap.mjs +9 -3
- package/mcp/README.md +8 -8
- package/mcp/install.ps1 +6 -3
- package/mcp/install.sh +5 -3
- package/mcp/server.mjs +81 -32
- package/package.json +7 -11
- package/src/agentcli.mjs +19 -0
- package/src/bridge.mjs +245 -0
- package/src/invariants.mjs +1 -1
- package/src/managed-session.mjs +178 -0
- package/src/protocol.mjs +1 -1
- package/src/session-dialogue.mjs +131 -0
- package/src/session-usage.mjs +34 -0
- package/src/state.mjs +8 -0
- package/src/terminal-bridge.mjs +224 -0
- package/src/update.mjs +1 -1
- package/tools/bridge.mjs +85 -0
- package/tools/mcp-register.mjs +22 -3
- package/tools/run-tests.mjs +10 -3
- package/tools/run.mjs +60 -0
- package/tools/setup.mjs +11 -0
- package/app/README.md +0 -366
- package/app/eval/edges.mjs +0 -242
- package/app/lib/adjacent.mjs +0 -125
- package/app/lib/agentcli.mjs +0 -153
- package/app/lib/analyze.mjs +0 -1159
- package/app/lib/cochange.mjs +0 -421
- package/app/lib/datanodes.mjs +0 -127
- package/app/lib/entry.mjs +0 -192
- package/app/lib/featuregraph.mjs +0 -389
- package/app/lib/features.mjs +0 -645
- package/app/lib/fetchrepo-run.mjs +0 -37
- package/app/lib/fetchrepo.mjs +0 -164
- package/app/lib/flow.mjs +0 -1089
- package/app/lib/ladder.mjs +0 -387
- package/app/lib/langs.mjs +0 -630
- package/app/lib/live.mjs +0 -346
- package/app/lib/llm.mjs +0 -594
- package/app/lib/newfile.mjs +0 -126
- package/app/lib/prdiff.mjs +0 -651
- package/app/lib/reveal.mjs +0 -316
- package/app/lib/roots.mjs +0 -186
- package/app/lib/scope.mjs +0 -342
- package/app/lib/session.mjs +0 -389
- package/app/lib/slots.mjs +0 -233
- package/app/lib/ssot.mjs +0 -277
- package/app/lib/teamview.mjs +0 -975
- package/app/lib/terms.ko.mjs +0 -169
- package/app/server.mjs +0 -1959
- package/app/web/shell.css +0 -538
- package/app/web/shell.html +0 -197
- package/app/web/shell.js +0 -638
- package/app/web/stage.js +0 -347
- package/app/web/words.js +0 -85
- package/src/repotarget.mjs +0 -84
- package/tools/cluster-experiment.mjs +0 -256
- package/tools/cluster-sweep.mjs +0 -226
- package/tools/make-icon.mjs +0 -108
- package/tools/persona-bench.mjs +0 -362
- package/tools/pick-repo.mjs +0 -229
- package/tools/reveal-demo.mjs +0 -158
- package/tools/shortcut.mjs +0 -121
- package/tools/smoke.mjs +0 -174
- package/tools/topicgraph.py +0 -154
package/mcp/install.ps1
CHANGED
|
@@ -142,7 +142,7 @@ if (-not (Test-Path -LiteralPath (Join-Path $Source 'bin\axmap.mjs'))) {
|
|
|
142
142
|
$inner = Join-Path $Source 'axmap'
|
|
143
143
|
if (Test-Path -LiteralPath (Join-Path $inner 'bin\axmap.mjs')) { $Source = (Resolve-Path -LiteralPath $inner).Path }
|
|
144
144
|
}
|
|
145
|
-
foreach ($need in @('bin\axmap.mjs', 'mcp\server.mjs', 'src\protocol.mjs', 'tools\mcp-register.mjs', '
|
|
145
|
+
foreach ($need in @('bin\axmap.mjs', 'mcp\server.mjs', 'src\protocol.mjs', 'tools\mcp-register.mjs', 'src\agentcli.mjs', 'package.json')) {
|
|
146
146
|
if (-not (Test-Path -LiteralPath (Join-Path $Source $need))) {
|
|
147
147
|
Fail @"
|
|
148
148
|
axMap 원본이 온전하지 않습니다. 없는 파일: $need
|
|
@@ -198,8 +198,8 @@ Say ''
|
|
|
198
198
|
Say "복사합니다 -> $Dest"
|
|
199
199
|
New-Item -ItemType Directory -Force -Path $Dest | Out-Null
|
|
200
200
|
|
|
201
|
-
#
|
|
202
|
-
foreach ($item in @('bin', 'src', 'mcp', 'tools', '
|
|
201
|
+
# Copy the CLI, MCP and their shared runtime sources.
|
|
202
|
+
foreach ($item in @('bin', 'src', 'mcp', 'tools', 'package.json', 'LICENSE')) {
|
|
203
203
|
$from = Join-Path $Source $item
|
|
204
204
|
$to = Join-Path $Dest $item
|
|
205
205
|
if (-not (Test-Path -LiteralPath $from)) { Hm "$item 없음 — 건너뜁니다"; continue }
|
|
@@ -219,6 +219,9 @@ foreach ($item in @('bin', 'src', 'mcp', 'tools', 'app', 'package.json', 'LICENS
|
|
|
219
219
|
}
|
|
220
220
|
}
|
|
221
221
|
|
|
222
|
+
& node (Join-Path $Dest 'tools\run.mjs') install-runtime $Source
|
|
223
|
+
if ($LASTEXITCODE -ne 0) { Fail '터미널 런타임 설치에 실패했습니다.' }
|
|
224
|
+
|
|
222
225
|
$server = Join-Path $Dest 'mcp\server.mjs'
|
|
223
226
|
if (-not (Test-Path -LiteralPath $server)) { Fail "복사가 끝났는데 서버가 없습니다: $server" }
|
|
224
227
|
|
package/mcp/install.sh
CHANGED
|
@@ -144,7 +144,7 @@ SOURCE=$(cd "$SOURCE" && pwd)
|
|
|
144
144
|
if [ ! -f "$SOURCE/bin/axmap.mjs" ] && [ -f "$SOURCE/axmap/bin/axmap.mjs" ]; then
|
|
145
145
|
SOURCE=$(cd "$SOURCE/axmap" && pwd)
|
|
146
146
|
fi
|
|
147
|
-
for need in bin/axmap.mjs mcp/server.mjs src/protocol.mjs tools/mcp-register.mjs
|
|
147
|
+
for need in bin/axmap.mjs mcp/server.mjs src/protocol.mjs tools/mcp-register.mjs src/agentcli.mjs package.json; do
|
|
148
148
|
if [ ! -e "$SOURCE/$need" ]; then
|
|
149
149
|
fail "axMap 원본이 온전하지 않습니다. 없는 파일: $need
|
|
150
150
|
본 곳: $SOURCE
|
|
@@ -198,8 +198,8 @@ say ''
|
|
|
198
198
|
say "복사합니다 -> $DEST"
|
|
199
199
|
mkdir -p "$DEST" || fail "설치 폴더를 만들지 못했습니다: $DEST"
|
|
200
200
|
|
|
201
|
-
#
|
|
202
|
-
for item in bin src mcp tools
|
|
201
|
+
# Copy the CLI, MCP and their shared runtime sources.
|
|
202
|
+
for item in bin src mcp tools package.json LICENSE; do
|
|
203
203
|
from="$SOURCE/$item"
|
|
204
204
|
to="$DEST/$item"
|
|
205
205
|
if [ ! -e "$from" ]; then hm "$item 없음 — 건너뜁니다"; continue; fi
|
|
@@ -218,6 +218,8 @@ for item in bin src mcp tools app package.json LICENSE; do
|
|
|
218
218
|
fi
|
|
219
219
|
done
|
|
220
220
|
|
|
221
|
+
node "$DEST/tools/run.mjs" install-runtime "$SOURCE" || fail "터미널 런타임 설치에 실패했습니다."
|
|
222
|
+
|
|
221
223
|
SERVER="$DEST/mcp/server.mjs"
|
|
222
224
|
[ -f "$SERVER" ] || fail "복사가 끝났는데 서버가 없습니다: $SERVER"
|
|
223
225
|
|
package/mcp/server.mjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* axMap MCP 서버.
|
|
4
4
|
*
|
|
5
5
|
* Claude Code 같은 AI 도구가 "나는 이 파일들을 건드리겠다"를 선언하게 만든다.
|
|
6
|
-
*
|
|
6
|
+
* 다른 에이전트가 작업 범위와 목적을 확인할 수 있다.
|
|
7
7
|
*
|
|
8
8
|
* 설정 (.mcp.json 또는 도구별 MCP 설정):
|
|
9
9
|
* {
|
|
@@ -212,6 +212,15 @@ const TTL = process.env.AXMAP_TTL ?? '45m'
|
|
|
212
212
|
*/
|
|
213
213
|
const SESSION = process.env.AXMAP_SESSION || process.env.CLAUDE_CODE_SESSION_ID || `mcp-${process.pid}`
|
|
214
214
|
|
|
215
|
+
const BRIDGE_TOOLS = new Set(['ax_sessions', 'ax_dispatch', 'ax_result', 'ax_chat', 'ax_dialogue', 'ax_chat_stop'])
|
|
216
|
+
function bridge(args, input) {
|
|
217
|
+
const r = spawnSync(process.execPath, [path.join(SELF, 'tools', 'bridge.mjs'), ...args], {
|
|
218
|
+
cwd: REPO, input, encoding: 'utf8', windowsHide: true,
|
|
219
|
+
env: { ...process.env, AXMAP_AGENT: AGENT }, timeout: 45000,
|
|
220
|
+
})
|
|
221
|
+
return { ok: r.status === 0, text: (r.stdout || '') + (r.stderr || '') + (r.error ? r.error.message : '') }
|
|
222
|
+
}
|
|
223
|
+
|
|
215
224
|
/**
|
|
216
225
|
* 쪽지함 CLI 를 부른다. `cli` 와 달리 stdin 으로 본문을 넘긴다.
|
|
217
226
|
*
|
|
@@ -376,6 +385,48 @@ function cli(args) {
|
|
|
376
385
|
// ---------------------------------------------------------------------------
|
|
377
386
|
|
|
378
387
|
const TOOLS = [
|
|
388
|
+
{
|
|
389
|
+
name: 'ax_sessions',
|
|
390
|
+
description: '현재 repo에 연결된 로컬 AI 세션, 역할, 상태와 측정 토큰 사용량을 확인한다. stdio는 사용자가 bridge serve로 시작한 관리형 세션이다. 등록만으로 연결 가능성이나 실행 성공을 보장하지 않는다.',
|
|
391
|
+
inputSchema: { type: 'object', properties: {} },
|
|
392
|
+
},
|
|
393
|
+
{
|
|
394
|
+
name: 'ax_dispatch',
|
|
395
|
+
description: '사용자가 허용한 작업을 연결된 상대 세션에 전달한다. 기존 맥락을 유지하며 새 모델 프로세스를 만들지 않는다. submitted는 완료가 아니다. unknown이면 자동 재시도하지 말고 ax_result로 확인한다. Codex/agy 관리형 stdio, 실험용 PTY/agentapi 연결을 지원한다.',
|
|
396
|
+
inputSchema: { type: 'object', properties: {
|
|
397
|
+
to: { type: 'string', description: 'ax_sessions에서 확인한 상대 세션 UUID' },
|
|
398
|
+
prompt: { type: 'string', description: '상대가 실행할 작업 요청' },
|
|
399
|
+
}, required: ['to', 'prompt'] },
|
|
400
|
+
},
|
|
401
|
+
{
|
|
402
|
+
name: 'ax_result',
|
|
403
|
+
description: '작업 요청의 제출/완료/불명 상태, 결과와 측정 사용량을 확인한다. 관리형 세션은 CLI의 구조화된 실행 완료 이벤트, 실험용 터미널은 해당 요청의 종료 훅으로 판정한다.',
|
|
404
|
+
inputSchema: { type: 'object', properties: {
|
|
405
|
+
id: { type: 'string', description: 'ax_dispatch가 반환한 요청 UUID' },
|
|
406
|
+
}, required: ['id'] },
|
|
407
|
+
},
|
|
408
|
+
{
|
|
409
|
+
name: 'ax_chat',
|
|
410
|
+
description: '사용자가 허용한 범위에서 대기 중인 관리형 세션 두 개의 자동 대화를 시작한다. 기본 최대 4회 실행, 측정 처리 토큰 50000, 메시지 2000자. 대기 중 모델 호출 없음. 한 번의 실행은 토큰 한도를 넘을 수 있으며 다음 실행 전에 검사한다. 접수는 완료가 아니다. ax_dialogue로 조회한다. 사용자 승인이나 연결 실패를 자동 우회하지 않는다.',
|
|
411
|
+
inputSchema: { type: 'object', properties: {
|
|
412
|
+
left: { type: 'string', description: '먼저 요청받을 stdio 세션 UUID' },
|
|
413
|
+
right: { type: 'string', description: '다른 stdio 세션 UUID' },
|
|
414
|
+
prompt: { type: 'string', description: '사용자가 부여한 작업과 종료 기준. 기본 2000자 이하' },
|
|
415
|
+
maxTurns: { type: 'integer', minimum: 1, maximum: 20 },
|
|
416
|
+
maxTokens: { type: 'integer', minimum: 1 },
|
|
417
|
+
maxMessageChars: { type: 'integer', minimum: 1, maximum: 8000 },
|
|
418
|
+
}, required: ['left', 'right', 'prompt'] },
|
|
419
|
+
},
|
|
420
|
+
{
|
|
421
|
+
name: 'ax_dialogue',
|
|
422
|
+
description: '자동 대화의 상태, 요청별 사용량과 누적 사용량을 확인한다. usage_unavailable는 측정 불가로 중단, token_limit는 다음 실행 시작을 막은 상태다.',
|
|
423
|
+
inputSchema: { type: 'object', properties: { id: { type: 'string' } }, required: ['id'] },
|
|
424
|
+
},
|
|
425
|
+
{
|
|
426
|
+
name: 'ax_chat_stop',
|
|
427
|
+
description: '자동 대화의 추가 중계를 중단한다. 이미 전달된 모델 요청은 계속 실행될 수 있다. 상대 CLI를 강제 종료하지 않는다.',
|
|
428
|
+
inputSchema: { type: 'object', properties: { id: { type: 'string' } }, required: ['id'] },
|
|
429
|
+
},
|
|
379
430
|
{
|
|
380
431
|
name: 'ax_brief',
|
|
381
432
|
description:
|
|
@@ -493,7 +544,7 @@ for (const tool of TOOLS) {
|
|
|
493
544
|
const readFrom = (base, rel) => {
|
|
494
545
|
try { return fs.readFileSync(path.join(base, rel), 'utf8') } catch { return null }
|
|
495
546
|
}
|
|
496
|
-
/** axMap 자신의 파일.
|
|
547
|
+
/** axMap 자신의 파일. 구성 목록처럼 **이 도구에 관한 것**만 여기서 읽는다. */
|
|
497
548
|
const readSelf = (rel) => readFrom(SELF, rel)
|
|
498
549
|
/** 대상 저장소의 파일. 브리핑의 내용은 원칙적으로 전부 이쪽이어야 한다. */
|
|
499
550
|
const readRepo = (rel) => readFrom(REPO, rel)
|
|
@@ -548,23 +599,19 @@ function gist(rel) {
|
|
|
548
599
|
* 경로만 여기 적고 설명은 파일에서 가져온다.
|
|
549
600
|
* 경로는 거의 안 바뀌고 설명은 자주 바뀌므로, 낡을 수 있는 쪽을 자동화한다.
|
|
550
601
|
*/
|
|
551
|
-
const
|
|
552
|
-
['판정
|
|
553
|
-
['불변식
|
|
554
|
-
['CLI ·
|
|
555
|
-
['MCP 서버
|
|
556
|
-
['
|
|
557
|
-
['
|
|
558
|
-
['
|
|
559
|
-
['
|
|
560
|
-
['
|
|
561
|
-
['화면 부트스트랩', 'app/web/shell.js'],
|
|
562
|
-
['가운데 칸 그리기', 'app/web/stage.js'],
|
|
563
|
-
['화면에 나가는 말 (어휘)', 'app/web/words.js'],
|
|
564
|
-
['데스크톱 셸 (Electron)', 'desktop/main.mjs'],
|
|
602
|
+
const COMPONENTS = [
|
|
603
|
+
['선점 판정', 'src/protocol.mjs'],
|
|
604
|
+
['불변식 검사', 'src/invariants.mjs'],
|
|
605
|
+
['CLI · Git · 훅', 'bin/axmap.mjs'],
|
|
606
|
+
['MCP 서버', 'mcp/server.mjs'],
|
|
607
|
+
['AI CLI 탐지', 'src/agentcli.mjs'],
|
|
608
|
+
['로컬 작업 요청', 'src/bridge.mjs'],
|
|
609
|
+
['터미널 세션 연결', 'src/terminal-bridge.mjs'],
|
|
610
|
+
['설치·설정', 'tools/setup.mjs'],
|
|
611
|
+
['쪽지함', 'tools/bus.mjs'],
|
|
565
612
|
]
|
|
566
613
|
|
|
567
|
-
/** axMap 자신을 겨눴을
|
|
614
|
+
/** axMap 자신을 겨눴을 때만 자체 구성 목록을 보여준다. */
|
|
568
615
|
function briefSelf() {
|
|
569
616
|
const out = []
|
|
570
617
|
out.push('# axMap — 새로 들어온 에이전트를 위한 브리핑')
|
|
@@ -589,12 +636,11 @@ function briefSelf() {
|
|
|
589
636
|
// 번호를 여기 적지 않는다. 늘릴 때마다 이 줄이 낡는다 — 실제로 I8 을 넣고 놓쳤다.
|
|
590
637
|
out.push('| 지켜야 할 성질 | `docs/INVARIANTS.md` |')
|
|
591
638
|
out.push('| 왜 파서인가 | `docs/EXPERIMENT.md` |')
|
|
592
|
-
out.push('| 무엇을 재는가 | `docs/BENCH.md` — M3 도달 호출 번호가 주 지표다 |')
|
|
593
639
|
out.push('')
|
|
594
640
|
|
|
595
|
-
out.push('##
|
|
641
|
+
out.push('## 주요 구성')
|
|
596
642
|
out.push('')
|
|
597
|
-
for (const [role, rel] of
|
|
643
|
+
for (const [role, rel] of COMPONENTS) out.push(`- **${role}** — \`${rel}\`\n ${gist(rel)}`)
|
|
598
644
|
out.push('')
|
|
599
645
|
return out
|
|
600
646
|
}
|
|
@@ -605,7 +651,7 @@ function briefSelf() {
|
|
|
605
651
|
* 🔴 여기서 **axMap 의 문서를 대신 보여주지 않는다.**
|
|
606
652
|
*
|
|
607
653
|
* 예전에는 브리핑이 통째로 `readSelf` 였다. 그래서 남의 프로젝트에 붙여도
|
|
608
|
-
* `src/protocol.mjs` · `
|
|
654
|
+
* `src/protocol.mjs` · `src/bridge.mjs` 같은 **axMap 의 주요 구성**이 나왔고,
|
|
609
655
|
* 받는 쪽에는 자기 저장소를 설명하는 것처럼 보인다.
|
|
610
656
|
*
|
|
611
657
|
* 이 저장소는 "낡은 지도는 없는 지도보다 나쁘다" 고 적어 두었는데,
|
|
@@ -613,7 +659,7 @@ function briefSelf() {
|
|
|
613
659
|
* 낡은 지도는 언젠가 맞았기라도 하다.
|
|
614
660
|
*
|
|
615
661
|
* 그래서 대상에 문서가 없으면 **없다고 말하고 끝낸다.** 채워 넣지 않는다.
|
|
616
|
-
*
|
|
662
|
+
* 구성 목록을 추측해서 만들지 않는다.
|
|
617
663
|
*/
|
|
618
664
|
function briefForeign() {
|
|
619
665
|
const out = []
|
|
@@ -665,8 +711,8 @@ function brief() {
|
|
|
665
711
|
/**
|
|
666
712
|
* 🔴 검증 목록을 `package.json` 에서 뽑지 않는다.
|
|
667
713
|
*
|
|
668
|
-
* scripts 에는
|
|
669
|
-
*
|
|
714
|
+
* scripts 에는 검사와 설치 같은 실행 명령이 섞일 수 있다.
|
|
715
|
+
* 전부 나열하면 새 에이전트가 실행만 해 놓고 "검증했다" 고 여긴다.
|
|
670
716
|
* **무엇이 통과해야 하는지를 정한 곳은 규칙 문서의 「검증」 절**이므로
|
|
671
717
|
* 거기서 읽어 온다. 규격이 진실이고 목록은 그 사본이다.
|
|
672
718
|
*
|
|
@@ -698,12 +744,6 @@ function brief() {
|
|
|
698
744
|
out.push('')
|
|
699
745
|
for (const c of cmds) out.push(`- \`${c}\``)
|
|
700
746
|
out.push('')
|
|
701
|
-
// axMap 자신에게만 해당하는 주의다. 남의 저장소에 붙여 말하지 않는다.
|
|
702
|
-
if (!isForeign()) {
|
|
703
|
-
out.push('🔴 `app/web/` 를 건드렸으면 `npm run smoke` 는 **선택이 아니다.**')
|
|
704
|
-
out.push('`npm test` 는 브라우저 코드를 실행하지 않으므로 화면이 죽어도 초록이다.')
|
|
705
|
-
out.push('')
|
|
706
|
-
}
|
|
707
747
|
}
|
|
708
748
|
|
|
709
749
|
out.push('장부는 선점·조회 시 자동으로 준비한다. 별도 `ax_init` 호출은 필요 없다.')
|
|
@@ -807,6 +847,14 @@ function dispatch(name, args = {}) {
|
|
|
807
847
|
const paths = Array.isArray(args.paths) ? args.paths.map(String) : []
|
|
808
848
|
|
|
809
849
|
switch (name) {
|
|
850
|
+
case 'ax_sessions': return bridge(['sessions'])
|
|
851
|
+
case 'ax_dispatch': return bridge(['send', '--to', args.to], args.prompt)
|
|
852
|
+
case 'ax_result': return bridge(['result', args.id])
|
|
853
|
+
case 'ax_chat': return bridge(['chat', '--background', '--left', args.left, '--right', args.right,
|
|
854
|
+
...['maxTurns', 'maxTokens', 'maxMessageChars'].flatMap(key => args[key] === undefined ? [] :
|
|
855
|
+
[`--${key.replace(/[A-Z]/g, c => `-${c.toLowerCase()}`)}`, String(args[key])])], args.prompt)
|
|
856
|
+
case 'ax_dialogue': return bridge(['dialogue', args.id])
|
|
857
|
+
case 'ax_chat_stop': return bridge(['stop', args.id])
|
|
810
858
|
case 'ax_brief':
|
|
811
859
|
return { ok: true, text: brief() }
|
|
812
860
|
case 'ax_init': {
|
|
@@ -1008,8 +1056,9 @@ async function handle(req) {
|
|
|
1008
1056
|
do { task = rootsTask; await task } while (task !== rootsTask)
|
|
1009
1057
|
}
|
|
1010
1058
|
const r = callTool(params?.name, params?.arguments ?? {})
|
|
1011
|
-
const
|
|
1012
|
-
|
|
1059
|
+
const localBridge = BRIDGE_TOOLS.has(params?.name)
|
|
1060
|
+
const warning = r.ok && !localBridge ? syncConfiguredProfile(REPO, BUS, { ...process.env, AXMAP_AGENT: AGENT }) : ''
|
|
1061
|
+
return { content: [{ type: 'text', text: r.text + (warning ? '\n' + warning : '') + (r.ok && !localBridge ? unreadBanner(params?.name, r.text) : '') }], isError: !r.ok }
|
|
1013
1062
|
}
|
|
1014
1063
|
if (method === 'ping') return {}
|
|
1015
1064
|
return null // 알림이거나 지원하지 않는 메서드
|
package/package.json
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
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": "1.
|
|
4
|
+
"version": "1.4.0",
|
|
5
5
|
"//private": "🔴 여기 있던 \"private\": true 를 2026-08-28 에 지웠다. 그 줄이 있는 동안 npm publish 는 거부됐다. 되돌리려면 다시 넣으면 되지만, 이미 올라간 버전은 그래도 안 사라진다 — npm 은 같은 번호를 덮어쓰지 못하고 삭제도 72시간 안에만 된다. 즉 이 줄을 되살리는 것은 '앞으로 안 올린다' 는 뜻이지 '올린 것을 없앤다' 는 뜻이 아니다.",
|
|
6
6
|
"//publishConfig": "axmap-cli 는 스코프가 없어서 기본이 이미 공개다. 즉 지금은 없어도 된다. 그래도 남겨 두는 이유: 나중에 @조직/axmap 처럼 스코프를 붙이는 날 이 줄이 없으면 publish 가 '유료 플랜이 필요하다'는 엉뚱한 말로 실패한다.",
|
|
7
7
|
"publishConfig": {
|
|
8
8
|
"access": "public"
|
|
9
9
|
},
|
|
10
10
|
"type": "module",
|
|
11
|
-
"description": "
|
|
11
|
+
"description": "Coordinate AI agents in Git with path claims, messaging, commit verification and MCP.",
|
|
12
12
|
"license": "MIT",
|
|
13
13
|
"repository": {
|
|
14
14
|
"type": "git",
|
|
@@ -26,13 +26,12 @@
|
|
|
26
26
|
"bin": {
|
|
27
27
|
"axmap": "bin/axmap.mjs"
|
|
28
28
|
},
|
|
29
|
-
"//files": "
|
|
29
|
+
"//files": "Published CLI, MCP, collaboration tools and shared runtime sources. Development tests and documentation stay in the source repository.",
|
|
30
30
|
"files": [
|
|
31
31
|
"bin/",
|
|
32
32
|
"src/",
|
|
33
33
|
"mcp/",
|
|
34
34
|
"tools/",
|
|
35
|
-
"app/",
|
|
36
35
|
"governance/",
|
|
37
36
|
".claude/",
|
|
38
37
|
"CLAUDE.md"
|
|
@@ -42,13 +41,7 @@
|
|
|
42
41
|
"setup": "node tools/setup.mjs",
|
|
43
42
|
"demo": "bash demo/run-demo.sh",
|
|
44
43
|
"demo:compare": "bash demo/conflict-vs-parser.sh",
|
|
45
|
-
"demo:chaos": "bash demo/chaos.sh"
|
|
46
|
-
"app": "node app/server.mjs",
|
|
47
|
-
"smoke": "node tools/smoke.mjs",
|
|
48
|
-
"desktop": "npm --prefix desktop start",
|
|
49
|
-
"desktop:smoke": "npm --prefix desktop run selftest",
|
|
50
|
-
"dist": "npm --prefix desktop run dist",
|
|
51
|
-
"shortcut": "node tools/shortcut.mjs --dev"
|
|
44
|
+
"demo:chaos": "bash demo/chaos.sh"
|
|
52
45
|
},
|
|
53
46
|
"engines": {
|
|
54
47
|
"node": ">=20"
|
|
@@ -56,5 +49,8 @@
|
|
|
56
49
|
"homepage": "https://github.com/rl2aderjoon/axmap#readme",
|
|
57
50
|
"bugs": {
|
|
58
51
|
"url": "https://github.com/rl2aderjoon/axmap/issues"
|
|
52
|
+
},
|
|
53
|
+
"dependencies": {
|
|
54
|
+
"node-pty": "npm:@lydell/node-pty@1.1.0"
|
|
59
55
|
}
|
|
60
56
|
}
|
package/src/agentcli.mjs
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** AI CLI executable discovery shared by setup and MCP registration. */
|
|
2
|
+
import { spawnSync } from 'node:child_process'
|
|
3
|
+
|
|
4
|
+
export const AGENTS = [
|
|
5
|
+
{ id: 'claude', name: 'Claude Code', bin: 'claude' },
|
|
6
|
+
{ id: 'agy', name: 'Antigravity', bin: 'agy' },
|
|
7
|
+
{ id: 'codex', name: 'Codex CLI', bin: 'codex' },
|
|
8
|
+
]
|
|
9
|
+
|
|
10
|
+
/** Resolve the executable without a shell so paths and arguments stay intact. */
|
|
11
|
+
export function resolveBin(bin) {
|
|
12
|
+
try {
|
|
13
|
+
const r = spawnSync(process.platform === 'win32' ? 'where.exe' : 'which', [bin], {
|
|
14
|
+
encoding: 'utf8', windowsHide: true,
|
|
15
|
+
})
|
|
16
|
+
if (r.status !== 0) return null
|
|
17
|
+
return r.stdout.split(/\r?\n/).map(s => s.trim()).find(Boolean) || null
|
|
18
|
+
} catch { return null }
|
|
19
|
+
}
|
package/src/bridge.mjs
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/** Local sessions are attached by the provider's own hooks. Sending a message
|
|
2
|
+
* must never silently create/resume a second model process. */
|
|
3
|
+
import fs from 'node:fs'
|
|
4
|
+
import os from 'node:os'
|
|
5
|
+
import path from 'node:path'
|
|
6
|
+
import { randomUUID } from 'node:crypto'
|
|
7
|
+
import { spawnSync } from 'node:child_process'
|
|
8
|
+
import { fileURLToPath } from 'node:url'
|
|
9
|
+
import { withRepoLock } from './repolock.mjs'
|
|
10
|
+
|
|
11
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
|
|
12
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
|
13
|
+
const bridgeHome = () => path.join(process.env.AXMAP_STATE_DIR || path.join(os.homedir(), '.axmap'), 'bridge')
|
|
14
|
+
const agyHome = () => process.env.AXMAP_AGY_HOME || path.join(os.homedir(), '.gemini', 'antigravity-cli')
|
|
15
|
+
const file = (kind, id) => {
|
|
16
|
+
if (!UUID.test(id || '')) throw new Error('세션/요청 ID는 UUID여야 합니다.')
|
|
17
|
+
return path.join(bridgeHome(), kind, `${id}.json`)
|
|
18
|
+
}
|
|
19
|
+
function readJSON(p) { return JSON.parse(fs.readFileSync(p, 'utf8')) }
|
|
20
|
+
function writeJSON(p, data) {
|
|
21
|
+
fs.mkdirSync(path.dirname(p), { recursive: true, mode: 0o700 })
|
|
22
|
+
const tmp = `${p}.${randomUUID()}.tmp`
|
|
23
|
+
try {
|
|
24
|
+
fs.writeFileSync(tmp, JSON.stringify(data, null, 2) + '\n', { mode: 0o600 })
|
|
25
|
+
fs.renameSync(tmp, p)
|
|
26
|
+
} finally { if (fs.existsSync(tmp)) fs.unlinkSync(tmp) }
|
|
27
|
+
}
|
|
28
|
+
function repoKey(cwd) {
|
|
29
|
+
const r = spawnSync('git', ['rev-parse', '--path-format=absolute', '--git-common-dir'], {
|
|
30
|
+
cwd, encoding: 'utf8', windowsHide: true,
|
|
31
|
+
})
|
|
32
|
+
if (r.status !== 0) throw new Error(`Git 작업 저장소를 확인하지 못했습니다: ${cwd}`)
|
|
33
|
+
const key = fs.realpathSync(r.stdout.trim())
|
|
34
|
+
return process.platform === 'win32' ? key.toLowerCase() : key
|
|
35
|
+
}
|
|
36
|
+
function addressError(address) {
|
|
37
|
+
// Only a loopback address exported by a local provider hook is accepted.
|
|
38
|
+
if (typeof address !== 'string' || !/^(?:https?:\/\/)?(?:127\.0\.0\.1|localhost|\[::1\]):[0-9]{1,5}$/.test(address)) {
|
|
39
|
+
throw new Error('공식 훅에서 받은 로컬 ANTIGRAVITY_LS_ADDRESS가 필요합니다.')
|
|
40
|
+
}
|
|
41
|
+
const port = Number(address.slice(address.lastIndexOf(':') + 1))
|
|
42
|
+
if (port < 1 || port > 65535) throw new Error('연결 포트가 잘못되었습니다.')
|
|
43
|
+
}
|
|
44
|
+
function runAPI(session, args) {
|
|
45
|
+
addressError(session.address)
|
|
46
|
+
const r = spawnSync(session.executable, ['agentapi', ...args], {
|
|
47
|
+
cwd: session.workspaces[0],
|
|
48
|
+
env: { ...process.env, ANTIGRAVITY_LS_ADDRESS: session.address },
|
|
49
|
+
encoding: 'utf8', windowsHide: true, timeout: 15000, maxBuffer: 1024 * 1024,
|
|
50
|
+
})
|
|
51
|
+
if (r.error || r.status !== 0) throw new Error(`열린 세션 연결 실패: ${r.error?.message || r.stderr?.trim() || r.stdout?.trim() || r.status}`)
|
|
52
|
+
const text = r.stdout.trim()
|
|
53
|
+
if (text) {
|
|
54
|
+
let data
|
|
55
|
+
try { data = JSON.parse(text) } catch { return text }
|
|
56
|
+
if (data?.error) throw new Error(`열린 세션 연결 실패: ${JSON.stringify(data.error)}`)
|
|
57
|
+
return data
|
|
58
|
+
}
|
|
59
|
+
return null
|
|
60
|
+
}
|
|
61
|
+
function transcript(session) {
|
|
62
|
+
const expected = path.join(agyHome(), 'brain', session.id, '.system_generated', 'logs', 'transcript.jsonl')
|
|
63
|
+
if (session.transcript !== expected) throw new Error('등록된 대화 기록 경로가 올바르지 않습니다.')
|
|
64
|
+
const lines = fs.readFileSync(expected, 'utf8').split('\n')
|
|
65
|
+
// A trailing incomplete record may be in flight; malformed complete records fail.
|
|
66
|
+
lines.pop()
|
|
67
|
+
return lines.filter(s => s.trim()).map(s => JSON.parse(s))
|
|
68
|
+
}
|
|
69
|
+
function publicSession(s) {
|
|
70
|
+
return { id: s.id, provider: s.provider, workspaces: s.workspaces, model: s.model,
|
|
71
|
+
transport: s.transport || 'agentapi', state: s.state,
|
|
72
|
+
registeredAt: s.registeredAt, event: s.event, fullyIdle: s.fullyIdle,
|
|
73
|
+
role: s.role, usage: s.usage, usageKnown: s.usageKnown, maxTokens: s.maxTokens, pendingApproval: s.pendingApproval }
|
|
74
|
+
}
|
|
75
|
+
export function sessions(cwd) {
|
|
76
|
+
const dir = path.join(bridgeHome(), 'sessions')
|
|
77
|
+
if (!fs.existsSync(dir)) return []
|
|
78
|
+
const repo = repoKey(cwd)
|
|
79
|
+
return fs.readdirSync(dir).filter(n => UUID.test(n.replace(/\.json$/, '')) && n.endsWith('.json'))
|
|
80
|
+
.map(n => readJSON(path.join(dir, n))).filter(s => s.repos.includes(repo)).map(publicSession)
|
|
81
|
+
}
|
|
82
|
+
function sessionFor(id, cwd) {
|
|
83
|
+
const s = readJSON(file('sessions', id))
|
|
84
|
+
if (!s.repos.includes(repoKey(cwd))) throw new Error('이 세션은 현재 작업 저장소에 등록되지 않았습니다.')
|
|
85
|
+
if (s.provider !== 'agy' && !['pty', 'stdio'].includes(s.transport)) throw new Error('아직 지원하지 않는 수신 세션입니다.')
|
|
86
|
+
return s
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Hook input and env come from Antigravity, not a remote message or repo file. */
|
|
90
|
+
export function registerHook(input, event, env = process.env) {
|
|
91
|
+
if (!['SessionStart', 'PreInvocation', 'Stop'].includes(event)) throw new Error('지원하지 않는 훅 이벤트입니다.')
|
|
92
|
+
addressError(env.ANTIGRAVITY_LS_ADDRESS)
|
|
93
|
+
const id = input.conversationId
|
|
94
|
+
const target = file('sessions', id)
|
|
95
|
+
const workspaces = input.workspacePaths
|
|
96
|
+
if (!Array.isArray(workspaces) || !workspaces.length || workspaces.some(p => !path.isAbsolute(p))) {
|
|
97
|
+
throw new Error('훅의 작업 저장소 경로가 없습니다.')
|
|
98
|
+
}
|
|
99
|
+
const executable = env.AXMAP_AGY_EXECUTABLE
|
|
100
|
+
if (!executable || !path.isAbsolute(executable) || !fs.existsSync(executable)) throw new Error('등록된 agy 실행 파일이 없습니다.')
|
|
101
|
+
const expected = path.join(agyHome(), 'brain', id, '.system_generated', 'logs', 'transcript.jsonl')
|
|
102
|
+
if (path.resolve(input.transcriptPath || '') !== expected) throw new Error('Antigravity CLI 대화 기록 경로가 아닙니다.')
|
|
103
|
+
const s = { id, provider: 'agy', executable, address: env.ANTIGRAVITY_LS_ADDRESS,
|
|
104
|
+
transcript: expected, workspaces, repos: [...new Set(workspaces.map(repoKey))], model: input.modelName || null,
|
|
105
|
+
registeredAt: new Date().toISOString(), event, fullyIdle: event === 'Stop' && input.fullyIdle === true }
|
|
106
|
+
writeJSON(target, s)
|
|
107
|
+
return publicSession(s)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function requestResult(id, cwd) {
|
|
111
|
+
const target = file('requests', id)
|
|
112
|
+
const request = readJSON(target)
|
|
113
|
+
if (request.repo !== repoKey(cwd)) throw new Error('이 요청은 현재 작업 저장소의 요청이 아닙니다.')
|
|
114
|
+
if (!['submitted', 'unknown'].includes(request.state)) return request
|
|
115
|
+
const s = sessionFor(request.to, cwd)
|
|
116
|
+
if (['pty', 'stdio'].includes(s.transport)) {
|
|
117
|
+
try { Object.assign(request, terminalRPC(s.id, { action: 'result', id })) }
|
|
118
|
+
catch (e) { request.state = 'unknown'; request.error = e.message }
|
|
119
|
+
writeJSON(target, request)
|
|
120
|
+
return request
|
|
121
|
+
}
|
|
122
|
+
const entries = transcript(s)
|
|
123
|
+
const marker = `[axMap request ${id}]`
|
|
124
|
+
const start = entries.findIndex((e, i) => i >= request.baseline && e.type === 'USER_INPUT' && String(e.content).includes(marker))
|
|
125
|
+
if (start < 0) return request
|
|
126
|
+
const tail = entries.slice(start + 1)
|
|
127
|
+
if (tail.some(e => e.type === 'USER_INPUT')) {
|
|
128
|
+
request.state = 'superseded'
|
|
129
|
+
request.error = '다른 사용자 입력이 이어져 이 요청의 완료를 확정할 수 없습니다.'
|
|
130
|
+
} else if (s.event === 'Stop' && s.fullyIdle && s.registeredAt > request.createdAt) {
|
|
131
|
+
const answer = tail.filter(e => e.source === 'MODEL' && e.type === 'PLANNER_RESPONSE' && e.status === 'DONE').at(-1)
|
|
132
|
+
if (answer) {
|
|
133
|
+
request.state = 'completed'
|
|
134
|
+
request.result = answer.content
|
|
135
|
+
request.completedAt = s.registeredAt
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (['completed', 'superseded'].includes(request.state)) writeJSON(target, request)
|
|
139
|
+
return request
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function sessionLeasePath(id) {
|
|
143
|
+
if (!UUID.test(id || '')) throw new Error('세션 ID는 UUID여야 합니다.')
|
|
144
|
+
return path.join(bridgeHome(), 'dialogues', `session-${id}.lock`)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function dispatchRequest({ to, prompt, cwd, from = 'axmap', dialogueId = null }) {
|
|
148
|
+
if (typeof prompt !== 'string' || !prompt.trim() || Buffer.byteLength(prompt) > 32768) throw new Error('요청 본문은 1~32768바이트여야 합니다.')
|
|
149
|
+
const s = sessionFor(to, cwd)
|
|
150
|
+
const lease = sessionLeasePath(to)
|
|
151
|
+
if (fs.existsSync(lease) && readJSON(lease).dialogueId !== dialogueId) throw new Error('이 세션은 자동 대화가 사용 중입니다. 해당 대화가 끝난 뒤 요청하세요.')
|
|
152
|
+
const dir = path.join(bridgeHome(), 'requests')
|
|
153
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 })
|
|
154
|
+
return withRepoLock(path.join(dir, `${to}.lock`), () => {
|
|
155
|
+
if (fs.existsSync(lease) && readJSON(lease).dialogueId !== dialogueId) throw new Error('이 세션은 자동 대화가 사용 중입니다.')
|
|
156
|
+
for (const n of fs.readdirSync(dir).filter(n => n.endsWith('.json'))) {
|
|
157
|
+
const previous = readJSON(path.join(dir, n))
|
|
158
|
+
if (previous.to !== to || !['sending', 'submitted', 'unknown'].includes(previous.state)) continue
|
|
159
|
+
const latest = previous.state === 'sending' ? previous : requestResult(previous.id, cwd)
|
|
160
|
+
if (['sending', 'submitted', 'unknown'].includes(latest.state)) throw new Error(`이 세션의 이전 요청이 아직 확정되지 않았습니다: ${latest.id}`)
|
|
161
|
+
}
|
|
162
|
+
// No fallback to `agy -p` or --conversation: both would start a separate process.
|
|
163
|
+
if (!['pty', 'stdio'].includes(s.transport)) {
|
|
164
|
+
runAPI(s, ['get-conversation-metadata', to])
|
|
165
|
+
if (!s.fullyIdle || s.event !== 'Stop') throw new Error('상대 세션의 대기 상태를 아직 확인하지 못했습니다. 작업 종료 훅을 기다리세요.')
|
|
166
|
+
}
|
|
167
|
+
const request = { id: randomUUID(), to, from, repo: repoKey(cwd), prompt,
|
|
168
|
+
createdAt: new Date().toISOString(), baseline: ['pty', 'stdio'].includes(s.transport) ? null : transcript(s).length, state: 'sending' }
|
|
169
|
+
const target = file('requests', request.id)
|
|
170
|
+
writeJSON(target, request)
|
|
171
|
+
try {
|
|
172
|
+
request.receipt = ['pty', 'stdio'].includes(s.transport)
|
|
173
|
+
? terminalRPC(to, { action: 'submit', request })
|
|
174
|
+
: runAPI(s, ['send-message', to, `[axMap request ${request.id}]\n요청자: ${from}\n${prompt}`])
|
|
175
|
+
request.state = 'submitted'
|
|
176
|
+
} catch (e) {
|
|
177
|
+
// A timeout may occur after delivery. Never retry automatically.
|
|
178
|
+
request.state = 'unknown'
|
|
179
|
+
request.error = e.message
|
|
180
|
+
}
|
|
181
|
+
writeJSON(target, request)
|
|
182
|
+
return request
|
|
183
|
+
})
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function terminalRPC(id, message) {
|
|
187
|
+
const r = spawnSync(process.execPath, [path.join(ROOT, 'tools', 'run.mjs'), 'rpc', id], {
|
|
188
|
+
input: JSON.stringify(message), encoding: 'utf8', windowsHide: true, timeout: 15000,
|
|
189
|
+
})
|
|
190
|
+
if (r.error || r.status !== 0) throw new Error(r.error?.message || r.stderr?.trim() || '터미널 세션에 연결하지 못했습니다.')
|
|
191
|
+
return JSON.parse(r.stdout)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export { bridgeHome, repoKey, readJSON, writeJSON }
|
|
195
|
+
|
|
196
|
+
export function uninstallHooks(home = os.homedir()) {
|
|
197
|
+
const target = path.join(home, '.gemini', 'config', 'hooks.json')
|
|
198
|
+
if (!fs.existsSync(target)) return { removed: false }
|
|
199
|
+
const config = readJSON(target)
|
|
200
|
+
if (!config || typeof config !== 'object' || Array.isArray(config)) throw new Error('기존 hooks.json이 객체가 아닙니다. 변경하지 않았습니다.')
|
|
201
|
+
if (!Object.hasOwn(config, 'axmap-bridge')) return { removed: false }
|
|
202
|
+
delete config['axmap-bridge']
|
|
203
|
+
writeJSON(target, config)
|
|
204
|
+
// A running provider can retain the old hook command until exit. Make its
|
|
205
|
+
// wrapper harmless immediately, without requiring another session restart.
|
|
206
|
+
if (process.platform === 'win32') {
|
|
207
|
+
for (const event of ['SessionStart', 'PreInvocation', 'Stop']) {
|
|
208
|
+
const wrapper = path.join(home, '.axmap', 'bridge', `agy-${event}.cmd`)
|
|
209
|
+
if (fs.existsSync(wrapper)) fs.writeFileSync(wrapper, '@echo off\r\necho {}\r\n')
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return { removed: true, hooks: target, note: '다른 훅은 유지합니다. 기존 CLI에 캐시된 훅은 정상 종료 시 해제됩니다.' }
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function installHooks({ executable, home = os.homedir() }) {
|
|
216
|
+
if (!path.isAbsolute(executable || '') || !fs.existsSync(executable)) throw new Error('agy 실행 파일의 절대 경로가 필요합니다.')
|
|
217
|
+
const target = path.join(home, '.gemini', 'config', 'hooks.json')
|
|
218
|
+
const original = fs.existsSync(target) ? fs.readFileSync(target, 'utf8') : null
|
|
219
|
+
const config = original === null ? {} : JSON.parse(original)
|
|
220
|
+
if (!config || typeof config !== 'object' || Array.isArray(config)) throw new Error('기존 hooks.json이 객체가 아닙니다. 변경하지 않았습니다.')
|
|
221
|
+
const script = path.join(ROOT, 'tools', 'bridge.mjs')
|
|
222
|
+
if ([process.execPath, script, executable].some(s => /["\r\n%&|<>^]/.test(s))) throw new Error('훅 명령 경로에 지원하지 않는 셸 문자가 있습니다.')
|
|
223
|
+
const definition = {}
|
|
224
|
+
for (const event of ['SessionStart', 'PreInvocation', 'Stop']) {
|
|
225
|
+
let command = `"${process.execPath}" "${script}" hook --event ${event} --executable "${executable}"`
|
|
226
|
+
if (process.platform === 'win32') {
|
|
227
|
+
// Antigravity's command runner escapes a quoted executable incorrectly on
|
|
228
|
+
// Windows. A bare .cmd entry point matches its documented hook examples;
|
|
229
|
+
// quotes inside the wrapper are interpreted by cmd, not its command parser.
|
|
230
|
+
const wrapper = path.join(home, '.axmap', 'bridge', `agy-${event}.cmd`)
|
|
231
|
+
if (/[\s"%&|<>^]/.test(wrapper)) throw new Error('Windows 훅 실행 경로에는 공백이나 셸 특수 문자가 없어야 합니다. 설정을 변경하지 않았습니다.')
|
|
232
|
+
fs.mkdirSync(path.dirname(wrapper), { recursive: true })
|
|
233
|
+
fs.writeFileSync(wrapper, `@echo off\r\n${command}\r\n`, { mode: 0o600 })
|
|
234
|
+
command = wrapper
|
|
235
|
+
}
|
|
236
|
+
definition[event] = [{ type: 'command', command, timeout: 10 }]
|
|
237
|
+
}
|
|
238
|
+
config['axmap-bridge'] = definition
|
|
239
|
+
fs.mkdirSync(path.dirname(target), { recursive: true })
|
|
240
|
+
const backup = `${target}.axmap-${randomUUID()}.bak`
|
|
241
|
+
if (original !== null) fs.writeFileSync(backup, original, { flag: 'wx', mode: 0o600 })
|
|
242
|
+
writeJSON(target, config)
|
|
243
|
+
return { hooks: target, backup: original === null ? null : backup,
|
|
244
|
+
note: '기존 실행 중인 CLI가 새 훅을 읽지 않으면 같은 대화를 재개해 한 번 다시 열어야 합니다. Remote Control은 필요 없습니다.' }
|
|
245
|
+
}
|
package/src/invariants.mjs
CHANGED
|
@@ -96,7 +96,7 @@ export function resurrectionViolations({ before, after, requested, me, now }) {
|
|
|
96
96
|
|
|
97
97
|
// ---------------------------------------------------------------------------
|
|
98
98
|
// I7 · 세션 이름 유일성 (상태 불변식)
|
|
99
|
-
//
|
|
99
|
+
// 관리 중인 동시에 살아있는 두 세션은 서로 다른 AXMAP_AGENT 를 갖는다.
|
|
100
100
|
//
|
|
101
101
|
// 🔴 "모든 에이전트의 이름이 다르다" 는 여기에 적을 수 없다. 사람이 두 셸에
|
|
102
102
|
// 같은 AXMAP_AGENT 를 넣으면 시스템이 막을 방법이 없고, 검사할 수 없는 문장을
|