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,235 @@
|
|
|
1
|
+
/** The user launches an interactive CLI. axMap owns its PTY and a local,
|
|
2
|
+
* authenticated pipe; remote requests never launch/resume another model. */
|
|
3
|
+
import fs from 'node:fs'
|
|
4
|
+
import os from 'node:os'
|
|
5
|
+
import path from 'node:path'
|
|
6
|
+
import net from 'node:net'
|
|
7
|
+
import { randomUUID, randomBytes, timingSafeEqual } from 'node:crypto'
|
|
8
|
+
import { fileURLToPath } from 'node:url'
|
|
9
|
+
import { createRequire } from 'node:module'
|
|
10
|
+
import { bridgeHome, repoKey, readJSON, writeJSON } from './bridge.mjs'
|
|
11
|
+
import { errorCode, errorMessage } from './session-types.mjs'
|
|
12
|
+
import type { IPty } from 'node-pty'
|
|
13
|
+
import type { Provider, SessionRecord, LocalMessage, JobRecord, RequestState, TranscriptEntry } from './session-types.mjs'
|
|
14
|
+
|
|
15
|
+
interface TerminalOptions {
|
|
16
|
+
executable: string; args?: string[]; provider: Provider; cwd?: string; onData?: (data: string) => void; cols?: number; rows?: number
|
|
17
|
+
}
|
|
18
|
+
interface CommandHook { type: string; command: string; timeout: number; statusMessage?: string }
|
|
19
|
+
interface HookGroup { hooks?: Array<{ statusMessage?: string; [key: string]: unknown }>; [key: string]: unknown }
|
|
20
|
+
interface HookConfig { hooks?: Record<string, HookGroup[]>; [key: string]: unknown }
|
|
21
|
+
|
|
22
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
|
23
|
+
const validID = (id: string | undefined) => /^[0-9a-f-]{36}$/i.test(id || '')
|
|
24
|
+
const sessionFile = (id: string) => {
|
|
25
|
+
if (!validID(id)) throw new Error('잘못된 터미널 세션 ID입니다.')
|
|
26
|
+
return path.join(bridgeHome(), 'sessions', `${id}.json`)
|
|
27
|
+
}
|
|
28
|
+
const pipeFor = (id: string) => process.platform === 'win32'
|
|
29
|
+
? `\\\\.\\pipe\\axmap-${id}` : path.join(bridgeHome(), `${id}.sock`)
|
|
30
|
+
|
|
31
|
+
export function sendTerminal(id: string, message: LocalMessage, token: string | null = null, { timeoutMs = 10000 } = {}): Promise<unknown> {
|
|
32
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1) throw new Error('양의 응답 시간 한도가 필요합니다.')
|
|
33
|
+
const s = readJSON<SessionRecord>(sessionFile(id))
|
|
34
|
+
if (!['pty', 'stdio'].includes(s.transport || '')) throw new Error('로컬 관리형 세션이 아닙니다.')
|
|
35
|
+
return new Promise((resolve, reject) => {
|
|
36
|
+
const socket = net.createConnection(pipeFor(id))
|
|
37
|
+
socket.setEncoding('utf8')
|
|
38
|
+
let buffer = ''
|
|
39
|
+
const fail = (e: Error) => { socket.destroy(); reject(e) }
|
|
40
|
+
socket.setTimeout(timeoutMs, () => fail(new Error('로컬 세션 응답 시간 초과. 자동 재전송하지 마세요.')))
|
|
41
|
+
socket.on('error', fail)
|
|
42
|
+
socket.on('connect', () => socket.write(JSON.stringify({ ...message, token: token || s.token }) + '\n'))
|
|
43
|
+
socket.on('data', chunk => {
|
|
44
|
+
buffer += chunk
|
|
45
|
+
if (Buffer.byteLength(buffer) > 1024 * 1024) return fail(new Error('로컬 응답 크기 제한 초과'))
|
|
46
|
+
const end = buffer.indexOf('\n')
|
|
47
|
+
if (end < 0) return
|
|
48
|
+
socket.end()
|
|
49
|
+
try {
|
|
50
|
+
const response = JSON.parse(buffer.slice(0, end)) as { error?: string; result?: unknown }
|
|
51
|
+
if (response.error) reject(new Error(response.error)); else resolve(response.result)
|
|
52
|
+
} catch (e) { reject(e) }
|
|
53
|
+
})
|
|
54
|
+
socket.on('end', () => { if (!buffer.includes('\n')) reject(new Error('세션 연결이 응답 없이 종료됐습니다.')) })
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function startTerminal({ executable, args = [], provider, cwd = process.cwd(), onData = () => {}, cols = 100, rows = 30 }: TerminalOptions) {
|
|
59
|
+
const { spawn } = await import('node-pty')
|
|
60
|
+
const id = randomUUID(), token = randomBytes(32).toString('hex')
|
|
61
|
+
const s: SessionRecord = { id, provider, transport: 'pty', token, workspaces: [cwd], repos: [repoKey(cwd)],
|
|
62
|
+
state: 'starting', fullyIdle: false, registeredAt: new Date().toISOString(), event: 'launch' }
|
|
63
|
+
const jobs = new Map<string, JobRecord>()
|
|
64
|
+
let pending: JobRecord | null = null, draft = false, term: IPty, closed = false, stopTimer: NodeJS.Timeout | undefined
|
|
65
|
+
const save = () => { s.registeredAt = new Date().toISOString(); writeJSON(sessionFile(id), s) }
|
|
66
|
+
const finish = (state: RequestState, result?: unknown, error?: string) => {
|
|
67
|
+
if (!pending) return
|
|
68
|
+
Object.assign(pending, { state, result, error, completedAt: new Date().toISOString() })
|
|
69
|
+
jobs.set(pending.id, { ...pending }); pending = null
|
|
70
|
+
}
|
|
71
|
+
const handle = (message: LocalMessage) => {
|
|
72
|
+
const offered = Buffer.from(typeof message.token === 'string' ? message.token : '')
|
|
73
|
+
const expected = Buffer.from(token)
|
|
74
|
+
if (offered.length !== expected.length || !timingSafeEqual(offered, expected)) throw new Error('로컬 세션 인증 실패')
|
|
75
|
+
if (message.action === 'status') return { state: s.state, id, provider }
|
|
76
|
+
if (message.action === 'result') {
|
|
77
|
+
const job = jobs.get(message.id)
|
|
78
|
+
if (!job) throw new Error('이 터미널에 해당 요청이 없습니다.')
|
|
79
|
+
return job
|
|
80
|
+
}
|
|
81
|
+
if (message.action === 'hook') {
|
|
82
|
+
clearTimeout(stopTimer)
|
|
83
|
+
const { input, event } = message
|
|
84
|
+
s.event = event; s.model = input.modelName || input.model || s.model
|
|
85
|
+
s.conversationId = input.conversationId || input.session_id || s.conversationId
|
|
86
|
+
if (event !== 'Stop') {
|
|
87
|
+
s.state = 'running'; s.fullyIdle = false
|
|
88
|
+
if (pending && event === 'UserPromptSubmit' && typeof input.prompt === 'string') {
|
|
89
|
+
pending.observed = input.prompt.includes(`[axMap request ${pending.id}]`)
|
|
90
|
+
}
|
|
91
|
+
save(); return {}
|
|
92
|
+
}
|
|
93
|
+
if (provider === 'agy' && input.fullyIdle !== true) { save(); return {} }
|
|
94
|
+
let answer = input.last_assistant_message
|
|
95
|
+
if (provider === 'agy') {
|
|
96
|
+
const log = path.join(process.env.AXMAP_AGY_HOME || path.join(os.homedir(), '.gemini', 'antigravity-cli'),
|
|
97
|
+
'brain', s.conversationId || '', '.system_generated', 'logs', 'transcript.jsonl')
|
|
98
|
+
if (path.resolve(input.transcriptPath || '') !== log) throw new Error('종료 훅의 대화 기록 경로가 다릅니다.')
|
|
99
|
+
const lines = fs.readFileSync(log, 'utf8').trimEnd().split('\n').filter(Boolean).map(line => JSON.parse(line) as TranscriptEntry)
|
|
100
|
+
const lastUser = lines.findLast(e => e.type === 'USER_INPUT')
|
|
101
|
+
if (pending) pending.observed = String(lastUser?.content).includes(`[axMap request ${pending.id}]`)
|
|
102
|
+
answer = lines.findLast(e => e.type === 'PLANNER_RESPONSE' && e.source === 'MODEL' && e.status === 'DONE')?.content
|
|
103
|
+
}
|
|
104
|
+
if (pending) finish(pending.observed && !pending.interrupted && typeof answer === 'string' ? 'completed' : 'superseded',
|
|
105
|
+
pending.observed && !pending.interrupted ? answer : undefined,
|
|
106
|
+
pending.observed && !pending.interrupted ? undefined : '다른 입력이 섞였거나 이 요청의 응답을 확인하지 못했습니다.')
|
|
107
|
+
// Stop runs before the provider restores its input UI. Allow that handoff
|
|
108
|
+
// to finish before accepting another injected prompt.
|
|
109
|
+
s.state = 'settling'; s.fullyIdle = false; save()
|
|
110
|
+
stopTimer = setTimeout(() => { if (!closed && s.state === 'settling') { s.state = 'idle'; s.fullyIdle = true; draft = false; save() } }, 500)
|
|
111
|
+
return {}
|
|
112
|
+
}
|
|
113
|
+
if (message.action === 'submit') {
|
|
114
|
+
const request = message.request
|
|
115
|
+
if (closed || s.state !== 'idle' || draft || pending) throw new Error('상대가 대기 중이 아니거나 입력 중입니다. 작업을 전달하지 않았습니다.')
|
|
116
|
+
if (!validID(request?.id) || request.to !== id || typeof request.prompt !== 'string' || !request.prompt.trim() || Buffer.byteLength(request.prompt) > 32768) throw new Error('요청 형식이 올바르지 않습니다.')
|
|
117
|
+
const prompt = `[axMap request ${request.id}]\n요청자: ${String(request.from || 'axmap')}\n${request.prompt}`
|
|
118
|
+
if (/[\x00-\x08\x0b-\x1f\x7f]/.test(prompt)) throw new Error('터미널 제어 문자가 포함된 요청은 보낼 수 없습니다.')
|
|
119
|
+
pending = { id: request.id, to: id, state: 'submitted' }; jobs.set(request.id, pending)
|
|
120
|
+
s.state = 'running'; s.fullyIdle = false; save()
|
|
121
|
+
term.write(`\x1b[200~${prompt}\x1b[201~`)
|
|
122
|
+
setTimeout(() => { if (!closed) term.write('\r') }, 100)
|
|
123
|
+
return { accepted: true, session: id }
|
|
124
|
+
}
|
|
125
|
+
throw new Error('지원하지 않는 로컬 세션 요청입니다.')
|
|
126
|
+
}
|
|
127
|
+
fs.mkdirSync(bridgeHome(), { recursive: true, mode: 0o700 })
|
|
128
|
+
const server = net.createServer(socket => {
|
|
129
|
+
socket.setEncoding('utf8')
|
|
130
|
+
let buffer = '', handled = false
|
|
131
|
+
socket.setTimeout(15000, () => socket.destroy())
|
|
132
|
+
socket.on('error', () => {})
|
|
133
|
+
socket.on('data', chunk => {
|
|
134
|
+
if (handled) return
|
|
135
|
+
buffer += chunk
|
|
136
|
+
if (Buffer.byteLength(buffer) > 65536) return socket.destroy()
|
|
137
|
+
const end = buffer.indexOf('\n'); if (end < 0) return
|
|
138
|
+
handled = true
|
|
139
|
+
try { socket.end(JSON.stringify({ result: handle(JSON.parse(buffer.slice(0, end))) }) + '\n') }
|
|
140
|
+
catch (e) { socket.end(JSON.stringify({ error: errorMessage(e) }) + '\n') }
|
|
141
|
+
})
|
|
142
|
+
})
|
|
143
|
+
await new Promise<void>((resolve, reject) => { server.once('error', reject); server.listen(pipeFor(id), resolve) })
|
|
144
|
+
if (process.platform !== 'win32') fs.chmodSync(pipeFor(id), 0o600)
|
|
145
|
+
save()
|
|
146
|
+
try {
|
|
147
|
+
term = spawn(executable, args, { name: 'xterm-256color', cols, rows, cwd, env: { ...process.env,
|
|
148
|
+
AXMAP_TERMINAL_SESSION: id, AXMAP_TERMINAL_TOKEN: token,
|
|
149
|
+
AXMAP_AGENT: `${provider}-${id.slice(0, 8)}`, AXMAP_SESSION: id,
|
|
150
|
+
}, useConpty: true, useConptyDll: process.platform === 'win32' })
|
|
151
|
+
} catch (e) { server.close(); s.state = 'closed'; save(); throw e }
|
|
152
|
+
term.onData(onData)
|
|
153
|
+
const exited = new Promise<{ exitCode: number; signal?: number }>(resolve => term.onExit(ev => {
|
|
154
|
+
if (closed) return
|
|
155
|
+
closed = true; clearTimeout(stopTimer)
|
|
156
|
+
// ConPTY may keep its input handle alive after a normal child exit.
|
|
157
|
+
if (process.platform === 'win32') { try { term.kill() } catch { /* already closed */ } }
|
|
158
|
+
finish('unknown', undefined, '요청의 완료를 확인하기 전에 세션이 종료됐습니다.')
|
|
159
|
+
s.state = 'closed'; s.fullyIdle = false; save(); server.close(); resolve(ev)
|
|
160
|
+
}))
|
|
161
|
+
return { id, pid: term.pid, exited,
|
|
162
|
+
input(data: string) {
|
|
163
|
+
const terminalReply = /^\x1b\[(?:\d+;\d+R|\?[^a-zA-Z]*[a-zA-Z])$/.test(String(data))
|
|
164
|
+
if (terminalReply) { term.write(data); return }
|
|
165
|
+
if (pending) pending.interrupted = true
|
|
166
|
+
draft = !String(data).includes('\r')
|
|
167
|
+
if (!draft) { s.state = 'running'; s.fullyIdle = false; save() }
|
|
168
|
+
term.write(data)
|
|
169
|
+
},
|
|
170
|
+
resize: (c: number, r: number) => term.resize(c, r),
|
|
171
|
+
close: () => term.kill(),
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Copy only production dependencies from the npm package, not a developer's
|
|
176
|
+
* entire node_modules. This also keeps the setup copy independent of npm cache. */
|
|
177
|
+
export function installTerminalRuntime(sourceRoot: string, destRoot = ROOT) {
|
|
178
|
+
const source = path.resolve(sourceRoot), dest = path.resolve(destRoot)
|
|
179
|
+
const require = createRequire(path.join(source, 'package.json'))
|
|
180
|
+
const copied = new Set<string>()
|
|
181
|
+
function copy(name: string): void {
|
|
182
|
+
if (copied.has(name)) return
|
|
183
|
+
const descriptor = require.resolve(`${name}/package.json`)
|
|
184
|
+
const pkg = readJSON<{ dependencies?: Record<string, string>; optionalDependencies?: Record<string, string> }>(descriptor)
|
|
185
|
+
const target = path.join(dest, 'node_modules', ...name.split('/'))
|
|
186
|
+
if (path.resolve(path.dirname(descriptor)) !== path.resolve(target)) {
|
|
187
|
+
fs.mkdirSync(path.dirname(target), { recursive: true })
|
|
188
|
+
fs.cpSync(path.dirname(descriptor), target, { recursive: true, force: true })
|
|
189
|
+
}
|
|
190
|
+
copied.add(name)
|
|
191
|
+
for (const dependency of Object.keys(pkg.dependencies || {})) copy(dependency)
|
|
192
|
+
for (const dependency of Object.keys(pkg.optionalDependencies || {})) {
|
|
193
|
+
try { require.resolve(`${dependency}/package.json`) }
|
|
194
|
+
catch (e) { if (errorCode(e) === 'MODULE_NOT_FOUND') continue; throw e }
|
|
195
|
+
copy(dependency)
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
copy('node-pty')
|
|
199
|
+
const installed = createRequire(path.join(dest, 'package.json'))('node-pty')
|
|
200
|
+
if (typeof installed.spawn !== 'function') throw new Error('설치된 터미널 라이브러리를 불러오지 못했습니다.')
|
|
201
|
+
return { packages: [...copied], destination: dest }
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function installTerminalHooks(home = os.homedir(), provider = 'agy') {
|
|
205
|
+
if (!['agy', 'codex'].includes(provider)) throw new Error('지원하지 않는 CLI 훅입니다.')
|
|
206
|
+
const target = provider === 'agy' ? path.join(home, '.gemini', 'config', 'hooks.json')
|
|
207
|
+
: path.join(home, '.codex', 'hooks.json')
|
|
208
|
+
const config = fs.existsSync(target) ? readJSON<HookConfig>(target) : {}
|
|
209
|
+
if (!config || typeof config !== 'object' || Array.isArray(config)) throw new Error('기존 hooks.json을 읽지 못했습니다.')
|
|
210
|
+
if (provider === 'codex' && config.hooks !== undefined && (!config.hooks || typeof config.hooks !== 'object' || Array.isArray(config.hooks))) throw new Error('기존 Codex hooks 설정이 객체가 아닙니다.')
|
|
211
|
+
const definition: Record<string, CommandHook[]> = {}
|
|
212
|
+
const script = path.join(ROOT, 'tools', 'run.mjs')
|
|
213
|
+
if ([process.execPath, script].some(p => /["\r\n%&|<>^]/.test(p))) throw new Error('훅 경로에 지원하지 않는 셸 문자가 있습니다.')
|
|
214
|
+
for (const event of provider === 'agy' ? ['SessionStart', 'PreInvocation', 'Stop'] : ['SessionStart', 'UserPromptSubmit', 'Stop']) {
|
|
215
|
+
let command = `"${process.execPath}" "${script}" hook ${event}`
|
|
216
|
+
if (process.platform === 'win32') {
|
|
217
|
+
const wrapper = path.join(home, '.axmap', 'bridge', `terminal-${provider}-${event}.cmd`)
|
|
218
|
+
if (/[\s"%&|<>^]/.test(wrapper)) throw new Error('이 Windows 훅 경로에는 공백/셸 문자를 사용할 수 없습니다.')
|
|
219
|
+
fs.mkdirSync(path.dirname(wrapper), { recursive: true })
|
|
220
|
+
fs.writeFileSync(wrapper, `@echo off\r\n${command}\r\n`)
|
|
221
|
+
command = wrapper
|
|
222
|
+
}
|
|
223
|
+
if (provider === 'agy') definition[event] = [{ type: 'command', command, timeout: 10 }]
|
|
224
|
+
else {
|
|
225
|
+
config.hooks ||= {}
|
|
226
|
+
const existing = config.hooks[event] || []
|
|
227
|
+
if (!Array.isArray(existing)) throw new Error(`기존 Codex ${event} 설정이 배열이 아닙니다.`)
|
|
228
|
+
config.hooks[event] = existing.filter(group => !(Array.isArray(group.hooks) && group.hooks.some(h => h.statusMessage === 'axMap terminal bridge')))
|
|
229
|
+
config.hooks[event].push({ hooks: [{ type: 'command', command, timeout: 10, statusMessage: 'axMap terminal bridge' }] })
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if (provider === 'agy') config['axmap-terminal'] = definition
|
|
233
|
+
writeJSON(target, config)
|
|
234
|
+
return target
|
|
235
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Generated from src/windows-console.mts. Edit the .mts source and run npm run build.
|
|
2
|
+
/** Cache a framework-only native helper; machines without csc keep the PS path. */
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
7
|
+
import { spawnSync } from 'node:child_process';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
const source = fileURLToPath(new URL('../tools/windows-console.cs', import.meta.url));
|
|
10
|
+
export function nativeConsoleHelper() {
|
|
11
|
+
if (process.platform !== 'win32' || process.env.AXMAP_CONSOLE_HELPER === 'powershell')
|
|
12
|
+
return null;
|
|
13
|
+
const framework = path.join(process.env.WINDIR || 'C:\\Windows', 'Microsoft.NET');
|
|
14
|
+
const compiler = ['Framework64', 'Framework'].map(p => path.join(framework, p, 'v4.0.30319', 'csc.exe')).find(p => fs.existsSync(p));
|
|
15
|
+
if (!compiler || !fs.existsSync(source))
|
|
16
|
+
return null;
|
|
17
|
+
const hash = createHash('sha256').update(fs.readFileSync(source)).digest('hex').slice(0, 24);
|
|
18
|
+
const dir = path.join(process.env.AXMAP_STATE_DIR || path.join(os.homedir(), '.axmap'), 'bridge', 'native');
|
|
19
|
+
const executable = path.join(dir, `console-${hash}.exe`);
|
|
20
|
+
if (fs.existsSync(executable))
|
|
21
|
+
return executable;
|
|
22
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
23
|
+
const temporary = path.join(dir, `${hash}-${randomUUID()}.exe`);
|
|
24
|
+
try {
|
|
25
|
+
const r = spawnSync(compiler, ['/nologo', '/optimize+', '/target:exe', '/reference:System.Web.Extensions.dll',
|
|
26
|
+
'/reference:System.Management.dll', `/out:${temporary}`, source], { encoding: 'utf8', windowsHide: true, timeout: 15000 });
|
|
27
|
+
if (r.error || r.status !== 0)
|
|
28
|
+
return null;
|
|
29
|
+
try {
|
|
30
|
+
fs.renameSync(temporary, executable);
|
|
31
|
+
}
|
|
32
|
+
catch (e) {
|
|
33
|
+
if (!fs.existsSync(executable))
|
|
34
|
+
throw e;
|
|
35
|
+
}
|
|
36
|
+
return executable;
|
|
37
|
+
}
|
|
38
|
+
finally {
|
|
39
|
+
if (fs.existsSync(temporary))
|
|
40
|
+
fs.unlinkSync(temporary);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** Cache a framework-only native helper; machines without csc keep the PS path. */
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import os from 'node:os'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import { createHash, randomUUID } from 'node:crypto'
|
|
6
|
+
import { spawnSync } from 'node:child_process'
|
|
7
|
+
import { fileURLToPath } from 'node:url'
|
|
8
|
+
|
|
9
|
+
const source = fileURLToPath(new URL('../tools/windows-console.cs', import.meta.url))
|
|
10
|
+
export function nativeConsoleHelper() {
|
|
11
|
+
if (process.platform !== 'win32' || process.env.AXMAP_CONSOLE_HELPER === 'powershell') return null
|
|
12
|
+
const framework = path.join(process.env.WINDIR || 'C:\\Windows', 'Microsoft.NET')
|
|
13
|
+
const compiler = ['Framework64', 'Framework'].map(p => path.join(framework, p, 'v4.0.30319', 'csc.exe')).find(p => fs.existsSync(p))
|
|
14
|
+
if (!compiler || !fs.existsSync(source)) return null
|
|
15
|
+
const hash = createHash('sha256').update(fs.readFileSync(source)).digest('hex').slice(0, 24)
|
|
16
|
+
const dir = path.join(process.env.AXMAP_STATE_DIR || path.join(os.homedir(), '.axmap'), 'bridge', 'native')
|
|
17
|
+
const executable = path.join(dir, `console-${hash}.exe`)
|
|
18
|
+
if (fs.existsSync(executable)) return executable
|
|
19
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 })
|
|
20
|
+
const temporary = path.join(dir, `${hash}-${randomUUID()}.exe`)
|
|
21
|
+
try {
|
|
22
|
+
const r = spawnSync(compiler, ['/nologo', '/optimize+', '/target:exe', '/reference:System.Web.Extensions.dll',
|
|
23
|
+
'/reference:System.Management.dll', `/out:${temporary}`, source], { encoding: 'utf8', windowsHide: true, timeout: 15000 })
|
|
24
|
+
if (r.error || r.status !== 0) return null
|
|
25
|
+
try { fs.renameSync(temporary, executable) }
|
|
26
|
+
catch (e) { if (!fs.existsSync(executable)) throw e }
|
|
27
|
+
return executable
|
|
28
|
+
} finally { if (fs.existsSync(temporary)) fs.unlinkSync(temporary) }
|
|
29
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/** Explicitly invoked real-console benchmark. Sends only the listed test prompts. */
|
|
3
|
+
import fs from 'node:fs'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import { randomUUID } from 'node:crypto'
|
|
6
|
+
import { consoleCall, attachConsole } from '../src/external-console.mjs'
|
|
7
|
+
import { dispatchRequest, waitForRequest } from '../src/bridge.mjs'
|
|
8
|
+
import { nativeConsoleHelper } from '../src/windows-console.mjs'
|
|
9
|
+
|
|
10
|
+
const args = process.argv.slice(2)
|
|
11
|
+
const option = n => { const i = args.indexOf(`--${n}`); return i < 0 ? undefined : args[i + 1] }
|
|
12
|
+
const pid = Number(option('pid'))
|
|
13
|
+
if (!Number.isSafeInteger(pid) || pid <= 0) throw new Error('Usage: node tools/bench-console.mjs --pid <agy PID> [--output file]')
|
|
14
|
+
if (!nativeConsoleHelper()) throw new Error('Native helper is unavailable; refusing to label fallback timings as native.')
|
|
15
|
+
const originalMode = process.env.AXMAP_CONSOLE_HELPER
|
|
16
|
+
const report = { measuredAt: new Date().toISOString(), platform: process.platform, node: process.version,
|
|
17
|
+
pid, cwd: process.cwd(), snapshots: {}, requests: [], safetyChecks: {} }
|
|
18
|
+
const median = values => [...values].sort((a, b) => a - b)[Math.floor(values.length / 2)]
|
|
19
|
+
try {
|
|
20
|
+
for (const mode of ['powershell', 'native']) {
|
|
21
|
+
if (mode === 'powershell') process.env.AXMAP_CONSOLE_HELPER = mode
|
|
22
|
+
else delete process.env.AXMAP_CONSOLE_HELPER
|
|
23
|
+
const samples = []
|
|
24
|
+
for (let i = 0; i < 7; i++) {
|
|
25
|
+
const start = performance.now()
|
|
26
|
+
consoleCall(pid)
|
|
27
|
+
samples.push(Number((performance.now() - start).toFixed(2)))
|
|
28
|
+
}
|
|
29
|
+
report.snapshots[mode] = { samplesMs: samples, medianMs: median(samples) }
|
|
30
|
+
console.log(JSON.stringify({ event: 'snapshots', mode, ...report.snapshots[mode] }))
|
|
31
|
+
}
|
|
32
|
+
const live = consoleCall(pid)
|
|
33
|
+
report.startedAt = live.startedAt
|
|
34
|
+
for (const [name, payload] of Object.entries({ changedIdentity: { startedAt: 'invalid' }, changedScreen: { startedAt: live.startedAt, expectedScreen: 'invalid', text: 'MUST_NOT_BE_SENT' } })) {
|
|
35
|
+
try { consoleCall(pid, name === 'changedScreen' ? 'send' : 'snapshot', payload); throw new Error('Safety rejection missing') }
|
|
36
|
+
catch (e) {
|
|
37
|
+
const expected = name === 'changedScreen' ? /Console changed/ : /identity changed/
|
|
38
|
+
if (!expected.test(e.message)) throw e
|
|
39
|
+
report.safetyChecks[name] = e.message
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const session = attachConsole({ pid, cwd: process.cwd() })
|
|
43
|
+
report.sessionId = session.id
|
|
44
|
+
const memory = randomUUID().slice(0, 8)
|
|
45
|
+
const prompts = [
|
|
46
|
+
{ prompt: `Transport latency test 1. Remember the code ${memory} for the next message. Do not use tools, MCP, commands or files. Reply in this chat with exactly: AXMAP_LATENCY_READY`, expected: 'AXMAP_LATENCY_READY' },
|
|
47
|
+
{ prompt: 'Transport latency test 2. Do not use tools, MCP, commands or files. Reply in this chat with only the code I asked you to remember in the previous message.', expected: memory },
|
|
48
|
+
{ prompt: 'Transport latency test 3 (Unicode). Do not use tools, MCP, commands or files. Reply in this chat with exactly: 한글_왕복_OK', expected: '한글_왕복_OK' },
|
|
49
|
+
]
|
|
50
|
+
for (const { prompt, expected } of prompts) {
|
|
51
|
+
const start = performance.now()
|
|
52
|
+
const request = dispatchRequest({ to: session.id, cwd: process.cwd(), from: 'codex-latency-benchmark', prompt })
|
|
53
|
+
const dispatchMs = performance.now() - start
|
|
54
|
+
const result = await waitForRequest(request.id, process.cwd(), 30000)
|
|
55
|
+
const entry = { id: request.id, dispatchMs: Number(dispatchMs.toFixed(2)), roundTripMs: Number((performance.now() - start).toFixed(2)),
|
|
56
|
+
state: result.state, expected, result: result.result, passed: result.state === 'completed' && String(result.result).includes(expected) }
|
|
57
|
+
report.requests.push(entry)
|
|
58
|
+
console.log(JSON.stringify({ event: 'reply', ...entry }))
|
|
59
|
+
if (!entry.passed) { process.exitCode = 1; break }
|
|
60
|
+
}
|
|
61
|
+
report.snapshotSpeedup = Number((report.snapshots.powershell.medianMs / report.snapshots.native.medianMs).toFixed(2))
|
|
62
|
+
if (option('output')) {
|
|
63
|
+
const output = path.resolve(option('output'))
|
|
64
|
+
fs.mkdirSync(path.dirname(output), { recursive: true })
|
|
65
|
+
fs.writeFileSync(output, JSON.stringify(report, null, 2) + '\n')
|
|
66
|
+
}
|
|
67
|
+
} finally {
|
|
68
|
+
if (originalMode === undefined) delete process.env.AXMAP_CONSOLE_HELPER
|
|
69
|
+
else process.env.AXMAP_CONSOLE_HELPER = originalMode
|
|
70
|
+
}
|
package/tools/bridge.mjs
CHANGED
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
import fs from 'node:fs'
|
|
3
3
|
import { resolveBin } from '../src/agentcli.mjs'
|
|
4
4
|
import { codexInvocation } from './mcp-register.mjs'
|
|
5
|
-
import { dispatchRequest, installHooks, registerHook,
|
|
5
|
+
import { dispatchRequest, installHooks, registerHook, waitForRequest, sessions, uninstallHooks } from '../src/bridge.mjs'
|
|
6
6
|
import { startManagedSession } from '../src/managed-session.mjs'
|
|
7
7
|
import { runDialogue, startBackgroundDialogue, dialogueResult, stopDialogue } from '../src/session-dialogue.mjs'
|
|
8
8
|
import { sendTerminal } from '../src/terminal-bridge.mjs'
|
|
9
9
|
import { sessionStatus, formatSessionStatus } from '../src/session-status.mjs'
|
|
10
10
|
import { attachConsole } from '../src/external-console.mjs'
|
|
11
|
+
import { postMail, collectMail, flushMail } from '../src/session-mailbox.mjs'
|
|
11
12
|
|
|
12
13
|
const args = process.argv.slice(2)
|
|
13
14
|
const [command] = args
|
|
@@ -15,14 +16,31 @@ const option = (name) => { const i = args.indexOf(`--${name}`); return i < 0 ? u
|
|
|
15
16
|
const output = (value) => console.log(JSON.stringify(value, null, 2))
|
|
16
17
|
try {
|
|
17
18
|
switch (command) {
|
|
19
|
+
case 'message': {
|
|
20
|
+
const r = postMail({ to: option('to'), cwd: process.cwd(), text: fs.readFileSync(0, 'utf8'),
|
|
21
|
+
priority: option('priority') || 'boundary', reason: option('reason'), topic: option('topic'), key: option('key'),
|
|
22
|
+
ttlSeconds: Number(option('ttl-seconds') || 3600) })
|
|
23
|
+
output(r)
|
|
24
|
+
if (r.delivery && !['submitted', 'empty'].includes(r.delivery.state)) process.exitCode = 2
|
|
25
|
+
break
|
|
26
|
+
}
|
|
27
|
+
case 'mailbox': output(collectMail({ to: option('to'), cwd: process.cwd(), at: option('at'), take: args.includes('--take'), all: args.includes('--all') })); break
|
|
28
|
+
case 'flush': {
|
|
29
|
+
const r = flushMail({ to: option('to'), cwd: process.cwd(), at: option('at') }); output(r)
|
|
30
|
+
if (['unknown', 'deferred'].includes(r.state)) process.exitCode = 2
|
|
31
|
+
break
|
|
32
|
+
}
|
|
18
33
|
case 'attach': output(attachConsole({ pid: Number(option('pid')), cwd: process.cwd() })); break
|
|
34
|
+
case 'connect':
|
|
19
35
|
case 'serve': {
|
|
20
36
|
const provider = args[1]
|
|
21
37
|
if (!['codex', 'agy'].includes(provider)) throw new Error('axmap bridge serve codex|agy를 사용하세요.')
|
|
22
38
|
const binary = resolveBin(provider)
|
|
23
39
|
if (!binary) throw new Error(`${provider} 실행 파일을 찾지 못했습니다.`)
|
|
24
40
|
const [executable, ...prefix] = provider === 'codex' ? codexInvocation(binary) : [binary]
|
|
41
|
+
if (command === 'connect' && (!option('thread') || !option('socket'))) throw new Error('bridge connect codex --thread <기존 UUID> --socket <로컬 경로>가 필요합니다.')
|
|
25
42
|
const session = await startManagedSession({ provider, executable, prefix, model: option('model'), role: option('role'),
|
|
43
|
+
...(command === 'connect' ? { existingThreadId: option('thread'), socketPath: option('socket'), observeActive: args.includes('--observe') } : {}),
|
|
26
44
|
maxTokens: Number(option('max-tokens') || 100000), onEvent: output })
|
|
27
45
|
output({ event: 'connected', ...session, exited: undefined, close: undefined })
|
|
28
46
|
process.once('SIGINT', session.close); process.once('SIGTERM', session.close)
|
|
@@ -41,6 +59,7 @@ try {
|
|
|
41
59
|
break
|
|
42
60
|
}
|
|
43
61
|
case 'dialogue': output(dialogueResult(args[1])); break
|
|
62
|
+
case 'notice': output(await sendTerminal(option('to'), { action: 'notice', text: fs.readFileSync(0, 'utf8') })); break
|
|
44
63
|
case 'stop': output(stopDialogue(args[1])); break
|
|
45
64
|
case 'answer': output(await sendTerminal(option('to'), { action: 'answer', id: option('request'), decision: option('decision') })); break
|
|
46
65
|
case 'install':
|
|
@@ -64,17 +83,23 @@ try {
|
|
|
64
83
|
break
|
|
65
84
|
}
|
|
66
85
|
case 'send': {
|
|
86
|
+
const waitMs = Number(option('wait-ms') || 0)
|
|
87
|
+
if (!Number.isInteger(waitMs) || waitMs < 0 || waitMs > 30000) throw new Error('wait-ms는 0~30000 정수여야 합니다.')
|
|
67
88
|
const r = dispatchRequest({ to: option('to'), prompt: fs.readFileSync(0, 'utf8'), cwd: process.cwd(), from: process.env.AXMAP_AGENT || 'axmap' })
|
|
68
|
-
|
|
69
|
-
|
|
89
|
+
const result = waitMs && r.state === 'submitted' ? await waitForRequest(r.id, process.cwd(), waitMs) : r
|
|
90
|
+
output(result)
|
|
91
|
+
if (result.state === 'unknown') process.exitCode = 2
|
|
70
92
|
break
|
|
71
93
|
}
|
|
72
|
-
case 'result': output(
|
|
94
|
+
case 'result': output(await waitForRequest(args[1], process.cwd(), Number(option('wait-ms') || 0))); break
|
|
73
95
|
case undefined:
|
|
74
96
|
case '--help':
|
|
75
97
|
console.log(`axMap 로컬 세션 연결 — 관리형 Codex/agy와 제한된 자동 대화
|
|
76
98
|
axmap bridge serve codex|agy [--model 이름] [--role 역할] [--max-tokens 100000]
|
|
77
99
|
명시적으로 새 관리형 CLI를 시작합니다. 기존 창에 임의로 붙지 않습니다.
|
|
100
|
+
axmap bridge connect codex --thread <기존 UUID> --socket <로컬 socket> [--role 역할] [--observe]
|
|
101
|
+
실행 중인 공식 Codex daemon의 기존 idle 대화에 연결합니다. 새 모델/대화를 만들지 않습니다.
|
|
102
|
+
--observe는 active 대화에도 관측만 연결하며 idle 전환 전에는 새 요청을 거절합니다.
|
|
78
103
|
axmap bridge chat --left <UUID> --right <UUID> [--max-turns 4] [--max-tokens 50000]
|
|
79
104
|
stdin 작업을 자동 중계합니다. 기본 메시지 한도 2000자, 대기 중 모델 호출 없음.
|
|
80
105
|
axmap bridge answer --to <UUID> --request <ID> --decision accept|decline|cancel
|
|
@@ -85,10 +110,20 @@ axmap bridge stop <대화 UUID> 추가 중계 중단 (이
|
|
|
85
110
|
axmap bridge install --experimental --executable <agy 절대 경로> 연결 훅 실험
|
|
86
111
|
axmap bridge uninstall 추가한 연결 훅 해제
|
|
87
112
|
axmap bridge sessions 이 repo에 연결된 세션
|
|
113
|
+
axmap bridge notice --to <UUID> stdin 안내를 기존 Codex 문맥에 기록 (모델 호출 없음)
|
|
88
114
|
axmap bridge attach --pid <PID> 이미 실행한 Windows agy 콘솔을 현재 repo에 연결
|
|
89
115
|
axmap bridge status [--json] 로컬 연결·작업·승인 대기·종료 상태 조회 (모델 호출 없음)
|
|
90
|
-
axmap bridge send --to <세션 UUID>
|
|
91
|
-
|
|
116
|
+
axmap bridge send --to <세션 UUID> [--wait-ms 30000] 본문은 stdin. 전달 후 지정 시간까지 로컬에서 회신 대기
|
|
117
|
+
send는 실행용 저수준 명령입니다. 일반 협업 알림에는 message를 사용하세요.
|
|
118
|
+
axmap bridge message --to <UUID> [--topic <갱신 주제>] [--key <중복 방지 키>] [--ttl-seconds 3600]
|
|
119
|
+
stdin 일반 메시지를 큐에 저장합니다. 모델 호출·새 턴 없음. 같은 발신자의 대기 중 topic만 최신으로 합칩니다.
|
|
120
|
+
axmap bridge message --to <UUID> --priority immediate --reason stop|data-loss|work-invalidated
|
|
121
|
+
긴급 사유를 보존하고 즉시 전달을 시도합니다. busy면 deferred, unknown이면 재전송 금지.
|
|
122
|
+
axmap bridge mailbox --to <UUID> [--all]
|
|
123
|
+
읽기만 합니다. --take --at checkpoint로 작업 경계에서 묶음 회수 (모델 호출 없음).
|
|
124
|
+
axmap bridge flush --to <UUID> --at task-complete|test-complete|experiment-complete|checkpoint
|
|
125
|
+
필요할 때만 idle 수신자에게 큐를 한 요청으로 전달. 자동 감시·중단·재전송 없음.
|
|
126
|
+
axmap bridge result <요청 UUID> [--wait-ms 30000] 제출/완료/불명 상태와 결과. 대기 중 모델 호출 없음
|
|
92
127
|
관리형 stdio: Codex App Server / agy stream-json. PTY와 agentapi는 별도 실험 경로입니다.`)
|
|
93
128
|
break
|
|
94
129
|
default: throw new Error(`알 수 없는 bridge 명령: ${command}`)
|
package/tools/build.mjs
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/** TS owns migrated modules; checked-in JS keeps Node 20 and vendored installs usable. */
|
|
3
|
+
import fs from 'node:fs'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import { fileURLToPath } from 'node:url'
|
|
6
|
+
import { spawnSync } from 'node:child_process'
|
|
7
|
+
import { createRequire } from 'node:module'
|
|
8
|
+
|
|
9
|
+
const root = fileURLToPath(new URL('../', import.meta.url))
|
|
10
|
+
const check = process.argv.includes('--check')
|
|
11
|
+
// The stable CLI also works with TS 7's native compiler, without an unstable API.
|
|
12
|
+
const compiler = path.join(path.dirname(createRequire(import.meta.url).resolve('typescript/package.json')), 'bin', 'tsc')
|
|
13
|
+
const compiled = spawnSync(process.execPath, [compiler, '--project', path.join(root, 'tsconfig.json'), '--pretty', 'false'], {
|
|
14
|
+
cwd: root, stdio: 'inherit', windowsHide: true,
|
|
15
|
+
})
|
|
16
|
+
if (compiled.error) throw compiled.error
|
|
17
|
+
if (compiled.status !== 0) process.exit(compiled.status ?? 1)
|
|
18
|
+
const outputs = []
|
|
19
|
+
function collect(directory) {
|
|
20
|
+
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
|
21
|
+
const source = path.join(directory, entry.name)
|
|
22
|
+
if (entry.isDirectory()) { collect(source); continue }
|
|
23
|
+
if (!entry.name.endsWith('.mts')) continue
|
|
24
|
+
const relative = path.relative(root, source).replaceAll(path.sep, '/')
|
|
25
|
+
const text = fs.readFileSync(path.join(root, '.axmap', 'tsbuild', relative.replace(/\.mts$/, '.mjs')), 'utf8')
|
|
26
|
+
outputs.push({ target: source.replace(/\.mts$/, '.mjs'),
|
|
27
|
+
content: `// Generated from ${relative}. Edit the .mts source and run npm run build.\n${text}` })
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
collect(path.join(root, 'src'))
|
|
31
|
+
if (!outputs.length) throw new Error('TypeScript emitted no migrated modules.')
|
|
32
|
+
let stale = false
|
|
33
|
+
for (const { target, content } of outputs) {
|
|
34
|
+
if (fs.existsSync(target) && fs.readFileSync(target, 'utf8').replaceAll('\r\n', '\n') === content.replaceAll('\r\n', '\n')) continue
|
|
35
|
+
if (check) { console.error(`Stale generated JavaScript: ${path.relative(root, target)} (run npm run build)`); stale = true }
|
|
36
|
+
else fs.writeFileSync(target, content)
|
|
37
|
+
}
|
|
38
|
+
if (stale) process.exit(1)
|
|
39
|
+
console.log(`${check ? 'Checked' : 'Built'} ${outputs.length} TypeScript modules.`)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import { runPipelineOperation } from '../src/pipeline-mcp.mjs'
|
|
4
|
+
import { errorMessage } from '../src/session-types.mjs'
|
|
5
|
+
import { installPipelineStatusline, pipelineStatusline } from '../src/pipeline-statusline.mjs'
|
|
6
|
+
import { runMonitor } from '../src/monitor-cli.mjs'
|
|
7
|
+
import { startPipelineWeb } from '../src/pipeline-web.mjs'
|
|
8
|
+
const output = value => process.stdout.write(JSON.stringify(value) + '\n')
|
|
9
|
+
let cancelled = false
|
|
10
|
+
process.on('message', message => { if (message?.type === 'cancel-observation') cancelled = true })
|
|
11
|
+
try {
|
|
12
|
+
const action = process.argv[2]
|
|
13
|
+
const option = name => { const at = process.argv.indexOf(`--${name}`); return at < 0 ? undefined : process.argv[at + 1] }
|
|
14
|
+
if (process.argv.includes('--web')) {
|
|
15
|
+
const web = await startPipelineWeb(option('cwd') || process.cwd(), Number(option('port') || 0))
|
|
16
|
+
console.log(JSON.stringify({ url: web.url, mode: 'read-only-local-pipeline' }))
|
|
17
|
+
const close = () => { void web.close() }; process.once('SIGINT', close); process.once('SIGTERM', close)
|
|
18
|
+
} else if (!action || action === 'view' || action.startsWith('--') && !['--help', '-h'].includes(action)) {
|
|
19
|
+
await runMonitor(['--graph', ...process.argv.slice(action === 'view' ? 3 : 2)])
|
|
20
|
+
} else if (action === '--help' || action === '-h') {
|
|
21
|
+
console.log('axmap pipeline [view] [--cwd <repo>] [--once | --json]\naxmap pipeline --web [--cwd <repo>] [--port <local-port>]\n세션 그래프 · 역할 · 실제 요청/응답 · 토큰 · 이벤트를 표시합니다. TTY에서는 실시간 갱신합니다.\naxmap pipeline install-statusline --provider claude|agy\naxmap pipeline uninstall-statusline --provider claude|agy\n상태줄 설치는 기존 명령을 보존하며 새 창은 열지 않습니다.\n대화 안 패널은 MCP ax_pipeline, 진행 알림은 ax_pipeline_watch를 사용하세요.')
|
|
22
|
+
} else if (action === 'install-statusline' || action === 'uninstall-statusline') {
|
|
23
|
+
console.log(JSON.stringify(installPipelineStatusline(option('provider'), option('home'), action === 'uninstall-statusline'), null, 2))
|
|
24
|
+
} else if (action === 'statusline') {
|
|
25
|
+
process.stdout.write(await pipelineStatusline(option('provider'), fs.readFileSync(0, 'utf8'), option('home')))
|
|
26
|
+
} else {
|
|
27
|
+
const args = JSON.parse(fs.readFileSync(0, 'utf8'))
|
|
28
|
+
const result = await runPipelineOperation(action, args, message => output({ type: 'progress', message }), () => cancelled)
|
|
29
|
+
output({ type: 'result', result })
|
|
30
|
+
}
|
|
31
|
+
} catch (e) {
|
|
32
|
+
if (process.argv[2] === 'statusline') console.log('axMap · 상태 확인 필요')
|
|
33
|
+
else output({ type: 'result', result: { content: [{ type: 'text', text: errorMessage(e) }], isError: true } })
|
|
34
|
+
} finally { if (process.connected) process.disconnect() }
|
package/tools/run-tests.mjs
CHANGED
|
@@ -40,9 +40,10 @@ if (files.length === 0) {
|
|
|
40
40
|
// Windows ConPTY owns native console workers. Exercise it after other test
|
|
41
41
|
// processes have exited so temporary console teardown cannot race the suite's
|
|
42
42
|
// many concurrent subprocess fixtures. Both batches remain mandatory.
|
|
43
|
-
const
|
|
43
|
+
const nativeNames = new Set(['terminal-bridge.test.mjs', 'monitor-cli.test.mjs'])
|
|
44
|
+
const native = process.platform === 'win32' ? files.filter(f => nativeNames.has(path.basename(f))) : []
|
|
44
45
|
const ordinary = files.filter(f => !native.includes(f))
|
|
45
|
-
for (const batch of [ordinary, native].filter(batch => batch.length)) {
|
|
46
|
+
for (const batch of [ordinary, ...native.map(f => [f])].filter(batch => batch.length)) {
|
|
46
47
|
const r = spawnSync(process.execPath, ['--test', ...batch], { stdio: 'inherit', cwd: root })
|
|
47
48
|
if (r.status !== 0) process.exit(r.status ?? 1)
|
|
48
49
|
}
|