aiterm-mcp 0.41.3 → 0.42.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/CHANGELOG.md +12 -1
- package/README.ja.md +1 -1
- package/README.md +1 -1
- package/dist/core.js +18 -1
- package/dist/index.js +17 -2
- package/dist/interim-words.js +256 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.42.0] - 2026-09-28
|
|
11
|
+
|
|
12
|
+
### 追加
|
|
13
|
+
|
|
14
|
+
- BellTeamとの連携用に、`pty_observe`がBellTeamからの要求にだけ、実行中のturnで書かれた作業の合間の言葉を返せるようにした。ほかの呼び手に対するツールの説明、引数、結果は変わらない。判断はrepositoryの`docs/adr/0071-bellteam-interim-words.md`。
|
|
15
|
+
|
|
16
|
+
### 修正
|
|
17
|
+
|
|
18
|
+
- Cursorは記録にturnの終了を書いたあとも、しばらく画面に実行中の表示を残す。完了通知の直後に`pty_send`すると差し込みと判定し、`STEER_NOT_QUEUED`で失敗していた(手元で3回中2回)。記録の末尾がturnの終了なら新しいturnとして送る。
|
|
19
|
+
|
|
10
20
|
## [0.41.3] - 2026-09-28
|
|
11
21
|
|
|
12
22
|
### 修正
|
|
@@ -1828,7 +1838,8 @@ prototype (preserved under `prototype/python/` as the porting source and referen
|
|
|
1828
1838
|
`ubuntu-latest` for Node 18/20/22, publishing to npm on `v*` tags with
|
|
1829
1839
|
provenance.
|
|
1830
1840
|
|
|
1831
|
-
[Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.
|
|
1841
|
+
[Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.42.0...HEAD
|
|
1842
|
+
[0.42.0]: https://github.com/kitepon/aiterm-mcp/compare/v0.41.3...v0.42.0
|
|
1832
1843
|
[0.41.3]: https://github.com/kitepon/aiterm-mcp/compare/v0.41.2...v0.41.3
|
|
1833
1844
|
[0.41.2]: https://github.com/kitepon/aiterm-mcp/compare/v0.41.1...v0.41.2
|
|
1834
1845
|
[0.41.1]: https://github.com/kitepon/aiterm-mcp/compare/v0.41.0...v0.41.1
|
package/README.ja.md
CHANGED
|
@@ -203,7 +203,7 @@ runtime-error store は canonical dotagents config の `collection.enabled: true
|
|
|
203
203
|
場合だけ収集し、既定OFF、network送信は行いません。tag起点CIのnpm provenance(OIDC Trusted
|
|
204
204
|
Publishing)で公開し、GitHub Release が Official MCP Registry を再登録します。
|
|
205
205
|
|
|
206
|
-
**状態:** 開発継続中 · 現行公開版 **v0.
|
|
206
|
+
**状態:** 開発継続中 · 現行公開版 **v0.42.0** · 動作対象は Linux · WSL2 · macOS · Windows ネイティブ · MIT · [変更履歴](CHANGELOG.md)。
|
|
207
207
|
|
|
208
208
|
### 更新と巻き戻し
|
|
209
209
|
|
package/README.md
CHANGED
|
@@ -217,7 +217,7 @@ collection is off by default and performs no network I/O. It ships via
|
|
|
217
217
|
tag-triggered CI with npm provenance (OIDC Trusted Publishing); the GitHub
|
|
218
218
|
Release re-registers the Official MCP Registry entry.
|
|
219
219
|
|
|
220
|
-
**Status:** actively maintained · current public release **v0.
|
|
220
|
+
**Status:** actively maintained · current public release **v0.42.0** · runs on Linux · WSL2 · macOS · native Windows (tmux on POSIX, the tmux-CLI-compatible [psmux](https://github.com/psmux/psmux) on native Windows — no WSL required) · MIT · see the [CHANGELOG](CHANGELOG.md).
|
|
221
221
|
|
|
222
222
|
### Update and rollback
|
|
223
223
|
|
package/dist/core.js
CHANGED
|
@@ -22,6 +22,7 @@ import { realGrokHome, resolveAndValidateGrokAuth, assertGrokModelAvailable, gro
|
|
|
22
22
|
import { bindCodexTranscriptSession, latestCodexCompletion, observeCodexDone, buildCodexAgentCmd, codexLaunchNote, codexTuiReady, codexPaneObservation, codexRateLimitModelSwitchDialog, codexApprovalDialog, codexStartupAction, CODEX_COMPOSER_MARKER_RE, codexModelChoice, codexEffortChoice, codexMoreReasoningChoice, codexTranscriptText, createCodexAgentMetadata, } from "./harnesses/codex.js";
|
|
23
23
|
import { OPERATION_ID_RE, CLAUDE_RESULT_MAX_BYTES, CLAUDE_EFFORTS, agentManagedClaudeSettingsPath, agentClaudeResultPath, agentClaudeOperationPath, agentClaudeApprovalReceiptPath, agentClaudeDispatchReceiptPath, validateOperationId, readClaudeResultText, assertClaudeAuthenticationReady, buildClaudeAgentCmd, claudeLaunchNote, claudeTuiReady, claudePaneObservation, claudeStartupAction, claudeLoginMethodMenu, CLAUDE_COMPOSER_MARKER_RE, createClaudeAgentMetadata, claudeSessionTranscriptPath, claudeApiErrorFromLine, claudeApiErrorAfter, } from "./harnesses/claude.js";
|
|
24
24
|
import { bindCursorTranscriptSession, cursorTurnBoundary, latestCursorCompletion, observeCursorDone, cursorTranscriptText, assertCursorAuthenticationReady, assertCursorModelAvailable, buildCursorAgentCmd, cursorAgentArgv, cursorPwshLaunchLine, cursorPromptWithLineage, createCursorAgentMetadata, cursorLaunchNote, cursorEffortNavigation, cursorTuiReady, cursorPaneObservation, cursorPromptHooksRunning, cursorUsageLimit, CURSOR_SUBMIT_SEQUENCE, CURSOR_COMPOSER_CONTENT_MARKER_RE, validateCursorModelEffort, } from "./harnesses/cursor.js";
|
|
25
|
+
import { readInterimWords, recordInterimBoundary } from "./interim-words.js";
|
|
25
26
|
import { resolveAgentBin, resolveThroughlineBin, runThroughlineHandoffContext, isWindowsNativeExecutable, agentBinForPaneShell, resolveWinPaneShell } from "./agent-resolver.js";
|
|
26
27
|
export { AitermError } from "./errors.js";
|
|
27
28
|
export { tmuxSpawnEnv } from "./tmux-runtime.js";
|
|
@@ -306,7 +307,8 @@ function cleanupAgentState(name) {
|
|
|
306
307
|
f.endsWith(".cursor-result.json") ||
|
|
307
308
|
f.endsWith(".claude-operation.json") ||
|
|
308
309
|
f.endsWith(".claude-approval.json") ||
|
|
309
|
-
f.endsWith(".claude-dispatch")
|
|
310
|
+
f.endsWith(".claude-dispatch") ||
|
|
311
|
+
f.endsWith(".interim.json"))
|
|
310
312
|
fs.unlinkSync(p);
|
|
311
313
|
else if (f.endsWith(".codex-home") || f.endsWith(".grok-home") || f.endsWith(".cursor-plugin") || f.endsWith(".home")) {
|
|
312
314
|
fs.rmSync(p, { recursive: true, force: true });
|
|
@@ -1333,6 +1335,7 @@ export function killAll() {
|
|
|
1333
1335
|
f.endsWith(".claude-result.json") ||
|
|
1334
1336
|
f.endsWith(".claude-operation.json") ||
|
|
1335
1337
|
f.endsWith(".claude-dispatch") ||
|
|
1338
|
+
f.endsWith(".interim.json") ||
|
|
1336
1339
|
f.endsWith(".codex-home") ||
|
|
1337
1340
|
f.endsWith(".grok-home") ||
|
|
1338
1341
|
f.endsWith(".home")) {
|
|
@@ -2406,6 +2409,14 @@ async function settlePublishedClaudeCompletionMarker(meta, marker) {
|
|
|
2406
2409
|
}
|
|
2407
2410
|
return active;
|
|
2408
2411
|
}
|
|
2412
|
+
/** BellTeamのため: agent sessionの現在(または直前)のturnの合間の言葉。agent sessionでなければnull。 */
|
|
2413
|
+
export function readSessionInterimWords(name, after) {
|
|
2414
|
+
assertSessionName(name);
|
|
2415
|
+
const dir = existingAgentsDir();
|
|
2416
|
+
if (!dir || !fs.readdirSync(dir).some((f) => f.startsWith(`${name}.`) && f.endsWith(".agent.json")))
|
|
2417
|
+
return null;
|
|
2418
|
+
return readInterimWords(loadAgentMetadata(name), after);
|
|
2419
|
+
}
|
|
2409
2420
|
/** agent harness の構造化 transcript から直近完了ターンの最終回答と相関情報を読む。 */
|
|
2410
2421
|
export async function readAgentTranscriptResult(name, o = {}) {
|
|
2411
2422
|
const meta = loadAgentMetadata(name);
|
|
@@ -3626,6 +3637,7 @@ export async function dispatchAgentTurn(name, text, o = {}) {
|
|
|
3626
3637
|
prepareSendText(dispatchText, { raw: o.raw });
|
|
3627
3638
|
await o.before_send?.({ session_id: name, launch_id: meta.launch_id, vendor: meta.kind,
|
|
3628
3639
|
harness: agentHarness(meta.kind), event_cursor: startOffset, operation_id: operationId });
|
|
3640
|
+
recordInterimBoundary(meta, startOffset, operationId);
|
|
3629
3641
|
if (meta.kind === "claude") {
|
|
3630
3642
|
// durable/anonymousを分岐する前に同じsend preflightを通す。拒否されるpromptの
|
|
3631
3643
|
// receipt/active markerだけを残して、来ないStopを待つ状態を作らない。
|
|
@@ -3716,6 +3728,11 @@ export async function sendAgentMessage(name, text, o = {}) {
|
|
|
3716
3728
|
clearClaudeApiErrorOperation(meta);
|
|
3717
3729
|
running = readClaudeOperationMarker(meta) !== null;
|
|
3718
3730
|
}
|
|
3731
|
+
// Cursorはturn_endedを記録へ書いたあとも、しばらく画面にbusy表示を残す。その間に差し込むと待ち行列へ入らず
|
|
3732
|
+
// STEER_NOT_QUEUEDで失敗していた(実測 2026-09-28、完了通知の直後の送信で3回中2回)。記録の末尾がturn_endedなら
|
|
3733
|
+
// turnは終わっているので新しいturnとして送る。dispatchは入力を受け付けるまで待ってから送る。
|
|
3734
|
+
if (running && meta.kind === "cursor" && latestCursorCompletion(meta, readTranscriptLines) !== null)
|
|
3735
|
+
running = false;
|
|
3719
3736
|
if (running) {
|
|
3720
3737
|
return steerRunningTurn(name, meta, text, { raw: o.raw, pane_input_recovery: paneInputRecovery });
|
|
3721
3738
|
}
|
package/dist/index.js
CHANGED
|
@@ -20,6 +20,7 @@ import { acceptRemote, callRemoteTool, observeRemoteAgentDone, remoteInputDescri
|
|
|
20
20
|
import { codexParentFromRequest } from "./codex-parent-receiver.js";
|
|
21
21
|
import { claudeParentFromRequest } from "./claude-parent-receiver.js";
|
|
22
22
|
import { cursorParentFromRequest, isCursorMcpClient } from "./cursor-parent-receiver.js";
|
|
23
|
+
import { INTERIM_RESULT_META_KEY, interimRequestFromMeta } from "./interim-words.js";
|
|
23
24
|
// package.json の version を実行時に読み、MCP initialize で配るサーバ版と一致させる。
|
|
24
25
|
// createRequire を使うのは、import 属性 `with { type: "json" }` が Node 18.20+ 限定で
|
|
25
26
|
// engines "node >=18"(18.0〜18.19)を SyntaxError で壊し、旧 `assert` 構文は逆に Node 22 で
|
|
@@ -587,16 +588,30 @@ registerRemoteAwareTool("pty_observe", {
|
|
|
587
588
|
cpu_seconds: z.number().nullable(), cpu_delta_seconds: z.number().nullable(), cpu_delta_complete: z.boolean().nullable(),
|
|
588
589
|
background_cpu_seconds: z.number().nullable(), background_cpu_delta_seconds: z.number().nullable(), background_cpu_delta_complete: z.boolean().nullable() }),
|
|
589
590
|
},
|
|
590
|
-
}, async ({ session_id, cursor }) => {
|
|
591
|
+
}, async ({ session_id, cursor }, extra) => {
|
|
591
592
|
try {
|
|
592
593
|
const observation = core.observeSession(session_id, cursor);
|
|
593
594
|
const result = { ...observation, ...(parentDelivery ? { parent_deliveries: parentDelivery.status(session_id) } : {}) };
|
|
594
|
-
return { ...ok(JSON.stringify(result)), structuredContent: { ...result } };
|
|
595
|
+
return { ...ok(JSON.stringify(result)), structuredContent: { ...result }, ...interimWordsMeta(session_id, extra._meta) };
|
|
595
596
|
}
|
|
596
597
|
catch (e) {
|
|
597
598
|
return fail(e);
|
|
598
599
|
}
|
|
599
600
|
});
|
|
601
|
+
// BellTeamのため: 要求の_metaに aiterm/caller="BellTeam" がある時だけ、結果の_metaへ合間の言葉を載せる。
|
|
602
|
+
// 説明と引数には出さない(interim-words.ts の冒頭)。それ以外の呼び手には何も足さない。
|
|
603
|
+
function interimWordsMeta(sessionId, requestMeta) {
|
|
604
|
+
const request = interimRequestFromMeta(requestMeta);
|
|
605
|
+
if (!request)
|
|
606
|
+
return {};
|
|
607
|
+
try {
|
|
608
|
+
const words = core.readSessionInterimWords(sessionId, request.after);
|
|
609
|
+
return words ? { _meta: { [INTERIM_RESULT_META_KEY]: words } } : {};
|
|
610
|
+
}
|
|
611
|
+
catch (e) {
|
|
612
|
+
return { _meta: { [INTERIM_RESULT_META_KEY]: { schema: "aiterm.interim-words.v1", error: e instanceof Error ? e.message : String(e) } } };
|
|
613
|
+
}
|
|
614
|
+
}
|
|
600
615
|
registerRemoteAwareTool("agent_approval", {
|
|
601
616
|
description: "Codexの現在の承認をinspectし、digestへ束縛した単発許可または拒否をrespondする。恒久許可は選ばない。Claudeは既存claude_approvalを使う。未知dialogはblockedのtyped errorで返す。",
|
|
602
617
|
inputSchema: {
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
// 合間の言葉: agent sessionがturnの途中で書き、そのあと道具を使った言葉。
|
|
2
|
+
// BellTeamのためにある。BellTeamは実行中のBotが何をしているかを会話画面へ出すため、
|
|
3
|
+
// pty_observeのMCP要求の_metaに caller="BellTeam" を入れて呼び、結果の_metaでこれを受け取る。
|
|
4
|
+
// 利用者向けのツール説明と引数には出さない(BellTeam以外のAIに指定させないため)。要らないものと見て消さないこと。
|
|
5
|
+
//
|
|
6
|
+
// 最後の回答と思考は含めない。APIエラーやsession limitの知らせは、記録に書かれていれば含める。
|
|
7
|
+
// ハーネスごとの見分け方(2026-09-28 記録で確認):
|
|
8
|
+
// Claude Code: text block で message.stop_reason="tool_use"。isApiErrorMessage の行は知らせ。
|
|
9
|
+
// Codex: assistant message の phase="commentary"。
|
|
10
|
+
// Grok: content が空でなく tool_calls を持つ assistant。
|
|
11
|
+
// Cursor: assistant の text のあとに tool_use が続くもの(Cursorは道具が終わってから行を書くので遅れて届く)。
|
|
12
|
+
import * as fs from "node:fs";
|
|
13
|
+
import * as path from "node:path";
|
|
14
|
+
import { AitermError } from "./errors.js";
|
|
15
|
+
import { agentHarness, agentsDir, assertSessionName, LAUNCH_ID_RE, readFileRange, safeStatSize, writeJson0600, } from "./agent-shared.js";
|
|
16
|
+
import { claudeSessionTranscriptPath } from "./harnesses/claude.js";
|
|
17
|
+
import { codexRootTranscript } from "./harnesses/codex.js";
|
|
18
|
+
import { grokSessionDirectory } from "./harnesses/grok.js";
|
|
19
|
+
import { cursorTranscript } from "./harnesses/cursor.js";
|
|
20
|
+
/** BellTeamが要求の_metaへ入れる呼び手の印。値が "BellTeam" の時だけ合間の言葉を返す。 */
|
|
21
|
+
export const INTERIM_CALLER_META_KEY = "aiterm/caller";
|
|
22
|
+
export const INTERIM_CALLER = "BellTeam";
|
|
23
|
+
/** 前回受け取った最後のseq。これより後の言葉だけを返す。 */
|
|
24
|
+
export const INTERIM_AFTER_META_KEY = "aiterm/interim_after";
|
|
25
|
+
/** 結果の_metaで返すキー。 */
|
|
26
|
+
export const INTERIM_RESULT_META_KEY = "aiterm/interim_words";
|
|
27
|
+
export function agentInterimBoundaryPath(name, launchId) {
|
|
28
|
+
assertSessionName(name);
|
|
29
|
+
if (!LAUNCH_ID_RE.test(launchId))
|
|
30
|
+
throw new AitermError(`launch_id が不正です: ${launchId}`, 2);
|
|
31
|
+
return path.join(agentsDir(), `${name}.${launchId}.interim.json`);
|
|
32
|
+
}
|
|
33
|
+
/** 要求の_metaがBellTeamの印を持つ時だけ、前回のseqを返す。それ以外はnull(何も足さない)。 */
|
|
34
|
+
export function interimRequestFromMeta(meta) {
|
|
35
|
+
if (!meta || typeof meta !== "object")
|
|
36
|
+
return null;
|
|
37
|
+
const record = meta;
|
|
38
|
+
if (record[INTERIM_CALLER_META_KEY] !== INTERIM_CALLER)
|
|
39
|
+
return null;
|
|
40
|
+
const after = record[INTERIM_AFTER_META_KEY];
|
|
41
|
+
return { after: typeof after === "number" && Number.isSafeInteger(after) && after > 0 ? after : 0 };
|
|
42
|
+
}
|
|
43
|
+
function parse(line) {
|
|
44
|
+
if (!line.trim())
|
|
45
|
+
return null;
|
|
46
|
+
try {
|
|
47
|
+
return JSON.parse(line);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
// 書込み途中の末尾行と、byte境界で切れた先頭行は読まない。
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// ---------------------------------------------------------------- Claude Code
|
|
55
|
+
function claudeText(content) {
|
|
56
|
+
if (typeof content === "string")
|
|
57
|
+
return content;
|
|
58
|
+
if (!Array.isArray(content))
|
|
59
|
+
return "";
|
|
60
|
+
return content.map((part) => (part?.type === "text" && typeof part.text === "string" ? part.text : "")).join("");
|
|
61
|
+
}
|
|
62
|
+
export function claudeInterimWords(lines) {
|
|
63
|
+
const words = [];
|
|
64
|
+
for (const line of lines) {
|
|
65
|
+
const record = parse(line);
|
|
66
|
+
if (record?.type !== "assistant" || record.isSidechain === true)
|
|
67
|
+
continue;
|
|
68
|
+
const at = typeof record.timestamp === "string" ? record.timestamp : null;
|
|
69
|
+
if (record.isApiErrorMessage === true) {
|
|
70
|
+
const text = claudeText(record.message?.content).trim();
|
|
71
|
+
if (text)
|
|
72
|
+
words.push({ text, kind: "error", at, turn_id: null });
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (record.message?.stop_reason !== "tool_use")
|
|
76
|
+
continue;
|
|
77
|
+
const text = claudeText(record.message?.content).trim();
|
|
78
|
+
if (text)
|
|
79
|
+
words.push({ text, kind: "interim", at, turn_id: null });
|
|
80
|
+
}
|
|
81
|
+
return words;
|
|
82
|
+
}
|
|
83
|
+
// ---------------------------------------------------------------- Codex
|
|
84
|
+
export function codexInterimWords(lines) {
|
|
85
|
+
const words = [];
|
|
86
|
+
for (const line of lines) {
|
|
87
|
+
const record = parse(line);
|
|
88
|
+
const payload = record?.payload;
|
|
89
|
+
if (record?.type !== "response_item" || payload?.type !== "message" || payload?.role !== "assistant"
|
|
90
|
+
|| payload?.phase !== "commentary" || !Array.isArray(payload?.content))
|
|
91
|
+
continue;
|
|
92
|
+
const text = payload.content
|
|
93
|
+
.map((item) => (item?.type === "output_text" && typeof item.text === "string" ? item.text : ""))
|
|
94
|
+
.join("")
|
|
95
|
+
.trim();
|
|
96
|
+
if (!text)
|
|
97
|
+
continue;
|
|
98
|
+
const turnId = payload.internal_chat_message_metadata_passthrough?.turn_id;
|
|
99
|
+
words.push({
|
|
100
|
+
text,
|
|
101
|
+
kind: "interim",
|
|
102
|
+
at: typeof record.timestamp === "string" ? record.timestamp : null,
|
|
103
|
+
turn_id: typeof turnId === "string" && turnId ? turnId : null,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
return words;
|
|
107
|
+
}
|
|
108
|
+
// ---------------------------------------------------------------- Grok
|
|
109
|
+
function grokRealUser(record) {
|
|
110
|
+
return record?.type === "user" && !("synthetic_reason" in record);
|
|
111
|
+
}
|
|
112
|
+
export function grokUserCount(lines) {
|
|
113
|
+
let count = 0;
|
|
114
|
+
for (const line of lines)
|
|
115
|
+
if (grokRealUser(parse(line)))
|
|
116
|
+
count++;
|
|
117
|
+
return count;
|
|
118
|
+
}
|
|
119
|
+
/** boundary+1番目のuser発話(このturnの依頼)以後のassistantのうち、言葉と道具の呼び出しを両方持つもの。 */
|
|
120
|
+
export function grokInterimWords(lines, boundary) {
|
|
121
|
+
const words = [];
|
|
122
|
+
let users = 0;
|
|
123
|
+
for (const line of lines) {
|
|
124
|
+
const record = parse(line);
|
|
125
|
+
if (grokRealUser(record))
|
|
126
|
+
users++;
|
|
127
|
+
if (users <= boundary || record?.type !== "assistant")
|
|
128
|
+
continue;
|
|
129
|
+
if (!Array.isArray(record.tool_calls) || record.tool_calls.length === 0)
|
|
130
|
+
continue;
|
|
131
|
+
const text = typeof record.content === "string" ? record.content.trim() : "";
|
|
132
|
+
if (text)
|
|
133
|
+
words.push({ text, kind: "interim", at: null, turn_id: null });
|
|
134
|
+
}
|
|
135
|
+
return words;
|
|
136
|
+
}
|
|
137
|
+
// ---------------------------------------------------------------- Cursor
|
|
138
|
+
/**
|
|
139
|
+
* boundary+1番目のuser発話(このturnの依頼)以後で、あとにtool_useが続いたtextの塊。
|
|
140
|
+
* 次にuser発話かturn_endedが来た塊はそのturnの最後の回答なので含めない。末尾で行き先が決まっていない塊も待つ。
|
|
141
|
+
*/
|
|
142
|
+
export function cursorInterimWords(lines, boundary) {
|
|
143
|
+
const words = [];
|
|
144
|
+
let users = 0;
|
|
145
|
+
let pending = [];
|
|
146
|
+
for (const line of lines) {
|
|
147
|
+
const record = parse(line);
|
|
148
|
+
if (!record)
|
|
149
|
+
continue;
|
|
150
|
+
if (record.role === "user" || record.type === "turn_ended") {
|
|
151
|
+
if (record.role === "user")
|
|
152
|
+
users++;
|
|
153
|
+
pending = [];
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
if (record.role !== "assistant" || !Array.isArray(record.message?.content))
|
|
157
|
+
continue;
|
|
158
|
+
for (const part of record.message.content) {
|
|
159
|
+
if (part?.type === "text" && typeof part.text === "string")
|
|
160
|
+
pending.push(part.text);
|
|
161
|
+
else if (part?.type === "tool_use") {
|
|
162
|
+
const text = pending.join("\n").trim();
|
|
163
|
+
if (text && users > boundary)
|
|
164
|
+
words.push({ text, kind: "interim", at: null, turn_id: null });
|
|
165
|
+
pending = [];
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return words;
|
|
170
|
+
}
|
|
171
|
+
// ---------------------------------------------------------------- 境界と読み取り
|
|
172
|
+
function claudeTranscriptSize(meta) {
|
|
173
|
+
const file = claudeSessionTranscriptPath(meta);
|
|
174
|
+
return file ? safeStatSize(file) : 0;
|
|
175
|
+
}
|
|
176
|
+
function grokChatHistory(meta) {
|
|
177
|
+
const dir = grokSessionDirectory(meta);
|
|
178
|
+
return dir ? path.join(dir, "chat_history.jsonl") : null;
|
|
179
|
+
}
|
|
180
|
+
function readLines(file, from = 0) {
|
|
181
|
+
if (!file)
|
|
182
|
+
return [];
|
|
183
|
+
const size = safeStatSize(file);
|
|
184
|
+
if (size <= from)
|
|
185
|
+
return [];
|
|
186
|
+
return readFileRange(file, from, size).toString("utf8").split("\n");
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* dispatchの送信直前に、このturnの言葉が記録のどこから始まるかを残す。
|
|
190
|
+
* eventCursorはdispatch receiptのevent_cursor(Codex・Cursorでは境界そのもの)。
|
|
191
|
+
* 失敗してもdispatchは止めない。境界が古いままでも、呼び手はevent_cursorの不一致で気づける。
|
|
192
|
+
*/
|
|
193
|
+
export function recordInterimBoundary(meta, eventCursor, operationId) {
|
|
194
|
+
try {
|
|
195
|
+
let boundary;
|
|
196
|
+
if (meta.kind === "claude")
|
|
197
|
+
boundary = claudeTranscriptSize(meta);
|
|
198
|
+
else if (meta.kind === "grok")
|
|
199
|
+
boundary = grokUserCount(readLines(grokChatHistory(meta)));
|
|
200
|
+
else
|
|
201
|
+
boundary = eventCursor;
|
|
202
|
+
const value = {
|
|
203
|
+
schema: "aiterm.interim-boundary.v1",
|
|
204
|
+
event_cursor: eventCursor,
|
|
205
|
+
operation_id: operationId,
|
|
206
|
+
boundary,
|
|
207
|
+
dispatched_at: new Date().toISOString(),
|
|
208
|
+
};
|
|
209
|
+
writeJson0600(agentInterimBoundaryPath(meta.aiterm_session, meta.launch_id), value);
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
/* 合間の言葉は表示のためだけにある。送信を妨げない。 */
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
function readInterimBoundary(meta) {
|
|
216
|
+
let value;
|
|
217
|
+
try {
|
|
218
|
+
value = JSON.parse(fs.readFileSync(agentInterimBoundaryPath(meta.aiterm_session, meta.launch_id), "utf8"));
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
if (value?.schema !== "aiterm.interim-boundary.v1" || !Number.isSafeInteger(value.boundary) || value.boundary < 0
|
|
224
|
+
|| !Number.isSafeInteger(value.event_cursor))
|
|
225
|
+
return null;
|
|
226
|
+
return value;
|
|
227
|
+
}
|
|
228
|
+
/** 現在(または直前)のturnで書かれた合間の言葉のうち、seqがafterより後のもの。 */
|
|
229
|
+
export function readInterimWords(meta, after) {
|
|
230
|
+
const recorded = readInterimBoundary(meta);
|
|
231
|
+
// dispatchの記録が無ければ、起動時promptのturnだけを記録の先頭から読む。promptなしで起動してまだ送っていない時は無い。
|
|
232
|
+
const launchTurn = !recorded && meta.initial_prompt !== "none";
|
|
233
|
+
let found = [];
|
|
234
|
+
if (recorded || launchTurn) {
|
|
235
|
+
const boundary = recorded?.boundary ?? 0;
|
|
236
|
+
if (meta.kind === "claude")
|
|
237
|
+
found = claudeInterimWords(readLines(claudeSessionTranscriptPath(meta), boundary));
|
|
238
|
+
else if (meta.kind === "codex")
|
|
239
|
+
found = codexInterimWords(readLines(codexRootTranscript(meta), boundary));
|
|
240
|
+
else if (meta.kind === "grok")
|
|
241
|
+
found = grokInterimWords(readLines(grokChatHistory(meta)), boundary);
|
|
242
|
+
else
|
|
243
|
+
found = cursorInterimWords(readLines(cursorTranscript(meta)), boundary);
|
|
244
|
+
}
|
|
245
|
+
const words = found.map((word, index) => ({ seq: index + 1, ...word }));
|
|
246
|
+
return {
|
|
247
|
+
schema: "aiterm.interim-words.v1",
|
|
248
|
+
session_id: meta.aiterm_session,
|
|
249
|
+
launch_id: meta.launch_id,
|
|
250
|
+
harness: agentHarness(meta.kind),
|
|
251
|
+
event_cursor: recorded?.event_cursor ?? null,
|
|
252
|
+
operation_id: recorded?.operation_id ?? null,
|
|
253
|
+
last_seq: words.length,
|
|
254
|
+
words: words.filter((word) => word.seq > after),
|
|
255
|
+
};
|
|
256
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aiterm-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.42.0",
|
|
4
4
|
"mcpName": "io.github.kitepon/aiterm-mcp",
|
|
5
5
|
"description": "Persistent terminal MCP with one harness-based launcher for Claude Code, Codex CLI, Grok CLI, and Cursor Agent CLI, plus durable PTYs for SSH, containers, and REPLs.",
|
|
6
6
|
"keywords": [
|