aiterm-mcp 0.8.0 → 0.9.1

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.
@@ -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
@@ -44,11 +44,19 @@ server.registerTool("pty_open", {
44
44
  }
45
45
  });
46
46
  server.registerTool("pty_send", {
47
- description: "セッションへテキスト(コマンド)を送る。送信後の出力は pty_read で取得する。",
47
+ description: "セッションへテキスト(コマンド)を送る。通常は送信だけ行い、出力は pty_read で取得する。" +
48
+ "agent_done:true で起動した Codex/Grok/Composer セッションは wait:'agent_done' で Stop hook まで待てる。",
48
49
  inputSchema: {
49
50
  session_id: z.string(),
50
51
  text: z.string().describe("送る文字列(コマンド)"),
51
52
  enter: z.boolean().default(true).describe("末尾で Enter を送る"),
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 行"),
52
60
  mark: z
53
61
  .boolean()
54
62
  .default(false)
@@ -58,8 +66,20 @@ server.registerTool("pty_send", {
58
66
  rtk: z.boolean().default(false).describe("既知コマンドを rtk 形へ委譲して送る(rtk 不在なら素通し)"),
59
67
  raw: z.boolean().default(false).describe("送信前サニタイズを無効化"),
60
68
  },
61
- }, async ({ session_id, text, enter, mark, force, rtk, raw }) => {
69
+ }, async ({ session_id, text, enter, wait, timeout, screen, lines, mark, force, rtk, raw }) => {
62
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
+ }
63
83
  return ok(core.send(session_id, text, { enter, mark, force, rtk, raw }));
64
84
  }
65
85
  catch (e) {
@@ -175,14 +195,19 @@ function registerAgentTool(toolName, kind, desc, grokLike) {
175
195
  .describe(agentEffortDesc(grokLike)),
176
196
  cwd: z.string().nullish().describe("作業ディレクトリ(対象リポのルート等・任意)"),
177
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') を使えるようにする"),
178
202
  },
179
- }, async ({ prompt, reasoning_effort, cwd, session_name }) => {
203
+ }, async ({ prompt, reasoning_effort, cwd, session_name, agent_done }) => {
180
204
  try {
181
205
  const [sid, hint] = core.openAgent(kind, {
182
206
  prompt: prompt ?? undefined,
183
207
  reasoning_effort: reasoning_effort ?? undefined,
184
208
  cwd: cwd ?? undefined,
185
209
  session_name: session_name ?? undefined,
210
+ agent_done: agent_done ?? false,
186
211
  });
187
212
  return ok(`session_id: ${sid}\n${hint}`);
188
213
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aiterm-mcp",
3
- "version": "0.8.0",
3
+ "version": "0.9.1",
4
4
  "mcpName": "io.github.kitepon-rgb/aiterm-mcp",
5
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": [