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
package/src/external-console.mjs
CHANGED
|
@@ -1,116 +1,159 @@
|
|
|
1
|
+
// Generated from src/external-console.mts. Edit the .mts source and run npm run build.
|
|
1
2
|
/** Attach to an existing Windows console; never launch or resume a model. */
|
|
2
|
-
import fs from 'node:fs'
|
|
3
|
-
import path from 'node:path'
|
|
4
|
-
import os from 'node:os'
|
|
5
|
-
import { createHash } from 'node:crypto'
|
|
6
|
-
import { spawnSync } from 'node:child_process'
|
|
7
|
-
import { fileURLToPath } from 'node:url'
|
|
8
|
-
import { bridgeHome, repoKey, readJSON, writeJSON } from './bridge.mjs'
|
|
9
|
-
|
|
10
|
-
const helper = fileURLToPath(new URL('../tools/windows-console.ps1', import.meta.url))
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import { createHash } from 'node:crypto';
|
|
7
|
+
import { spawnSync } from 'node:child_process';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
import { bridgeHome, repoKey, readJSON, writeJSON } from './bridge.mjs';
|
|
10
|
+
import { nativeConsoleHelper } from './windows-console.mjs';
|
|
11
|
+
const helper = fileURLToPath(new URL('../tools/windows-console.ps1', import.meta.url));
|
|
11
12
|
export function consoleCall(pid, action = 'snapshot', payload = {}) {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
13
|
+
if (process.platform !== 'win32')
|
|
14
|
+
throw new Error('독립 agy 콘솔 연결은 현재 Windows에서 지원합니다.');
|
|
15
|
+
if (!Number.isSafeInteger(pid) || pid <= 0)
|
|
16
|
+
throw new Error('양의 정수 PID가 필요합니다.');
|
|
17
|
+
const native = nativeConsoleHelper();
|
|
18
|
+
const r = spawnSync(native || 'powershell.exe', native ? [String(pid), action] : ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass',
|
|
19
|
+
'-File', helper, '-TargetPid', String(pid), '-Action', action], {
|
|
20
|
+
input: JSON.stringify(payload), encoding: 'utf8', windowsHide: true, timeout: 8000, maxBuffer: 1024 * 1024,
|
|
21
|
+
});
|
|
22
|
+
let data;
|
|
23
|
+
try {
|
|
24
|
+
data = JSON.parse(r.stdout);
|
|
25
|
+
}
|
|
26
|
+
catch { /* report the underlying failure */ }
|
|
27
|
+
if (r.error || r.status !== 0 || data?.error || !data)
|
|
28
|
+
throw new Error(data?.error || r.error?.message || r.stderr?.trim() || '기존 콘솔 연결 실패');
|
|
29
|
+
if (action === 'ancestor' && data.ignored)
|
|
30
|
+
return { ignored: true };
|
|
31
|
+
if (typeof data.pid !== 'number' || typeof data.startedAt !== 'string' || (action !== 'ancestor' && typeof data.screen !== 'string'))
|
|
32
|
+
throw new Error('기존 콘솔 응답 형식이 올바르지 않습니다.');
|
|
33
|
+
return data;
|
|
22
34
|
}
|
|
23
35
|
function identityID({ pid, startedAt }) {
|
|
24
|
-
|
|
25
|
-
|
|
36
|
+
const hex = createHash('sha256').update(`agy-console:${pid}:${startedAt}`).digest('hex');
|
|
37
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-4${hex.slice(13, 16)}-a${hex.slice(17, 20)}-${hex.slice(20, 32)}`;
|
|
26
38
|
}
|
|
27
|
-
const target = id => path.join(bridgeHome(), 'sessions', `${id}.json`)
|
|
39
|
+
const target = (id) => path.join(bridgeHome(), 'sessions', `${id}.json`);
|
|
28
40
|
export function consoleIdle(screen) {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
41
|
+
// Use the last input box only. Old answers may contain quoted prompt boxes.
|
|
42
|
+
const borders = [...screen.matchAll(/^[─━]{10,}\r?$/gm)];
|
|
43
|
+
if (borders.length < 2)
|
|
44
|
+
return false;
|
|
45
|
+
const top = borders.at(-2), bottom = borders.at(-1);
|
|
46
|
+
if (!top || !bottom)
|
|
47
|
+
return false;
|
|
48
|
+
const input = screen.slice(top.index + top[0].length, bottom.index).trim();
|
|
49
|
+
const footer = screen.slice(bottom.index + bottom[0].length);
|
|
50
|
+
return input === '>' && /\? for shortcuts/.test(footer) && !/esc to cancel/i.test(footer);
|
|
36
51
|
}
|
|
37
52
|
export function attachConsole({ pid, cwd }) {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
53
|
+
const live = consoleCall(pid);
|
|
54
|
+
const id = identityID(live);
|
|
55
|
+
const previous = fs.existsSync(target(id)) ? readJSON(target(id)) : {};
|
|
56
|
+
const repo = repoKey(cwd);
|
|
57
|
+
if (previous.repos && !previous.repos.includes(repo))
|
|
58
|
+
throw new Error('이 프로세스는 다른 저장소에 등록되어 있습니다.');
|
|
59
|
+
const idle = consoleIdle(live.screen);
|
|
60
|
+
const s = { ...previous, id, provider: 'agy', transport: 'console', pid, startedAt: live.startedAt,
|
|
61
|
+
workspaces: previous.workspaces || [path.resolve(cwd)], repos: previous.repos || [repo],
|
|
62
|
+
state: idle ? 'idle' : 'unknown', fullyIdle: idle, registeredAt: new Date().toISOString(), event: previous.event || 'attach' };
|
|
63
|
+
writeJSON(target(id), s);
|
|
64
|
+
return { id, provider: s.provider, transport: s.transport, pid, state: s.state, workspaces: s.workspaces };
|
|
49
65
|
}
|
|
50
66
|
export function registerExternalHook(input, event) {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
67
|
+
if (!['SessionStart', 'PreInvocation', 'Stop'].includes(event))
|
|
68
|
+
throw new Error('지원하지 않는 훅 이벤트입니다.');
|
|
69
|
+
const identity = consoleCall(process.pid, 'ancestor');
|
|
70
|
+
if (identity.ignored)
|
|
71
|
+
return {};
|
|
72
|
+
const id = identityID(identity);
|
|
73
|
+
const previous = fs.existsSync(target(id)) ? readJSON(target(id)) : {};
|
|
74
|
+
const workspaces = input.workspacePaths || previous.workspaces;
|
|
75
|
+
if (!Array.isArray(workspaces) || !workspaces.length || workspaces.some(p => !path.isAbsolute(p)))
|
|
76
|
+
throw new Error('훅의 작업 저장소 경로가 없습니다.');
|
|
77
|
+
const conversationId = input.conversationId;
|
|
78
|
+
if (typeof conversationId !== 'string' || !/^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/i.test(conversationId))
|
|
79
|
+
throw new Error('훅의 대화 UUID가 없습니다.');
|
|
80
|
+
const transcript = path.join(process.env.AXMAP_AGY_HOME || path.join(os.homedir(), '.gemini', 'antigravity-cli'), 'brain', conversationId, '.system_generated', 'logs', 'transcript.jsonl');
|
|
81
|
+
if (path.resolve(input.transcriptPath || '') !== transcript)
|
|
82
|
+
throw new Error('훅의 대화 기록 경로가 다릅니다.');
|
|
83
|
+
const idle = event === 'Stop' && input.fullyIdle === true;
|
|
84
|
+
writeJSON(target(id), { ...previous, ...identity, id, provider: 'agy', transport: 'console', conversationId,
|
|
85
|
+
transcript, workspaces, repos: [...new Set(workspaces.map(repoKey))], model: input.modelName || previous.model,
|
|
86
|
+
event, state: idle ? 'idle' : 'running', fullyIdle: idle, registeredAt: new Date().toISOString() });
|
|
87
|
+
return { id };
|
|
68
88
|
}
|
|
69
|
-
export function externalStatus(
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
89
|
+
export function externalStatus(session) {
|
|
90
|
+
const s = readJSON(target(session.id));
|
|
91
|
+
const live = consoleCall(consolePID(s), 'snapshot', { startedAt: s.startedAt });
|
|
92
|
+
const state = consoleIdle(live.screen) ? 'idle' : 'unknown';
|
|
93
|
+
writeJSON(target(s.id), { ...readJSON(target(s.id)), state, fullyIdle: state === 'idle' });
|
|
94
|
+
return { id: s.id, state };
|
|
95
|
+
}
|
|
96
|
+
function consolePID(s) {
|
|
97
|
+
if (!s.pid || !s.startedAt)
|
|
98
|
+
throw new Error('등록된 콘솔 프로세스 신원이 없습니다.');
|
|
99
|
+
return s.pid;
|
|
75
100
|
}
|
|
76
101
|
export function checkConsole(s) {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
102
|
+
const live = consoleCall(consolePID(s), 'snapshot', { startedAt: s.startedAt });
|
|
103
|
+
if (!consoleIdle(live.screen))
|
|
104
|
+
throw new Error('상대 콘솔이 빈 입력창에서 대기 중이지 않습니다. 입력을 보내지 않았습니다.');
|
|
105
|
+
return live;
|
|
80
106
|
}
|
|
81
107
|
export function validateConsolePrompt(prompt, from) {
|
|
82
|
-
|
|
108
|
+
if (/[\x00-\x08\x0b-\x1f\x7f]/.test(prompt.replace(/\r\n/g, '\n')) || /[\x00-\x1f\x7f]/.test(from))
|
|
109
|
+
throw new Error('콘솔 요청에는 제어 문자를 포함할 수 없습니다.');
|
|
83
110
|
}
|
|
84
111
|
export function findConsoleTranscript(s, request) {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
112
|
+
if (s.transcript)
|
|
113
|
+
return s;
|
|
114
|
+
const brain = path.join(process.env.AXMAP_AGY_HOME || path.join(os.homedir(), '.gemini', 'antigravity-cli'), 'brain');
|
|
115
|
+
if (!fs.existsSync(brain))
|
|
116
|
+
return s;
|
|
117
|
+
const marker = `[axMap request ${request.id}]`;
|
|
118
|
+
const matches = [];
|
|
119
|
+
for (const entry of fs.readdirSync(brain, { withFileTypes: true })) {
|
|
120
|
+
if (!entry.isDirectory() || !/^[0-9a-f-]{36}$/i.test(entry.name))
|
|
121
|
+
continue;
|
|
122
|
+
const log = path.join(brain, entry.name, '.system_generated', 'logs', 'transcript.jsonl');
|
|
123
|
+
let stat;
|
|
124
|
+
try {
|
|
125
|
+
stat = fs.statSync(log);
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (stat.mtimeMs < Date.parse(request.createdAt) || stat.size > 32 * 1024 * 1024)
|
|
131
|
+
continue;
|
|
132
|
+
const records = fs.readFileSync(log, 'utf8').split('\n').slice(0, -1);
|
|
133
|
+
if (records.some(line => {
|
|
134
|
+
try {
|
|
135
|
+
const e = JSON.parse(line);
|
|
136
|
+
return e.type === 'USER_INPUT' && String(e.content).includes(marker);
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
}))
|
|
142
|
+
matches.push({ conversationId: entry.name, transcript: log });
|
|
143
|
+
}
|
|
144
|
+
if (matches.length !== 1)
|
|
145
|
+
return s;
|
|
146
|
+
const updated = { ...readJSON(target(s.id)), ...matches[0] };
|
|
147
|
+
writeJSON(target(s.id), updated);
|
|
148
|
+
return updated;
|
|
106
149
|
}
|
|
107
|
-
export function submitConsole(s, request) {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
150
|
+
export function submitConsole(s, request, preflight) {
|
|
151
|
+
validateConsolePrompt(request.prompt, request.from);
|
|
152
|
+
const live = preflight || checkConsole(s);
|
|
153
|
+
// Bracketed paste preserves multiline prompts and prevents embedded Enter keys
|
|
154
|
+
// from becoming separate submissions. Only the final CR submits the message.
|
|
155
|
+
const text = `\x1b[200~[axMap request ${request.id}]\n요청자: ${request.from}\n${request.prompt.replace(/\r\n/g, '\n')}\x1b[201~\r`;
|
|
156
|
+
consoleCall(consolePID(s), 'send', { startedAt: s.startedAt, expectedScreen: live.screen, text });
|
|
157
|
+
writeJSON(target(s.id), { ...readJSON(target(s.id)), state: 'running', fullyIdle: false });
|
|
158
|
+
return { accepted: true, pid: s.pid };
|
|
116
159
|
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/** Attach to an existing Windows console; never launch or resume a model. */
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import os from 'node:os'
|
|
5
|
+
import { createHash } from 'node:crypto'
|
|
6
|
+
import { spawnSync } from 'node:child_process'
|
|
7
|
+
import { fileURLToPath } from 'node:url'
|
|
8
|
+
import { bridgeHome, repoKey, readJSON, writeJSON } from './bridge.mjs'
|
|
9
|
+
import { nativeConsoleHelper } from './windows-console.mjs'
|
|
10
|
+
import type { SessionRecord, RequestRecord, ConsoleSnapshot, HookInput, TranscriptEntry } from './session-types.mjs'
|
|
11
|
+
|
|
12
|
+
const helper = fileURLToPath(new URL('../tools/windows-console.ps1', import.meta.url))
|
|
13
|
+
export function consoleCall(pid: number, action?: 'snapshot' | 'send', payload?: Record<string, unknown>): ConsoleSnapshot
|
|
14
|
+
export function consoleCall(pid: number, action: 'ancestor', payload?: Record<string, unknown>): ConsoleSnapshot | { ignored: true }
|
|
15
|
+
export function consoleCall(pid: number, action = 'snapshot', payload: Record<string, unknown> = {}): ConsoleSnapshot | { ignored: true } {
|
|
16
|
+
if (process.platform !== 'win32') throw new Error('독립 agy 콘솔 연결은 현재 Windows에서 지원합니다.')
|
|
17
|
+
if (!Number.isSafeInteger(pid) || pid <= 0) throw new Error('양의 정수 PID가 필요합니다.')
|
|
18
|
+
const native = nativeConsoleHelper()
|
|
19
|
+
const r = spawnSync(native || 'powershell.exe', native ? [String(pid), action] : ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass',
|
|
20
|
+
'-File', helper, '-TargetPid', String(pid), '-Action', action], {
|
|
21
|
+
input: JSON.stringify(payload), encoding: 'utf8', windowsHide: true, timeout: 8000, maxBuffer: 1024 * 1024,
|
|
22
|
+
})
|
|
23
|
+
let data: (Omit<Partial<ConsoleSnapshot>, 'ignored'> & { error?: string; ignored?: boolean }) | undefined
|
|
24
|
+
try { data = JSON.parse(r.stdout) } catch { /* report the underlying failure */ }
|
|
25
|
+
if (r.error || r.status !== 0 || data?.error || !data) throw new Error(data?.error || r.error?.message || r.stderr?.trim() || '기존 콘솔 연결 실패')
|
|
26
|
+
if (action === 'ancestor' && data.ignored) return { ignored: true }
|
|
27
|
+
if (typeof data.pid !== 'number' || typeof data.startedAt !== 'string' || (action !== 'ancestor' && typeof data.screen !== 'string')) throw new Error('기존 콘솔 응답 형식이 올바르지 않습니다.')
|
|
28
|
+
return data as ConsoleSnapshot
|
|
29
|
+
}
|
|
30
|
+
function identityID({ pid, startedAt }: Pick<ConsoleSnapshot, 'pid' | 'startedAt'>): string {
|
|
31
|
+
const hex = createHash('sha256').update(`agy-console:${pid}:${startedAt}`).digest('hex')
|
|
32
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-4${hex.slice(13, 16)}-a${hex.slice(17, 20)}-${hex.slice(20, 32)}`
|
|
33
|
+
}
|
|
34
|
+
const target = (id: string) => path.join(bridgeHome(), 'sessions', `${id}.json`)
|
|
35
|
+
export function consoleIdle(screen: string): boolean {
|
|
36
|
+
// Use the last input box only. Old answers may contain quoted prompt boxes.
|
|
37
|
+
const borders = [...screen.matchAll(/^[─━]{10,}\r?$/gm)]
|
|
38
|
+
if (borders.length < 2) return false
|
|
39
|
+
const top = borders.at(-2), bottom = borders.at(-1)
|
|
40
|
+
if (!top || !bottom) return false
|
|
41
|
+
const input = screen.slice(top.index + top[0].length, bottom.index).trim()
|
|
42
|
+
const footer = screen.slice(bottom.index + bottom[0].length)
|
|
43
|
+
return input === '>' && /\? for shortcuts/.test(footer) && !/esc to cancel/i.test(footer)
|
|
44
|
+
}
|
|
45
|
+
export function attachConsole({ pid, cwd }: { pid: number; cwd: string }) {
|
|
46
|
+
const live = consoleCall(pid)
|
|
47
|
+
const id = identityID(live)
|
|
48
|
+
const previous = fs.existsSync(target(id)) ? readJSON<Partial<SessionRecord>>(target(id)) : {}
|
|
49
|
+
const repo = repoKey(cwd)
|
|
50
|
+
if (previous.repos && !previous.repos.includes(repo)) throw new Error('이 프로세스는 다른 저장소에 등록되어 있습니다.')
|
|
51
|
+
const idle = consoleIdle(live.screen)
|
|
52
|
+
const s: SessionRecord = { ...previous, id, provider: 'agy', transport: 'console', pid, startedAt: live.startedAt,
|
|
53
|
+
workspaces: previous.workspaces || [path.resolve(cwd)], repos: previous.repos || [repo],
|
|
54
|
+
state: idle ? 'idle' : 'unknown', fullyIdle: idle, registeredAt: new Date().toISOString(), event: previous.event || 'attach' }
|
|
55
|
+
writeJSON(target(id), s)
|
|
56
|
+
return { id, provider: s.provider, transport: s.transport, pid, state: s.state, workspaces: s.workspaces }
|
|
57
|
+
}
|
|
58
|
+
export function registerExternalHook(input: HookInput, event: string) {
|
|
59
|
+
if (!['SessionStart', 'PreInvocation', 'Stop'].includes(event)) throw new Error('지원하지 않는 훅 이벤트입니다.')
|
|
60
|
+
const identity = consoleCall(process.pid, 'ancestor')
|
|
61
|
+
if (identity.ignored) return {}
|
|
62
|
+
const id = identityID(identity)
|
|
63
|
+
const previous = fs.existsSync(target(id)) ? readJSON<Partial<SessionRecord>>(target(id)) : {}
|
|
64
|
+
const workspaces = input.workspacePaths || previous.workspaces
|
|
65
|
+
if (!Array.isArray(workspaces) || !workspaces.length || workspaces.some(p => !path.isAbsolute(p))) throw new Error('훅의 작업 저장소 경로가 없습니다.')
|
|
66
|
+
const conversationId = input.conversationId
|
|
67
|
+
if (typeof conversationId !== 'string' || !/^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/i.test(conversationId)) throw new Error('훅의 대화 UUID가 없습니다.')
|
|
68
|
+
const transcript = path.join(process.env.AXMAP_AGY_HOME || path.join(os.homedir(), '.gemini', 'antigravity-cli'),
|
|
69
|
+
'brain', conversationId, '.system_generated', 'logs', 'transcript.jsonl')
|
|
70
|
+
if (path.resolve(input.transcriptPath || '') !== transcript) throw new Error('훅의 대화 기록 경로가 다릅니다.')
|
|
71
|
+
const idle = event === 'Stop' && input.fullyIdle === true
|
|
72
|
+
writeJSON(target(id), { ...previous, ...identity, id, provider: 'agy', transport: 'console', conversationId,
|
|
73
|
+
transcript, workspaces, repos: [...new Set(workspaces.map(repoKey))], model: input.modelName || previous.model,
|
|
74
|
+
event, state: idle ? 'idle' : 'running', fullyIdle: idle, registeredAt: new Date().toISOString() })
|
|
75
|
+
return { id }
|
|
76
|
+
}
|
|
77
|
+
export function externalStatus(session: Pick<SessionRecord, 'id'>) {
|
|
78
|
+
const s = readJSON<SessionRecord>(target(session.id))
|
|
79
|
+
const live = consoleCall(consolePID(s), 'snapshot', { startedAt: s.startedAt })
|
|
80
|
+
const state: 'idle' | 'unknown' = consoleIdle(live.screen) ? 'idle' : 'unknown'
|
|
81
|
+
writeJSON(target(s.id), { ...readJSON<SessionRecord>(target(s.id)), state, fullyIdle: state === 'idle' })
|
|
82
|
+
return { id: s.id, state }
|
|
83
|
+
}
|
|
84
|
+
function consolePID(s: SessionRecord): number {
|
|
85
|
+
if (!s.pid || !s.startedAt) throw new Error('등록된 콘솔 프로세스 신원이 없습니다.')
|
|
86
|
+
return s.pid
|
|
87
|
+
}
|
|
88
|
+
export function checkConsole(s: SessionRecord): ConsoleSnapshot {
|
|
89
|
+
const live = consoleCall(consolePID(s), 'snapshot', { startedAt: s.startedAt })
|
|
90
|
+
if (!consoleIdle(live.screen)) throw new Error('상대 콘솔이 빈 입력창에서 대기 중이지 않습니다. 입력을 보내지 않았습니다.')
|
|
91
|
+
return live
|
|
92
|
+
}
|
|
93
|
+
export function validateConsolePrompt(prompt: string, from: string): void {
|
|
94
|
+
if (/[\x00-\x08\x0b-\x1f\x7f]/.test(prompt.replace(/\r\n/g, '\n')) || /[\x00-\x1f\x7f]/.test(from)) throw new Error('콘솔 요청에는 제어 문자를 포함할 수 없습니다.')
|
|
95
|
+
}
|
|
96
|
+
export function findConsoleTranscript(s: SessionRecord, request: RequestRecord): SessionRecord {
|
|
97
|
+
if (s.transcript) return s
|
|
98
|
+
const brain = path.join(process.env.AXMAP_AGY_HOME || path.join(os.homedir(), '.gemini', 'antigravity-cli'), 'brain')
|
|
99
|
+
if (!fs.existsSync(brain)) return s
|
|
100
|
+
const marker = `[axMap request ${request.id}]`
|
|
101
|
+
const matches = []
|
|
102
|
+
for (const entry of fs.readdirSync(brain, { withFileTypes: true })) {
|
|
103
|
+
if (!entry.isDirectory() || !/^[0-9a-f-]{36}$/i.test(entry.name)) continue
|
|
104
|
+
const log = path.join(brain, entry.name, '.system_generated', 'logs', 'transcript.jsonl')
|
|
105
|
+
let stat
|
|
106
|
+
try { stat = fs.statSync(log) } catch { continue }
|
|
107
|
+
if (stat.mtimeMs < Date.parse(request.createdAt) || stat.size > 32 * 1024 * 1024) continue
|
|
108
|
+
const records = fs.readFileSync(log, 'utf8').split('\n').slice(0, -1)
|
|
109
|
+
if (records.some(line => {
|
|
110
|
+
try { const e = JSON.parse(line) as TranscriptEntry; return e.type === 'USER_INPUT' && String(e.content).includes(marker) }
|
|
111
|
+
catch { return false }
|
|
112
|
+
})) matches.push({ conversationId: entry.name, transcript: log })
|
|
113
|
+
}
|
|
114
|
+
if (matches.length !== 1) return s
|
|
115
|
+
const updated = { ...readJSON<SessionRecord>(target(s.id)), ...matches[0] }
|
|
116
|
+
writeJSON(target(s.id), updated)
|
|
117
|
+
return updated
|
|
118
|
+
}
|
|
119
|
+
export function submitConsole(s: SessionRecord, request: RequestRecord, preflight?: ConsoleSnapshot) {
|
|
120
|
+
validateConsolePrompt(request.prompt, request.from)
|
|
121
|
+
const live = preflight || checkConsole(s)
|
|
122
|
+
// Bracketed paste preserves multiline prompts and prevents embedded Enter keys
|
|
123
|
+
// from becoming separate submissions. Only the final CR submits the message.
|
|
124
|
+
const text = `\x1b[200~[axMap request ${request.id}]\n요청자: ${request.from}\n${request.prompt.replace(/\r\n/g, '\n')}\x1b[201~\r`
|
|
125
|
+
consoleCall(consolePID(s), 'send', { startedAt: s.startedAt, expectedScreen: live.screen, text })
|
|
126
|
+
writeJSON(target(s.id), { ...readJSON<SessionRecord>(target(s.id)), state: 'running', fullyIdle: false })
|
|
127
|
+
return { accepted: true, pid: s.pid }
|
|
128
|
+
}
|