aiterm-mcp 0.59.1 → 0.60.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 +54 -1
- package/README.ja.md +1 -1
- package/README.md +2 -2
- package/dist/agent-shared.js +13 -7
- package/dist/aiterm-wait-cli.js +17 -3
- package/dist/claude-stop-hook.js +22 -1
- package/dist/core.js +101 -18
- package/dist/harnesses/codex.js +47 -11
- package/dist/index.js +27 -7
- package/dist/interim-words.js +80 -8
- package/dist/parent-delivery.js +2 -1
- package/dist/steer-follow.js +610 -0
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -25,7 +25,8 @@ import { claudeParentFromRequest, discardClaudeHookRequest } from "./claude-pare
|
|
|
25
25
|
import { parentDeliveryDiagnostic } from "./parent-hook-diagnostic.js";
|
|
26
26
|
import { cursorParentFromRequest, isCursorMcpClient } from "./cursor-parent-receiver.js";
|
|
27
27
|
import { waitProcessCommandLine } from "./cursor-parent-receive.js";
|
|
28
|
-
import { INTERIM_RESULT_META_KEY, interimRequestFromMeta } from "./interim-words.js";
|
|
28
|
+
import { INTERIM_RESULT_META_KEY, interimCallerFromMeta, interimRequestFromMeta } from "./interim-words.js";
|
|
29
|
+
import { SEAT_TURNS_RESULT_META_KEY, STEER_RESULT_META_KEY } from "./steer-follow.js";
|
|
29
30
|
// package.json の version を実行時に読み、MCP initialize で配るサーバ版と一致させる。
|
|
30
31
|
// createRequire を使うのは、import 属性 `with { type: "json" }` が Node 18.20+ 限定で
|
|
31
32
|
// engines "node >=18"(18.0〜18.19)を SyntaxError で壊し、旧 `assert` 構文は逆に Node 22 で
|
|
@@ -368,8 +369,11 @@ registerRemoteAwareTool("pty_send", {
|
|
|
368
369
|
if (rtk)
|
|
369
370
|
throw new Error("agent session への dispatch は rtk:true と併用できません");
|
|
370
371
|
delivery = await deliveryForRequest(extra);
|
|
371
|
-
|
|
372
|
+
// BellTeamのため(ADR 0122): 差し込みになった時、その文を取った番の終わりを待つ手がかりを返す。印の無い呼び手には何も足さない。
|
|
373
|
+
const receipt = await core.sendAgentMessage(session_id, core.attachImages(text, image), { raw, before_send: delivery?.before_send, preface,
|
|
374
|
+
follow: interimCallerFromMeta(extra._meta) });
|
|
372
375
|
if (receipt.schema === "aiterm.agent-steer.v1") {
|
|
376
|
+
const follow = receipt.follow ?? null;
|
|
373
377
|
return {
|
|
374
378
|
content: [
|
|
375
379
|
{
|
|
@@ -382,14 +386,15 @@ registerRemoteAwareTool("pty_send", {
|
|
|
382
386
|
schema: "aiterm.pty-send-result.v1",
|
|
383
387
|
mode: "agent_steer",
|
|
384
388
|
session_id: receipt.session_id,
|
|
385
|
-
event_cursor: null,
|
|
386
|
-
wait_process: null,
|
|
389
|
+
event_cursor: follow ? follow.event_cursor : null,
|
|
390
|
+
wait_process: follow ? core.agentWaitProcess(receipt.session_id, follow.event_cursor, {}, follow.steer_id) : null,
|
|
387
391
|
launch_id: receipt.launch_id,
|
|
388
392
|
vendor: receipt.vendor,
|
|
389
393
|
harness: receipt.harness,
|
|
390
394
|
submit_residue: null,
|
|
391
395
|
pane_input_recovery: receipt.pane_input_recovery,
|
|
392
396
|
},
|
|
397
|
+
...(follow ? { _meta: { [STEER_RESULT_META_KEY]: { schema: "aiterm.steer-ref.v1", steer_id: follow.steer_id, interim_from: follow.interim_from } } } : {}),
|
|
393
398
|
};
|
|
394
399
|
}
|
|
395
400
|
const waited = completionWait(delivery, receipt.session_id, receipt.event_cursor);
|
|
@@ -624,15 +629,27 @@ registerRemoteAwareTool("pty_list", {
|
|
|
624
629
|
environment: z.record(z.string(), z.string().nullable()),
|
|
625
630
|
})),
|
|
626
631
|
},
|
|
627
|
-
}, async ({ env_keys }) => {
|
|
632
|
+
}, async ({ env_keys }, extra) => {
|
|
628
633
|
try {
|
|
629
634
|
const result = core.listSessionsResult(env_keys);
|
|
630
|
-
return { ...ok(core.listSessions(result)), structuredContent: { ...result } };
|
|
635
|
+
return { ...ok(core.listSessions(result)), structuredContent: { ...result }, ...seatTurnsMeta(result.sessions, extra._meta) };
|
|
631
636
|
}
|
|
632
637
|
catch (e) {
|
|
633
638
|
return fail(e);
|
|
634
639
|
}
|
|
635
640
|
});
|
|
641
|
+
// BellTeamのため(ADR 0122): 要求の_metaに aiterm/caller="BellTeam" がある時だけ、結果の_metaへ席の今の番を載せる。
|
|
642
|
+
// 説明と引数には出さない(steer-follow.ts の冒頭)。それ以外の呼び手には何も足さない。
|
|
643
|
+
function seatTurnsMeta(sessions, requestMeta) {
|
|
644
|
+
if (!interimCallerFromMeta(requestMeta))
|
|
645
|
+
return {};
|
|
646
|
+
try {
|
|
647
|
+
return { _meta: { [SEAT_TURNS_RESULT_META_KEY]: core.readSeatTurns(sessions.filter(session => session.harness !== null).map(session => session.session_id)) } };
|
|
648
|
+
}
|
|
649
|
+
catch (e) {
|
|
650
|
+
return { _meta: { [SEAT_TURNS_RESULT_META_KEY]: { schema: "aiterm.seat-turns.v1", error: e instanceof Error ? e.message : String(e) } } };
|
|
651
|
+
}
|
|
652
|
+
}
|
|
636
653
|
const nativeProcessIdentitySchema = z.object({
|
|
637
654
|
pid: z.number().int(), process_group_id: z.number().int().nullable(),
|
|
638
655
|
started_identity: z.string(), argv_digest: z.string(),
|
|
@@ -675,7 +692,10 @@ function interimWordsMeta(sessionId, requestMeta) {
|
|
|
675
692
|
if (!request)
|
|
676
693
|
return {};
|
|
677
694
|
try {
|
|
678
|
-
|
|
695
|
+
// 読み始める位置が付いているのに読めない時は、最後のdispatchの言葉を返さない(呼び手が別の番の言葉を出してしまう)。
|
|
696
|
+
if (request.from_invalid)
|
|
697
|
+
throw new Error("INTERIM_FROM_INVALID: aiterm/interim_from の値を読めません");
|
|
698
|
+
const words = core.readSessionInterimWords(sessionId, request.after, request.from ?? null);
|
|
679
699
|
return words ? { _meta: { [INTERIM_RESULT_META_KEY]: words } } : {};
|
|
680
700
|
}
|
|
681
701
|
catch (e) {
|
package/dist/interim-words.js
CHANGED
|
@@ -24,21 +24,47 @@ export const INTERIM_CALLER = "BellTeam";
|
|
|
24
24
|
export const INTERIM_AFTER_META_KEY = "aiterm/interim_after";
|
|
25
25
|
/** 結果の_metaで返すキー。 */
|
|
26
26
|
export const INTERIM_RESULT_META_KEY = "aiterm/interim_words";
|
|
27
|
+
/**
|
|
28
|
+
* 読み始める位置。差し込みの返りと「席の今の番」が渡す文字列(中身は呼び手に読ませない)。
|
|
29
|
+
* 指定した時は、最後のdispatchの境界ではなく、その位置より後の言葉を返す(ADR 0122)。
|
|
30
|
+
*/
|
|
31
|
+
export const INTERIM_FROM_META_KEY = "aiterm/interim_from";
|
|
32
|
+
export function interimFromToken(from) {
|
|
33
|
+
return `v1:${from.event_cursor}:${from.unit === "byte" ? "b" : "u"}:${from.boundary}`;
|
|
34
|
+
}
|
|
35
|
+
export function parseInterimFrom(token) {
|
|
36
|
+
if (typeof token !== "string")
|
|
37
|
+
return null;
|
|
38
|
+
const match = /^v1:(\d{1,15}):([bu]):(\d{1,15})$/.exec(token);
|
|
39
|
+
if (!match)
|
|
40
|
+
return null;
|
|
41
|
+
return { event_cursor: Number(match[1]), unit: match[2] === "b" ? "byte" : "user", boundary: Number(match[3]) };
|
|
42
|
+
}
|
|
27
43
|
export function agentInterimBoundaryPath(name, launchId) {
|
|
28
44
|
assertSessionName(name);
|
|
29
45
|
if (!LAUNCH_ID_RE.test(launchId))
|
|
30
46
|
throw new AitermError(`launch_id が不正です: ${launchId}`, 2);
|
|
31
47
|
return path.join(agentsDir(), `${name}.${launchId}.interim.json`);
|
|
32
48
|
}
|
|
33
|
-
/** 要求の_metaがBellTeam
|
|
49
|
+
/** 要求の_metaがBellTeamの印を持つか。 */
|
|
50
|
+
export function interimCallerFromMeta(meta) {
|
|
51
|
+
return !!meta && typeof meta === "object" && meta[INTERIM_CALLER_META_KEY] === INTERIM_CALLER;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* 要求の_metaがBellTeamの印を持つ時だけ、前回のseqを返す。それ以外はnull(何も足さない)。
|
|
55
|
+
* 読み始める位置(aiterm/interim_from)が付いていれば、fromに入れる。付いているのに読めない時はfromがnullでfrom_invalidがtrue
|
|
56
|
+
* (最後のdispatchの言葉を黙って返すと、呼び手は別の番の言葉を出してしまう)。
|
|
57
|
+
*/
|
|
34
58
|
export function interimRequestFromMeta(meta) {
|
|
35
|
-
if (!meta
|
|
59
|
+
if (!interimCallerFromMeta(meta))
|
|
36
60
|
return null;
|
|
37
61
|
const record = meta;
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
62
|
+
const raw = record[INTERIM_AFTER_META_KEY];
|
|
63
|
+
const after = typeof raw === "number" && Number.isSafeInteger(raw) && raw > 0 ? raw : 0;
|
|
64
|
+
if (record[INTERIM_FROM_META_KEY] === undefined)
|
|
65
|
+
return { after };
|
|
66
|
+
const from = parseInterimFrom(record[INTERIM_FROM_META_KEY]);
|
|
67
|
+
return from ? { after, from } : { after, from: null, from_invalid: true };
|
|
42
68
|
}
|
|
43
69
|
function parse(line) {
|
|
44
70
|
if (!line.trim())
|
|
@@ -238,8 +264,54 @@ function readInterimBoundary(meta) {
|
|
|
238
264
|
return null;
|
|
239
265
|
return value;
|
|
240
266
|
}
|
|
241
|
-
/**
|
|
242
|
-
|
|
267
|
+
/**
|
|
268
|
+
* 今の時点の読み始めの位置。差し込みを打つ直前に取る(その差し込みより後に書かれた言葉だけを読むため)。
|
|
269
|
+
* Claude・Codex・Cursorは記録のbyte位置、Grokは記録中のuser発話の数(「今すぐ送る」で入った文は、次のuser発話になる)。
|
|
270
|
+
* Cursorは、番が動いている間は記録を足すだけなので、byte位置が動かない(末尾の行を書き換えるのは、次の番の開始の時)。
|
|
271
|
+
*/
|
|
272
|
+
export function interimFromNow(meta, eventCursor) {
|
|
273
|
+
if (meta.kind === "grok")
|
|
274
|
+
return { event_cursor: eventCursor, unit: "user", boundary: grokUserCount(readLines(grokChatHistory(meta))) };
|
|
275
|
+
let file = null;
|
|
276
|
+
try {
|
|
277
|
+
file = meta.kind === "claude" ? claudeSessionTranscriptPath(meta)
|
|
278
|
+
: meta.kind === "codex" ? (meta.vendor_session_id ? codexBoundTranscript(meta) : codexRootTranscript(meta))
|
|
279
|
+
: cursorTranscript(meta);
|
|
280
|
+
}
|
|
281
|
+
catch { /* 記録を決められない時は、頭から */ }
|
|
282
|
+
return { event_cursor: eventCursor, unit: "byte", boundary: file ? safeStatSize(file) : 0 };
|
|
283
|
+
}
|
|
284
|
+
/** 渡された位置より後に書かれた合間の言葉。位置の単位がharnessと合わない時は、言葉なし。 */
|
|
285
|
+
function interimWordsFrom(meta, from) {
|
|
286
|
+
if (meta.kind === "grok")
|
|
287
|
+
return from.unit === "user" ? grokInterimWords(readLines(grokChatHistory(meta)), from.boundary) : [];
|
|
288
|
+
if (from.unit !== "byte")
|
|
289
|
+
return [];
|
|
290
|
+
if (meta.kind === "claude")
|
|
291
|
+
return claudeInterimWords(readLines(claudeSessionTranscriptPath(meta), from.boundary));
|
|
292
|
+
if (meta.kind === "codex")
|
|
293
|
+
return codexInterimWords(readLines(meta.vendor_session_id ? codexBoundTranscript(meta) : codexRootTranscript(meta), from.boundary));
|
|
294
|
+
// 位置より後の行だけを渡すので、user発話の数では絞らない。
|
|
295
|
+
return cursorInterimWords(readLines(cursorTranscript(meta), from.boundary), -1);
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* 現在(または直前)のturnで書かれた合間の言葉のうち、seqがafterより後のもの。
|
|
299
|
+
* fromを渡した時は、最後のdispatchの境界ではなく、その位置より後の言葉(seqはその位置から1で数える)。
|
|
300
|
+
*/
|
|
301
|
+
export function readInterimWords(meta, after, from = null) {
|
|
302
|
+
if (from) {
|
|
303
|
+
const words = interimWordsFrom(meta, from).map((word, index) => ({ seq: index + 1, ...word }));
|
|
304
|
+
return {
|
|
305
|
+
schema: "aiterm.interim-words.v1",
|
|
306
|
+
session_id: meta.aiterm_session,
|
|
307
|
+
launch_id: meta.launch_id,
|
|
308
|
+
harness: agentHarness(meta.kind),
|
|
309
|
+
event_cursor: from.event_cursor,
|
|
310
|
+
operation_id: null,
|
|
311
|
+
last_seq: words.length,
|
|
312
|
+
words: words.filter((word) => word.seq > after),
|
|
313
|
+
};
|
|
314
|
+
}
|
|
243
315
|
const recorded = readInterimBoundary(meta);
|
|
244
316
|
// dispatchの記録が無ければ、起動時promptのturnだけを記録の先頭から読む。promptなしで起動してまだ送っていない時は無い。
|
|
245
317
|
const launchTurn = !recorded && meta.initial_prompt !== "none";
|
package/dist/parent-delivery.js
CHANGED
|
@@ -307,7 +307,8 @@ export class ParentDeliveryManager {
|
|
|
307
307
|
job.capture = (async () => {
|
|
308
308
|
if (completion.launch_id !== job.record.boundary.launch_id)
|
|
309
309
|
throw new AitermError("PARENT_DELIVERY_LAUNCH_CHANGED: 子のlaunchが置換されました", 2);
|
|
310
|
-
|
|
310
|
+
// not_takenは差し込みを追う待ちだけが返す。親配送の待ちは差し込みを追わないので、来ない。
|
|
311
|
+
if (completion.outcome === "running" || completion.outcome === "timeout" || completion.outcome === "not_taken")
|
|
311
312
|
throw new AitermError("PARENT_DELIVERY_NOT_COMPLETED: 子の完了を確認できません", 2);
|
|
312
313
|
const record = job.record;
|
|
313
314
|
record.child_outcome = completion.outcome;
|