axmap-cli 1.4.0 → 1.6.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.
Files changed (46) hide show
  1. package/README.ko.md +36 -0
  2. package/README.md +36 -0
  3. package/bin/axmap.mjs +6 -0
  4. package/mcp/README.md +6 -0
  5. package/mcp/pipeline.html +157 -0
  6. package/mcp/server.mjs +130 -20
  7. package/package.json +11 -3
  8. package/src/agentcli.mjs +17 -14
  9. package/src/agentcli.mts +19 -0
  10. package/src/bridge.mjs +299 -210
  11. package/src/bridge.mts +281 -0
  12. package/src/external-console.mjs +159 -0
  13. package/src/external-console.mts +128 -0
  14. package/src/managed-session.mjs +263 -169
  15. package/src/managed-session.mts +198 -0
  16. package/src/monitor-cli.mjs +87 -0
  17. package/src/monitor-cli.mts +64 -0
  18. package/src/monitor.mjs +240 -0
  19. package/src/monitor.mts +195 -0
  20. package/src/node-pty.d.ts +13 -0
  21. package/src/pipeline-mcp.mjs +109 -0
  22. package/src/pipeline-mcp.mts +103 -0
  23. package/src/pipeline-statusline.mjs +135 -0
  24. package/src/pipeline-statusline.mts +106 -0
  25. package/src/session-dialogue.mjs +168 -116
  26. package/src/session-dialogue.mts +136 -0
  27. package/src/session-status.mjs +54 -0
  28. package/src/session-status.mts +50 -0
  29. package/src/session-types.mjs +7 -0
  30. package/src/session-types.mts +118 -0
  31. package/src/session-usage.mjs +33 -28
  32. package/src/session-usage.mts +39 -0
  33. package/src/terminal-bridge.mjs +294 -209
  34. package/src/terminal-bridge.mts +235 -0
  35. package/src/windows-console.mjs +42 -0
  36. package/src/windows-console.mts +29 -0
  37. package/tools/bench-console.mjs +70 -0
  38. package/tools/bridge.mjs +20 -6
  39. package/tools/build.mjs +39 -0
  40. package/tools/monitor.mjs +5 -0
  41. package/tools/pipeline.mjs +26 -0
  42. package/tools/run-tests.mjs +3 -2
  43. package/tools/run.mjs +6 -2
  44. package/tools/setup.mjs +1 -1
  45. package/tools/windows-console.cs +116 -0
  46. package/tools/windows-console.ps1 +91 -0
package/README.ko.md CHANGED
@@ -19,12 +19,48 @@ axmap setup
19
19
 
20
20
  [사용법](docs/INSTALL.md) · [MCP 연결](mcp/README.md) · [개발 가이드](docs/ONBOARD-TEAM.md)
21
21
 
22
+ 세션·메시지 핵심 모듈은 strict TypeScript로 작성합니다. 단계적 전환 범위와 빌드 방법은 [TypeScript 개발 안내](docs/TYPESCRIPT.md)를 참고하세요.
23
+
24
+ 지원되는 MCP Apps 호스트에서는 대화 안에서 파이프라인을 확인하고, Claude Code·agy에서는 기존 상태줄에 진행을 추가할 수 있습니다. 호스트별 지원이 다르며 ChatGPT·Claude 앱의 실제 임베디드 화면은 아직 검증하지 않았습니다. [클라이언트 설정과 제한](docs/PIPELINE-CLIENTS.md). 선택적으로 별도 터미널의 `axmap monitor`도 제공합니다([사용법](docs/MONITOR.md)).
25
+
22
26
  ---
23
27
 
24
28
  ## 변경 기록
25
29
 
26
30
  최신 변경부터 정리합니다.
27
31
 
32
+ ### 1.6.0
33
+
34
+ <sub>2026.10.26</sub>
35
+
36
+ **요청 흐름을 표시하고 기존 Windows agy 연결의 관측 비용을 줄입니다.**
37
+
38
+ - Windows 콘솔 도우미를 매번 컴파일하지 않고 캐시하며 PowerShell 대체 경로를 유지합니다.
39
+ - 제한 시간 안에서 응답을 기다리는 `wait_ms`·`--wait-ms`와 비동기 MCP 진행 알림을 추가합니다.
40
+ - MCP Apps 파이프라인 리소스, 텍스트 대체 출력, 기존 명령을 보존하는 Claude Code·agy 상태줄을 제공합니다. 별도 터미널 표시가 필요하면 `axmap monitor`를 사용합니다.
41
+ - 세션·파이프라인 모듈을 strict TypeScript로 작성하며 Node.js 20+용 JavaScript를 함께 배포합니다. 실행에 TypeScript 설치는 필요하지 않습니다.
42
+
43
+ Windows의 실제 agy에서 전달·진행 알림·상태줄의 완료 표시를 확인했습니다. ChatGPT·Claude UI는 기능 협상과 프로토콜 테스트로 검증했으며 실제 제품의 임베디드 화면 검증은 남아 있습니다. 이 Codex 세션에는 새 파이프라인 도구가 노출되지 않았습니다. 로컬 stdio 지원만으로 ChatGPT 웹용 원격 엔드포인트가 생기지는 않습니다.
44
+ [클라이언트 설정과 제한 →](docs/PIPELINE-CLIENTS.md)
45
+
46
+ ---
47
+
48
+ ### 1.5.0
49
+
50
+ <sub>2026.10.21</sub>
51
+
52
+ **Windows에서 따로 실행한 Antigravity 대화창을 재시작 없이 연결합니다.**
53
+
54
+ - 일반 `agy`로 직접 연 대화창도 설치된 훅으로 자동 등록합니다. 이미 열린 창은 대상 저장소에서 `axmap bridge attach --pid <PID>`로 연결합니다.
55
+ - `ax_dispatch`와 `ax_result`가 기존 프로세스와 대화를 유지합니다. 빈 입력창의 대기 상태를 확인해 전달하고, 요청 UUID가 있는 대화 기록과 현재 콘솔 상태로 결과를 판정합니다.
56
+ - `axmap bridge status [--json]`으로 모델 작업을 보내지 않고 로컬 연결 상태를 확인합니다.
57
+ - 독립 콘솔 연결은 현재 Windows agy만 지원합니다. 토큰 예산을 검사하는 `ax_chat`은 관리형 stdio 세션을 사용합니다.
58
+
59
+ 실제 기존 agy 창의 메시지 왕복과 회귀 테스트 574개 통과를 확인했습니다. 독립 콘솔의 자동 등록, 한글 전달, 입력 보호와 프로세스 식별 검사도 포함합니다.
60
+ [연결 안내 →](docs/SESSION-BRIDGE.md)
61
+
62
+ ---
63
+
28
64
  ### 1.4.0
