@ycodium-ai/agent-grok 0.2.3254 → 0.2.3265

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.
Files changed (2) hide show
  1. package/dist/index.js +451 -76
  2. package/package.json +4 -4
package/dist/index.js CHANGED
@@ -159,6 +159,9 @@ var GROK_CARD = {
159
159
 
160
160
  // src/grokInstance.ts
161
161
  import * as NodeCrypto3 from "node:crypto";
162
+ import {
163
+ raceAbort as raceAbort2
164
+ } from "@ycodium-ai/plugin-api/executor";
162
165
  import { nodeStreamsFromHandle as nodeStreamsFromHandle2 } from "@ycodium-ai/plugin-api/process-stdin";
163
166
 
164
167
  // ../../packages/ycodium-acp-executor/src/executor.ts
@@ -25191,11 +25194,15 @@ function createAcpConnection(options) {
25191
25194
  });
25192
25195
  };
25193
25196
  const close = (error62) => shutdown(error62 ?? new AcpConnectionClosedError());
25197
+ const abandonPending = (error62) => {
25198
+ rejectPending(error62);
25199
+ };
25194
25200
  return {
25195
25201
  request,
25196
25202
  begin,
25197
25203
  notify,
25198
25204
  awaitInboundIdle,
25205
+ abandonPending,
25199
25206
  close,
25200
25207
  closed
25201
25208
  };
