axmap-cli 1.5.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/pipeline.md +9 -0
- package/README.ko.md +37 -0
- package/README.md +37 -0
- package/bin/axmap.mjs +6 -0
- package/mcp/README.md +9 -0
- package/mcp/pipeline.html +184 -0
- package/mcp/server.mjs +161 -20
- package/package.json +12 -3
- package/skills/pipeline/SKILL.md +14 -0
- package/src/agentcli.mjs +17 -14
- package/src/agentcli.mts +19 -0
- package/src/bridge.mjs +297 -219
- package/src/bridge.mts +282 -0
- package/src/codex-connection.mjs +151 -0
- package/src/codex-connection.mts +83 -0
- package/src/external-console.mjs +140 -97
- package/src/external-console.mts +128 -0
- package/src/managed-session.mjs +325 -169
- package/src/managed-session.mts +242 -0
- package/src/monitor-cli.mjs +89 -0
- package/src/monitor-cli.mts +66 -0
- package/src/monitor.mjs +245 -0
- package/src/monitor.mts +199 -0
- package/src/node-pty.d.ts +13 -0
- package/src/pipeline-graph.mjs +55 -0
- package/src/pipeline-graph.mts +51 -0
- package/src/pipeline-mcp.mjs +111 -0
- package/src/pipeline-mcp.mts +105 -0
- package/src/pipeline-statusline.mjs +135 -0
- package/src/pipeline-statusline.mts +106 -0
- package/src/pipeline-web.mjs +56 -0
- package/src/pipeline-web.mts +31 -0
- package/src/session-dialogue.mjs +168 -116
- package/src/session-dialogue.mts +136 -0
- package/src/session-mailbox.mjs +190 -0
- package/src/session-mailbox.mts +157 -0
- package/src/session-status.mjs +47 -41
- package/src/session-status.mts +50 -0
- package/src/session-types.mjs +7 -0
- package/src/session-types.mts +119 -0
- package/src/session-usage.mjs +33 -28
- package/src/session-usage.mts +39 -0
- package/src/terminal-bridge.mjs +293 -209
- package/src/terminal-bridge.mts +235 -0
- package/src/windows-console.mjs +42 -0
- package/src/windows-console.mts +29 -0
- package/tools/bench-console.mjs +70 -0
- package/tools/bridge.mjs +41 -6
- package/tools/build.mjs +39 -0
- package/tools/monitor.mjs +5 -0
- package/tools/pipeline.mjs +34 -0
- package/tools/run-tests.mjs +3 -2
- package/tools/windows-console.cs +116 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: axMap 로컬 세션 그래프와 실제 요청·응답·토큰 상태를 표시한다
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
현재 작업 저장소에 대해 `ax_pipeline`을 호출해 MCP Apps 패널을 연다. 연결된 세션, 역할, 요청/응답 연결, 실제 사용량과 이벤트를 표시한다. 토큰/진행률을 추정하거나 모델을 호출하지 않는다.
|
|
6
|
+
|
|
7
|
+
도구 또는 UI 확장이 없으면 `axmap pipeline --cwd <현재 Git 저장소 절대경로>`로 터미널 그래프를 연다. 대화 출력만 가능한 환경은 `--once` 스냅샷임을 명시한다. 다른 저장소로 임의 전환하거나 세션을 새로 만들지 않는다.
|
|
8
|
+
|
|
9
|
+
명령 호출 자체는 세션 실행·재전송·승인·설정 변경을 요청하는 것이 아니다.
|
package/README.ko.md
CHANGED
|
@@ -19,12 +19,49 @@ 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.7.0
|
|
33
|
+
|
|
34
|
+
<sub>2026.10.27</sub>
|
|
35
|
+
|
|
36
|
+
**일반 협업 알림을 작업 경계에서 모아 처리하고 기존 로컬 Codex 대화를 연결합니다.**
|
|
37
|
+
|
|
38
|
+
- 일반 알림은 `ax_message` / `axmap bridge message`로 새 모델 턴 없이 저장합니다. 수신자는 작업·테스트·실험의 안전한 경계에서 `ax_mailbox`로 묶음 회수합니다.
|
|
39
|
+
- 중단 요청·데이터 손상 위험·현재 작업 무효화를 명시적 긴급 사유로 구분합니다. 수신자가 작업 중이면 전달을 보류하며 실행 중 작업을 강제 중단하지 않습니다.
|
|
40
|
+
- 발신자·시각·이력을 보존하고 만료, 멱등 키, 같은 발신자·주제의 대기 업데이트 최신화를 지원합니다. 전달불명 메시지는 자동 재전송하지 않습니다.
|
|
41
|
+
- 로컬 제어 소켓으로 기존 Codex 대화를 연결합니다. 작업 중에는 관측만 허용하고 새 요청 전 대기 상태를 확인합니다.
|
|
42
|
+
- `axmap pipeline` 그래프와 읽기 전용 loopback 웹 화면, 새 모델 턴을 시작하지 않는 Codex 문맥 안내를 추가합니다.
|
|
43
|
+
|
|
44
|
+
받은 편지함은 격리 provider와 CLI/MCP 계약으로 검증했습니다. 실제 턴 절감량은 미측정이며 실행 중 강제 중단은 지원하지 않습니다. 수신 에이전트가 작업 경계에서 받은 편지함을 회수해야 합니다. 새 도구를 발견하려면 MCP를 다시 연결하세요. 실행용 기존 `ax_dispatch`는 유지합니다.
|
|
45
|
+
[받은 편지함 안내 →](docs/SESSION-MAILBOX.md) · [로컬 파이프라인 →](docs/LOCAL-PIPELINE.md)
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
### 1.6.0
|
|
50
|
+
|
|
51
|
+
<sub>2026.10.26</sub>
|
|
52
|
+
|
|
53
|
+
**요청 흐름을 표시하고 기존 Windows agy 연결의 관측 비용을 줄입니다.**
|
|
54
|
+
|
|
55
|
+
- Windows 콘솔 도우미를 매번 컴파일하지 않고 캐시하며 PowerShell 대체 경로를 유지합니다.
|
|
56
|
+
- 제한 시간 안에서 응답을 기다리는 `wait_ms`·`--wait-ms`와 비동기 MCP 진행 알림을 추가합니다.
|
|
57
|
+
- MCP Apps 파이프라인 리소스, 텍스트 대체 출력, 기존 명령을 보존하는 Claude Code·agy 상태줄을 제공합니다. 별도 터미널 표시가 필요하면 `axmap monitor`를 사용합니다.
|
|
58
|
+
- 세션·파이프라인 모듈을 strict TypeScript로 작성하며 Node.js 20+용 JavaScript를 함께 배포합니다. 실행에 TypeScript 설치는 필요하지 않습니다.
|
|
59
|
+
|
|
60
|
+
Windows의 실제 agy에서 전달·진행 알림·상태줄의 완료 표시를 확인했습니다. ChatGPT·Claude UI는 기능 협상과 프로토콜 테스트로 검증했으며 실제 제품의 임베디드 화면 검증은 남아 있습니다. 이 Codex 세션에는 새 파이프라인 도구가 노출되지 않았습니다. 로컬 stdio 지원만으로 ChatGPT 웹용 원격 엔드포인트가 생기지는 않습니다.
|
|
61
|
+
[클라이언트 설정과 제한 →](docs/PIPELINE-CLIENTS.md)
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
28
65
|
### 1.5.0
|
|
29
66
|
|
|
30
67
|
<sub>2026.10.21</sub>
|
package/README.md
CHANGED
|
@@ -19,12 +19,49 @@ 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.7.0
|
|
33
|
+
|
|
34
|
+
<sub>October 27, 2026</sub>
|
|
35
|
+
|
|
36
|
+
**Batch routine collaboration messages at safe work boundaries and connect existing local Codex threads.**
|
|
37
|
+
|
|
38
|
+
- Use `ax_message` / `axmap bridge message` for routine updates without starting a model turn. Receivers collect messages together with `ax_mailbox` at task, test or experiment checkpoints.
|
|
39
|
+
- Explicit urgent reasons distinguish stop requests, data-loss risks and changes that invalidate current work. Busy recipients defer delivery; this release does not force-interrupt active work.
|
|
40
|
+
- Preserve message provenance and history while supporting expiration, idempotency keys and latest pending updates per sender/topic. Uncertain delivery is never automatically retried.
|
|
41
|
+
- Connect an existing Codex thread through its local control socket, with read-only observation of active work and idle checks before submission.
|
|
42
|
+
- Add the `axmap pipeline` graph, a read-only loopback web view and context-only Codex notices that do not start a new model turn.
|
|
43
|
+
|
|
44
|
+
Mailbox behavior is covered by isolated provider and CLI/MCP tests. Actual turn savings and active-work interruption are not claimed. Receivers must collect their mailbox at safe checkpoints; reconnect MCP clients to discover the new tools. Existing execution-oriented `ax_dispatch` remains available.
|
|
45
|
+
[Mailbox guide →](docs/SESSION-MAILBOX.md) · [Local pipeline →](docs/LOCAL-PIPELINE.md)
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
### 1.6.0
|
|
50
|
+
|
|
51
|
+
<sub>October 26, 2026</sub>
|
|
52
|
+
|
|
53
|
+
**Observe request flow and reduce the overhead of existing Windows agy connections.**
|
|
54
|
+
|
|
55
|
+
- Cache the Windows console helper instead of recompiling it for every observation, with PowerShell fallback.
|
|
56
|
+
- Add bounded `wait_ms` / `--wait-ms` response waiting and asynchronous MCP progress notifications.
|
|
57
|
+
- 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.
|
|
58
|
+
- Author session and pipeline modules in strict TypeScript while shipping generated JavaScript for Node.js 20+; TypeScript is not required at runtime.
|
|
59
|
+
|
|
60
|
+
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.
|
|
61
|
+
[Client setup and limits →](docs/PIPELINE-CLIENTS.md)
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
28
65
|
### 1.5.0
|
|
29
66
|
|
|
30
67
|
<sub>October 21, 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?)` | 끝나면 즉시. 생략하면 전부 반납 |
|
|
@@ -41,6 +47,9 @@ https://github.com/rl2aderjoon/axmap/blob/main/mcp/SETUP-FOR-AI.md
|
|
|
41
47
|
| `ax_inbox(id?)` | 다른 에이전트가 나에게 보낸 쪽지를 읽는다 |
|
|
42
48
|
| `ax_sessions()` | 같은 repo의 연결된 세션·역할·상태·사용량 |
|
|
43
49
|
| `ax_dispatch(to, prompt)` / `ax_result(id)` | 연결된 세션에 단일 작업 전달·결과 조회 |
|
|
50
|
+
| `ax_message(to, text)` | 일반 협업 알림 무호출 큐 저장. 즉시 전달은 명시적 긴급 사유 필요 |
|
|
51
|
+
| `ax_mailbox(to, take, at)` | 안전한 작업 경계에서 메시지 묶음 회수. 모델 호출 없음 |
|
|
52
|
+
| `ax_mailbox_flush(to, at)` | 필요할 때 idle 상대에게 대기 메시지를 한 요청으로 전달 |
|
|
44
53
|
| `ax_chat(left, right, prompt, maxTurns?, maxTokens?)` | 관리형 세션 두 개의 제한된 자동 대화를 백그라운드 접수 |
|
|
45
54
|
| `ax_dialogue(id)` / `ax_chat_stop(id)` | 대화·토큰 조회 / 추가 중계 중단 |
|
|
46
55
|
| `ax_send(to?, subject, body)` | 다른 에이전트에게 쪽지를 보낸다 |
|
|
@@ -0,0 +1,184 @@
|
|
|
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
|
+
.topology { position:relative; padding-top:6px; }
|
|
23
|
+
.router { width:max-content; max-width:100%; margin:0 auto 28px; padding:14px 24px; border:1px solid var(--accent); border-radius:8px; text-align:center; background:var(--soft); }
|
|
24
|
+
.router strong { display:block; font-size:16px; }
|
|
25
|
+
.router::after { content:''; display:block; position:absolute; height:28px; width:1px; background:var(--line); left:50%; margin-top:14px; }
|
|
26
|
+
.sessions { display:grid; grid-template-columns:repeat(auto-fit,minmax(210px,1fr)); gap:20px; }
|
|
27
|
+
.session { position:relative; display:flex; flex-direction:column; gap:7px; align-items:stretch; border:1px solid var(--line); padding:14px; border-radius:8px; min-width:0; overflow-wrap:anywhere; }
|
|
28
|
+
.session::before { content:''; position:absolute; height:20px; width:1px; background:var(--line); left:50%; top:-21px; }
|
|
29
|
+
.session.running { border-color:var(--accent); background:var(--soft); }
|
|
30
|
+
.session.running::after { content:''; position:absolute; width:6px; height:6px; border-radius:50%; background:var(--accent); left:calc(50% - 3px); top:-20px; animation:packet 1.4s linear infinite; }
|
|
31
|
+
body:not([data-live="true"]) .session.running::after { animation-play-state:paused; }
|
|
32
|
+
.session .identity { font-size:12px; color:var(--muted); }
|
|
33
|
+
.session .usage { font-variant-numeric:tabular-nums; border-top:1px solid var(--line); padding-top:7px; }
|
|
34
|
+
.session .activity { font-size:12px; color:var(--muted); }
|
|
35
|
+
.session .state { font-weight:500; }
|
|
36
|
+
.claims { padding:12px 0; color:var(--muted); overflow-wrap:anywhere; }
|
|
37
|
+
@keyframes packet { from { transform:translateY(0); } to { transform:translateY(20px); } }
|
|
38
|
+
.session strong { font-weight:600; }
|
|
39
|
+
.subtle { color:var(--muted); }
|
|
40
|
+
.job { padding:14px 0; border-bottom:1px solid var(--line); }
|
|
41
|
+
.job:last-child { border-bottom:0; }
|
|
42
|
+
.route { font-weight:500; overflow-wrap:anywhere; }
|
|
43
|
+
.task { margin-top:9px; color:var(--muted); overflow-wrap:anywhere; }
|
|
44
|
+
.steps { display:grid; grid-template-columns:repeat(4,minmax(0,1fr)); gap:8px; margin-top:16px; }
|
|
45
|
+
.step { position:relative; display:flex; flex-direction:column; align-items:center; text-align:center; gap:6px; font-size:12px; }
|
|
46
|
+
.step:not(:last-child)::after { content:''; position:absolute; top:14px; left:calc(50% + 18px); width:calc(100% - 28px); height:1px; background:var(--line); }
|
|
47
|
+
.mark { display:grid; place-items:center; position:relative; width:28px; height:28px; border-radius:50%; background:var(--line); color:var(--ink); }
|
|
48
|
+
.step.done .mark,.step.current .mark { background:var(--soft); }
|
|
49
|
+
.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; }
|
|
50
|
+
body:not([data-live="true"]) .step.current .mark::after { animation-play-state:paused; }
|
|
51
|
+
.issue { color:var(--warning); margin-top:10px; overflow-wrap:anywhere; }
|
|
52
|
+
.events { list-style:none; padding:0; margin:0; font-size:12px; }
|
|
53
|
+
.events li { padding:3px 0; overflow-wrap:anywhere; }
|
|
54
|
+
time { color:var(--muted); margin-right:10px; font-variant-numeric:tabular-nums; }
|
|
55
|
+
footer { border-top:1px solid var(--line); padding-top:10px; margin-top:14px; color:var(--muted); font-size:12px; }
|
|
56
|
+
@keyframes spin { to { transform:rotate(360deg); } }
|
|
57
|
+
@media (prefers-reduced-motion:reduce) { .step.current .mark::after,.session.running::after { animation:none; } }
|
|
58
|
+
@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;} }
|
|
59
|
+
</style>
|
|
60
|
+
</head>
|
|
61
|
+
<body data-live="false">
|
|
62
|
+
<main id="axmap-pipeline">
|
|
63
|
+
<header><h1>axMap 실행 흐름</h1><span class="status" id="status">연결 확인 중</span><button type="button" id="pause">새로고침 일시정지</button></header>
|
|
64
|
+
<div class="scope" id="scope">실제 작업 상태를 기다리는 중</div>
|
|
65
|
+
<section class="topology" aria-label="등록된 세션과 로컬 메시지 연결">
|
|
66
|
+
<div class="router"><strong>axMap · 로컬 라우터</strong><span class="subtle">기존 대화 유지 · 요청 ID 추적 · Git 왕복 없음</span></div>
|
|
67
|
+
<div class="sessions" id="sessions"></div>
|
|
68
|
+
</section>
|
|
69
|
+
<section aria-label="요청 파이프라인" id="jobs"></section>
|
|
70
|
+
<div class="issue" id="issue" role="status" aria-live="polite"></div>
|
|
71
|
+
<section id="event-section"><h2>최근 이벤트</h2><ul class="events" id="events"></ul></section>
|
|
72
|
+
<footer id="checked">대화 안에서 상태만 조회합니다. 모델 호출·작업 재전송·자동 승인 없음.</footer>
|
|
73
|
+
</main>
|
|
74
|
+
<script>
|
|
75
|
+
(() => {
|
|
76
|
+
const root=document.getElementById('axmap-pipeline');
|
|
77
|
+
const status=document.getElementById('status'),scope=document.getElementById('scope');
|
|
78
|
+
const sessions=document.getElementById('sessions'),jobs=document.getElementById('jobs');
|
|
79
|
+
const issue=document.getElementById('issue'),events=document.getElementById('events');
|
|
80
|
+
const checked=document.getElementById('checked'),pause=document.getElementById('pause');
|
|
81
|
+
const requests=new Map();let sequence=0,ready=false,mode='standard',cwd=null,requestedCwd=null,paused=false,stopped=false,inflight=false,timer,unavailable=false;
|
|
82
|
+
const pending=new Set(['sending','submitted','unknown']);
|
|
83
|
+
const states={sending:'전달 중',submitted:'응답 대기',completed:'완료 확인',unknown:'전송 확인 필요',failed:'실패',superseded:'다른 입력으로 대체됨'};
|
|
84
|
+
const sessionStates={starting:'시작 중',running:'작업 중',idle:'대기',awaiting_user:'승인 필요',settling:'대기 전환 중',closed:'종료',unknown:'상태 미확인'};
|
|
85
|
+
const connectionStates={connected:'연결됨',closed:'종료됨',unreachable:'연결 확인 불가',unverified:'확인 미지원'};
|
|
86
|
+
const text=value=>String(value??'').replace(/[\x00-\x1f\x7f-\x9f\u202a-\u202e\u2066-\u2069]/g,' ').slice(0,600);
|
|
87
|
+
const node=(tag,content,className)=>{const el=document.createElement(tag);if(content!==undefined)el.textContent=text(content);if(className)el.className=className;return el};
|
|
88
|
+
const notify=(method,params)=>window.parent.postMessage({jsonrpc:'2.0',method,params},'*');
|
|
89
|
+
function request(method,params){
|
|
90
|
+
const id=`axmap-ui-${++sequence}`;
|
|
91
|
+
return new Promise((resolve,reject)=>{
|
|
92
|
+
const timeout=setTimeout(()=>{requests.delete(id);reject(new Error('호스트 응답 시간 초과'))},10000);
|
|
93
|
+
requests.set(id,{resolve,reject,timeout});window.parent.postMessage({jsonrpc:'2.0',id,method,params},'*');
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
function context(value){
|
|
97
|
+
if(value?.theme==='light'||value?.theme==='dark')document.documentElement.dataset.theme=value.theme;
|
|
98
|
+
const allowed=new Set(['--color-background-primary','--color-text-primary','--color-text-secondary','--color-border-primary']);
|
|
99
|
+
for(const [key,color]of Object.entries(value?.styles?.variables||{}))if(allowed.has(key)&&typeof color==='string')document.documentElement.style.setProperty(key,color);
|
|
100
|
+
}
|
|
101
|
+
function setLive(value,message){document.body.dataset.live=String(value);status.textContent=message;}
|
|
102
|
+
function render(data){
|
|
103
|
+
if(!data||data.schemaVersion!==1||data.type!=='pipeline'||typeof data.cwd!=='string'||!Array.isArray(data.requests)||!Array.isArray(data.sessions))return;
|
|
104
|
+
if(cwd&&data.cwd!==cwd){issue.textContent='다른 저장소의 상태를 받았습니다. 현재 패널의 저장소를 유지합니다.';setLive(false,'저장소 확인 필요');return;}
|
|
105
|
+
cwd=data.cwd;scope.textContent=text(cwd);sessions.replaceChildren();jobs.replaceChildren();events.replaceChildren();
|
|
106
|
+
const ordered=[...data.sessions].sort((a,b)=>Number(b.connection==='connected')-Number(a.connection==='connected'));
|
|
107
|
+
for(const session of ordered.slice(0,6)){
|
|
108
|
+
const waiting=data.requests.filter(job=>job.to===session.id&&pending.has(job.state));
|
|
109
|
+
const line=node('article',undefined,`session${waiting.length&&session.connection==='connected'?' running':''}`);
|
|
110
|
+
line.append(node('strong',session.role||`${session.provider} 세션`),node('span',`${session.provider} · ${text(session.id).slice(0,8)} · ${session.transport||'연결 방식 미확인'}`,'identity'),node('span',`${session.pendingApproval?'사용자 승인 필요':sessionStates[session.state]||'상태 미확인'} · ${connectionStates[session.connection]||'확인 미지원'}`,'state'),node('span',session.model||'모델 미확인','identity'));
|
|
111
|
+
line.append(node('div',session.usage?.totalTokens==null?'토큰 미측정':`누적 처리 ${session.usage.totalTokens.toLocaleString()} tokens`,'usage'),node('div',waiting.length?`${waiting.length}건 응답 대기`:session.state==='idle'?'새 요청을 받을 수 있음':'대기 중인 axMap 요청 없음','activity'));
|
|
112
|
+
sessions.append(line);
|
|
113
|
+
}
|
|
114
|
+
const active=data.requests.filter(r=>pending.has(r.state));
|
|
115
|
+
const selected=[...active,...data.requests.filter(r=>!pending.has(r.state))].slice(0,4);
|
|
116
|
+
if(!selected.length)jobs.append(node('p','새 요청을 기다리는 중','subtle'));
|
|
117
|
+
for(const job of selected){
|
|
118
|
+
const item=node('article',undefined,'job');item.append(node('div',`${text(job.from)} → ${text(job.to).slice(0,8)} · ${states[job.state]||'확인 필요'}`,'route'));
|
|
119
|
+
const steps=node('div',undefined,'steps');
|
|
120
|
+
const done=job.state==='completed',sent=job.state==='submitted'||done;
|
|
121
|
+
['접수','전달','응답','완료 확인'].forEach((label,index)=>{
|
|
122
|
+
const complete=done||index===0||(sent&&index===1);
|
|
123
|
+
const current=!done&&((job.state==='sending'&&index===1)||(job.state==='submitted'&&index===2));
|
|
124
|
+
const step=node('div',undefined,`step${complete?' done':current?' current':''}`);
|
|
125
|
+
const mark=node('span',complete?'✓':job.state==='unknown'&&index===1?'?':String(index+1),'mark');mark.setAttribute('aria-hidden','true');
|
|
126
|
+
step.append(mark,node('span',current&&index===2?'응답 대기':label));steps.append(step);
|
|
127
|
+
});
|
|
128
|
+
item.append(steps,node('div',job.task,'task'));if(job.error)item.append(node('div',job.error,'issue'));jobs.append(item);
|
|
129
|
+
const measured=[];
|
|
130
|
+
if(Number.isFinite(job.latencyMs))measured.push(`왕복 ${(job.latencyMs/1000).toFixed(2)}초`);
|
|
131
|
+
measured.push(job.usage?.totalTokens==null?'요청 토큰 미측정':`요청 처리 ${job.usage.totalTokens.toLocaleString()} tokens`);
|
|
132
|
+
item.append(node('div',measured.join(' · '),'task'));
|
|
133
|
+
}
|
|
134
|
+
for(const event of (data.events||[]).slice(0,5)){
|
|
135
|
+
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);
|
|
136
|
+
}
|
|
137
|
+
issue.textContent=(data.issues||[]).slice(0,2).map(text).join(' · ');
|
|
138
|
+
checked.textContent=`관측 ${text(data.checkedAt)} · 상태 조회만 수행 · 작업 제어 없음`;
|
|
139
|
+
setLive(ready&&!paused&&!document.hidden,!ready?(unavailable?'현재 상태 · 자동 갱신 미지원':'연결 확인 중'):paused?'새로고침 일시정지':document.hidden?'화면 숨김':`진행 확인 ${active.length}개`);
|
|
140
|
+
}
|
|
141
|
+
function consume(result){if(result?.isError)throw new Error(result.content?.[0]?.text||'상태 조회 실패');render(result?.structuredContent);}
|
|
142
|
+
function schedule(){clearTimeout(timer);if(ready&&!paused&&!stopped&&!document.hidden)timer=setTimeout(poll,1200);}
|
|
143
|
+
async function poll(){
|
|
144
|
+
if(inflight||!ready||paused||stopped||document.hidden)return;
|
|
145
|
+
inflight=true;
|
|
146
|
+
try{const scope=cwd||requestedCwd,args=scope?{cwd:scope}:{};const result=mode==='local'?await window.axmapLocalRead():mode==='openai'?await window.openai.callTool('ax_pipeline_state',args):await request('tools/call',{name:'ax_pipeline_state',arguments:args});consume(result);}
|
|
147
|
+
catch(error){setLive(false,'연결 확인 필요');issue.textContent=text(error?.message||error);}
|
|
148
|
+
finally{inflight=false;schedule();}
|
|
149
|
+
}
|
|
150
|
+
window.addEventListener('message',event=>{
|
|
151
|
+
if(event.source!==window.parent)return;const message=event.data;if(!message||message.jsonrpc!=='2.0')return;
|
|
152
|
+
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;}
|
|
153
|
+
if(message.method==='ui/notifications/tool-result'){try{consume(message.params)}catch(error){issue.textContent=text(error.message);}}
|
|
154
|
+
if(message.method==='ui/notifications/tool-input'&&!cwd&&typeof message.params?.arguments?.cwd==='string')requestedCwd=message.params.arguments.cwd;
|
|
155
|
+
if(message.method==='ui/notifications/host-context-changed')context(message.params);
|
|
156
|
+
if(message.method==='ui/resource-teardown'){
|
|
157
|
+
stopped=true;clearTimeout(timer);resize.disconnect();setLive(false,'관측 종료');
|
|
158
|
+
if(message.id!==undefined)window.parent.postMessage({jsonrpc:'2.0',id:message.id,result:{}},'*');
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
document.addEventListener('visibilitychange',()=>{if(document.hidden){clearTimeout(timer);setLive(false,'화면 숨김')}else if(!paused){void poll()}});
|
|
162
|
+
pause.addEventListener('click',()=>{paused=!paused;pause.textContent=paused?'실시간 새로고침 재개':'새로고침 일시정지';if(paused){clearTimeout(timer);setLive(false,'새로고침 일시정지')}else{void poll()}});
|
|
163
|
+
let reportedHeight=0;
|
|
164
|
+
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})}});
|
|
165
|
+
resize.observe(root);
|
|
166
|
+
window.addEventListener('pagehide',()=>{stopped=true;clearTimeout(timer);resize.disconnect()});
|
|
167
|
+
if(window.openai?.toolOutput)render(window.openai.toolOutput);
|
|
168
|
+
(async()=>{
|
|
169
|
+
if(document.documentElement.dataset.axmapLocal==='true'){mode='local';ready=true;void poll();return;}
|
|
170
|
+
try{
|
|
171
|
+
const result=await request('ui/initialize',{protocolVersion:'2026-01-26',appInfo:{name:'axmap-pipeline',version:'1.0.0'},appCapabilities:{availableDisplayModes:['inline']}});
|
|
172
|
+
context(result?.hostContext);notify('ui/notifications/initialized',{});
|
|
173
|
+
ready=!!result?.hostCapabilities?.serverTools;
|
|
174
|
+
if(!ready){unavailable=true;setLive(false,'자동 갱신 미지원');issue.textContent='현재 호스트가 상태 조회 도구 호출을 제공하지 않습니다. ax_pipeline_watch로 진행 상태를 확인할 수 있습니다.';return;}
|
|
175
|
+
}catch(error){
|
|
176
|
+
if(typeof window.openai?.callTool==='function'){mode='openai';ready=true;}
|
|
177
|
+
else{unavailable=true;setLive(false,'MCP Apps 연결 필요');issue.textContent='이 화면에서는 실시간 UI 연결을 확인하지 못했습니다. 현재 대화에서 ax_pipeline_watch를 사용할 수 있습니다.';return;}
|
|
178
|
+
}
|
|
179
|
+
void poll();
|
|
180
|
+
})();
|
|
181
|
+
})();
|
|
182
|
+
</script>
|
|
183
|
+
</body>
|
|
184
|
+
</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'
|
|
@@ -212,7 +212,58 @@ 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'])
|
|
215
|
+
const BRIDGE_TOOLS = new Set(['ax_sessions', 'ax_dispatch', 'ax_result', 'ax_chat', 'ax_dialogue', 'ax_chat_stop', 'ax_message', 'ax_mailbox', 'ax_mailbox_flush'])
|
|
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,17 +436,68 @@ 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로 시작한 관리형 세션이다. 등록만으로 연결 가능성이나 실행 성공을 보장하지 않는다.',
|
|
391
465
|
inputSchema: { type: 'object', properties: {} },
|
|
392
466
|
},
|
|
467
|
+
{
|
|
468
|
+
name: 'ax_message',
|
|
469
|
+
description: '에이전트 협업 메시지. 일반 진행·결과·계약 확인은 기본 boundary 큐에 저장하며 새 턴을 만들지 않는다. 긴급 중단·데이터 손상·현재 작업 무효화만 immediate와 reason을 명시한다. 긴급도 작업 중인 세션을 강제 중단하지 않으며 deferred/unknown을 전달 성공으로 취급하지 않는다. 단순 알림 ACK 금지.',
|
|
470
|
+
inputSchema: { type: 'object', properties: {
|
|
471
|
+
to: { type: 'string' }, text: { type: 'string' },
|
|
472
|
+
priority: { type: 'string', enum: ['boundary', 'immediate'], default: 'boundary' },
|
|
473
|
+
reason: { type: 'string', enum: ['stop', 'data-loss', 'work-invalidated'] },
|
|
474
|
+
topic: { type: 'string', description: '명시적으로 같은 주제인 대기 업데이트만 최신으로 대체. 발신자별 분리.' },
|
|
475
|
+
key: { type: 'string', description: '동일 메시지 재접수 방지. 같은 키에 다른 내용은 거절.' },
|
|
476
|
+
ttl_seconds: { type: 'integer', minimum: 1, maximum: 86400, default: 3600 },
|
|
477
|
+
}, required: ['to', 'text'] },
|
|
478
|
+
},
|
|
479
|
+
{
|
|
480
|
+
name: 'ax_mailbox',
|
|
481
|
+
description: '작업 시작·테스트 완료·실험 종료 등 안전한 경계에서 받은 협업 메시지를 한 번에 읽는다. 기본 조회는 읽음 처리하지 않는다. take=true와 at을 명시하면 묶음 회수하며 새 모델 턴은 없다. collected는 실행 완료나 사람 승인이 아니다. 단순 결과 알림에 회신하지 말고 필요한 결정만 모아 처리한다.',
|
|
482
|
+
inputSchema: { type: 'object', properties: {
|
|
483
|
+
to: { type: 'string' }, take: { type: 'boolean', default: false }, all: { type: 'boolean', default: false },
|
|
484
|
+
at: { type: 'string', enum: ['task-complete', 'test-complete', 'experiment-complete', 'checkpoint'] },
|
|
485
|
+
}, required: ['to'] },
|
|
486
|
+
},
|
|
487
|
+
{
|
|
488
|
+
name: 'ax_mailbox_flush',
|
|
489
|
+
description: '명시한 작업 경계에서 대기 메시지를 idle 상대에게 한 요청으로 전달한다. 수신자가 직접 ax_mailbox로 회수하는 방법을 우선한다. 빈 큐는 모델 호출 없음. 전달불명·프로세스 중단 후 자동 재전송하지 않는다.',
|
|
490
|
+
inputSchema: { type: 'object', properties: { to: { type: 'string' },
|
|
491
|
+
at: { type: 'string', enum: ['task-complete', 'test-complete', 'experiment-complete', 'checkpoint'] },
|
|
492
|
+
}, required: ['to', 'at'] },
|
|
493
|
+
},
|
|
393
494
|
{
|
|
394
495
|
name: 'ax_dispatch',
|
|
395
|
-
description: '
|
|
496
|
+
description: '명시적으로 상대의 실행 턴이 필요한 작업용 저수준 전달이다. 일반 진행·완료·계약 확인 알림에는 새 턴 없는 ax_message를 사용하고 수신자가 작업 경계에서 ax_mailbox로 묶음 회수한다. 기존 맥락을 유지하며 새 모델 프로세스를 만들지 않는다. submitted는 완료가 아니다. unknown이면 자동 재시도하지 말고 ax_result로 확인한다. 작업 중인 세션을 강제 중단하지 않는다.',
|
|
396
497
|
inputSchema: { type: 'object', properties: {
|
|
397
498
|
to: { type: 'string', description: 'ax_sessions에서 확인한 상대 세션 UUID' },
|
|
398
499
|
prompt: { type: 'string', description: '상대가 실행할 작업 요청' },
|
|
500
|
+
wait_ms: { type: 'integer', minimum: 0, maximum: 30000, description: '전달 후 회신을 로컬에서 기다릴 최대 시간(ms). 짧은 요청은 30000으로 한 번에 회수한다. 기본 0.' },
|
|
399
501
|
}, required: ['to', 'prompt'] },
|
|
400
502
|
},
|
|
401
503
|
{
|
|
@@ -403,6 +505,7 @@ const TOOLS = [
|
|
|
403
505
|
description: '작업 요청의 제출/완료/불명 상태, 결과와 측정 사용량을 확인한다. 관리형 세션은 구조화된 완료 이벤트, PTY는 종료 훅, 독립 Windows console은 요청 UUID가 있는 대화 기록과 현재 빈 입력창의 대기 상태로 판정한다.',
|
|
404
506
|
inputSchema: { type: 'object', properties: {
|
|
405
507
|
id: { type: 'string', description: 'ax_dispatch가 반환한 요청 UUID' },
|
|
508
|
+
wait_ms: { type: 'integer', minimum: 0, maximum: 30000, description: '로컬 회신 대기(ms). 모델 호출 없이 완료 즉시 반환. 기본 0.' },
|
|
406
509
|
}, required: ['id'] },
|
|
407
510
|
},
|
|
408
511
|
{
|
|
@@ -848,8 +951,12 @@ function dispatch(name, args = {}) {
|
|
|
848
951
|
|
|
849
952
|
switch (name) {
|
|
850
953
|
case 'ax_sessions': return bridge(['sessions'])
|
|
851
|
-
case '
|
|
852
|
-
|
|
954
|
+
case 'ax_message': return bridge(['message', '--to', args.to, '--priority', args.priority ?? 'boundary',
|
|
955
|
+
...[['reason', args.reason], ['topic', args.topic], ['key', args.key], ['ttl-seconds', args.ttl_seconds]].flatMap(([k, v]) => v === undefined ? [] : [`--${k}`, String(v)])], args.text)
|
|
956
|
+
case 'ax_mailbox': return bridge(['mailbox', '--to', args.to, ...(args.take ? ['--take'] : []), ...(args.all ? ['--all'] : []), ...(args.at ? ['--at', args.at] : [])])
|
|
957
|
+
case 'ax_mailbox_flush': return bridge(['flush', '--to', args.to, '--at', args.at])
|
|
958
|
+
case 'ax_dispatch': return bridge(['send', '--to', args.to, '--wait-ms', String(args.wait_ms ?? 0)], args.prompt)
|
|
959
|
+
case 'ax_result': return bridge(['result', args.id, '--wait-ms', String(args.wait_ms ?? 0)])
|
|
853
960
|
case 'ax_chat': return bridge(['chat', '--background', '--left', args.left, '--right', args.right,
|
|
854
961
|
...['maxTurns', 'maxTokens', 'maxMessageChars'].flatMap(key => args[key] === undefined ? [] :
|
|
855
962
|
[`--${key.replace(/[A-Z]/g, c => `-${c.toLowerCase()}`)}`, String(args[key])])], args.prompt)
|
|
@@ -1041,13 +1148,28 @@ async function handle(req) {
|
|
|
1041
1148
|
|
|
1042
1149
|
if (method === 'initialize') {
|
|
1043
1150
|
rootsSupported = !!params?.capabilities?.roots
|
|
1151
|
+
clientHasApps = !!params?.capabilities?.extensions?.['io.modelcontextprotocol/ui']?.mimeTypes?.includes(PIPELINE_MIME)
|
|
1044
1152
|
return {
|
|
1045
1153
|
protocolVersion: '2024-11-05',
|
|
1046
|
-
capabilities: { tools: {} },
|
|
1154
|
+
capabilities: { tools: {}, resources: {} },
|
|
1047
1155
|
serverInfo: { name: 'axmap', version: VERSION },
|
|
1156
|
+
instructions: 'axMap works across ChatGPT, Claude and agy. Routine collaboration updates go through ax_message (boundary queue, no new turn). At safe task/test/experiment checkpoints collect your ax_mailbox with take=true and at; batch decisions, do not ACK informational updates. Use immediate only for stop/data-loss/work-invalidated; busy delivery is deferred, not an interrupt. ax_dispatch starts an explicit execution request, not a routine notification. 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. Observations never call a model. Do not resubmit unknown requests.',
|
|
1048
1157
|
}
|
|
1049
1158
|
}
|
|
1050
|
-
if (method === 'tools/list') return { tools: TOOLS
|
|
1159
|
+
if (method === 'tools/list') return { tools: TOOLS.filter(tool => clientHasApps || tool.name !== 'ax_pipeline_state').map(tool => {
|
|
1160
|
+
if (clientHasApps || !tool._meta?.ui?.resourceUri) return tool
|
|
1161
|
+
const { ui, 'openai/outputTemplate': outputTemplate, ...metadata } = tool._meta
|
|
1162
|
+
return { ...tool, _meta: metadata }
|
|
1163
|
+
}) }
|
|
1164
|
+
if (method === 'resources/list') return { resources: [{ uri: PIPELINE_URI, name: 'axmap-pipeline', title: 'axMap 실행 흐름', mimeType: PIPELINE_MIME }] }
|
|
1165
|
+
if (method === 'resources/read') {
|
|
1166
|
+
if (params?.uri !== PIPELINE_URI) throw Object.assign(new Error('알 수 없는 UI 리소스입니다.'), { code: -32602 })
|
|
1167
|
+
return { contents: [{ uri: PIPELINE_URI, mimeType: PIPELINE_MIME,
|
|
1168
|
+
text: fs.readFileSync(path.join(SELF, 'mcp', 'pipeline.html'), 'utf8'),
|
|
1169
|
+
_meta: { ui: { prefersBorder: true, csp: { connectDomains: [], resourceDomains: ['https://cdn.jsdelivr.net'] } },
|
|
1170
|
+
'openai/widgetCSP': { connect_domains: [], resource_domains: ['https://cdn.jsdelivr.net'] } },
|
|
1171
|
+
}] }
|
|
1172
|
+
}
|
|
1051
1173
|
if (method === 'tools/call') {
|
|
1052
1174
|
if (rootsSupported && !process.env.AXMAP_REPO) {
|
|
1053
1175
|
if (!rootsTask || rootsError) refreshRoots()
|
|
@@ -1055,7 +1177,19 @@ async function handle(req) {
|
|
|
1055
1177
|
let task
|
|
1056
1178
|
do { task = rootsTask; await task } while (task !== rootsTask)
|
|
1057
1179
|
}
|
|
1058
|
-
const
|
|
1180
|
+
const args = params?.arguments ?? {}
|
|
1181
|
+
if (PIPELINE_TOOLS.has(params?.name)) {
|
|
1182
|
+
const cwd = resolveRepo(args)
|
|
1183
|
+
return pipeline(params.name === 'ax_pipeline_watch' ? 'watch' : 'snapshot', args, req, cwd)
|
|
1184
|
+
}
|
|
1185
|
+
if (['ax_dispatch', 'ax_result'].includes(params?.name)) {
|
|
1186
|
+
let switched
|
|
1187
|
+
try { switched = syncRepo(args) } catch (e) { return { content: [{ type: 'text', text: e.message }], isError: true } }
|
|
1188
|
+
const answer = await pipeline(params.name === 'ax_dispatch' ? 'dispatch' : 'result', args, req, REPO)
|
|
1189
|
+
if (switched) answer.content[0].text = switchNotice(switched) + '\n\n' + answer.content[0].text
|
|
1190
|
+
return answer
|
|
1191
|
+
}
|
|
1192
|
+
const r = callTool(params?.name, args)
|
|
1059
1193
|
const localBridge = BRIDGE_TOOLS.has(params?.name)
|
|
1060
1194
|
const warning = r.ok && !localBridge ? syncConfiguredProfile(REPO, BUS, { ...process.env, AXMAP_AGENT: AGENT }) : ''
|
|
1061
1195
|
return { content: [{ type: 'text', text: r.text + (warning ? '\n' + warning : '') + (r.ok && !localBridge ? unreadBanner(params?.name, r.text) : '') }], isError: !r.ok }
|
|
@@ -1067,6 +1201,16 @@ async function handle(req) {
|
|
|
1067
1201
|
const rl = readline.createInterface({ input: process.stdin })
|
|
1068
1202
|
// Execute tools in order, but keep reading client replies while a tool awaits roots.
|
|
1069
1203
|
let requests = Promise.resolve()
|
|
1204
|
+
let initialized = Promise.resolve(), concurrentViews = 0
|
|
1205
|
+
async function respond(req) {
|
|
1206
|
+
try {
|
|
1207
|
+
const result = await handle(req)
|
|
1208
|
+
if (result === null) send({ jsonrpc: '2.0', id: req.id, error: { code: -32601, message: `지원하지 않는 메서드: ${req.method}` } })
|
|
1209
|
+
else send({ jsonrpc: '2.0', id: req.id, result })
|
|
1210
|
+
} catch (e) {
|
|
1211
|
+
send({ jsonrpc: '2.0', id: req.id, error: { code: typeof e.code === 'number' ? e.code : -32603, message: e.message } })
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1070
1214
|
rl.on('line', (line) => {
|
|
1071
1215
|
const s = line.trim()
|
|
1072
1216
|
if (!s) return
|
|
@@ -1083,22 +1227,19 @@ rl.on('line', (line) => {
|
|
|
1083
1227
|
return
|
|
1084
1228
|
}
|
|
1085
1229
|
if (req.id === undefined) {
|
|
1230
|
+
if (req.method === 'notifications/cancelled') { runningPipeline.get(req.params?.requestId)?.(); return }
|
|
1086
1231
|
if (rootsSupported && !process.env.AXMAP_REPO &&
|
|
1087
1232
|
['notifications/initialized', 'notifications/roots/list_changed'].includes(req.method)) refreshRoots()
|
|
1088
1233
|
return
|
|
1089
1234
|
}
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
} catch (e) {
|
|
1099
|
-
send({ jsonrpc: '2.0', id: req.id, error: { code: -32603, message: e.message } })
|
|
1100
|
-
}
|
|
1101
|
-
})
|
|
1235
|
+
if ((req.method === 'tools/call' && PIPELINE_TOOLS.has(req.params?.name)) || ['resources/list', 'resources/read'].includes(req.method)) {
|
|
1236
|
+
if (concurrentViews >= 4) { send({ jsonrpc: '2.0', id: req.id, error: { code: -32000, message: '동시 파이프라인 조회 한도입니다. 잠시 후 다시 조회하세요.' } }); return }
|
|
1237
|
+
concurrentViews++
|
|
1238
|
+
void initialized.then(() => respond(req)).finally(() => { concurrentViews-- })
|
|
1239
|
+
return
|
|
1240
|
+
}
|
|
1241
|
+
requests = requests.then(() => respond(req))
|
|
1242
|
+
if (req.method === 'initialize') initialized = requests
|
|
1102
1243
|
})
|
|
1103
1244
|
|
|
1104
1245
|
process.stderr.write(`axMap MCP · 저장소 ${REPO} · 에이전트 ${AGENT} (${ACTOR})\n`)
|