29
65
 
30
66
  <sub>2026.10.20</sub>
package/README.md CHANGED
@@ -19,12 +19,48 @@ Each Git repository's ledger is prepared on first use. Collaborators must share
19
19
 
20
20
  [Usage](docs/INSTALL.md) · [MCP setup](mcp/README.md) · [Development](docs/ONBOARD-TEAM.md)
21
21
 
22
+ Session and messaging modules are authored in strict TypeScript. See the [TypeScript development guide](docs/TYPESCRIPT.md) for the incremental migration and build workflow.
23
+
24
+ View pipeline state inside compatible MCP Apps hosts, or add it to the existing Claude Code / agy statusline. Host support varies; actual ChatGPT and Claude embedded rendering remains unverified. See [client support and setup](docs/PIPELINE-CLIENTS.md). An optional separate terminal view is available with `axmap monitor` ([guide](docs/MONITOR.md)).
25
+
22
26
  ---
23
27
 
24
28
  ## Changelog
25
29
 
26
30
  Newest changes first.
27
31
 
32
+ ### 1.6.0
33
+
34
+ <sub>October 26, 2026</sub>
35
+
36
+ **Observe request flow and reduce the overhead of existing Windows agy connections.**
37
+
38
+ - Cache the Windows console helper instead of recompiling it for every observation, with PowerShell fallback.
39
+ - Add bounded `wait_ms` / `--wait-ms` response waiting and asynchronous MCP progress notifications.
40
+ - Add MCP Apps pipeline resources, text fallback, and optional Claude Code / agy statuslines that preserve existing commands. An optional `axmap monitor` shows the same flow in a terminal.
41
+ - Author session and pipeline modules in strict TypeScript while shipping generated JavaScript for Node.js 20+; TypeScript is not required at runtime.
42
+
43
+ Actual agy delivery, progress and native statusline completion were verified on Windows. ChatGPT / Claude UI support is capability-based and covered by protocol tests; embedded rendering in those products remains unverified. This Codex session did not expose the new pipeline tools. Local stdio support does not provide a remote ChatGPT web endpoint.
44
+ [Client setup and limits →](docs/PIPELINE-CLIENTS.md)
45
+
46
+ ---
47
+
48
+ ### 1.5.0
49
+
50
+ <sub>October 21, 2026</sub>
51
+
52
+ **Connect an existing Windows Antigravity conversation without restarting it.**
53
+
54
+ - Interactive `agy` sessions launched directly register through the installed hooks. Use `axmap bridge attach --pid <PID>` from the target repository for an already open console.
55
+ - `ax_dispatch` and `ax_result` preserve the existing process and conversation. Delivery requires an empty, idle input box; results match the request UUID in the transcript and the live console state.
56
+ - `axmap bridge status [--json]` checks local connections without submitting model work.
57
+ - Independent console connections currently support Windows agy only. Token-budgeted `ax_chat` still requires managed stdio sessions.
58
+
59
+ Validated against an existing real agy console and 574 passing regression tests, including independent-console registration, Unicode delivery, input protection and process identity checks.
60
+ [Connection guide →](docs/SESSION-BRIDGE.md)
61
+
62
+ ---
63
+
28
64
  ### 1.4.0
29
65
 
30
66
  <sub>October 20, 2026</sub>
