aiterm-mcp 0.7.1 → 0.9.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/README.ja.md +141 -66
- package/README.md +140 -65
- package/dist/codex-stop-hook.js +124 -0
- package/dist/core.js +1197 -45
- package/dist/grok-stop-hook.js +120 -0
- package/dist/index.js +58 -10
- package/dist/rtk.js +53 -18
- package/package.json +5 -2
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as os from "node:os";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
const LAUNCH_ID_RE = /^[0-9a-f]{32}$/;
|
|
6
|
+
const SESSION_RE = /^[A-Za-z0-9_-]{1,64}$/;
|
|
7
|
+
const MAX_STDIN_BYTES = 1024 * 1024;
|
|
8
|
+
function fail(message) {
|
|
9
|
+
process.stderr.write(`aiterm grok-stop-hook: ${message}\n`);
|
|
10
|
+
process.exit(0);
|
|
11
|
+
}
|
|
12
|
+
function noop() {
|
|
13
|
+
process.exit(0);
|
|
14
|
+
}
|
|
15
|
+
function hasAitermEnv() {
|
|
16
|
+
return !!(process.env.AITERM_AGENT_KIND ||
|
|
17
|
+
process.env.AITERM_SESSION_ID ||
|
|
18
|
+
process.env.AITERM_AGENT_SESSION_ID ||
|
|
19
|
+
process.env.AITERM_AGENT_LAUNCH_ID);
|
|
20
|
+
}
|
|
21
|
+
function uid() {
|
|
22
|
+
if (typeof process.getuid !== "function")
|
|
23
|
+
fail("POSIX getuid が使えません");
|
|
24
|
+
return process.getuid();
|
|
25
|
+
}
|
|
26
|
+
function runtimeStateBase() {
|
|
27
|
+
const xdg = process.env.XDG_RUNTIME_DIR;
|
|
28
|
+
if (xdg) {
|
|
29
|
+
try {
|
|
30
|
+
if (fs.statSync(xdg).isDirectory())
|
|
31
|
+
return xdg;
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
/* XDG_RUNTIME_DIR が壊れている CI/非 login 環境では os.tmpdir() に戻す */
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return os.tmpdir();
|
|
38
|
+
}
|
|
39
|
+
function secureAgentsDir() {
|
|
40
|
+
const root = path.join(runtimeStateBase(), `aiterm-mcp-${uid()}`);
|
|
41
|
+
const agents = path.join(root, "agents");
|
|
42
|
+
const rst = fs.lstatSync(root);
|
|
43
|
+
if (!rst.isDirectory() || rst.isSymbolicLink() || rst.uid !== uid() || (rst.mode & 0o077) !== 0) {
|
|
44
|
+
fail(`agent state root が安全ではありません: ${root}`);
|
|
45
|
+
}
|
|
46
|
+
const ast = fs.lstatSync(agents);
|
|
47
|
+
if (!ast.isDirectory() || ast.isSymbolicLink() || ast.uid !== uid() || (ast.mode & 0o077) !== 0) {
|
|
48
|
+
fail(`agent state dir が安全ではありません: ${agents}`);
|
|
49
|
+
}
|
|
50
|
+
return agents;
|
|
51
|
+
}
|
|
52
|
+
function str(v) {
|
|
53
|
+
return typeof v === "string" ? v : null;
|
|
54
|
+
}
|
|
55
|
+
async function readStdin() {
|
|
56
|
+
const chunks = [];
|
|
57
|
+
let total = 0;
|
|
58
|
+
for await (const chunk of process.stdin) {
|
|
59
|
+
const b = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk));
|
|
60
|
+
total += b.length;
|
|
61
|
+
if (total > MAX_STDIN_BYTES)
|
|
62
|
+
fail("payload が大きすぎます");
|
|
63
|
+
chunks.push(b);
|
|
64
|
+
}
|
|
65
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
66
|
+
}
|
|
67
|
+
function appendEvent(file, event) {
|
|
68
|
+
const nofollow = fs.constants.O_NOFOLLOW ?? 0;
|
|
69
|
+
const line = JSON.stringify(event) + "\n";
|
|
70
|
+
if (Buffer.byteLength(line, "utf8") > 64 * 1024)
|
|
71
|
+
fail("event line が大きすぎます");
|
|
72
|
+
const fd = fs.openSync(file, fs.constants.O_CREAT | fs.constants.O_APPEND | fs.constants.O_WRONLY | nofollow, 0o600);
|
|
73
|
+
try {
|
|
74
|
+
const st = fs.fstatSync(fd);
|
|
75
|
+
if (!st.isFile() || st.uid !== uid() || st.nlink !== 1 || (st.mode & 0o077) !== 0) {
|
|
76
|
+
fail(`event file が安全ではありません: ${file}`);
|
|
77
|
+
}
|
|
78
|
+
fs.writeSync(fd, line, undefined, "utf8");
|
|
79
|
+
}
|
|
80
|
+
finally {
|
|
81
|
+
fs.closeSync(fd);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
async function main() {
|
|
85
|
+
if (!hasAitermEnv())
|
|
86
|
+
noop();
|
|
87
|
+
const kind = process.env.AITERM_AGENT_KIND;
|
|
88
|
+
const session = process.env.AITERM_SESSION_ID || process.env.AITERM_AGENT_SESSION_ID || "";
|
|
89
|
+
const launchId = process.env.AITERM_AGENT_LAUNCH_ID || "";
|
|
90
|
+
if (kind !== "grok" && kind !== "composer")
|
|
91
|
+
fail(`AITERM_AGENT_KIND が grok/composer ではありません: ${kind ?? ""}`);
|
|
92
|
+
if (!SESSION_RE.test(session))
|
|
93
|
+
fail(`session id が不正です: ${session}`);
|
|
94
|
+
if (!LAUNCH_ID_RE.test(launchId))
|
|
95
|
+
fail(`launch id が不正です: ${launchId}`);
|
|
96
|
+
let payload = {};
|
|
97
|
+
const input = await readStdin();
|
|
98
|
+
if (input.trim()) {
|
|
99
|
+
try {
|
|
100
|
+
payload = JSON.parse(input);
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
payload = {};
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const agents = secureAgentsDir();
|
|
107
|
+
const eventFile = path.join(agents, `${session}.${launchId}.events.jsonl`);
|
|
108
|
+
appendEvent(eventFile, {
|
|
109
|
+
type: "agent_done",
|
|
110
|
+
vendor: kind,
|
|
111
|
+
aiterm_session: session,
|
|
112
|
+
launch_id: launchId,
|
|
113
|
+
vendor_session_id: str(payload.sessionId),
|
|
114
|
+
turn_id: str(payload.promptId),
|
|
115
|
+
reason: str(payload.reason) ?? str(payload.hookEventName) ?? "stop",
|
|
116
|
+
done_status: "turn_done",
|
|
117
|
+
at: new Date().toISOString(),
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
main().catch((e) => fail(e instanceof Error ? e.message : String(e)));
|
package/dist/index.js
CHANGED
|
@@ -12,7 +12,13 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
12
12
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
13
13
|
import { z } from "zod";
|
|
14
14
|
import * as core from "./core.js";
|
|
15
|
-
|
|
15
|
+
import { createRequire } from "node:module";
|
|
16
|
+
// package.json の version を実行時に読み、MCP initialize で配るサーバ版と一致させる。
|
|
17
|
+
// createRequire を使うのは、import 属性 `with { type: "json" }` が Node 18.20+ 限定で
|
|
18
|
+
// engines "node >=18"(18.0〜18.19)を SyntaxError で壊し、旧 `assert` 構文は逆に Node 22 で
|
|
19
|
+
// 除去済み=どちらの静的構文も 18〜22 全域を満たせないため(実行時 require が唯一全域で動く)。
|
|
20
|
+
const pkg = createRequire(import.meta.url)("../package.json");
|
|
21
|
+
const server = new McpServer({ name: "aiterm", version: pkg.version });
|
|
16
22
|
function ok(s) {
|
|
17
23
|
return { content: [{ type: "text", text: s }] };
|
|
18
24
|
}
|
|
@@ -38,18 +44,42 @@ server.registerTool("pty_open", {
|
|
|
38
44
|
}
|
|
39
45
|
});
|
|
40
46
|
server.registerTool("pty_send", {
|
|
41
|
-
description: "セッションへテキスト(コマンド)
|
|
47
|
+
description: "セッションへテキスト(コマンド)を送る。通常は送信だけ行い、出力は pty_read で取得する。" +
|
|
48
|
+
"agent_done:true で起動した Codex/Grok/Composer セッションは wait:'agent_done' で Stop hook まで待てる。",
|
|
42
49
|
inputSchema: {
|
|
43
50
|
session_id: z.string(),
|
|
44
51
|
text: z.string().describe("送る文字列(コマンド)"),
|
|
45
52
|
enter: z.boolean().default(true).describe("末尾で Enter を送る"),
|
|
46
|
-
|
|
53
|
+
wait: z
|
|
54
|
+
.enum(["none", "agent_done"])
|
|
55
|
+
.default("none")
|
|
56
|
+
.describe("none=従来通り送信のみ。agent_done=agent Stop hook まで待って最終画面を返す"),
|
|
57
|
+
timeout: z.number().default(600).describe("wait:'agent_done' の最大待ち秒数"),
|
|
58
|
+
screen: z.boolean().default(true).describe("wait:'agent_done' の返り値を描画済みスクリーンにする"),
|
|
59
|
+
lines: z.number().int().nullish().describe("wait:'agent_done' で返す末尾 N 行"),
|
|
60
|
+
mark: z
|
|
61
|
+
.boolean()
|
|
62
|
+
.default(false)
|
|
63
|
+
.describe("完了 sentinel(終了コード付き)で包む。pty_read(wait:true) が until 無しでも自動検出して" +
|
|
64
|
+
"完了確定する(ネスト中や非シェル前面でも効く確実な完了検出。手で until を組む必要なし)"),
|
|
47
65
|
force: z.boolean().default(false).describe("破壊的コマンドゲートを越える"),
|
|
48
66
|
rtk: z.boolean().default(false).describe("既知コマンドを rtk 形へ委譲して送る(rtk 不在なら素通し)"),
|
|
49
67
|
raw: z.boolean().default(false).describe("送信前サニタイズを無効化"),
|
|
50
68
|
},
|
|
51
|
-
}, async ({ session_id, text, enter, mark, force, rtk, raw }) => {
|
|
69
|
+
}, async ({ session_id, text, enter, wait, timeout, screen, lines, mark, force, rtk, raw }) => {
|
|
52
70
|
try {
|
|
71
|
+
if (wait === "agent_done") {
|
|
72
|
+
return ok(await core.sendAndWaitAgentDone(session_id, text, {
|
|
73
|
+
enter,
|
|
74
|
+
mark,
|
|
75
|
+
force,
|
|
76
|
+
rtk,
|
|
77
|
+
raw,
|
|
78
|
+
timeout,
|
|
79
|
+
screen,
|
|
80
|
+
lines: lines ?? null,
|
|
81
|
+
}));
|
|
82
|
+
}
|
|
53
83
|
return ok(core.send(session_id, text, { enter, mark, force, rtk, raw }));
|
|
54
84
|
}
|
|
55
85
|
catch (e) {
|
|
@@ -61,8 +91,18 @@ server.registerTool("pty_read", {
|
|
|
61
91
|
"削減: 制御文字除去 / 反復圧縮 / head+tail 折りたたみ+復元ヒント+メタ併記。",
|
|
62
92
|
inputSchema: {
|
|
63
93
|
session_id: z.string(),
|
|
64
|
-
wait: z
|
|
65
|
-
|
|
94
|
+
wait: z
|
|
95
|
+
.boolean()
|
|
96
|
+
.default(false)
|
|
97
|
+
.describe("完了まで待つ(dead / mark sentinel 自動検出 / until / 出力静止∧シェル復帰 / timeout)"),
|
|
98
|
+
until: z
|
|
99
|
+
.string()
|
|
100
|
+
.nullish()
|
|
101
|
+
.describe("この文字列が出たら完了とみなす(既定はリテラル部分一致。`$ ` や `[..]` もそのまま探せる)"),
|
|
102
|
+
until_regex: z
|
|
103
|
+
.boolean()
|
|
104
|
+
.default(false)
|
|
105
|
+
.describe("until を正規表現として扱う(既定 false=リテラル部分一致。メタ文字を使いたい時のみ true)"),
|
|
66
106
|
timeout: z.number().default(10).describe("wait の最大待ち秒数"),
|
|
67
107
|
screen: z.boolean().default(false).describe("描画済みスクリーン(TUI 向け)"),
|
|
68
108
|
full: z.boolean().default(false).describe("増分でなく全文"),
|
|
@@ -71,20 +111,23 @@ server.registerTool("pty_read", {
|
|
|
71
111
|
raw: z.boolean().default(false).describe("削減せず生テキスト"),
|
|
72
112
|
rtk: z.boolean().default(false).describe("直前コマンド別の自前 reducer(git/grep/pytest 等)で縮約"),
|
|
73
113
|
},
|
|
74
|
-
}, async ({ session_id, wait, until, timeout, screen, full, lines, line_range, raw, rtk }) => {
|
|
114
|
+
}, async ({ session_id, wait, until, until_regex, timeout, screen, full, lines, line_range, raw, rtk }) => {
|
|
75
115
|
try {
|
|
76
116
|
let range = null;
|
|
77
117
|
if (line_range) {
|
|
78
118
|
const idx = line_range.indexOf(":");
|
|
79
119
|
const lo = idx < 0 ? line_range : line_range.slice(0, idx);
|
|
80
120
|
const hi = idx < 0 ? "" : line_range.slice(idx + 1);
|
|
81
|
-
//
|
|
121
|
+
// 不正/空/負の上端は「末尾まで」(null) に倒す。"5:abc" を空に潰さず "5:" と同じく 5 行目以降に。
|
|
122
|
+
// 下端は負値を 0 にクランプする("-3:5" が負 slice にならないように・C12)。
|
|
123
|
+
const loN = Math.max(0, parseInt(lo, 10) || 0);
|
|
82
124
|
const hiN = hi ? parseInt(hi, 10) : NaN;
|
|
83
|
-
range = [
|
|
125
|
+
range = [loN, Number.isNaN(hiN) || hiN < 0 ? null : hiN];
|
|
84
126
|
}
|
|
85
127
|
const out = await core.readOutput(session_id, {
|
|
86
128
|
wait,
|
|
87
129
|
until: until ?? null,
|
|
130
|
+
untilRegex: until_regex,
|
|
88
131
|
timeout,
|
|
89
132
|
screen,
|
|
90
133
|
full,
|
|
@@ -152,14 +195,19 @@ function registerAgentTool(toolName, kind, desc, grokLike) {
|
|
|
152
195
|
.describe(agentEffortDesc(grokLike)),
|
|
153
196
|
cwd: z.string().nullish().describe("作業ディレクトリ(対象リポのルート等・任意)"),
|
|
154
197
|
session_name: z.string().nullish().describe("セッション名(省略で自動採番)"),
|
|
198
|
+
agent_done: z
|
|
199
|
+
.boolean()
|
|
200
|
+
.default(false)
|
|
201
|
+
.describe("managed Stop hook を有効化し、pty_send(wait:'agent_done') を使えるようにする"),
|
|
155
202
|
},
|
|
156
|
-
}, async ({ prompt, reasoning_effort, cwd, session_name }) => {
|
|
203
|
+
}, async ({ prompt, reasoning_effort, cwd, session_name, agent_done }) => {
|
|
157
204
|
try {
|
|
158
205
|
const [sid, hint] = core.openAgent(kind, {
|
|
159
206
|
prompt: prompt ?? undefined,
|
|
160
207
|
reasoning_effort: reasoning_effort ?? undefined,
|
|
161
208
|
cwd: cwd ?? undefined,
|
|
162
209
|
session_name: session_name ?? undefined,
|
|
210
|
+
agent_done: agent_done ?? false,
|
|
163
211
|
});
|
|
164
212
|
return ok(`session_id: ${sid}\n${hint}`);
|
|
165
213
|
}
|
package/dist/rtk.js
CHANGED
|
@@ -28,16 +28,23 @@ function truncate(s, n) {
|
|
|
28
28
|
return "...";
|
|
29
29
|
return cp.slice(0, n - 3).join("") + "...";
|
|
30
30
|
}
|
|
31
|
-
|
|
31
|
+
// プロンプト行の判定(C3): 行全体が「プロンプト前置文字(語/@ : ~ / \ [] . -)+プロンプト記号($#%>)+末尾空白」
|
|
32
|
+
// の形のときだけプロンプトとみなす。末尾記号だけを見ると `</div>` `>>>` `echo x >` 等の本文行を誤除去する。
|
|
33
|
+
const PROMPT_TAIL = /^[\w@.:~\/\\\[\]-]*[$#%>]\s*$/;
|
|
32
34
|
/** 観測ログ片からエコー行と前後のプロンプト行を落とし、コマンド出力本体だけ残す(発見的)。 */
|
|
33
35
|
export function stripShellFrame(text, command) {
|
|
34
36
|
const lines = text.split("\n");
|
|
35
37
|
const cmd = command.trim();
|
|
36
38
|
let start = 0;
|
|
37
39
|
if (cmd) {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
+
// 最初の一致(=コマンドエコー行)だけを落とす。最後の一致まで進めると、出力本体に cmd 文字列が
|
|
41
|
+
// 再出現(cat したファイル内・commit メッセージ内 等)した場合に本体を丸ごと捨ててしまう(C2)。
|
|
42
|
+
for (let i = 0; i < lines.length; i++) {
|
|
43
|
+
if (lines[i].includes(cmd)) {
|
|
40
44
|
start = i + 1;
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
41
48
|
}
|
|
42
49
|
let end = lines.length;
|
|
43
50
|
while (end > start) {
|
|
@@ -77,7 +84,10 @@ export function reducePytest(output) {
|
|
|
77
84
|
flush();
|
|
78
85
|
continue;
|
|
79
86
|
}
|
|
80
|
-
if (t.startsWith("===") &&
|
|
87
|
+
if (t.startsWith("===") &&
|
|
88
|
+
(t.includes("passed") || t.includes("failed") || t.includes("skipped") || t.includes("error"))) {
|
|
89
|
+
// 収集エラーのみ(`=== 1 error in Xs ===`)も要約行として拾う。拾わないと全ゼロ扱いで
|
|
90
|
+
// "No tests collected"(無害誤読)に潰れる。"ERRORS" セクション見出しは大文字ゆえ非該当。
|
|
81
91
|
summaryLine = t;
|
|
82
92
|
continue;
|
|
83
93
|
}
|
|
@@ -85,7 +95,7 @@ export function reducePytest(output) {
|
|
|
85
95
|
!t.startsWith("===") &&
|
|
86
96
|
!t.startsWith("FAILED") &&
|
|
87
97
|
!t.startsWith("ERROR") &&
|
|
88
|
-
(t.includes(" passed") || t.includes(" failed") || t.includes(" skipped")) &&
|
|
98
|
+
(t.includes(" passed") || t.includes(" failed") || t.includes(" skipped") || t.includes(" error")) &&
|
|
89
99
|
t.includes(" in ")) {
|
|
90
100
|
summaryLine = t;
|
|
91
101
|
continue;
|
|
@@ -114,13 +124,16 @@ export function reducePytest(output) {
|
|
|
114
124
|
}
|
|
115
125
|
}
|
|
116
126
|
flush();
|
|
117
|
-
const [p, f, s, xf, xp] = parsePytestCounts(summaryLine);
|
|
118
|
-
if (p === 0 && f === 0 && s === 0 && xf === 0 && xp === 0)
|
|
127
|
+
const [p, f, s, xf, xp, e] = parsePytestCounts(summaryLine);
|
|
128
|
+
if (p === 0 && f === 0 && s === 0 && xf === 0 && xp === 0 && e === 0)
|
|
119
129
|
return "Pytest: No tests collected";
|
|
120
|
-
|
|
130
|
+
// error(収集/内部エラー)は失敗の一種=緑扱いにしない。extras に含め、"N passed" 早期 return を止める。
|
|
131
|
+
const extras = s > 0 || xf > 0 || xp > 0 || e > 0 || xfailLines.length > 0;
|
|
121
132
|
if (f === 0 && p > 0 && !extras)
|
|
122
133
|
return `Pytest: ${p} passed`;
|
|
123
134
|
let head = `Pytest: ${p} passed, ${f} failed`;
|
|
135
|
+
if (e > 0)
|
|
136
|
+
head += `, ${e} error${e === 1 ? "" : "s"}`;
|
|
124
137
|
if (s > 0)
|
|
125
138
|
head += `, ${s} skipped`;
|
|
126
139
|
if (xf > 0)
|
|
@@ -183,7 +196,7 @@ export function reducePytest(output) {
|
|
|
183
196
|
return out.join("\n").trim();
|
|
184
197
|
}
|
|
185
198
|
function parsePytestCounts(summary) {
|
|
186
|
-
let p = 0, f = 0, s = 0, xf = 0, xp = 0;
|
|
199
|
+
let p = 0, f = 0, s = 0, xf = 0, xp = 0, e = 0;
|
|
187
200
|
for (const part of summary.split(",")) {
|
|
188
201
|
const words = part.split(/\s+/).filter(Boolean);
|
|
189
202
|
for (let i = 0; i < words.length; i++) {
|
|
@@ -193,6 +206,7 @@ function parsePytestCounts(summary) {
|
|
|
193
206
|
if (Number.isNaN(n))
|
|
194
207
|
continue;
|
|
195
208
|
const w = words[i];
|
|
209
|
+
// "error"/"errors" は passed/failed/skipped/xfailed/xpassed のいずれの部分文字列でもないので順不同で安全。
|
|
196
210
|
if (w.includes("xpassed"))
|
|
197
211
|
xp = n;
|
|
198
212
|
else if (w.includes("xfailed"))
|
|
@@ -203,9 +217,11 @@ function parsePytestCounts(summary) {
|
|
|
203
217
|
f = n;
|
|
204
218
|
else if (w.includes("skipped"))
|
|
205
219
|
s = n;
|
|
220
|
+
else if (w.includes("error"))
|
|
221
|
+
e = n;
|
|
206
222
|
}
|
|
207
223
|
}
|
|
208
|
-
return [p, f, s, xf, xp];
|
|
224
|
+
return [p, f, s, xf, xp, e];
|
|
209
225
|
}
|
|
210
226
|
// ---------------------------------------------------------------- grep
|
|
211
227
|
const GREP_LINE = /^(.*?):(\d+):(.*)$/;
|
|
@@ -310,7 +326,8 @@ export function reduceGitLog(output) {
|
|
|
310
326
|
else if (t && !subject && (ln.startsWith(" ") || ln.startsWith("\t")))
|
|
311
327
|
subject = t;
|
|
312
328
|
else if (t && (ln.startsWith(" ") || ln.startsWith("\t"))) {
|
|
313
|
-
|
|
329
|
+
// trailer は大小文字が実装依存(本リポ規約は Co-Authored-By:)。大小無視で除外する(C5)。
|
|
330
|
+
if (!/^(signed-off-by|co-authored-by):/i.test(t))
|
|
314
331
|
body.push(t);
|
|
315
332
|
}
|
|
316
333
|
}
|
|
@@ -355,13 +372,25 @@ function applyFilter(rule, output) {
|
|
|
355
372
|
return body;
|
|
356
373
|
}
|
|
357
374
|
// ---------------------------------------------------------------- ルーティング
|
|
375
|
+
const WRAPPERS = new Set(["sudo", "env", "command", "exec"]);
|
|
376
|
+
const RUNNERS = new Set(["uv", "poetry", "pdm", "rye", "hatch", "pipenv"]);
|
|
377
|
+
// [verb, sub, stripped]。stripped は前置(ラッパー/環境代入/フラグ/ランナー run)を除いた実コマンド。
|
|
378
|
+
// 誤分類は classify で null→generic に落ちるだけなので、読み飛ばしは積極的で安全(C4)。
|
|
358
379
|
function basenameCmd(command) {
|
|
359
380
|
const toks = command.trim().split(/\s+/).filter(Boolean);
|
|
360
381
|
let i = 0;
|
|
361
|
-
|
|
382
|
+
// 前置ラッパー(sudo/env/…)・環境代入(FOO=1)・そのフラグ(-E/-i 等)を読み飛ばす
|
|
383
|
+
while (i < toks.length && (toks[i].includes("=") || WRAPPERS.has(toks[i]) || toks[i].startsWith("-")))
|
|
362
384
|
i++;
|
|
385
|
+
// ランナー( uv/poetry run <cmd> )は run と共に読み飛ばして実コマンドへ
|
|
386
|
+
if (i + 1 < toks.length && RUNNERS.has(toks[i]) && toks[i + 1] === "run") {
|
|
387
|
+
i += 2;
|
|
388
|
+
while (i < toks.length && (toks[i].includes("=") || toks[i].startsWith("-")))
|
|
389
|
+
i++;
|
|
390
|
+
}
|
|
391
|
+
const stripped = toks.slice(i).join(" ");
|
|
363
392
|
if (i >= toks.length)
|
|
364
|
-
return ["", ""];
|
|
393
|
+
return ["", "", ""];
|
|
365
394
|
const verb = toks[i].split("/").pop();
|
|
366
395
|
let sub = "";
|
|
367
396
|
for (const t of toks.slice(i + 1)) {
|
|
@@ -370,10 +399,10 @@ function basenameCmd(command) {
|
|
|
370
399
|
break;
|
|
371
400
|
}
|
|
372
401
|
}
|
|
373
|
-
return [verb, sub];
|
|
402
|
+
return [verb, sub, stripped];
|
|
374
403
|
}
|
|
375
404
|
export function classify(command) {
|
|
376
|
-
const [verb, sub] = basenameCmd(command);
|
|
405
|
+
const [verb, sub, stripped] = basenameCmd(command);
|
|
377
406
|
if (verb === "git") {
|
|
378
407
|
if (sub === "status")
|
|
379
408
|
return "git-status";
|
|
@@ -385,10 +414,12 @@ export function classify(command) {
|
|
|
385
414
|
return "grep";
|
|
386
415
|
if (verb === "pytest" || verb === "py.test")
|
|
387
416
|
return "pytest";
|
|
388
|
-
|
|
417
|
+
// python3 / python3.11 等のバージョン付きも許容(C4: `python3 -m pytest` を拾う)
|
|
418
|
+
if (/^python[0-9.]*$/.test(verb) && command.includes("pytest"))
|
|
389
419
|
return "pytest";
|
|
420
|
+
// FILTERS は basename 経由の stripped で判定(C6: `sudo df` `sudo make` も分類できる)
|
|
390
421
|
for (const rule of FILTERS)
|
|
391
|
-
if (rule.match.test(
|
|
422
|
+
if (rule.match.test(stripped))
|
|
392
423
|
return rule.name;
|
|
393
424
|
return null;
|
|
394
425
|
}
|
|
@@ -398,7 +429,11 @@ const REDUCERS = {
|
|
|
398
429
|
grep: reduceGrep,
|
|
399
430
|
pytest: (o) => reducePytest(o),
|
|
400
431
|
};
|
|
401
|
-
/**
|
|
432
|
+
/**
|
|
433
|
+
* コマンドに応じた reducer を観測出力へ適用。返り値 [reducedText|null, reducerName|null]。
|
|
434
|
+
* 前提(C13): `output` は制御文字除去済み(ANSI/CR 等)であること。色付き/生 PTY 出力を直接渡すと
|
|
435
|
+
* 誤パースし得る。呼び手 core.readOutput は `stripShellFrame(stripControl(text), cmd)` で前処理してから渡す。
|
|
436
|
+
*/
|
|
402
437
|
export function reduce(command, output) {
|
|
403
438
|
const name = classify(command);
|
|
404
439
|
if (name === null)
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aiterm-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"mcpName": "io.github.kitepon-rgb/aiterm-mcp",
|
|
5
|
-
"description": "AI-driven persistent terminal as a local stdio MCP server (tmux-backed). Holds one local PTY; SSH and containers are just commands you send into it. Token-reducing reads.",
|
|
5
|
+
"description": "AI-driven persistent terminal as a local stdio MCP server (tmux-backed). Holds one local PTY; SSH and containers are just commands you send into it. Also launches interactive Codex/Grok/Composer agent TUIs in a persistent terminal. Token-reducing reads.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"mcp",
|
|
8
8
|
"mcp-server",
|
|
@@ -15,6 +15,9 @@
|
|
|
15
15
|
"claude-code",
|
|
16
16
|
"cursor",
|
|
17
17
|
"ai-agent",
|
|
18
|
+
"agent",
|
|
19
|
+
"codex",
|
|
20
|
+
"grok",
|
|
18
21
|
"devtools"
|
|
19
22
|
],
|
|
20
23
|
"license": "MIT",
|