aiterm-mcp 0.58.4 → 0.59.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,6 +8,7 @@ import { randomBytes } from "node:crypto";
8
8
  import { AitermError } from "../errors.js";
9
9
  import { spawnAgentControlCommand } from "../agent-resolver.js";
10
10
  import { checkedCatalog } from "../model-catalog.js";
11
+ import { FastModeError } from "../fast-mode.js";
11
12
  import { AGENT_DONE_POLL_MS, CURSOR_SCREEN_POLL_MS, pollGate, AGENT_EVENT_MAX_BYTES, agentEventPath, agentHarness, agentLineageFields, agentMetadataPath, createEmpty0600, readFileRange, safeStatSize, shq, sleep, subagentInstruction, writeAgentMetadata, writeScopeLaunchNote, } from "../agent-shared.js";
12
13
  const CURSOR_TRANSCRIPT_MATCH_MAX_BYTES = 1024 * 1024;
13
14
  const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
@@ -350,10 +351,15 @@ export function validateCursorModelEffort(model, effort) {
350
351
  throw new AitermError("Cursor CLIのmodelへ既にparameter overrideがあります。reasoning_effortとの二重指定はできません", 2);
351
352
  }
352
353
  }
353
- export function cursorModelArgument(model, effort) {
354
+ export function cursorModelArgument(model, effort, fast = false) {
354
355
  if (!model)
355
356
  return null;
356
- return effort ? `${model}-${effort}` : model;
357
+ // fastは完成形のIDの末尾(`<model>[-<effort>]-fast`)。そのIDが一覧にあるかは assertCursorModelAvailable が見る。
358
+ return `${model}${effort ? `-${effort}` : ""}${fast ? "-fast" : ""}`;
359
+ }
360
+ /** modelへ直に書かれたIDが、fast版の完成形か(`composer-2.5-fast`、`gpt-5.3-codex-low-fast`)。 */
361
+ export function cursorModelIsFastVariant(model) {
362
+ return !!model && model.endsWith("-fast");
357
363
  }
358
364
  const CURSOR_AUTH_TIMEOUT_MS = 5_000;
359
365
  const CURSOR_MODELS_TIMEOUT_MS = 5_000;
@@ -426,11 +432,15 @@ export function cursorModelChoices(bin, cwd) {
426
432
  export function cursorCatalogFromLines(lines) {
427
433
  const choices = new Map();
428
434
  let defaultModel = null;
435
+ // fast: true で起こす時に渡すID。鍵は `${model}` と `${model}-${effort}`、値は一覧にある `-fast` 付きの完成形。
436
+ const fastIds = {};
429
437
  for (const line of lines) {
430
438
  const parsed = cursorBaseModel(line.id);
431
439
  if (!parsed)
432
440
  continue;
433
441
  const choice = choices.get(parsed.base) ?? { efforts: new Set(), label: null };
442
+ if (parsed.fast)
443
+ fastIds[parsed.effort ? `${parsed.base}-${parsed.effort}` : parsed.base] = line.id;
434
444
  // -fast版だけにあるeffortは `${model}-${effort}` の連結で作れないので数えない。
435
445
  if (parsed.effort && !parsed.fast)
436
446
  choice.efforts.add(parsed.effort);
@@ -452,13 +462,19 @@ export function cursorCatalogFromLines(lines) {
452
462
  efforts: CURSOR_SELECTABLE_EFFORTS.filter((effort) => choice.efforts.has(effort)),
453
463
  default_effort: null,
454
464
  hidden: false,
465
+ // そのmodelのどれかの組(effortなし、またはeffort付き)にfast版がある。組ごとの有無は起動時に一覧で確かめる。
466
+ fast: Object.keys(fastIds).some((key) => key === id || key.startsWith(`${id}-`)),
455
467
  })),
468
+ fast_ids: fastIds,
456
469
  });
457
470
  }
