aiterm-mcp 0.27.8 → 0.28.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.
@@ -1,4 +1,4 @@
1
- // エージェント CLI(claude / codex / grok)・Throughline・pane shell(Windows は Git Bash)の
1
+ // エージェント CLI(claude / codex / grok / cursor-agent)・Throughline・pane shell(Windows は Git Bash)の
2
2
  // 実行ファイルをどう見つけ、どう起動するかの所有者(OS 分岐の所有者。tmux とは独立)。
3
3
  import { spawnSync } from "node:child_process";
4
4
  import * as fs from "node:fs";
@@ -115,7 +115,9 @@ export function resolveAgentBin(kind) {
115
115
  ? ["CLAUDE_BIN", [".local", "bin", "claude"], "claude"]
116
116
  : kind === "codex"
117
117
  ? ["CODEX_BIN", [".local", "bin", "codex"], "codex"]
118
- : ["GROK_BIN", [".grok", "bin", isWin ? "grok.exe" : "grok"], "grok"];
118
+ : kind === "cursor"
119
+ ? ["CURSOR_AGENT_BIN", [".local", "bin", "cursor-agent"], "cursor-agent"]
120
+ : ["GROK_BIN", [".grok", "bin", isWin ? "grok.exe" : "grok"], "grok"];
119
121
  const fromEnv = process.env[envVar];
120
122
  if (fromEnv) {
121
123
  // 明示指定 env は実在を検証する。存在しないパスを黙って返すと、session を作って
@@ -125,9 +127,16 @@ export function resolveAgentBin(kind) {
125
127
  return resolveWindowsCodexShim(kind, fromEnv);
126
128
  throw new AitermError(`${envVar} に指定された ${name} が存在しません: ${fromEnv}`, 2);
127
129
  }
128
- const cand = path.join(home, ...rel);
129
- if (isUsableAgentExecutableFile(cand))
130
- return resolveWindowsCodexShim(kind, cand);
130
+ const defaultCandidates = [path.join(home, ...rel)];
131
+ if (kind === "cursor" && isWin && process.env.LOCALAPPDATA) {
132
+ // Cursor公式Windows installerは %LOCALAPPDATA%\cursor-agent をPATHへ追加し、
133
+ // cursor-agent.exe/cmdを置く。GUI hostの古いPATHでも公式配置を直接解決する。
134
+ defaultCandidates.unshift(path.join(process.env.LOCALAPPDATA, "cursor-agent", "cursor-agent.exe"), path.join(process.env.LOCALAPPDATA, "cursor-agent", "cursor-agent.cmd"));
135
+ }
136
+ for (const cand of defaultCandidates) {
137
+ if (isUsableAgentExecutableFile(cand))
138
+ return resolveWindowsCodexShim(kind, cand);
139
+ }
131
140
  const w = spawnSync(isWin ? "where" : "which", [name], {
132
141
  encoding: "utf8",
133
142
  timeout: 5000,
@@ -164,7 +164,18 @@ export function agentLabel(kind) {
164
164
  ? "Grok Build(Composer)"
165
165
  : kind === "grok"
166
166
  ? "Grok Build(Grok)"
167
- : "Codex";
167
+ : kind === "cursor"
168
+ ? "Cursor Agent CLI"
169
+ : "Codex";
170
+ }
171
+ export function agentHarness(kind) {
172
+ return kind === "claude"
173
+ ? "claude-code"
174
+ : kind === "codex"
175
+ ? "codex-cli"
176
+ : kind === "cursor"
177
+ ? "cursor-cli"
178
+ : "grok-cli";
168
179
  }
169
180
  export function subagentInstruction(meta) {
170
181
  if (meta.agent_role !== "subagent" ||
@@ -193,10 +204,12 @@ export function subagentInstruction(meta) {
193
204
  export function writeScopeLaunchNote(kind, writeScope) {
194
205
  return writeScope === undefined
195
206
  ? ""
196
- : (kind === "codex" || kind === "grok" || kind === "composer") && writeScope === "read-only"
197
- ? `\n能力宣言: write_scope=${JSON.stringify(writeScope)}。${agentLabel(kind)} CLIへ --sandbox read-only を付与し、書込みを実効禁止。` +
198
- (kind === "codex" ? "" : "MCPツール許可は --always-approve で自動承認(sandbox内のため能力拡大なし)。")
199
- : `\n能力宣言: write_scope=${JSON.stringify(writeScope)}。パス単位のsandbox allowlistに対応するCLI引数がないため宣言の記録のみ(構造的unsupported)。`;
207
+ : kind === "cursor" && writeScope === "read-only"
208
+ ? `\n能力宣言: write_scope=${JSON.stringify(writeScope)}。Cursor Agent CLIへ --mode ask を付与し、書込みを実効禁止。`
209
+ : (kind === "codex" || kind === "grok" || kind === "composer") && writeScope === "read-only"
210
+ ? `\n能力宣言: write_scope=${JSON.stringify(writeScope)}。${agentLabel(kind)} CLIへ --sandbox read-only を付与し、書込みを実効禁止。` +
211
+ (kind === "codex" ? "" : "MCPツール許可は --always-approve で自動承認(sandbox内のため能力拡大なし)。")
212
+ : `\n能力宣言: write_scope=${JSON.stringify(writeScope)}。パス単位のsandbox allowlistに対応するCLI引数がないため宣言の記録のみ(構造的unsupported)。`;
200
213
  }
201
214
  export function agentLineageFields(context) {
202
215
  return {
@@ -42,7 +42,7 @@ export function parseArgs(argv) {
42
42
  if (cursor !== null)
43
43
  throw new Error(`--cursor が重複しています。${USAGE}`);
44
44
  if (!/^\d+$/.test(v))
45
- throw new Error(`--cursor は0以上の整数byte offsetだけを受理します。${USAGE}`);
45
+ throw new Error(`--cursor は0以上の整数完了境界だけを受理します。${USAGE}`);
46
46
  cursor = Number(v);
47
47
  }
48
48
  else {
package/dist/core.js CHANGED
@@ -13,14 +13,16 @@ import * as path from "node:path";
13
13
  import { createHash, randomBytes } from "node:crypto";
14
14
  import * as rtk from "./rtk.js";
15
15
  import { AitermError, telemetryOwnedFailure, ownTelemetryFailure } from "./errors.js";
16
- import { isWin, SOCKDIR, tmuxCommand, loadPtyBufferChunk, pasteBufferBaseArgs, TMUX_EMPTY_CONFIG, attachCommand, normalizePaneCommand, settlePaneLog, paneCwdArgument, } from "./tmux-runtime.js";
17
- import { sleep, currentUid, runtimeStateBase, safeStatSize, readFileRange, writeJson0600, createEmpty0600, shq, LAUNCH_ID_RE, AGENT_DONE_POLL_MS, AGENT_EVENT_MAX_BYTES, assertSessionName, agentsDir, agentEventPath, agentMetadataPath, writeAgentMetadata, AGENT_EVENT_TAIL_BYTES, agentLabel, agentLineageFields, } from "./agent-shared.js";
16
+ import { isWin, SOCKDIR, tmuxCommand, loadPtyBufferChunk, pasteBufferBaseArgs, TMUX_EMPTY_CONFIG, attachCommand, normalizePaneCommand, appendMarkSentinel, settlePaneLog, paneCwdArgument, } from "./tmux-runtime.js";
17
+ import { sleep, currentUid, runtimeStateBase, safeStatSize, readFileRange, writeJson0600, createEmpty0600, shq, LAUNCH_ID_RE, AGENT_DONE_POLL_MS, AGENT_EVENT_MAX_BYTES, assertSessionName, agentsDir, agentEventPath, agentMetadataPath, writeAgentMetadata, AGENT_EVENT_TAIL_BYTES, agentLabel, agentHarness, subagentInstruction, agentLineageFields, } from "./agent-shared.js";
18
18
  import { GROK_MODEL_DEFAULTS, realGrokHome, resolveAndValidateGrokAuth, assertGrokModelAvailable, grokEventsTranscript, latestGrokCompletion, grokInitializationComplete, observeGrokDone, buildGrokAgentCmd, grokLaunchNote, grokEnvTokens, grokTuiReady, GROK_COMPOSER_MARKER_RE, grokFooterHasConfiguration, grokTranscriptText, createGrokAgentMetadata, } from "./vendors/grok.js";
19
19
  import { bindCodexTranscriptSession, latestCodexCompletion, observeCodexDone, buildCodexAgentCmd, codexLaunchNote, codexTuiReady, CODEX_COMPOSER_MARKER_RE, codexModelChoice, codexEffortChoice, codexMoreReasoningChoice, codexTranscriptText, createCodexAgentMetadata, } from "./vendors/codex.js";
20
20
  import { OPERATION_ID_RE, CLAUDE_RESULT_MAX_BYTES, CLAUDE_EFFORTS, agentManagedClaudeSettingsPath, agentClaudeResultPath, agentClaudeOperationPath, agentClaudeApprovalReceiptPath, agentClaudeDispatchReceiptPath, validateOperationId, readClaudeResultText, assertClaudeAuthenticationReady, buildClaudeAgentCmd, claudeLaunchNote, claudeTuiReady, CLAUDE_COMPOSER_MARKER_RE, createClaudeAgentMetadata, } from "./vendors/claude.js";
21
+ import { bindCursorTranscriptSession, cursorTurnBoundary, latestCursorCompletion, observeCursorDone, cursorTranscriptText, assertCursorAuthenticationReady, assertCursorModelAvailable, buildCursorAgentCmd, createCursorAgentMetadata, cursorLaunchNote, cursorEffortNavigation, cursorTuiReady, CURSOR_COMPOSER_MARKER_RE, validateCursorModelEffort, } from "./vendors/cursor.js";
21
22
  import { resolveAgentBin, resolveThroughlineBin, runThroughlineHandoffContext, isWindowsNativeExecutable, agentBinForPaneShell, resolveWinPaneShell } from "./agent-resolver.js";
22
23
  export { AitermError } from "./errors.js";
23
24
  export { tmuxSpawnEnv } from "./tmux-runtime.js";
25
+ export { agentHarness } from "./agent-shared.js";
24
26
  // 完了検出
25
27
  export const DEFAULT_TIMEOUT = 10.0;
26
28
  const POLL = 0.25;
@@ -202,11 +204,12 @@ function cleanupAgentState(name) {
202
204
  f.endsWith(".claude-settings.json") ||
203
205
  f.endsWith(".claude-mcp.json") ||
204
206
  f.endsWith(".claude-result.json") ||
207
+ f.endsWith(".cursor-result.json") ||
205
208
  f.endsWith(".claude-operation.json") ||
206
209
  f.endsWith(".claude-approval.json") ||
207
210
  f.endsWith(".claude-dispatch"))
208
211
  fs.unlinkSync(p);
209
- else if (f.endsWith(".codex-home") || f.endsWith(".grok-home") || f.endsWith(".home")) {
212
+ else if (f.endsWith(".codex-home") || f.endsWith(".grok-home") || f.endsWith(".cursor-plugin") || f.endsWith(".home")) {
210
213
  fs.rmSync(p, { recursive: true, force: true });
211
214
  }
212
215
  }
@@ -651,10 +654,11 @@ export function send(name, text, o = {}) {
651
654
  assertInitialPromptNotPendingForSend(name, !!o.force);
652
655
  assertManagedClaudeCredentialCommandNotSent(name, text);
653
656
  text = prepareSendText(text, o);
657
+ let fg = "";
654
658
  if (o.mark) {
655
659
  // mark の sentinel は POSIX シェル構文。前面が fish/csh/tcsh 等の非 POSIX 対話シェルだと "$?" が
656
660
  // 壊れて sentinel が成立しない。黙って壊れた完了検出を作らず、明示エラーで until を促す(B8)。
657
- const fg = paneCurrentCommand(name);
661
+ fg = paneCurrentCommand(name);
658
662
  if (NON_POSIX_MARK_SHELLS.has(fg)) {
659
663
  throw new AitermError(`mark は POSIX シェル(bash/sh/zsh/dash)前提です。前面が ${fg} のため sentinel の "$?" が` +
660
664
  `正しく展開されません。until で完了パターンを指定してください。`, 2);
@@ -675,7 +679,7 @@ export function send(name, text, o = {}) {
675
679
  if (o.rtk && !o.force)
676
680
  assertNotDestructive(text, 3, "rtk 変換後: ");
677
681
  if (o.mark)
678
- text = text + `; printf '\\n<<<AITERM_DONE rc=%d>>>\\n' "$?"`;
682
+ text = appendMarkSentinel(text, fg);
679
683
  assertSendTextSize(text, o.rtk || o.mark ? "変換後の送信文字列" : "送信文字列");
680
684
  const reportedText = text;
681
685
  if (!o.raw && text.includes("\n") && ATOMIC_MULTILINE_SHELLS.has(paneCurrentCommand(name))) {
@@ -922,6 +926,7 @@ export function listSessions() {
922
926
  return line;
923
927
  const agent = [
924
928
  `agent=${meta.kind}`,
929
+ `harness=${agentHarness(meta.kind)}`,
925
930
  "agent_done=true",
926
931
  meta.write_scope === undefined ? null : `write_scope=${JSON.stringify(meta.write_scope)}`,
927
932
  meta.vendor_session_id ? `vendor_session_id=${meta.vendor_session_id}` : null,
@@ -1583,7 +1588,7 @@ function loadAgentMetadata(name) {
1583
1588
  throw new AitermError(`agent metadata を読めません: ${e.message}`, 2);
1584
1589
  }
1585
1590
  const m = raw;
1586
- if ((m.kind !== "claude" && m.kind !== "codex" && m.kind !== "grok" && m.kind !== "composer") ||
1591
+ if ((m.kind !== "claude" && m.kind !== "codex" && m.kind !== "grok" && m.kind !== "composer" && m.kind !== "cursor") ||
1587
1592
  m.aiterm_session !== name ||
1588
1593
  typeof m.launch_id !== "string" ||
1589
1594
  !LAUNCH_ID_RE.test(m.launch_id)) {
@@ -1654,6 +1659,31 @@ function loadAgentMetadata(name) {
1654
1659
  codex_home: recordedHome,
1655
1660
  };
1656
1661
  }
1662
+ if (m.kind === "cursor") {
1663
+ const recordedHome = m.cursor_home;
1664
+ if (m.hook_route !== "shared_cursor_home" ||
1665
+ m.completion_route !== "cursor_transcript" ||
1666
+ typeof recordedHome !== "string" ||
1667
+ !path.isAbsolute(recordedHome)) {
1668
+ throw new AitermError("agent metadata の cursor_home が不正です(絶対パスの記録が必要)", 2);
1669
+ }
1670
+ return {
1671
+ kind: "cursor",
1672
+ aiterm_session: name,
1673
+ launch_id: m.launch_id,
1674
+ event_file: expectedEvent,
1675
+ created_at: typeof m.created_at === "string" ? m.created_at : "",
1676
+ cwd: typeof m.cwd === "string" ? m.cwd : null,
1677
+ ...(typeof m.write_scope === "string" ? { write_scope: m.write_scope } : {}),
1678
+ vendor_session_id: typeof m.vendor_session_id === "string" ? m.vendor_session_id : null,
1679
+ initial_prompt: normalizeInitialPromptState(m.initial_prompt),
1680
+ hook_route: "shared_cursor_home",
1681
+ completion_route: "cursor_transcript",
1682
+ ...loadAgentLineageFields(m, true),
1683
+ node_platform: process.platform,
1684
+ cursor_home: recordedHome,
1685
+ };
1686
+ }
1657
1687
  // grok_home も codex_home と同じ理由で per-launch の記録値を正とする(席専用 GROK_HOME)。
1658
1688
  // 認証正本の照合も記録された home に対して行う——現在の env の home と比較すると、
1659
1689
  // 席専用 home で起動した session が別 instance から拒否される。
@@ -1790,6 +1820,15 @@ function bindCompletedInitialPrompt(meta) {
1790
1820
  setInitialPromptState(meta, "done");
1791
1821
  return;
1792
1822
  }
1823
+ if (meta.kind === "cursor" && meta.completion_route === "cursor_transcript") {
1824
+ const done = latestCursorCompletion(meta, readTranscriptLines);
1825
+ if (!done) {
1826
+ throw new AitermError(`agent session '${meta.aiterm_session}' は起動時 prompt の完了待ちです。${agentWaitGuide(meta.aiterm_session)}`, 2);
1827
+ }
1828
+ bindAgentVendorSession(meta, done);
1829
+ setInitialPromptState(meta, "done");
1830
+ return;
1831
+ }
1793
1832
  if ((meta.kind === "grok" || meta.kind === "composer") && meta.completion_route === "grok_transcript") {
1794
1833
  if (!latestGrokCompletion(meta, readTranscriptLines)) {
1795
1834
  throw new AitermError(`agent session '${meta.aiterm_session}' は起動時 prompt の完了待ちです。${agentWaitGuide(meta.aiterm_session)}`, 2);
@@ -1827,6 +1866,9 @@ function tryLoadAgentMetadata(name) {
1827
1866
  function latestAgentDoneEvent(meta, expectedOperationId = null) {
1828
1867
  if (meta.kind === "codex")
1829
1868
  return latestCodexCompletion(meta, readTranscriptLines);
1869
+ if (meta.kind === "cursor" && meta.completion_route === "cursor_transcript") {
1870
+ return latestCursorCompletion(meta, readTranscriptLines);
1871
+ }
1830
1872
  if ((meta.kind === "grok" || meta.kind === "composer") && meta.completion_route === "grok_transcript") {
1831
1873
  return latestGrokCompletion(meta, readTranscriptLines);
1832
1874
  }
@@ -1892,6 +1934,14 @@ function recoverAgentVendorSession(meta) {
1892
1934
  setInitialPromptState(meta, "done");
1893
1935
  return;
1894
1936
  }
1937
+ if (meta.kind === "cursor") {
1938
+ const transcript = bindCursorTranscriptSession(meta);
1939
+ if (!transcript)
1940
+ return;
1941
+ if (meta.initial_prompt === "pending" && latestCursorCompletion(meta, readTranscriptLines))
1942
+ setInitialPromptState(meta, "done");
1943
+ return;
1944
+ }
1895
1945
  const size = safeStatSize(meta.event_file);
1896
1946
  if (size === 0)
1897
1947
  return;
@@ -1915,6 +1965,10 @@ function agentCompletionCursor(meta) {
1915
1965
  const transcript = grokEventsTranscript(meta);
1916
1966
  return transcript ? safeStatSize(transcript) : 0;
1917
1967
  }
1968
+ if (meta.kind === "cursor" && meta.completion_route === "cursor_transcript") {
1969
+ bindCursorTranscriptSession(meta);
1970
+ return cursorTurnBoundary(meta);
1971
+ }
1918
1972
  if (meta.kind !== "codex")
1919
1973
  return safeStatSize(meta.event_file);
1920
1974
  const transcript = bindCodexTranscriptSession(meta);
@@ -1928,7 +1982,7 @@ function transcriptUnavailable() {
1928
1982
  throw new AitermError(`transcript がまだありません。ターン完了後に再取得してください。${agentWaitGuide()}`, 2);
1929
1983
  }
1930
1984
  function transcriptNotFound(vendor) {
1931
- throw new AitermError(`最終 assistant メッセージを特定できませんでした(vendor=${vendor})。screen で確認してください。`, 2);
1985
+ throw new AitermError(`最終 assistant メッセージを特定できませんでした(harness=${agentHarness(vendor)}, vendor=${vendor})。screen で確認してください。`, 2);
1932
1986
  }
1933
1987
  function readTranscriptLines(file) {
1934
1988
  try {
@@ -2000,6 +2054,9 @@ export async function readAgentTranscript(name, o = {}) {
2000
2054
  transcriptUnavailable();
2001
2055
  text = readClaudeResultText(meta, done, operationId, transcriptUnavailable);
2002
2056
  }
2057
+ else if (meta.kind === "cursor") {
2058
+ text = cursorTranscriptText(meta, readTranscriptLines, transcriptUnavailable);
2059
+ }
2003
2060
  else if (meta.kind === "codex") {
2004
2061
  text = codexTranscriptText(meta, turnId, readTranscriptLines, transcriptUnavailable);
2005
2062
  }
@@ -2016,6 +2073,7 @@ export async function readAgentTranscript(name, o = {}) {
2016
2073
  "agent_transcript",
2017
2074
  `vendor=${meta.kind}`,
2018
2075
  `turn_id=${turnId ?? "unknown"}`,
2076
+ `harness=${agentHarness(meta.kind)}`,
2019
2077
  done?.operation_id ? `operation_id=${done.operation_id}` : null,
2020
2078
  `raw_chars=${rawChars}`,
2021
2079
  ].filter(Boolean).join(" ");
@@ -2053,6 +2111,7 @@ function agentReadMetadataSuffix(name, screen) {
2053
2111
  const bits = [
2054
2112
  "agent",
2055
2113
  `vendor=${meta.kind}`,
2114
+ `harness=${agentHarness(meta.kind)}`,
2056
2115
  `initial_prompt=${meta.initial_prompt}`,
2057
2116
  `agent_event_seen=${ev ? "true" : "false"}`,
2058
2117
  "completion_attribution=none",
@@ -2144,7 +2203,7 @@ export function detectAgentRateLimit(kind, aitermSession) {
2144
2203
  return null;
2145
2204
  }
2146
2205
  // 外部waiterプロセス用の純リーダー観測。lock・PTY・metadata書込・dispatch状態には一切触れない。
2147
- // Codexはrollout transcript、他vendorはevent fileを増分走査し、vendor_session_idのbind永続化を
2206
+ // Codex/Cursor/Grokは各harnessのtranscript、Claudeはevent fileを増分走査し、vendor_session_idのbind永続化を
2148
2207
  // 行わない(waiterは観測者であって所有者でない)。
2149
2208
  export async function observeAgentDone(name, o = {}) {
2150
2209
  const meta = loadAgentMetadata(name);
@@ -2153,12 +2212,15 @@ export async function observeAgentDone(name, o = {}) {
2153
2212
  throw new AitermError("operation_id はClaude agent sessionだけで使用できます", 2);
2154
2213
  }
2155
2214
  if (o.cursor != null && (!Number.isInteger(o.cursor) || o.cursor < 0)) {
2156
- throw new AitermError("cursor は0以上の整数byte offsetで指定してください", 2);
2215
+ throw new AitermError("cursor は0以上の整数完了境界で指定してください", 2);
2157
2216
  }
2158
2217
  const timeout = o.timeout ?? DEFAULT_AGENT_DONE_TIMEOUT;
2159
2218
  if (meta.kind === "codex" && meta.completion_route === "codex_transcript") {
2160
2219
  return observeCodexDone(meta, timeout, o.cursor, detectAgentRateLimit);
2161
2220
  }
2221
+ if (meta.kind === "cursor" && meta.completion_route === "cursor_transcript") {
2222
+ return observeCursorDone(meta, timeout, o.cursor, detectAgentRateLimit);
2223
+ }
2162
2224
  if ((meta.kind === "grok" || meta.kind === "composer") && meta.completion_route === "grok_transcript") {
2163
2225
  return observeGrokDone(meta, timeout, o.cursor, detectAgentRateLimit);
2164
2226
  }
@@ -2175,6 +2237,7 @@ export async function observeAgentDone(name, o = {}) {
2175
2237
  session_id: meta.aiterm_session,
2176
2238
  launch_id: meta.launch_id,
2177
2239
  vendor: meta.kind,
2240
+ harness: agentHarness(meta.kind),
2178
2241
  outcome,
2179
2242
  operation_id: ev?.operation_id ?? operationId,
2180
2243
  vendor_session_id: ev?.vendor_session_id ?? meta.vendor_session_id ?? null,
@@ -2224,19 +2287,27 @@ function isAgentTuiReady(kind, screen) {
2224
2287
  return claudeTuiReady(screen);
2225
2288
  if (kind === "codex")
2226
2289
  return codexTuiReady(screen);
2290
+ if (kind === "cursor")
2291
+ return cursorTuiReady(screen);
2227
2292
  return grokTuiReady(screen);
2228
2293
  }
2229
- // Codex/Claude は実行中に「(esc to interrupt)」を表示する(実機採取)。startup 側の処理
2230
- // (MCP initialize 等)が走ったまま composer だけ描画されている画面は入力受付とみなさない。
2294
+ // Codex/Claude は実行中に「(esc to interrupt)」、Cursor は「Working」+
2295
+ // 「ctrl+c to stop」を表示する(いずれも実機採取)。startup 側の処理(MCP initialize 等)が
2296
+ // 走ったまま古い composer がscrollbackに残る画面は入力受付とみなさない。
2231
2297
  // Grok/Composer は busy 表示文字列の実機根拠が未採取のため対象外(誤ブロックで起動不能にしない)。
2232
- const AGENT_TUI_BUSY_KINDS = new Set(["codex", "claude"]);
2233
- const AGENT_TUI_BUSY_RE = /esc to interrupt/i;
2298
+ function isAgentTuiBusy(kind, screen) {
2299
+ if (kind === "cursor")
2300
+ return /ctrl\+c to stop/i.test(screen);
2301
+ if (kind === "codex" || kind === "claude")
2302
+ return /esc to interrupt/i.test(screen);
2303
+ return false;
2304
+ }
2234
2305
  // ready gate 用: 入力欄マーカーがあっても busy 表示中は ready と数えない。
2235
2306
  // frontend 推定(inferAgentFrontend)は「agent TUI が前面か」を見るだけなので isAgentTuiReady のまま。
2236
2307
  function isAgentTuiIdleReady(kind, screen) {
2237
2308
  if (!isAgentTuiReady(kind, screen))
2238
2309
  return false;
2239
- return !(AGENT_TUI_BUSY_KINDS.has(kind) && AGENT_TUI_BUSY_RE.test(screen));
2310
+ return !isAgentTuiBusy(kind, screen);
2240
2311
  }
2241
2312
  async function waitAgentTuiReadyImpl(kind, sample, sleepFn, opts = {}) {
2242
2313
  const timeoutMs = opts.timeoutMs ?? AGENT_TUI_READY_TIMEOUT_MS;
@@ -2285,11 +2356,21 @@ function agentSubmitResidueTail(text) {
2285
2356
  return cps.slice(-AGENT_SUBMIT_RESIDUE_TAIL_CHARS).join("");
2286
2357
  }
2287
2358
  function agentSubmitResidueOnScreen(kind, screen, tail) {
2359
+ // Cursorは送信成立後のactive turnで明示的な停止UIを出す。scrollbackに残った旧「>」を
2360
+ // composerと誤認すると、実際には走っているpromptをsubmit座礁として報告してしまう。
2361
+ if (kind === "cursor" && isAgentTuiBusy(kind, screen))
2362
+ return false;
2288
2363
  const lines = screen.split("\n");
2289
2364
  // 入力欄マーカーは ready 判定と同じ記号を行頭基準で探す。submit 済みの transcript echo は
2290
2365
  // マーカー行より上に出るため、最後のマーカー行以降だけを composer 領域として見る。
2291
2366
  // grok/composer は Windows native 描画(`>`・実測 1.0.4)も ready 判定と同様に受ける。
2292
- const markerRe = kind === "codex" ? CODEX_COMPOSER_MARKER_RE : kind === "claude" ? CLAUDE_COMPOSER_MARKER_RE : GROK_COMPOSER_MARKER_RE;
2367
+ const markerRe = kind === "codex"
2368
+ ? CODEX_COMPOSER_MARKER_RE
2369
+ : kind === "claude"
2370
+ ? CLAUDE_COMPOSER_MARKER_RE
2371
+ : kind === "cursor"
2372
+ ? CURSOR_COMPOSER_MARKER_RE
2373
+ : GROK_COMPOSER_MARKER_RE;
2293
2374
  let markerIdx = -1;
2294
2375
  for (let i = lines.length - 1; i >= 0; i--) {
2295
2376
  if (markerRe.test(lines[i])) {
@@ -2428,7 +2509,7 @@ export async function sendInitialAgentPrompt(name, text, o = {}) {
2428
2509
  const ready = await waitAgentTuiReady(name, meta, o.ready_timeout ?? AGENT_TUI_READY_TIMEOUT_MS);
2429
2510
  if (!ready.ready) {
2430
2511
  return {
2431
- text: `initial_prompt=not_sent vendor=${meta.kind} ready=false samples=${ready.samples}\n` +
2512
+ text: `initial_prompt=not_sent vendor=${meta.kind} ready=false samples=${ready.samples} harness=${agentHarness(meta.kind)}\n` +
2432
2513
  `agent session '${name}' の ${agentLabel(meta.kind)} TUI が入力受付状態になりません。prompt は送信していません。`,
2433
2514
  event_cursor: null,
2434
2515
  submit_residue: null,
@@ -2449,7 +2530,7 @@ export async function sendInitialAgentPrompt(name, text, o = {}) {
2449
2530
  }
2450
2531
  const residue = await detectAgentSubmitResidue(name, meta.kind, text);
2451
2532
  return {
2452
- text: `initial_prompt=pending vendor=${meta.kind} event_cursor=${startOffset}\n` +
2533
+ text: `initial_prompt=pending vendor=${meta.kind} event_cursor=${startOffset} harness=${agentHarness(meta.kind)}\n` +
2453
2534
  `起動時 prompt を送信した。${agentDispatchGuide(name, startOffset)}` +
2454
2535
  agentSubmitResidueWarning(name, residue.residue),
2455
2536
  event_cursor: startOffset,
@@ -2542,6 +2623,48 @@ export async function configureAgent(name, opts) {
2542
2623
  schema: "aiterm.agent-configure-result.v1",
2543
2624
  session_id: name,
2544
2625
  provider: "claude",
2626
+ harness: agentHarness("claude"),
2627
+ model,
2628
+ reasoning_effort: effort,
2629
+ };
2630
+ }
2631
+ if (meta.kind === "cursor") {
2632
+ validateCursorModelEffort(model, effort);
2633
+ if (!model)
2634
+ throw new AitermError("Cursorのreasoning_effort変更にはmodelも指定してください", 2);
2635
+ const bin = resolveAgentBin("cursor");
2636
+ if (!bin)
2637
+ throw new AitermError("Cursor Agent CLI の CLI が見つかりません", 2);
2638
+ assertCursorModelAvailable(bin, meta.cwd ?? process.cwd(), model, effort);
2639
+ // Cursor TUIの `/model <base model>` はmodelを直接切り替える一方、flatten済みcatalog ID
2640
+ // (例: gpt-5.6-luna-high)を渡すとno matchesのfilter画面へ入る。effortは標準model pickerの
2641
+ // parameter editorで選び、同じsessionと会話contextを保つ。
2642
+ await sendAgentPromptText(name, `/model ${model}`);
2643
+ const modelReady = await waitAgentTuiReady(name, meta, AGENT_TUI_READY_TIMEOUT_MS);
2644
+ if (!modelReady.ready)
2645
+ throw new AitermError("Cursorのmodel変更完了を確認できません", 2);
2646
+ if (effort) {
2647
+ await sendAgentPromptText(name, "/model");
2648
+ await waitForScreenText(name, "Available models");
2649
+ sendMenuChoice(name, "Tab");
2650
+ const parameterScreen = await waitForScreenText(name, "Edit Parameters");
2651
+ const navigation = cursorEffortNavigation(parameterScreen, effort);
2652
+ for (let i = 0; i < navigation.down; i++)
2653
+ sendMenuChoice(name, "Down");
2654
+ sendMenuChoice(name, "Enter");
2655
+ await waitForScreenText(name, `${navigation.label} ✓`);
2656
+ sendMenuChoice(name, "Escape");
2657
+ await waitForScreenText(name, "Available models");
2658
+ sendMenuChoice(name, "Enter");
2659
+ const effortReady = await waitAgentTuiReady(name, meta, AGENT_TUI_READY_TIMEOUT_MS);
2660
+ if (!effortReady.ready)
2661
+ throw new AitermError("Cursorのreasoning effort変更完了を確認できません", 2);
2662
+ }
2663
+ return {
2664
+ schema: "aiterm.agent-configure-result.v1",
2665
+ session_id: name,
2666
+ provider: "cursor",
2667
+ harness: agentHarness("cursor"),
2545
2668
  model,
2546
2669
  reasoning_effort: effort,
2547
2670
  };
@@ -2563,6 +2686,7 @@ export async function configureAgent(name, opts) {
2563
2686
  schema: "aiterm.agent-configure-result.v1",
2564
2687
  session_id: name,
2565
2688
  provider: meta.kind,
2689
+ harness: agentHarness(meta.kind),
2566
2690
  model,
2567
2691
  reasoning_effort: effort,
2568
2692
  };
@@ -2601,6 +2725,7 @@ export async function configureAgent(name, opts) {
2601
2725
  schema: "aiterm.agent-configure-result.v1",
2602
2726
  session_id: name,
2603
2727
  provider: "codex",
2728
+ harness: agentHarness("codex"),
2604
2729
  model,
2605
2730
  reasoning_effort: effort,
2606
2731
  };
@@ -2635,6 +2760,11 @@ export async function dispatchAgentTurn(name, text, o = {}) {
2635
2760
  }
2636
2761
  }
2637
2762
  const startOffset = agentCompletionCursor(meta);
2763
+ // promptなしで起動したCursorは、最初のdispatch時点ではtranscriptとの相関markerをまだ持たない。
2764
+ // その1回だけlaunch contextを加え、以後はbind済みconversationへ通常textだけを送る。
2765
+ const dispatchText = meta.kind === "cursor" && !meta.vendor_session_id
2766
+ ? `${subagentInstruction(meta)}\n\n${text}`
2767
+ : text;
2638
2768
  if (meta.kind === "claude") {
2639
2769
  // durable/anonymousを分岐する前に同じsend preflightを通す。拒否されるpromptの
2640
2770
  // receipt/active markerだけを残して、来ないStopを待つ状態を作らない。
@@ -2645,7 +2775,7 @@ export async function dispatchAgentTurn(name, text, o = {}) {
2645
2775
  else
2646
2776
  reserveAnonymousClaudeTurn(meta);
2647
2777
  }
2648
- send(name, text, {
2778
+ send(name, dispatchText, {
2649
2779
  enter: false,
2650
2780
  force: o.force,
2651
2781
  raw: o.raw,
@@ -2657,12 +2787,13 @@ export async function dispatchAgentTurn(name, text, o = {}) {
2657
2787
  // Codex TUI は literal text 投入直後の Enter を取り落とすことがある。agent 経路だけ submit を分離する。
2658
2788
  await sleep(AGENT_SUBMIT_DELAY_MS);
2659
2789
  sendKey(name, "Enter", { preserveAgentOperation: meta.kind === "claude" });
2660
- const residue = await detectAgentSubmitResidue(name, meta.kind, text);
2790
+ const residue = await detectAgentSubmitResidue(name, meta.kind, dispatchText);
2661
2791
  return {
2662
2792
  schema: "aiterm.agent-dispatch.v1",
2663
2793
  session_id: meta.aiterm_session,
2664
2794
  launch_id: meta.launch_id,
2665
2795
  vendor: meta.kind,
2796
+ harness: agentHarness(meta.kind),
2666
2797
  event_cursor: startOffset,
2667
2798
  operation_id: operationId,
2668
2799
  submit_residue: residue.residue,
@@ -2776,6 +2907,8 @@ function buildAgentCmd(kind, bin, model, effort, prompt, meta = null) {
2776
2907
  return buildClaudeAgentCmd(bin, model, effort, prompt, meta);
2777
2908
  if (kind === "codex")
2778
2909
  return buildCodexAgentCmd(bin, model, effort, prompt, meta);
2910
+ if (kind === "cursor")
2911
+ return buildCursorAgentCmd(bin, model, effort, prompt, meta);
2779
2912
  return buildGrokAgentCmd(kind, bin, model, effort, prompt, meta);
2780
2913
  }
2781
2914
  function agentEnvPrefix(meta, sid, envVars = []) {
@@ -2798,7 +2931,7 @@ function agentEnvPrefix(meta, sid, envVars = []) {
2798
2931
  `AITERM_AGENT_LINEAGE=${shq(meta.lineage ?? `host-root>${meta.kind}:${sid}`)}`,
2799
2932
  `AITERM_AGENT_DELEGATION_ALLOWED=${shq(meta.delegation_allowed === true ? "true" : "false")}`,
2800
2933
  ];
2801
- if (meta.kind === "claude" || meta.kind === "codex")
2934
+ if (meta.kind === "claude" || meta.kind === "codex" || meta.kind === "cursor")
2802
2935
  return common;
2803
2936
  return [...grokEnvTokens(meta), ...common];
2804
2937
  })();
@@ -2807,6 +2940,8 @@ function agentEnvPrefix(meta, sid, envVars = []) {
2807
2940
  function buildAgentLaunchNote(kind, model, effort, meta) {
2808
2941
  if (kind === "claude")
2809
2942
  return claudeLaunchNote(model, effort, meta);
2943
+ if (kind === "cursor")
2944
+ return cursorLaunchNote(model, effort, meta);
2810
2945
  if (kind !== "codex")
2811
2946
  return grokLaunchNote(kind, model, effort, meta);
2812
2947
  return codexLaunchNote(model, effort, meta);
@@ -2861,6 +2996,8 @@ export function openAgent(kind, opts = {}) {
2861
2996
  if (effort && kind === "claude" && !CLAUDE_EFFORTS.has(effort)) {
2862
2997
  throw new AitermError("Claude Code の reasoning_effort は low/medium/high/xhigh/max のいずれかです", 2);
2863
2998
  }
2999
+ if (kind === "cursor")
3000
+ validateCursorModelEffort(model, effort);
2864
3001
  const agentDone = !!opts.agent_done;
2865
3002
  const launchOperationId = opts.launch_operation_id == null
2866
3003
  ? null
@@ -2890,7 +3027,13 @@ export function openAgent(kind, opts = {}) {
2890
3027
  ownTelemetryFailure("AITERM.VENDOR_LAUNCHER_FAILED", error, 2);
2891
3028
  }
2892
3029
  if (!bin) {
2893
- const where = kind === "claude" ? "~/.local/bin/claude" : kind === "codex" ? "~/.local/bin/codex" : "~/.grok/bin/grok";
3030
+ const where = kind === "claude"
3031
+ ? "~/.local/bin/claude"
3032
+ : kind === "codex"
3033
+ ? "~/.local/bin/codex"
3034
+ : kind === "cursor"
3035
+ ? "~/.local/bin/cursor-agent"
3036
+ : "~/.grok/bin/grok";
2894
3037
  ownTelemetryFailure("AITERM.VENDOR_LAUNCHER_FAILED", new AitermError(`${label} の CLI が見つかりません(${where} か PATH が必要)`, 2), 2);
2895
3038
  }
2896
3039
  // cwd 検証(session を作る前に。cd 失敗はシェル内で静かに死に「起動した」と偽成功を返すため)。
@@ -2930,6 +3073,8 @@ export function openAgent(kind, opts = {}) {
2930
3073
  }
2931
3074
  if (kind === "claude")
2932
3075
  assertClaudeAuthenticationReady(bin);
3076
+ if (kind === "cursor")
3077
+ assertCursorAuthenticationReady(bin);
2933
3078
  // Windows の起動コマンドは native psmux pane の Git Bash で走る(WSL 橋・/mnt/c 変換は
2934
3079
  // e3f5fc8 で全廃。bin/cwd は Windows パスの forward slash 形のまま渡す)。
2935
3080
  // 未検証リスク: npm グローバル導入の codex.cmd/.bat シムの対話 TUI 描画は実 Windows でしか確認
@@ -2951,6 +3096,9 @@ export function openAgent(kind, opts = {}) {
2951
3096
  if (requestedModel)
2952
3097
  assertGrokModelAvailable(bin, cwd ?? process.cwd(), requestedModel);
2953
3098
  }
3099
+ if (kind === "cursor" && model) {
3100
+ assertCursorModelAvailable(bin, cwd ?? process.cwd(), model, effort);
3101
+ }
2954
3102
  let sid;
2955
3103
  let hint;
2956
3104
  try {
@@ -2971,7 +3119,9 @@ export function openAgent(kind, opts = {}) {
2971
3119
  ? createClaudeAgentMetadata(sid, cwd, opts.prompt ? "pending" : "none", launchOperationId, launchRequestDigest, lineageContext, model, effort)
2972
3120
  : kind === "codex"
2973
3121
  ? createCodexAgentMetadata(sid, cwd, opts.prompt ? "pending" : "none", { model, effort }, writeScope, lineageContext)
2974
- : createGrokAgentMetadata(kind, sid, cwd, opts.prompt ? "pending" : "none", grokAuthPath, writeScope, lineageContext)
3122
+ : kind === "cursor"
3123
+ ? createCursorAgentMetadata(sid, cwd, opts.prompt ? "pending" : "none", writeScope, lineageContext)
3124
+ : createGrokAgentMetadata(kind, sid, cwd, opts.prompt ? "pending" : "none", grokAuthPath, writeScope, lineageContext)
2975
3125
  : null;
2976
3126
  if (meta)
2977
3127
  agentMetadataNegativeCache.delete(sid);
@@ -3002,7 +3152,7 @@ export function openAgent(kind, opts = {}) {
3002
3152
  }
3003
3153
  throw failure;
3004
3154
  }
3005
- const driveHint = agentDone && kind === "claude"
3155
+ const driveHint = agentDone
3006
3156
  ? `TUI の描画には数秒かかる。少し置いてから pty_read(${sid}, screen:true) で画面を読み、` +
3007
3157
  `turnはpty_send(${sid}, "...")で送る(自動で非ブロックdispatch=投げっぱなしでよい・完了通知はaiterm-wait)。中断はpty_key(${sid}, "C-c")、` +
3008
3158
  `Stopが来ない場合の解除はpty_close(${sid})を使う。`
@@ -3036,7 +3186,7 @@ export async function openAgentWithInitialPrompt(kind, opts = {}) {
3036
3186
  }
3037
3187
  // v0.16.0: launcher は常に managed(Stop hook つき)で立つ。手動運転したい場合は
3038
3188
  // pty_open で素の PTY を開き、vendor CLI を自分で send する。
3039
- // 第3要素は「起動時点でturnが走っているか」の event_cursor: Grok/Composer の argv prompt は
3189
+ // 第3要素は「起動時点でturnが走っているか」の event_cursor: Grok/Composer/Cursor の argv prompt は
3040
3190
  // event file 新規作成直後の起動=境界0、prompt なしの起動は turn なし=null。
3041
3191
  if (!prompt || (kind !== "codex" && kind !== "claude")) {
3042
3192
  const [sid, hint] = openAgent(kind, {
@@ -3050,7 +3200,7 @@ export async function openAgentWithInitialPrompt(kind, opts = {}) {
3050
3200
  write_scope: opts.write_scope,
3051
3201
  env_vars: opts.env_vars,
3052
3202
  });
3053
- // argv prompt(grok/composer)は composer を経由しないため submit 座礁観測の対象外。
3203
+ // argv prompt(grok/composer/cursor)は composer を経由しないため submit 座礁観測の対象外。
3054
3204
  return [sid, hint, prompt ? 0 : null, null];
3055
3205
  }
3056
3206
  const [sid, hint] = openAgent(kind, {