package/bin/axmap.mjs CHANGED
@@ -2624,6 +2624,8 @@ exec node "$AX" verify
2624
2624
  const RUNNERS = {
2625
2625
  run: ['tools/run.mjs', '사용자가 여는 AI CLI를 연결한다'],
2626
2626
  bridge: ['tools/bridge.mjs', '열린 로컬 AI 세션에 작업을 전달한다'],
2627
+ monitor: ['tools/monitor.mjs', '세션과 요청 파이프라인을 실시간으로 표시한다'],
2628
+ pipeline: ['tools/pipeline.mjs', '대화 안 파이프라인 표시와 CLI 상태줄을 연결한다'],
2627
2629
  gate: ['governance/gate.mjs', '이 MR 이 정족수를 채웠는가'],
2628
2630
  vote: ['governance/vote.mjs', '표를 던진다'],
2629
2631
  bus: ['tools/bus.mjs', '에이전트 사이 쪽지함'],
@@ -2834,6 +2836,8 @@ const HELP = `axmap - AI 에이전트 작업 선점 프로토콜
2834
2836
  axmap vote 표를 던진다
2835
2837
  axmap bus 에이전트 사이 쪽지함 (post · list · read · reply)
2836
2838
  axmap bridge 로컬 세션·자동 대화 (serve · sessions · send · chat · dialogue)
2839
+ axmap monitor 에이전트·요청 파이프라인·최근 이벤트 실시간 화면
2840
+ axmap pipeline 대화 안 표시·Claude/agy 상태줄 연결 (--help)
2837
2841
  axmap run agy [인수...] 연결된 Antigravity CLI를 직접 연다
2838
2842
  axmap mr-target 이 브랜치가 저 브랜치로 갈 수 있는가
2839
2843
  axmap version 브랜치 단계로 다음 버전을 정한다
@@ -2876,6 +2880,8 @@ switch (cmd) {
2876
2880
  // 이름을 경로로 바꿔 주는 것들 (RUNNERS). 인자는 손대지 않고 그대로 간다.
2877
2881
  case 'setup':
2878
2882
  case 'bridge':
2883
+ case 'monitor':
2884
+ case 'pipeline':
2879
2885
  case 'run':
2880
2886
  case 'gate':
2881
2887
  case 'vote':
package/mcp/README.md CHANGED
@@ -30,10 +30,16 @@ https://github.com/rl2aderjoon/axmap/blob/main/mcp/SETUP-FOR-AI.md
30
30
 
31
31
  ## 도구
32
32
 
33
+ 현재 대화 안의 파이프라인 패널과 Claude Code·agy 상태줄 설치는
34
+ [PIPELINE-CLIENTS.md](../docs/PIPELINE-CLIENTS.md)를 참고한다.
35
+
33
36
  | 도구 | 언제 |
34
37
  |---|---|
35
38
  | `ax_brief()` | 이 저장소에서 처음 일할 때 **가장 먼저** |
36
39
  | `ax_status()` | 지금 누가 어디를 잡고 있나 |
40
+ | `ax_pipeline()` | 대화 안의 파이프라인 패널 또는 텍스트 스냅샷 |
41
+ | `ax_pipeline_state()` | MCP Apps 패널의 상태 갱신 전용 |
42
+ | `ax_pipeline_watch()` | 현재 도구 호출의 진행 알림으로 상태 관찰 |
37
43
  | `ax_check(paths)` | 내가 건드릴 곳이 비었나 — **선언은 안 한다.** 계획 세울 때 |
38
44
  | `ax_claim(paths, intent, task?)` | **파일을 고치기 전에 반드시.** intent 는 사람이 화면에서 읽는다 |
39
45
  | `ax_release(paths?)` | 끝나면 즉시. 생략하면 전부 반납 |
@@ -0,0 +1,157 @@
1
+ <!doctype html>
2
+ <html lang="ko">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width,initial-scale=1">
6
+ <style>
7
+ @import url('https://cdn.jsdelivr.net/npm/pretendard@1.3.9/dist/web/static/pretendard.css');
8
+ :root { color-scheme:light dark; --surface:var(--color-background-primary,light-dark(#fff,#17191d)); --ink:var(--color-text-primary,light-dark(#20242b,#edf0f5)); --muted:var(--color-text-secondary,light-dark(#5b6574,#afb8c6)); --line:var(--color-border-primary,light-dark(#dce1e8,#3b414b)); --accent:light-dark(#2461c6,#80b0ff); --soft:light-dark(#edf3ff,#25364e); --warning:light-dark(#8a5600,#f5c56b); }
9
+ :root[data-theme="dark"] { color-scheme:dark; }
10
+ :root[data-theme="light"] { color-scheme:light; }
11
+ * { box-sizing:border-box; }
12
+ body { margin:0; padding:18px; background:var(--surface); color:var(--ink); font:14px/1.55 Pretendard,system-ui,sans-serif; }
13
+ h1 { font-size:18px; font-weight:600; margin:0; }
14
+ h2 { font-size:12px; font-weight:500; color:var(--muted); margin:20px 0 8px; }
15
+ header { display:flex; flex-wrap:wrap; align-items:center; gap:12px; }
16
+ .status { display:flex; align-items:center; gap:7px; color:var(--muted); font-size:12px; }
17
+ .status::before { content:''; width:7px; height:7px; border-radius:50%; background:var(--muted); }
18
+ body[data-live="true"] .status::before { background:var(--accent); }
19
+ .scope { font-size:12px; color:var(--muted); margin:6px 0 16px; overflow-wrap:anywhere; }
20
+ button { margin-left:auto; font:inherit; color:var(--ink); background:var(--surface); border:1px solid var(--line); border-radius:6px; padding:5px 10px; cursor:pointer; }
21
+ button:focus-visible { outline:2px solid var(--accent); outline-offset:3px; }
22
+ .sessions { display:flex; flex-wrap:wrap; gap:8px 18px; font-size:12px; }
23
+ .session { display:flex; flex-wrap:wrap; gap:6px; align-items:center; }
24
+ .session strong { font-weight:600; }
25
+ .subtle { color:var(--muted); }
26
+ .job { padding:14px 0; border-bottom:1px solid var(--line); }
27
+ .job:last-child { border-bottom:0; }
28
+ .route { font-weight:500; overflow-wrap:anywhere; }
29
+ .task { margin-top:9px; color:var(--muted); overflow-wrap:anywhere; }
30
+ .steps { display:grid; grid-template-columns:repeat(4,minmax(0,1fr)); gap:8px; margin-top:16px; }
31
+ .step { position:relative; display:flex; flex-direction:column; align-items:center; text-align:center; gap:6px; font-size:12px; }
32
+ .step:not(:last-child)::after { content:''; position:absolute; top:14px; left:calc(50% + 18px); width:calc(100% - 28px); height:1px; background:var(--line); }
33
+ .mark { display:grid; place-items:center; position:relative; width:28px; height:28px; border-radius:50%; background:var(--line); color:var(--ink); }
34
+ .step.done .mark,.step.current .mark { background:var(--soft); }
35
+ .step.current .mark::after { content:''; position:absolute; inset:-3px; border:2px solid transparent; border-top-color:var(--accent); border-radius:50%; animation:spin 1s linear infinite; }
36
+ body:not([data-live="true"]) .step.current .mark::after { animation-play-state:paused; }
37
+ .issue { color:var(--warning); margin-top:10px; overflow-wrap:anywhere; }
38
+ .events { list-style:none; padding:0; margin:0; font-size:12px; }
39
+ .events li { padding:3px 0; overflow-wrap:anywhere; }
40
+ time { color:var(--muted); margin-right:10px; font-variant-numeric:tabular-nums; }
41
+ footer { border-top:1px solid var(--line); padding-top:10px; margin-top:14px; color:var(--muted); font-size:12px; }
42
+ @keyframes spin { to { transform:rotate(360deg); } }
43
+ @media (prefers-reduced-motion:reduce) { .step.current .mark::after { animation:none; } }
44
+ @media(max-width:420px) { body{padding:12px;} .steps{grid-template-columns:1fr;gap:10px;} .step{flex-direction:row;text-align:left;gap:12px;} .step:not(:last-child)::after{top:31px;left:14px;height:7px;width:1px;} button{margin-left:0;} }
45
+ </style>
46
+ </head>
47
+ <body data-live="false">
48
+ <main id="axmap-pipeline">
49
+ <header><h1>axMap 실행 흐름</h1><span class="status" id="status">연결 확인 중</span><button type="button" id="pause">새로고침 일시정지</button></header>
50
+ <div class="scope" id="scope">실제 작업 상태를 기다리는 중</div>
51
+ <div class="sessions" id="sessions"></div>
52
+ <section aria-label="요청 파이프라인" id="jobs"></section>
53
+ <div class="issue" id="issue" role="status" aria-live="polite"></div>
54
+ <section id="event-section"><h2>최근 이벤트</h2><ul class="events" id="events"></ul></section>
55
+ <footer id="checked">대화 안에서 상태만 조회합니다. 모델 호출·작업 재전송·자동 승인 없음.</footer>
56
+ </main>
57
+ <script>
58
+ (() => {
59
+ const root=document.getElementById('axmap-pipeline');
60
+ const status=document.getElementById('status'),scope=document.getElementById('scope');
61
+ const sessions=document.getElementById('sessions'),jobs=document.getElementById('jobs');
62
+ const issue=document.getElementById('issue'),events=document.getElementById('events');
63
+ const checked=document.getElementById('checked'),pause=document.getElementById('pause');
64
+ const requests=new Map();let sequence=0,ready=false,mode='standard',cwd=null,requestedCwd=null,paused=false,stopped=false,inflight=false,timer,unavailable=false;
65
+ const pending=new Set(['sending','submitted','unknown']);
66
+ const states={sending:'전달 중',submitted:'응답 대기',completed:'완료 확인',unknown:'전송 확인 필요',failed:'실패',superseded:'다른 입력으로 대체됨'};
67
+ const sessionStates={starting:'시작 중',running:'작업 중',idle:'대기',awaiting_user:'승인 필요',settling:'대기 전환 중',closed:'종료',unknown:'상태 미확인'};
68
+ const connectionStates={connected:'연결됨',closed:'종료됨',unreachable:'연결 확인 불가',unverified:'확인 미지원'};
69
+ const text=value=>String(value??'').replace(/[\x00-\x1f\x7f-\x9f\u202a-\u202e\u2066-\u2069]/g,' ').slice(0,600);
70
+ const node=(tag,content,className)=>{const el=document.createElement(tag);if(content!==undefined)el.textContent=text(content);if(className)el.className=className;return el};
71
+ const notify=(method,params)=>window.parent.postMessage({jsonrpc:'2.0',method,params},'*');
72
+ function request(method,params){
73
+ const id=`axmap-ui-${++sequence}`;
74
+ return new Promise((resolve,reject)=>{
75
+ const timeout=setTimeout(()=>{requests.delete(id);reject(new Error('호스트 응답 시간 초과'))},10000);
76
+ requests.set(id,{resolve,reject,timeout});window.parent.postMessage({jsonrpc:'2.0',id,method,params},'*');
77
+ });
78
+ }
79
+ function context(value){
80
+ if(value?.theme==='light'||value?.theme==='dark')document.documentElement.dataset.theme=value.theme;
81
+ const allowed=new Set(['--color-background-primary','--color-text-primary','--color-text-secondary','--color-border-primary']);
82
+ for(const [key,color]of Object.entries(value?.styles?.variables||{}))if(allowed.has(key)&&typeof color==='string')document.documentElement.style.setProperty(key,color);
83
+ }
84
+ function setLive(value,message){document.body.dataset.live=String(value);status.textContent=message;}
85
+ function render(data){
86
+ if(!data||data.schemaVersion!==1||data.type!=='pipeline'||typeof data.cwd!=='string'||!Array.isArray(data.requests)||!Array.isArray(data.sessions))return;
87
+ if(cwd&&data.cwd!==cwd){issue.textContent='다른 저장소의 상태를 받았습니다. 현재 패널의 저장소를 유지합니다.';setLive(false,'저장소 확인 필요');return;}
88
+ cwd=data.cwd;scope.textContent=text(cwd);sessions.replaceChildren();jobs.replaceChildren();events.replaceChildren();
89
+ for(const session of data.sessions.slice(0,6)){
90
+ const line=node('div',undefined,'session');line.append(node('strong',`${session.provider} · ${text(session.id).slice(0,8)}`),node('span',`${session.pendingApproval?'사용자 승인 필요':sessionStates[session.state]||'상태 미확인'} · ${connectionStates[session.connection]||'확인 미지원'}`,'subtle'));sessions.append(line);
91
+ }
92
+ const active=data.requests.filter(r=>pending.has(r.state));
93
+ const selected=[...active,...data.requests.filter(r=>!pending.has(r.state))].slice(0,4);
94
+ if(!selected.length)jobs.append(node('p','새 요청을 기다리는 중','subtle'));
95
+ for(const job of selected){
96
+ const item=node('article',undefined,'job');item.append(node('div',`${text(job.from)} → ${text(job.to).slice(0,8)} · ${states[job.state]||'확인 필요'}`,'route'));
97
+ const steps=node('div',undefined,'steps');
98
+ const done=job.state==='completed',sent=job.state==='submitted'||done;
99
+ ['접수','전달','응답','완료 확인'].forEach((label,index)=>{
100
+ const complete=done||index===0||(sent&&index===1);
101
+ const current=!done&&((job.state==='sending'&&index===1)||(job.state==='submitted'&&index===2));
102
+ const step=node('div',undefined,`step${complete?' done':current?' current':''}`);
103
+ const mark=node('span',complete?'✓':job.state==='unknown'&&index===1?'?':String(index+1),'mark');mark.setAttribute('aria-hidden','true');
104
+ step.append(mark,node('span',current&&index===2?'응답 대기':label));steps.append(step);
105
+ });
106
+ item.append(steps,node('div',job.task,'task'));if(job.error)item.append(node('div',job.error,'issue'));jobs.append(item);
107
+ }
108
+ for(const event of (data.events||[]).slice(0,5)){
109
+ const row=node('li');const date=new Date(event.at);row.append(node('time',Number.isFinite(date.getTime())?date.toLocaleTimeString('en-GB',{hour12:false}):'시각 미확인'),node('span',event.label));events.append(row);
110
+ }
111
+ issue.textContent=(data.issues||[]).slice(0,2).map(text).join(' · ');
112
+ checked.textContent=`관측 ${text(data.checkedAt)} · 상태 조회만 수행 · 작업 제어 없음`;
113
+ setLive(ready&&!paused&&!document.hidden,!ready?(unavailable?'현재 상태 · 자동 갱신 미지원':'연결 확인 중'):paused?'새로고침 일시정지':document.hidden?'화면 숨김':`진행 확인 ${active.length}개`);
114
+ }
115
+ function consume(result){if(result?.isError)throw new Error(result.content?.[0]?.text||'상태 조회 실패');render(result?.structuredContent);}
116
+ function schedule(){clearTimeout(timer);if(ready&&!paused&&!stopped&&!document.hidden)timer=setTimeout(poll,1200);}
117
+ async function poll(){
118
+ if(inflight||!ready||paused||stopped||document.hidden)return;
119
+ inflight=true;
120
+ try{const scope=cwd||requestedCwd,args=scope?{cwd:scope}:{};const result=mode==='openai'?await window.openai.callTool('ax_pipeline_state',args):await request('tools/call',{name:'ax_pipeline_state',arguments:args});consume(result);}
121
+ catch(error){setLive(false,'연결 확인 필요');issue.textContent=text(error?.message||error);}
122
+ finally{inflight=false;schedule();}
123
+ }
124
+ window.addEventListener('message',event=>{
125
+ if(event.source!==window.parent)return;const message=event.data;if(!message||message.jsonrpc!=='2.0')return;
126
+ if(requests.has(message.id)){const waiting=requests.get(message.id);requests.delete(message.id);clearTimeout(waiting.timeout);message.error?waiting.reject(new Error(message.error.message||'호스트 오류')):waiting.resolve(message.result);return;}
127
+ if(message.method==='ui/notifications/tool-result'){try{consume(message.params)}catch(error){issue.textContent=text(error.message);}}
128
+ if(message.method==='ui/notifications/tool-input'&&!cwd&&typeof message.params?.arguments?.cwd==='string')requestedCwd=message.params.arguments.cwd;
129
+ if(message.method==='ui/notifications/host-context-changed')context(message.params);
130
+ if(message.method==='ui/resource-teardown'){
131
+ stopped=true;clearTimeout(timer);resize.disconnect();setLive(false,'관측 종료');
132
+ if(message.id!==undefined)window.parent.postMessage({jsonrpc:'2.0',id:message.id,result:{}},'*');
133
+ }
134
+ });
135
+ document.addEventListener('visibilitychange',()=>{if(document.hidden){clearTimeout(timer);setLive(false,'화면 숨김')}else if(!paused){void poll()}});
136
+ pause.addEventListener('click',()=>{paused=!paused;pause.textContent=paused?'실시간 새로고침 재개':'새로고침 일시정지';if(paused){clearTimeout(timer);setLive(false,'새로고침 일시정지')}else{void poll()}});
137
+ let reportedHeight=0;
138
+ const resize=new ResizeObserver(()=>{if(!ready||mode!=='standard'||stopped)return;const height=Math.ceil(root.getBoundingClientRect().height)+36;if(height!==reportedHeight){reportedHeight=height;notify('ui/notifications/size-changed',{height})}});
139
+ resize.observe(root);
140
+ window.addEventListener('pagehide',()=>{stopped=true;clearTimeout(timer);resize.disconnect()});
141
+ if(window.openai?.toolOutput)render(window.openai.toolOutput);
142
+ (async()=>{
143
+ try{
144
+ const result=await request('ui/initialize',{protocolVersion:'2026-01-26',appInfo:{name:'axmap-pipeline',version:'1.0.0'},appCapabilities:{availableDisplayModes:['inline']}});
145
+ context(result?.hostContext);notify('ui/notifications/initialized',{});
146
+ ready=!!result?.hostCapabilities?.serverTools;
147
+ if(!ready){unavailable=true;setLive(false,'자동 갱신 미지원');issue.textContent='현재 호스트가 상태 조회 도구 호출을 제공하지 않습니다. ax_pipeline_watch로 진행 상태를 확인할 수 있습니다.';return;}
148
+ }catch(error){
149
+ if(typeof window.openai?.callTool==='function'){mode='openai';ready=true;}
150
+ else{unavailable=true;setLive(false,'MCP Apps 연결 필요');issue.textContent='이 화면에서는 실시간 UI 연결을 확인하지 못했습니다. 현재 대화에서 ax_pipeline_watch를 사용할 수 있습니다.';return;}
151
+ }
152
+ void poll();
153
+ })();
154
+ })();
155
+ </script>
156
+ </body>
157
+ </html>
package/mcp/server.mjs CHANGED
@@ -40,7 +40,7 @@
40
40
  * stdio 전송 — 줄바꿈으로 구분된 JSON-RPC 2.0.
41
41
  */
42
42
 
43
- import { spawnSync } from 'node:child_process'
43
+ import { spawn, spawnSync } from 'node:child_process'
44
44
  import fs from 'node:fs'
45
45
  import path from 'node:path'
46
46
  import readline from 'node:readline'
@@ -213,6 +213,57 @@ const TTL = process.env.AXMAP_TTL ?? '45m'
213
213
  const SESSION = process.env.AXMAP_SESSION || process.env.CLAUDE_CODE_SESSION_ID || `mcp-${process.pid}`
214
214
 
215
215
  const BRIDGE_TOOLS = new Set(['ax_sessions', 'ax_dispatch', 'ax_result', 'ax_chat', 'ax_dialogue', 'ax_chat_stop'])
216
+ const PIPELINE_TOOLS = new Set(['ax_pipeline', 'ax_pipeline_state', 'ax_pipeline_watch'])
217
+ const PIPELINE_URI = 'ui://axmap/pipeline/v1.html'
218
+ const PIPELINE_MIME = 'text/html;profile=mcp-app'
219
+ let clientHasApps = false
220
+ const runningPipeline = new Map()
221
+
222
+ /** Each observation captures its repo. It may run beside a long dispatch without
223
+ * changing REPO, switching claims, or blocking UI refresh behind model work. */
224
+ function pipeline(action, args, req, cwd) {
225
+ return new Promise(resolve => {
226
+ const offered = req.params?._meta?.progressToken
227
+ const token = typeof offered === 'string' || Number.isInteger(offered) ? offered : undefined
228
+ let buffer = '', stderr = '', result = null, ended = false, sequence = 0, reason = ''
229
+ const child = spawn(process.execPath, [path.join(SELF, 'tools', 'pipeline.mjs'), action], {
230
+ cwd, windowsHide: true, env: { ...process.env, AXMAP_AGENT: AGENT }, stdio: ['pipe', 'pipe', 'pipe', 'ipc'],
231
+ })
232
+ const finish = fallback => {
233
+ if (ended) return
234
+ ended = true; clearTimeout(timer); runningPipeline.delete(req.id)
235
+ const answer = result || { content: [{ type: 'text', text: fallback || reason || stderr || '파이프라인 응답을 확인하지 못했습니다. 요청을 자동 재전송하지 않습니다.' }], isError: true }
236
+ if (answer.structuredContent) answer.structuredContent.display = { mode: clientHasApps ? 'mcp-app' : 'text-progress', progress: token !== undefined }
237
+ resolve(answer)
238
+ }
239
+ const timer = setTimeout(() => { reason = '파이프라인 조회 시간 초과. 이미 전달한 작업은 계속될 수 있습니다.'; child.kill() }, 45000)
240
+ runningPipeline.set(req.id, () => {
241
+ reason = '조회 대기를 중단했습니다. 이미 전달한 작업은 계속 실행됩니다.'
242
+ // A dispatch holds a cross-process lock during submission. Let it finish
243
+ // that atomic step, then stop waiting; killing it could strand the lock.
244
+ if (child.connected) child.send({ type: 'cancel-observation' }, () => {})
245
+ })
246
+ child.stdout.setEncoding('utf8')
247
+ child.stdout.on('data', chunk => {
248
+ buffer += chunk
249
+ if (Buffer.byteLength(buffer) > 2 * 1024 * 1024) { reason = '파이프라인 응답 크기 제한 초과'; child.kill(); return }
250
+ while (buffer.includes('\n')) {
251
+ const at = buffer.indexOf('\n'), line = buffer.slice(0, at); buffer = buffer.slice(at + 1)
252
+ try {
253
+ const event = JSON.parse(line)
254
+ if (event.type === 'progress' && token !== undefined && !result && !ended && !reason) {
255
+ send({ jsonrpc: '2.0', method: 'notifications/progress', params: { progressToken: token, progress: ++sequence, message: String(event.message) } })
256
+ } else if (event.type === 'result' && event.result?.content) result = event.result
257
+ } catch { reason = '파이프라인 응답 형식 오류'; child.kill() }
258
+ }
259
+ })
260
+ child.stderr.on('data', chunk => { stderr = (stderr + chunk.toString('utf8')).slice(-8192) })
261
+ child.stdin.on('error', () => {})
262
+ child.once('error', error => finish(error.message))
263
+ child.once('close', () => finish())
264
+ child.stdin.end(JSON.stringify({ ...args, __progress: token !== undefined }))
265
+ })
266
+ }
216
267
  function bridge(args, input) {
217
268
  const r = spawnSync(process.execPath, [path.join(SELF, 'tools', 'bridge.mjs'), ...args], {
218
269
  cwd: REPO, input, encoding: 'utf8', windowsHide: true,
@@ -385,6 +436,29 @@ function cli(args) {
385
436
  // ---------------------------------------------------------------------------
386
437
 
387
438
  const TOOLS = [
439
+ {
440
+ name: 'ax_pipeline',
441
+ description: '현재 대화에서 axMap 실행 흐름을 연다. MCP Apps 지원 화면은 실시간 패널, 다른 클라이언트는 같은 상태를 텍스트로 표시한다. 상태 조회만 수행하며 새 창이나 모델을 실행하지 않는다.',
442
+ annotations: { readOnlyHint: true, openWorldHint: false },
443
+ inputSchema: { type: 'object', properties: {} },
444
+ _meta: { ui: { resourceUri: PIPELINE_URI }, 'openai/outputTemplate': PIPELINE_URI, 'antigravity/alwaysLoad': true },
445
+ },
446
+ {
447
+ name: 'ax_pipeline_state',
448
+ description: '열린 파이프라인 패널의 실제 상태를 갱신한다. 다른 작업을 중단하거나 재전송하지 않는다.',
449
+ annotations: { readOnlyHint: true, openWorldHint: false },
450
+ inputSchema: { type: 'object', properties: {} },
451
+ _meta: { ui: { visibility: ['app'] } },
452
+ },
453
+ {
454
+ name: 'ax_pipeline_watch',
455
+ description: '현재 대화의 도구 진행 표시로 axMap 상태를 관측한다. progressToken이 있는 클라이언트에 MCP 진행 알림을 보내며, 없는 경우 현재 상태를 한 번 반환한다. 모델 호출이나 새 창 없음.',
456
+ annotations: { readOnlyHint: true, openWorldHint: false },
457
+ inputSchema: { type: 'object', properties: {
458
+ duration_ms: { type: 'integer', minimum: 0, maximum: 25000, description: '최대 관측 시간. 기본 5000ms.' },
459
+ interval_ms: { type: 'integer', minimum: 500, maximum: 5000, description: '조회 간격. 기본 1000ms.' },
460
+ } },
461
+ },
388
462
  {
389
463
  name: 'ax_sessions',
390
464
  description: '현재 repo에 연결된 로컬 AI 세션, 역할, 상태와 측정 토큰 사용량을 확인한다. stdio는 사용자가 bridge serve로 시작한 관리형 세션이다. 등록만으로 연결 가능성이나 실행 성공을 보장하지 않는다.',
@@ -392,17 +466,19 @@ const TOOLS = [
392
466
  },
393
467
  {
394
468
  name: 'ax_dispatch',
395
- description: '사용자가 허용한 작업을 연결된 상대 세션에 전달한다. 기존 맥락을 유지하며 새 모델 프로세스를 만들지 않는다. submitted는 완료가 아니다. unknown이면 자동 재시도하지 말고 ax_result로 확인한다. Codex/agy 관리형 stdio, 실험용 PTY/agentapi 연결을 지원한다.',
469
+ description: '사용자가 허용한 작업을 연결된 상대 세션에 전달한다. 기존 맥락을 유지하며 새 모델 프로세스를 만들지 않는다. submitted는 완료가 아니다. unknown이면 자동 재시도하지 말고 ax_result로 확인한다. Codex/agy 관리형 stdio, 별도로 실행한 Windows agy console, 실험용 PTY/agentapi 연결을 지원한다.',
396
470
  inputSchema: { type: 'object', properties: {
397
471
  to: { type: 'string', description: 'ax_sessions에서 확인한 상대 세션 UUID' },
398
472
  prompt: { type: 'string', description: '상대가 실행할 작업 요청' },
473
+ wait_ms: { type: 'integer', minimum: 0, maximum: 30000, description: '전달 후 회신을 로컬에서 기다릴 최대 시간(ms). 짧은 요청은 30000으로 한 번에 회수한다. 기본 0.' },
399
474
  }, required: ['to', 'prompt'] },
400
475
  },
401
476
  {
402
477
  name: 'ax_result',
403
- description: '작업 요청의 제출/완료/불명 상태, 결과와 측정 사용량을 확인한다. 관리형 세션은 CLI의 구조화된 실행 완료 이벤트, 실험용 터미널은 해당 요청의 종료 훅으로 판정한다.',
478
+ description: '작업 요청의 제출/완료/불명 상태, 결과와 측정 사용량을 확인한다. 관리형 세션은 구조화된 완료 이벤트, PTY는 종료 훅, 독립 Windows console은 요청 UUID가 있는 대화 기록과 현재 빈 입력창의 대기 상태로 판정한다.',
404
479
  inputSchema: { type: 'object', properties: {
405
480
  id: { type: 'string', description: 'ax_dispatch가 반환한 요청 UUID' },
481
+ wait_ms: { type: 'integer', minimum: 0, maximum: 30000, description: '로컬 회신 대기(ms). 모델 호출 없이 완료 즉시 반환. 기본 0.' },
406
482
  }, required: ['id'] },
407
483
  },
408
484
  {
@@ -848,8 +924,8 @@ function dispatch(name, args = {}) {
848
924
 
849
925
  switch (name) {
850
926
  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])
927
+ case 'ax_dispatch': return bridge(['send', '--to', args.to, '--wait-ms', String(args.wait_ms ?? 0)], args.prompt)
928
+ case 'ax_result': return bridge(['result', args.id, '--wait-ms', String(args.wait_ms ?? 0)])
853
929
  case 'ax_chat': return bridge(['chat', '--background', '--left', args.left, '--right', args.right,
854
930
  ...['maxTurns', 'maxTokens', 'maxMessageChars'].flatMap(key => args[key] === undefined ? [] :
855
931
  [`--${key.replace(/[A-Z]/g, c => `-${c.toLowerCase()}`)}`, String(args[key])])], args.prompt)
@@ -1041,13 +1117,28 @@ async function handle(req) {
1041
1117
 
1042
1118
  if (method === 'initialize') {
1043
1119
  rootsSupported = !!params?.capabilities?.roots
1120
+ clientHasApps = !!params?.capabilities?.extensions?.['io.modelcontextprotocol/ui']?.mimeTypes?.includes(PIPELINE_MIME)
1044
1121
  return {
1045
1122
  protocolVersion: '2024-11-05',
1046
- capabilities: { tools: {} },
1123
+ capabilities: { tools: {}, resources: {} },
1047
1124
  serverInfo: { name: 'axmap', version: VERSION },
1125
+ instructions: 'axMap works across ChatGPT, Claude and agy. Open ax_pipeline once for an inline panel when the host supports MCP Apps. Otherwise ax_pipeline_watch and ax_dispatch/ax_result with wait_ms report standard MCP progress when the client supplies progressToken. These observations never call a model. Do not resubmit unknown requests.',
1048
1126
  }
1049
1127
  }
1050
- if (method === 'tools/list') return { tools: TOOLS }
1128
+ if (method === 'tools/list') return { tools: TOOLS.filter(tool => clientHasApps || tool.name !== 'ax_pipeline_state').map(tool => {
1129
+ if (clientHasApps || !tool._meta?.ui?.resourceUri) return tool
1130
+ const { ui, 'openai/outputTemplate': outputTemplate, ...metadata } = tool._meta
1131
+ return { ...tool, _meta: metadata }
1132
+ }) }
1133
+ if (method === 'resources/list') return { resources: [{ uri: PIPELINE_URI, name: 'axmap-pipeline', title: 'axMap 실행 흐름', mimeType: PIPELINE_MIME }] }
1134
+ if (method === 'resources/read') {
1135
+ if (params?.uri !== PIPELINE_URI) throw Object.assign(new Error('알 수 없는 UI 리소스입니다.'), { code: -32602 })
1136
+ return { contents: [{ uri: PIPELINE_URI, mimeType: PIPELINE_MIME,
1137
+ text: fs.readFileSync(path.join(SELF, 'mcp', 'pipeline.html'), 'utf8'),
1138
+ _meta: { ui: { prefersBorder: true, csp: { connectDomains: [], resourceDomains: ['https://cdn.jsdelivr.net'] } },
1139
+ 'openai/widgetCSP': { connect_domains: [], resource_domains: ['https://cdn.jsdelivr.net'] } },
1140
+ }] }
1141
+ }
1051
1142
  if (method === 'tools/call') {
1052
1143
  if (rootsSupported && !process.env.AXMAP_REPO) {
1053
1144
  if (!rootsTask || rootsError) refreshRoots()
@@ -1055,7 +1146,19 @@ async function handle(req) {
1055
1146
  let task
1056
1147
  do { task = rootsTask; await task } while (task !== rootsTask)
1057
1148
  }
1058
- const r = callTool(params?.name, params?.arguments ?? {})
1149
+ const args = params?.arguments ?? {}
1150
+ if (PIPELINE_TOOLS.has(params?.name)) {
1151
+ const cwd = resolveRepo(args)
1152
+ return pipeline(params.name === 'ax_pipeline_watch' ? 'watch' : 'snapshot', args, req, cwd)
1153
+ }
1154
+ if (['ax_dispatch', 'ax_result'].includes(params?.name)) {
1155
+ let switched
1156
+ try { switched = syncRepo(args) } catch (e) { return { content: [{ type: 'text', text: e.message }], isError: true } }
1157
+ const answer = await pipeline(params.name === 'ax_dispatch' ? 'dispatch' : 'result', args, req, REPO)
1158
+ if (switched) answer.content[0].text = switchNotice(switched) + '\n\n' + answer.content[0].text
1159
+ return answer
1160
+ }
1161
+ const r = callTool(params?.name, args)
1059
1162
  const localBridge = BRIDGE_TOOLS.has(params?.name)
1060
1163
  const warning = r.ok && !localBridge ? syncConfiguredProfile(REPO, BUS, { ...process.env, AXMAP_AGENT: AGENT }) : ''
1061
1164
  return { content: [{ type: 'text', text: r.text + (warning ? '\n' + warning : '') + (r.ok && !localBridge ? unreadBanner(params?.name, r.text) : '') }], isError: !r.ok }
@@ -1067,6 +1170,16 @@ async function handle(req) {
1067
1170
  const rl = readline.createInterface({ input: process.stdin })
1068
1171
  // Execute tools in order, but keep reading client replies while a tool awaits roots.
1069
1172
  let requests = Promise.resolve()
1173
+ let initialized = Promise.resolve(), concurrentViews = 0
1174
+ async function respond(req) {
1175
+ try {
1176
+ const result = await handle(req)
1177
+ if (result === null) send({ jsonrpc: '2.0', id: req.id, error: { code: -32601, message: `지원하지 않는 메서드: ${req.method}` } })
1178
+ else send({ jsonrpc: '2.0', id: req.id, result })
1179
+ } catch (e) {
1180
+ send({ jsonrpc: '2.0', id: req.id, error: { code: typeof e.code === 'number' ? e.code : -32603, message: e.message } })
1181
+ }
1182
+ }
1070
1183
  rl.on('line', (line) => {
1071
1184
  const s = line.trim()
1072
1185
  if (!s) return
@@ -1083,22 +1196,19 @@ rl.on('line', (line) => {
1083
1196
  return
1084
1197
  }
1085
1198
  if (req.id === undefined) {
1199
+ if (req.method === 'notifications/cancelled') { runningPipeline.get(req.params?.requestId)?.(); return }
1086
1200
  if (rootsSupported && !process.env.AXMAP_REPO &&
1087
1201
  ['notifications/initialized', 'notifications/roots/list_changed'].includes(req.method)) refreshRoots()
1088
1202
  return
1089
1203
  }
1090
- requests = requests.then(async () => {
1091
- try {
1092
- const result = await handle(req)
1093
- if (result === null) {
1094
- send({ jsonrpc: '2.0', id: req.id, error: { code: -32601, message: `지원하지 않는 메서드: ${req.method}` } })
1095
- } else {
1096
- send({ jsonrpc: '2.0', id: req.id, result })
1097
- }
1098
- } catch (e) {
1099
- send({ jsonrpc: '2.0', id: req.id, error: { code: -32603, message: e.message } })
1100
- }
1101
- })
1204
+ if ((req.method === 'tools/call' && PIPELINE_TOOLS.has(req.params?.name)) || ['resources/list', 'resources/read'].includes(req.method)) {
1205
+ if (concurrentViews >= 4) { send({ jsonrpc: '2.0', id: req.id, error: { code: -32000, message: '동시 파이프라인 조회 한도입니다. 잠시 후 다시 조회하세요.' } }); return }
1206
+ concurrentViews++
1207
+ void initialized.then(() => respond(req)).finally(() => { concurrentViews-- })
1208
+ return
1209
+ }
1210
+ requests = requests.then(() => respond(req))
1211
+ if (req.method === 'initialize') initialized = requests
1102
1212
  })
1103
1213
 
1104
1214
  process.stderr.write(`axMap MCP · 저장소 ${REPO} · 에이전트 ${AGENT} (${ACTOR})\n`)
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": "1.4.0",
4
+ "version": "1.6.0",
5
5
  "//private": "🔴 여기 있던 \"private\": true 를 2026-08-28 에 지웠다. 그 줄이 있는 동안 npm publish 는 거부됐다. 되돌리려면 다시 넣으면 되지만, 이미 올라간 버전은 그래도 안 사라진다 — npm 은 같은 번호를 덮어쓰지 못하고 삭제도 72시간 안에만 된다. 즉 이 줄을 되살리는 것은 '앞으로 안 올린다' 는 뜻이지 '올린 것을 없앤다' 는 뜻이 아니다.",
6
6
  "//publishConfig": "axmap-cli 는 스코프가 없어서 기본이 이미 공개다. 즉 지금은 없어도 된다. 그래도 남겨 두는 이유: 나중에 @조직/axmap 처럼 스코프를 붙이는 날 이 줄이 없으면 publish 가 '유료 플랜이 필요하다'는 엉뚱한 말로 실패한다.",
7
7
  "publishConfig": {
@@ -37,11 +37,15 @@
37
37
  "CLAUDE.md"
38
38
  ],
39
39
  "scripts": {
40
- "test": "node tools/run-tests.mjs",
40
+ "test": "npm run build:check && node tools/run-tests.mjs",
41
41
  "setup": "node tools/setup.mjs",
42
42
  "demo": "bash demo/run-demo.sh",
43
43
  "demo:compare": "bash demo/conflict-vs-parser.sh",
44
- "demo:chaos": "bash demo/chaos.sh"
44
+ "demo:chaos": "bash demo/chaos.sh",
45
+ "build": "node tools/build.mjs",
46
+ "build:check": "node tools/build.mjs --check",
47
+ "typecheck": "tsc --noEmit",
48
+ "prepack": "npm run build"
45
49
  },
46
50
  "engines": {
47
51
  "node": ">=20"
@@ -52,5 +56,9 @@
52
56
  },
53
57
  "dependencies": {
54
58
  "node-pty": "npm:@lydell/node-pty@1.1.0"
59
+ },
60
+ "devDependencies": {
61
+ "@types/node": "20.19.43",
62
+ "typescript": "7.0.2"
55
63
  }
56
64
  }
package/src/agentcli.mjs CHANGED
@@ -1,19 +1,22 @@
1
+ // Generated from src/agentcli.mts. Edit the .mts source and run npm run build.
1
2
  /** AI CLI executable discovery shared by setup and MCP registration. */
2
- import { spawnSync } from 'node:child_process'
3
-
3
+ import { spawnSync } from 'node:child_process';
4
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
-
5
+ { id: 'claude', name: 'Claude Code', bin: 'claude' },
6
+ { id: 'agy', name: 'Antigravity', bin: 'agy' },
7
+ { id: 'codex', name: 'Codex CLI', bin: 'codex' },
8
+ ];
10
9
  /** Resolve the executable without a shell so paths and arguments stay intact. */
11
10
  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 }
11
+ try {
12
+ const r = spawnSync(process.platform === 'win32' ? 'where.exe' : 'which', [bin], {
13
+ encoding: 'utf8', windowsHide: true,
14
+ });
15
+ if (r.status !== 0)
16
+ return null;
17
+ return r.stdout.split(/\r?\n/).map(s => s.trim()).find(Boolean) || null;
18
+ }
19
+ catch {
20
+ return null;
21
+ }
19
22
  }
@@ -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: string): string | null {
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
+ }