aiterm-mcp 0.27.9 → 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.
@@ -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
@@ -14,13 +14,15 @@ import { createHash, randomBytes } from "node:crypto";
14
14
  import * as rtk from "./rtk.js";
15
15
  import { AitermError, telemetryOwnedFailure, ownTelemetryFailure } from "./errors.js";
16
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, agentLineageFields, } from "./agent-shared.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
  }
@@ -923,6 +926,7 @@ export function listSessions() {
923
926
  return line;
924
927
  const agent = [
925
928
  `agent=${meta.kind}`,
929
+ `harness=${agentHarness(meta.kind)}`,
926
930
  "agent_done=true",
927
931
  meta.write_scope === undefined ? null : `write_scope=${JSON.stringify(meta.write_scope)}`,
928
932
  meta.vendor_session_id ? `vendor_session_id=${meta.vendor_session_id}` : null,
@@ -1584,7 +1588,7 @@ function loadAgentMetadata(name) {
1584
1588
  throw new AitermError(`agent metadata を読めません: ${e.message}`, 2);
1585
1589
  }
1586
1590
  const m = raw;
1587
- 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") ||
1588
1592
  m.aiterm_session !== name ||
1589
1593
  typeof m.launch_id !== "string" ||
1590
1594
  !LAUNCH_ID_RE.test(m.launch_id)) {
@@ -1655,6 +1659,31 @@ function loadAgentMetadata(name) {
1655
1659
  codex_home: recordedHome,
1656
1660
  };
1657
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
+ }
1658
1687
  // grok_home も codex_home と同じ理由で per-launch の記録値を正とする(席専用 GROK_HOME)。
1659
1688
  // 認証正本の照合も記録された home に対して行う——現在の env の home と比較すると、
1660
1689
  // 席専用 home で起動した session が別 instance から拒否される。
@@ -1791,6 +1820,15 @@ function bindCompletedInitialPrompt(meta) {
1791
1820
  setInitialPromptState(meta, "done");
1792
1821
  return;
1793
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
+ }
1794
1832
  if ((meta.kind === "grok" || meta.kind === "composer") && meta.completion_route === "grok_transcript") {
1795
1833
  if (!latestGrokCompletion(meta, readTranscriptLines)) {
1796
1834
  throw new AitermError(`agent session '${meta.aiterm_session}' は起動時 prompt の完了待ちです。${agentWaitGuide(meta.aiterm_session)}`, 2);
@@ -1828,6 +1866,9 @@ function tryLoadAgentMetadata(name) {
1828
1866
  function latestAgentDoneEvent(meta, expectedOperationId = null) {
1829
1867
  if (meta.kind === "codex")
1830
1868
  return latestCodexCompletion(meta, readTranscriptLines);
1869
+ if (meta.kind === "cursor" && meta.completion_route === "cursor_transcript") {
1870
+ return latestCursorCompletion(meta, readTranscriptLines);
1871
+ }
1831
1872
  if ((meta.kind === "grok" || meta.kind === "composer") && meta.completion_route === "grok_transcript") {
1832
1873
  return latestGrokCompletion(meta, readTranscriptLines);
1833
1874
  }
@@ -1893,6 +1934,14 @@ function recoverAgentVendorSession(meta) {
1893
1934
  setInitialPromptState(meta, "done");
1894
1935
  return;
1895
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
+ }
1896
1945
  const size = safeStatSize(meta.event_file);
1897
1946
  if (size === 0)
1898
1947
  return;
@@ -1916,6 +1965,10 @@ function agentCompletionCursor(meta) {
1916
1965
  const transcript = grokEventsTranscript(meta);
1917
1966
  return transcript ? safeStatSize(transcript) : 0;
1918
1967
  }
1968
+ if (meta.kind === "cursor" && meta.completion_route === "cursor_transcript") {
1969
+ bindCursorTranscriptSession(meta);
1970
+ return cursorTurnBoundary(meta);
1971
+ }
1919
1972
  if (meta.kind !== "codex")
1920
1973
  return safeStatSize(meta.event_file);
1921
1974
  const transcript = bindCodexTranscriptSession(meta);
@@ -1929,7 +1982,7 @@ function transcriptUnavailable() {
1929
1982
  throw new AitermError(`transcript がまだありません。ターン完了後に再取得してください。${agentWaitGuide()}`, 2);
1930
1983
  }
1931
1984
  function transcriptNotFound(vendor) {
1932
- throw new AitermError(`最終 assistant メッセージを特定できませんでした(vendor=${vendor})。screen で確認してください。`, 2);
1985
+ throw new AitermError(`最終 assistant メッセージを特定できませんでした(harness=${agentHarness(vendor)}, vendor=${vendor})。screen で確認してください。`, 2);
1933
1986
  }
1934
1987
  function readTranscriptLines(file) {
1935
1988
  try {
@@ -2001,6 +2054,9 @@ export async function readAgentTranscript(name, o = {}) {
2001
2054
  transcriptUnavailable();
2002
2055
  text = readClaudeResultText(meta, done, operationId, transcriptUnavailable);
2003
2056
  }
2057
+ else if (meta.kind === "cursor") {
2058
+ text = cursorTranscriptText(meta, readTranscriptLines, transcriptUnavailable);
2059
+ }
2004
2060
  else if (meta.kind === "codex") {
2005
2061
  text = codexTranscriptText(meta, turnId, readTranscriptLines, transcriptUnavailable);
2006
2062
  }
@@ -2017,6 +2073,7 @@ export async function readAgentTranscript(name, o = {}) {
2017
2073
  "agent_transcript",
2018
2074
  `vendor=${meta.kind}`,
2019
2075
  `turn_id=${turnId ?? "unknown"}`,
2076
+ `harness=${agentHarness(meta.kind)}`,
2020
2077
  done?.operation_id ? `operation_id=${done.operation_id}` : null,
2021
2078
  `raw_chars=${rawChars}`,
2022
2079
  ].filter(Boolean).join(" ");
@@ -2054,6 +2111,7 @@ function agentReadMetadataSuffix(name, screen) {
2054
2111
  const bits = [
2055
2112
  "agent",
2056
2113
  `vendor=${meta.kind}`,
2114
+ `harness=${agentHarness(meta.kind)}`,
2057
2115
  `initial_prompt=${meta.initial_prompt}`,
2058
2116
  `agent_event_seen=${ev ? "true" : "false"}`,
2059
2117
  "completion_attribution=none",
@@ -2145,7 +2203,7 @@ export function detectAgentRateLimit(kind, aitermSession) {
2145
2203
  return null;
2146
2204
  }
2147
2205
  // 外部waiterプロセス用の純リーダー観測。lock・PTY・metadata書込・dispatch状態には一切触れない。
2148
- // Codexはrollout transcript、他vendorはevent fileを増分走査し、vendor_session_idのbind永続化を
2206
+ // Codex/Cursor/Grokは各harnessのtranscript、Claudeはevent fileを増分走査し、vendor_session_idのbind永続化を
2149
2207
  // 行わない(waiterは観測者であって所有者でない)。
2150
2208
  export async function observeAgentDone(name, o = {}) {
2151
2209
  const meta = loadAgentMetadata(name);
@@ -2154,12 +2212,15 @@ export async function observeAgentDone(name, o = {}) {
2154
2212
  throw new AitermError("operation_id はClaude agent sessionだけで使用できます", 2);
2155
2213
  }
2156
2214
  if (o.cursor != null && (!Number.isInteger(o.cursor) || o.cursor < 0)) {
2157
- throw new AitermError("cursor は0以上の整数byte offsetで指定してください", 2);
2215
+ throw new AitermError("cursor は0以上の整数完了境界で指定してください", 2);
2158
2216
  }
2159
2217
  const timeout = o.timeout ?? DEFAULT_AGENT_DONE_TIMEOUT;
2160
2218
  if (meta.kind === "codex" && meta.completion_route === "codex_transcript") {
2161
2219
  return observeCodexDone(meta, timeout, o.cursor, detectAgentRateLimit);
2162
2220
  }
2221
+ if (meta.kind === "cursor" && meta.completion_route === "cursor_transcript") {
2222
+ return observeCursorDone(meta, timeout, o.cursor, detectAgentRateLimit);
2223
+ }
2163
2224
  if ((meta.kind === "grok" || meta.kind === "composer") && meta.completion_route === "grok_transcript") {
2164
2225
  return observeGrokDone(meta, timeout, o.cursor, detectAgentRateLimit);
2165
2226
  }
@@ -2176,6 +2237,7 @@ export async function observeAgentDone(name, o = {}) {
2176
2237
  session_id: meta.aiterm_session,
2177
2238
  launch_id: meta.launch_id,
2178
2239
  vendor: meta.kind,
2240
+ harness: agentHarness(meta.kind),
2179
2241
  outcome,
2180
2242
  operation_id: ev?.operation_id ?? operationId,
2181
2243
  vendor_session_id: ev?.vendor_session_id ?? meta.vendor_session_id ?? null,
@@ -2225,19 +2287,27 @@ function isAgentTuiReady(kind, screen) {
2225
2287
  return claudeTuiReady(screen);
2226
2288
  if (kind === "codex")
2227
2289
  return codexTuiReady(screen);
2290
+ if (kind === "cursor")
2291
+ return cursorTuiReady(screen);
2228
2292
  return grokTuiReady(screen);
2229
2293
  }
2230
- // Codex/Claude は実行中に「(esc to interrupt)」を表示する(実機採取)。startup 側の処理
2231
- // (MCP initialize 等)が走ったまま composer だけ描画されている画面は入力受付とみなさない。
2294
+ // Codex/Claude は実行中に「(esc to interrupt)」、Cursor は「Working」+
2295
+ // 「ctrl+c to stop」を表示する(いずれも実機採取)。startup 側の処理(MCP initialize 等)が
2296
+ // 走ったまま古い composer がscrollbackに残る画面は入力受付とみなさない。
2232
2297
  // Grok/Composer は busy 表示文字列の実機根拠が未採取のため対象外(誤ブロックで起動不能にしない)。
2233
- const AGENT_TUI_BUSY_KINDS = new Set(["codex", "claude"]);
2234
- 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
+ }
2235
2305
  // ready gate 用: 入力欄マーカーがあっても busy 表示中は ready と数えない。
2236
2306
  // frontend 推定(inferAgentFrontend)は「agent TUI が前面か」を見るだけなので isAgentTuiReady のまま。
2237
2307
  function isAgentTuiIdleReady(kind, screen) {
2238
2308
  if (!isAgentTuiReady(kind, screen))
2239
2309
  return false;
2240
- return !(AGENT_TUI_BUSY_KINDS.has(kind) && AGENT_TUI_BUSY_RE.test(screen));
2310
+ return !isAgentTuiBusy(kind, screen);
2241
2311
  }
2242
2312
  async function waitAgentTuiReadyImpl(kind, sample, sleepFn, opts = {}) {
2243
2313
  const timeoutMs = opts.timeoutMs ?? AGENT_TUI_READY_TIMEOUT_MS;
@@ -2286,11 +2356,21 @@ function agentSubmitResidueTail(text) {
2286
2356
  return cps.slice(-AGENT_SUBMIT_RESIDUE_TAIL_CHARS).join("");
2287
2357
  }
2288
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;
2289
2363
  const lines = screen.split("\n");
2290
2364
  // 入力欄マーカーは ready 判定と同じ記号を行頭基準で探す。submit 済みの transcript echo は
2291
2365
  // マーカー行より上に出るため、最後のマーカー行以降だけを composer 領域として見る。
2292
2366
  // grok/composer は Windows native 描画(`>`・実測 1.0.4)も ready 判定と同様に受ける。
2293
- 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;
2294
2374
  let markerIdx = -1;
2295
2375
  for (let i = lines.length - 1; i >= 0; i--) {
2296
2376
  if (markerRe.test(lines[i])) {
@@ -2429,7 +2509,7 @@ export async function sendInitialAgentPrompt(name, text, o = {}) {
2429
2509
  const ready = await waitAgentTuiReady(name, meta, o.ready_timeout ?? AGENT_TUI_READY_TIMEOUT_MS);
2430
2510
  if (!ready.ready) {
2431
2511
  return {
2432
- 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` +
2433
2513
  `agent session '${name}' の ${agentLabel(meta.kind)} TUI が入力受付状態になりません。prompt は送信していません。`,
2434
2514
  event_cursor: null,
2435
2515
  submit_residue: null,
@@ -2450,7 +2530,7 @@ export async function sendInitialAgentPrompt(name, text, o = {}) {
2450
2530
  }
2451
2531
  const residue = await detectAgentSubmitResidue(name, meta.kind, text);
2452
2532
  return {
2453
- 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` +
2454
2534
  `起動時 prompt を送信した。${agentDispatchGuide(name, startOffset)}` +
2455
2535
  agentSubmitResidueWarning(name, residue.residue),
2456
2536
  event_cursor: startOffset,
@@ -2543,6 +2623,48 @@ export async function configureAgent(name, opts) {
2543
2623
  schema: "aiterm.agent-configure-result.v1",
2544
2624
  session_id: name,
2545
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"),
2546
2668
  model,
2547
2669
  reasoning_effort: effort,
2548
2670
  };
@@ -2564,6 +2686,7 @@ export async function configureAgent(name, opts) {
2564
2686
  schema: "aiterm.agent-configure-result.v1",
2565
2687
  session_id: name,
2566
2688
  provider: meta.kind,
2689
+ harness: agentHarness(meta.kind),
2567
2690
  model,
2568
2691
  reasoning_effort: effort,
2569
2692
  };
@@ -2602,6 +2725,7 @@ export async function configureAgent(name, opts) {
2602
2725
  schema: "aiterm.agent-configure-result.v1",
2603
2726
  session_id: name,
2604
2727
  provider: "codex",
2728
+ harness: agentHarness("codex"),
2605
2729
  model,
2606
2730
  reasoning_effort: effort,
2607
2731
  };
@@ -2636,6 +2760,11 @@ export async function dispatchAgentTurn(name, text, o = {}) {
2636
2760
  }
2637
2761
  }
2638
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;
2639
2768
  if (meta.kind === "claude") {
2640
2769
  // durable/anonymousを分岐する前に同じsend preflightを通す。拒否されるpromptの
2641
2770
  // receipt/active markerだけを残して、来ないStopを待つ状態を作らない。
@@ -2646,7 +2775,7 @@ export async function dispatchAgentTurn(name, text, o = {}) {
2646
2775
  else
2647
2776
  reserveAnonymousClaudeTurn(meta);
2648
2777
  }
2649
- send(name, text, {
2778
+ send(name, dispatchText, {
2650
2779
  enter: false,
2651
2780
  force: o.force,
2652
2781
  raw: o.raw,
@@ -2658,12 +2787,13 @@ export async function dispatchAgentTurn(name, text, o = {}) {
2658
2787
  // Codex TUI は literal text 投入直後の Enter を取り落とすことがある。agent 経路だけ submit を分離する。
2659
2788
  await sleep(AGENT_SUBMIT_DELAY_MS);
2660
2789
  sendKey(name, "Enter", { preserveAgentOperation: meta.kind === "claude" });
2661
- const residue = await detectAgentSubmitResidue(name, meta.kind, text);
2790
+ const residue = await detectAgentSubmitResidue(name, meta.kind, dispatchText);
2662
2791
  return {
2663
2792
  schema: "aiterm.agent-dispatch.v1",
2664
2793
  session_id: meta.aiterm_session,
2665
2794
  launch_id: meta.launch_id,
2666
2795
  vendor: meta.kind,
2796
+ harness: agentHarness(meta.kind),
2667
2797
  event_cursor: startOffset,
2668
2798
  operation_id: operationId,
2669
2799
  submit_residue: residue.residue,
@@ -2777,6 +2907,8 @@ function buildAgentCmd(kind, bin, model, effort, prompt, meta = null) {
2777
2907
  return buildClaudeAgentCmd(bin, model, effort, prompt, meta);
2778
2908
  if (kind === "codex")
2779
2909
  return buildCodexAgentCmd(bin, model, effort, prompt, meta);
2910
+ if (kind === "cursor")
2911
+ return buildCursorAgentCmd(bin, model, effort, prompt, meta);
2780
2912
  return buildGrokAgentCmd(kind, bin, model, effort, prompt, meta);
2781
2913
  }
2782
2914
  function agentEnvPrefix(meta, sid, envVars = []) {
@@ -2799,7 +2931,7 @@ function agentEnvPrefix(meta, sid, envVars = []) {
2799
2931
  `AITERM_AGENT_LINEAGE=${shq(meta.lineage ?? `host-root>${meta.kind}:${sid}`)}`,
2800
2932
  `AITERM_AGENT_DELEGATION_ALLOWED=${shq(meta.delegation_allowed === true ? "true" : "false")}`,
2801
2933
  ];
2802
- if (meta.kind === "claude" || meta.kind === "codex")
2934
+ if (meta.kind === "claude" || meta.kind === "codex" || meta.kind === "cursor")
2803
2935
  return common;
2804
2936
  return [...grokEnvTokens(meta), ...common];
2805
2937
  })();
@@ -2808,6 +2940,8 @@ function agentEnvPrefix(meta, sid, envVars = []) {
2808
2940
  function buildAgentLaunchNote(kind, model, effort, meta) {
2809
2941
  if (kind === "claude")
2810
2942
  return claudeLaunchNote(model, effort, meta);
2943
+ if (kind === "cursor")
2944
+ return cursorLaunchNote(model, effort, meta);
2811
2945
  if (kind !== "codex")
2812
2946
  return grokLaunchNote(kind, model, effort, meta);
2813
2947
  return codexLaunchNote(model, effort, meta);
@@ -2862,6 +2996,8 @@ export function openAgent(kind, opts = {}) {
2862
2996
  if (effort && kind === "claude" && !CLAUDE_EFFORTS.has(effort)) {
2863
2997
  throw new AitermError("Claude Code の reasoning_effort は low/medium/high/xhigh/max のいずれかです", 2);
2864
2998
  }
2999
+ if (kind === "cursor")
3000
+ validateCursorModelEffort(model, effort);
2865
3001
  const agentDone = !!opts.agent_done;
2866
3002
  const launchOperationId = opts.launch_operation_id == null
2867
3003
  ? null
@@ -2891,7 +3027,13 @@ export function openAgent(kind, opts = {}) {
2891
3027
  ownTelemetryFailure("AITERM.VENDOR_LAUNCHER_FAILED", error, 2);
2892
3028
  }
2893
3029
  if (!bin) {
2894
- 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";
2895
3037
  ownTelemetryFailure("AITERM.VENDOR_LAUNCHER_FAILED", new AitermError(`${label} の CLI が見つかりません(${where} か PATH が必要)`, 2), 2);
2896
3038
  }
2897
3039
  // cwd 検証(session を作る前に。cd 失敗はシェル内で静かに死に「起動した」と偽成功を返すため)。
@@ -2931,6 +3073,8 @@ export function openAgent(kind, opts = {}) {
2931
3073
  }
2932
3074
  if (kind === "claude")
2933
3075
  assertClaudeAuthenticationReady(bin);
3076
+ if (kind === "cursor")
3077
+ assertCursorAuthenticationReady(bin);
2934
3078
  // Windows の起動コマンドは native psmux pane の Git Bash で走る(WSL 橋・/mnt/c 変換は
2935
3079
  // e3f5fc8 で全廃。bin/cwd は Windows パスの forward slash 形のまま渡す)。
2936
3080
  // 未検証リスク: npm グローバル導入の codex.cmd/.bat シムの対話 TUI 描画は実 Windows でしか確認
@@ -2952,6 +3096,9 @@ export function openAgent(kind, opts = {}) {
2952
3096
  if (requestedModel)
2953
3097
  assertGrokModelAvailable(bin, cwd ?? process.cwd(), requestedModel);
2954
3098
  }
3099
+ if (kind === "cursor" && model) {
3100
+ assertCursorModelAvailable(bin, cwd ?? process.cwd(), model, effort);
3101
+ }
2955
3102
  let sid;
2956
3103
  let hint;
2957
3104
  try {
@@ -2972,7 +3119,9 @@ export function openAgent(kind, opts = {}) {
2972
3119
  ? createClaudeAgentMetadata(sid, cwd, opts.prompt ? "pending" : "none", launchOperationId, launchRequestDigest, lineageContext, model, effort)
2973
3120
  : kind === "codex"
2974
3121
  ? createCodexAgentMetadata(sid, cwd, opts.prompt ? "pending" : "none", { model, effort }, writeScope, lineageContext)
2975
- : 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)
2976
3125
  : null;
2977
3126
  if (meta)
2978
3127
  agentMetadataNegativeCache.delete(sid);
@@ -3003,7 +3152,7 @@ export function openAgent(kind, opts = {}) {
3003
3152
  }
3004
3153
  throw failure;
3005
3154
  }
3006
- const driveHint = agentDone && kind === "claude"
3155
+ const driveHint = agentDone
3007
3156
  ? `TUI の描画には数秒かかる。少し置いてから pty_read(${sid}, screen:true) で画面を読み、` +
3008
3157
  `turnはpty_send(${sid}, "...")で送る(自動で非ブロックdispatch=投げっぱなしでよい・完了通知はaiterm-wait)。中断はpty_key(${sid}, "C-c")、` +
3009
3158
  `Stopが来ない場合の解除はpty_close(${sid})を使う。`
@@ -3037,7 +3186,7 @@ export async function openAgentWithInitialPrompt(kind, opts = {}) {
3037
3186
  }
3038
3187
  // v0.16.0: launcher は常に managed(Stop hook つき)で立つ。手動運転したい場合は
3039
3188
  // pty_open で素の PTY を開き、vendor CLI を自分で send する。
3040
- // 第3要素は「起動時点でturnが走っているか」の event_cursor: Grok/Composer の argv prompt は
3189
+ // 第3要素は「起動時点でturnが走っているか」の event_cursor: Grok/Composer/Cursor の argv prompt は
3041
3190
  // event file 新規作成直後の起動=境界0、prompt なしの起動は turn なし=null。
3042
3191
  if (!prompt || (kind !== "codex" && kind !== "claude")) {
3043
3192
  const [sid, hint] = openAgent(kind, {
@@ -3051,7 +3200,7 @@ export async function openAgentWithInitialPrompt(kind, opts = {}) {
3051
3200
  write_scope: opts.write_scope,
3052
3201
  env_vars: opts.env_vars,
3053
3202
  });
3054
- // argv prompt(grok/composer)は composer を経由しないため submit 座礁観測の対象外。
3203
+ // argv prompt(grok/composer/cursor)は composer を経由しないため submit 座礁観測の対象外。
3055
3204
  return [sid, hint, prompt ? 0 : null, null];
3056
3205
  }
3057
3206
  const [sid, hint] = openAgent(kind, {