458
- export function assertCursorModelAvailable(bin, cwd, model, effort) {
459
- const effective = cursorModelArgument(model, effort);
471
+ export function assertCursorModelAvailable(bin, cwd, model, effort, fast = false) {
472
+ const effective = cursorModelArgument(model, effort, fast);
460
473
  const models = cursorModelCatalog(bin, cwd);
461
- const available = effort ? models.includes(effective) : models.includes(model) || models.some((id) => id.startsWith(`${model}-`));
474
+ if (fast && !models.includes(effective)) {
475
+ throw new FastModeError("FAST_MODE_NOT_SUPPORTED", `Cursor の一覧に ${JSON.stringify(effective)} がありません(model=${model}${effort ? `、reasoning_effort=${effort}` : ""} のfast版)。起動していません。`);
476
+ }
477
+ const available = effort || fast ? models.includes(effective) : models.includes(model) || models.some((id) => id.startsWith(`${model}-`));
462
478
  if (!available) {
463
479
  throw new AitermError(`Cursor model catalog に ${JSON.stringify(effective)} がありません。` +
464
480
  "modelとreasoning_effortから作る正規IDが存在しないため、別modelへfallbackせず中止しました", 2);
@@ -468,7 +484,7 @@ export function cursorEffortNavigation(screen, effort) {
468
484
  const label = CURSOR_EFFORT_LABELS[effort.toLowerCase()];
469
485
  if (!label)
470
486
  throw new AitermError(`Cursorのreasoning_effort ${JSON.stringify(effort)} は未対応です`, 2);
471
- const choices = screen.split("\n").flatMap((line) => {
487
+ const choices = cursorParameterLines(screen).flatMap((line) => {
472
488
  const match = line.match(/^\s*(→\s*)?[●○◯]\s+(.+?)(?:\s+✓)?\s*$/);
473
489
  return match ? [{ selected: !!match[1], label: match[2] }] : [];
474
490
  });
@@ -496,10 +512,10 @@ export function assertCursorAuthenticationReady(bin) {
496
512
  "通常端末で `agent status` が成功することを確認してください。", 2);
497
513
  }
498
514
  /** 起動argv(先頭が実行ファイル)。paneのshellに合わせた引用は呼び出し側が行う。 */
499
- export function cursorAgentArgv(bin, model, effort, prompt, meta) {
515
+ export function cursorAgentArgv(bin, model, effort, prompt, meta, fast = false) {
500
516
  validateCursorModelEffort(model, effort);
501
517
  const parts = [bin];
502
- const modelArg = cursorModelArgument(model, effort);
518
+ const modelArg = cursorModelArgument(model, effort, fast);
503
519
  if (modelArg)
504
520
  parts.push("--model", modelArg);
505
521
  // managed agentは人の対話承認を待てない。Cursor公式の無人運転フラグで
@@ -511,9 +527,9 @@ export function cursorAgentArgv(bin, model, effort, prompt, meta) {
511
527
  parts.push(meta ? cursorPromptWithLineage(meta, prompt) : prompt);
512
528
  return parts;
513
529
  }
514
- export function buildCursorAgentCmd(bin, model, effort, prompt, meta) {
530
+ export function buildCursorAgentCmd(bin, model, effort, prompt, meta, fast = false) {
515
531
  // 固定の語彙(--force、--mode ask等)は引用しない。実行ファイル、model、promptだけを引用する。
516
- const argv = cursorAgentArgv(bin, model, effort, prompt, meta);
532
+ const argv = cursorAgentArgv(bin, model, effort, prompt, meta, fast);
517
533
  return argv.map((part, index) => index === 0 || argv[index - 1] === "--model" || (prompt !== null && index === argv.length - 1) ? shq(part) : part).join(" ");
518
534
  }
519
535
  /**
@@ -551,8 +567,11 @@ const CURSOR_START_PROMPT_MARKER_RE = /(?:^|\n)\s*(?:→|->)\s*Plan,\s*search,\s
551
567
  export function cursorTuiReady(screen) {
552
568
  if (CURSOR_FOLLOWUP_MARKER_RE.test(screen))
553
569
  return true;
554
- return /Cursor Agent/i.test(screen) &&
555
- (CURSOR_COMPOSER_MARKER_RE.test(screen) || CURSOR_START_PROMPT_MARKER_RE.test(screen));
570
+ // 入力欄の案内(Plan, search, build anything)はCursorの入力欄だけが出す文なので、見出し(Cursor Agent)が画面の外へ流れた後も
571
+ // 入力待ちと読む。まだ番を流していない席で `/model` の選ぶ画面を開いて閉じると、見出しは流れて戻らない(2026.10.01)。
572
+ if (CURSOR_START_PROMPT_MARKER_RE.test(screen))
573
+ return true;
574
+ return /Cursor Agent/i.test(screen) && CURSOR_COMPOSER_MARKER_RE.test(screen);
556
575
  }
557
576
  // 利用上限に達したCursorは、入力欄も戻さずに説明を出して止まる(2026.09.26-dd393fe、macOSで実測)。
558
577
  // transcriptには、user turnを残さずにturn_ended(status "error")だけを書く事がある(実記録で8件)。その時は完了の条件
@@ -642,3 +661,68 @@ export function cursorAuthPane(screen) {
642
661
  return { url: authUrl(screen, ["cursor.com", "www.cursor.com", "auth.cursor.com"]),
643
662
  user_code: null, input_required: false, message: null };
644
663
  }
664
+ /**
665
+ * `/model` の画面の今の姿を、画面の最後の行(操作の案内)で読む。list は選ぶ画面、parameters はパラメータの画面、null は閉じている。
666
+ * Cursorは選ぶ画面を閉じても前の行を消さないので(上に残る)、文字の有無では今の姿が分からない。
667
+ */
668
+ export function cursorPickerView(screen) {
669
+ const last = screen.split("\n").map((line) => line.trim()).filter(Boolean).at(-1) ?? "";
670
+ if (/Enter to select\s*•\s*Esc to go back/.test(last))
671
+ return "parameters";
672
+ if (/Enter to select\s*•\s*Tab to edit/.test(last))
673
+ return "list";
674
+ return null;
675
+ }
676
+ /** 今開いているパラメータの画面の行(最後の「Edit Parameters」の見出しより後)。見出しが無ければ画面の全部。 */
677
+ function cursorParameterLines(screen) {
678
+ const lines = screen.split("\n");
679
+ let title = lines.length - 1;
680
+ while (title >= 0 && !lines[title].includes("Edit Parameters"))
681
+ title--;
682
+ return lines.slice(title + 1);
683
+ }
684
+ /** 今開いているパラメータの画面の選べる行。cursor は選択中(→)の行、label は印と✓を除いた名前(Low・High・Fast 等)。 */
685
+ export function cursorParameterRows(screen) {
686
+ return cursorParameterLines(screen).flatMap((line) => {
687
+ const match = line.trim().match(/^(→\s*)?[●○◯◉]\s+(.+?)(?:\s+✓)?\s*$/u);
688
+ return match ? [{ label: match[2], cursor: !!match[1] }] : [];
689
+ });
690
+ }
691
+ /** 今開いているパラメータの画面で、そのeffortの行に選択済みの印(✓)が付いているか。 */
692
+ export function cursorEffortSelected(screen, label) {
693
+ return cursorParameterLines(screen).some((line) => new RegExp(`[●○◯]\\s+${label.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s+✓\\s*$`).test(line));
694
+ }
695
+ // ---- ファスト。Cursorは `/model` のパラメータの画面(Edit Parameters)に「Fast」の行がある(2026.10.01)。
696
+ // 入っていない: 「◯ Fast」 / 入っている: 「◉ Fast」。行が無いmodelはfastへ切り替えられない。
697
+ /** パラメータの画面のFastの行。state は今の入り切り、down は選択中(→)の行からFastの行までの下矢印の数。 */
698
+ export function cursorFastParameter(screen) {
699
+ if (!screen.includes("Edit Parameters"))
700
+ return null;
701
+ // 画面は行ごとに横へずれて描かれる事がある。行の中身だけを見る。
702
+ const rows = cursorParameterLines(screen).map((line) => line.trim()).filter((line) => /^(?:→\s*)?[●○◯◉]\s+\S/u.test(line));
703
+ const fast = rows.findIndex((line) => /^(?:→\s*)?[◯◉]\s+Fast\s*$/u.test(line));
704
+ const selected = rows.findIndex((line) => line.startsWith("→"));
705
+ if (fast < 0 || selected < 0)
706
+ return null;
707
+ return { state: rows[fast].includes("◉"), down: fast - selected };
708
+ }
709
+ /** 見出しのmodelの表示(「Codex 5.3 Low Fast」)が、ファストが入っている形か。 */
710
+ export function cursorHeaderShowsFast(screen) {
711
+ // 見出しは「<modelの表示> <権限の表示>」の1行。権限の表示は Run Everything/Ask 等(画面の幅で後ろが切れる事がある)。
712
+ const header = screen.split("\n").map((line) => line.trim())
713
+ .filter((line) => /\S\s{2,}(?:Run|Auto-Run|Ask|Plan|Manual)\b/.test(line)).at(-1);
714
+ if (!header)
715
+ return null;
716
+ return /\bFast\s{2,}/.test(header);
717
+ }
718
+ /**
719
+ * Cursorが、起動の --model と `/model` の選択のたびに、利用者の ~/.cursor/cli-config.json へ書く項目(次の会話の既定のmodelとパラメータ)。
720
+ * 書く版と書かない版がある(2026.10.01-14929f9は書く。fastを入れた選択なら `selectedModel.parameters` に fast: true が残る)。
721
+ * 席だけの切り替えにするため、切り替えの直前の値へ戻す。
722
+ */
723
+ export const CURSOR_DEFAULT_MODEL_KEYS = [
724
+ "model", "hasChangedDefaultModel", "modelParameters", "selectedModel", "modelSelectionHistory", "maxMode",
725
+ ];
726
+ export function cursorUserConfigPath(cursorHome) {
727
+ return path.join(cursorHome, "cli-config.json");
728
+ }
@@ -117,16 +117,41 @@ export function grokCatalogFromInitialize(stdout, summary = "") {
117
117
  // 候補に無い既定値は黙って捨てず、checkedCatalogで形式異常にする。
118
118
  default_effort: efforts.length > 0 ? defaultEffort : null,
119
119
  hidden: false,
120
+ fast: false,
120
121
  };
121
122
  });
123
+ // Grokのfastは別のmodel(表示名が「<元の表示名> Fast」。1.0.50: Grok 4.7 と Grok 4.7 Fast=grok-4.7-build-fast)。
124
+ // fast版は別のmodelとして並べず、元のmodelの fast: true にまとめる。元のmodelが一覧に無いfast版は、そのまま1つのmodelとして残す。
125
+ const fastIds = grokFastVariants(models);
126
+ const variants = new Set(Object.values(fastIds));
127
+ const current = typeof state.currentModelId === "string" ? state.currentModelId : null;
128
+ const baseOf = (id) => Object.keys(fastIds).find((base) => fastIds[base] === id) ?? id;
122
129
  return checkedCatalog("Grok", {
123
130
  source: "grok agent stdio initialize (_meta.modelState)",
124
131
  harness_version: typeof meta.agentVersion === "string" ? meta.agentVersion : null,
125
- default_model: typeof state.currentModelId === "string" ? state.currentModelId : null,
132
+ default_model: baseOf(current),
126
133
  adapter_efforts: {},
127
- models,
134
+ models: models.filter((model) => !variants.has(model.id))
135
+ .map((model) => ({ ...model, fast: model.id in fastIds })),
136
+ fast_ids: fastIds,
128
137
  });
129
138
  }
139
+ /** 元のmodel ID → fast版のmodel ID。表示名が「<元の表示名> Fast」のmodelを、その元のmodelのfast版と読む。 */
140
+ export function grokFastVariants(models) {
141
+ const byName = new Map(models.flatMap((model) => model.display_name ? [[model.display_name, model.id]] : []));
142
+ const pairs = {};
143
+ for (const model of models) {
144
+ const match = model.display_name?.match(/^(.+) Fast$/);
145
+ const base = match ? byName.get(match[1]) : undefined;
146
+ if (base && base !== model.id)
147
+ pairs[base] = model.id;
148
+ }
149
+ return pairs;
150
+ }
151
+ /** model IDが、一覧の上でほかのmodelのfast版なら、その元のmodel ID。 */
152
+ export function grokFastBase(catalog, model) {
153
+ return Object.keys(catalog.fast_ids ?? {}).find((base) => catalog.fast_ids?.[base] === model) ?? null;
154
+ }
130
155
  export function assertGrokModelAvailable(bin, cwd, model) {
131
156
  const models = grokModelCatalog(bin, cwd);
132
157
  if (!models.includes(model)) {
@@ -630,3 +655,135 @@ export function grokAuthPane(screen) {
630
655
  }
631
656
  return { url, user_code: embedded, input_required: false, message: null };
632
657
  }
658
+ // ---- ファスト。Grokのfastは別のmodel。Grokは、起動の --model と、動いている席の `/model <ID> [effort]`・`/effort` で、
659
+ // 利用者のconfig.tomlの [models] の default と default_reasoning_effort を書き換える(1.0.50で確かめた。「✓ Default model: …」)。
660
+ // 席だけの切り替えにするため、切り替えの直前の値へ戻す。
661
+ const GROK_DEFAULT_KEYS = ["default", "default_reasoning_effort"];
662
+ /** [models] の節の中の `<key> = …` の行の番号。節や行が無ければnull。 */
663
+ function grokModelsLine(rows, key) {
664
+ const start = rows.findIndex((row) => /^\s*\[models\]\s*(?:#.*)?$/.test(row));
665
+ if (start < 0)
666
+ return null;
667
+ for (let index = start + 1; index < rows.length && !/^\s*\[/.test(rows[index]); index++) {
668
+ if (new RegExp(`^\\s*${key}\\s*=`).test(rows[index]))
669
+ return index;
670
+ }
671
+ return null;
672
+ }
673
+ export function readGrokModelDefaults(configPath) {
674
+ let body;
675
+ try {
676
+ body = fs.readFileSync(configPath, "utf8");
677
+ }
678
+ catch {
679
+ body = null;
680
+ }
681
+ return grokModelDefaultsFromBody(body);
682
+ }
683
+ /** config.tomlの中身(読めなかった時はnull)から、[models] の既定を読む。 */
684
+ export function grokModelDefaultsFromBody(body) {
685
+ if (body === null)
686
+ return { body: null, default: { present: false, value: null }, default_reasoning_effort: { present: false, value: null } };
687
+ const rows = body.split(/\r?\n/);
688
+ const pick = (key) => {
689
+ const line = grokModelsLine(rows, key);
690
+ if (line === null)
691
+ return { present: false, value: null };
692
+ const value = rows[line].match(/=\s*"([^"\\]*)"\s*(?:#.*)?$/);
693
+ return { present: true, value: value ? value[1] : null };
694
+ };
695
+ return { body, default: pick("default"), default_reasoning_effort: pick("default_reasoning_effort") };
696
+ }
697
+ function withoutGrokDefaults(body) {
698
+ const rows = body.split(/\r?\n/);
699
+ for (const key of GROK_DEFAULT_KEYS) {
700
+ const line = grokModelsLine(rows, key);
701
+ if (line !== null)
702
+ rows.splice(line, 1);
703
+ }
704
+ return rows.join("\n");
705
+ }
706
+ /**
707
+ * Grokがconfig.tomlへ書いた [models] の既定(default・default_reasoning_effort)を、切り替えの直前の値へ戻す。
708
+ * 切り替えの前後で、ほかの行が同じなら前の中身をそのまま書き戻す。ほかの行も変わっていたら、既定の行だけを直す。
709
+ * 前の中身を読めなかった時、元の値が読めない書き方だった時、今のfileに [models] の節が無い時は、その項目に触らない。
710
+ */
711
+ export function restoreGrokModelDefaults(configPath, before) {
712
+ if (before.body === null)
713
+ return;
714
+ let now;
715
+ try {
716
+ now = fs.readFileSync(configPath, "utf8");
717
+ }
718
+ catch {
719
+ return;
720
+ }
721
+ if (now === before.body)
722
+ return;
723
+ const eol = now.includes("\r\n") ? "\r\n" : "\n";
724
+ const rows = now.split(/\r?\n/);
725
+ for (const key of GROK_DEFAULT_KEYS) {
726
+ const pin = before[key];
727
+ if (pin.present && pin.value === null)
728
+ continue;
729
+ const line = grokModelsLine(rows, key);
730
+ if (pin.present) {
731
+ const wanted = `${key} = ${JSON.stringify(pin.value)}`;
732
+ if (line !== null)
733
+ rows[line] = wanted;
734
+ else {
735
+ const header = rows.findIndex((row) => /^\s*\[models\]\s*(?:#.*)?$/.test(row));
736
+ if (header >= 0)
737
+ rows.splice(header + 1, 0, wanted);
738
+ }
739
+ }
740
+ else if (line !== null) {
741
+ rows.splice(line, 1);
742
+ }
743
+ }
744
+ const edited = rows.join(eol);
745
+ if (edited === now)
746
+ return;
747
+ // ほかの行が前と同じなら、前の中身(空白や並びもそのまま)を書き戻す。
748
+ const next = withoutGrokDefaults(edited) === withoutGrokDefaults(before.body) ? before.body : edited;
749
+ const temp = `${configPath}.aiterm-restore-${process.pid}`;
750
+ fs.writeFileSync(temp, next, { mode: fs.statSync(configPath).mode & 0o777 });
751
+ try {
752
+ fs.renameSync(temp, configPath);
753
+ }
754
+ catch (error) {
755
+ // Windowsでは、ほかのprocessがfileを開いている間、置き換えに失敗する事がある。途中のfileを残さず、呼び手に次の回で試させる。
756
+ try {
757
+ fs.unlinkSync(temp);
758
+ }
759
+ catch { /* 残っても中身は前の値 */ }
760
+ throw error;
761
+ }
762
+ }
763
+ /**
764
+ * 足元の枠の行の「<modelの表示> (<effort>)」から、modelの表示(「Grok 4.7 Fast」)を読む。権限の表示(· always-approve)は、付く時と付かない時がある。
765
+ * 切り替えの知らせ(Switched to Grok 4.7 Fast (high effort))は、かっこの中に空白があるので拾わない。
766
+ */
767
+ export function grokFooterModelName(screen) {
768
+ let name = null;
769
+ for (const line of screen.split("\n")) {
770
+ const match = line.match(/(Grok [^()·│╯╰─]+?) \([a-z-]+\)/);
771
+ if (match)
772
+ name = match[1].trim();
773
+ }
774
+ return name;
775
+ }
776
+ /** 足元の「Grok 4.7 Fast (high)」から、今のmodelを一覧のIDで返す。base は元のmodel、fast はfast版かどうか。 */
777
+ export function grokModelFromFooter(screen, catalog) {
778
+ const name = grokFooterModelName(screen);
779
+ if (!name)
780
+ return null;
781
+ const fastIds = catalog.fast_ids ?? {};
782
+ for (const model of catalog.models) {
783
+ if (model.display_name === name)
784
+ return { id: model.id, base: model.id, fast: false };
785
+ if (fastIds[model.id] && `${model.display_name} Fast` === name)
786
+ return { id: fastIds[model.id], base: model.id, fast: true };
787
+ }
788
+ return null;
789
+ }
package/dist/index.js CHANGED
@@ -834,15 +834,27 @@ registerRemoteAwareTool("agent_auth", {
834
834
  return fail(error);
835
835
  }
836
836
  });
837
+ // ファスト(高速の応答。ADR 0114)。断りの符号は4つのharnessで共通で、文の先頭に付く。
838
+ const fastModeErrorCodes = ["FAST_MODE_NOT_SUPPORTED", "FAST_MODE_UNAVAILABLE", "FAST_MODE_MODEL_CONFLICT", "FAST_MODE_UNCONFIRMED"];
839
+ const fastModeFailureSchema = z.object({
840
+ code: z.enum(fastModeErrorCodes),
841
+ message: z.string(),
842
+ });
843
+ const fastModeDesc = "ファスト(harnessの高速の応答。費用や利用量が増える)。席は必ず通常の速度で起きる。trueはその席をfastへ切り替え、falseは通常の速度にする" +
844
+ "(fastに対応していないmodelでもfalseは受ける)。切り替えは席だけに留め、harnessが利用者の既定へ書いた分は切り替えの直前の値へ戻す。" +
845
+ "断りは文の先頭の符号で分かる: FAST_MODE_NOT_SUPPORTED(そのmodelは対応していない)、FAST_MODE_UNAVAILABLE(アカウントや契約で使えない)、" +
846
+ "FAST_MODE_MODEL_CONFLICT(modelへfast版のIDを書いてfalseを渡した)、FAST_MODE_UNCONFIRMED(切り替えた後の状態を確かめられない)。";
837
847
  registerRemoteAwareTool("agent_configure", {
838
- description: "起動済みのClaude/Codex/Grok/Cursor agent sessionを再起動せず、会話contextを保ったままmodel/reasoning effortを変更する。" +
848
+ description: "起動済みのClaude/Codex/Grok/Cursor agent sessionを再起動せず、会話contextを保ったままmodel/reasoning effort/fastを変更する。" +
839
849
  "各harnessのCLI標準model操作を使う。Cursorのreasoning_effort変更はmodelと同時指定する。" +
840
850
  "Claude Codeの席では /model の選ぶ画面の「この会話だけ」(s)で替え、端末の ~/.claude/settings.json の既定は書き換えない。" +
841
- "その選び方が無い版、選ぶ画面に無いmodel、そのmodelで選べないeffortは、何も変えずに断る。",
851
+ "その選び方が無い版、選ぶ画面に無いmodel、そのmodelで選べないeffortは、何も変えずに断る。" +
852
+ "fastだけの指定も受ける。" + fastModeDesc,
842
853
  inputSchema: {
843
854
  session_id: z.string().regex(/^[A-Za-z0-9_-]{1,64}$/),
844
855
  model: z.string().min(1).nullish().describe("変更後のmodel。省略時はmodelを変更しない"),
845
856
  reasoning_effort: z.string().min(1).nullish().describe("変更後のreasoning effort。省略時はeffortを変更しない"),
857
+ fast: z.boolean().nullish().describe("変更後のfast。省略時は、その席の今の状態を変えない"),
846
858
  },
847
859
  outputSchema: {
848
860
  schema: z.literal("aiterm.agent-configure-result.v1"),
@@ -851,10 +863,11 @@ registerRemoteAwareTool("agent_configure", {
851
863
  harness: z.enum(["claude-code", "codex-cli", "grok-cli", "cursor-cli"]),
852
864
  model: z.string().nullable(),
853
865
  reasoning_effort: z.string().nullable(),
866
+ fast: z.boolean().nullable().optional().describe("切り替えた後の、席のfastの状態。Aitermが確かめた事の無い席(前の版が起こした席)はnull"),
854
867
  },
855
- }, async ({ session_id, model, reasoning_effort }) => {
868
+ }, async ({ session_id, model, reasoning_effort, fast }) => {
856
869
  try {
857
- const result = await core.configureAgent(session_id, { model, reasoning_effort });
870
+ const result = await core.configureAgent(session_id, { model, reasoning_effort, fast });
858
871
  return {
859
872
  content: [{ type: "text", text: JSON.stringify(result) }],
860
873
  structuredContent: { ...result },
@@ -870,11 +883,14 @@ const agentModelChoiceSchema = z.object({
870
883
  efforts: z.array(z.string()).describe("このmodelで選べるreasoning_effort。空ならeffort非対応"),
871
884
  default_effort: z.string().nullable(),
872
885
  hidden: z.boolean().describe("harnessが一覧で隠すmodel(Codexのinclude_hidden時だけtrueがある)"),
886
+ fast: z.boolean().describe("このharness・このmodelでfastへ切り替えられるか(agent_launch/agent_configureのfast: true)。" +
887
+ "fastが別のmodel IDのharness(Cursor・Grok)は、fast版を別のmodelとして並べず、元のmodelのここがtrueになる"),
873
888
  });
874
889
  registerRemoteAwareTool("agent_models", {
875
890
  description: "harnessが今返すmodelとreasoning effortの候補を、そのharnessの公式の一覧から取得する。promptもturnも送らず推論を消費しない。" +
876
891
  "Codexはapp-server model/list、Claude Codeはstream-json initializeのmodels、Grokはagent stdio initializeのmodelState、" +
877
892
  "Cursorはcursor-agent modelsを素のmodel IDとeffortへ分けたもの。返るidとeffortsはagent_launch/agent_configureへそのまま渡せる。" +
893
+ "modelごとのfastは、そのmodelでfastへ切り替えられるか(harnessが返す値)。fast_unavailableは、アカウントの都合でどのmodelでも使えない時の訳。" +
878
894
  "取得不能はMODEL_CATALOG_UNAVAILABLE、形式異常はMODEL_CATALOG_INVALIDのエラーで返し、別の一覧へfallbackしない。" +
879
895
  "remoteを付けると別端末のharnessの候補を返す。",
880
896
  inputSchema: {
@@ -890,6 +906,7 @@ registerRemoteAwareTool("agent_models", {
890
906
  default_model: z.string().nullable().describe("model省略時にharnessが使うmodel。一覧のIDで表せなければnull"),
891
907
  efforts: z.array(z.string()).describe("全modelのeffortの和"),
892
908
  adapter_efforts: z.record(z.string(), z.string()).describe("harnessの一覧には無く、Aitermのadapterが足したeffortと理由"),
909
+ fast_unavailable: fastModeFailureSchema.nullable().optional().describe("このアカウント・この端末では、どのmodelでもfastを使えない時の訳(harnessが先に答えられる時だけ。今はClaude Code)。使える・分からない時はnull"),
893
910
  models: z.array(agentModelChoiceSchema),
894
911
  },
895
912
  }, async ({ harness, cwd, include_hidden }) => {
@@ -907,6 +924,11 @@ registerRemoteAwareTool("agent_models", {
907
924
  });
908
925
  // 新規APIの選択軸はharness。旧4 launcher名は移行期間中の薄いaliasとして同じ実装へ流す。
909
926
  const kindForHarness = (harness) => harness === "claude-code" ? "claude" : harness === "codex-cli" ? "codex" : harness === "cursor-cli" ? "cursor" : "grok";
927
+ const fastModeLaunchDesc = "ファスト(harnessの高速の応答。費用や利用量が増える)。省略とfalseは通常の速度で起こす(利用者の設定にfastが残っていても通常)。" +
928
+ "trueは通常で起こしてから、その席をfastへ切り替える。modelがfastに対応していない時(FAST_MODE_NOT_SUPPORTED)と、" +
929
+ "modelへfast版のIDを書いてfalseを渡した時(FAST_MODE_MODEL_CONFLICT)は起動を断る。アカウントの都合で使えない時・確かめられない時は、" +
930
+ "起動を断らずに通常の速度で起こし、返りの fast を false、fast_error に訳を入れる。" +
931
+ "modelへfast版のIDを直に書いて省略した時だけ、そのIDのまま起こす(返りの fast は true)。";
910
932
  const agentModelDesc = (kind) => kind === "claude"
911
933
  ? "起動モデル(例: claude-sonnet-4-6)。省略時はClaude CLI既定"
912
934
  : kind === "codex"
@@ -966,7 +988,7 @@ async function launchRemoteAgent(target, harness, args, extra) {
966
988
  }
967
989
  async function launchAgent(kind, args, extra) {
968
990
  const supportsWriteScope = kind !== "claude";
969
- const { prompt, image, throughline_source_session, throughline_supplement_file, model, reasoning_effort, env_vars, cwd, session_name, launch_operation_id, write_scope, trust_project } = args;
991
+ const { prompt, image, throughline_source_session, throughline_supplement_file, model, reasoning_effort, fast, env_vars, cwd, session_name, launch_operation_id, write_scope, trust_project } = args;
970
992
  let delivery = null;
971
993
  try {
972
994
  if (!supportsWriteScope && write_scope !== undefined) {
@@ -975,12 +997,13 @@ async function launchAgent(kind, args, extra) {
975
997
  const initialPrompt = image && image.length > 0 ? core.attachImages(prompt ?? "", image) : prompt ?? undefined;
976
998
  if (initialPrompt || throughline_source_session)
977
999
  delivery = await deliveryForRequest(extra);
978
- const [sid, hint, eventCursor, submitResidue, initialDelivery, startup] = await core.openAgentWithInitialPrompt(kind, {
1000
+ const [sid, hint, eventCursor, submitResidue, initialDelivery, startup, fastState] = await core.openAgentWithInitialPrompt(kind, {
979
1001
  prompt: initialPrompt,
980
1002
  throughline_source_session,
981
1003
  throughline_supplement_file,
982
1004
  model: model ?? undefined,
983
1005
  reasoning_effort: reasoning_effort ?? undefined,
1006
+ fast: fast ?? undefined,
984
1007
  env_vars,
985
1008
  trust_project,
986
1009
  before_send: delivery?.before_send,
@@ -1001,6 +1024,8 @@ async function launchAgent(kind, args, extra) {
1001
1024
  submit_residue: submitResidue,
1002
1025
  initial_prompt: initialDelivery,
1003
1026
  startup,
1027
+ fast: fastState.fast,
1028
+ fast_error: fastState.fast_error,
1004
1029
  ...(supportsWriteScope && write_scope !== undefined
1005
1030
  ? {
1006
1031
  write_scope,
@@ -1012,8 +1037,11 @@ async function launchAgent(kind, args, extra) {
1012
1037
  };
1013
1038
  // 開始未確認(submitted_unconfirmed)は誤りにしない。sessionは立ち、promptは入力欄を離れ、配送も登録済み。
1014
1039
  // 誤りで返すとClaude Code親は受け口のhookを走らせず、子の回答が届かない(ADR 0093)。状態はinitial_promptと本文で伝える。
1040
+ const fastLine = fastState.fast_error
1041
+ ? `\nfast: false(通常の速度で起こした)。${fastState.fast_error.message}`
1042
+ : fast === true ? `\nfast: ${fastState.fast}` : "";
1015
1043
  return {
1016
- content: [{ type: "text", text: `session_id: ${sid}\n${hint}${deliveryIdLine(delivery)}` }],
1044
+ content: [{ type: "text", text: `session_id: ${sid}\n${hint}${fastLine}${deliveryIdLine(delivery)}` }],
1017
1045
  structuredContent: structured,
1018
1046
  };
1019
1047
  }
@@ -1073,6 +1101,7 @@ function registerAgentTool(toolName, kind, desc) {
1073
1101
  model: z.string().nullish().describe(agentModelDesc(kind)),
1074
1102
  // CLI/model側の値集合が版で変わるため公開enumでは縛らない(core側も同方針)。
1075
1103
  reasoning_effort: z.string().nullish().describe(agentEffortDesc(kind)),
1104
+ fast: z.boolean().nullish().describe(fastModeLaunchDesc),
1076
1105
  env_vars: z.array(z.string()).optional().describe("起動したagentへ現在のMCP processから継承する環境変数名。値はtool引数へ渡さない"),
1077
1106
  trust_project: z.boolean().optional().describe("対象projectを信頼し、既知のworkspace・project hooks・MCP初期同意を起動中に進める"),
1078
1107
  cwd: z.string().nullish().describe("作業ディレクトリ(対象リポのルート等・任意)"),
@@ -1097,6 +1126,8 @@ function registerAgentTool(toolName, kind, desc) {
1097
1126
  // 初回prompt dispatch後のsubmit座礁観測(additive)。true=composerに残存を確認(submit未成立の疑い)/
1098
1127
  // false=残存を観測せず(submit成立の保証ではない)/ null=promptなし・判定不能。
1099
1128
  submit_residue: z.boolean().nullable(),
1129
+ fast: z.boolean().optional().describe("起こした後の、席のfastの状態"),
1130
+ fast_error: fastModeFailureSchema.nullable().optional().describe("fastを頼まれたが、通常の速度で起こした時の訳"),
1100
1131
  ...writeScopeOutputSchema,
1101
1132
  },
1102
1133
  }, async (args, extra) => launchAgent(kind, args, extra));
@@ -1115,6 +1146,7 @@ server.registerTool("agent_launch", {
1115
1146
  throughline_supplement_file: z.string().min(1).optional().describe("Throughline 0.10.8以降へそのまま渡すproject束縛済み長期記憶・知識の補足JSON path"),
1116
1147
  model: z.string().nullish().describe("harnessが選ぶモデル。provider名ではなくlive catalog上のmodel ID"),
1117
1148
  reasoning_effort: z.string().nullish().describe("harness adapterが標準CLI表現へ変換する思考レベル。Cursorではmodel同時指定が必要"),
1149
+ fast: z.boolean().nullish().describe(fastModeLaunchDesc),
1118
1150
  env_vars: z.array(z.string()).optional().describe("現在のMCP processから継承する環境変数名"),
1119
1151
  trust_project: z.boolean().optional().describe("対象projectを信頼し、既知のworkspace・project hooks・MCP初期同意を起動中に進める"),
1120
1152
  cwd: z.string().nullish().describe("作業ディレクトリ(絶対パス・任意)"),
@@ -1138,6 +1170,8 @@ server.registerTool("agent_launch", {
1138
1170
  wait_command: z.string().nullable(),
1139
1171
  parent_delivery: parentDeliveryOutputSchema.optional(),
1140
1172
  submit_residue: z.boolean().nullable(),
1173
+ fast: z.boolean().optional().describe("起こした後の、席のfastの状態(この項目を返さない版の別端末では無い)"),
1174
+ fast_error: fastModeFailureSchema.nullable().optional().describe("fastを頼まれたが、通常の速度で起こした時の訳"),
1141
1175
  write_scope: z.string().optional(),
1142
1176
  write_scope_enforcement: z.enum(["enforced_read_only", "declaration_only_unsupported"]).optional(),
1143
1177
  },
@@ -0,0 +1,17 @@
1
+ import { existsSync } from "node:fs";
2
+ /**
3
+ * このprocessのNodeを、後からでも起こせる場所で返す。自分でNodeを起こす時と、呼び手へ「起こす先」として渡す時に使う。
4
+ * `process.execPath`は起きた時の実体で、長く動くprocessでは後から消える事がある(Homebrewは版ごとの場所へ入れ、更新で前の版を消す。
5
+ * 2026-10-12、MacでNodeが26.10.0_1から26.11.1へ上がり、前の版から起きていたMCP process 19個が、無いNodeを起こそうとして黙って失敗していた)。
6
+ * Homebrewの時は同じformulaのopt(更新でも残る)を返す。実体が消えていて代わりも無い時は、PATHから探させる(`node`)。
7
+ * aiterm-steer-deliveryの同じ名前の関数と同じ決まり(0.4.6〜)。
8
+ */
9
+ export function liveNodeExecutable(executable = process.execPath, exists = existsSync) {
10
+ const brew = /^(.*)\/Cellar\/(node(?:@\d+)?)\/[^/]+\/bin\/node$/.exec(executable);
11
+ if (brew) {
12
+ const opt = `${brew[1]}/opt/${brew[2]}/bin/node`;
13
+ if (exists(opt))
14
+ return opt;
15
+ }
16
+ return exists(executable) ? executable : "node";
17
+ }
@@ -50,6 +50,7 @@ export function agentModelsResult(harness, catalog) {
50
50
  default_model: catalog.default_model,
51
51
  efforts: sortEfforts(catalog.models.flatMap(model => model.efforts)),
52
52
  adapter_efforts: catalog.adapter_efforts,
53
+ fast_unavailable: catalog.fast_unavailable ?? null,
53
54
  models: catalog.models,
54
55
  };
55
56
  }
@@ -22,6 +22,7 @@ import { createRequire } from "node:module";
22
22
  import { fileURLToPath } from "node:url";
23
23
  import { realCodexHome, readCodexWakeResult } from "aiterm-steer-delivery";
24
24
  import { AITERM_PROFILE } from "./steer-profile.js";
25
+ import { liveNodeExecutable } from "./live-node.js";
25
26
  import { checkCodexParent, submitCodexParentAnswer, withCodexReceiver } from "./codex-parent-receiver.js";
26
27
  import { codexHookDeliveryState, codexHookDirectory, codexInputDirectory } from "./codex-hook-state.js";
27
28
  export const PARENT_DELIVERY_SCHEMA = "aiterm.parent-delivery.v1";
@@ -134,7 +135,7 @@ async function deliveryState(target, delivery) {
134
135
  * アプリ配下のprocessは、PATHが細い事がある(2026-10-07、素のsshの環境からの呼び出しで起きた)。
135
136
  * nodeの場所がPATHに無い時だけ頭へ足す。既にあれば、並びも中身も変えない。
136
137
  */
137
- export function pathWithNode(env, node = process.execPath, platform = process.platform) {
138
+ export function pathWithNode(env, node = liveNodeExecutable(), platform = process.platform) {
138
139
  const windows = platform === "win32";
139
140
  // Windowsの環境変数名は大文字小文字を区別しない(`Path`で入っている事が多い)。
140
141
  const key = Object.keys(env).find(name => windows ? name.toLowerCase() === "path" : name === "PATH") ?? "PATH";
@@ -146,7 +147,7 @@ export function pathWithNode(env, node = process.execPath, platform = process.pl
146
147
  }
147
148
  /** この命令の場所と版。製品側の入口(aiterm-steer-delivery)が、呼べる相手かどうかを確かめるのに使う。 */
148
149
  export function providerDescription() {
149
- return { version: pkg.version, node: process.execPath, cli: fileURLToPath(import.meta.url) };
150
+ return { version: pkg.version, node: liveNodeExecutable(), cli: fileURLToPath(import.meta.url) };
150
151
  }
151
152
  export async function runParentDelivery(argv) {
152
153
  const [kind, command, ...args] = argv;
@@ -7,6 +7,7 @@ import { createRequire } from "node:module";
7
7
  import * as path from "node:path";
8
8
  import { fileURLToPath } from "node:url";
9
9
  import { defaultRuntimeErrorReportPaths, readBoundedFile, runtimeErrorReportingStatus } from "./runtime-error-os.js";
10
+ import { liveNodeExecutable } from "./live-node.js";
10
11
  import { RuntimeErrorStore } from "./runtime-error-store.js";
11
12
  const pkg = createRequire(import.meta.url)("../package.json");
12
13
  const WORKER = fileURLToPath(new URL("./runtime-error-worker.js", import.meta.url));
@@ -252,7 +253,7 @@ export function triggerRuntimeErrorReport(env = process.env) {
252
253
  try {
253
254
  if (runtimeErrorReportingStatus(defaultRuntimeErrorReportPaths().reportingConfigPath) !== "enabled")
254
255
  return false;
255
- const child = spawn(process.execPath, [WORKER, "report"], { stdio: "ignore", detached: true, windowsHide: true, env });
256
+ const child = spawn(liveNodeExecutable(), [WORKER, "report"], { stdio: "ignore", detached: true, windowsHide: true, env });
256
257
  child.once("error", () => { });
257
258
  child.unref();
258
259
  return true;
@@ -4,6 +4,7 @@ import * as fs from "node:fs";
4
4
  import * as path from "node:path";
5
5
  import { createRequire } from "node:module";
6
6
  import { defaultRuntimeErrorReportPaths, runtimeErrorReportingStatus } from "./runtime-error-os.js";
7
+ import { liveNodeExecutable } from "./live-node.js";
7
8
  import { defaultRuntimeErrorPaths, windowsPrivateDaclCommand, expectedHostProfiles, readBoundedFile, processStartIdentity, forceKill } from "./runtime-error-os.js";
8
9
  export { defaultRuntimeErrorPaths, windowsPrivateDaclCommand, windowsPrivateDaclVerifyCommand } from "./runtime-error-os.js";
9
10
  import { fileURLToPath } from "node:url";
@@ -673,7 +674,7 @@ export function recordRuntimeError(code, options = {}) {
673
674
  fixedStoreFailure(stderr);
674
675
  } };
675
676
  try {
676
- const child = spawn(process.execPath, [options.workerPath ?? WORKER, "record", code], {
677
+ const child = spawn(liveNodeExecutable(), [options.workerPath ?? WORKER, "record", code], {
677
678
  stdio: "ignore", windowsHide: true, env: process.env,
678
679
  });
679
680
  const timer = setTimeout(() => { report(); forceKill(child); }, options.timeoutMs ?? defaultWorkerTimeoutMs("record"));
@@ -707,7 +708,7 @@ export async function runtimeErrorStoreDiagnostic(options = {}) {
707
708
  } };
708
709
  let child;
709
710
  try {
710
- child = spawn(process.execPath, [options.workerPath ?? WORKER, "diagnostic"], {
711
+ child = spawn(liveNodeExecutable(), [options.workerPath ?? WORKER, "diagnostic"], {
711
712
  stdio: ["ignore", "pipe", "ignore"], windowsHide: true, env: process.env,
712
713
  });
713
714
  }