@@ -25661,10 +25668,11 @@ function createExecutorPrompt({
25661
25668
  const prompt = async (input2) => {
25662
25669
  failIfPromptUnavailable();
25663
25670
  const started = requireStarted();
25671
+ let sessionId = started.sessionId;
25664
25672
  let record3;
25665
25673
  const settle2 = (rest) => settleTurn({
25666
25674
  expectedGeneration: started.generation,
25667
- expectedSessionId: started.sessionId,
25675
+ expectedSessionId: sessionId,
25668
25676
  record: record3,
25669
25677
  ...rest
25670
25678
  });
@@ -25748,12 +25756,32 @@ function createExecutorPrompt({
25748
25756
  const queuedTurnId = turnId;
25749
25757
  const isStale = () => interruptedTurnIds.has(queuedTurnId) || input2.expectedActiveTurnId !== void 0 && shared.activeTurnId !== queuedTurnId;
25750
25758
  if (!isStale()) {
25751
- shared.modelSelectionState = await applyRequestedModel({
25752
- state: shared.modelSelectionState,
25753
- acpSessionId: started.sessionId,
25754
- requestedModelId: input2.requestedModelId,
25755
- expectedGeneration: started.generation
25756
- });
25759
+ try {
25760
+ const applied = await applyRequestedModel({
25761
+ state: shared.modelSelectionState,
25762
+ acpSessionId: sessionId,
25763
+ setup: shared.startResult?.sessionSetupResult ?? {},
25764
+ requestedModelId: input2.requestedModelId,
25765
+ expectedGeneration: started.generation
25766
+ });
25767
+ shared.modelSelectionState = applied.state;
25768
+ sessionId = applied.sessionId;
25769
+ record3.sessionId = applied.sessionId;
25770
+ } catch (error62) {
25771
+ const failureCategory = readAcpFailureCategory(error62);
25772
+ if (failureCategory === "model-unavailable") {
25773
+ const errorMessage = error62 instanceof Error && error62.message.trim().length > 0 ? error62.message : "Requested model is not in this agent's catalog. This turn was not sent.";
25774
+ settle2({
25775
+ turnId,
25776
+ stopReason: "error",
25777
+ state: "failed",
25778
+ errorMessage,
25779
+ failureCategory
25780
+ });
25781
+ throw error62 instanceof Error ? withFailureCategory(error62, failureCategory) : error62;
25782
+ }
25783
+ throw error62;
25784
+ }
25757
25785
  }
25758
25786
  failIfDisposed();
25759
25787
  if (isStale()) {
@@ -25764,7 +25792,7 @@ function createExecutorPrompt({
25764
25792
  let written = false;
25765
25793
  try {
25766
25794
  response = await sendPromptRpc(
25767
- started.sessionId,
25795
+ sessionId,
25768
25796
  promptParts,
25769
25797
  () => {
25770
25798
  written = true;
@@ -25814,7 +25842,7 @@ function createExecutorPrompt({
25814
25842
  if (hooks.readContextWindow !== void 0) {
25815
25843
  try {
25816
25844
  contextWindow = await hooks.readContextWindow({
25817
- sessionId: started.sessionId,
25845
+ sessionId,
25818
25846
  promptResponse: response
25819
25847
  }) ?? void 0;
25820
25848
  } catch {
@@ -26457,6 +26485,56 @@ function acpInitializePayload(clientInfo, overrides) {
26457
26485
  };
26458
26486
  }
26459
26487
 
26488
+ // ../../packages/ycodium-acp-executor/src/refreshDeadline.ts
26489
+ var MODEL_CATALOG_REFRESH_TIMEOUT_MS = 9e4;
26490
+
26491
+ // ../../packages/ycodium-acp-executor/src/model-send.ts
26492
+ function judgeRequestedModel(input2) {
26493
+ const requested = normalizeModelId(input2.requestedModelId);
26494
+ const current = input2.state.currentModelId;
26495
+ if (requested === void 0 || requested === current) return { kind: "keep" };
26496
+ const hasCatalog = input2.state.advertisedModelValues.length > 0;
26497
+ const hasCurrent = current !== void 0;
26498
+ if (!hasCatalog && !hasCurrent) return { kind: "keep" };
26499
+ const canSwitch = input2.state.strategy === "session/set_model" && input2.state.sessionModelsAdvertised || input2.state.strategy === "config-option" && input2.state.modelConfigId !== void 0;
26500
+ const value = hasCatalog ? matchAdvertisedModelValue(requested, input2.state.advertisedModelValues) : void 0;
26501
+ if (value !== void 0 && value === current) return { kind: "keep" };
26502
+ if (value !== void 0 && canSwitch) return { kind: "switch", value };
26503
+ return input2.refreshed ? { kind: "unavailable" } : { kind: "refresh" };
26504
+ }
26505
+ function modelUnavailableError(requestedModelId, currentModelId) {
26506
+ const current = currentModelId ?? "its own model";
26507
+ return withFailureCategory(
26508
+ new Error(
26509
+ `Requested model "${requestedModelId}" is not in this agent's catalog; it is on "${current}". This turn was not sent. Refresh the model list in this agent's settings and try again.`
26510
+ ),
26511
+ "model-unavailable"
26512
+ );
26513
+ }
26514
+ var ModelCatalogRefreshTimeoutError = class extends Error {
26515
+ constructor() {
26516
+ super("model catalog refresh timed out");
26517
+ this.name = "ModelCatalogRefreshTimeoutError";
26518
+ }
26519
+ };
26520
+ function modelRefreshTimedOutError(requestedModelId) {
26521
+ return withFailureCategory(
26522
+ new Error(
26523
+ `Refreshing this agent's model list timed out, so this turn was not sent. Requested model "${requestedModelId}".`
26524
+ ),
26525
+ "model-unavailable"
26526
+ );
26527
+ }
26528
+ function modelRefreshFailedError(requestedModelId, error62) {
26529
+ const detail = error62 instanceof Error && error62.message.trim().length > 0 ? error62.message : "the model list could not be read";
26530
+ return withFailureCategory(
26531
+ new Error(
26532
+ `Refreshing this agent's model list failed (${detail}), so this turn was not sent. Requested model "${requestedModelId}".`
26533
+ ),
26534
+ "model-unavailable"
26535
+ );
26536
+ }
26537
+
26460
26538
  // ../../packages/ycodium-acp-executor/src/runtime-mode.ts
26461
26539
  function isRecord5(value) {
26462
26540
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -26487,43 +26565,131 @@ function createExecutorSession({
26487
26565
  failIfDisposed,
26488
26566
  shared
26489
26567
  }) {
26490
- const applyRequestedModel = async (input2) => {
26491
- const requestedModelId = normalizeModelId(input2.requestedModelId);
26492
- if (requestedModelId === void 0 || requestedModelId === input2.state.currentModelId || input2.state.strategy === "session/set_model" && !input2.state.sessionModelsAdvertised || input2.state.strategy === "config-option" && input2.state.modelConfigId === void 0) {
26493
- return input2.state;
26494
- }
26495
- const value = matchAdvertisedModelValue(requestedModelId, input2.state.advertisedModelValues);
26496
- if (value === void 0) {
26497
- if (requestedModelId !== shared.lastUnmatchedModelId) {
26498
- shared.lastUnmatchedModelId = requestedModelId;
26499
- options.log?.info("requested model is not in the agent catalog; keeping its own model", {
26500
- requestedModelId,
26501
- currentModelId: input2.state.currentModelId,
26502
- advertisedModelValues: input2.state.advertisedModelValues
26503
- });
26504
- }
26505
- return input2.state;
26568
+ const openFreshSession = async (expectedGeneration) => {
26569
+ const initialized = shared.initializeResult;
26570
+ if (initialized === void 0) {
26571
+ throw new Error("no handshake");
26506
26572
  }
26507
- if (value === input2.state.currentModelId) {
26508
- return input2.state;
26573
+ const mcpServers = filterMcpServersByAgentCapabilities(
26574
+ options.mcpServers ?? [],
26575
+ initialized.agentCapabilities
26576
+ );
26577
+ const additionalDirectories = options.additionalDirectories !== void 0 && options.additionalDirectories.length > 0 ? { additionalDirectories: options.additionalDirectories } : {};
26578
+ const created = await connection.request("session/new", {
26579
+ cwd: options.cwd,
26580
+ mcpServers,
26581
+ ...additionalDirectories
26582
+ });
26583
+ if (!isLive(expectedGeneration)) {
26584
+ throw new AcpExecutorDisposedError();
26509
26585
  }
26586
+ if (created.sessionId === void 0 || created.sessionId.length === 0) {
26587
+ throw new Error("session/new did not return a session id");
26588
+ }
26589
+ return { sessionId: created.sessionId, setup: created };
26590
+ };
26591
+ const switchModel = async (input2) => {
26510
26592
  let configOptions = input2.state.configOptions;
26511
26593
  if (input2.state.strategy === "config-option") {
26512
26594
  const switched = await connection.request(
26513
26595
  "session/set_config_option",
26514
- { sessionId: input2.acpSessionId, configId: input2.state.modelConfigId, value }
26596
+ { sessionId: input2.sessionId, configId: input2.state.modelConfigId, value: input2.value }
26515
26597
  );
26516
26598
  configOptions = switched?.configOptions ?? configOptions;
26517
26599
  } else {
26518
26600
  await connection.request("session/set_model", {
26519
- sessionId: input2.acpSessionId,
26520
- modelId: value
26601
+ sessionId: input2.sessionId,
26602
+ modelId: input2.value
26521
26603
  });
26522
26604
  }
26523
26605
  if (!isLive(input2.expectedGeneration)) {
26524
26606
  throw new AcpExecutorDisposedError();
26525
26607
  }
26526
- return { ...input2.state, currentModelId: value, configOptions };
26608
+ return { ...input2.state, currentModelId: input2.value, configOptions };
26609
+ };
26610
+ const adoptReopenedSession = (applied) => {
26611
+ if (shared.sessionId === void 0 && shared.startResult === void 0) return;
26612
+ const setup = applied.setup;
26613
+ shared.sessionId = applied.sessionId;
26614
+ shared.modelSelectionState = applied.state;
26615
+ shared.modeState = parseSessionModeState(setup);
26616
+ if (shared.startResult !== void 0) {
26617
+ shared.startResult = {
26618
+ ...shared.startResult,
26619
+ sessionId: applied.sessionId,
26620
+ sessionSetupResult: setup,
26621
+ modelConfigId: extractModelConfigId(setup),
26622
+ modelId: applied.state.currentModelId
26623
+ };
26624
+ }
26625
+ };
26626
+ const applyRequestedModel = async (input2) => {
26627
+ async function applyOnce(state, sessionId, setup, refreshed) {
26628
+ const requestedModelId = normalizeModelId(input2.requestedModelId) ?? "";
26629
+ const judgment = judgeRequestedModel({
26630
+ state,
26631
+ requestedModelId: input2.requestedModelId,
26632
+ refreshed
26633
+ });
26634
+ if (judgment.kind === "keep") return { state, sessionId, setup };
26635
+ if (judgment.kind === "unavailable") {
26636
+ throw modelUnavailableError(requestedModelId, state.currentModelId);
26637
+ }
26638
+ if (judgment.kind === "switch") {
26639
+ return {
26640
+ state: await switchModel({
26641
+ state,
26642
+ sessionId,
26643
+ value: judgment.value,
26644
+ expectedGeneration: input2.expectedGeneration
26645
+ }),
26646
+ sessionId,
26647
+ setup
26648
+ };
26649
+ }
26650
+ return refreshAndRejudge(requestedModelId);
26651
+ }
26652
+ async function refreshAndRejudge(requestedModelId) {
26653
+ const timeoutMs = options.modelCatalogRefreshTimeoutMs ?? MODEL_CATALOG_REFRESH_TIMEOUT_MS;
26654
+ let timedOut = false;
26655
+ const timeoutError = new ModelCatalogRefreshTimeoutError();
26656
+ let timer;
26657
+ const work = (async () => {
26658
+ await options.refreshCatalogForSend?.();
26659
+ if (timedOut) throw timeoutError;
26660
+ const reopened = await openFreshSession(input2.expectedGeneration);
26661
+ if (timedOut) throw timeoutError;
26662
+ const next = modelSelectionStateFromSetup(reopened.setup);
26663
+ const applied = await applyOnce(next, reopened.sessionId, reopened.setup, true);
26664
+ if (timedOut) throw timeoutError;
26665
+ adoptReopenedSession(applied);
26666
+ return applied;
26667
+ })();
26668
+ void work.then(
26669
+ () => void 0,
26670
+ () => void 0
26671
+ );
26672
+ const deadline = new Promise((_resolve, reject) => {
26673
+ timer = setTimeout(() => {
26674
+ timedOut = true;
26675
+ connection.abandonPending(timeoutError);
26676
+ reject(timeoutError);
26677
+ }, timeoutMs);
26678
+ });
26679
+ try {
26680
+ return await Promise.race([work, deadline]);
26681
+ } catch (error62) {
26682
+ if (error62 instanceof AcpExecutorDisposedError) throw error62;
26683
+ if (timedOut || error62 instanceof ModelCatalogRefreshTimeoutError) {
26684
+ throw modelRefreshTimedOutError(requestedModelId);
26685
+ }
26686
+ if (readAcpFailureCategory(error62) === "model-unavailable") throw error62;
26687
+ throw modelRefreshFailedError(requestedModelId, error62);
26688
+ } finally {
26689
+ if (timer !== void 0) clearTimeout(timer);
26690
+ }
26691
+ }
26692
+ return applyOnce(input2.state, input2.acpSessionId, input2.setup, false);
26527
26693
  };
26528
26694
  const startOnce = async (expectedGeneration) => {
26529
26695
  const initializePayload = acpInitializePayload(options.clientInfo, options.clientCapabilities);
@@ -26592,12 +26758,17 @@ function createExecutorSession({
26592
26758
  categories: unhandledCategories
26593
26759
  });
26594
26760
  }
26595
- const nextModelSelectionState = await applyRequestedModel({
26596
- state: modelSelectionStateFromSetup(setup),
26761
+ const openedSetup = setup;
26762
+ const appliedModel = await applyRequestedModel({
26763
+ state: modelSelectionStateFromSetup(openedSetup),
26597
26764
  acpSessionId: nextSessionId,
26765
+ setup: openedSetup,
26598
26766
  requestedModelId: options.requestedModelId,
26599
26767
  expectedGeneration
26600
26768
  });
26769
+ nextSessionId = appliedModel.sessionId;
26770
+ setup = appliedModel.setup;
26771
+ const nextModelSelectionState = appliedModel.state;
26601
26772
  await applyRequestedEffort({
26602
26773
  connection,
26603
26774
  log: options.log,
@@ -27132,7 +27303,6 @@ function createAcpExecutor(options) {
27132
27303
  currentModelId: void 0,
27133
27304
  advertisedModelValues: []
27134
27305
  },
27135
- lastUnmatchedModelId: void 0,
27136
27306
  initializeResult: void 0,
27137
27307
  startResult: void 0,
27138
27308
  startInFlight: void 0,
@@ -28696,7 +28866,8 @@ var runGrokInspect = async (input2) => {
28696
28866
  args: GROK_INSPECT_ARGS,
28697
28867
  cwd,
28698
28868
  owner: { bindingId: host.bindingId },
28699
- ...Object.keys(env).length > 0 ? { env } : {}
28869
+ ...Object.keys(env).length > 0 ? { env } : {},
28870
+ ...input2.signal === void 0 ? {} : { signal: input2.signal }
28700
28871
  });
28701
28872
  const run = await collectProbeProcess(handle, {
28702
28873
  timeoutMs: INSPECT_TIMEOUT_MS,
@@ -28782,10 +28953,21 @@ function grokOpenCommands(session) {
28782
28953
  };
28783
28954
  }
28784
28955
 
28956
+ // src/modelCacheRefresh.ts
28957
+ import * as NodeFS4 from "node:fs";
28958
+ import * as NodePath6 from "node:path";
28959
+ import {
28960
+ ExecutorSpawnAbortedError as ExecutorSpawnAbortedError2,
28961
+ ExecutorSpawnRefusedError as ExecutorSpawnRefusedError2,
28962
+ throwIfExecutorSpawnAborted
28963
+ } from "@ycodium-ai/plugin-api/executor";
28964
+
28785
28965
  // src/probeModels.ts
28786
28966
  import {
28787
28967
  describeExecutorSpawnFailure,
28788
- ExecutorSpawnRefusedError
28968
+ ExecutorSpawnAbortedError,
28969
+ ExecutorSpawnRefusedError,
28970
+ raceAbort
28789
28971
  } from "@ycodium-ai/plugin-api/executor";
28790
28972
  import { nodeStreamsFromHandle } from "@ycodium-ai/plugin-api/process-stdin";
28791
28973
  var PROBE_REQUEST_TIMEOUT_MS = 2e4;
@@ -28809,21 +28991,27 @@ async function probeGrokModels(input2) {
28809
28991
  args: tokenizeLaunchArgs(recipe.launchArgs),
28810
28992
  cwd: process.cwd(),
28811
28993
  owner: { bindingId: host.bindingId },
28812
- ...Object.keys(env).length > 0 ? { env } : {}
28994
+ ...Object.keys(env).length > 0 ? { env } : {},
28995
+ ...input2.signal === void 0 ? {} : { signal: input2.signal }
28813
28996
  });
28814
28997
  } catch (error62) {
28815
- if (error62 instanceof ExecutorSpawnRefusedError) throw error62;
28998
+ if (error62 instanceof ExecutorSpawnRefusedError || error62 instanceof ExecutorSpawnAbortedError) {
28999
+ throw error62;
29000
+ }
28816
29001
  return { kind: "not-started", ...describeExecutorSpawnFailure(error62) };
28817
29002
  }
28818
29003
  let handshake;
28819
29004
  try {
28820
29005
  const { readable, writable } = nodeStreamsFromHandle(handle);
28821
- handshake = await openAcpProbeHandshake({
28822
- readable,
28823
- writable,
28824
- clientInfo: GROK_CLIENT_INFO,
28825
- requestTimeoutMs: PROBE_REQUEST_TIMEOUT_MS
28826
- });
29006
+ handshake = await raceAbort(
29007
+ openAcpProbeHandshake({
29008
+ readable,
29009
+ writable,
29010
+ clientInfo: GROK_CLIENT_INFO,
29011
+ requestTimeoutMs: PROBE_REQUEST_TIMEOUT_MS
29012
+ }),
29013
+ input2.signal
29014
+ );
28827
29015
  return { kind: "answered", models: grokProbeModels(handshake.initializeResult) };
28828
29016
  } catch (error62) {
28829
29017
  return { kind: "failed", detail: failureDetail(error62) };
@@ -28833,34 +29021,195 @@ async function probeGrokModels(input2) {
28833
29021
  }
28834
29022
  }
28835
29023
  async function probeGrokVersion(input2) {
28836
- const { recipe, host, env } = input2;
29024
+ const { recipe, host, env, signal } = input2;
28837
29025
  return probeCliVersion({
28838
29026
  spawn: () => host.process.spawn({
28839
29027
  program: recipe.binaryPath,
28840
29028
  args: ["--version"],
28841
29029
  owner: { bindingId: host.bindingId },
28842
- ...Object.keys(env).length > 0 ? { env } : {}
29030
+ ...Object.keys(env).length > 0 ? { env } : {},
29031
+ ...signal === void 0 ? {} : { signal }
28843
29032
  }),
28844
29033
  timeoutMs: PROBE_REQUEST_TIMEOUT_MS
28845
29034
  });
28846
29035
  }
28847
29036
 
29037
+ // src/modelCacheRefresh.ts
29038
+ var GROK_MODEL_CACHE_FILE = "models_cache.json";
29039
+ var GROK_MODEL_CACHE_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
29040
+ var GROK_MODELS_COMMAND_TIMEOUT_MS = 6e4;
29041
+ function grokModelCachePath(accountHome) {
29042
+ return NodePath6.join(accountHome, GROK_MODEL_CACHE_FILE);
29043
+ }
29044
+ function readGrokModelCacheMtimeMs(accountHome) {
29045
+ try {
29046
+ return NodeFS4.statSync(grokModelCachePath(accountHome)).mtimeMs;
29047
+ } catch {
29048
+ return null;
29049
+ }
29050
+ }
29051
+ function grokModelCacheNeedsRefresh(input2) {
29052
+ if (input2.force) return true;
29053
+ if (input2.cacheMtimeMs === null) return true;
29054
+ if (input2.nowMs - input2.cacheMtimeMs > GROK_MODEL_CACHE_MAX_AGE_MS) return true;
29055
+ return input2.lastSeenCliVersion !== null && input2.cliVersion !== null && input2.cliVersion !== input2.lastSeenCliVersion;
29056
+ }
29057
+ async function drain(stream) {
29058
+ for await (const chunk of stream) {
29059
+ void chunk;
29060
+ }
29061
+ }
29062
+ async function runGrokModelsCommand(input2) {
29063
+ const { recipe, host, env } = input2;
29064
+ throwIfExecutorSpawnAborted(input2.signal);
29065
+ let handle;
29066
+ try {
29067
+ handle = await host.process.spawn({
29068
+ program: recipe.binaryPath,
29069
+ args: ["models"],
29070
+ cwd: process.cwd(),
29071
+ owner: { bindingId: host.bindingId },
29072
+ ...Object.keys(env).length > 0 ? { env } : {},
29073
+ ...input2.signal === void 0 ? {} : { signal: input2.signal }
29074
+ });
29075
+ } catch (error62) {
29076
+ if (error62 instanceof ExecutorSpawnAbortedError2) throw error62;
29077
+ host.log.warn("grok models could not be started; keeping the previous model list", {
29078
+ detail: error62 instanceof ExecutorSpawnRefusedError2 ? "process limit" : String(error62)
29079
+ });
29080
+ return false;
29081
+ }
29082
+ const finished = Promise.all([drain(handle.stdout), drain(handle.stderr), handle.exited]).then(
29083
+ ([, , code]) => code
29084
+ );
29085
+ let timer;
29086
+ const timeout = new Promise((resolve2) => {
29087
+ timer = setTimeout(() => resolve2("timeout"), GROK_MODELS_COMMAND_TIMEOUT_MS);
29088
+ timer.unref?.();
29089
+ });
29090
+ try {
29091
+ const outcome = await Promise.race([finished, timeout]);
29092
+ if (outcome === "timeout") {
29093
+ finished.catch(() => void 0);
29094
+ host.log.warn("grok models timed out; keeping the previous model list", {
29095
+ timeoutMs: GROK_MODELS_COMMAND_TIMEOUT_MS
29096
+ });
29097
+ return false;
29098
+ }
29099
+ if (outcome !== 0) {
29100
+ host.log.warn("grok models exited non-zero; keeping the previous model list", {
29101
+ exitCode: outcome
29102
+ });
29103
+ return false;
29104
+ }
29105
+ return true;
29106
+ } finally {
29107
+ if (timer !== void 0) clearTimeout(timer);
29108
+ await handle.kill("SIGTERM").catch(() => void 0);
29109
+ }
29110
+ }
29111
+ var modelsCommandSlots = /* @__PURE__ */ new Map();
29112
+ function runCoalescedModelsCommand(cachePath, force, run) {
29113
+ const existing = modelsCommandSlots.get(cachePath);
29114
+ if (existing !== void 0) {
29115
+ if (force && existing.followUp === void 0) {
29116
+ const followUp = existing.current.then(
29117
+ () => run(),
29118
+ () => run()
29119
+ );
29120
+ existing.followUp = followUp;
29121
+ void followUp.finally(() => {
29122
+ if (modelsCommandSlots.get(cachePath)?.followUp === followUp) {
29123
+ modelsCommandSlots.delete(cachePath);
29124
+ }
29125
+ });
29126
+ return followUp;
29127
+ }
29128
+ return existing.followUp ?? existing.current;
29129
+ }
29130
+ const current = run();
29131
+ const slot = { current };
29132
+ modelsCommandSlots.set(cachePath, slot);
29133
+ void current.finally(() => {
29134
+ if (slot.followUp !== void 0) return;
29135
+ if (modelsCommandSlots.get(cachePath) === slot) modelsCommandSlots.delete(cachePath);
29136
+ });
29137
+ return current;
29138
+ }
29139
+ function createGrokModelCatalog(input2) {
29140
+ let lastSeenCliVersion = null;
29141
+ let skipUnforced = false;
29142
+ const nowMs = input2.nowMs ?? (() => Date.now());
29143
+ const readCliVersion = (signal) => {
29144
+ if (input2.recipe.binaryPath.length === 0) return Promise.resolve(null);
29145
+ return probeGrokVersion({
29146
+ recipe: input2.recipe,
29147
+ host: input2.host,
29148
+ env: input2.spawnEnv(),
29149
+ ...signal === void 0 ? {} : { signal }
29150
+ });
29151
+ };
29152
+ const refreshModelCatalog = async (request) => {
29153
+ try {
29154
+ const cliVersion = request.cliVersion !== void 0 ? request.cliVersion : await readCliVersion();
29155
+ if (!request.force && skipUnforced) {
29156
+ skipUnforced = false;
29157
+ if (cliVersion !== null) lastSeenCliVersion = cliVersion;
29158
+ return;
29159
+ }
29160
+ const needs = grokModelCacheNeedsRefresh({
29161
+ cacheMtimeMs: readGrokModelCacheMtimeMs(input2.accountHome()),
29162
+ nowMs: nowMs(),
29163
+ lastSeenCliVersion,
29164
+ cliVersion,
29165
+ force: request.force
29166
+ });
29167
+ let commandOk = true;
29168
+ if (needs) {
29169
+ commandOk = await runCoalescedModelsCommand(
29170
+ grokModelCachePath(input2.accountHome()),
29171
+ request.force,
29172
+ () => runGrokModelsCommand({
29173
+ recipe: input2.recipe,
29174
+ host: input2.host,
29175
+ env: input2.spawnEnv(),
29176
+ ...request.signal === void 0 ? {} : { signal: request.signal }
29177
+ })
29178
+ );
29179
+ }
29180
+ if (cliVersion !== null) lastSeenCliVersion = cliVersion;
29181
+ if (request.force && request.armProbeSkip === true) skipUnforced = true;
29182
+ if (!commandOk && request.surfaceErrors === true) {
29183
+ throw new Error("grok models failed");
29184
+ }
29185
+ } catch (error62) {
29186
+ if (error62 instanceof ExecutorSpawnAbortedError2) throw error62;
29187
+ if (request.surfaceErrors === true) throw error62;
29188
+ input2.host.log.warn("grok model cache refresh failed; keeping the previous model list", {
29189
+ detail: error62 instanceof Error ? error62.message : String(error62)
29190
+ });
29191
+ }
29192
+ };
29193
+ const refreshForSend = () => refreshModelCatalog({ force: true, surfaceErrors: true });
29194
+ return { readCliVersion, refreshModelCatalog, refreshForSend };
29195
+ }
29196
+
28848
29197
  // src/grokHome.ts
28849
29198
  import * as NodeCrypto2 from "node:crypto";
28850
29199
  import * as NodeOS3 from "node:os";
28851
- import * as NodePath6 from "node:path";
29200
+ import * as NodePath7 from "node:path";
28852
29201
  import { mergeLaunchEnv } from "@ycodium-ai/plugin-api/executor";
28853
29202
  function expandHomePath(value) {
28854
29203
  if (value === "~") return NodeOS3.homedir();
28855
29204
  if (value.startsWith("~/") || value.startsWith("~\\")) {
28856
- return NodePath6.join(NodeOS3.homedir(), value.slice(2));
29205
+ return NodePath7.join(NodeOS3.homedir(), value.slice(2));
28857
29206
  }
28858
29207
  return value;
28859
29208
  }
28860
29209
  function resolveGrokHomePath(value) {
28861
29210
  const trimmed3 = value.trim();
28862
- if (trimmed3.length === 0) return NodePath6.join(NodeOS3.homedir(), ".grok");
28863
- return NodePath6.resolve(expandHomePath(trimmed3));
29211
+ if (trimmed3.length === 0) return NodePath7.join(NodeOS3.homedir(), ".grok");
29212
+ return NodePath7.resolve(expandHomePath(trimmed3));
28864
29213
  }
28865
29214
  function probeGrokAccount(accountPath) {
28866
29215
  const resolved = resolveGrokHomePath(accountPath);
@@ -28879,12 +29228,12 @@ function grokContinuationKey(recipe) {
28879
29228
  function resolveAccountHomePath(recipe, configuredHome) {
28880
29229
  const fromEnvironment = recipe.environment["GROK_HOME"]?.trim();
28881
29230
  if (fromEnvironment !== void 0 && fromEnvironment.length > 0) {
28882
- return NodePath6.resolve(expandHomePath(fromEnvironment));
29231
+ return NodePath7.resolve(expandHomePath(fromEnvironment));
28883
29232
  }
28884
29233
  return resolveGrokHomePath(configuredHome);
28885
29234
  }
28886
29235
  function absentPath() {
28887
- return NodePath6.join(
29236
+ return NodePath7.join(
28888
29237
  NodeOS3.tmpdir(),
28889
29238
  `ycodium-grok-no-auth-${NodeCrypto2.randomUUID()}`,
28890
29239
  "auth.json"
@@ -29012,7 +29361,22 @@ var createGrokInstance = (config2, host) => {
29012
29361
  return { status: "unknown" };
29013
29362
  }
29014
29363
  };
29015
- const probe = async () => {
29364
+ const spawnEnv = () => {
29365
+ const configuredHome = recipe.homePath;
29366
+ return buildSpawnEnv(
29367
+ recipe,
29368
+ resolveGrokHomePath(configuredHome),
29369
+ configuredHome.trim().length > 0
29370
+ );
29371
+ };
29372
+ const modelCatalog = createGrokModelCatalog({
29373
+ recipe,
29374
+ host,
29375
+ spawnEnv,
29376
+ accountHome: () => resolveAccountHomePath(recipe, recipe.homePath)
29377
+ });
29378
+ const probe = async (signal) => {
29379
+ if (signal?.aborted) throw new Error("aborted");
29016
29380
  if (recipe.binaryPath.length === 0) {
29017
29381
  return {
29018
29382
  installed: false,
@@ -29022,17 +29386,35 @@ var createGrokInstance = (config2, host) => {
29022
29386
  detail: "Grok program path is not configured"
29023
29387
  };
29024
29388
  }
29025
- const configuredHome = recipe.homePath;
29026
- const env = buildSpawnEnv(
29027
- recipe,
29028
- resolveGrokHomePath(configuredHome),
29029
- configuredHome.trim().length > 0
29389
+ const env = spawnEnv();
29390
+ const version2 = await raceAbort2(modelCatalog.readCliVersion(signal), signal);
29391
+ if (signal?.aborted) throw new Error("aborted");
29392
+ await raceAbort2(
29393
+ modelCatalog.refreshModelCatalog({
29394
+ force: false,
29395
+ cliVersion: version2,
29396
+ ...signal === void 0 ? {} : { signal }
29397
+ }),
29398
+ signal
29399
+ );
29400
+ if (signal?.aborted) throw new Error("aborted");
29401
+ const [outcome, skills] = await raceAbort2(
29402
+ Promise.all([
29403
+ probeGrokModels({
29404
+ recipe,
29405
+ host,
29406
+ env,
29407
+ ...signal === void 0 ? {} : { signal }
29408
+ }),
29409
+ probeGrokSkills({
29410
+ recipe,
29411
+ host,
29412
+ env,
29413
+ ...signal === void 0 ? {} : { signal }
29414
+ })
29415
+ ]),
29416
+ signal
29030
29417
  );
29031
- const [outcome, version2, skills] = await Promise.all([
29032
- probeGrokModels({ recipe, host, env }),
29033
- probeGrokVersion({ recipe, host, env }),
29034
- probeGrokSkills({ recipe, host, env })
29035
- ]);
29036
29418
  if (outcome.kind !== "answered") {
29037
29419
  return {
29038
29420
  installed: outcome.kind === "failed",
@@ -29057,21 +29439,18 @@ var createGrokInstance = (config2, host) => {
29057
29439
  };
29058
29440
  const instance = {
29059
29441
  probe,
29442
+ readCliVersion: () => modelCatalog.readCliVersion(),
29443
+ refreshModelCatalog: ({ force }) => modelCatalog.refreshModelCatalog({ force, armProbeSkip: force }),
29060
29444
  // 项目里的技能:探测在 home 下跑 inspect 只看得到整机的;这里在项目目录里再跑一次。
29061
29445
  workspaceInventory: ({ cwd }) => {
29062
29446
  if (recipe.binaryPath.length === 0) {
29063
29447
  return Promise.reject(new Error("Grok program path is not configured"));
29064
29448
  }
29065
- const configuredHome = recipe.homePath;
29066
29449
  return inspectGrokWorkspace({
29067
29450
  recipe,
29068
29451
  host,
29069
29452
  cwd,
29070
- env: buildSpawnEnv(
29071
- recipe,
29072
- resolveGrokHomePath(configuredHome),
29073
- configuredHome.trim().length > 0
29074
- )
29453
+ env: spawnEnv()
29075
29454
  });
29076
29455
  },
29077
29456
  // 有意不处理 input.readOnlyDirectories:grok 的 read_file 在每一档都不弹审批、
@@ -29090,12 +29469,7 @@ var createGrokInstance = (config2, host) => {
29090
29469
  if (disposed) throw new Error("instance disposed");
29091
29470
  throwIfStartAborted(signal);
29092
29471
  }
29093
- const configuredHome = recipe.homePath;
29094
- const env = buildSpawnEnv(
29095
- recipe,
29096
- resolveGrokHomePath(configuredHome),
29097
- configuredHome.trim().length > 0
29098
- );
29472
+ const env = spawnEnv();
29099
29473
  const effort = modelSelectionStringOption(input2.modelSelection, GROK_REASONING_OPTION_ID);
29100
29474
  const baseArgs = tokenizeLaunchArgs(recipe.launchArgs);
29101
29475
  const insertAt = baseArgs.length <= 1 ? baseArgs.length : baseArgs.length - 1;
@@ -29185,6 +29559,7 @@ var createGrokInstance = (config2, host) => {
29185
29559
  }
29186
29560
  },
29187
29561
  ...requestedModelId === void 0 ? {} : { requestedModelId },
29562
+ refreshCatalogForSend: () => modelCatalog.refreshForSend(),
29188
29563
  ...input2.bindingGeneration === void 0 ? {} : { originBindingGeneration: input2.bindingGeneration },
29189
29564
  // 内置编排那一条排第一,后面跟用户在插件中心配的;缺了这一栏,
29190
29565
  // `mcp__ycodium__*` 整套工具(浏览器等)在这只手的会话里都不存在。
@@ -29401,7 +29776,7 @@ var executors = [
29401
29776
  ];
29402
29777
  var index_default = definePlugin({
29403
29778
  id: "agent-grok",
29404
- apiVersion: 30,
29779
+ apiVersion: 31,
29405
29780
  executors,
29406
29781
  activate(ctx) {
29407
29782
  ctx.log.info("grok executor ready");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ycodium-ai/agent-grok",
3
- "version": "0.2.3254",
3
+ "version": "0.2.3265",
4
4
  "description": "Grok executor: grok CLI over Agent Client Protocol stdio.",
5
5
  "keywords": [
6
6
  "ycodium",
@@ -21,9 +21,9 @@
21
21
  "@types/node": "24.12.4",
22
22
  "esbuild": "0.28.1",
23
23
  "vite-plus": "0.2.2",
24
- "@ycodium-ai/plugin-api": "0.2.3254",
25
- "ycodium-executor-testing": "0.0.0",
26
- "ycodium-acp-executor": "0.2.3254"
24
+ "@ycodium-ai/plugin-api": "0.2.3265",
25
+ "ycodium-acp-executor": "0.2.3265",
26
+ "ycodium-executor-testing": "0.0.0"
27
27
  },
28
28
  "ycodium": {
29
29
  "plugin": "./dist/index.js",